1use crate::config::UvUrlBuilder;
4use anyhow::Result;
5use std::collections::HashMap;
6use vx_plugin::{ToolContext, ToolExecutionResult, VersionInfo, VxTool};
7use vx_version::{GitHubVersionFetcher, VersionFetcher};
8
9macro_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 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 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 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 Ok("latest".to_string())
87 }
88
89 async fn get_installed_versions(&self) -> Result<Vec<String>, anyhow::Error> {
90 Ok(vec![])
92 }
93
94 async fn execute(
95 &self,
96 args: &[String],
97 context: &ToolContext,
98 ) -> Result<ToolExecutionResult, anyhow::Error> {
99 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 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 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
171uv_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}