create-noy-db 0.9.0 → 0.10.0-pre.1

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,7 +1,149 @@
1
1
  #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/wizard/augment.ts
13
+ var augment_exports = {};
14
+ __export(augment_exports, {
15
+ augmentNuxtConfig: () => augmentNuxtConfig,
16
+ writeAugmentedConfig: () => writeAugmentedConfig
17
+ });
18
+ import { promises as fs4 } from "fs";
19
+ import { loadFile, generateCode, builders } from "magicast";
20
+ import { createPatch } from "diff";
21
+ async function augmentNuxtConfig(options) {
22
+ const originalCode = await fs4.readFile(options.configPath, "utf8");
23
+ const mod = await loadFile(options.configPath);
24
+ const exported = mod.exports.default;
25
+ if (exported === void 0 || exported === null) {
26
+ return {
27
+ kind: "unsupported-shape",
28
+ configPath: options.configPath,
29
+ reason: `${options.configPath} has no default export. Expected \`export default defineNuxtConfig({...})\` or \`export default {...}\`.`
30
+ };
31
+ }
32
+ const config = resolveConfigObject(exported);
33
+ if (!config) {
34
+ return {
35
+ kind: "unsupported-shape",
36
+ configPath: options.configPath,
37
+ reason: `Could not find the config object in ${options.configPath}. Expected \`export default defineNuxtConfig({ modules: [], ... })\` or a plain object literal.`
38
+ };
39
+ }
40
+ const skipReasons = [];
41
+ const modulesRaw = config.modules;
42
+ let modulesWasMissing = false;
43
+ if (modulesRaw === void 0) {
44
+ modulesWasMissing = true;
45
+ config.modules = [];
46
+ } else if (typeof modulesRaw !== "object" || !isProxyArray(modulesRaw)) {
47
+ return {
48
+ kind: "unsupported-shape",
49
+ configPath: options.configPath,
50
+ reason: `\`modules\` in ${options.configPath} is not an array literal. Edit it manually and re-run the wizard if you want to continue.`
51
+ };
52
+ }
53
+ const modules = config.modules;
54
+ const alreadyHasModule = Array.from(modules).some((m) => String(m) === "@noy-db/in-nuxt");
55
+ if (alreadyHasModule) {
56
+ skipReasons.push("`@noy-db/in-nuxt` already in modules");
57
+ } else {
58
+ modules.push("@noy-db/in-nuxt");
59
+ }
60
+ const noydbRaw = config.noydb;
61
+ if (noydbRaw !== void 0) {
62
+ skipReasons.push("`noydb` key already set");
63
+ } else {
64
+ config.noydb = builders.raw(
65
+ `{ adapter: '${options.adapter}', pinia: true, devtools: true }`
66
+ );
67
+ }
68
+ if (skipReasons.length === 2 && !modulesWasMissing) {
69
+ return {
70
+ kind: "already-configured",
71
+ configPath: options.configPath,
72
+ reason: skipReasons.join("; ")
73
+ };
74
+ }
75
+ const generated = generateCode(mod).code;
76
+ const diff = createPatch(
77
+ options.configPath,
78
+ originalCode,
79
+ generated,
80
+ "",
81
+ "",
82
+ { context: 3 }
83
+ );
84
+ return {
85
+ kind: "proposed-change",
86
+ configPath: options.configPath,
87
+ originalCode,
88
+ newCode: generated,
89
+ diff,
90
+ dryRun: options.dryRun === true
91
+ };
92
+ }
93
+ async function writeAugmentedConfig(configPath, newCode) {
94
+ await fs4.writeFile(configPath, newCode, "utf8");
95
+ }
96
+ function resolveConfigObject(exported) {
97
+ if (!exported || typeof exported !== "object") return null;
98
+ const proxy = exported;
99
+ if (proxy.$type === "function-call" && proxy.$args) {
100
+ const firstArg = proxy.$args[0];
101
+ if (firstArg && typeof firstArg === "object") {
102
+ return firstArg;
103
+ }
104
+ return null;
105
+ }
106
+ if (proxy.$type === "object" || proxy.$type === void 0) {
107
+ return proxy;
108
+ }
109
+ return null;
110
+ }
111
+ function isProxyArray(value) {
112
+ if (!value || typeof value !== "object") return false;
113
+ const proxy = value;
114
+ return proxy.$type === "array";
115
+ }
116
+ var init_augment = __esm({
117
+ "src/wizard/augment.ts"() {
118
+ "use strict";
119
+ }
120
+ });
2
121
 
3
122
  // src/bin/noy-db.ts
4
- import pc from "picocolors";
123
+ import { spawnSync as spawnSync2 } from "child_process";
124
+ import pc2 from "picocolors";
125
+
126
+ // src/commands/resolve-bin.ts
127
+ import { existsSync, readFileSync } from "fs";
128
+ import { dirname, join } from "path";
129
+ function resolveBin(pkg, binName, from = process.cwd()) {
130
+ let dir = from;
131
+ for (; ; ) {
132
+ const manifestPath = join(dir, "node_modules", ...pkg.split("/"), "package.json");
133
+ if (existsSync(manifestPath)) {
134
+ try {
135
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
136
+ const rel = typeof manifest.bin === "string" ? manifest.bin : manifest.bin?.[binName];
137
+ return rel ? join(dirname(manifestPath), rel) : null;
138
+ } catch {
139
+ return null;
140
+ }
141
+ }
142
+ const parent = dirname(dir);
143
+ if (parent === dir) return null;
144
+ dir = parent;
145
+ }
146
+ }
5
147
 
6
148
  // src/commands/add.ts
7
149
  import { promises as fs } from "fs";
@@ -307,36 +449,1490 @@ async function backup(options) {
307
449
  }
308
450
  }
309
451
 
