browser-pilot-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +231 -0
- package/dist/chunk-77SA2ZLI.js +18 -0
- package/dist/cli.js +1136 -0
- package/dist/daemon.js +516 -0
- package/dist/defuddle-bundle-B7XUXIMB.js +7 -0
- package/package.json +35 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { writeFileSync as writeFileSync3, readFileSync as readFileSync5, existsSync as existsSync6 } from "fs";
|
|
6
|
+
import { resolve as resolvePath } from "path";
|
|
7
|
+
|
|
8
|
+
// src/session.ts
|
|
9
|
+
import { spawn } from "child_process";
|
|
10
|
+
import { fileURLToPath } from "url";
|
|
11
|
+
import { existsSync as existsSync4 } from "fs";
|
|
12
|
+
|
|
13
|
+
// src/client.ts
|
|
14
|
+
import http from "http";
|
|
15
|
+
import { existsSync, readFileSync } from "fs";
|
|
16
|
+
|
|
17
|
+
// src/paths.ts
|
|
18
|
+
import { join } from "path";
|
|
19
|
+
import { homedir } from "os";
|
|
20
|
+
var STATE_DIR = join(homedir(), ".browser-pilot");
|
|
21
|
+
var STATE_FILE = join(STATE_DIR, "state.json");
|
|
22
|
+
var SOCKET_PATH = join(STATE_DIR, "daemon.sock");
|
|
23
|
+
var PID_FILE = join(STATE_DIR, "daemon.pid");
|
|
24
|
+
var REFS_FILE = join(STATE_DIR, "refs.json");
|
|
25
|
+
|
|
26
|
+
// src/client.ts
|
|
27
|
+
function isDaemonRunning() {
|
|
28
|
+
if (!existsSync(PID_FILE)) return false;
|
|
29
|
+
const pid = parseInt(readFileSync(PID_FILE, "utf-8").trim(), 10);
|
|
30
|
+
try {
|
|
31
|
+
process.kill(pid, 0);
|
|
32
|
+
return true;
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
var DaemonClient = class {
|
|
38
|
+
request(path, body) {
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
const req = http.request(
|
|
41
|
+
{
|
|
42
|
+
socketPath: SOCKET_PATH,
|
|
43
|
+
path,
|
|
44
|
+
method: body !== void 0 ? "POST" : "GET",
|
|
45
|
+
headers: body !== void 0 ? { "Content-Type": "application/json" } : {},
|
|
46
|
+
timeout: 6e4
|
|
47
|
+
},
|
|
48
|
+
(res) => {
|
|
49
|
+
let data = "";
|
|
50
|
+
res.on("data", (chunk) => {
|
|
51
|
+
data += chunk.toString();
|
|
52
|
+
});
|
|
53
|
+
res.on("end", () => {
|
|
54
|
+
try {
|
|
55
|
+
const parsed = JSON.parse(data);
|
|
56
|
+
if (parsed.error) reject(new Error(parsed.error));
|
|
57
|
+
else resolve(parsed.result ?? parsed);
|
|
58
|
+
} catch {
|
|
59
|
+
reject(new Error(`Invalid daemon response: ${data}`));
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
req.on("error", (err) => {
|
|
65
|
+
reject(new Error(`Cannot reach daemon: ${err.message}. Run 'bp connect' first.`));
|
|
66
|
+
});
|
|
67
|
+
req.on("timeout", () => {
|
|
68
|
+
req.destroy();
|
|
69
|
+
reject(new Error("Daemon request timeout"));
|
|
70
|
+
});
|
|
71
|
+
if (body !== void 0) req.write(JSON.stringify(body));
|
|
72
|
+
req.end();
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
async send(method, params, sessionId) {
|
|
76
|
+
return this.request("/cdp", { method, params, sessionId });
|
|
77
|
+
}
|
|
78
|
+
async health() {
|
|
79
|
+
try {
|
|
80
|
+
await this.request("/health");
|
|
81
|
+
return true;
|
|
82
|
+
} catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async healthInfo() {
|
|
87
|
+
try {
|
|
88
|
+
return await this.request("/health");
|
|
89
|
+
} catch {
|
|
90
|
+
return { ok: false };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async shutdown() {
|
|
94
|
+
try {
|
|
95
|
+
await this.request("/shutdown", {});
|
|
96
|
+
} catch {
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async dialogs() {
|
|
100
|
+
const res = await this.request("/dialogs");
|
|
101
|
+
return res.dialogs ?? [];
|
|
102
|
+
}
|
|
103
|
+
async discoveredTargets() {
|
|
104
|
+
const res = await this.request("/discovered");
|
|
105
|
+
return res.targets ?? [];
|
|
106
|
+
}
|
|
107
|
+
async setAuth(username, password) {
|
|
108
|
+
await this.request("/auth", { username, password });
|
|
109
|
+
}
|
|
110
|
+
async clearAuth() {
|
|
111
|
+
await this.request("/auth", { username: "", password: "" });
|
|
112
|
+
}
|
|
113
|
+
// ── Network methods ──────────────────────────────
|
|
114
|
+
async enableNetwork(sessionId) {
|
|
115
|
+
await this.request("/net/enable", { sessionId });
|
|
116
|
+
}
|
|
117
|
+
async netRequests(opts) {
|
|
118
|
+
const p = new URLSearchParams();
|
|
119
|
+
if (opts?.limit) p.set("limit", String(opts.limit));
|
|
120
|
+
if (opts?.url) p.set("url", opts.url);
|
|
121
|
+
if (opts?.method) p.set("method", opts.method);
|
|
122
|
+
if (opts?.status) p.set("status", opts.status);
|
|
123
|
+
if (opts?.type) p.set("type", opts.type);
|
|
124
|
+
if (opts?.after) p.set("after", String(opts.after));
|
|
125
|
+
const qs = p.toString();
|
|
126
|
+
return this.request(`/net/requests${qs ? "?" + qs : ""}`);
|
|
127
|
+
}
|
|
128
|
+
async netRequestDetail(id) {
|
|
129
|
+
return this.request(`/net/request/${id}`);
|
|
130
|
+
}
|
|
131
|
+
async netBody(id) {
|
|
132
|
+
return this.request(`/net/body/${id}`);
|
|
133
|
+
}
|
|
134
|
+
async netClear() {
|
|
135
|
+
await this.request("/net/clear", {});
|
|
136
|
+
}
|
|
137
|
+
async netAddRule(rule) {
|
|
138
|
+
return this.request("/net/rules", rule);
|
|
139
|
+
}
|
|
140
|
+
async netRules() {
|
|
141
|
+
return this.request("/net/rules");
|
|
142
|
+
}
|
|
143
|
+
async netRemoveRule(id) {
|
|
144
|
+
await this.request("/net/rules/remove", id !== void 0 ? { id } : { all: true });
|
|
145
|
+
}
|
|
146
|
+
close() {
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// src/chrome.ts
|
|
151
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
152
|
+
import { join as join2 } from "path";
|
|
153
|
+
import { homedir as homedir2, platform } from "os";
|
|
154
|
+
function getDataDirs() {
|
|
155
|
+
const home = homedir2();
|
|
156
|
+
const os = platform();
|
|
157
|
+
if (os === "darwin") {
|
|
158
|
+
const base = join2(home, "Library", "Application Support");
|
|
159
|
+
return [
|
|
160
|
+
{ name: "Chrome", path: join2(base, "Google", "Chrome") },
|
|
161
|
+
{ name: "Chrome Beta", path: join2(base, "Google", "Chrome Beta") },
|
|
162
|
+
{ name: "Chrome Canary", path: join2(base, "Google", "Chrome Canary") },
|
|
163
|
+
{ name: "Brave", path: join2(base, "BraveSoftware", "Brave-Browser") },
|
|
164
|
+
{ name: "Edge", path: join2(base, "Microsoft Edge") },
|
|
165
|
+
{ name: "Chromium", path: join2(base, "Chromium") }
|
|
166
|
+
];
|
|
167
|
+
}
|
|
168
|
+
if (os === "linux") {
|
|
169
|
+
return [
|
|
170
|
+
{ name: "Chrome", path: join2(home, ".config", "google-chrome") },
|
|
171
|
+
{ name: "Chromium", path: join2(home, ".config", "chromium") },
|
|
172
|
+
{ name: "Brave", path: join2(home, ".config", "BraveSoftware", "Brave-Browser") },
|
|
173
|
+
{ name: "Edge", path: join2(home, ".config", "microsoft-edge") }
|
|
174
|
+
];
|
|
175
|
+
}
|
|
176
|
+
if (os === "win32") {
|
|
177
|
+
const appData = process.env.LOCALAPPDATA ?? join2(home, "AppData", "Local");
|
|
178
|
+
return [
|
|
179
|
+
{ name: "Chrome", path: join2(appData, "Google", "Chrome", "User Data") },
|
|
180
|
+
{ name: "Brave", path: join2(appData, "BraveSoftware", "Brave-Browser", "User Data") },
|
|
181
|
+
{ name: "Edge", path: join2(appData, "Microsoft", "Edge", "User Data") }
|
|
182
|
+
];
|
|
183
|
+
}
|
|
184
|
+
return [];
|
|
185
|
+
}
|
|
186
|
+
function discoverChrome(browserFilter) {
|
|
187
|
+
for (const { name, path: dataDir } of getDataDirs()) {
|
|
188
|
+
if (browserFilter && !name.toLowerCase().includes(browserFilter.toLowerCase())) continue;
|
|
189
|
+
const portFile = join2(dataDir, "DevToolsActivePort");
|
|
190
|
+
if (!existsSync2(portFile)) continue;
|
|
191
|
+
try {
|
|
192
|
+
const lines = readFileSync2(portFile, "utf-8").trim().split("\n");
|
|
193
|
+
if (lines.length < 2) continue;
|
|
194
|
+
const port = parseInt(lines[0], 10);
|
|
195
|
+
const wsPath = lines[1];
|
|
196
|
+
if (isNaN(port) || !wsPath) continue;
|
|
197
|
+
return {
|
|
198
|
+
port,
|
|
199
|
+
wsPath,
|
|
200
|
+
wsUrl: `ws://127.0.0.1:${port}${wsPath}`,
|
|
201
|
+
browser: name,
|
|
202
|
+
dataDir
|
|
203
|
+
};
|
|
204
|
+
} catch {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// src/state.ts
|
|
212
|
+
import { readFileSync as readFileSync3, writeFileSync, existsSync as existsSync3, mkdirSync, unlinkSync } from "fs";
|
|
213
|
+
function loadState() {
|
|
214
|
+
if (!existsSync3(STATE_FILE)) return null;
|
|
215
|
+
try {
|
|
216
|
+
return JSON.parse(readFileSync3(STATE_FILE, "utf-8"));
|
|
217
|
+
} catch {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function saveState(state) {
|
|
222
|
+
if (!existsSync3(STATE_DIR)) mkdirSync(STATE_DIR, { recursive: true, mode: 448 });
|
|
223
|
+
writeFileSync(STATE_FILE, JSON.stringify(state, null, 2), { mode: 384 });
|
|
224
|
+
}
|
|
225
|
+
function clearState() {
|
|
226
|
+
try {
|
|
227
|
+
unlinkSync(STATE_FILE);
|
|
228
|
+
} catch {
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/page-scripts.ts
|
|
233
|
+
var GET_CLICK_COORDS = `function() {
|
|
234
|
+
this.scrollIntoView({block:'center',inline:'center'});
|
|
235
|
+
const r = this.getBoundingClientRect();
|
|
236
|
+
return JSON.stringify({x: r.x + r.width/2, y: r.y + r.height/2});
|
|
237
|
+
}`;
|
|
238
|
+
var SET_VALUE = `function(text, clear) {
|
|
239
|
+
this.focus();
|
|
240
|
+
const proto = this instanceof HTMLTextAreaElement
|
|
241
|
+
? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
|
|
242
|
+
const setter = Object.getOwnPropertyDescriptor(proto, 'value')?.set;
|
|
243
|
+
const val = clear ? text : this.value + text;
|
|
244
|
+
if (setter) setter.call(this, val); else this.value = val;
|
|
245
|
+
this.dispatchEvent(new Event('input', {bubbles:true}));
|
|
246
|
+
this.dispatchEvent(new Event('change', {bubbles:true}));
|
|
247
|
+
}`;
|
|
248
|
+
var PAGE_INFO = `JSON.stringify({title:document.title,url:location.href})`;
|
|
249
|
+
var PAGE_DIMENSIONS = `JSON.stringify({
|
|
250
|
+
width: Math.max(document.documentElement.scrollWidth, document.documentElement.clientWidth),
|
|
251
|
+
height: Math.max(document.documentElement.scrollHeight, document.documentElement.clientHeight)
|
|
252
|
+
})`;
|
|
253
|
+
var INJECT_BORDER = `(() => {
|
|
254
|
+
if (document.getElementById('__bp_overlay')) return;
|
|
255
|
+
const d = document.createElement('div');
|
|
256
|
+
d.id = '__bp_overlay';
|
|
257
|
+
d.setAttribute('aria-hidden','true');
|
|
258
|
+
d.setAttribute('role','presentation');
|
|
259
|
+
Object.assign(d.style, {position:'fixed',inset:'0',zIndex:'2147483647',pointerEvents:'none'});
|
|
260
|
+
document.documentElement.appendChild(d);
|
|
261
|
+
try{d.animate([
|
|
262
|
+
{boxShadow:'inset 0 0 20px rgba(59,130,246,.8),inset 0 0 40px rgba(59,130,246,.4),inset 0 0 80px rgba(59,130,246,.15)'},
|
|
263
|
+
{boxShadow:'inset 0 0 30px rgba(59,130,246,1),inset 0 0 60px rgba(59,130,246,.5),inset 0 0 100px rgba(59,130,246,.2)'},
|
|
264
|
+
{boxShadow:'inset 0 0 20px rgba(59,130,246,.8),inset 0 0 40px rgba(59,130,246,.4),inset 0 0 80px rgba(59,130,246,.15)'},
|
|
265
|
+
],{duration:2500,iterations:Infinity,easing:'ease-in-out'})}catch(e){}
|
|
266
|
+
})()`;
|
|
267
|
+
function elementRect(selector) {
|
|
268
|
+
return `JSON.stringify((() => {
|
|
269
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
270
|
+
if (!el) return null;
|
|
271
|
+
const r = el.getBoundingClientRect();
|
|
272
|
+
return {x:r.x, y:r.y, width:r.width, height:r.height};
|
|
273
|
+
})())`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// src/session.ts
|
|
277
|
+
async function startDaemon(wsUrl) {
|
|
278
|
+
const script = fileURLToPath(new URL("daemon.js", import.meta.url));
|
|
279
|
+
const child = spawn(process.execPath, [script, wsUrl], { detached: true, stdio: "ignore" });
|
|
280
|
+
child.unref();
|
|
281
|
+
const client = new DaemonClient();
|
|
282
|
+
const deadline = Date.now() + 6e4;
|
|
283
|
+
while (Date.now() < deadline) {
|
|
284
|
+
if (await client.health()) return client;
|
|
285
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
286
|
+
}
|
|
287
|
+
throw new Error(`Connection timeout. Make sure to click "Allow" in Chrome's authorization dialog.`);
|
|
288
|
+
}
|
|
289
|
+
async function getDaemon(wsUrl) {
|
|
290
|
+
if (isDaemonRunning()) {
|
|
291
|
+
const client = new DaemonClient();
|
|
292
|
+
const info = await client.healthInfo();
|
|
293
|
+
if (info.ok) {
|
|
294
|
+
if (info.wsUrl === wsUrl) return client;
|
|
295
|
+
await client.shutdown();
|
|
296
|
+
const deadline = Date.now() + 5e3;
|
|
297
|
+
while (Date.now() < deadline && existsSync4(SOCKET_PATH)) {
|
|
298
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return startDaemon(wsUrl);
|
|
303
|
+
}
|
|
304
|
+
async function verifyPilotTargets(client, state) {
|
|
305
|
+
const { targetInfos } = await client.send("Target.getTargets");
|
|
306
|
+
const existing = new Set(targetInfos.map((t) => t.targetId));
|
|
307
|
+
state.pilotTargetIds = state.pilotTargetIds.filter((id) => existing.has(id));
|
|
308
|
+
if (state.pilotTargetIds.length === 0) return false;
|
|
309
|
+
if (!state.pilotTargetIds.includes(state.activeTargetId)) {
|
|
310
|
+
state.activeTargetId = state.pilotTargetIds[0];
|
|
311
|
+
state.activeSessionId = void 0;
|
|
312
|
+
}
|
|
313
|
+
saveState(state);
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
async function initSession(transport, sessionId) {
|
|
317
|
+
await transport.send("Page.enable", {}, sessionId).catch(() => {
|
|
318
|
+
});
|
|
319
|
+
await transport.send("Runtime.evaluate", { expression: INJECT_BORDER }, sessionId).catch(() => {
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
async function ensureSession(client, state) {
|
|
323
|
+
if (state.activeSessionId) {
|
|
324
|
+
try {
|
|
325
|
+
await client.send("Runtime.evaluate", { expression: "1" }, state.activeSessionId);
|
|
326
|
+
return state.activeSessionId;
|
|
327
|
+
} catch {
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const { sessionId } = await client.send("Target.attachToTarget", {
|
|
331
|
+
targetId: state.activeTargetId,
|
|
332
|
+
flatten: true
|
|
333
|
+
});
|
|
334
|
+
await initSession(client, sessionId);
|
|
335
|
+
state.activeSessionId = sessionId;
|
|
336
|
+
saveState(state);
|
|
337
|
+
return sessionId;
|
|
338
|
+
}
|
|
339
|
+
async function connectFresh(browserFilter) {
|
|
340
|
+
const chrome = discoverChrome(browserFilter);
|
|
341
|
+
if (!chrome) {
|
|
342
|
+
throw new Error(
|
|
343
|
+
"Cannot find Chrome DevTools port.\nOpen chrome://inspect/#remote-debugging in Chrome and toggle ON."
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
const client = await getDaemon(chrome.wsUrl);
|
|
347
|
+
const { targetId } = await client.send("Target.createTarget", {
|
|
348
|
+
url: "about:blank",
|
|
349
|
+
newWindow: true
|
|
350
|
+
});
|
|
351
|
+
const { sessionId } = await client.send("Target.attachToTarget", {
|
|
352
|
+
targetId,
|
|
353
|
+
flatten: true
|
|
354
|
+
});
|
|
355
|
+
await initSession(client, sessionId);
|
|
356
|
+
const state = {
|
|
357
|
+
wsEndpoint: chrome.wsUrl,
|
|
358
|
+
browser: chrome.browser,
|
|
359
|
+
pilotTargetIds: [targetId],
|
|
360
|
+
activeTargetId: targetId,
|
|
361
|
+
activeSessionId: sessionId
|
|
362
|
+
};
|
|
363
|
+
saveState(state);
|
|
364
|
+
return { client, state };
|
|
365
|
+
}
|
|
366
|
+
async function resumeExisting() {
|
|
367
|
+
const state = loadState();
|
|
368
|
+
if (!state) return null;
|
|
369
|
+
if (!isDaemonRunning()) return null;
|
|
370
|
+
const client = new DaemonClient();
|
|
371
|
+
if (!await client.health()) return null;
|
|
372
|
+
const valid = await verifyPilotTargets(client, state);
|
|
373
|
+
if (!valid) return null;
|
|
374
|
+
return { client, state };
|
|
375
|
+
}
|
|
376
|
+
async function resume(browserFilter) {
|
|
377
|
+
const existing = await resumeExisting();
|
|
378
|
+
if (existing) return existing;
|
|
379
|
+
return connectFresh(browserFilter);
|
|
380
|
+
}
|
|
381
|
+
async function withPilot(fn) {
|
|
382
|
+
const { client, state } = await resume();
|
|
383
|
+
const sessionId = await ensureSession(client, state);
|
|
384
|
+
await fn({ transport: client, state, sessionId });
|
|
385
|
+
}
|
|
386
|
+
async function disconnect() {
|
|
387
|
+
if (isDaemonRunning()) {
|
|
388
|
+
const client = new DaemonClient();
|
|
389
|
+
try {
|
|
390
|
+
await client.shutdown();
|
|
391
|
+
} catch {
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
clearState();
|
|
395
|
+
}
|
|
396
|
+
async function waitForLoad(transport, sessionId, timeout = 3e4) {
|
|
397
|
+
const start = Date.now();
|
|
398
|
+
while (Date.now() - start < timeout) {
|
|
399
|
+
try {
|
|
400
|
+
const { result } = await transport.send("Runtime.evaluate", {
|
|
401
|
+
expression: "document.readyState"
|
|
402
|
+
}, sessionId);
|
|
403
|
+
if (result.value === "complete") {
|
|
404
|
+
await transport.send("Runtime.evaluate", { expression: INJECT_BORDER }, sessionId).catch(() => {
|
|
405
|
+
});
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
} catch {
|
|
409
|
+
}
|
|
410
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
411
|
+
}
|
|
412
|
+
throw new Error("Page load timeout");
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// src/snapshot.ts
|
|
416
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, existsSync as existsSync5 } from "fs";
|
|
417
|
+
var INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
|
|
418
|
+
"button",
|
|
419
|
+
"link",
|
|
420
|
+
"textbox",
|
|
421
|
+
"searchbox",
|
|
422
|
+
"combobox",
|
|
423
|
+
"listbox",
|
|
424
|
+
"checkbox",
|
|
425
|
+
"radio",
|
|
426
|
+
"spinbutton",
|
|
427
|
+
"slider",
|
|
428
|
+
"switch",
|
|
429
|
+
"menuitem",
|
|
430
|
+
"menuitemcheckbox",
|
|
431
|
+
"menuitemradio",
|
|
432
|
+
"tab"
|
|
433
|
+
]);
|
|
434
|
+
function saveRefs(targetId, entries) {
|
|
435
|
+
writeFileSync2(REFS_FILE, JSON.stringify({ targetId, entries }), { mode: 384 });
|
|
436
|
+
}
|
|
437
|
+
function loadRefs(expectedTargetId) {
|
|
438
|
+
if (!existsSync5(REFS_FILE)) return [];
|
|
439
|
+
try {
|
|
440
|
+
const stored = JSON.parse(readFileSync4(REFS_FILE, "utf-8"));
|
|
441
|
+
if (expectedTargetId && stored.targetId !== expectedTargetId) return [];
|
|
442
|
+
return stored.entries;
|
|
443
|
+
} catch {
|
|
444
|
+
return [];
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
async function takeSnapshot(transport, sessionId, targetId, limit = 50) {
|
|
448
|
+
const { result: info } = await transport.send("Runtime.evaluate", {
|
|
449
|
+
expression: PAGE_INFO,
|
|
450
|
+
returnByValue: true
|
|
451
|
+
}, sessionId);
|
|
452
|
+
const { title, url } = JSON.parse(info.value);
|
|
453
|
+
const { nodes } = await transport.send("Accessibility.getFullAXTree", {}, sessionId);
|
|
454
|
+
const map = /* @__PURE__ */ new Map();
|
|
455
|
+
for (const n of nodes) map.set(n.nodeId, { ...n, children: [] });
|
|
456
|
+
let root = null;
|
|
457
|
+
for (const [, node] of map) {
|
|
458
|
+
if (node.childIds) {
|
|
459
|
+
node.children = node.childIds.map((id) => map.get(id)).filter(Boolean);
|
|
460
|
+
}
|
|
461
|
+
if (!node.parentId) root = node;
|
|
462
|
+
}
|
|
463
|
+
const refs = [];
|
|
464
|
+
const elements = [];
|
|
465
|
+
function walk(node) {
|
|
466
|
+
if (!node) return;
|
|
467
|
+
if (!node.ignored) {
|
|
468
|
+
const role = node.role?.value;
|
|
469
|
+
if (role && INTERACTIVE_ROLES.has(role) && node.backendDOMNodeId !== void 0) {
|
|
470
|
+
const props = Object.fromEntries(
|
|
471
|
+
(node.properties || []).map((p) => [p.name, p.value?.value])
|
|
472
|
+
);
|
|
473
|
+
const name = node.name?.value || "";
|
|
474
|
+
const value = node.value?.value;
|
|
475
|
+
if (!props.disabled && (name || value) && refs.length < limit) {
|
|
476
|
+
const checked = props.checked === "true" || props.checked === true ? true : void 0;
|
|
477
|
+
refs.push({ backendNodeId: node.backendDOMNodeId, role, name });
|
|
478
|
+
elements.push({ ref: refs.length, backendNodeId: node.backendDOMNodeId, role, name, value, checked });
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
for (const child of node.children) walk(child);
|
|
483
|
+
}
|
|
484
|
+
if (root) walk(root);
|
|
485
|
+
saveRefs(targetId, refs);
|
|
486
|
+
const lines = [`[page] ${title} | ${url}`, ""];
|
|
487
|
+
if (elements.length === 0) {
|
|
488
|
+
lines.push("(no interactive elements)");
|
|
489
|
+
} else {
|
|
490
|
+
for (const el of elements) {
|
|
491
|
+
let line = `[${el.ref}] ${el.role} "${el.name}"`;
|
|
492
|
+
if (el.value !== void 0 && el.value !== "") line += ` value="${el.value}"`;
|
|
493
|
+
if (el.checked) line += " checked";
|
|
494
|
+
lines.push(line);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return { text: lines.join("\n"), data: { title, url, elements } };
|
|
498
|
+
}
|
|
499
|
+
function isRef(target) {
|
|
500
|
+
return /^\d+$/.test(target);
|
|
501
|
+
}
|
|
502
|
+
async function resolveTarget(transport, sessionId, target, targetId) {
|
|
503
|
+
if (isRef(target)) {
|
|
504
|
+
const refs = loadRefs(targetId);
|
|
505
|
+
const ref = parseInt(target, 10);
|
|
506
|
+
if (ref < 1 || ref > refs.length) {
|
|
507
|
+
throw new Error(`Ref [${ref}] not found. Run 'bp snapshot' to refresh.`);
|
|
508
|
+
}
|
|
509
|
+
const { object } = await transport.send("DOM.resolveNode", {
|
|
510
|
+
backendNodeId: refs[ref - 1].backendNodeId
|
|
511
|
+
}, sessionId);
|
|
512
|
+
return object.objectId;
|
|
513
|
+
}
|
|
514
|
+
const { result } = await transport.send("Runtime.evaluate", {
|
|
515
|
+
expression: `document.querySelector(${JSON.stringify(target)})`
|
|
516
|
+
}, sessionId);
|
|
517
|
+
if (!result.objectId) throw new Error(`Element not found: ${target}`);
|
|
518
|
+
return result.objectId;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// src/cli.ts
|
|
522
|
+
var program = new Command();
|
|
523
|
+
program.name("bp").description("Control your browser from the command line").version("0.1.0").option("--human", "force human-readable output (default when TTY)").addHelpText("after", `
|
|
524
|
+
Workflow:
|
|
525
|
+
bp connect # one-time setup (click Allow in Chrome)
|
|
526
|
+
bp open <url> # navigate \u2014 returns snapshot with [ref] numbers
|
|
527
|
+
bp click <ref> # interact \u2014 returns updated snapshot
|
|
528
|
+
bp type <ref> <text> # input text \u2014 returns updated snapshot
|
|
529
|
+
bp press <key> # keyboard \u2014 returns updated snapshot
|
|
530
|
+
bp eval <js> # run JavaScript (escape hatch for anything)
|
|
531
|
+
|
|
532
|
+
Refs:
|
|
533
|
+
open/click/type/press return numbered interactive elements like:
|
|
534
|
+
[1] link "Home" [2] textbox "Search" [3] button "Submit"
|
|
535
|
+
Use the number in subsequent commands: bp click 1, bp type 2 "hello"
|
|
536
|
+
|
|
537
|
+
Output:
|
|
538
|
+
JSON by default when piped (for LLM/script use).
|
|
539
|
+
Human-readable when run in a terminal (TTY). Force with --human.
|
|
540
|
+
Actions return: {"ok":true, "title":"...", "url":"...", "elements":[...]}
|
|
541
|
+
Errors return: {"ok":false, "error":"...", "hint":"..."}
|
|
542
|
+
|
|
543
|
+
Edge cases:
|
|
544
|
+
bp upload <ref> <filepath> # file input upload
|
|
545
|
+
bp auth <user> <pass> # HTTP Basic Auth
|
|
546
|
+
bp frame # list iframes
|
|
547
|
+
bp frame 1 # eval in iframe context
|
|
548
|
+
bp frame 0 # back to top frame
|
|
549
|
+
Dialogs (alert/confirm) are auto-handled by the daemon.
|
|
550
|
+
|
|
551
|
+
Eval (replaces scroll, back, forward, extract, etc.):
|
|
552
|
+
bp eval "history.back()" # go back
|
|
553
|
+
bp eval "history.forward()" # go forward
|
|
554
|
+
bp eval "location.reload()" # reload
|
|
555
|
+
bp eval "window.scrollBy(0, 500)" # scroll down
|
|
556
|
+
bp eval "document.querySelector('h1').textContent" # extract text
|
|
557
|
+
bp eval "document.querySelector('div').innerHTML" # extract HTML
|
|
558
|
+
bp eval "JSON.stringify(localStorage)" # read storage
|
|
559
|
+
echo 'complex js here' | bp eval # stdin for complex JS
|
|
560
|
+
`);
|
|
561
|
+
function useJson() {
|
|
562
|
+
if (program.opts().human) return false;
|
|
563
|
+
return !process.stdout.isTTY;
|
|
564
|
+
}
|
|
565
|
+
function emit(data, human) {
|
|
566
|
+
if (useJson()) console.log(JSON.stringify(data));
|
|
567
|
+
else if (human) console.log(human);
|
|
568
|
+
}
|
|
569
|
+
function fail(error, hint) {
|
|
570
|
+
if (useJson()) console.log(JSON.stringify({ ok: false, error, ...hint ? { hint } : {} }));
|
|
571
|
+
else console.error(`\u2717 ${error}${hint ? `
|
|
572
|
+
hint: ${hint}` : ""}`);
|
|
573
|
+
process.exit(1);
|
|
574
|
+
}
|
|
575
|
+
function emitSnapshot(result) {
|
|
576
|
+
if (useJson()) console.log(JSON.stringify({ ok: true, ...result.data }));
|
|
577
|
+
else console.log(result.text);
|
|
578
|
+
}
|
|
579
|
+
function action(fn) {
|
|
580
|
+
return (...args) => fn(...args).catch((err) => {
|
|
581
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
582
|
+
if (msg.includes("not found") && msg.includes("Ref")) fail(msg, "Run 'bp snapshot' to refresh element refs.");
|
|
583
|
+
if (msg.includes("Not connected")) fail(msg, "Run 'bp connect' first.");
|
|
584
|
+
if (msg.includes("Page load timeout")) fail(msg, "Page may still be loading. Retry the command after a moment.");
|
|
585
|
+
fail(msg);
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
function normalizeUrl(url) {
|
|
589
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(url)) return url;
|
|
590
|
+
return `https://${url}`;
|
|
591
|
+
}
|
|
592
|
+
function parseLimit(raw) {
|
|
593
|
+
const n = parseInt(raw, 10);
|
|
594
|
+
if (isNaN(n) || n < 1) throw new Error("--limit must be a positive number");
|
|
595
|
+
return n;
|
|
596
|
+
}
|
|
597
|
+
function readStdin() {
|
|
598
|
+
if (process.stdin.isTTY) return Promise.resolve("");
|
|
599
|
+
return new Promise((resolve) => {
|
|
600
|
+
let d = "";
|
|
601
|
+
process.stdin.on("data", (c) => {
|
|
602
|
+
d += c;
|
|
603
|
+
});
|
|
604
|
+
process.stdin.on("end", () => resolve(d.trim()));
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
async function snap(t, sid, tid, limit) {
|
|
608
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
609
|
+
await waitForLoad(t, sid, 1e4);
|
|
610
|
+
return takeSnapshot(t, sid, tid, limit);
|
|
611
|
+
}
|
|
612
|
+
async function dispatchClick(t, sid, x, y) {
|
|
613
|
+
await t.send("Input.dispatchMouseEvent", { type: "mouseMoved", x, y, button: "none" }, sid);
|
|
614
|
+
await t.send("Input.dispatchMouseEvent", { type: "mousePressed", x, y, button: "left", clickCount: 1 }, sid);
|
|
615
|
+
await t.send("Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button: "left", clickCount: 1 }, sid);
|
|
616
|
+
}
|
|
617
|
+
var KEY_DEFS = {
|
|
618
|
+
enter: { key: "Enter", code: "Enter", keyCode: 13 },
|
|
619
|
+
tab: { key: "Tab", code: "Tab", keyCode: 9 },
|
|
620
|
+
escape: { key: "Escape", code: "Escape", keyCode: 27 },
|
|
621
|
+
esc: { key: "Escape", code: "Escape", keyCode: 27 },
|
|
622
|
+
backspace: { key: "Backspace", code: "Backspace", keyCode: 8 },
|
|
623
|
+
delete: { key: "Delete", code: "Delete", keyCode: 46 },
|
|
624
|
+
space: { key: " ", code: "Space", keyCode: 32 },
|
|
625
|
+
arrowup: { key: "ArrowUp", code: "ArrowUp", keyCode: 38 },
|
|
626
|
+
arrowdown: { key: "ArrowDown", code: "ArrowDown", keyCode: 40 },
|
|
627
|
+
arrowleft: { key: "ArrowLeft", code: "ArrowLeft", keyCode: 37 },
|
|
628
|
+
arrowright: { key: "ArrowRight", code: "ArrowRight", keyCode: 39 },
|
|
629
|
+
home: { key: "Home", code: "Home", keyCode: 36 },
|
|
630
|
+
end: { key: "End", code: "End", keyCode: 35 },
|
|
631
|
+
pageup: { key: "PageUp", code: "PageUp", keyCode: 33 },
|
|
632
|
+
pagedown: { key: "PageDown", code: "PageDown", keyCode: 34 }
|
|
633
|
+
};
|
|
634
|
+
var MOD_DEFS = {
|
|
635
|
+
control: { key: "Control", code: "ControlLeft", keyCode: 17, mask: 2 },
|
|
636
|
+
ctrl: { key: "Control", code: "ControlLeft", keyCode: 17, mask: 2 },
|
|
637
|
+
shift: { key: "Shift", code: "ShiftLeft", keyCode: 16, mask: 8 },
|
|
638
|
+
alt: { key: "Alt", code: "AltLeft", keyCode: 18, mask: 1 },
|
|
639
|
+
meta: { key: "Meta", code: "MetaLeft", keyCode: 91, mask: 4 },
|
|
640
|
+
cmd: { key: "Meta", code: "MetaLeft", keyCode: 91, mask: 4 },
|
|
641
|
+
command: { key: "Meta", code: "MetaLeft", keyCode: 91, mask: 4 }
|
|
642
|
+
};
|
|
643
|
+
async function dispatchKey(t, sid, combo) {
|
|
644
|
+
const parts = combo.split("+");
|
|
645
|
+
const mainKey = parts.pop();
|
|
646
|
+
const mods = parts.map((p) => {
|
|
647
|
+
const m = MOD_DEFS[p.toLowerCase()];
|
|
648
|
+
if (!m) throw new Error(`Unknown modifier: ${p}`);
|
|
649
|
+
return m;
|
|
650
|
+
});
|
|
651
|
+
const modifierFlags = mods.reduce((n, m) => n | m.mask, 0);
|
|
652
|
+
const kd = KEY_DEFS[mainKey.toLowerCase()];
|
|
653
|
+
const key = kd?.key ?? mainKey;
|
|
654
|
+
const code = kd?.code ?? (mainKey.length === 1 ? `Key${mainKey.toUpperCase()}` : mainKey);
|
|
655
|
+
const keyCode = kd?.keyCode ?? mainKey.toUpperCase().charCodeAt(0);
|
|
656
|
+
for (const m of mods) {
|
|
657
|
+
await t.send("Input.dispatchKeyEvent", { type: "keyDown", key: m.key, code: m.code, windowsVirtualKeyCode: m.keyCode, modifiers: modifierFlags }, sid);
|
|
658
|
+
}
|
|
659
|
+
const text = key === "Enter" ? "\r" : kd ? "" : mainKey;
|
|
660
|
+
await t.send("Input.dispatchKeyEvent", { type: "keyDown", key, code, windowsVirtualKeyCode: keyCode, text, modifiers: modifierFlags }, sid);
|
|
661
|
+
await t.send("Input.dispatchKeyEvent", { type: "keyUp", key, code, windowsVirtualKeyCode: keyCode, modifiers: modifierFlags }, sid);
|
|
662
|
+
for (const m of mods.reverse()) {
|
|
663
|
+
await t.send("Input.dispatchKeyEvent", { type: "keyUp", key: m.key, code: m.code, windowsVirtualKeyCode: m.keyCode }, sid);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
program.command("connect").description("Connect to Chrome and create pilot window").option("-b, --browser <name>", "browser to connect to").addHelpText("after", "\nExamples:\n bp connect\n bp connect --browser brave").action(action(async (opts) => {
|
|
667
|
+
if (!useJson()) {
|
|
668
|
+
console.log("Connecting to Chrome...");
|
|
669
|
+
console.log(`If prompted, click "Allow" in Chrome's authorization dialog.
|
|
670
|
+
`);
|
|
671
|
+
}
|
|
672
|
+
const { state } = await connectFresh(opts.browser);
|
|
673
|
+
emit(
|
|
674
|
+
{ ok: true, browser: state.browser },
|
|
675
|
+
`\u2713 Connected to ${state.browser}
|
|
676
|
+
\u2713 Pilot window created (daemon running in background)
|
|
677
|
+
|
|
678
|
+
Ready! Try: bp open https://example.com`
|
|
679
|
+
);
|
|
680
|
+
}));
|
|
681
|
+
program.command("disconnect").description("Close pilot window and stop daemon").action(action(async () => {
|
|
682
|
+
const existing = await resumeExisting();
|
|
683
|
+
if (existing) {
|
|
684
|
+
for (const id of existing.state.pilotTargetIds) {
|
|
685
|
+
try {
|
|
686
|
+
await existing.client.send("Target.closeTarget", { targetId: id });
|
|
687
|
+
} catch {
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
await disconnect();
|
|
692
|
+
emit({ ok: true }, "\u2713 Disconnected");
|
|
693
|
+
}));
|
|
694
|
+
program.command("open <url>").description("Navigate to URL and return page snapshot").option("-n, --new", "open in new tab").option("-l, --limit <n>", "max elements in snapshot", "50").addHelpText("after", "\nExamples:\n bp open https://github.com\n bp open github.com --new\n bp open https://example.com --limit 20").action(action(async (url, opts) => {
|
|
695
|
+
url = normalizeUrl(url);
|
|
696
|
+
const limit = parseLimit(opts.limit);
|
|
697
|
+
await withPilot(async ({ transport, state, sessionId }) => {
|
|
698
|
+
let sid = sessionId;
|
|
699
|
+
let tid = state.activeTargetId;
|
|
700
|
+
if (opts.new) {
|
|
701
|
+
const { targetId } = await transport.send("Target.createTarget", { url });
|
|
702
|
+
const r = await transport.send("Target.attachToTarget", { targetId, flatten: true });
|
|
703
|
+
await initSession(transport, r.sessionId);
|
|
704
|
+
state.pilotTargetIds.push(targetId);
|
|
705
|
+
state.activeTargetId = targetId;
|
|
706
|
+
state.activeSessionId = r.sessionId;
|
|
707
|
+
state.frameContextId = void 0;
|
|
708
|
+
saveState(state);
|
|
709
|
+
sid = r.sessionId;
|
|
710
|
+
tid = targetId;
|
|
711
|
+
} else {
|
|
712
|
+
await transport.send("Page.navigate", { url }, sid);
|
|
713
|
+
state.frameContextId = void 0;
|
|
714
|
+
saveState(state);
|
|
715
|
+
}
|
|
716
|
+
await waitForLoad(transport, sid);
|
|
717
|
+
emitSnapshot(await takeSnapshot(transport, sid, tid, limit));
|
|
718
|
+
});
|
|
719
|
+
}));
|
|
720
|
+
program.command("snapshot").description("Get interactive elements on the page").option("-l, --limit <n>", "max elements to return", "50").addHelpText("after", "\nExamples:\n bp snapshot\n bp snapshot --limit 100").action(action(async (opts) => {
|
|
721
|
+
const limit = parseLimit(opts.limit);
|
|
722
|
+
await withPilot(async ({ transport, sessionId, state }) => {
|
|
723
|
+
emitSnapshot(await takeSnapshot(transport, sessionId, state.activeTargetId, limit));
|
|
724
|
+
});
|
|
725
|
+
}));
|
|
726
|
+
program.command("click <ref>").description("Click element by ref number and return page snapshot").option("-l, --limit <n>", "max elements in snapshot", "50").addHelpText("after", "\nRef is a number from the snapshot output.\n\nExamples:\n bp click 3\n bp click 3 --limit 10").action(action(async (ref, opts) => {
|
|
727
|
+
const limit = parseLimit(opts.limit);
|
|
728
|
+
await withPilot(async ({ transport, sessionId, state }) => {
|
|
729
|
+
const objectId = await resolveTarget(transport, sessionId, ref, state.activeTargetId);
|
|
730
|
+
try {
|
|
731
|
+
const { result } = await transport.send("Runtime.callFunctionOn", {
|
|
732
|
+
objectId,
|
|
733
|
+
functionDeclaration: GET_CLICK_COORDS,
|
|
734
|
+
returnByValue: true
|
|
735
|
+
}, sessionId);
|
|
736
|
+
const { x, y } = JSON.parse(result.value);
|
|
737
|
+
await dispatchClick(transport, sessionId, x, y);
|
|
738
|
+
} finally {
|
|
739
|
+
await transport.send("Runtime.releaseObject", { objectId }, sessionId).catch(() => {
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
emitSnapshot(await snap(transport, sessionId, state.activeTargetId, limit));
|
|
743
|
+
});
|
|
744
|
+
}));
|
|
745
|
+
program.command("type <ref> <text>").description("Type text into element and return page snapshot").option("-c, --clear", "clear field before typing").option("-s, --submit", "press Enter after typing").option("-l, --limit <n>", "max elements in snapshot", "50").addHelpText("after", '\nExamples:\n bp type 2 "hello world"\n bp type 5 "query" --submit\n bp type 3 "new value" --clear').action(action(async (ref, text, opts) => {
|
|
746
|
+
const limit = parseLimit(opts.limit);
|
|
747
|
+
await withPilot(async ({ transport, sessionId, state }) => {
|
|
748
|
+
const objectId = await resolveTarget(transport, sessionId, ref, state.activeTargetId);
|
|
749
|
+
try {
|
|
750
|
+
await transport.send("Runtime.callFunctionOn", {
|
|
751
|
+
objectId,
|
|
752
|
+
functionDeclaration: SET_VALUE,
|
|
753
|
+
arguments: [{ value: text }, { value: !!opts.clear }]
|
|
754
|
+
}, sessionId);
|
|
755
|
+
if (opts.submit) await dispatchKey(transport, sessionId, "Enter");
|
|
756
|
+
} finally {
|
|
757
|
+
await transport.send("Runtime.releaseObject", { objectId }, sessionId).catch(() => {
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
emitSnapshot(await snap(transport, sessionId, state.activeTargetId, limit));
|
|
761
|
+
});
|
|
762
|
+
}));
|
|
763
|
+
program.command("press <key>").description("Press key combo (e.g. Enter, Escape, Control+a) and return snapshot").option("-l, --limit <n>", "max elements in snapshot", "50").addHelpText("after", "\nKeys: Enter, Escape, Tab, Space, Backspace, Delete,\n ArrowUp, ArrowDown, ArrowLeft, ArrowRight,\n Home, End, PageUp, PageDown\nModifiers: Control (Ctrl), Shift, Alt, Meta (Cmd)\n\nExamples:\n bp press Enter\n bp press Escape\n bp press Control+a\n bp press Meta+c").action(action(async (key, opts) => {
|
|
764
|
+
const limit = parseLimit(opts.limit);
|
|
765
|
+
await withPilot(async ({ transport, sessionId, state }) => {
|
|
766
|
+
await dispatchKey(transport, sessionId, key);
|
|
767
|
+
emitSnapshot(await snap(transport, sessionId, state.activeTargetId, limit));
|
|
768
|
+
});
|
|
769
|
+
}));
|
|
770
|
+
program.command("eval [expression]").description("Execute JavaScript (pass via argument or stdin)").addHelpText("after", `
|
|
771
|
+
This is the escape hatch \u2014 anything JS can do, eval can do.
|
|
772
|
+
|
|
773
|
+
Examples:
|
|
774
|
+
bp eval "document.title"
|
|
775
|
+
bp eval "history.back()"
|
|
776
|
+
bp eval "window.scrollBy(0, 500)"
|
|
777
|
+
bp eval "document.querySelector('h1').textContent"
|
|
778
|
+
echo 'complex js' | bp eval`).action(action(async (expression) => {
|
|
779
|
+
if (!expression) {
|
|
780
|
+
expression = await readStdin();
|
|
781
|
+
if (!expression) throw new Error("No expression. Pass as argument or pipe via stdin.");
|
|
782
|
+
}
|
|
783
|
+
await withPilot(async ({ transport, sessionId, state }) => {
|
|
784
|
+
const evalParams = { expression, returnByValue: true, awaitPromise: true };
|
|
785
|
+
if (state.frameContextId) evalParams.contextId = state.frameContextId;
|
|
786
|
+
const { result, exceptionDetails } = await transport.send("Runtime.evaluate", evalParams, sessionId);
|
|
787
|
+
if (exceptionDetails) {
|
|
788
|
+
throw new Error(exceptionDetails.exception?.description || exceptionDetails.text || "Evaluation error");
|
|
789
|
+
}
|
|
790
|
+
if (useJson()) {
|
|
791
|
+
console.log(JSON.stringify({ ok: true, value: result.value }));
|
|
792
|
+
} else if (result.value !== void 0) {
|
|
793
|
+
console.log(typeof result.value === "object" ? JSON.stringify(result.value, null, 2) : String(result.value));
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
}));
|
|
797
|
+
program.command("upload <filepath>").description('Upload file (auto-finds <input type="file"> on the page)').option("--nth <n>", "which file input to use if multiple exist", "1").addHelpText("after", "\nAuto-detects file inputs on the page. No ref needed.\n\nExamples:\n bp upload ./photo.jpg\n bp upload /tmp/resume.pdf\n bp upload ./doc.pdf --nth 2 # if multiple file inputs").action(action(async (filepath, opts) => {
|
|
798
|
+
const absPath = resolvePath(filepath);
|
|
799
|
+
if (!existsSync6(absPath)) throw new Error(`File not found: ${absPath}`);
|
|
800
|
+
await withPilot(async ({ transport, sessionId, state }) => {
|
|
801
|
+
const { result } = await transport.send("Runtime.evaluate", {
|
|
802
|
+
expression: `JSON.stringify(Array.from(document.querySelectorAll('input[type=file]')).map((el,i) => ({index:i+1, name:el.name||el.id||'unnamed', accept:el.accept||'*'})))`,
|
|
803
|
+
returnByValue: true
|
|
804
|
+
}, sessionId);
|
|
805
|
+
const inputs = JSON.parse(result.value);
|
|
806
|
+
if (inputs.length === 0) throw new Error('No <input type="file"> found on this page.');
|
|
807
|
+
const nth = parseInt(opts.nth, 10);
|
|
808
|
+
if (nth < 1 || nth > inputs.length) {
|
|
809
|
+
const list = inputs.map((i) => ` ${i.index}. ${i.name} (${i.accept})`).join("\n");
|
|
810
|
+
throw new Error(`${inputs.length} file input(s) found. Use --nth to choose:
|
|
811
|
+
${list}`);
|
|
812
|
+
}
|
|
813
|
+
const { result: elResult } = await transport.send("Runtime.evaluate", {
|
|
814
|
+
expression: `document.querySelectorAll('input[type=file]')[${nth - 1}]`
|
|
815
|
+
}, sessionId);
|
|
816
|
+
try {
|
|
817
|
+
const { node } = await transport.send("DOM.describeNode", { objectId: elResult.objectId }, sessionId);
|
|
818
|
+
await transport.send("DOM.setFileInputFiles", {
|
|
819
|
+
files: [absPath],
|
|
820
|
+
backendNodeId: node.backendNodeId
|
|
821
|
+
}, sessionId);
|
|
822
|
+
} finally {
|
|
823
|
+
if (elResult.objectId) {
|
|
824
|
+
await transport.send("Runtime.releaseObject", { objectId: elResult.objectId }, sessionId).catch(() => {
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
emitSnapshot(await snap(transport, sessionId, state.activeTargetId));
|
|
829
|
+
});
|
|
830
|
+
}));
|
|
831
|
+
program.command("screenshot [filename]").description("Capture screenshot").option("-f, --full", "capture full page").option("--selector <sel>", "capture specific element").addHelpText("after", '\nExamples:\n bp screenshot\n bp screenshot page.png\n bp screenshot --full\n bp screenshot --selector ".chart"').action(action(async (filename, opts) => {
|
|
832
|
+
await withPilot(async ({ transport, sessionId }) => {
|
|
833
|
+
const params = { format: "png" };
|
|
834
|
+
if (opts.full) {
|
|
835
|
+
const { result } = await transport.send("Runtime.evaluate", {
|
|
836
|
+
expression: PAGE_DIMENSIONS,
|
|
837
|
+
returnByValue: true
|
|
838
|
+
}, sessionId);
|
|
839
|
+
const dims = JSON.parse(result.value);
|
|
840
|
+
params.captureBeyondViewport = true;
|
|
841
|
+
params.clip = { x: 0, y: 0, ...dims, scale: 1 };
|
|
842
|
+
}
|
|
843
|
+
if (opts.selector) {
|
|
844
|
+
const { result } = await transport.send("Runtime.evaluate", {
|
|
845
|
+
expression: elementRect(opts.selector),
|
|
846
|
+
returnByValue: true
|
|
847
|
+
}, sessionId);
|
|
848
|
+
const rect = result.value ? JSON.parse(result.value) : null;
|
|
849
|
+
if (!rect) throw new Error(`Element not found: ${opts.selector}`);
|
|
850
|
+
params.clip = { ...rect, scale: 1 };
|
|
851
|
+
}
|
|
852
|
+
const { data } = await transport.send("Page.captureScreenshot", params, sessionId);
|
|
853
|
+
const file = filename ?? `screenshot-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, -5)}.png`;
|
|
854
|
+
writeFileSync3(file, Buffer.from(data, "base64"));
|
|
855
|
+
emit({ ok: true, file }, `\u2713 Screenshot saved to ${file}`);
|
|
856
|
+
});
|
|
857
|
+
}));
|
|
858
|
+
program.command("pdf [filename]").description("Save page as PDF").option("--landscape", "landscape orientation").addHelpText("after", "\nExamples:\n bp pdf\n bp pdf report.pdf\n bp pdf report.pdf --landscape").action(action(async (filename, opts) => {
|
|
859
|
+
await withPilot(async ({ transport, sessionId }) => {
|
|
860
|
+
const params = {};
|
|
861
|
+
if (opts.landscape) params.landscape = true;
|
|
862
|
+
const { data } = await transport.send("Page.printToPDF", params, sessionId);
|
|
863
|
+
const file = filename ?? `page-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, -5)}.pdf`;
|
|
864
|
+
writeFileSync3(file, Buffer.from(data, "base64"));
|
|
865
|
+
emit({ ok: true, file }, `\u2713 PDF saved to ${file}`);
|
|
866
|
+
});
|
|
867
|
+
}));
|
|
868
|
+
program.command("cookies [domain]").description("View cookies (CDP-only, includes HttpOnly)").addHelpText("after", "\nExamples:\n bp cookies\n bp cookies github.com").action(action(async (domain) => {
|
|
869
|
+
await withPilot(async ({ transport, sessionId }) => {
|
|
870
|
+
const { result: info } = await transport.send("Runtime.evaluate", {
|
|
871
|
+
expression: "location.href",
|
|
872
|
+
returnByValue: true
|
|
873
|
+
}, sessionId);
|
|
874
|
+
const urls = domain ? [`https://${domain}`, `http://${domain}`] : [info.value];
|
|
875
|
+
const { cookies } = await transport.send("Network.getCookies", { urls }, sessionId);
|
|
876
|
+
if (useJson()) {
|
|
877
|
+
console.log(JSON.stringify({ ok: true, cookies }));
|
|
878
|
+
} else if (cookies.length === 0) {
|
|
879
|
+
console.log("No cookies found.");
|
|
880
|
+
} else {
|
|
881
|
+
for (const c of cookies) {
|
|
882
|
+
const exp = c.expires === -1 ? "Session" : new Date(c.expires * 1e3).toISOString().slice(0, 10);
|
|
883
|
+
console.log(`${c.name.padEnd(30)} ${c.domain.padEnd(25)} ${exp}`);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
});
|
|
887
|
+
}));
|
|
888
|
+
program.command("frame [target]").description("List frames, or switch to a frame by index (0=top)").addHelpText("after", "\nExamples:\n bp frame # list all frames\n bp frame 1 # switch eval context to frame 1\n bp frame 0 # switch back to top frame").action(action(async (target) => {
|
|
889
|
+
await withPilot(async ({ transport, sessionId, state }) => {
|
|
890
|
+
const { frameTree } = await transport.send("Page.getFrameTree", {}, sessionId);
|
|
891
|
+
const frames = [];
|
|
892
|
+
function collect(node) {
|
|
893
|
+
frames.push({ id: node.frame.id, url: node.frame.url, name: node.frame.name || "" });
|
|
894
|
+
for (const child of node.childFrames || []) collect(child);
|
|
895
|
+
}
|
|
896
|
+
collect(frameTree);
|
|
897
|
+
if (target === void 0) {
|
|
898
|
+
const list = frames.map((f, i) => ({ index: i, ...f }));
|
|
899
|
+
if (useJson()) {
|
|
900
|
+
console.log(JSON.stringify({ ok: true, frames: list }));
|
|
901
|
+
} else {
|
|
902
|
+
for (const [i, f] of frames.entries()) {
|
|
903
|
+
console.log(`${i === 0 ? "* " : " "}${i} ${f.url} ${f.name}`);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
} else {
|
|
907
|
+
const idx = parseInt(target, 10);
|
|
908
|
+
if (isNaN(idx) || idx < 0 || idx >= frames.length) {
|
|
909
|
+
throw new Error(`Frame index out of range (0-${frames.length - 1})`);
|
|
910
|
+
}
|
|
911
|
+
if (idx === 0) {
|
|
912
|
+
state.frameContextId = void 0;
|
|
913
|
+
} else {
|
|
914
|
+
const { executionContextId } = await transport.send("Page.createIsolatedWorld", {
|
|
915
|
+
frameId: frames[idx].id
|
|
916
|
+
}, sessionId);
|
|
917
|
+
state.frameContextId = executionContextId;
|
|
918
|
+
}
|
|
919
|
+
saveState(state);
|
|
920
|
+
emit(
|
|
921
|
+
{ ok: true, frame: idx, url: frames[idx].url },
|
|
922
|
+
`\u2713 Switched to frame ${idx}: ${frames[idx].url}`
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
});
|
|
926
|
+
}));
|
|
927
|
+
program.command("auth [username] [password]").description("Set or clear HTTP Basic Auth credentials").option("--clear", "clear stored credentials").addHelpText("after", "\nSets credentials for HTTP 401 challenges.\nCall before navigating to the auth-protected URL.\n\nExamples:\n bp auth admin secret123\n bp open https://staging.example.com\n bp auth --clear").action(action(async (username, password, opts) => {
|
|
928
|
+
const existing = await resumeExisting();
|
|
929
|
+
if (!existing) throw new Error("Not connected");
|
|
930
|
+
const { client, state } = existing;
|
|
931
|
+
if (opts.clear || !username) {
|
|
932
|
+
await client.clearAuth();
|
|
933
|
+
emit({ ok: true }, "\u2713 Auth credentials cleared");
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
if (!password) throw new Error("Usage: bp auth <username> <password>");
|
|
937
|
+
await client.setAuth(username, password);
|
|
938
|
+
emit({ ok: true }, "\u2713 Auth credentials set (scoped to HTTP 401 challenges)");
|
|
939
|
+
}));
|
|
940
|
+
program.command("tabs").description("List pilot tabs (auto-adopts discovered popups)").action(action(async () => {
|
|
941
|
+
const existing = await resumeExisting();
|
|
942
|
+
if (!existing) throw new Error("Not connected");
|
|
943
|
+
const { client, state } = existing;
|
|
944
|
+
const { targetInfos } = await client.send("Target.getTargets");
|
|
945
|
+
const discovered = await client.discoveredTargets();
|
|
946
|
+
const knownIds = new Set(state.pilotTargetIds);
|
|
947
|
+
const existingIds = new Set(targetInfos.map((t) => t.targetId));
|
|
948
|
+
let adopted = 0;
|
|
949
|
+
for (const d of discovered) {
|
|
950
|
+
if (!knownIds.has(d.targetId) && existingIds.has(d.targetId)) {
|
|
951
|
+
state.pilotTargetIds.push(d.targetId);
|
|
952
|
+
knownIds.add(d.targetId);
|
|
953
|
+
adopted++;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
if (adopted > 0) saveState(state);
|
|
957
|
+
const tabs = state.pilotTargetIds.map((id, i) => {
|
|
958
|
+
const t = targetInfos.find((t2) => t2.targetId === id);
|
|
959
|
+
return t ? { index: i, url: t.url || "about:blank", title: t.title || "", active: id === state.activeTargetId } : null;
|
|
960
|
+
}).filter(Boolean);
|
|
961
|
+
if (useJson()) {
|
|
962
|
+
console.log(JSON.stringify({ ok: true, tabs }));
|
|
963
|
+
} else if (tabs.length === 0) {
|
|
964
|
+
console.log("No pilot tabs open.");
|
|
965
|
+
} else {
|
|
966
|
+
for (const t of tabs) console.log(`${t.active ? "*" : " "} ${t.index} ${t.url} ${t.title}`);
|
|
967
|
+
}
|
|
968
|
+
}));
|
|
969
|
+
program.command("tab <index>").description("Switch to tab by index").action(action(async (indexStr) => {
|
|
970
|
+
const existing = await resumeExisting();
|
|
971
|
+
if (!existing) throw new Error("Not connected");
|
|
972
|
+
const { client, state } = existing;
|
|
973
|
+
const index = parseInt(indexStr, 10);
|
|
974
|
+
if (index < 0 || index >= state.pilotTargetIds.length) {
|
|
975
|
+
throw new Error(`Tab index out of range (0-${state.pilotTargetIds.length - 1})`);
|
|
976
|
+
}
|
|
977
|
+
state.activeTargetId = state.pilotTargetIds[index];
|
|
978
|
+
state.activeSessionId = void 0;
|
|
979
|
+
state.frameContextId = void 0;
|
|
980
|
+
saveState(state);
|
|
981
|
+
await client.send("Target.activateTarget", { targetId: state.activeTargetId });
|
|
982
|
+
emit({ ok: true, index }, `\u2713 Switched to tab ${index}`);
|
|
983
|
+
}));
|
|
984
|
+
program.command("close").description("Close current pilot tab").option("-a, --all", "close all tabs").action(action(async (opts) => {
|
|
985
|
+
const existing = await resumeExisting();
|
|
986
|
+
if (!existing) throw new Error("Not connected");
|
|
987
|
+
const { client, state } = existing;
|
|
988
|
+
if (opts.all) {
|
|
989
|
+
for (const id of [...state.pilotTargetIds]) {
|
|
990
|
+
try {
|
|
991
|
+
await client.send("Target.closeTarget", { targetId: id });
|
|
992
|
+
} catch {
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
clearState();
|
|
996
|
+
emit({ ok: true }, "\u2713 All tabs closed");
|
|
997
|
+
} else {
|
|
998
|
+
await client.send("Target.closeTarget", { targetId: state.activeTargetId });
|
|
999
|
+
state.pilotTargetIds = state.pilotTargetIds.filter((id) => id !== state.activeTargetId);
|
|
1000
|
+
if (state.pilotTargetIds.length > 0) {
|
|
1001
|
+
state.activeTargetId = state.pilotTargetIds[0];
|
|
1002
|
+
state.activeSessionId = void 0;
|
|
1003
|
+
state.frameContextId = void 0;
|
|
1004
|
+
saveState(state);
|
|
1005
|
+
emit({ ok: true, remaining: state.pilotTargetIds.length }, "\u2713 Tab closed");
|
|
1006
|
+
} else {
|
|
1007
|
+
clearState();
|
|
1008
|
+
emit({ ok: true }, "\u2713 Last tab closed");
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}));
|
|
1012
|
+
async function ensureNet() {
|
|
1013
|
+
const existing = await resumeExisting();
|
|
1014
|
+
if (!existing) throw new Error("Not connected");
|
|
1015
|
+
const { client, state } = existing;
|
|
1016
|
+
if (state.activeSessionId) await client.enableNetwork(state.activeSessionId);
|
|
1017
|
+
return { client, state };
|
|
1018
|
+
}
|
|
1019
|
+
var netCmd = program.command("net").description("Network monitoring and interception").option("-l, --limit <n>", "max requests to show", "20").option("--url <pattern>", "filter by URL wildcard").option("--method <method>", "filter by HTTP method").option("--status <code>", "filter by status (200, 4xx, 5xx)").option("--type <types>", "filter by resource type (xhr,fetch,document)").option("--after <id>", "show requests after this ID").addHelpText("after", `
|
|
1020
|
+
Examples:
|
|
1021
|
+
bp net # list recent requests
|
|
1022
|
+
bp net --url "*api*" --method POST # filter
|
|
1023
|
+
bp net show 3 # full details + body
|
|
1024
|
+
bp net block "*tracking*" # block URLs
|
|
1025
|
+
bp net mock "*api/data*" --body '{"ok":true}'
|
|
1026
|
+
bp net rules # list active rules
|
|
1027
|
+
bp net remove --all # clear rules`).action(action(async (opts) => {
|
|
1028
|
+
const { client } = await ensureNet();
|
|
1029
|
+
const { requests, total } = await client.netRequests({
|
|
1030
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : void 0,
|
|
1031
|
+
url: opts.url,
|
|
1032
|
+
method: opts.method,
|
|
1033
|
+
status: opts.status,
|
|
1034
|
+
type: opts.type,
|
|
1035
|
+
after: opts.after ? parseInt(opts.after, 10) : void 0
|
|
1036
|
+
});
|
|
1037
|
+
if (useJson()) {
|
|
1038
|
+
console.log(JSON.stringify({ ok: true, requests, total }));
|
|
1039
|
+
} else if (requests.length === 0) {
|
|
1040
|
+
console.log("No requests captured.");
|
|
1041
|
+
} else {
|
|
1042
|
+
console.log(` ${"#".padStart(4)} ${"METHOD".padEnd(7)} ${"STATUS".padEnd(7)} ${"TYPE".padEnd(8)} ${"TIME".padEnd(8)} URL`);
|
|
1043
|
+
for (const r of requests) {
|
|
1044
|
+
const time = r.time ? `${r.time}ms` : r.error ? "FAIL" : "...";
|
|
1045
|
+
const status = r.status ? String(r.status) : r.error ? "ERR" : "...";
|
|
1046
|
+
console.log(` ${String(r.id).padStart(4)} ${r.method.padEnd(7)} ${status.padEnd(7)} ${(r.type || "").padEnd(8)} ${time.padEnd(8)} ${r.url}`);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
}));
|
|
1050
|
+
netCmd.command("show <id>").description("Show full request/response details").option("--save <file>", "save response body to file").action(action(async (idStr, opts) => {
|
|
1051
|
+
const { client } = await ensureNet();
|
|
1052
|
+
const id = parseInt(idStr, 10);
|
|
1053
|
+
if (opts.save) {
|
|
1054
|
+
const { body } = await client.netBody(id);
|
|
1055
|
+
writeFileSync3(opts.save, body);
|
|
1056
|
+
emit({ ok: true, file: opts.save }, `Saved to ${opts.save}`);
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
const detail = await client.netRequestDetail(id);
|
|
1060
|
+
let responseBody;
|
|
1061
|
+
try {
|
|
1062
|
+
responseBody = (await client.netBody(id)).body;
|
|
1063
|
+
} catch {
|
|
1064
|
+
}
|
|
1065
|
+
if (useJson()) {
|
|
1066
|
+
console.log(JSON.stringify({ ok: true, ...detail, responseBody }));
|
|
1067
|
+
} else {
|
|
1068
|
+
console.log(`#${detail.id} ${detail.method} ${detail.url}`);
|
|
1069
|
+
console.log(`Status: ${detail.status ?? "pending"} ${detail.statusText ?? ""}`);
|
|
1070
|
+
if (detail.postData) console.log(`
|
|
1071
|
+
Request Body:
|
|
1072
|
+
${detail.postData}`);
|
|
1073
|
+
if (responseBody) {
|
|
1074
|
+
console.log(`
|
|
1075
|
+
Response (${detail.mimeType}):`);
|
|
1076
|
+
console.log(responseBody.length > 2e3 ? responseBody.slice(0, 2e3) + "\n... (truncated)" : responseBody);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
}));
|
|
1080
|
+
netCmd.command("block <pattern>").description("Block requests matching URL pattern").action(action(async (pattern) => {
|
|
1081
|
+
const { client } = await ensureNet();
|
|
1082
|
+
const { rule } = await client.netAddRule({ type: "block", pattern });
|
|
1083
|
+
emit({ ok: true, rule }, `Rule #${rule.id}: blocking "${pattern}"`);
|
|
1084
|
+
}));
|
|
1085
|
+
netCmd.command("mock <pattern>").description("Mock responses for matching URLs").option("--body <json>", "response body").option("--file <path>", "read body from file").option("--status <code>", "HTTP status", "200").action(action(async (pattern, opts) => {
|
|
1086
|
+
const { client } = await ensureNet();
|
|
1087
|
+
let body = opts.body || "";
|
|
1088
|
+
if (opts.file) {
|
|
1089
|
+
const filePath = resolvePath(opts.file);
|
|
1090
|
+
if (!existsSync6(filePath)) throw new Error(`File not found: ${filePath}`);
|
|
1091
|
+
body = readFileSync5(filePath, "utf-8");
|
|
1092
|
+
}
|
|
1093
|
+
const { rule } = await client.netAddRule({
|
|
1094
|
+
type: "mock",
|
|
1095
|
+
pattern,
|
|
1096
|
+
status: parseInt(opts.status, 10),
|
|
1097
|
+
body
|
|
1098
|
+
});
|
|
1099
|
+
emit({ ok: true, rule }, `Rule #${rule.id}: mocking "${pattern}" -> ${opts.status}`);
|
|
1100
|
+
}));
|
|
1101
|
+
netCmd.command("headers <pattern> <header...>").description("Add/override request headers for matching URLs").action(action(async (pattern, headerStrs) => {
|
|
1102
|
+
const { client } = await ensureNet();
|
|
1103
|
+
const headers = headerStrs.map((h) => {
|
|
1104
|
+
const [name, ...rest] = h.split(":");
|
|
1105
|
+
return { name: name.trim(), value: rest.join(":").trim() };
|
|
1106
|
+
});
|
|
1107
|
+
const { rule } = await client.netAddRule({ type: "headers", pattern, headers });
|
|
1108
|
+
emit({ ok: true, rule }, `Rule #${rule.id}: headers for "${pattern}"`);
|
|
1109
|
+
}));
|
|
1110
|
+
netCmd.command("rules").description("List active interception rules").action(action(async () => {
|
|
1111
|
+
const { client } = await ensureNet();
|
|
1112
|
+
const { rules } = await client.netRules();
|
|
1113
|
+
if (useJson()) {
|
|
1114
|
+
console.log(JSON.stringify({ ok: true, rules }));
|
|
1115
|
+
} else if (rules.length === 0) {
|
|
1116
|
+
console.log("No active rules.");
|
|
1117
|
+
} else {
|
|
1118
|
+
for (const r of rules) console.log(` #${r.id} ${r.type.toUpperCase()} "${r.pattern}"`);
|
|
1119
|
+
}
|
|
1120
|
+
}));
|
|
1121
|
+
netCmd.command("remove [ruleId]").description("Remove interception rule(s)").option("-a, --all", "remove all rules").action(action(async (ruleId, opts) => {
|
|
1122
|
+
const { client } = await ensureNet();
|
|
1123
|
+
if (opts.all) {
|
|
1124
|
+
await client.netRemoveRule();
|
|
1125
|
+
emit({ ok: true }, "All rules removed");
|
|
1126
|
+
} else if (ruleId) {
|
|
1127
|
+
await client.netRemoveRule(parseInt(ruleId, 10));
|
|
1128
|
+
emit({ ok: true }, `Rule #${ruleId} removed`);
|
|
1129
|
+
} else throw new Error("Specify a rule ID or use --all");
|
|
1130
|
+
}));
|
|
1131
|
+
netCmd.command("clear").description("Clear captured request log").action(action(async () => {
|
|
1132
|
+
const { client } = await ensureNet();
|
|
1133
|
+
await client.netClear();
|
|
1134
|
+
emit({ ok: true }, "Request log cleared");
|
|
1135
|
+
}));
|
|
1136
|
+
program.parse();
|