1use std::collections::HashSet;
2use std::fs;
3use std::path::{Path, PathBuf};
4use thiserror::Error;
5
6#[derive(Error, Debug)]
7pub enum OutputError {
8 #[error("IO error: {0}")]
9 Io(#[from] std::io::Error),
10 #[error("Invalid output path: {0}")]
11 InvalidPath(String),
12 #[error("Permission denied: {0}")]
13 PermissionDenied(String),
14}
15
16pub struct OutputManager {
17 output_dir: PathBuf,
18 managed_files: HashSet<String>,
19 backup_dir: Option<PathBuf>,
20}
21
22impl OutputManager {
23 pub fn new<P: AsRef<Path>>(output_dir: P) -> Self {
24 Self {
25 output_dir: output_dir.as_ref().to_path_buf(),
26 managed_files: HashSet::new(),
27 backup_dir: None,
28 }
29 }
30
31 pub fn with_backup<P: AsRef<Path>>(output_dir: P, backup_dir: Option<P>) -> Self {
32 Self {
33 output_dir: output_dir.as_ref().to_path_buf(),
34 managed_files: HashSet::new(),
35 backup_dir: backup_dir.map(|p| p.as_ref().to_path_buf()),
36 }
37 }
38
39 pub fn prepare_output_directory(&self) -> Result<(), OutputError> {
41 if !self.output_dir.exists() {
42 fs::create_dir_all(&self.output_dir).map_err(|e| {
43 OutputError::PermissionDenied(format!(
44 "Cannot create output directory {}: {}",
45 self.output_dir.display(),
46 e
47 ))
48 })?;
49 }
50
51 let test_file = self.output_dir.join(".write_test");
53 fs::write(&test_file, "test").map_err(|e| {
54 OutputError::PermissionDenied(format!(
55 "Cannot write to output directory {}: {}",
56 self.output_dir.display(),
57 e
58 ))
59 })?;
60 fs::remove_file(&test_file).ok(); Ok(())
63 }
64
65 pub fn register_managed_file(&mut self, filename: &str) {
67 self.managed_files.insert(filename.to_string());
68 }
69
70 pub fn cleanup_old_files(&self, current_files: &[String]) -> Result<Vec<String>, OutputError> {
72 let mut cleaned_files = Vec::new();
73
74 if !self.output_dir.exists() {
75 return Ok(cleaned_files);
76 }
77
78 let current_set: HashSet<String> = current_files.iter().cloned().collect();
79
80 let entries = fs::read_dir(&self.output_dir)?;
82
83 for entry in entries {
84 let entry = entry?;
85 let path = entry.path();
86
87 if path.is_file() {
88 if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
89 if self.is_generated_file(filename) && !current_set.contains(filename) {
91 self.backup_and_remove_file(&path)?;
92 cleaned_files.push(filename.to_string());
93 }
94 }
95 }
96 }
97
98 Ok(cleaned_files)
99 }
100
101 fn is_generated_file(&self, filename: &str) -> bool {
103 let generated_patterns = [
105 "types.ts",
106 "types.d.ts",
107 "commands.ts",
108 "commands.d.ts",
109 "schemas.ts",
110 "schemas.d.ts",
111 "index.ts",
112 "index.d.ts",
113 "models.ts",
114 "models.d.ts",
115 "bindings.ts",
116 "bindings.d.ts",
117 ];
118
119 generated_patterns.contains(&filename)
120 || filename.starts_with("generated_")
121 || filename.contains("_generated")
122 || self.managed_files.contains(filename)
123 }
124
125 fn backup_and_remove_file(&self, file_path: &Path) -> Result<(), OutputError> {
127 if let Some(backup_dir) = &self.backup_dir {
128 if !backup_dir.exists() {
129 fs::create_dir_all(backup_dir)?;
130 }
131
132 if let Some(filename) = file_path.file_name() {
133 let backup_path = backup_dir.join(format!(
134 "{}.backup.{}",
135 filename.to_string_lossy(),
136 chrono::Utc::now().timestamp()
137 ));
138 fs::copy(file_path, backup_path)?;
139 }
140 }
141
142 fs::remove_file(file_path)?;
143 Ok(())
144 }
145
146 pub fn write_file(&self, filename: &str, content: &str) -> Result<PathBuf, OutputError> {
148 let file_path = self.output_dir.join(filename);
149
150 if let Some(parent) = file_path.parent() {
152 if !parent.exists() {
153 fs::create_dir_all(parent)?;
154 }
155 }
156
157 let temp_path = file_path.with_extension("tmp");
159 fs::write(&temp_path, content)?;
160 fs::rename(&temp_path, &file_path)?;
161
162 Ok(file_path)
163 }
164
165 pub fn verify_output(&self, expected_files: &[String]) -> Result<Vec<String>, OutputError> {
167 let mut missing_files = Vec::new();
168
169 for expected in expected_files {
170 let file_path = self.output_dir.join(expected);
171 if !file_path.exists() {
172 missing_files.push(expected.clone());
173 }
174 }
175
176 Ok(missing_files)
177 }
178
179 pub fn get_generation_metadata(&self) -> Result<GenerationMetadata, OutputError> {
181 let mut metadata = GenerationMetadata {
182 output_directory: self.output_dir.clone(),
183 generated_at: chrono::Utc::now(),
184 files: Vec::new(),
185 total_size: 0,
186 };
187
188 if !self.output_dir.exists() {
189 return Ok(metadata);
190 }
191
192 let entries = fs::read_dir(&self.output_dir)?;
193
194 for entry in entries {
195 let entry = entry?;
196 let path = entry.path();
197
198 if path.is_file() {
199 if let Ok(metadata_entry) = entry.metadata() {
200 let size = metadata_entry.len();
201 metadata.total_size += size;
202
203 if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
204 metadata.files.push(FileMetadata {
205 name: filename.to_string(),
206 path: path.clone(),
207 size,
208 modified: metadata_entry.modified().ok(),
209 });
210 }
211 }
212 }
213 }
214
215 Ok(metadata)
216 }
217
218 pub fn finalize_generation(&mut self, generated_files: &[String]) -> Result<(), OutputError> {
220 self.prepare_output_directory()?;
221
222 for file in generated_files {
224 self.register_managed_file(file);
225 }
226
227 let cleaned = self.cleanup_old_files(generated_files)?;
229 if !cleaned.is_empty() {
230 eprintln!("Cleaned up {} old generated files", cleaned.len());
231 }
232
233 let missing = self.verify_output(generated_files)?;
235 if !missing.is_empty() {
236 return Err(OutputError::InvalidPath(format!(
237 "Missing generated files: {}",
238 missing.join(", ")
239 )));
240 }
241
242 Ok(())
243 }
244
245 pub fn create_summary_report(&self) -> Result<String, OutputError> {
247 let metadata = self.get_generation_metadata()?;
248
249 let mut report = String::new();
250 report.push_str("# TypeScript Generation Summary\n\n");
251 report.push_str(&format!(
252 "Generated at: {}\n",
253 metadata.generated_at.format("%Y-%m-%d %H:%M:%S UTC")
254 ));
255 report.push_str(&format!(
256 "Output directory: {}\n",
257 metadata.output_directory.display()
258 ));
259 report.push_str(&format!("Total files: {}\n", metadata.files.len()));
260 report.push_str(&format!("Total size: {} bytes\n\n", metadata.total_size));
261
262 report.push_str("## Generated Files\n\n");
263 for file in &metadata.files {
264 report.push_str(&format!("- **{}** ({} bytes)\n", file.name, file.size));
265 }
266
267 Ok(report)
268 }
269}
270
271#[derive(Debug)]
272pub struct GenerationMetadata {
273 pub output_directory: PathBuf,
274 pub generated_at: chrono::DateTime<chrono::Utc>,
275 pub files: Vec<FileMetadata>,
276 pub total_size: u64,
277}
278
279#[derive(Debug)]
280pub struct FileMetadata {
281 pub name: String,
282 pub path: PathBuf,
283 pub size: u64,
284 pub modified: Option<std::time::SystemTime>,
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290 use tempfile::TempDir;
291
292 #[test]
293 fn test_prepare_output_directory() {
294 let temp_dir = TempDir::new().unwrap();
295 let output_path = temp_dir.path().join("output");
296
297 let manager = OutputManager::new(&output_path);
298 manager.prepare_output_directory().unwrap();
299
300 assert!(output_path.exists());
301 assert!(output_path.is_dir());
302 }
303
304 #[test]
305 fn test_write_file() {
306 let temp_dir = TempDir::new().unwrap();
307 let manager = OutputManager::new(temp_dir.path());
308
309 manager.prepare_output_directory().unwrap();
310
311 let content = "export interface Test { name: string; }";
312 let file_path = manager.write_file("test.ts", content).unwrap();
313
314 assert!(file_path.exists());
315 let written_content = fs::read_to_string(&file_path).unwrap();
316 assert_eq!(written_content, content);
317 }
318
319 #[test]
320 fn test_is_generated_file() {
321 let temp_dir = TempDir::new().unwrap();
322 let manager = OutputManager::new(temp_dir.path());
323
324 assert!(manager.is_generated_file("types.ts"));
326 assert!(manager.is_generated_file("commands.ts"));
327 assert!(manager.is_generated_file("schemas.ts"));
328 assert!(manager.is_generated_file("index.ts"));
329
330 assert!(manager.is_generated_file("generated_models.ts"));
332 assert!(manager.is_generated_file("api_generated.ts"));
333
334 assert!(!manager.is_generated_file("user_code.ts"));
336 assert!(!manager.is_generated_file("main.ts"));
337 assert!(!manager.is_generated_file("app.vue"));
338 }
339
340 #[test]
341 fn test_cleanup_old_files() {
342 let temp_dir = TempDir::new().unwrap();
343 let manager = OutputManager::new(temp_dir.path());
344
345 manager.prepare_output_directory().unwrap();
346
347 fs::write(temp_dir.path().join("types.ts"), "old content").unwrap();
349 fs::write(temp_dir.path().join("commands.ts"), "old content").unwrap();
350 fs::write(temp_dir.path().join("user_file.ts"), "user content").unwrap();
351
352 let current_files = vec!["types.ts".to_string()];
354
355 let cleaned = manager.cleanup_old_files(¤t_files).unwrap();
356
357 assert_eq!(cleaned.len(), 1);
359 assert!(cleaned.contains(&"commands.ts".to_string()));
360
361 assert!(temp_dir.path().join("types.ts").exists());
362 assert!(!temp_dir.path().join("commands.ts").exists());
363 assert!(temp_dir.path().join("user_file.ts").exists());
364 }
365
366 #[test]
367 fn test_verify_output() {
368 let temp_dir = TempDir::new().unwrap();
369 let manager = OutputManager::new(temp_dir.path());
370
371 manager.prepare_output_directory().unwrap();
372
373 fs::write(temp_dir.path().join("types.ts"), "content").unwrap();
375
376 let expected = vec!["types.ts".to_string(), "commands.ts".to_string()];
377 let missing = manager.verify_output(&expected).unwrap();
378
379 assert_eq!(missing.len(), 1);
380 assert!(missing.contains(&"commands.ts".to_string()));
381 }
382
383 #[test]
384 fn test_generation_metadata() {
385 let temp_dir = TempDir::new().unwrap();
386 let manager = OutputManager::new(temp_dir.path());
387
388 manager.prepare_output_directory().unwrap();
389
390 fs::write(temp_dir.path().join("types.ts"), "interface Test {}").unwrap();
392 fs::write(
393 temp_dir.path().join("commands.ts"),
394 "export function test() {}",
395 )
396 .unwrap();
397
398 let metadata = manager.get_generation_metadata().unwrap();
399
400 assert_eq!(metadata.files.len(), 2);
401 assert!(metadata.total_size > 0);
402 assert_eq!(metadata.output_directory, temp_dir.path());
403
404 let types_file = metadata
406 .files
407 .iter()
408 .find(|f| f.name == "types.ts")
409 .unwrap();
410 assert_eq!(types_file.size, 17); }
412}