@radishbot/sdk 0.1.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/README.md +192 -0
- package/package.json +24 -0
- package/src/cli.ts +383 -0
- package/src/connection.ts +124 -0
- package/src/index.ts +383 -0
- package/src/module_bindings/action_table.ts +20 -0
- package/src/module_bindings/add_log_reducer.ts +19 -0
- package/src/module_bindings/add_logs_batch_reducer.ts +17 -0
- package/src/module_bindings/api_key_table.ts +17 -0
- package/src/module_bindings/check_timeouts_reducer.ts +15 -0
- package/src/module_bindings/create_root_flow_reducer.ts +17 -0
- package/src/module_bindings/create_sub_flow_reducer.ts +19 -0
- package/src/module_bindings/finish_action_reducer.ts +17 -0
- package/src/module_bindings/finish_flow_reducer.ts +17 -0
- package/src/module_bindings/flow_table.ts +24 -0
- package/src/module_bindings/index.ts +175 -0
- package/src/module_bindings/log_entry_table.ts +20 -0
- package/src/module_bindings/register_key_reducer.ts +16 -0
- package/src/module_bindings/start_action_reducer.ts +17 -0
- package/src/module_bindings/types/procedures.ts +10 -0
- package/src/module_bindings/types/reducers.ts +28 -0
- package/src/module_bindings/types.ts +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# Radish Logger
|
|
2
|
+
|
|
3
|
+
Flow-based logging SDK. Track what your app is doing with nested actions, timing, and structured logs — then inspect everything in the dashboard.
|
|
4
|
+
|
|
5
|
+
No account needed. Generate a key, start logging.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @radishbot/sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { RL, generateKey } from '@radishbot/sdk';
|
|
17
|
+
|
|
18
|
+
const key = generateKey(); // save this — it's your dashboard login
|
|
19
|
+
|
|
20
|
+
const root = await RL(key);
|
|
21
|
+
|
|
22
|
+
await root.a('handle-request', async (req) => {
|
|
23
|
+
console.log('GET /api/users'); // automatically captured
|
|
24
|
+
|
|
25
|
+
const users = await req.a('db-query', async () => {
|
|
26
|
+
return await db.query('SELECT * FROM users');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
console.log('Done', { count: users.length });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
await root.finish();
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Open the dashboard, paste your key, see everything.
|
|
36
|
+
|
|
37
|
+
## Console Capture
|
|
38
|
+
|
|
39
|
+
Inside `.a()` callbacks, `console.log/warn/error/debug` are automatically captured as action logs. Output still prints to the terminal — but it also gets sent to the dashboard with full context.
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
await root.a('migrate', async () => {
|
|
43
|
+
console.log('Starting migration'); // → info log
|
|
44
|
+
console.warn('Deprecated column found'); // → warn log
|
|
45
|
+
console.error('Failed to migrate users'); // → error log
|
|
46
|
+
console.debug('SQL: ALTER TABLE ...'); // → debug log
|
|
47
|
+
console.log('Done', { tables: 5 }); // → info log with data
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Objects are properly serialized — no more `[object Object]`.
|
|
52
|
+
|
|
53
|
+
## Actions
|
|
54
|
+
|
|
55
|
+
Actions are nested scopes. Every `RL()` call creates a root action at `/`. Sub-actions branch off from there.
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
const root = await RL(key);
|
|
59
|
+
|
|
60
|
+
await root.a('request', async (req) => {
|
|
61
|
+
console.log('handling request');
|
|
62
|
+
|
|
63
|
+
await req.a('database', async () => {
|
|
64
|
+
console.log('querying users');
|
|
65
|
+
}); // auto-finished
|
|
66
|
+
|
|
67
|
+
await req.a('response', async () => {
|
|
68
|
+
console.log('sending 200');
|
|
69
|
+
}); // auto-finished
|
|
70
|
+
}); // auto-finished
|
|
71
|
+
|
|
72
|
+
await root.finish();
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
If your function throws, the action is marked as errored and the exception propagates:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
await root.a('risky-op', async () => {
|
|
79
|
+
throw new Error('something broke');
|
|
80
|
+
}); // action → error, rethrows
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Return values pass through:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
const users = await flow.a('db-query', async () => {
|
|
87
|
+
return await db.query('SELECT * FROM users');
|
|
88
|
+
});
|
|
89
|
+
// users is the query result
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Duplicate names are allowed — the dashboard shows them as `request:1`, `request:2`, etc:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
await root.a('batch', async (batch) => {
|
|
96
|
+
for (const item of items) {
|
|
97
|
+
await batch.a('request', async () => {
|
|
98
|
+
await processItem(item);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
For long-lived actions (websockets, streams), use the manual API:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
const stream = root.action('websocket');
|
|
108
|
+
stream.info('connected');
|
|
109
|
+
// ... hours later ...
|
|
110
|
+
stream.info('disconnected');
|
|
111
|
+
await stream.finish();
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Explicit Logging
|
|
115
|
+
|
|
116
|
+
Outside of `.a()` callbacks, or when you want to log to a specific action:
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
action.info('request received', { method: 'POST', path: '/api/users' });
|
|
120
|
+
action.warn('rate limit approaching', { remaining: 3 });
|
|
121
|
+
action.error('query failed', new Error('connection refused'));
|
|
122
|
+
action.debug('cache hit', { key: 'user:123', ttl: 300 });
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Cross-Context Actions
|
|
126
|
+
|
|
127
|
+
Export an action's handle to continue logging from another process, worker, or service:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
// Service A
|
|
131
|
+
const action = root.action('job');
|
|
132
|
+
const handle = await action.exportID();
|
|
133
|
+
// pass handle to service B via queue, HTTP, etc.
|
|
134
|
+
|
|
135
|
+
// Service B
|
|
136
|
+
import { restoreFlow } from '@radishbot/sdk';
|
|
137
|
+
const action = await restoreFlow(key, handle);
|
|
138
|
+
action.info('continuing from service B');
|
|
139
|
+
await action.finish();
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Configuration
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
const root = await RL(key, {
|
|
146
|
+
host: 'wss://maincloud.spacetimedb.com', // default
|
|
147
|
+
dbName: 'radish-log', // default
|
|
148
|
+
defaultTimeout: 100, // seconds, default 100
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Sub-actions can have their own timeout:
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
await root.a('quick-task', async () => {
|
|
156
|
+
// ...
|
|
157
|
+
}, 10); // 10 second timeout
|
|
158
|
+
|
|
159
|
+
const slow = root.action('batch-job', 3600); // 1 hour
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Actions that exceed their timeout are automatically marked as timed out.
|
|
163
|
+
|
|
164
|
+
## API Reference
|
|
165
|
+
|
|
166
|
+
### `RL(secretKey, options?) → Promise<Flow>`
|
|
167
|
+
|
|
168
|
+
Connect and create a root action.
|
|
169
|
+
|
|
170
|
+
### `generateKey() → string`
|
|
171
|
+
|
|
172
|
+
Generate a random secret key (prefix `rl_`).
|
|
173
|
+
|
|
174
|
+
### `restoreFlow(secretKey, handle, options?) → Promise<Flow>`
|
|
175
|
+
|
|
176
|
+
Restore an action from an exported handle string.
|
|
177
|
+
|
|
178
|
+
### Flow (Action)
|
|
179
|
+
|
|
180
|
+
| Method | Description |
|
|
181
|
+
|---|---|
|
|
182
|
+
| `.a(name, fn, timeout?)` | Run a sub-action. Auto-finish, console capture. Returns fn result. |
|
|
183
|
+
| `.action(name, timeout?)` | Create a sub-action manually. |
|
|
184
|
+
| `.finish()` | Finish the action. |
|
|
185
|
+
| `.finishWithError(err?)` | Finish the action as errored. |
|
|
186
|
+
| `.info(msg, data?)` | Log at info level. |
|
|
187
|
+
| `.warn(msg, data?)` | Log at warn level. |
|
|
188
|
+
| `.error(msg, data?)` | Log at error level. |
|
|
189
|
+
| `.debug(msg, data?)` | Log at debug level. |
|
|
190
|
+
| `.log(msg, data?, level?)` | Log at any level. |
|
|
191
|
+
| `.exportID()` | Export handle for cross-context restore. |
|
|
192
|
+
| `.getId()` | Get server-assigned action ID. |
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@radishbot/sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"types": "src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"radish": "./src/cli.ts"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"src",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"cli": "bun run ./src/cli.ts"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"spacetimedb": "^2.0.3",
|
|
22
|
+
"ws": "^8.19.0"
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Radish CLI — inspect flows and logs from the terminal.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* radish <key> List recent flows
|
|
7
|
+
* radish <key> <flowId> Show flow detail + logs
|
|
8
|
+
* radish <key> tail Live-tail all logs
|
|
9
|
+
* radish <key> tree <flowId> Show flow tree
|
|
10
|
+
*/
|
|
11
|
+
import { DbConnection } from './module_bindings';
|
|
12
|
+
|
|
13
|
+
const HOST = 'wss://maincloud.spacetimedb.com';
|
|
14
|
+
const DB_NAME = 'radish-log';
|
|
15
|
+
|
|
16
|
+
// ── Colors ───────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
const c = {
|
|
19
|
+
reset: '\x1b[0m',
|
|
20
|
+
dim: '\x1b[2m',
|
|
21
|
+
bold: '\x1b[1m',
|
|
22
|
+
red: '\x1b[31m',
|
|
23
|
+
green: '\x1b[32m',
|
|
24
|
+
yellow: '\x1b[33m',
|
|
25
|
+
blue: '\x1b[34m',
|
|
26
|
+
magenta: '\x1b[35m',
|
|
27
|
+
cyan: '\x1b[36m',
|
|
28
|
+
gray: '\x1b[90m',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function levelColor(level: string): string {
|
|
32
|
+
switch (level) {
|
|
33
|
+
case 'info': return c.blue;
|
|
34
|
+
case 'warn': return c.yellow;
|
|
35
|
+
case 'error': return c.red;
|
|
36
|
+
case 'debug': return c.gray;
|
|
37
|
+
default: return c.dim;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function statusColor(status: string): string {
|
|
42
|
+
switch (status) {
|
|
43
|
+
case 'open': return c.blue;
|
|
44
|
+
case 'finished': return c.green;
|
|
45
|
+
case 'error': return c.red;
|
|
46
|
+
case 'timeout': return c.yellow;
|
|
47
|
+
default: return c.dim;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── Helpers ──────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
async function hashKey(key: string): Promise<string> {
|
|
54
|
+
const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(key));
|
|
55
|
+
return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function tsMicros(ts: any): bigint { return ts.microsSinceUnixEpoch; }
|
|
59
|
+
|
|
60
|
+
function fmtTime(ts: any): string {
|
|
61
|
+
return new Date(Number(tsMicros(ts) / 1000n)).toLocaleTimeString();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function fmtDateTime(ts: any): string {
|
|
65
|
+
const d = new Date(Number(tsMicros(ts) / 1000n));
|
|
66
|
+
return d.toLocaleString();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function fmtMs(us: number): string {
|
|
70
|
+
const ms = us / 1000;
|
|
71
|
+
if (ms < 1) return '<1ms';
|
|
72
|
+
if (ms < 1000) return `${Math.round(ms)}ms`;
|
|
73
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
74
|
+
return `${(ms / 60000).toFixed(1)}m`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function fmtDuration(startTs: any, endMicros: bigint): string {
|
|
78
|
+
return fmtMs(Number(endMicros - tsMicros(startTs)));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function fmtData(data: string): string {
|
|
82
|
+
if (!data || data === '{}') return '';
|
|
83
|
+
try {
|
|
84
|
+
const parsed = JSON.parse(data);
|
|
85
|
+
const str = JSON.stringify(parsed, null, 2);
|
|
86
|
+
// Indent continuation lines
|
|
87
|
+
return str.split('\n').map((line, i) => i === 0 ? ' ' + c.dim + line + c.reset : ' ' + c.dim + line + c.reset).join('\n');
|
|
88
|
+
} catch {
|
|
89
|
+
return ' ' + c.dim + data + c.reset;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function padRight(s: string, n: number): string {
|
|
94
|
+
return s.length >= n ? s.slice(0, n) : s + ' '.repeat(n - s.length);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── Connect ──────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
function connect(keyHash: string): Promise<{ conn: DbConnection; flows: () => any[]; logs: () => any[] }> {
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
const conn = DbConnection.builder()
|
|
102
|
+
.withUri(HOST)
|
|
103
|
+
.withDatabaseName(DB_NAME)
|
|
104
|
+
.onConnect((c, _identity, _token) => {
|
|
105
|
+
c.subscriptionBuilder()
|
|
106
|
+
.onApplied(() => {
|
|
107
|
+
const flows = () => {
|
|
108
|
+
const result: any[] = [];
|
|
109
|
+
for (const f of c.db.flow.iter()) if (f.keyHash === keyHash) result.push(f);
|
|
110
|
+
return result;
|
|
111
|
+
};
|
|
112
|
+
const logs = () => {
|
|
113
|
+
const result: any[] = [];
|
|
114
|
+
for (const l of c.db.logEntry.iter()) result.push(l);
|
|
115
|
+
return result;
|
|
116
|
+
};
|
|
117
|
+
resolve({ conn: c, flows, logs });
|
|
118
|
+
})
|
|
119
|
+
.subscribeToAllTables();
|
|
120
|
+
})
|
|
121
|
+
.onConnectError((_ctx, err) => reject(new Error(`Connection failed: ${err}`)))
|
|
122
|
+
.build();
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── Commands ─────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
function cmdList(flows: any[]) {
|
|
129
|
+
const roots = flows
|
|
130
|
+
.filter(f => f.parentFlowId === 0n)
|
|
131
|
+
.sort((a, b) => {
|
|
132
|
+
const am = tsMicros(a.createdAt), bm = tsMicros(b.createdAt);
|
|
133
|
+
return am > bm ? -1 : am < bm ? 1 : 0;
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (roots.length === 0) {
|
|
137
|
+
console.log(`${c.dim}No flows found for this key.${c.reset}`);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
console.log(`${c.bold}Flows${c.reset} ${c.dim}(${roots.length} total)${c.reset}\n`);
|
|
142
|
+
console.log(`${c.dim} ${'ID'.padEnd(8)} ${'STATUS'.padEnd(10)} ${'DURATION'.padEnd(10)} CREATED${c.reset}`);
|
|
143
|
+
console.log(`${c.dim} ${'─'.repeat(8)} ${'─'.repeat(10)} ${'─'.repeat(10)} ${'─'.repeat(24)}${c.reset}`);
|
|
144
|
+
|
|
145
|
+
for (const f of roots.slice(0, 30)) {
|
|
146
|
+
const id = f.id.toString().padEnd(8);
|
|
147
|
+
const sc = statusColor(f.status);
|
|
148
|
+
const status = padRight(f.status, 10);
|
|
149
|
+
const dur = f.finishedAt !== 0n ? padRight(fmtDuration(f.createdAt, f.finishedAt), 10) : padRight('—', 10);
|
|
150
|
+
const time = fmtDateTime(f.createdAt);
|
|
151
|
+
console.log(` ${c.dim}${id}${c.reset} ${sc}${status}${c.reset} ${dur} ${c.dim}${time}${c.reset}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (roots.length > 30) {
|
|
155
|
+
console.log(`${c.dim} ... and ${roots.length - 30} more${c.reset}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function cmdShow(flowId: bigint, allFlows: any[], allLogs: any[]) {
|
|
160
|
+
const flow = allFlows.find(f => f.id === flowId);
|
|
161
|
+
if (!flow) {
|
|
162
|
+
console.error(`${c.red}Flow ${flowId} not found.${c.reset}`);
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Header
|
|
167
|
+
const sc = statusColor(flow.status);
|
|
168
|
+
console.log(`${c.bold}Flow #${flow.id}${c.reset} ${sc}${flow.status}${c.reset}`);
|
|
169
|
+
console.log(`${c.dim}Created: ${fmtDateTime(flow.createdAt)}${c.reset}`);
|
|
170
|
+
if (flow.finishedAt !== 0n) {
|
|
171
|
+
console.log(`${c.dim}Duration: ${fmtDuration(flow.createdAt, flow.finishedAt)}${c.reset}`);
|
|
172
|
+
}
|
|
173
|
+
console.log();
|
|
174
|
+
|
|
175
|
+
// Child flows
|
|
176
|
+
const children = allFlows
|
|
177
|
+
.filter(f => f.parentFlowId === flowId)
|
|
178
|
+
.sort((a, b) => {
|
|
179
|
+
const am = tsMicros(a.createdAt), bm = tsMicros(b.createdAt);
|
|
180
|
+
return am < bm ? -1 : am > bm ? 1 : 0;
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
if (children.length > 0) {
|
|
184
|
+
console.log(`${c.bold}Actions${c.reset}\n`);
|
|
185
|
+
for (const ch of children) {
|
|
186
|
+
const sc = statusColor(ch.status);
|
|
187
|
+
const dur = ch.finishedAt !== 0n ? fmtDuration(ch.createdAt, ch.finishedAt) : '…';
|
|
188
|
+
console.log(` ${sc}●${c.reset} ${c.bold}${ch.name}${c.reset} ${c.dim}#${ch.id}${c.reset} ${sc}${ch.status}${c.reset} ${c.dim}${dur}${c.reset}`);
|
|
189
|
+
}
|
|
190
|
+
console.log();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Collect logs from this flow and all descendants
|
|
194
|
+
const flowIds = new Set<bigint>();
|
|
195
|
+
function collectIds(parentId: bigint) {
|
|
196
|
+
flowIds.add(parentId);
|
|
197
|
+
for (const f of allFlows) {
|
|
198
|
+
if (f.parentFlowId === parentId) collectIds(f.id);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
collectIds(flowId);
|
|
202
|
+
|
|
203
|
+
const logs = allLogs
|
|
204
|
+
.filter(l => flowIds.has(l.flowId) && l.level !== 'flow' && l.level !== 'action')
|
|
205
|
+
.sort((a, b) => {
|
|
206
|
+
const am = tsMicros(a.createdAt), bm = tsMicros(b.createdAt);
|
|
207
|
+
return am < bm ? -1 : am > bm ? 1 : 0;
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
if (logs.length === 0) {
|
|
211
|
+
console.log(`${c.dim}No logs.${c.reset}`);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Find source flow name for each log
|
|
216
|
+
const flowMap = new Map<bigint, string>();
|
|
217
|
+
for (const f of allFlows) flowMap.set(f.id, f.name === '/' ? 'root' : f.name);
|
|
218
|
+
|
|
219
|
+
console.log(`${c.bold}Logs${c.reset}\n`);
|
|
220
|
+
for (const l of logs) {
|
|
221
|
+
const time = fmtTime(l.createdAt);
|
|
222
|
+
const lc = levelColor(l.level);
|
|
223
|
+
const lvl = padRight(l.level, 5);
|
|
224
|
+
const source = l.flowId !== flowId ? ` ${c.cyan}[${flowMap.get(l.flowId) || '?'}]${c.reset}` : '';
|
|
225
|
+
const data = fmtData(l.data);
|
|
226
|
+
console.log(` ${c.dim}${time}${c.reset} ${lc}${lvl}${c.reset}${source} ${l.message}${data}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function cmdTree(flowId: bigint, allFlows: any[]) {
|
|
231
|
+
const flow = allFlows.find(f => f.id === flowId);
|
|
232
|
+
if (!flow) {
|
|
233
|
+
console.error(`${c.red}Flow ${flowId} not found.${c.reset}`);
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
console.log(`${c.bold}Flow Tree #${flow.id}${c.reset}\n`);
|
|
238
|
+
|
|
239
|
+
function printNode(f: any, prefix: string, connector: string) {
|
|
240
|
+
const sc = statusColor(f.status);
|
|
241
|
+
const name = f.name === '/' ? 'root' : f.name;
|
|
242
|
+
const dur = f.finishedAt !== 0n ? ` ${c.dim}${fmtDuration(f.createdAt, f.finishedAt)}${c.reset}` : '';
|
|
243
|
+
console.log(`${prefix}${connector}${sc}●${c.reset} ${c.bold}${name}${c.reset} ${c.dim}#${f.id}${c.reset} ${sc}${f.status}${c.reset}${dur}`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function printChildren(parentId: bigint, prefix: string) {
|
|
247
|
+
const children = allFlows
|
|
248
|
+
.filter(ch => ch.parentFlowId === parentId)
|
|
249
|
+
.sort((a, b) => {
|
|
250
|
+
const am = tsMicros(a.createdAt), bm = tsMicros(b.createdAt);
|
|
251
|
+
return am < bm ? -1 : am > bm ? 1 : 0;
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
for (let i = 0; i < children.length; i++) {
|
|
255
|
+
const isLast = i === children.length - 1;
|
|
256
|
+
printNode(children[i], prefix, isLast ? '└─ ' : '├─ ');
|
|
257
|
+
printChildren(children[i].id, prefix + (isLast ? ' ' : '│ '));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
printNode(flow, '', '');
|
|
262
|
+
printChildren(flowId, '');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function cmdTail(keyHash: string) {
|
|
266
|
+
console.log(`${c.bold}Tailing logs...${c.reset} ${c.dim}(Ctrl+C to stop)${c.reset}\n`);
|
|
267
|
+
|
|
268
|
+
const seen = new Set<bigint>();
|
|
269
|
+
const flowNames = new Map<bigint, string>();
|
|
270
|
+
|
|
271
|
+
const conn = await new Promise<DbConnection>((resolve, reject) => {
|
|
272
|
+
DbConnection.builder()
|
|
273
|
+
.withUri(HOST)
|
|
274
|
+
.withDatabaseName(DB_NAME)
|
|
275
|
+
.onConnect((c, _identity, _token) => {
|
|
276
|
+
c.subscriptionBuilder()
|
|
277
|
+
.onApplied(() => {
|
|
278
|
+
// Seed known flows and seen logs
|
|
279
|
+
for (const f of c.db.flow.iter()) {
|
|
280
|
+
if (f.keyHash === keyHash) flowNames.set(f.id, f.name === '/' ? 'root' : f.name);
|
|
281
|
+
}
|
|
282
|
+
for (const l of c.db.logEntry.iter()) seen.add(l.id);
|
|
283
|
+
resolve(c);
|
|
284
|
+
})
|
|
285
|
+
.subscribeToAllTables();
|
|
286
|
+
})
|
|
287
|
+
.onConnectError((_ctx, err) => reject(new Error(`Connection failed: ${err}`)))
|
|
288
|
+
.build();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// Poll for new logs
|
|
292
|
+
setInterval(() => {
|
|
293
|
+
// Update flow names
|
|
294
|
+
for (const f of conn.db.flow.iter()) {
|
|
295
|
+
if (f.keyHash === keyHash && !flowNames.has(f.id)) {
|
|
296
|
+
flowNames.set(f.id, f.name === '/' ? 'root' : f.name);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const newLogs: any[] = [];
|
|
301
|
+
for (const l of conn.db.logEntry.iter()) {
|
|
302
|
+
if (!seen.has(l.id) && l.level !== 'flow' && l.level !== 'action') {
|
|
303
|
+
// Only show logs belonging to this key's flows
|
|
304
|
+
if (flowNames.has(l.flowId)) {
|
|
305
|
+
seen.add(l.id);
|
|
306
|
+
newLogs.push(l);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
newLogs.sort((a, b) => {
|
|
312
|
+
const am = tsMicros(a.createdAt), bm = tsMicros(b.createdAt);
|
|
313
|
+
return am < bm ? -1 : am > bm ? 1 : 0;
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
for (const l of newLogs) {
|
|
317
|
+
const time = fmtTime(l.createdAt);
|
|
318
|
+
const lc = levelColor(l.level);
|
|
319
|
+
const lvl = padRight(l.level, 5);
|
|
320
|
+
const source = `${c.cyan}[${flowNames.get(l.flowId) || '?'}]${c.reset}`;
|
|
321
|
+
const data = fmtData(l.data);
|
|
322
|
+
console.log(`${c.dim}${time}${c.reset} ${lc}${lvl}${c.reset} ${source} ${l.message}${data}`);
|
|
323
|
+
}
|
|
324
|
+
}, 500);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ── Main ─────────────────────────────────────────────────────
|
|
328
|
+
|
|
329
|
+
async function main() {
|
|
330
|
+
const args = process.argv.slice(2);
|
|
331
|
+
|
|
332
|
+
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
333
|
+
console.log(`
|
|
334
|
+
${c.bold}radish${c.reset} — inspect flows and logs
|
|
335
|
+
|
|
336
|
+
${c.bold}Usage:${c.reset}
|
|
337
|
+
radish <key> List recent flows
|
|
338
|
+
radish <key> <flowId> Show flow detail + logs
|
|
339
|
+
radish <key> tree <flowId> Show flow tree
|
|
340
|
+
radish <key> tail Live-tail all logs
|
|
341
|
+
`);
|
|
342
|
+
process.exit(0);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const secretKey = args[0];
|
|
346
|
+
const keyHash = await hashKey(secretKey);
|
|
347
|
+
const subcmd = args[1];
|
|
348
|
+
|
|
349
|
+
if (subcmd === 'tail') {
|
|
350
|
+
await cmdTail(keyHash);
|
|
351
|
+
return; // stays alive
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Connect and fetch data
|
|
355
|
+
process.stderr.write(`${c.dim}Connecting...${c.reset}`);
|
|
356
|
+
const { conn, flows, logs } = await connect(keyHash);
|
|
357
|
+
process.stderr.write(`\r${' '.repeat(20)}\r`);
|
|
358
|
+
|
|
359
|
+
const allFlows = flows();
|
|
360
|
+
const allLogs = logs();
|
|
361
|
+
|
|
362
|
+
if (!subcmd) {
|
|
363
|
+
cmdList(allFlows);
|
|
364
|
+
} else if (subcmd === 'tree') {
|
|
365
|
+
const flowId = BigInt(args[2] || '0');
|
|
366
|
+
if (flowId === 0n) {
|
|
367
|
+
console.error(`${c.red}Usage: radish <key> tree <flowId>${c.reset}`);
|
|
368
|
+
process.exit(1);
|
|
369
|
+
}
|
|
370
|
+
cmdTree(flowId, allFlows);
|
|
371
|
+
} else {
|
|
372
|
+
const flowId = BigInt(subcmd);
|
|
373
|
+
cmdShow(flowId, allFlows, allLogs);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
conn.disconnect();
|
|
377
|
+
process.exit(0);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
main().catch(err => {
|
|
381
|
+
console.error(`${c.red}${err.message}${c.reset}`);
|
|
382
|
+
process.exit(1);
|
|
383
|
+
});
|