Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 30 additions & 85 deletions src/commander.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Command as CommanderCommand } from 'commander';
import t, { Command as TabCommand, type RootCommand } from './t';

Check warning on line 2 in src/commander.ts

View workflow job for this annotation

GitHub Actions / Lint and Type Check

Imports "TabCommand" are only used as type

// rawArgs is available on (just) the Commander root command, but is not included in the TypeScript types.
interface CommandWithRawArgs extends CommanderCommand {
Expand All @@ -24,12 +24,6 @@
): 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';
Expand Down Expand Up @@ -101,71 +95,39 @@
});
}

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 `<value>` 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);
}

Expand Down Expand Up @@ -200,24 +162,8 @@
// 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);
}
}
Expand All @@ -231,10 +177,9 @@
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()}`
Expand Down
10 changes: 10 additions & 0 deletions tests/__snapshots__/cli.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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
"
`;
Expand Down Expand Up @@ -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
"
`;
Expand Down Expand Up @@ -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
"
`;
Expand All @@ -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
"
`;
Expand Down Expand Up @@ -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
"
`;
Expand Down Expand Up @@ -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
"
`;
Expand All @@ -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
"
`;
Expand Down
Loading