/** * 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 { parseArgs as nodeParseArgs } from "node:util"; // --------------------------------------------------------------------------- // Flag & Arg descriptors // --------------------------------------------------------------------------- export interface FlagDescriptor { kind: K; description?: string; char?: string; default?: unknown; multiple?: boolean; options?: readonly string[]; required?: boolean; /** The string value may be omitted; parsers decide whether to consume it. */ optionalValue?: 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; /** The string value may be omitted; parsers decide whether to consume it. */ optionalValue?: 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; }, }; /** * Thrown when CLI argument/flag parsing or validation fails (unknown flag, * bad option value, missing required arg, etc.). `run()` catches this to print * the message and render usage instead of crashing as an uncaught exception. */ export class CliParseError extends Error { constructor(message: string) { super(message); this.name = "CliParseError"; } } // --------------------------------------------------------------------------- // 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 CommandCtor { new (argv: string[], config: CliConfig): Command; description?: string; hidden?: boolean; strict?: boolean; aliases?: string[]; examples?: string[]; flags?: Record; args?: Record; delegateHelp?: boolean; } /** 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 let rawValues: Record | undefined>; let positionals: string[]; try { const parsed = nodeParseArgs({ args: this.argv, options, allowPositionals: true, strict, }); rawValues = parsed.values; positionals = parsed.positionals; } catch (err) { // node:util parseArgs throws on unknown flags / malformed input — surface // it as a CliParseError so run() renders usage instead of crashing. throw new CliParseError(err instanceof Error ? err.message : String(err)); } // 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 { if (typeof raw !== "string" || !/^-?\d+$/.test(raw)) { throw new CliParseError(`Expected integer for --${name}, got "${String(raw)}"`); } const n = Number(raw); if (!Number.isSafeInteger(n)) { throw new CliParseError(`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 CliParseError( `Expected --${name} to be one of: ${[...desc.options].join(", ")}; got "${val}"`, ); } } flags[name] = val; } // Validate required if (desc.required && flags[name] === undefined) { throw new CliParseError(`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 CliParseError(`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 CliParseError( `Expected ${argName} to be one of: ${[...desc.options].join(", ")}; got "${argVal}"`, ); } } } if (strict && posIdx < positionals.length) { const unexpected = positionals.slice(posIdx); const rendered = unexpected.map(value => JSON.stringify(value)).join(", "); throw new CliParseError(`Unexpected argument${unexpected.length === 1 ? "" : "s"}: ${rendered}`); } 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(C => C.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, C] of visible.sort((a, b) => a[0].localeCompare(b[0]))) { lines.push(` ${name.padEnd(maxLen + 2)}${C.description ?? ""}`); } lines.push(""); } process.stdout.write(lines.join("\n")); } /** 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"); const argNames = Object.keys(Cmd.args ?? {}); const argStr = argNames.length > 0 ? ` ${argNames.map(n => `[${n.toUpperCase()}]`).join(" ")}` : ""; const hasFlags = Object.keys(Cmd.flags ?? {}).length > 0; lines.push(` $ ${bin} ${id}${argStr}${hasFlags ? " [FLAGS]" : ""}\n`); renderCommandBody(lines, Cmd); process.stdout.write(lines.join("\n")); } function renderCommandBody(lines: string[], Cmd: CommandCtor): void { const argDefs = Cmd.args ?? {}; const flagDefs = Cmd.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" ? "=" : desc.optionalValue ? "[=]" : "="; 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 (Cmd.examples && Cmd.examples.length > 0) { lines.push("EXAMPLES"); for (const ex of Cmd.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; aliases?: string[]; /** Owns parsing, help, loading and failures for this family when supplied. */ dispatch?: (argv: string[], context: CommandEntryContext) => Promise; } export interface CommandEntryContext { bin: string; version: string; /** Canonical registered entry name, including when invoked through an alias. */ command: string; } export interface RunOptions { bin: string; version: string; argv: string[]; commands: CommandEntry[]; /** Custom help renderer. Receives fully-populated config. */ help?: (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 === "") { const config = await loadAllCommands(opts); if (opts.help) { await opts.help(config); } else { renderRootHelp(config); } return; } // Version if (commandId === "--version" || commandId === "-v") { process.stdout.write(`${bin}/${version}\n`); return; } const dispatchedEntry = findEntry(opts.commands, commandId); if (dispatchedEntry?.dispatch) { await dispatchedEntry.dispatch(commandArgv, { bin, version, command: dispatchedEntry.name }); return; } // Per-command help. Commands with nested subcommands can opt into receiving // help flags themselves so `cmd subcommand --help` can render subcommand help. const delimiterIndex = commandArgv.indexOf("--"); const helpArgs = delimiterIndex === -1 ? commandArgv : commandArgv.slice(0, delimiterIndex); if (helpArgs.includes("--help") || helpArgs.includes("-h")) { const entry = findEntry(opts.commands, commandId); if (!entry) { process.stderr.write(`Unknown command: ${commandId}\n`); return; } const Cmd = await entry.load(); if (Cmd.delegateHelp) { const config: CliConfig = { bin, version, commands: new Map([[entry.name, Cmd]]) }; const instance = new Cmd(commandArgv, config); await instance.run(); } else { const config: CliConfig = { bin, version, commands: new Map([[entry.name, Cmd]]) }; renderCommandHelp(bin, entry.name, config.commands.get(entry.name) ?? Cmd); } 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 entry.load(); const config: CliConfig = { bin, version, commands: new Map([[entry.name, Cmd]]) }; const instance = new Cmd(commandArgv, config); try { await instance.run(); } catch (err) { if (err instanceof CliParseError) { // Invalid args/flags for a real command: print the problem + usage and // exit with a usage error, instead of crashing as an uncaught exception. process.stderr.write(`${err.message}\n\n`); renderCommandHelp(bin, entry.name, Cmd); process.exitCode = 2; return; } throw err; } } /** Resolve all command loaders for help/alias display. */ async function loadAllCommands(opts: RunOptions): Promise { const commands = new Map(); const loaded = await Promise.all(opts.commands.map(async e => [e.name, await e.load()] as const)); for (const [name, Cmd] of loaded) { commands.set(name, Cmd); } return { bin: opts.bin, version: opts.version, commands }; }