dap-mcp-server 0.1.12 → 0.1.16

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.
Files changed (58) hide show
  1. package/README.md +25 -7
  2. package/SKILL.md +37 -186
  3. package/build/adapters/detect.d.ts +12 -0
  4. package/build/adapters/detect.js +91 -0
  5. package/build/adapters/detect.js.map +1 -1
  6. package/build/adapters/registry.js +35 -12
  7. package/build/adapters/registry.js.map +1 -1
  8. package/build/diagnostics/doctor.d.ts +64 -0
  9. package/build/diagnostics/doctor.js +209 -0
  10. package/build/diagnostics/doctor.js.map +1 -0
  11. package/build/server.js +305 -83
  12. package/build/server.js.map +1 -1
  13. package/build/session/inspect.d.ts +6 -2
  14. package/build/session/inspect.js +24 -8
  15. package/build/session/inspect.js.map +1 -1
  16. package/build/session/run.js +2 -9
  17. package/build/session/run.js.map +1 -1
  18. package/build/session/session.d.ts +16 -0
  19. package/build/session/session.js +63 -8
  20. package/build/session/session.js.map +1 -1
  21. package/integrations/claude/dap-debugging/.claude-plugin/plugin.json +9 -0
  22. package/integrations/claude/dap-debugging/.mcp.json +8 -0
  23. package/integrations/claude/dap-debugging/skills/dap-debugging/SKILL.md +49 -0
  24. package/package.json +8 -3
  25. package/.forgejo/workflows/publish.yml +0 -63
  26. package/CLAUDE.md +0 -71
  27. package/TODO.md +0 -32
  28. package/docs/plans/2026-03-05-dap-wrapper-mcp-design.md +0 -326
  29. package/docs/plans/2026-03-05-dap-wrapper-mcp-implementation.md +0 -2815
  30. package/src/adapters/builtin.ts +0 -75
  31. package/src/adapters/detect.test.ts +0 -36
  32. package/src/adapters/detect.ts +0 -36
  33. package/src/adapters/registry.test.ts +0 -74
  34. package/src/adapters/registry.ts +0 -86
  35. package/src/dap/dap-client.test.ts +0 -118
  36. package/src/dap/dap-client.ts +0 -588
  37. package/src/dap/protocol.test.ts +0 -82
  38. package/src/dap/protocol.ts +0 -78
  39. package/src/dap/ring-buffer.test.ts +0 -82
  40. package/src/dap/ring-buffer.ts +0 -64
  41. package/src/dap/types.ts +0 -115
  42. package/src/index.ts +0 -30
  43. package/src/integration/debug-lifecycle.test.ts +0 -131
  44. package/src/server.ts +0 -1096
  45. package/src/session/inspect.test.ts +0 -126
  46. package/src/session/inspect.ts +0 -160
  47. package/src/session/run.test.ts +0 -74
  48. package/src/session/run.ts +0 -144
  49. package/src/session/session-manager.test.ts +0 -74
  50. package/src/session/session-manager.ts +0 -67
  51. package/src/session/session.test.ts +0 -126
  52. package/src/session/session.ts +0 -482
  53. package/src/test-fixtures/crash-adapter.js +0 -23
  54. package/src/test-fixtures/mock-adapter.js +0 -164
  55. package/src/test-fixtures/no-stop-adapter.js +0 -77
  56. package/src/test-fixtures/silent-adapter.js +0 -24
  57. package/src/test-fixtures/tcp-mock-adapter.js +0 -87
  58. package/tsconfig.json +0 -16
