@flrande/browserctl 0.1.0-dev.7.1
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/LICENSE +21 -0
- package/README-CN.md +66 -0
- package/README.md +66 -0
- package/apps/browserctl/src/commands/a11y-snapshot.ts +20 -0
- package/apps/browserctl/src/commands/act.ts +20 -0
- package/apps/browserctl/src/commands/common.test.ts +87 -0
- package/apps/browserctl/src/commands/common.ts +191 -0
- package/apps/browserctl/src/commands/console-list.ts +20 -0
- package/apps/browserctl/src/commands/cookie-clear.ts +18 -0
- package/apps/browserctl/src/commands/cookie-get.ts +18 -0
- package/apps/browserctl/src/commands/cookie-set.ts +22 -0
- package/apps/browserctl/src/commands/dialog-arm.ts +20 -0
- package/apps/browserctl/src/commands/dom-query-all.ts +18 -0
- package/apps/browserctl/src/commands/dom-query.ts +18 -0
- package/apps/browserctl/src/commands/download-trigger.ts +22 -0
- package/apps/browserctl/src/commands/download-wait.test.ts +67 -0
- package/apps/browserctl/src/commands/download-wait.ts +27 -0
- package/apps/browserctl/src/commands/element-screenshot.ts +20 -0
- package/apps/browserctl/src/commands/frame-list.ts +16 -0
- package/apps/browserctl/src/commands/frame-snapshot.ts +18 -0
- package/apps/browserctl/src/commands/network-wait-for.ts +100 -0
- package/apps/browserctl/src/commands/profile-list.ts +16 -0
- package/apps/browserctl/src/commands/profile-use.ts +18 -0
- package/apps/browserctl/src/commands/response-body.ts +24 -0
- package/apps/browserctl/src/commands/screenshot.ts +16 -0
- package/apps/browserctl/src/commands/snapshot.ts +16 -0
- package/apps/browserctl/src/commands/status.ts +10 -0
- package/apps/browserctl/src/commands/storage-get.ts +20 -0
- package/apps/browserctl/src/commands/storage-set.ts +22 -0
- package/apps/browserctl/src/commands/tab-close.ts +20 -0
- package/apps/browserctl/src/commands/tab-focus.ts +20 -0
- package/apps/browserctl/src/commands/tab-open.ts +19 -0
- package/apps/browserctl/src/commands/tabs.ts +13 -0
- package/apps/browserctl/src/commands/upload-arm.ts +26 -0
- package/apps/browserctl/src/daemon-client.test.ts +253 -0
- package/apps/browserctl/src/daemon-client.ts +632 -0
- package/apps/browserctl/src/e2e.test.ts +99 -0
- package/apps/browserctl/src/main.test.ts +215 -0
- package/apps/browserctl/src/main.ts +372 -0
- package/apps/browserctl/src/smoke.test.ts +16 -0
- package/apps/browserctl/src/smoke.ts +5 -0
- package/apps/browserd/src/bootstrap.ts +432 -0
- package/apps/browserd/src/chrome-relay-extension-bridge.test.ts +275 -0
- package/apps/browserd/src/chrome-relay-extension-bridge.ts +506 -0
- package/apps/browserd/src/container.ts +1531 -0
- package/apps/browserd/src/main.test.ts +864 -0
- package/apps/browserd/src/main.ts +7 -0
- package/bin/browserctl.cjs +21 -0
- package/bin/browserd.cjs +21 -0
- package/extensions/chrome-relay/README-CN.md +38 -0
- package/extensions/chrome-relay/README.md +38 -0
- package/extensions/chrome-relay/background.js +1687 -0
- package/extensions/chrome-relay/manifest.json +15 -0
- package/extensions/chrome-relay/popup.html +369 -0
- package/extensions/chrome-relay/popup.js +972 -0
- package/package.json +51 -0
- package/packages/core/src/bootstrap.test.ts +10 -0
- package/packages/core/src/driver-registry.test.ts +45 -0
- package/packages/core/src/driver-registry.ts +22 -0
- package/packages/core/src/driver.ts +47 -0
- package/packages/core/src/index.ts +5 -0
- package/packages/core/src/ref-cache.test.ts +61 -0
- package/packages/core/src/ref-cache.ts +28 -0
- package/packages/core/src/session-store.test.ts +49 -0
- package/packages/core/src/session-store.ts +33 -0
- package/packages/core/src/types.ts +9 -0
- package/packages/driver-chrome-relay/src/chrome-relay-driver.test.ts +634 -0
- package/packages/driver-chrome-relay/src/chrome-relay-driver.ts +2206 -0
- package/packages/driver-chrome-relay/src/chrome-relay-extension-runtime.test.ts +264 -0
- package/packages/driver-chrome-relay/src/chrome-relay-extension-runtime.ts +521 -0
- package/packages/driver-chrome-relay/src/index.ts +26 -0
- package/packages/driver-managed/src/index.ts +22 -0
- package/packages/driver-managed/src/managed-driver.test.ts +59 -0
- package/packages/driver-managed/src/managed-driver.ts +125 -0
- package/packages/driver-managed/src/managed-local-driver.test.ts +506 -0
- package/packages/driver-managed/src/managed-local-driver.ts +2021 -0
- package/packages/driver-remote-cdp/src/index.ts +19 -0
- package/packages/driver-remote-cdp/src/remote-cdp-driver.test.ts +617 -0
- package/packages/driver-remote-cdp/src/remote-cdp-driver.ts +2042 -0
- package/packages/protocol/src/envelope.test.ts +25 -0
- package/packages/protocol/src/envelope.ts +31 -0
- package/packages/protocol/src/errors.test.ts +17 -0
- package/packages/protocol/src/errors.ts +11 -0
- package/packages/protocol/src/index.ts +3 -0
- package/packages/protocol/src/tools.ts +3 -0
- package/packages/transport-mcp-stdio/src/index.ts +3 -0
- package/packages/transport-mcp-stdio/src/sdk-server.ts +139 -0
- package/packages/transport-mcp-stdio/src/server.test.ts +281 -0
- package/packages/transport-mcp-stdio/src/server.ts +183 -0
- package/packages/transport-mcp-stdio/src/tool-map.ts +67 -0
- package/scripts/smoke.ps1 +127 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 browserctl contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README-CN.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# browserctl
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 简体中文
|
|
4
|
+
|
|
5
|
+
最快路径:通过 relay 扩展直接控制你已打开的 Edge/Chrome。
|
|
6
|
+
|
|
7
|
+
## 扩展模式最简上手(推荐)
|
|
8
|
+
|
|
9
|
+
前置条件:
|
|
10
|
+
|
|
11
|
+
- Node.js 22+
|
|
12
|
+
- npm 或 pnpm
|
|
13
|
+
- PowerShell 7 (`pwsh`)
|
|
14
|
+
- Edge 或 Chrome
|
|
15
|
+
|
|
16
|
+
1. 安装已发布包:
|
|
17
|
+
|
|
18
|
+
```powershell
|
|
19
|
+
npm install --global @flrande/browserctl
|
|
20
|
+
# 或:pnpm add --global @flrande/browserctl
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
2. 用扩展 relay 模式启动 daemon:
|
|
24
|
+
|
|
25
|
+
```powershell
|
|
26
|
+
$env:BROWSERD_DEFAULT_DRIVER = "chrome-relay"
|
|
27
|
+
$env:BROWSERD_CHROME_RELAY_MODE = "extension"
|
|
28
|
+
$env:BROWSERD_CHROME_RELAY_URL = "http://127.0.0.1:9223"
|
|
29
|
+
$env:BROWSERD_CHROME_RELAY_EXTENSION_TOKEN = "relay-secret"
|
|
30
|
+
browserctl daemon-stop --json
|
|
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
|
|
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
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
若 `connected` 为 `false`,重新打开扩展弹窗并点击 `Reconnect`。
|
|
59
|
+
|
|
60
|
+
## 完整文档
|
|
61
|
+
|
|
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
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# browserctl
|
|
2
|
+
|
|
3
|
+
English | [简体中文](README-CN.md)
|
|
4
|
+
|
|
5
|
+
Fastest path: control your current Edge/Chrome through the relay extension.
|
|
6
|
+
|
|
7
|
+
## Extension Quick Start (Recommended)
|
|
8
|
+
|
|
9
|
+
Prerequisites:
|
|
10
|
+
|
|
11
|
+
- Node.js 22+
|
|
12
|
+
- npm or pnpm
|
|
13
|
+
- PowerShell 7 (`pwsh`)
|
|
14
|
+
- Edge or Chrome
|
|
15
|
+
|
|
16
|
+
1. Install the released package:
|
|
17
|
+
|
|
18
|
+
```powershell
|
|
19
|
+
npm install --global @flrande/browserctl
|
|
20
|
+
# or: pnpm add --global @flrande/browserctl
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
2. Start daemon in extension relay mode:
|
|
24
|
+
|
|
25
|
+
```powershell
|
|
26
|
+
$env:BROWSERD_DEFAULT_DRIVER = "chrome-relay"
|
|
27
|
+
$env:BROWSERD_CHROME_RELAY_MODE = "extension"
|
|
28
|
+
$env:BROWSERD_CHROME_RELAY_URL = "http://127.0.0.1:9223"
|
|
29
|
+
$env:BROWSERD_CHROME_RELAY_EXTENSION_TOKEN = "relay-secret"
|
|
30
|
+
browserctl daemon-stop --json
|
|
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
|
|
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
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
If `connected` is `false`, reopen extension popup and click `Reconnect`.
|
|
59
|
+
|
|
60
|
+
## Full Docs
|
|
61
|
+
|
|
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
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { callDaemonTool } from "../daemon-client";
|
|
2
|
+
import { buildToolArguments, parseCommandContext, requirePositionalArg } from "./common";
|
|
3
|
+
|
|
4
|
+
export type A11ySnapshotCommandResult = Record<string, unknown>;
|
|
5
|
+
|
|
6
|
+
export async function runA11ySnapshotCommand(args: string[]): Promise<A11ySnapshotCommandResult> {
|
|
7
|
+
const context = parseCommandContext(args);
|
|
8
|
+
const targetId = requirePositionalArg(context, 0, "targetId");
|
|
9
|
+
const selector = context.positional[1];
|
|
10
|
+
|
|
11
|
+
return await callDaemonTool<A11ySnapshotCommandResult>(
|
|
12
|
+
"browser.a11y.snapshot",
|
|
13
|
+
buildToolArguments(context, {
|
|
14
|
+
targetId,
|
|
15
|
+
...(typeof selector === "string" && selector.trim().length > 0
|
|
16
|
+
? { selector: selector.trim() }
|
|
17
|
+
: {})
|
|
18
|
+
})
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { callDaemonTool } from "../daemon-client";
|
|
2
|
+
import { buildToolArguments, parseCommandContext, requirePositionalArg } from "./common";
|
|
3
|
+
|
|
4
|
+
export type ActCommandResult = Record<string, unknown>;
|
|
5
|
+
|
|
6
|
+
export async function runActCommand(args: string[]): Promise<ActCommandResult> {
|
|
7
|
+
const context = parseCommandContext(args);
|
|
8
|
+
const actionType = requirePositionalArg(context, 0, "actionType");
|
|
9
|
+
const targetId = requirePositionalArg(context, 1, "targetId");
|
|
10
|
+
|
|
11
|
+
return await callDaemonTool<ActCommandResult>(
|
|
12
|
+
"browser.act",
|
|
13
|
+
buildToolArguments(context, {
|
|
14
|
+
targetId,
|
|
15
|
+
action: {
|
|
16
|
+
type: actionType
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
DAEMON_STARTUP_ARGUMENT,
|
|
5
|
+
buildToolArguments,
|
|
6
|
+
parseCommandContext
|
|
7
|
+
} from "./common";
|
|
8
|
+
|
|
9
|
+
const ORIGINAL_AUTH_TOKEN = process.env.BROWSERCTL_AUTH_TOKEN;
|
|
10
|
+
const ORIGINAL_BROWSER = process.env.BROWSERCTL_BROWSER;
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
if (ORIGINAL_AUTH_TOKEN === undefined) {
|
|
14
|
+
delete process.env.BROWSERCTL_AUTH_TOKEN;
|
|
15
|
+
} else {
|
|
16
|
+
process.env.BROWSERCTL_AUTH_TOKEN = ORIGINAL_AUTH_TOKEN;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (ORIGINAL_BROWSER === undefined) {
|
|
20
|
+
delete process.env.BROWSERCTL_BROWSER;
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
process.env.BROWSERCTL_BROWSER = ORIGINAL_BROWSER;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("parseCommandContext", () => {
|
|
28
|
+
it("uses BROWSERCTL_AUTH_TOKEN by default", () => {
|
|
29
|
+
process.env.BROWSERCTL_AUTH_TOKEN = "env-token";
|
|
30
|
+
|
|
31
|
+
const context = parseCommandContext(["target:1"]);
|
|
32
|
+
|
|
33
|
+
expect(context.authToken).toBe("env-token");
|
|
34
|
+
expect(context.positional).toEqual(["target:1"]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("allows --token to override the environment token", () => {
|
|
38
|
+
process.env.BROWSERCTL_AUTH_TOKEN = "env-token";
|
|
39
|
+
|
|
40
|
+
const context = parseCommandContext(["--token", "cli-token", "target:1"]);
|
|
41
|
+
|
|
42
|
+
expect(context.authToken).toBe("cli-token");
|
|
43
|
+
expect(context.positional).toEqual(["target:1"]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("throws when --token is missing a value", () => {
|
|
47
|
+
expect(() => parseCommandContext(["--token"])).toThrow("Missing value for --token.");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("supports --browser edge preset and forwards daemon startup metadata", () => {
|
|
51
|
+
const context = parseCommandContext(["--browser", "edge", "target:1"]);
|
|
52
|
+
|
|
53
|
+
expect(context.daemonStartup).toEqual({
|
|
54
|
+
managedLocal: {
|
|
55
|
+
browserName: "chromium",
|
|
56
|
+
channel: "msedge"
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
expect(buildToolArguments(context)).toMatchObject({
|
|
60
|
+
[DAEMON_STARTUP_ARGUMENT]: {
|
|
61
|
+
managedLocal: {
|
|
62
|
+
browserName: "chromium",
|
|
63
|
+
channel: "msedge"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("supports BROWSERCTL_BROWSER env preset", () => {
|
|
70
|
+
process.env.BROWSERCTL_BROWSER = "edge";
|
|
71
|
+
|
|
72
|
+
const context = parseCommandContext(["target:1"]);
|
|
73
|
+
|
|
74
|
+
expect(context.daemonStartup).toEqual({
|
|
75
|
+
managedLocal: {
|
|
76
|
+
browserName: "chromium",
|
|
77
|
+
channel: "msedge"
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("throws on unsupported --browser values", () => {
|
|
83
|
+
expect(() => parseCommandContext(["--browser", "opera"])).toThrow(
|
|
84
|
+
"Invalid value for --browser: opera. Supported values: chromium, chrome, edge."
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
export type ManagedLocalBrowserPreset = "chromium" | "chrome" | "edge";
|
|
2
|
+
|
|
3
|
+
export type DaemonStartupConfig = {
|
|
4
|
+
managedLocal: {
|
|
5
|
+
browserName: "chromium";
|
|
6
|
+
channel?: string;
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type CommandContext = {
|
|
11
|
+
sessionId: string;
|
|
12
|
+
profile?: string;
|
|
13
|
+
authToken?: string;
|
|
14
|
+
daemonStartup?: DaemonStartupConfig;
|
|
15
|
+
positional: string[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const DAEMON_STARTUP_ARGUMENT = "__browserctlDaemonStartup";
|
|
19
|
+
|
|
20
|
+
const DEFAULT_SESSION_ID = "cli:local";
|
|
21
|
+
const SUPPORTED_BROWSER_PRESETS = new Set<ManagedLocalBrowserPreset>([
|
|
22
|
+
"chromium",
|
|
23
|
+
"chrome",
|
|
24
|
+
"edge"
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
function resolveAuthToken(value: string | undefined): string | undefined {
|
|
28
|
+
if (value === undefined) {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const trimmedValue = value.trim();
|
|
33
|
+
return trimmedValue.length > 0 ? trimmedValue : undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function requireValue(args: string[], index: number, option: string): string {
|
|
37
|
+
const value = args[index + 1];
|
|
38
|
+
if (value === undefined || value.startsWith("--")) {
|
|
39
|
+
throw new Error(`Missing value for ${option}.`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseBrowserPreset(
|
|
46
|
+
value: string | undefined,
|
|
47
|
+
optionName: string
|
|
48
|
+
): ManagedLocalBrowserPreset | undefined {
|
|
49
|
+
if (value === undefined) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const normalizedValue = value.trim().toLowerCase();
|
|
54
|
+
if (normalizedValue.length === 0) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (SUPPORTED_BROWSER_PRESETS.has(normalizedValue as ManagedLocalBrowserPreset)) {
|
|
59
|
+
return normalizedValue as ManagedLocalBrowserPreset;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Invalid value for ${optionName}: ${value}. Supported values: chromium, chrome, edge.`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function resolveDaemonStartupConfig(
|
|
68
|
+
browserPreset: ManagedLocalBrowserPreset | undefined
|
|
69
|
+
): DaemonStartupConfig | undefined {
|
|
70
|
+
if (browserPreset === undefined) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (browserPreset === "edge") {
|
|
75
|
+
return {
|
|
76
|
+
managedLocal: {
|
|
77
|
+
browserName: "chromium",
|
|
78
|
+
channel: "msedge"
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (browserPreset === "chrome") {
|
|
84
|
+
return {
|
|
85
|
+
managedLocal: {
|
|
86
|
+
browserName: "chromium",
|
|
87
|
+
channel: "chrome"
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
managedLocal: {
|
|
94
|
+
browserName: "chromium"
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function parseCommandContext(args: string[]): CommandContext {
|
|
100
|
+
const positional: string[] = [];
|
|
101
|
+
let sessionId = DEFAULT_SESSION_ID;
|
|
102
|
+
let profile: string | undefined;
|
|
103
|
+
let authToken = resolveAuthToken(process.env.BROWSERCTL_AUTH_TOKEN);
|
|
104
|
+
let browserPreset = parseBrowserPreset(process.env.BROWSERCTL_BROWSER, "BROWSERCTL_BROWSER");
|
|
105
|
+
|
|
106
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
107
|
+
const token = args[index];
|
|
108
|
+
|
|
109
|
+
if (token === "--session") {
|
|
110
|
+
sessionId = requireValue(args, index, "--session");
|
|
111
|
+
index += 1;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (token === "--profile") {
|
|
116
|
+
profile = requireValue(args, index, "--profile");
|
|
117
|
+
index += 1;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (token === "--token") {
|
|
122
|
+
authToken = resolveAuthToken(requireValue(args, index, "--token"));
|
|
123
|
+
if (authToken === undefined) {
|
|
124
|
+
throw new Error("Missing value for --token.");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
index += 1;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (token === "--browser") {
|
|
132
|
+
browserPreset = parseBrowserPreset(requireValue(args, index, "--browser"), "--browser");
|
|
133
|
+
index += 1;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (token === "--") {
|
|
138
|
+
positional.push(...args.slice(index + 1));
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
positional.push(token);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
sessionId,
|
|
147
|
+
profile,
|
|
148
|
+
authToken,
|
|
149
|
+
daemonStartup: resolveDaemonStartupConfig(browserPreset),
|
|
150
|
+
positional
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function buildToolArguments(
|
|
155
|
+
context: CommandContext,
|
|
156
|
+
args: Record<string, unknown> = {}
|
|
157
|
+
): Record<string, unknown> {
|
|
158
|
+
const baseArguments: Record<string, unknown> = {
|
|
159
|
+
sessionId: context.sessionId
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
if (context.profile !== undefined) {
|
|
163
|
+
baseArguments.profile = context.profile;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (context.authToken !== undefined) {
|
|
167
|
+
baseArguments.authToken = context.authToken;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (context.daemonStartup !== undefined) {
|
|
171
|
+
baseArguments[DAEMON_STARTUP_ARGUMENT] = context.daemonStartup;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
...baseArguments,
|
|
176
|
+
...args
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function requirePositionalArg(
|
|
181
|
+
context: CommandContext,
|
|
182
|
+
index: number,
|
|
183
|
+
name: string
|
|
184
|
+
): string {
|
|
185
|
+
const value = context.positional[index];
|
|
186
|
+
if (value === undefined) {
|
|
187
|
+
throw new Error(`Missing required argument: ${name}.`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return value;
|
|
191
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { callDaemonTool } from "../daemon-client";
|
|
2
|
+
import { buildToolArguments, parseCommandContext, requirePositionalArg } from "./common";
|
|
3
|
+
|
|
4
|
+
export type ConsoleListCommandResult = {
|
|
5
|
+
driver: string;
|
|
6
|
+
targetId: string;
|
|
7
|
+
entries: unknown[];
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export async function runConsoleListCommand(args: string[]): Promise<ConsoleListCommandResult> {
|
|
11
|
+
const context = parseCommandContext(args);
|
|
12
|
+
const targetId = requirePositionalArg(context, 0, "targetId");
|
|
13
|
+
|
|
14
|
+
return await callDaemonTool<ConsoleListCommandResult>(
|
|
15
|
+
"browser.console.list",
|
|
16
|
+
buildToolArguments(context, {
|
|
17
|
+
targetId
|
|
18
|
+
})
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { callDaemonTool } from "../daemon-client";
|
|
2
|
+
import { buildToolArguments, parseCommandContext, requirePositionalArg } from "./common";
|
|
3
|
+
|
|
4
|
+
export type CookieClearCommandResult = Record<string, unknown>;
|
|
5
|
+
|
|
6
|
+
export async function runCookieClearCommand(args: string[]): Promise<CookieClearCommandResult> {
|
|
7
|
+
const context = parseCommandContext(args);
|
|
8
|
+
const targetId = requirePositionalArg(context, 0, "targetId");
|
|
9
|
+
const name = context.positional[1];
|
|
10
|
+
|
|
11
|
+
return await callDaemonTool<CookieClearCommandResult>(
|
|
12
|
+
"browser.cookie.clear",
|
|
13
|
+
buildToolArguments(context, {
|
|
14
|
+
targetId,
|
|
15
|
+
...(typeof name === "string" && name.trim().length > 0 ? { name: name.trim() } : {})
|
|
16
|
+
})
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { callDaemonTool } from "../daemon-client";
|
|
2
|
+
import { buildToolArguments, parseCommandContext, requirePositionalArg } from "./common";
|
|
3
|
+
|
|
4
|
+
export type CookieGetCommandResult = Record<string, unknown>;
|
|
5
|
+
|
|
6
|
+
export async function runCookieGetCommand(args: string[]): Promise<CookieGetCommandResult> {
|
|
7
|
+
const context = parseCommandContext(args);
|
|
8
|
+
const targetId = requirePositionalArg(context, 0, "targetId");
|
|
9
|
+
const name = context.positional[1];
|
|
10
|
+
|
|
11
|
+
return await callDaemonTool<CookieGetCommandResult>(
|
|
12
|
+
"browser.cookie.get",
|
|
13
|
+
buildToolArguments(context, {
|
|
14
|
+
targetId,
|
|
15
|
+
...(typeof name === "string" && name.trim().length > 0 ? { name: name.trim() } : {})
|
|
16
|
+
})
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { callDaemonTool } from "../daemon-client";
|
|
2
|
+
import { buildToolArguments, parseCommandContext, requirePositionalArg } from "./common";
|
|
3
|
+
|
|
4
|
+
export type CookieSetCommandResult = Record<string, unknown>;
|
|
5
|
+
|
|
6
|
+
export async function runCookieSetCommand(args: string[]): Promise<CookieSetCommandResult> {
|
|
7
|
+
const context = parseCommandContext(args);
|
|
8
|
+
const targetId = requirePositionalArg(context, 0, "targetId");
|
|
9
|
+
const name = requirePositionalArg(context, 1, "name");
|
|
10
|
+
const value = requirePositionalArg(context, 2, "value");
|
|
11
|
+
const url = context.positional[3];
|
|
12
|
+
|
|
13
|
+
return await callDaemonTool<CookieSetCommandResult>(
|
|
14
|
+
"browser.cookie.set",
|
|
15
|
+
buildToolArguments(context, {
|
|
16
|
+
targetId,
|
|
17
|
+
name,
|
|
18
|
+
value,
|
|
19
|
+
...(typeof url === "string" && url.trim().length > 0 ? { url: url.trim() } : {})
|
|
20
|
+
})
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { callDaemonTool } from "../daemon-client";
|
|
2
|
+
import { buildToolArguments, parseCommandContext, requirePositionalArg } from "./common";
|
|
3
|
+
|
|
4
|
+
export type DialogArmCommandResult = {
|
|
5
|
+
driver: string;
|
|
6
|
+
targetId: string;
|
|
7
|
+
armed: boolean;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export async function runDialogArmCommand(args: string[]): Promise<DialogArmCommandResult> {
|
|
11
|
+
const context = parseCommandContext(args);
|
|
12
|
+
const targetId = requirePositionalArg(context, 0, "targetId");
|
|
13
|
+
|
|
14
|
+
return await callDaemonTool<DialogArmCommandResult>(
|
|
15
|
+
"browser.dialog.arm",
|
|
16
|
+
buildToolArguments(context, {
|
|
17
|
+
targetId
|
|
18
|
+
})
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { callDaemonTool } from "../daemon-client";
|
|
2
|
+
import { buildToolArguments, parseCommandContext, requirePositionalArg } from "./common";
|
|
3
|
+
|
|
4
|
+
export type DomQueryAllCommandResult = Record<string, unknown>;
|
|
5
|
+
|
|
6
|
+
export async function runDomQueryAllCommand(args: string[]): Promise<DomQueryAllCommandResult> {
|
|
7
|
+
const context = parseCommandContext(args);
|
|
8
|
+
const targetId = requirePositionalArg(context, 0, "targetId");
|
|
9
|
+
const selector = requirePositionalArg(context, 1, "selector");
|
|
10
|
+
|
|
11
|
+
return await callDaemonTool<DomQueryAllCommandResult>(
|
|
12
|
+
"browser.dom.queryAll",
|
|
13
|
+
buildToolArguments(context, {
|
|
14
|
+
targetId,
|
|
15
|
+
selector
|
|
16
|
+
})
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { callDaemonTool } from "../daemon-client";
|
|
2
|
+
import { buildToolArguments, parseCommandContext, requirePositionalArg } from "./common";
|
|
3
|
+
|
|
4
|
+
export type DomQueryCommandResult = Record<string, unknown>;
|
|
5
|
+
|
|
6
|
+
export async function runDomQueryCommand(args: string[]): Promise<DomQueryCommandResult> {
|
|
7
|
+
const context = parseCommandContext(args);
|
|
8
|
+
const targetId = requirePositionalArg(context, 0, "targetId");
|
|
9
|
+
const selector = requirePositionalArg(context, 1, "selector");
|
|
10
|
+
|
|
11
|
+
return await callDaemonTool<DomQueryCommandResult>(
|
|
12
|
+
"browser.dom.query",
|
|
13
|
+
buildToolArguments(context, {
|
|
14
|
+
targetId,
|
|
15
|
+
selector
|
|
16
|
+
})
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { callDaemonTool } from "../daemon-client";
|
|
2
|
+
import { buildToolArguments, parseCommandContext, requirePositionalArg } from "./common";
|
|
3
|
+
|
|
4
|
+
export type DownloadTriggerCommandResult = {
|
|
5
|
+
driver: string;
|
|
6
|
+
targetId: string;
|
|
7
|
+
triggered: boolean;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export async function runDownloadTriggerCommand(
|
|
11
|
+
args: string[]
|
|
12
|
+
): Promise<DownloadTriggerCommandResult> {
|
|
13
|
+
const context = parseCommandContext(args);
|
|
14
|
+
const targetId = requirePositionalArg(context, 0, "targetId");
|
|
15
|
+
|
|
16
|
+
return await callDaemonTool<DownloadTriggerCommandResult>(
|
|
17
|
+
"browser.download.trigger",
|
|
18
|
+
buildToolArguments(context, {
|
|
19
|
+
targetId
|
|
20
|
+
})
|
|
21
|
+
);
|
|
22
|
+
}
|