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 downloader
140 .download_and_install(self.name(), version, &download_url)
141 .await
142 }
143
144 async fn default_execute_workflow(
146 &self,
147 args: &[String],
148 context: &ToolContext,
149 ) -> Result<ToolExecutionResult> {
150 let exe_path = if context.use_system_path {
152 which::which(self.name()).map_err(|_| crate::VxError::ToolNotFound {
153 tool_name: self.name().to_string(),
154 })?
155 } else {
156 let active_version = self.get_active_version().await?;
158 let install_dir = self.get_version_install_dir(&active_version);
159 let env = VxEnvironment::new().expect("Failed to create VX environment");
160 env.find_executable_in_dir(&install_dir, self.name())?
161 };
162
163 let mut cmd = std::process::Command::new(&exe_path);
165 cmd.args(args);
166
167 if let Some(cwd) = &context.working_directory {
168 cmd.current_dir(cwd);
169 }
170
171 for (key, value) in &context.environment_variables {
172 cmd.env(key, value);
173 }
174
175 let status = cmd.status().map_err(|e| crate::VxError::Other {
176 message: format!("Failed to execute {}: {}", self.name(), e),
177 })?;
178
179 Ok(ToolExecutionResult {
180 exit_code: status.code().unwrap_or(1),
181 stdout: None, stderr: None,
183 })
184 }
185
186 async fn get_active_version(&self) -> Result<String> {
188 let env = VxEnvironment::new().expect("Failed to create VX environment");
189
190 if let Some(active_version) = env.get_active_version(self.name())? {
192 return Ok(active_version);
193 }
194
195 let installed_versions = self.get_installed_versions().await?;
197 installed_versions
198 .first()
199 .cloned()
200 .ok_or_else(|| crate::VxError::ToolNotInstalled {
201 tool_name: self.name().to_string(),
202 })
203 }
204
205 async fn get_installed_versions(&self) -> Result<Vec<String>> {
207 let env = VxEnvironment::new().expect("Failed to create VX environment");
208 env.list_installed_versions(self.name())
209 }
210
211 async fn remove_version(&self, version: &str, force: bool) -> Result<()> {
213 let version_dir = self.get_version_install_dir(version);
214
215 if !version_dir.exists() {
217 if !force {
218 return Err(crate::VxError::VersionNotInstalled {
219 tool_name: self.name().to_string(),
220 version: version.to_string(),
221 });
222 }
223 return Ok(());
225 }
226
227 match std::fs::remove_dir_all(&version_dir) {
229 Ok(()) => Ok(()),
230 Err(e) => {
231 if force {
232 match e.kind() {
234 std::io::ErrorKind::NotFound => {
235 Ok(())
237 }
238 std::io::ErrorKind::PermissionDenied => {
239 Err(crate::VxError::PermissionError {
241 message: format!(
242 "Permission denied when removing {} {}: {}",
243 self.name(),
244 version,
245 e
246 ),
247 })
248 }
249 _ => {
250 Err(crate::VxError::IoError {
252 message: format!(
253 "Failed to remove {} {} directory: {}",
254 self.name(),
255 version,
256 e
257 ),
258 })
259 }
260 }
261 } else {
262 Err(e.into())
264 }
265 }
266 }
267 }
268
269 async fn get_status(&self) -> Result<ToolStatus> {
271 let installed_versions = self.get_installed_versions().await?;
272 let current_version = if !installed_versions.is_empty() {
273 self.get_active_version().await.ok()
274 } else {
275 None
276 };
277
278 Ok(ToolStatus {
279 installed: !installed_versions.is_empty(),
280 current_version,
281 installed_versions,
282 })
283 }
284
285 fn metadata(&self) -> HashMap<String, String> {
287 HashMap::new()
288 }
289}
290
291pub trait UrlBuilder: Send + Sync {
293 fn download_url(&self, version: &str) -> Option<String>;
294 fn versions_url(&self) -> &str;
295}
296
297pub trait VersionParser: Send + Sync {
299 fn parse_versions(&self, json: &Value, include_prerelease: bool) -> Result<Vec<VersionInfo>>;
300}
301
302#[async_trait::async_trait]
307pub trait VxPackageManager: Send + Sync {
308 fn name(&self) -> &str;
310
311 fn ecosystem(&self) -> Ecosystem;
313
314 fn description(&self) -> &str {
316 "A package manager"
317 }
318
319 async fn is_available(&self) -> Result<bool> {
321 Ok(which::which(self.name()).is_ok())
323 }
324
325 fn is_preferred_for_project(&self, project_path: &Path) -> bool {
328 let config_files = self.get_config_files();
330 config_files
331 .iter()
332 .any(|file| project_path.join(file).exists())
333 }
334
335 fn get_config_files(&self) -> Vec<&str> {
337 vec![]
338 }
339
340 async fn install_packages(&self, packages: &[PackageSpec], project_path: &Path) -> Result<()>;
342
343 async fn remove_packages(&self, packages: &[String], project_path: &Path) -> Result<()> {
345 self.run_command(&["remove"], packages, project_path).await
346 }
347
348 async fn update_packages(&self, packages: &[String], project_path: &Path) -> Result<()> {
350 if packages.is_empty() {
351 self.run_command(&["update"], &[], project_path).await
352 } else {
353 self.run_command(&["update"], packages, project_path).await
354 }
355 }
356
357 async fn list_packages(&self, project_path: &Path) -> Result<Vec<PackageInfo>> {
359 self.default_list_packages(project_path).await
361 }
362
363 async fn search_packages(&self, query: &str) -> Result<Vec<PackageInfo>> {
365 self.run_search_command(query).await
366 }
367
368 async fn run_command(
370 &self,
371 command: &[&str],
372 args: &[String],
373 project_path: &Path,
374 ) -> Result<()> {
375 let mut cmd = std::process::Command::new(self.name());
376 cmd.args(command);
377 cmd.args(args);
378 cmd.current_dir(project_path);
379
380 let status = cmd
381 .status()
382 .map_err(|e| crate::VxError::PackageManagerError {
383 manager: self.name().to_string(),
384 message: format!("Failed to run command: {}", e),
385 })?;
386
387 if !status.success() {
388 return Err(crate::VxError::PackageManagerError {
389 manager: self.name().to_string(),
390 message: format!("Command failed with exit code: {:?}", status.code()),
391 });
392 }
393
394 Ok(())
395 }
396
397 async fn default_list_packages(&self, _project_path: &Path) -> Result<Vec<PackageInfo>> {
399 Ok(vec![])
402 }
403
404 async fn run_search_command(&self, query: &str) -> Result<Vec<PackageInfo>> {
406 let _ = query;
409 Ok(vec![])
410 }
411
412 fn get_install_command(&self) -> Vec<&str> {
414 vec!["install"]
415 }
416
417 fn get_add_command(&self) -> Vec<&str> {
419 vec!["add"]
420 }
421
422 fn metadata(&self) -> HashMap<String, String> {
424 HashMap::new()
425 }
426}
427
428#[async_trait::async_trait]
432pub trait VxPlugin: Send + Sync {
433 fn name(&self) -> &str;
435
436 fn description(&self) -> &str {
438 "A vx plugin"
439 }
440
441 fn version(&self) -> &str {
443 "0.1.0"
444 }
445
446 fn tools(&self) -> Vec<Box<dyn VxTool>> {
448 vec![]
449 }
450
451 fn package_managers(&self) -> Vec<Box<dyn VxPackageManager>> {
453 vec![]
454 }
455
456 async fn initialize(&mut self) -> Result<()> {
458 Ok(())
459 }
460
461 fn supports_tool(&self, tool_name: &str) -> bool {
463 self.tools()
464 .iter()
465 .any(|tool| tool.name() == tool_name || tool.aliases().contains(&tool_name))
466 }
467
468 fn supports_package_manager(&self, pm_name: &str) -> bool {
470 self.package_managers()
471 .iter()
472 .any(|pm| pm.name() == pm_name)
473 }
474
475 fn metadata(&self) -> HashMap<String, String> {
477 HashMap::new()
478 }
479}
480
481pub struct StandardPlugin {
483 name: String,
484 description: String,
485 version: String,
486 tool_factory: Box<dyn Fn() -> Box<dyn VxTool> + Send + Sync>,
487}
488
489impl StandardPlugin {
490 pub fn new<F>(name: String, description: String, version: String, tool_factory: F) -> Self
491 where
492 F: Fn() -> Box<dyn VxTool> + Send + Sync + 'static,
493 {
494 Self {
495 name,
496 description,
497 version,
498 tool_factory: Box::new(tool_factory),
499 }
500 }
501}
502
503#[async_trait::async_trait]
504impl VxPlugin for StandardPlugin {
505 fn name(&self) -> &str {
506 &self.name
507 }
508
509 fn description(&self) -> &str {
510 &self.description
511 }
512
513 fn version(&self) -> &str {
514 &self.version
515 }
516
517 fn tools(&self) -> Vec<Box<dyn VxTool>> {
518 vec![(self.tool_factory)()]
519 }
520}
521
522pub struct ConfigurableTool {
527 metadata: ToolMetadata,
528 config_manager: FigmentConfigManager,
529 url_builder: Box<dyn UrlBuilder>,
530 version_parser: Box<dyn VersionParser>,
531}
532
533impl ConfigurableTool {
534 pub fn new(
535 metadata: ToolMetadata,
536 url_builder: Box<dyn UrlBuilder>,
537 version_parser: Box<dyn VersionParser>,
538 ) -> Result<Self> {
539 let config_manager =
540 FigmentConfigManager::new().or_else(|_| FigmentConfigManager::minimal())?;
541
542 Ok(Self {
543 metadata,
544 config_manager,
545 url_builder,
546 version_parser,
547 })
548 }
549}
550
551#[async_trait::async_trait]
552impl VxTool for ConfigurableTool {
553 fn name(&self) -> &str {
554 &self.metadata.name
555 }
556
557 fn description(&self) -> &str {
558 &self.metadata.description
559 }
560
561 fn aliases(&self) -> Vec<&str> {
562 self.metadata.aliases.iter().map(|s| s.as_str()).collect()
563 }
564
565 async fn fetch_versions(&self, include_prerelease: bool) -> Result<Vec<VersionInfo>> {
566 let json = HttpUtils::fetch_json(self.url_builder.versions_url()).await?;
567 self.version_parser
568 .parse_versions(&json, include_prerelease)
569 }
570
571 async fn get_download_url(&self, version: &str) -> Result<Option<String>> {
572 if let Ok(url) = self
574 .config_manager
575 .get_download_url(&self.metadata.name, version)
576 {
577 return Ok(Some(url));
578 }
579
580 Ok(self.url_builder.download_url(version))
582 }
583}
584
585#[derive(Debug, Clone)]
587pub struct ToolMetadata {
588 pub name: String,
589 pub description: String,
590 pub aliases: Vec<String>,
591}