@thingd/cli 0.54.0 → 0.55.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/interactive.d.ts.map +1 -1
- package/dist/interactive.js +229 -29
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"interactive.d.ts","sourceRoot":"","sources":["../src/interactive.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"interactive.d.ts","sourceRoot":"","sources":["../src/interactive.ts"],"names":[],"mappings":"AA+/EA,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CA0FvD"}
|
package/dist/interactive.js
CHANGED
|
@@ -80,6 +80,8 @@ let totalActiveJobsCount = 0;
|
|
|
80
80
|
let totalDeadJobsCount = 0;
|
|
81
81
|
let totalLinksCount = 0;
|
|
82
82
|
let cloudError = null;
|
|
83
|
+
const eventsByStream = new Map();
|
|
84
|
+
const jobsByQueue = new Map();
|
|
83
85
|
let objectsHistory = [];
|
|
84
86
|
let eventsHistory = [];
|
|
85
87
|
let activeJobsHistory = [];
|
|
@@ -294,6 +296,29 @@ async function fetchResources() {
|
|
|
294
296
|
objectsByCollection.set(col, []);
|
|
295
297
|
}
|
|
296
298
|
}
|
|
299
|
+
// Populate events per stream
|
|
300
|
+
eventsByStream.clear();
|
|
301
|
+
await Promise.all(streams.map(async (s) => {
|
|
302
|
+
try {
|
|
303
|
+
const evts = await db.events.list(s, { limit: 20 });
|
|
304
|
+
eventsByStream.set(s, evts);
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
eventsByStream.set(s, []);
|
|
308
|
+
}
|
|
309
|
+
}));
|
|
310
|
+
// Populate jobs per queue
|
|
311
|
+
jobsByQueue.clear();
|
|
312
|
+
await Promise.all(queues.map(async (q) => {
|
|
313
|
+
try {
|
|
314
|
+
const active = await db.queue(q).list();
|
|
315
|
+
const dead = await db.queue(q).dead();
|
|
316
|
+
jobsByQueue.set(q, { active, dead });
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
jobsByQueue.set(q, { active: [], dead: [] });
|
|
320
|
+
}
|
|
321
|
+
}));
|
|
297
322
|
// Calculate Deltas for Operations Throughput Rates
|
|
298
323
|
const prevObjects = objectsHistory.length > 0
|
|
299
324
|
? (objectsHistory[objectsHistory.length - 1] ?? totalObjects)
|
|
@@ -510,15 +535,40 @@ function buildTree() {
|
|
|
510
535
|
});
|
|
511
536
|
}
|
|
512
537
|
for (const stream of streams) {
|
|
538
|
+
const sOpen = expandedSet.has(`stream:${stream}`);
|
|
513
539
|
nodes.push({
|
|
514
540
|
id: `stream:${stream}`,
|
|
515
541
|
parentId: "cat:streams",
|
|
516
542
|
type: "stream",
|
|
517
|
-
label: `${pc.
|
|
543
|
+
label: `${sOpen ? pc.cyan("▾") : pc.dim("▸")} ${pc.green(stream)}`,
|
|
518
544
|
depth: 1,
|
|
519
|
-
expandable:
|
|
545
|
+
expandable: true,
|
|
520
546
|
ref: { name: stream },
|
|
521
547
|
});
|
|
548
|
+
if (sOpen) {
|
|
549
|
+
const evts = eventsByStream.get(stream) ?? [];
|
|
550
|
+
if (evts.length === 0) {
|
|
551
|
+
nodes.push({
|
|
552
|
+
id: `empty:evt:${stream}`,
|
|
553
|
+
parentId: `stream:${stream}`,
|
|
554
|
+
type: "status",
|
|
555
|
+
label: pc.dim("(no events)"),
|
|
556
|
+
depth: 2,
|
|
557
|
+
expandable: false,
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
for (const evt of evts) {
|
|
561
|
+
nodes.push({
|
|
562
|
+
id: `evt:${stream}:${evt.id}`,
|
|
563
|
+
parentId: `stream:${stream}`,
|
|
564
|
+
type: "event",
|
|
565
|
+
label: `${pc.dim("·")} ${pc.dim(evt.type || "unknown")}`,
|
|
566
|
+
depth: 2,
|
|
567
|
+
expandable: false,
|
|
568
|
+
ref: { stream: stream, eventId: evt.id, eventData: evt },
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
}
|
|
522
572
|
}
|
|
523
573
|
}
|
|
524
574
|
// Queues
|
|
@@ -541,15 +591,87 @@ function buildTree() {
|
|
|
541
591
|
});
|
|
542
592
|
}
|
|
543
593
|
for (const q of queues) {
|
|
594
|
+
const qOpen = expandedSet.has(`queue:${q}`);
|
|
544
595
|
nodes.push({
|
|
545
596
|
id: `queue:${q}`,
|
|
546
597
|
parentId: "cat:queues",
|
|
547
598
|
type: "queue",
|
|
548
|
-
label: `${pc.
|
|
599
|
+
label: `${qOpen ? pc.cyan("▾") : pc.dim("▸")} ${pc.magenta(q)}`,
|
|
549
600
|
depth: 1,
|
|
550
|
-
expandable:
|
|
601
|
+
expandable: true,
|
|
551
602
|
ref: { name: q },
|
|
552
603
|
});
|
|
604
|
+
if (qOpen) {
|
|
605
|
+
const jobData = jobsByQueue.get(q);
|
|
606
|
+
const activeJobs = jobData?.active ?? [];
|
|
607
|
+
const deadJobs = jobData?.dead ?? [];
|
|
608
|
+
// Active jobs subcategory
|
|
609
|
+
const activeOpen = expandedSet.has(`queue:${q}:active`);
|
|
610
|
+
nodes.push({
|
|
611
|
+
id: `queue:${q}:active`,
|
|
612
|
+
parentId: `queue:${q}`,
|
|
613
|
+
type: "category",
|
|
614
|
+
label: `${activeOpen ? pc.cyan("▾") : pc.dim("▸")} ${pc.bold("Active")} ${pc.dim(`(${activeJobs.length})`)}`,
|
|
615
|
+
depth: 2,
|
|
616
|
+
expandable: true,
|
|
617
|
+
});
|
|
618
|
+
if (activeOpen) {
|
|
619
|
+
if (activeJobs.length === 0) {
|
|
620
|
+
nodes.push({
|
|
621
|
+
id: `empty:active:${q}`,
|
|
622
|
+
parentId: `queue:${q}:active`,
|
|
623
|
+
type: "status",
|
|
624
|
+
label: pc.dim("(no active jobs)"),
|
|
625
|
+
depth: 3,
|
|
626
|
+
expandable: false,
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
for (const job of activeJobs) {
|
|
630
|
+
nodes.push({
|
|
631
|
+
id: `job:${q}:active:${job.id}`,
|
|
632
|
+
parentId: `queue:${q}:active`,
|
|
633
|
+
type: "job",
|
|
634
|
+
label: `${pc.cyan("●")} ${pc.dim(job.id.slice(0, 12))}`,
|
|
635
|
+
depth: 3,
|
|
636
|
+
expandable: false,
|
|
637
|
+
ref: { queue: q, jobId: job.id, status: "active", jobData: job },
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
// Dead jobs subcategory
|
|
642
|
+
const deadOpen = expandedSet.has(`queue:${q}:dead`);
|
|
643
|
+
nodes.push({
|
|
644
|
+
id: `queue:${q}:dead`,
|
|
645
|
+
parentId: `queue:${q}`,
|
|
646
|
+
type: "category",
|
|
647
|
+
label: `${deadOpen ? pc.cyan("▾") : pc.dim("▸")} ${pc.bold("Dead")} ${pc.dim(`(${deadJobs.length})`)}`,
|
|
648
|
+
depth: 2,
|
|
649
|
+
expandable: true,
|
|
650
|
+
});
|
|
651
|
+
if (deadOpen) {
|
|
652
|
+
if (deadJobs.length === 0) {
|
|
653
|
+
nodes.push({
|
|
654
|
+
id: `empty:dead:${q}`,
|
|
655
|
+
parentId: `queue:${q}:dead`,
|
|
656
|
+
type: "status",
|
|
657
|
+
label: pc.dim("(no dead jobs)"),
|
|
658
|
+
depth: 3,
|
|
659
|
+
expandable: false,
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
for (const job of deadJobs) {
|
|
663
|
+
nodes.push({
|
|
664
|
+
id: `job:${q}:dead:${job.id}`,
|
|
665
|
+
parentId: `queue:${q}:dead`,
|
|
666
|
+
type: "job",
|
|
667
|
+
label: `${pc.red("○")} ${pc.dim(job.id.slice(0, 12))}`,
|
|
668
|
+
depth: 3,
|
|
669
|
+
expandable: false,
|
|
670
|
+
ref: { queue: q, jobId: job.id, status: "dead", jobData: job },
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
553
675
|
}
|
|
554
676
|
}
|
|
555
677
|
// Links
|
|
@@ -647,43 +769,59 @@ async function loadContent(node) {
|
|
|
647
769
|
}
|
|
648
770
|
else if (node.type === "stream" && node.ref) {
|
|
649
771
|
const ref = node.ref;
|
|
650
|
-
const
|
|
651
|
-
let res = `${pc.bold(ref.name)} ${pc.dim(`(${
|
|
652
|
-
|
|
653
|
-
|
|
772
|
+
const evts = eventsByStream.get(ref.name) ?? [];
|
|
773
|
+
let res = `${pc.bold(ref.name)} ${pc.dim(`(${evts.length} events shown)`)}\n\n`;
|
|
774
|
+
res += pc.dim("Expand to browse individual events, or press [c] to append.\n");
|
|
775
|
+
content = res;
|
|
776
|
+
}
|
|
777
|
+
else if (node.type === "event" && node.ref) {
|
|
778
|
+
const ref = node.ref;
|
|
779
|
+
const evt = ref.eventData;
|
|
780
|
+
let res = `${pc.bold(evt.type || "unknown")} ${pc.dim(evt.id)}\n`;
|
|
781
|
+
res += ` ${pc.dim("Stream:")} ${pc.green(ref.stream)}\n`;
|
|
782
|
+
res += ` ${pc.dim("Created:")} ${pc.dim(evt.createdAt || "—")}\n\n`;
|
|
783
|
+
const display = { ...evt };
|
|
784
|
+
for (const k of ["id", "stream", "sequence", "createdAt", "idempotencyKey"]) {
|
|
785
|
+
delete display[k];
|
|
786
|
+
}
|
|
787
|
+
if (Object.keys(display).length > 0) {
|
|
788
|
+
res += highlightJson(display);
|
|
654
789
|
}
|
|
655
790
|
else {
|
|
656
|
-
|
|
657
|
-
const ts = e.createdAt ? pc.dim(String(e.createdAt)) : "";
|
|
658
|
-
const type = pc.magenta(e.type || "unknown");
|
|
659
|
-
return ` ${ts} ${type}`;
|
|
660
|
-
});
|
|
661
|
-
res += lines.join("\n");
|
|
791
|
+
res += pc.dim("No payload.");
|
|
662
792
|
}
|
|
663
793
|
content = res;
|
|
664
794
|
}
|
|
665
795
|
else if (node.type === "queue" && node.ref) {
|
|
666
796
|
const ref = node.ref;
|
|
667
|
-
const
|
|
668
|
-
const
|
|
797
|
+
const jobData = jobsByQueue.get(ref.name);
|
|
798
|
+
const active = jobData?.active ?? [];
|
|
799
|
+
const dead = jobData?.dead ?? [];
|
|
669
800
|
let res = `${pc.bold(ref.name)}\n\n`;
|
|
670
801
|
res += `${pc.cyan("Active")} ${pc.dim(`(${active.length})`)}\n`;
|
|
671
|
-
|
|
672
|
-
|
|
802
|
+
res += `${pc.red("Dead")} ${pc.dim(`(${dead.length})`)}\n\n`;
|
|
803
|
+
res += pc.dim("Expand Active or Dead to browse jobs, [c] to push new job.");
|
|
804
|
+
content = res;
|
|
805
|
+
}
|
|
806
|
+
else if (node.type === "job" && node.ref) {
|
|
807
|
+
const ref = node.ref;
|
|
808
|
+
const job = ref.jobData;
|
|
809
|
+
let res = `${pc.bold(job.id)} ${pc.yellow(job.status || ref.status)}\n`;
|
|
810
|
+
res += ` ${pc.dim("Queue:")} ${pc.magenta(ref.queue)}\n`;
|
|
811
|
+
res += ` ${pc.dim("Attempts:")} ${job.attempts}/${job.maxAttempts}\n`;
|
|
812
|
+
res += ` ${pc.dim("Created:")} ${pc.dim(job.createdAt || "—")}\n`;
|
|
813
|
+
if (job.lastError) {
|
|
814
|
+
res += ` ${pc.dim("Error:")} ${pc.red(job.lastError)}\n`;
|
|
815
|
+
}
|
|
816
|
+
res += "\n";
|
|
817
|
+
if (job.payload && Object.keys(job.payload).length > 0) {
|
|
818
|
+
res += highlightJson(job.payload);
|
|
673
819
|
}
|
|
674
820
|
else {
|
|
675
|
-
|
|
676
|
-
res += ` ${pc.cyan("●")} ${j.id} ${pc.yellow(j.status)} ${pc.dim(`${j.attempts}/${j.maxAttempts}`)}\n`;
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
res += `${pc.red("Dead")} ${pc.dim(`(${dead.length})`)}\n`;
|
|
680
|
-
if (dead.length === 0) {
|
|
681
|
-
res += pc.dim(" No dead jobs\n");
|
|
821
|
+
res += pc.dim("No payload.");
|
|
682
822
|
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
res += ` ${pc.red("○")} ${j.id} ${pc.dim(`${j.attempts}/${j.maxAttempts}`)}\n`;
|
|
686
|
-
}
|
|
823
|
+
if (ref.status === "dead") {
|
|
824
|
+
res += `\n\n${pc.dim("[e] Retry (ack) [d] Nack (remove from dead letter)")}`;
|
|
687
825
|
}
|
|
688
826
|
content = res;
|
|
689
827
|
}
|
|
@@ -1220,6 +1358,41 @@ async function handleEdit(selected) {
|
|
|
1220
1358
|
}
|
|
1221
1359
|
});
|
|
1222
1360
|
}
|
|
1361
|
+
else if (selected.type === "job" && selected.ref) {
|
|
1362
|
+
const ref = selected.ref;
|
|
1363
|
+
if (ref.status === "dead") {
|
|
1364
|
+
openForm(`Retry Dead Job: ${ref.jobId.slice(0, 12)}`, [
|
|
1365
|
+
{
|
|
1366
|
+
id: "action",
|
|
1367
|
+
label: "Action",
|
|
1368
|
+
value: "ack",
|
|
1369
|
+
options: ["ack", "nack"],
|
|
1370
|
+
},
|
|
1371
|
+
{
|
|
1372
|
+
id: "error",
|
|
1373
|
+
label: "Error message (for nack)",
|
|
1374
|
+
placeholder: "Optional error",
|
|
1375
|
+
},
|
|
1376
|
+
], async (vals) => {
|
|
1377
|
+
const action = vals.action || "";
|
|
1378
|
+
if (action === "ack") {
|
|
1379
|
+
await db.queue(ref.queue).ack(ref.jobId);
|
|
1380
|
+
}
|
|
1381
|
+
else if (action === "nack") {
|
|
1382
|
+
await db.queue(ref.queue).nack(ref.jobId, { error: vals.error || "Rejected" });
|
|
1383
|
+
}
|
|
1384
|
+
else {
|
|
1385
|
+
throw new Error("Action must be 'ack' or 'nack'.");
|
|
1386
|
+
}
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
else {
|
|
1390
|
+
// Active job — nack to fail it back to ready
|
|
1391
|
+
openForm(`Nack Job: ${ref.jobId.slice(0, 12)}`, [{ id: "error", label: "Error message", placeholder: "Optional" }], async (vals) => {
|
|
1392
|
+
await db.queue(ref.queue).nack(ref.jobId, { error: vals.error || "Nacked" });
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1223
1396
|
else {
|
|
1224
1397
|
openForm("Edit Not Supported", [{ id: "msg", label: "Error", value: "Editing is only available for Objects and Queues." }], async () => { });
|
|
1225
1398
|
}
|
|
@@ -1284,6 +1457,33 @@ async function handleDelete(selected) {
|
|
|
1284
1457
|
}
|
|
1285
1458
|
});
|
|
1286
1459
|
}
|
|
1460
|
+
else if (selected.type === "job" && selected.ref) {
|
|
1461
|
+
const ref = selected.ref;
|
|
1462
|
+
if (ref.status === "dead") {
|
|
1463
|
+
openForm(`Remove Dead Job: ${ref.jobId.slice(0, 12)}`, [
|
|
1464
|
+
{
|
|
1465
|
+
id: "action",
|
|
1466
|
+
label: "Action",
|
|
1467
|
+
value: "nack",
|
|
1468
|
+
options: ["ack", "nack"],
|
|
1469
|
+
},
|
|
1470
|
+
{ id: "confirm", label: 'Type "yes" to confirm', placeholder: "yes" },
|
|
1471
|
+
], async (vals) => {
|
|
1472
|
+
if ((vals.confirm || "").toLowerCase() !== "yes") {
|
|
1473
|
+
throw new Error("Canceled");
|
|
1474
|
+
}
|
|
1475
|
+
if (vals.action === "ack") {
|
|
1476
|
+
await db.queue(ref.queue).ack(ref.jobId);
|
|
1477
|
+
}
|
|
1478
|
+
else {
|
|
1479
|
+
await db.queue(ref.queue).nack(ref.jobId, { error: "Removed from dead letter" });
|
|
1480
|
+
}
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
else {
|
|
1484
|
+
openForm("Delete Not For Active Jobs", [{ id: "msg", label: "Info", value: "Use [e] to nack an active job back to ready state." }], async () => { });
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1287
1487
|
else {
|
|
1288
1488
|
openForm("Delete Not Supported", [
|
|
1289
1489
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thingd/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.55.0",
|
|
4
4
|
"description": "CLI, Interactive TUI Dashboard, and MCP server for thingd — a fast object-first data engine for applications and AI agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://engine.thingd.cloud",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"cli-table3": "^0.6.5",
|
|
46
46
|
"picocolors": "^1.1.1",
|
|
47
47
|
"zod": "^4.4.3",
|
|
48
|
-
"@thingd/sdk": "0.
|
|
48
|
+
"@thingd/sdk": "0.55.0"
|
|
49
49
|
},
|
|
50
50
|
"engines": {
|
|
51
51
|
"node": ">=24.0.0"
|