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,701 @@
|
|
|
1
|
+
import type Protocol from "devtools-protocol/types/protocol.js";
|
|
2
|
+
import type { RemoteObject } from "../formatter/values.ts";
|
|
3
|
+
import { formatValue } from "../formatter/values.ts";
|
|
4
|
+
import type { ConsoleMessage, DebugSession, ExceptionEntry } from "./session.ts";
|
|
5
|
+
|
|
6
|
+
export async function evalExpression(
|
|
7
|
+
session: DebugSession,
|
|
8
|
+
expression: string,
|
|
9
|
+
options: {
|
|
10
|
+
frame?: string;
|
|
11
|
+
awaitPromise?: boolean;
|
|
12
|
+
throwOnSideEffect?: boolean;
|
|
13
|
+
timeout?: number;
|
|
14
|
+
} = {},
|
|
15
|
+
): Promise<{
|
|
16
|
+
ref: string;
|
|
17
|
+
type: string;
|
|
18
|
+
value: string;
|
|
19
|
+
objectId?: string;
|
|
20
|
+
}> {
|
|
21
|
+
if (!session.cdp) {
|
|
22
|
+
throw new Error("No active debug session");
|
|
23
|
+
}
|
|
24
|
+
if (session.sessionState !== "paused") {
|
|
25
|
+
throw new Error("Cannot eval: process is not paused");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Determine which frame to evaluate in
|
|
29
|
+
let frameIndex = 0;
|
|
30
|
+
if (options.frame) {
|
|
31
|
+
const entry = session.refs.resolve(options.frame);
|
|
32
|
+
if (entry?.meta?.frameIndex !== undefined) {
|
|
33
|
+
frameIndex = entry.meta.frameIndex as number;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const targetFrame = session.pausedCallFrames[frameIndex];
|
|
38
|
+
if (!targetFrame) {
|
|
39
|
+
throw new Error("No call frame available");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const callFrameId = targetFrame.callFrameId;
|
|
43
|
+
|
|
44
|
+
// Resolve @ref patterns in the expression
|
|
45
|
+
let resolvedExpression = expression;
|
|
46
|
+
const refPattern = /@[vof]\d+/g;
|
|
47
|
+
const refMatches = expression.match(refPattern);
|
|
48
|
+
if (refMatches) {
|
|
49
|
+
const refEntries: Array<{
|
|
50
|
+
ref: string;
|
|
51
|
+
name: string;
|
|
52
|
+
objectId: string;
|
|
53
|
+
}> = [];
|
|
54
|
+
for (const ref of refMatches) {
|
|
55
|
+
const remoteId = session.refs.resolveId(ref);
|
|
56
|
+
if (remoteId) {
|
|
57
|
+
const argName = `__ndbg_ref_${ref.slice(1)}`;
|
|
58
|
+
resolvedExpression = resolvedExpression.replace(ref, argName);
|
|
59
|
+
refEntries.push({
|
|
60
|
+
ref,
|
|
61
|
+
name: argName,
|
|
62
|
+
objectId: remoteId,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// If we have ref entries, use callFunctionOn to bind them
|
|
68
|
+
if (refEntries.length > 0) {
|
|
69
|
+
const argNames = refEntries.map((e) => e.name);
|
|
70
|
+
const funcBody = `return (function(${argNames.join(", ")}) { return ${resolvedExpression}; })(...arguments)`;
|
|
71
|
+
const firstObjectId = refEntries[0]?.objectId;
|
|
72
|
+
if (!firstObjectId) {
|
|
73
|
+
throw new Error("No object ID for ref resolution");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const callFnParams: Protocol.Runtime.CallFunctionOnRequest = {
|
|
77
|
+
functionDeclaration: `function() { ${funcBody} }`,
|
|
78
|
+
arguments: refEntries.map((e) => ({
|
|
79
|
+
objectId: e.objectId,
|
|
80
|
+
})),
|
|
81
|
+
objectId: firstObjectId,
|
|
82
|
+
returnByValue: false,
|
|
83
|
+
generatePreview: true,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
if (options.awaitPromise) {
|
|
87
|
+
callFnParams.awaitPromise = true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const evalPromise = session.cdp.send("Runtime.callFunctionOn", callFnParams);
|
|
91
|
+
|
|
92
|
+
let evalResponse: Protocol.Runtime.CallFunctionOnResponse;
|
|
93
|
+
if (options.timeout) {
|
|
94
|
+
const timeoutPromise = Bun.sleep(options.timeout).then(() => {
|
|
95
|
+
throw new Error(`Evaluation timed out after ${options.timeout}ms`);
|
|
96
|
+
});
|
|
97
|
+
evalResponse = (await Promise.race([
|
|
98
|
+
evalPromise,
|
|
99
|
+
timeoutPromise,
|
|
100
|
+
])) as Protocol.Runtime.CallFunctionOnResponse;
|
|
101
|
+
} else {
|
|
102
|
+
evalResponse = await evalPromise;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return session.processEvalResult(evalResponse, expression);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Standard evaluation on call frame
|
|
110
|
+
const frameEvalParams: Protocol.Debugger.EvaluateOnCallFrameRequest = {
|
|
111
|
+
callFrameId,
|
|
112
|
+
expression: resolvedExpression,
|
|
113
|
+
returnByValue: false,
|
|
114
|
+
generatePreview: true,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
if (options.throwOnSideEffect) {
|
|
118
|
+
frameEvalParams.throwOnSideEffect = true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const evalPromise = session.cdp.send("Debugger.evaluateOnCallFrame", frameEvalParams);
|
|
122
|
+
|
|
123
|
+
let evalResponse: Protocol.Debugger.EvaluateOnCallFrameResponse;
|
|
124
|
+
if (options.timeout) {
|
|
125
|
+
const timeoutPromise = Bun.sleep(options.timeout).then(() => {
|
|
126
|
+
throw new Error(`Evaluation timed out after ${options.timeout}ms`);
|
|
127
|
+
});
|
|
128
|
+
evalResponse = (await Promise.race([
|
|
129
|
+
evalPromise,
|
|
130
|
+
timeoutPromise,
|
|
131
|
+
])) as Protocol.Debugger.EvaluateOnCallFrameResponse;
|
|
132
|
+
} else {
|
|
133
|
+
evalResponse = await evalPromise;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return session.processEvalResult(evalResponse, expression);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function getVars(
|
|
140
|
+
session: DebugSession,
|
|
141
|
+
options: { frame?: string; names?: string[]; allScopes?: boolean } = {},
|
|
142
|
+
): Promise<Array<{ ref: string; name: string; type: string; value: string }>> {
|
|
143
|
+
if (!session.cdp) {
|
|
144
|
+
throw new Error("No active debug session");
|
|
145
|
+
}
|
|
146
|
+
if (session.sessionState !== "paused") {
|
|
147
|
+
throw new Error("Cannot get vars: process is not paused");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Clear volatile refs at the start
|
|
151
|
+
session.refs.clearVolatile();
|
|
152
|
+
|
|
153
|
+
// Determine which frame to inspect
|
|
154
|
+
let frameIndex = 0;
|
|
155
|
+
if (options.frame) {
|
|
156
|
+
const entry = session.refs.resolve(options.frame);
|
|
157
|
+
if (entry?.meta?.frameIndex !== undefined) {
|
|
158
|
+
frameIndex = entry.meta.frameIndex as number;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const targetFrame = session.pausedCallFrames[frameIndex];
|
|
163
|
+
if (!targetFrame) {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const scopeChain = targetFrame.scopeChain;
|
|
168
|
+
if (!scopeChain) {
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const variables: Array<{
|
|
173
|
+
ref: string;
|
|
174
|
+
name: string;
|
|
175
|
+
type: string;
|
|
176
|
+
value: string;
|
|
177
|
+
}> = [];
|
|
178
|
+
|
|
179
|
+
for (const scope of scopeChain) {
|
|
180
|
+
const scopeType = scope.type;
|
|
181
|
+
|
|
182
|
+
// Include local, module, block, and script scopes by default.
|
|
183
|
+
// Include closure scope only with allScopes. Always skip global.
|
|
184
|
+
const includeScope =
|
|
185
|
+
scopeType === "local" ||
|
|
186
|
+
scopeType === "module" ||
|
|
187
|
+
scopeType === "block" ||
|
|
188
|
+
scopeType === "script" ||
|
|
189
|
+
(options.allScopes && scopeType === "closure");
|
|
190
|
+
|
|
191
|
+
if (includeScope) {
|
|
192
|
+
const scopeObj = scope.object;
|
|
193
|
+
const objectId = scopeObj.objectId;
|
|
194
|
+
if (!objectId) continue;
|
|
195
|
+
|
|
196
|
+
const propsResult = await session.cdp.send("Runtime.getProperties", {
|
|
197
|
+
objectId,
|
|
198
|
+
ownProperties: true,
|
|
199
|
+
generatePreview: true,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const properties = propsResult.result;
|
|
203
|
+
|
|
204
|
+
for (const prop of properties) {
|
|
205
|
+
const propName = prop.name;
|
|
206
|
+
const propValue = prop.value as RemoteObject | undefined;
|
|
207
|
+
|
|
208
|
+
if (!propValue) continue;
|
|
209
|
+
|
|
210
|
+
// Skip internal properties
|
|
211
|
+
if (propName.startsWith("__")) continue;
|
|
212
|
+
|
|
213
|
+
// Apply name filter if provided
|
|
214
|
+
if (options.names && options.names.length > 0) {
|
|
215
|
+
if (!options.names.includes(propName)) continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const remoteId = (propValue.objectId as string) ?? `primitive:${propName}`;
|
|
219
|
+
const ref = session.refs.addVar(remoteId, propName);
|
|
220
|
+
|
|
221
|
+
variables.push({
|
|
222
|
+
ref,
|
|
223
|
+
name: propName,
|
|
224
|
+
type: propValue.type,
|
|
225
|
+
value: formatValue(propValue),
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Skip "global" scope
|
|
231
|
+
if (scopeType === "global") continue;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return variables;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export async function getProps(
|
|
238
|
+
session: DebugSession,
|
|
239
|
+
ref: string,
|
|
240
|
+
options: {
|
|
241
|
+
own?: boolean;
|
|
242
|
+
internal?: boolean;
|
|
243
|
+
depth?: number;
|
|
244
|
+
} = {},
|
|
245
|
+
): Promise<
|
|
246
|
+
Array<{
|
|
247
|
+
ref?: string;
|
|
248
|
+
name: string;
|
|
249
|
+
type: string;
|
|
250
|
+
value: string;
|
|
251
|
+
isOwn?: boolean;
|
|
252
|
+
isAccessor?: boolean;
|
|
253
|
+
}>
|
|
254
|
+
> {
|
|
255
|
+
if (!session.cdp) {
|
|
256
|
+
throw new Error("No active debug session");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const entry = session.refs.resolve(ref);
|
|
260
|
+
if (!entry) {
|
|
261
|
+
throw new Error(`Unknown ref: ${ref}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const objectId = entry.remoteId;
|
|
265
|
+
|
|
266
|
+
// Verify it's a valid object ID (not a primitive placeholder)
|
|
267
|
+
if (objectId.startsWith("primitive:") || objectId.startsWith("eval:")) {
|
|
268
|
+
throw new Error(`Ref ${ref} is a primitive and has no properties`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const propsParams: Protocol.Runtime.GetPropertiesRequest = {
|
|
272
|
+
objectId,
|
|
273
|
+
ownProperties: options.own ?? true,
|
|
274
|
+
generatePreview: true,
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
if (options.internal) {
|
|
278
|
+
propsParams.accessorPropertiesOnly = false;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const propsResult = await session.cdp.send("Runtime.getProperties", propsParams);
|
|
282
|
+
const properties = propsResult.result ?? [];
|
|
283
|
+
const internalProps = options.internal ? (propsResult.internalProperties ?? []) : [];
|
|
284
|
+
|
|
285
|
+
const result: Array<{
|
|
286
|
+
ref?: string;
|
|
287
|
+
name: string;
|
|
288
|
+
type: string;
|
|
289
|
+
value: string;
|
|
290
|
+
isOwn?: boolean;
|
|
291
|
+
isAccessor?: boolean;
|
|
292
|
+
}> = [];
|
|
293
|
+
|
|
294
|
+
for (const prop of properties) {
|
|
295
|
+
const propName = prop.name;
|
|
296
|
+
const propValue = prop.value as RemoteObject | undefined;
|
|
297
|
+
const isOwn = prop.isOwn;
|
|
298
|
+
const getDesc = prop.get as RemoteObject | undefined;
|
|
299
|
+
const setDesc = prop.set as RemoteObject | undefined;
|
|
300
|
+
const isAccessor =
|
|
301
|
+
!!(getDesc?.type && getDesc.type !== "undefined") ||
|
|
302
|
+
!!(setDesc?.type && setDesc.type !== "undefined");
|
|
303
|
+
|
|
304
|
+
if (!propValue && !isAccessor) continue;
|
|
305
|
+
|
|
306
|
+
const displayValue = propValue
|
|
307
|
+
? propValue
|
|
308
|
+
: ({
|
|
309
|
+
type: "function",
|
|
310
|
+
description: "getter/setter",
|
|
311
|
+
} as RemoteObject);
|
|
312
|
+
|
|
313
|
+
let propRef: string | undefined;
|
|
314
|
+
if (propValue?.objectId) {
|
|
315
|
+
propRef = session.refs.addObject(propValue.objectId, propName);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const item: {
|
|
319
|
+
ref?: string;
|
|
320
|
+
name: string;
|
|
321
|
+
type: string;
|
|
322
|
+
value: string;
|
|
323
|
+
isOwn?: boolean;
|
|
324
|
+
isAccessor?: boolean;
|
|
325
|
+
} = {
|
|
326
|
+
name: propName,
|
|
327
|
+
type: displayValue.type,
|
|
328
|
+
value: formatValue(displayValue),
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
if (propRef) {
|
|
332
|
+
item.ref = propRef;
|
|
333
|
+
}
|
|
334
|
+
if (isOwn !== undefined) {
|
|
335
|
+
item.isOwn = isOwn;
|
|
336
|
+
}
|
|
337
|
+
if (isAccessor) {
|
|
338
|
+
item.isAccessor = true;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
result.push(item);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Add internal properties
|
|
345
|
+
for (const prop of internalProps) {
|
|
346
|
+
const propName = prop.name;
|
|
347
|
+
const propValue = prop.value as RemoteObject | undefined;
|
|
348
|
+
|
|
349
|
+
if (!propValue) continue;
|
|
350
|
+
|
|
351
|
+
let propRef: string | undefined;
|
|
352
|
+
if (propValue.objectId) {
|
|
353
|
+
propRef = session.refs.addObject(propValue.objectId, propName);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const item: {
|
|
357
|
+
ref?: string;
|
|
358
|
+
name: string;
|
|
359
|
+
type: string;
|
|
360
|
+
value: string;
|
|
361
|
+
isOwn?: boolean;
|
|
362
|
+
isAccessor?: boolean;
|
|
363
|
+
} = {
|
|
364
|
+
name: `[[${propName}]]`,
|
|
365
|
+
type: propValue.type,
|
|
366
|
+
value: formatValue(propValue),
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
if (propRef) {
|
|
370
|
+
item.ref = propRef;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
result.push(item);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return result;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export async function getSource(
|
|
380
|
+
session: DebugSession,
|
|
381
|
+
options: { file?: string; lines?: number; all?: boolean; generated?: boolean } = {},
|
|
382
|
+
): Promise<{
|
|
383
|
+
url: string;
|
|
384
|
+
lines: Array<{ line: number; text: string; current?: boolean }>;
|
|
385
|
+
}> {
|
|
386
|
+
if (!session.cdp) {
|
|
387
|
+
throw new Error("No active debug session");
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
let scriptId: string | undefined;
|
|
391
|
+
let url = "";
|
|
392
|
+
let currentLine: number | undefined;
|
|
393
|
+
|
|
394
|
+
if (options.file) {
|
|
395
|
+
// Find the script by file name
|
|
396
|
+
const scriptUrl = session.findScriptUrl(options.file);
|
|
397
|
+
if (!scriptUrl) {
|
|
398
|
+
throw new Error(`No loaded script matches "${options.file}"`);
|
|
399
|
+
}
|
|
400
|
+
url = scriptUrl;
|
|
401
|
+
// Find the scriptId for this URL
|
|
402
|
+
for (const [sid, info] of session.scripts) {
|
|
403
|
+
if (info.url === scriptUrl) {
|
|
404
|
+
scriptId = sid;
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
// If we are paused in this file, mark the current line
|
|
409
|
+
if (
|
|
410
|
+
session.sessionState === "paused" &&
|
|
411
|
+
session.pauseInfo &&
|
|
412
|
+
session.pauseInfo.scriptId === scriptId
|
|
413
|
+
) {
|
|
414
|
+
currentLine = session.pauseInfo.line;
|
|
415
|
+
}
|
|
416
|
+
} else {
|
|
417
|
+
// Use current pause location
|
|
418
|
+
if (session.sessionState !== "paused" || !session.pauseInfo?.scriptId) {
|
|
419
|
+
throw new Error("Not paused; specify --file to view source");
|
|
420
|
+
}
|
|
421
|
+
scriptId = session.pauseInfo.scriptId;
|
|
422
|
+
url = session.scripts.get(scriptId)?.url ?? "";
|
|
423
|
+
currentLine = session.pauseInfo.line;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (!scriptId) {
|
|
427
|
+
throw new Error("Could not determine script to show");
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Try to get original source from source map (unless --generated)
|
|
431
|
+
let scriptSource: string | null = null;
|
|
432
|
+
let useOriginalSource = false;
|
|
433
|
+
let originalCurrentLine: number | undefined;
|
|
434
|
+
|
|
435
|
+
if (!options.generated) {
|
|
436
|
+
// Check if this file is being requested by original source path
|
|
437
|
+
const smMatch = session.sourceMapResolver.findScriptForSource(options.file ?? "");
|
|
438
|
+
if (smMatch) {
|
|
439
|
+
scriptId = smMatch.scriptId;
|
|
440
|
+
const origSource = session.sourceMapResolver.getOriginalSource(scriptId, options.file ?? "");
|
|
441
|
+
if (origSource) {
|
|
442
|
+
scriptSource = origSource;
|
|
443
|
+
useOriginalSource = true;
|
|
444
|
+
url = options.file ?? url;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Also try source map for the current scriptId (when paused at a .js file that has a .ts source)
|
|
449
|
+
if (!useOriginalSource) {
|
|
450
|
+
const smInfo = session.sourceMapResolver.getInfo(scriptId);
|
|
451
|
+
if (smInfo && smInfo.sources.length > 0) {
|
|
452
|
+
const primarySource = smInfo.sources[0];
|
|
453
|
+
if (primarySource) {
|
|
454
|
+
const origSource = session.sourceMapResolver.getOriginalSource(scriptId, primarySource);
|
|
455
|
+
if (origSource) {
|
|
456
|
+
scriptSource = origSource;
|
|
457
|
+
useOriginalSource = true;
|
|
458
|
+
url = primarySource;
|
|
459
|
+
// Translate current line to original
|
|
460
|
+
if (currentLine !== undefined) {
|
|
461
|
+
const original = session.sourceMapResolver.toOriginal(scriptId, currentLine + 1, 0);
|
|
462
|
+
if (original) {
|
|
463
|
+
originalCurrentLine = original.line - 1; // 0-based
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (!scriptSource) {
|
|
473
|
+
const sourceResult = await session.cdp.send("Debugger.getScriptSource", {
|
|
474
|
+
scriptId,
|
|
475
|
+
});
|
|
476
|
+
scriptSource = sourceResult.scriptSource;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const sourceLines = scriptSource.split("\n");
|
|
480
|
+
const effectiveCurrentLine =
|
|
481
|
+
useOriginalSource && originalCurrentLine !== undefined ? originalCurrentLine : currentLine;
|
|
482
|
+
|
|
483
|
+
const linesContext = options.lines ?? 5;
|
|
484
|
+
let startLine: number;
|
|
485
|
+
let endLine: number;
|
|
486
|
+
|
|
487
|
+
if (options.all) {
|
|
488
|
+
startLine = 0;
|
|
489
|
+
endLine = sourceLines.length - 1;
|
|
490
|
+
} else if (effectiveCurrentLine !== undefined) {
|
|
491
|
+
startLine = Math.max(0, effectiveCurrentLine - linesContext);
|
|
492
|
+
endLine = Math.min(sourceLines.length - 1, effectiveCurrentLine + linesContext);
|
|
493
|
+
} else {
|
|
494
|
+
// No current line (viewing a different file while paused), show from the top
|
|
495
|
+
startLine = 0;
|
|
496
|
+
endLine = Math.min(sourceLines.length - 1, linesContext * 2);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const lines: Array<{ line: number; text: string; current?: boolean }> = [];
|
|
500
|
+
for (let i = startLine; i <= endLine; i++) {
|
|
501
|
+
const entry: { line: number; text: string; current?: boolean } = {
|
|
502
|
+
line: i + 1, // 1-based
|
|
503
|
+
text: sourceLines[i] ?? "",
|
|
504
|
+
};
|
|
505
|
+
if (effectiveCurrentLine !== undefined && i === effectiveCurrentLine) {
|
|
506
|
+
entry.current = true;
|
|
507
|
+
}
|
|
508
|
+
lines.push(entry);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return { url, lines };
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export function getScripts(
|
|
515
|
+
session: DebugSession,
|
|
516
|
+
filter?: string,
|
|
517
|
+
): Array<{ scriptId: string; url: string; sourceMapURL?: string }> {
|
|
518
|
+
const result: Array<{ scriptId: string; url: string; sourceMapURL?: string }> = [];
|
|
519
|
+
for (const info of session.scripts.values()) {
|
|
520
|
+
// Filter out empty-URL scripts
|
|
521
|
+
if (!info.url) continue;
|
|
522
|
+
// Apply filter if provided
|
|
523
|
+
if (filter && !info.url.includes(filter)) continue;
|
|
524
|
+
|
|
525
|
+
const entry: { scriptId: string; url: string; sourceMapURL?: string } = {
|
|
526
|
+
scriptId: info.scriptId,
|
|
527
|
+
url: info.url,
|
|
528
|
+
};
|
|
529
|
+
if (info.sourceMapURL) {
|
|
530
|
+
entry.sourceMapURL = info.sourceMapURL;
|
|
531
|
+
}
|
|
532
|
+
result.push(entry);
|
|
533
|
+
}
|
|
534
|
+
return result;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export function getStack(
|
|
538
|
+
session: DebugSession,
|
|
539
|
+
options: { asyncDepth?: number; generated?: boolean } = {},
|
|
540
|
+
): Array<{
|
|
541
|
+
ref: string;
|
|
542
|
+
functionName: string;
|
|
543
|
+
file: string;
|
|
544
|
+
line: number;
|
|
545
|
+
column?: number;
|
|
546
|
+
isAsync?: boolean;
|
|
547
|
+
}> {
|
|
548
|
+
if (session.sessionState !== "paused" || !session.cdp) {
|
|
549
|
+
throw new Error("Not paused");
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Clear volatile refs so frame refs are fresh
|
|
553
|
+
session.refs.clearVolatile();
|
|
554
|
+
|
|
555
|
+
const callFrames = session.pausedCallFrames;
|
|
556
|
+
const stackFrames: Array<{
|
|
557
|
+
ref: string;
|
|
558
|
+
functionName: string;
|
|
559
|
+
file: string;
|
|
560
|
+
line: number;
|
|
561
|
+
column?: number;
|
|
562
|
+
isAsync?: boolean;
|
|
563
|
+
}> = [];
|
|
564
|
+
|
|
565
|
+
for (let i = 0; i < callFrames.length; i++) {
|
|
566
|
+
const frame = callFrames[i];
|
|
567
|
+
if (!frame) continue;
|
|
568
|
+
const callFrameId = frame.callFrameId;
|
|
569
|
+
const funcName = frame.functionName || "(anonymous)";
|
|
570
|
+
const loc = frame.location;
|
|
571
|
+
const sid = loc.scriptId;
|
|
572
|
+
const lineNum = loc.lineNumber + 1; // 1-based
|
|
573
|
+
const colNum = loc.columnNumber;
|
|
574
|
+
let url = session.scripts.get(sid)?.url ?? "";
|
|
575
|
+
let displayLine = lineNum;
|
|
576
|
+
let displayCol = colNum !== undefined ? colNum + 1 : undefined;
|
|
577
|
+
let resolvedName: string | null = null;
|
|
578
|
+
|
|
579
|
+
if (!options.generated) {
|
|
580
|
+
const resolved = session.resolveOriginalLocation(sid, lineNum, colNum ?? 0);
|
|
581
|
+
if (resolved) {
|
|
582
|
+
url = resolved.url;
|
|
583
|
+
displayLine = resolved.line;
|
|
584
|
+
displayCol = resolved.column;
|
|
585
|
+
}
|
|
586
|
+
const smOriginal = session.sourceMapResolver.toOriginal(sid, lineNum, colNum ?? 0);
|
|
587
|
+
resolvedName = smOriginal?.name ?? null;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const ref = session.refs.addFrame(callFrameId, funcName, { frameIndex: i });
|
|
591
|
+
|
|
592
|
+
const stackEntry: {
|
|
593
|
+
ref: string;
|
|
594
|
+
functionName: string;
|
|
595
|
+
file: string;
|
|
596
|
+
line: number;
|
|
597
|
+
column?: number;
|
|
598
|
+
isAsync?: boolean;
|
|
599
|
+
} = {
|
|
600
|
+
ref,
|
|
601
|
+
functionName: resolvedName ?? funcName,
|
|
602
|
+
file: url,
|
|
603
|
+
line: displayLine,
|
|
604
|
+
};
|
|
605
|
+
if (displayCol !== undefined) {
|
|
606
|
+
stackEntry.column = displayCol;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
stackFrames.push(stackEntry);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return stackFrames;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
export async function searchInScripts(
|
|
616
|
+
session: DebugSession,
|
|
617
|
+
query: string,
|
|
618
|
+
options: {
|
|
619
|
+
scriptId?: string;
|
|
620
|
+
isRegex?: boolean;
|
|
621
|
+
caseSensitive?: boolean;
|
|
622
|
+
} = {},
|
|
623
|
+
): Promise<Array<{ url: string; line: number; column: number; content: string }>> {
|
|
624
|
+
if (!session.cdp) {
|
|
625
|
+
throw new Error("No active debug session");
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const results: Array<{ url: string; line: number; column: number; content: string }> = [];
|
|
629
|
+
|
|
630
|
+
const scriptsToSearch: Array<{ scriptId: string; url: string }> = [];
|
|
631
|
+
|
|
632
|
+
if (options.scriptId) {
|
|
633
|
+
const info = session.scripts.get(options.scriptId);
|
|
634
|
+
if (info) {
|
|
635
|
+
scriptsToSearch.push({ scriptId: options.scriptId, url: info.url });
|
|
636
|
+
}
|
|
637
|
+
} else {
|
|
638
|
+
for (const [sid, info] of session.scripts) {
|
|
639
|
+
if (!info.url) continue;
|
|
640
|
+
scriptsToSearch.push({ scriptId: sid, url: info.url });
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
for (const script of scriptsToSearch) {
|
|
645
|
+
try {
|
|
646
|
+
const searchResult = await session.cdp.send("Debugger.searchInContent", {
|
|
647
|
+
scriptId: script.scriptId,
|
|
648
|
+
query,
|
|
649
|
+
isRegex: options.isRegex ?? false,
|
|
650
|
+
caseSensitive: options.caseSensitive ?? false,
|
|
651
|
+
});
|
|
652
|
+
const matches = searchResult.result;
|
|
653
|
+
if (matches) {
|
|
654
|
+
for (const match of matches) {
|
|
655
|
+
results.push({
|
|
656
|
+
url: script.url,
|
|
657
|
+
line: (match.lineNumber ?? 0) + 1, // 1-based
|
|
658
|
+
column: 1, // SearchMatch doesn't provide column
|
|
659
|
+
content: match.lineContent ?? "",
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
} catch {
|
|
664
|
+
// Script may have been garbage collected, skip
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
return results;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
export function getConsoleMessages(
|
|
672
|
+
session: DebugSession,
|
|
673
|
+
options: { level?: string; since?: number; clear?: boolean } = {},
|
|
674
|
+
): ConsoleMessage[] {
|
|
675
|
+
let messages = [...session.consoleMessages];
|
|
676
|
+
if (options.level) {
|
|
677
|
+
messages = messages.filter((m) => m.level === options.level);
|
|
678
|
+
}
|
|
679
|
+
if (options.since !== undefined && options.since > 0) {
|
|
680
|
+
messages = messages.slice(-options.since);
|
|
681
|
+
}
|
|
682
|
+
if (options.clear) {
|
|
683
|
+
session.consoleMessages = [];
|
|
684
|
+
}
|
|
685
|
+
return messages;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
export function getExceptions(
|
|
689
|
+
session: DebugSession,
|
|
690
|
+
options: { since?: number } = {},
|
|
691
|
+
): ExceptionEntry[] {
|
|
692
|
+
let entries = [...session.exceptionEntries];
|
|
693
|
+
if (options.since !== undefined && options.since > 0) {
|
|
694
|
+
entries = entries.slice(-options.since);
|
|
695
|
+
}
|
|
696
|
+
return entries;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
export function clearConsole(session: DebugSession): void {
|
|
700
|
+
session.consoleMessages = [];
|
|
701
|
+
}
|