mai_cli/storage/
registry.rs1use crate::core::{InstallScope, Pack, Version};
2use crate::error::Result;
3use crate::storage::xdg::XdgPaths;
4use std::path::{Path, PathBuf};
5
6pub struct Registry {
7 global_packs_dir: PathBuf,
8 local_packs_dir: Option<PathBuf>,
9}
10
11impl Registry {
12 pub fn new(paths: &XdgPaths) -> Self {
13 Self {
14 global_packs_dir: paths.packs_dir(),
15 local_packs_dir: None,
16 }
17 }
18
19 #[allow(dead_code)]
21 pub fn with_local(paths: &XdgPaths, project_root: &Path) -> Self {
22 Self {
23 global_packs_dir: paths.packs_dir(),
24 local_packs_dir: Some(project_root.join(".mai").join("packs")),
25 }
26 }
27
28 fn packs_dir_for_scope(&self, scope: InstallScope) -> &Path {
30 match scope {
31 InstallScope::Global => &self.global_packs_dir,
32 InstallScope::Local => self
33 .local_packs_dir
34 .as_ref()
35 .unwrap_or(&self.global_packs_dir),
36 }
37 }
38
39 pub fn pack_dir(&self, tool: &str, pack: &Pack) -> PathBuf {
40 let base = self.packs_dir_for_scope(pack.scope);
41 base.join(tool)
42 .join(&pack.name)
43 .join(pack.version.to_string())
44 }
45
46 pub fn install_pack(&self, tool: &str, pack: &Pack) -> Result<PathBuf> {
47 let pack_dir = self.pack_dir(tool, pack);
48 std::fs::create_dir_all(&pack_dir)?;
49 log::info!("Installed {} to {}", pack.id(), pack_dir.display());
50 Ok(pack_dir)
51 }
52
53 pub fn remove_pack(&self, tool: &str, pack: &Pack) -> Result<()> {
54 let pack_dir = self.pack_dir(tool, pack);
55 if pack_dir.exists() {
56 std::fs::remove_dir_all(&pack_dir)?;
57 log::info!("Removed {}", pack.id());
58 }
59 Ok(())
60 }
61
62 pub fn list_packs(&self, tool: Option<&str>, scope: Option<InstallScope>) -> Result<Vec<Pack>> {
64 let mut packs = Vec::new();
65
66 let scopes = match scope {
67 Some(s) => vec![s],
68 None => vec![InstallScope::Local, InstallScope::Global],
69 };
70
71 for scope in scopes {
72 let base = self.packs_dir_for_scope(scope);
73 if !base.exists() {
74 continue;
75 }
76
77 let tool_dirs = if let Some(t) = tool {
79 vec![base.join(t)]
80 } else {
81 std::fs::read_dir(base)?
82 .filter_map(|e| e.ok())
83 .filter(|e| e.path().is_dir())
84 .map(|e| e.path())
85 .collect()
86 };
87
88 for tool_path in tool_dirs {
89 if !tool_path.exists() {
90 continue;
91 }
92
93 let tool_name = tool_path
94 .file_name()
95 .unwrap_or_default()
96 .to_string_lossy()
97 .to_string();
98
99 for pack_entry in std::fs::read_dir(&tool_path)? {
100 let pack_entry = pack_entry?;
101 let pack_name = pack_entry.file_name().to_string_lossy().to_string();
102
103 for version_entry in std::fs::read_dir(pack_entry.path())? {
104 let version_entry = version_entry?;
105 let version_str = version_entry.file_name().to_string_lossy().to_string();
106 if let Ok(version) = Version::parse(&version_str) {
107 let mut pack =
108 Pack::new(&pack_name, crate::core::PackType::Skill, version)
109 .with_tool(&tool_name)
110 .with_scope(scope);
111
112 let manifest_path = version_entry.path().join("manifest.toml");
114 if manifest_path.exists()
115 && let Ok(content) = std::fs::read_to_string(&manifest_path)
116 && let Ok(metadata) =
117 toml::from_str::<crate::core::PackMetadata>(&content)
118 {
119 pack = pack.with_metadata(metadata);
120 }
121
122 packs.push(pack);
123 }
124 }
125 }
126 }
127 }
128
129 Ok(packs)
130 }
131
132 #[allow(dead_code)]
134 pub fn has_pack(&self, tool: &str, pack: &Pack) -> bool {
135 self.pack_dir(tool, pack).exists()
136 }
137
138 pub fn find_pack(
140 &self,
141 tool: &str,
142 name: &str,
143 pack_type: crate::core::PackType,
144 ) -> Option<Pack> {
145 if let Ok(local_packs) = self.list_packs(Some(tool), Some(InstallScope::Local))
147 && let Some(pack) = local_packs
148 .into_iter()
149 .find(|p| p.name == name && p.pack_type == pack_type)
150 {
151 return Some(pack);
152 }
153
154 if let Ok(global_packs) = self.list_packs(Some(tool), Some(InstallScope::Global))
156 && let Some(pack) = global_packs
157 .into_iter()
158 .find(|p| p.name == name && p.pack_type == pack_type)
159 {
160 return Some(pack);
161 }
162
163 None
164 }
165}