@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.
@@ -1 +1 @@
1
- {"version":3,"file":"interactive.d.ts","sourceRoot":"","sources":["../src/interactive.ts"],"names":[],"mappings":"AAyvEA,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CA0FvD"}
1
+ {"version":3,"file":"interactive.d.ts","sourceRoot":"","sources":["../src/interactive.ts"],"names":[],"mappings":"AA+/EA,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CA0FvD"}
@@ -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.green("")} ${pc.green(stream)}`,
543
+ label: `${sOpen ? pc.cyan("") : pc.dim("▸")} ${pc.green(stream)}`,
518
544
  depth: 1,
519
- expandable: false,
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.magenta("")} ${pc.magenta(q)}`,
599
+ label: `${qOpen ? pc.cyan("") : pc.dim("▸")} ${pc.magenta(q)}`,
549
600
  depth: 1,
550
- expandable: false,
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 events = await db.events.list(ref.name);
651
- let res = `${pc.bold(ref.name)} ${pc.dim(`(${events.length} events)`)}\n\n`;
652
- if (events.length === 0) {
653
- res += pc.dim("No events in this stream.");
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
- const lines = events.map((e) => {
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 queue = db.queue(ref.name);
668
- const [active, dead] = await Promise.all([queue.list(), queue.dead()]);
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
- if (active.length === 0) {
672
- res += pc.dim(" No jobs\n");
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
- for (const j of active) {
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
- else {
684
- for (const j of dead) {
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.54.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.54.0"
48
+ "@thingd/sdk": "0.55.0"
49
49
  },
50
50
  "engines": {
51
51
  "node": ">=24.0.0"