ai_model_vault/
plugins.rs1use std::collections::BTreeMap;
13use std::fs;
14use std::path::{Path, PathBuf};
15
16use serde::{Deserialize, Serialize};
17
18use crate::error::{Result, VaultError};
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct PluginManifest {
25 pub id: String,
27 pub name: String,
29 pub version: String,
31 pub description: String,
33 pub author: Option<String>,
35 pub min_aim_version: Option<String>,
37 pub capabilities: Vec<String>,
39 pub entry_point: Option<String>,
41}
42
43#[derive(Debug, Clone, Serialize)]
45pub struct PluginInfo {
46 pub manifest: PluginManifest,
47 pub path: PathBuf,
48 pub loaded: bool,
49}
50
51pub struct PluginRegistry {
55 plugins_dir: PathBuf,
56 plugins: BTreeMap<String, PluginInfo>,
57}
58
59impl PluginRegistry {
60 pub fn new(plugins_dir: &Path) -> Result<Self> {
62 let mut registry = Self {
63 plugins_dir: plugins_dir.to_path_buf(),
64 plugins: BTreeMap::new(),
65 };
66 registry.discover()?;
67 Ok(registry)
68 }
69
70 pub fn discover(&mut self) -> Result<usize> {
72 self.plugins.clear();
73
74 if !self.plugins_dir.is_dir() {
75 return Ok(0);
76 }
77
78 let mut count = 0;
79 for entry in fs::read_dir(&self.plugins_dir)? {
80 let entry = entry?;
81 let path = entry.path();
82
83 let manifest_path = if path.is_dir() {
85 path.join("plugin.json")
86 } else if path.extension().and_then(|e| e.to_str()) == Some("json")
87 && path
88 .file_stem()
89 .and_then(|s| s.to_str())
90 .map(|s| s.ends_with(".plugin"))
91 .unwrap_or(false)
92 {
93 path.clone()
94 } else {
95 continue;
96 };
97
98 if manifest_path.exists() {
99 match self.load_manifest(&manifest_path) {
100 Ok(manifest) => {
101 let info = PluginInfo {
102 manifest: manifest.clone(),
103 path: manifest_path
104 .parent()
105 .unwrap_or(&self.plugins_dir)
106 .to_path_buf(),
107 loaded: false,
108 };
109 self.plugins.insert(manifest.id.clone(), info);
110 count += 1;
111 }
112 Err(e) => {
113 tracing::warn!("Skipping plugin at {}: {}", manifest_path.display(), e);
114 }
115 }
116 }
117 }
118
119 Ok(count)
120 }
121
122 fn load_manifest(&self, path: &Path) -> Result<PluginManifest> {
123 let data = fs::read_to_string(path)?;
124 let manifest: PluginManifest = serde_json::from_str(&data)?;
125
126 if manifest.id.is_empty() {
127 return Err(VaultError::InvalidInput(
128 "Plugin manifest missing 'id' field".into(),
129 ));
130 }
131
132 Ok(manifest)
133 }
134
135 pub fn list(&self) -> Vec<&PluginInfo> {
137 self.plugins.values().collect()
138 }
139
140 pub fn get(&self, id: &str) -> Option<&PluginInfo> {
142 self.plugins.get(id)
143 }
144
145 pub fn install(&mut self, manifest: PluginManifest) -> Result<()> {
147 let plugin_dir = self.plugins_dir.join(&manifest.id);
148 fs::create_dir_all(&plugin_dir)?;
149
150 let manifest_path = plugin_dir.join("plugin.json");
151 let json = serde_json::to_string_pretty(&manifest)?;
152 fs::write(&manifest_path, json)?;
153
154 let info = PluginInfo {
155 manifest: manifest.clone(),
156 path: plugin_dir,
157 loaded: false,
158 };
159 self.plugins.insert(manifest.id, info);
160 Ok(())
161 }
162
163 pub fn uninstall(&mut self, id: &str) -> Result<bool> {
165 if let Some(info) = self.plugins.remove(id) {
166 if info.path.is_dir() {
167 fs::remove_dir_all(&info.path)?;
168 }
169 Ok(true)
170 } else {
171 Ok(false)
172 }
173 }
174
175 pub fn display(&self) -> String {
177 let mut out = String::new();
178 if self.plugins.is_empty() {
179 out.push_str("No plugins installed.\n");
180 return out;
181 }
182
183 out.push_str(&format!(
184 "{:<30} {:<10} {}\n",
185 "ID", "Version", "Description"
186 ));
187 out.push_str(&format!("{}\n", "─".repeat(60)));
188
189 for info in self.plugins.values() {
190 out.push_str(&format!(
191 "{:<30} {:<10} {}\n",
192 info.manifest.id, info.manifest.version, info.manifest.description
193 ));
194 }
195 out
196 }
197}
198
199#[cfg(test)]
202mod tests {
203 use super::*;
204
205 #[test]
206 fn test_empty_registry() {
207 let dir = tempfile::tempdir().unwrap();
208 let plugins_dir = dir.path().join("plugins");
209 fs::create_dir_all(&plugins_dir).unwrap();
210 let registry = PluginRegistry::new(&plugins_dir).unwrap();
211 assert_eq!(registry.list().len(), 0);
212 }
213
214 #[test]
215 fn test_discover_plugin() {
216 let dir = tempfile::tempdir().unwrap();
217 let plugins_dir = dir.path().join("plugins");
218 let plugin_dir = plugins_dir.join("test-plugin");
219 fs::create_dir_all(&plugin_dir).unwrap();
220
221 let manifest = PluginManifest {
222 id: "test-plugin".into(),
223 name: "Test Plugin".into(),
224 version: "1.0.0".into(),
225 description: "A test plugin".into(),
226 author: Some("Test".into()),
227 min_aim_version: None,
228 capabilities: vec!["custom-command".into()],
229 entry_point: None,
230 };
231
232 let json = serde_json::to_string_pretty(&manifest).unwrap();
233 fs::write(plugin_dir.join("plugin.json"), json).unwrap();
234
235 let registry = PluginRegistry::new(&plugins_dir).unwrap();
236 assert_eq!(registry.list().len(), 1);
237 assert_eq!(
238 registry.get("test-plugin").unwrap().manifest.name,
239 "Test Plugin"
240 );
241 }
242
243 #[test]
244 fn test_install_and_uninstall() {
245 let dir = tempfile::tempdir().unwrap();
246 let plugins_dir = dir.path().join("plugins");
247 fs::create_dir_all(&plugins_dir).unwrap();
248
249 let mut registry = PluginRegistry::new(&plugins_dir).unwrap();
250
251 let manifest = PluginManifest {
252 id: "my-plugin".into(),
253 name: "My Plugin".into(),
254 version: "0.1.0".into(),
255 description: "Custom converter".into(),
256 author: None,
257 min_aim_version: Some("1.3.0".into()),
258 capabilities: vec![],
259 entry_point: None,
260 };
261
262 registry.install(manifest).unwrap();
263 assert_eq!(registry.list().len(), 1);
264
265 assert!(registry.uninstall("my-plugin").unwrap());
266 assert_eq!(registry.list().len(), 0);
267 }
268
269 #[test]
270 fn test_display() {
271 let dir = tempfile::tempdir().unwrap();
272 let plugins_dir = dir.path().join("plugins");
273 fs::create_dir_all(&plugins_dir).unwrap();
274
275 let mut registry = PluginRegistry::new(&plugins_dir).unwrap();
276 registry
277 .install(PluginManifest {
278 id: "demo".into(),
279 name: "Demo".into(),
280 version: "1.0.0".into(),
281 description: "Demo plugin".into(),
282 author: None,
283 min_aim_version: None,
284 capabilities: vec![],
285 entry_point: None,
286 })
287 .unwrap();
288
289 let text = registry.display();
290 assert!(text.contains("demo"));
291 }
292}