feat: add built-in upgrade command for self-update (#898)

Adds a new `agent-browser upgrade` command that automatically detects the installation method (npm, Homebrew, or Cargo) and runs the appropriate update command.

**Changes:**
- Added new `upgrade.rs` module with upgrade logic
- Updated `main.rs` to handle the `upgrade` command
- Added upgrade help text in `output.rs`
- Updated README.md and documentation with upgrade instructions
- Updated SKILL.md to mention the upgrade command

**Implementation details:**
- Fetches latest version from npm registry to show version diff
- Auto-detects installation method by checking Homebrew, Cargo paths, and npm global packages
- Provides fallback instructions if installation method cannot be determined
- Uses existing color module for consistent styled output
- Gracefully handles network failures and continues with upgrade

Fixes #895
This commit is contained in:
Chris Tate
2026-03-17 23:29:07 -05:00
committed by GitHub
parent b8a5fc7101
commit 8cfba1752d
6 changed files with 233 additions and 1 deletions
+8
View File
@@ -7,6 +7,7 @@ mod native;
mod output;
#[cfg(test)]
mod test_utils;
mod upgrade;
mod validation;
use serde_json::json;
@@ -26,6 +27,7 @@ use install::run_install;
use output::{
print_command_help, print_help, print_response_with_opts, print_version, OutputOptions,
};
use upgrade::run_upgrade;
fn serialize_json_value(value: &serde_json::Value) -> String {
serde_json::to_string(value).unwrap_or_else(|_| {
@@ -226,6 +228,12 @@ fn main() {
return;
}
// Handle upgrade separately
if clean.first().map(|s| s.as_str()) == Some("upgrade") {
run_upgrade();
return;
}
// Handle session separately (doesn't need daemon)
if clean.first().map(|s| s.as_str()) == Some("session") {
run_session(&clean, &flags.session, flags.json);
+17
View File
@@ -2266,6 +2266,22 @@ Examples:
"##
}
// === Upgrade ===
"upgrade" => {
r##"
agent-browser upgrade - Upgrade to the latest version
Usage: agent-browser upgrade
Detects the current installation method (npm, Homebrew, or Cargo) and runs
the appropriate update command. Displays the version change on success, or
informs you if you are already on the latest version.
Examples:
agent-browser upgrade
"##
}
// === Connect ===
"connect" => {
r##"
@@ -2572,6 +2588,7 @@ Sessions:
Setup:
install Install browser binaries
install --with-deps Also install system dependencies (Linux)
upgrade Upgrade to the latest version
Snapshot Options:
-i, --interactive Only interactive elements
+186
View File
@@ -0,0 +1,186 @@
use crate::color;
use std::process::{exit, Command, Stdio};
const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
const NPM_REGISTRY_URL: &str = "https://registry.npmjs.org/agent-browser/latest";
enum InstallMethod {
Npm,
Homebrew,
Cargo,
Unknown,
}
async fn fetch_latest_version() -> Result<String, String> {
let resp = reqwest::get(NPM_REGISTRY_URL)
.await
.map_err(|e| format!("Failed to fetch version info: {}", e))?;
let body: serde_json::Value = resp
.json()
.await
.map_err(|e| format!("Failed to parse version info: {}", e))?;
body.get("version")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.ok_or_else(|| "No version field in registry response".to_string())
}
fn detect_install_method() -> InstallMethod {
// Check Homebrew (available on macOS and Linux)
#[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) {
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;
}
}
// 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) {
return InstallMethod::Npm;
}
InstallMethod::Unknown
}
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,
}
}
pub fn run_upgrade() {
let current = CURRENT_VERSION;
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap_or_else(|e| {
eprintln!(
"{} Failed to create runtime: {}",
color::error_indicator(),
e
);
exit(1);
});
let latest = match rt.block_on(fetch_latest_version()) {
Ok(v) => v,
Err(e) => {
eprintln!(
"{} Could not check latest version: {}",
color::warning_indicator(),
e
);
String::new()
}
};
if !latest.is_empty() && current == latest.as_str() {
println!(
"{} agent-browser is already at the latest version (v{})",
color::success_indicator(),
current
);
return;
}
let method = detect_install_method();
let method_name = match &method {
InstallMethod::Npm => "npm",
InstallMethod::Homebrew => "Homebrew",
InstallMethod::Cargo => "Cargo",
InstallMethod::Unknown => "",
};
if matches!(method, InstallMethod::Unknown) {
eprintln!(
"{} Could not detect installation method.",
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");
exit(1);
}
println!("Detected installation via {}.", method_name);
if !latest.is_empty() {
println!(
"{}",
color::cyan(&format!(
"Upgrading agent-browser... v{} → v{}",
current, latest
))
);
} else {
println!(
"{}",
color::cyan(&format!("Upgrading agent-browser (v{})...", current))
);
}
let success = run_upgrade_command(&method);
if success {
if !latest.is_empty() {
println!(
"{} Done! v{} → v{}",
color::success_indicator(),
current,
latest
);
} else {
println!("{} Done!", color::success_indicator());
}
} else {
eprintln!("{} Upgrade failed.", color::error_indicator());
exit(1);
}
}