@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 ADDED
@@ -0,0 +1,2 @@
1
+ - Add this as an installable global binary.
2
+ - .help, .clearhistory, etc.
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
@@ -0,0 +1,6 @@
1
+ import { loadHistory } from './lib/history';
2
+ import { setupWebSocket } from './lib/ws';
3
+
4
+
5
+ await loadHistory();
6
+ await setupWebSocket();
package/src/lib/cli.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { cli } from 'cleye';
2
+
3
+
4
+ const argv = cli({
5
+ name: 'ub-debugger',
6
+ flags: {
7
+ port: {
8
+ type: Number,
9
+ description: 'The port number to host the debugger on.',
10
+ default: 9090
11
+ }
12
+ }
13
+ });
14
+
15
+ export default argv;
@@ -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
+ }
@@ -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
+ }