1use super::loader::PluginLoader;
2use super::manifest::PluginManifest;
3use std::collections::HashMap;
4use std::path::{Path, PathBuf};
5
6pub struct PluginRegistry {
7 plugins: HashMap<String, PluginEntry>,
8 recipes: HashMap<String, String>,
9 loader: PluginLoader,
10}
11
12#[derive(Debug, Clone)]
13pub struct PluginEntry {
14 pub manifest: PluginManifest,
15 pub path: PathBuf,
16 pub enabled: bool,
17}
18
19impl PluginRegistry {
20 pub fn new() -> Self {
21 Self {
22 plugins: HashMap::new(),
23 recipes: HashMap::new(),
24 loader: PluginLoader::new(),
25 }
26 }
27
28 pub fn discover(&mut self, project_root: &Path) -> Vec<DiscoveryReport> {
29 self.loader.add_default_paths(project_root);
30 let results = self.loader.discover();
31
32 results
33 .into_iter()
34 .map(|r| {
35 let name = r
36 .manifest
37 .as_ref()
38 .map(|m| m.name.clone())
39 .unwrap_or_default();
40 let (status, error) = match &r.manifest {
41 Some(m) => {
42 let errs = m.validate();
43 if errs.is_empty() {
44 (DiscoveryStatus::Valid, None)
45 } else {
46 let msg = errs.iter()
47 .map(|e| e.to_string())
48 .collect::<Vec<_>>()
49 .join("; ");
50 (DiscoveryStatus::Invalid, Some(msg))
51 }
52 }
53 None => {
54 match &r.status {
55 super::loader::DiscoveryStatus::Error(err) => {
56 (DiscoveryStatus::Invalid, Some(err.clone()))
57 }
58 _ => (DiscoveryStatus::NotFound, None),
59 }
60 }
61 };
62 let version = r.manifest.as_ref().map(|m| m.version.clone()).unwrap_or_default();
63 let recipes = r.manifest.as_ref().map(|m| m.recipes.iter().map(|r| r.name.clone()).collect()).unwrap_or_default();
64 DiscoveryReport {
65 path: r.path,
66 name,
67 version,
68 recipes,
69 status,
70 error,
71 }
72 })
73 .collect()
74 }
75
76 pub fn register(&mut self, path: &Path) -> Result<(), RegistryError> {
77 let plugin = self
78 .loader
79 .load_plugin(path)
80 .map_err(|e| RegistryError::LoadError(e.to_string()))?;
81 let name = plugin.manifest.name.clone();
82
83 if self.plugins.contains_key(&name) {
84 return Err(RegistryError::Conflict(name));
85 }
86
87 for recipe in &plugin.manifest.recipes {
88 if self.recipes.contains_key(&recipe.name) {
89 eprintln!(
90 "Warning: recipe '{}' already registered, conflicts with plugin '{}'",
91 recipe.name, name
92 );
93 }
94 self.recipes.insert(recipe.name.clone(), name.clone());
95 }
96
97 self.plugins.insert(
98 name,
99 PluginEntry {
100 manifest: plugin.manifest,
101 path: path.to_path_buf(),
102 enabled: true,
103 },
104 );
105
106 Ok(())
107 }
108
109 pub fn unregister(&mut self, name: &str) -> Option<PluginEntry> {
110 if let Some(entry) = self.plugins.remove(name) {
111 let recipe_names: Vec<String> = self
112 .recipes
113 .iter()
114 .filter(|(_, plugin_name)| *plugin_name == name)
115 .map(|(k, _)| k.clone())
116 .collect();
117
118 for recipe in recipe_names {
119 self.recipes.remove(&recipe);
120 }
121 Some(entry)
122 } else {
123 None
124 }
125 }
126
127 pub fn get(&self, name: &str) -> Option<&PluginEntry> {
128 self.plugins.get(name)
129 }
130
131 pub fn get_recipe<'a>(&'a self, recipe_name: &'a str) -> Option<(&'a PluginEntry, &'a str)> {
132 self.recipes
133 .get(recipe_name)
134 .and_then(move |plugin_name| self.plugins.get(plugin_name).map(|p| (p, recipe_name)))
135 }
136
137 pub fn list_plugins(&self) -> Vec<&PluginEntry> {
138 self.plugins.values().collect()
139 }
140
141 pub fn list_recipes(&self) -> Vec<(&String, &String)> {
142 self.recipes.iter().collect()
143 }
144
145 pub fn summary(&self) -> RegistrySummary {
146 let enabled = self.plugins.values().filter(|p| p.enabled).count();
147 let total_recipes = self.recipes.len();
148
149 let conflicts = self.detect_conflicts();
150
151 RegistrySummary {
152 total_plugins: self.plugins.len(),
153 enabled_plugins: enabled,
154 total_recipes,
155 conflicts,
156 }
157 }
158
159 fn detect_conflicts(&self) -> Vec<Conflict> {
160 let mut conflicts = Vec::new();
161 let mut recipe_counts: HashMap<String, Vec<String>> = HashMap::new();
162
163 for plugin in self.plugins.values() {
164 for recipe in &plugin.manifest.recipes {
165 recipe_counts
166 .entry(recipe.name.clone())
167 .or_default()
168 .push(plugin.manifest.name.clone());
169 }
170 }
171
172 for (recipe, plugins) in recipe_counts {
173 if plugins.len() > 1 {
174 conflicts.push(Conflict {
175 recipe,
176 plugins,
177 });
178 }
179 }
180
181 conflicts
182 }
183
184 pub fn check_compatibility(&self) -> Vec<CompatibilityIssue> {
185 self.loader
186 .incompatible_plugins()
187 .into_iter()
188 .map(|i| CompatibilityIssue {
189 plugin: i.plugin_name,
190 required: i.required_version,
191 current: i.current_version,
192 })
193 .collect()
194 }
195
196 pub fn diagnostics(&self) -> Diagnostics {
197 let summary = self.summary();
198 let compatibility = self.check_compatibility();
199
200 Diagnostics {
201 summary,
202 compatibility_issues: compatibility,
203 loader_stats: LoaderStats {
204 search_paths: self.loader.search_paths.len(),
205 loaded_count: self.loader.loaded_plugins().len(),
206 },
207 }
208 }
209}
210
211impl Default for PluginRegistry {
212 fn default() -> Self {
213 Self::new()
214 }
215}
216
217#[derive(Debug, Clone)]
218pub struct DiscoveryReport {
219 pub path: std::path::PathBuf,
220 pub name: String,
221 pub version: String,
222 pub recipes: Vec<String>,
223 pub status: DiscoveryStatus,
224 pub error: Option<String>,
225}
226
227#[derive(Debug, Clone, PartialEq)]
228pub enum DiscoveryStatus {
229 Valid,
230 Invalid,
231 NotFound,
232}
233
234#[derive(Debug, Clone)]
235pub struct RegistrySummary {
236 pub total_plugins: usize,
237 pub enabled_plugins: usize,
238 pub total_recipes: usize,
239 pub conflicts: Vec<Conflict>,
240}
241
242impl std::fmt::Display for RegistrySummary {
243 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
244 use colored::Colorize;
245 writeln!(
246 f,
247 "Plugins: {}/{}",
248 self.enabled_plugins, self.total_plugins
249 )?;
250 writeln!(f, "Recipes: {}", self.total_recipes)?;
251 if !self.conflicts.is_empty() {
252 writeln!(f, "{}", "⚠️ Recipe Conflicts Detected:".yellow().bold())?;
253 for conflict in &self.conflicts {
254 writeln!(
255 f,
256 " - Recipe '{}' is defined by multiple plugins: {}",
257 conflict.recipe.cyan().bold(),
258 conflict.plugins.join(", ")
259 )?;
260 }
261 }
262 Ok(())
263 }
264}
265
266#[derive(Debug, Clone)]
267pub struct Conflict {
268 pub recipe: String,
269 pub plugins: Vec<String>,
270}
271
272#[derive(Debug, Clone)]
273pub struct CompatibilityIssue {
274 pub plugin: String,
275 pub required: String,
276 pub current: String,
277}
278
279#[derive(Debug)]
280pub struct Diagnostics {
281 pub summary: RegistrySummary,
282 pub compatibility_issues: Vec<CompatibilityIssue>,
283 pub loader_stats: LoaderStats,
284}
285
286#[derive(Debug)]
287pub struct LoaderStats {
288 pub search_paths: usize,
289 pub loaded_count: usize,
290}
291
292#[derive(Debug, thiserror::Error)]
293pub enum RegistryError {
294 #[error("Plugin not found: {0}")]
295 NotFound(String),
296 #[error("Plugin conflict: {0} already registered")]
297 Conflict(String),
298 #[error("Load error: {0}")]
299 LoadError(String),
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305 use std::io::Write;
306 use tempfile::TempDir;
307
308 fn create_test_plugin(name: &str) -> TempDir {
309 let dir = tempfile::tempdir().unwrap();
310 let manifest = format!(
311 r#"
312name = "{}"
313version = "1.0.0"
314
315[[recipes]]
316name = "test-recipe"
317description = "A test recipe"
318
319[compatibility]
320morph_cli_version = ">=0.1.0"
321"#,
322 name
323 );
324 let mut file = std::fs::File::create(dir.path().join("morph-cli-plugin.toml")).unwrap();
325 file.write_all(manifest.as_bytes()).unwrap();
326 dir
327 }
328
329 #[test]
330 fn test_register_plugin() {
331 let dir = create_test_plugin("test-plugin");
332 let mut registry = PluginRegistry::new();
333 let path = dir.path().join("morph-cli-plugin.toml");
334
335 registry.register(&path).unwrap();
336 assert_eq!(registry.plugins.len(), 1);
337 }
338
339 #[test]
340 fn test_unregister_plugin() {
341 let dir = create_test_plugin("test-plugin");
342 let mut registry = PluginRegistry::new();
343 let path = dir.path().join("morph-cli-plugin.toml");
344
345 registry.register(&path).unwrap();
346 let removed = registry.unregister("test-plugin");
347 assert!(removed.is_some());
348 assert!(registry.plugins.is_empty());
349 }
350
351 #[test]
352 fn test_get_plugin() {
353 let dir = create_test_plugin("test-plugin");
354 let mut registry = PluginRegistry::new();
355 let path = dir.path().join("morph-cli-plugin.toml");
356
357 registry.register(&path).unwrap();
358 let plugin = registry.get("test-plugin");
359 assert!(plugin.is_some());
360 }
361
362 #[test]
363 fn test_list_plugins() {
364 let dir = create_test_plugin("test-plugin");
365 let mut registry = PluginRegistry::new();
366 let path = dir.path().join("morph-cli-plugin.toml");
367
368 registry.register(&path).unwrap();
369 let plugins = registry.list_plugins();
370 assert_eq!(plugins.len(), 1);
371 }
372
373 #[test]
374 fn test_summary() {
375 let dir = create_test_plugin("test-plugin");
376 let mut registry = PluginRegistry::new();
377 let path = dir.path().join("morph-cli-plugin.toml");
378
379 registry.register(&path).unwrap();
380 let summary = registry.summary();
381 assert_eq!(summary.total_plugins, 1);
382 }
383
384 #[test]
385 fn test_conflict_detection() {
386 let mut registry = PluginRegistry::new();
387
388 let dir_a = create_test_plugin("plugin-a");
389 let path_a = dir_a.path().join("morph-cli-plugin.toml");
390 registry.register(&path_a).unwrap();
391
392 let dir_b = create_test_plugin("plugin-b");
393 let path_b = dir_b.path().join("morph-cli-plugin.toml");
394 registry.register(&path_b).unwrap();
395
396 let summary = registry.summary();
397 assert_eq!(summary.conflicts.len(), 1);
398 assert_eq!(summary.conflicts[0].recipe, "test-recipe");
399 assert!(summary.conflicts[0].plugins.contains(&"plugin-a".to_string()));
400 assert!(summary.conflicts[0].plugins.contains(&"plugin-b".to_string()));
401 }
402}