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    "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
71/// Default-ON DevTools parity commands that MUST appear in `COMMANDS`.
72pub 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
106/// Official DevTools tool-ref name → CLI subcommand (agent discovery).
107pub 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}