@thingd/cli 0.55.0 → 0.57.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 +530 -9
- 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":"AA8lGA,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CA0FvD"}
|
package/dist/interactive.js
CHANGED
|
@@ -69,6 +69,7 @@ let collections = [];
|
|
|
69
69
|
let streams = [];
|
|
70
70
|
let queues = [];
|
|
71
71
|
let objectsByCollection = new Map();
|
|
72
|
+
const collectionOptions = new Map();
|
|
72
73
|
const expandedSet = new Set(["cat:collections", "cat:streams", "cat:queues"]);
|
|
73
74
|
let cursorIndex = 0;
|
|
74
75
|
let maintenanceCursor = 0;
|
|
@@ -91,6 +92,7 @@ let objectWriteRateHistory = [];
|
|
|
91
92
|
let eventAppendRateHistory = [];
|
|
92
93
|
let viewerLines = ["Select an item to view details."];
|
|
93
94
|
let viewerScroll = 0;
|
|
95
|
+
let showHelp = false;
|
|
94
96
|
let lastNeighborsRef = "";
|
|
95
97
|
let loadedItemId = "";
|
|
96
98
|
let loadTimer = null;
|
|
@@ -244,7 +246,21 @@ async function fetchResourcesFallback() {
|
|
|
244
246
|
objectsByCollection.clear();
|
|
245
247
|
await Promise.all(collections.map(async (col) => {
|
|
246
248
|
try {
|
|
247
|
-
const
|
|
249
|
+
const opts = collectionOptions.get(col);
|
|
250
|
+
const listOpts = {};
|
|
251
|
+
if (opts?.sortBy) {
|
|
252
|
+
listOpts.sortBy = { field: opts.sortBy, direction: opts.sortDir ?? "asc" };
|
|
253
|
+
}
|
|
254
|
+
if (opts?.limit) {
|
|
255
|
+
listOpts.limit = opts.limit;
|
|
256
|
+
}
|
|
257
|
+
if (opts?.offset) {
|
|
258
|
+
listOpts.offset = opts.offset;
|
|
259
|
+
}
|
|
260
|
+
if (opts?.filter) {
|
|
261
|
+
listOpts.filter = opts.filter;
|
|
262
|
+
}
|
|
263
|
+
const list = await db.listObjects(col, Object.keys(listOpts).length > 0 ? listOpts : undefined);
|
|
248
264
|
objectsByCollection.set(col, list.map((o) => o.id));
|
|
249
265
|
}
|
|
250
266
|
catch {
|
|
@@ -289,7 +305,21 @@ async function fetchResources() {
|
|
|
289
305
|
objectsByCollection.clear();
|
|
290
306
|
for (const col of collections) {
|
|
291
307
|
try {
|
|
292
|
-
const
|
|
308
|
+
const opts = collectionOptions.get(col);
|
|
309
|
+
const listOpts = {};
|
|
310
|
+
if (opts?.sortBy) {
|
|
311
|
+
listOpts.sortBy = { field: opts.sortBy, direction: opts.sortDir ?? "asc" };
|
|
312
|
+
}
|
|
313
|
+
if (opts?.limit) {
|
|
314
|
+
listOpts.limit = opts.limit;
|
|
315
|
+
}
|
|
316
|
+
if (opts?.offset) {
|
|
317
|
+
listOpts.offset = opts.offset;
|
|
318
|
+
}
|
|
319
|
+
if (opts?.filter) {
|
|
320
|
+
listOpts.filter = opts.filter;
|
|
321
|
+
}
|
|
322
|
+
const list = await db.listObjects(col, Object.keys(listOpts).length > 0 ? listOpts : undefined);
|
|
293
323
|
objectsByCollection.set(col, list.map((o) => o.id));
|
|
294
324
|
}
|
|
295
325
|
catch {
|
|
@@ -758,6 +788,27 @@ async function loadContent(node) {
|
|
|
758
788
|
const ref = node.ref;
|
|
759
789
|
const objs = objectsByCollection.get(ref.name) ?? [];
|
|
760
790
|
let res = `${pc.bold(ref.name)} ${pc.dim(`(${objs.length} objects)`)}\n\n`;
|
|
791
|
+
// Schema info
|
|
792
|
+
try {
|
|
793
|
+
const schemas = await db.schema(ref.name);
|
|
794
|
+
if (schemas.length > 0) {
|
|
795
|
+
const fields = schemas[0]?.fields ?? [];
|
|
796
|
+
if (fields.length > 0) {
|
|
797
|
+
res += `${pc.bold("Fields")}\n`;
|
|
798
|
+
for (const f of fields) {
|
|
799
|
+
const icon = f.nullable ? pc.dim("⊙") : pc.cyan("◎");
|
|
800
|
+
const sample = f.sampleValues.length > 0
|
|
801
|
+
? pc.dim(` e.g. ${String(f.sampleValues[0]).slice(0, 20)}`)
|
|
802
|
+
: "";
|
|
803
|
+
res += ` ${icon} ${f.name}: ${f.type}${sample}\n`;
|
|
804
|
+
}
|
|
805
|
+
res += "\n";
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
catch {
|
|
810
|
+
// Schema not available for this store
|
|
811
|
+
}
|
|
761
812
|
if (objs.length === 0) {
|
|
762
813
|
res += pc.dim("No objects in this collection.");
|
|
763
814
|
}
|
|
@@ -865,6 +916,38 @@ async function loadContent(node) {
|
|
|
865
916
|
content += ` ${pc.dim("Links".padEnd(14))} ${pc.blue(String(totalLinksCount).padEnd(6))} ${pc.dim("total")}\n`;
|
|
866
917
|
content += ` ${pc.dim("Active Jobs".padEnd(14))} ${pc.yellow(String(totalActiveJobsCount).padEnd(6))} ${pc.dim("in flight")}\n`;
|
|
867
918
|
content += ` ${pc.dim("Dead Jobs".padEnd(14))} ${pc.red(String(totalDeadJobsCount).padEnd(6))} ${pc.dim("failed")}\n\n`;
|
|
919
|
+
// Collection breakdown bar chart
|
|
920
|
+
if (collections.length > 0) {
|
|
921
|
+
content += ` ${pc.bold("Collections")}\n`;
|
|
922
|
+
const maxCount = Math.max(1, ...collections.map((c) => (objectsByCollection.get(c) ?? []).length));
|
|
923
|
+
const barMax = Math.max(5, viewW - 30);
|
|
924
|
+
for (const col of collections) {
|
|
925
|
+
const count = (objectsByCollection.get(col) ?? []).length;
|
|
926
|
+
const barLen = Math.max(1, Math.round((count / maxCount) * barMax));
|
|
927
|
+
const bar = barLen > 0 ? pc.cyan("█".repeat(barLen)) : "";
|
|
928
|
+
content += ` ${pc.dim(col.slice(0, 12).padEnd(12))} ${bar} ${pc.dim(String(count))}\n`;
|
|
929
|
+
}
|
|
930
|
+
content += "\n";
|
|
931
|
+
}
|
|
932
|
+
// Capacity gauge bar
|
|
933
|
+
const totalHistorical = totalObjects +
|
|
934
|
+
totalEventsCount +
|
|
935
|
+
totalLinksCount +
|
|
936
|
+
totalActiveJobsCount +
|
|
937
|
+
totalDeadJobsCount;
|
|
938
|
+
if (totalHistorical > 0) {
|
|
939
|
+
const capBarW = Math.max(5, viewW - 20);
|
|
940
|
+
const objFrac = totalObjects / totalHistorical;
|
|
941
|
+
const evtFrac = totalEventsCount / totalHistorical;
|
|
942
|
+
const linkFrac = totalLinksCount / totalHistorical;
|
|
943
|
+
const objChars = Math.round(objFrac * capBarW);
|
|
944
|
+
const evtChars = Math.round(evtFrac * capBarW);
|
|
945
|
+
const linkChars = Math.round(linkFrac * capBarW);
|
|
946
|
+
const jobChars = capBarW - objChars - evtChars - linkChars;
|
|
947
|
+
content += ` ${pc.bold("Distribution")}\n`;
|
|
948
|
+
content += ` ${pc.dim(" ")}${pc.cyan("█".repeat(Math.max(0, objChars)))}${pc.green("█".repeat(Math.max(0, evtChars)))}${pc.blue("█".repeat(Math.max(0, linkChars)))}${pc.yellow("█".repeat(Math.max(0, jobChars)))}\n`;
|
|
949
|
+
content += ` ${pc.cyan("■")} objs ${pc.green("■")} evts ${pc.blue("■")} links ${pc.yellow("■")} jobs\n\n`;
|
|
950
|
+
}
|
|
868
951
|
content += ` ${pc.bold("Connection")}\n`;
|
|
869
952
|
content += ` ${pc.dim("Driver".padEnd(14))} ${driverName}\n`;
|
|
870
953
|
content += ` ${pc.dim("Path".padEnd(14))} ${dbPath || ":memory:"}\n`;
|
|
@@ -948,14 +1031,20 @@ function draw() {
|
|
|
948
1031
|
scrollOffset = Math.max(0, Math.min(scrollOffset, Math.max(0, tree.length - bodyH)));
|
|
949
1032
|
let buf = "\u001B[H"; // Move to top-left
|
|
950
1033
|
// Header — opencode style: clean, no inverse bar
|
|
951
|
-
if (
|
|
952
|
-
buf += ` ${pc.cyan("◈")} ${pc.bold("thingd")} ${pc.dim("
|
|
1034
|
+
if (showHelp) {
|
|
1035
|
+
buf += ` ${pc.cyan("◈")} ${pc.bold("thingd")} ${pc.dim("Help (press any key)")}\n`;
|
|
1036
|
+
}
|
|
1037
|
+
else if (!connected) {
|
|
1038
|
+
buf += ` ${pc.cyan("◈")} ${pc.bold("thingd")} ${pc.dim("Select Environment")} ${pc.dim("[?] help")}\n`;
|
|
953
1039
|
}
|
|
954
1040
|
else if (formState?.active) {
|
|
955
|
-
buf += ` ${pc.cyan("◈")} ${pc.bold("thingd")} ${pc.cyan(driver.toUpperCase())} ${pc.dim("Input Mode")}\n`;
|
|
1041
|
+
buf += ` ${pc.cyan("◈")} ${pc.bold("thingd")} ${pc.cyan(driver.toUpperCase())} ${pc.dim("Input Mode")} ${pc.dim("[?] help")}\n`;
|
|
956
1042
|
}
|
|
957
1043
|
else {
|
|
958
|
-
const
|
|
1044
|
+
const breadcrumb = computeBreadcrumbs();
|
|
1045
|
+
const base = ` ${pc.cyan("◈")} ${pc.bold("thingd")} ${pc.cyan(driver.toUpperCase())}`;
|
|
1046
|
+
const dash = breadcrumb ? ` ${pc.dim("▸")} ` : "";
|
|
1047
|
+
const label = `${base}${dash}${breadcrumb} ${pc.dim("[?] help")}`;
|
|
959
1048
|
buf += `${padToWidth(label, W)}\n`;
|
|
960
1049
|
}
|
|
961
1050
|
buf += `${pc.dim("─".repeat(W))}\n`;
|
|
@@ -1041,14 +1130,107 @@ function draw() {
|
|
|
1041
1130
|
help = ` ${pc.dim("↑↓")} nav ${pc.dim("enter")} connect ${pc.dim("q")} quit `;
|
|
1042
1131
|
}
|
|
1043
1132
|
else {
|
|
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 `;
|
|
1133
|
+
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("N")} nlq ${pc.dim("a")} agg ${pc.dim("t")} ts ${pc.dim("o")} options ${pc.dim("b")} batch ${pc.dim("i")} info ${pc.dim("r")} refresh ${pc.dim("s")} switch ${pc.dim("l")} logout ${pc.dim("q")} quit `;
|
|
1045
1134
|
}
|
|
1046
1135
|
buf += `${pc.dim("─".repeat(W))}\n`;
|
|
1047
1136
|
buf += padToWidth(help, W);
|
|
1048
1137
|
// Clear to end
|
|
1049
1138
|
buf += "\u001B[J";
|
|
1139
|
+
if (showHelp) {
|
|
1140
|
+
buf += computeHelpOverlay(W, H);
|
|
1141
|
+
}
|
|
1050
1142
|
process.stdout.write(buf);
|
|
1051
1143
|
}
|
|
1144
|
+
function computeBreadcrumbs() {
|
|
1145
|
+
if (!connected) {
|
|
1146
|
+
return "";
|
|
1147
|
+
}
|
|
1148
|
+
const tree = buildTree();
|
|
1149
|
+
const node = tree[cursorIndex];
|
|
1150
|
+
if (!node) {
|
|
1151
|
+
return "";
|
|
1152
|
+
}
|
|
1153
|
+
const labels = [];
|
|
1154
|
+
const climb = (id) => {
|
|
1155
|
+
if (!id) {
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
const n = tree.find((t) => t.id === id);
|
|
1159
|
+
if (!n) {
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
if (n.parentId) {
|
|
1163
|
+
climb(n.parentId);
|
|
1164
|
+
}
|
|
1165
|
+
const clean = n.label
|
|
1166
|
+
.replace(/[▾▸●○·◇◈◉⬡]/g, "")
|
|
1167
|
+
.trim()
|
|
1168
|
+
.replace(/^\S+\s+/, "")
|
|
1169
|
+
.trim();
|
|
1170
|
+
if (clean && !clean.startsWith("(")) {
|
|
1171
|
+
labels.push(clean);
|
|
1172
|
+
}
|
|
1173
|
+
};
|
|
1174
|
+
climb(node.id);
|
|
1175
|
+
return labels.length > 0 ? pc.dim(labels.join(" > ")) : "";
|
|
1176
|
+
}
|
|
1177
|
+
function computeHelpOverlay(W, H) {
|
|
1178
|
+
const lines = [
|
|
1179
|
+
`${pc.bold(" thingd TUI Help")}`,
|
|
1180
|
+
"",
|
|
1181
|
+
` ${pc.bold("Navigation")}`,
|
|
1182
|
+
` ${pc.dim("↑↓/j/k")} Move cursor`,
|
|
1183
|
+
` ${pc.dim("←→/h/l")} Expand/collapse tree`,
|
|
1184
|
+
` ${pc.dim("Enter")} Toggle expand`,
|
|
1185
|
+
"",
|
|
1186
|
+
` ${pc.bold("Operations")}`,
|
|
1187
|
+
` ${pc.dim("c")} Create resource`,
|
|
1188
|
+
` ${pc.dim("e")} Edit object/job`,
|
|
1189
|
+
` ${pc.dim("d")} Delete object/link/job`,
|
|
1190
|
+
` ${pc.dim("r")} Refresh data`,
|
|
1191
|
+
` ${pc.dim("i")} Connection info`,
|
|
1192
|
+
"",
|
|
1193
|
+
` ${pc.bold("Data Views")}`,
|
|
1194
|
+
` ${pc.dim("n")} Object neighbors (links)`,
|
|
1195
|
+
` ${pc.dim("a")} Aggregate (count/sum/avg/min/max)`,
|
|
1196
|
+
` ${pc.dim("t")} Time-series query`,
|
|
1197
|
+
` ${pc.dim("N")} Natural language query`,
|
|
1198
|
+
` ${pc.dim("/f")} Search objects`,
|
|
1199
|
+
` ${pc.dim("o")} Object listing options`,
|
|
1200
|
+
` ${pc.dim("b")} Batch put/delete`,
|
|
1201
|
+
"",
|
|
1202
|
+
` ${pc.bold("System")}`,
|
|
1203
|
+
` ${pc.dim("s")} Switch driver`,
|
|
1204
|
+
` ${pc.dim("m")} Maintenance`,
|
|
1205
|
+
` ${pc.dim("l")} Logout`,
|
|
1206
|
+
` ${pc.dim("q")} Quit`,
|
|
1207
|
+
` ${pc.dim("?")} Toggle this help`,
|
|
1208
|
+
"",
|
|
1209
|
+
` ${pc.dim("Press any key to close help.")}`,
|
|
1210
|
+
];
|
|
1211
|
+
const helpW = 44;
|
|
1212
|
+
const helpH = lines.length + 2;
|
|
1213
|
+
const col = Math.max(0, Math.floor((W - helpW) / 2));
|
|
1214
|
+
const row = Math.max(0, Math.floor((H - helpH) / 2));
|
|
1215
|
+
let out = "";
|
|
1216
|
+
for (let r = 0; r < H; r++) {
|
|
1217
|
+
if (r >= row && r < row + helpH) {
|
|
1218
|
+
const lineIdx = r - row;
|
|
1219
|
+
if (lineIdx === 0 || lineIdx === helpH - 1) {
|
|
1220
|
+
out += `${" ".repeat(col) + pc.dim(`┌${"─".repeat(helpW - 2)}┐`)}\n`;
|
|
1221
|
+
}
|
|
1222
|
+
else {
|
|
1223
|
+
const text = lines[lineIdx - 1] ?? "";
|
|
1224
|
+
const padded = text + " ".repeat(Math.max(0, helpW - visibleWidth(text) - 2));
|
|
1225
|
+
out += `${" ".repeat(col) + pc.dim("│")} ${padded} ${pc.dim("│")}\n`;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
else {
|
|
1229
|
+
out += "\n";
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
return out;
|
|
1233
|
+
}
|
|
1052
1234
|
/** Pad/truncate `text` to exactly `width` visible characters. */
|
|
1053
1235
|
function fitToWidth(text, width, highlight) {
|
|
1054
1236
|
const vw = visibleWidth(text);
|
|
@@ -1661,6 +1843,316 @@ async function handleNeighbors(selected) {
|
|
|
1661
1843
|
draw();
|
|
1662
1844
|
});
|
|
1663
1845
|
}
|
|
1846
|
+
async function handleAggregate(selected) {
|
|
1847
|
+
const defaultCol = selected?.type === "collection"
|
|
1848
|
+
? (selected.ref?.name ?? "")
|
|
1849
|
+
: selected?.type === "object"
|
|
1850
|
+
? (selected.ref?.collection ?? "")
|
|
1851
|
+
: "";
|
|
1852
|
+
openForm("Aggregate", [
|
|
1853
|
+
{
|
|
1854
|
+
id: "function",
|
|
1855
|
+
label: "Function",
|
|
1856
|
+
value: "count",
|
|
1857
|
+
options: ["count", "sum", "avg", "min", "max"],
|
|
1858
|
+
},
|
|
1859
|
+
{
|
|
1860
|
+
id: "collection",
|
|
1861
|
+
label: "Collection",
|
|
1862
|
+
value: defaultCol,
|
|
1863
|
+
options: collections,
|
|
1864
|
+
allowCustom: true,
|
|
1865
|
+
},
|
|
1866
|
+
{ id: "field", label: "Field (for sum/avg/min/max)", placeholder: "field name" },
|
|
1867
|
+
{ id: "groupBy", label: "Group By (optional field)", placeholder: "field name" },
|
|
1868
|
+
{ id: "filter", label: "Filter (optional JSON)", placeholder: '{"status":"active"}' },
|
|
1869
|
+
], async (vals) => {
|
|
1870
|
+
const func = vals.function || "count";
|
|
1871
|
+
const collection = (vals.collection || "").trim();
|
|
1872
|
+
if (!collection) {
|
|
1873
|
+
throw new Error("Collection is required.");
|
|
1874
|
+
}
|
|
1875
|
+
const field = (vals.field || "").trim() || undefined;
|
|
1876
|
+
const groupBy = (vals.groupBy || "").trim() || undefined;
|
|
1877
|
+
const filter = vals.filter?.trim() ? JSON.parse(vals.filter.trim()) : undefined;
|
|
1878
|
+
const options = { groupBy, filter };
|
|
1879
|
+
const a = db.aggregate;
|
|
1880
|
+
const result = func === "count"
|
|
1881
|
+
? await a.count(collection, options)
|
|
1882
|
+
: func === "sum"
|
|
1883
|
+
? await a.sum(collection, field, options)
|
|
1884
|
+
: func === "avg"
|
|
1885
|
+
? await a.avg(collection, field, options)
|
|
1886
|
+
: func === "min"
|
|
1887
|
+
? await a.min(collection, field, options)
|
|
1888
|
+
: await a.max(collection, field, options);
|
|
1889
|
+
const lines = [
|
|
1890
|
+
` ${pc.bold("Aggregate")} ${pc.cyan(func)} ${pc.dim(`on ${collection}`)}`,
|
|
1891
|
+
"",
|
|
1892
|
+
` ${pc.dim("Total:")} ${pc.bold(String(result.total))}`,
|
|
1893
|
+
"",
|
|
1894
|
+
];
|
|
1895
|
+
if (result.groups && result.groups.length > 0) {
|
|
1896
|
+
lines.push(` ${pc.bold("Groups")}`);
|
|
1897
|
+
const maxVal = Math.max(...result.groups.map((g) => g.value));
|
|
1898
|
+
for (const g of result.groups) {
|
|
1899
|
+
const barLen = Math.max(1, Math.round((g.value / maxVal) * 20));
|
|
1900
|
+
const bar = pc.cyan("█".repeat(barLen));
|
|
1901
|
+
lines.push(` ${g.key}: ${bar} ${pc.dim(String(g.value))}`);
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
viewerLines = lines;
|
|
1905
|
+
loadedItemId = "aggregate_result";
|
|
1906
|
+
draw();
|
|
1907
|
+
});
|
|
1908
|
+
}
|
|
1909
|
+
async function handleTimeseries(selected) {
|
|
1910
|
+
const defaultCol = selected?.type === "collection"
|
|
1911
|
+
? (selected.ref?.name ?? "")
|
|
1912
|
+
: selected?.type === "object"
|
|
1913
|
+
? (selected.ref?.collection ?? "")
|
|
1914
|
+
: "";
|
|
1915
|
+
openForm("Time Series", [
|
|
1916
|
+
{
|
|
1917
|
+
id: "function",
|
|
1918
|
+
label: "Function",
|
|
1919
|
+
value: "count",
|
|
1920
|
+
options: ["count", "sum", "avg", "min", "max"],
|
|
1921
|
+
},
|
|
1922
|
+
{
|
|
1923
|
+
id: "collection",
|
|
1924
|
+
label: "Collection",
|
|
1925
|
+
value: defaultCol,
|
|
1926
|
+
options: collections,
|
|
1927
|
+
allowCustom: true,
|
|
1928
|
+
},
|
|
1929
|
+
{ id: "field", label: "Field (optional)", placeholder: "field name" },
|
|
1930
|
+
{
|
|
1931
|
+
id: "bucket",
|
|
1932
|
+
label: "Bucket",
|
|
1933
|
+
value: "day",
|
|
1934
|
+
options: ["hour", "day", "week", "month"],
|
|
1935
|
+
},
|
|
1936
|
+
{ id: "from", label: "From (ISO date, optional)", placeholder: "2024-01-01" },
|
|
1937
|
+
{ id: "to", label: "To (ISO date, optional)", placeholder: "2024-12-31" },
|
|
1938
|
+
{ id: "filter", label: "Filter (optional JSON)", placeholder: '{"status":"active"}' },
|
|
1939
|
+
], async (vals) => {
|
|
1940
|
+
const func = vals.function || "count";
|
|
1941
|
+
const collection = (vals.collection || "").trim();
|
|
1942
|
+
if (!collection) {
|
|
1943
|
+
throw new Error("Collection is required.");
|
|
1944
|
+
}
|
|
1945
|
+
const field = (vals.field || "").trim() || undefined;
|
|
1946
|
+
const bucket = (vals.bucket || "day");
|
|
1947
|
+
const from = (vals.from || "").trim() || undefined;
|
|
1948
|
+
const to = (vals.to || "").trim() || undefined;
|
|
1949
|
+
const filter = vals.filter?.trim() ? JSON.parse(vals.filter.trim()) : undefined;
|
|
1950
|
+
const result = await db.timeseries(collection, {
|
|
1951
|
+
function: func,
|
|
1952
|
+
field,
|
|
1953
|
+
bucket,
|
|
1954
|
+
from,
|
|
1955
|
+
to,
|
|
1956
|
+
filter,
|
|
1957
|
+
});
|
|
1958
|
+
const lines = [
|
|
1959
|
+
` ${pc.bold("Time Series")} ${pc.cyan(func)} ${pc.dim(`on ${collection}, ${bucket}ly`)}`,
|
|
1960
|
+
"",
|
|
1961
|
+
];
|
|
1962
|
+
if (result.buckets.length === 0) {
|
|
1963
|
+
lines.push(pc.dim("No data for the selected range."));
|
|
1964
|
+
}
|
|
1965
|
+
else {
|
|
1966
|
+
const maxVal = Math.max(...result.buckets.map((b) => b.value));
|
|
1967
|
+
for (const b of result.buckets) {
|
|
1968
|
+
const barLen = Math.max(1, Math.round((b.value / maxVal) * 20));
|
|
1969
|
+
const bar = pc.green("█".repeat(barLen));
|
|
1970
|
+
lines.push(` ${b.label}: ${bar} ${pc.dim(String(b.value))}`);
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
viewerLines = lines;
|
|
1974
|
+
loadedItemId = "timeseries_result";
|
|
1975
|
+
draw();
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
async function handleNlq(selected) {
|
|
1979
|
+
const defaultCol = selected?.type === "collection"
|
|
1980
|
+
? (selected.ref?.name ?? "")
|
|
1981
|
+
: selected?.type === "object"
|
|
1982
|
+
? (selected.ref?.collection ?? "")
|
|
1983
|
+
: "";
|
|
1984
|
+
openForm("Natural Language Query", [
|
|
1985
|
+
{ id: "question", label: "Question", placeholder: "How many active users?" },
|
|
1986
|
+
{
|
|
1987
|
+
id: "collection",
|
|
1988
|
+
label: "Collection (optional)",
|
|
1989
|
+
value: defaultCol,
|
|
1990
|
+
options: collections,
|
|
1991
|
+
allowCustom: true,
|
|
1992
|
+
},
|
|
1993
|
+
], async (vals) => {
|
|
1994
|
+
const question = (vals.question || "").trim();
|
|
1995
|
+
if (!question) {
|
|
1996
|
+
throw new Error("Question is required.");
|
|
1997
|
+
}
|
|
1998
|
+
const collection = (vals.collection || "").trim() || undefined;
|
|
1999
|
+
const result = await db.nlq.query(question, {
|
|
2000
|
+
collection,
|
|
2001
|
+
});
|
|
2002
|
+
const intent = result.intent;
|
|
2003
|
+
const lines = [
|
|
2004
|
+
` ${pc.bold("NLQ Result")}`,
|
|
2005
|
+
` ${pc.dim(`Question: ${question}`)}`,
|
|
2006
|
+
"",
|
|
2007
|
+
` ${result.answer}`,
|
|
2008
|
+
"",
|
|
2009
|
+
];
|
|
2010
|
+
if (intent) {
|
|
2011
|
+
lines.push(` ${pc.dim("Interpreted as:")} ${intent.action} ${pc.dim("on")} ${pc.cyan(intent.collection)}`);
|
|
2012
|
+
if (intent.function) {
|
|
2013
|
+
lines.push(` ${pc.dim("Function:")} ${intent.function}`);
|
|
2014
|
+
}
|
|
2015
|
+
if (intent.field) {
|
|
2016
|
+
lines.push(` ${pc.dim("Field:")} ${intent.field}`);
|
|
2017
|
+
}
|
|
2018
|
+
if (intent.query) {
|
|
2019
|
+
lines.push(` ${pc.dim("Query:")} ${intent.query}`);
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
if (result.data !== undefined && result.data !== null) {
|
|
2023
|
+
lines.push("", highlightJson(result.data));
|
|
2024
|
+
}
|
|
2025
|
+
viewerLines = lines;
|
|
2026
|
+
loadedItemId = "nlq_result";
|
|
2027
|
+
draw();
|
|
2028
|
+
});
|
|
2029
|
+
}
|
|
2030
|
+
async function handleCollectionOptions(selected) {
|
|
2031
|
+
const colName = selected?.type === "collection"
|
|
2032
|
+
? (selected.ref?.name ?? "")
|
|
2033
|
+
: selected?.type === "object"
|
|
2034
|
+
? (selected.ref?.collection ?? "")
|
|
2035
|
+
: "";
|
|
2036
|
+
if (!colName) {
|
|
2037
|
+
viewerLines = [pc.yellow("Select a collection first, then press [o] to set listing options.")];
|
|
2038
|
+
loadedItemId = "options_info";
|
|
2039
|
+
draw();
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2042
|
+
const current = collectionOptions.get(colName) ?? {};
|
|
2043
|
+
openForm(`Listing Options: ${colName}`, [
|
|
2044
|
+
{
|
|
2045
|
+
id: "sortBy",
|
|
2046
|
+
label: "Sort By",
|
|
2047
|
+
value: current.sortBy ?? "",
|
|
2048
|
+
options: ["", "id", "created_at", "updated_at", "version"],
|
|
2049
|
+
allowCustom: true,
|
|
2050
|
+
},
|
|
2051
|
+
{
|
|
2052
|
+
id: "sortDir",
|
|
2053
|
+
label: "Sort Direction",
|
|
2054
|
+
value: current.sortDir ?? "asc",
|
|
2055
|
+
options: ["asc", "desc"],
|
|
2056
|
+
},
|
|
2057
|
+
{ id: "limit", label: "Limit", value: String(current.limit ?? ""), placeholder: "50" },
|
|
2058
|
+
{ id: "offset", label: "Offset", value: String(current.offset ?? ""), placeholder: "0" },
|
|
2059
|
+
{
|
|
2060
|
+
id: "filter",
|
|
2061
|
+
label: "Filter (JSON)",
|
|
2062
|
+
value: current.filter ? JSON.stringify(current.filter) : "",
|
|
2063
|
+
placeholder: '{"status":"active"}',
|
|
2064
|
+
},
|
|
2065
|
+
], async (vals) => {
|
|
2066
|
+
const opts = {};
|
|
2067
|
+
if (vals.sortBy) {
|
|
2068
|
+
opts.sortBy = vals.sortBy;
|
|
2069
|
+
opts.sortDir = vals.sortDir || "asc";
|
|
2070
|
+
}
|
|
2071
|
+
if (vals.limit) {
|
|
2072
|
+
opts.limit = parseInt(vals.limit, 10) || 50;
|
|
2073
|
+
}
|
|
2074
|
+
if (vals.offset) {
|
|
2075
|
+
opts.offset = parseInt(vals.offset, 10) || 0;
|
|
2076
|
+
}
|
|
2077
|
+
if (vals.filter?.trim()) {
|
|
2078
|
+
opts.filter = JSON.parse(vals.filter.trim());
|
|
2079
|
+
}
|
|
2080
|
+
if (Object.keys(opts).length > 0) {
|
|
2081
|
+
collectionOptions.set(colName, opts);
|
|
2082
|
+
}
|
|
2083
|
+
else {
|
|
2084
|
+
collectionOptions.delete(colName);
|
|
2085
|
+
}
|
|
2086
|
+
await fetchResources();
|
|
2087
|
+
const tree = buildTree();
|
|
2088
|
+
const idx = tree.findIndex((n) => n.id === `col:${colName}`);
|
|
2089
|
+
if (idx !== -1) {
|
|
2090
|
+
cursorIndex = idx;
|
|
2091
|
+
}
|
|
2092
|
+
const n = tree[cursorIndex];
|
|
2093
|
+
if (n) {
|
|
2094
|
+
scheduleLoad(n);
|
|
2095
|
+
}
|
|
2096
|
+
});
|
|
2097
|
+
}
|
|
2098
|
+
async function handleBatchOps(selected) {
|
|
2099
|
+
const colName = selected?.type === "collection"
|
|
2100
|
+
? (selected.ref?.name ?? "")
|
|
2101
|
+
: selected?.type === "object"
|
|
2102
|
+
? (selected.ref?.collection ?? "")
|
|
2103
|
+
: "";
|
|
2104
|
+
if (!colName) {
|
|
2105
|
+
viewerLines = [pc.yellow("Select a collection first, then press [b] for batch operations.")];
|
|
2106
|
+
loadedItemId = "batch_info";
|
|
2107
|
+
draw();
|
|
2108
|
+
return;
|
|
2109
|
+
}
|
|
2110
|
+
openForm(`Batch Ops: ${colName}`, [
|
|
2111
|
+
{
|
|
2112
|
+
id: "action",
|
|
2113
|
+
label: "Action",
|
|
2114
|
+
value: "put",
|
|
2115
|
+
options: ["put", "delete"],
|
|
2116
|
+
},
|
|
2117
|
+
{
|
|
2118
|
+
id: "input",
|
|
2119
|
+
label: "JSON File Path or IDs (comma-sep for delete)",
|
|
2120
|
+
placeholder: "/path/to/file.json or id1,id2,id3",
|
|
2121
|
+
},
|
|
2122
|
+
], async (vals) => {
|
|
2123
|
+
const action = vals.action || "";
|
|
2124
|
+
const input = (vals.input || "").trim();
|
|
2125
|
+
if (!input) {
|
|
2126
|
+
throw new Error("Input is required.");
|
|
2127
|
+
}
|
|
2128
|
+
if (action === "put") {
|
|
2129
|
+
const data = JSON.parse(await fs.promises.readFile(input, "utf-8"));
|
|
2130
|
+
const objects = Array.isArray(data) ? data : (data.objects ?? [data]);
|
|
2131
|
+
const result = await db.putBatch(colName, objects);
|
|
2132
|
+
viewerLines = [
|
|
2133
|
+
` ${pc.bold("Batch Put Complete")}`,
|
|
2134
|
+
` ${pc.dim(`Collection: ${colName}`)}`,
|
|
2135
|
+
` ${pc.dim(`Objects: ${result.length}`)}`,
|
|
2136
|
+
];
|
|
2137
|
+
loadedItemId = "batch_result";
|
|
2138
|
+
draw();
|
|
2139
|
+
}
|
|
2140
|
+
else if (action === "delete") {
|
|
2141
|
+
const ids = input
|
|
2142
|
+
.split(",")
|
|
2143
|
+
.map((s) => s.trim())
|
|
2144
|
+
.filter(Boolean);
|
|
2145
|
+
const count = await db.deleteBatch(colName, ids);
|
|
2146
|
+
viewerLines = [
|
|
2147
|
+
` ${pc.bold("Batch Delete Complete")}`,
|
|
2148
|
+
` ${pc.dim(`Collection: ${colName}`)}`,
|
|
2149
|
+
` ${pc.dim(`Deleted: ${count} objects`)}`,
|
|
2150
|
+
];
|
|
2151
|
+
loadedItemId = "batch_result";
|
|
2152
|
+
draw();
|
|
2153
|
+
}
|
|
2154
|
+
});
|
|
2155
|
+
}
|
|
1664
2156
|
async function handleMaintenance() {
|
|
1665
2157
|
// Cycle through maintenance operations with each press of 'm'
|
|
1666
2158
|
const operations = ["health", "checkpoint", "backup"];
|
|
@@ -1745,6 +2237,12 @@ function setupKeypress() {
|
|
|
1745
2237
|
if (!key) {
|
|
1746
2238
|
return;
|
|
1747
2239
|
}
|
|
2240
|
+
// Help overlay dismisses on any keypress
|
|
2241
|
+
if (showHelp) {
|
|
2242
|
+
showHelp = false;
|
|
2243
|
+
draw();
|
|
2244
|
+
return;
|
|
2245
|
+
}
|
|
1748
2246
|
// Quit
|
|
1749
2247
|
if ((key.ctrl && key.name === "c") || key.name === "q") {
|
|
1750
2248
|
if (formState?.active && key.name !== "q") {
|
|
@@ -1861,13 +2359,17 @@ function setupKeypress() {
|
|
|
1861
2359
|
}
|
|
1862
2360
|
}
|
|
1863
2361
|
else if (!connected) {
|
|
1864
|
-
// Driver selection mode — only Enter
|
|
2362
|
+
// Driver selection mode — only Enter and ? work
|
|
1865
2363
|
if (key.name === "return") {
|
|
1866
2364
|
const node = tree[cursorIndex];
|
|
1867
2365
|
if (node) {
|
|
1868
2366
|
await handleConnect(node);
|
|
1869
2367
|
}
|
|
1870
2368
|
}
|
|
2369
|
+
else if (str === "?") {
|
|
2370
|
+
showHelp = !showHelp;
|
|
2371
|
+
draw();
|
|
2372
|
+
}
|
|
1871
2373
|
}
|
|
1872
2374
|
else {
|
|
1873
2375
|
// Connected mode — full set of shortcuts
|
|
@@ -1953,12 +2455,31 @@ function setupKeypress() {
|
|
|
1953
2455
|
else if (str === "/" || str === "f" || str === "F") {
|
|
1954
2456
|
await handleSearch();
|
|
1955
2457
|
}
|
|
2458
|
+
else if (str === "?") {
|
|
2459
|
+
showHelp = !showHelp;
|
|
2460
|
+
draw();
|
|
2461
|
+
}
|
|
1956
2462
|
else if (str === "i" || str === "I") {
|
|
1957
2463
|
await handleInfo();
|
|
1958
2464
|
}
|
|
1959
|
-
else if (str === "n"
|
|
2465
|
+
else if (str === "n") {
|
|
1960
2466
|
await handleNeighbors(tree[cursorIndex]);
|
|
1961
2467
|
}
|
|
2468
|
+
else if (str === "N") {
|
|
2469
|
+
await handleNlq(tree[cursorIndex]);
|
|
2470
|
+
}
|
|
2471
|
+
else if (str === "a" || str === "A") {
|
|
2472
|
+
await handleAggregate(tree[cursorIndex]);
|
|
2473
|
+
}
|
|
2474
|
+
else if (str === "t" || str === "T") {
|
|
2475
|
+
await handleTimeseries(tree[cursorIndex]);
|
|
2476
|
+
}
|
|
2477
|
+
else if (str === "o" || str === "O") {
|
|
2478
|
+
await handleCollectionOptions(tree[cursorIndex]);
|
|
2479
|
+
}
|
|
2480
|
+
else if (str === "b" || str === "B") {
|
|
2481
|
+
await handleBatchOps(tree[cursorIndex]);
|
|
2482
|
+
}
|
|
1962
2483
|
else if (str === "m" || str === "M") {
|
|
1963
2484
|
await handleMaintenance();
|
|
1964
2485
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thingd/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.57.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.57.0"
|
|
49
49
|
},
|
|
50
50
|
"engines": {
|
|
51
51
|
"node": ">=24.0.0"
|