@flrande/browserctl 0.2.0-dev.12.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README-CN.md CHANGED
@@ -2,50 +2,65 @@
2
2
 
3
3
  [English](README.md) | 简体中文
4
4
 
5
- 面向 coding agent 的扩展优先浏览器控制工具集。
5
+ 最快路径:通过 relay 扩展直接控制你已打开的 Edge/Chrome。
6
6
 
7
- ## 给 Agent 安装
7
+ ## 扩展模式最简上手(推荐)
8
8
 
9
- 把下面文本贴给 agent:
9
+ 前置条件:
10
10
 
11
- ```text
12
- 请抓取并遵循 https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/INSTALL.md 中的说明。
13
- ```
14
-
15
- 平台快捷入口:
16
-
17
- - Codex:`https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/.codex/INSTALL.md`
18
- - Claude Code:`https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/.claude-plugin/INSTALL.md`
19
- - Cursor:`https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/.cursor-plugin/INSTALL.md`
20
- - OpenCode:`https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/.opencode/INSTALL.md`
11
+ - Node.js 22+
12
+ - npm 或 pnpm
13
+ - PowerShell 7 (`pwsh`)
14
+ - Edge 或 Chrome
21
15
 
22
- 默认 skill:
23
-
24
- - 名称:`browserctl`
25
- - 路径:`skills/browserctl`
26
-
27
- ## 浏览器快速开始(扩展默认)
16
+ 1. 安装已发布包:
28
17
 
29
18
  ```powershell
30
19
  npm install --global @flrande/browserctl
20
+ # 或:pnpm add --global @flrande/browserctl
21
+ ```
31
22
 
23
+ 2. 用扩展 relay 模式启动 daemon:
24
+
25
+ ```powershell
32
26
  $env:BROWSERD_DEFAULT_DRIVER = "chrome-relay"
33
27
  $env:BROWSERD_CHROME_RELAY_MODE = "extension"
34
28
  $env:BROWSERD_CHROME_RELAY_URL = "http://127.0.0.1:9223"
35
- $env:BROWSERD_CHROME_RELAY_EXTENSION_TOKEN = "browserctl-relay"
36
-
29
+ $env:BROWSERD_CHROME_RELAY_EXTENSION_TOKEN = "relay-secret"
37
30
  browserctl daemon-stop --json
38
31
  browserctl daemon-start --json
32
+ ```
33
+
34
+ 3. 加载扩展:
35
+
36
+ 1. 打开 `edge://extensions` 或 `chrome://extensions`。
37
+ 2. 开启开发者模式。
38
+ 3. 选择“加载已解压的扩展程序”,目录为 `extensions/chrome-relay`。
39
+ 4. 在扩展弹窗中设置:
40
+ - `Bridge URL`: `ws://127.0.0.1:9223/bridge`
41
+ - `Token`: `relay-secret`
42
+ 5. 点击 `Save`,再点击 `Reconnect`。
43
+
44
+ 4. 连接验证与首个命令:
45
+
46
+ ```powershell
39
47
  browserctl status --json --session relay --profile chrome-relay
48
+ browserctl tab-open --json --session relay --profile chrome-relay https://example.com
49
+ ```
50
+
51
+ ## 30 秒排查
52
+
53
+ ```powershell
54
+ browserctl daemon-status --json
55
+ Invoke-WebRequest -UseBasicParsing -Uri "http://127.0.0.1:9223/browserctl/relay/status" -TimeoutSec 3
40
56
  ```
41
57
 
58
+ 若 `connected` 为 `false`,重新打开扩展弹窗并点击 `Reconnect`。
59
+
42
60
  ## 完整文档
43
61
 
44
- - [Install Guide (English)](INSTALL.md)
45
- - [安装指南(中文)](INSTALL-CN.md)
46
- - [User Guide (English)](docs/user-guide.md)
47
- - [使用指南(中文)](docs/user-guide-cn.md)
48
- - [CLI Reference (English)](docs/cli-reference.md)
49
- - [CLI 参考(中文)](docs/cli-reference-cn.md)
50
- - [Maintainer Onboarding (English)](docs/maintainer-onboarding.md)
51
- - [维护者上手文档(中文)](docs/maintainer-onboarding-cn.md)
62
+ - [Full User Guide (English)](docs/user-guide.md)
63
+ - [完整使用文档(中文)](docs/user-guide-cn.md)
64
+ - [Chrome Relay Extension README (English)](extensions/chrome-relay/README.md)
65
+ - [Chrome Relay 扩展文档(中文)](extensions/chrome-relay/README-CN.md)
66
+
package/README.md CHANGED
@@ -2,50 +2,65 @@
2
2
 
3
3
  English | [简体中文](README-CN.md)
4
4
 
5
- Extension-first browser control toolkit for coding agents.
5
+ Fastest path: control your current Edge/Chrome through the relay extension.
6
6
 
7
- ## Install In Agents
7
+ ## Extension Quick Start (Recommended)
8
8
 
9
- Paste this to your agent:
9
+ Prerequisites:
10
10
 
11
- ```text
12
- Fetch and follow instructions from https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/INSTALL.md
13
- ```
14
-
15
- Platform shortcuts:
16
-
17
- - Codex: `https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/.codex/INSTALL.md`
18
- - Claude Code: `https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/.claude-plugin/INSTALL.md`
19
- - Cursor: `https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/.cursor-plugin/INSTALL.md`
20
- - OpenCode: `https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/.opencode/INSTALL.md`
11
+ - Node.js 22+
12
+ - npm or pnpm
13
+ - PowerShell 7 (`pwsh`)
14
+ - Edge or Chrome
21
15
 
22
- Default skill:
23
-
24
- - Name: `browserctl`
25
- - Path: `skills/browserctl`
26
-
27
- ## Browser Quick Start (Extension Default)
16
+ 1. Install the released package:
28
17
 
29
18
  ```powershell
30
19
  npm install --global @flrande/browserctl
20
+ # or: pnpm add --global @flrande/browserctl
21
+ ```
31
22
 
