soffit 0.1.1

Customizable statusline manager for Claude Code with desktop editor and custom widget support
use anyhow::{bail, Result};
use std::process::Command;

const REPO: &str = "noxcraftdev/soffit";

pub fn run() -> Result<()> {
    let current = env!("CARGO_PKG_VERSION");
    println!("soffit {current}");
    println!("Checking for updates...");

    let output = Command::new("curl")
        .args([
            "-s",
            "--max-time",
            "10",
            &format!("https://api.github.com/repos/{REPO}/releases/latest"),
        ])
        .output()?;

    if !output.status.success() {
        bail!("failed to check for updates");
    }

    let v: serde_json::Value = serde_json::from_slice(&output.stdout)?;
    let tag = v
        .get("tag_name")
        .and_then(|t| t.as_str())
        .ok_or_else(|| anyhow::anyhow!("no release found"))?;
    let latest = tag.strip_prefix('v').unwrap_or(tag);

    if latest == current {
        println!("Already up to date.");
        return Ok(());
    }

    println!("New version available: {latest}");

    let exe = std::env::current_exe()?;
    let exe_str = exe.to_string_lossy();

    if exe_str.contains("/.cargo/bin/") {
        println!("Updating via cargo...");
        let status = Command::new("cargo")
            .args(["install", "soffit", "--force"])
            .status()?;
        if !status.success() {
            bail!("cargo install failed");
        }
    } else {
        println!("Downloading soffit {latest}...");
        let target = detect_target()?;
        let url =
            format!("https://github.com/{REPO}/releases/download/v{latest}/soffit-{target}.tar.gz");

        let tmp = std::env::temp_dir().join("soffit-update");
        let _ = std::fs::remove_dir_all(&tmp);
        std::fs::create_dir_all(&tmp)?;

        let tarball = tmp.join("soffit.tar.gz");
        let status = Command::new("curl")
            .args(["-fsSL", &url, "-o"])
            .arg(&tarball)
            .status()?;
        if !status.success() {
            bail!("download failed — no pre-built binary for {target}");
        }

        let status = Command::new("tar")
            .args(["xzf"])
            .arg(&tarball)
            .arg("-C")
            .arg(&tmp)
            .status()?;
        if !status.success() {
            bail!("failed to extract archive");
        }

        let new_binary = tmp.join("soffit");
        if !new_binary.exists() {
            bail!("binary not found in archive");
        }

        let exe_tmp = exe.with_extension("tmp");
        std::fs::copy(&new_binary, &exe_tmp)?;
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            std::fs::set_permissions(&exe_tmp, std::fs::Permissions::from_mode(0o755))?;
        }
        std::fs::rename(&exe_tmp, &exe)?;

        let _ = std::fs::remove_dir_all(&tmp);
    }

    println!("Updated to soffit {latest}");
    Ok(())
}

fn detect_target() -> Result<String> {
    let os = std::env::consts::OS;
    let arch = std::env::consts::ARCH;
    match (os, arch) {
        ("linux", "x86_64") => Ok("x86_64-unknown-linux-gnu".to_string()),
        ("macos", "x86_64") => Ok("x86_64-apple-darwin".to_string()),
        ("macos", "aarch64") => Ok("aarch64-apple-darwin".to_string()),
        _ => anyhow::bail!(
            "unsupported platform: {os}/{arch} — install from source with: cargo install soffit"
        ),
    }
}