pocket_cli/plugins/
mod.rs

1//! Plugin architecture for Pocket CLI
2//!
3//! This module provides a plugin system for extending Pocket CLI functionality.
4//! Plugins can add new commands, modify existing behavior, or provide additional features.
5
6pub mod backup;
7
8use std::collections::HashMap;
9use std::path::Path;
10use anyhow::{Result, Context};
11use serde::{Serialize, Deserialize};
12
13/// Trait that all plugins must implement
14pub trait Plugin: Send + Sync {
15    /// Returns the name of the plugin
16    fn name(&self) -> &str;
17    
18    /// Returns the version of the plugin
19    fn version(&self) -> &str;
20    
21    /// Returns a description of the plugin
22    fn description(&self) -> &str;
23    
24    /// Initializes the plugin with the given configuration
25    fn initialize(&mut self, config: &PluginConfig) -> Result<()>;
26    
27    /// Executes a command provided by the plugin
28    fn execute(&self, command: &str, args: &[String]) -> Result<()>;
29    
30    /// Returns a list of commands provided by the plugin
31    fn commands(&self) -> Vec<PluginCommand>;
32    
33    /// Cleans up any resources used by the plugin
34    fn cleanup(&mut self) -> Result<()>;
35}
36
37/// Configuration for a plugin
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct PluginConfig {
40    /// The name of the plugin
41    pub name: String,
42    
43    /// Whether the plugin is enabled
44    pub enabled: bool,
45    
46    /// Additional configuration options for the plugin
47    #[serde(default)]
48    pub options: HashMap<String, serde_json::Value>,
49}
50
51/// A command provided by a plugin
52#[derive(Debug, Clone)]
53pub struct PluginCommand {
54    /// The name of the command
55    pub name: String,
56    
57    /// A description of the command
58    pub description: String,
59    
60    /// The usage pattern for the command
61    pub usage: String,
62}
63
64/// Manager for loading and running plugins
65pub struct PluginManager {
66    /// The loaded plugins
67    plugins: Vec<Box<dyn Plugin>>,
68    
69    /// Configuration for each plugin
70    configs: HashMap<String, PluginConfig>,
71    
72    /// The directory where plugins are stored
73    plugin_dir: std::path::PathBuf,
74}
75
76impl PluginManager {
77    /// Creates a new plugin manager with the given plugin directory
78    pub fn new(plugin_dir: impl AsRef<Path>) -> Self {
79        Self {
80            plugins: Vec::new(),
81            configs: HashMap::new(),
82            plugin_dir: plugin_dir.as_ref().to_path_buf(),
83        }
84    }
85    
86    /// Loads all plugins from the plugin directory
87    pub fn load_plugins(&mut self) -> Result<()> {
88        // Ensure the plugin directory exists
89        if !self.plugin_dir.exists() {
90            std::fs::create_dir_all(&self.plugin_dir)
91                .context("Failed to create plugin directory")?;
92            return Ok(());
93        }
94        
95        // Load plugin configurations
96        self.load_configs()?;
97        
98        // Load dynamic libraries (this is a placeholder for actual dynamic loading)
99        // In a real implementation, this would use libloading or similar to load plugins
100        // For now, we'll just use built-in plugins
101        
102        // Register built-in plugins
103        self.register_builtin_plugins()?;
104        
105        Ok(())
106    }
107    
108    /// Loads plugin configurations from the plugin directory
109    fn load_configs(&mut self) -> Result<()> {
110        let config_path = self.plugin_dir.join("plugins.json");
111        
112        if !config_path.exists() {
113            // Create a default configuration if none exists
114            let default_configs: HashMap<String, PluginConfig> = HashMap::new();
115            let json = serde_json::to_string_pretty(&default_configs)?;
116            std::fs::write(&config_path, json)?;
117            return Ok(());
118        }
119        
120        // Read and parse the configuration file
121        let json = std::fs::read_to_string(&config_path)?;
122        self.configs = serde_json::from_str(&json)?;
123        
124        Ok(())
125    }
126    
127    /// Saves plugin configurations to the plugin directory
128    pub fn save_configs(&self) -> Result<()> {
129        let config_path = self.plugin_dir.join("plugins.json");
130        let json = serde_json::to_string_pretty(&self.configs)?;
131        std::fs::write(&config_path, json)?;
132        Ok(())
133    }
134    
135    /// Registers built-in plugins
136    fn register_builtin_plugins(&mut self) -> Result<()> {
137        // Register the backup plugin
138        use crate::plugins::backup::BackupPlugin;
139        
140        // Create the data directory path (parent of plugin_dir)
141        let data_dir = self.plugin_dir.parent().unwrap_or(&self.plugin_dir).to_path_buf();
142        
143        // Register the backup plugin
144        self.register_plugin(Box::new(BackupPlugin::new(data_dir)))?;
145        
146        Ok(())
147    }
148    
149    /// Registers a plugin with the manager
150    pub fn register_plugin(&mut self, mut plugin: Box<dyn Plugin>) -> Result<()> {
151        let name = plugin.name().to_string();
152        
153        // Get or create a configuration for the plugin
154        let config = self.configs.entry(name.clone()).or_insert_with(|| {
155            PluginConfig {
156                name: name.clone(),
157                enabled: true,
158                options: HashMap::new(),
159            }
160        });
161        
162        // Initialize the plugin with its configuration
163        plugin.initialize(config)?;
164        
165        // Add the plugin to the list
166        self.plugins.push(plugin);
167        
168        Ok(())
169    }
170    
171    /// Returns a list of all loaded plugins
172    pub fn list_plugins(&self) -> Vec<(&str, &str, bool)> {
173        self.plugins.iter()
174            .map(|p| {
175                let name = p.name();
176                let version = p.version();
177                let enabled = self.configs.get(name)
178                    .map(|c| c.enabled)
179                    .unwrap_or(false);
180                (name, version, enabled)
181            })
182            .collect()
183    }
184    
185    /// Enables a plugin by name
186    pub fn enable_plugin(&mut self, name: &str) -> Result<()> {
187        if let Some(config) = self.configs.get_mut(name) {
188            config.enabled = true;
189            self.save_configs()?;
190            Ok(())
191        } else {
192            anyhow::bail!("Plugin '{}' not found", name)
193        }
194    }
195    
196    /// Disables a plugin by name
197    pub fn disable_plugin(&mut self, name: &str) -> Result<()> {
198        if let Some(config) = self.configs.get_mut(name) {
199            config.enabled = false;
200            self.save_configs()?;
201            Ok(())
202        } else {
203            anyhow::bail!("Plugin '{}' not found", name)
204        }
205    }
206    
207    /// Executes a command provided by a plugin
208    pub fn execute_command(&self, plugin_name: &str, command: &str, args: &[String]) -> Result<()> {
209        // Find the plugin
210        let plugin = self.plugins.iter()
211            .find(|p| p.name() == plugin_name)
212            .ok_or_else(|| anyhow::anyhow!("Plugin '{}' not found", plugin_name))?;
213        
214        // Check if the plugin is enabled
215        let enabled = self.configs.get(plugin_name)
216            .map(|c| c.enabled)
217            .unwrap_or(false);
218        
219        if !enabled {
220            anyhow::bail!("Plugin '{}' is disabled", plugin_name);
221        }
222        
223        // Execute the command
224        plugin.execute(command, args)
225    }
226    
227    /// Returns a list of all commands provided by all enabled plugins
228    pub fn list_commands(&self) -> Vec<(String, Vec<PluginCommand>)> {
229        self.plugins.iter()
230            .filter(|p| {
231                self.configs.get(p.name())
232                    .map(|c| c.enabled)
233                    .unwrap_or(false)
234            })
235            .map(|p| (p.name().to_string(), p.commands()))
236            .collect()
237    }
238    
239    /// Cleans up all plugins
240    pub fn cleanup(&mut self) -> Result<()> {
241        for plugin in &mut self.plugins {
242            plugin.cleanup()?;
243        }
244        Ok(())
245    }
246}
247
248impl Drop for PluginManager {
249    fn drop(&mut self) {
250        // Attempt to clean up plugins when the manager is dropped
251        let _ = self.cleanup();
252    }
253}