feat: add interactive pick project flow

This commit is contained in:
2026-08-23 08:08:21 +05:00
parent de9b6ad0dd
commit 31bbeca941
5 changed files with 152 additions and 8 deletions
Generated
+110
View File
@@ -346,6 +346,18 @@ dependencies = [
"windows-sys 0.61.2", "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]] [[package]]
name = "const-oid" name = "const-oid"
version = "0.10.2" version = "0.10.2"
@@ -524,6 +536,19 @@ dependencies = [
"cipher", "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]] [[package]]
name = "digest" name = "digest"
version = "0.11.3" version = "0.11.3"
@@ -629,6 +654,12 @@ dependencies = [
"windows-sys 0.61.2", "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]] [[package]]
name = "enum_dispatch" name = "enum_dispatch"
version = "0.3.13" version = "0.3.13"
@@ -647,6 +678,22 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 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]] [[package]]
name = "ff" name = "ff"
version = "0.14.0" version = "0.14.0"
@@ -767,6 +814,15 @@ dependencies = [
"slab", "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]] [[package]]
name = "generic-array" name = "generic-array"
version = "0.14.9" version = "0.14.9"
@@ -1029,6 +1085,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]] [[package]]
name = "lock_api" name = "lock_api"
version = "0.4.14" version = "0.4.14"
@@ -1596,6 +1658,19 @@ dependencies = [
"semver", "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]] [[package]]
name = "rustversion" name = "rustversion"
version = "1.0.23" version = "1.0.23"
@@ -1761,6 +1836,12 @@ dependencies = [
"sponge-cursor", "sponge-cursor",
] ]
[[package]]
name = "shell-words"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77"
[[package]] [[package]]
name = "shlex" name = "shlex"
version = "2.0.1" version = "2.0.1"
@@ -1928,6 +2009,19 @@ dependencies = [
"unicode-ident", "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]] [[package]]
name = "thiserror" name = "thiserror"
version = "2.0.20" version = "2.0.20"
@@ -1948,6 +2042,15 @@ dependencies = [
"syn 3.0.3", "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]] [[package]]
name = "tokio" name = "tokio"
version = "1.53.1" version = "1.53.1"
@@ -2039,6 +2142,12 @@ version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "unicode-width"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]] [[package]]
name = "universal-hash" name = "universal-hash"
version = "0.6.1" version = "0.6.1"
@@ -2369,6 +2478,7 @@ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
"colored", "colored",
"dialoguer",
"enable-ansi-support", "enable-ansi-support",
"home", "home",
"russh", "russh",
+1
View File
@@ -14,6 +14,7 @@ edition = "2024"
anyhow = "1.0.104" anyhow = "1.0.104"
clap = { version = "4.6.6", features = ["derive"] } clap = { version = "4.6.6", features = ["derive"] }
colored = "3.1.1" colored = "3.1.1"
dialoguer = { version = "0.12.0", features = ["fuzzy-select"] }
enable-ansi-support = "0.3.1" enable-ansi-support = "0.3.1"
home = "0.5.12" home = "0.5.12"
russh = { version = "0.62.7", default-features = false, features = [ russh = { version = "0.62.7", default-features = false, features = [
+1
View File
@@ -21,6 +21,7 @@ cargo install --path .
```sh ```sh
xboctploy --list # показать проекты из конфига xboctploy --list # показать проекты из конфига
xboctploy --list-servers # показать серверы из конфига xboctploy --list-servers # показать серверы из конфига
xboctploy --pick # интерактивно выбрать проект (нечёткий поиск)
xboctploy --dry-run demo # напечатать план деплоя без выполнения xboctploy --dry-run demo # напечатать план деплоя без выполнения
xboctploy demo # задеплоить проект demo xboctploy demo # задеплоить проект demo
``` ```
+4 -1
View File
@@ -6,7 +6,7 @@ use clap::{ArgGroup, Parser};
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
#[command(name = "xboctploy", version, about)] #[command(name = "xboctploy", version, about)]
#[command(arg_required_else_help = true)] #[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 { pub struct Cli {
/// Project name from deploy.toml /// Project name from deploy.toml
pub project: Option<String>, pub project: Option<String>,
@@ -22,4 +22,7 @@ pub struct Cli {
/// List configured servers and exit /// List configured servers and exit
#[arg(long)] #[arg(long)]
pub list_servers: bool, pub list_servers: bool,
/// Interactively pick a project to deploy (fuzzy search)
#[arg(long)]
pub pick: bool,
} }
+36 -7
View File
@@ -7,7 +7,7 @@ mod sync;
use std::process::ExitCode; use std::process::ExitCode;
use anyhow::{Context, Result}; use anyhow::{Context, Result, bail};
use clap::Parser; use clap::Parser;
use colored::Colorize; use colored::Colorize;
@@ -56,14 +56,43 @@ fn run(cli: &cli::Cli) -> Result<()> {
return Ok(()); return Ok(());
} }
let name = cli let name = if cli.pick {
.project match pick_project(&cfg)? {
.as_deref() Some(name) => name,
.expect("clap guarantees <PROJECT> or --list"); None => return Ok(()),
}
} else {
cli.project
.clone()
.expect("clap guarantees <PROJECT> or a mode flag")
};
let project = cfg let project = cfg
.projects .projects
.get(name) .get(&name)
.with_context(|| format!("project '{name}' not found in {}", path.display()))?; .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<Option<String>> {
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"),
}
} }