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