vtcode_core/plugins/
manager.rs1use std::path::{Path, PathBuf};
7use std::sync::Arc;
8
9use anyhow::{Context, Result};
10use tokio::sync::RwLock;
11use tracing::{debug, info, warn};
12use vtcode_commons::MultiErrors;
13
14use super::{PluginCache, PluginLoader, PluginManifest, PluginResult, PluginRuntime};
15use crate::config::PluginRuntimeConfig;
16
17pub struct PluginManager {
19 runtime: Arc<PluginRuntime>,
21 loader: Arc<PluginLoader>,
23 cache: Arc<RwLock<PluginCache>>,
25 refresh_worker: Arc<RwLock<RefreshWorkerState>>,
27}
28
29#[derive(Debug, Default)]
32struct RefreshWorkerState {
33 is_idle: bool,
35}
36
37impl PluginManager {
38 pub fn new(config: PluginRuntimeConfig, base_dir: PathBuf) -> Result<Self> {
40 let runtime = Arc::new(PluginRuntime::new(config.clone(), base_dir.join("runtime")));
41 let cache = Arc::new(RwLock::new(PluginCache::new(base_dir.join("cache"))));
42
43 let loader = Arc::new(PluginLoader::new(base_dir.join("installed"), runtime.as_ref().clone()));
44
45 Ok(Self {
46 runtime,
47 loader,
48 cache,
49 refresh_worker: Arc::new(RwLock::new(RefreshWorkerState::default())),
50 })
51 }
52
53 pub async fn install_plugin(&self, source: super::loader::PluginSource, name: Option<String>) -> PluginResult<()> {
55 self.loader.install_plugin(source, name).await?;
57 Ok(())
58 }
59
60 pub async fn uninstall_plugin(&self, plugin_name: &str) -> PluginResult<()> {
62 self.loader.uninstall_plugin(plugin_name).await?;
64 Ok(())
65 }
66
67 pub async fn enable_plugin(&self, plugin_name: &str) -> PluginResult<()> {
69 self.runtime.enable_plugin(plugin_name).await?;
70 Ok(())
71 }
72
73 pub async fn disable_plugin(&self, plugin_name: &str) -> PluginResult<()> {
75 self.runtime.disable_plugin(plugin_name).await?;
76 Ok(())
77 }
78
79 pub async fn load_plugin(&self, plugin_path: &Path) -> PluginResult<()> {
81 self.runtime.load_plugin(plugin_path).await?;
82 Ok(())
83 }
84
85 pub async fn get_plugin(&self, plugin_id: &str) -> PluginResult<super::runtime::PluginHandle> {
87 self.runtime.get_plugin(plugin_id).await
88 }
89
90 pub async fn list_installed_plugins(&self) -> PluginResult<Vec<String>> {
92 self.loader.list_installed_plugins().await
93 }
94
95 pub async fn list_loaded_plugins(&self) -> Vec<super::runtime::PluginHandle> {
97 self.runtime.list_plugins().await
98 }
99
100 pub async fn process_plugin_components(
102 &self,
103 plugin_path: &Path,
104 manifest: &PluginManifest,
105 ) -> Result<super::components::PluginComponents> {
106 super::components::PluginComponentsHandler::process_all_components(plugin_path, manifest).await
107 }
108
109 pub async fn is_plugin_enabled(&self, plugin_id: &str) -> bool {
111 self.runtime.is_plugin_enabled(plugin_id).await
112 }
113
114 pub async fn cache_plugin(&self, plugin_id: &str, source_path: &Path) -> PluginResult<PathBuf> {
116 let mut cache = self.cache.write().await;
117 cache.cache_plugin(plugin_id, source_path).await
118 }
119
120 pub async fn get_cached_plugin(&self, plugin_id: &str) -> Option<PathBuf> {
122 let cache = self.cache.read().await;
123 cache.get_cached_plugin(plugin_id).cloned()
124 }
125
126 pub async fn refresh_non_curated_plugin_cache(&self, roots: &[PathBuf]) -> Result<RefreshResult> {
139 {
141 let worker = self.refresh_worker.read().await;
142 if !worker.is_idle {
143 debug!("non-curated plugin cache refresh already in progress, skipping");
144 return Ok(RefreshResult::SkippedAlreadyInProgress);
145 }
146 }
147
148 {
150 let mut worker = self.refresh_worker.write().await;
151 if !worker.is_idle {
152 return Ok(RefreshResult::SkippedAlreadyInProgress);
153 }
154 worker.is_idle = false;
155 }
156
157 let result = self.refresh_non_curated_from_roots_impl(roots).await;
158
159 let mut worker = self.refresh_worker.write().await;
160 worker.is_idle = true;
161
162 result
163 }
164
165 async fn refresh_non_curated_from_roots_impl(&self, roots: &[PathBuf]) -> Result<RefreshResult> {
167 if roots.is_empty() {
168 debug!("no workspace roots provided for non-curated plugin cache refresh");
169 return Ok(RefreshResult::NoRootsProvided);
170 }
171
172 let mut refreshed_count = 0usize;
173 let mut errors: MultiErrors<String> = MultiErrors::new();
174
175 for root in roots {
176 if !tokio::fs::try_exists(root).await.unwrap_or(false) {
177 debug!("workspace root does not exist, skipping: {}", root.display());
178 continue;
179 }
180
181 match self.scan_root_for_plugins(root).await {
182 Ok(plugins) => {
183 for plugin_info in &plugins {
184 if let Some(existing) = self.get_cached_plugin(&plugin_info.name).await
186 && tokio::fs::try_exists(&existing).await.unwrap_or(false)
187 && plugin_info.version_matches_existing(&existing).await
188 {
189 debug!("plugin '{}' version unchanged, skipping cache update", plugin_info.name);
190 continue;
191 }
192
193 if let Err(e) = self.cache_plugin(&plugin_info.name, &plugin_info.path).await {
194 errors.push(format!("failed to cache plugin '{}': {e}", plugin_info.name));
195 } else {
196 refreshed_count += 1;
197 info!("cached non-curated plugin: {}", plugin_info.name);
198 }
199 }
200 }
201 Err(e) => {
202 errors.push(format!("failed to scan root {}: {e}", root.display()));
203 }
204 }
205 }
206
207 Ok(RefreshResult::Success { refreshed_count, errors })
208 }
209
210 async fn scan_root_for_plugins(&self, root: &Path) -> Result<Vec<DiscoveredPluginInfo>> {
212 let mut discovered = Vec::new();
213
214 let plugin_roots = vec![root.join(".vtcode").join("plugins"), root.join("plugins")];
216
217 for plugin_root in plugin_roots {
218 if !tokio::fs::try_exists(&plugin_root).await.unwrap_or(false) {
219 continue;
220 }
221
222 let entries = match tokio::fs::read_dir(&plugin_root).await {
224 Ok(entries) => entries,
225 Err(e) => {
226 warn!("Failed to read plugin root {}: {}", plugin_root.display(), e);
227 continue;
228 }
229 };
230
231 let mut dirs = Vec::new();
233 let mut entries = entries;
234 while let Ok(Some(entry)) = entries.next_entry().await {
235 if entry.file_type().await.is_ok_and(|ft| ft.is_dir()) {
236 dirs.push(entry.path());
237 }
238 }
239
240 for plugin_dir in dirs {
241 let manifest_path = plugin_dir.join(".vtcode-plugin").join("plugin.json");
242
243 if !tokio::fs::try_exists(&manifest_path).await.unwrap_or(false) {
244 continue;
245 }
246
247 match self.load_plugin_manifest(&manifest_path).await {
248 Ok(info) => discovered.push(info),
249 Err(e) => {
250 warn!("Failed to load plugin manifest from {}: {}", manifest_path.display(), e);
251 }
252 }
253 }
254 }
255
256 Ok(discovered)
257 }
258
259 async fn load_plugin_manifest(&self, manifest_path: &Path) -> Result<DiscoveredPluginInfo> {
261 let content = tokio::fs::read_to_string(manifest_path)
262 .await
263 .with_context(|| format!("failed to read plugin manifest at {}", manifest_path.display()))?;
264 let manifest: PluginManifest = serde_json::from_str(&content)
265 .with_context(|| format!("failed to parse plugin manifest at {}", manifest_path.display()))?;
266
267 Ok(DiscoveredPluginInfo {
268 name: manifest.name.clone(),
269 version: manifest.version.clone(),
270 path: manifest_path
271 .parent()
272 .and_then(|p| p.parent())
273 .unwrap_or(manifest_path)
274 .to_path_buf(),
275 })
276 }
277}
278
279#[derive(Debug)]
281#[non_exhaustive]
282pub enum RefreshResult {
283 Success {
285 refreshed_count: usize,
286 errors: MultiErrors<String>,
287 },
288 SkippedAlreadyInProgress,
290 NoRootsProvided,
292}
293
294#[derive(Debug, Clone)]
296pub struct DiscoveredPluginInfo {
297 pub name: String,
299 pub version: Option<String>,
301 pub path: PathBuf,
303}
304
305impl DiscoveredPluginInfo {
306 async fn version_matches_existing(&self, existing_path: &Path) -> bool {
308 let Some(ref current_version) = self.version else {
310 return false;
311 };
312
313 let cached_manifest_path = existing_path.join(".vtcode-plugin").join("plugin.json");
315 if !tokio::fs::try_exists(&cached_manifest_path).await.unwrap_or(false) {
316 return false;
317 }
318
319 match tokio::fs::read_to_string(&cached_manifest_path).await {
320 Ok(content) => match serde_json::from_str::<PluginManifest>(&content) {
321 Ok(cached) => cached.version.as_deref() == Some(current_version),
322 Err(_) => false,
323 },
324 Err(_) => false,
325 }
326 }
327}