Skip to main content

claude_agent/plugins/
discovery.rs

1use std::path::{Path, PathBuf};
2
3use super::PluginError;
4use super::manifest::{PLUGIN_CONFIG_DIR, PluginDescriptor, PluginManifest};
5
6pub struct PluginDiscovery;
7
8impl PluginDiscovery {
9    /// Returns the default plugins directory: `~/.claude/plugins/`.
10    pub fn default_plugins_dir() -> Option<PathBuf> {
11        crate::common::home_dir().map(|home| home.join(".claude").join("plugins"))
12    }
13
14    pub fn discover(dirs: &[PathBuf]) -> Result<Vec<PluginDescriptor>, PluginError> {
15        let mut descriptors = Vec::new();
16
17        for dir in dirs {
18            if !dir.exists() {
19                continue;
20            }
21
22            if Self::is_plugin_root(dir) {
23                let manifest = PluginManifest::load(dir)?;
24                descriptors.push(PluginDescriptor::new(manifest, dir.clone()));
25            } else {
26                Self::scan_children(dir, &mut descriptors)?;
27            }
28        }
29
30        Ok(descriptors)
31    }
32
33    fn is_plugin_root(dir: &Path) -> bool {
34        dir.join(PLUGIN_CONFIG_DIR).is_dir()
35    }
36
37    fn scan_children(
38        parent: &Path,
39        descriptors: &mut Vec<PluginDescriptor>,
40    ) -> Result<(), PluginError> {
41        let entries = std::fs::read_dir(parent)?;
42
43        for entry in entries {
44            let entry = entry?;
45            let path = entry.path();
46            if path.is_dir() && Self::is_plugin_root(&path) {
47                let manifest = PluginManifest::load(&path)?;
48                descriptors.push(PluginDescriptor::new(manifest, path));
49            }
50        }
51
52        Ok(())
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59    use tempfile::tempdir;
60
61    fn create_plugin(parent: &Path, name: &str) -> PathBuf {
62        let plugin_dir = parent.join(name);
63        let config_dir = plugin_dir.join(PLUGIN_CONFIG_DIR);
64        std::fs::create_dir_all(&config_dir).unwrap();
65        std::fs::write(
66            config_dir.join("plugin.json"),
67            format!(
68                r#"{{"name":"{}","description":"Test","version":"1.0.0"}}"#,
69                name
70            ),
71        )
72        .unwrap();
73        plugin_dir
74    }
75
76    #[test]
77    fn test_discover_direct_plugin_root() {
78        let dir = tempdir().unwrap();
79        let plugin_dir = create_plugin(dir.path(), "my-plugin");
80
81        let descriptors = PluginDiscovery::discover(&[plugin_dir]).unwrap();
82        assert_eq!(descriptors.len(), 1);
83        assert_eq!(descriptors[0].name(), "my-plugin");
84    }
85
86    #[test]
87    fn test_discover_parent_directory() {
88        let dir = tempdir().unwrap();
89        create_plugin(dir.path(), "plugin-a");
90        create_plugin(dir.path(), "plugin-b");
91
92        let descriptors = PluginDiscovery::discover(&[dir.path().to_path_buf()]).unwrap();
93        assert_eq!(descriptors.len(), 2);
94        let names: Vec<&str> = descriptors.iter().map(|d| d.name()).collect();
95        assert!(names.contains(&"plugin-a"));
96        assert!(names.contains(&"plugin-b"));
97    }
98
99    #[test]
100    fn test_discover_nonexistent_dir() {
101        let descriptors = PluginDiscovery::discover(&[PathBuf::from("/nonexistent/path")]).unwrap();
102        assert!(descriptors.is_empty());
103    }
104
105    #[test]
106    fn test_discover_empty_dir() {
107        let dir = tempdir().unwrap();
108        let descriptors = PluginDiscovery::discover(&[dir.path().to_path_buf()]).unwrap();
109        assert!(descriptors.is_empty());
110    }
111
112    #[test]
113    fn test_discover_mixed_dirs() {
114        let dir = tempdir().unwrap();
115        create_plugin(dir.path(), "real-plugin");
116        // non-plugin subdirectory
117        std::fs::create_dir(dir.path().join("not-a-plugin")).unwrap();
118
119        let descriptors = PluginDiscovery::discover(&[dir.path().to_path_buf()]).unwrap();
120        assert_eq!(descriptors.len(), 1);
121        assert_eq!(descriptors[0].name(), "real-plugin");
122    }
123
124    #[test]
125    fn test_discover_multiple_dirs() {
126        let dir1 = tempdir().unwrap();
127        let dir2 = tempdir().unwrap();
128        create_plugin(dir1.path(), "p1");
129        create_plugin(dir2.path(), "p2");
130
131        let descriptors =
132            PluginDiscovery::discover(&[dir1.path().to_path_buf(), dir2.path().to_path_buf()])
133                .unwrap();
134        assert_eq!(descriptors.len(), 2);
135    }
136
137    #[test]
138    fn test_default_plugins_dir() {
139        let dir = PluginDiscovery::default_plugins_dir();
140        assert!(dir.is_some());
141        let path = dir.unwrap();
142        assert!(path.ends_with(".claude/plugins"));
143    }
144}