@thingd/cli 0.79.0 → 0.80.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":"data-movement.d.ts","sourceRoot":"","sources":["../src/data-movement.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,KAAK,UAAU,EAAwD,MAAM,YAAY,CAAC;AAwEnG,wBAAsB,SAAS,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAyClE;AAED,wBAAsB,SAAS,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAsFlE;AAoND,wBAAsB,WAAW,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAmHpE;AAED,wBAAsB,SAAS,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAyClE"}
1
+ {"version":3,"file":"data-movement.d.ts","sourceRoot":"","sources":["../src/data-movement.ts"],"names":[],"mappings":"AAqBA,OAAO,EACL,KAAK,UAAU,EAOhB,MAAM,YAAY,CAAC;AAwEpB,wBAAsB,SAAS,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAyClE;AAED,wBAAsB,SAAS,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAsFlE;AAoND,wBAAsB,WAAW,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAmMpE;AAoHD,wBAAsB,SAAS,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAyClE"}
@@ -1,6 +1,10 @@
1
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { once } from "node:events";
2
+ import { createReadStream, createWriteStream, existsSync, readFileSync, writeFileSync, } from "node:fs";
2
3
  import { resolve } from "node:path";
3
- import { hasFlag, requiredFlag, stringFlag, withDb, writeJson } from "./index.js";
4
+ import { createInterface } from "node:readline";
5
+ import { Readable } from "node:stream";
6
+ import { pipeline } from "node:stream/promises";
7
+ import { hasFlag, requiredFlag, resolveConnection, stringFlag, withDb, writeJson, } from "./index.js";
4
8
  const SIDECAR_DEFAULT_URL = "http://localhost:8757";
