use std::collections::HashMap;
use crate::core::recipe::Recipe;
use crate::recipes;
pub struct RecipeRegistry {
recipes: HashMap<&'static str, Box<dyn Recipe>>,
}
impl RecipeRegistry {
pub fn new() -> Self {
let mut registry = Self {
recipes: HashMap::new(),
};
recipes::register_all(&mut registry);
registry
}
pub fn register<R>(&mut self, recipe: R)
where
R: Recipe + 'static,
{
let name = recipe.metadata().name;
self.recipes.insert(name, Box::new(recipe));
}
pub fn find(&self, name: &str) -> Option<&dyn Recipe> {
self.recipes.get(name).map(Box::as_ref)
}
pub fn all(&self) -> Vec<&dyn Recipe> {
let mut recipes: Vec<&dyn Recipe> = self.recipes.values().map(Box::as_ref).collect();
recipes.sort_by(|left, right| left.metadata().name.cmp(right.metadata().name));
recipes
}
pub fn load_plugins(&mut self, project_root: &std::path::Path) {
let default_paths = vec![
project_root.join("plugins"),
project_root.join(".morph-cli").join("plugins"),
dirs::home_dir()
.map(|h| h.join(".morph-cli").join("plugins"))
.unwrap_or_default(),
std::path::PathBuf::from("/usr/local/lib/morph-cli/plugins"),
];
let has_any_plugins = default_paths.iter().any(|p| {
p.exists() && p.is_dir() && std::fs::read_dir(p)
.map(|mut entries| entries.next().is_some())
.unwrap_or(false)
});
if !has_any_plugins {
return;
}
let mut plugin_registry = crate::core::plugins::registry::PluginRegistry::new();
let reports = plugin_registry.discover(project_root);
for report in reports {
if report.status == crate::core::plugins::DiscoveryStatus::Valid {
let path = report.path.clone();
if let Ok(_) = plugin_registry.register(&path) {
if let Some(plugin) = plugin_registry.get(&report.name) {
for recipe_entry in &plugin.manifest.recipes {
self.register_plugin_recipe(recipe_entry, &plugin.manifest.name);
}
}
}
}
}
}
fn register_plugin_recipe(&mut self, entry: &crate::core::plugins::manifest::RecipeEntry, plugin_name: &str) {
let name = entry.name.clone();
let description = format!("{} (from plugin: {})",
entry.description.as_deref().unwrap_or("No description"),
plugin_name
);
let metadata = create_dynamic_metadata(name, description, vec!["*".to_string()]);
self.recipes.insert(metadata.name, Box::new(PluginProxyRecipe { metadata }));
}
}
struct PluginProxyRecipe {
metadata: &'static crate::core::recipe::RecipeMetadata,
}
impl crate::core::recipe::Recipe for PluginProxyRecipe {
fn metadata(&self) -> &'static crate::core::recipe::RecipeMetadata {
self.metadata
}
fn detect(&self, _root: &std::path::Path, _progress: &indicatif::ProgressBar) -> anyhow::Result<crate::core::recipe::DetectionReport> {
Ok(crate::core::recipe::DetectionReport::default())
}
fn transform(&self, _report: &crate::core::recipe::DetectionReport, _options: crate::core::recipe::TransformOptions) -> anyhow::Result<crate::core::recipe::TransformReport> {
anyhow::bail!("Plugin recipes are currently metadata-only and cannot be executed directly.")
}
}
fn create_dynamic_metadata(name: String, description: String, extensions: Vec<String>) -> &'static crate::core::recipe::RecipeMetadata {
let name_str = Box::leak(name.into_boxed_str());
let desc_str = Box::leak(description.into_boxed_str());
let ext_strs: Vec<&'static str> = extensions.into_iter().map(|s| Box::leak(s.into_boxed_str()) as &str).collect();
let ext_slice = Box::leak(ext_strs.into_boxed_slice());
Box::leak(Box::new(crate::core::recipe::RecipeMetadata {
name: name_str,
description: desc_str,
supported_extensions: ext_slice,
required_recipes: &[],
incompatible_recipes: &[],
should_run_before: &[],
should_run_after: &[],
maturity: crate::core::recipe::RecipeMaturity::Stable,
category: crate::core::recipe::RecipeCategory::Migration,
tags: &[],
}))
}