vtcode_core/plugins/
components.rs1use std::path::{Path, PathBuf};
11
12use anyhow::Result;
13use tokio::fs;
14
15use crate::plugins::PluginManifest;
16
17pub struct CommandsHandler;
19
20impl CommandsHandler {
21 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 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 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
61pub struct AgentsHandler;
63
64impl AgentsHandler {
65 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 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 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
105pub struct SkillsHandler;
107
108impl SkillsHandler {
109 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 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 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 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
155pub struct HooksHandler;
157
158impl HooksHandler {
159 pub async fn process_hooks(
161 plugin_path: &Path,
162 manifest_hooks: Option<serde_json::Value>,
163 ) -> Result<Option<PathBuf>> {
164 if let Some(hooks_config) = manifest_hooks {
166 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 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
193pub struct McpServersHandler;
195
196impl McpServersHandler {
197 pub async fn process_mcp_servers(
199 plugin_path: &Path,
200 manifest_mcp: Option<serde_json::Value>,
201 ) -> Result<Option<PathBuf>> {
202 if let Some(mcp_config) = manifest_mcp {
204 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 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
231pub struct LspServersHandler;
233
234impl LspServersHandler {
235 pub async fn process_lsp_servers(
237 plugin_path: &Path,
238 manifest_lsp: Option<serde_json::Value>,
239 ) -> Result<Option<PathBuf>> {
240 if let Some(lsp_config) = manifest_lsp {
242 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 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
269pub struct PluginComponentsHandler;
271
272impl PluginComponentsHandler {
273 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, }),
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, }),
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, }),
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
323pub 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}