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,492 @@
|
|
|
1
|
+
import type Protocol from "devtools-protocol/types/protocol.js";
|
|
2
|
+
import type { DebugSession } from "./session.ts";
|
|
3
|
+
|
|
4
|
+
export async function setBreakpoint(
|
|
5
|
+
session: DebugSession,
|
|
6
|
+
file: string,
|
|
7
|
+
line: number,
|
|
8
|
+
options?: { condition?: string; hitCount?: number; urlRegex?: string },
|
|
9
|
+
): Promise<{ ref: string; location: { url: string; line: number; column?: number } }> {
|
|
10
|
+
if (!session.cdp) {
|
|
11
|
+
throw new Error("No active debug session");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const condition = session.buildBreakpointCondition(options?.condition, options?.hitCount);
|
|
15
|
+
|
|
16
|
+
// Try source map translation (.ts → .js) before setting breakpoint
|
|
17
|
+
let originalFile: string | null = null;
|
|
18
|
+
let originalLine: number | null = null;
|
|
19
|
+
let actualLine = line;
|
|
20
|
+
let actualFile = file;
|
|
21
|
+
|
|
22
|
+
if (!options?.urlRegex) {
|
|
23
|
+
const generated = session.sourceMapResolver.toGenerated(file, line, 0);
|
|
24
|
+
if (generated) {
|
|
25
|
+
originalFile = file;
|
|
26
|
+
originalLine = line;
|
|
27
|
+
actualLine = generated.line;
|
|
28
|
+
// Find the URL of the generated script
|
|
29
|
+
const scriptInfo = session.scripts.get(generated.scriptId);
|
|
30
|
+
if (scriptInfo) {
|
|
31
|
+
actualFile = scriptInfo.url;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const params: Protocol.Debugger.SetBreakpointByUrlRequest = {
|
|
37
|
+
lineNumber: actualLine - 1, // CDP uses 0-based lines
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
let url: string | null = null;
|
|
41
|
+
if (options?.urlRegex) {
|
|
42
|
+
// Use urlRegex directly without resolving file path
|
|
43
|
+
params.urlRegex = options.urlRegex;
|
|
44
|
+
} else {
|
|
45
|
+
url = session.findScriptUrl(actualFile);
|
|
46
|
+
if (url) {
|
|
47
|
+
params.url = url;
|
|
48
|
+
} else {
|
|
49
|
+
// Fall back to urlRegex to match partial paths
|
|
50
|
+
params.urlRegex = `${actualFile.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (condition) {
|
|
54
|
+
params.condition = condition;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const r = await session.cdp.send("Debugger.setBreakpointByUrl", params);
|
|
58
|
+
|
|
59
|
+
const loc = r.locations[0];
|
|
60
|
+
const resolvedUrl = originalFile ?? url ?? file;
|
|
61
|
+
const resolvedLine = originalLine ?? (loc ? loc.lineNumber + 1 : line); // Convert back to 1-based
|
|
62
|
+
const resolvedColumn = loc?.columnNumber;
|
|
63
|
+
|
|
64
|
+
const meta: Record<string, unknown> = {
|
|
65
|
+
url: resolvedUrl,
|
|
66
|
+
line: resolvedLine,
|
|
67
|
+
};
|
|
68
|
+
if (originalFile) {
|
|
69
|
+
meta.originalUrl = originalFile;
|
|
70
|
+
meta.originalLine = originalLine;
|
|
71
|
+
meta.generatedUrl = url ?? actualFile;
|
|
72
|
+
meta.generatedLine = loc ? loc.lineNumber + 1 : actualLine;
|
|
73
|
+
}
|
|
74
|
+
if (resolvedColumn !== undefined) {
|
|
75
|
+
meta.column = resolvedColumn;
|
|
76
|
+
}
|
|
77
|
+
if (options?.condition) {
|
|
78
|
+
meta.condition = options.condition;
|
|
79
|
+
}
|
|
80
|
+
if (options?.hitCount) {
|
|
81
|
+
meta.hitCount = options.hitCount;
|
|
82
|
+
}
|
|
83
|
+
if (options?.urlRegex) {
|
|
84
|
+
meta.urlRegex = options.urlRegex;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const ref = session.refs.addBreakpoint(r.breakpointId, meta);
|
|
88
|
+
|
|
89
|
+
const location: { url: string; line: number; column?: number } = {
|
|
90
|
+
url: resolvedUrl,
|
|
91
|
+
line: resolvedLine,
|
|
92
|
+
};
|
|
93
|
+
if (resolvedColumn !== undefined) {
|
|
94
|
+
location.column = resolvedColumn;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return { ref, location };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function removeBreakpoint(session: DebugSession, ref: string): Promise<void> {
|
|
101
|
+
if (!session.cdp) {
|
|
102
|
+
throw new Error("No active debug session");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const entry = session.refs.resolve(ref);
|
|
106
|
+
if (!entry) {
|
|
107
|
+
throw new Error(`Unknown ref: ${ref}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (entry.type !== "BP" && entry.type !== "LP") {
|
|
111
|
+
throw new Error(`Ref ${ref} is not a breakpoint or logpoint`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
await session.cdp.send("Debugger.removeBreakpoint", {
|
|
115
|
+
breakpointId: entry.remoteId,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
session.refs.remove(ref);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function removeAllBreakpoints(session: DebugSession): Promise<void> {
|
|
122
|
+
if (!session.cdp) {
|
|
123
|
+
throw new Error("No active debug session");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const bps = session.refs.list("BP");
|
|
127
|
+
const lps = session.refs.list("LP");
|
|
128
|
+
const all = [...bps, ...lps];
|
|
129
|
+
|
|
130
|
+
for (const entry of all) {
|
|
131
|
+
await session.cdp.send("Debugger.removeBreakpoint", {
|
|
132
|
+
breakpointId: entry.remoteId,
|
|
133
|
+
});
|
|
134
|
+
session.refs.remove(entry.ref);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function listBreakpoints(session: DebugSession): Array<{
|
|
139
|
+
ref: string;
|
|
140
|
+
type: "BP" | "LP";
|
|
141
|
+
url: string;
|
|
142
|
+
line: number;
|
|
143
|
+
column?: number;
|
|
144
|
+
condition?: string;
|
|
145
|
+
hitCount?: number;
|
|
146
|
+
template?: string;
|
|
147
|
+
disabled?: boolean;
|
|
148
|
+
originalUrl?: string;
|
|
149
|
+
originalLine?: number;
|
|
150
|
+
}> {
|
|
151
|
+
const bps = session.refs.list("BP");
|
|
152
|
+
const lps = session.refs.list("LP");
|
|
153
|
+
const all = [...bps, ...lps];
|
|
154
|
+
|
|
155
|
+
const results: Array<{
|
|
156
|
+
ref: string;
|
|
157
|
+
type: "BP" | "LP";
|
|
158
|
+
url: string;
|
|
159
|
+
line: number;
|
|
160
|
+
column?: number;
|
|
161
|
+
condition?: string;
|
|
162
|
+
hitCount?: number;
|
|
163
|
+
template?: string;
|
|
164
|
+
disabled?: boolean;
|
|
165
|
+
originalUrl?: string;
|
|
166
|
+
originalLine?: number;
|
|
167
|
+
}> = all.map((entry) => {
|
|
168
|
+
const meta = entry.meta ?? {};
|
|
169
|
+
const item: {
|
|
170
|
+
ref: string;
|
|
171
|
+
type: "BP" | "LP";
|
|
172
|
+
url: string;
|
|
173
|
+
line: number;
|
|
174
|
+
column?: number;
|
|
175
|
+
condition?: string;
|
|
176
|
+
hitCount?: number;
|
|
177
|
+
template?: string;
|
|
178
|
+
disabled?: boolean;
|
|
179
|
+
originalUrl?: string;
|
|
180
|
+
originalLine?: number;
|
|
181
|
+
} = {
|
|
182
|
+
ref: entry.ref,
|
|
183
|
+
type: entry.type as "BP" | "LP",
|
|
184
|
+
url: meta.url as string,
|
|
185
|
+
line: meta.line as number,
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
if (meta.column !== undefined) {
|
|
189
|
+
item.column = meta.column as number;
|
|
190
|
+
}
|
|
191
|
+
if (meta.condition !== undefined) {
|
|
192
|
+
item.condition = meta.condition as string;
|
|
193
|
+
}
|
|
194
|
+
if (meta.hitCount !== undefined) {
|
|
195
|
+
item.hitCount = meta.hitCount as number;
|
|
196
|
+
}
|
|
197
|
+
if (meta.template !== undefined) {
|
|
198
|
+
item.template = meta.template as string;
|
|
199
|
+
}
|
|
200
|
+
if (meta.originalUrl !== undefined) {
|
|
201
|
+
item.originalUrl = meta.originalUrl as string;
|
|
202
|
+
item.originalLine = meta.originalLine as number;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return item;
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Include disabled breakpoints
|
|
209
|
+
for (const [ref, entry] of session.disabledBreakpoints) {
|
|
210
|
+
const meta = entry.meta;
|
|
211
|
+
const item: {
|
|
212
|
+
ref: string;
|
|
213
|
+
type: "BP" | "LP";
|
|
214
|
+
url: string;
|
|
215
|
+
line: number;
|
|
216
|
+
column?: number;
|
|
217
|
+
condition?: string;
|
|
218
|
+
hitCount?: number;
|
|
219
|
+
template?: string;
|
|
220
|
+
disabled?: boolean;
|
|
221
|
+
} = {
|
|
222
|
+
ref,
|
|
223
|
+
type: (meta.type as "BP" | "LP") ?? "BP",
|
|
224
|
+
url: meta.url as string,
|
|
225
|
+
line: meta.line as number,
|
|
226
|
+
disabled: true,
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
if (meta.column !== undefined) {
|
|
230
|
+
item.column = meta.column as number;
|
|
231
|
+
}
|
|
232
|
+
if (meta.condition !== undefined) {
|
|
233
|
+
item.condition = meta.condition as string;
|
|
234
|
+
}
|
|
235
|
+
if (meta.hitCount !== undefined) {
|
|
236
|
+
item.hitCount = meta.hitCount as number;
|
|
237
|
+
}
|
|
238
|
+
if (meta.template !== undefined) {
|
|
239
|
+
item.template = meta.template as string;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
results.push(item);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return results;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export async function toggleBreakpoint(
|
|
249
|
+
session: DebugSession,
|
|
250
|
+
ref: string,
|
|
251
|
+
): Promise<{ ref: string; state: "enabled" | "disabled" }> {
|
|
252
|
+
if (!session.cdp) {
|
|
253
|
+
throw new Error("No active debug session");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (ref === "all") {
|
|
257
|
+
// Toggle all: if any are enabled, disable all; otherwise enable all
|
|
258
|
+
const activeBps = session.refs.list("BP");
|
|
259
|
+
const activeLps = session.refs.list("LP");
|
|
260
|
+
const allActive = [...activeBps, ...activeLps];
|
|
261
|
+
|
|
262
|
+
if (allActive.length > 0) {
|
|
263
|
+
// Disable all active breakpoints
|
|
264
|
+
for (const entry of allActive) {
|
|
265
|
+
await session.cdp.send("Debugger.removeBreakpoint", {
|
|
266
|
+
breakpointId: entry.remoteId,
|
|
267
|
+
});
|
|
268
|
+
const meta = { ...(entry.meta ?? {}), type: entry.type };
|
|
269
|
+
session.disabledBreakpoints.set(entry.ref, {
|
|
270
|
+
breakpointId: entry.remoteId,
|
|
271
|
+
meta,
|
|
272
|
+
});
|
|
273
|
+
session.refs.remove(entry.ref);
|
|
274
|
+
}
|
|
275
|
+
return { ref: "all", state: "disabled" };
|
|
276
|
+
}
|
|
277
|
+
// Re-enable all disabled breakpoints
|
|
278
|
+
const disabledRefs = [...session.disabledBreakpoints.keys()];
|
|
279
|
+
for (const dRef of disabledRefs) {
|
|
280
|
+
const entry = session.disabledBreakpoints.get(dRef);
|
|
281
|
+
if (!entry) continue;
|
|
282
|
+
await reEnableBreakpoint(session, dRef, entry);
|
|
283
|
+
}
|
|
284
|
+
return { ref: "all", state: "enabled" };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Single breakpoint toggle
|
|
288
|
+
// Check if it's currently active
|
|
289
|
+
const activeEntry = session.refs.resolve(ref);
|
|
290
|
+
if (activeEntry && (activeEntry.type === "BP" || activeEntry.type === "LP")) {
|
|
291
|
+
// Disable it
|
|
292
|
+
await session.cdp.send("Debugger.removeBreakpoint", {
|
|
293
|
+
breakpointId: activeEntry.remoteId,
|
|
294
|
+
});
|
|
295
|
+
const meta = { ...(activeEntry.meta ?? {}), type: activeEntry.type };
|
|
296
|
+
session.disabledBreakpoints.set(ref, {
|
|
297
|
+
breakpointId: activeEntry.remoteId,
|
|
298
|
+
meta,
|
|
299
|
+
});
|
|
300
|
+
session.refs.remove(ref);
|
|
301
|
+
return { ref, state: "disabled" };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Check if it's disabled
|
|
305
|
+
const disabledEntry = session.disabledBreakpoints.get(ref);
|
|
306
|
+
if (disabledEntry) {
|
|
307
|
+
await reEnableBreakpoint(session, ref, disabledEntry);
|
|
308
|
+
return { ref, state: "enabled" };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
throw new Error(`Unknown breakpoint ref: ${ref}`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function reEnableBreakpoint(
|
|
315
|
+
session: DebugSession,
|
|
316
|
+
ref: string,
|
|
317
|
+
entry: { breakpointId: string; meta: Record<string, unknown> },
|
|
318
|
+
): Promise<void> {
|
|
319
|
+
if (!session.cdp) return;
|
|
320
|
+
|
|
321
|
+
const meta = entry.meta;
|
|
322
|
+
const line = meta.line as number;
|
|
323
|
+
const url = meta.url as string | undefined;
|
|
324
|
+
const condition = meta.condition as string | undefined;
|
|
325
|
+
const hitCount = meta.hitCount as number | undefined;
|
|
326
|
+
const urlRegex = meta.urlRegex as string | undefined;
|
|
327
|
+
|
|
328
|
+
const builtCondition = session.buildBreakpointCondition(condition, hitCount);
|
|
329
|
+
|
|
330
|
+
const bpParams: Protocol.Debugger.SetBreakpointByUrlRequest = {
|
|
331
|
+
lineNumber: line - 1,
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
if (urlRegex) {
|
|
335
|
+
bpParams.urlRegex = urlRegex;
|
|
336
|
+
} else if (url) {
|
|
337
|
+
bpParams.url = url;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (builtCondition) {
|
|
341
|
+
bpParams.condition = builtCondition;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const r = await session.cdp.send("Debugger.setBreakpointByUrl", bpParams);
|
|
345
|
+
|
|
346
|
+
// Re-create the ref entry in the ref table
|
|
347
|
+
const type = (meta.type as string) === "LP" ? "LP" : "BP";
|
|
348
|
+
const newMeta = { ...meta };
|
|
349
|
+
delete newMeta.type; // type is stored in the ref entry, not meta
|
|
350
|
+
if (type === "BP") {
|
|
351
|
+
session.refs.addBreakpoint(r.breakpointId, newMeta);
|
|
352
|
+
} else {
|
|
353
|
+
session.refs.addLogpoint(r.breakpointId, newMeta);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
session.disabledBreakpoints.delete(ref);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export async function getBreakableLocations(
|
|
360
|
+
session: DebugSession,
|
|
361
|
+
file: string,
|
|
362
|
+
startLine: number,
|
|
363
|
+
endLine: number,
|
|
364
|
+
): Promise<Array<{ line: number; column: number }>> {
|
|
365
|
+
if (!session.cdp) {
|
|
366
|
+
throw new Error("No active debug session");
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const scriptUrl = session.findScriptUrl(file);
|
|
370
|
+
if (!scriptUrl) {
|
|
371
|
+
throw new Error(`No loaded script matches "${file}"`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Find the scriptId for this URL
|
|
375
|
+
let scriptId: string | undefined;
|
|
376
|
+
for (const [sid, info] of session.scripts) {
|
|
377
|
+
if (info.url === scriptUrl) {
|
|
378
|
+
scriptId = sid;
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (!scriptId) {
|
|
384
|
+
throw new Error(`No scriptId found for "${file}"`);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const r = await session.cdp.send("Debugger.getPossibleBreakpoints", {
|
|
388
|
+
start: { scriptId, lineNumber: startLine - 1 },
|
|
389
|
+
end: { scriptId, lineNumber: endLine },
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
return r.locations.map((loc) => ({
|
|
393
|
+
line: loc.lineNumber + 1, // Convert to 1-based
|
|
394
|
+
column: (loc.columnNumber ?? 0) + 1, // Convert to 1-based
|
|
395
|
+
}));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export async function setLogpoint(
|
|
399
|
+
session: DebugSession,
|
|
400
|
+
file: string,
|
|
401
|
+
line: number,
|
|
402
|
+
template: string,
|
|
403
|
+
options?: { condition?: string; maxEmissions?: number },
|
|
404
|
+
): Promise<{ ref: string; location: { url: string; line: number; column?: number } }> {
|
|
405
|
+
if (!session.cdp) {
|
|
406
|
+
throw new Error("No active debug session");
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const url = session.findScriptUrl(file);
|
|
410
|
+
|
|
411
|
+
// Build the logpoint condition: evaluate console.log(...), then return false
|
|
412
|
+
// so execution does not pause.
|
|
413
|
+
let logExpr = `console.log(${template})`;
|
|
414
|
+
if (options?.condition) {
|
|
415
|
+
logExpr = `(${options.condition}) ? (${logExpr}, false) : false`;
|
|
416
|
+
} else {
|
|
417
|
+
logExpr = `${logExpr}, false`;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const lpParams: Protocol.Debugger.SetBreakpointByUrlRequest = {
|
|
421
|
+
lineNumber: line - 1, // CDP uses 0-based lines
|
|
422
|
+
condition: logExpr,
|
|
423
|
+
};
|
|
424
|
+
if (url) {
|
|
425
|
+
lpParams.url = url;
|
|
426
|
+
} else {
|
|
427
|
+
lpParams.urlRegex = `${file.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const r = await session.cdp.send("Debugger.setBreakpointByUrl", lpParams);
|
|
431
|
+
|
|
432
|
+
const loc = r.locations[0];
|
|
433
|
+
const resolvedUrl = url ?? file;
|
|
434
|
+
const resolvedLine = loc ? loc.lineNumber + 1 : line;
|
|
435
|
+
const resolvedColumn = loc?.columnNumber;
|
|
436
|
+
|
|
437
|
+
const meta: Record<string, unknown> = {
|
|
438
|
+
url: resolvedUrl,
|
|
439
|
+
line: resolvedLine,
|
|
440
|
+
template,
|
|
441
|
+
};
|
|
442
|
+
if (resolvedColumn !== undefined) {
|
|
443
|
+
meta.column = resolvedColumn;
|
|
444
|
+
}
|
|
445
|
+
if (options?.condition) {
|
|
446
|
+
meta.condition = options.condition;
|
|
447
|
+
}
|
|
448
|
+
if (options?.maxEmissions) {
|
|
449
|
+
meta.maxEmissions = options.maxEmissions;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const ref = session.refs.addLogpoint(r.breakpointId, meta);
|
|
453
|
+
|
|
454
|
+
const location: { url: string; line: number; column?: number } = {
|
|
455
|
+
url: resolvedUrl,
|
|
456
|
+
line: resolvedLine,
|
|
457
|
+
};
|
|
458
|
+
if (resolvedColumn !== undefined) {
|
|
459
|
+
location.column = resolvedColumn;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return { ref, location };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export async function setExceptionPause(
|
|
466
|
+
session: DebugSession,
|
|
467
|
+
mode: "all" | "uncaught" | "caught" | "none",
|
|
468
|
+
): Promise<void> {
|
|
469
|
+
if (!session.cdp) {
|
|
470
|
+
throw new Error("No active debug session");
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// CDP only supports "none", "all", and "uncaught".
|
|
474
|
+
// Map "caught" to "all" since CDP does not have a "caught-only" mode.
|
|
475
|
+
let cdpState: Protocol.Debugger.SetPauseOnExceptionsRequest["state"];
|
|
476
|
+
switch (mode) {
|
|
477
|
+
case "all":
|
|
478
|
+
cdpState = "all";
|
|
479
|
+
break;
|
|
480
|
+
case "uncaught":
|
|
481
|
+
cdpState = "uncaught";
|
|
482
|
+
break;
|
|
483
|
+
case "caught":
|
|
484
|
+
cdpState = "all";
|
|
485
|
+
break;
|
|
486
|
+
case "none":
|
|
487
|
+
cdpState = "none";
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
await session.cdp.send("Debugger.setPauseOnExceptions", { state: cdpState });
|
|
492
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { DebugSession } from "./session.ts";
|
|
2
|
+
|
|
3
|
+
export async function continueExecution(session: DebugSession): Promise<void> {
|
|
4
|
+
if (!session.isPaused()) {
|
|
5
|
+
throw new Error("Cannot continue: process is not paused");
|
|
6
|
+
}
|
|
7
|
+
if (!session.cdp) {
|
|
8
|
+
throw new Error("Cannot continue: no CDP connection");
|
|
9
|
+
}
|
|
10
|
+
const waiter = session.createPauseWaiter();
|
|
11
|
+
await session.cdp.send("Debugger.resume");
|
|
12
|
+
await waiter;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function stepExecution(
|
|
16
|
+
session: DebugSession,
|
|
17
|
+
mode: "over" | "into" | "out",
|
|
18
|
+
): Promise<void> {
|
|
19
|
+
if (!session.isPaused()) {
|
|
20
|
+
throw new Error("Cannot step: process is not paused");
|
|
21
|
+
}
|
|
22
|
+
if (!session.cdp) {
|
|
23
|
+
throw new Error("Cannot step: no CDP connection");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const methodMap = {
|
|
27
|
+
over: "Debugger.stepOver",
|
|
28
|
+
into: "Debugger.stepInto",
|
|
29
|
+
out: "Debugger.stepOut",
|
|
30
|
+
} as const;
|
|
31
|
+
|
|
32
|
+
const waiter = session.createPauseWaiter();
|
|
33
|
+
await session.cdp.send(methodMap[mode]);
|
|
34
|
+
await waiter;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function pauseExecution(session: DebugSession): Promise<void> {
|
|
38
|
+
if (session.sessionState !== "running") {
|
|
39
|
+
throw new Error("Cannot pause: process is not running");
|
|
40
|
+
}
|
|
41
|
+
if (!session.cdp) {
|
|
42
|
+
throw new Error("Cannot pause: no CDP connection");
|
|
43
|
+
}
|
|
44
|
+
const waiter = session.createPauseWaiter();
|
|
45
|
+
await session.cdp.send("Debugger.pause");
|
|
46
|
+
await waiter;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function runToLocation(
|
|
50
|
+
session: DebugSession,
|
|
51
|
+
file: string,
|
|
52
|
+
line: number,
|
|
53
|
+
): Promise<void> {
|
|
54
|
+
if (!session.isPaused()) {
|
|
55
|
+
throw new Error("Cannot run-to: process is not paused");
|
|
56
|
+
}
|
|
57
|
+
if (!session.cdp) {
|
|
58
|
+
throw new Error("Cannot run-to: no CDP connection");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Find the script URL matching the given file (by suffix)
|
|
62
|
+
const scriptUrl = session.findScriptUrl(file);
|
|
63
|
+
if (!scriptUrl) {
|
|
64
|
+
throw new Error(`Cannot run-to: no loaded script matches "${file}"`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Set a temporary breakpoint (CDP lines are 0-based)
|
|
68
|
+
const bpResult = await session.cdp.send("Debugger.setBreakpointByUrl", {
|
|
69
|
+
lineNumber: line - 1,
|
|
70
|
+
urlRegex: scriptUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const breakpointId = bpResult.breakpointId;
|
|
74
|
+
|
|
75
|
+
// Resume execution — set up waiter before sending resume
|
|
76
|
+
const waiter = session.createPauseWaiter();
|
|
77
|
+
await session.cdp.send("Debugger.resume");
|
|
78
|
+
await waiter;
|
|
79
|
+
|
|
80
|
+
// Remove the temporary breakpoint
|
|
81
|
+
if (breakpointId && session.cdp) {
|
|
82
|
+
try {
|
|
83
|
+
await session.cdp.send("Debugger.removeBreakpoint", { breakpointId });
|
|
84
|
+
} catch {
|
|
85
|
+
// Breakpoint may already be gone if process exited
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function restartFrameExecution(
|
|
91
|
+
session: DebugSession,
|
|
92
|
+
frameRef?: string,
|
|
93
|
+
): Promise<{ status: string }> {
|
|
94
|
+
if (!session.isPaused()) {
|
|
95
|
+
throw new Error("Cannot restart frame: process is not paused");
|
|
96
|
+
}
|
|
97
|
+
if (!session.cdp) {
|
|
98
|
+
throw new Error("Cannot restart frame: no CDP connection");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let callFrameId: string;
|
|
102
|
+
if (frameRef) {
|
|
103
|
+
const entry = session.refs.resolve(frameRef);
|
|
104
|
+
if (!entry) {
|
|
105
|
+
throw new Error(`Unknown frame ref: ${frameRef}`);
|
|
106
|
+
}
|
|
107
|
+
callFrameId = entry.remoteId;
|
|
108
|
+
} else {
|
|
109
|
+
const topFrame = session.pausedCallFrames[0];
|
|
110
|
+
if (!topFrame) {
|
|
111
|
+
throw new Error("No call frames available");
|
|
112
|
+
}
|
|
113
|
+
callFrameId = topFrame.callFrameId;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const waiter = session.createPauseWaiter();
|
|
117
|
+
await session.cdp.send("Debugger.restartFrame", { callFrameId, mode: "StepInto" });
|
|
118
|
+
await waiter;
|
|
119
|
+
|
|
120
|
+
return { status: "restarted" };
|
|
121
|
+
}
|