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,213 @@
|
||||
//! Command implementations and dispatch.
|
||||
|
||||
pub mod completion_cmd;
|
||||
pub mod config_cmd;
|
||||
pub mod doctor_cmd;
|
||||
pub mod export_import;
|
||||
pub mod info_cmd;
|
||||
pub mod install_cmd;
|
||||
pub mod list_cmd;
|
||||
pub mod run_cmd;
|
||||
pub mod search_cmd;
|
||||
pub mod self_update;
|
||||
pub mod status_cmd;
|
||||
pub mod uninstall_cmd;
|
||||
pub mod update_cmd;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::cli::Command;
|
||||
use crate::config::{AgentDef, MethodSpec};
|
||||
use crate::state::InstalledEntry;
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
|
||||
pub fn execute(app: &App) -> Result<i32> {
|
||||
match &app.cli.command {
|
||||
None => crate::repl::run(app),
|
||||
Some(cmd) => execute_command(app, cmd),
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute one parsed command (also used by the interactive shell).
|
||||
pub fn execute_command(app: &App, cmd: &Command) -> Result<i32> {
|
||||
match cmd {
|
||||
Command::List { all, category } => list_cmd::run(app, *all, category.as_deref()),
|
||||
Command::Start(a) => run_cmd::start(app, a),
|
||||
Command::Stop {
|
||||
agent,
|
||||
force,
|
||||
timeout,
|
||||
} => run_cmd::stop(app, agent, *force, *timeout),
|
||||
Command::Restart {
|
||||
start,
|
||||
force,
|
||||
timeout,
|
||||
} => run_cmd::restart(app, start, *force, *timeout),
|
||||
Command::Status { agent } => status_cmd::run(app, agent.as_deref()),
|
||||
Command::Install {
|
||||
agent,
|
||||
method,
|
||||
force,
|
||||
} => install_cmd::run(app, agent, method.as_deref(), *force),
|
||||
Command::Uninstall { agent, purge } => uninstall_cmd::run(app, agent, *purge),
|
||||
Command::Update { agent, all } => update_cmd::run(app, agent.as_deref(), *all),
|
||||
Command::Search { keyword, category } => search_cmd::run(app, keyword, category.as_deref()),
|
||||
Command::Info { agent } => info_cmd::run(app, agent),
|
||||
Command::Config(sub) => config_cmd::run(app, sub),
|
||||
Command::Doctor { fix } => doctor_cmd::run(app, *fix),
|
||||
Command::Run { agent, args } => run_cmd::run(app, agent, args),
|
||||
Command::Completion { shell } => completion_cmd::run(*shell),
|
||||
Command::SelfUpdate { check, to } => self_update::run(app, *check, to.as_deref()),
|
||||
Command::Export { output } => export_import::export_cmd(app, output),
|
||||
Command::Import { file } => export_import::import_cmd(app, file),
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub struct ResolvedExec {
|
||||
pub program: String,
|
||||
pub args: Vec<String>,
|
||||
pub env: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
/// Resolve the program, arguments and environment for running an agent.
|
||||
/// Preference order: recorded binary > run command first token (resolved on
|
||||
/// PATH when possible).
|
||||
pub fn resolve_exec(
|
||||
app: &App,
|
||||
agent: &AgentDef,
|
||||
extra_args: &[String],
|
||||
extra_env: &BTreeMap<String, String>,
|
||||
) -> Result<ResolvedExec> {
|
||||
let entry: Option<InstalledEntry> = app.state.get(&agent.name).ok().flatten();
|
||||
let mut tokens = agent.run_tokens();
|
||||
if tokens.is_empty() {
|
||||
bail!("agent '{}' has no run command", agent.name);
|
||||
}
|
||||
let program = match &entry {
|
||||
Some(e) if !e.bins.is_empty() => {
|
||||
let p = std::path::PathBuf::from(&e.bins[0]);
|
||||
if p.exists() || app.dry_run() {
|
||||
p.display().to_string()
|
||||
} else {
|
||||
tokens.remove(0)
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let first = tokens.remove(0);
|
||||
if first.contains(std::path::MAIN_SEPARATOR) || Path::new(&first).exists() {
|
||||
first
|
||||
} else if let Ok(p) = which::which(&first) {
|
||||
p.display().to_string()
|
||||
} else {
|
||||
first
|
||||
}
|
||||
}
|
||||
};
|
||||
let mut args: Vec<String> = tokens;
|
||||
args.extend(agent.args.iter().cloned());
|
||||
args.extend(extra_args.iter().cloned());
|
||||
let mut env = agent.env.clone();
|
||||
env.extend(extra_env.clone());
|
||||
Ok(ResolvedExec { program, args, env })
|
||||
}
|
||||
|
||||
/// Parse --env KEY=VALUE entries.
|
||||
pub fn parse_env_list(items: &[String]) -> Result<BTreeMap<String, String>> {
|
||||
let mut map = BTreeMap::new();
|
||||
for item in items {
|
||||
let Some((k, v)) = item.split_once('=') else {
|
||||
bail!("invalid --env '{item}': expected KEY=VALUE");
|
||||
};
|
||||
if k.trim().is_empty() {
|
||||
bail!("invalid --env '{item}': empty key");
|
||||
}
|
||||
map.insert(k.to_string(), v.to_string());
|
||||
}
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
/// Parse --args entries; quoted strings are split on whitespace.
|
||||
pub fn parse_extra_args(items: &[String]) -> Vec<String> {
|
||||
let mut out = Vec::new();
|
||||
for item in items {
|
||||
if item.contains(' ') || item.contains('\t') {
|
||||
if let Ok(parts) = shell_words::split(item) {
|
||||
out.extend(parts);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
out.push(item.clone());
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Resolve an agent by name or alias, with a helpful error message.
|
||||
pub fn require_agent<'a>(app: &'a App, name_or_alias: &str) -> Result<&'a AgentDef> {
|
||||
app.catalog.resolve(name_or_alias).ok_or_else(|| {
|
||||
anyhow!(
|
||||
"unknown agent '{name_or_alias}' — run 'am search <keyword>' or 'am list' to see available agents"
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// One-line description of an install method.
|
||||
pub fn describe_method(m: &MethodSpec) -> String {
|
||||
let mut parts = vec![format!("type={}", m.kind)];
|
||||
if let Some(p) = &m.package {
|
||||
parts.push(format!("package={p}"));
|
||||
}
|
||||
if let Some(u) = &m.url {
|
||||
parts.push(format!("url={u}"));
|
||||
}
|
||||
if let Some(s) = &m.script {
|
||||
parts.push(format!("script={s}"));
|
||||
}
|
||||
if let Some(r) = &m.repo {
|
||||
parts.push(format!("repo={r}"));
|
||||
}
|
||||
if let Some(b) = &m.binary_path {
|
||||
parts.push(format!("binary_path={b}"));
|
||||
}
|
||||
parts.join(" ")
|
||||
}
|
||||
|
||||
/// Select an install method by index (1-based) or type name.
|
||||
pub fn select_method<'a>(
|
||||
methods: &[&'a MethodSpec],
|
||||
sel: Option<&str>,
|
||||
) -> Result<&'a MethodSpec> {
|
||||
if methods.is_empty() {
|
||||
bail!("no install methods defined");
|
||||
}
|
||||
match sel {
|
||||
None => Ok(methods[0]),
|
||||
Some(s) => {
|
||||
if let Ok(idx) = s.parse::<usize>() {
|
||||
if (1..=methods.len()).contains(&idx) {
|
||||
return Ok(methods[idx - 1]);
|
||||
}
|
||||
bail!("method index {idx} out of range (1..={})", methods.len());
|
||||
}
|
||||
let lower = s.trim_start_matches("type:").to_lowercase();
|
||||
methods
|
||||
.iter()
|
||||
.find(|m| m.kind.as_str() == lower)
|
||||
.copied()
|
||||
.ok_or_else(|| {
|
||||
anyhow!(
|
||||
"unknown method '{s}'; available: {}",
|
||||
methods
|
||||
.iter()
|
||||
.map(|m| m.kind.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user