@unbound-app/debugger 0.8.0 → 0.9.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.
Files changed (2) hide show
  1. package/dist/index.mjs +86 -45
  2. package/package.json +3 -3
package/dist/index.mjs CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env bun
2
2
  import { stripEscapeSequences, terminal } from "terminal-kit";
3
3
  import { join } from "node:path";
4
- import { parseMessage } from "@unbound-app/debugger-protocol";
4
+ import { DEFAULT_EVAL_TIMEOUT_MS, MAX_EVAL_TIMEOUT_MS, parseMessage } from "@unbound-app/debugger-protocol";
5
5
  import os from "node:os";
6
6
  import { cli } from "cleye";
7
- //#region src/lib/history.ts
7
+ // src/lib/history.ts
8
8
  const HISTORY_PATH = join(__dirname, "..", "..", ".history.json");
9
9
  const HISTORY_FILE = Bun.file(HISTORY_PATH);
10
10
  let history = [];
@@ -25,10 +25,10 @@ function addHistoryItem(item) {
25
25
  function persistHistory() {
26
26
  HISTORY_FILE.write(JSON.stringify(history, null, 2));
27
27
  }
28
- //#endregion
29
- //#region src/lib/session.ts
28
+ // src/lib/session.ts
30
29
  const state$2 = {
31
30
  device: null,
31
+ deviceInfo: null,
32
32
  controllers: /* @__PURE__ */ new Set()
33
33
  };
34
34
  const pending = /* @__PURE__ */ new Map();
@@ -38,20 +38,20 @@ const deviceListeners = /* @__PURE__ */ new Set();
38
38
  const evalListeners = /* @__PURE__ */ new Set();
39
39
  const resultListeners = /* @__PURE__ */ new Set();
40
40
  const controllerListeners = /* @__PURE__ */ new Set();
41
- /** How long to wait for a device to answer an eval before failing it. */
42
- const EVAL_TIMEOUT_MS = 1e4;
43
41
  const PLUGIN_PUSH_TIMEOUT_MS = 6e4;
44
42
  /**
45
- * @description Registers the connected device, rejecting a second one so eval routing stays
46
- * unambiguous.
43
+ * @description Registers the connected device, replacing any earlier one so a relaunched or
44
+ * reconnecting app always takes over from a socket that died without closing.
47
45
  * @param socket The device's socket.
48
- * @returns Whether the device was accepted (false if one is already connected).
49
46
  */
50
47
  function attachDevice(socket) {
51
- if (state$2.device) return false;
48
+ const previous = state$2.device;
49
+ if (previous) {
50
+ detachDevice(previous);
51
+ previous.close();
52
+ }
52
53
  state$2.device = socket;
53
54
  broadcastDeviceStatus(true);
54
- return true;
55
55
  }
56
56
  /**
57
57
  * @description Clears the device on disconnect, notifies controllers, and fails every in-flight eval
@@ -61,6 +61,7 @@ function attachDevice(socket) {
61
61
  function detachDevice(socket) {
62
62
  if (state$2.device !== socket) return;
63
63
  state$2.device = null;
64
+ state$2.deviceInfo = null;
64
65
  for (const [id, entry] of pending) {
65
66
  clearTimeout(entry.timer);
66
67
  entry.deliver({
@@ -113,18 +114,27 @@ function detachController(socket) {
113
114
  * @description Runs code on the device on behalf of a controller socket, delivering the id-correlated
114
115
  * result back over that same socket. Sends an error frame instead if no device is connected.
115
116
  * @param socket The controller that issued the request.
116
- * @param id The controller-chosen correlation id, echoed on the result.
117
- * @param code The JavaScript to evaluate on the device.
117
+ * @param request The controller's eval, whose `id` correlates the result and whose `timeout` bounds the wait.
118
118
  */
119
- function evaluateForController(socket, id, code) {
119
+ function evaluateForController(socket, request) {
120
+ const { id, code } = request;
120
121
  const deliver = getControllerDelivery(socket);
121
122
  for (const listener of evalListeners) listener(code);
122
- dispatch(id, code, (result) => {
123
+ dispatch(id, code, resolveEvalTimeout(request.timeout), (result) => {
123
124
  for (const listener of resultListeners) listener(result);
124
125
  deliver(result);
125
126
  });
126
127
  }
127
128
  /**
129
+ * @description Picks how long an eval may run: the requested timeout when it is a positive number of
130
+ * milliseconds, capped at {@link MAX_EVAL_TIMEOUT_MS}, otherwise {@link DEFAULT_EVAL_TIMEOUT_MS}.
131
+ * @param requested The timeout the controller asked for, if any.
132
+ * @returns The timeout to apply, in milliseconds.
133
+ */
134
+ function resolveEvalTimeout(requested) {
135
+ return typeof requested === "number" && requested > 0 ? Math.min(requested, MAX_EVAL_TIMEOUT_MS) : DEFAULT_EVAL_TIMEOUT_MS;
136
+ }
137
+ /**
128
138
  * @description Forwards a controller's {@link PluginPushRequest} to the device and delivers the
129
139
  * id-correlated {@link PluginPushResult} back over that same controller's socket. Replies with an
130
140
  * error frame instead if no device is connected or it doesn't answer within {@link PLUGIN_PUSH_TIMEOUT_MS}.
@@ -161,16 +171,16 @@ function pushPluginForController(socket, request) {
161
171
  /**
162
172
  * @description Runs code on the device on behalf of an in-process caller (the terminal REPL),
163
173
  * resolving with the device's answer. Rejects if no device is connected or it doesn't answer within
164
- * {@link EVAL_TIMEOUT_MS}.
174
+ * {@link DEFAULT_EVAL_TIMEOUT_MS}.
165
175
  * @param code The JavaScript to evaluate on the device.
166
176
  * @returns The device's {@link EvalResult}.
167
177
  */
168
178
  function evaluate(code) {
169
179
  return new Promise((resolve) => {
170
- dispatch(crypto.randomUUID(), code, resolve);
180
+ dispatch(crypto.randomUUID(), code, DEFAULT_EVAL_TIMEOUT_MS, resolve);
171
181
  });
172
182
  }
173
- function dispatch(id, code, deliver) {
183
+ function dispatch(id, code, timeout, deliver) {
174
184
  if (!state$2.device) {
175
185
  deliver({
176
186
  type: "eval-result",
@@ -186,9 +196,9 @@ function dispatch(id, code, deliver) {
186
196
  type: "eval-result",
187
197
  id,
188
198
  ok: false,
189
- error: `Evaluation timed out after ${EVAL_TIMEOUT_MS}ms.`
199
+ error: `Evaluation timed out after ${timeout}ms.`
190
200
  });
191
- }, EVAL_TIMEOUT_MS);
201
+ }, timeout);
192
202
  pending.set(id, {
193
203
  deliver,
194
204
  timer
@@ -221,6 +231,11 @@ function handleDeviceMessage$1(message) {
221
231
  entry.deliver(message);
222
232
  return;
223
233
  }
234
+ if (message.type === "hello") {
235
+ state$2.deviceInfo = message.device;
236
+ broadcastDeviceStatus(true);
237
+ return;
238
+ }
224
239
  if (message.type !== "log") return;
225
240
  for (const socket of state$2.controllers) socket.send(JSON.stringify(message));
226
241
  for (const listener of logListeners) listener(message);
@@ -235,6 +250,15 @@ function onLog(listener) {
235
250
  return () => void logListeners.delete(listener);
236
251
  }
237
252
  /**
253
+ * @description Subscribes to device connect/disconnect transitions.
254
+ * @param listener Called with whether a device is now connected.
255
+ * @returns An unsubscribe function.
256
+ */
257
+ function onDevice(listener) {
258
+ deviceListeners.add(listener);
259
+ return () => void deviceListeners.delete(listener);
260
+ }
261
+ /**
238
262
  * @description Subscribes to code a controller (e.g. the MCP) sends to evaluate.
239
263
  * @param listener Called with each controller's code.
240
264
  * @returns An unsubscribe function.
@@ -270,17 +294,18 @@ function getControllerDelivery(socket) {
270
294
  return deliver;
271
295
  }
272
296
  function sendDeviceStatus(socket, connected) {
297
+ const device = connected ? state$2.deviceInfo ?? void 0 : void 0;
273
298
  socket.send(JSON.stringify({
274
299
  type: "device-status",
275
- connected
300
+ connected,
301
+ device
276
302
  }));
277
303
  }
278
304
  function broadcastDeviceStatus(connected) {
279
305
  for (const socket of state$2.controllers) sendDeviceStatus(socket, connected);
280
- for (const listener of deviceListeners) listener(connected);
306
+ for (const listener of deviceListeners) listener(connected, state$2.deviceInfo);
281
307
  }
282
- //#endregion
283
- //#region src/lib/repl.ts
308
+ // src/lib/repl.ts
284
309
  const state$1 = {
285
310
  repl: null,
286
311
  currentInput: ""
@@ -322,8 +347,7 @@ function abortCurrentReplInput({ deletePrompt = false, deleteCurrentInput = fals
322
347
  if (deletePrompt) terminal.deleteLine(-1);
323
348
  if (deleteCurrentInput) state$1.currentInput = "";
324
349
  }
325
- //#endregion
326
- //#region src/lib/format-log.ts
350
+ // src/lib/format-log.ts
327
351
  /**
328
352
  * @description Cleans a Hermes-joined console line into space-separated, unquoted output.
329
353
  * @param message The raw `nativeLoggingHook` message string.
@@ -344,7 +368,7 @@ function splitTopLevel(input) {
344
368
  let inString = false;
345
369
  let start = 0;
346
370
  for (let i = 0, len = input.length; i < len; i++) {
347
- const char = input[i];
371
+ const char = input.charAt(i);
348
372
  if (inString) {
349
373
  if (char === "\\") {
350
374
  i++;
@@ -384,8 +408,7 @@ function unquoteSegment(segment) {
384
408
  if (trimmed.length < 2 || trimmed[0] !== "'" || trimmed[trimmed.length - 1] !== "'") return trimmed;
385
409
  return trimmed.slice(1, -1).replace(/\\'/g, "'");
386
410
  }
387
- //#endregion
388
- //#region src/lib/banner.ts
411
+ // src/lib/banner.ts
389
412
  /**
390
413
  * Renders the debugger's startup banner: a pink rounded box sized to the terminal, capped on wide
391
414
  * ones, falling back to plain starred lines when the terminal is too narrow for a box.
@@ -472,8 +495,7 @@ function rgb(hex, text) {
472
495
  function boldRgb(hex, text) {
473
496
  terminal.bold.colorRgbHex(hex, text);
474
497
  }
475
- //#endregion
476
- //#region src/lib/cli.ts
498
+ // src/lib/cli.ts
477
499
  const argv = cli({
478
500
  name: "ub-debugger",
479
501
  flags: { port: {
@@ -482,8 +504,7 @@ const argv = cli({
482
504
  default: 9090
483
505
  } }
484
506
  });
485
- //#endregion
486
- //#region src/lib/ws.ts
507
+ // src/lib/ws.ts
487
508
  const state = { isInitialized: false };
488
509
  const MESSAGE_LEVELS = {
489
510
  1: terminal.gray,
@@ -505,7 +526,24 @@ const SYSTEM_COLORS = {
505
526
  * @param text The line to print (the glyph is prepended and a trailing newline added).
506
527
  */
507
528
  function system(color, text) {
508
- terminal.bold.colorRgbHex(SYSTEM_COLORS[color], `${SYSTEM_GLYPH} ${text}\n`);
529
+ terminal.bold.colorRgbHex(SYSTEM_COLORS[color], `${timestamp()} ${SYSTEM_GLYPH} ${text}\n`);
530
+ }
531
+ /**
532
+ * @description Formats the current local time for the start of a bridge log line.
533
+ * @param now The moment to format.
534
+ * @returns The time as `HH:MM:SS.mmm`.
535
+ */
536
+ function timestamp(now = /* @__PURE__ */ new Date()) {
537
+ const pad = (value, width = 2) => String(value).padStart(width, "0");
538
+ return `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}.${pad(now.getMilliseconds(), 3)}`;
539
+ }
540
+ /**
541
+ * @description Describes a device from its hello for the bridge log.
542
+ * @param device What the device reported.
543
+ * @returns A one-line summary such as `ios, Discord 351.0, loader 1.0.0, client 0.9.0`.
544
+ */
545
+ function describeDevice(device) {
546
+ return `${device.platform}, Discord ${device.discordVersion}, loader ${device.loaderVersion}, client ${device.clientVersion}`;
509
547
  }
510
548
  /**
511
549
  * @description Boots the bridge: the phone-facing WebSocket server, the shared session routing, and
@@ -522,7 +560,6 @@ function setupWebSocket() {
522
560
  server.upgrade(request, { data: { role: "controller" } });
523
561
  return;
524
562
  }
525
- if (state$2.device) return new Response(JSON.stringify({ error: "A device is already connected." }), { status: 503 });
526
563
  server.upgrade(request, { data: { role: "device" } });
527
564
  },
528
565
  websocket: {
@@ -543,7 +580,7 @@ function setupWebSocket() {
543
580
  handleControllerMessage(ws, message);
544
581
  return;
545
582
  }
546
- handleDeviceMessage(message);
583
+ if (ws === state$2.device) handleDeviceMessage(message);
547
584
  },
548
585
  close(ws) {
549
586
  if (ws.data.role === "controller") {
@@ -561,18 +598,24 @@ function setupWebSocket() {
561
598
  });
562
599
  onLog((log) => {
563
600
  abortCurrentReplInput({ deletePrompt: true });
564
- MESSAGE_LEVELS[log.level](`« ${formatLog(stripEscapeSequences(log.message))}\n`);
601
+ MESSAGE_LEVELS[log.level](`${timestamp()} « ${formatLog(stripEscapeSequences(log.message))}\n`);
565
602
  takeReplInput();
566
603
  });
567
604
  onControllerEval((code) => {
568
605
  abortCurrentReplInput({ deletePrompt: true });
569
- terminal.magenta(`» [mcp] ${code}\n`);
606
+ terminal.magenta(`${timestamp()} » [mcp] ${code}\n`);
570
607
  takeReplInput();
571
608
  });
572
609
  onControllerResult((result) => {
573
610
  abortCurrentReplInput({ deletePrompt: true });
574
- if (result.ok) terminal.gray(`« ${result.value}\n`);
575
- else terminal.red(`« ${result.error}\n`);
611
+ if (result.ok) terminal.gray(`${timestamp()} « ${result.value}\n`);
612
+ else terminal.red(`${timestamp()} « ${result.error}\n`);
613
+ takeReplInput();
614
+ });
615
+ onDevice((connected, device) => {
616
+ if (!connected || !device) return;
617
+ abortCurrentReplInput({ deletePrompt: true });
618
+ system("green", `Device identified: ${describeDevice(device)}`);
576
619
  takeReplInput();
577
620
  });
578
621
  onController((connected, count) => {
@@ -586,7 +629,7 @@ function setupWebSocket() {
586
629
  function handleDeviceMessage(raw) {
587
630
  const message = parseMessage(raw);
588
631
  if (!message) {
589
- system("red", "Parsing failed on message from device");
632
+ system("red", `Parsing failed on message from device: ${JSON.stringify(raw.slice(0, 200))}`);
590
633
  return;
591
634
  }
592
635
  handleDeviceMessage$1(message);
@@ -594,7 +637,7 @@ function handleDeviceMessage(raw) {
594
637
  function handleControllerMessage(ws, raw) {
595
638
  const message = parseMessage(raw);
596
639
  if (message?.type === "eval") {
597
- evaluateForController(ws, message.id, message.code);
640
+ evaluateForController(ws, message);
598
641
  return;
599
642
  }
600
643
  if (message?.type === "plugin-push") pushPluginForController(ws, message);
@@ -610,9 +653,7 @@ function getLocalAddress() {
610
653
  for (const iface of ifaces) if (iface.family === "IPv4" && !iface.internal) return iface.address;
611
654
  }
612
655
  }
613
- //#endregion
614
- //#region src/index.ts
656
+ // src/index.ts
615
657
  await loadHistory();
616
658
  setupWebSocket();
617
- //#endregion
618
659
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unbound-app/debugger",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "A REPL debugger for @unbound-app/client runtime instances.",
5
5
  "author": {
6
6
  "name": "Mario P.",
@@ -24,8 +24,8 @@
24
24
  "build": "tsdown"
25
25
  },
26
26
  "dependencies": {
27
- "@unbound-app/debugger-protocol": "0.8.0",
28
- "cleye": "^1.3.4",
27
+ "@unbound-app/debugger-protocol": "0.9.0",
28
+ "cleye": "^2.7.0",
29
29
  "terminal-kit": "^3.1.2"
30
30
  },
31
31
  "peerDependencies": {