vtcode_core/plugins/
directory.rs1use hashbrown::HashMap;
7use std::path::{Path, PathBuf};
8
9use tokio::fs;
10
11use crate::plugins::{PluginError, PluginManifest, PluginResult};
12
13pub struct PluginTemplate;
15
16impl PluginTemplate {
17 pub async fn create_plugin_skeleton(
19 plugin_dir: &Path,
20 manifest: &PluginManifest,
21 ) -> PluginResult<()> {
22 fs::create_dir_all(plugin_dir).await.map_err(|e| {
24 PluginError::LoadingError(format!("Failed to create plugin directory: {}", e))
25 })?;
26
27 let vtcode_plugin_dir = plugin_dir.join(".vtcode-plugin");
29 fs::create_dir_all(&vtcode_plugin_dir).await.map_err(|e| {
30 PluginError::LoadingError(format!("Failed to create .vtcode-plugin directory: {}", e))
31 })?;
32
33 let manifest_path = vtcode_plugin_dir.join("plugin.json");
35 let manifest_json =
36 serde_json::to_string_pretty(manifest).map_err(PluginError::JsonError)?;
37 fs::write(&manifest_path, &manifest_json)
38 .await
39 .map_err(|e| {
40 PluginError::LoadingError(format!("Failed to write plugin manifest: {}", e))
41 })?;
42
43 Self::create_standard_directories(plugin_dir, manifest).await?;
45
46 Self::create_example_files(plugin_dir, manifest).await?;
48
49 Ok(())
50 }
51
52 async fn create_standard_directories(
54 plugin_dir: &Path,
55 manifest: &PluginManifest,
56 ) -> PluginResult<()> {
57 if manifest.commands.is_some() || Self::should_create_default_commands_dir(manifest) {
59 let commands_dir = plugin_dir.join("commands");
60 fs::create_dir_all(&commands_dir).await.map_err(|e| {
61 PluginError::LoadingError(format!("Failed to create commands directory: {}", e))
62 })?;
63 }
64
65 if manifest.agents.is_some() || Self::should_create_default_agents_dir(manifest) {
67 let agents_dir = plugin_dir.join("agents");
68 fs::create_dir_all(&agents_dir).await.map_err(|e| {
69 PluginError::LoadingError(format!("Failed to create agents directory: {}", e))
70 })?;
71 }
72
73 if manifest.skills.is_some() || Self::should_create_default_skills_dir(manifest) {
75 let skills_dir = plugin_dir.join("skills");
76 fs::create_dir_all(&skills_dir).await.map_err(|e| {
77 PluginError::LoadingError(format!("Failed to create skills directory: {}", e))
78 })?;
79 }
80
81 if manifest.hooks.is_some() {
83 let hooks_dir = plugin_dir.join("hooks");
84 fs::create_dir_all(&hooks_dir).await.map_err(|e| {
85 PluginError::LoadingError(format!("Failed to create hooks directory: {}", e))
86 })?;
87 }
88
89 Ok(())
90 }
91
92 async fn create_example_files(
94 plugin_dir: &Path,
95 manifest: &PluginManifest,
96 ) -> PluginResult<()> {
97 let commands_dir = plugin_dir.join("commands");
99 if commands_dir.exists() {
100 let example_command = commands_dir.join("example.md");
101 if !example_command.exists() {
102 let example_content = format!(
103 r#"---
104name: {plugin_name}-example
105description: Example command for {plugin_name} plugin
106parameters:
107 - name: input
108 type: string
109 description: Example input parameter
110---
111
112# {plugin_name} Example Command
113
114This is an example command for the {plugin_name} plugin.
115
116## Usage
117
118`/{plugin_name}-example <input>`
119
120## Description
121
122This command demonstrates how to create a plugin command for VT Code.
123"#,
124 plugin_name = manifest.name
125 );
126 fs::write(&example_command, example_content)
127 .await
128 .map_err(|e| {
129 PluginError::LoadingError(format!(
130 "Failed to create example command: {}",
131 e
132 ))
133 })?;
134 }
135 }
136
137 let agents_dir = plugin_dir.join("agents");
139 if agents_dir.exists() {
140 let example_agent = agents_dir.join("example.md");
141 if !example_agent.exists() {
142 let example_content = format!(
143 r#"---
144description: Example agent for {plugin_name} plugin
145capabilities: ["example-task", "demo-capability"]
146---
147
148# {plugin_name} Example Agent
149
150This is an example agent for the {plugin_name} plugin.
151
152## Capabilities
153- Perform example tasks
154- Demonstrate agent functionality
155
156## Context and examples
157This agent can be used to demonstrate how agents work in VT Code plugins.
158"#,
159 plugin_name = manifest.name
160 );
161 fs::write(&example_agent, example_content)
162 .await
163 .map_err(|e| {
164 PluginError::LoadingError(format!("Failed to create example agent: {}", e))
165 })?;
166 }
167 }
168
169 let skills_dir = plugin_dir.join("skills");
171 if skills_dir.exists() {
172 let example_skill_dir = skills_dir.join("example-skill");
173 fs::create_dir_all(&example_skill_dir).await.map_err(|e| {
174 PluginError::LoadingError(format!(
175 "Failed to create example skill directory: {}",
176 e
177 ))
178 })?;
179
180 let skill_md = example_skill_dir.join("SKILL.md");
181 if !skill_md.exists() {
182 let example_content = format!(
183 r#"---
184name: {plugin_name}-example-skill
185description: Example skill for {plugin_name} plugin
186parameters:
187 - name: input
188 type: string
189 description: Example input parameter
190---
191
192# {plugin_name} Example Skill
193
194This is an example skill for the {plugin_name} plugin.
195
196## Purpose
197
198This skill demonstrates how to create a model-invoked capability in VT Code.
199"#,
200 plugin_name = manifest.name
201 );
202 fs::write(&skill_md, example_content).await.map_err(|e| {
203 PluginError::LoadingError(format!("Failed to create example skill: {}", e))
204 })?;
205 }
206 }
207
208 let hooks_dir = plugin_dir.join("hooks");
210 if hooks_dir.exists() {
211 let hooks_config = hooks_dir.join("hooks.json");
212 if !hooks_config.exists() {
213 let example_content = r#"{
214 "hooks": {
215 "PostToolUse": [
216 {
217 "matcher": "Write|Edit",
218 "hooks": [
219 {
220 "type": "command",
221 "command": "${VTCODE_PLUGIN_ROOT}/scripts/post-edit.sh"
222 }
223 ]
224 }
225 ]
226 }
227}"#;
228 fs::write(&hooks_config, example_content)
229 .await
230 .map_err(|e| {
231 PluginError::LoadingError(format!(
232 "Failed to create example hooks config: {}",
233 e
234 ))
235 })?;
236 }
237 }
238
239 if manifest.mcp_servers.is_some() {
241 let mcp_config = plugin_dir.join(".mcp.json");
242 if !mcp_config.exists() {
243 let example_content = r#"{
244 "example-server": {
245 "command": "node",
246 "args": ["${VTCODE_PLUGIN_ROOT}/mcp-server.js"],
247 "env": {
248 "PLUGIN_ROOT": "${VTCODE_PLUGIN_ROOT}"
249 }
250 }
251}"#;
252 fs::write(&mcp_config, example_content).await.map_err(|e| {
253 PluginError::LoadingError(format!("Failed to create example MCP config: {}", e))
254 })?;
255 }
256 }
257
258 if manifest.lsp_servers.is_some() {
260 let lsp_config = plugin_dir.join(".lsp.json");
261 if !lsp_config.exists() {
262 let example_content = r#"{
263 "example-lsp": {
264 "command": "example-lsp-server",
265 "args": ["--stdio"],
266 "extensionToLanguage": {
267 ".example": "example"
268 }
269 }
270}"#;
271 fs::write(&lsp_config, example_content).await.map_err(|e| {
272 PluginError::LoadingError(format!("Failed to create example LSP config: {}", e))
273 })?;
274 }
275 }
276
277 Ok(())
278 }
279
280 fn should_create_default_commands_dir(manifest: &PluginManifest) -> bool {
282 manifest.commands.is_none()
285 }
286
287 fn should_create_default_agents_dir(manifest: &PluginManifest) -> bool {
289 manifest.agents.is_none()
292 }
293
294 fn should_create_default_skills_dir(manifest: &PluginManifest) -> bool {
296 manifest.skills.is_none()
299 }
300
301 pub async fn validate_plugin_structure(plugin_dir: &Path) -> PluginResult<()> {
303 if !plugin_dir.exists() {
305 return Err(PluginError::LoadingError(format!(
306 "Plugin directory does not exist: {}",
307 plugin_dir.display()
308 )));
309 }
310
311 let manifest_path = plugin_dir.join(".vtcode-plugin/plugin.json");
313 if !manifest_path.exists() {
314 return Err(PluginError::ManifestValidationError(format!(
315 "Plugin manifest not found at: {}",
316 manifest_path.display()
317 )));
318 }
319
320 let manifest_content = fs::read_to_string(&manifest_path)
322 .await
323 .map_err(|e| PluginError::LoadingError(format!("Failed to read manifest: {}", e)))?;
324
325 let _manifest: PluginManifest = serde_json::from_str(&manifest_content).map_err(|e| {
326 PluginError::ManifestValidationError(format!("Invalid manifest JSON: {}", e))
327 })?;
328
329 Ok(())
330 }
331}
332
333pub struct PluginDirectory;
335
336impl PluginDirectory {
337 pub fn get_standard_structure() -> HashMap<&'static str, &'static str> {
339 let mut structure = HashMap::new();
340 structure.insert(".vtcode-plugin/", "Plugin manifest directory (required)");
341 structure.insert("commands/", "Slash command Markdown files");
342 structure.insert("agents/", "Agent profile Markdown files");
343 structure.insert("skills/", "Agent Skills with SKILL.md files");
344 structure.insert("hooks/", "Hook configurations");
345 structure.insert("scripts/", "Hook and utility scripts");
346 structure.insert("LICENSE", "License file");
347 structure.insert("CHANGELOG.md", "Version history");
348 structure.insert(".mcp.json", "MCP server definitions");
349 structure.insert(".lsp.json", "LSP server configurations");
350 structure
351 }
352
353 pub async fn create_from_template(
355 base_dir: &Path,
356 plugin_name: &str,
357 description: &str,
358 ) -> PluginResult<PathBuf> {
359 let plugin_dir = base_dir.join(plugin_name);
360
361 let manifest = PluginManifest {
362 name: plugin_name.to_string(),
363 version: Some("1.0.0".to_string()),
364 description: Some(description.to_string()),
365 author: None,
366 homepage: None,
367 repository: None,
368 license: Some("MIT".to_string()),
369 keywords: Some(vec!["vtcode".to_string(), "plugin".to_string()]),
370 commands: None,
371 agents: None,
372 skills: None,
373 hooks: None,
374 mcp_servers: None,
375 output_styles: None,
376 lsp_servers: None,
377 };
378
379 PluginTemplate::create_plugin_skeleton(&plugin_dir, &manifest).await?;
380 Ok(plugin_dir)
381 }
382}