23
+ 2. Start daemon in extension relay mode:
24
+
25
+ ```powershell
32
26
  $env:BROWSERD_DEFAULT_DRIVER = "chrome-relay"
33
27
  $env:BROWSERD_CHROME_RELAY_MODE = "extension"
34
28
  $env:BROWSERD_CHROME_RELAY_URL = "http://127.0.0.1:9223"
35
- $env:BROWSERD_CHROME_RELAY_EXTENSION_TOKEN = "browserctl-relay"
36
-
29
+ $env:BROWSERD_CHROME_RELAY_EXTENSION_TOKEN = "relay-secret"
37
30
  browserctl daemon-stop --json
38
31
  browserctl daemon-start --json
32
+ ```
33
+
34
+ 3. Load extension:
35
+
36
+ 1. Open `edge://extensions` or `chrome://extensions`.
37
+ 2. Enable Developer mode.
38
+ 3. Load unpacked extension from `extensions/chrome-relay`.
39
+ 4. In extension popup, set:
40
+ - `Bridge URL`: `ws://127.0.0.1:9223/bridge`
41
+ - `Token`: `relay-secret`
42
+ 5. Click `Save` then `Reconnect`.
43
+
44
+ 4. Verify and run first command:
45
+
46
+ ```powershell
39
47
  browserctl status --json --session relay --profile chrome-relay
48
+ browserctl tab-open --json --session relay --profile chrome-relay https://example.com
49
+ ```
50
+
51
+ ## 30-Second Troubleshooting
52
+
53
+ ```powershell
54
+ browserctl daemon-status --json
55
+ Invoke-WebRequest -UseBasicParsing -Uri "http://127.0.0.1:9223/browserctl/relay/status" -TimeoutSec 3
40
56
  ```
41
57
 
58
+ If `connected` is `false`, reopen extension popup and click `Reconnect`.
59
+
42
60
  ## Full Docs
43
61
 
44
- - [Install Guide (English)](INSTALL.md)
45
- - [安装指南(中文)](INSTALL-CN.md)
46
- - [User Guide (English)](docs/user-guide.md)
47
- - [使用指南(中文)](docs/user-guide-cn.md)
48
- - [CLI Reference (English)](docs/cli-reference.md)
49
- - [CLI 参考(中文)](docs/cli-reference-cn.md)
50
- - [Maintainer Onboarding (English)](docs/maintainer-onboarding.md)
51
- - [维护者上手文档(中文)](docs/maintainer-onboarding-cn.md)
62
+ - [Full User Guide (English)](docs/user-guide.md)
63
+ - [完整使用文档(中文)](docs/user-guide-cn.md)
64
+ - [Chrome Relay Extension README (English)](extensions/chrome-relay/README.md)
65
+ - [Chrome Relay 扩展文档(中文)](extensions/chrome-relay/README-CN.md)
66
+
@@ -2,10 +2,9 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest";
2
2
 
3
3
  import { stopDaemon } from "./daemon-client";
4
4
  import { EXIT_CODES, runCli } from "./main";
5
- import { reserveLoopbackPort } from "./test-port";
6
5
 
6
+ const TEST_DAEMON_PORT = "42491";
7
7
  const TEST_SESSION_ID = "session:e2e";
8
- let testDaemonPort = 0;
9
8
 
