agent-dbg 0.1.0 → 0.1.2
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/.claude/settings.local.json +7 -5
- package/.claude/skills/agent-dbg/SKILL.md +116 -0
- package/.claude/skills/agent-dbg/references/commands.md +173 -0
- package/.vscode/launch.json +19 -0
- package/CLAUDE.md +2 -2
- package/PROGRESS.md +91 -91
- package/README.md +45 -17
- package/{ndbg-spec.md → SPEC.md} +152 -152
- package/dist/main.js +12500 -0
- package/package.json +3 -3
- package/src/cdp/client.ts +18 -4
- package/src/cdp/logger.ts +69 -0
- package/src/cli/parser.ts +54 -43
- package/src/commands/attach.ts +2 -2
- package/src/commands/blackbox-ls.ts +1 -1
- package/src/commands/blackbox-rm.ts +3 -3
- package/src/commands/blackbox.ts +2 -2
- package/src/commands/break-ls.ts +3 -2
- package/src/commands/break-rm.ts +2 -2
- package/src/commands/break-toggle.ts +2 -2
- package/src/commands/break.ts +46 -17
- package/src/commands/breakable.ts +2 -2
- package/src/commands/catch.ts +2 -2
- package/src/commands/console.ts +1 -1
- package/src/commands/continue.ts +5 -18
- package/src/commands/eval.ts +2 -2
- package/src/commands/exceptions.ts +1 -1
- package/src/commands/hotpatch.ts +2 -2
- package/src/commands/launch.ts +7 -11
- package/src/commands/logpoint.ts +7 -6
- package/src/commands/logs.ts +116 -0
- package/src/commands/pause.ts +5 -18
- package/src/commands/print-state.ts +85 -0
- package/src/commands/props.ts +2 -2
- package/src/commands/restart-frame.ts +1 -1
- package/src/commands/restart.ts +42 -0
- package/src/commands/run-to.ts +7 -20
- package/src/commands/scripts.ts +1 -1
- package/src/commands/search.ts +4 -3
- package/src/commands/set-return.ts +2 -2
- package/src/commands/set.ts +3 -3
- package/src/commands/source.ts +3 -2
- package/src/commands/sourcemap.ts +1 -1
- package/src/commands/stack.ts +1 -1
- package/src/commands/state.ts +3 -73
- package/src/commands/status.ts +3 -2
- package/src/commands/step.ts +5 -18
- package/src/commands/vars.ts +3 -1
- package/src/daemon/entry.ts +16 -6
- package/src/daemon/paths.ts +6 -2
- package/src/daemon/server.ts +1 -1
- package/src/daemon/session-breakpoints.ts +7 -2
- package/src/daemon/session-inspection.ts +6 -10
- package/src/daemon/session-state.ts +6 -5
- package/src/daemon/session.ts +23 -3
- package/src/formatter/logs.ts +110 -0
- package/src/formatter/path.ts +27 -0
- package/src/formatter/stack.ts +5 -2
- package/src/formatter/variables.ts +48 -1
- package/src/main.ts +2 -0
- package/src/protocol/messages.ts +3 -0
- package/tests/fixtures/async-app.js +1 -1
- package/tests/fixtures/error-app.js +1 -1
- package/tests/fixtures/simple-app.js +1 -1
- package/tests/fixtures/ts-app/src/app.ts +4 -0
- package/tests/integration/state.test.ts +7 -7
- package/tests/unit/daemon.test.ts +1 -1
- package/tests/unit/formatter.test.ts +8 -4
- package/.bin/ndbg +0 -0
- package/.claude/skills/ndbg-debugger/ndbg-debugger/SKILL.md +0 -116
- package/.claude/skills/ndbg-debugger/ndbg-debugger/references/commands.md +0 -173
package/src/commands/state.ts
CHANGED
|
@@ -1,19 +1,14 @@
|
|
|
1
1
|
import { registerCommand } from "../cli/registry.ts";
|
|
2
2
|
import { DaemonClient } from "../daemon/client.ts";
|
|
3
3
|
import type { StateSnapshot } from "../daemon/session.ts";
|
|
4
|
-
import
|
|
5
|
-
import { formatSource } from "../formatter/source.ts";
|
|
6
|
-
import type { StackFrame } from "../formatter/stack.ts";
|
|
7
|
-
import { formatStack } from "../formatter/stack.ts";
|
|
8
|
-
import type { Variable } from "../formatter/variables.ts";
|
|
9
|
-
import { formatVariables } from "../formatter/variables.ts";
|
|
4
|
+
import { printState } from "./print-state.ts";
|
|
10
5
|
|
|
11
6
|
registerCommand("state", async (args) => {
|
|
12
7
|
const session = args.global.session;
|
|
13
8
|
|
|
14
9
|
if (!DaemonClient.isRunning(session)) {
|
|
15
10
|
console.error(`No active session "${session}"`);
|
|
16
|
-
console.error(" -> Try:
|
|
11
|
+
console.error(" -> Try: agent-dbg launch --brk node app.js");
|
|
17
12
|
return 1;
|
|
18
13
|
}
|
|
19
14
|
|
|
@@ -53,72 +48,7 @@ registerCommand("state", async (args) => {
|
|
|
53
48
|
return 0;
|
|
54
49
|
}
|
|
55
50
|
|
|
56
|
-
|
|
57
|
-
if (data.status !== "paused") {
|
|
58
|
-
const icon = data.status === "running" ? "\u25B6" : "\u25CB";
|
|
59
|
-
console.log(`${icon} ${data.status === "running" ? "Running" : "Idle"}`);
|
|
60
|
-
return 0;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Paused state — header
|
|
64
|
-
const loc = data.location
|
|
65
|
-
? `${data.location.url}:${data.location.line}${data.location.column !== undefined ? `:${data.location.column}` : ""}`
|
|
66
|
-
: "unknown";
|
|
67
|
-
const reason = data.reason ?? "unknown";
|
|
68
|
-
console.log(`\u23F8 Paused at ${loc} (${reason})`);
|
|
69
|
-
|
|
70
|
-
const showAll = !stateArgs.vars && !stateArgs.stack && !stateArgs.breakpoints && !stateArgs.code;
|
|
71
|
-
|
|
72
|
-
// Source section
|
|
73
|
-
if ((showAll || stateArgs.code) && data.source?.lines) {
|
|
74
|
-
console.log("");
|
|
75
|
-
console.log("Source:");
|
|
76
|
-
const sourceLines: SourceLine[] = data.source.lines.map((l) => ({
|
|
77
|
-
lineNumber: l.line,
|
|
78
|
-
content: l.text,
|
|
79
|
-
isCurrent: l.current,
|
|
80
|
-
currentColumn: l.current ? data.location?.column : undefined,
|
|
81
|
-
}));
|
|
82
|
-
console.log(formatSource(sourceLines));
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Locals section
|
|
86
|
-
if ((showAll || stateArgs.vars) && data.locals) {
|
|
87
|
-
console.log("");
|
|
88
|
-
console.log("Locals:");
|
|
89
|
-
const vars: Variable[] = data.locals.map((v) => ({
|
|
90
|
-
ref: v.ref,
|
|
91
|
-
name: v.name,
|
|
92
|
-
value: v.value,
|
|
93
|
-
}));
|
|
94
|
-
const formatted = formatVariables(vars);
|
|
95
|
-
if (formatted) {
|
|
96
|
-
console.log(formatted);
|
|
97
|
-
} else {
|
|
98
|
-
console.log(" (no locals)");
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Stack section
|
|
103
|
-
if ((showAll || stateArgs.stack) && data.stack) {
|
|
104
|
-
console.log("");
|
|
105
|
-
console.log("Stack:");
|
|
106
|
-
const frames: StackFrame[] = data.stack.map((f) => ({
|
|
107
|
-
ref: f.ref,
|
|
108
|
-
functionName: f.functionName,
|
|
109
|
-
file: f.file,
|
|
110
|
-
line: f.line,
|
|
111
|
-
column: f.column,
|
|
112
|
-
isAsync: f.isAsync,
|
|
113
|
-
}));
|
|
114
|
-
console.log(formatStack(frames));
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Breakpoints section
|
|
118
|
-
if ((showAll || stateArgs.breakpoints) && data.breakpointCount !== undefined) {
|
|
119
|
-
console.log("");
|
|
120
|
-
console.log(`Breakpoints: ${data.breakpointCount} active`);
|
|
121
|
-
}
|
|
51
|
+
printState(data);
|
|
122
52
|
|
|
123
53
|
return 0;
|
|
124
54
|
});
|
package/src/commands/status.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { registerCommand } from "../cli/registry.ts";
|
|
2
2
|
import { DaemonClient } from "../daemon/client.ts";
|
|
3
|
+
import { shortPath } from "../formatter/path.ts";
|
|
3
4
|
|
|
4
5
|
registerCommand("status", async (args) => {
|
|
5
6
|
const session = args.global.session;
|
|
6
7
|
|
|
7
8
|
if (!DaemonClient.isRunning(session)) {
|
|
8
9
|
console.error(`No active session "${session}"`);
|
|
9
|
-
console.error(" -> Try:
|
|
10
|
+
console.error(" -> Try: agent-dbg launch --brk node app.js");
|
|
10
11
|
return 1;
|
|
11
12
|
}
|
|
12
13
|
|
|
@@ -47,7 +48,7 @@ registerCommand("status", async (args) => {
|
|
|
47
48
|
|
|
48
49
|
if (data.pauseInfo) {
|
|
49
50
|
const loc = data.pauseInfo.url
|
|
50
|
-
? `${data.pauseInfo.url}:${data.pauseInfo.line}${data.pauseInfo.column !== undefined ? `:${data.pauseInfo.column}` : ""}`
|
|
51
|
+
? `${shortPath(data.pauseInfo.url)}:${data.pauseInfo.line}${data.pauseInfo.column !== undefined ? `:${data.pauseInfo.column}` : ""}`
|
|
51
52
|
: "unknown";
|
|
52
53
|
console.log(` Paused: ${data.pauseInfo.reason} at ${loc}`);
|
|
53
54
|
}
|
package/src/commands/step.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { registerCommand } from "../cli/registry.ts";
|
|
2
2
|
import { DaemonClient } from "../daemon/client.ts";
|
|
3
|
-
import type {
|
|
3
|
+
import type { StateSnapshot } from "../daemon/session.ts";
|
|
4
|
+
import { printState } from "./print-state.ts";
|
|
4
5
|
|
|
5
6
|
registerCommand("step", async (args) => {
|
|
6
7
|
const session = args.global.session;
|
|
7
8
|
|
|
8
9
|
if (!DaemonClient.isRunning(session)) {
|
|
9
10
|
console.error(`No active session "${session}"`);
|
|
10
|
-
console.error(" -> Try:
|
|
11
|
+
console.error(" -> Try: agent-dbg launch --brk node app.js");
|
|
11
12
|
return 1;
|
|
12
13
|
}
|
|
13
14
|
|
|
@@ -24,27 +25,13 @@ registerCommand("step", async (args) => {
|
|
|
24
25
|
return 1;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
const data = response.data as
|
|
28
|
+
const data = response.data as StateSnapshot;
|
|
28
29
|
|
|
29
30
|
if (args.global.json) {
|
|
30
31
|
console.log(JSON.stringify(data, null, 2));
|
|
31
32
|
} else {
|
|
32
|
-
|
|
33
|
+
printState(data);
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
return 0;
|
|
36
37
|
});
|
|
37
|
-
|
|
38
|
-
function printStatus(data: SessionStatus): void {
|
|
39
|
-
if (data.state === "paused" && data.pauseInfo) {
|
|
40
|
-
const col = data.pauseInfo.column !== undefined ? `:${data.pauseInfo.column + 1}` : "";
|
|
41
|
-
const loc = data.pauseInfo.url
|
|
42
|
-
? `${data.pauseInfo.url}:${(data.pauseInfo.line ?? 0) + 1}${col}`
|
|
43
|
-
: "unknown";
|
|
44
|
-
console.log(`Paused at ${loc} (${data.pauseInfo.reason})`);
|
|
45
|
-
} else if (data.state === "running") {
|
|
46
|
-
console.log("Running");
|
|
47
|
-
} else {
|
|
48
|
-
console.log(`${data.state}`);
|
|
49
|
-
}
|
|
50
|
-
}
|
package/src/commands/vars.ts
CHANGED
|
@@ -8,7 +8,7 @@ registerCommand("vars", async (args) => {
|
|
|
8
8
|
|
|
9
9
|
if (!DaemonClient.isRunning(session)) {
|
|
10
10
|
console.error(`No active session "${session}"`);
|
|
11
|
-
console.error(" -> Try:
|
|
11
|
+
console.error(" -> Try: agent-dbg launch --brk node app.js");
|
|
12
12
|
return 1;
|
|
13
13
|
}
|
|
14
14
|
|
|
@@ -45,6 +45,7 @@ registerCommand("vars", async (args) => {
|
|
|
45
45
|
name: string;
|
|
46
46
|
type: string;
|
|
47
47
|
value: string;
|
|
48
|
+
scope: string;
|
|
48
49
|
}>;
|
|
49
50
|
|
|
50
51
|
if (args.global.json) {
|
|
@@ -61,6 +62,7 @@ registerCommand("vars", async (args) => {
|
|
|
61
62
|
ref: v.ref,
|
|
62
63
|
name: v.name,
|
|
63
64
|
value: v.value,
|
|
65
|
+
scope: v.scope,
|
|
64
66
|
}));
|
|
65
67
|
const formatted = formatVariables(vars);
|
|
66
68
|
if (formatted) {
|
package/src/daemon/entry.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { DebugSession } from "./session.ts";
|
|
|
6
6
|
const daemonIdx = process.argv.indexOf("--daemon");
|
|
7
7
|
const session = daemonIdx !== -1 ? process.argv[daemonIdx + 1] : process.argv[2];
|
|
8
8
|
if (!session) {
|
|
9
|
-
console.error("Usage:
|
|
9
|
+
console.error("Usage: agent-dbg --daemon <session> [--timeout <seconds>]");
|
|
10
10
|
process.exit(1);
|
|
11
11
|
}
|
|
12
12
|
|
|
@@ -52,32 +52,37 @@ server.onRequest(async (req: DaemonRequest): Promise<DaemonResponse> => {
|
|
|
52
52
|
|
|
53
53
|
case "continue": {
|
|
54
54
|
await debugSession.continue();
|
|
55
|
-
|
|
55
|
+
const stateAfter = await debugSession.buildState();
|
|
56
|
+
return { ok: true, data: stateAfter };
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
case "step": {
|
|
59
60
|
const { mode = "over" } = req.args;
|
|
60
61
|
await debugSession.step(mode);
|
|
61
|
-
|
|
62
|
+
const stateAfter = await debugSession.buildState();
|
|
63
|
+
return { ok: true, data: stateAfter };
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
case "pause": {
|
|
65
67
|
await debugSession.pause();
|
|
66
|
-
|
|
68
|
+
const stateAfter = await debugSession.buildState();
|
|
69
|
+
return { ok: true, data: stateAfter };
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
case "run-to": {
|
|
70
73
|
const { file, line } = req.args;
|
|
71
74
|
await debugSession.runTo(file, line);
|
|
72
|
-
|
|
75
|
+
const stateAfter = await debugSession.buildState();
|
|
76
|
+
return { ok: true, data: stateAfter };
|
|
73
77
|
}
|
|
74
78
|
|
|
75
79
|
case "break": {
|
|
76
|
-
const { file, line, condition, hitCount, urlRegex } = req.args;
|
|
80
|
+
const { file, line, condition, hitCount, urlRegex, column } = req.args;
|
|
77
81
|
const bpResult = await debugSession.setBreakpoint(file, line, {
|
|
78
82
|
condition,
|
|
79
83
|
hitCount,
|
|
80
84
|
urlRegex,
|
|
85
|
+
column,
|
|
81
86
|
});
|
|
82
87
|
return { ok: true, data: bpResult };
|
|
83
88
|
}
|
|
@@ -229,6 +234,11 @@ server.onRequest(async (req: DaemonRequest): Promise<DaemonResponse> => {
|
|
|
229
234
|
return { ok: true, data: "disabled" };
|
|
230
235
|
}
|
|
231
236
|
|
|
237
|
+
case "restart": {
|
|
238
|
+
const result = await debugSession.restart();
|
|
239
|
+
return { ok: true, data: result };
|
|
240
|
+
}
|
|
241
|
+
|
|
232
242
|
case "stop":
|
|
233
243
|
await debugSession.stop();
|
|
234
244
|
setTimeout(() => {
|
package/src/daemon/paths.ts
CHANGED
|
@@ -4,10 +4,10 @@ import { join } from "node:path";
|
|
|
4
4
|
export function getSocketDir(): string {
|
|
5
5
|
const xdgRuntime = process.env.XDG_RUNTIME_DIR;
|
|
6
6
|
if (xdgRuntime) {
|
|
7
|
-
return join(xdgRuntime, "
|
|
7
|
+
return join(xdgRuntime, "agent-dbg");
|
|
8
8
|
}
|
|
9
9
|
const tmpdir = process.env.TMPDIR || "/tmp";
|
|
10
|
-
return join(tmpdir, `
|
|
10
|
+
return join(tmpdir, `agent-dbg-${process.getuid?.() ?? 0}`);
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export function getSocketPath(session: string): string {
|
|
@@ -18,6 +18,10 @@ export function getLockPath(session: string): string {
|
|
|
18
18
|
return join(getSocketDir(), `${session}.lock`);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
export function getLogPath(session: string): string {
|
|
22
|
+
return join(getSocketDir(), `${session}.cdp.log`);
|
|
23
|
+
}
|
|
24
|
+
|
|
21
25
|
export function ensureSocketDir(): void {
|
|
22
26
|
const dir = getSocketDir();
|
|
23
27
|
if (!existsSync(dir)) {
|
package/src/daemon/server.ts
CHANGED
|
@@ -5,7 +5,7 @@ export async function setBreakpoint(
|
|
|
5
5
|
session: DebugSession,
|
|
6
6
|
file: string,
|
|
7
7
|
line: number,
|
|
8
|
-
options?: { condition?: string; hitCount?: number; urlRegex?: string },
|
|
8
|
+
options?: { condition?: string; hitCount?: number; urlRegex?: string; column?: number },
|
|
9
9
|
): Promise<{ ref: string; location: { url: string; line: number; column?: number } }> {
|
|
10
10
|
if (!session.cdp) {
|
|
11
11
|
throw new Error("No active debug session");
|
|
@@ -17,14 +17,16 @@ export async function setBreakpoint(
|
|
|
17
17
|
let originalFile: string | null = null;
|
|
18
18
|
let originalLine: number | null = null;
|
|
19
19
|
let actualLine = line;
|
|
20
|
+
let actualColumn: number | undefined = options?.column !== undefined ? options.column - 1 : undefined; // user column is 1-based
|
|
20
21
|
let actualFile = file;
|
|
21
22
|
|
|
22
23
|
if (!options?.urlRegex) {
|
|
23
|
-
const generated = session.sourceMapResolver.toGenerated(file, line, 0);
|
|
24
|
+
const generated = session.sourceMapResolver.toGenerated(file, line, actualColumn ?? 0);
|
|
24
25
|
if (generated) {
|
|
25
26
|
originalFile = file;
|
|
26
27
|
originalLine = line;
|
|
27
28
|
actualLine = generated.line;
|
|
29
|
+
actualColumn = generated.column;
|
|
28
30
|
// Find the URL of the generated script
|
|
29
31
|
const scriptInfo = session.scripts.get(generated.scriptId);
|
|
30
32
|
if (scriptInfo) {
|
|
@@ -36,6 +38,9 @@ export async function setBreakpoint(
|
|
|
36
38
|
const params: Protocol.Debugger.SetBreakpointByUrlRequest = {
|
|
37
39
|
lineNumber: actualLine - 1, // CDP uses 0-based lines
|
|
38
40
|
};
|
|
41
|
+
if (actualColumn !== undefined) {
|
|
42
|
+
params.columnNumber = actualColumn;
|
|
43
|
+
}
|
|
39
44
|
|
|
40
45
|
let url: string | null = null;
|
|
41
46
|
if (options?.urlRegex) {
|
|
@@ -54,7 +54,7 @@ export async function evalExpression(
|
|
|
54
54
|
for (const ref of refMatches) {
|
|
55
55
|
const remoteId = session.refs.resolveId(ref);
|
|
56
56
|
if (remoteId) {
|
|
57
|
-
const argName = `
|
|
57
|
+
const argName = `__adbg_ref_${ref.slice(1)}`;
|
|
58
58
|
resolvedExpression = resolvedExpression.replace(ref, argName);
|
|
59
59
|
refEntries.push({
|
|
60
60
|
ref,
|
|
@@ -139,7 +139,7 @@ export async function evalExpression(
|
|
|
139
139
|
export async function getVars(
|
|
140
140
|
session: DebugSession,
|
|
141
141
|
options: { frame?: string; names?: string[]; allScopes?: boolean } = {},
|
|
142
|
-
): Promise<Array<{ ref: string; name: string; type: string; value: string }>> {
|
|
142
|
+
): Promise<Array<{ ref: string; name: string; type: string; value: string; scope: string }>> {
|
|
143
143
|
if (!session.cdp) {
|
|
144
144
|
throw new Error("No active debug session");
|
|
145
145
|
}
|
|
@@ -174,19 +174,14 @@ export async function getVars(
|
|
|
174
174
|
name: string;
|
|
175
175
|
type: string;
|
|
176
176
|
value: string;
|
|
177
|
+
scope: string;
|
|
177
178
|
}> = [];
|
|
178
179
|
|
|
179
180
|
for (const scope of scopeChain) {
|
|
180
181
|
const scopeType = scope.type;
|
|
181
182
|
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
const includeScope =
|
|
185
|
-
scopeType === "local" ||
|
|
186
|
-
scopeType === "module" ||
|
|
187
|
-
scopeType === "block" ||
|
|
188
|
-
scopeType === "script" ||
|
|
189
|
-
(options.allScopes && scopeType === "closure");
|
|
183
|
+
// Show all scopes except "global" (too noisy — thousands of entries)
|
|
184
|
+
const includeScope = scopeType !== "global";
|
|
190
185
|
|
|
191
186
|
if (includeScope) {
|
|
192
187
|
const scopeObj = scope.object;
|
|
@@ -223,6 +218,7 @@ export async function getVars(
|
|
|
223
218
|
name: propName,
|
|
224
219
|
type: propValue.type,
|
|
225
220
|
value: formatValue(propValue),
|
|
221
|
+
scope: scopeType,
|
|
226
222
|
});
|
|
227
223
|
}
|
|
228
224
|
}
|
|
@@ -198,13 +198,13 @@ export async function buildState(
|
|
|
198
198
|
try {
|
|
199
199
|
const scopeChain = targetFrame.scopeChain;
|
|
200
200
|
if (scopeChain) {
|
|
201
|
-
const
|
|
201
|
+
const vars: Array<{ ref: string; name: string; value: string; scope: string }> = [];
|
|
202
202
|
|
|
203
203
|
for (const scope of scopeChain) {
|
|
204
204
|
const scopeType = scope.type;
|
|
205
205
|
|
|
206
|
-
//
|
|
207
|
-
if (scopeType
|
|
206
|
+
// Show all scopes except "global" (too noisy — thousands of entries)
|
|
207
|
+
if (scopeType !== "global") {
|
|
208
208
|
const scopeObj = scope.object;
|
|
209
209
|
const objectId = scopeObj.objectId;
|
|
210
210
|
if (!objectId) continue;
|
|
@@ -229,10 +229,11 @@ export async function buildState(
|
|
|
229
229
|
const remoteId = propValue.objectId ?? `primitive:${propName}`;
|
|
230
230
|
const ref = session.refs.addVar(remoteId as string, propName);
|
|
231
231
|
|
|
232
|
-
|
|
232
|
+
vars.push({
|
|
233
233
|
ref,
|
|
234
234
|
name: propName,
|
|
235
235
|
value: formatValue(propValue),
|
|
236
|
+
scope: scopeType,
|
|
236
237
|
});
|
|
237
238
|
}
|
|
238
239
|
}
|
|
@@ -241,7 +242,7 @@ export async function buildState(
|
|
|
241
242
|
if (scopeType === "global") continue;
|
|
242
243
|
}
|
|
243
244
|
|
|
244
|
-
snapshot.
|
|
245
|
+
snapshot.vars = vars;
|
|
245
246
|
}
|
|
246
247
|
} catch {
|
|
247
248
|
// Variables not available
|
package/src/daemon/session.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { Subprocess } from "bun";
|
|
2
2
|
import type Protocol from "devtools-protocol/types/protocol.js";
|
|
3
3
|
import { CdpClient } from "../cdp/client.ts";
|
|
4
|
+
import { CdpLogger } from "../cdp/logger.ts";
|
|
4
5
|
import type { RemoteObject } from "../formatter/values.ts";
|
|
5
6
|
import { formatValue } from "../formatter/values.ts";
|
|
6
7
|
import { RefTable } from "../refs/ref-table.ts";
|
|
7
8
|
import { SourceMapResolver } from "../sourcemap/resolver.ts";
|
|
9
|
+
import { ensureSocketDir, getLogPath } from "./paths.ts";
|
|
8
10
|
import {
|
|
9
11
|
addBlackbox as addBlackboxImpl,
|
|
10
12
|
listBlackbox as listBlackboxImpl,
|
|
@@ -73,7 +75,7 @@ export interface StateSnapshot {
|
|
|
73
75
|
reason?: string;
|
|
74
76
|
location?: { url: string; line: number; column?: number };
|
|
75
77
|
source?: { lines: Array<{ line: number; text: string; current?: boolean }> };
|
|
76
|
-
|
|
78
|
+
vars?: Array<{ ref: string; name: string; value: string; scope: string }>;
|
|
77
79
|
stack?: Array<{
|
|
78
80
|
ref: string;
|
|
79
81
|
functionName: string;
|
|
@@ -152,9 +154,14 @@ export class DebugSession {
|
|
|
152
154
|
blackboxPatterns: string[] = [];
|
|
153
155
|
disabledBreakpoints: Map<string, { breakpointId: string; meta: Record<string, unknown> }> =
|
|
154
156
|
new Map();
|
|
157
|
+
launchCommand: string[] | null = null;
|
|
158
|
+
launchOptions: { brk?: boolean; port?: number } | null = null;
|
|
159
|
+
cdpLogger: CdpLogger;
|
|
155
160
|
|
|
156
161
|
constructor(session: string) {
|
|
157
162
|
this.session = session;
|
|
163
|
+
ensureSocketDir();
|
|
164
|
+
this.cdpLogger = new CdpLogger(getLogPath(session));
|
|
158
165
|
}
|
|
159
166
|
|
|
160
167
|
// ── Session lifecycle ─────────────────────────────────────────────
|
|
@@ -171,6 +178,9 @@ export class DebugSession {
|
|
|
171
178
|
throw new Error("Command array must not be empty");
|
|
172
179
|
}
|
|
173
180
|
|
|
181
|
+
this.launchCommand = command;
|
|
182
|
+
this.launchOptions = options;
|
|
183
|
+
|
|
174
184
|
const brk = options.brk ?? true;
|
|
175
185
|
const port = options.port ?? 0;
|
|
176
186
|
const inspectFlag = brk ? `--inspect-brk=${port}` : `--inspect=${port}`;
|
|
@@ -311,6 +321,16 @@ export class DebugSession {
|
|
|
311
321
|
this.sourceMapResolver.clear();
|
|
312
322
|
}
|
|
313
323
|
|
|
324
|
+
async restart(): Promise<LaunchResult> {
|
|
325
|
+
if (!this.launchCommand) {
|
|
326
|
+
throw new Error("No previous launch to restart. Use 'launch' first.");
|
|
327
|
+
}
|
|
328
|
+
const command = this.launchCommand;
|
|
329
|
+
const options = this.launchOptions ?? {};
|
|
330
|
+
await this.stop();
|
|
331
|
+
return this.launch(command, options);
|
|
332
|
+
}
|
|
333
|
+
|
|
314
334
|
get sessionState(): "idle" | "running" | "paused" {
|
|
315
335
|
return this.state;
|
|
316
336
|
}
|
|
@@ -653,7 +673,7 @@ export class DebugSession {
|
|
|
653
673
|
|
|
654
674
|
buildBreakpointCondition(condition?: string, hitCount?: number): string | undefined {
|
|
655
675
|
if (hitCount && hitCount > 0) {
|
|
656
|
-
const countVar = `
|
|
676
|
+
const countVar = `__adbg_bp_count_${Date.now()}`;
|
|
657
677
|
const hitExpr = `(typeof ${countVar} === "undefined" ? (${countVar} = 1) : ++${countVar}) >= ${hitCount}`;
|
|
658
678
|
if (condition) {
|
|
659
679
|
return `(${hitExpr}) && (${condition})`;
|
|
@@ -721,7 +741,7 @@ export class DebugSession {
|
|
|
721
741
|
}
|
|
722
742
|
|
|
723
743
|
private async connectCdp(wsUrl: string): Promise<void> {
|
|
724
|
-
const cdp = await CdpClient.connect(wsUrl);
|
|
744
|
+
const cdp = await CdpClient.connect(wsUrl, this.cdpLogger);
|
|
725
745
|
this.cdp = cdp;
|
|
726
746
|
|
|
727
747
|
// Set up event handlers before enabling domains so we don't miss any events
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { CdpLogEntry } from "../cdp/logger.ts";
|
|
2
|
+
|
|
3
|
+
function formatTime(ts: number): string {
|
|
4
|
+
const d = new Date(ts);
|
|
5
|
+
const h = String(d.getHours()).padStart(2, "0");
|
|
6
|
+
const m = String(d.getMinutes()).padStart(2, "0");
|
|
7
|
+
const s = String(d.getSeconds()).padStart(2, "0");
|
|
8
|
+
const ms = String(d.getMilliseconds()).padStart(3, "0");
|
|
9
|
+
return `${h}:${m}:${s}.${ms}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function truncate(s: string, max: number): string {
|
|
13
|
+
return s.length > max ? `${s.slice(0, max - 3)}...` : s;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type Summarizer = (entry: CdpLogEntry) => string;
|
|
17
|
+
|
|
18
|
+
const eventSummarizers: Record<string, Summarizer> = {
|
|
19
|
+
"Debugger.scriptParsed": (e) => {
|
|
20
|
+
const p = e.params ?? {};
|
|
21
|
+
const url = (p.url as string) || "(anonymous)";
|
|
22
|
+
const lines = p.endLine != null ? Number(p.endLine) + 1 : "?";
|
|
23
|
+
const hasMap = p.sourceMapURL ? "yes" : "no";
|
|
24
|
+
return `scriptId=${p.scriptId} url=${url} lines=${lines} sourceMap=${hasMap}`;
|
|
25
|
+
},
|
|
26
|
+
"Debugger.paused": (e) => {
|
|
27
|
+
const p = e.params ?? {};
|
|
28
|
+
const reason = (p.reason as string) ?? "unknown";
|
|
29
|
+
const frames = p.callFrames as Array<Record<string, unknown>> | undefined;
|
|
30
|
+
const top = frames?.[0];
|
|
31
|
+
let loc = "";
|
|
32
|
+
if (top) {
|
|
33
|
+
const location = top.location as Record<string, unknown> | undefined;
|
|
34
|
+
const url = (top.url as string) || `script:${location?.scriptId}`;
|
|
35
|
+
loc = ` location=${url}:${location?.lineNumber}`;
|
|
36
|
+
}
|
|
37
|
+
const count = frames?.length ?? 0;
|
|
38
|
+
return `reason=${reason}${loc} callFrames=${count}`;
|
|
39
|
+
},
|
|
40
|
+
"Debugger.resumed": () => "",
|
|
41
|
+
"Runtime.consoleAPICalled": (e) => {
|
|
42
|
+
const p = e.params ?? {};
|
|
43
|
+
const type = (p.type as string) ?? "log";
|
|
44
|
+
const args = p.args as Array<unknown> | undefined;
|
|
45
|
+
return `type=${type} args=${args?.length ?? 0}`;
|
|
46
|
+
},
|
|
47
|
+
"Runtime.exceptionThrown": (e) => {
|
|
48
|
+
const p = e.params ?? {};
|
|
49
|
+
const detail = p.exceptionDetails as Record<string, unknown> | undefined;
|
|
50
|
+
const text = (detail?.text as string) ?? "";
|
|
51
|
+
return truncate(text, 80);
|
|
52
|
+
},
|
|
53
|
+
"Runtime.executionContextCreated": (e) => {
|
|
54
|
+
const p = e.params ?? {};
|
|
55
|
+
const ctx = p.context as Record<string, unknown> | undefined;
|
|
56
|
+
return `contextId=${ctx?.id}`;
|
|
57
|
+
},
|
|
58
|
+
"Runtime.executionContextDestroyed": (e) => {
|
|
59
|
+
const p = e.params ?? {};
|
|
60
|
+
return `executionContextId=${p.executionContextId}`;
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const responseSummarizers: Record<string, Summarizer> = {
|
|
65
|
+
"Debugger.setBreakpointByUrl": (e) => {
|
|
66
|
+
const r = (e.result ?? {}) as Record<string, unknown>;
|
|
67
|
+
return `breakpointId=${r.breakpointId}`;
|
|
68
|
+
},
|
|
69
|
+
"Debugger.setBreakpoint": (e) => {
|
|
70
|
+
const r = (e.result ?? {}) as Record<string, unknown>;
|
|
71
|
+
return `breakpointId=${r.breakpointId}`;
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
function summarizeParams(entry: CdpLogEntry): string {
|
|
76
|
+
const p = entry.params;
|
|
77
|
+
if (!p || Object.keys(p).length === 0) return "";
|
|
78
|
+
return truncate(JSON.stringify(p), 120);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function summarizeResult(entry: CdpLogEntry): string {
|
|
82
|
+
const summarizer = responseSummarizers[entry.method];
|
|
83
|
+
if (summarizer) return summarizer(entry);
|
|
84
|
+
// Don't dump large results by default
|
|
85
|
+
return "";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function formatLogEntry(entry: CdpLogEntry): string {
|
|
89
|
+
const time = `[${formatTime(entry.ts)}]`;
|
|
90
|
+
|
|
91
|
+
if (entry.dir === "send") {
|
|
92
|
+
const params = summarizeParams(entry);
|
|
93
|
+
return `${time} -> ${entry.method}${params ? ` ${params}` : ""}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (entry.dir === "recv") {
|
|
97
|
+
const idStr = entry.id != null ? ` #${entry.id}` : "";
|
|
98
|
+
const msStr = entry.ms != null ? ` (${entry.ms}ms)` : "";
|
|
99
|
+
if (entry.error) {
|
|
100
|
+
return `${time} <- ${entry.method}${idStr}${msStr} ERROR: ${entry.error.message}`;
|
|
101
|
+
}
|
|
102
|
+
const summary = summarizeResult(entry);
|
|
103
|
+
return `${time} <- ${entry.method}${idStr}${msStr}${summary ? ` ${summary}` : ""}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// event
|
|
107
|
+
const summarizer = eventSummarizers[entry.method];
|
|
108
|
+
const summary = summarizer ? summarizer(entry) : summarizeParams(entry);
|
|
109
|
+
return `${time} <- ${entry.method}${summary ? ` ${summary}` : ""}`;
|
|
110
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { relative } from "node:path";
|
|
2
|
+
|
|
3
|
+
const cwd = process.cwd();
|
|
4
|
+
|
|
5
|
+
/** Shorten a file path for display: strip file:// prefix, make relative to cwd. */
|
|
6
|
+
export function shortPath(path: string): string {
|
|
7
|
+
// Strip file:// protocol
|
|
8
|
+
if (path.startsWith("file://")) {
|
|
9
|
+
path = path.slice(7);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Keep node: and other protocol URLs as-is
|
|
13
|
+
if (path.includes("://") || path.startsWith("node:")) {
|
|
14
|
+
return path;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Make relative to cwd
|
|
18
|
+
if (path.startsWith("/")) {
|
|
19
|
+
const rel = relative(cwd, path);
|
|
20
|
+
// Only use relative if it's actually shorter and doesn't escape too far
|
|
21
|
+
if (!rel.startsWith("../../..") && rel.length < path.length) {
|
|
22
|
+
return rel.startsWith("..") ? rel : `./${rel}`;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return path;
|
|
27
|
+
}
|
package/src/formatter/stack.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { shortPath } from "./path.ts";
|
|
2
|
+
|
|
1
3
|
export interface StackFrame {
|
|
2
4
|
ref: string;
|
|
3
5
|
functionName: string;
|
|
@@ -59,10 +61,11 @@ export function formatStack(frames: StackFrame[]): string {
|
|
|
59
61
|
|
|
60
62
|
const ref = frame.ref.padEnd(maxRefLen);
|
|
61
63
|
const name = frame.functionName.padEnd(maxNameLen);
|
|
64
|
+
const file = shortPath(frame.file);
|
|
62
65
|
const loc =
|
|
63
66
|
frame.column !== undefined
|
|
64
|
-
? `${
|
|
65
|
-
: `${
|
|
67
|
+
? `${file}:${frame.line}:${frame.column}`
|
|
68
|
+
: `${file}:${frame.line}`;
|
|
66
69
|
outputLines.push(`${ref} ${name} ${loc}`);
|
|
67
70
|
}
|
|
68
71
|
|