/*
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 };
}