diff --git a/.gitignore b/.gitignore index d1d9e00..5a923c0 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ dist/ # Native binaries (keep the launcher scripts) bin/agent-browser-* +bin/.install-method !bin/agent-browser !bin/agent-browser.cmd diff --git a/cli/src/upgrade.rs b/cli/src/upgrade.rs index 8fbedd5..14642ac 100644 --- a/cli/src/upgrade.rs +++ b/cli/src/upgrade.rs @@ -1,4 +1,5 @@ use crate::color; +use std::path::Path; use std::process::{exit, Command, Stdio}; const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -6,6 +7,9 @@ const NPM_REGISTRY_URL: &str = "https://registry.npmjs.org/agent-browser/latest" enum InstallMethod { Npm, + Pnpm, + Yarn, + Bun, Homebrew, Cargo, Unknown, @@ -27,69 +31,157 @@ async fn fetch_latest_version() -> Result { .ok_or_else(|| "No version field in registry response".to_string()) } +/// Parse the `.install-method` marker written by postinstall.js. +fn read_install_method_marker(exe_dir: &Path) -> Option { + let contents = std::fs::read_to_string(exe_dir.join(".install-method")).ok()?; + match contents.trim() { + "npm" => Some(InstallMethod::Npm), + "pnpm" => Some(InstallMethod::Pnpm), + "yarn" => Some(InstallMethod::Yarn), + "bun" => Some(InstallMethod::Bun), + _ => None, + } +} + fn detect_install_method() -> InstallMethod { - // Check Homebrew (available on macOS and Linux) + if let Ok(exe) = std::env::current_exe() { + // Resolve symlinks to find the real binary location + let real_path = exe.canonicalize().unwrap_or(exe); + + // Preferred: read the marker file written at install time + if let Some(dir) = real_path.parent() { + if let Some(method) = read_install_method_marker(dir) { + return method; + } + } + + // Fallback: infer from executable path + let path_str = real_path.to_string_lossy(); + + if path_str.contains("/.cargo/bin/") || path_str.contains("\\.cargo\\bin\\") { + return InstallMethod::Cargo; + } + + if path_str.contains("/Cellar/agent-browser/") + || path_str.contains("/homebrew/") + || path_str.contains("/linuxbrew/") + { + return InstallMethod::Homebrew; + } + + if path_str.contains("/pnpm/") || path_str.contains("/pnpm-global/") { + return InstallMethod::Pnpm; + } + + if path_str.contains("/.yarn/") || path_str.contains("/yarn/global/") { + return InstallMethod::Yarn; + } + + if path_str.contains("/.bun/") { + return InstallMethod::Bun; + } + + if path_str.contains("node_modules/agent-browser") + || path_str.contains("node_modules\\agent-browser") + { + return InstallMethod::Npm; + } + } + + // Last resort: probe package managers via subprocess + #[cfg(any(target_os = "macos", target_os = "linux"))] { - let brew_check = Command::new("brew") - .args(["list", "agent-browser"]) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status(); - if brew_check.map(|s| s.success()).unwrap_or(false) { + if command_succeeds("brew", &["list", "agent-browser"]) { return InstallMethod::Homebrew; } } - // Check Cargo installation by executable path - if let Ok(exe) = std::env::current_exe() { - let path_str = exe.to_string_lossy(); - if path_str.contains("/.cargo/bin/") || path_str.contains("\\.cargo\\bin\\") { - return InstallMethod::Cargo; - } + if command_output_contains( + "pnpm", + &["list", "-g", "agent-browser", "--depth=0"], + "agent-browser", + ) { + return InstallMethod::Pnpm; } - // Check npm global installation - let npm_check = Command::new("npm") - .args(["list", "-g", "agent-browser", "--depth=0"]) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status(); - if npm_check.map(|s| s.success()).unwrap_or(false) { + if command_output_contains("yarn", &["global", "list", "--depth=0"], "agent-browser") { + return InstallMethod::Yarn; + } + + if command_output_contains("bun", &["pm", "ls", "-g"], "agent-browser") { + return InstallMethod::Bun; + } + + if command_succeeds("npm", &["list", "-g", "agent-browser", "--depth=0"]) { return InstallMethod::Npm; } InstallMethod::Unknown } +fn command_succeeds(cmd: &str, args: &[&str]) -> bool { + Command::new(cmd) + .args(args) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .map(|s| s.success()) + .unwrap_or(false) +} + +fn command_output_contains(cmd: &str, args: &[&str], needle: &str) -> bool { + Command::new(cmd) + .args(args) + .stderr(Stdio::null()) + .output() + .map(|o| o.status.success() && String::from_utf8_lossy(&o.stdout).contains(needle)) + .unwrap_or(false) +} + fn run_upgrade_command(method: &InstallMethod) -> bool { - match method { - InstallMethod::Npm => { - println!("Running: npm install -g agent-browser@latest"); - Command::new("npm") - .args(["install", "-g", "agent-browser@latest"]) - .status() - .map(|s| s.success()) - .unwrap_or(false) - } - InstallMethod::Homebrew => { - println!("Running: brew upgrade agent-browser"); - Command::new("brew") - .args(["upgrade", "agent-browser"]) - .status() - .map(|s| s.success()) - .unwrap_or(false) - } - InstallMethod::Cargo => { - println!("Running: cargo install agent-browser --force"); - Command::new("cargo") - .args(["install", "agent-browser", "--force"]) - .status() - .map(|s| s.success()) - .unwrap_or(false) - } - InstallMethod::Unknown => false, - } + let (cmd, args, display): (&str, &[&str], &str) = match method { + InstallMethod::Npm => ( + "npm", + &["install", "-g", "agent-browser@latest"], + "npm install -g agent-browser@latest", + ), + InstallMethod::Pnpm => ( + "pnpm", + &["add", "-g", "agent-browser@latest"], + "pnpm add -g agent-browser@latest", + ), + // NOTE: `yarn global` is Yarn Classic (v1) only; Yarn Berry (v2+) removed it. + // Users on Yarn v2+ won't reach this path — detection falls through to Unknown. + InstallMethod::Yarn => ( + "yarn", + &["global", "add", "agent-browser@latest"], + "yarn global add agent-browser@latest", + ), + InstallMethod::Bun => ( + "bun", + &["install", "-g", "agent-browser@latest"], + "bun install -g agent-browser@latest", + ), + InstallMethod::Homebrew => ( + "brew", + &["upgrade", "agent-browser"], + "brew upgrade agent-browser", + ), + InstallMethod::Cargo => ( + "cargo", + &["install", "agent-browser", "--force"], + "cargo install agent-browser --force", + ), + InstallMethod::Unknown => return false, + }; + + println!("Running: {}", display); + Command::new(cmd) + .args(args) + .status() + .map(|s| s.success()) + .unwrap_or(false) } pub fn run_upgrade() { @@ -132,6 +224,9 @@ pub fn run_upgrade() { let method_name = match &method { InstallMethod::Npm => "npm", + InstallMethod::Pnpm => "pnpm", + InstallMethod::Yarn => "yarn", + InstallMethod::Bun => "bun", InstallMethod::Homebrew => "Homebrew", InstallMethod::Cargo => "Cargo", InstallMethod::Unknown => "", @@ -143,9 +238,12 @@ pub fn run_upgrade() { color::error_indicator() ); eprintln!(" To update manually, run one of:"); - eprintln!(" npm install -g agent-browser@latest # npm"); - eprintln!(" brew upgrade agent-browser # Homebrew"); - eprintln!(" cargo install agent-browser --force # Cargo"); + eprintln!(" npm install -g agent-browser@latest # npm"); + eprintln!(" pnpm add -g agent-browser@latest # pnpm"); + eprintln!(" yarn global add agent-browser@latest # yarn"); + eprintln!(" bun install -g agent-browser@latest # bun"); + eprintln!(" brew upgrade agent-browser # Homebrew"); + eprintln!(" cargo install agent-browser --force # Cargo"); exit(1); } diff --git a/scripts/postinstall.js b/scripts/postinstall.js index 01a8bce..80fe2fc 100644 --- a/scripts/postinstall.js +++ b/scripts/postinstall.js @@ -80,6 +80,31 @@ async function downloadFile(url, dest) { }); } +/** + * Detect which package manager ran this postinstall and write a marker file + * next to the binary so `agent-browser upgrade` can use the correct one + * without fragile path heuristics or slow subprocess probing. + * + * npm_config_user_agent is set by npm/pnpm/yarn/bun during lifecycle scripts, + * e.g. "pnpm/8.10.0 node/v20.10.0 linux x64" + */ +function writeInstallMethod() { + const ua = process.env.npm_config_user_agent || ''; + let method = ''; + if (ua.startsWith('pnpm/')) method = 'pnpm'; + else if (ua.startsWith('yarn/')) method = 'yarn'; + else if (ua.startsWith('bun/')) method = 'bun'; + else if (ua.startsWith('npm/')) method = 'npm'; + + if (method) { + try { + writeFileSync(join(binDir, '.install-method'), method); + } catch { + // Non-critical — upgrade will fall back to heuristics + } + } +} + async function main() { // Check if binary already exists if (existsSync(binaryPath)) { @@ -88,10 +113,12 @@ async function main() { chmodSync(binaryPath, 0o755); } console.log(`✓ Native binary ready: ${binaryName}`); - + + writeInstallMethod(); + // On global installs, fix npm's bin entry to use native binary directly await fixGlobalInstallBin(); - + showInstallReminder(); return; } @@ -106,12 +133,12 @@ async function main() { try { await downloadFile(DOWNLOAD_URL, binaryPath); - + // Make executable on Unix if (platform() !== 'win32') { chmodSync(binaryPath, 0o755); } - + console.log(`✓ Downloaded native binary: ${binaryName}`); } catch (err) { console.log(`Could not download native binary: ${err.message}`); @@ -121,6 +148,8 @@ async function main() { console.log(' 2. Run: npm run build:native'); } + writeInstallMethod(); + // On global installs, fix npm's bin entry to use native binary directly // This avoids the /bin/sh error on Windows and provides zero-overhead execution await fixGlobalInstallBin();