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