1use crate::{GitHubVersionFetcher, NodeVersionFetcher, Result, VersionError, VersionFetcher};
4use serde::{Deserialize, Serialize};
5use std::fmt;
6use std::process::Command;
7use which::which;
8
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
11pub struct Version {
12 pub major: u32,
13 pub minor: u32,
14 pub patch: u32,
15 pub pre: Option<String>,
16}
17
18impl Version {
19 pub fn parse(version_str: &str) -> Result<Self> {
21 let version_str = version_str.trim_start_matches('v');
22
23 let (main_version, pre) = if let Some(dash_pos) = version_str.find('-') {
25 let main = &version_str[..dash_pos];
26 let pre_part = &version_str[dash_pos + 1..];
27 (main, Some(pre_part.to_string()))
28 } else {
29 (version_str, None)
30 };
31
32 let parts: Vec<&str> = main_version.split('.').collect();
34
35 if parts.len() < 3 {
36 return Err(VersionError::InvalidVersion {
37 version: version_str.to_string(),
38 reason: "Version must have at least major.minor.patch".to_string(),
39 });
40 }
41
42 let major = parts[0].parse().map_err(|_| VersionError::InvalidVersion {
43 version: version_str.to_string(),
44 reason: format!("Invalid major version: {}", parts[0]),
45 })?;
46
47 let minor = parts[1].parse().map_err(|_| VersionError::InvalidVersion {
48 version: version_str.to_string(),
49 reason: format!("Invalid minor version: {}", parts[1]),
50 })?;
51
52 let patch = parts[2].parse().map_err(|_| VersionError::InvalidVersion {
53 version: version_str.to_string(),
54 reason: format!("Invalid patch version: {}", parts[2]),
55 })?;
56
57 Ok(Self {
58 major,
59 minor,
60 patch,
61 pre,
62 })
63 }
64
65 pub fn as_string(&self) -> String {
67 match &self.pre {
68 Some(pre) => format!("{}.{}.{}-{}", self.major, self.minor, self.patch, pre),
69 None => format!("{}.{}.{}", self.major, self.minor, self.patch),
70 }
71 }
72
73 pub fn is_prerelease(&self) -> bool {
75 self.pre.is_some()
76 }
77}
78
79impl fmt::Display for Version {
80 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81 write!(f, "{}", self.as_string())
82 }
83}
84
85pub struct VersionManager;
87
88impl VersionManager {
89 pub fn get_installed_version(tool_name: &str) -> Result<Option<Version>> {
91 if which(tool_name).is_err() {
93 return Ok(None);
94 }
95
96 let output = Command::new(tool_name)
98 .arg("--version")
99 .output()
100 .map_err(|e| VersionError::CommandError {
101 command: format!("{} --version", tool_name),
102 source: e,
103 })?;
104
105 if !output.status.success() {
106 return Ok(None);
107 }
108
109 let version_output = String::from_utf8_lossy(&output.stdout);
110 let version_line = version_output.lines().next().unwrap_or("");
111
112 let version_str = Self::extract_version_from_output(version_line)?;
114 let version = Version::parse(&version_str)?;
115
116 Ok(Some(version))
117 }
118
119 pub async fn get_latest_version(tool_name: &str) -> Result<Version> {
121 match tool_name {
122 "uv" => {
123 let fetcher = GitHubVersionFetcher::new("astral-sh", "uv");
124 let version_info = fetcher.get_latest_version().await?.ok_or_else(|| {
125 VersionError::VersionNotFound {
126 version: "latest".to_string(),
127 tool: tool_name.to_string(),
128 }
129 })?;
130 Version::parse(&version_info.version)
131 }
132 "node" => {
133 let fetcher = NodeVersionFetcher::new();
134 let version_info = fetcher.get_latest_version().await?.ok_or_else(|| {
135 VersionError::VersionNotFound {
136 version: "latest".to_string(),
137 tool: tool_name.to_string(),
138 }
139 })?;
140 Version::parse(&version_info.version)
141 }
142 _ => Err(VersionError::Other {
143 message: format!("Unsupported tool for version checking: {}", tool_name),
144 }),
145 }
146 }
147
148 pub fn extract_version_from_output(output: &str) -> Result<String> {
150 let patterns = [
152 r"(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?)", r"v(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?)", ];
155
156 for pattern in &patterns {
157 if let Ok(re) = regex::Regex::new(pattern) {
158 if let Some(captures) = re.captures(output) {
159 if let Some(version) = captures.get(1) {
160 return Ok(version.as_str().to_string());
161 }
162 }
163 }
164 }
165
166 Err(VersionError::Other {
167 message: format!("Could not extract version from output: {}", output),
168 })
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 #[test]
177 fn test_version_parsing() {
178 let version = Version::parse("1.2.3").unwrap();
179 assert_eq!(version.major, 1);
180 assert_eq!(version.minor, 2);
181 assert_eq!(version.patch, 3);
182 assert_eq!(version.pre, None);
183 assert!(!version.is_prerelease());
184
185 let prerelease = Version::parse("1.2.3-alpha.1").unwrap();
186 assert_eq!(prerelease.major, 1);
187 assert_eq!(prerelease.minor, 2);
188 assert_eq!(prerelease.patch, 3);
189 assert_eq!(prerelease.pre, Some("alpha.1".to_string()));
190 assert!(prerelease.is_prerelease());
191 }
192
193 #[test]
194 fn test_version_display() {
195 let version = Version::parse("1.2.3").unwrap();
196 assert_eq!(format!("{}", version), "1.2.3");
197
198 let prerelease = Version::parse("1.2.3-alpha.1").unwrap();
199 assert_eq!(format!("{}", prerelease), "1.2.3-alpha.1");
200 }
201
202 #[test]
203 fn test_version_comparison() {
204 let v1 = Version::parse("1.2.3").unwrap();
205 let v2 = Version::parse("1.2.4").unwrap();
206 let v3 = Version::parse("1.3.0").unwrap();
207
208 assert!(v1 < v2);
209 assert!(v2 < v3);
210 assert!(v1 < v3);
211 }
212
213 #[test]
214 fn test_extract_version_from_output() {
215 assert_eq!(
216 VersionManager::extract_version_from_output("node v18.17.0").unwrap(),
217 "18.17.0"
218 );
219 assert_eq!(
220 VersionManager::extract_version_from_output("uv 0.1.0").unwrap(),
221 "0.1.0"
222 );
223 assert_eq!(
224 VersionManager::extract_version_from_output("go version go1.21.0 linux/amd64").unwrap(),
225 "1.21.0"
226 );
227 }
228}