Skip to main content

browser_automation_cli/
lib.rs

1//! # browser-automation-cli
2//!
3//! One-shot Chrome CDP automation library and CLI for AI agents.
4//!
5//! Lifecycle is always **BORN → EXECUTE → FINALIZE → DIE** in a single process.
6//! There is no daemon, no npm runtime, and no remote telemetry.
7//!
8//! ## Overview
9//!
10//! - Parse argv with clap ([`cli`])
11//! - Dispatch one command or a multi-step `run --script` session
12//! - Launch system Chrome/Chromium through chromiumoxide CDP
13//! - Always attempt FINALIZE (Browser.close, wait, kill fallback)
14//!
15//! ## Quick Start
16//!
17//! ```bash
18//! cargo install --path . --locked
19//! browser-automation-cli doctor --offline --quick --json
20//! browser-automation-cli goto https://example.com --json
21//! ```
22//!
23//! Library entry for embedding or tests:
24//!
25//! ```no_run
26//! use std::process::ExitCode;
27//!
28//! fn main() -> ExitCode {
29//!     browser_automation_cli::run()
30//! }
31//! ```
32//!
33//! ## Features
34//!
35//! This crate has no Cargo feature flags.
36//! Rustdoc embeds Mermaid lifecycle diagrams via `aquamarine` on documented entry points.
37//! Optional CLI categories are process flags:
38//!
39//! - `--category-memory` — deep heap tools
40//! - `--category-extensions` — extension tools
41//! - `--category-third-party` — third-party DevTools helpers
42//! - `--category-webmcp` — webmcp tools
43//! - `--experimental-vision` — coordinate click
44//! - `--experimental-screencast` — screencast export (needs ffmpeg)
45//!
46//! ## Targets
47//!
48//! Documented and tested for:
49//!
50//! - `x86_64-unknown-linux-gnu`
51//! - `x86_64-apple-darwin`
52//! - `aarch64-apple-darwin`
53//! - `x86_64-pc-windows-msvc`
54//! - `aarch64-unknown-linux-musl`
55//!
56//! Chrome automation is not supported on `wasm32-unknown-unknown`.
57//!
58//! ## MSRV
59//!
60//! Minimum Supported Rust Version is **1.88.0** (`rust-version` in `Cargo.toml`).
61//!
62//! ## Safety
63//!
64//! - No remote telemetry is emitted by this crate
65//! - Local tracing stays on stderr only
66//! - `docs.rs` builds pass `--cfg docsrs` and may enable `doc_cfg` on nightly
67//! - Unix paths may call `libc` for signal defaults and last-resort process kill
68//!
69//! ## Error handling
70//!
71//! Public errors use [`error::CliError`] with sysexits-style exit codes.
72//! JSON agents should parse the envelope from [`envelope`].
73//!
74//! ## Examples
75//!
76//! ```no_run
77//! use browser_automation_cli::error::{CliError, ErrorKind};
78//! use browser_automation_cli::exit_code_for;
79//!
80//! let err = CliError::new(ErrorKind::Unavailable, "chrome not found");
81//! assert_eq!(exit_code_for(&err), 69);
82//! ```
83//!
84//! ## See also
85//!
86//! - Crate README and `docs/HOW_TO_USE.md`
87//! - `docs/schemas/` for JSON contracts
88//! - `skill/browser-automation-cli-en/SKILL.md` for agent skill surface
89
90#![cfg_attr(docsrs, feature(doc_cfg))]
91#![warn(missing_docs)]
92#![warn(rustdoc::missing_crate_level_docs)]
93#![deny(rustdoc::broken_intra_doc_links)]
94#![warn(rustdoc::private_intra_doc_links)]
95#![deny(rustdoc::invalid_html_tags)]
96#![deny(rustdoc::invalid_rust_codeblocks)]
97#![deny(rustdoc::bare_urls)]
98#![warn(rustdoc::redundant_explicit_links)]
99
100/// Chrome one-shot session: launch, actions, reap.
101pub mod browser;
102/// HTTP/parse cache under XDG (one-shot L1 + SQLite L2).
103pub mod cache;
104/// Clap derive surface and global flags.
105pub mod cli;
106/// ANSI color helpers for human stderr diagnostics.
107pub mod color;
108/// PRD command dispatch (meta paths and browser one-shot).
109pub mod commands_prd;
110/// Shared constants (schema version, product name).
111pub mod constants;
112/// Local install diagnostics (`doctor`).
113pub mod doctor;
114/// JSON success/error envelopes for agents.
115pub mod envelope;
116/// Typed CLI errors and exit codes.
117pub mod error;
118/// One-shot filesystem path discovery (`find-paths`).
119pub mod find_paths;
120/// Locale messaging helpers.
121pub mod i18n;
122/// Install path helpers for doctor and packaging checks.
123pub mod install;
124/// Cooperative cancel and FINALIZE ledger.
125pub mod lifecycle;
126/// Optional one-shot LLM HTTP extract (XDG key only).
127pub mod llm_local;
128/// Local MITM capture, CA, and HAR export (one-shot).
129pub mod mitm_local;
130/// Native CDP stack (browser, network, snapshot, heap).
131pub mod native;
132/// One-shot QR encode/decode (no Chrome).
133pub mod qr_local;
134/// Owned residual path discovery (CLI marker + chromium tmp).
135pub mod residual;
136/// Named retry policies with backoff and jitter.
137pub mod retry;
138/// robots.txt policy enforcement.
139pub mod robots;
140/// Local scrape/crawl/map/search/parse (HTTP + files; one-shot).
141pub mod scrape_local;
142/// Structural lint scan/rewrite one-shot (§5AC).
143pub mod sg_local;
144/// XLSX write-only path via rust_xlsxwriter (§5Z).
145pub mod sheet_local;
146/// Input and path validation helpers.
147pub mod validation;
148/// Windows Job Object helpers (stubs on non-Windows).
149pub mod win_job;
150/// Workflow journal DAG (petgraph + SQLite), one-shot run/resume.
151pub mod workflow_local;
152/// XDG Base Directory paths and config file (no `.env` at runtime).
153pub mod xdg;
154
155#[cfg(test)]
156#[allow(missing_docs)]
157pub mod test_utils;
158
159use std::process::ExitCode;
160
161use clap::{CommandFactory, Parser};
162
163use crate::cli::Cli;
164use crate::error::CliError;
165use crate::lifecycle::Lifecycle;
166
167/// Parse argv and run the one-shot CLI.
168///
169/// Always attempts FINALIZE before returning, including on clap help/version paths.
170///
171/// # Lifecycle
172///
173/// ```mermaid
174/// flowchart LR
175///   BORN → EXECUTE → FINALIZE → DIE
176/// ```
177///
178/// # Returns
179///
180/// Process [`ExitCode`] mapped from sysexits-style CLI codes.
181#[cfg_attr(doc, aquamarine::aquamarine)]
182pub fn run() -> ExitCode {
183    #[cfg(unix)]
184    unsafe {
185        libc::signal(libc::SIGPIPE, libc::SIG_DFL);
186    }
187
188    #[cfg(windows)]
189    {
190        std::env::set_var("MSYS_NO_PATHCONV", "1");
191        std::env::set_var("MSYS2_ARG_CONV_EXCL", "*");
192    }
193
194    let life = Lifecycle::new();
195
196    let cli = match Cli::try_parse() {
197        Ok(c) => c,
198        Err(e) => {
199            // clap: DisplayHelp/DisplayVersion → exit 0; usage errors → 2
200            // GAP-002: when `--json` is on argv, emit agent envelope on stdout (not human clap only).
201            let code = e.exit_code();
202            let wants_json = std::env::args_os().any(|a| a == "--json");
203            let is_help_or_version = matches!(
204                e.kind(),
205                clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion
206            );
207            if wants_json && !is_help_or_version && code != 0 {
208                let msg = e.to_string();
209                let err = crate::error::CliError::with_suggestion(
210                    crate::error::ErrorKind::Usage,
211                    msg.lines().next().unwrap_or("invalid arguments").to_string(),
212                    "Check --help or schema --cmd <name>; pass valid argv",
213                );
214                let _ = crate::envelope::print_error_json(&err);
215            } else {
216                let _ = e.print();
217            }
218            life.finalize();
219            return ExitCode::from(code as u8);
220        }
221    };
222
223    // Resolve language once (CLI flag > XDG config > OS locale). Used for human suggestions.
224    let lang = crate::i18n::resolve_lang(cli.globals.lang.as_deref());
225    crate::i18n::set_effective_lang(lang);
226
227    init_tracing(
228        cli.globals.quiet,
229        cli.globals.verbose,
230        cli.globals.debug,
231        cli.globals.lang.as_deref(),
232    );
233
234    let code = commands_prd::dispatch(cli, &life);
235    // finalize is called inside dispatch; call again is idempotent
236    life.finalize();
237    if code <= 0 {
238        ExitCode::SUCCESS
239    } else if code >= 256 {
240        ExitCode::from(255)
241    } else {
242        ExitCode::from(code as u8)
243    }
244}
245
246/// Run clap `debug_assert` on the command tree (tests and diagnostics).
247pub fn command_factory_debug_assert() {
248    Cli::command().debug_assert();
249}
250
251/// Map a [`CliError`] to its process exit code without parsing argv.
252///
253/// Useful for unit tests and library callers that already hold a typed error.
254pub fn exit_code_for(err: &CliError) -> u8 {
255    err.exit_code()
256}
257
258/// Local-only tracing on stderr (zero remote telemetry).
259///
260/// Level order: `--quiet` / `--debug` / `--verbose` / XDG `log_level` / default `error`.
261/// Product settings never read `RUST_LOG` (XDG + argv only).
262fn init_tracing(quiet: bool, verbose: bool, debug: bool, _cli_lang: Option<&str>) {
263    use tracing_subscriber::EnvFilter;
264
265    let xdg_level = crate::xdg::load_config()
266        .ok()
267        .and_then(|c| c.log_level)
268        .filter(|s| !s.trim().is_empty());
269
270    let filter = if quiet {
271        EnvFilter::new("error")
272    } else if debug {
273        EnvFilter::new("debug")
274    } else if verbose {
275        EnvFilter::new("info")
276    } else if let Some(level) = xdg_level {
277        EnvFilter::new(level)
278    } else {
279        EnvFilter::new("error")
280    };
281
282    let log_to_file = crate::xdg::load_config()
283        .ok()
284        .and_then(|c| c.log_to_file)
285        .unwrap_or(false);
286
287    if log_to_file {
288        // Optional rotated local file under XDG state (GAP-012). Never remote telemetry.
289        if let Ok(state) = crate::xdg::state_dir() {
290            let log_dir = state.join("log");
291            let _ = std::fs::create_dir_all(&log_dir);
292            let file_appender =
293                tracing_appender::rolling::daily(&log_dir, "browser-automation-cli");
294            let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
295            // Leak guard for process lifetime (one-shot DIE frees it).
296            std::mem::forget(_guard);
297            let _ = tracing_subscriber::fmt()
298                .with_env_filter(filter)
299                .with_writer(non_blocking)
300                .with_target(false)
301                .try_init();
302            return;
303        }
304    }
305
306    let _ = tracing_subscriber::fmt()
307        .with_env_filter(filter)
308        .with_writer(std::io::stderr)
309        .with_target(false)
310        .try_init();
311}