Skip to main content

mai_cli/storage/
registry.rs

1use 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    /// Create a registry with local project support
20    #[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    /// Get packs directory for the given scope
29    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    /// List packs for a given scope, or all scopes if scope is None
63    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            // If tool is specified, only look in that tool's directory
78            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                            // Try to load metadata from manifest.toml if it exists
113                            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    /// Check if a pack is installed in the given scope
133    #[allow(dead_code)]
134    pub fn has_pack(&self, tool: &str, pack: &Pack) -> bool {
135        self.pack_dir(tool, pack).exists()
136    }
137
138    /// Find a pack by name and type, checking local first, then global
139    pub fn find_pack(
140        &self,
141        tool: &str,
142        name: &str,
143        pack_type: crate::core::PackType,
144    ) -> Option<Pack> {
145        // Check local first
146        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        // Then check global
155        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}