Skip to main content

browser_automation_cli/
cli.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Clap derive surface for browser-automation-cli (PRD Layer L).
3//!
4//! Help text on flags is the primary documentation for this module.
5//! Item-level rustdoc is intentionally light: clap `///` strings power `--help`
6//! and man pages; agent skills cover recipes (audit D-02/D-11).
7#![allow(missing_docs)]
8
9use clap::{ArgAction, Args, Parser, Subcommand, ValueEnum, ValueHint};
10
11/// One-shot browser automation CLI for AI agents.
12#[derive(Debug, Parser)]
13#[command(
14    name = "browser-automation-cli",
15    version,
16    author,
17    about = "One-shot browser automation CLI (Chrome CDP). BORN, EXECUTE, FINALIZE, DIE.",
18    long_about = None,
19    propagate_version = true,
20    after_help = "Examples:\n  \
21browser-automation-cli doctor --json\n  \
22browser-automation-cli goto https://example.com --json\n  \
23browser-automation-cli schema run\n  \
24browser-automation-cli run --script steps.ndjson --json-steps\n  \
25browser-automation-cli config path\n\n\
26Exit codes follow sysexits-style mapping (2 usage, 69 unavailable, 70 software, 124 timeout).\n\
27Config is XDG-only (config set); product settings do not read process environment variables."
28)]
29pub struct Cli {
30    /// Global flags shared by all subcommands
31    #[command(flatten)]
32    pub globals: GlobalOpts,
33
34    /// Subcommand to execute (one-shot)
35    #[command(subcommand)]
36    pub command: Commands,
37}
38
39/// Global options applied to every subcommand.
40///
41/// Flattened into the root [`Cli`] via `#[command(flatten)]`.
42#[derive(Debug, Clone, Args)]
43pub struct GlobalOpts {
44    /// Emit machine-readable JSON success/error envelopes on stdout
45    #[arg(long, global = true, action = ArgAction::SetTrue, help_heading = "Output")]
46    pub json: bool,
47
48    /// GAP-020: stream one NDJSON object per `run` step on stdout (step,cmd,ok,result)
49    #[arg(
50        long = "json-steps",
51        global = true,
52        action = ArgAction::SetTrue,
53        help_heading = "Output"
54    )]
55    pub json_steps: bool,
56
57    /// Suppress non-error human logs on stderr
58    #[arg(
59        short = 'q',
60        long = "quiet",
61        global = true,
62        action = ArgAction::SetTrue,
63        conflicts_with_all = ["verbose", "debug"],
64        help_heading = "Output"
65    )]
66    pub quiet: bool,
67
68    /// Increase stderr verbosity (`-v` / `--verbose` = info; or `config set log_level debug`)
69    #[arg(
70        short = 'v',
71        long = "verbose",
72        global = true,
73        action = ArgAction::SetTrue,
74        conflicts_with = "quiet",
75        help_heading = "Output"
76    )]
77    pub verbose: bool,
78
79    /// Maximum tracing detail on stderr (debug/trace)
80    #[arg(
81        long = "debug",
82        global = true,
83        action = ArgAction::SetTrue,
84        conflicts_with = "quiet",
85        help_heading = "Output"
86    )]
87    pub debug: bool,
88
89    /// Force plain stderr (no ANSI colors; accessibility / agent-friendly)
90    #[arg(
91        long = "plain",
92        global = true,
93        action = ArgAction::SetTrue,
94        help_heading = "Output"
95    )]
96    pub plain: bool,
97
98    /// Global wall-clock timeout in seconds (0 = no override)
99    #[arg(
100        long,
101        global = true,
102        default_value_t = 0,
103        value_name = "SECS",
104        help_heading = "Timeouts"
105    )]
106    pub timeout: u64,
107
108    /// Max concurrent I/O tasks (batch/crawl/CDP fan-out) and Rayon CPU pool hint
109    ///
110    /// `0` = auto: `min(cpus, (free_ram_mb×50%)/64, 64)`. Every fan-out is
111    /// hard-capped (no unbounded `join_all` / spawn loops).
112    #[arg(
113        long = "max-concurrency",
114        global = true,
115        default_value_t = 0,
116        value_name = "N",
117        help_heading = "Parallelism"
118    )]
119    pub max_concurrency: usize,
120
121    /// Per-step timeout in seconds for `run` scripts (0 = inherit global timeout)
122    #[arg(
123        long,
124        global = true,
125        default_value_t = 0,
126        value_name = "SECS",
127        help_heading = "Timeouts"
128    )]
129    pub step_timeout: u64,
130
131    /// Launch Chrome with a visible window (debug; default headless=new)
132    #[arg(
133        long,
134        global = true,
135        action = ArgAction::SetTrue,
136        help_heading = "Browser"
137    )]
138    pub headed: bool,
139
140    /// Directory for screenshots, PDFs, and other one-shot artifacts
141    #[arg(
142        long,
143        global = true,
144        value_name = "DIR",
145        value_hint = ValueHint::DirPath,
146        help_heading = "Browser"
147    )]
148    pub artifacts_dir: Option<std::path::PathBuf>,
149
150    /// Force UI language (`en` / `pt-BR`); default: flag → env → XDG → OS → en
151    ///
152    /// Accepts BCP 47 (`pt-BR`, `en`) or legacy `pt`. Env override:
153    /// `BROWSER_AUTOMATION_CLI_LANG`. Machine JSON stays English.
154    #[arg(long, global = true, value_name = "LANG", help_heading = "Output")]
155    pub lang: Option<String>,
156
157    /// Capture console messages during browser commands
158    #[arg(
159        long,
160        global = true,
161        action = ArgAction::SetTrue,
162        help_heading = "Browser"
163    )]
164    pub capture_console: bool,
165
166    /// Capture network requests during browser commands
167    #[arg(
168        long,
169        global = true,
170        action = ArgAction::SetTrue,
171        help_heading = "Browser"
172    )]
173    pub capture_network: bool,
174
175    /// Skip robots.txt policy checks (requires risk acceptance for blocked hosts)
176    #[arg(
177        long,
178        global = true,
179        action = ArgAction::SetTrue,
180        help_heading = "Robots"
181    )]
182    pub ignore_robots: bool,
183
184    /// Explicitly accept robots.txt override risk when using --ignore-robots
185    #[arg(
186        long,
187        global = true,
188        action = ArgAction::SetTrue,
189        help_heading = "Robots"
190    )]
191    pub i_accept_robots_risk: bool,
192
193    /// Enable deep heap analysis tools (PRD category-memory)
194    #[arg(
195        long,
196        global = true,
197        action = ArgAction::SetTrue,
198        help_heading = "Categories"
199    )]
200    pub category_memory: bool,
201
202    /// Enable extension management tools
203    #[arg(
204        long,
205        global = true,
206        action = ArgAction::SetTrue,
207        help_heading = "Categories"
208    )]
209    pub category_extensions: bool,
210
211    /// Enable third-party developer tool surface
212    #[arg(
213        long,
214        global = true,
215        action = ArgAction::SetTrue,
216        help_heading = "Categories"
217    )]
218    pub category_third_party: bool,
219
220    /// Enable WebMCP-compatible tool surface
221    #[arg(
222        long,
223        global = true,
224        action = ArgAction::SetTrue,
225        help_heading = "Categories"
226    )]
227    pub category_webmcp: bool,
228
229    /// Enable experimental screencast (may require ffmpeg for file export)
230    #[arg(
231        long,
232        global = true,
233        action = ArgAction::SetTrue,
234        help_heading = "Categories"
235    )]
236    pub experimental_screencast: bool,
237
238    /// Enable coordinate click-at (vision) tools
239    #[arg(
240        long,
241        global = true,
242        action = ArgAction::SetTrue,
243        help_heading = "Categories"
244    )]
245    pub experimental_vision: bool,
246
247    /// Enable one-shot local MITM proxy and route Chrome through it (PRD §5E / GAP-019)
248    #[arg(
249        long,
250        global = true,
251        action = ArgAction::SetTrue,
252        help_heading = "MITM"
253    )]
254    pub mitm: bool,
255
256    /// Directory for MITM CA key+cert PEM (default: XDG data)
257    #[arg(
258        long,
259        global = true,
260        value_name = "DIR",
261        value_hint = ValueHint::DirPath,
262        help_heading = "MITM"
263    )]
264    pub mitm_ca_dir: Option<std::path::PathBuf>,
265
266    /// Write HAR 1.2 to this path on FINALIZE when --mitm is active
267    #[arg(
268        long,
269        global = true,
270        value_name = "FILE",
271        value_hint = ValueHint::FilePath,
272        help_heading = "MITM"
273    )]
274    pub mitm_har: Option<std::path::PathBuf>,
275
276    /// Comma-separated hosts to decrypt (empty = all via proxy)
277    #[arg(long, global = true, value_name = "HOSTS", help_heading = "MITM")]
278    pub mitm_hosts: Option<String>,
279
280    /// Capture WebSocket frames in MITM handler
281    #[arg(
282        long,
283        global = true,
284        action = ArgAction::SetTrue,
285        help_heading = "MITM"
286    )]
287    pub mitm_ws: bool,
288
289    /// Max body bytes retained per exchange
290    #[arg(long, global = true, value_name = "BYTES", help_heading = "MITM")]
291    pub mitm_max_body_bytes: Option<usize>,
292
293    /// Drop image/video/audio bodies from MITM capture
294    #[arg(
295        long,
296        global = true,
297        action = ArgAction::SetTrue,
298        help_heading = "MITM"
299    )]
300    pub mitm_no_media_bodies: bool,
301
302    /// Redact Authorization/Cookie secrets in MITM exports (default on when set)
303    #[arg(
304        long,
305        global = true,
306        action = ArgAction::SetTrue,
307        help_heading = "MITM"
308    )]
309    pub mitm_redact_secrets: bool,
310}
311
312#[derive(Debug, Subcommand)]
313pub enum Commands {
314    /// Diagnose Chrome install and one-shot readiness
315    ///
316    /// Use global `--json` for machine-readable envelopes (no local `--json` —
317    /// avoids silent shadowing of the global flag).
318    Doctor {
319        #[arg(long, action = ArgAction::SetTrue)]
320        offline: bool,
321        #[arg(long, action = ArgAction::SetTrue)]
322        quick: bool,
323        #[arg(long, action = ArgAction::SetTrue)]
324        fix: bool,
325    },
326    /// List available commands
327    ///
328    /// Use global `--json` for the machine inventory payload.
329    Commands,
330    /// JSON Schema fragment for a command (agent discovery)
331    /// GAP-022: accepts `schema run` or `schema --cmd run`.
332    Schema {
333        /// Command name via --cmd
334        #[arg(long = "cmd", value_name = "CMD")]
335        cmd: Option<String>,
336        /// Command name as positional (preferred agent UX)
337        #[arg(value_name = "CMD")]
338        cmd_positional: Option<String>,
339    },
340    /// Print CLI version
341    Version,
342    /// Show resolved UI locale and detection diagnostics (human suggestions only)
343    Locale,
344    /// Navigate to a URL (one-shot)
345    Goto {
346        #[arg(value_hint = ValueHint::Url)]
347        url: String,
348        /// JS to evaluate before navigation (tool-ref initScript)
349        #[arg(long)]
350        init_script: Option<String>,
351        /// Auto-handle beforeunload: accept | dismiss (GAP-003; flag alone = accept)
352        #[arg(long, value_enum, num_args = 0..=1, default_missing_value = "accept")]
353        handle_before_unload: Option<BeforeUnloadAction>,
354        /// Navigation timeout override in milliseconds
355        #[arg(long)]
356        navigation_timeout_ms: Option<u64>,
357    },
358    /// Accessibility snapshot with @eN refs
359    View {
360        /// Full a11y tree (tool-ref take_snapshot.verbose / run JSON `"verbose":true`).
361        ///
362        /// CLI long name is `--detailed` so it does not silently shadow global
363        /// `--verbose` (product logging). Multi-step `run` scripts still use the
364        /// JSON key `verbose` for DevTools tool-ref parity.
365        #[arg(long = "detailed", action = ArgAction::SetTrue)]
366        verbose: bool,
367        #[arg(long, value_hint = ValueHint::FilePath)]
368        path: Option<std::path::PathBuf>,
369        /// Allow empty about:blank snapshots (GAP-012)
370        #[arg(long, action = ArgAction::SetTrue)]
371        allow_empty: bool,
372    },
373    /// Click an element (selector or @eN)
374    Press {
375        target: String,
376        #[arg(long, action = ArgAction::SetTrue)]
377        dblclick: bool,
378        /// Attach slim a11y snapshot in the same process after the action
379        #[arg(long, action = ArgAction::SetTrue)]
380        include_snapshot: bool,
381    },
382    /// Click at page CSS coordinates (requires --experimental-vision)
383    ClickAt {
384        #[arg(long)]
385        x: f64,
386        #[arg(long)]
387        y: f64,
388        #[arg(long, action = ArgAction::SetTrue)]
389        dblclick: bool,
390        /// Attach slim a11y snapshot after the click
391        #[arg(long, action = ArgAction::SetTrue)]
392        include_snapshot: bool,
393    },
394    /// Fill an input value (select/checkbox/radio/text smart fill)
395    Write {
396        target: String,
397        value: String,
398        /// Attach slim a11y snapshot after fill
399        #[arg(long, action = ArgAction::SetTrue)]
400        include_snapshot: bool,
401    },
402    /// Press a keyboard key
403    Keys {
404        key: String,
405        /// Attach slim a11y snapshot after the key press
406        #[arg(long, action = ArgAction::SetTrue)]
407        include_snapshot: bool,
408    },
409    /// Type text (tool-ref type_text). Use --target or --focus-only.
410    Type {
411        /// Text to type (required positional)
412        text: String,
413        /// CSS selector or @eN (optional; use --focus-only for focused element)
414        #[arg(long)]
415        target: Option<String>,
416        #[arg(long, action = ArgAction::SetTrue)]
417        clear: bool,
418        /// Optional key to press after typing (e.g. Enter)
419        #[arg(long)]
420        submit: Option<String>,
421        /// Type into currently focused element without resolving a target
422        #[arg(long, action = ArgAction::SetTrue)]
423        focus_only: bool,
424    },
425    /// Wait for ms and/or text and/or selector and/or load state
426    Wait {
427        #[arg(long, default_value_t = 0)]
428        ms: u64,
429        /// Text to wait for (repeatable; resolves when any value appears — tool-ref OR)
430        #[arg(long = "text", action = clap::ArgAction::Append)]
431        text: Vec<String>,
432        #[arg(long)]
433        selector: Option<String>,
434        /// Page lifecycle: load | domcontentloaded | networkidle | none
435        #[arg(long)]
436        state: Option<String>,
437        /// Max wait time in milliseconds for text/selector/state (0 = default)
438        #[arg(long)]
439        wait_timeout_ms: Option<u64>,
440        /// Attach slim a11y snapshot after the wait succeeds
441        #[arg(long, action = ArgAction::SetTrue)]
442        include_snapshot: bool,
443    },
444    /// Hover an element
445    Hover {
446        target: String,
447        /// Attach slim a11y snapshot after hover
448        #[arg(long, action = ArgAction::SetTrue)]
449        include_snapshot: bool,
450    },
451    /// Drag from one target to another
452    Drag {
453        #[arg(long)]
454        from: String,
455        #[arg(long)]
456        to: String,
457        /// Attach slim a11y snapshot after drag
458        #[arg(long, action = ArgAction::SetTrue)]
459        include_snapshot: bool,
460    },
461    /// Fill multiple form fields from JSON `[{target|uid,value},...]`
462    FillForm {
463        /// JSON array payload (not the global envelope flag `--json`)
464        #[arg(long = "fields-json", value_name = "JSON")]
465        fields_json: String,
466        /// Attach slim a11y snapshot after fill-form
467        #[arg(long, action = ArgAction::SetTrue)]
468        include_snapshot: bool,
469    },
470    /// Upload a file to a file input
471    Upload {
472        target: String,
473        #[arg(value_hint = ValueHint::FilePath)]
474        path: std::path::PathBuf,
475        /// Attach slim a11y snapshot after upload
476        #[arg(long, action = ArgAction::SetTrue)]
477        include_snapshot: bool,
478    },
479    /// History back
480    Back,
481    /// History forward
482    Forward,
483    /// Reload current page
484    Reload {
485        #[arg(long, action = ArgAction::SetTrue)]
486        ignore_cache: bool,
487        /// JS to run before navigation/reload (tool-ref initScript)
488        #[arg(long)]
489        init_script: Option<String>,
490        /// Auto-handle beforeunload: accept | dismiss (GAP-003; flag alone = accept)
491        #[arg(long, value_enum, num_args = 0..=1, default_missing_value = "accept")]
492        handle_before_unload: Option<BeforeUnloadAction>,
493    },
494    /// Evaluate JavaScript (expression or function declaration)
495    Eval {
496        /// JS expression or function declaration `() => ...`
497        expression: String,
498        /// Snapshot uids (@eN) passed as function args (JSON array of strings)
499        #[arg(long)]
500        args: Option<String>,
501        /// accept | dismiss | prompt response text (default accept)
502        #[arg(long)]
503        dialog_action: Option<String>,
504        /// Write evaluate result JSON to this path
505        #[arg(long, value_hint = ValueHint::FilePath)]
506        file_path: Option<std::path::PathBuf>,
507        /// Evaluate inside an extension service worker target (tool-ref serviceWorkerId)
508        #[arg(long)]
509        service_worker_id: Option<String>,
510    },
511    /// Capture a screenshot
512    Grab {
513        #[arg(long, value_hint = ValueHint::FilePath)]
514        path: Option<std::path::PathBuf>,
515        #[arg(long, value_enum, default_value_t = GrabFormat::Png)]
516        format: GrabFormat,
517        #[arg(long, action = ArgAction::SetTrue)]
518        full_page: bool,
519        #[arg(long)]
520        quality: Option<i32>,
521        /// CSS selector or @eN element to capture
522        #[arg(long)]
523        element: Option<String>,
524    },
525    /// Print current page to PDF via CDP Page.printToPDF (one-shot)
526    PrintPdf {
527        /// Output path for the PDF artifact
528        #[arg(long, value_hint = ValueHint::FilePath)]
529        path: Option<std::path::PathBuf>,
530        /// Optional URL to navigate before printing (one-shot)
531        #[arg(long)]
532        url: Option<String>,
533    },
534    /// One-shot change check against a baseline file (hash/text)
535    Monitor {
536        #[command(subcommand)]
537        action: MonitorAction,
538    },
539    /// Run multi-step NDJSON script in one process
540    Run {
541        #[arg(long, value_hint = ValueHint::FilePath)]
542        script: std::path::PathBuf,
543    },
544    /// Single-step inline command (same surface as `run` steps: goto, wait, view, press, …)
545    Exec {
546        // Do NOT set allow_hyphen_values: global flags like --json after `exec`
547        // must stay on GlobalOpts, not be swallowed into trailing args.
548        #[arg(trailing_var_arg = true)]
549        args: Vec<String>,
550    },
551    /// Extract text/attribute from a target, or LLM extract with --llm
552    Extract {
553        /// Selector, @eN ref, about:blank target, or http(s) URL for LLM/text path
554        target: String,
555        #[arg(long)]
556        attr: Option<String>,
557        /// Opt-in LLM HTTP extract (requires XDG openrouter_api_key)
558        #[arg(long, action = ArgAction::SetTrue)]
559        llm: bool,
560        /// Question for LLM extract
561        #[arg(long)]
562        question: Option<String>,
563        /// Path to JSON schema file for structured LLM extract
564        #[arg(long, value_hint = ValueHint::FilePath)]
565        schema_json: Option<std::path::PathBuf>,
566    },
567    /// Extract visible text from a target (PRD §7 `text`)
568    Text { target: String },
569    /// Scroll page or element by delta pixels (PRD §7 `scroll`)
570    Scroll {
571        /// CSS selector or @eN (optional; omit for window scroll)
572        #[arg(long)]
573        target: Option<String>,
574        #[arg(long, default_value_t = 0.0)]
575        delta_x: f64,
576        #[arg(long, default_value_t = 0.0)]
577        delta_y: f64,
578    },
579    /// Cookie jar helpers for the active page (Network domain)
580    Cookie {
581        #[command(subcommand)]
582        action: CookieAction,
583    },
584    /// Read one attribute from a target
585    Attr { target: String, name: String },
586    /// Assertions (url / text / console)
587    Assert {
588        #[command(subcommand)]
589        kind: AssertKind,
590    },
591    /// Captured console messages (--capture-console)
592    Console {
593        #[command(subcommand)]
594        action: ConsoleAction,
595    },
596    /// Captured network requests (--capture-network)
597    Net {
598        #[command(subcommand)]
599        action: NetAction,
600    },
601    /// Page info or multi-tab management
602    Page {
603        #[command(subcommand)]
604        action: Option<PageAction>,
605    },
606    /// Accept or dismiss dialogs
607    Dialog {
608        #[command(subcommand)]
609        action: DialogAction,
610    },
611    /// Navigate and return body text / formats (local HTTP or CDP scrape)
612    Scrape {
613        #[arg(value_hint = ValueHint::Url)]
614        url: String,
615        /// text | markdown | html | links | metadata | … (CSV or repeatable; alias --formats)
616        #[arg(long = "format", alias = "formats", value_delimiter = ',', num_args = 1.., default_value = "text")]
617        format: Vec<String>,
618        /// http (reqwest+scraper) or browser (CDP)
619        #[arg(long, default_value = "browser")]
620        engine: String,
621        /// Prefer main/article content heuristics
622        #[arg(long, action = ArgAction::SetTrue)]
623        only_main_content: bool,
624        /// Optional one-shot webhook POST of the result envelope data (127.0.0.1/operator URL)
625        #[arg(long)]
626        webhook_url: Option<String>,
627    },
628    /// Scrape many URLs from a file (HTTP or browser engine, one-shot)
629    BatchScrape {
630        #[arg(long, value_hint = ValueHint::FilePath)]
631        urls_file: std::path::PathBuf,
632        #[arg(long = "format", alias = "formats", default_value = "text")]
633        format: String,
634        /// Concurrent HTTP fetches (`0` = use global `--max-concurrency` / auto)
635        #[arg(long, default_value_t = 0)]
636        concurrency: usize,
637        /// http (default) or browser (CDP per URL; GAP-010)
638        #[arg(long, default_value = "http")]
639        engine: String,
640    },
641    /// Crawl from a seed URL (HTTP BFS or browser, one-shot)
642    Crawl {
643        #[arg(value_hint = ValueHint::Url)]
644        url: String,
645        #[arg(long, alias = "max-pages", default_value_t = 20)]
646        limit: usize,
647        #[arg(long, default_value_t = 2)]
648        max_depth: usize,
649        #[arg(long = "format", alias = "formats", default_value = "text")]
650        format: String,
651        /// Stay on seed host
652        #[arg(long, default_value_t = true)]
653        same_host: bool,
654        /// http (default) or browser (GAP-010)
655        #[arg(long, default_value = "http")]
656        engine: String,
657    },
658    /// Map site URLs from a seed (HTTP)
659    Map {
660        #[arg(value_hint = ValueHint::Url)]
661        url: String,
662        #[arg(long, default_value_t = 50)]
663        limit: usize,
664        #[arg(long, default_value_t = 2)]
665        max_depth: usize,
666    },
667    /// Local search (HTTP SERP links or URL map)
668    Search {
669        query: String,
670        #[arg(long, default_value_t = 10)]
671        limit: usize,
672    },
673    /// Parse a local file (html/md/txt/pdf/docx/xlsx text extract)
674    Parse {
675        #[arg(value_hint = ValueHint::FilePath)]
676        path: std::path::PathBuf,
677        /// Mask email/phone/card-like patterns in text output
678        #[arg(long, action = ArgAction::SetTrue)]
679        redact_pii: bool,
680    },
681    /// QR encode/decode one-shot (no Chrome)
682    Qr {
683        #[command(subcommand)]
684        action: QrAction,
685    },
686    /// Discover filesystem paths (fd-like UX; binary remains browser-automation-cli)
687    FindPaths {
688        /// Regex pattern on name/path (optional)
689        pattern: Option<String>,
690        /// Root paths to search
691        #[arg(num_args = 0..)]
692        paths: Vec<String>,
693        /// Filter by extension (e.g. rs, html)
694        #[arg(long)]
695        extension: Option<String>,
696        /// Include hidden files
697        #[arg(long, action = ArgAction::SetTrue)]
698        hidden: bool,
699        /// Do not respect .gitignore
700        #[arg(long, action = ArgAction::SetTrue)]
701        no_ignore: bool,
702        /// Max directory depth
703        #[arg(long)]
704        max_depth: Option<usize>,
705        /// Entry type: f|d
706        #[arg(long = "type")]
707        entry_type: Option<String>,
708        /// Max results
709        #[arg(long, default_value_t = 10000)]
710        limit: usize,
711        /// Shell-style glob filter (e.g. `**/*.rs`) — GAP-A011 / §5AE
712        #[arg(long)]
713        glob: Option<String>,
714    },
715    /// Structural lint scan for forbidden product patterns (one-shot; §5AC / GAP-A011)
716    SgScan {
717        /// Roots to scan (default: `.`)
718        #[arg(num_args = 0..)]
719        paths: Vec<String>,
720        /// Max findings (0 = unlimited)
721        #[arg(long, default_value_t = 500)]
722        limit: usize,
723    },
724    /// Structural rewrite for known-safe fixes (dry-run default; `--apply` writes)
725    SgRewrite {
726        /// Roots to rewrite (default: `.`)
727        #[arg(num_args = 0..)]
728        paths: Vec<String>,
729        /// Apply changes (default is dry-run report only)
730        #[arg(long, action = ArgAction::SetTrue)]
731        apply: bool,
732    },
733    /// Write a simple XLSX workbook from CSV/JSON (one-shot; §5Z / GAP-A011)
734    SheetWrite {
735        /// Input path (.csv or .json array-of-objects)
736        #[arg(value_hint = ValueHint::FilePath)]
737        input: std::path::PathBuf,
738        /// Output .xlsx path
739        #[arg(long, short = 'o', value_hint = ValueHint::FilePath)]
740        out: std::path::PathBuf,
741        /// Worksheet name
742        #[arg(long, default_value = "Sheet1")]
743        sheet: String,
744    },
745    /// MITM capture / CA / HAR (one-shot local)
746    Mitm {
747        #[command(subcommand)]
748        action: MitmAction,
749    },
750    /// Workflow journal DAG (petgraph + SQLite)
751    Workflow {
752        #[command(subcommand)]
753        action: WorkflowAction,
754    },
755    /// XDG config and path management (no .env at runtime)
756    Config {
757        #[command(subcommand)]
758        action: ConfigAction,
759    },
760    /// Emulate device / network / UA / geo / CPU
761    Emulate {
762        #[arg(long)]
763        user_agent: Option<String>,
764        #[arg(long)]
765        locale: Option<String>,
766        #[arg(long)]
767        timezone: Option<String>,
768        #[arg(long, action = ArgAction::SetTrue)]
769        offline: bool,
770        #[arg(long)]
771        latitude: Option<f64>,
772        #[arg(long)]
773        longitude: Option<f64>,
774        #[arg(long)]
775        media: Option<String>,
776        /// Network preset: Offline, No throttling, Slow 3G, Fast 3G, Slow 4G, Fast 4G
777        #[arg(long)]
778        network_conditions: Option<String>,
779        /// CPU slowdown factor 1..=20 (1 disables)
780        #[arg(long)]
781        cpu_throttling_rate: Option<f64>,
782        /// prefers-color-scheme: dark | light | auto
783        #[arg(long)]
784        color_scheme: Option<String>,
785        /// Extra HTTP headers as JSON object string
786        #[arg(long)]
787        extra_headers: Option<String>,
788        /// Viewport `WxHxDPR` with optional `,mobile`, `,touch`, `,landscape` flags
789        #[arg(long)]
790        viewport: Option<String>,
791    },
792    /// Resize page viewport
793    Resize {
794        #[arg(long)]
795        width: i32,
796        #[arg(long)]
797        height: i32,
798        #[arg(long, default_value_t = 1.0)]
799        scale: f64,
800        #[arg(long, action = ArgAction::SetTrue)]
801        mobile: bool,
802    },
803    /// Performance trace / metrics
804    Perf {
805        #[command(subcommand)]
806        action: PerfAction,
807    },
808    /// Run Lighthouse audit (external binary)
809    Lighthouse {
810        #[arg(value_hint = ValueHint::Url)]
811        url: String,
812        #[arg(long, value_hint = ValueHint::DirPath)]
813        out_dir: Option<std::path::PathBuf>,
814        #[arg(long, default_value = "desktop")]
815        device: String,
816        /// navigation (default) or snapshot (maps to navigation in one-shot CLI)
817        #[arg(long, default_value = "navigation")]
818        mode: String,
819        #[arg(long, value_hint = ValueHint::FilePath)]
820        lighthouse_path: Option<std::path::PathBuf>,
821    },
822    /// Screencast start/stop (experimental)
823    Screencast {
824        #[command(subcommand)]
825        action: ScreencastAction,
826    },
827    /// Heap snapshot tools (requires --category-memory for deep analysis)
828    Heap {
829        #[command(subcommand)]
830        action: HeapAction,
831    },
832    /// Chrome extension tools (requires --category-extensions)
833    Extension {
834        #[command(subcommand)]
835        action: ExtensionAction,
836    },
837    /// Third-party developer tools surface (requires --category-third-party)
838    Devtools3p {
839        #[command(subcommand)]
840        action: Devtools3pAction,
841    },
842    /// Web surface tools (requires --category-webmcp)
843    Webmcp {
844        #[command(subcommand)]
845        action: WebmcpAction,
846    },
847    /// Generate shell completions (path-level, no Chrome)
848    Completions {
849        #[arg(value_enum)]
850        shell: CompletionShell,
851    },
852    /// Generate man page (roff) via clap_mangen (path-level, no Chrome)
853    Man {
854        /// Write man page to PATH instead of stdout
855        #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)]
856        out: Option<std::path::PathBuf>,
857    },
858}
859
860#[derive(Debug, Clone, Subcommand)]
861pub enum PerfAction {
862    Start {
863        #[arg(long, value_hint = ValueHint::FilePath)]
864        path: Option<std::path::PathBuf>,
865        #[arg(long, action = ArgAction::SetTrue)]
866        reload: bool,
867        /// Auto-stop after page load/reload (tool-ref autoStop)
868        #[arg(long, action = ArgAction::SetTrue)]
869        auto_stop: bool,
870    },
871    Stop {
872        #[arg(long, value_hint = ValueHint::FilePath)]
873        path: Option<std::path::PathBuf>,
874    },
875    Insight {
876        /// Insight name (e.g. DocumentLatency, LCPBreakdown)
877        #[arg(long)]
878        name: Option<String>,
879        /// Insight set id from perf stop "available_insight_sets"
880        #[arg(long)]
881        insight_set_id: Option<String>,
882        /// Alias for --name (tool-ref insightName)
883        #[arg(long)]
884        insight_name: Option<String>,
885    },
886}
887
888#[derive(Debug, Clone, Subcommand)]
889pub enum ScreencastAction {
890    Start {
891        #[arg(long, value_hint = ValueHint::FilePath)]
892        path: Option<std::path::PathBuf>,
893    },
894    Stop {
895        /// Output path (.webm/.mp4 encodes via ffmpeg; otherwise PNG frames dir)
896        #[arg(long, value_hint = ValueHint::FilePath)]
897        path: Option<std::path::PathBuf>,
898    },
899}
900
901#[derive(Debug, Clone, Subcommand)]
902pub enum HeapAction {
903    Take {
904        #[arg(long, value_hint = ValueHint::FilePath)]
905        path: std::path::PathBuf,
906    },
907    Close {
908        #[arg(long, value_hint = ValueHint::FilePath)]
909        path: std::path::PathBuf,
910    },
911    Compare {
912        #[arg(long, value_hint = ValueHint::FilePath)]
913        base: std::path::PathBuf,
914        #[arg(long, value_hint = ValueHint::FilePath)]
915        current: std::path::PathBuf,
916        /// Optional class index filter (tool-ref classIndex)
917        #[arg(long)]
918        class_index: Option<u64>,
919    },
920    Summary {
921        #[arg(long, value_hint = ValueHint::FilePath)]
922        path: std::path::PathBuf,
923    },
924    Details {
925        #[arg(long, value_hint = ValueHint::FilePath)]
926        path: std::path::PathBuf,
927        #[arg(long)]
928        filter_name: Option<String>,
929        #[arg(long)]
930        page_idx: Option<usize>,
931        #[arg(long)]
932        page_size: Option<usize>,
933    },
934    ClassNodes {
935        #[arg(long, value_hint = ValueHint::FilePath)]
936        path: std::path::PathBuf,
937        #[arg(long)]
938        id: u64,
939        #[arg(long)]
940        filter_name: Option<String>,
941        #[arg(long)]
942        page_idx: Option<usize>,
943        #[arg(long)]
944        page_size: Option<usize>,
945    },
946    Dominators {
947        #[arg(long, value_hint = ValueHint::FilePath)]
948        path: std::path::PathBuf,
949        #[arg(long)]
950        node: u64,
951    },
952    DupStrings {
953        #[arg(long, value_hint = ValueHint::FilePath)]
954        path: std::path::PathBuf,
955        #[arg(long)]
956        page_idx: Option<usize>,
957        #[arg(long)]
958        page_size: Option<usize>,
959    },
960    Edges {
961        #[arg(long, value_hint = ValueHint::FilePath)]
962        path: std::path::PathBuf,
963        #[arg(long)]
964        node: u64,
965        #[arg(long)]
966        page_idx: Option<usize>,
967        #[arg(long)]
968        page_size: Option<usize>,
969    },
970    Retainers {
971        #[arg(long, value_hint = ValueHint::FilePath)]
972        path: std::path::PathBuf,
973        #[arg(long)]
974        node: u64,
975        #[arg(long)]
976        page_idx: Option<usize>,
977        #[arg(long)]
978        page_size: Option<usize>,
979    },
980    Paths {
981        #[arg(long, value_hint = ValueHint::FilePath)]
982        path: std::path::PathBuf,
983        #[arg(long)]
984        node: u64,
985        #[arg(long, default_value_t = 8)]
986        max_depth: u64,
987        #[arg(long)]
988        max_nodes: Option<u64>,
989        #[arg(long)]
990        max_siblings: Option<u64>,
991    },
992    /// Detailed info for one heap object (size, distance, retained size, detachedness)
993    ObjectDetails {
994        #[arg(long, value_hint = ValueHint::FilePath)]
995        path: std::path::PathBuf,
996        #[arg(long)]
997        node: u64,
998    },
999}
1000
1001#[derive(Debug, Clone, Subcommand)]
1002pub enum ExtensionAction {
1003    List,
1004    Install {
1005        path: std::path::PathBuf,
1006    },
1007    Reload {
1008        id: String,
1009        /// Unpacked extension dir so one-shot can --load-extension before reload
1010        #[arg(long, value_hint = ValueHint::FilePath)]
1011        path: Option<std::path::PathBuf>,
1012    },
1013    Trigger {
1014        id: String,
1015        /// Unpacked extension dir so one-shot can --load-extension before trigger
1016        #[arg(long, value_hint = ValueHint::FilePath)]
1017        path: Option<std::path::PathBuf>,
1018    },
1019    Uninstall {
1020        id: String,
1021    },
1022}
1023
1024#[derive(Debug, Clone, Subcommand)]
1025pub enum Devtools3pAction {
1026    List {
1027        /// Optional page URL to open before discovery
1028        #[arg(long)]
1029        url: Option<String>,
1030    },
1031    Exec {
1032        name: String,
1033        #[arg(long)]
1034        params: Option<String>,
1035        #[arg(long)]
1036        url: Option<String>,
1037    },
1038}
1039
1040#[derive(Debug, Clone, Subcommand)]
1041pub enum WebmcpAction {
1042    List {
1043        #[arg(long)]
1044        url: Option<String>,
1045    },
1046    Exec {
1047        name: String,
1048        #[arg(long)]
1049        input: Option<String>,
1050        #[arg(long)]
1051        url: Option<String>,
1052    },
1053}
1054
1055#[derive(Debug, Clone, Copy, ValueEnum)]
1056pub enum CompletionShell {
1057    Bash,
1058    Zsh,
1059    Fish,
1060    Elvish,
1061    Powershell,
1062}
1063
1064#[derive(Debug, Clone, Subcommand)]
1065pub enum QrAction {
1066    /// Encode text to PNG, SVG, or terminal matrix
1067    Encode {
1068        #[arg(long)]
1069        text: String,
1070        /// png | svg | terminal
1071        #[arg(long, default_value = "png")]
1072        format: String,
1073        #[arg(long, value_hint = ValueHint::FilePath)]
1074        path: Option<std::path::PathBuf>,
1075    },
1076    /// Decode QR payload from an image file
1077    Decode {
1078        #[arg(long, value_hint = ValueHint::FilePath)]
1079        path: std::path::PathBuf,
1080    },
1081}
1082
1083#[derive(Debug, Clone, Subcommand)]
1084pub enum MonitorAction {
1085    /// Compare URL body hash/text to a baseline file and exit
1086    Check {
1087        /// URL to fetch/scrape one-shot
1088        #[arg(long)]
1089        url: String,
1090        /// Baseline file path (created on first run if missing when --write-baseline)
1091        #[arg(long, value_hint = ValueHint::FilePath)]
1092        baseline: std::path::PathBuf,
1093        /// Write/update baseline after check
1094        #[arg(long, action = ArgAction::SetTrue)]
1095        write_baseline: bool,
1096        /// Use browser engine instead of HTTP
1097        #[arg(long, default_value = "http")]
1098        engine: String,
1099    },
1100}
1101
1102#[derive(Debug, Clone, Subcommand)]
1103pub enum PageAction {
1104    /// Current page url and title (default when bare `page`)
1105    Info,
1106    /// List tabs in this one-shot process
1107    List,
1108    /// Open a new tab
1109    New {
1110        #[arg(long)]
1111        url: Option<String>,
1112        /// Open without focusing (tool-ref background)
1113        #[arg(long, action = ArgAction::SetTrue)]
1114        background: bool,
1115        /// Named isolated browser context (tool-ref isolatedContext string; GAP-004)
1116        #[arg(long, num_args = 0..=1, default_missing_value = "default-isolated")]
1117        isolated_context: Option<String>,
1118    },
1119    /// Select tab by zero-based index (alias: --page-id)
1120    Select {
1121        #[arg(value_name = "INDEX")]
1122        index: Option<usize>,
1123        /// Tool-ref pageId alias for index
1124        #[arg(long = "page-id")]
1125        page_id: Option<usize>,
1126        /// Bring selected tab to front (tool-ref bringToFront)
1127        #[arg(long, default_value_t = true)]
1128        bring_to_front: bool,
1129    },
1130    /// Close a tab (default: active)
1131    Close {
1132        #[arg(long)]
1133        index: Option<usize>,
1134        /// Tool-ref pageId alias for index
1135        #[arg(long = "page-id")]
1136        page_id: Option<usize>,
1137    },
1138    /// Return the stable tab id of the active page (tool-ref get_tab_id)
1139    TabId,
1140}
1141
1142#[derive(Debug, Clone, Subcommand)]
1143pub enum CookieAction {
1144    /// List cookies (optional URL filter)
1145    List {
1146        #[arg(long)]
1147        url: Option<String>,
1148    },
1149    /// Set cookies from a JSON array of cookie objects
1150    Set {
1151        /// JSON array: [{"name":"a","value":"b","url":"https://..."}]
1152        /// (long name avoids shadowing global envelope `--json`)
1153        #[arg(long = "cookies-json", value_name = "JSON")]
1154        cookies_json: String,
1155    },
1156    /// Clear all browser cookies in this one-shot process
1157    Clear,
1158}
1159
1160#[derive(Debug, Clone, Copy, ValueEnum, Default)]
1161pub enum GrabFormat {
1162    #[default]
1163    Png,
1164    Jpeg,
1165    Webp,
1166}
1167
1168/// GAP-003: tool-ref handleBeforeUnload accept | dismiss.
1169#[derive(Debug, Clone, Copy, ValueEnum)]
1170pub enum BeforeUnloadAction {
1171    Accept,
1172    Dismiss,
1173}
1174
1175impl BeforeUnloadAction {
1176    /// CDP dialog action token.
1177    pub fn as_str(self) -> &'static str {
1178        match self {
1179            Self::Accept => "accept",
1180            Self::Dismiss => "dismiss",
1181        }
1182    }
1183}
1184
1185
1186#[derive(Debug, Clone, Subcommand)]
1187pub enum AssertKind {
1188    Url {
1189        value: String,
1190        #[arg(long, action = ArgAction::SetTrue)]
1191        contains: bool,
1192    },
1193    Text {
1194        value: String,
1195        #[arg(long)]
1196        target: Option<String>,
1197    },
1198    Console {
1199        #[arg(long, default_value = "error")]
1200        level: String,
1201        #[arg(long, default_value_t = 0)]
1202        max: u64,
1203    },
1204    /// GAP-025: require zero console messages (any level)
1205    ConsoleEmpty,
1206    /// GAP-025: require no message text matching pattern
1207    ConsoleNoMatch {
1208        #[arg(long)]
1209        pattern: String,
1210    },
1211}
1212
1213#[derive(Debug, Clone, Subcommand)]
1214pub enum ConsoleAction {
1215    List {
1216        /// 0-based page index for pagination
1217        #[arg(long)]
1218        page_idx: Option<usize>,
1219        /// Max messages per page
1220        #[arg(long)]
1221        page_size: Option<usize>,
1222        /// Filter by types (comma-separated: log,warning,error,info,debug)
1223        #[arg(long)]
1224        types: Option<String>,
1225        /// Include messages preserved across navigations in this process
1226        #[arg(long, action = ArgAction::SetTrue)]
1227        include_preserved: bool,
1228        /// Optional service worker id filter
1229        #[arg(long)]
1230        service_worker_id: Option<String>,
1231    },
1232    Get {
1233        id: usize,
1234    },
1235    Clear,
1236    Dump {
1237        #[arg(long, value_hint = ValueHint::FilePath)]
1238        path: std::path::PathBuf,
1239    },
1240}
1241
1242#[derive(Debug, Clone, Subcommand)]
1243pub enum NetAction {
1244    List {
1245        /// 0-based page index for pagination
1246        #[arg(long)]
1247        page_idx: Option<usize>,
1248        /// Max requests per page
1249        #[arg(long)]
1250        page_size: Option<usize>,
1251        /// Filter resource types (comma-separated: Document,Script,XHR,Fetch,...)
1252        #[arg(long)]
1253        resource_types: Option<String>,
1254        /// Include requests preserved over recent navigations in this process
1255        #[arg(long, action = ArgAction::SetTrue)]
1256        include_preserved: bool,
1257    },
1258    Get {
1259        /// 0-based index in net list, or CDP requestId string
1260        id: String,
1261        #[arg(long, value_hint = ValueHint::FilePath)]
1262        request_path: Option<std::path::PathBuf>,
1263        #[arg(long, value_hint = ValueHint::FilePath)]
1264        response_path: Option<std::path::PathBuf>,
1265    },
1266}
1267
1268#[derive(Debug, Clone, Subcommand)]
1269pub enum DialogAction {
1270    Accept {
1271        #[arg(long)]
1272        text: Option<String>,
1273        /// Soft-ok when no dialog is showing (GAP-006)
1274        #[arg(long, action = ArgAction::SetTrue)]
1275        if_present: bool,
1276    },
1277    Dismiss {
1278        /// Soft-ok when no dialog is showing (GAP-006)
1279        #[arg(long, action = ArgAction::SetTrue)]
1280        if_present: bool,
1281    },
1282}
1283
1284#[derive(Debug, Clone, Subcommand)]
1285pub enum MitmAction {
1286    /// CA paths, capture count, bind policy
1287    Status,
1288    /// List captured exchanges
1289    List {
1290        #[arg(long)]
1291        host: Option<String>,
1292        #[arg(long, default_value_t = 100)]
1293        limit: usize,
1294    },
1295    /// Get one exchange by id
1296    Get { id: u64 },
1297    /// Export HAR 1.2 JSON
1298    Har {
1299        #[arg(long, value_hint = ValueHint::FilePath)]
1300        out: std::path::PathBuf,
1301    },
1302    /// Export capture as JSON/NDJSON
1303    Export {
1304        #[arg(long, default_value = "json")]
1305        format: String,
1306        #[arg(long, value_hint = ValueHint::FilePath)]
1307        out: std::path::PathBuf,
1308    },
1309    /// Unique hosts seen
1310    Domains,
1311    /// REST/GraphQL endpoint discovery
1312    Apis {
1313        #[arg(long)]
1314        kind: Option<String>,
1315    },
1316    /// Ensure local CA under XDG data
1317    InitCa,
1318    /// Start one-shot MITM proxy on 127.0.0.1 (ephemeral port); captures until timeout
1319    Start {
1320        /// Seconds to keep the proxy alive (one-shot; default 30)
1321        #[arg(long, default_value_t = 30)]
1322        seconds: u64,
1323    },
1324    /// One-shot: proxy + Chrome + navigate URL + capture (GAP-011 / GAP-019)
1325    CaptureUrl {
1326        /// Target URL to open through the MITM proxy
1327        #[arg(value_hint = ValueHint::Url)]
1328        url: String,
1329        /// Max seconds for the whole one-shot (default 30)
1330        #[arg(long, default_value_t = 30)]
1331        seconds: u64,
1332        /// Optional HAR output path
1333        #[arg(long, value_hint = ValueHint::FilePath)]
1334        har: Option<std::path::PathBuf>,
1335        /// Optional host allowlist for TLS intercept
1336        #[arg(long)]
1337        hosts: Option<String>,
1338    },
1339    /// GraphQL operations discovered in capture
1340    Graphql {
1341        #[arg(long, default_value_t = 100)]
1342        limit: usize,
1343    },
1344    /// WebSocket frames from capture
1345    Ws {
1346        #[command(subcommand)]
1347        action: MitmWsAction,
1348    },
1349    /// Short-circuit block host/path (persists for next start/capture-url in same process config note)
1350    Block {
1351        #[arg(long)]
1352        host: Option<String>,
1353        #[arg(long)]
1354        path: Option<String>,
1355    },
1356    /// Allowlist host for TLS intercept
1357    Allow {
1358        #[arg(long)]
1359        host: String,
1360    },
1361    /// Show or set redact-secrets policy for exports
1362    Redact {
1363        /// When true, redact Authorization/Cookie (default true)
1364        #[arg(long, default_value_t = true)]
1365        secrets: bool,
1366    },
1367}
1368
1369#[derive(Debug, Clone, Subcommand)]
1370pub enum MitmWsAction {
1371    /// List captured WebSocket frames
1372    List {
1373        #[arg(long, default_value_t = 100)]
1374        limit: usize,
1375    },
1376    /// Get one frame by id
1377    Get { id: u64 },
1378}
1379
1380#[derive(Debug, Clone, Subcommand)]
1381pub enum WorkflowAction {
1382    /// Validate DAG and execute offline steps; journal under XDG state
1383    Run {
1384        #[arg(long, value_hint = ValueHint::FilePath)]
1385        manifest: std::path::PathBuf,
1386        #[arg(long, value_hint = ValueHint::FilePath)]
1387        journal: Option<std::path::PathBuf>,
1388    },
1389    /// Resume / re-run from journal + manifest
1390    Resume {
1391        #[arg(long, value_hint = ValueHint::FilePath)]
1392        manifest: std::path::PathBuf,
1393        #[arg(long, value_hint = ValueHint::FilePath)]
1394        journal: Option<std::path::PathBuf>,
1395    },
1396    /// Show journal step statuses
1397    Status {
1398        #[arg(long, value_hint = ValueHint::FilePath)]
1399        journal: Option<std::path::PathBuf>,
1400        #[arg(long)]
1401        name: Option<String>,
1402    },
1403}
1404
1405#[derive(Debug, Clone, Subcommand)]
1406pub enum ConfigAction {
1407    /// Print resolved XDG paths
1408    Path,
1409    /// Create XDG layout + default config.toml
1410    Init,
1411    /// Show config values
1412    Show,
1413    /// Set a config key (lang|timeout|artifacts_dir|ignore_robots|namespace|encryption_key|color|log_level|log_to_file|chrome_path|lighthouse_path|openrouter_api_key|llm_base_url|llm_model|cache_backend|cache_redis_url)
1414    Set { key: String, value: String },
1415    /// Get one config key
1416    Get { key: Option<String> },
1417    /// List supported config keys and defaults (GAP-018)
1418    ListKeys,
1419}