mammoth_cli/
manager.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3use anyhow::{Context, Result};
4use indicatif::{ProgressBar, ProgressStyle};
5use crate::config::{Config, Repo, Template};
6use crate::utils::copy_directory;
7use colored::*;
8use dialoguer::Confirm;
9use serde_json;
10
11pub struct TemplateManager {
12    pub config: Config,
13    cache_dir: PathBuf,
14}
15
16impl TemplateManager {
17    pub fn new() -> Result<Self> {
18        let config_path = Self::get_config_path()?;
19        let config = if config_path.exists() {
20            let content = fs::read_to_string(&config_path).context("Failed to read config file")?;
21            serde_json::from_str(&content).context("Failed to parse config file")?
22        } else {
23            Config {
24                repos: vec![],
25                templates: vec![],
26            }
27        };
28        
29        let cache_dir = Self::get_cache_dir()?;
30        fs::create_dir_all(&cache_dir).context("Failed to create cache directory")?;
31        
32        Ok(Self { config, cache_dir })
33    }
34    
35    fn get_config_path() -> Result<PathBuf> {
36        let config_dir = dirs::config_dir()
37            .unwrap_or_else(|| PathBuf::from(".config"))
38            .join("mammoth-cli");
39        fs::create_dir_all(&config_dir).context("Failed to create config directory")?;
40        Ok(config_dir.join("templates.json"))
41    }
42    
43    fn get_cache_dir() -> Result<PathBuf> {
44        let cache_dir = dirs::cache_dir()
45            .unwrap_or_else(|| PathBuf::from(".cache"))
46            .join("mammoth-cli")
47            .join("templates");
48        Ok(cache_dir)
49    }
50    
51    pub fn save_config(&self) -> Result<()> {
52        let config_path = Self::get_config_path()?;
53        let content =
54            serde_json::to_string_pretty(&self.config).context("Failed to serialize config")?;
55        fs::write(config_path, content).context("Failed to write config file")?;
56        Ok(())
57    }
58    
59    pub fn get_template_by_id(&self, id: &str) -> Option<&Template> {
60        self.config.templates.iter().find(|t| t.id == id)
61    }
62    
63    pub fn get_repo_by_name(&self, name: &str) -> Option<&Repo> {
64        self.config.repos.iter().find(|r| r.name == name)
65    }
66    
67    fn get_template_cache_path(&self, template: &Template) -> PathBuf {
68        self.cache_dir.join(&template.repo).join(&template.id)
69    }
70    
71    pub async fn download_template(&self, template: &Template, force: bool) -> Result<()> {
72        let repo = self
73            .get_repo_by_name(&template.repo)
74            .ok_or_else(|| anyhow::anyhow!("Repository '{}' not found", template.repo))?;
75        
76        let cache_path = self.get_template_cache_path(template);
77        
78        if cache_path.exists() && !force {
79            println!("✨ Template '{}' already cached", template.id);
80            return Ok(());
81        }
82        
83        println!("🚀 Downloading template '{}'...", template.id);
84        
85        // Create progress bar
86        let pb = ProgressBar::new(100);
87        pb.set_style(
88            ProgressStyle::default_bar()
89                .template(
90                    "{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} {msg}",
91                )
92                .unwrap()
93                .progress_chars("#>-"),
94        );
95        
96        // Create temporary directory for sparse clone
97        let temp_dir = self.cache_dir.join(format!("temp_{}", repo.name));
98        
99        // 确保清理旧的临时目录
100        self.cleanup_temp_dir(&temp_dir)?;
101        fs::create_dir_all(&temp_dir).context("Failed to create temp dir")?;
102        
103        // 使用 Result 来确保清理操作
104        let result = self.download_template_internal(template, repo, &temp_dir, &cache_path, &pb).await;
105        
106        // 无论成功还是失败,都尝试清理临时目录
107        if let Err(ref e) = result {
108            eprintln!("❌ Download failed: {}", e);
109        }
110        
111        // 清理临时目录
112        self.cleanup_temp_dir(&temp_dir)?;
113        
114        result
115    }
116    
117    async fn download_template_internal(
118        &self,
119        template: &Template,
120        repo: &Repo,
121        temp_dir: &Path,
122        cache_path: &Path,
123        pb: &ProgressBar,
124    ) -> Result<()> {
125        pb.set_message("Preparing sparse checkout...");
126        pb.inc(20);
127        
128        // Clone repository with sparse checkout and timeout
129        pb.set_message("Cloning repository...");
130        pb.inc(30);
131        
132        let clone_result = tokio::time::timeout(
133            std::time::Duration::from_secs(300), // 5分钟超时
134            tokio::process::Command::new("git")
135                .args([
136                    "clone",
137                    "--no-checkout",
138                    "--filter=blob:none",
139                    "--sparse",
140                    &repo.url,
141                    &temp_dir.to_string_lossy(),
142                ])
143                .status(),
144        )
145        .await;
146        
147        let status = match clone_result {
148            Ok(Ok(status)) => status,
149            Ok(Err(e)) => anyhow::bail!("Failed to clone repository: {}", e),
150            Err(_) => anyhow::bail!("Git clone timed out after 5 minutes"),
151        };
152        
153        if !status.success() {
154            anyhow::bail!("Failed to clone repository: {}", repo.url);
155        }
156        
157        // Set sparse checkout directory
158        pb.set_message("Configuring sparse checkout...");
159        pb.inc(40);
160        
161        let sparse_result = tokio::time::timeout(
162            std::time::Duration::from_secs(60), // 1分钟超时
163            tokio::process::Command::new("git")
164                .args(["sparse-checkout", "set", &template.path])
165                .current_dir(temp_dir)
166                .status(),
167        )
168        .await;
169        
170        let status = match sparse_result {
171            Ok(Ok(status)) => status,
172            Ok(Err(e)) => anyhow::bail!("Failed to set sparse checkout: {}", e),
173            Err(_) => anyhow::bail!("Sparse checkout timed out"),
174        };
175        
176        if !status.success() {
177            anyhow::bail!("Failed to set sparse checkout for path: {}", template.path);
178        }
179        
180        // Checkout the specific branch
181        pb.set_message("Checking out files...");
182        pb.inc(50);
183        
184        let checkout_result = tokio::time::timeout(
185            std::time::Duration::from_secs(120), // 2分钟超时
186            tokio::process::Command::new("git")
187                .args(["checkout", &repo.branch])
188                .current_dir(temp_dir)
189                .status(),
190        )
191        .await;
192        
193        let status = match checkout_result {
194            Ok(Ok(status)) => status,
195            Ok(Err(e)) => anyhow::bail!("Failed to checkout branch: {}", e),
196            Err(_) => anyhow::bail!("Git checkout timed out"),
197        };
198        
199        if !status.success() {
200            anyhow::bail!("Failed to checkout branch: {}", repo.branch);
201        }
202        
203        // Create target directory
204        fs::create_dir_all(cache_path.parent().unwrap())
205            .context("Failed to create repo cache parent dir")?;
206        
207        // Move template files to cache location
208        pb.set_message("Copying template files...");
209        pb.inc(60);
210        
211        let template_source = temp_dir.join(&template.path);
212        if !template_source.exists() {
213            anyhow::bail!("Template path '{}' not found in repository", template.path);
214        }
215        
216        // 安全地清理和复制文件
217        self.safe_copy_template_files(&template_source, cache_path)?;
218        
219        pb.finish_with_message("Template downloaded successfully!");
220        println!(
221            "✅ Template '{}' downloaded to: {}",
222            template.id,
223            cache_path.display()
224        );
225        
226        Ok(())
227    }
228    
229    fn cleanup_temp_dir(&self, temp_dir: &Path) -> Result<()> {
230        if temp_dir.exists() {
231            // 在 Windows 上,可能需要多次尝试删除
232            for attempt in 1..=3 {
233                match fs::remove_dir_all(temp_dir) {
234                    Ok(_) => {
235                        if attempt > 1 {
236                            println!("✅ Temp directory cleaned on attempt {}", attempt);
237                        }
238                        return Ok(());
239                    }
240                    Err(e) => {
241                        if attempt == 3 {
242                            eprintln!("⚠️  Warning: Failed to remove temp dir after 3 attempts: {}", e);
243                            return Err(e.into());
244                        }
245                        // 等待一小段时间再重试
246                        std::thread::sleep(std::time::Duration::from_millis(500));
247                    }
248                }
249            }
250        }
251        Ok(())
252    }
253    
254    fn safe_copy_template_files(&self, source: &Path, dest: &Path) -> Result<()> {
255        // 如果目标目录存在,先尝试删除
256        if dest.exists() {
257            // 在 Windows 上,可能需要多次尝试
258            for attempt in 1..=3 {
259                match fs::remove_dir_all(dest) {
260                    Ok(_) => break,
261                    Err(e) => {
262                        if attempt == 3 {
263                            anyhow::bail!("Failed to remove old cache after 3 attempts: {}", e);
264                        }
265                        std::thread::sleep(std::time::Duration::from_millis(500));
266                    }
267                }
268            }
269        }
270        
271        // 复制文件
272        copy_directory(source, dest).context("Failed to copy template files")?;
273        
274        Ok(())
275    }
276    
277    pub async fn download_all_templates(&self, force: bool) -> Result<()> {
278        println!("🚀 Downloading all templates...");
279        
280        for template in &self.config.templates {
281            match self.download_template(template, force).await {
282                Ok(_) => {}
283                Err(e) => {
284                    println!("❌ Failed to download template '{}': {}", template.id, e);
285                }
286            }
287        }
288        
289        println!("🎉 All templates downloaded!");
290        Ok(())
291    }
292    
293    pub fn list_templates(&self, verbose: bool) {
294        if verbose {
295            println!("{}", "📋 Available Templates".bold().blue());
296        } else {
297            println!("{}", "📋 Template List".bold().blue());
298        }
299        println!();
300        
301        if self.config.templates.is_empty() {
302            println!("No templates available. Add templates first.");
303            return;
304        }
305        
306        for template in &self.config.templates {
307            let cache_path = self.get_template_cache_path(template);
308            let status = if cache_path.exists() {
309                "✅".green()
310            } else {
311                "❌".red()
312            };
313            
314            if verbose {
315                // 全信息显示模式
316                println!("{} {} - {}", status, template.id.bold(), template.name);
317                println!("   Description: {}", template.description);
318                println!("   Language: {}", template.language);
319                println!("   Repository: {}", template.repo);
320                println!("   Path: {}", template.path);
321                println!("   Tags: {}", template.tags.join(", "));
322                println!();
323            } else {
324                // 简要信息显示模式
325                println!(
326                    "{} {} - {} ({})",
327                    status,
328                    template.id.bold(),
329                    template.name,
330                    template.language
331                );
332            }
333        }
334        
335        if !verbose {
336            println!();
337            println!("💡 Use --verbose to see detailed information");
338        }
339    }
340    
341    pub fn add_template(
342        &mut self,
343        id: String,
344        name: String,
345        repo: String,
346        path: String,
347        description: String,
348        language: String,
349        tags: Option<String>,
350    ) -> Result<()> {
351        // Verify repository exists
352        if !self.config.repos.iter().any(|r| r.name == repo) {
353            anyhow::bail!(
354                "Repository '{}' not found. Add it first with 'repo add'",
355                repo
356            );
357        }
358        
359        // Check if template ID already exists
360        if self.config.templates.iter().any(|t| t.id == id) {
361            anyhow::bail!("Template with ID '{}' already exists", id);
362        }
363        
364        // Parse tags
365        let tags_vec = if let Some(tags_str) = tags {
366            tags_str
367                .split(',')
368                .map(|s| s.trim().to_string())
369                .filter(|s| !s.is_empty())
370                .collect()
371        } else {
372            vec![]
373        };
374        
375        let template = Template {
376            id,
377            name,
378            repo,
379            path,
380            description,
381            language,
382            tags: tags_vec,
383        };
384        
385        self.config.templates.push(template);
386        self.save_config()?;
387        
388        println!("🎉 Template added successfully!");
389        Ok(())
390    }
391    
392    pub fn remove_template(&mut self, id: &str) -> Result<()> {
393        let index = self.config.templates.iter().position(|t| t.id == id);
394        
395        if let Some(index) = index {
396            self.config.templates.remove(index);
397            self.save_config()?;
398            println!("🗑️  Template '{}' removed successfully!", id);
399        } else {
400            anyhow::bail!("Template '{}' not found", id);
401        }
402        
403        Ok(())
404    }
405    
406    pub fn add_repo(&mut self, name: String, url: String, branch: String) -> Result<()> {
407        // Check if repository already exists
408        if self.config.repos.iter().any(|r| r.name == name) {
409            anyhow::bail!("Repository '{}' already exists", name);
410        }
411        
412        let repo = Repo { name, url, branch };
413        self.config.repos.push(repo);
414        self.save_config()?;
415        
416        println!("🎉 Repository added successfully!");
417        Ok(())
418    }
419    
420    pub fn remove_repo(&mut self, name: &str) -> Result<()> {
421        // Check if any templates use this repository
422        if self.config.templates.iter().any(|t| t.repo == name) {
423            anyhow::bail!(
424                "Cannot remove repository '{}' - it is used by templates",
425                name
426            );
427        }
428        
429        let index = self.config.repos.iter().position(|r| r.name == name);
430        
431        if let Some(index) = index {
432            self.config.repos.remove(index);
433            self.save_config()?;
434            println!("🗑️  Repository '{}' removed successfully!", name);
435        } else {
436            anyhow::bail!("Repository '{}' not found", name);
437        }
438        
439        Ok(())
440    }
441    
442    pub fn copy_template_files(&self, template: &Template, project_path: &Path) -> Result<()> {
443        let cache_path = self.get_template_cache_path(template);
444        
445        if !cache_path.exists() {
446            anyhow::bail!(
447                "Template '{}' not cached. Run 'template download {}' first",
448                template.id,
449                template.id
450            );
451        }
452        
453        copy_directory(&cache_path, project_path)?;
454        Ok(())
455    }
456    
457    pub fn list_repos(&self) {
458        println!("{}", "📦 Configured Template Repositories".bold().blue());
459        println!();
460        if self.config.repos.is_empty() {
461            println!("No repositories configured. Add repositories first.");
462            return;
463        }
464        for repo in &self.config.repos {
465            println!("{} - {}", repo.name.bold(), repo.url);
466            println!("   🪐Branch: {}", repo.branch);
467            println!();
468        }
469    }
470    
471    pub fn export_config(&self, output: &str, include_cache: bool) -> Result<()> {
472        println!("📤 Exporting configuration to: {}", output);
473        
474        let export_config = Config {
475            repos: self.config.repos.clone(),
476            templates: self.config.templates.clone(),
477        };
478        
479        // 如果包含缓存信息,添加缓存状态
480        if include_cache {
481            println!("📦 Including cache information...");
482            // 这里可以添加缓存相关的元数据
483        }
484        
485        let content = serde_json::to_string_pretty(&export_config)
486            .context("Failed to serialize configuration")?;
487        
488        fs::write(output, content)
489            .with_context(|| format!("Failed to write configuration to: {}", output))?;
490        
491        println!("✅ Configuration exported successfully!");
492        println!(
493            "📊 Exported {} repositories and {} templates",
494            export_config.repos.len(),
495            export_config.templates.len()
496        );
497        
498        Ok(())
499    }
500    
501    pub fn import_config(&mut self, file: &str, mode: &str, skip_validation: bool) -> Result<()> {
502        println!("📥 Importing configuration from: {}", file);
503        
504        let config_content = fs::read_to_string(file)
505            .with_context(|| format!("Failed to read configuration file: {}", file))?;
506        
507        let import_config: Config =
508            serde_json::from_str(&config_content).context("Failed to parse configuration file")?;
509        
510        if !skip_validation {
511            self.validate_import_config(&import_config)?;
512        }
513        
514        match mode.to_lowercase().as_str() {
515            "merge" => {
516                println!("🔄 Merging configuration...");
517                self.merge_config(import_config)?;
518            }
519            "overwrite" => {
520                println!("⚠️  Overwriting configuration...");
521                self.config = import_config;
522            }
523            _ => {
524                anyhow::bail!("Invalid import mode: {}. Use 'merge' or 'overwrite'", mode);
525            }
526        }
527        
528        self.save_config()?;
529        
530        println!("✅ Configuration imported successfully!");
531        println!(
532            "📊 Current configuration: {} repositories and {} templates",
533            self.config.repos.len(),
534            self.config.templates.len()
535        );
536        
537        Ok(())
538    }
539    
540    pub fn validate_config_file(&self, file: &str) -> Result<()> {
541        println!("🔍 Validating configuration file: {}", file);
542        
543        let config_content = fs::read_to_string(file)
544            .with_context(|| format!("Failed to read configuration file: {}", file))?;
545        
546        let config: Config =
547            serde_json::from_str(&config_content).context("Failed to parse configuration file")?;
548        
549        self.validate_import_config(&config)?;
550        
551        println!("✅ Configuration file is valid!");
552        println!(
553            "📊 Contains {} repositories and {} templates",
554            config.repos.len(),
555            config.templates.len()
556        );
557        
558        Ok(())
559    }
560    
561    fn validate_import_config(&self, import_config: &Config) -> Result<()> {
562        let mut validation_errors = Vec::new();
563        let mut validation_warnings = Vec::new();
564        
565        // 验证仓库配置
566        for repo in &import_config.repos {
567            if repo.name.is_empty() {
568                validation_errors.push("Repository name cannot be empty".to_string());
569            }
570            if repo.url.is_empty() {
571                validation_errors.push(format!("Repository '{}' URL cannot be empty", repo.name));
572            }
573            if repo.branch.is_empty() {
574                validation_errors
575                    .push(format!("Repository '{}' branch cannot be empty", repo.name));
576            }
577        }
578        
579        // 验证模板配置
580        for template in &import_config.templates {
581            if template.id.is_empty() {
582                validation_errors.push("Template ID cannot be empty".to_string());
583            }
584            if template.name.is_empty() {
585                validation_errors.push(format!("Template '{}' name cannot be empty", template.id));
586            }
587            if template.repo.is_empty() {
588                validation_errors.push(format!(
589                    "Template '{}' repository cannot be empty",
590                    template.id
591                ));
592            }
593            if template.path.is_empty() {
594                validation_errors.push(format!("Template '{}' path cannot be empty", template.id));
595            }
596            
597            // 检查模板引用的仓库是否存在
598            if !import_config.repos.iter().any(|r| r.name == template.repo) {
599                validation_warnings.push(format!(
600                    "Template '{}' references non-existent repository '{}'",
601                    template.id, template.repo
602                ));
603            }
604        }
605        
606        // 报告错误和警告
607        if !validation_errors.is_empty() {
608            println!("❌ Validation errors:");
609            for error in validation_errors {
610                println!("  {}", error);
611            }
612            anyhow::bail!("Configuration validation failed");
613        }
614        
615        if !validation_warnings.is_empty() {
616            println!("⚠️  Validation warnings:");
617            for warning in validation_warnings {
618                println!("  {}", warning);
619            }
620        }
621        
622        Ok(())
623    }
624    
625    fn merge_config(&mut self, import_config: Config) -> Result<()> {
626        let mut merged_repos = 0;
627        let mut merged_templates = 0;
628        
629        // 合并仓库
630        for import_repo in import_config.repos {
631            if let Some(existing_repo) = self
632                .config
633                .repos
634                .iter_mut()
635                .find(|r| r.name == import_repo.name)
636            {
637                // 更新现有仓库
638                existing_repo.url = import_repo.url;
639                existing_repo.branch = import_repo.branch;
640                merged_repos += 1;
641            } else {
642                // 添加新仓库
643                self.config.repos.push(import_repo);
644                merged_repos += 1;
645            }
646        }
647        
648        // 合并模板
649        for import_template in import_config.templates {
650            if let Some(existing_template) = self
651                .config
652                .templates
653                .iter_mut()
654                .find(|t| t.id == import_template.id)
655            {
656                // 更新现有模板
657                *existing_template = import_template;
658                merged_templates += 1;
659            } else {
660                // 添加新模板
661                self.config.templates.push(import_template);
662                merged_templates += 1;
663            }
664        }
665        
666        println!(
667            "📊 Merged {} repositories and {} templates",
668            merged_repos, merged_templates
669        );
670        
671        Ok(())
672    }
673    
674    pub fn clean_templates(&mut self, all: bool, force: bool) -> Result<()> {
675        if !force {
676            let message = if all {
677                "⚠️  This will remove ALL templates, cache, and configuration. Are you sure?"
678            } else {
679                "⚠️  This will remove ALL cached template files. Are you sure?"
680            };
681            
682            let confirm = Confirm::new()
683                .with_prompt(message)
684                .default(false)
685                .interact()?;
686            
687            if !confirm {
688                println!("❌ Clean operation cancelled");
689                return Ok(());
690            }
691        }
692        
693        println!("🧹 Cleaning templates...");
694        
695        // 清理缓存目录
696        if self.cache_dir.exists() {
697            match fs::remove_dir_all(&self.cache_dir) {
698                Ok(_) => println!("✅ Cache directory cleaned"),
699                Err(e) => println!("⚠️  Failed to clean cache directory: {}", e),
700            }
701        }
702        
703        // 重新创建缓存目录
704        fs::create_dir_all(&self.cache_dir).context("Failed to recreate cache directory")?;
705        
706        if all {
707            // 清理配置文件
708            let config_path = Self::get_config_path()?;
709            if config_path.exists() {
710                match fs::remove_file(&config_path) {
711                    Ok(_) => println!("✅ Configuration file removed"),
712                    Err(e) => println!("⚠️  Failed to remove configuration file: {}", e),
713                }
714            }
715            
716            // 重置配置
717            self.config = Config {
718                repos: vec![],
719                templates: vec![],
720            };
721        }
722        
723        println!("🎉 Clean operation completed!");
724        if all {
725            println!("📝 Configuration has been reset to empty state");
726        } else {
727            println!("💾 Configuration preserved, only cache was cleaned");
728        }
729        
730        Ok(())
731    }
732    
733    pub fn show_info(&self, json: bool) -> Result<()> {
734        if json {
735            // 以JSON格式显示配置
736            let config_json = serde_json::to_string_pretty(&self.config)
737                .context("Failed to serialize configuration")?;
738            println!("{}", config_json);
739        } else {
740            // 以友好格式显示配置信息
741            println!("{}", "📋 Current Configuration".bold().blue());
742            println!();
743            
744            // 显示仓库信息
745            println!("{}", "📦 Repositories".bold().yellow());
746            if self.config.repos.is_empty() {
747                println!("  No repositories configured");
748            } else {
749                for repo in &self.config.repos {
750                    println!("  {} - {}", repo.name.bold(), repo.url);
751                    println!("    Branch: {}", repo.branch);
752                }
753            }
754            println!();
755            
756            // 显示模板信息
757            println!("{}", "🎨 Templates".bold().yellow());
758            if self.config.templates.is_empty() {
759                println!("  No templates configured");
760            } else {
761                for template in &self.config.templates {
762                    let cache_path = self.get_template_cache_path(template);
763                    let status = if cache_path.exists() {
764                        "✅".green()
765                    } else {
766                        "❌".red()
767                    };
768                    
769                    println!("  {} {} - {}", status, template.id.bold(), template.name);
770                    println!("    Description: {}", template.description);
771                    println!("    Language: {}", template.language);
772                    println!("    Repository: {}", template.repo);
773                    println!("    Path: {}", template.path);
774                    println!("    Tags: {}", template.tags.join(", "));
775                    println!();
776                }
777            }
778            
779            // 显示统计信息
780            println!("{}", "📊 Statistics".bold().yellow());
781            println!("  Repositories: {}", self.config.repos.len());
782            println!("  Templates: {}", self.config.templates.len());
783            
784            // 显示缓存状态
785            let cached_count = self
786                .config
787                .templates
788                .iter()
789                .filter(|t| self.get_template_cache_path(t).exists())
790                .count();
791            println!(
792                "  Cached templates: {}/{}",
793                cached_count,
794                self.config.templates.len()
795            );
796            
797            // 显示配置路径
798            println!();
799            println!("{}", "📁 Paths".bold().yellow());
800            match Self::get_config_path() {
801                Ok(path) => println!("  Config: {}", path.display()),
802                Err(_) => println!("  Config: Unable to determine path"),
803            }
804            println!("  Cache: {}", self.cache_dir.display());
805        }
806        
807        Ok(())
808    }
809}