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.
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
//! 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}");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user