The upgrade story spanned four parts (CLI, daemon, extension, skill) with no single view and — worst — the extension was a total black box: nothing reported which build was live, so a user could sit on a stale extension with zero signal. - ext (0.4.7): on connect the extension sends a `hello` with chrome.runtime.getManifest().version; the native-messaging host records it to a `relay-ext-version` sidecar (next to relay-cdp-url, removed on exit). - build.rs embeds the shipped extension version (AB_CONNECT_VERSION, read from the ext manifest at compile time) so the CLI knows what extension it expects. - `chrome-use doctor` gains a Versions section: CLI (vs the cached latest from the background update check), extension (connected version vs the bundled expected — warns + tells you to reload it in Chrome if behind), and skill (bundled, version- locked; `skills add` copies may be stale). Daemon coherence was already covered. So 'which of the four parts is on what version, and what needs upgrading' is now one command. Verified: doctor warns on a simulated old extension and passes on a current one; gracefully shows 'not connected / predates reporting' when the host hasn't learned a version yet.
233 lines
8.4 KiB
Rust
233 lines
8.4 KiB
Rust
use crate::color;
|
|
use std::path::PathBuf;
|
|
use std::process::{exit, Command, Stdio};
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
|
|
const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
|
|
|
|
/// 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/chrome-use/main/install.sh";
|
|
|
|
/// GitHub API for the latest published release (used by the update check).
|
|
const LATEST_RELEASE_API: &str =
|
|
"https://api.github.com/repos/leeguooooo/chrome-use/releases/latest";
|
|
|
|
/// Re-check the latest version at most this often (seconds).
|
|
const UPDATE_CHECK_INTERVAL_SECS: u64 = 86_400; // once a day
|
|
|
|
fn now_secs() -> u64 {
|
|
SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.map(|d| d.as_secs())
|
|
.unwrap_or(0)
|
|
}
|
|
|
|
fn update_cache_path() -> PathBuf {
|
|
crate::connection::config_home().join("update-check.json")
|
|
}
|
|
|
|
fn write_update_cache(checked_at: u64, latest: &str) {
|
|
let path = update_cache_path();
|
|
if let Some(parent) = path.parent() {
|
|
let _ = std::fs::create_dir_all(parent);
|
|
}
|
|
let body = serde_json::json!({ "checked_at": checked_at, "latest": latest }).to_string();
|
|
let _ = std::fs::write(&path, body);
|
|
}
|
|
|
|
/// Parse a dotted version (`1.2.1`, `v1.2.1`, `1.2.1-fork.3`) into a comparable
|
|
/// `(major, minor, patch)`, ignoring any pre-release/build suffix.
|
|
fn parse_version(v: &str) -> Option<(u64, u64, u64)> {
|
|
let core = v.trim().trim_start_matches('v');
|
|
let core = core.split(['-', '+']).next().unwrap_or(core);
|
|
let mut parts = core.split('.');
|
|
let major = parts.next()?.parse().ok()?;
|
|
let minor = parts.next().unwrap_or("0").parse().ok()?;
|
|
let patch = parts.next().unwrap_or("0").parse().ok()?;
|
|
Some((major, minor, patch))
|
|
}
|
|
|
|
fn is_newer(latest: &str, current: &str) -> bool {
|
|
matches!((parse_version(latest), parse_version(current)), (Some(l), Some(c)) if l > c)
|
|
}
|
|
|
|
/// Public semver-ish comparison (`latest` strictly newer than `current`), so
|
|
/// `doctor` can flag a stale extension/CLI without re-implementing parsing.
|
|
pub fn version_is_newer(latest: &str, current: &str) -> bool {
|
|
is_newer(latest, current)
|
|
}
|
|
|
|
/// The latest CLI version recorded by the background update check, if any.
|
|
/// `doctor` uses it to show "a newer chrome-use is available" without a network
|
|
/// call (the `__update-check` worker refreshes the cache out of band).
|
|
pub fn cached_latest_version() -> Option<String> {
|
|
std::fs::read_to_string(update_cache_path())
|
|
.ok()
|
|
.and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
|
|
.and_then(|j| {
|
|
j.get("latest")
|
|
.and_then(|v| v.as_str())
|
|
.map(|s| s.to_string())
|
|
})
|
|
.filter(|s| !s.is_empty())
|
|
}
|
|
|
|
/// Hidden `__update-check` subcommand: fetch the latest release tag and cache it.
|
|
/// Spawned detached by [`maybe_notify_update`] so the network call never blocks a
|
|
/// real command. Uses `curl` (no extra deps, matches `upgrade`).
|
|
pub fn run_update_check() {
|
|
let latest = Command::new("curl")
|
|
.args([
|
|
"-fsSL",
|
|
"--max-time",
|
|
"8",
|
|
"-H",
|
|
"User-Agent: chrome-use-update-check",
|
|
LATEST_RELEASE_API,
|
|
])
|
|
.output()
|
|
.ok()
|
|
.filter(|o| o.status.success())
|
|
.and_then(|o| serde_json::from_slice::<serde_json::Value>(&o.stdout).ok())
|
|
.and_then(|j| {
|
|
j.get("tag_name")
|
|
.and_then(|v| v.as_str())
|
|
.map(|s| s.trim_start_matches('v').to_string())
|
|
});
|
|
if let Some(latest) = latest {
|
|
write_update_cache(now_secs(), &latest);
|
|
}
|
|
}
|
|
|
|
/// Non-blocking "update available" notice. Called once per command run:
|
|
/// - prints a one-line hint to **stderr** (never stdout, so `--json` is clean)
|
|
/// when a cached release is newer than the running binary;
|
|
/// - refreshes the cached latest version at most once a day via a **detached**
|
|
/// background process, so the current command never waits on the network.
|
|
///
|
|
/// Skipped for meta commands (upgrade/install/doctor/`__*`/--version/--help),
|
|
/// in CI, in daemon mode, and when CHROME_USE_NO_UPDATE_CHECK /
|
|
/// AGENT_BROWSER_NO_UPDATE_CHECK is set.
|
|
pub fn maybe_notify_update() {
|
|
if std::env::var_os("CHROME_USE_NO_UPDATE_CHECK").is_some()
|
|
|| std::env::var_os("AGENT_BROWSER_NO_UPDATE_CHECK").is_some()
|
|
|| std::env::var_os("CI").is_some()
|
|
|| std::env::var_os("AGENT_BROWSER_DAEMON").is_some()
|
|
{
|
|
return;
|
|
}
|
|
let first = std::env::args().nth(1).unwrap_or_default();
|
|
if first.starts_with("__")
|
|
|| matches!(
|
|
first.as_str(),
|
|
"upgrade" | "install" | "doctor" | "dashboard" | "daemon"
|
|
)
|
|
{
|
|
return;
|
|
}
|
|
if std::env::args().any(|a| matches!(a.as_str(), "--version" | "-V" | "--help" | "-h")) {
|
|
return;
|
|
}
|
|
|
|
let (checked_at, latest) = std::fs::read_to_string(update_cache_path())
|
|
.ok()
|
|
.and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
|
|
.map(|j| {
|
|
(
|
|
j.get("checked_at").and_then(|v| v.as_u64()).unwrap_or(0),
|
|
j.get("latest")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or_default()
|
|
.to_string(),
|
|
)
|
|
})
|
|
.unwrap_or((0, String::new()));
|
|
|
|
if is_newer(&latest, CURRENT_VERSION) {
|
|
eprintln!(
|
|
"{} chrome-use {latest} is available (you have {CURRENT_VERSION}) — run `chrome-use upgrade`",
|
|
color::warning_indicator()
|
|
);
|
|
}
|
|
|
|
// Refresh in the background at most once a day. Bump the timestamp first
|
|
// (keeping the last-known latest) so concurrent runs don't all spawn a
|
|
// checker, then fire a detached child that does the network fetch.
|
|
if now_secs().saturating_sub(checked_at) >= UPDATE_CHECK_INTERVAL_SECS {
|
|
write_update_cache(now_secs(), &latest);
|
|
if let Ok(exe) = std::env::current_exe() {
|
|
let _ = Command::new(exe)
|
|
.arg("__update-check")
|
|
.stdin(Stdio::null())
|
|
.stdout(Stdio::null())
|
|
.stderr(Stdio::null())
|
|
.spawn();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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 chrome-use@latest`, which installed the
|
|
/// UNRELATED upstream `chrome-use` 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() {
|
|
println!(
|
|
"{}",
|
|
color::cyan(&format!(
|
|
"Upgrading chrome-use (currently v{}) from the latest GitHub Release...",
|
|
CURRENT_VERSION
|
|
))
|
|
);
|
|
|
|
#[cfg(windows)]
|
|
{
|
|
eprintln!(
|
|
"{} Automatic upgrade isn't supported on Windows.",
|
|
color::warning_indicator()
|
|
);
|
|
eprintln!(" Download the latest chrome-use-win32-x64.tar.gz from:");
|
|
eprintln!(" https://github.com/leeguooooo/chrome-use/releases/latest");
|
|
eprintln!(" and replace chrome-use.exe on your PATH.");
|
|
exit(1);
|
|
}
|
|
|
|
#[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()));
|
|
|
|
let install_cmd = format!("curl -fsSL {} | sh", INSTALL_URL);
|
|
println!("Running: {}", install_cmd);
|
|
|
|
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);
|
|
}
|
|
|
|
let ok = cmd.status().map(|s| s.success()).unwrap_or(false);
|
|
if ok {
|
|
println!(
|
|
"{} Upgrade complete — run `chrome-use --version` to confirm.",
|
|
color::success_indicator()
|
|
);
|
|
} else {
|
|
eprintln!(
|
|
"{} Upgrade failed. Install manually:",
|
|
color::error_indicator()
|
|
);
|
|
eprintln!(" curl -fsSL {} | sh", INSTALL_URL);
|
|
exit(1);
|
|
}
|
|
}
|
|
}
|