/** * Argv precondition shared by the CLIs — the companion to `require-target-dir`. * * Every CLI here parses argv with a chain of `if (a === '--x') … else if …`. * Two things fall through that chain silently: * * 1. **An unknown flag.** With no `else` branch, `--zzz` leaves no trace: the * CLI exits 0 with a full payload — a confident answer to a question the * caller did not ask. Measured live (#51): `knowledge-refresh`'s only knob * reached the CLI malformed, was ignored, and the command reported "all 14 * entries re-verified within the last 90 days" about a threshold the user * had just overridden. * * 2. **A value-taking flag whose value is another flag.** The guard * `a === '--output-file' && args[i + 1]` asks only whether a next token * exists, never whether it is a *value*. Measured live (#57): * `manifest --output-file --json` wrote a file literally named `--json` * into the caller's working directory, exit 0, with `--json` mode silently * dropped. A wrong answer is bad; an unintended file on disk is worse. * * This runs BEFORE the CLI's own loop and does not replace it. That is * deliberate: valid argv reaches the existing parser byte-for-byte unchanged, so * no frozen snapshot can move. Malformed argv never reaches it at all. * * By the exit-code contract, a malformed argument is exit **3** — the scanner * did not get to do its job — never 0/1/2, which are verdicts about a * configuration that WAS examined. */ /** * Find the first thing wrong with `args`. * * @param {string[]} args - argv slice (no node/script entries). * @param {{ boolean?: string[], value?: string[] }} spec - the CLI's flag surface. * @returns {string|null} diagnostic, or null when argv is well-formed. */ export function findArgError(args, spec) { const booleanFlags = new Set(spec.boolean || []); const valueFlags = new Set(spec.value || []); for (let i = 0; i < args.length; i++) { const a = args[i]; // Positionals and subcommands are the CLI's own business. if (!a.startsWith('-')) continue; if (booleanFlags.has(a)) continue; if (valueFlags.has(a)) { const next = args[i + 1]; if (next === undefined) { return `flag "${a}" needs a value, but nothing followed it`; } if (next.startsWith('-')) { return `flag "${a}" needs a value, but the next argument was another flag: "${next}"`; } i++; // consume the value so it is never re-examined as a positional continue; } return `unknown flag "${a}"`; } return null; } /** * Gate a CLI on well-formed argv. Writes the diagnostic and sets the exit code * itself, so callers stay a two-line guard: * * if (!requireValidArgs(args, ARG_SPEC)) return; * * Never throws, and never calls `process.exit()` — an abrupt exit discards * unflushed stdout when the CLI is on a pipe. * * @param {string[]} args - argv slice. * @param {{ boolean?: string[], value?: string[] }} spec - the CLI's flag surface. * @returns {boolean} true when the CLI may proceed. */ export function requireValidArgs(args, spec) { const error = findArgError(args, spec); if (error === null) return true; process.stderr.write(`Error: ${error}\n`); process.exitCode = 3; return false; }