@noy-db/cli 0.1.0-pre.3

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.js ADDED
@@ -0,0 +1,602 @@
1
+ // src/commands/inspect.ts
2
+ import { readNoydbBundleHeader } from "@noy-db/hub";
3
+ import { readFile } from "fs/promises";
4
+ async function inspect(filePath) {
5
+ const bytes = await readFile(filePath);
6
+ const header = readNoydbBundleHeader(new Uint8Array(bytes));
7
+ return {
8
+ formatVersion: header.formatVersion,
9
+ handle: header.handle,
10
+ bodyBytes: header.bodyBytes,
11
+ bodySha256: header.bodySha256
12
+ };
13
+ }
14
+ async function runInspect(argv) {
15
+ const file = argv[0];
16
+ if (!file) {
17
+ process.stderr.write("usage: noydb inspect <file.noydb>\n");
18
+ return 2;
19
+ }
20
+ try {
21
+ const info = await inspect(file);
22
+ process.stdout.write(JSON.stringify(info, null, 2) + "\n");
23
+ return 0;
24
+ } catch (err) {
25
+ process.stderr.write(`inspect failed: ${err.message}
26
+ `);
27
+ return 1;
28
+ }
29
+ }
30
+
31
+ // src/commands/verify.ts
32
+ import { readNoydbBundle, readNoydbBundleHeader as readNoydbBundleHeader2 } from "@noy-db/hub";
33
+ import { readFile as readFile2 } from "fs/promises";
34
+ async function verify(filePath) {
35
+ const bytes = new Uint8Array(await readFile2(filePath));
36
+ const checks = { magic: false, header: false, bodyHash: false };
37
+ let handle = "";
38
+ let bodyBytes = 0;
39
+ try {
40
+ const header = readNoydbBundleHeader2(bytes);
41
+ checks.magic = true;
42
+ checks.header = true;
43
+ handle = header.handle;
44
+ bodyBytes = header.bodyBytes;
45
+ await readNoydbBundle(bytes);
46
+ checks.bodyHash = true;
47
+ return { ok: true, file: filePath, handle, bodyBytes, checks };
48
+ } catch (err) {
49
+ return {
50
+ ok: false,
51
+ file: filePath,
52
+ handle,
53
+ bodyBytes,
54
+ checks,
55
+ error: err.message
56
+ };
57
+ }
58
+ }
59
+ async function runVerify(argv) {
60
+ const file = argv[0];
61
+ if (!file) {
62
+ process.stderr.write("usage: noydb verify <file.noydb>\n");
63
+ return 2;
64
+ }
65
+ const report = await verify(file);
66
+ process.stdout.write(JSON.stringify(report, null, 2) + "\n");
67
+ return report.ok ? 0 : 1;
68
+ }
69
+
70
+ // src/commands/config.ts
71
+ import { pathToFileURL } from "url";
72
+ import { resolve } from "path";
73
+ function safeStringify(v) {
74
+ if (typeof v === "string") return v;
75
+ if (typeof v === "number" || typeof v === "boolean" || v === null || v === void 0) {
76
+ return String(v);
77
+ }
78
+ try {
79
+ return JSON.stringify(v);
80
+ } catch {
81
+ return "<unserializable>";
82
+ }
83
+ }
84
+ function validateOptions(opts) {
85
+ const issues = [];
86
+ if (!isRecord(opts)) {
87
+ issues.push({
88
+ severity: "error",
89
+ code: "not-object",
90
+ path: "<root>",
91
+ message: "NoydbOptions must be an object"
92
+ });
93
+ return { ok: false, issues };
94
+ }
95
+ if (!opts.store) {
96
+ issues.push({
97
+ severity: "error",
98
+ code: "missing-store",
99
+ path: "store",
100
+ message: "`store` is required"
101
+ });
102
+ } else if (!isStoreShape(opts.store)) {
103
+ issues.push({
104
+ severity: "error",
105
+ code: "bad-store-shape",
106
+ path: "store",
107
+ message: "`store` does not expose the 6-method NoydbStore contract"
108
+ });
109
+ }
110
+ if (opts.sync !== void 0) {
111
+ const targets = normalizeSync(opts.sync);
112
+ targets.forEach((t, i) => validateTarget(t, `sync[${i}]`, issues));
113
+ }
114
+ if (opts.syncPolicy !== void 0 && opts.sync === void 0) {
115
+ issues.push({
116
+ severity: "warn",
117
+ code: "policy-without-sync",
118
+ path: "syncPolicy",
119
+ message: "`syncPolicy` has no effect without a `sync` target"
120
+ });
121
+ }
122
+ if (typeof opts.user !== "string" || !opts.user) {
123
+ issues.push({
124
+ severity: "warn",
125
+ code: "missing-user",
126
+ path: "user",
127
+ message: '`user` identifier is recommended \u2014 audit entries default to "anonymous" without it'
128
+ });
129
+ }
130
+ if (opts.secret === void 0 && opts.passphrase === void 0 && opts.encrypt !== false) {
131
+ issues.push({
132
+ severity: "warn",
133
+ code: "no-secret",
134
+ path: "secret",
135
+ message: "`secret` / `passphrase` missing and `encrypt` not explicitly false \u2014 vault open will fail"
136
+ });
137
+ }
138
+ const hasError = issues.some((i) => i.severity === "error");
139
+ return { ok: !hasError, issues };
140
+ }
141
+ async function loadOptionsFromFile(filePath) {
142
+ const abs = resolve(filePath);
143
+ if (abs.endsWith(".ts") || abs.endsWith(".mts") || abs.endsWith(".cts")) {
144
+ throw new Error(
145
+ `TypeScript config files are not directly loadable \u2014 Node has no native .ts loader.
146
+ Options:
147
+ (a) compile first: tsc ${filePath} && noydb config validate ${filePath.replace(/\.[mc]?ts$/, ".js")}
148
+ (b) run via tsx: npx tsx $(which noydb) config validate ${filePath}
149
+ (c) rename to .mjs/.js if your config has no TS-only syntax`
150
+ );
151
+ }
152
+ const mod = await import(pathToFileURL(abs).href);
153
+ const value = mod.default ?? mod;
154
+ if (typeof value === "function") {
155
+ return await value();
156
+ }
157
+ return value;
158
+ }
159
+ async function runConfigValidate(argv) {
160
+ const file = argv[0];
161
+ if (!file) {
162
+ process.stderr.write("usage: noydb config validate <file.ts|js>\n");
163
+ return 2;
164
+ }
165
+ let opts;
166
+ try {
167
+ opts = await loadOptionsFromFile(file);
168
+ } catch (err) {
169
+ process.stderr.write(`failed to load ${file}: ${err.message}
170
+ `);
171
+ return 1;
172
+ }
173
+ const report = validateOptions(opts);
174
+ process.stdout.write(JSON.stringify(report, null, 2) + "\n");
175
+ return report.ok ? 0 : 1;
176
+ }
177
+ function scaffold(profile) {
178
+ switch (profile) {
179
+ case "A":
180
+ return {
181
+ profile,
182
+ notes: "Local-only single-user. No cloud.",
183
+ code: [
184
+ `import { createNoydb } from '@noy-db/hub'`,
185
+ `import { jsonFile } from '@noy-db/to-file'`,
186
+ ``,
187
+ `export default {`,
188
+ ` store: jsonFile({ dir: process.env.NOYDB_DATA_DIR ?? './data' }),`,
189
+ ` user: process.env.NOYDB_USER ?? 'owner',`,
190
+ ` secret: process.env.NOYDB_SECRET,`,
191
+ `}`
192
+ ].join("\n"),
193
+ env: `NOYDB_USER=owner
194
+ NOYDB_SECRET=
195
+ NOYDB_DATA_DIR=./data
196
+ `
197
+ };
198
+ case "B":
199
+ return {
200
+ profile,
201
+ notes: "Offline-first + cloud mirror. Local authoritative, sync opportunistically.",
202
+ code: [
203
+ `import { createNoydb, INDEXED_STORE_POLICY } from '@noy-db/hub'`,
204
+ `import { browserIdbStore } from '@noy-db/to-browser-idb'`,
205
+ `import { awsDynamoStore } from '@noy-db/to-aws-dynamo'`,
206
+ ``,
207
+ `export default {`,
208
+ ` store: browserIdbStore({ prefix: process.env.NOYDB_APP ?? 'myapp' }),`,
209
+ ` sync: [{`,
210
+ ` store: awsDynamoStore({`,
211
+ ` table: process.env.NOYDB_DYNAMO_TABLE!,`,
212
+ ` region: process.env.AWS_REGION ?? 'us-east-1',`,
213
+ ` }),`,
214
+ ` role: 'sync-peer',`,
215
+ ` label: 'dynamo-live',`,
216
+ ` }],`,
217
+ ` syncPolicy: INDEXED_STORE_POLICY,`,
218
+ ` user: process.env.NOYDB_USER ?? 'owner',`,
219
+ ` secret: process.env.NOYDB_SECRET,`,
220
+ `}`
221
+ ].join("\n"),
222
+ env: `NOYDB_USER=owner
223
+ NOYDB_SECRET=
224
+ NOYDB_APP=myapp
225
+ NOYDB_DYNAMO_TABLE=myapp-live
226
+ AWS_REGION=us-east-1
227
+ `
228
+ };
229
+ case "C":
230
+ return {
231
+ profile,
232
+ notes: "Records + blobs split (routeStore). Dynamo for records, S3 for blobs.",
233
+ code: [
234
+ `import { createNoydb, routeStore } from '@noy-db/hub'`,
235
+ `import { awsDynamoStore } from '@noy-db/to-aws-dynamo'`,
236
+ `import { awsS3Store } from '@noy-db/to-aws-s3'`,
237
+ ``,
238
+ `export default {`,
239
+ ` store: routeStore({`,
240
+ ` default: awsDynamoStore({`,
241
+ ` table: process.env.NOYDB_DYNAMO_TABLE!,`,
242
+ ` region: process.env.AWS_REGION ?? 'us-east-1',`,
243
+ ` }),`,
244
+ ` blobs: awsS3Store({`,
245
+ ` bucket: process.env.NOYDB_S3_BUCKET!,`,
246
+ ` region: process.env.AWS_REGION ?? 'us-east-1',`,
247
+ ` }),`,
248
+ ` }),`,
249
+ ` user: process.env.NOYDB_USER ?? 'owner',`,
250
+ ` secret: process.env.NOYDB_SECRET,`,
251
+ `}`
252
+ ].join("\n"),
253
+ env: `NOYDB_USER=owner
254
+ NOYDB_SECRET=
255
+ NOYDB_DYNAMO_TABLE=myapp-records
256
+ NOYDB_S3_BUCKET=myapp-blobs
257
+ AWS_REGION=us-east-1
258
+ `
259
+ };
260
+ case "G":
261
+ return {
262
+ profile,
263
+ notes: "Middleware-hardened production: retry + breaker + cache + metrics.",
264
+ code: [
265
+ `import { createNoydb, wrapStore, withRetry, withCircuitBreaker, withCache, withHealthCheck, withMetrics } from '@noy-db/hub'`,
266
+ `import { awsDynamoStore } from '@noy-db/to-aws-dynamo'`,
267
+ ``,
268
+ `export default {`,
269
+ ` store: wrapStore(`,
270
+ ` awsDynamoStore({ table: process.env.NOYDB_DYNAMO_TABLE! }),`,
271
+ ` withRetry({ maxRetries: 3 }),`,
272
+ ` withCircuitBreaker({ failureThreshold: 5, resetTimeoutMs: 30_000 }),`,
273
+ ` withCache({ ttlMs: 60_000 }),`,
274
+ ` withHealthCheck(),`,
275
+ ` withMetrics({ onOperation: op => console.log(op) }),`,
276
+ ` ),`,
277
+ ` user: process.env.NOYDB_USER ?? 'owner',`,
278
+ ` secret: process.env.NOYDB_SECRET,`,
279
+ `}`
280
+ ].join("\n"),
281
+ env: `NOYDB_USER=owner
282
+ NOYDB_SECRET=
283
+ NOYDB_DYNAMO_TABLE=myapp-prod
284
+ `
285
+ };
286
+ case "D":
287
+ return {
288
+ profile,
289
+ notes: "Hot + cold tiered. Records age out to archive after N days.",
290
+ code: [
291
+ `import { createNoydb, routeStore } from '@noy-db/hub'`,
292
+ `import { awsDynamoStore } from '@noy-db/to-aws-dynamo'`,
293
+ `import { awsS3Store } from '@noy-db/to-aws-s3'`,
294
+ ``,
295
+ `export default {`,
296
+ ` store: routeStore({`,
297
+ ` default: awsDynamoStore({ table: process.env.NOYDB_DYNAMO_TABLE! }),`,
298
+ ` age: {`,
299
+ ` cold: awsS3Store({ bucket: process.env.NOYDB_S3_COLD! }),`,
300
+ ` coldAfterDays: Number(process.env.NOYDB_COLD_AFTER_DAYS ?? '90'),`,
301
+ ` },`,
302
+ ` }),`,
303
+ ` user: process.env.NOYDB_USER ?? 'owner',`,
304
+ ` secret: process.env.NOYDB_SECRET,`,
305
+ `}`
306
+ ].join("\n"),
307
+ env: `NOYDB_USER=owner
308
+ NOYDB_SECRET=
309
+ NOYDB_DYNAMO_TABLE=myapp-hot
310
+ NOYDB_S3_COLD=myapp-archive
311
+ NOYDB_COLD_AFTER_DAYS=90
312
+ `
313
+ };
314
+ case "E":
315
+ return {
316
+ profile,
317
+ notes: "Multi-peer team sync. Primary + peer + backup + archive.",
318
+ code: [
319
+ `import { createNoydb } from '@noy-db/hub'`,
320
+ `import { browserIdbStore } from '@noy-db/to-browser-idb'`,
321
+ `import { awsDynamoStore } from '@noy-db/to-aws-dynamo'`,
322
+ `import { awsS3Store } from '@noy-db/to-aws-s3'`,
323
+ ``,
324
+ `export default {`,
325
+ ` store: browserIdbStore({ prefix: process.env.NOYDB_APP ?? 'team' }),`,
326
+ ` sync: [`,
327
+ ` { store: awsDynamoStore({ table: process.env.NOYDB_DYNAMO_TABLE! }), role: 'sync-peer', label: 'team-hot' },`,
328
+ ` { store: awsS3Store({ bucket: process.env.NOYDB_S3_BACKUP! }), role: 'backup', label: 'team-backup' },`,
329
+ ` { store: awsS3Store({ bucket: process.env.NOYDB_S3_ARCHIVE! }), role: 'archive', label: 'team-archive' },`,
330
+ ` ],`,
331
+ ` user: process.env.NOYDB_USER ?? 'member',`,
332
+ ` secret: process.env.NOYDB_SECRET,`,
333
+ `}`
334
+ ].join("\n"),
335
+ env: `NOYDB_USER=member
336
+ NOYDB_SECRET=
337
+ NOYDB_APP=team
338
+ NOYDB_DYNAMO_TABLE=team-hot
339
+ NOYDB_S3_BACKUP=team-backup
340
+ NOYDB_S3_ARCHIVE=team-archive
341
+ `
342
+ };
343
+ case "F":
344
+ return {
345
+ profile,
346
+ notes: "CRDT collaboration \u2014 Yjs-backed shared records over the encrypted envelope.",
347
+ code: [
348
+ `import { createNoydb } from '@noy-db/hub'`,
349
+ `import { browserIdbStore } from '@noy-db/to-browser-idb'`,
350
+ `import { awsDynamoStore } from '@noy-db/to-aws-dynamo'`,
351
+ `// import { yjsCollection } from '@noy-db/in-yjs' // use inside your app`,
352
+ ``,
353
+ `export default {`,
354
+ ` store: browserIdbStore({ prefix: 'collab' }),`,
355
+ ` sync: [{ store: awsDynamoStore({ table: process.env.NOYDB_DYNAMO_TABLE! }), role: 'sync-peer' }],`,
356
+ ` user: process.env.NOYDB_USER!,`,
357
+ ` secret: process.env.NOYDB_SECRET,`,
358
+ `}`,
359
+ ``,
360
+ `// After createNoydb(), replace normal collections with yjsCollection() for CRDT fields.`
361
+ ].join("\n"),
362
+ env: `NOYDB_USER=
363
+ NOYDB_SECRET=
364
+ NOYDB_DYNAMO_TABLE=collab-live
365
+ `
366
+ };
367
+ case "H":
368
+ return {
369
+ profile,
370
+ notes: "USB-portable \u2014 everything on a single file store, no cloud.",
371
+ code: [
372
+ `import { createNoydb } from '@noy-db/hub'`,
373
+ `import { jsonFile } from '@noy-db/to-file'`,
374
+ ``,
375
+ `export default {`,
376
+ ` store: jsonFile({ dir: process.env.NOYDB_DATA_DIR ?? '/Volumes/MY_USB/data' }),`,
377
+ ` user: process.env.NOYDB_USER!,`,
378
+ ` secret: process.env.NOYDB_SECRET,`,
379
+ `}`
380
+ ].join("\n"),
381
+ env: `NOYDB_USER=
382
+ NOYDB_SECRET=
383
+ NOYDB_DATA_DIR=/Volumes/MY_USB/data
384
+ `
385
+ };
386
+ case "I":
387
+ return {
388
+ profile,
389
+ notes: "Multi-tenant geographic sharding \u2014 per-vault primary per region.",
390
+ code: [
391
+ `import { createNoydb } from '@noy-db/hub'`,
392
+ `import { awsDynamoStore } from '@noy-db/to-aws-dynamo'`,
393
+ ``,
394
+ `// Pick the nearest region at init time based on tenant config.`,
395
+ `const region = process.env.NOYDB_REGION ?? 'ap-southeast-1'`,
396
+ `const table = \`myapp-\${region}\``,
397
+ ``,
398
+ `export default {`,
399
+ ` store: awsDynamoStore({ table, region }),`,
400
+ ` user: process.env.NOYDB_USER!,`,
401
+ ` secret: process.env.NOYDB_SECRET,`,
402
+ `}`
403
+ ].join("\n"),
404
+ env: `NOYDB_USER=
405
+ NOYDB_SECRET=
406
+ NOYDB_REGION=ap-southeast-1
407
+ `
408
+ };
409
+ case "J":
410
+ return {
411
+ profile,
412
+ notes: "Authentication bridge (passphrase-less unlock via OIDC or WebAuthn).",
413
+ code: [
414
+ `import { createNoydb } from '@noy-db/hub'`,
415
+ `import { browserIdbStore } from '@noy-db/to-browser-idb'`,
416
+ `// import { unlockWebAuthn } from '@noy-db/on-webauthn' // in the browser`,
417
+ `// import { keyConnector } from '@noy-db/on-oidc' // in a server app`,
418
+ ``,
419
+ `export default {`,
420
+ ` store: browserIdbStore({ prefix: process.env.NOYDB_APP ?? 'app' }),`,
421
+ ` user: process.env.NOYDB_USER!,`,
422
+ ` // secret supplied by the unlock method at openVault() time, not here.`,
423
+ `}`
424
+ ].join("\n"),
425
+ env: `NOYDB_USER=
426
+ NOYDB_APP=app
427
+ `
428
+ };
429
+ }
430
+ }
431
+ async function runConfigScaffold(argv) {
432
+ const profileArg = argv.find((a) => a.startsWith("--profile="));
433
+ const profile = profileArg?.split("=")[1] ?? "A";
434
+ if (!/^[A-J]$/.test(profile)) {
435
+ process.stderr.write(`unknown profile: ${profile}. Valid: A-J (see docs/guides/topology-matrix.md)
436
+ `);
437
+ return 2;
438
+ }
439
+ const out = scaffold(profile);
440
+ 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
441
+ `);
442
+ process.stdout.write(`// ${out.notes}
443
+
444
+ `);
445
+ process.stdout.write(out.code + "\n\n");
446
+ if (out.env) {
447
+ 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
448
+ `);
449
+ process.stdout.write(out.env);
450
+ }
451
+ return 0;
452
+ }
453
+ function isRecord(v) {
454
+ return typeof v === "object" && v !== null && !Array.isArray(v);
455
+ }
456
+ function isStoreShape(v) {
457
+ if (!isRecord(v)) return false;
458
+ return ["get", "put", "delete", "list", "loadAll", "saveAll"].every((m) => typeof v[m] === "function");
459
+ }
460
+ function normalizeSync(v) {
461
+ if (Array.isArray(v)) return v;
462
+ return [v];
463
+ }
464
+ function validateTarget(t, path, issues) {
465
+ if (!isRecord(t)) {
466
+ issues.push({
467
+ severity: "error",
468
+ code: "bad-target",
469
+ path,
470
+ message: "sync target must be an object with { store, role }"
471
+ });
472
+ return;
473
+ }
474
+ if (t["role"] === void 0) {
475
+ if (!isStoreShape(t)) {
476
+ issues.push({
477
+ severity: "error",
478
+ code: "bad-store-shape",
479
+ path,
480
+ message: "sync target store does not expose the 6-method contract"
481
+ });
482
+ }
483
+ return;
484
+ }
485
+ const validRoles = ["sync-peer", "backup", "archive"];
486
+ if (!validRoles.includes(safeStringify(t["role"]))) {
487
+ issues.push({
488
+ severity: "error",
489
+ code: "bad-role",
490
+ path: `${path}.role`,
491
+ message: `role must be one of ${validRoles.join(", ")}; got ${safeStringify(t["role"])}`
492
+ });
493
+ }
494
+ if (!t["store"] || !isStoreShape(t["store"])) {
495
+ issues.push({
496
+ severity: "error",
497
+ code: "bad-store-shape",
498
+ path: `${path}.store`,
499
+ message: "sync target store does not expose the 6-method contract"
500
+ });
501
+ }
502
+ if (t["role"] === "archive" && isRecord(t["policy"]) && isRecord(t["policy"]["pull"])) {
503
+ issues.push({
504
+ severity: "error",
505
+ code: "archive-pull-configured",
506
+ path: `${path}.policy.pull`,
507
+ message: "archive targets are push-only \u2014 pull policy is invalid"
508
+ });
509
+ }
510
+ }
511
+
512
+ // src/commands/monitor.ts
513
+ import { toMeter } from "@noy-db/to-meter";
514
+ async function runMonitor(argv) {
515
+ const file = argv[0];
516
+ if (!file) {
517
+ process.stderr.write("usage: noydb monitor <config.ts> [--interval=ms]\n");
518
+ return 2;
519
+ }
520
+ const intervalArg = argv.find((a) => a.startsWith("--interval="));
521
+ const intervalMs = intervalArg ? parseInt(intervalArg.split("=")[1] ?? "5000", 10) : 5e3;
522
+ let opts;
523
+ try {
524
+ const loaded = await loadOptionsFromFile(file);
525
+ if (typeof loaded !== "object" || loaded === null) {
526
+ process.stderr.write(`config file must export a NoydbOptions-shaped object
527
+ `);
528
+ return 1;
529
+ }
530
+ opts = loaded;
531
+ } catch (err) {
532
+ process.stderr.write(`failed to load ${file}: ${err.message}
533
+ `);
534
+ return 1;
535
+ }
536
+ const innerStore = opts["store"];
537
+ if (!innerStore || typeof innerStore !== "object") {
538
+ process.stderr.write("config has no `store` \u2014 nothing to monitor\n");
539
+ return 1;
540
+ }
541
+ const { store: metered, meter } = toMeter(innerStore, {
542
+ degradedMs: 500,
543
+ onDegraded: (e) => process.stderr.write(`DEGRADED: ${e.reason}
544
+ `),
545
+ onRestored: (e) => process.stderr.write(`RESTORED: ${e.reason}
546
+ `)
547
+ });
548
+ const liveOpts = { ...opts, store: metered };
549
+ const hub = await import("@noy-db/hub");
550
+ await hub.createNoydb(liveOpts);
551
+ process.stdout.write(`monitoring ${file} \u2014 interval ${intervalMs}ms \u2014 Ctrl-C to stop
552
+
553
+ `);
554
+ const stop = installSigintHandler(meter);
555
+ return new Promise((resolveP) => {
556
+ const timer = setInterval(() => {
557
+ if (stop.signalled) {
558
+ clearInterval(timer);
559
+ meter.close();
560
+ resolveP(0);
561
+ return;
562
+ }
563
+ const snap = meter.snapshot();
564
+ process.stdout.write(formatSnapshot(snap) + "\n");
565
+ }, intervalMs);
566
+ });
567
+ }
568
+ function installSigintHandler(meter) {
569
+ const state = { signalled: false };
570
+ const handler = () => {
571
+ state.signalled = true;
572
+ meter.close();
573
+ };
574
+ process.on("SIGINT", handler);
575
+ process.on("SIGTERM", handler);
576
+ return state;
577
+ }
578
+ function formatSnapshot(snap) {
579
+ const lines = [];
580
+ const ts = new Date(snap.collectedAt).toISOString().slice(11, 19);
581
+ lines.push(`[${ts}] status=${snap.status} calls=${snap.totalCalls} casConflicts=${snap.casConflicts} windowMs=${snap.windowMs}`);
582
+ for (const m of ["get", "put", "delete", "list", "loadAll", "saveAll"]) {
583
+ const s = snap.byMethod[m];
584
+ if (s.count === 0) continue;
585
+ 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`);
586
+ }
587
+ return lines.join("\n");
588
+ }
589
+ export {
590
+ formatSnapshot,
591
+ inspect,
592
+ loadOptionsFromFile,
593
+ runConfigScaffold,
594
+ runConfigValidate,
595
+ runInspect,
596
+ runMonitor,
597
+ runVerify,
598
+ scaffold,
599
+ validateOptions,
600
+ verify
601
+ };
602
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/inspect.ts","../src/commands/verify.ts","../src/commands/config.ts","../src/commands/monitor.ts"],"sourcesContent":["/**\n * `noydb inspect <file.noydb>` — print the unencrypted header.\n *\n * This command is the security-sensitive one in the CLI: it must\n * never prompt for a passphrase and never decrypt anything. It reads\n * exactly the unencrypted header bytes (magic + flags + header\n * length prefix + JSON header) and returns structured metadata.\n *\n * What the header reveals:\n * - `formatVersion` — container format version\n * - `handle` — ULID identifier\n * - `bodyBytes` — compressed body size\n * - `bodySha256` — body integrity hash\n *\n * What it does **not** reveal: record counts, collection names,\n * ciphertext, keyring, timestamps inside records. Those live past\n * the decryption boundary.\n *\n * @module\n */\nimport { readNoydbBundleHeader } from '@noy-db/hub'\nimport { readFile } from 'node:fs/promises'\n\nexport interface InspectResult {\n formatVersion: number\n handle: string\n bodyBytes: number\n bodySha256: string\n}\n\nexport async function inspect(filePath: string): Promise<InspectResult> {\n const bytes = await readFile(filePath)\n const header = readNoydbBundleHeader(new Uint8Array(bytes))\n return {\n formatVersion: header.formatVersion,\n handle: header.handle,\n bodyBytes: header.bodyBytes,\n bodySha256: header.bodySha256,\n }\n}\n\nexport async function runInspect(argv: readonly string[]): Promise<number> {\n const file = argv[0]\n if (!file) {\n process.stderr.write('usage: noydb inspect <file.noydb>\\n')\n return 2\n }\n try {\n const info = await inspect(file)\n process.stdout.write(JSON.stringify(info, null, 2) + '\\n')\n return 0\n } catch (err) {\n process.stderr.write(`inspect failed: ${(err as Error).message}\\n`)\n return 1\n }\n}\n","/**\n * `noydb verify <file.noydb>` — integrity check.\n *\n * Validates the three integrity properties of a bundle that can be\n * verified **without the passphrase**:\n *\n * 1. Magic prefix + format version match what this CLI supports.\n * 2. Header parses as JSON with the minimum-disclosure key set.\n * 3. Compressed body SHA-256 matches `header.bodySha256`.\n *\n * Ledger-head verification (which does need the passphrase) is not\n * covered here — that's a future enhancement when an `open` subcommand\n * lands. For now, `verify` answers \"was this file transmitted intact?\"\n * without involving keys.\n *\n * @module\n */\nimport { readNoydbBundle, readNoydbBundleHeader } from '@noy-db/hub'\nimport { readFile } from 'node:fs/promises'\n\nexport interface VerifyReport {\n ok: boolean\n file: string\n handle: string\n bodyBytes: number\n checks: { magic: boolean; header: boolean; bodyHash: boolean }\n error?: string\n}\n\nexport async function verify(filePath: string): Promise<VerifyReport> {\n const bytes = new Uint8Array(await readFile(filePath))\n const checks = { magic: false, header: false, bodyHash: false }\n\n let handle = ''\n let bodyBytes = 0\n\n try {\n const header = readNoydbBundleHeader(bytes)\n checks.magic = true\n checks.header = true\n handle = header.handle\n bodyBytes = header.bodyBytes\n\n // readNoydbBundle() verifies bodySha256 internally — if it doesn't\n // throw, the body hash matched.\n await readNoydbBundle(bytes)\n checks.bodyHash = true\n\n return { ok: true, file: filePath, handle, bodyBytes, checks }\n } catch (err) {\n return {\n ok: false, file: filePath, handle, bodyBytes, checks,\n error: (err as Error).message,\n }\n }\n}\n\nexport async function runVerify(argv: readonly string[]): Promise<number> {\n const file = argv[0]\n if (!file) {\n process.stderr.write('usage: noydb verify <file.noydb>\\n')\n return 2\n }\n const report = await verify(file)\n process.stdout.write(JSON.stringify(report, null, 2) + '\\n')\n return report.ok ? 0 : 1\n}\n","/**\n * `noydb config validate` and `noydb config scaffold`.\n *\n * **validate** — sanity-check a `NoydbOptions` value at runtime. The\n * input file is expected to default-export a `NoydbOptions` object\n * (or a factory returning one). We dynamically import it and check:\n *\n * - `store` is present and exposes the 6-method `NoydbStore` shape.\n * - Sync targets have role ∈ {sync-peer, backup, archive} and a store.\n * - `archive` targets don't carry a pull policy (archive is push-only).\n * - `syncPolicy` pairs with at least one sync target.\n * - `blob` routes (if present) sit on a bundle-capable store.\n *\n * **scaffold** — emit a working skeleton for one of the topology\n * profiles from `docs/guides/topology-matrix.md`. Output goes to stdout\n * (pipe to a file yourself) and is a ready-to-edit `.ts` + `.env`\n * pair, concatenated so the consumer can split them.\n *\n * @module\n */\nimport { pathToFileURL } from 'node:url'\nimport { resolve } from 'node:path'\n\nexport interface ValidationIssue {\n severity: 'warn' | 'error'\n code: string\n path: string\n message: string\n}\n\nexport interface ValidationReport {\n ok: boolean\n issues: ValidationIssue[]\n}\n\n/**\n * Validate a NoydbOptions-shaped object (not a file path — this is\n * the programmatic API). Conservative: if a check can't be made\n * confidently, it's skipped rather than warned, to avoid nagging.\n */\n/** Coerces arbitrary values to a human-readable string without triggering\n * `[object Object]` — used by the config validator to report bad field types. */\nfunction safeStringify(v: unknown): string {\n if (typeof v === 'string') return v\n if (typeof v === 'number' || typeof v === 'boolean' || v === null || v === undefined) {\n return String(v)\n }\n try { return JSON.stringify(v) } catch { return '<unserializable>' }\n}\n\nexport function validateOptions(opts: unknown): ValidationReport {\n const issues: ValidationIssue[] = []\n\n if (!isRecord(opts)) {\n issues.push({ severity: 'error', code: 'not-object', path: '<root>',\n message: 'NoydbOptions must be an object' })\n return { ok: false, issues }\n }\n\n // Required: store\n if (!opts.store) {\n issues.push({ severity: 'error', code: 'missing-store', path: 'store',\n message: '`store` is required' })\n } else if (!isStoreShape(opts.store)) {\n issues.push({ severity: 'error', code: 'bad-store-shape', path: 'store',\n message: '`store` does not expose the 6-method NoydbStore contract' })\n }\n\n // sync — accept single store, SyncTarget, or SyncTarget[]\n if (opts.sync !== undefined) {\n const targets = normalizeSync(opts.sync)\n targets.forEach((t, i) => validateTarget(t, `sync[${i}]`, issues))\n }\n\n // syncPolicy requires at least one sync target\n if (opts.syncPolicy !== undefined && opts.sync === undefined) {\n issues.push({ severity: 'warn', code: 'policy-without-sync', path: 'syncPolicy',\n message: '`syncPolicy` has no effect without a `sync` target' })\n }\n\n // user is recommended\n if (typeof opts.user !== 'string' || !opts.user) {\n issues.push({ severity: 'warn', code: 'missing-user', path: 'user',\n message: '`user` identifier is recommended — audit entries default to \"anonymous\" without it' })\n }\n\n // passphrase-or-encryption check\n if (opts.secret === undefined && opts.passphrase === undefined && opts.encrypt !== false) {\n issues.push({ severity: 'warn', code: 'no-secret', path: 'secret',\n message: '`secret` / `passphrase` missing and `encrypt` not explicitly false — vault open will fail' })\n }\n\n const hasError = issues.some((i) => i.severity === 'error')\n return { ok: !hasError, issues }\n}\n\n/** Dynamically import a JS/MJS/CJS config file and pull the NoydbOptions\n * out of it. Accepts a default export that is either the options\n * object or a zero-arg function (sync or async) returning the options.\n *\n * **TypeScript note.** Node cannot `import()` a bare `.ts` file without\n * a loader. If the adopter's config is TypeScript, they should either\n * compile it first (`tsc foo.ts`) or run the CLI under a TS-capable\n * runtime (e.g. `tsx $(which noydb) config validate foo.ts`). The\n * function throws a human-readable error for `.ts` input rather than\n * propagating Node's cryptic `Unknown file extension` message. */\nexport async function loadOptionsFromFile(filePath: string): Promise<unknown> {\n const abs = resolve(filePath)\n if (abs.endsWith('.ts') || abs.endsWith('.mts') || abs.endsWith('.cts')) {\n throw new Error(\n `TypeScript config files are not directly loadable — Node has no native .ts loader.\\n` +\n ` Options:\\n` +\n ` (a) compile first: tsc ${filePath} && noydb config validate ${filePath.replace(/\\.[mc]?ts$/, '.js')}\\n` +\n ` (b) run via tsx: npx tsx $(which noydb) config validate ${filePath}\\n` +\n ` (c) rename to .mjs/.js if your config has no TS-only syntax`,\n )\n }\n const mod = await import(pathToFileURL(abs).href) as { default?: unknown }\n const value = mod.default ?? mod\n if (typeof value === 'function') {\n return await (value as () => Promise<unknown>)()\n }\n return value\n}\n\nexport async function runConfigValidate(argv: readonly string[]): Promise<number> {\n const file = argv[0]\n if (!file) {\n process.stderr.write('usage: noydb config validate <file.ts|js>\\n')\n return 2\n }\n let opts: unknown\n try {\n opts = await loadOptionsFromFile(file)\n } catch (err) {\n process.stderr.write(`failed to load ${file}: ${(err as Error).message}\\n`)\n return 1\n }\n\n const report = validateOptions(opts)\n process.stdout.write(JSON.stringify(report, null, 2) + '\\n')\n return report.ok ? 0 : 1\n}\n\n// ── Scaffold ────────────────────────────────────────────────────────────\n\n/** Profiles match `docs/guides/topology-matrix.md` § View 3. */\nexport type Profile = 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J'\n\nexport interface ScaffoldResult {\n profile: Profile\n code: string\n env: string\n notes: string\n}\n\nexport function scaffold(profile: Profile): ScaffoldResult {\n switch (profile) {\n case 'A': return {\n profile, notes: 'Local-only single-user. No cloud.',\n code: [\n `import { createNoydb } from '@noy-db/hub'`,\n `import { jsonFile } from '@noy-db/to-file'`,\n ``,\n `export default {`,\n ` store: jsonFile({ dir: process.env.NOYDB_DATA_DIR ?? './data' }),`,\n ` user: process.env.NOYDB_USER ?? 'owner',`,\n ` secret: process.env.NOYDB_SECRET,`,\n `}`,\n ].join('\\n'),\n env: `NOYDB_USER=owner\\nNOYDB_SECRET=\\nNOYDB_DATA_DIR=./data\\n`,\n }\n case 'B': return {\n profile, notes: 'Offline-first + cloud mirror. Local authoritative, sync opportunistically.',\n code: [\n `import { createNoydb, INDEXED_STORE_POLICY } from '@noy-db/hub'`,\n `import { browserIdbStore } from '@noy-db/to-browser-idb'`,\n `import { awsDynamoStore } from '@noy-db/to-aws-dynamo'`,\n ``,\n `export default {`,\n ` store: browserIdbStore({ prefix: process.env.NOYDB_APP ?? 'myapp' }),`,\n ` sync: [{`,\n ` store: awsDynamoStore({`,\n ` table: process.env.NOYDB_DYNAMO_TABLE!,`,\n ` region: process.env.AWS_REGION ?? 'us-east-1',`,\n ` }),`,\n ` role: 'sync-peer',`,\n ` label: 'dynamo-live',`,\n ` }],`,\n ` syncPolicy: INDEXED_STORE_POLICY,`,\n ` user: process.env.NOYDB_USER ?? 'owner',`,\n ` secret: process.env.NOYDB_SECRET,`,\n `}`,\n ].join('\\n'),\n env: `NOYDB_USER=owner\\nNOYDB_SECRET=\\nNOYDB_APP=myapp\\nNOYDB_DYNAMO_TABLE=myapp-live\\nAWS_REGION=us-east-1\\n`,\n }\n case 'C': return {\n profile, notes: 'Records + blobs split (routeStore). Dynamo for records, S3 for blobs.',\n code: [\n `import { createNoydb, routeStore } from '@noy-db/hub'`,\n `import { awsDynamoStore } from '@noy-db/to-aws-dynamo'`,\n `import { awsS3Store } from '@noy-db/to-aws-s3'`,\n ``,\n `export default {`,\n ` store: routeStore({`,\n ` default: awsDynamoStore({`,\n ` table: process.env.NOYDB_DYNAMO_TABLE!,`,\n ` region: process.env.AWS_REGION ?? 'us-east-1',`,\n ` }),`,\n ` blobs: awsS3Store({`,\n ` bucket: process.env.NOYDB_S3_BUCKET!,`,\n ` region: process.env.AWS_REGION ?? 'us-east-1',`,\n ` }),`,\n ` }),`,\n ` user: process.env.NOYDB_USER ?? 'owner',`,\n ` secret: process.env.NOYDB_SECRET,`,\n `}`,\n ].join('\\n'),\n env: `NOYDB_USER=owner\\nNOYDB_SECRET=\\nNOYDB_DYNAMO_TABLE=myapp-records\\nNOYDB_S3_BUCKET=myapp-blobs\\nAWS_REGION=us-east-1\\n`,\n }\n case 'G': return {\n profile, notes: 'Middleware-hardened production: retry + breaker + cache + metrics.',\n code: [\n `import { createNoydb, wrapStore, withRetry, withCircuitBreaker, withCache, withHealthCheck, withMetrics } from '@noy-db/hub'`,\n `import { awsDynamoStore } from '@noy-db/to-aws-dynamo'`,\n ``,\n `export default {`,\n ` store: wrapStore(`,\n ` awsDynamoStore({ table: process.env.NOYDB_DYNAMO_TABLE! }),`,\n ` withRetry({ maxRetries: 3 }),`,\n ` withCircuitBreaker({ failureThreshold: 5, resetTimeoutMs: 30_000 }),`,\n ` withCache({ ttlMs: 60_000 }),`,\n ` withHealthCheck(),`,\n ` withMetrics({ onOperation: op => console.log(op) }),`,\n ` ),`,\n ` user: process.env.NOYDB_USER ?? 'owner',`,\n ` secret: process.env.NOYDB_SECRET,`,\n `}`,\n ].join('\\n'),\n env: `NOYDB_USER=owner\\nNOYDB_SECRET=\\nNOYDB_DYNAMO_TABLE=myapp-prod\\n`,\n }\n case 'D': return {\n profile, notes: 'Hot + cold tiered. Records age out to archive after N days.',\n code: [\n `import { createNoydb, routeStore } from '@noy-db/hub'`,\n `import { awsDynamoStore } from '@noy-db/to-aws-dynamo'`,\n `import { awsS3Store } from '@noy-db/to-aws-s3'`,\n ``,\n `export default {`,\n ` store: routeStore({`,\n ` default: awsDynamoStore({ table: process.env.NOYDB_DYNAMO_TABLE! }),`,\n ` age: {`,\n ` cold: awsS3Store({ bucket: process.env.NOYDB_S3_COLD! }),`,\n ` coldAfterDays: Number(process.env.NOYDB_COLD_AFTER_DAYS ?? '90'),`,\n ` },`,\n ` }),`,\n ` user: process.env.NOYDB_USER ?? 'owner',`,\n ` secret: process.env.NOYDB_SECRET,`,\n `}`,\n ].join('\\n'),\n env: `NOYDB_USER=owner\\nNOYDB_SECRET=\\nNOYDB_DYNAMO_TABLE=myapp-hot\\nNOYDB_S3_COLD=myapp-archive\\nNOYDB_COLD_AFTER_DAYS=90\\n`,\n }\n case 'E': return {\n profile, notes: 'Multi-peer team sync. Primary + peer + backup + archive.',\n code: [\n `import { createNoydb } from '@noy-db/hub'`,\n `import { browserIdbStore } from '@noy-db/to-browser-idb'`,\n `import { awsDynamoStore } from '@noy-db/to-aws-dynamo'`,\n `import { awsS3Store } from '@noy-db/to-aws-s3'`,\n ``,\n `export default {`,\n ` store: browserIdbStore({ prefix: process.env.NOYDB_APP ?? 'team' }),`,\n ` sync: [`,\n ` { store: awsDynamoStore({ table: process.env.NOYDB_DYNAMO_TABLE! }), role: 'sync-peer', label: 'team-hot' },`,\n ` { store: awsS3Store({ bucket: process.env.NOYDB_S3_BACKUP! }), role: 'backup', label: 'team-backup' },`,\n ` { store: awsS3Store({ bucket: process.env.NOYDB_S3_ARCHIVE! }), role: 'archive', label: 'team-archive' },`,\n ` ],`,\n ` user: process.env.NOYDB_USER ?? 'member',`,\n ` secret: process.env.NOYDB_SECRET,`,\n `}`,\n ].join('\\n'),\n env: `NOYDB_USER=member\\nNOYDB_SECRET=\\nNOYDB_APP=team\\nNOYDB_DYNAMO_TABLE=team-hot\\nNOYDB_S3_BACKUP=team-backup\\nNOYDB_S3_ARCHIVE=team-archive\\n`,\n }\n case 'F': return {\n profile, notes: 'CRDT collaboration — Yjs-backed shared records over the encrypted envelope.',\n code: [\n `import { createNoydb } from '@noy-db/hub'`,\n `import { browserIdbStore } from '@noy-db/to-browser-idb'`,\n `import { awsDynamoStore } from '@noy-db/to-aws-dynamo'`,\n `// import { yjsCollection } from '@noy-db/in-yjs' // use inside your app`,\n ``,\n `export default {`,\n ` store: browserIdbStore({ prefix: 'collab' }),`,\n ` sync: [{ store: awsDynamoStore({ table: process.env.NOYDB_DYNAMO_TABLE! }), role: 'sync-peer' }],`,\n ` user: process.env.NOYDB_USER!,`,\n ` secret: process.env.NOYDB_SECRET,`,\n `}`,\n ``,\n `// After createNoydb(), replace normal collections with yjsCollection() for CRDT fields.`,\n ].join('\\n'),\n env: `NOYDB_USER=\\nNOYDB_SECRET=\\nNOYDB_DYNAMO_TABLE=collab-live\\n`,\n }\n case 'H': return {\n profile, notes: 'USB-portable — everything on a single file store, no cloud.',\n code: [\n `import { createNoydb } from '@noy-db/hub'`,\n `import { jsonFile } from '@noy-db/to-file'`,\n ``,\n `export default {`,\n ` store: jsonFile({ dir: process.env.NOYDB_DATA_DIR ?? '/Volumes/MY_USB/data' }),`,\n ` user: process.env.NOYDB_USER!,`,\n ` secret: process.env.NOYDB_SECRET,`,\n `}`,\n ].join('\\n'),\n env: `NOYDB_USER=\\nNOYDB_SECRET=\\nNOYDB_DATA_DIR=/Volumes/MY_USB/data\\n`,\n }\n case 'I': return {\n profile, notes: 'Multi-tenant geographic sharding — per-vault primary per region.',\n code: [\n `import { createNoydb } from '@noy-db/hub'`,\n `import { awsDynamoStore } from '@noy-db/to-aws-dynamo'`,\n ``,\n `// Pick the nearest region at init time based on tenant config.`,\n `const region = process.env.NOYDB_REGION ?? 'ap-southeast-1'`,\n `const table = \\`myapp-\\${region}\\``,\n ``,\n `export default {`,\n ` store: awsDynamoStore({ table, region }),`,\n ` user: process.env.NOYDB_USER!,`,\n ` secret: process.env.NOYDB_SECRET,`,\n `}`,\n ].join('\\n'),\n env: `NOYDB_USER=\\nNOYDB_SECRET=\\nNOYDB_REGION=ap-southeast-1\\n`,\n }\n case 'J': return {\n profile, notes: 'Authentication bridge (passphrase-less unlock via OIDC or WebAuthn).',\n code: [\n `import { createNoydb } from '@noy-db/hub'`,\n `import { browserIdbStore } from '@noy-db/to-browser-idb'`,\n `// import { unlockWebAuthn } from '@noy-db/on-webauthn' // in the browser`,\n `// import { keyConnector } from '@noy-db/on-oidc' // in a server app`,\n ``,\n `export default {`,\n ` store: browserIdbStore({ prefix: process.env.NOYDB_APP ?? 'app' }),`,\n ` user: process.env.NOYDB_USER!,`,\n ` // secret supplied by the unlock method at openVault() time, not here.`,\n `}`,\n ].join('\\n'),\n env: `NOYDB_USER=\\nNOYDB_APP=app\\n`,\n }\n }\n}\n\nexport async function runConfigScaffold(argv: readonly string[]): Promise<number> {\n const profileArg = argv.find((a) => a.startsWith('--profile='))\n const profile = (profileArg?.split('=')[1] ?? 'A') as Profile\n if (!/^[A-J]$/.test(profile)) {\n process.stderr.write(`unknown profile: ${profile}. Valid: A-J (see docs/guides/topology-matrix.md)\\n`)\n return 2\n }\n const out = scaffold(profile)\n process.stdout.write(`// ── noydb config (profile ${out.profile}) ───────────────────\\n`)\n process.stdout.write(`// ${out.notes}\\n\\n`)\n process.stdout.write(out.code + '\\n\\n')\n if (out.env) {\n process.stdout.write(`// ── .env template ───────────────────────────────────────\\n`)\n process.stdout.write(out.env)\n }\n return 0\n}\n\n// ── Helpers ─────────────────────────────────────────────────────────────\n\nfunction isRecord(v: unknown): v is Record<string, unknown> {\n return typeof v === 'object' && v !== null && !Array.isArray(v)\n}\n\nfunction isStoreShape(v: unknown): boolean {\n if (!isRecord(v)) return false\n return ['get', 'put', 'delete', 'list', 'loadAll', 'saveAll']\n .every((m) => typeof v[m] === 'function')\n}\n\nfunction normalizeSync(v: unknown): unknown[] {\n if (Array.isArray(v)) return v\n return [v]\n}\n\nfunction validateTarget(t: unknown, path: string, issues: ValidationIssue[]): void {\n if (!isRecord(t)) {\n issues.push({ severity: 'error', code: 'bad-target', path,\n message: 'sync target must be an object with { store, role }' })\n return\n }\n if (t['role'] === undefined) {\n // Bare store is allowed — core wraps it as { store, role: 'sync-peer' }\n if (!isStoreShape(t)) {\n issues.push({ severity: 'error', code: 'bad-store-shape', path,\n message: 'sync target store does not expose the 6-method contract' })\n }\n return\n }\n const validRoles = ['sync-peer', 'backup', 'archive']\n if (!validRoles.includes(safeStringify(t['role']))) {\n issues.push({ severity: 'error', code: 'bad-role', path: `${path}.role`,\n message: `role must be one of ${validRoles.join(', ')}; got ${safeStringify(t['role'])}` })\n }\n if (!t['store'] || !isStoreShape(t['store'])) {\n issues.push({ severity: 'error', code: 'bad-store-shape', path: `${path}.store`,\n message: 'sync target store does not expose the 6-method contract' })\n }\n if (t['role'] === 'archive' && isRecord(t['policy']) && isRecord(t['policy']['pull'])) {\n issues.push({ severity: 'error', code: 'archive-pull-configured', path: `${path}.policy.pull`,\n message: 'archive targets are push-only — pull policy is invalid' })\n }\n}\n","/**\n * `noydb monitor <config.ts>` — live text dashboard of store metrics.\n *\n * Loads a `NoydbOptions` from a config file, wraps the primary store\n * in `@noy-db/to-meter`, creates a `Noydb` instance, and prints a\n * refreshing snapshot to stdout at a configurable interval. Ctrl-C\n * to stop.\n *\n * This is the scope of issue — CLI-first. A web dashboard\n * is deferred: the meter handle already exposes everything a dashboard\n * needs via `snapshot()` + `subscribe()`.\n *\n * @module\n */\nimport { toMeter } from '@noy-db/to-meter'\nimport type { MeterSnapshot } from '@noy-db/to-meter'\nimport { loadOptionsFromFile } from './config.js'\n\nexport interface MonitorOptions {\n intervalMs: number\n iterations?: number // undefined → run forever\n}\n\nexport async function runMonitor(argv: readonly string[]): Promise<number> {\n const file = argv[0]\n if (!file) {\n process.stderr.write('usage: noydb monitor <config.ts> [--interval=ms]\\n')\n return 2\n }\n\n const intervalArg = argv.find((a) => a.startsWith('--interval='))\n const intervalMs = intervalArg ? parseInt(intervalArg.split('=')[1] ?? '5000', 10) : 5_000\n\n let opts: Record<string, unknown>\n try {\n const loaded = await loadOptionsFromFile(file)\n if (typeof loaded !== 'object' || loaded === null) {\n process.stderr.write(`config file must export a NoydbOptions-shaped object\\n`)\n return 1\n }\n opts = loaded as Record<string, unknown>\n } catch (err) {\n process.stderr.write(`failed to load ${file}: ${(err as Error).message}\\n`)\n return 1\n }\n\n const innerStore = opts['store']\n if (!innerStore || typeof innerStore !== 'object') {\n process.stderr.write('config has no `store` — nothing to monitor\\n')\n return 1\n }\n\n const { store: metered, meter } = toMeter(innerStore as never, {\n degradedMs: 500,\n onDegraded: (e) => process.stderr.write(`DEGRADED: ${e.reason}\\n`),\n onRestored: (e) => process.stderr.write(`RESTORED: ${e.reason}\\n`),\n })\n\n // Replace the store in the options object so the Noydb instance\n // goes through the meter.\n const liveOpts = { ...opts, store: metered } as Record<string, unknown>\n\n // Dynamically import hub so the CLI doesn't need to bundle it at\n // build time. Adopter's installed @noy-db/hub version wins.\n const hub = await import('@noy-db/hub') as { createNoydb: (o: unknown) => Promise<unknown> }\n await hub.createNoydb(liveOpts)\n\n process.stdout.write(`monitoring ${file} — interval ${intervalMs}ms — Ctrl-C to stop\\n\\n`)\n\n const stop = installSigintHandler(meter)\n\n return new Promise<number>((resolveP) => {\n const timer = setInterval(() => {\n if (stop.signalled) {\n clearInterval(timer)\n meter.close()\n resolveP(0)\n return\n }\n const snap = meter.snapshot()\n process.stdout.write(formatSnapshot(snap) + '\\n')\n }, intervalMs)\n })\n}\n\nfunction installSigintHandler(meter: { close(): void }): { signalled: boolean } {\n const state = { signalled: false }\n const handler = () => { state.signalled = true; meter.close() }\n process.on('SIGINT', handler)\n process.on('SIGTERM', handler)\n return state\n}\n\nexport function formatSnapshot(snap: MeterSnapshot): string {\n const lines: string[] = []\n const ts = new Date(snap.collectedAt).toISOString().slice(11, 19)\n lines.push(`[${ts}] status=${snap.status} calls=${snap.totalCalls} casConflicts=${snap.casConflicts} windowMs=${snap.windowMs}`)\n for (const m of ['get', 'put', 'delete', 'list', 'loadAll', 'saveAll'] as const) {\n const s = snap.byMethod[m]\n if (s.count === 0) continue\n 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`)\n }\n return lines.join('\\n')\n}\n"],"mappings":"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","names":["readNoydbBundleHeader","readFile"]}