feat(cli): replaced oclif with lightweight custom CLI framework in pi-utils
- Created lightweight CLI framework in @oh-my-pi/pi-utils/cli as drop-in replacement for oclif with zero dependencies. - Migrated coding-agent CLI from oclif framework to @oh-my-pi/pi-utils/cli with lazy command loading and simplified architecture. - Changed default root command from 'index' to 'launch' for improved UX. - Removed @oclif/core and @oclif/plugin-autocomplete dependencies from coding-agent package. - Deleted custom oclif help renderer (oclif-help.ts) as help rendering is now integrated into the new CLI framework. - Moved launch command from nested index/index.ts to top-level launch.ts for clearer command structure.
This commit is contained in:
@@ -1,6 +1,18 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
### Changed
|
||||
|
||||
- Migrated CLI framework from oclif to lightweight pi-utils CLI runner
|
||||
- Replaced oclif command registration with explicit command entries in cli.ts
|
||||
- Changed default root command name from 'index' to 'launch'
|
||||
- Updated all command imports to use @oh-my-pi/pi-utils/cli instead of @oclif/core
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed @oclif/core and @oclif/plugin-autocomplete dependencies
|
||||
- Removed oclif configuration from package.json
|
||||
- Removed custom oclif help renderer (oclif-help.ts)
|
||||
|
||||
## [11.10.0] - 2026-02-10
|
||||
### Breaking Changes
|
||||
|
||||
@@ -10,14 +10,6 @@
|
||||
"bin": {
|
||||
"omp": "src/cli.ts"
|
||||
},
|
||||
"oclif": {
|
||||
"bin": "omp",
|
||||
"commands": "./src/commands",
|
||||
"helpClass": "./src/cli/oclif-help.ts",
|
||||
"plugins": [
|
||||
"@oclif/plugin-autocomplete"
|
||||
]
|
||||
},
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
@@ -88,8 +80,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@mozilla/readability": "0.6.0",
|
||||
"@oclif/core": "^4.8.0",
|
||||
"@oclif/plugin-autocomplete": "^3.2.40",
|
||||
"@oh-my-pi/omp-stats": "workspace:*",
|
||||
"@oh-my-pi/pi-agent-core": "workspace:*",
|
||||
"@oh-my-pi/pi-ai": "workspace:*",
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* CLI entry point for the refactored coding agent.
|
||||
* Uses main.ts with AgentSession and new mode modules.
|
||||
*
|
||||
* Test with: npx tsx src/cli-new.ts [args...]
|
||||
* CLI entry point — registers all commands explicitly and delegates to the
|
||||
* lightweight CLI runner from pi-utils.
|
||||
*/
|
||||
import { run } from "@oclif/core";
|
||||
import { APP_NAME } from "./config";
|
||||
import { type CommandEntry, run } from "@oh-my-pi/pi-utils/cli";
|
||||
import { APP_NAME, VERSION } from "./config";
|
||||
|
||||
// oclif's warn() doesn't unwrap AggregateError — override to surface the real messages
|
||||
// Unwrap AggregateError in console.warn to surface real messages
|
||||
const originalWarn = console.warn;
|
||||
console.warn = (...args: unknown[]) => {
|
||||
for (const arg of args) {
|
||||
@@ -23,9 +21,38 @@ console.warn = (...args: unknown[]) => {
|
||||
};
|
||||
|
||||
process.title = APP_NAME;
|
||||
const argv = process.argv.slice(2);
|
||||
const runArgv = argv.length === 0 || argv[0]?.startsWith("-") ? ["index", ...argv] : argv;
|
||||
run(runArgv, import.meta.url).catch((error: unknown) => {
|
||||
|
||||
const commands: CommandEntry[] = [
|
||||
{ name: "launch", load: () => import("./commands/launch").then(m => m.default) },
|
||||
{ name: "commit", load: () => import("./commands/commit").then(m => m.default) },
|
||||
{ name: "config", load: () => import("./commands/config").then(m => m.default) },
|
||||
{ name: "grep", load: () => import("./commands/grep").then(m => m.default) },
|
||||
{ name: "jupyter", load: () => import("./commands/jupyter").then(m => m.default) },
|
||||
{ name: "plugin", load: () => import("./commands/plugin").then(m => m.default) },
|
||||
{ name: "setup", load: () => import("./commands/setup").then(m => m.default) },
|
||||
{ name: "shell", load: () => import("./commands/shell").then(m => m.default) },
|
||||
{ name: "stats", load: () => import("./commands/stats").then(m => m.default) },
|
||||
{ name: "update", load: () => import("./commands/update").then(m => m.default) },
|
||||
{ name: "search", load: () => import("./commands/web-search").then(m => m.default), aliases: ["q"] },
|
||||
];
|
||||
|
||||
async function showHelp(config: import("@oh-my-pi/pi-utils/cli").CliConfig): Promise<void> {
|
||||
const { renderRootHelp } = await import("@oh-my-pi/pi-utils/cli");
|
||||
const { getExtraHelpText } = await import("./cli/args");
|
||||
renderRootHelp(config);
|
||||
const extra = getExtraHelpText();
|
||||
if (extra.trim().length > 0) {
|
||||
process.stdout.write(`\n${extra}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Run the CLI with the given argv (no `process.argv` prefix). */
|
||||
export function runCli(argv: string[]): Promise<void> {
|
||||
const runArgv = argv.length === 0 || argv[0]?.startsWith("-") ? ["launch", ...argv] : argv;
|
||||
return run({ bin: APP_NAME, version: VERSION, argv: runArgv, commands, help: showHelp });
|
||||
}
|
||||
|
||||
runCli(process.argv.slice(2)).catch((error: unknown) => {
|
||||
const message = error instanceof Error ? (error.stack ?? error.message) : String(error);
|
||||
process.stderr.write(`${message}\n`);
|
||||
process.exit(1);
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
/**
|
||||
* Custom help renderer for the coding agent CLI.
|
||||
*/
|
||||
import { CommandHelp, Help } from "@oclif/core";
|
||||
import { getExtraHelpText } from "./args";
|
||||
|
||||
export default class OclifHelp extends Help {
|
||||
async showRootHelp(): Promise<void> {
|
||||
await super.showRootHelp();
|
||||
const rootCommand = this.config.findCommand("index");
|
||||
if (rootCommand) {
|
||||
const rootHelp = new CommandHelp(rootCommand, this.config, {
|
||||
...this.opts,
|
||||
sections: ["arguments", "flags", "examples"],
|
||||
});
|
||||
const output = rootHelp.generate();
|
||||
if (output.trim().length > 0) {
|
||||
process.stdout.write(`\n${output}\n`);
|
||||
}
|
||||
}
|
||||
const extra = getExtraHelpText();
|
||||
if (extra.trim().length > 0) {
|
||||
process.stdout.write(`\n${extra}\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Generate and optionally push a commit with changelog updates.
|
||||
*/
|
||||
import { Command, Flags } from "@oclif/core";
|
||||
import { Command, Flags } from "@oh-my-pi/pi-utils/cli";
|
||||
import { runCommitCommand } from "../commit";
|
||||
import type { CommitCommandArgs } from "../commit/types";
|
||||
import { initTheme } from "../modes/theme/theme";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Manage configuration settings.
|
||||
*/
|
||||
import { Args, Command, Flags } from "@oclif/core";
|
||||
import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
|
||||
import { type ConfigAction, type ConfigCommandArgs, runConfigCommand } from "../cli/config-cli";
|
||||
import { initTheme } from "../modes/theme/theme";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Test grep tool.
|
||||
*/
|
||||
import { Args, Command, Flags } from "@oclif/core";
|
||||
import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
|
||||
import { type GrepCommandArgs, runGrepCommand } from "../cli/grep-cli";
|
||||
import { initTheme } from "../modes/theme/theme";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Manage the shared Jupyter gateway.
|
||||
*/
|
||||
import { Args, Command } from "@oclif/core";
|
||||
import { Args, Command } from "@oh-my-pi/pi-utils/cli";
|
||||
import { type JupyterAction, type JupyterCommandArgs, runJupyterCommand } from "../cli/jupyter-cli";
|
||||
import { initTheme } from "../modes/theme/theme";
|
||||
|
||||
|
||||
+4
-4
@@ -1,10 +1,10 @@
|
||||
/**
|
||||
* Root command for the coding agent CLI.
|
||||
*/
|
||||
import { Args, Command, Flags } from "@oclif/core";
|
||||
import { parseArgs } from "../../cli/args";
|
||||
import { APP_NAME } from "../../config";
|
||||
import { runRootCommand } from "../../main";
|
||||
import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
|
||||
import { parseArgs } from "../cli/args";
|
||||
import { APP_NAME } from "../config";
|
||||
import { runRootCommand } from "../main";
|
||||
|
||||
export default class Index extends Command {
|
||||
static description = "AI coding assistant";
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Manage plugins (install, uninstall, list, etc.).
|
||||
*/
|
||||
import { Args, Command, Flags } from "@oclif/core";
|
||||
import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
|
||||
import { type PluginAction, type PluginCommandArgs, runPluginCommand } from "../cli/plugin-cli";
|
||||
import { initTheme } from "../modes/theme/theme";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Install dependencies for optional features.
|
||||
*/
|
||||
import { Args, Command, Flags } from "@oclif/core";
|
||||
import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
|
||||
import { runSetupCommand, type SetupCommandArgs, type SetupComponent } from "../cli/setup-cli";
|
||||
import { initTheme } from "../modes/theme/theme";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Interactive shell console.
|
||||
*/
|
||||
import { Command, Flags } from "@oclif/core";
|
||||
import { Command, Flags } from "@oh-my-pi/pi-utils/cli";
|
||||
import { runShellCommand, type ShellCommandArgs } from "../cli/shell-cli";
|
||||
import { initTheme } from "../modes/theme/theme";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* View usage statistics dashboard.
|
||||
*/
|
||||
import { Command, Flags } from "@oclif/core";
|
||||
import { Command, Flags } from "@oh-my-pi/pi-utils/cli";
|
||||
import { runStatsCommand, type StatsCommandArgs } from "../cli/stats-cli";
|
||||
import { initTheme } from "../modes/theme/theme";
|
||||
|
||||
@@ -10,8 +10,8 @@ export default class Stats extends Command {
|
||||
|
||||
static flags = {
|
||||
port: Flags.integer({ char: "p", description: "Port for the dashboard server", default: 3847 }),
|
||||
json: Flags.boolean({ char: "j", description: "Output stats as JSON" }),
|
||||
summary: Flags.boolean({ char: "s", description: "Print summary to console" }),
|
||||
json: Flags.boolean({ char: "j", description: "Output stats as JSON", default: false }),
|
||||
summary: Flags.boolean({ char: "s", description: "Print summary to console", default: false }),
|
||||
};
|
||||
|
||||
async run(): Promise<void> {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Check for and install updates.
|
||||
*/
|
||||
import { Command, Flags } from "@oclif/core";
|
||||
import { Command, Flags } from "@oh-my-pi/pi-utils/cli";
|
||||
import { runUpdateCommand } from "../cli/update-cli";
|
||||
import { initTheme } from "../modes/theme/theme";
|
||||
|
||||
@@ -9,8 +9,8 @@ export default class Update extends Command {
|
||||
static description = "Check for and install updates";
|
||||
|
||||
static flags = {
|
||||
force: Flags.boolean({ char: "f", description: "Force update" }),
|
||||
check: Flags.boolean({ char: "c", description: "Check for updates without installing" }),
|
||||
force: Flags.boolean({ char: "f", description: "Force update", default: false }),
|
||||
check: Flags.boolean({ char: "c", description: "Check for updates without installing", default: false }),
|
||||
};
|
||||
|
||||
async run(): Promise<void> {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Test web search providers.
|
||||
*/
|
||||
import { Args, Command, Flags } from "@oclif/core";
|
||||
import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
|
||||
import { runSearchCommand, type SearchCommandArgs } from "../cli/web-search-cli";
|
||||
import type { SearchProviderId } from "../web/search/types";
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { createInterface } from "node:readline/promises";
|
||||
import { run } from "@oclif/core";
|
||||
import { type ImageContent, supportsXhigh } from "@oh-my-pi/pi-ai";
|
||||
import { $env, postmortem } from "@oh-my-pi/pi-utils";
|
||||
import chalk from "chalk";
|
||||
@@ -713,6 +712,6 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
||||
}
|
||||
|
||||
export async function main(args: string[]): Promise<void> {
|
||||
const argv = args.length === 0 ? ["index"] : args;
|
||||
await run(argv, import.meta.url);
|
||||
const { runCli } = await import("./cli");
|
||||
await runCli(args.length === 0 ? ["launch"] : args);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"import": "./src/index.ts"
|
||||
},
|
||||
"./cli": {
|
||||
"types": "./src/cli.ts",
|
||||
"import": "./src/cli.ts"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -0,0 +1,392 @@
|
||||
/**
|
||||
* 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<K extends "string" | "boolean" | "integer" = "string" | "boolean" | "integer"> {
|
||||
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<T extends FlagInput>(opts?: T): FlagDescriptor<"string"> & T {
|
||||
return { kind: "string" as const, ...opts } as FlagDescriptor<"string"> & T;
|
||||
},
|
||||
boolean<T extends FlagInput>(opts?: T): FlagDescriptor<"boolean"> & T {
|
||||
return { kind: "boolean" as const, ...opts } as FlagDescriptor<"boolean"> & T;
|
||||
},
|
||||
integer<T extends FlagInput & { default?: number }>(opts?: T): FlagDescriptor<"integer"> & T {
|
||||
return { kind: "integer" as const, ...opts } as FlagDescriptor<"integer"> & T;
|
||||
},
|
||||
};
|
||||
export const Args = {
|
||||
string<T extends ArgInput>(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 extends FlagDescriptor> = 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 ArgDescriptor> = D extends { multiple: true } ? string[] | undefined : string | undefined;
|
||||
|
||||
type FlagValues<T extends Record<string, FlagDescriptor>> = { [K in keyof T]: FlagValue<T[K]> };
|
||||
type ArgValues<T extends Record<string, ArgDescriptor>> = { [K in keyof T]: ArgValue<T[K]> };
|
||||
|
||||
export interface ParseOutput<
|
||||
F extends Record<string, FlagDescriptor> = Record<string, FlagDescriptor>,
|
||||
A extends Record<string, ArgDescriptor> = Record<string, ArgDescriptor>,
|
||||
> {
|
||||
flags: FlagValues<F>;
|
||||
args: ArgValues<A>;
|
||||
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<string, FlagDescriptor>;
|
||||
args?: Record<string, ArgDescriptor>;
|
||||
}
|
||||
|
||||
/** 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<string, CommandCtor>;
|
||||
}
|
||||
|
||||
/** 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<void>;
|
||||
|
||||
/**
|
||||
* Parse argv against the static `flags` and `args` declared on the
|
||||
* concrete command class. Returns a typed `{ flags, args, argv }` object.
|
||||
*/
|
||||
async parse<C extends CommandCtor>(
|
||||
_Cmd: C,
|
||||
): Promise<
|
||||
ParseOutput<
|
||||
NonNullable<C["flags"]> extends Record<string, FlagDescriptor>
|
||||
? NonNullable<C["flags"]>
|
||||
: Record<string, FlagDescriptor>,
|
||||
NonNullable<C["args"]> extends Record<string, ArgDescriptor>
|
||||
? NonNullable<C["args"]>
|
||||
: Record<string, ArgDescriptor>
|
||||
>
|
||||
> {
|
||||
const Cmd = _Cmd as CommandCtor;
|
||||
const flagDefs = (Cmd.flags ?? {}) as Record<string, FlagDescriptor>;
|
||||
const argDefs = (Cmd.args ?? {}) as Record<string, ArgDescriptor>;
|
||||
const strict = Cmd.strict ?? true;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
const { values: rawValues, positionals } = nodeParseArgs({
|
||||
args: this.argv,
|
||||
options,
|
||||
allowPositionals: true,
|
||||
strict: strict && Object.keys(argDefs).length === 0,
|
||||
});
|
||||
|
||||
// Convert raw values to proper types
|
||||
const flags: Record<string, unknown> = {};
|
||||
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 {
|
||||
flags[name] = Number.parseInt(raw as string, 10);
|
||||
}
|
||||
} else if (desc.kind === "boolean") {
|
||||
flags[name] =
|
||||
raw !== undefined ? Boolean(raw) : desc.default !== undefined ? Boolean(desc.default) : undefined;
|
||||
} else {
|
||||
// string
|
||||
flags[name] = raw !== undefined && typeof raw !== "boolean" ? raw : (desc.default ?? undefined);
|
||||
}
|
||||
}
|
||||
|
||||
// Map positionals to named args in declaration order
|
||||
const args: Record<string, unknown> = {};
|
||||
let posIdx = 0;
|
||||
for (const [argName, desc] of Object.entries(argDefs)) {
|
||||
if (desc.multiple) {
|
||||
args[argName] = positionals.slice(posIdx);
|
||||
posIdx = positionals.length;
|
||||
} else {
|
||||
args[argName] = positionals[posIdx];
|
||||
posIdx++;
|
||||
}
|
||||
}
|
||||
|
||||
return { flags, args, argv: positionals } as never;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Help rendering
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Render full root help: header, index 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 index command's flags/args/examples inline
|
||||
const indexCmd = commands.get("index");
|
||||
if (indexCmd) {
|
||||
renderCommandBody(lines, indexCmd);
|
||||
}
|
||||
|
||||
// List visible subcommands
|
||||
const visible = [...commands.entries()].filter(([name, C]) => !C.hidden && name !== "index");
|
||||
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" ? "=<int>" : "=<value>";
|
||||
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<CommandCtor>;
|
||||
aliases?: string[];
|
||||
}
|
||||
|
||||
export interface RunOptions {
|
||||
bin: string;
|
||||
version: string;
|
||||
argv: string[];
|
||||
commands: CommandEntry[];
|
||||
/** Custom help renderer. Receives fully-populated config. */
|
||||
help?: (config: CliConfig) => Promise<void> | void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<void> {
|
||||
const { bin, version, argv } = opts;
|
||||
|
||||
const commandId = argv[0] ?? "index";
|
||||
const commandArgv = argv.slice(1);
|
||||
|
||||
// Top-level help
|
||||
if (commandId === "--help" || commandId === "-h" || commandId === "help") {
|
||||
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;
|
||||
}
|
||||
|
||||
// Per-command help
|
||||
if (commandArgv.includes("--help") || commandArgv.includes("-h")) {
|
||||
const config = await loadAllCommands(opts);
|
||||
const Cmd = config.commands.get(commandId);
|
||||
if (Cmd) {
|
||||
renderCommandHelp(bin, commandId, Cmd);
|
||||
} else {
|
||||
process.stderr.write(`Unknown command: ${commandId}\n`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Find command by name or alias
|
||||
let entry = opts.commands.find(e => e.name === commandId);
|
||||
if (!entry) {
|
||||
entry = opts.commands.find(e => e.aliases?.includes(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);
|
||||
await instance.run();
|
||||
}
|
||||
|
||||
/** Resolve all command loaders for help/alias display. */
|
||||
async function loadAllCommands(opts: RunOptions): Promise<CliConfig> {
|
||||
const commands = new Map<string, CommandCtor>();
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user