use std::{collections::HashMap, fs, path::Path};
use include_dir::{Dir, include_dir};
static TEMPLATES_COMMON: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/templates/common");
static TEMPLATES_DESIGN: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/templates/design");
static TEMPLATES_BUNDLE: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/templates/bundle");
static TEMPLATES_DEPLOY: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/templates/deploy");
static TEMPLATES_PROVIDER: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/templates/provider");
static TEMPLATES_WASM_COMPONENT: Dir<'_> =
include_dir!("$CARGO_MANIFEST_DIR/templates/wasm-component");
static TEMPLATES_LLM: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/templates/llm");
static TEMPLATES_MCP: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/templates/mcp");
static TEMPLATES_OPENAPI_CONNECTOR: Dir<'_> =
include_dir!("$CARGO_MANIFEST_DIR/templates/openapi-connector");
#[derive(Debug, Clone)]
pub struct TemplateEntry {
pub src_bytes: &'static [u8],
pub dst_rel: String,
}
fn collect(dir: &'static Dir<'static>) -> Vec<TemplateEntry> {
let mut out = Vec::new();
collect_rec(dir, &mut out);
out
}
fn collect_rec(dir: &'static Dir<'static>, out: &mut Vec<TemplateEntry>) {
for entry in dir.entries() {
match entry {
include_dir::DirEntry::File(f) => {
let rel = f.path().to_string_lossy().to_string();
let dst = translate_dst(&rel);
out.push(TemplateEntry {
src_bytes: f.contents(),
dst_rel: dst,
});
}
include_dir::DirEntry::Dir(d) => collect_rec(d, out),
}
}
}
fn translate_dst(rel: &str) -> String {
let mut dst = rel.trim_end_matches(".tmpl").to_string();
if dst == "gitignore" {
dst = ".gitignore".to_string();
}
if let Some(rest) = dst.strip_prefix("claude/") {
dst = format!(".claude/{rest}");
}
dst
}
pub fn load_templates_common() -> Vec<TemplateEntry> {
collect(&TEMPLATES_COMMON)
}
fn overlay(base: Vec<TemplateEntry>, over: Vec<TemplateEntry>) -> Vec<TemplateEntry> {
let mut out = base;
for entry in over {
if let Some(slot) = out.iter_mut().find(|e| e.dst_rel == entry.dst_rel) {
*slot = entry;
} else {
out.push(entry);
}
}
out
}
pub fn load_templates_kind(kind: &str) -> Vec<TemplateEntry> {
match kind {
"design" => collect(&TEMPLATES_DESIGN),
"bundle" => collect(&TEMPLATES_BUNDLE),
"deploy" => collect(&TEMPLATES_DEPLOY),
"provider" => collect(&TEMPLATES_PROVIDER),
"wasm-component" => overlay(
collect(&TEMPLATES_DESIGN),
collect(&TEMPLATES_WASM_COMPONENT),
),
"llm" => collect(&TEMPLATES_LLM),
"mcp" => collect(&TEMPLATES_MCP),
"openapi-connector" => collect(&TEMPLATES_OPENAPI_CONNECTOR),
_ => Vec::new(),
}
}
pub struct Context {
values: HashMap<&'static str, String>,
}
impl Context {
pub fn new() -> Self {
Self {
values: HashMap::new(),
}
}
pub fn set(&mut self, key: &'static str, value: impl Into<String>) -> &mut Self {
self.values.insert(key, value.into());
self
}
pub fn render(&self, template: &str) -> anyhow::Result<String> {
let mut out = template.to_string();
let mut remaining_passes = 4;
while remaining_passes > 0 {
let before = out.clone();
for (key, value) in &self.values {
let token = format!("{{{{{key}}}}}");
out = out.replace(&token, value);
}
if out == before {
break;
}
remaining_passes -= 1;
}
if let Some(pos) = out.find("{{") {
let end = out[pos..].find("}}").map_or(out.len(), |e| pos + e + 2);
anyhow::bail!("unsubstituted placeholder: {}", &out[pos..end]);
}
Ok(out)
}
}
impl Default for Context {
fn default() -> Self {
Self::new()
}
}
pub fn write_file(path: &Path, bytes: &[u8]) -> anyhow::Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(path, bytes)?;
Ok(())
}
#[allow(dead_code)] pub fn render_and_write(ctx: &Context, template: &str, path: &Path) -> anyhow::Result<()> {
let rendered = ctx.render(template)?;
write_file(path, rendered.as_bytes())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn render_substitutes_placeholder() {
let mut ctx = Context::new();
ctx.set("name", "demo");
let out = ctx.render("hello {{name}}!").unwrap();
assert_eq!(out, "hello demo!");
}
#[test]
fn render_multiple_placeholders() {
let mut ctx = Context::new();
ctx.set("name", "demo").set("version", "0.1.0");
let out = ctx.render("{{name}}@{{version}}").unwrap();
assert_eq!(out, "demo@0.1.0");
}
#[test]
fn render_unsubstituted_placeholder_errors() {
let ctx = Context::new();
let err = ctx.render("hello {{missing}}").unwrap_err();
assert!(err.to_string().contains("{{missing}}"));
}
#[test]
fn render_literal_text_passthrough() {
let ctx = Context::new();
let out = ctx.render("plain text no braces").unwrap();
assert_eq!(out, "plain text no braces");
}
#[test]
fn write_file_creates_parent_dirs() {
let tmp = tempfile::tempdir().unwrap();
let dst = tmp.path().join("a/b/c/file.txt");
write_file(&dst, b"hello").unwrap();
assert_eq!(std::fs::read(&dst).unwrap(), b"hello");
}
#[test]
fn render_and_write_substitutes_before_writing() {
let tmp = tempfile::tempdir().unwrap();
let dst = tmp.path().join("out.txt");
let mut ctx = Context::new();
ctx.set("who", "world");
render_and_write(&ctx, "hello {{who}}", &dst).unwrap();
assert_eq!(std::fs::read_to_string(&dst).unwrap(), "hello world");
}
#[test]
fn load_common_returns_gitignore_template() {
let entries = load_templates_common();
assert!(
entries
.iter()
.any(|e| e.dst_rel == "gitignore.tmpl" || e.dst_rel == ".gitignore")
);
}
#[test]
fn load_common_returns_agent_onboarding_docs() {
let entries = load_templates_common();
for expected in ["AGENTS.md", "CLAUDE.md"] {
assert!(
entries.iter().any(|e| e.dst_rel == expected),
"common templates missing {expected}: {:?}",
entries.iter().map(|e| &e.dst_rel).collect::<Vec<_>>(),
);
}
let claude = entries
.iter()
.find(|e| e.dst_rel == "CLAUDE.md")
.expect("CLAUDE.md present");
let body = std::str::from_utf8(claude.src_bytes).expect("utf8");
assert!(
body.contains("AGENTS.md"),
"CLAUDE.md must reference AGENTS.md:\n{body}",
);
}
#[test]
fn load_common_returns_dotclaude_config() {
let entries = load_templates_common();
for expected in [".claude/settings.json", ".claude/commands/check.md"] {
assert!(
entries.iter().any(|e| e.dst_rel == expected),
"common templates missing {expected}: {:?}",
entries.iter().map(|e| &e.dst_rel).collect::<Vec<_>>(),
);
}
let settings = entries
.iter()
.find(|e| e.dst_rel == ".claude/settings.json")
.expect(".claude/settings.json present");
let body = std::str::from_utf8(settings.src_bytes).expect("utf8");
let parsed: serde_json::Value = serde_json::from_str(body).expect("settings.json parses");
let allow = parsed
.get("permissions")
.and_then(|p| p.get("allow"))
.and_then(|a| a.as_array())
.expect("permissions.allow is an array");
assert!(
allow.iter().any(|v| v.as_str() == Some("Bash(gtdx:*)")),
"settings.json must pre-approve gtdx commands:\n{body}",
);
}
#[test]
fn load_kind_design_returns_cargo_toml() {
let entries = load_templates_kind("design");
assert!(entries.iter().any(|e| e.dst_rel == "Cargo.toml"));
assert!(entries.iter().any(|e| e.dst_rel == "describe.json"));
assert!(entries.iter().any(|e| e.dst_rel == "src/lib.rs"));
}
#[test]
fn every_kind_template_ships_rust_toolchain_pinned_to_1_95_0() {
for kind in [
"design",
"bundle",
"deploy",
"provider",
"wasm-component",
"llm",
"mcp",
] {
let entries = load_templates_kind(kind);
let toolchain = entries
.iter()
.find(|e| e.dst_rel == "rust-toolchain.toml")
.unwrap_or_else(|| panic!("kind {kind} missing rust-toolchain.toml template"));
let content = std::str::from_utf8(toolchain.src_bytes).expect("utf8");
assert!(
content.contains("channel = \"1.95.0\""),
"kind {kind} toolchain template does not pin 1.95.0:\n{content}",
);
}
}
#[test]
fn every_kind_template_ships_wit_bindgen_rt_0_41() {
for kind in ["design", "bundle", "deploy", "provider", "llm"] {
let entries = load_templates_kind(kind);
let cargo_toml = entries
.iter()
.find(|e| e.dst_rel == "Cargo.toml")
.unwrap_or_else(|| panic!("kind {kind} missing Cargo.toml template"));
let content = std::str::from_utf8(cargo_toml.src_bytes).expect("utf8");
assert!(
content.contains("wit-bindgen-rt = { version = \"0.41\""),
"kind {kind} Cargo.toml does not pin wit-bindgen-rt 0.41:\n{content}",
);
}
}
#[test]
fn mcp_kind_template_ships_wit_bindgen_macros() {
let entries = load_templates_kind("mcp");
let cargo_toml = entries
.iter()
.find(|e| e.dst_rel == "Cargo.toml")
.unwrap_or_else(|| panic!("mcp kind missing Cargo.toml template"));
let content = std::str::from_utf8(cargo_toml.src_bytes).expect("utf8");
assert!(
content.contains("wit-bindgen") && content.contains("macros"),
"mcp Cargo.toml must depend on wit-bindgen with macros feature:\n{content}",
);
assert!(
!content.contains("wit-bindgen-rt"),
"mcp Cargo.toml must NOT use wit-bindgen-rt (use wit-bindgen + macros instead):\n{content}",
);
}
#[test]
fn wasm_component_cargo_toml_has_no_workspace_inherits() {
let entries = load_templates_kind("wasm-component");
let cargo_toml = entries
.iter()
.find(|e| e.dst_rel.ends_with("Cargo.toml"))
.expect("wasm-component must ship a Cargo.toml template");
let content = std::str::from_utf8(cargo_toml.src_bytes).expect("utf8");
assert!(
!content.contains(".workspace = true") && !content.contains(".workspace=true"),
"wasm-component Cargo.toml must not inherit from a (non-existent) workspace:\n{content}",
);
assert!(
content.contains("edition = \"2024\""),
"wasm-component Cargo.toml must pin edition concretely:\n{content}",
);
}
#[test]
fn every_kind_describe_template_is_v2() {
for kind in [
"design",
"bundle",
"deploy",
"provider",
"wasm-component",
"llm",
"mcp",
] {
let entries = load_templates_kind(kind);
let describe = entries
.iter()
.find(|e| e.dst_rel == "describe.json")
.unwrap_or_else(|| panic!("kind {kind} missing describe.json template"));
let content = std::str::from_utf8(describe.src_bytes).expect("utf8");
assert!(
content.contains("\"apiVersion\": \"greentic.ai/v2\""),
"kind {kind} describe.json template not v2:\n{content}",
);
assert!(
content.contains("\"compat\":"),
"kind {kind} describe.json missing `compat` block:\n{content}",
);
assert!(
content.contains("\"components\":"),
"kind {kind} describe.json missing `runtime.components` map:\n{content}",
);
assert!(
!content.contains("\"component\": \"extension.wasm\""),
"kind {kind} describe.json still has v1 singular `runtime.component`:\n{content}",
);
}
}
#[test]
fn load_kind_llm_returns_full_template_set() {
let entries = load_templates_kind("llm");
let names: Vec<&str> = entries.iter().map(|e| e.dst_rel.as_str()).collect();
assert!(
names.contains(&"Cargo.toml"),
"missing Cargo.toml: {names:?}"
);
assert!(
names.contains(&"describe.json"),
"missing describe.json: {names:?}"
);
assert!(
names.contains(&"src/lib.rs"),
"missing src/lib.rs: {names:?}"
);
assert!(
names.contains(&"wit/world.wit"),
"missing wit/world.wit: {names:?}"
);
assert!(
names.contains(&"rust-toolchain.toml"),
"missing rust-toolchain.toml: {names:?}"
);
}
#[test]
fn load_kind_mcp_returns_full_template_set() {
let entries = load_templates_kind("mcp");
let names: Vec<&str> = entries.iter().map(|e| e.dst_rel.as_str()).collect();
for expected in [
"Cargo.toml",
"describe.json",
"src/lib.rs",
"wit/world.wit",
"wit/deps/wasix-mcp/package.wit",
"rust-toolchain.toml",
] {
assert!(names.contains(&expected), "missing {expected}: {names:?}");
}
}
}