1use handlebars::Handlebars;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::fs;
5use std::io;
6use std::path::{Path, PathBuf};
7use tracing::{debug, info};
8
9#[derive(Debug, Clone, Deserialize)]
11pub struct TemplateMetadata {
12 pub template: TemplateInfo,
13 pub files: HashMap<String, String>,
14}
15
16#[derive(Debug, Clone, Deserialize)]
17pub struct TemplateInfo {
18 pub name: String,
19 pub description: String,
20}
21
22#[derive(Debug, Clone)]
24pub struct Template {
25 pub name: String,
26 pub description: String,
27 pub files: HashMap<String, String>,
28}
29
30#[derive(Debug, Clone, Serialize)]
32pub struct TemplateData {
33 pub project_name: String,
34 pub project_name_snake: String,
35}
36
37fn templates_dir() -> Result<PathBuf, io::Error> {
39 if let Ok(exe_path) = std::env::current_exe() {
43 if let Some(exe_dir) = exe_path.parent() {
44 let templates_path = exe_dir.join("templates");
45 debug!("Trying executable dir: {}", templates_path.display());
46 if templates_path.exists() {
47 debug!("Found templates at: {}", templates_path.display());
48 return Ok(templates_path);
49 }
50 }
51 }
52
53 let cwd_templates = std::env::current_dir()?.join("templates");
55 debug!("Trying current working dir: {}", cwd_templates.display());
56 if cwd_templates.exists() {
57 debug!("Found templates at: {}", cwd_templates.display());
58 return Ok(cwd_templates);
59 }
60
61 let cli_crate_templates = std::env::current_dir()?
63 .join("crates")
64 .join("theater-cli")
65 .join("templates");
66 debug!("Trying CLI crate dir: {}", cli_crate_templates.display());
67 if cli_crate_templates.exists() {
68 debug!("Found templates at: {}", cli_crate_templates.display());
69 return Ok(cli_crate_templates);
70 }
71
72 let manifest_dir = env!("CARGO_MANIFEST_DIR");
74 let compile_time_templates = PathBuf::from(manifest_dir).join("templates");
75 debug!(
76 "Trying compile-time dir: {}",
77 compile_time_templates.display()
78 );
79 if compile_time_templates.exists() {
80 debug!("Found templates at: {}", compile_time_templates.display());
81 return Ok(compile_time_templates);
82 }
83
84 Err(io::Error::new(
85 io::ErrorKind::NotFound,
86 "Templates directory not found in any expected location",
87 ))
88}
89
90pub fn available_templates() -> Result<HashMap<String, Template>, io::Error> {
92 let mut templates = HashMap::new();
93 let templates_path = templates_dir()?;
94
95 if !templates_path.exists() {
96 return Err(io::Error::new(
97 io::ErrorKind::NotFound,
98 format!(
99 "Templates directory not found: {}",
100 templates_path.display()
101 ),
102 ));
103 }
104
105 for entry in fs::read_dir(&templates_path)? {
107 let entry = entry?;
108 let path = entry.path();
109
110 if path.is_dir() {
111 let template_name = path.file_name().and_then(|n| n.to_str()).ok_or_else(|| {
112 io::Error::new(
113 io::ErrorKind::InvalidData,
114 "Invalid template directory name",
115 )
116 })?;
117
118 let metadata_path = path.join("template.toml");
120 if metadata_path.exists() {
121 match load_template_metadata(&metadata_path) {
122 Ok(metadata) => {
123 debug!(
124 "Loaded template: {} - {}",
125 template_name, metadata.template.description
126 );
127 let template = Template {
128 name: metadata.template.name,
129 description: metadata.template.description,
130 files: metadata.files,
131 };
132 templates.insert(template_name.to_string(), template);
133 }
134 Err(e) => {
135 debug!("Failed to load template {}: {}", template_name, e);
136 }
137 }
138 } else {
139 debug!("Template {} missing template.toml, skipping", template_name);
140 }
141 }
142 }
143
144 if templates.is_empty() {
145 return Err(io::Error::new(
146 io::ErrorKind::NotFound,
147 "No valid templates found",
148 ));
149 }
150
151 Ok(templates)
152}
153
154fn load_template_metadata(path: &Path) -> Result<TemplateMetadata, io::Error> {
156 let content = fs::read_to_string(path)?;
157 toml::from_str(&content).map_err(|e| {
158 io::Error::new(
159 io::ErrorKind::InvalidData,
160 format!("Invalid template.toml: {}", e),
161 )
162 })
163}
164
165pub fn create_project(
167 template_name: &str,
168 project_name: &str,
169 target_dir: &Path,
170) -> Result<(), io::Error> {
171 let templates = available_templates()?;
172 let template = templates
173 .get(template_name)
174 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Template not found"))?;
175
176 info!(
177 "Creating new {} project '{}' in {}",
178 template_name,
179 project_name,
180 target_dir.display()
181 );
182
183 fs::create_dir_all(target_dir)?;
185
186 let mut handlebars = Handlebars::new();
188 handlebars.set_strict_mode(true);
189
190 handlebars.register_helper(
192 "default",
193 Box::new(
194 |h: &handlebars::Helper,
195 _: &Handlebars,
196 _: &handlebars::Context,
197 _: &mut handlebars::RenderContext,
198 out: &mut dyn handlebars::Output|
199 -> handlebars::HelperResult {
200 let value = h.param(0).and_then(|v| v.value().as_str());
201 let default = h.param(1).and_then(|v| v.value().as_str()).unwrap_or("");
202
203 let result = if let Some(val) = value {
204 if val.is_empty() {
205 default
206 } else {
207 val
208 }
209 } else {
210 default
211 };
212
213 out.write(result)?;
214 Ok(())
215 },
216 ),
217 );
218
219 let template_data = TemplateData {
221 project_name: project_name.to_string(),
222 project_name_snake: project_name.replace('-', "_"),
223 };
224
225 let template_dir = templates_dir()?.join(template_name);
227
228 for (target_path, template_file) in &template.files {
230 let source_file_path = template_dir.join(template_file);
231 let target_file_path = target_dir.join(target_path);
232
233 if let Some(parent) = target_file_path.parent() {
235 if !parent.exists() {
236 fs::create_dir_all(parent)?;
237 }
238 }
239
240 let template_content = fs::read_to_string(&source_file_path).map_err(|e| {
242 io::Error::new(
243 io::ErrorKind::NotFound,
244 format!(
245 "Template file not found: {} ({})",
246 source_file_path.display(),
247 e
248 ),
249 )
250 })?;
251
252 let rendered_content = handlebars
254 .render_template(&template_content, &template_data)
255 .map_err(|e| {
256 io::Error::new(
257 io::ErrorKind::InvalidData,
258 format!("Template rendering failed for {}: {}", template_file, e),
259 )
260 })?;
261
262 debug!(
263 "Creating file: {} ({} bytes)",
264 target_file_path.display(),
265 rendered_content.len()
266 );
267
268 fs::write(&target_file_path, rendered_content)?;
270 }
271
272 info!("Project '{}' created successfully!", project_name);
273 info!("Note: You may need to run 'wkg wit fetch' to fetch WIT dependencies");
274
275 Ok(())
276}
277
278pub fn list_templates() -> Result<(), io::Error> {
280 let templates = available_templates()?;
281
282 println!("Available templates:");
283 for (name, template) in templates {
284 println!(" {}: {}", name, template.description);
285 }
286
287 Ok(())
288}