fix: restore Danish CLI commands/ and tsconfig files dropped by old .gitignore (#53)

The pre-#21 .gitignore's unanchored 'commands/' rule silently excluded
.agents/skills/*/cli/src/commands/ (and the tsconfigs) from the initial
release, so every clone's four Danish portal CLIs failed on import with
'Cannot find module ./commands/search.js'. #21 fixed the rule but the
files were never restored - git history has no trace of them.

Restored from the maintainer's working copies, including the updated
jobindex helpers.ts (Jobindex moved search results from the JSON
endpoint, which now returns 204, into an embedded HTML Stash blob).

Verified: all four CLIs typecheck and return live results with their
documented flags. Surfaced while reviewing #52.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mads Lorentzen
2026-07-07 17:39:47 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent fce2cf23c0
commit f3d4448cca
18 changed files with 2085 additions and 0 deletions
@@ -0,0 +1,101 @@
import { defineCommand, option } from "@bunli/core"
import { z } from "zod"
import { apiFetch, writeError } from "../helpers.js"
interface LocationItem {
id: string
text: string
value: string
category: string
slug: string
}
interface LocationGroup {
title: string
items: LocationItem[]
}
export const locations = defineCommand({
name: "locations",
description: "Suggest municipalities, zip codes, and regions for a query",
options: {
query: option(z.string().optional(), {
description: "Location text to search (city, zip code, region) (required)",
}),
limit: option(z.coerce.number().optional(), {
description: "Cap total suggestions returned",
}),
format: option(z.enum(["json", "table", "plain"]).default("json"), {
description: "Output format: json, table, plain",
}),
},
handler: async ({ flags, signal }) => {
if (signal.aborted) return
if (!flags.query) {
writeError("--query is required", "MISSING_REQUIRED")
process.exit(1)
}
try {
const raw = await apiFetch<LocationGroup[]>("/api/search/locations", {
q: flags.query,
})
if (signal.aborted) return
// Filter out groups with no items
const filtered = raw.filter((g) => g.items && g.items.length > 0)
let result = filtered
if (flags.limit !== undefined) {
// Apply limit across all groups
let remaining = flags.limit
result = []
for (const group of filtered) {
if (remaining <= 0) break
const items = group.items.slice(0, remaining)
remaining -= items.length
if (items.length > 0) {
result.push({ title: group.title, items })
}
}
}
if (flags.format === "json") {
console.log(JSON.stringify(result, null, 2))
} else if (flags.format === "table") {
outputTable(result)
} else {
outputPlain(result)
}
} catch (err) {
writeError(err instanceof Error ? err.message : String(err), "API_ERROR")
process.exit(1)
}
},
})
function outputTable(data: LocationGroup[]): void {
console.log("category id text value slug")
for (const group of data) {
for (const item of group.items) {
const cat = item.category.padEnd(12)
const id = item.id.substring(0, 22).padEnd(22)
const text = item.text.substring(0, 28).padEnd(28)
const value = String(item.value).padEnd(11)
const slug = item.slug
console.log(`${cat} ${id} ${text} ${value} ${slug}`)
}
}
}
function outputPlain(data: LocationGroup[]): void {
for (const group of data) {
console.log(`=== ${group.title} ===`)
for (const item of group.items) {
console.log(` ${item.text} (${item.category}, value=${item.value}, slug=${item.slug})`)
}
}
}