@noy-db/cli 0.1.0-pre.10

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.
@@ -0,0 +1,650 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/commands/inspect.ts
4
+ import { readNoydbBundleHeader } from "@noy-db/hub";
5
+ import { readFile } from "fs/promises";
6
+ async function inspect(filePath) {
7
+ const bytes = await readFile(filePath);
8
+ const header = readNoydbBundleHeader(new Uint8Array(bytes));
9
+ return {
10
+ formatVersion: header.formatVersion,
11
+ handle: header.handle,
12
+ bodyBytes: header.bodyBytes,
13
+ bodySha256: header.bodySha256
14
+ };
15
+ }
16
+ async function runInspect(argv) {
17
+ const file = argv[0];
18
+ if (!file) {
19
+ process.stderr.write("usage: noydb inspect <file.noydb>\n");
20
+ return 2;
21
+ }
22
+ try {
23
+ const info = await inspect(file);
24
+ process.stdout.write(JSON.stringify(info, null, 2) + "\n");
25
+ return 0;
26
+ } catch (err) {
27
+ process.stderr.write(`inspect failed: ${err.message}
28
+ `);
29
+ return 1;
30
+ }
31
+ }
32
+
33
+ // src/commands/verify.ts
34
+ import { readNoydbBundle, readNoydbBundleHeader as readNoydbBundleHeader2 } from "@noy-db/hub";
35
+ import { readFile as readFile2 } from "fs/promises";
36
+ async function verify(filePath) {
37
+ const bytes = new Uint8Array(await readFile2(filePath));
38
+ const checks = { magic: false, header: false, bodyHash: false };
39
+ let handle = "";
40
+ let bodyBytes = 0;
41
+ try {
42
+ const header = readNoydbBundleHeader2(bytes);
43
+ checks.magic = true;
44
+ checks.header = true;
45
+ handle = header.handle;
46
+ bodyBytes = header.bodyBytes;
47
+ await readNoydbBundle(bytes);
48
+ checks.bodyHash = true;
49
+ return { ok: true, file: filePath, handle, bodyBytes, checks };
50
+ } catch (err) {
51
+ return {
52
+ ok: false,
53
+ file: filePath,
54
+ handle,
55
+ bodyBytes,
56
+ checks,
57
+ error: err.message
58
+ };
59
+ }
60
+ }
61
+ async function runVerify(argv) {
62
+ const file = argv[0];
63
+ if (!file) {
64
+ process.stderr.write("usage: noydb verify <file.noydb>\n");
65
+ return 2;
66
+ }
67
+ const report = await verify(file);
68
+ process.stdout.write(JSON.stringify(report, null, 2) + "\n");
69
+ return report.ok ? 0 : 1;
70
+ }
71
+
72
+ // src/commands/config.ts
73
+ import { pathToFileURL } from "url";
74
+ import { resolve } from "path";
75
+ function safeStringify(v) {
76
+ if (typeof v === "string") return v;
77
+ if (typeof v === "number" || typeof v === "boolean" || v === null || v === void 0) {
78
+ return String(v);
79
+ }
80
+ try {
81
+ return JSON.stringify(v);
82
+ } catch {
83
+ return "<unserializable>";
84
+ }
85
+ }
86
+ function validateOptions(opts) {
87
+ const issues = [];
88
+ if (!isRecord(opts)) {
89
+ issues.push({
90
+ severity: "error",
91
+ code: "not-object",
92
+ path: "<root>",
93
+ message: "NoydbOptions must be an object"
94
+ });
95
+ return { ok: false, issues };
96
+ }
97
+ if (!opts.store) {
98
+ issues.push({
99
+ severity: "error",
100
+ code: "missing-store",
101
+ path: "store",
102
+ message: "`store` is required"
103
+ });
104
+ } else if (!isStoreShape(opts.store)) {
105
+ issues.push({
106
+ severity: "error",
107
+ code: "bad-store-shape",
108
+ path: "store",
109
+ message: "`store` does not expose the 6-method NoydbStore contract"
110
+ });
111
+ }
112
+ if (opts.sync !== void 0) {
113
+ const targets = normalizeSync(opts.sync);
114
+ targets.forEach((t, i) => validateTarget(t, `sync[${i}]`, issues));
115
+ }
116
+ if (opts.syncPolicy !== void 0 && opts.sync === void 0) {
117
+ issues.push({
118
+ severity: "warn",
119
+ code: "policy-without-sync",
120
+ path: "syncPolicy",
121
+ message: "`syncPolicy` has no effect without a `sync` target"
122
+ });
123
+ }
124
+ if (typeof opts.user !== "string" || !opts.user) {
125
+ issues.push({
126
+ severity: "warn",
127
+ code: "missing-user",
128
+ path: "user",
129
+ message: '`user` identifier is recommended \u2014 audit entries default to "anonymous" without it'
130
+ });
131
+ }
132
+ if (opts.secret === void 0 && opts.passphrase === void 0 && opts.encrypt !== false) {
133
+ issues.push({
134
+ severity: "warn",
135
+ code: "no-secret",
136
+ path: "secret",
137
+ message: "`secret` / `passphrase` missing and `encrypt` not explicitly false \u2014 vault open will fail"
138
+ });
139
+ }
140
+ const hasError = issues.some((i) => i.severity === "error");
141
+ return { ok: !hasError, issues };
142
+ }
143
+ async function loadOptionsFromFile(filePath) {
144
+ const abs = resolve(filePath);
145
+ if (abs.endsWith(".ts") || abs.endsWith(".mts") || abs.endsWith(".cts")) {
146
+ throw new Error(
147
+ `TypeScript config files are not directly loadable \u2014 Node has no native .ts loader.
148
+ Options:
149
+ (a) compile first: tsc ${filePath} && noydb config validate ${filePath.replace(/\.[mc]?ts$/, ".js")}
150
+ (b) run via tsx: npx tsx $(which noydb) config validate ${filePath}
151
+ (c) rename to .mjs/.js if your config has no TS-only syntax`
152
+ );
153
+ }
154
+ const mod = await import(pathToFileURL(abs).href);
155
+ const value = mod.default ?? mod;
156
+ if (typeof value === "function") {
157
+ return await value();
158
+ }
159
+ return value;
160
+ }
161
+ async function runConfigValidate(argv) {
162
+ const file = argv[0];
163
+ if (!file) {
164
+ process.stderr.write("usage: noydb config validate <file.ts|js>\n");
165
+ return 2;
166
+ }
167
+ let opts;
168
+ try {
169
+ opts = await loadOptionsFromFile(file);
170
+ } catch (err) {
171
+ process.stderr.write(`failed to load ${file}: ${err.message}
172
+ `);
173
+ return 1;
174
+ }
175
+ const report = validateOptions(opts);
176
+ process.stdout.write(JSON.stringify(report, null, 2) + "\n");
177
+ return report.ok ? 0 : 1;
178
+ }
179
+ function scaffold(profile) {
180
+ switch (profile) {
181
+ case "A":
182
+ return {
183
+ profile,
184
+ notes: "Local-only single-user. No cloud.",
185
+ code: [
186
+ `import { createNoydb } from '@noy-db/hub'`,
187
+ `import { jsonFile } from '@noy-db/to-file'`,
188
+ ``,
189
+ `export default {`,
190
+ ` store: jsonFile({ dir: process.env.NOYDB_DATA_DIR ?? './data' }),`,
191
+ ` user: process.env.NOYDB_USER ?? 'owner',`,
192
+ ` secret: process.env.NOYDB_SECRET,`,
193
+ `}`
194
+ ].join("\n"),
195
+ env: `NOYDB_USER=owner
196
+ NOYDB_SECRET=
197
+ NOYDB_DATA_DIR=./data
198
+ `
199
+ };
200
+ case "B":
201
+ return {
202
+ profile,
203
+ notes: "Offline-first + cloud mirror. Local authoritative, sync opportunistically.",
204
+ code: [
205
+ `import { createNoydb, INDEXED_STORE_POLICY } from '@noy-db/hub'`,
206
+ `import { browserIdbStore } from '@noy-db/to-browser-idb'`,
207
+ `import { awsDynamoStore } from '@noy-db/to-aws-dynamo'`,
208
+ ``,
209
+ `export default {`,
210
+ ` store: browserIdbStore({ prefix: process.env.NOYDB_APP ?? 'myapp' }),`,
211
+ ` sync: [{`,
212
+ ` store: awsDynamoStore({`,
213
+ ` table: process.env.NOYDB_DYNAMO_TABLE!,`,
214
+ ` region: process.env.AWS_REGION ?? 'us-east-1',`,
215
+ ` }),`,
216
+ ` role: 'sync-peer',`,
217
+ ` label: 'dynamo-live',`,
218
+ ` }],`,
219
+ ` syncPolicy: INDEXED_STORE_POLICY,`,
220
+ ` user: process.env.NOYDB_USER ?? 'owner',`,
221
+ ` secret: process.env.NOYDB_SECRET,`,
222
+ `}`
223
+ ].join("\n"),
224
+ env: `NOYDB_USER=owner
225
+ NOYDB_SECRET=
226
+ NOYDB_APP=myapp
227
+ NOYDB_DYNAMO_TABLE=myapp-live
228
+ AWS_REGION=us-east-1
229
+ `
230
+ };
231
+ case "C":
232
+ return {
233
+ profile,
234
+ notes: "Records + blobs split (routeStore). Dynamo for records, S3 for blobs.",
235
+ code: [
236
+ `import { createNoydb, routeStore } from '@noy-db/hub'`,
237
+ `import { awsDynamoStore } from '@noy-db/to-aws-dynamo'`,
238
+ `import { awsS3Store } from '@noy-db/to-aws-s3'`,
239
+ ``,
240
+ `export default {`,
241
+ ` store: routeStore({`,
242
+ ` default: awsDynamoStore({`,
243
+ ` table: process.env.NOYDB_DYNAMO_TABLE!,`,
244
+ ` region: process.env.AWS_REGION ?? 'us-east-1',`,
245
+ ` }),`,
246
+ ` blobs: awsS3Store({`,
247
+ ` bucket: process.env.NOYDB_S3_BUCKET!,`,
248
+ ` region: process.env.AWS_REGION ?? 'us-east-1',`,
249
+ ` }),`,
250
+ ` }),`,
251
+ ` user: process.env.NOYDB_USER ?? 'owner',`,
252
+ ` secret: process.env.NOYDB_SECRET,`,
253
+ `}`
254
+ ].join("\n"),
255
+ env: `NOYDB_USER=owner
256
+ NOYDB_SECRET=
257
+ NOYDB_DYNAMO_TABLE=myapp-records
258
+ NOYDB_S3_BUCKET=myapp-blobs
259
+ AWS_REGION=us-east-1
260
+ `
261
+ };
262
+ case "G":
263
+ return {
264
+ profile,
265
+ notes: "Middleware-hardened production: retry + breaker + cache + metrics.",
266
+ code: [
267
+ `import { createNoydb, wrapStore, withRetry, withCircuitBreaker, withCache, withHealthCheck, withMetrics } from '@noy-db/hub'`,
268
+ `import { awsDynamoStore } from '@noy-db/to-aws-dynamo'`,
269
+ ``,
270
+ `export default {`,
271
+ ` store: wrapStore(`,
272
+ ` awsDynamoStore({ table: process.env.NOYDB_DYNAMO_TABLE! }),`,
273
+ ` withRetry({ maxRetries: 3 }),`,
274
+ ` withCircuitBreaker({ failureThreshold: 5, resetTimeoutMs: 30_000 }),`,
275
+ ` withCache({ ttlMs: 60_000 }),`,
276
+ ` withHealthCheck(),`,
277
+ ` withMetrics({ onOperation: op => console.log(op) }),`,
278
+ ` ),`,
279
+ ` user: process.env.NOYDB_USER ?? 'owner',`,
280
+ ` secret: process.env.NOYDB_SECRET,`,
281
+ `}`
282
+ ].join("\n"),
283
+ env: `NOYDB_USER=owner
284
+ NOYDB_SECRET=
285
+ NOYDB_DYNAMO_TABLE=myapp-prod
286
+ `
287
+ };
288
+ case "D":
289
+ return {
290
+ profile,
291
+ notes: "Hot + cold tiered. Records age out to archive after N days.",
292
+ code: [
293
+ `import { createNoydb, routeStore } from '@noy-db/hub'`,
294
+ `import { awsDynamoStore } from '@noy-db/to-aws-dynamo'`,
295
+ `import { awsS3Store } from '@noy-db/to-aws-s3'`,
296
+ ``,
297
+ `export default {`,
298
+ ` store: routeStore({`,
299
+ ` default: awsDynamoStore({ table: process.env.NOYDB_DYNAMO_TABLE! }),`,
300
+ ` age: {`,
301
+ ` cold: awsS3Store({ bucket: process.env.NOYDB_S3_COLD! }),`,
302
+ ` coldAfterDays: Number(process.env.NOYDB_COLD_AFTER_DAYS ?? '90'),`,
303
+ ` },`,
304
+ ` }),`,
305
+ ` user: process.env.NOYDB_USER ?? 'owner',`,
306
+ ` secret: process.env.NOYDB_SECRET,`,
307
+ `}`
308
+ ].join("\n"),
309
+ env: `NOYDB_USER=owner
310
+ NOYDB_SECRET=
311
+ NOYDB_DYNAMO_TABLE=myapp-hot
312
+ NOYDB_S3_COLD=myapp-archive
313
+ NOYDB_COLD_AFTER_DAYS=90
314
+ `
315
+ };
316
+ case "E":
317
+ return {
318
+ profile,
319
+ notes: "Multi-peer team sync. Primary + peer + backup + archive.",
320
+ code: [
321
+ `import { createNoydb } from '@noy-db/hub'`,
322
+ `import { browserIdbStore } from '@noy-db/to-browser-idb'`,
323
+ `import { awsDynamoStore } from '@noy-db/to-aws-dynamo'`,
324
+ `import { awsS3Store } from '@noy-db/to-aws-s3'`,
325
+ ``,
326
+ `export default {`,
327
+ ` store: browserIdbStore({ prefix: process.env.NOYDB_APP ?? 'team' }),`,
328
+ ` sync: [`,
329
+ ` { store: awsDynamoStore({ table: process.env.NOYDB_DYNAMO_TABLE! }), role: 'sync-peer', label: 'team-hot' },`,
330
+ ` { store: awsS3Store({ bucket: process.env.NOYDB_S3_BACKUP! }), role: 'backup', label: 'team-backup' },`,
331
+ ` { store: awsS3Store({ bucket: process.env.NOYDB_S3_ARCHIVE! }), role: 'archive', label: 'team-archive' },`,
332
+ ` ],`,
333
+ ` user: process.env.NOYDB_USER ?? 'member',`,
334
+ ` secret: process.env.NOYDB_SECRET,`,
335
+ `}`
336
+ ].join("\n"),
337
+ env: `NOYDB_USER=member
338
+ NOYDB_SECRET=
339
+ NOYDB_APP=team
340
+ NOYDB_DYNAMO_TABLE=team-hot
341
+ NOYDB_S3_BACKUP=team-backup
342
+ NOYDB_S3_ARCHIVE=team-archive
343
+ `
344
+ };
345
+ case "F":
346
+ return {
347
+ profile,
348
+ notes: "CRDT collaboration \u2014 Yjs-backed shared records over the encrypted envelope.",
349
+ code: [
350
+ `import { createNoydb } from '@noy-db/hub'`,
351
+ `import { browserIdbStore } from '@noy-db/to-browser-idb'`,
352
+ `import { awsDynamoStore } from '@noy-db/to-aws-dynamo'`,
353
+ `// import { yjsCollection } from '@noy-db/in-yjs' // use inside your app`,
354
+ ``,
355
+ `export default {`,
356
+ ` store: browserIdbStore({ prefix: 'collab' }),`,
357
+ ` sync: [{ store: awsDynamoStore({ table: process.env.NOYDB_DYNAMO_TABLE! }), role: 'sync-peer' }],`,
358
+ ` user: process.env.NOYDB_USER!,`,
359
+ ` secret: process.env.NOYDB_SECRET,`,
360
+ `}`,
361
+ ``,
362
+ `// After createNoydb(), replace normal collections with yjsCollection() for CRDT fields.`
363
+ ].join("\n"),
364
+ env: `NOYDB_USER=
365
+ NOYDB_SECRET=
366
+ NOYDB_DYNAMO_TABLE=collab-live
367
+ `
368
+ };
369
+ case "H":
370
+ return {
371
+ profile,
372
+ notes: "USB-portable \u2014 everything on a single file store, no cloud.",
373
+ code: [
374
+ `import { createNoydb } from '@noy-db/hub'`,
375
+ `import { jsonFile } from '@noy-db/to-file'`,
376
+ ``,
377
+ `export default {`,
378
+ ` store: jsonFile({ dir: process.env.NOYDB_DATA_DIR ?? '/Volumes/MY_USB/data' }),`,
379
+ ` user: process.env.NOYDB_USER!,`,
380
+ ` secret: process.env.NOYDB_SECRET,`,
381
+ `}`
382
+ ].join("\n"),
383
+ env: `NOYDB_USER=
384
+ NOYDB_SECRET=
385
+ NOYDB_DATA_DIR=/Volumes/MY_USB/data
386
+ `
387
+ };
388
+ case "I":
389
+ return {
390
+ profile,
391
+ notes: "Multi-tenant geographic sharding \u2014 per-vault primary per region.",
392
+ code: [
393
+ `import { createNoydb } from '@noy-db/hub'`,
394
+ `import { awsDynamoStore } from '@noy-db/to-aws-dynamo'`,
395
+ ``,
396
+ `// Pick the nearest region at init time based on tenant config.`,
397
+ `const region = process.env.NOYDB_REGION ?? 'ap-southeast-1'`,
398
+ `const table = \`myapp-\${region}\``,
399
+ ``,
400
+ `export default {`,
401
+ ` store: awsDynamoStore({ table, region }),`,
402
+ ` user: process.env.NOYDB_USER!,`,
403
+ ` secret: process.env.NOYDB_SECRET,`,
404
+ `}`
405
+ ].join("\n"),
406
+ env: `NOYDB_USER=
407
+ NOYDB_SECRET=
408
+ NOYDB_REGION=ap-southeast-1
409
+ `
410
+ };
411
+ case "J":
412
+ return {
413
+ profile,
414
+ notes: "Authentication bridge (passphrase-less unlock via OIDC or WebAuthn).",
415
+ code: [
416
+ `import { createNoydb } from '@noy-db/hub'`,
417
+ `import { browserIdbStore } from '@noy-db/to-browser-idb'`,
418
+ `// import { unlockWebAuthn } from '@noy-db/on-webauthn' // in the browser`,
419
+ `// import { keyConnector } from '@noy-db/on-oidc' // in a server app`,
420
+ ``,
421
+ `export default {`,
422
+ ` store: browserIdbStore({ prefix: process.env.NOYDB_APP ?? 'app' }),`,
423
+ ` user: process.env.NOYDB_USER!,`,
424
+ ` // secret supplied by the unlock method at openVault() time, not here.`,
425
+ `}`
426
+ ].join("\n"),
427
+ env: `NOYDB_USER=
428
+ NOYDB_APP=app
429
+ `
430
+ };
431
+ }
432
+ }
433
+ async function runConfigScaffold(argv) {
434
+ const profileArg = argv.find((a) => a.startsWith("--profile="));
435
+ const profile = profileArg?.split("=")[1] ?? "A";
436
+ if (!/^[A-J]$/.test(profile)) {
437
+ process.stderr.write(`unknown profile: ${profile}. Valid: A-J (see docs/guides/topology-matrix.md)
438
+ `);
439
+ return 2;
440
+ }
441
+ const out = scaffold(profile);
442
+ 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
443
+ `);
444
+ process.stdout.write(`// ${out.notes}
445
+
446
+ `);
447
+ process.stdout.write(out.code + "\n\n");
448
+ if (out.env) {
449
+ 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
450
+ `);
451
+ process.stdout.write(out.env);
452
+ }
453
+ return 0;
454
+ }
455
+ function isRecord(v) {
456
+ return typeof v === "object" && v !== null && !Array.isArray(v);
457
+ }
458
+ function isStoreShape(v) {
459
+ if (!isRecord(v)) return false;
460
+ return ["get", "put", "delete", "list", "loadAll", "saveAll"].every((m) => typeof v[m] === "function");
461
+ }
462
+ function normalizeSync(v) {
463
+ if (Array.isArray(v)) return v;
464
+ return [v];
465
+ }
466
+ function validateTarget(t, path, issues) {
467
+ if (!isRecord(t)) {
468
+ issues.push({
469
+ severity: "error",
470
+ code: "bad-target",
471
+ path,
472
+ message: "sync target must be an object with { store, role }"
473
+ });
474
+ return;
475
+ }
476
+ if (t["role"] === void 0) {
477
+ if (!isStoreShape(t)) {
478
+ issues.push({
479
+ severity: "error",
480
+ code: "bad-store-shape",
481
+ path,
482
+ message: "sync target store does not expose the 6-method contract"
483
+ });
484
+ }
485
+ return;
486
+ }
487
+ const validRoles = ["sync-peer", "backup", "archive"];
488
+ if (!validRoles.includes(safeStringify(t["role"]))) {
489
+ issues.push({
490
+ severity: "error",
491
+ code: "bad-role",
492
+ path: `${path}.role`,
493
+ message: `role must be one of ${validRoles.join(", ")}; got ${safeStringify(t["role"])}`
494
+ });
495
+ }
496
+ if (!t["store"] || !isStoreShape(t["store"])) {
497
+ issues.push({
498
+ severity: "error",
499
+ code: "bad-store-shape",
500
+ path: `${path}.store`,
501
+ message: "sync target store does not expose the 6-method contract"
502
+ });
503
+ }
504
+ if (t["role"] === "archive" && isRecord(t["policy"]) && isRecord(t["policy"]["pull"])) {
505
+ issues.push({
506
+ severity: "error",
507
+ code: "archive-pull-configured",
508
+ path: `${path}.policy.pull`,
509
+ message: "archive targets are push-only \u2014 pull policy is invalid"
510
+ });
511
+ }
512
+ }
513
+
514
+ // src/commands/monitor.ts
515
+ import { toMeter } from "@noy-db/to-meter";
516
+ async function runMonitor(argv) {
517
+ const file = argv[0];
518
+ if (!file) {
519
+ process.stderr.write("usage: noydb monitor <config.ts> [--interval=ms]\n");
520
+ return 2;
521
+ }
522
+ const intervalArg = argv.find((a) => a.startsWith("--interval="));
523
+ const intervalMs = intervalArg ? parseInt(intervalArg.split("=")[1] ?? "5000", 10) : 5e3;
524
+ let opts;
525
+ try {
526
+ const loaded = await loadOptionsFromFile(file);
527
+ if (typeof loaded !== "object" || loaded === null) {
528
+ process.stderr.write(`config file must export a NoydbOptions-shaped object
529
+ `);
530
+ return 1;
531
+ }
532
+ opts = loaded;
533
+ } catch (err) {
534
+ process.stderr.write(`failed to load ${file}: ${err.message}
535
+ `);
536
+ return 1;
537
+ }
538
+ const innerStore = opts["store"];
539
+ if (!innerStore || typeof innerStore !== "object") {
540
+ process.stderr.write("config has no `store` \u2014 nothing to monitor\n");
541
+ return 1;
542
+ }
543
+ const { store: metered, meter } = toMeter(innerStore, {
544
+ degradedMs: 500,
545
+ onDegraded: (e) => process.stderr.write(`DEGRADED: ${e.reason}
546
+ `),
547
+ onRestored: (e) => process.stderr.write(`RESTORED: ${e.reason}
548
+ `)
549
+ });
550
+ const liveOpts = { ...opts, store: metered };
551
+ const hub = await import("@noy-db/hub");
552
+ await hub.createNoydb(liveOpts);
553
+ process.stdout.write(`monitoring ${file} \u2014 interval ${intervalMs}ms \u2014 Ctrl-C to stop
554
+
555
+ `);
556
+ const stop = installSigintHandler(meter);
557
+ return new Promise((resolveP) => {
558
+ const timer = setInterval(() => {
559
+ if (stop.signalled) {
560
+ clearInterval(timer);
561
+ meter.close();
562
+ resolveP(0);
563
+ return;
564
+ }
565
+ const snap = meter.snapshot();
566
+ process.stdout.write(formatSnapshot(snap) + "\n");
567
+ }, intervalMs);
568
+ });
569
+ }
570
+ function installSigintHandler(meter) {
571
+ const state = { signalled: false };
572
+ const handler = () => {
573
+ state.signalled = true;
574
+ meter.close();
575
+ };
576
+ process.on("SIGINT", handler);
577
+ process.on("SIGTERM", handler);
578
+ return state;
579
+ }
580
+ function formatSnapshot(snap) {
581
+ const lines = [];
582
+ const ts = new Date(snap.collectedAt).toISOString().slice(11, 19);
583
+ lines.push(`[${ts}] status=${snap.status} calls=${snap.totalCalls} casConflicts=${snap.casConflicts} windowMs=${snap.windowMs}`);
584
+ for (const m of ["get", "put", "delete", "list", "loadAll", "saveAll"]) {
585
+ const s = snap.byMethod[m];
586
+ if (s.count === 0) continue;
587
+ 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`);
588
+ }
589
+ return lines.join("\n");
590
+ }
591
+
592
+ // src/bin/noydb.ts
593
+ var VERSION = "0.1.0";
594
+ function usage() {
595
+ return [
596
+ "noydb \u2014 command-line tools for @noy-db",
597
+ "",
598
+ "Usage:",
599
+ " noydb inspect <file.noydb> Print bundle header (no passphrase)",
600
+ " noydb verify <file.noydb> Verify bundle integrity (no passphrase)",
601
+ " noydb config validate <file.js|mjs> Sanity-check a NoydbOptions file",
602
+ " noydb config scaffold [--profile=<A-J>] Emit a topology-profile skeleton",
603
+ " noydb monitor <file.js|mjs> [--interval=ms] Live dashboard of store metrics",
604
+ "",
605
+ "TypeScript configs: run under a TS-capable runtime, e.g. `npx tsx $(which noydb) config validate foo.ts`.",
606
+ " noydb --version Print CLI version",
607
+ " noydb --help Show this message",
608
+ "",
609
+ "Profiles for `config scaffold` map to docs/guides/topology-matrix.md \xA7 View 3."
610
+ ].join("\n");
611
+ }
612
+ async function main(argv) {
613
+ const [cmd, ...rest] = argv;
614
+ if (!cmd || cmd === "--help" || cmd === "-h" || cmd === "help") {
615
+ process.stdout.write(usage() + "\n");
616
+ return 0;
617
+ }
618
+ if (cmd === "--version" || cmd === "-v") {
619
+ process.stdout.write(VERSION + "\n");
620
+ return 0;
621
+ }
622
+ switch (cmd) {
623
+ case "inspect":
624
+ return runInspect(rest);
625
+ case "verify":
626
+ return runVerify(rest);
627
+ case "monitor":
628
+ return runMonitor(rest);
629
+ case "config": {
630
+ const [sub, ...subRest] = rest;
631
+ if (sub === "validate") return runConfigValidate(subRest);
632
+ if (sub === "scaffold") return runConfigScaffold(subRest);
633
+ process.stderr.write(`unknown config subcommand: ${sub ?? "(none)"}
634
+ `);
635
+ return 2;
636
+ }
637
+ default:
638
+ process.stderr.write(`unknown command: ${cmd}
639
+
640
+ ${usage()}
641
+ `);
642
+ return 2;
643
+ }
644
+ }
645
+ main(process.argv.slice(2)).then((code) => process.exit(code)).catch((err) => {
646
+ process.stderr.write(`fatal: ${err.message}
647
+ `);
648
+ process.exit(1);
649
+ });
650
+ //# sourceMappingURL=noydb.js.map