refactor(api): namespaced SDK + dedicated transport layer
Testing / remote-protocol-compat (0.9.3) (push) Successful in 42s
Testing / remote-protocol-compat (0.9.5) (push) Successful in 44s
Package Extension / package-extension (push) Successful in 43s
Build & Publish Package / publish (push) Successful in 43s
Testing / test (push) Successful in 45s
Testing / remote-protocol-compat (0.9.3) (push) Successful in 42s
Testing / remote-protocol-compat (0.9.5) (push) Successful in 44s
Package Extension / package-extension (push) Successful in 43s
Build & Publish Package / publish (push) Successful in 43s
Testing / test (push) Successful in 45s
Restructure the Python API and internals around composable namespaces and a standalone transport/endpoint layer. Bump to 0.12.0. Python API: - Replace flat methods (b.tabs_list(), b.group_list()) with namespaces: b.nav, b.tabs, b.groups, b.windows, b.dom, b.extract, b.page, b.storage, b.cookies, b.session, b.perf, b.extension. - Shrink browser_cli/__init__.py to a thin composition root; move all behaviour into browser_cli/sdk/ (one module per namespace + factories, base, routing). Internals: - Add browser_cli/transport.py and remote_transport.py to isolate IPC from command logic; client.py now delegates instead of owning transport. - Add browser_cli/endpoints.py for endpoint resolution and browser_cli/errors.py for shared error types. - Extract markdown rendering into browser_cli/markdown.py (out of extract). - Add USER_AGENT to version_manager. Tooling & tests: - Add justfile with common dev tasks. - Update CLI commands and demo to the namespaced API. - Rework tests for the new layout; add test_transport.py and test_refactor_boundaries.py to lock in module boundaries. BREAKING CHANGE: flat API methods are removed in favour of namespaces (e.g. b.tabs_list() -> b.tabs.list(), b.group_list() -> b.groups.list()).
This commit is contained in:
+10
-33
@@ -88,7 +88,6 @@ WINDOWS_NATIVE_HOST_REGISTRY_KEYS = {
|
||||
"vivaldi": [r"Software\Vivaldi\NativeMessagingHosts"],
|
||||
}
|
||||
|
||||
|
||||
def _rename_target_profile(target_browser: str | None) -> str | None:
|
||||
if target_browser:
|
||||
return target_browser
|
||||
@@ -98,7 +97,6 @@ def _rename_target_profile(target_browser: str | None) -> str | None:
|
||||
return active[0].profile
|
||||
return None
|
||||
|
||||
|
||||
def _ensure_unique_browser_alias(alias: str, target_browser: str | None) -> None:
|
||||
target_profile = _rename_target_profile(target_browser)
|
||||
|
||||
@@ -107,14 +105,12 @@ def _ensure_unique_browser_alias(alias: str, target_browser: str | None) -> None
|
||||
if alias in profiles and alias != target_profile:
|
||||
raise click.ClickException(f"Browser alias '{alias}' already exists")
|
||||
|
||||
|
||||
def _native_host_exe() -> Path:
|
||||
base = install_base_dir()
|
||||
if is_windows():
|
||||
return base / "libexec" / "browser-cli-native-host.cmd"
|
||||
return base / "libexec" / "browser-cli-native-host"
|
||||
|
||||
|
||||
def _write_native_host_exe(path: Path) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if is_windows():
|
||||
@@ -128,13 +124,11 @@ def _write_native_host_exe(path: Path) -> None:
|
||||
)
|
||||
path.chmod(path.stat().st_mode | 0o111)
|
||||
|
||||
|
||||
def _windows_registry_views():
|
||||
import winreg
|
||||
|
||||
return [0, getattr(winreg, "KEY_WOW64_32KEY", 0), getattr(winreg, "KEY_WOW64_64KEY", 0)]
|
||||
|
||||
|
||||
def _register_windows_native_host(browser: str, manifest_path: Path) -> list[str]:
|
||||
import winreg
|
||||
|
||||
@@ -152,7 +146,6 @@ def _register_windows_native_host(browser: str, manifest_path: Path) -> list[str
|
||||
console.print(f"[yellow]Could not write registry key {full_key}: {e}[/yellow]")
|
||||
return installed
|
||||
|
||||
|
||||
def _project_version() -> str:
|
||||
pyproject_path = Path(__file__).resolve().parent.parent / "pyproject.toml"
|
||||
try:
|
||||
@@ -168,7 +161,6 @@ def _project_version() -> str:
|
||||
except PackageNotFoundError:
|
||||
return "unknown"
|
||||
|
||||
|
||||
def _print_version(ctx, param, value):
|
||||
if not value or ctx.resilient_parsing:
|
||||
return
|
||||
@@ -214,14 +206,12 @@ def main(ctx, browser, remote, key):
|
||||
os.environ["BROWSER_CLI_KEY"] = key
|
||||
ctx.call_on_close(lambda: os.environ.pop("BROWSER_CLI_KEY", None))
|
||||
|
||||
|
||||
# ── auth ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
@click.group("auth")
|
||||
def auth_group():
|
||||
"""Manage Ed25519 keys for public-key authentication with browser-cli serve."""
|
||||
|
||||
|
||||
@auth_group.command("keygen")
|
||||
@click.option("--output", "-o", default=None, metavar="PATH", help="Output path for the private key PEM.")
|
||||
@click.option("--force", is_flag=True, help="Overwrite existing key.")
|
||||
@@ -243,7 +233,6 @@ def cmd_auth_keygen(output, force):
|
||||
console.print(f"\nOn the serve host, trust this key:")
|
||||
console.print(f" [dim]browser-cli auth trust {pub_hex}[/dim]")
|
||||
|
||||
|
||||
@auth_group.command("trust")
|
||||
@click.argument("pubkey")
|
||||
@click.option("--name", default="", metavar="NAME", help="Human-friendly label for this key.")
|
||||
@@ -290,7 +279,6 @@ def cmd_auth_trust(ctx, pubkey, name, keys_file):
|
||||
else:
|
||||
console.print(f"[yellow]Already trusted:[/yellow] {pubkey}")
|
||||
|
||||
|
||||
@auth_group.command("show")
|
||||
@click.option("--key", "key_src", default=None, metavar="PATH|agent[:<selector>]",
|
||||
help="Key source: path to PEM file, 'agent', or 'agent:<comment-filter>'.")
|
||||
@@ -325,7 +313,6 @@ def cmd_auth_show(key_src):
|
||||
console.print(f"[red]Failed to load key:[/red] {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@auth_group.command("keys")
|
||||
@click.option("--file", "keys_file", default=None, metavar="PATH", help="Authorized keys file (default: ~/.config/browser-cli/authorized_keys).")
|
||||
@click.pass_context
|
||||
@@ -362,7 +349,6 @@ def cmd_auth_keys(ctx, keys_file):
|
||||
table.add_row(name, entry.get("pubkey", ""))
|
||||
console.print(table)
|
||||
|
||||
|
||||
main.add_command(auth_group)
|
||||
|
||||
# ── Sub-command groups ─────────────────────────────────────────────────────────
|
||||
@@ -381,9 +367,15 @@ main.add_command(perf_group)
|
||||
main.add_command(extension_group)
|
||||
main.add_command(cmd_serve)
|
||||
|
||||
|
||||
# ── clients ────────────────────────────────────────────────────────────────────
|
||||
|
||||
def _append_clients(into, label, *, profile=None, remote=None, key=None):
|
||||
"""Query clients.list for one target and append each, tagged with *label*."""
|
||||
result = send_command("clients.list", profile=profile, remote=remote, key=key)
|
||||
for c in (result or []):
|
||||
c["profile"] = label
|
||||
into.append(c)
|
||||
|
||||
@click.group("clients", invoke_without_command=True)
|
||||
@click.pass_context
|
||||
def clients_group(ctx):
|
||||
@@ -409,10 +401,7 @@ def clients_group(ctx):
|
||||
sys.exit(1)
|
||||
for target in targets:
|
||||
try:
|
||||
result = send_command("clients.list", profile=target.profile, remote=resolved.remote, key=key)
|
||||
for c in (result or []):
|
||||
c["profile"] = target.display_name
|
||||
all_clients.append(c)
|
||||
_append_clients(all_clients, target.display_name, profile=target.profile, remote=resolved.remote, key=key)
|
||||
except (BrowserNotConnected, RuntimeError):
|
||||
continue
|
||||
elif remote:
|
||||
@@ -432,10 +421,7 @@ def clients_group(ctx):
|
||||
for profile_name, sock_path in profiles.items():
|
||||
display_profile = display_browser_name(profile_name, sock_path)
|
||||
try:
|
||||
result = send_command("clients.list", profile=profile_name)
|
||||
for c in (result or []):
|
||||
c["profile"] = display_profile
|
||||
all_clients.append(c)
|
||||
_append_clients(all_clients, display_profile, profile=profile_name)
|
||||
except (BrowserNotConnected, RuntimeError):
|
||||
# Socket registered but browser no longer connected
|
||||
all_clients.append({
|
||||
@@ -449,10 +435,7 @@ def clients_group(ctx):
|
||||
if target.remote is None:
|
||||
continue
|
||||
try:
|
||||
result = send_command("clients.list", profile=target.profile, remote=target.remote)
|
||||
for c in (result or []):
|
||||
c["profile"] = target.display_name
|
||||
all_clients.append(c)
|
||||
_append_clients(all_clients, target.display_name, profile=target.profile, remote=target.remote)
|
||||
except (BrowserNotConnected, RuntimeError):
|
||||
continue
|
||||
|
||||
@@ -475,10 +458,8 @@ def clients_group(ctx):
|
||||
)
|
||||
console.print(table)
|
||||
|
||||
|
||||
main.add_command(clients_group)
|
||||
|
||||
|
||||
@clients_group.command("rename")
|
||||
@click.option(
|
||||
"--browser", "target_browser", default=None, metavar="ALIAS",
|
||||
@@ -495,7 +476,6 @@ def cmd_clients_rename(target_browser, alias):
|
||||
sys.exit(1)
|
||||
console.print(f"[green]Profile renamed to '{alias}'[/green]")
|
||||
|
||||
|
||||
# ── install ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@main.command("install")
|
||||
@@ -567,7 +547,6 @@ def cmd_install(browser):
|
||||
console.print("\n[green bold]✓ Installation complete![/green bold]")
|
||||
console.print(" After restarting the browser, try: [cyan]browser-cli tabs list[/cyan]")
|
||||
|
||||
|
||||
# ── native-host (hidden, called by Chrome via native messaging) ────────────────
|
||||
|
||||
@main.command("native-host", hidden=True)
|
||||
@@ -576,7 +555,6 @@ def cmd_native_host():
|
||||
from browser_cli.native_host import main as _main
|
||||
_main()
|
||||
|
||||
|
||||
# ── completion ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@main.command("completion")
|
||||
@@ -624,6 +602,5 @@ def cmd_completion(shell, script):
|
||||
console.print()
|
||||
console.print(f" [cyan]uv run browser-cli completion fish --script > ~/.config/fish/completions/browser-cli.fish[/cyan]")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user