Skip to content

Best Practices

This guide covers best practices for implementing effective autocompletions with Tab, including performance optimization, user experience considerations, and common patterns.

Always include descriptive text for your completions to help users understand what each option does:

// Good
devCmd.option('--port', 'Port number', function(complete) {
complete('3000', 'Development port (default)');
complete('8080', 'Production port');
complete('9000', 'Alternative port');
}, 'p');
// Avoid
devCmd.option('--port', 'Port number', function(complete) {
complete('3000', '');
complete('8080', '');
}, 'p');

Make your completions responsive to what the user is typing:

devCmd.option('--mode', 'Build mode', function(complete) {
// If user is typing 'dev', suggest development
if (this.toComplete?.startsWith('dev')) {
complete('development', 'Development mode');
return;
}
// If user is typing 'prod', suggest production
if (this.toComplete?.startsWith('prod')) {
complete('production', 'Production mode');
return;
}
// Default suggestions
complete('development', 'Development mode');
complete('production', 'Production mode');
complete('staging', 'Staging mode');
});

Always handle errors in your completion handlers to prevent crashes:

devCmd.option('--config', 'Config file', async function(complete) {
try {
const files = await fs.readdir('.');
const configFiles = files.filter(f => f.includes('config'));
configFiles.forEach(file => complete(file, `Config file: ${file}`));
} catch (error) {
// Provide fallback completions instead of failing
complete('vite.config.ts', 'Vite config file');
complete('vite.config.js', 'Vite config file');
}
});

Cache expensive operations and limit results to maintain responsiveness:

let cachedScripts: string[] | null = null;
t.command('run', 'Run scripts')
.argument('script', async function(complete) {
if (cachedScripts) {
cachedScripts.forEach(script => complete(script, `Run ${script} script`));
return;
}
try {
const packageJson = JSON.parse(await fs.readFile('package.json', 'utf8'));
const scripts = Object.keys(packageJson.scripts || {});
cachedScripts = scripts.slice(0, 20); // Limit to 20 scripts
cachedScripts.forEach(script => complete(script, `Run ${script} script`));
} catch (error) {
// Fallback completions
complete('dev', 'Start development server');
complete('build', 'Build for production');
}
});

Follow consistent naming conventions for commands and options:

// Good - consistent with common CLI patterns
t.command('dev', 'Start development server');
t.command('build', 'Build for production');
t.command('deploy', 'Deploy application');
// Avoid - inconsistent naming
t.command('start-dev', 'Start development server');
t.command('build-prod', 'Build for production');
t.command('deploy-app', 'Deploy application');

Organize related commands logically:

// Development commands
t.command('dev', 'Start development server');
t.command('dev build', 'Build in development mode');
t.command('dev test', 'Run tests in development mode');
// Production commands
t.command('build', 'Build for production');
t.command('deploy', 'Deploy to production');
t.command('deploy staging', 'Deploy to staging');

Provide short aliases only for commonly used options:

// Good - short aliases for common options
devCmd.option('--port', 'Port number', handler, 'p');
devCmd.option('--host', 'Host address', handler, 'h');
devCmd.option('--verbose', 'Enable verbose logging', 'v');
// Avoid - too many short aliases
devCmd.option('--config', 'Config file', handler, 'c');
devCmd.option('--mode', 'Build mode', handler, 'm');
devCmd.option('--output', 'Output directory', handler, 'o');
devCmd.option('--source', 'Source directory', handler, 's');

Use boolean flags for simple on/off options:

// Good - boolean flags for simple options
devCmd.option('--verbose', 'Enable verbose logging', 'v');
devCmd.option('--quiet', 'Suppress output', 'q');
devCmd.option('--watch', 'Watch for changes', 'w');
// Good - value options for complex data
devCmd.option('--port', 'Port number', function(complete) {
complete('3000', 'Development port');
complete('8080', 'Production port');
}, 'p');

When possible, provide sensible default values in your suggestions:

devCmd.option('--port', 'Port number', function(complete) {
complete('3000', 'Development port (default)');
complete('8080', 'Production port');
complete('9000', 'Alternative port');
}, 'p');

Choose option names that clearly indicate their purpose:

// Good - descriptive names
devCmd.option('--output-dir', 'Output directory', handler);
devCmd.option('--source-map', 'Generate source maps', handler);
// Avoid - ambiguous names
devCmd.option('--output', 'Output directory', handler);
devCmd.option('--map', 'Generate source maps', handler);

Use variadic arguments when users might want to specify multiple files:

// Good - variadic argument for multiple files
t.command('lint', 'Lint files')
.argument('files', function(complete) {
complete('src/', 'Source directory');
complete('tests/', 'Tests directory');
complete('*.ts', 'TypeScript files');
}, true); // true = variadic argument

Make argument suggestions contextual to the command:

t.command('copy', 'Copy files')
.argument('source', function(complete) {
// Suggest source locations
complete('src/', 'Source directory');
complete('dist/', 'Distribution directory');
complete('public/', 'Public assets');
})
.argument('destination', function(complete) {
// Suggest destination locations
complete('build/', 'Build output');
complete('release/', 'Release directory');
complete('backup/', 'Backup location');
});

Package Manager Integration Best Practices

Section titled “Package Manager Integration Best Practices”

When building CLI tools, test that they work with Tab’s package manager integration:

Terminal window
# Test if your CLI supports Tab completions
my-cli complete -- --help
# Expected output format:
--help Show help information
:0

