claude_agent/plugins/
discovery.rs1use std::path::{Path, PathBuf};
2
3use super::PluginError;
4use super::manifest::{PLUGIN_CONFIG_DIR, PluginDescriptor, PluginManifest};
5
6pub struct PluginDiscovery;
7
8impl PluginDiscovery {
9 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 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}