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
+182
View File
@@ -0,0 +1,182 @@
//! config subcommands: show, path, edit, validate, add.
use super::*;
use crate::cli::ConfigCmd;
use crate::config;
use crate::output::print_json;
use anyhow::{anyhow, Context, Result};
use std::path::{Path, PathBuf};
use std::process::Command;
const USER_CONFIG_TEMPLATE: &str = "# User configuration for agent-manager.
# This file overrides/completes the embedded default catalog.
# Run 'am config show' to see the effective configuration.
version: \"1.0\"
agents: []
";
pub fn run(app: &App, cmd: &ConfigCmd) -> Result<i32> {
match cmd {
ConfigCmd::Show => {
if app.json() {
print_json(&app.config);
} else {
println!("{}", serde_yaml::to_string(&app.config)?);
}
Ok(0)
}
ConfigCmd::Path => {
if app.json() {
print_json(&serde_json::json!({
"origin": app.config_origin.as_ref().map(|p| p.display().to_string()),
"user_dir": app.paths.config_dir.as_ref().map(|p| p.display().to_string()),
"user_file": user_config_path_if_any(app).map(|p| p.display().to_string()),
"local_file": PathBuf::from(config::LOCAL_CONFIG_NAME).display().to_string(),
}));
} else {
match &app.config_origin {
Some(p) => println!("active config: {}", p.display()),
None => println!("no user config found — using the embedded default catalog"),
}
if let Some(d) = &app.paths.config_dir {
println!("user config directory: {}", d.display());
println!(
"user config file: {}",
d.join(config::CONFIG_FILE_NAME).display()
);
}
println!(
"local config file: {} (current directory)",
PathBuf::from(config::LOCAL_CONFIG_NAME).display()
);
}
Ok(0)
}
ConfigCmd::Validate => {
let problems = config::validate(&app.config);
if app.json() {
print_json(&serde_json::json!({
"valid": problems.is_empty(),
"problems": problems,
}));
return Ok(if problems.is_empty() { 0 } else { 1 });
}
if problems.is_empty() {
app.log
.success(&format!("configuration is valid ({} agents)", app.catalog.len()));
Ok(0)
} else {
app.log.error(&format!(
"configuration problems:
- {}",
problems.join("
- ")
));
Ok(1)
}
}
ConfigCmd::Edit => {
let path = user_config_path(app)?;
let editor = std::env::var("VISUAL")
.or_else(|_| std::env::var("EDITOR"))
.unwrap_or_else(|_| {
if cfg!(windows) {
"notepad".to_string()
} else {
"vi".to_string()
}
});
let status = Command::new(&editor)
.arg(&path)
.status()
.with_context(|| format!("cannot launch editor '{editor}'"))?;
Ok(status.code().unwrap_or(1))
}
ConfigCmd::Add { file } => add_file(app, file),
}
}
fn user_config_path_if_any(app: &App) -> Option<PathBuf> {
app.paths
.config_dir
.as_ref()
.map(|d| d.join(config::CONFIG_FILE_NAME))
}
/// Ensure the user config file exists (creating a template when needed).
fn user_config_path(app: &App) -> Result<PathBuf> {
let dir = app
.paths
.config_dir
.clone()
.ok_or_else(|| anyhow!("cannot determine the user config directory (no HOME set)"))?;
std::fs::create_dir_all(&dir)
.with_context(|| format!("cannot create {}", dir.display()))?;
let path = dir.join(config::CONFIG_FILE_NAME);
if !path.exists() {
std::fs::write(&path, USER_CONFIG_TEMPLATE)
.with_context(|| format!("cannot create {}", path.display()))?;
}
Ok(path)
}
/// config add: copy an agent definition file into the config dir and
/// register it via an include entry in the user config file.
fn add_file(app: &App, file: &Path) -> Result<i32> {
config::parse_file(file)?;
let dir = app
.paths
.config_dir
.clone()
.ok_or_else(|| anyhow!("cannot determine the user config directory (no HOME set)"))?;
std::fs::create_dir_all(&dir)?;
let base = file
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("agents.yaml")
.to_string();
let dest = dir.join(&base);
std::fs::copy(file, &dest)
.with_context(|| format!("cannot copy {} to {}", file.display(), dest.display()))?;
let cfg_path = user_config_path(app)?;
let text = std::fs::read_to_string(&cfg_path)?;
let mut value: serde_yaml::Value = serde_yaml::from_str(&text)
.unwrap_or_else(|_| serde_yaml::Value::Null);
match &mut value {
serde_yaml::Value::Mapping(map) => {
let key = serde_yaml::Value::String("include".to_string());
match map.get_mut(&key) {
Some(serde_yaml::Value::Sequence(seq)) => {
let entry = serde_yaml::Value::String(base.clone());
if !seq.contains(&entry) {
seq.push(entry);
}
}
_ => {
map.insert(
key,
serde_yaml::Value::Sequence(vec![serde_yaml::Value::String(
base.clone(),
)]),
);
}
}
std::fs::write(&cfg_path, serde_yaml::to_string(&value)?)
.with_context(|| format!("cannot write {}", cfg_path.display()))?;
}
_ => {
std::fs::write(
&cfg_path,
format!("version: \"1.0\"\ninclude:\n - {base}\n"),
)?;
}
}
app.log.success(&format!(
"added {} and registered it in {}",
dest.display(),
cfg_path.display()
));
Ok(0)
}