dev3000 0.0.173 → 0.0.175
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 +0 -4
- package/dist/cdp-monitor.d.ts +21 -2
- package/dist/cdp-monitor.d.ts.map +1 -1
- package/dist/cdp-monitor.js +449 -107
- package/dist/cdp-monitor.js.map +1 -1
- package/dist/cli.js +193 -216
- package/dist/cli.js.map +1 -1
- package/dist/commands/crawl.d.ts.map +1 -1
- package/dist/commands/crawl.js +4 -43
- package/dist/commands/crawl.js.map +1 -1
- package/dist/commands/errors.d.ts.map +1 -1
- package/dist/commands/errors.js +4 -53
- package/dist/commands/errors.js.map +1 -1
- package/dist/commands/fix.d.ts.map +1 -1
- package/dist/commands/fix.js +5 -74
- package/dist/commands/fix.js.map +1 -1
- package/dist/commands/logs.d.ts.map +1 -1
- package/dist/commands/logs.js +4 -53
- package/dist/commands/logs.js.map +1 -1
- package/dist/commands/resume.d.ts +11 -0
- package/dist/commands/resume.d.ts.map +1 -0
- package/dist/commands/resume.js +75 -0
- package/dist/commands/resume.js.map +1 -0
- package/dist/commands/skill-runner.d.ts +14 -0
- package/dist/commands/skill-runner.d.ts.map +1 -0
- package/dist/commands/skill-runner.js +494 -0
- package/dist/commands/skill-runner.js.map +1 -0
- package/dist/dev-environment.d.ts +26 -3
- package/dist/dev-environment.d.ts.map +1 -1
- package/dist/dev-environment.js +285 -118
- package/dist/dev-environment.js.map +1 -1
- package/dist/skills/d3k/internal-skill.md +145 -0
- package/dist/skills/index.test.ts +28 -1
- package/dist/skills/index.ts +58 -7
- package/dist/tui-interface-opentui.d.ts +2 -0
- package/dist/tui-interface-opentui.d.ts.map +1 -1
- package/dist/tui-interface-opentui.js +17 -3
- package/dist/tui-interface-opentui.js.map +1 -1
- package/dist/utils/agent-browser.d.ts.map +1 -1
- package/dist/utils/agent-browser.js +6 -3
- package/dist/utils/agent-browser.js.map +1 -1
- package/dist/utils/agent-detection.d.ts +1 -0
- package/dist/utils/agent-detection.d.ts.map +1 -1
- package/dist/utils/agent-detection.js +11 -0
- package/dist/utils/agent-detection.js.map +1 -1
- package/dist/utils/agent-selection.js +4 -4
- package/dist/utils/agent-selection.js.map +1 -1
- package/dist/utils/browser-command-argv.d.ts +1 -1
- package/dist/utils/browser-command-argv.d.ts.map +1 -1
- package/dist/utils/browser-command-argv.js +1 -1
- package/dist/utils/browser-command-argv.js.map +1 -1
- package/dist/utils/project-metadata.d.ts +4 -0
- package/dist/utils/project-metadata.d.ts.map +1 -0
- package/dist/utils/project-metadata.js +48 -0
- package/dist/utils/project-metadata.js.map +1 -0
- package/dist/utils/project-name.d.ts +2 -0
- package/dist/utils/project-name.d.ts.map +1 -1
- package/dist/utils/project-name.js +6 -0
- package/dist/utils/project-name.js.map +1 -1
- package/dist/utils/session.d.ts +14 -0
- package/dist/utils/session.d.ts.map +1 -0
- package/dist/utils/session.js +65 -0
- package/dist/utils/session.js.map +1 -0
- package/dist/utils/version-check.js +2 -2
- package/dist/utils/version-check.js.map +1 -1
- package/package.json +9 -21
- package/dist/commands/cloud-check-pr.d.ts +0 -9
- package/dist/commands/cloud-check-pr.d.ts.map +0 -1
- package/dist/commands/cloud-check-pr.js +0 -243
- package/dist/commands/cloud-check-pr.js.map +0 -1
- package/dist/commands/cloud-fix.d.ts +0 -13
- package/dist/commands/cloud-fix.d.ts.map +0 -1
- package/dist/commands/cloud-fix.js +0 -79
- package/dist/commands/cloud-fix.js.map +0 -1
- package/dist/commands/find-component.d.ts +0 -8
- package/dist/commands/find-component.d.ts.map +0 -1
- package/dist/commands/find-component.js +0 -182
- package/dist/commands/find-component.js.map +0 -1
- package/dist/skills/d3k/SKILL.md +0 -126
- package/dist/skills/index.d.ts +0 -46
- package/dist/skills/index.d.ts.map +0 -1
- package/dist/skills/index.js +0 -174
- package/dist/skills/index.js.map +0 -1
package/dist/cdp-monitor.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
|
-
import { existsSync, mkdtempSync, readFileSync, writeFileSync } from "fs";
|
|
2
|
+
import { existsSync, mkdtempSync, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
3
3
|
import { tmpdir } from "os";
|
|
4
4
|
import { dirname, join } from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
@@ -43,6 +43,190 @@ const EMBEDDED_LOADING_HTML = `<!DOCTYPE html>
|
|
|
43
43
|
</div>
|
|
44
44
|
</body>
|
|
45
45
|
</html>`;
|
|
46
|
+
const DEFAULT_CDP_COMMAND_TIMEOUT_MS = 10000;
|
|
47
|
+
const DEFAULT_NAVIGATION_TIMEOUT_MS = 60000;
|
|
48
|
+
export const CHROME_CRASH_RESTORE_SUPPRESSION_FLAGS = [
|
|
49
|
+
"--disable-session-crashed-bubble",
|
|
50
|
+
"--disable-restore-session-state",
|
|
51
|
+
"--hide-crash-restore-bubble"
|
|
52
|
+
];
|
|
53
|
+
function isRecord(value) {
|
|
54
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
55
|
+
}
|
|
56
|
+
function patchChromePreferences(data) {
|
|
57
|
+
let changed = false;
|
|
58
|
+
const profile = isRecord(data.profile) ? data.profile : {};
|
|
59
|
+
if (data.profile !== profile) {
|
|
60
|
+
data.profile = profile;
|
|
61
|
+
changed = true;
|
|
62
|
+
}
|
|
63
|
+
if (profile.exit_type !== "Normal") {
|
|
64
|
+
profile.exit_type = "Normal";
|
|
65
|
+
changed = true;
|
|
66
|
+
}
|
|
67
|
+
if (profile.exited_cleanly !== true) {
|
|
68
|
+
profile.exited_cleanly = true;
|
|
69
|
+
changed = true;
|
|
70
|
+
}
|
|
71
|
+
return changed;
|
|
72
|
+
}
|
|
73
|
+
function patchChromeLocalState(data) {
|
|
74
|
+
let changed = false;
|
|
75
|
+
if (data.exit_type === "Crashed") {
|
|
76
|
+
data.exit_type = "Normal";
|
|
77
|
+
changed = true;
|
|
78
|
+
}
|
|
79
|
+
if (data.exited_cleanly === false) {
|
|
80
|
+
data.exited_cleanly = true;
|
|
81
|
+
changed = true;
|
|
82
|
+
}
|
|
83
|
+
if (isRecord(data.profile)) {
|
|
84
|
+
if (data.profile.exit_type === "Crashed") {
|
|
85
|
+
data.profile.exit_type = "Normal";
|
|
86
|
+
changed = true;
|
|
87
|
+
}
|
|
88
|
+
if (data.profile.exited_cleanly === false) {
|
|
89
|
+
data.profile.exited_cleanly = true;
|
|
90
|
+
changed = true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return changed;
|
|
94
|
+
}
|
|
95
|
+
function patchJsonFile(filePath, patch) {
|
|
96
|
+
if (!existsSync(filePath)) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
const data = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
101
|
+
if (!isRecord(data) || !patch(data)) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
writeFileSync(filePath, JSON.stringify(data));
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function getChromePreferencesFiles(profileDir) {
|
|
112
|
+
const files = new Set([join(profileDir, "Default", "Preferences")]);
|
|
113
|
+
if (!existsSync(profileDir)) {
|
|
114
|
+
return Array.from(files);
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
for (const entry of readdirSync(profileDir, { withFileTypes: true })) {
|
|
118
|
+
if (entry.isDirectory() && (entry.name === "Default" || entry.name.startsWith("Profile "))) {
|
|
119
|
+
files.add(join(profileDir, entry.name, "Preferences"));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// Ignore unreadable profile directories.
|
|
125
|
+
}
|
|
126
|
+
return Array.from(files);
|
|
127
|
+
}
|
|
128
|
+
export function resetChromeCrashRestoreState(profileDir) {
|
|
129
|
+
let changedFiles = 0;
|
|
130
|
+
for (const preferencesFile of getChromePreferencesFiles(profileDir)) {
|
|
131
|
+
if (patchJsonFile(preferencesFile, patchChromePreferences)) {
|
|
132
|
+
changedFiles++;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (patchJsonFile(join(profileDir, "Local State"), patchChromeLocalState)) {
|
|
136
|
+
changedFiles++;
|
|
137
|
+
}
|
|
138
|
+
return changedFiles;
|
|
139
|
+
}
|
|
140
|
+
export function getLoadingHtmlCandidates(currentDir, execPath = process.execPath) {
|
|
141
|
+
const candidates = [join(currentDir, "src/loading.html"), join(currentDir, "loading.html")];
|
|
142
|
+
const packageRoot = dirname(dirname(execPath));
|
|
143
|
+
candidates.push(join(packageRoot, "src/loading.html"));
|
|
144
|
+
candidates.push(join(packageRoot, "loading.html"));
|
|
145
|
+
candidates.push(join(process.cwd(), "src/loading.html"));
|
|
146
|
+
return candidates;
|
|
147
|
+
}
|
|
148
|
+
function isD3kLoadingPageUrl(url) {
|
|
149
|
+
if (!url?.startsWith("file://")) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
const pathname = decodeURIComponent(new URL(url).pathname);
|
|
154
|
+
return pathname.includes("/dev3000-loading-") && pathname.endsWith("/loading.html");
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function matchesAppServerPort(url, appServerPort) {
|
|
161
|
+
if (!url || !appServerPort) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
const parsed = new URL(url);
|
|
166
|
+
const hostname = parsed.hostname;
|
|
167
|
+
const port = parsed.port || (parsed.protocol === "https:" ? "443" : "80");
|
|
168
|
+
return (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "0.0.0.0") && port === appServerPort;
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function matchesInitialAppUrl(url, initialAppUrl) {
|
|
175
|
+
if (!url || !initialAppUrl) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
const actual = new URL(url);
|
|
180
|
+
const expected = new URL(initialAppUrl);
|
|
181
|
+
const actualPort = actual.port || (actual.protocol === "https:" ? "443" : "80");
|
|
182
|
+
const expectedPort = expected.port || (expected.protocol === "https:" ? "443" : "80");
|
|
183
|
+
return (actual.protocol === expected.protocol &&
|
|
184
|
+
actual.hostname === expected.hostname &&
|
|
185
|
+
actualPort === expectedPort &&
|
|
186
|
+
actual.pathname === expected.pathname);
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function formatTargetForLog(target) {
|
|
193
|
+
return `${target.type || "unknown"}:${target.url || "<no-url>"}`;
|
|
194
|
+
}
|
|
195
|
+
export function selectCDPTarget(targets, options = {}) {
|
|
196
|
+
const debuggableTargets = targets.filter((target) => target.webSocketDebuggerUrl);
|
|
197
|
+
if (debuggableTargets.length === 0) {
|
|
198
|
+
throw new Error(`No debuggable target found in Chrome (found ${targets.length} targets)`);
|
|
199
|
+
}
|
|
200
|
+
const contextualMatches = debuggableTargets
|
|
201
|
+
.map((target) => ({
|
|
202
|
+
target,
|
|
203
|
+
score: (matchesInitialAppUrl(target.url, options.initialAppUrl) ? 100 : 0) +
|
|
204
|
+
(matchesAppServerPort(target.url, options.appServerPort) ? 70 : 0) +
|
|
205
|
+
(isD3kLoadingPageUrl(target.url) ? 90 : 0) +
|
|
206
|
+
(target.type === "page" ? 10 : 0)
|
|
207
|
+
}))
|
|
208
|
+
.sort((left, right) => right.score - left.score);
|
|
209
|
+
const bestContextualScore = contextualMatches[0]?.score || 0;
|
|
210
|
+
const minimumContextualScore = 70;
|
|
211
|
+
if (bestContextualScore >= minimumContextualScore) {
|
|
212
|
+
return contextualMatches[0].target;
|
|
213
|
+
}
|
|
214
|
+
const pageTarget = debuggableTargets.find((target) => target.type === "page");
|
|
215
|
+
if (!options.appServerPort && !options.initialAppUrl) {
|
|
216
|
+
return pageTarget || debuggableTargets[0];
|
|
217
|
+
}
|
|
218
|
+
if (debuggableTargets.length === 1 && debuggableTargets[0]?.url === "about:blank") {
|
|
219
|
+
return debuggableTargets[0];
|
|
220
|
+
}
|
|
221
|
+
const expectedDetails = [
|
|
222
|
+
options.initialAppUrl ? `url ${options.initialAppUrl}` : null,
|
|
223
|
+
options.appServerPort ? `port ${options.appServerPort}` : null,
|
|
224
|
+
"d3k loading page"
|
|
225
|
+
]
|
|
226
|
+
.filter(Boolean)
|
|
227
|
+
.join(", ");
|
|
228
|
+
throw new Error(`CDP target mismatch on port ${options.appServerPort || "unknown"}; expected ${expectedDetails}, found ${debuggableTargets.map(formatTargetForLog).join(", ")}`);
|
|
229
|
+
}
|
|
46
230
|
export class CDPMonitor {
|
|
47
231
|
browser = null;
|
|
48
232
|
connection = null;
|
|
@@ -69,7 +253,9 @@ export class CDPMonitor {
|
|
|
69
253
|
framework; // Framework hint from project detection
|
|
70
254
|
reactTrackingEnabled = false;
|
|
71
255
|
lastReactSnapshotLogTime = 0;
|
|
72
|
-
|
|
256
|
+
pendingCommands = new Map();
|
|
257
|
+
navigationTimeoutMs = DEFAULT_NAVIGATION_TIMEOUT_MS;
|
|
258
|
+
constructor(profileDir, screenshotDir, logger, debug = false, browserPath, pluginReactScan = false, appServerPort, initialAppUrl, navigationTimeoutMs = DEFAULT_NAVIGATION_TIMEOUT_MS, debugPort, headless = false, framework) {
|
|
73
259
|
this.profileDir = profileDir;
|
|
74
260
|
this.screenshotDir = screenshotDir;
|
|
75
261
|
this.appServerPort = appServerPort;
|
|
@@ -78,6 +264,7 @@ export class CDPMonitor {
|
|
|
78
264
|
this.browserPath = browserPath;
|
|
79
265
|
this.pluginReactScan = pluginReactScan;
|
|
80
266
|
this.initialAppUrl = initialAppUrl;
|
|
267
|
+
this.navigationTimeoutMs = navigationTimeoutMs;
|
|
81
268
|
this.headless = headless;
|
|
82
269
|
this.framework = framework;
|
|
83
270
|
this.reactTrackingEnabled = framework === "nextjs";
|
|
@@ -269,11 +456,7 @@ export class CDPMonitor {
|
|
|
269
456
|
const currentDir = dirname(currentFile);
|
|
270
457
|
let loadingHtml;
|
|
271
458
|
try {
|
|
272
|
-
const loadingHtmlCandidates =
|
|
273
|
-
join(currentDir, "src/loading.html"),
|
|
274
|
-
join(currentDir, "loading.html"),
|
|
275
|
-
join(process.cwd(), "src/loading.html")
|
|
276
|
-
];
|
|
459
|
+
const loadingHtmlCandidates = getLoadingHtmlCandidates(currentDir);
|
|
277
460
|
const loadingHtmlPath = loadingHtmlCandidates.find((path) => existsSync(path));
|
|
278
461
|
if (!loadingHtmlPath) {
|
|
279
462
|
throw new Error("No loading.html found in expected locations");
|
|
@@ -347,10 +530,21 @@ export class CDPMonitor {
|
|
|
347
530
|
*/
|
|
348
531
|
async killExistingChromeWithProfile() {
|
|
349
532
|
try {
|
|
350
|
-
//
|
|
533
|
+
// Build a set of PIDs that must never be killed: this Node process and
|
|
534
|
+
// its parent. d3k's own argv contains the profile path (via --profile-dir),
|
|
535
|
+
// which previously caused a substring match here and made d3k SIGTERM itself.
|
|
536
|
+
const selfPids = new Set();
|
|
537
|
+
if (typeof process.pid === "number")
|
|
538
|
+
selfPids.add(process.pid);
|
|
539
|
+
if (typeof process.ppid === "number")
|
|
540
|
+
selfPids.add(process.ppid);
|
|
541
|
+
// Find Chrome processes using this profile directory. We only match the
|
|
542
|
+
// canonical `--user-data-dir=<profile>` form Chrome consumes; matching the
|
|
543
|
+
// bare path was too loose and caught unrelated processes (including d3k itself).
|
|
351
544
|
const processes = await this.listProcesses();
|
|
352
545
|
const pids = processes
|
|
353
|
-
.filter((proc) =>
|
|
546
|
+
.filter((proc) => !selfPids.has(proc.pid))
|
|
547
|
+
.filter((proc) => proc.command.includes(`--user-data-dir=${this.profileDir}`))
|
|
354
548
|
.map((proc) => proc.pid)
|
|
355
549
|
.filter((pid) => pid !== this.browser?.pid);
|
|
356
550
|
for (const pid of pids) {
|
|
@@ -374,6 +568,10 @@ export class CDPMonitor {
|
|
|
374
568
|
async launchChrome() {
|
|
375
569
|
// Kill any existing Chrome using this profile to prevent CDP conflicts
|
|
376
570
|
await this.killExistingChromeWithProfile();
|
|
571
|
+
const resetProfileFiles = resetChromeCrashRestoreState(this.profileDir);
|
|
572
|
+
if (resetProfileFiles > 0) {
|
|
573
|
+
this.debugLog(`Reset Chrome crash restore state in ${resetProfileFiles} profile file(s)`);
|
|
574
|
+
}
|
|
377
575
|
return new Promise((resolve, reject) => {
|
|
378
576
|
// Use custom browser path if provided, otherwise try different Chrome executables based on platform
|
|
379
577
|
const chromeCommands = this.browserPath
|
|
@@ -383,6 +581,8 @@ export class CDPMonitor {
|
|
|
383
581
|
"google-chrome",
|
|
384
582
|
"chrome",
|
|
385
583
|
"chromium",
|
|
584
|
+
"brave",
|
|
585
|
+
"brave-browser",
|
|
386
586
|
"/Applications/Arc.app/Contents/MacOS/Arc",
|
|
387
587
|
"/Applications/Comet.app/Contents/MacOS/Comet",
|
|
388
588
|
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"
|
|
@@ -422,8 +622,7 @@ export class CDPMonitor {
|
|
|
422
622
|
"--disable-sync",
|
|
423
623
|
"--metrics-recording-only",
|
|
424
624
|
"--disable-default-apps",
|
|
425
|
-
|
|
426
|
-
"--disable-restore-session-state"
|
|
625
|
+
...CHROME_CRASH_RESTORE_SUPPRESSION_FLAGS
|
|
427
626
|
];
|
|
428
627
|
if (shouldEnableReactDevTools && reactDevToolsExtensionPath) {
|
|
429
628
|
chromeArgs.push(`--disable-extensions-except=${reactDevToolsExtensionPath}`);
|
|
@@ -442,6 +641,10 @@ export class CDPMonitor {
|
|
|
442
641
|
chromeArgs.push("--disable-setuid-sandbox");
|
|
443
642
|
chromeArgs.push("--disable-gpu");
|
|
444
643
|
chromeArgs.push("--disable-dev-shm-usage");
|
|
644
|
+
chromeArgs.push("--disable-background-timer-throttling");
|
|
645
|
+
chromeArgs.push("--disable-backgrounding-occluded-windows");
|
|
646
|
+
chromeArgs.push("--disable-renderer-backgrounding");
|
|
647
|
+
chromeArgs.push("--window-size=1920,1080");
|
|
445
648
|
this.debugLog("Launching Chrome in headless mode");
|
|
446
649
|
}
|
|
447
650
|
else {
|
|
@@ -538,15 +741,15 @@ export class CDPMonitor {
|
|
|
538
741
|
try {
|
|
539
742
|
// Get the WebSocket URL from Chrome's debug endpoint
|
|
540
743
|
const targetsResponse = await fetch(`http://localhost:${this.debugPort}/json`);
|
|
541
|
-
let targets = await targetsResponse.json();
|
|
542
|
-
this.debugLog(`Found ${targets.length} targets: ${JSON.stringify(targets.map((
|
|
744
|
+
let targets = (await targetsResponse.json());
|
|
745
|
+
this.debugLog(`Found ${targets.length} targets: ${JSON.stringify(targets.map((target) => ({ type: target.type ?? "unknown", url: target.url ?? "" })))}`);
|
|
543
746
|
if (targets.length === 0) {
|
|
544
747
|
this.debugLog("No debuggable targets found; creating a blank page target");
|
|
545
748
|
const newTargetResponse = await fetch(`http://localhost:${this.debugPort}/json/new?about:blank`, {
|
|
546
749
|
method: "PUT"
|
|
547
750
|
});
|
|
548
751
|
if (newTargetResponse.ok) {
|
|
549
|
-
const createdTarget = await newTargetResponse.json();
|
|
752
|
+
const createdTarget = (await newTargetResponse.json());
|
|
550
753
|
targets = [createdTarget];
|
|
551
754
|
this.debugLog(`Created page target: ${createdTarget.id || "unknown"} - ${createdTarget.url || ""}`);
|
|
552
755
|
}
|
|
@@ -554,19 +757,14 @@ export class CDPMonitor {
|
|
|
554
757
|
this.debugLog(`Failed to create page target: HTTP ${newTargetResponse.status}`);
|
|
555
758
|
}
|
|
556
759
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
pageTarget = targets.find((target) => target.webSocketDebuggerUrl);
|
|
562
|
-
if (pageTarget) {
|
|
563
|
-
this.debugLog(`No 'page' type target found, using target of type '${pageTarget.type}' instead`);
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
if (!pageTarget) {
|
|
567
|
-
throw new Error(`No debuggable target found in Chrome (found ${targets.length} targets)`);
|
|
568
|
-
}
|
|
760
|
+
const pageTarget = selectCDPTarget(targets, {
|
|
761
|
+
appServerPort: this.appServerPort,
|
|
762
|
+
initialAppUrl: this.initialAppUrl
|
|
763
|
+
});
|
|
569
764
|
const wsUrl = pageTarget.webSocketDebuggerUrl;
|
|
765
|
+
if (!wsUrl) {
|
|
766
|
+
throw new Error(`Selected CDP target does not have a websocket URL: ${formatTargetForLog(pageTarget)}`);
|
|
767
|
+
}
|
|
570
768
|
this.cdpUrl = wsUrl; // Store the CDP URL
|
|
571
769
|
this.debugLog(`Found page target: ${pageTarget.title || "Unknown"} - ${pageTarget.url}`);
|
|
572
770
|
this.debugLog(`Got CDP WebSocket URL: ${wsUrl}`);
|
|
@@ -599,6 +797,7 @@ export class CDPMonitor {
|
|
|
599
797
|
});
|
|
600
798
|
ws.on("close", (code, reason) => {
|
|
601
799
|
this.debugLog(`WebSocket closed with code ${code}, reason: ${reason}`);
|
|
800
|
+
this.rejectPendingCDPCommands(new Error(`CDP connection closed before response (code=${code})`));
|
|
602
801
|
if (!this.isShuttingDown) {
|
|
603
802
|
this.logger("browser", `[CDP] Connection lost unexpectedly (code: ${code}, reason: ${reason})`);
|
|
604
803
|
this.logger("browser", "[CDP] CDP connection lost - check for Chrome crash or server issues");
|
|
@@ -682,7 +881,7 @@ export class CDPMonitor {
|
|
|
682
881
|
throw err;
|
|
683
882
|
}
|
|
684
883
|
}
|
|
685
|
-
async sendCDPCommand(method, params = {}) {
|
|
884
|
+
async sendCDPCommand(method, params = {}, timeoutMs = DEFAULT_CDP_COMMAND_TIMEOUT_MS) {
|
|
686
885
|
if (!this.connection) {
|
|
687
886
|
throw new Error("No CDP connection available");
|
|
688
887
|
}
|
|
@@ -693,42 +892,32 @@ export class CDPMonitor {
|
|
|
693
892
|
method,
|
|
694
893
|
params
|
|
695
894
|
};
|
|
696
|
-
const messageHandler = (data) => {
|
|
697
|
-
try {
|
|
698
|
-
const message = JSON.parse(data.toString());
|
|
699
|
-
if (message.id === id) {
|
|
700
|
-
this.connection?.ws.removeListener("message", messageHandler);
|
|
701
|
-
if (message.error) {
|
|
702
|
-
reject(new Error(message.error.message));
|
|
703
|
-
}
|
|
704
|
-
else {
|
|
705
|
-
resolve(message.result);
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
catch (error) {
|
|
710
|
-
this.connection?.ws.removeListener("message", messageHandler);
|
|
711
|
-
reject(error);
|
|
712
|
-
}
|
|
713
|
-
};
|
|
714
|
-
this.connection?.ws.on("message", messageHandler);
|
|
715
|
-
// Command timeout
|
|
716
895
|
const timeout = setTimeout(() => {
|
|
717
|
-
this.
|
|
896
|
+
this.pendingCommands.delete(id);
|
|
897
|
+
this.debugLog(`CDP command #${id} timed out after ${timeoutMs}ms: ${method}`);
|
|
718
898
|
reject(new Error(`CDP command timeout: ${method}`));
|
|
719
|
-
},
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
};
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
899
|
+
}, timeoutMs);
|
|
900
|
+
this.pendingCommands.set(id, {
|
|
901
|
+
method,
|
|
902
|
+
startedAt: Date.now(),
|
|
903
|
+
timeout,
|
|
904
|
+
resolve,
|
|
905
|
+
reject: (error) => reject(error)
|
|
906
|
+
});
|
|
907
|
+
this.debugLog(`Sending CDP command #${id}: ${method} ${JSON.stringify(params)}`);
|
|
908
|
+
this.connection?.ws.send(JSON.stringify(command), (error) => {
|
|
909
|
+
if (!error) {
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
const pending = this.pendingCommands.get(id);
|
|
913
|
+
if (!pending) {
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
clearTimeout(pending.timeout);
|
|
917
|
+
this.pendingCommands.delete(id);
|
|
918
|
+
this.debugLog(`Failed to send CDP command #${id}: ${method}: ${String(error)}`);
|
|
919
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
920
|
+
});
|
|
732
921
|
});
|
|
733
922
|
}
|
|
734
923
|
getBundleTypeLabel(bundleType) {
|
|
@@ -808,39 +997,39 @@ export class CDPMonitor {
|
|
|
808
997
|
}
|
|
809
998
|
async enableCDPDomains() {
|
|
810
999
|
const domains = [
|
|
811
|
-
"Runtime",
|
|
812
|
-
"Network",
|
|
813
|
-
"Page",
|
|
814
|
-
"DOM",
|
|
815
|
-
"Performance",
|
|
816
|
-
"Security",
|
|
817
|
-
"Log",
|
|
818
|
-
"Target"
|
|
1000
|
+
{ name: "Runtime", required: true },
|
|
1001
|
+
{ name: "Network", required: true },
|
|
1002
|
+
{ name: "Page", required: true },
|
|
1003
|
+
{ name: "DOM", required: true },
|
|
1004
|
+
{ name: "Performance", required: false },
|
|
1005
|
+
{ name: "Security", required: false },
|
|
1006
|
+
{ name: "Log", required: true },
|
|
1007
|
+
{ name: "Target", required: false }
|
|
819
1008
|
// Note: Input domain is for dispatching events, not monitoring them - we use JS injection instead
|
|
820
1009
|
];
|
|
821
1010
|
for (const domain of domains) {
|
|
822
1011
|
try {
|
|
823
|
-
this.debugLog(`Enabling CDP domain: ${domain}`);
|
|
824
|
-
await this.sendCDPCommand(`${domain}.enable
|
|
825
|
-
this.debugLog(`Successfully enabled CDP domain: ${domain}`);
|
|
1012
|
+
this.debugLog(`Enabling CDP domain: ${domain.name}`);
|
|
1013
|
+
await this.sendCDPCommand(`${domain.name}.enable`, {}, 3000);
|
|
1014
|
+
this.debugLog(`Successfully enabled CDP domain: ${domain.name}`);
|
|
826
1015
|
if (this.debug) {
|
|
827
|
-
this.logger("browser", `[CDP] Enabled ${domain} domain`);
|
|
1016
|
+
this.logger("browser", `[CDP] Enabled ${domain.name} domain`);
|
|
828
1017
|
}
|
|
829
1018
|
}
|
|
830
1019
|
catch (error) {
|
|
831
|
-
this.debugLog(`Failed to enable CDP domain ${domain}: ${error}`);
|
|
832
|
-
|
|
1020
|
+
this.debugLog(`Failed to enable CDP domain ${domain.name}: ${error}`);
|
|
1021
|
+
if (domain.required) {
|
|
1022
|
+
throw new Error(`Failed to enable required CDP domain ${domain.name}: ${String(error)}`);
|
|
1023
|
+
}
|
|
833
1024
|
if (this.debug) {
|
|
834
|
-
this.logger("browser", `[CDP] Failed to enable ${domain}: ${error}`);
|
|
1025
|
+
this.logger("browser", `[CDP] Failed to enable optional ${domain.name}: ${error}`);
|
|
835
1026
|
}
|
|
836
|
-
// Continue with other domains instead of throwing
|
|
837
1027
|
}
|
|
838
1028
|
}
|
|
839
|
-
this.debugLog("
|
|
840
|
-
await this.sendCDPCommand("Runtime.enable");
|
|
1029
|
+
this.debugLog("Setting async call stack depth for console and exception capture");
|
|
841
1030
|
await this.sendCDPCommand("Runtime.setAsyncCallStackDepth", {
|
|
842
1031
|
maxDepth: 32
|
|
843
|
-
});
|
|
1032
|
+
}, 3000);
|
|
844
1033
|
this.debugLog("CDP domains enabled successfully");
|
|
845
1034
|
// Set viewport for headless mode to ensure consistent CLS measurements
|
|
846
1035
|
// Without this, headless Chrome defaults to 800x600 which can cause
|
|
@@ -859,6 +1048,18 @@ export class CDPMonitor {
|
|
|
859
1048
|
catch (error) {
|
|
860
1049
|
this.debugLog(`Failed to set viewport: ${error}`);
|
|
861
1050
|
}
|
|
1051
|
+
try {
|
|
1052
|
+
await this.sendCDPCommand("Page.bringToFront", {});
|
|
1053
|
+
}
|
|
1054
|
+
catch (error) {
|
|
1055
|
+
this.debugLog(`Failed to bring headless page to front: ${error}`);
|
|
1056
|
+
}
|
|
1057
|
+
try {
|
|
1058
|
+
await this.sendCDPCommand("Emulation.setFocusEmulationEnabled", { enabled: true });
|
|
1059
|
+
}
|
|
1060
|
+
catch (error) {
|
|
1061
|
+
this.debugLog(`Failed to enable focus emulation: ${error}`);
|
|
1062
|
+
}
|
|
862
1063
|
}
|
|
863
1064
|
}
|
|
864
1065
|
setupEventHandlers() {
|
|
@@ -1089,8 +1290,34 @@ export class CDPMonitor {
|
|
|
1089
1290
|
onCDPEvent(method, handler) {
|
|
1090
1291
|
this.eventHandlers.set(method, handler);
|
|
1091
1292
|
}
|
|
1293
|
+
rejectPendingCDPCommands(error) {
|
|
1294
|
+
for (const [id, pending] of this.pendingCommands) {
|
|
1295
|
+
clearTimeout(pending.timeout);
|
|
1296
|
+
this.pendingCommands.delete(id);
|
|
1297
|
+
pending.reject(error);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1092
1300
|
handleCDPMessage(message) {
|
|
1301
|
+
if (typeof message.id === "number") {
|
|
1302
|
+
const pending = this.pendingCommands.get(message.id);
|
|
1303
|
+
if (!pending) {
|
|
1304
|
+
this.debugLog(`Received CDP response for unknown command #${message.id}`);
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
clearTimeout(pending.timeout);
|
|
1308
|
+
this.pendingCommands.delete(message.id);
|
|
1309
|
+
const duration = Date.now() - pending.startedAt;
|
|
1310
|
+
if (message.error) {
|
|
1311
|
+
this.debugLog(`Received CDP error for #${message.id} (${pending.method}) after ${duration}ms: ${message.error.message}`);
|
|
1312
|
+
pending.reject(new Error(message.error.message || `CDP command failed: ${pending.method}`));
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
this.debugLog(`Received CDP response for #${message.id} (${pending.method}) after ${duration}ms`);
|
|
1316
|
+
pending.resolve(message.result || {});
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1093
1319
|
if (message.method) {
|
|
1320
|
+
this.debugLog(`Received CDP event: ${message.method}`);
|
|
1094
1321
|
const handler = this.eventHandlers.get(message.method);
|
|
1095
1322
|
if (handler) {
|
|
1096
1323
|
const event = {
|
|
@@ -1103,7 +1330,7 @@ export class CDPMonitor {
|
|
|
1103
1330
|
}
|
|
1104
1331
|
}
|
|
1105
1332
|
}
|
|
1106
|
-
async navigateToUrl(url) {
|
|
1333
|
+
async navigateToUrl(url, timeoutMs = this.navigationTimeoutMs) {
|
|
1107
1334
|
if (!this.connection) {
|
|
1108
1335
|
throw new Error("No CDP connection available");
|
|
1109
1336
|
}
|
|
@@ -1113,7 +1340,7 @@ export class CDPMonitor {
|
|
|
1113
1340
|
try {
|
|
1114
1341
|
const result = await this.sendCDPCommand("Page.navigate", {
|
|
1115
1342
|
url
|
|
1116
|
-
});
|
|
1343
|
+
}, timeoutMs);
|
|
1117
1344
|
const navigationTime = Date.now() - navigationStartTime;
|
|
1118
1345
|
this.debugLog(`Navigation command sent successfully (${navigationTime}ms)`);
|
|
1119
1346
|
this.debugLog(`Navigation result: ${JSON.stringify(result)}`);
|
|
@@ -1635,37 +1862,148 @@ export class CDPMonitor {
|
|
|
1635
1862
|
this.isShuttingDown = true;
|
|
1636
1863
|
this.debugLog("Shutdown signaled - reconnection attempts will be blocked");
|
|
1637
1864
|
}
|
|
1865
|
+
async waitForBrowserExit(timeoutMs) {
|
|
1866
|
+
const browser = this.browser;
|
|
1867
|
+
if (!browser) {
|
|
1868
|
+
return true;
|
|
1869
|
+
}
|
|
1870
|
+
if (browser.exitCode !== null || browser.killed) {
|
|
1871
|
+
return true;
|
|
1872
|
+
}
|
|
1873
|
+
return await new Promise((resolve) => {
|
|
1874
|
+
const onExit = () => {
|
|
1875
|
+
clearTimeout(timer);
|
|
1876
|
+
resolve(true);
|
|
1877
|
+
};
|
|
1878
|
+
const timer = setTimeout(() => {
|
|
1879
|
+
browser.removeListener("exit", onExit);
|
|
1880
|
+
resolve(false);
|
|
1881
|
+
}, timeoutMs);
|
|
1882
|
+
browser.once("exit", onExit);
|
|
1883
|
+
});
|
|
1884
|
+
}
|
|
1885
|
+
async sendBrowserCloseCommand() {
|
|
1886
|
+
try {
|
|
1887
|
+
await this.sendCDPCommand("Browser.close", {}, 3000);
|
|
1888
|
+
this.debugLog("Sent Browser.close command");
|
|
1889
|
+
return;
|
|
1890
|
+
}
|
|
1891
|
+
catch (error) {
|
|
1892
|
+
this.debugLog(`Browser.close on page target failed: ${error}`);
|
|
1893
|
+
}
|
|
1894
|
+
const versionResponse = await fetch(`http://localhost:${this.debugPort}/json/version`, {
|
|
1895
|
+
signal: AbortSignal.timeout(1000)
|
|
1896
|
+
});
|
|
1897
|
+
if (!versionResponse.ok) {
|
|
1898
|
+
throw new Error(`Failed to get browser CDP endpoint: HTTP ${versionResponse.status}`);
|
|
1899
|
+
}
|
|
1900
|
+
const version = (await versionResponse.json());
|
|
1901
|
+
if (!version.webSocketDebuggerUrl) {
|
|
1902
|
+
throw new Error("Browser CDP endpoint did not include webSocketDebuggerUrl");
|
|
1903
|
+
}
|
|
1904
|
+
await new Promise((resolve, reject) => {
|
|
1905
|
+
const ws = new WebSocket(version.webSocketDebuggerUrl);
|
|
1906
|
+
let commandSent = false;
|
|
1907
|
+
let settled = false;
|
|
1908
|
+
const settle = (callback) => {
|
|
1909
|
+
if (settled)
|
|
1910
|
+
return;
|
|
1911
|
+
settled = true;
|
|
1912
|
+
clearTimeout(timeout);
|
|
1913
|
+
try {
|
|
1914
|
+
ws.close();
|
|
1915
|
+
}
|
|
1916
|
+
catch {
|
|
1917
|
+
// Ignore close errors.
|
|
1918
|
+
}
|
|
1919
|
+
callback();
|
|
1920
|
+
};
|
|
1921
|
+
const timeout = setTimeout(() => {
|
|
1922
|
+
settle(() => reject(new Error("Browser.close command timed out")));
|
|
1923
|
+
}, 3000);
|
|
1924
|
+
ws.on("open", () => {
|
|
1925
|
+
commandSent = true;
|
|
1926
|
+
ws.send(JSON.stringify({ id: 1, method: "Browser.close", params: {} }), (error) => {
|
|
1927
|
+
if (error) {
|
|
1928
|
+
settle(() => reject(error));
|
|
1929
|
+
}
|
|
1930
|
+
});
|
|
1931
|
+
});
|
|
1932
|
+
ws.on("message", (data) => {
|
|
1933
|
+
try {
|
|
1934
|
+
const message = JSON.parse(data.toString());
|
|
1935
|
+
if (message.id !== 1) {
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
const responseError = message.error;
|
|
1939
|
+
if (responseError) {
|
|
1940
|
+
settle(() => reject(new Error(responseError.message || "Browser.close failed")));
|
|
1941
|
+
return;
|
|
1942
|
+
}
|
|
1943
|
+
settle(resolve);
|
|
1944
|
+
}
|
|
1945
|
+
catch (error) {
|
|
1946
|
+
settle(() => reject(error instanceof Error ? error : new Error(String(error))));
|
|
1947
|
+
}
|
|
1948
|
+
});
|
|
1949
|
+
ws.on("error", (error) => {
|
|
1950
|
+
settle(() => reject(error));
|
|
1951
|
+
});
|
|
1952
|
+
ws.on("close", () => {
|
|
1953
|
+
if (commandSent) {
|
|
1954
|
+
settle(resolve);
|
|
1955
|
+
}
|
|
1956
|
+
else {
|
|
1957
|
+
settle(() => reject(new Error("Browser CDP websocket closed before Browser.close was sent")));
|
|
1958
|
+
}
|
|
1959
|
+
});
|
|
1960
|
+
});
|
|
1961
|
+
this.debugLog("Sent Browser.close command via browser CDP endpoint");
|
|
1962
|
+
}
|
|
1638
1963
|
async shutdown() {
|
|
1639
1964
|
this.isShuttingDown = true;
|
|
1640
|
-
|
|
1641
|
-
|
|
1965
|
+
let browserClosedCleanly = false;
|
|
1966
|
+
// Ask the browser process to exit first so Chrome doesn't think it crashed.
|
|
1967
|
+
if (this.connection) {
|
|
1642
1968
|
try {
|
|
1643
|
-
|
|
1644
|
-
await this.
|
|
1645
|
-
this.debugLog("Sent Page.close command");
|
|
1646
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1969
|
+
await this.sendBrowserCloseCommand();
|
|
1970
|
+
browserClosedCleanly = await this.waitForBrowserExit(5000);
|
|
1647
1971
|
}
|
|
1648
1972
|
catch (_e) {
|
|
1649
|
-
this.debugLog("
|
|
1973
|
+
this.debugLog("Browser.close failed, trying page/tab close fallback");
|
|
1650
1974
|
}
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1975
|
+
if (!browserClosedCleanly) {
|
|
1976
|
+
try {
|
|
1977
|
+
// Try to close the page
|
|
1978
|
+
await this.sendCDPCommand("Page.close");
|
|
1979
|
+
this.debugLog("Sent Page.close command");
|
|
1980
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1981
|
+
}
|
|
1982
|
+
catch (_e) {
|
|
1983
|
+
this.debugLog("Page.close failed, trying Target.closeTarget");
|
|
1984
|
+
}
|
|
1985
|
+
try {
|
|
1986
|
+
// Get the list of targets to find our specific tab
|
|
1987
|
+
const targets = (await this.sendCDPCommand("Target.getTargets"));
|
|
1988
|
+
this.debugLog(`Found ${targets.targetInfos?.length || 0} targets`);
|
|
1989
|
+
// Find our page target
|
|
1990
|
+
const pageTarget = targets.targetInfos?.find((t) => t.type === "page");
|
|
1991
|
+
if (pageTarget) {
|
|
1992
|
+
this.debugLog(`Closing page target: ${pageTarget.targetId}`);
|
|
1993
|
+
await this.sendCDPCommand("Target.closeTarget", {
|
|
1994
|
+
targetId: pageTarget.targetId
|
|
1995
|
+
});
|
|
1996
|
+
this.debugLog("Closed Chrome tab via CDP");
|
|
1997
|
+
}
|
|
1998
|
+
// Give it more time for the tab to close
|
|
1999
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2000
|
+
}
|
|
2001
|
+
catch (_e) {
|
|
2002
|
+
this.debugLog("Failed to close tab via CDP, will force close Chrome");
|
|
2003
|
+
}
|
|
2004
|
+
if (!browserClosedCleanly) {
|
|
2005
|
+
browserClosedCleanly = await this.waitForBrowserExit(1500);
|
|
1663
2006
|
}
|
|
1664
|
-
// Give it more time for the tab to close
|
|
1665
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1666
|
-
}
|
|
1667
|
-
catch (_e) {
|
|
1668
|
-
this.debugLog("Failed to close tab via CDP, will force close Chrome");
|
|
1669
2007
|
}
|
|
1670
2008
|
}
|
|
1671
2009
|
// Close CDP connection
|
|
@@ -1678,7 +2016,11 @@ export class CDPMonitor {
|
|
|
1678
2016
|
}
|
|
1679
2017
|
this.connection = null;
|
|
1680
2018
|
}
|
|
1681
|
-
|
|
2019
|
+
if (browserClosedCleanly) {
|
|
2020
|
+
this.chromePids.clear();
|
|
2021
|
+
return;
|
|
2022
|
+
}
|
|
2023
|
+
// Kill only the Chrome processes for THIS instance as a fallback.
|
|
1682
2024
|
await this.killInstanceChromeProcesses();
|
|
1683
2025
|
}
|
|
1684
2026
|
async killInstanceChromeProcesses() {
|