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
- 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.
156 lines
5.4 KiB
Python
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
|