cdp-toolkit 1.0.0 → 1.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/CONTRACT.md +1 -1
- package/README.md +34 -0
- package/dist/cli.js +3156 -1290
- package/dist/index.js +1116 -927
- package/dist/mcp.js +4472 -2626
- package/package.json +12 -6
- package/src/backend.ts +86 -0
- package/src/bidi/client.ts +319 -0
- package/src/bidi/driver.ts +823 -0
- package/src/bidi/launch.ts +192 -0
- package/src/bidi/protocol.ts +782 -0
- package/src/bidi/snapshot.ts +419 -0
- package/src/bidi-tools.ts +193 -0
- package/src/capabilities.ts +43 -0
- package/src/cdp/driver.ts +660 -0
- package/src/cli.ts +72 -14
- package/src/driver.ts +424 -0
- package/src/firefox-tools.ts +14 -0
- package/src/index.ts +49 -41
- package/src/mcp.ts +59 -9
- package/src/shared-tools.ts +420 -0
- package/src/tools/dialogs.ts +0 -236
- package/src/tools/emulation.ts +0 -212
- package/src/tools/input.ts +0 -399
- package/src/tools/pages.ts +0 -159
- package/src/tools/screenshot.ts +0 -180
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Firefox launcher for the WebDriver BiDi backend.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists at all: unlike Chrome, there is no attach-to-the-user's-
|
|
5
|
+
* running-browser story here. You cannot add --remote-debugging-port to an
|
|
6
|
+
* already-running Firefox: relaunching the binary with that flag hands off
|
|
7
|
+
* to the running instance and exits silently, opening no port. The flag
|
|
8
|
+
* only takes effect on the ORIGINAL launch of a process. That means this
|
|
9
|
+
* module must own full process lifetime, start it, own its pid, and reap
|
|
10
|
+
* it, because there is no later point at which a debug port can be bolted
|
|
11
|
+
* on. Do not "simplify" this into an attach-to-running-browser helper; that
|
|
12
|
+
* shape cannot work on Firefox and was verified against Firefox 153.0.3.
|
|
13
|
+
*
|
|
14
|
+
* Zero runtime dependencies: only node:child_process, node:fs, node:net,
|
|
15
|
+
* node:os and node:path.
|
|
16
|
+
*/
|
|
17
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
18
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
19
|
+
import { createConnection, createServer } from "node:net";
|
|
20
|
+
import { tmpdir } from "node:os";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
|
|
23
|
+
export interface LaunchFirefoxOptions {
|
|
24
|
+
/**
|
|
25
|
+
* Explicit debug port. Otherwise a genuinely free ephemeral port is picked.
|
|
26
|
+
* 9222 is the conventional CDP debug port; nothing here defaults to it or
|
|
27
|
+
* treats it specially. If the requested port is already bound, launch
|
|
28
|
+
* fails with a clear "already in use" error rather than silently reusing
|
|
29
|
+
* whatever is listening there.
|
|
30
|
+
*/
|
|
31
|
+
port?: number;
|
|
32
|
+
/** Explicit profile directory. Otherwise a fresh throwaway profile is created and deleted on close(). */
|
|
33
|
+
profilePath?: string;
|
|
34
|
+
/** Launch headless. Default false. */
|
|
35
|
+
headless?: boolean;
|
|
36
|
+
/** How long to wait for the debug port to accept a connection, in ms. Default 15000. */
|
|
37
|
+
timeoutMs?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface LaunchedFirefox {
|
|
41
|
+
port: number;
|
|
42
|
+
pid: number;
|
|
43
|
+
profilePath: string;
|
|
44
|
+
close(): Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function resolveBinary(): string {
|
|
48
|
+
const candidates = [
|
|
49
|
+
process.env.FIREFOX_BIN,
|
|
50
|
+
"/Applications/Firefox.app/Contents/MacOS/firefox",
|
|
51
|
+
"firefox",
|
|
52
|
+
];
|
|
53
|
+
for (const c of candidates) {
|
|
54
|
+
if (c) return c;
|
|
55
|
+
}
|
|
56
|
+
throw new Error(
|
|
57
|
+
"Firefox binary not found. Tried FIREFOX_BIN env var, " +
|
|
58
|
+
"/Applications/Firefox.app/Contents/MacOS/firefox, and 'firefox' on PATH.",
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Bind the requested port (0 = OS-assigned ephemeral), read it, release it.
|
|
64
|
+
* Small TOCTOU window between release and Firefox's own bind is acceptable
|
|
65
|
+
* for a test launcher. When a specific port is requested and it is already
|
|
66
|
+
* bound, this surfaces that as a clear "already in use" error instead of
|
|
67
|
+
* silently falling back to something else.
|
|
68
|
+
*/
|
|
69
|
+
function pickPort(requested: number | undefined): Promise<number> {
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
const server = createServer();
|
|
72
|
+
server.once("error", (err: NodeJS.ErrnoException) => {
|
|
73
|
+
if (requested !== undefined && err.code === "EADDRINUSE") {
|
|
74
|
+
reject(new Error(`port ${requested} is already in use`));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
reject(err);
|
|
78
|
+
});
|
|
79
|
+
server.listen(requested ?? 0, "127.0.0.1", () => {
|
|
80
|
+
const addr = server.address();
|
|
81
|
+
if (addr === null || typeof addr === "string") {
|
|
82
|
+
server.close();
|
|
83
|
+
reject(new Error("failed to allocate a port"));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const { port } = addr;
|
|
87
|
+
server.close(() => resolve(port));
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function pollPort(port: number, deadlineMs: number): Promise<void> {
|
|
93
|
+
const start = Date.now();
|
|
94
|
+
return new Promise((resolve, reject) => {
|
|
95
|
+
const attempt = () => {
|
|
96
|
+
const sock = createConnection({ port, host: "127.0.0.1" });
|
|
97
|
+
const onFail = () => {
|
|
98
|
+
sock.destroy();
|
|
99
|
+
if (Date.now() - start >= deadlineMs) {
|
|
100
|
+
reject(
|
|
101
|
+
new Error(
|
|
102
|
+
`Firefox debug port ${port} did not accept a connection within ${deadlineMs}ms`,
|
|
103
|
+
),
|
|
104
|
+
);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
setTimeout(attempt, 100);
|
|
108
|
+
};
|
|
109
|
+
sock.once("connect", () => {
|
|
110
|
+
sock.destroy();
|
|
111
|
+
resolve();
|
|
112
|
+
});
|
|
113
|
+
sock.once("error", onFail);
|
|
114
|
+
};
|
|
115
|
+
attempt();
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Wait for the child process to actually exit, not merely for kill() to return. */
|
|
120
|
+
function waitForExit(proc: ChildProcess, deadlineMs: number): Promise<void> {
|
|
121
|
+
return new Promise((resolve) => {
|
|
122
|
+
if (proc.exitCode !== null || proc.signalCode !== null) {
|
|
123
|
+
resolve();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const timer = setTimeout(() => resolve(), deadlineMs);
|
|
127
|
+
proc.once("exit", () => {
|
|
128
|
+
clearTimeout(timer);
|
|
129
|
+
resolve();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Launch Firefox with the BiDi debug port enabled and resolve once the port
|
|
136
|
+
* actually accepts a TCP connection. Never resolves on a fixed sleep.
|
|
137
|
+
*/
|
|
138
|
+
export async function launchFirefox(opts: LaunchFirefoxOptions = {}): Promise<LaunchedFirefox> {
|
|
139
|
+
const binary = resolveBinary();
|
|
140
|
+
|
|
141
|
+
const port = await pickPort(opts.port);
|
|
142
|
+
|
|
143
|
+
const isThrowaway = opts.profilePath === undefined;
|
|
144
|
+
const profilePath = opts.profilePath ?? mkdtempSync(join(tmpdir(), "cdp-toolkit-ff-profile-"));
|
|
145
|
+
|
|
146
|
+
const args = ["--profile", profilePath, "--remote-debugging-port", String(port), "--no-remote"];
|
|
147
|
+
if (opts.headless) args.push("--headless");
|
|
148
|
+
|
|
149
|
+
const proc = spawn(binary, args, { stdio: "ignore", detached: false });
|
|
150
|
+
|
|
151
|
+
let closed = false;
|
|
152
|
+
const close = async (): Promise<void> => {
|
|
153
|
+
if (closed) return;
|
|
154
|
+
closed = true;
|
|
155
|
+
if (proc.exitCode === null && proc.signalCode === null) {
|
|
156
|
+
proc.kill("SIGTERM");
|
|
157
|
+
await waitForExit(proc, 5000);
|
|
158
|
+
if (proc.exitCode === null && proc.signalCode === null) {
|
|
159
|
+
proc.kill("SIGKILL");
|
|
160
|
+
await waitForExit(proc, 5000);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (isThrowaway) {
|
|
164
|
+
rmSync(profilePath, { recursive: true, force: true });
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const pid = proc.pid;
|
|
169
|
+
if (pid === undefined) {
|
|
170
|
+
await close();
|
|
171
|
+
throw new Error(`failed to spawn Firefox binary at ${binary}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const timeoutMs = opts.timeoutMs ?? 15_000;
|
|
175
|
+
try {
|
|
176
|
+
await pollPort(port, timeoutMs);
|
|
177
|
+
} catch (err) {
|
|
178
|
+
await close();
|
|
179
|
+
throw err;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return { port, pid, profilePath, close };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/*
|
|
186
|
+
* CDP methods/domains used: none, this module speaks no browser protocol,
|
|
187
|
+
* it only launches the process and confirms the debug port is listening.
|
|
188
|
+
* Parity gap vs the Chrome-side story: there is no equivalent of attaching
|
|
189
|
+
* to an already-running browser instance the way client.ts can resolve a
|
|
190
|
+
* target on an already-listening port 9222. Firefox always requires this
|
|
191
|
+
* launcher to have started the process.
|
|
192
|
+
*/
|