allow to rename the profile without a browser restart and remove old sockets and registry entry when browser closes
This commit is contained in:
@@ -122,7 +122,7 @@ browser-cli/
|
|||||||
|
|
||||||
All commands are run with `uv run browser-cli [--browser ALIAS] <command>`.
|
All commands are run with `uv run browser-cli [--browser ALIAS] <command>`.
|
||||||
|
|
||||||
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 <current-alias> <new-alias>`.
|
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 <current-alias> <new-alias>`. Closed browsers are removed from the client registry automatically.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
@@ -385,6 +385,6 @@ bash examples/demo.sh
|
|||||||
|
|
||||||
- **Chrome internal pages** (`chrome://`, `brave://`, `about:`) cannot be scripted. DOM and extract commands only work on regular `http://` and `https://` pages.
|
- **Chrome internal pages** (`chrome://`, `brave://`, `about:`) cannot be scripted. DOM and extract commands only work on regular `http://` and `https://` pages.
|
||||||
- **Profile switching** via `windows open --profile` depends on browser support and does not replace launching a separate browser profile externally with `--profile-directory`.
|
- **Profile switching** via `windows open --profile` depends on browser support and does not replace launching a separate browser profile externally with `--profile-directory`.
|
||||||
- **Multiple browser instances can be auto-distinguished, but generated aliases are temporary**. Unaliased browsers get UUID aliases from the native host, which avoids collisions but is less ergonomic than setting a stable alias with `browser-cli rename-profile --browser <current-alias> <new-alias>` and restarting that browser.
|
- **Multiple browser instances can be auto-distinguished, but generated aliases are temporary**. Unaliased browsers get UUID aliases from the native host, which avoids collisions but is less ergonomic than setting a stable alias with `browser-cli rename-profile --browser <current-alias> <new-alias>`.
|
||||||
- **Supported install targets are explicit, not “all Chromium browsers”**. The installer currently supports Chrome, Chromium, Brave, Edge, and Vivaldi. Other Chromium-based browsers may use different or shared native messaging manifest locations, so they need browser-specific verification before being added safely.
|
- **Supported install targets are explicit, not “all Chromium browsers”**. The installer currently supports Chrome, Chromium, Brave, Edge, and Vivaldi. Other Chromium-based browsers may use different or shared native messaging manifest locations, so they need browser-specific verification before being added safely.
|
||||||
- **Linux and macOS only** — Windows native messaging paths are not yet handled.
|
- **Linux and macOS only** — Windows native messaging paths are not yet handled.
|
||||||
|
|||||||
@@ -191,7 +191,6 @@ def cmd_rename_profile(target_browser, alias):
|
|||||||
console.print(f"[red]Error:[/red] {e}")
|
console.print(f"[red]Error:[/red] {e}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
console.print(f"[green]Profile renamed to '{alias}'[/green]")
|
console.print(f"[green]Profile renamed to '{alias}'[/green]")
|
||||||
console.print(" Restart the browser for the change to take effect.")
|
|
||||||
|
|
||||||
|
|
||||||
# ── install ────────────────────────────────────────────────────────────────────
|
# ── install ────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -98,6 +98,9 @@ def stdin_reader(alias: str):
|
|||||||
# Profile alias handshake
|
# Profile alias handshake
|
||||||
if msg.get("type") == "hello":
|
if msg.get("type") == "hello":
|
||||||
continue # already handled during startup
|
continue # already handled during startup
|
||||||
|
if msg.get("type") == "bye":
|
||||||
|
_cleanup(alias)
|
||||||
|
os._exit(0)
|
||||||
|
|
||||||
msg_id = msg.get("id")
|
msg_id = msg.get("id")
|
||||||
if msg_id:
|
if msg_id:
|
||||||
|
|||||||
+53
-7
@@ -8,27 +8,53 @@
|
|||||||
|
|
||||||
const NATIVE_HOST = "com.browsercli.host";
|
const NATIVE_HOST = "com.browsercli.host";
|
||||||
let port = null;
|
let port = null;
|
||||||
|
let keepaliveEnabled = true;
|
||||||
|
|
||||||
// ── Connection management ─────────────────────────────────────────────────────
|
// ── Connection management ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function sendControlMessage(targetPort, message) {
|
||||||
|
if (!targetPort) return;
|
||||||
|
try {
|
||||||
|
targetPort.postMessage(message);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("[browser-cli] Failed to send control message:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function disconnectPort({ sendBye = false } = {}) {
|
||||||
|
const currentPort = port;
|
||||||
|
if (!currentPort) return;
|
||||||
|
|
||||||
|
if (sendBye) sendControlMessage(currentPort, { type: "bye" });
|
||||||
|
|
||||||
|
if (port === currentPort) port = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
currentPort.disconnect();
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("[browser-cli] Failed to disconnect native port:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function getProfileAlias() {
|
async function getProfileAlias() {
|
||||||
const { profileAlias } = await chrome.storage.local.get("profileAlias");
|
const { profileAlias } = await chrome.storage.local.get("profileAlias");
|
||||||
return profileAlias || "default";
|
return profileAlias || "default";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function connect() {
|
async function connect() {
|
||||||
if (port) return;
|
if (port || !keepaliveEnabled) return;
|
||||||
try {
|
try {
|
||||||
port = chrome.runtime.connectNative(NATIVE_HOST);
|
const nativePort = chrome.runtime.connectNative(NATIVE_HOST);
|
||||||
port.onMessage.addListener(onMessage);
|
port = nativePort;
|
||||||
port.onDisconnect.addListener(() => {
|
nativePort.onMessage.addListener(onMessage);
|
||||||
port = null;
|
nativePort.onDisconnect.addListener(() => {
|
||||||
|
if (port === nativePort) port = null;
|
||||||
const err = chrome.runtime.lastError;
|
const err = chrome.runtime.lastError;
|
||||||
if (err) console.warn("[browser-cli] Native host disconnected:", err.message);
|
if (err) console.warn("[browser-cli] Native host disconnected:", err.message);
|
||||||
});
|
});
|
||||||
// Send hello so native host knows which profile/alias this is
|
// Send hello so native host knows which profile/alias this is
|
||||||
const alias = await getProfileAlias();
|
const alias = await getProfileAlias();
|
||||||
port.postMessage({ type: "hello", alias });
|
nativePort.postMessage({ type: "hello", alias });
|
||||||
console.log("[browser-cli] Connected to native host as profile:", alias);
|
console.log("[browser-cli] Connected to native host as profile:", alias);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
port = null;
|
port = null;
|
||||||
@@ -38,12 +64,26 @@ async function connect() {
|
|||||||
|
|
||||||
chrome.runtime.onInstalled.addListener(connect);
|
chrome.runtime.onInstalled.addListener(connect);
|
||||||
chrome.runtime.onStartup.addListener(connect);
|
chrome.runtime.onStartup.addListener(connect);
|
||||||
|
chrome.runtime.onSuspend.addListener(() => {
|
||||||
|
disconnectPort({ sendBye: true });
|
||||||
|
});
|
||||||
|
chrome.windows.onCreated.addListener(() => {
|
||||||
|
keepaliveEnabled = true;
|
||||||
|
if (!port) connect();
|
||||||
|
});
|
||||||
|
chrome.windows.onRemoved.addListener(async () => {
|
||||||
|
const windows = await chrome.windows.getAll({});
|
||||||
|
if (windows.length > 0) return;
|
||||||
|
|
||||||
|
keepaliveEnabled = false;
|
||||||
|
disconnectPort({ sendBye: true });
|
||||||
|
});
|
||||||
|
|
||||||
// Keepalive alarm — prevents service worker suspension and reconnects if needed
|
// Keepalive alarm — prevents service worker suspension and reconnects if needed
|
||||||
chrome.alarms.create("keepalive", { periodInMinutes: 0.4 });
|
chrome.alarms.create("keepalive", { periodInMinutes: 0.4 });
|
||||||
chrome.alarms.onAlarm.addListener((alarm) => {
|
chrome.alarms.onAlarm.addListener((alarm) => {
|
||||||
if (alarm.name === "keepalive") {
|
if (alarm.name === "keepalive") {
|
||||||
if (!port) connect();
|
if (!port && keepaliveEnabled) connect();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -69,6 +109,12 @@ async function onMessage(msg) {
|
|||||||
console.log("[browser-cli] →", command, data);
|
console.log("[browser-cli] →", command, data);
|
||||||
port.postMessage({ id, success: true, data });
|
port.postMessage({ id, success: true, data });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (command === "clients.rename_profile" && error === undefined) {
|
||||||
|
disconnectPort({ sendBye: true });
|
||||||
|
keepaliveEnabled = true;
|
||||||
|
await connect();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function dispatch(command, args) {
|
async function dispatch(command, args) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "browser-cli",
|
"name": "browser-cli",
|
||||||
"version": "0.5.1",
|
"version": "0.5.2",
|
||||||
"description": "Control your browser from the terminal via browser-cli",
|
"description": "Control your browser from the terminal via browser-cli",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"tabs",
|
"tabs",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "browser-cli"
|
name = "browser-cli"
|
||||||
version = "0.5.1"
|
version = "0.5.2"
|
||||||
description = "Control your real running browser from the terminal via a Chrome extension"
|
description = "Control your real running browser from the terminal via a Chrome extension"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ def test_rename_profile_uses_global_browser_target_when_set():
|
|||||||
|
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
send_command.assert_called_once_with("clients.rename_profile", {"alias": "work"}, profile=None)
|
send_command.assert_called_once_with("clients.rename_profile", {"alias": "work"}, profile=None)
|
||||||
|
assert "Restart the browser" not in result.output
|
||||||
|
|
||||||
def test_install_help_lists_supported_browsers():
|
def test_install_help_lists_supported_browsers():
|
||||||
result = CliRunner().invoke(main, ["install", "--help"])
|
result = CliRunner().invoke(main, ["install", "--help"])
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import browser_cli.native_host as native_host
|
||||||
|
|
||||||
|
|
||||||
|
def _raise_system_exit(code: int):
|
||||||
|
raise SystemExit(code)
|
||||||
|
|
||||||
|
|
||||||
|
def test_cleanup_removes_socket_and_registry_entry(monkeypatch, tmp_path):
|
||||||
|
alias = "work"
|
||||||
|
socket_path = tmp_path / "work.sock"
|
||||||
|
socket_path.write_text("")
|
||||||
|
registry_path = tmp_path / "registry.json"
|
||||||
|
registry_path.write_text(json.dumps({alias: str(socket_path), "other": str(tmp_path / "other.sock")}))
|
||||||
|
|
||||||
|
monkeypatch.setattr(native_host, "SOCKET_DIR", tmp_path)
|
||||||
|
monkeypatch.setattr(native_host, "REGISTRY_PATH", registry_path)
|
||||||
|
|
||||||
|
native_host._cleanup(alias)
|
||||||
|
|
||||||
|
assert not socket_path.exists()
|
||||||
|
assert json.loads(registry_path.read_text()) == {"other": str(tmp_path / "other.sock")}
|
||||||
|
|
||||||
|
|
||||||
|
def test_stdin_reader_cleans_up_on_eof(monkeypatch):
|
||||||
|
cleaned = []
|
||||||
|
|
||||||
|
monkeypatch.setattr(native_host, "read_native_message", lambda stream: None)
|
||||||
|
monkeypatch.setattr(native_host, "_cleanup", cleaned.append)
|
||||||
|
monkeypatch.setattr(native_host.os, "_exit", _raise_system_exit)
|
||||||
|
monkeypatch.setattr(native_host.sys, "stdin", SimpleNamespace(buffer=object()))
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit, match="0"):
|
||||||
|
native_host.stdin_reader("work")
|
||||||
|
|
||||||
|
assert cleaned == ["work"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_stdin_reader_cleans_up_on_bye(monkeypatch):
|
||||||
|
cleaned = []
|
||||||
|
messages = iter([{"type": "bye"}])
|
||||||
|
|
||||||
|
monkeypatch.setattr(native_host, "read_native_message", lambda stream: next(messages))
|
||||||
|
monkeypatch.setattr(native_host, "_cleanup", cleaned.append)
|
||||||
|
monkeypatch.setattr(native_host.os, "_exit", _raise_system_exit)
|
||||||
|
monkeypatch.setattr(native_host.sys, "stdin", SimpleNamespace(buffer=object()))
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit, match="0"):
|
||||||
|
native_host.stdin_reader("work")
|
||||||
|
|
||||||
|
assert cleaned == ["work"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_stdin_reader_routes_response_messages(monkeypatch):
|
||||||
|
response_queue = native_host.queue.Queue()
|
||||||
|
native_host.PENDING["msg-1"] = response_queue
|
||||||
|
messages = iter([{"type": "hello"}, {"id": "msg-1", "success": True}, None])
|
||||||
|
|
||||||
|
monkeypatch.setattr(native_host, "read_native_message", lambda stream: next(messages))
|
||||||
|
monkeypatch.setattr(native_host, "_cleanup", lambda alias: None)
|
||||||
|
monkeypatch.setattr(native_host.os, "_exit", _raise_system_exit)
|
||||||
|
monkeypatch.setattr(native_host.sys, "stdin", SimpleNamespace(buffer=object()))
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit, match="0"):
|
||||||
|
native_host.stdin_reader("work")
|
||||||
|
|
||||||
|
assert response_queue.get_nowait() == {"id": "msg-1", "success": True}
|
||||||
|
native_host.PENDING.clear()
|
||||||
@@ -4,7 +4,7 @@ requires-python = ">=3.10"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "browser-cli"
|
name = "browser-cli"
|
||||||
version = "0.5.1"
|
version = "0.5.2"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "click" },
|
{ name = "click" },
|
||||||
|
|||||||
Reference in New Issue
Block a user