1#![allow(dead_code)]
3
4use std::collections::HashSet;
5use std::path::Path;
6use std::sync::Mutex;
7
8use once_cell::sync::Lazy;
9
10use super::frontmatter_parser::parse_frontmatter;
11use super::loader::load_all_plugins_cache_only;
12use super::plugin_options_storage::{
13 load_plugin_options, substitute_plugin_variables, substitute_user_config_in_content,
14};
15use super::walk_plugin_markdown::{WalkPluginMarkdownOpts, walk_plugin_markdown};
16use crate::plugin::types::PluginManifest;
17
18static PLUGIN_COMMAND_CACHE: Lazy<Mutex<Option<Vec<Command>>>> = Lazy::new(|| Mutex::new(None));
19
20#[derive(Clone, Debug)]
22pub struct Command {
23 pub command_type: String,
24 pub name: String,
25 pub description: String,
26 pub content: String,
27 pub source: String,
28 pub plugin: Option<String>,
29 pub is_hidden: bool,
30 pub allowed_tools: Vec<String>,
31}
32
33pub async fn load_plugin_commands() -> Result<Vec<Command>, Box<dyn std::error::Error + Send + Sync>>
35{
36 {
37 let cache = PLUGIN_COMMAND_CACHE.lock().unwrap();
38 if let Some(ref commands) = *cache {
39 return Ok(commands.clone());
40 }
41 }
42
43 let plugin_result = load_all_plugins_cache_only().await?;
44 let mut all_commands = Vec::new();
45
46 for plugin in &plugin_result.enabled {
47 let mut loaded_paths = HashSet::new();
48
49 if let Some(ref commands_path) = plugin.commands_path {
51 match load_commands_from_directory(
52 Path::new(commands_path),
53 &plugin.name,
54 &plugin.source,
55 &plugin.manifest,
56 &plugin.path,
57 &mut loaded_paths,
58 false,
59 )
60 .await
61 {
62 Ok(commands) => {
63 log::debug!(
64 "Loaded {} commands from plugin {} default directory",
65 commands.len(),
66 plugin.name
67 );
68 all_commands.extend(commands);
69 }
70 Err(e) => log::debug!(
71 "Failed to load commands from plugin {} default directory: {}",
72 plugin.name,
73 e
74 ),
75 }
76 }
77
78 if let Some(ref commands_paths) = plugin.commands_paths {
80 for command_path in commands_paths {
81 if let Ok(commands) = load_commands_from_path(
82 command_path,
83 &plugin.name,
84 &plugin.source,
85 &plugin.manifest,
86 &plugin.path,
87 &mut loaded_paths,
88 )
89 .await
90 {
91 all_commands.extend(commands);
92 }
93 }
94 }
95 }
96
97 log::debug!("Total plugin commands loaded: {}", all_commands.len());
98
99 {
100 let mut cache = PLUGIN_COMMAND_CACHE.lock().unwrap();
101 *cache = Some(all_commands.clone());
102 }
103
104 Ok(all_commands)
105}
106
107async fn load_commands_from_directory(
108 commands_path: &Path,
109 plugin_name: &str,
110 source_name: &str,
111 plugin_manifest: &PluginManifest,
112 plugin_path: &str,
113 loaded_paths: &mut HashSet<String>,
114 is_skill_mode: bool,
115) -> Result<Vec<Command>, Box<dyn std::error::Error + Send + Sync>> {
116 use std::sync::Arc;
117 use tokio::sync::Mutex;
118
119 let commands: Arc<Mutex<Vec<Command>>> = Arc::new(Mutex::new(Vec::new()));
120
121 walk_plugin_markdown(
122 commands_path,
123 |full_path, _namespace| {
124 let plugin_name = plugin_name.to_string();
125 let source_name = source_name.to_string();
126 let manifest = plugin_manifest.clone();
127 let plugin_path = plugin_path.to_string();
128 let base_dir = commands_path.to_path_buf();
129 let commands = Arc::clone(&commands);
130
131 Box::pin(async move {
132 let path_str = full_path.clone();
133
134 if let Ok(content) = tokio::fs::read_to_string(&full_path).await {
135 let command_name =
136 get_command_name_from_file(Path::new(&full_path), &base_dir, &plugin_name);
137
138 if let Ok(Some(command)) = create_plugin_command(
139 &command_name,
140 &content,
141 &path_str,
142 &source_name,
143 &manifest,
144 &plugin_path,
145 false,
146 is_skill_mode,
147 )
148 .await
149 {
150 commands.lock().await.push(command);
151 }
152 }
153 })
154 },
155 WalkPluginMarkdownOpts {
156 stop_at_skill_dir: Some(false),
157 log_label: Some("commands".to_string()),
158 },
159 )
160 .await
161 .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)?;
162
163 Ok(Arc::try_unwrap(commands).unwrap().into_inner())
164}
165
166fn get_command_name_from_file(file_path: &Path, base_dir: &Path, plugin_name: &str) -> String {
167 let is_skill = is_skill_file(file_path);
168
169 let command_base_name = if is_skill {
170 file_path
171 .parent()
172 .and_then(|p| p.file_name())
173 .map(|n| n.to_string_lossy().to_string())
174 .unwrap_or_default()
175 } else {
176 file_path
177 .file_stem()
178 .map(|s| s.to_string_lossy().to_string())
179 .unwrap_or_default()
180 };
181
182 let relative_path = file_path
183 .parent()
184 .and_then(|p| p.strip_prefix(base_dir).ok())
185 .map(|p| p.to_string_lossy().to_string())
186 .unwrap_or_default();
187
188 let namespace = if relative_path.is_empty() {
189 String::new()
190 } else {
191 relative_path.replace('/', ":")
192 };
193
194 if namespace.is_empty() {
195 format!("{}:{}", plugin_name, command_base_name)
196 } else {
197 format!("{}:{}:{}", plugin_name, namespace, command_base_name)
198 }
199}
200
201fn is_skill_file(file_path: &Path) -> bool {
202 file_path
203 .file_name()
204 .map(|n| n.to_string_lossy().to_string())
205 .map(|n| n.to_lowercase() == "skill.md")
206 .unwrap_or(false)
207}
208
209async fn create_plugin_command(
210 command_name: &str,
211 content: &str,
212 file_path: &str,
213 source_name: &str,
214 plugin_manifest: &PluginManifest,
215 plugin_path: &str,
216 is_skill: bool,
217 is_skill_mode: bool,
218) -> Result<Option<Command>, Box<dyn std::error::Error + Send + Sync>> {
219 let (frontmatter, markdown_content) = parse_frontmatter(content, file_path);
220
221 let description = frontmatter
222 .get("description")
223 .and_then(|v| v.as_str())
224 .unwrap_or(if is_skill {
225 "Plugin skill"
226 } else {
227 "Plugin command"
228 })
229 .to_string();
230
231 let mut final_content = if is_skill_mode {
232 if let Some(parent) = std::path::Path::new(file_path).parent() {
233 format!(
234 "Base directory for this skill: {}\n\n{}",
235 parent.display(),
236 markdown_content
237 )
238 } else {
239 markdown_content.to_string()
240 }
241 } else {
242 markdown_content.to_string()
243 };
244
245 final_content = substitute_plugin_variables(&final_content, plugin_path, source_name);
247
248 if plugin_manifest.user_config.is_some() {
250 let options = load_plugin_options(source_name);
251 final_content = substitute_user_config_in_content(
252 &final_content,
253 &options,
254 plugin_manifest.user_config.as_ref().unwrap(),
255 );
256 }
257
258 Ok(Some(Command {
259 command_type: "prompt".to_string(),
260 name: command_name.to_string(),
261 description,
262 content: final_content,
263 source: "plugin".to_string(),
264 plugin: Some(source_name.to_string()),
265 is_hidden: false,
266 allowed_tools: Vec::new(),
267 }))
268}
269
270async fn load_commands_from_path(
271 command_path: &str,
272 plugin_name: &str,
273 source_name: &str,
274 plugin_manifest: &PluginManifest,
275 plugin_path: &str,
276 loaded_paths: &mut HashSet<String>,
277) -> Result<Vec<Command>, Box<dyn std::error::Error + Send + Sync>> {
278 let metadata = tokio::fs::metadata(command_path)
279 .await
280 .map_err(|e| format!("Failed to stat {}: {}", command_path, e))?;
281
282 if metadata.is_dir() {
283 load_commands_from_directory(
284 Path::new(command_path),
285 plugin_name,
286 source_name,
287 plugin_manifest,
288 plugin_path,
289 loaded_paths,
290 false,
291 )
292 .await
293 } else if metadata.is_file()
294 && Path::new(command_path)
295 .extension()
296 .map(|e| e.to_string_lossy() == "md")
297 .unwrap_or(false)
298 {
299 if loaded_paths.contains(command_path) {
300 return Ok(Vec::new());
301 }
302 loaded_paths.insert(command_path.to_string());
303
304 let content = tokio::fs::read_to_string(command_path)
305 .await
306 .map_err(|e| format!("Failed to read {}: {}", command_path, e))?;
307 let path_str = command_path.to_string();
308 let command_name = format!(
309 "{}:{}",
310 plugin_name,
311 Path::new(command_path)
312 .file_stem()
313 .map(|s| s.to_string_lossy().to_string())
314 .unwrap_or_default()
315 );
316
317 match create_plugin_command(
318 &command_name,
319 &content,
320 &path_str,
321 source_name,
322 plugin_manifest,
323 plugin_path,
324 false,
325 false,
326 )
327 .await
328 {
329 Ok(Some(cmd)) => Ok(vec![cmd]),
330 _ => Ok(Vec::new()),
331 }
332 } else {
333 Ok(Vec::new())
334 }
335}
336
337pub fn clear_plugin_command_cache() {
339 let mut cache = PLUGIN_COMMAND_CACHE.lock().unwrap();
340 *cache = None;
341}