@flrande/browserctl 0.2.0 → 0.3.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/INSTALL-CN.md +92 -0
- package/INSTALL.md +92 -0
- package/README-CN.md +42 -39
- package/README.md +42 -39
- package/apps/browserctl/src/e2e.test.ts +9 -5
- package/apps/browserctl/src/smoke.e2e.test.ts +97 -0
- package/apps/browserctl/src/test-port.ts +26 -0
- package/apps/browserd/src/chrome-relay-extension-bridge.test.ts +6 -31
- package/apps/browserd/src/container.ts +12 -10
- package/apps/browserd/src/main.test.ts +81 -46
- package/apps/browserd/src/test-port.ts +26 -0
- package/apps/browserd/src/tool-matrix.test.ts +9 -5
- package/extensions/chrome-relay/README-CN.md +1 -0
- package/extensions/chrome-relay/README.md +1 -0
- package/package.json +6 -4
- package/apps/browserctl/src/smoke.test.ts +0 -16
- package/apps/browserctl/src/smoke.ts +0 -5
- package/scripts/smoke.ps1 +0 -127
package/INSTALL-CN.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
默认推荐使用“仓库同源更新”,保证 skill、扩展和 CLI 版本一致。
|
|
27
|
+
|
|
28
|
+
PowerShell 7:
|
|
29
|
+
|
|
30
|
+
```powershell
|
|
31
|
+
$repoPath = Join-Path $env:USERPROFILE ".codex\\browserctl" # 按你的本地克隆路径调整
|
|
32
|
+
|
|
33
|
+
git -C $repoPath pull --ff-only
|
|
34
|
+
npm install --global "$repoPath"
|
|
35
|
+
|
|
36
|
+
browserctl daemon-stop --json
|
|
37
|
+
browserctl daemon-start --json
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Bash:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
repoPath="$HOME/.codex/browserctl" # 按你的本地克隆路径调整
|
|
44
|
+
|
|
45
|
+
git -C "$repoPath" pull --ff-only
|
|
46
|
+
npm install --global "$repoPath"
|
|
47
|
+
|
|
48
|
+
browserctl daemon-stop --json
|
|
49
|
+
browserctl daemon-start --json
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
拉取后需要在 `edge://extensions` 或 `chrome://extensions` 里刷新扩展:
|
|
53
|
+
|
|
54
|
+
1. 打开扩展管理页。
|
|
55
|
+
2. 找到 `BrowserCtl Relay`。
|
|
56
|
+
3. 点击 `Reload`。
|
|
57
|
+
|
|
58
|
+
验证命令:
|
|
59
|
+
|
|
60
|
+
PowerShell 7:
|
|
61
|
+
|
|
62
|
+
```powershell
|
|
63
|
+
browserctl status --json --session relay --profile chrome-relay
|
|
64
|
+
Invoke-WebRequest -UseBasicParsing -Uri "http://127.0.0.1:9223/browserctl/relay/status" -TimeoutSec 3
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Bash:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
browserctl status --json --session relay --profile chrome-relay
|
|
71
|
+
curl --fail --max-time 3 "http://127.0.0.1:9223/browserctl/relay/status"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
可选兜底(非默认):仅从 npm 仓库更新 CLI。
|
|
75
|
+
|
|
76
|
+
PowerShell 7:
|
|
77
|
+
|
|
78
|
+
```powershell
|
|
79
|
+
npm install --global @flrande/browserctl@latest
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Bash:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
npm install --global @flrande/browserctl@latest
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## 兜底约定
|
|
89
|
+
|
|
90
|
+
如果平台安装入口不可用,agent 应直接抓取并遵循:
|
|
91
|
+
|
|
92
|
+
- `https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/skills/browserctl/SKILL.md`
|
package/INSTALL.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
## Update After Install
|
|
25
|
+
|
|
26
|
+
Recommended default: use repository-synced updates so skill, extension, and CLI stay aligned.
|
|
27
|
+
|
|
28
|
+
PowerShell 7:
|
|
29
|
+
|
|
30
|
+
```powershell
|
|
31
|
+
$repoPath = Join-Path $env:USERPROFILE ".codex\\browserctl" # adjust to your local clone path
|
|
32
|
+
|
|
33
|
+
git -C $repoPath pull --ff-only
|
|
34
|
+
npm install --global "$repoPath"
|
|
35
|
+
|
|
36
|
+
browserctl daemon-stop --json
|
|
37
|
+
browserctl daemon-start --json
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Bash:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
repoPath="$HOME/.codex/browserctl" # adjust to your local clone path
|
|
44
|
+
|
|
45
|
+
git -C "$repoPath" pull --ff-only
|
|
46
|
+
npm install --global "$repoPath"
|
|
47
|
+
|
|
48
|
+
browserctl daemon-stop --json
|
|
49
|
+
browserctl daemon-start --json
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Reload the extension in `edge://extensions` or `chrome://extensions` after pulling:
|
|
53
|
+
|
|
54
|
+
1. Open extension management page.
|
|
55
|
+
2. Find `BrowserCtl Relay`.
|
|
56
|
+
3. Click `Reload`.
|
|
57
|
+
|
|
58
|
+
Verify:
|
|
59
|
+
|
|
60
|
+
PowerShell 7:
|
|
61
|
+
|
|
62
|
+
```powershell
|
|
63
|
+
browserctl status --json --session relay --profile chrome-relay
|
|
64
|
+
Invoke-WebRequest -UseBasicParsing -Uri "http://127.0.0.1:9223/browserctl/relay/status" -TimeoutSec 3
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Bash:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
browserctl status --json --session relay --profile chrome-relay
|
|
71
|
+
curl --fail --max-time 3 "http://127.0.0.1:9223/browserctl/relay/status"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Optional fallback (not the default): update CLI from npm registry only.
|
|
75
|
+
|
|
76
|
+
PowerShell 7:
|
|
77
|
+
|
|
78
|
+
```powershell
|
|
79
|
+
npm install --global @flrande/browserctl@latest
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Bash:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
npm install --global @flrande/browserctl@latest
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Fallback Contract
|
|
89
|
+
|
|
90
|
+
If platform-specific install is unavailable, agents should fetch and follow:
|
|
91
|
+
|
|
92
|
+
- `https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/skills/browserctl/SKILL.md`
|
package/README-CN.md
CHANGED
|
@@ -2,65 +2,68 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | 简体中文
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
面向 coding agent 的扩展优先浏览器控制工具集。
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## 给 Agent 安装
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
把下面文本贴给 agent:
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
- Edge 或 Chrome
|
|
11
|
+
```text
|
|
12
|
+
请抓取并遵循 https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/INSTALL.md 中的说明。
|
|
13
|
+
```
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
平台快捷入口:
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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`
|
|
21
|
+
|
|
22
|
+
默认 skill:
|
|
23
|
+
|
|
24
|
+
- 名称:`browserctl`
|
|
25
|
+
- 路径:`skills/browserctl`
|
|
22
26
|
|
|
23
|
-
|
|
27
|
+
## 浏览器快速开始(扩展默认)
|
|
28
|
+
|
|
29
|
+
PowerShell 7:
|
|
24
30
|
|
|
25
31
|
```powershell
|
|
32
|
+
npm install --global @flrande/browserctl
|
|
33
|
+
|
|
26
34
|
$env:BROWSERD_DEFAULT_DRIVER = "chrome-relay"
|
|
27
35
|
$env:BROWSERD_CHROME_RELAY_MODE = "extension"
|
|
28
36
|
$env:BROWSERD_CHROME_RELAY_URL = "http://127.0.0.1:9223"
|
|
29
|
-
$env:BROWSERD_CHROME_RELAY_EXTENSION_TOKEN = "relay
|
|
37
|
+
$env:BROWSERD_CHROME_RELAY_EXTENSION_TOKEN = "browserctl-relay"
|
|
38
|
+
|
|
30
39
|
browserctl daemon-stop --json
|
|
31
40
|
browserctl daemon-start --json
|
|
41
|
+
browserctl status --json --session relay --profile chrome-relay
|
|
32
42
|
```
|
|
33
43
|
|
|
34
|
-
|
|
44
|
+
Bash:
|
|
35
45
|
|
|
36
|
-
|
|
37
|
-
|
|
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`。
|
|
46
|
+
```bash
|
|
47
|
+
npm install --global @flrande/browserctl
|
|
43
48
|
|
|
44
|
-
|
|
49
|
+
export BROWSERD_DEFAULT_DRIVER="chrome-relay"
|
|
50
|
+
export BROWSERD_CHROME_RELAY_MODE="extension"
|
|
51
|
+
export BROWSERD_CHROME_RELAY_URL="http://127.0.0.1:9223"
|
|
52
|
+
export BROWSERD_CHROME_RELAY_EXTENSION_TOKEN="browserctl-relay"
|
|
45
53
|
|
|
46
|
-
|
|
54
|
+
browserctl daemon-stop --json
|
|
55
|
+
browserctl daemon-start --json
|
|
47
56
|
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
|
|
56
57
|
```
|
|
57
58
|
|
|
58
|
-
若 `connected` 为 `false`,重新打开扩展弹窗并点击 `Reconnect`。
|
|
59
|
-
|
|
60
59
|
## 完整文档
|
|
61
60
|
|
|
62
|
-
- [
|
|
63
|
-
- [
|
|
64
|
-
- [
|
|
65
|
-
- [
|
|
66
|
-
|
|
61
|
+
- [Install Guide (English)](INSTALL.md)
|
|
62
|
+
- [安装指南(中文)](INSTALL-CN.md)
|
|
63
|
+
- [User Guide (English)](docs/user-guide.md)
|
|
64
|
+
- [使用指南(中文)](docs/user-guide-cn.md)
|
|
65
|
+
- 安装后更新(skill + 扩展 + CLI):[使用指南](docs/user-guide-cn.md)、[CLI 参考](docs/cli-reference-cn.md)
|
|
66
|
+
- [CLI Reference (English)](docs/cli-reference.md)
|
|
67
|
+
- [CLI 参考(中文)](docs/cli-reference-cn.md)
|
|
68
|
+
- [Maintainer Onboarding (English)](docs/maintainer-onboarding.md)
|
|
69
|
+
- [维护者上手文档(中文)](docs/maintainer-onboarding-cn.md)
|
package/README.md
CHANGED
|
@@ -2,65 +2,68 @@
|
|
|
2
2
|
|
|
3
3
|
English | [简体中文](README-CN.md)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Extension-first browser control toolkit for coding agents.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Install In Agents
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Paste this to your agent:
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
- Edge or Chrome
|
|
11
|
+
```text
|
|
12
|
+
Fetch and follow instructions from https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/INSTALL.md
|
|
13
|
+
```
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
Platform shortcuts:
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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`
|
|
21
|
+
|
|
22
|
+
Default skill:
|
|
23
|
+
|
|
24
|
+
- Name: `browserctl`
|
|
25
|
+
- Path: `skills/browserctl`
|
|
22
26
|
|
|
23
|
-
|
|
27
|
+
## Browser Quick Start (Extension Default)
|
|
28
|
+
|
|
29
|
+
PowerShell 7:
|
|
24
30
|
|
|
25
31
|
```powershell
|
|
32
|
+
npm install --global @flrande/browserctl
|
|
33
|
+
|
|
26
34
|
$env:BROWSERD_DEFAULT_DRIVER = "chrome-relay"
|
|
27
35
|
$env:BROWSERD_CHROME_RELAY_MODE = "extension"
|
|
28
36
|
$env:BROWSERD_CHROME_RELAY_URL = "http://127.0.0.1:9223"
|
|
29
|
-
$env:BROWSERD_CHROME_RELAY_EXTENSION_TOKEN = "relay
|
|
37
|
+
$env:BROWSERD_CHROME_RELAY_EXTENSION_TOKEN = "browserctl-relay"
|
|
38
|
+
|
|
30
39
|
browserctl daemon-stop --json
|
|
31
40
|
browserctl daemon-start --json
|
|
41
|
+
browserctl status --json --session relay --profile chrome-relay
|
|
32
42
|
```
|
|
33
43
|
|
|
34
|
-
|
|
44
|
+
Bash:
|
|
35
45
|
|
|
36
|
-
|
|
37
|
-
|
|
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`.
|
|
46
|
+
```bash
|
|
47
|
+
npm install --global @flrande/browserctl
|
|
43
48
|
|
|
44
|
-
|
|
49
|
+
export BROWSERD_DEFAULT_DRIVER="chrome-relay"
|
|
50
|
+
export BROWSERD_CHROME_RELAY_MODE="extension"
|
|
51
|
+
export BROWSERD_CHROME_RELAY_URL="http://127.0.0.1:9223"
|
|
52
|
+
export BROWSERD_CHROME_RELAY_EXTENSION_TOKEN="browserctl-relay"
|
|
45
53
|
|
|
46
|
-
|
|
54
|
+
browserctl daemon-stop --json
|
|
55
|
+
browserctl daemon-start --json
|
|
47
56
|
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
|
|
56
57
|
```
|
|
57
58
|
|
|
58
|
-
If `connected` is `false`, reopen extension popup and click `Reconnect`.
|
|
59
|
-
|
|
60
59
|
## Full Docs
|
|
61
60
|
|
|
62
|
-
- [
|
|
63
|
-
- [
|
|
64
|
-
- [
|
|
65
|
-
- [
|
|
66
|
-
|
|
61
|
+
- [Install Guide (English)](INSTALL.md)
|
|
62
|
+
- [安装指南(中文)](INSTALL-CN.md)
|
|
63
|
+
- [User Guide (English)](docs/user-guide.md)
|
|
64
|
+
- [使用指南(中文)](docs/user-guide-cn.md)
|
|
65
|
+
- Update after install (skill + extension + CLI): [User Guide](docs/user-guide.md), [CLI Reference](docs/cli-reference.md)
|
|
66
|
+
- [CLI Reference (English)](docs/cli-reference.md)
|
|
67
|
+
- [CLI 参考(中文)](docs/cli-reference-cn.md)
|
|
68
|
+
- [Maintainer Onboarding (English)](docs/maintainer-onboarding.md)
|
|
69
|
+
- [维护者上手文档(中文)](docs/maintainer-onboarding-cn.md)
|
|
@@ -2,9 +2,10 @@ 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";
|
|
5
6
|
|
|
6
|
-
const TEST_DAEMON_PORT = "42491";
|
|
7
7
|
const TEST_SESSION_ID = "session:e2e";
|
|
8
|
+
let testDaemonPort = 0;
|
|
8
9
|
|
|
9
10
|
function createIoCapture() {
|
|
10
11
|
const state = {
|
|
@@ -34,17 +35,20 @@ function parseJsonLine(state: { stdout: string }): Record<string, unknown> {
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
beforeEach(async () => {
|
|
37
|
-
|
|
38
|
+
testDaemonPort = await reserveLoopbackPort();
|
|
39
|
+
process.env.BROWSERCTL_DAEMON_PORT = String(testDaemonPort);
|
|
38
40
|
process.env.BROWSERD_MANAGED_LOCAL_ENABLED = "false";
|
|
39
41
|
process.env.BROWSERD_DEFAULT_DRIVER = "managed";
|
|
40
|
-
|
|
42
|
+
process.env.BROWSERD_CHROME_RELAY_MODE = "cdp";
|
|
43
|
+
await stopDaemon(testDaemonPort);
|
|
41
44
|
});
|
|
42
45
|
|
|
43
46
|
afterEach(async () => {
|
|
44
|
-
await stopDaemon(
|
|
47
|
+
await stopDaemon(testDaemonPort);
|
|
45
48
|
delete process.env.BROWSERCTL_DAEMON_PORT;
|
|
46
49
|
delete process.env.BROWSERD_MANAGED_LOCAL_ENABLED;
|
|
47
50
|
delete process.env.BROWSERD_DEFAULT_DRIVER;
|
|
51
|
+
delete process.env.BROWSERD_CHROME_RELAY_MODE;
|
|
48
52
|
});
|
|
49
53
|
|
|
50
54
|
describe("browserctl e2e", () => {
|
|
@@ -57,7 +61,7 @@ describe("browserctl e2e", () => {
|
|
|
57
61
|
expect(startPayload.ok).toBe(true);
|
|
58
62
|
const startData = startPayload.data as Record<string, unknown>;
|
|
59
63
|
expect(startData.running).toBe(true);
|
|
60
|
-
expect(startData.port).toBe(
|
|
64
|
+
expect(startData.port).toBe(testDaemonPort);
|
|
61
65
|
|
|
62
66
|
const openCapture = createIoCapture();
|
|
63
67
|
const openExitCode = await runCli(
|
|
@@ -0,0 +1,97 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
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,33 +1,8 @@
|
|
|
1
|
-
import { createServer } from "node:net";
|
|
2
|
-
|
|
3
1
|
import { afterEach, describe, expect, it } from "vitest";
|
|
4
2
|
import { WebSocket } from "ws";
|
|
5
3
|
|
|
6
4
|
import { createChromeRelayExtensionBridge } from "./chrome-relay-extension-bridge";
|
|
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
|
-
}
|
|
5
|
+
import { reserveLoopbackPort } from "./test-port";
|
|
31
6
|
|
|
32
7
|
async function waitForCondition(predicate: () => boolean, timeoutMs = 1000): Promise<void> {
|
|
33
8
|
const start = Date.now();
|
|
@@ -61,7 +36,7 @@ afterEach(async () => {
|
|
|
61
36
|
|
|
62
37
|
describe("createChromeRelayExtensionBridge", () => {
|
|
63
38
|
it("requires token configuration for extension bridge startup", async () => {
|
|
64
|
-
const port = await
|
|
39
|
+
const port = await reserveLoopbackPort();
|
|
65
40
|
|
|
66
41
|
expect(() =>
|
|
67
42
|
createChromeRelayExtensionBridge({
|
|
@@ -71,7 +46,7 @@ describe("createChromeRelayExtensionBridge", () => {
|
|
|
71
46
|
});
|
|
72
47
|
|
|
73
48
|
it("sends requests to extension client and receives responses", async () => {
|
|
74
|
-
const port = await
|
|
49
|
+
const port = await reserveLoopbackPort();
|
|
75
50
|
const bridge = createChromeRelayExtensionBridge({
|
|
76
51
|
relayUrl: `http://127.0.0.1:${port}`,
|
|
77
52
|
token: "secret-token",
|
|
@@ -134,7 +109,7 @@ describe("createChromeRelayExtensionBridge", () => {
|
|
|
134
109
|
});
|
|
135
110
|
|
|
136
111
|
it("rejects invoke when extension is not connected", async () => {
|
|
137
|
-
const port = await
|
|
112
|
+
const port = await reserveLoopbackPort();
|
|
138
113
|
const bridge = createChromeRelayExtensionBridge({
|
|
139
114
|
relayUrl: `http://127.0.0.1:${port}`,
|
|
140
115
|
token: "secret-token",
|
|
@@ -148,7 +123,7 @@ describe("createChromeRelayExtensionBridge", () => {
|
|
|
148
123
|
});
|
|
149
124
|
|
|
150
125
|
it("enforces token validation for extension websocket connection", async () => {
|
|
151
|
-
const port = await
|
|
126
|
+
const port = await reserveLoopbackPort();
|
|
152
127
|
const bridge = createChromeRelayExtensionBridge({
|
|
153
128
|
relayUrl: `http://127.0.0.1:${port}`,
|
|
154
129
|
token: "secret-token",
|
|
@@ -188,7 +163,7 @@ describe("createChromeRelayExtensionBridge", () => {
|
|
|
188
163
|
});
|
|
189
164
|
|
|
190
165
|
it("forwards extension event envelopes to bridge listeners", async () => {
|
|
191
|
-
const port = await
|
|
166
|
+
const port = await reserveLoopbackPort();
|
|
192
167
|
const bridge = createChromeRelayExtensionBridge({
|
|
193
168
|
relayUrl: `http://127.0.0.1:${port}`,
|
|
194
169
|
token: "secret-token",
|
|
@@ -53,9 +53,11 @@ 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";
|
|
56
57
|
const DEFAULT_MANAGED_LOCAL_BROWSER_NAME: ManagedLocalBrowserName = "chromium";
|
|
57
58
|
const DEFAULT_MANAGED_LOCAL_HEADLESS = true;
|
|
58
|
-
const DEFAULT_CHROME_RELAY_MODE: ChromeRelayMode = "
|
|
59
|
+
const DEFAULT_CHROME_RELAY_MODE: ChromeRelayMode = "extension";
|
|
60
|
+
const DEFAULT_CHROME_RELAY_EXTENSION_TOKEN = "browserctl-relay";
|
|
59
61
|
const DEFAULT_CHROME_RELAY_EXTENSION_REQUEST_TIMEOUT_MS = 5_000;
|
|
60
62
|
const DEFAULT_NETWORK_WAIT_TIMEOUT_MS = 10_000;
|
|
61
63
|
const DEFAULT_NETWORK_WAIT_POLL_MS = 200;
|
|
@@ -181,7 +183,11 @@ function parseManagedLocalBrowserName(value: string | undefined): ManagedLocalBr
|
|
|
181
183
|
|
|
182
184
|
function parseChromeRelayMode(value: string | undefined): ChromeRelayMode {
|
|
183
185
|
const normalizedValue = value?.trim().toLowerCase();
|
|
184
|
-
|
|
186
|
+
if (normalizedValue === "extension" || normalizedValue === "cdp") {
|
|
187
|
+
return normalizedValue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return DEFAULT_CHROME_RELAY_MODE;
|
|
185
191
|
}
|
|
186
192
|
|
|
187
193
|
export type BrowserdContainer = {
|
|
@@ -198,16 +204,12 @@ export function loadBrowserdConfig(
|
|
|
198
204
|
): BrowserdConfig {
|
|
199
205
|
const managedLocalEnabled = parseBooleanFlag(env.BROWSERD_MANAGED_LOCAL_ENABLED, true);
|
|
200
206
|
const chromeRelayMode = parseChromeRelayMode(env.BROWSERD_CHROME_RELAY_MODE);
|
|
201
|
-
const chromeRelayExtensionToken =
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
"BROWSERD_CHROME_RELAY_EXTENSION_TOKEN is required when BROWSERD_CHROME_RELAY_MODE=extension."
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
+
const chromeRelayExtensionToken =
|
|
208
|
+
resolveNonEmptyString(env.BROWSERD_CHROME_RELAY_EXTENSION_TOKEN) ??
|
|
209
|
+
(chromeRelayMode === "extension" ? DEFAULT_CHROME_RELAY_EXTENSION_TOKEN : undefined);
|
|
207
210
|
|
|
208
211
|
const defaultDriver =
|
|
209
|
-
resolveNonEmptyString(env.BROWSERD_DEFAULT_DRIVER) ??
|
|
210
|
-
(managedLocalEnabled ? MANAGED_LOCAL_DRIVER_KEY : DEFAULT_DRIVER_KEY);
|
|
212
|
+
resolveNonEmptyString(env.BROWSERD_DEFAULT_DRIVER) ?? DEFAULT_CONFIG_DRIVER_KEY;
|
|
211
213
|
|
|
212
214
|
return {
|
|
213
215
|
chromeRelayUrl: env.BROWSERD_CHROME_RELAY_URL ?? "http://127.0.0.1:9223",
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
1
|
+
import { beforeEach, 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";
|
|
7
8
|
|
|
8
9
|
function waitForNextJsonLine(stream: PassThrough): Promise<Record<string, unknown>> {
|
|
9
10
|
return new Promise((resolve, reject) => {
|
|
@@ -78,11 +79,33 @@ function sendTcpToolRequest(
|
|
|
78
79
|
});
|
|
79
80
|
}
|
|
80
81
|
|
|
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
|
+
|
|
81
102
|
describe("browserd container", () => {
|
|
82
103
|
it("uses secure defaults for new security config fields", () => {
|
|
83
104
|
const config = loadBrowserdConfig({});
|
|
84
105
|
|
|
85
|
-
expect(config.defaultDriver).toBe("
|
|
106
|
+
expect(config.defaultDriver).toBe("chrome-relay");
|
|
107
|
+
expect(config.chromeRelayMode).toBe("extension");
|
|
108
|
+
expect(config.chromeRelayExtensionToken).toBe("browserctl-relay");
|
|
86
109
|
expect(config.managedLocalEnabled).toBe(true);
|
|
87
110
|
expect(config.uploadRoot).toBeUndefined();
|
|
88
111
|
expect(config.downloadRoot).toBeUndefined();
|
|
@@ -91,12 +114,13 @@ describe("browserd container", () => {
|
|
|
91
114
|
});
|
|
92
115
|
|
|
93
116
|
it("registers managed-local, managed, chrome-relay, and remote-cdp drivers by default", () => {
|
|
94
|
-
const c = createContainer();
|
|
117
|
+
const c = createContainer(loadBrowserdConfig(createTestEnv()));
|
|
95
118
|
|
|
96
119
|
expect(c.drivers.has("managed-local")).toBe(true);
|
|
97
120
|
expect(c.drivers.has("managed")).toBe(true);
|
|
98
121
|
expect(c.drivers.has("chrome-relay")).toBe(true);
|
|
99
122
|
expect(c.drivers.has("remote-cdp")).toBe(true);
|
|
123
|
+
c.close();
|
|
100
124
|
});
|
|
101
125
|
|
|
102
126
|
it("uses env override in loadBrowserdConfig", () => {
|
|
@@ -110,7 +134,7 @@ describe("browserd container", () => {
|
|
|
110
134
|
|
|
111
135
|
expect(config.remoteCdpUrl).toBe("http://127.0.0.1:9333/devtools/browser/override");
|
|
112
136
|
expect(config.chromeRelayUrl).toBe("http://127.0.0.1:9223");
|
|
113
|
-
expect(config.defaultDriver).toBe("
|
|
137
|
+
expect(config.defaultDriver).toBe("chrome-relay");
|
|
114
138
|
expect(config.managedLocalEnabled).toBe(true);
|
|
115
139
|
expect(config.uploadRoot).toBe("C:\\safe\\uploads");
|
|
116
140
|
expect(config.downloadRoot).toBe("C:\\safe\\downloads");
|
|
@@ -132,31 +156,42 @@ describe("browserd container", () => {
|
|
|
132
156
|
expect(config.chromeRelayExtensionRequestTimeoutMs).toBe(7000);
|
|
133
157
|
});
|
|
134
158
|
|
|
135
|
-
it("
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
).
|
|
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");
|
|
141
174
|
});
|
|
142
175
|
|
|
143
|
-
it("
|
|
176
|
+
it("keeps chrome-relay as default when managed-local is explicitly disabled", () => {
|
|
144
177
|
const config = loadBrowserdConfig({
|
|
145
178
|
BROWSERD_MANAGED_LOCAL_ENABLED: "false"
|
|
146
179
|
});
|
|
147
180
|
|
|
148
181
|
expect(config.managedLocalEnabled).toBe(false);
|
|
149
|
-
expect(config.defaultDriver).toBe("
|
|
182
|
+
expect(config.defaultDriver).toBe("chrome-relay");
|
|
150
183
|
});
|
|
151
184
|
|
|
152
185
|
it("does not register managed-local driver when explicitly disabled", () => {
|
|
153
186
|
const c = createContainer(
|
|
154
187
|
loadBrowserdConfig({
|
|
188
|
+
...createTestEnv(),
|
|
155
189
|
BROWSERD_MANAGED_LOCAL_ENABLED: "false"
|
|
156
190
|
})
|
|
157
191
|
);
|
|
158
192
|
|
|
159
193
|
expect(c.drivers.has("managed-local")).toBe(false);
|
|
194
|
+
c.close();
|
|
160
195
|
});
|
|
161
196
|
});
|
|
162
197
|
|
|
@@ -165,7 +200,7 @@ describe("browserd bootstrap", () => {
|
|
|
165
200
|
const input = new PassThrough();
|
|
166
201
|
const output = new PassThrough();
|
|
167
202
|
|
|
168
|
-
const runtime = bootstrapBrowserd({ input, output });
|
|
203
|
+
const runtime = bootstrapBrowserd({ env: createTestEnv(), input, output });
|
|
169
204
|
|
|
170
205
|
expect(runtime.mcpStdioStarted).toBe(true);
|
|
171
206
|
expect(runtime.container.drivers.has("managed")).toBe(true);
|
|
@@ -179,13 +214,13 @@ describe("browserd bootstrap", () => {
|
|
|
179
214
|
});
|
|
180
215
|
|
|
181
216
|
it("supports tcp transport mode for persistent daemon use", async () => {
|
|
182
|
-
const port =
|
|
217
|
+
const port = testTcpPort;
|
|
183
218
|
const runtime = bootstrapBrowserd({
|
|
184
|
-
env: {
|
|
219
|
+
env: createTestEnv({
|
|
185
220
|
BROWSERD_TRANSPORT: "tcp",
|
|
186
221
|
BROWSERD_PORT: String(port),
|
|
187
222
|
BROWSERD_AUTH_TOKEN: "tcp-token"
|
|
188
|
-
}
|
|
223
|
+
})
|
|
189
224
|
});
|
|
190
225
|
|
|
191
226
|
const response = await sendTcpToolRequest(port, {
|
|
@@ -215,10 +250,10 @@ describe("browserd bootstrap", () => {
|
|
|
215
250
|
try {
|
|
216
251
|
expect(() => {
|
|
217
252
|
runtime = bootstrapBrowserd({
|
|
218
|
-
env: {
|
|
253
|
+
env: createTestEnv({
|
|
219
254
|
BROWSERD_TRANSPORT: "tcp",
|
|
220
|
-
BROWSERD_PORT:
|
|
221
|
-
}
|
|
255
|
+
BROWSERD_PORT: String(testTcpPort)
|
|
256
|
+
})
|
|
222
257
|
});
|
|
223
258
|
}).toThrow("BROWSERD_AUTH_TOKEN");
|
|
224
259
|
} finally {
|
|
@@ -229,7 +264,7 @@ describe("browserd bootstrap", () => {
|
|
|
229
264
|
it("processes line-delimited requests and writes response with id", async () => {
|
|
230
265
|
const input = new PassThrough();
|
|
231
266
|
const output = new PassThrough();
|
|
232
|
-
const runtime = bootstrapBrowserd({ input, output, stdioProtocol: "legacy" });
|
|
267
|
+
const runtime = bootstrapBrowserd({ env: createTestEnv(), input, output, stdioProtocol: "legacy" });
|
|
233
268
|
|
|
234
269
|
const response = await sendToolRequest(input, output, {
|
|
235
270
|
id: "request-1",
|
|
@@ -247,7 +282,7 @@ describe("browserd bootstrap", () => {
|
|
|
247
282
|
expect(response.data).toMatchObject({
|
|
248
283
|
kind: "browserd",
|
|
249
284
|
ready: true,
|
|
250
|
-
driver: "
|
|
285
|
+
driver: "chrome-relay"
|
|
251
286
|
});
|
|
252
287
|
|
|
253
288
|
runtime.close();
|
|
@@ -259,10 +294,10 @@ describe("browserd bootstrap", () => {
|
|
|
259
294
|
const input = new PassThrough();
|
|
260
295
|
const output = new PassThrough();
|
|
261
296
|
const runtime = bootstrapBrowserd({
|
|
262
|
-
env: {
|
|
297
|
+
env: createTestEnv({
|
|
263
298
|
BROWSERD_MANAGED_LOCAL_ENABLED: "true",
|
|
264
299
|
BROWSERD_DEFAULT_DRIVER: "managed-local"
|
|
265
|
-
},
|
|
300
|
+
}),
|
|
266
301
|
input,
|
|
267
302
|
output,
|
|
268
303
|
stdioProtocol: "legacy"
|
|
@@ -297,10 +332,10 @@ describe("browserd bootstrap", () => {
|
|
|
297
332
|
const input = new PassThrough();
|
|
298
333
|
const output = new PassThrough();
|
|
299
334
|
const runtime = bootstrapBrowserd({
|
|
300
|
-
env: {
|
|
335
|
+
env: createTestEnv({
|
|
301
336
|
BROWSERD_DEFAULT_DRIVER: "managed-local",
|
|
302
337
|
BROWSERD_MANAGED_LOCAL_ENABLED: "false"
|
|
303
|
-
},
|
|
338
|
+
}),
|
|
304
339
|
input,
|
|
305
340
|
output,
|
|
306
341
|
stdioProtocol: "legacy"
|
|
@@ -331,9 +366,9 @@ describe("browserd bootstrap", () => {
|
|
|
331
366
|
const input = new PassThrough();
|
|
332
367
|
const output = new PassThrough();
|
|
333
368
|
const runtime = bootstrapBrowserd({
|
|
334
|
-
env: {
|
|
369
|
+
env: createTestEnv({
|
|
335
370
|
BROWSERD_AUTH_TOKEN: "secret-token"
|
|
336
|
-
},
|
|
371
|
+
}),
|
|
337
372
|
input,
|
|
338
373
|
output,
|
|
339
374
|
stdioProtocol: "legacy"
|
|
@@ -362,9 +397,9 @@ describe("browserd bootstrap", () => {
|
|
|
362
397
|
const input = new PassThrough();
|
|
363
398
|
const output = new PassThrough();
|
|
364
399
|
const runtime = bootstrapBrowserd({
|
|
365
|
-
env: {
|
|
400
|
+
env: createTestEnv({
|
|
366
401
|
BROWSERD_AUTH_TOKEN: "secret-token"
|
|
367
|
-
},
|
|
402
|
+
}),
|
|
368
403
|
input,
|
|
369
404
|
output,
|
|
370
405
|
stdioProtocol: "legacy"
|
|
@@ -394,11 +429,11 @@ describe("browserd bootstrap", () => {
|
|
|
394
429
|
const input = new PassThrough();
|
|
395
430
|
const output = new PassThrough();
|
|
396
431
|
const runtime = bootstrapBrowserd({
|
|
397
|
-
env: {
|
|
432
|
+
env: createTestEnv({
|
|
398
433
|
BROWSERD_DEFAULT_DRIVER: "managed",
|
|
399
434
|
BROWSERD_AUTH_TOKEN: "secret-token",
|
|
400
435
|
BROWSERD_AUTH_SCOPES: "read"
|
|
401
|
-
},
|
|
436
|
+
}),
|
|
402
437
|
input,
|
|
403
438
|
output,
|
|
404
439
|
stdioProtocol: "legacy"
|
|
@@ -429,11 +464,11 @@ describe("browserd bootstrap", () => {
|
|
|
429
464
|
const input = new PassThrough();
|
|
430
465
|
const output = new PassThrough();
|
|
431
466
|
const runtime = bootstrapBrowserd({
|
|
432
|
-
env: {
|
|
467
|
+
env: createTestEnv({
|
|
433
468
|
BROWSERD_DEFAULT_DRIVER: "managed",
|
|
434
469
|
BROWSERD_AUTH_TOKEN: "secret-token",
|
|
435
470
|
BROWSERD_AUTH_SCOPES: "read,act"
|
|
436
|
-
},
|
|
471
|
+
}),
|
|
437
472
|
input,
|
|
438
473
|
output,
|
|
439
474
|
stdioProtocol: "legacy"
|
|
@@ -494,10 +529,10 @@ describe("browserd bootstrap", () => {
|
|
|
494
529
|
const input = new PassThrough();
|
|
495
530
|
const output = new PassThrough();
|
|
496
531
|
const runtime = bootstrapBrowserd({
|
|
497
|
-
env: {
|
|
532
|
+
env: createTestEnv({
|
|
498
533
|
BROWSERD_DEFAULT_DRIVER: "managed",
|
|
499
534
|
BROWSERD_UPLOAD_ROOT: "C:\\allowed\\uploads"
|
|
500
|
-
},
|
|
535
|
+
}),
|
|
501
536
|
input,
|
|
502
537
|
output,
|
|
503
538
|
stdioProtocol: "legacy"
|
|
@@ -539,9 +574,9 @@ describe("browserd bootstrap", () => {
|
|
|
539
574
|
const input = new PassThrough();
|
|
540
575
|
const output = new PassThrough();
|
|
541
576
|
const runtime = bootstrapBrowserd({
|
|
542
|
-
env: {
|
|
577
|
+
env: createTestEnv({
|
|
543
578
|
BROWSERD_DEFAULT_DRIVER: "managed"
|
|
544
|
-
},
|
|
579
|
+
}),
|
|
545
580
|
input,
|
|
546
581
|
output,
|
|
547
582
|
stdioProtocol: "legacy"
|
|
@@ -584,10 +619,10 @@ describe("browserd bootstrap", () => {
|
|
|
584
619
|
const input = new PassThrough();
|
|
585
620
|
const output = new PassThrough();
|
|
586
621
|
const runtime = bootstrapBrowserd({
|
|
587
|
-
env: {
|
|
622
|
+
env: createTestEnv({
|
|
588
623
|
BROWSERD_DEFAULT_DRIVER: "managed",
|
|
589
624
|
BROWSERD_DOWNLOAD_ROOT: "C:\\allowed\\downloads"
|
|
590
|
-
},
|
|
625
|
+
}),
|
|
591
626
|
input,
|
|
592
627
|
output,
|
|
593
628
|
stdioProtocol: "legacy"
|
|
@@ -646,9 +681,9 @@ describe("browserd bootstrap", () => {
|
|
|
646
681
|
const input = new PassThrough();
|
|
647
682
|
const output = new PassThrough();
|
|
648
683
|
const runtime = bootstrapBrowserd({
|
|
649
|
-
env: {
|
|
684
|
+
env: createTestEnv({
|
|
650
685
|
BROWSERD_DEFAULT_DRIVER: "managed"
|
|
651
|
-
},
|
|
686
|
+
}),
|
|
652
687
|
input,
|
|
653
688
|
output,
|
|
654
689
|
stdioProtocol: "legacy"
|
|
@@ -690,9 +725,9 @@ describe("browserd bootstrap", () => {
|
|
|
690
725
|
const input = new PassThrough();
|
|
691
726
|
const output = new PassThrough();
|
|
692
727
|
const runtime = bootstrapBrowserd({
|
|
693
|
-
env: {
|
|
728
|
+
env: createTestEnv({
|
|
694
729
|
BROWSERD_DEFAULT_DRIVER: "managed"
|
|
695
|
-
},
|
|
730
|
+
}),
|
|
696
731
|
input,
|
|
697
732
|
output,
|
|
698
733
|
stdioProtocol: "legacy"
|
|
@@ -739,9 +774,9 @@ describe("browserd bootstrap", () => {
|
|
|
739
774
|
const input = new PassThrough();
|
|
740
775
|
const output = new PassThrough();
|
|
741
776
|
const runtime = bootstrapBrowserd({
|
|
742
|
-
env: {
|
|
777
|
+
env: createTestEnv({
|
|
743
778
|
BROWSERD_DEFAULT_DRIVER: "managed"
|
|
744
|
-
},
|
|
779
|
+
}),
|
|
745
780
|
input,
|
|
746
781
|
output,
|
|
747
782
|
stdioProtocol: "legacy"
|
|
@@ -819,7 +854,7 @@ describe("browserd bootstrap", () => {
|
|
|
819
854
|
it("preserves id and trace/session metadata when queue-level error handling runs", async () => {
|
|
820
855
|
const input = new PassThrough();
|
|
821
856
|
const output = new PassThrough();
|
|
822
|
-
const runtime = bootstrapBrowserd({ input, output, stdioProtocol: "legacy" });
|
|
857
|
+
const runtime = bootstrapBrowserd({ env: createTestEnv(), input, output, stdioProtocol: "legacy" });
|
|
823
858
|
|
|
824
859
|
const originalWrite = output.write.bind(output);
|
|
825
860
|
let shouldThrow = true;
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
}
|
|
@@ -3,6 +3,7 @@ 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";
|
|
6
7
|
|
|
7
8
|
function waitForNextJsonLine(stream: PassThrough): Promise<Record<string, unknown>> {
|
|
8
9
|
return new Promise((resolve, reject) => {
|
|
@@ -40,12 +41,15 @@ function sendToolRequest(
|
|
|
40
41
|
return responsePromise;
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
function createManagedLegacyRuntime() {
|
|
44
|
+
async function createManagedLegacyRuntime() {
|
|
45
|
+
const relayPort = await reserveLoopbackPort();
|
|
46
|
+
|
|
44
47
|
const input = new PassThrough();
|
|
45
48
|
const output = new PassThrough();
|
|
46
49
|
const runtime = bootstrapBrowserd({
|
|
47
50
|
env: {
|
|
48
|
-
BROWSERD_DEFAULT_DRIVER: "managed"
|
|
51
|
+
BROWSERD_DEFAULT_DRIVER: "managed",
|
|
52
|
+
BROWSERD_CHROME_RELAY_URL: `http://127.0.0.1:${relayPort}`
|
|
49
53
|
},
|
|
50
54
|
input,
|
|
51
55
|
output,
|
|
@@ -61,7 +65,7 @@ function createManagedLegacyRuntime() {
|
|
|
61
65
|
|
|
62
66
|
describe("browserd tool matrix", () => {
|
|
63
67
|
it("returns E_INVALID_ARG for missing required arguments on routed tools", async () => {
|
|
64
|
-
const { input, output, runtime } = createManagedLegacyRuntime();
|
|
68
|
+
const { input, output, runtime } = await createManagedLegacyRuntime();
|
|
65
69
|
|
|
66
70
|
try {
|
|
67
71
|
const toolNames = [
|
|
@@ -109,7 +113,7 @@ describe("browserd tool matrix", () => {
|
|
|
109
113
|
});
|
|
110
114
|
|
|
111
115
|
it("returns E_DRIVER_UNAVAILABLE for structured action tools on managed driver", async () => {
|
|
112
|
-
const { input, output, runtime } = createManagedLegacyRuntime();
|
|
116
|
+
const { input, output, runtime } = await createManagedLegacyRuntime();
|
|
113
117
|
|
|
114
118
|
try {
|
|
115
119
|
const openResponse = await sendToolRequest(input, output, {
|
|
@@ -235,7 +239,7 @@ describe("browserd tool matrix", () => {
|
|
|
235
239
|
});
|
|
236
240
|
|
|
237
241
|
it("routes managed-driver tools including profile.use, act, focus/close, and waitFor timeout", async () => {
|
|
238
|
-
const { input, output, runtime } = createManagedLegacyRuntime();
|
|
242
|
+
const { input, output, runtime } = await createManagedLegacyRuntime();
|
|
239
243
|
|
|
240
244
|
try {
|
|
241
245
|
const profileUseResponse = await sendToolRequest(input, output, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flrande/browserctl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"bin": {
|
|
6
6
|
"browserctl": "bin/browserctl.cjs",
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"packages/protocol/src",
|
|
17
17
|
"packages/transport-mcp-stdio/src",
|
|
18
18
|
"extensions/chrome-relay",
|
|
19
|
-
"
|
|
19
|
+
"INSTALL.md",
|
|
20
|
+
"INSTALL-CN.md",
|
|
20
21
|
"bin",
|
|
21
22
|
"README.md",
|
|
22
23
|
"README-CN.md",
|
|
@@ -43,8 +44,9 @@
|
|
|
43
44
|
"test:unit": "vitest run --config vitest.config.ts",
|
|
44
45
|
"test:contract": "vitest run --config vitest.contract.config.ts",
|
|
45
46
|
"test:e2e": "vitest run --config vitest.e2e.config.ts",
|
|
46
|
-
"test:
|
|
47
|
-
"
|
|
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",
|
|
48
50
|
"typecheck": "pnpm exec tsc --noEmit -p tsconfig.typecheck.json",
|
|
49
51
|
"lint": "node ./scripts/lint.mjs"
|
|
50
52
|
}
|
|
@@ -1,16 +0,0 @@
|
|
|
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
|
-
});
|
package/scripts/smoke.ps1
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
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
|
-
}
|