vx_core/
version_manager.rs1use anyhow::Result;
2use reqwest;
3use serde::{Deserialize, Serialize};
4use std::fmt;
5use std::process::Command;
6use which::which;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct Version {
10 pub major: u32,
11 pub minor: u32,
12 pub patch: u32,
13 pub pre: Option<String>,
14}
15
16impl Version {
17 pub fn parse(version_str: &str) -> Result<Self> {
18 let version_str = version_str.trim_start_matches('v');
19 let parts: Vec<&str> = version_str.split('.').collect();
20
21 if parts.len() < 3 {
22 return Err(anyhow::anyhow!("Invalid version format: {}", version_str));
23 }
24
25 let major = parts[0].parse()?;
26 let minor = parts[1].parse()?;
27
28 let patch_part = parts[2];
30 let (patch, pre) = if let Some(dash_pos) = patch_part.find('-') {
31 let patch = patch_part[..dash_pos].parse()?;
32 let pre = Some(patch_part[dash_pos + 1..].to_string());
33 (patch, pre)
34 } else {
35 (patch_part.parse()?, None)
36 };
37
38 Ok(Self {
39 major,
40 minor,
41 patch,
42 pre,
43 })
44 }
45
46 pub fn as_string(&self) -> String {
47 match &self.pre {
48 Some(pre) => format!("{}.{}.{}-{}", self.major, self.minor, self.patch, pre),
49 None => format!("{}.{}.{}", self.major, self.minor, self.patch),
50 }
51 }
52}
53
54impl fmt::Display for Version {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 write!(f, "{}", self.as_string())
57 }
58}
59
60pub struct VersionManager;
61
62impl VersionManager {
63 pub fn get_installed_version(tool_name: &str) -> Result<Option<Version>> {
65 if which(tool_name).is_err() {
67 return Ok(None);
68 }
69
70 let output = Command::new(tool_name).arg("--version").output()?;
72
73 if !output.status.success() {
74 return Ok(None);
75 }
76
77 let version_output = String::from_utf8_lossy(&output.stdout);
78 let version_line = version_output.lines().next().unwrap_or("");
79
80 let version_str = Self::extract_version_from_output(version_line)?;
82 let version = Version::parse(&version_str)?;
83
84 Ok(Some(version))
85 }
86
87 pub async fn get_latest_version(tool_name: &str) -> Result<Version> {
89 match tool_name {
90 "uv" => Self::get_uv_latest_version().await,
91 "node" => Self::get_node_latest_version().await,
92 _ => Err(anyhow::anyhow!(
93 "Unsupported tool for version checking: {}",
94 tool_name
95 )),
96 }
97 }
98
99 async fn get_uv_latest_version() -> Result<Version> {
100 let client = reqwest::Client::new();
101 let response = client
102 .get("https://api.github.com/repos/astral-sh/uv/releases/latest")
103 .header("User-Agent", "vx-tool")
104 .send()
105 .await?;
106
107 let release: serde_json::Value = response.json().await?;
108 let tag_name = release["tag_name"]
109 .as_str()
110 .ok_or_else(|| anyhow::anyhow!("Could not find tag_name in release"))?;
111
112 Version::parse(tag_name)
113 }
114
115 async fn get_node_latest_version() -> Result<Version> {
116 let client = reqwest::Client::new();
117 let response = client
118 .get("https://nodejs.org/dist/index.json")
119 .header("User-Agent", "vx-tool")
120 .send()
121 .await?;
122
123 let releases: serde_json::Value = response.json().await?;
124 let latest = releases
125 .as_array()
126 .and_then(|arr| arr.first())
127 .ok_or_else(|| anyhow::anyhow!("Could not find latest Node.js version"))?;
128
129 let version_str = latest["version"]
130 .as_str()
131 .ok_or_else(|| anyhow::anyhow!("Could not find version in Node.js release"))?;
132
133 Version::parse(version_str)
134 }
135
136 pub fn extract_version_from_output(output: &str) -> Result<String> {
137 let patterns = [
139 r"(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?)", r"v(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?)", ];
142
143 for pattern in &patterns {
144 if let Ok(re) = regex::Regex::new(pattern) {
145 if let Some(captures) = re.captures(output) {
146 if let Some(version) = captures.get(1) {
147 return Ok(version.as_str().to_string());
148 }
149 }
150 }
151 }
152
153 Err(anyhow::anyhow!(
154 "Could not extract version from output: {}",
155 output
156 ))
157 }
158}