@thingd/cli 0.53.2 → 0.54.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":"AAylEA,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CA0FvD"}
1
+ {"version":3,"file":"interactive.d.ts","sourceRoot":"","sources":["../src/interactive.ts"],"names":[],"mappings":"AAyvEA,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CA0FvD"}
@@ -89,6 +89,7 @@ let objectWriteRateHistory = [];
89
89
  let eventAppendRateHistory = [];
90
90
  let viewerLines = ["Select an item to view details."];
91
91
  let viewerScroll = 0;
92
+ let lastNeighborsRef = "";
92
93
  let loadedItemId = "";
93
94
  let loadTimer = null;
94
95
  let pollTimer = null;
@@ -551,6 +552,14 @@ function buildTree() {
551
552
  });
552
553
  }
553
554
  }
555
+ // Links
556
+ nodes.push({
557
+ id: "node:links",
558
+ type: "link",
559
+ label: `${pc.blue("◈")} ${pc.dim("Links")} ${pc.dim(`(${totalLinksCount})`)}`,
560
+ depth: 0,
561
+ expandable: false,
562
+ });
554
563
  // Metrics
555
564
  nodes.push({
556
565
  id: "node:status",
@@ -742,6 +751,19 @@ async function loadContent(node) {
742
751
  content += `\n ${pc.yellow("⚠")} ${pc.dim(cloudError)}\n`;
743
752
  }
744
753
  }
754
+ else if (node.type === "link") {
755
+ content = [
756
+ ` ${pc.bold("Links")} ${pc.dim(`(${totalLinksCount} total`)}${lastNeighborsRef ? pc.dim(`, last browsed: ${lastNeighborsRef}`) : ""})`,
757
+ "",
758
+ totalLinksCount === 0
759
+ ? ` ${pc.dim("No links yet.")}`
760
+ : ` ${pc.dim("Select an object and press")} ${pc.bold("n")} ${pc.dim("to browse its neighbors.")}`,
761
+ "",
762
+ ` ${pc.bold("Operations")}`,
763
+ ` ${pc.bold("[c]")} Create a new link`,
764
+ ` ${pc.bold("[d]")} Delete a link by ID`,
765
+ ].join("\n");
766
+ }
745
767
  else if (node.type === "category") {
746
768
  content = pc.dim("Expand to browse items.");
747
769
  }
@@ -881,7 +903,7 @@ function draw() {
881
903
  help = ` ${pc.dim("↑↓")} nav ${pc.dim("enter")} connect ${pc.dim("q")} quit `;
882
904
  }
883
905
  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 `;
906
+ 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
907
  }
886
908
  buf += `${pc.dim("─".repeat(W))}\n`;
887
909
  buf += padToWidth(help, W);
@@ -1058,23 +1080,33 @@ async function handleCreate(selected) {
1058
1080
  openForm("Create Resource", [
1059
1081
  {
1060
1082
  id: "kind",
1061
- label: "Kind (object, event, queue)",
1062
- value: defaultStream ? "event" : defaultQueue ? "queue" : "object",
1063
- options: ["object", "event", "queue"],
1083
+ label: "Kind (object, event, queue, link)",
1084
+ value: defaultStream
1085
+ ? "event"
1086
+ : defaultQueue
1087
+ ? "queue"
1088
+ : selected?.type === "link"
1089
+ ? "link"
1090
+ : "object",
1091
+ options: ["object", "event", "queue", "link"],
1064
1092
  },
1065
1093
  {
1066
1094
  id: "target",
1067
- label: "Target (Collection, Stream, or Queue Name)",
1095
+ label: "Target (Collection, Stream, Queue, or From Reference)",
1068
1096
  value: defaultCol || defaultStream || defaultQueue,
1069
1097
  options: Array.from(new Set([...collections, ...streams, ...queues])).sort(),
1070
1098
  allowCustom: true,
1071
1099
  },
1072
1100
  {
1073
1101
  id: "objId",
1074
- label: "Object ID (only for objects, auto if blank)",
1102
+ label: "Object / To Reference ID (auto if blank for objects)",
1075
1103
  placeholder: "Leave blank to auto-generate",
1076
1104
  },
1077
- { id: "payload", label: "Data (JSON or key=value)", placeholder: 'e.g. name="John" age=30' },
1105
+ {
1106
+ id: "payload",
1107
+ label: "Data, Link Type, or JSON Fields",
1108
+ placeholder: 'e.g. name="John" age=30 or {"linkType":"follows","weight":1}',
1109
+ },
1078
1110
  ], async (vals) => {
1079
1111
  const kind = (vals.kind || "").toLowerCase();
1080
1112
  const target = (vals.target || "").trim();
@@ -1111,8 +1143,34 @@ async function handleCreate(selected) {
1111
1143
  await db.queue(target).push(data);
1112
1144
  expandedSet.add("cat:queues");
1113
1145
  }
1146
+ else if (kind === "link") {
1147
+ const toRef = (vals.objId || "").trim();
1148
+ if (!toRef) {
1149
+ throw new Error("To Reference is required (use Object ID field).");
1150
+ }
1151
+ let linkType = "related";
1152
+ let weight;
1153
+ let metadataJson;
1154
+ try {
1155
+ const parsed = JSON.parse(vals.payload || "{}");
1156
+ linkType = parsed.linkType || parsed.link_type || "related";
1157
+ if (parsed.weight !== undefined) {
1158
+ weight = Number(parsed.weight);
1159
+ }
1160
+ if (parsed.metadata || parsed.metadataJson) {
1161
+ metadataJson =
1162
+ typeof parsed.metadata === "string"
1163
+ ? parsed.metadata
1164
+ : JSON.stringify(parsed.metadata);
1165
+ }
1166
+ }
1167
+ catch {
1168
+ linkType = (vals.payload || "").trim() || "related";
1169
+ }
1170
+ await db.links.create(target, linkType, toRef, weight, metadataJson);
1171
+ }
1114
1172
  else {
1115
- throw new Error("Kind must be 'object', 'event', or 'queue'.");
1173
+ throw new Error("Kind must be 'object', 'event', 'queue', or 'link'.");
1116
1174
  }
1117
1175
  });
1118
1176
  }
@@ -1204,8 +1262,36 @@ async function handleDelete(selected) {
1204
1262
  }
1205
1263
  });
1206
1264
  }
1265
+ else if (selected.type === "link" || loadedItemId === "neighbors_result") {
1266
+ openForm("Delete Link", [
1267
+ {
1268
+ id: "linkId",
1269
+ label: "Link ID",
1270
+ placeholder: "Paste the link ID from the neighbors view",
1271
+ },
1272
+ { id: "confirm", label: 'Type "yes" to confirm deletion', placeholder: "yes" },
1273
+ ], async (vals) => {
1274
+ const linkId = (vals.linkId || "").trim();
1275
+ if (!linkId) {
1276
+ throw new Error("Link ID is required.");
1277
+ }
1278
+ if ((vals.confirm || "").toLowerCase() !== "yes") {
1279
+ throw new Error("Canceled");
1280
+ }
1281
+ const ok = await db.links.delete(linkId);
1282
+ if (!ok) {
1283
+ throw new Error(`Link '${linkId}' not found.`);
1284
+ }
1285
+ });
1286
+ }
1207
1287
  else {
1208
- openForm("Delete Not Supported", [{ id: "msg", label: "Error", value: "Deletion is only available for Objects and Queues." }], async () => { });
1288
+ openForm("Delete Not Supported", [
1289
+ {
1290
+ id: "msg",
1291
+ label: "Error",
1292
+ value: "Deletion is only available for Objects, Links, and Queues.",
1293
+ },
1294
+ ], async () => { });
1209
1295
  }
1210
1296
  }
1211
1297
  async function handleSearch() {
@@ -1318,6 +1404,63 @@ async function handleInfo() {
1318
1404
  viewerLines = lines;
1319
1405
  loadedItemId = "info_status";
1320
1406
  }
1407
+ async function handleNeighbors(selected) {
1408
+ if (selected?.type !== "object" || !selected.ref) {
1409
+ viewerLines = [
1410
+ ` ${pc.yellow("Neighbors")}`,
1411
+ "",
1412
+ ` ${pc.dim("Select an object first, then press")} ${pc.bold("n")} ${pc.dim("to browse its links.")}`,
1413
+ ];
1414
+ loadedItemId = "neighbors_info";
1415
+ draw();
1416
+ return;
1417
+ }
1418
+ const ref = selected.ref;
1419
+ const fromRef = `${ref.collection}/${ref.id}`;
1420
+ openForm(`Neighbors of ${fromRef}`, [
1421
+ {
1422
+ id: "direction",
1423
+ label: "Direction",
1424
+ value: "Both",
1425
+ options: ["Both", "Outgoing", "Incoming"],
1426
+ },
1427
+ {
1428
+ id: "linkType",
1429
+ label: "Link Type (optional, leave blank for all)",
1430
+ placeholder: "e.g. follows, owns",
1431
+ },
1432
+ {
1433
+ id: "limit",
1434
+ label: "Max Results (optional)",
1435
+ placeholder: "50",
1436
+ },
1437
+ ], async (vals) => {
1438
+ const direction = (vals.direction || "Both");
1439
+ const linkType = (vals.linkType || "").trim() || undefined;
1440
+ const limitStr = vals.limit || "";
1441
+ const limit = limitStr ? parseInt(limitStr, 10) || undefined : undefined;
1442
+ const links = await db.links.neighbors(fromRef, direction, { linkType, limit });
1443
+ lastNeighborsRef = fromRef;
1444
+ viewerLines = [
1445
+ ` ${pc.bold("Neighbors of")} ${pc.cyan(fromRef)}`,
1446
+ ` ${pc.dim(`(${links.length} link${links.length !== 1 ? "s" : ""}${direction !== "Both" ? `, ${direction.toLowerCase()}` : ""}${linkType ? `, type: ${linkType}` : ""})`)}`,
1447
+ "",
1448
+ ...(links.length === 0 ? [` ${pc.dim("No links found.")}`] : []),
1449
+ ...links.flatMap((link) => [
1450
+ ` ${pc.blue("◈")} ${pc.dim(link.id)}`,
1451
+ ` ${link.fromRef} ${pc.cyan(link.linkType)} ${link.toRef}`,
1452
+ link.weight !== undefined ? ` ${pc.dim(`weight: ${link.weight}`)}` : "",
1453
+ link.metadataJson && link.metadataJson !== "{}"
1454
+ ? ` ${pc.dim(`metadata: ${link.metadataJson}`)}`
1455
+ : "",
1456
+ "",
1457
+ ]),
1458
+ pc.dim("[d] Delete a link by ID [c] Create a new link"),
1459
+ ];
1460
+ loadedItemId = "neighbors_result";
1461
+ draw();
1462
+ });
1463
+ }
1321
1464
  async function handleMaintenance() {
1322
1465
  // Cycle through maintenance operations with each press of 'm'
1323
1466
  const operations = ["health", "checkpoint", "backup"];
@@ -1613,6 +1756,9 @@ function setupKeypress() {
1613
1756
  else if (str === "i" || str === "I") {
1614
1757
  await handleInfo();
1615
1758
  }
1759
+ else if (str === "n" || str === "N") {
1760
+ await handleNeighbors(tree[cursorIndex]);
1761
+ }
1616
1762
  else if (str === "m" || str === "M") {
1617
1763
  await handleMaintenance();
1618
1764
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thingd/cli",
3
- "version": "0.53.2",
3
+ "version": "0.54.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.53.2"
48
+ "@thingd/sdk": "0.54.0"
49
49
  },
50
50
  "engines": {
51
51
  "node": ">=24.0.0"