10
9
  function createIoCapture() {
11
10
  const state = {
@@ -35,20 +34,17 @@ function parseJsonLine(state: { stdout: string }): Record<string, unknown> {
35
34
  }
36
35
 
37
36
  beforeEach(async () => {
38
- testDaemonPort = await reserveLoopbackPort();
39
- process.env.BROWSERCTL_DAEMON_PORT = String(testDaemonPort);
37
+ process.env.BROWSERCTL_DAEMON_PORT = TEST_DAEMON_PORT;
40
38
  process.env.BROWSERD_MANAGED_LOCAL_ENABLED = "false";
41
39
  process.env.BROWSERD_DEFAULT_DRIVER = "managed";
42
- process.env.BROWSERD_CHROME_RELAY_MODE = "cdp";
43
- await stopDaemon(testDaemonPort);
40
+ await stopDaemon(Number.parseInt(TEST_DAEMON_PORT, 10));
44
41
  });
45
42
 
46
43
  afterEach(async () => {
47
- await stopDaemon(testDaemonPort);
44
+ await stopDaemon(Number.parseInt(TEST_DAEMON_PORT, 10));
48
45
  delete process.env.BROWSERCTL_DAEMON_PORT;
49
46
  delete process.env.BROWSERD_MANAGED_LOCAL_ENABLED;
50
47
  delete process.env.BROWSERD_DEFAULT_DRIVER;
51
- delete process.env.BROWSERD_CHROME_RELAY_MODE;
52
48
  });
53
49
 
54
50
  describe("browserctl e2e", () => {
@@ -61,7 +57,7 @@ describe("browserctl e2e", () => {
61
57
  expect(startPayload.ok).toBe(true);
62
58
  const startData = startPayload.data as Record<string, unknown>;
63
59
  expect(startData.running).toBe(true);
64
- expect(startData.port).toBe(testDaemonPort);
60
+ expect(startData.port).toBe(Number.parseInt(TEST_DAEMON_PORT, 10));
65
61
 
66
62
  const openCapture = createIoCapture();
67
63
  const openExitCode = await runCli(
@@ -0,0 +1,16 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { getSmokeCommand } from "./smoke";
4
+
5
+ describe("smoke wiring", () => {
6
+ it("returns a PowerShell command with required flags and default script path", () => {
7
+ const command = getSmokeCommand();
8
+
9
+ expect(command).toContain("pwsh");
10
+ expect(command).toContain("-NoLogo");
11
+ expect(command).toContain("-NoProfile");
12
+ expect(command).toContain("-NonInteractive");
13
+ expect(command).toContain("-File");
14
+ expect(command).toContain('".\\scripts\\smoke.ps1"');
15
+ });
16
+ });
@@ -0,0 +1,5 @@
1
+ const DEFAULT_SMOKE_SCRIPT_PATH = ".\\scripts\\smoke.ps1";
2
+
3
+ export function getSmokeCommand(scriptPath: string = DEFAULT_SMOKE_SCRIPT_PATH): string {
4
+ return `pwsh -NoLogo -NoProfile -NonInteractive -File "${scriptPath}"`;
5
+ }
@@ -1,8 +1,33 @@
1
+ import { createServer } from "node:net";
2
+
1
3
  import { afterEach, describe, expect, it } from "vitest";
2
4
  import { WebSocket } from "ws";
3
5
 
4
6
  import { createChromeRelayExtensionBridge } from "./chrome-relay-extension-bridge";
5
- import { reserveLoopbackPort } from "./test-port";
7
+
8
+ async function reservePort(): Promise<number> {
9
+ return await new Promise<number>((resolve, reject) => {
10
+ const server = createServer();
11
+ server.once("error", reject);
12
+ server.listen(0, "127.0.0.1", () => {
13
+ const address = server.address();
14
+ if (typeof address !== "object" || address === null) {
15
+ server.close(() => resolve(0));
16
+ return;
17
+ }
18
+
19
+ const port = address.port;
20
+ server.close((closeError) => {
21
+ if (closeError !== undefined) {
22
+ reject(closeError);
23
+ return;
24
+ }
25
+
26
+ resolve(port);
27
+ });
28
+ });
29
+ });
30
+ }
6
31
 
7
32
  async function waitForCondition(predicate: () => boolean, timeoutMs = 1000): Promise<void> {
8
33
  const start = Date.now();
@@ -36,7 +61,7 @@ afterEach(async () => {
36
61
 
37
62
  describe("createChromeRelayExtensionBridge", () => {
38
63
  it("requires token configuration for extension bridge startup", async () => {
39
- const port = await reserveLoopbackPort();
64
+ const port = await reservePort();
40
65
 
41
66
  expect(() =>
42
67
  createChromeRelayExtensionBridge({
@@ -46,7 +71,7 @@ describe("createChromeRelayExtensionBridge", () => {
46
71
  });
47
72
 
48
73
  it("sends requests to extension client and receives responses", async () => {
49
- const port = await reserveLoopbackPort();
74
+ const port = await reservePort();
50
75
  const bridge = createChromeRelayExtensionBridge({
51
76
  relayUrl: `http://127.0.0.1:${port}`,
52
77
  token: "secret-token",
@@ -109,7 +134,7 @@ describe("createChromeRelayExtensionBridge", () => {
109
134
  });
110
135
 
111
136
  it("rejects invoke when extension is not connected", async () => {
112
- const port = await reserveLoopbackPort();
137
+ const port = await reservePort();
113
138
  const bridge = createChromeRelayExtensionBridge({
114
139
  relayUrl: `http://127.0.0.1:${port}`,
115
140
  token: "secret-token",
@@ -123,7 +148,7 @@ describe("createChromeRelayExtensionBridge", () => {
123
148
  });
124
149
 
125
150
  it("enforces token validation for extension websocket connection", async () => {
126
- const port = await reserveLoopbackPort();
151
+ const port = await reservePort();
127
152
  const bridge = createChromeRelayExtensionBridge({
128
153
  relayUrl: `http://127.0.0.1:${port}`,
129
154
  token: "secret-token",
@@ -163,7 +188,7 @@ describe("createChromeRelayExtensionBridge", () => {
163
188
  });
164
189
 
165
190
  it("forwards extension event envelopes to bridge listeners", async () => {
166
- const port = await reserveLoopbackPort();
191
+ const port = await reservePort();
167
192
  const bridge = createChromeRelayExtensionBridge({
168
193
  relayUrl: `http://127.0.0.1:${port}`,
169
194
  token: "secret-token",
@@ -53,11 +53,9 @@ export type BrowserdConfig = {
53
53
 
54
54
  const DEFAULT_DRIVER_KEY = "managed";
55
55
  const MANAGED_LOCAL_DRIVER_KEY = "managed-local";
56
- const DEFAULT_CONFIG_DRIVER_KEY = "chrome-relay";
57
56
  const DEFAULT_MANAGED_LOCAL_BROWSER_NAME: ManagedLocalBrowserName = "chromium";
58
57
  const DEFAULT_MANAGED_LOCAL_HEADLESS = true;
59
- const DEFAULT_CHROME_RELAY_MODE: ChromeRelayMode = "extension";
60
- const DEFAULT_CHROME_RELAY_EXTENSION_TOKEN = "browserctl-relay";
58
+ const DEFAULT_CHROME_RELAY_MODE: ChromeRelayMode = "cdp";
61
59
  const DEFAULT_CHROME_RELAY_EXTENSION_REQUEST_TIMEOUT_MS = 5_000;
62
60
  const DEFAULT_NETWORK_WAIT_TIMEOUT_MS = 10_000;
63
61
  const DEFAULT_NETWORK_WAIT_POLL_MS = 200;
@@ -183,11 +181,7 @@ function parseManagedLocalBrowserName(value: string | undefined): ManagedLocalBr
183
181
 
184
182
  function parseChromeRelayMode(value: string | undefined): ChromeRelayMode {
185
183
  const normalizedValue = value?.trim().toLowerCase();
186
- if (normalizedValue === "extension" || normalizedValue === "cdp") {
187
- return normalizedValue;
188
- }
189
-
190
- return DEFAULT_CHROME_RELAY_MODE;
184
+ return normalizedValue === "extension" ? "extension" : DEFAULT_CHROME_RELAY_MODE;
191
185
  }
192
186
 
193
187
  export type BrowserdContainer = {
@@ -204,12 +198,16 @@ export function loadBrowserdConfig(
204
198
  ): BrowserdConfig {
205
199
  const managedLocalEnabled = parseBooleanFlag(env.BROWSERD_MANAGED_LOCAL_ENABLED, true);
206
200
  const chromeRelayMode = parseChromeRelayMode(env.BROWSERD_CHROME_RELAY_MODE);
207
- const chromeRelayExtensionToken =
208
- resolveNonEmptyString(env.BROWSERD_CHROME_RELAY_EXTENSION_TOKEN) ??
209
- (chromeRelayMode === "extension" ? DEFAULT_CHROME_RELAY_EXTENSION_TOKEN : undefined);
201
+ const chromeRelayExtensionToken = resolveNonEmptyString(env.BROWSERD_CHROME_RELAY_EXTENSION_TOKEN);
202
+ if (chromeRelayMode === "extension" && chromeRelayExtensionToken === undefined) {
203
+ throw new Error(
204
+ "BROWSERD_CHROME_RELAY_EXTENSION_TOKEN is required when BROWSERD_CHROME_RELAY_MODE=extension."
205
+ );
206
+ }
210
207
 
211
208
  const defaultDriver =
212
- resolveNonEmptyString(env.BROWSERD_DEFAULT_DRIVER) ?? DEFAULT_CONFIG_DRIVER_KEY;
209
+ resolveNonEmptyString(env.BROWSERD_DEFAULT_DRIVER) ??
210
+ (managedLocalEnabled ? MANAGED_LOCAL_DRIVER_KEY : DEFAULT_DRIVER_KEY);
213
211
 
214
212
  return {
215
213
  chromeRelayUrl: env.BROWSERD_CHROME_RELAY_URL ?? "http://127.0.0.1:9223",
@@ -1,10 +1,9 @@
1
- import { beforeEach, describe, expect, it } from "vitest";
1
+ import { describe, expect, it } from "vitest";
2
2
  import { PassThrough } from "node:stream";
3
3
  import { createConnection } from "node:net";
4
4
 
5
5
  import { createContainer, loadBrowserdConfig } from "./container";
6
6
  import { bootstrapBrowserd } from "./bootstrap";
7
- import { reserveLoopbackPort } from "./test-port";
8
7
 
9
8
  function waitForNextJsonLine(stream: PassThrough): Promise<Record<string, unknown>> {
10
9
  return new Promise((resolve, reject) => {
@@ -79,33 +78,11 @@ function sendTcpToolRequest(
79
78
  });
80
79
  }
81
80
 
82
- let testRelayPort = 0;
83
- let testTcpPort = 0;
84
-
85
- beforeEach(async () => {
86
- testRelayPort = await reserveLoopbackPort();
87
- testTcpPort = await reserveLoopbackPort();
88
- while (testTcpPort === testRelayPort) {
89
- testTcpPort = await reserveLoopbackPort();
90
- }
91
- });
92
-
93
- function createTestEnv(
94
- overrides: Record<string, string | undefined> = {}
95
- ): Record<string, string | undefined> {
96
- return {
97
- BROWSERD_CHROME_RELAY_URL: `http://127.0.0.1:${testRelayPort}`,
98
- ...overrides
99
- };
100
- }
101
-
102
81
  describe("browserd container", () => {
103
82
  it("uses secure defaults for new security config fields", () => {
104
83
  const config = loadBrowserdConfig({});
105
84
 
106
- expect(config.defaultDriver).toBe("chrome-relay");
107
- expect(config.chromeRelayMode).toBe("extension");
108
- expect(config.chromeRelayExtensionToken).toBe("browserctl-relay");
85
+ expect(config.defaultDriver).toBe("managed-local");
109
86
  expect(config.managedLocalEnabled).toBe(true);
110
87
  expect(config.uploadRoot).toBeUndefined();
111
88
  expect(config.downloadRoot).toBeUndefined();
@@ -114,13 +91,12 @@ describe("browserd container", () => {
114
91
  });
115
92
 
116
93
  it("registers managed-local, managed, chrome-relay, and remote-cdp drivers by default", () => {
117
- const c = createContainer(loadBrowserdConfig(createTestEnv()));
94
+ const c = createContainer();
118
95
 
119
96
  expect(c.drivers.has("managed-local")).toBe(true);
120
97
  expect(c.drivers.has("managed")).toBe(true);
121
98
  expect(c.drivers.has("chrome-relay")).toBe(true);
122
99
  expect(c.drivers.has("remote-cdp")).toBe(true);
123
- c.close();
124
100
  });
125
101
 
126
102
  it("uses env override in loadBrowserdConfig", () => {
@@ -134,7 +110,7 @@ describe("browserd container", () => {
134
110
 
135
111
  expect(config.remoteCdpUrl).toBe("http://127.0.0.1:9333/devtools/browser/override");
136
112
  expect(config.chromeRelayUrl).toBe("http://127.0.0.1:9223");
137
- expect(config.defaultDriver).toBe("chrome-relay");
113
+ expect(config.defaultDriver).toBe("managed-local");
138
114
  expect(config.managedLocalEnabled).toBe(true);
139
115
  expect(config.uploadRoot).toBe("C:\\safe\\uploads");
140
116
  expect(config.downloadRoot).toBe("C:\\safe\\downloads");
@@ -156,42 +132,31 @@ describe("browserd container", () => {
156
132
  expect(config.chromeRelayExtensionRequestTimeoutMs).toBe(7000);
157
133
  });
158
134
 
159
- it("respects explicit cdp relay mode override", () => {
160
- const config = loadBrowserdConfig({
161
- BROWSERD_CHROME_RELAY_MODE: "cdp"
162
- });
163
-
164
- expect(config.chromeRelayMode).toBe("cdp");
165
- });
166
-
167
- it("uses default extension relay token when extension mode is enabled", () => {
168
- const config = loadBrowserdConfig({
169
- BROWSERD_CHROME_RELAY_MODE: "extension"
170
- });
171
-
172
- expect(config.chromeRelayMode).toBe("extension");
173
- expect(config.chromeRelayExtensionToken).toBe("browserctl-relay");
135
+ it("requires extension relay token when extension mode is enabled", () => {
136
+ expect(() =>
137
+ loadBrowserdConfig({
138
+ BROWSERD_CHROME_RELAY_MODE: "extension"
139
+ })
140
+ ).toThrow("BROWSERD_CHROME_RELAY_EXTENSION_TOKEN");
174
141
  });
175
142
 
176
- it("keeps chrome-relay as default when managed-local is explicitly disabled", () => {
143
+ it("defaults to managed when managed-local is explicitly disabled", () => {
177
144
  const config = loadBrowserdConfig({
178
145
  BROWSERD_MANAGED_LOCAL_ENABLED: "false"
179
146
  });
180
147
 
181
148
  expect(config.managedLocalEnabled).toBe(false);
182
- expect(config.defaultDriver).toBe("chrome-relay");
149
+ expect(config.defaultDriver).toBe("managed");
183
150
  });
184
151
 
185
152
  it("does not register managed-local driver when explicitly disabled", () => {
186
153
  const c = createContainer(
187
154
  loadBrowserdConfig({
188
- ...createTestEnv(),
189
155
  BROWSERD_MANAGED_LOCAL_ENABLED: "false"
190
156
  })
191
157
  );
192
158
 
193
159
  expect(c.drivers.has("managed-local")).toBe(false);
194
- c.close();
195
160
  });
196
161
  });
197
162
 
@@ -200,7 +165,7 @@ describe("browserd bootstrap", () => {
200
165
  const input = new PassThrough();
201
166
  const output = new PassThrough();
202
167
 
203
- const runtime = bootstrapBrowserd({ env: createTestEnv(), input, output });
168
+ const runtime = bootstrapBrowserd({ input, output });
204
169
 
205
170
  expect(runtime.mcpStdioStarted).toBe(true);
206
171
  expect(runtime.container.drivers.has("managed")).toBe(true);
@@ -214,13 +179,13 @@ describe("browserd bootstrap", () => {
214
179
  });
215
180
 
216
181
  it("supports tcp transport mode for persistent daemon use", async () => {
217
- const port = testTcpPort;
182
+ const port = 41419;
218
183
  const runtime = bootstrapBrowserd({
219
- env: createTestEnv({
184
+ env: {
220
185
  BROWSERD_TRANSPORT: "tcp",
221
186
  BROWSERD_PORT: String(port),
222
187
  BROWSERD_AUTH_TOKEN: "tcp-token"
223
- })
188
+ }
224
189
  });
225
190
 
226
191
  const response = await sendTcpToolRequest(port, {
@@ -250,10 +215,10 @@ describe("browserd bootstrap", () => {
250
215
  try {
251
216
  expect(() => {
252
217
  runtime = bootstrapBrowserd({
253
- env: createTestEnv({
218
+ env: {
254
219
  BROWSERD_TRANSPORT: "tcp",
255
- BROWSERD_PORT: String(testTcpPort)
256
- })
220
+ BROWSERD_PORT: "41420"
221
+ }
257
222
  });
258
223
  }).toThrow("BROWSERD_AUTH_TOKEN");
259
224
  } finally {
@@ -264,7 +229,7 @@ describe("browserd bootstrap", () => {
264
229
  it("processes line-delimited requests and writes response with id", async () => {
265
230
  const input = new PassThrough();
266
231
  const output = new PassThrough();
267
- const runtime = bootstrapBrowserd({ env: createTestEnv(), input, output, stdioProtocol: "legacy" });
232
+ const runtime = bootstrapBrowserd({ input, output, stdioProtocol: "legacy" });
268
233
 
269
234
  const response = await sendToolRequest(input, output, {
270
235
  id: "request-1",
@@ -282,7 +247,7 @@ describe("browserd bootstrap", () => {
282
247
  expect(response.data).toMatchObject({
283
248
  kind: "browserd",
284
249
  ready: true,
285
- driver: "chrome-relay"
250
+ driver: "managed-local"
286
251
  });
287
252
 
288
253
  runtime.close();
@@ -294,10 +259,10 @@ describe("browserd bootstrap", () => {
294
259
  const input = new PassThrough();
295
260
  const output = new PassThrough();
296
261
  const runtime = bootstrapBrowserd({
297
- env: createTestEnv({
262
+ env: {
298
263
  BROWSERD_MANAGED_LOCAL_ENABLED: "true",
299
264
  BROWSERD_DEFAULT_DRIVER: "managed-local"
300
- }),
265
+ },
301
266
  input,
302
267
  output,
303
268
  stdioProtocol: "legacy"
@@ -332,10 +297,10 @@ describe("browserd bootstrap", () => {
332
297
  const input = new PassThrough();
333
298
  const output = new PassThrough();
334
299
  const runtime = bootstrapBrowserd({
335
- env: createTestEnv({
300
+ env: {
336
301
  BROWSERD_DEFAULT_DRIVER: "managed-local",
337
302
  BROWSERD_MANAGED_LOCAL_ENABLED: "false"
338
- }),
303
+ },
339
304
  input,
340
305
  output,
341
306
  stdioProtocol: "legacy"
@@ -366,9 +331,9 @@ describe("browserd bootstrap", () => {
366
331
  const input = new PassThrough();
367
332
  const output = new PassThrough();
368
333
  const runtime = bootstrapBrowserd({
369
- env: createTestEnv({
334
+ env: {
370
335
  BROWSERD_AUTH_TOKEN: "secret-token"
371
- }),
336
+ },
372
337
  input,
373
338
  output,
374
339
  stdioProtocol: "legacy"
@@ -397,9 +362,9 @@ describe("browserd bootstrap", () => {
397
362
  const input = new PassThrough();
398
363
  const output = new PassThrough();
399
364
  const runtime = bootstrapBrowserd({
400
- env: createTestEnv({
365
+ env: {
401
366
  BROWSERD_AUTH_TOKEN: "secret-token"
402
- }),
367
+ },
403
368
  input,
404
369
  output,
405
370
  stdioProtocol: "legacy"
@@ -429,11 +394,11 @@ describe("browserd bootstrap", () => {
429
394
  const input = new PassThrough();
430
395
  const output = new PassThrough();
431
396
  const runtime = bootstrapBrowserd({
432
- env: createTestEnv({
397
+ env: {
433
398
  BROWSERD_DEFAULT_DRIVER: "managed",
434
399
  BROWSERD_AUTH_TOKEN: "secret-token",
435
400
  BROWSERD_AUTH_SCOPES: "read"
436
- }),
401
+ },
437
402
  input,
438
403
  output,
439
404
  stdioProtocol: "legacy"
@@ -464,11 +429,11 @@ describe("browserd bootstrap", () => {
464
429
  const input = new PassThrough();
465
430
  const output = new PassThrough();
466
431
  const runtime = bootstrapBrowserd({
467
- env: createTestEnv({
432
+ env: {
468
433
  BROWSERD_DEFAULT_DRIVER: "managed",
469
434
  BROWSERD_AUTH_TOKEN: "secret-token",
470
435
  BROWSERD_AUTH_SCOPES: "read,act"
471
- }),
436
+ },
472
437
  input,
473
438
  output,
474
439
  stdioProtocol: "legacy"
@@ -529,10 +494,10 @@ describe("browserd bootstrap", () => {
529
494
  const input = new PassThrough();
530
495
  const output = new PassThrough();
531
496
  const runtime = bootstrapBrowserd({
532
- env: createTestEnv({
497
+ env: {
533
498
  BROWSERD_DEFAULT_DRIVER: "managed",
534
499
  BROWSERD_UPLOAD_ROOT: "C:\\allowed\\uploads"
535
- }),
500
+ },
536
501
  input,
537
502
  output,
538
503
  stdioProtocol: "legacy"
@@ -574,9 +539,9 @@ describe("browserd bootstrap", () => {
574
539
  const input = new PassThrough();
575
540
  const output = new PassThrough();
576
541
  const runtime = bootstrapBrowserd({
577
- env: createTestEnv({
542
+ env: {
578
543
  BROWSERD_DEFAULT_DRIVER: "managed"
579
- }),
544
+ },
580
545
  input,
581
546
  output,
582
547
  stdioProtocol: "legacy"
@@ -619,10 +584,10 @@ describe("browserd bootstrap", () => {
619
584
  const input = new PassThrough();
620
585
  const output = new PassThrough();
621
586
  const runtime = bootstrapBrowserd({
622
- env: createTestEnv({
587
+ env: {
623
588
  BROWSERD_DEFAULT_DRIVER: "managed",
624
589
  BROWSERD_DOWNLOAD_ROOT: "C:\\allowed\\downloads"
625
- }),
590
+ },
626
591
  input,
627
592
  output,
628
593
  stdioProtocol: "legacy"
@@ -681,9 +646,9 @@ describe("browserd bootstrap", () => {
681
646
  const input = new PassThrough();
682
647
  const output = new PassThrough();
683
648
  const runtime = bootstrapBrowserd({
684
- env: createTestEnv({
649
+ env: {
685
650
  BROWSERD_DEFAULT_DRIVER: "managed"
686
- }),
651
+ },
687
652
  input,
688
653
  output,
689
654
  stdioProtocol: "legacy"
@@ -725,9 +690,9 @@ describe("browserd bootstrap", () => {
725
690
  const input = new PassThrough();
726
691
  const output = new PassThrough();
727
692
  const runtime = bootstrapBrowserd({
728
- env: createTestEnv({
693
+ env: {
729
694
  BROWSERD_DEFAULT_DRIVER: "managed"
730
- }),
695
+ },
731
696
  input,
732
697
  output,
733
698
  stdioProtocol: "legacy"
@@ -774,9 +739,9 @@ describe("browserd bootstrap", () => {
774
739
  const input = new PassThrough();
775
740
  const output = new PassThrough();
776
741
  const runtime = bootstrapBrowserd({
777
- env: createTestEnv({
742
+ env: {
778
743
  BROWSERD_DEFAULT_DRIVER: "managed"
779
- }),
744
+ },
780
745
  input,
781
746
  output,
782
747
  stdioProtocol: "legacy"
@@ -854,7 +819,7 @@ describe("browserd bootstrap", () => {
854
819
  it("preserves id and trace/session metadata when queue-level error handling runs", async () => {
855
820
  const input = new PassThrough();
856
821
  const output = new PassThrough();
857
- const runtime = bootstrapBrowserd({ env: createTestEnv(), input, output, stdioProtocol: "legacy" });
822
+ const runtime = bootstrapBrowserd({ input, output, stdioProtocol: "legacy" });
858
823
 
859
824
  const originalWrite = output.write.bind(output);
860
825
  let shouldThrow = true;
@@ -3,7 +3,6 @@ import { PassThrough } from "node:stream";
3
3
  import { describe, expect, it } from "vitest";
4
4
 
5
5
  import { bootstrapBrowserd } from "./bootstrap";
6
- import { reserveLoopbackPort } from "./test-port";
7
6
 
8
7
  function waitForNextJsonLine(stream: PassThrough): Promise<Record<string, unknown>> {
9
8
  return new Promise((resolve, reject) => {
@@ -41,15 +40,12 @@ function sendToolRequest(
41
40
  return responsePromise;
42
41
  }
43
42
 
44
- async function createManagedLegacyRuntime() {
45
- const relayPort = await reserveLoopbackPort();
46
-
43
+ function createManagedLegacyRuntime() {
47
44
  const input = new PassThrough();
48
45
  const output = new PassThrough();
49
46
  const runtime = bootstrapBrowserd({
50
47
  env: {
51
- BROWSERD_DEFAULT_DRIVER: "managed",
52
- BROWSERD_CHROME_RELAY_URL: `http://127.0.0.1:${relayPort}`
48
+ BROWSERD_DEFAULT_DRIVER: "managed"
53
49
  },
54
50
  input,
55
51
  output,
@@ -65,7 +61,7 @@ async function createManagedLegacyRuntime() {
65
61
 
66
62
  describe("browserd tool matrix", () => {
67
63
  it("returns E_INVALID_ARG for missing required arguments on routed tools", async () => {
68
- const { input, output, runtime } = await createManagedLegacyRuntime();
64
+ const { input, output, runtime } = createManagedLegacyRuntime();
69
65
 
70
66
  try {
71
67
  const toolNames = [
@@ -113,7 +109,7 @@ describe("browserd tool matrix", () => {
113
109
  });
114
110
 
115
111
  it("returns E_DRIVER_UNAVAILABLE for structured action tools on managed driver", async () => {
116
- const { input, output, runtime } = await createManagedLegacyRuntime();
112
+ const { input, output, runtime } = createManagedLegacyRuntime();
117
113
 
118
114
  try {
119
115
  const openResponse = await sendToolRequest(input, output, {
@@ -239,7 +235,7 @@ describe("browserd tool matrix", () => {
239
235
  });
240
236
 
241
237
  it("routes managed-driver tools including profile.use, act, focus/close, and waitFor timeout", async () => {
242
- const { input, output, runtime } = await createManagedLegacyRuntime();
238
+ const { input, output, runtime } = createManagedLegacyRuntime();
243
239
 
244
240
  try {
245
241
  const profileUseResponse = await sendToolRequest(input, output, {
@@ -23,7 +23,6 @@
23
23
  默认 Bridge URL:
24
24
 
25
25
  - `ws://127.0.0.1:9223/bridge`
26
- - 默认 token(当 daemon 使用默认配置时):`browserctl-relay`
27
26
 
28
27
  ## 弹窗故障排查
29
28
 
@@ -23,7 +23,6 @@ This extension uses the `debugger` permission to collect console/network telemet
23
23
  Default bridge URL:
24
24
 
25
25
  - `ws://127.0.0.1:9223/bridge`
26
- - Default token (if daemon uses defaults): `browserctl-relay`
27
26
 
28
27
  ## Troubleshooting In Popup
29
28
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flrande/browserctl",
3
- "version": "0.2.0-dev.12.1",
3
+ "version": "0.2.0",
4
4
  "private": false,
5
5
  "bin": {
6
6
  "browserctl": "bin/browserctl.cjs",
@@ -16,8 +16,7 @@
16
16
  "packages/protocol/src",
17
17
  "packages/transport-mcp-stdio/src",
18
18
  "extensions/chrome-relay",
19
- "INSTALL.md",
20
- "INSTALL-CN.md",
19
+ "scripts/smoke.ps1",
21
20
  "bin",
22
21
  "README.md",
23
22
  "README-CN.md",
@@ -44,9 +43,8 @@
44
43
  "test:unit": "vitest run --config vitest.config.ts",
45
44
  "test:contract": "vitest run --config vitest.contract.config.ts",
46
45
  "test:e2e": "vitest run --config vitest.e2e.config.ts",
47
- "test:smoke": "vitest run --config vitest.smoke.config.ts",
48
- "test:all": "pnpm run test:unit && pnpm run test:contract && pnpm run test:e2e && pnpm run test:smoke",
49
- "build": "npm pack --dry-run",
46
+ "test:all": "pnpm run test:unit && pnpm run test:contract && pnpm run test:e2e",
47
+ "build": "pnpm publish --dry-run --no-git-checks",
50
48
  "typecheck": "pnpm exec tsc --noEmit -p tsconfig.typecheck.json",
51
49
  "lint": "node ./scripts/lint.mjs"
52
50
  }
@@ -0,0 +1,127 @@
1
+ $ErrorActionPreference = 'Stop'
2
+ Set-StrictMode -Version Latest
3
+
4
+ $scriptDir = Split-Path -Parent $PSCommandPath
5
+ $repoRoot = Resolve-Path -LiteralPath (Join-Path $scriptDir '..')
6
+ $browserdEntry = Join-Path $repoRoot 'apps/browserd/src/main.ts'
7
+ $browserctlEntry = Join-Path $repoRoot 'apps/browserctl/src/main.ts'
8
+
9
+ $daemonProcess = $null
10
+ $daemonPort = 41337
11
+ $timeoutMs = 10000
12
+ $pollIntervalMs = 250
13
+ $pnpmExecutable = if ($IsWindows) { 'pnpm.cmd' } else { 'pnpm' }
14
+
15
+ function Invoke-SmokeStatus {
16
+ param(
17
+ [Parameter(Mandatory = $true)]
18
+ [string]$BrowserctlEntry
19
+ )
20
+
21
+ $previousPort = $env:BROWSERCTL_DAEMON_PORT
22
+ $env:BROWSERCTL_DAEMON_PORT = "$daemonPort"
23
+
24
+ $statusOutput = & pnpm exec tsx $BrowserctlEntry status --json 2>&1
25
+ $exitCode = $LASTEXITCODE
26
+ $statusText = [string]::Join([Environment]::NewLine, @($statusOutput))
27
+
28
+ if ($null -eq $previousPort) {
29
+ Remove-Item Env:\BROWSERCTL_DAEMON_PORT -ErrorAction SilentlyContinue
30
+ } else {
31
+ $env:BROWSERCTL_DAEMON_PORT = $previousPort
32
+ }
33
+
34
+ if ($exitCode -ne 0) {
35
+ return @{
36
+ Ok = $false
37
+ Reason = "browserctl status exited with code $exitCode. Output: $statusText"
38
+ }
39
+ }
40
+
41
+ $statusPayload = $null
42
+ try {
43
+ $statusPayload = $statusText | ConvertFrom-Json -ErrorAction Stop
44
+ } catch {
45
+ return @{
46
+ Ok = $false
47
+ Reason = "browserctl status returned invalid JSON. Output: $statusText"
48
+ }
49
+ }
50
+
51
+ if (-not $statusPayload.ok) {
52
+ return @{
53
+ Ok = $false
54
+ Reason = 'browserctl status returned ok=false.'
55
+ }
56
+ }
57
+
58
+ $errorProperty = $statusPayload.PSObject.Properties['error']
59
+ if ($null -ne $errorProperty -and $null -ne $errorProperty.Value) {
60
+ $errorJson = $errorProperty.Value | ConvertTo-Json -Compress
61
+ return @{
62
+ Ok = $false
63
+ Reason = "browserctl status returned error payload: $errorJson"
64
+ }
65
+ }
66
+
67
+ return @{
68
+ Ok = $true
69
+ Payload = $statusPayload
70
+ }
71
+ }
72
+
73
+ try {
74
+ $previousPort = $env:BROWSERCTL_DAEMON_PORT
75
+ $env:BROWSERCTL_DAEMON_PORT = "$daemonPort"
76
+ & pnpm exec tsx $browserctlEntry daemon-stop --json *> $null
77
+ if ($null -eq $previousPort) {
78
+ Remove-Item Env:\BROWSERCTL_DAEMON_PORT -ErrorAction SilentlyContinue
79
+ } else {
80
+ $env:BROWSERCTL_DAEMON_PORT = $previousPort
81
+ }
82
+
83
+ $daemonEnv = @{
84
+ BROWSERD_TRANSPORT = 'tcp'
85
+ BROWSERD_PORT = "$daemonPort"
86
+ }
87
+
88
+ $daemonProcess = Start-Process -FilePath $pnpmExecutable `
89
+ -ArgumentList @('exec', 'tsx', $browserdEntry) `
90
+ -WorkingDirectory $repoRoot `
91
+ -Environment $daemonEnv `
92
+ -PassThru
93
+
94
+ $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
95
+ $lastFailure = 'status probe did not run'
96
+ $statusResult = $null
97
+
98
+ while ($stopwatch.ElapsedMilliseconds -lt $timeoutMs) {
99
+ $statusResult = Invoke-SmokeStatus -BrowserctlEntry $browserctlEntry
100
+ if ($statusResult.Ok) {
101
+ break
102
+ }
103
+
104
+ $lastFailure = [string]$statusResult.Reason
105
+ Start-Sleep -Milliseconds $pollIntervalMs
106
+ }
107
+
108
+ if ($null -eq $statusResult -or -not $statusResult.Ok) {
109
+ $elapsedMs = [int]$stopwatch.ElapsedMilliseconds
110
+ throw "Smoke failed: browserctl status --json was not ready within ${timeoutMs}ms (elapsed ${elapsedMs}ms). Last failure: $lastFailure"
111
+ }
112
+
113
+ Write-Host 'Smoke passed: daemon started and browserctl status --json returned success.'
114
+ } finally {
115
+ if ($null -ne $daemonProcess -and -not $daemonProcess.HasExited) {
116
+ Stop-Process -Id $daemonProcess.Id -ErrorAction SilentlyContinue
117
+ }
118
+
119
+ $previousPort = $env:BROWSERCTL_DAEMON_PORT
120
+ $env:BROWSERCTL_DAEMON_PORT = "$daemonPort"
121
+ & pnpm exec tsx $browserctlEntry daemon-stop --json *> $null
122
+ if ($null -eq $previousPort) {
123
+ Remove-Item Env:\BROWSERCTL_DAEMON_PORT -ErrorAction SilentlyContinue
124
+ } else {
125
+ $env:BROWSERCTL_DAEMON_PORT = $previousPort
126
+ }
127
+ }
package/INSTALL-CN.md DELETED
@@ -1,28 +0,0 @@
1
- # 安装 browserctl Skills
2
-
3
- [English](INSTALL.md) | 简体中文
4
-
5
- 本仓库发布一个默认可安装 skill:
6
-
7
- - `skills/browserctl`
8
-
9
- ## 给 Agent 的主提示词
10
-
11
- 把下面文本贴给 agent:
12
-
13
- ```text
14
- 请抓取并遵循 https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/INSTALL.md 中的安装说明。
15
- ```
16
-
17
- ## 平台安装入口
18
-
19
- - Codex:`https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/.codex/INSTALL.md`
20
- - Claude Code:`https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/.claude-plugin/INSTALL.md`
21
- - Cursor:`https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/.cursor-plugin/INSTALL.md`
22
- - OpenCode:`https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/.opencode/INSTALL.md`
23
-
24
- ## 兜底约定
25
-
26
- 如果平台安装入口不可用,agent 应直接抓取并遵循:
27
-
28
- - `https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/skills/browserctl/SKILL.md`
package/INSTALL.md DELETED
@@ -1,28 +0,0 @@
1
- # Install browserctl Skills
2
-
3
- English | [简体中文](INSTALL-CN.md)
4
-
5
- This repository publishes one default installable skill:
6
-
7
- - `skills/browserctl`
8
-
9
- ## Primary Agent Prompt
10
-
11
- Paste this to your agent:
12
-
13
- ```text
14
- Fetch and follow instructions from https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/INSTALL.md
15
- ```
16
-
17
- ## Platform Guides
18
-
19
- - Codex: `https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/.codex/INSTALL.md`
20
- - Claude Code: `https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/.claude-plugin/INSTALL.md`
21
- - Cursor: `https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/.cursor-plugin/INSTALL.md`
22
- - OpenCode: `https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/.opencode/INSTALL.md`
23
-
24
- ## Fallback Contract
25
-
26
- If platform-specific install is unavailable, agents should fetch and follow:
27
-
28
- - `https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/skills/browserctl/SKILL.md`
@@ -1,97 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
2
-
3
- import { stopDaemon } from "./daemon-client";
4
- import { EXIT_CODES, runCli } from "./main";
5
- import { reserveLoopbackPort } from "./test-port";
6
-
7
- let testDaemonPort = 0;
8
- let activeRelayPort = 0;
9
-
10
- function createIoCapture() {
11
- const state = {
12
- stdout: "",
13
- stderr: ""
14
- };
15
-
16
- return {
17
- state,
18
- io: {
19
- stdout: {
20
- write(content: string) {
21
- state.stdout += content;
22
- }
23
- },
24
- stderr: {
25
- write(content: string) {
26
- state.stderr += content;
27
- }
28
- }
29
- }
30
- };
31
- }
32
-
33
- function parseJsonLine(state: { stdout: string }): Record<string, unknown> {
34
- return JSON.parse(state.stdout.trim()) as Record<string, unknown>;
35
- }
36
-
37
- beforeEach(async () => {
38
- testDaemonPort = await reserveLoopbackPort();
39
- activeRelayPort = await reserveLoopbackPort();
40
- while (activeRelayPort === testDaemonPort) {
41
- activeRelayPort = await reserveLoopbackPort();
42
- }
43
-
44
- process.env.BROWSERCTL_DAEMON_PORT = String(testDaemonPort);
45
- process.env.BROWSERD_CHROME_RELAY_URL = `http://127.0.0.1:${activeRelayPort}`;
46
- delete process.env.BROWSERD_MANAGED_LOCAL_ENABLED;
47
- delete process.env.BROWSERD_DEFAULT_DRIVER;
48
- delete process.env.BROWSERD_CHROME_RELAY_MODE;
49
- delete process.env.BROWSERD_CHROME_RELAY_EXTENSION_TOKEN;
50
- await stopDaemon(testDaemonPort);
51
- });
52
-
53
- afterEach(async () => {
54
- await stopDaemon(testDaemonPort);
55
- delete process.env.BROWSERCTL_DAEMON_PORT;
56
- delete process.env.BROWSERD_CHROME_RELAY_URL;
57
- delete process.env.BROWSERD_MANAGED_LOCAL_ENABLED;
58
- delete process.env.BROWSERD_DEFAULT_DRIVER;
59
- delete process.env.BROWSERD_CHROME_RELAY_MODE;
60
- delete process.env.BROWSERD_CHROME_RELAY_EXTENSION_TOKEN;
61
- });
62
-
63
- describe("browserctl smoke e2e", () => {
64
- it("starts with extension-first defaults and reports chrome-relay status", async () => {
65
- const startCapture = createIoCapture();
66
- const startExitCode = await runCli(["daemon-start", "--json"], startCapture.io);
67
-
68
- expect(startExitCode).toBe(EXIT_CODES.OK);
69
- const startPayload = parseJsonLine(startCapture.state);
70
- expect(startPayload.ok).toBe(true);
71
-
72
- const statusCapture = createIoCapture();
73
- const statusExitCode = await runCli(["status", "--json"], statusCapture.io);
74
-
75
- expect(statusExitCode).toBe(EXIT_CODES.OK);
76
- const statusPayload = parseJsonLine(statusCapture.state);
77
- expect(statusPayload.ok).toBe(true);
78
- const statusData = statusPayload.data as Record<string, unknown>;
79
- expect(statusData.driver).toBe("chrome-relay");
80
- expect(statusData.status).toMatchObject({
81
- kind: "chrome-relay",
82
- connected: false
83
- });
84
- const relayStatus = statusData.status as Record<string, unknown>;
85
- expect(String(relayStatus.relayUrl ?? "")).toContain(`:${activeRelayPort}`);
86
-
87
- const stopCapture = createIoCapture();
88
- const stopExitCode = await runCli(["daemon-stop", "--json"], stopCapture.io);
89
- expect(stopExitCode).toBe(EXIT_CODES.OK);
90
- const stopPayload = parseJsonLine(stopCapture.state);
91
- expect(stopPayload.ok).toBe(true);
92
- expect(stopPayload.data).toMatchObject({
93
- stopped: true,
94
- port: testDaemonPort
95
- });
96
- }, 20_000);
97
- });
@@ -1,26 +0,0 @@
1
- import { createServer } from "node:net";
2
-
3
- export async function reserveLoopbackPort(): Promise<number> {
4
- return await new Promise<number>((resolve, reject) => {
5
- const server = createServer();
6
-
7
- server.once("error", reject);
8
- server.listen(0, "127.0.0.1", () => {
9
- const address = server.address();
10
- if (typeof address !== "object" || address === null) {
11
- server.close(() => reject(new Error("Failed to reserve loopback port.")));
12
- return;
13
- }
14
-
15
- const port = address.port;
16
- server.close((error) => {
17
- if (error !== undefined) {
18
- reject(error);
19
- return;
20
- }
21
-
22
- resolve(port);
23
- });
24
- });
25
- });
26
- }
@@ -1,26 +0,0 @@
1
- import { createServer } from "node:net";
2
-
3
- export async function reserveLoopbackPort(): Promise<number> {
4
- return await new Promise<number>((resolve, reject) => {
5
- const server = createServer();
6
-
7
- server.once("error", reject);
8
- server.listen(0, "127.0.0.1", () => {
9
- const address = server.address();
10
- if (typeof address !== "object" || address === null) {
11
- server.close(() => reject(new Error("Failed to reserve loopback port.")));
12
- return;
13
- }
14
-
15
- const port = address.port;
16
- server.close((error) => {
17
- if (error !== undefined) {
18
- reject(error);
19
- return;
20
- }
21
-
22
- resolve(port);
23
- });
24
- });
25
- });
26
- }