/* shared CLI flag parsing — common flag consumption, help/json token detection, and usage text for the werkstatt CLI surface. Do not execute commands or touch the registry — pure argv parsing only. Do not parse command-declared flags — that is resolveCommandFlags' job in runtime/argv.ts. consumeCommonFlags is the single parser for CLI-envelope flags; batch entries re-parse through it so per-entry flags override globals. RFC-1309: extracted from cli/index.ts so batch.ts and unit tests can import the parser without invoking main(); added --quiet, --batch, --stop-on-fail. */ // @ai-invariant flag tokenizers stay pure — no I/O, no registry access; all effects flow through the caller export interface CommonFlags { siteName: string | undefined; allSites: boolean; dryRun: boolean; force: boolean; noRegistryCache: boolean; concurrency: number | undefined; collectErrors: boolean; quiet: boolean; batch: string | undefined; stopOnFail: boolean; outputFormat: "pretty" | "json"; remaining: string[]; } /** * CLI-envelope flag names consumed before `run` dispatches — the union of * `consumeCommonFlags`, `isJsonFlagToken`, and `argvRequestsHelp` surfaces. * These tokens never reach `resolveCommandFlags`, so a documented * `werkstatt run --flag` carrying one is valid even when the command's * own schema omits it. command.docs.lint (RFC-1313) derives its exemption * set from this list + KERNEL_UNIVERSAL_FLAGS — never a hardcoded copy. * Keep in sync with the parsers below (drift-guarded in index.test.ts). */ export const COMMON_CLI_FLAG_NAMES = [ "site", "all", "dry-run", "force", "no-registry-cache", "concurrency", "collect-errors", "quiet", "batch", "stop-on-fail", "json", "output", "help", "h", ] as const; export function consumeCommonFlags(argv: string[]): CommonFlags { const remaining: string[] = []; let siteName: string | undefined; let allSites = false; let dryRun = false; let force = false; let noRegistryCache = false; let concurrency: number | undefined; let collectErrors = false; let quiet = false; let batch: string | undefined; let stopOnFail = false; let outputFormat: "pretty" | "json" = "pretty"; for (let index = 0; index < argv.length; index += 1) { const entry = argv[index]; if (entry === "--site") { siteName = argv[index + 1]; index += 1; continue; } if (entry.startsWith("--site=")) { siteName = entry.slice("--site=".length); continue; } if (entry === "--all" || entry === "--all=true" || entry === "--all=1") { allSites = true; continue; } if (entry === "--dry-run" || entry === "--dry-run=true" || entry === "--dry-run=1") { dryRun = true; continue; } if (entry === "--force" || entry === "--force=true" || entry === "--force=1") { force = true; continue; } if ( entry === "--no-registry-cache" || entry === "--no-registry-cache=true" || entry === "--no-registry-cache=1" ) { noRegistryCache = true; continue; } if (isJsonFlagToken(entry)) { outputFormat = "json"; continue; } if ( entry === "--collect-errors" || entry === "--collect-errors=true" || entry === "--collect-errors=1" ) { collectErrors = true; continue; } if (entry === "--concurrency") { const value = argv[index + 1]; const parsed = Number.parseInt(value ?? "", 10); if (Number.isFinite(parsed) && parsed > 0) { concurrency = parsed; index += 1; } continue; } if (entry.startsWith("--concurrency=")) { const parsed = Number.parseInt(entry.slice("--concurrency=".length), 10); if (Number.isFinite(parsed) && parsed > 0) { concurrency = parsed; } continue; } if (entry === "--quiet" || entry === "--quiet=true" || entry === "--quiet=1") { quiet = true; continue; } if (entry === "--batch") { batch = argv[index + 1]; index += 1; continue; } if (entry.startsWith("--batch=")) { batch = entry.slice("--batch=".length); continue; } if ( entry === "--stop-on-fail" || entry === "--stop-on-fail=true" || entry === "--stop-on-fail=1" ) { stopOnFail = true; continue; } remaining.push(entry); } return { siteName, allSites, dryRun, force, noRegistryCache, concurrency, collectErrors, quiet, batch, stopOnFail, outputFormat, remaining, }; } export function printUsage() { console.log("werkstatt sites list [--json]"); console.log( "werkstatt run [--site ] [--all] [--dry-run] [--no-registry-cache] [--quiet] [--json|--output] [-- ...args]", ); console.log( "werkstatt run --batch [--stop-on-fail] [--site ] [--all] [--dry-run] [--quiet] [--json]", ); console.log( "werkstatt pipeline [--site ] [--all] [--dry-run] [--force] [--no-registry-cache] [--concurrency N] [--collect-errors] [--quiet] [--json]", ); } /** * RFC-0326: print a `[Modified N file(s): ...] Re-read before editing.` line * to stdout in pretty mode when the report has a non-empty filesModified array. * In JSON mode, filesModified is already a top-level field on the report — no * extra printing needed. */ export function printFilesModified( report: { filesModified?: string[] }, outputFormat: "pretty" | "json", ): void { if (outputFormat === "json") return; const files = report.filesModified; if (!files || files.length === 0) return; console.log(`[Modified ${files.length} file(s): ${files.join(", ")}] Re-read before editing.`); } export function isJsonFlagToken(entry: string): boolean { return ( entry === "--json" || entry === "--json=true" || entry === "--json=1" || entry === "--output" || entry === "--output=json" || entry === "--output=true" || entry === "--output=1" ); } export function argvRequestsJson(argv: string[]): boolean { return argv.some(isJsonFlagToken); } export function argvRequestsHelp(argv: string[]): boolean { return argv.some((entry) => entry === "--help" || entry === "-h"); }