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