agent-manager v0.2.0 : gestionnaire d'agents IA locaux
- CLI Rust (am) : list/install/uninstall/update/start/stop/restart/run/status/search/info/config/doctor/completion/export/import/self-update + shell interactif (Tab, historique) - 9 methodes d'installation (npm, pip, uv, cargo, go, bun, curl, binary, git) - catalogue de 72 agents, dependances, processus, securite (dry-run, checksums) - sondes de version paralleles + cache (am list ~25 ms a chaud) - installateurs one-liner : install.sh, install.ps1
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
//! 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<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)?;
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user