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
+197
View File
@@ -0,0 +1,197 @@
//! info: everything known about one agent.
use super::*;
use crate::output::print_json;
use serde::Serialize;
#[derive(Serialize)]
struct InfoJson {
name: String,
display_name: String,
description: Option<String>,
category: Option<String>,
website: Option<String>,
tags: Vec<String>,
status: String,
version: Option<String>,
managed: bool,
running: bool,
pid: Option<u32>,
installable: bool,
note: Option<String>,
platforms: Vec<String>,
run: Option<String>,
args: Vec<String>,
env: std::collections::BTreeMap<String, String>,
install_methods: Vec<String>,
post_install: Vec<String>,
dependencies: Vec<DepJson>,
path: Option<String>,
}
#[derive(Serialize)]
struct DepJson {
name: String,
min_version: Option<String>,
install_hint: Option<String>,
}
pub fn run(app: &App, name: &str) -> Result<i32> {
let agent = require_agent(app, name)?;
let entry = app.state.get(&agent.name).ok().flatten();
let running = entry
.as_ref()
.and_then(|e| e.pid)
.map(crate::process::is_running)
.unwrap_or(false);
let methods: Vec<String> = agent
.install
.as_ref()
.map(|spec| spec.all_methods().iter().map(|m| describe_method(m)).collect())
.unwrap_or_default();
let deps: Vec<DepJson> = agent
.dependencies
.iter()
.map(|d| DepJson {
name: d.name.clone(),
min_version: d.min_version.clone(),
install_hint: d.install_hint.clone(),
})
.collect();
if app.json() {
print_json(&InfoJson {
name: agent.name.clone(),
display_name: agent.title().to_string(),
description: agent.description.clone(),
category: agent.category.clone(),
website: agent.website.clone(),
tags: agent.tags.clone(),
status: if running {
"running".to_string()
} else if entry.is_some() {
"installed".to_string()
} else if !agent.installable {
"not-installable".to_string()
} else {
"not-installed".to_string()
},
version: entry.as_ref().and_then(|e| e.version.clone()),
managed: entry.is_some(),
running,
pid: if running { entry.as_ref().and_then(|e| e.pid) } else { None },
installable: agent.installable,
note: agent.note.clone(),
platforms: agent.platforms.clone(),
run: agent.run.clone(),
args: agent.args.clone(),
env: agent.env.clone(),
install_methods: methods,
post_install: agent
.install
.as_ref()
.map(|s| s.post_install.clone())
.unwrap_or_default(),
dependencies: deps,
path: entry
.as_ref()
.map(|e| e.install_dir.clone())
.or_else(|| external_path(agent).map(|p| p.display().to_string())),
});
return Ok(0);
}
println!("{}", agent.title());
if agent.name != agent.title() {
println!(" slug: {}", agent.name);
}
if let Some(d) = &agent.description {
println!(" description: {d}");
}
if let Some(c) = &agent.category {
println!(" category: {c}");
}
if let Some(w) = &agent.website {
println!(" website: {w}");
}
if !agent.tags.is_empty() {
println!(" tags: {}", agent.tags.join(", "));
}
if !agent.platforms.is_empty() {
println!(" platforms: {}", agent.platforms.join(", "));
}
// Status block
let status = if running {
format!("running (pid {})", entry.as_ref().and_then(|e| e.pid).unwrap_or(0))
} else if entry.is_some() {
format!("installed (version {})", entry.as_ref().and_then(|e| e.version.clone()).unwrap_or_else(|| "unknown".to_string()))
} else if !agent.installable {
"not installable locally".to_string()
} else if external_path(agent).is_some() {
"found on PATH (external — not managed)".to_string()
} else {
"not installed".to_string()
};
println!(" status: {status}");
if let Some(run) = &agent.run {
let args = if agent.args.is_empty() {
String::new()
} else {
format!(" {}", agent.args.join(" "))
};
println!(" run: {run}{args}");
}
if !agent.env.is_empty() {
println!(
" env: {}",
agent
.env
.iter()
.map(|(k, v)| format!("{k}={v}"))
.collect::<Vec<_>>()
.join(" ")
);
}
if let Some(spec) = &agent.install {
println!(" install methods:");
for (i, m) in spec.all_methods().iter().enumerate() {
let note = m
.note
.as_deref()
.map(|n| format!(" ({n})"))
.unwrap_or_default();
println!(" {}) {}{}", i + 1, describe_method(m), note);
}
if !spec.post_install.is_empty() {
println!(" post-install: {}", spec.post_install.join(" && "));
}
}
if !agent.dependencies.is_empty() {
println!(" dependencies:");
for d in &agent.dependencies {
let min = d
.min_version
.as_deref()
.map(|m| format!(" >= {m}"))
.unwrap_or_default();
let hint = d
.install_hint
.as_deref()
.map(|h| format!(" — {h}"))
.unwrap_or_default();
println!(" - {}{}{}", d.name, min, hint);
}
}
if let Some(n) = &agent.note {
println!(" note: {n}");
}
Ok(0)
}
fn external_path(agent: &AgentDef) -> Option<std::path::PathBuf> {
let first = agent.first_token()?;
which::which(first).ok()
}