452
+ // src/commands/pod/inspect.ts
453
+ import { readPodHeader } from "@noy-db/hub";
454
+ import { readFile } from "fs/promises";
455
+ async function inspect(filePath) {
456
+ const bytes = await readFile(filePath);
457
+ const header = readPodHeader(new Uint8Array(bytes));
458
+ return {
459
+ formatVersion: header.formatVersion,
460
+ handle: header.handle,
461
+ bodyBytes: header.bodyBytes,
462
+ bodySha256: header.bodySha256
463
+ };
464
+ }
465
+ async function runInspect(argv) {
466
+ const file = argv[0];
467
+ if (!file) {
468
+ process.stderr.write("usage: noy-db pod inspect <file.noydb>\n");
469
+ return 2;
470
+ }
471
+ try {
472
+ const info = await inspect(file);
473
+ process.stdout.write(JSON.stringify(info, null, 2) + "\n");
474
+ return 0;
475
+ } catch (err) {
476
+ process.stderr.write(`inspect failed: ${err.message}
477
+ `);
478
+ return 1;
479
+ }
480
+ }
481
+
482
+ // src/commands/pod/verify.ts
483
+ import { readPod, readPodHeader as readPodHeader2 } from "@noy-db/hub";
484
+ import { readFile as readFile2 } from "fs/promises";
485
+ async function verify(filePath) {
486
+ const bytes = new Uint8Array(await readFile2(filePath));
487
+ const checks = { magic: false, header: false, bodyHash: false };
488
+ let handle = "";
489
+ let bodyBytes = 0;
490
+ try {
491
+ const header = readPodHeader2(bytes);
492
+ checks.magic = true;
493
+ checks.header = true;
494
+ handle = header.handle;
495
+ bodyBytes = header.bodyBytes;
496
+ await readPod(bytes);
497
+ checks.bodyHash = true;
498
+ return { ok: true, file: filePath, handle, bodyBytes, checks };
499
+ } catch (err) {
500
+ return {
501
+ ok: false,
502
+ file: filePath,
503
+ handle,
504
+ bodyBytes,
505
+ checks,
506
+ error: err.message
507
+ };
508
+ }
509
+ }
510
+ async function runVerify(argv) {
511
+ const file = argv[0];
512
+ if (!file) {
513
+ process.stderr.write("usage: noy-db pod verify <file.noydb>\n");
514
+ return 2;
515
+ }
516
+ const report = await verify(file);
517
+ process.stdout.write(JSON.stringify(report, null, 2) + "\n");
518
+ return report.ok ? 0 : 1;
519
+ }
520
+
521
+ // src/commands/pod/describe.ts
522
+ import { readFile as readFile3, writeFile, mkdir } from "fs/promises";
523
+ import { dirname as dirname2, basename, join as join2 } from "path";
524
+ import { createHash } from "crypto";
525
+ import { stringify as yamlStringify } from "yaml";
526
+ import { createNoydb as createNoydb5, readPod as readPod2, loadPersistedSchema } from "@noy-db/hub";
527
+ import { ConflictError } from "@noy-db/hub/to";
528
+ var VERSION = "0.1.0";
529
+ function memoryStore2() {
530
+ const data = /* @__PURE__ */ new Map();
531
+ const getColl = (v, c) => {
532
+ let vm = data.get(v);
533
+ if (!vm) {
534
+ vm = /* @__PURE__ */ new Map();
535
+ data.set(v, vm);
536
+ }
537
+ let cm = vm.get(c);
538
+ if (!cm) {
539
+ cm = /* @__PURE__ */ new Map();
540
+ vm.set(c, cm);
541
+ }
542
+ return cm;
543
+ };
544
+ return {
545
+ async get(v, c, id) {
546
+ return data.get(v)?.get(c)?.get(id) ?? null;
547
+ },
548
+ async put(v, c, id, env, ev) {
549
+ const coll = getColl(v, c);
550
+ const ex = coll.get(id);
551
+ if (ev !== void 0 && ex && ex._v !== ev) throw new ConflictError(ex._v);
552
+ coll.set(id, env);
553
+ },
554
+ async delete(v, c, id) {
555
+ data.get(v)?.get(c)?.delete(id);
556
+ },
557
+ async list(v, c) {
558
+ return [...data.get(v)?.get(c)?.keys() ?? []];
559
+ },
560
+ async loadAll(v) {
561
+ const vm = data.get(v);
562
+ const snap = {};
563
+ if (vm) for (const [cn, cm] of vm) {
564
+ if (cn.startsWith("_")) continue;
565
+ const r = {};
566
+ for (const [id, e] of cm) r[id] = e;
567
+ snap[cn] = r;
568
+ }
569
+ return snap;
570
+ },
571
+ async saveAll(v, snap) {
572
+ for (const [cn, recs] of Object.entries(snap)) {
573
+ const coll = getColl(v, cn);
574
+ for (const [id, env] of Object.entries(recs)) coll.set(id, env);
575
+ }
576
+ }
577
+ };
578
+ }
579
+ async function describeBundle(opts) {
580
+ const bytes = await readFile3(opts.bundlePath);
581
+ const sourceSha256 = createHash("sha256").update(bytes).digest("hex");
582
+ const { dumpJson } = await readPod2(new Uint8Array(bytes));
583
+ const backup2 = JSON.parse(dumpJson);
584
+ const compartmentName = backup2._compartment ?? "vault";
585
+ const db = await createNoydb5({
586
+ store: memoryStore2(),
587
+ user: opts.user,
588
+ secret: opts.secret
589
+ });
590
+ const vault = await db.openVault(compartmentName);
591
+ await vault.load(dumpJson);
592
+ const collectionNames = await vault.collections();
593
+ for (const name of collectionNames) {
594
+ if (!name.startsWith("_")) vault.collection(name);
595
+ }
596
+ const snapshot = await vault.dumpSchema({
597
+ withStats: opts.withStats,
598
+ sampleSize: opts.sampleSize
599
+ });
600
+ const enriched = opts.schemas === "full" ? await enrichWithFullSchemas(snapshot, vault, collectionNames) : snapshot;
601
+ const { _noydb_snapshot: _ignore, ...rest } = enriched;
602
+ void _ignore;
603
+ const withProvenance = {
604
+ _noydb_snapshot: 2,
605
+ // #1447 — carried through from the snapshot rather than restated, so a CLI
606
+ // dump reports the same emitter capabilities the vault did. Restating it
607
+ // here would let the two drift, and a reader cannot tell which is right.
608
+ reports: rest.reports,
609
+ _provenance: {
610
+ generatedBy: `noy-db describe v${VERSION}`,
611
+ source: basename(opts.bundlePath),
612
+ sourceSha256,
613
+ emittedAt: enriched.emittedAt
614
+ },
615
+ vault: rest.vault,
616
+ emittedAt: rest.emittedAt,
617
+ subsystems: rest.subsystems,
618
+ collections: rest.collections,
619
+ materializedViews: rest.materializedViews,
620
+ overlayViews: rest.overlayViews,
621
+ derivations: rest.derivations,
622
+ ...rest.internal !== void 0 ? { internal: rest.internal } : {}
623
+ };
624
+ if (opts.schemas === "sidecar" && opts.outPath) {
625
+ await writeSidecarSchemas(opts.outPath, vault, collectionNames);
626
+ }
627
+ const emitted = emit(withProvenance, opts.format);
628
+ if (opts.outPath) {
629
+ await mkdir(dirname2(opts.outPath), { recursive: true });
630
+ await writeFile(opts.outPath, emitted);
631
+ }
632
+ return emitted;
633
+ }
634
+ async function enrichWithFullSchemas(snapshot, vault, collectionNames) {
635
+ const state = vault._introspectState();
636
+ const collections = {};
637
+ for (const name of Object.keys(snapshot.collections)) {
638
+ const base = snapshot.collections[name];
639
+ if (!base) continue;
640
+ if (!collectionNames.includes(name)) {
641
+ collections[name] = base;
642
+ continue;
643
+ }
644
+ try {
645
+ const dek = await state.getDEK(name);
646
+ const persisted = await loadPersistedSchema(state.adapter, state.name, name, dek);
647
+ if (persisted?.jsonSchema) {
648
+ collections[name] = { ...base, jsonSchema: persisted.jsonSchema };
649
+ } else {
650
+ collections[name] = base;
651
+ }
652
+ } catch {
653
+ collections[name] = base;
654
+ }
655
+ }
656
+ return { ...snapshot, collections };
657
+ }
658
+ async function writeSidecarSchemas(outPath, vault, collectionNames) {
659
+ const state = vault._introspectState();
660
+ const sidecarDir = `${outPath}.schemas`;
661
+ await mkdir(sidecarDir, { recursive: true });
662
+ for (const name of collectionNames) {
663
+ if (name.startsWith("_")) continue;
664
+ try {
665
+ const dek = await state.getDEK(name);
666
+ const persisted = await loadPersistedSchema(state.adapter, state.name, name, dek);
667
+ if (persisted?.jsonSchema) {
668
+ await writeFile(
669
+ join2(sidecarDir, `${name}.schema.json`),
670
+ JSON.stringify(persisted.jsonSchema, null, 2)
671
+ );
672
+ }
673
+ } catch {
674
+ }
675
+ }
676
+ }
677
+ function emit(snapshot, format) {
678
+ if (format === "json") {
679
+ return JSON.stringify(snapshot, null, 2) + "\n";
680
+ }
681
+ return yamlStringify(snapshot, {
682
+ aliasDuplicateObjects: false,
683
+ indent: 2,
684
+ lineWidth: 0
685
+ // never wrap; preserves long strings
686
+ });
687
+ }
688
+ function parseArgs(argv) {
689
+ const out = {
690
+ bundlePath: "",
691
+ format: "yaml",
692
+ withStats: false,
693
+ schemas: "none",
694
+ sampleSize: 50
695
+ };
696
+ let i = 0;
697
+ while (i < argv.length) {
698
+ const arg = argv[i];
699
+ if (arg === "--format") {
700
+ const v = argv[++i];
701
+ if (v !== "yaml" && v !== "json") return { error: `--format must be yaml or json (got: ${v ?? "<none>"})` };
702
+ out.format = v;
703
+ } else if (arg === "--with-stats") {
704
+ out.withStats = true;
705
+ } else if (arg === "--schemas") {
706
+ const v = argv[++i];
707
+ if (v !== "none" && v !== "full" && v !== "sidecar") {
708
+ return { error: `--schemas must be none|full|sidecar (got: ${v ?? "<none>"})` };
709
+ }
710
+ out.schemas = v;
711
+ } else if (arg === "-o" || arg === "--out") {
712
+ const v = argv[++i];
713
+ if (v === void 0) return { error: "-o requires a value" };
714
+ out.outPath = v;
715
+ } else if (arg === "--secret") {
716
+ const v = argv[++i];
717
+ if (v === void 0) return { error: "--secret requires a value" };
718
+ out.secret = v;
719
+ } else if (arg === "--user") {
720
+ const v = argv[++i];
721
+ if (v === void 0) return { error: "--user requires a value" };
722
+ out.user = v;
723
+ } else if (arg === "--sample") {
724
+ const n = parseInt(argv[++i] ?? "", 10);
725
+ if (Number.isNaN(n) || n < 0) return { error: `--sample must be a non-negative integer` };
726
+ out.sampleSize = n;
727
+ } else if (arg === "-h" || arg === "--help") {
728
+ return { error: "help" };
729
+ } else if (arg?.startsWith("-")) {
730
+ return { error: `unknown option: ${arg}` };
731
+ } else if (!out.bundlePath) {
732
+ out.bundlePath = arg ?? "";
733
+ } else {
734
+ return { error: `unexpected positional arg: ${arg}` };
735
+ }
736
+ i++;
737
+ }
738
+ if (!out.bundlePath) return { error: "missing bundle path" };
739
+ return out;
740
+ }
741
+ function usage() {
742
+ return [
743
+ "usage: noy-db describe <bundle-path> [options]",
744
+ "",
745
+ " --format <yaml|json> output format (default: yaml)",
746
+ " --with-stats include records / bytes / oldest / newest per collection",
747
+ " --schemas <mode> JSON Schema body inclusion (default: none)",
748
+ " - none: summary only",
749
+ " - full: inline complete JSON Schema per collection",
750
+ " - sidecar: write `<out>.schemas/<col>.schema.json` siblings",
751
+ " -o, --out <file> write to file instead of stdout",
752
+ " --user <userId> keyring user identifier (required)",
753
+ " --secret <p> decryption secret (or via NOYDB_SECRET env)",
754
+ " --sample <n> max records sampled when no persisted/live schema (default: 50)",
755
+ "",
756
+ "Exit codes: 0 ok, 1 internal error, 2 usage error, 3 auth failure"
757
+ ].join("\n");
758
+ }
759
+ async function runDescribe(argv) {
760
+ const parsed = parseArgs(argv);
761
+ if ("error" in parsed) {
762
+ if (parsed.error === "help") {
763
+ process.stdout.write(usage() + "\n");
764
+ return 0;
765
+ }
766
+ process.stderr.write(`describe: ${parsed.error}
767
+
768
+ ${usage()}
769
+ `);
770
+ return 2;
771
+ }
772
+ const secret = parsed.secret ?? process.env.NOYDB_SECRET;
773
+ if (!secret) {
774
+ process.stderr.write("describe: secret required \u2014 pass --secret or set NOYDB_SECRET env\n");
775
+ return 3;
776
+ }
777
+ if (parsed.secret) {
778
+ process.stderr.write(
779
+ "[noy-db] warning: --secret appears in shell history; consider NOYDB_SECRET env instead\n"
780
+ );
781
+ }
782
+ if (!parsed.user) {
783
+ process.stderr.write("describe: --user <userId> is required to unlock the bundle\n");
784
+ return 2;
785
+ }
786
+ try {
787
+ const out = await describeBundle({
788
+ bundlePath: parsed.bundlePath,
789
+ user: parsed.user,
790
+ secret,
791
+ format: parsed.format,
792
+ withStats: parsed.withStats,
793
+ schemas: parsed.schemas,
794
+ ...parsed.outPath !== void 0 ? { outPath: parsed.outPath } : {},
795
+ sampleSize: parsed.sampleSize
796
+ });
797
+ if (!parsed.outPath) process.stdout.write(out);
798
+ return 0;
799
+ } catch (err) {
800
+ const msg = err instanceof Error ? err.message : String(err);
801
+ if (/decrypt|tamper|wrong key|invalid/i.test(msg)) {
802
+ process.stderr.write(`describe: decryption failed \u2014 check --user and --secret
803
+ ${msg}
804
+ `);
805
+ return 3;
806
+ }
807
+ process.stderr.write(`describe: ${msg}
808
+ `);
809
+ return 1;
810
+ }
811
+ }
812
+
813
+ // src/commands/pod/config.ts
814
+ import { pathToFileURL } from "url";
815
+ import { resolve } from "path";
816
+ function safeStringify(v) {
817
+ if (typeof v === "string") return v;
818
+ if (typeof v === "number" || typeof v === "boolean" || v === null || v === void 0) {
819
+ return String(v);
820
+ }
821
+ try {
822
+ return JSON.stringify(v);
823
+ } catch {
824
+ return "<unserializable>";
825
+ }
826
+ }
827
+ function validateOptions(opts) {
828
+ const issues = [];
829
+ if (!isRecord(opts)) {
830
+ issues.push({
831
+ severity: "error",
832
+ code: "not-object",
833
+ path: "<root>",
834
+ message: "NoydbOptions must be an object"
835
+ });
836
+ return { ok: false, issues };
837
+ }
838
+ if (!opts.store) {
839
+ issues.push({
840
+ severity: "error",
841
+ code: "missing-store",
842
+ path: "store",
843
+ message: "`store` is required"
844
+ });
845
+ } else if (!isStoreShape(opts.store)) {
846
+ issues.push({
847
+ severity: "error",
848
+ code: "bad-store-shape",
849
+ path: "store",
850
+ message: "`store` does not expose the 6-method NoydbStore contract"
851
+ });
852
+ }
853
+ if (opts.sync !== void 0) {
854
+ const targets = normalizeSync(opts.sync);
855
+ targets.forEach((t, i) => validateTarget(t, `sync[${i}]`, issues));
856
+ }
857
+ if (opts.syncPolicy !== void 0 && opts.sync === void 0) {
858
+ issues.push({
859
+ severity: "warn",
860
+ code: "policy-without-sync",
861
+ path: "syncPolicy",
862
+ message: "`syncPolicy` has no effect without a `sync` target"
863
+ });
864
+ }
865
+ if (typeof opts.user !== "string" || !opts.user) {
866
+ issues.push({
867
+ severity: "warn",
868
+ code: "missing-user",
869
+ path: "user",
870
+ message: '`user` identifier is recommended \u2014 audit entries default to "anonymous" without it'
871
+ });
872
+ }
873
+ if (opts.secret === void 0 && opts.secret === void 0 && opts.encrypt !== false) {
874
+ issues.push({
875
+ severity: "warn",
876
+ code: "no-secret",
877
+ path: "secret",
878
+ message: "`secret` / `secret` missing and `encrypt` not explicitly false \u2014 vault open will fail"
879
+ });
880
+ }
881
+ const hasError = issues.some((i) => i.severity === "error");
882
+ return { ok: !hasError, issues };
883
+ }
884
+ async function loadOptionsFromFile(filePath) {
885
+ const abs = resolve(filePath);
886
+ if (abs.endsWith(".ts") || abs.endsWith(".mts") || abs.endsWith(".cts")) {
887
+ throw new Error(
888
+ `TypeScript config files are not directly loadable \u2014 Node has no native .ts loader.
889
+ Options:
890
+ (a) compile first: tsc ${filePath} && noy-db config validate ${filePath.replace(/\.[mc]?ts$/, ".js")}
891
+ (b) run via tsx: npx tsx $(which noy-db) config validate ${filePath}
892
+ (c) rename to .mjs/.js if your config has no TS-only syntax`
893
+ );
894
+ }
895
+ const mod = await import(pathToFileURL(abs).href);
896
+ const value = mod.default ?? mod;
897
+ if (typeof value === "function") {
898
+ return await value();
899
+ }
900
+ return value;
901
+ }
902
+ async function runConfigValidate(argv) {
903
+ const file = argv[0];
904
+ if (!file) {
905
+ process.stderr.write("usage: noy-db config validate <file.ts|js>\n");
906
+ return 2;
907
+ }
908
+ let opts;
909
+ try {
910
+ opts = await loadOptionsFromFile(file);
911
+ } catch (err) {
912
+ process.stderr.write(`failed to load ${file}: ${err.message}
913
+ `);
914
+ return 1;
915
+ }
916
+ const report = validateOptions(opts);
917
+ process.stdout.write(JSON.stringify(report, null, 2) + "\n");
918
+ return report.ok ? 0 : 1;
919
+ }
920
+ var PROFILE_IDS = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"];
921
+ function scaffold(profile) {
922
+ switch (profile) {
923
+ case "A":
924
+ return {
925
+ profile,
926
+ notes: "Local-only single-user. No cloud.",
927
+ code: [
928
+ `import { createNoydb } from '@noy-db/hub'`,
929
+ `import { toFile } from '@noy-db/to-file'`,
930
+ ``,
931
+ `export default {`,
932
+ ` store: toFile({ dir: process.env.NOYDB_DATA_DIR ?? './data' }),`,
933
+ ` user: process.env.NOYDB_USER ?? 'owner',`,
934
+ ` secret: process.env.NOYDB_SECRET,`,
935
+ `}`
936
+ ].join("\n"),
937
+ env: `NOYDB_USER=owner
938
+ NOYDB_SECRET=
939
+ NOYDB_DATA_DIR=./data
940
+ `
941
+ };
942
+ case "B":
943
+ return {
944
+ profile,
945
+ notes: "Offline-first + cloud mirror. Local authoritative, sync opportunistically.",
946
+ code: [
947
+ `import { createNoydb, INDEXED_STORE_POLICY } from '@noy-db/hub'`,
948
+ `import { toBrowserIdb } from '@noy-db/to-browser-idb'`,
949
+ `import { toAwsDynamo } from '@noy-db/to-aws-dynamo'`,
950
+ ``,
951
+ `export default {`,
952
+ ` store: toBrowserIdb({ prefix: process.env.NOYDB_APP ?? 'myapp' }),`,
953
+ ` sync: [{`,
954
+ ` store: toAwsDynamo({`,
955
+ ` table: process.env.NOYDB_DYNAMO_TABLE!,`,
956
+ ` region: process.env.AWS_REGION ?? 'us-east-1',`,
957
+ ` }),`,
958
+ ` role: 'sync-peer',`,
959
+ ` label: 'dynamo-live',`,
960
+ ` }],`,
961
+ ` syncPolicy: INDEXED_STORE_POLICY,`,
962
+ ` user: process.env.NOYDB_USER ?? 'owner',`,
963
+ ` secret: process.env.NOYDB_SECRET,`,
964
+ `}`
965
+ ].join("\n"),
966
+ env: `NOYDB_USER=owner
967
+ NOYDB_SECRET=
968
+ NOYDB_APP=myapp
969
+ NOYDB_DYNAMO_TABLE=myapp-live
970
+ AWS_REGION=us-east-1
971
+ `
972
+ };
973
+ case "C":
974
+ return {
975
+ profile,
976
+ notes: "Records + blobs split (routeStore). Dynamo for records, S3 for blobs.",
977
+ code: [
978
+ `import { createNoydb, routeStore } from '@noy-db/hub'`,
979
+ `import { toAwsDynamo } from '@noy-db/to-aws-dynamo'`,
980
+ `import { toAwsS3 } from '@noy-db/to-aws-s3'`,
981
+ ``,
982
+ `export default {`,
983
+ ` store: routeStore({`,
984
+ ` default: toAwsDynamo({`,
985
+ ` table: process.env.NOYDB_DYNAMO_TABLE!,`,
986
+ ` region: process.env.AWS_REGION ?? 'us-east-1',`,
987
+ ` }),`,
988
+ ` blobs: toAwsS3({`,
989
+ ` bucket: process.env.NOYDB_S3_BUCKET!,`,
990
+ ` region: process.env.AWS_REGION ?? 'us-east-1',`,
991
+ ` }),`,
992
+ ` }),`,
993
+ ` user: process.env.NOYDB_USER ?? 'owner',`,
994
+ ` secret: process.env.NOYDB_SECRET,`,
995
+ `}`
996
+ ].join("\n"),
997
+ env: `NOYDB_USER=owner
998
+ NOYDB_SECRET=
999
+ NOYDB_DYNAMO_TABLE=myapp-records
1000
+ NOYDB_S3_BUCKET=myapp-blobs
1001
+ AWS_REGION=us-east-1
1002
+ `
1003
+ };
1004
+ case "G":
1005
+ return {
1006
+ profile,
1007
+ notes: "Middleware-hardened production: retry + breaker + cache + metrics.",
1008
+ code: [
1009
+ `import { createNoydb, wrapStore, withRetry, withCircuitBreaker, withCache, withHealthCheck, withMetrics } from '@noy-db/hub'`,
1010
+ `import { toAwsDynamo } from '@noy-db/to-aws-dynamo'`,
1011
+ ``,
1012
+ `export default {`,
1013
+ ` store: wrapStore(`,
1014
+ ` toAwsDynamo({ table: process.env.NOYDB_DYNAMO_TABLE! }),`,
1015
+ ` withRetry({ maxRetries: 3 }),`,
1016
+ ` withCircuitBreaker({ failureThreshold: 5, resetTimeoutMs: 30_000 }),`,
1017
+ ` withCache({ ttlMs: 60_000 }),`,
1018
+ ` withHealthCheck(),`,
1019
+ ` withMetrics({ onOperation: op => console.log(op) }),`,
1020
+ ` ),`,
1021
+ ` user: process.env.NOYDB_USER ?? 'owner',`,
1022
+ ` secret: process.env.NOYDB_SECRET,`,
1023
+ `}`
1024
+ ].join("\n"),
1025
+ env: `NOYDB_USER=owner
1026
+ NOYDB_SECRET=
1027
+ NOYDB_DYNAMO_TABLE=myapp-prod
1028
+ `
1029
+ };
1030
+ case "D":
1031
+ return {
1032
+ profile,
1033
+ notes: "Hot + cold tiered. Records age out to archive after N days.",
1034
+ code: [
1035
+ `import { createNoydb, routeStore } from '@noy-db/hub'`,
1036
+ `import { toAwsDynamo } from '@noy-db/to-aws-dynamo'`,
1037
+ `import { toAwsS3 } from '@noy-db/to-aws-s3'`,
1038
+ ``,
1039
+ `export default {`,
1040
+ ` store: routeStore({`,
1041
+ ` default: toAwsDynamo({ table: process.env.NOYDB_DYNAMO_TABLE! }),`,
1042
+ ` age: {`,
1043
+ ` cold: toAwsS3({ bucket: process.env.NOYDB_S3_COLD! }),`,
1044
+ ` coldAfterDays: Number(process.env.NOYDB_COLD_AFTER_DAYS ?? '90'),`,
1045
+ ` },`,
1046
+ ` }),`,
1047
+ ` user: process.env.NOYDB_USER ?? 'owner',`,
1048
+ ` secret: process.env.NOYDB_SECRET,`,
1049
+ `}`
1050
+ ].join("\n"),
1051
+ env: `NOYDB_USER=owner
1052
+ NOYDB_SECRET=
1053
+ NOYDB_DYNAMO_TABLE=myapp-hot
1054
+ NOYDB_S3_COLD=myapp-archive
1055
+ NOYDB_COLD_AFTER_DAYS=90
1056
+ `
1057
+ };
1058
+ case "E":
1059
+ return {
1060
+ profile,
1061
+ notes: "Multi-peer team sync. Primary + peer + backup + archive.",
1062
+ code: [
1063
+ `import { createNoydb } from '@noy-db/hub'`,
1064
+ `import { toBrowserIdb } from '@noy-db/to-browser-idb'`,
1065
+ `import { toAwsDynamo } from '@noy-db/to-aws-dynamo'`,
1066
+ `import { toAwsS3 } from '@noy-db/to-aws-s3'`,
1067
+ ``,
1068
+ `export default {`,
1069
+ ` store: toBrowserIdb({ prefix: process.env.NOYDB_APP ?? 'team' }),`,
1070
+ ` sync: [`,
1071
+ ` { store: toAwsDynamo({ table: process.env.NOYDB_DYNAMO_TABLE! }), role: 'sync-peer', label: 'team-hot' },`,
1072
+ ` { store: toAwsS3({ bucket: process.env.NOYDB_S3_BACKUP! }), role: 'backup', label: 'team-backup' },`,
1073
+ ` { store: toAwsS3({ bucket: process.env.NOYDB_S3_ARCHIVE! }), role: 'archive', label: 'team-archive' },`,
1074
+ ` ],`,
1075
+ ` user: process.env.NOYDB_USER ?? 'member',`,
1076
+ ` secret: process.env.NOYDB_SECRET,`,
1077
+ `}`
1078
+ ].join("\n"),
1079
+ env: `NOYDB_USER=member
1080
+ NOYDB_SECRET=
1081
+ NOYDB_APP=team
1082
+ NOYDB_DYNAMO_TABLE=team-hot
1083
+ NOYDB_S3_BACKUP=team-backup
1084
+ NOYDB_S3_ARCHIVE=team-archive
1085
+ `
1086
+ };
1087
+ case "F":
1088
+ return {
1089
+ profile,
1090
+ notes: "CRDT collaboration \u2014 Yjs-backed shared records over the encrypted envelope.",
1091
+ code: [
1092
+ `import { createNoydb } from '@noy-db/hub'`,
1093
+ `import { toBrowserIdb } from '@noy-db/to-browser-idb'`,
1094
+ `import { toAwsDynamo } from '@noy-db/to-aws-dynamo'`,
1095
+ `// import { yjsCollection } from '@noy-db/in-yjs' // use inside your app`,
1096
+ ``,
1097
+ `export default {`,
1098
+ ` store: toBrowserIdb({ prefix: 'collab' }),`,
1099
+ ` sync: [{ store: toAwsDynamo({ table: process.env.NOYDB_DYNAMO_TABLE! }), role: 'sync-peer' }],`,
1100
+ ` user: process.env.NOYDB_USER!,`,
1101
+ ` secret: process.env.NOYDB_SECRET,`,
1102
+ `}`,
1103
+ ``,
1104
+ `// After createNoydb(), replace normal collections with yjsCollection() for CRDT fields.`
1105
+ ].join("\n"),
1106
+ env: `NOYDB_USER=
1107
+ NOYDB_SECRET=
1108
+ NOYDB_DYNAMO_TABLE=collab-live
1109
+ `
1110
+ };
1111
+ case "H":
1112
+ return {
1113
+ profile,
1114
+ notes: "USB-portable \u2014 everything on a single file store, no cloud.",
1115
+ code: [
1116
+ `import { createNoydb } from '@noy-db/hub'`,
1117
+ `import { toFile } from '@noy-db/to-file'`,
1118
+ ``,
1119
+ `export default {`,
1120
+ ` store: toFile({ dir: process.env.NOYDB_DATA_DIR ?? '/Volumes/MY_USB/data' }),`,
1121
+ ` user: process.env.NOYDB_USER!,`,
1122
+ ` secret: process.env.NOYDB_SECRET,`,
1123
+ `}`
1124
+ ].join("\n"),
1125
+ env: `NOYDB_USER=
1126
+ NOYDB_SECRET=
1127
+ NOYDB_DATA_DIR=/Volumes/MY_USB/data
1128
+ `
1129
+ };
1130
+ case "I":
1131
+ return {
1132
+ profile,
1133
+ notes: "Multi-tenant geographic sharding \u2014 per-vault primary per region.",
1134
+ code: [
1135
+ `import { createNoydb } from '@noy-db/hub'`,
1136
+ `import { toAwsDynamo } from '@noy-db/to-aws-dynamo'`,
1137
+ ``,
1138
+ `// Pick the nearest region at init time based on tenant config.`,
1139
+ `const region = process.env.NOYDB_REGION ?? 'ap-southeast-1'`,
1140
+ `const table = \`myapp-\${region}\``,
1141
+ ``,
1142
+ `export default {`,
1143
+ ` store: toAwsDynamo({ table, region }),`,
1144
+ ` user: process.env.NOYDB_USER!,`,
1145
+ ` secret: process.env.NOYDB_SECRET,`,
1146
+ `}`
1147
+ ].join("\n"),
1148
+ env: `NOYDB_USER=
1149
+ NOYDB_SECRET=
1150
+ NOYDB_REGION=ap-southeast-1
1151
+ `
1152
+ };
1153
+ case "J":
1154
+ return {
1155
+ profile,
1156
+ notes: "Authentication bridge (secret-less unlock via OIDC or WebAuthn).",
1157
+ code: [
1158
+ `import { createNoydb } from '@noy-db/hub'`,
1159
+ `import { toBrowserIdb } from '@noy-db/to-browser-idb'`,
1160
+ `// import { unlockWebAuthn } from '@noy-db/on-webauthn' // in the browser`,
1161
+ `// import { unlockOidc } from '@noy-db/on-oidc' // in a server app`,
1162
+ ``,
1163
+ `export default {`,
1164
+ ` store: toBrowserIdb({ prefix: process.env.NOYDB_APP ?? 'app' }),`,
1165
+ ` user: process.env.NOYDB_USER!,`,
1166
+ ` // secret supplied by the unlock method at openVault() time, not here.`,
1167
+ `}`
1168
+ ].join("\n"),
1169
+ env: `NOYDB_USER=
1170
+ NOYDB_APP=app
1171
+ `
1172
+ };
1173
+ }
1174
+ }
1175
+ function listProfiles() {
1176
+ return PROFILE_IDS.map((id) => ` ${id} ${scaffold(id).notes}`).join("\n");
1177
+ }
1178
+ function runConfigProfiles() {
1179
+ process.stdout.write(`Topology profiles for \`noy-db config scaffold --profile=<id>\`:
1180
+ ${listProfiles()}
1181
+ `);
1182
+ return 0;
1183
+ }
1184
+ async function runConfigScaffold(argv) {
1185
+ if (argv.includes("--help") || argv.includes("-h")) {
1186
+ process.stdout.write(`Usage: noy-db config scaffold [--profile=<id>] (default A)
1187
+
1188
+ ${listProfiles()}
1189
+
1190
+ The config goes to stdout; the .env template goes to stderr.
1191
+ `);
1192
+ return 0;
1193
+ }
1194
+ const profileArg = argv.find((a) => a.startsWith("--profile="));
1195
+ const profile = profileArg?.split("=")[1] ?? "A";
1196
+ if (!PROFILE_IDS.includes(profile)) {
1197
+ process.stderr.write(`unknown profile: ${profile}. Valid: ${PROFILE_IDS.join(" ")} \u2014 run \`noy-db config profiles\`
1198
+ `);
1199
+ return 2;
1200
+ }
1201
+ const out = scaffold(profile);
1202
+ process.stdout.write(`// \u2500\u2500 noy-db config (profile ${out.profile}) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1203
+ `);
1204
+ process.stdout.write(`// ${out.notes}
1205
+
1206
+ `);
1207
+ process.stdout.write(out.code + "\n");
1208
+ if (out.env) {
1209
+ process.stderr.write(`
1210
+ # \u2500\u2500 .env template (profile ${out.profile}) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1211
+ `);
1212
+ process.stderr.write(out.env);
1213
+ }
1214
+ return 0;
1215
+ }
1216
+ function isRecord(v) {
1217
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1218
+ }
1219
+ function isStoreShape(v) {
1220
+ if (!isRecord(v)) return false;
1221
+ return ["get", "put", "delete", "list", "loadAll", "saveAll"].every((m) => typeof v[m] === "function");
1222
+ }
1223
+ function normalizeSync(v) {
1224
+ if (Array.isArray(v)) return v;
1225
+ return [v];
1226
+ }
1227
+ function validateTarget(t, path4, issues) {
1228
+ if (!isRecord(t)) {
1229
+ issues.push({
1230
+ severity: "error",
1231
+ code: "bad-target",
1232
+ path: path4,
1233
+ message: "sync target must be an object with { store, role }"
1234
+ });
1235
+ return;
1236
+ }
1237
+ if (t["role"] === void 0) {
1238
+ if (!isStoreShape(t)) {
1239
+ issues.push({
1240
+ severity: "error",
1241
+ code: "bad-store-shape",
1242
+ path: path4,
1243
+ message: "sync target store does not expose the 6-method contract"
1244
+ });
1245
+ }
1246
+ return;
1247
+ }
1248
+ const validRoles = ["sync-peer", "backup", "archive"];
1249
+ if (!validRoles.includes(safeStringify(t["role"]))) {
1250
+ issues.push({
1251
+ severity: "error",
1252
+ code: "bad-role",
1253
+ path: `${path4}.role`,
1254
+ message: `role must be one of ${validRoles.join(", ")}; got ${safeStringify(t["role"])}`
1255
+ });
1256
+ }
1257
+ if (!t["store"] || !isStoreShape(t["store"])) {
1258
+ issues.push({
1259
+ severity: "error",
1260
+ code: "bad-store-shape",
1261
+ path: `${path4}.store`,
1262
+ message: "sync target store does not expose the 6-method contract"
1263
+ });
1264
+ }
1265
+ if (t["role"] === "archive" && isRecord(t["policy"]) && isRecord(t["policy"]["pull"])) {
1266
+ issues.push({
1267
+ severity: "error",
1268
+ code: "archive-pull-configured",
1269
+ path: `${path4}.policy.pull`,
1270
+ message: "archive targets are push-only \u2014 pull policy is invalid"
1271
+ });
1272
+ }
1273
+ }
1274
+
1275
+ // src/commands/pod/monitor.ts
1276
+ async function runMonitor(argv) {
1277
+ const file = argv[0];
1278
+ if (!file) {
1279
+ process.stderr.write("usage: noy-db monitor <config.ts> [--interval=ms]\n");
1280
+ return 2;
1281
+ }
1282
+ const intervalArg = argv.find((a) => a.startsWith("--interval="));
1283
+ const intervalMs = intervalArg ? parseInt(intervalArg.split("=")[1] ?? "5000", 10) : 5e3;
1284
+ let opts;
1285
+ try {
1286
+ const loaded = await loadOptionsFromFile(file);
1287
+ if (typeof loaded !== "object" || loaded === null) {
1288
+ process.stderr.write(`config file must export a NoydbOptions-shaped object
1289
+ `);
1290
+ return 1;
1291
+ }
1292
+ opts = loaded;
1293
+ } catch (err) {
1294
+ process.stderr.write(`failed to load ${file}: ${err.message}
1295
+ `);
1296
+ return 1;
1297
+ }
1298
+ const innerStore = opts["store"];
1299
+ if (!innerStore || typeof innerStore !== "object") {
1300
+ process.stderr.write("config has no `store` \u2014 nothing to monitor\n");
1301
+ return 1;
1302
+ }
1303
+ const meter = innerStore.meter;
1304
+ if (!meter || typeof meter.snapshot !== "function") {
1305
+ process.stderr.write(
1306
+ "config `store` carries no meter handle \u2014 wrap it in the config file:\n import { toMeter } from '@noy-db/to-meter'\n export default { store: toMeter(inner, { degradedMs: 500 }), ... }\nonDegraded / onRestored belong there too.\n"
1307
+ );
1308
+ return 1;
1309
+ }
1310
+ const hub = await import("@noy-db/hub");
1311
+ await hub.createNoydb(opts);
1312
+ process.stdout.write(`monitoring ${file} \u2014 interval ${intervalMs}ms \u2014 Ctrl-C to stop
1313
+
1314
+ `);
1315
+ const stop = installSigintHandler(meter);
1316
+ return new Promise((resolveP) => {
1317
+ const timer = setInterval(() => {
1318
+ if (stop.signalled) {
1319
+ clearInterval(timer);
1320
+ meter.close();
1321
+ resolveP(0);
1322
+ return;
1323
+ }
1324
+ const snap = meter.snapshot();
1325
+ process.stdout.write(formatSnapshot(snap) + "\n");
1326
+ }, intervalMs);
1327
+ });
1328
+ }
1329
+ function installSigintHandler(meter) {
1330
+ const state = { signalled: false };
1331
+ const handler = () => {
1332
+ state.signalled = true;
1333
+ meter.close();
1334
+ };
1335
+ process.on("SIGINT", handler);
1336
+ process.on("SIGTERM", handler);
1337
+ return state;
1338
+ }
1339
+ function formatSnapshot(snap) {
1340
+ const lines = [];
1341
+ const ts = new Date(snap.collectedAt).toISOString().slice(11, 19);
1342
+ lines.push(`[${ts}] status=${snap.status} calls=${snap.totalCalls} casConflicts=${snap.casConflicts} windowMs=${snap.windowMs}`);
1343
+ for (const m of ["get", "put", "delete", "list", "loadAll", "saveAll"]) {
1344
+ const s = snap.byMethod[m];
1345
+ if (s.count === 0) continue;
1346
+ 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`);
1347
+ }
1348
+ return lines.join("\n");
1349
+ }
1350
+
1351
+ // src/commands/doctor.ts
1352
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
1353
+ import { dirname as dirname3, join as join3, resolve as resolve2 } from "path";
1354
+ import { pathToFileURL as pathToFileURL2 } from "url";
1355
+ var f = (id, level, message, fix, where) => where === void 0 ? { id, level, message, fix } : { id, level, message, fix, where };
1356
+ var isRecord2 = (v) => typeof v === "object" && v !== null;
1357
+ function gitignores(root, name) {
1358
+ const p = join3(root, ".gitignore");
1359
+ if (!existsSync2(p)) return false;
1360
+ return readFileSync2(p, "utf8").split("\n").some((l) => {
1361
+ const t = l.trim();
1362
+ return t === name || t === `/${name}` || t === `${name}*` || t === ".env*" && name.startsWith(".env");
1363
+ });
1364
+ }
1365
+ async function listSafely(store, vault, collection) {
1366
+ try {
1367
+ return await store.list(vault, collection);
1368
+ } catch {
1369
+ return null;
1370
+ }
1371
+ }
1372
+ var secretChecks = {
1373
+ id: "NOY-S",
1374
+ run: ({ options, configPath, mode }) => {
1375
+ if (!options) return [];
1376
+ const out = [];
1377
+ const secret = options["secret"];
1378
+ if (typeof secret === "string" && secret.length > 0) {
1379
+ out.push(f(
1380
+ "NOY-S001",
1381
+ "error",
1382
+ "the secret is a literal string in the config \u2014 it ships with your source",
1383
+ "read it from the environment (`secret: process.env.NOYDB_SECRET`) or prompt for it; keep `.env` out of git (see NOY-H001)",
1384
+ configPath
1385
+ ));
1386
+ } else if (secret === void 0 && options["getKeyring"] === void 0 && options["secretMode"] !== "managed") {
1387
+ out.push(f(
1388
+ "NOY-S002",
1389
+ mode === "prod" ? "warn" : "info",
1390
+ "no secret and no getKeyring in the config \u2014 the app must supply one at runtime",
1391
+ "that is fine for an app that prompts; set `secret: process.env.NOYDB_SECRET` for a headless process",
1392
+ configPath
1393
+ ));
1394
+ }
1395
+ if (options["secretMode"] === "managed" && options["sealingKey"] === void 0) {
1396
+ out.push(f(
1397
+ "NOY-S003",
1398
+ "error",
1399
+ '`secretMode: "managed"` without a `sealingKey` \u2014 managed mode has nothing to seal with',
1400
+ "pass a sealer from an `@noy-db/at-*` package (`sealingKey: awsKms({...})`), or drop managed mode",
1401
+ configPath
1402
+ ));
1403
+ }
1404
+ return out;
1405
+ }
1406
+ };
1407
+ var capsuleChecks = {
1408
+ id: "NOY-C",
1409
+ run: ({ options, configPath, mode, manifest }) => {
1410
+ const out = [];
1411
+ if (options?.["encrypt"] === false) {
1412
+ out.push(f(
1413
+ "NOY-C001",
1414
+ mode === "prod" ? "error" : "warn",
1415
+ "`encrypt: false` \u2014 records are stored as plaintext; this is not zero-knowledge",
1416
+ 'remove `encrypt: false` for anything that leaves your machine; it exists for the "see the ciphertext" control and tests',
1417
+ configPath
1418
+ ));
1419
+ }
1420
+ const deps = { ...manifest.dependencies, ...manifest.devDependencies };
1421
+ if ("@noy-db/exclave-plain" in deps) {
1422
+ out.push(f(
1423
+ "NOY-C002",
1424
+ mode === "prod" ? "error" : "warn",
1425
+ "`@noy-db/exclave-plain` is installed \u2014 when bound (`noy-db:exclave-plain` condition) the vault stores readable rows",
1426
+ "keep it to builds whose access control lives in the store (IAM, a DB grant); never in a build that serves untrusted readers"
1427
+ ));
1428
+ }
1429
+ return out;
1430
+ }
1431
+ };
1432
+ var topologyChecks = {
1433
+ id: "NOY-T",
1434
+ run: ({ options, configPath, mode }) => {
1435
+ if (!options) return [];
1436
+ const out = [];
1437
+ const store = options["store"];
1438
+ const storeName = isRecord2(store) && typeof store["name"] === "string" ? store["name"] : void 0;
1439
+ if (storeName === "memory" || storeName === "memoryStore") {
1440
+ out.push(f(
1441
+ "NOY-T001",
1442
+ mode === "prod" ? "error" : "info",
1443
+ "the store is in-memory \u2014 data does not survive a reload",
1444
+ "pick a persistent store: `noy-db add @noy-db/to-browser-idb` (browser) or `@noy-db/to-file` (Node)",
1445
+ configPath
1446
+ ));
1447
+ }
1448
+ const sync = options["sync"];
1449
+ const hasSync = Array.isArray(sync) ? sync.length > 0 : sync !== void 0 && sync !== null;
1450
+ if (!hasSync) {
1451
+ out.push(f(
1452
+ "NOY-T002",
1453
+ "info",
1454
+ "no sync target \u2014 this vault lives in one store only; lose it and the data is gone",
1455
+ "add a second store as a sync target (a cloud bucket, a file on a USB stick): `noy-db config profiles` shows the shapes",
1456
+ configPath
1457
+ ));
1458
+ }
1459
+ return out;
1460
+ }
1461
+ };
1462
+ var recoveryChecks = {
1463
+ id: "NOY-R",
1464
+ run: async ({ store, vaults, options, mode }) => {
1465
+ if (!store || vaults.length === 0) return [];
1466
+ const out = [];
1467
+ for (const vault of vaults) {
1468
+ const users = await listSafely(store, vault, "_keyring");
1469
+ if (users === null) continue;
1470
+ if (users.length === 0) continue;
1471
+ const entries = async (id) => {
1472
+ try {
1473
+ const env = await store.get(vault, "_meta", id);
1474
+ if (!env) return 0;
1475
+ const doc = JSON.parse(env._data ?? "");
1476
+ return Array.isArray(doc.entries) ? doc.entries.length : 0;
1477
+ } catch {
1478
+ return 0;
1479
+ }
1480
+ };
1481
+ const paper = await entries("recovery-paper");
1482
+ const shamir = await entries("recovery-shamir");
1483
+ if (paper + shamir === 0) {
1484
+ out.push(f(
1485
+ "NOY-R001",
1486
+ mode === "prod" ? "error" : "warn",
1487
+ `vault "${vault}" has no recovery enrolled \u2014 a forgotten secret means the data is gone`,
1488
+ "enrol one: `noy-db add @noy-db/on-recovery` (printable codes) or `@noy-db/on-shamir` (k-of-n shares), then `db.team.enrollRecovery(...)`",
1489
+ vault
1490
+ ));
1491
+ } else if (shamir === 0 && options?.["secretMode"] === "managed") {
1492
+ out.push(f(
1493
+ "NOY-R002",
1494
+ "error",
1495
+ `vault "${vault}" is managed-mode with only paper recovery \u2014 managed mode requires a strong (Shamir) profile`,
1496
+ "enrol Shamir: `noy-db add @noy-db/on-shamir`",
1497
+ vault
1498
+ ));
1499
+ }
1500
+ let owners = 0;
1501
+ for (const uid of users) {
1502
+ try {
1503
+ const env = await store.get(vault, "_keyring", uid);
1504
+ const file = env ? JSON.parse(env._data ?? "") : null;
1505
+ if (file?.role === "owner") owners++;
1506
+ } catch {
1507
+ }
1508
+ }
1509
+ if (owners === 1 && users.length > 1) {
1510
+ out.push(f(
1511
+ "NOY-R003",
1512
+ "info",
1513
+ `vault "${vault}" has one owner among ${users.length} users`,
1514
+ "a second owner can recover members and rotate keys when the first is unavailable: `noy-db user add <id> owner \u2026`",
1515
+ vault
1516
+ ));
1517
+ }
1518
+ }
1519
+ return out;
1520
+ }
1521
+ };
1522
+ var dependencyChecks = {
1523
+ id: "NOY-D",
1524
+ run: ({ root, manifest, configPath }) => {
1525
+ const out = [];
1526
+ const deps = { ...manifest.dependencies, ...manifest.devDependencies };
1527
+ const hub = deps["@noy-db/hub"];
1528
+ if (!hub) {
1529
+ out.push(f("NOY-D001", "error", "`@noy-db/hub` is not a dependency of this project", "add it: `pnpm add @noy-db/hub`", join3(root, "package.json")));
1530
+ }
1531
+ if (configPath && existsSync2(configPath)) {
1532
+ const src = readFileSync2(configPath, "utf8");
1533
+ const named = new Set([...src.matchAll(/from\s+['"](@noy-db\/[a-z0-9-]+)/g)].map((m) => m[1]));
1534
+ for (const pkg of named) {
1535
+ if (!(pkg in deps)) {
1536
+ out.push(f("NOY-D002", "error", `the config imports \`${pkg}\`, which is not in package.json`, `install it: \`pnpm add ${pkg}\``, configPath));
1537
+ }
1538
+ }
1539
+ }
1540
+ return out;
1541
+ }
1542
+ };
1543
+ var hygieneChecks = {
1544
+ id: "NOY-H",
1545
+ run: ({ root }) => {
1546
+ const out = [];
1547
+ const hasEnv = existsSync2(join3(root, ".env")) || existsSync2(join3(root, ".env.local"));
1548
+ if (hasEnv && !gitignores(root, ".env")) {
1549
+ out.push(f(
1550
+ "NOY-H001",
1551
+ "error",
1552
+ "a `.env` file exists and `.gitignore` does not ignore it \u2014 one `git add .` away from committing secrets",
1553
+ "add `.env` and `.env.*` to .gitignore (`noy-db doctor --fix` does this)",
1554
+ join3(root, ".gitignore")
1555
+ ));
1556
+ }
1557
+ return out;
1558
+ }
1559
+ };
1560
+ var BUILTIN_CHECKS = [secretChecks, capsuleChecks, topologyChecks, recoveryChecks, dependencyChecks, hygieneChecks];
1561
+ async function satelliteChecks(root, manifest) {
1562
+ const checks = [];
1563
+ const findings = [];
1564
+ const names = Object.keys({ ...manifest.dependencies, ...manifest.devDependencies }).filter((n) => n.startsWith("@noy-db/") && n !== "@noy-db/hub");
1565
+ for (const name of names) {
1566
+ const manifestPath = join3(root, "node_modules", ...name.split("/"), "package.json");
1567
+ if (!existsSync2(manifestPath)) continue;
1568
+ try {
1569
+ const pkg = JSON.parse(readFileSync2(manifestPath, "utf8"));
1570
+ const entry = isRecord2(pkg.exports) && "." in pkg.exports ? (() => {
1571
+ const dot = pkg.exports["."];
1572
+ return typeof dot === "string" ? dot : isRecord2(dot) ? dot["default"] ?? dot["import"] : void 0;
1573
+ })() : pkg.module ?? pkg.main;
1574
+ if (!entry) continue;
1575
+ const mod = await import(pathToFileURL2(join3(dirname3(manifestPath), entry)).href);
1576
+ if (Array.isArray(mod.noydbDoctorChecks)) {
1577
+ for (const c of mod.noydbDoctorChecks) if (c && typeof c.id === "string" && typeof c.run === "function") checks.push(c);
1578
+ }
1579
+ } catch (err) {
1580
+ findings.push(f("NOY-D003", "warn", `\`${name}\` is installed but failed to import: ${err.message.split("\n")[0]}`, "reinstall it, or check its peer dependencies", name));
1581
+ }
1582
+ }
1583
+ return { checks, findings };
1584
+ }
1585
+ async function doctor(opts = {}) {
1586
+ const root = resolve2(opts.root ?? process.cwd());
1587
+ const mode = opts.mode ?? (process.env["NODE_ENV"] === "production" ? "prod" : "dev");
1588
+ const manifestPath = join3(root, "package.json");
1589
+ const manifest = existsSync2(manifestPath) ? JSON.parse(readFileSync2(manifestPath, "utf8")) : {};
1590
+ const configPath = opts.configPath ?? ["noydb.config.mjs", "noydb.config.js"].map((n) => join3(root, n)).find((p) => existsSync2(p));
1591
+ let options;
1592
+ let store;
1593
+ const findings = [];
1594
+ if (configPath) {
1595
+ try {
1596
+ const loaded = await loadOptionsFromFile(configPath);
1597
+ if (isRecord2(loaded)) {
1598
+ options = loaded;
1599
+ const s = loaded["store"];
1600
+ if (isRecord2(s) && typeof s["get"] === "function" && typeof s["list"] === "function") store = s;
1601
+ }
1602
+ } catch (err) {
1603
+ findings.push(f("NOY-D004", "error", `the config file did not load: ${err.message.split("\n")[0]}`, "run `noy-db config validate <file>` for the details", configPath));
1604
+ }
1605
+ }
1606
+ let vaults = opts.vaults ?? [];
1607
+ if (vaults.length === 0 && store && typeof store.listVaults === "function") {
1608
+ try {
1609
+ vaults = await store.listVaults();
1610
+ } catch {
1611
+ vaults = [];
1612
+ }
1613
+ }
1614
+ const ctx = { mode, root, manifest, vaults, ...configPath ? { configPath } : {}, ...options ? { options } : {}, ...store ? { store } : {} };
1615
+ const sat = opts.noSatellites ? { checks: [], findings: [] } : await satelliteChecks(root, manifest);
1616
+ findings.push(...sat.findings);
1617
+ const checks = [...BUILTIN_CHECKS, ...sat.checks];
1618
+ const ran = [];
1619
+ for (const c of checks) {
1620
+ ran.push(c.id);
1621
+ try {
1622
+ findings.push(...await c.run(ctx));
1623
+ } catch (err) {
1624
+ findings.push(f("NOY-D005", "warn", `check ${c.id} threw: ${err.message.split("\n")[0]}`, "report this against the package that registered the check"));
1625
+ }
1626
+ }
1627
+ const counts = { error: 0, warn: 0, info: 0 };
1628
+ for (const x of findings) counts[x.level]++;
1629
+ return { mode, findings, counts, ran };
1630
+ }
1631
+ function applyFixes(root, report) {
1632
+ const done = [];
1633
+ if (report.findings.some((x) => x.id === "NOY-H001")) {
1634
+ const p = join3(root, ".gitignore");
1635
+ const cur = existsSync2(p) ? readFileSync2(p, "utf8") : "";
1636
+ const add = [".env", ".env.*"].filter((l) => !cur.split("\n").some((x) => x.trim() === l));
1637
+ if (add.length) {
1638
+ const next = cur + (cur.endsWith("\n") || cur === "" ? "" : "\n") + "# noy-db doctor (NOY-H001): never commit secrets\n" + add.join("\n") + "\n";
1639
+ writeFileSync(p, next);
1640
+ done.push(`.gitignore += ${add.join(", ")}`);
1641
+ }
1642
+ }
1643
+ return done;
1644
+ }
1645
+ var LEVEL_RANK = { error: 0, warn: 1, info: 2 };
1646
+ var ICON = { error: "\u2717", warn: "!", info: "\xB7" };
1647
+ function formatReport(r) {
1648
+ const lines = [`noy-db doctor \u2014 mode ${r.mode}, ${r.ran.length} check group(s)`];
1649
+ if (r.findings.length === 0) lines.push(" \u2713 nothing to report");
1650
+ for (const x of [...r.findings].sort((a, b) => LEVEL_RANK[a.level] - LEVEL_RANK[b.level] || a.id.localeCompare(b.id))) {
1651
+ lines.push(` ${ICON[x.level]} ${x.id} ${x.message}${x.where ? ` (${x.where})` : ""}`);
1652
+ lines.push(` \u2192 ${x.fix}`);
1653
+ }
1654
+ lines.push(` ${r.counts.error} error \xB7 ${r.counts.warn} warn \xB7 ${r.counts.info} info`);
1655
+ return lines.join("\n");
1656
+ }
1657
+ async function runDoctor(argv) {
1658
+ const flag = (n) => argv.find((a) => a.startsWith(`--${n}=`))?.slice(n.length + 3);
1659
+ if (argv.includes("--help") || argv.includes("-h")) {
1660
+ process.stdout.write([
1661
+ "Usage: noy-db doctor [config.mjs] [--mode=dev|prod] [--vault=<name>] [--json] [--fix] [--fail-level=error|warn|info]",
1662
+ "",
1663
+ "Readiness checks that need no secret. Ids are stable (NOY-S secret \xB7 NOY-R recovery \xB7 NOY-T topology \xB7",
1664
+ "NOY-C capsule \xB7 NOY-D dependencies \xB7 NOY-H hygiene); every finding carries a fix.",
1665
+ "",
1666
+ " --mode dev (default unless NODE_ENV=production) or prod \u2014 the same finding can be a warn in dev and an error in prod",
1667
+ " --fail-level exit 1 at or above this level. DEFAULT: error. Pass --fail-level=warn to gate a CI job on warnings too.",
1668
+ " --fix apply the idempotent fixes (today: gitignore .env)",
1669
+ " --json machine-readable report",
1670
+ " crypto the in-memory crypto round-trip (formerly `noy-db verify`)"
1671
+ ].join("\n") + "\n");
1672
+ return 0;
1673
+ }
1674
+ const configArg = argv.find((a) => !a.startsWith("--"));
1675
+ const modeArg = flag("mode");
1676
+ if (modeArg !== void 0 && modeArg !== "dev" && modeArg !== "prod") {
1677
+ process.stderr.write(`--mode must be dev or prod (got ${modeArg})
1678
+ `);
1679
+ return 2;
1680
+ }
1681
+ const failArg = flag("fail-level") ?? "error";
1682
+ if (!(failArg in LEVEL_RANK)) {
1683
+ process.stderr.write(`--fail-level must be error, warn or info (got ${failArg})
1684
+ `);
1685
+ return 2;
1686
+ }
1687
+ const vault = flag("vault");
1688
+ const report = await doctor({
1689
+ ...configArg ? { configPath: resolve2(configArg) } : {},
1690
+ ...modeArg ? { mode: modeArg } : {},
1691
+ ...vault ? { vaults: [vault] } : {}
1692
+ });
1693
+ let fixed = [];
1694
+ if (argv.includes("--fix")) fixed = applyFixes(process.cwd(), report);
1695
+ if (argv.includes("--json")) process.stdout.write(JSON.stringify({ ...report, fixed }, null, 2) + "\n");
1696
+ else {
1697
+ process.stdout.write(formatReport(report) + "\n");
1698
+ for (const d of fixed) process.stdout.write(` fixed: ${d}
1699
+ `);
1700
+ process.stdout.write(` (exit is non-zero at --fail-level=${failArg}; default error)
1701
+ `);
1702
+ }
1703
+ const worst = Math.min(...report.findings.map((x) => LEVEL_RANK[x.level]), 99);
1704
+ return worst <= LEVEL_RANK[failArg] ? 1 : 0;
1705
+ }
1706
+
1707
+ // src/commands/add-package.ts
1708
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
1709
+ import { join as join4 } from "path";
1710
+ import { spawnSync } from "child_process";
1711
+ import pc from "picocolors";
1712
+
1713
+ // src/wizard/render.ts
1714
+ import { promises as fs3 } from "fs";
1715
+ import path3 from "path";
1716
+ import { fileURLToPath } from "url";
1717
+ function templateDir(name) {
1718
+ const here = fileURLToPath(import.meta.url);
1719
+ const packageRoot = path3.resolve(path3.dirname(here), "..", "..");
1720
+ return path3.join(packageRoot, "templates", name);
1721
+ }
1722
+
1723
+ // src/commands/add-package.ts
1724
+ var CATALOG_URL = "https://noy-db.lanna.ai/catalog.json";
1725
+ function bundledCatalog() {
1726
+ return JSON.parse(readFileSync3(templateDir("catalog.json"), "utf8"));
1727
+ }
1728
+ async function loadCatalog(opts = {}) {
1729
+ if (!opts.offline) {
1730
+ try {
1731
+ const ctl = new AbortController();
1732
+ const t = setTimeout(() => ctl.abort(), opts.timeoutMs ?? 3e3);
1733
+ const res = await (opts.fetchImpl ?? fetch)(CATALOG_URL, { signal: ctl.signal });
1734
+ clearTimeout(t);
1735
+ if (res.ok) {
1736
+ const catalog = await res.json();
1737
+ if (catalog.schemaVersion === 1 && Array.isArray(catalog.packages)) return { catalog, source: "live" };
1738
+ }
1739
+ } catch {
1740
+ }
1741
+ }
1742
+ return { catalog: bundledCatalog(), source: "bundled" };
1743
+ }
1744
+ function normalisePrefix(input) {
1745
+ if (!input) return void 0;
1746
+ const s = input.replace(/^@noy-db\//, "").replace(/-$/, "");
1747
+ return /^(to|in|on|as|by|at)$/.test(s) ? `${s}-` : void 0;
1748
+ }
1749
+ function formatList(catalog, prefix) {
1750
+ const groups = /* @__PURE__ */ new Map();
1751
+ for (const p of catalog.packages) {
1752
+ if (prefix && p.prefix !== prefix) continue;
1753
+ (groups.get(p.prefix) ?? groups.set(p.prefix, []).get(p.prefix)).push(p);
1754
+ }
1755
+ const out = [];
1756
+ for (const [pfx, pkgs] of groups) {
1757
+ const g = catalog.prefixes[pfx];
1758
+ out.push(`${pc.bold(pfx)} ${g?.reads ?? ""} ${pc.dim(`\xB7 plaintext: ${g?.plaintext ?? "?"}`)}`);
1759
+ for (const p of pkgs) out.push(` ${p.package.padEnd(30)} ${p.name}${p.status !== "stable" ? pc.dim(` (${p.status})`) : ""}`);
1760
+ out.push("");
1761
+ }
1762
+ out.push(pc.dim(`noy-db add <package> installs one and tells you what trust boundary it crosses.`));
1763
+ return out.join("\n");
1764
+ }
1765
+ function findPackage(catalog, input) {
1766
+ const full = input.startsWith("@noy-db/") ? input : `@noy-db/${input}`;
1767
+ return catalog.packages.find((p) => p.package === full) ?? catalog.packages.find((p) => p.package.endsWith(`-${input}`));
1768
+ }
1769
+ function detectPackageManager(root) {
1770
+ if (existsSync3(join4(root, "bun.lockb")) || existsSync3(join4(root, "bun.lock"))) return "bun";
1771
+ if (existsSync3(join4(root, "yarn.lock"))) return "yarn";
1772
+ if (existsSync3(join4(root, "package-lock.json"))) return "npm";
1773
+ return "pnpm";
1774
+ }
1775
+ function installArgv(pm, pkg, dev) {
1776
+ switch (pm) {
1777
+ case "npm":
1778
+ return ["npm", "install", ...dev ? ["--save-dev"] : [], pkg];
1779
+ case "yarn":
1780
+ return ["yarn", "add", ...dev ? ["--dev"] : [], pkg];
1781
+ case "bun":
1782
+ return ["bun", "add", ...dev ? ["--dev"] : [], pkg];
1783
+ default:
1784
+ return ["pnpm", "add", ...dev ? ["-D"] : [], pkg];
1785
+ }
1786
+ }
1787
+ function snippetFor(p) {
1788
+ const name = p.package.replace("@noy-db/", "");
1789
+ const camel = name.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
1790
+ switch (p.prefix) {
1791
+ case "to-":
1792
+ return `import { ${camel} } from '${p.package}'
1793
+ // createNoydb({ store: ${camel}({ /* \u2026 */ }), user, secret }) \u2014 or as a sync target: sync: [{ store: ${camel}(...), role: 'backup' }]`;
1794
+ case "as-":
1795
+ return `import { ${camel.replace(/^as/, "to")} } from '${p.package}'
1796
+ // await vault.export(${camel.replace(/^as/, "to")}(), { collections: ['\u2026'] }) \u26A0\uFE0F the output is plaintext`;
1797
+ case "on-":
1798
+ return `import \u2026 from '${p.package}'
1799
+ // an unlock method is enrolled per user; see the package README for enroll*/unlock* and \`noy-db doctor\` for what is enrolled`;
1800
+ case "by-":
1801
+ return `import \u2026 from '${p.package}'
1802
+ // a session-share transport; pair it on both peers that already hold the secret`;
1803
+ case "at-":
1804
+ return `import \u2026 from '${p.package}'
1805
+ // createNoydb({ secretMode: 'managed', sealingKey: <provider>(\u2026) }) \u26A0\uFE0F the host you name can decrypt the slice it unseals`;
1806
+ default:
1807
+ return `import \u2026 from '${p.package}'`;
1808
+ }
1809
+ }
1810
+ async function addPackage(input, opts = {}) {
1811
+ const root = opts.root ?? process.cwd();
1812
+ const { catalog } = await loadCatalog({ ...opts.offline !== void 0 ? { offline: opts.offline } : {}, ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {} });
1813
+ const pkg = findPackage(catalog, input);
1814
+ if (!pkg) {
1815
+ const near = catalog.packages.filter((p) => p.package.includes(input.replace(/^@noy-db\//, ""))).map((p) => p.package).slice(0, 5);
1816
+ throw new Error(`no package named "${input}" in the catalog${near.length ? ` \u2014 did you mean: ${near.join(", ")}` : ""}. \`noy-db add --list\` shows them all.`);
1817
+ }
1818
+ const pm = detectPackageManager(root);
1819
+ const command = installArgv(pm, pkg.package, opts.dev ?? false);
1820
+ let installed = false;
1821
+ if (!opts.dryRun) {
1822
+ const r = spawnSync(command[0], command.slice(1), { cwd: root, stdio: "inherit" });
1823
+ if (r.status !== 0) throw new Error(`${command.join(" ")} exited ${r.status ?? "with a signal"}`);
1824
+ installed = true;
1825
+ }
1826
+ let wired = null;
1827
+ if (pkg.prefix === "in-" && pkg.package === "@noy-db/in-nuxt") {
1828
+ const cfg = ["nuxt.config.ts", "nuxt.config.js", "nuxt.config.mjs"].map((n) => join4(root, n)).find((p) => existsSync3(p));
1829
+ if (cfg && !opts.dryRun) {
1830
+ const { augmentNuxtConfig: augmentNuxtConfig2, writeAugmentedConfig: writeAugmentedConfig2 } = await Promise.resolve().then(() => (init_augment(), augment_exports));
1831
+ const res = await augmentNuxtConfig2({ configPath: cfg, adapter: "browser" });
1832
+ if (res.kind === "proposed-change") {
1833
+ await writeAugmentedConfig2(cfg, res.newCode);
1834
+ wired = "nuxt-module";
1835
+ } else if (res.kind === "already-configured") wired = "nuxt-module";
1836
+ }
1837
+ }
1838
+ const trust = catalog.prefixes[pkg.prefix]?.trust ?? "";
1839
+ return { pkg, installed, command, wired, trust, snippet: snippetFor(pkg) };
1840
+ }
1841
+ async function runAddPackage(argv) {
1842
+ const flags = new Set(argv.filter((a) => a.startsWith("--")));
1843
+ const positional = argv.filter((a) => !a.startsWith("--"));
1844
+ if (flags.has("--help") || flags.has("-h")) {
1845
+ process.stdout.write([
1846
+ "Usage: noy-db add <package> [--dev] [--dry-run] [--offline]",
1847
+ " noy-db add --list [to|in|on|as|by|at]",
1848
+ "",
1849
+ "Installs a @noy-db package with your package manager, wires it where a config can be edited",
1850
+ "(a Nuxt module), prints the import to paste otherwise, and says what trust boundary the layer crosses.",
1851
+ `The list comes from ${CATALOG_URL}, falling back to the copy this version ships.`
1852
+ ].join("\n") + "\n");
1853
+ return 0;
1854
+ }
1855
+ if (flags.has("--list")) {
1856
+ const prefix = normalisePrefix(positional[0]);
1857
+ if (positional[0] && !prefix) {
1858
+ process.stderr.write(`unknown prefix "${positional[0]}" \u2014 one of to in on as by at
1859
+ `);
1860
+ return 2;
1861
+ }
1862
+ const { catalog, source } = await loadCatalog({ offline: flags.has("--offline") });
1863
+ process.stdout.write(formatList(catalog, prefix) + "\n" + pc.dim(`(catalog: ${source}${source === "bundled" ? ", " + catalog.packages.length + " packages" : ""})`) + "\n");
1864
+ return 0;
1865
+ }
1866
+ const input = positional[0];
1867
+ if (!input) {
1868
+ process.stderr.write("usage: noy-db add <package> (or `noy-db add --list`)\n");
1869
+ return 2;
1870
+ }
1871
+ try {
1872
+ const r = await addPackage(input, { dev: flags.has("--dev"), dryRun: flags.has("--dry-run"), offline: flags.has("--offline") });
1873
+ process.stdout.write(`${r.installed ? pc.green("\u2714") : pc.dim("dry:")} ${r.command.join(" ")}
1874
+ `);
1875
+ if (r.wired) process.stdout.write(`${pc.green("\u2714")} wired into nuxt.config (${r.wired})
1876
+ `);
1877
+ process.stdout.write(`
1878
+ ${pc.bold(r.pkg.prefix)} ${r.trust}
1879
+
1880
+ ${r.snippet}
1881
+ `);
1882
+ return 0;
1883
+ } catch (err) {
1884
+ process.stderr.write(`${pc.red("error:")} ${err.message}
1885
+ `);
1886
+ return 1;
1887
+ }
1888
+ }
1889
+
310
1890
  // src/bin/noy-db.ts
311
1891
  var HELP = `Usage: noy-db <command> [args]
312
1892
 
313
- Commands:
1893
+ Project
314
1894
  add <collection> Scaffold a new collection store + page
315
- add user <userId> <role> [options] Grant a new user access to a vault
316
- verify In-memory crypto integrity check
317
- rotate [options] Rotate DEKs for a vault
1895
+ add <@noy-db/package> [--dev] [--dry-run] Install a package, wire it, print the trust boundary
1896
+ add --list [to|in|on|as|by|at] The landscape \u2014 every package, by layer
1897
+ user add <userId> <role> [options] Grant a new user access to a vault
1898
+ user rotate [options] Rotate DEKs for a vault
318
1899
  backup <target> [options] Dump a vault to a file
319
- help Show this message
1900
+ doctor [config] [--mode=prod] [--fix] Readiness checks: secret, recovery, topology, capsule, deps, hygiene
1901
+ doctor crypto In-memory crypto round-trip check
1902
+
1903
+ Pods (.noydb files)
1904
+ pod inspect <file.noydb> Print the bundle header (no secret)
1905
+ pod verify <file.noydb> Verify bundle integrity (no secret)
1906
+ describe <file.noydb> [options] YAML/JSON schema audit (needs the secret)
320
1907
 
321
- Common options for rotate / add user / backup:
1908
+ Configuration
1909
+ config validate <file.js|mjs> Sanity-check a NoydbOptions file
1910
+ config scaffold [--profile=<id>] Topology skeleton \u2192 stdout, .env template \u2192 stderr
1911
+ config profiles List the ten topology profiles
1912
+
1913
+ Running
1914
+ monitor <file.js|mjs> [--interval=ms] Live store metrics (the store must be metered)
1915
+ inspect <file.js|mjs> --vault=<name> [...] Terminal inspector (installs on demand)
1916
+
1917
+ Common options for user add / user rotate / backup:
322
1918
  --dir <path> Data directory (file adapter). Default: ./data
323
- --vault <name> Vault (tenant) name. Required.
1919
+ --vault <name> Vault (tenant) name. Required.
324
1920
  --user <id> Your user id in the vault. Required.
325
- --collections <list> Comma-separated collection list. Format:
326
- rotate: invoices,clients
327
- add user: invoices:rw,clients:ro (operator/client only)
1921
+ --collections <list> rotate: invoices,clients \xB7 user add: invoices:rw,clients:ro
328
1922
 
329
1923
  Examples:
330
1924
  noy-db add invoices
331
- noy-db add user accountant-ann operator --dir ./data --vault demo-co --user owner-alice --collections invoices:rw,clients:ro
332
- noy-db verify
333
- noy-db rotate --dir ./data --vault demo-co --user owner-alice
334
- noy-db backup ./backups/demo-co-2026-04-07.json --dir ./data --vault demo-co --user owner-alice
335
-
336
- Run from the root of a project that already has a noy-db file
337
- adapter directory in place. For new projects, use
338
- \`npm create noy-db\` instead.
1925
+ noy-db user add ann operator --dir ./data --vault demo-co --user owner-alice --collections invoices:rw
1926
+ noy-db config profiles
1927
+ noy-db config scaffold --profile=B > noydb.config.mjs
1928
+ noy-db doctor --mode=prod --fail-level=warn
1929
+
1930
+ New project? \`npm create noy-db@latest\`.
339
1931
  `;
1932
+ function renamed(from, to) {
1933
+ process.stderr.write(`${pc2.dim(`(\`noy-db ${from}\` is now \`noy-db ${to}\`)`)}
1934
+ `);
1935
+ }
340
1936
  async function main() {
341
1937
  const [, , command, ...rest] = process.argv;
342
1938
  switch (command) {
@@ -346,71 +1942,145 @@ async function main() {
346
1942
  case "--help":
347
1943
  process.stdout.write(HELP);
348
1944
  return;
349
- case "add":
1945
+ case "add": {
1946
+ if (rest[0] === "user") {
1947
+ renamed("add user", "user add");
1948
+ await runAddUser(rest.slice(1));
1949
+ return;
1950
+ }
1951
+ const first = rest[0];
1952
+ if (rest.some((a) => a === "--list" || a === "--help") || first && /^(@noy-db\/|to-|in-|on-|as-|by-|at-)/.test(first)) {
1953
+ process.exit(await runAddPackage(rest));
1954
+ }
350
1955
  await runAdd(rest);
351
1956
  return;
352
- case "verify":
353
- await runVerify();
1957
+ }
1958
+ case "user": {
1959
+ const [sub, ...subRest] = rest;
1960
+ if (sub === "add") {
1961
+ await runAddUser(subRest);
1962
+ return;
1963
+ }
1964
+ if (sub === "rotate") {
1965
+ await runRotate(subRest);
1966
+ return;
1967
+ }
1968
+ fail2(`\`noy-db user\` takes \`add\` or \`rotate\``);
354
1969
  return;
1970
+ }
355
1971
  case "rotate":
1972
+ renamed("rotate", "user rotate");
356
1973
  await runRotate(rest);
357
1974
  return;
358
1975
  case "backup":
359
1976
  await runBackup(rest);
360
1977
  return;
1978
+ case "verify":
1979
+ renamed("verify", "doctor crypto");
1980
+ await runCryptoCheck();
1981
+ return;
1982
+ case "doctor": {
1983
+ if (rest[0] === "crypto") {
1984
+ await runCryptoCheck();
1985
+ return;
1986
+ }
1987
+ process.exit(await runDoctor(rest));
1988
+ return;
1989
+ }
1990
+ case "pod": {
1991
+ const [sub, ...subRest] = rest;
1992
+ if (sub === "inspect") process.exit(await runInspect(subRest));
1993
+ if (sub === "verify") process.exit(await runVerify(subRest));
1994
+ fail2(`\`noy-db pod\` takes \`inspect\` or \`verify\``);
1995
+ return;
1996
+ }
1997
+ case "describe":
1998
+ process.exit(await runDescribe(rest));
1999
+ return;
2000
+ case "config": {
2001
+ const [sub, ...subRest] = rest;
2002
+ if (sub === "validate") process.exit(await runConfigValidate(subRest));
2003
+ if (sub === "scaffold") process.exit(await runConfigScaffold(subRest));
2004
+ if (sub === "profiles") process.exit(runConfigProfiles());
2005
+ fail2(`\`noy-db config\` takes \`validate\`, \`scaffold\` or \`profiles\``);
2006
+ return;
2007
+ }
2008
+ case "monitor":
2009
+ process.exit(await runMonitor(rest));
2010
+ return;
2011
+ case "inspect":
2012
+ process.exit(runTuiInspect(rest));
2013
+ return;
361
2014
  default:
362
- process.stderr.write(`${pc.red("error:")} unknown command '${command}'
2015
+ process.stderr.write(`${pc2.red("error:")} unknown command '${command}'
363
2016
 
364
2017
  ${HELP}`);
365
2018
  process.exit(2);
366
2019
  }
367
2020
  }
2021
+ function fail2(message) {
2022
+ process.stderr.write(`${pc2.red("error:")} ${message}
2023
+ `);
2024
+ process.exit(2);
2025
+ }
2026
+ function runTuiInspect(args) {
2027
+ const bin = resolveBin("@noy-db/in-devtools-tui", "noydb-inspect");
2028
+ if (!bin) {
2029
+ process.stderr.write(
2030
+ `${pc2.red("error:")} the terminal inspector is not installed in this project.
2031
+
2032
+ ${pc2.bold("pnpm add -D @noy-db/in-devtools-tui")} ${pc2.dim("(or npm/yarn/bun)")}
2033
+
2034
+ then re-run \`noy-db inspect\`. It needs a config file, --vault=<name>, and a secret (--secret or NOYDB_SECRET).
2035
+ `
2036
+ );
2037
+ return 2;
2038
+ }
2039
+ const r = spawnSync2(process.execPath, [bin, ...args], { stdio: "inherit" });
2040
+ return r.status ?? 1;
2041
+ }
368
2042
  async function runAdd(args) {
369
2043
  const first = args[0];
370
- if (first === "user") {
371
- await runAddUser(args.slice(1));
372
- return;
373
- }
374
2044
  if (!first) {
375
2045
  process.stderr.write(
376
- `${pc.red("error:")} \`noy-db add\` requires either a collection name or the \`user\` subcommand
2046
+ `${pc2.red("error:")} \`noy-db add\` requires a collection name
377
2047
 
378
2048
  Examples:
379
2049
  noy-db add invoices
380
- noy-db add user alice operator ...
2050
+ noy-db user add alice operator ...
381
2051
  `
382
2052
  );
383
2053
  process.exit(2);
384
2054
  }
385
2055
  try {
386
2056
  const result = await addCollection({ name: first });
387
- process.stdout.write(`${pc.green("\u2714")} Created:
2057
+ process.stdout.write(`${pc2.green("\u2714")} Created:
388
2058
  `);
389
2059
  for (const file of result.files) {
390
- process.stdout.write(` ${pc.dim("\u2192")} ${file}
2060
+ process.stdout.write(` ${pc2.dim("\u2192")} ${file}
391
2061
  `);
392
2062
  }
393
2063
  process.stdout.write(
394
2064
  `
395
- Next: visit ${pc.cyan(`/${first}`)} in your dev server, then edit ${pc.bold(`app/stores/${first}.ts`)} to match your domain.
2065
+ Next: visit ${pc2.cyan(`/${first}`)} in your dev server, then edit ${pc2.bold(`app/stores/${first}.ts`)} to match your domain.
396
2066
  `
397
2067
  );
398
2068
  } catch (err) {
399
- process.stderr.write(`${pc.red("error:")} ${err.message}
2069
+ process.stderr.write(`${pc2.red("error:")} ${err.message}
400
2070
  `);
401
2071
  process.exit(1);
402
2072
  }
403
2073
  }
404
- async function runVerify() {
405
- process.stdout.write(`${pc.dim("\u25B8 Running noy-db integrity check\u2026")}
2074
+ async function runCryptoCheck() {
2075
+ process.stdout.write(`${pc2.dim("\u25B8 Running noy-db crypto round-trip\u2026")}
406
2076
  `);
407
2077
  const result = await verifyIntegrity();
408
2078
  if (result.ok) {
409
- process.stdout.write(`${pc.green("\u2714")} ${result.message} ${pc.dim(`(${result.durationMs}ms)`)}
2079
+ process.stdout.write(`${pc2.green("\u2714")} ${result.message} ${pc2.dim(`(${result.durationMs}ms)`)}
410
2080
  `);
411
2081
  return;
412
2082
  }
413
- process.stderr.write(`${pc.red("\u2718")} ${result.message} ${pc.dim(`(${result.durationMs}ms)`)}
2083
+ process.stderr.write(`${pc2.red("\u2718")} ${result.message} ${pc2.dim(`(${result.durationMs}ms)`)}
414
2084
  `);
415
2085
  process.exit(1);
416
2086
  }
@@ -419,9 +2089,9 @@ async function runAddUser(args) {
419
2089
  const roleInput = args[1];
420
2090
  if (!userId || !roleInput) {
421
2091
  process.stderr.write(
422
- `${pc.red("error:")} \`noy-db add user\` requires <userId> <role>
2092
+ `${pc2.red("error:")} \`noy-db user add\` requires <userId> <role>
423
2093
 
424
- Example: noy-db add user ann operator --dir ./data --vault demo-co --user owner-alice --collections invoices:rw
2094
+ Example: noy-db user add ann operator --dir ./data --vault demo-co --user owner-alice --collections invoices:rw
425
2095
  `
426
2096
  );
427
2097
  process.exit(2);
@@ -430,7 +2100,7 @@ Example: noy-db add user ann operator --dir ./data --vault demo-co --user owner-
430
2100
  try {
431
2101
  role = assertRole(roleInput);
432
2102
  } catch (err) {
433
- process.stderr.write(`${pc.red("error:")} ${err.message}
2103
+ process.stderr.write(`${pc2.red("error:")} ${err.message}
434
2104
  `);
435
2105
  process.exit(2);
436
2106
  return;
@@ -452,11 +2122,11 @@ Example: noy-db add user ann operator --dir ./data --vault demo-co --user owner-
452
2122
  try {
453
2123
  const result = await addUser(opts);
454
2124
  process.stdout.write(
455
- `${pc.green("\u2714")} Granted ${pc.bold(result.role)} access to ${pc.cyan(result.userId)} in vault ${pc.cyan(vault)}.
2125
+ `${pc2.green("\u2714")} Granted ${pc2.bold(result.role)} access to ${pc2.cyan(result.userId)} in vault ${pc2.cyan(vault)}.
456
2126
  `
457
2127
  );
458
2128
  } catch (err) {
459
- process.stderr.write(`${pc.red("error:")} ${err.message}
2129
+ process.stderr.write(`${pc2.red("error:")} ${err.message}
460
2130
  `);
461
2131
  process.exit(1);
462
2132
  }
@@ -472,15 +2142,15 @@ async function runRotate(args) {
472
2142
  try {
473
2143
  const result = await rotate(opts);
474
2144
  process.stdout.write(
475
- `${pc.green("\u2714")} Rotated ${pc.bold(String(result.rotated.length))} collection(s) in ${pc.cyan(vault)}:
2145
+ `${pc2.green("\u2714")} Rotated ${pc2.bold(String(result.rotated.length))} collection(s) in ${pc2.cyan(vault)}:
476
2146
  `
477
2147
  );
478
2148
  for (const name of result.rotated) {
479
- process.stdout.write(` ${pc.dim("\u2192")} ${name}
2149
+ process.stdout.write(` ${pc2.dim("\u2192")} ${name}
480
2150
  `);
481
2151
  }
482
2152
  } catch (err) {
483
- process.stderr.write(`${pc.red("error:")} ${err.message}
2153
+ process.stderr.write(`${pc2.red("error:")} ${err.message}
484
2154
  `);
485
2155
  process.exit(1);
486
2156
  }
@@ -489,7 +2159,7 @@ async function runBackup(args) {
489
2159
  const target = args[0];
490
2160
  if (!target) {
491
2161
  process.stderr.write(
492
- `${pc.red("error:")} \`noy-db backup\` requires a target path
2162
+ `${pc2.red("error:")} \`noy-db backup\` requires a target path
493
2163
 
494
2164
  Example: noy-db backup ./backups/demo.json --dir ./data --vault demo-co --user owner-alice
495
2165
  `
@@ -503,11 +2173,11 @@ Example: noy-db backup ./backups/demo.json --dir ./data --vault demo-co --user o
503
2173
  try {
504
2174
  const result = await backup({ dir, vault, user, target });
505
2175
  process.stdout.write(
506
- `${pc.green("\u2714")} Wrote backup: ${pc.cyan(result.path)} ${pc.dim(`(${result.bytes} bytes)`)}
2176
+ `${pc2.green("\u2714")} Wrote backup: ${pc2.cyan(result.path)} ${pc2.dim(`(${result.bytes} bytes)`)}
507
2177
  `
508
2178
  );
509
2179
  } catch (err) {
510
- process.stderr.write(`${pc.red("error:")} ${err.message}
2180
+ process.stderr.write(`${pc2.red("error:")} ${err.message}
511
2181
  `);
512
2182
  process.exit(1);
513
2183
  }
@@ -531,10 +2201,8 @@ function parseFlags(args) {
531
2201
  function requireFlag(flags, name) {
532
2202
  const value = flags[name];
533
2203
  if (!value) {
534
- process.stderr.write(
535
- `${pc.red("error:")} missing required flag --${name}
536
- `
537
- );
2204
+ process.stderr.write(`${pc2.red("error:")} missing required flag --${name}
2205
+ `);
538
2206
  process.exit(2);
539
2207
  }
540
2208
  return value;
@@ -545,17 +2213,18 @@ function parsePermissions(input) {
545
2213
  for (const pair of input.split(",").map((s) => s.trim()).filter(Boolean)) {
546
2214
  const [name, mode] = pair.split(":");
547
2215
  if (!name || !mode || mode !== "rw" && mode !== "ro") {
548
- throw new Error(
549
- `Invalid --collections entry "${pair}" \u2014 expected "name:rw" or "name:ro"`
550
- );
2216
+ throw new Error(`Invalid --collections entry "${pair}" \u2014 expected "name:rw" or "name:ro"`);
551
2217
  }
552
2218
  out[name] = mode;
553
2219
  }
554
2220
  return Object.keys(out).length > 0 ? out : null;
555
2221
  }
556
2222
  main().catch((err) => {
557
- process.stderr.write(`${pc.red("fatal:")} ${err.message}
2223
+ process.stderr.write(`${pc2.red("fatal:")} ${err.message}
558
2224
  `);
559
2225
  process.exit(1);
560
2226
  });
2227
+ export {
2228
+ runTuiInspect
2229
+ };
561
2230
  //# sourceMappingURL=noy-db.js.map