@thingd/cli 0.53.1 → 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":"AAolEA,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",
@@ -588,7 +597,12 @@ function scheduleLoad(node) {
588
597
  ? ` Ephemeral in-memory database.\n All data is destroyed on exit.\n\n ${pc.dim("Best for: testing, prototyping")}\n\n ${pc.dim("Press")} ${pc.bold("Enter")} ${pc.dim("to connect.")}`
589
598
  : d === "native"
590
599
  ? ` Persistent SQLite database.\n Data is stored on disk.\n\n ${pc.dim("Best for: local development, single-node")}\n\n ${pc.dim("Press")} ${pc.bold("Enter")} ${pc.dim("to connect.")}`
591
- : ` Connect to a remote thingd instance.\n Requires a URL and optional auth token.\n\n ${pc.dim("Best for: production, multi-node")}\n\n ${pc.dim("Press")} ${pc.bold("Enter")} ${pc.dim("to connect.")}`,
600
+ : (() => {
601
+ const hasCfg = !!readCloudConfig()?.token;
602
+ return hasCfg
603
+ ? ` Connect to a remote thingd instance.\n Requires a URL and optional auth token.\n\n ${pc.dim("Best for: production, multi-node")}\n\n ${pc.dim("Press")} ${pc.bold("Enter")} ${pc.dim("to connect.")}`
604
+ : ` ${pc.yellow("Not logged in to thingd Cloud.")}\n\n Run ${pc.cyan("thingd cloud login")} to authenticate, or\n press ${pc.bold("Enter")} to connect with a URL and token manually.`;
605
+ })(),
592
606
  ].join("\n");
593
607
  viewerLines = info.split("\n");
594
608
  loadedItemId = node.id;
@@ -737,6 +751,19 @@ async function loadContent(node) {
737
751
  content += `\n ${pc.yellow("⚠")} ${pc.dim(cloudError)}\n`;
738
752
  }
739
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
+ }
740
767
  else if (node.type === "category") {
741
768
  content = pc.dim("Expand to browse items.");
742
769
  }
@@ -876,7 +903,7 @@ function draw() {
876
903
  help = ` ${pc.dim("↑↓")} nav ${pc.dim("enter")} connect ${pc.dim("q")} quit `;
877
904
  }
878
905
  else {
879
- 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 `;
880
907
  }
881
908
  buf += `${pc.dim("─".repeat(W))}\n`;
882
909
  buf += padToWidth(help, W);
@@ -1053,23 +1080,33 @@ async function handleCreate(selected) {
1053
1080
  openForm("Create Resource", [
1054
1081
  {
1055
1082
  id: "kind",
1056
- label: "Kind (object, event, queue)",
1057
- value: defaultStream ? "event" : defaultQueue ? "queue" : "object",
1058
- 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"],
1059
1092
  },
1060
1093
  {
1061
1094
  id: "target",
1062
- label: "Target (Collection, Stream, or Queue Name)",
1095
+ label: "Target (Collection, Stream, Queue, or From Reference)",
1063
1096
  value: defaultCol || defaultStream || defaultQueue,
1064
1097
  options: Array.from(new Set([...collections, ...streams, ...queues])).sort(),
1065
1098
  allowCustom: true,
1066
1099
  },
1067
1100
  {
1068
1101
  id: "objId",
1069
- label: "Object ID (only for objects, auto if blank)",
1102
+ label: "Object / To Reference ID (auto if blank for objects)",
1070
1103
  placeholder: "Leave blank to auto-generate",
1071
1104
  },
1072
- { 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
+ },
1073
1110
  ], async (vals) => {
1074
1111
  const kind = (vals.kind || "").toLowerCase();
1075
1112
  const target = (vals.target || "").trim();
@@ -1106,8 +1143,34 @@ async function handleCreate(selected) {
1106
1143
  await db.queue(target).push(data);
1107
1144
  expandedSet.add("cat:queues");
1108
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
+ }
1109
1172
  else {
1110
- throw new Error("Kind must be 'object', 'event', or 'queue'.");
1173
+ throw new Error("Kind must be 'object', 'event', 'queue', or 'link'.");
1111
1174
  }
1112
1175
  });
1113
1176
  }
@@ -1199,8 +1262,36 @@ async function handleDelete(selected) {
1199
1262
  }
1200
1263
  });
1201
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
+ }
1202
1287
  else {
1203
- 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 () => { });
1204
1295
  }
1205
1296
  }
1206
1297
  async function handleSearch() {
@@ -1313,6 +1404,63 @@ async function handleInfo() {
1313
1404
  viewerLines = lines;
1314
1405
  loadedItemId = "info_status";
1315
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
+ }
1316
1464
  async function handleMaintenance() {
1317
1465
  // Cycle through maintenance operations with each press of 'm'
1318
1466
  const operations = ["health", "checkpoint", "backup"];
@@ -1608,6 +1756,9 @@ function setupKeypress() {
1608
1756
  else if (str === "i" || str === "I") {
1609
1757
  await handleInfo();
1610
1758
  }
1759
+ else if (str === "n" || str === "N") {
1760
+ await handleNeighbors(tree[cursorIndex]);
1761
+ }
1611
1762
  else if (str === "m" || str === "M") {
1612
1763
  await handleMaintenance();
1613
1764
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thingd/cli",
3
- "version": "0.53.1",
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.1"
48
+ "@thingd/sdk": "0.54.0"
49
49
  },
50
50
  "engines": {
51
51
  "node": ">=24.0.0"