Skip to main content

vtcode_core/marketplace/
installer.rs

1//! Plugin installer for marketplace system
2
3use 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
13/// Plugin installer that handles downloading and installing plugins from marketplaces
14pub struct PluginInstaller {
15    /// Base directory for installed plugins
16    pub plugins_dir: PathBuf,
17
18    /// Reference to the core plugin runtime for integration
19    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    /// Install a plugin from its manifest
31    pub async fn install_plugin(&self, manifest: &PluginManifest) -> Result<()> {
32        // Create plugins directory if it doesn't exist
33        ensure_dir_exists(&self.plugins_dir).await?;
34
35        // Create plugin installation directory
36        let plugin_dir = self.plugins_dir.join(&manifest.id);
37        ensure_dir_exists(&plugin_dir).await?;
38
39        // Download the plugin from its source
40        self.download_plugin(manifest, &plugin_dir).await?;
41
42        // Save the manifest to the plugin directory
43        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        // Integrate with VT Code's existing plugin system
48        self.integrate_with_core_plugin_system(&manifest_path)
49            .await?;
50
51        Ok(())
52    }
53
54    /// Integrate the installed plugin with VT Code's core plugin system
55    async fn integrate_with_core_plugin_system(&self, manifest_path: &Path) -> Result<()> {
56        // This would load the plugin into VT Code's plugin runtime
57        if let Some(runtime) = &self.core_plugin_runtime {
58            // Load the plugin manifest and register it with the core runtime
59            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    /// Download plugin from its source
69    async fn download_plugin(&self, manifest: &PluginManifest, plugin_dir: &Path) -> Result<()> {
70        // Validate the manifest before downloading
71        self.validate_manifest(manifest)?;
72
73        tracing::info!(plugin_id = %manifest.id, source = %manifest.source, "downloading plugin");
74
75        // Determine the source type and download accordingly
76        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            // Local path
82            self.download_from_local(manifest, plugin_dir).await?;
83        } else {
84            // Assume it's a git repository
85            self.download_from_git(manifest, plugin_dir).await?;
86        }
87
88        Ok(())
89    }
90
91    /// Download plugin from HTTP source
92    async fn download_from_http(&self, manifest: &PluginManifest, plugin_dir: &Path) -> Result<()> {
93        // For now, we'll create a placeholder since we don't have the actual HTTP client configured
94        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    /// Download plugin from local file
108    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        // Copy the file from source to destination
118        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    /// Download plugin from local path
131    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        // Copy the file from source to destination
145        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    /// Download plugin from git repository
158    async fn download_from_git(&self, manifest: &PluginManifest, plugin_dir: &Path) -> Result<()> {
159        // For now, we'll create a placeholder since we don't have git functionality integrated
160        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    /// Validate the plugin manifest before installation
174    pub fn validate_manifest(&self, manifest: &PluginManifest) -> Result<()> {
175        // Validate required fields
176        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        // Validate entrypoint path
181        if manifest.entrypoint.as_os_str().is_empty() {
182            bail!("Plugin manifest must have a valid entrypoint path");
183        }
184
185        // Validate trust level if specified
186        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                    // Valid trust level
192                }
193            }
194        }
195
196        // Validate dependencies if any
197        validate_all_non_empty(&manifest.dependencies, "Plugin dependencies")?;
198
199        Ok(())
200    }
201
202    /// Uninstall a plugin by ID
203    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        // Remove from VT Code's plugin system before filesystem removal
208        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    /// Remove plugin from VT Code's core plugin system
221    async fn remove_from_core_plugin_system(&self, plugin_id: &str) -> Result<()> {
222        // Remove the plugin from VT Code's plugin runtime
223        if let Some(runtime) = &self.core_plugin_runtime {
224            // Unload the plugin by ID
225            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    /// Check if a plugin is installed
238    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}