@@ -1,588 +0,0 @@
1
- /**
2
- * Copyright (c) 2026 Ivan Iraci <ivan.iraci@professioneit.com>
3
- *
4
- * Permission is hereby granted, free of charge, to any person obtaining a copy
5
- * of this software and associated documentation files (the "Software"), to deal
6
- * in the Software without restriction, including without limitation the rights
7
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
- * copies of the Software, and to permit persons to whom the Software is
9
- * furnished to do so, subject to the following conditions:
10
- *
11
- * The above copyright notice and this permission notice shall be included in
12
- * all copies or substantial portions of the Software.
13
- *
14
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
- * SOFTWARE.
21
- */
22
-
23
- import { spawn, ChildProcess } from "node:child_process";
24
- import { EventEmitter } from "node:events";
25
- import { Socket, createConnection } from "node:net";
26
- import type { Writable } from "node:stream";
27
- import { DebugProtocol } from "@vscode/debugprotocol";
28
- import { encodeMessage, MessageParser } from "./protocol.js";
29
-
30
- export class DAPClientError extends Error {
31
- constructor(message: string) {
32
- super(message);
33
- this.name = "DAPClientError";
34
- }
35
- }
36
-
37
- interface PendingRequest {
38
- resolve: (response: DebugProtocol.Response) => void;
39
- reject: (error: Error) => void;
40
- timer?: ReturnType<typeof setTimeout>;
41
- }
42
-
43
- /**
44
- * One DAP transport connection. Multiple connections can exist when a
45
- * "parent" session (e.g. vscode-js-debug) spawns child sessions via the
46
- * `startDebugging` reverse request. Each connection has its own request
47
- * sequence counter and pending-request map; reading is via its parser.
48
- */
49
- interface Conn {
50
- /** "parent" = the initial connection. "child" = spawned via startDebugging. */
51
- kind: "parent" | "child";
52
- /** DAP-message-framed write target. For stdio, the child stdin; for tcp, the socket. */
53
- writeStream: Writable;
54
- parser: MessageParser;
55
- pending: Map<number, PendingRequest>;
56
- seq: number;
57
- /** Present only for TCP connections. */
58
- socket?: Socket;
59
- /** True once the underlying transport is closed. */
60
- closed: boolean;
61
- }
62
-
63
- export interface DAPClientOptions {
64
- /** "stdio" (default) or "tcp". */
65
- transport?: "stdio" | "tcp";
66
- /**
67
- * Regex with one capture group that matches the TCP port the adapter is
68
- * listening on. Required when transport is "tcp". The regex is applied to
69
- * the cumulative adapter stdout text.
70
- */
71
- portMatcher?: string;
72
- /** Timeout waiting for the adapter to announce its TCP port, in ms. */
73
- tcpConnectTimeout?: number;
74
- }
75
-
76
- export class DAPClient extends EventEmitter {
77
- private runtime: string;
78
- private executable: string;
79
- private args: string[];
80
- private transport: "stdio" | "tcp";
81
- private portMatcher?: RegExp;
82
- private tcpConnectTimeout: number;
83
- private process?: ChildProcess;
84
- private conns: Conn[] = [];
85
- /** Index into `conns` of the connection that handles user-sent requests. */
86
- private activeIdx = -1;
87
- /** TCP target host (set during spawn for tcp mode). */
88
- private tcpHost?: string;
89
- /** TCP target port (set during spawn for tcp mode). */
90
- private tcpPort?: number;
91
- /**
92
- * While a startDebugging handler is bootstrapping a new child session
93
- * (initialize + launch), user-issued `sendRequest` calls await this so they
94
- * don't race the in-progress handshake.
95
- */
96
- private transitionLock?: Promise<void>;
97
- /**
98
- * Last setBreakpoints arguments per source path. Replayed onto new child
99
- * sessions when startDebugging causes the active connection to change —
100
- * adapters like vscode-js-debug spawn a child for the actual debuggee
101
- * AFTER the parent has accepted setBreakpoints, so the child wouldn't know
102
- * about them otherwise.
103
- */
104
- private bpHistory: Map<string, Record<string, unknown>> = new Map();
105
- private excBpHistory?: Record<string, unknown>;
106
- private configurationDoneSent = false;
107
- private stderrOutput: string[] = [];
108
- private stdoutPreamble = "";
109
- private _exited = false;
110
- private _exitCode?: number;
111
-
112
- constructor(
113
- runtime: string,
114
- executable: string,
115
- args: string[] = [],
116
- options: DAPClientOptions = {}
117
- ) {
118
- super();
119
- this.runtime = runtime;
120
- this.executable = executable;
121
- this.args = args;
122
- this.transport = options.transport ?? "stdio";
123
- if (this.transport === "tcp") {
124
- if (!options.portMatcher) {
125
- throw new DAPClientError(
126
- "portMatcher is required when transport is 'tcp'"
127
- );
128
- }
129
- this.portMatcher = new RegExp(options.portMatcher);
130
- }
131
- this.tcpConnectTimeout = options.tcpConnectTimeout ?? 10000;
132
- }
133
-
134
- async spawn(): Promise<void> {
135
- return new Promise<void>((resolve, reject) => {
136
- const runtimeArgs = this.runtime
137
- ? [this.executable, ...this.args]
138
- : this.args;
139
- const cmd = this.runtime || this.executable;
140
-
141
- this.process = spawn(cmd, runtimeArgs, {
142
- stdio: ["pipe", "pipe", "pipe"],
143
- detached: true,
144
- });
145
-
146
- // Detached child must be unref'd so the parent Node process can exit
147
- // even if the adapter is still running (cleanup happens via dispose()).
148
- this.process.unref();
149
-
150
- this.process.stderr!.on("data", (data: Buffer) => {
151
- this.stderrOutput.push(data.toString());
152
- });
153
-
154
- // Suppress EPIPE and other stream errors on stdin; the exit handler handles cleanup
155
- this.process.stdin!.on("error", () => {
156
- // ignore — exit event will reject pending requests
157
- });
158
-
159
- this.process.on("error", (err) => {
160
- reject(new DAPClientError(`Failed to spawn adapter: ${err.message}`));
161
- });
162
-
163
- this.process.on("exit", (code, signal) => {
164
- this._exited = true;
165
- this._exitCode = code ?? undefined;
166
- // Reject pending across all connections.
167
- for (const conn of this.conns) {
168
- for (const [, req] of conn.pending) {
169
- if (req.timer) clearTimeout(req.timer);
170
- req.reject(
171
- new DAPClientError(
172
- `Adapter exited (code=${code}, signal=${signal}). stderr: ${this.stderrOutput.join("")}`
173
- )
174
- );
175
- }
176
- conn.pending.clear();
177
- }
178
- this.emit("exit", code, signal);
179
- });
180
-
181
- if (this.transport === "stdio") {
182
- // DAP messages flow over the adapter's stdin/stdout. Single connection
183
- // (no startDebugging support over stdio).
184
- const conn = this.makeConn("parent", this.process.stdin!, this.process.stdout!);
185
- this.conns.push(conn);
186
- this.activeIdx = 0;
187
- // Resolve once the process is spawned (use flag to prevent double-resolve)
188
- let resolved = false;
189
- this.process.stdout!.once("readable", () => {
190
- if (!resolved) { resolved = true; resolve(); }
191
- });
192
- setTimeout(() => {
193
- if (!resolved) { resolved = true; resolve(); }
194
- }, 100);
195
- } else {
196
- // TCP transport: read the adapter's stdout banner to discover the
197
- // port, then connect via TCP and route DAP messages over the socket.
198
- let portFound = false;
199
- const connectTimer = setTimeout(() => {
200
- if (!portFound) {
201
- reject(
202
- new DAPClientError(
203
- `Timed out waiting for adapter to announce TCP port. ` +
204
- `Pattern: ${this.portMatcher!.source}. ` +
205
- `Stdout so far: ${this.stdoutPreamble.slice(0, 500)}`
206
- )
207
- );
208
- }
209
- }, this.tcpConnectTimeout);
210
-
211
- this.process.stdout!.on("data", (data: Buffer) => {
212
- this.stdoutPreamble += data.toString();
213
- if (portFound) return;
214
- const m = this.portMatcher!.exec(this.stdoutPreamble);
215
- if (m) {
216
- portFound = true;
217
- clearTimeout(connectTimer);
218
- const port = Number(m[1]);
219
- if (!Number.isFinite(port) || port <= 0) {
220
- reject(
221
- new DAPClientError(
222
- `Invalid port matched from adapter stdout: '${m[1]}'`
223
- )
224
- );
225
- return;
226
- }
227
- this.tcpHost = "127.0.0.1";
228
- this.tcpPort = port;
229
- this.openTcpConnection("parent")
230
- .then((conn) => {
231
- this.conns.push(conn);
232
- this.activeIdx = 0;
233
- resolve();
234
- })
235
- .catch(reject);
236
- }
237
- });
238
- }
239
- });
240
- }
241
-
242
- /**
243
- * Record session-shaping requests so we can replay them on child sessions.
244
- * `setBreakpoints` and `setExceptionBreakpoints` need to be re-sent because
245
- * each DAP session has its own breakpoint state. `configurationDone` is
246
- * tracked so we know whether the parent has already finished its
247
- * configuration phase — if so, child sessions need the same signal.
248
- */
249
- private trackOutgoing(command: string, args: Record<string, unknown> | undefined): void {
250
- if (command === "setBreakpoints" && args) {
251
- const src = (args.source as { path?: string } | undefined)?.path;
252
- if (src) {
253
- this.bpHistory.set(src, args);
254
- }
255
- } else if (command === "setExceptionBreakpoints" && args) {
256
- this.excBpHistory = args;
257
- } else if (command === "configurationDone") {
258
- this.configurationDoneSent = true;
259
- }
260
- }
261
-
262
- /** Build a Conn around a writable/readable stream pair. */
263
- private makeConn(kind: "parent" | "child", writeStream: Writable, dataStream: NodeJS.ReadableStream): Conn {
264
- const conn: Conn = {
265
- kind,
266
- writeStream,
267
- parser: new MessageParser((msg) => this.handleMessage(msg, conn)),
268
- pending: new Map(),
269
- seq: 1,
270
- closed: false,
271
- };
272
- dataStream.on("data", (data: Buffer) => conn.parser.feed(data));
273
- return conn;
274
- }
275
-
276
- /** Open a fresh TCP connection to the adapter's DAP server. */
277
- private openTcpConnection(kind: "parent" | "child"): Promise<Conn> {
278
- return new Promise<Conn>((resolve, reject) => {
279
- if (this.tcpHost === undefined || this.tcpPort === undefined) {
280
- reject(new DAPClientError("TCP target host/port not set"));
281
- return;
282
- }
283
- const sock = createConnection({ host: this.tcpHost, port: this.tcpPort }, () => {
284
- const conn = this.makeConn(kind, sock, sock);
285
- conn.socket = sock;
286
- sock.on("close", () => this.onConnClose(conn));
287
- sock.on("error", (err) => {
288
- this.stderrOutput.push(`socket error (${kind}): ${err.message}\n`);
289
- });
290
- resolve(conn);
291
- });
292
- sock.once("error", (err) => {
293
- reject(
294
- new DAPClientError(
295
- `Failed to connect to adapter TCP port ${this.tcpPort}: ${err.message}`
296
- )
297
- );
298
- });
299
- });
300
- }
301
-
302
- private onConnClose(conn: Conn): void {
303
- if (conn.closed) return;
304
- conn.closed = true;
305
- for (const [, req] of conn.pending) {
306
- if (req.timer) clearTimeout(req.timer);
307
- req.reject(
308
- new DAPClientError(
309
- `DAP ${conn.kind} connection closed before response. stderr: ${this.stderrOutput.join("")}`
310
- )
311
- );
312
- }
313
- conn.pending.clear();
314
- // If the active connection just closed, fall back to the most recent
315
- // non-closed one. If none remain, leave activeIdx pointing at the closed
316
- // conn — sendRequest will fail with "transport is not writable".
317
- if (this.conns[this.activeIdx] === conn) {
318
- for (let i = this.conns.length - 1; i >= 0; i--) {
319
- if (!this.conns[i].closed) {
320
- this.activeIdx = i;
321
- break;
322
- }
323
- }
324
- }
325
- }
326
-
327
- async sendRequest(
328
- command: string,
329
- args?: Record<string, unknown>,
330
- timeout = 10000
331
- ): Promise<DebugProtocol.Response> {
332
- if (this._exited) {
333
- throw new DAPClientError(
334
- `Adapter has exited (code=${this._exitCode}). stderr: ${this.stderrOutput.join("")}`
335
- );
336
- }
337
- // Wait for any in-progress session transition (e.g. handleStartDebugging
338
- // setting up a child session) before sending. Otherwise the request would
339
- // race the handshake or land on a half-initialized child connection.
340
- if (this.transitionLock) {
341
- await this.transitionLock;
342
- }
343
- return this.sendRequestOn(this.conns[this.activeIdx], command, args, timeout);
344
- }
345
-
346
- /** Internal: send a request on a specific connection. */
347
- private sendRequestOn(
348
- conn: Conn | undefined,
349
- command: string,
350
- args: Record<string, unknown> | undefined,
351
- timeout: number
352
- ): Promise<DebugProtocol.Response> {
353
- if (!conn || conn.closed || !conn.writeStream.writable) {
354
- throw new DAPClientError("Adapter transport is not writable");
355
- }
356
- this.trackOutgoing(command, args);
357
- const seq = conn.seq++;
358
- const request: DebugProtocol.Request = {
359
- seq,
360
- type: "request",
361
- command,
362
- arguments: args,
363
- };
364
- return new Promise<DebugProtocol.Response>((resolve, reject) => {
365
- const pending: PendingRequest = { resolve, reject };
366
- if (timeout > 0) {
367
- pending.timer = setTimeout(() => {
368
- conn.pending.delete(seq);
369
- reject(new DAPClientError(`Request '${command}' timeout after ${timeout}ms`));
370
- }, timeout);
371
- }
372
- conn.pending.set(seq, pending);
373
- try {
374
- conn.writeStream.write(encodeMessage(request), (err) => {
375
- if (err) {
376
- const p = conn.pending.get(seq);
377
- if (p) {
378
- if (p.timer) clearTimeout(p.timer);
379
- conn.pending.delete(seq);
380
- p.reject(new DAPClientError(`Write to adapter failed: ${err.message}`));
381
- }
382
- }
383
- });
384
- } catch (err) {
385
- conn.pending.delete(seq);
386
- if (pending.timer) clearTimeout(pending.timer);
387
- reject(new DAPClientError(`Write to adapter failed: ${(err as Error).message}`));
388
- }
389
- });
390
- }
391
-
392
- private handleMessage(msg: DebugProtocol.ProtocolMessage, conn: Conn): void {
393
- if (msg.type === "response") {
394
- const response = msg as DebugProtocol.Response;
395
- const pending = conn.pending.get(response.request_seq);
396
- if (pending) {
397
- if (pending.timer) clearTimeout(pending.timer);
398
- conn.pending.delete(response.request_seq);
399
- if (response.success) {
400
- pending.resolve(response);
401
- } else {
402
- pending.reject(
403
- new DAPClientError(
404
- `DAP error on '${response.command}': ${response.message || "unknown error"}`
405
- )
406
- );
407
- }
408
- }
409
- } else if (msg.type === "request") {
410
- const req = msg as DebugProtocol.Request;
411
- if (req.command === "startDebugging" && this.transport === "tcp") {
412
- // The active session is asking us to open a child session for a
413
- // newly-spawned target (vscode-js-debug does this for every node
414
- // process it attaches to). Handle natively rather than rejecting.
415
- this.handleStartDebugging(req, conn).catch((err) => {
416
- this.stderrOutput.push(`startDebugging handler failed: ${(err as Error).message}\n`);
417
- });
418
- return;
419
- }
420
- // Reject other reverse requests — the MCP server has no terminal etc.
421
- this.replyToReverseRequest(conn, req, {
422
- success: false,
423
- message: `Reverse request '${req.command}' is not supported by this client`,
424
- });
425
- } else if (msg.type === "event") {
426
- const event = msg as DebugProtocol.Event;
427
- this.emit(event.event, event);
428
- this.emit("event", event);
429
- }
430
- }
431
-
432
- /** Reply to a reverse request, on the connection that sent it. */
433
- private replyToReverseRequest(
434
- conn: Conn,
435
- req: DebugProtocol.Request,
436
- result: { success: boolean; message?: string; body?: Record<string, unknown> }
437
- ): void {
438
- const response: DebugProtocol.Response = {
439
- seq: conn.seq++,
440
- type: "response",
441
- request_seq: req.seq,
442
- command: req.command,
443
- success: result.success,
444
- message: result.message,
445
- body: result.body,
446
- };
447
- try {
448
- conn.writeStream.write(encodeMessage(response));
449
- } catch {
450
- // ignore — transport may have closed
451
- }
452
- }
453
-
454
- /**
455
- * Handle a `startDebugging` reverse request from an active session.
456
- *
457
- * Per the DAP startDebugging protocol, the client should open a new DAP
458
- * session for the supplied configuration and run the launch/attach
459
- * handshake against the SAME adapter. For TCP-server adapters like
460
- * vscode-js-debug, we open a fresh TCP connection to the same port, run
461
- * initialize + launch (or attach) using the request's configuration, and
462
- * make that new connection the active one for subsequent user requests.
463
- *
464
- * The `transitionLock` prevents user `sendRequest` calls from racing the
465
- * handshake.
466
- */
467
- private async handleStartDebugging(req: DebugProtocol.Request, fromConn: Conn): Promise<void> {
468
- let resolveLock: (() => void) | undefined;
469
- this.transitionLock = new Promise<void>((r) => { resolveLock = r; });
470
- try {
471
- const argsObj = (req.arguments ?? {}) as {
472
- configuration?: Record<string, unknown>;
473
- request?: "launch" | "attach";
474
- };
475
- const childConfig = argsObj.configuration ?? {};
476
- const childRequest = argsObj.request ?? "launch";
477
-
478
- const childConn = await this.openTcpConnection("child");
479
- this.conns.push(childConn);
480
- const newIdx = this.conns.length - 1;
481
- // Make it active so the silent handshake calls below go to the child.
482
- this.activeIdx = newIdx;
483
-
484
- // Run initialize on the child. The adapterID is taken from the supplied
485
- // configuration when present (vscode-js-debug uses "pwa-node").
486
- await this.sendRequestOn(
487
- childConn,
488
- "initialize",
489
- {
490
- adapterID: (childConfig.type as string | undefined) ?? "child",
491
- clientID: "dap-mcp-server",
492
- clientName: "DAP MCP Server",
493
- linesStartAt1: true,
494
- columnsStartAt1: true,
495
- pathFormat: "path",
496
- },
497
- 10000
498
- );
499
-
500
- // Run launch/attach with the provided configuration. The configuration
501
- // typically carries adapter-specific fields like __pendingTargetId that
502
- // bind the new session to a specific target. Fire-and-forget — the
503
- // adapter blocks the response until configurationDone, which we issue
504
- // ourselves below after replaying breakpoints.
505
- const childLaunchPromise = this.sendRequestOn(
506
- childConn,
507
- childRequest,
508
- childConfig,
509
- 30000
510
- );
511
- childLaunchPromise.catch(() => { /* nothing awaits this directly */ });
512
-
513
- // Replay configuration state established on the previous active
514
- // connection so the new child session honors the same breakpoints.
515
- // Without this, vscode-js-debug-style adapters (where the parent
516
- // accepts setBreakpoints but the child is what actually runs the
517
- // debuggee) would never pause at user-set breakpoints.
518
- for (const args of this.bpHistory.values()) {
519
- try {
520
- await this.sendRequestOn(childConn, "setBreakpoints", args, 10000);
521
- } catch {
522
- // Best-effort replay — keep going.
523
- }
524
- }
525
- if (this.excBpHistory) {
526
- try {
527
- await this.sendRequestOn(
528
- childConn,
529
- "setExceptionBreakpoints",
530
- this.excBpHistory,
531
- 10000
532
- );
533
- } catch {
534
- // ignore
535
- }
536
- }
537
- if (this.configurationDoneSent) {
538
- try {
539
- await this.sendRequestOn(childConn, "configurationDone", undefined, 10000);
540
- } catch {
541
- // ignore
542
- }
543
- }
544
-
545
- // Acknowledge the reverse request so the parent can proceed.
546
- this.replyToReverseRequest(fromConn, req, { success: true });
547
- } catch (err) {
548
- this.replyToReverseRequest(fromConn, req, {
549
- success: false,
550
- message: `Failed to start child debug session: ${(err as Error).message}`,
551
- });
552
- } finally {
553
- resolveLock?.();
554
- this.transitionLock = undefined;
555
- }
556
- }
557
-
558
- get exited(): boolean {
559
- return this._exited;
560
- }
561
-
562
- get stderr(): string {
563
- return this.stderrOutput.join("");
564
- }
565
-
566
- async dispose(): Promise<void> {
567
- for (const conn of this.conns) {
568
- if (conn.socket && !conn.socket.destroyed) {
569
- try { conn.socket.destroy(); } catch { /* ignore */ }
570
- }
571
- }
572
- if (this.process && !this._exited) {
573
- this.process.stdin?.end();
574
- this.process.kill("SIGTERM");
575
- await new Promise<void>((resolve) => {
576
- const timer = setTimeout(() => {
577
- this.process?.kill("SIGKILL");
578
- resolve();
579
- }, 3000);
580
- this.process!.on("exit", () => {
581
- clearTimeout(timer);
582
- resolve();
583
- });
584
- });
585
- }
586
- this.removeAllListeners();
587
- }
588
- }
@@ -1,82 +0,0 @@
1
- /**
2
- * Copyright (c) 2026 Ivan Iraci <ivan.iraci@professioneit.com>
3
- *
4
- * Permission is hereby granted, free of charge, to any person obtaining a copy
5
- * of this software and associated documentation files (the "Software"), to deal
6
- * in the Software without restriction, including without limitation the rights
7
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
- * copies of the Software, and to permit persons to whom the Software is
9
- * furnished to do so, subject to the following conditions:
10
- *
11
- * The above copyright notice and this permission notice shall be included in
12
- * all copies or substantial portions of the Software.
13
- *
14
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
- * SOFTWARE.
21
- */
22
-
23
- import { describe, it, expect } from "vitest";
24
- import { encodeMessage, MessageParser } from "./protocol.js";
25
- import { DebugProtocol } from "@vscode/debugprotocol";
26
-
27
- describe("encodeMessage", () => {
28
- it("encodes a DAP message with Content-Length header", () => {
29
- const msg: DebugProtocol.Request = {
30
- seq: 1,
31
- type: "request",
32
- command: "initialize",
33
- arguments: { adapterID: "test" },
34
- };
35
- const encoded = encodeMessage(msg);
36
- const str = encoded.toString("utf-8");
37
- const json = JSON.stringify(msg);
38
- expect(str).toBe(`Content-Length: ${Buffer.byteLength(json)}\r\n\r\n${json}`);
39
- });
40
- });
41
-
42
- describe("MessageParser", () => {
43
- it("parses a single complete message", () => {
44
- const messages: DebugProtocol.ProtocolMessage[] = [];
45
- const parser = new MessageParser((msg) => messages.push(msg));
46
-
47
- const body = JSON.stringify({ seq: 1, type: "response", command: "initialize", success: true, request_seq: 1 });
48
- const raw = `Content-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`;
49
- parser.feed(Buffer.from(raw));
50
-
51
- expect(messages).toHaveLength(1);
52
- expect(messages[0].seq).toBe(1);
53
- });
54
-
55
- it("handles messages split across multiple chunks", () => {
56
- const messages: DebugProtocol.ProtocolMessage[] = [];
57
- const parser = new MessageParser((msg) => messages.push(msg));
58
-
59
- const body = JSON.stringify({ seq: 1, type: "event", event: "initialized" });
60
- const raw = `Content-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`;
61
- const mid = Math.floor(raw.length / 2);
62
-
63
- parser.feed(Buffer.from(raw.slice(0, mid)));
64
- expect(messages).toHaveLength(0);
65
- parser.feed(Buffer.from(raw.slice(mid)));
66
- expect(messages).toHaveLength(1);
67
- });
68
-
69
- it("handles multiple messages in one chunk", () => {
70
- const messages: DebugProtocol.ProtocolMessage[] = [];
71
- const parser = new MessageParser((msg) => messages.push(msg));
72
-
73
- const body1 = JSON.stringify({ seq: 1, type: "event", event: "initialized" });
74
- const body2 = JSON.stringify({ seq: 2, type: "event", event: "stopped" });
75
- const raw = `Content-Length: ${Buffer.byteLength(body1)}\r\n\r\n${body1}Content-Length: ${Buffer.byteLength(body2)}\r\n\r\n${body2}`;
76
- parser.feed(Buffer.from(raw));
77
-
78
- expect(messages).toHaveLength(2);
79
- expect(messages[0].seq).toBe(1);
80
- expect(messages[1].seq).toBe(2);
81
- });
82
- });