@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/bin/capture
CHANGED
|
@@ -32,50 +32,6 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
32
32
|
mod
|
|
33
33
|
));
|
|
34
34
|
|
|
35
|
-
// src/har-manager.ts
|
|
36
|
-
function harFilePath(id) {
|
|
37
|
-
return path.join(HAR_DIR, `${id}.json`);
|
|
38
|
-
}
|
|
39
|
-
function createHarRecording() {
|
|
40
|
-
const id = Math.random().toString(36).slice(2, 8);
|
|
41
|
-
const harPath = harFilePath(id);
|
|
42
|
-
fs.mkdirSync(HAR_DIR, { recursive: true });
|
|
43
|
-
fs.writeFileSync(harPath, JSON.stringify({ log: { entries: [] } }, null, 2));
|
|
44
|
-
return { id, path: harPath };
|
|
45
|
-
}
|
|
46
|
-
function readHarRecording(id) {
|
|
47
|
-
try {
|
|
48
|
-
return JSON.parse(fs.readFileSync(harFilePath(id), "utf-8"));
|
|
49
|
-
} catch {
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
function appendToHarRecording(id, entries) {
|
|
54
|
-
const harPath = harFilePath(id);
|
|
55
|
-
let har;
|
|
56
|
-
try {
|
|
57
|
-
har = JSON.parse(fs.readFileSync(harPath, "utf-8"));
|
|
58
|
-
} catch {
|
|
59
|
-
har = { log: { entries: [] } };
|
|
60
|
-
}
|
|
61
|
-
har.log.entries.push(...entries);
|
|
62
|
-
fs.writeFileSync(harPath, JSON.stringify(har, null, 2));
|
|
63
|
-
}
|
|
64
|
-
function deleteHarRecording(id) {
|
|
65
|
-
try {
|
|
66
|
-
fs.unlinkSync(harFilePath(id));
|
|
67
|
-
} catch {
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
var fs, path, HAR_DIR;
|
|
71
|
-
var init_har_manager = __esm({
|
|
72
|
-
"src/har-manager.ts"() {
|
|
73
|
-
fs = __toESM(require("fs"));
|
|
74
|
-
path = __toESM(require("path"));
|
|
75
|
-
HAR_DIR = "/tmp/capture-har";
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
|
|
79
35
|
// node_modules/.pnpm/ws@8.20.0/node_modules/ws/lib/constants.js
|
|
80
36
|
var require_constants = __commonJS({
|
|
81
37
|
"node_modules/.pnpm/ws@8.20.0/node_modules/ws/lib/constants.js"(exports2, module2) {
|
|
@@ -2279,7 +2235,7 @@ var require_websocket = __commonJS({
|
|
|
2279
2235
|
var http = require("http");
|
|
2280
2236
|
var net = require("net");
|
|
2281
2237
|
var tls = require("tls");
|
|
2282
|
-
var { randomBytes, createHash } = require("crypto");
|
|
2238
|
+
var { randomBytes, createHash: createHash2 } = require("crypto");
|
|
2283
2239
|
var { Duplex, Readable } = require("stream");
|
|
2284
2240
|
var { URL: URL2 } = require("url");
|
|
2285
2241
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
@@ -2939,7 +2895,7 @@ var require_websocket = __commonJS({
|
|
|
2939
2895
|
abortHandshake(websocket, socket, "Invalid Upgrade header");
|
|
2940
2896
|
return;
|
|
2941
2897
|
}
|
|
2942
|
-
const digest =
|
|
2898
|
+
const digest = createHash2("sha1").update(key + GUID).digest("base64");
|
|
2943
2899
|
if (res.headers["sec-websocket-accept"] !== digest) {
|
|
2944
2900
|
abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
|
|
2945
2901
|
return;
|
|
@@ -3306,7 +3262,7 @@ var require_websocket_server = __commonJS({
|
|
|
3306
3262
|
var EventEmitter = require("events");
|
|
3307
3263
|
var http = require("http");
|
|
3308
3264
|
var { Duplex } = require("stream");
|
|
3309
|
-
var { createHash } = require("crypto");
|
|
3265
|
+
var { createHash: createHash2 } = require("crypto");
|
|
3310
3266
|
var extension2 = require_extension();
|
|
3311
3267
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
3312
3268
|
var subprotocol2 = require_subprotocol();
|
|
@@ -3607,7 +3563,7 @@ var require_websocket_server = __commonJS({
|
|
|
3607
3563
|
);
|
|
3608
3564
|
}
|
|
3609
3565
|
if (this._state > RUNNING) return abortHandshake(socket, 503);
|
|
3610
|
-
const digest =
|
|
3566
|
+
const digest = createHash2("sha1").update(key + GUID).digest("base64");
|
|
3611
3567
|
const headers = [
|
|
3612
3568
|
"HTTP/1.1 101 Switching Protocols",
|
|
3613
3569
|
"Upgrade: websocket",
|
|
@@ -3708,377 +3664,418 @@ var init_wrapper = __esm({
|
|
|
3708
3664
|
}
|
|
3709
3665
|
});
|
|
3710
3666
|
|
|
3711
|
-
// src/
|
|
3712
|
-
|
|
3667
|
+
// src/cdp/client.ts
|
|
3668
|
+
var CDPClient;
|
|
3669
|
+
var init_client = __esm({
|
|
3670
|
+
"src/cdp/client.ts"() {
|
|
3671
|
+
init_wrapper();
|
|
3672
|
+
CDPClient = class {
|
|
3673
|
+
ws;
|
|
3674
|
+
messageId = 0;
|
|
3675
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
3676
|
+
pendingTimeouts = /* @__PURE__ */ new Map();
|
|
3677
|
+
eventHandlers = /* @__PURE__ */ new Map();
|
|
3678
|
+
ready;
|
|
3679
|
+
constructor(wsUrl, connectTimeout = 5e3) {
|
|
3680
|
+
this.ws = new wrapper_default(wsUrl);
|
|
3681
|
+
this.ready = new Promise((resolve4, reject) => {
|
|
3682
|
+
const timeout = setTimeout(() => {
|
|
3683
|
+
reject(new Error(`WebSocket connection timeout (${connectTimeout}ms)`));
|
|
3684
|
+
}, connectTimeout);
|
|
3685
|
+
this.ws.on("open", () => {
|
|
3686
|
+
clearTimeout(timeout);
|
|
3687
|
+
resolve4();
|
|
3688
|
+
});
|
|
3689
|
+
this.ws.on("error", (err) => {
|
|
3690
|
+
clearTimeout(timeout);
|
|
3691
|
+
reject(err);
|
|
3692
|
+
});
|
|
3693
|
+
});
|
|
3694
|
+
this.ws.on("message", (data) => {
|
|
3695
|
+
const msg = JSON.parse(data.toString());
|
|
3696
|
+
if (msg.id !== void 0) {
|
|
3697
|
+
const pending = this.pendingRequests.get(msg.id);
|
|
3698
|
+
if (pending) {
|
|
3699
|
+
this.pendingRequests.delete(msg.id);
|
|
3700
|
+
const timer = this.pendingTimeouts.get(msg.id);
|
|
3701
|
+
if (timer) {
|
|
3702
|
+
clearTimeout(timer);
|
|
3703
|
+
this.pendingTimeouts.delete(msg.id);
|
|
3704
|
+
}
|
|
3705
|
+
if (msg.error) {
|
|
3706
|
+
pending.reject(new Error(msg.error.message));
|
|
3707
|
+
} else {
|
|
3708
|
+
pending.resolve(msg.result);
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
} else if (msg.method) {
|
|
3712
|
+
const handlers = this.eventHandlers.get(msg.method);
|
|
3713
|
+
handlers?.forEach((h) => h(msg.params));
|
|
3714
|
+
}
|
|
3715
|
+
});
|
|
3716
|
+
}
|
|
3717
|
+
async waitReady() {
|
|
3718
|
+
await this.ready;
|
|
3719
|
+
}
|
|
3720
|
+
async send(method, params = {}, timeout = 6e4) {
|
|
3721
|
+
await this.ready;
|
|
3722
|
+
const id = ++this.messageId;
|
|
3723
|
+
return new Promise((resolve4, reject) => {
|
|
3724
|
+
this.pendingRequests.set(id, { resolve: resolve4, reject });
|
|
3725
|
+
this.ws.send(JSON.stringify({ id, method, params }));
|
|
3726
|
+
const timer = setTimeout(() => {
|
|
3727
|
+
if (this.pendingRequests.has(id)) {
|
|
3728
|
+
this.pendingRequests.delete(id);
|
|
3729
|
+
this.pendingTimeouts.delete(id);
|
|
3730
|
+
reject(new Error(`CDP request timeout (${timeout}ms): ${method}`));
|
|
3731
|
+
}
|
|
3732
|
+
}, timeout);
|
|
3733
|
+
this.pendingTimeouts.set(id, timer);
|
|
3734
|
+
});
|
|
3735
|
+
}
|
|
3736
|
+
on(event, handler) {
|
|
3737
|
+
if (!this.eventHandlers.has(event)) {
|
|
3738
|
+
this.eventHandlers.set(event, []);
|
|
3739
|
+
}
|
|
3740
|
+
this.eventHandlers.get(event).push(handler);
|
|
3741
|
+
}
|
|
3742
|
+
close() {
|
|
3743
|
+
for (const timer of this.pendingTimeouts.values()) {
|
|
3744
|
+
clearTimeout(timer);
|
|
3745
|
+
}
|
|
3746
|
+
this.pendingTimeouts.clear();
|
|
3747
|
+
this.ws.close();
|
|
3748
|
+
}
|
|
3749
|
+
};
|
|
3750
|
+
}
|
|
3751
|
+
});
|
|
3752
|
+
|
|
3753
|
+
// src/cdp/detect.ts
|
|
3754
|
+
function getDefaultBrowserId() {
|
|
3713
3755
|
try {
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3756
|
+
const output = (0, import_child_process.execSync)(
|
|
3757
|
+
"defaults read com.apple.LaunchServices/com.apple.launchservices.secure LSHandlers 2>/dev/null",
|
|
3758
|
+
{ encoding: "utf-8" }
|
|
3759
|
+
);
|
|
3760
|
+
const match = output.match(
|
|
3761
|
+
/LSHandlerRoleAll = "([^"]+)";\s*LSHandlerURLScheme = https;/
|
|
3762
|
+
);
|
|
3763
|
+
return match?.[1] ?? null;
|
|
3721
3764
|
} catch {
|
|
3722
3765
|
return null;
|
|
3723
3766
|
}
|
|
3724
3767
|
}
|
|
3725
|
-
function
|
|
3726
|
-
fs2.mkdirSync(CAPTURE_ROOT, { recursive: true });
|
|
3727
|
-
fs2.writeFileSync(ACTIVE_PATH, JSON.stringify(state, null, 2));
|
|
3728
|
-
}
|
|
3729
|
-
function clearActiveSession() {
|
|
3768
|
+
async function probeCdpPort(port) {
|
|
3730
3769
|
try {
|
|
3731
|
-
|
|
3770
|
+
const controller = new AbortController();
|
|
3771
|
+
const timeout = setTimeout(() => controller.abort(), 500);
|
|
3772
|
+
const resp = await fetch(`http://localhost:${port}/json/version`, {
|
|
3773
|
+
signal: controller.signal
|
|
3774
|
+
});
|
|
3775
|
+
clearTimeout(timeout);
|
|
3776
|
+
if (resp.ok) {
|
|
3777
|
+
const version = await resp.json();
|
|
3778
|
+
const browser = version.Browser;
|
|
3779
|
+
const userAgent = version["User-Agent"] ?? "";
|
|
3780
|
+
if (!browser) return null;
|
|
3781
|
+
const isElectron = userAgent.includes("Electron");
|
|
3782
|
+
let app = browser;
|
|
3783
|
+
if (isElectron) {
|
|
3784
|
+
const appMatch = userAgent.match(
|
|
3785
|
+
/(\S+?)\/[\d.]+ Chrome\/[\d.]+ Electron/
|
|
3786
|
+
);
|
|
3787
|
+
if (appMatch) app = appMatch[1];
|
|
3788
|
+
}
|
|
3789
|
+
return { browser, app, isElectron };
|
|
3790
|
+
}
|
|
3732
3791
|
} catch {
|
|
3733
3792
|
}
|
|
3793
|
+
return null;
|
|
3734
3794
|
}
|
|
3735
|
-
function
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3795
|
+
function getLocalhostListeningPorts() {
|
|
3796
|
+
try {
|
|
3797
|
+
const output = (0, import_child_process.execSync)(
|
|
3798
|
+
'netstat -an 2>/dev/null | grep "127.0.0.1.*LISTEN"',
|
|
3799
|
+
{ encoding: "utf-8" }
|
|
3800
|
+
);
|
|
3801
|
+
const ports = [];
|
|
3802
|
+
for (const line of output.split("\n")) {
|
|
3803
|
+
const match = line.match(/127\.0\.0\.1\.(\d+)/);
|
|
3804
|
+
if (match) {
|
|
3805
|
+
ports.push(parseInt(match[1], 10));
|
|
3806
|
+
}
|
|
3807
|
+
}
|
|
3808
|
+
return ports;
|
|
3809
|
+
} catch {
|
|
3810
|
+
return [];
|
|
3811
|
+
}
|
|
3741
3812
|
}
|
|
3742
|
-
function
|
|
3743
|
-
|
|
3813
|
+
async function detectCdpPortsAsync() {
|
|
3814
|
+
const ports = getLocalhostListeningPorts();
|
|
3815
|
+
const results = [];
|
|
3816
|
+
const probes = ports.map(async (port) => {
|
|
3817
|
+
const probe = await probeCdpPort(port);
|
|
3818
|
+
if (probe) {
|
|
3819
|
+
const lower = probe.browser.toLowerCase();
|
|
3820
|
+
let bundleId = "unknown";
|
|
3821
|
+
for (const [bid, patterns] of Object.entries(BROWSER_PATTERNS)) {
|
|
3822
|
+
if (patterns.some((p) => lower.includes(p))) {
|
|
3823
|
+
bundleId = bid;
|
|
3824
|
+
break;
|
|
3825
|
+
}
|
|
3826
|
+
}
|
|
3827
|
+
results.push({
|
|
3828
|
+
port,
|
|
3829
|
+
app: probe.app,
|
|
3830
|
+
bundleId,
|
|
3831
|
+
isElectron: probe.isElectron
|
|
3832
|
+
});
|
|
3833
|
+
}
|
|
3834
|
+
});
|
|
3835
|
+
await Promise.all(probes);
|
|
3836
|
+
return results;
|
|
3744
3837
|
}
|
|
3745
|
-
function
|
|
3746
|
-
const
|
|
3747
|
-
if (
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
const
|
|
3753
|
-
|
|
3754
|
-
|
|
3838
|
+
async function detectCdpPort() {
|
|
3839
|
+
const endpoints = await detectCdpPortsAsync();
|
|
3840
|
+
if (endpoints.length === 0) {
|
|
3841
|
+
throw new Error(
|
|
3842
|
+
"No browser with CDP found. Start your browser with remote debugging enabled:\n Arc: Already enabled by default\n Chrome: --remote-debugging-port=9222\n Electron apps expose CDP automatically"
|
|
3843
|
+
);
|
|
3844
|
+
}
|
|
3845
|
+
const defaultBrowser = getDefaultBrowserId();
|
|
3846
|
+
if (defaultBrowser) {
|
|
3847
|
+
const match = endpoints.find(
|
|
3848
|
+
(p) => p.bundleId === defaultBrowser && !p.isElectron
|
|
3849
|
+
);
|
|
3850
|
+
if (match) return match.port;
|
|
3851
|
+
}
|
|
3852
|
+
const nonElectron = endpoints.find((p) => !p.isElectron);
|
|
3853
|
+
return (nonElectron ?? endpoints[0]).port;
|
|
3755
3854
|
}
|
|
3756
|
-
var
|
|
3757
|
-
var
|
|
3758
|
-
"src/
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3855
|
+
var import_child_process, BROWSER_PATTERNS;
|
|
3856
|
+
var init_detect = __esm({
|
|
3857
|
+
"src/cdp/detect.ts"() {
|
|
3858
|
+
import_child_process = require("child_process");
|
|
3859
|
+
BROWSER_PATTERNS = {
|
|
3860
|
+
"company.thebrowser.browser": ["arc"],
|
|
3861
|
+
"com.google.chrome": ["chrome", "google chrome"],
|
|
3862
|
+
"com.brave.browser": ["brave"],
|
|
3863
|
+
"com.microsoft.edgemac": ["edge", "microsoft edge"],
|
|
3864
|
+
"org.chromium.chromium": ["chromium"]
|
|
3865
|
+
};
|
|
3764
3866
|
}
|
|
3765
3867
|
});
|
|
3766
3868
|
|
|
3767
|
-
// src/
|
|
3768
|
-
async function
|
|
3769
|
-
await
|
|
3770
|
-
|
|
3771
|
-
const { nodes } = await client.send("Accessibility.getFullAXTree");
|
|
3772
|
-
await client.send("Accessibility.disable");
|
|
3773
|
-
const nameLower = name.toLowerCase();
|
|
3774
|
-
const matches = nodes.filter((n) => {
|
|
3775
|
-
if (!n.backendDOMNodeId) return false;
|
|
3776
|
-
if (!n.name?.value) return false;
|
|
3777
|
-
if (n.name.value.toLowerCase() !== nameLower) return false;
|
|
3778
|
-
if (role && n.role?.value !== role) return false;
|
|
3779
|
-
return true;
|
|
3780
|
-
});
|
|
3781
|
-
if (matches.length === 0) {
|
|
3869
|
+
// src/cdp/targets.ts
|
|
3870
|
+
async function getBrowserClient(port) {
|
|
3871
|
+
const versionResp = await fetch(`http://localhost:${port}/json/version`);
|
|
3872
|
+
if (!versionResp.ok) {
|
|
3782
3873
|
throw new Error(
|
|
3783
|
-
`
|
|
3874
|
+
`Failed to connect to CDP on port ${port}: ${versionResp.status}`
|
|
3784
3875
|
);
|
|
3785
3876
|
}
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3877
|
+
const version = await versionResp.json();
|
|
3878
|
+
const client = new CDPClient(version.webSocketDebuggerUrl);
|
|
3879
|
+
await client.waitReady();
|
|
3880
|
+
return { client, browserWsUrl: version.webSocketDebuggerUrl };
|
|
3881
|
+
}
|
|
3882
|
+
async function listTargets(port) {
|
|
3883
|
+
const { client } = await getBrowserClient(port);
|
|
3884
|
+
try {
|
|
3885
|
+
await client.send("Target.setDiscoverTargets", { discover: true });
|
|
3886
|
+
const result = await client.send("Target.getTargets");
|
|
3887
|
+
return result.targetInfos.map((t) => ({
|
|
3888
|
+
id: t.targetId,
|
|
3889
|
+
title: t.title,
|
|
3890
|
+
url: t.url,
|
|
3891
|
+
type: t.type,
|
|
3892
|
+
webSocketDebuggerUrl: `ws://localhost:${port}/devtools/page/${t.targetId}`
|
|
3893
|
+
}));
|
|
3894
|
+
} finally {
|
|
3895
|
+
client.close();
|
|
3896
|
+
}
|
|
3897
|
+
}
|
|
3898
|
+
async function findTab(port, urlPattern) {
|
|
3899
|
+
const targets = await listTargets(port);
|
|
3900
|
+
const pages = targets.filter((t) => t.type === "page");
|
|
3901
|
+
if (!urlPattern) {
|
|
3902
|
+
return pages[0] ?? null;
|
|
3903
|
+
}
|
|
3904
|
+
const pattern = urlPattern.toLowerCase();
|
|
3905
|
+
return pages.find((t) => t.url.toLowerCase().includes(pattern)) ?? null;
|
|
3906
|
+
}
|
|
3907
|
+
async function findTabById(port, targetId) {
|
|
3908
|
+
const targets = await listTargets(port);
|
|
3909
|
+
const exact = targets.find((t) => t.id === targetId);
|
|
3910
|
+
if (exact) return exact;
|
|
3911
|
+
if (targetId.length >= 4) {
|
|
3912
|
+
const prefix = targetId.toUpperCase();
|
|
3913
|
+
const matches = targets.filter((t) => t.id.toUpperCase().startsWith(prefix));
|
|
3914
|
+
if (matches.length === 1) return matches[0];
|
|
3915
|
+
if (matches.length > 1) {
|
|
3794
3916
|
throw new Error(
|
|
3795
|
-
`
|
|
3796
|
-
${
|
|
3917
|
+
`Ambiguous target prefix "${targetId}" matches ${matches.length} tabs:
|
|
3918
|
+
` + matches.map((t) => ` ${t.id.slice(0, 8)} ${t.url}`).join("\n")
|
|
3797
3919
|
);
|
|
3798
3920
|
}
|
|
3799
3921
|
}
|
|
3800
|
-
|
|
3801
|
-
const backendNodeId = match.backendDOMNodeId;
|
|
3802
|
-
await client.send("DOM.scrollIntoViewIfNeeded", { backendNodeId });
|
|
3803
|
-
const { model } = await client.send("DOM.getBoxModel", { backendNodeId });
|
|
3804
|
-
const quad = model.content;
|
|
3805
|
-
const x = (quad[0] + quad[2] + quad[4] + quad[6]) / 4;
|
|
3806
|
-
const y = (quad[1] + quad[3] + quad[5] + quad[7]) / 4;
|
|
3807
|
-
await client.send("Input.dispatchMouseEvent", {
|
|
3808
|
-
type: "mousePressed",
|
|
3809
|
-
x,
|
|
3810
|
-
y,
|
|
3811
|
-
button: "left",
|
|
3812
|
-
clickCount: 1
|
|
3813
|
-
});
|
|
3814
|
-
await client.send("Input.dispatchMouseEvent", {
|
|
3815
|
-
type: "mouseReleased",
|
|
3816
|
-
x,
|
|
3817
|
-
y,
|
|
3818
|
-
button: "left",
|
|
3819
|
-
clickCount: 1
|
|
3820
|
-
});
|
|
3821
|
-
return {
|
|
3822
|
-
x: Math.round(x),
|
|
3823
|
-
y: Math.round(y),
|
|
3824
|
-
role: match.role?.value ?? "unknown",
|
|
3825
|
-
name: match.name?.value ?? name
|
|
3826
|
-
};
|
|
3827
|
-
}
|
|
3828
|
-
async function typeText(client, text) {
|
|
3829
|
-
await client.send("Input.insertText", { text });
|
|
3922
|
+
return null;
|
|
3830
3923
|
}
|
|
3831
|
-
async function
|
|
3832
|
-
const
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3924
|
+
async function openTab(port, url) {
|
|
3925
|
+
const { client } = await getBrowserClient(port);
|
|
3926
|
+
try {
|
|
3927
|
+
const result = await client.send("Target.createTarget", {
|
|
3928
|
+
url,
|
|
3929
|
+
background: true
|
|
3930
|
+
});
|
|
3931
|
+
return {
|
|
3932
|
+
id: result.targetId,
|
|
3933
|
+
title: "",
|
|
3934
|
+
url,
|
|
3935
|
+
type: "page",
|
|
3936
|
+
webSocketDebuggerUrl: `ws://localhost:${port}/devtools/page/${result.targetId}`
|
|
3937
|
+
};
|
|
3938
|
+
} finally {
|
|
3939
|
+
client.close();
|
|
3940
|
+
}
|
|
3836
3941
|
}
|
|
3837
|
-
var
|
|
3838
|
-
"src/
|
|
3942
|
+
var init_targets = __esm({
|
|
3943
|
+
"src/cdp/targets.ts"() {
|
|
3944
|
+
init_client();
|
|
3839
3945
|
}
|
|
3840
3946
|
});
|
|
3841
3947
|
|
|
3842
|
-
// src/
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
cdpMain: () => main,
|
|
3851
|
-
detectCdpPort: () => detectCdpPort,
|
|
3852
|
-
executeInBrowser: () => executeInBrowser,
|
|
3853
|
-
findTab: () => findTab,
|
|
3854
|
-
findTabById: () => findTabById,
|
|
3855
|
-
getAccessibilityTree: () => getAccessibilityTree,
|
|
3856
|
-
isTabLocked: () => isTabLocked,
|
|
3857
|
-
listTargets: () => listTargets,
|
|
3858
|
-
navigateAndRecord: () => navigateAndRecord,
|
|
3859
|
-
navigateAndWait: () => navigateAndWait,
|
|
3860
|
-
openTab: () => openTab,
|
|
3861
|
-
recordTraffic: () => recordTraffic,
|
|
3862
|
-
releaseTabLock: () => releaseTabLock,
|
|
3863
|
-
withTabLock: () => withTabLock
|
|
3864
|
-
});
|
|
3865
|
-
function shouldRecordRequest(url) {
|
|
3866
|
-
if (SKIP_EXTENSIONS.test(url)) return false;
|
|
3867
|
-
if (SKIP_DOMAINS.test(url)) return false;
|
|
3868
|
-
return true;
|
|
3869
|
-
}
|
|
3870
|
-
function printConsoleSummary(entries) {
|
|
3871
|
-
const errors = entries.filter((e) => e.level === "error");
|
|
3872
|
-
const warnings = entries.filter((e) => e.level === "warning");
|
|
3873
|
-
if (errors.length > 0 || warnings.length > 0) {
|
|
3874
|
-
console.error(
|
|
3875
|
-
`
|
|
3876
|
-
Console: ${errors.length} errors, ${warnings.length} warnings`
|
|
3877
|
-
);
|
|
3878
|
-
for (const entry of [...errors, ...warnings]) {
|
|
3879
|
-
const prefix = entry.level === "error" ? "ERR" : "WARN";
|
|
3880
|
-
const src = entry.source ? ` (${entry.source})` : "";
|
|
3881
|
-
const text = entry.text.length > 200 ? entry.text.slice(0, 197) + "..." : entry.text;
|
|
3882
|
-
console.error(` [${prefix}] ${text}${src}`);
|
|
3948
|
+
// src/session-context.ts
|
|
3949
|
+
function getActiveSession() {
|
|
3950
|
+
try {
|
|
3951
|
+
if (!fs.existsSync(ACTIVE_PATH)) return null;
|
|
3952
|
+
const state = JSON.parse(fs.readFileSync(ACTIVE_PATH, "utf-8"));
|
|
3953
|
+
if (!fs.existsSync(state.dir)) {
|
|
3954
|
+
clearActiveSession();
|
|
3955
|
+
return null;
|
|
3883
3956
|
}
|
|
3957
|
+
return state;
|
|
3958
|
+
} catch {
|
|
3959
|
+
return null;
|
|
3884
3960
|
}
|
|
3885
3961
|
}
|
|
3886
|
-
function
|
|
3887
|
-
|
|
3888
|
-
|
|
3962
|
+
function setActiveSession(state) {
|
|
3963
|
+
fs.mkdirSync(CAPTURE_ROOT, { recursive: true });
|
|
3964
|
+
fs.writeFileSync(ACTIVE_PATH, JSON.stringify(state, null, 2));
|
|
3965
|
+
}
|
|
3966
|
+
function clearActiveSession() {
|
|
3889
3967
|
try {
|
|
3890
|
-
|
|
3891
|
-
} catch
|
|
3892
|
-
const stripped = {
|
|
3893
|
-
log: {
|
|
3894
|
-
...har.log,
|
|
3895
|
-
entries: har.log.entries.map((e) => ({
|
|
3896
|
-
...e,
|
|
3897
|
-
response: { ...e.response, content: { text: "[body stripped \u2014 too large to serialize]" } }
|
|
3898
|
-
}))
|
|
3899
|
-
}
|
|
3900
|
-
};
|
|
3901
|
-
fs3.writeFileSync(harPath, JSON.stringify(stripped, null, 2));
|
|
3902
|
-
console.error(" WARNING: Response bodies stripped from HAR (too large to serialize)");
|
|
3903
|
-
}
|
|
3904
|
-
const errors = har.log.entries.filter((e) => e.response.status >= 400);
|
|
3905
|
-
console.error(
|
|
3906
|
-
`
|
|
3907
|
-
HAR: ${har.log.entries.length} requests captured \u2192 ${harPath}`
|
|
3908
|
-
);
|
|
3909
|
-
if (errors.length > 0) {
|
|
3910
|
-
console.error(` ${errors.length} failed:`);
|
|
3911
|
-
for (const entry of errors) {
|
|
3912
|
-
const method = entry.request.method.padEnd(6);
|
|
3913
|
-
const status = entry.response.status;
|
|
3914
|
-
const url = entry.request.url.length > 100 ? entry.request.url.slice(0, 97) + "..." : entry.request.url;
|
|
3915
|
-
console.error(` ${method} ${status} ${url}`);
|
|
3916
|
-
}
|
|
3968
|
+
fs.unlinkSync(ACTIVE_PATH);
|
|
3969
|
+
} catch {
|
|
3917
3970
|
}
|
|
3918
|
-
return harPath;
|
|
3919
3971
|
}
|
|
3920
|
-
function
|
|
3921
|
-
const
|
|
3922
|
-
|
|
3923
|
-
const
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
parsed.out = next;
|
|
3932
|
-
i++;
|
|
3933
|
-
} else if (arg === "--json") {
|
|
3934
|
-
parsed.json = true;
|
|
3935
|
-
} else if (arg === "--interactive") {
|
|
3936
|
-
parsed.interactive = true;
|
|
3937
|
-
} else if (arg === "--har-out" && next) {
|
|
3938
|
-
parsed.harOut = next;
|
|
3939
|
-
i++;
|
|
3940
|
-
} else if (arg === "--record") {
|
|
3941
|
-
parsed.record = true;
|
|
3942
|
-
} else if (arg === "--duration" && next) {
|
|
3943
|
-
parsed.duration = parseInt(next, 10);
|
|
3944
|
-
i++;
|
|
3945
|
-
} else if (arg === "--settle" && next) {
|
|
3946
|
-
parsed.settle = parseInt(next, 10);
|
|
3947
|
-
i++;
|
|
3948
|
-
} else if (arg === "--file" && next) {
|
|
3949
|
-
parsed.file = next;
|
|
3950
|
-
i++;
|
|
3951
|
-
} else if (arg === "--nested") {
|
|
3952
|
-
parsed.nested = true;
|
|
3953
|
-
} else if (arg === "--har" && next) {
|
|
3954
|
-
parsed.har = next;
|
|
3955
|
-
i++;
|
|
3956
|
-
} else if (arg === "--new") {
|
|
3957
|
-
parsed.new = true;
|
|
3958
|
-
} else if (arg === "--target" && next) {
|
|
3959
|
-
parsed.target = next;
|
|
3960
|
-
i++;
|
|
3961
|
-
} else if (arg === "--role" && next) {
|
|
3962
|
-
parsed.role = next;
|
|
3963
|
-
i++;
|
|
3964
|
-
} else if (arg === "--into" && next) {
|
|
3965
|
-
parsed.into = next;
|
|
3966
|
-
i++;
|
|
3967
|
-
} else if (arg === "--no-screenshot") {
|
|
3968
|
-
parsed.noScreenshot = true;
|
|
3969
|
-
} else if (arg === "--viewport" && next) {
|
|
3970
|
-
parsed.viewport = next;
|
|
3971
|
-
i++;
|
|
3972
|
-
} else if (arg === "--full-page") {
|
|
3973
|
-
parsed.fullPage = true;
|
|
3974
|
-
} else if (arg === "--height" && next) {
|
|
3975
|
-
parsed.height = parseInt(next, 10);
|
|
3976
|
-
i++;
|
|
3977
|
-
} else if (arg === "--filter-url" && next) {
|
|
3978
|
-
parsed.filterUrl = next;
|
|
3979
|
-
i++;
|
|
3980
|
-
} else if (arg === "--filter-status" && next) {
|
|
3981
|
-
parsed.filterStatus = next;
|
|
3982
|
-
i++;
|
|
3983
|
-
} else if (arg === "--filter-method" && next) {
|
|
3984
|
-
parsed.filterMethod = next.toUpperCase();
|
|
3985
|
-
i++;
|
|
3986
|
-
} else if (arg === "--limit" && next) {
|
|
3987
|
-
parsed.limit = parseInt(next, 10);
|
|
3988
|
-
i++;
|
|
3989
|
-
} else if (arg === "--help" || arg === "-h") {
|
|
3990
|
-
parsed.help = true;
|
|
3991
|
-
} else if (arg.startsWith("--")) {
|
|
3992
|
-
console.error(`Unknown flag: ${arg}`);
|
|
3993
|
-
process.exit(1);
|
|
3994
|
-
} else {
|
|
3995
|
-
positional.push(arg);
|
|
3996
|
-
}
|
|
3997
|
-
}
|
|
3998
|
-
if (!parsed.target && process.env.CDP_TARGET) {
|
|
3999
|
-
parsed.target = process.env.CDP_TARGET;
|
|
4000
|
-
}
|
|
4001
|
-
if (!parsed.har && process.env.CDP_HAR_ID) {
|
|
4002
|
-
parsed.har = process.env.CDP_HAR_ID;
|
|
4003
|
-
}
|
|
3972
|
+
function updateActiveSession(patch) {
|
|
3973
|
+
const current = getActiveSession();
|
|
3974
|
+
if (!current) return null;
|
|
3975
|
+
const updated = { ...current, ...patch };
|
|
3976
|
+
setActiveSession(updated);
|
|
3977
|
+
return updated;
|
|
3978
|
+
}
|
|
3979
|
+
function sanitizeLabel(label) {
|
|
3980
|
+
return label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
|
|
3981
|
+
}
|
|
3982
|
+
function nextStepPath(action, label) {
|
|
4004
3983
|
const session = getActiveSession();
|
|
4005
|
-
if (session)
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
3984
|
+
if (!session) return null;
|
|
3985
|
+
const step = session.stepCount + 1;
|
|
3986
|
+
const nn = String(step).padStart(2, "0");
|
|
3987
|
+
const sanitized = sanitizeLabel(label);
|
|
3988
|
+
const filename = `${nn}-${action}-${sanitized}.png`;
|
|
3989
|
+
const shotPath = path.join(session.dir, "shots", filename);
|
|
3990
|
+
updateActiveSession({ stepCount: step });
|
|
3991
|
+
return shotPath;
|
|
4010
3992
|
}
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
3993
|
+
var fs, path, os, CAPTURE_ROOT, ACTIVE_PATH;
|
|
3994
|
+
var init_session_context = __esm({
|
|
3995
|
+
"src/session-context.ts"() {
|
|
3996
|
+
fs = __toESM(require("fs"));
|
|
3997
|
+
path = __toESM(require("path"));
|
|
3998
|
+
os = __toESM(require("os"));
|
|
3999
|
+
CAPTURE_ROOT = path.join(os.tmpdir(), "capture-sessions");
|
|
4000
|
+
ACTIVE_PATH = path.join(CAPTURE_ROOT, ".active");
|
|
4014
4001
|
}
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
}
|
|
4002
|
+
});
|
|
4003
|
+
|
|
4004
|
+
// src/cdp/screenshot.ts
|
|
4005
|
+
async function captureScreenshot(client, viewport, options) {
|
|
4006
|
+
const MAX_DIM = 1600;
|
|
4007
|
+
if (viewport) {
|
|
4008
|
+
await client.send("Emulation.setDeviceMetricsOverride", {
|
|
4009
|
+
width: viewport.width,
|
|
4010
|
+
height: viewport.height,
|
|
4011
|
+
deviceScaleFactor: 1,
|
|
4012
|
+
mobile: false
|
|
4013
|
+
});
|
|
4014
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
4027
4015
|
}
|
|
4028
|
-
if (
|
|
4029
|
-
const
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4016
|
+
if (options?.fullPage) {
|
|
4017
|
+
const layoutMetrics = await client.send("Page.getLayoutMetrics");
|
|
4018
|
+
const contentWidth = layoutMetrics.cssVisualViewport?.clientWidth ?? viewport?.width ?? 1280;
|
|
4019
|
+
const contentHeight = layoutMetrics.contentSize?.height ?? viewport?.height ?? 800;
|
|
4020
|
+
await client.send("Emulation.setDeviceMetricsOverride", {
|
|
4021
|
+
width: contentWidth,
|
|
4022
|
+
height: Math.ceil(contentHeight),
|
|
4023
|
+
deviceScaleFactor: 1,
|
|
4024
|
+
mobile: false
|
|
4025
|
+
});
|
|
4026
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
4033
4027
|
}
|
|
4034
|
-
|
|
4035
|
-
|
|
4028
|
+
let screenshotOpts = {
|
|
4029
|
+
format: "png",
|
|
4030
|
+
captureBeyondViewport: false
|
|
4031
|
+
};
|
|
4032
|
+
try {
|
|
4033
|
+
const metrics = await client.send("Page.getLayoutMetrics");
|
|
4034
|
+
const vw = metrics.cssVisualViewport?.clientWidth ?? 0;
|
|
4035
|
+
const vh = metrics.cssVisualViewport?.clientHeight ?? 0;
|
|
4036
|
+
const sx = metrics.cssVisualViewport?.pageX ?? 0;
|
|
4037
|
+
const sy = metrics.cssVisualViewport?.pageY ?? 0;
|
|
4038
|
+
const dprResult = await client.send("Runtime.evaluate", {
|
|
4039
|
+
expression: "window.devicePixelRatio",
|
|
4040
|
+
returnByValue: true
|
|
4041
|
+
});
|
|
4042
|
+
const dpr = dprResult.result.value ?? 1;
|
|
4043
|
+
const actualMaxSide = Math.max(vw, vh) * dpr;
|
|
4044
|
+
const scale = actualMaxSide > MAX_DIM ? MAX_DIM / actualMaxSide : 1 / dpr;
|
|
4045
|
+
screenshotOpts = {
|
|
4046
|
+
...screenshotOpts,
|
|
4047
|
+
clip: { x: sx, y: sy, width: vw, height: vh, scale }
|
|
4048
|
+
};
|
|
4049
|
+
} catch {
|
|
4036
4050
|
}
|
|
4037
|
-
const
|
|
4038
|
-
|
|
4039
|
-
|
|
4051
|
+
const result = await client.send(
|
|
4052
|
+
"Page.captureScreenshot",
|
|
4053
|
+
screenshotOpts
|
|
4054
|
+
);
|
|
4055
|
+
if (viewport || options?.fullPage) {
|
|
4056
|
+
await client.send("Emulation.clearDeviceMetricsOverride");
|
|
4040
4057
|
}
|
|
4041
|
-
|
|
4042
|
-
await client.waitReady();
|
|
4043
|
-
return { client, tab };
|
|
4058
|
+
return Buffer.from(result.data, "base64");
|
|
4044
4059
|
}
|
|
4045
|
-
async function
|
|
4046
|
-
|
|
4047
|
-
const
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
`ERROR: No HAR recording found for --har "${parsed.har}". Run 'har create' first.`
|
|
4055
|
-
);
|
|
4056
|
-
process.exit(1);
|
|
4057
|
-
}
|
|
4058
|
-
harRecorder = new HARRecorder(client);
|
|
4059
|
-
await harRecorder.start();
|
|
4060
|
-
}
|
|
4061
|
-
try {
|
|
4062
|
-
const result = await fn(client, tab);
|
|
4063
|
-
if (harRecorder && parsed.har) {
|
|
4064
|
-
const settle = opts.settle !== void 0 ? opts.settle : 3e3;
|
|
4065
|
-
if (settle > 0) {
|
|
4066
|
-
await new Promise((r) => setTimeout(r, settle));
|
|
4067
|
-
}
|
|
4068
|
-
const har = await harRecorder.finish();
|
|
4069
|
-
if (har.log.entries.length > 0) {
|
|
4070
|
-
appendToHarRecording(parsed.har, har.log.entries);
|
|
4071
|
-
console.error(
|
|
4072
|
-
` [har:${parsed.har}] +${har.log.entries.length} entries`
|
|
4073
|
-
);
|
|
4074
|
-
}
|
|
4075
|
-
}
|
|
4076
|
-
printConsoleSummary(consoleRecorder.finish());
|
|
4077
|
-
return result;
|
|
4078
|
-
} finally {
|
|
4079
|
-
client.close();
|
|
4080
|
-
}
|
|
4060
|
+
async function autoScreenshot(client, action, label, noScreenshot) {
|
|
4061
|
+
if (noScreenshot) return null;
|
|
4062
|
+
const shotPath = nextStepPath(action, label);
|
|
4063
|
+
if (!shotPath) return null;
|
|
4064
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
4065
|
+
const png = await captureScreenshot(client);
|
|
4066
|
+
fs2.writeFileSync(shotPath, png);
|
|
4067
|
+
console.error(` [screenshot] ${shotPath}`);
|
|
4068
|
+
return shotPath;
|
|
4081
4069
|
}
|
|
4070
|
+
var fs2;
|
|
4071
|
+
var init_screenshot = __esm({
|
|
4072
|
+
"src/cdp/screenshot.ts"() {
|
|
4073
|
+
fs2 = __toESM(require("fs"));
|
|
4074
|
+
init_session_context();
|
|
4075
|
+
}
|
|
4076
|
+
});
|
|
4077
|
+
|
|
4078
|
+
// src/cdp/a11y.ts
|
|
4082
4079
|
function flattenA11yTree(nodes, opts = {}, depth = 0) {
|
|
4083
4080
|
const lines = [];
|
|
4084
4081
|
for (const node of nodes) {
|
|
@@ -4097,263 +4094,251 @@ function flattenA11yTree(nodes, opts = {}, depth = 0) {
|
|
|
4097
4094
|
}
|
|
4098
4095
|
return lines;
|
|
4099
4096
|
}
|
|
4100
|
-
function
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
/LSHandlerRoleAll = "([^"]+)";\s*LSHandlerURLScheme = https;/
|
|
4108
|
-
);
|
|
4109
|
-
return match?.[1] ?? null;
|
|
4110
|
-
} catch {
|
|
4111
|
-
return null;
|
|
4097
|
+
async function getAccessibilityTree(client) {
|
|
4098
|
+
await client.send("Accessibility.enable");
|
|
4099
|
+
const result = await client.send("Accessibility.getFullAXTree");
|
|
4100
|
+
await client.send("Accessibility.disable");
|
|
4101
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
4102
|
+
for (const node of result.nodes) {
|
|
4103
|
+
nodeMap.set(node.nodeId, node);
|
|
4112
4104
|
}
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
const
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
signal: controller.signal
|
|
4105
|
+
const SKIP_ROLES = /* @__PURE__ */ new Set(["none", "generic", "InlineTextBox"]);
|
|
4106
|
+
function buildNode(node) {
|
|
4107
|
+
const role = node.role?.value ?? "";
|
|
4108
|
+
const childNodes = (node.childIds ?? []).flatMap((id) => {
|
|
4109
|
+
const child = nodeMap.get(id);
|
|
4110
|
+
return child ? buildNode(child) : [];
|
|
4120
4111
|
});
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
const version = await resp.json();
|
|
4124
|
-
const browser = version.Browser;
|
|
4125
|
-
const userAgent = version["User-Agent"] ?? "";
|
|
4126
|
-
if (!browser) return null;
|
|
4127
|
-
const isElectron = userAgent.includes("Electron");
|
|
4128
|
-
let app = browser;
|
|
4129
|
-
if (isElectron) {
|
|
4130
|
-
const appMatch = userAgent.match(
|
|
4131
|
-
/(\S+?)\/[\d.]+ Chrome\/[\d.]+ Electron/
|
|
4132
|
-
);
|
|
4133
|
-
if (appMatch) app = appMatch[1];
|
|
4134
|
-
}
|
|
4135
|
-
return { browser, app, isElectron };
|
|
4112
|
+
if (SKIP_ROLES.has(role)) {
|
|
4113
|
+
return childNodes;
|
|
4136
4114
|
}
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
try {
|
|
4143
|
-
const output = (0, import_child_process.execSync)(
|
|
4144
|
-
'netstat -an 2>/dev/null | grep "127.0.0.1.*LISTEN"',
|
|
4145
|
-
{ encoding: "utf-8" }
|
|
4146
|
-
);
|
|
4147
|
-
const ports = [];
|
|
4148
|
-
for (const line of output.split("\n")) {
|
|
4149
|
-
const match = line.match(/127\.0\.0\.1\.(\d+)/);
|
|
4150
|
-
if (match) {
|
|
4151
|
-
ports.push(parseInt(match[1], 10));
|
|
4115
|
+
return [
|
|
4116
|
+
{
|
|
4117
|
+
role,
|
|
4118
|
+
name: node.name?.value ?? "",
|
|
4119
|
+
children: childNodes
|
|
4152
4120
|
}
|
|
4153
|
-
|
|
4154
|
-
return ports;
|
|
4155
|
-
} catch {
|
|
4156
|
-
return [];
|
|
4121
|
+
];
|
|
4157
4122
|
}
|
|
4123
|
+
const root = result.nodes[0];
|
|
4124
|
+
return root ? buildNode(root) : [];
|
|
4158
4125
|
}
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
}
|
|
4180
|
-
});
|
|
4181
|
-
await Promise.all(probes);
|
|
4182
|
-
return results;
|
|
4183
|
-
}
|
|
4184
|
-
async function detectCdpPort() {
|
|
4185
|
-
const endpoints = await detectCdpPortsAsync();
|
|
4186
|
-
if (endpoints.length === 0) {
|
|
4187
|
-
throw new Error(
|
|
4188
|
-
"No browser with CDP found. Start your browser with remote debugging enabled:\n Arc: Already enabled by default\n Chrome: --remote-debugging-port=9222\n Electron apps expose CDP automatically"
|
|
4189
|
-
);
|
|
4126
|
+
var INTERACTIVE_ROLES;
|
|
4127
|
+
var init_a11y = __esm({
|
|
4128
|
+
"src/cdp/a11y.ts"() {
|
|
4129
|
+
INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
|
|
4130
|
+
"button",
|
|
4131
|
+
"link",
|
|
4132
|
+
"textbox",
|
|
4133
|
+
"searchbox",
|
|
4134
|
+
"combobox",
|
|
4135
|
+
"checkbox",
|
|
4136
|
+
"radio",
|
|
4137
|
+
"menuitem",
|
|
4138
|
+
"tab",
|
|
4139
|
+
"switch",
|
|
4140
|
+
"slider",
|
|
4141
|
+
"spinbutton",
|
|
4142
|
+
"option",
|
|
4143
|
+
"menuitemcheckbox",
|
|
4144
|
+
"menuitemradio"
|
|
4145
|
+
]);
|
|
4190
4146
|
}
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4147
|
+
});
|
|
4148
|
+
|
|
4149
|
+
// src/cdp/console-recorder.ts
|
|
4150
|
+
function printConsoleSummary(entries) {
|
|
4151
|
+
const errors = entries.filter((e) => e.level === "error");
|
|
4152
|
+
const warnings = entries.filter((e) => e.level === "warning");
|
|
4153
|
+
if (errors.length > 0 || warnings.length > 0) {
|
|
4154
|
+
console.error(
|
|
4155
|
+
`
|
|
4156
|
+
Console: ${errors.length} errors, ${warnings.length} warnings`
|
|
4195
4157
|
);
|
|
4196
|
-
|
|
4158
|
+
for (const entry of [...errors, ...warnings]) {
|
|
4159
|
+
const prefix = entry.level === "error" ? "ERR" : "WARN";
|
|
4160
|
+
const src = entry.source ? ` (${entry.source})` : "";
|
|
4161
|
+
const text = entry.text.length > 200 ? entry.text.slice(0, 197) + "..." : entry.text;
|
|
4162
|
+
console.error(` [${prefix}] ${text}${src}`);
|
|
4163
|
+
}
|
|
4197
4164
|
}
|
|
4198
|
-
const nonElectron = endpoints.find((p) => !p.isElectron);
|
|
4199
|
-
return (nonElectron ?? endpoints[0]).port;
|
|
4200
4165
|
}
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4166
|
+
var ConsoleRecorder;
|
|
4167
|
+
var init_console_recorder = __esm({
|
|
4168
|
+
"src/cdp/console-recorder.ts"() {
|
|
4169
|
+
ConsoleRecorder = class {
|
|
4170
|
+
constructor(client) {
|
|
4171
|
+
this.client = client;
|
|
4172
|
+
}
|
|
4173
|
+
entries = [];
|
|
4174
|
+
async start() {
|
|
4175
|
+
await this.client.send("Runtime.enable");
|
|
4176
|
+
this.client.on("Runtime.consoleAPICalled", (params) => {
|
|
4177
|
+
const p = params;
|
|
4178
|
+
const text = p.args.map(
|
|
4179
|
+
(a) => a.value !== void 0 ? String(a.value) : a.description ?? ""
|
|
4180
|
+
).join(" ");
|
|
4181
|
+
this.entries.push({
|
|
4182
|
+
level: p.type,
|
|
4183
|
+
text,
|
|
4184
|
+
timestamp: p.timestamp
|
|
4185
|
+
});
|
|
4186
|
+
});
|
|
4187
|
+
this.client.on("Runtime.exceptionThrown", (params) => {
|
|
4188
|
+
const p = params;
|
|
4189
|
+
this.entries.push({
|
|
4190
|
+
level: "error",
|
|
4191
|
+
text: p.exceptionDetails.exception?.description ?? p.exceptionDetails.text ?? "Unknown exception",
|
|
4192
|
+
timestamp: p.timestamp,
|
|
4193
|
+
source: p.exceptionDetails.url ? `${p.exceptionDetails.url}:${p.exceptionDetails.lineNumber}` : void 0
|
|
4194
|
+
});
|
|
4195
|
+
});
|
|
4196
|
+
}
|
|
4197
|
+
finish() {
|
|
4198
|
+
return [...this.entries];
|
|
4199
|
+
}
|
|
4200
|
+
};
|
|
4207
4201
|
}
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4202
|
+
});
|
|
4203
|
+
|
|
4204
|
+
// src/cdp/har-recorder.ts
|
|
4205
|
+
function shouldRecordRequest(url) {
|
|
4206
|
+
if (SKIP_EXTENSIONS.test(url)) return false;
|
|
4207
|
+
if (SKIP_DOMAINS.test(url)) return false;
|
|
4208
|
+
return true;
|
|
4212
4209
|
}
|
|
4213
|
-
|
|
4214
|
-
|
|
4210
|
+
var SKIP_EXTENSIONS, SKIP_DOMAINS, MAX_BODY_SIZE, HARRecorder;
|
|
4211
|
+
var init_har_recorder = __esm({
|
|
4212
|
+
"src/cdp/har-recorder.ts"() {
|
|
4213
|
+
SKIP_EXTENSIONS = /\.(png|jpg|jpeg|gif|webp|svg|ico|woff2?|ttf|eot|css|mp4|webm|mp3)(\?|$)/i;
|
|
4214
|
+
SKIP_DOMAINS = /(google-analytics|googletagmanager|doubleclick|facebook\.com\/tr|px\.ads|analytics|tracking|beacon|telemetry)/i;
|
|
4215
|
+
MAX_BODY_SIZE = 256 * 1024;
|
|
4216
|
+
HARRecorder = class {
|
|
4217
|
+
constructor(client) {
|
|
4218
|
+
this.client = client;
|
|
4219
|
+
}
|
|
4220
|
+
requests = /* @__PURE__ */ new Map();
|
|
4221
|
+
responses = /* @__PURE__ */ new Map();
|
|
4222
|
+
get responseCount() {
|
|
4223
|
+
return this.responses.size;
|
|
4224
|
+
}
|
|
4225
|
+
async start() {
|
|
4226
|
+
await this.client.send("Network.enable");
|
|
4227
|
+
this.client.on("Network.requestWillBeSent", (params) => {
|
|
4228
|
+
const p = params;
|
|
4229
|
+
if (!shouldRecordRequest(p.request.url)) return;
|
|
4230
|
+
this.requests.set(p.requestId, {
|
|
4231
|
+
requestId: p.requestId,
|
|
4232
|
+
url: p.request.url,
|
|
4233
|
+
method: p.request.method,
|
|
4234
|
+
headers: p.request.headers,
|
|
4235
|
+
postData: p.request.postData,
|
|
4236
|
+
timestamp: p.timestamp
|
|
4237
|
+
});
|
|
4238
|
+
});
|
|
4239
|
+
this.client.on("Network.responseReceived", (params) => {
|
|
4240
|
+
const p = params;
|
|
4241
|
+
if (!this.requests.has(p.requestId)) return;
|
|
4242
|
+
this.responses.set(p.requestId, {
|
|
4243
|
+
requestId: p.requestId,
|
|
4244
|
+
url: p.response.url,
|
|
4245
|
+
status: p.response.status,
|
|
4246
|
+
headers: p.response.headers,
|
|
4247
|
+
timestamp: p.timestamp
|
|
4248
|
+
});
|
|
4249
|
+
});
|
|
4250
|
+
}
|
|
4251
|
+
buildHar() {
|
|
4252
|
+
return {
|
|
4253
|
+
log: {
|
|
4254
|
+
entries: Array.from(this.responses.values()).map((resp) => {
|
|
4255
|
+
const req = this.requests.get(resp.requestId);
|
|
4256
|
+
return {
|
|
4257
|
+
startedDateTime: new Date(resp.timestamp * 1e3).toISOString(),
|
|
4258
|
+
request: {
|
|
4259
|
+
method: req?.method ?? "GET",
|
|
4260
|
+
url: resp.url,
|
|
4261
|
+
headers: Object.entries(req?.headers ?? {}).map(
|
|
4262
|
+
([name, value]) => ({ name, value })
|
|
4263
|
+
),
|
|
4264
|
+
postData: req?.postData ? { text: req.postData } : void 0
|
|
4265
|
+
},
|
|
4266
|
+
response: {
|
|
4267
|
+
status: resp.status,
|
|
4268
|
+
headers: Object.entries(resp.headers).map(([name, value]) => ({
|
|
4269
|
+
name,
|
|
4270
|
+
value
|
|
4271
|
+
})),
|
|
4272
|
+
content: { text: resp.body }
|
|
4273
|
+
}
|
|
4274
|
+
};
|
|
4275
|
+
})
|
|
4276
|
+
}
|
|
4277
|
+
};
|
|
4278
|
+
}
|
|
4279
|
+
async finish() {
|
|
4280
|
+
await Promise.allSettled(
|
|
4281
|
+
Array.from(this.responses.entries()).map(async ([reqId, resp]) => {
|
|
4282
|
+
const bodyResult = await this.client.send("Network.getResponseBody", {
|
|
4283
|
+
requestId: reqId
|
|
4284
|
+
}, 5e3);
|
|
4285
|
+
const decoded = bodyResult.base64Encoded ? Buffer.from(bodyResult.body, "base64").toString() : bodyResult.body;
|
|
4286
|
+
resp.body = decoded.length > MAX_BODY_SIZE ? decoded.slice(0, MAX_BODY_SIZE) + `
|
|
4287
|
+
[truncated: ${decoded.length} bytes]` : decoded;
|
|
4288
|
+
})
|
|
4289
|
+
);
|
|
4290
|
+
return this.buildHar();
|
|
4291
|
+
}
|
|
4292
|
+
finishPartial() {
|
|
4293
|
+
return this.buildHar();
|
|
4294
|
+
}
|
|
4295
|
+
};
|
|
4296
|
+
}
|
|
4297
|
+
});
|
|
4298
|
+
|
|
4299
|
+
// src/cdp/har-output.ts
|
|
4300
|
+
function writeHarAndPrintSummary(har, harOutPath) {
|
|
4301
|
+
if (har.log.entries.length === 0 && !harOutPath) return void 0;
|
|
4302
|
+
const harPath = harOutPath ?? `/tmp/capture-har-${Date.now()}.json`;
|
|
4215
4303
|
try {
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4304
|
+
fs3.writeFileSync(harPath, JSON.stringify(har, null, 2));
|
|
4305
|
+
} catch (err) {
|
|
4306
|
+
const stripped = {
|
|
4307
|
+
log: {
|
|
4308
|
+
...har.log,
|
|
4309
|
+
entries: har.log.entries.map((e) => ({
|
|
4310
|
+
...e,
|
|
4311
|
+
response: { ...e.response, content: { text: "[body stripped \u2014 too large to serialize]" } }
|
|
4312
|
+
}))
|
|
4313
|
+
}
|
|
4314
|
+
};
|
|
4315
|
+
fs3.writeFileSync(harPath, JSON.stringify(stripped, null, 2));
|
|
4316
|
+
console.error(" WARNING: Response bodies stripped from HAR (too large to serialize)");
|
|
4227
4317
|
}
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4318
|
+
const errors = har.log.entries.filter((e) => e.response.status >= 400);
|
|
4319
|
+
console.error(
|
|
4320
|
+
`
|
|
4321
|
+
HAR: ${har.log.entries.length} requests captured \u2192 ${harPath}`
|
|
4322
|
+
);
|
|
4323
|
+
if (errors.length > 0) {
|
|
4324
|
+
console.error(` ${errors.length} failed:`);
|
|
4325
|
+
for (const entry of errors) {
|
|
4326
|
+
const method = entry.request.method.padEnd(6);
|
|
4327
|
+
const status = entry.response.status;
|
|
4328
|
+
const url = entry.request.url.length > 100 ? entry.request.url.slice(0, 97) + "..." : entry.request.url;
|
|
4329
|
+
console.error(` ${method} ${status} ${url}`);
|
|
4330
|
+
}
|
|
4234
4331
|
}
|
|
4235
|
-
|
|
4236
|
-
return pages.find((t) => t.url.toLowerCase().includes(pattern)) ?? null;
|
|
4332
|
+
return harPath;
|
|
4237
4333
|
}
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
if (targetId.length >= 4) {
|
|
4243
|
-
const prefix = targetId.toUpperCase();
|
|
4244
|
-
const matches = targets.filter((t) => t.id.toUpperCase().startsWith(prefix));
|
|
4245
|
-
if (matches.length === 1) return matches[0];
|
|
4246
|
-
if (matches.length > 1) {
|
|
4247
|
-
throw new Error(
|
|
4248
|
-
`Ambiguous target prefix "${targetId}" matches ${matches.length} tabs:
|
|
4249
|
-
` + matches.map((t) => ` ${t.id.slice(0, 8)} ${t.url}`).join("\n")
|
|
4250
|
-
);
|
|
4251
|
-
}
|
|
4252
|
-
}
|
|
4253
|
-
return null;
|
|
4254
|
-
}
|
|
4255
|
-
async function openTab(port, url) {
|
|
4256
|
-
const { client } = await getBrowserClient(port);
|
|
4257
|
-
try {
|
|
4258
|
-
const result = await client.send("Target.createTarget", {
|
|
4259
|
-
url,
|
|
4260
|
-
background: true
|
|
4261
|
-
});
|
|
4262
|
-
return {
|
|
4263
|
-
id: result.targetId,
|
|
4264
|
-
title: "",
|
|
4265
|
-
url,
|
|
4266
|
-
type: "page",
|
|
4267
|
-
webSocketDebuggerUrl: `ws://localhost:${port}/devtools/page/${result.targetId}`
|
|
4268
|
-
};
|
|
4269
|
-
} finally {
|
|
4270
|
-
client.close();
|
|
4271
|
-
}
|
|
4272
|
-
}
|
|
4273
|
-
async function captureScreenshot(client, viewport, options) {
|
|
4274
|
-
const MAX_DIM = 1600;
|
|
4275
|
-
if (viewport) {
|
|
4276
|
-
await client.send("Emulation.setDeviceMetricsOverride", {
|
|
4277
|
-
width: viewport.width,
|
|
4278
|
-
height: viewport.height,
|
|
4279
|
-
deviceScaleFactor: 1,
|
|
4280
|
-
mobile: false
|
|
4281
|
-
});
|
|
4282
|
-
await new Promise((r) => setTimeout(r, 150));
|
|
4283
|
-
}
|
|
4284
|
-
if (options?.fullPage) {
|
|
4285
|
-
const layoutMetrics = await client.send("Page.getLayoutMetrics");
|
|
4286
|
-
const contentWidth = layoutMetrics.cssVisualViewport?.clientWidth ?? viewport?.width ?? 1280;
|
|
4287
|
-
const contentHeight = layoutMetrics.contentSize?.height ?? viewport?.height ?? 800;
|
|
4288
|
-
await client.send("Emulation.setDeviceMetricsOverride", {
|
|
4289
|
-
width: contentWidth,
|
|
4290
|
-
height: Math.ceil(contentHeight),
|
|
4291
|
-
deviceScaleFactor: 1,
|
|
4292
|
-
mobile: false
|
|
4293
|
-
});
|
|
4294
|
-
await new Promise((r) => setTimeout(r, 150));
|
|
4295
|
-
}
|
|
4296
|
-
let screenshotOpts = {
|
|
4297
|
-
format: "png",
|
|
4298
|
-
captureBeyondViewport: false
|
|
4299
|
-
};
|
|
4300
|
-
try {
|
|
4301
|
-
const metrics = await client.send("Page.getLayoutMetrics");
|
|
4302
|
-
const vw = metrics.cssVisualViewport?.clientWidth ?? 0;
|
|
4303
|
-
const vh = metrics.cssVisualViewport?.clientHeight ?? 0;
|
|
4304
|
-
const sx = metrics.cssVisualViewport?.pageX ?? 0;
|
|
4305
|
-
const sy = metrics.cssVisualViewport?.pageY ?? 0;
|
|
4306
|
-
const dprResult = await client.send("Runtime.evaluate", {
|
|
4307
|
-
expression: "window.devicePixelRatio",
|
|
4308
|
-
returnByValue: true
|
|
4309
|
-
});
|
|
4310
|
-
const dpr = dprResult.result.value ?? 1;
|
|
4311
|
-
const actualMaxSide = Math.max(vw, vh) * dpr;
|
|
4312
|
-
const scale = actualMaxSide > MAX_DIM ? MAX_DIM / actualMaxSide : 1 / dpr;
|
|
4313
|
-
screenshotOpts = {
|
|
4314
|
-
...screenshotOpts,
|
|
4315
|
-
clip: { x: sx, y: sy, width: vw, height: vh, scale }
|
|
4316
|
-
};
|
|
4317
|
-
} catch {
|
|
4318
|
-
}
|
|
4319
|
-
const result = await client.send(
|
|
4320
|
-
"Page.captureScreenshot",
|
|
4321
|
-
screenshotOpts
|
|
4322
|
-
);
|
|
4323
|
-
if (viewport || options?.fullPage) {
|
|
4324
|
-
await client.send("Emulation.clearDeviceMetricsOverride");
|
|
4325
|
-
}
|
|
4326
|
-
return Buffer.from(result.data, "base64");
|
|
4327
|
-
}
|
|
4328
|
-
async function getAccessibilityTree(client) {
|
|
4329
|
-
await client.send("Accessibility.enable");
|
|
4330
|
-
const result = await client.send("Accessibility.getFullAXTree");
|
|
4331
|
-
await client.send("Accessibility.disable");
|
|
4332
|
-
const nodeMap = /* @__PURE__ */ new Map();
|
|
4333
|
-
for (const node of result.nodes) {
|
|
4334
|
-
nodeMap.set(node.nodeId, node);
|
|
4335
|
-
}
|
|
4336
|
-
const SKIP_ROLES = /* @__PURE__ */ new Set(["none", "generic", "InlineTextBox"]);
|
|
4337
|
-
function buildNode(node) {
|
|
4338
|
-
const role = node.role?.value ?? "";
|
|
4339
|
-
const childNodes = (node.childIds ?? []).flatMap((id) => {
|
|
4340
|
-
const child = nodeMap.get(id);
|
|
4341
|
-
return child ? buildNode(child) : [];
|
|
4342
|
-
});
|
|
4343
|
-
if (SKIP_ROLES.has(role)) {
|
|
4344
|
-
return childNodes;
|
|
4345
|
-
}
|
|
4346
|
-
return [
|
|
4347
|
-
{
|
|
4348
|
-
role,
|
|
4349
|
-
name: node.name?.value ?? "",
|
|
4350
|
-
children: childNodes
|
|
4351
|
-
}
|
|
4352
|
-
];
|
|
4334
|
+
var fs3;
|
|
4335
|
+
var init_har_output = __esm({
|
|
4336
|
+
"src/cdp/har-output.ts"() {
|
|
4337
|
+
fs3 = __toESM(require("fs"));
|
|
4353
4338
|
}
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4339
|
+
});
|
|
4340
|
+
|
|
4341
|
+
// src/cdp/execute.ts
|
|
4357
4342
|
async function executeInBrowser(code, options = {}) {
|
|
4358
4343
|
const port = options.port ?? await detectCdpPort();
|
|
4359
4344
|
if (!options.targetId) {
|
|
@@ -4421,6 +4406,19 @@ async function executeInBrowser(code, options = {}) {
|
|
|
4421
4406
|
};
|
|
4422
4407
|
}
|
|
4423
4408
|
}
|
|
4409
|
+
var init_execute = __esm({
|
|
4410
|
+
"src/cdp/execute.ts"() {
|
|
4411
|
+
init_client();
|
|
4412
|
+
init_detect();
|
|
4413
|
+
init_targets();
|
|
4414
|
+
init_console_recorder();
|
|
4415
|
+
init_har_recorder();
|
|
4416
|
+
init_console_recorder();
|
|
4417
|
+
init_har_output();
|
|
4418
|
+
}
|
|
4419
|
+
});
|
|
4420
|
+
|
|
4421
|
+
// src/cdp/record.ts
|
|
4424
4422
|
async function recordTraffic(options) {
|
|
4425
4423
|
const port = options.port ?? await detectCdpPort();
|
|
4426
4424
|
const duration = options.duration ?? 10;
|
|
@@ -4493,14 +4491,14 @@ async function navigateAndRecord(options) {
|
|
|
4493
4491
|
try {
|
|
4494
4492
|
await Promise.race([
|
|
4495
4493
|
(async () => {
|
|
4496
|
-
await new Promise((
|
|
4494
|
+
await new Promise((resolve4) => {
|
|
4497
4495
|
const timer = setTimeout(() => {
|
|
4498
4496
|
console.error("Page load timeout (10s), continuing with settle...");
|
|
4499
|
-
|
|
4497
|
+
resolve4();
|
|
4500
4498
|
}, 1e4);
|
|
4501
4499
|
client.on("Page.loadEventFired", () => {
|
|
4502
4500
|
clearTimeout(timer);
|
|
4503
|
-
|
|
4501
|
+
resolve4();
|
|
4504
4502
|
});
|
|
4505
4503
|
});
|
|
4506
4504
|
console.error(`Settling for ${settle}ms...`);
|
|
@@ -4541,14 +4539,14 @@ async function navigateAndWait(port, url, options = {}) {
|
|
|
4541
4539
|
const client = new CDPClient(tab.webSocketDebuggerUrl);
|
|
4542
4540
|
await client.waitReady();
|
|
4543
4541
|
await client.send("Page.enable");
|
|
4544
|
-
await new Promise((
|
|
4542
|
+
await new Promise((resolve4, reject) => {
|
|
4545
4543
|
const timer = setTimeout(
|
|
4546
4544
|
() => reject(new Error(`Page load timeout: ${url}`)),
|
|
4547
4545
|
timeout
|
|
4548
4546
|
);
|
|
4549
4547
|
client.on("Page.loadEventFired", () => {
|
|
4550
4548
|
clearTimeout(timer);
|
|
4551
|
-
|
|
4549
|
+
resolve4();
|
|
4552
4550
|
});
|
|
4553
4551
|
});
|
|
4554
4552
|
client.close();
|
|
@@ -4556,8 +4554,19 @@ async function navigateAndWait(port, url, options = {}) {
|
|
|
4556
4554
|
const refreshed = targets.find((t) => t.id === tab.id);
|
|
4557
4555
|
return refreshed ?? tab;
|
|
4558
4556
|
}
|
|
4557
|
+
var init_record = __esm({
|
|
4558
|
+
"src/cdp/record.ts"() {
|
|
4559
|
+
init_client();
|
|
4560
|
+
init_detect();
|
|
4561
|
+
init_targets();
|
|
4562
|
+
init_har_recorder();
|
|
4563
|
+
init_har_output();
|
|
4564
|
+
}
|
|
4565
|
+
});
|
|
4566
|
+
|
|
4567
|
+
// src/cdp/tab-lock.ts
|
|
4559
4568
|
function lockPath(tabId) {
|
|
4560
|
-
return
|
|
4569
|
+
return path2.join(LOCK_DIR, `capture-cdp-tab-${tabId}.lock`);
|
|
4561
4570
|
}
|
|
4562
4571
|
function isPidAlive(pid) {
|
|
4563
4572
|
try {
|
|
@@ -4569,7 +4578,7 @@ function isPidAlive(pid) {
|
|
|
4569
4578
|
}
|
|
4570
4579
|
function isLockStale(filePath) {
|
|
4571
4580
|
try {
|
|
4572
|
-
const content =
|
|
4581
|
+
const content = fs4.readFileSync(filePath, "utf-8");
|
|
4573
4582
|
const { pid, ts } = JSON.parse(content);
|
|
4574
4583
|
if (!isPidAlive(pid)) return true;
|
|
4575
4584
|
return Date.now() - ts > LOCK_STALE_MS;
|
|
@@ -4583,7 +4592,7 @@ function registerCleanup() {
|
|
|
4583
4592
|
const cleanup = () => {
|
|
4584
4593
|
for (const file of heldLocks) {
|
|
4585
4594
|
try {
|
|
4586
|
-
|
|
4595
|
+
fs4.unlinkSync(file);
|
|
4587
4596
|
} catch {
|
|
4588
4597
|
}
|
|
4589
4598
|
}
|
|
@@ -4606,9 +4615,9 @@ async function acquireTabLock(tabId) {
|
|
|
4606
4615
|
let delay = LOCK_POLL_BASE_MS;
|
|
4607
4616
|
while (true) {
|
|
4608
4617
|
try {
|
|
4609
|
-
const fd =
|
|
4610
|
-
|
|
4611
|
-
|
|
4618
|
+
const fd = fs4.openSync(file, "wx");
|
|
4619
|
+
fs4.writeSync(fd, JSON.stringify({ pid: process.pid, ts: Date.now() }));
|
|
4620
|
+
fs4.closeSync(fd);
|
|
4612
4621
|
heldLocks.add(file);
|
|
4613
4622
|
return;
|
|
4614
4623
|
} catch (err) {
|
|
@@ -4616,7 +4625,7 @@ async function acquireTabLock(tabId) {
|
|
|
4616
4625
|
}
|
|
4617
4626
|
if (isLockStale(file)) {
|
|
4618
4627
|
try {
|
|
4619
|
-
|
|
4628
|
+
fs4.unlinkSync(file);
|
|
4620
4629
|
} catch {
|
|
4621
4630
|
}
|
|
4622
4631
|
continue;
|
|
@@ -4633,7 +4642,7 @@ async function acquireTabLock(tabId) {
|
|
|
4633
4642
|
function isTabLocked(tabId) {
|
|
4634
4643
|
const file = lockPath(tabId);
|
|
4635
4644
|
try {
|
|
4636
|
-
|
|
4645
|
+
fs4.accessSync(file);
|
|
4637
4646
|
return !isLockStale(file);
|
|
4638
4647
|
} catch {
|
|
4639
4648
|
return false;
|
|
@@ -4643,7 +4652,7 @@ function releaseTabLock(tabId) {
|
|
|
4643
4652
|
const file = lockPath(tabId);
|
|
4644
4653
|
heldLocks.delete(file);
|
|
4645
4654
|
try {
|
|
4646
|
-
|
|
4655
|
+
fs4.unlinkSync(file);
|
|
4647
4656
|
} catch {
|
|
4648
4657
|
}
|
|
4649
4658
|
}
|
|
@@ -4655,588 +4664,124 @@ async function withTabLock(tabId, fn) {
|
|
|
4655
4664
|
releaseTabLock(tabId);
|
|
4656
4665
|
}
|
|
4657
4666
|
}
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
case "detect": {
|
|
4673
|
-
if (parsed.help) {
|
|
4674
|
-
console.log(
|
|
4675
|
-
"Usage: capture detect\n\nDetect CDP port (prioritizes default browser).\nReturns JSON with port, app name, and all discovered endpoints."
|
|
4676
|
-
);
|
|
4677
|
-
process.exit(0);
|
|
4678
|
-
}
|
|
4679
|
-
const endpoints = await detectCdpPortsAsync();
|
|
4680
|
-
if (endpoints.length === 0) {
|
|
4681
|
-
console.error(
|
|
4682
|
-
"ERROR: No browser with CDP found.\n\nFix: Start a browser with remote debugging enabled:\n Arc: Already enabled by default\n Chrome: --remote-debugging-port=9222\n Electron apps expose CDP automatically"
|
|
4683
|
-
);
|
|
4684
|
-
process.exit(1);
|
|
4685
|
-
}
|
|
4686
|
-
const defaultBrowser = getDefaultBrowserId();
|
|
4687
|
-
const preferred = endpoints.find(
|
|
4688
|
-
(p) => p.bundleId === defaultBrowser && !p.isElectron
|
|
4689
|
-
) ?? endpoints.find((p) => !p.isElectron) ?? endpoints[0];
|
|
4690
|
-
console.log(
|
|
4691
|
-
JSON.stringify(
|
|
4692
|
-
{
|
|
4693
|
-
port: preferred.port,
|
|
4694
|
-
app: preferred.app,
|
|
4695
|
-
isElectron: preferred.isElectron,
|
|
4696
|
-
all: endpoints.map((e) => ({
|
|
4697
|
-
port: e.port,
|
|
4698
|
-
app: e.app,
|
|
4699
|
-
isElectron: e.isElectron
|
|
4700
|
-
}))
|
|
4701
|
-
},
|
|
4702
|
-
null,
|
|
4703
|
-
2
|
|
4704
|
-
)
|
|
4705
|
-
);
|
|
4706
|
-
console.error(
|
|
4707
|
-
`
|
|
4708
|
-
Found ${endpoints.length} CDP endpoint${endpoints.length > 1 ? "s" : ""}. Default: port ${preferred.port} (${preferred.app}).
|
|
4667
|
+
var fs4, os2, path2, LOCK_DIR, LOCK_STALE_MS, LOCK_TIMEOUT_MS, LOCK_POLL_BASE_MS, heldLocks, cleanupRegistered;
|
|
4668
|
+
var init_tab_lock = __esm({
|
|
4669
|
+
"src/cdp/tab-lock.ts"() {
|
|
4670
|
+
fs4 = __toESM(require("fs"));
|
|
4671
|
+
os2 = __toESM(require("os"));
|
|
4672
|
+
path2 = __toESM(require("path"));
|
|
4673
|
+
LOCK_DIR = os2.tmpdir();
|
|
4674
|
+
LOCK_STALE_MS = 12e4;
|
|
4675
|
+
LOCK_TIMEOUT_MS = 6e4;
|
|
4676
|
+
LOCK_POLL_BASE_MS = 200;
|
|
4677
|
+
heldLocks = /* @__PURE__ */ new Set();
|
|
4678
|
+
cleanupRegistered = false;
|
|
4679
|
+
}
|
|
4680
|
+
});
|
|
4709
4681
|
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4682
|
+
// src/cdp/args.ts
|
|
4683
|
+
function parseCliArgs(argv) {
|
|
4684
|
+
const command = argv[0] ?? "";
|
|
4685
|
+
const positional = [];
|
|
4686
|
+
const parsed = { command, positional };
|
|
4687
|
+
for (let i = 1; i < argv.length; i++) {
|
|
4688
|
+
const arg = argv[i];
|
|
4689
|
+
const next = argv[i + 1];
|
|
4690
|
+
if (arg === "--port" && next) {
|
|
4691
|
+
parsed.port = parseInt(next, 10);
|
|
4692
|
+
i++;
|
|
4693
|
+
} else if (arg === "--out" && next) {
|
|
4694
|
+
parsed.out = next;
|
|
4695
|
+
i++;
|
|
4696
|
+
} else if (arg === "--json") {
|
|
4697
|
+
parsed.json = true;
|
|
4698
|
+
} else if (arg === "--interactive") {
|
|
4699
|
+
parsed.interactive = true;
|
|
4700
|
+
} else if (arg === "--har-out" && next) {
|
|
4701
|
+
parsed.harOut = next;
|
|
4702
|
+
i++;
|
|
4703
|
+
} else if (arg === "--record") {
|
|
4704
|
+
parsed.record = true;
|
|
4705
|
+
} else if (arg === "--duration" && next) {
|
|
4706
|
+
parsed.duration = parseInt(next, 10);
|
|
4707
|
+
i++;
|
|
4708
|
+
} else if (arg === "--settle" && next) {
|
|
4709
|
+
parsed.settle = parseInt(next, 10);
|
|
4710
|
+
i++;
|
|
4711
|
+
} else if (arg === "--file" && next) {
|
|
4712
|
+
parsed.file = next;
|
|
4713
|
+
i++;
|
|
4714
|
+
} else if (arg === "--nested") {
|
|
4715
|
+
parsed.nested = true;
|
|
4716
|
+
} else if (arg === "--har" && next) {
|
|
4717
|
+
parsed.har = next;
|
|
4718
|
+
i++;
|
|
4719
|
+
} else if (arg === "--new") {
|
|
4720
|
+
parsed.new = true;
|
|
4721
|
+
} else if (arg === "--target" && next) {
|
|
4722
|
+
parsed.target = next;
|
|
4723
|
+
i++;
|
|
4724
|
+
} else if (arg === "--role" && next) {
|
|
4725
|
+
parsed.role = next;
|
|
4726
|
+
i++;
|
|
4727
|
+
} else if (arg === "--into" && next) {
|
|
4728
|
+
parsed.into = next;
|
|
4729
|
+
i++;
|
|
4730
|
+
} else if (arg === "--no-screenshot") {
|
|
4731
|
+
parsed.noScreenshot = true;
|
|
4732
|
+
} else if (arg === "--viewport" && next) {
|
|
4733
|
+
parsed.viewport = next;
|
|
4734
|
+
i++;
|
|
4735
|
+
} else if (arg === "--full-page") {
|
|
4736
|
+
parsed.fullPage = true;
|
|
4737
|
+
} else if (arg === "--height" && next) {
|
|
4738
|
+
parsed.height = parseInt(next, 10);
|
|
4739
|
+
i++;
|
|
4740
|
+
} else if (arg === "--filter-url" && next) {
|
|
4741
|
+
parsed.filterUrl = next;
|
|
4742
|
+
i++;
|
|
4743
|
+
} else if (arg === "--filter-status" && next) {
|
|
4744
|
+
parsed.filterStatus = next;
|
|
4745
|
+
i++;
|
|
4746
|
+
} else if (arg === "--filter-method" && next) {
|
|
4747
|
+
parsed.filterMethod = next.toUpperCase();
|
|
4748
|
+
i++;
|
|
4749
|
+
} else if (arg === "--limit" && next) {
|
|
4750
|
+
parsed.limit = parseInt(next, 10);
|
|
4751
|
+
i++;
|
|
4752
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
4753
|
+
parsed.help = true;
|
|
4754
|
+
} else if (arg.startsWith("--")) {
|
|
4755
|
+
console.error(`Unknown flag: ${arg}`);
|
|
4756
|
+
process.exit(1);
|
|
4757
|
+
} else {
|
|
4758
|
+
positional.push(arg);
|
|
4714
4759
|
}
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
Target a tab with: --target <id prefix> (first 8 chars sufficient)`
|
|
4735
|
-
);
|
|
4736
|
-
} else {
|
|
4737
|
-
const endpoints = await detectCdpPortsAsync();
|
|
4738
|
-
if (endpoints.length === 0) {
|
|
4739
|
-
console.error("No CDP endpoints found.");
|
|
4740
|
-
process.exit(1);
|
|
4741
|
-
}
|
|
4742
|
-
const allPages = [];
|
|
4743
|
-
await Promise.all(endpoints.map(async (ep) => {
|
|
4744
|
-
try {
|
|
4745
|
-
const targets = await listTargets(ep.port);
|
|
4746
|
-
const pages = targets.filter((t) => t.type === "page");
|
|
4747
|
-
for (const t of pages) {
|
|
4748
|
-
allPages.push({ id: t.id, title: t.title, url: t.url, port: ep.port, app: ep.app });
|
|
4749
|
-
}
|
|
4750
|
-
} catch {
|
|
4751
|
-
}
|
|
4752
|
-
}));
|
|
4753
|
-
console.log(JSON.stringify(allPages, null, 2));
|
|
4754
|
-
console.error(
|
|
4755
|
-
`
|
|
4756
|
-
${allPages.length} tab${allPages.length !== 1 ? "s" : ""} across ${endpoints.length} CDP endpoint${endpoints.length !== 1 ? "s" : ""}.
|
|
4757
|
-
|
|
4758
|
-
Target a tab with: --target <id prefix> (first 8 chars sufficient)`
|
|
4759
|
-
);
|
|
4760
|
-
}
|
|
4761
|
-
break;
|
|
4762
|
-
}
|
|
4763
|
-
case "open": {
|
|
4764
|
-
if (parsed.help) {
|
|
4765
|
-
console.log(
|
|
4766
|
-
'Usage: capture open <url> [--new] [--port <port>]\n\nOpen URL in browser (returns tab ID).\n\nExample: capture open "https://app.example.com" --new'
|
|
4767
|
-
);
|
|
4768
|
-
process.exit(0);
|
|
4769
|
-
}
|
|
4770
|
-
const url = parsed.positional[0];
|
|
4771
|
-
if (!url) {
|
|
4772
|
-
console.error(
|
|
4773
|
-
'ERROR: Missing URL.\n\nUsage: capture open <url> [--new] [--port <port>]\n\nExample: capture open "https://app.example.com" --new'
|
|
4774
|
-
);
|
|
4775
|
-
process.exit(1);
|
|
4776
|
-
}
|
|
4777
|
-
const p = parsed.port ?? await detectCdpPort();
|
|
4778
|
-
const tab = await navigateAndWait(p, url, { forceNew: parsed.new });
|
|
4779
|
-
console.log(JSON.stringify({
|
|
4780
|
-
id: tab.id,
|
|
4781
|
-
title: tab.title,
|
|
4782
|
-
url: tab.url,
|
|
4783
|
-
port: p
|
|
4784
|
-
}, null, 2));
|
|
4785
|
-
console.error(
|
|
4786
|
-
`
|
|
4787
|
-
Opened: ${tab.title || url}
|
|
4788
|
-
|
|
4789
|
-
For a multi-step validation, prefer a session (auto-targets this kind of tab):
|
|
4790
|
-
capture session start --url "${url}"
|
|
4791
|
-
|
|
4792
|
-
Or interact one-off against this tab:
|
|
4793
|
-
capture a11y --port ${p} --target ${tab.id.slice(0, 8)} --interactive
|
|
4794
|
-
capture screenshot --port ${p} --target ${tab.id.slice(0, 8)}
|
|
4795
|
-
capture exec "<js>" --port ${p} --target ${tab.id.slice(0, 8)}`
|
|
4796
|
-
);
|
|
4797
|
-
break;
|
|
4798
|
-
}
|
|
4799
|
-
case "exec": {
|
|
4800
|
-
if (parsed.help) {
|
|
4801
|
-
console.log(
|
|
4802
|
-
`Usage: capture exec <code> [--target <id>] [--record] [--file <path>] [--har <id>]
|
|
4803
|
-
|
|
4804
|
-
Execute JavaScript in a browser tab. Supports await (wrapped in async IIFE).
|
|
4805
|
-
|
|
4806
|
-
Examples:
|
|
4807
|
-
capture exec "document.title" --target <id>
|
|
4808
|
-
capture exec "await fetch('/api/data').then(r=>r.json())" --target <id>
|
|
4809
|
-
capture exec "document.querySelector('.btn').click()" --target <id>
|
|
4810
|
-
capture exec --file /tmp/scrape.js --target <id> --record`
|
|
4811
|
-
);
|
|
4812
|
-
process.exit(0);
|
|
4813
|
-
}
|
|
4814
|
-
let code;
|
|
4815
|
-
if (parsed.file) {
|
|
4816
|
-
code = fs3.readFileSync(parsed.file, "utf-8");
|
|
4817
|
-
} else {
|
|
4818
|
-
code = parsed.positional[0];
|
|
4819
|
-
if (!code) {
|
|
4820
|
-
console.error(
|
|
4821
|
-
`ERROR: Missing code to execute.
|
|
4822
|
-
|
|
4823
|
-
Usage: capture exec <code> [--target <id>] [--record] [--file <path>]
|
|
4760
|
+
}
|
|
4761
|
+
if (!parsed.target && process.env.CDP_TARGET) {
|
|
4762
|
+
parsed.target = process.env.CDP_TARGET;
|
|
4763
|
+
}
|
|
4764
|
+
if (!parsed.har && process.env.CDP_HAR_ID) {
|
|
4765
|
+
parsed.har = process.env.CDP_HAR_ID;
|
|
4766
|
+
}
|
|
4767
|
+
const session = getActiveSession();
|
|
4768
|
+
if (session) {
|
|
4769
|
+
if (!parsed.har && session.harId) parsed.har = session.harId;
|
|
4770
|
+
if (!parsed.target && session.targetId) parsed.target = session.targetId;
|
|
4771
|
+
}
|
|
4772
|
+
return parsed;
|
|
4773
|
+
}
|
|
4774
|
+
var init_args = __esm({
|
|
4775
|
+
"src/cdp/args.ts"() {
|
|
4776
|
+
init_session_context();
|
|
4777
|
+
}
|
|
4778
|
+
});
|
|
4824
4779
|
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
process.exit(1);
|
|
4831
|
-
}
|
|
4832
|
-
}
|
|
4833
|
-
await withConnection(
|
|
4834
|
-
parsed,
|
|
4835
|
-
async (client) => {
|
|
4836
|
-
await client.send("Emulation.setFocusEmulationEnabled", { enabled: true });
|
|
4837
|
-
let standaloneRecorder;
|
|
4838
|
-
if (parsed.record) {
|
|
4839
|
-
standaloneRecorder = new HARRecorder(client);
|
|
4840
|
-
await standaloneRecorder.start();
|
|
4841
|
-
}
|
|
4842
|
-
const needsAsyncWrap = /\bawait\b/.test(code);
|
|
4843
|
-
const expression = needsAsyncWrap ? `(async () => { ${code} })()` : code;
|
|
4844
|
-
const evalResult = await client.send("Runtime.evaluate", {
|
|
4845
|
-
expression,
|
|
4846
|
-
awaitPromise: true,
|
|
4847
|
-
returnByValue: true
|
|
4848
|
-
});
|
|
4849
|
-
if (standaloneRecorder) {
|
|
4850
|
-
const har = await standaloneRecorder.finish();
|
|
4851
|
-
writeHarAndPrintSummary(har, parsed.harOut);
|
|
4852
|
-
}
|
|
4853
|
-
if (evalResult.exceptionDetails) {
|
|
4854
|
-
console.error(
|
|
4855
|
-
`ERROR: ${evalResult.exceptionDetails.exception?.description ?? "Unknown error"}`
|
|
4856
|
-
);
|
|
4857
|
-
process.exit(1);
|
|
4858
|
-
}
|
|
4859
|
-
console.log(JSON.stringify(evalResult.result?.value));
|
|
4860
|
-
},
|
|
4861
|
-
{ settle: 3e3 }
|
|
4862
|
-
);
|
|
4863
|
-
break;
|
|
4864
|
-
}
|
|
4865
|
-
case "record": {
|
|
4866
|
-
if (parsed.help) {
|
|
4867
|
-
console.log(
|
|
4868
|
-
"Usage: capture record --target <id> [--duration <secs>] [--har-out <path>]\n\nPassive HAR recording for the specified duration (default: 10s)."
|
|
4869
|
-
);
|
|
4870
|
-
process.exit(0);
|
|
4871
|
-
}
|
|
4872
|
-
if (!parsed.target) {
|
|
4873
|
-
console.error(
|
|
4874
|
-
"Usage: capture record --target <id> [--duration <secs>] [--har-out <path>]"
|
|
4875
|
-
);
|
|
4876
|
-
process.exit(1);
|
|
4877
|
-
}
|
|
4878
|
-
const result = await recordTraffic({
|
|
4879
|
-
port: parsed.port,
|
|
4880
|
-
targetId: parsed.target,
|
|
4881
|
-
duration: parsed.duration,
|
|
4882
|
-
harOutPath: parsed.harOut
|
|
4883
|
-
});
|
|
4884
|
-
console.log(
|
|
4885
|
-
JSON.stringify(
|
|
4886
|
-
{ entryCount: result.entryCount, harPath: result.harPath },
|
|
4887
|
-
null,
|
|
4888
|
-
2
|
|
4889
|
-
)
|
|
4890
|
-
);
|
|
4891
|
-
break;
|
|
4892
|
-
}
|
|
4893
|
-
case "reset-tab": {
|
|
4894
|
-
if (parsed.help) {
|
|
4895
|
-
console.log(
|
|
4896
|
-
'Usage: capture reset-tab <url> [--port <port>]\n\nAbandon a stuck/unresponsive tab and open a fresh one.\nUpdates session context so subsequent commands auto-target the new tab.\n\nExample: capture reset-tab "https://www.instagram.com/"'
|
|
4897
|
-
);
|
|
4898
|
-
process.exit(0);
|
|
4899
|
-
}
|
|
4900
|
-
const url = parsed.positional[0];
|
|
4901
|
-
if (!url) {
|
|
4902
|
-
console.error(
|
|
4903
|
-
"Usage: capture reset-tab <url> [--port <port>]"
|
|
4904
|
-
);
|
|
4905
|
-
process.exit(1);
|
|
4906
|
-
}
|
|
4907
|
-
const p = parsed.port ?? await detectCdpPort();
|
|
4908
|
-
const tab = await openTab(p, url);
|
|
4909
|
-
if (tab.webSocketDebuggerUrl) {
|
|
4910
|
-
const client = new CDPClient(tab.webSocketDebuggerUrl);
|
|
4911
|
-
await client.waitReady();
|
|
4912
|
-
await client.send("Page.enable");
|
|
4913
|
-
await new Promise((resolve2) => {
|
|
4914
|
-
const timer = setTimeout(() => resolve2(), 1e4);
|
|
4915
|
-
client.on("Page.loadEventFired", () => {
|
|
4916
|
-
clearTimeout(timer);
|
|
4917
|
-
resolve2();
|
|
4918
|
-
});
|
|
4919
|
-
});
|
|
4920
|
-
client.close();
|
|
4921
|
-
}
|
|
4922
|
-
updateActiveSession({ targetId: tab.id });
|
|
4923
|
-
console.log(JSON.stringify({ id: tab.id, url: tab.url, port: p }, null, 2));
|
|
4924
|
-
console.error(`
|
|
4925
|
-
New tab opened. Session context updated.`);
|
|
4926
|
-
console.error(`Use --target ${tab.id.slice(0, 8)} if passing target explicitly.`);
|
|
4927
|
-
break;
|
|
4928
|
-
}
|
|
4929
|
-
case "navigate": {
|
|
4930
|
-
if (parsed.help) {
|
|
4931
|
-
console.log(
|
|
4932
|
-
'Usage: capture navigate <url> [--har-out <path>] [--settle <ms>] [--har <id>]\n\nNavigate to URL and record HAR. Appends to session HAR if --har is set.\n\nExample: capture navigate "https://app.example.com/dashboard" --settle 3000'
|
|
4933
|
-
);
|
|
4934
|
-
process.exit(0);
|
|
4935
|
-
}
|
|
4936
|
-
const url = parsed.positional[0];
|
|
4937
|
-
if (!url) {
|
|
4938
|
-
console.error(
|
|
4939
|
-
"Usage: capture navigate <url> [--har-out <path>] [--settle <ms>]"
|
|
4940
|
-
);
|
|
4941
|
-
process.exit(1);
|
|
4942
|
-
}
|
|
4943
|
-
const result = await navigateAndRecord({
|
|
4944
|
-
port: parsed.port,
|
|
4945
|
-
url,
|
|
4946
|
-
targetId: parsed.target,
|
|
4947
|
-
harOutPath: parsed.har ? void 0 : parsed.harOut,
|
|
4948
|
-
settle: parsed.settle
|
|
4949
|
-
});
|
|
4950
|
-
if (parsed.har && result.har.log.entries.length > 0) {
|
|
4951
|
-
appendToHarRecording(parsed.har, result.har.log.entries);
|
|
4952
|
-
console.error(` [har:${parsed.har}] +${result.har.log.entries.length} entries`);
|
|
4953
|
-
}
|
|
4954
|
-
console.log(
|
|
4955
|
-
JSON.stringify(
|
|
4956
|
-
{
|
|
4957
|
-
entryCount: result.entryCount,
|
|
4958
|
-
harPath: result.harPath,
|
|
4959
|
-
tabUrl: result.tab.url,
|
|
4960
|
-
timedOut: result.timedOut
|
|
4961
|
-
},
|
|
4962
|
-
null,
|
|
4963
|
-
2
|
|
4964
|
-
)
|
|
4965
|
-
);
|
|
4966
|
-
break;
|
|
4967
|
-
}
|
|
4968
|
-
case "screenshot": {
|
|
4969
|
-
if (parsed.help) {
|
|
4970
|
-
console.log(
|
|
4971
|
-
"Usage: capture screenshot [--target <id>] [--out <path>] [--viewport <preset>] [--height <px>] [--full-page]\n\nCapture a screenshot of the targeted tab. Saves to --out or auto-generates a path.\n\nOptions:\n --viewport <preset> Viewport preset (default: desktop)\n desktop-wide 1920x1080\n desktop 1280x800\n tablet 768x1024\n mobile 390x844\n --height <px> Override viewport height (e.g. 1600)\n --full-page Capture the entire scrollable page"
|
|
4972
|
-
);
|
|
4973
|
-
process.exit(0);
|
|
4974
|
-
}
|
|
4975
|
-
const VIEWPORTS = {
|
|
4976
|
-
"desktop-wide": { width: 1920, height: 1080 },
|
|
4977
|
-
"desktop": { width: 1280, height: 800 },
|
|
4978
|
-
"tablet": { width: 768, height: 1024 },
|
|
4979
|
-
"mobile": { width: 390, height: 844 }
|
|
4980
|
-
};
|
|
4981
|
-
const viewportName = parsed.viewport ?? "desktop";
|
|
4982
|
-
const viewport = VIEWPORTS[viewportName];
|
|
4983
|
-
if (!viewport) {
|
|
4984
|
-
console.error(`Unknown viewport "${viewportName}". Options: ${Object.keys(VIEWPORTS).join(", ")}`);
|
|
4985
|
-
process.exit(1);
|
|
4986
|
-
}
|
|
4987
|
-
if (parsed.height) {
|
|
4988
|
-
viewport.height = parsed.height;
|
|
4989
|
-
}
|
|
4990
|
-
const result = await withConnection(
|
|
4991
|
-
parsed,
|
|
4992
|
-
async (client) => {
|
|
4993
|
-
const png = await captureScreenshot(client, viewport, { fullPage: parsed.fullPage });
|
|
4994
|
-
const outPath = parsed.out ? parsed.out : nextStepPath("screenshot", "manual") ?? `/tmp/capture-screenshot-${Date.now()}.png`;
|
|
4995
|
-
fs3.writeFileSync(outPath, png);
|
|
4996
|
-
return { path: outPath, bytes: png.length };
|
|
4997
|
-
},
|
|
4998
|
-
{ settle: 0 }
|
|
4999
|
-
);
|
|
5000
|
-
console.log(JSON.stringify(result, null, 2));
|
|
5001
|
-
break;
|
|
5002
|
-
}
|
|
5003
|
-
case "click": {
|
|
5004
|
-
if (parsed.help) {
|
|
5005
|
-
console.log(
|
|
5006
|
-
'Usage: capture click "name" [--role button|link|...] [--no-screenshot]\n\nClick an element by its accessible name. Auto-captures a screenshot after clicking.'
|
|
5007
|
-
);
|
|
5008
|
-
process.exit(0);
|
|
5009
|
-
}
|
|
5010
|
-
const name = parsed.positional[0];
|
|
5011
|
-
if (!name) {
|
|
5012
|
-
console.error('Usage: capture click "name" [--role button|link|...] [--no-screenshot]');
|
|
5013
|
-
process.exit(1);
|
|
5014
|
-
}
|
|
5015
|
-
const clickSettle = parsed.settle !== void 0 ? parsed.settle : parsed.har ? 2500 : 1e3;
|
|
5016
|
-
const result = await withConnection(
|
|
5017
|
-
parsed,
|
|
5018
|
-
async (client) => {
|
|
5019
|
-
const clickResult = await clickByName(client, name, parsed.role);
|
|
5020
|
-
const screenshot = await autoScreenshot(client, "click", name, parsed.noScreenshot);
|
|
5021
|
-
return { ...clickResult, screenshot };
|
|
5022
|
-
},
|
|
5023
|
-
{ settle: clickSettle }
|
|
5024
|
-
);
|
|
5025
|
-
console.log(JSON.stringify(result, null, 2));
|
|
5026
|
-
break;
|
|
5027
|
-
}
|
|
5028
|
-
case "type": {
|
|
5029
|
-
if (parsed.help) {
|
|
5030
|
-
console.log(
|
|
5031
|
-
'Usage: capture type "text" [--into "field name"] [--no-screenshot]\n\nType text into the focused element, or into a named field with --into.'
|
|
5032
|
-
);
|
|
5033
|
-
process.exit(0);
|
|
5034
|
-
}
|
|
5035
|
-
const text = parsed.positional[0];
|
|
5036
|
-
if (!text) {
|
|
5037
|
-
console.error('Usage: capture type "text" [--into "field name"] [--no-screenshot]');
|
|
5038
|
-
process.exit(1);
|
|
5039
|
-
}
|
|
5040
|
-
const result = await withConnection(
|
|
5041
|
-
parsed,
|
|
5042
|
-
async (client) => {
|
|
5043
|
-
let field = null;
|
|
5044
|
-
if (parsed.into) {
|
|
5045
|
-
await focusAndType(client, parsed.into, text, parsed.role);
|
|
5046
|
-
field = parsed.into;
|
|
5047
|
-
} else {
|
|
5048
|
-
await typeText(client, text);
|
|
5049
|
-
}
|
|
5050
|
-
const label = field ?? text;
|
|
5051
|
-
const screenshot = await autoScreenshot(client, "type", label, parsed.noScreenshot);
|
|
5052
|
-
return { typed: text, field, screenshot };
|
|
5053
|
-
},
|
|
5054
|
-
{ settle: parsed.settle !== void 0 ? parsed.settle : parsed.har ? 1500 : 500 }
|
|
5055
|
-
);
|
|
5056
|
-
console.log(JSON.stringify(result, null, 2));
|
|
5057
|
-
break;
|
|
5058
|
-
}
|
|
5059
|
-
case "a11y": {
|
|
5060
|
-
if (parsed.help) {
|
|
5061
|
-
console.log(
|
|
5062
|
-
"Usage: capture a11y [--target <id>] [--json] [--interactive]\n\nGet the accessibility tree. Use --interactive for interactive elements only.\nUse --json for structured JSON output."
|
|
5063
|
-
);
|
|
5064
|
-
process.exit(0);
|
|
5065
|
-
}
|
|
5066
|
-
const result = await withConnection(
|
|
5067
|
-
parsed,
|
|
5068
|
-
async (client) => {
|
|
5069
|
-
const tree = await getAccessibilityTree(client);
|
|
5070
|
-
if (parsed.json) {
|
|
5071
|
-
return tree;
|
|
5072
|
-
}
|
|
5073
|
-
const lines = flattenA11yTree(tree, {
|
|
5074
|
-
interactive: parsed.interactive
|
|
5075
|
-
});
|
|
5076
|
-
return lines.join("\n");
|
|
5077
|
-
},
|
|
5078
|
-
{ settle: 0 }
|
|
5079
|
-
);
|
|
5080
|
-
if (parsed.json) {
|
|
5081
|
-
console.log(JSON.stringify(result, null, 2));
|
|
5082
|
-
} else {
|
|
5083
|
-
console.log(result);
|
|
5084
|
-
}
|
|
5085
|
-
if (!parsed.json) {
|
|
5086
|
-
const lineCount = typeof result === "string" ? result.split("\n").filter(Boolean).length : 0;
|
|
5087
|
-
if (lineCount === 0) {
|
|
5088
|
-
console.error(
|
|
5089
|
-
"\nNo elements found." + (parsed.interactive ? " Try without --interactive to see all nodes." : "") + '\n\nIf this page is part of your project, consider adding ARIA attributes:\n - role="button|link|textbox" on interactive elements\n - aria-label="..." on elements without visible text\n - Semantic HTML (<button>, <input>, <nav>) provides roles automatically'
|
|
5090
|
-
);
|
|
5091
|
-
} else if (parsed.interactive && lineCount < 5) {
|
|
5092
|
-
console.error(
|
|
5093
|
-
`
|
|
5094
|
-
Only ${lineCount} interactive element${lineCount === 1 ? "" : "s"} found. If this seems low, the page may lack ARIA markup.
|
|
5095
|
-
Consider adding role and aria-label attributes to interactive elements in your frontend code.`
|
|
5096
|
-
);
|
|
5097
|
-
}
|
|
5098
|
-
}
|
|
5099
|
-
break;
|
|
5100
|
-
}
|
|
5101
|
-
case "har": {
|
|
5102
|
-
if (parsed.help) {
|
|
5103
|
-
console.log(
|
|
5104
|
-
"Usage: capture har <create|read|delete>\n\n create Create a new HAR recording (returns id)\n read [id] Read accumulated HAR entries (id optional in active session)\n delete <id> Delete a HAR recording\n\nFilters (read):\n --filter-url <pattern> Substring or regex match on URL\n --filter-status <code> Status code, prefix (e.g. 4), or range (e.g. 400-499)\n --filter-method <method> HTTP method (GET, POST, ...)\n --limit <N> Return only the first N matching entries\n\nPass --har <id> to exec/navigate commands to append traffic to a recording."
|
|
5105
|
-
);
|
|
5106
|
-
process.exit(0);
|
|
5107
|
-
}
|
|
5108
|
-
const subcommand = args[1];
|
|
5109
|
-
if (subcommand === "create") {
|
|
5110
|
-
const { id, path: harPath } = createHarRecording();
|
|
5111
|
-
console.log(JSON.stringify({ id, path: harPath }, null, 2));
|
|
5112
|
-
console.error(
|
|
5113
|
-
`
|
|
5114
|
-
Pass --har ${id} to subsequent commands to record traffic.`
|
|
5115
|
-
);
|
|
5116
|
-
break;
|
|
5117
|
-
}
|
|
5118
|
-
if (subcommand === "read") {
|
|
5119
|
-
let id = args[2];
|
|
5120
|
-
if (id && id.startsWith("--")) id = void 0;
|
|
5121
|
-
if (!id) {
|
|
5122
|
-
const session = getActiveSession();
|
|
5123
|
-
if (session?.harId) {
|
|
5124
|
-
id = session.harId;
|
|
5125
|
-
} else {
|
|
5126
|
-
console.error("Usage: capture har read [id]\n ID is optional when a capture session is active.");
|
|
5127
|
-
process.exit(1);
|
|
5128
|
-
}
|
|
5129
|
-
}
|
|
5130
|
-
const har = readHarRecording(id);
|
|
5131
|
-
if (!har) {
|
|
5132
|
-
console.error(`No HAR recording found for id: ${id}`);
|
|
5133
|
-
process.exit(1);
|
|
5134
|
-
}
|
|
5135
|
-
let entries = har.log.entries;
|
|
5136
|
-
const total = entries.length;
|
|
5137
|
-
const filters = [];
|
|
5138
|
-
if (parsed.filterUrl) {
|
|
5139
|
-
const pattern = parsed.filterUrl;
|
|
5140
|
-
let re = null;
|
|
5141
|
-
try {
|
|
5142
|
-
re = new RegExp(pattern, "i");
|
|
5143
|
-
} catch {
|
|
5144
|
-
re = null;
|
|
5145
|
-
}
|
|
5146
|
-
entries = entries.filter(
|
|
5147
|
-
(e) => re ? re.test(e.request.url) : e.request.url.toLowerCase().includes(pattern.toLowerCase())
|
|
5148
|
-
);
|
|
5149
|
-
filters.push(`url~${pattern}`);
|
|
5150
|
-
}
|
|
5151
|
-
if (parsed.filterStatus) {
|
|
5152
|
-
const s = parsed.filterStatus;
|
|
5153
|
-
let matcher;
|
|
5154
|
-
if (/^\d+-\d+$/.test(s)) {
|
|
5155
|
-
const [lo, hi] = s.split("-").map((n) => parseInt(n, 10));
|
|
5156
|
-
matcher = (c) => c >= lo && c <= hi;
|
|
5157
|
-
} else if (/^\d+$/.test(s)) {
|
|
5158
|
-
if (s.length < 3) {
|
|
5159
|
-
matcher = (c) => String(c).startsWith(s);
|
|
5160
|
-
} else {
|
|
5161
|
-
const n = parseInt(s, 10);
|
|
5162
|
-
matcher = (c) => c === n;
|
|
5163
|
-
}
|
|
5164
|
-
} else {
|
|
5165
|
-
matcher = () => true;
|
|
5166
|
-
}
|
|
5167
|
-
entries = entries.filter((e) => matcher(e.response.status));
|
|
5168
|
-
filters.push(`status=${s}`);
|
|
5169
|
-
}
|
|
5170
|
-
if (parsed.filterMethod) {
|
|
5171
|
-
const m = parsed.filterMethod;
|
|
5172
|
-
entries = entries.filter((e) => e.request.method.toUpperCase() === m);
|
|
5173
|
-
filters.push(`method=${m}`);
|
|
5174
|
-
}
|
|
5175
|
-
if (typeof parsed.limit === "number" && parsed.limit > 0) {
|
|
5176
|
-
entries = entries.slice(0, parsed.limit);
|
|
5177
|
-
filters.push(`limit=${parsed.limit}`);
|
|
5178
|
-
}
|
|
5179
|
-
const filtersLabel = filters.length > 0 ? ` [${filters.join(", ")}]` : "";
|
|
5180
|
-
console.error(
|
|
5181
|
-
`HAR: ${harFilePath(id)} (${entries.length}/${total} entries)${filtersLabel}`
|
|
5182
|
-
);
|
|
5183
|
-
console.log(JSON.stringify({ log: { entries } }, null, 2));
|
|
5184
|
-
break;
|
|
5185
|
-
}
|
|
5186
|
-
if (subcommand === "delete") {
|
|
5187
|
-
const id = args[2];
|
|
5188
|
-
if (!id) {
|
|
5189
|
-
console.error("Usage: capture har delete <id>");
|
|
5190
|
-
process.exit(1);
|
|
5191
|
-
}
|
|
5192
|
-
deleteHarRecording(id);
|
|
5193
|
-
console.log(JSON.stringify({ deleted: true, id }, null, 2));
|
|
5194
|
-
break;
|
|
5195
|
-
}
|
|
5196
|
-
console.error(
|
|
5197
|
-
"Usage: capture har <create|read|delete>\n create Create a new HAR recording (returns id)\n read <id> Read accumulated HAR entries\n delete <id> Delete a HAR recording"
|
|
5198
|
-
);
|
|
5199
|
-
process.exit(1);
|
|
5200
|
-
break;
|
|
5201
|
-
}
|
|
5202
|
-
case "network": {
|
|
5203
|
-
if (parsed.help) {
|
|
5204
|
-
console.log(
|
|
5205
|
-
'Usage: capture network <offline|online> [--target <id>]\n\nToggle network connectivity for a tab. Use "offline" to simulate\nnetwork failure (kills WebSocket connections, blocks HTTP requests).\nUse "online" to restore connectivity.'
|
|
5206
|
-
);
|
|
5207
|
-
process.exit(0);
|
|
5208
|
-
}
|
|
5209
|
-
const mode = parsed.positional[0];
|
|
5210
|
-
if (!mode || !["offline", "online"].includes(mode)) {
|
|
5211
|
-
console.error("Usage: capture network <offline|online> [--target <id>]");
|
|
5212
|
-
process.exit(1);
|
|
5213
|
-
}
|
|
5214
|
-
const offline = mode === "offline";
|
|
5215
|
-
const result = await withConnection(
|
|
5216
|
-
parsed,
|
|
5217
|
-
async (client) => {
|
|
5218
|
-
await client.send("Network.enable");
|
|
5219
|
-
await client.send("Network.emulateNetworkConditions", {
|
|
5220
|
-
offline,
|
|
5221
|
-
latency: offline ? -1 : 0,
|
|
5222
|
-
downloadThroughput: offline ? 0 : -1,
|
|
5223
|
-
uploadThroughput: offline ? 0 : -1
|
|
5224
|
-
});
|
|
5225
|
-
return { network: mode, offline };
|
|
5226
|
-
},
|
|
5227
|
-
{ settle: 0 }
|
|
5228
|
-
);
|
|
5229
|
-
console.log(JSON.stringify(result, null, 2));
|
|
5230
|
-
if (offline) {
|
|
5231
|
-
console.error("\nNetwork disabled. WebSocket connections will drop.");
|
|
5232
|
-
console.error("Restore with: capture network online");
|
|
5233
|
-
} else {
|
|
5234
|
-
console.error("\nNetwork restored.");
|
|
5235
|
-
}
|
|
5236
|
-
break;
|
|
5237
|
-
}
|
|
5238
|
-
default:
|
|
5239
|
-
console.log(`CDP \u2014 browser automation via exec, a11y interactions, and screenshots.
|
|
4780
|
+
// src/cdp/usage.ts
|
|
4781
|
+
var CDP_USAGE;
|
|
4782
|
+
var init_usage = __esm({
|
|
4783
|
+
"src/cdp/usage.ts"() {
|
|
4784
|
+
CDP_USAGE = `CDP \u2014 browser automation via exec, a11y interactions, and screenshots.
|
|
5240
4785
|
|
|
5241
4786
|
Commands:
|
|
5242
4787
|
exec <code> Execute JavaScript in a browser tab
|
|
@@ -5255,6 +4800,10 @@ Commands:
|
|
|
5255
4800
|
har create Create a HAR recording (returns id)
|
|
5256
4801
|
har read [id] Read accumulated HAR entries (id optional in active session)
|
|
5257
4802
|
har delete <id> Delete a HAR recording
|
|
4803
|
+
lib list List available vault libs (dev checkout only)
|
|
4804
|
+
lib search "<q>" Fuzzy-search lib functions [--limit N]
|
|
4805
|
+
lib show <name> Lib + function summaries
|
|
4806
|
+
lib read <name> [fn\u2026] Full input/output schemas (+ .ts source path)
|
|
5258
4807
|
|
|
5259
4808
|
Options:
|
|
5260
4809
|
--port <port> Override CDP port
|
|
@@ -5297,246 +4846,1498 @@ Examples:
|
|
|
5297
4846
|
capture har create
|
|
5298
4847
|
capture exec "document.querySelector('form').submit()" --target <id> --har <id>
|
|
5299
4848
|
capture har read <id>
|
|
5300
|
-
|
|
4849
|
+
capture lib search "search emails"
|
|
4850
|
+
capture lib read gmail searchEmails
|
|
4851
|
+
capture exec "import {searchEmails} from 'libs/gmail'; const ctx = await getContext(); return await searchEmails({ ...ctx, query: 'invoice' })"
|
|
4852
|
+
`;
|
|
4853
|
+
}
|
|
4854
|
+
});
|
|
4855
|
+
|
|
4856
|
+
// src/cdp/commands/tabs.ts
|
|
4857
|
+
async function cmdDetect(parsed, _args) {
|
|
4858
|
+
if (parsed.help) {
|
|
4859
|
+
console.log(
|
|
4860
|
+
"Usage: capture detect\n\nDetect CDP port (prioritizes default browser).\nReturns JSON with port, app name, and all discovered endpoints."
|
|
4861
|
+
);
|
|
4862
|
+
process.exit(0);
|
|
4863
|
+
}
|
|
4864
|
+
const endpoints = await detectCdpPortsAsync();
|
|
4865
|
+
if (endpoints.length === 0) {
|
|
4866
|
+
console.error(
|
|
4867
|
+
"ERROR: No browser with CDP found.\n\nFix: Start a browser with remote debugging enabled:\n Arc: Already enabled by default\n Chrome: --remote-debugging-port=9222\n Electron apps expose CDP automatically"
|
|
4868
|
+
);
|
|
4869
|
+
process.exit(1);
|
|
5301
4870
|
}
|
|
4871
|
+
const defaultBrowser = getDefaultBrowserId();
|
|
4872
|
+
const preferred = endpoints.find(
|
|
4873
|
+
(p) => p.bundleId === defaultBrowser && !p.isElectron
|
|
4874
|
+
) ?? endpoints.find((p) => !p.isElectron) ?? endpoints[0];
|
|
4875
|
+
console.log(
|
|
4876
|
+
JSON.stringify(
|
|
4877
|
+
{
|
|
4878
|
+
port: preferred.port,
|
|
4879
|
+
app: preferred.app,
|
|
4880
|
+
isElectron: preferred.isElectron,
|
|
4881
|
+
all: endpoints.map((e) => ({
|
|
4882
|
+
port: e.port,
|
|
4883
|
+
app: e.app,
|
|
4884
|
+
isElectron: e.isElectron
|
|
4885
|
+
}))
|
|
4886
|
+
},
|
|
4887
|
+
null,
|
|
4888
|
+
2
|
|
4889
|
+
)
|
|
4890
|
+
);
|
|
4891
|
+
console.error(
|
|
4892
|
+
`
|
|
4893
|
+
Found ${endpoints.length} CDP endpoint${endpoints.length > 1 ? "s" : ""}. Default: port ${preferred.port} (${preferred.app}).
|
|
4894
|
+
|
|
4895
|
+
Next (most tasks): capture session start --url <your app url>
|
|
4896
|
+
(one-off): capture list --port ${preferred.port}`
|
|
4897
|
+
);
|
|
4898
|
+
return;
|
|
5302
4899
|
}
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
4900
|
+
async function cmdList(parsed, _args) {
|
|
4901
|
+
if (parsed.help) {
|
|
4902
|
+
console.log(
|
|
4903
|
+
"Usage: capture list [--port <port>]\n\nList all browser tabs across all CDP endpoints (or a specific port).\nReturns JSON array of {id, title, url, port, app}."
|
|
4904
|
+
);
|
|
4905
|
+
process.exit(0);
|
|
4906
|
+
}
|
|
4907
|
+
if (parsed.port) {
|
|
4908
|
+
const targets = await listTargets(parsed.port);
|
|
4909
|
+
const pages = targets.filter((t) => t.type === "page");
|
|
4910
|
+
console.log(JSON.stringify(pages.map((t) => ({
|
|
4911
|
+
id: t.id,
|
|
4912
|
+
title: t.title,
|
|
4913
|
+
url: t.url
|
|
4914
|
+
})), null, 2));
|
|
4915
|
+
console.error(
|
|
4916
|
+
`
|
|
4917
|
+
${pages.length} tab${pages.length !== 1 ? "s" : ""} on port ${parsed.port}.
|
|
4918
|
+
|
|
4919
|
+
Target a tab with: --target <id prefix> (first 8 chars sufficient)`
|
|
4920
|
+
);
|
|
4921
|
+
} else {
|
|
4922
|
+
const endpoints = await detectCdpPortsAsync();
|
|
4923
|
+
if (endpoints.length === 0) {
|
|
4924
|
+
console.error("No CDP endpoints found.");
|
|
4925
|
+
process.exit(1);
|
|
4926
|
+
}
|
|
4927
|
+
const allPages = [];
|
|
4928
|
+
await Promise.all(endpoints.map(async (ep) => {
|
|
4929
|
+
try {
|
|
4930
|
+
const targets = await listTargets(ep.port);
|
|
4931
|
+
const pages = targets.filter((t) => t.type === "page");
|
|
4932
|
+
for (const t of pages) {
|
|
4933
|
+
allPages.push({ id: t.id, title: t.title, url: t.url, port: ep.port, app: ep.app });
|
|
4934
|
+
}
|
|
4935
|
+
} catch {
|
|
5325
4936
|
}
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
4937
|
+
}));
|
|
4938
|
+
console.log(JSON.stringify(allPages, null, 2));
|
|
4939
|
+
console.error(
|
|
4940
|
+
`
|
|
4941
|
+
${allPages.length} tab${allPages.length !== 1 ? "s" : ""} across ${endpoints.length} CDP endpoint${endpoints.length !== 1 ? "s" : ""}.
|
|
4942
|
+
|
|
4943
|
+
Target a tab with: --target <id prefix> (first 8 chars sufficient)`
|
|
4944
|
+
);
|
|
4945
|
+
}
|
|
4946
|
+
return;
|
|
4947
|
+
}
|
|
4948
|
+
async function cmdOpen(parsed, _args) {
|
|
4949
|
+
if (parsed.help) {
|
|
4950
|
+
console.log(
|
|
4951
|
+
'Usage: capture open <url> [--new] [--port <port>]\n\nOpen URL in browser (returns tab ID).\n\nExample: capture open "https://app.example.com" --new'
|
|
4952
|
+
);
|
|
4953
|
+
process.exit(0);
|
|
4954
|
+
}
|
|
4955
|
+
const url = parsed.positional[0];
|
|
4956
|
+
if (!url) {
|
|
4957
|
+
console.error(
|
|
4958
|
+
'ERROR: Missing URL.\n\nUsage: capture open <url> [--new] [--port <port>]\n\nExample: capture open "https://app.example.com" --new'
|
|
4959
|
+
);
|
|
4960
|
+
process.exit(1);
|
|
4961
|
+
}
|
|
4962
|
+
const p = parsed.port ?? await detectCdpPort();
|
|
4963
|
+
const tab = await navigateAndWait(p, url, { forceNew: parsed.new });
|
|
4964
|
+
console.log(JSON.stringify({
|
|
4965
|
+
id: tab.id,
|
|
4966
|
+
title: tab.title,
|
|
4967
|
+
url: tab.url,
|
|
4968
|
+
port: p
|
|
4969
|
+
}, null, 2));
|
|
4970
|
+
console.error(
|
|
4971
|
+
`
|
|
4972
|
+
Opened: ${tab.title || url}
|
|
4973
|
+
|
|
4974
|
+
For a multi-step validation, prefer a session (auto-targets this kind of tab):
|
|
4975
|
+
capture session start --url "${url}"
|
|
4976
|
+
|
|
4977
|
+
Or interact one-off against this tab:
|
|
4978
|
+
capture a11y --port ${p} --target ${tab.id.slice(0, 8)} --interactive
|
|
4979
|
+
capture screenshot --port ${p} --target ${tab.id.slice(0, 8)}
|
|
4980
|
+
capture exec "<js>" --port ${p} --target ${tab.id.slice(0, 8)}`
|
|
4981
|
+
);
|
|
4982
|
+
return;
|
|
4983
|
+
}
|
|
4984
|
+
async function cmdResetTab(parsed, _args) {
|
|
4985
|
+
if (parsed.help) {
|
|
4986
|
+
console.log(
|
|
4987
|
+
'Usage: capture reset-tab <url> [--port <port>]\n\nAbandon a stuck/unresponsive tab and open a fresh one.\nUpdates session context so subsequent commands auto-target the new tab.\n\nExample: capture reset-tab "https://www.instagram.com/"'
|
|
4988
|
+
);
|
|
4989
|
+
process.exit(0);
|
|
4990
|
+
}
|
|
4991
|
+
const url = parsed.positional[0];
|
|
4992
|
+
if (!url) {
|
|
4993
|
+
console.error(
|
|
4994
|
+
"Usage: capture reset-tab <url> [--port <port>]"
|
|
4995
|
+
);
|
|
4996
|
+
process.exit(1);
|
|
4997
|
+
}
|
|
4998
|
+
const p = parsed.port ?? await detectCdpPort();
|
|
4999
|
+
const tab = await openTab(p, url);
|
|
5000
|
+
if (tab.webSocketDebuggerUrl) {
|
|
5001
|
+
const client = new CDPClient(tab.webSocketDebuggerUrl);
|
|
5002
|
+
await client.waitReady();
|
|
5003
|
+
await client.send("Page.enable");
|
|
5004
|
+
await new Promise((resolve4) => {
|
|
5005
|
+
const timer = setTimeout(() => resolve4(), 1e4);
|
|
5006
|
+
client.on("Page.loadEventFired", () => {
|
|
5007
|
+
clearTimeout(timer);
|
|
5008
|
+
resolve4();
|
|
5009
|
+
});
|
|
5010
|
+
});
|
|
5011
|
+
client.close();
|
|
5012
|
+
}
|
|
5013
|
+
updateActiveSession({ targetId: tab.id });
|
|
5014
|
+
console.log(JSON.stringify({ id: tab.id, url: tab.url, port: p }, null, 2));
|
|
5015
|
+
console.error(`
|
|
5016
|
+
New tab opened. Session context updated.`);
|
|
5017
|
+
console.error(`Use --target ${tab.id.slice(0, 8)} if passing target explicitly.`);
|
|
5018
|
+
return;
|
|
5019
|
+
}
|
|
5020
|
+
var init_tabs = __esm({
|
|
5021
|
+
"src/cdp/commands/tabs.ts"() {
|
|
5022
|
+
init_detect();
|
|
5023
|
+
init_targets();
|
|
5024
|
+
init_record();
|
|
5025
|
+
init_client();
|
|
5026
|
+
init_session_context();
|
|
5027
|
+
}
|
|
5028
|
+
});
|
|
5029
|
+
|
|
5030
|
+
// src/har-manager.ts
|
|
5031
|
+
function harFilePath(id) {
|
|
5032
|
+
return path3.join(HAR_DIR, `${id}.json`);
|
|
5033
|
+
}
|
|
5034
|
+
function createHarRecording() {
|
|
5035
|
+
const id = Math.random().toString(36).slice(2, 8);
|
|
5036
|
+
const harPath = harFilePath(id);
|
|
5037
|
+
fs5.mkdirSync(HAR_DIR, { recursive: true });
|
|
5038
|
+
fs5.writeFileSync(harPath, JSON.stringify({ log: { entries: [] } }, null, 2));
|
|
5039
|
+
return { id, path: harPath };
|
|
5040
|
+
}
|
|
5041
|
+
function readHarRecording(id) {
|
|
5042
|
+
try {
|
|
5043
|
+
return JSON.parse(fs5.readFileSync(harFilePath(id), "utf-8"));
|
|
5044
|
+
} catch {
|
|
5045
|
+
return null;
|
|
5046
|
+
}
|
|
5047
|
+
}
|
|
5048
|
+
function appendToHarRecording(id, entries) {
|
|
5049
|
+
const harPath = harFilePath(id);
|
|
5050
|
+
let har;
|
|
5051
|
+
try {
|
|
5052
|
+
har = JSON.parse(fs5.readFileSync(harPath, "utf-8"));
|
|
5053
|
+
} catch {
|
|
5054
|
+
har = { log: { entries: [] } };
|
|
5055
|
+
}
|
|
5056
|
+
har.log.entries.push(...entries);
|
|
5057
|
+
fs5.writeFileSync(harPath, JSON.stringify(har, null, 2));
|
|
5058
|
+
}
|
|
5059
|
+
function deleteHarRecording(id) {
|
|
5060
|
+
try {
|
|
5061
|
+
fs5.unlinkSync(harFilePath(id));
|
|
5062
|
+
} catch {
|
|
5063
|
+
}
|
|
5064
|
+
}
|
|
5065
|
+
var fs5, path3, HAR_DIR;
|
|
5066
|
+
var init_har_manager = __esm({
|
|
5067
|
+
"src/har-manager.ts"() {
|
|
5068
|
+
fs5 = __toESM(require("fs"));
|
|
5069
|
+
path3 = __toESM(require("path"));
|
|
5070
|
+
HAR_DIR = "/tmp/capture-har";
|
|
5071
|
+
}
|
|
5072
|
+
});
|
|
5073
|
+
|
|
5074
|
+
// src/cdp/connection.ts
|
|
5075
|
+
async function connectForCommand(parsed) {
|
|
5076
|
+
if (!parsed.target) {
|
|
5077
|
+
throw new Error('Use --target <tabId> to target a tab. Run "capture list" to see available tabs.');
|
|
5078
|
+
}
|
|
5079
|
+
let tab = null;
|
|
5080
|
+
if (parsed.port) {
|
|
5081
|
+
tab = await findTabById(parsed.port, parsed.target);
|
|
5082
|
+
} else {
|
|
5083
|
+
const endpoints = await detectCdpPortsAsync();
|
|
5084
|
+
for (const ep of endpoints) {
|
|
5085
|
+
try {
|
|
5086
|
+
tab = await findTabById(ep.port, parsed.target);
|
|
5087
|
+
if (tab) break;
|
|
5088
|
+
} catch {
|
|
5351
5089
|
}
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5090
|
+
}
|
|
5091
|
+
}
|
|
5092
|
+
if (!tab) {
|
|
5093
|
+
const query = parsed.target;
|
|
5094
|
+
throw new Error(
|
|
5095
|
+
`No tab found for target "${query}". Run "capture list" to see available tabs.`
|
|
5096
|
+
);
|
|
5097
|
+
}
|
|
5098
|
+
if (!tab.webSocketDebuggerUrl) {
|
|
5099
|
+
throw new Error("Tab has no WebSocket debugger URL");
|
|
5100
|
+
}
|
|
5101
|
+
const activeSession = getActiveSession();
|
|
5102
|
+
if (activeSession && !activeSession.targetId) {
|
|
5103
|
+
updateActiveSession({ targetId: tab.id });
|
|
5104
|
+
}
|
|
5105
|
+
const client = new CDPClient(tab.webSocketDebuggerUrl);
|
|
5106
|
+
await client.waitReady();
|
|
5107
|
+
return { client, tab };
|
|
5108
|
+
}
|
|
5109
|
+
async function withConnection(parsed, fn, opts = {}) {
|
|
5110
|
+
const { client, tab } = await connectForCommand(parsed);
|
|
5111
|
+
const consoleRecorder = new ConsoleRecorder(client);
|
|
5112
|
+
await consoleRecorder.start();
|
|
5113
|
+
let harRecorder;
|
|
5114
|
+
if (parsed.har) {
|
|
5115
|
+
const harPath = harFilePath(parsed.har);
|
|
5116
|
+
if (!fs6.existsSync(harPath)) {
|
|
5117
|
+
console.error(
|
|
5118
|
+
`ERROR: No HAR recording found for --har "${parsed.har}". Run 'har create' first.`
|
|
5119
|
+
);
|
|
5120
|
+
process.exit(1);
|
|
5121
|
+
}
|
|
5122
|
+
harRecorder = new HARRecorder(client);
|
|
5123
|
+
await harRecorder.start();
|
|
5124
|
+
}
|
|
5125
|
+
try {
|
|
5126
|
+
const result = await fn(client, tab);
|
|
5127
|
+
if (harRecorder && parsed.har) {
|
|
5128
|
+
const settle = opts.settle !== void 0 ? opts.settle : 3e3;
|
|
5129
|
+
if (settle > 0) {
|
|
5130
|
+
await new Promise((r) => setTimeout(r, settle));
|
|
5379
5131
|
}
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
}, 5e3);
|
|
5386
|
-
const decoded = bodyResult.base64Encoded ? Buffer.from(bodyResult.body, "base64").toString() : bodyResult.body;
|
|
5387
|
-
resp.body = decoded.length > MAX_BODY_SIZE ? decoded.slice(0, MAX_BODY_SIZE) + `
|
|
5388
|
-
[truncated: ${decoded.length} bytes]` : decoded;
|
|
5389
|
-
})
|
|
5132
|
+
const har = await harRecorder.finish();
|
|
5133
|
+
if (har.log.entries.length > 0) {
|
|
5134
|
+
appendToHarRecording(parsed.har, har.log.entries);
|
|
5135
|
+
console.error(
|
|
5136
|
+
` [har:${parsed.har}] +${har.log.entries.length} entries`
|
|
5390
5137
|
);
|
|
5391
|
-
return this.buildHar();
|
|
5392
|
-
}
|
|
5393
|
-
finishPartial() {
|
|
5394
|
-
return this.buildHar();
|
|
5395
5138
|
}
|
|
5396
|
-
}
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5139
|
+
}
|
|
5140
|
+
printConsoleSummary(consoleRecorder.finish());
|
|
5141
|
+
return result;
|
|
5142
|
+
} finally {
|
|
5143
|
+
client.close();
|
|
5144
|
+
}
|
|
5145
|
+
}
|
|
5146
|
+
var fs6;
|
|
5147
|
+
var init_connection = __esm({
|
|
5148
|
+
"src/cdp/connection.ts"() {
|
|
5149
|
+
fs6 = __toESM(require("fs"));
|
|
5150
|
+
init_client();
|
|
5151
|
+
init_targets();
|
|
5152
|
+
init_detect();
|
|
5153
|
+
init_console_recorder();
|
|
5154
|
+
init_har_recorder();
|
|
5155
|
+
init_har_manager();
|
|
5156
|
+
init_session_context();
|
|
5157
|
+
}
|
|
5158
|
+
});
|
|
5159
|
+
|
|
5160
|
+
// src/vault/paths.ts
|
|
5161
|
+
function vaultLibsDir() {
|
|
5162
|
+
const candidates = [
|
|
5163
|
+
path4.resolve(__dirname, "..", "vault", "libs"),
|
|
5164
|
+
// built bin: bin/../vault/libs
|
|
5165
|
+
path4.resolve(__dirname, "..", "..", "vault", "libs")
|
|
5166
|
+
// tsx dev: src/vault/../../vault/libs
|
|
5167
|
+
];
|
|
5168
|
+
const hit = candidates.find(
|
|
5169
|
+
(p) => fs7.existsSync(path4.join(p, "_runtime", "index.ts"))
|
|
5170
|
+
);
|
|
5171
|
+
if (!hit) throw new Error(DEV_ONLY_MSG);
|
|
5172
|
+
return hit;
|
|
5173
|
+
}
|
|
5174
|
+
function vaultRepoRoot() {
|
|
5175
|
+
return path4.dirname(vaultLibsDir());
|
|
5176
|
+
}
|
|
5177
|
+
function listLibs() {
|
|
5178
|
+
return fs7.readdirSync(vaultLibsDir(), { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith("_")).map((e) => e.name).sort();
|
|
5179
|
+
}
|
|
5180
|
+
function unknownLibMsg(name) {
|
|
5181
|
+
return `Unknown lib "${name}". Available: ${listLibs().join(", ")}`;
|
|
5182
|
+
}
|
|
5183
|
+
function isValidLibName(name) {
|
|
5184
|
+
return VALID_LIB_NAME.test(name) && !name.includes("..");
|
|
5185
|
+
}
|
|
5186
|
+
async function loadEsbuild() {
|
|
5187
|
+
return await import("esbuild").catch(() => {
|
|
5188
|
+
throw new Error(DEV_ONLY_MSG);
|
|
5189
|
+
});
|
|
5190
|
+
}
|
|
5191
|
+
var fs7, path4, DEV_ONLY_MSG, VALID_LIB_NAME;
|
|
5192
|
+
var init_paths = __esm({
|
|
5193
|
+
"src/vault/paths.ts"() {
|
|
5194
|
+
fs7 = __toESM(require("fs"));
|
|
5195
|
+
path4 = __toESM(require("path"));
|
|
5196
|
+
DEV_ONLY_MSG = "This is a dev-only feature of capture \u2014 run it from a capture checkout\n(vault/ source + `pnpm install`). Not available in the published package.";
|
|
5197
|
+
VALID_LIB_NAME = /^[A-Za-z0-9_.-]+$/;
|
|
5198
|
+
}
|
|
5199
|
+
});
|
|
5200
|
+
|
|
5201
|
+
// src/vault/bundle.ts
|
|
5202
|
+
function splitLeadingImports(code) {
|
|
5203
|
+
const imports = [];
|
|
5204
|
+
let body = code;
|
|
5205
|
+
for (; ; ) {
|
|
5206
|
+
const m = IMPORT_STMT_RE.exec(body);
|
|
5207
|
+
if (!m) break;
|
|
5208
|
+
imports.push(m[0]);
|
|
5209
|
+
body = body.slice(m[0].length);
|
|
5210
|
+
}
|
|
5211
|
+
return { imports: imports.join(""), body };
|
|
5212
|
+
}
|
|
5213
|
+
function hasImports(code) {
|
|
5214
|
+
return splitLeadingImports(code).imports.length > 0;
|
|
5215
|
+
}
|
|
5216
|
+
function preflightLibs(imports) {
|
|
5217
|
+
const libs = vaultLibsDir();
|
|
5218
|
+
LIB_SPECIFIER_RE.lastIndex = 0;
|
|
5219
|
+
let m;
|
|
5220
|
+
while ((m = LIB_SPECIFIER_RE.exec(imports)) !== null) {
|
|
5221
|
+
const name = m[2];
|
|
5222
|
+
if (!isValidLibName(name) || !fs8.existsSync(path5.join(libs, name, "index.ts"))) {
|
|
5223
|
+
throw new Error(unknownLibMsg(name));
|
|
5224
|
+
}
|
|
5225
|
+
}
|
|
5226
|
+
}
|
|
5227
|
+
function buildEntry(code) {
|
|
5228
|
+
const { imports, body } = splitLeadingImports(code);
|
|
5229
|
+
const expr = body.trim().replace(/;+\s*$/, "");
|
|
5230
|
+
return [
|
|
5231
|
+
imports,
|
|
5232
|
+
`const ${SENTINEL} = (async () => { ${expr} })();`,
|
|
5233
|
+
`${SENTINEL}.then();`
|
|
5234
|
+
].join("\n");
|
|
5235
|
+
}
|
|
5236
|
+
function vaultResolver() {
|
|
5237
|
+
const libs = vaultLibsDir();
|
|
5238
|
+
const runtime = path5.join(libs, "_runtime", "index.ts");
|
|
5239
|
+
return {
|
|
5240
|
+
name: "vault",
|
|
5241
|
+
setup(build) {
|
|
5242
|
+
build.onResolve({ filter: /^@vallum\/_runtime$/ }, () => ({
|
|
5243
|
+
path: runtime
|
|
5244
|
+
}));
|
|
5245
|
+
build.onResolve({ filter: /^libs\// }, (args) => {
|
|
5246
|
+
const name = args.path.slice("libs/".length);
|
|
5247
|
+
if (!isValidLibName(name)) {
|
|
5248
|
+
return { errors: [{ text: unknownLibMsg(name) }] };
|
|
5249
|
+
}
|
|
5250
|
+
const entry = path5.join(libs, name, "index.ts");
|
|
5251
|
+
return fs8.existsSync(entry) ? { path: entry } : { errors: [{ text: unknownLibMsg(name) }] };
|
|
5252
|
+
});
|
|
5253
|
+
}
|
|
5254
|
+
};
|
|
5255
|
+
}
|
|
5256
|
+
async function runEsbuild(entry) {
|
|
5257
|
+
const esbuild = await loadEsbuild();
|
|
5258
|
+
const opts = {
|
|
5259
|
+
stdin: {
|
|
5260
|
+
contents: entry,
|
|
5261
|
+
resolveDir: vaultRepoRoot(),
|
|
5262
|
+
loader: "ts",
|
|
5263
|
+
sourcefile: "exec.ts"
|
|
5264
|
+
},
|
|
5265
|
+
plugins: [vaultResolver()],
|
|
5266
|
+
bundle: true,
|
|
5267
|
+
write: false,
|
|
5268
|
+
format: "iife",
|
|
5269
|
+
target: "es2020",
|
|
5270
|
+
platform: "browser",
|
|
5271
|
+
treeShaking: true,
|
|
5272
|
+
minify: false,
|
|
5273
|
+
logLevel: "silent"
|
|
5274
|
+
};
|
|
5275
|
+
try {
|
|
5276
|
+
return await esbuild.build(opts);
|
|
5277
|
+
} catch (error) {
|
|
5278
|
+
if (error instanceof Error && error.message.includes("service is no longer running")) {
|
|
5279
|
+
esbuild.stop();
|
|
5280
|
+
return await esbuild.build(opts);
|
|
5281
|
+
}
|
|
5282
|
+
throw error;
|
|
5283
|
+
}
|
|
5284
|
+
}
|
|
5285
|
+
function applyReturnInsert(bundled) {
|
|
5286
|
+
const anchor = `${SENTINEL}.then(`;
|
|
5287
|
+
const i = bundled.lastIndexOf(anchor);
|
|
5288
|
+
return i === -1 ? bundled : bundled.slice(0, i) + "return " + bundled.slice(i);
|
|
5289
|
+
}
|
|
5290
|
+
function formatBuildErrors(errors) {
|
|
5291
|
+
return errors.map((e) => {
|
|
5292
|
+
const l = e.location;
|
|
5293
|
+
const loc = l ? ` (${l.file}:${l.line}:${l.column})` : "";
|
|
5294
|
+
return `${e.text}${loc}`;
|
|
5295
|
+
}).join("\n");
|
|
5296
|
+
}
|
|
5297
|
+
async function bundleExec(code) {
|
|
5298
|
+
preflightLibs(splitLeadingImports(code).imports);
|
|
5299
|
+
const entry = buildEntry(code);
|
|
5300
|
+
let result;
|
|
5301
|
+
try {
|
|
5302
|
+
result = await runEsbuild(entry);
|
|
5303
|
+
} catch (error) {
|
|
5304
|
+
const errs = error?.errors;
|
|
5305
|
+
if (Array.isArray(errs) && errs.length > 0) {
|
|
5306
|
+
throw new Error(formatBuildErrors(errs));
|
|
5307
|
+
}
|
|
5308
|
+
throw error;
|
|
5309
|
+
}
|
|
5310
|
+
if (!result.outputFiles || result.outputFiles.length === 0) {
|
|
5311
|
+
throw new Error("esbuild produced no output");
|
|
5312
|
+
}
|
|
5313
|
+
return applyReturnInsert(result.outputFiles[0].text);
|
|
5314
|
+
}
|
|
5315
|
+
var fs8, path5, SENTINEL, IMPORT_STMT_RE, LIB_SPECIFIER_RE;
|
|
5316
|
+
var init_bundle = __esm({
|
|
5317
|
+
"src/vault/bundle.ts"() {
|
|
5318
|
+
fs8 = __toESM(require("fs"));
|
|
5319
|
+
path5 = __toESM(require("path"));
|
|
5320
|
+
init_paths();
|
|
5321
|
+
SENTINEL = "__CAPTURE_RESULT";
|
|
5322
|
+
IMPORT_STMT_RE = /^\s*(?:(?:\/\/[^\n]*|\/\*[\s\S]*?\*\/)\s*)*import\b(?!\s*\()[^'"`]*?(['"])(?:[^'"`\\]|\\.)*\1[^\n;]*;?/;
|
|
5323
|
+
LIB_SPECIFIER_RE = /(['"])libs\/((?:[^'"\\]|\\.)*)\1/g;
|
|
5324
|
+
}
|
|
5325
|
+
});
|
|
5326
|
+
|
|
5327
|
+
// src/cdp/commands/exec.ts
|
|
5328
|
+
async function cmdExec(parsed, _args) {
|
|
5329
|
+
if (parsed.help) {
|
|
5330
|
+
console.log(
|
|
5331
|
+
`Usage: capture exec <code> [--target <id>] [--record] [--file <path>] [--har <id>]
|
|
5332
|
+
|
|
5333
|
+
Execute JavaScript in a browser tab. Supports await (wrapped in async IIFE).
|
|
5334
|
+
|
|
5335
|
+
Examples:
|
|
5336
|
+
capture exec "document.title" --target <id>
|
|
5337
|
+
capture exec "await fetch('/api/data').then(r=>r.json())" --target <id>
|
|
5338
|
+
capture exec "document.querySelector('.btn').click()" --target <id>
|
|
5339
|
+
capture exec --file /tmp/scrape.js --target <id> --record
|
|
5340
|
+
|
|
5341
|
+
Import vault libs (dev checkout only): static imports first, then your code.
|
|
5342
|
+
capture exec "import {searchEmails} from 'libs/gmail'; const ctx = await getContext(); return await searchEmails({ ...ctx, query: 'invoice' })"`
|
|
5343
|
+
);
|
|
5344
|
+
process.exit(0);
|
|
5345
|
+
}
|
|
5346
|
+
let code;
|
|
5347
|
+
if (parsed.file) {
|
|
5348
|
+
code = fs9.readFileSync(parsed.file, "utf-8");
|
|
5349
|
+
} else {
|
|
5350
|
+
code = parsed.positional[0];
|
|
5351
|
+
if (!code) {
|
|
5352
|
+
console.error(
|
|
5353
|
+
`ERROR: Missing code to execute.
|
|
5354
|
+
|
|
5355
|
+
Usage: capture exec <code> [--target <id>] [--record] [--file <path>]
|
|
5356
|
+
|
|
5357
|
+
Examples:
|
|
5358
|
+
capture exec "document.title" --target <id>
|
|
5359
|
+
capture exec "document.querySelector('.btn').click()" --target <id>
|
|
5360
|
+
capture exec --file /tmp/scrape.js --target <id> --record`
|
|
5361
|
+
);
|
|
5362
|
+
process.exit(1);
|
|
5363
|
+
}
|
|
5364
|
+
}
|
|
5365
|
+
let prebuilt;
|
|
5366
|
+
if (hasImports(code)) {
|
|
5367
|
+
try {
|
|
5368
|
+
prebuilt = await bundleExec(code);
|
|
5369
|
+
} catch (e) {
|
|
5370
|
+
console.error(`ERROR: ${e.message}`);
|
|
5371
|
+
process.exit(1);
|
|
5372
|
+
}
|
|
5373
|
+
}
|
|
5374
|
+
await withConnection(
|
|
5375
|
+
parsed,
|
|
5376
|
+
async (client) => {
|
|
5377
|
+
await client.send("Emulation.setFocusEmulationEnabled", { enabled: true });
|
|
5378
|
+
let standaloneRecorder;
|
|
5379
|
+
if (parsed.record) {
|
|
5380
|
+
standaloneRecorder = new HARRecorder(client);
|
|
5381
|
+
await standaloneRecorder.start();
|
|
5382
|
+
}
|
|
5383
|
+
const expression = prebuilt ? prebuilt : /\bawait\b/.test(code) ? `(async () => { ${code} })()` : code;
|
|
5384
|
+
const evalResult = await client.send("Runtime.evaluate", {
|
|
5385
|
+
expression,
|
|
5386
|
+
awaitPromise: true,
|
|
5387
|
+
returnByValue: true
|
|
5388
|
+
});
|
|
5389
|
+
if (standaloneRecorder) {
|
|
5390
|
+
const har = await standaloneRecorder.finish();
|
|
5391
|
+
writeHarAndPrintSummary(har, parsed.harOut);
|
|
5400
5392
|
}
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
const text = p.args.map(
|
|
5407
|
-
(a) => a.value !== void 0 ? String(a.value) : a.description ?? ""
|
|
5408
|
-
).join(" ");
|
|
5409
|
-
this.entries.push({
|
|
5410
|
-
level: p.type,
|
|
5411
|
-
text,
|
|
5412
|
-
timestamp: p.timestamp
|
|
5413
|
-
});
|
|
5414
|
-
});
|
|
5415
|
-
this.client.on("Runtime.exceptionThrown", (params) => {
|
|
5416
|
-
const p = params;
|
|
5417
|
-
this.entries.push({
|
|
5418
|
-
level: "error",
|
|
5419
|
-
text: p.exceptionDetails.exception?.description ?? p.exceptionDetails.text ?? "Unknown exception",
|
|
5420
|
-
timestamp: p.timestamp,
|
|
5421
|
-
source: p.exceptionDetails.url ? `${p.exceptionDetails.url}:${p.exceptionDetails.lineNumber}` : void 0
|
|
5422
|
-
});
|
|
5423
|
-
});
|
|
5393
|
+
if (evalResult.exceptionDetails) {
|
|
5394
|
+
console.error(
|
|
5395
|
+
`ERROR: ${evalResult.exceptionDetails.exception?.description ?? "Unknown error"}`
|
|
5396
|
+
);
|
|
5397
|
+
process.exit(1);
|
|
5424
5398
|
}
|
|
5425
|
-
|
|
5426
|
-
|
|
5399
|
+
console.log(JSON.stringify(evalResult.result?.value));
|
|
5400
|
+
},
|
|
5401
|
+
{ settle: 3e3 }
|
|
5402
|
+
);
|
|
5403
|
+
return;
|
|
5404
|
+
}
|
|
5405
|
+
var fs9;
|
|
5406
|
+
var init_exec = __esm({
|
|
5407
|
+
"src/cdp/commands/exec.ts"() {
|
|
5408
|
+
fs9 = __toESM(require("fs"));
|
|
5409
|
+
init_connection();
|
|
5410
|
+
init_har_recorder();
|
|
5411
|
+
init_har_output();
|
|
5412
|
+
init_bundle();
|
|
5413
|
+
}
|
|
5414
|
+
});
|
|
5415
|
+
|
|
5416
|
+
// src/interact.ts
|
|
5417
|
+
async function clickByName(client, name, role) {
|
|
5418
|
+
await client.send("Accessibility.enable");
|
|
5419
|
+
await client.send("DOM.enable");
|
|
5420
|
+
const { nodes } = await client.send("Accessibility.getFullAXTree");
|
|
5421
|
+
await client.send("Accessibility.disable");
|
|
5422
|
+
const nameLower = name.toLowerCase();
|
|
5423
|
+
const matches = nodes.filter((n) => {
|
|
5424
|
+
if (!n.backendDOMNodeId) return false;
|
|
5425
|
+
if (!n.name?.value) return false;
|
|
5426
|
+
if (n.name.value.toLowerCase() !== nameLower) return false;
|
|
5427
|
+
if (role && n.role?.value !== role) return false;
|
|
5428
|
+
return true;
|
|
5429
|
+
});
|
|
5430
|
+
if (matches.length === 0) {
|
|
5431
|
+
throw new Error(
|
|
5432
|
+
`No element found with accessible name "${name}"${role ? ` and role "${role}"` : ""}. Run \`capture a11y --interactive\` to see available elements.`
|
|
5433
|
+
);
|
|
5434
|
+
}
|
|
5435
|
+
if (matches.length > 1 && !role) {
|
|
5436
|
+
const INPUT_ROLES = /* @__PURE__ */ new Set(["textbox", "searchbox", "combobox", "textarea", "spinbutton"]);
|
|
5437
|
+
const inputMatches = matches.filter((n) => n.role?.value && INPUT_ROLES.has(n.role.value));
|
|
5438
|
+
if (inputMatches.length === 1) {
|
|
5439
|
+
matches.length = 0;
|
|
5440
|
+
matches.push(inputMatches[0]);
|
|
5441
|
+
} else {
|
|
5442
|
+
const summary = matches.slice(0, 5).map((n) => ` ${n.role?.value} "${n.name?.value}"`).join("\n");
|
|
5443
|
+
throw new Error(
|
|
5444
|
+
`Multiple elements match "${name}" \u2014 disambiguate with --role:
|
|
5445
|
+
${summary}`
|
|
5446
|
+
);
|
|
5447
|
+
}
|
|
5448
|
+
}
|
|
5449
|
+
const match = matches[0];
|
|
5450
|
+
const backendNodeId = match.backendDOMNodeId;
|
|
5451
|
+
await client.send("DOM.scrollIntoViewIfNeeded", { backendNodeId });
|
|
5452
|
+
const { model } = await client.send("DOM.getBoxModel", { backendNodeId });
|
|
5453
|
+
const quad = model.content;
|
|
5454
|
+
const x = (quad[0] + quad[2] + quad[4] + quad[6]) / 4;
|
|
5455
|
+
const y = (quad[1] + quad[3] + quad[5] + quad[7]) / 4;
|
|
5456
|
+
await client.send("Input.dispatchMouseEvent", {
|
|
5457
|
+
type: "mousePressed",
|
|
5458
|
+
x,
|
|
5459
|
+
y,
|
|
5460
|
+
button: "left",
|
|
5461
|
+
clickCount: 1
|
|
5462
|
+
});
|
|
5463
|
+
await client.send("Input.dispatchMouseEvent", {
|
|
5464
|
+
type: "mouseReleased",
|
|
5465
|
+
x,
|
|
5466
|
+
y,
|
|
5467
|
+
button: "left",
|
|
5468
|
+
clickCount: 1
|
|
5469
|
+
});
|
|
5470
|
+
return {
|
|
5471
|
+
x: Math.round(x),
|
|
5472
|
+
y: Math.round(y),
|
|
5473
|
+
role: match.role?.value ?? "unknown",
|
|
5474
|
+
name: match.name?.value ?? name
|
|
5475
|
+
};
|
|
5476
|
+
}
|
|
5477
|
+
async function typeText(client, text) {
|
|
5478
|
+
await client.send("Input.insertText", { text });
|
|
5479
|
+
}
|
|
5480
|
+
async function focusAndType(client, fieldName, text, role) {
|
|
5481
|
+
const result = await clickByName(client, fieldName, role);
|
|
5482
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
5483
|
+
await typeText(client, text);
|
|
5484
|
+
return result;
|
|
5485
|
+
}
|
|
5486
|
+
var init_interact = __esm({
|
|
5487
|
+
"src/interact.ts"() {
|
|
5488
|
+
}
|
|
5489
|
+
});
|
|
5490
|
+
|
|
5491
|
+
// src/cdp/commands/ui.ts
|
|
5492
|
+
async function cmdScreenshot(parsed, _args) {
|
|
5493
|
+
if (parsed.help) {
|
|
5494
|
+
console.log(
|
|
5495
|
+
"Usage: capture screenshot [--target <id>] [--out <path>] [--viewport <preset>] [--height <px>] [--full-page]\n\nCapture a screenshot of the targeted tab. Saves to --out or auto-generates a path.\n\nOptions:\n --viewport <preset> Viewport preset (default: desktop)\n desktop-wide 1920x1080\n desktop 1280x800\n tablet 768x1024\n mobile 390x844\n --height <px> Override viewport height (e.g. 1600)\n --full-page Capture the entire scrollable page"
|
|
5496
|
+
);
|
|
5497
|
+
process.exit(0);
|
|
5498
|
+
}
|
|
5499
|
+
const VIEWPORTS = {
|
|
5500
|
+
"desktop-wide": { width: 1920, height: 1080 },
|
|
5501
|
+
"desktop": { width: 1280, height: 800 },
|
|
5502
|
+
"tablet": { width: 768, height: 1024 },
|
|
5503
|
+
"mobile": { width: 390, height: 844 }
|
|
5504
|
+
};
|
|
5505
|
+
const viewportName = parsed.viewport ?? "desktop";
|
|
5506
|
+
const viewport = VIEWPORTS[viewportName];
|
|
5507
|
+
if (!viewport) {
|
|
5508
|
+
console.error(`Unknown viewport "${viewportName}". Options: ${Object.keys(VIEWPORTS).join(", ")}`);
|
|
5509
|
+
process.exit(1);
|
|
5510
|
+
}
|
|
5511
|
+
if (parsed.height) {
|
|
5512
|
+
viewport.height = parsed.height;
|
|
5513
|
+
}
|
|
5514
|
+
const result = await withConnection(
|
|
5515
|
+
parsed,
|
|
5516
|
+
async (client) => {
|
|
5517
|
+
const png = await captureScreenshot(client, viewport, { fullPage: parsed.fullPage });
|
|
5518
|
+
const outPath = parsed.out ? parsed.out : nextStepPath("screenshot", "manual") ?? `/tmp/capture-screenshot-${Date.now()}.png`;
|
|
5519
|
+
fs10.writeFileSync(outPath, png);
|
|
5520
|
+
return { path: outPath, bytes: png.length };
|
|
5521
|
+
},
|
|
5522
|
+
{ settle: 0 }
|
|
5523
|
+
);
|
|
5524
|
+
console.log(JSON.stringify(result, null, 2));
|
|
5525
|
+
return;
|
|
5526
|
+
}
|
|
5527
|
+
async function cmdClick(parsed, _args) {
|
|
5528
|
+
if (parsed.help) {
|
|
5529
|
+
console.log(
|
|
5530
|
+
'Usage: capture click "name" [--role button|link|...] [--no-screenshot]\n\nClick an element by its accessible name. Auto-captures a screenshot after clicking.'
|
|
5531
|
+
);
|
|
5532
|
+
process.exit(0);
|
|
5533
|
+
}
|
|
5534
|
+
const name = parsed.positional[0];
|
|
5535
|
+
if (!name) {
|
|
5536
|
+
console.error('Usage: capture click "name" [--role button|link|...] [--no-screenshot]');
|
|
5537
|
+
process.exit(1);
|
|
5538
|
+
}
|
|
5539
|
+
const clickSettle = parsed.settle !== void 0 ? parsed.settle : parsed.har ? 2500 : 1e3;
|
|
5540
|
+
const result = await withConnection(
|
|
5541
|
+
parsed,
|
|
5542
|
+
async (client) => {
|
|
5543
|
+
const clickResult = await clickByName(client, name, parsed.role);
|
|
5544
|
+
const screenshot = await autoScreenshot(client, "click", name, parsed.noScreenshot);
|
|
5545
|
+
return { ...clickResult, screenshot };
|
|
5546
|
+
},
|
|
5547
|
+
{ settle: clickSettle }
|
|
5548
|
+
);
|
|
5549
|
+
console.log(JSON.stringify(result, null, 2));
|
|
5550
|
+
return;
|
|
5551
|
+
}
|
|
5552
|
+
async function cmdType(parsed, _args) {
|
|
5553
|
+
if (parsed.help) {
|
|
5554
|
+
console.log(
|
|
5555
|
+
'Usage: capture type "text" [--into "field name"] [--no-screenshot]\n\nType text into the focused element, or into a named field with --into.'
|
|
5556
|
+
);
|
|
5557
|
+
process.exit(0);
|
|
5558
|
+
}
|
|
5559
|
+
const text = parsed.positional[0];
|
|
5560
|
+
if (!text) {
|
|
5561
|
+
console.error('Usage: capture type "text" [--into "field name"] [--no-screenshot]');
|
|
5562
|
+
process.exit(1);
|
|
5563
|
+
}
|
|
5564
|
+
const result = await withConnection(
|
|
5565
|
+
parsed,
|
|
5566
|
+
async (client) => {
|
|
5567
|
+
let field = null;
|
|
5568
|
+
if (parsed.into) {
|
|
5569
|
+
await focusAndType(client, parsed.into, text, parsed.role);
|
|
5570
|
+
field = parsed.into;
|
|
5571
|
+
} else {
|
|
5572
|
+
await typeText(client, text);
|
|
5427
5573
|
}
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
"
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
"com.microsoft.edgemac": ["edge", "microsoft edge"],
|
|
5451
|
-
"org.chromium.chromium": ["chromium"]
|
|
5452
|
-
};
|
|
5453
|
-
CDPClient = class {
|
|
5454
|
-
ws;
|
|
5455
|
-
messageId = 0;
|
|
5456
|
-
pendingRequests = /* @__PURE__ */ new Map();
|
|
5457
|
-
pendingTimeouts = /* @__PURE__ */ new Map();
|
|
5458
|
-
eventHandlers = /* @__PURE__ */ new Map();
|
|
5459
|
-
ready;
|
|
5460
|
-
constructor(wsUrl, connectTimeout = 5e3) {
|
|
5461
|
-
this.ws = new wrapper_default(wsUrl);
|
|
5462
|
-
this.ready = new Promise((resolve2, reject) => {
|
|
5463
|
-
const timeout = setTimeout(() => {
|
|
5464
|
-
reject(new Error(`WebSocket connection timeout (${connectTimeout}ms)`));
|
|
5465
|
-
}, connectTimeout);
|
|
5466
|
-
this.ws.on("open", () => {
|
|
5467
|
-
clearTimeout(timeout);
|
|
5468
|
-
resolve2();
|
|
5469
|
-
});
|
|
5470
|
-
this.ws.on("error", (err) => {
|
|
5471
|
-
clearTimeout(timeout);
|
|
5472
|
-
reject(err);
|
|
5473
|
-
});
|
|
5474
|
-
});
|
|
5475
|
-
this.ws.on("message", (data) => {
|
|
5476
|
-
const msg = JSON.parse(data.toString());
|
|
5477
|
-
if (msg.id !== void 0) {
|
|
5478
|
-
const pending = this.pendingRequests.get(msg.id);
|
|
5479
|
-
if (pending) {
|
|
5480
|
-
this.pendingRequests.delete(msg.id);
|
|
5481
|
-
const timer = this.pendingTimeouts.get(msg.id);
|
|
5482
|
-
if (timer) {
|
|
5483
|
-
clearTimeout(timer);
|
|
5484
|
-
this.pendingTimeouts.delete(msg.id);
|
|
5485
|
-
}
|
|
5486
|
-
if (msg.error) {
|
|
5487
|
-
pending.reject(new Error(msg.error.message));
|
|
5488
|
-
} else {
|
|
5489
|
-
pending.resolve(msg.result);
|
|
5490
|
-
}
|
|
5491
|
-
}
|
|
5492
|
-
} else if (msg.method) {
|
|
5493
|
-
const handlers = this.eventHandlers.get(msg.method);
|
|
5494
|
-
handlers?.forEach((h) => h(msg.params));
|
|
5495
|
-
}
|
|
5496
|
-
});
|
|
5574
|
+
const label = field ?? text;
|
|
5575
|
+
const screenshot = await autoScreenshot(client, "type", label, parsed.noScreenshot);
|
|
5576
|
+
return { typed: text, field, screenshot };
|
|
5577
|
+
},
|
|
5578
|
+
{ settle: parsed.settle !== void 0 ? parsed.settle : parsed.har ? 1500 : 500 }
|
|
5579
|
+
);
|
|
5580
|
+
console.log(JSON.stringify(result, null, 2));
|
|
5581
|
+
return;
|
|
5582
|
+
}
|
|
5583
|
+
async function cmdA11y(parsed, _args) {
|
|
5584
|
+
if (parsed.help) {
|
|
5585
|
+
console.log(
|
|
5586
|
+
"Usage: capture a11y [--target <id>] [--json] [--interactive]\n\nGet the accessibility tree. Use --interactive for interactive elements only.\nUse --json for structured JSON output."
|
|
5587
|
+
);
|
|
5588
|
+
process.exit(0);
|
|
5589
|
+
}
|
|
5590
|
+
const result = await withConnection(
|
|
5591
|
+
parsed,
|
|
5592
|
+
async (client) => {
|
|
5593
|
+
const tree = await getAccessibilityTree(client);
|
|
5594
|
+
if (parsed.json) {
|
|
5595
|
+
return tree;
|
|
5497
5596
|
}
|
|
5498
|
-
|
|
5499
|
-
|
|
5597
|
+
const lines = flattenA11yTree(tree, {
|
|
5598
|
+
interactive: parsed.interactive
|
|
5599
|
+
});
|
|
5600
|
+
return lines.join("\n");
|
|
5601
|
+
},
|
|
5602
|
+
{ settle: 0 }
|
|
5603
|
+
);
|
|
5604
|
+
if (parsed.json) {
|
|
5605
|
+
console.log(JSON.stringify(result, null, 2));
|
|
5606
|
+
} else {
|
|
5607
|
+
console.log(result);
|
|
5608
|
+
}
|
|
5609
|
+
if (!parsed.json) {
|
|
5610
|
+
const lineCount = typeof result === "string" ? result.split("\n").filter(Boolean).length : 0;
|
|
5611
|
+
if (lineCount === 0) {
|
|
5612
|
+
console.error(
|
|
5613
|
+
"\nNo elements found." + (parsed.interactive ? " Try without --interactive to see all nodes." : "") + '\n\nIf this page is part of your project, consider adding ARIA attributes:\n - role="button|link|textbox" on interactive elements\n - aria-label="..." on elements without visible text\n - Semantic HTML (<button>, <input>, <nav>) provides roles automatically'
|
|
5614
|
+
);
|
|
5615
|
+
} else if (parsed.interactive && lineCount < 5) {
|
|
5616
|
+
console.error(
|
|
5617
|
+
`
|
|
5618
|
+
Only ${lineCount} interactive element${lineCount === 1 ? "" : "s"} found. If this seems low, the page may lack ARIA markup.
|
|
5619
|
+
Consider adding role and aria-label attributes to interactive elements in your frontend code.`
|
|
5620
|
+
);
|
|
5621
|
+
}
|
|
5622
|
+
}
|
|
5623
|
+
return;
|
|
5624
|
+
}
|
|
5625
|
+
var fs10;
|
|
5626
|
+
var init_ui = __esm({
|
|
5627
|
+
"src/cdp/commands/ui.ts"() {
|
|
5628
|
+
fs10 = __toESM(require("fs"));
|
|
5629
|
+
init_connection();
|
|
5630
|
+
init_screenshot();
|
|
5631
|
+
init_a11y();
|
|
5632
|
+
init_interact();
|
|
5633
|
+
init_session_context();
|
|
5634
|
+
}
|
|
5635
|
+
});
|
|
5636
|
+
|
|
5637
|
+
// src/cdp/commands/traffic.ts
|
|
5638
|
+
async function cmdRecord(parsed, _args) {
|
|
5639
|
+
if (parsed.help) {
|
|
5640
|
+
console.log(
|
|
5641
|
+
"Usage: capture record --target <id> [--duration <secs>] [--har-out <path>]\n\nPassive HAR recording for the specified duration (default: 10s)."
|
|
5642
|
+
);
|
|
5643
|
+
process.exit(0);
|
|
5644
|
+
}
|
|
5645
|
+
if (!parsed.target) {
|
|
5646
|
+
console.error(
|
|
5647
|
+
"Usage: capture record --target <id> [--duration <secs>] [--har-out <path>]"
|
|
5648
|
+
);
|
|
5649
|
+
process.exit(1);
|
|
5650
|
+
}
|
|
5651
|
+
const result = await recordTraffic({
|
|
5652
|
+
port: parsed.port,
|
|
5653
|
+
targetId: parsed.target,
|
|
5654
|
+
duration: parsed.duration,
|
|
5655
|
+
harOutPath: parsed.harOut
|
|
5656
|
+
});
|
|
5657
|
+
console.log(
|
|
5658
|
+
JSON.stringify(
|
|
5659
|
+
{ entryCount: result.entryCount, harPath: result.harPath },
|
|
5660
|
+
null,
|
|
5661
|
+
2
|
|
5662
|
+
)
|
|
5663
|
+
);
|
|
5664
|
+
return;
|
|
5665
|
+
}
|
|
5666
|
+
async function cmdNavigate(parsed, _args) {
|
|
5667
|
+
if (parsed.help) {
|
|
5668
|
+
console.log(
|
|
5669
|
+
'Usage: capture navigate <url> [--har-out <path>] [--settle <ms>] [--har <id>]\n\nNavigate to URL and record HAR. Appends to session HAR if --har is set.\n\nExample: capture navigate "https://app.example.com/dashboard" --settle 3000'
|
|
5670
|
+
);
|
|
5671
|
+
process.exit(0);
|
|
5672
|
+
}
|
|
5673
|
+
const url = parsed.positional[0];
|
|
5674
|
+
if (!url) {
|
|
5675
|
+
console.error(
|
|
5676
|
+
"Usage: capture navigate <url> [--har-out <path>] [--settle <ms>]"
|
|
5677
|
+
);
|
|
5678
|
+
process.exit(1);
|
|
5679
|
+
}
|
|
5680
|
+
const result = await navigateAndRecord({
|
|
5681
|
+
port: parsed.port,
|
|
5682
|
+
url,
|
|
5683
|
+
targetId: parsed.target,
|
|
5684
|
+
harOutPath: parsed.har ? void 0 : parsed.harOut,
|
|
5685
|
+
settle: parsed.settle
|
|
5686
|
+
});
|
|
5687
|
+
if (parsed.har && result.har.log.entries.length > 0) {
|
|
5688
|
+
appendToHarRecording(parsed.har, result.har.log.entries);
|
|
5689
|
+
console.error(` [har:${parsed.har}] +${result.har.log.entries.length} entries`);
|
|
5690
|
+
}
|
|
5691
|
+
console.log(
|
|
5692
|
+
JSON.stringify(
|
|
5693
|
+
{
|
|
5694
|
+
entryCount: result.entryCount,
|
|
5695
|
+
harPath: result.harPath,
|
|
5696
|
+
tabUrl: result.tab.url,
|
|
5697
|
+
timedOut: result.timedOut
|
|
5698
|
+
},
|
|
5699
|
+
null,
|
|
5700
|
+
2
|
|
5701
|
+
)
|
|
5702
|
+
);
|
|
5703
|
+
return;
|
|
5704
|
+
}
|
|
5705
|
+
async function cmdNetwork(parsed, _args) {
|
|
5706
|
+
if (parsed.help) {
|
|
5707
|
+
console.log(
|
|
5708
|
+
'Usage: capture network <offline|online> [--target <id>]\n\nToggle network connectivity for a tab. Use "offline" to simulate\nnetwork failure (kills WebSocket connections, blocks HTTP requests).\nUse "online" to restore connectivity.'
|
|
5709
|
+
);
|
|
5710
|
+
process.exit(0);
|
|
5711
|
+
}
|
|
5712
|
+
const mode = parsed.positional[0];
|
|
5713
|
+
if (!mode || !["offline", "online"].includes(mode)) {
|
|
5714
|
+
console.error("Usage: capture network <offline|online> [--target <id>]");
|
|
5715
|
+
process.exit(1);
|
|
5716
|
+
}
|
|
5717
|
+
const offline = mode === "offline";
|
|
5718
|
+
const result = await withConnection(
|
|
5719
|
+
parsed,
|
|
5720
|
+
async (client) => {
|
|
5721
|
+
await client.send("Network.enable");
|
|
5722
|
+
await client.send("Network.emulateNetworkConditions", {
|
|
5723
|
+
offline,
|
|
5724
|
+
latency: offline ? -1 : 0,
|
|
5725
|
+
downloadThroughput: offline ? 0 : -1,
|
|
5726
|
+
uploadThroughput: offline ? 0 : -1
|
|
5727
|
+
});
|
|
5728
|
+
return { network: mode, offline };
|
|
5729
|
+
},
|
|
5730
|
+
{ settle: 0 }
|
|
5731
|
+
);
|
|
5732
|
+
console.log(JSON.stringify(result, null, 2));
|
|
5733
|
+
if (offline) {
|
|
5734
|
+
console.error("\nNetwork disabled. WebSocket connections will drop.");
|
|
5735
|
+
console.error("Restore with: capture network online");
|
|
5736
|
+
} else {
|
|
5737
|
+
console.error("\nNetwork restored.");
|
|
5738
|
+
}
|
|
5739
|
+
return;
|
|
5740
|
+
}
|
|
5741
|
+
var init_traffic = __esm({
|
|
5742
|
+
"src/cdp/commands/traffic.ts"() {
|
|
5743
|
+
init_record();
|
|
5744
|
+
init_connection();
|
|
5745
|
+
init_har_manager();
|
|
5746
|
+
}
|
|
5747
|
+
});
|
|
5748
|
+
|
|
5749
|
+
// src/cdp/commands/har.ts
|
|
5750
|
+
async function cmdHar(parsed, args) {
|
|
5751
|
+
if (parsed.help) {
|
|
5752
|
+
console.log(
|
|
5753
|
+
"Usage: capture har <create|read|delete>\n\n create Create a new HAR recording (returns id)\n read [id] Read accumulated HAR entries (id optional in active session)\n delete <id> Delete a HAR recording\n\nFilters (read):\n --filter-url <pattern> Substring or regex match on URL\n --filter-status <code> Status code, prefix (e.g. 4), or range (e.g. 400-499)\n --filter-method <method> HTTP method (GET, POST, ...)\n --limit <N> Return only the first N matching entries\n\nPass --har <id> to exec/navigate commands to append traffic to a recording."
|
|
5754
|
+
);
|
|
5755
|
+
process.exit(0);
|
|
5756
|
+
}
|
|
5757
|
+
const subcommand = args[1];
|
|
5758
|
+
if (subcommand === "create") {
|
|
5759
|
+
const { id, path: harPath } = createHarRecording();
|
|
5760
|
+
console.log(JSON.stringify({ id, path: harPath }, null, 2));
|
|
5761
|
+
console.error(
|
|
5762
|
+
`
|
|
5763
|
+
Pass --har ${id} to subsequent commands to record traffic.`
|
|
5764
|
+
);
|
|
5765
|
+
return;
|
|
5766
|
+
}
|
|
5767
|
+
if (subcommand === "read") {
|
|
5768
|
+
let id = args[2];
|
|
5769
|
+
if (id && id.startsWith("--")) id = void 0;
|
|
5770
|
+
if (!id) {
|
|
5771
|
+
const session = getActiveSession();
|
|
5772
|
+
if (session?.harId) {
|
|
5773
|
+
id = session.harId;
|
|
5774
|
+
} else {
|
|
5775
|
+
console.error("Usage: capture har read [id]\n ID is optional when a capture session is active.");
|
|
5776
|
+
process.exit(1);
|
|
5500
5777
|
}
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5778
|
+
}
|
|
5779
|
+
const har = readHarRecording(id);
|
|
5780
|
+
if (!har) {
|
|
5781
|
+
console.error(`No HAR recording found for id: ${id}`);
|
|
5782
|
+
process.exit(1);
|
|
5783
|
+
}
|
|
5784
|
+
let entries = har.log.entries;
|
|
5785
|
+
const total = entries.length;
|
|
5786
|
+
const filters = [];
|
|
5787
|
+
if (parsed.filterUrl) {
|
|
5788
|
+
const pattern = parsed.filterUrl;
|
|
5789
|
+
let re = null;
|
|
5790
|
+
try {
|
|
5791
|
+
re = new RegExp(pattern, "i");
|
|
5792
|
+
} catch {
|
|
5793
|
+
re = null;
|
|
5516
5794
|
}
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
5795
|
+
entries = entries.filter(
|
|
5796
|
+
(e) => re ? re.test(e.request.url) : e.request.url.toLowerCase().includes(pattern.toLowerCase())
|
|
5797
|
+
);
|
|
5798
|
+
filters.push(`url~${pattern}`);
|
|
5799
|
+
}
|
|
5800
|
+
if (parsed.filterStatus) {
|
|
5801
|
+
const s = parsed.filterStatus;
|
|
5802
|
+
let matcher;
|
|
5803
|
+
if (/^\d+-\d+$/.test(s)) {
|
|
5804
|
+
const [lo, hi] = s.split("-").map((n) => parseInt(n, 10));
|
|
5805
|
+
matcher = (c) => c >= lo && c <= hi;
|
|
5806
|
+
} else if (/^\d+$/.test(s)) {
|
|
5807
|
+
if (s.length < 3) {
|
|
5808
|
+
matcher = (c) => String(c).startsWith(s);
|
|
5809
|
+
} else {
|
|
5810
|
+
const n = parseInt(s, 10);
|
|
5811
|
+
matcher = (c) => c === n;
|
|
5520
5812
|
}
|
|
5521
|
-
|
|
5813
|
+
} else {
|
|
5814
|
+
matcher = () => true;
|
|
5522
5815
|
}
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5816
|
+
entries = entries.filter((e) => matcher(e.response.status));
|
|
5817
|
+
filters.push(`status=${s}`);
|
|
5818
|
+
}
|
|
5819
|
+
if (parsed.filterMethod) {
|
|
5820
|
+
const m = parsed.filterMethod;
|
|
5821
|
+
entries = entries.filter((e) => e.request.method.toUpperCase() === m);
|
|
5822
|
+
filters.push(`method=${m}`);
|
|
5823
|
+
}
|
|
5824
|
+
if (typeof parsed.limit === "number" && parsed.limit > 0) {
|
|
5825
|
+
entries = entries.slice(0, parsed.limit);
|
|
5826
|
+
filters.push(`limit=${parsed.limit}`);
|
|
5827
|
+
}
|
|
5828
|
+
const filtersLabel = filters.length > 0 ? ` [${filters.join(", ")}]` : "";
|
|
5829
|
+
console.error(
|
|
5830
|
+
`HAR: ${harFilePath(id)} (${entries.length}/${total} entries)${filtersLabel}`
|
|
5831
|
+
);
|
|
5832
|
+
console.log(JSON.stringify({ log: { entries } }, null, 2));
|
|
5833
|
+
return;
|
|
5834
|
+
}
|
|
5835
|
+
if (subcommand === "delete") {
|
|
5836
|
+
const id = args[2];
|
|
5837
|
+
if (!id) {
|
|
5838
|
+
console.error("Usage: capture har delete <id>");
|
|
5839
|
+
process.exit(1);
|
|
5840
|
+
}
|
|
5841
|
+
deleteHarRecording(id);
|
|
5842
|
+
console.log(JSON.stringify({ deleted: true, id }, null, 2));
|
|
5843
|
+
return;
|
|
5844
|
+
}
|
|
5845
|
+
console.error(
|
|
5846
|
+
"Usage: capture har <create|read|delete>\n create Create a new HAR recording (returns id)\n read <id> Read accumulated HAR entries\n delete <id> Delete a HAR recording"
|
|
5847
|
+
);
|
|
5848
|
+
process.exit(1);
|
|
5849
|
+
}
|
|
5850
|
+
var init_har = __esm({
|
|
5851
|
+
"src/cdp/commands/har.ts"() {
|
|
5852
|
+
init_har_manager();
|
|
5853
|
+
init_session_context();
|
|
5854
|
+
}
|
|
5855
|
+
});
|
|
5856
|
+
|
|
5857
|
+
// src/vault/docs.ts
|
|
5858
|
+
function isAgentVisible(doc) {
|
|
5859
|
+
return AGENT_VISIBLE_VISIBILITIES.has(doc.visibility);
|
|
5860
|
+
}
|
|
5861
|
+
function cacheDir() {
|
|
5862
|
+
return path6.join(vaultRepoRoot(), ".cache", "docs");
|
|
5863
|
+
}
|
|
5864
|
+
function schemasPathFor(svc) {
|
|
5865
|
+
return path6.join(vaultLibsDir(), svc, "schemas.ts");
|
|
5866
|
+
}
|
|
5867
|
+
function srcSha(svc) {
|
|
5868
|
+
return (0, import_crypto.createHash)("sha256").update(fs11.readFileSync(schemasPathFor(svc), "utf8")).digest("hex");
|
|
5869
|
+
}
|
|
5870
|
+
function toJsonSchema(zodSchema) {
|
|
5871
|
+
const s = zodSchema;
|
|
5872
|
+
if (s?._zod?.def?.type === "void") {
|
|
5873
|
+
const desc = s.description;
|
|
5874
|
+
return desc ? { type: "null", description: desc } : { type: "null" };
|
|
5875
|
+
}
|
|
5876
|
+
if (!s?.toJSONSchema) {
|
|
5877
|
+
throw new Error(
|
|
5878
|
+
"Schema missing toJSONSchema method - ensure Zod 4+ is installed"
|
|
5879
|
+
);
|
|
5880
|
+
}
|
|
5881
|
+
return s.toJSONSchema();
|
|
5882
|
+
}
|
|
5883
|
+
async function buildDoc(svc) {
|
|
5884
|
+
const esbuild = await loadEsbuild();
|
|
5885
|
+
const result = await esbuild.build({
|
|
5886
|
+
entryPoints: [schemasPathFor(svc)],
|
|
5887
|
+
bundle: true,
|
|
5888
|
+
write: false,
|
|
5889
|
+
format: "cjs",
|
|
5890
|
+
platform: "node",
|
|
5891
|
+
target: "es2020",
|
|
5892
|
+
logLevel: "silent"
|
|
5893
|
+
});
|
|
5894
|
+
const text = result.outputFiles?.[0]?.text;
|
|
5895
|
+
if (!text) throw new Error(`esbuild produced no output for ${svc}/schemas.ts`);
|
|
5896
|
+
const mod = { exports: {} };
|
|
5897
|
+
new Function("module", "exports", "require", text)(
|
|
5898
|
+
mod,
|
|
5899
|
+
mod.exports,
|
|
5900
|
+
cjsRequire
|
|
5901
|
+
);
|
|
5902
|
+
const x = mod.exports;
|
|
5903
|
+
const allSchemas = x.allSchemas ?? [];
|
|
5904
|
+
const doc = {
|
|
5905
|
+
library: `@vallum/${svc}`,
|
|
5906
|
+
description: x.libraryDescription ?? `${svc} service library`,
|
|
5907
|
+
visibility: x.libraryVisibility ?? "public",
|
|
5908
|
+
functions: allSchemas.map((schema) => ({
|
|
5909
|
+
name: schema.name,
|
|
5910
|
+
description: schema.description,
|
|
5911
|
+
...schema.notes ? { notes: schema.notes } : {},
|
|
5912
|
+
input: toJsonSchema(schema.input),
|
|
5913
|
+
output: toJsonSchema(schema.output)
|
|
5914
|
+
}))
|
|
5915
|
+
};
|
|
5916
|
+
if (x.libraryNotes) doc.notes = x.libraryNotes;
|
|
5917
|
+
if (x.loginUrl) doc.loginUrl = x.loginUrl;
|
|
5918
|
+
return doc;
|
|
5919
|
+
}
|
|
5920
|
+
async function getDoc(name) {
|
|
5921
|
+
if (!isValidLibName(name) || !fs11.existsSync(schemasPathFor(name))) {
|
|
5922
|
+
throw new Error(unknownLibMsg(name));
|
|
5923
|
+
}
|
|
5924
|
+
const sha = srcSha(name);
|
|
5925
|
+
const cacheFile = path6.join(cacheDir(), `${name}.json`);
|
|
5926
|
+
if (fs11.existsSync(cacheFile)) {
|
|
5927
|
+
try {
|
|
5928
|
+
const cached = JSON.parse(fs11.readFileSync(cacheFile, "utf8"));
|
|
5929
|
+
if (cached._srcSha === sha) {
|
|
5930
|
+
delete cached._srcSha;
|
|
5931
|
+
return cached;
|
|
5932
|
+
}
|
|
5933
|
+
} catch {
|
|
5934
|
+
}
|
|
5935
|
+
}
|
|
5936
|
+
const doc = await buildDoc(name);
|
|
5937
|
+
fs11.mkdirSync(cacheDir(), { recursive: true });
|
|
5938
|
+
fs11.writeFileSync(
|
|
5939
|
+
cacheFile,
|
|
5940
|
+
JSON.stringify({ ...doc, _srcSha: sha }, null, 2)
|
|
5941
|
+
);
|
|
5942
|
+
return doc;
|
|
5943
|
+
}
|
|
5944
|
+
async function listDocs() {
|
|
5945
|
+
const out = [];
|
|
5946
|
+
for (const name of listLibs()) {
|
|
5947
|
+
let doc;
|
|
5948
|
+
try {
|
|
5949
|
+
doc = await getDoc(name);
|
|
5950
|
+
} catch (e) {
|
|
5951
|
+
console.error(
|
|
5952
|
+
`WARN: skipping lib "${name}" \u2014 ${e.message}`
|
|
5953
|
+
);
|
|
5954
|
+
continue;
|
|
5955
|
+
}
|
|
5956
|
+
if (isAgentVisible(doc)) out.push(doc);
|
|
5957
|
+
}
|
|
5958
|
+
return out;
|
|
5959
|
+
}
|
|
5960
|
+
var fs11, path6, import_crypto, AGENT_VISIBLE_VISIBILITIES, cjsRequire;
|
|
5961
|
+
var init_docs = __esm({
|
|
5962
|
+
"src/vault/docs.ts"() {
|
|
5963
|
+
fs11 = __toESM(require("fs"));
|
|
5964
|
+
path6 = __toESM(require("path"));
|
|
5965
|
+
import_crypto = require("crypto");
|
|
5966
|
+
init_paths();
|
|
5967
|
+
AGENT_VISIBLE_VISIBILITIES = /* @__PURE__ */ new Set(["public", "chat"]);
|
|
5968
|
+
cjsRequire = (id) => {
|
|
5969
|
+
if (typeof require === "function") return require(id);
|
|
5970
|
+
throw new Error(`cannot require('${id}') \u2014 unavailable in this runtime`);
|
|
5971
|
+
};
|
|
5972
|
+
}
|
|
5973
|
+
});
|
|
5974
|
+
|
|
5975
|
+
// src/cdp/commands/lib.ts
|
|
5976
|
+
function scoreToken(token, service, fnName, fnDesc, serviceDesc) {
|
|
5977
|
+
const q = token.toLowerCase();
|
|
5978
|
+
const fn = fnName.toLowerCase();
|
|
5979
|
+
const svc = service.toLowerCase();
|
|
5980
|
+
const fnD = fnDesc.toLowerCase();
|
|
5981
|
+
const svcD = serviceDesc.toLowerCase();
|
|
5982
|
+
let score = 0;
|
|
5983
|
+
let match = "";
|
|
5984
|
+
if (fn === q) {
|
|
5985
|
+
score += FUNCTION_NAME_EXACT;
|
|
5986
|
+
match = "function-name (exact)";
|
|
5987
|
+
} else if (fn.startsWith(q)) {
|
|
5988
|
+
score += FUNCTION_NAME_PREFIX;
|
|
5989
|
+
match = "function-name (prefix)";
|
|
5990
|
+
} else if (fn.includes(q)) {
|
|
5991
|
+
score += FUNCTION_NAME_SUBSTRING;
|
|
5992
|
+
match = "function-name";
|
|
5993
|
+
}
|
|
5994
|
+
if (svc.includes(q)) {
|
|
5995
|
+
score += SERVICE_NAME_MATCH;
|
|
5996
|
+
if (!match) match = "service-name";
|
|
5997
|
+
}
|
|
5998
|
+
if (fnD.includes(q)) {
|
|
5999
|
+
score += FUNCTION_DESC_SUBSTRING;
|
|
6000
|
+
if (!match) match = "function-desc";
|
|
6001
|
+
}
|
|
6002
|
+
if (svcD.includes(q)) {
|
|
6003
|
+
score += SERVICE_DESC_SUBSTRING;
|
|
6004
|
+
if (!match) match = "service-desc";
|
|
6005
|
+
}
|
|
6006
|
+
return { score, match };
|
|
6007
|
+
}
|
|
6008
|
+
function scoreHit(query, service, fnName, fnDesc, serviceDesc) {
|
|
6009
|
+
const tokens = query.split(/\s+/).filter(Boolean);
|
|
6010
|
+
if (tokens.length === 0) return { score: 0, match: "" };
|
|
6011
|
+
if (tokens.length === 1) {
|
|
6012
|
+
return scoreToken(tokens[0], service, fnName, fnDesc, serviceDesc);
|
|
6013
|
+
}
|
|
6014
|
+
let total = 0;
|
|
6015
|
+
let bestScore = -1;
|
|
6016
|
+
let bestMatch = "";
|
|
6017
|
+
for (const token of tokens) {
|
|
6018
|
+
const r = scoreToken(token, service, fnName, fnDesc, serviceDesc);
|
|
6019
|
+
if (r.score === 0) return { score: 0, match: "" };
|
|
6020
|
+
total += r.score;
|
|
6021
|
+
if (r.score > bestScore) {
|
|
6022
|
+
bestScore = r.score;
|
|
6023
|
+
bestMatch = r.match;
|
|
6024
|
+
}
|
|
6025
|
+
}
|
|
6026
|
+
return { score: total, match: bestMatch };
|
|
6027
|
+
}
|
|
6028
|
+
function resolveName(query, names) {
|
|
6029
|
+
if (names.includes(query)) return { name: query };
|
|
6030
|
+
const lower = query.toLowerCase();
|
|
6031
|
+
const prefix = names.filter((n) => n.toLowerCase().startsWith(lower));
|
|
6032
|
+
if (prefix.length === 1) return { name: prefix[0] };
|
|
6033
|
+
if (prefix.length > 1) {
|
|
6034
|
+
return { error: `Ambiguous lib "${query}" \u2014 matches: ${prefix.join(", ")}` };
|
|
6035
|
+
}
|
|
6036
|
+
return { error: `Unknown lib "${query}". Available: ${names.join(", ")}` };
|
|
6037
|
+
}
|
|
6038
|
+
async function cmdLib(parsed, _args) {
|
|
6039
|
+
if (parsed.help) {
|
|
6040
|
+
console.log(USAGE);
|
|
6041
|
+
process.exit(0);
|
|
6042
|
+
}
|
|
6043
|
+
const sub = parsed.positional[0];
|
|
6044
|
+
if (sub === "list") {
|
|
6045
|
+
const docs = await listDocs();
|
|
6046
|
+
const rows = docs.map((d) => {
|
|
6047
|
+
const name = svcName(d);
|
|
6048
|
+
return {
|
|
6049
|
+
name,
|
|
6050
|
+
functionCount: d.functions.length,
|
|
6051
|
+
description: d.description,
|
|
6052
|
+
srcPath: srcPathFor(name)
|
|
6053
|
+
};
|
|
6054
|
+
});
|
|
6055
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
6056
|
+
console.error(
|
|
6057
|
+
`
|
|
6058
|
+
${rows.length} libs. Next: capture lib show <name> \xB7 capture lib read <name> <fn>`
|
|
6059
|
+
);
|
|
6060
|
+
return;
|
|
6061
|
+
}
|
|
6062
|
+
if (sub === "search") {
|
|
6063
|
+
const query = parsed.positional[1];
|
|
6064
|
+
if (!query) {
|
|
6065
|
+
console.error('Usage: capture lib search "<query>" [--limit N]');
|
|
6066
|
+
process.exit(1);
|
|
6067
|
+
}
|
|
6068
|
+
const limit = Math.min(
|
|
6069
|
+
typeof parsed.limit === "number" && parsed.limit > 0 ? parsed.limit : 20,
|
|
6070
|
+
500
|
|
6071
|
+
);
|
|
6072
|
+
const docs = await listDocs();
|
|
6073
|
+
const hits = [];
|
|
6074
|
+
for (const d of docs) {
|
|
6075
|
+
const service = svcName(d);
|
|
6076
|
+
for (const fn of d.functions) {
|
|
6077
|
+
const { score, match } = scoreHit(
|
|
6078
|
+
query,
|
|
6079
|
+
service,
|
|
6080
|
+
fn.name,
|
|
6081
|
+
fn.description ?? "",
|
|
6082
|
+
d.description ?? ""
|
|
6083
|
+
);
|
|
6084
|
+
if (score > 0) {
|
|
6085
|
+
hits.push({
|
|
6086
|
+
service,
|
|
6087
|
+
function: fn.name,
|
|
6088
|
+
match,
|
|
6089
|
+
score,
|
|
6090
|
+
description: fn.description,
|
|
6091
|
+
srcPath: srcPathFor(service)
|
|
6092
|
+
});
|
|
5526
6093
|
}
|
|
5527
|
-
this.pendingTimeouts.clear();
|
|
5528
|
-
this.ws.close();
|
|
5529
6094
|
}
|
|
6095
|
+
}
|
|
6096
|
+
hits.sort((a, b) => b.score - a.score);
|
|
6097
|
+
const top = hits.slice(0, limit);
|
|
6098
|
+
console.log(JSON.stringify(top, null, 2));
|
|
6099
|
+
console.error(
|
|
6100
|
+
`
|
|
6101
|
+
${hits.length} hit(s)${hits.length > limit ? ` (showing ${limit})` : ""}. Next: capture lib read <service> <function>`
|
|
6102
|
+
);
|
|
6103
|
+
return;
|
|
6104
|
+
}
|
|
6105
|
+
if (sub === "show") {
|
|
6106
|
+
const name = parsed.positional[1];
|
|
6107
|
+
if (!name) {
|
|
6108
|
+
console.error("Usage: capture lib show <name>");
|
|
6109
|
+
process.exit(1);
|
|
6110
|
+
}
|
|
6111
|
+
const r = resolveName(name, listLibs());
|
|
6112
|
+
if ("error" in r) {
|
|
6113
|
+
console.error(r.error);
|
|
6114
|
+
process.exit(1);
|
|
6115
|
+
}
|
|
6116
|
+
let doc;
|
|
6117
|
+
try {
|
|
6118
|
+
doc = await getDoc(r.name);
|
|
6119
|
+
} catch (e) {
|
|
6120
|
+
console.error(e.message);
|
|
6121
|
+
process.exit(1);
|
|
6122
|
+
}
|
|
6123
|
+
if (!isAgentVisible(doc)) {
|
|
6124
|
+
console.error(
|
|
6125
|
+
`Unknown lib "${name}". Available: ${listLibs().join(", ")}`
|
|
6126
|
+
);
|
|
6127
|
+
process.exit(1);
|
|
6128
|
+
}
|
|
6129
|
+
const service = r.name;
|
|
6130
|
+
const out = {
|
|
6131
|
+
service,
|
|
6132
|
+
description: doc.description,
|
|
6133
|
+
...doc.loginUrl ? { loginUrl: doc.loginUrl } : {},
|
|
6134
|
+
...doc.notes ? { notes: doc.notes } : {},
|
|
6135
|
+
srcPath: srcPathFor(service),
|
|
6136
|
+
functions: doc.functions.map((f) => ({
|
|
6137
|
+
name: f.name,
|
|
6138
|
+
description: f.description
|
|
6139
|
+
}))
|
|
5530
6140
|
};
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
6141
|
+
console.log(JSON.stringify(out, null, 2));
|
|
6142
|
+
console.error(
|
|
6143
|
+
`
|
|
6144
|
+
Schemas required before exec \u2014 capture lib read ${service} <fn>`
|
|
6145
|
+
);
|
|
6146
|
+
return;
|
|
6147
|
+
}
|
|
6148
|
+
if (sub === "read") {
|
|
6149
|
+
const name = parsed.positional[1];
|
|
6150
|
+
if (!name) {
|
|
6151
|
+
console.error("Usage: capture lib read <name> [fn\u2026]");
|
|
6152
|
+
process.exit(1);
|
|
6153
|
+
}
|
|
6154
|
+
const r = resolveName(name, listLibs());
|
|
6155
|
+
if ("error" in r) {
|
|
6156
|
+
console.error(r.error);
|
|
6157
|
+
process.exit(1);
|
|
6158
|
+
}
|
|
6159
|
+
let doc;
|
|
6160
|
+
try {
|
|
6161
|
+
doc = await getDoc(r.name);
|
|
6162
|
+
} catch (e) {
|
|
6163
|
+
console.error(e.message);
|
|
6164
|
+
process.exit(1);
|
|
6165
|
+
}
|
|
6166
|
+
const service = svcName(doc);
|
|
6167
|
+
const fns = parsed.positional.slice(2);
|
|
6168
|
+
if (fns.length === 0) {
|
|
6169
|
+
const out2 = {
|
|
6170
|
+
service,
|
|
6171
|
+
description: doc.description,
|
|
6172
|
+
...doc.notes ? { notes: doc.notes } : {},
|
|
6173
|
+
srcPath: srcPathFor(service),
|
|
6174
|
+
functions: doc.functions.map((f) => ({
|
|
6175
|
+
name: f.name,
|
|
6176
|
+
description: f.description
|
|
6177
|
+
}))
|
|
6178
|
+
};
|
|
6179
|
+
console.log(JSON.stringify(out2, null, 2));
|
|
6180
|
+
console.error(
|
|
6181
|
+
`
|
|
6182
|
+
Name functions to load schemas: capture lib read ${service} <fn> [fn\u2026]`
|
|
6183
|
+
);
|
|
6184
|
+
return;
|
|
6185
|
+
}
|
|
6186
|
+
const byName = new Map(doc.functions.map((f) => [f.name, f]));
|
|
6187
|
+
const found = [];
|
|
6188
|
+
const missing = [];
|
|
6189
|
+
for (const op of fns) {
|
|
6190
|
+
const fn = byName.get(op);
|
|
6191
|
+
if (fn) found.push(fn);
|
|
6192
|
+
else missing.push(op);
|
|
6193
|
+
}
|
|
6194
|
+
if (found.length === 0) {
|
|
6195
|
+
const available = doc.functions.map((f) => f.name).join(", ");
|
|
6196
|
+
const label = missing.length > 1 ? `Functions "${missing.join('", "')}"` : `Function "${missing[0]}"`;
|
|
6197
|
+
console.error(`${label} not in ${service}. Available: ${available}`);
|
|
6198
|
+
process.exit(1);
|
|
6199
|
+
}
|
|
6200
|
+
const out = {
|
|
6201
|
+
service,
|
|
6202
|
+
srcPath: srcPathFor(service),
|
|
6203
|
+
functions: found.map((f) => ({
|
|
6204
|
+
name: f.name,
|
|
6205
|
+
description: f.description,
|
|
6206
|
+
...f.notes ? { notes: f.notes } : {},
|
|
6207
|
+
input: f.input,
|
|
6208
|
+
output: f.output
|
|
6209
|
+
})),
|
|
6210
|
+
...missing.length > 0 ? { missing } : {}
|
|
6211
|
+
};
|
|
6212
|
+
console.log(JSON.stringify(out, null, 2));
|
|
6213
|
+
const firstFn = found[0].name;
|
|
6214
|
+
console.error(
|
|
6215
|
+
`
|
|
6216
|
+
Run it: capture exec "import {${firstFn}} from 'libs/${service}'; return await ${firstFn}({\u2026})"`
|
|
6217
|
+
);
|
|
6218
|
+
return;
|
|
6219
|
+
}
|
|
6220
|
+
console.error(USAGE);
|
|
6221
|
+
process.exit(1);
|
|
6222
|
+
}
|
|
6223
|
+
var path7, USAGE, FUNCTION_NAME_EXACT, FUNCTION_NAME_PREFIX, FUNCTION_NAME_SUBSTRING, SERVICE_NAME_MATCH, FUNCTION_DESC_SUBSTRING, SERVICE_DESC_SUBSTRING, svcName, srcPathFor;
|
|
6224
|
+
var init_lib = __esm({
|
|
6225
|
+
"src/cdp/commands/lib.ts"() {
|
|
6226
|
+
path7 = __toESM(require("path"));
|
|
6227
|
+
init_paths();
|
|
6228
|
+
init_docs();
|
|
6229
|
+
USAGE = `Usage: capture lib <list|search|show|read> [options]
|
|
6230
|
+
|
|
6231
|
+
list List available libs (name, fn count, description, srcPath)
|
|
6232
|
+
search "<query>" [--limit N] Fuzzy-search functions across all libs (ranked)
|
|
6233
|
+
show <name> Lib + function summaries (no schemas)
|
|
6234
|
+
read <name> [fn\u2026] Full input/output JSON schemas (+ .ts source path)
|
|
6235
|
+
|
|
6236
|
+
Then run a lib in the active authenticated tab:
|
|
6237
|
+
capture exec "import {<fn>} from 'libs/<name>'; return await <fn>({\u2026})"
|
|
6238
|
+
|
|
6239
|
+
Dev-checkout-only feature (requires vault/ source + esbuild).`;
|
|
6240
|
+
FUNCTION_NAME_EXACT = 100;
|
|
6241
|
+
FUNCTION_NAME_PREFIX = 60;
|
|
6242
|
+
FUNCTION_NAME_SUBSTRING = 40;
|
|
6243
|
+
SERVICE_NAME_MATCH = 50;
|
|
6244
|
+
FUNCTION_DESC_SUBSTRING = 20;
|
|
6245
|
+
SERVICE_DESC_SUBSTRING = 10;
|
|
6246
|
+
svcName = (doc) => doc.library.replace(/^@vallum\//, "");
|
|
6247
|
+
srcPathFor = (name) => path7.join(vaultLibsDir(), name, "index.ts");
|
|
6248
|
+
}
|
|
6249
|
+
});
|
|
6250
|
+
|
|
6251
|
+
// src/cdp/dispatch.ts
|
|
6252
|
+
async function cdpMain2() {
|
|
6253
|
+
const args = process.argv.slice(2);
|
|
6254
|
+
const parsed = parseCliArgs(args);
|
|
6255
|
+
switch (parsed.command) {
|
|
6256
|
+
case "detect":
|
|
6257
|
+
return cmdDetect(parsed, args);
|
|
6258
|
+
case "list":
|
|
6259
|
+
return cmdList(parsed, args);
|
|
6260
|
+
case "open":
|
|
6261
|
+
return cmdOpen(parsed, args);
|
|
6262
|
+
case "reset-tab":
|
|
6263
|
+
return cmdResetTab(parsed, args);
|
|
6264
|
+
case "exec":
|
|
6265
|
+
return cmdExec(parsed, args);
|
|
6266
|
+
case "screenshot":
|
|
6267
|
+
return cmdScreenshot(parsed, args);
|
|
6268
|
+
case "click":
|
|
6269
|
+
return cmdClick(parsed, args);
|
|
6270
|
+
case "type":
|
|
6271
|
+
return cmdType(parsed, args);
|
|
6272
|
+
case "a11y":
|
|
6273
|
+
return cmdA11y(parsed, args);
|
|
6274
|
+
case "record":
|
|
6275
|
+
return cmdRecord(parsed, args);
|
|
6276
|
+
case "navigate":
|
|
6277
|
+
return cmdNavigate(parsed, args);
|
|
6278
|
+
case "network":
|
|
6279
|
+
return cmdNetwork(parsed, args);
|
|
6280
|
+
case "har":
|
|
6281
|
+
return cmdHar(parsed, args);
|
|
6282
|
+
case "lib":
|
|
6283
|
+
return cmdLib(parsed, args);
|
|
6284
|
+
default:
|
|
6285
|
+
console.log(CDP_USAGE);
|
|
6286
|
+
}
|
|
6287
|
+
}
|
|
6288
|
+
var init_dispatch = __esm({
|
|
6289
|
+
"src/cdp/dispatch.ts"() {
|
|
6290
|
+
init_args();
|
|
6291
|
+
init_usage();
|
|
6292
|
+
init_tabs();
|
|
6293
|
+
init_exec();
|
|
6294
|
+
init_ui();
|
|
6295
|
+
init_traffic();
|
|
6296
|
+
init_har();
|
|
6297
|
+
init_lib();
|
|
6298
|
+
}
|
|
6299
|
+
});
|
|
6300
|
+
|
|
6301
|
+
// src/cdp.ts
|
|
6302
|
+
var cdp_exports = {};
|
|
6303
|
+
__export(cdp_exports, {
|
|
6304
|
+
CDPClient: () => CDPClient,
|
|
6305
|
+
ConsoleRecorder: () => ConsoleRecorder,
|
|
6306
|
+
HARRecorder: () => HARRecorder,
|
|
6307
|
+
acquireTabLock: () => acquireTabLock,
|
|
6308
|
+
captureScreenshot: () => captureScreenshot,
|
|
6309
|
+
cdpMain: () => cdpMain2,
|
|
6310
|
+
detectCdpPort: () => detectCdpPort,
|
|
6311
|
+
executeInBrowser: () => executeInBrowser,
|
|
6312
|
+
findTab: () => findTab,
|
|
6313
|
+
findTabById: () => findTabById,
|
|
6314
|
+
getAccessibilityTree: () => getAccessibilityTree,
|
|
6315
|
+
isTabLocked: () => isTabLocked,
|
|
6316
|
+
listTargets: () => listTargets,
|
|
6317
|
+
navigateAndRecord: () => navigateAndRecord,
|
|
6318
|
+
navigateAndWait: () => navigateAndWait,
|
|
6319
|
+
openTab: () => openTab,
|
|
6320
|
+
recordTraffic: () => recordTraffic,
|
|
6321
|
+
releaseTabLock: () => releaseTabLock,
|
|
6322
|
+
withTabLock: () => withTabLock
|
|
6323
|
+
});
|
|
6324
|
+
var isMainModule;
|
|
6325
|
+
var init_cdp = __esm({
|
|
6326
|
+
"src/cdp.ts"() {
|
|
6327
|
+
init_client();
|
|
6328
|
+
init_detect();
|
|
6329
|
+
init_targets();
|
|
6330
|
+
init_screenshot();
|
|
6331
|
+
init_a11y();
|
|
6332
|
+
init_execute();
|
|
6333
|
+
init_record();
|
|
6334
|
+
init_har_recorder();
|
|
6335
|
+
init_console_recorder();
|
|
6336
|
+
init_tab_lock();
|
|
6337
|
+
init_dispatch();
|
|
5537
6338
|
isMainModule = process.argv[1]?.endsWith("cdp.ts") || process.argv[1]?.endsWith("cdp.mjs");
|
|
5538
6339
|
if (isMainModule) {
|
|
5539
|
-
|
|
6340
|
+
cdpMain().then(() => {
|
|
5540
6341
|
if (process.stdout.writableNeedDrain) {
|
|
5541
6342
|
process.stdout.once("drain", () => process.exit(0));
|
|
5542
6343
|
} else {
|
|
@@ -5551,26 +6352,30 @@ var init_cdp = __esm({
|
|
|
5551
6352
|
});
|
|
5552
6353
|
|
|
5553
6354
|
// src/capture.ts
|
|
5554
|
-
var
|
|
5555
|
-
var
|
|
6355
|
+
var fs13 = __toESM(require("fs"));
|
|
6356
|
+
var path9 = __toESM(require("path"));
|
|
6357
|
+
init_cdp();
|
|
6358
|
+
|
|
6359
|
+
// src/session/commands.ts
|
|
6360
|
+
var fs12 = __toESM(require("fs"));
|
|
6361
|
+
var path8 = __toESM(require("path"));
|
|
5556
6362
|
var os3 = __toESM(require("os"));
|
|
5557
6363
|
var import_child_process2 = require("child_process");
|
|
5558
6364
|
init_har_manager();
|
|
5559
|
-
init_cdp();
|
|
5560
6365
|
init_session_context();
|
|
5561
|
-
var CAPTURE_ROOT2 =
|
|
6366
|
+
var CAPTURE_ROOT2 = path8.join(os3.tmpdir(), "capture-sessions");
|
|
5562
6367
|
function sessionDir(id) {
|
|
5563
|
-
return
|
|
6368
|
+
return path8.join(CAPTURE_ROOT2, id);
|
|
5564
6369
|
}
|
|
5565
6370
|
function sessionMetaPath(id) {
|
|
5566
|
-
return
|
|
6371
|
+
return path8.join(sessionDir(id), ".session.json");
|
|
5567
6372
|
}
|
|
5568
6373
|
function readSession(id) {
|
|
5569
6374
|
const metaPath = sessionMetaPath(id);
|
|
5570
|
-
if (!
|
|
6375
|
+
if (!fs12.existsSync(metaPath)) {
|
|
5571
6376
|
throw new Error(`No capture session found: ${id}`);
|
|
5572
6377
|
}
|
|
5573
|
-
return JSON.parse(
|
|
6378
|
+
return JSON.parse(fs12.readFileSync(metaPath, "utf-8"));
|
|
5574
6379
|
}
|
|
5575
6380
|
function generateId() {
|
|
5576
6381
|
return `cap-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
@@ -5584,8 +6389,8 @@ async function start(args) {
|
|
|
5584
6389
|
}
|
|
5585
6390
|
const id = generateId();
|
|
5586
6391
|
const dir = sessionDir(id);
|
|
5587
|
-
|
|
5588
|
-
|
|
6392
|
+
fs12.mkdirSync(path8.join(dir, "shots"), { recursive: true });
|
|
6393
|
+
fs12.mkdirSync(path8.join(dir, "a11y"), { recursive: true });
|
|
5589
6394
|
let harId = null;
|
|
5590
6395
|
try {
|
|
5591
6396
|
const harResult = createHarRecording();
|
|
@@ -5614,7 +6419,7 @@ async function start(args) {
|
|
|
5614
6419
|
stepCount: 0,
|
|
5615
6420
|
logPids: []
|
|
5616
6421
|
};
|
|
5617
|
-
|
|
6422
|
+
fs12.writeFileSync(sessionMetaPath(id), JSON.stringify(session, null, 2));
|
|
5618
6423
|
setActiveSession({
|
|
5619
6424
|
sessionId: id,
|
|
5620
6425
|
dir,
|
|
@@ -5627,8 +6432,8 @@ async function start(args) {
|
|
|
5627
6432
|
bundleDir: dir,
|
|
5628
6433
|
harId,
|
|
5629
6434
|
targetId,
|
|
5630
|
-
shotsDir:
|
|
5631
|
-
a11yDir:
|
|
6435
|
+
shotsDir: path8.join(dir, "shots"),
|
|
6436
|
+
a11yDir: path8.join(dir, "a11y")
|
|
5632
6437
|
};
|
|
5633
6438
|
console.log(JSON.stringify(result, null, 2));
|
|
5634
6439
|
console.error(`
|
|
@@ -5645,8 +6450,8 @@ function logCommand(args) {
|
|
|
5645
6450
|
console.error("Usage: capture log <path> [--name label] [--session <id>]");
|
|
5646
6451
|
process.exit(1);
|
|
5647
6452
|
}
|
|
5648
|
-
const resolved =
|
|
5649
|
-
if (!
|
|
6453
|
+
const resolved = path8.resolve(sourcePath);
|
|
6454
|
+
if (!fs12.existsSync(resolved)) {
|
|
5650
6455
|
throw new Error(`Log file not found: ${resolved}`);
|
|
5651
6456
|
}
|
|
5652
6457
|
let name = null;
|
|
@@ -5663,21 +6468,21 @@ function logCommand(args) {
|
|
|
5663
6468
|
sessionId = active.sessionId;
|
|
5664
6469
|
}
|
|
5665
6470
|
const session = readSession(sessionId);
|
|
5666
|
-
name = name ??
|
|
5667
|
-
const logsDir =
|
|
5668
|
-
|
|
5669
|
-
const destPath =
|
|
5670
|
-
const outFd =
|
|
6471
|
+
name = name ?? path8.basename(resolved, path8.extname(resolved));
|
|
6472
|
+
const logsDir = path8.join(session.dir, "logs");
|
|
6473
|
+
fs12.mkdirSync(logsDir, { recursive: true });
|
|
6474
|
+
const destPath = path8.join(logsDir, `${name}.log`);
|
|
6475
|
+
const outFd = fs12.openSync(destPath, "a");
|
|
5671
6476
|
const child = (0, import_child_process2.spawn)(
|
|
5672
6477
|
"sh",
|
|
5673
6478
|
["-c", `tail -f "${resolved}" | perl -MPOSIX -ne 'print strftime("%Y-%m-%dT%H:%M:%SZ",gmtime())." ".$_'`],
|
|
5674
6479
|
{ detached: true, stdio: ["ignore", outFd, "ignore"] }
|
|
5675
6480
|
);
|
|
5676
6481
|
child.unref();
|
|
5677
|
-
|
|
6482
|
+
fs12.closeSync(outFd);
|
|
5678
6483
|
const pid = child.pid;
|
|
5679
6484
|
session.logPids.push({ pid, name, sourcePath: resolved });
|
|
5680
|
-
|
|
6485
|
+
fs12.writeFileSync(sessionMetaPath(session.id), JSON.stringify(session, null, 2));
|
|
5681
6486
|
console.log(JSON.stringify({ name, sourcePath: resolved, destPath, pid }, null, 2));
|
|
5682
6487
|
}
|
|
5683
6488
|
async function stop(args) {
|
|
@@ -5699,17 +6504,17 @@ async function stop(args) {
|
|
|
5699
6504
|
if (session.logPids?.length) {
|
|
5700
6505
|
await new Promise((r) => setTimeout(r, 200));
|
|
5701
6506
|
}
|
|
5702
|
-
const shotsDir =
|
|
5703
|
-
const screenshots =
|
|
5704
|
-
const a11yDir =
|
|
5705
|
-
const a11y =
|
|
6507
|
+
const shotsDir = path8.join(session.dir, "shots");
|
|
6508
|
+
const screenshots = fs12.existsSync(shotsDir) ? fs12.readdirSync(shotsDir).filter((f) => f.endsWith(".png") || f.endsWith(".jpg")).map((f) => ({ name: f, path: path8.join(shotsDir, f) })) : [];
|
|
6509
|
+
const a11yDir = path8.join(session.dir, "a11y");
|
|
6510
|
+
const a11y = fs12.existsSync(a11yDir) ? fs12.readdirSync(a11yDir).filter((f) => f.endsWith(".json") || f.endsWith(".txt")).map((f) => ({ name: f, path: path8.join(a11yDir, f) })) : [];
|
|
5706
6511
|
let har = null;
|
|
5707
6512
|
if (session.harId) {
|
|
5708
6513
|
try {
|
|
5709
6514
|
const harData = readHarRecording(session.harId);
|
|
5710
6515
|
if (harData) {
|
|
5711
|
-
const harPath =
|
|
5712
|
-
|
|
6516
|
+
const harPath = path8.join(session.dir, "har.json");
|
|
6517
|
+
fs12.writeFileSync(harPath, JSON.stringify(harData, null, 2));
|
|
5713
6518
|
har = { id: session.harId, path: harPath, entryCount: harData.log.entries.length };
|
|
5714
6519
|
try {
|
|
5715
6520
|
deleteHarRecording(session.harId);
|
|
@@ -5720,16 +6525,16 @@ async function stop(args) {
|
|
|
5720
6525
|
console.error(`Warning: could not read HAR: ${err instanceof Error ? err.message : err}`);
|
|
5721
6526
|
}
|
|
5722
6527
|
}
|
|
5723
|
-
const logsDir =
|
|
5724
|
-
const logs =
|
|
5725
|
-
const filePath =
|
|
5726
|
-
const content =
|
|
6528
|
+
const logsDir = path8.join(session.dir, "logs");
|
|
6529
|
+
const logs = fs12.existsSync(logsDir) ? fs12.readdirSync(logsDir).filter((f) => f.endsWith(".log")).map((f) => {
|
|
6530
|
+
const filePath = path8.join(logsDir, f);
|
|
6531
|
+
const content = fs12.readFileSync(filePath, "utf-8");
|
|
5727
6532
|
const lines = content ? content.split("\n").filter(Boolean).length : 0;
|
|
5728
6533
|
return { name: f, path: filePath, lines };
|
|
5729
6534
|
}) : [];
|
|
5730
6535
|
const knownDirs = /* @__PURE__ */ new Set(["shots", "a11y", "logs"]);
|
|
5731
6536
|
const knownFiles = /* @__PURE__ */ new Set([".session.json", "har.json", "bundle.json"]);
|
|
5732
|
-
const other =
|
|
6537
|
+
const other = fs12.readdirSync(session.dir).filter((f) => !knownDirs.has(f) && !knownFiles.has(f)).map((f) => ({ name: f, path: path8.join(session.dir, f) }));
|
|
5733
6538
|
clearActiveSession();
|
|
5734
6539
|
const manifest = {
|
|
5735
6540
|
id: session.id,
|
|
@@ -5743,8 +6548,8 @@ async function stop(args) {
|
|
|
5743
6548
|
logs,
|
|
5744
6549
|
other
|
|
5745
6550
|
};
|
|
5746
|
-
const bundlePath =
|
|
5747
|
-
|
|
6551
|
+
const bundlePath = path8.join(session.dir, "bundle.json");
|
|
6552
|
+
fs12.writeFileSync(bundlePath, JSON.stringify(manifest, null, 2));
|
|
5748
6553
|
console.log(JSON.stringify({
|
|
5749
6554
|
bundlePath,
|
|
5750
6555
|
summary: {
|
|
@@ -5761,13 +6566,13 @@ Bundle written: ${bundlePath}`);
|
|
|
5761
6566
|
console.error(`Read it: capture session view ${id}`);
|
|
5762
6567
|
}
|
|
5763
6568
|
function list() {
|
|
5764
|
-
if (!
|
|
6569
|
+
if (!fs12.existsSync(CAPTURE_ROOT2)) {
|
|
5765
6570
|
console.log("[]");
|
|
5766
6571
|
return;
|
|
5767
6572
|
}
|
|
5768
|
-
const sessions =
|
|
6573
|
+
const sessions = fs12.readdirSync(CAPTURE_ROOT2).filter((d) => fs12.existsSync(sessionMetaPath(d))).map((d) => {
|
|
5769
6574
|
const session = readSession(d);
|
|
5770
|
-
const hasBundled =
|
|
6575
|
+
const hasBundled = fs12.existsSync(path8.join(session.dir, "bundle.json"));
|
|
5771
6576
|
return { id: session.id, startedAt: session.startedAt, url: session.url, status: hasBundled ? "stopped" : "active" };
|
|
5772
6577
|
});
|
|
5773
6578
|
console.log(JSON.stringify(sessions, null, 2));
|
|
@@ -5779,12 +6584,12 @@ function view(args) {
|
|
|
5779
6584
|
process.exit(1);
|
|
5780
6585
|
}
|
|
5781
6586
|
const session = readSession(id);
|
|
5782
|
-
const bundlePath =
|
|
5783
|
-
if (!
|
|
6587
|
+
const bundlePath = path8.join(session.dir, "bundle.json");
|
|
6588
|
+
if (!fs12.existsSync(bundlePath)) {
|
|
5784
6589
|
console.error(`Session ${id} hasn't been stopped yet. Run: capture session stop ${id}`);
|
|
5785
6590
|
process.exit(1);
|
|
5786
6591
|
}
|
|
5787
|
-
const manifest = JSON.parse(
|
|
6592
|
+
const manifest = JSON.parse(fs12.readFileSync(bundlePath, "utf-8"));
|
|
5788
6593
|
const filter = args.find((_, i) => args[i - 1] === "--filter");
|
|
5789
6594
|
if (filter) {
|
|
5790
6595
|
const section = manifest[filter];
|
|
@@ -5832,17 +6637,19 @@ Related: capture log <path> [--name label] Tail a log into the active session
|
|
|
5832
6637
|
See also: capture --help Full command list`);
|
|
5833
6638
|
}
|
|
5834
6639
|
}
|
|
5835
|
-
|
|
6640
|
+
|
|
6641
|
+
// src/capture.ts
|
|
6642
|
+
async function main() {
|
|
5836
6643
|
const [command, ...args] = process.argv.slice(2);
|
|
5837
6644
|
if (command === "--version" || command === "-v" || command === "version") {
|
|
5838
|
-
const declared = "1.3.
|
|
6645
|
+
const declared = "1.3.5";
|
|
5839
6646
|
if (declared) {
|
|
5840
6647
|
console.log(declared);
|
|
5841
6648
|
return;
|
|
5842
6649
|
}
|
|
5843
6650
|
try {
|
|
5844
|
-
const pkgPath =
|
|
5845
|
-
const pkg = JSON.parse(
|
|
6651
|
+
const pkgPath = path9.resolve(__dirname, "..", "package.json");
|
|
6652
|
+
const pkg = JSON.parse(fs13.readFileSync(pkgPath, "utf-8"));
|
|
5846
6653
|
console.log(pkg.version);
|
|
5847
6654
|
} catch {
|
|
5848
6655
|
console.log("unknown");
|
|
@@ -5866,9 +6673,10 @@ async function main2() {
|
|
|
5866
6673
|
case "record":
|
|
5867
6674
|
case "navigate":
|
|
5868
6675
|
case "har":
|
|
6676
|
+
case "lib":
|
|
5869
6677
|
case "network":
|
|
5870
6678
|
case "list":
|
|
5871
|
-
return
|
|
6679
|
+
return cdpMain2();
|
|
5872
6680
|
default:
|
|
5873
6681
|
console.log(`Capture \u2014 browser automation over CDP for validating UI features.
|
|
5874
6682
|
|
|
@@ -5921,6 +6729,14 @@ DIAGNOSTICS & ONE-OFFS (no session needed)
|
|
|
5921
6729
|
network <offline|online> Toggle connectivity for a tab
|
|
5922
6730
|
har create | read [id] | delete <id> Manage standalone HAR recordings
|
|
5923
6731
|
|
|
6732
|
+
LIBRARY (vault libs \u2014 dev checkout only)
|
|
6733
|
+
|
|
6734
|
+
lib list List available libs
|
|
6735
|
+
lib search "<query>" [--limit N] Fuzzy-search functions across libs
|
|
6736
|
+
lib show <name> Lib + function summaries
|
|
6737
|
+
lib read <name> [fn\u2026] Full input/output schemas (+ .ts source path)
|
|
6738
|
+
exec "import {fn} from 'libs/<name>'; return await fn({\u2026})" Run a lib in the active tab
|
|
6739
|
+
|
|
5924
6740
|
TARGETING (only when NOT in a session, or picking a parallel tab)
|
|
5925
6741
|
|
|
5926
6742
|
--target <tabId> Exact id; a prefix of 8 chars is enough (preferred, parallel-safe)
|
|
@@ -5939,7 +6755,7 @@ PREREQ \u2014 a browser with CDP must be running:
|
|
|
5939
6755
|
Verify with: capture detect`);
|
|
5940
6756
|
}
|
|
5941
6757
|
}
|
|
5942
|
-
|
|
6758
|
+
main().catch((err) => {
|
|
5943
6759
|
console.error(err.message);
|
|
5944
6760
|
process.exit(1);
|
|
5945
6761
|
});
|