1use anyhow::{Context, Result};
2use colored::Colorize;
3use std::time::Duration;
4
5const GITHUB_REPO: &str = "gtkacz/auto-commit-rs";
6const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
7
8pub struct VersionCheck {
9 pub latest: String,
10 pub current: String,
11 pub update_available: bool,
12}
13
14pub fn fetch_latest_version() -> Result<String> {
16 let url = format!(
17 "https://api.github.com/repos/{}/releases/latest",
18 GITHUB_REPO
19 );
20 let agent = ureq::AgentBuilder::new()
21 .timeout(Duration::from_secs(5))
22 .build();
23 let response: serde_json::Value = agent
24 .get(&url)
25 .set("User-Agent", "cgen")
26 .set("Accept", "application/vnd.github.v3+json")
27 .call()
28 .context("Failed to reach GitHub API")?
29 .into_json()
30 .context("Failed to parse GitHub API response")?;
31
32 let tag = response["tag_name"]
33 .as_str()
34 .context("No tag_name in GitHub release response")?;
35
36 Ok(tag.to_string())
37}
38
39pub fn parse_semver(version: &str) -> Option<(u64, u64, u64)> {
41 let v = version.strip_prefix('v').unwrap_or(version);
42 let parts: Vec<&str> = v.split('.').collect();
43 if parts.len() != 3 {
44 return None;
45 }
46 Some((
47 parts[0].parse().ok()?,
48 parts[1].parse().ok()?,
49 parts[2].parse().ok()?,
50 ))
51}
52
53pub fn check_version() -> Result<VersionCheck> {
55 let latest = fetch_latest_version()?;
56 let current = CURRENT_VERSION.to_string();
57
58 let update_available = match (parse_semver(&latest), parse_semver(¤t)) {
59 (Some(latest_v), Some(current_v)) => latest_v > current_v,
60 _ => false,
61 };
62
63 Ok(VersionCheck {
64 latest,
65 current,
66 update_available,
67 })
68}
69
70pub fn run_update() -> Result<()> {
72 if is_cargo_available() {
73 println!("{}", "Updating via cargo...".cyan().bold());
74 let status = std::process::Command::new("cargo")
75 .args(["install", "auto-commit-rs"])
76 .status()
77 .context("Failed to run cargo install")?;
78
79 if !status.success() {
80 anyhow::bail!("cargo install failed with exit code {}", status);
81 }
82 } else {
83 run_platform_installer()?;
84 }
85
86 println!("{}", "Update complete!".green().bold());
87 Ok(())
88}
89
90fn is_cargo_available() -> bool {
91 std::process::Command::new("cargo")
92 .arg("--version")
93 .stdout(std::process::Stdio::null())
94 .stderr(std::process::Stdio::null())
95 .status()
96 .map(|s| s.success())
97 .unwrap_or(false)
98}
99
100fn run_platform_installer() -> Result<()> {
101 if cfg!(target_os = "windows") {
102 println!("{}", "Updating via PowerShell installer...".cyan().bold());
103 let status = std::process::Command::new("powershell")
104 .args([
105 "-ExecutionPolicy",
106 "Bypass",
107 "-Command",
108 "irm https://raw.githubusercontent.com/gtkacz/auto-commit-rs/main/scripts/install.ps1 | iex",
109 ])
110 .status()
111 .context("Failed to run PowerShell installer")?;
112
113 if !status.success() {
114 anyhow::bail!("PowerShell installer failed");
115 }
116 } else {
117 println!("{}", "Updating via install script...".cyan().bold());
118 let status = std::process::Command::new("bash")
119 .args([
120 "-c",
121 "curl -fsSL https://raw.githubusercontent.com/gtkacz/auto-commit-rs/main/scripts/install.sh | bash",
122 ])
123 .status()
124 .context("Failed to run install script")?;
125
126 if !status.success() {
127 anyhow::bail!("Install script failed");
128 }
129 }
130
131 Ok(())
132}
133
134pub fn print_update_warning(latest: &str) {
136 eprintln!(
137 "\n{} {} → {} (run {} to update)",
138 "Update available!".yellow().bold(),
139 CURRENT_VERSION.dimmed(),
140 latest.green(),
141 "cgen update".cyan(),
142 );
143}
144
145pub fn current_version() -> &'static str {
146 CURRENT_VERSION
147}