Ensure your CLI follows the Tab completion protocol:

// In your CLI entry point
if (process.argv[2] === 'complete') {
const shell = process.argv[3];
if (shell && ['zsh', 'bash', 'fish', 'powershell'].includes(shell)) {
t.setup('my-cli', process.execPath, shell);
} else {
const separatorIndex = process.argv.indexOf('--');
const completionArgs = separatorIndex !== -1 ? process.argv.slice(separatorIndex + 1) : [];
t.parse(completionArgs);
}
}

For package manager integration, provide completions for all major commands:

// Example: Comprehensive pnpm completions
const addCmd = t.command('add', 'Install packages');
addCmd.option('--save-dev', 'Save to devDependencies', 'D');
addCmd.option('--save-optional', 'Save to optionalDependencies', 'O');
addCmd.option('--global', 'Install globally', 'g');
const runCmd = t.command('run', 'Run scripts');
runCmd.argument('script', async function(complete) {
try {
const packageJson = JSON.parse(await fs.readFile('package.json', 'utf8'));
const scripts = Object.keys(packageJson.scripts || {});
scripts.forEach(script => complete(script, `Run ${script} script`));
} catch (error) {
// Fallback completions
complete('dev', 'Start development server');
complete('build', 'Build for production');
}
}, true);

Cache results of expensive operations to improve responsiveness:

let cachedDependencies: string[] | null = null;
t.command('remove', 'Remove packages')
.argument('package', async function(complete) {
if (cachedDependencies) {
cachedDependencies.forEach(dep => complete(dep, 'Installed package'));
return;
}
try {
const packageJson = JSON.parse(await fs.readFile('package.json', 'utf8'));
const deps = {
...packageJson.dependencies,
...packageJson.devDependencies,
};
cachedDependencies = Object.keys(deps);
cachedDependencies.forEach(dep => complete(dep, 'Installed package'));
} catch (error) {
// Fallback completions
complete('react', 'React library');
complete('typescript', 'TypeScript compiler');
}
});

Limit the number of completions to maintain performance:

devCmd.option('--config', 'Config file', async function(complete) {
try {
const files = await fs.readdir('.');
const configFiles = files.filter(f => f.includes('config'));
// Limit to 10 results for performance
configFiles.slice(0, 10).forEach(file => complete(file, `Config file: ${file}`));
} catch (error) {
complete('vite.config.ts', 'Vite config file');
}
});

Only use async operations when necessary:

// Good - sync operations for simple completions
devCmd.option('--mode', 'Build mode', function(complete) {
complete('development', 'Development mode');
complete('production', 'Production mode');
complete('staging', 'Staging mode');
});
// Good - async operations for dynamic data
devCmd.option('--config', 'Config file', async function(complete) {
const files = await fs.readdir('.');
const configFiles = files.filter(f => f.includes('config'));
configFiles.forEach(file => complete(file, `Config file: ${file}`));
});

Start with common options and provide more specific ones as users type:

devCmd.option('--mode', 'Build mode', function(complete) {
if (this.toComplete?.startsWith('dev')) {
complete('development', 'Development mode');
return;
}
if (this.toComplete?.startsWith('prod')) {
complete('production', 'Production mode');
return;
}
// Show all options initially
complete('development', 'Development mode');
complete('production', 'Production mode');
complete('staging', 'Staging mode');
complete('test', 'Test mode');
});

Maintain consistent description formatting:

// Good - consistent formatting
devCmd.option('--port', 'Port number', function(complete) {
complete('3000', 'Development port (default)');
complete('8080', 'Production port');
complete('9000', 'Alternative port');
}, 'p');
// Avoid - inconsistent formatting
devCmd.option('--port', 'Port number', function(complete) {
complete('3000', 'dev port');
complete('8080', 'Production port');
complete('9000', 'alt port');
}, 'p');

Include default values in descriptions when helpful:

devCmd.option('--port', 'Port number', function(complete) {
complete('3000', 'Development port (default)');
complete('8080', 'Production port');
}, 'p');
devCmd.option('--host', 'Host address', function(complete) {
complete('localhost', 'Localhost (default)');
complete('0.0.0.0', 'All interfaces');
}, 'h');

Regularly test your completions to ensure they work correctly:

Terminal window
# Test command completions
my-cli complete -- "dev"
# Test option completions
my-cli complete -- "dev --port"
# Test argument completions
my-cli complete -- "copy src/"
# Test with package managers
pnpm my-cli complete -- "dev --port"

Test how your completions handle error conditions:

// Test with missing files
devCmd.option('--config', 'Config file', async function(complete) {
try {
const files = await fs.readdir('.');
const configFiles = files.filter(f => f.includes('config'));
configFiles.forEach(file => complete(file, `Config file: ${file}`));
} catch (error) {
// Ensure fallback completions work
complete('vite.config.ts', 'Vite config file');
complete('vite.config.js', 'Vite config file');
}
});

Monitor completion performance, especially for async operations:

// Add timing for performance monitoring
devCmd.option('--config', 'Config file', async function(complete) {
const start = Date.now();
try {
const files = await fs.readdir('.');
const configFiles = files.filter(f => f.includes('config'));
configFiles.forEach(file => complete(file, `Config file: ${file}`));
} catch (error) {
complete('vite.config.ts', 'Vite config file');
}
const duration = Date.now() - start;
if (duration > 100) {
console.warn(`Slow completion: ${duration}ms`);
}
});