Skip to content

Commit 61d10e2

Browse files
killagu-clawclaude
andauthored
feat(pm): add shell completion generation via utoo completions <shell> (#2601)
* feat(pm): add shell completion generation via `utoo completions <shell>` Uses clap_complete to generate completion scripts for bash, zsh, fish, elvish, and powershell. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * 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 --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 055a90a commit 61d10e2

4 files changed

Lines changed: 102 additions & 7 deletions

File tree

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/pm/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ atty = "0.2"
1515
bytes = "1.11.0"
1616
chrono = { version = "0.4", features = ["serde"] }
1717
clap = { workspace = true }
18+
clap_complete = "4"
1819
colored = "2.1"
1920
dashmap = "6.1.0"
2021
deno_semver = "0.7"

crates/pm/src/constants.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,7 @@ pub mod cmd {
5757
pub const INIT_NAME: &str = "init";
5858
pub const INIT_ALIAS: &str = "create";
5959
pub const INIT_ABOUT: &str = "Create a package.json file";
60+
61+
pub const COMPLETIONS_NAME: &str = "completions";
62+
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";
6063
}

crates/pm/src/main.rs

Lines changed: 88 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::process;
22

33
use anyhow::{Context, Result};
4-
use clap::{Parser, Subcommand};
4+
use clap::{CommandFactory, Parser, Subcommand};
55
use cmd::config::{handle_config_get, handle_config_list, handle_config_set};
66
use cmd::deps::build_deps;
77
use cmd::execute::execute;
@@ -30,16 +30,31 @@ mod service;
3030
mod util;
3131

3232
use crate::constants::cmd::{
33-
CLEAN_ABOUT, CLEAN_ALIAS, CLEAN_NAME, CONFIG_ABOUT, CONFIG_ALIAS, CONFIG_NAME, DEPS_ABOUT,
34-
DEPS_ALIAS, DEPS_NAME, EXECUTE_ABOUT, EXECUTE_ALIAS, EXECUTE_NAME, INIT_ABOUT, INIT_ALIAS,
35-
INIT_NAME, INSTALL_ABOUT, INSTALL_ALIAS, INSTALL_NAME, LINK_ABOUT, LINK_ALIAS, LINK_NAME,
36-
LIST_ALIAS, LIST_NAME, REBUILD_ABOUT, REBUILD_ALIAS, REBUILD_NAME, RUN_ALIAS, RUN_NAME,
37-
UNINSTALL_ABOUT, UNINSTALL_ALIAS, UNINSTALL_NAME, UPDATE_ABOUT, UPDATE_ALIAS, UPDATE_NAME,
38-
VIEW_ABOUT, VIEW_ALIAS, VIEW_ALIAS_INFO, VIEW_ALIAS_SHOW, VIEW_NAME,
33+
CLEAN_ABOUT, CLEAN_ALIAS, CLEAN_NAME, COMPLETIONS_ABOUT, COMPLETIONS_NAME, CONFIG_ABOUT,
34+
CONFIG_ALIAS, CONFIG_NAME, DEPS_ABOUT, DEPS_ALIAS, DEPS_NAME, EXECUTE_ABOUT, EXECUTE_ALIAS,
35+
EXECUTE_NAME, INIT_ABOUT, INIT_ALIAS, INIT_NAME, INSTALL_ABOUT, INSTALL_ALIAS, INSTALL_NAME,
36+
LINK_ABOUT, LINK_ALIAS, LINK_NAME, LIST_ALIAS, LIST_NAME, REBUILD_ABOUT, REBUILD_ALIAS,
37+
REBUILD_NAME, RUN_ALIAS, RUN_NAME, UNINSTALL_ABOUT, UNINSTALL_ALIAS, UNINSTALL_NAME,
38+
UPDATE_ABOUT, UPDATE_ALIAS, UPDATE_NAME, VIEW_ABOUT, VIEW_ALIAS, VIEW_ALIAS_INFO,
39+
VIEW_ALIAS_SHOW, VIEW_NAME,
3940
};
4041
use crate::constants::{APP_ABOUT, APP_NAME, APP_VERSION};
4142
use crate::helper::workspace::update_cwd_to_root;
4243

44+
fn detect_shell_from_env() -> Option<clap_complete::Shell> {
45+
// Most common on Unix-like systems.
46+
let shell_path = std::env::var("SHELL").ok()?;
47+
let name = shell_path.rsplit('/').next().unwrap_or(shell_path.as_str());
48+
49+
match name {
50+
"bash" => Some(clap_complete::Shell::Bash),
51+
"zsh" => Some(clap_complete::Shell::Zsh),
52+
"fish" => Some(clap_complete::Shell::Fish),
53+
// Leave PowerShell + Elvish to explicit flags; auto-detect tends to be unreliable.
54+
_ => None,
55+
}
56+
}
57+
4358
#[derive(Parser)]
4459
#[command(name = APP_NAME)]
4560
#[command(version = APP_VERSION)]
@@ -257,6 +272,14 @@ enum Commands {
257272
#[arg(long, short)]
258273
yes: bool,
259274
},
275+
276+
/// Generate shell completion scripts
277+
#[command(name = COMPLETIONS_NAME, about = COMPLETIONS_ABOUT)]
278+
Completions {
279+
/// Shell to generate completions for (auto-detected if omitted)
280+
#[arg(value_enum)]
281+
shell: Option<clap_complete::Shell>,
282+
},
260283
}
261284

