Skip to main content

vtcode_core/plugins/
components.rs

1//! Plugin component handlers for VT Code
2//!
3//! This module handles the different types of components that plugins can provide:
4//! - Commands (slash commands)
5//! - Agents (specialized profiles)
6//! - Skills (model-invoked capabilities)
7//! - Hooks (event handlers)
8//! - MCP servers (Model Context Protocol)
9
10use std::path::{Path, PathBuf};
11
12use anyhow::Result;
13use tokio::fs;
14
15use crate::plugins::PluginManifest;
16
17/// Handler for plugin commands (slash commands)
18pub struct CommandsHandler;
19
20impl CommandsHandler {
21    /// Process plugin commands from the plugin directory
22    pub async fn process_commands(plugin_path: &Path, manifest_commands: Option<Vec<String>>) -> Result<Vec<PathBuf>> {
23        let mut command_files = Vec::new();
24
25        // Add commands from manifest paths
26        if let Some(manifest_paths) = manifest_commands {
27            for path in manifest_paths {
28                let full_path = plugin_path.join(&path);
29                if fs::metadata(&full_path)
30                    .await
31                    .map(|metadata| metadata.is_file())
32                    .unwrap_or(false)
33                {
34                    command_files.push(full_path);
35                }
36            }
37        }
38
39        // Also look for commands in the default commands/ directory
40        let default_commands_dir = plugin_path.join("commands");
41        if fs::metadata(&default_commands_dir)
42            .await
43            .map(|metadata| metadata.is_dir())
44            .unwrap_or(false)
45        {
46            let mut entries = fs::read_dir(&default_commands_dir).await?;
47            while let Some(entry) = entries.next_entry().await? {
48                let path = entry.path();
49                if fs::metadata(&path).await.map(|metadata| metadata.is_file()).unwrap_or(false)
50                    && path.extension().is_some_and(|ext| ext == "md")
51                {
52                    command_files.push(path);
53                }
54            }
55        }
56
57        Ok(command_files)
58    }
59}
60
61/// Handler for plugin agents.
62pub struct AgentsHandler;
63
64impl AgentsHandler {
65    /// Process plugin agents from the plugin directory
66    pub async fn process_agents(plugin_path: &Path, manifest_agents: Option<Vec<String>>) -> Result<Vec<PathBuf>> {
67        let mut agent_files = Vec::new();
68
69        // Add agents from manifest paths
70        if let Some(manifest_paths) = manifest_agents {
71            for path in manifest_paths {
72                let full_path = plugin_path.join(&path);
73                if fs::metadata(&full_path)
74                    .await
75                    .map(|metadata| metadata.is_file())
76                    .unwrap_or(false)
77                {
78                    agent_files.push(full_path);
79                }
80            }
81        }
82
83        // Also look for agents in the default agents/ directory
84        let default_agents_dir = plugin_path.join("agents");
85        if fs::metadata(&default_agents_dir)
86            .await
87            .map(|metadata| metadata.is_dir())
88            .unwrap_or(false)
89        {
90            let mut entries = fs::read_dir(&default_agents_dir).await?;
91            while let Some(entry) = entries.next_entry().await? {
92                let path = entry.path();
93                if fs::metadata(&path).await.map(|metadata| metadata.is_file()).unwrap_or(false)
94                    && path.extension().is_some_and(|ext| ext == "md")
95                {
96                    agent_files.push(path);
97                }
98            }
99        }
100
101        Ok(agent_files)
102    }
103}
104
105/// Handler for plugin skills
106pub struct SkillsHandler;
107
108impl SkillsHandler {
109    /// Process plugin skills from the plugin directory
110    pub async fn process_skills(plugin_path: &Path, manifest_skills: Option<Vec<String>>) -> Result<Vec<PathBuf>> {
111        let mut skill_dirs = Vec::new();
112
113        // Add skills from manifest paths
114        if let Some(manifest_paths) = manifest_skills {
115            for path in manifest_paths {
116                let full_path = plugin_path.join(&path);
117                if fs::metadata(&full_path)
118                    .await
119                    .map(|metadata| metadata.is_dir())
120                    .unwrap_or(false)
121                {
122                    skill_dirs.push(full_path);
123                }
124            }
125        }
126
127        // Also look for skills in the default skills/ directory
128        let default_skills_dir = plugin_path.join("skills");
129        if fs::metadata(&default_skills_dir)
130            .await
131            .map(|metadata| metadata.is_dir())
132            .unwrap_or(false)
133        {
134            let mut entries = fs::read_dir(&default_skills_dir).await?;
135            while let Some(entry) = entries.next_entry().await? {
136                let path = entry.path();
137                if fs::metadata(&path).await.map(|metadata| metadata.is_dir()).unwrap_or(false) {
138                    // Check if it contains a SKILL.md file
139                    let skill_md = path.join("SKILL.md");
140                    if fs::metadata(&skill_md)
141                        .await
142                        .map(|metadata| metadata.is_file())
143                        .unwrap_or(false)
144                    {
145                        skill_dirs.push(path);
146                    }
147                }
148            }
149        }
150
151        Ok(skill_dirs)
152    }
153}
154
155/// Handler for plugin hooks
156pub struct HooksHandler;
157
158impl HooksHandler {
159    /// Process plugin hooks from the plugin directory
160    pub async fn process_hooks(
161        plugin_path: &Path,
162        manifest_hooks: Option<serde_json::Value>,
163    ) -> Result<Option<PathBuf>> {
164        // Check for hooks in manifest
165        if let Some(hooks_config) = manifest_hooks {
166            // If hooks config is a string, treat it as a path
167            if let Some(path_str) = hooks_config.as_str() {
168                let hooks_path = plugin_path.join(path_str);
169                if fs::metadata(&hooks_path)
170                    .await
171                    .map(|metadata| metadata.is_file())
172                    .unwrap_or(false)
173                {
174                    return Ok(Some(hooks_path));
175                }
176            }
177        }
178
179        // Look for hooks in the default hooks/ directory
180        let default_hooks_path = plugin_path.join("hooks/hooks.json");
181        if fs::metadata(&default_hooks_path)
182            .await
183            .map(|metadata| metadata.is_file())
184            .unwrap_or(false)
185        {
186            return Ok(Some(default_hooks_path));
187        }
188
189        Ok(None)
190    }
191}
192
193/// Handler for plugin MCP servers
194pub struct McpServersHandler;
195
196impl McpServersHandler {
197    /// Process plugin MCP servers from the plugin directory
198    pub async fn process_mcp_servers(
199        plugin_path: &Path,
200        manifest_mcp: Option<serde_json::Value>,
201    ) -> Result<Option<PathBuf>> {
202        // Check for MCP config in manifest
203        if let Some(mcp_config) = manifest_mcp {
204            // If MCP config is a string, treat it as a path
205            if let Some(path_str) = mcp_config.as_str() {
206                let mcp_path = plugin_path.join(path_str);
207                if fs::metadata(&mcp_path)
208                    .await
209                    .map(|metadata| metadata.is_file())
210                    .unwrap_or(false)
211                {
212                    return Ok(Some(mcp_path));
213                }
214            }
215        }
216
217        // Look for MCP config in the default .mcp.json file
218        let default_mcp_path = plugin_path.join(".mcp.json");
219        if fs::metadata(&default_mcp_path)
220            .await
221            .map(|metadata| metadata.is_file())
222            .unwrap_or(false)
223        {
224            return Ok(Some(default_mcp_path));
225        }
226
227        Ok(None)
228    }
229}
230
231/// Handler for plugin LSP servers
232pub struct LspServersHandler;
233
234impl LspServersHandler {
235    /// Process plugin LSP servers from the plugin directory
236    pub async fn process_lsp_servers(
237        plugin_path: &Path,
238        manifest_lsp: Option<serde_json::Value>,
239    ) -> Result<Option<PathBuf>> {
240        // Check for LSP config in manifest
241        if let Some(lsp_config) = manifest_lsp {
242            // If LSP config is a string, treat it as a path
243            if let Some(path_str) = lsp_config.as_str() {
244                let lsp_path = plugin_path.join(path_str);
245                if fs::metadata(&lsp_path)
246                    .await
247                    .map(|metadata| metadata.is_file())
248                    .unwrap_or(false)
249                {
250                    return Ok(Some(lsp_path));
251                }
252            }
253        }
254
255        // Look for LSP config in the default .lsp.json file
256        let default_lsp_path = plugin_path.join(".lsp.json");
257        if fs::metadata(&default_lsp_path)
258            .await
259            .map(|metadata| metadata.is_file())
260            .unwrap_or(false)
261        {
262            return Ok(Some(default_lsp_path));
263        }
264
265        Ok(None)
266    }
267}
268
269/// A comprehensive handler that processes all plugin components
270pub struct PluginComponentsHandler;
271
272impl PluginComponentsHandler {
273    /// Process all components for a plugin
274    pub async fn process_all_components<P: AsRef<Path>>(
275        plugin_path: P,
276        manifest: &PluginManifest,
277    ) -> Result<PluginComponents> {
278        let path_buf = plugin_path.as_ref().to_path_buf();
279        let commands = CommandsHandler::process_commands(&path_buf, manifest.commands.clone()).await?;
280
281        let agents = AgentsHandler::process_agents(&path_buf, manifest.agents.clone()).await?;
282
283        let skills = SkillsHandler::process_skills(&path_buf, manifest.skills.clone()).await?;
284
285        let hooks = HooksHandler::process_hooks(
286            &path_buf,
287            manifest.hooks.as_ref().map(|h| match h {
288                crate::plugins::manifest::HookConfig::Path(path) => serde_json::Value::String(path.clone()),
289                crate::plugins::manifest::HookConfig::Inline(_) => serde_json::Value::Null, // For inline, we'll handle separately
290            }),
291        )
292        .await?;
293
294        let mcp_servers = McpServersHandler::process_mcp_servers(
295            &path_buf,
296            manifest.mcp_servers.as_ref().map(|m| match m {
297                crate::plugins::manifest::McpServerConfig::Path(path) => serde_json::Value::String(path.clone()),
298                crate::plugins::manifest::McpServerConfig::Inline(_) => serde_json::Value::Null, // For inline, we'll handle separately
299            }),
300        )
301        .await?;
302
303        let lsp_servers = LspServersHandler::process_lsp_servers(
304            &path_buf,
305            manifest.lsp_servers.as_ref().map(|l| match l {
306                crate::plugins::manifest::LspServerConfig::Path(path) => serde_json::Value::String(path.clone()),
307                crate::plugins::manifest::LspServerConfig::Inline(_) => serde_json::Value::Null, // For inline, we'll handle separately
308            }),
309        )
310        .await?;
311
312        Ok(PluginComponents {
313            commands,
314            agents,
315            skills,
316            hooks,
317            mcp_servers,
318            lsp_servers,
319        })
320    }
321}
322
323/// Structure containing all plugin components
324pub struct PluginComponents {
325    pub commands: Vec<PathBuf>,
326    pub agents: Vec<PathBuf>,
327    pub skills: Vec<PathBuf>,
328    pub hooks: Option<PathBuf>,
329    pub mcp_servers: Option<PathBuf>,
330    pub lsp_servers: Option<PathBuf>,
331}