bctx 0.1.23

bctx CLI — intercept CLI commands and compress output for LLM coding agents
use anyhow::{Context, Result};
use std::path::Path;

#[derive(Debug, PartialEq)]
pub enum InstallMethod {
    Homebrew,
    Npm,
    Cargo,
    Standalone,
}

/// Detect how bctx was installed by inspecting its resolved path.
pub fn detect_install_method(binary_path: &str) -> InstallMethod {
    let path = binary_path.to_lowercase();

    // Homebrew installs under /opt/homebrew (Apple Silicon), /usr/local/Cellar,
    // or /home/linuxbrew/.linuxbrew on Linux.
    if path.contains("/cellar/") || path.contains("/homebrew/") || path.contains("linuxbrew") {
        return InstallMethod::Homebrew;
    }

    // npm global installs land in a path containing node_modules/.bin,
    // a platform prefix like /usr/local/lib/node_modules, ~/.npm-global,
    // the npm prefix set via $NPM_CONFIG_PREFIX, or nvm-managed node dirs
    // (e.g. ~/.nvm/versions/node/vX.Y.Z/bin/).
    if path.contains("node_modules")
        || path.contains("npm")
        || path.contains(".npm")
        || path.contains("/.nvm/")
        || path.contains("\\.nvm\\")
    {
        return InstallMethod::Npm;
    }

    // cargo install puts binaries at ~/.cargo/bin
    if path.contains("/.cargo/bin") || path.contains("\\.cargo\\bin") {
        return InstallMethod::Cargo;
    }

    // Everything else (curl | sh, manual copy, CI image) is a standalone binary.
    InstallMethod::Standalone
}

/// Find the path of the currently running bctx binary.
fn locate_self() -> String {
    std::env::current_exe()
        .ok()
        .map(|p| p.to_string_lossy().to_string())
        .unwrap_or_default()
}

/// Run a shell command, streaming its output to the terminal.
fn run_upgrade(cmd: &str, args: &[&str]) -> Result<()> {
    let status = std::process::Command::new(cmd)
        .args(args)
        .status()
        .with_context(|| format!("failed to launch `{cmd}`"))?;

    if !status.success() {
        anyhow::bail!("`{cmd}` exited with status {status}");
    }
    Ok(())
}

pub fn handle() -> Result<()> {
    let binary_path = locate_self();
    let method = detect_install_method(&binary_path);

    match method {
        InstallMethod::Homebrew => {
            println!("Detected install: Homebrew");
            println!("Running: brew upgrade bctx");
            run_upgrade("brew", &["upgrade", "bctx"])
        }

        InstallMethod::Npm => {
            println!("Detected install: npm");
            println!("Running: npm install -g bctx-bin@latest");
            run_upgrade("npm", &["install", "-g", "bctx-bin@latest"])
        }

        InstallMethod::Cargo => {
            println!("Detected install: cargo");
            println!("Running: cargo install bctx --force");
            run_upgrade("cargo", &["install", "bctx", "--force"])
        }

        InstallMethod::Standalone => {
            if cfg!(target_os = "windows") {
                // Windows: re-run the PowerShell installer
                println!("Detected install: standalone binary (Windows)");
                println!("Running: irm https://betterctx.com/install.ps1 | iex");
                let status = std::process::Command::new("powershell")
                    .args([
                        "-NoProfile",
                        "-ExecutionPolicy",
                        "Bypass",
                        "-Command",
                        "irm https://betterctx.com/install.ps1 | iex",
                    ])
                    .status()
                    .context("failed to launch PowerShell installer")?;
                if !status.success() {
                    anyhow::bail!("PowerShell installer exited with non-zero status");
                }
                return Ok(());
            }

            // macOS / Linux: re-run the universal curl installer.
            println!("Detected install: standalone binary (curl installer)");
            println!("Running: curl -fsSL https://betterctx.com/install.sh | sh");

            if !Path::new("/usr/bin/curl").exists() && which_in_path("curl").is_none() {
                anyhow::bail!(
                    "curl is not available. Download the latest release from \
                     https://betterctx.com/releases and replace the binary manually."
                );
            }

            let status = std::process::Command::new("sh")
                .args(["-c", "curl -fsSL https://betterctx.com/install.sh | sh"])
                .status()
                .context("failed to launch upgrade script")?;

            if !status.success() {
                anyhow::bail!("upgrade script exited with non-zero status");
            }
            Ok(())
        }
    }
}

fn which_in_path(name: &str) -> Option<std::path::PathBuf> {
    std::env::var_os("PATH").and_then(|paths| {
        std::env::split_paths(&paths).find_map(|dir| {
            let candidate = dir.join(name);
            if candidate.is_file() {
                Some(candidate)
            } else {
                None
            }
        })
    })
}

// ── Tests ─────────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn homebrew_apple_silicon() {
        assert_eq!(
            detect_install_method("/opt/homebrew/bin/bctx"),
            InstallMethod::Homebrew
        );
    }

    #[test]
    fn homebrew_intel_cellar() {
        assert_eq!(
            detect_install_method("/usr/local/Cellar/bctx/0.1.7/bin/bctx"),
            InstallMethod::Homebrew
        );
    }

    #[test]
    fn homebrew_linux() {
        assert_eq!(
            detect_install_method("/home/linuxbrew/.linuxbrew/bin/bctx"),
            InstallMethod::Homebrew
        );
    }

    #[test]
    fn npm_global_node_modules() {
        assert_eq!(
            detect_install_method("/usr/local/lib/node_modules/.bin/bctx"),
            InstallMethod::Npm
        );
    }

    #[test]
    fn npm_global_prefix() {
        assert_eq!(
            detect_install_method("/Users/dev/.npm-global/bin/bctx"),
            InstallMethod::Npm
        );
    }

    #[test]
    fn npm_nvm_managed() {
        assert_eq!(
            detect_install_method("/Users/dev/.nvm/versions/node/v25.8.1/bin/bctx"),
            InstallMethod::Npm
        );
    }

    #[test]
    fn cargo_bin() {
        assert_eq!(
            detect_install_method("/Users/dev/.cargo/bin/bctx"),
            InstallMethod::Cargo
        );
    }

    #[test]
    fn standalone_local_bin() {
        assert_eq!(
            detect_install_method("/usr/local/bin/bctx"),
            InstallMethod::Standalone
        );
    }

    #[test]
    fn standalone_home_local_bin() {
        assert_eq!(
            detect_install_method("/home/user/.local/bin/bctx"),
            InstallMethod::Standalone
        );
    }

    #[test]
    fn standalone_ci_image_path() {
        assert_eq!(
            detect_install_method("/usr/bin/bctx"),
            InstallMethod::Standalone
        );
    }

    #[test]
    fn detection_is_case_insensitive_for_homebrew() {
        // Homebrew paths on case-preserving file systems may have mixed case
        assert_eq!(
            detect_install_method("/usr/local/Cellar/bctx/bin/bctx"),
            InstallMethod::Homebrew
        );
    }
}