allow to ask for remote host profiles and save token on first connection for later use

This commit is contained in:
2026-05-01 19:07:04 +02:00
parent 2f982fa714
commit 5ff340a6d3
11 changed files with 216 additions and 33 deletions
+5 -2
View File
@@ -54,7 +54,7 @@ class BrowserCLI:
if self._browser is not None:
return []
targets = active_browser_targets()
if len(targets) <= 1:
if len(targets) <= 1 and not any(target.remote for target in targets):
return []
return targets
@@ -62,7 +62,10 @@ class BrowserCLI:
results = []
for target in self._multi_browser_targets():
try:
data = send_command(command, args, profile=target.profile)
if target.remote:
data = send_command(command, args, profile=target.profile, remote=target.remote, token=target.token)
else:
data = send_command(command, args, profile=target.profile)
except (BrowserNotConnected, RuntimeError):
continue
results.append((target, data))
+14
View File
@@ -31,6 +31,7 @@ from browser_cli.client import (
REGISTRY_PATH,
active_browser_targets,
display_browser_name,
save_remote_token,
)
from browser_cli.platform import install_base_dir, is_windows
@@ -185,6 +186,8 @@ def main(ctx, browser, remote, token):
ctx.obj["token"] = token
if remote:
os.environ["BROWSER_CLI_REMOTE"] = remote
if token:
save_remote_token(remote, token)
if token:
os.environ["BROWSER_CLI_TOKEN"] = token
@@ -249,6 +252,17 @@ def clients_group(ctx):
"extensionVersion": "disconnected",
})
for target in active_browser_targets():
if target.remote is None:
continue
try:
result = send_command("clients.list", profile=target.profile, remote=target.remote, token=target.token)
for c in (result or []):
c["profile"] = target.display_name
all_clients.append(c)
except (BrowserNotConnected, RuntimeError):
continue
if not all_clients:
console.print("[yellow]No browser clients found. Start a browser with the extension enabled first.[/yellow]")
sys.exit(1)
+82 -11
View File
@@ -21,6 +21,7 @@ from typing import Any
from browser_cli.platform import endpoint_for_alias, is_windows, registry_path
REGISTRY_PATH = registry_path()
REMOTE_REGISTRY_PATH = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "browser-cli" / "remotes.json"
class BrowserNotConnected(Exception):
@@ -32,6 +33,8 @@ class BrowserTarget:
profile: str
display_name: str
socket_path: str
remote: str | None = None
token: str | None = None
def _active_endpoints(reg: dict) -> dict:
@@ -47,17 +50,85 @@ def display_browser_name(profile_name: str, sock_path: str) -> str:
return Path(sock_path).stem or profile_name
def active_browser_targets() -> list[BrowserTarget]:
if not REGISTRY_PATH.exists():
return []
def _load_remotes() -> dict[str, dict[str, str]]:
if not REMOTE_REGISTRY_PATH.exists():
return {}
try:
reg = json.loads(REGISTRY_PATH.read_text())
data = json.loads(REMOTE_REGISTRY_PATH.read_text(encoding="utf-8"))
except Exception:
return []
return [
BrowserTarget(profile=profile, display_name=display_browser_name(profile, sock_path), socket_path=sock_path)
for profile, sock_path in _active_endpoints(reg).items()
]
return {}
if not isinstance(data, dict):
return {}
return {str(endpoint): cfg for endpoint, cfg in data.items() if isinstance(cfg, dict)}
def save_remote_token(endpoint: str, token: str | None) -> None:
"""Persist the auth token for a remote endpoint used by this client."""
if not endpoint or not token:
return
remotes = _load_remotes()
current = remotes.get(endpoint, {})
current["token"] = token
remotes[endpoint] = current
REMOTE_REGISTRY_PATH.parent.mkdir(parents=True, exist_ok=True)
REMOTE_REGISTRY_PATH.write_text(json.dumps(remotes, indent=2, sort_keys=True), encoding="utf-8")
try:
REMOTE_REGISTRY_PATH.chmod(0o600)
except OSError:
pass
def token_for_remote(endpoint: str | None) -> str | None:
if not endpoint:
return None
cfg = _load_remotes().get(endpoint) or {}
token = cfg.get("token")
return str(token) if token else None
def _remote_display_name(endpoint: str, profile_name: str, display_name: str) -> str:
host, sep, port = endpoint.rpartition(":")
remote_name = host if sep and port == "8765" else endpoint
return f"{remote_name}:{display_name or profile_name}"
def _remote_browser_targets() -> list[BrowserTarget]:
targets: list[BrowserTarget] = []
for endpoint, cfg in _load_remotes().items():
token = str(cfg.get("token") or "") or None
try:
remote_targets = send_command("browser-cli.targets", remote=endpoint, token=token)
except (BrowserNotConnected, RuntimeError):
continue
for item in remote_targets or []:
profile = str(item.get("profile") or "default")
display = str(item.get("displayName") or profile)
targets.append(
BrowserTarget(
profile=profile,
display_name=_remote_display_name(endpoint, profile, display),
socket_path="",
remote=endpoint,
token=token,
)
)
return targets
def active_browser_targets(*, include_remotes: bool = True) -> list[BrowserTarget]:
targets: list[BrowserTarget] = []
if REGISTRY_PATH.exists():
try:
reg = json.loads(REGISTRY_PATH.read_text())
except Exception:
reg = {}
targets.extend(
BrowserTarget(profile=profile, display_name=display_browser_name(profile, sock_path), socket_path=sock_path)
for profile, sock_path in _active_endpoints(reg).items()
)
if include_remotes:
targets.extend(_remote_browser_targets())
return targets
def _resolve_socket(profile: str | None = None) -> str:
@@ -76,7 +147,7 @@ def _resolve_socket(profile: str | None = None) -> str:
# Auto-detect: error when multiple browser instances are active
try:
active = active_browser_targets()
active = active_browser_targets(include_remotes=False)
if len(active) > 1:
aliases = [target.profile for target in active]
examples = "\n".join(f" browser-cli --browser {a} ..." for a in aliases)
@@ -101,7 +172,7 @@ def _resolve_socket(profile: str | None = None) -> str:
def send_command(command: str, args: dict | None = None, profile: str | None = None, remote: str | None = None, token: str | None = None) -> Any:
"""Send a command to the browser and return the response data."""
remote_endpoint = remote or os.environ.get("BROWSER_CLI_REMOTE")
resolved_token = token or os.environ.get("BROWSER_CLI_TOKEN")
resolved_token = token or os.environ.get("BROWSER_CLI_TOKEN") or token_for_remote(remote_endpoint)
msg = {
"id": str(uuid.uuid4()),
"command": command,
+6 -4
View File
@@ -17,8 +17,10 @@ def _handle(command, args=None, profile=None):
raise SystemExit(1)
def _handle_multi(command, args=None, profile=None):
def _handle_multi(command, args=None, profile=None, remote=None, token=None):
try:
if remote:
return send_command(command, args or {}, profile=profile, remote=remote, token=token)
return send_command(command, args or {}, profile=profile)
except (BrowserNotConnected, RuntimeError):
return None
@@ -29,7 +31,7 @@ def _multi_browser_targets():
if root.obj.get("browser_explicit"):
return []
targets = active_browser_targets()
if len(targets) <= 1:
if len(targets) <= 1 and not any(target.remote for target in targets):
return []
return targets
@@ -71,7 +73,7 @@ def group_list():
if targets:
groups = []
for target in targets:
result = _handle_multi("group.list", profile=target.profile)
result = _handle_multi("group.list", profile=target.profile, remote=target.remote, token=target.token)
if result is None:
continue
groups.extend({**group, "browser": target.display_name} for group in result)
@@ -104,7 +106,7 @@ def group_count():
total = 0
rows = 0
for target in targets:
count = _handle_multi("group.count", profile=target.profile)
count = _handle_multi("group.count", profile=target.profile, remote=target.remote, token=target.token)
if count is None:
continue
count = int(count or 0)
+11
View File
@@ -56,6 +56,17 @@ def _proxy_request(client_sock:socket.socket, addr:tuple, profile:str|None, serv
_log(addr, command, None, "DENIED", "bad token")
return
if command == "browser-cli.targets":
from browser_cli.client import active_browser_targets
targets = [
{"profile": target.profile, "displayName": target.display_name}
for target in active_browser_targets(include_remotes=False)
]
data = json.dumps({"id": msg_id, "success": True, "data": targets}).encode()
client_sock.sendall(struct.pack("<I", len(data)) + data)
_log(addr, command, None, "OK")
return
resolved_profile = msg.get("_route") or profile
strip = {"token", "_route"}
+5 -3
View File
@@ -16,8 +16,10 @@ def _handle(command, args=None, profile=None):
raise SystemExit(1)
def _handle_multi(command, args=None, profile=None):
def _handle_multi(command, args=None, profile=None, remote=None, token=None):
try:
if remote:
return send_command(command, args or {}, profile=profile, remote=remote, token=token)
return send_command(command, args or {}, profile=profile)
except (BrowserNotConnected, RuntimeError):
return None
@@ -28,7 +30,7 @@ def _multi_browser_targets():
if root.obj.get("browser_explicit"):
return []
targets = active_browser_targets()
if len(targets) <= 1:
if len(targets) <= 1 and not any(target.remote for target in targets):
return []
return targets
@@ -92,7 +94,7 @@ def session_list():
if targets:
sessions = []
for target in targets:
result = _handle_multi("session.list", profile=target.profile)
result = _handle_multi("session.list", profile=target.profile, remote=target.remote, token=target.token)
if result is None:
continue
sessions.extend({**session, "browser": target.display_name} for session in result)
+6 -4
View File
@@ -18,8 +18,10 @@ def _handle(command, args=None, profile=None):
raise SystemExit(1)
def _handle_multi(command, args=None, profile=None):
def _handle_multi(command, args=None, profile=None, remote=None, token=None):
try:
if remote:
return send_command(command, args or {}, profile=profile, remote=remote, token=token)
return send_command(command, args or {}, profile=profile)
except (BrowserNotConnected, RuntimeError):
return None
@@ -30,7 +32,7 @@ def _multi_browser_targets():
if root.obj.get("browser_explicit"):
return []
targets = active_browser_targets()
if len(targets) <= 1:
if len(targets) <= 1 and not any(target.remote for target in targets):
return []
return targets
@@ -76,7 +78,7 @@ def tabs_list():
if targets:
tabs = []
for target in targets:
result = _handle_multi("tabs.list", profile=target.profile)
result = _handle_multi("tabs.list", profile=target.profile, remote=target.remote, token=target.token)
if result is None:
continue
tabs.extend({**tab, "browser": target.display_name} for tab in result)
@@ -163,7 +165,7 @@ def tabs_count(pattern):
total = 0
rows = 0
for target in targets:
count = _handle_multi("tabs.count", {"pattern": pattern}, profile=target.profile)
count = _handle_multi("tabs.count", {"pattern": pattern}, profile=target.profile, remote=target.remote, token=target.token)
if count is None:
continue
count = int(count or 0)
+5 -3
View File
@@ -17,8 +17,10 @@ def _handle(command, args=None, profile=None):
raise SystemExit(1)
def _handle_multi(command, args=None, profile=None):
def _handle_multi(command, args=None, profile=None, remote=None, token=None):
try:
if remote:
return send_command(command, args or {}, profile=profile, remote=remote, token=token)
return send_command(command, args or {}, profile=profile)
except (BrowserNotConnected, RuntimeError):
return None
@@ -29,7 +31,7 @@ def _multi_browser_targets():
if root.obj.get("browser_explicit"):
return []
targets = active_browser_targets()
if len(targets) <= 1:
if len(targets) <= 1 and not any(target.remote for target in targets):
return []
return targets
@@ -69,7 +71,7 @@ def windows_list():
if targets:
windows = []
for target in targets:
result = _handle_multi("windows.list", profile=target.profile)
result = _handle_multi("windows.list", profile=target.profile, remote=target.remote, token=target.token)
if result is None:
continue
windows.extend({**window, "browser": target.display_name} for window in result)