[READ-ONLY] Mirror of https://github.com/bombshell-dev/tab. shell autocompletions for javascript CLIs
4

Configure Feed

Select the types of activity you want to include in your feed.

feat(commander): use Commander for determining integration details (#131)

* Use Commander built-in support for determining option flags and types, visible options, and visible commands.

* Add automatic support for Commander option choices

* changeset

---------

Co-authored-by: AmirSa12 <amirhosseinpr184@gmail.com>

authored by

John Gee
AmirSa12
and committed by
GitHub
(Jun 15, 2026, 12:32 PM +0330) 314d836c 53ea5e17

+66 -103
+5
.changeset/free-doors-fix.md
··· 1 + --- 2 + '@bomb.sh/tab': patch 3 + --- 4 + 5 + feat(commander): use commander for determining integration details (#131)
+1 -9
examples/demo.commander.ts
··· 108 108 complete('production', 'Production mode'); 109 109 }; 110 110 } 111 - const logLevelOption = completion.options.get('logLevel'); 112 - if (logLevelOption) { 113 - logLevelOption.handler = (complete) => { 114 - complete('info', 'Info level'); 115 - complete('warn', 'Warn level'); 116 - complete('error', 'Error level'); 117 - complete('silent', 'Silent level'); 118 - }; 119 - } 111 + // Note: loglevel automatically gets completions because uses ".choices()" in option definition. 120 112 121 113 // Options on dev command 122 114 const devCommandInstance = completion.commands.get('dev');
+41 -85
src/commander.ts
··· 1 1 import type { Command as CommanderCommand } from 'commander'; 2 - import t, { Command as TabCommand, type RootCommand } from './t'; 2 + import t, { Command as TabCommand, type RootCommand, OptionHandler } from './t'; 3 3 4 4 // rawArgs is available on (just) the Commander root command, but is not included in the TypeScript types. 5 5 interface CommandWithRawArgs extends CommanderCommand { ··· 23 23 completionConfig?: { completionCommandName?: string } 24 24 ): RootCommand { 25 25 const programName = instance.name(); 26 - 27 - // Process the root command 28 - processRootCommand(instance); 29 - 30 - // Process all subcommands 31 - processSubcommands(instance); 32 26 33 27 // Make a `completion` command with a required command-argument. 34 28 const completionCommandName = ··· 101 95 }); 102 96 } 103 97 98 + // Now we have added complete and completion command... 99 + // Process the root command 100 + processRootCommand(instance); 101 + 102 + // Process all subcommands 103 + processSubcommands(instance); 104 + 104 105 return t; 105 106 } 106 107 107 - /** 108 - * Detect whether a commander option flag expects a value argument. 109 - * Options with `<value>` or `[value]` in their flags are value-taking. 110 - */ 111 - function optionTakesValue(flags: string): boolean { 112 - return flags.includes('<') || flags.includes('['); 113 - } 108 + function processOptions(t: TabCommand, cmd: CommanderCommand): void { 109 + // visibleOptions handles hidden options and built-in help option 110 + const visibleOptions = cmd.createHelp().visibleOptions(cmd); 111 + for (const option of visibleOptions) { 112 + // Commander has at least one of short and long option flags, but can have just one. 113 + // Commander also allows special case, shortish long and long like '--ws, --workspace'. 114 + // Remove the leading dashes to get the names. 115 + let shortName = option.short?.slice(1); 116 + if (shortName && shortName[0] === '-') shortName = undefined; // ignore shortish long 117 + const longName = option.long?.slice(2); 118 + if (longName) { 119 + const optionTakesValue = option.required || option.optional; 120 + const choices = option.argChoices ?? []; 114 121 115 - /** 116 - * Register a commander option with the tab library, correctly setting 117 - * isBoolean based on whether the option takes a value. 118 - * 119 - * The tab Command.option() method infers isBoolean from the argument types: 120 - * - string arg → alias, isBoolean=true 121 - * - function arg → handler, isBoolean=false 122 - * So for value-taking options with an alias, we pass a no-op handler 123 - * and the alias separately to get isBoolean=false. 124 - */ 125 - function registerOption( 126 - tabCommand: { 127 - option: ( 128 - value: string, 129 - description: string, 130 - handlerOrAlias?: ((...args: unknown[]) => void) | string, 131 - alias?: string 132 - ) => unknown; 133 - }, 134 - flags: string, 135 - longFlag: string, 136 - description: string, 137 - shortFlag?: string 138 - ): void { 139 - const takesValue = optionTakesValue(flags); 140 - if (shortFlag) { 141 - if (takesValue) { 142 - // Pass a no-op handler to force isBoolean=false, with alias as 4th arg 143 - tabCommand.option(longFlag, description, () => {}, shortFlag); 144 - } else { 145 - tabCommand.option(longFlag, description, shortFlag); 146 - } 147 - } else { 148 - if (takesValue) { 149 - tabCommand.option(longFlag, description, () => {}); 150 - } else { 151 - tabCommand.option(longFlag, description); 122 + let optionHandler: OptionHandler | undefined = undefined; 123 + if (optionTakesValue && choices.length > 0) { 124 + optionHandler = (complete) => { 125 + for (const choice of choices) complete(choice, ''); 126 + }; 127 + } else if (optionTakesValue) { 128 + optionHandler = () => {}; 129 + } 130 + 131 + if (optionHandler) { 132 + t.option(longName, option.description, optionHandler, shortName); 133 + } else { 134 + t.option(longName, option.description, shortName); 135 + } 152 136 } 153 137 } 154 138 } 155 139 156 140 function processRootCommand(command: CommanderCommand): void { 157 - // Add root command options to the root t instance 158 - for (const option of command.options) { 159 - // Extract short flag from the name if it exists (e.g., "-c, --config" -> "c") 160 - const flags = option.flags; 161 - const shortFlag = flags.match(/^-([a-zA-Z]), --/)?.[1]; 162 - const longFlag = flags.match(/--([a-zA-Z0-9-]+)/)?.[1]; 163 - 164 - if (longFlag) { 165 - registerOption(t, flags, longFlag, option.description || '', shortFlag); 166 - } 167 - } 168 - 141 + processOptions(t, command); 169 142 processArguments(t, command); 170 143 } 171 144 ··· 200 173 // Add command using t.ts API 201 174 const command = t.command(path, cmd.description() || ''); 202 175 203 - // Add command options 204 - for (const option of cmd.options) { 205 - // Extract short flag from the name if it exists (e.g., "-c, --config" -> "c") 206 - const flags = option.flags; 207 - const shortFlag = flags.match(/^-([a-zA-Z]), --/)?.[1]; 208 - const longFlag = flags.match(/--([a-zA-Z0-9-]+)/)?.[1]; 209 - 210 - if (longFlag) { 211 - registerOption( 212 - command, 213 - flags, 214 - longFlag, 215 - option.description || '', 216 - shortFlag 217 - ); 218 - } 219 - } 220 - 176 + // Add command options and arguments 177 + processOptions(command, cmd); 221 178 processArguments(command, cmd); 222 179 } 223 180 } ··· 231 188 commandMap.set(parentPath, command); 232 189 233 190 // Process subcommands 234 - for (const subcommand of command.commands) { 235 - // Skip the completion command 236 - if (subcommand.name() === 'complete') continue; 237 - 191 + // visibleCommands handles hidden commands and built-in help command 192 + const visibleCommands = command.createHelp().visibleCommands(command); 193 + for (const subcommand of visibleCommands) { 238 194 // Build the full path for this subcommand 239 195 const subcommandPath = parentPath 240 196 ? `${parentPath} ${subcommand.name()}`
+19 -9
tests/__snapshots__/cli.test.ts.snap
··· 666 666 --config Use specified config file 667 667 --mode Set env mode 668 668 --logLevel Specify log level 669 + --help display help for command 669 670 :4 670 671 " 671 672 `; ··· 700 701 --config Use specified config file 701 702 --mode Set env mode 702 703 --logLevel Specify log level 704 + --help display help for command 703 705 :4 704 706 " 705 707 `; ··· 819 821 deploy Deploy the application 820 822 lint Lint source files 821 823 copy Copy files 824 + complete Generate shell completion scripts 825 + help display help for command 822 826 :4 823 827 " 824 828 `; ··· 830 834 deploy Deploy the application 831 835 lint Lint source files 832 836 copy Copy files 837 + complete Generate shell completion scripts 838 + help display help for command 833 839 :4 834 840 " 835 841 `; ··· 845 851 `; 846 852 847 853 exports[`cli completion tests for commander > root command option tests > should complete root command --logLevel option values 1`] = ` 848 - "info Info level 849 - warn Warn level 850 - error Error level 851 - silent Silent level 854 + "info 855 + warn 856 + error 857 + silent 852 858 :4 853 859 " 854 860 `; 855 861 856 862 exports[`cli completion tests for commander > root command option tests > should complete root command --logLevel option with partial input 1`] = ` 857 - "info Info level 863 + "info 858 864 :4 859 865 " 860 866 `; ··· 877 883 --config Use specified config file 878 884 --mode Set env mode 879 885 --logLevel Specify log level 886 + --help display help for command 880 887 :4 881 888 " 882 889 `; ··· 888 895 `; 889 896 890 897 exports[`cli completion tests for commander > root command option tests > should complete root command short flag -l option values 1`] = ` 891 - "info Info level 892 - warn Warn level 893 - error Error level 894 - silent Silent level 898 + "info 899 + warn 900 + error 901 + silent 895 902 :4 896 903 " 897 904 `; ··· 926 933 --config Use specified config file 927 934 --mode Set env mode 928 935 --logLevel Specify log level 936 + --help display help for command 929 937 :4 930 938 " 931 939 `; ··· 937 945 deploy Deploy the application 938 946 lint Lint source files 939 947 copy Copy files 948 + complete Generate shell completion scripts 949 + help display help for command 940 950 :4 941 951 " 942 952 `;