mod cli; mod config; mod deploy; mod git; mod local; mod ssh; mod sync; use std::{fs, 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::parse(); match run(&cli) { Ok(()) => ExitCode::SUCCESS, Err(err) => { eprintln!("{} {err:#}", "error:".red().bold()); ExitCode::FAILURE } } } 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}"); } Ok(()) } fn handle_edit_config() -> Result<()> { let path = config::global_config_path(home::home_dir().as_deref()) .context("cannot determine home directory")?; if let Some(parent) = path.parent() { fs::create_dir_all(parent) .with_context(|| format!("cannot create {}", parent.display()))?; } if !path.exists() { fs::write(&path, "").with_context(|| format!("cannot create {}", path.display()))?; } let editor = std::env::var("EDITOR") .or_else(|_| std::env::var("VISUAL")) .unwrap_or_else(|_| { #[cfg(target_os = "windows")] { "notepad".to_string() } #[cfg(not(target_os = "windows"))] { "vi".to_string() } }); let cmd = format!("{editor} {}", path.display()); let mut process = { #[cfg(target_os = "windows")] { let mut c = std::process::Command::new("cmd"); c.arg("/C").arg(&cmd); c } #[cfg(not(target_os = "windows"))] { let mut c = std::process::Command::new("sh"); c.arg("-c").arg(&cmd); c } }; let status = process .status() .with_context(|| format!("failed to launch editor: {editor}"))?; if !status.success() { std::process::exit(status.code().unwrap_or(1)); } 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}"); } 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 run(cli: &Cli) -> Result<()> { let path = config::resolve(cli.config.as_deref())?; let cfg = config::load(&path)?; 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), Some(Commands::EditConfig) => handle_edit_config(), None => handle_default(&path, &cfg), } } 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"), } } 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()), } }