@flrande/browserctl 0.1.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/LICENSE +21 -0
- package/README-CN.md +1155 -0
- package/README.md +1155 -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.md +36 -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
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import * as actCommand from "./commands/act";
|
|
4
|
+
import * as daemonClient from "./daemon-client";
|
|
5
|
+
import * as screenshotCommand from "./commands/screenshot";
|
|
6
|
+
import * as statusCommand from "./commands/status";
|
|
7
|
+
import * as tabsCommand from "./commands/tabs";
|
|
8
|
+
import { EXIT_CODES, parseArgs, runCli } from "./main";
|
|
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
|
+
afterEach(() => {
|
|
34
|
+
vi.restoreAllMocks();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("cli", () => {
|
|
38
|
+
it("parses status command", () => {
|
|
39
|
+
expect(parseArgs(["status"]).command).toBe("status");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("preserves unknown flags and '--' separator as command args", () => {
|
|
43
|
+
expect(parseArgs(["status", "--profile", "default", "--", "--raw"])).toEqual({
|
|
44
|
+
command: "status",
|
|
45
|
+
commandArgs: ["--profile", "default", "--", "--raw"],
|
|
46
|
+
json: false
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("consumes --json globally while preserving command args", () => {
|
|
51
|
+
expect(parseArgs(["--json", "tabs", "--verbose"])).toEqual({
|
|
52
|
+
command: "tabs",
|
|
53
|
+
commandArgs: ["--verbose"],
|
|
54
|
+
json: true
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("writes JSON output on successful run", async () => {
|
|
59
|
+
vi.spyOn(statusCommand, "runStatusCommand").mockResolvedValue({
|
|
60
|
+
kind: "browserd",
|
|
61
|
+
ready: true
|
|
62
|
+
});
|
|
63
|
+
const { io, state } = createIoCapture();
|
|
64
|
+
|
|
65
|
+
const exitCode = await runCli(["status", "--json"], io);
|
|
66
|
+
|
|
67
|
+
expect(exitCode).toBe(EXIT_CODES.OK);
|
|
68
|
+
expect(state.stderr).toBe("");
|
|
69
|
+
expect(JSON.parse(state.stdout)).toEqual({
|
|
70
|
+
ok: true,
|
|
71
|
+
command: "status",
|
|
72
|
+
data: {
|
|
73
|
+
kind: "browserd",
|
|
74
|
+
ready: true
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("returns INVALID_ARGS for unknown command", async () => {
|
|
80
|
+
const { io, state } = createIoCapture();
|
|
81
|
+
|
|
82
|
+
const exitCode = await runCli(["unknown"], io);
|
|
83
|
+
|
|
84
|
+
expect(exitCode).toBe(EXIT_CODES.INVALID_ARGS);
|
|
85
|
+
expect(state.stdout).toBe("");
|
|
86
|
+
expect(state.stderr).toContain("Unknown command: unknown");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("writes non-JSON output when --json is not provided", async () => {
|
|
90
|
+
vi.spyOn(tabsCommand, "runTabsCommand").mockResolvedValue({
|
|
91
|
+
driver: "managed",
|
|
92
|
+
tabs: ["target:1"]
|
|
93
|
+
});
|
|
94
|
+
const { io, state } = createIoCapture();
|
|
95
|
+
|
|
96
|
+
const exitCode = await runCli(["tabs"], io);
|
|
97
|
+
|
|
98
|
+
expect(exitCode).toBe(EXIT_CODES.OK);
|
|
99
|
+
expect(state.stderr).toBe("");
|
|
100
|
+
expect(state.stdout).toMatch(/^tabs: /);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("dispatches to act command handler with forwarded args", async () => {
|
|
104
|
+
const actSpy = vi.spyOn(actCommand, "runActCommand").mockResolvedValue({
|
|
105
|
+
ok: true
|
|
106
|
+
});
|
|
107
|
+
const { io } = createIoCapture();
|
|
108
|
+
|
|
109
|
+
const exitCode = await runCli(["act", "click", "tab:42"], io);
|
|
110
|
+
|
|
111
|
+
expect(exitCode).toBe(EXIT_CODES.OK);
|
|
112
|
+
expect(actSpy).toHaveBeenCalledTimes(1);
|
|
113
|
+
expect(actSpy).toHaveBeenCalledWith(["click", "tab:42"]);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("dispatches to screenshot command handler with forwarded args", async () => {
|
|
117
|
+
const screenshotSpy = vi.spyOn(screenshotCommand, "runScreenshotCommand").mockResolvedValue({
|
|
118
|
+
ok: true
|
|
119
|
+
});
|
|
120
|
+
const { io } = createIoCapture();
|
|
121
|
+
|
|
122
|
+
const exitCode = await runCli(["screenshot", "tab:42"], io);
|
|
123
|
+
|
|
124
|
+
expect(exitCode).toBe(EXIT_CODES.OK);
|
|
125
|
+
expect(screenshotSpy).toHaveBeenCalledTimes(1);
|
|
126
|
+
expect(screenshotSpy).toHaveBeenCalledWith(["tab:42"]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("dispatches to status command handler", async () => {
|
|
130
|
+
const statusSpy = vi.spyOn(statusCommand, "runStatusCommand").mockResolvedValue({
|
|
131
|
+
kind: "browserd",
|
|
132
|
+
ready: true
|
|
133
|
+
});
|
|
134
|
+
const { io } = createIoCapture();
|
|
135
|
+
|
|
136
|
+
const exitCode = await runCli(["status", "--json"], io);
|
|
137
|
+
|
|
138
|
+
expect(exitCode).toBe(EXIT_CODES.OK);
|
|
139
|
+
expect(statusSpy).toHaveBeenCalledTimes(1);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("dispatches daemon-status through daemon client", async () => {
|
|
143
|
+
const daemonStatusSpy = vi.spyOn(daemonClient, "getDaemonStatus").mockResolvedValue({
|
|
144
|
+
running: true,
|
|
145
|
+
port: 41337,
|
|
146
|
+
pid: 12345
|
|
147
|
+
});
|
|
148
|
+
const { io, state } = createIoCapture();
|
|
149
|
+
|
|
150
|
+
const exitCode = await runCli(["daemon-status", "--json"], io);
|
|
151
|
+
|
|
152
|
+
expect(exitCode).toBe(EXIT_CODES.OK);
|
|
153
|
+
expect(daemonStatusSpy).toHaveBeenCalledTimes(1);
|
|
154
|
+
expect(JSON.parse(state.stdout)).toEqual({
|
|
155
|
+
ok: true,
|
|
156
|
+
command: "daemon-status",
|
|
157
|
+
data: {
|
|
158
|
+
running: true,
|
|
159
|
+
port: 41337,
|
|
160
|
+
pid: 12345
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("forwards --browser preset to daemon-start", async () => {
|
|
166
|
+
const daemonStartSpy = vi.spyOn(daemonClient, "ensureDaemonRunning").mockResolvedValue({
|
|
167
|
+
running: true,
|
|
168
|
+
port: 41337,
|
|
169
|
+
pid: 12345
|
|
170
|
+
});
|
|
171
|
+
const { io } = createIoCapture();
|
|
172
|
+
|
|
173
|
+
const exitCode = await runCli(["daemon-start", "--json", "--browser", "edge"], io);
|
|
174
|
+
|
|
175
|
+
expect(exitCode).toBe(EXIT_CODES.OK);
|
|
176
|
+
expect(daemonStartSpy).toHaveBeenCalledTimes(1);
|
|
177
|
+
expect(daemonStartSpy).toHaveBeenCalledWith({
|
|
178
|
+
authToken: undefined,
|
|
179
|
+
startup: {
|
|
180
|
+
managedLocal: {
|
|
181
|
+
browserName: "chromium",
|
|
182
|
+
channel: "msedge"
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("adds relay setup guidance when chrome-relay connection fails", async () => {
|
|
189
|
+
vi.spyOn(statusCommand, "runStatusCommand").mockRejectedValue(
|
|
190
|
+
new Error("E_INTERNAL: connect ECONNREFUSED 127.0.0.1:9223")
|
|
191
|
+
);
|
|
192
|
+
const { io, state } = createIoCapture();
|
|
193
|
+
|
|
194
|
+
const exitCode = await runCli(["status", "--profile", "chrome-relay"], io);
|
|
195
|
+
|
|
196
|
+
expect(exitCode).toBe(EXIT_CODES.COMMAND_ERROR);
|
|
197
|
+
expect(state.stderr).toContain("chrome-relay 连接失败");
|
|
198
|
+
expect(state.stderr).toContain("扩展");
|
|
199
|
+
expect(state.stderr).toContain("BROWSERD_CHROME_RELAY_URL");
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("adds relay setup guidance when extension relay is not connected", async () => {
|
|
203
|
+
vi.spyOn(statusCommand, "runStatusCommand").mockRejectedValue(
|
|
204
|
+
new Error("E_INTERNAL: Chrome relay extension is not connected.")
|
|
205
|
+
);
|
|
206
|
+
const { io, state } = createIoCapture();
|
|
207
|
+
|
|
208
|
+
const exitCode = await runCli(["status", "--profile", "chrome-relay"], io);
|
|
209
|
+
|
|
210
|
+
expect(exitCode).toBe(EXIT_CODES.COMMAND_ERROR);
|
|
211
|
+
expect(state.stderr).toContain("chrome-relay 连接失败");
|
|
212
|
+
expect(state.stderr).toContain("BROWSERD_CHROME_RELAY_MODE=extension");
|
|
213
|
+
expect(state.stderr).toContain("extensions/chrome-relay");
|
|
214
|
+
});
|
|
215
|
+
});
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { pathToFileURL } from "node:url";
|
|
2
|
+
|
|
3
|
+
import { runActCommand } from "./commands/act";
|
|
4
|
+
import { runA11ySnapshotCommand } from "./commands/a11y-snapshot";
|
|
5
|
+
import { runCookieClearCommand } from "./commands/cookie-clear";
|
|
6
|
+
import { runCookieGetCommand } from "./commands/cookie-get";
|
|
7
|
+
import { runCookieSetCommand } from "./commands/cookie-set";
|
|
8
|
+
import { runConsoleListCommand } from "./commands/console-list";
|
|
9
|
+
import { runDialogArmCommand } from "./commands/dialog-arm";
|
|
10
|
+
import { runDomQueryAllCommand } from "./commands/dom-query-all";
|
|
11
|
+
import { runDomQueryCommand } from "./commands/dom-query";
|
|
12
|
+
import { runDownloadTriggerCommand } from "./commands/download-trigger";
|
|
13
|
+
import { runDownloadWaitCommand } from "./commands/download-wait";
|
|
14
|
+
import { runElementScreenshotCommand } from "./commands/element-screenshot";
|
|
15
|
+
import { runFrameListCommand } from "./commands/frame-list";
|
|
16
|
+
import { runFrameSnapshotCommand } from "./commands/frame-snapshot";
|
|
17
|
+
import { runNetworkWaitForCommand } from "./commands/network-wait-for";
|
|
18
|
+
import { parseCommandContext } from "./commands/common";
|
|
19
|
+
import { runProfileListCommand } from "./commands/profile-list";
|
|
20
|
+
import { runProfileUseCommand } from "./commands/profile-use";
|
|
21
|
+
import { runResponseBodyCommand } from "./commands/response-body";
|
|
22
|
+
import { runScreenshotCommand } from "./commands/screenshot";
|
|
23
|
+
import { runSnapshotCommand } from "./commands/snapshot";
|
|
24
|
+
import { runStatusCommand } from "./commands/status";
|
|
25
|
+
import { runStorageGetCommand } from "./commands/storage-get";
|
|
26
|
+
import { runStorageSetCommand } from "./commands/storage-set";
|
|
27
|
+
import { runTabCloseCommand } from "./commands/tab-close";
|
|
28
|
+
import { runTabFocusCommand } from "./commands/tab-focus";
|
|
29
|
+
import { runTabOpenCommand } from "./commands/tab-open";
|
|
30
|
+
import { runTabsCommand } from "./commands/tabs";
|
|
31
|
+
import { runUploadArmCommand } from "./commands/upload-arm";
|
|
32
|
+
import { ensureDaemonRunning, getDaemonStatus, stopDaemon } from "./daemon-client";
|
|
33
|
+
|
|
34
|
+
export const EXIT_CODES = {
|
|
35
|
+
OK: 0,
|
|
36
|
+
COMMAND_ERROR: 1,
|
|
37
|
+
INVALID_ARGS: 2
|
|
38
|
+
} as const;
|
|
39
|
+
|
|
40
|
+
export type CommandName =
|
|
41
|
+
| "status"
|
|
42
|
+
| "tabs"
|
|
43
|
+
| "profile-list"
|
|
44
|
+
| "profile-use"
|
|
45
|
+
| "tab-open"
|
|
46
|
+
| "tab-focus"
|
|
47
|
+
| "tab-close"
|
|
48
|
+
| "snapshot"
|
|
49
|
+
| "screenshot"
|
|
50
|
+
| "dom-query"
|
|
51
|
+
| "dom-query-all"
|
|
52
|
+
| "element-screenshot"
|
|
53
|
+
| "a11y-snapshot"
|
|
54
|
+
| "network-wait-for"
|
|
55
|
+
| "cookie-get"
|
|
56
|
+
| "cookie-set"
|
|
57
|
+
| "cookie-clear"
|
|
58
|
+
| "storage-get"
|
|
59
|
+
| "storage-set"
|
|
60
|
+
| "frame-list"
|
|
61
|
+
| "frame-snapshot"
|
|
62
|
+
| "act"
|
|
63
|
+
| "upload-arm"
|
|
64
|
+
| "dialog-arm"
|
|
65
|
+
| "download-trigger"
|
|
66
|
+
| "download-wait"
|
|
67
|
+
| "console-list"
|
|
68
|
+
| "response-body"
|
|
69
|
+
| "daemon-status"
|
|
70
|
+
| "daemon-start"
|
|
71
|
+
| "daemon-stop";
|
|
72
|
+
|
|
73
|
+
export interface ParsedArgs {
|
|
74
|
+
command: CommandName | null;
|
|
75
|
+
commandArgs: string[];
|
|
76
|
+
json: boolean;
|
|
77
|
+
error?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const VALID_COMMANDS: ReadonlySet<CommandName> = new Set([
|
|
81
|
+
"status",
|
|
82
|
+
"tabs",
|
|
83
|
+
"profile-list",
|
|
84
|
+
"profile-use",
|
|
85
|
+
"tab-open",
|
|
86
|
+
"tab-focus",
|
|
87
|
+
"tab-close",
|
|
88
|
+
"snapshot",
|
|
89
|
+
"screenshot",
|
|
90
|
+
"dom-query",
|
|
91
|
+
"dom-query-all",
|
|
92
|
+
"element-screenshot",
|
|
93
|
+
"a11y-snapshot",
|
|
94
|
+
"network-wait-for",
|
|
95
|
+
"cookie-get",
|
|
96
|
+
"cookie-set",
|
|
97
|
+
"cookie-clear",
|
|
98
|
+
"storage-get",
|
|
99
|
+
"storage-set",
|
|
100
|
+
"frame-list",
|
|
101
|
+
"frame-snapshot",
|
|
102
|
+
"act",
|
|
103
|
+
"upload-arm",
|
|
104
|
+
"dialog-arm",
|
|
105
|
+
"download-trigger",
|
|
106
|
+
"download-wait",
|
|
107
|
+
"console-list",
|
|
108
|
+
"response-body",
|
|
109
|
+
"daemon-status",
|
|
110
|
+
"daemon-start",
|
|
111
|
+
"daemon-stop"
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
const CHROME_RELAY_FAILURE_GUIDANCE = [
|
|
115
|
+
"chrome-relay 连接失败,请先完成以下检查:",
|
|
116
|
+
"1) 扩展 relay 方案:设置 BROWSERD_CHROME_RELAY_MODE=extension。",
|
|
117
|
+
"2) 在 chrome://extensions 或 edge://extensions 中加载 extensions/chrome-relay(Load unpacked)。",
|
|
118
|
+
"3) 在扩展弹窗里确认 Bridge URL 与 BROWSERD_CHROME_RELAY_URL 一致(默认 ws://127.0.0.1:9223/bridge)。",
|
|
119
|
+
"4) 如果你走 CDP relay 方案,确认浏览器已启动远程调试端口(例如 --remote-debugging-port=9223)。",
|
|
120
|
+
"5) 确认 BROWSERD_CHROME_RELAY_URL 指向可访问地址(默认 http://127.0.0.1:9223)。"
|
|
121
|
+
].join("\n");
|
|
122
|
+
|
|
123
|
+
export function parseArgs(argv: string[]): ParsedArgs {
|
|
124
|
+
const positional = [];
|
|
125
|
+
let json = false;
|
|
126
|
+
|
|
127
|
+
for (const value of argv) {
|
|
128
|
+
if (value === "--json") {
|
|
129
|
+
json = true;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
positional.push(value);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const [commandToken, ...commandArgs] = positional;
|
|
137
|
+
if (commandToken === undefined) {
|
|
138
|
+
return {
|
|
139
|
+
command: null,
|
|
140
|
+
commandArgs: [],
|
|
141
|
+
json,
|
|
142
|
+
error: "Missing command."
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!VALID_COMMANDS.has(commandToken as CommandName)) {
|
|
147
|
+
return {
|
|
148
|
+
command: null,
|
|
149
|
+
commandArgs: [],
|
|
150
|
+
json,
|
|
151
|
+
error: `Unknown command: ${commandToken}`
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
command: commandToken as CommandName,
|
|
157
|
+
commandArgs,
|
|
158
|
+
json
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function tryResolveProfile(commandArgs: string[]): string | undefined {
|
|
163
|
+
try {
|
|
164
|
+
return parseCommandContext(commandArgs).profile;
|
|
165
|
+
} catch {
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function isConnectionFailureMessage(message: string): boolean {
|
|
171
|
+
return /ECONNREFUSED|ECONNRESET|ETIMEDOUT|ENOTFOUND|Timed out/i.test(message);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function shouldAttachChromeRelayGuidance(commandArgs: string[], message: string): boolean {
|
|
175
|
+
const lowerMessage = message.toLowerCase();
|
|
176
|
+
if (lowerMessage.includes("chrome relay extension is not connected")) {
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!isConnectionFailureMessage(message)) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const profile = tryResolveProfile(commandArgs);
|
|
185
|
+
if (profile === "chrome-relay") {
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
lowerMessage.includes("chrome-relay") ||
|
|
191
|
+
lowerMessage.includes("connectovercdp") ||
|
|
192
|
+
lowerMessage.includes("/json/version") ||
|
|
193
|
+
lowerMessage.includes("9223")
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function enrichCommandErrorMessage(commandArgs: string[], message: string): string {
|
|
198
|
+
if (!shouldAttachChromeRelayGuidance(commandArgs, message)) {
|
|
199
|
+
return message;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return `${message}\n\n${CHROME_RELAY_FAILURE_GUIDANCE}`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
interface WritableLike {
|
|
206
|
+
write(content: string): void;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export interface CliIo {
|
|
210
|
+
stdout: WritableLike;
|
|
211
|
+
stderr: WritableLike;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function formatHumanOutput(command: CommandName, data: unknown): string {
|
|
215
|
+
return `${command}: ${JSON.stringify(data)}\n`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function writeError(io: CliIo, message: string, json: boolean, exitCode: number): void {
|
|
219
|
+
if (json) {
|
|
220
|
+
io.stderr.write(
|
|
221
|
+
`${JSON.stringify({
|
|
222
|
+
ok: false,
|
|
223
|
+
error: {
|
|
224
|
+
message,
|
|
225
|
+
exitCode
|
|
226
|
+
}
|
|
227
|
+
})}\n`
|
|
228
|
+
);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
io.stderr.write(`${message}\n`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function executeCommand(command: CommandName, commandArgs: string[]): Promise<unknown> {
|
|
236
|
+
switch (command) {
|
|
237
|
+
case "status":
|
|
238
|
+
return await runStatusCommand(commandArgs);
|
|
239
|
+
case "tabs":
|
|
240
|
+
return await runTabsCommand(commandArgs);
|
|
241
|
+
case "profile-list":
|
|
242
|
+
return await runProfileListCommand(commandArgs);
|
|
243
|
+
case "profile-use":
|
|
244
|
+
return await runProfileUseCommand(commandArgs);
|
|
245
|
+
case "tab-open":
|
|
246
|
+
return await runTabOpenCommand(commandArgs);
|
|
247
|
+
case "tab-focus":
|
|
248
|
+
return await runTabFocusCommand(commandArgs);
|
|
249
|
+
case "tab-close":
|
|
250
|
+
return await runTabCloseCommand(commandArgs);
|
|
251
|
+
case "snapshot":
|
|
252
|
+
return await runSnapshotCommand(commandArgs);
|
|
253
|
+
case "screenshot":
|
|
254
|
+
return await runScreenshotCommand(commandArgs);
|
|
255
|
+
case "dom-query":
|
|
256
|
+
return await runDomQueryCommand(commandArgs);
|
|
257
|
+
case "dom-query-all":
|
|
258
|
+
return await runDomQueryAllCommand(commandArgs);
|
|
259
|
+
case "element-screenshot":
|
|
260
|
+
return await runElementScreenshotCommand(commandArgs);
|
|
261
|
+
case "a11y-snapshot":
|
|
262
|
+
return await runA11ySnapshotCommand(commandArgs);
|
|
263
|
+
case "network-wait-for":
|
|
264
|
+
return await runNetworkWaitForCommand(commandArgs);
|
|
265
|
+
case "cookie-get":
|
|
266
|
+
return await runCookieGetCommand(commandArgs);
|
|
267
|
+
case "cookie-set":
|
|
268
|
+
return await runCookieSetCommand(commandArgs);
|
|
269
|
+
case "cookie-clear":
|
|
270
|
+
return await runCookieClearCommand(commandArgs);
|
|
271
|
+
case "storage-get":
|
|
272
|
+
return await runStorageGetCommand(commandArgs);
|
|
273
|
+
case "storage-set":
|
|
274
|
+
return await runStorageSetCommand(commandArgs);
|
|
275
|
+
case "frame-list":
|
|
276
|
+
return await runFrameListCommand(commandArgs);
|
|
277
|
+
case "frame-snapshot":
|
|
278
|
+
return await runFrameSnapshotCommand(commandArgs);
|
|
279
|
+
case "act":
|
|
280
|
+
return await runActCommand(commandArgs);
|
|
281
|
+
case "upload-arm":
|
|
282
|
+
return await runUploadArmCommand(commandArgs);
|
|
283
|
+
case "dialog-arm":
|
|
284
|
+
return await runDialogArmCommand(commandArgs);
|
|
285
|
+
case "download-trigger":
|
|
286
|
+
return await runDownloadTriggerCommand(commandArgs);
|
|
287
|
+
case "download-wait":
|
|
288
|
+
return await runDownloadWaitCommand(commandArgs);
|
|
289
|
+
case "console-list":
|
|
290
|
+
return await runConsoleListCommand(commandArgs);
|
|
291
|
+
case "response-body":
|
|
292
|
+
return await runResponseBodyCommand(commandArgs);
|
|
293
|
+
case "daemon-status": {
|
|
294
|
+
const context = parseCommandContext(commandArgs);
|
|
295
|
+
return await getDaemonStatus({
|
|
296
|
+
authToken: context.authToken
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
case "daemon-start": {
|
|
300
|
+
const context = parseCommandContext(commandArgs);
|
|
301
|
+
return await ensureDaemonRunning({
|
|
302
|
+
authToken: context.authToken,
|
|
303
|
+
startup: context.daemonStartup
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
case "daemon-stop":
|
|
307
|
+
return stopDaemon();
|
|
308
|
+
default:
|
|
309
|
+
return neverCommand(command);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function neverCommand(command: never): never {
|
|
314
|
+
throw new Error(`Unsupported command: ${command}`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export async function runCli(
|
|
318
|
+
argv: string[],
|
|
319
|
+
io: CliIo = {
|
|
320
|
+
stdout: process.stdout,
|
|
321
|
+
stderr: process.stderr
|
|
322
|
+
}
|
|
323
|
+
): Promise<number> {
|
|
324
|
+
const parsedArgs = parseArgs(argv);
|
|
325
|
+
if (parsedArgs.error !== undefined) {
|
|
326
|
+
writeError(io, parsedArgs.error, parsedArgs.json, EXIT_CODES.INVALID_ARGS);
|
|
327
|
+
return EXIT_CODES.INVALID_ARGS;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
const data = await executeCommand(parsedArgs.command, parsedArgs.commandArgs);
|
|
332
|
+
|
|
333
|
+
if (parsedArgs.json) {
|
|
334
|
+
io.stdout.write(
|
|
335
|
+
`${JSON.stringify({
|
|
336
|
+
ok: true,
|
|
337
|
+
command: parsedArgs.command,
|
|
338
|
+
data
|
|
339
|
+
})}\n`
|
|
340
|
+
);
|
|
341
|
+
} else {
|
|
342
|
+
io.stdout.write(formatHumanOutput(parsedArgs.command, data));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return EXIT_CODES.OK;
|
|
346
|
+
} catch (error) {
|
|
347
|
+
const message = error instanceof Error ? error.message : "Unknown command failure.";
|
|
348
|
+
writeError(
|
|
349
|
+
io,
|
|
350
|
+
enrichCommandErrorMessage(parsedArgs.commandArgs, message),
|
|
351
|
+
parsedArgs.json,
|
|
352
|
+
EXIT_CODES.COMMAND_ERROR
|
|
353
|
+
);
|
|
354
|
+
return EXIT_CODES.COMMAND_ERROR;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export async function main(argv: string[] = process.argv.slice(2)): Promise<number> {
|
|
359
|
+
return await runCli(argv);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
363
|
+
void main()
|
|
364
|
+
.then((exitCode) => {
|
|
365
|
+
process.exitCode = exitCode;
|
|
366
|
+
})
|
|
367
|
+
.catch((error) => {
|
|
368
|
+
const message = error instanceof Error ? error.message : "Unexpected CLI failure.";
|
|
369
|
+
process.stderr.write(`${message}\n`);
|
|
370
|
+
process.exitCode = EXIT_CODES.COMMAND_ERROR;
|
|
371
|
+
});
|
|
372
|
+
}
|
|
@@ -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
|
+
});
|