from __future__ import annotations import importlib import json from pathlib import Path from typing import Any, cast import click from rich.console import Console from browser_cli.command_security import CommandPolicy, assert_command_allowed from browser_cli.commands import client_from_ctx, handle_errors console = Console() def _load_steps(path: Path): text = path.read_text(encoding="utf-8") if path.suffix.lower() in {".yaml", ".yml"}: try: yaml = cast(Any, importlib.import_module("yaml")) except Exception as exc: raise click.ClickException("YAML scripts require PyYAML; use JSON or install PyYAML") from exc return yaml.safe_load(text) return json.loads(text) def _parse_step(step): if isinstance(step, str): return step, {} if isinstance(step, dict): if "command" in step: return step["command"], step.get("args") or {} if len(step) == 1: command, args = next(iter(step.items())) return command, args or {} raise click.ClickException(f"Invalid script step: {step!r}") @click.command("script") @click.argument("file", type=click.Path(exists=True, dir_okay=False, path_type=Path)) @click.option("--json", "json_output", is_flag=True, help="Print all step results as JSON") @click.option("--continue-on-error", is_flag=True, help="Continue after failed steps") @click.option("--allow-read-page", is_flag=True, help="Allow page-content read commands such as extract.* and dom.text") @click.option("--allow-control", is_flag=True, help="Allow browser-control commands such as nav.*, tabs.close, dom.click") @click.option("--allow-dangerous", is_flag=True, help="Allow high-risk commands such as dom.eval, storage.*, screenshots") @handle_errors def cmd_script(file: Path, json_output: bool, continue_on_error: bool, allow_read_page: bool, allow_control: bool, allow_dangerous: bool): """Run a JSON/YAML batch script of browser-cli wire commands.""" steps = _load_steps(file) if not isinstance(steps, list): raise click.ClickException("Script root must be a list") client = client_from_ctx() policy = CommandPolicy(allow_read_page=allow_read_page, allow_control=allow_control, allow_dangerous=allow_dangerous) results = [] for index, step in enumerate(steps, start=1): command, args = _parse_step(step) try: assert_command_allowed(command, policy) result = client.command(command, args) results.append({"index": index, "command": command, "ok": True, "result": result}) if not json_output: console.print(f"[green]✓[/green] {index}: {command}") except Exception as exc: results.append({"index": index, "command": command, "ok": False, "error": str(exc)}) if not continue_on_error: if json_output: click.echo(json.dumps(results, indent=2, default=str)) raise if not json_output: console.print(f"[red]✗[/red] {index}: {command}: {exc}") if json_output: click.echo(json.dumps(results, indent=2, default=str))