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