Compare commits
2
Commits
1a2bb73b62
...
097b68c39d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
097b68c39d | ||
|
|
9979af9702 |
+46
@@ -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
@@ -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>,
|
||||||
}
|
}
|
||||||
|
|||||||
+65
-36
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
let path = config::resolve(cli.config.as_deref())?;
|
fn handle_list(path: &Path, cfg: &config::Config) -> Result<()> {
|
||||||
let cfg = config::load(&path)?;
|
println!("Config: {}", config::display(&path));
|
||||||
|
println!("Configured projects:");
|
||||||
|
for name in cfg.projects.keys() {
|
||||||
|
println!(" - {name}");
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
if cli.list_servers {
|
fn handle_list_servers(path: &Path, cfg: &config::Config) -> Result<()> {
|
||||||
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 {
|
||||||
@@ -51,47 +69,50 @@ fn run(cli: &cli::Cli) -> Result<()> {
|
|||||||
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()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user