Bunli
PackagesPlugin Packages

@bunli/plugin-completions

Shell completion generation plugin for Bunli

Installation

bun add @bunli/plugin-completions

Features

  • 🐚 Multi-shell support: Generate completions for Bash, Zsh, Fish, and PowerShell
  • 🎯 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";
import build from "./commands/build.js";
import deploy from "./commands/deploy.js";

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

cli.command(deploy);
cli.command(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()] as const,
});

Generating Completions

The plugin adds a completions command to your CLI:

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

Installation Methods

Bash Completions

Option 1 - Current session:

source <(my-cli completions bash)

Option 2 - Save to completion directory:

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

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

Option 3 - Add to ~/.bashrc:

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

Zsh Completions

Option 1 - Current session:

source <(my-cli completions zsh)

Option 2 - User completion directory:

mkdir -p ~/.zsh/completions
my-cli completions 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 zsh > /usr/local/share/zsh/site-functions/_my-cli
# Restart your shell

Fish Completions

User completions:

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

System-wide:

sudo my-cli completions 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 {
  /**
   * Path to Bunli-generated command metadata.
   * Default: ".bunli/commands.gen.ts" (relative to process.cwd()).
   */
  generatedPath?: string;

  /**
   * Name registered with the shell completion script (usually the package.json bin key).
   */
  commandName?: string;

  /**
   * Executable invocation used by shell scripts (can include args, e.g. "bunx my-cli").
   * Default: commandName.
   */
  executable?: string;

  /**
   * Whether to register command aliases from generated metadata.
   * Default: true.
   */
  includeAliases?: boolean;

  /**
   * Whether to include Bunli global flags on every command.
   * Default: true.
   */
  includeGlobalFlags?: boolean;
}

Shell Types

The plugin supports four shells:

PowerShell Completions

User profile:

my-cli completions powershell >> $PROFILE
# Reload profile:
. $PROFILE

Or save to a file:

my-cli completions powershell > ~/Documents/PowerShell/my-cli_completions.ps1
# Add to $PROFILE:
. ~/Documents/PowerShell/my-cli_completions.ps1

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 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}\n`);

Documentation for Users

Include installation instructions in your README:

## Shell Completions

Enable tab completion for your shell:

### Bash

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

### Zsh

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

### Fish

```bash
my-cli completions 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