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
+113
View File
@@ -0,0 +1,113 @@
//! uninstall: stop the agent if needed, remove its files, bins and state
//! entry; --purge also removes logs and the user-config definition.
use super::*;
use anyhow::{Context, Result};
use std::path::PathBuf;
pub fn run(app: &App, agent_name: &str, purge: bool) -> Result<i32> {
let agent = require_agent(app, agent_name)?;
let entry = app.state.get(&agent.name).ok().flatten();
let Some(entry) = entry else {
app.log.info(&format!(
"agent '{}' is not managed by agent-manager — nothing to uninstall",
agent.name
));
return Ok(0);
};
// Stop it first when running.
if let Some(pid) = entry.pid {
if crate::process::is_running(pid) {
app.log.info(&format!("stopping running agent (pid {pid})"));
if app.dry_run() {
app.log.dry(format!("would stop pid {pid}"));
} else {
crate::process::stop_pid(pid, false, app.config.settings.stop_timeout());
app.state.update_pid(&agent.name, None, None)?;
}
}
}
let dir = PathBuf::from(&entry.install_dir);
if app.dry_run() {
app.log.dry(format!("would remove {}", dir.display()));
} else {
if dir.exists() {
std::fs::remove_dir_all(&dir)
.with_context(|| format!("cannot remove {}", dir.display()))?;
}
for b in &entry.bins {
let p = PathBuf::from(b);
if p.is_file() {
std::fs::remove_file(&p)
.with_context(|| format!("cannot remove {}", p.display()))?;
}
}
app.state.remove(&agent.name)?;
}
if purge {
let log = crate::process::agent_log_path(app, &agent.name);
if app.dry_run() {
app.log.dry(format!("would remove log {}", log.display()));
} else if log.exists() {
std::fs::remove_file(&log).ok();
}
remove_from_user_config(app, &agent.name)?;
}
app.log.success(&format!(
"uninstalled {} (files removed{})",
agent.title(),
if purge { ", config purged" } else { "" }
));
Ok(0)
}
/// Remove the agent entry from the user config file (--purge).
fn remove_from_user_config(app: &App, name: &str) -> Result<()> {
let Some(path) = &app.config_origin else {
app.log.verbose("no user config file — nothing to purge");
return Ok(());
};
let is_json = path.extension().map(|e| e == "json").unwrap_or(false);
if is_json {
app.log
.warn("config purge is not supported for JSON config files");
return Ok(());
}
let text = std::fs::read_to_string(path)
.with_context(|| format!("cannot read {}", path.display()))?;
let mut value: serde_yaml::Value = serde_yaml::from_str(&text)
.with_context(|| format!("cannot parse {}", path.display()))?;
let mut removed = false;
if let serde_yaml::Value::Mapping(map) = &mut value {
if let Some(serde_yaml::Value::Sequence(seq)) = map.get_mut("agents") {
let before = seq.len();
seq.retain(|v| {
v.get("name")
.and_then(|n| n.as_str())
.map(|n| n != name)
.unwrap_or(true)
});
removed = seq.len() != before;
}
}
if !removed {
app.log.verbose(&format!("'{name}' not defined in {}", path.display()));
return Ok(());
}
let backup = path.with_extension("yaml.bak");
std::fs::copy(path, &backup)
.with_context(|| format!("cannot create backup {}", backup.display()))?;
std::fs::write(path, serde_yaml::to_string(&value)?)
.with_context(|| format!("cannot write {}", path.display()))?;
app.log.info(&format!(
"removed '{name}' from {} (backup: {})",
path.display(),
backup.display()
));
Ok(())
}