strixonomy_plugin/
discovery.rs1use crate::manifest::{parse_manifest, DiscoveredPlugin};
2use std::fs;
3use std::path::{Path, PathBuf};
4use thiserror::Error;
5
6pub const PLUGIN_DIR: &str = ".strixonomy/plugins";
8
9#[derive(Debug, Error)]
10pub enum PluginDiscoveryError {
11 #[error("IO error reading {path}: {source}")]
12 Io { path: PathBuf, source: std::io::Error },
13 #[error("invalid manifest {path}: {message}")]
14 InvalidManifest { path: PathBuf, message: String },
15}
16
17pub fn discover_plugins(workspace: &Path) -> Result<Vec<DiscoveredPlugin>, PluginDiscoveryError> {
19 let plugins_dir = strixonomy_core::plugins_dir(workspace);
20 if !plugins_dir.is_dir() {
21 return Ok(Vec::new());
22 }
23 let mut discovered = Vec::new();
24 for entry in fs::read_dir(&plugins_dir)
25 .map_err(|source| PluginDiscoveryError::Io { path: plugins_dir.clone(), source })?
26 {
27 let entry = entry
28 .map_err(|source| PluginDiscoveryError::Io { path: plugins_dir.clone(), source })?;
29 let path = entry.path();
30 if path.extension().and_then(|e| e.to_str()) != Some("toml") {
31 continue;
32 }
33 let text = fs::read_to_string(&path)
34 .map_err(|source| PluginDiscoveryError::Io { path: path.clone(), source })?;
35 let manifest = parse_manifest(&text).map_err(|e| {
36 PluginDiscoveryError::InvalidManifest { path: path.clone(), message: e.to_string() }
37 })?;
38 discovered.push(DiscoveredPlugin { manifest, manifest_path: path });
39 }
40 Ok(discovered)
41}
42
43#[cfg(test)]
44mod tests {
45 use super::*;
46
47 #[test]
48 fn discovers_plugins_dir() {
49 let dir = tempfile::tempdir().unwrap();
50 let plugins = dir.path().join(PLUGIN_DIR);
51 fs::create_dir_all(&plugins).unwrap();
52 fs::write(
53 plugins.join("demo.toml"),
54 r#"
55[plugin]
56id = "demo"
57name = "Demo"
58version = "0.1.0"
59kind = "validator"
60entry = "./demo"
61"#,
62 )
63 .unwrap();
64 let found = discover_plugins(dir.path()).unwrap();
65 assert_eq!(found.len(), 1);
66 assert_eq!(found[0].manifest.id.as_deref(), Some("demo"));
67 }
68}