Skip to main content

realm/templates/
manager.rs

1use super::builtin::{fastapi, nextjs, react, svelte, vue};
2use super::template::{Template, TemplateFile};
3use crate::config::RealmConfig;
4use anyhow::{anyhow, Context, Result};
5use dirs::home_dir;
6use std::fs;
7use std::path::{Path, PathBuf};
8
9pub struct TemplateManager {
10  templates_dir: PathBuf,
11}
12
13impl TemplateManager {
14  pub fn new() -> Result<Self> {
15    let home = home_dir().ok_or_else(|| anyhow!("Could not find home directory"))?;
16    let templates_dir = home.join(".realm").join("templates");
17
18    if !templates_dir.exists() {
19      fs::create_dir_all(&templates_dir).context("Failed to create templates directory")?;
20    }
21
22    Ok(Self { templates_dir })
23  }
24
25  pub fn create_template_from_current_dir(&self, name: &str) -> Result<()> {
26    let current_dir = std::env::current_dir()?;
27
28    // Check if realm.yml exists
29    let realm_yml_path = current_dir.join("realm.yml");
30    if !realm_yml_path.exists() {
31      return Err(anyhow!(
32        "No realm.yml found in current directory. Initialize a realm project first."
33      ));
34    }
35
36    let realm_config = RealmConfig::load(&realm_yml_path)?;
37
38    // Collect all files in current directory (excluding common ignore patterns)
39    let mut files = Vec::new();
40    self.collect_template_files(&current_dir, &current_dir, &mut files)?;
41
42    let template = Template {
43      name: name.to_string(),
44      description: format!("Template created from {}", current_dir.display()),
45      version: "1.0.0".to_string(),
46      files,
47      realm_config,
48      variables: std::collections::HashMap::new(),
49    };
50
51    // Save template
52    let template_dir = self.templates_dir.join(name);
53    fs::create_dir_all(&template_dir)?;
54
55    let template_file = template_dir.join("template.yml");
56    let template_content = serde_yaml::to_string(&template)?;
57    fs::write(template_file, template_content)?;
58
59    println!("Template '{name}' created successfully");
60    println!("Template saved to: {}", template_dir.display());
61
62    Ok(())
63  }
64
65  pub fn init_from_template(&self, template_name: &str, target_dir: &Path) -> Result<()> {
66    let template = self.load_template(template_name)?;
67
68    if target_dir.exists() && target_dir.read_dir()?.next().is_some() {
69      return Err(anyhow!("Target directory is not empty"));
70    }
71
72    fs::create_dir_all(target_dir)?;
73
74    // Create files from template
75    for file in &template.files {
76      let file_path = target_dir.join(&file.path);
77
78      if let Some(parent) = file_path.parent() {
79        fs::create_dir_all(parent)?;
80      }
81
82      // Process template variables (simple string replacement)
83      let content = self.process_template_variables(&file.content, &template.variables);
84      fs::write(&file_path, content)?;
85
86      // Set executable if needed
87      #[cfg(unix)]
88      if file.executable {
89        use std::os::unix::fs::PermissionsExt;
90        let mut perms = fs::metadata(&file_path)?.permissions();
91        perms.set_mode(0o755);
92        fs::set_permissions(&file_path, perms)?;
93      }
94    }
95
96    // Create realm.yml
97    let realm_yml_path = target_dir.join("realm.yml");
98    template.realm_config.save(&realm_yml_path)?;
99
100    println!(
101      "Project created from template '{}' in {}",
102      template_name,
103      target_dir.display()
104    );
105    println!("Next steps:");
106    println!("  cd {}", target_dir.display());
107    println!("  realm init .venv");
108    println!("  source .venv/bin/activate");
109    println!("  realm start");
110
111    Ok(())
112  }
113
114  pub fn list_templates(&self) -> Result<Vec<String>> {
115    let mut templates = Vec::new();
116
117    if !self.templates_dir.exists() {
118      return Ok(templates);
119    }
120
121    for entry in fs::read_dir(&self.templates_dir)? {
122      let entry = entry?;
123      if entry.file_type()?.is_dir() {
124        if let Some(name) = entry.file_name().to_str() {
125          templates.push(name.to_string());
126        }
127      }
128    }
129
130    templates.sort();
131    Ok(templates)
132  }
133
134  fn load_template(&self, name: &str) -> Result<Template> {
135    let template_file = self.templates_dir.join(name).join("template.yml");
136
137    if !template_file.exists() {
138      return Err(anyhow!("Template '{}' not found", name));
139    }
140
141    let content = fs::read_to_string(template_file)?;
142    let template: Template = serde_yaml::from_str(&content)?;
143    Ok(template)
144  }
145
146  fn collect_template_files(
147    &self,
148    base_dir: &Path,
149    current_dir: &Path,
150    files: &mut Vec<TemplateFile>,
151  ) -> Result<()> {
152    for entry in fs::read_dir(current_dir)? {
153      let entry = entry?;
154      let path = entry.path();
155
156      // Skip common ignore patterns
157      if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
158        if name.starts_with('.') && name != ".env" {
159          continue;
160        }
161        if matches!(name, "node_modules" | "target" | "dist" | "build" | ".git") {
162          continue;
163        }
164      }
165
166      let relative_path = path.strip_prefix(base_dir)?.to_string_lossy().to_string();
167
168      if path.is_file() {
169        let content = fs::read_to_string(&path)?;
170        let executable = self.is_executable(&path)?;
171
172        files.push(TemplateFile {
173          path: relative_path,
174          content,
175          executable,
176        });
177      } else if path.is_dir() {
178        self.collect_template_files(base_dir, &path, files)?;
179      }
180    }
181
182    Ok(())
183  }
184
185  fn is_executable(&self, path: &Path) -> Result<bool> {
186    #[cfg(unix)]
187    {
188      use std::os::unix::fs::PermissionsExt;
189      let metadata = fs::metadata(path)?;
190      Ok(metadata.permissions().mode() & 0o111 != 0)
191    }
192    #[cfg(not(unix))]
193    {
194      Ok(false)
195    }
196  }
197
198  fn process_template_variables(
199    &self,
200    content: &str,
201    _variables: &std::collections::HashMap<String, String>,
202  ) -> String {
203    // Simple implementation - could be enhanced with proper templating
204    content.to_string()
205  }
206
207  pub fn create_builtin_templates(&self) -> Result<()> {
208    react::create_template(&self.templates_dir)?;
209    svelte::create_template(&self.templates_dir)?;
210    vue::create_template(&self.templates_dir)?;
211    nextjs::create_template(&self.templates_dir)?;
212    fastapi::create_template(&self.templates_dir)?;
213    Ok(())
214  }
215}
216
217impl Default for TemplateManager {
218  fn default() -> Self {
219    Self::new().expect("Failed to create TemplateManager")
220  }
221}