1mod archive;
15mod github;
16mod platform;
17mod version_cache;
18
19use anyhow::{Context, Result};
20use semver::Version;
21use std::env;
22use std::fs;
23
24pub use github::{GitHubAsset, GitHubRelease};
26
27pub fn extract_semver_version(build_version: &str) -> &str {
43 build_version.split('-').next().unwrap_or(build_version)
45}
46
47fn get_current_version() -> Result<(String, Version)> {
57 let full_version = crate::VERSION;
58 let semver_str = extract_semver_version(full_version);
59 let semver_version = Version::parse(semver_str).context(format!(
60 "Failed to parse version from BUILD_VERSION: {}",
61 semver_str
62 ))?;
63
64 Ok((full_version.to_string(), semver_version))
65}
66
67fn get_version_comparison() -> Result<Option<(String, Version, Version, GitHubRelease)>> {
80 let release =
81 github::fetch_latest_release().context("Failed to fetch latest release information")?;
82
83 let (current_version_display, current_version) = get_current_version()?;
84
85 let latest_version_str = release.tag_name.trim_start_matches('v');
87 let latest_version = Version::parse(latest_version_str).context(format!(
88 "Failed to parse latest version: {}",
89 latest_version_str
90 ))?;
91
92 let _ = version_cache::record_version_check(&latest_version.to_string());
95
96 if latest_version <= current_version {
97 println!("Already on the latest version (v{})", current_version);
98 return Ok(None);
99 }
100
101 Ok(Some((
102 current_version_display,
103 current_version,
104 latest_version,
105 release,
106 )))
107}
108
109pub fn check_update() -> Result<Option<String>> {
120 if crate::utils::network_disabled() {
122 return Ok(None);
123 }
124 match get_version_comparison()? {
125 Some((current_version, _, latest_version, release)) => {
126 println!(
127 "Update available: v{} → v{}",
128 extract_semver_version(¤t_version),
129 latest_version
130 );
131 Ok(Some(release.tag_name))
132 }
133 None => Ok(None),
134 }
135}
136
137fn perform_installation(
155 current_version: &str,
156 latest_version: &Version,
157 release: &GitHubRelease,
158) -> Result<()> {
159 let asset_pattern = platform::get_asset_pattern(&latest_version.to_string())?;
161 let asset = release
162 .assets
163 .iter()
164 .find(|a| a.name == asset_pattern)
165 .context(format!(
166 "Update failed: No binary found for {} ({})",
167 env::consts::OS,
168 std::env::consts::ARCH
169 ))?;
170
171 let current_exe =
173 env::current_exe().context("Update failed: Cannot locate current executable")?;
174
175 let temp_dir = env::temp_dir();
177 let archive_path = temp_dir.join(&asset.name);
178
179 github::download_file(&asset.browser_download_url, &archive_path)
180 .context("Update failed: Download error")?;
181
182 let extract_dir = temp_dir.join("vct_update");
184 if extract_dir.exists() {
185 fs::remove_dir_all(&extract_dir)
186 .context("Update failed: Cannot clean temporary directory")?;
187 }
188 fs::create_dir_all(&extract_dir).context("Update failed: Cannot create temporary directory")?;
189
190 let new_binary = if asset.name.ends_with(".tar.gz") {
191 archive::extract_targz(&archive_path, &extract_dir)
192 .context("Update failed: Cannot extract archive")?
193 } else if asset.name.ends_with(".zip") {
194 archive::extract_zip(&archive_path, &extract_dir)
195 .context("Update failed: Cannot extract archive")?
196 } else {
197 anyhow::bail!("Update failed: Unsupported archive format");
198 };
199
200 #[cfg(unix)]
202 platform::perform_update_unix(¤t_exe, &new_binary)
203 .context("Update failed: Cannot replace binary")?;
204
205 #[cfg(windows)]
206 platform::perform_update_windows(¤t_exe, &new_binary)
207 .context("Update failed: Cannot replace binary")?;
208
209 let _ = fs::remove_file(&archive_path);
211 let _ = fs::remove_dir_all(&extract_dir);
212
213 println!(
215 "Upgraded from v{} to v{}",
216 extract_semver_version(current_version),
217 latest_version
218 );
219 println!(
220 "https://github.com/Mai0313/VibeCodingTracker/releases/tag/{}",
221 release.tag_name
222 );
223 println!(
224 "If you like this tool, please star us on GitHub: https://github.com/Mai0313/VibeCodingTracker"
225 );
226
227 Ok(())
228}
229
230pub fn perform_update() -> Result<()> {
241 let Some((current_version, _, latest_version, release)) = get_version_comparison()? else {
243 return Ok(());
245 };
246
247 perform_installation(¤t_version, &latest_version, &release)
248}
249
250pub fn perform_force_update() -> Result<()> {
261 let release =
262 github::fetch_latest_release().context("Failed to fetch latest release information")?;
263
264 let (current_version_display, _) = get_current_version()?;
265
266 let latest_version_str = release.tag_name.trim_start_matches('v');
268 let latest_version = Version::parse(latest_version_str).context(format!(
269 "Failed to parse latest version: {}",
270 latest_version_str
271 ))?;
272
273 let _ = version_cache::record_version_check(&latest_version.to_string());
274
275 perform_installation(¤t_version_display, &latest_version, &release)
276}
277
278pub fn update_interactive(force: bool) -> Result<()> {
291 println!("Checking for updates...");
292
293 if force {
294 perform_force_update()
296 } else {
297 if check_update()?.is_some() {
299 print!("Continue? (y/N): ");
300 std::io::Write::flush(&mut std::io::stdout())?;
301
302 let mut input = String::new();
303 std::io::stdin().read_line(&mut input)?;
304
305 if !input.trim().eq_ignore_ascii_case("y") {
306 println!("Cancelled");
307 return Ok(());
308 }
309 perform_update()
310 } else {
311 Ok(())
312 }
313 }
314}
315
316#[doc(hidden)]
318pub use archive::{extract_targz, extract_zip};
319#[doc(hidden)]
320pub use platform::get_asset_pattern;
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325
326 #[test]
327 fn test_extract_semver_version_clean() {
328 let version = "0.1.6";
330 assert_eq!(extract_semver_version(version), "0.1.6");
331 }
332
333 #[test]
334 fn test_extract_semver_version_with_git_metadata() {
335 let version = "0.1.6-5-g1234567";
337 assert_eq!(extract_semver_version(version), "0.1.6");
338 }
339
340 #[test]
341 fn test_extract_semver_version_with_dirty_flag() {
342 let version = "0.1.6-5-g1234567-dirty";
344 assert_eq!(extract_semver_version(version), "0.1.6");
345 }
346
347 #[test]
348 fn test_extract_semver_version_rc() {
349 let version = "1.0.0-rc.1";
351 assert_eq!(extract_semver_version(version), "1.0.0");
352 }
353
354 #[test]
355 fn test_extract_semver_version_beta() {
356 let version = "2.3.4-beta.2";
358 assert_eq!(extract_semver_version(version), "2.3.4");
359 }
360
361 #[test]
362 fn test_extract_semver_version_alpha() {
363 let version = "0.5.0-alpha";
365 assert_eq!(extract_semver_version(version), "0.5.0");
366 }
367
368 #[test]
369 fn test_extract_semver_version_complex() {
370 let version = "1.2.3-45-gabcdef0-modified";
372 assert_eq!(extract_semver_version(version), "1.2.3");
373 }
374
375 #[test]
376 fn test_extract_semver_version_single_digit() {
377 assert_eq!(extract_semver_version("1.0.0"), "1.0.0");
379 assert_eq!(extract_semver_version("0.0.1"), "0.0.1");
380 }
381
382 #[test]
383 fn test_extract_semver_version_large_numbers() {
384 assert_eq!(extract_semver_version("10.20.30"), "10.20.30");
386 assert_eq!(extract_semver_version("100.200.300-1-g123"), "100.200.300");
387 }
388
389 #[test]
390 fn test_extract_semver_version_empty() {
391 assert_eq!(extract_semver_version(""), "");
393 }
394
395 #[test]
396 fn test_extract_semver_version_no_dashes() {
397 let version = "2.4.8";
399 assert_eq!(extract_semver_version(version), "2.4.8");
400 }
401
402 #[test]
403 fn test_extract_semver_version_multiple_dashes() {
404 let version = "1.0.0-pre-release-candidate";
406 assert_eq!(extract_semver_version(version), "1.0.0");
407 }
408
409 #[test]
410 fn test_extract_semver_version_only_major_minor() {
411 let version = "1.2";
413 assert_eq!(extract_semver_version(version), "1.2");
414 }
415
416 #[test]
417 fn test_extract_semver_version_with_v_prefix() {
418 let version = "v1.2.3-dirty";
421 assert_eq!(extract_semver_version(version), "v1.2.3");
422 }
423
424 #[test]
425 fn test_extract_semver_version_consistency() {
426 let version = "3.1.4-15-g926535-dirty";
428 let result1 = extract_semver_version(version);
429 let result2 = extract_semver_version(version);
430 assert_eq!(result1, result2);
431 }
432
433 #[test]
434 fn test_extract_semver_version_zero_version() {
435 assert_eq!(extract_semver_version("0.0.0"), "0.0.0");
437 assert_eq!(extract_semver_version("0.0.0-dev"), "0.0.0");
438 }
439
440 #[test]
441 fn test_extract_semver_version_patch_zero() {
442 assert_eq!(extract_semver_version("1.5.0"), "1.5.0");
444 assert_eq!(extract_semver_version("2.0.0-rc1"), "2.0.0");
445 }
446}