@unbound-app/debugger 0.7.2 → 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.
- package/dist/index.mjs +86 -45
- 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
|
-
|
|
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
|
-
|
|
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,
|
|
46
|
-
*
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
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 ${
|
|
199
|
+
error: `Evaluation timed out after ${timeout}ms.`
|
|
190
200
|
});
|
|
191
|
-
},
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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](
|
|
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(
|
|
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(
|
|
575
|
-
else terminal.red(
|
|
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",
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
28
|
-
"cleye": "^
|
|
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": {
|