mod archive;
mod github;
mod platform;
mod version_cache;
use anyhow::{Context, Result};
use semver::Version;
use std::env;
use std::fs;
pub use github::{GitHubAsset, GitHubRelease};
pub fn extract_semver_version(build_version: &str) -> &str {
build_version.split('-').next().unwrap_or(build_version)
}
fn get_current_version() -> Result<(String, Version)> {
let full_version = crate::VERSION;
let semver_str = extract_semver_version(full_version);
let semver_version = Version::parse(semver_str).context(format!(
"Failed to parse version from BUILD_VERSION: {}",
semver_str
))?;
Ok((full_version.to_string(), semver_version))
}
fn get_version_comparison() -> Result<Option<(String, Version, Version, GitHubRelease)>> {
let release =
github::fetch_latest_release().context("Failed to fetch latest release information")?;
let (current_version_display, current_version) = get_current_version()?;
let latest_version_str = release.tag_name.trim_start_matches('v');
let latest_version = Version::parse(latest_version_str).context(format!(
"Failed to parse latest version: {}",
latest_version_str
))?;
let _ = version_cache::record_version_check(&latest_version.to_string());
if latest_version <= current_version {
println!("Already on the latest version (v{})", current_version);
return Ok(None);
}
Ok(Some((
current_version_display,
current_version,
latest_version,
release,
)))
}
pub fn check_update() -> Result<Option<String>> {
if crate::utils::network_disabled() {
return Ok(None);
}
match get_version_comparison()? {
Some((current_version, _, latest_version, release)) => {
println!(
"Update available: v{} → v{}",
extract_semver_version(¤t_version),
latest_version
);
Ok(Some(release.tag_name))
}
None => Ok(None),
}
}
fn perform_installation(
current_version: &str,
latest_version: &Version,
release: &GitHubRelease,
) -> Result<()> {
let asset_pattern = platform::get_asset_pattern(&latest_version.to_string())?;
let asset = release
.assets
.iter()
.find(|a| a.name == asset_pattern)
.context(format!(
"Update failed: No binary found for {} ({})",
env::consts::OS,
std::env::consts::ARCH
))?;
let current_exe =
env::current_exe().context("Update failed: Cannot locate current executable")?;
let temp_dir = env::temp_dir();
let archive_path = temp_dir.join(&asset.name);
github::download_file(&asset.browser_download_url, &archive_path)
.context("Update failed: Download error")?;
let extract_dir = temp_dir.join("vct_update");
if extract_dir.exists() {
fs::remove_dir_all(&extract_dir)
.context("Update failed: Cannot clean temporary directory")?;
}
fs::create_dir_all(&extract_dir).context("Update failed: Cannot create temporary directory")?;
let new_binary = if asset.name.ends_with(".tar.gz") {
archive::extract_targz(&archive_path, &extract_dir)
.context("Update failed: Cannot extract archive")?
} else if asset.name.ends_with(".zip") {
archive::extract_zip(&archive_path, &extract_dir)
.context("Update failed: Cannot extract archive")?
} else {
anyhow::bail!("Update failed: Unsupported archive format");
};
#[cfg(unix)]
platform::perform_update_unix(¤t_exe, &new_binary)
.context("Update failed: Cannot replace binary")?;
#[cfg(windows)]
platform::perform_update_windows(¤t_exe, &new_binary)
.context("Update failed: Cannot replace binary")?;
let _ = fs::remove_file(&archive_path);
let _ = fs::remove_dir_all(&extract_dir);
println!(
"Upgraded from v{} to v{}",
extract_semver_version(current_version),
latest_version
);
println!(
"https://github.com/Mai0313/VibeCodingTracker/releases/tag/{}",
release.tag_name
);
println!(
"If you like this tool, please star us on GitHub: https://github.com/Mai0313/VibeCodingTracker"
);
Ok(())
}
pub fn perform_update() -> Result<()> {
let Some((current_version, _, latest_version, release)) = get_version_comparison()? else {
return Ok(());
};
perform_installation(¤t_version, &latest_version, &release)
}
pub fn perform_force_update() -> Result<()> {
let release =
github::fetch_latest_release().context("Failed to fetch latest release information")?;
let (current_version_display, _) = get_current_version()?;
let latest_version_str = release.tag_name.trim_start_matches('v');
let latest_version = Version::parse(latest_version_str).context(format!(
"Failed to parse latest version: {}",
latest_version_str
))?;
let _ = version_cache::record_version_check(&latest_version.to_string());
perform_installation(¤t_version_display, &latest_version, &release)
}
pub fn update_interactive(force: bool) -> Result<()> {
println!("Checking for updates...");
if force {
perform_force_update()
} else {
if check_update()?.is_some() {
print!("Continue? (y/N): ");
std::io::Write::flush(&mut std::io::stdout())?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
if !input.trim().eq_ignore_ascii_case("y") {
println!("Cancelled");
return Ok(());
}
perform_update()
} else {
Ok(())
}
}
}
#[doc(hidden)]
pub use archive::{extract_targz, extract_zip};
#[doc(hidden)]
pub use platform::get_asset_pattern;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_semver_version_clean() {
let version = "0.1.6";
assert_eq!(extract_semver_version(version), "0.1.6");
}
#[test]
fn test_extract_semver_version_with_git_metadata() {
let version = "0.1.6-5-g1234567";
assert_eq!(extract_semver_version(version), "0.1.6");
}
#[test]
fn test_extract_semver_version_with_dirty_flag() {
let version = "0.1.6-5-g1234567-dirty";
assert_eq!(extract_semver_version(version), "0.1.6");
}
#[test]
fn test_extract_semver_version_rc() {
let version = "1.0.0-rc.1";
assert_eq!(extract_semver_version(version), "1.0.0");
}
#[test]
fn test_extract_semver_version_beta() {
let version = "2.3.4-beta.2";
assert_eq!(extract_semver_version(version), "2.3.4");
}
#[test]
fn test_extract_semver_version_alpha() {
let version = "0.5.0-alpha";
assert_eq!(extract_semver_version(version), "0.5.0");
}
#[test]
fn test_extract_semver_version_complex() {
let version = "1.2.3-45-gabcdef0-modified";
assert_eq!(extract_semver_version(version), "1.2.3");
}
#[test]
fn test_extract_semver_version_single_digit() {
assert_eq!(extract_semver_version("1.0.0"), "1.0.0");
assert_eq!(extract_semver_version("0.0.1"), "0.0.1");
}
#[test]
fn test_extract_semver_version_large_numbers() {
assert_eq!(extract_semver_version("10.20.30"), "10.20.30");
assert_eq!(extract_semver_version("100.200.300-1-g123"), "100.200.300");
}
#[test]
fn test_extract_semver_version_empty() {
assert_eq!(extract_semver_version(""), "");
}
#[test]
fn test_extract_semver_version_no_dashes() {
let version = "2.4.8";
assert_eq!(extract_semver_version(version), "2.4.8");
}
#[test]
fn test_extract_semver_version_multiple_dashes() {
let version = "1.0.0-pre-release-candidate";
assert_eq!(extract_semver_version(version), "1.0.0");
}
#[test]
fn test_extract_semver_version_only_major_minor() {
let version = "1.2";
assert_eq!(extract_semver_version(version), "1.2");
}
#[test]
fn test_extract_semver_version_with_v_prefix() {
let version = "v1.2.3-dirty";
assert_eq!(extract_semver_version(version), "v1.2.3");
}
#[test]
fn test_extract_semver_version_consistency() {
let version = "3.1.4-15-g926535-dirty";
let result1 = extract_semver_version(version);
let result2 = extract_semver_version(version);
assert_eq!(result1, result2);
}
#[test]
fn test_extract_semver_version_zero_version() {
assert_eq!(extract_semver_version("0.0.0"), "0.0.0");
assert_eq!(extract_semver_version("0.0.0-dev"), "0.0.0");
}
#[test]
fn test_extract_semver_version_patch_zero() {
assert_eq!(extract_semver_version("1.5.0"), "1.5.0");
assert_eq!(extract_semver_version("2.0.0-rc1"), "2.0.0");
}
}