vx_plugin/tool.rs
1//! Tool plugin trait and related functionality
2//!
3//! This module defines the `VxTool` trait, which is the core interface for implementing
4//! tool support in the vx ecosystem. Tools can be anything from compilers and interpreters
5//! to CLI utilities and development tools.
6
7use crate::{Result, ToolContext, ToolExecutionResult, ToolStatus, VersionInfo};
8use async_trait::async_trait;
9use std::collections::HashMap;
10use std::path::{Path, PathBuf};
11use vx_paths::{with_executable_extension, PathManager};
12
13/// Simplified trait for implementing tool support
14///
15/// This trait provides sensible defaults for most methods, so developers only need
16/// to implement the essential functionality for their specific tool.
17///
18/// # Required Methods
19///
20/// - `name()`: Return the tool name
21/// - `fetch_versions()`: Fetch available versions from the tool's source
22///
23/// # Optional Methods
24///
25/// All other methods have default implementations that work for most tools,
26/// but can be overridden for custom behavior.
27///
28/// # Example
29///
30/// ```rust,no_run
31/// use vx_plugin::{VxTool, VersionInfo, Result};
32/// use async_trait::async_trait;
33///
34/// struct MyTool;
35///
36/// #[async_trait]
37/// impl VxTool for MyTool {
38/// fn name(&self) -> &str {
39/// "mytool"
40/// }
41///
42/// async fn fetch_versions(&self, include_prerelease: bool) -> Result<Vec<VersionInfo>> {
43/// // Fetch versions from your tool's API or registry
44/// Ok(vec![
45/// VersionInfo::new("1.0.0"),
46/// VersionInfo::new("1.1.0"),
47/// ])
48/// }
49/// }
50/// ```
51#[async_trait]
52pub trait VxTool: Send + Sync {
53 /// Tool name (required)
54 ///
55 /// This should be a unique identifier for the tool, typically matching
56 /// the executable name or common name used to invoke the tool.
57 fn name(&self) -> &str;
58
59 /// Tool description (optional, has default)
60 ///
61 /// A human-readable description of what this tool does.
62 fn description(&self) -> &str {
63 "A development tool"
64 }
65
66 /// Supported aliases for this tool (optional)
67 ///
68 /// Alternative names that can be used to refer to this tool.
69 /// For example, "node" might have aliases like "nodejs".
70 fn aliases(&self) -> Vec<&str> {
71 vec![]
72 }
73
74 /// Fetch available versions from the tool's official source
75 ///
76 /// This is the main method developers need to implement. It should
77 /// fetch version information from the tool's official source (GitHub releases,
78 /// package registry, etc.) and return a list of available versions.
79 ///
80 /// # Arguments
81 ///
82 /// * `include_prerelease` - Whether to include prerelease/beta versions
83 ///
84 /// # Returns
85 ///
86 /// A vector of `VersionInfo` objects containing version details.
87 async fn fetch_versions(&self, include_prerelease: bool) -> Result<Vec<VersionInfo>>;
88
89 /// Install a specific version of the tool
90 ///
91 /// Default implementation provides a basic download-and-extract workflow
92 /// that works for most tools. Override this method if your tool requires
93 /// special installation procedures.
94 ///
95 /// # Arguments
96 ///
97 /// * `version` - The version to install
98 /// * `force` - Whether to force reinstallation if already installed
99 async fn install_version(&self, version: &str, force: bool) -> Result<()> {
100 if !force && self.is_version_installed(version).await? {
101 return Err(anyhow::anyhow!(
102 "Version {} of {} is already installed. Use --force to reinstall.",
103 version,
104 self.name()
105 ));
106 }
107
108 let install_dir = self.get_version_install_dir(version);
109 let _exe_path = self.default_install_workflow(version, &install_dir).await?;
110
111 // Verify installation
112 if !self.is_version_installed(version).await? {
113 return Err(anyhow::anyhow!(
114 "Installation verification failed for {} version {}",
115 self.name(),
116 version
117 ));
118 }
119
120 Ok(())
121 }
122 /// Check if a version is installed
123 ///
124 /// Default implementation checks for the existence of the tool's executable
125 /// in the standard vx path structure.
126 async fn is_version_installed(&self, version: &str) -> Result<bool> {
127 let path_manager = PathManager::new().unwrap_or_else(|_| PathManager::default());
128 Ok(path_manager.is_tool_version_installed(self.name(), version))
129 }
130
131 /// Execute the tool with given arguments
132 ///
133 /// Default implementation finds the tool executable and runs it
134 /// with the provided arguments and context.
135 async fn execute(&self, args: &[String], context: &ToolContext) -> Result<ToolExecutionResult> {
136 // Default implementation would use the tool execution logic
137 let _ = (args, context);
138 Ok(ToolExecutionResult::success())
139 }
140
141 /// Get the executable path within an installation directory
142 ///
143 /// Override this if your tool has a non-standard layout.
144 /// The default implementation uses the standard vx path structure.
145 async fn get_executable_path(&self, install_dir: &Path) -> Result<PathBuf> {
146 let exe_name = with_executable_extension(self.name());
147
148 // For standard vx installations, the executable should be directly in the version directory
149 let standard_path = install_dir.join(&exe_name);
150 if standard_path.exists() {
151 return Ok(standard_path);
152 }
153
154 // Try common locations for legacy or non-standard installations
155 let candidates = vec![
156 install_dir.join("bin").join(&exe_name),
157 install_dir.join("Scripts").join(&exe_name), // Windows Python-style
158 ];
159
160 for candidate in candidates {
161 if candidate.exists() {
162 return Ok(candidate);
163 }
164 }
165
166 // Default to standard vx path structure
167 Ok(standard_path)
168 }
169
170 /// Get download URL for a specific version and current platform
171 ///
172 /// Override this to provide platform-specific URLs.
173 /// The default implementation tries to extract URLs from version info.
174 async fn get_download_url(&self, version: &str) -> Result<Option<String>> {
175 let versions = self.fetch_versions(true).await?;
176 Ok(versions
177 .iter()
178 .find(|v| v.version == version)
179 .and_then(|v| v.download_url.clone()))
180 }
181 /// Get installation directory for a specific version
182 ///
183 /// Returns the path where this version of the tool should be installed.
184 /// Uses the standard vx path structure: ~/.vx/tools/<tool>/<version>
185 fn get_version_install_dir(&self, version: &str) -> PathBuf {
186 // Use PathManager for consistent path structure
187 let path_manager = PathManager::new().unwrap_or_else(|_| PathManager::default());
188 path_manager.tool_version_dir(self.name(), version)
189 }
190
191 /// Get base installation directory for this tool
192 ///
193 /// Returns the base directory where all versions of this tool are installed.
194 /// Uses the standard vx path structure: ~/.vx/tools/<tool>
195 fn get_base_install_dir(&self) -> PathBuf {
196 // Use PathManager for consistent path structure
197 let path_manager = PathManager::new().unwrap_or_else(|_| PathManager::default());
198 path_manager.tool_dir(self.name())
199 }
200
201 /// Get the currently active version
202 ///
203 /// Default implementation returns the latest installed version.
204 async fn get_active_version(&self) -> Result<String> {
205 let installed_versions = self.get_installed_versions().await?;
206 installed_versions
207 .first()
208 .cloned()
209 .ok_or_else(|| anyhow::anyhow!("No versions installed for {}", self.name()))
210 }
211
212 /// Get all installed versions
213 ///
214 /// Default implementation uses PathManager to scan for installed versions.
215 async fn get_installed_versions(&self) -> Result<Vec<String>> {
216 let path_manager = PathManager::new().unwrap_or_else(|_| PathManager::default());
217 let mut versions = path_manager.list_tool_versions(self.name())?;
218
219 // Sort versions (newest first)
220 versions.sort_by(|a, b| b.cmp(a));
221 Ok(versions)
222 }
223 /// Remove a specific version of the tool
224 ///
225 /// Default implementation uses PathManager to remove the version.
226 async fn remove_version(&self, version: &str, force: bool) -> Result<()> {
227 let path_manager = PathManager::new().unwrap_or_else(|_| PathManager::default());
228
229 if !path_manager.is_tool_version_installed(self.name(), version) {
230 if !force {
231 return Err(anyhow::anyhow!(
232 "Version {} of {} is not installed",
233 version,
234 self.name()
235 ));
236 }
237 return Ok(());
238 }
239
240 path_manager.remove_tool_version(self.name(), version)?;
241 Ok(())
242 }
243
244 /// Get tool status (installed versions, active version, etc.)
245 ///
246 /// Default implementation gathers status information from other methods.
247 async fn get_status(&self) -> Result<ToolStatus> {
248 let installed_versions = self.get_installed_versions().await?;
249 let current_version = if !installed_versions.is_empty() {
250 self.get_active_version().await.ok()
251 } else {
252 None
253 };
254
255 Ok(ToolStatus {
256 installed: !installed_versions.is_empty(),
257 current_version,
258 installed_versions,
259 })
260 }
261
262 /// Default installation workflow (download + extract)
263 ///
264 /// Most tools can use this as-is. This method handles the common pattern
265 /// of downloading a tool from a URL and extracting it to the installation directory.
266 async fn default_install_workflow(&self, version: &str, install_dir: &Path) -> Result<PathBuf> {
267 // Get download URL
268 let _download_url = self.get_download_url(version).await?.ok_or_else(|| {
269 anyhow::anyhow!(
270 "No download URL found for {} version {}",
271 self.name(),
272 version
273 )
274 })?;
275
276 // Create installation directory
277 std::fs::create_dir_all(install_dir)?;
278
279 // For now, this is a placeholder implementation
280 // In a real implementation, this would:
281 // 1. Download the file from download_url
282 // 2. Extract it to install_dir
283 // 3. Set up any necessary symlinks or scripts
284 // 4. Return the path to the main executable
285
286 // Create executable in standard vx path structure
287 let path_manager = PathManager::new().unwrap_or_else(|_| PathManager::default());
288 let exe_path = path_manager.tool_executable_path(self.name(), version);
289
290 if let Some(parent) = exe_path.parent() {
291 std::fs::create_dir_all(parent)?;
292 }
293
294 // Create a placeholder file to indicate installation
295 std::fs::write(
296 &exe_path,
297 format!(
298 "#!/bin/bash\necho 'This is {} version {}'\n",
299 self.name(),
300 version
301 ),
302 )?;
303
304 // Make it executable on Unix systems
305 #[cfg(unix)]
306 {
307 use std::os::unix::fs::PermissionsExt;
308 let mut perms = std::fs::metadata(&exe_path)?.permissions();
309 perms.set_mode(0o755);
310 std::fs::set_permissions(&exe_path, perms)?;
311 }
312
313 Ok(exe_path)
314 }
315
316 /// Additional metadata for the tool (optional)
317 ///
318 /// Override this to provide tool-specific metadata such as
319 /// supported platforms, configuration options, etc.
320 fn metadata(&self) -> HashMap<String, String> {
321 HashMap::new()
322 }
323}
324
325/// Helper trait for URL builders that can generate download URLs
326pub trait UrlBuilder: Send + Sync {
327 /// Generate download URL for a specific version
328 fn download_url(&self, version: &str) -> Option<String>;
329
330 /// Get the base URL for fetching version information
331 fn versions_url(&self) -> &str;
332}
333
334/// Helper trait for version parsers that can parse API responses
335pub trait VersionParser: Send + Sync {
336 /// Parse version information from JSON response
337 fn parse_versions(
338 &self,
339 json: &serde_json::Value,
340 include_prerelease: bool,
341 ) -> Result<Vec<VersionInfo>>;
342}
343
344/// Configuration-driven tool implementation
345///
346/// This tool uses configuration to determine download URLs and version sources,
347/// making it highly configurable without code changes.
348pub struct ConfigurableTool {
349 metadata: crate::ToolMetadata,
350 url_builder: Box<dyn UrlBuilder>,
351 #[allow(dead_code)]
352 version_parser: Box<dyn VersionParser>,
353}
354
355impl ConfigurableTool {
356 /// Create a new configurable tool
357 pub fn new(
358 metadata: crate::ToolMetadata,
359 url_builder: Box<dyn UrlBuilder>,
360 version_parser: Box<dyn VersionParser>,
361 ) -> Self {
362 Self {
363 metadata,
364 url_builder,
365 version_parser,
366 }
367 }
368
369 /// Get the tool metadata
370 pub fn metadata(&self) -> &crate::ToolMetadata {
371 &self.metadata
372 }
373}
374
375#[async_trait]
376impl VxTool for ConfigurableTool {
377 fn name(&self) -> &str {
378 &self.metadata.name
379 }
380
381 fn description(&self) -> &str {
382 &self.metadata.description
383 }
384
385 fn aliases(&self) -> Vec<&str> {
386 self.metadata.aliases.iter().map(|s| s.as_str()).collect()
387 }
388
389 async fn fetch_versions(&self, include_prerelease: bool) -> Result<Vec<VersionInfo>> {
390 // For now, we'll use a placeholder implementation
391 // In a real implementation, this would fetch from the URL builder's versions URL
392 let _ = include_prerelease;
393
394 // Placeholder: return some example versions
395 Ok(vec![
396 VersionInfo::new("1.0.0"),
397 VersionInfo::new("1.1.0"),
398 VersionInfo::new("2.0.0"),
399 ])
400 }
401
402 async fn get_download_url(&self, version: &str) -> Result<Option<String>> {
403 // Use the URL builder to generate download URL
404 Ok(self.url_builder.download_url(version))
405 }
406
407 fn metadata(&self) -> HashMap<String, String> {
408 self.metadata.metadata.clone()
409 }
410}