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