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",
]
[[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",
+1
View File
@@ -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 = [
+1
View File
@@ -21,6 +21,7 @@ cargo install --path .
```sh
xboctploy --list # показать проекты из конфига
xboctploy --list-servers # показать серверы из конфига
xboctploy --pick # интерактивно выбрать проект (нечёткий поиск)
xboctploy --dry-run demo # напечатать план деплоя без выполнения
xboctploy demo # задеплоить проект demo
```
+4 -1
View File
@@ -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<String>,
@@ -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,
}
+36 -7
View File
@@ -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 <PROJECT> or --list");
let name = if cli.pick {
match pick_project(&cfg)? {
Some(name) => name,
None => return Ok(()),
}
} else {
cli.project
.clone()
.expect("clap guarantees <PROJECT> 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<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"),
}
}