Files
ai-job-search/.agents/skills/jobindex-search/cli/src/cli.ts
T
Rudy Mizrahi Celekli 060f2dbe78 fix(portal-clis): reject missing Danish option values before dispatch (#573)
* fix(portal-clis): reject missing Danish option values before dispatch

The Bunli parser discards known bare options before schema validation, silently dropping filters or applying defaults. Extend the existing pre-dispatch guards across the four Danish portals while preserving built-in switches, valued and equals-form arguments, and existing unknown-flag errors.

Actual CLI subprocess regressions deny and record every fetch to demonstrate that missing filters reach request construction on master. Valued-option and help controls keep accepted command forms covered without contacting any portal.

* docs(changelog): record fix for #573
2026-10-18 07:02:41 +02:00

85 lines
3.5 KiB
TypeScript

import { createCLI } from "@bunli/core"
import { writeError } from "./helpers.js"
import { search } from "./commands/search.js"
import { detail } from "./commands/detail.js"
const cli = await createCLI({
name: "jobindex-cli",
version: "0.1.0",
description: "CLI for searching jobs on Jobindex.dk",
})
const commands = [search, detail]
for (const command of commands) {
cli.command(command)
}
// Reject unknown flags before dispatch. bunli silently discards them, and a
// silently discarded filter changes what the search returns without any error
// (a wrong flag name once returned an entire portal's database as if it
// matched the query). add-portal.md's contract requires a bogus flag to exit 1
// with a JSON error on stderr; this enforces it for the reference CLIs too.
//
// Both dash forms are checked. This loop inspected only `--long` tokens until
// #426, so an undefined short flag was discarded in silence: `-q "..."` on a
// portal whose keyword flag is `--search-string` returned the whole database
// as a successful, unfiltered search. Declared shorts and bunli's built-in
// -h/-v stay valid; every other single-dash token is rejected, including a
// negative number. bunli does not consume a `-`-prefixed token as the previous
// flag's value - it discards it - so `--radius -5` silently fell back to the
// default radius rather than failing its own `min(1)` schema. Erroring on it
// is the same trade linkedin-search already makes. A value that must begin
// with a dash uses the `--flag=value` form, which is checked as a long flag.
const argv = process.argv.slice(2)
const invoked = commands.find((c) => (c as { name?: string }).name === argv[0])
if (invoked) {
const options =
(invoked as { options?: Record<string, { short?: string } | undefined> }).options ?? {}
const known = new Set([...Object.keys(options), "help", "version"])
const knownShorts = new Set(
Object.values(options)
.map((o) => o?.short)
.filter((s): s is string => typeof s === "string")
.concat("h", "v"),
)
const rejectFlag = (rendered: string): never => {
writeError(
`unknown flag ${rendered} for '${argv[0]}' - flags are never silently ignored, because a discarded filter changes what the search returns; see --help for the supported flags`,
"UNKNOWN_FLAG",
)
process.exit(1)
}
for (const token of argv.slice(1)) {
if (token === "--") break
if (token.startsWith("--")) {
const flag = token.slice(2).split("=")[0]
if (!known.has(flag)) rejectFlag(`--${flag}`)
} else if (token.startsWith("-") && token !== "-") {
const flag = token.slice(1).split("=")[0]
if (!knownShorts.has(flag)) rejectFlag(`-${flag}`)
}
}
// Every declared portal option takes a value; only the built-in help and
// version flags are switches. Bunli drops a bare known option before schema
// validation, so an optional filter disappears or a default is used instead.
// Check after unknown flags so negative tokens keep their existing error.
for (let i = 1; i < argv.length; i++) {
const token = argv[i]
if (token === "--") break
if (!token.startsWith("-") || token.includes("=")) continue
const name = token.replace(/^-+/, "")
const isOption = token.startsWith("--")
? options[name] !== undefined
: Object.values(options).some((o) => o?.short === name)
if (!isOption) continue
const next = argv[i + 1]
if (next === undefined || next.startsWith("-")) {
writeError(`option ${token} requires a value`, "BAD_ARG")
process.exit(1)
}
}
}
await cli.run()