Skip to main content

vtcode_core/plugins/
directory.rs

1//! Plugin template and directory structure utilities for VT Code
2//!
3//! Provides utilities for creating and validating plugin directory structures
4//! according to VT Code's plugin system specification.
5
6use hashbrown::HashMap;
7use std::path::{Path, PathBuf};
8
9use tokio::fs;
10
11use crate::plugins::{PluginError, PluginManifest, PluginResult};
12
13/// Plugin template generator
14pub struct PluginTemplate;
15
16impl PluginTemplate {
17    /// Create a new plugin with the standard directory structure
18    pub async fn create_plugin_skeleton(
19        plugin_dir: &Path,
20        manifest: &PluginManifest,
21    ) -> PluginResult<()> {
22        // Create the plugin directory
23        fs::create_dir_all(plugin_dir).await.map_err(|e| {
24            PluginError::LoadingError(format!("Failed to create plugin directory: {}", e))
25        })?;
26
27        // Create the .vtcode-plugin directory for the manifest
28        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        // Write the plugin manifest
34        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        // Create standard directories if specified in manifest
44        Self::create_standard_directories(plugin_dir, manifest).await?;
45
46        // Create example files
47        Self::create_example_files(plugin_dir, manifest).await?;
48
49        Ok(())
50    }
51
52    /// Create standard plugin directories
53    async fn create_standard_directories(
54        plugin_dir: &Path,
55        manifest: &PluginManifest,
56    ) -> PluginResult<()> {
57        // Create commands directory
58        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        // Create agents directory
66        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        // Create skills directory
74        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        // Create hooks directory
82        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    /// Create example files for the plugin
93    async fn create_example_files(
94        plugin_dir: &Path,
95        manifest: &PluginManifest,
96    ) -> PluginResult<()> {
97        // Create an example command if commands directory exists
98        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        // Create an example agent if agents directory exists
138        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        // Create an example skill if skills directory exists
170        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        // Create an example hooks config if hooks directory exists
209        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        // Create an example MCP config if MCP servers are specified
240        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        // Create an example LSP config if LSP servers are specified
259        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    /// Determine if commands directory should be created by default
281    fn should_create_default_commands_dir(manifest: &PluginManifest) -> bool {
282        // Create default commands directory if no explicit commands are specified
283        // but the plugin might benefit from having commands
284        manifest.commands.is_none()
285    }
286
287    /// Determine if agents directory should be created by default
288    fn should_create_default_agents_dir(manifest: &PluginManifest) -> bool {
289        // Create default agents directory if no explicit agents are specified
290        // but the plugin might benefit from having agents
291        manifest.agents.is_none()
292    }
293
294    /// Determine if skills directory should be created by default
295    fn should_create_default_skills_dir(manifest: &PluginManifest) -> bool {
296        // Create default skills directory if no explicit skills are specified
297        // but the plugin might benefit from having skills
298        manifest.skills.is_none()
299    }
300
301    /// Validate plugin directory structure
302    pub async fn validate_plugin_structure(plugin_dir: &Path) -> PluginResult<()> {
303        // Check if plugin directory exists
304        if !plugin_dir.exists() {
305            return Err(PluginError::LoadingError(format!(
306                "Plugin directory does not exist: {}",
307                plugin_dir.display()
308            )));
309        }
310
311        // Check if manifest exists
312        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        // Validate manifest can be parsed
321        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
333/// Plugin directory utilities
334pub struct PluginDirectory;
335
336impl PluginDirectory {
337    /// Get the standard plugin directory structure
338    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    /// Create a plugin from a template
354    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}