5
9
  const DEFAULT_REDACT_KEYS = [
6
10
  "password",
@@ -336,31 +340,48 @@ async function runImportDb(context, source, explicitType) {
336
340
  }
337
341
  export async function runSnapshot(context) {
338
342
  const subCommand = context.parsed.tokens[1];
343
+ const connection = resolveConnection(context);
344
+ if (connection.cloud) {
345
+ await runRemoteSnapshot(context, subCommand);
346
+ return;
347
+ }
339
348
  if (subCommand === "create") {
340
349
  const outPath = requiredFlag(context.parsed, "out");
341
350
  await withDb(context, async (db) => {
342
- const collectionsMap = {};
351
+ const output = createWriteStream(resolve(outPath), "utf8");
352
+ const writeRecord = async (record) => {
353
+ if (!output.write(`${JSON.stringify(record)}\n`)) {
354
+ await once(output, "drain");
355
+ }
356
+ };
357
+ await writeRecord({
358
+ type: "thingd.snapshot",
359
+ version: "2.0.0",
360
+ timestamp: new Date().toISOString(),
361
+ format: "jsonl",
362
+ });
343
363
  const cols = await db.listCollections();
344
364
  for (const col of cols) {
345
- collectionsMap[col] = await db.listObjects(col);
365
+ const objects = await db.listObjects(col);
366
+ for (const object of objects) {
367
+ await writeRecord({ type: "object", collection: col, object });
368
+ }
346
369
  }
347
370
  const eventsList = await db.events.list();
348
- const queuesMap = {};
371
+ for (const event of eventsList) {
372
+ await writeRecord({ type: "event", event });
373
+ }
349
374
  const queues = await db.listQueues();
350
375
  for (const q of queues) {
351
376
  const queue = db.queue(q);
352
377
  const [active, dead] = await Promise.all([queue.list(), queue.dead()]);
353
- queuesMap[q] = { active, dead };
378
+ for (const job of [...active, ...dead]) {
379
+ await writeRecord({ type: "queue", job });
380
+ }
354
381
  }
355
- const snapshot = {
356
- version: "1.0.0",
357
- timestamp: new Date().toISOString(),
358
- collections: collectionsMap,
359
- events: eventsList,
360
- queues: queuesMap,
361
- };
362
- writeFileSync(resolve(outPath), JSON.stringify(snapshot, null, 2), "utf8");
363
- writeJson(context.stdout, { success: true, out: outPath }, context.pretty);
382
+ output.end();
383
+ await once(output, "close");
384
+ writeJson(context.stdout, { success: true, out: outPath, format: "thingd-snapshot-jsonl", version: "2.0.0" }, context.pretty);
364
385
  });
365
386
  }
366
387
  else if (subCommand === "restore") {
@@ -369,13 +390,65 @@ export async function runSnapshot(context) {
369
390
  if (!existsSync(resolvedPath)) {
370
391
  throw new Error(`Snapshot file not found: ${inPath}`);
371
392
  }
372
- const snapshot = JSON.parse(readFileSync(resolvedPath, "utf8"));
373
- if (snapshot.version !== "1.0.0") {
374
- throw new Error(`Unsupported snapshot version: ${snapshot.version}`);
393
+ if (inPath.endsWith(".jsonl") || inPath.endsWith(".ndjson")) {
394
+ await restoreJsonlStream(context, inPath, resolvedPath);
395
+ return;
396
+ }
397
+ const content = readFileSync(resolvedPath, "utf8");
398
+ const firstLine = content
399
+ .split("\n")
400
+ .find((line) => line.trim().length > 0)
401
+ ?.trim() ?? "";
402
+ const isJsonl = firstLine.includes('"type":"thingd.snapshot"');
403
+ const snapshot = isJsonl ? null : JSON.parse(content);
404
+ if (!isJsonl && snapshot?.version !== "1.0.0") {
405
+ throw new Error(`Unsupported snapshot version: ${snapshot?.version ?? "unknown"}`);
375
406
  }
376
407
  context.stderr.write("Restoring snapshot... (consider 'thingd backup --out pre-restore.db' first)\n");
377
408
  await withDb(context, async (db) => {
378
409
  try {
410
+ if (isJsonl) {
411
+ const records = content
412
+ .split("\n")
413
+ .map((line) => line.trim())
414
+ .filter(Boolean)
415
+ .map((line) => JSON.parse(line));
416
+ const header = records.shift();
417
+ if (header?.type !== "thingd.snapshot" || header.version !== "2.0.0") {
418
+ throw new Error(`Unsupported JSONL snapshot version: ${String(header?.version ?? "unknown")}`);
419
+ }
420
+ for (const record of records) {
421
+ if (record.type === "object") {
422
+ const object = record.object;
423
+ const cleanObject = { ...object };
424
+ delete cleanObject.collection;
425
+ delete cleanObject.createdAt;
426
+ delete cleanObject.updatedAt;
427
+ delete cleanObject.version;
428
+ await db.put(String(record.collection), cleanObject);
429
+ }
430
+ else if (record.type === "event") {
431
+ const event = record.event;
432
+ const cleanEvent = { ...event };
433
+ delete cleanEvent.id;
434
+ delete cleanEvent.createdAt;
435
+ delete cleanEvent.stream;
436
+ await db.events.append(event.stream, cleanEvent);
437
+ }
438
+ else if (record.type === "queue") {
439
+ const job = record.job;
440
+ await db.queue(job.queue).push(job.payload, {
441
+ idempotencyKey: job.id,
442
+ maxAttempts: job.maxAttempts,
443
+ });
444
+ }
445
+ else {
446
+ throw new Error(`Unsupported snapshot record type: ${String(record.type)}`);
447
+ }
448
+ }
449
+ writeJson(context.stdout, { success: true, in: inPath, format: "thingd-snapshot-jsonl", records: records.length }, context.pretty);
450
+ return;
451
+ }
379
452
  // 1. Restore Collections (clear existing first for true restore)
380
453
  if (snapshot.collections) {
381
454
  for (const [colName, objects] of Object.entries(snapshot.collections)) {
@@ -438,6 +511,100 @@ export async function runSnapshot(context) {
438
511
  throw new Error(`Unknown snapshot command: ${subCommand}. Expected 'create' or 'restore'.`);
439
512
  }
440
513
  }
514
+ async function restoreJsonlStream(context, inPath, resolvedPath) {
515
+ context.stderr.write("Restoring snapshot... (consider 'thingd backup --out pre-restore.db' first)\n");
516
+ await withDb(context, async (db) => {
517
+ const input = createInterface({ input: createReadStream(resolvedPath), crlfDelay: Infinity });
518
+ let headerSeen = false;
519
+ let records = 0;
520
+ try {
521
+ for await (const line of input) {
522
+ if (!line.trim()) {
523
+ continue;
524
+ }
525
+ const record = JSON.parse(line);
526
+ if (!headerSeen) {
527
+ if (record.type !== "thingd.snapshot" || record.version !== "2.0.0") {
528
+ throw new Error(`Unsupported JSONL snapshot version: ${String(record.version ?? "unknown")}`);
529
+ }
530
+ headerSeen = true;
531
+ continue;
532
+ }
533
+ if (record.type === "object") {
534
+ const object = record.object;
535
+ const cleanObject = { ...object };
536
+ delete cleanObject.collection;
537
+ delete cleanObject.createdAt;
538
+ delete cleanObject.updatedAt;
539
+ delete cleanObject.version;
540
+ await db.put(String(record.collection), cleanObject);
541
+ }
542
+ else if (record.type === "event") {
543
+ const event = record.event;
544
+ const cleanEvent = { ...event };
545
+ delete cleanEvent.id;
546
+ delete cleanEvent.createdAt;
547
+ delete cleanEvent.stream;
548
+ await db.events.append(event.stream, cleanEvent);
549
+ }
550
+ else if (record.type === "queue") {
551
+ const job = record.job;
552
+ await db.queue(job.queue).push(job.payload, {
553
+ idempotencyKey: job.id,
554
+ maxAttempts: job.maxAttempts,
555
+ });
556
+ }
557
+ else {
558
+ throw new Error(`Unsupported snapshot record type: ${String(record.type)}`);
559
+ }
560
+ records += 1;
561
+ }
562
+ }
563
+ finally {
564
+ input.close();
565
+ }
566
+ if (!headerSeen) {
567
+ throw new Error("Snapshot is missing its header");
568
+ }
569
+ writeJson(context.stdout, { success: true, in: inPath, format: "thingd-snapshot-jsonl", records }, context.pretty);
570
+ });
571
+ }
572
+ async function runRemoteSnapshot(context, subCommand) {
573
+ const inPath = stringFlag(context.parsed, "in");
574
+ const outPath = stringFlag(context.parsed, "out");
575
+ const base = resolveSidecarUrl(context);
576
+ const headers = authHeaders(context);
577
+ if (subCommand === "create") {
578
+ if (!outPath) {
579
+ throw new Error("Expected --out <path> for remote snapshot export.");
580
+ }
581
+ const response = await fetch(`${base}/v1/snapshot`, { headers });
582
+ if (!response.ok || !response.body) {
583
+ throw new Error(`Remote snapshot export failed: HTTP ${response.status}`);
584
+ }
585
+ await pipeline(Readable.fromWeb(response.body), createWriteStream(resolve(outPath)));
586
+ writeJson(context.stdout, { success: true, out: outPath, format: "thingd-snapshot-jsonl", remote: true }, context.pretty);
587
+ return;
588
+ }
589
+ if (subCommand === "restore") {
590
+ if (!inPath) {
591
+ throw new Error("Expected --in <path> for remote snapshot import.");
592
+ }
593
+ const response = await fetch(`${base}/v1/snapshot`, {
594
+ method: "POST",
595
+ headers: { ...headers, "content-type": "application/x-ndjson" },
596
+ body: createReadStream(resolve(inPath)),
597
+ duplex: "half",
598
+ });
599
+ const result = (await response.json());
600
+ if (!response.ok) {
601
+ throw new Error(`Remote snapshot import failed: ${JSON.stringify(result)}`);
602
+ }
603
+ writeJson(context.stdout, result, context.pretty);
604
+ return;
605
+ }
606
+ throw new Error(`Unknown snapshot command: ${subCommand}. Expected 'create' or 'restore'.`);
607
+ }
441
608
  export async function runBackup(context) {
442
609
  const inPath = stringFlag(context.parsed, "in");
443
610
  const outPath = stringFlag(context.parsed, "out");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thingd/cli",
3
- "version": "0.79.0",
3
+ "version": "0.80.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,14 +42,15 @@
42
42
  },
43
43
  "dependencies": {
44
44
  "@modelcontextprotocol/sdk": "^1.30.0",
45
- "@thingd/sdk": "^0.79.0",
45
+ "@thingd/sdk": "^0.80.0",
46
46
  "cli-table3": "^0.6.5",
47
47
  "picocolors": "^1.1.1",
48
48
  "zod": "^4.4.3"
49
49
  },
50
50
  "scripts": {
51
51
  "build": "pnpm --filter frontend build && tsc -p tsconfig.json && node -e \"fs.mkdirSync('dist/dashboard', { recursive: true }); fs.cpSync('src/dashboard/public', 'dist/dashboard/public', { recursive: true })\"",
52
- "test": "pnpm build && node --test test/*.test.mjs"
52
+ "test": "pnpm build && pnpm test:built",
53
+ "test:built": "node --test test/*.test.mjs"
53
54
  },
54
55
  "engines": {
55
56
  "node": ">=24.0.0"