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}