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:
+208
@@ -0,0 +1,208 @@
|
||||
//! Dependency checking: probe required tools (node, python, go, ...),
|
||||
//! compare minimum versions, and produce actionable error messages.
|
||||
|
||||
use crate::app::App;
|
||||
use crate::config::{AgentDef, Dependency};
|
||||
use crate::runner::Runner;
|
||||
use anyhow::{bail, Result};
|
||||
use std::collections::BTreeMap;
|
||||
use std::cmp::Ordering;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DepCheck {
|
||||
pub name: String,
|
||||
pub found: bool,
|
||||
pub version: Option<String>,
|
||||
pub min: Option<String>,
|
||||
pub ok: bool,
|
||||
pub detail: String,
|
||||
}
|
||||
|
||||
/// Default probe command for a dependency name: (program, args).
|
||||
fn default_probe(name: &str) -> (String, Vec<String>) {
|
||||
match name {
|
||||
"go" => ("go".to_string(), vec!["version".to_string()]),
|
||||
_ => (name.to_string(), vec!["--version".to_string()]),
|
||||
}
|
||||
}
|
||||
|
||||
/// Probe one dependency. Python is special: python3 is preferred.
|
||||
pub fn check_dep(runner: &dyn Runner, dep: &Dependency) -> DepCheck {
|
||||
let probes: Vec<(String, Vec<String>)> = if dep.name == "python" {
|
||||
vec![
|
||||
("python3".to_string(), vec!["--version".to_string()]),
|
||||
("python".to_string(), vec!["--version".to_string()]),
|
||||
]
|
||||
} else if let Some(check) = &dep.check {
|
||||
match shell_words::split(check) {
|
||||
Ok(tokens) if !tokens.is_empty() => {
|
||||
let prog = tokens[0].clone();
|
||||
vec![(prog, tokens[1..].to_vec())]
|
||||
}
|
||||
_ => vec![default_probe(&dep.name)],
|
||||
}
|
||||
} else {
|
||||
vec![default_probe(&dep.name)]
|
||||
};
|
||||
let empty = BTreeMap::new();
|
||||
for (prog, args) in probes {
|
||||
if runner.which(&prog).is_none() {
|
||||
continue;
|
||||
}
|
||||
match runner.run(&prog, &args, &empty, None, true) {
|
||||
Ok(out) if out.code == 0 => {
|
||||
let combined = format!("{}
|
||||
{}", out.stdout, out.stderr);
|
||||
let version = crate::version::find_version(&combined);
|
||||
let ok = match (&dep.min_version, &version) {
|
||||
(Some(min), Some(v)) => crate::version::compare(v, min)
|
||||
.map(|o| o != Ordering::Less)
|
||||
.unwrap_or(true),
|
||||
_ => true,
|
||||
};
|
||||
let detail = if ok {
|
||||
version.clone().unwrap_or_else(|| "found".to_string())
|
||||
} else {
|
||||
format!(
|
||||
"version {} is too old (need >= {})",
|
||||
version.as_deref().unwrap_or("unknown"),
|
||||
dep.min_version.as_deref().unwrap_or("?")
|
||||
)
|
||||
};
|
||||
return DepCheck {
|
||||
name: dep.name.clone(),
|
||||
found: true,
|
||||
version,
|
||||
min: dep.min_version.clone(),
|
||||
ok,
|
||||
detail,
|
||||
};
|
||||
}
|
||||
Ok(out) => {
|
||||
return DepCheck {
|
||||
name: dep.name.clone(),
|
||||
found: true,
|
||||
version: None,
|
||||
min: dep.min_version.clone(),
|
||||
ok: false,
|
||||
detail: format!("probe failed (exit code {})", out.code),
|
||||
};
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
DepCheck {
|
||||
name: dep.name.clone(),
|
||||
found: false,
|
||||
version: None,
|
||||
min: dep.min_version.clone(),
|
||||
ok: false,
|
||||
detail: "not found on PATH".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check every dependency of an agent.
|
||||
pub fn check_agent(agent: &AgentDef, runner: &dyn Runner) -> Vec<DepCheck> {
|
||||
agent
|
||||
.dependencies
|
||||
.iter()
|
||||
.map(|d| check_dep(runner, d))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Fail with an actionable message when dependencies are missing.
|
||||
/// With auto_install_deps enabled, dependencies that define an install_cmd
|
||||
/// are offered for automatic installation first.
|
||||
pub fn ensure_deps(app: &App, agent: &AgentDef, runner: &dyn Runner) -> Result<()> {
|
||||
let mut checks = check_agent(agent, runner);
|
||||
if checks.iter().all(|c| c.ok) {
|
||||
return Ok(());
|
||||
}
|
||||
if app.config.settings.auto_install_deps() && !app.dry_run() {
|
||||
for dep in &agent.dependencies {
|
||||
if dep.install_cmd.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if check_dep(runner, dep).ok {
|
||||
continue;
|
||||
}
|
||||
let cmd_line = dep.install_cmd.join(" ");
|
||||
app.log.warn(&format!("dependency '{}' is missing", dep.name));
|
||||
let proceed = app
|
||||
.confirm(&format!("Run '{cmd_line}' to install it?"))
|
||||
.unwrap_or(false);
|
||||
if proceed {
|
||||
let prog = dep.install_cmd[0].clone();
|
||||
let args: Vec<String> = dep.install_cmd[1..].to_vec();
|
||||
let empty = BTreeMap::new();
|
||||
let out = runner.run(&prog, &args, &empty, None, false)?;
|
||||
if out.code != 0 {
|
||||
app.log
|
||||
.warn(&format!("automatic installation of '{}' failed", dep.name));
|
||||
}
|
||||
}
|
||||
}
|
||||
checks = check_agent(agent, runner);
|
||||
}
|
||||
if checks.iter().all(|c| c.ok) {
|
||||
return Ok(());
|
||||
}
|
||||
let mut lines = Vec::new();
|
||||
for c in checks.iter().filter(|c| !c.ok) {
|
||||
let mut line = format!(" - {}: {}", c.name, c.detail);
|
||||
if let Some(min) = &c.min {
|
||||
line.push_str(&format!(" (minimum: {min})"));
|
||||
}
|
||||
if let Some(dep) = agent.dependencies.iter().find(|d| d.name == c.name) {
|
||||
if let Some(hint) = &dep.install_hint {
|
||||
line.push_str(&format!(" — install via {hint}"));
|
||||
}
|
||||
}
|
||||
lines.push(line);
|
||||
}
|
||||
bail!(
|
||||
"agent '{}' has unmet dependencies:
|
||||
{}",
|
||||
agent.name,
|
||||
lines.join("
|
||||
")
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::Dependency;
|
||||
use crate::runner::MockRunner;
|
||||
|
||||
#[test]
|
||||
fn missing_tool_reported() {
|
||||
let runner = MockRunner::new();
|
||||
let dep = Dependency {
|
||||
name: "node".to_string(),
|
||||
check: None,
|
||||
min_version: Some("18.0.0".to_string()),
|
||||
install_hint: Some("https://nodejs.org".to_string()),
|
||||
install_cmd: vec![],
|
||||
};
|
||||
let c = check_dep(&runner, &dep);
|
||||
assert!(!c.ok);
|
||||
assert!(!c.found);
|
||||
assert_eq!(c.detail, "not found on PATH");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn python3_preferred() {
|
||||
let runner = MockRunner::new().with_found("python3").with_found("python");
|
||||
let dep = Dependency {
|
||||
name: "python".to_string(),
|
||||
check: None,
|
||||
min_version: Some("3.9".to_string()),
|
||||
install_hint: None,
|
||||
install_cmd: vec![],
|
||||
};
|
||||
let c = check_dep(&runner, &dep);
|
||||
// MockRunner returns empty output => version unknown => ok when no min.
|
||||
assert!(c.found);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user