use anyhow::{Context, Result};
use std::collections::HashMap;
use std::env;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
pub struct PluginManager {
plugin_paths: Vec<PathBuf>,
plugins: HashMap<String, Plugin>,
}
#[derive(Debug, Clone)]
pub struct Plugin {
name: String,
path: PathBuf,
description: Option<String>,
}
impl Plugin {
pub fn new(name: String, path: PathBuf) -> Self {
Self {
name,
path,
description: None,
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn description(&self) -> Option<&str> {
self.description.as_deref()
}
pub fn with_description(mut self, desc: String) -> Self {
self.description = Some(desc);
self
}
pub fn execute(&self, args: &[String], config: &crate::config::Config) -> Result<i32> {
let mut cmd = Command::new(&self.path);
cmd.args(args);
cmd.env("IPFRS_DATA_DIR", &config.general.data_dir);
cmd.env("IPFRS_LOG_LEVEL", &config.general.log_level);
if let Some(api_url) = &config.api.remote_url {
cmd.env("IPFRS_API_URL", api_url);
}
if let Some(api_token) = &config.api.api_token {
cmd.env("IPFRS_API_TOKEN", api_token);
}
cmd.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
let status = cmd
.status()
.with_context(|| format!("Failed to execute plugin '{}'", self.name))?;
Ok(status.code().unwrap_or(1))
}
pub fn query_metadata(&mut self) -> Result<()> {
let output = Command::new(&self.path).arg("--plugin-info").output();
if let Ok(output) = output {
if output.status.success() {
if let Ok(info) = serde_json::from_slice::<HashMap<String, String>>(&output.stdout)
{
if let Some(desc) = info.get("description") {
self.description = Some(desc.clone());
}
}
}
}
Ok(())
}
}
impl PluginManager {
pub fn new() -> Self {
let mut plugin_paths = Vec::new();
if let Some(home) = dirs::home_dir() {
plugin_paths.push(home.join(".ipfrs").join("plugins"));
}
#[cfg(unix)]
{
plugin_paths.push(PathBuf::from("/usr/local/lib/ipfrs/plugins"));
}
if let Ok(custom_paths) = env::var("IPFRS_PLUGIN_PATH") {
for path in custom_paths.split(':') {
if !path.is_empty() {
plugin_paths.push(PathBuf::from(path));
}
}
}
Self {
plugin_paths,
plugins: HashMap::new(),
}
}
pub fn add_plugin_path(&mut self, path: PathBuf) {
if !self.plugin_paths.contains(&path) {
self.plugin_paths.push(path);
}
}
pub fn discover_plugins(&mut self) -> Vec<&Plugin> {
self.plugins.clear();
for plugin_dir in &self.plugin_paths {
if !plugin_dir.exists() {
continue;
}
if let Ok(entries) = std::fs::read_dir(plugin_dir) {
for entry in entries.flatten() {
let path = entry.path();
if !path.is_file() {
continue;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Ok(metadata) = path.metadata() {
let permissions = metadata.permissions();
if permissions.mode() & 0o111 == 0 {
continue;
}
}
}
if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
if let Some(plugin_name) = filename.strip_prefix("ipfrs-plugin-") {
let mut plugin = Plugin::new(plugin_name.to_string(), path.clone());
let _ = plugin.query_metadata();
self.plugins.insert(plugin_name.to_string(), plugin);
}
}
}
}
}
self.plugins.values().collect()
}
pub fn get_plugin(&self, name: &str) -> Option<&Plugin> {
self.plugins.get(name)
}
pub fn list_plugins(&self) -> Vec<&str> {
self.plugins.keys().map(|s| s.as_str()).collect()
}
pub fn execute_plugin(
&self,
name: &str,
args: &[String],
config: &crate::config::Config,
) -> Result<i32> {
let plugin = self
.get_plugin(name)
.with_context(|| format!("Plugin '{}' not found", name))?;
plugin.execute(args, config)
}
}
impl Default for PluginManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_plugin_creation() {
let plugin = Plugin::new(
"test".to_string(),
PathBuf::from("/usr/local/lib/ipfrs/plugins/ipfrs-plugin-test"),
);
assert_eq!(plugin.name(), "test");
assert_eq!(
plugin.path(),
Path::new("/usr/local/lib/ipfrs/plugins/ipfrs-plugin-test")
);
assert!(plugin.description().is_none());
}
#[test]
fn test_plugin_with_description() {
let plugin = Plugin::new("test".to_string(), PathBuf::from("/tmp/plugin"))
.with_description("A test plugin".to_string());
assert_eq!(plugin.description(), Some("A test plugin"));
}
#[test]
fn test_plugin_manager_creation() {
let manager = PluginManager::new();
assert!(!manager.plugin_paths.is_empty());
}
#[test]
fn test_add_plugin_path() {
let mut manager = PluginManager::new();
let custom_path = PathBuf::from("/custom/plugins");
manager.add_plugin_path(custom_path.clone());
assert!(manager.plugin_paths.contains(&custom_path));
let initial_count = manager.plugin_paths.len();
manager.add_plugin_path(custom_path.clone());
assert_eq!(manager.plugin_paths.len(), initial_count);
}
#[test]
fn test_plugin_manager_default() {
let manager = PluginManager::default();
assert!(!manager.plugin_paths.is_empty());
}
#[test]
fn test_list_plugins_empty() {
let manager = PluginManager::new();
assert_eq!(manager.list_plugins().len(), 0);
}
#[test]
fn test_get_plugin_not_found() {
let manager = PluginManager::new();
assert!(manager.get_plugin("nonexistent").is_none());
}
}