diff --git a/src/cli.rs b/src/cli.rs index a702c26..0568d89 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,32 +1,52 @@ 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. #[derive(Debug, Parser)] #[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 { - /// Project name from deploy.toml - #[arg(short, long)] - pub project: Option, + // Global flags + /// Use global config only + #[arg(short, long, global = true)] + pub global: bool, + /// 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, + /// Print planned steps without executing anything #[arg(long)] pub dry_run: bool, - /// List configured projects and exit - #[arg(short, long)] - pub list: bool, - /// List configured servers and exit - #[arg(long)] - pub list_servers: bool, - /// Interactively pick a project to deploy (fuzzy search) - #[arg(short, long)] - pub pick: bool, - /// Deploy the project whose workdir matches the current folder (global config) - #[arg(short, long)] - pub this: bool, + + #[command(subcommand)] + pub command: Option, +} + +#[derive(Subcommand, Debug)] +pub enum Commands { + /// Run deploy + Deploy(DeployArgs), + + /// Interactive select project to deploy + 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, } diff --git a/src/main.rs b/src/main.rs index 6ec51ca..ed4a7c4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,19 +5,25 @@ mod local; mod ssh; mod sync; -use std::process::ExitCode; +use std::{path::Path, process::ExitCode}; use anyhow::{Context, Result, bail}; use clap::Parser; use colored::Colorize; +use crate::{ + cli::{Cli, Commands, DeployArgs}, + config::{Config, Project}, +}; + fn main() -> ExitCode { #[cfg(windows)] if enable_ansi_support::enable_ansi_support().is_err() { colored::control::set_override(false); } - let cli = cli::Cli::parse(); + let cli = Cli::parse(); + match run(&cli) { Ok(()) => ExitCode::SUCCESS, Err(err) => { @@ -27,71 +33,86 @@ fn main() -> ExitCode { } } -fn run(cli: &cli::Cli) -> Result<()> { - if cli.this { - return deploy_this(cli.dry_run); - } - - let path = config::resolve(cli.config.as_deref())?; - let cfg = config::load(&path)?; - - if cli.list_servers { - println!("config: {}", config::display(&path)); - println!("configured servers:"); - for (name, server) in &cfg.servers { - let mut line = format!(" - {name} {}", server.host); - if let Some(port) = server.port { - line.push_str(&format!(":{port}")); - } - if let Some(user) = &server.user { - line.push_str(&format!(" as {user}")); - } - println!("{line}"); - } - return Ok(()); - } - - if cli.list { - println!("config: {}", config::display(&path)); - 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, - None => return Ok(()), - } - } else { - cli.project - .clone() - .expect("clap guarantees or a mode flag") +fn handle_deploy(cli: &Cli, args: &DeployArgs, cfg: &config::Config) -> Result<()> { + let name = match &args.project_name { + 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(()); +} + +fn handle_list_servers(path: &Path, cfg: &config::Config) -> Result<()> { + println!("config: {}", config::display(&path)); + println!("configured servers:"); + for (name, server) in &cfg.servers { + let mut line = format!(" - {name} {}", server.host); + if let Some(port) = server.port { + line.push_str(&format!(":{port}")); + } + if let Some(user) = &server.user { + line.push_str(&format!(" as {user}")); + } + println!("{line}"); + } + return Ok(()); +} + +fn handle_pick(cli: &Cli, path: &Path, cfg: &config::Config) -> Result<()> { + let name = match pick_project(&cfg)? { + Some(name) => name, + None => return Ok(()), + }; + let project = cfg .projects .get(&name) .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) } -fn deploy_this(dry_run: bool) -> Result<()> { - let path = config::resolve(None)?; +fn run(cli: &Cli) -> Result<()> { + let path = config::resolve(cli.config.as_deref())?; let cfg = config::load(&path)?; - let cwd = std::env::current_dir().context("cannot determine current directory")?; - let Some(name) = config::find_by_cwd(&cfg, &cwd) else { - bail!( - "no project in {} has workdir matching {}", - config::display(&path), - cwd.display() - ); - }; - println!("config: {}", config::display(&path)); - deploy::deploy(name, &cfg.projects[name], dry_run) + match &cli.command { + Some(Commands::Deploy(args)) => handle_deploy(&cli, args, &cfg), + Some(Commands::Pick) => handle_pick(&cli, &path, &cfg), + Some(Commands::List) => handle_list(&path, &cfg), + Some(Commands::ListServers) => handle_list_servers(&path, &cfg), + None => handle_default(&path, &cfg), + } } fn pick_project(cfg: &config::Config) -> Result> { @@ -116,3 +137,11 @@ fn pick_project(cfg: &config::Config) -> Result> { 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()), + } +}