// CLI dispatcher — recursively walks CLICommand tree and wires up yargs import yargs, { type Argv } from "yargs" import { hideBin } from "yargs/helpers" import { CLICommand } from "./CLICommand" interface CLIOpts { prog: string description: string } export class CLI { private prog: string private description: string constructor(opts: CLIOpts) { this.prog = opts.prog this.description = opts.description } private registerCommand(y: Argv, CommandClass: new () => CLICommand): void { const cmd = new CommandClass() const subcommands = (CommandClass as any)._subcommands as (new () => CLICommand)[] | undefined if (subcommands?.length) { // Branch command with subcommands y.command( cmd.name, cmd.help, (sub) => { for (const Sub of subcommands) { this.registerCommand(sub, Sub) } return sub.demandCommand(1, `Please specify a ${cmd.name} subcommand`) }, () => {} ) } else { // Leaf command y.command( cmd.name, cmd.help, (sub) => cmd.addArguments(sub), async (args) => { const code = await cmd.execute(args) process.exit(code) } ) } } bootstrap(commands: (new () => CLICommand)[]): void { const y = yargs(hideBin(process.argv)) .scriptName(this.prog) .usage(`${this.description}\n\n$0 [options]`) .option("verbose", { alias: "v", type: "boolean", description: "Enable verbose output", default: false, }) .demandCommand(1, "Please specify a command") .strict() .help() for (const Cmd of commands) { this.registerCommand(y, Cmd) } y.parse() } }