Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/bombshell-dev/tab. shell autocompletions for javascript CLIs
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466import { exec } from 'child_process';import { describe, it, expect, test } from 'vitest';
function runCommand(command: string): Promise<string> { return new Promise((resolve, reject) => { exec(command, (error, stdout, stderr) => { if (error) { reject(stderr); } else { resolve(stdout); } }); });}
const cliTools = ['t', 'citty', 'cac', 'commander'];
describe.each(cliTools)('cli completion tests for %s', (cliTool) => { // For Commander, we need to skip most of the tests since it handles completion differently const shouldSkipTest = cliTool === 'commander';
// Commander uses a different command structure for completion // TODO: why commander does that? our convention is the -- part which should be always there. const commandPrefix = cliTool === 'commander' ? `pnpm tsx examples/demo.${cliTool}.ts complete` : `pnpm tsx examples/demo.${cliTool}.ts complete --`;
it.runIf(!shouldSkipTest)('should complete cli options', async () => { const output = await runCommand(`${commandPrefix}`); expect(output).toMatchSnapshot(); });
describe.runIf(!shouldSkipTest)('cli option completion tests', () => { const optionTests = [ { partial: '--p', expected: '--port' }, { partial: '-p', expected: '-p' }, // Test short flag completion { partial: '-H', expected: '-H' }, // Test another short flag completion ];
test.each(optionTests)( "should complete option for partial input '%s'", async ({ partial }) => { const command = `${commandPrefix} dev ${partial}`; const output = await runCommand(command); expect(output).toMatchSnapshot(); } ); });
describe.runIf(!shouldSkipTest)('cli option exclusion tests', () => { const alreadySpecifiedTests = [ { specified: '--config', shouldNotContain: '--config' }, ];
test.each(alreadySpecifiedTests)( "should not suggest already specified option '%s'", async ({ specified, shouldNotContain }) => { const command = `${commandPrefix} ${specified} --`; const output = await runCommand(command); expect(output).toMatchSnapshot(); } ); });
describe.runIf(!shouldSkipTest)('cli option value handling', () => { it('should resolve port value correctly', async () => { const command = `${commandPrefix} dev --port=3`; const output = await runCommand(command); expect(output).toMatchSnapshot(); });
it('should not show duplicate options', async () => { const command = `${commandPrefix} --config vite.config.js --`; const output = await runCommand(command); expect(output).toMatchSnapshot(); });
it('should resolve config option values correctly', async () => { const command = `${commandPrefix} --config vite.config`; const output = await runCommand(command); expect(output).toMatchSnapshot(); });
it('should handle unknown options with no completions', async () => { const command = `${commandPrefix} --unknownoption`; const output = await runCommand(command); expect(output.trim()).toMatchSnapshot(); }); });
describe.runIf(!shouldSkipTest)('boolean option handling', () => { it('should not provide value completions for boolean options', async () => { const command = `${commandPrefix} dev --verbose ""`; const output = await runCommand(command); // Boolean options should return just the directive, no completions expect(output.trim()).toBe(':4'); });
it('should not provide value completions for short boolean options', async () => { const command = `${commandPrefix} dev -v ""`; const output = await runCommand(command); // Boolean options should return just the directive, no completions expect(output.trim()).toBe(':4'); });
it('should not interfere with command completion after boolean options', async () => { const command = `${commandPrefix} dev --verbose s`; const output = await runCommand(command); // Should complete subcommands that start with 's' even after a boolean option expect(output).toContain('start'); });
it('should not interfere with option completion after boolean options', async () => { const command = `${commandPrefix} dev --verbose --h`; const output = await runCommand(command); // Should complete subcommands that start with 's' even after a boolean option expect(output).toContain('--host'); }); });
describe.runIf(!shouldSkipTest)('option API overload tests', () => { it('should handle basic option (name + description only) as boolean flag', async () => { // This tests the case: option('quiet', 'Suppress output') const command = `${commandPrefix} dev --quiet ""`; const output = await runCommand(command); // Should be treated as boolean flag (no value completion) expect(output.trim()).toBe(':4'); });
it('should handle option with alias only as boolean flag', async () => { // This tests the case: option('verbose', 'Enable verbose', 'v') const command = `${commandPrefix} dev --verbose ""`; const output = await runCommand(command); // Should be treated as boolean flag (no value completion) expect(output.trim()).toBe(':4'); });
it('should handle option with alias only (short flag) as boolean flag', async () => { // This tests the short flag version: -v instead of --verbose const command = `${commandPrefix} dev -v ""`; const output = await runCommand(command); // Should be treated as boolean flag (no value completion) expect(output.trim()).toBe(':4'); });
it('should handle option with handler only as value option', async () => { // This tests the case: option('port', 'Port number', handlerFunction) const command = `${commandPrefix} dev --port ""`; const output = await runCommand(command); // Should provide value completions because it has a handler expect(output).toContain('3000'); expect(output).toContain('8080'); });
it('should handle option with both handler and alias as value option', async () => { // This tests the case: option('config', 'Config file', handlerFunction, 'c') const command = `${commandPrefix} --config ""`; const output = await runCommand(command); // Should provide value completions because it has a handler expect(output).toContain('vite.config.ts'); expect(output).toContain('vite.config.js'); });
it('should handle option with both handler and alias (short flag) as value option', async () => { // This tests the short flag version with handler: -c instead of --config const command = `${commandPrefix} -c ""`; const output = await runCommand(command); // Should provide value completions because it has a handler expect(output).toContain('vite.config.ts'); expect(output).toContain('vite.config.js'); });
it('should correctly detect boolean vs value options in mixed scenarios', async () => { // Test that boolean options don't interfere with value options const command = `${commandPrefix} dev --verbose --port ""`; const output = await runCommand(command); // Should complete port values, not be confused by preceding boolean flag expect(output).toContain('3000'); expect(output).toContain('8080'); });
it('should correctly handle aliases for different option types', async () => { // Mix of boolean flag with alias (-v) and value option with alias (-p) const command = `${commandPrefix} dev -v -p ""`; const output = await runCommand(command); // Should complete port values via short flag expect(output).toContain('3000'); expect(output).toContain('8080'); }); });
describe.runIf(!shouldSkipTest)('--config option tests', () => { it('should complete --config option values', async () => { const command = `${commandPrefix} --config ""`; const output = await runCommand(command); expect(output).toMatchSnapshot(); });
it('should complete --config option with partial input', async () => { const command = `${commandPrefix} --config vite.config`; const output = await runCommand(command); expect(output).toMatchSnapshot(); });
it('should complete --config option with equals sign', async () => { const command = `${commandPrefix} --config=vite.config`; const output = await runCommand(command); expect(output).toMatchSnapshot(); });
it('should complete short flag -c option values', async () => { const command = `${commandPrefix} -c ""`; const output = await runCommand(command); expect(output).toMatchSnapshot(); });
it('should complete short flag -c option with partial input', async () => { const command = `${commandPrefix} -c vite.config`; const output = await runCommand(command); expect(output).toMatchSnapshot(); });
it('should not suggest --config after it has been used', async () => { const command = `${commandPrefix} --config vite.config.ts --`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); });
describe.runIf(!shouldSkipTest)('root command argument tests', () => { it('should complete root command project argument', async () => { const command = `${commandPrefix} ""`; const output = await runCommand(command); expect(output).toMatchSnapshot(); });
it('should complete root command project argument with partial input', async () => { const command = `${commandPrefix} my-`; const output = await runCommand(command); expect(output).toMatchSnapshot(); });
it('should complete root command project argument after options', async () => { const command = `${commandPrefix} --config vite.config.ts ""`; const output = await runCommand(command); expect(output).toMatchSnapshot(); });
it('should complete root command project argument with options and partial input', async () => { const command = `${commandPrefix} --mode development my-`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); });
describe.runIf(!shouldSkipTest)('root command option tests', () => { it('should complete root command --mode option values', async () => { const command = `${commandPrefix} --mode ""`; const output = await runCommand(command); expect(output).toMatchSnapshot(); });
it('should complete root command --mode option with partial input', async () => { const command = `${commandPrefix} --mode dev`; const output = await runCommand(command); expect(output).toMatchSnapshot(); });
it('should complete root command --logLevel option values', async () => { const command = `${commandPrefix} --logLevel ""`; const output = await runCommand(command); expect(output).toMatchSnapshot(); });
it('should complete root command --logLevel option with partial input', async () => { const command = `${commandPrefix} --logLevel i`; const output = await runCommand(command); expect(output).toMatchSnapshot(); });
it('should complete root command short flag -m option values', async () => { const command = `${commandPrefix} -m ""`; const output = await runCommand(command); expect(output).toMatchSnapshot(); });
it('should complete root command short flag -l option values', async () => { const command = `${commandPrefix} -l ""`; const output = await runCommand(command); expect(output).toMatchSnapshot(); });
it('should complete root command options after project argument', async () => { const command = `${commandPrefix} my-app --`; const output = await runCommand(command); expect(output).toMatchSnapshot(); });
it('should complete root command options with partial input after project argument', async () => { const command = `${commandPrefix} my-app --m`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); });
describe.runIf(!shouldSkipTest)( 'edge case completions for end with space', () => { it('should suggest port values if user ends with space after `--port`', async () => { const command = `${commandPrefix} dev --port ""`; const output = await runCommand(command); expect(output).toMatchSnapshot(); });
it("should keep suggesting the --port option if user typed partial but didn't end with space", async () => { const command = `${commandPrefix} dev --po`; const output = await runCommand(command); expect(output).toMatchSnapshot(); });
it("should suggest port values if user typed `--port=` and hasn't typed a space or value yet", async () => { const command = `${commandPrefix} dev --port=`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); } );
describe.runIf(!shouldSkipTest)('short flag handling', () => { it('should handle short flag value completion', async () => { const command = `${commandPrefix} dev -p `; const output = await runCommand(command); expect(output).toMatchSnapshot(); });
it('should handle short flag with equals sign', async () => { const command = `${commandPrefix} dev -p=3`; const output = await runCommand(command); expect(output).toMatchSnapshot(); });
it('should handle global short flags', async () => { const command = `${commandPrefix} -c `; const output = await runCommand(command); expect(output).toMatchSnapshot(); });
it('should not show duplicate options when short flag is used', async () => { const command = `${commandPrefix} -c vite.config.js --`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); });
describe.runIf(!shouldSkipTest)('positional argument completions', () => { it('should complete multiple positional arguments when ending with space', async () => { const command = `${commandPrefix} lint ""`; const output = await runCommand(command); expect(output).toMatchSnapshot(); });
it('should complete multiple positional arguments when ending with part of the value', async () => { const command = `${commandPrefix} lint ind`; const output = await runCommand(command); expect(output).toMatchSnapshot(); });
it('should complete single positional argument when ending with space', async () => { const command = `${commandPrefix} lint main.ts ""`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); });
describe.runIf(!shouldSkipTest)('copy command argument handlers', () => { it('should complete source argument with directory suggestions', async () => { const command = `${commandPrefix} copy ""`; const output = await runCommand(command); expect(output).toMatchSnapshot(); });
it('should complete destination argument with build suggestions', async () => { const command = `${commandPrefix} copy src/ ""`; const output = await runCommand(command); expect(output).toMatchSnapshot(); });
it('should filter source suggestions when typing partial input', async () => { const command = `${commandPrefix} copy s`; const output = await runCommand(command); expect(output).toMatchSnapshot(); });
it('should filter destination suggestions when typing partial input', async () => { const command = `${commandPrefix} copy src/ b`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); });
describe.runIf(!shouldSkipTest)('lint command argument handlers', () => { it('should complete files argument with file suggestions', async () => { const command = `${commandPrefix} lint ""`; const output = await runCommand(command); expect(output).toMatchSnapshot(); });
it('should filter file suggestions when typing partial input', async () => { const command = `${commandPrefix} lint m`; const output = await runCommand(command); expect(output).toMatchSnapshot(); });
it('should continue completing variadic files argument after first file', async () => { const command = `${commandPrefix} lint main.ts ""`; const output = await runCommand(command); expect(output).toMatchSnapshot(); });
it('should continue completing variadic suggestions after first file', async () => { const command = `${commandPrefix} lint main.ts i`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); });});
// Add specific tests for Commanderdescribe('commander specific tests', () => { it('should complete commands', async () => { const command = `pnpm tsx examples/demo.commander.ts complete -- `; const output = await runCommand(command); expect(output).toContain('serve'); expect(output).toContain('build'); expect(output).toContain('deploy'); });
it('should handle subcommands', async () => { // First, we need to check if deploy is recognized as a command const command1 = `pnpm tsx examples/demo.commander.ts complete -- deploy`; const output1 = await runCommand(command1); expect(output1).toContain('deploy'); expect(output1).toContain('Deploy the application');
// Then we need to check if the deploy command has subcommands // We can check this by running the deploy command with --help const command2 = `pnpm tsx examples/demo.commander.ts deploy --help`; const output2 = await runCommand(command2); expect(output2).toContain('staging'); expect(output2).toContain('production'); });});
describe('shell completion script generation', () => { const shells = ['zsh', 'bash', 'fish', 'powershell']; const cliTool = 'commander'; // Use commander for shell script generation tests
test.each(shells)('should generate %s completion script', async (shell) => { const command = `pnpm tsx examples/demo.${cliTool}.ts complete ${shell}`; const output = await runCommand(command); expect(output).toContain(`# ${shell} completion for`); expect(output.length).toBeGreaterThan(100); // Ensure we got a substantial script });});