Skip to main content

theater_cli/templates/
mod.rs

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/// Template metadata loaded from template.toml
10#[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/// Combined template info with files for internal use
23#[derive(Debug, Clone)]
24pub struct Template {
25    pub name: String,
26    pub description: String,
27    pub files: HashMap<String, String>,
28}
29
30/// Template data for rendering
31#[derive(Debug, Clone, Serialize)]
32pub struct TemplateData {
33    pub project_name: String,
34    pub project_name_snake: String,
35}
36
37/// Get the path to the templates directory
38fn templates_dir() -> Result<PathBuf, io::Error> {
39    // Try multiple possible locations for templates
40
41    // 1. First try relative to the current executable (for installed binaries)
42    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    // 2. Try relative to current working directory (for development)
54    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    // 3. Try in the CLI crate directory (for development from project root)
62    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    // 4. Fallback to compile-time path (for development)
73    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
90/// Available templates for creating new actors
91pub 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    // Read all template directories
106    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            // Load template.toml
119            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
154/// Load template metadata from template.toml
155fn 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
165/// Create a new actor project from a template
166pub 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    // Create the target directory
184    fs::create_dir_all(target_dir)?;
185
186    // Setup Handlebars renderer
187    let mut handlebars = Handlebars::new();
188    handlebars.set_strict_mode(true);
189
190    // Register default helper (this should match what's used in the main theater crate)
191    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    // Prepare template data
220    let template_data = TemplateData {
221        project_name: project_name.to_string(),
222        project_name_snake: project_name.replace('-', "_"),
223    };
224
225    // Get template directory
226    let template_dir = templates_dir()?.join(template_name);
227
228    // Create all template files
229    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        // Create parent directories if they don't exist
234        if let Some(parent) = target_file_path.parent() {
235            if !parent.exists() {
236                fs::create_dir_all(parent)?;
237            }
238        }
239
240        // Read template content
241        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        // Render template with Handlebars
253        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        // Write the rendered file
269        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
278/// List all available templates
279pub 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}