From b7e66a079ff03d0e4ca51d867635c4cde07980b5 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 14 Jun 2026 11:58:41 +1200 Subject: [PATCH] Use Commander built-in support for determining option flags and types, visible options, and visible commands. --- src/commander.ts | 115 +++++++-------------------- tests/__snapshots__/cli.test.ts.snap | 10 +++ 2 files changed, 40 insertions(+), 85 deletions(-) diff --git a/src/commander.ts b/src/commander.ts index 7832bd0..de978f9 100644 --- a/src/commander.ts +++ b/src/commander.ts @@ -24,12 +24,6 @@ export default function tab( ): RootCommand { const programName = instance.name(); - // Process the root command - processRootCommand(instance); - - // Process all subcommands - processSubcommands(instance); - // Make a `completion` command with a required command-argument. const completionCommandName = completionConfig?.completionCommandName ?? 'complete'; @@ -101,71 +95,39 @@ export default function tab( }); } - return t; -} + // Now we have added complete and completion command... + // Process the root command + processRootCommand(instance); -/** - * Detect whether a commander option flag expects a value argument. - * Options with `` or `[value]` in their flags are value-taking. - */ -function optionTakesValue(flags: string): boolean { - return flags.includes('<') || flags.includes('['); + // Process all subcommands + processSubcommands(instance); + + return t; } -/** - * Register a commander option with the tab library, correctly setting - * isBoolean based on whether the option takes a value. - * - * The tab Command.option() method infers isBoolean from the argument types: - * - string arg → alias, isBoolean=true - * - function arg → handler, isBoolean=false - * So for value-taking options with an alias, we pass a no-op handler - * and the alias separately to get isBoolean=false. - */ -function registerOption( - tabCommand: { - option: ( - value: string, - description: string, - handlerOrAlias?: ((...args: unknown[]) => void) | string, - alias?: string - ) => unknown; - }, - flags: string, - longFlag: string, - description: string, - shortFlag?: string -): void { - const takesValue = optionTakesValue(flags); - if (shortFlag) { - if (takesValue) { - // Pass a no-op handler to force isBoolean=false, with alias as 4th arg - tabCommand.option(longFlag, description, () => {}, shortFlag); - } else { - tabCommand.option(longFlag, description, shortFlag); - } - } else { - if (takesValue) { - tabCommand.option(longFlag, description, () => {}); - } else { - tabCommand.option(longFlag, description); +function processOptions(t: TabCommand, cmd: CommanderCommand): void { + // visibleOptions handles hidden options and built-in help option + const visibleOptions = cmd.createHelp().visibleOptions(cmd); + for (const option of visibleOptions) { + // Commander has at least one of short and long option flags, but can have just one. + // Commander also allows special case, shortish long and long like '--ws, --workspace'. + // Remove the leading dashes to get the names. + let shortName = option.short?.slice(1); + if (shortName && shortName[0] === '-') shortName = undefined; // ignore shortish long + const longName = option.long?.slice(2); + if (longName) { + const optionTakesValue = option.required || option.optional; + if (optionTakesValue) { + t.option(longName, option.description, () => {}, shortName); + } else { + t.option(longName, option.description, shortName); + } } } } function processRootCommand(command: CommanderCommand): void { - // Add root command options to the root t instance - for (const option of command.options) { - // Extract short flag from the name if it exists (e.g., "-c, --config" -> "c") - const flags = option.flags; - const shortFlag = flags.match(/^-([a-zA-Z]), --/)?.[1]; - const longFlag = flags.match(/--([a-zA-Z0-9-]+)/)?.[1]; - - if (longFlag) { - registerOption(t, flags, longFlag, option.description || '', shortFlag); - } - } - + processOptions(t, command); processArguments(t, command); } @@ -200,24 +162,8 @@ function processSubcommands(rootCommand: CommanderCommand): void { // Add command using t.ts API const command = t.command(path, cmd.description() || ''); - // Add command options - for (const option of cmd.options) { - // Extract short flag from the name if it exists (e.g., "-c, --config" -> "c") - const flags = option.flags; - const shortFlag = flags.match(/^-([a-zA-Z]), --/)?.[1]; - const longFlag = flags.match(/--([a-zA-Z0-9-]+)/)?.[1]; - - if (longFlag) { - registerOption( - command, - flags, - longFlag, - option.description || '', - shortFlag - ); - } - } - + // Add command options and arguments + processOptions(command, cmd); processArguments(command, cmd); } } @@ -231,10 +177,9 @@ function collectCommands( commandMap.set(parentPath, command); // Process subcommands - for (const subcommand of command.commands) { - // Skip the completion command - if (subcommand.name() === 'complete') continue; - + // visibleCommands handles hidden commands and built-in help command + const visibleCommands = command.createHelp().visibleCommands(command); + for (const subcommand of visibleCommands) { // Build the full path for this subcommand const subcommandPath = parentPath ? `${parentPath} ${subcommand.name()}` diff --git a/tests/__snapshots__/cli.test.ts.snap b/tests/__snapshots__/cli.test.ts.snap index 2261316..09f1af1 100644 --- a/tests/__snapshots__/cli.test.ts.snap +++ b/tests/__snapshots__/cli.test.ts.snap @@ -666,6 +666,7 @@ exports[`cli completion tests for commander > --config option tests > should not --config Use specified config file --mode Set env mode --logLevel Specify log level +--help display help for command :4 " `; @@ -700,6 +701,7 @@ exports[`cli completion tests for commander > cli option value handling > should --config Use specified config file --mode Set env mode --logLevel Specify log level +--help display help for command :4 " `; @@ -819,6 +821,8 @@ build Build the project deploy Deploy the application lint Lint source files copy Copy files +complete Generate shell completion scripts +help display help for command :4 " `; @@ -830,6 +834,8 @@ build Build the project deploy Deploy the application lint Lint source files copy Copy files +complete Generate shell completion scripts +help display help for command :4 " `; @@ -877,6 +883,7 @@ exports[`cli completion tests for commander > root command option tests > should --config Use specified config file --mode Set env mode --logLevel Specify log level +--help display help for command :4 " `; @@ -926,6 +933,7 @@ exports[`cli completion tests for commander > short flag handling > should not s --config Use specified config file --mode Set env mode --logLevel Specify log level +--help display help for command :4 " `; @@ -937,6 +945,8 @@ build Build the project deploy Deploy the application lint Lint source files copy Copy files +complete Generate shell completion scripts +help display help for command :4 " `;