mod download;
mod github;
mod install;
mod platform;
pub use github::Release;
pub struct UpdateConfig {
pub binary_name: &'static str,
pub crate_name: &'static str,
pub current_version: &'static str,
pub github_owner: &'static str,
pub github_repo: &'static str,
}
pub async fn run_update(config: &UpdateConfig) -> anyhow::Result<()> {
println!("Checking for updates...");
let (os, arch) = platform::platform_target()?;
let release = github::fetch_latest_release(config.github_owner, config.github_repo).await?;
let latest_version = github::parse_version(&release.tag_name)?;
let current_version = semver::Version::parse(config.current_version).map_err(|e| {
anyhow::anyhow!(
"Failed to parse current version '{}': {}",
config.current_version,
e
)
})?;
println!("Current version: {}", current_version);
println!("Latest version: {}", latest_version);
if current_version >= latest_version {
println!("\nAlready up to date (v{}).", current_version);
return Ok(());
}
let asset = match github::find_matching_asset(&release, config.binary_name, &os, &arch) {
Some(a) => a,
None => {
println!("\nNo pre-built binary found for {}-{}.", os, arch);
println!("Run manually: cargo install {}", config.crate_name);
return Ok(());
}
};
println!(
"\nDownloading {} v{} ({}-{})...",
config.binary_name, latest_version, os, arch
);
let new_binary =
download::download_and_extract(&asset.browser_download_url, config.binary_name).await?;
install::replace_binary(&new_binary)?;
println!(
"\nUpdated {}: {} -> {}",
config.binary_name, current_version, latest_version
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_platform_detection() {
let result = platform::platform_target();
assert!(result.is_ok(), "platform_target() should succeed");
let (os, arch) = result.unwrap();
assert!(
os == "darwin" || os == "linux",
"OS should be darwin or linux, got: {}",
os
);
assert!(
arch == "arm64" || arch == "x86_64",
"Arch should be arm64 or x86_64, got: {}",
arch
);
}
#[test]
fn test_asset_name_generation() {
let name = github::asset_name("a3s-code", "0.3.0", "darwin", "arm64");
assert_eq!(name, "a3s-code-0.3.0-darwin-arm64.tar.gz");
}
#[test]
fn test_version_compare_newer() {
let current = semver::Version::parse("0.1.0").unwrap();
let latest = semver::Version::parse("0.2.0").unwrap();
assert!(current < latest, "0.1.0 should be less than 0.2.0");
}
#[test]
fn test_version_compare_same() {
let current = semver::Version::parse("0.2.0").unwrap();
let latest = semver::Version::parse("0.2.0").unwrap();
assert!(current >= latest, "0.2.0 should be >= 0.2.0");
}
#[test]
fn test_version_compare_older() {
let current = semver::Version::parse("0.3.0").unwrap();
let latest = semver::Version::parse("0.2.0").unwrap();
assert!(current >= latest, "0.3.0 should be >= 0.2.0");
}
#[test]
fn test_parse_github_release_json() {
let json = serde_json::json!({
"tag_name": "v0.3.0",
"body": "Bug fixes and improvements",
"assets": [
{
"name": "a3s-code-0.3.0-darwin-arm64.tar.gz",
"browser_download_url": "https://example.com/a3s-code-0.3.0-darwin-arm64.tar.gz"
},
{
"name": "a3s-code-0.3.0-linux-x86_64.tar.gz",
"browser_download_url": "https://example.com/a3s-code-0.3.0-linux-x86_64.tar.gz"
}
]
});
let release: Release = serde_json::from_value(json).unwrap();
assert_eq!(release.tag_name, "v0.3.0");
assert_eq!(release.body.as_deref(), Some("Bug fixes and improvements"));
assert_eq!(release.assets.len(), 2);
assert_eq!(release.assets[0].name, "a3s-code-0.3.0-darwin-arm64.tar.gz");
}
#[test]
fn test_find_matching_asset() {
let release = Release {
tag_name: "v0.3.0".to_string(),
body: None,
assets: vec![
github::Asset {
name: "a3s-code-0.3.0-darwin-arm64.tar.gz".to_string(),
browser_download_url: "https://example.com/darwin-arm64.tar.gz".to_string(),
},
github::Asset {
name: "a3s-code-0.3.0-linux-x86_64.tar.gz".to_string(),
browser_download_url: "https://example.com/linux-x86_64.tar.gz".to_string(),
},
],
};
let found = github::find_matching_asset(&release, "a3s-code", "darwin", "arm64");
assert!(found.is_some());
assert!(found.unwrap().name.contains("darwin-arm64"));
let found = github::find_matching_asset(&release, "a3s-code", "linux", "x86_64");
assert!(found.is_some());
assert!(found.unwrap().name.contains("linux-x86_64"));
let found = github::find_matching_asset(&release, "a3s-code", "linux", "riscv64");
assert!(found.is_none());
}
#[test]
fn test_strip_version_prefix() {
let v1 = github::parse_version("v0.2.0").unwrap();
assert_eq!(v1, semver::Version::parse("0.2.0").unwrap());
let v2 = github::parse_version("0.2.0").unwrap();
assert_eq!(v2, semver::Version::parse("0.2.0").unwrap());
}
}