vtcode_core/plugins/
runtime.rs1use hashbrown::HashMap;
6use std::path::{Path, PathBuf};
7use std::sync::Arc;
8
9use tokio::sync::RwLock;
10
11use super::{PluginError, PluginId, PluginManifest, PluginResult};
12use crate::config::PluginRuntimeConfig;
13use crate::utils::file_utils::read_file_with_context;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum PluginState {
18 Active,
20 Installed,
22 Disabled,
24 Error,
26}
27
28#[derive(Debug, Clone)]
30pub struct PluginHandle {
31 pub id: PluginId,
33 pub manifest: PluginManifest,
35 pub path: PathBuf,
37 pub state: PluginState,
39 pub loaded_at: Option<std::time::SystemTime>,
41}
42
43#[derive(Debug, Clone)]
45pub struct PluginRuntime {
46 plugins: Arc<RwLock<HashMap<PluginId, PluginHandle>>>,
48}
49
50impl PluginRuntime {
51 pub fn new(_config: PluginRuntimeConfig, _base_dir: PathBuf) -> Self {
53 Self {
54 plugins: Arc::new(RwLock::new(HashMap::new())),
55 }
56 }
57
58 pub async fn load_plugin(&self, plugin_path: &Path) -> PluginResult<PluginHandle> {
60 if !plugin_path.exists() {
62 return Err(PluginError::NotFound(plugin_path.display().to_string()));
63 }
64
65 let manifest = self.load_manifest(plugin_path).await?;
67
68 self.validate_manifest(&manifest)?;
70
71 let handle = PluginHandle {
73 id: manifest.name.clone(),
74 manifest: manifest.clone(),
75 path: plugin_path.to_path_buf(),
76 state: PluginState::Active,
77 loaded_at: Some(std::time::SystemTime::now()),
78 };
79
80 {
82 let mut plugins = self.plugins.write().await;
83 plugins.insert(manifest.name.clone(), handle.clone());
84 }
85
86 Ok(handle)
87 }
88
89 async fn load_manifest(&self, plugin_path: &Path) -> PluginResult<PluginManifest> {
91 let manifest_path = plugin_path.join(".vtcode-plugin/plugin.json");
92
93 if !manifest_path.exists() {
94 return Err(PluginError::ManifestValidationError(format!(
95 "Plugin manifest not found at: {}",
96 manifest_path.display()
97 )));
98 }
99
100 let manifest_content = read_file_with_context(&manifest_path, "plugin manifest")
101 .await
102 .map_err(|e| PluginError::LoadingError(format!("Failed to read manifest: {}", e)))?;
103
104 let manifest: PluginManifest = serde_json::from_str(&manifest_content).map_err(|e| {
105 PluginError::ManifestValidationError(format!("Invalid manifest JSON: {}", e))
106 })?;
107
108 Ok(manifest)
109 }
110
111 fn validate_manifest(&self, manifest: &PluginManifest) -> PluginResult<()> {
113 if manifest.name.is_empty() {
114 return Err(PluginError::ManifestValidationError(
115 "Plugin name is required".to_string(),
116 ));
117 }
118
119 if !self.is_valid_plugin_name(&manifest.name) {
121 return Err(PluginError::ManifestValidationError(
122 "Plugin name must be in kebab-case (lowercase with hyphens)".to_string(),
123 ));
124 }
125
126 Ok(())
127 }
128
129 fn is_valid_plugin_name(&self, name: &str) -> bool {
131 name.chars()
133 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
134 && !name.starts_with('-')
135 && !name.ends_with('-')
136 && !name.is_empty()
137 }
138
139 pub async fn unload_plugin(&self, plugin_id: &str) -> PluginResult<()> {
141 let mut plugins = self.plugins.write().await;
142 if plugins.remove(plugin_id).is_none() {
143 return Err(PluginError::NotFound(plugin_id.to_string()));
144 }
145 Ok(())
146 }
147
148 pub async fn get_plugin(&self, plugin_id: &str) -> PluginResult<PluginHandle> {
150 let plugins = self.plugins.read().await;
151 plugins
152 .get(plugin_id)
153 .cloned()
154 .ok_or_else(|| PluginError::NotFound(plugin_id.to_string()))
155 }
156
157 pub async fn list_plugins(&self) -> Vec<PluginHandle> {
159 let plugins = self.plugins.read().await;
160 plugins.values().cloned().collect()
161 }
162
163 pub async fn enable_plugin(&self, plugin_id: &str) -> PluginResult<()> {
165 let mut plugins = self.plugins.write().await;
166 if let Some(handle) = plugins.get_mut(plugin_id) {
167 handle.state = PluginState::Active;
168 Ok(())
169 } else {
170 Err(PluginError::NotFound(plugin_id.to_string()))
171 }
172 }
173
174 pub async fn disable_plugin(&self, plugin_id: &str) -> PluginResult<()> {
176 let mut plugins = self.plugins.write().await;
177 if let Some(handle) = plugins.get_mut(plugin_id) {
178 handle.state = PluginState::Disabled;
179 Ok(())
180 } else {
181 Err(PluginError::NotFound(plugin_id.to_string()))
182 }
183 }
184
185 pub async fn is_plugin_enabled(&self, plugin_id: &str) -> bool {
187 if let Ok(handle) = self.get_plugin(plugin_id).await {
188 handle.state == PluginState::Active
189 } else {
190 false
191 }
192 }
193}