@crouton-kit/capture 1.3.3 → 1.3.5
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/bin/capture +2293 -1477
- package/package.json +7 -3
- package/src/capture.ts +10 -383
- package/src/cdp/a11y.ts +104 -0
- package/src/cdp/args.ts +101 -0
- package/src/cdp/client.ts +97 -0
- package/src/cdp/commands/exec.ts +102 -0
- package/src/cdp/commands/har.ts +128 -0
- package/src/cdp/commands/lib.ts +356 -0
- package/src/cdp/commands/tabs.ts +189 -0
- package/src/cdp/commands/traffic.ts +121 -0
- package/src/cdp/commands/ui.ts +172 -0
- package/src/cdp/connection.ts +108 -0
- package/src/cdp/console-recorder.ts +80 -0
- package/src/cdp/detect.ts +150 -0
- package/src/cdp/dispatch.ts +32 -0
- package/src/cdp/execute.ts +117 -0
- package/src/cdp/har-output.ts +47 -0
- package/src/cdp/har-recorder.ts +147 -0
- package/src/cdp/record.ts +221 -0
- package/src/cdp/screenshot.ts +100 -0
- package/src/cdp/tab-lock.ts +132 -0
- package/src/cdp/targets.ts +112 -0
- package/src/cdp/types.ts +36 -0
- package/src/cdp/usage.ts +69 -0
- package/src/cdp.ts +17 -2337
- package/src/session/commands.ts +385 -0
- package/src/vault/bundle.ts +205 -0
- package/src/vault/docs.ts +224 -0
- package/src/vault/paths.ts +87 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crouton-kit/capture",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.5",
|
|
4
4
|
"description": "Browser automation and UI validation via CDP — session-based screenshots, HAR, a11y, click/type, and JS exec",
|
|
5
5
|
"bin": {
|
|
6
6
|
"capture": "./bin/capture"
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
"src"
|
|
11
11
|
],
|
|
12
12
|
"scripts": {
|
|
13
|
-
"build": "esbuild src/capture.ts --bundle --platform=node --format=cjs --outfile=bin/capture '--banner:js=#!/usr/bin/env node' \"--define:globalThis.__CAPTURE_VERSION__=\\\"$npm_package_version\\\"\" && chmod +x bin/capture"
|
|
13
|
+
"build": "esbuild src/capture.ts --bundle --platform=node --format=cjs --outfile=bin/capture --external:esbuild '--banner:js=#!/usr/bin/env node' \"--define:globalThis.__CAPTURE_VERSION__=\\\"$npm_package_version\\\"\" && chmod +x bin/capture",
|
|
14
|
+
"dev": "tsx src/capture.ts",
|
|
15
|
+
"test": "node --import tsx --test test/*.test.ts"
|
|
14
16
|
},
|
|
15
17
|
"repository": {
|
|
16
18
|
"type": "git",
|
|
@@ -33,7 +35,9 @@
|
|
|
33
35
|
"devDependencies": {
|
|
34
36
|
"@types/ws": "^8.18.1",
|
|
35
37
|
"esbuild": "^0.24.0",
|
|
36
|
-
"
|
|
38
|
+
"tsx": "^4",
|
|
39
|
+
"typescript": "^5.7.0",
|
|
40
|
+
"zod": "^4.3.5"
|
|
37
41
|
},
|
|
38
42
|
"dependencies": {
|
|
39
43
|
"ws": "^8.19.0"
|
package/src/capture.ts
CHANGED
|
@@ -22,390 +22,8 @@
|
|
|
22
22
|
|
|
23
23
|
import * as fs from "fs";
|
|
24
24
|
import * as path from "path";
|
|
25
|
-
import * as os from "os";
|
|
26
|
-
import { spawn } from "child_process";
|
|
27
|
-
import {
|
|
28
|
-
createHarRecording,
|
|
29
|
-
readHarRecording,
|
|
30
|
-
deleteHarRecording,
|
|
31
|
-
} from "./har-manager.js";
|
|
32
25
|
import { cdpMain } from "./cdp.js";
|
|
33
|
-
import {
|
|
34
|
-
getActiveSession,
|
|
35
|
-
setActiveSession,
|
|
36
|
-
clearActiveSession,
|
|
37
|
-
} from "./session-context.js";
|
|
38
|
-
|
|
39
|
-
const CAPTURE_ROOT = path.join(os.tmpdir(), "capture-sessions");
|
|
40
|
-
|
|
41
|
-
interface LogPid {
|
|
42
|
-
pid: number;
|
|
43
|
-
name: string;
|
|
44
|
-
sourcePath: string;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
interface Session {
|
|
48
|
-
id: string;
|
|
49
|
-
dir: string;
|
|
50
|
-
harId: string | null;
|
|
51
|
-
startedAt: string;
|
|
52
|
-
url: string | null;
|
|
53
|
-
targetId: string | null;
|
|
54
|
-
stepCount: number;
|
|
55
|
-
logPids: LogPid[];
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
interface BundleManifest {
|
|
59
|
-
id: string;
|
|
60
|
-
startedAt: string;
|
|
61
|
-
stoppedAt: string;
|
|
62
|
-
duration: number;
|
|
63
|
-
url: string | null;
|
|
64
|
-
screenshots: Array<{ name: string; path: string }>;
|
|
65
|
-
har: { id: string; path: string; entryCount: number } | null;
|
|
66
|
-
a11y: Array<{ name: string; path: string }>;
|
|
67
|
-
logs: Array<{ name: string; path: string; lines: number }>;
|
|
68
|
-
other: Array<{ name: string; path: string }>;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function sessionDir(id: string): string {
|
|
72
|
-
return path.join(CAPTURE_ROOT, id);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function sessionMetaPath(id: string): string {
|
|
76
|
-
return path.join(sessionDir(id), ".session.json");
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function readSession(id: string): Session {
|
|
80
|
-
const metaPath = sessionMetaPath(id);
|
|
81
|
-
if (!fs.existsSync(metaPath)) {
|
|
82
|
-
throw new Error(`No capture session found: ${id}`);
|
|
83
|
-
}
|
|
84
|
-
return JSON.parse(fs.readFileSync(metaPath, "utf-8")) as Session;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function generateId(): string {
|
|
88
|
-
return `cap-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// ============================================================================
|
|
92
|
-
// Session Commands
|
|
93
|
-
// ============================================================================
|
|
94
|
-
|
|
95
|
-
async function start(args: string[]): Promise<void> {
|
|
96
|
-
let url: string | null = null;
|
|
97
|
-
|
|
98
|
-
for (let i = 0; i < args.length; i++) {
|
|
99
|
-
if (args[i] === "--url" && args[i + 1]) {
|
|
100
|
-
url = args[++i];
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const id = generateId();
|
|
105
|
-
const dir = sessionDir(id);
|
|
106
|
-
fs.mkdirSync(path.join(dir, "shots"), { recursive: true });
|
|
107
|
-
fs.mkdirSync(path.join(dir, "a11y"), { recursive: true });
|
|
108
|
-
|
|
109
|
-
// Start HAR recording directly
|
|
110
|
-
let harId: string | null = null;
|
|
111
|
-
try {
|
|
112
|
-
const harResult = createHarRecording();
|
|
113
|
-
harId = harResult.id;
|
|
114
|
-
} catch (err) {
|
|
115
|
-
console.error(`Warning: could not start HAR recording: ${err instanceof Error ? err.message : err}`);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Open tab if URL provided
|
|
119
|
-
let targetId: string | null = null;
|
|
120
|
-
if (url) {
|
|
121
|
-
try {
|
|
122
|
-
const { detectCdpPort, navigateAndWait } = await import('./cdp.js');
|
|
123
|
-
const port = await detectCdpPort();
|
|
124
|
-
const tab = await navigateAndWait(port, url);
|
|
125
|
-
targetId = tab.id;
|
|
126
|
-
} catch (err) {
|
|
127
|
-
console.error(`Warning: could not open tab: ${err instanceof Error ? err.message : err}`);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const session: Session = {
|
|
132
|
-
id,
|
|
133
|
-
dir,
|
|
134
|
-
harId,
|
|
135
|
-
startedAt: new Date().toISOString(),
|
|
136
|
-
url,
|
|
137
|
-
targetId,
|
|
138
|
-
stepCount: 0,
|
|
139
|
-
logPids: [],
|
|
140
|
-
};
|
|
141
|
-
fs.writeFileSync(sessionMetaPath(id), JSON.stringify(session, null, 2));
|
|
142
|
-
|
|
143
|
-
// Set as active session for auto-defaults
|
|
144
|
-
setActiveSession({
|
|
145
|
-
sessionId: id,
|
|
146
|
-
dir,
|
|
147
|
-
harId,
|
|
148
|
-
targetId,
|
|
149
|
-
stepCount: 0,
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
// Output for agent consumption
|
|
153
|
-
const result = {
|
|
154
|
-
sessionId: id,
|
|
155
|
-
bundleDir: dir,
|
|
156
|
-
harId,
|
|
157
|
-
targetId,
|
|
158
|
-
shotsDir: path.join(dir, "shots"),
|
|
159
|
-
a11yDir: path.join(dir, "a11y"),
|
|
160
|
-
};
|
|
161
|
-
console.log(JSON.stringify(result, null, 2));
|
|
162
|
-
|
|
163
|
-
// Agent-friendly next steps on stderr
|
|
164
|
-
console.error(`\nCapture session started: ${id}`);
|
|
165
|
-
if (targetId) {
|
|
166
|
-
console.error(`Tab opened — session context active. No need to pass --target or --har.`);
|
|
167
|
-
}
|
|
168
|
-
console.error(`\nWhen done: capture session stop ${id}`);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function logCommand(args: string[]): void {
|
|
172
|
-
const sourcePath = args[0];
|
|
173
|
-
if (!sourcePath) {
|
|
174
|
-
console.error("Usage: capture log <path> [--name label] [--session <id>]");
|
|
175
|
-
process.exit(1);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const resolved = path.resolve(sourcePath);
|
|
179
|
-
if (!fs.existsSync(resolved)) {
|
|
180
|
-
throw new Error(`Log file not found: ${resolved}`);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
let name: string | null = null;
|
|
184
|
-
let sessionId: string | null = null;
|
|
185
|
-
for (let i = 1; i < args.length; i++) {
|
|
186
|
-
if (args[i] === "--name" && args[i + 1]) name = args[++i];
|
|
187
|
-
if (args[i] === "--session" && args[i + 1]) sessionId = args[++i];
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (!sessionId) {
|
|
191
|
-
const active = getActiveSession();
|
|
192
|
-
if (!active) {
|
|
193
|
-
throw new Error("No active capture session. Start one or pass --session <id>.");
|
|
194
|
-
}
|
|
195
|
-
sessionId = active.sessionId;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const session = readSession(sessionId);
|
|
199
|
-
name = name ?? path.basename(resolved, path.extname(resolved));
|
|
200
|
-
|
|
201
|
-
const logsDir = path.join(session.dir, "logs");
|
|
202
|
-
fs.mkdirSync(logsDir, { recursive: true });
|
|
203
|
-
|
|
204
|
-
const destPath = path.join(logsDir, `${name}.log`);
|
|
205
|
-
const outFd = fs.openSync(destPath, "a");
|
|
206
|
-
|
|
207
|
-
const child = spawn(
|
|
208
|
-
"sh",
|
|
209
|
-
["-c", `tail -f "${resolved}" | perl -MPOSIX -ne 'print strftime("%Y-%m-%dT%H:%M:%SZ",gmtime())." ".$_'`],
|
|
210
|
-
{ detached: true, stdio: ["ignore", outFd, "ignore"] },
|
|
211
|
-
);
|
|
212
|
-
child.unref();
|
|
213
|
-
fs.closeSync(outFd);
|
|
214
|
-
|
|
215
|
-
const pid = child.pid!;
|
|
216
|
-
session.logPids.push({ pid, name, sourcePath: resolved });
|
|
217
|
-
fs.writeFileSync(sessionMetaPath(session.id), JSON.stringify(session, null, 2));
|
|
218
|
-
|
|
219
|
-
console.log(JSON.stringify({ name, sourcePath: resolved, destPath, pid }, null, 2));
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
async function stop(args: string[]): Promise<void> {
|
|
223
|
-
const id = args[0];
|
|
224
|
-
if (!id) {
|
|
225
|
-
console.error("Usage: capture session stop <session-id>");
|
|
226
|
-
process.exit(1);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const session = readSession(id);
|
|
230
|
-
const stoppedAt = new Date().toISOString();
|
|
231
|
-
const startMs = new Date(session.startedAt).getTime();
|
|
232
|
-
const duration = Date.now() - startMs;
|
|
233
|
-
|
|
234
|
-
// Kill log tailers
|
|
235
|
-
for (const lp of session.logPids ?? []) {
|
|
236
|
-
try { process.kill(-lp.pid, "SIGTERM"); } catch { /* already dead */ }
|
|
237
|
-
}
|
|
238
|
-
if (session.logPids?.length) {
|
|
239
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Collect screenshots
|
|
243
|
-
const shotsDir = path.join(session.dir, "shots");
|
|
244
|
-
const screenshots = fs.existsSync(shotsDir)
|
|
245
|
-
? fs.readdirSync(shotsDir)
|
|
246
|
-
.filter((f) => f.endsWith(".png") || f.endsWith(".jpg"))
|
|
247
|
-
.map((f) => ({ name: f, path: path.join(shotsDir, f) }))
|
|
248
|
-
: [];
|
|
249
|
-
|
|
250
|
-
// Collect a11y snapshots
|
|
251
|
-
const a11yDir = path.join(session.dir, "a11y");
|
|
252
|
-
const a11y = fs.existsSync(a11yDir)
|
|
253
|
-
? fs.readdirSync(a11yDir)
|
|
254
|
-
.filter((f) => f.endsWith(".json") || f.endsWith(".txt"))
|
|
255
|
-
.map((f) => ({ name: f, path: path.join(a11yDir, f) }))
|
|
256
|
-
: [];
|
|
257
|
-
|
|
258
|
-
// Collect HAR directly from har-manager
|
|
259
|
-
let har: BundleManifest["har"] = null;
|
|
260
|
-
if (session.harId) {
|
|
261
|
-
try {
|
|
262
|
-
const harData = readHarRecording(session.harId);
|
|
263
|
-
if (harData) {
|
|
264
|
-
const harPath = path.join(session.dir, "har.json");
|
|
265
|
-
fs.writeFileSync(harPath, JSON.stringify(harData, null, 2));
|
|
266
|
-
har = { id: session.harId, path: harPath, entryCount: harData.log.entries.length };
|
|
267
|
-
// Clean up the HAR recording
|
|
268
|
-
try { deleteHarRecording(session.harId); } catch { /* best effort */ }
|
|
269
|
-
}
|
|
270
|
-
} catch (err) {
|
|
271
|
-
console.error(`Warning: could not read HAR: ${err instanceof Error ? err.message : err}`);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// Collect log files
|
|
276
|
-
const logsDir = path.join(session.dir, "logs");
|
|
277
|
-
const logs = fs.existsSync(logsDir)
|
|
278
|
-
? fs.readdirSync(logsDir)
|
|
279
|
-
.filter((f) => f.endsWith(".log"))
|
|
280
|
-
.map((f) => {
|
|
281
|
-
const filePath = path.join(logsDir, f);
|
|
282
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
283
|
-
const lines = content ? content.split("\n").filter(Boolean).length : 0;
|
|
284
|
-
return { name: f, path: filePath, lines };
|
|
285
|
-
})
|
|
286
|
-
: [];
|
|
287
|
-
|
|
288
|
-
// Collect anything else dropped in the session dir
|
|
289
|
-
const knownDirs = new Set(["shots", "a11y", "logs"]);
|
|
290
|
-
const knownFiles = new Set([".session.json", "har.json", "bundle.json"]);
|
|
291
|
-
const other = fs.readdirSync(session.dir)
|
|
292
|
-
.filter((f) => !knownDirs.has(f) && !knownFiles.has(f))
|
|
293
|
-
.map((f) => ({ name: f, path: path.join(session.dir, f) }));
|
|
294
|
-
|
|
295
|
-
// Clear active session context
|
|
296
|
-
clearActiveSession();
|
|
297
|
-
|
|
298
|
-
const manifest: BundleManifest = {
|
|
299
|
-
id: session.id,
|
|
300
|
-
startedAt: session.startedAt,
|
|
301
|
-
stoppedAt,
|
|
302
|
-
duration,
|
|
303
|
-
url: session.url,
|
|
304
|
-
screenshots,
|
|
305
|
-
har,
|
|
306
|
-
a11y,
|
|
307
|
-
logs,
|
|
308
|
-
other,
|
|
309
|
-
};
|
|
310
|
-
|
|
311
|
-
const bundlePath = path.join(session.dir, "bundle.json");
|
|
312
|
-
fs.writeFileSync(bundlePath, JSON.stringify(manifest, null, 2));
|
|
313
|
-
|
|
314
|
-
console.log(JSON.stringify({
|
|
315
|
-
bundlePath,
|
|
316
|
-
summary: {
|
|
317
|
-
duration,
|
|
318
|
-
screenshots: screenshots.length,
|
|
319
|
-
harEntries: har?.entryCount ?? 0,
|
|
320
|
-
a11ySnapshots: a11y.length,
|
|
321
|
-
logFiles: logs.length,
|
|
322
|
-
otherFiles: other.length,
|
|
323
|
-
},
|
|
324
|
-
}, null, 2));
|
|
325
|
-
|
|
326
|
-
console.error(`\nBundle written: ${bundlePath}`);
|
|
327
|
-
console.error(`Read it: capture session view ${id}`);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function list(): void {
|
|
331
|
-
if (!fs.existsSync(CAPTURE_ROOT)) {
|
|
332
|
-
console.log("[]");
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
const sessions = fs.readdirSync(CAPTURE_ROOT)
|
|
337
|
-
.filter((d) => fs.existsSync(sessionMetaPath(d)))
|
|
338
|
-
.map((d) => {
|
|
339
|
-
const session = readSession(d);
|
|
340
|
-
const hasBundled = fs.existsSync(path.join(session.dir, "bundle.json"));
|
|
341
|
-
return { id: session.id, startedAt: session.startedAt, url: session.url, status: hasBundled ? "stopped" : "active" };
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
console.log(JSON.stringify(sessions, null, 2));
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
function view(args: string[]): void {
|
|
348
|
-
const id = args[0];
|
|
349
|
-
if (!id) {
|
|
350
|
-
console.error("Usage: capture session view <session-id> [--filter screenshots|har|a11y]");
|
|
351
|
-
process.exit(1);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
const session = readSession(id);
|
|
355
|
-
const bundlePath = path.join(session.dir, "bundle.json");
|
|
356
|
-
|
|
357
|
-
if (!fs.existsSync(bundlePath)) {
|
|
358
|
-
console.error(`Session ${id} hasn't been stopped yet. Run: capture session stop ${id}`);
|
|
359
|
-
process.exit(1);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
const manifest = JSON.parse(fs.readFileSync(bundlePath, "utf-8")) as BundleManifest;
|
|
363
|
-
|
|
364
|
-
const filter = args.find((_, i) => args[i - 1] === "--filter");
|
|
365
|
-
if (filter) {
|
|
366
|
-
const section = manifest[filter as keyof BundleManifest];
|
|
367
|
-
console.log(JSON.stringify(section, null, 2));
|
|
368
|
-
} else {
|
|
369
|
-
console.log(JSON.stringify(manifest, null, 2));
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
async function sessionMain(args: string[]): Promise<void> {
|
|
374
|
-
const [subcommand, ...rest] = args;
|
|
375
|
-
|
|
376
|
-
switch (subcommand) {
|
|
377
|
-
case "start": return start(rest);
|
|
378
|
-
case "stop": return stop(rest);
|
|
379
|
-
case "list": return list();
|
|
380
|
-
case "view": return view(rest);
|
|
381
|
-
default:
|
|
382
|
-
console.log(`capture session — manage capture sessions
|
|
383
|
-
|
|
384
|
-
Sub-commands:
|
|
385
|
-
start [--url <url>] Start a session (opens tab, records HAR, sets active context)
|
|
386
|
-
stop <session-id> Finalize and bundle artifacts (screenshots, HAR, a11y, logs)
|
|
387
|
-
list List active and stopped sessions
|
|
388
|
-
view <id> [--filter section] View bundle manifest; section = screenshots|har|a11y|logs
|
|
389
|
-
|
|
390
|
-
Why sessions: once started, every subsequent capture command auto-fills
|
|
391
|
-
--target (the tab) and --har (the recording). No manual flag threading.
|
|
392
|
-
|
|
393
|
-
Typical flow:
|
|
394
|
-
1. capture session start --url http://localhost:3000
|
|
395
|
-
2. Interact — no --target / --har needed:
|
|
396
|
-
capture a11y --interactive
|
|
397
|
-
capture click "Sign in"
|
|
398
|
-
capture type "hi@me.com" --into "Email"
|
|
399
|
-
capture screenshot
|
|
400
|
-
capture navigate https://app.example.com/dashboard
|
|
401
|
-
capture har read --filter-url /api
|
|
402
|
-
3. capture session stop <session-id>
|
|
403
|
-
4. capture session view <session-id>
|
|
404
|
-
|
|
405
|
-
Related: capture log <path> [--name label] Tail a log into the active session
|
|
406
|
-
See also: capture --help Full command list`);
|
|
407
|
-
}
|
|
408
|
-
}
|
|
26
|
+
import { sessionMain, logCommand } from "./session/commands.js";
|
|
409
27
|
|
|
410
28
|
// ============================================================================
|
|
411
29
|
// CLI
|
|
@@ -451,6 +69,7 @@ async function main(): Promise<void> {
|
|
|
451
69
|
case "record":
|
|
452
70
|
case "navigate":
|
|
453
71
|
case "har":
|
|
72
|
+
case "lib":
|
|
454
73
|
case "network":
|
|
455
74
|
case "list":
|
|
456
75
|
return cdpMain();
|
|
@@ -507,6 +126,14 @@ DIAGNOSTICS & ONE-OFFS (no session needed)
|
|
|
507
126
|
network <offline|online> Toggle connectivity for a tab
|
|
508
127
|
har create | read [id] | delete <id> Manage standalone HAR recordings
|
|
509
128
|
|
|
129
|
+
LIBRARY (vault libs — dev checkout only)
|
|
130
|
+
|
|
131
|
+
lib list List available libs
|
|
132
|
+
lib search "<query>" [--limit N] Fuzzy-search functions across libs
|
|
133
|
+
lib show <name> Lib + function summaries
|
|
134
|
+
lib read <name> [fn…] Full input/output schemas (+ .ts source path)
|
|
135
|
+
exec "import {fn} from 'libs/<name>'; return await fn({…})" Run a lib in the active tab
|
|
136
|
+
|
|
510
137
|
TARGETING (only when NOT in a session, or picking a parallel tab)
|
|
511
138
|
|
|
512
139
|
--target <tabId> Exact id; a prefix of 8 chars is enough (preferred, parallel-safe)
|
package/src/cdp/a11y.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { type CDPClient } from './client.js';
|
|
2
|
+
|
|
3
|
+
export const INTERACTIVE_ROLES = new Set([
|
|
4
|
+
'button',
|
|
5
|
+
'link',
|
|
6
|
+
'textbox',
|
|
7
|
+
'searchbox',
|
|
8
|
+
'combobox',
|
|
9
|
+
'checkbox',
|
|
10
|
+
'radio',
|
|
11
|
+
'menuitem',
|
|
12
|
+
'tab',
|
|
13
|
+
'switch',
|
|
14
|
+
'slider',
|
|
15
|
+
'spinbutton',
|
|
16
|
+
'option',
|
|
17
|
+
'menuitemcheckbox',
|
|
18
|
+
'menuitemradio',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
export function flattenA11yTree(
|
|
22
|
+
nodes: A11yNode[],
|
|
23
|
+
opts: { interactive?: boolean } = {},
|
|
24
|
+
depth = 0,
|
|
25
|
+
): string[] {
|
|
26
|
+
const lines: string[] = [];
|
|
27
|
+
|
|
28
|
+
for (const node of nodes) {
|
|
29
|
+
// Filter to interactive roles if requested
|
|
30
|
+
if (opts.interactive && !INTERACTIVE_ROLES.has(node.role)) {
|
|
31
|
+
lines.push(...flattenA11yTree(node.children, opts, depth));
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Skip nodes without meaningful role or name
|
|
36
|
+
if (!node.role || node.role === 'StaticText') {
|
|
37
|
+
lines.push(...flattenA11yTree(node.children, opts, depth));
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const indent = ' '.repeat(depth);
|
|
42
|
+
const nameStr = node.name ? ` "${node.name}"` : '';
|
|
43
|
+
lines.push(`${indent}${node.role}${nameStr}`);
|
|
44
|
+
|
|
45
|
+
// Recurse
|
|
46
|
+
lines.push(...flattenA11yTree(node.children, opts, depth + 1));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return lines;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface AXNode {
|
|
53
|
+
nodeId: string;
|
|
54
|
+
role?: { value: string };
|
|
55
|
+
name?: { value: string };
|
|
56
|
+
childIds?: string[];
|
|
57
|
+
properties?: Array<{ name: string; value: { value: unknown } }>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface A11yNode {
|
|
61
|
+
role: string;
|
|
62
|
+
name: string;
|
|
63
|
+
children: A11yNode[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function getAccessibilityTree(
|
|
67
|
+
client: CDPClient,
|
|
68
|
+
): Promise<A11yNode[]> {
|
|
69
|
+
await client.send('Accessibility.enable');
|
|
70
|
+
const result = (await client.send('Accessibility.getFullAXTree')) as {
|
|
71
|
+
nodes: AXNode[];
|
|
72
|
+
};
|
|
73
|
+
await client.send('Accessibility.disable');
|
|
74
|
+
|
|
75
|
+
const nodeMap = new Map<string, AXNode>();
|
|
76
|
+
for (const node of result.nodes) {
|
|
77
|
+
nodeMap.set(node.nodeId, node);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const SKIP_ROLES = new Set(['none', 'generic', 'InlineTextBox']);
|
|
81
|
+
|
|
82
|
+
function buildNode(node: AXNode): A11yNode[] {
|
|
83
|
+
const role = node.role?.value ?? '';
|
|
84
|
+
const childNodes = (node.childIds ?? []).flatMap((id) => {
|
|
85
|
+
const child = nodeMap.get(id);
|
|
86
|
+
return child ? buildNode(child) : [];
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (SKIP_ROLES.has(role)) {
|
|
90
|
+
return childNodes;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return [
|
|
94
|
+
{
|
|
95
|
+
role,
|
|
96
|
+
name: node.name?.value ?? '',
|
|
97
|
+
children: childNodes,
|
|
98
|
+
},
|
|
99
|
+
];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const root = result.nodes[0];
|
|
103
|
+
return root ? buildNode(root) : [];
|
|
104
|
+
}
|
package/src/cdp/args.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { type ParsedArgs } from './types.js';
|
|
2
|
+
import { getActiveSession } from '../session-context.js';
|
|
3
|
+
|
|
4
|
+
export function parseCliArgs(argv: string[]): ParsedArgs {
|
|
5
|
+
const command = argv[0] ?? '';
|
|
6
|
+
const positional: string[] = [];
|
|
7
|
+
const parsed: Partial<ParsedArgs> = { command, positional };
|
|
8
|
+
|
|
9
|
+
for (let i = 1; i < argv.length; i++) {
|
|
10
|
+
const arg = argv[i];
|
|
11
|
+
const next = argv[i + 1];
|
|
12
|
+
|
|
13
|
+
if (arg === '--port' && next) {
|
|
14
|
+
parsed.port = parseInt(next, 10);
|
|
15
|
+
i++;
|
|
16
|
+
} else if (arg === '--out' && next) {
|
|
17
|
+
parsed.out = next;
|
|
18
|
+
i++;
|
|
19
|
+
} else if (arg === '--json') {
|
|
20
|
+
parsed.json = true;
|
|
21
|
+
} else if (arg === '--interactive') {
|
|
22
|
+
parsed.interactive = true;
|
|
23
|
+
} else if (arg === '--har-out' && next) {
|
|
24
|
+
parsed.harOut = next;
|
|
25
|
+
i++;
|
|
26
|
+
} else if (arg === '--record') {
|
|
27
|
+
parsed.record = true;
|
|
28
|
+
} else if (arg === '--duration' && next) {
|
|
29
|
+
parsed.duration = parseInt(next, 10);
|
|
30
|
+
i++;
|
|
31
|
+
} else if (arg === '--settle' && next) {
|
|
32
|
+
parsed.settle = parseInt(next, 10);
|
|
33
|
+
i++;
|
|
34
|
+
} else if (arg === '--file' && next) {
|
|
35
|
+
parsed.file = next;
|
|
36
|
+
i++;
|
|
37
|
+
} else if (arg === '--nested') {
|
|
38
|
+
parsed.nested = true;
|
|
39
|
+
} else if (arg === '--har' && next) {
|
|
40
|
+
parsed.har = next;
|
|
41
|
+
i++;
|
|
42
|
+
} else if (arg === '--new') {
|
|
43
|
+
parsed.new = true;
|
|
44
|
+
} else if (arg === '--target' && next) {
|
|
45
|
+
parsed.target = next;
|
|
46
|
+
i++;
|
|
47
|
+
} else if (arg === '--role' && next) {
|
|
48
|
+
parsed.role = next;
|
|
49
|
+
i++;
|
|
50
|
+
} else if (arg === '--into' && next) {
|
|
51
|
+
parsed.into = next;
|
|
52
|
+
i++;
|
|
53
|
+
} else if (arg === '--no-screenshot') {
|
|
54
|
+
parsed.noScreenshot = true;
|
|
55
|
+
} else if (arg === '--viewport' && next) {
|
|
56
|
+
parsed.viewport = next;
|
|
57
|
+
i++;
|
|
58
|
+
} else if (arg === '--full-page') {
|
|
59
|
+
parsed.fullPage = true;
|
|
60
|
+
} else if (arg === '--height' && next) {
|
|
61
|
+
parsed.height = parseInt(next, 10);
|
|
62
|
+
i++;
|
|
63
|
+
} else if (arg === '--filter-url' && next) {
|
|
64
|
+
parsed.filterUrl = next;
|
|
65
|
+
i++;
|
|
66
|
+
} else if (arg === '--filter-status' && next) {
|
|
67
|
+
parsed.filterStatus = next;
|
|
68
|
+
i++;
|
|
69
|
+
} else if (arg === '--filter-method' && next) {
|
|
70
|
+
parsed.filterMethod = next.toUpperCase();
|
|
71
|
+
i++;
|
|
72
|
+
} else if (arg === '--limit' && next) {
|
|
73
|
+
parsed.limit = parseInt(next, 10);
|
|
74
|
+
i++;
|
|
75
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
76
|
+
parsed.help = true;
|
|
77
|
+
} else if (arg.startsWith('--')) {
|
|
78
|
+
console.error(`Unknown flag: ${arg}`);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
} else {
|
|
81
|
+
positional.push(arg);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Fill gaps from environment variables (set by pipeline orchestrators)
|
|
86
|
+
if (!parsed.target && process.env.CDP_TARGET) {
|
|
87
|
+
parsed.target = process.env.CDP_TARGET;
|
|
88
|
+
}
|
|
89
|
+
if (!parsed.har && process.env.CDP_HAR_ID) {
|
|
90
|
+
parsed.har = process.env.CDP_HAR_ID;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Fill remaining gaps from active session context (explicit flags always win)
|
|
94
|
+
const session = getActiveSession();
|
|
95
|
+
if (session) {
|
|
96
|
+
if (!parsed.har && session.harId) parsed.har = session.harId;
|
|
97
|
+
if (!parsed.target && session.targetId) parsed.target = session.targetId;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return parsed as ParsedArgs;
|
|
101
|
+
}
|