use hashbrown::HashMap;
use std::path::{Path, PathBuf};
use tokio::fs;
use crate::plugins::{PluginError, PluginManifest, PluginResult};
pub struct PluginTemplate;
impl PluginTemplate {
pub async fn create_plugin_skeleton(
plugin_dir: &Path,
manifest: &PluginManifest,
) -> PluginResult<()> {
fs::create_dir_all(plugin_dir).await.map_err(|e| {
PluginError::LoadingError(format!("Failed to create plugin directory: {}", e))
})?;
let vtcode_plugin_dir = plugin_dir.join(".vtcode-plugin");
fs::create_dir_all(&vtcode_plugin_dir).await.map_err(|e| {
PluginError::LoadingError(format!("Failed to create .vtcode-plugin directory: {}", e))
})?;
let manifest_path = vtcode_plugin_dir.join("plugin.json");
let manifest_json =
serde_json::to_string_pretty(manifest).map_err(PluginError::JsonError)?;
fs::write(&manifest_path, &manifest_json)
.await
.map_err(|e| {
PluginError::LoadingError(format!("Failed to write plugin manifest: {}", e))
})?;
Self::create_standard_directories(plugin_dir, manifest).await?;
Self::create_example_files(plugin_dir, manifest).await?;
Ok(())
}
async fn create_standard_directories(
plugin_dir: &Path,
manifest: &PluginManifest,
) -> PluginResult<()> {
if manifest.commands.is_some() || Self::should_create_default_commands_dir(manifest) {
let commands_dir = plugin_dir.join("commands");
fs::create_dir_all(&commands_dir).await.map_err(|e| {
PluginError::LoadingError(format!("Failed to create commands directory: {}", e))
})?;
}
if manifest.agents.is_some() || Self::should_create_default_agents_dir(manifest) {
let agents_dir = plugin_dir.join("agents");
fs::create_dir_all(&agents_dir).await.map_err(|e| {
PluginError::LoadingError(format!("Failed to create agents directory: {}", e))
})?;
}
if manifest.skills.is_some() || Self::should_create_default_skills_dir(manifest) {
let skills_dir = plugin_dir.join("skills");
fs::create_dir_all(&skills_dir).await.map_err(|e| {
PluginError::LoadingError(format!("Failed to create skills directory: {}", e))
})?;
}
if manifest.hooks.is_some() {
let hooks_dir = plugin_dir.join("hooks");
fs::create_dir_all(&hooks_dir).await.map_err(|e| {
PluginError::LoadingError(format!("Failed to create hooks directory: {}", e))
})?;
}
Ok(())
}
async fn create_example_files(
plugin_dir: &Path,
manifest: &PluginManifest,
) -> PluginResult<()> {
let commands_dir = plugin_dir.join("commands");
if commands_dir.exists() {
let example_command = commands_dir.join("example.md");
if !example_command.exists() {
let example_content = format!(
r#"---
name: {plugin_name}-example
description: Example command for {plugin_name} plugin
parameters:
- name: input
type: string
description: Example input parameter
---
# {plugin_name} Example Command
This is an example command for the {plugin_name} plugin.
## Usage
`/{plugin_name}-example <input>`
## Description
This command demonstrates how to create a plugin command for VT Code.
"#,
plugin_name = manifest.name
);
fs::write(&example_command, example_content)
.await
.map_err(|e| {
PluginError::LoadingError(format!(
"Failed to create example command: {}",
e
))
})?;
}
}
let agents_dir = plugin_dir.join("agents");
if agents_dir.exists() {
let example_agent = agents_dir.join("example.md");
if !example_agent.exists() {
let example_content = format!(
r#"---
description: Example agent for {plugin_name} plugin
capabilities: ["example-task", "demo-capability"]
---
# {plugin_name} Example Agent
This is an example agent for the {plugin_name} plugin.
## Capabilities
- Perform example tasks
- Demonstrate agent functionality
## Context and examples
This agent can be used to demonstrate how agents work in VT Code plugins.
"#,
plugin_name = manifest.name
);
fs::write(&example_agent, example_content)
.await
.map_err(|e| {
PluginError::LoadingError(format!("Failed to create example agent: {}", e))
})?;
}
}
let skills_dir = plugin_dir.join("skills");
if skills_dir.exists() {
let example_skill_dir = skills_dir.join("example-skill");
fs::create_dir_all(&example_skill_dir).await.map_err(|e| {
PluginError::LoadingError(format!(
"Failed to create example skill directory: {}",
e
))
})?;
let skill_md = example_skill_dir.join("SKILL.md");
if !skill_md.exists() {
let example_content = format!(
r#"---
name: {plugin_name}-example-skill
description: Example skill for {plugin_name} plugin
parameters:
- name: input
type: string
description: Example input parameter
---
# {plugin_name} Example Skill
This is an example skill for the {plugin_name} plugin.
## Purpose
This skill demonstrates how to create a model-invoked capability in VT Code.
"#,
plugin_name = manifest.name
);
fs::write(&skill_md, example_content).await.map_err(|e| {
PluginError::LoadingError(format!("Failed to create example skill: {}", e))
})?;
}
}
let hooks_dir = plugin_dir.join("hooks");
if hooks_dir.exists() {
let hooks_config = hooks_dir.join("hooks.json");
if !hooks_config.exists() {
let example_content = r#"{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "${VTCODE_PLUGIN_ROOT}/scripts/post-edit.sh"
}
]
}
]
}
}"#;
fs::write(&hooks_config, example_content)
.await
.map_err(|e| {
PluginError::LoadingError(format!(
"Failed to create example hooks config: {}",
e
))
})?;
}
}
if manifest.mcp_servers.is_some() {
let mcp_config = plugin_dir.join(".mcp.json");
if !mcp_config.exists() {
let example_content = r#"{
"example-server": {
"command": "node",
"args": ["${VTCODE_PLUGIN_ROOT}/mcp-server.js"],
"env": {
"PLUGIN_ROOT": "${VTCODE_PLUGIN_ROOT}"
}
}
}"#;
fs::write(&mcp_config, example_content).await.map_err(|e| {
PluginError::LoadingError(format!("Failed to create example MCP config: {}", e))
})?;
}
}
if manifest.lsp_servers.is_some() {
let lsp_config = plugin_dir.join(".lsp.json");
if !lsp_config.exists() {
let example_content = r#"{
"example-lsp": {
"command": "example-lsp-server",
"args": ["--stdio"],
"extensionToLanguage": {
".example": "example"
}
}
}"#;
fs::write(&lsp_config, example_content).await.map_err(|e| {
PluginError::LoadingError(format!("Failed to create example LSP config: {}", e))
})?;
}
}
Ok(())
}
fn should_create_default_commands_dir(manifest: &PluginManifest) -> bool {
manifest.commands.is_none()
}
fn should_create_default_agents_dir(manifest: &PluginManifest) -> bool {
manifest.agents.is_none()
}
fn should_create_default_skills_dir(manifest: &PluginManifest) -> bool {
manifest.skills.is_none()
}
pub async fn validate_plugin_structure(plugin_dir: &Path) -> PluginResult<()> {
if !plugin_dir.exists() {
return Err(PluginError::LoadingError(format!(
"Plugin directory does not exist: {}",
plugin_dir.display()
)));
}
let manifest_path = plugin_dir.join(".vtcode-plugin/plugin.json");
if !manifest_path.exists() {
return Err(PluginError::ManifestValidationError(format!(
"Plugin manifest not found at: {}",
manifest_path.display()
)));
}
let manifest_content = fs::read_to_string(&manifest_path)
.await
.map_err(|e| PluginError::LoadingError(format!("Failed to read manifest: {}", e)))?;
let _manifest: PluginManifest = serde_json::from_str(&manifest_content).map_err(|e| {
PluginError::ManifestValidationError(format!("Invalid manifest JSON: {}", e))
})?;
Ok(())
}
}
pub struct PluginDirectory;
impl PluginDirectory {
pub fn get_standard_structure() -> HashMap<&'static str, &'static str> {
let mut structure = HashMap::new();
structure.insert(".vtcode-plugin/", "Plugin manifest directory (required)");
structure.insert("commands/", "Slash command Markdown files");
structure.insert("agents/", "Agent profile Markdown files");
structure.insert("skills/", "Agent Skills with SKILL.md files");
structure.insert("hooks/", "Hook configurations");
structure.insert("scripts/", "Hook and utility scripts");
structure.insert("LICENSE", "License file");
structure.insert("CHANGELOG.md", "Version history");
structure.insert(".mcp.json", "MCP server definitions");
structure.insert(".lsp.json", "LSP server configurations");
structure
}
pub async fn create_from_template(
base_dir: &Path,
plugin_name: &str,
description: &str,
) -> PluginResult<PathBuf> {
let plugin_dir = base_dir.join(plugin_name);
let manifest = PluginManifest {
name: plugin_name.to_string(),
version: Some("1.0.0".to_string()),
description: Some(description.to_string()),
author: None,
homepage: None,
repository: None,
license: Some("MIT".to_string()),
keywords: Some(vec!["vtcode".to_string(), "plugin".to_string()]),
commands: None,
agents: None,
skills: None,
hooks: None,
mcp_servers: None,
output_styles: None,
lsp_servers: None,
};
PluginTemplate::create_plugin_skeleton(&plugin_dir, &manifest).await?;
Ok(plugin_dir)
}
}