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