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.
164 lines
6.9 KiB
Python
164 lines
6.9 KiB
Python
"""Native Messaging host installation command."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import click
|
|
from rich.console import Console
|
|
|
|
from browser_cli.constants import (
|
|
ALLOWED_EXTENSION_IDS,
|
|
CHROME_WEBSTORE_URL,
|
|
EXTENSION_ID,
|
|
FIREFOX_ADDON_URL,
|
|
FIREFOX_EXTENSION_ID,
|
|
NATIVE_HOST_DIRS,
|
|
NATIVE_HOST_NAME,
|
|
SUPPORTED_BROWSERS,
|
|
WEBSTORE_EXTENSION_ID,
|
|
WINDOWS_NATIVE_HOST_REGISTRY_KEYS,
|
|
)
|
|
from browser_cli.platform import install_base_dir, is_windows
|
|
|
|
console = Console()
|
|
|
|
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():
|
|
path.write_text(
|
|
f'@echo off\r\n"{sys.executable}" -c "from browser_cli.native.host import main; main()" %*\r\n',
|
|
encoding="utf-8",
|
|
)
|
|
else:
|
|
path.write_text(f'#!{sys.executable}\nfrom browser_cli.native.host import main\nmain()\n')
|
|
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
|
|
|
|
installed = []
|
|
for key_path in WINDOWS_NATIVE_HOST_REGISTRY_KEYS[browser]:
|
|
full_key = f"{key_path}\\{NATIVE_HOST_NAME}"
|
|
for view in _windows_registry_views():
|
|
try:
|
|
access = winreg.KEY_WRITE | view
|
|
key = winreg.CreateKeyEx(winreg.HKEY_CURRENT_USER, full_key, 0, access)
|
|
with key:
|
|
winreg.SetValueEx(key, "", 0, winreg.REG_SZ, str(manifest_path))
|
|
installed.append(f"HKCU\\{full_key}")
|
|
except OSError as e:
|
|
console.print(f"[yellow]Could not write registry key {full_key}: {e}[/yellow]")
|
|
return installed
|
|
|
|
@click.command("install")
|
|
@click.argument("browser", type=click.Choice(SUPPORTED_BROWSERS), default="chrome")
|
|
@click.option("--dev", is_flag=True, help="Print developer instructions for loading an unpacked/temporary build instead of the public store listing.")
|
|
def cmd_install(browser, dev):
|
|
"""Register the native messaging host and print extension install instructions."""
|
|
host_exe = native_host_exe()
|
|
write_native_host_exe(host_exe)
|
|
|
|
if dev:
|
|
_print_dev_instructions(browser)
|
|
else:
|
|
_print_store_instructions(browser)
|
|
|
|
manifest = _native_host_manifest(browser, host_exe)
|
|
installed = _install_manifest(browser, host_exe, manifest)
|
|
if not installed:
|
|
console.print("[red]Failed to install native host manifest[/red]")
|
|
sys.exit(1)
|
|
|
|
for p in installed:
|
|
label = "Registered native host" if is_windows() else "Wrote native host manifest"
|
|
console.print(f"[green]✓[/green] {label}: {p}")
|
|
console.print(f"[green]✓[/green] Installed native host: {host_exe}")
|
|
console.print(f"\n[bold]Step 2:[/bold] Restart {browser.capitalize()} completely (quit app, then reopen)")
|
|
console.print("\n[green bold]✓ Installation complete![/green bold]")
|
|
console.print(" After restarting the browser, try: [cyan]browser-cli tabs list[/cyan]")
|
|
|
|
def _print_store_instructions(browser: str) -> None:
|
|
console.print("\n[bold]Step 1:[/bold] Install the extension")
|
|
if browser == "firefox":
|
|
console.print(" Open Firefox Add-ons and click [bold]Add to Firefox[/bold]:")
|
|
console.print(f" [cyan]{FIREFOX_ADDON_URL}[/cyan]")
|
|
console.print(" [dim]Firefox support is experimental; tab-group commands require browser tab group APIs.[/dim]\n")
|
|
else:
|
|
console.print(f" Open the Chrome Web Store and click [bold]Add to {browser.capitalize()}[/bold]:")
|
|
console.print(f" [cyan]{CHROME_WEBSTORE_URL}[/cyan]")
|
|
console.print(" [dim]Brave, Edge, Vivaldi and Chromium can install from the Chrome Web Store too.[/dim]")
|
|
console.print(" [dim]Developing the extension? Run 'browser-cli install <browser> --dev' for the unpacked-load steps.[/dim]\n")
|
|
|
|
def _print_dev_instructions(browser: str) -> None:
|
|
ext_url = {
|
|
"chrome": "chrome://extensions",
|
|
"chromium": "chrome://extensions",
|
|
"brave": "brave://extensions",
|
|
"edge": "edge://extensions",
|
|
"vivaldi": "vivaldi://extensions",
|
|
"firefox": "about:debugging#/runtime/this-firefox",
|
|
}[browser]
|
|
console.print("\n[bold]Step 1:[/bold] Load the unpacked extension (developer mode)")
|
|
console.print(f" 1. Open [cyan]{ext_url}[/cyan]")
|
|
if browser == "firefox":
|
|
repo_root = Path(__file__).parent.parent.parent
|
|
firefox_manifest = repo_root / "dist" / "extension-package-firefox" / "manifest.json"
|
|
console.print(" 2. Build the Firefox-compatible temporary extension:")
|
|
console.print(" [cyan]npm run package:extension:firefox[/cyan]")
|
|
console.print(" 3. Click [bold]Load Temporary Add-on...[/bold]")
|
|
console.print(f" 4. Select: [cyan]{firefox_manifest}[/cyan]")
|
|
console.print(" Do not select extension/manifest.json; Firefox currently rejects background.service_worker there.")
|
|
console.print(f" 5. Firefox extension ID is [cyan]{FIREFOX_EXTENSION_ID}[/cyan]")
|
|
console.print(" Note: Firefox support is experimental; tab-group commands require browser tab group APIs.\n")
|
|
else:
|
|
console.print(" 2. Enable [bold]Developer mode[/bold] (top-right toggle)")
|
|
console.print(f" 3. Click [bold]Load unpacked[/bold] → select: [cyan]{Path(__file__).parent.parent.parent / 'extension'}[/cyan]")
|
|
console.print(f" 4. Testing extension ID will be [cyan]{EXTENSION_ID}[/cyan] (fixed by built-in key)")
|
|
console.print(f" Chrome Web Store extension ID is [cyan]{WEBSTORE_EXTENSION_ID}[/cyan]\n")
|
|
|
|
def _native_host_manifest(browser: str, host_exe: Path) -> dict:
|
|
base = {
|
|
"name": NATIVE_HOST_NAME,
|
|
"description": "browser-cli native messaging host",
|
|
"path": str(host_exe),
|
|
"type": "stdio",
|
|
}
|
|
if browser == "firefox":
|
|
return {**base, "allowed_extensions": [FIREFOX_EXTENSION_ID]}
|
|
return {
|
|
**base,
|
|
"allowed_origins": [f"chrome-extension://{extension_id}/" for extension_id in ALLOWED_EXTENSION_IDS],
|
|
}
|
|
|
|
def _install_manifest(browser: str, host_exe: Path, manifest: dict) -> list:
|
|
if is_windows():
|
|
manifest_dir = host_exe.parent
|
|
manifest_dir.mkdir(parents=True, exist_ok=True)
|
|
manifest_path = manifest_dir / f"{NATIVE_HOST_NAME}.json"
|
|
manifest_path.write_text(json.dumps(manifest, indent=2), encoding="utf-8")
|
|
return _register_windows_native_host(browser, manifest_path)
|
|
|
|
platform = "darwin" if sys.platform == "darwin" else "linux"
|
|
installed = []
|
|
for directory in NATIVE_HOST_DIRS[browser][platform]:
|
|
try:
|
|
directory.mkdir(parents=True, exist_ok=True)
|
|
manifest_path = directory / f"{NATIVE_HOST_NAME}.json"
|
|
manifest_path.write_text(json.dumps(manifest, indent=2))
|
|
installed.append(manifest_path)
|
|
except Exception as e:
|
|
console.print(f"[yellow]Could not write to {directory}: {e}[/yellow]")
|
|
return installed
|