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
+187
View File
@@ -0,0 +1,187 @@
//! start, stop, restart and run: process management commands.
use super::*;
use crate::cli::StartArgs;
use crate::process;
use anyhow::{bail, Context, Result};
use std::collections::BTreeMap;
use std::ffi::OsString;
use std::path::PathBuf;
use std::process::Command;
pub fn start(app: &App, opts: &StartArgs) -> Result<i32> {
let extra_env = parse_env_list(&opts.env)?;
let extra_args = parse_extra_args(&opts.args);
if let Some(group) = crate::catalog::Catalog::parse_group_selector(&opts.agent) {
let members = app.catalog.group_members(group);
if members.is_empty() {
bail!("unknown group '{group}'");
}
for m in members {
start_one(app, m, &extra_args, &extra_env, opts.notify, true)?;
}
return Ok(0);
}
let agent = require_agent(app, &opts.agent)?;
start_one(app, agent, &extra_args, &extra_env, opts.notify, opts.background)
}
fn start_one(
app: &App,
agent: &AgentDef,
extra_args: &[String],
extra_env: &BTreeMap<String, String>,
notify: bool,
background: bool,
) -> Result<i32> {
let exec = resolve_exec(app, agent, extra_args, extra_env)?;
let bin = PathBuf::from(&exec.program);
let is_managed = app.state.get(&agent.name).ok().flatten().is_some();
if background {
let pid = process::spawn_background(app, &agent.name, &bin, &exec.args, &exec.env)?;
if app.dry_run() {
return Ok(0);
}
if is_managed {
app.state
.update_pid(&agent.name, Some(pid), Some(crate::installers::now_rfc3339()))?;
} else {
app.log.warn(&format!(
"agent '{}' is external (not installed by agent-manager); PID {pid} is not recorded",
agent.name
));
}
let log = process::agent_log_path(app, &agent.name);
app.log.success(&format!(
"{} started (pid {pid}); log: {}",
agent.title(),
log.display()
));
if notify {
process::desktop_notify("agent-manager", &format!("{} started", agent.title()));
}
Ok(0)
} else {
if app.dry_run() {
app.log.dry(format!(
"would run {} {} (foreground)",
exec.program,
exec.args.join(" ")
));
return Ok(0);
}
let (prog, prefix) = crate::runner::resolve_program(&exec.program);
let mut full_args = prefix;
full_args.extend(exec.args.iter().cloned());
let status = Command::new(&prog)
.args(&full_args)
.envs(&exec.env)
.status()
.with_context(|| format!("failed to run {}", exec.program))?;
Ok(status.code().unwrap_or(1))
}
}
pub fn stop(app: &App, target: &str, force: bool, timeout: Option<u64>) -> Result<i32> {
let timeout = timeout.unwrap_or_else(|| app.config.settings.stop_timeout());
if let Some(group) = crate::catalog::Catalog::parse_group_selector(target) {
let members = app.catalog.group_members(group);
if members.is_empty() {
bail!("unknown group '{group}'");
}
for m in members.iter().rev() {
stop_one(app, m, force, timeout)?;
}
return Ok(0);
}
let agent = require_agent(app, target)?;
stop_one(app, agent, force, timeout)
}
fn stop_one(app: &App, agent: &AgentDef, force: bool, timeout: u64) -> Result<i32> {
let Some(entry) = app.state.get(&agent.name).ok().flatten() else {
app.log.info(&format!(
"agent '{}' is not managed by agent-manager; nothing to stop",
agent.name
));
return Ok(0);
};
let Some(pid) = entry.pid else {
app.log.info(&format!("agent '{}' is not running", agent.name));
return Ok(0);
};
if app.dry_run() {
app.log.dry(format!(
"would stop pid {pid} ({}{})",
agent.name,
if force { "force" } else { "graceful" }
));
return Ok(0);
}
if !process::is_running(pid) {
app.log.warn(&format!(
"stale PID {pid} for '{}' (process no longer running); cleaning up",
agent.name
));
app.state.update_pid(&agent.name, None, None)?;
return Ok(0);
}
app.log.info(&format!("stopping '{}' (pid {pid})", agent.name));
let stopped = process::stop_pid(pid, force, timeout);
app.state.update_pid(&agent.name, None, None)?;
if stopped {
app.log.success(&format!("{} stopped", agent.title()));
Ok(0)
} else {
app.log.error(&format!("failed to stop pid {pid}"));
Ok(1)
}
}
pub fn restart(app: &App, opts: &StartArgs, force: bool, timeout: Option<u64>) -> Result<i32> {
let timeout = timeout.unwrap_or_else(|| app.config.settings.stop_timeout());
if let Some(group) = crate::catalog::Catalog::parse_group_selector(&opts.agent) {
let members = app.catalog.group_members(group);
if members.is_empty() {
bail!("unknown group '{group}'");
}
for m in members.iter().rev() {
stop_one(app, m, force, timeout)?;
}
let extra_env = parse_env_list(&opts.env)?;
let extra_args = parse_extra_args(&opts.args);
for m in members {
start_one(app, m, &extra_args, &extra_env, opts.notify, true)?;
}
return Ok(0);
}
let agent = require_agent(app, &opts.agent)?;
stop_one(app, agent, force, timeout)?;
let extra_env = parse_env_list(&opts.env)?;
let extra_args = parse_extra_args(&opts.args);
start_one(app, agent, &extra_args, &extra_env, opts.notify, opts.background)
}
/// run: execute the agent command directly, no process management.
pub fn run(app: &App, target: &str, extra: &[OsString]) -> Result<i32> {
let agent = require_agent(app, target)?;
let extra_args: Vec<String> = extra.iter().map(|o| o.to_string_lossy().to_string()).collect();
let exec = resolve_exec(app, agent, &extra_args, &BTreeMap::new())?;
if app.dry_run() {
app.log.dry(format!(
"would run {} {}",
exec.program,
exec.args.join(" ")
));
return Ok(0);
}
let (prog, prefix) = crate::runner::resolve_program(&exec.program);
let mut full_args = prefix;
full_args.extend(exec.args.iter().cloned());
let status = Command::new(&prog)
.args(&full_args)
.envs(&exec.env)
.status()
.with_context(|| format!("failed to run {}", exec.program))?;
Ok(status.code().unwrap_or(1))
}