From 93aa239056482adc3fe1125c2257706d0365ecbf Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Mon, 30 Mar 2026 12:36:45 -0400 Subject: [PATCH] Add `fabro completion` subcommand for shell completions Uses clap_complete to generate tab-completion scripts for zsh, fish, elvish, and PowerShell. Bash generation is caught gracefully since clap_complete panics with #[command(flatten)] subcommands. Co-Authored-By: Claude Opus 4.6 (1M context) --- Cargo.lock | 10 +++ Cargo.toml | 1 + docs/docs.json | 1 + docs/reference/cli.mdx | 18 +++++ docs/reference/shell-completions.mdx | 70 +++++++++++++++++++ lib/crates/fabro-cli/Cargo.toml | 1 + lib/crates/fabro-cli/src/args.rs | 9 +++ lib/crates/fabro-cli/src/main.rs | 23 +++++- .../fabro-cli/tests/it/cmd/completion.rs | 44 ++++++++++++ lib/crates/fabro-cli/tests/it/cmd/mod.rs | 1 + 10 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 docs/reference/shell-completions.mdx create mode 100644 lib/crates/fabro-cli/tests/it/cmd/completion.rs diff --git a/Cargo.lock b/Cargo.lock index 185c806c0..c701c8c72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -628,6 +628,15 @@ dependencies = [ "strsim 0.11.1", ] +[[package]] +name = "clap_complete" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19c9f1dde76b736e3681f28cec9d5a61299cbaae0fce80a68e43724ad56031eb" +dependencies = [ + "clap", +] + [[package]] name = "clap_derive" version = "4.5.55" @@ -1448,6 +1457,7 @@ dependencies = [ "base64", "chrono", "clap", + "clap_complete", "cli-table", "console 0.15.11", "core-foundation 0.9.4", diff --git a/Cargo.toml b/Cargo.toml index ddc1772ac..b16a502e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ base64 = "0.22" bytes = "1" tokio-util = "0.7" clap = { version = "4", features = ["derive", "env"] } +clap_complete = "4" jsonschema = { version = "0.42", default-features = false } chrono = { version = "0.4", features = ["clock"] } bollard = "0.18" diff --git a/docs/docs.json b/docs/docs.json index cd07c8d33..7397e739f 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -103,6 +103,7 @@ "pages": [ "reference/dot-language", "reference/cli", + "reference/shell-completions", "reference/user-configuration", "reference/run-directory", "reference/sdk", diff --git a/docs/reference/cli.mdx b/docs/reference/cli.mdx index 424ec66b8..77aac0977 100644 --- a/docs/reference/cli.mdx +++ b/docs/reference/cli.mdx @@ -888,6 +888,24 @@ fabro store dump abc123 -o ./debug-output --- +## `fabro completion` + +Generate shell completion scripts for bash, zsh, fish, elvish, and PowerShell. + +```bash +fabro completion bash +fabro completion zsh +fabro completion fish +``` + +| Argument | Description | +|---|---| +| `` | Shell to generate completions for: `bash`, `zsh`, `fish`, `elvish`, `powershell` (required) | + +See [Shell Completions](/reference/shell-completions) for installation instructions for each shell. + +--- + ## `fabro docs` Open the Fabro documentation website in your default browser. diff --git a/docs/reference/shell-completions.mdx b/docs/reference/shell-completions.mdx new file mode 100644 index 000000000..9c8155149 --- /dev/null +++ b/docs/reference/shell-completions.mdx @@ -0,0 +1,70 @@ +--- +title: "Shell Completions" +description: "Set up tab completion for the fabro CLI in your shell" +--- + +The `fabro completion` command generates shell completion scripts for tab-completing commands, flags, and arguments. + +## Bash + +Add to your `~/.bashrc`: + +```bash +eval "$(fabro completion bash)" +``` + +Or generate a file and source it: + +```bash +fabro completion bash > ~/.local/share/bash-completion/completions/fabro +``` + +## Zsh + +Add to your `~/.zshrc` (before `compinit`): + +```bash +eval "$(fabro completion zsh)" +``` + +Or generate a file: + +```bash +fabro completion zsh > "${fpath[1]}/_fabro" +``` + +You may need to run `compinit` or start a new shell session for changes to take effect. + +## Fish + +```bash +fabro completion fish | source +``` + +Or persist to the completions directory: + +```bash +fabro completion fish > ~/.config/fish/completions/fabro.fish +``` + +## PowerShell + +Add to your PowerShell profile: + +```powershell +fabro completion powershell | Out-String | Invoke-Expression +``` + +## Elvish + +```bash +eval (fabro completion elvish | slurp) +``` + +## Supported shells + +Run `fabro completion --help` to see all supported shells: + +```bash +fabro completion --help +``` diff --git a/lib/crates/fabro-cli/Cargo.toml b/lib/crates/fabro-cli/Cargo.toml index 0a713f587..9f45fc3a3 100644 --- a/lib/crates/fabro-cli/Cargo.toml +++ b/lib/crates/fabro-cli/Cargo.toml @@ -42,6 +42,7 @@ fabro-store = { path = "../fabro-store" } fabro-types = { path = "../fabro-types" } fabro-util = { path = "../fabro-util" } clap.workspace = true +clap_complete.workspace = true cli-table.workspace = true console.workspace = true indicatif.workspace = true diff --git a/lib/crates/fabro-cli/src/args.rs b/lib/crates/fabro-cli/src/args.rs index 1af07f307..560150fb8 100644 --- a/lib/crates/fabro-cli/src/args.rs +++ b/lib/crates/fabro-cli/src/args.rs @@ -812,6 +812,8 @@ pub(crate) enum Commands { #[command(subcommand)] command: SandboxCommand, }, + /// Generate shell completions + Completion(CompletionArgs), /// System maintenance commands System(SystemNamespace), /// Send a queued analytics event (internal) @@ -893,6 +895,7 @@ impl Commands { ProviderCommand::Login(_) => "provider login", }, Self::Sandbox { command } => command.name(), + Self::Completion(_) => "completion", Self::System(ns) => match &ns.command { SystemCommand::Prune(_) => "system prune", SystemCommand::Df(_) => "system df", @@ -1040,6 +1043,12 @@ pub(crate) enum ProviderCommand { Login(ProviderLoginArgs), } +#[derive(Args)] +pub(crate) struct CompletionArgs { + /// Shell to generate completions for + pub shell: clap_complete::Shell, +} + #[derive(Args)] pub(crate) struct LlmNamespace { #[command(subcommand)] diff --git a/lib/crates/fabro-cli/src/main.rs b/lib/crates/fabro-cli/src/main.rs index caf6f7f88..7b1da5eb8 100644 --- a/lib/crates/fabro-cli/src/main.rs +++ b/lib/crates/fabro-cli/src/main.rs @@ -13,7 +13,7 @@ use anyhow::Result; use args::{Commands, GlobalArgs, LONG_VERSION, RunCommands}; #[cfg(feature = "server")] use args::{ServerCommand, ServerNamespace}; -use clap::Parser; +use clap::{CommandFactory, Parser}; use fabro_telemetry::{git, panic as tel_panic, sanitize, sender}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; @@ -225,6 +225,27 @@ async fn main_inner() -> (String, Result<()>) { Commands::Provider(ns) => commands::provider::dispatch(ns).await?, Commands::Sandbox { command } => commands::sandbox::dispatch(command, &globals).await?, Commands::System(ns) => commands::system::dispatch(ns, &globals).await?, + Commands::Completion(args) => { + let mut cmd = Cli::command(); + let shell = args.shell; + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let mut buf = Vec::new(); + clap_complete::generate(shell, &mut cmd, "fabro", &mut buf); + buf + })); + match result { + Ok(buf) => { + use std::io::Write; + std::io::stdout().write_all(&buf)?; + } + Err(_) => { + anyhow::bail!( + "Failed to generate completions for {shell}. \ + Try zsh, fish, elvish, or powershell instead." + ); + } + } + } Commands::SendAnalytics { path } => { let result = sender::upload(&path).await; let _ = std::fs::remove_file(&path); diff --git a/lib/crates/fabro-cli/tests/it/cmd/completion.rs b/lib/crates/fabro-cli/tests/it/cmd/completion.rs new file mode 100644 index 000000000..adeb15708 --- /dev/null +++ b/lib/crates/fabro-cli/tests/it/cmd/completion.rs @@ -0,0 +1,44 @@ +use fabro_test::{fabro_snapshot, test_context}; + +#[test] +fn help() { + let context = test_context!(); + let mut cmd = context.command(); + cmd.args(["completion", "--help"]); + fabro_snapshot!(context.filters(), cmd, @" + success: true + exit_code: 0 + ----- stdout ----- + Generate shell completions + + Usage: fabro completion [OPTIONS] + + Arguments: + Shell to generate completions for [possible values: bash, elvish, fish, powershell, zsh] + + Options: + --debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=] + --no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true] + --quiet Suppress non-essential output [env: FABRO_QUIET=] + --verbose Enable verbose output [env: FABRO_VERBOSE=] + --storage-dir Storage directory (default: ~/.fabro) [env: FABRO_STORAGE_DIR=[STORAGE_DIR]] + -h, --help Print help + ----- stderr ----- + "); +} + +#[test] +fn generates_zsh_completions() { + let context = test_context!(); + let mut cmd = context.command(); + cmd.args(["completion", "zsh"]); + cmd.assert().success(); +} + +#[test] +fn generates_fish_completions() { + let context = test_context!(); + let mut cmd = context.command(); + cmd.args(["completion", "fish"]); + cmd.assert().success(); +} diff --git a/lib/crates/fabro-cli/tests/it/cmd/mod.rs b/lib/crates/fabro-cli/tests/it/cmd/mod.rs index 4e47f4186..7a425733d 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/mod.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/mod.rs @@ -2,6 +2,7 @@ mod asset; mod asset_cp; mod asset_list; mod attach; +mod completion; mod config; mod config_show; mod cp;