5cec57e06d
Testing / remote-protocol-compat (0.9.3) (push) Successful in 40s
Testing / remote-protocol-compat (0.9.5) (push) Successful in 38s
Testing / test (push) Failing after 1m3s
Package Extension / package-extension (push) Successful in 29s
Build & Publish Package / publish (push) Successful in 33s
- Add safe-by-default policy gates for raw command surfaces: command, script, and serve-http /command. - Require explicit opt-ins for page reads, browser control, and high-risk commands such as dom.eval, storage.*, and screenshots. - Remove all cookies support from CLI, SDK, extension commands, permissions, constants, docs, and tests. - Add diagnostic, events, watch, workspace, remote, raw command, script, HTTP gateway, tree-view, session import/export, and extension info/capability commands. - Add Chrome Web Store packaging that strips manifest.key while keeping local packages with a stable native-messaging extension ID. - Bump browser-cli and extension version to 0.14.1 and cover the new behavior with pytest and extension packaging tests. BREAKING CHANGE: cookies commands and the b.cookies SDK namespace have been removed; generic raw command execution now blocks non-safe commands unless explicitly allowed.
92 lines
3.1 KiB
Python
92 lines
3.1 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import click
|
|
from rich.console import Console
|
|
from rich.table import Table
|
|
|
|
from browser_cli.commands import client_from_ctx, handle_errors
|
|
from browser_cli.constants import CONFIG_DIR
|
|
|
|
console = Console()
|
|
WORKSPACES_PATH = CONFIG_DIR / "workspaces.json"
|
|
|
|
def _load() -> dict:
|
|
try:
|
|
return json.loads(WORKSPACES_PATH.read_text(encoding="utf-8"))
|
|
except FileNotFoundError:
|
|
return {}
|
|
|
|
def _save(data: dict) -> None:
|
|
WORKSPACES_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
WORKSPACES_PATH.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
|
|
@click.group("workspace")
|
|
def workspace_group():
|
|
"""Named browser workspaces built on top of sessions."""
|
|
|
|
@workspace_group.command("save")
|
|
@click.argument("name")
|
|
@click.option("--session", "session_name", default=None, help="Session name to save/use (default: workspace name)")
|
|
@click.option("--profile", default=None, help="Performance profile to remember")
|
|
@handle_errors
|
|
def workspace_save(name, session_name, profile):
|
|
session_name = session_name or name
|
|
result = client_from_ctx().session.save(session_name)
|
|
data = _load()
|
|
data[name] = {"session": session_name, "profile": profile}
|
|
_save(data)
|
|
console.print(f"[green]Workspace '{name}' saved[/green] ({result.get('tabs', 0) if isinstance(result, dict) else 0} tabs)")
|
|
|
|
@workspace_group.command("load")
|
|
@click.argument("name")
|
|
@click.option("--lazy", is_flag=True, help="Lazy-restore tabs")
|
|
@click.option("--eager-tabs", type=int, default=10, show_default=True)
|
|
@handle_errors
|
|
def workspace_load(name, lazy, eager_tabs):
|
|
data = _load()
|
|
ws = data.get(name)
|
|
if not ws:
|
|
raise click.ClickException(f"Workspace '{name}' not found")
|
|
client = client_from_ctx()
|
|
if ws.get("profile"):
|
|
client.perf.set_profile(ws["profile"])
|
|
result = client.session.load(ws["session"], lazy=lazy, eager_tabs=eager_tabs)
|
|
console.print(f"[green]Workspace '{name}' loaded[/green] ({result.get('tabs', 0) if isinstance(result, dict) else 0} tabs)")
|
|
|
|
@workspace_group.command("switch")
|
|
@click.argument("name")
|
|
@click.option("--lazy", is_flag=True)
|
|
@handle_errors
|
|
def workspace_switch(name, lazy):
|
|
"""Load a workspace. Alias for workspace load."""
|
|
ctx = click.get_current_context()
|
|
ctx.invoke(workspace_load, name=name, lazy=lazy, eager_tabs=10)
|
|
|
|
@workspace_group.command("list")
|
|
def workspace_list():
|
|
"""List configured workspaces."""
|
|
data = _load()
|
|
if not data:
|
|
console.print("[yellow]No workspaces[/yellow]")
|
|
return
|
|
table = Table(show_header=True, header_style="bold cyan")
|
|
table.add_column("Name")
|
|
table.add_column("Session")
|
|
table.add_column("Profile")
|
|
for name, ws in sorted(data.items()):
|
|
table.add_row(name, ws.get("session", ""), ws.get("profile") or "")
|
|
console.print(table)
|
|
|
|
@workspace_group.command("remove")
|
|
@click.argument("name")
|
|
def workspace_remove(name):
|
|
data = _load()
|
|
if name not in data:
|
|
raise click.ClickException(f"Workspace '{name}' not found")
|
|
del data[name]
|
|
_save(data)
|
|
console.print(f"[green]Workspace '{name}' removed[/green]")
|