vtcode_core/plugins/
loader.rs1use std::path::{Path, PathBuf};
6use tokio::fs;
7
8use super::{PluginError, PluginManifest, PluginResult, PluginRuntime};
9
10#[derive(Debug, Clone)]
12pub enum PluginSource {
13 Local(PathBuf),
15 Git(String),
17 Http(String),
19 Marketplace(String),
21}
22
23pub struct PluginLoader {
25 plugins_dir: PathBuf,
27 runtime: PluginRuntime,
29}
30
31impl PluginLoader {
32 pub fn new(plugins_dir: PathBuf, runtime: PluginRuntime) -> Self {
34 Self {
35 plugins_dir,
36 runtime,
37 }
38 }
39
40 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 self.runtime.load_plugin(&plugin_dir).await?;
57 Ok(())
58 }
59
60 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 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 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 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 self.copy_directory(&source_path, &install_dir).await?;
95
96 Ok(install_dir)
97 }
98
99 async fn install_from_git(&self, url: &str, name: Option<&str>) -> PluginResult<PathBuf> {
101 use tempfile::TempDir;
102 use tokio::process::Command;
103
104 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 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 let plugin_name = name
131 .map(|s| s.to_string())
132 .unwrap_or_else(|| self.extract_name_from_git_url(url));
133
134 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 self.copy_directory(temp_path, &install_dir).await?;
142
143 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 async fn install_from_http(&self, url: &str, name: Option<&str>) -> PluginResult<PathBuf> {
156 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 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 async fn install_from_marketplace(
194 &self,
195 marketplace_id: &str,
196 name: Option<&str>,
197 ) -> PluginResult<PathBuf> {
198 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 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 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 self.runtime.unload_plugin(plugin_name).await?;
248
249 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 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 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 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 fn extract_name_from_git_url(&self, url: &str) -> String {
327 url.trim_end_matches(".git")
329 .split('/')
330 .next_back()
331 .unwrap_or("unknown-plugin")
332 .to_string()
333 }
334
335 fn extract_name_from_url(&self, url: &str) -> String {
337 url.split('/')
339 .next_back()
340 .unwrap_or("unknown-plugin")
341 .to_string()
342 }
343}