- 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)
282 lines
9.4 KiB
Rust
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))))
|
|
}
|