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,208 @@
import { defineCommand, option } from "@bunli/core"
import { z } from "zod"
import { apiFetch, writeError } from "../helpers.js"
interface SearchApiResponse {
jobAds: JobAdRaw[]
searchFacets: SearchFacetsRaw
totalJobAdCount: number
searchString: string | null
}
interface JobAdRaw {
jobAdId: string
title: string
hiringOrgName: string
occupation: string | null
municipality: string | null
postalCode: number | null
postalDistrictName: string | null
country: string
publicationDate: string
applicationDeadline: string | null
applicationDeadlineStatus: string | null
workHourPartTime: boolean
isExternal: boolean
hasLogo: boolean
logoUrl: string | null
cvr: string | null
workPlaceAddress: string
conceptUriDa?: string | null
isSeen: boolean
isFavorite: boolean
description?: string
}
interface SearchFacetsRaw {
regions: Array<{ type: string; jobAdCount: number }>
workHours: Array<{ type: string; jobAdCount: number }>
employmentDurations: Array<{ type: string; jobAdCount: number }>
occupationAreas: Array<{ identifier: string; jobAdCount: number }>
countries: Array<{ label: string; identifier: string; jobAdCount: number }>
}
export const search = defineCommand({
name: "search",
description: "Search for job ads with filters",
options: {
"search-string": option(z.string().optional(), {
description: "Free-text keyword search (job title, skills, employer)",
}),
page: option(z.coerce.number().default(1), {
description: "Page number (1-indexed)",
}),
"per-page": option(z.coerce.number().default(10), {
description: "Results per page",
}),
order: option(z.string().default("PublicationDate"), {
description: "Sort order: PublicationDate, BestMatch, ApplicationDate",
}),
region: option(z.string().optional(), {
description: "One region value",
}),
"work-hours": option(z.string().optional(), {
description: "FullTime or PartTime",
}),
duration: option(z.string().optional(), {
description: "Permanent or Temporary",
}),
"job-type": option(z.string().optional(), {
description: "Announcement type: Ordinaert, Efterloenner, Foertidspension",
}),
"postal-code": option(z.string().optional(), {
description: "Postal code for radius search",
}),
radius: option(z.coerce.number().default(50), {
description: "Radius in km from postal code",
}),
"occupation-area": option(z.string().optional(), {
description: "Occupation area identifier, e.g. 10000",
}),
"occupation-group": option(z.string().optional(), {
description: "Occupation group identifier, e.g. 10060",
}),
limit: option(z.coerce.number().optional(), {
description: "Cap total results returned by CLI",
}),
format: option(z.enum(["json", "table", "plain"]).default("json"), {
description: "Output format: json, table, plain",
}),
},
handler: async ({ flags, signal }) => {
if (signal.aborted) return
const params: Record<string, string> = {
resultsPerPage: String(flags["per-page"]),
pageNumber: String(flags.page),
orderType: flags.order,
}
if (flags["search-string"]) params["searchString"] = flags["search-string"]
if (flags.region) params["regions"] = flags.region
if (flags["work-hours"]) params["workHoursType"] = flags["work-hours"]
if (flags.duration) params["employmentDurationType"] = flags.duration
if (flags["job-type"]) params["jobAnnouncementType"] = flags["job-type"]
if (flags["postal-code"]) {
params["postalCode"] = flags["postal-code"]
params["kmRadius"] = String(flags.radius)
}
if (flags["occupation-area"]) params["occupationAreas"] = flags["occupation-area"]
if (flags["occupation-group"]) params["occupationGroups"] = flags["occupation-group"]
try {
const data = await apiFetch<SearchApiResponse>("/FindJob/Search", params)
if (signal.aborted) return
// Map raw job ads to documented output shape (omit description)
let results = data.jobAds.map((job) => ({
jobAdId: job.jobAdId,
title: job.title,
hiringOrgName: job.hiringOrgName,
occupation: job.occupation ?? null,
municipality: job.municipality ?? null,
postalCode: job.postalCode ?? null,
postalDistrictName: job.postalDistrictName ?? null,
country: job.country,
publicationDate: job.publicationDate,
applicationDeadline: job.applicationDeadline ?? null,
applicationDeadlineStatus: job.applicationDeadlineStatus ?? null,
workHourPartTime: job.workHourPartTime,
isExternal: job.isExternal,
hasLogo: job.hasLogo,
logoUrl: job.logoUrl ?? null,
cvr: job.cvr ?? null,
workPlaceAddress: job.workPlaceAddress ?? "",
isSeen: job.isSeen,
isFavorite: job.isFavorite,
}))
if (flags.limit !== undefined) {
results = results.slice(0, flags.limit)
}
const facets = {
regions: data.searchFacets.regions ?? [],
workHours: data.searchFacets.workHours ?? [],
employmentDurations: data.searchFacets.employmentDurations ?? [],
occupationAreas: data.searchFacets.occupationAreas ?? [],
countries: data.searchFacets.countries ?? [],
}
const meta = {
totalJobAdCount: data.totalJobAdCount,
pageNumber: flags.page,
resultsPerPage: flags["per-page"],
searchString: data.searchString ?? null,
}
const output = { meta, facets, results }
if (flags.format === "json") {
console.log(JSON.stringify(output, null, 2))
} else if (flags.format === "table") {
outputTable(results)
} else {
outputPlain(results)
}
} catch (err) {
writeError(err instanceof Error ? err.message : String(err), "API_ERROR")
process.exit(1)
}
},
})
type JobAdResult = {
jobAdId: string
title: string
hiringOrgName: string
occupation: string | null
municipality: string | null
postalCode: number | null
publicationDate: string
applicationDeadline: string | null
}
function outputTable(results: JobAdResult[]): void {
console.log("jobAdId title employer municipality")
for (const r of results) {
const id = r.jobAdId.padEnd(36)
const title = (r.title ?? "-").substring(0, 36).padEnd(36)
const employer = (r.hiringOrgName ?? "-").substring(0, 26).padEnd(26)
const municipality = String(r.municipality ?? "-")
console.log(`${id} ${title} ${employer} ${municipality}`)
}
}
function outputPlain(results: JobAdResult[]): void {
for (const r of results) {
console.log(`id: ${r.jobAdId}`)
console.log(`title: ${r.title}`)
console.log(`employer: ${r.hiringOrgName}`)
console.log(`occupation: ${r.occupation ?? "-"}`)
console.log(`municipality: ${r.municipality ?? "-"}`)
console.log(`published: ${r.publicationDate}`)
console.log(`deadline: ${r.applicationDeadline ?? "-"}`)
console.log("")
}
}