type Notify = (message: string, level: "info" | "warning") => void; const FALLBACK_DEFAULT_COMMAND = ["health"]; const INVALID_DEFAULT_COMMANDS = new Set(["run", "rerun", "about", "version", "update"]); export function resolveFallowRunArgs(rawArgs: string[], configuredDefaultArgs: string[]): string[] { if (isExplicitFallowCommand(rawArgs)) return rawArgs; const defaultArgs = resolveDefaultArgs(configuredDefaultArgs); validateDefaultCommand(defaultArgs); return [...defaultArgs, ...runOverrides(rawArgs)]; } function isExplicitFallowCommand(args: string[]): boolean { return args.length > 0 && args[0] !== "run"; } function resolveDefaultArgs(configured: string[]): string[] { return configured.length ? configured : FALLBACK_DEFAULT_COMMAND; } function runOverrides(args: string[]): string[] { return args[0] === "run" ? args.slice(1) : []; } function validateDefaultCommand(args: string[]): void { if (!args[0] || INVALID_DEFAULT_COMMANDS.has(args[0])) { throw new Error("PI_FALLOW_DEFAULT_COMMAND must start with an executable Fallow command such as health or dead-code."); } } export function needsFallowBaseDetection(rawArgs: string[]): boolean { if (rawArgs[0] !== "pr") return false; const prArgs = rawArgs.slice(1); if (prArgs.some((arg) => arg === "--help" || arg === "-h")) return false; return !hasFlag(prArgs, "--base"); } export function normalizeFallowArgs( rawArgs: string[], baseRef: string, lastFallowArgs: string[] | null, notify: Notify, ): string[] | null { const firstArg = rawArgs[0]; if (firstArg === "rerun") return buildRerunFallowArgs(rawArgs, lastFallowArgs, notify); if (firstArg === "pr") return buildPrFallowArgs(rawArgs.slice(1), baseRef); return resolveFallbackArgs(rawArgs); } function buildRerunFallowArgs( rawArgs: string[], lastFallowArgs: string[] | null, notify: Notify, ): string[] | null { if (!lastFallowArgs) { notify("No previous /fallow command to rerun.", "warning"); return null; } if (rawArgs.length > 1) notify("/fallow rerun uses the last command and ignores extra arguments.", "info"); return [...lastFallowArgs]; } function buildPrFallowArgs(prArgs: string[], baseRef: string): string[] { const skipDefaults = prArgs.some((arg) => arg === "--help" || arg === "-h"); const fallbackArgs = skipDefaults ? prArgs : withBaseAndGateFallback(prArgs, baseRef); return ["audit", ...fallbackArgs]; } function withBaseAndGateFallback(args: string[], baseRef: string): string[] { const normalized = [...args]; if (!hasFlag(normalized, "--base")) normalized.push("--base", baseRef); if (!hasFlag(normalized, "--gate")) normalized.push("--gate", "new-only"); return normalized; } function hasFlag(args: string[], flag: string): boolean { for (const arg of args) { if (arg === flag || arg.startsWith(`${flag}=`)) return true; } return false; } function resolveFallbackArgs(rawArgs: string[]): string[] { const normalized = [...rawArgs]; validateRequiredCommandArgs(normalized); const command = normalized[0] ?? ""; const alias = commandAliasMap[command]; if (alias) return alias(normalized); const translator = traceCommandMap[command]; return translator ? translator(normalized) : normalized; } function validateRequiredCommandArgs(args: string[]): void { if (args[0] !== "explain") return; if (args.some((arg) => arg === "--help" || arg === "-h")) return; if (args.slice(1).some((arg) => !arg.startsWith("-"))) return; throw new Error("explain requires at least one issue type, for example: /fallow explain unused-export"); } const commandAliasMap: Record string[]> = { all: (args) => args.slice(1), "check-changed": buildCheckChangedFallowArgs, "project-info": (args) => ["list", ...args.slice(1)], "list-boundaries": (args) => ["list", "--boundaries", ...args.slice(1)], "fix-preview": (args) => ["fix", "--dry-run", ...args.slice(1)], "fix-apply": (args) => ["fix", "--yes", ...args.slice(1)], "coverage-analyze": (args) => ["coverage", "analyze", ...args.slice(1)], }; function buildCheckChangedFallowArgs(args: string[]): string[] { const changedArgs = args.slice(1); const wantsHelp = changedArgs.some((arg) => arg === "--help" || arg === "-h"); if (!wantsHelp && !hasFlag(changedArgs, "--changed-since") && !hasFlag(changedArgs, "--base")) { throw new Error("check-changed requires --changed-since or --base."); } return changedArgs; } const traceCommandMap: Record string[]> = { "trace-file": (args) => { if (!args[1]) throw new Error("trace-file requires file."); return ["dead-code", "--trace-file", ...args.slice(1)]; }, "trace-export": (args) => { if (!args[1] || !args[2]) throw new Error("trace-export requires file and exportName."); return ["dead-code", "--trace", `${args[1]}:${args[2]}`, ...args.slice(3)]; }, "trace-dependency": (args) => { if (!args[1]) throw new Error("trace-dependency requires packageName."); return ["dead-code", "--trace-dependency", ...args.slice(1)]; }, "trace-clone": (args) => parseTraceCloneArgs(args), }; function parseTraceCloneArgs(args: string[]): string[] { const { fileOrPath, line } = getTraceCloneInput(args); if (line) return buildTraceCloneFromLine(fileOrPath, line, args); const parsed = parseTraceCloneFromPath(fileOrPath); return buildTraceCloneFromParsed(parsed, args); } function getTraceCloneInput(args: string[]): { fileOrPath: string; line?: string } { if (!args[1]) throw new Error("trace-clone requires file and line."); return { fileOrPath: args[1], line: args[2] }; } function parseTraceCloneFromPath(fileOrPath: string): RegExpMatchArray { const match = /^(.*):(\d+)$/.exec(fileOrPath); if (!match) throw new Error("trace-clone requires file and line."); return match; } function buildTraceCloneFromLine(fileOrPath: string, line: string, args: string[]): string[] { if (!/^\d+$/.test(line)) throw new Error("trace-clone requires file and numeric line."); return ["dupes", "--trace", `${fileOrPath}:${line}`, ...args.slice(3)]; } function buildTraceCloneFromParsed(match: RegExpMatchArray, args: string[]): string[] { return ["dupes", "--trace", `${match[1]}:${match[2]}`, ...args.slice(2)]; }