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: "jobbank-cli", version: "1.0.0", description: "CLI for Akademikernes Jobbank (jobbank.dk) — job search for highly educated candidates", }) 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 }).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}`) } } } await cli.run()