1use anyhow::{Context, Result};
65use std::collections::HashMap;
66use std::env;
67use std::path::{Path, PathBuf};
68use std::process::{Command, Stdio};
69
70pub struct PluginManager {
72 plugin_paths: Vec<PathBuf>,
73 plugins: HashMap<String, Plugin>,
74}
75
76#[derive(Debug, Clone)]
78pub struct Plugin {
79 name: String,
80 path: PathBuf,
81 description: Option<String>,
82}
83
84impl Plugin {
85 pub fn new(name: String, path: PathBuf) -> Self {
87 Self {
88 name,
89 path,
90 description: None,
91 }
92 }
93
94 pub fn name(&self) -> &str {
96 &self.name
97 }
98
99 pub fn path(&self) -> &Path {
101 &self.path
102 }
103
104 pub fn description(&self) -> Option<&str> {
106 self.description.as_deref()
107 }
108
109 pub fn with_description(mut self, desc: String) -> Self {
111 self.description = Some(desc);
112 self
113 }
114
115 pub fn execute(&self, args: &[String], config: &crate::config::Config) -> Result<i32> {
117 let mut cmd = Command::new(&self.path);
118 cmd.args(args);
119
120 cmd.env("IPFRS_DATA_DIR", &config.general.data_dir);
122 cmd.env("IPFRS_LOG_LEVEL", &config.general.log_level);
123
124 if let Some(api_url) = &config.api.remote_url {
125 cmd.env("IPFRS_API_URL", api_url);
126 }
127
128 if let Some(api_token) = &config.api.api_token {
129 cmd.env("IPFRS_API_TOKEN", api_token);
130 }
131
132 cmd.stdin(Stdio::inherit())
134 .stdout(Stdio::inherit())
135 .stderr(Stdio::inherit());
136
137 let status = cmd
138 .status()
139 .with_context(|| format!("Failed to execute plugin '{}'", self.name))?;
140
141 Ok(status.code().unwrap_or(1))
142 }
143
144 pub fn query_metadata(&mut self) -> Result<()> {
147 let output = Command::new(&self.path).arg("--plugin-info").output();
148
149 if let Ok(output) = output {
150 if output.status.success() {
151 if let Ok(info) = serde_json::from_slice::<HashMap<String, String>>(&output.stdout)
152 {
153 if let Some(desc) = info.get("description") {
154 self.description = Some(desc.clone());
155 }
156 }
157 }
158 }
159
160 Ok(())
161 }
162}
163
164impl PluginManager {
165 pub fn new() -> Self {
167 let mut plugin_paths = Vec::new();
168
169 if let Some(home) = dirs::home_dir() {
171 plugin_paths.push(home.join(".ipfrs").join("plugins"));
172 }
173
174 #[cfg(unix)]
176 {
177 plugin_paths.push(PathBuf::from("/usr/local/lib/ipfrs/plugins"));
178 }
179
180 if let Ok(custom_paths) = env::var("IPFRS_PLUGIN_PATH") {
182 for path in custom_paths.split(':') {
183 if !path.is_empty() {
184 plugin_paths.push(PathBuf::from(path));
185 }
186 }
187 }
188
189 Self {
190 plugin_paths,
191 plugins: HashMap::new(),
192 }
193 }
194
195 pub fn add_plugin_path(&mut self, path: PathBuf) {
197 if !self.plugin_paths.contains(&path) {
198 self.plugin_paths.push(path);
199 }
200 }
201
202 pub fn discover_plugins(&mut self) -> Vec<&Plugin> {
204 self.plugins.clear();
205
206 for plugin_dir in &self.plugin_paths {
207 if !plugin_dir.exists() {
208 continue;
209 }
210
211 if let Ok(entries) = std::fs::read_dir(plugin_dir) {
212 for entry in entries.flatten() {
213 let path = entry.path();
214
215 if !path.is_file() {
217 continue;
218 }
219
220 #[cfg(unix)]
221 {
222 use std::os::unix::fs::PermissionsExt;
223 if let Ok(metadata) = path.metadata() {
224 let permissions = metadata.permissions();
225 if permissions.mode() & 0o111 == 0 {
227 continue;
228 }
229 }
230 }
231
232 if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
234 if let Some(plugin_name) = filename.strip_prefix("ipfrs-plugin-") {
235 let mut plugin = Plugin::new(plugin_name.to_string(), path.clone());
236
237 let _ = plugin.query_metadata();
239
240 self.plugins.insert(plugin_name.to_string(), plugin);
241 }
242 }
243 }
244 }
245 }
246
247 self.plugins.values().collect()
248 }
249
250 pub fn get_plugin(&self, name: &str) -> Option<&Plugin> {
252 self.plugins.get(name)
253 }
254
255 pub fn list_plugins(&self) -> Vec<&str> {
257 self.plugins.keys().map(|s| s.as_str()).collect()
258 }
259
260 pub fn execute_plugin(
262 &self,
263 name: &str,
264 args: &[String],
265 config: &crate::config::Config,
266 ) -> Result<i32> {
267 let plugin = self
268 .get_plugin(name)
269 .with_context(|| format!("Plugin '{}' not found", name))?;
270
271 plugin.execute(args, config)
272 }
273}
274
275impl Default for PluginManager {
276 fn default() -> Self {
277 Self::new()
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284
285 #[test]
286 fn test_plugin_creation() {
287 let plugin = Plugin::new(
288 "test".to_string(),
289 PathBuf::from("/usr/local/lib/ipfrs/plugins/ipfrs-plugin-test"),
290 );
291
292 assert_eq!(plugin.name(), "test");
293 assert_eq!(
294 plugin.path(),
295 Path::new("/usr/local/lib/ipfrs/plugins/ipfrs-plugin-test")
296 );
297 assert!(plugin.description().is_none());
298 }
299
300 #[test]
301 fn test_plugin_with_description() {
302 let plugin = Plugin::new("test".to_string(), PathBuf::from("/tmp/plugin"))
303 .with_description("A test plugin".to_string());
304
305 assert_eq!(plugin.description(), Some("A test plugin"));
306 }
307
308 #[test]
309 fn test_plugin_manager_creation() {
310 let manager = PluginManager::new();
311 assert!(!manager.plugin_paths.is_empty());
312 }
313
314 #[test]
315 fn test_add_plugin_path() {
316 let mut manager = PluginManager::new();
317 let custom_path = PathBuf::from("/custom/plugins");
318
319 manager.add_plugin_path(custom_path.clone());
320 assert!(manager.plugin_paths.contains(&custom_path));
321
322 let initial_count = manager.plugin_paths.len();
324 manager.add_plugin_path(custom_path.clone());
325 assert_eq!(manager.plugin_paths.len(), initial_count);
326 }
327
328 #[test]
329 fn test_plugin_manager_default() {
330 let manager = PluginManager::default();
331 assert!(!manager.plugin_paths.is_empty());
332 }
333
334 #[test]
335 fn test_list_plugins_empty() {
336 let manager = PluginManager::new();
337 assert_eq!(manager.list_plugins().len(), 0);
338 }
339
340 #[test]
341 fn test_get_plugin_not_found() {
342 let manager = PluginManager::new();
343 assert!(manager.get_plugin("nonexistent").is_none());
344 }
345}