//! install: resolve the agent, check dependencies, confirm, run the right //! installer, and record the installation in the state database. use super::*; use crate::events::{Event, EventKind}; 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, provider: Option<&str>, model: Option<&str>, no_config: 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)?; // Provider/model resolution (issue #90): flag > agent.provider > // settings.default_provider ; flag --model > agent.model > // provider.default_model. Displayed in the summary (and by --dry-run). let resolved = resolve_provider_config(app, agent, provider, model)?; if no_config { app.log .info(crate::i18n::tr("--no-config : aucune configuration post-install")); } else if !spec.configurable { app.log.info(&crate::tr_fmt!( "{} ne prend aucune configuration (install.configurable: false)", agent.name )); } // 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); } } // Post-install provider configuration (issue #91): write the agent // config files with the resolved provider/model. Skipped on --no-config // and on agents declaring install.configurable: false; never reached in // dry-run (returns earlier). if !no_config && spec.configurable { crate::agent_config::apply_post_install(app, agent, &resolved)?; } let entry = installers::make_entry(agent, method, &outcome, app); let version = entry.version.clone().unwrap_or_else(|| "unknown".to_string()); app.state.set(&entry)?; let mut args = vec![format!("method={}", method.kind)]; if let Some((p, m)) = &resolved { args.push(format!("provider={p}")); if let Some(m) = m { args.push(format!("model={m}")); } } app.emit( &Event::now(EventKind::Install) .with_agent(agent.name.clone()) .with_args(args), ); crate::hooks::run_hooks( app, "on_install", &std::env::current_dir().unwrap_or_default(), ); 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) } /// Resolve the provider + model of this install (issue #90) and print what /// will be configured. Returns (provider, model) when resolved. /// /// Validation rules: /// - an explicit `--provider` that is not registered → error listing the /// registry (the user asked for it by name); /// - an agent-declared provider that is not registered → warning only /// (catalog metadata may reference a provider the user has not added yet); /// - a model absent from the provider's model list → warning, not blocking. fn resolve_provider_config( app: &App, agent: &AgentDef, flag_provider: Option<&str>, flag_model: Option<&str>, ) -> Result)>> { let flag_p = flag_provider.filter(|p| !p.is_empty()); let agent_p = crate::agent_config::provider_pref(agent).filter(|p| !p.is_empty()); // Explicit provider requested but not registered: hard error. if let Some(p) = flag_p { if crate::providers::get(&app.config, p).is_none() { let known = crate::providers::names(&app.config); let hint = if known.is_empty() { "am providers add --base-url ".to_string() } else { known.join(", ") }; bail!(crate::tr_fmt!( "provider '{}' inconnu — providers enregistrés: {}", p, hint )); } } // Agent-declared provider not registered: warn (unless the user already // overrode it with --provider, which was validated above). if let Some(p) = agent_p { if flag_p.is_none() && crate::providers::get(&app.config, p).is_none() { app.log.warn(&crate::tr_fmt!( "provider '{}' (déclaré par {}) non enregistré — voir: am providers add {} --base-url ", p, agent.name, p )); return Ok(None); } } let Some((pname, def, model)) = crate::providers::resolve_for( &app.config, crate::agent_config::provider_pref(agent), agent.model.as_deref(), flag_provider, flag_model, ) else { app.log.info(crate::i18n::tr( "aucun provider configuré — voir: am providers add --base-url ", )); return Ok(None); }; // Model not in the provider's list: warn, keep going. if let Some(m) = model { if !def.models.is_empty() && !def.models.iter().any(|x| x == m) { app.log.warn(&crate::tr_fmt!( "modèle '{}' absent de la liste du provider '{}' — modèles: {}", m, pname, def.models.join(", ") )); } } let model_label = model.unwrap_or("-"); app.log.info(&crate::tr_fmt!( "configuré avec le provider {} (modèle {})", pname, model_label )); Ok(Some((pname.to_string(), model.map(String::from)))) }