1use crate::{
7 Ecosystem, FigmentConfigManager, HttpUtils, PackageInfo, PackageSpec, Result, ToolContext,
8 ToolDownloader, ToolExecutionResult, ToolStatus, VersionInfo, VxEnvironment,
9};
10use serde_json::Value;
11use std::collections::HashMap;
12use std::path::{Path, PathBuf};
13
14#[async_trait::async_trait]
19pub trait VxTool: Send + Sync {
20 fn name(&self) -> &str;
22
23 fn description(&self) -> &str {
25 "A development tool"
26 }
27
28 fn aliases(&self) -> Vec<&str> {
30 vec![]
31 }
32
33 async fn fetch_versions(&self, include_prerelease: bool) -> Result<Vec<VersionInfo>>;
36
37 async fn install_version(&self, version: &str, force: bool) -> Result<()> {
40 if !force && self.is_version_installed(version).await? {
41 return Err(crate::VxError::VersionAlreadyInstalled {
42 tool_name: self.name().to_string(),
43 version: version.to_string(),
44 });
45 }
46
47 let install_dir = self.get_version_install_dir(version);
48 let _exe_path = self.default_install_workflow(version, &install_dir).await?;
49
50 if !self.is_version_installed(version).await? {
52 return Err(crate::VxError::InstallationFailed {
53 tool_name: self.name().to_string(),
54 version: version.to_string(),
55 message: "Installation verification failed".to_string(),
56 });
57 }
58
59 Ok(())
60 }
61
62 async fn is_version_installed(&self, version: &str) -> Result<bool> {
64 let env = VxEnvironment::new().expect("Failed to create VX environment");
65 Ok(env.is_version_installed(self.name(), version))
66 }
67
68 async fn execute(&self, args: &[String], context: &ToolContext) -> Result<ToolExecutionResult> {
70 self.default_execute_workflow(args, context).await
71 }
72
73 async fn get_executable_path(&self, install_dir: &Path) -> Result<PathBuf> {
76 let exe_name = if cfg!(target_os = "windows") {
77 format!("{}.exe", self.name())
78 } else {
79 self.name().to_string()
80 };
81
82 let candidates = vec![
84 install_dir.join(&exe_name),
85 install_dir.join("bin").join(&exe_name),
86 install_dir.join("Scripts").join(&exe_name), ];
88
89 for candidate in candidates {
90 if candidate.exists() {
91 return Ok(candidate);
92 }
93 }
94
95 Ok(install_dir.join("bin").join(exe_name))
97 }
98
99 async fn get_download_url(&self, version: &str) -> Result<Option<String>> {
102 let versions = self.fetch_versions(true).await?;
104 Ok(versions
105 .iter()
106 .find(|v| v.version == version)
107 .and_then(|v| v.download_url.clone()))
108 }
109
110 fn get_version_install_dir(&self, version: &str) -> PathBuf {
112 let env = VxEnvironment::new().expect("Failed to create VX environment");
113 env.get_version_install_dir(self.name(), version)
114 }
115
116 fn get_base_install_dir(&self) -> PathBuf {
118 let env = VxEnvironment::new().expect("Failed to create VX environment");
119 env.get_tool_install_dir(self.name())
120 }
121
122 async fn default_install_workflow(
125 &self,
126 version: &str,
127 _install_dir: &Path,
128 ) -> Result<PathBuf> {
129 let download_url = self.get_download_url(version).await?.ok_or_else(|| {
131 crate::VxError::DownloadUrlNotFound {
132 tool_name: self.name().to_string(),
133 version: version.to_string(),
134 }
135 })?;
136
137 let downloader = ToolDownloader::new()?;
139 let exe_path = downloader
140 .download_and_install(self.name(), version, &download_url)
141 .await?;
142
143 let global_manager = crate::GlobalToolManager::new()?;
145 let install_dir = self.get_version_install_dir(version);
146 global_manager
147 .register_global_tool(self.name(), version, &install_dir)
148 .await?;
149
150 Ok(exe_path)
151 }
152
153 async fn default_execute_workflow(
155 &self,
156 args: &[String],
157 context: &ToolContext,
158 ) -> Result<ToolExecutionResult> {
159 let exe_path = if context.use_system_path {
161 which::which(self.name()).map_err(|_| crate::VxError::ToolNotFound {
162 tool_name: self.name().to_string(),
163 })?
164 } else {
165 let active_version = self.get_active_version().await?;
167 let install_dir = self.get_version_install_dir(&active_version);
168 let env = VxEnvironment::new().expect("Failed to create VX environment");
169 env.find_executable_in_dir(&install_dir, self.name())?
170 };
171
172 let mut cmd = std::process::Command::new(&exe_path);
174 cmd.args(args);
175
176 if let Some(cwd) = &context.working_directory {
177 cmd.current_dir(cwd);
178 }
179
180 for (key, value) in &context.environment_variables {
181 cmd.env(key, value);
182 }
183
184 let status = cmd.status().map_err(|e| crate::VxError::Other {
185 message: format!("Failed to execute {}: {}", self.name(), e),
186 })?;
187
188 Ok(ToolExecutionResult {
189 exit_code: status.code().unwrap_or(1),
190 stdout: None, stderr: None,
192 })
193 }
194
195 async fn get_active_version(&self) -> Result<String> {
197 let env = VxEnvironment::new().expect("Failed to create VX environment");
198
199 if let Some(active_version) = env.get_active_version(self.name())? {
201 return Ok(active_version);
202 }
203
204 let installed_versions = self.get_installed_versions().await?;
206 installed_versions
207 .first()
208 .cloned()
209 .ok_or_else(|| crate::VxError::ToolNotInstalled {
210 tool_name: self.name().to_string(),
211 })
212 }
213
214 async fn get_installed_versions(&self) -> Result<Vec<String>> {
216 let env = VxEnvironment::new().expect("Failed to create VX environment");
217 env.list_installed_versions(self.name())
218 }
219
220 async fn remove_version(&self, version: &str, force: bool) -> Result<()> {
222 let version_dir = self.get_version_install_dir(version);
223
224 if !version_dir.exists() {
226 if !force {
227 return Err(crate::VxError::VersionNotInstalled {
228 tool_name: self.name().to_string(),
229 version: version.to_string(),
230 });
231 }
232 return Ok(());
234 }
235
236 let global_manager = crate::GlobalToolManager::new()?;
238 if !force && !global_manager.can_remove_tool(self.name()).await? {
239 let dependents = global_manager.get_tool_dependents(self.name()).await?;
240 return Err(crate::VxError::Other {
241 message: format!(
242 "Cannot remove {} {} - it is referenced by virtual environments: {}. Use --force to override.",
243 self.name(),
244 version,
245 dependents.join(", ")
246 ),
247 });
248 }
249
250 match std::fs::remove_dir_all(&version_dir) {
252 Ok(()) => {
253 if let Err(e) = global_manager.remove_global_tool(self.name()).await {
255 eprintln!("Warning: Failed to remove tool from global registry: {}", e);
257 }
258 Ok(())
259 }
260 Err(e) => {
261 if force {
262 match e.kind() {
264 std::io::ErrorKind::NotFound => {
265 Ok(())
267 }
268 std::io::ErrorKind::PermissionDenied => {
269 Err(crate::VxError::PermissionError {
271 message: format!(
272 "Permission denied when removing {} {}: {}",
273 self.name(),
274 version,
275 e
276 ),
277 })
278 }
279 _ => {
280 Err(crate::VxError::IoError {
282 message: format!(
283 "Failed to remove {} {} directory: {}",
284 self.name(),
285 version,
286 e
287 ),
288 })
289 }
290 }
291 } else {
292 Err(e.into())
294 }
295 }
296 }
297 }
298
299 async fn get_status(&self) -> Result<ToolStatus> {
301 let installed_versions = self.get_installed_versions().await?;
302 let current_version = if !installed_versions.is_empty() {
303 self.get_active_version().await.ok()
304 } else {
305 None
306 };
307
308 Ok(ToolStatus {
309 installed: !installed_versions.is_empty(),
310 current_version,
311 installed_versions,
312 })
313 }
314
315 fn metadata(&self) -> HashMap<String, String> {
317 HashMap::new()
318 }
319}
320
321pub trait UrlBuilder: Send + Sync {
323 fn download_url(&self, version: &str) -> Option<String>;
324 fn versions_url(&self) -> &str;
325}
326
327pub trait VersionParser: Send + Sync {
329 fn parse_versions(&self, json: &Value, include_prerelease: bool) -> Result<Vec<VersionInfo>>;
330}
331
332#[async_trait::async_trait]
337pub trait VxPackageManager: Send + Sync {
338 fn name(&self) -> &str;
340
341 fn ecosystem(&self) -> Ecosystem;
343
344 fn description(&self) -> &str {
346 "A package manager"
347 }
348
349 async fn is_available(&self) -> Result<bool> {
351 Ok(which::which(self.name()).is_ok())
353 }
354
355 fn is_preferred_for_project(&self, project_path: &Path) -> bool {
358 let config_files = self.get_config_files();
360 config_files
361 .iter()
362 .any(|file| project_path.join(file).exists())
363 }
364
365 fn get_config_files(&self) -> Vec<&str> {
367 vec![]
368 }
369
370 async fn install_packages(&self, packages: &[PackageSpec], project_path: &Path) -> Result<()>;
372
373 async fn remove_packages(&self, packages: &[String], project_path: &Path) -> Result<()> {
375 self.run_command(&["remove"], packages, project_path).await
376 }
377
378 async fn update_packages(&self, packages: &[String], project_path: &Path) -> Result<()> {
380 if packages.is_empty() {
381 self.run_command(&["update"], &[], project_path).await
382 } else {
383 self.run_command(&["update"], packages, project_path).await
384 }
385 }
386
387 async fn list_packages(&self, project_path: &Path) -> Result<Vec<PackageInfo>> {
389 self.default_list_packages(project_path).await
391 }
392
393 async fn search_packages(&self, query: &str) -> Result<Vec<PackageInfo>> {
395 self.run_search_command(query).await
396 }
397
398 async fn run_command(
400 &self,
401 command: &[&str],
402 args: &[String],
403 project_path: &Path,
404 ) -> Result<()> {
405 let mut cmd = std::process::Command::new(self.name());
406 cmd.args(command);
407 cmd.args(args);
408 cmd.current_dir(project_path);
409
410 let status = cmd
411 .status()
412 .map_err(|e| crate::VxError::PackageManagerError {
413 manager: self.name().to_string(),
414 message: format!("Failed to run command: {}", e),
415 })?;
416
417 if !status.success() {
418 return Err(crate::VxError::PackageManagerError {
419 manager: self.name().to_string(),
420 message: format!("Command failed with exit code: {:?}", status.code()),
421 });
422 }
423
424 Ok(())
425 }
426
427 async fn default_list_packages(&self, _project_path: &Path) -> Result<Vec<PackageInfo>> {
429 Ok(vec![])
432 }
433
434 async fn run_search_command(&self, query: &str) -> Result<Vec<PackageInfo>> {
436 let _ = query;
439 Ok(vec![])
440 }
441
442 fn get_install_command(&self) -> Vec<&str> {
444 vec!["install"]
445 }
446
447 fn get_add_command(&self) -> Vec<&str> {
449 vec!["add"]
450 }
451
452 fn metadata(&self) -> HashMap<String, String> {
454 HashMap::new()
455 }
456}
457
458#[async_trait::async_trait]
462pub trait VxPlugin: Send + Sync {
463 fn name(&self) -> &str;
465
466 fn description(&self) -> &str {
468 "A vx plugin"
469 }
470
471 fn version(&self) -> &str {
473 "0.1.0"
474 }
475
476 fn tools(&self) -> Vec<Box<dyn VxTool>> {
478 vec![]
479 }
480
481 fn package_managers(&self) -> Vec<Box<dyn VxPackageManager>> {
483 vec![]
484 }
485
486 async fn initialize(&mut self) -> Result<()> {
488 Ok(())
489 }
490
491 fn supports_tool(&self, tool_name: &str) -> bool {
493 self.tools()
494 .iter()
495 .any(|tool| tool.name() == tool_name || tool.aliases().contains(&tool_name))
496 }
497
498 fn supports_package_manager(&self, pm_name: &str) -> bool {
500 self.package_managers()
501 .iter()
502 .any(|pm| pm.name() == pm_name)
503 }
504
505 fn metadata(&self) -> HashMap<String, String> {
507 HashMap::new()
508 }
509}
510
511pub struct StandardPlugin {
513 name: String,
514 description: String,
515 version: String,
516 tool_factory: Box<dyn Fn() -> Box<dyn VxTool> + Send + Sync>,
517}
518
519impl StandardPlugin {
520 pub fn new<F>(name: String, description: String, version: String, tool_factory: F) -> Self
521 where
522 F: Fn() -> Box<dyn VxTool> + Send + Sync + 'static,
523 {
524 Self {
525 name,
526 description,
527 version,
528 tool_factory: Box::new(tool_factory),
529 }
530 }
531}
532
533#[async_trait::async_trait]
534impl VxPlugin for StandardPlugin {
535 fn name(&self) -> &str {
536 &self.name
537 }
538
539 fn description(&self) -> &str {
540 &self.description
541 }
542
543 fn version(&self) -> &str {
544 &self.version
545 }
546
547 fn tools(&self) -> Vec<Box<dyn VxTool>> {
548 vec![(self.tool_factory)()]
549 }
550}
551
552pub struct ConfigurableTool {
557 metadata: ToolMetadata,
558 config_manager: FigmentConfigManager,
559 url_builder: Box<dyn UrlBuilder>,
560 version_parser: Box<dyn VersionParser>,
561}
562
563impl ConfigurableTool {
564 pub fn new(
565 metadata: ToolMetadata,
566 url_builder: Box<dyn UrlBuilder>,
567 version_parser: Box<dyn VersionParser>,
568 ) -> Result<Self> {
569 let config_manager =
570 FigmentConfigManager::new().or_else(|_| FigmentConfigManager::minimal())?;
571
572 Ok(Self {
573 metadata,
574 config_manager,
575 url_builder,
576 version_parser,
577 })
578 }
579}
580
581#[async_trait::async_trait]
582impl VxTool for ConfigurableTool {
583 fn name(&self) -> &str {
584 &self.metadata.name
585 }
586
587 fn description(&self) -> &str {
588 &self.metadata.description
589 }
590
591 fn aliases(&self) -> Vec<&str> {
592 self.metadata.aliases.iter().map(|s| s.as_str()).collect()
593 }
594
595 async fn fetch_versions(&self, include_prerelease: bool) -> Result<Vec<VersionInfo>> {
596 let json = HttpUtils::fetch_json(self.url_builder.versions_url()).await?;
597 self.version_parser
598 .parse_versions(&json, include_prerelease)
599 }
600
601 async fn get_download_url(&self, version: &str) -> Result<Option<String>> {
602 if let Ok(url) = self
604 .config_manager
605 .get_download_url(&self.metadata.name, version)
606 {
607 return Ok(Some(url));
608 }
609
610 Ok(self.url_builder.download_url(version))
612 }
613}
614
615#[derive(Debug, Clone)]
617pub struct ToolMetadata {
618 pub name: String,
619 pub description: String,
620 pub aliases: Vec<String>,
621}