vx_tool_uv/
uv_tool.rs

1//! UV tool implementations - Python package management tools
2
3use crate::config::UvUrlBuilder;
4use anyhow::Result;
5use std::collections::HashMap;
6use vx_plugin::{ToolContext, ToolExecutionResult, VersionInfo, VxTool};
7use vx_version::{GitHubVersionFetcher, VersionFetcher};
8
9/// Macro to generate UV tool implementations using VxTool trait
10macro_rules! uv_vx_tool {
11    ($name:ident, $cmd:literal, $desc:literal, $homepage:expr) => {
12        #[derive(Debug, Clone)]
13        pub struct $name {
14            version_fetcher: GitHubVersionFetcher,
15        }
16
17        impl $name {
18            pub fn new() -> Self {
19                Self {
20                    version_fetcher: GitHubVersionFetcher::new("astral-sh", "uv"),
21                }
22            }
23        }
24
25        #[async_trait::async_trait]
26        impl VxTool for $name {
27            fn name(&self) -> &str {
28                $cmd
29            }
30
31            fn description(&self) -> &str {
32                $desc
33            }
34
35            fn aliases(&self) -> Vec<&str> {
36                vec![]
37            }
38
39            async fn fetch_versions(
40                &self,
41                include_prerelease: bool,
42            ) -> Result<Vec<VersionInfo>, anyhow::Error> {
43                // For UV, fetch from GitHub releases
44                self.version_fetcher
45                    .fetch_versions(include_prerelease)
46                    .await
47                    .map_err(|e| anyhow::anyhow!("Failed to fetch versions: {}", e))
48            }
49
50            async fn install_version(
51                &self,
52                version: &str,
53                force: bool,
54            ) -> Result<(), anyhow::Error> {
55                if !force && self.is_version_installed(version).await? {
56                    return Err(anyhow::anyhow!(
57                        "Version {} already installed for {}",
58                        version,
59                        self.name()
60                    ));
61                }
62
63                let install_dir = self.get_version_install_dir(version);
64                let _exe_path = self.default_install_workflow(version, &install_dir).await?;
65
66                // Verify installation
67                if !self.is_version_installed(version).await? {
68                    return Err(anyhow::anyhow!(
69                        "Installation verification failed for {} version {}",
70                        self.name(),
71                        version
72                    ));
73                }
74
75                Ok(())
76            }
77
78            async fn is_version_installed(&self, version: &str) -> Result<bool, anyhow::Error> {
79                // Simple implementation - check if version directory exists
80                let install_dir = self.get_version_install_dir(version);
81                Ok(install_dir.exists())
82            }
83
84            async fn get_active_version(&self) -> Result<String, anyhow::Error> {
85                // Simple implementation - return a default version
86                Ok("latest".to_string())
87            }
88
89            async fn get_installed_versions(&self) -> Result<Vec<String>, anyhow::Error> {
90                // Simple implementation - return empty list
91                Ok(vec![])
92            }
93
94            async fn execute(
95                &self,
96                args: &[String],
97                context: &ToolContext,
98            ) -> Result<ToolExecutionResult, anyhow::Error> {
99                // Simple implementation - execute the tool directly
100                let tool_name = if self.name() == "uvx" {
101                    "uv"
102                } else {
103                    self.name()
104                };
105                let mut cmd = std::process::Command::new(tool_name);
106
107                // For uvx, add "tool run" prefix
108                if self.name() == "uvx" {
109                    cmd.arg("tool");
110                    cmd.arg("run");
111                }
112
113                cmd.args(args);
114
115                if let Some(cwd) = &context.working_directory {
116                    cmd.current_dir(cwd);
117                }
118
119                for (key, value) in &context.environment_variables {
120                    cmd.env(key, value);
121                }
122
123                let status = cmd
124                    .status()
125                    .map_err(|e| anyhow::anyhow!("Failed to execute {}: {}", self.name(), e))?;
126
127                Ok(ToolExecutionResult {
128                    exit_code: status.code().unwrap_or(1),
129                    stdout: None,
130                    stderr: None,
131                })
132            }
133
134            async fn get_download_url(
135                &self,
136                version: &str,
137            ) -> Result<Option<String>, anyhow::Error> {
138                use vx_tool_standard::StandardUrlBuilder;
139                if version == "latest" {
140                    // For latest, get the actual latest version first
141                    let versions = self.fetch_versions(false).await?;
142                    if let Some(latest_version) = versions.first() {
143                        return Ok(UvUrlBuilder::download_url(&latest_version.version));
144                    }
145                    return Ok(None);
146                }
147                Ok(UvUrlBuilder::download_url(version))
148            }
149
150            fn metadata(&self) -> HashMap<String, String> {
151                let mut meta = HashMap::new();
152                meta.insert("homepage".to_string(), $homepage.unwrap_or("").to_string());
153                meta.insert("ecosystem".to_string(), "python".to_string());
154                meta.insert(
155                    "repository".to_string(),
156                    "https://github.com/astral-sh/uv".to_string(),
157                );
158                meta.insert("license".to_string(), "MIT OR Apache-2.0".to_string());
159                meta
160            }
161        }
162
163        impl Default for $name {
164            fn default() -> Self {
165                Self::new()
166            }
167        }
168    };
169}
170
171// Define UV tools using the VxTool macro
172uv_vx_tool!(
173    UvCommand,
174    "uv",
175    "An extremely fast Python package installer and resolver",
176    Some("https://docs.astral.sh/uv/")
177);
178uv_vx_tool!(
179    UvxTool,
180    "uvx",
181    "Python application runner",
182    Some("https://docs.astral.sh/uv/")
183);
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn test_uv_tool_creation() {
191        let tool = UvCommand::new();
192        assert_eq!(tool.name(), "uv");
193        assert!(!tool.description().is_empty());
194    }
195
196    #[test]
197    fn test_uvx_tool_creation() {
198        let tool = UvxTool::new();
199        assert_eq!(tool.name(), "uvx");
200        assert!(!tool.description().is_empty());
201    }
202
203    #[test]
204    fn test_uv_tool_metadata() {
205        let tool = UvCommand::new();
206        let metadata = tool.metadata();
207
208        assert!(metadata.contains_key("homepage"));
209        assert!(metadata.contains_key("ecosystem"));
210        assert_eq!(metadata.get("ecosystem"), Some(&"python".to_string()));
211    }
212}