@radishbot/sdk 0.2.1 → 0.3.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/package.json +1 -1
- package/src/cli.ts +231 -79
- package/src/index.ts +18 -1
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -3,59 +3,92 @@
|
|
|
3
3
|
* Radish CLI — inspect flows and logs from the terminal.
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
|
-
* radish
|
|
7
|
-
* radish <key>
|
|
8
|
-
* radish <key>
|
|
9
|
-
* radish <key> tree <flowId>
|
|
6
|
+
* radish keygen Generate a new secret key
|
|
7
|
+
* radish <key> List recent flows
|
|
8
|
+
* radish <key> <flowId> Show flow detail + logs
|
|
9
|
+
* radish <key> tree <flowId> Show flow tree
|
|
10
|
+
* radish <key> tail Live-tail all logs
|
|
11
|
+
* radish <key> errors Show error groups
|
|
12
|
+
* radish <key> resolve <errorGroupId> Resolve an error group
|
|
13
|
+
* radish <key> ignore <errorGroupId> Ignore an error group
|
|
10
14
|
*/
|
|
11
|
-
import { DbConnection } from
|
|
15
|
+
import { DbConnection } from "./module_bindings";
|
|
12
16
|
|
|
13
|
-
const HOST =
|
|
14
|
-
const DB_NAME =
|
|
17
|
+
const HOST = "wss://maincloud.spacetimedb.com";
|
|
18
|
+
const DB_NAME = "radish-log";
|
|
15
19
|
|
|
16
20
|
// ── Colors ───────────────────────────────────────────────────
|
|
17
21
|
|
|
18
22
|
const c = {
|
|
19
|
-
reset:
|
|
20
|
-
dim:
|
|
21
|
-
bold:
|
|
22
|
-
red:
|
|
23
|
-
green:
|
|
24
|
-
yellow:
|
|
25
|
-
blue:
|
|
26
|
-
magenta:
|
|
27
|
-
cyan:
|
|
28
|
-
gray:
|
|
23
|
+
reset: "\x1b[0m",
|
|
24
|
+
dim: "\x1b[2m",
|
|
25
|
+
bold: "\x1b[1m",
|
|
26
|
+
red: "\x1b[31m",
|
|
27
|
+
green: "\x1b[32m",
|
|
28
|
+
yellow: "\x1b[33m",
|
|
29
|
+
blue: "\x1b[34m",
|
|
30
|
+
magenta: "\x1b[35m",
|
|
31
|
+
cyan: "\x1b[36m",
|
|
32
|
+
gray: "\x1b[90m",
|
|
29
33
|
};
|
|
30
34
|
|
|
31
35
|
function levelColor(level: string): string {
|
|
32
36
|
switch (level) {
|
|
33
|
-
case
|
|
34
|
-
|
|
35
|
-
case
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
case "info":
|
|
38
|
+
return c.blue;
|
|
39
|
+
case "warn":
|
|
40
|
+
return c.yellow;
|
|
41
|
+
case "error":
|
|
42
|
+
return c.red;
|
|
43
|
+
case "debug":
|
|
44
|
+
return c.gray;
|
|
45
|
+
default:
|
|
46
|
+
return c.dim;
|
|
38
47
|
}
|
|
39
48
|
}
|
|
40
49
|
|
|
41
50
|
function statusColor(status: string): string {
|
|
42
51
|
switch (status) {
|
|
43
|
-
case
|
|
44
|
-
|
|
45
|
-
case
|
|
46
|
-
|
|
47
|
-
|
|
52
|
+
case "open":
|
|
53
|
+
return c.blue;
|
|
54
|
+
case "finished":
|
|
55
|
+
return c.green;
|
|
56
|
+
case "error":
|
|
57
|
+
return c.red;
|
|
58
|
+
case "timeout":
|
|
59
|
+
return c.yellow;
|
|
60
|
+
default:
|
|
61
|
+
return c.dim;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function errorStatusColor(status: string): string {
|
|
66
|
+
switch (status) {
|
|
67
|
+
case "open":
|
|
68
|
+
return c.red;
|
|
69
|
+
case "regressed":
|
|
70
|
+
return c.yellow;
|
|
71
|
+
case "resolved":
|
|
72
|
+
return c.green;
|
|
73
|
+
case "ignored":
|
|
74
|
+
return c.gray;
|
|
75
|
+
default:
|
|
76
|
+
return c.dim;
|
|
48
77
|
}
|
|
49
78
|
}
|
|
50
79
|
|
|
51
80
|
// ── Helpers ──────────────────────────────────────────────────
|
|
52
81
|
|
|
53
82
|
async function hashKey(key: string): Promise<string> {
|
|
54
|
-
const buf = await crypto.subtle.digest(
|
|
55
|
-
return Array.from(new Uint8Array(buf))
|
|
83
|
+
const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(key));
|
|
84
|
+
return Array.from(new Uint8Array(buf))
|
|
85
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
86
|
+
.join("");
|
|
56
87
|
}
|
|
57
88
|
|
|
58
|
-
function tsMicros(ts: any): bigint {
|
|
89
|
+
function tsMicros(ts: any): bigint {
|
|
90
|
+
return ts.microsSinceUnixEpoch;
|
|
91
|
+
}
|
|
59
92
|
|
|
60
93
|
function fmtTime(ts: any): string {
|
|
61
94
|
return new Date(Number(tsMicros(ts) / 1000n)).toLocaleTimeString();
|
|
@@ -68,7 +101,7 @@ function fmtDateTime(ts: any): string {
|
|
|
68
101
|
|
|
69
102
|
function fmtMs(us: number): string {
|
|
70
103
|
const ms = us / 1000;
|
|
71
|
-
if (ms < 1) return
|
|
104
|
+
if (ms < 1) return "<1ms";
|
|
72
105
|
if (ms < 1000) return `${Math.round(ms)}ms`;
|
|
73
106
|
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
74
107
|
return `${(ms / 60000).toFixed(1)}m`;
|
|
@@ -79,24 +112,31 @@ function fmtDuration(startTs: any, endMicros: bigint): string {
|
|
|
79
112
|
}
|
|
80
113
|
|
|
81
114
|
function fmtData(data: string): string {
|
|
82
|
-
if (!data || data ===
|
|
115
|
+
if (!data || data === "{}") return "";
|
|
83
116
|
try {
|
|
84
117
|
const parsed = JSON.parse(data);
|
|
85
118
|
const str = JSON.stringify(parsed, null, 2);
|
|
86
119
|
// Indent continuation lines
|
|
87
|
-
return str
|
|
120
|
+
return str
|
|
121
|
+
.split("\n")
|
|
122
|
+
.map((line, i) =>
|
|
123
|
+
i === 0 ? " " + c.dim + line + c.reset : " " + c.dim + line + c.reset,
|
|
124
|
+
)
|
|
125
|
+
.join("\n");
|
|
88
126
|
} catch {
|
|
89
|
-
return
|
|
127
|
+
return " " + c.dim + data + c.reset;
|
|
90
128
|
}
|
|
91
129
|
}
|
|
92
130
|
|
|
93
131
|
function padRight(s: string, n: number): string {
|
|
94
|
-
return s.length >= n ? s.slice(0, n) : s +
|
|
132
|
+
return s.length >= n ? s.slice(0, n) : s + " ".repeat(n - s.length);
|
|
95
133
|
}
|
|
96
134
|
|
|
97
135
|
// ── Connect ──────────────────────────────────────────────────
|
|
98
136
|
|
|
99
|
-
function connect(
|
|
137
|
+
function connect(
|
|
138
|
+
keyHash: string,
|
|
139
|
+
): Promise<{ conn: DbConnection; flows: () => any[]; logs: () => any[]; errorGroups: () => any[] }> {
|
|
100
140
|
return new Promise((resolve, reject) => {
|
|
101
141
|
const conn = DbConnection.builder()
|
|
102
142
|
.withUri(HOST)
|
|
@@ -114,7 +154,12 @@ function connect(keyHash: string): Promise<{ conn: DbConnection; flows: () => an
|
|
|
114
154
|
for (const l of c.db.logEntry.iter()) result.push(l);
|
|
115
155
|
return result;
|
|
116
156
|
};
|
|
117
|
-
|
|
157
|
+
const errorGroups = () => {
|
|
158
|
+
const result: any[] = [];
|
|
159
|
+
for (const g of c.db.errorGroup.iter()) if (g.keyHash === keyHash) result.push(g);
|
|
160
|
+
return result;
|
|
161
|
+
};
|
|
162
|
+
resolve({ conn: c, flows, logs, errorGroups });
|
|
118
163
|
})
|
|
119
164
|
.subscribeToAllTables();
|
|
120
165
|
})
|
|
@@ -127,9 +172,10 @@ function connect(keyHash: string): Promise<{ conn: DbConnection; flows: () => an
|
|
|
127
172
|
|
|
128
173
|
function cmdList(flows: any[]) {
|
|
129
174
|
const roots = flows
|
|
130
|
-
.filter(f => f.parentFlowId === 0n)
|
|
175
|
+
.filter((f) => f.parentFlowId === 0n)
|
|
131
176
|
.sort((a, b) => {
|
|
132
|
-
const am = tsMicros(a.createdAt),
|
|
177
|
+
const am = tsMicros(a.createdAt),
|
|
178
|
+
bm = tsMicros(b.createdAt);
|
|
133
179
|
return am > bm ? -1 : am < bm ? 1 : 0;
|
|
134
180
|
});
|
|
135
181
|
|
|
@@ -138,17 +184,29 @@ function cmdList(flows: any[]) {
|
|
|
138
184
|
return;
|
|
139
185
|
}
|
|
140
186
|
|
|
187
|
+
const hasRelease = roots.some((f) => f.release && f.release !== "");
|
|
188
|
+
|
|
141
189
|
console.log(`${c.bold}Flows${c.reset} ${c.dim}(${roots.length} total)${c.reset}\n`);
|
|
142
|
-
|
|
143
|
-
|
|
190
|
+
if (hasRelease) {
|
|
191
|
+
console.log(
|
|
192
|
+
`${c.dim} ${"ID".padEnd(8)} ${"STATUS".padEnd(10)} ${"RELEASE".padEnd(10)} ${"DURATION".padEnd(10)} CREATED${c.reset}`,
|
|
193
|
+
);
|
|
194
|
+
console.log(
|
|
195
|
+
`${c.dim} ${"─".repeat(8)} ${"─".repeat(10)} ${"─".repeat(10)} ${"─".repeat(10)} ${"─".repeat(24)}${c.reset}`,
|
|
196
|
+
);
|
|
197
|
+
} else {
|
|
198
|
+
console.log(`${c.dim} ${"ID".padEnd(8)} ${"STATUS".padEnd(10)} ${"DURATION".padEnd(10)} CREATED${c.reset}`);
|
|
199
|
+
console.log(`${c.dim} ${"─".repeat(8)} ${"─".repeat(10)} ${"─".repeat(10)} ${"─".repeat(24)}${c.reset}`);
|
|
200
|
+
}
|
|
144
201
|
|
|
145
202
|
for (const f of roots.slice(0, 30)) {
|
|
146
203
|
const id = f.id.toString().padEnd(8);
|
|
147
204
|
const sc = statusColor(f.status);
|
|
148
205
|
const status = padRight(f.status, 10);
|
|
149
|
-
const dur = f.finishedAt !== 0n ? padRight(fmtDuration(f.createdAt, f.finishedAt), 10) : padRight(
|
|
206
|
+
const dur = f.finishedAt !== 0n ? padRight(fmtDuration(f.createdAt, f.finishedAt), 10) : padRight("—", 10);
|
|
150
207
|
const time = fmtDateTime(f.createdAt);
|
|
151
|
-
|
|
208
|
+
const release = hasRelease ? padRight(f.release || "—", 10) + " " : "";
|
|
209
|
+
console.log(` ${c.dim}${id}${c.reset} ${sc}${status}${c.reset} ${release}${dur} ${c.dim}${time}${c.reset}`);
|
|
152
210
|
}
|
|
153
211
|
|
|
154
212
|
if (roots.length > 30) {
|
|
@@ -157,7 +215,7 @@ function cmdList(flows: any[]) {
|
|
|
157
215
|
}
|
|
158
216
|
|
|
159
217
|
function cmdShow(flowId: bigint, allFlows: any[], allLogs: any[]) {
|
|
160
|
-
const flow = allFlows.find(f => f.id === flowId);
|
|
218
|
+
const flow = allFlows.find((f) => f.id === flowId);
|
|
161
219
|
if (!flow) {
|
|
162
220
|
console.error(`${c.red}Flow ${flowId} not found.${c.reset}`);
|
|
163
221
|
process.exit(1);
|
|
@@ -165,18 +223,23 @@ function cmdShow(flowId: bigint, allFlows: any[], allLogs: any[]) {
|
|
|
165
223
|
|
|
166
224
|
// Header
|
|
167
225
|
const sc = statusColor(flow.status);
|
|
168
|
-
|
|
226
|
+
const releaseTag = flow.release ? ` ${c.dim}(${flow.release})${c.reset}` : "";
|
|
227
|
+
console.log(`${c.bold}Flow #${flow.id}${c.reset} ${sc}${flow.status}${c.reset}${releaseTag}`);
|
|
169
228
|
console.log(`${c.dim}Created: ${fmtDateTime(flow.createdAt)}${c.reset}`);
|
|
170
229
|
if (flow.finishedAt !== 0n) {
|
|
171
230
|
console.log(`${c.dim}Duration: ${fmtDuration(flow.createdAt, flow.finishedAt)}${c.reset}`);
|
|
172
231
|
}
|
|
232
|
+
if (flow.release) {
|
|
233
|
+
console.log(`${c.dim}Release: ${flow.release}${c.reset}`);
|
|
234
|
+
}
|
|
173
235
|
console.log();
|
|
174
236
|
|
|
175
237
|
// Child flows
|
|
176
238
|
const children = allFlows
|
|
177
|
-
.filter(f => f.parentFlowId === flowId)
|
|
239
|
+
.filter((f) => f.parentFlowId === flowId)
|
|
178
240
|
.sort((a, b) => {
|
|
179
|
-
const am = tsMicros(a.createdAt),
|
|
241
|
+
const am = tsMicros(a.createdAt),
|
|
242
|
+
bm = tsMicros(b.createdAt);
|
|
180
243
|
return am < bm ? -1 : am > bm ? 1 : 0;
|
|
181
244
|
});
|
|
182
245
|
|
|
@@ -184,8 +247,10 @@ function cmdShow(flowId: bigint, allFlows: any[], allLogs: any[]) {
|
|
|
184
247
|
console.log(`${c.bold}Actions${c.reset}\n`);
|
|
185
248
|
for (const ch of children) {
|
|
186
249
|
const sc = statusColor(ch.status);
|
|
187
|
-
const dur = ch.finishedAt !== 0n ? fmtDuration(ch.createdAt, ch.finishedAt) :
|
|
188
|
-
console.log(
|
|
250
|
+
const dur = ch.finishedAt !== 0n ? fmtDuration(ch.createdAt, ch.finishedAt) : "…";
|
|
251
|
+
console.log(
|
|
252
|
+
` ${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}`,
|
|
253
|
+
);
|
|
189
254
|
}
|
|
190
255
|
console.log();
|
|
191
256
|
}
|
|
@@ -201,9 +266,10 @@ function cmdShow(flowId: bigint, allFlows: any[], allLogs: any[]) {
|
|
|
201
266
|
collectIds(flowId);
|
|
202
267
|
|
|
203
268
|
const logs = allLogs
|
|
204
|
-
.filter(l => flowIds.has(l.flowId) && l.level !==
|
|
269
|
+
.filter((l) => flowIds.has(l.flowId) && l.level !== "flow" && l.level !== "action")
|
|
205
270
|
.sort((a, b) => {
|
|
206
|
-
const am = tsMicros(a.createdAt),
|
|
271
|
+
const am = tsMicros(a.createdAt),
|
|
272
|
+
bm = tsMicros(b.createdAt);
|
|
207
273
|
return am < bm ? -1 : am > bm ? 1 : 0;
|
|
208
274
|
});
|
|
209
275
|
|
|
@@ -214,21 +280,21 @@ function cmdShow(flowId: bigint, allFlows: any[], allLogs: any[]) {
|
|
|
214
280
|
|
|
215
281
|
// Find source flow name for each log
|
|
216
282
|
const flowMap = new Map<bigint, string>();
|
|
217
|
-
for (const f of allFlows) flowMap.set(f.id, f.name ===
|
|
283
|
+
for (const f of allFlows) flowMap.set(f.id, f.name === "/" ? "root" : f.name);
|
|
218
284
|
|
|
219
285
|
console.log(`${c.bold}Logs${c.reset}\n`);
|
|
220
286
|
for (const l of logs) {
|
|
221
287
|
const time = fmtTime(l.createdAt);
|
|
222
288
|
const lc = levelColor(l.level);
|
|
223
289
|
const lvl = padRight(l.level, 5);
|
|
224
|
-
const source = l.flowId !== flowId ? ` ${c.cyan}[${flowMap.get(l.flowId) ||
|
|
290
|
+
const source = l.flowId !== flowId ? ` ${c.cyan}[${flowMap.get(l.flowId) || "?"}]${c.reset}` : "";
|
|
225
291
|
const data = fmtData(l.data);
|
|
226
292
|
console.log(` ${c.dim}${time}${c.reset} ${lc}${lvl}${c.reset}${source} ${l.message}${data}`);
|
|
227
293
|
}
|
|
228
294
|
}
|
|
229
295
|
|
|
230
296
|
function cmdTree(flowId: bigint, allFlows: any[]) {
|
|
231
|
-
const flow = allFlows.find(f => f.id === flowId);
|
|
297
|
+
const flow = allFlows.find((f) => f.id === flowId);
|
|
232
298
|
if (!flow) {
|
|
233
299
|
console.error(`${c.red}Flow ${flowId} not found.${c.reset}`);
|
|
234
300
|
process.exit(1);
|
|
@@ -238,28 +304,31 @@ function cmdTree(flowId: bigint, allFlows: any[]) {
|
|
|
238
304
|
|
|
239
305
|
function printNode(f: any, prefix: string, connector: string) {
|
|
240
306
|
const sc = statusColor(f.status);
|
|
241
|
-
const name = f.name ===
|
|
242
|
-
const dur = f.finishedAt !== 0n ? ` ${c.dim}${fmtDuration(f.createdAt, f.finishedAt)}${c.reset}` :
|
|
243
|
-
console.log(
|
|
307
|
+
const name = f.name === "/" ? "root" : f.name;
|
|
308
|
+
const dur = f.finishedAt !== 0n ? ` ${c.dim}${fmtDuration(f.createdAt, f.finishedAt)}${c.reset}` : "";
|
|
309
|
+
console.log(
|
|
310
|
+
`${prefix}${connector}${sc}●${c.reset} ${c.bold}${name}${c.reset} ${c.dim}#${f.id}${c.reset} ${sc}${f.status}${c.reset}${dur}`,
|
|
311
|
+
);
|
|
244
312
|
}
|
|
245
313
|
|
|
246
314
|
function printChildren(parentId: bigint, prefix: string) {
|
|
247
315
|
const children = allFlows
|
|
248
|
-
.filter(ch => ch.parentFlowId === parentId)
|
|
316
|
+
.filter((ch) => ch.parentFlowId === parentId)
|
|
249
317
|
.sort((a, b) => {
|
|
250
|
-
const am = tsMicros(a.createdAt),
|
|
318
|
+
const am = tsMicros(a.createdAt),
|
|
319
|
+
bm = tsMicros(b.createdAt);
|
|
251
320
|
return am < bm ? -1 : am > bm ? 1 : 0;
|
|
252
321
|
});
|
|
253
322
|
|
|
254
323
|
for (let i = 0; i < children.length; i++) {
|
|
255
324
|
const isLast = i === children.length - 1;
|
|
256
|
-
printNode(children[i], prefix, isLast ?
|
|
257
|
-
printChildren(children[i].id, prefix + (isLast ?
|
|
325
|
+
printNode(children[i], prefix, isLast ? "└─ " : "├─ ");
|
|
326
|
+
printChildren(children[i].id, prefix + (isLast ? " " : "│ "));
|
|
258
327
|
}
|
|
259
328
|
}
|
|
260
329
|
|
|
261
|
-
printNode(flow,
|
|
262
|
-
printChildren(flowId,
|
|
330
|
+
printNode(flow, "", "");
|
|
331
|
+
printChildren(flowId, "");
|
|
263
332
|
}
|
|
264
333
|
|
|
265
334
|
async function cmdTail(keyHash: string) {
|
|
@@ -277,7 +346,7 @@ async function cmdTail(keyHash: string) {
|
|
|
277
346
|
.onApplied(() => {
|
|
278
347
|
// Seed known flows and seen logs
|
|
279
348
|
for (const f of c.db.flow.iter()) {
|
|
280
|
-
if (f.keyHash === keyHash) flowNames.set(f.id, f.name ===
|
|
349
|
+
if (f.keyHash === keyHash) flowNames.set(f.id, f.name === "/" ? "root" : f.name);
|
|
281
350
|
}
|
|
282
351
|
for (const l of c.db.logEntry.iter()) seen.add(l.id);
|
|
283
352
|
resolve(c);
|
|
@@ -293,13 +362,13 @@ async function cmdTail(keyHash: string) {
|
|
|
293
362
|
// Update flow names
|
|
294
363
|
for (const f of conn.db.flow.iter()) {
|
|
295
364
|
if (f.keyHash === keyHash && !flowNames.has(f.id)) {
|
|
296
|
-
flowNames.set(f.id, f.name ===
|
|
365
|
+
flowNames.set(f.id, f.name === "/" ? "root" : f.name);
|
|
297
366
|
}
|
|
298
367
|
}
|
|
299
368
|
|
|
300
369
|
const newLogs: any[] = [];
|
|
301
370
|
for (const l of conn.db.logEntry.iter()) {
|
|
302
|
-
if (!seen.has(l.id) && l.level !==
|
|
371
|
+
if (!seen.has(l.id) && l.level !== "flow" && l.level !== "action") {
|
|
303
372
|
// Only show logs belonging to this key's flows
|
|
304
373
|
if (flowNames.has(l.flowId)) {
|
|
305
374
|
seen.add(l.id);
|
|
@@ -309,7 +378,8 @@ async function cmdTail(keyHash: string) {
|
|
|
309
378
|
}
|
|
310
379
|
|
|
311
380
|
newLogs.sort((a, b) => {
|
|
312
|
-
const am = tsMicros(a.createdAt),
|
|
381
|
+
const am = tsMicros(a.createdAt),
|
|
382
|
+
bm = tsMicros(b.createdAt);
|
|
313
383
|
return am < bm ? -1 : am > bm ? 1 : 0;
|
|
314
384
|
});
|
|
315
385
|
|
|
@@ -317,57 +387,139 @@ async function cmdTail(keyHash: string) {
|
|
|
317
387
|
const time = fmtTime(l.createdAt);
|
|
318
388
|
const lc = levelColor(l.level);
|
|
319
389
|
const lvl = padRight(l.level, 5);
|
|
320
|
-
const source = `${c.cyan}[${flowNames.get(l.flowId) ||
|
|
390
|
+
const source = `${c.cyan}[${flowNames.get(l.flowId) || "?"}]${c.reset}`;
|
|
321
391
|
const data = fmtData(l.data);
|
|
322
392
|
console.log(`${c.dim}${time}${c.reset} ${lc}${lvl}${c.reset} ${source} ${l.message}${data}`);
|
|
323
393
|
}
|
|
324
394
|
}, 500);
|
|
325
395
|
}
|
|
326
396
|
|
|
397
|
+
function cmdErrors(errorGroups: any[]) {
|
|
398
|
+
const statusOrder: Record<string, number> = { open: 0, regressed: 1, resolved: 2, ignored: 3 };
|
|
399
|
+
const sorted = [...errorGroups].sort((a, b) => {
|
|
400
|
+
const ao = statusOrder[a.status] ?? 99,
|
|
401
|
+
bo = statusOrder[b.status] ?? 99;
|
|
402
|
+
if (ao !== bo) return ao - bo;
|
|
403
|
+
// Within same status priority, sort by count descending
|
|
404
|
+
return a.count > b.count ? -1 : a.count < b.count ? 1 : 0;
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
if (sorted.length === 0) {
|
|
408
|
+
console.log(`${c.dim}No error groups found for this key.${c.reset}`);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
console.log(`${c.bold}Errors${c.reset} ${c.dim}(${sorted.length} groups)${c.reset}\n`);
|
|
413
|
+
console.log(
|
|
414
|
+
`${c.dim} ${"ID".padEnd(8)} ${"STATUS".padEnd(10)} ${"COUNT".padEnd(6)} ${"RELEASE".padEnd(10)} ${"PATH".padEnd(23)} MESSAGE${c.reset}`,
|
|
415
|
+
);
|
|
416
|
+
console.log(
|
|
417
|
+
`${c.dim} ${"─".repeat(8)} ${"─".repeat(10)} ${"─".repeat(6)} ${"─".repeat(10)} ${"─".repeat(23)} ${"─".repeat(25)}${c.reset}`,
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
for (const g of sorted) {
|
|
421
|
+
const id = g.id.toString().padEnd(8);
|
|
422
|
+
const esc = errorStatusColor(g.status);
|
|
423
|
+
const status = padRight(g.status, 10);
|
|
424
|
+
const count = g.count.toString().padEnd(6);
|
|
425
|
+
const release = padRight(g.release || "—", 10);
|
|
426
|
+
const path = padRight(g.path || "—", 23);
|
|
427
|
+
const message = g.message.length > 50 ? g.message.slice(0, 47) + "..." : g.message;
|
|
428
|
+
console.log(
|
|
429
|
+
` ${c.dim}${id}${c.reset} ${esc}${status}${c.reset} ${count} ${c.dim}${release}${c.reset} ${c.dim}${path}${c.reset} ${message}`,
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async function cmdUpdateErrorGroupStatus(conn: DbConnection, keyHash: string, errorGroupId: string, status: string) {
|
|
435
|
+
conn.reducers.updateErrorGroupStatus({
|
|
436
|
+
keyHash,
|
|
437
|
+
errorGroupId: BigInt(errorGroupId),
|
|
438
|
+
status,
|
|
439
|
+
});
|
|
440
|
+
const label = status === "resolved" ? "Resolved" : "Ignored";
|
|
441
|
+
console.log(`${c.green}${label} error group #${errorGroupId}.${c.reset}`);
|
|
442
|
+
}
|
|
443
|
+
|
|
327
444
|
// ── Main ─────────────────────────────────────────────────────
|
|
328
445
|
|
|
329
446
|
async function main() {
|
|
330
447
|
const args = process.argv.slice(2);
|
|
331
448
|
|
|
332
|
-
if (args.length === 0 || args[0] ===
|
|
449
|
+
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
333
450
|
console.log(`
|
|
334
451
|
${c.bold}radish${c.reset} — inspect flows and logs
|
|
335
452
|
|
|
336
453
|
${c.bold}Usage:${c.reset}
|
|
337
|
-
radish
|
|
338
|
-
radish <key>
|
|
339
|
-
radish <key>
|
|
340
|
-
radish <key>
|
|
454
|
+
radish keygen Generate a new secret key
|
|
455
|
+
radish <key> List recent flows
|
|
456
|
+
radish <key> <flowId> Show flow detail + logs
|
|
457
|
+
radish <key> tree <flowId> Show flow tree
|
|
458
|
+
radish <key> tail Live-tail all logs
|
|
459
|
+
radish <key> errors Show error groups
|
|
460
|
+
radish <key> resolve <errorGroupId> Resolve an error group
|
|
461
|
+
radish <key> ignore <errorGroupId> Ignore an error group
|
|
341
462
|
`);
|
|
342
463
|
process.exit(0);
|
|
343
464
|
}
|
|
344
465
|
|
|
466
|
+
// keygen subcommand — no key required
|
|
467
|
+
if (args[0] === "keygen") {
|
|
468
|
+
const bytes = new Uint8Array(32);
|
|
469
|
+
crypto.getRandomValues(bytes);
|
|
470
|
+
const key =
|
|
471
|
+
"rl_" +
|
|
472
|
+
Array.from(bytes)
|
|
473
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
474
|
+
.join("");
|
|
475
|
+
console.log(`\n${c.bold}Generated key:${c.reset} ${key}\n`);
|
|
476
|
+
console.log(`Save this key — it's your dashboard login.`);
|
|
477
|
+
console.log(`Dashboard: ${c.cyan}https://radish-log.spacetimedb.com${c.reset}\n`);
|
|
478
|
+
process.exit(0);
|
|
479
|
+
}
|
|
480
|
+
|
|
345
481
|
const secretKey = args[0];
|
|
346
482
|
const keyHash = await hashKey(secretKey);
|
|
347
483
|
const subcmd = args[1];
|
|
348
484
|
|
|
349
|
-
if (subcmd ===
|
|
485
|
+
if (subcmd === "tail") {
|
|
350
486
|
await cmdTail(keyHash);
|
|
351
487
|
return; // stays alive
|
|
352
488
|
}
|
|
353
489
|
|
|
354
490
|
// Connect and fetch data
|
|
355
491
|
process.stderr.write(`${c.dim}Connecting...${c.reset}`);
|
|
356
|
-
const { conn, flows, logs } = await connect(keyHash);
|
|
357
|
-
process.stderr.write(`\r${
|
|
492
|
+
const { conn, flows, logs, errorGroups } = await connect(keyHash);
|
|
493
|
+
process.stderr.write(`\r${" ".repeat(20)}\r`);
|
|
358
494
|
|
|
359
495
|
const allFlows = flows();
|
|
360
496
|
const allLogs = logs();
|
|
361
497
|
|
|
362
498
|
if (!subcmd) {
|
|
363
499
|
cmdList(allFlows);
|
|
364
|
-
} else if (subcmd ===
|
|
365
|
-
const flowId = BigInt(args[2] ||
|
|
500
|
+
} else if (subcmd === "tree") {
|
|
501
|
+
const flowId = BigInt(args[2] || "0");
|
|
366
502
|
if (flowId === 0n) {
|
|
367
503
|
console.error(`${c.red}Usage: radish <key> tree <flowId>${c.reset}`);
|
|
368
504
|
process.exit(1);
|
|
369
505
|
}
|
|
370
506
|
cmdTree(flowId, allFlows);
|
|
507
|
+
} else if (subcmd === "errors") {
|
|
508
|
+
cmdErrors(errorGroups());
|
|
509
|
+
} else if (subcmd === "resolve") {
|
|
510
|
+
const errorGroupId = args[2];
|
|
511
|
+
if (!errorGroupId) {
|
|
512
|
+
console.error(`${c.red}Usage: radish <key> resolve <errorGroupId>${c.reset}`);
|
|
513
|
+
process.exit(1);
|
|
514
|
+
}
|
|
515
|
+
await cmdUpdateErrorGroupStatus(conn, keyHash, errorGroupId, "resolved");
|
|
516
|
+
} else if (subcmd === "ignore") {
|
|
517
|
+
const errorGroupId = args[2];
|
|
518
|
+
if (!errorGroupId) {
|
|
519
|
+
console.error(`${c.red}Usage: radish <key> ignore <errorGroupId>${c.reset}`);
|
|
520
|
+
process.exit(1);
|
|
521
|
+
}
|
|
522
|
+
await cmdUpdateErrorGroupStatus(conn, keyHash, errorGroupId, "ignored");
|
|
371
523
|
} else {
|
|
372
524
|
const flowId = BigInt(subcmd);
|
|
373
525
|
cmdShow(flowId, allFlows, allLogs);
|
|
@@ -377,7 +529,7 @@ ${c.bold}Usage:${c.reset}
|
|
|
377
529
|
process.exit(0);
|
|
378
530
|
}
|
|
379
531
|
|
|
380
|
-
main().catch(err => {
|
|
532
|
+
main().catch((err) => {
|
|
381
533
|
console.error(`${c.red}${err.message}${c.reset}`);
|
|
382
534
|
process.exit(1);
|
|
383
535
|
});
|
package/src/index.ts
CHANGED
|
@@ -260,6 +260,19 @@ export class Flow {
|
|
|
260
260
|
return this._id!;
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
+
/** Disconnect the underlying SpacetimeDB connection */
|
|
264
|
+
disconnect(): void {
|
|
265
|
+
this._sdk.disconnect();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Finish the flow and disconnect */
|
|
269
|
+
async finishAndDisconnect(): Promise<void> {
|
|
270
|
+
await this.finish();
|
|
271
|
+
// Small delay to allow the finish reducer to be sent
|
|
272
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
273
|
+
this.disconnect();
|
|
274
|
+
}
|
|
275
|
+
|
|
263
276
|
/** Schedule a drain on the next microtask — batches sync logs with ~0ms delay */
|
|
264
277
|
private _scheduleFlush() {
|
|
265
278
|
if (this._flushScheduled) return;
|
|
@@ -345,7 +358,11 @@ export async function RL(secretKey: string, options: RLOptions = {}): Promise<Fl
|
|
|
345
358
|
const keyHash = await hashKey(secretKey);
|
|
346
359
|
const sdk = new SdkConnection(host, dbName);
|
|
347
360
|
sdk.setKeyHash(keyHash);
|
|
348
|
-
|
|
361
|
+
try {
|
|
362
|
+
await sdk.connect();
|
|
363
|
+
} catch (e) {
|
|
364
|
+
throw new Error(`Failed to connect to Radish (${host}/${dbName}): ${e instanceof Error ? e.message : e}`);
|
|
365
|
+
}
|
|
349
366
|
|
|
350
367
|
// Register key (idempotent — updates retention if changed)
|
|
351
368
|
try {
|