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}