/* Argv-to-flags parsing for kernel commands: the legacy global-heuristic parser (parseKernelArgv) and the RFC-0260 schema-driven parser (resolveCommandFlags) that resolves flags strictly against a command's declared (+ universal) flag schema. Do not introduce app-specific runtime composition or deployment behavior into this reusable package source file. The legacy global-heuristic parser is preserved for compatibility — new parsing goes through flag specs. RFC-1097: step 6 — compass.migrate codemod run Mechanical v1 to v2 header migration across the workspace: 942 files rewritten — CHANGE_SUMMARY windows collapsed into history, forbidden v1 blocks stripped, KEY_DECISIONS seeded from @ai-invariant comments (5 files) or TODO placeholders (103 files), blocks reordered to canonical order. RFC-1097: sweep — werkstatt-engine clean Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics. Repeated kind-"string" flags now emit KERNEL-FLAG-02 instead of silently promoting to an array that crashes `as string` handler reads (the rfc.validate --id TypeError class); inline values containing "=" are preserved via indexOf split instead of split("=", 2) truncation. Session-retro 2026-10-08: kind-"string[]" flags greedily consume consecutive bare tokens until the next --flag, so `--files a b` resolves to ["a","b"] instead of KERNEL-ARG-01 on `b`; a bare positional not preceded by a string[] flag still diagnoses. RFC-1306: kernelArgvFromFlags — serializes a delegated flag record into argv so ctx.invoke children pass through normal schema resolution (KERNEL-FLAG-02 on missing values). RFC-0303 */ import type { Diagnostic, KernelCommandDefinition, KernelFlagSpec, KernelFlagValue, } from "@warpgogol/werkstatt-shared/kernel"; const _VALUE_FLAG_NAMES = new Set(["root", "package", "history"]); /** * RFC-1306: inverse of the flag parsers — serializes a flags record into the * argv token form `resolveCommandFlags` accepts. Used by the `ctx.invoke` * port to delegate to a sibling command by name. `true` → bare `--name`, * `false` → `--name=false`, strings → `--name=value`, arrays → one * `--name=value` per element (addFlagValue accumulates repeats). */ export function kernelArgvFromFlags(flags: Record): string[] { const argv: string[] = []; for (const [name, value] of Object.entries(flags)) { if (value === undefined) continue; if (value === true) { argv.push(`--${name}`); } else if (Array.isArray(value)) { for (const item of value) argv.push(`--${name}=${item}`); } else { argv.push(`--${name}=${value}`); } } return argv; } function addFlagValue( target: Record, key: string, value: string | boolean, ) { const existing = target[key]; if (existing === undefined) { target[key] = value; return; } if (Array.isArray(existing)) { if (typeof value === "string") { target[key] = [...existing, value]; } return; } if (typeof existing === "string") { if (typeof value === "string") { target[key] = [existing, value]; return; } target[key] = existing; return; } target[key] = value; } /** * @deprecated RFC-0260: this global heuristic set only governs the legacy parse * path used by commands that do NOT declare a `flags` schema. Schema-carrying * commands are parsed by {@link resolveCommandFlags} instead, which decides * boolean-ness from the command's own (and the universal) flag specs. Once * every command has migrated to a `flags` schema, this set and the heuristic * path in {@link parseKernelArgv} are deleted (RFC-0260 rollout step 4). * * Flags that are always boolean switches. A boolean flag never consumes the following token as its * value, so `--dry-run alpha` parses as the boolean `dry-run` plus the positional `alpha` (rather * than greedily swallowing `alpha`). Value flags (`--site x`, `--root y`, `--tag z`) are NOT listed * and keep consuming the next bare token. The set is the union of every flag the kernel commands * read as a boolean (`flags.x === true`) plus the universal CLI switches. */ const KERNEL_BOOLEAN_FLAGS = new Set([ "all", "all-apps", "approve-all", "dry-run", "force", "help", "inplace", "json", "mini", "packages", "page", "prod", "quiet", "regen", "regenerate", "report-only", "show-versions", "strict", "verbose", "version", "write", ]); /** Universal flag aliases resolved before schema lookup (--output → --json). */ const KERNEL_FLAG_ALIASES: Record = { output: "json", }; export function parseKernelArgv(argv: string[]): { argv: string[]; flags: Record; diagnostics: Diagnostic[]; } { const flags: Record = {}; const diagnostics: Diagnostic[] = []; let passthrough = false; for (let index = 0; index < argv.length; index += 1) { const entry = argv[index]; if (passthrough) { diagnostics.push({ ruleId: "KERNEL-ARG-01", severity: "error", message: `Unexpected positional argument "${entry}". All arguments must be passed as flags.`, fixHint: `Convert "${entry}" to a flag, e.g. --id ${entry}.`, }); continue; } if (entry === "--") { passthrough = true; continue; } if (!entry.startsWith("--")) { diagnostics.push({ ruleId: "KERNEL-ARG-01", severity: "error", message: `Unexpected positional argument "${entry}". All arguments must be passed as flags.`, fixHint: `Convert "${entry}" to a flag, e.g. --id ${entry}.`, }); continue; } const withoutPrefix = entry.slice(2); const eqIndex = withoutPrefix.indexOf("="); const rawFlagName = eqIndex === -1 ? withoutPrefix : withoutPrefix.slice(0, eqIndex); const inlineValue = eqIndex === -1 ? undefined : withoutPrefix.slice(eqIndex + 1); // Aliases apply to bare switches only — `--output=json|true|1` is // normalized by the CLI token scan; `--output=` stays unknown. const flagName = inlineValue === undefined ? (KERNEL_FLAG_ALIASES[rawFlagName] ?? rawFlagName) : rawFlagName; if (!flagName) { continue; } if (inlineValue !== undefined) { addFlagValue(flags, flagName, inlineValue); continue; } // Boolean switches never consume the following token; it stays a positional arg. if (KERNEL_BOOLEAN_FLAGS.has(flagName)) { addFlagValue(flags, flagName, true); continue; } const next = argv[index + 1]; if (typeof next === "string" && !next.startsWith("--")) { addFlagValue(flags, flagName, next); index += 1; continue; } addFlagValue(flags, flagName, true); } return { argv: [...argv], flags, diagnostics, }; } // --------------------------------------------------------------------------- // RFC-0260: typed kernel command flag schemas // --------------------------------------------------------------------------- /** * Flags every kernel command accepts regardless of its own declared schema. * Merged into every schema-carrying command before flags are resolved, so an * individual command's `flags` map only needs to declare what is specific to * it (RFC-0260). */ export const KERNEL_UNIVERSAL_FLAGS: Record = { site: { kind: "string", description: "Target a specific site by name (RFC-0378)." }, all: { kind: "boolean", description: "Run against every discovered app." }, json: { kind: "boolean", description: "Emit machine-readable JSON instead of pretty output." }, quiet: { kind: "boolean", description: "Suppress non-essential log output." }, verbose: { kind: "boolean", description: "Emit additional diagnostic log output." }, root: { kind: "string", description: "Override the resolved workspace root." }, "dry-run": { kind: "boolean", description: "Report intended mutations without writing them." }, force: { kind: "boolean", description: "Bypass a normally-blocking safety check." }, help: { kind: "boolean", description: "Print command help instead of executing." }, }; /** * RFC-0260: resolve raw argv against a command's declared flag schema * (merged with {@link KERNEL_UNIVERSAL_FLAGS}). Unlike {@link parseKernelArgv}, * boolean-ness is decided per-flag from the schema instead of a global * heuristic set, so an unknown flag is a hard error (KERNEL-FLAG-01) instead * of being silently accepted as `true`. * * Passthrough semantics are unchanged: tokens after a bare `--` are never * flag-interpreted and produce KERNEL-ARG-01 diagnostics per RFC-0609. */ export function resolveCommandFlags( rawArgv: string[], definition: KernelCommandDefinition, ): { flags: Record; diagnostics: Diagnostic[] } { const schema: Record = { ...KERNEL_UNIVERSAL_FLAGS, ...(definition.flags ?? {}), }; const validFlagNames = Object.keys(schema).sort(); const flags: Record = {}; const diagnostics: Diagnostic[] = []; let passthrough = false; for (let index = 0; index < rawArgv.length; index += 1) { const entry = rawArgv[index]; if (passthrough) { diagnostics.push({ ruleId: "KERNEL-ARG-01", severity: "error", message: `Unexpected positional argument "${entry}" for command "${definition.name}". All arguments must be passed as flags.`, fixHint: `Convert "${entry}" to a flag, e.g. --id ${entry}.`, }); continue; } if (entry === "--") { passthrough = true; continue; } if (!entry.startsWith("--")) { diagnostics.push({ ruleId: "KERNEL-ARG-01", severity: "error", message: `Unexpected positional argument "${entry}" for command "${definition.name}". All arguments must be passed as flags.`, fixHint: `Convert "${entry}" to a flag, e.g. --id ${entry}.`, }); continue; } const withoutPrefix = entry.slice(2); const eqIndex = withoutPrefix.indexOf("="); const rawFlagName = eqIndex === -1 ? withoutPrefix : withoutPrefix.slice(0, eqIndex); const inlineValue = eqIndex === -1 ? undefined : withoutPrefix.slice(eqIndex + 1); // Aliases apply to bare switches only — `--output=json|true|1` is // normalized by the CLI token scan; `--output=` stays unknown. const flagName = inlineValue === undefined ? (KERNEL_FLAG_ALIASES[rawFlagName] ?? rawFlagName) : rawFlagName; if (!flagName) continue; const spec = schema[flagName]; if (!spec) { diagnostics.push({ ruleId: "KERNEL-FLAG-01", severity: "error", message: `Unknown flag "--${flagName}" for command "${definition.name}". Valid flags: ${validFlagNames.join(", ")}`, fixHint: `Remove --${flagName}, fix the typo, or add it to the flags schema for ${definition.name}.`, }); continue; } if (inlineValue !== undefined) { addFlagValue( flags, flagName, spec.kind === "boolean" ? inlineValue !== "false" : inlineValue, ); continue; } if (spec.kind === "boolean") { addFlagValue(flags, flagName, true); continue; } // string[] flags consume every following bare token — positional args are // never valid (KERNEL-ARG-01), so `--files a b` unambiguously means the // value list [a, b]. Bare `--` still switches to passthrough. if (spec.kind === "string[]") { let consumed = 0; while (index + 1 < rawArgv.length && !rawArgv[index + 1]!.startsWith("--")) { addFlagValue(flags, flagName, rawArgv[index + 1]!); index += 1; consumed += 1; } if (consumed > 0) continue; // fall through to the missing-value diagnostic } else { const next = rawArgv[index + 1]; if (typeof next === "string" && !next.startsWith("--")) { addFlagValue(flags, flagName, next); index += 1; continue; } } diagnostics.push({ ruleId: "KERNEL-FLAG-02", severity: "error", message: `Flag "--${flagName}" for command "${definition.name}" requires a value but none was given.`, fixHint: `Pass --${flagName} or --${flagName}=.`, }); } for (const [flagName, spec] of Object.entries(schema)) { if (spec.required && flags[flagName] === undefined) { diagnostics.push({ ruleId: "KERNEL-FLAG-03", severity: "error", message: `Required flag "--${flagName}" is missing for command "${definition.name}".`, fixHint: `Pass --${flagName} .`, }); } else if (flags[flagName] === undefined && spec.default !== undefined) { flags[flagName] = spec.default; } else if (spec.kind === "string" && Array.isArray(flags[flagName])) { diagnostics.push({ ruleId: "KERNEL-FLAG-02", severity: "error", message: `Flag "--${flagName}" for command "${definition.name}" accepts a single value but was given multiple.`, fixHint: `Pass --${flagName} once, or declare the flag as kind "string[]" if multiple values are intended.`, }); } } return { flags, diagnostics }; }