Skip to main content

browser_automation_cli/commands_prd/
meta.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Machine-readable command list and JSON Schema fragments for agents.
3
4use serde_json::{json, Value};
5
6use crate::envelope::print_success_json;
7use crate::error::{CliError, ErrorKind};
8
9/// Full CLI command surface registered for agents (`commands --json`).
10pub const COMMANDS: &[&str] = &[
11    "doctor",
12    "commands",
13    "schema",
14    "version",
15    "locale",
16    "goto",
17    "view",
18    "press",
19    "click-at",
20    "write",
21    "keys",
22    "type",
23    "wait",
24    "hover",
25    "drag",
26    "fill-form",
27    "select-option",
28    "pick",
29    "upload",
30    "back",
31    "forward",
32    "reload",
33    "eval",
34    "grab",
35    "print-pdf",
36    "monitor",
37    "run",
38    "exec",
39    "extract",
40    "text",
41    "scroll",
42    "cookie",
43    "attr",
44    "assert",
45    "console",
46    "net",
47    "page",
48    "dialog",
49    "scrape",
50    "batch-scrape",
51    "crawl",
52    "map",
53    "search",
54    "parse",
55    "qr",
56    "find-paths",
57    "sg-scan",
58    "sg-rewrite",
59    "sheet-write",
60    "mitm",
61    "workflow",
62    "config",
63    "emulate",
64    "resize",
65    "perf",
66    "lighthouse",
67    "screencast",
68    "heap",
69    "extension",
70    "devtools3p",
71    "webmcp",
72    "completions",
73    "man",
74];
75
76/// Default-ON DevTools parity commands that MUST appear in `COMMANDS`.
77pub const PARITY_DEFAULT_ON_REQUIRED: &[&str] = &[
78    "goto",
79    "view",
80    "press",
81    "write",
82    "keys",
83    "type",
84    "wait",
85    "hover",
86    "drag",
87    "fill-form",
88    "select-option",
89    "pick",
90    "upload",
91    "back",
92    "forward",
93    "reload",
94    "eval",
95    "grab",
96    "console",
97    "net",
98    "page",
99    "dialog",
100    "emulate",
101    "resize",
102    "perf",
103    "lighthouse",
104    "run",
105    "exec",
106    "doctor",
107    "commands",
108    "schema",
109    "version",
110    "completions",
111    "man",
112];
113
114/// Official DevTools tool-ref name → CLI subcommand (agent discovery).
115pub const DEVTOOLS_TOOL_MAP: &[(&str, &str)] = &[
116    ("click", "press"),
117    ("drag", "drag"),
118    ("fill", "write"),
119    ("fill_form", "fill-form"),
120    ("handle_dialog", "dialog"),
121    ("hover", "hover"),
122    ("press_key", "keys"),
123    ("type_text", "type"),
124    ("upload_file", "upload"),
125    ("click_at", "click-at"),
126    ("navigate_page", "goto|back|forward|reload"),
127    ("new_page", "page new"),
128    ("list_pages", "page list"),
129    ("select_page", "page select"),
130    ("close_page", "page close"),
131    ("get_tab_id", "page tab-id"),
132    ("wait_for", "wait"),
133    ("emulate", "emulate"),
134    ("resize_page", "resize"),
135    ("performance_start_trace", "perf start"),
136    ("performance_stop_trace", "perf stop"),
137    ("performance_analyze_insight", "perf insight"),
138    ("list_network_requests", "net list"),
139    ("get_network_request", "net get"),
140    ("evaluate_script", "eval"),
141    ("list_console_messages", "console list"),
142    ("get_console_message", "console get"),
143    ("take_screenshot", "grab"),
144    ("take_snapshot", "view"),
145    ("lighthouse_audit", "lighthouse"),
146    ("screencast_start", "screencast start"),
147    ("screencast_stop", "screencast stop"),
148    ("take_heapsnapshot", "heap take"),
149    ("close_heapsnapshot", "heap close"),
150    ("compare_heapsnapshots", "heap compare"),
151    ("get_heapsnapshot_summary", "heap summary"),
152    ("get_heapsnapshot_details", "heap details"),
153    ("get_heapsnapshot_class_nodes", "heap class-nodes"),
154    ("get_heapsnapshot_dominators", "heap dominators"),
155    ("get_heapsnapshot_duplicate_strings", "heap dup-strings"),
156    ("get_heapsnapshot_edges", "heap edges"),
157    ("get_heapsnapshot_retainers", "heap retainers"),
158    ("get_heapsnapshot_retaining_paths", "heap paths"),
159    ("get_heapsnapshot_object_details", "heap object-details"),
160    ("install_extension", "extension install"),
161    ("list_extensions", "extension list"),
162    ("reload_extension", "extension reload"),
163    ("trigger_extension_action", "extension trigger"),
164    ("uninstall_extension", "extension uninstall"),
165    ("list_3p_developer_tools", "devtools3p list"),
166    ("execute_3p_developer_tool", "devtools3p exec"),
167    ("list_webmcp_tools", "webmcp list"),
168    ("execute_webmcp_tool", "webmcp exec"),
169];
170
171pub fn list_commands(json: bool) -> Result<(), CliError> {
172    let map: Vec<Value> = DEVTOOLS_TOOL_MAP
173        .iter()
174        .map(|(tool, cli)| json!({ "tool": tool, "cli": cli }))
175        .collect();
176    let data = json!({
177        "commands": COMMANDS,
178        "schema_version": 1,
179        "parity_default_on": PARITY_DEFAULT_ON_REQUIRED,
180        "devtools_tool_map": map,
181        "binary": "browser-automation-cli",
182    });
183    if json {
184        print_success_json(data)?;
185    } else {
186        for c in COMMANDS {
187            crate::output::writeln_stdout(*c)?;
188        }
189        crate::output::flush_stdout()?;
190    }
191    Ok(())
192}
193
194fn schema_object(description: &str, properties: Value, required: &[&str]) -> Value {
195    json!({
196        "type": "object",
197        "description": description,
198        "properties": properties,
199        "required": required,
200        "additionalProperties": false,
201    })
202}
203
204fn schema_for(cmd: &str) -> Option<Value> {
205    let props = match cmd {
206        "doctor" => schema_object(
207            "Diagnose local Chrome install and one-shot readiness",
208            json!({
209                "offline": { "type": "boolean", "description": "Skip network probes" },
210                "quick": { "type": "boolean", "description": "Skip live launch test" },
211                "fix": { "type": "boolean", "description": "Apply safe repairs when possible" },
212                "json": {
213                    "type": "boolean",
214                    "description": "Global envelope flag --json (not a local doctor flag)"
215                }
216            }),
217            &[],
218        ),
219        "commands" => schema_object(
220            "List available commands",
221            json!({
222                "json": {
223                    "type": "boolean",
224                    "description": "Global envelope flag --json (not a local commands flag)"
225                }
226            }),
227            &[],
228        ),
229        "schema" => schema_object(
230            "JSON Schema fragment for one command",
231            json!({
232                "cmd": { "type": "string", "description": "Command name from `commands`" }
233            }),
234            &["cmd"],
235        ),
236        "version" => schema_object("Print CLI version (JSON when --json)", json!({}), &[]),
237        "locale" => schema_object(
238            "Show resolved UI locale diagnostics (suggestions only; JSON machine keys English)",
239            json!({}),
240            &[],
241        ),
242        "goto" => schema_object(
243            "Navigate to URL and wait for load (one-shot)",
244            json!({
245                "url": { "type": "string", "description": "Absolute URL or about:blank" },
246                "init_script": { "type": "string", "description": "JS to evaluate before navigation (tool-ref initScript)" },
247                "handle_before_unload": {
248                    "type": "string",
249                    "enum": ["accept", "dismiss"],
250                    "description": "Auto-handle beforeunload via CDP: accept | dismiss (GAP-003; CLI flag alone defaults to accept; never injects preventDefault)"
251                },
252                "navigation_timeout_ms": { "type": "integer", "description": "Navigation timeout override in milliseconds" }
253            }),
254            &["url"],
255        ),
256        "view" => schema_object(
257            "Accessibility snapshot with @eN refs",
258            json!({
259                "verbose": {
260                    "type": "boolean",
261                    "description": "Full a11y tree (run/JSON tool-ref). CLI flag is --detailed (avoids shadowing global --verbose)."
262                },
263                "detailed": {
264                    "type": "boolean",
265                    "description": "CLI alias of verbose for one-shot argv (maps to verbose in handlers)"
266                },
267                "path": { "type": "string", "description": "Optional file to write tree text" }
268            }),
269            &[],
270        ),
271        "press" => schema_object(
272            "Click element by CSS selector or @eN",
273            json!({
274                "target": { "type": "string" },
275                "dblclick": { "type": "boolean" },
276                "include_snapshot": { "type": "boolean" }
277            }),
278            &["target"],
279        ),
280        "click-at" => schema_object(
281            "Click at page CSS coordinates (requires --experimental-vision)",
282            json!({
283                "x": { "type": "number" },
284                "y": { "type": "number" },
285                "dblclick": { "type": "boolean" },
286                "include_snapshot": { "type": "boolean" }
287            }),
288            &["x", "y"],
289        ),
290        "write" => schema_object(
291            "Smart fill: text, select option, checkbox/radio true|false",
292            json!({
293                "target": { "type": "string" },
294                "value": { "type": "string" },
295                "include_snapshot": { "type": "boolean" }
296            }),
297            &["target", "value"],
298        ),
299        "keys" => schema_object(
300            "Press a CDP key name",
301            json!({ "key": { "type": "string" } }),
302            &["key"],
303        ),
304        "type" => schema_object(
305            "Type text into a target",
306            json!({
307                "target": { "type": "string" },
308                "text": { "type": "string" },
309                "clear": { "type": "boolean" },
310                "submit": { "type": "string", "description": "Optional key after type (e.g. Enter)" },
311                "focus_only": { "type": "boolean", "description": "Focus target without typing" },
312                "include_snapshot": { "type": "boolean" }
313            }),
314            &["text"],
315        ),
316        "wait" => schema_object(
317            "Wait ms and/or text and/or CSS selector (comma OR / array) and/or URL and/or load state (GAP-019/024)",
318            json!({
319                "ms": { "type": "integer", "minimum": 0 },
320                "text": {
321                    "oneOf": [
322                        { "type": "string" },
323                        { "type": "array", "items": { "type": "string" } }
324                    ],
325                    "description": "Repeatable --text values; any match wins (OR)"
326                },
327                "selector": {
328                    "oneOf": [
329                        { "type": "string", "description": "CSS selector; comma-separated OR supported (GAP-019)" },
330                        { "type": "array", "items": { "type": "string" } }
331                    ]
332                },
333                "selectors": { "type": "array", "items": { "type": "string" }, "description": "OR list of CSS selectors" },
334                "url": { "type": "string", "description": "Exact location.href match (GAP-024)" },
335                "url_contains": { "type": "string", "description": "Substring match on location.href (GAP-024)" },
336                "navigation": { "type": "boolean", "description": "Wait for load lifecycle (GAP-024)" },
337                "state": {
338                    "type": "string",
339                    "enum": ["load", "domcontentloaded", "networkidle", "none"]
340                },
341                "wait_timeout_ms": { "type": "integer", "minimum": 0 },
342                "include_snapshot": { "type": "boolean" }
343            }),
344            &[],
345        ),
346        "hover" => schema_object(
347            "Hover element by CSS selector or @eN",
348            json!({ "target": { "type": "string" } }),
349            &["target"],
350        ),
351        "drag" => schema_object(
352            "Drag from one target to another",
353            json!({
354                "from": { "type": "string" },
355                "to": { "type": "string" }
356            }),
357            &["from", "to"],
358        ),
359        "fill-form" => schema_object(
360            "Fill multiple fields from JSON array",
361            json!({
362                "json": {
363                    "type": "string",
364                    "description": "JSON array of {target,value} objects (run/JSON key). CLI flag: --fields-json"
365                },
366                "fields": {
367                    "type": "array",
368                    "description": "Preferred run-script form: array of {target|uid,value}"
369                },
370                "fields_json": {
371                    "type": "string",
372                    "description": "CLI long name --fields-json (avoids shadowing global --json)"
373                }
374            }),
375            &[],
376        ),
377        "select-option" | "select_option" | "pick" => schema_object(
378            "Pick option from custom select / badge popover / role=option (GAP-023)",
379            json!({
380                "target": { "type": "string", "description": "Trigger control (badge/button)" },
381                "option": { "type": "string", "description": "Option text, CSS selector, or role label" },
382                "value": { "type": "string" },
383                "include_snapshot": { "type": "boolean" }
384            }),
385            &["target", "option"],
386        ),
387        "upload" => schema_object(
388            "Upload a regular file to a file input",
389            json!({
390                "target": { "type": "string" },
391                "path": { "type": "string" }
392            }),
393            &["target", "path"],
394        ),
395        "back" => schema_object("History back", json!({}), &[]),
396        "forward" => schema_object("History forward", json!({}), &[]),
397        "reload" => schema_object(
398            "Reload page",
399            json!({ "ignore_cache": { "type": "boolean" } }),
400            &[],
401        ),
402        "eval" => schema_object(
403            "Evaluate JavaScript expression or function declaration",
404            json!({
405                "expression": { "type": "string" },
406                "args": { "type": "string", "description": "JSON array of function args" },
407                "dialog_action": { "type": "string", "description": "accept|dismiss during evaluate" },
408                "file_path": { "type": "string", "description": "Optional path to write result" }
409            }),
410            &["expression"],
411        ),
412        "grab" => schema_object(
413            "Screenshot (png/jpeg/webp)",
414            json!({
415                "path": { "type": "string" },
416                "format": { "type": "string", "enum": ["png", "jpeg", "webp"] },
417                "full_page": { "type": "boolean" },
418                "quality": { "type": "integer" },
419                "element": { "type": "string", "description": "CSS selector or @eN" }
420            }),
421            &[],
422        ),
423        "print-pdf" => schema_object(
424            "Print current page to PDF via CDP Page.printToPDF (one-shot)",
425            json!({
426                "path": { "type": "string", "description": "Output PDF path" },
427                "url": { "type": "string", "description": "Optional URL to navigate before print" }
428            }),
429            &[],
430        ),
431        "monitor" => schema_object(
432            "One-shot change check against a baseline file (hash/text)",
433            json!({
434                "action": { "type": "string", "enum": ["check"] },
435                "url": { "type": "string" },
436                "baseline": { "type": "string", "description": "Baseline file path" },
437                "write_baseline": { "type": "boolean" },
438                "engine": { "type": "string", "enum": ["http", "browser"] }
439            }),
440            &["action", "url", "baseline"],
441        ),
442        "run" => schema_object(
443            "Execute multi-step script in one process; script file is NDJSON (one object per line) or a top-level JSON array of step objects",
444            json!({ "script": { "type": "string", "description": "Path to script file (.jsonl NDJSON or .json array of steps)" } }),
445            &["script"],
446        ),
447        "exec" => schema_object(
448            "Single-step inline command (same surface as run steps)",
449            json!({
450                "args": {
451                    "type": "array",
452                    "items": { "type": "string" },
453                    "description": "e.g. [\"goto\", \"about:blank\"] or [\"wait\", \"--ms\", \"100\"]"
454                }
455            }),
456            &["args"],
457        ),
458        "extract" => schema_object(
459            "Extract text or attribute from target, or LLM extract with --llm",
460            json!({
461                "target": { "type": "string", "description": "CSS/@eN, http(s) URL, or file path for --llm" },
462                "attr": { "type": "string" },
463                "llm": { "type": "boolean" },
464                "question": { "type": "string" },
465                "schema_json": { "type": "string", "description": "Path to JSON schema file" }
466            }),
467            &["target"],
468        ),
469        "text" => schema_object(
470            "Extract visible text from target (PRD §7)",
471            json!({
472                "target": { "type": "string" }
473            }),
474            &["target"],
475        ),
476        "scroll" => schema_object(
477            "Scroll window or element by pixel deltas",
478            json!({
479                "target": { "type": "string", "description": "Optional CSS/@eN" },
480                "delta_x": { "type": "number" },
481                "delta_y": { "type": "number" },
482                "dx": { "type": "number", "description": "Alias for delta_x" },
483                "dy": { "type": "number", "description": "Alias for delta_y" }
484            }),
485            &[],
486        ),
487        "cookie" => schema_object(
488            "Cookie list/set/clear for the one-shot browser process",
489            json!({
490                "action": { "type": "string", "enum": ["list", "set", "clear"] },
491                "url": { "type": "string" },
492                "json": {
493                    "type": "string",
494                    "description": "JSON array for set (run/JSON key). CLI flag: --cookies-json"
495                },
496                "cookies": {
497                    "type": "array",
498                    "description": "Preferred run-script form for set"
499                },
500                "cookies_json": {
501                    "type": "string",
502                    "description": "CLI long name --cookies-json (avoids shadowing global --json)"
503                }
504            }),
505            &["action"],
506        ),
507        "attr" => schema_object(
508            "Read one attribute from target",
509            json!({
510                "target": { "type": "string" },
511                "name": { "type": "string" }
512            }),
513            &["target", "name"],
514        ),
515        "assert" => schema_object(
516            "Assertion helpers (url/text/console)",
517            json!({
518                "kind": { "type": "string", "enum": ["url", "text", "console", "console_empty", "console_no_match"] },
519                "pattern": { "type": "string", "description": "For console_no_match (GAP-025)" },
520                "value": { "type": "string" },
521                "url": { "type": "string" },
522                "url_contains": { "type": "string" },
523                "text": { "type": "string" },
524                "text_contains": { "type": "string" },
525                "contains": { "type": "boolean" },
526                "target": { "type": "string" },
527                "level": { "type": "string" },
528                "max": { "type": "integer" }
529            }),
530            &[],
531        ),
532        "console" => schema_object(
533            "List/get/clear/dump captured console messages (needs --capture-console)",
534            json!({
535                "action": { "type": "string", "enum": ["list", "get", "clear", "dump"] },
536                "id": { "type": "integer", "minimum": 0 },
537                "path": { "type": "string" }
538            }),
539            &["action"],
540        ),
541        "net" => schema_object(
542            "List or get captured network requests (needs --capture-network)",
543            json!({
544                "action": { "type": "string", "enum": ["list", "get"] },
545                "id": { "type": "integer", "minimum": 0 },
546                "request_path": { "type": "string" },
547                "response_path": { "type": "string" }
548            }),
549            &["action"],
550        ),
551        "page" => schema_object(
552            "Page info or multi-tab list|new|select|close|tab-id",
553            json!({
554                "action": {
555                    "type": "string",
556                    "enum": ["info", "list", "new", "select", "close", "tab-id"]
557                },
558                "url": { "type": "string" },
559                "index": { "type": "integer", "minimum": 0 },
560                "background": { "type": "boolean", "description": "Open new tab without focusing (page new)" },
561                "isolated_context": {
562                    "type": "string",
563                    "description": "Named isolated browser context for page new (tool-ref isolatedContext; GAP-004; flag alone = default-isolated)"
564                },
565                "page_id": { "type": "integer", "minimum": 0, "description": "Tool-ref pageId alias for index (select/close)" },
566                "bring_to_front": { "type": "boolean", "description": "Bring selected tab to front (page select; default true)" }
567            }),
568            &[],
569        ),
570        "dialog" => schema_object(
571            "Accept or dismiss open dialog",
572            json!({
573                "action": { "type": "string", "enum": ["accept", "dismiss"] },
574                "text": { "type": "string", "description": "Optional prompt response text (accept only)" },
575                "if_present": {
576                    "type": "boolean",
577                    "description": "Soft-ok when no dialog is showing (GAP-006); envelope dialog_shown:false"
578                }
579            }),
580            &["action"],
581        ),
582        "scrape" => schema_object(
583            "Navigate and return body text / formats (local HTTP or CDP scrape)",
584            json!({
585                "url": { "type": "string" },
586                "format": {
587                    "oneOf": [
588                        {
589                            "type": "string",
590                            "enum": [
591                                "text", "markdown", "html", "raw-html", "links", "metadata",
592                                "screenshot", "summary", "product", "branding"
593                            ]
594                        },
595                        {
596                            "type": "array",
597                            "items": {
598                                "type": "string",
599                                "enum": [
600                                    "text", "markdown", "html", "raw-html", "links", "metadata",
601                                    "screenshot", "summary", "product", "branding"
602                                ]
603                            }
604                        }
605                    ],
606                    "description": "Single format, CSV multi-format, or array (GAP-009); browser applies via outerHTML"
607                },
608                "formats": {
609                    "description": "Alias of format for multi-value (GAP-018)",
610                    "oneOf": [
611                        { "type": "string" },
612                        { "type": "array", "items": { "type": "string" } }
613                    ]
614                },
615                "engine": {
616                    "type": "string",
617                    "enum": ["http", "browser"],
618                    "description": "Default browser (CDP)"
619                },
620                "only_main_content": { "type": "boolean" },
621                "webhook_url": {
622                    "type": "string",
623                    "description": "Optional one-shot operator POST of result data (not product telemetry)"
624                }
625            }),
626            &["url"],
627        ),
628        "batch-scrape" => schema_object(
629            "Scrape many URLs from a file (HTTP or browser engine, one-shot)",
630            json!({
631                "urls_file": { "type": "string", "description": "Path to file with one URL per line" },
632                "format": {
633                    "type": "string",
634                    "enum": ["text", "markdown", "html", "links", "metadata", "raw-html", "screenshot", "summary", "product", "branding"],
635                    "description": "Single format or CSV multi-format when supported"
636                },
637                "engine": {
638                    "type": "string",
639                    "enum": ["http", "browser"],
640                    "description": "Default http; browser uses CDP per URL (GAP-010)"
641                },
642                "concurrency": { "type": "integer", "minimum": 1 }
643            }),
644            &["urls_file"],
645        ),
646        "crawl" => schema_object(
647            "Crawl from a seed URL (HTTP BFS or browser, one-shot)",
648            json!({
649                "url": { "type": "string" },
650                "limit": { "type": "integer", "minimum": 1 },
651                "max_pages": { "type": "integer", "minimum": 1, "description": "Alias of limit" },
652                "max_depth": { "type": "integer", "minimum": 0 },
653                "format": { "type": "string" },
654                "same_host": { "type": "boolean" },
655                "engine": {
656                    "type": "string",
657                    "enum": ["http", "browser"],
658                    "description": "Default http; browser engine for JS-rendered crawl (GAP-010)"
659                }
660            }),
661            &["url"],
662        ),
663        "map" => schema_object(
664            "Map site URLs from a seed (HTTP)",
665            json!({
666                "url": { "type": "string" },
667                "limit": { "type": "integer", "minimum": 1 },
668                "max_depth": { "type": "integer", "minimum": 0 }
669            }),
670            &["url"],
671        ),
672        "search" => schema_object(
673            "Local search (HTTP SERP links or URL map)",
674            json!({
675                "query": { "type": "string" },
676                "limit": { "type": "integer", "minimum": 1 }
677            }),
678            &["query"],
679        ),
680        "parse" => schema_object(
681            "Parse a local file (html/md/txt/pdf/docx/xlsx)",
682            json!({
683                "path": { "type": "string" },
684                "redact_pii": { "type": "boolean" }
685            }),
686            &["path"],
687        ),
688        "qr" => schema_object(
689            "QR encode/decode one-shot (no Chrome)",
690            json!({
691                "action": { "type": "string", "enum": ["encode", "decode"] },
692                "text": { "type": "string" },
693                "format": { "type": "string", "enum": ["png", "svg", "terminal"] },
694                "path": { "type": "string" }
695            }),
696            &["action"],
697        ),
698        "find-paths" => schema_object(
699            "Discover filesystem paths (fd-like; no Chrome)",
700            json!({
701                "pattern": { "type": "string" },
702                "paths": { "type": "array", "items": { "type": "string" } },
703                "extension": { "type": "string" },
704                "hidden": { "type": "boolean" },
705                "no_ignore": { "type": "boolean" },
706                "max_depth": { "type": "integer" },
707                "type": { "type": "string", "enum": ["f", "d"] },
708                "limit": { "type": "integer" },
709                "glob": { "type": "string", "description": "Shell-style glob e.g. **/*.rs" }
710            }),
711            &[],
712        ),
713        "sg-scan" => schema_object(
714            "Structural lint scan for forbidden product patterns (one-shot; no Chrome)",
715            json!({
716                "paths": { "type": "array", "items": { "type": "string" } },
717                "limit": { "type": "integer" }
718            }),
719            &[],
720        ),
721        "sg-rewrite" => schema_object(
722            "Structural rewrite dry-run/apply for safe patterns only (one-shot; no Chrome)",
723            json!({
724                "paths": { "type": "array", "items": { "type": "string" } },
725                "apply": { "type": "boolean" }
726            }),
727            &[],
728        ),
729        "sheet-write" => schema_object(
730            "Write XLSX from CSV/JSON (write-only; no Chrome)",
731            json!({
732                "input": { "type": "string" },
733                "out": { "type": "string" },
734                "sheet": { "type": "string" }
735            }),
736            &["input", "out"],
737        ),
738        "mitm" => schema_object(
739            "MITM capture / CA / HAR (one-shot local 127.0.0.1)",
740            json!({
741                "action": {
742                    "type": "string",
743                    "enum": [
744                        "status", "list", "get", "har", "export",
745                        "domains", "apis", "init-ca", "start"
746                    ]
747                },
748                "id": { "type": "string" },
749                "out": { "type": "string" },
750                "seconds": { "type": "integer", "minimum": 1 },
751                "limit": { "type": "integer", "minimum": 1 }
752            }),
753            &["action"],
754        ),
755        "workflow" => schema_object(
756            "Workflow journal DAG (petgraph + SQLite under XDG state)",
757            json!({
758                "action": { "type": "string", "enum": ["run", "resume", "status"] },
759                "manifest": { "type": "string", "description": "JSON workflow manifest path" },
760                "journal": { "type": "string" },
761                "name": { "type": "string" }
762            }),
763            &["action"],
764        ),
765        "config" => schema_object(
766            "XDG config and path management (no product env at runtime)",
767            json!({
768                "action": {
769                    "type": "string",
770                    "enum": ["path", "init", "show", "set", "get", "list-keys"]
771                },
772                "key": {
773                    "type": "string",
774                    "description": "For set/get: 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"
775                },
776                "value": { "type": "string" }
777            }),
778            &["action"],
779        ),
780        "emulate" => schema_object(
781            "Emulate UA locale timezone network geo media CPU viewport headers",
782            json!({
783                "user_agent": { "type": "string" },
784                "locale": { "type": "string" },
785                "timezone": { "type": "string" },
786                "offline": { "type": "boolean" },
787                "latitude": { "type": "number" },
788                "longitude": { "type": "number" },
789                "media": { "type": "string" },
790                "network_conditions": { "type": "string" },
791                "cpu_throttling_rate": { "type": "number" },
792                "color_scheme": { "type": "string" },
793                "extra_headers": { "type": "string" },
794                "viewport": { "type": "string" }
795            }),
796            &[],
797        ),
798        "resize" => schema_object(
799            "Resize viewport",
800            json!({
801                "width": { "type": "integer" },
802                "height": { "type": "integer" },
803                "scale": { "type": "number" },
804                "mobile": { "type": "boolean" }
805            }),
806            &["width", "height"],
807        ),
808        "perf" => schema_object(
809            "Performance start|stop|insight",
810            json!({
811                "action": { "type": "string", "enum": ["start", "stop", "insight"] },
812                "path": { "type": "string" },
813                "reload": { "type": "boolean" },
814                "name": { "type": "string" }
815            }),
816            &["action"],
817        ),
818        "lighthouse" => schema_object(
819            "External lighthouse audit with JSON scores",
820            json!({
821                "url": { "type": "string" },
822                "out_dir": { "type": "string" },
823                "device": { "type": "string" },
824                "mode": { "type": "string" },
825                "lighthouse_path": { "type": "string" }
826            }),
827            &["url"],
828        ),
829        "screencast" => schema_object(
830            "Screencast start|stop (requires --experimental-screencast)",
831            json!({
832                "action": { "type": "string", "enum": ["start", "stop"] },
833                "path": { "type": "string" }
834            }),
835            &["action"],
836        ),
837        "heap" => schema_object(
838            "Heap snapshot tools (deep ops need --category-memory)",
839            json!({
840                "action": { "type": "string" },
841                "path": { "type": "string" },
842                "base": { "type": "string" },
843                "current": { "type": "string" },
844                "id": { "type": "integer" },
845                "node": { "type": "integer" }
846            }),
847            &["action"],
848        ),
849        "extension" => schema_object(
850            "Extension tools (requires --category-extensions)",
851            json!({
852                "action": { "type": "string" },
853                "path": { "type": "string" },
854                "id": { "type": "string" }
855            }),
856            &["action"],
857        ),
858        "devtools3p" => schema_object(
859            "Third-party tools surface (requires --category-third-party)",
860            json!({
861                "action": { "type": "string", "enum": ["list", "exec"] },
862                "name": { "type": "string" },
863                "params": { "type": "string" },
864                "url": { "type": "string" }
865            }),
866            &["action"],
867        ),
868        "webmcp" => schema_object(
869            "Web surface tools (requires --category-webmcp)",
870            json!({
871                "action": { "type": "string", "enum": ["list", "exec"] },
872                "name": { "type": "string" },
873                "input": { "type": "string" },
874                "url": { "type": "string" }
875            }),
876            &["action"],
877        ),
878        "completions" => schema_object(
879            "Generate shell completions (no Chrome)",
880            json!({
881                "shell": {
882                    "type": "string",
883                    "enum": ["bash", "zsh", "fish", "elvish", "powershell"]
884                }
885            }),
886            &["shell"],
887        ),
888        "man" => schema_object(
889            "Generate man page (roff) via clap_mangen (no Chrome)",
890            json!({
891                "out": {
892                    "type": "string",
893                    "description": "Optional output path; default stdout"
894                }
895            }),
896            &[],
897        ),
898        _ => return None,
899    };
900    Some(props)
901}
902
903pub fn schema_for_cmd(cmd: &str, json: bool) -> Result<(), CliError> {
904    if !COMMANDS.contains(&cmd) {
905        return Err(CliError::with_suggestion(
906            ErrorKind::Usage,
907            format!("unknown command for schema: {cmd}"),
908            "use `browser-automation-cli commands --json` to list commands",
909        ));
910    }
911    let fragment = schema_for(cmd)
912        .unwrap_or_else(|| schema_object(&format!("Schema fragment for `{cmd}`"), json!({}), &[]));
913    let data = json!({
914        "command": cmd,
915        "schema_version": 1,
916        "schema": fragment,
917        "type": fragment.get("type").cloned().unwrap_or(json!("object")),
918        "description": fragment.get("description").cloned().unwrap_or(json!("")),
919        "properties": fragment.get("properties").cloned().unwrap_or(json!({})),
920        "required": fragment.get("required").cloned().unwrap_or(json!([])),
921    });
922    if json {
923        print_success_json(data)?;
924    } else {
925        let pretty = serde_json::to_string_pretty(&data).unwrap_or_default();
926        crate::output::writeln_stdout(pretty)?;
927    }
928    Ok(())
929}
930
931#[cfg(test)]
932mod tests {
933    use super::*;
934
935    #[test]
936    fn parity_default_on_subset_of_commands() {
937        for req in PARITY_DEFAULT_ON_REQUIRED {
938            assert!(
939                COMMANDS.contains(req),
940                "parity command missing from COMMANDS: {req}"
941            );
942        }
943    }
944
945    #[test]
946    fn commands_unique() {
947        let mut seen = std::collections::BTreeSet::new();
948        for c in COMMANDS {
949            assert!(seen.insert(*c), "duplicate command: {c}");
950        }
951    }
952
953    #[test]
954    fn config_schema_includes_list_keys_and_cache_keys() {
955        let frag = schema_for("config").expect("config schema");
956        let action_enum = frag
957            .pointer("/properties/action/enum")
958            .and_then(|v| v.as_array())
959            .expect("action.enum");
960        let actions: Vec<&str> = action_enum.iter().filter_map(|v| v.as_str()).collect();
961        assert!(
962            actions.contains(&"list-keys"),
963            "config action enum must include list-keys: {actions:?}"
964        );
965        for required in ["path", "init", "show", "set", "get", "list-keys"] {
966            assert!(
967                actions.contains(&required),
968                "missing config action {required} in {actions:?}"
969            );
970        }
971        let key_desc = frag
972            .pointer("/properties/key/description")
973            .and_then(|v| v.as_str())
974            .unwrap_or("");
975        for key in [
976            "lang",
977            "timeout",
978            "artifacts_dir",
979            "ignore_robots",
980            "namespace",
981            "encryption_key",
982            "color",
983            "log_level",
984            "log_to_file",
985            "chrome_path",
986            "lighthouse_path",
987            "openrouter_api_key",
988            "llm_base_url",
989            "llm_model",
990            "cache_backend",
991            "cache_redis_url",
992        ] {
993            assert!(
994                key_desc.contains(key),
995                "config key description missing {key}: {key_desc}"
996            );
997        }
998    }
999
1000    #[test]
1001    fn run_schema_documents_ndjson_and_array() {
1002        let frag = schema_for("run").expect("run schema");
1003        let desc = frag
1004            .get("description")
1005            .and_then(|v| v.as_str())
1006            .unwrap_or("");
1007        let script_desc = frag
1008            .pointer("/properties/script/description")
1009            .and_then(|v| v.as_str())
1010            .unwrap_or("");
1011        assert!(
1012            desc.to_ascii_lowercase().contains("array")
1013                || script_desc.to_ascii_lowercase().contains("array"),
1014            "run schema must document JSON array scripts: desc={desc} script={script_desc}"
1015        );
1016        assert!(
1017            desc.to_ascii_lowercase().contains("ndjson")
1018                || script_desc.to_ascii_lowercase().contains("ndjson")
1019                || script_desc.contains("jsonl"),
1020            "run schema must document NDJSON scripts: desc={desc} script={script_desc}"
1021        );
1022    }
1023}