@thingd/cli 0.53.2 → 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 +384 -38
- 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 = [];
|
|
@@ -89,6 +91,7 @@ let objectWriteRateHistory = [];
|
|
|
89
91
|
let eventAppendRateHistory = [];
|
|
90
92
|
let viewerLines = ["Select an item to view details."];
|
|
91
93
|
let viewerScroll = 0;
|
|
94
|
+
let lastNeighborsRef = "";
|
|
92
95
|
let loadedItemId = "";
|
|
93
96
|
let loadTimer = null;
|
|
94
97
|
let pollTimer = null;
|
|
@@ -293,6 +296,29 @@ async function fetchResources() {
|
|
|
293
296
|
objectsByCollection.set(col, []);
|
|
294
297
|
}
|
|
295
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
|
+
}));
|
|
296
322
|
// Calculate Deltas for Operations Throughput Rates
|
|
297
323
|
const prevObjects = objectsHistory.length > 0
|
|
298
324
|
? (objectsHistory[objectsHistory.length - 1] ?? totalObjects)
|
|
@@ -509,15 +535,40 @@ function buildTree() {
|
|
|
509
535
|
});
|
|
510
536
|
}
|
|
511
537
|
for (const stream of streams) {
|
|
538
|
+
const sOpen = expandedSet.has(`stream:${stream}`);
|
|
512
539
|
nodes.push({
|
|
513
540
|
id: `stream:${stream}`,
|
|
514
541
|
parentId: "cat:streams",
|
|
515
542
|
type: "stream",
|
|
516
|
-
label: `${pc.
|
|
543
|
+
label: `${sOpen ? pc.cyan("▾") : pc.dim("▸")} ${pc.green(stream)}`,
|
|
517
544
|
depth: 1,
|
|
518
|
-
expandable:
|
|
545
|
+
expandable: true,
|
|
519
546
|
ref: { name: stream },
|
|
520
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
|
+
}
|
|
521
572
|
}
|
|
522
573
|
}
|
|
523
574
|
// Queues
|
|
@@ -540,17 +591,97 @@ function buildTree() {
|
|
|
540
591
|
});
|
|
541
592
|
}
|
|
542
593
|
for (const q of queues) {
|
|
594
|
+
const qOpen = expandedSet.has(`queue:${q}`);
|
|
543
595
|
nodes.push({
|
|
544
596
|
id: `queue:${q}`,
|
|
545
597
|
parentId: "cat:queues",
|
|
546
598
|
type: "queue",
|
|
547
|
-
label: `${pc.
|
|
599
|
+
label: `${qOpen ? pc.cyan("▾") : pc.dim("▸")} ${pc.magenta(q)}`,
|
|
548
600
|
depth: 1,
|
|
549
|
-
expandable:
|
|
601
|
+
expandable: true,
|
|
550
602
|
ref: { name: q },
|
|
551
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
|
+
}
|
|
552
675
|
}
|
|
553
676
|
}
|
|
677
|
+
// Links
|
|
678
|
+
nodes.push({
|
|
679
|
+
id: "node:links",
|
|
680
|
+
type: "link",
|
|
681
|
+
label: `${pc.blue("◈")} ${pc.dim("Links")} ${pc.dim(`(${totalLinksCount})`)}`,
|
|
682
|
+
depth: 0,
|
|
683
|
+
expandable: false,
|
|
684
|
+
});
|
|
554
685
|
// Metrics
|
|
555
686
|
nodes.push({
|
|
556
687
|
id: "node:status",
|
|
@@ -638,43 +769,59 @@ async function loadContent(node) {
|
|
|
638
769
|
}
|
|
639
770
|
else if (node.type === "stream" && node.ref) {
|
|
640
771
|
const ref = node.ref;
|
|
641
|
-
const
|
|
642
|
-
let res = `${pc.bold(ref.name)} ${pc.dim(`(${
|
|
643
|
-
|
|
644
|
-
|
|
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);
|
|
645
789
|
}
|
|
646
790
|
else {
|
|
647
|
-
|
|
648
|
-
const ts = e.createdAt ? pc.dim(String(e.createdAt)) : "";
|
|
649
|
-
const type = pc.magenta(e.type || "unknown");
|
|
650
|
-
return ` ${ts} ${type}`;
|
|
651
|
-
});
|
|
652
|
-
res += lines.join("\n");
|
|
791
|
+
res += pc.dim("No payload.");
|
|
653
792
|
}
|
|
654
793
|
content = res;
|
|
655
794
|
}
|
|
656
795
|
else if (node.type === "queue" && node.ref) {
|
|
657
796
|
const ref = node.ref;
|
|
658
|
-
const
|
|
659
|
-
const
|
|
797
|
+
const jobData = jobsByQueue.get(ref.name);
|
|
798
|
+
const active = jobData?.active ?? [];
|
|
799
|
+
const dead = jobData?.dead ?? [];
|
|
660
800
|
let res = `${pc.bold(ref.name)}\n\n`;
|
|
661
801
|
res += `${pc.cyan("Active")} ${pc.dim(`(${active.length})`)}\n`;
|
|
662
|
-
|
|
663
|
-
|
|
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);
|
|
664
819
|
}
|
|
665
820
|
else {
|
|
666
|
-
|
|
667
|
-
res += ` ${pc.cyan("●")} ${j.id} ${pc.yellow(j.status)} ${pc.dim(`${j.attempts}/${j.maxAttempts}`)}\n`;
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
res += `${pc.red("Dead")} ${pc.dim(`(${dead.length})`)}\n`;
|
|
671
|
-
if (dead.length === 0) {
|
|
672
|
-
res += pc.dim(" No dead jobs\n");
|
|
821
|
+
res += pc.dim("No payload.");
|
|
673
822
|
}
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
res += ` ${pc.red("○")} ${j.id} ${pc.dim(`${j.attempts}/${j.maxAttempts}`)}\n`;
|
|
677
|
-
}
|
|
823
|
+
if (ref.status === "dead") {
|
|
824
|
+
res += `\n\n${pc.dim("[e] Retry (ack) [d] Nack (remove from dead letter)")}`;
|
|
678
825
|
}
|
|
679
826
|
content = res;
|
|
680
827
|
}
|
|
@@ -742,6 +889,19 @@ async function loadContent(node) {
|
|
|
742
889
|
content += `\n ${pc.yellow("⚠")} ${pc.dim(cloudError)}\n`;
|
|
743
890
|
}
|
|
744
891
|
}
|
|
892
|
+
else if (node.type === "link") {
|
|
893
|
+
content = [
|
|
894
|
+
` ${pc.bold("Links")} ${pc.dim(`(${totalLinksCount} total`)}${lastNeighborsRef ? pc.dim(`, last browsed: ${lastNeighborsRef}`) : ""})`,
|
|
895
|
+
"",
|
|
896
|
+
totalLinksCount === 0
|
|
897
|
+
? ` ${pc.dim("No links yet.")}`
|
|
898
|
+
: ` ${pc.dim("Select an object and press")} ${pc.bold("n")} ${pc.dim("to browse its neighbors.")}`,
|
|
899
|
+
"",
|
|
900
|
+
` ${pc.bold("Operations")}`,
|
|
901
|
+
` ${pc.bold("[c]")} Create a new link`,
|
|
902
|
+
` ${pc.bold("[d]")} Delete a link by ID`,
|
|
903
|
+
].join("\n");
|
|
904
|
+
}
|
|
745
905
|
else if (node.type === "category") {
|
|
746
906
|
content = pc.dim("Expand to browse items.");
|
|
747
907
|
}
|
|
@@ -881,7 +1041,7 @@ function draw() {
|
|
|
881
1041
|
help = ` ${pc.dim("↑↓")} nav ${pc.dim("enter")} connect ${pc.dim("q")} quit `;
|
|
882
1042
|
}
|
|
883
1043
|
else {
|
|
884
|
-
help = ` ${pc.dim("↑↓")} nav ${pc.dim("←→")} toggle ${pc.dim("c")} create ${pc.dim("e")} edit ${pc.dim("d")} delete ${pc.dim("/")} search ${pc.dim("i")} info ${pc.dim("r")} refresh ${pc.dim("s")} switch ${pc.dim("l")} logout ${pc.dim("q")} quit `;
|
|
1044
|
+
help = ` ${pc.dim("↑↓")} nav ${pc.dim("←→")} toggle ${pc.dim("c")} create ${pc.dim("e")} edit ${pc.dim("d")} delete ${pc.dim("/")} search ${pc.dim("n")} neighbors ${pc.dim("i")} info ${pc.dim("r")} refresh ${pc.dim("s")} switch ${pc.dim("l")} logout ${pc.dim("q")} quit `;
|
|
885
1045
|
}
|
|
886
1046
|
buf += `${pc.dim("─".repeat(W))}\n`;
|
|
887
1047
|
buf += padToWidth(help, W);
|
|
@@ -1058,23 +1218,33 @@ async function handleCreate(selected) {
|
|
|
1058
1218
|
openForm("Create Resource", [
|
|
1059
1219
|
{
|
|
1060
1220
|
id: "kind",
|
|
1061
|
-
label: "Kind (object, event, queue)",
|
|
1062
|
-
value: defaultStream
|
|
1063
|
-
|
|
1221
|
+
label: "Kind (object, event, queue, link)",
|
|
1222
|
+
value: defaultStream
|
|
1223
|
+
? "event"
|
|
1224
|
+
: defaultQueue
|
|
1225
|
+
? "queue"
|
|
1226
|
+
: selected?.type === "link"
|
|
1227
|
+
? "link"
|
|
1228
|
+
: "object",
|
|
1229
|
+
options: ["object", "event", "queue", "link"],
|
|
1064
1230
|
},
|
|
1065
1231
|
{
|
|
1066
1232
|
id: "target",
|
|
1067
|
-
label: "Target (Collection, Stream, or
|
|
1233
|
+
label: "Target (Collection, Stream, Queue, or From Reference)",
|
|
1068
1234
|
value: defaultCol || defaultStream || defaultQueue,
|
|
1069
1235
|
options: Array.from(new Set([...collections, ...streams, ...queues])).sort(),
|
|
1070
1236
|
allowCustom: true,
|
|
1071
1237
|
},
|
|
1072
1238
|
{
|
|
1073
1239
|
id: "objId",
|
|
1074
|
-
label: "Object
|
|
1240
|
+
label: "Object / To Reference ID (auto if blank for objects)",
|
|
1075
1241
|
placeholder: "Leave blank to auto-generate",
|
|
1076
1242
|
},
|
|
1077
|
-
{
|
|
1243
|
+
{
|
|
1244
|
+
id: "payload",
|
|
1245
|
+
label: "Data, Link Type, or JSON Fields",
|
|
1246
|
+
placeholder: 'e.g. name="John" age=30 or {"linkType":"follows","weight":1}',
|
|
1247
|
+
},
|
|
1078
1248
|
], async (vals) => {
|
|
1079
1249
|
const kind = (vals.kind || "").toLowerCase();
|
|
1080
1250
|
const target = (vals.target || "").trim();
|
|
@@ -1111,8 +1281,34 @@ async function handleCreate(selected) {
|
|
|
1111
1281
|
await db.queue(target).push(data);
|
|
1112
1282
|
expandedSet.add("cat:queues");
|
|
1113
1283
|
}
|
|
1284
|
+
else if (kind === "link") {
|
|
1285
|
+
const toRef = (vals.objId || "").trim();
|
|
1286
|
+
if (!toRef) {
|
|
1287
|
+
throw new Error("To Reference is required (use Object ID field).");
|
|
1288
|
+
}
|
|
1289
|
+
let linkType = "related";
|
|
1290
|
+
let weight;
|
|
1291
|
+
let metadataJson;
|
|
1292
|
+
try {
|
|
1293
|
+
const parsed = JSON.parse(vals.payload || "{}");
|
|
1294
|
+
linkType = parsed.linkType || parsed.link_type || "related";
|
|
1295
|
+
if (parsed.weight !== undefined) {
|
|
1296
|
+
weight = Number(parsed.weight);
|
|
1297
|
+
}
|
|
1298
|
+
if (parsed.metadata || parsed.metadataJson) {
|
|
1299
|
+
metadataJson =
|
|
1300
|
+
typeof parsed.metadata === "string"
|
|
1301
|
+
? parsed.metadata
|
|
1302
|
+
: JSON.stringify(parsed.metadata);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
catch {
|
|
1306
|
+
linkType = (vals.payload || "").trim() || "related";
|
|
1307
|
+
}
|
|
1308
|
+
await db.links.create(target, linkType, toRef, weight, metadataJson);
|
|
1309
|
+
}
|
|
1114
1310
|
else {
|
|
1115
|
-
throw new Error("Kind must be 'object', 'event', or '
|
|
1311
|
+
throw new Error("Kind must be 'object', 'event', 'queue', or 'link'.");
|
|
1116
1312
|
}
|
|
1117
1313
|
});
|
|
1118
1314
|
}
|
|
@@ -1162,6 +1358,41 @@ async function handleEdit(selected) {
|
|
|
1162
1358
|
}
|
|
1163
1359
|
});
|
|
1164
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
|
+
}
|
|
1165
1396
|
else {
|
|
1166
1397
|
openForm("Edit Not Supported", [{ id: "msg", label: "Error", value: "Editing is only available for Objects and Queues." }], async () => { });
|
|
1167
1398
|
}
|
|
@@ -1204,8 +1435,63 @@ async function handleDelete(selected) {
|
|
|
1204
1435
|
}
|
|
1205
1436
|
});
|
|
1206
1437
|
}
|
|
1438
|
+
else if (selected.type === "link" || loadedItemId === "neighbors_result") {
|
|
1439
|
+
openForm("Delete Link", [
|
|
1440
|
+
{
|
|
1441
|
+
id: "linkId",
|
|
1442
|
+
label: "Link ID",
|
|
1443
|
+
placeholder: "Paste the link ID from the neighbors view",
|
|
1444
|
+
},
|
|
1445
|
+
{ id: "confirm", label: 'Type "yes" to confirm deletion', placeholder: "yes" },
|
|
1446
|
+
], async (vals) => {
|
|
1447
|
+
const linkId = (vals.linkId || "").trim();
|
|
1448
|
+
if (!linkId) {
|
|
1449
|
+
throw new Error("Link ID is required.");
|
|
1450
|
+
}
|
|
1451
|
+
if ((vals.confirm || "").toLowerCase() !== "yes") {
|
|
1452
|
+
throw new Error("Canceled");
|
|
1453
|
+
}
|
|
1454
|
+
const ok = await db.links.delete(linkId);
|
|
1455
|
+
if (!ok) {
|
|
1456
|
+
throw new Error(`Link '${linkId}' not found.`);
|
|
1457
|
+
}
|
|
1458
|
+
});
|
|
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
|
+
}
|
|
1207
1487
|
else {
|
|
1208
|
-
openForm("Delete Not Supported", [
|
|
1488
|
+
openForm("Delete Not Supported", [
|
|
1489
|
+
{
|
|
1490
|
+
id: "msg",
|
|
1491
|
+
label: "Error",
|
|
1492
|
+
value: "Deletion is only available for Objects, Links, and Queues.",
|
|
1493
|
+
},
|
|
1494
|
+
], async () => { });
|
|
1209
1495
|
}
|
|
1210
1496
|
}
|
|
1211
1497
|
async function handleSearch() {
|
|
@@ -1318,6 +1604,63 @@ async function handleInfo() {
|
|
|
1318
1604
|
viewerLines = lines;
|
|
1319
1605
|
loadedItemId = "info_status";
|
|
1320
1606
|
}
|
|
1607
|
+
async function handleNeighbors(selected) {
|
|
1608
|
+
if (selected?.type !== "object" || !selected.ref) {
|
|
1609
|
+
viewerLines = [
|
|
1610
|
+
` ${pc.yellow("Neighbors")}`,
|
|
1611
|
+
"",
|
|
1612
|
+
` ${pc.dim("Select an object first, then press")} ${pc.bold("n")} ${pc.dim("to browse its links.")}`,
|
|
1613
|
+
];
|
|
1614
|
+
loadedItemId = "neighbors_info";
|
|
1615
|
+
draw();
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
const ref = selected.ref;
|
|
1619
|
+
const fromRef = `${ref.collection}/${ref.id}`;
|
|
1620
|
+
openForm(`Neighbors of ${fromRef}`, [
|
|
1621
|
+
{
|
|
1622
|
+
id: "direction",
|
|
1623
|
+
label: "Direction",
|
|
1624
|
+
value: "Both",
|
|
1625
|
+
options: ["Both", "Outgoing", "Incoming"],
|
|
1626
|
+
},
|
|
1627
|
+
{
|
|
1628
|
+
id: "linkType",
|
|
1629
|
+
label: "Link Type (optional, leave blank for all)",
|
|
1630
|
+
placeholder: "e.g. follows, owns",
|
|
1631
|
+
},
|
|
1632
|
+
{
|
|
1633
|
+
id: "limit",
|
|
1634
|
+
label: "Max Results (optional)",
|
|
1635
|
+
placeholder: "50",
|
|
1636
|
+
},
|
|
1637
|
+
], async (vals) => {
|
|
1638
|
+
const direction = (vals.direction || "Both");
|
|
1639
|
+
const linkType = (vals.linkType || "").trim() || undefined;
|
|
1640
|
+
const limitStr = vals.limit || "";
|
|
1641
|
+
const limit = limitStr ? parseInt(limitStr, 10) || undefined : undefined;
|
|
1642
|
+
const links = await db.links.neighbors(fromRef, direction, { linkType, limit });
|
|
1643
|
+
lastNeighborsRef = fromRef;
|
|
1644
|
+
viewerLines = [
|
|
1645
|
+
` ${pc.bold("Neighbors of")} ${pc.cyan(fromRef)}`,
|
|
1646
|
+
` ${pc.dim(`(${links.length} link${links.length !== 1 ? "s" : ""}${direction !== "Both" ? `, ${direction.toLowerCase()}` : ""}${linkType ? `, type: ${linkType}` : ""})`)}`,
|
|
1647
|
+
"",
|
|
1648
|
+
...(links.length === 0 ? [` ${pc.dim("No links found.")}`] : []),
|
|
1649
|
+
...links.flatMap((link) => [
|
|
1650
|
+
` ${pc.blue("◈")} ${pc.dim(link.id)}`,
|
|
1651
|
+
` ${link.fromRef} ${pc.cyan(link.linkType)} ${link.toRef}`,
|
|
1652
|
+
link.weight !== undefined ? ` ${pc.dim(`weight: ${link.weight}`)}` : "",
|
|
1653
|
+
link.metadataJson && link.metadataJson !== "{}"
|
|
1654
|
+
? ` ${pc.dim(`metadata: ${link.metadataJson}`)}`
|
|
1655
|
+
: "",
|
|
1656
|
+
"",
|
|
1657
|
+
]),
|
|
1658
|
+
pc.dim("[d] Delete a link by ID [c] Create a new link"),
|
|
1659
|
+
];
|
|
1660
|
+
loadedItemId = "neighbors_result";
|
|
1661
|
+
draw();
|
|
1662
|
+
});
|
|
1663
|
+
}
|
|
1321
1664
|
async function handleMaintenance() {
|
|
1322
1665
|
// Cycle through maintenance operations with each press of 'm'
|
|
1323
1666
|
const operations = ["health", "checkpoint", "backup"];
|
|
@@ -1613,6 +1956,9 @@ function setupKeypress() {
|
|
|
1613
1956
|
else if (str === "i" || str === "I") {
|
|
1614
1957
|
await handleInfo();
|
|
1615
1958
|
}
|
|
1959
|
+
else if (str === "n" || str === "N") {
|
|
1960
|
+
await handleNeighbors(tree[cursorIndex]);
|
|
1961
|
+
}
|
|
1616
1962
|
else if (str === "m" || str === "M") {
|
|
1617
1963
|
await handleMaintenance();
|
|
1618
1964
|
}
|
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"
|