Skip to main content

vtcode_core/plugins/
loader.rs

1//! Plugin loader for VT Code
2//!
3//! Handles the discovery, installation, and loading of plugins from various sources.
4
5use std::path::{Path, PathBuf};
6use tokio::fs;
7
8use super::{PluginError, PluginManifest, PluginResult, PluginRuntime};
9
10/// Plugin source types
11#[derive(Debug, Clone)]
12pub enum PluginSource {
13    /// Local directory
14    Local(PathBuf),
15    /// Git repository
16    Git(String),
17    /// HTTP URL
18    Http(String),
19    /// Marketplace identifier
20    Marketplace(String),
21}
22
23/// Plugin loader that handles plugin installation and management
24pub struct PluginLoader {
25    /// Base directory for plugin installations
26    plugins_dir: PathBuf,
27    /// Runtime for managing loaded plugins
28    runtime: PluginRuntime,
29}
30
31impl PluginLoader {
32    /// Create a new plugin loader
33    pub fn new(plugins_dir: PathBuf, runtime: PluginRuntime) -> Self {
34        Self {
35            plugins_dir,
36            runtime,
37        }
38    }
39
40    /// Install a plugin from a source
41    pub async fn install_plugin(
42        &self,
43        source: PluginSource,
44        name: Option<String>,
45    ) -> PluginResult<()> {
46        let plugin_dir = match source {
47            PluginSource::Local(path) => self.install_from_local(path).await?,
48            PluginSource::Git(url) => self.install_from_git(&url, name.as_deref()).await?,
49            PluginSource::Http(url) => self.install_from_http(&url, name.as_deref()).await?,
50            PluginSource::Marketplace(id) => {
51                self.install_from_marketplace(&id, name.as_deref()).await?
52            }
53        };
54
55        // Load the installed plugin
56        self.runtime.load_plugin(&plugin_dir).await?;
57        Ok(())
58    }
59
60    /// Install plugin from local directory
61    async fn install_from_local(&self, source_path: PathBuf) -> PluginResult<PathBuf> {
62        if !source_path.exists() {
63            return Err(PluginError::LoadingError(format!(
64                "Source path does not exist: {}",
65                source_path.display()
66            )));
67        }
68
69        // Validate that it contains a plugin manifest
70        let manifest_path = source_path.join(".vtcode-plugin/plugin.json");
71        if !manifest_path.exists() {
72            return Err(PluginError::ManifestValidationError(format!(
73                "Plugin manifest not found in source: {}",
74                manifest_path.display()
75            )));
76        }
77
78        // Load the manifest to get the plugin name
79        let manifest_content = fs::read_to_string(&manifest_path)
80            .await
81            .map_err(|e| PluginError::LoadingError(format!("Failed to read manifest: {}", e)))?;
82
83        let manifest: PluginManifest = serde_json::from_str(&manifest_content).map_err(|e| {
84            PluginError::ManifestValidationError(format!("Invalid manifest JSON: {}", e))
85        })?;
86
87        // Create installation directory
88        let install_dir = self.plugins_dir.join(&manifest.name);
89        fs::create_dir_all(&install_dir).await.map_err(|e| {
90            PluginError::LoadingError(format!("Failed to create plugin directory: {}", e))
91        })?;
92
93        // Copy plugin files to installation directory
94        self.copy_directory(&source_path, &install_dir).await?;
95
96        Ok(install_dir)
97    }
98
99    /// Install plugin from Git repository
100    async fn install_from_git(&self, url: &str, name: Option<&str>) -> PluginResult<PathBuf> {
101        use tempfile::TempDir;
102        use tokio::process::Command;
103
104        // Create a temporary directory for the git clone
105        let temp_dir = TempDir::new().map_err(|e| {
106            PluginError::LoadingError(format!("Failed to create temporary directory: {}", e))
107        })?;
108        let temp_path = temp_dir.path();
109
110        // Execute git clone command
111        let output = Command::new("git")
112            .arg("clone")
113            .arg(url)
114            .arg(temp_path)
115            .output()
116            .await
117            .map_err(|e| {
118                PluginError::LoadingError(format!("Failed to execute git clone: {}", e))
119            })?;
120
121        if !output.status.success() {
122            let stderr = String::from_utf8_lossy(&output.stderr);
123            return Err(PluginError::LoadingError(format!(
124                "Git clone failed: {}",
125                stderr
126            )));
127        }
128
129        // Determine plugin name
130        let plugin_name = name
131            .map(|s| s.to_string())
132            .unwrap_or_else(|| self.extract_name_from_git_url(url));
133
134        // Create installation directory
135        let install_dir = self.plugins_dir.join(&plugin_name);
136        fs::create_dir_all(&install_dir).await.map_err(|e| {
137            PluginError::LoadingError(format!("Failed to create plugin directory: {}", e))
138        })?;
139
140        // Copy the cloned repository contents to the installation directory
141        self.copy_directory(temp_path, &install_dir).await?;
142
143        // Verify that the plugin manifest exists in the installed directory
144        let manifest_path = install_dir.join(".vtcode-plugin/plugin.json");
145        if !manifest_path.exists() {
146            return Err(PluginError::ManifestValidationError(
147                "Plugin manifest not found in cloned repository".to_string(),
148            ));
149        }
150
151        Ok(install_dir)
152    }
153
154    /// Install plugin from HTTP URL
155    async fn install_from_http(&self, url: &str, name: Option<&str>) -> PluginResult<PathBuf> {
156        // For now, create a placeholder implementation
157        let plugin_name = name
158            .map(|s| s.to_string())
159            .unwrap_or_else(|| self.extract_name_from_url(url));
160
161        let install_dir = self.plugins_dir.join(&plugin_name);
162        fs::create_dir_all(&install_dir).await.map_err(|e| {
163            PluginError::LoadingError(format!("Failed to create plugin directory: {}", e))
164        })?;
165
166        // Create a placeholder manifest file
167        let placeholder_manifest = format!(
168            r#"{{
169  "name": "{}",
170  "version": "1.0.0",
171  "description": "Placeholder for HTTP-installed plugin from {}"
172}}"#,
173            plugin_name, url
174        );
175
176        let manifest_path = install_dir.join(".vtcode-plugin/plugin.json");
177        let manifest_parent = manifest_path.parent().ok_or_else(|| {
178            PluginError::LoadingError(
179                "Failed to resolve parent directory for plugin manifest path".to_string(),
180            )
181        })?;
182        fs::create_dir_all(manifest_parent).await?;
183        fs::write(&manifest_path, placeholder_manifest)
184            .await
185            .map_err(|e| {
186                PluginError::LoadingError(format!("Failed to create placeholder manifest: {}", e))
187            })?;
188
189        Ok(install_dir)
190    }
191
192    /// Install plugin from marketplace
193    async fn install_from_marketplace(
194        &self,
195        marketplace_id: &str,
196        name: Option<&str>,
197    ) -> PluginResult<PathBuf> {
198        // For now, create a placeholder implementation
199        let plugin_name = name.map(|s| s.to_string()).unwrap_or_else(|| {
200            marketplace_id
201                .split('/')
202                .next_back()
203                .unwrap_or(marketplace_id)
204                .to_string()
205        });
206
207        let install_dir = self.plugins_dir.join(&plugin_name);
208        fs::create_dir_all(&install_dir).await.map_err(|e| {
209            PluginError::LoadingError(format!("Failed to create plugin directory: {}", e))
210        })?;
211
212        // Create a placeholder manifest file
213        let placeholder_manifest = format!(
214            r#"{{
215  "name": "{}",
216  "version": "1.0.0",
217  "description": "Placeholder for marketplace-installed plugin from {}"
218}}"#,
219            plugin_name, marketplace_id
220        );
221
222        let manifest_path = install_dir.join(".vtcode-plugin/plugin.json");
223        let manifest_parent = manifest_path.parent().ok_or_else(|| {
224            PluginError::LoadingError(
225                "Failed to resolve parent directory for plugin manifest path".to_string(),
226            )
227        })?;
228        fs::create_dir_all(manifest_parent).await?;
229        fs::write(&manifest_path, placeholder_manifest)
230            .await
231            .map_err(|e| {
232                PluginError::LoadingError(format!("Failed to create placeholder manifest: {}", e))
233            })?;
234
235        Ok(install_dir)
236    }
237
238    /// Uninstall a plugin
239    pub async fn uninstall_plugin(&self, plugin_name: &str) -> PluginResult<()> {
240        let plugin_dir = self.plugins_dir.join(plugin_name);
241
242        if !plugin_dir.exists() {
243            return Err(PluginError::NotFound(plugin_name.to_string()));
244        }
245
246        // Unload the plugin from runtime first
247        self.runtime.unload_plugin(plugin_name).await?;
248
249        // Remove the plugin directory
250        fs::remove_dir_all(&plugin_dir).await.map_err(|e| {
251            PluginError::LoadingError(format!("Failed to remove plugin directory: {}", e))
252        })?;
253
254        Ok(())
255    }
256
257    /// List installed plugins
258    pub async fn list_installed_plugins(&self) -> PluginResult<Vec<String>> {
259        let mut plugins = Vec::new();
260
261        if !self.plugins_dir.exists() {
262            return Ok(plugins);
263        }
264
265        let mut entries = fs::read_dir(&self.plugins_dir).await.map_err(|e| {
266            PluginError::LoadingError(format!("Failed to read plugins directory: {}", e))
267        })?;
268
269        while let Some(entry) = entries.next_entry().await.map_err(|e| {
270            PluginError::LoadingError(format!("Failed to read directory entry: {}", e))
271        })? {
272            let path = entry.path();
273            if path.is_dir() {
274                // Check if it contains a plugin manifest
275                let manifest_path = path.join(".vtcode-plugin/plugin.json");
276                if manifest_path.exists()
277                    && let Some(name) = path.file_name()
278                {
279                    plugins.push(name.to_string_lossy().to_string());
280                }
281            }
282        }
283
284        Ok(plugins)
285    }
286
287    /// Copy directory recursively
288    async fn copy_directory(&self, src: &Path, dst: &Path) -> PluginResult<()> {
289        Box::pin(async {
290            if !src.is_dir() {
291                return Err(PluginError::LoadingError(format!(
292                    "Source is not a directory: {}",
293                    src.display()
294                )));
295            }
296
297            fs::create_dir_all(dst).await.map_err(|e| {
298                PluginError::LoadingError(format!("Failed to create destination directory: {}", e))
299            })?;
300
301            let mut entries = fs::read_dir(src).await.map_err(|e| {
302                PluginError::LoadingError(format!("Failed to read source directory: {}", e))
303            })?;
304
305            while let Some(entry) = entries.next_entry().await.map_err(|e| {
306                PluginError::LoadingError(format!("Failed to read directory entry: {}", e))
307            })? {
308                let src_path = entry.path();
309                let dst_path = dst.join(entry.file_name());
310
311                if src_path.is_dir() {
312                    self.copy_directory(&src_path, &dst_path).await?;
313                } else {
314                    fs::copy(&src_path, &dst_path).await.map_err(|e| {
315                        PluginError::LoadingError(format!("Failed to copy file: {}", e))
316                    })?;
317                }
318            }
319
320            Ok(())
321        })
322        .await
323    }
324
325    /// Extract plugin name from Git URL
326    fn extract_name_from_git_url(&self, url: &str) -> String {
327        // Extract name from git URL (e.g., https://github.com/user/repo.git -> repo)
328        url.trim_end_matches(".git")
329            .split('/')
330            .next_back()
331            .unwrap_or("unknown-plugin")
332            .to_string()
333    }
334
335    /// Extract plugin name from URL
336    fn extract_name_from_url(&self, url: &str) -> String {
337        // Extract name from URL path
338        url.split('/')
339            .next_back()
340            .unwrap_or("unknown-plugin")
341            .to_string()
342    }
343}