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#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct DependencySpec {
56 pub name: String,
58 #[serde(rename = "type")]
60 pub pack_type: PackType,
61 #[serde(default = "default_version")]
63 pub version: String,
64 #[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#[derive(Debug, Clone, Serialize, Deserialize, Default)]
86pub struct ProjectManifest {
87 #[serde(default)]
89 pub project: ProjectInfo,
90 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct LockedDependency {
142 pub name: String,
144 #[serde(rename = "type")]
146 pub pack_type: PackType,
147 pub version: Version,
149 pub hash: String,
151 #[serde(default)]
153 pub source: Option<String>,
154 #[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 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 pub fn verify_hash(&self, content: &[u8]) -> bool {
199 let computed = Self::compute_hash(content);
200 computed == self.hash
201 }
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize, Default)]
206pub struct LockFile {
207 pub version: String,
209 #[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 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 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#[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}