Tab
Shell autocompletions are largely missing in the JavaScript CLI ecosystem. tab provides a simple API for adding autocompletions to any JavaScript CLI tool.
Additionally, tab supports autocompletions for pnpm, npm, yarn, and bun.
Modern CLI libraries like Gunshi include tab completion natively in their core.
Major CLIs and tooling projects including Cloudflare, Nuxt, Astro, and Vitest also adopted tab to provide shell completions for their users.
As 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.
tab solves this complexity by providing autocompletions that work consistently across zsh, bash, fish, and powershell.
Installation
Section titled “Installation”For Package Manager Completions
Section titled “For Package Manager Completions”Global install is recommended:
npm install -g @bomb.sh/tabThen enable completions permanently:
# For zshecho 'source <(tab pnpm zsh)' >> ~/.zshrcsource ~/.zshrc
# For bashecho 'source <(tab pnpm bash)' >> ~/.bashrcsource ~/.bashrcFor CLI Library (Adding Completions to Your CLI)
Section titled “For CLI Library (Adding Completions to Your CLI)”npm install @bomb.sh/tab# orpnpm add @bomb.sh/tab# oryarn add @bomb.sh/tab# orbun add @bomb.sh/tabQuick Start
Section titled “Quick Start”Add autocompletions to your CLI tool:
import t from '@bomb.sh/tab';
// Define your CLI structureconst devCmd = t.command('dev', 'Start development server');devCmd.option('port', 'Specify port', (complete) => { complete('3000', 'Development port'); complete('8080', 'Production port');});
// Handle completion requestsif (process.argv[2] === 'complete') { const shell = process.argv[3]; if (shell === '--') { const args = process.argv.slice(4); t.parse(args); } else { t.setup('my-cli', 'node my-cli.js', shell); }}Test your completions:
node my-cli.js complete -- dev --port=<TAB># Output: --port=3000 Development port# --port=8080 Production portInstall for users:
# One-time setupsource <(my-cli complete zsh)
# Permanent setupmy-cli complete zsh > ~/.my-cli-completion.zshecho 'source ~/.my-cli-completion.zsh' >> ~/.zshrcPositional Arguments
Section titled “Positional Arguments”In addition to options, tab supports positional arguments with completion handlers:
// Root-level positional argt.argument('project', (complete) => { complete('my-app', 'My application');});
// Command-level positional argst.command('copy', 'Copy files') .argument('source', (complete) => { complete('src/', 'Source directory'); complete('dist/', 'Distribution directory'); }) .argument('destination', (complete) => { complete('build/', 'Build output'); complete('release/', 'Release directory'); });
// Variadic arguments (pass true as the third argument)t.command('lint', 'Lint project').argument( 'files', (complete) => { complete('src/', 'Source directory'); complete('tests/', 'Tests directory'); }, true);Framework adapters infer positional arguments from your CLI framework definitions. See the Commander.js integration below for adapter-specific examples.
Package Manager Completions
Section titled “Package Manager Completions”As mentioned earlier, tab provides completions for package managers as well:
# Generate and install completion scripts (requires global install)tab pnpm zsh > ~/.pnpm-completion.zsh && echo 'source ~/.pnpm-completion.zsh' >> ~/.zshrctab npm bash > ~/.npm-completion.bash && echo 'source ~/.npm-completion.bash' >> ~/.bashrctab yarn fish > ~/.config/fish/completions/yarn.fishtab bun powershell > ~/.bun-completion.ps1 && echo '. ~/.bun-completion.ps1' >> $PROFILEWithout a global install, use npx:
npx @bomb.sh/tab pnpm zsh > ~/.pnpm-completion.zsh && echo 'source ~/.pnpm-completion.zsh' >> ~/.zshrcExample in action:
pnpm install --reporter=<TAB># Shows: append-only, default, ndjson, silent
yarn add --emoji=<TAB># Shows: true, falseCompleting locally-installed CLIs
Section titled “Completing locally-installed CLIs”Package manager completion does more than complete the package manager’s own flags — it also delegates to CLIs installed as local project dependencies. If a CLI implements tab’s completion protocol (directly or via a framework adapter), it becomes completable through your package manager without being on your PATH and without installing its completion script separately:
pnpm exec my-cli <TAB> # completes my-cli's subcommands and flagspnpm dlx my-cli <TAB>pnpm my-cli <TAB> # the bare form works tooUnder the hood, tab strips the package-manager wrapper (exec, x, run, dlx), detects whether the target CLI supports completion, and forwards the request to it — falling back to running the CLI through the package manager (e.g. pnpm my-cli complete -- …) so locally-installed binaries resolve. The same works for npm exec, yarn, and bun x.
Completion is registered against the package-manager binary (npm, pnpm, yarn, bun). npx and bunx are separate commands with no completion of their own, so npx my-cli <TAB> / bunx my-cli <TAB> won’t complete — use npm exec my-cli / bun x my-cli instead.
Framework Adapters
Section titled “Framework Adapters”tab provides adapters for popular JavaScript CLI frameworks.
CAC Integration
Section titled “CAC Integration”import cac from 'cac';import tab from '@bomb.sh/tab/cac';
const cli = cac('my-cli');
// Define your CLIcli .command('dev', 'Start dev server') .option('--port <port>', 'Specify port') .option('--host <host>', 'Specify host');
// Initialize tab completionsconst completion = await tab(cli);
// Add custom completions for option valuesconst devCommand = completion.commands.get('dev');const portOption = devCommand?.options.get('port');if (portOption) { portOption.handler = (complete) => { complete('3000', 'Development port'); complete('8080', 'Production port'); };}
cli.parse();Citty Integration
Section titled “Citty Integration”import { defineCommand, createMain } from 'citty';import tab from '@bomb.sh/tab/citty';
const main = defineCommand({ meta: { name: 'my-cli', description: 'My CLI tool' }, subCommands: { dev: defineCommand({ meta: { name: 'dev', description: 'Start dev server' }, args: { port: { type: 'string', description: 'Specify port' }, host: { type: 'string', description: 'Specify host' }, }, }), },});
// Initialize tab completionsconst completion = await tab(main);
// Add custom completionsconst devCommand = completion.commands.get('dev');const portOption = devCommand?.options.get('port');if (portOption) { portOption.handler = (complete) => { complete('3000', 'Development port'); complete('8080', 'Production port'); };}
const cli = createMain(main);cli();Commander.js Integration
Section titled “Commander.js Integration”import { Command, Option } from 'commander';import tab from '@bomb.sh/tab/commander';
const program = new Command('my-cli');program.version('1.0.0');
// Options with .choices() get completions automaticallyprogram.addOption( new Option('-l, --logLevel <level>', 'Specify log level').choices([ 'info', 'warn', 'error', 'silent', ]));
program .command('serve') .description('Start the server') .option('-p, --port <number>', 'port to use', '3000') .option('-H, --host <host>', 'host to use', 'localhost') .action(() => { console.log('Starting server...'); });
program .command('lint [files...]') .description('Lint source files') .option('--fix', 'automatically fix problems');
program .command('copy <source> <destination>') .description('Copy files');
// Initialize tab completionsconst completion = tab(program);
// Custom option completionsconst serveCommand = completion.commands.get('serve');const portOption = serveCommand?.options.get('port');if (portOption) { portOption.handler = (complete) => { complete('3000', 'Default port'); complete('8080', 'Alternative port'); };}
// Custom positional argument completionsconst lintCommand = completion.commands.get('lint');const filesArg = lintCommand?.arguments.get('files');if (filesArg) { filesArg.handler = (complete) => { complete('main.ts', 'Main file'); complete('index.ts', 'Index file'); };}
const copyCommand = completion.commands.get('copy');const sourceArg = copyCommand?.arguments.get('source');if (sourceArg) { sourceArg.handler = (complete) => { complete('src/', 'Source directory'); complete('dist/', 'Distribution directory'); };}
program.parse();Options defined with Commander’s .choices() are picked up automatically — no custom handler needed. For positional arguments, the adapter reads argument definitions from your commands; add handlers via arguments.get('name') when you need custom suggestions.
The Commander integration supports customising the command name used to generate shell completion scripts. The default is complete. If you use a custom name like completion, it appears in help as completion <shell>, while runtime suggestions stay on the hidden complete -- [args...] command. Use your custom command when generating shell scripts:
const completion = tab(program, { completionCommandName: 'completion' });How It Works
Section titled “How It Works”tab uses a standardized completion protocol that any CLI can implement:
# Generate shell completion scriptmy-cli complete zsh
# Parse completion request (called by shell)my-cli complete -- install --port=""Output Format:
--port=3000 Development port--port=8080 Production port:4Contributing
Section titled “Contributing”We welcome contributions! tab’s architecture makes it easy to add support for new package managers or CLI frameworks.
Acknowledgments
Section titled “Acknowledgments”tab was inspired by the great Cobra project, which set the standard for CLI tooling in the Go ecosystem.
Adoption Support
Section titled “Adoption Support”We want to make it as easy as possible for the JS ecosystem to enjoy great autocompletions.
We at Thundraa would be happy to help any open source CLI utility adopt tab.
If you maintain a CLI and would like autocompletions set up for your users, just drop the details in our Adopting tab discussion.
We’ll gladly help and even open a PR to get you started.