Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab9b8d96ca | ||
|
|
5c734c51b6 | ||
|
|
dc54855784 | ||
|
|
ed61be3359 | ||
|
|
54b61f4375 |
@@ -36,7 +36,7 @@ jobs:
|
||||
- { name: macOS ARM64, os: macos-latest, target: aarch64-apple-darwin, asset: agent-browser-darwin-arm64, use_zigbuild: false, ext: '' }
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.inputs.tag || github.ref }}
|
||||
|
||||
@@ -102,7 +102,7 @@ jobs:
|
||||
)
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ matrix.asset }}
|
||||
path: dist/${{ matrix.asset }}.tar.gz*
|
||||
@@ -117,7 +117,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
@@ -126,7 +126,7 @@ jobs:
|
||||
run: ls -la dist
|
||||
|
||||
- name: Attach to release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
tag_name: ${{ github.event.inputs.tag || github.ref_name }}
|
||||
files: |
|
||||
|
||||
@@ -29,7 +29,7 @@ Downloads the prebuilt binary for your platform from the latest [GitHub Release]
|
||||
<details>
|
||||
<summary>Other ways to install</summary>
|
||||
|
||||
- **Pin a version:** `AGENT_BROWSER_VERSION=v0.27.0-fork.11 curl -fsSL https://raw.githubusercontent.com/leeguooooo/agent-browser-stealth/main/install.sh | sh`
|
||||
- **Pin a version:** `AGENT_BROWSER_VERSION=v0.27.0-fork.12 curl -fsSL https://raw.githubusercontent.com/leeguooooo/agent-browser-stealth/main/install.sh | sh`
|
||||
- **Custom location:** `AGENT_BROWSER_BIN_DIR=$HOME/bin curl -fsSL … | sh`
|
||||
- **Windows:** download `agent-browser-win32-x64.tar.gz` from the [Releases page](https://github.com/leeguooooo/agent-browser-stealth/releases) and put `agent-browser.exe` on your PATH.
|
||||
- **npm (legacy):** `npm install -g agent-browser-stealth` — still published, but GitHub Releases is the primary channel now.
|
||||
@@ -45,14 +45,39 @@ npx skills add leeguooooo/agent-browser-stealth
|
||||
|
||||
This drops `skills/agent-browser` (and the specialized `skill-data/{core,electron,slack,dogfood,agentcore,vercel-sandbox}`) into your project so your AI agent gets the right usage patterns and pre-approved bash permissions for `agent-browser`, `agent-browser-stealth`, and `abs`.
|
||||
|
||||
## Setup (one time)
|
||||
## Command names
|
||||
|
||||
Enable Chrome DevTools Protocol in your Chrome:
|
||||
`agent-browser`, `agent-browser-stealth`, and `abs` are **the same binary** —
|
||||
`abs` is just a short alias. There is no separate "stealth executable"; stealth
|
||||
is a runtime behavior (see [Anti-detection](#anti-detection) below), applied
|
||||
automatically based on whether you attach to your real Chrome or `--launch` a
|
||||
fresh one.
|
||||
|
||||
1. Open `chrome://inspect/#remote-debugging` in Chrome
|
||||
2. Toggle the switch on
|
||||
## Setup: connect to your Chrome
|
||||
|
||||
That's it. This setting persists across Chrome restarts.
|
||||
Attaching uses the Chrome DevTools Protocol, which Chrome only exposes when it is
|
||||
**launched with a remote-debugging port**. This is a startup flag, not a setting
|
||||
— the `chrome://inspect` toggle alone is **not** enough (it only enables target
|
||||
discovery, not the CDP attach).
|
||||
|
||||
**Recommended — fully quit Chrome, then relaunch with the port:**
|
||||
|
||||
```bash
|
||||
# macOS
|
||||
open -a "Google Chrome" --args --remote-debugging-port=9222
|
||||
# Linux
|
||||
google-chrome --remote-debugging-port=9222
|
||||
# Windows: add --remote-debugging-port=9222 to your Chrome shortcut's target
|
||||
```
|
||||
|
||||
Then run `agent-browser open <url>` — it auto-discovers the port and attaches.
|
||||
On first attach, **Chrome 136+ shows an "Allow remote debugging?" dialog — click
|
||||
Allow once** (it persists for that Chrome session).
|
||||
|
||||
**No setup / don't want to touch your real Chrome?** Use
|
||||
`agent-browser --launch open <url>` to spawn a fresh isolated stealth browser
|
||||
(full anti-detection patches applied; see below). This always works without any
|
||||
port setup and is what CI uses automatically.
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -68,14 +93,24 @@ agent-browser screenshot ./page.png
|
||||
|
||||
The agent operates in your Chrome — you'll see tabs opening, pages loading, clicks happening in real time. You can take over at any point (e.g. solve a CAPTCHA), then let the agent continue.
|
||||
|
||||
### Standalone mode
|
||||
### Standalone mode (`--launch`)
|
||||
|
||||
If you need a separate browser (CI, testing, etc.):
|
||||
Spawn a separate browser instead of attaching to your running Chrome:
|
||||
|
||||
```bash
|
||||
# Throwaway: fresh, EMPTY profile — no cookies, no login (good for CI/testing)
|
||||
agent-browser --launch open https://example.com
|
||||
|
||||
# Keep your login: launch with your real Chrome profile (cookies/sessions intact)
|
||||
agent-browser --launch --profile auto open https://x.com/home
|
||||
# or name it explicitly: --profile Default / --profile "Profile 1"
|
||||
```
|
||||
|
||||
> ⚠️ Plain `--launch` (no `--profile`) uses a **temporary empty profile** — you will
|
||||
> NOT be logged into anything. For logged-in sites use `--profile auto` (picks the
|
||||
> Chrome profile you used most recently) or `--profile <name>`. agent-browser prints
|
||||
> a warning when you `--launch` without a profile.
|
||||
|
||||
In CI environments, standalone mode is used automatically.
|
||||
|
||||
## Anti-detection
|
||||
|
||||
Generated
+1
-1
@@ -45,7 +45,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-browser-stealth"
|
||||
version = "0.27.0-fork.12"
|
||||
version = "0.27.0-fork.14"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"async-trait",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "agent-browser-stealth"
|
||||
version = "0.27.0-fork.12"
|
||||
version = "0.27.0-fork.14"
|
||||
edition = "2021"
|
||||
description = "Fast browser automation CLI for AI agents"
|
||||
license = "Apache-2.0"
|
||||
|
||||
@@ -531,6 +531,18 @@ fn main() {
|
||||
let mut flags = parse_flags(&args);
|
||||
let clean = clean_args(&args);
|
||||
|
||||
// Loudly warn when launching a fresh browser with no profile: it gets a
|
||||
// temporary EMPTY profile (no cookies / no login). For logged-in sites the
|
||||
// user almost always wants --profile auto (their real Chrome profile).
|
||||
// Skipped under CI (force_launch is implicit there and login isn't expected).
|
||||
if flags.force_launch && flags.profile.is_none() && env::var("CI").is_err() {
|
||||
eprintln!(
|
||||
"⚠ --launch uses a temporary EMPTY browser profile (no cookies, no login). \
|
||||
For logged-in sites, add `--profile auto` (or `--profile Default`) to reuse \
|
||||
your real Chrome session."
|
||||
);
|
||||
}
|
||||
|
||||
let has_help = args.iter().any(|a| a == "--help" || a == "-h");
|
||||
let has_version = args.iter().any(|a| a == "--version" || a == "-V");
|
||||
|
||||
|
||||
@@ -1609,9 +1609,12 @@ async fn auto_launch(state: &mut DaemonState) -> Result<(), String> {
|
||||
"Could not connect to your Chrome browser.\n\n\
|
||||
If Chrome showed an \"Allow remote debugging?\" dialog, click \
|
||||
Allow and re-run — that consent is what lets agent-browser attach.\n\n\
|
||||
Otherwise, to let agent-browser work with your existing Chrome (recommended):\n\
|
||||
Otherwise, to let agent-browser reuse your logged-in Chrome (recommended):\n\
|
||||
{}\n\n\
|
||||
Or start a standalone browser with: agent-browser --launch open <url>\n\n\
|
||||
Or launch a separate browser that KEEPS your login state:\n \
|
||||
agent-browser --launch --profile auto open <url>\n\
|
||||
(plain `--launch` alone uses a temporary EMPTY profile — no cookies, \
|
||||
no logged-in sessions.)\n\n\
|
||||
Note: remote debugging is a startup flag, not a Chrome setting — \
|
||||
chrome://inspect/#remote-debugging only enables target discovery and \
|
||||
does NOT expose the CDP HTTP API on /json/version. \
|
||||
@@ -2172,9 +2175,12 @@ async fn handle_launch(cmd: &Value, state: &mut DaemonState) -> Result<Value, St
|
||||
"Could not connect to your Chrome browser.\n\n\
|
||||
If Chrome showed an \"Allow remote debugging?\" dialog, click \
|
||||
Allow and re-run — that consent is what lets agent-browser attach.\n\n\
|
||||
Otherwise, to let agent-browser work with your existing Chrome (recommended):\n\
|
||||
Otherwise, to let agent-browser reuse your logged-in Chrome (recommended):\n\
|
||||
{}\n\n\
|
||||
Or start a standalone browser with: agent-browser --launch open <url>\n\n\
|
||||
Or launch a separate browser that KEEPS your login state:\n \
|
||||
agent-browser --launch --profile auto open <url>\n\
|
||||
(plain `--launch` alone uses a temporary EMPTY profile — no cookies, \
|
||||
no logged-in sessions.)\n\n\
|
||||
Note: remote debugging is a startup flag, not a Chrome setting — \
|
||||
chrome://inspect/#remote-debugging only enables target discovery and \
|
||||
does NOT expose the CDP HTTP API on /json/version. \
|
||||
|
||||
@@ -917,6 +917,18 @@ pub fn list_chrome_profiles(user_data_dir: &Path) -> Vec<ChromeProfile> {
|
||||
/// 3. Case-insensitive directory name match
|
||||
///
|
||||
/// Returns the resolved directory name, or an error with available profiles.
|
||||
/// Read `profile.last_used` (the directory name of the profile Chrome opened
|
||||
/// most recently) from a user-data dir's `Local State`. Used to resolve
|
||||
/// `--profile auto`.
|
||||
fn read_last_used_profile(user_data_dir: &Path) -> Option<String> {
|
||||
let content = std::fs::read_to_string(user_data_dir.join("Local State")).ok()?;
|
||||
let json: serde_json::Value = serde_json::from_str(&content).ok()?;
|
||||
json.get("profile")?
|
||||
.get("last_used")?
|
||||
.as_str()
|
||||
.map(String::from)
|
||||
}
|
||||
|
||||
pub fn resolve_chrome_profile(user_data_dir: &Path, input: &str) -> Result<String, String> {
|
||||
let profiles = list_chrome_profiles(user_data_dir);
|
||||
|
||||
@@ -928,6 +940,21 @@ pub fn resolve_chrome_profile(user_data_dir: &Path, input: &str) -> Result<Strin
|
||||
));
|
||||
}
|
||||
|
||||
// "auto": pick the profile Chrome last used (else "Default", else the first
|
||||
// one), so `--profile auto` reuses the real logged-in profile without the
|
||||
// user having to name it explicitly.
|
||||
if input.eq_ignore_ascii_case("auto") {
|
||||
if let Some(lu) = read_last_used_profile(user_data_dir) {
|
||||
if let Some(p) = profiles.iter().find(|p| p.directory == lu) {
|
||||
return Ok(p.directory.clone());
|
||||
}
|
||||
}
|
||||
if let Some(p) = profiles.iter().find(|p| p.directory == "Default") {
|
||||
return Ok(p.directory.clone());
|
||||
}
|
||||
return Ok(profiles[0].directory.clone());
|
||||
}
|
||||
|
||||
// Tier 1: exact directory name match
|
||||
if let Some(p) = profiles.iter().find(|p| p.directory == input) {
|
||||
return Ok(p.directory.clone());
|
||||
@@ -1673,6 +1700,44 @@ mod tests {
|
||||
assert!(!is_chrome_profile_name("relative/path"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_chrome_profile_auto_prefers_last_used() {
|
||||
let tmp = std::env::temp_dir().join("ab-auto-lastused-test");
|
||||
let _ = std::fs::remove_dir_all(&tmp);
|
||||
std::fs::create_dir_all(&tmp).unwrap();
|
||||
let local_state = serde_json::json!({
|
||||
"profile": {
|
||||
"last_used": "Profile 2",
|
||||
"info_cache": { "Default": {"name": "Person 1"}, "Profile 2": {"name": "Work"} }
|
||||
}
|
||||
});
|
||||
std::fs::write(
|
||||
tmp.join("Local State"),
|
||||
serde_json::to_string(&local_state).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(resolve_chrome_profile(&tmp, "auto").unwrap(), "Profile 2");
|
||||
assert_eq!(resolve_chrome_profile(&tmp, "AUTO").unwrap(), "Profile 2");
|
||||
let _ = std::fs::remove_dir_all(&tmp);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_chrome_profile_auto_falls_back_to_default() {
|
||||
let tmp = std::env::temp_dir().join("ab-auto-default-test");
|
||||
let _ = std::fs::remove_dir_all(&tmp);
|
||||
std::fs::create_dir_all(&tmp).unwrap();
|
||||
let local_state = serde_json::json!({
|
||||
"profile": { "info_cache": { "Default": {"name": "Person 1"}, "Profile 2": {"name": "Work"} } }
|
||||
});
|
||||
std::fs::write(
|
||||
tmp.join("Local State"),
|
||||
serde_json::to_string(&local_state).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(resolve_chrome_profile(&tmp, "auto").unwrap(), "Default");
|
||||
let _ = std::fs::remove_dir_all(&tmp);
|
||||
}
|
||||
|
||||
/// Helper to create a fake Chrome user-data dir with a `Local State` file.
|
||||
fn create_fake_local_state(base: &Path, profiles: &[(&str, &str)]) {
|
||||
let mut info_cache = serde_json::Map::new();
|
||||
|
||||
+49
-263
@@ -1,284 +1,70 @@
|
||||
use crate::color;
|
||||
use std::path::Path;
|
||||
use std::process::{exit, Command, Stdio};
|
||||
use std::process::{exit, Command};
|
||||
|
||||
const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
const NPM_REGISTRY_URL: &str = "https://registry.npmjs.org/agent-browser/latest";
|
||||
|
||||
enum InstallMethod {
|
||||
Npm,
|
||||
Pnpm,
|
||||
Yarn,
|
||||
Bun,
|
||||
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())
|
||||
}
|
||||
|
||||
/// Parse the `.install-method` marker written by postinstall.js.
|
||||
fn read_install_method_marker(exe_dir: &Path) -> Option<InstallMethod> {
|
||||
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 {
|
||||
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"))]
|
||||
{
|
||||
if command_succeeds("brew", &["list", "agent-browser"]) {
|
||||
return InstallMethod::Homebrew;
|
||||
}
|
||||
}
|
||||
|
||||
if command_output_contains(
|
||||
"pnpm",
|
||||
&["list", "-g", "agent-browser", "--depth=0"],
|
||||
"agent-browser",
|
||||
) {
|
||||
return InstallMethod::Pnpm;
|
||||
}
|
||||
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
/// Canonical installer for the stealth fork. `upgrade` just re-runs it, so the
|
||||
/// upgrade path and the install path are identical (GitHub Release, no npm).
|
||||
const INSTALL_URL: &str =
|
||||
"https://raw.githubusercontent.com/leeguooooo/agent-browser-stealth/main/install.sh";
|
||||
|
||||
/// Upgrade to the latest GitHub Release.
|
||||
///
|
||||
/// The stealth fork ships as a prebuilt binary attached to a GitHub Release —
|
||||
/// NOT via the npm registry. Earlier this command (inherited from upstream)
|
||||
/// ran `npm/pnpm install -g agent-browser@latest`, which installed the
|
||||
/// UNRELATED upstream `agent-browser` package and clobbered the user's setup.
|
||||
/// Now `upgrade` simply re-runs install.sh into the same directory as the
|
||||
/// current binary, so it always tracks the freshest GitHub Release.
|
||||
pub fn run_upgrade() {
|
||||
let current = CURRENT_VERSION;
|
||||
println!(
|
||||
"{}",
|
||||
color::cyan(&format!(
|
||||
"Upgrading agent-browser-stealth (currently v{}) from the latest GitHub Release...",
|
||||
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::Pnpm => "pnpm",
|
||||
InstallMethod::Yarn => "yarn",
|
||||
InstallMethod::Bun => "bun",
|
||||
InstallMethod::Homebrew => "Homebrew",
|
||||
InstallMethod::Cargo => "Cargo",
|
||||
InstallMethod::Unknown => "",
|
||||
};
|
||||
|
||||
if matches!(method, InstallMethod::Unknown) {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
eprintln!(
|
||||
"{} Could not detect installation method.",
|
||||
color::error_indicator()
|
||||
"{} Automatic upgrade isn't supported on Windows.",
|
||||
color::warning_indicator()
|
||||
);
|
||||
eprintln!(" To update manually, run one of:");
|
||||
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");
|
||||
eprintln!(" Download the latest agent-browser-win32-x64.tar.gz from:");
|
||||
eprintln!(" https://github.com/leeguooooo/agent-browser-stealth/releases/latest");
|
||||
eprintln!(" and replace agent-browser.exe on your PATH.");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
println!("Detected installation via {}.", method_name);
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
// Install into the SAME directory as the running binary (in-place
|
||||
// upgrade), so we don't create a second copy elsewhere on PATH.
|
||||
let bin_dir = std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|p| p.canonicalize().ok())
|
||||
.and_then(|p| p.parent().map(|d| d.to_path_buf()));
|
||||
|
||||
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 install_cmd = format!("curl -fsSL {} | sh", INSTALL_URL);
|
||||
println!("Running: {}", install_cmd);
|
||||
|
||||
let success = run_upgrade_command(&method);
|
||||
let mut cmd = Command::new("sh");
|
||||
cmd.arg("-c").arg(&install_cmd);
|
||||
if let Some(ref dir) = bin_dir {
|
||||
cmd.env("AGENT_BROWSER_BIN_DIR", dir);
|
||||
}
|
||||
|
||||
if success {
|
||||
if !latest.is_empty() {
|
||||
let ok = cmd.status().map(|s| s.success()).unwrap_or(false);
|
||||
if ok {
|
||||
println!(
|
||||
"{} Done! v{} → v{}",
|
||||
color::success_indicator(),
|
||||
current,
|
||||
latest
|
||||
"{} Upgrade complete — run `agent-browser-stealth --version` to confirm.",
|
||||
color::success_indicator()
|
||||
);
|
||||
} else {
|
||||
println!("{} Done!", color::success_indicator());
|
||||
eprintln!("{} Upgrade failed. Install manually:", color::error_indicator());
|
||||
eprintln!(" curl -fsSL {} | sh", INSTALL_URL);
|
||||
exit(1);
|
||||
}
|
||||
} else {
|
||||
eprintln!("{} Upgrade failed.", color::error_indicator());
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "agent-browser-stealth",
|
||||
"version": "0.27.0-fork.12",
|
||||
"version": "0.27.0-fork.14",
|
||||
"description": "Browser automation CLI for AI agents — stealth fork with anti-detection",
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@11.1.3",
|
||||
|
||||
Reference in New Issue
Block a user