1mod download;
8mod github;
9mod install;
10mod platform;
11
12pub use github::Release;
13
14pub struct UpdateConfig {
16 pub binary_name: &'static str,
18 pub crate_name: &'static str,
20 pub current_version: &'static str,
22 pub github_owner: &'static str,
24 pub github_repo: &'static str,
26}
27
28pub async fn run_update(config: &UpdateConfig) -> anyhow::Result<()> {
30 println!("Checking for updates...");
31
32 let (os, arch) = platform::platform_target()?;
33 let release = github::fetch_latest_release(config.github_owner, config.github_repo).await?;
34 let latest_version = github::parse_version(&release.tag_name)?;
35 let current_version = semver::Version::parse(config.current_version).map_err(|e| {
36 anyhow::anyhow!(
37 "Failed to parse current version '{}': {}",
38 config.current_version,
39 e
40 )
41 })?;
42
43 println!("Current version: {}", current_version);
44 println!("Latest version: {}", latest_version);
45
46 if current_version >= latest_version {
47 println!("\nAlready up to date (v{}).", current_version);
48 return Ok(());
49 }
50
51 let asset = match github::find_matching_asset(&release, config.binary_name, &os, &arch) {
52 Some(a) => a,
53 None => {
54 println!("\nNo pre-built binary found for {}-{}.", os, arch);
55 println!("Run manually: cargo install {}", config.crate_name);
56 return Ok(());
57 }
58 };
59
60 println!(
61 "\nDownloading {} v{} ({}-{})...",
62 config.binary_name, latest_version, os, arch
63 );
64
65 let new_binary =
66 download::download_and_extract(&asset.browser_download_url, config.binary_name).await?;
67 install::replace_binary(&new_binary)?;
68
69 println!(
70 "\nUpdated {}: {} -> {}",
71 config.binary_name, current_version, latest_version
72 );
73
74 Ok(())
75}
76
77#[cfg(test)]
82mod tests {
83 use super::*;
84
85 #[test]
86 fn test_platform_detection() {
87 let result = platform::platform_target();
88 assert!(result.is_ok(), "platform_target() should succeed");
89 let (os, arch) = result.unwrap();
90 assert!(
91 os == "darwin" || os == "linux",
92 "OS should be darwin or linux, got: {}",
93 os
94 );
95 assert!(
96 arch == "arm64" || arch == "x86_64",
97 "Arch should be arm64 or x86_64, got: {}",
98 arch
99 );
100 }
101
102 #[test]
103 fn test_asset_name_generation() {
104 let name = github::asset_name("a3s-code", "0.3.0", "darwin", "arm64");
105 assert_eq!(name, "a3s-code-0.3.0-darwin-arm64.tar.gz");
106 }
107
108 #[test]
109 fn test_version_compare_newer() {
110 let current = semver::Version::parse("0.1.0").unwrap();
111 let latest = semver::Version::parse("0.2.0").unwrap();
112 assert!(current < latest, "0.1.0 should be less than 0.2.0");
113 }
114
115 #[test]
116 fn test_version_compare_same() {
117 let current = semver::Version::parse("0.2.0").unwrap();
118 let latest = semver::Version::parse("0.2.0").unwrap();
119 assert!(current >= latest, "0.2.0 should be >= 0.2.0");
120 }
121
122 #[test]
123 fn test_version_compare_older() {
124 let current = semver::Version::parse("0.3.0").unwrap();
125 let latest = semver::Version::parse("0.2.0").unwrap();
126 assert!(current >= latest, "0.3.0 should be >= 0.2.0");
127 }
128
129 #[test]
130 fn test_parse_github_release_json() {
131 let json = serde_json::json!({
132 "tag_name": "v0.3.0",
133 "body": "Bug fixes and improvements",
134 "assets": [
135 {
136 "name": "a3s-code-0.3.0-darwin-arm64.tar.gz",
137 "browser_download_url": "https://example.com/a3s-code-0.3.0-darwin-arm64.tar.gz"
138 },
139 {
140 "name": "a3s-code-0.3.0-linux-x86_64.tar.gz",
141 "browser_download_url": "https://example.com/a3s-code-0.3.0-linux-x86_64.tar.gz"
142 }
143 ]
144 });
145
146 let release: Release = serde_json::from_value(json).unwrap();
147 assert_eq!(release.tag_name, "v0.3.0");
148 assert_eq!(release.body.as_deref(), Some("Bug fixes and improvements"));
149 assert_eq!(release.assets.len(), 2);
150 assert_eq!(release.assets[0].name, "a3s-code-0.3.0-darwin-arm64.tar.gz");
151 }
152
153 #[test]
154 fn test_find_matching_asset() {
155 let release = Release {
156 tag_name: "v0.3.0".to_string(),
157 body: None,
158 assets: vec![
159 github::Asset {
160 name: "a3s-code-0.3.0-darwin-arm64.tar.gz".to_string(),
161 browser_download_url: "https://example.com/darwin-arm64.tar.gz".to_string(),
162 },
163 github::Asset {
164 name: "a3s-code-0.3.0-linux-x86_64.tar.gz".to_string(),
165 browser_download_url: "https://example.com/linux-x86_64.tar.gz".to_string(),
166 },
167 ],
168 };
169
170 let found = github::find_matching_asset(&release, "a3s-code", "darwin", "arm64");
172 assert!(found.is_some());
173 assert!(found.unwrap().name.contains("darwin-arm64"));
174
175 let found = github::find_matching_asset(&release, "a3s-code", "linux", "x86_64");
177 assert!(found.is_some());
178 assert!(found.unwrap().name.contains("linux-x86_64"));
179
180 let found = github::find_matching_asset(&release, "a3s-code", "linux", "riscv64");
182 assert!(found.is_none());
183 }
184
185 #[test]
186 fn test_strip_version_prefix() {
187 let v1 = github::parse_version("v0.2.0").unwrap();
188 assert_eq!(v1, semver::Version::parse("0.2.0").unwrap());
189
190 let v2 = github::parse_version("0.2.0").unwrap();
191 assert_eq!(v2, semver::Version::parse("0.2.0").unwrap());
192 }
193}