Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/free-doors-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@bomb.sh/tab': patch
---

feat(commander): use commander for determining integration details (#131)
10 changes: 1 addition & 9 deletions examples/demo.commander.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,15 +108,7 @@ if (modeOption) {
complete('production', 'Production mode');
};
}
const logLevelOption = completion.options.get('logLevel');
if (logLevelOption) {
logLevelOption.handler = (complete) => {
complete('info', 'Info level');
complete('warn', 'Warn level');
complete('error', 'Error level');
complete('silent', 'Silent level');
};
}
// Note: loglevel automatically gets completions because uses ".choices()" in option definition.

// Options on dev command
const devCommandInstance = completion.commands.get('dev');
Expand Down
126 changes: 41 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';
import t, { Command as TabCommand, type RootCommand, OptionHandler } 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" and "OptionHandler" 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,50 @@
});
}

// Now we have added complete and completion command...
// Process the root command
processRootCommand(instance);

// Process all subcommands
processSubcommands(instance);

return t;
}

/**
* 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('[');
}
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;
const choices = option.argChoices ?? [];

let optionHandler: OptionHandler | undefined = undefined;
if (optionTakesValue && choices.length > 0) {
optionHandler = (complete) => {
for (const choice of choices) complete(choice, '');
};
} else if (optionTakesValue) {
optionHandler = () => {};
}

/**
* 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);
if (optionHandler) {
t.option(longName, option.description, optionHandler, 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 +173,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 +188,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
28 changes: 19 additions & 9 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 All @@ -845,16 +851,16 @@ exports[`cli completion tests for commander > root command argument tests > shou
`;

exports[`cli completion tests for commander > root command option tests > should complete root command --logLevel option values 1`] = `
"info Info level
warn Warn level
error Error level
silent Silent level
"info
warn
error
silent
:4
"
`;

exports[`cli completion tests for commander > root command option tests > should complete root command --logLevel option with partial input 1`] = `
"info Info level
"info
:4
"
`;
Expand All @@ -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 All @@ -888,10 +895,10 @@ exports[`cli completion tests for commander > root command option tests > should
`;

exports[`cli completion tests for commander > root command option tests > should complete root command short flag -l option values 1`] = `
"info Info level
warn Warn level
error Error level
silent Silent level
"info
warn
error
silent
: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