---
title: Tab
description: Shell autocompletions for JavaScript CLI tools
---

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](https://github.com/kazupon/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

### For Package Manager Completions

Global install is recommended:

```bash
npm install -g @bomb.sh/tab
```

Then enable completions permanently:

```bash
# For zsh
echo 'source <(tab pnpm zsh)' >> ~/.zshrc
source ~/.zshrc

# For bash
echo 'source <(tab pnpm bash)' >> ~/.bashrc
source ~/.bashrc

```

### For CLI Library (Adding Completions to Your CLI)

```bash
npm install @bomb.sh/tab
# or
pnpm add @bomb.sh/tab
# or
yarn add @bomb.sh/tab
# or
bun add @bomb.sh/tab
```

## Quick Start

Add autocompletions to your CLI tool:

```typescript
import t from '@bomb.sh/tab';

// Define your CLI structure
const devCmd = t.command('dev', 'Start development server');
devCmd.option('port', 'Specify port', (complete) => {
  complete('3000', 'Development port');
  complete('8080', 'Production port');
});

// Handle completion requests
if (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:

```bash
node my-cli.js complete -- dev --port=<TAB>
# Output: --port=3000  Development port
#         --port=8080  Production port
```

Install for users:

```bash
# One-time setup
source <(my-cli complete zsh)

# Permanent setup
my-cli complete zsh > ~/.my-cli-completion.zsh
echo 'source ~/.my-cli-completion.zsh' >> ~/.zshrc
```

## Positional Arguments

In addition to options, tab supports positional arguments with completion handlers:

```typescript
// Root-level positional arg
t.argument('project', (complete) => {
  complete('my-app', 'My application');
});

// Command-level positional args
t.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](#commanderjs-integration) below for adapter-specific examples.

## Package Manager Completions

As mentioned earlier, tab provides completions for package managers as well:

```bash
# Generate and install completion scripts (requires global install)
tab pnpm zsh > ~/.pnpm-completion.zsh && echo 'source ~/.pnpm-completion.zsh' >> ~/.zshrc
tab npm bash > ~/.npm-completion.bash && echo 'source ~/.npm-completion.bash' >> ~/.bashrc
tab yarn fish > ~/.config/fish/completions/yarn.fish
tab bun powershell > ~/.bun-completion.ps1 && echo '. ~/.bun-completion.ps1' >> $PROFILE
```

Without a global install, use `npx`:

```bash
npx @bomb.sh/tab pnpm zsh > ~/.pnpm-completion.zsh && echo 'source ~/.pnpm-completion.zsh' >> ~/.zshrc
```

Example in action:

```bash
pnpm install --reporter=<TAB>
# Shows: append-only, default, ndjson, silent

yarn add --emoji=<TAB>
# Shows: true, false
```

### 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](#framework-adapters)), it becomes completable through your package manager _without_ being on your `PATH` and without installing its completion script separately:

```bash
pnpm exec my-cli <TAB>      # completes my-cli's subcommands and flags
pnpm dlx my-cli <TAB>
pnpm my-cli <TAB>           # the bare form works too
```

Under 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

tab provides adapters for popular JavaScript CLI frameworks.

### CAC Integration

```typescript
import cac from 'cac';
import tab from '@bomb.sh/tab/cac';

const cli = cac('my-cli');

// Define your CLI
cli
  .command('dev', 'Start dev server')
  .option('--port <port>', 'Specify port')
  .option('--host <host>', 'Specify host');

// Initialize tab completions
const completion = await tab(cli);

// Add custom completions for option values
const 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

```typescript
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 completions
const completion = await tab(main);

// Add custom completions
const 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

```typescript
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 automatically
program.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 completions
const completion = tab(program);

// Custom option completions
const 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 completions
const 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:

```javascript
const completion = tab(program, { completionCommandName: 'completion' });
```

## How It Works

tab uses a standardized completion protocol that any CLI can implement:

```bash
# Generate shell completion script
my-cli complete zsh

# Parse completion request (called by shell)
my-cli complete -- install --port=""
```

**Output Format:**

```
--port=3000    Development port
--port=8080    Production port
:4
```

## Contributing

We welcome contributions! tab's architecture makes it easy to add support for new package managers or CLI frameworks.

## Acknowledgments

tab was inspired by the great [Cobra](https://github.com/spf13/cobra/) project, which set the standard for CLI tooling in the Go ecosystem.

## Adoption Support

We want to make it as easy as possible for the JS ecosystem to enjoy great autocompletions.  
We at [Thundraa](https://thundraa.com) 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](https://github.com/bombshell-dev/tab/discussions/61).  
We'll gladly help and even open a PR to get you started.
