1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
mod base64;
mod browser;
mod cdp;
mod cli;
mod commands;
#[cfg(unix)]
mod daemon;
mod element;
mod element_ref;
mod element_selector;
mod element_controls;
mod geometry;
mod hit_test;
mod hints;
mod landing;
mod pipe;
mod pipe_dispatch;
mod pipe_dispatch_actions;
mod pipe_report;
mod profiles;
mod read_back;
mod render;
mod run;
mod run_helpers;
mod session;
mod setup;
mod snapshot;
mod snapshot_secret;
mod truncate;
mod verdict;
mod verdict_evidence;
mod verdict_words;
/// Shared error type alias used across the crate.
pub(crate) type BoxError = Box<dyn std::error::Error>;
use clap::Parser;
use serde_json::json;
use crate::cli::Cli;
use crate::run_helpers::error_hint;
#[tokio::main]
async fn main() {
// Not `Cli::parse()`: clap exits 2 on a usage error, and 2 now means "the assertion did
// not hold" (`commands::assert`). A wrong flag is the caller's mistake, not a fact about
// the page, so it joins every other operational failure at 1 and leaves 2 to mean one
// thing. `--help`/`--version` still print to stdout and exit 0.
let cli = match Cli::try_parse() {
Ok(cli) => cli,
Err(e) => {
let usage = !matches!(
e.kind(),
clap::error::ErrorKind::DisplayHelp
| clap::error::ErrorKind::DisplayVersion
| clap::error::ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
);
if usage {
// One usage error is really about flag position, and clap's own tip for it sends
// the reader off to escape an argument they never meant as a string. `hints`
// rewrites that one and returns every other error unchanged. Help and version
// still go through clap, on stdout.
let argv: Vec<String> = std::env::args().collect();
eprint!("{}", hints::usage_error(&e.to_string(), &argv));
} else {
let _ = e.print();
}
std::process::exit(i32::from(usage));
}
};
let json_mode = cli.json;
// Captured before `cli` is consumed by `run`: an error hint names a command to run, and
// that command has to reach the browser THIS invocation drove, not the default one.
let browser = cli.browser.clone();
// Parse and stop. The embedded guide (`llm-guide.txt`, printed by `--help`) is what
// an agent copies its invocations from, and it once documented a flag that did not
// exist; checking it against the parser needs a way to reach clap's verdict —
// including missing required arguments, which `--help` short-circuits past — without
// launching a browser. Env var rather than a flag: this is a test affordance, not
// part of the command surface.
if std::env::var_os("CHROME_AGENT_PARSE_ONLY").is_some() {
return;
}
// Clean up this invocation's managed Chrome on Ctrl+C — and only this one. The
// handler used to walk every entry in the shared sessions.json and kill each pid
// raw, so interrupting one agent killed every other agent's browser mid-task and
// bypassed the PID-reuse guard every other kill path goes through. Installed after
// parsing because it needs to know which browser is ours — and whether we own one
// at all: `--browser` is global, so `daemon start` carries the default name for a
// browser it never launched.
let interrupted_browser = run_helpers::interrupt_owns_browser(&cli.command).then(|| cli.browser.clone());
tokio::spawn(async move {
if matches!(tokio::signal::ctrl_c().await, Ok(())) {
if let Some(name) = interrupted_browser
&& let Ok(store) = session::load_session()
&& let Some(pid) = run_helpers::interrupt_kill_target(&store, &name) {
run_helpers::kill_pid(pid);
}
std::process::exit(130);
}
});
if let Err(e) = run::run(cli).await {
// An assertion that did not hold is not a broken tool: it gets its own exit code so
// a caller can tell "the page is not in that state" (2) from "the browser never
// started" (1). Checked before the generic handler below, which would print it as a
// failure and exit 1 — the very conflation the code exists to remove.
if let Some(not_held) = e.downcast_ref::<commands::assert::NotHeld>() {
std::process::exit(not_held.report());
}
let msg = e.to_string();
if json_mode {
let hint = error_hint(&msg, &browser);
let mut obj = json!({"ok": false, "error": msg});
if let Some(h) = hint {
obj["hint"] = json!(h);
}
println!("{}", serde_json::to_string(&obj).unwrap_or_default());
} else {
eprintln!("error: {msg}");
if let Some(hint) = error_hint(&msg, &browser) {
eprintln!("hint: {hint}");
}
}
std::process::exit(1);
}
}