1use crate::config::PluginConfig;
4use crate::error::{PluginError, Result};
5use std::path::{Path, PathBuf};
6use tracing::{debug, info};
7
8#[derive(Debug, Clone)]
10pub struct DiscoveredPlugin {
11 pub path: PathBuf,
13
14 pub name: String,
16
17 pub is_local: bool,
19}
20
21impl DiscoveredPlugin {
22 pub fn new(path: PathBuf, is_local: bool) -> Self {
24 let name = path
25 .file_stem()
26 .and_then(|s| s.to_str())
27 .unwrap_or("unknown")
28 .to_string();
29
30 DiscoveredPlugin {
31 path,
32 name,
33 is_local,
34 }
35 }
36}
37
38pub fn discover_plugins(
40 config: &PluginConfig,
41 project_root: &Path,
42) -> Result<Vec<DiscoveredPlugin>> {
43 let mut plugins = Vec::new();
44 let paths = config.all_paths(project_root);
45 let local_dir = PluginConfig::local_plugins_dir(project_root);
46
47 for base_path in paths.iter() {
48 if !base_path.exists() {
49 debug!("Plugin path does not exist: {}", base_path.display());
50 continue;
51 }
52
53 let is_local = *base_path == local_dir;
54
55 let pattern = base_path.join("*.scm");
57 match glob::glob(&pattern.to_string_lossy()) {
58 Ok(entries) => {
59 for entry in entries.flatten() {
60 debug!("Discovered plugin: {}", entry.display());
61 plugins.push(DiscoveredPlugin::new(entry, is_local));
62 }
63 }
64 Err(e) => {
65 debug!("Failed to glob plugins in {}: {}", base_path.display(), e);
66 }
67 }
68 }
69
70 info!("Discovered {} plugins", plugins.len());
71 Ok(plugins)
72}
73
74pub fn find_plugin(
76 name: &str,
77 config: &PluginConfig,
78 project_root: &Path,
79) -> Result<DiscoveredPlugin> {
80 let paths = config.all_paths(project_root);
81 let local_dir = PluginConfig::local_plugins_dir(project_root);
82
83 let filename = if name.ends_with(".scm") {
85 name.to_string()
86 } else {
87 format!("{}.scm", name)
88 };
89
90 for base_path in paths.iter() {
91 let plugin_path = base_path.join(&filename);
92 if plugin_path.exists() {
93 let is_local = *base_path == local_dir;
94 return Ok(DiscoveredPlugin::new(plugin_path, is_local));
95 }
96 }
97
98 Err(PluginError::not_found(PathBuf::from(name)))
99}
100
101pub fn plugins_dir_exists(project_root: &Path) -> bool {
103 project_root.join(".hx").join("plugins").exists()
104}
105
106pub fn create_plugins_dir(project_root: &Path) -> Result<PathBuf> {
108 let plugins_dir = project_root.join(".hx").join("plugins");
109
110 if !plugins_dir.exists() {
111 std::fs::create_dir_all(&plugins_dir).map_err(|e| {
112 PluginError::io(
113 format!(
114 "failed to create plugins directory: {}",
115 plugins_dir.display()
116 ),
117 e,
118 )
119 })?;
120 }
121
122 Ok(plugins_dir)
123}
124
125pub struct PluginPaths {
127 pub local: PathBuf,
129 pub global: Option<PathBuf>,
131 pub custom: Vec<PathBuf>,
133}
134
135impl PluginPaths {
136 pub fn for_project(config: &PluginConfig, project_root: &Path) -> Self {
138 let local = project_root.join(".hx").join("plugins");
139
140 let global =
141 directories::BaseDirs::new().map(|dirs| dirs.config_dir().join("hx").join("plugins"));
142
143 let custom = config
144 .paths
145 .iter()
146 .map(|p| {
147 let expanded = shellexpand(p);
148 PathBuf::from(expanded)
149 })
150 .collect();
151
152 PluginPaths {
153 local,
154 global,
155 custom,
156 }
157 }
158
159 pub fn existing(&self) -> Vec<&PathBuf> {
161 let mut paths = Vec::new();
162
163 if self.local.exists() {
164 paths.push(&self.local);
165 }
166
167 for path in &self.custom {
168 if path.exists() {
169 paths.push(path);
170 }
171 }
172
173 if let Some(ref global) = self.global
174 && global.exists()
175 {
176 paths.push(global);
177 }
178
179 paths
180 }
181}
182
183fn shellexpand(path: &str) -> String {
184 if path.starts_with("~/")
185 && let Some(dirs) = directories::BaseDirs::new()
186 {
187 return format!("{}{}", dirs.home_dir().display(), &path[1..]);
188 }
189 path.to_string()
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195 use tempfile::tempdir;
196
197 #[test]
198 fn test_discover_plugins_empty() {
199 let temp = tempdir().unwrap();
200 let config = PluginConfig::default();
201 let plugins = discover_plugins(&config, temp.path()).unwrap();
202 assert!(plugins.is_empty());
203 }
204
205 #[test]
206 fn test_create_plugins_dir() {
207 let temp = tempdir().unwrap();
208 let plugins_dir = create_plugins_dir(temp.path()).unwrap();
209 assert!(plugins_dir.exists());
210 assert!(plugins_dir.ends_with(".hx/plugins"));
211 }
212}