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 { plugins: Arc::new(RwLock::new(HashMap::new())) }
54 }
55
56 pub async fn load_plugin(&self, plugin_path: &Path) -> PluginResult<PluginHandle> {
58 if !tokio::fs::try_exists(plugin_path).await.unwrap_or(false) {
60 return Err(PluginError::NotFound(plugin_path.display().to_string().into()));
61 }
62
63 let manifest = self.load_manifest(plugin_path).await?;
65
66 self.validate_manifest(&manifest)?;
68
69 let handle = PluginHandle {
71 id: manifest.name.clone().into(),
72 manifest: manifest.clone(),
73 path: plugin_path.to_path_buf(),
74 state: PluginState::Active,
75 loaded_at: Some(std::time::SystemTime::now()),
76 };
77
78 {
80 let mut plugins = self.plugins.write().await;
81 plugins.insert(manifest.name.clone().into(), handle.clone());
82 }
83
84 Ok(handle)
85 }
86
87 async fn load_manifest(&self, plugin_path: &Path) -> PluginResult<PluginManifest> {
89 let manifest_path = plugin_path.join(".vtcode-plugin/plugin.json");
90
91 if !tokio::fs::try_exists(&manifest_path).await.unwrap_or(false) {
92 return Err(PluginError::ManifestValidationError(format!(
93 "Plugin manifest not found at: {}",
94 manifest_path.display()
95 )));
96 }
97
98 let manifest_content = read_file_with_context(&manifest_path, "plugin manifest")
99 .await
100 .map_err(|e| PluginError::LoadingError(format!("Failed to read manifest: {e}")))?;
101
102 let manifest: PluginManifest = serde_json::from_str(&manifest_content)
103 .map_err(|e| PluginError::ManifestValidationError(format!("Invalid manifest JSON: {e}")))?;
104
105 Ok(manifest)
106 }
107
108 fn validate_manifest(&self, manifest: &PluginManifest) -> PluginResult<()> {
110 if manifest.name.is_empty() {
111 return Err(PluginError::ManifestValidationError("Plugin name is required".to_string()));
112 }
113
114 if !self.is_valid_plugin_name(&manifest.name) {
116 return Err(PluginError::ManifestValidationError(
117 "Plugin name must be in kebab-case (lowercase with hyphens)".to_string(),
118 ));
119 }
120
121 Ok(())
122 }
123
124 fn is_valid_plugin_name(&self, name: &str) -> bool {
126 name.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
128 && !name.starts_with('-')
129 && !name.ends_with('-')
130 && !name.is_empty()
131 }
132
133 pub async fn unload_plugin(&self, plugin_id: &str) -> PluginResult<()> {
135 let mut plugins = self.plugins.write().await;
136 if plugins.remove(plugin_id).is_none() {
137 return Err(PluginError::NotFound(plugin_id.to_string().into()));
138 }
139 Ok(())
140 }
141
142 pub async fn get_plugin(&self, plugin_id: &str) -> PluginResult<PluginHandle> {
144 let plugins = self.plugins.read().await;
145 plugins
146 .get(plugin_id)
147 .cloned()
148 .ok_or_else(|| PluginError::NotFound(plugin_id.to_string().into()))
149 }
150
151 pub async fn list_plugins(&self) -> Vec<PluginHandle> {
153 let plugins = self.plugins.read().await;
154 plugins.values().cloned().collect()
155 }
156
157 pub async fn enable_plugin(&self, plugin_id: &str) -> PluginResult<()> {
159 let mut plugins = self.plugins.write().await;
160 if let Some(handle) = plugins.get_mut(plugin_id) {
161 handle.state = PluginState::Active;
162 Ok(())
163 } else {
164 Err(PluginError::NotFound(plugin_id.to_string().into()))
165 }
166 }
167
168 pub async fn disable_plugin(&self, plugin_id: &str) -> PluginResult<()> {
170 let mut plugins = self.plugins.write().await;
171 if let Some(handle) = plugins.get_mut(plugin_id) {
172 handle.state = PluginState::Disabled;
173 Ok(())
174 } else {
175 Err(PluginError::NotFound(plugin_id.to_string().into()))
176 }
177 }
178
179 pub async fn is_plugin_enabled(&self, plugin_id: &str) -> bool {
181 if let Ok(handle) = self.get_plugin(plugin_id).await {
182 handle.state == PluginState::Active
183 } else {
184 false
185 }
186 }
187}