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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470import { 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) => { const commandPrefix = `pnpm tsx examples/demo.${cliTool}.ts complete --`;
it('should complete cli options', async () => { const output = await runCommand(`${commandPrefix}`); expect(output).toMatchSnapshot(); });
describe('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('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('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('boolean option handling', () => { it('should complete subcommands and arguments after boolean options', async () => { const command = `${commandPrefix} dev --verbose ""`; const output = await runCommand(command); // After a boolean option, should show subcommands/arguments (not flag values) expect(output).toContain('build'); expect(output).toContain('start'); expect(output).not.toContain('verbose'); });
it('should complete subcommands and arguments after short boolean options', async () => { const command = `${commandPrefix} dev -v ""`; const output = await runCommand(command); // After a short boolean option, should show subcommands/arguments (not flag values) expect(output).toContain('build'); expect(output).toContain('start'); expect(output).not.toContain('verbose'); });
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 options that start with '--h' even after a boolean option expect(output).toContain('--host'); }); });
describe('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 — shows subcommands, not flag values expect(output).toContain('build'); expect(output).not.toContain('quiet'); });
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 — shows subcommands, not flag values expect(output).toContain('build'); expect(output).not.toContain('verbose'); });
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 — shows subcommands, not flag values expect(output).toContain('build'); expect(output).not.toContain('verbose'); });
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('--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('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('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('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('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('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('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('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 () => { // Check subcommands of root 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');
// Check subcommands of subcommand. const command2 = `pnpm tsx examples/demo.commander.ts complete -- deploy ""`; const output2 = await runCommand(command2); expect(output2).toContain('staging'); expect(output2).toContain('production'); });
it('should intercept completion when using parseAsync', async () => { const command = `pnpm tsx examples/demo.commander-async.ts complete -- `; const output = await runCommand(command); expect(output).toContain('greet'); expect(output).toContain('Say hello'); // directive line must be present, proving t.parse was invoked expect(output).toMatch(/:\d+\s*$/); });});
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 });});