use std::fs;
use std::process::Command;
use anyhow::{bail, Context, Result};
use console::style;
use crate::output;
const REPO: &str = "https://github.com/styrene-lab/nex";
const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
pub fn run() -> Result<()> {
let target = detect_target()?;
let current = CURRENT_VERSION;
output::status("checking for updates...");
let latest = fetch_latest_version()?;
if latest == current {
println!(
" {} nex {} is already the latest",
style("✓").green().bold(),
current
);
return Ok(());
}
println!(
" nex {} -> {}",
style(current).yellow(),
style(&latest).green()
);
let url = format!("{REPO}/releases/download/v{latest}/nex-{target}.tar.gz");
output::status(&format!("downloading nex {latest}..."));
let tmpdir = tempfile::tempdir().context("failed to create temp dir")?;
let tarball = tmpdir.path().join("nex.tar.gz");
let dl = Command::new("curl")
.args(["-fsSL", &url, "-o"])
.arg(&tarball)
.status()
.context("failed to run curl")?;
if !dl.success() {
bail!("download failed — release may not exist for {target}");
}
let extract = Command::new("tar")
.args(["-xzf"])
.arg(&tarball)
.args(["-C"])
.arg(tmpdir.path())
.status()
.context("failed to extract archive")?;
if !extract.success() {
bail!("archive extraction failed");
}
let new_binary = tmpdir.path().join("nex");
if !new_binary.exists() {
bail!("binary not found in release archive");
}
let current_exe = std::env::current_exe().context("could not determine current binary path")?;
let real_path = fs::canonicalize(¤t_exe).unwrap_or_else(|_| current_exe.clone());
let needs_sudo = !is_writable(&real_path);
output::status(&format!("replacing {}...", real_path.display()));
if needs_sudo {
let status = Command::new("sudo")
.args(["cp", "-f"])
.arg(&new_binary)
.arg(&real_path)
.status()
.context("failed to run sudo")?;
if !status.success() {
bail!("sudo cp failed — could not replace {}", real_path.display());
}
let _ = Command::new("sudo")
.args(["chmod", "+x"])
.arg(&real_path)
.status();
} else {
let backup = real_path.with_extension("old");
if let Err(e) = fs::rename(&real_path, &backup) {
bail!("could not replace {} — {e}", real_path.display());
}
if let Err(e) = fs::copy(&new_binary, &real_path) {
let _ = fs::rename(&backup, &real_path);
bail!("failed to install new binary: {e}");
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = fs::set_permissions(&real_path, fs::Permissions::from_mode(0o755));
}
let _ = fs::remove_file(&backup);
}
let verify = Command::new(&real_path).arg("--version").output();
if !verify.map(|o| o.status.success()).unwrap_or(false) {
output::warn("new binary may not be working — verify with `nex --version`");
}
println!(
"\n {} nex updated {} -> {}",
style("✓").green().bold(),
current,
style(&latest).green()
);
Ok(())
}
fn is_writable(path: &std::path::Path) -> bool {
if path.exists() {
std::fs::OpenOptions::new().write(true).open(path).is_ok()
} else if let Some(parent) = path.parent() {
let test = parent.join(".nex-write-test");
if std::fs::write(&test, b"").is_ok() {
let _ = std::fs::remove_file(&test);
true
} else {
false
}
} else {
false
}
}
fn detect_target() -> Result<String> {
let os = std::env::consts::OS;
let arch = std::env::consts::ARCH;
let target = match (arch, os) {
("aarch64", "macos") => "aarch64-apple-darwin",
("x86_64", "macos") => "x86_64-apple-darwin",
("aarch64", "linux") => "aarch64-unknown-linux-gnu",
("x86_64", "linux") => "x86_64-unknown-linux-gnu",
_ => bail!("unsupported platform: {arch}-{os}"),
};
Ok(target.to_string())
}
fn fetch_latest_version() -> Result<String> {
let output = Command::new("curl")
.args([
"-fsSL",
"-H",
"Accept: application/vnd.github+json",
"https://api.github.com/repos/styrene-lab/nex/releases/latest",
])
.output()
.context("failed to query GitHub releases")?;
if !output.status.success() {
bail!("could not fetch latest release from GitHub");
}
let json: serde_json::Value =
serde_json::from_slice(&output.stdout).context("invalid JSON from GitHub API")?;
let tag = json
.get("tag_name")
.and_then(|v| v.as_str())
.context("no tag_name in release")?;
let version = tag.strip_prefix('v').unwrap_or(tag);
Ok(version.to_string())
}