From 9dea64c33754f0c46474bec5ccdc2ef0f0fb9e97 Mon Sep 17 00:00:00 2001 From: killagu-claw Date: Mon, 16 Feb 2026 21:09:40 +0800 Subject: [PATCH 1/2] feat(pm): add shell completion generation via `utoo completions ` Uses clap_complete to generate completion scripts for bash, zsh, fish, elvish, and powershell. Co-Authored-By: Claude Opus 4.6 --- Cargo.lock | 10 ++++++ crates/pm/Cargo.toml | 1 + crates/pm/src/constants.rs | 3 ++ crates/pm/src/main.rs | 65 ++++++++++++++++++++++++++++++++++---- 4 files changed, 72 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d6278c391d..b41856c4c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -884,6 +884,15 @@ dependencies = [ "strsim 0.11.1", ] +[[package]] +name = "clap_complete" +version = "4.5.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c757a3b7e39161a4e56f9365141ada2a6c915a8622c408ab6bb4b5d047371031" +dependencies = [ + "clap", +] + [[package]] name = "clap_derive" version = "4.5.47" @@ -9703,6 +9712,7 @@ dependencies = [ "bytes", "chrono", "clap", + "clap_complete", "colored 2.2.0", "dashmap 6.1.0", "deno_semver", diff --git a/crates/pm/Cargo.toml b/crates/pm/Cargo.toml index e7982e9e96..2c4bed88c7 100644 --- a/crates/pm/Cargo.toml +++ b/crates/pm/Cargo.toml @@ -15,6 +15,7 @@ atty = "0.2" bytes = "1.11.0" chrono = { version = "0.4", features = ["serde"] } clap = { workspace = true } +clap_complete = "4" colored = "2.1" dashmap = "6.1.0" deno_semver = "0.7" diff --git a/crates/pm/src/constants.rs b/crates/pm/src/constants.rs index 91ba39870b..65ac144188 100644 --- a/crates/pm/src/constants.rs +++ b/crates/pm/src/constants.rs @@ -57,4 +57,7 @@ pub mod cmd { pub const INIT_NAME: &str = "init"; pub const INIT_ALIAS: &str = "create"; pub const INIT_ABOUT: &str = "Create a package.json file"; + + pub const COMPLETIONS_NAME: &str = "completions"; + pub const COMPLETIONS_ABOUT: &str = "Generate shell completion scripts"; } diff --git a/crates/pm/src/main.rs b/crates/pm/src/main.rs index 2b055376de..7898b6bccb 100644 --- a/crates/pm/src/main.rs +++ b/crates/pm/src/main.rs @@ -1,7 +1,7 @@ use std::process; use anyhow::{Context, Result}; -use clap::{Parser, Subcommand}; +use clap::{CommandFactory, Parser, Subcommand}; use cmd::config::{handle_config_get, handle_config_list, handle_config_set}; use cmd::deps::build_deps; use cmd::execute::execute; @@ -30,12 +30,13 @@ mod service; mod util; use crate::constants::cmd::{ - CLEAN_ABOUT, CLEAN_ALIAS, CLEAN_NAME, CONFIG_ABOUT, CONFIG_ALIAS, CONFIG_NAME, DEPS_ABOUT, - DEPS_ALIAS, DEPS_NAME, EXECUTE_ABOUT, EXECUTE_ALIAS, EXECUTE_NAME, INIT_ABOUT, INIT_ALIAS, - INIT_NAME, INSTALL_ABOUT, INSTALL_ALIAS, INSTALL_NAME, LINK_ABOUT, LINK_ALIAS, LINK_NAME, - LIST_ALIAS, LIST_NAME, REBUILD_ABOUT, REBUILD_ALIAS, REBUILD_NAME, RUN_ALIAS, RUN_NAME, - UNINSTALL_ABOUT, UNINSTALL_ALIAS, UNINSTALL_NAME, UPDATE_ABOUT, UPDATE_ALIAS, UPDATE_NAME, - VIEW_ABOUT, VIEW_ALIAS, VIEW_ALIAS_INFO, VIEW_ALIAS_SHOW, VIEW_NAME, + CLEAN_ABOUT, CLEAN_ALIAS, CLEAN_NAME, COMPLETIONS_ABOUT, COMPLETIONS_NAME, CONFIG_ABOUT, + CONFIG_ALIAS, CONFIG_NAME, DEPS_ABOUT, DEPS_ALIAS, DEPS_NAME, EXECUTE_ABOUT, EXECUTE_ALIAS, + EXECUTE_NAME, INIT_ABOUT, INIT_ALIAS, INIT_NAME, INSTALL_ABOUT, INSTALL_ALIAS, INSTALL_NAME, + LINK_ABOUT, LINK_ALIAS, LINK_NAME, LIST_ALIAS, LIST_NAME, REBUILD_ABOUT, REBUILD_ALIAS, + REBUILD_NAME, RUN_ALIAS, RUN_NAME, UNINSTALL_ABOUT, UNINSTALL_ALIAS, UNINSTALL_NAME, + UPDATE_ABOUT, UPDATE_ALIAS, UPDATE_NAME, VIEW_ABOUT, VIEW_ALIAS, VIEW_ALIAS_INFO, + VIEW_ALIAS_SHOW, VIEW_NAME, }; use crate::constants::{APP_ABOUT, APP_NAME, APP_VERSION}; use crate::helper::workspace::update_cwd_to_root; @@ -257,6 +258,13 @@ enum Commands { #[arg(long, short)] yes: bool, }, + + /// Generate shell completion scripts + #[command(name = COMPLETIONS_NAME, about = COMPLETIONS_ABOUT)] + Completions { + /// Shell to generate completions for + shell: clap_complete::Shell, + }, } fn main() { @@ -307,6 +315,12 @@ async fn async_main() -> Result<()> { return Ok(()); } + // Handle completions early to avoid unnecessary initialization (tracing, registry, auto-update) + if let Some(Commands::Completions { shell }) = cli.command { + clap_complete::generate(shell, &mut Cli::command(), APP_NAME, &mut std::io::stdout()); + return Ok(()); + } + // Initialize tracing (replaces set_verbose) let (log_file, _guard) = init_tracing(cli.verbose).context("Failed to initialize logging")?; @@ -541,7 +555,44 @@ async fn async_main() -> Result<()> { log_time_end("All packages installed"); } } + // Completions is handled early before initialization + Some(Commands::Completions { .. }) => unreachable!(), } Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + use clap::CommandFactory; + + #[test] + fn test_cli_debug_assert() { + // Validates that the clap command definition has no conflicts or issues + Cli::command().debug_assert(); + } + + #[test] + fn test_completions_generates_output() { + for shell in [ + clap_complete::Shell::Bash, + clap_complete::Shell::Zsh, + clap_complete::Shell::Fish, + clap_complete::Shell::PowerShell, + clap_complete::Shell::Elvish, + ] { + let mut buf = Vec::new(); + clap_complete::generate(shell, &mut Cli::command(), APP_NAME, &mut buf); + let output = String::from_utf8(buf).expect("completion output should be valid UTF-8"); + assert!( + !output.is_empty(), + "{shell} completion should produce output" + ); + assert!( + output.contains("install"), + "{shell} completion should contain subcommands" + ); + } + } +} From bbdd544de1de46ac8728a0a2bee2c09b59ffd793 Mon Sep 17 00:00:00 2001 From: killagu-claw Date: Tue, 17 Feb 2026 11:08:53 +0800 Subject: [PATCH 2/2] feat(pm): improve completions UX - auto-detect shell from /bin/zsh when omitted - run generation in spawn_blocking to avoid blocking tokio runtime - add help text with common shell setup hints --- crates/pm/src/constants.rs | 2 +- crates/pm/src/main.rs | 36 +++++++++++++++++++++++++++++++++--- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/crates/pm/src/constants.rs b/crates/pm/src/constants.rs index 65ac144188..0b2cd4b66f 100644 --- a/crates/pm/src/constants.rs +++ b/crates/pm/src/constants.rs @@ -59,5 +59,5 @@ pub mod cmd { pub const INIT_ABOUT: &str = "Create a package.json file"; pub const COMPLETIONS_NAME: &str = "completions"; - pub const COMPLETIONS_ABOUT: &str = "Generate shell completion scripts"; + pub const COMPLETIONS_ABOUT: &str = "Generate shell completion scripts\n\nAdd to your shell config:\n bash: echo 'eval \"$(utoo completions bash)\"' >> ~/.bashrc\n zsh: echo 'eval \"$(utoo completions zsh)\"' >> ~/.zshrc\n fish: utoo completions fish > ~/.config/fish/completions/utoo.fish"; } diff --git a/crates/pm/src/main.rs b/crates/pm/src/main.rs index 7898b6bccb..744d9db9ef 100644 --- a/crates/pm/src/main.rs +++ b/crates/pm/src/main.rs @@ -41,6 +41,20 @@ use crate::constants::cmd::{ use crate::constants::{APP_ABOUT, APP_NAME, APP_VERSION}; use crate::helper::workspace::update_cwd_to_root; +fn detect_shell_from_env() -> Option { + // Most common on Unix-like systems. + let shell_path = std::env::var("SHELL").ok()?; + let name = shell_path.rsplit('/').next().unwrap_or(shell_path.as_str()); + + match name { + "bash" => Some(clap_complete::Shell::Bash), + "zsh" => Some(clap_complete::Shell::Zsh), + "fish" => Some(clap_complete::Shell::Fish), + // Leave PowerShell + Elvish to explicit flags; auto-detect tends to be unreliable. + _ => None, + } +} + #[derive(Parser)] #[command(name = APP_NAME)] #[command(version = APP_VERSION)] @@ -262,8 +276,9 @@ enum Commands { /// Generate shell completion scripts #[command(name = COMPLETIONS_NAME, about = COMPLETIONS_ABOUT)] Completions { - /// Shell to generate completions for - shell: clap_complete::Shell, + /// Shell to generate completions for (auto-detected if omitted) + #[arg(value_enum)] + shell: Option, }, } @@ -317,7 +332,22 @@ async fn async_main() -> Result<()> { // Handle completions early to avoid unnecessary initialization (tracing, registry, auto-update) if let Some(Commands::Completions { shell }) = cli.command { - clap_complete::generate(shell, &mut Cli::command(), APP_NAME, &mut std::io::stdout()); + let shell = shell.or_else(detect_shell_from_env); + + let Some(shell) = shell else { + eprintln!( + "Could not detect shell. Usage: utoo completions " + ); + process::exit(2); + }; + + tokio::task::spawn_blocking(move || { + let mut cmd = Cli::command(); + clap_complete::generate(shell, &mut cmd, APP_NAME, &mut std::io::stdout()); + }) + .await + .context("Failed to generate shell completions")?; + return Ok(()); }