use anyhow::{Context, Result};
use std::path::Path;
#[derive(Debug, PartialEq)]
pub enum InstallMethod {
Homebrew,
Npm,
Cargo,
Standalone,
}
pub fn detect_install_method(binary_path: &str) -> InstallMethod {
let path = binary_path.to_lowercase();
if path.contains("/cellar/") || path.contains("/homebrew/") || path.contains("linuxbrew") {
return InstallMethod::Homebrew;
}
if path.contains("node_modules") || path.contains("npm") || path.contains(".npm") {
return InstallMethod::Npm;
}
if path.contains("/.cargo/bin") || path.contains("\\.cargo\\bin") {
return InstallMethod::Cargo;
}
InstallMethod::Standalone
}
fn locate_self() -> String {
std::env::current_exe()
.ok()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default()
}
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") {
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(());
}
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
}
})
})
}
#[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 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() {
assert_eq!(
detect_install_method("/usr/local/Cellar/bctx/bin/bctx"),
InstallMethod::Homebrew
);
}
}