Files
agent-manager/src/commands/install_cmd.rs
T
bruno 68eb170f57 feat: v0.7.0 — configuration post-install : bloc config: par agent (env_map + fichiers), adaptateurs TOML/YAML/JSON/key=value, édition douce, jamais de token en clair (api_key = @secret) (closes #91)
- Nouveau module src/agent_config.rs : apply_post_install (écriture fichiers), apply_env_map (env injectée au start/run), provider_pref (provider > config.provider_default > default_provider)
- am install écrit les fichiers de config de l'agent avec le provider/modèle résolus ; --no-config / install.configurable:false / --dry-run respectés ; agent sans adaptateur → message clair, install réussit
- Catalogue : blocs config: pour claude-code, codex, agentty (+ provider_default)
2026-08-19 20:41:34 -04:00

282 lines
9.4 KiB
Rust

//! 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<i32> {
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<String> = 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<Option<(String, Option<String>)>> {
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 <nom> --base-url <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 <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 <nom> --base-url <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))))
}