//! 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 { let Some(tpl) = template_of(name) else { let list = TEMPLATES .iter() .map(|(n, _)| *n) .collect::>() .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}"); } }