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.
139 lines
3.9 KiB
Rust
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}");
|
|
}
|
|
}
|