Files
agent-manager/src/templates.rs
T
bruno 6a6676fc5d feat: phase 2 — sessions export + rétention, init --template (closes #52 #53)
Axe 3 (#52) : templates embarqués dans le binaire (web, python, rust, cli)
- am init --template <stack> génère une config pré-remplie : groupe dev,
  profil par défaut, hooks projet (npm install / uv sync / cargo fetch)
- substitution {project} {dir} au rendu ; --template list pour lister
- fallback détection automatique de stack quand aucun template
- config générée validée (parse YAML + validate) par les tests

Axe 4 (#53) : am sessions export + rétention configurable
- am sessions --export <id> [--output <fichier>] : JSON complet et
  reproductible (métadonnées + commandes history/<id>.jsonl + extrait de
  log 4 Ko + rendu lisible 'readable'), compatible archivage manuel
- settings.sessions_retention_days (défaut 90) : purge au démarrage
  (silencieuse) et am sessions --retention <jours>
- purge limitée à l'index dérivé sessions.json — le journal d'événements
  primaire n'est jamais touché ; EventKind::Prune journalisé (visibilité
  am audit)
- sessions en cours (running) jamais purgées ; erreur claire si id inconnu

REPL : sessions --export/--retention/--output + init --template ; help
specs, tip cheat sheet, config.yaml, README, man pages régénérées.
Version 0.4.7, ROADMAP cochée, 276 tests verts.
2026-08-18 10:08:20 -04:00

139 lines
3.9 KiB
Rust

//! Embedded project templates for 'am init --template' (issue #52).
//! Each template renders an agent-manager.yaml pre-filled for a stack:
//! a dev group, a default profile and project hooks. Variables {project}
//! and {dir} are substituted at render time.
use anyhow::{bail, Result};
/// (name, description) of every available template.
pub const TEMPLATES: &[(&str, &str)] = &[
("web", "frontend/web full-stack : node, npm, tests, serveur local"),
("python", "python : uv/pip, pytest, ruff, mypy"),
("rust", "rust : cargo, clippy, tests, rustfmt"),
("cli", "générique CLI : commandes shell, scripts, git"),
];
/// The four embedded templates.
const WEB: &str = r#"# Généré par 'am init --template web' — projet {project}
# Répertoire : {dir}
version: "1.0"
groups:
dev: [claude-code, opencode, gemini-cli]
profiles:
dev:
agent: claude-code
env:
NODE_ENV: development
CI: "1"
args: []
settings:
hooks:
on_install: ["npm install"]
on_start: ["npm run dev"]
agents: []
"#;
const PYTHON: &str = r#"# Généré par 'am init --template python' — projet {project}
# Répertoire : {dir}
version: "1.0"
groups:
dev: [claude-code, codex, aider]
profiles:
dev:
agent: claude-code
env:
PYTHONUNBUFFERED: "1"
PYTHONDONTWRITEBYTECODE: "1"
args: []
settings:
hooks:
on_install: ["uv sync --all-extras"]
on_start: ["uv run pytest -q"]
agents: []
"#;
const RUST: &str = r#"# Généré par 'am init --template rust' — projet {project}
# Répertoire : {dir}
version: "1.0"
groups:
dev: [claude-code, codex, jcode]
profiles:
dev:
agent: claude-code
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: "1"
args: []
settings:
hooks:
on_install: ["cargo fetch"]
on_start: ["cargo test"]
agents: []
"#;
const CLI: &str = r#"# Généré par 'am init --template cli' — projet {project}
# Répertoire : {dir}
version: "1.0"
groups:
dev: [claude-code, opencode]
profiles:
dev:
agent: claude-code
env: {}
args: []
agents: []
"#;
fn template_of(name: &str) -> Option<&'static str> {
match name {
"web" => Some(WEB),
"python" => Some(PYTHON),
"rust" => Some(RUST),
"cli" => Some(CLI),
_ => None,
}
}
/// Render a template for a project. `project` is the project name (used in
/// the header comment), `dir` the absolute directory path. Unknown template
/// names fail with the available list.
pub fn render(name: &str, project: &str, dir: &str) -> Result<String> {
let Some(tpl) = template_of(name) else {
let list = TEMPLATES
.iter()
.map(|(n, _)| *n)
.collect::<Vec<_>>()
.join(", ");
bail!("unknown template '{name}' — available: {list}");
};
Ok(tpl.replace("{project}", project).replace("{dir}", dir))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn renders_all_templates_with_substitution() {
for (name, _) in TEMPLATES {
let out = render(name, "mon-app", "/work/mon-app").unwrap();
assert!(out.contains("mon-app"), "{name}: project substituted");
assert!(out.contains("/work/mon-app"), "{name}: dir substituted");
assert!(!out.contains("{project}"), "{name}: no leftover var");
assert!(!out.contains("{dir}"), "{name}: no leftover var");
// The rendered config must parse and validate.
let cfg: crate::config::Config = serde_yaml::from_str(&out).unwrap_or_else(|e| {
panic!("{name}: invalid YAML: {e}\n{out}")
});
assert!(crate::config::validate(&cfg).is_empty(), "{name}: invalid config");
assert!(!cfg.groups.is_empty(), "{name}: a dev group is expected");
}
}
#[test]
fn unknown_template_fails_with_list() {
let err = render("cobol", "x", "/tmp").unwrap_err();
assert!(err.to_string().contains("web"), "{err}");
}
}