/** * Minimal CLI framework — drop-in replacement for the subset of @oclif/core * actually used by the coding agent. Provides `Command`, `Args`, `Flags`, * and a `run()` entry point with explicit command registration. * * Design goals: * - Zero dependencies beyond node builtins * - No filesystem scanning, no manifest files, no plugin loading * - Lazy command imports (only the invoked command is loaded) * - Typed `this.parse()` output matching oclif's API shape */ import * as fs from "node:fs"; import { parseArgs as nodeParseArgs } from "node:util"; /** * Streaming startup marker, enabled by `PI_DEBUG_STARTUP`. Local copy of * `logger.startupMarker` so the minimal `--version`/bootstrap import graph * stays free of the winston-backed logger module. Synchronous on purpose: * a command module whose import hangs (dlopen, fs on a dead mount) must * still leave its `:start` marker behind. */ function startupMarker(text: string): void { if (!process.env.PI_DEBUG_STARTUP) return; try { fs.writeSync(2, `[startup] ${text}\n`); } catch { // stderr unavailable; markers are best-effort } } /** * A user-facing argument/flag validation failure. Thrown by {@link Command.parse} * for missing/invalid positionals and flags. The top-level {@link run} handler * prints its message plus the command usage line to stderr and exits 1, instead * of letting it bubble to the process-level catch — which would dump a minified * `dist/cli.js` code frame over a plain argument mistake (issue #5369). */ export class CliUsageError extends Error { constructor(message: string) { super(message); this.name = "CliUsageError"; } } // --------------------------------------------------------------------------- // Flag & Arg descriptors // --------------------------------------------------------------------------- export interface FlagDescriptor { kind: K; description?: string; char?: string; default?: unknown; multiple?: boolean; options?: readonly string[]; required?: boolean; } export interface ArgDescriptor { kind: "string"; description?: string; required?: boolean; multiple?: boolean; options?: readonly string[]; } interface FlagInput { description?: string; char?: string; default?: unknown; multiple?: boolean; options?: readonly string[]; required?: boolean; } interface ArgInput { description?: string; required?: boolean; multiple?: boolean; options?: readonly string[]; } /** Builders that match the `Flags.*()` / `Args.*()` API from oclif. */ export const Flags = { string(opts?: T): FlagDescriptor<"string"> & T { return { kind: "string" as const, ...opts } as FlagDescriptor<"string"> & T; }, boolean(opts?: T): FlagDescriptor<"boolean"> & T { return { kind: "boolean" as const, ...opts } as FlagDescriptor<"boolean"> & T; }, integer(opts?: T): FlagDescriptor<"integer"> & T { return { kind: "integer" as const, ...opts } as FlagDescriptor<"integer"> & T; }, }; export const Args = { string(opts?: T): ArgDescriptor & T { return { kind: "string" as const, ...opts } as ArgDescriptor & T; }, }; // --------------------------------------------------------------------------- // Parse result types — mirrors oclif's typed output from this.parse() // --------------------------------------------------------------------------- type FlagValue = D["kind"] extends "boolean" ? D extends { default: boolean } ? boolean : boolean | undefined : D["kind"] extends "integer" ? D extends { default: number } ? number : number | undefined : D extends { multiple: true } ? string[] | undefined : string | undefined; type ArgValue = D extends { multiple: true } ? string[] | undefined : string | undefined; type FlagValues> = { [K in keyof T]: FlagValue }; type ArgValues> = { [K in keyof T]: ArgValue }; export interface ParseOutput< F extends Record = Record, A extends Record = Record, > { flags: FlagValues; args: ArgValues; argv: string[]; } // --------------------------------------------------------------------------- // Command base class // --------------------------------------------------------------------------- export interface CommandMetadata { description?: string; hidden?: boolean; flags?: Record; args?: Record; examples?: string[]; } export interface CommandCtor extends CommandMetadata { new (argv: string[], config: CliConfig): Command; strict?: boolean; aliases?: string[]; } /** Configuration passed to every command instance and help renderers. */ export interface CliConfig { bin: string; version: string; /** All registered commands keyed by their canonical name. */ commands: Map; } /** Minimal Command base matching the oclif surface we use. */ export abstract class Command { argv: string[]; config: CliConfig; constructor(argv: string[], config: CliConfig) { this.argv = argv; this.config = config; } abstract run(): Promise; /** * Parse argv against the static `flags` and `args` declared on the * concrete command class. Returns a typed `{ flags, args, argv }` object. */ async parse( _Cmd: C, ): Promise< ParseOutput< NonNullable extends Record ? NonNullable : Record, NonNullable extends Record ? NonNullable : Record > > { const Cmd = _Cmd as CommandCtor; const flagDefs = (Cmd.flags ?? {}) as Record; const argDefs = (Cmd.args ?? {}) as Record; const strict = Cmd.strict !== false; // Build node:util parseArgs options from flag descriptors const options: Record< string, { type: "string" | "boolean"; short?: string; multiple?: boolean; default?: string | boolean } > = {}; for (const [name, desc] of Object.entries(flagDefs)) { const opt: (typeof options)[string] = { type: desc.kind === "boolean" ? "boolean" : "string", }; if (desc.char) opt.short = desc.char; if (desc.multiple) opt.multiple = true; if (desc.default !== undefined) { opt.default = desc.kind === "boolean" ? Boolean(desc.default) : String(desc.default); } options[name] = opt; } // strict=false when command declares args (positionals must pass through) // or when the command itself opts out const { values: rawValues, positionals } = (() => { try { return nodeParseArgs({ args: this.argv, options, allowPositionals: true, strict, }); } catch (error) { throw new CliUsageError(error instanceof Error ? error.message : String(error)); } })(); // Convert raw values to proper types and validate const flags: Record = {}; for (const [name, desc] of Object.entries(flagDefs)) { const raw = rawValues[name]; if (desc.kind === "integer") { if (raw === undefined || typeof raw === "boolean") { flags[name] = desc.default ?? undefined; } else { const n = Number.parseInt(raw as string, 10); if (Number.isNaN(n)) { throw new CliUsageError(`Expected integer for --${name}, got "${raw}"`); } flags[name] = n; } } else if (desc.kind === "boolean") { flags[name] = raw !== undefined ? Boolean(raw) : desc.default !== undefined ? Boolean(desc.default) : undefined; } else { // string const val = raw !== undefined && typeof raw !== "boolean" ? raw : (desc.default ?? undefined); // Validate options constraint if (val !== undefined && desc.options && !Array.isArray(val)) { if (!desc.options.includes(val as string)) { throw new CliUsageError( `Expected --${name} to be one of: ${[...desc.options].join(", ")}; got "${val}"`, ); } } flags[name] = val; } // Validate required if (desc.required && flags[name] === undefined) { throw new CliUsageError(`Missing required flag: --${name}`); } } // Map positionals to named args in declaration order and validate const args: Record = {}; let posIdx = 0; for (const [argName, desc] of Object.entries(argDefs)) { if (desc.multiple) { const val = positionals.slice(posIdx); args[argName] = val.length > 0 ? val : undefined; posIdx = positionals.length; } else { const val = positionals[posIdx]; args[argName] = val; posIdx++; } // Validate required if (desc.required && args[argName] === undefined) { throw new CliUsageError(`Missing required argument: ${argName}`); } // Validate options constraint const argVal = args[argName]; if (argVal !== undefined && desc.options && typeof argVal === "string") { if (!desc.options.includes(argVal)) { throw new CliUsageError( `Expected ${argName} to be one of: ${[...desc.options].join(", ")}; got "${argVal}"`, ); } } } return { flags, args, argv: positionals } as never; } } // --------------------------------------------------------------------------- // Help rendering // --------------------------------------------------------------------------- /** Render full root help: header, default command details, subcommand list. */ export function renderRootHelp(config: CliConfig): void { const { bin, version, commands } = config; const lines: string[] = []; lines.push(`${bin} v${version}\n`); lines.push("USAGE"); lines.push(` $ ${bin} [COMMAND]\n`); // Show the default command's flags/args/examples inline. // The default command is the one marked hidden (it's the implicit entry point). const defaultCmd = [...commands.values()].find(command => command.hidden); if (defaultCmd) { renderCommandBody(lines, defaultCmd); } // List visible subcommands const visible = [...commands.entries()].filter(([, C]) => !C.hidden); if (visible.length > 0) { lines.push("COMMANDS"); const maxLen = Math.max(...visible.map(([n]) => n.length)); for (const [name, command] of visible.sort((a, b) => a[0].localeCompare(b[0]))) { lines.push(` ${name.padEnd(maxLen + 2)}${command.description ?? ""}`); } lines.push(""); } process.stdout.write(lines.join("\n")); } /** * Format a command's positional args for a USAGE line. Required args render * bare (`MODELS`), optional args wrapped in brackets (`[MODELS]`), and * `multiple` args get a trailing ellipsis (`MODELS...`) so a required * variadic reads as `MODELS...`, not the misleading optional `[MODELS]`. */ function formatUsageArgs(Cmd: CommandCtor): string { const entries = Object.entries(Cmd.args ?? {}); if (entries.length === 0) return ""; const parts = entries.map(([name, desc]) => { const label = `${name.toUpperCase()}${desc.multiple ? "..." : ""}`; return desc.required ? label : `[${label}]`; }); return ` ${parts.join(" ")}`; } /** Build the single USAGE line for a command (without the leading label). */ export function commandUsageLine(bin: string, id: string, Cmd: CommandCtor): string { const hasFlags = Object.keys(Cmd.flags ?? {}).length > 0; return `$ ${bin} ${id}${formatUsageArgs(Cmd)}${hasFlags ? " [FLAGS]" : ""}`; } /** Render help for a single command. */ export function renderCommandHelp(bin: string, id: string, Cmd: CommandCtor): void { const lines: string[] = []; if (Cmd.description) lines.push(`${Cmd.description}\n`); lines.push("USAGE"); lines.push(` ${commandUsageLine(bin, id, Cmd)}\n`); renderCommandBody(lines, Cmd); process.stdout.write(lines.join("\n")); } function renderCommandBody(lines: string[], command: CommandMetadata): void { const argDefs = command.args ?? {}; const flagDefs = command.flags ?? {}; // Arguments const argEntries = Object.entries(argDefs); if (argEntries.length > 0) { lines.push("ARGUMENTS"); const maxLen = Math.max(...argEntries.map(([n]) => n.length)); for (const [name, desc] of argEntries) { const parts = [name.toUpperCase().padEnd(maxLen + 2)]; if (desc.description) parts.push(desc.description); if (desc.options) parts.push(`(${[...desc.options].join("|")})`); lines.push(` ${parts.join(" ")}`); } lines.push(""); } // Flags const flagEntries = Object.entries(flagDefs); if (flagEntries.length > 0) { lines.push("FLAGS"); const formatted: [string, string][] = []; for (const [name, desc] of flagEntries) { const charPart = desc.char ? `-${desc.char}, ` : " "; const namePart = `--${name}`; const typePart = desc.kind === "boolean" ? "" : desc.kind === "integer" ? "=" : "="; formatted.push([` ${charPart}${namePart}${typePart}`, desc.description ?? ""]); } const maxLeft = Math.max(...formatted.map(([l]) => l.length)); for (const [left, right] of formatted) { lines.push(`${left.padEnd(maxLeft + 2)}${right}`); } lines.push(""); } // Examples if (command.examples && command.examples.length > 0) { lines.push("EXAMPLES"); for (const ex of command.examples) { for (const line of ex.split("\n")) { lines.push(` ${line}`); } } lines.push(""); } } // --------------------------------------------------------------------------- // CLI entry point // --------------------------------------------------------------------------- /** A lazily-loaded command: canonical name, loader, and optional aliases. */ export interface CommandEntry { name: string; load: () => Promise; help?: CommandMetadata; aliases?: string[]; } export interface RunOptions { bin: string; version: string; argv: string[]; commands: CommandEntry[]; /** Custom help renderer with the fully loaded command constructors. */ help?: (config: CliConfig) => Promise | void; /** Lightweight help renderer backed by static command metadata. */ metadataHelp?: (config: CliConfig) => Promise | void; } /** Find a command entry by exact name or alias. */ function findEntry(commands: CommandEntry[], id: string): CommandEntry | undefined { return commands.find(e => e.name === id) ?? commands.find(e => e.aliases?.includes(id)); } /** * Main entry point — replaces `run()` from @oclif/core. * * Each command is explicitly registered with a lazy loader. * No filesystem scanning, no plugin system, no package.json reading. */ export async function run(opts: RunOptions): Promise { const { bin, version, argv } = opts; const commandId = argv[0] ?? ""; const commandArgv = argv.slice(1); // Top-level help if (commandId === "--help" || commandId === "-h" || commandId === "help" || commandId === "") { if (opts.help) { await opts.help(await loadAllCommands(opts)); } else { const config = await loadAllCommandMetadata(opts); if (opts.metadataHelp) { await opts.metadataHelp(config); } else { renderRootHelp(config); } } return; } // Version if (commandId === "--version" || commandId === "-v") { process.stdout.write(`${bin}/${version}\n`); return; } // Per-command help: load only the requested command. Loading the full // command table here would make `omp --help` hang or crash whenever // any *unrelated* command module misbehaves at import time. if (commandArgv.includes("--help") || commandArgv.includes("-h")) { const entry = findEntry(opts.commands, commandId); if (entry) { const Cmd = await loadEntry(entry); renderCommandHelp(bin, entry.name, Cmd); } else { process.stderr.write(`Unknown command: ${commandId}\n`); } return; } // Find command by name or alias const entry = findEntry(opts.commands, commandId); if (!entry) { process.stderr.write(`Error: command ${commandId} not found\n`); process.exitCode = 1; return; } const Cmd = await loadEntry(entry); const config: CliConfig = { bin, version, commands: new Map([[entry.name, Cmd]]) }; const instance = new Cmd(commandArgv, config); try { await instance.run(); } catch (error) { // A usage mistake (missing/invalid arg or flag) is not a crash: print the // message and the command's usage line, then exit 1. Letting it reach the // process-level catch would dump a minified `dist/cli.js` code frame over a // plain argument error (issue #5369). if (error instanceof CliUsageError) { process.stderr.write(`error: ${error.message}\n\n`); process.stderr.write(`USAGE\n ${commandUsageLine(bin, entry.name, Cmd)}\n`); process.stderr.write(`\nRun \`${bin} ${entry.name} --help\` for details.\n`); process.exitCode = 1; return; } throw error; } } /** Load one command module, leaving streaming markers around the import. */ async function loadEntry(entry: CommandEntry): Promise { startupMarker(`cli:load:${entry.name}:start`); const Cmd = await entry.load(); startupMarker(`cli:load:${entry.name}:done`); return Cmd; } /** Load every command constructor for backward-compatible custom help callbacks. */ async function loadAllCommands(opts: RunOptions): Promise { const loaded = await Promise.all(opts.commands.map(async entry => [entry.name, await loadEntry(entry)] as const)); return { bin: opts.bin, version: opts.version, commands: new Map(loaded) }; } /** Resolve static command metadata for lightweight root help. */ async function loadAllCommandMetadata(opts: RunOptions): Promise> { const loaded = await Promise.all( opts.commands.map(async entry => [entry.name, entry.help ?? (await loadEntry(entry))] as const), ); return { bin: opts.bin, version: opts.version, commands: new Map(loaded) }; }