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