1use rust_embed::RustEmbed;
4use std::fs;
5use std::path::PathBuf;
6use tempfile::TempDir;
7use thiserror::Error;
8
9#[derive(Debug, Error)]
11pub enum TemplateError {
12 #[error("Failed to create temporary directory: {0}")]
13 TempDirCreation(#[from] std::io::Error),
14
15 #[error("Failed to extract template file '{path}': {source}")]
16 FileExtraction {
17 path: String,
18 source: std::io::Error,
19 },
20
21 #[error("Template file '{0}' not found in embedded templates")]
22 TemplateNotFound(String),
23}
24
25#[derive(RustEmbed)]
27#[folder = "templates/"]
28pub struct Templates;
29
30pub fn extract_to_temp() -> Result<(TempDir, PathBuf), TemplateError> {
41 let temp_dir = TempDir::new()?;
42 let temp_path = temp_dir.path().to_path_buf();
43
44 for file_path in Templates::iter() {
45 let file_data = Templates::get(&file_path)
46 .ok_or_else(|| TemplateError::TemplateNotFound(file_path.to_string()))?;
47
48 let target_path = temp_path.join(file_path.as_ref());
49
50 if let Some(parent) = target_path.parent() {
52 fs::create_dir_all(parent).map_err(|e| TemplateError::FileExtraction {
53 path: file_path.to_string(),
54 source: e,
55 })?;
56 }
57
58 fs::write(&target_path, file_data.data.as_ref()).map_err(|e| {
60 TemplateError::FileExtraction {
61 path: file_path.to_string(),
62 source: e,
63 }
64 })?;
65 }
66
67 Ok((temp_dir, temp_path))
68}
69
70pub fn required_gitignore_lines() -> Vec<&'static str> {
72 vec![
73 ".beads/.br_history/",
74 ".beads/beads.db",
75 ".beads/beads.db-wal",
76 ".beads/last-touched",
77 ]
78}
79
80pub fn managed_files() -> Vec<&'static str> {
81 vec![
82 "AGENTS.md",
83 "CLAUDE.md", "RUST_STYLE_GUIDE.md",
85 "TESTING.md",
86 ".claude/agents/coordinator.md",
87 ".claude/agents/coding.md",
88 ".claude/agents/judge.md",
89 ".claude/agents/tidy.md",
90 ".claude/agents/reflection.md",
91 ".config/nextest.toml",
92 "deny.toml",
93 "rustfmt.toml",
94 ".devcontainer/Dockerfile",
95 ".devcontainer/devcontainer.json",
96 ".beads/config.yaml",
97 "justfile-rustbucket",
98 ]
99}
100
101pub fn seed_files() -> Vec<(&'static str, &'static str)> {
109 vec![
110 ("ratchets.toml.liquid", "ratchets.toml"),
111 ("STYLE_GUIDE.md.liquid", "STYLE_GUIDE.md"),
112 ]
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn test_extract_to_temp() -> Result<(), Box<dyn std::error::Error>> {
121 let (_temp_dir, temp_path) = extract_to_temp()?;
122 assert!(temp_path.exists());
123 assert!(temp_path.is_dir());
124 Ok(())
125 }
126
127 #[test]
128 fn test_managed_files_not_empty() {
129 let files = managed_files();
130 assert!(!files.is_empty());
131 assert_eq!(files.len(), 16);
132 }
133
134 #[test]
135 fn test_managed_files_includes_expected() {
136 let files = managed_files();
137 assert!(files.contains(&"AGENTS.md"));
138 assert!(files.contains(&"RUST_STYLE_GUIDE.md"));
139 assert!(files.contains(&".config/nextest.toml"));
140 assert!(files.contains(&".devcontainer/Dockerfile"));
141 }
142
143 #[test]
144 fn test_seed_files_registers_ratchets_toml() {
145 let seeds = seed_files();
146 assert!(seeds.contains(&("ratchets.toml.liquid", "ratchets.toml")));
147 }
148
149 #[test]
150 fn test_seed_files_registers_style_guide() {
151 let seeds = seed_files();
152 assert!(seeds.contains(&("STYLE_GUIDE.md.liquid", "STYLE_GUIDE.md")));
153 }
154
155 #[test]
156 fn test_ratchets_toml_not_managed() {
157 let managed = managed_files();
158 assert!(!managed.contains(&"ratchets.toml"));
159 assert_eq!(managed.len(), 16);
160 }
161
162 #[test]
163 fn test_style_guide_not_managed() {
164 let managed = managed_files();
165 assert!(!managed.contains(&"STYLE_GUIDE.md"));
166 assert_eq!(managed.len(), 16);
167 }
168}