@unbound-app/debugger 1.0.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/TODO.md +2 -0
- package/package.json +21 -0
- package/src/index.ts +6 -0
- package/src/lib/cli.ts +15 -0
- package/src/lib/history.ts +29 -0
- package/src/lib/repl.ts +47 -0
- package/src/lib/ws.ts +97 -0
- package/tsconfig.json +29 -0
package/TODO.md
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@unbound-app/debugger",
|
|
3
|
+
"description": "A REPL debugger for @unbound-app instances.",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"module": "src/index.ts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"ub-debugger": "src/index.ts"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@types/bun": "latest",
|
|
12
|
+
"@types/terminal-kit": "^2.5.7"
|
|
13
|
+
},
|
|
14
|
+
"peerDependencies": {
|
|
15
|
+
"typescript": "^5"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"cleye": "^1.3.4",
|
|
19
|
+
"terminal-kit": "^3.1.2"
|
|
20
|
+
}
|
|
21
|
+
}
|
package/src/index.ts
ADDED
package/src/lib/cli.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { terminal } from 'terminal-kit';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
type History = string[];
|
|
6
|
+
|
|
7
|
+
const HISTORY_PATH = join(__dirname, '..', '..', '.history.json');
|
|
8
|
+
const HISTORY_FILE = Bun.file(HISTORY_PATH);
|
|
9
|
+
|
|
10
|
+
export let history: History = [];
|
|
11
|
+
|
|
12
|
+
export async function loadHistory() {
|
|
13
|
+
try {
|
|
14
|
+
if (await HISTORY_FILE.exists()) {
|
|
15
|
+
history = await HISTORY_FILE.json();
|
|
16
|
+
}
|
|
17
|
+
} catch (error) {
|
|
18
|
+
terminal.red.bold(`Failed to load history.\n`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function addHistoryItem(item: string) {
|
|
23
|
+
history.push(item);
|
|
24
|
+
persistHistory();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function persistHistory() {
|
|
28
|
+
HISTORY_FILE.write(JSON.stringify(history, null, 2));
|
|
29
|
+
}
|
package/src/lib/repl.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { terminal } from 'terminal-kit';
|
|
2
|
+
|
|
3
|
+
import { addHistoryItem, history } from './history';
|
|
4
|
+
import { send } from './ws';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export const state: {
|
|
8
|
+
repl: ReturnType<typeof terminal.inputField> | null;
|
|
9
|
+
} = {
|
|
10
|
+
repl: null
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function setupReplListeners() {
|
|
14
|
+
terminal.on('key', (name: string) => {
|
|
15
|
+
if (name !== 'CTRL_C') return;
|
|
16
|
+
process.exit(0);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function takeReplInput() {
|
|
21
|
+
abortCurrentReplInput();
|
|
22
|
+
|
|
23
|
+
terminal.bold.cyan('» ');
|
|
24
|
+
|
|
25
|
+
state.repl = terminal.inputField({
|
|
26
|
+
minLength: 1,
|
|
27
|
+
history
|
|
28
|
+
}, (error, input) => {
|
|
29
|
+
if (error) return console.error(error);
|
|
30
|
+
if (!input) return;
|
|
31
|
+
|
|
32
|
+
abortCurrentReplInput(true);
|
|
33
|
+
terminal.white.bold(`» ${input}\n`);
|
|
34
|
+
addHistoryItem(input);
|
|
35
|
+
send(input);
|
|
36
|
+
|
|
37
|
+
takeReplInput();
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function abortCurrentReplInput(shouldDeletePrompt: boolean = false) {
|
|
42
|
+
if (state.repl) {
|
|
43
|
+
state.repl.abort();
|
|
44
|
+
state.repl = null;
|
|
45
|
+
if (shouldDeletePrompt) terminal.deleteLine(-1);
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/lib/ws.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { terminal } from 'terminal-kit';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
|
|
4
|
+
import { abortCurrentReplInput, setupReplListeners, takeReplInput } from './repl';
|
|
5
|
+
import argv from './cli';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
interface SocketMessage {
|
|
9
|
+
level: 1 | 2 | 3;
|
|
10
|
+
message: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const state: {
|
|
14
|
+
isInitialized: boolean,
|
|
15
|
+
socket: Bun.ServerWebSocket | null;
|
|
16
|
+
} = {
|
|
17
|
+
isInitialized: false,
|
|
18
|
+
socket: null
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const MESSAGE_LEVELS = {
|
|
22
|
+
1: terminal.gray,
|
|
23
|
+
2: terminal.yellow,
|
|
24
|
+
3: terminal.red
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function setupWebSocket() {
|
|
28
|
+
if (state.isInitialized) return;
|
|
29
|
+
|
|
30
|
+
Bun.serve({
|
|
31
|
+
port: argv.flags.port,
|
|
32
|
+
fetch(request, server) {
|
|
33
|
+
if (state.socket) {
|
|
34
|
+
return new Response(
|
|
35
|
+
JSON.stringify({ error: 'Debugger is busy.' }),
|
|
36
|
+
{ status: 503 }
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
server.upgrade(request);
|
|
41
|
+
},
|
|
42
|
+
websocket: {
|
|
43
|
+
open(ws) {
|
|
44
|
+
state.socket = ws;
|
|
45
|
+
terminal.bold.green('Client connected.\n');
|
|
46
|
+
|
|
47
|
+
setupReplListeners();
|
|
48
|
+
takeReplInput();
|
|
49
|
+
},
|
|
50
|
+
message(_, message: string) {
|
|
51
|
+
try {
|
|
52
|
+
const payload: SocketMessage = JSON.parse(message);
|
|
53
|
+
abortCurrentReplInput(true);
|
|
54
|
+
|
|
55
|
+
MESSAGE_LEVELS[payload.level](`« ${payload.message}\n`);
|
|
56
|
+
|
|
57
|
+
takeReplInput();
|
|
58
|
+
} catch (error) {
|
|
59
|
+
terminal.red(`Parsing failed on message from client.\n`);
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
close() {
|
|
63
|
+
state.socket = null;
|
|
64
|
+
abortCurrentReplInput(true);
|
|
65
|
+
terminal.red('Client disconnected.\n');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const localAddress = getLocalAddress();
|
|
71
|
+
|
|
72
|
+
terminal.gray(`Unbound's Debugger is now active. Connect using ${localAddress}:${argv.flags.port}\n`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function getLocalAddress() {
|
|
76
|
+
const interfaces = os.networkInterfaces();
|
|
77
|
+
|
|
78
|
+
for (const ifaces of Object.values(interfaces)) {
|
|
79
|
+
if (!ifaces) continue;
|
|
80
|
+
|
|
81
|
+
for (const iface of ifaces) {
|
|
82
|
+
// Skip over internal (i.e., 127.0.0.1) and non-IPv4 addresses
|
|
83
|
+
if (iface.family === 'IPv4' && !iface.internal) {
|
|
84
|
+
return iface.address;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
export function send(...payload: Parameters<Bun.ServerWebSocket['send']>) {
|
|
92
|
+
if (!state.socket) {
|
|
93
|
+
return terminal.bold.red('You cannot send messages without a connected client.\n');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
state.socket.send(...payload);
|
|
97
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
// Environment setup & latest features
|
|
4
|
+
"lib": ["ESNext"],
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "Preserve",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
|
|
11
|
+
// Bundler mode
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
// Best practices
|
|
18
|
+
"strict": true,
|
|
19
|
+
"skipLibCheck": true,
|
|
20
|
+
"noFallthroughCasesInSwitch": true,
|
|
21
|
+
"noUncheckedIndexedAccess": true,
|
|
22
|
+
"noImplicitOverride": true,
|
|
23
|
+
|
|
24
|
+
// Some stricter flags (disabled by default)
|
|
25
|
+
"noUnusedLocals": false,
|
|
26
|
+
"noUnusedParameters": false,
|
|
27
|
+
"noPropertyAccessFromIndexSignature": false
|
|
28
|
+
}
|
|
29
|
+
}
|