diff --git a/README.md b/README.md index 0457bf0..1155a81 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,7 @@ browser-cli/ All commands are run with `uv run browser-cli [--browser ALIAS] `. -Use `--browser ALIAS` when multiple browser instances are connected. You can inspect the active instances with `browser-cli clients` and assign a persistent profile alias from inside the target browser with `browser-cli rename-profile --browser `. +If exactly one browser instance is connected, commands auto-target it. Use `--browser ALIAS` when multiple browser instances are connected. You can inspect the active instances with `browser-cli clients` and assign a persistent profile alias from inside the target browser with `browser-cli rename-profile --browser `. Important: profile aliases are browser-instance aliases, not window aliases. Window aliases created with `windows rename` are only for targeting windows in commands like `nav open --window work`. If a browser instance has no explicit profile alias set, the native host gives it a generated UUID alias so multiple unaliased browsers stay distinct. @@ -268,7 +268,7 @@ browser-cli session auto-save off ### Misc ```sh -browser-cli clients # show connected browser info +browser-cli clients # show connected browser info from the registry browser-cli rename-profile --browser abcd1234 work # rename one connected browser instance browser-cli --browser abcd1234 rename-profile work # equivalent global form browser-cli install brave # (re)register the native host diff --git a/browser_cli/cli.py b/browser_cli/cli.py index d577f6d..8372cb6 100755 --- a/browser_cli/cli.py +++ b/browser_cli/cli.py @@ -124,7 +124,7 @@ main.add_command(search_group) def cmd_clients(): """Show connected browser clients.""" import json as _json - from browser_cli.client import REGISTRY_PATH, DEFAULT_SOCKET + from browser_cli.client import REGISTRY_PATH # Build a map of profile → socket path from the registry profiles: dict[str, str] = {} @@ -133,8 +133,6 @@ def cmd_clients(): profiles = _json.loads(REGISTRY_PATH.read_text()) except Exception: pass - if not profiles: - profiles = {"default": DEFAULT_SOCKET} all_clients = [] for profile_name, sock_path in profiles.items(): @@ -148,7 +146,7 @@ def cmd_clients(): all_clients.append({"profile": profile_name, "name": "—", "version": "—", "platform": "disconnected"}) if not all_clients: - console.print("[yellow]No browser clients found[/yellow]") + console.print("[yellow]No browser clients found. Start a browser with the extension enabled first.[/yellow]") sys.exit(1) from rich.table import Table @@ -158,7 +156,7 @@ def cmd_clients(): table.add_column("Version") table.add_column("Platform") for c in all_clients: - table.add_row(c.get("profile", "default"), c.get("name", ""), c.get("version", ""), c.get("platform", "")) + table.add_row(c.get("profile", ""), c.get("name", ""), c.get("version", ""), c.get("platform", "")) console.print(table) diff --git a/browser_cli/client.py b/browser_cli/client.py index f934569..36f9e38 100644 --- a/browser_cli/client.py +++ b/browser_cli/client.py @@ -6,7 +6,7 @@ Profile selection order: 1. Explicit `profile` argument to send_command() 2. BROWSER_CLI_PROFILE environment variable 3. First entry in /tmp/.browser_cli/registry.json - 4. Fallback: /tmp/.browser_cli/default.sock + 4. Otherwise, no browser can be resolved automatically """ import json import os @@ -18,7 +18,6 @@ from typing import Any SOCKET_DIR = Path("/tmp/.browser_cli") REGISTRY_PATH = SOCKET_DIR / "registry.json" -DEFAULT_SOCKET = str(SOCKET_DIR / "default.sock") class BrowserNotConnected(Exception): @@ -64,7 +63,11 @@ def _resolve_socket(profile: str | None = None) -> str: except Exception: pass - return DEFAULT_SOCKET + raise BrowserNotConnected( + "Cannot resolve a browser socket automatically.\n" + "Make sure the browser is running with the browser-cli extension enabled,\n" + "or pass --browser / set BROWSER_CLI_PROFILE to a known alias." + ) def send_command(command: str, args: dict | None = None, profile: str | None = None) -> Any: diff --git a/pyproject.toml b/pyproject.toml index fba0a37..0fa9f9c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "browser-cli" -version = "0.4.0" +version = "0.4.1" description = "Control your real running browser from the terminal via a Chrome extension" requires-python = ">=3.10" dependencies = [ diff --git a/tests/test_cli.py b/tests/test_cli.py index df2f032..74c771f 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -47,3 +47,10 @@ def test_install_help_lists_supported_browsers(): assert result.exit_code == 0 assert "[chrome|chromium|brave|edge|vivaldi]" in result.output + +def test_clients_exits_cleanly_when_registry_is_missing(): + with patch("browser_cli.client.REGISTRY_PATH", Path("/nonexistent/browser-cli-registry.json")): + result = CliRunner().invoke(main, ["clients"]) + + assert result.exit_code == 1 + assert "No browser clients found" in result.output diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 0000000..2eb9562 --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,40 @@ +import json +from pathlib import Path + +import pytest + +from browser_cli.client import BrowserNotConnected, _resolve_socket + +def test_resolve_socket_raises_when_registry_missing(monkeypatch): + monkeypatch.delenv("BROWSER_CLI_PROFILE", raising=False) + monkeypatch.setattr("browser_cli.client.REGISTRY_PATH", Path("/nonexistent/browser-cli-registry.json")) + + with pytest.raises(BrowserNotConnected, match="Cannot resolve a browser socket automatically"): + _resolve_socket() + +def test_resolve_socket_uses_only_active_registry_entry(monkeypatch, tmp_path): + monkeypatch.delenv("BROWSER_CLI_PROFILE", raising=False) + + socket_path = tmp_path / "browser.sock" + socket_path.write_text("") + registry_path = tmp_path / "registry.json" + registry_path.write_text(json.dumps({"abc-uuid": str(socket_path)})) + + monkeypatch.setattr("browser_cli.client.REGISTRY_PATH", registry_path) + + assert _resolve_socket() == str(socket_path) + +def test_resolve_socket_raises_when_multiple_active_entries(monkeypatch, tmp_path): + monkeypatch.delenv("BROWSER_CLI_PROFILE", raising=False) + + first_socket = tmp_path / "one.sock" + second_socket = tmp_path / "two.sock" + first_socket.write_text("") + second_socket.write_text("") + registry_path = tmp_path / "registry.json" + registry_path.write_text(json.dumps({"uuid-1": str(first_socket), "uuid-2": str(second_socket)})) + + monkeypatch.setattr("browser_cli.client.REGISTRY_PATH", registry_path) + + with pytest.raises(BrowserNotConnected, match="Multiple browser instances are active: uuid-1, uuid-2"): + _resolve_socket()