Skip to main content

ferrous_forge/templates/repository/
mod.rs

1//! Template Repository System - Community template sharing and management
2//!
3//! This module provides functionality for:
4//! - Fetching templates from GitHub repositories
5//! - Validating template structure before installation
6//! - Caching templates locally in ~/.config/ferrous-forge/templates/
7//! - Template versioning and updates
8//!
9//! @task T021
10//! @epic T014
11
12use crate::error::{Error, Result};
13use crate::templates::manifest::TemplateManifest;
14use serde::{Deserialize, Serialize};
15use std::collections::HashMap;
16use std::path::{Path, PathBuf};
17
18/// GitHub API client for fetching templates
19pub mod github;
20
21/// Template metadata stored in cache
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct CachedTemplate {
24    /// Template name
25    pub name: String,
26    /// Source repository (e.g., "gh:user/repo")
27    pub source: String,
28    /// Template version
29    pub version: String,
30    /// When the template was fetched
31    pub fetched_at: chrono::DateTime<chrono::Utc>,
32    /// When the template was last updated
33    pub updated_at: chrono::DateTime<chrono::Utc>,
34    /// Path to cached template directory
35    pub cache_path: PathBuf,
36    /// Template manifest
37    pub manifest: TemplateManifest,
38}
39
40/// Template repository manager
41pub struct TemplateRepository {
42    /// Cache directory for templates
43    cache_dir: PathBuf,
44    /// Index of cached templates
45    index: TemplateIndex,
46}
47
48/// Index of cached templates
49#[derive(Debug, Clone, Serialize, Deserialize, Default)]
50pub struct TemplateIndex {
51    /// Map of template name to cached template info
52    pub templates: HashMap<String, CachedTemplate>,
53    /// Index version for migrations
54    pub version: u32,
55}
56
57impl TemplateRepository {
58    /// Create a new template repository manager
59    ///
60    /// # Errors
61    ///
62    /// Returns an error if the cache directory cannot be created or accessed.
63    pub fn new() -> Result<Self> {
64        let cache_dir = Self::cache_dir()?;
65        std::fs::create_dir_all(&cache_dir).map_err(|e| {
66            Error::template(format!("Failed to create template cache directory: {e}"))
67        })?;
68
69        let index = Self::load_index(&cache_dir)?;
70
71        Ok(Self { cache_dir, index })
72    }
73
74    /// Get the cache directory path
75    fn cache_dir() -> Result<PathBuf> {
76        let config_dir =
77            dirs::config_dir().ok_or_else(|| Error::config("Could not find config directory"))?;
78        Ok(config_dir.join("ferrous-forge").join("templates"))
79    }
80
81    /// Load the template index from disk
82    fn load_index(cache_dir: &Path) -> Result<TemplateIndex> {
83        let index_path = cache_dir.join("index.json");
84        if index_path.exists() {
85            let content = std::fs::read_to_string(&index_path)
86                .map_err(|e| Error::template(format!("Failed to read template index: {e}")))?;
87            let index: TemplateIndex = serde_json::from_str(&content)
88                .map_err(|e| Error::template(format!("Failed to parse template index: {e}")))?;
89            Ok(index)
90        } else {
91            Ok(TemplateIndex {
92                version: 1,
93                templates: HashMap::new(),
94            })
95        }
96    }
97
98    /// Save the template index to disk
99    fn save_index(&self) -> Result<()> {
100        let index_path = self.cache_dir.join("index.json");
101        let content = serde_json::to_string_pretty(&self.index)
102            .map_err(|e| Error::template(format!("Failed to serialize template index: {e}")))?;
103        std::fs::write(&index_path, content)
104            .map_err(|e| Error::template(format!("Failed to write template index: {e}")))?;
105        Ok(())
106    }
107
108    /// List all cached templates
109    pub fn list_cached(&self) -> Vec<&CachedTemplate> {
110        self.index.templates.values().collect()
111    }
112
113    /// Get a cached template by name
114    pub fn get_cached(&self, name: &str) -> Option<&CachedTemplate> {
115        self.index.templates.get(name)
116    }
117
118    /// Check if a template is cached
119    pub fn is_cached(&self, name: &str) -> bool {
120        self.index.templates.contains_key(name)
121    }
122
123    /// Add a template to the cache
124    ///
125    /// # Errors
126    ///
127    /// Returns an error if the cache index cannot be saved.
128    pub fn add_to_cache(&mut self, template: CachedTemplate) -> Result<()> {
129        self.index.templates.insert(template.name.clone(), template);
130        self.save_index()
131    }
132
133    /// Remove a template from cache
134    ///
135    /// # Errors
136    ///
137    /// Returns an error if the cached directory or index cannot be updated.
138    pub fn remove_from_cache(&mut self, name: &str) -> Result<()> {
139        if let Some(template) = self.index.templates.remove(name) {
140            // Remove the cached directory
141            if template.cache_path.exists() {
142                std::fs::remove_dir_all(&template.cache_path).map_err(|e| {
143                    Error::template(format!("Failed to remove cached template: {e}"))
144                })?;
145            }
146        }
147        self.save_index()
148    }
149
150    /// Get cache directory path
151    pub fn cache_directory(&self) -> &Path {
152        &self.cache_dir
153    }
154
155    /// Get the path where a template should be cached
156    pub fn template_cache_path(&self, name: &str) -> PathBuf {
157        self.cache_dir.join(name)
158    }
159}
160
161impl CachedTemplate {
162    /// Check if the template needs an update (older than 24 hours)
163    pub fn needs_update(&self) -> bool {
164        let age = chrono::Utc::now() - self.updated_at;
165        age.num_hours() >= 24
166    }
167}