Crust logoCrust

Completion

Generate static shell completion scripts for bash, zsh, and fish.

Usage

cli.ts
import { Crust, defineCommand } from "@crustjs/core";
import { completion } from "@crustjs/extensions";

const app = new Crust("my-cli", { version: "1.2.3" })
	.extend(completion())
	.add(
		defineCommand("build", (command) =>
			command
				.flags({ name: "target", type: "string", choices: ["browser", "bun", "node"] })
				.action(() => {}),
		),
	)
	.action(() => {});

await app.execute();
$ my-cli completion bash | sed -n '3,7p'
__my_cli_init_completion() {
	COMPREPLY=()
	cur="${COMP_WORDS[COMP_CWORD]}"
	if (( COMP_CWORD > 0 )); then prev="${COMP_WORDS[COMP_CWORD-1]}"; else prev=""; fi
	words=("${COMP_WORDS[@]}")

completion() adds a completion <shell> root command and a build hook. The generated bash, zsh, and fish scripts contain static commands, flags, and choices, so they do not call the CLI when the user presses Tab.

Quick shell setup

# Bash, in ~/.bashrc
source <(my-cli completion bash)

# Zsh, after compinit in ~/.zshrc
source <(my-cli completion zsh)

# Fish, in ~/.config/fish/config.fish
status --is-interactive; and my-cli completion fish | source

Here, my-cli is the installed application executable. Source output only from a CLI you trust, and use one loading method per shell.

New shells generate completion from the installed CLI at startup. Open a new shell after updating the CLI to refresh the definitions.

API

const snapshot = await app.snapshot();
const bash = renderBashCompletion(snapshot);
const zsh = renderZshCompletion(snapshot);
const fish = renderFishCompletion(snapshot);
CompletionOptions optionTypeDefault
commandstring"completion"
binNamestringRoot command meta.name
versionstringRoot command meta.version

The renderers accept a root Command Snapshot plus binName and version overrides. The runtime command, renderers, and build hook require a version from the option or root metadata.

The positional <shell> accepts bash, zsh, or fish. command changes the contributed command name, while binName changes the executable name embedded in every script.

Value candidates

command.flags({
  name: "target",
  type: "string",
  choices: ["browser", "bun", "node"],
});
// my-cli build --target <Tab> offers browser, bun, and node

String choices become static candidates. path and unconstrained string values use file completion where supported, while url and json values do not.

Boolean flags do not consume a value. Hidden commands and their descendants are omitted.

Package-manager and advanced setup

$ my-cli completion bash --output-dir completions/
$ find completions -maxdepth 1 -type f -printf '%f\n' | sort
_my-cli
my-cli
my-cli.fish

With --output-dir, the required <shell> still selects a valid invocation, but the command writes all three files. It preserves unrelated files in that directory.

Build artifacts

.crust/artifacts/completions/my-cli
.crust/artifacts/completions/_my-cli
.crust/artifacts/completions/my-cli.fish

The build hook renders these three files; crust build writes them and records them as artifacts owned by crust:completion. Set binName when the installed binary differs from the root command name.

Saved files and autoload

mkdir -p "${XDG_DATA_HOME:-$HOME/.local/share}/bash-completion/completions"
my-cli completion bash > "${XDG_DATA_HOME:-$HOME/.local/share}/bash-completion/completions/my-cli"

Saved files avoid running the generator at shell startup. Autoloading the Bash file requires bash-completion to be installed and initialized.

For Zsh, initialize its completion system and explicitly source the saved file:

mkdir -p ~/.zsh/completions
my-cli completion zsh > ~/.zsh/completions/_my-cli
source ~/.zsh/completions/_my-cli # after compinit or framework initialization

Crust's generated Zsh script misses the first Tab completion when loaded from $fpath for binary names containing - or .. Explicit sourcing avoids that issue. Regenerate or replace saved files after CLI changes.

On this page