mod cli; mod config; mod deploy; mod local; mod ssh; mod sync; use std::process::ExitCode; use anyhow::{Context, Result, bail}; use clap::Parser; use colored::Colorize; fn main() -> ExitCode { #[cfg(windows)] if enable_ansi_support::enable_ansi_support().is_err() { colored::control::set_override(false); } let cli = cli::Cli::parse(); match run(&cli) { Ok(()) => ExitCode::SUCCESS, Err(err) => { eprintln!("{} {err:#}", "error:".red().bold()); ExitCode::FAILURE } } } 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") }; let project = cfg .projects .get(&name) .with_context(|| format!("project '{name}' not found in {}", path.display()))?; deploy::deploy(&name, project, cli.dry_run) } fn deploy_this(dry_run: bool) -> Result<()> { let path = config::resolve(None)?; 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) } fn pick_project(cfg: &config::Config) -> Result> { 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"), } }