reasonkit-core 0.1.8

The Reasoning Engine — Auditable Reasoning for Production AI | Rust-Native | Turn Prompts into Protocols
//! Plugin Management System for ReasonKit CLI
//!
//! Handles discovery, registration, and execution of external plugins.
//! Plugins are executable binaries or scripts located in the configured plugin directory.

use std::collections::HashMap;
use std::path::PathBuf;
use std::process::Command;
use tracing::info;
use walkdir::WalkDir;

/// Plugin metadata
#[derive(Debug, Clone)]
pub struct Plugin {
    /// Name of the plugin (without rk- prefix)
    pub name: String,
    /// Path to the executable
    pub path: PathBuf,
    /// Description (from metadata or file info)
    pub description: String,
}

/// Plugin Manager
pub struct PluginManager {
    /// Directory to scan for plugins
    plugin_dir: PathBuf,
    /// Map of plugin name to Plugin struct
    plugins: HashMap<String, Plugin>,
}

impl PluginManager {
    /// Create a new PluginManager
    pub fn new(plugin_dir: PathBuf) -> Self {
        Self {
            plugin_dir,
            plugins: HashMap::new(),
        }
    }

    /// Scan for plugins in the plugin directory
    pub fn scan(&mut self) -> anyhow::Result<usize> {
        if !self.plugin_dir.exists() {
            return Ok(0);
        }

        info!("Scanning for plugins in {}", self.plugin_dir.display());
        self.plugins.clear();

        for entry in WalkDir::new(&self.plugin_dir)
            .min_depth(1)
            .max_depth(1)
            .into_iter()
            .filter_map(|e| e.ok())
        {
            let path = entry.path();
            if path.is_file() {
                // Check if executable (on Unix)
                #[cfg(unix)]
                {
                    use std::os::unix::fs::PermissionsExt;
                    if let Ok(metadata) = path.metadata() {
                        if metadata.permissions().mode() & 0o111 == 0 {
                            continue; // Not executable
                        }
                    }
                }

                if let Some(filename) = path.file_name().and_then(|s| s.to_str()) {
                    // Convention: rk-<name>
                    if filename.starts_with("rk-") {
                        let name = filename.strip_prefix("rk-").unwrap().to_string();
                        self.plugins.insert(
                            name.clone(),
                            Plugin {
                                name,
                                path: path.to_path_buf(),
                                description: format!("External plugin: {}", filename),
                            },
                        );
                    }
                }
            }
        }

        Ok(self.plugins.len())
    }

    /// Get a plugin by name
    pub fn get(&self, name: &str) -> Option<&Plugin> {
        self.plugins.get(name)
    }

    /// List all plugins
    pub fn list(&self) -> Vec<&Plugin> {
        self.plugins.values().collect()
    }

    /// Execute a plugin with arguments
    pub fn execute(&self, name: &str, args: &[String]) -> anyhow::Result<std::process::ExitStatus> {
        if let Some(plugin) = self.get(name) {
            info!("Executing plugin: {} {:?}", plugin.path.display(), args);
            let status = Command::new(&plugin.path).args(args).status()?;
            Ok(status)
        } else {
            Err(anyhow::anyhow!("Plugin not found: {}", name))
        }
    }
}