Skip to main content

browser_automation_cli/
cli.rs

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