/** * lib/cli-args.ts — argument parsing that ALWAYS answers with an envelope. * * Audit 2026-09-24 (C-WARN-11, B-WARN-09): an unknown flag made every CLI exit * 1 with an EMPTY stdout — `node:util.parseArgs` throws `ERR_PARSE_ARGS_*`, * commander writes its message to stderr and calls process.exit — so the * orchestrators parsed nothing and could not tell a typo from a crash. Two * wrappers, one contract: a parse error prints a `failGenerate` envelope * (`success:false`, `errors[]` naming the accepted options) on stdout and * exits 1; `--help` / `--version` keep exiting 0. * * - `parseArgsOrFail(config, command?)` — drop-in for `parseArgs` (same * return type; import it `as parseArgs` and the call sites stay as they are). * - `withEnvelopeExit(program, command)` — for commander programs: installs * `exitOverride` + `configureOutput` (commander's own message stays on stderr). * * The command name defaults to the CLI folder name (`…/cli//index.ts`). */ import { basename, dirname } from 'node:path' import { parseArgs, type ParseArgsConfig } from 'node:util' import { failGenerate, printEnvelope } from './output.js' /** `…/cli//index.ts` → ``. */ export function inferCommand(): string { const entry = process.argv[1] ?? '' const folder = basename(dirname(entry)) return folder && folder !== '.' ? folder : 'cli' } function acceptedOptions(config: ParseArgsConfig): string { const names = Object.keys(config.options ?? {}).map((o) => `--${o}`) return names.length > 0 ? ` Accepted options: ${names.join(', ')}.` : '' } /** * `node:util.parseArgs` that never dies silently: an unknown option, a missing * value or an unexpected positional prints a `success:false` envelope and exits 1. */ export function parseArgsOrFail(config: T, command: string = inferCommand()) { try { return parseArgs(config) } catch (err) { const code = (err as { code?: string }).code ?? '' if (!code.startsWith('ERR_PARSE_ARGS')) throw err printEnvelope(failGenerate(command, [`${(err as Error).message}${acceptedOptions(config)}`])) return process.exit(1) } } /** The two commander methods the wrapper needs — structural, so lib/ carries no commander import. */ export interface CommanderLike { exitOverride(callback?: (err: { code: string; exitCode: number; message: string }) => void): this configureOutput(config: { writeErr?: (str: string) => void; writeOut?: (str: string) => void }): this } /** * Commander program that never dies silently: an unknown option / missing * mandatory value prints a `success:false` envelope on stdout and exits 1 * (commander's message stays on stderr); help and version still exit 0. */ export function withEnvelopeExit

(program: P, command: string): P { return program .configureOutput({ writeErr: (str) => { process.stderr.write(str) } }) .exitOverride((err) => { if (err.exitCode === 0 || err.code === 'commander.helpDisplayed' || err.code === 'commander.help' || err.code === 'commander.version') { process.exit(0) } printEnvelope(failGenerate(command, [err.message.trim() || `commander: ${err.code}`])) process.exit(err.exitCode || 1) }) }