Skip to main content

mai_cli/core/
config.rs

1use crate::core::pack::{Pack, PackType};
2use crate::core::version::Version;
3use crate::error::{Error, Result};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::path::PathBuf;
7
8#[derive(Debug, Clone, Serialize, Deserialize, Default)]
9pub struct ToolConfig {
10    #[serde(default)]
11    pub installed_packs: Vec<Pack>,
12    #[serde(skip)]
13    #[allow(dead_code)]
14    pub config_path: Option<PathBuf>,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize, Default)]
18pub struct MaiConfig {
19    #[serde(default)]
20    pub active_tool: Option<String>,
21    #[serde(default)]
22    pub tools: HashMap<String, ToolConfig>,
23}
24
25impl MaiConfig {
26    pub fn new() -> Self {
27        Self::default()
28    }
29
30    #[allow(dead_code)]
31    pub fn get_tool(&self, name: &str) -> Option<&ToolConfig> {
32        self.tools.get(name)
33    }
34
35    #[allow(dead_code)]
36    pub fn get_tool_mut(&mut self, name: &str) -> Option<&mut ToolConfig> {
37        self.tools.get_mut(name)
38    }
39
40    pub fn set_active_tool(&mut self, tool: impl Into<String>) {
41        self.active_tool = Some(tool.into());
42    }
43
44    pub fn active_tool(&self) -> Option<&str> {
45        self.active_tool.as_deref()
46    }
47}
48
49// ============================================================================
50// Project Manifest (mai.toml)
51// ============================================================================
52
53/// Dependency specification in mai.toml
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct DependencySpec {
56    /// Pack name
57    pub name: String,
58    /// Pack type (skill/command/mcp)
59    #[serde(rename = "type")]
60    pub pack_type: PackType,
61    /// Version requirement (e.g., "1.0.0", "^1.0", "~1.2", ">=1.0.0, <2.0.0")
62    #[serde(default = "default_version")]
63    pub version: String,
64    /// Optional tool constraint
65    #[serde(default)]
66    pub tool: Option<String>,
67}
68
69fn default_version() -> String {
70    "latest".to_string()
71}
72
73impl Default for DependencySpec {
74    fn default() -> Self {
75        Self {
76            name: String::new(),
77            pack_type: PackType::Skill,
78            version: default_version(),
79            tool: None,
80        }
81    }
82}
83
84/// Project manifest structure (mai.toml)
85#[derive(Debug, Clone, Serialize, Deserialize, Default)]
86pub struct ProjectManifest {
87    /// Project metadata
88    #[serde(default)]
89    pub project: ProjectInfo,
90    /// Dependencies (flat array with tool field)
91    #[serde(default)]
92    pub dependencies: Vec<DependencySpec>,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize, Default)]
96pub struct ProjectInfo {
97    #[serde(default)]
98    pub name: String,
99    #[serde(default)]
100    pub description: Option<String>,
101    #[serde(default)]
102    pub version: Option<String>,
103}
104
105impl ProjectManifest {
106    #[allow(dead_code)]
107    pub fn new() -> Self {
108        Self::default()
109    }
110
111    pub fn load(path: &std::path::Path) -> Result<Self> {
112        let content = std::fs::read_to_string(path).map_err(Error::from)?;
113        toml::from_str(&content).map_err(Error::from)
114    }
115
116    pub fn save(&self, path: &std::path::Path) -> Result<()> {
117        let content = toml::to_string_pretty(self).map_err(Error::from)?;
118        std::fs::write(path, content).map_err(Error::from)?;
119        Ok(())
120    }
121
122    #[allow(dead_code)]
123    pub fn get_dependencies(&self, tool: Option<&str>) -> Vec<&DependencySpec> {
124        if let Some(tool) = tool {
125            self.dependencies
126                .iter()
127                .filter(|d| d.tool.as_deref() == Some(tool))
128                .collect()
129        } else {
130            self.dependencies.iter().collect()
131        }
132    }
133}
134
135// ============================================================================
136// Lock File (mai.lock)
137// ============================================================================
138
139/// Locked dependency with exact version and hash
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct LockedDependency {
142    /// Pack name
143    pub name: String,
144    /// Pack type
145    #[serde(rename = "type")]
146    pub pack_type: PackType,
147    /// Exact resolved version
148    pub version: Version,
149    /// Content hash for integrity verification
150    pub hash: String,
151    /// Source URL (if available)
152    #[serde(default)]
153    pub source: Option<String>,
154    /// Tool constraint
155    #[serde(default)]
156    pub tool: Option<String>,
157}
158
159impl LockedDependency {
160    pub fn new(
161        name: impl Into<String>,
162        pack_type: PackType,
163        version: Version,
164        hash: impl Into<String>,
165    ) -> Self {
166        Self {
167            name: name.into(),
168            pack_type,
169            version,
170            hash: hash.into(),
171            source: None,
172            tool: None,
173        }
174    }
175
176    pub fn with_tool(mut self, tool: impl Into<String>) -> Self {
177        self.tool = Some(tool.into());
178        self
179    }
180
181    #[allow(dead_code)]
182    pub fn with_source(mut self, source: impl Into<String>) -> Self {
183        self.source = Some(source.into());
184        self
185    }
186
187    /// Compute SHA256 hash of pack content
188    pub fn compute_hash(content: &[u8]) -> String {
189        use std::collections::hash_map::DefaultHasher;
190        use std::hash::Hasher;
191
192        let mut hasher = DefaultHasher::new();
193        hasher.write(content);
194        format!("{:016x}", hasher.finish())
195    }
196
197    /// Verify integrity of content against stored hash
198    pub fn verify_hash(&self, content: &[u8]) -> bool {
199        let computed = Self::compute_hash(content);
200        computed == self.hash
201    }
202}
203
204/// Lock file structure (mai.lock)
205#[derive(Debug, Clone, Serialize, Deserialize, Default)]
206pub struct LockFile {
207    /// Lock file format version
208    pub version: String,
209    /// Locked dependencies by tool
210    #[serde(default)]
211    pub packs: HashMap<String, Vec<LockedDependency>>,
212}
213
214impl LockFile {
215    pub fn new() -> Self {
216        Self {
217            version: "1.0.0".to_string(),
218            packs: HashMap::new(),
219        }
220    }
221
222    pub fn load(path: &std::path::Path) -> Result<Self> {
223        let content = std::fs::read_to_string(path).map_err(Error::from)?;
224        toml::from_str(&content).map_err(Error::from)
225    }
226
227    pub fn save(&self, path: &std::path::Path) -> Result<()> {
228        let content = toml::to_string_pretty(self).map_err(Error::from)?;
229        std::fs::write(path, content).map_err(Error::from)?;
230        Ok(())
231    }
232
233    pub fn add_dependency(&mut self, tool: &str, dep: LockedDependency) {
234        self.packs.entry(tool.to_string()).or_default().push(dep);
235    }
236
237    #[allow(dead_code)]
238    pub fn get_dependencies(&self, tool: Option<&str>) -> Vec<&LockedDependency> {
239        if let Some(tool) = tool {
240            self.packs
241                .get(tool)
242                .map(|deps| deps.iter().collect())
243                .unwrap_or_default()
244        } else {
245            self.packs.values().flat_map(|deps| deps.iter()).collect()
246        }
247    }
248
249    #[allow(dead_code)]
250    pub fn find_dependency(&self, tool: &str, name: &str) -> Option<&LockedDependency> {
251        self.packs.get(tool)?.iter().find(|dep| dep.name == name)
252    }
253
254    /// Verify all locked dependencies have valid hashes
255    pub fn verify_integrity(&self, data_dir: &std::path::Path) -> IntegrityReport {
256        let mut report = IntegrityReport::new();
257
258        for (tool, deps) in &self.packs {
259            for dep in deps {
260                let pack_path = data_dir
261                    .join(tool)
262                    .join(&dep.name)
263                    .join(dep.version.to_string());
264
265                if !pack_path.exists() {
266                    report.missing.push(dep.clone());
267                    continue;
268                }
269
270                // Read pack content and verify hash
271                if let Ok(content) = std::fs::read(&pack_path) {
272                    if !dep.verify_hash(&content) {
273                        report.hash_mismatch.push(dep.clone());
274                    } else {
275                        report.verified.push(dep.clone());
276                    }
277                } else {
278                    report.unreadable.push(dep.clone());
279                }
280            }
281        }
282
283        report
284    }
285}
286
287/// Integrity verification report
288#[derive(Debug, Clone, Default)]
289pub struct IntegrityReport {
290    pub verified: Vec<LockedDependency>,
291    pub missing: Vec<LockedDependency>,
292    pub hash_mismatch: Vec<LockedDependency>,
293    pub unreadable: Vec<LockedDependency>,
294}
295
296impl IntegrityReport {
297    pub fn new() -> Self {
298        Self::default()
299    }
300
301    pub fn is_valid(&self) -> bool {
302        self.missing.is_empty() && self.hash_mismatch.is_empty() && self.unreadable.is_empty()
303    }
304
305    pub fn summary(&self) -> String {
306        format!(
307            "Verified: {}, Missing: {}, Hash mismatch: {}, Unreadable: {}",
308            self.verified.len(),
309            self.missing.len(),
310            self.hash_mismatch.len(),
311            self.unreadable.len()
312        )
313    }
314}