vx_core/
proxy.rs

1//! Transparent proxy system for vx
2//!
3//! This module provides the core transparent proxy functionality that allows
4//! users to run tools through vx without explicit activation or PATH manipulation.
5//!
6//! The proxy system:
7//! - Automatically detects project configuration (.vx.toml)
8//! - Resolves the correct tool version for the current context
9//! - Ensures tools are installed (with auto-install if enabled)
10//! - Transparently executes the tool with the correct version
11
12use crate::{config_figment::FigmentConfigManager, PluginRegistry, Result, VenvManager, VxError};
13use std::env;
14use std::path::PathBuf;
15use std::process::{Command, Stdio};
16
17/// Transparent proxy for tool execution
18pub struct ToolProxy {
19    /// Virtual environment manager for project context
20    venv_manager: VenvManager,
21    /// Plugin registry for tool resolution
22    plugin_registry: PluginRegistry,
23    /// Configuration manager for project and global settings
24    config_manager: FigmentConfigManager,
25}
26
27/// Tool execution context
28#[derive(Debug, Clone)]
29pub struct ProxyContext {
30    /// Tool name being executed
31    pub tool_name: String,
32    /// Arguments passed to the tool
33    pub args: Vec<String>,
34    /// Current working directory
35    pub working_dir: PathBuf,
36    /// Environment variables
37    pub env_vars: std::collections::HashMap<String, String>,
38}
39
40impl ToolProxy {
41    /// Create a new tool proxy
42    pub fn new() -> Result<Self> {
43        let venv_manager = VenvManager::new()?;
44        let plugin_registry = PluginRegistry::new();
45        let config_manager = FigmentConfigManager::new()?;
46
47        Ok(Self {
48            venv_manager,
49            plugin_registry,
50            config_manager,
51        })
52    }
53
54    /// Execute a tool transparently through the proxy
55    pub async fn execute_tool(&self, tool_name: &str, args: &[String]) -> Result<i32> {
56        // Create execution context
57        let context = ProxyContext {
58            tool_name: tool_name.to_string(),
59            args: args.to_vec(),
60            working_dir: env::current_dir().map_err(|e| VxError::Other {
61                message: format!("Failed to get current directory: {}", e),
62            })?,
63            env_vars: env::vars().collect(),
64        };
65
66        // Resolve the tool executable path
67        let executable_path = self.resolve_tool_executable(&context).await?;
68
69        // Execute the tool
70        self.execute_with_path(&executable_path, &context).await
71    }
72
73    /// Resolve the executable path for a tool in the current context
74    async fn resolve_tool_executable(&self, context: &ProxyContext) -> Result<PathBuf> {
75        // First, try to ensure the tool is available through the venv manager
76        // This handles project-specific version resolution and auto-installation
77        match self
78            .venv_manager
79            .ensure_tool_available(&context.tool_name)
80            .await
81        {
82            Ok(path) => return Ok(path),
83            Err(_) => {
84                // If venv manager fails, try to find the tool through plugins
85                // This is a fallback for tools not managed by vx
86            }
87        }
88
89        // Try to find the tool through the plugin registry
90        if let Some(tool) = self.plugin_registry.get_tool(&context.tool_name) {
91            // Check if any version is installed
92            let installed_versions = tool.get_installed_versions().await?;
93            if let Some(latest_version) = installed_versions.first() {
94                let install_dir = tool.get_version_install_dir(latest_version);
95                // Use VxEnvironment to find the executable
96                let env = crate::VxEnvironment::new()?;
97                return env.find_executable_in_dir(&install_dir, &context.tool_name);
98            } else {
99                // No version installed, try auto-installation
100                return self
101                    .auto_install_tool(&context.tool_name, tool.as_ref())
102                    .await;
103            }
104        }
105
106        // Last resort: check system PATH
107        if let Ok(path) = which::which(&context.tool_name) {
108            return Ok(path);
109        }
110
111        Err(VxError::Other {
112            message: format!(
113                "Tool '{}' not found. Install it with 'vx install {}' or ensure it's available in your PATH.",
114                context.tool_name, context.tool_name
115            ),
116        })
117    }
118
119    /// Execute a tool with the resolved executable path
120    async fn execute_with_path(
121        &self,
122        executable_path: &PathBuf,
123        context: &ProxyContext,
124    ) -> Result<i32> {
125        let mut command = Command::new(executable_path);
126
127        // Add arguments
128        command.args(&context.args);
129
130        // Set working directory
131        command.current_dir(&context.working_dir);
132
133        // Set environment variables
134        for (key, value) in &context.env_vars {
135            command.env(key, value);
136        }
137
138        // Configure stdio to inherit from parent process
139        command.stdin(Stdio::inherit());
140        command.stdout(Stdio::inherit());
141        command.stderr(Stdio::inherit());
142
143        // Execute the command
144        let mut child = command.spawn().map_err(|e| VxError::Other {
145            message: format!("Failed to execute tool '{}': {}", context.tool_name, e),
146        })?;
147
148        // Wait for the process to complete
149        let status = child.wait().map_err(|e| VxError::Other {
150            message: format!("Failed to wait for tool '{}': {}", context.tool_name, e),
151        })?;
152
153        // Return the exit code
154        Ok(status.code().unwrap_or(-1))
155    }
156
157    /// Auto-install a tool if auto-installation is enabled
158    async fn auto_install_tool(
159        &self,
160        tool_name: &str,
161        tool: &dyn crate::plugin::VxTool,
162    ) -> Result<PathBuf> {
163        // Check if auto-installation is enabled from configuration
164        let auto_install_enabled = self.config_manager.config().defaults.auto_install;
165
166        if !auto_install_enabled {
167            return Err(VxError::Other {
168                message: format!(
169                    "Tool '{}' is not installed and auto-installation is disabled. Install it with 'vx install {}'.",
170                    tool_name, tool_name
171                ),
172            });
173        }
174
175        // Get the latest available version
176        let available_versions = tool.fetch_versions(false).await?;
177        let latest_version = available_versions.first().ok_or_else(|| VxError::Other {
178            message: format!("No versions available for tool '{}'", tool_name),
179        })?;
180
181        // Install the latest version
182        tool.install_version(&latest_version.version, false).await?;
183
184        // Get the installation directory and find the executable
185        let install_dir = tool.get_version_install_dir(&latest_version.version);
186        let env = crate::VxEnvironment::new()?;
187        env.find_executable_in_dir(&install_dir, tool_name)
188    }
189
190    /// Check if a tool is available (installed or in PATH)
191    pub async fn is_tool_available(&self, tool_name: &str) -> bool {
192        // Check through venv manager first
193        if self
194            .venv_manager
195            .ensure_tool_available(tool_name)
196            .await
197            .is_ok()
198        {
199            return true;
200        }
201
202        // Check through plugin registry
203        if let Some(tool) = self.plugin_registry.get_tool(tool_name) {
204            if let Ok(versions) = tool.get_installed_versions().await {
205                if !versions.is_empty() {
206                    return true;
207                }
208            }
209        }
210
211        // Check system PATH
212        which::which(tool_name).is_ok()
213    }
214
215    /// Get the version of a tool that would be used in the current context
216    pub async fn get_effective_version(&self, tool_name: &str) -> Result<String> {
217        // Try to get project-specific version from configuration first
218        if let Some(version) = self.config_manager.get_project_tool_version(tool_name) {
219            return Ok(version);
220        }
221
222        // Fallback to venv manager
223        if let Ok(Some(version)) = self.venv_manager.get_project_tool_version(tool_name).await {
224            return Ok(version);
225        }
226
227        // Try to get version from installed tools
228        if let Some(tool) = self.plugin_registry.get_tool(tool_name) {
229            let versions = tool.get_installed_versions().await?;
230            if let Some(latest) = versions.first() {
231                return Ok(latest.clone());
232            }
233        }
234
235        // Try to get version from system tool
236        if let Ok(path) = which::which(tool_name) {
237            // Try to execute tool --version to get version
238            if let Ok(output) = Command::new(&path).arg("--version").output() {
239                if output.status.success() {
240                    let version_output = String::from_utf8_lossy(&output.stdout);
241                    // Extract version from output (this is a simplified approach)
242                    if let Some(line) = version_output.lines().next() {
243                        return Ok(line.to_string());
244                    }
245                }
246            }
247        }
248
249        Err(VxError::Other {
250            message: format!("Could not determine version for tool '{}'", tool_name),
251        })
252    }
253
254    /// Get the configuration manager
255    pub fn config_manager(&self) -> &FigmentConfigManager {
256        &self.config_manager
257    }
258
259    /// Validate the current configuration
260    pub fn validate_config(&self) -> Result<Vec<String>> {
261        self.config_manager.validate()
262    }
263
264    /// Initialize a new project configuration
265    pub fn init_project_config(
266        &self,
267        tools: Option<std::collections::HashMap<String, String>>,
268        interactive: bool,
269    ) -> Result<()> {
270        self.config_manager.init_project_config(tools, interactive)
271    }
272
273    /// Sync project configuration (install all required tools)
274    pub async fn sync_project(&self, force: bool) -> Result<Vec<String>> {
275        self.config_manager.sync_project(force).await
276    }
277}
278
279impl Default for ToolProxy {
280    fn default() -> Self {
281        Self::new().expect("Failed to create ToolProxy")
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288
289    #[tokio::test]
290    async fn test_tool_proxy_creation() {
291        let proxy = ToolProxy::new();
292        assert!(proxy.is_ok());
293    }
294
295    #[tokio::test]
296    async fn test_is_tool_available() {
297        let proxy = ToolProxy::new().unwrap();
298
299        // Test with a tool that should be available on most systems
300        let available = proxy.is_tool_available("echo").await;
301        // Note: This might fail on some systems, but it's a basic test
302        // In a real test environment, we'd mock the dependencies
303        println!("Echo available: {}", available);
304    }
305
306    #[tokio::test]
307    async fn test_auto_install_functionality() {
308        let proxy = ToolProxy::new().unwrap();
309
310        // Test that auto-install logic is properly integrated
311        // This test verifies the method exists and can be called
312        // In a real environment, this would test with a mock tool
313
314        // For now, just test that the proxy can be created and basic methods work
315        assert!(proxy.plugin_registry.get_tool("nonexistent").is_none());
316
317        // Test effective version retrieval for system tools
318        if let Ok(version) = proxy.get_effective_version("echo").await {
319            println!("Echo version: {}", version);
320        }
321    }
322
323    #[tokio::test]
324    async fn test_config_management() {
325        let proxy = ToolProxy::new().unwrap();
326
327        // Test configuration validation
328        let validation_result = proxy.validate_config();
329        assert!(validation_result.is_ok());
330
331        // Test configuration access
332        let config = proxy.config_manager().config();
333        assert!(config.defaults.auto_install); // Should be true by default
334
335        // Test project tool version retrieval
336        let version = proxy
337            .config_manager()
338            .get_project_tool_version("nonexistent");
339        assert!(version.is_none()); // Should be None for non-existent tools
340
341        println!("Configuration management tests passed");
342    }
343
344    #[tokio::test]
345    async fn test_proxy_context_creation() {
346        let context = ProxyContext {
347            tool_name: "test-tool".to_string(),
348            args: vec!["--version".to_string()],
349            working_dir: std::env::current_dir().unwrap(),
350            env_vars: std::env::vars().collect(),
351        };
352
353        assert_eq!(context.tool_name, "test-tool");
354        assert_eq!(context.args, vec!["--version"]);
355        assert!(!context.env_vars.is_empty());
356    }
357
358    #[tokio::test]
359    async fn test_proxy_initialization() {
360        let proxy = ToolProxy::new();
361        assert!(proxy.is_ok(), "ToolProxy creation should succeed");
362
363        if let Ok(proxy) = proxy {
364            // Test that all components are properly initialized
365            let config = proxy.config_manager().config();
366            assert!(config.defaults.auto_install); // Default should be true
367
368            // Test validation
369            let validation = proxy.validate_config();
370            assert!(validation.is_ok(), "Config validation should succeed");
371        }
372    }
373
374    #[tokio::test]
375    async fn test_effective_version_resolution() {
376        let proxy = ToolProxy::new().unwrap();
377
378        // Test with non-existent tool
379        let result = proxy.get_effective_version("nonexistent-tool").await;
380        assert!(result.is_err(), "Should fail for non-existent tool");
381
382        // Test with system tool (if available)
383        if let Ok(version) = proxy.get_effective_version("echo").await {
384            assert!(!version.is_empty(), "Version should not be empty");
385        }
386    }
387
388    #[tokio::test]
389    async fn test_tool_availability_check() {
390        let proxy = ToolProxy::new().unwrap();
391
392        // Test with system tool
393        let available = proxy.is_tool_available("echo").await;
394        // This might be true or false depending on the system, but should not panic
395        println!("Echo available: {}", available);
396
397        // Test with definitely non-existent tool
398        let not_available = proxy
399            .is_tool_available("definitely-nonexistent-tool-12345")
400            .await;
401        assert!(!not_available, "Non-existent tool should not be available");
402    }
403
404    #[tokio::test]
405    async fn test_config_integration() {
406        let proxy = ToolProxy::new().unwrap();
407
408        // Test that configuration is properly integrated
409        let config = proxy.config_manager().config();
410
411        // Test default values
412        assert!(config.defaults.auto_install);
413        assert!(!config.defaults.default_registry.is_empty());
414
415        // Test tool configuration access
416        let tool_config = proxy.config_manager().get_tool_config("nonexistent");
417        assert!(tool_config.is_none());
418    }
419}