1use serde_json::{json, Value};
4
5use crate::envelope::print_success_json;
6use crate::error::{CliError, ErrorKind};
7
8pub 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
73pub 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
110pub 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}