1use crate::{config_figment::FigmentConfigManager, PluginRegistry, Result, VenvManager, VxError};
13use std::env;
14use std::path::PathBuf;
15use std::process::{Command, Stdio};
16
17pub struct ToolProxy {
19 venv_manager: VenvManager,
21 plugin_registry: PluginRegistry,
23 config_manager: FigmentConfigManager,
25}
26
27#[derive(Debug, Clone)]
29pub struct ProxyContext {
30 pub tool_name: String,
32 pub args: Vec<String>,
34 pub working_dir: PathBuf,
36 pub env_vars: std::collections::HashMap<String, String>,
38}
39
40impl ToolProxy {
41 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 pub async fn execute_tool(&self, tool_name: &str, args: &[String]) -> Result<i32> {
56 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 let executable_path = self.resolve_tool_executable(&context).await?;
68
69 self.execute_with_path(&executable_path, &context).await
71 }
72
73 async fn resolve_tool_executable(&self, context: &ProxyContext) -> Result<PathBuf> {
75 match self
78 .venv_manager
79 .ensure_tool_available(&context.tool_name)
80 .await
81 {
82 Ok(path) => return Ok(path),
83 Err(_) => {
84 }
87 }
88
89 if let Some(tool) = self.plugin_registry.get_tool(&context.tool_name) {
91 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 let env = crate::VxEnvironment::new()?;
97 return env.find_executable_in_dir(&install_dir, &context.tool_name);
98 } else {
99 return self
101 .auto_install_tool(&context.tool_name, tool.as_ref())
102 .await;
103 }
104 }
105
106 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 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 command.args(&context.args);
129
130 command.current_dir(&context.working_dir);
132
133 for (key, value) in &context.env_vars {
135 command.env(key, value);
136 }
137
138 command.stdin(Stdio::inherit());
140 command.stdout(Stdio::inherit());
141 command.stderr(Stdio::inherit());
142
143 let mut child = command.spawn().map_err(|e| VxError::Other {
145 message: format!("Failed to execute tool '{}': {}", context.tool_name, e),
146 })?;
147
148 let status = child.wait().map_err(|e| VxError::Other {
150 message: format!("Failed to wait for tool '{}': {}", context.tool_name, e),
151 })?;
152
153 Ok(status.code().unwrap_or(-1))
155 }
156
157 async fn auto_install_tool(
159 &self,
160 tool_name: &str,
161 tool: &dyn crate::plugin::VxTool,
162 ) -> Result<PathBuf> {
163 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 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 tool.install_version(&latest_version.version, false).await?;
183
184 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 pub async fn is_tool_available(&self, tool_name: &str) -> bool {
192 if self
194 .venv_manager
195 .ensure_tool_available(tool_name)
196 .await
197 .is_ok()
198 {
199 return true;
200 }
201
202 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 which::which(tool_name).is_ok()
213 }
214
215 pub async fn get_effective_version(&self, tool_name: &str) -> Result<String> {
217 if let Some(version) = self.config_manager.get_project_tool_version(tool_name) {
219 return Ok(version);
220 }
221
222 if let Ok(Some(version)) = self.venv_manager.get_project_tool_version(tool_name).await {
224 return Ok(version);
225 }
226
227 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 if let Ok(path) = which::which(tool_name) {
237 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 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 pub fn config_manager(&self) -> &FigmentConfigManager {
256 &self.config_manager
257 }
258
259 pub fn validate_config(&self) -> Result<Vec<String>> {
261 self.config_manager.validate()
262 }
263
264 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 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 let available = proxy.is_tool_available("echo").await;
301 println!("Echo available: {}", available);
304 }
305
306 #[tokio::test]
307 async fn test_auto_install_functionality() {
308 let proxy = ToolProxy::new().unwrap();
309
310 assert!(proxy.plugin_registry.get_tool("nonexistent").is_none());
316
317 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 let validation_result = proxy.validate_config();
329 assert!(validation_result.is_ok());
330
331 let config = proxy.config_manager().config();
333 assert!(config.defaults.auto_install); let version = proxy
337 .config_manager()
338 .get_project_tool_version("nonexistent");
339 assert!(version.is_none()); 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 let config = proxy.config_manager().config();
366 assert!(config.defaults.auto_install); 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 let result = proxy.get_effective_version("nonexistent-tool").await;
380 assert!(result.is_err(), "Should fail for non-existent tool");
381
382 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 let available = proxy.is_tool_available("echo").await;
394 println!("Echo available: {}", available);
396
397 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 let config = proxy.config_manager().config();
410
411 assert!(config.defaults.auto_install);
413 assert!(!config.defaults.default_registry.is_empty());
414
415 let tool_config = proxy.config_manager().get_tool_config("nonexistent");
417 assert!(tool_config.is_none());
418 }
419}