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:
+186
@@ -0,0 +1,186 @@
|
||||
//! Command execution abstraction: real process execution for production,
|
||||
//! an in-memory recorder for tests and dry runs.
|
||||
|
||||
use crate::output::Logger;
|
||||
use anyhow::{Context, Result};
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct RunOutput {
|
||||
pub code: i32,
|
||||
pub stdout: String,
|
||||
pub stderr: String,
|
||||
}
|
||||
|
||||
/// Resolve a command name to an executable path, handling Windows
|
||||
/// .cmd/.bat scripts (which std::process::Command cannot launch directly).
|
||||
/// Returns the program to spawn and a prefix of arguments to insert before
|
||||
/// the caller's arguments (e.g. cmd.exe /C script.cmd).
|
||||
pub fn resolve_program(cmd: &str) -> (String, Vec<String>) {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let path = if cmd.contains('\\') || cmd.contains('/') {
|
||||
Some(PathBuf::from(cmd))
|
||||
} else {
|
||||
which::which(cmd).ok()
|
||||
};
|
||||
if let Some(p) = path {
|
||||
let ext = p
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.unwrap_or("")
|
||||
.to_lowercase();
|
||||
if ext == "cmd" || ext == "bat" {
|
||||
let comspec = std::env::var("COMSPEC").unwrap_or_else(|_| "cmd.exe".to_string());
|
||||
return (comspec, vec!["/C".to_string(), p.display().to_string()]);
|
||||
}
|
||||
return (p.display().to_string(), Vec::new());
|
||||
}
|
||||
}
|
||||
(cmd.to_string(), Vec::new())
|
||||
}
|
||||
|
||||
/// Execute external commands with logging and dry-run support.
|
||||
pub trait Runner {
|
||||
fn run(
|
||||
&self,
|
||||
cmd: &str,
|
||||
args: &[String],
|
||||
env: &BTreeMap<String, String>,
|
||||
cwd: Option<&Path>,
|
||||
capture: bool,
|
||||
) -> Result<RunOutput>;
|
||||
|
||||
/// Locate a binary on the PATH (like the which command).
|
||||
fn which(&self, bin: &str) -> Option<PathBuf>;
|
||||
}
|
||||
|
||||
/// Production runner backed by std::process::Command.
|
||||
pub struct SystemRunner<'a> {
|
||||
pub dry_run: bool,
|
||||
pub verbose: bool,
|
||||
pub log: &'a Logger,
|
||||
}
|
||||
|
||||
impl<'a> SystemRunner<'a> {
|
||||
pub fn new(dry_run: bool, verbose: bool, log: &'a Logger) -> Self {
|
||||
Self {
|
||||
dry_run,
|
||||
verbose,
|
||||
log,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Runner for SystemRunner<'a> {
|
||||
fn run(
|
||||
&self,
|
||||
cmd: &str,
|
||||
args: &[String],
|
||||
env: &BTreeMap<String, String>,
|
||||
cwd: Option<&Path>,
|
||||
capture: bool,
|
||||
) -> Result<RunOutput> {
|
||||
let display = if args.is_empty() {
|
||||
cmd.to_string()
|
||||
} else {
|
||||
format!("{cmd} {}", args.join(" "))
|
||||
};
|
||||
if self.dry_run {
|
||||
self.log.dry(format!("would run: {display}"));
|
||||
return Ok(RunOutput::default());
|
||||
}
|
||||
self.log.cmd(&display);
|
||||
let (prog, prefix) = resolve_program(cmd);
|
||||
let mut full_args = prefix;
|
||||
full_args.extend(args.iter().cloned());
|
||||
let mut c = std::process::Command::new(&prog);
|
||||
c.args(&full_args);
|
||||
c.envs(env);
|
||||
if let Some(dir) = cwd {
|
||||
c.current_dir(dir);
|
||||
}
|
||||
if capture {
|
||||
let out = c
|
||||
.output()
|
||||
.with_context(|| format!("failed to execute: {display}"))?;
|
||||
let code = out.status.code().unwrap_or(-1);
|
||||
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
|
||||
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
|
||||
self.log.result(code, &stderr);
|
||||
Ok(RunOutput {
|
||||
code,
|
||||
stdout,
|
||||
stderr,
|
||||
})
|
||||
} else {
|
||||
let status = c
|
||||
.status()
|
||||
.with_context(|| format!("failed to execute: {display}"))?;
|
||||
Ok(RunOutput {
|
||||
code: status.code().unwrap_or(-1),
|
||||
stdout: String::new(),
|
||||
stderr: String::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn which(&self, bin: &str) -> Option<PathBuf> {
|
||||
which::which(bin).ok()
|
||||
}
|
||||
}
|
||||
|
||||
/// In-memory runner for tests: records every invocation, returns success.
|
||||
#[derive(Default)]
|
||||
pub struct MockRunner {
|
||||
pub calls: std::sync::Mutex<Vec<String>>,
|
||||
pub found: HashSet<String>,
|
||||
pub outputs: std::sync::Mutex<Vec<RunOutput>>,
|
||||
}
|
||||
|
||||
impl MockRunner {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn with_found(mut self, bin: &str) -> Self {
|
||||
self.found.insert(bin.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_output(mut self, out: RunOutput) -> Self {
|
||||
self.outputs.get_mut().unwrap().push(out);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn snapshot(&self) -> Vec<String> {
|
||||
self.calls.lock().unwrap().clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Runner for MockRunner {
|
||||
fn run(
|
||||
&self,
|
||||
cmd: &str,
|
||||
args: &[String],
|
||||
_env: &BTreeMap<String, String>,
|
||||
_cwd: Option<&Path>,
|
||||
_capture: bool,
|
||||
) -> Result<RunOutput> {
|
||||
self.calls
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(format!("{cmd} {}", args.join(" ")));
|
||||
let out = self.outputs.lock().unwrap().pop();
|
||||
Ok(out.unwrap_or_default())
|
||||
}
|
||||
|
||||
fn which(&self, bin: &str) -> Option<PathBuf> {
|
||||
if self.found.contains(bin) {
|
||||
Some(PathBuf::from(bin))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user