Skip to content

Adapters

Tab provides adapters for popular CLI frameworks to make integration even easier. These adapters automatically extract commands and options from your CLI framework and allow you to add custom completion handlers.

The CAC adapter automatically detects commands and options from your CAC instance and provides completion handlers for customization.

import cac from 'cac';
import tab from '@bombsh/tab/cac';
const cli = cac('my-cli');
cli.command('dev', 'Start dev server').option('--port <port>', 'Specify port');
cli.command('build', 'Build for production').option('--mode <mode>', 'Build mode');
const completion = tab(cli);
// Get the dev command completion handler
const devCommandCompletion = completion.commands.get('dev');
// Get and configure the port option completion handler
const portOptionCompletion = devCommandCompletion.options.get('--port');
portOptionCompletion.handler = async () => {
return [
{ value: '3000', description: 'Development port' },
{ value: '8080', description: 'Production port' },
];
};
// Configure build mode completions
const buildCommandCompletion = completion.commands.get('build');
const modeOptionCompletion = buildCommandCompletion.options.get('--mode');
modeOptionCompletion.handler = async () => {
return [
{ value: 'development', description: 'Development build' },
{ value: 'production', description: 'Production build' },
];
};
cli.parse();

The CAC adapter:

  1. Extracts Commands: Automatically detects all commands defined with cli.command()
  2. Extracts Options: Identifies options defined with .option() for each command
  3. Provides Handlers: Gives you access to completion handlers for each command and option
  4. Maintains Structure: Preserves the command hierarchy and option relationships

The Citty adapter works with Citty’s command definitions and provides a similar interface for adding completions.

import { defineCommand, createMain } from 'citty';
import tab from '@bombsh/tab/citty';
const main = defineCommand({
meta: {
name: 'my-cli',
description: 'My CLI tool',
},
});
const devCommand = defineCommand({
meta: {
name: 'dev',
description: 'Start dev server',
},
args: {
port: { type: 'string', description: 'Specify port' },
host: { type: 'string', description: 'Specify host' },
},
});
const buildCommand = defineCommand({
meta: {
name: 'build',
description: 'Build for production',
},
args: {
mode: { type: 'string', description: 'Build mode' },
},
});
main.subCommands = {
dev: devCommand,
build: buildCommand,
};
const completion = await tab(main);
// Configure completions
const devCommandCompletion = completion.commands.get('dev');
if (devCommandCompletion) {
const portOptionCompletion = devCommandCompletion.options.get('--port');
if (portOptionCompletion) {
portOptionCompletion.handler = async () => {
return [
{ value: '3000', description: 'Development port' },
{ value: '8080', description: 'Production port' },
];
};
}
}
const cli = createMain(main);
cli();

The Citty adapter:

  1. Processes Command Definitions: Analyzes the command structure defined with defineCommand()
  2. Extracts Arguments: Identifies arguments defined in the args object
  3. Handles Subcommands: Recursively processes nested subcommands
  4. Provides Type Safety: Leverages Citty’s type system for better completion accuracy

The Commander adapter works with Commander.js and provides completion handlers for its command structure.

import { Command } from 'commander';
import tab from '@bombsh/tab/commander';
const program = new Command();
program
.name('my-cli')
.description('My CLI tool');
program
.command('dev')
.description('Start development server')
.option('-p, --port <port>', 'Specify port')
.option('-h, --host <host>', 'Specify host');
program
.command('build')
.description('Build for production')
.option('-m, --mode <mode>', 'Build mode');
const completion = tab(program);
// Configure completions
const devCommandCompletion = completion.commands.get('dev');
if (devCommandCompletion) {
const portOptionCompletion = devCommandCompletion.options.get('--port');
if (portOptionCompletion) {
portOptionCompletion.handler = async () => {
return [
{ value: '3000', description: 'Development port' },
{ value: '8080', description: 'Production port' },
];
};
}
}
program.parse();

The Commander adapter:

  1. Processes Commands: Analyzes the command structure defined with program.command()
  2. Extracts Options: Identifies options defined with .option()
  3. Handles Aliases: Automatically processes short and long option aliases
  4. Maintains Hierarchy: Preserves the command hierarchy and option relationships

All adapters support a configuration object to customize completion behavior:

import { CompletionConfig } from '@bombsh/tab';
const config: CompletionConfig = {
handler: async (previousArgs, toComplete, endsWithSpace) => {
// Default handler for all commands
return [];
},
options: {
port: {
handler: async () => {
return [
{ value: '3000', description: 'Development port' },
{ value: '8080', description: 'Production port' },
];
},
},
},
subCommands: {
dev: {
handler: async () => {
return [
{ value: 'dev', description: 'Development mode' },
];
},
options: {
port: {
handler: async () => {
return [
{ value: '3000', description: 'Dev port' },
];
},
},
},
},
},
};
// Use with any adapter
const completion = await tab(cli, config);

Always handle errors gracefully in your completion handlers:

portOptionCompletion.handler = async () => {
try {
// Expensive operation
const results = await getPorts();
return results.map(port => ({
value: port.toString(),
description: `Port ${port}`
}));
} catch (error) {
// Return empty array instead of throwing
console.error('Error in completion handler:', error);
return [];
}
};

Make your completions responsive to what the user is typing:

portOptionCompletion.handler = async (previousArgs, toComplete, endsWithSpace) => {
if (toComplete.startsWith('30')) {
return [
{ value: '3000', description: 'Development port' },
];
}
return [
{ value: '3000', description: 'Development port' },
{ value: '8080', description: 'Production port' },
{ value: '9000', description: 'Alternative port' },
];
};

Cache expensive operations and limit results:

let cachedPorts: Item[] | null = null;
portOptionCompletion.handler = async () => {
if (cachedPorts) {
return cachedPorts;
}
const ports = await getAvailablePorts();
cachedPorts = ports.slice(0, 20).map(port => ({
value: port.toString(),
description: `Port ${port}`
}));
return cachedPorts;
};
  • CAC automatically handles option parsing, so your completion handlers should focus on providing relevant suggestions
  • Use the option name as defined in your CAC command (e.g., --port <port> becomes --port)
  • Citty’s type system provides better type safety for completions
  • Arguments defined in the args object are automatically converted to options
  • Subcommands are processed recursively
  • Commander.js supports both short and long option aliases
  • The adapter automatically handles both forms
  • Commands can have multiple aliases

After setting up your adapter, you still need to handle shell completion setup:

// Add this to your CLI entry point
if (process.argv[2] === 'complete') {
const shell = process.argv[3];
if (shell && ['zsh', 'bash', 'fish', 'powershell'].includes(shell)) {
// Generate shell completion script
const script = generateCompletionScript(shell, 'my-cli', process.execPath);
console.log(script);
} else {
// Handle completion requests
const separatorIndex = process.argv.indexOf('--');
const completionArgs = separatorIndex !== -1 ? process.argv.slice(separatorIndex + 1) : [];
await completion.parse(completionArgs);
}
}