Release binaries / Build macOS ARM64 (push) Has been cancelled
Release binaries / Build macOS x64 (push) Has been cancelled
Release binaries / Build Linux ARM64 (push) Has been cancelled
Release binaries / Build Linux musl ARM64 (push) Has been cancelled
Release binaries / Build Linux musl x64 (push) Has been cancelled
Release binaries / Build Linux x64 (push) Has been cancelled
Release binaries / Build Windows x64 (push) Has been cancelled
Release binaries / Attach binaries to GitHub Release (push) Has been cancelled
Standalone product rename across the whole repo (issue: project identity): - Binary/package/repo/skill/docs: agent-browser[-stealth] → chrome-use (single binary name `chrome-use`; old aliases agent-browser/abs dropped). - Version: 0.27.0-fork.51 → 1.0.0 (drop the upstream-fork counter). - Native-messaging host: com.agent_browser.connect → com.leeguoo.chrome_use (CLI + ab-connect extension in lockstep — this is a breaking handshake change, extension bumped 0.4.2 → 0.5.0, needs a Web Store republish). - Config dir: ~/.agent-browser → ~/.chrome-use. - README/zh: reframed from "stealth fork of agent-browser" to a standalone product with a small `originally based on vercel-labs/agent-browser` credit. - Kept AGENT_BROWSER_* env vars working (63 vars across the codebase; renaming them would break every existing script/skill for no user-facing gain). Build green, 802 unit tests pass, fmt + clippy clean. Upstream attribution to vercel-labs/agent-browser preserved.
290 lines
7.1 KiB
Rust
290 lines
7.1 KiB
Rust
//! Diagnose an chrome-use installation.
|
|
//!
|
|
//! Runs a battery of checks across environment, Chrome install, daemon
|
|
//! state, config files, encryption, providers, network reachability, and
|
|
//! a live headless browser launch test.
|
|
//!
|
|
//! Auto-cleans stale daemon socket/pid/version sidecar files. Destructive
|
|
//! repairs (reinstalling Chrome, purging old state files, generating a
|
|
//! missing encryption key) are gated behind `--fix`.
|
|
|
|
mod chrome;
|
|
mod config;
|
|
mod daemon;
|
|
mod environment;
|
|
mod fix;
|
|
mod helpers;
|
|
mod launch;
|
|
mod network;
|
|
mod providers;
|
|
mod security;
|
|
|
|
use serde_json::{json, Value};
|
|
|
|
use crate::color;
|
|
|
|
#[derive(Default, Clone, Copy)]
|
|
pub struct DoctorOptions {
|
|
pub offline: bool,
|
|
pub quick: bool,
|
|
pub fix: bool,
|
|
pub json: bool,
|
|
}
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
|
#[repr(u8)]
|
|
pub(crate) enum Status {
|
|
Pass,
|
|
Warn,
|
|
Fail,
|
|
Info,
|
|
}
|
|
|
|
impl Status {
|
|
fn as_str(&self) -> &'static str {
|
|
match self {
|
|
Status::Pass => "pass",
|
|
Status::Warn => "warn",
|
|
Status::Fail => "fail",
|
|
Status::Info => "info",
|
|
}
|
|
}
|
|
|
|
fn label(&self) -> String {
|
|
match self {
|
|
Status::Pass => color::green("pass"),
|
|
Status::Warn => color::yellow("warn"),
|
|
Status::Fail => color::red("fail"),
|
|
Status::Info => color::dim("info"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub(crate) struct Check {
|
|
pub id: String,
|
|
pub category: &'static str,
|
|
pub status: Status,
|
|
pub message: String,
|
|
pub fix: Option<String>,
|
|
}
|
|
|
|
impl Check {
|
|
fn new(
|
|
id: impl Into<String>,
|
|
category: &'static str,
|
|
status: Status,
|
|
message: impl Into<String>,
|
|
) -> Self {
|
|
Self {
|
|
id: id.into(),
|
|
category,
|
|
status,
|
|
message: message.into(),
|
|
fix: None,
|
|
}
|
|
}
|
|
|
|
fn with_fix(mut self, fix: impl Into<String>) -> Self {
|
|
self.fix = Some(fix.into());
|
|
self
|
|
}
|
|
}
|
|
|
|
/// Run the doctor command. Returns the process exit code.
|
|
pub fn run_doctor(opts: DoctorOptions) -> i32 {
|
|
let mut checks: Vec<Check> = Vec::new();
|
|
let mut fixed: Vec<String> = Vec::new();
|
|
|
|
environment::check(&mut checks);
|
|
chrome::check(&mut checks);
|
|
daemon::check(&mut checks);
|
|
config::check(&mut checks);
|
|
security::check(&mut checks);
|
|
providers::check(&mut checks);
|
|
|
|
if !opts.offline {
|
|
network::check(&mut checks);
|
|
}
|
|
|
|
if !opts.quick {
|
|
launch::check(&mut checks);
|
|
}
|
|
|
|
if opts.fix {
|
|
fix::run(&mut checks, &mut fixed);
|
|
}
|
|
|
|
let summary = summarize(&checks);
|
|
let exit_code = if summary.fail > 0 { 1 } else { 0 };
|
|
|
|
if opts.json {
|
|
print_json(&checks, &summary, &fixed, exit_code == 0);
|
|
} else {
|
|
print_text(&checks, &summary, &fixed, opts.fix);
|
|
}
|
|
|
|
exit_code
|
|
}
|
|
|
|
struct Summary {
|
|
pass: usize,
|
|
warn: usize,
|
|
fail: usize,
|
|
}
|
|
|
|
fn summarize(checks: &[Check]) -> Summary {
|
|
let mut s = Summary {
|
|
pass: 0,
|
|
warn: 0,
|
|
fail: 0,
|
|
};
|
|
for c in checks {
|
|
match c.status {
|
|
Status::Pass => s.pass += 1,
|
|
Status::Warn => s.warn += 1,
|
|
Status::Fail => s.fail += 1,
|
|
Status::Info => {}
|
|
}
|
|
}
|
|
s
|
|
}
|
|
|
|
fn print_text(checks: &[Check], summary: &Summary, fixed: &[String], fix_ran: bool) {
|
|
println!("{}", color::bold("chrome-use doctor"));
|
|
|
|
let mut current_category = "";
|
|
for c in checks {
|
|
if c.category != current_category {
|
|
current_category = c.category;
|
|
println!();
|
|
println!("{}", color::bold(current_category));
|
|
}
|
|
println!(" {} {}", c.status.label(), c.message);
|
|
if let Some(fix) = &c.fix {
|
|
println!(" {} {}", color::dim("fix:"), fix);
|
|
}
|
|
}
|
|
|
|
if !fixed.is_empty() {
|
|
println!();
|
|
println!("{}", color::bold("Fixed"));
|
|
for line in fixed {
|
|
println!(" {} {}", color::green("done"), line);
|
|
}
|
|
}
|
|
|
|
println!();
|
|
let line = format!(
|
|
"Summary: {} pass, {} warn, {} fail",
|
|
summary.pass, summary.warn, summary.fail
|
|
);
|
|
if summary.fail > 0 {
|
|
println!("{}", color::red(&line));
|
|
} else if summary.warn > 0 {
|
|
println!("{}", color::yellow(&line));
|
|
} else {
|
|
println!("{}", color::green(&line));
|
|
}
|
|
|
|
if !fix_ran && checks.iter().any(|c| c.fix.is_some()) {
|
|
println!();
|
|
println!(
|
|
"{} Run with {} to attempt repairs.",
|
|
color::dim("tip:"),
|
|
color::bold("--fix")
|
|
);
|
|
}
|
|
}
|
|
|
|
fn print_json(checks: &[Check], summary: &Summary, fixed: &[String], success: bool) {
|
|
let checks_json: Vec<Value> = checks
|
|
.iter()
|
|
.map(|c| {
|
|
let mut obj = json!({
|
|
"id": c.id,
|
|
"category": c.category,
|
|
"status": c.status.as_str(),
|
|
"message": c.message,
|
|
});
|
|
if let Some(fix) = &c.fix {
|
|
obj["fix"] = json!(fix);
|
|
}
|
|
obj
|
|
})
|
|
.collect();
|
|
|
|
let payload = json!({
|
|
"success": success,
|
|
"summary": {
|
|
"pass": summary.pass,
|
|
"warn": summary.warn,
|
|
"fail": summary.fail,
|
|
},
|
|
"checks": checks_json,
|
|
"fixed": fixed,
|
|
});
|
|
println!("{}", payload);
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_summary_counts_each_status() {
|
|
let checks = vec![
|
|
Check::new("a", "Cat", Status::Pass, "ok"),
|
|
Check::new("b", "Cat", Status::Pass, "ok"),
|
|
Check::new("c", "Cat", Status::Warn, "meh"),
|
|
Check::new("d", "Cat", Status::Fail, "no"),
|
|
Check::new("e", "Cat", Status::Info, "fyi"),
|
|
];
|
|
let s = summarize(&checks);
|
|
assert_eq!(s.pass, 2);
|
|
assert_eq!(s.warn, 1);
|
|
assert_eq!(s.fail, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_summary_zeroes_when_only_info() {
|
|
let checks = vec![Check::new("a", "Cat", Status::Info, "ignored")];
|
|
let s = summarize(&checks);
|
|
assert_eq!(s.pass, 0);
|
|
assert_eq!(s.warn, 0);
|
|
assert_eq!(s.fail, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_status_label_does_not_panic() {
|
|
for s in &[Status::Pass, Status::Warn, Status::Fail, Status::Info] {
|
|
assert!(!s.label().is_empty());
|
|
assert!(!s.as_str().is_empty());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_status_as_str_values() {
|
|
assert_eq!(Status::Pass.as_str(), "pass");
|
|
assert_eq!(Status::Warn.as_str(), "warn");
|
|
assert_eq!(Status::Fail.as_str(), "fail");
|
|
assert_eq!(Status::Info.as_str(), "info");
|
|
}
|
|
|
|
#[test]
|
|
fn test_check_new_and_with_fix() {
|
|
let c = Check::new("id", "cat", Status::Warn, "msg").with_fix("do thing");
|
|
assert_eq!(c.id, "id");
|
|
assert_eq!(c.category, "cat");
|
|
assert_eq!(c.status, Status::Warn);
|
|
assert_eq!(c.message, "msg");
|
|
assert_eq!(c.fix.as_deref(), Some("do thing"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_check_new_no_fix_by_default() {
|
|
let c = Check::new("id", "cat", Status::Pass, "msg");
|
|
assert!(c.fix.is_none());
|
|
}
|
|
}
|