feat(version): extension reports its version; doctor shows all-component coherence
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.
This commit is contained in:
@@ -3,6 +3,23 @@ use std::env;
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// Embed the version of the `ab-connect` extension this CLI ships alongside, so
|
||||||
|
/// `doctor` can tell a connected extension "you're older than what this CLI
|
||||||
|
/// expects, update it." Read from the extension manifest at build time so it
|
||||||
|
/// stays in sync with whatever extension version is in the same checkout/release
|
||||||
|
/// (the ext is on its own 0.4.x line, separate from the CLI version). Falls back
|
||||||
|
/// to "unknown" if the manifest can't be read.
|
||||||
|
fn embed_extension_version() {
|
||||||
|
let manifest = Path::new("../extensions/ab-connect/manifest.json");
|
||||||
|
println!("cargo:rerun-if-changed=../extensions/ab-connect/manifest.json");
|
||||||
|
let version = fs::read_to_string(manifest)
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
|
||||||
|
.and_then(|v| v.get("version").and_then(|x| x.as_str()).map(String::from))
|
||||||
|
.unwrap_or_else(|| "unknown".to_string());
|
||||||
|
println!("cargo:rustc-env=AB_CONNECT_VERSION={}", version);
|
||||||
|
}
|
||||||
|
|
||||||
/// Ensure `packages/dashboard/out/` exists so `rust-embed` doesn't fail during
|
/// Ensure `packages/dashboard/out/` exists so `rust-embed` doesn't fail during
|
||||||
/// Rust-only dev builds where the dashboard hasn't been built. The placeholder
|
/// Rust-only dev builds where the dashboard hasn't been built. The placeholder
|
||||||
/// `index.html` is only written when the directory is completely absent.
|
/// `index.html` is only written when the directory is completely absent.
|
||||||
@@ -20,6 +37,7 @@ fn ensure_dashboard_dir() {
|
|||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
ensure_dashboard_dir();
|
ensure_dashboard_dir();
|
||||||
|
embed_extension_version();
|
||||||
|
|
||||||
let protocol_dir = Path::new("cdp-protocol");
|
let protocol_dir = Path::new("cdp-protocol");
|
||||||
let out_dir = env::var("OUT_DIR").unwrap();
|
let out_dir = env::var("OUT_DIR").unwrap();
|
||||||
|
|||||||
@@ -449,6 +449,28 @@ pub fn relay_url() -> Option<String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sidecar recording the connected extension's version, written by the host when
|
||||||
|
/// it receives the extension's `hello` (sibling of `relay-cdp-url`). Lets
|
||||||
|
/// `doctor` surface which extension build is live without a CDP round-trip.
|
||||||
|
fn relay_ext_version_path() -> PathBuf {
|
||||||
|
relay_url_path().with_file_name("relay-ext-version")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Version of the connected `ab-connect` extension, if the host learned it from
|
||||||
|
/// the extension's `hello`. `None` when no extension has connected since the
|
||||||
|
/// host started, or the extension predates version reporting.
|
||||||
|
pub fn relay_ext_version() -> Option<String> {
|
||||||
|
let s = std::fs::read_to_string(relay_ext_version_path())
|
||||||
|
.ok()?
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
|
if s.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Hidden `__nm-host` mode: launched by Chrome for the ab-connect extension.
|
/// Hidden `__nm-host` mode: launched by Chrome for the ab-connect extension.
|
||||||
///
|
///
|
||||||
/// Bridges the extension (native-messaging stdio, envelope protocol) to a local
|
/// Bridges the extension (native-messaging stdio, envelope protocol) to a local
|
||||||
@@ -570,6 +592,15 @@ async fn nm_host_main() {
|
|||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(_) => continue,
|
Err(_) => continue,
|
||||||
};
|
};
|
||||||
|
// Extension version handshake: record it next to the relay URL so
|
||||||
|
// `doctor` can report which extension build is live (and whether it's
|
||||||
|
// behind). Best-effort; the message carries no CDP payload.
|
||||||
|
if v.get("method").and_then(|m| m.as_str()) == Some("hello") {
|
||||||
|
if let Some(ver) = v.get("version").and_then(|x| x.as_str()) {
|
||||||
|
let _ = std::fs::write(relay_ext_version_path(), ver);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let outs = {
|
let outs = {
|
||||||
let mut s = state.lock().await;
|
let mut s = state.lock().await;
|
||||||
s.handle_ext_message(&v, "")
|
s.handle_ext_message(&v, "")
|
||||||
@@ -602,6 +633,7 @@ async fn nm_host_main() {
|
|||||||
}
|
}
|
||||||
nm_log("[nm-host] stdin EOF — Chrome closed the port");
|
nm_log("[nm-host] stdin EOF — Chrome closed the port");
|
||||||
let _ = std::fs::remove_file(relay_url_path());
|
let _ = std::fs::remove_file(relay_url_path());
|
||||||
|
let _ = std::fs::remove_file(relay_ext_version_path());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ mod launch;
|
|||||||
mod network;
|
mod network;
|
||||||
mod providers;
|
mod providers;
|
||||||
mod security;
|
mod security;
|
||||||
|
mod versions;
|
||||||
|
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
@@ -97,6 +98,7 @@ pub fn run_doctor(opts: DoctorOptions) -> i32 {
|
|||||||
let mut fixed: Vec<String> = Vec::new();
|
let mut fixed: Vec<String> = Vec::new();
|
||||||
|
|
||||||
environment::check(&mut checks);
|
environment::check(&mut checks);
|
||||||
|
versions::check(&mut checks);
|
||||||
chrome::check(&mut checks);
|
chrome::check(&mut checks);
|
||||||
daemon::check(&mut checks);
|
daemon::check(&mut checks);
|
||||||
config::check(&mut checks);
|
config::check(&mut checks);
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
//! Version-coherence checks across all four moving parts: the CLI binary, the
|
||||||
|
//! per-session daemons (covered by `daemon.rs`), the connected `ab-connect`
|
||||||
|
//! extension, and the bundled skill. The extension was previously a black box —
|
||||||
|
//! nothing reported which build was live — so a user could sit on an old
|
||||||
|
//! extension with no signal. The extension now reports its version over the
|
||||||
|
//! relay (`hello`), the host records it, and this surfaces it in one place.
|
||||||
|
|
||||||
|
use super::{Check, Status};
|
||||||
|
use crate::{connect, upgrade};
|
||||||
|
|
||||||
|
pub(super) fn check(checks: &mut Vec<Check>) {
|
||||||
|
let category = "Versions";
|
||||||
|
let cli_version = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
|
// CLI — compare against the latest seen by the background update check.
|
||||||
|
match upgrade::cached_latest_version() {
|
||||||
|
Some(latest) if upgrade::version_is_newer(&latest, cli_version) => {
|
||||||
|
checks.push(
|
||||||
|
Check::new(
|
||||||
|
"versions.cli",
|
||||||
|
category,
|
||||||
|
Status::Warn,
|
||||||
|
format!("CLI {cli_version} (newer available: {latest})"),
|
||||||
|
)
|
||||||
|
.with_fix("chrome-use upgrade".to_string()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
checks.push(Check::new(
|
||||||
|
"versions.cli",
|
||||||
|
category,
|
||||||
|
Status::Pass,
|
||||||
|
format!("CLI {cli_version}"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extension — the build this CLI shipped alongside (embedded at compile time
|
||||||
|
// from the extension manifest) is what we expect to be running.
|
||||||
|
let expected_ext = env!("AB_CONNECT_VERSION");
|
||||||
|
match connect::relay_ext_version() {
|
||||||
|
Some(ext) if upgrade::version_is_newer(expected_ext, &ext) => {
|
||||||
|
checks.push(
|
||||||
|
Check::new(
|
||||||
|
"versions.extension",
|
||||||
|
category,
|
||||||
|
Status::Warn,
|
||||||
|
format!("extension {ext} is behind the bundled {expected_ext}"),
|
||||||
|
)
|
||||||
|
.with_fix(
|
||||||
|
"update ab-connect in Chrome: chrome://extensions \u{2192} reload \
|
||||||
|
(or wait for the Web Store auto-update)"
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Some(ext) => {
|
||||||
|
checks.push(Check::new(
|
||||||
|
"versions.extension",
|
||||||
|
category,
|
||||||
|
Status::Pass,
|
||||||
|
format!("extension {ext}"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
checks.push(Check::new(
|
||||||
|
"versions.extension",
|
||||||
|
category,
|
||||||
|
Status::Info,
|
||||||
|
format!(
|
||||||
|
"extension not connected (or it predates version reporting — \
|
||||||
|
expected {expected_ext})"
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skill — ships inside the same release artifact as the binary, so it's
|
||||||
|
// version-locked here. Copies made elsewhere via `skills add` aren't.
|
||||||
|
checks.push(Check::new(
|
||||||
|
"versions.skill",
|
||||||
|
category,
|
||||||
|
Status::Info,
|
||||||
|
format!(
|
||||||
|
"skills bundled with this CLI ({cli_version}); copies made via `skills add` \
|
||||||
|
elsewhere may be stale — re-run to refresh"
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
@@ -52,6 +52,27 @@ fn is_newer(latest: &str, current: &str) -> bool {
|
|||||||
matches!((parse_version(latest), parse_version(current)), (Some(l), Some(c)) if l > c)
|
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.
|
/// 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
|
/// Spawned detached by [`maybe_notify_update`] so the network call never blocks a
|
||||||
/// real command. Uses `curl` (no extra deps, matches `upgrade`).
|
/// real command. Uses `curl` (no extra deps, matches `upgrade`).
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -108,6 +108,12 @@ function connectHost() {
|
|||||||
// reconnect. Keep chrome.debugger attached so reconnect is cheap.
|
// reconnect. Keep chrome.debugger attached so reconnect is cheap.
|
||||||
for (const tabId of tabs.keys()) setBadge(tabId, 'connecting')
|
for (const tabId of tabs.keys()) setBadge(tabId, 'connecting')
|
||||||
})
|
})
|
||||||
|
// Report our version so the host can tell the CLI/`doctor` which extension
|
||||||
|
// build is live (otherwise the extension version is a black box — the user
|
||||||
|
// can't tell they're on an old one). Best-effort; ignored by older hosts.
|
||||||
|
try {
|
||||||
|
postToHost({ method: 'hello', version: chrome.runtime.getManifest().version })
|
||||||
|
} catch {}
|
||||||
// Tell the daemon about everything we already have attached, then attach
|
// Tell the daemon about everything we already have attached, then attach
|
||||||
// anything new.
|
// anything new.
|
||||||
reannounceAttachedTabs()
|
reannounceAttachedTabs()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "chrome-use",
|
"name": "chrome-use",
|
||||||
"version": "0.4.6",
|
"version": "0.4.7",
|
||||||
"description": "Let chrome-use drive your logged-in Chrome \u2014 install once, no token, no per-use confirmation.",
|
"description": "Let chrome-use drive your logged-in Chrome \u2014 install once, no token, no per-use confirmation.",
|
||||||
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6vQIyscGIPYPZdSpPwPL0+0gxUROyRgCpmvCSDoc8XUm4qm97VbKnD9Ijc1lV22lNWZtE78gaRjt6BeSfuMgnBymnhLKjN1gU6AI5QUU0mrJyeHdWKvrKQR5FmsM2A7Xr1ykE2SiiS8zNUS3Y/6O5l+Nva7wrVy6E4a2dkBVQkOsu+DV+nEZvhIyuDY5D5SPXqNwUTWTaglwj5mjvHz36xSwCWlPmrtJ+ED0AUyrb2z4GIOmvk4kqtBVrh/UD058klLo4CkYOnIybB5aV6WYuwarfPY4bF/dLggPem+ewLNTUNBuwrxj/A4nUv0LJTuRO8rR7f8WR9qnRCY0Ic5saQIDAQAB",
|
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6vQIyscGIPYPZdSpPwPL0+0gxUROyRgCpmvCSDoc8XUm4qm97VbKnD9Ijc1lV22lNWZtE78gaRjt6BeSfuMgnBymnhLKjN1gU6AI5QUU0mrJyeHdWKvrKQR5FmsM2A7Xr1ykE2SiiS8zNUS3Y/6O5l+Nva7wrVy6E4a2dkBVQkOsu+DV+nEZvhIyuDY5D5SPXqNwUTWTaglwj5mjvHz36xSwCWlPmrtJ+ED0AUyrb2z4GIOmvk4kqtBVrh/UD058klLo4CkYOnIybB5aV6WYuwarfPY4bF/dLggPem+ewLNTUNBuwrxj/A4nUv0LJTuRO8rR7f8WR9qnRCY0Ic5saQIDAQAB",
|
||||||
"icons": {
|
"icons": {
|
||||||
|
|||||||
Reference in New Issue
Block a user