Skip to main content
Every incur CLI has a built-in completions command that generates shell hook scripts for tab completion. The hook calls back into your binary at every tab press, so completions are always in sync with your commands.

Installation

Generate and install completions for your shell:

Bash

eval "$(my-cli completions bash)"
Add to ~/.bashrc for persistence:
echo 'eval "$(my-cli completions bash)"' >> ~/.bashrc

Zsh

eval "$(my-cli completions zsh)"
Add to ~/.zshrc for persistence:
echo 'eval "$(my-cli completions zsh)"' >> ~/.zshrc

Fish

my-cli completions fish | source
Add to ~/.config/fish/config.fish for persistence:
echo 'my-cli completions fish | source' >> ~/.config/fish/config.fish

Nushell

let _incur_complete_my_cli = {|spans|
    COMPLETE=nushell my-cli -- ...$spans | from json
}
Add to your nushell config and register as an external completer.

How It Works

Completions are dynamic—the hook calls back into your CLI at every tab press:
$ my-cli <TAB>

    Shell hook invokes: COMPLETE=bash my-cli -- my-cli

    CLI returns: "install\vstatus\vdeploy"

    Shell displays: install  status  deploy
This means:
  • Completions are always accurate
  • No generated files to maintain
  • Subcommands, options, and enum values are suggested automatically
  • Works with mounted CLIs and OpenAPI-generated commands

Command Completions

Tab completion suggests available commands:
$ my-cli <TAB>
install    # Install a package
status     # Show repo status
deploy     # Deploy to an environment

Subcommand Completions

Command groups suppress trailing space so you can keep tabbing:
$ my-cli pr <TAB>
list       # List pull requests
create     # Create a pull request
merge      # Merge a pull request

$ my-cli pr list <TAB>
--state    # Filter by state

Option Completions

Options are suggested when the current word starts with -:
$ my-cli install --<TAB>
--saveDev  # Save as dev dependency
--force    # Force install

$ my-cli install -<TAB>
-D         # Save as dev dependency
-f         # Force install
Short aliases are automatically suggested.

Enum Value Completions

Enum options suggest their valid values:
import { Cli, z } from 'incur'

Cli.create('my-cli', { description: 'My CLI' })
  .command('deploy', {
    args: z.object({ 
      env: z.enum(['staging', 'production']) 
    }),
    run(c) {
      return { deployed: c.args.env }
    },
  })
  .serve()
$ my-cli deploy <TAB>
staging
production

Boolean Options

Boolean options don’t expect values, so completion continues:
import { Cli, z } from 'incur'

Cli.create('my-cli', { description: 'My CLI' })
  .command('install', {
    options: z.object({ 
      saveDev: z.boolean().optional(),
      force: z.boolean().optional(),
    }),
    run() {
      return { installed: true }
    },
  })
  .serve()
$ my-cli install --saveDev <TAB>
--force    # Next option suggestion

Descriptions in Completions

Descriptions from schemas appear in completions:
import { Cli, z } from 'incur'

Cli.create('my-cli', { description: 'My CLI' })
  .command('deploy', {
    description: 'Deploy to an environment',
    args: z.object({ 
      env: z.enum(['staging', 'production']).describe('Target environment') 
    }),
    options: z.object({ 
      force: z.boolean().optional().describe('Force deployment') 
    }),
    run(c) {
      return { deployed: c.args.env }
    },
  })
  .serve()
In zsh and fish, descriptions appear alongside completions:
$ my-cli <TAB>
deploy -- Deploy to an environment

$ my-cli deploy --<TAB>
--force -- Force deployment

Multi-Command CLI

Completions work with complex command structures:
import { Cli, z } from 'incur'

const pr = Cli.create('pr', { description: 'Pull request commands' })
  .command('list', {
    description: 'List pull requests',
    options: z.object({ 
      state: z.enum(['open', 'closed', 'all']).default('open') 
    }),
    run(c) {
      return { prs: [] }
    },
  })

const cli = Cli.create('my-cli', { description: 'My CLI' })
  .command(pr)
  .command('deploy', {
    description: 'Deploy to an environment',
    run() {
      return { deployed: true }
    },
  })

cli.serve()
$ my-cli <TAB>
pr         # Pull request commands
deploy     # Deploy to an environment

$ my-cli pr <TAB>
list       # List pull requests

$ my-cli pr list --state <TAB>
open
closed
all

Completion Help

Run completions --help for setup instructions:
$ my-cli completions --help
Output
my-cli completions – Generate shell completion script

Usage: my-cli completions <shell>

Arguments:
  shell  Shell to generate completions for (bash, zsh, fish, nushell)

Examples:
  # Bash
  eval "$(my-cli completions bash)"
  echo 'eval "$(my-cli completions bash)"' >> ~/.bashrc

  # Zsh
  eval "$(my-cli completions zsh)"
  echo 'eval "$(my-cli completions zsh)"' >> ~/.zshrc

  # Fish
  my-cli completions fish | source
  echo 'my-cli completions fish | source' >> ~/.config/fish/config.fish

Implementation

Completions are handled automatically. No additional code needed:
import { Cli, z } from 'incur'

Cli.create('my-cli', { description: 'My CLI' })
  .command('status', {
    description: 'Show repo status',
    run() {
      return { clean: true }
    },
  })
  .serve()
This CLI automatically supports:
  • my-cli completions bash
  • my-cli completions zsh
  • my-cli completions fish
  • my-cli completions nushell

Source Code

The completions implementation is in src/Completions.ts:

Register Hook

import { register } from 'incur/Completions'

const hook = register('bash', 'my-cli')
// Returns shell-specific hook script

Complete Function

import { complete, format } from 'incur/Completions'

const candidates = complete(
  commands,      // Command map
  rootCommand,   // Root command definition
  argv,          // Current command line tokens
  cursorIndex,   // Index of word being completed
)

const output = format('bash', candidates)
// Returns shell-specific formatted output

Environment Variables

Completions use these environment variables:
VariableDescription
COMPLETEShell type: bash, zsh, fish, nushell
_COMPLETE_INDEXIndex of the word being completed
The shell hook sets these when invoking the CLI.

Debugging Completions

Test completions manually:
# Bash
COMPLETE=bash _COMPLETE_INDEX=1 my-cli -- my-cli

# Zsh
COMPLETE=zsh _COMPLETE_INDEX=1 my-cli -- my-cli

# Fish
COMPLETE=fish my-cli -- my-cli ''

# Nushell
COMPLETE=nushell my-cli -- my-cli | from json
This simulates what the shell does when you press tab.

Multiple Binaries

If your CLI has multiple binary names (aliases in package.json), completions work for all of them:
import { Cli } from 'incur'

Cli.create('my-cli', {
  description: 'My CLI',
  aliases: ['mc', 'mycli'],
})
  .command('status', {
    run() {
      return { clean: true }
    },
  })
  .serve()
# Install completions for all aliases
eval "$(my-cli completions bash)"
eval "$(mc completions bash)"
eval "$(mycli completions bash)"

Advanced: Custom Completions

For advanced use cases, you can use the completions API directly:
import * as Completions from 'incur/Completions'

const candidates: Completions.Candidate[] = [
  { value: 'install', description: 'Install a package' },
  { value: 'status', description: 'Show status', noSpace: true },
]

const output = Completions.format('bash', candidates)
console.log(output)
Completions are dynamic and always in sync with your commands. Just install once per shell and they’ll work forever.

Build docs developers (and LLMs) love