1use std::collections::HashMap;
4use vx_core::{
5 GitHubVersionParser, HttpUtils, Result, ToolContext, ToolExecutionResult, UvUrlBuilder,
6 VersionInfo, VxEnvironment, VxError, VxTool,
7};
8
9macro_rules! uv_vx_tool {
11 ($name:ident, $cmd:literal, $desc:literal, $homepage:expr) => {
12 #[derive(Debug, Clone)]
13 pub struct $name {
14 _url_builder: UvUrlBuilder,
15 _version_parser: GitHubVersionParser,
16 }
17
18 impl $name {
19 pub fn new() -> Self {
20 Self {
21 _url_builder: UvUrlBuilder::new(),
22 _version_parser: GitHubVersionParser::new("astral-sh", "uv"),
23 }
24 }
25 }
26
27 #[async_trait::async_trait]
28 impl VxTool for $name {
29 fn name(&self) -> &str {
30 $cmd
31 }
32
33 fn description(&self) -> &str {
34 $desc
35 }
36
37 fn aliases(&self) -> Vec<&str> {
38 vec![]
39 }
40
41 async fn fetch_versions(&self, include_prerelease: bool) -> Result<Vec<VersionInfo>> {
42 let json = HttpUtils::fetch_json(UvUrlBuilder::versions_url()).await?;
44 GitHubVersionParser::parse_versions(&json, include_prerelease)
45 }
46
47 async fn install_version(&self, version: &str, force: bool) -> Result<()> {
48 if !force && self.is_version_installed(version).await? {
49 return Err(vx_core::VxError::VersionAlreadyInstalled {
50 tool_name: self.name().to_string(),
51 version: version.to_string(),
52 });
53 }
54
55 let install_dir = self.get_version_install_dir(version);
56 let _exe_path = self.default_install_workflow(version, &install_dir).await?;
57
58 if !self.is_version_installed(version).await? {
60 return Err(vx_core::VxError::InstallationFailed {
61 tool_name: self.name().to_string(),
62 version: version.to_string(),
63 message: "Installation verification failed".to_string(),
64 });
65 }
66
67 Ok(())
68 }
69
70 async fn is_version_installed(&self, version: &str) -> Result<bool> {
71 let env = VxEnvironment::new().expect("Failed to create VX environment");
72
73 if self.name() == "uvx" {
75 return Ok(env.is_version_installed("uv", version));
76 }
77
78 Ok(env.is_version_installed(self.name(), version))
79 }
80
81 async fn get_active_version(&self) -> Result<String> {
82 let env = VxEnvironment::new().expect("Failed to create VX environment");
83
84 if self.name() == "uvx" {
86 if let Some(active_version) = env.get_active_version("uv")? {
87 return Ok(active_version);
88 }
89
90 let installed_versions = env.list_installed_versions("uv")?;
91 return installed_versions.first().cloned().ok_or_else(|| {
92 VxError::ToolNotInstalled {
93 tool_name: "uv".to_string(),
94 }
95 });
96 }
97
98 if let Some(active_version) = env.get_active_version(self.name())? {
100 return Ok(active_version);
101 }
102
103 let installed_versions = env.list_installed_versions(self.name())?;
104 installed_versions
105 .first()
106 .cloned()
107 .ok_or_else(|| VxError::ToolNotInstalled {
108 tool_name: self.name().to_string(),
109 })
110 }
111
112 async fn get_installed_versions(&self) -> Result<Vec<String>> {
113 let env = VxEnvironment::new().expect("Failed to create VX environment");
114
115 if self.name() == "uvx" {
117 return env.list_installed_versions("uv");
118 }
119
120 env.list_installed_versions(self.name())
121 }
122
123 async fn execute(
124 &self,
125 args: &[String],
126 context: &ToolContext,
127 ) -> Result<ToolExecutionResult> {
128 if self.name() == "uvx" && !context.use_system_path {
130 let active_version = self.get_active_version().await?;
131 let env = VxEnvironment::new().expect("Failed to create VX environment");
132 let uv_install_dir = env.get_version_install_dir("uv", &active_version);
133
134 let exe_path = env.find_executable_in_dir(&uv_install_dir, "uv")?;
136
137 let mut cmd = std::process::Command::new(&exe_path);
139 cmd.arg("tool");
140 cmd.arg("run");
141 cmd.args(args);
142
143 if let Some(cwd) = &context.working_directory {
144 cmd.current_dir(cwd);
145 }
146
147 for (key, value) in &context.environment_variables {
148 cmd.env(key, value);
149 }
150
151 let status = cmd.status().map_err(|e| VxError::Other {
152 message: format!("Failed to execute {}: {}", self.name(), e),
153 })?;
154
155 return Ok(ToolExecutionResult {
156 exit_code: status.code().unwrap_or(1),
157 stdout: None,
158 stderr: None,
159 });
160 }
161
162 self.default_execute_workflow(args, context).await
164 }
165
166 async fn get_download_url(&self, version: &str) -> Result<Option<String>> {
167 if version == "latest" {
168 let versions = self.fetch_versions(false).await?;
170 if let Some(latest_version) = versions.first() {
171 return Ok(UvUrlBuilder::download_url(&latest_version.version));
172 }
173 return Ok(None);
174 }
175 Ok(UvUrlBuilder::download_url(version))
176 }
177
178 fn metadata(&self) -> HashMap<String, String> {
179 let mut meta = HashMap::new();
180 meta.insert("homepage".to_string(), $homepage.unwrap_or("").to_string());
181 meta.insert("ecosystem".to_string(), "python".to_string());
182 meta.insert(
183 "repository".to_string(),
184 "https://github.com/astral-sh/uv".to_string(),
185 );
186 meta.insert("license".to_string(), "MIT OR Apache-2.0".to_string());
187 meta
188 }
189 }
190
191 impl Default for $name {
192 fn default() -> Self {
193 Self::new()
194 }
195 }
196 };
197}
198
199uv_vx_tool!(
201 UvCommand,
202 "uv",
203 "An extremely fast Python package installer and resolver",
204 Some("https://docs.astral.sh/uv/")
205);
206uv_vx_tool!(
207 UvxTool,
208 "uvx",
209 "Python application runner",
210 Some("https://docs.astral.sh/uv/")
211);
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
218 fn test_uv_tool_creation() {
219 let tool = UvCommand::new();
220 assert_eq!(tool.name(), "uv");
221 assert!(!tool.description().is_empty());
222 }
223
224 #[test]
225 fn test_uvx_tool_creation() {
226 let tool = UvxTool::new();
227 assert_eq!(tool.name(), "uvx");
228 assert!(!tool.description().is_empty());
229 }
230
231 #[test]
232 fn test_uv_tool_metadata() {
233 let tool = UvCommand::new();
234 let metadata = tool.metadata();
235
236 assert!(metadata.contains_key("homepage"));
237 assert!(metadata.contains_key("ecosystem"));
238 assert_eq!(metadata.get("ecosystem"), Some(&"python".to_string()));
239 }
240}