1use std::io::Read;
8use std::path::Path;
9
10use anyhow::{bail, Context, Result};
11use sha2::{Digest, Sha256};
12
13const REPO: &str = "vima-tech/Innate";
14const GITHUB_API: &str = "https://api.github.com";
15
16pub fn run_upgrade(version: Option<&str>, db_path: &Path, check_only: bool) -> Result<()> {
21 let current_ver = env!("CARGO_PKG_VERSION");
22 let target = current_target();
23
24 if target == "unknown" {
25 bail!(
26 "Unsupported platform for auto-upgrade. \
27 Build from source: cargo build --release --manifest-path core/Cargo.toml"
28 );
29 }
30
31 let target_ver = match version {
33 Some(v) => v.trim_start_matches('v').to_string(),
34 None => latest_version()?,
35 };
36
37 if target_ver == current_ver {
38 println!("innate {current_ver} is already up to date.");
39 return Ok(());
40 }
41
42 println!("innate {current_ver} → {target_ver}");
43
44 if check_only {
45 println!("Run `innate upgrade` (without --check) to install.");
46 return Ok(());
47 }
48
49 let ext = if cfg!(target_os = "windows") {
50 ".exe"
51 } else {
52 ""
53 };
54 let asset = format!("innate-{target}{ext}");
55 let base = format!("https://github.com/{REPO}/releases/download/v{target_ver}");
56
57 let sha_text = http_get_text(&format!("{base}/{asset}.sha256"))
59 .with_context(|| format!("Could not fetch checksum for {asset} v{target_ver}"))?;
60 let expected_sha = sha_text
61 .split_whitespace()
62 .next()
63 .context("SHA-256 file is empty")?
64 .to_lowercase();
65
66 println!(" Downloading {asset}…");
68 let bytes = http_get_bytes(&format!("{base}/{asset}"))
69 .with_context(|| format!("Could not download {asset}"))?;
70
71 let actual_sha = crate::utils::hex(&Sha256::digest(&bytes));
73 if actual_sha != expected_sha {
74 bail!(
75 "SHA-256 mismatch — download may be corrupted.\n \
76 expected: {expected_sha}\n \
77 got: {actual_sha}"
78 );
79 }
80
81 let exe = std::env::current_exe().context("Cannot resolve current executable path")?;
83 let tmp = exe.with_extension("upgrade.tmp");
84 std::fs::write(&tmp, &bytes).context("Failed to write temporary binary")?;
85
86 #[cfg(unix)]
87 {
88 use std::os::unix::fs::PermissionsExt;
89 std::fs::set_permissions(&tmp, std::fs::Permissions::from_mode(0o755))
90 .context("Failed to mark binary as executable")?;
91 }
92
93 #[cfg(target_os = "windows")]
96 {
97 let old = exe.with_extension("old.exe");
98 std::fs::rename(&exe, &old).context("Failed to move old binary")?;
99 if let Err(e) = std::fs::rename(&tmp, &exe) {
100 let _ = std::fs::rename(&old, &exe);
102 return Err(e).context("Failed to install new binary (rolled back)");
103 }
104 println!(" Previous binary saved as {}", old.display());
105 }
106 #[cfg(not(target_os = "windows"))]
107 {
108 std::fs::rename(&tmp, &exe)
109 .context("Failed to replace binary — try: sudo innate upgrade")?;
110 }
111
112 println!("✓ innate {target_ver} installed.");
113
114 if db_path.exists() {
116 println!(" Migrating schema…");
117 match crate::migrate::run_migrations(db_path) {
118 Ok(applied) if applied.is_empty() => println!(" Schema already at {target_ver}."),
119 Ok(applied) => {
120 for step in &applied {
121 println!(" applied: {step}");
122 }
123 println!(" Migration complete.");
124 }
125 Err(e) => eprintln!(" Migration warning (run `innate migrate` manually): {e}"),
126 }
127 }
128
129 Ok(())
130}
131
132fn latest_version() -> Result<String> {
137 let url = format!("{GITHUB_API}/repos/{REPO}/releases/latest");
138 let text = http_get_text(&url).context("Failed to fetch latest release info from GitHub")?;
139 let json: serde_json::Value = serde_json::from_str(&text)?;
140 json["tag_name"]
141 .as_str()
142 .map(|t| t.trim_start_matches('v').to_string())
143 .context("GitHub response missing tag_name — try specifying a version: innate upgrade --version 0.1.8")
144}
145
146fn http_get_text(url: &str) -> Result<String> {
147 let mut resp = ureq::get(url)
148 .header(
149 "User-Agent",
150 &format!("innate/{}", env!("CARGO_PKG_VERSION")),
151 )
152 .header("Accept", "application/vnd.github+json")
153 .call()
154 .with_context(|| format!("HTTP GET {url}"))?;
155 Ok(resp.body_mut().read_to_string()?)
156}
157
158fn http_get_bytes(url: &str) -> Result<Vec<u8>> {
159 let mut resp = ureq::get(url)
160 .header(
161 "User-Agent",
162 &format!("innate/{}", env!("CARGO_PKG_VERSION")),
163 )
164 .call()
165 .with_context(|| format!("HTTP GET {url}"))?;
166 let mut buf = Vec::new();
167 resp.body_mut().as_reader().read_to_end(&mut buf)?;
170 Ok(buf)
171}
172
173fn current_target() -> &'static str {
178 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
179 return "x86_64-unknown-linux-musl";
180 #[cfg(all(target_os = "linux", target_arch = "aarch64"))]
181 return "aarch64-unknown-linux-musl";
182 #[cfg(all(target_os = "linux", target_arch = "arm"))]
183 return "armv7-unknown-linux-musleabihf";
184 #[cfg(all(target_os = "macos", target_arch = "x86_64"))]
185 return "x86_64-apple-darwin";
186 #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
187 return "aarch64-apple-darwin";
188 #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
189 return "x86_64-pc-windows-msvc";
190 #[allow(unreachable_code)]
191 "unknown"
192}