vx_core/
version_manager.rs

1use 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        // Handle patch version with pre-release
29        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    /// Check if a tool is installed and get its version
64    pub fn get_installed_version(tool_name: &str) -> Result<Option<Version>> {
65        // Check if tool is available in PATH
66        if which(tool_name).is_err() {
67            return Ok(None);
68        }
69
70        // Try to get version
71        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        // Extract version number from output
81        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    /// Get latest stable version from GitHub releases (for tools that support it)
88    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        // Common patterns for version extraction
138        let patterns = [
139            r"(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?)",  // Standard semver
140            r"v(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?)", // With 'v' prefix
141        ];
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}