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,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))
|
||||
}
|
||||
Reference in New Issue
Block a user