Files
browser-cli/browser_cli/commands/__init__.py
T
daniel156161 6fa931aa36
Testing / remote-protocol-compat (0.9.5) (push) Successful in 56s
Testing / remote-protocol-compat (0.9.3) (push) Successful in 59s
Testing / test (push) Successful in 1m1s
Build & Publish Package / publish (push) Successful in 33s
Package Extension / package-extension (push) Successful in 36s
feat: harden remote serve and reuse connections
- Gate TCP serve commands with safe-by-default policies, per-key allow tokens, per-key rate limiting, and audit labels.
- Reuse authenticated encrypted remote sessions and parallelize/caches multi-browser fanout to reduce repeated handshake roundtrips.
- Increase paged native-host batch size with extension-side byte budgeting to speed large tab listings safely.
- Point install output at public Chrome Web Store / Firefox AMO listings by default, with --dev preserving unpacked workflows.
- Share search-engine metadata between CLI and SDK and bump the package/extension version to 0.16.0.
- Cover the new security, pooling, paging, install, and fanout behavior with expanded Python and extension tests.
2026-06-18 14:24:15 +02:00

156 lines
5.4 KiB
Python

"""Shared helpers for the Click CLI command modules.
Every CLI command is a thin presentation layer over the Python SDK: it builds a
:class:`~browser_cli.BrowserCLI` from the global ``--browser/--remote/--key``
options, calls the matching SDK namespace method, and renders the result. The
SDK is the single source of truth for command strings, argument shapes, and
multi-browser routing.
"""
import functools
import click
from rich.console import Console
from rich.table import Table
from browser_cli import BrowserCLI, BrowserCounts
from browser_cli.client import BrowserNotConnected
from browser_cli.constants import GENTLE_MODES
_console = Console()
# Reusable ``--tab`` option: select a tab by ID (default: the active tab).
tab_option = click.option("--tab", "tab_id", type=int, default=None, help="Tab ID (default: active tab)")
def gentle_mode_option(help_text: str):
"""Reusable ``--gentle-mode`` Click option (throttle mode for large operations)."""
return click.option(
"--gentle-mode",
type=click.Choice(GENTLE_MODES),
default="auto",
show_default=True,
help=help_text,
)
def command_policy_options(fn):
"""Reusable raw-command safety flags for /command-like entry points."""
fn = click.option(
"--allow-all",
is_flag=True,
help="Allow every command (equivalent to --allow-read-page --allow-control --allow-dangerous --allow-keys)",
)(fn)
fn = click.option(
"--allow-keys",
is_flag=True,
help="Allow key-management commands (list/trust authorized keys over --remote)",
)(fn)
fn = click.option(
"--allow-dangerous",
is_flag=True,
help="Allow high-risk commands such as dom.eval, storage.*, screenshots",
)(fn)
fn = click.option(
"--allow-control",
is_flag=True,
help="Allow browser-control commands such as nav.*, tabs.close, dom.click",
)(fn)
fn = click.option(
"--allow-read-page",
is_flag=True,
help="Allow page-content read commands such as extract.* and dom.text",
)(fn)
return fn
def command_policy_from_options(*, allow_read_page: bool, allow_control: bool, allow_dangerous: bool, allow_keys: bool = False, allow_all: bool = False):
"""Build a CommandPolicy from shared raw-command safety flags."""
from browser_cli.command_security import CommandPolicy
if allow_all:
return CommandPolicy.unrestricted()
return CommandPolicy(
allow_read_page=allow_read_page,
allow_control=allow_control,
allow_dangerous=allow_dangerous,
allow_keys=allow_keys,
)
def command_categories_from_options(*, allow_read_page: bool, allow_control: bool, allow_dangerous: bool, allow_keys: bool = False, allow_all: bool = False):
"""Convert the shared --allow-* flags into a category list, or None if none were set.
None means "no explicit policy" — the key falls back to the server-wide default.
"""
if allow_all:
return ["all"]
cats = []
if allow_read_page:
cats.append("read-page")
if allow_control:
cats.append("control")
if allow_dangerous:
cats.append("dangerous")
if allow_keys:
cats.append("keys")
return cats or None
def print_counts(result, noun: str, *, single_suffix: str = "") -> None:
"""Render a count result.
In multi-browser mode (*result* is a :class:`~browser_cli.BrowserCounts`) print a
per-browser table with a Total row; otherwise print a single ``N noun(s)`` line.
"""
if isinstance(result, BrowserCounts):
table = Table(show_header=True, header_style="bold cyan")
table.add_column("Browser", no_wrap=True)
table.add_column(f"{noun.capitalize()}s", justify="right")
rendered_groups: set[str] = set()
for name, count in result.by_browser.items():
group = result.browser_groups.get(name)
if group:
if group not in rendered_groups:
group_total = sum(
browser_count
for browser_name, browser_count in result.by_browser.items()
if result.browser_groups.get(browser_name) == group
)
table.add_row(f"[bold]{group}[/bold]", str(group_total))
rendered_groups.add(group)
display_name = name.removeprefix(f"{group}:")
table.add_row(f" {display_name}", str(count))
else:
table.add_row(name, str(count))
table.add_row("Total", str(result.total))
_console.print(table)
else:
_console.print(f"[bold]{result}[/bold] {noun}(s){single_suffix}")
def client_from_ctx() -> BrowserCLI:
"""Build a BrowserCLI from the root context's global options.
Reads ``browser``/``remote``/``key`` set by the top-level ``main`` group.
Falls back to an unconfigured client when a command group is invoked
standalone (e.g. in unit tests).
"""
obj = click.get_current_context().find_root().obj or {}
return BrowserCLI(browser=obj.get("browser"), remote=obj.get("remote"), key=obj.get("key"))
def handle_errors(fn):
"""Decorate a CLI command so SDK exceptions become clean errors + exit(1).
Apply as the innermost decorator (directly above ``def``) so Click's option
decorators attach their params to the wrapper.
"""
@functools.wraps(fn)
def wrapper(*args, **kwargs):
try:
return fn(*args, **kwargs)
except BrowserNotConnected as e:
_console.print(f"[red]Error:[/red] {e}")
raise SystemExit(1)
except PermissionError as e:
_console.print(f"[red]Blocked:[/red] {e}")
raise SystemExit(1)
except RuntimeError as e:
_console.print(f"[red]Browser error:[/red] {e}")
raise SystemExit(1)
return wrapper