//! install: resolve the agent, check dependencies, confirm, run the right //! installer, and record the installation in the state database. use super::*; use crate::installers; use crate::runner::SystemRunner; use anyhow::{anyhow, bail, Result}; use std::collections::BTreeMap; pub fn run(app: &App, agent_name: &str, method_sel: Option<&str>, force: bool) -> Result { let agent = require_agent(app, agent_name)?; let runner = SystemRunner::new(app.dry_run(), app.cli.verbose, &app.log); if !agent.supports_current_platform() { bail!( "agent '{}' is not available on {} (supported: {})", agent.name, crate::config::current_platform(), agent.platforms.join(", ") ); } if !agent.installable { bail!( "agent '{}' is not installable locally{}", agent.name, agent .note .as_deref() .map(|n| format!(": {n}")) .unwrap_or_default() ); } let spec = agent .install .as_ref() .ok_or_else(|| anyhow!("agent '{}' has no install definition", agent.name))?; let methods = spec.all_methods(); let method = select_method(&methods, method_sel)?; // Already installed? let existing = app.state.get(&agent.name).ok().flatten(); if existing.is_some() && !force && !app.dry_run() { let version = existing .as_ref() .and_then(|e| e.version.clone()) .unwrap_or_else(|| "version unknown".to_string()); bail!( "agent '{}' is already installed ({version}); use --force to reinstall, or 'am update {}'", agent.name, agent.name ); } if let Some(e) = &existing { if let Some(pid) = e.pid { if crate::process::is_running(pid) { bail!( "agent '{}' is currently running (pid {pid}); stop it first with 'am stop {}'", agent.name, agent.name ); } } } // Dependencies (checked against real tools unless dry-run). crate::deps::ensure_deps(app, agent, &runner)?; // Confirmation summary for scripts and destructive actions. if !app.dry_run() && !app.yes() && app.config.settings.confirm_before_run() && method.kind == crate::config::InstallType::Curl { app.log.info(&format!( "About to download and run the install script for {}", agent.title() )); app.log .info(&format!(" script: {}", method.script.as_deref().unwrap_or("?"))); if !app.confirm("Run this install script?")? { app.log.info("aborted"); return Ok(0); } } else if !app.dry_run() && !app.yes() && existing.is_some() { app.log.info(&format!( "Reinstalling {} via {}", agent.title(), describe_method(method) )); if !app.confirm("Proceed with reinstall?")? { app.log.info("aborted"); return Ok(0); } } app.log.info(&format!( "installing {} ({})…", agent.title(), describe_method(method) )); let mut outcome = installers::run_install(app, agent, method, &runner)?; if app.dry_run() { app.log.info("dry run complete — nothing was changed"); return Ok(0); } // Post-install commands (bin dir first on PATH). if !spec.post_install.is_empty() { let bin_str = app.paths.bin_dir.display().to_string(); let mut env = BTreeMap::new(); if let Ok(path) = std::env::var("PATH") { env.insert( "PATH".to_string(), format!("{bin_str}{}{path}", std::path::MAIN_SEPARATOR), ); } for cmd in &spec.post_install { let tokens = shell_words::split(cmd) .map_err(|e| anyhow!("cannot parse post-install command '{cmd}': {e}"))?; if tokens.is_empty() { continue; } let prog = tokens[0].clone(); let args: Vec = tokens[1..].to_vec(); app.log.info(&format!("post-install: {cmd}")); installers::sh(&runner, &prog, &args, &env, None)?; } } // Fallback version detection. if outcome.version.is_none() { if let Some(bin) = outcome.bins.first() { outcome.version = installers::detect_version(&runner, bin); } } let entry = installers::make_entry(agent, method, &outcome, app); let version = entry.version.clone().unwrap_or_else(|| "unknown".to_string()); app.state.set(&entry)?; app.log.success(&format!( "installed {} (v{version}) in {}", agent.title(), entry.install_dir )); if existing.is_none() { app.log.info(&format!( "start it with: am start {}", agent.name )); } Ok(0) }