agent-dbg 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/.bin/ndbg +0 -0
- package/.claude/settings.local.json +21 -0
- package/.claude/skills/ndbg-debugger/ndbg-debugger/SKILL.md +116 -0
- package/.claude/skills/ndbg-debugger/ndbg-debugger/references/commands.md +173 -0
- package/CLAUDE.md +43 -0
- package/PROGRESS.md +261 -0
- package/README.md +67 -0
- package/biome.json +41 -0
- package/ndbg-spec.md +958 -0
- package/package.json +30 -0
- package/src/cdp/client.ts +198 -0
- package/src/cdp/types.ts +16 -0
- package/src/cli/parser.ts +287 -0
- package/src/cli/registry.ts +7 -0
- package/src/cli/types.ts +24 -0
- package/src/commands/attach.ts +47 -0
- package/src/commands/blackbox-ls.ts +38 -0
- package/src/commands/blackbox-rm.ts +57 -0
- package/src/commands/blackbox.ts +48 -0
- package/src/commands/break-ls.ts +57 -0
- package/src/commands/break-rm.ts +40 -0
- package/src/commands/break-toggle.ts +42 -0
- package/src/commands/break.ts +145 -0
- package/src/commands/breakable.ts +69 -0
- package/src/commands/catch.ts +38 -0
- package/src/commands/console.ts +61 -0
- package/src/commands/continue.ts +46 -0
- package/src/commands/eval.ts +70 -0
- package/src/commands/exceptions.ts +61 -0
- package/src/commands/hotpatch.ts +67 -0
- package/src/commands/launch.ts +69 -0
- package/src/commands/logpoint.ts +78 -0
- package/src/commands/pause.ts +46 -0
- package/src/commands/props.ts +77 -0
- package/src/commands/restart-frame.ts +36 -0
- package/src/commands/run-to.ts +70 -0
- package/src/commands/scripts.ts +57 -0
- package/src/commands/search.ts +73 -0
- package/src/commands/sessions.ts +71 -0
- package/src/commands/set-return.ts +49 -0
- package/src/commands/set.ts +61 -0
- package/src/commands/source.ts +59 -0
- package/src/commands/sourcemap.ts +66 -0
- package/src/commands/stack.ts +64 -0
- package/src/commands/state.ts +124 -0
- package/src/commands/status.ts +57 -0
- package/src/commands/step.ts +50 -0
- package/src/commands/stop.ts +27 -0
- package/src/commands/vars.ts +71 -0
- package/src/daemon/client.ts +147 -0
- package/src/daemon/entry.ts +242 -0
- package/src/daemon/paths.ts +26 -0
- package/src/daemon/server.ts +185 -0
- package/src/daemon/session-blackbox.ts +41 -0
- package/src/daemon/session-breakpoints.ts +492 -0
- package/src/daemon/session-execution.ts +121 -0
- package/src/daemon/session-inspection.ts +701 -0
- package/src/daemon/session-mutation.ts +197 -0
- package/src/daemon/session-state.ts +258 -0
- package/src/daemon/session.ts +938 -0
- package/src/daemon/spawn.ts +53 -0
- package/src/formatter/errors.ts +15 -0
- package/src/formatter/source.ts +74 -0
- package/src/formatter/stack.ts +70 -0
- package/src/formatter/values.ts +269 -0
- package/src/formatter/variables.ts +20 -0
- package/src/main.ts +45 -0
- package/src/protocol/messages.ts +316 -0
- package/src/refs/ref-table.ts +120 -0
- package/src/refs/resolver.ts +24 -0
- package/src/sourcemap/resolver.ts +318 -0
- package/tests/fixtures/async-app.js +34 -0
- package/tests/fixtures/console-app.js +12 -0
- package/tests/fixtures/error-app.js +28 -0
- package/tests/fixtures/exception-app.js +6 -0
- package/tests/fixtures/inspect-app.js +10 -0
- package/tests/fixtures/mutation-app.js +9 -0
- package/tests/fixtures/simple-app.js +50 -0
- package/tests/fixtures/step-app.js +13 -0
- package/tests/fixtures/ts-app/src/app.ts +21 -0
- package/tests/fixtures/ts-app/tsconfig.json +14 -0
- package/tests/integration/blackbox.test.ts +135 -0
- package/tests/integration/break-extras.test.ts +241 -0
- package/tests/integration/breakpoint.test.ts +217 -0
- package/tests/integration/console.test.ts +275 -0
- package/tests/integration/execution.test.ts +247 -0
- package/tests/integration/inspection.test.ts +311 -0
- package/tests/integration/mutation.test.ts +178 -0
- package/tests/integration/session.test.ts +223 -0
- package/tests/integration/source.test.ts +209 -0
- package/tests/integration/sourcemap.test.ts +214 -0
- package/tests/integration/state.test.ts +208 -0
- package/tests/unit/cdp-client.test.ts +422 -0
- package/tests/unit/daemon.test.ts +286 -0
- package/tests/unit/formatter.test.ts +716 -0
- package/tests/unit/parser.test.ts +105 -0
- package/tests/unit/refs.test.ts +383 -0
- package/tests/unit/sourcemap.test.ts +236 -0
- package/tsconfig.json +32 -0
|
@@ -0,0 +1,938 @@
|
|
|
1
|
+
import type { Subprocess } from "bun";
|
|
2
|
+
import type Protocol from "devtools-protocol/types/protocol.js";
|
|
3
|
+
import { CdpClient } from "../cdp/client.ts";
|
|
4
|
+
import type { RemoteObject } from "../formatter/values.ts";
|
|
5
|
+
import { formatValue } from "../formatter/values.ts";
|
|
6
|
+
import { RefTable } from "../refs/ref-table.ts";
|
|
7
|
+
import { SourceMapResolver } from "../sourcemap/resolver.ts";
|
|
8
|
+
import {
|
|
9
|
+
addBlackbox as addBlackboxImpl,
|
|
10
|
+
listBlackbox as listBlackboxImpl,
|
|
11
|
+
removeBlackbox as removeBlackboxImpl,
|
|
12
|
+
} from "./session-blackbox.ts";
|
|
13
|
+
import {
|
|
14
|
+
getBreakableLocations as getBreakableLocationsImpl,
|
|
15
|
+
listBreakpoints as listBreakpointsImpl,
|
|
16
|
+
removeAllBreakpoints as removeAllBreakpointsImpl,
|
|
17
|
+
removeBreakpoint as removeBreakpointImpl,
|
|
18
|
+
setBreakpoint as setBreakpointImpl,
|
|
19
|
+
setExceptionPause as setExceptionPauseImpl,
|
|
20
|
+
setLogpoint as setLogpointImpl,
|
|
21
|
+
toggleBreakpoint as toggleBreakpointImpl,
|
|
22
|
+
} from "./session-breakpoints.ts";
|
|
23
|
+
import {
|
|
24
|
+
continueExecution,
|
|
25
|
+
pauseExecution,
|
|
26
|
+
restartFrameExecution,
|
|
27
|
+
runToLocation,
|
|
28
|
+
stepExecution,
|
|
29
|
+
} from "./session-execution.ts";
|
|
30
|
+
import {
|
|
31
|
+
clearConsole as clearConsoleImpl,
|
|
32
|
+
evalExpression,
|
|
33
|
+
getConsoleMessages as getConsoleMessagesImpl,
|
|
34
|
+
getExceptions as getExceptionsImpl,
|
|
35
|
+
getProps as getPropsImpl,
|
|
36
|
+
getScripts as getScriptsImpl,
|
|
37
|
+
getSource as getSourceImpl,
|
|
38
|
+
getStack as getStackImpl,
|
|
39
|
+
getVars as getVarsImpl,
|
|
40
|
+
searchInScripts as searchInScriptsImpl,
|
|
41
|
+
} from "./session-inspection.ts";
|
|
42
|
+
import {
|
|
43
|
+
hotpatch as hotpatchImpl,
|
|
44
|
+
setReturnValue as setReturnValueImpl,
|
|
45
|
+
setVariable as setVariableImpl,
|
|
46
|
+
} from "./session-mutation.ts";
|
|
47
|
+
import { buildState as buildStateImpl } from "./session-state.ts";
|
|
48
|
+
|
|
49
|
+
export interface PauseInfo {
|
|
50
|
+
reason: string;
|
|
51
|
+
scriptId?: string;
|
|
52
|
+
url?: string;
|
|
53
|
+
line?: number;
|
|
54
|
+
column?: number;
|
|
55
|
+
callFrameCount?: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface StateOptions {
|
|
59
|
+
vars?: boolean;
|
|
60
|
+
stack?: boolean;
|
|
61
|
+
breakpoints?: boolean;
|
|
62
|
+
code?: boolean;
|
|
63
|
+
compact?: boolean;
|
|
64
|
+
depth?: number;
|
|
65
|
+
lines?: number;
|
|
66
|
+
frame?: string; // @fN ref
|
|
67
|
+
allScopes?: boolean;
|
|
68
|
+
generated?: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface StateSnapshot {
|
|
72
|
+
status: string; // "paused" | "running" | "idle"
|
|
73
|
+
reason?: string;
|
|
74
|
+
location?: { url: string; line: number; column?: number };
|
|
75
|
+
source?: { lines: Array<{ line: number; text: string; current?: boolean }> };
|
|
76
|
+
locals?: Array<{ ref: string; name: string; value: string }>;
|
|
77
|
+
stack?: Array<{
|
|
78
|
+
ref: string;
|
|
79
|
+
functionName: string;
|
|
80
|
+
file: string;
|
|
81
|
+
line: number;
|
|
82
|
+
column?: number;
|
|
83
|
+
isAsync?: boolean;
|
|
84
|
+
}>;
|
|
85
|
+
breakpointCount?: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface ConsoleMessage {
|
|
89
|
+
timestamp: number;
|
|
90
|
+
level: string; // "log" | "warn" | "error" | "info" | "debug" | "trace"
|
|
91
|
+
text: string;
|
|
92
|
+
args?: string[]; // formatted args
|
|
93
|
+
url?: string;
|
|
94
|
+
line?: number;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface ExceptionEntry {
|
|
98
|
+
timestamp: number;
|
|
99
|
+
text: string;
|
|
100
|
+
description?: string;
|
|
101
|
+
url?: string;
|
|
102
|
+
line?: number;
|
|
103
|
+
column?: number;
|
|
104
|
+
stackTrace?: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface ScriptInfo {
|
|
108
|
+
scriptId: string;
|
|
109
|
+
url: string;
|
|
110
|
+
sourceMapURL?: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface LaunchResult {
|
|
114
|
+
pid: number;
|
|
115
|
+
wsUrl: string;
|
|
116
|
+
paused: boolean;
|
|
117
|
+
pauseInfo?: PauseInfo;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface AttachResult {
|
|
121
|
+
wsUrl: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface SessionStatus {
|
|
125
|
+
session: string;
|
|
126
|
+
state: "idle" | "running" | "paused";
|
|
127
|
+
pid?: number;
|
|
128
|
+
wsUrl?: string;
|
|
129
|
+
pauseInfo?: PauseInfo;
|
|
130
|
+
uptime: number;
|
|
131
|
+
scriptCount: number;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const INSPECTOR_URL_REGEX = /Debugger listening on (wss?:\/\/\S+)/;
|
|
135
|
+
const INSPECTOR_TIMEOUT_MS = 5_000;
|
|
136
|
+
|
|
137
|
+
export class DebugSession {
|
|
138
|
+
cdp: CdpClient | null = null;
|
|
139
|
+
refs: RefTable = new RefTable();
|
|
140
|
+
sourceMapResolver: SourceMapResolver = new SourceMapResolver();
|
|
141
|
+
childProcess: Subprocess<"ignore", "pipe", "pipe"> | null = null;
|
|
142
|
+
state: "idle" | "running" | "paused" = "idle";
|
|
143
|
+
pauseInfo: PauseInfo | null = null;
|
|
144
|
+
pausedCallFrames: Protocol.Debugger.CallFrame[] = [];
|
|
145
|
+
scripts: Map<string, ScriptInfo> = new Map();
|
|
146
|
+
wsUrl: string | null = null;
|
|
147
|
+
startTime: number = Date.now();
|
|
148
|
+
session: string;
|
|
149
|
+
onProcessExit: (() => void) | null = null;
|
|
150
|
+
consoleMessages: Array<ConsoleMessage> = [];
|
|
151
|
+
exceptionEntries: Array<ExceptionEntry> = [];
|
|
152
|
+
blackboxPatterns: string[] = [];
|
|
153
|
+
disabledBreakpoints: Map<string, { breakpointId: string; meta: Record<string, unknown> }> =
|
|
154
|
+
new Map();
|
|
155
|
+
|
|
156
|
+
constructor(session: string) {
|
|
157
|
+
this.session = session;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ── Session lifecycle ─────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
async launch(
|
|
163
|
+
command: string[],
|
|
164
|
+
options: { brk?: boolean; port?: number } = {},
|
|
165
|
+
): Promise<LaunchResult> {
|
|
166
|
+
if (this.state !== "idle") {
|
|
167
|
+
throw new Error("Session already has an active debug target");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (command.length === 0) {
|
|
171
|
+
throw new Error("Command array must not be empty");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const brk = options.brk ?? true;
|
|
175
|
+
const port = options.port ?? 0;
|
|
176
|
+
const inspectFlag = brk ? `--inspect-brk=${port}` : `--inspect=${port}`;
|
|
177
|
+
|
|
178
|
+
// Build the args: inject inspect flag after the runtime (first element)
|
|
179
|
+
const runtime = command[0] as string;
|
|
180
|
+
const rest = command.slice(1);
|
|
181
|
+
const spawnArgs = [runtime, inspectFlag, ...rest];
|
|
182
|
+
|
|
183
|
+
const proc = Bun.spawn(spawnArgs, {
|
|
184
|
+
stdin: "ignore",
|
|
185
|
+
stdout: "pipe",
|
|
186
|
+
stderr: "pipe",
|
|
187
|
+
});
|
|
188
|
+
this.childProcess = proc;
|
|
189
|
+
|
|
190
|
+
// Monitor child process exit in the background
|
|
191
|
+
this.monitorProcessExit(proc);
|
|
192
|
+
|
|
193
|
+
// Read stderr to find the inspector URL
|
|
194
|
+
const wsUrl = await this.readInspectorUrl(proc.stderr);
|
|
195
|
+
this.wsUrl = wsUrl;
|
|
196
|
+
|
|
197
|
+
// Connect CDP
|
|
198
|
+
await this.connectCdp(wsUrl);
|
|
199
|
+
|
|
200
|
+
// If brk mode, ensure the session enters "paused" state.
|
|
201
|
+
// On older Node.js versions, Debugger.paused fires automatically after
|
|
202
|
+
// Debugger.enable. On newer versions (v24+), the initial --inspect-brk
|
|
203
|
+
// pause does not emit the event, so we request an explicit pause and then
|
|
204
|
+
// signal Runtime.runIfWaitingForDebugger so the process starts execution
|
|
205
|
+
// and immediately hits our pause request.
|
|
206
|
+
if (brk) {
|
|
207
|
+
await this.waitForBrkPause();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const result: LaunchResult = {
|
|
211
|
+
pid: proc.pid,
|
|
212
|
+
wsUrl,
|
|
213
|
+
paused: this.sessionState === "paused",
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
if (this.pauseInfo) {
|
|
217
|
+
result.pauseInfo = this.pauseInfo;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async attach(target: string): Promise<AttachResult> {
|
|
224
|
+
if (this.state !== "idle" && !this.cdp) {
|
|
225
|
+
throw new Error("Session already has an active debug target");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let wsUrl: string;
|
|
229
|
+
|
|
230
|
+
if (target.startsWith("ws://") || target.startsWith("wss://")) {
|
|
231
|
+
wsUrl = target;
|
|
232
|
+
} else {
|
|
233
|
+
// Treat as a port number
|
|
234
|
+
const port = parseInt(target, 10);
|
|
235
|
+
if (Number.isNaN(port) || port <= 0 || port > 65535) {
|
|
236
|
+
throw new Error(
|
|
237
|
+
`Invalid attach target: "${target}". Provide a ws:// URL or a port number.`,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
wsUrl = await this.discoverWsUrl(port);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
this.wsUrl = wsUrl;
|
|
244
|
+
await this.connectCdp(wsUrl);
|
|
245
|
+
|
|
246
|
+
return { wsUrl };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
getStatus(): SessionStatus {
|
|
250
|
+
const status: SessionStatus = {
|
|
251
|
+
session: this.session,
|
|
252
|
+
state: this.state,
|
|
253
|
+
uptime: Math.floor((Date.now() - this.startTime) / 1000),
|
|
254
|
+
scriptCount: this.scripts.size,
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
if (this.childProcess) {
|
|
258
|
+
status.pid = this.childProcess.pid;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (this.wsUrl) {
|
|
262
|
+
status.wsUrl = this.wsUrl;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (this.pauseInfo) {
|
|
266
|
+
// Source-map translate pauseInfo for display
|
|
267
|
+
const translated = { ...this.pauseInfo };
|
|
268
|
+
if (translated.scriptId && translated.line !== undefined) {
|
|
269
|
+
const resolved = this.resolveOriginalLocation(
|
|
270
|
+
translated.scriptId,
|
|
271
|
+
translated.line + 1, // pauseInfo.line is 0-based
|
|
272
|
+
translated.column ?? 0,
|
|
273
|
+
);
|
|
274
|
+
if (resolved) {
|
|
275
|
+
translated.url = resolved.url;
|
|
276
|
+
translated.line = resolved.line - 1; // back to 0-based for pauseInfo
|
|
277
|
+
if (resolved.column !== undefined) {
|
|
278
|
+
translated.column = resolved.column - 1;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
status.pauseInfo = translated;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return status;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async stop(): Promise<void> {
|
|
289
|
+
if (this.cdp) {
|
|
290
|
+
this.cdp.disconnect();
|
|
291
|
+
this.cdp = null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (this.childProcess) {
|
|
295
|
+
try {
|
|
296
|
+
this.childProcess.kill();
|
|
297
|
+
} catch {
|
|
298
|
+
// Process may already be dead
|
|
299
|
+
}
|
|
300
|
+
this.childProcess = null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
this.state = "idle";
|
|
304
|
+
this.pauseInfo = null;
|
|
305
|
+
this.wsUrl = null;
|
|
306
|
+
this.scripts.clear();
|
|
307
|
+
this.refs.clearAll();
|
|
308
|
+
this.consoleMessages = [];
|
|
309
|
+
this.exceptionEntries = [];
|
|
310
|
+
this.disabledBreakpoints.clear();
|
|
311
|
+
this.sourceMapResolver.clear();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
get sessionState(): "idle" | "running" | "paused" {
|
|
315
|
+
return this.state;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
get targetPid(): number | null {
|
|
319
|
+
return this.childProcess?.pid ?? null;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ── Delegated methods ─────────────────────────────────────────────
|
|
323
|
+
|
|
324
|
+
// State snapshot
|
|
325
|
+
async buildState(options: StateOptions = {}): Promise<StateSnapshot> {
|
|
326
|
+
return buildStateImpl(this, options);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Breakpoints
|
|
330
|
+
async setBreakpoint(
|
|
331
|
+
file: string,
|
|
332
|
+
line: number,
|
|
333
|
+
options?: { condition?: string; hitCount?: number; urlRegex?: string },
|
|
334
|
+
): Promise<{ ref: string; location: { url: string; line: number; column?: number } }> {
|
|
335
|
+
return setBreakpointImpl(this, file, line, options);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async removeBreakpoint(ref: string): Promise<void> {
|
|
339
|
+
return removeBreakpointImpl(this, ref);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async removeAllBreakpoints(): Promise<void> {
|
|
343
|
+
return removeAllBreakpointsImpl(this);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
listBreakpoints(): Array<{
|
|
347
|
+
ref: string;
|
|
348
|
+
type: "BP" | "LP";
|
|
349
|
+
url: string;
|
|
350
|
+
line: number;
|
|
351
|
+
column?: number;
|
|
352
|
+
condition?: string;
|
|
353
|
+
hitCount?: number;
|
|
354
|
+
template?: string;
|
|
355
|
+
disabled?: boolean;
|
|
356
|
+
originalUrl?: string;
|
|
357
|
+
originalLine?: number;
|
|
358
|
+
}> {
|
|
359
|
+
return listBreakpointsImpl(this);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async toggleBreakpoint(ref: string): Promise<{ ref: string; state: "enabled" | "disabled" }> {
|
|
363
|
+
return toggleBreakpointImpl(this, ref);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async getBreakableLocations(
|
|
367
|
+
file: string,
|
|
368
|
+
startLine: number,
|
|
369
|
+
endLine: number,
|
|
370
|
+
): Promise<Array<{ line: number; column: number }>> {
|
|
371
|
+
return getBreakableLocationsImpl(this, file, startLine, endLine);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async setLogpoint(
|
|
375
|
+
file: string,
|
|
376
|
+
line: number,
|
|
377
|
+
template: string,
|
|
378
|
+
options?: { condition?: string; maxEmissions?: number },
|
|
379
|
+
): Promise<{ ref: string; location: { url: string; line: number; column?: number } }> {
|
|
380
|
+
return setLogpointImpl(this, file, line, template, options);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async setExceptionPause(mode: "all" | "uncaught" | "caught" | "none"): Promise<void> {
|
|
384
|
+
return setExceptionPauseImpl(this, mode);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Inspection
|
|
388
|
+
async eval(
|
|
389
|
+
expression: string,
|
|
390
|
+
options: {
|
|
391
|
+
frame?: string;
|
|
392
|
+
awaitPromise?: boolean;
|
|
393
|
+
throwOnSideEffect?: boolean;
|
|
394
|
+
timeout?: number;
|
|
395
|
+
} = {},
|
|
396
|
+
): Promise<{
|
|
397
|
+
ref: string;
|
|
398
|
+
type: string;
|
|
399
|
+
value: string;
|
|
400
|
+
objectId?: string;
|
|
401
|
+
}> {
|
|
402
|
+
return evalExpression(this, expression, options);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async getVars(
|
|
406
|
+
options: { frame?: string; names?: string[]; allScopes?: boolean } = {},
|
|
407
|
+
): Promise<Array<{ ref: string; name: string; type: string; value: string }>> {
|
|
408
|
+
return getVarsImpl(this, options);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async getProps(
|
|
412
|
+
ref: string,
|
|
413
|
+
options: {
|
|
414
|
+
own?: boolean;
|
|
415
|
+
internal?: boolean;
|
|
416
|
+
depth?: number;
|
|
417
|
+
} = {},
|
|
418
|
+
): Promise<
|
|
419
|
+
Array<{
|
|
420
|
+
ref?: string;
|
|
421
|
+
name: string;
|
|
422
|
+
type: string;
|
|
423
|
+
value: string;
|
|
424
|
+
isOwn?: boolean;
|
|
425
|
+
isAccessor?: boolean;
|
|
426
|
+
}>
|
|
427
|
+
> {
|
|
428
|
+
return getPropsImpl(this, ref, options);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async getSource(
|
|
432
|
+
options: { file?: string; lines?: number; all?: boolean; generated?: boolean } = {},
|
|
433
|
+
): Promise<{
|
|
434
|
+
url: string;
|
|
435
|
+
lines: Array<{ line: number; text: string; current?: boolean }>;
|
|
436
|
+
}> {
|
|
437
|
+
return getSourceImpl(this, options);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
getScripts(filter?: string): Array<{ scriptId: string; url: string; sourceMapURL?: string }> {
|
|
441
|
+
return getScriptsImpl(this, filter);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
getStack(options: { asyncDepth?: number; generated?: boolean } = {}): Array<{
|
|
445
|
+
ref: string;
|
|
446
|
+
functionName: string;
|
|
447
|
+
file: string;
|
|
448
|
+
line: number;
|
|
449
|
+
column?: number;
|
|
450
|
+
isAsync?: boolean;
|
|
451
|
+
}> {
|
|
452
|
+
return getStackImpl(this, options);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async searchInScripts(
|
|
456
|
+
query: string,
|
|
457
|
+
options: {
|
|
458
|
+
scriptId?: string;
|
|
459
|
+
isRegex?: boolean;
|
|
460
|
+
caseSensitive?: boolean;
|
|
461
|
+
} = {},
|
|
462
|
+
): Promise<Array<{ url: string; line: number; column: number; content: string }>> {
|
|
463
|
+
return searchInScriptsImpl(this, query, options);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
getConsoleMessages(
|
|
467
|
+
options: { level?: string; since?: number; clear?: boolean } = {},
|
|
468
|
+
): ConsoleMessage[] {
|
|
469
|
+
return getConsoleMessagesImpl(this, options);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
getExceptions(options: { since?: number } = {}): ExceptionEntry[] {
|
|
473
|
+
return getExceptionsImpl(this, options);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
clearConsole(): void {
|
|
477
|
+
clearConsoleImpl(this);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Mutation
|
|
481
|
+
async setVariable(
|
|
482
|
+
varName: string,
|
|
483
|
+
value: string,
|
|
484
|
+
options: { frame?: string } = {},
|
|
485
|
+
): Promise<{ name: string; oldValue?: string; newValue: string; type: string }> {
|
|
486
|
+
return setVariableImpl(this, varName, value, options);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
async setReturnValue(value: string): Promise<{ value: string; type: string }> {
|
|
490
|
+
return setReturnValueImpl(this, value);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
async hotpatch(
|
|
494
|
+
file: string,
|
|
495
|
+
newSource: string,
|
|
496
|
+
options: { dryRun?: boolean } = {},
|
|
497
|
+
): Promise<{ status: string; callFrames?: unknown[]; exceptionDetails?: unknown }> {
|
|
498
|
+
return hotpatchImpl(this, file, newSource, options);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Execution control
|
|
502
|
+
async continue(): Promise<void> {
|
|
503
|
+
return continueExecution(this);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async step(mode: "over" | "into" | "out"): Promise<void> {
|
|
507
|
+
return stepExecution(this, mode);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
async pause(): Promise<void> {
|
|
511
|
+
return pauseExecution(this);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async runTo(file: string, line: number): Promise<void> {
|
|
515
|
+
return runToLocation(this, file, line);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
async restartFrame(frameRef?: string): Promise<{ status: string }> {
|
|
519
|
+
return restartFrameExecution(this, frameRef);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Blackboxing
|
|
523
|
+
async addBlackbox(patterns: string[]): Promise<string[]> {
|
|
524
|
+
return addBlackboxImpl(this, patterns);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
listBlackbox(): string[] {
|
|
528
|
+
return listBlackboxImpl(this);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async removeBlackbox(patterns: string[]): Promise<string[]> {
|
|
532
|
+
return removeBlackboxImpl(this, patterns);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// ── Public helpers (used by extracted modules) ─────────────────────
|
|
536
|
+
|
|
537
|
+
processEvalResult(
|
|
538
|
+
result: {
|
|
539
|
+
result: Protocol.Runtime.RemoteObject;
|
|
540
|
+
exceptionDetails?: Protocol.Runtime.ExceptionDetails;
|
|
541
|
+
},
|
|
542
|
+
expression: string,
|
|
543
|
+
): { ref: string; type: string; value: string; objectId?: string } {
|
|
544
|
+
const evalResult = result.result as RemoteObject | undefined;
|
|
545
|
+
const exceptionDetails = result.exceptionDetails;
|
|
546
|
+
|
|
547
|
+
if (exceptionDetails) {
|
|
548
|
+
const exception = exceptionDetails.exception as RemoteObject | undefined;
|
|
549
|
+
const errorText = exception
|
|
550
|
+
? formatValue(exception)
|
|
551
|
+
: (exceptionDetails.text ?? "Evaluation error");
|
|
552
|
+
throw new Error(errorText);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (!evalResult) {
|
|
556
|
+
throw new Error("No result from evaluation");
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const remoteId = (evalResult.objectId as string) ?? `eval:${Date.now()}`;
|
|
560
|
+
const ref = this.refs.addVar(remoteId, expression);
|
|
561
|
+
const resultData: {
|
|
562
|
+
ref: string;
|
|
563
|
+
type: string;
|
|
564
|
+
value: string;
|
|
565
|
+
objectId?: string;
|
|
566
|
+
} = {
|
|
567
|
+
ref,
|
|
568
|
+
type: evalResult.type,
|
|
569
|
+
value: formatValue(evalResult),
|
|
570
|
+
};
|
|
571
|
+
if (evalResult.objectId) {
|
|
572
|
+
resultData.objectId = evalResult.objectId;
|
|
573
|
+
}
|
|
574
|
+
return resultData;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
findScriptUrl(file: string): string | null {
|
|
578
|
+
// Try exact suffix match first
|
|
579
|
+
for (const script of this.scripts.values()) {
|
|
580
|
+
if (script.url && script.url.endsWith(file)) {
|
|
581
|
+
return script.url;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
// Try matching after stripping file:// prefix
|
|
585
|
+
for (const script of this.scripts.values()) {
|
|
586
|
+
if (!script.url) continue;
|
|
587
|
+
const stripped = script.url.startsWith("file://") ? script.url.slice(7) : script.url;
|
|
588
|
+
if (stripped.endsWith(file)) {
|
|
589
|
+
return script.url;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
// Try matching just the basename
|
|
593
|
+
const needle = file.includes("/") ? file : `/${file}`;
|
|
594
|
+
for (const script of this.scripts.values()) {
|
|
595
|
+
if (!script.url) continue;
|
|
596
|
+
const stripped = script.url.startsWith("file://") ? script.url.slice(7) : script.url;
|
|
597
|
+
if (stripped.endsWith(needle)) {
|
|
598
|
+
return script.url;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
// Fallback: try source map resolver for .ts files etc.
|
|
602
|
+
const smMatch = this.sourceMapResolver.findScriptForSource(file);
|
|
603
|
+
if (smMatch) {
|
|
604
|
+
return smMatch.url;
|
|
605
|
+
}
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Creates a promise that resolves when the next `Debugger.paused` event
|
|
611
|
+
* fires, the process exits, or the timeout expires. Must be created
|
|
612
|
+
* BEFORE sending the CDP command that triggers execution so we don't
|
|
613
|
+
* miss events. Does NOT check current state — the caller is about to
|
|
614
|
+
* send a resume/step command.
|
|
615
|
+
*/
|
|
616
|
+
createPauseWaiter(timeoutMs = 30_000): Promise<void> {
|
|
617
|
+
return new Promise<void>((resolve) => {
|
|
618
|
+
let settled = false;
|
|
619
|
+
|
|
620
|
+
const settle = () => {
|
|
621
|
+
if (settled) return;
|
|
622
|
+
settled = true;
|
|
623
|
+
clearTimeout(timer);
|
|
624
|
+
clearInterval(pollTimer);
|
|
625
|
+
this.cdp?.off("Debugger.paused", handler);
|
|
626
|
+
this.onProcessExit = null;
|
|
627
|
+
resolve();
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
const timer = setTimeout(() => {
|
|
631
|
+
// Don't reject — the process is still running, just not paused yet
|
|
632
|
+
settle();
|
|
633
|
+
}, timeoutMs);
|
|
634
|
+
|
|
635
|
+
const handler = () => {
|
|
636
|
+
settle();
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
// Poll as a fallback in case the event/callback is missed
|
|
640
|
+
// (e.g., process exits and monitorProcessExit runs before
|
|
641
|
+
// onProcessExit is set, or CDP disconnects clearing listeners)
|
|
642
|
+
const pollTimer = setInterval(() => {
|
|
643
|
+
if (this.isPaused() || this.state === "idle") {
|
|
644
|
+
settle();
|
|
645
|
+
}
|
|
646
|
+
}, 100);
|
|
647
|
+
|
|
648
|
+
this.cdp?.on("Debugger.paused", handler);
|
|
649
|
+
// Also resolve if the process exits during execution
|
|
650
|
+
this.onProcessExit = settle;
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
buildBreakpointCondition(condition?: string, hitCount?: number): string | undefined {
|
|
655
|
+
if (hitCount && hitCount > 0) {
|
|
656
|
+
const countVar = `__ndbg_bp_count_${Date.now()}`;
|
|
657
|
+
const hitExpr = `(typeof ${countVar} === "undefined" ? (${countVar} = 1) : ++${countVar}) >= ${hitCount}`;
|
|
658
|
+
if (condition) {
|
|
659
|
+
return `(${hitExpr}) && (${condition})`;
|
|
660
|
+
}
|
|
661
|
+
return hitExpr;
|
|
662
|
+
}
|
|
663
|
+
return condition;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Resolve a generated location to its original source-mapped location.
|
|
668
|
+
* Option A: when toOriginal returns null but the script has a source map,
|
|
669
|
+
* still return the original source URL (with the generated line number).
|
|
670
|
+
*/
|
|
671
|
+
resolveOriginalLocation(
|
|
672
|
+
scriptId: string,
|
|
673
|
+
line1Based: number,
|
|
674
|
+
column: number,
|
|
675
|
+
): { url: string; line: number; column?: number } | null {
|
|
676
|
+
const original = this.sourceMapResolver.toOriginal(scriptId, line1Based, column);
|
|
677
|
+
if (original) {
|
|
678
|
+
return { url: original.source, line: original.line, column: original.column + 1 };
|
|
679
|
+
}
|
|
680
|
+
// Fallback: script has a source map but this line has no mapping
|
|
681
|
+
const primaryUrl = this.sourceMapResolver.getScriptOriginalUrl(scriptId);
|
|
682
|
+
if (primaryUrl) {
|
|
683
|
+
return { url: primaryUrl, line: line1Based };
|
|
684
|
+
}
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
isPaused(): boolean {
|
|
689
|
+
return this.state === "paused";
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// ── Private helpers ───────────────────────────────────────────────
|
|
693
|
+
|
|
694
|
+
private async waitForBrkPause(): Promise<void> {
|
|
695
|
+
// Give the Debugger.paused event a moment to arrive (older Node.js)
|
|
696
|
+
if (!this.isPaused()) {
|
|
697
|
+
await Bun.sleep(100);
|
|
698
|
+
}
|
|
699
|
+
// On Node.js v24+, --inspect-brk does not emit Debugger.paused when the
|
|
700
|
+
// debugger connects after the process is already paused. We request an
|
|
701
|
+
// explicit pause and then signal Runtime.runIfWaitingForDebugger so the
|
|
702
|
+
// process starts execution and immediately hits our pause request.
|
|
703
|
+
if (!this.isPaused() && this.cdp) {
|
|
704
|
+
await this.cdp.send("Debugger.pause");
|
|
705
|
+
await this.cdp.send("Runtime.runIfWaitingForDebugger");
|
|
706
|
+
const deadline = Date.now() + 2_000;
|
|
707
|
+
while (!this.isPaused() && Date.now() < deadline) {
|
|
708
|
+
await Bun.sleep(50);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
// On Node.js v24+, the initial --inspect-brk pause lands in an internal
|
|
712
|
+
// bootstrap module (node:internal/...) rather than the user script.
|
|
713
|
+
// Resume past internal pauses until we reach user code.
|
|
714
|
+
let skips = 0;
|
|
715
|
+
while (this.isPaused() && this.cdp && this.pauseInfo?.url?.startsWith("node:") && skips < 5) {
|
|
716
|
+
skips++;
|
|
717
|
+
const waiter = this.createPauseWaiter(5_000);
|
|
718
|
+
await this.cdp.send("Debugger.resume");
|
|
719
|
+
await waiter;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
private async connectCdp(wsUrl: string): Promise<void> {
|
|
724
|
+
const cdp = await CdpClient.connect(wsUrl);
|
|
725
|
+
this.cdp = cdp;
|
|
726
|
+
|
|
727
|
+
// Set up event handlers before enabling domains so we don't miss any events
|
|
728
|
+
this.setupCdpEventHandlers(cdp);
|
|
729
|
+
|
|
730
|
+
await cdp.enableDomains();
|
|
731
|
+
|
|
732
|
+
// Re-apply blackbox patterns if any exist
|
|
733
|
+
if (this.blackboxPatterns.length > 0) {
|
|
734
|
+
await cdp.send("Debugger.setBlackboxPatterns", {
|
|
735
|
+
patterns: this.blackboxPatterns,
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Update state to running if not already paused
|
|
740
|
+
if (this.state === "idle") {
|
|
741
|
+
this.state = "running";
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
private setupCdpEventHandlers(cdp: CdpClient): void {
|
|
746
|
+
cdp.on("Debugger.paused", (p) => {
|
|
747
|
+
this.state = "paused";
|
|
748
|
+
const callFrames = p.callFrames;
|
|
749
|
+
this.pausedCallFrames = callFrames ?? [];
|
|
750
|
+
const topFrame = callFrames?.[0];
|
|
751
|
+
const location = topFrame?.location;
|
|
752
|
+
const scriptId = location?.scriptId;
|
|
753
|
+
const url = scriptId ? this.scripts.get(scriptId)?.url : undefined;
|
|
754
|
+
|
|
755
|
+
this.pauseInfo = {
|
|
756
|
+
reason: p.reason ?? "unknown",
|
|
757
|
+
scriptId,
|
|
758
|
+
url,
|
|
759
|
+
line: location?.lineNumber,
|
|
760
|
+
column: location?.columnNumber,
|
|
761
|
+
callFrameCount: callFrames?.length,
|
|
762
|
+
};
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
cdp.on("Debugger.resumed", () => {
|
|
766
|
+
this.state = "running";
|
|
767
|
+
this.pauseInfo = null;
|
|
768
|
+
this.pausedCallFrames = [];
|
|
769
|
+
this.refs.clearVolatile();
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
cdp.on("Debugger.scriptParsed", (p) => {
|
|
773
|
+
const scriptId = p.scriptId;
|
|
774
|
+
if (scriptId) {
|
|
775
|
+
const info: ScriptInfo = {
|
|
776
|
+
scriptId,
|
|
777
|
+
url: p.url ?? "",
|
|
778
|
+
};
|
|
779
|
+
const sourceMapURL = p.sourceMapURL;
|
|
780
|
+
if (sourceMapURL) {
|
|
781
|
+
info.sourceMapURL = sourceMapURL;
|
|
782
|
+
// Load source map asynchronously (fire-and-forget)
|
|
783
|
+
this.sourceMapResolver.loadSourceMap(scriptId, info.url, sourceMapURL).catch(() => {});
|
|
784
|
+
}
|
|
785
|
+
this.scripts.set(scriptId, info);
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
cdp.on("Runtime.executionContextDestroyed", () => {
|
|
790
|
+
// The main execution context has been destroyed — the script has
|
|
791
|
+
// finished. The Node.js process may stay alive because the
|
|
792
|
+
// inspector connection keeps the event loop running, but debugging
|
|
793
|
+
// is effectively over.
|
|
794
|
+
this.state = "idle";
|
|
795
|
+
this.pauseInfo = null;
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
cdp.on("Runtime.consoleAPICalled", (p) => {
|
|
799
|
+
const type = p.type ?? "log";
|
|
800
|
+
const args = p.args ?? [];
|
|
801
|
+
// Format each arg using formatValue
|
|
802
|
+
const formattedArgs = args.map((a) => formatValue(a as unknown as RemoteObject));
|
|
803
|
+
const text = formattedArgs.join(" ");
|
|
804
|
+
// Get stack trace info if available
|
|
805
|
+
const stackTrace = p.stackTrace;
|
|
806
|
+
const eventCallFrames = stackTrace?.callFrames;
|
|
807
|
+
const topFrame = eventCallFrames?.[0];
|
|
808
|
+
const msg: ConsoleMessage = {
|
|
809
|
+
timestamp: Date.now(),
|
|
810
|
+
level: type,
|
|
811
|
+
text,
|
|
812
|
+
args: formattedArgs,
|
|
813
|
+
url: topFrame?.url,
|
|
814
|
+
line: topFrame?.lineNumber !== undefined ? topFrame.lineNumber + 1 : undefined,
|
|
815
|
+
};
|
|
816
|
+
this.consoleMessages.push(msg);
|
|
817
|
+
if (this.consoleMessages.length > 1000) {
|
|
818
|
+
this.consoleMessages.shift();
|
|
819
|
+
}
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
cdp.on("Runtime.exceptionThrown", (p) => {
|
|
823
|
+
const details = p.exceptionDetails;
|
|
824
|
+
if (!details) return;
|
|
825
|
+
const exception = details.exception;
|
|
826
|
+
const entry: ExceptionEntry = {
|
|
827
|
+
timestamp: Date.now(),
|
|
828
|
+
text: details.text ?? "Exception",
|
|
829
|
+
description: exception?.description,
|
|
830
|
+
url: details.url,
|
|
831
|
+
line: details.lineNumber !== undefined ? details.lineNumber + 1 : undefined,
|
|
832
|
+
column: details.columnNumber !== undefined ? details.columnNumber + 1 : undefined,
|
|
833
|
+
};
|
|
834
|
+
// Extract stack trace string
|
|
835
|
+
const stackTrace = details.stackTrace;
|
|
836
|
+
if (stackTrace?.callFrames) {
|
|
837
|
+
const frames = stackTrace.callFrames;
|
|
838
|
+
entry.stackTrace = frames
|
|
839
|
+
.map((f) => {
|
|
840
|
+
const fn = f.functionName || "(anonymous)";
|
|
841
|
+
const frameUrl = f.url;
|
|
842
|
+
const frameLine = f.lineNumber + 1;
|
|
843
|
+
return ` at ${fn} (${frameUrl}:${frameLine})`;
|
|
844
|
+
})
|
|
845
|
+
.join("\n");
|
|
846
|
+
}
|
|
847
|
+
this.exceptionEntries.push(entry);
|
|
848
|
+
if (this.exceptionEntries.length > 1000) {
|
|
849
|
+
this.exceptionEntries.shift();
|
|
850
|
+
}
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
private monitorProcessExit(proc: Subprocess<"ignore", "pipe", "pipe">): void {
|
|
855
|
+
proc.exited
|
|
856
|
+
.then(() => {
|
|
857
|
+
// Child process has exited
|
|
858
|
+
this.childProcess = null;
|
|
859
|
+
if (this.cdp) {
|
|
860
|
+
this.cdp.disconnect();
|
|
861
|
+
this.cdp = null;
|
|
862
|
+
}
|
|
863
|
+
this.state = "idle";
|
|
864
|
+
this.pauseInfo = null;
|
|
865
|
+
this.onProcessExit?.();
|
|
866
|
+
})
|
|
867
|
+
.catch(() => {
|
|
868
|
+
// Error waiting for exit, treat as exited
|
|
869
|
+
this.childProcess = null;
|
|
870
|
+
this.state = "idle";
|
|
871
|
+
this.pauseInfo = null;
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
private async readInspectorUrl(stderr: ReadableStream<Uint8Array>): Promise<string> {
|
|
876
|
+
const reader = stderr.getReader();
|
|
877
|
+
const decoder = new TextDecoder();
|
|
878
|
+
let accumulated = "";
|
|
879
|
+
|
|
880
|
+
const timeout = setTimeout(() => {
|
|
881
|
+
reader.cancel().catch(() => {});
|
|
882
|
+
}, INSPECTOR_TIMEOUT_MS);
|
|
883
|
+
|
|
884
|
+
try {
|
|
885
|
+
while (true) {
|
|
886
|
+
const { done, value } = await reader.read();
|
|
887
|
+
if (done) {
|
|
888
|
+
break;
|
|
889
|
+
}
|
|
890
|
+
accumulated += decoder.decode(value, { stream: true });
|
|
891
|
+
|
|
892
|
+
const match = INSPECTOR_URL_REGEX.exec(accumulated);
|
|
893
|
+
if (match?.[1]) {
|
|
894
|
+
clearTimeout(timeout);
|
|
895
|
+
// Release the reader so the stream is not locked
|
|
896
|
+
reader.releaseLock();
|
|
897
|
+
return match[1];
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
} catch {
|
|
901
|
+
// Reader was cancelled (timeout) or stream errored
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
clearTimeout(timeout);
|
|
905
|
+
throw new Error(
|
|
906
|
+
`Failed to detect inspector URL within ${INSPECTOR_TIMEOUT_MS}ms. Stderr: ${accumulated.slice(0, 500)}`,
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
private async discoverWsUrl(port: number): Promise<string> {
|
|
911
|
+
const url = `http://127.0.0.1:${port}/json`;
|
|
912
|
+
let response: Response;
|
|
913
|
+
try {
|
|
914
|
+
response = await fetch(url);
|
|
915
|
+
} catch (err) {
|
|
916
|
+
throw new Error(
|
|
917
|
+
`Cannot connect to inspector at port ${port}: ${err instanceof Error ? err.message : String(err)}`,
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
if (!response.ok) {
|
|
922
|
+
throw new Error(`Inspector at port ${port} returned HTTP ${response.status}`);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
const targets = (await response.json()) as Array<Record<string, unknown>>;
|
|
926
|
+
const target = targets[0];
|
|
927
|
+
if (!target) {
|
|
928
|
+
throw new Error(`No debug targets found at port ${port}`);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
const wsUrl = target.webSocketDebuggerUrl as string | undefined;
|
|
932
|
+
if (!wsUrl) {
|
|
933
|
+
throw new Error(`Debug target at port ${port} has no webSocketDebuggerUrl`);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
return wsUrl;
|
|
937
|
+
}
|
|
938
|
+
}
|