@noy-db/cli 0.2.0-pre.3 → 0.2.0-pre.31

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.cjs DELETED
@@ -1,941 +0,0 @@
1
- "use strict";
2
- var __create = Object.create;
3
- var __defProp = Object.defineProperty;
4
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
- var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
- var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __export = (target, all) => {
9
- for (var name in all)
10
- __defProp(target, name, { get: all[name], enumerable: true });
11
- };
12
- var __copyProps = (to, from, except, desc) => {
13
- if (from && typeof from === "object" || typeof from === "function") {
14
- for (let key of __getOwnPropNames(from))
15
- if (!__hasOwnProp.call(to, key) && key !== except)
16
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
- }
18
- return to;
19
- };
20
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
- // If the importer is in node compatibility mode or this is not an ESM
22
- // file that has been converted to a CommonJS file using a Babel-
23
- // compatible transform (i.e. "__esModule" has not been set), then set
24
- // "default" to the CommonJS "module.exports" for node compatibility.
25
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
- mod
27
- ));
28
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
-
30
- // src/index.ts
31
- var src_exports = {};
32
- __export(src_exports, {
33
- describeBundle: () => describeBundle,
34
- formatSnapshot: () => formatSnapshot,
35
- inspect: () => inspect,
36
- loadOptionsFromFile: () => loadOptionsFromFile,
37
- runConfigScaffold: () => runConfigScaffold,
38
- runConfigValidate: () => runConfigValidate,
39
- runDescribe: () => runDescribe,
40
- runInspect: () => runInspect,
41
- runMonitor: () => runMonitor,
42
- runVerify: () => runVerify,
43
- scaffold: () => scaffold,
44
- validateOptions: () => validateOptions,
45
- verify: () => verify
46
- });
47
- module.exports = __toCommonJS(src_exports);
48
-
49
- // src/commands/inspect.ts
50
- var import_hub = require("@noy-db/hub");
51
- var import_promises = require("fs/promises");
52
- async function inspect(filePath) {
53
- const bytes = await (0, import_promises.readFile)(filePath);
54
- const header = (0, import_hub.readNoydbBundleHeader)(new Uint8Array(bytes));
55
- return {
56
- formatVersion: header.formatVersion,
57
- handle: header.handle,
58
- bodyBytes: header.bodyBytes,
59
- bodySha256: header.bodySha256
60
- };
61
- }
62
- async function runInspect(argv) {
63
- const file = argv[0];
64
- if (!file) {
65
- process.stderr.write("usage: noydb inspect <file.noydb>\n");
66
- return 2;
67
- }
68
- try {
69
- const info = await inspect(file);
70
- process.stdout.write(JSON.stringify(info, null, 2) + "\n");
71
- return 0;
72
- } catch (err) {
73
- process.stderr.write(`inspect failed: ${err.message}
74
- `);
75
- return 1;
76
- }
77
- }
78
-
79
- // src/commands/verify.ts
80
- var import_hub2 = require("@noy-db/hub");
81
- var import_promises2 = require("fs/promises");
82
- async function verify(filePath) {
83
- const bytes = new Uint8Array(await (0, import_promises2.readFile)(filePath));
84
- const checks = { magic: false, header: false, bodyHash: false };
85
- let handle = "";
86
- let bodyBytes = 0;
87
- try {
88
- const header = (0, import_hub2.readNoydbBundleHeader)(bytes);
89
- checks.magic = true;
90
- checks.header = true;
91
- handle = header.handle;
92
- bodyBytes = header.bodyBytes;
93
- await (0, import_hub2.readNoydbBundle)(bytes);
94
- checks.bodyHash = true;
95
- return { ok: true, file: filePath, handle, bodyBytes, checks };
96
- } catch (err) {
97
- return {
98
- ok: false,
99
- file: filePath,
100
- handle,
101
- bodyBytes,
102
- checks,
103
- error: err.message
104
- };
105
- }
106
- }
107
- async function runVerify(argv) {
108
- const file = argv[0];
109
- if (!file) {
110
- process.stderr.write("usage: noydb verify <file.noydb>\n");
111
- return 2;
112
- }
113
- const report = await verify(file);
114
- process.stdout.write(JSON.stringify(report, null, 2) + "\n");
115
- return report.ok ? 0 : 1;
116
- }
117
-
118
- // src/commands/config.ts
119
- var import_node_url = require("url");
120
- var import_node_path = require("path");
121
- function safeStringify(v) {
122
- if (typeof v === "string") return v;
123
- if (typeof v === "number" || typeof v === "boolean" || v === null || v === void 0) {
124
- return String(v);
125
- }
126
- try {
127
- return JSON.stringify(v);
128
- } catch {
129
- return "<unserializable>";
130
- }
131
- }
132
- function validateOptions(opts) {
133
- const issues = [];
134
- if (!isRecord(opts)) {
135
- issues.push({
136
- severity: "error",
137
- code: "not-object",
138
- path: "<root>",
139
- message: "NoydbOptions must be an object"
140
- });
141
- return { ok: false, issues };
142
- }
143
- if (!opts.store) {
144
- issues.push({
145
- severity: "error",
146
- code: "missing-store",
147
- path: "store",
148
- message: "`store` is required"
149
- });
150
- } else if (!isStoreShape(opts.store)) {
151
- issues.push({
152
- severity: "error",
153
- code: "bad-store-shape",
154
- path: "store",
155
- message: "`store` does not expose the 6-method NoydbStore contract"
156
- });
157
- }
158
- if (opts.sync !== void 0) {
159
- const targets = normalizeSync(opts.sync);
160
- targets.forEach((t, i) => validateTarget(t, `sync[${i}]`, issues));
161
- }
162
- if (opts.syncPolicy !== void 0 && opts.sync === void 0) {
163
- issues.push({
164
- severity: "warn",
165
- code: "policy-without-sync",
166
- path: "syncPolicy",
167
- message: "`syncPolicy` has no effect without a `sync` target"
168
- });
169
- }
170
- if (typeof opts.user !== "string" || !opts.user) {
171
- issues.push({
172
- severity: "warn",
173
- code: "missing-user",
174
- path: "user",
175
- message: '`user` identifier is recommended \u2014 audit entries default to "anonymous" without it'
176
- });
177
- }
178
- if (opts.secret === void 0 && opts.passphrase === void 0 && opts.encrypt !== false) {
179
- issues.push({
180
- severity: "warn",
181
- code: "no-secret",
182
- path: "secret",
183
- message: "`secret` / `passphrase` missing and `encrypt` not explicitly false \u2014 vault open will fail"
184
- });
185
- }
186
- const hasError = issues.some((i) => i.severity === "error");
187
- return { ok: !hasError, issues };
188
- }
189
- async function loadOptionsFromFile(filePath) {
190
- const abs = (0, import_node_path.resolve)(filePath);
191
- if (abs.endsWith(".ts") || abs.endsWith(".mts") || abs.endsWith(".cts")) {
192
- throw new Error(
193
- `TypeScript config files are not directly loadable \u2014 Node has no native .ts loader.
194
- Options:
195
- (a) compile first: tsc ${filePath} && noydb config validate ${filePath.replace(/\.[mc]?ts$/, ".js")}
196
- (b) run via tsx: npx tsx $(which noydb) config validate ${filePath}
197
- (c) rename to .mjs/.js if your config has no TS-only syntax`
198
- );
199
- }
200
- const mod = await import((0, import_node_url.pathToFileURL)(abs).href);
201
- const value = mod.default ?? mod;
202
- if (typeof value === "function") {
203
- return await value();
204
- }
205
- return value;
206
- }
207
- async function runConfigValidate(argv) {
208
- const file = argv[0];
209
- if (!file) {
210
- process.stderr.write("usage: noydb config validate <file.ts|js>\n");
211
- return 2;
212
- }
213
- let opts;
214
- try {
215
- opts = await loadOptionsFromFile(file);
216
- } catch (err) {
217
- process.stderr.write(`failed to load ${file}: ${err.message}
218
- `);
219
- return 1;
220
- }
221
- const report = validateOptions(opts);
222
- process.stdout.write(JSON.stringify(report, null, 2) + "\n");
223
- return report.ok ? 0 : 1;
224
- }
225
- function scaffold(profile) {
226
- switch (profile) {
227
- case "A":
228
- return {
229
- profile,
230
- notes: "Local-only single-user. No cloud.",
231
- code: [
232
- `import { createNoydb } from '@noy-db/hub'`,
233
- `import { jsonFile } from '@noy-db/to-file'`,
234
- ``,
235
- `export default {`,
236
- ` store: jsonFile({ dir: process.env.NOYDB_DATA_DIR ?? './data' }),`,
237
- ` user: process.env.NOYDB_USER ?? 'owner',`,
238
- ` secret: process.env.NOYDB_SECRET,`,
239
- `}`
240
- ].join("\n"),
241
- env: `NOYDB_USER=owner
242
- NOYDB_SECRET=
243
- NOYDB_DATA_DIR=./data
244
- `
245
- };
246
- case "B":
247
- return {
248
- profile,
249
- notes: "Offline-first + cloud mirror. Local authoritative, sync opportunistically.",
250
- code: [
251
- `import { createNoydb, INDEXED_STORE_POLICY } from '@noy-db/hub'`,
252
- `import { browserIdbStore } from '@noy-db/to-browser-idb'`,
253
- `import { awsDynamoStore } from '@noy-db/to-aws-dynamo'`,
254
- ``,
255
- `export default {`,
256
- ` store: browserIdbStore({ prefix: process.env.NOYDB_APP ?? 'myapp' }),`,
257
- ` sync: [{`,
258
- ` store: awsDynamoStore({`,
259
- ` table: process.env.NOYDB_DYNAMO_TABLE!,`,
260
- ` region: process.env.AWS_REGION ?? 'us-east-1',`,
261
- ` }),`,
262
- ` role: 'sync-peer',`,
263
- ` label: 'dynamo-live',`,
264
- ` }],`,
265
- ` syncPolicy: INDEXED_STORE_POLICY,`,
266
- ` user: process.env.NOYDB_USER ?? 'owner',`,
267
- ` secret: process.env.NOYDB_SECRET,`,
268
- `}`
269
- ].join("\n"),
270
- env: `NOYDB_USER=owner
271
- NOYDB_SECRET=
272
- NOYDB_APP=myapp
273
- NOYDB_DYNAMO_TABLE=myapp-live
274
- AWS_REGION=us-east-1
275
- `
276
- };
277
- case "C":
278
- return {
279
- profile,
280
- notes: "Records + blobs split (routeStore). Dynamo for records, S3 for blobs.",
281
- code: [
282
- `import { createNoydb, routeStore } from '@noy-db/hub'`,
283
- `import { awsDynamoStore } from '@noy-db/to-aws-dynamo'`,
284
- `import { awsS3Store } from '@noy-db/to-aws-s3'`,
285
- ``,
286
- `export default {`,
287
- ` store: routeStore({`,
288
- ` default: awsDynamoStore({`,
289
- ` table: process.env.NOYDB_DYNAMO_TABLE!,`,
290
- ` region: process.env.AWS_REGION ?? 'us-east-1',`,
291
- ` }),`,
292
- ` blobs: awsS3Store({`,
293
- ` bucket: process.env.NOYDB_S3_BUCKET!,`,
294
- ` region: process.env.AWS_REGION ?? 'us-east-1',`,
295
- ` }),`,
296
- ` }),`,
297
- ` user: process.env.NOYDB_USER ?? 'owner',`,
298
- ` secret: process.env.NOYDB_SECRET,`,
299
- `}`
300
- ].join("\n"),
301
- env: `NOYDB_USER=owner
302
- NOYDB_SECRET=
303
- NOYDB_DYNAMO_TABLE=myapp-records
304
- NOYDB_S3_BUCKET=myapp-blobs
305
- AWS_REGION=us-east-1
306
- `
307
- };
308
- case "G":
309
- return {
310
- profile,
311
- notes: "Middleware-hardened production: retry + breaker + cache + metrics.",
312
- code: [
313
- `import { createNoydb, wrapStore, withRetry, withCircuitBreaker, withCache, withHealthCheck, withMetrics } from '@noy-db/hub'`,
314
- `import { awsDynamoStore } from '@noy-db/to-aws-dynamo'`,
315
- ``,
316
- `export default {`,
317
- ` store: wrapStore(`,
318
- ` awsDynamoStore({ table: process.env.NOYDB_DYNAMO_TABLE! }),`,
319
- ` withRetry({ maxRetries: 3 }),`,
320
- ` withCircuitBreaker({ failureThreshold: 5, resetTimeoutMs: 30_000 }),`,
321
- ` withCache({ ttlMs: 60_000 }),`,
322
- ` withHealthCheck(),`,
323
- ` withMetrics({ onOperation: op => console.log(op) }),`,
324
- ` ),`,
325
- ` user: process.env.NOYDB_USER ?? 'owner',`,
326
- ` secret: process.env.NOYDB_SECRET,`,
327
- `}`
328
- ].join("\n"),
329
- env: `NOYDB_USER=owner
330
- NOYDB_SECRET=
331
- NOYDB_DYNAMO_TABLE=myapp-prod
332
- `
333
- };
334
- case "D":
335
- return {
336
- profile,
337
- notes: "Hot + cold tiered. Records age out to archive after N days.",
338
- code: [
339
- `import { createNoydb, routeStore } from '@noy-db/hub'`,
340
- `import { awsDynamoStore } from '@noy-db/to-aws-dynamo'`,
341
- `import { awsS3Store } from '@noy-db/to-aws-s3'`,
342
- ``,
343
- `export default {`,
344
- ` store: routeStore({`,
345
- ` default: awsDynamoStore({ table: process.env.NOYDB_DYNAMO_TABLE! }),`,
346
- ` age: {`,
347
- ` cold: awsS3Store({ bucket: process.env.NOYDB_S3_COLD! }),`,
348
- ` coldAfterDays: Number(process.env.NOYDB_COLD_AFTER_DAYS ?? '90'),`,
349
- ` },`,
350
- ` }),`,
351
- ` user: process.env.NOYDB_USER ?? 'owner',`,
352
- ` secret: process.env.NOYDB_SECRET,`,
353
- `}`
354
- ].join("\n"),
355
- env: `NOYDB_USER=owner
356
- NOYDB_SECRET=
357
- NOYDB_DYNAMO_TABLE=myapp-hot
358
- NOYDB_S3_COLD=myapp-archive
359
- NOYDB_COLD_AFTER_DAYS=90
360
- `
361
- };
362
- case "E":
363
- return {
364
- profile,
365
- notes: "Multi-peer team sync. Primary + peer + backup + archive.",
366
- code: [
367
- `import { createNoydb } from '@noy-db/hub'`,
368
- `import { browserIdbStore } from '@noy-db/to-browser-idb'`,
369
- `import { awsDynamoStore } from '@noy-db/to-aws-dynamo'`,
370
- `import { awsS3Store } from '@noy-db/to-aws-s3'`,
371
- ``,
372
- `export default {`,
373
- ` store: browserIdbStore({ prefix: process.env.NOYDB_APP ?? 'team' }),`,
374
- ` sync: [`,
375
- ` { store: awsDynamoStore({ table: process.env.NOYDB_DYNAMO_TABLE! }), role: 'sync-peer', label: 'team-hot' },`,
376
- ` { store: awsS3Store({ bucket: process.env.NOYDB_S3_BACKUP! }), role: 'backup', label: 'team-backup' },`,
377
- ` { store: awsS3Store({ bucket: process.env.NOYDB_S3_ARCHIVE! }), role: 'archive', label: 'team-archive' },`,
378
- ` ],`,
379
- ` user: process.env.NOYDB_USER ?? 'member',`,
380
- ` secret: process.env.NOYDB_SECRET,`,
381
- `}`
382
- ].join("\n"),
383
- env: `NOYDB_USER=member
384
- NOYDB_SECRET=
385
- NOYDB_APP=team
386
- NOYDB_DYNAMO_TABLE=team-hot
387
- NOYDB_S3_BACKUP=team-backup
388
- NOYDB_S3_ARCHIVE=team-archive
389
- `
390
- };
391
- case "F":
392
- return {
393
- profile,
394
- notes: "CRDT collaboration \u2014 Yjs-backed shared records over the encrypted envelope.",
395
- code: [
396
- `import { createNoydb } from '@noy-db/hub'`,
397
- `import { browserIdbStore } from '@noy-db/to-browser-idb'`,
398
- `import { awsDynamoStore } from '@noy-db/to-aws-dynamo'`,
399
- `// import { yjsCollection } from '@noy-db/in-yjs' // use inside your app`,
400
- ``,
401
- `export default {`,
402
- ` store: browserIdbStore({ prefix: 'collab' }),`,
403
- ` sync: [{ store: awsDynamoStore({ table: process.env.NOYDB_DYNAMO_TABLE! }), role: 'sync-peer' }],`,
404
- ` user: process.env.NOYDB_USER!,`,
405
- ` secret: process.env.NOYDB_SECRET,`,
406
- `}`,
407
- ``,
408
- `// After createNoydb(), replace normal collections with yjsCollection() for CRDT fields.`
409
- ].join("\n"),
410
- env: `NOYDB_USER=
411
- NOYDB_SECRET=
412
- NOYDB_DYNAMO_TABLE=collab-live
413
- `
414
- };
415
- case "H":
416
- return {
417
- profile,
418
- notes: "USB-portable \u2014 everything on a single file store, no cloud.",
419
- code: [
420
- `import { createNoydb } from '@noy-db/hub'`,
421
- `import { jsonFile } from '@noy-db/to-file'`,
422
- ``,
423
- `export default {`,
424
- ` store: jsonFile({ dir: process.env.NOYDB_DATA_DIR ?? '/Volumes/MY_USB/data' }),`,
425
- ` user: process.env.NOYDB_USER!,`,
426
- ` secret: process.env.NOYDB_SECRET,`,
427
- `}`
428
- ].join("\n"),
429
- env: `NOYDB_USER=
430
- NOYDB_SECRET=
431
- NOYDB_DATA_DIR=/Volumes/MY_USB/data
432
- `
433
- };
434
- case "I":
435
- return {
436
- profile,
437
- notes: "Multi-tenant geographic sharding \u2014 per-vault primary per region.",
438
- code: [
439
- `import { createNoydb } from '@noy-db/hub'`,
440
- `import { awsDynamoStore } from '@noy-db/to-aws-dynamo'`,
441
- ``,
442
- `// Pick the nearest region at init time based on tenant config.`,
443
- `const region = process.env.NOYDB_REGION ?? 'ap-southeast-1'`,
444
- `const table = \`myapp-\${region}\``,
445
- ``,
446
- `export default {`,
447
- ` store: awsDynamoStore({ table, region }),`,
448
- ` user: process.env.NOYDB_USER!,`,
449
- ` secret: process.env.NOYDB_SECRET,`,
450
- `}`
451
- ].join("\n"),
452
- env: `NOYDB_USER=
453
- NOYDB_SECRET=
454
- NOYDB_REGION=ap-southeast-1
455
- `
456
- };
457
- case "J":
458
- return {
459
- profile,
460
- notes: "Authentication bridge (passphrase-less unlock via OIDC or WebAuthn).",
461
- code: [
462
- `import { createNoydb } from '@noy-db/hub'`,
463
- `import { browserIdbStore } from '@noy-db/to-browser-idb'`,
464
- `// import { unlockWebAuthn } from '@noy-db/on-webauthn' // in the browser`,
465
- `// import { keyConnector } from '@noy-db/on-oidc' // in a server app`,
466
- ``,
467
- `export default {`,
468
- ` store: browserIdbStore({ prefix: process.env.NOYDB_APP ?? 'app' }),`,
469
- ` user: process.env.NOYDB_USER!,`,
470
- ` // secret supplied by the unlock method at openVault() time, not here.`,
471
- `}`
472
- ].join("\n"),
473
- env: `NOYDB_USER=
474
- NOYDB_APP=app
475
- `
476
- };
477
- }
478
- }
479
- async function runConfigScaffold(argv) {
480
- const profileArg = argv.find((a) => a.startsWith("--profile="));
481
- const profile = profileArg?.split("=")[1] ?? "A";
482
- if (!/^[A-J]$/.test(profile)) {
483
- process.stderr.write(`unknown profile: ${profile}. Valid: A-J (see docs/guides/topology-matrix.md)
484
- `);
485
- return 2;
486
- }
487
- const out = scaffold(profile);
488
- process.stdout.write(`// \u2500\u2500 noydb config (profile ${out.profile}) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
489
- `);
490
- process.stdout.write(`// ${out.notes}
491
-
492
- `);
493
- process.stdout.write(out.code + "\n\n");
494
- if (out.env) {
495
- process.stdout.write(`// \u2500\u2500 .env template \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
496
- `);
497
- process.stdout.write(out.env);
498
- }
499
- return 0;
500
- }
501
- function isRecord(v) {
502
- return typeof v === "object" && v !== null && !Array.isArray(v);
503
- }
504
- function isStoreShape(v) {
505
- if (!isRecord(v)) return false;
506
- return ["get", "put", "delete", "list", "loadAll", "saveAll"].every((m) => typeof v[m] === "function");
507
- }
508
- function normalizeSync(v) {
509
- if (Array.isArray(v)) return v;
510
- return [v];
511
- }
512
- function validateTarget(t, path, issues) {
513
- if (!isRecord(t)) {
514
- issues.push({
515
- severity: "error",
516
- code: "bad-target",
517
- path,
518
- message: "sync target must be an object with { store, role }"
519
- });
520
- return;
521
- }
522
- if (t["role"] === void 0) {
523
- if (!isStoreShape(t)) {
524
- issues.push({
525
- severity: "error",
526
- code: "bad-store-shape",
527
- path,
528
- message: "sync target store does not expose the 6-method contract"
529
- });
530
- }
531
- return;
532
- }
533
- const validRoles = ["sync-peer", "backup", "archive"];
534
- if (!validRoles.includes(safeStringify(t["role"]))) {
535
- issues.push({
536
- severity: "error",
537
- code: "bad-role",
538
- path: `${path}.role`,
539
- message: `role must be one of ${validRoles.join(", ")}; got ${safeStringify(t["role"])}`
540
- });
541
- }
542
- if (!t["store"] || !isStoreShape(t["store"])) {
543
- issues.push({
544
- severity: "error",
545
- code: "bad-store-shape",
546
- path: `${path}.store`,
547
- message: "sync target store does not expose the 6-method contract"
548
- });
549
- }
550
- if (t["role"] === "archive" && isRecord(t["policy"]) && isRecord(t["policy"]["pull"])) {
551
- issues.push({
552
- severity: "error",
553
- code: "archive-pull-configured",
554
- path: `${path}.policy.pull`,
555
- message: "archive targets are push-only \u2014 pull policy is invalid"
556
- });
557
- }
558
- }
559
-
560
- // src/commands/monitor.ts
561
- var import_to_meter = require("@noy-db/to-meter");
562
- async function runMonitor(argv) {
563
- const file = argv[0];
564
- if (!file) {
565
- process.stderr.write("usage: noydb monitor <config.ts> [--interval=ms]\n");
566
- return 2;
567
- }
568
- const intervalArg = argv.find((a) => a.startsWith("--interval="));
569
- const intervalMs = intervalArg ? parseInt(intervalArg.split("=")[1] ?? "5000", 10) : 5e3;
570
- let opts;
571
- try {
572
- const loaded = await loadOptionsFromFile(file);
573
- if (typeof loaded !== "object" || loaded === null) {
574
- process.stderr.write(`config file must export a NoydbOptions-shaped object
575
- `);
576
- return 1;
577
- }
578
- opts = loaded;
579
- } catch (err) {
580
- process.stderr.write(`failed to load ${file}: ${err.message}
581
- `);
582
- return 1;
583
- }
584
- const innerStore = opts["store"];
585
- if (!innerStore || typeof innerStore !== "object") {
586
- process.stderr.write("config has no `store` \u2014 nothing to monitor\n");
587
- return 1;
588
- }
589
- const { store: metered, meter } = (0, import_to_meter.toMeter)(innerStore, {
590
- degradedMs: 500,
591
- onDegraded: (e) => process.stderr.write(`DEGRADED: ${e.reason}
592
- `),
593
- onRestored: (e) => process.stderr.write(`RESTORED: ${e.reason}
594
- `)
595
- });
596
- const liveOpts = { ...opts, store: metered };
597
- const hub = await import("@noy-db/hub");
598
- await hub.createNoydb(liveOpts);
599
- process.stdout.write(`monitoring ${file} \u2014 interval ${intervalMs}ms \u2014 Ctrl-C to stop
600
-
601
- `);
602
- const stop = installSigintHandler(meter);
603
- return new Promise((resolveP) => {
604
- const timer = setInterval(() => {
605
- if (stop.signalled) {
606
- clearInterval(timer);
607
- meter.close();
608
- resolveP(0);
609
- return;
610
- }
611
- const snap = meter.snapshot();
612
- process.stdout.write(formatSnapshot(snap) + "\n");
613
- }, intervalMs);
614
- });
615
- }
616
- function installSigintHandler(meter) {
617
- const state = { signalled: false };
618
- const handler = () => {
619
- state.signalled = true;
620
- meter.close();
621
- };
622
- process.on("SIGINT", handler);
623
- process.on("SIGTERM", handler);
624
- return state;
625
- }
626
- function formatSnapshot(snap) {
627
- const lines = [];
628
- const ts = new Date(snap.collectedAt).toISOString().slice(11, 19);
629
- lines.push(`[${ts}] status=${snap.status} calls=${snap.totalCalls} casConflicts=${snap.casConflicts} windowMs=${snap.windowMs}`);
630
- for (const m of ["get", "put", "delete", "list", "loadAll", "saveAll"]) {
631
- const s = snap.byMethod[m];
632
- if (s.count === 0) continue;
633
- lines.push(` ${m.padEnd(7)} count=${s.count} errors=${s.errors} p50=${s.p50}ms p99=${s.p99}ms max=${s.max}ms avg=${s.avg}ms`);
634
- }
635
- return lines.join("\n");
636
- }
637
-
638
- // src/commands/describe.ts
639
- var import_promises3 = require("fs/promises");
640
- var import_node_path2 = require("path");
641
- var import_node_crypto = require("crypto");
642
- var import_yaml = require("yaml");
643
- var import_hub3 = require("@noy-db/hub");
644
- var VERSION = "0.1.0";
645
- function memoryStore() {
646
- const data = /* @__PURE__ */ new Map();
647
- const getColl = (v, c) => {
648
- let vm = data.get(v);
649
- if (!vm) {
650
- vm = /* @__PURE__ */ new Map();
651
- data.set(v, vm);
652
- }
653
- let cm = vm.get(c);
654
- if (!cm) {
655
- cm = /* @__PURE__ */ new Map();
656
- vm.set(c, cm);
657
- }
658
- return cm;
659
- };
660
- return {
661
- async get(v, c, id) {
662
- return data.get(v)?.get(c)?.get(id) ?? null;
663
- },
664
- async put(v, c, id, env, ev) {
665
- const coll = getColl(v, c);
666
- const ex = coll.get(id);
667
- if (ev !== void 0 && ex && ex._v !== ev) throw new import_hub3.ConflictError(ex._v);
668
- coll.set(id, env);
669
- },
670
- async delete(v, c, id) {
671
- data.get(v)?.get(c)?.delete(id);
672
- },
673
- async list(v, c) {
674
- return [...data.get(v)?.get(c)?.keys() ?? []];
675
- },
676
- async loadAll(v) {
677
- const vm = data.get(v);
678
- const snap = {};
679
- if (vm) for (const [cn, cm] of vm) {
680
- if (cn.startsWith("_")) continue;
681
- const r = {};
682
- for (const [id, e] of cm) r[id] = e;
683
- snap[cn] = r;
684
- }
685
- return snap;
686
- },
687
- async saveAll(v, snap) {
688
- for (const [cn, recs] of Object.entries(snap)) {
689
- const coll = getColl(v, cn);
690
- for (const [id, env] of Object.entries(recs)) coll.set(id, env);
691
- }
692
- }
693
- };
694
- }
695
- async function describeBundle(opts) {
696
- const bytes = await (0, import_promises3.readFile)(opts.bundlePath);
697
- const sourceSha256 = (0, import_node_crypto.createHash)("sha256").update(bytes).digest("hex");
698
- const { dumpJson } = await (0, import_hub3.readNoydbBundle)(new Uint8Array(bytes));
699
- const backup = JSON.parse(dumpJson);
700
- const compartmentName = backup._compartment ?? "vault";
701
- const db = await (0, import_hub3.createNoydb)({
702
- store: memoryStore(),
703
- user: opts.user,
704
- secret: opts.passphrase
705
- });
706
- const vault = await db.openVault(compartmentName);
707
- await vault.load(dumpJson);
708
- const collectionNames = await vault.collections();
709
- for (const name of collectionNames) {
710
- if (!name.startsWith("_")) vault.collection(name);
711
- }
712
- const snapshot = await vault.dumpSchema({
713
- withStats: opts.withStats,
714
- sampleSize: opts.sampleSize
715
- });
716
- const enriched = opts.schemas === "full" ? await enrichWithFullSchemas(snapshot, vault, collectionNames) : snapshot;
717
- const { _noydb_snapshot: _ignore, ...rest } = enriched;
718
- void _ignore;
719
- const withProvenance = {
720
- _noydb_snapshot: 1,
721
- _provenance: {
722
- generatedBy: `noydb describe v${VERSION}`,
723
- source: (0, import_node_path2.basename)(opts.bundlePath),
724
- sourceSha256,
725
- emittedAt: enriched.emittedAt
726
- },
727
- vault: rest.vault,
728
- emittedAt: rest.emittedAt,
729
- subsystems: rest.subsystems,
730
- collections: rest.collections,
731
- materializedViews: rest.materializedViews,
732
- overlayViews: rest.overlayViews,
733
- derivations: rest.derivations,
734
- ...rest.internal !== void 0 ? { internal: rest.internal } : {},
735
- ...rest.aclRoles !== void 0 ? { aclRoles: rest.aclRoles } : {}
736
- };
737
- if (opts.schemas === "sidecar" && opts.outPath) {
738
- await writeSidecarSchemas(opts.outPath, vault, collectionNames);
739
- }
740
- const emitted = emit(withProvenance, opts.format);
741
- if (opts.outPath) {
742
- await (0, import_promises3.mkdir)((0, import_node_path2.dirname)(opts.outPath), { recursive: true });
743
- await (0, import_promises3.writeFile)(opts.outPath, emitted);
744
- }
745
- return emitted;
746
- }
747
- async function enrichWithFullSchemas(snapshot, vault, collectionNames) {
748
- const state = vault._introspectState();
749
- const collections = {};
750
- for (const name of Object.keys(snapshot.collections)) {
751
- const base = snapshot.collections[name];
752
- if (!base) continue;
753
- if (!collectionNames.includes(name)) {
754
- collections[name] = base;
755
- continue;
756
- }
757
- try {
758
- const dek = await state.getDEK(name);
759
- const persisted = await (0, import_hub3.loadPersistedSchema)(state.adapter, state.name, name, dek);
760
- if (persisted?.jsonSchema) {
761
- collections[name] = { ...base, jsonSchema: persisted.jsonSchema };
762
- } else {
763
- collections[name] = base;
764
- }
765
- } catch {
766
- collections[name] = base;
767
- }
768
- }
769
- return { ...snapshot, collections };
770
- }
771
- async function writeSidecarSchemas(outPath, vault, collectionNames) {
772
- const state = vault._introspectState();
773
- const sidecarDir = `${outPath}.schemas`;
774
- await (0, import_promises3.mkdir)(sidecarDir, { recursive: true });
775
- for (const name of collectionNames) {
776
- if (name.startsWith("_")) continue;
777
- try {
778
- const dek = await state.getDEK(name);
779
- const persisted = await (0, import_hub3.loadPersistedSchema)(state.adapter, state.name, name, dek);
780
- if (persisted?.jsonSchema) {
781
- await (0, import_promises3.writeFile)(
782
- (0, import_node_path2.join)(sidecarDir, `${name}.schema.json`),
783
- JSON.stringify(persisted.jsonSchema, null, 2)
784
- );
785
- }
786
- } catch {
787
- }
788
- }
789
- }
790
- function emit(snapshot, format) {
791
- if (format === "json") {
792
- return JSON.stringify(snapshot, null, 2) + "\n";
793
- }
794
- return (0, import_yaml.stringify)(snapshot, {
795
- aliasDuplicateObjects: false,
796
- indent: 2,
797
- lineWidth: 0
798
- // never wrap; preserves long strings
799
- });
800
- }
801
- function parseArgs(argv) {
802
- const out = {
803
- bundlePath: "",
804
- format: "yaml",
805
- withStats: false,
806
- schemas: "none",
807
- sampleSize: 50
808
- };
809
- let i = 0;
810
- while (i < argv.length) {
811
- const arg = argv[i];
812
- if (arg === "--format") {
813
- const v = argv[++i];
814
- if (v !== "yaml" && v !== "json") return { error: `--format must be yaml or json (got: ${v ?? "<none>"})` };
815
- out.format = v;
816
- } else if (arg === "--with-stats") {
817
- out.withStats = true;
818
- } else if (arg === "--schemas") {
819
- const v = argv[++i];
820
- if (v !== "none" && v !== "full" && v !== "sidecar") {
821
- return { error: `--schemas must be none|full|sidecar (got: ${v ?? "<none>"})` };
822
- }
823
- out.schemas = v;
824
- } else if (arg === "-o" || arg === "--out") {
825
- const v = argv[++i];
826
- if (v === void 0) return { error: "-o requires a value" };
827
- out.outPath = v;
828
- } else if (arg === "--passphrase") {
829
- const v = argv[++i];
830
- if (v === void 0) return { error: "--passphrase requires a value" };
831
- out.passphrase = v;
832
- } else if (arg === "--user") {
833
- const v = argv[++i];
834
- if (v === void 0) return { error: "--user requires a value" };
835
- out.user = v;
836
- } else if (arg === "--sample") {
837
- const n = parseInt(argv[++i] ?? "", 10);
838
- if (Number.isNaN(n) || n < 0) return { error: `--sample must be a non-negative integer` };
839
- out.sampleSize = n;
840
- } else if (arg === "-h" || arg === "--help") {
841
- return { error: "help" };
842
- } else if (arg?.startsWith("-")) {
843
- return { error: `unknown option: ${arg}` };
844
- } else if (!out.bundlePath) {
845
- out.bundlePath = arg ?? "";
846
- } else {
847
- return { error: `unexpected positional arg: ${arg}` };
848
- }
849
- i++;
850
- }
851
- if (!out.bundlePath) return { error: "missing bundle path" };
852
- return out;
853
- }
854
- function usage() {
855
- return [
856
- "usage: noydb describe <bundle-path> [options]",
857
- "",
858
- " --format <yaml|json> output format (default: yaml)",
859
- " --with-stats include records / bytes / oldest / newest per collection",
860
- " --schemas <mode> JSON Schema body inclusion (default: none)",
861
- " - none: summary only",
862
- " - full: inline complete JSON Schema per collection",
863
- " - sidecar: write `<out>.schemas/<col>.schema.json` siblings",
864
- " -o, --out <file> write to file instead of stdout",
865
- " --user <userId> keyring user identifier (required)",
866
- " --passphrase <p> decryption passphrase (or via NOYDB_PASSPHRASE env)",
867
- " --sample <n> max records sampled when no persisted/live schema (default: 50)",
868
- "",
869
- "Exit codes: 0 ok, 1 internal error, 2 usage error, 3 auth failure"
870
- ].join("\n");
871
- }
872
- async function runDescribe(argv) {
873
- const parsed = parseArgs(argv);
874
- if ("error" in parsed) {
875
- if (parsed.error === "help") {
876
- process.stdout.write(usage() + "\n");
877
- return 0;
878
- }
879
- process.stderr.write(`describe: ${parsed.error}
880
-
881
- ${usage()}
882
- `);
883
- return 2;
884
- }
885
- const passphrase = parsed.passphrase ?? process.env.NOYDB_PASSPHRASE;
886
- if (!passphrase) {
887
- process.stderr.write("describe: passphrase required \u2014 pass --passphrase or set NOYDB_PASSPHRASE env\n");
888
- return 3;
889
- }
890
- if (parsed.passphrase) {
891
- process.stderr.write(
892
- "[noy-db] warning: --passphrase appears in shell history; consider NOYDB_PASSPHRASE env instead\n"
893
- );
894
- }
895
- if (!parsed.user) {
896
- process.stderr.write("describe: --user <userId> is required to unlock the bundle\n");
897
- return 2;
898
- }
899
- try {
900
- const out = await describeBundle({
901
- bundlePath: parsed.bundlePath,
902
- user: parsed.user,
903
- passphrase,
904
- format: parsed.format,
905
- withStats: parsed.withStats,
906
- schemas: parsed.schemas,
907
- ...parsed.outPath !== void 0 ? { outPath: parsed.outPath } : {},
908
- sampleSize: parsed.sampleSize
909
- });
910
- if (!parsed.outPath) process.stdout.write(out);
911
- return 0;
912
- } catch (err) {
913
- const msg = err instanceof Error ? err.message : String(err);
914
- if (/decrypt|tamper|wrong key|invalid/i.test(msg)) {
915
- process.stderr.write(`describe: decryption failed \u2014 check --user and --passphrase
916
- ${msg}
917
- `);
918
- return 3;
919
- }
920
- process.stderr.write(`describe: ${msg}
921
- `);
922
- return 1;
923
- }
924
- }
925
- // Annotate the CommonJS export names for ESM import in node:
926
- 0 && (module.exports = {
927
- describeBundle,
928
- formatSnapshot,
929
- inspect,
930
- loadOptionsFromFile,
931
- runConfigScaffold,
932
- runConfigValidate,
933
- runDescribe,
934
- runInspect,
935
- runMonitor,
936
- runVerify,
937
- scaffold,
938
- validateOptions,
939
- verify
940
- });
941
- //# sourceMappingURL=index.cjs.map