morph_cli/core/
registry.rs1use 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 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 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}