realm/templates/
manager.rs1use 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 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 let mut files = Vec::new();
40 self.collect_template_files(¤t_dir, ¤t_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 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 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 let content = self.process_template_variables(&file.content, &template.variables);
84 fs::write(&file_path, content)?;
85
86 #[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 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 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 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}