@thingd/cli 0.80.0 → 0.82.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/index.d.ts.map +1 -1
- package/dist/index.js +65 -2
- package/dist/native-archive.d.ts +3 -0
- package/dist/native-archive.d.ts.map +1 -0
- package/dist/native-archive.js +134 -0
- package/package.json +2 -2
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAeA,OAAO,EAYL,MAAM,EACN,KAAK,YAAY,EAClB,MAAM,aAAa,CAAC;AASrB,KAAK,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAEjD,KAAK,YAAY,GAAG;IAClB,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B,CAAC;AAEF,KAAK,YAAY,GAAG,MAAM,CAAC,cAAc,CAAC;AAE1C,MAAM,MAAM,aAAa,GAAG;IAC1B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,KAAK,CAAC,EAAE,YAAY,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAC7B,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,UAAU,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,YAAY,CAAC;IACrB,MAAM,EAAE,YAAY,CAAC;IACrB,KAAK,EAAE,YAAY,CAAC;IACpB,MAAM,EAAE,OAAO,CAAC;CACjB,CAAC;AAsCF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,OAAO,CAAC;IACf,sDAAsD;IACtD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAeA,OAAO,EAYL,MAAM,EACN,KAAK,YAAY,EAClB,MAAM,aAAa,CAAC;AASrB,KAAK,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAEjD,KAAK,YAAY,GAAG;IAClB,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B,CAAC;AAEF,KAAK,YAAY,GAAG,MAAM,CAAC,cAAc,CAAC;AAE1C,MAAM,MAAM,aAAa,GAAG;IAC1B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,KAAK,CAAC,EAAE,YAAY,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAC7B,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,UAAU,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,YAAY,CAAC;IACrB,MAAM,EAAE,YAAY,CAAC;IACrB,KAAK,EAAE,YAAY,CAAC;IACpB,MAAM,EAAE,OAAO,CAAC;CACjB,CAAC;AAsCF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,OAAO,CAAC;IACf,sDAAsD;IACtD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAwGF,wBAAsB,MAAM,CAC1B,IAAI,WAAwB,EAC5B,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,MAAM,CAAC,CA8EjB;AA0yCD,wBAAsB,MAAM,CAC1B,OAAO,EAAE,UAAU,EACnB,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GACtC,OAAO,CAAC,IAAI,CAAC,CAgBf;AA4BD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,UAAU,GAAG,iBAAiB,CAuDxE;AAiDD,wBAAgB,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAEjE;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAE/E;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAGlF;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAMrE;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAEnF;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAMrF;AAyFD,wBAAgB,SAAS,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI,CAEpF;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAElE"}
|
package/dist/index.js
CHANGED
|
@@ -105,7 +105,10 @@ Usage:
|
|
|
105
105
|
thingd cloud instance list <project>
|
|
106
106
|
thingd cloud api-key create <project> <name>
|
|
107
107
|
thingd db checkpoint [--path <path>]
|
|
108
|
+
thingd db compact [--path <path>]
|
|
108
109
|
thingd db integrity [--path <path>]
|
|
110
|
+
thingd db backup --out <archive.tar> [--path <path>]
|
|
111
|
+
thingd db restore --in <archive.tar> --destination <path> [--replace]
|
|
109
112
|
thingd db reencrypt --source <path> --destination <path> [--allow-plaintext-output]
|
|
110
113
|
|
|
111
114
|
Options:
|
|
@@ -319,21 +322,33 @@ async function runCommand(context) {
|
|
|
319
322
|
if (command === "db") {
|
|
320
323
|
const sub = context.parsed.tokens[1];
|
|
321
324
|
if (!sub) {
|
|
322
|
-
throw new Error("Expected db subcommand: checkpoint, integrity");
|
|
325
|
+
throw new Error("Expected db subcommand: checkpoint, compact, integrity");
|
|
323
326
|
}
|
|
324
327
|
if (sub === "checkpoint") {
|
|
325
328
|
await runDbCheckpoint(context);
|
|
326
329
|
return;
|
|
327
330
|
}
|
|
331
|
+
if (sub === "compact") {
|
|
332
|
+
await runDbCompact(context);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
328
335
|
if (sub === "integrity") {
|
|
329
336
|
await runDbIntegrity(context);
|
|
330
337
|
return;
|
|
331
338
|
}
|
|
339
|
+
if (sub === "backup") {
|
|
340
|
+
await runDbNativeBackup(context);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (sub === "restore") {
|
|
344
|
+
await runDbNativeRestore(context);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
332
347
|
if (sub === "reencrypt") {
|
|
333
348
|
await runDbReencrypt(context);
|
|
334
349
|
return;
|
|
335
350
|
}
|
|
336
|
-
throw new Error(`Unknown db subcommand: ${sub}. Expected: checkpoint, integrity, reencrypt`);
|
|
351
|
+
throw new Error(`Unknown db subcommand: ${sub}. Expected: checkpoint, compact, integrity, backup, restore, reencrypt`);
|
|
337
352
|
}
|
|
338
353
|
throw new Error(`Unknown command: ${command}`);
|
|
339
354
|
}
|
|
@@ -416,6 +431,20 @@ async function runDbCheckpoint(context) {
|
|
|
416
431
|
writeJson(context.stdout, result, context.pretty);
|
|
417
432
|
});
|
|
418
433
|
}
|
|
434
|
+
async function runDbCompact(context) {
|
|
435
|
+
const connection = resolveConnection(context);
|
|
436
|
+
if (connection.cloud || connection.driver !== "native") {
|
|
437
|
+
throw new Error("db compact requires a local native database");
|
|
438
|
+
}
|
|
439
|
+
const db = await NativeThingStore.open(connection.path, connection.encryptionKey);
|
|
440
|
+
try {
|
|
441
|
+
await db.compact();
|
|
442
|
+
writeJson(context.stdout, { ok: true, path: connection.path }, context.pretty);
|
|
443
|
+
}
|
|
444
|
+
finally {
|
|
445
|
+
await db.close();
|
|
446
|
+
}
|
|
447
|
+
}
|
|
419
448
|
async function runDbIntegrity(context) {
|
|
420
449
|
await withDb(context, async (db) => {
|
|
421
450
|
try {
|
|
@@ -427,6 +456,40 @@ async function runDbIntegrity(context) {
|
|
|
427
456
|
}
|
|
428
457
|
});
|
|
429
458
|
}
|
|
459
|
+
async function runDbNativeBackup(context) {
|
|
460
|
+
const output = stringFlag(context.parsed, "out");
|
|
461
|
+
if (!output) {
|
|
462
|
+
throw new Error("db backup requires --out <archive.tar>");
|
|
463
|
+
}
|
|
464
|
+
const connection = resolveConnection(context);
|
|
465
|
+
if (connection.cloud || connection.driver !== "native") {
|
|
466
|
+
throw new Error("db backup requires a local native database; logical snapshots support remote stores");
|
|
467
|
+
}
|
|
468
|
+
const db = await ThingD.open({
|
|
469
|
+
path: connection.path,
|
|
470
|
+
driver: "native",
|
|
471
|
+
encryption: connection.encryptionKey ? { key: connection.encryptionKey } : undefined,
|
|
472
|
+
});
|
|
473
|
+
try {
|
|
474
|
+
db.walCheckpoint();
|
|
475
|
+
}
|
|
476
|
+
finally {
|
|
477
|
+
await db.close();
|
|
478
|
+
}
|
|
479
|
+
const { createNativeArchive } = await import("./native-archive.js");
|
|
480
|
+
await createNativeArchive(connection.path, output);
|
|
481
|
+
writeJson(context.stdout, { ok: true, out: resolve(output), format: "thingd-native-tar" }, context.pretty);
|
|
482
|
+
}
|
|
483
|
+
async function runDbNativeRestore(context) {
|
|
484
|
+
const input = stringFlag(context.parsed, "in");
|
|
485
|
+
const destination = stringFlag(context.parsed, "destination");
|
|
486
|
+
if (!input || !destination) {
|
|
487
|
+
throw new Error("db restore requires --in <archive.tar> and --destination <path>");
|
|
488
|
+
}
|
|
489
|
+
const { restoreNativeArchive } = await import("./native-archive.js");
|
|
490
|
+
await restoreNativeArchive(input, destination, hasFlag(context.parsed, "replace"));
|
|
491
|
+
writeJson(context.stdout, { ok: true, destination: resolve(destination), format: "thingd-native-tar" }, context.pretty);
|
|
492
|
+
}
|
|
430
493
|
async function runCompletions(context) {
|
|
431
494
|
const shell = optionalToken(context.parsed, 1) ?? "bash";
|
|
432
495
|
const cmds = [
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native-archive.d.ts","sourceRoot":"","sources":["../src/native-archive.ts"],"names":[],"mappings":"AA0DA,wBAAsB,mBAAmB,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAoC/F;AAED,wBAAsB,oBAAoB,CACxC,WAAW,EAAE,MAAM,EACnB,eAAe,EAAE,MAAM,EACvB,OAAO,EAAE,OAAO,GACf,OAAO,CAAC,IAAI,CAAC,CAkEf"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { execFile, execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
const METADATA_NAME = "thingd-backup.json";
|
|
8
|
+
async function runTar(args) {
|
|
9
|
+
try {
|
|
10
|
+
await execFileAsync("tar", args, { maxBuffer: 16 * 1024 * 1024 });
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
14
|
+
throw new Error(`tar operation failed: ${detail}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function assertDirectory(path, label) {
|
|
18
|
+
if (!existsSync(path) || !lstatSync(path).isDirectory()) {
|
|
19
|
+
throw new Error(`${label} must be an existing directory: ${path}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function assertArchivePathSafe(path) {
|
|
23
|
+
if (path.startsWith("/") || path.startsWith("\\") || /^[A-Za-z]:[\\/]/.test(path)) {
|
|
24
|
+
throw new Error(`Unsafe archive path: ${path}`);
|
|
25
|
+
}
|
|
26
|
+
const normalized = path.replaceAll("\\", "/");
|
|
27
|
+
if (normalized.split("/").includes("..")) {
|
|
28
|
+
throw new Error(`Unsafe archive path: ${path}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function archiveEntries(archive) {
|
|
32
|
+
const result = execFileSync("tar", ["-tf", archive], {
|
|
33
|
+
encoding: "utf8",
|
|
34
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
35
|
+
});
|
|
36
|
+
return result.split(/\r?\n/).filter(Boolean);
|
|
37
|
+
}
|
|
38
|
+
export async function createNativeArchive(sourcePath, outputPath) {
|
|
39
|
+
const source = resolve(sourcePath);
|
|
40
|
+
const output = resolve(outputPath);
|
|
41
|
+
assertDirectory(source, "Native database");
|
|
42
|
+
const outputRelative = relative(source, output);
|
|
43
|
+
if (outputRelative && !outputRelative.startsWith("..") && !isAbsolute(outputRelative)) {
|
|
44
|
+
throw new Error("Backup archive must be outside the source database directory");
|
|
45
|
+
}
|
|
46
|
+
if (existsSync(output)) {
|
|
47
|
+
throw new Error(`Backup destination already exists: ${output}`);
|
|
48
|
+
}
|
|
49
|
+
mkdirSync(dirname(output), { recursive: true });
|
|
50
|
+
const staging = mkdtempSync(join(tmpdir(), "thingd-backup-"));
|
|
51
|
+
const metadata = {
|
|
52
|
+
format: "thingd-native-tar",
|
|
53
|
+
version: 1,
|
|
54
|
+
sourceName: basename(source),
|
|
55
|
+
createdAt: new Date().toISOString(),
|
|
56
|
+
};
|
|
57
|
+
const metadataPath = join(staging, METADATA_NAME);
|
|
58
|
+
writeFileSync(metadataPath, `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
|
|
59
|
+
try {
|
|
60
|
+
await runTar([
|
|
61
|
+
"-cf",
|
|
62
|
+
output,
|
|
63
|
+
"-C",
|
|
64
|
+
dirname(source),
|
|
65
|
+
basename(source),
|
|
66
|
+
"-C",
|
|
67
|
+
staging,
|
|
68
|
+
METADATA_NAME,
|
|
69
|
+
]);
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
rmSync(staging, { recursive: true, force: true });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export async function restoreNativeArchive(archivePath, destinationPath, replace) {
|
|
76
|
+
const archive = resolve(archivePath);
|
|
77
|
+
const destination = resolve(destinationPath);
|
|
78
|
+
if (!existsSync(archive) || !lstatSync(archive).isFile()) {
|
|
79
|
+
throw new Error(`Backup archive not found: ${archive}`);
|
|
80
|
+
}
|
|
81
|
+
if (existsSync(destination) && !replace) {
|
|
82
|
+
throw new Error(`Restore destination already exists: ${destination}; pass --replace to replace it`);
|
|
83
|
+
}
|
|
84
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
85
|
+
const entries = archiveEntries(archive);
|
|
86
|
+
for (const entry of entries) {
|
|
87
|
+
assertArchivePathSafe(entry);
|
|
88
|
+
}
|
|
89
|
+
if (!entries.includes(METADATA_NAME)) {
|
|
90
|
+
throw new Error(`Invalid Thingd archive: missing ${METADATA_NAME}`);
|
|
91
|
+
}
|
|
92
|
+
const roots = new Set(entries
|
|
93
|
+
.filter((entry) => entry !== METADATA_NAME)
|
|
94
|
+
.map((entry) => entry.split("/")[0])
|
|
95
|
+
.filter(Boolean));
|
|
96
|
+
if (roots.size !== 1) {
|
|
97
|
+
throw new Error("Invalid Thingd archive: expected exactly one database directory");
|
|
98
|
+
}
|
|
99
|
+
const stagingParent = mkdtempSync(join(dirname(destination), ".thingd-restore-"));
|
|
100
|
+
const root = [...roots][0];
|
|
101
|
+
if (!root) {
|
|
102
|
+
throw new Error("Invalid Thingd archive: missing database directory");
|
|
103
|
+
}
|
|
104
|
+
const staging = join(stagingParent, root);
|
|
105
|
+
let previous;
|
|
106
|
+
try {
|
|
107
|
+
await runTar(["-xf", archive, "-C", stagingParent]);
|
|
108
|
+
const metadata = JSON.parse(readFileSync(join(stagingParent, METADATA_NAME), "utf8"));
|
|
109
|
+
if (metadata.format !== "thingd-native-tar" || metadata.version !== 1) {
|
|
110
|
+
throw new Error("Invalid Thingd archive metadata");
|
|
111
|
+
}
|
|
112
|
+
assertDirectory(staging, "Restored native database");
|
|
113
|
+
if (!existsSync(join(staging, "lock")) || !existsSync(join(staging, "keyspaces"))) {
|
|
114
|
+
throw new Error("Invalid Thingd archive: missing lock or keyspaces directory");
|
|
115
|
+
}
|
|
116
|
+
if (replace && existsSync(destination)) {
|
|
117
|
+
previous = `${destination}.previous-${Date.now()}`;
|
|
118
|
+
renameSync(destination, previous);
|
|
119
|
+
}
|
|
120
|
+
renameSync(staging, destination);
|
|
121
|
+
if (previous) {
|
|
122
|
+
rmSync(previous, { recursive: true, force: true });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (previous && !existsSync(destination) && existsSync(previous)) {
|
|
127
|
+
renameSync(previous, destination);
|
|
128
|
+
}
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
rmSync(stagingParent, { recursive: true, force: true });
|
|
133
|
+
}
|
|
134
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thingd/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.82.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",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
45
|
-
"@thingd/sdk": "^0.
|
|
45
|
+
"@thingd/sdk": "^0.82.0",
|
|
46
46
|
"cli-table3": "^0.6.5",
|
|
47
47
|
"picocolors": "^1.1.1",
|
|
48
48
|
"zod": "^4.4.3"
|