vtcode_core/marketplace/
installer.rs1use std::path::{Path, PathBuf};
4
5use crate::tools::plugins::PluginRuntime;
6use crate::utils::file_utils::{ensure_dir_exists, write_file_with_context, write_json_file};
7use crate::utils::validation::{validate_all_non_empty, validate_non_empty, validate_path_exists};
8use anyhow::{Context, Result, bail};
9use tokio::fs;
10
11use super::PluginManifest;
12
13pub struct PluginInstaller {
15 pub plugins_dir: PathBuf,
17
18 core_plugin_runtime: Option<PluginRuntime>,
20}
21
22impl PluginInstaller {
23 pub fn new(plugins_dir: PathBuf, core_plugin_runtime: Option<PluginRuntime>) -> Self {
24 Self {
25 plugins_dir,
26 core_plugin_runtime,
27 }
28 }
29
30 pub async fn install_plugin(&self, manifest: &PluginManifest) -> Result<()> {
32 ensure_dir_exists(&self.plugins_dir).await?;
34
35 let plugin_dir = self.plugins_dir.join(&manifest.id);
37 ensure_dir_exists(&plugin_dir).await?;
38
39 self.download_plugin(manifest, &plugin_dir).await?;
41
42 let manifest_dir = plugin_dir.join(".vtcode-plugin");
44 let manifest_path = manifest_dir.join("plugin.json");
45 write_json_file(&manifest_path, manifest).await?;
46
47 self.integrate_with_core_plugin_system(&manifest_path)
49 .await?;
50
51 Ok(())
52 }
53
54 async fn integrate_with_core_plugin_system(&self, manifest_path: &Path) -> Result<()> {
56 if let Some(runtime) = &self.core_plugin_runtime {
58 let handle = runtime.register_manifest(manifest_path).await?;
60 tracing::info!(plugin_id = %handle.manifest.id, "registered plugin with core runtime");
61 } else {
62 tracing::info!(path = %manifest_path.display(), "no core plugin runtime, skipping integration");
63 }
64
65 Ok(())
66 }
67
68 async fn download_plugin(&self, manifest: &PluginManifest, plugin_dir: &Path) -> Result<()> {
70 self.validate_manifest(manifest)?;
72
73 tracing::info!(plugin_id = %manifest.id, source = %manifest.source, "downloading plugin");
74
75 if manifest.source.starts_with("http") {
77 self.download_from_http(manifest, plugin_dir).await?;
78 } else if manifest.source.starts_with("file://") {
79 self.download_from_file(manifest, plugin_dir).await?;
80 } else if Path::new(&manifest.source).exists() {
81 self.download_from_local(manifest, plugin_dir).await?;
83 } else {
84 self.download_from_git(manifest, plugin_dir).await?;
86 }
87
88 Ok(())
89 }
90
91 async fn download_from_http(&self, manifest: &PluginManifest, plugin_dir: &Path) -> Result<()> {
93 let placeholder_path = plugin_dir.join(&manifest.entrypoint);
95
96 write_file_with_context(
97 &placeholder_path,
98 &format!("# HTTP Downloaded plugin: {}\n", manifest.id),
99 "plugin entrypoint",
100 )
101 .await?;
102
103 tracing::info!(plugin_id = %manifest.id, "http download completed");
104 Ok(())
105 }
106
107 async fn download_from_file(&self, manifest: &PluginManifest, plugin_dir: &Path) -> Result<()> {
109 let source_path = PathBuf::from(&manifest.source.replace("file://", ""));
110 validate_path_exists(&source_path, "Local source file")?;
111
112 let dest_path = plugin_dir.join(&manifest.entrypoint);
113 if let Some(parent) = dest_path.parent() {
114 ensure_dir_exists(parent).await?;
115 }
116
117 fs::copy(&source_path, &dest_path).await.with_context(|| {
119 format!(
120 "Failed to copy plugin from {} to {}",
121 source_path.display(),
122 dest_path.display()
123 )
124 })?;
125
126 tracing::info!(plugin_id = %manifest.id, "local file copy completed");
127 Ok(())
128 }
129
130 async fn download_from_local(
132 &self,
133 manifest: &PluginManifest,
134 plugin_dir: &Path,
135 ) -> Result<()> {
136 let source_path = PathBuf::from(&manifest.source);
137 validate_path_exists(&source_path, "Local source path")?;
138
139 let dest_path = plugin_dir.join(&manifest.entrypoint);
140 if let Some(parent) = dest_path.parent() {
141 ensure_dir_exists(parent).await?;
142 }
143
144 fs::copy(&source_path, &dest_path).await.with_context(|| {
146 format!(
147 "Failed to copy plugin from {} to {}",
148 source_path.display(),
149 dest_path.display()
150 )
151 })?;
152
153 tracing::info!(plugin_id = %manifest.id, "local path copy completed");
154 Ok(())
155 }
156
157 async fn download_from_git(&self, manifest: &PluginManifest, plugin_dir: &Path) -> Result<()> {
159 let placeholder_path = plugin_dir.join(&manifest.entrypoint);
161
162 write_file_with_context(
163 &placeholder_path,
164 &format!("# Git downloaded plugin: {}\n", manifest.id),
165 "plugin entrypoint",
166 )
167 .await?;
168
169 tracing::info!(plugin_id = %manifest.id, "git download completed");
170 Ok(())
171 }
172
173 pub fn validate_manifest(&self, manifest: &PluginManifest) -> Result<()> {
175 validate_non_empty(&manifest.id, "Plugin ID")?;
177 validate_non_empty(&manifest.name, "Plugin name")?;
178 validate_non_empty(&manifest.source, "Plugin source URL")?;
179
180 if manifest.entrypoint.as_os_str().is_empty() {
182 bail!("Plugin manifest must have a valid entrypoint path");
183 }
184
185 if let Some(trust_level) = &manifest.trust_level {
187 match trust_level {
188 crate::config::PluginTrustLevel::Sandbox
189 | crate::config::PluginTrustLevel::Trusted
190 | crate::config::PluginTrustLevel::Untrusted => {
191 }
193 }
194 }
195
196 validate_all_non_empty(&manifest.dependencies, "Plugin dependencies")?;
198
199 Ok(())
200 }
201
202 pub async fn uninstall_plugin(&self, plugin_id: &str) -> Result<()> {
204 let plugin_dir = self.plugins_dir.join(plugin_id);
205 validate_path_exists(&plugin_dir, "Installed plugin")?;
206
207 self.remove_from_core_plugin_system(plugin_id).await?;
209
210 fs::remove_dir_all(&plugin_dir).await.with_context(|| {
211 format!(
212 "Failed to remove plugin directory: {}",
213 plugin_dir.display()
214 )
215 })?;
216
217 Ok(())
218 }
219
220 async fn remove_from_core_plugin_system(&self, plugin_id: &str) -> Result<()> {
222 if let Some(runtime) = &self.core_plugin_runtime {
224 runtime
226 .unload_plugin(plugin_id)
227 .await
228 .with_context(|| format!("Failed to unload plugin from runtime: {}", plugin_id))?;
229 tracing::info!(plugin_id = %plugin_id, "unloaded plugin from core runtime");
230 } else {
231 tracing::info!(plugin_id = %plugin_id, "no core plugin runtime, skipping removal");
232 }
233
234 Ok(())
235 }
236
237 pub async fn is_installed(&self, plugin_id: &str) -> bool {
239 let plugin_dir = self.plugins_dir.join(plugin_id);
240 plugin_dir.exists()
241 }
242}