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:
2026-08-15 11:26:34 -04:00
commit 8ee3ddf3db
79 changed files with 15724 additions and 0 deletions
+152
View File
@@ -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)
}