···6677Additionally, tab supports autocompletions for `pnpm`, `npm`, `yarn`, and `bun`.
8899-Modern CLI libraries like [Gunshi](https://github.com/kazupon/gunshi) include tab completion natively in their core.
99+Tab has already been adopted by major tools and CLI frameworks, including:
1010+1111+<table align="center">
1212+ <tr>
1313+ <td align="center">
1414+ <a href="https://www.cloudflare.com/">
1515+ <img src="https://github.com/cloudflare.png?size=200" alt="Cloudflare" width="64"><br>
1616+ Cloudflare
1717+ </a>
1818+ </td>
1919+ <td align="center">
2020+ <a href="https://nuxt.com/">
2121+ <img src="https://github.com/nuxt.png?size=200" alt="Nuxt" width="64"><br>
2222+ Nuxt
2323+ </a>
2424+ </td>
2525+ <td align="center">
2626+ <a href="https://astro.build/">
2727+ <img src="https://github.com/withastro.png?size=200" alt="Astro" width="64"><br>
2828+ Astro
2929+ </a>
3030+ </td>
3131+ <td align="center">
3232+ <a href="https://vitest.dev/">
3333+ <img src="https://github.com/vitest-dev.png?size=200" alt="Vitest" width="64"><br>
3434+ Vitest
3535+ </a>
3636+ </td>
3737+ <td align="center">
3838+ <a href="https://github.com/kazupon/gunshi">
3939+ <img src="https://raw.githubusercontent.com/kazupon/gunshi/main/assets/logo.png" alt="Gunshi" width="64"><br>
4040+ Gunshi
4141+ </a>
4242+ </td>
4343+ <td align="center">
4444+ <a href="https://github.com/clercjs/clerc">
4545+ <img src="https://raw.githubusercontent.com/clercjs/clerc/main/docs/public/logo.webp" alt="Clerc" width="64"><br>
4646+ Clerc
4747+ </a>
4848+ </td>
4949+ </tr>
5050+</table>
10511152As CLI tooling authors, if we can spare our users a second or two by not checking documentation or writing the `-h` flag, we're doing them a huge favor. The unconscious mind loves hitting the [TAB] key and always expects feedback. When nothing happens, it breaks the user's flow - a frustration apparent across the whole JavaScript CLI tooling ecosystem.
1253···220261program.parse();
221262```
222263264264+The Commander integration supports customising the command name to generate the shell completion script. The default is `complete`. If you use a custom name
265265+like `completion` then it will be visible in the help as `completion <shell>`, while the runtime suggestions will be hidden (`complete -- [args...]`).
266266+You'll need to use your custom command when following examples on this page to generate the shell completion script.
267267+268268+```javascript
269269+const completion = tab(program, { completionCommandName: 'completion' });
270270+```
271271+223272## Bring Your Own Completion Logic
224273225274If your CLI framework already implements the logic for figuring out what to
···275324A working integration with [stricli](https://github.com/bloomberg/stricli) is
276325in `examples/demo.stricli.ts`.
277326278278----
327327+### Custom Integrations
279328280329tab uses a standardized completion protocol that any CLI can implement:
281330
···11-import * as zsh from './zsh';
22-import * as bash from './bash';
33-import * as fish from './fish';
44-import * as powershell from './powershell';
55-import type { Command as CommanderCommand, ParseOptions } from 'commander';
66-import t, { type RootCommand } from './t';
77-import { assertDoubleDashes } from './shared';
11+import type { Command as CommanderCommand } from 'commander';
22+import t, { Command as TabCommand, type RootCommand, OptionHandler } from './t';
33+44+// rawArgs is available on (just) the Commander root command, but is not included in the TypeScript types.
55+interface CommandWithRawArgs extends CommanderCommand {
66+ rawArgs: string[];
77+}
8899const execPath = process.execPath;
1010const processArgs = process.argv.slice(1);
···1818 return path.includes(' ') ? `'${path}'` : path;
1919}
20202121-export default function tab(instance: CommanderCommand): RootCommand {
2121+export default function tab(
2222+ instance: CommanderCommand,
2323+ completionConfig?: { completionCommandName?: string }
2424+): RootCommand {
2225 const programName = instance.name();
23262424- // Process the root command
2525- processRootCommand(instance);
2626-2727- // Process all subcommands
2828- processSubcommands(instance);
2929-3030- // Add the complete command for normal shell script generation
3131- instance
3232- .command('complete [shell]')
2727+ // Make a `completion` command with a required command-argument.
2828+ const completionCommandName =
2929+ completionConfig?.completionCommandName ?? 'complete';
3030+ const completionCommand = instance
3131+ .createCommand(completionCommandName)
3332 .description('Generate shell completion scripts')
3434- .action(async (shell) => {
3535- switch (shell) {
3636- case 'zsh': {
3737- const script = zsh.generate(programName, x);
3838- console.log(script);
3939- break;
4040- }
4141- case 'bash': {
4242- const script = bash.generate(programName, x);
4343- console.log(script);
4444- break;
4545- }
4646- case 'fish': {
4747- const script = fish.generate(programName, x);
4848- console.log(script);
4949- break;
5050- }
5151- case 'powershell': {
5252- const script = powershell.generate(programName, x);
5353- console.log(script);
5454- break;
5555- }
5656- case 'debug': {
5757- // Debug mode to print all collected commands
5858- const commandMap = new Map<string, CommanderCommand>();
5959- collectCommands(instance, '', commandMap);
6060- console.log('Collected commands:');
6161- for (const [path, cmd] of commandMap.entries()) {
6262- console.log(
6363- `- ${path || '<root>'}: ${cmd.description() || 'No description'}`
6464- );
6565- }
6666- break;
6767- }
6868- default: {
6969- console.error(`Unknown shell: ${shell}`);
7070- console.error('Supported shells: zsh, bash, fish, powershell');
7171- process.exit(1);
7272- }
3333+ .addArgument(
3434+ instance
3535+ .createArgument('<shell>', 'Shell type for completion script')
3636+ .choices(['zsh', 'bash', 'fish', 'powershell'])
3737+ )
3838+ .action((shell) => {
3939+ t.setup(programName, x, shell);
4040+ });
4141+ completionCommand.copyInheritedSettings(instance);
4242+4343+ // Make a `complete` command for generating tab-time complete suggestions.
4444+ const completeCommand = instance
4545+ .createCommand('complete')
4646+ .description('Generate completion suggestions')
4747+ .usage('-- [args...]')
4848+ .argument('[args...]')
4949+ .action((args) => {
5050+ if (completionCommandName !== 'complete') {
5151+ // Check for user trying to generate shell completion script, since not using usual tab overloaded complete `command`.
5252+ const rawArgs = (instance as CommandWithRawArgs).rawArgs;
5353+ if (args.length === 1 && !rawArgs.includes('--'))
5454+ instance.error(
5555+ `error: completion requests are called like \`complete -- [args]\`.\n(Did you mean \`${completionCommandName} ${args[0]}\` to generate shell script?)`
5656+ );
7357 }
5858+5959+ t.parse(args);
7460 });
6161+ completeCommand.copyInheritedSettings(instance);
75627676- // Override the parse method to handle completion requests before normal parsing
7777- const originalParse = instance.parse.bind(instance);
7878- instance.parse = function (argv?: readonly string[], options?: ParseOptions) {
7979- const args = argv || process.argv;
8080- const completeIndex = args.findIndex((arg) => arg === 'complete');
8181- const dashDashIndex = args.findIndex((arg) => arg === '--');
6363+ if (completionCommandName !== 'complete') {
6464+ // We have indepdendent commands so can hook them up directly.
6565+ instance.addCommand(completionCommand);
6666+ instance.addCommand(completeCommand, { hidden: true });
6767+ } else {
6868+ // We need to add a dual-use command, work out calling pattern, and dispatch.
6969+ instance
7070+ .command('complete')
7171+ .description('Generate shell completion scripts')
7272+ .argument(
7373+ '[shell]',
7474+ 'shell type (choices: "zsh", "bash", "fish", "powershell")'
7575+ )
7676+ .allowExcessArguments()
7777+ .action((_shell, _options, cmd) => {
7878+ // Work out how we are being called, by user or by script as completion handler.
7979+ const rawArgs = (instance as CommandWithRawArgs).rawArgs;
8080+ const completeIndex = rawArgs.indexOf('complete');
8181+ const dashDashIndex = rawArgs.indexOf('--');
82828383- if (
8484- completeIndex !== -1 &&
8585- dashDashIndex !== -1 &&
8686- dashDashIndex > completeIndex
8787- ) {
8888- // This is a completion request, handle it directly
8989- const extra = args.slice(dashDashIndex + 1);
8383+ if (
8484+ completeIndex !== -1 &&
8585+ dashDashIndex !== -1 &&
8686+ dashDashIndex === completeIndex + 1
8787+ ) {
8888+ // Commander stripped `--`, so put it back for reparse
8989+ completeCommand.parse(['--', ...cmd.args], { from: 'user' });
9090+ } else {
9191+ completionCommand.parse(cmd.args, {
9292+ from: 'user',
9393+ });
9494+ }
9595+ });
9696+ }
90979191- // Handle the completion directly
9292- assertDoubleDashes(programName);
9393- t.parse(extra);
9494- return instance;
9595- }
9898+ // Now we have added complete and completion command...
9999+ // Process the root command
100100+ processRootCommand(instance);
961019797- // Normal parsing
9898- return originalParse(argv, options);
9999- };
102102+ // Process all subcommands
103103+ processSubcommands(instance);
100104101105 return t;
102106}
103107104104-/**
105105- * Detect whether a commander option flag expects a value argument.
106106- * Options with `<value>` or `[value]` in their flags are value-taking.
107107- */
108108-function optionTakesValue(flags: string): boolean {
109109- return flags.includes('<') || flags.includes('[');
110110-}
108108+function processOptions(t: TabCommand, cmd: CommanderCommand): void {
109109+ // visibleOptions handles hidden options and built-in help option
110110+ const visibleOptions = cmd.createHelp().visibleOptions(cmd);
111111+ for (const option of visibleOptions) {
112112+ // Commander has at least one of short and long option flags, but can have just one.
113113+ // Commander also allows special case, shortish long and long like '--ws, --workspace'.
114114+ // Remove the leading dashes to get the names.
115115+ let shortName = option.short?.slice(1);
116116+ if (shortName && shortName[0] === '-') shortName = undefined; // ignore shortish long
117117+ const longName = option.long?.slice(2);
118118+ if (longName) {
119119+ const optionTakesValue = option.required || option.optional;
120120+ const choices = option.argChoices ?? [];
111121112112-/**
113113- * Register a commander option with the tab library, correctly setting
114114- * isBoolean based on whether the option takes a value.
115115- *
116116- * The tab Command.option() method infers isBoolean from the argument types:
117117- * - string arg → alias, isBoolean=true
118118- * - function arg → handler, isBoolean=false
119119- * So for value-taking options with an alias, we pass a no-op handler
120120- * and the alias separately to get isBoolean=false.
121121- */
122122-function registerOption(
123123- tabCommand: {
124124- option: (
125125- value: string,
126126- description: string,
127127- handlerOrAlias?: ((...args: unknown[]) => void) | string,
128128- alias?: string
129129- ) => unknown;
130130- },
131131- flags: string,
132132- longFlag: string,
133133- description: string,
134134- shortFlag?: string
135135-): void {
136136- const takesValue = optionTakesValue(flags);
137137- if (shortFlag) {
138138- if (takesValue) {
139139- // Pass a no-op handler to force isBoolean=false, with alias as 4th arg
140140- tabCommand.option(longFlag, description, () => {}, shortFlag);
141141- } else {
142142- tabCommand.option(longFlag, description, shortFlag);
143143- }
144144- } else {
145145- if (takesValue) {
146146- tabCommand.option(longFlag, description, () => {});
147147- } else {
148148- tabCommand.option(longFlag, description);
122122+ let optionHandler: OptionHandler | undefined = undefined;
123123+ if (optionTakesValue && choices.length > 0) {
124124+ optionHandler = (complete) => {
125125+ for (const choice of choices) complete(choice, '');
126126+ };
127127+ } else if (optionTakesValue) {
128128+ optionHandler = () => {};
129129+ }
130130+131131+ if (optionHandler) {
132132+ t.option(longName, option.description, optionHandler, shortName);
133133+ } else {
134134+ t.option(longName, option.description, shortName);
135135+ }
149136 }
150137 }
151138}
152139153140function processRootCommand(command: CommanderCommand): void {
154154- // Add root command options to the root t instance
155155- for (const option of command.options) {
156156- // Extract short flag from the name if it exists (e.g., "-c, --config" -> "c")
157157- const flags = option.flags;
158158- const shortFlag = flags.match(/^-([a-zA-Z]), --/)?.[1];
159159- const longFlag = flags.match(/--([a-zA-Z0-9-]+)/)?.[1];
141141+ processOptions(t, command);
142142+ processArguments(t, command);
143143+}
160144161161- if (longFlag) {
162162- registerOption(t, flags, longFlag, option.description || '', shortFlag);
145145+function processArguments(tabCommand: TabCommand, cmd: CommanderCommand): void {
146146+ for (const arg of cmd.registeredArguments) {
147147+ const choices = arg.argChoices;
148148+ if (choices?.length) {
149149+ tabCommand.argument(
150150+ arg.name(),
151151+ (complete) => {
152152+ for (const choice of choices) complete(choice, '');
153153+ },
154154+ arg.variadic
155155+ );
156156+ } else {
157157+ tabCommand.argument(arg.name(), undefined, arg.variadic);
163158 }
164159 }
165160}
···178173 // Add command using t.ts API
179174 const command = t.command(path, cmd.description() || '');
180175181181- // Add command options
182182- for (const option of cmd.options) {
183183- // Extract short flag from the name if it exists (e.g., "-c, --config" -> "c")
184184- const flags = option.flags;
185185- const shortFlag = flags.match(/^-([a-zA-Z]), --/)?.[1];
186186- const longFlag = flags.match(/--([a-zA-Z0-9-]+)/)?.[1];
187187-188188- if (longFlag) {
189189- registerOption(
190190- command,
191191- flags,
192192- longFlag,
193193- option.description || '',
194194- shortFlag
195195- );
196196- }
197197- }
176176+ // Add command options and arguments
177177+ processOptions(command, cmd);
178178+ processArguments(command, cmd);
198179 }
199180}
200181···207188 commandMap.set(parentPath, command);
208189209190 // Process subcommands
210210- for (const subcommand of command.commands) {
211211- // Skip the completion command
212212- if (subcommand.name() === 'complete') continue;
213213-191191+ // visibleCommands handles hidden commands and built-in help command
192192+ const visibleCommands = command.createHelp().visibleCommands(command);
193193+ for (const subcommand of visibleCommands) {
214194 // Build the full path for this subcommand
215195 const subcommandPath = parentPath
216196 ? `${parentPath} ${subcommand.name()}`
+4-2
src/powershell.ts
···9393 # Remove the flag part
9494 $Flag, $WordToComplete = $WordToComplete.Split("=", 2)
9595 }
9696-9797- if ( $WordToComplete -eq "" -And ( -Not $IsEqualFlag )) {
9696+ $HasTrailingEmptyArg = $QuotedArgs -match "(^| )''$"
9797+ __${name}_debug "HasTrailingEmptyArg: $HasTrailingEmptyArg"
9898+9999+ if ( $WordToComplete -eq "" -And ( -Not $IsEqualFlag ) -And ( -Not $HasTrailingEmptyArg )) {
98100 # If the last parameter is complete (there is a space following it)
99101 # We add an extra empty parameter so we can indicate this to the go method.
100102 __${name}_debug "Adding extra empty parameter"
+1-1
src/zsh.ts
···48484949 # Prepare the command to obtain completions, ensuring arguments are quoted for eval
5050 local -a args_to_quote=("\${(@)words[2,-1]}")
5151- if [ "\${lastChar}" = "" ]; then
5151+ if [ "\${lastChar}" = "" ] && [ "\${args_to_quote[-1]}" != "" ]; then
5252 # If the last parameter is complete (there is a space following it)
5353 # We add an extra empty parameter so we can indicate this to the go completion code.
5454 __${name}_debug "Adding extra empty parameter"
+325
tests/__snapshots__/cli.test.ts.snap
···626626"
627627`;
628628629629+exports[`cli completion tests for commander > --config option tests > should complete --config option values 1`] = `
630630+"vite.config.ts Vite config file
631631+vite.config.js Vite config file
632632+:4
633633+"
634634+`;
635635+636636+exports[`cli completion tests for commander > --config option tests > should complete --config option with equals sign 1`] = `
637637+"vite.config.ts Vite config file
638638+vite.config.js Vite config file
639639+:4
640640+"
641641+`;
642642+643643+exports[`cli completion tests for commander > --config option tests > should complete --config option with partial input 1`] = `
644644+"vite.config.ts Vite config file
645645+vite.config.js Vite config file
646646+:4
647647+"
648648+`;
649649+650650+exports[`cli completion tests for commander > --config option tests > should complete short flag -c option values 1`] = `
651651+"vite.config.ts Vite config file
652652+vite.config.js Vite config file
653653+:4
654654+"
655655+`;
656656+657657+exports[`cli completion tests for commander > --config option tests > should complete short flag -c option with partial input 1`] = `
658658+"vite.config.ts Vite config file
659659+vite.config.js Vite config file
660660+:4
661661+"
662662+`;
663663+664664+exports[`cli completion tests for commander > --config option tests > should not suggest --config after it has been used 1`] = `
665665+"--version output the version number
666666+--config Use specified config file
667667+--mode Set env mode
668668+--logLevel Specify log level
669669+--help display help for command
670670+:4
671671+"
672672+`;
673673+674674+exports[`cli completion tests for commander > cli option completion tests > should complete option for partial input '{ partial: '--p', expected: '--port' }' 1`] = `
675675+"--port Specify port
676676+:4
677677+"
678678+`;
679679+680680+exports[`cli completion tests for commander > cli option completion tests > should complete option for partial input '{ partial: '-H', expected: '-H' }' 1`] = `
681681+"-H Specify hostname
682682+:4
683683+"
684684+`;
685685+686686+exports[`cli completion tests for commander > cli option completion tests > should complete option for partial input '{ partial: '-p', expected: '-p' }' 1`] = `
687687+"-p Specify port
688688+:4
689689+"
690690+`;
691691+692692+exports[`cli completion tests for commander > cli option exclusion tests > should not suggest already specified option '{ specified: '--config', shouldNotContain: '--config' }' 1`] = `
693693+":4
694694+"
695695+`;
696696+697697+exports[`cli completion tests for commander > cli option value handling > should handle unknown options with no completions 1`] = `":4"`;
698698+699699+exports[`cli completion tests for commander > cli option value handling > should not show duplicate options 1`] = `
700700+"--version output the version number
701701+--config Use specified config file
702702+--mode Set env mode
703703+--logLevel Specify log level
704704+--help display help for command
705705+:4
706706+"
707707+`;
708708+709709+exports[`cli completion tests for commander > cli option value handling > should resolve config option values correctly 1`] = `
710710+"vite.config.ts Vite config file
711711+vite.config.js Vite config file
712712+:4
713713+"
714714+`;
715715+716716+exports[`cli completion tests for commander > cli option value handling > should resolve port value correctly 1`] = `
717717+"3000 Development server port
718718+:4
719719+"
720720+`;
721721+722722+exports[`cli completion tests for commander > copy command argument handlers > should complete destination argument with build suggestions 1`] = `
723723+"build/ Build output
724724+release/ Release directory
725725+backup/ Backup location
726726+:4
727727+"
728728+`;
729729+730730+exports[`cli completion tests for commander > copy command argument handlers > should complete source argument with directory suggestions 1`] = `
731731+"src/ Source directory
732732+dist/ Distribution directory
733733+public/ Public assets
734734+:4
735735+"
736736+`;
737737+738738+exports[`cli completion tests for commander > copy command argument handlers > should filter destination suggestions when typing partial input 1`] = `
739739+"build/ Build output
740740+backup/ Backup location
741741+:4
742742+"
743743+`;
744744+745745+exports[`cli completion tests for commander > copy command argument handlers > should filter source suggestions when typing partial input 1`] = `
746746+"src/ Source directory
747747+:4
748748+"
749749+`;
750750+751751+exports[`cli completion tests for commander > edge case completions for end with space > should keep suggesting the --port option if user typed partial but didn't end with space 1`] = `
752752+"--port Specify port
753753+:4
754754+"
755755+`;
756756+757757+exports[`cli completion tests for commander > edge case completions for end with space > should suggest port values if user ends with space after \`--port\` 1`] = `
758758+"3000 Development server port
759759+8080 Alternative port
760760+:4
761761+"
762762+`;
763763+764764+exports[`cli completion tests for commander > edge case completions for end with space > should suggest port values if user typed \`--port=\` and hasn't typed a space or value yet 1`] = `
765765+"3000 Development server port
766766+8080 Alternative port
767767+:4
768768+"
769769+`;
770770+771771+exports[`cli completion tests for commander > lint command argument handlers > should complete files argument with file suggestions 1`] = `
772772+"main.ts Main file
773773+index.ts Index file
774774+:4
775775+"
776776+`;
777777+778778+exports[`cli completion tests for commander > lint command argument handlers > should continue completing variadic files argument after first file 1`] = `
779779+"main.ts Main file
780780+index.ts Index file
781781+:4
782782+"
783783+`;
784784+785785+exports[`cli completion tests for commander > lint command argument handlers > should continue completing variadic suggestions after first file 1`] = `
786786+"index.ts Index file
787787+:4
788788+"
789789+`;
790790+791791+exports[`cli completion tests for commander > lint command argument handlers > should filter file suggestions when typing partial input 1`] = `
792792+"main.ts Main file
793793+:4
794794+"
795795+`;
796796+797797+exports[`cli completion tests for commander > positional argument completions > should complete multiple positional arguments when ending with part of the value 1`] = `
798798+"index.ts Index file
799799+:4
800800+"
801801+`;
802802+803803+exports[`cli completion tests for commander > positional argument completions > should complete multiple positional arguments when ending with space 1`] = `
804804+"main.ts Main file
805805+index.ts Index file
806806+:4
807807+"
808808+`;
809809+810810+exports[`cli completion tests for commander > positional argument completions > should complete single positional argument when ending with space 1`] = `
811811+"main.ts Main file
812812+index.ts Index file
813813+:4
814814+"
815815+`;
816816+817817+exports[`cli completion tests for commander > root command argument tests > should complete root command project argument 1`] = `
818818+"dev Start dev server
819819+serve Start the server
820820+build Build the project
821821+deploy Deploy the application
822822+lint Lint source files
823823+copy Copy files
824824+complete Generate shell completion scripts
825825+help display help for command
826826+:4
827827+"
828828+`;
829829+830830+exports[`cli completion tests for commander > root command argument tests > should complete root command project argument after options 1`] = `
831831+"dev Start dev server
832832+serve Start the server
833833+build Build the project
834834+deploy Deploy the application
835835+lint Lint source files
836836+copy Copy files
837837+complete Generate shell completion scripts
838838+help display help for command
839839+:4
840840+"
841841+`;
842842+843843+exports[`cli completion tests for commander > root command argument tests > should complete root command project argument with options and partial input 1`] = `
844844+":4
845845+"
846846+`;
847847+848848+exports[`cli completion tests for commander > root command argument tests > should complete root command project argument with partial input 1`] = `
849849+":4
850850+"
851851+`;
852852+853853+exports[`cli completion tests for commander > root command option tests > should complete root command --logLevel option values 1`] = `
854854+"info
855855+warn
856856+error
857857+silent
858858+:4
859859+"
860860+`;
861861+862862+exports[`cli completion tests for commander > root command option tests > should complete root command --logLevel option with partial input 1`] = `
863863+"info
864864+:4
865865+"
866866+`;
867867+868868+exports[`cli completion tests for commander > root command option tests > should complete root command --mode option values 1`] = `
869869+"development Development mode
870870+production Production mode
871871+:4
872872+"
873873+`;
874874+875875+exports[`cli completion tests for commander > root command option tests > should complete root command --mode option with partial input 1`] = `
876876+"development Development mode
877877+:4
878878+"
879879+`;
880880+881881+exports[`cli completion tests for commander > root command option tests > should complete root command options after project argument 1`] = `
882882+"--version output the version number
883883+--config Use specified config file
884884+--mode Set env mode
885885+--logLevel Specify log level
886886+--help display help for command
887887+:4
888888+"
889889+`;
890890+891891+exports[`cli completion tests for commander > root command option tests > should complete root command options with partial input after project argument 1`] = `
892892+"--mode Set env mode
893893+:4
894894+"
895895+`;
896896+897897+exports[`cli completion tests for commander > root command option tests > should complete root command short flag -l option values 1`] = `
898898+"info
899899+warn
900900+error
901901+silent
902902+:4
903903+"
904904+`;
905905+906906+exports[`cli completion tests for commander > root command option tests > should complete root command short flag -m option values 1`] = `
907907+"development Development mode
908908+production Production mode
909909+:4
910910+"
911911+`;
912912+913913+exports[`cli completion tests for commander > short flag handling > should handle global short flags 1`] = `
914914+"-c Use specified config file
915915+:4
916916+"
917917+`;
918918+919919+exports[`cli completion tests for commander > short flag handling > should handle short flag value completion 1`] = `
920920+"-p Specify port
921921+:4
922922+"
923923+`;
924924+925925+exports[`cli completion tests for commander > short flag handling > should handle short flag with equals sign 1`] = `
926926+"3000 Development server port
927927+:4
928928+"
929929+`;
930930+931931+exports[`cli completion tests for commander > short flag handling > should not show duplicate options when short flag is used 1`] = `
932932+"--version output the version number
933933+--config Use specified config file
934934+--mode Set env mode
935935+--logLevel Specify log level
936936+--help display help for command
937937+:4
938938+"
939939+`;
940940+941941+exports[`cli completion tests for commander > should complete cli options 1`] = `
942942+"dev Start dev server
943943+serve Start the server
944944+build Build the project
945945+deploy Deploy the application
946946+lint Lint source files
947947+copy Copy files
948948+complete Generate shell completion scripts
949949+help display help for command
950950+:4
951951+"
952952+`;
953953+629954exports[`cli completion tests for t > --config option tests > should complete --config option values 1`] = `
630955"vite.config.ts Vite config file
631956vite.config.js Vite config file
+44-47
tests/cli.test.ts
···1616const cliTools = ['t', 'citty', 'cac', 'commander'];
17171818describe.each(cliTools)('cli completion tests for %s', (cliTool) => {
1919- // For Commander, we need to skip most of the tests since it handles completion differently
2020- const shouldSkipTest = cliTool === 'commander';
2121-2222- // Commander uses a different command structure for completion
2323- // TODO: why commander does that? our convention is the -- part which should be always there.
2424- const commandPrefix =
2525- cliTool === 'commander'
2626- ? `pnpm tsx examples/demo.${cliTool}.ts complete`
2727- : `pnpm tsx examples/demo.${cliTool}.ts complete --`;
1919+ const commandPrefix = `pnpm tsx examples/demo.${cliTool}.ts complete --`;
28202929- it.runIf(!shouldSkipTest)('should complete cli options', async () => {
2121+ it('should complete cli options', async () => {
3022 const output = await runCommand(`${commandPrefix}`);
3123 expect(output).toMatchSnapshot();
3224 });
33253434- describe.runIf(!shouldSkipTest)('cli option completion tests', () => {
2626+ describe('cli option completion tests', () => {
3527 const optionTests = [
3628 { partial: '--p', expected: '--port' },
3729 { partial: '-p', expected: '-p' }, // Test short flag completion
···4840 );
4941 });
50425151- describe.runIf(!shouldSkipTest)('cli option exclusion tests', () => {
4343+ describe('cli option exclusion tests', () => {
5244 const alreadySpecifiedTests = [
5345 { specified: '--config', shouldNotContain: '--config' },
5446 ];
···6355 );
6456 });
65576666- describe.runIf(!shouldSkipTest)('cli option value handling', () => {
5858+ describe('cli option value handling', () => {
6759 it('should resolve port value correctly', async () => {
6860 const command = `${commandPrefix} dev --port=3`;
6961 const output = await runCommand(command);
···8981 });
9082 });
91839292- describe.runIf(!shouldSkipTest)('boolean option handling', () => {
8484+ describe('boolean option handling', () => {
9385 it('should complete subcommands and arguments after boolean options', async () => {
9486 const command = `${commandPrefix} dev --verbose ""`;
9587 const output = await runCommand(command);
···118110 it('should not interfere with option completion after boolean options', async () => {
119111 const command = `${commandPrefix} dev --verbose --h`;
120112 const output = await runCommand(command);
121121- // Should complete subcommands that start with 's' even after a boolean option
113113+ // Should complete options that start with '--h' even after a boolean option
122114 expect(output).toContain('--host');
123115 });
124116 });
125117126126- describe.runIf(!shouldSkipTest)('option API overload tests', () => {
118118+ describe('option API overload tests', () => {
127119 it('should handle basic option (name + description only) as boolean flag', async () => {
128120 // This tests the case: option('quiet', 'Suppress output')
129121 const command = `${commandPrefix} dev --quiet ""`;
···197189 });
198190 });
199191200200- describe.runIf(!shouldSkipTest)('--config option tests', () => {
192192+ describe('--config option tests', () => {
201193 it('should complete --config option values', async () => {
202194 const command = `${commandPrefix} --config ""`;
203195 const output = await runCommand(command);
···235227 });
236228 });
237229238238- describe.runIf(!shouldSkipTest)('root command argument tests', () => {
230230+ describe('root command argument tests', () => {
239231 it('should complete root command project argument', async () => {
240232 const command = `${commandPrefix} ""`;
241233 const output = await runCommand(command);
···261253 });
262254 });
263255264264- describe.runIf(!shouldSkipTest)('root command option tests', () => {
256256+ describe('root command option tests', () => {
265257 it('should complete root command --mode option values', async () => {
266258 const command = `${commandPrefix} --mode ""`;
267259 const output = await runCommand(command);
···311303 });
312304 });
313305314314- describe.runIf(!shouldSkipTest)(
315315- 'edge case completions for end with space',
316316- () => {
317317- it('should suggest port values if user ends with space after `--port`', async () => {
318318- const command = `${commandPrefix} dev --port ""`;
319319- const output = await runCommand(command);
320320- expect(output).toMatchSnapshot();
321321- });
306306+ describe('edge case completions for end with space', () => {
307307+ it('should suggest port values if user ends with space after `--port`', async () => {
308308+ const command = `${commandPrefix} dev --port ""`;
309309+ const output = await runCommand(command);
310310+ expect(output).toMatchSnapshot();
311311+ });
322312323323- it("should keep suggesting the --port option if user typed partial but didn't end with space", async () => {
324324- const command = `${commandPrefix} dev --po`;
325325- const output = await runCommand(command);
326326- expect(output).toMatchSnapshot();
327327- });
313313+ it("should keep suggesting the --port option if user typed partial but didn't end with space", async () => {
314314+ const command = `${commandPrefix} dev --po`;
315315+ const output = await runCommand(command);
316316+ expect(output).toMatchSnapshot();
317317+ });
328318329329- it("should suggest port values if user typed `--port=` and hasn't typed a space or value yet", async () => {
330330- const command = `${commandPrefix} dev --port=`;
331331- const output = await runCommand(command);
332332- expect(output).toMatchSnapshot();
333333- });
334334- }
335335- );
319319+ it("should suggest port values if user typed `--port=` and hasn't typed a space or value yet", async () => {
320320+ const command = `${commandPrefix} dev --port=`;
321321+ const output = await runCommand(command);
322322+ expect(output).toMatchSnapshot();
323323+ });
324324+ });
336325337337- describe.runIf(!shouldSkipTest)('short flag handling', () => {
326326+ describe('short flag handling', () => {
338327 it('should handle short flag value completion', async () => {
339328 const command = `${commandPrefix} dev -p `;
340329 const output = await runCommand(command);
···360349 });
361350 });
362351363363- describe.runIf(!shouldSkipTest)('positional argument completions', () => {
352352+ describe('positional argument completions', () => {
364353 it('should complete multiple positional arguments when ending with space', async () => {
365354 const command = `${commandPrefix} lint ""`;
366355 const output = await runCommand(command);
···380369 });
381370 });
382371383383- describe.runIf(!shouldSkipTest)('copy command argument handlers', () => {
372372+ describe('copy command argument handlers', () => {
384373 it('should complete source argument with directory suggestions', async () => {
385374 const command = `${commandPrefix} copy ""`;
386375 const output = await runCommand(command);
···406395 });
407396 });
408397409409- describe.runIf(!shouldSkipTest)('lint command argument handlers', () => {
398398+ describe('lint command argument handlers', () => {
410399 it('should complete files argument with file suggestions', async () => {
411400 const command = `${commandPrefix} lint ""`;
412401 const output = await runCommand(command);
···444433 });
445434446435 it('should handle subcommands', async () => {
447447- // First, we need to check if deploy is recognized as a command
436436+ // Check subcommands of root command.
448437 const command1 = `pnpm tsx examples/demo.commander.ts complete -- deploy`;
449438 const output1 = await runCommand(command1);
450439 expect(output1).toContain('deploy');
451440 expect(output1).toContain('Deploy the application');
452441453453- // Then we need to check if the deploy command has subcommands
454454- // We can check this by running the deploy command with --help
455455- const command2 = `pnpm tsx examples/demo.commander.ts deploy --help`;
442442+ // Check subcommands of subcommand.
443443+ const command2 = `pnpm tsx examples/demo.commander.ts complete -- deploy ""`;
456444 const output2 = await runCommand(command2);
457445 expect(output2).toContain('staging');
458446 expect(output2).toContain('production');
447447+ });
448448+449449+ it('should intercept completion when using parseAsync', async () => {
450450+ const command = `pnpm tsx examples/demo.commander-async.ts complete -- `;
451451+ const output = await runCommand(command);
452452+ expect(output).toContain('greet');
453453+ expect(output).toContain('Say hello');
454454+ // directive line must be present, proving t.parse was invoked
455455+ expect(output).toMatch(/:\d+\s*$/);
459456 });
460457});
461458