vtcode_core/plugins/
directory.rs1use hashbrown::HashMap;
7use std::path::{Path, PathBuf};
8
9use tokio::fs;
10
11use crate::plugins::{PluginError, PluginManifest, PluginResult, validate_plugin_component};
12
13pub struct PluginTemplate;
15
16impl PluginTemplate {
17 pub async fn create_plugin_skeleton(plugin_dir: &Path, manifest: &PluginManifest) -> PluginResult<()> {
19 fs::create_dir_all(plugin_dir)
21 .await
22 .map_err(|e| PluginError::LoadingError(format!("Failed to create plugin directory: {e}")))?;
23
24 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 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 Self::create_standard_directories(plugin_dir, manifest).await?;
39
40 Self::create_example_files(plugin_dir, manifest).await?;
42
43 Ok(())
44 }
45
46 async fn create_standard_directories(plugin_dir: &Path, manifest: &PluginManifest) -> PluginResult<()> {
48 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 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 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 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 async fn create_example_files(plugin_dir: &Path, manifest: &PluginManifest) -> PluginResult<()> {
85 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 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 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 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 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 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 fn should_create_default_commands_dir(manifest: &PluginManifest) -> bool {
255 manifest.commands.is_none()
258 }
259
260 fn should_create_default_agents_dir(manifest: &PluginManifest) -> bool {
262 manifest.agents.is_none()
265 }
266
267 fn should_create_default_skills_dir(manifest: &PluginManifest) -> bool {
269 manifest.skills.is_none()
272 }
273
274 pub async fn validate_plugin_structure(plugin_dir: &Path) -> PluginResult<()> {
276 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 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 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
305pub struct PluginDirectory;
307
308impl PluginDirectory {
309 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 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}