1use std::{fs, path::Path};
2
3use anyhow::Result;
4use chrono::Utc;
5use comfy_table::{Attribute, Cell, Color, Table};
6use serde::{Deserialize, Serialize};
7
8use crate::{AppContext, templates::OxideTemplate};
9
10#[derive(Serialize, Deserialize)]
11pub struct TemplatesCache {
12 #[serde(rename = "lastUpdated")]
13 pub last_updated: String,
14 pub templates: Vec<CachedTemplate>,
15}
16
17#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
18pub struct CachedTemplate {
19 pub name: String,
20 pub version: String,
21 pub source: String,
22 pub path: String,
23 pub official: bool,
24 pub commit_sha: String,
25}
26
27pub fn update_templates_cache(
28 template_path: &Path,
29 path: &Path,
30 commit_sha: &str,
31) -> Result<CachedTemplate> {
32 let oxide_json = template_path.join(path).join("oxide.template.json");
33 let content = fs::read_to_string(&oxide_json)?;
34 let template_info: OxideTemplate = serde_json::from_str(&content)?;
35
36 let templates_json = template_path.join("oxide-templates.json");
37 let mut templates_info: TemplatesCache = if templates_json.exists() {
38 let content = fs::read_to_string(&templates_json)?;
39 serde_json::from_str(&content)?
40 } else {
41 TemplatesCache {
42 last_updated: Utc::now().to_rfc3339(),
43 templates: Vec::new(),
44 }
45 };
46
47 templates_info.last_updated = Utc::now().to_rfc3339();
48
49 templates_info
51 .templates
52 .retain(|t| t.name != template_info.name);
53 let cached_template = CachedTemplate {
54 name: template_info.name,
55 version: template_info.version,
56 source: template_info.repository.url,
57 path: path.to_string_lossy().to_string(),
58 official: template_info.official,
59 commit_sha: commit_sha.to_string(),
60 };
61 templates_info.templates.push(cached_template.clone());
62
63 fs::write(
64 &templates_json,
65 serde_json::to_string_pretty(&templates_info)?,
66 )?;
67
68 Ok(cached_template)
69}
70
71pub fn get_cached_template(ctx: &AppContext, name: &str) -> Result<Option<CachedTemplate>> {
72 let templates_json = ctx.paths.templates.join("oxide-templates.json");
73
74 if !templates_json.exists() {
75 return Ok(None);
76 }
77
78 let content = fs::read_to_string(&templates_json)?;
79 let templates_info: TemplatesCache = serde_json::from_str(&content)?;
80
81 Ok(
82 templates_info
83 .templates
84 .into_iter()
85 .find(|t| t.name == name),
86 )
87}
88
89pub fn remove_template_from_cache(template_path: &Path, template_name: &str) -> Result<()> {
90 let templates_json = template_path.join("oxide-templates.json");
91
92 if !templates_json.exists() {
93 return Err(anyhow::anyhow!(
94 "Template '{}' is not installed",
95 template_name
96 ));
97 }
98
99 let content = fs::read_to_string(&templates_json)?;
100 let mut templates_info: TemplatesCache = serde_json::from_str(&content)?;
101
102 let exists = templates_info
103 .templates
104 .iter()
105 .any(|t| t.name == template_name);
106 if !exists {
107 return Err(anyhow::anyhow!(
108 "Template '{}' is not installed",
109 template_name
110 ));
111 }
112
113 templates_info.last_updated = Utc::now().to_rfc3339();
114
115 if let Some(t) = templates_info
116 .templates
117 .iter()
118 .find(|t| t.name == template_name)
119 {
120 let cleanup_path = template_path.join(&t.path);
121
122 if cleanup_path.exists() {
123 if let Err(e) = fs::remove_dir_all(&cleanup_path) {
124 println!("Failed to remove: {}", e);
125 }
126
127 let mut current = cleanup_path.parent();
128 while let Some(parent) = current {
129 if parent == template_path {
130 break;
131 }
132 if fs::remove_dir(parent).is_err() {
133 break;
134 }
135 current = parent.parent();
136 }
137 }
138 }
139
140 templates_info
141 .templates
142 .retain(|template| template.name != template_name);
143
144 fs::write(
145 &templates_json,
146 serde_json::to_string_pretty(&templates_info)?,
147 )?;
148
149 println!("✓ Removed template '{}'", template_name);
150 Ok(())
151}
152
153pub fn get_installed_templates(template_path: &Path) -> Result<()> {
154 let templates_json = template_path.join("oxide-templates.json");
155
156 let templates_info: TemplatesCache = if templates_json.exists() {
157 let content = fs::read_to_string(&templates_json)?;
158 serde_json::from_str(&content)?
159 } else {
160 TemplatesCache {
161 last_updated: Utc::now().to_rfc3339(),
162 templates: Vec::new(),
163 }
164 };
165
166 if templates_info.templates.is_empty() {
167 println!("No templates installed yet.");
168 return Ok(());
169 }
170
171 let mut table = Table::new();
172
173 table.set_header(vec![
174 Cell::new("Name").add_attribute(Attribute::Bold),
175 Cell::new("Version").add_attribute(Attribute::Bold),
176 Cell::new("Official").add_attribute(Attribute::Bold),
177 ]);
178
179 for template in templates_info.templates {
180 table.add_row(vec![
181 Cell::new(&template.name),
182 Cell::new(&template.version),
183 Cell::new(if template.official { "✓" } else { "✗" }).fg(if template.official {
184 Color::Green
185 } else {
186 Color::Red
187 }),
188 ]);
189 }
190
191 println!(
192 "\nInstalled templates (last updated: {}):",
193 templates_info.last_updated
194 );
195 println!("{table}");
196
197 Ok(())
198}
199
200pub fn is_template_installed(ctx: &AppContext, template_name: &str) -> Result<bool> {
201 let templates_json = ctx.paths.templates.join("oxide-templates.json");
202
203 let templates_info: TemplatesCache = if templates_json.exists() {
204 let content = fs::read_to_string(&templates_json)?;
205 serde_json::from_str(&content)?
206 } else {
207 TemplatesCache {
208 last_updated: Utc::now().to_rfc3339(),
209 templates: Vec::new(),
210 }
211 };
212
213 let path = Path::new(template_name);
214 if !ctx.paths.templates.join(path).exists() {
215 return Ok(false);
216 }
217
218 Ok(
219 templates_info
220 .templates
221 .iter()
222 .any(|t| t.name == template_name),
223 )
224}