Bunli
PackagesPlugin Packages

@bunli/plugin-completions

Shell completion generation plugin for Bunli

@bunli/plugin-completions

The completions plugin automatically generates shell completion scripts for your Bunli CLI applications. It supports Bash, Zsh, and Fish shells with full support for commands, options, flags, and enum value completions.

Installation

bun add @bunli/plugin-completions

Features

  • 🐚 Multi-shell support: Generate completions for Bash, Zsh, and Fish
  • 🎯 Type-aware: Leverages command metadata from Bunli's type generation
  • 📋 Enum completions: Automatically completes enum option values
  • Zero configuration: Works out of the box with your existing commands
  • 🔧 Flexible output: Save to file or output to stdout
  • 📝 Shell-specific syntax: Generates idiomatic completions for each shell
  • 🚀 Auto-generated: No manual completion script maintenance required

Basic Usage

import { createCLI } from '@bunli/core'
import { completionsPlugin } from '@bunli/plugin-completions'

const cli = await createCLI({
  name: 'my-cli',
  version: '1.0.0',
  plugins: [completionsPlugin()] as const
})

await cli.load({
  deploy: () => import('./commands/deploy'),
  build: () => import('./commands/build')
})

await cli.run()

Or in your bunli.config.ts:

import { defineConfig } from '@bunli/core'
import { completionsPlugin } from '@bunli/plugin-completions'

export default defineConfig({
  name: 'my-cli',
  plugins: [completionsPlugin()]
})

Generating Completions

The plugin adds a completions command to your CLI:

# Output to stdout with installation instructions
my-cli completions --shell bash
my-cli completions --shell zsh
my-cli completions --shell fish

# Save to file
my-cli completions --shell bash --output completions.sh
my-cli completions -s zsh -o _my-cli
my-cli completions -s fish -o my-cli.fish

Command Options

Required Options

  • --shell, -s <shell>: Target shell for completion generation
    • Values: bash, zsh, fish

Optional Options

  • --output, -o <path>: Output file path (default: stdout)

Installation Methods

Bash Completions

Option 1 - Current session:

source <(my-cli completions --shell bash)

Option 2 - Save to completion directory:

# Linux
my-cli completions --shell bash > /etc/bash_completion.d/my-cli

# macOS with Homebrew
my-cli completions --shell bash > $(brew --prefix)/etc/bash_completion.d/my-cli

Option 3 - Add to ~/.bashrc:

echo 'source <(my-cli completions --shell bash)' >> ~/.bashrc
source ~/.bashrc

Zsh Completions

Option 1 - Current session:

source <(my-cli completions --shell zsh)

Option 2 - User completion directory:

mkdir -p ~/.zsh/completions
my-cli completions --shell zsh > ~/.zsh/completions/_my-cli

# Add to ~/.zshrc if not already present:
echo 'fpath=(~/.zsh/completions $fpath)' >> ~/.zshrc
echo 'autoload -U compinit && compinit' >> ~/.zshrc
source ~/.zshrc

Option 3 - System-wide:

sudo my-cli completions --shell zsh > /usr/local/share/zsh/site-functions/_my-cli
# Restart your shell

Fish Completions

User completions:

my-cli completions --shell fish > ~/.config/fish/completions/my-cli.fish
# Completions will be available in new fish sessions

System-wide:

sudo my-cli completions --shell fish > /usr/share/fish/vendor_completions.d/my-cli.fish

How It Works

The plugin leverages Bunli's metadata generation system to create accurate completions:

  1. Metadata Extraction: Reads the .bunli/commands.gen.ts file generated by Bunli
  2. Command Structure: Parses command names, descriptions, options, and flags
  3. Type Information: Uses Zod schema metadata to extract enum values and validation rules
  4. Shell-Specific Generation: Generates appropriate completion syntax for each shell

Examples

Enum Value Completions

Given a command with enum options:

import { defineCommand, option } from '@bunli/core'
import { z } from 'zod'

export default defineCommand({
  name: 'deploy',
  description: 'Deploy application',
  options: {
    environment: option(
      z.enum(['development', 'staging', 'production']),
      {
        short: 'e',
        description: 'Target environment'
      }
    ),
    region: option(
      z.enum(['us-east-1', 'us-west-2', 'eu-west-1']),
      {
        short: 'r',
        description: 'AWS region'
      }
    )
  },
  handler: async ({ flags }) => {
    console.log(`Deploying to ${flags.environment} in ${flags.region}`)
  }
})

The completions plugin will generate:

Bash:

# Provides enum completions for --environment
if [[ "$prev" == "--environment" || "$prev" == "-e" ]]; then
  COMPREPLY=($(compgen -W "development staging production" -- "$cur"))
  return
fi

# Provides enum completions for --region
if [[ "$prev" == "--region" || "$prev" == "-r" ]]; then
  COMPREPLY=($(compgen -W "us-east-1 us-west-2 eu-west-1" -- "$cur"))
  return
fi

Zsh:

"(-e --environment)"{-e,--environment}[Target environment]:(development staging production)
"(-r --region)"{-r,--region}[AWS region]:(us-east-1 us-west-2 eu-west-1)

Fish:

complete -c my-cli -n '__fish_seen_subcommand_from deploy' \
  -l environment -s e -d 'Target environment' -r -a 'development staging production'
