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
+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);
}
}