Compare commits

..
2 Commits
Author SHA1 Message Date
Ku6epXBOCTuK 097b68c39d feat: update app with new commands flow 2026-08-29 18:54:38 +05:00
Ku6epXBOCTuK 9979af9702 docs: update docs, archive old 2026-08-29 17:04:01 +05:00
5 changed files with 170 additions and 75 deletions
+46
View File
@@ -0,0 +1,46 @@
# 📄 Обновленный UX-манифест
## Определение контекста
- **Папка проекта:** папка, в которой есть `deploy.toml` или путь к которой прописан в глобальном конфиге.
- **Приоритет загрузки конфига:** `--config` $\rightarrow$ файл проекта (`deploy.toml`) $\rightarrow$ глобальный конфиг. Флаг `--global` (`-g`) принудительно оставляет только глобальный конфиг.
---
## 1. Запуск без подкоманд (`xd [FLAGS]`)
Проверка: находимся ли мы в папке проекта?
- **Да** — выводит краткую информацию по текущему проекту и подсказку:
`To deploy current project run 'xd deploy'`
- **Нет** — выводит список проектов из глобального конфига.
---
## 2. Подкоманда `xd deploy [PROJECT_NAME]`
Логика определения цели деплоя:
1. **Если `PROJECT_NAME` указан:** деплоить проект с этим именем из текущего конфига.
2. **Если `PROJECT_NAME` НЕ указан:**
- **Мы в папке проекта** $\rightarrow$ деплоить текущий проект.
- **Мы НЕ в папке проекта** $\rightarrow$ завершить работу с понятной ошибкой и подсказкой:
`Error: Project not found in current directory. Specify PROJECT_NAME or run 'xd pick' / 'xd list'`
---
## 3. Список подкоманд
- **`xd deploy [PROJECT]`** — запустить деплой проекта (текущего или указанного).
- **`xd list`** (алиас: `ls`) — вывести список проектов из текущего конфига.
- **`xd list-servers`** — вывести список серверов из текущего конфига.
- **`xd pick`** — интерактивный выбор проекта из текущего конфига с последующим деплоем.
---
## 4. Глобальные флаги (доступны для любых подкоманд)
- **`-c, --config <FILE>`** — путь к файлу конфигурации.
- **`-g, --global`** — использовать только глобальный конфиг (_конфликтует с `--config_`).
- **`--dry-run`** — прогон без выполнения действий.
+39 -19
View File
@@ -1,32 +1,52 @@
use std::path::PathBuf; use std::path::PathBuf;
use clap::{ArgGroup, Parser}; use clap::{Args, Parser, Subcommand};
/// Deploy hobby projects from a local PC to a VPS over SSH. /// Deploy hobby projects from a local PC to a VPS over SSH.
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
#[command(name = "xboct-deploy", version, about)] #[command(name = "xboct-deploy", version, about)]
#[command(arg_required_else_help = true)]
#[command(group = ArgGroup::new("target").required(true).args(["project", "list", "list_servers", "pick", "this"]))]
pub struct Cli { pub struct Cli {
/// Project name from deploy.toml // Global flags
#[arg(short, long)] /// Use global config only
pub project: Option<String>, #[arg(short, long, global = true)]
pub global: bool,
/// Explicit path to the config file /// Explicit path to the config file
#[arg(long, value_name = "PATH")] #[arg(
short,
long,
value_name = "PATH",
global = true,
conflicts_with = "global"
)]
pub config: Option<PathBuf>, pub config: Option<PathBuf>,
/// Print planned steps without executing anything /// Print planned steps without executing anything
#[arg(long)] #[arg(long)]
pub dry_run: bool, pub dry_run: bool,
/// List configured projects and exit
#[arg(short, long)] #[command(subcommand)]
pub list: bool, pub command: Option<Commands>,
/// List configured servers and exit }
#[arg(long)]
pub list_servers: bool, #[derive(Subcommand, Debug)]
/// Interactively pick a project to deploy (fuzzy search) pub enum Commands {
#[arg(short, long)] /// Run deploy
pub pick: bool, Deploy(DeployArgs),
/// Deploy the project whose workdir matches the current folder (global config)
#[arg(short, long)] /// Interactive select project to deploy
pub this: bool, Pick,
/// List all available projects
List,
/// List all available servers
ListServers,
}
#[derive(Args, Debug)]
pub struct DeployArgs {
/// Name of the project to deploy
#[arg(value_name = "PROJECT_NAME")]
pub project_name: Option<String>,
} }
+67 -38
View File
@@ -5,19 +5,25 @@ mod local;
mod ssh; mod ssh;
mod sync; mod sync;
use std::process::ExitCode; use std::{path::Path, process::ExitCode};
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use clap::Parser; use clap::Parser;
use colored::Colorize; use colored::Colorize;
use crate::{
cli::{Cli, Commands, DeployArgs},
config::{Config, Project},
};
fn main() -> ExitCode { fn main() -> ExitCode {
#[cfg(windows)] #[cfg(windows)]
if enable_ansi_support::enable_ansi_support().is_err() { if enable_ansi_support::enable_ansi_support().is_err() {
colored::control::set_override(false); colored::control::set_override(false);
} }
let cli = cli::Cli::parse(); let cli = Cli::parse();
match run(&cli) { match run(&cli) {
Ok(()) => ExitCode::SUCCESS, Ok(()) => ExitCode::SUCCESS,
Err(err) => { Err(err) => {
@@ -27,15 +33,27 @@ fn main() -> ExitCode {
} }
} }
fn run(cli: &cli::Cli) -> Result<()> { fn handle_deploy(cli: &Cli, args: &DeployArgs, cfg: &config::Config) -> Result<()> {
if cli.this { let name = match &args.project_name {
return deploy_this(cli.dry_run); Some(name) => name,
None => get_project_name_by_cwd(cfg)?,
};
let project = &cfg.projects[name];
deploy(&cli, &name, project)
}
fn handle_list(path: &Path, cfg: &config::Config) -> Result<()> {
println!("Config: {}", config::display(&path));
println!("Configured projects:");
for name in cfg.projects.keys() {
println!(" - {name}");
} }
return Ok(());
}
let path = config::resolve(cli.config.as_deref())?; fn handle_list_servers(path: &Path, cfg: &config::Config) -> Result<()> {
let cfg = config::load(&path)?;
if cli.list_servers {
println!("config: {}", config::display(&path)); println!("config: {}", config::display(&path));
println!("configured servers:"); println!("configured servers:");
for (name, server) in &cfg.servers { for (name, server) in &cfg.servers {
@@ -49,49 +67,52 @@ fn run(cli: &cli::Cli) -> Result<()> {
println!("{line}"); println!("{line}");
} }
return Ok(()); return Ok(());
} }
if cli.list { fn handle_pick(cli: &Cli, path: &Path, cfg: &config::Config) -> Result<()> {
println!("config: {}", config::display(&path)); let name = match pick_project(&cfg)? {
println!("configured projects:");
for name in cfg.projects.keys() {
println!(" - {name}");
}
return Ok(());
}
let name = if cli.pick {
match pick_project(&cfg)? {
Some(name) => name, Some(name) => name,
None => return Ok(()), 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(&cli, &name, project)
}
fn handle_default(path: &Path, cfg: &config::Config) -> Result<()> {
match get_project_name_by_cwd(cfg) {
Ok(project_name) => {
println!("Current project: '{}'", project_name);
println!("`To deploy run 'xd deploy'`");
Ok(())
}
Err(_) => {
println!("No project found in current directory");
println!("Run 'xd deploy [project]' to deploy a project");
handle_list(path, cfg)
}
}
}
fn deploy(cli: &Cli, name: &str, project: &Project) -> Result<()> {
deploy::deploy(&name, project, cli.dry_run) deploy::deploy(&name, project, cli.dry_run)
} }
fn deploy_this(dry_run: bool) -> Result<()> { fn run(cli: &Cli) -> Result<()> {
let path = config::resolve(None)?; let path = config::resolve(cli.config.as_deref())?;
let cfg = config::load(&path)?; let cfg = config::load(&path)?;
let cwd = std::env::current_dir().context("cannot determine current directory")?; match &cli.command {
let Some(name) = config::find_by_cwd(&cfg, &cwd) else { Some(Commands::Deploy(args)) => handle_deploy(&cli, args, &cfg),
bail!( Some(Commands::Pick) => handle_pick(&cli, &path, &cfg),
"no project in {} has workdir matching {}", Some(Commands::List) => handle_list(&path, &cfg),
config::display(&path), Some(Commands::ListServers) => handle_list_servers(&path, &cfg),
cwd.display() None => handle_default(&path, &cfg),
); }
};
println!("config: {}", config::display(&path));
deploy::deploy(name, &cfg.projects[name], dry_run)
} }
fn pick_project(cfg: &config::Config) -> Result<Option<String>> { fn pick_project(cfg: &config::Config) -> Result<Option<String>> {
@@ -116,3 +137,11 @@ fn pick_project(cfg: &config::Config) -> Result<Option<String>> {
Err(err) => Err(err).context("project selection failed"), Err(err) => Err(err).context("project selection failed"),
} }
} }
fn get_project_name_by_cwd(cfg: &Config) -> Result<&str> {
let cwd = std::env::current_dir().context("cannot determine current directory")?;
match config::find_by_cwd(&cfg, &cwd) {
Some(name) => Ok(name),
None => bail!("no project found in {}", cwd.display()),
}
}