Skip to main content

browser_automation_cli/commands_prd/
meta.rs

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