From b69ff49bda1a0e0e033b944786cbba85a6bf92ed Mon Sep 17 00:00:00 2001 From: Paul Valladares <85648028+dreyfus92@users.noreply.github.com> Date: Thu, 7 Aug 2025 12:57:23 -0500 Subject: [PATCH] feat: Add documentation for Tab (#17) --- astro.config.mjs | 13 +- src/content/docs/tab/api/core.mdx | 295 ++++++++++ .../docs/tab/basics/getting-started.mdx | 169 ++++++ src/content/docs/tab/guides/adapters.mdx | 299 ++++++++++ .../docs/tab/guides/best-practices.mdx | 312 +++++++++++ src/content/docs/tab/guides/examples.mdx | 512 ++++++++++++++++++ src/content/docs/tab/index.mdx | 76 +++ 7 files changed, 1675 insertions(+), 1 deletion(-) create mode 100644 src/content/docs/tab/api/core.mdx create mode 100644 src/content/docs/tab/basics/getting-started.mdx create mode 100644 src/content/docs/tab/guides/adapters.mdx create mode 100644 src/content/docs/tab/guides/best-practices.mdx create mode 100644 src/content/docs/tab/guides/examples.mdx create mode 100644 src/content/docs/tab/index.mdx diff --git a/astro.config.mjs b/astro.config.mjs index 588810c..f8c2d65 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -101,7 +101,18 @@ export default defineConfig({ link: "args/api", }, ], - } + }, + { + label: "Tab", + id: "tab", + icon: "right-caret", + link: "/tab", + items: [ + { label: "Basics", autogenerate: { directory: "tab/basics" } }, + { label: "API", autogenerate: { directory: "tab/api" } }, + { label: "Guides", autogenerate: { directory: "tab/guides" } }, + ] + }, ]), ], }), diff --git a/src/content/docs/tab/api/core.mdx b/src/content/docs/tab/api/core.mdx new file mode 100644 index 0000000..8582b1e --- /dev/null +++ b/src/content/docs/tab/api/core.mdx @@ -0,0 +1,295 @@ +--- +title: Core API +description: Complete API reference for the Tab core package +--- + +This page provides the complete API reference for the Tab core package, including all classes, methods, types, and interfaces. + +## Classes + +### Completion + +The main class for managing command and option completions. + +```ts +import { Completion } from '@bombsh/tab'; + +const completion = new Completion(); +``` + +#### Methods + +##### addCommand + +Adds a completion handler for a command. + +**Parameters:** +- `name` (string): The command name +- `description` (string): Command description +- `args` (boolean[]): Array indicating which arguments are required (false) or optional (true) +- `handler` (Handler): Function that returns completion suggestions +- `parent` (string, optional): Parent command name for nested commands + +**Returns:** string - The command key + +**Example:** +```ts +// Command with required argument: "vite " +completion.addCommand('dev', 'Start development server', [false], async () => { + return [ + { value: 'src/main.ts', description: 'Main entry point' }, + { value: 'src/index.ts', description: 'Index entry point' }, + ]; +}); + +// Command with optional argument: "vite [entry]" +completion.addCommand('serve', 'Start the server', [true], async () => { + return [ + { value: 'dist', description: 'Distribution directory' }, + ]; +}); + +// Nested command: "vite dev build" +completion.addCommand('build', 'Build project', [], async () => { + return [ + { value: 'build', description: 'Build command' }, + ]; +}, 'dev'); +``` + +##### addOption + +Adds a completion handler for an option of a specific command. + +**Parameters:** +- `command` (string): The command name +- `option` (string): The option name (e.g., '--port') +- `description` (string): Option description +- `handler` (Handler): Function that returns completion suggestions +- `alias` (string, optional): Short flag alias (e.g., 'p' for '--port') + +**Returns:** string - The option name + +**Example:** +```ts +completion.addOption('dev', '--port', 'Port number', async () => { + return [ + { value: '3000', description: 'Development port' }, + { value: '8080', description: 'Production port' }, + ]; +}, 'p'); +``` + +##### parse + +Parses command line arguments and outputs completion suggestions to stdout. + +**Parameters:** +- `args` (string[]): Command line arguments + +**Example:** +```ts +await completion.parse(['--port']); +// Outputs completion suggestions to stdout +``` + +### script + +Generates shell completion scripts. + +**Parameters:** +- `shell` (string): Target shell ('zsh', 'bash', 'fish', 'powershell') +- `name` (string): CLI tool name +- `execPath` (string): Executable path for the CLI tool + +**Example:** +```ts +import { script } from '@bombsh/tab'; + +script('zsh', 'my-cli', '/usr/bin/node /path/to/my-cli'); +``` + +## Types + +### Handler + +Function type for completion handlers. + +```ts +type Handler = ( + previousArgs: string[], + toComplete: string, + endsWithSpace: boolean +) => Item[] | Promise; +``` + +**Parameters:** +- `previousArgs` (string[]): Previously typed arguments +- `toComplete` (string): The text being completed +- `endsWithSpace` (boolean): Whether the input ends with a space + +**Returns:** Item[] | Promise<Item[]> + +**Example:** +```ts +const handler: Handler = async (previousArgs, toComplete, endsWithSpace) => { + // Check if user is typing 'prod' to suggest production + if (toComplete.startsWith('prod')) { + return [ + { value: 'production', description: 'Production environment' }, + ]; + } + + return [ + { value: 'development', description: 'Development environment' }, + { value: 'staging', description: 'Staging environment' }, + { value: 'production', description: 'Production environment' }, + ]; +}; +``` + +### Item + +Object representing a completion suggestion. + +```ts +type Item = { + value: string; + description: string; +}; +``` + +**Properties:** +- `value` (string): The completion value +- `description` (string): Description of the completion + +**Example:** +```ts +const item: Item = { + value: '3000', + description: 'Development port' +}; +``` + +### `Positional` + +Type for positional argument configuration. + +```ts +type Positional = { + required: boolean; + variadic: boolean; + completion: Handler; +}; +``` + +## Constants + +### ShellCompRequestCmd + +The name of the hidden command used to request completion results. + +```ts +export const ShellCompRequestCmd: string = '__complete'; +``` + +### ShellCompNoDescRequestCmd + +The name of the hidden command used to request completion results without descriptions. + +```ts +export const ShellCompNoDescRequestCmd: string = '__completeNoDesc'; +``` + +### ShellCompDirective + +Bit map representing different behaviors the shell can be instructed to have. + +```ts +export const ShellCompDirective = { + ShellCompDirectiveError: 1 << 0, + ShellCompDirectiveNoSpace: 1 << 1, + ShellCompDirectiveNoFileComp: 1 << 2, + ShellCompDirectiveFilterFileExt: 1 << 3, + ShellCompDirectiveFilterDirs: 1 << 4, + ShellCompDirectiveKeepOrder: 1 << 5, + ShellCompDirectiveDefault: 0, +}; +``` + +## Complete Example + +Here's a complete example showing all the core API features: + +```ts +#!/usr/bin/env node +import { Completion, script } from '@bombsh/tab'; + +const name = 'my-cli'; +const completion = new Completion(); + +// Add command completions with positional arguments +completion.addCommand('dev', 'Start development server', [false], async (previousArgs, toComplete, endsWithSpace) => { + if (toComplete.startsWith('dev')) { + return [ + { value: 'dev', description: 'Start in development mode' }, + ]; + } + + return [ + { value: 'dev', description: 'Start in development mode' }, + { value: 'prod', description: 'Start in production mode' }, + ]; +}); + +// Add option completions +completion.addOption('dev', '--port', 'Port number', 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' }, + ]; +}, 'p'); + +// Helper function to quote paths with spaces +function quoteIfNeeded(path: string) { + return path.includes(' ') ? `'${path}'` : path; +} + +// Get the executable path for shell completion +const execPath = process.execPath; +const processArgs = process.argv.slice(1); +const quotedExecPath = quoteIfNeeded(execPath); +const quotedProcessArgs = processArgs.map(quoteIfNeeded); +const quotedProcessExecArgs = process.execArgv.map(quoteIfNeeded); +const x = `${quotedExecPath} ${quotedProcessExecArgs.join(' ')} ${quotedProcessArgs[0]}`; + +// Handle completion requests +if (process.argv[2] === '--') { + try { + await completion.parse(process.argv.slice(2)); + } catch (error) { + console.error('Completion error:', error.message); + process.exit(1); + } +} else { + const shell = process.argv[2]; + if (['zsh', 'bash', 'fish', 'powershell'].includes(shell)) { + script(shell, name, x); + } else { + console.error(`Unsupported shell: ${shell}`); + process.exit(1); + } +} +``` + +## Next Steps + +- Learn about [Framework Adapters](/docs/tab/guides/adapters/) for easier integration +- Check out [Examples](/docs/tab/guides/examples/) for practical use cases +- Explore [Best Practices](/docs/tab/guides/best-practices/) for effective implementations \ No newline at end of file diff --git a/src/content/docs/tab/basics/getting-started.mdx b/src/content/docs/tab/basics/getting-started.mdx new file mode 100644 index 0000000..8aa1afc --- /dev/null +++ b/src/content/docs/tab/basics/getting-started.mdx @@ -0,0 +1,169 @@ +--- +title: Getting Started +description: Learn how to add shell autocompletions to your CLI with Tab +--- + +import { Tabs, TabItem } from '@astrojs/starlight/components'; + +Tab provides shell autocompletions for JavaScript CLI tools, making your CLI feel as polished as native tools like git. This guide will walk you through adding autocompletions to your CLI tool. + +## Installation + +Install Tab using your preferred package manager: + + + + ```bash + npm install @bombsh/tab + ``` + + + ```bash + pnpm add @bombsh/tab + ``` + + + ```bash + yarn add @bombsh/tab + ``` + + + +## Basic Usage + +Here's a simple example of how to add autocompletions to your CLI: + +```ts +import { Completion, script } from '@bombsh/tab'; + +const name = 'my-cli'; +const completion = new Completion(); + +// Add command completions with positional arguments +completion.addCommand( + 'start', + 'Start the application', + [false], // Required argument + async (previousArgs, toComplete, endsWithSpace) => { + return [ + { value: 'dev', description: 'Start in development mode' }, + { value: 'prod', description: 'Start in production mode' }, + ]; + } +); + +// Add option completions +completion.addOption( + 'start', + '--port', + 'Specify the port number', + async (previousArgs, toComplete, endsWithSpace) => { + return [ + { value: '3000', description: 'Development port' }, + { value: '8080', description: 'Production port' }, + ]; + }, + 'p' // Short flag alias +); + +// Helper function to quote paths with spaces +function quoteIfNeeded(path: string) { + return path.includes(' ') ? `'${path}'` : path; +} + +// Get the executable path for shell completion +const execPath = process.execPath; +const processArgs = process.argv.slice(1); +const quotedExecPath = quoteIfNeeded(execPath); +const quotedProcessArgs = processArgs.map(quoteIfNeeded); +const quotedProcessExecArgs = process.execArgv.map(quoteIfNeeded); +const x = `${quotedExecPath} ${quotedProcessExecArgs.join(' ')} ${quotedProcessArgs[0]}`; + +// Handle completion requests +if (process.argv[2] === '--') { + // Autocompletion logic + await completion.parse(process.argv.slice(2)); +} else { + // Generate shell completion script + script(process.argv[2], name, x); +} +``` + +## Understanding Positional Arguments + +The `args` parameter in `addCommand` is an array of booleans that indicates which arguments are required or optional: + +```ts +// Command: "my-cli start " +completion.addCommand('start', 'Start the app', [false], handler); + +// Command: "my-cli serve [entry]" +completion.addCommand('serve', 'Serve the app', [true], handler); + +// Command: "my-cli build [output]" +completion.addCommand('build', 'Build the app', [false, true], handler); + +// Command: "my-cli dev [...files]" +completion.addCommand('dev', 'Dev mode', [true], handler); // Variadic argument +``` + +## Shell Setup + +After adding Tab to your CLI, users need to set up shell completion. Here are the recommended approaches for different shells: + +### Zsh + +```bash +# Generate completion script +my-cli complete zsh > ~/completion-for-my-cli.zsh + +# Add to your .zshrc +echo 'source ~/completion-for-my-cli.zsh' >> ~/.zshrc +``` + +### Bash + +```bash +# Generate completion script +my-cli complete bash > ~/.bash_completion.d/my-cli + +# Add to your .bashrc +echo 'source ~/.bash_completion.d/my-cli' >> ~/.bashrc +``` + +### Fish + +```bash +# Generate completion script +my-cli complete fish > ~/.config/fish/completions/my-cli.fish +``` + +### PowerShell + +```powershell +# Generate completion script +my-cli complete powershell > $PROFILE.CurrentUserAllHosts +``` + +## How It Works + +Tab works by adding a `complete` command to your CLI that generates shell-specific completion scripts. When users type `my-cli complete zsh`, Tab generates a completion script that the shell can source. + +The completion script communicates with your CLI through the autocompletion server. When users press Tab, the shell calls your CLI with special arguments, and Tab returns the appropriate completions. + +### Autocompletion Server + +Your CLI becomes an autocompletion server when you add Tab. The server responds to completion requests with suggestions and ends its output with `:{Number}` to indicate the number of completions. + +For example: +```bash +my-cli complete -- --po +--port Specify the port number +:0 +``` + +## Next Steps + +- Learn about [Framework Adapters](/docs/tab/guides/adapters/) for easier integration with CAC, Citty, and Commander.js +- Explore [Best Practices](/docs/tab/guides/best-practices/) for implementing effective autocompletions +- Check out the [API Reference](/docs/tab/api/core/) for detailed documentation \ No newline at end of file diff --git a/src/content/docs/tab/guides/adapters.mdx b/src/content/docs/tab/guides/adapters.mdx new file mode 100644 index 0000000..f3dab07 --- /dev/null +++ b/src/content/docs/tab/guides/adapters.mdx @@ -0,0 +1,299 @@ +--- +title: Adapters +description: Use Tab with popular CLI frameworks like CAC, Citty, and Commander.js +--- + +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. + +## CAC Adapter + +The CAC adapter automatically detects commands and options from your CAC instance and provides completion handlers for customization. + +```ts +import cac from 'cac'; +import tab from '@bombsh/tab/cac'; + +const cli = cac('my-cli'); + +cli.command('dev', 'Start dev server').option('--port ', 'Specify port'); +cli.command('build', 'Build for production').option('--mode ', 'Build mode'); + +const completion = await 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 ( + previousArgs, + toComplete, + endsWithSpace +) => { + 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(); +``` + +### How the CAC Adapter Works + +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 + +## Citty Adapter + +The Citty adapter works with Citty's command definitions and provides a similar interface for adding completions. + +```ts +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(); +``` + +### How the Citty Adapter Works + +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 + +## Commander Adapter + +The Commander adapter works with Commander.js and provides completion handlers for its command structure. + +```ts +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 ', 'Specify port') + .option('-h, --host ', 'Specify host'); + +program + .command('build') + .description('Build for production') + .option('-m, --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(); +``` + +### How the Commander Adapter Works + +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 + +## Configuration + +All adapters support a configuration object to customize completion behavior: + +```ts +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); +``` + +## Best Practices + +### 1. Error Handling + +Always handle errors gracefully in your completion handlers: + +```ts +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 []; + } +}; +``` + +### 2. Context-Aware Completions + +Make your completions responsive to what the user is typing: + +```ts +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' }, + ]; +}; +``` + +### 3. Performance Optimization + +Cache expensive operations and limit results: + +```ts +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; +}; +``` + +## Next Steps + +- Learn about [Best Practices](/docs/tab/guides/best-practices/) for effective autocompletions +- Check out [Examples](/docs/tab/guides/examples/) for practical use cases +- Explore the [API Reference](/docs/tab/api/core/) for advanced usage \ No newline at end of file diff --git a/src/content/docs/tab/guides/best-practices.mdx b/src/content/docs/tab/guides/best-practices.mdx new file mode 100644 index 0000000..4419277 --- /dev/null +++ b/src/content/docs/tab/guides/best-practices.mdx @@ -0,0 +1,312 @@ +--- +title: Best Practices +description: Learn best practices for implementing effective autocompletions with Tab +--- + +This guide covers best practices for implementing autocompletions that provide a great user experience and integrate seamlessly with your CLI tool. + +## Completion Handler Design + +### Keep Handlers Fast + +Completion handlers should return results quickly since they're called frequently as users type: + +```ts +// ✅ Good: Fast, synchronous completion +completion.addOption('dev', '--port', 'Port number', () => { + return [ + { value: '3000', description: 'Development port' }, + { value: '8080', description: 'Production port' }, + ]; +}); + +// ❌ Avoid: Slow, async operations in completion handlers +completion.addOption('dev', '--file', 'File path', async () => { + const files = await fs.readdir('.'); // This can be slow + return files.map(f => ({ value: f, description: 'File' })); +}); +``` + +### Use Context Appropriately + +Leverage the completion context to provide relevant suggestions: + +```ts +completion.addOption('deploy', '--env', 'Environment', async (previousArgs, toComplete, endsWithSpace) => { + // Check if user is completing an environment name + if (toComplete.startsWith('prod')) { + return [ + { value: 'production', description: 'Production environment' }, + ]; + } + + return [ + { value: 'development', description: 'Development environment' }, + { value: 'staging', description: 'Staging environment' }, + { value: 'production', description: 'Production environment' }, + ]; +}); +``` + +### Provide Meaningful Descriptions + +Always include descriptions to help users understand what each completion means: + +```ts +// ✅ Good: Clear descriptions +completion.addCommand('deploy', 'Deploy application', () => { + return [ + { value: 'dev', description: 'Deploy to development environment' }, + { value: 'staging', description: 'Deploy to staging environment' }, + { value: 'prod', description: 'Deploy to production environment' }, + ]; +}); + +// ❌ Avoid: No descriptions +completion.addCommand('deploy', 'Deploy application', () => { + return [ + { value: 'dev' }, + { value: 'staging' }, + { value: 'prod' }, + ]; +}); +``` + +## Shell Integration + +### Handle Spaces Correctly + +Pay attention to the `endsWithSpace` parameter to provide appropriate completions: + +```ts +completion.addOption('dev', '--config', 'Config file', async (previousArgs, toComplete, endsWithSpace) => { + if (endsWithSpace) { + // User typed a space, suggest files + return [ + { value: 'config.json', description: 'JSON config file' }, + { value: 'config.yaml', description: 'YAML config file' }, + ]; + } else { + // User is typing, filter based on input + const suggestions = [ + { value: 'config.json', description: 'JSON config file' }, + { value: 'config.yaml', description: 'YAML config file' }, + ]; + + return suggestions.filter(s => s.value.startsWith(toComplete)); + } +}); +``` + +### Support Multiple Shells + +Test your completions across different shells to ensure compatibility: + +```ts +// Generate completion scripts for all supported shells +if (process.argv[2] === '--') { + await completion.parse(process.argv.slice(2), 'start'); +} else { + const shell = process.argv[2]; + if (['zsh', 'bash', 'fish', 'powershell'].includes(shell)) { + script(shell, 'my-cli', execPath); + } else { + console.error(`Unsupported shell: ${shell}`); + process.exit(1); + } +} +``` + +## User Experience + +### Progressive Disclosure + +Show relevant completions based on what the user has already typed: + +```ts +completion.addCommand('deploy', 'Deploy application', async (previousArgs, toComplete, endsWithSpace) => { + // If user typed 'prod', only show production-related options + if (toComplete.startsWith('prod')) { + return [ + { value: 'production', description: 'Production environment' }, + ]; + } + + // Show all options otherwise + return [ + { value: 'development', description: 'Development environment' }, + { value: 'staging', description: 'Staging environment' }, + { value: 'production', description: 'Production environment' }, + ]; +}); +``` + +### Consistent Naming + +Use consistent naming patterns across your CLI: + +```ts +// ✅ Good: Consistent naming +completion.addCommand('deploy', 'Deploy application', () => { + return [ + { value: 'dev', description: 'Deploy to development' }, + { value: 'staging', description: 'Deploy to staging' }, + { value: 'prod', description: 'Deploy to production' }, + ]; +}); + +completion.addCommand('build', 'Build application', () => { + return [ + { value: 'dev', description: 'Development build' }, + { value: 'staging', description: 'Staging build' }, + { value: 'prod', description: 'Production build' }, + ]; +}); +``` + +### Error Handling + +Handle errors gracefully in completion handlers: + +```ts +completion.addOption('deploy', '--config', 'Config file', async () => { + try { + const files = await fs.readdir('.'); + return files + .filter(f => f.endsWith('.json') || f.endsWith('.yaml')) + .map(f => ({ value: f, description: `Config file: ${f}` })); + } catch (error) { + // Return empty array instead of throwing + console.error('Error reading config files:', error); + return []; + } +}); +``` + +## Performance Considerations + +### Cache Expensive Operations + +Cache results for expensive operations that don't change frequently: + +```ts +let cachedPorts: Array<{ value: string; description: string }> | null = null; + +completion.addOption('dev', '--port', 'Port number', async () => { + if (cachedPorts) { + return cachedPorts; + } + + // Expensive operation (e.g., reading from config) + const ports = await getAvailablePorts(); + cachedPorts = ports.map(p => ({ + value: p.toString(), + description: `Port ${p}` + })); + + return cachedPorts; +}); +``` + +### Limit Result Sets + +Don't overwhelm users with too many completions: + +```ts +completion.addOption('search', '--file', 'File pattern', async (previousArgs, toComplete, endsWithSpace) => { + const files = await getMatchingFiles(toComplete); + + // Limit to 20 results to avoid overwhelming the user + return files.slice(0, 20).map(f => ({ + value: f, + description: `File: ${f}` + })); +}); +``` + +## Testing + +### Test Completion Handlers + +Write tests for your completion handlers to ensure they work correctly: + +```ts +import { Completion } from '@bombsh/tab'; + +describe('CLI Completions', () => { + let completion: Completion; + + beforeEach(() => { + completion = new Completion(); + // Setup your completion handlers + }); + + test('should suggest ports for --port option', async () => { + const result = await completion.parse(['--port'], 'dev'); + expect(result).toContainEqual({ value: '3000', description: 'Development port' }); + }); + + test('should filter suggestions based on input', async () => { + const result = await completion.parse(['--port', '30'], 'dev'); + expect(result).toContainEqual({ value: '3000', description: 'Development port' }); + expect(result).not.toContainEqual({ value: '8080', description: 'Production port' }); + }); +}); +``` + +### Test Shell Integration + +Test that your completion scripts work correctly in different shells: + +```bash +# Test zsh completion +source <(my-cli complete zsh) +my-cli dev --po # Should suggest --port + +# Test bash completion +source <(my-cli complete bash) +my-cli dev --po # Should suggest --port +``` + +## Common Patterns + +### File Completions + +```ts +completion.addOption('build', '--config', 'Config file', async (previousArgs, toComplete, endsWithSpace) => { + const files = await fs.readdir('.'); + return files + .filter(f => f.endsWith('.json') || f.endsWith('.yaml')) + .map(f => ({ value: f, description: `Config: ${f}` })); +}); +``` + +### Environment Completions + +```ts +completion.addOption('deploy', '--env', 'Environment', () => { + return [ + { value: 'development', description: 'Development environment' }, + { value: 'staging', description: 'Staging environment' }, + { value: 'production', description: 'Production environment' }, + ]; +}); +``` + +### Command Completions + +```ts +completion.addCommand('deploy', 'Deploy application', () => { + return [ + { value: 'dev', description: 'Deploy to development' }, + { value: 'staging', description: 'Deploy to staging' }, + { value: 'prod', description: 'Deploy to production' }, + ]; +}); +``` + +## Next Steps + +- Check out [Examples](/docs/tab/guides/examples/) for more practical use cases +- Explore the [API Reference](/docs/tab/api/core/) for detailed documentation +- Learn about [Framework Adapters](/docs/tab/guides/adapters/) for easier integration \ No newline at end of file diff --git a/src/content/docs/tab/guides/examples.mdx b/src/content/docs/tab/guides/examples.mdx new file mode 100644 index 0000000..6ae12a0 --- /dev/null +++ b/src/content/docs/tab/guides/examples.mdx @@ -0,0 +1,512 @@ +--- +title: Examples +description: Practical examples of using Tab with different CLI frameworks and scenarios +--- + +This guide provides practical examples of using Tab with different CLI frameworks and common scenarios you might encounter when building CLI tools. + +## Basic CLI with Core API + +A simple CLI tool with basic autocompletions: + +```ts +#!/usr/bin/env node +import { Completion, script } from '@bombsh/tab'; + +const name = 'my-cli'; +const completion = new Completion(); + +// Add command completions with positional arguments +completion.addCommand('dev', 'Start development server', [false], () => { + return [ + { value: 'dev', description: 'Start in development mode' }, + { value: 'prod', description: 'Start in production mode' }, + ]; +}); + +completion.addCommand('build', 'Build for production', [false], () => { + return [ + { value: 'build', description: 'Build for production' }, + { value: 'build:dev', description: 'Build for development' }, + ]; +}); + +// Add option completions +completion.addOption('dev', '--port', 'Port number', () => { + return [ + { value: '3000', description: 'Development port' }, + { value: '8080', description: 'Production port' }, + ]; +}, 'p'); + +completion.addOption('dev', '--host', 'Host address', () => { + return [ + { value: 'localhost', description: 'Local development' }, + { value: '0.0.0.0', description: 'All interfaces' }, + ]; +}, 'h'); + +// Helper function to quote paths with spaces +function quoteIfNeeded(path: string) { + return path.includes(' ') ? `'${path}'` : path; +} + +// Get the executable path for shell completion +const execPath = process.execPath; +const processArgs = process.argv.slice(1); +const quotedExecPath = quoteIfNeeded(execPath); +const quotedProcessArgs = processArgs.map(quoteIfNeeded); +const quotedProcessExecArgs = process.execArgv.map(quoteIfNeeded); +const x = `${quotedExecPath} ${quotedProcessExecArgs.join(' ')} ${quotedProcessArgs[0]}`; + +// Handle completion requests +if (process.argv[2] === '--') { + await completion.parse(process.argv.slice(2)); +} else { + script(process.argv[2], name, x); +} +``` + +## CAC Framework Example + +A more complex CLI using CAC with Tab integration: + +```ts +#!/usr/bin/env node +import cac from 'cac'; +import tab from '@bombsh/tab/cac'; + +const cli = cac('my-cli'); + +// Define commands +cli.command('dev', 'Start development server') + .option('--port ', 'Specify port', { default: 3000 }) + .option('--host ', 'Specify host', { default: 'localhost' }) + .option('--config ', 'Config file') + .action((options) => { + console.log('Starting dev server...', options); + }); + +cli.command('build', 'Build for production') + .option('--mode ', 'Build mode', { default: 'production' }) + .option('--out-dir ', 'Output directory') + .action((options) => { + console.log('Building...', options); + }); + +cli.command('deploy', 'Deploy application') + .option('--env ', 'Deployment environment') + .option('--region ', 'Deployment region') + .action((options) => { + console.log('Deploying...', options); + }); + +// Initialize tab completion +const completion = await tab(cli); + +// Configure custom completions +for (const command of completion.commands.values()) { + if (command.name === 'dev') { + const portOption = command.options.get('--port'); + if (portOption) { + portOption.handler = async () => { + return [ + { value: '3000', description: 'Development port' }, + { value: '8080', description: 'Production port' }, + ]; + }; + } + + const hostOption = command.options.get('--host'); + if (hostOption) { + hostOption.handler = async () => { + return [ + { value: 'localhost', description: 'Local development' }, + { value: '0.0.0.0', description: 'All interfaces' }, + ]; + }; + } + + const configOption = command.options.get('--config'); + if (configOption) { + configOption.handler = async () => { + return [ + { value: 'vite.config.ts', description: 'Vite config' }, + { value: 'vite.config.js', description: 'Vite config' }, + ]; + }; + } + } + + if (command.name === 'build') { + const modeOption = command.options.get('--mode'); + if (modeOption) { + modeOption.handler = async () => { + return [ + { value: 'development', description: 'Development build' }, + { value: 'production', description: 'Production build' }, + ]; + }; + } + + const outDirOption = command.options.get('--out-dir'); + if (outDirOption) { + outDirOption.handler = async () => { + return [ + { value: 'dist', description: 'Distribution directory' }, + { value: 'build', description: 'Build directory' }, + ]; + }; + } + } + + if (command.name === 'deploy') { + const envOption = command.options.get('--env'); + if (envOption) { + envOption.handler = async () => { + return [ + { value: 'staging', description: 'Staging environment' }, + { value: 'production', description: 'Production environment' }, + ]; + }; + } + + const regionOption = command.options.get('--region'); + if (regionOption) { + regionOption.handler = async () => { + return [ + { value: 'us-east-1', description: 'US East (N. Virginia)' }, + { value: 'us-west-2', description: 'US West (Oregon)' }, + { value: 'eu-west-1', description: 'Europe (Ireland)' }, + ]; + }; + } + } +} + +cli.parse(); +``` + +## Citty Framework Example + +A CLI using Citty with Tab integration: + +```ts +#!/usr/bin/env node +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 development server', + }, + args: { + port: { type: 'string', description: 'Specify port' }, + host: { type: 'string', description: 'Specify host' }, + config: { type: 'string', description: 'Config file' }, + }, +}); + +const buildCommand = defineCommand({ + meta: { + name: 'build', + description: 'Build for production', + }, + args: { + mode: { type: 'string', description: 'Build mode' }, + outDir: { type: 'string', description: 'Output directory' }, + }, +}); + +main.subCommands = { + dev: devCommand, + build: buildCommand, +}; + +const completion = await tab(main); + +// Configure completions +const devCommandCompletion = completion.commands.get('dev'); +if (devCommandCompletion) { + const portOption = devCommandCompletion.options.get('--port'); + if (portOption) { + portOption.handler = async () => { + return [ + { value: '3000', description: 'Development port' }, + { value: '8080', description: 'Production port' }, + ]; + }; + } + + const hostOption = devCommandCompletion.options.get('--host'); + if (hostOption) { + hostOption.handler = async () => { + return [ + { value: 'localhost', description: 'Local development' }, + { value: '0.0.0.0', description: 'All interfaces' }, + ]; + }; + } + + const configOption = devCommandCompletion.options.get('--config'); + if (configOption) { + configOption.handler = async () => { + return [ + { value: 'vite.config.ts', description: 'Vite config' }, + { value: 'vite.config.js', description: 'Vite config' }, + ]; + }; + } +} + +const buildCommandCompletion = completion.commands.get('build'); +if (buildCommandCompletion) { + const modeOption = buildCommandCompletion.options.get('--mode'); + if (modeOption) { + modeOption.handler = async () => { + return [ + { value: 'development', description: 'Development build' }, + { value: 'production', description: 'Production build' }, + ]; + }; + } + + const outDirOption = buildCommandCompletion.options.get('--out-dir'); + if (outDirOption) { + outDirOption.handler = async () => { + return [ + { value: 'dist', description: 'Distribution directory' }, + { value: 'build', description: 'Build directory' }, + ]; + }; + } +} + +const cli = createMain(main); +cli(); +``` + +## Commander Framework Example + +A CLI using Commander.js with Tab integration: + +```ts +#!/usr/bin/env node +import { Command } from 'commander'; +import tab from '@bombsh/tab/commander'; + +const program = new Command(); + +program + .name('my-cli') + .description('My CLI tool') + .version('1.0.0'); + +program + .command('dev') + .description('Start development server') + .option('-p, --port ', 'Specify port', '3000') + .option('-h, --host ', 'Specify host', 'localhost') + .option('-c, --config ', 'Config file') + .action((options) => { + console.log('Starting dev server...', options); + }); + +program + .command('build') + .description('Build for production') + .option('-m, --mode ', 'Build mode', 'production') + .option('-o, --out-dir ', 'Output directory', 'dist') + .action((options) => { + console.log('Building...', options); + }); + +program + .command('deploy') + .description('Deploy application') + .option('-e, --env ', 'Deployment environment') + .option('-r, --region ', 'Deployment region') + .action((options) => { + console.log('Deploying...', options); + }); + +// Initialize tab completion +const completion = tab(program); + +// Configure custom completions +const devCommandCompletion = completion.commands.get('dev'); +if (devCommandCompletion) { + const portOption = devCommandCompletion.options.get('--port'); + if (portOption) { + portOption.handler = async () => { + return [ + { value: '3000', description: 'Development port' }, + { value: '8080', description: 'Production port' }, + ]; + }; + } + + const hostOption = devCommandCompletion.options.get('--host'); + if (hostOption) { + hostOption.handler = async () => { + return [ + { value: 'localhost', description: 'Local development' }, + { value: '0.0.0.0', description: 'All interfaces' }, + ]; + }; + } + + const configOption = devCommandCompletion.options.get('--config'); + if (configOption) { + configOption.handler = async () => { + return [ + { value: 'vite.config.ts', description: 'Vite config' }, + { value: 'vite.config.js', description: 'Vite config' }, + ]; + }; + } +} + +const buildCommandCompletion = completion.commands.get('build'); +if (buildCommandCompletion) { + const modeOption = buildCommandCompletion.options.get('--mode'); + if (modeOption) { + modeOption.handler = async () => { + return [ + { value: 'development', description: 'Development build' }, + { value: 'production', description: 'Production build' }, + ]; + }; + } + + const outDirOption = buildCommandCompletion.options.get('--out-dir'); + if (outDirOption) { + outDirOption.handler = async () => { + return [ + { value: 'dist', description: 'Distribution directory' }, + { value: 'build', description: 'Build directory' }, + ]; + }; + } +} + +const deployCommandCompletion = completion.commands.get('deploy'); +if (deployCommandCompletion) { + const envOption = deployCommandCompletion.options.get('--env'); + if (envOption) { + envOption.handler = async () => { + return [ + { value: 'staging', description: 'Staging environment' }, + { value: 'production', description: 'Production environment' }, + ]; + }; + } + + const regionOption = deployCommandCompletion.options.get('--region'); + if (regionOption) { + regionOption.handler = async () => { + return [ + { value: 'us-east-1', description: 'US East (N. Virginia)' }, + { value: 'us-west-2', description: 'US West (Oregon)' }, + { value: 'eu-west-1', description: 'Europe (Ireland)' }, + ]; + }; + } +} + +program.parse(); +``` + +## Advanced Examples + +### Dynamic File Completions + +```ts +import { readdir } from 'fs/promises'; +import { join } from 'path'; + +// Dynamic file completion +configOption.handler = async () => { + try { + const files = await readdir('.'); + const configFiles = files.filter(f => + f.endsWith('.json') || f.endsWith('.js') || f.endsWith('.ts') + ); + + return configFiles.map(f => ({ + value: f, + description: `Config file: ${f}` + })); + } catch (error) { + return []; + } +}; +``` + +### Context-Aware Completions + +```ts +// Context-aware completion based on previous arguments +portOption.handler = async (previousArgs, toComplete, endsWithSpace) => { + // Check if user is typing a specific port + if (toComplete.startsWith('30')) { + return [ + { value: '3000', description: 'Development port' }, + ]; + } + + // Check if user specified a host that affects port suggestions + const hostIndex = previousArgs.indexOf('--host'); + if (hostIndex !== -1 && hostIndex < previousArgs.length - 1) { + const host = previousArgs[hostIndex + 1]; + if (host === '0.0.0.0') { + return [ + { value: '8080', description: 'Production port' }, + { value: '9000', description: 'Alternative port' }, + ]; + } + } + + return [ + { value: '3000', description: 'Development port' }, + { value: '8080', description: 'Production port' }, + { value: '9000', description: 'Alternative port' }, + ]; +}; +``` + +### Cached Completions + +```ts +// Cache expensive operations +let cachedPorts: Item[] | null = null; + +portOption.handler = async () => { + if (cachedPorts) { + return cachedPorts; + } + + // Expensive operation to get available ports + const ports = await getAvailablePorts(); + cachedPorts = ports.slice(0, 20).map(port => ({ + value: port.toString(), + description: `Port ${port}` + })); + + return cachedPorts; +}; +``` + +## Next Steps + +- Learn about [Best Practices](/docs/tab/guides/best-practices/) for effective autocompletions +- Explore the [API Reference](/docs/tab/api/core/) for advanced usage +- Check out the [Framework Adapters](/docs/tab/guides/adapters/) for easier integration \ No newline at end of file diff --git a/src/content/docs/tab/index.mdx b/src/content/docs/tab/index.mdx new file mode 100644 index 0000000..d7b8802 --- /dev/null +++ b/src/content/docs/tab/index.mdx @@ -0,0 +1,76 @@ +--- +title: Tab +description: Shell autocompletions for JavaScript CLI tools +--- + +import { CardGrid, Card } from '@astrojs/starlight/components'; + +Tab is a powerful tool that brings shell autocompletions to JavaScript CLI tools. Inspired by tools like git and their excellent autocompletion experience, Tab makes the same functionality available for any JavaScript CLI project. + +## Features + + + + + Seamless integration with zsh, bash, fish, and PowerShell. Get native autocompletion experience for your CLI tools. + + + + Built-in adapters for popular CLI frameworks including CAC, Citty, and Commander.js for easy integration. + + + + Full TypeScript support with type-safe completion handlers and autocomplete suggestions. + + + + Define custom completion logic for commands and options with dynamic suggestions based on context. + + + + Works with any JavaScript CLI tool, regardless of the underlying framework or architecture. + + + + Simple installation and configuration with minimal code changes to add autocompletion to your CLI. + + + + +## Quick Start + +Add shell autocompletions to your CLI in just a few lines: + +```ts +import { Completion, script } from '@bombsh/tab'; + +const completion = new Completion(); + +completion.addCommand('dev', 'Start development server', [false], async () => { + return [ + { value: 'dev', description: 'Start in development mode' }, + { value: 'prod', description: 'Start in production mode' }, + ]; +}); + +completion.addOption('dev', '--port', 'Port number', async () => { + return [ + { value: '3000', description: 'Development port' }, + { value: '8080', description: 'Production port' }, + ]; +}, 'p'); + +// Generate shell completion script +if (process.argv[2] === '--') { + await completion.parse(process.argv.slice(2)); +} else { + script(process.argv[2], 'my-cli', process.execPath); +} +``` + +## What's Next? + +- [Getting Started](/docs/tab/basics/getting-started/) - Learn how to add autocompletions to your CLI +- [Framework Adapters](/docs/tab/guides/adapters/) - Use Tab with CAC, Citty, and Commander.js +- [Best Practices](/docs/tab/guides/best-practices/) - Learn best practices for implementing autocompletions +- [API Reference](/docs/tab/api/core/) - Detailed API documentation \ No newline at end of file -- 2.51.2