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:
| Variable | Description |
|---|
COMPLETE | Shell type: bash, zsh, fish, nushell |
_COMPLETE_INDEX | Index 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.