aboutsummaryrefslogtreecommitdiff
path: root/src/cdp/endpoint.ts
diff options
context:
space:
mode:
authorIgor Soarez <igor@soarez.org>2026-07-28 22:08:44 +0100
committerIgor Soarez <igor@soarez.org>2026-10-14 12:17:59 +0100
commit95ff5e8eaefb35d79109f7ddfa1de5bf45a8daff (patch)
tree397c6cde867cb5028fcc5d00c1eb84d8a949da50 /src/cdp/endpoint.ts
parentbfb533fc9a829819b9dd425607101d40c676d45c (diff)
feat(cdp): default to the shared browser, and say so in /browser-status
Two follow-ups to BU_CDP_HTTP. Default the endpoint. The remote host is now a built-in default (DEFAULT_REMOTE_CDP), so nothing has to export BU_CDP_HTTP just to get the usual setup. The variable becomes an override rather than a requirement. Precedence is deliberately asymmetric, in src/cdp/endpoint.ts: BU_CDP_HTTP=host:port that endpoint; unreachable is a hard error BU_CDP_HTTP=local|off upstream behaviour, this machine's own browser unset the default, falling back to local when unreachable An explicitly configured endpoint is an instruction, so failing to reach it must not quietly automate a different browser - that would drive the operator's own signed-in Chrome instead of the shared one. The built-in default is only a preference, so a laptop away from the network still gets the local browser the extension was originally written for. Resolution is pure, with no I/O, so the status command can report the configured intent without touching the network and the daemon can resolve the same way in its own process. /browser-status no longer reports a bare "Disconnected". The connection is lazy - nothing dials the browser until the first browser_* tool runs - so a fresh session always showed a red light that reads as a fault. It now distinguishes "not connected yet, connects on first use" from "target unreachable", and names the target with a reachability check, so the common confusion answers itself. 26 tests for the resolution table, covering the aliases, malformed values that must be rejected rather than coerced, and that the default constant is itself well-formed - if it ever stopped parsing, every machine would silently fall back to local.
Diffstat (limited to 'src/cdp/endpoint.ts')
-rw-r--r--src/cdp/endpoint.ts84
1 files changed, 84 insertions, 0 deletions
diff --git a/src/cdp/endpoint.ts b/src/cdp/endpoint.ts
new file mode 100644
index 0000000..1be5322
--- /dev/null
+++ b/src/cdp/endpoint.ts
@@ -0,0 +1,84 @@
+/**
+ * Which browser this harness should drive.
+ *
+ * Upstream only ever drives a browser on localhost. This fork adds a remote
+ * target so pi can drive a shared Chrome on another host (see FORK.md).
+ *
+ * Resolution is pure — no I/O — so the status command can report the configured
+ * intent without touching the network, and the daemon can resolve the same way
+ * in its own process.
+ */
+
+/** Default remote CDP endpoint: castle's Chrome, exposed on the wire network. */
+export const DEFAULT_REMOTE_CDP = "10.88.0.25:9223";
+
+/** Values of BU_CDP_HTTP that mean "ignore the default, use this machine". */
+const LOCAL_ALIASES = new Set(["local", "localhost", "off", "none", "0", "no"]);
+
+export type CdpTarget =
+ | {
+ readonly kind: "remote";
+ readonly host: string;
+ readonly port: number;
+ /** "env" was asked for explicitly; "default" is this fork's built-in. */
+ readonly source: "env" | "default";
+ }
+ | { readonly kind: "local"; readonly source: "env" }
+ | { readonly kind: "invalid"; readonly raw: string; readonly reason: string };
+
+/**
+ * Precedence:
+ * BU_CDP_HTTP="host:port" -> that remote, and a failure to reach it is fatal
+ * BU_CDP_HTTP=local|off|… -> upstream behaviour, this machine's own browser
+ * unset -> DEFAULT_REMOTE_CDP, falling back to local when
+ * it isn't reachable
+ *
+ * The asymmetry is deliberate. An explicit endpoint is an instruction, so
+ * silently driving a different browser instead would be wrong. The built-in
+ * default is only a preference, so a laptop away from the wire network still
+ * gets the local browser the extension was originally written for.
+ */
+export const resolveCdpTarget = (env: NodeJS.ProcessEnv = process.env): CdpTarget => {
+ const raw = env["BU_CDP_HTTP"]?.trim();
+
+ if (raw === undefined || raw === "") {
+ const parsed = parseHostPort(DEFAULT_REMOTE_CDP);
+ // Unreachable in practice: the constant above is validated by its own test.
+ if (!parsed) return { kind: "local", source: "env" };
+ return { kind: "remote", host: parsed.host, port: parsed.port, source: "default" };
+ }
+
+ if (LOCAL_ALIASES.has(raw.toLowerCase())) return { kind: "local", source: "env" };
+
+ const parsed = parseHostPort(raw);
+ if (!parsed) {
+ return {
+ kind: "invalid",
+ raw,
+ reason: `expected "host:port" or one of ${[...LOCAL_ALIASES].join(", ")}`,
+ };
+ }
+ return { kind: "remote", host: parsed.host, port: parsed.port, source: "env" };
+};
+
+const parseHostPort = (value: string): { host: string; port: number } | null => {
+ const sep = value.lastIndexOf(":");
+ if (sep === -1) return null;
+ const host = value.slice(0, sep).trim();
+ const port = Number(value.slice(sep + 1).trim());
+ if (!host) return null;
+ if (!Number.isInteger(port) || port <= 0 || port >= 65536) return null;
+ return { host, port };
+};
+
+/** One-line description for /browser-status and error messages. */
+export const describeCdpTarget = (target: CdpTarget): string => {
+ switch (target.kind) {
+ case "remote":
+ return `${target.host}:${target.port}` + (target.source === "default" ? " (built-in default)" : " (BU_CDP_HTTP)");
+ case "local":
+ return "this machine's own browser (BU_CDP_HTTP=local)";
+ case "invalid":
+ return `invalid BU_CDP_HTTP "${target.raw}" — ${target.reason}`;
+ }
+};