diff --git a/Cargo.lock b/Cargo.lock index c7ba5e9..db8e692 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -346,6 +346,18 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "console" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c" +dependencies = [ + "encode_unicode", + "libc", + "unicode-width", + "windows-sys 0.61.2", +] + [[package]] name = "const-oid" version = "0.10.2" @@ -524,6 +536,19 @@ dependencies = [ "cipher", ] +[[package]] +name = "dialoguer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25f104b501bf2364e78d0d3974cbc774f738f5865306ed128e1e0d7499c0ad96" +dependencies = [ + "console", + "fuzzy-matcher", + "shell-words", + "tempfile", + "zeroize", +] + [[package]] name = "digest" version = "0.11.3" @@ -629,6 +654,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "enum_dispatch" version = "0.3.13" @@ -647,6 +678,22 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + [[package]] name = "ff" version = "0.14.0" @@ -767,6 +814,15 @@ dependencies = [ "slab", ] +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + [[package]] name = "generic-array" version = "0.14.9" @@ -1029,6 +1085,12 @@ dependencies = [ "libc", ] +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "lock_api" version = "0.4.14" @@ -1596,6 +1658,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + [[package]] name = "rustversion" version = "1.0.23" @@ -1761,6 +1836,12 @@ dependencies = [ "sponge-cursor", ] +[[package]] +name = "shell-words" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" + [[package]] name = "shlex" version = "2.0.1" @@ -1928,6 +2009,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "thiserror" version = "2.0.20" @@ -1948,6 +2042,15 @@ dependencies = [ "syn 3.0.3", ] +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + [[package]] name = "tokio" version = "1.53.1" @@ -2039,6 +2142,12 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "universal-hash" version = "0.6.1" @@ -2369,6 +2478,7 @@ dependencies = [ "anyhow", "clap", "colored", + "dialoguer", "enable-ansi-support", "home", "russh", diff --git a/Cargo.toml b/Cargo.toml index 2a2225b..5770b71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ edition = "2024" anyhow = "1.0.104" clap = { version = "4.6.6", features = ["derive"] } colored = "3.1.1" +dialoguer = { version = "0.12.0", features = ["fuzzy-select"] } enable-ansi-support = "0.3.1" home = "0.5.12" russh = { version = "0.62.7", default-features = false, features = [ diff --git a/README.md b/README.md index 726336b..b5565db 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ cargo install --path . ```sh xboctploy --list # показать проекты из конфига xboctploy --list-servers # показать серверы из конфига +xboctploy --pick # интерактивно выбрать проект (нечёткий поиск) xboctploy --dry-run demo # напечатать план деплоя без выполнения xboctploy demo # задеплоить проект demo ``` diff --git a/src/cli.rs b/src/cli.rs index 9f3ab66..c802605 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -6,7 +6,7 @@ use clap::{ArgGroup, Parser}; #[derive(Debug, Parser)] #[command(name = "xboctploy", version, about)] #[command(arg_required_else_help = true)] -#[command(group = ArgGroup::new("target").required(true).args(["project", "list", "list_servers"]))] +#[command(group = ArgGroup::new("target").required(true).args(["project", "list", "list_servers", "pick"]))] pub struct Cli { /// Project name from deploy.toml pub project: Option, @@ -22,4 +22,7 @@ pub struct Cli { /// List configured servers and exit #[arg(long)] pub list_servers: bool, + /// Interactively pick a project to deploy (fuzzy search) + #[arg(long)] + pub pick: bool, } diff --git a/src/main.rs b/src/main.rs index df91263..79fc6a2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ mod sync; use std::process::ExitCode; -use anyhow::{Context, Result}; +use anyhow::{Context, Result, bail}; use clap::Parser; use colored::Colorize; @@ -56,14 +56,43 @@ fn run(cli: &cli::Cli) -> Result<()> { return Ok(()); } - let name = cli - .project - .as_deref() - .expect("clap guarantees or --list"); + let name = if cli.pick { + match pick_project(&cfg)? { + Some(name) => name, + None => return Ok(()), + } + } else { + cli.project + .clone() + .expect("clap guarantees or a mode flag") + }; let project = cfg .projects - .get(name) + .get(&name) .with_context(|| format!("project '{name}' not found in {}", path.display()))?; - deploy::deploy(name, project, cli.dry_run) + deploy::deploy(&name, project, cli.dry_run) +} + +fn pick_project(cfg: &config::Config) -> Result> { + use dialoguer::FuzzySelect; + + use std::io::IsTerminal; + if !std::io::stdout().is_terminal() { + bail!("interactive picker needs a real terminal; use --list instead"); + } + let names: Vec<&str> = cfg.projects.keys().map(String::as_str).collect(); + match FuzzySelect::new() + .with_prompt("Pick a project") + .items(&names) + .default(0) + .interact() + { + Ok(idx) => Ok(Some(names[idx].to_string())), + Err(dialoguer::Error::IO(err)) if err.kind() == std::io::ErrorKind::Interrupted => { + println!("cancelled."); + Ok(None) + } + Err(err) => Err(err).context("project selection failed"), + } }