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