Skip to main content

morph_cli/core/
registry.rs

1use std::collections::HashMap;
2
3use crate::core::recipe::Recipe;
4use crate::recipes;
5
6pub struct RecipeRegistry {
7    recipes: HashMap<&'static str, Box<dyn Recipe>>,
8}
9
10impl RecipeRegistry {
11    pub fn new() -> Self {
12        let mut registry = Self {
13            recipes: HashMap::new(),
14        };
15
16        recipes::register_all(&mut registry);
17        registry
18    }
19
20    pub fn register<R>(&mut self, recipe: R)
21    where
22        R: Recipe + 'static,
23    {
24        let name = recipe.metadata().name;
25        self.recipes.insert(name, Box::new(recipe));
26    }
27
28    pub fn find(&self, name: &str) -> Option<&dyn Recipe> {
29        self.recipes.get(name).map(Box::as_ref)
30    }
31
32    pub fn all(&self) -> Vec<&dyn Recipe> {
33        let mut recipes: Vec<&dyn Recipe> = self.recipes.values().map(Box::as_ref).collect();
34        recipes.sort_by(|left, right| left.metadata().name.cmp(right.metadata().name));
35        recipes
36    }
37
38    pub fn load_plugins(&mut self, project_root: &std::path::Path) {
39        // Quick check to see if any potential plugin directory actually exists.
40        // This avoids full discovery, instantiation and filesystem walking!
41        let default_paths = vec![
42            project_root.join("plugins"),
43            project_root.join(".morph-cli").join("plugins"),
44            dirs::home_dir()
45                .map(|h| h.join(".morph-cli").join("plugins"))
46                .unwrap_or_default(),
47            std::path::PathBuf::from("/usr/local/lib/morph-cli/plugins"),
48        ];
49        
50        let has_any_plugins = default_paths.iter().any(|p| {
51            p.exists() && p.is_dir() && std::fs::read_dir(p)
52                .map(|mut entries| entries.next().is_some())
53                .unwrap_or(false)
54        });
55
56        if !has_any_plugins {
57            return;
58        }
59
60        let mut plugin_registry = crate::core::plugins::registry::PluginRegistry::new();
61        let reports = plugin_registry.discover(project_root);
62        
63        for report in reports {
64            if report.status == crate::core::plugins::DiscoveryStatus::Valid {
65                // We need to actually register them in the plugin registry to get the manifests
66                // But discovery results already have recipes list.
67                // For full metadata, we'd need to load the manifest.
68                let path = report.path.clone();
69                if let Ok(_) = plugin_registry.register(&path) {
70                    if let Some(plugin) = plugin_registry.get(&report.name) {
71                        for recipe_entry in &plugin.manifest.recipes {
72                            self.register_plugin_recipe(recipe_entry, &plugin.manifest.name);
73                        }
74                    }
75                }
76            }
77        }
78    }
79
80    fn register_plugin_recipe(&mut self, entry: &crate::core::plugins::manifest::RecipeEntry, plugin_name: &str) {
81        let name = entry.name.clone();
82        let description = format!("{} (from plugin: {})", 
83            entry.description.as_deref().unwrap_or("No description"), 
84            plugin_name
85        );
86        
87        let metadata = create_dynamic_metadata(name, description, vec!["*".to_string()]);
88        self.recipes.insert(metadata.name, Box::new(PluginProxyRecipe { metadata }));
89    }
90}
91
92struct PluginProxyRecipe {
93    metadata: &'static crate::core::recipe::RecipeMetadata,
94}
95
96impl crate::core::recipe::Recipe for PluginProxyRecipe {
97    fn metadata(&self) -> &'static crate::core::recipe::RecipeMetadata {
98        self.metadata
99    }
100
101    fn detect(&self, _root: &std::path::Path, _progress: &indicatif::ProgressBar) -> anyhow::Result<crate::core::recipe::DetectionReport> {
102        Ok(crate::core::recipe::DetectionReport::default())
103    }
104
105    fn transform(&self, _report: &crate::core::recipe::DetectionReport, _options: crate::core::recipe::TransformOptions) -> anyhow::Result<crate::core::recipe::TransformReport> {
106        anyhow::bail!("Plugin recipes are currently metadata-only and cannot be executed directly.")
107    }
108}
109
110fn create_dynamic_metadata(name: String, description: String, extensions: Vec<String>) -> &'static crate::core::recipe::RecipeMetadata {
111    let name_str = Box::leak(name.into_boxed_str());
112    let desc_str = Box::leak(description.into_boxed_str());
113    let ext_strs: Vec<&'static str> = extensions.into_iter().map(|s| Box::leak(s.into_boxed_str()) as &str).collect();
114    let ext_slice = Box::leak(ext_strs.into_boxed_slice());
115
116    Box::leak(Box::new(crate::core::recipe::RecipeMetadata {
117        name: name_str,
118        description: desc_str,
119        supported_extensions: ext_slice,
120        required_recipes: &[],
121        incompatible_recipes: &[],
122        should_run_before: &[],
123        should_run_after: &[],
124        maturity: crate::core::recipe::RecipeMaturity::Stable,
125        category: crate::core::recipe::RecipeCategory::Migration,
126        tags: &[],
127    }))
128}