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