262285
fn main() {
@@ -307,6 +330,27 @@ async fn async_main() -> Result<()> {
307330
return Ok(());
308331
}
309332

333+
// Handle completions early to avoid unnecessary initialization (tracing, registry, auto-update)
334+
if let Some(Commands::Completions { shell }) = cli.command {
335+
let shell = shell.or_else(detect_shell_from_env);
336+
337+
let Some(shell) = shell else {
338+
eprintln!(
339+
"Could not detect shell. Usage: utoo completions <bash|zsh|fish|powershell|elvish>"
340+
);
341+
process::exit(2);
342+
};
343+
344+
tokio::task::spawn_blocking(move || {
345+
let mut cmd = Cli::command();
346+
clap_complete::generate(shell, &mut cmd, APP_NAME, &mut std::io::stdout());
347+
})
348+
.await
349+
.context("Failed to generate shell completions")?;
350+
351+
return Ok(());
352+
}
353+
310354
// Initialize tracing (replaces set_verbose)
311355
let (log_file, _guard) = init_tracing(cli.verbose).context("Failed to initialize logging")?;
312356

@@ -541,7 +585,44 @@ async fn async_main() -> Result<()> {
541585
log_time_end("All packages installed");
542586
}
543587
}
588+
// Completions is handled early before initialization
589+
Some(Commands::Completions { .. }) => unreachable!(),
544590
}
545591

546592
Ok(())
547593
}
594+
595+
#[cfg(test)]
596+
mod tests {
597+
use super::*;
598+
use clap::CommandFactory;
599+
600+
#[test]
601+
fn test_cli_debug_assert() {
602+
// Validates that the clap command definition has no conflicts or issues
603+
Cli::command().debug_assert();
604+
}
605+
606+
#[test]
607+
fn test_completions_generates_output() {
608+
for shell in [
609+
clap_complete::Shell::Bash,
610+
clap_complete::Shell::Zsh,
611+
clap_complete::Shell::Fish,
612+
clap_complete::Shell::PowerShell,
613+
clap_complete::Shell::Elvish,
614+
] {
615+
let mut buf = Vec::new();
616+
clap_complete::generate(shell, &mut Cli::command(), APP_NAME, &mut buf);
617+
let output = String::from_utf8(buf).expect("completion output should be valid UTF-8");
618+
assert!(
619+
!output.is_empty(),
620+
"{shell} completion should produce output"
621+
);
622+
assert!(
623+
output.contains("install"),
624+
"{shell} completion should contain subcommands"
625+
);
626+
}
627+
}
628+
}

0 commit comments

Comments
 (0)