complete -c my-cli -n '__fish_seen_subcommand_from deploy' \
  -l region -s r -d 'AWS region' -r -a 'us-east-1 us-west-2 eu-west-1'

Multiple Commands

// commands/build.ts
export default defineCommand({
  name: 'build',
  description: 'Build the project',
  options: {
    mode: option(z.enum(['development', 'production']), {
      short: 'm',
      description: 'Build mode'
    })
  },
  handler: async ({ flags }) => {
    console.log(`Building in ${flags.mode} mode`)
  }
})

// commands/test.ts
export default defineCommand({
  name: 'test',
  description: 'Run tests',
  options: {
    coverage: option(z.boolean().optional(), {
      short: 'c',
      description: 'Generate coverage report'
    })
  },
  handler: async ({ flags }) => {
    console.log('Running tests...')
  }
})

Completions will include both commands:

# Bash
my-cli <TAB>
# Shows: build test --help --version

my-cli build --mode <TAB>
# Shows: development production

Boolean Flags

Boolean flags are automatically detected and don't require values:

export default defineCommand({
  name: 'serve',
  options: {
    watch: option(z.boolean().optional(), {
      short: 'w',
      description: 'Watch for changes'
    }),
    open: option(z.boolean().optional(), {
      short: 'o',
      description: 'Open in browser'
    })
  },
  handler: async ({ flags }) => {
    // ...
  }
})
my-cli serve --watch --open  # No values required
my-cli serve -w -o           # Short flags work too

Plugin Options

interface CompletionsPluginOptions {
  // Currently no configuration options
  // Reserved for future features like:
  // - Custom completion handlers
  // - Additional shell support
  // - Completion behavior customization
}

Best Practices

1. Use Enum Types for Fixed Values

// ✅ Good - generates completions
environment: option(z.enum(['dev', 'staging', 'prod']), {
  description: 'Target environment'
})

// ❌ Less useful - no completions
environment: option(z.string(), {
  description: 'Target environment'
})

2. Provide Descriptions

// ✅ Good - shows helpful descriptions
preset: option(z.enum(['minimal', 'standard', 'full']), {
  description: 'Configuration preset'
})

// ❌ Less helpful - no context
preset: option(z.enum(['minimal', 'standard', 'full']))

3. Use Short Flags

// ✅ Good - both long and short forms complete
force: option(z.boolean().optional(), {
  short: 'f',
  description: 'Force operation'
})

4. Generate After Command Changes

Always regenerate completions when you add or modify commands:

# Make changes to commands
# ...

# Regenerate type metadata
bun run generate

# Update installed completions
my-cli completions --shell bash > ~/.bash_completion.d/my-cli

Distribution

Including Completions in Your Package

You can include pre-generated completions in your package:

{
  "name": "my-cli",
  "files": [
    "dist",
    "completions"
  ],
  "scripts": {
    "completions": "bun run cli.js completions",
    "postinstall": "node scripts/install-completions.js"
  }
}

Create a postinstall script to prompt users:

// scripts/install-completions.js
import { execSync } from 'child_process'
import { existsSync, writeFileSync, mkdirSync } from 'fs'
import { homedir } from 'os'
import { join } from 'path'

const shell = process.env.SHELL?.includes('zsh') ? 'zsh'
           : process.env.SHELL?.includes('fish') ? 'fish'
           : 'bash'

console.log(`\n🐚 Shell completions are available for ${shell}`)
console.log(`Run: my-cli completions --shell ${shell}\n`)

Documentation for Users

Include installation instructions in your README:

## Shell Completions

Enable tab completion for your shell:

### Bash
```bash
my-cli completions --shell bash > ~/.bash_completion.d/my-cli
source ~/.bashrc
```

### Zsh
```bash
my-cli completions --shell zsh > ~/.zsh/completions/_my-cli
# Add to ~/.zshrc:
fpath=(~/.zsh/completions $fpath)
autoload -U compinit && compinit
```

### Fish
```bash
my-cli completions --shell fish > ~/.config/fish/completions/my-cli.fish
```

Limitations

  • No subcommand completions: Currently only supports top-level commands
  • No dynamic completions: Completions are static based on command metadata
  • No positional argument completions: Only flags and options are completed
  • No custom completion functions: Cannot define custom completion logic

Troubleshooting

Completions Not Working

  1. Check shell type: Verify you generated completions for the correct shell

    echo $SHELL
  2. Verify installation: Make sure the completion file is in the correct location

    # Bash
    ls ~/.bash_completion.d/
    
    # Zsh
    ls ~/.zsh/completions/
    
    # Fish
    ls ~/.config/fish/completions/
  3. Reload shell: Source your shell config or restart your shell

    # Bash
    source ~/.bashrc
    
    # Zsh
    source ~/.zshrc
    
    # Fish - restart fish

Enum Values Not Completing

  1. Regenerate metadata: Run bun run generate to update command metadata
  2. Check schema: Verify you're using z.enum() not z.string()
  3. Test output: Check the generated completion script includes enum values

Wrong Command Name

The plugin uses the CLI name from createCLI() or bunli.config.ts. Update it:

const cli = await createCLI({
  name: 'correct-cli-name',  // This name is used in completions
  // ...
})

See Also

On this page