create-noy-db 0.3.0-pre.2 → 0.3.0-pre.4

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.
@@ -143,6 +143,7 @@ function fail(start, message) {
143
143
  }
144
144
 
145
145
  // src/commands/rotate.ts
146
+ import { withTeam } from "@noy-db/hub/team";
146
147
  import { createNoydb as createNoydb2 } from "@noy-db/hub";
147
148
  import { jsonFile } from "@noy-db/to-file";
148
149
 
@@ -188,7 +189,9 @@ async function rotate(options) {
188
189
  db = await createDb({
189
190
  store: buildAdapter(options.dir),
190
191
  user: options.user,
191
- secret
192
+ secret,
193
+ // Key rotation is the opt-in team capability (#267).
194
+ teamStrategy: withTeam()
192
195
  });
193
196
  const vault = await db.openVault(options.vault);
194
197
  const targets = options.collections && options.collections.length > 0 ? options.collections : await vault.collections();
@@ -205,6 +208,7 @@ async function rotate(options) {
205
208
  }
206
209
 
207
210
  // src/commands/add-user.ts
211
+ import { withTeam as withTeam2 } from "@noy-db/hub/team";
208
212
  import { createNoydb as createNoydb3 } from "@noy-db/hub";
209
213
  import { jsonFile as jsonFile2 } from "@noy-db/to-file";
210
214
  async function addUser(options) {
@@ -233,7 +237,10 @@ async function addUser(options) {
233
237
  db = await createDb({
234
238
  store: buildAdapter(options.dir),
235
239
  user: options.callerUser,
236
- secret: callerSecret
240
+ secret: callerSecret,
241
+ // Multi-user grant is the opt-in team capability (#267) — the CLI
242
+ // add-user command is by definition a multi-user operation.
243
+ teamStrategy: withTeam2()
237
244
  });
238
245
  const grantOpts = {
239
246
  userId: options.newUserId,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/bin/noy-db.ts","../../src/commands/add.ts","../../src/commands/verify.ts","../../src/commands/rotate.ts","../../src/commands/shared.ts","../../src/commands/add-user.ts","../../src/commands/backup.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * The `noy-db` bin. Invoked from inside an existing project by\n * `pnpm exec noy-db <command>`, `npx noy-db <command>`, etc.\n *\n * Subcommands:\n *\n * add <collection> Scaffold store + page files\n * add user <id> <role> [--collections…] Grant a new user access\n * verify In-memory integrity check\n * rotate --vault … --user … Rotate DEKs for a vault\n * backup <target> --vault … … Dump a vault to a file\n * help Show usage\n *\n * Future subcommands (deferred to a follow-up):\n * seed Re-run the project's seeder script (needs\n * a design decision on how seed scripts auth)\n * backup s3://... S3 targets (would bundle @aws-sdk; lives in\n * an optional companion package instead)\n * restore <file> Load a dumped backup into a vault\n *\n * ## Dispatcher design\n *\n * The dispatcher is intentionally dead simple: read argv[2], pick\n * a handler, pass the remaining args. No flag DSL, no auto-help\n * generation. Each subcommand's argv parser lives next to the\n * subcommand itself so the wiring is obvious and the pure\n * subcommand functions stay testable in isolation.\n *\n * ## Passphrase handling invariants\n *\n * Every subcommand that unlocks a vault goes through the\n * shared `defaultReadPassphrase` helper (see `commands/shared.ts`).\n * That helper:\n *\n * - Uses @clack/prompts `password()` so nothing echoes to stdout\n * - Never logs the returned value\n * - Validates \"not empty\" up front; the real strength check\n * happens inside the core's KEK derivation\n * - Aborts the process on Ctrl-C before any I/O happens\n *\n * The passphrase never leaves the closure in which it was read —\n * every subcommand closes the Noydb instance in a `finally` block\n * so the KEK is cleared from memory on the way out.\n */\n\nimport pc from 'picocolors'\nimport { addCollection } from '../commands/add.js'\nimport { verifyIntegrity } from '../commands/verify.js'\nimport { rotate, type RotateOptions } from '../commands/rotate.js'\nimport { addUser, type AddUserOptions } from '../commands/add-user.js'\nimport { backup } from '../commands/backup.js'\nimport { assertRole, parseCollectionList } from '../commands/shared.js'\n\nconst HELP = `Usage: noy-db <command> [args]\n\nCommands:\n add <collection> Scaffold a new collection store + page\n add user <userId> <role> [options] Grant a new user access to a vault\n verify In-memory crypto integrity check\n rotate [options] Rotate DEKs for a vault\n backup <target> [options] Dump a vault to a file\n help Show this message\n\nCommon options for rotate / add user / backup:\n --dir <path> Data directory (file adapter). Default: ./data\n --vault <name> Vault (tenant) name. Required.\n --user <id> Your user id in the vault. Required.\n --collections <list> Comma-separated collection list. Format:\n rotate: invoices,clients\n add user: invoices:rw,clients:ro (operator/client only)\n\nExamples:\n noy-db add invoices\n noy-db add user accountant-ann operator --dir ./data --vault demo-co --user owner-alice --collections invoices:rw,clients:ro\n noy-db verify\n noy-db rotate --dir ./data --vault demo-co --user owner-alice\n noy-db backup ./backups/demo-co-2026-04-07.json --dir ./data --vault demo-co --user owner-alice\n\nRun from the root of a project that already has a noy-db file\nadapter directory in place. For new projects, use\n\\`npm create @noy-db\\` instead.\n`\n\nasync function main(): Promise<void> {\n const [, , command, ...rest] = process.argv\n\n switch (command) {\n case undefined:\n case 'help':\n case '-h':\n case '--help':\n process.stdout.write(HELP)\n return\n\n case 'add':\n await runAdd(rest)\n return\n\n case 'verify':\n await runVerify()\n return\n\n case 'rotate':\n await runRotate(rest)\n return\n\n case 'backup':\n await runBackup(rest)\n return\n\n default:\n process.stderr.write(`${pc.red('error:')} unknown command '${command}'\\n\\n${HELP}`)\n process.exit(2)\n }\n}\n\n// ─── `add` dispatcher — branches between `add <collection>` and `add user …` ─\n\nasync function runAdd(args: string[]): Promise<void> {\n const first = args[0]\n if (first === 'user') {\n await runAddUser(args.slice(1))\n return\n }\n // Legacy path: `noy-db add <collection>` scaffolds store + page files.\n if (!first) {\n process.stderr.write(\n `${pc.red('error:')} \\`noy-db add\\` requires either a collection name or the \\`user\\` subcommand\\n\\n` +\n `Examples:\\n noy-db add invoices\\n noy-db add user alice operator ...\\n`,\n )\n process.exit(2)\n }\n try {\n const result = await addCollection({ name: first })\n process.stdout.write(`${pc.green('✔')} Created:\\n`)\n for (const file of result.files) {\n process.stdout.write(` ${pc.dim('→')} ${file}\\n`)\n }\n process.stdout.write(\n `\\nNext: visit ${pc.cyan(`/${first}`)} in your dev server, then edit ${pc.bold(`app/stores/${first}.ts`)} to match your domain.\\n`,\n )\n } catch (err) {\n process.stderr.write(`${pc.red('error:')} ${(err as Error).message}\\n`)\n process.exit(1)\n }\n}\n\n// ─── `verify` ──────────────────────────────────────────────────────────\n\nasync function runVerify(): Promise<void> {\n process.stdout.write(`${pc.dim('▸ Running noy-db integrity check…')}\\n`)\n const result = await verifyIntegrity()\n if (result.ok) {\n process.stdout.write(`${pc.green('✔')} ${result.message} ${pc.dim(`(${result.durationMs}ms)`)}\\n`)\n return\n }\n process.stderr.write(`${pc.red('✘')} ${result.message} ${pc.dim(`(${result.durationMs}ms)`)}\\n`)\n process.exit(1)\n}\n\n// ─── `add user` ────────────────────────────────────────────────────────\n\nasync function runAddUser(args: string[]): Promise<void> {\n // Positional: userId, role. Then flag bag.\n const userId = args[0]\n const roleInput = args[1]\n if (!userId || !roleInput) {\n process.stderr.write(\n `${pc.red('error:')} \\`noy-db add user\\` requires <userId> <role>\\n\\n` +\n `Example: noy-db add user ann operator --dir ./data --vault demo-co --user owner-alice --collections invoices:rw\\n`,\n )\n process.exit(2)\n }\n\n let role\n try {\n role = assertRole(roleInput)\n } catch (err) {\n process.stderr.write(`${pc.red('error:')} ${(err as Error).message}\\n`)\n process.exit(2)\n return\n }\n\n const flags = parseFlags(args.slice(2))\n const dir = flags.dir ?? './data'\n const vault = requireFlag(flags, 'vault')\n const callerUser = requireFlag(flags, 'user')\n\n const permissions = parsePermissions(flags['collections'])\n\n const opts: AddUserOptions = {\n dir,\n vault,\n callerUser,\n newUserId: userId,\n role,\n }\n if (flags['display-name']) opts.newUserDisplayName = flags['display-name']\n if (permissions) opts.permissions = permissions\n\n try {\n const result = await addUser(opts)\n process.stdout.write(\n `${pc.green('✔')} Granted ${pc.bold(result.role)} access to ${pc.cyan(result.userId)} in vault ${pc.cyan(vault)}.\\n`,\n )\n } catch (err) {\n process.stderr.write(`${pc.red('error:')} ${(err as Error).message}\\n`)\n process.exit(1)\n }\n}\n\n// ─── `rotate` ──────────────────────────────────────────────────────────\n\nasync function runRotate(args: string[]): Promise<void> {\n const flags = parseFlags(args)\n const dir = flags.dir ?? './data'\n const vault = requireFlag(flags, 'vault')\n const user = requireFlag(flags, 'user')\n\n const opts: RotateOptions = { dir, vault, user }\n const collections = parseCollectionList(flags['collections'])\n if (collections) opts.collections = collections\n\n try {\n const result = await rotate(opts)\n process.stdout.write(\n `${pc.green('✔')} Rotated ${pc.bold(String(result.rotated.length))} collection(s) in ${pc.cyan(vault)}:\\n`,\n )\n for (const name of result.rotated) {\n process.stdout.write(` ${pc.dim('→')} ${name}\\n`)\n }\n } catch (err) {\n process.stderr.write(`${pc.red('error:')} ${(err as Error).message}\\n`)\n process.exit(1)\n }\n}\n\n// ─── `backup` ──────────────────────────────────────────────────────────\n\nasync function runBackup(args: string[]): Promise<void> {\n const target = args[0]\n if (!target) {\n process.stderr.write(\n `${pc.red('error:')} \\`noy-db backup\\` requires a target path\\n\\n` +\n `Example: noy-db backup ./backups/demo.json --dir ./data --vault demo-co --user owner-alice\\n`,\n )\n process.exit(2)\n }\n\n const flags = parseFlags(args.slice(1))\n const dir = flags.dir ?? './data'\n const vault = requireFlag(flags, 'vault')\n const user = requireFlag(flags, 'user')\n\n try {\n const result = await backup({ dir, vault, user, target })\n process.stdout.write(\n `${pc.green('✔')} Wrote backup: ${pc.cyan(result.path)} ${pc.dim(`(${result.bytes} bytes)`)}\\n`,\n )\n } catch (err) {\n process.stderr.write(`${pc.red('error:')} ${(err as Error).message}\\n`)\n process.exit(1)\n }\n}\n\n// ─── Shared argv helpers ───────────────────────────────────────────────\n\n/**\n * Parse a sequence of `--key value` flag pairs into a record.\n * Boolean flags (`--flag` with no value) become `\"true\"`. Unknown\n * shapes (positional args after the known set) are ignored — the\n * caller has already peeled positionals before passing in.\n */\nfunction parseFlags(args: string[]): Record<string, string> {\n const out: Record<string, string> = {}\n for (let i = 0; i < args.length; i++) {\n const arg = args[i]\n if (!arg || !arg.startsWith('--')) continue\n const key = arg.slice(2)\n const next = args[i + 1]\n if (next && !next.startsWith('--')) {\n out[key] = next\n i++\n } else {\n out[key] = 'true'\n }\n }\n return out\n}\n\nfunction requireFlag(flags: Record<string, string>, name: string): string {\n const value = flags[name]\n if (!value) {\n process.stderr.write(\n `${pc.red('error:')} missing required flag --${name}\\n`,\n )\n process.exit(2)\n }\n return value\n}\n\n/**\n * Parse `--collections invoices:rw,clients:ro` into a\n * `{ invoices: 'rw', clients: 'ro' }` record. Returns `null` when\n * the input is empty or undefined.\n */\nfunction parsePermissions(\n input: string | undefined,\n): Record<string, 'rw' | 'ro'> | null {\n if (!input) return null\n const out: Record<string, 'rw' | 'ro'> = {}\n for (const pair of input.split(',').map((s) => s.trim()).filter(Boolean)) {\n const [name, mode] = pair.split(':')\n if (!name || !mode || (mode !== 'rw' && mode !== 'ro')) {\n throw new Error(\n `Invalid --collections entry \"${pair}\" — expected \"name:rw\" or \"name:ro\"`,\n )\n }\n out[name] = mode\n }\n return Object.keys(out).length > 0 ? out : null\n}\n\nmain().catch((err: unknown) => {\n process.stderr.write(`${pc.red('fatal:')} ${(err as Error).message}\\n`)\n process.exit(1)\n})\n","/**\n * `noy-db add <collection>` — scaffold a new collection inside an existing\n * Nuxt 4 project that already has `@noy-db/in-nuxt` configured.\n *\n * The command writes two files:\n *\n * 1. `app/stores/<collection>.ts` — a `defineNoydbStore<T>()` call with\n * a placeholder `T` interface and one example field. The user fills\n * in the real shape after the file is created.\n *\n * 2. `app/pages/<collection>.vue` — a minimal CRUD page that lists,\n * adds, and deletes records. The store ID and collection name are\n * derived from the argument; everything else is boilerplate.\n *\n * The command refuses to overwrite existing files. If either target\n * already exists it logs which one and exits non-zero — the user has to\n * delete or move the file first. There's no `--force` because forcing an\n * overwrite of generated UI code is almost always a footgun in disguise.\n */\n\nimport { promises as fs } from 'node:fs'\nimport path from 'node:path'\n\nexport interface AddCollectionOptions {\n /** The collection name. Must be a lowercase identifier. */\n name: string\n /** Project root. Defaults to `process.cwd()`. */\n cwd?: string\n /** Compartment id to embed in the generated store. Defaults to `default`. */\n vault?: string\n}\n\n/**\n * Result returned to callers (the bin entry uses this to format output;\n * tests assert on the file paths).\n */\nexport interface AddCollectionResult {\n /** Files written, in the order they were created. */\n files: string[]\n}\n\n/**\n * Lowercase identifier check, narrower than the project-name check —\n * collection names become Vue component names, Pinia store IDs, AND TS\n * symbols, so we keep them simple: letters, digits, hyphens, must start\n * with a letter.\n */\nexport function validateCollectionName(name: string): string | null {\n if (!name) return 'Collection name is required'\n if (!/^[a-z][a-z0-9-]*$/.test(name)) {\n return 'Collection name must start with a lowercase letter and contain only lowercase letters, digits, or hyphens'\n }\n return null\n}\n\nexport async function addCollection(\n options: AddCollectionOptions,\n): Promise<AddCollectionResult> {\n const err = validateCollectionName(options.name)\n if (err) throw new Error(err)\n\n const cwd = options.cwd ?? process.cwd()\n const vault = options.vault ?? 'default'\n const name = options.name\n\n // Convert kebab-case to PascalCase for the TS interface name and the\n // Vue helper variable.\n const PascalName = name\n .split('-')\n .map((s) => (s.length === 0 ? s : (s[0]?.toUpperCase() ?? '') + s.slice(1)))\n .join('')\n const useFnName = `use${PascalName}`\n\n const storePath = path.join(cwd, 'app', 'stores', `${name}.ts`)\n const pagePath = path.join(cwd, 'app', 'pages', `${name}.vue`)\n\n // Refuse to overwrite. Doing both checks before writing either file\n // means a partial write is impossible — either both files land or\n // neither does.\n for (const target of [storePath, pagePath]) {\n if (await pathExists(target)) {\n throw new Error(`Refusing to overwrite existing file: ${target}`)\n }\n }\n\n await fs.mkdir(path.dirname(storePath), { recursive: true })\n await fs.mkdir(path.dirname(pagePath), { recursive: true })\n\n await fs.writeFile(storePath, renderStore(name, PascalName, useFnName, vault), 'utf8')\n await fs.writeFile(pagePath, renderPage(name, PascalName, useFnName), 'utf8')\n\n return { files: [storePath, pagePath] }\n}\n\nasync function pathExists(p: string): Promise<boolean> {\n try {\n await fs.access(p)\n return true\n } catch {\n return false\n }\n}\n\n/**\n * Renders the `app/stores/<name>.ts` file. The interface is intentionally\n * minimal — `id` and `name` are the only fields. The user is expected to\n * extend it after generation; we'd rather not lock them into a domain\n * shape we guessed.\n */\nfunction renderStore(\n name: string,\n PascalName: string,\n useFnName: string,\n vault: string,\n): string {\n return `// Generated by \\`noy-db add ${name}\\`.\n//\n// Edit the ${PascalName} interface to match your domain, then call\n// \\`${useFnName}()\\` from any component.\n//\n// defineNoydbStore is auto-imported by @noy-db/in-nuxt. The vault\n// id is the tenant/company namespace — change it if you have multiple.\n\nexport interface ${PascalName} {\n id: string\n name: string\n}\n\nexport const ${useFnName} = defineNoydbStore<${PascalName}>('${name}', {\n vault: '${vault}',\n})\n`\n}\n\n/**\n * Renders the `app/pages/<name>.vue` page — list + add + delete. Picks\n * the simplest possible template-renderer pattern (`v-for`, no fancy\n * components) so the generated file is easy to read and modify.\n */\nfunction renderPage(name: string, PascalName: string, useFnName: string): string {\n return `<!--\n Generated by \\`noy-db add ${name}\\`.\n Visit /${name} in your dev server.\n-->\n<script setup lang=\"ts\">\nconst ${name} = ${useFnName}()\nawait ${name}.$ready\n\nfunction addOne() {\n ${name}.add({\n id: crypto.randomUUID(),\n name: 'New ${PascalName}',\n })\n}\n\nfunction removeOne(id: string) {\n ${name}.remove(id)\n}\n</script>\n\n<template>\n <main>\n <h1>${PascalName}</h1>\n <button @click=\"addOne\">Add ${PascalName}</button>\n <ul>\n <li v-for=\"item in ${name}.items\" :key=\"item.id\">\n {{ item.name }}\n <button @click=\"removeOne(item.id)\">Delete</button>\n </li>\n </ul>\n </main>\n</template>\n`\n}\n","/**\n * `noy-db verify` — end-to-end integrity check.\n *\n * Opens an in-memory NOYDB instance, writes a record, reads it back,\n * decrypts it, and asserts the round-trip is byte-identical. The check\n * exercises the full crypto path (PBKDF2 → KEK → DEK → AES-GCM) without\n * touching any user data on disk.\n *\n * Why an in-memory check is the right scope:\n * - It validates that @noy-db/core, @noy-db/memory, and the user's\n * installed Node version all agree on Web Crypto. That's the most\n * common silent failure for first-time installers.\n * - It cannot accidentally corrupt user data because there isn't any.\n * - It runs in well under one second, so users actually run it.\n *\n * What this command does NOT do (intentionally):\n * - Open the user's actual vault file/dynamo/s3/browser store.\n * That requires the user's passphrase — not something we want a CLI\n * `verify` command to prompt for. The full passphrase-driven verify\n * belongs in `nuxi noydb verify` once the auth story for CLIs lands\n * in. For now `noy-db verify` is the dependency-graph smoke test.\n */\n\nimport { createNoydb } from '@noy-db/hub'\nimport { memory } from '@noy-db/to-memory'\n\nexport interface VerifyResult {\n /** `true` if the round-trip succeeded; `false` if anything diverged. */\n ok: boolean\n /** Human-readable status. Always set, even on success. */\n message: string\n /** Wall-clock time the integrity check took, in ms. */\n durationMs: number\n}\n\n/**\n * Runs the end-to-end check. Pure function — no console output, no\n * `process.exit`. The bin wrapper handles formatting and exit codes so\n * the function is trivial to call from tests.\n */\nexport async function verifyIntegrity(): Promise<VerifyResult> {\n const start = performance.now()\n try {\n const db = await createNoydb({\n store: memory(),\n user: 'noy-db-verify',\n // The passphrase here is throwaway — the in-memory adapter never\n // persists anything, and the KEK is destroyed when we call close()\n // a few lines down. We use a non-trivial value just to exercise\n // PBKDF2 properly.\n secret: 'noy-db-verify-passphrase-2026',\n })\n const company = await db.openVault('verify-co')\n const collection = company.collection<{ id: string; n: number }>('verify')\n\n // Round-trip a single record. We pick a value that's small enough\n // to print on failure but large enough to ensure encryption isn't\n // accidentally a no-op.\n const original = { id: 'verify-1', n: 42 }\n await collection.put('verify-1', original)\n const got = await collection.get('verify-1')\n if (!got || got.id !== original.id || got.n !== original.n) {\n return fail(start, `Round-trip mismatch: got ${JSON.stringify(got)}`)\n }\n\n // Make sure the query DSL works too — this catches the case where\n // the user's @noy-db/core install is at (no query DSL) but the\n // CLI was updated to.\n const found = collection.query().where('n', '==', 42).toArray()\n if (found.length !== 1) {\n return fail(start, `Query DSL mismatch: expected 1 result, got ${found.length}`)\n }\n\n db.close()\n\n return {\n ok: true,\n message: 'noy-db integrity check passed',\n durationMs: Math.round(performance.now() - start),\n }\n } catch (err) {\n return fail(start, `Integrity check threw: ${(err as Error).message}`)\n }\n}\n\nfunction fail(start: number, message: string): VerifyResult {\n return {\n ok: false,\n message,\n durationMs: Math.round(performance.now() - start),\n }\n}\n","/**\n * `noy-db rotate` — rotate the DEKs for one or more collections in\n * a vault.\n *\n * What it does\n * ------------\n * For each target collection:\n *\n * 1. Generate a fresh DEK\n * 2. Decrypt every record with the old DEK\n * 3. Re-encrypt every record with the new DEK\n * 4. Re-wrap the new DEK into every remaining user's keyring\n *\n * The old DEKs become unreachable as soon as the keyring files are\n * updated. This is the \"just rotate\" path — nobody is revoked,\n * everybody keeps their current permissions, but the key material\n * is replaced.\n *\n * Why expose this as a CLI command\n * --------------------------------\n * Two real-world scenarios:\n *\n * 1. **Suspected key leak.** An operator lost a laptop, a\n * developer accidentally pasted a passphrase into a Slack\n * channel, a USB stick went missing. Even if you think the\n * passphrase is safe, rotating is cheap insurance.\n *\n * 2. **Scheduled rotation.** Some compliance regimes require\n * periodic key rotation regardless of exposure. A CLI makes\n * this scriptable from cron or a CI job.\n *\n * This module is test-first: all inputs are plain options, the\n * passphrase reader is injected, and the Noydb factory is\n * injectable. The production bin is a thin wrapper that defaults\n * those injections to their real implementations.\n */\n\nimport { createNoydb, type Noydb, type NoydbStore } from '@noy-db/hub'\nimport { jsonFile } from '@noy-db/to-file'\nimport type { ReadPassphrase } from './shared.js'\nimport { defaultReadPassphrase } from './shared.js'\n\nexport interface RotateOptions {\n /** Directory containing the vault data (file adapter only). */\n dir: string\n /** Vault (tenant) name to rotate keys in. */\n vault: string\n /** The user id of the operator running the rotate. */\n user: string\n /**\n * Explicit list of collections to rotate. When undefined, the\n * rotation targets every collection the user has a DEK for —\n * resolved at run time by reading the vault snapshot.\n */\n collections?: string[]\n /** Injected passphrase reader. Defaults to the clack implementation. */\n readPassphrase?: ReadPassphrase\n /**\n * Injected Noydb factory. Production code leaves this undefined\n * and gets `createNoydb`; tests pass a constructor that builds\n * against an in-memory adapter.\n */\n createDb?: typeof createNoydb\n /**\n * Injected adapter factory. Production code leaves this undefined\n * and gets `jsonFile`; tests pass one that returns the shared\n * in-memory adapter their fixture used.\n */\n buildAdapter?: (dir: string) => NoydbStore\n}\n\nexport interface RotateResult {\n /** The collections that were actually rotated. */\n rotated: string[]\n}\n\n/**\n * Run the rotate flow against a file-adapter vault. Returns\n * the list of collections that were rotated so callers can display\n * it to the user.\n *\n * Throws `Error` on any auth/adapter/rotate failure. The bin\n * catches these and prints a friendly message; direct callers\n * (tests) can inspect the error message to assert specific\n * failure modes.\n */\nexport async function rotate(options: RotateOptions): Promise<RotateResult> {\n const readPassphrase = options.readPassphrase ?? defaultReadPassphrase\n const buildAdapter = options.buildAdapter ?? ((dir) => jsonFile({ dir }))\n const createDb = options.createDb ?? createNoydb\n\n // Read the passphrase BEFORE opening the database. This way a\n // cancelled prompt (Ctrl-C at the password entry) leaves the\n // adapter completely untouched — no files opened, no locks held.\n const secret = await readPassphrase(`Passphrase for ${options.user}`)\n\n let db: Noydb | null = null\n try {\n db = await createDb({\n store: buildAdapter(options.dir),\n user: options.user,\n secret,\n })\n\n // Resolve \"all collections\" by asking the vault. This\n // happens BEFORE rotate() is called, so the list is stable\n // across the operation — adding a new collection mid-rotate\n // would be a race we're not guarding against (single-writer\n // assumption applies).\n const vault = await db.openVault(options.vault)\n const targets = options.collections && options.collections.length > 0\n ? options.collections\n : await vault.collections()\n\n if (targets.length === 0) {\n throw new Error(\n `Vault \"${options.vault}\" has no collections to rotate.`,\n )\n }\n\n await db.rotate(options.vault, targets)\n return { rotated: targets }\n } finally {\n // Always close the DB on exit — success or failure. Close()\n // clears the KEK and DEKs from process memory, which is the\n // final line of defense if the passphrase somehow leaked\n // into a log line above this block.\n db?.close()\n }\n}\n","/**\n * Shared primitives for the interactive `noy-db` subcommands that\n * need to unlock a real vault.\n *\n * Three things live here:\n *\n * 1. `ReadPassphrase` — a tiny interface for \"prompt the user for\n * a passphrase\", with a test-friendly default. Subcommands take\n * this as an injected dependency so tests can short-circuit\n * the prompt without spawning a pty.\n *\n * 2. `defaultReadPassphrase` — the production implementation,\n * built on `@clack/prompts` `password()`. Never echoes the\n * value to the terminal, never logs it, clears it from the\n * returned promise after the caller consumes it.\n *\n * 3. `assertRole` — narrow unknown string input to the Role type\n * with a consistent error message.\n *\n * ## Why pull this out\n *\n * `rotate`, `addUser`, and `backup` all need the same \"prompt for\n * a passphrase\" shape and the same \"open a file adapter and get\n * back a Noydb instance\" shape. Duplicating it in three files would\n * drift over time; centralizing means one place to audit the\n * passphrase-handling contract (never log, never persist, clear\n * local variables after use).\n */\n\nimport { password, isCancel, cancel } from '@clack/prompts'\nimport type { Role } from '@noy-db/hub'\n\nconst VALID_ROLES = ['owner', 'admin', 'operator', 'viewer', 'client'] as const\n\n/**\n * Asynchronous passphrase reader. Production code passes\n * `defaultReadPassphrase`; tests pass a stub that returns a fixed\n * string without touching stdin.\n *\n * The `label` is shown to the user as the prompt message. It\n * should never contain the expected passphrase or any secret.\n */\nexport type ReadPassphrase = (label: string) => Promise<string>\n\n/**\n * Clack-based passphrase prompt. Cancellation (Ctrl-C) aborts the\n * process with exit code 1 — prompts are always the first thing to\n * fire in a subcommand, so aborting here doesn't leave the system\n * in a half-mutated state.\n */\nexport const defaultReadPassphrase: ReadPassphrase = async (label) => {\n const value = await password({\n message: label,\n // Basic sanity: reject empty strings up front. We don't enforce\n // length here because the caller's KEK-derivation step will\n // reject weak passphrases with its own, richer error.\n validate: (v) => (v.length === 0 ? 'Passphrase cannot be empty' : undefined),\n })\n if (isCancel(value)) {\n cancel('Cancelled.')\n process.exit(1)\n }\n return value\n}\n\n/**\n * Narrow an unknown string to the `Role` type from @noy-db/core.\n * Used by the `add user` subcommand to validate the role argument\n * before passing it to `noydb.grant()`.\n */\nexport function assertRole(input: string): Role {\n if (!(VALID_ROLES as readonly string[]).includes(input)) {\n throw new Error(\n `Invalid role \"${input}\" — must be one of: ${VALID_ROLES.join(', ')}`,\n )\n }\n return input as Role\n}\n\n/**\n * Split a comma-separated collection list into an array of names,\n * trimming whitespace and dropping empties. Returns null if the\n * input itself is empty or undefined — the caller decides whether\n * that means \"all collections\" or \"error\".\n */\nexport function parseCollectionList(input: string | undefined): string[] | null {\n if (!input) return null\n const parts = input\n .split(',')\n .map((s) => s.trim())\n .filter((s) => s.length > 0)\n return parts.length > 0 ? parts : null\n}\n","/**\n * `noy-db add user <id> <role>` — grant a new user access to a\n * vault.\n *\n * What it does\n * ------------\n * Wraps `noydb.grant()` in the CLI's auth-prompt ritual:\n *\n * 1. Prompt the caller for their own passphrase (to unlock the\n * caller's keyring and derive the wrapping key).\n * 2. Prompt for the new user's passphrase.\n * 3. Prompt for confirmation of the new passphrase.\n * 4. Reject on mismatch.\n * 5. Call `noydb.grant(vault, { userId, role, passphrase, permissions })`.\n *\n * For owner/admin/viewer roles, every collection is granted\n * automatically (the core keyring.ts grant logic handles that via\n * the `permissions` field). For operator/client, the caller must\n * pass a `--collections` list because those roles need explicit\n * per-collection permissions.\n *\n * ## What this does NOT do\n *\n * - No email/invite flow — is about local-CLI key management,\n * not out-of-band user enrollment.\n * - No rollback on partial failure — `grant()` is atomic at the\n * core level (keyring file writes last, after DEK wrapping), so\n * partial-state-on-crash is already handled.\n */\n\nimport { createNoydb, type Noydb, type NoydbStore, type Role } from '@noy-db/hub'\nimport { jsonFile } from '@noy-db/to-file'\nimport type { ReadPassphrase } from './shared.js'\nimport { defaultReadPassphrase } from './shared.js'\n\nexport interface AddUserOptions {\n /** Directory containing the vault data (file adapter only). */\n dir: string\n /** Vault (tenant) name to grant access to. */\n vault: string\n /** The user id of the caller running the grant. */\n callerUser: string\n /** The new user's id (must not already exist in the vault keyring). */\n newUserId: string\n /** The new user's display name — shown in UI and audit logs. Defaults to `newUserId`. */\n newUserDisplayName?: string\n /** The new user's role. */\n role: Role\n /**\n * Per-collection permissions. Required when `role` is operator or\n * client; ignored for owner/admin/viewer (they get everything\n * via the core's resolvePermissions logic).\n *\n * Shape: `{ invoices: 'rw', clients: 'ro' }`. CLI callers pass\n * `--collections invoices:rw,clients:ro` and the argv parser\n * converts it to this shape.\n */\n permissions?: Record<string, 'rw' | 'ro'>\n /** Injected passphrase reader. Defaults to the clack implementation. */\n readPassphrase?: ReadPassphrase\n /** Injected Noydb factory. */\n createDb?: typeof createNoydb\n /** Injected adapter factory. */\n buildAdapter?: (dir: string) => NoydbStore\n}\n\nexport interface AddUserResult {\n /** The userId that was granted access. */\n userId: string\n /** The role they were granted. */\n role: Role\n}\n\n/**\n * Run the grant flow. Two passphrase prompts: caller's, then new\n * user's (twice for confirmation). Calls `noydb.grant()` with the\n * collected values.\n */\nexport async function addUser(options: AddUserOptions): Promise<AddUserResult> {\n const readPassphrase = options.readPassphrase ?? defaultReadPassphrase\n const buildAdapter = options.buildAdapter ?? ((dir) => jsonFile({ dir }))\n const createDb = options.createDb ?? createNoydb\n\n // Operator/client roles NEED explicit permissions. Reject here\n // rather than in the middle of the grant, so the caller sees the\n // problem before any I/O happens.\n if (\n (options.role === 'operator' || options.role === 'client') &&\n (!options.permissions || Object.keys(options.permissions).length === 0)\n ) {\n throw new Error(\n `Role \"${options.role}\" requires explicit --collections — e.g. --collections invoices:rw,clients:ro`,\n )\n }\n\n const callerSecret = await readPassphrase(\n `Your passphrase (${options.callerUser})`,\n )\n const newSecret = await readPassphrase(\n `New passphrase for ${options.newUserId}`,\n )\n const confirmSecret = await readPassphrase(\n `Confirm passphrase for ${options.newUserId}`,\n )\n\n if (newSecret !== confirmSecret) {\n throw new Error(`Passphrases do not match — grant aborted.`)\n }\n\n let db: Noydb | null = null\n try {\n db = await createDb({\n store: buildAdapter(options.dir),\n user: options.callerUser,\n secret: callerSecret,\n })\n\n // Build the grant options. Only include `permissions` when the\n // caller actually supplied them — otherwise the core's\n // resolvePermissions fills in the role defaults. The spread\n // (rather than post-assignment) keeps the object literal\n // compatible with `GrantOptions`'s readonly `permissions`.\n const grantOpts: Parameters<Noydb['grant']>[1] = {\n userId: options.newUserId,\n displayName: options.newUserDisplayName ?? options.newUserId,\n role: options.role,\n passphrase: newSecret,\n ...(options.permissions ? { permissions: options.permissions } : {}),\n }\n\n await db.grant(options.vault, grantOpts)\n\n return {\n userId: options.newUserId,\n role: options.role,\n }\n } finally {\n db?.close()\n }\n}\n","/**\n * `noy-db backup <target>` — dump a vault to a local file.\n *\n * What it does\n * ------------\n * Wraps `vault.dump()` in the CLI's auth-prompt ritual, then\n * writes the serialized backup to the requested path. As of,\n * `dump()` already produces a verifiable backup (embedded\n * ledgerHead, full `_ledger` / `_ledger_deltas` snapshots) — the\n * CLI just moves bytes; the integrity guarantees come from core.\n *\n * ## Target URI support\n *\n * ships **`file://` only** (or a plain filesystem path).\n * The issue spec originally called for `s3://` as well, but\n * wiring @aws-sdk into @noy-db/create would defeat the\n * zero-runtime-deps story for the CLI package. S3 backup is\n * deferred to a follow-up that can live in @noy-db/s3-cli or a\n * similar optional companion package.\n *\n * Accepted forms:\n * - `file:///absolute/path.json`\n * - `file://./relative/path.json`\n * - `/absolute/path.json` (treated as `file://`)\n * - `./relative/path.json` (treated as `file://`)\n *\n * ## What this does NOT do\n *\n * - No encryption of the backup BEYOND what noy-db already does.\n * The dumped file is a valid noy-db backup, which means\n * individual records are still encrypted but the keyring is\n * included (wrapped with each user's KEK). Anyone who loads\n * the backup still needs the correct passphrase to read.\n * - No restore — that's a separate subcommand tracked as a\n * follow-up. For now users can restore via\n * `vault.load(backupString)` from their own app code.\n */\n\nimport { promises as fs } from 'node:fs'\nimport path from 'node:path'\nimport { createNoydb, type Noydb, type NoydbStore } from '@noy-db/hub'\nimport { withHistory } from '@noy-db/hub/history'\nimport { jsonFile } from '@noy-db/to-file'\nimport type { ReadPassphrase } from './shared.js'\nimport { defaultReadPassphrase } from './shared.js'\n\nexport interface BackupOptions {\n /** Directory containing the vault data (file adapter only). */\n dir: string\n /** Vault (tenant) name to back up. */\n vault: string\n /** The user id of the operator running the backup. */\n user: string\n /**\n * Where to write the backup. Accepts a `file://` URI or a plain\n * filesystem path. Relative paths resolve against `process.cwd()`.\n */\n target: string\n /** Injected passphrase reader. */\n readPassphrase?: ReadPassphrase\n /** Injected Noydb factory. */\n createDb?: typeof createNoydb\n /** Injected adapter factory. */\n buildAdapter?: (dir: string) => NoydbStore\n}\n\nexport interface BackupResult {\n /** Absolute filesystem path the backup was written to. */\n path: string\n /** Size of the serialized backup in bytes. */\n bytes: number\n}\n\n/**\n * Parse a backup target into an absolute filesystem path. Rejects\n * unsupported URI schemes (s3://, https://, etc.) early so the\n * caller doesn't silently write to the wrong place.\n */\nexport function resolveBackupTarget(target: string, cwd: string = process.cwd()): string {\n // Strip the `file://` prefix if present. The rest of the string\n // is treated as a filesystem path. We accept both `file:///abs`\n // (three slashes, absolute) and `file://./rel` (two slashes,\n // relative) because real-world users write both.\n let raw = target\n if (target.startsWith('file://')) {\n raw = target.slice('file://'.length)\n } else if (target.includes('://')) {\n // Any other scheme is unsupported.\n throw new Error(\n `Unsupported backup target scheme: \"${target.split('://')[0]}://\". ` +\n `Only file:// and plain filesystem paths are supported in. ` +\n `S3 backups will land in a follow-up.`,\n )\n }\n return path.resolve(cwd, raw)\n}\n\nexport async function backup(options: BackupOptions): Promise<BackupResult> {\n const readPassphrase = options.readPassphrase ?? defaultReadPassphrase\n const buildAdapter = options.buildAdapter ?? ((dir) => jsonFile({ dir }))\n const createDb = options.createDb ?? createNoydb\n\n // Resolve the target FIRST so a bad URI fails before any\n // passphrase is collected. This keeps the UX clean: a typo in\n // `s3://bucket/x` rejects without asking for a secret the user\n // would then have to type again.\n const absolutePath = resolveBackupTarget(options.target)\n\n const secret = await readPassphrase(`Passphrase for ${options.user}`)\n\n let db: Noydb | null = null\n try {\n db = await createDb({\n store: buildAdapter(options.dir),\n user: options.user,\n secret,\n // Opt into the history strategy so `vault.dump()` embeds\n // `ledgerHead` for tamper-evident verification at restore.\n // Source vaults that were never written with history simply\n // have an empty ledger; the embedded head is omitted in that\n // case and the backup is treated as legacy on load.\n historyStrategy: withHistory(),\n })\n const vault = await db.openVault(options.vault)\n const serialized = await vault.dump()\n\n // Make sure the parent directory exists. If the user passed\n // `./backups/2026/demo.json` and `./backups/2026` doesn't\n // exist yet, we create it. This is the common case for\n // scripted rotations dropping into a date-based folder.\n await fs.mkdir(path.dirname(absolutePath), { recursive: true })\n await fs.writeFile(absolutePath, serialized, 'utf8')\n\n return {\n path: absolutePath,\n bytes: Buffer.byteLength(serialized, 'utf8'),\n }\n } finally {\n db?.close()\n }\n}\n"],"mappings":"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","names":["createNoydb","createNoydb","createNoydb","jsonFile","jsonFile","createNoydb","fs","path","createNoydb","jsonFile","path","jsonFile","createNoydb","fs"]}
1
+ {"version":3,"sources":["../../src/bin/noy-db.ts","../../src/commands/add.ts","../../src/commands/verify.ts","../../src/commands/rotate.ts","../../src/commands/shared.ts","../../src/commands/add-user.ts","../../src/commands/backup.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * The `noy-db` bin. Invoked from inside an existing project by\n * `pnpm exec noy-db <command>`, `npx noy-db <command>`, etc.\n *\n * Subcommands:\n *\n * add <collection> Scaffold store + page files\n * add user <id> <role> [--collections…] Grant a new user access\n * verify In-memory integrity check\n * rotate --vault … --user … Rotate DEKs for a vault\n * backup <target> --vault … … Dump a vault to a file\n * help Show usage\n *\n * Future subcommands (deferred to a follow-up):\n * seed Re-run the project's seeder script (needs\n * a design decision on how seed scripts auth)\n * backup s3://... S3 targets (would bundle @aws-sdk; lives in\n * an optional companion package instead)\n * restore <file> Load a dumped backup into a vault\n *\n * ## Dispatcher design\n *\n * The dispatcher is intentionally dead simple: read argv[2], pick\n * a handler, pass the remaining args. No flag DSL, no auto-help\n * generation. Each subcommand's argv parser lives next to the\n * subcommand itself so the wiring is obvious and the pure\n * subcommand functions stay testable in isolation.\n *\n * ## Passphrase handling invariants\n *\n * Every subcommand that unlocks a vault goes through the\n * shared `defaultReadPassphrase` helper (see `commands/shared.ts`).\n * That helper:\n *\n * - Uses @clack/prompts `password()` so nothing echoes to stdout\n * - Never logs the returned value\n * - Validates \"not empty\" up front; the real strength check\n * happens inside the core's KEK derivation\n * - Aborts the process on Ctrl-C before any I/O happens\n *\n * The passphrase never leaves the closure in which it was read —\n * every subcommand closes the Noydb instance in a `finally` block\n * so the KEK is cleared from memory on the way out.\n */\n\nimport pc from 'picocolors'\nimport { addCollection } from '../commands/add.js'\nimport { verifyIntegrity } from '../commands/verify.js'\nimport { rotate, type RotateOptions } from '../commands/rotate.js'\nimport { addUser, type AddUserOptions } from '../commands/add-user.js'\nimport { backup } from '../commands/backup.js'\nimport { assertRole, parseCollectionList } from '../commands/shared.js'\n\nconst HELP = `Usage: noy-db <command> [args]\n\nCommands:\n add <collection> Scaffold a new collection store + page\n add user <userId> <role> [options] Grant a new user access to a vault\n verify In-memory crypto integrity check\n rotate [options] Rotate DEKs for a vault\n backup <target> [options] Dump a vault to a file\n help Show this message\n\nCommon options for rotate / add user / backup:\n --dir <path> Data directory (file adapter). Default: ./data\n --vault <name> Vault (tenant) name. Required.\n --user <id> Your user id in the vault. Required.\n --collections <list> Comma-separated collection list. Format:\n rotate: invoices,clients\n add user: invoices:rw,clients:ro (operator/client only)\n\nExamples:\n noy-db add invoices\n noy-db add user accountant-ann operator --dir ./data --vault demo-co --user owner-alice --collections invoices:rw,clients:ro\n noy-db verify\n noy-db rotate --dir ./data --vault demo-co --user owner-alice\n noy-db backup ./backups/demo-co-2026-04-07.json --dir ./data --vault demo-co --user owner-alice\n\nRun from the root of a project that already has a noy-db file\nadapter directory in place. For new projects, use\n\\`npm create @noy-db\\` instead.\n`\n\nasync function main(): Promise<void> {\n const [, , command, ...rest] = process.argv\n\n switch (command) {\n case undefined:\n case 'help':\n case '-h':\n case '--help':\n process.stdout.write(HELP)\n return\n\n case 'add':\n await runAdd(rest)\n return\n\n case 'verify':\n await runVerify()\n return\n\n case 'rotate':\n await runRotate(rest)\n return\n\n case 'backup':\n await runBackup(rest)\n return\n\n default:\n process.stderr.write(`${pc.red('error:')} unknown command '${command}'\\n\\n${HELP}`)\n process.exit(2)\n }\n}\n\n// ─── `add` dispatcher — branches between `add <collection>` and `add user …` ─\n\nasync function runAdd(args: string[]): Promise<void> {\n const first = args[0]\n if (first === 'user') {\n await runAddUser(args.slice(1))\n return\n }\n // Legacy path: `noy-db add <collection>` scaffolds store + page files.\n if (!first) {\n process.stderr.write(\n `${pc.red('error:')} \\`noy-db add\\` requires either a collection name or the \\`user\\` subcommand\\n\\n` +\n `Examples:\\n noy-db add invoices\\n noy-db add user alice operator ...\\n`,\n )\n process.exit(2)\n }\n try {\n const result = await addCollection({ name: first })\n process.stdout.write(`${pc.green('✔')} Created:\\n`)\n for (const file of result.files) {\n process.stdout.write(` ${pc.dim('→')} ${file}\\n`)\n }\n process.stdout.write(\n `\\nNext: visit ${pc.cyan(`/${first}`)} in your dev server, then edit ${pc.bold(`app/stores/${first}.ts`)} to match your domain.\\n`,\n )\n } catch (err) {\n process.stderr.write(`${pc.red('error:')} ${(err as Error).message}\\n`)\n process.exit(1)\n }\n}\n\n// ─── `verify` ──────────────────────────────────────────────────────────\n\nasync function runVerify(): Promise<void> {\n process.stdout.write(`${pc.dim('▸ Running noy-db integrity check…')}\\n`)\n const result = await verifyIntegrity()\n if (result.ok) {\n process.stdout.write(`${pc.green('✔')} ${result.message} ${pc.dim(`(${result.durationMs}ms)`)}\\n`)\n return\n }\n process.stderr.write(`${pc.red('✘')} ${result.message} ${pc.dim(`(${result.durationMs}ms)`)}\\n`)\n process.exit(1)\n}\n\n// ─── `add user` ────────────────────────────────────────────────────────\n\nasync function runAddUser(args: string[]): Promise<void> {\n // Positional: userId, role. Then flag bag.\n const userId = args[0]\n const roleInput = args[1]\n if (!userId || !roleInput) {\n process.stderr.write(\n `${pc.red('error:')} \\`noy-db add user\\` requires <userId> <role>\\n\\n` +\n `Example: noy-db add user ann operator --dir ./data --vault demo-co --user owner-alice --collections invoices:rw\\n`,\n )\n process.exit(2)\n }\n\n let role\n try {\n role = assertRole(roleInput)\n } catch (err) {\n process.stderr.write(`${pc.red('error:')} ${(err as Error).message}\\n`)\n process.exit(2)\n return\n }\n\n const flags = parseFlags(args.slice(2))\n const dir = flags.dir ?? './data'\n const vault = requireFlag(flags, 'vault')\n const callerUser = requireFlag(flags, 'user')\n\n const permissions = parsePermissions(flags['collections'])\n\n const opts: AddUserOptions = {\n dir,\n vault,\n callerUser,\n newUserId: userId,\n role,\n }\n if (flags['display-name']) opts.newUserDisplayName = flags['display-name']\n if (permissions) opts.permissions = permissions\n\n try {\n const result = await addUser(opts)\n process.stdout.write(\n `${pc.green('✔')} Granted ${pc.bold(result.role)} access to ${pc.cyan(result.userId)} in vault ${pc.cyan(vault)}.\\n`,\n )\n } catch (err) {\n process.stderr.write(`${pc.red('error:')} ${(err as Error).message}\\n`)\n process.exit(1)\n }\n}\n\n// ─── `rotate` ──────────────────────────────────────────────────────────\n\nasync function runRotate(args: string[]): Promise<void> {\n const flags = parseFlags(args)\n const dir = flags.dir ?? './data'\n const vault = requireFlag(flags, 'vault')\n const user = requireFlag(flags, 'user')\n\n const opts: RotateOptions = { dir, vault, user }\n const collections = parseCollectionList(flags['collections'])\n if (collections) opts.collections = collections\n\n try {\n const result = await rotate(opts)\n process.stdout.write(\n `${pc.green('✔')} Rotated ${pc.bold(String(result.rotated.length))} collection(s) in ${pc.cyan(vault)}:\\n`,\n )\n for (const name of result.rotated) {\n process.stdout.write(` ${pc.dim('→')} ${name}\\n`)\n }\n } catch (err) {\n process.stderr.write(`${pc.red('error:')} ${(err as Error).message}\\n`)\n process.exit(1)\n }\n}\n\n// ─── `backup` ──────────────────────────────────────────────────────────\n\nasync function runBackup(args: string[]): Promise<void> {\n const target = args[0]\n if (!target) {\n process.stderr.write(\n `${pc.red('error:')} \\`noy-db backup\\` requires a target path\\n\\n` +\n `Example: noy-db backup ./backups/demo.json --dir ./data --vault demo-co --user owner-alice\\n`,\n )\n process.exit(2)\n }\n\n const flags = parseFlags(args.slice(1))\n const dir = flags.dir ?? './data'\n const vault = requireFlag(flags, 'vault')\n const user = requireFlag(flags, 'user')\n\n try {\n const result = await backup({ dir, vault, user, target })\n process.stdout.write(\n `${pc.green('✔')} Wrote backup: ${pc.cyan(result.path)} ${pc.dim(`(${result.bytes} bytes)`)}\\n`,\n )\n } catch (err) {\n process.stderr.write(`${pc.red('error:')} ${(err as Error).message}\\n`)\n process.exit(1)\n }\n}\n\n// ─── Shared argv helpers ───────────────────────────────────────────────\n\n/**\n * Parse a sequence of `--key value` flag pairs into a record.\n * Boolean flags (`--flag` with no value) become `\"true\"`. Unknown\n * shapes (positional args after the known set) are ignored — the\n * caller has already peeled positionals before passing in.\n */\nfunction parseFlags(args: string[]): Record<string, string> {\n const out: Record<string, string> = {}\n for (let i = 0; i < args.length; i++) {\n const arg = args[i]\n if (!arg || !arg.startsWith('--')) continue\n const key = arg.slice(2)\n const next = args[i + 1]\n if (next && !next.startsWith('--')) {\n out[key] = next\n i++\n } else {\n out[key] = 'true'\n }\n }\n return out\n}\n\nfunction requireFlag(flags: Record<string, string>, name: string): string {\n const value = flags[name]\n if (!value) {\n process.stderr.write(\n `${pc.red('error:')} missing required flag --${name}\\n`,\n )\n process.exit(2)\n }\n return value\n}\n\n/**\n * Parse `--collections invoices:rw,clients:ro` into a\n * `{ invoices: 'rw', clients: 'ro' }` record. Returns `null` when\n * the input is empty or undefined.\n */\nfunction parsePermissions(\n input: string | undefined,\n): Record<string, 'rw' | 'ro'> | null {\n if (!input) return null\n const out: Record<string, 'rw' | 'ro'> = {}\n for (const pair of input.split(',').map((s) => s.trim()).filter(Boolean)) {\n const [name, mode] = pair.split(':')\n if (!name || !mode || (mode !== 'rw' && mode !== 'ro')) {\n throw new Error(\n `Invalid --collections entry \"${pair}\" — expected \"name:rw\" or \"name:ro\"`,\n )\n }\n out[name] = mode\n }\n return Object.keys(out).length > 0 ? out : null\n}\n\nmain().catch((err: unknown) => {\n process.stderr.write(`${pc.red('fatal:')} ${(err as Error).message}\\n`)\n process.exit(1)\n})\n","/**\n * `noy-db add <collection>` — scaffold a new collection inside an existing\n * Nuxt 4 project that already has `@noy-db/in-nuxt` configured.\n *\n * The command writes two files:\n *\n * 1. `app/stores/<collection>.ts` — a `defineNoydbStore<T>()` call with\n * a placeholder `T` interface and one example field. The user fills\n * in the real shape after the file is created.\n *\n * 2. `app/pages/<collection>.vue` — a minimal CRUD page that lists,\n * adds, and deletes records. The store ID and collection name are\n * derived from the argument; everything else is boilerplate.\n *\n * The command refuses to overwrite existing files. If either target\n * already exists it logs which one and exits non-zero — the user has to\n * delete or move the file first. There's no `--force` because forcing an\n * overwrite of generated UI code is almost always a footgun in disguise.\n */\n\nimport { promises as fs } from 'node:fs'\nimport path from 'node:path'\n\nexport interface AddCollectionOptions {\n /** The collection name. Must be a lowercase identifier. */\n name: string\n /** Project root. Defaults to `process.cwd()`. */\n cwd?: string\n /** Compartment id to embed in the generated store. Defaults to `default`. */\n vault?: string\n}\n\n/**\n * Result returned to callers (the bin entry uses this to format output;\n * tests assert on the file paths).\n */\nexport interface AddCollectionResult {\n /** Files written, in the order they were created. */\n files: string[]\n}\n\n/**\n * Lowercase identifier check, narrower than the project-name check —\n * collection names become Vue component names, Pinia store IDs, AND TS\n * symbols, so we keep them simple: letters, digits, hyphens, must start\n * with a letter.\n */\nexport function validateCollectionName(name: string): string | null {\n if (!name) return 'Collection name is required'\n if (!/^[a-z][a-z0-9-]*$/.test(name)) {\n return 'Collection name must start with a lowercase letter and contain only lowercase letters, digits, or hyphens'\n }\n return null\n}\n\nexport async function addCollection(\n options: AddCollectionOptions,\n): Promise<AddCollectionResult> {\n const err = validateCollectionName(options.name)\n if (err) throw new Error(err)\n\n const cwd = options.cwd ?? process.cwd()\n const vault = options.vault ?? 'default'\n const name = options.name\n\n // Convert kebab-case to PascalCase for the TS interface name and the\n // Vue helper variable.\n const PascalName = name\n .split('-')\n .map((s) => (s.length === 0 ? s : (s[0]?.toUpperCase() ?? '') + s.slice(1)))\n .join('')\n const useFnName = `use${PascalName}`\n\n const storePath = path.join(cwd, 'app', 'stores', `${name}.ts`)\n const pagePath = path.join(cwd, 'app', 'pages', `${name}.vue`)\n\n // Refuse to overwrite. Doing both checks before writing either file\n // means a partial write is impossible — either both files land or\n // neither does.\n for (const target of [storePath, pagePath]) {\n if (await pathExists(target)) {\n throw new Error(`Refusing to overwrite existing file: ${target}`)\n }\n }\n\n await fs.mkdir(path.dirname(storePath), { recursive: true })\n await fs.mkdir(path.dirname(pagePath), { recursive: true })\n\n await fs.writeFile(storePath, renderStore(name, PascalName, useFnName, vault), 'utf8')\n await fs.writeFile(pagePath, renderPage(name, PascalName, useFnName), 'utf8')\n\n return { files: [storePath, pagePath] }\n}\n\nasync function pathExists(p: string): Promise<boolean> {\n try {\n await fs.access(p)\n return true\n } catch {\n return false\n }\n}\n\n/**\n * Renders the `app/stores/<name>.ts` file. The interface is intentionally\n * minimal — `id` and `name` are the only fields. The user is expected to\n * extend it after generation; we'd rather not lock them into a domain\n * shape we guessed.\n */\nfunction renderStore(\n name: string,\n PascalName: string,\n useFnName: string,\n vault: string,\n): string {\n return `// Generated by \\`noy-db add ${name}\\`.\n//\n// Edit the ${PascalName} interface to match your domain, then call\n// \\`${useFnName}()\\` from any component.\n//\n// defineNoydbStore is auto-imported by @noy-db/in-nuxt. The vault\n// id is the tenant/company namespace — change it if you have multiple.\n\nexport interface ${PascalName} {\n id: string\n name: string\n}\n\nexport const ${useFnName} = defineNoydbStore<${PascalName}>('${name}', {\n vault: '${vault}',\n})\n`\n}\n\n/**\n * Renders the `app/pages/<name>.vue` page — list + add + delete. Picks\n * the simplest possible template-renderer pattern (`v-for`, no fancy\n * components) so the generated file is easy to read and modify.\n */\nfunction renderPage(name: string, PascalName: string, useFnName: string): string {\n return `<!--\n Generated by \\`noy-db add ${name}\\`.\n Visit /${name} in your dev server.\n-->\n<script setup lang=\"ts\">\nconst ${name} = ${useFnName}()\nawait ${name}.$ready\n\nfunction addOne() {\n ${name}.add({\n id: crypto.randomUUID(),\n name: 'New ${PascalName}',\n })\n}\n\nfunction removeOne(id: string) {\n ${name}.remove(id)\n}\n</script>\n\n<template>\n <main>\n <h1>${PascalName}</h1>\n <button @click=\"addOne\">Add ${PascalName}</button>\n <ul>\n <li v-for=\"item in ${name}.items\" :key=\"item.id\">\n {{ item.name }}\n <button @click=\"removeOne(item.id)\">Delete</button>\n </li>\n </ul>\n </main>\n</template>\n`\n}\n","/**\n * `noy-db verify` — end-to-end integrity check.\n *\n * Opens an in-memory NOYDB instance, writes a record, reads it back,\n * decrypts it, and asserts the round-trip is byte-identical. The check\n * exercises the full crypto path (PBKDF2 → KEK → DEK → AES-GCM) without\n * touching any user data on disk.\n *\n * Why an in-memory check is the right scope:\n * - It validates that @noy-db/core, @noy-db/memory, and the user's\n * installed Node version all agree on Web Crypto. That's the most\n * common silent failure for first-time installers.\n * - It cannot accidentally corrupt user data because there isn't any.\n * - It runs in well under one second, so users actually run it.\n *\n * What this command does NOT do (intentionally):\n * - Open the user's actual vault file/dynamo/s3/browser store.\n * That requires the user's passphrase — not something we want a CLI\n * `verify` command to prompt for. The full passphrase-driven verify\n * belongs in `nuxi noydb verify` once the auth story for CLIs lands\n * in. For now `noy-db verify` is the dependency-graph smoke test.\n */\n\nimport { createNoydb } from '@noy-db/hub'\nimport { memory } from '@noy-db/to-memory'\n\nexport interface VerifyResult {\n /** `true` if the round-trip succeeded; `false` if anything diverged. */\n ok: boolean\n /** Human-readable status. Always set, even on success. */\n message: string\n /** Wall-clock time the integrity check took, in ms. */\n durationMs: number\n}\n\n/**\n * Runs the end-to-end check. Pure function — no console output, no\n * `process.exit`. The bin wrapper handles formatting and exit codes so\n * the function is trivial to call from tests.\n */\nexport async function verifyIntegrity(): Promise<VerifyResult> {\n const start = performance.now()\n try {\n const db = await createNoydb({\n store: memory(),\n user: 'noy-db-verify',\n // The passphrase here is throwaway — the in-memory adapter never\n // persists anything, and the KEK is destroyed when we call close()\n // a few lines down. We use a non-trivial value just to exercise\n // PBKDF2 properly.\n secret: 'noy-db-verify-passphrase-2026',\n })\n const company = await db.openVault('verify-co')\n const collection = company.collection<{ id: string; n: number }>('verify')\n\n // Round-trip a single record. We pick a value that's small enough\n // to print on failure but large enough to ensure encryption isn't\n // accidentally a no-op.\n const original = { id: 'verify-1', n: 42 }\n await collection.put('verify-1', original)\n const got = await collection.get('verify-1')\n if (!got || got.id !== original.id || got.n !== original.n) {\n return fail(start, `Round-trip mismatch: got ${JSON.stringify(got)}`)\n }\n\n // Make sure the query DSL works too — this catches the case where\n // the user's @noy-db/core install is at (no query DSL) but the\n // CLI was updated to.\n const found = collection.query().where('n', '==', 42).toArray()\n if (found.length !== 1) {\n return fail(start, `Query DSL mismatch: expected 1 result, got ${found.length}`)\n }\n\n db.close()\n\n return {\n ok: true,\n message: 'noy-db integrity check passed',\n durationMs: Math.round(performance.now() - start),\n }\n } catch (err) {\n return fail(start, `Integrity check threw: ${(err as Error).message}`)\n }\n}\n\nfunction fail(start: number, message: string): VerifyResult {\n return {\n ok: false,\n message,\n durationMs: Math.round(performance.now() - start),\n }\n}\n","/**\n * `noy-db rotate` — rotate the DEKs for one or more collections in\n * a vault.\n *\n * What it does\n * ------------\n * For each target collection:\n *\n * 1. Generate a fresh DEK\n * 2. Decrypt every record with the old DEK\n * 3. Re-encrypt every record with the new DEK\n * 4. Re-wrap the new DEK into every remaining user's keyring\n *\n * The old DEKs become unreachable as soon as the keyring files are\n * updated. This is the \"just rotate\" path — nobody is revoked,\n * everybody keeps their current permissions, but the key material\n * is replaced.\n *\n * Why expose this as a CLI command\n * --------------------------------\n * Two real-world scenarios:\n *\n * 1. **Suspected key leak.** An operator lost a laptop, a\n * developer accidentally pasted a passphrase into a Slack\n * channel, a USB stick went missing. Even if you think the\n * passphrase is safe, rotating is cheap insurance.\n *\n * 2. **Scheduled rotation.** Some compliance regimes require\n * periodic key rotation regardless of exposure. A CLI makes\n * this scriptable from cron or a CI job.\n *\n * This module is test-first: all inputs are plain options, the\n * passphrase reader is injected, and the Noydb factory is\n * injectable. The production bin is a thin wrapper that defaults\n * those injections to their real implementations.\n */\n\nimport { withTeam } from '@noy-db/hub/team'\nimport { createNoydb, type Noydb, type NoydbStore } from '@noy-db/hub'\nimport { jsonFile } from '@noy-db/to-file'\nimport type { ReadPassphrase } from './shared.js'\nimport { defaultReadPassphrase } from './shared.js'\n\nexport interface RotateOptions {\n /** Directory containing the vault data (file adapter only). */\n dir: string\n /** Vault (tenant) name to rotate keys in. */\n vault: string\n /** The user id of the operator running the rotate. */\n user: string\n /**\n * Explicit list of collections to rotate. When undefined, the\n * rotation targets every collection the user has a DEK for —\n * resolved at run time by reading the vault snapshot.\n */\n collections?: string[]\n /** Injected passphrase reader. Defaults to the clack implementation. */\n readPassphrase?: ReadPassphrase\n /**\n * Injected Noydb factory. Production code leaves this undefined\n * and gets `createNoydb`; tests pass a constructor that builds\n * against an in-memory adapter.\n */\n createDb?: typeof createNoydb\n /**\n * Injected adapter factory. Production code leaves this undefined\n * and gets `jsonFile`; tests pass one that returns the shared\n * in-memory adapter their fixture used.\n */\n buildAdapter?: (dir: string) => NoydbStore\n}\n\nexport interface RotateResult {\n /** The collections that were actually rotated. */\n rotated: string[]\n}\n\n/**\n * Run the rotate flow against a file-adapter vault. Returns\n * the list of collections that were rotated so callers can display\n * it to the user.\n *\n * Throws `Error` on any auth/adapter/rotate failure. The bin\n * catches these and prints a friendly message; direct callers\n * (tests) can inspect the error message to assert specific\n * failure modes.\n */\nexport async function rotate(options: RotateOptions): Promise<RotateResult> {\n const readPassphrase = options.readPassphrase ?? defaultReadPassphrase\n const buildAdapter = options.buildAdapter ?? ((dir) => jsonFile({ dir }))\n const createDb = options.createDb ?? createNoydb\n\n // Read the passphrase BEFORE opening the database. This way a\n // cancelled prompt (Ctrl-C at the password entry) leaves the\n // adapter completely untouched — no files opened, no locks held.\n const secret = await readPassphrase(`Passphrase for ${options.user}`)\n\n let db: Noydb | null = null\n try {\n db = await createDb({\n store: buildAdapter(options.dir),\n user: options.user,\n secret,\n // Key rotation is the opt-in team capability (#267).\n teamStrategy: withTeam(),\n })\n\n // Resolve \"all collections\" by asking the vault. This\n // happens BEFORE rotate() is called, so the list is stable\n // across the operation — adding a new collection mid-rotate\n // would be a race we're not guarding against (single-writer\n // assumption applies).\n const vault = await db.openVault(options.vault)\n const targets = options.collections && options.collections.length > 0\n ? options.collections\n : await vault.collections()\n\n if (targets.length === 0) {\n throw new Error(\n `Vault \"${options.vault}\" has no collections to rotate.`,\n )\n }\n\n await db.rotate(options.vault, targets)\n return { rotated: targets }\n } finally {\n // Always close the DB on exit — success or failure. Close()\n // clears the KEK and DEKs from process memory, which is the\n // final line of defense if the passphrase somehow leaked\n // into a log line above this block.\n db?.close()\n }\n}\n","/**\n * Shared primitives for the interactive `noy-db` subcommands that\n * need to unlock a real vault.\n *\n * Three things live here:\n *\n * 1. `ReadPassphrase` — a tiny interface for \"prompt the user for\n * a passphrase\", with a test-friendly default. Subcommands take\n * this as an injected dependency so tests can short-circuit\n * the prompt without spawning a pty.\n *\n * 2. `defaultReadPassphrase` — the production implementation,\n * built on `@clack/prompts` `password()`. Never echoes the\n * value to the terminal, never logs it, clears it from the\n * returned promise after the caller consumes it.\n *\n * 3. `assertRole` — narrow unknown string input to the Role type\n * with a consistent error message.\n *\n * ## Why pull this out\n *\n * `rotate`, `addUser`, and `backup` all need the same \"prompt for\n * a passphrase\" shape and the same \"open a file adapter and get\n * back a Noydb instance\" shape. Duplicating it in three files would\n * drift over time; centralizing means one place to audit the\n * passphrase-handling contract (never log, never persist, clear\n * local variables after use).\n */\n\nimport { password, isCancel, cancel } from '@clack/prompts'\nimport type { Role } from '@noy-db/hub'\n\nconst VALID_ROLES = ['owner', 'admin', 'operator', 'viewer', 'client'] as const\n\n/**\n * Asynchronous passphrase reader. Production code passes\n * `defaultReadPassphrase`; tests pass a stub that returns a fixed\n * string without touching stdin.\n *\n * The `label` is shown to the user as the prompt message. It\n * should never contain the expected passphrase or any secret.\n */\nexport type ReadPassphrase = (label: string) => Promise<string>\n\n/**\n * Clack-based passphrase prompt. Cancellation (Ctrl-C) aborts the\n * process with exit code 1 — prompts are always the first thing to\n * fire in a subcommand, so aborting here doesn't leave the system\n * in a half-mutated state.\n */\nexport const defaultReadPassphrase: ReadPassphrase = async (label) => {\n const value = await password({\n message: label,\n // Basic sanity: reject empty strings up front. We don't enforce\n // length here because the caller's KEK-derivation step will\n // reject weak passphrases with its own, richer error.\n validate: (v) => (v.length === 0 ? 'Passphrase cannot be empty' : undefined),\n })\n if (isCancel(value)) {\n cancel('Cancelled.')\n process.exit(1)\n }\n return value\n}\n\n/**\n * Narrow an unknown string to the `Role` type from @noy-db/core.\n * Used by the `add user` subcommand to validate the role argument\n * before passing it to `noydb.grant()`.\n */\nexport function assertRole(input: string): Role {\n if (!(VALID_ROLES as readonly string[]).includes(input)) {\n throw new Error(\n `Invalid role \"${input}\" — must be one of: ${VALID_ROLES.join(', ')}`,\n )\n }\n return input as Role\n}\n\n/**\n * Split a comma-separated collection list into an array of names,\n * trimming whitespace and dropping empties. Returns null if the\n * input itself is empty or undefined — the caller decides whether\n * that means \"all collections\" or \"error\".\n */\nexport function parseCollectionList(input: string | undefined): string[] | null {\n if (!input) return null\n const parts = input\n .split(',')\n .map((s) => s.trim())\n .filter((s) => s.length > 0)\n return parts.length > 0 ? parts : null\n}\n","/**\n * `noy-db add user <id> <role>` — grant a new user access to a\n * vault.\n *\n * What it does\n * ------------\n * Wraps `noydb.grant()` in the CLI's auth-prompt ritual:\n *\n * 1. Prompt the caller for their own passphrase (to unlock the\n * caller's keyring and derive the wrapping key).\n * 2. Prompt for the new user's passphrase.\n * 3. Prompt for confirmation of the new passphrase.\n * 4. Reject on mismatch.\n * 5. Call `noydb.grant(vault, { userId, role, passphrase, permissions })`.\n *\n * For owner/admin/viewer roles, every collection is granted\n * automatically (the core keyring.ts grant logic handles that via\n * the `permissions` field). For operator/client, the caller must\n * pass a `--collections` list because those roles need explicit\n * per-collection permissions.\n *\n * ## What this does NOT do\n *\n * - No email/invite flow — is about local-CLI key management,\n * not out-of-band user enrollment.\n * - No rollback on partial failure — `grant()` is atomic at the\n * core level (keyring file writes last, after DEK wrapping), so\n * partial-state-on-crash is already handled.\n */\n\nimport { withTeam } from '@noy-db/hub/team'\nimport { createNoydb, type Noydb, type NoydbStore, type Role } from '@noy-db/hub'\nimport { jsonFile } from '@noy-db/to-file'\nimport type { ReadPassphrase } from './shared.js'\nimport { defaultReadPassphrase } from './shared.js'\n\nexport interface AddUserOptions {\n /** Directory containing the vault data (file adapter only). */\n dir: string\n /** Vault (tenant) name to grant access to. */\n vault: string\n /** The user id of the caller running the grant. */\n callerUser: string\n /** The new user's id (must not already exist in the vault keyring). */\n newUserId: string\n /** The new user's display name — shown in UI and audit logs. Defaults to `newUserId`. */\n newUserDisplayName?: string\n /** The new user's role. */\n role: Role\n /**\n * Per-collection permissions. Required when `role` is operator or\n * client; ignored for owner/admin/viewer (they get everything\n * via the core's resolvePermissions logic).\n *\n * Shape: `{ invoices: 'rw', clients: 'ro' }`. CLI callers pass\n * `--collections invoices:rw,clients:ro` and the argv parser\n * converts it to this shape.\n */\n permissions?: Record<string, 'rw' | 'ro'>\n /** Injected passphrase reader. Defaults to the clack implementation. */\n readPassphrase?: ReadPassphrase\n /** Injected Noydb factory. */\n createDb?: typeof createNoydb\n /** Injected adapter factory. */\n buildAdapter?: (dir: string) => NoydbStore\n}\n\nexport interface AddUserResult {\n /** The userId that was granted access. */\n userId: string\n /** The role they were granted. */\n role: Role\n}\n\n/**\n * Run the grant flow. Two passphrase prompts: caller's, then new\n * user's (twice for confirmation). Calls `noydb.grant()` with the\n * collected values.\n */\nexport async function addUser(options: AddUserOptions): Promise<AddUserResult> {\n const readPassphrase = options.readPassphrase ?? defaultReadPassphrase\n const buildAdapter = options.buildAdapter ?? ((dir) => jsonFile({ dir }))\n const createDb = options.createDb ?? createNoydb\n\n // Operator/client roles NEED explicit permissions. Reject here\n // rather than in the middle of the grant, so the caller sees the\n // problem before any I/O happens.\n if (\n (options.role === 'operator' || options.role === 'client') &&\n (!options.permissions || Object.keys(options.permissions).length === 0)\n ) {\n throw new Error(\n `Role \"${options.role}\" requires explicit --collections — e.g. --collections invoices:rw,clients:ro`,\n )\n }\n\n const callerSecret = await readPassphrase(\n `Your passphrase (${options.callerUser})`,\n )\n const newSecret = await readPassphrase(\n `New passphrase for ${options.newUserId}`,\n )\n const confirmSecret = await readPassphrase(\n `Confirm passphrase for ${options.newUserId}`,\n )\n\n if (newSecret !== confirmSecret) {\n throw new Error(`Passphrases do not match — grant aborted.`)\n }\n\n let db: Noydb | null = null\n try {\n db = await createDb({\n store: buildAdapter(options.dir),\n user: options.callerUser,\n secret: callerSecret,\n // Multi-user grant is the opt-in team capability (#267) — the CLI\n // add-user command is by definition a multi-user operation.\n teamStrategy: withTeam(),\n })\n\n // Build the grant options. Only include `permissions` when the\n // caller actually supplied them — otherwise the core's\n // resolvePermissions fills in the role defaults. The spread\n // (rather than post-assignment) keeps the object literal\n // compatible with `GrantOptions`'s readonly `permissions`.\n const grantOpts: Parameters<Noydb['grant']>[1] = {\n userId: options.newUserId,\n displayName: options.newUserDisplayName ?? options.newUserId,\n role: options.role,\n passphrase: newSecret,\n ...(options.permissions ? { permissions: options.permissions } : {}),\n }\n\n await db.grant(options.vault, grantOpts)\n\n return {\n userId: options.newUserId,\n role: options.role,\n }\n } finally {\n db?.close()\n }\n}\n","/**\n * `noy-db backup <target>` — dump a vault to a local file.\n *\n * What it does\n * ------------\n * Wraps `vault.dump()` in the CLI's auth-prompt ritual, then\n * writes the serialized backup to the requested path. As of,\n * `dump()` already produces a verifiable backup (embedded\n * ledgerHead, full `_ledger` / `_ledger_deltas` snapshots) — the\n * CLI just moves bytes; the integrity guarantees come from core.\n *\n * ## Target URI support\n *\n * ships **`file://` only** (or a plain filesystem path).\n * The issue spec originally called for `s3://` as well, but\n * wiring @aws-sdk into @noy-db/create would defeat the\n * zero-runtime-deps story for the CLI package. S3 backup is\n * deferred to a follow-up that can live in @noy-db/s3-cli or a\n * similar optional companion package.\n *\n * Accepted forms:\n * - `file:///absolute/path.json`\n * - `file://./relative/path.json`\n * - `/absolute/path.json` (treated as `file://`)\n * - `./relative/path.json` (treated as `file://`)\n *\n * ## What this does NOT do\n *\n * - No encryption of the backup BEYOND what noy-db already does.\n * The dumped file is a valid noy-db backup, which means\n * individual records are still encrypted but the keyring is\n * included (wrapped with each user's KEK). Anyone who loads\n * the backup still needs the correct passphrase to read.\n * - No restore — that's a separate subcommand tracked as a\n * follow-up. For now users can restore via\n * `vault.load(backupString)` from their own app code.\n */\n\nimport { promises as fs } from 'node:fs'\nimport path from 'node:path'\nimport { createNoydb, type Noydb, type NoydbStore } from '@noy-db/hub'\nimport { withHistory } from '@noy-db/hub/history'\nimport { jsonFile } from '@noy-db/to-file'\nimport type { ReadPassphrase } from './shared.js'\nimport { defaultReadPassphrase } from './shared.js'\n\nexport interface BackupOptions {\n /** Directory containing the vault data (file adapter only). */\n dir: string\n /** Vault (tenant) name to back up. */\n vault: string\n /** The user id of the operator running the backup. */\n user: string\n /**\n * Where to write the backup. Accepts a `file://` URI or a plain\n * filesystem path. Relative paths resolve against `process.cwd()`.\n */\n target: string\n /** Injected passphrase reader. */\n readPassphrase?: ReadPassphrase\n /** Injected Noydb factory. */\n createDb?: typeof createNoydb\n /** Injected adapter factory. */\n buildAdapter?: (dir: string) => NoydbStore\n}\n\nexport interface BackupResult {\n /** Absolute filesystem path the backup was written to. */\n path: string\n /** Size of the serialized backup in bytes. */\n bytes: number\n}\n\n/**\n * Parse a backup target into an absolute filesystem path. Rejects\n * unsupported URI schemes (s3://, https://, etc.) early so the\n * caller doesn't silently write to the wrong place.\n */\nexport function resolveBackupTarget(target: string, cwd: string = process.cwd()): string {\n // Strip the `file://` prefix if present. The rest of the string\n // is treated as a filesystem path. We accept both `file:///abs`\n // (three slashes, absolute) and `file://./rel` (two slashes,\n // relative) because real-world users write both.\n let raw = target\n if (target.startsWith('file://')) {\n raw = target.slice('file://'.length)\n } else if (target.includes('://')) {\n // Any other scheme is unsupported.\n throw new Error(\n `Unsupported backup target scheme: \"${target.split('://')[0]}://\". ` +\n `Only file:// and plain filesystem paths are supported in. ` +\n `S3 backups will land in a follow-up.`,\n )\n }\n return path.resolve(cwd, raw)\n}\n\nexport async function backup(options: BackupOptions): Promise<BackupResult> {\n const readPassphrase = options.readPassphrase ?? defaultReadPassphrase\n const buildAdapter = options.buildAdapter ?? ((dir) => jsonFile({ dir }))\n const createDb = options.createDb ?? createNoydb\n\n // Resolve the target FIRST so a bad URI fails before any\n // passphrase is collected. This keeps the UX clean: a typo in\n // `s3://bucket/x` rejects without asking for a secret the user\n // would then have to type again.\n const absolutePath = resolveBackupTarget(options.target)\n\n const secret = await readPassphrase(`Passphrase for ${options.user}`)\n\n let db: Noydb | null = null\n try {\n db = await createDb({\n store: buildAdapter(options.dir),\n user: options.user,\n secret,\n // Opt into the history strategy so `vault.dump()` embeds\n // `ledgerHead` for tamper-evident verification at restore.\n // Source vaults that were never written with history simply\n // have an empty ledger; the embedded head is omitted in that\n // case and the backup is treated as legacy on load.\n historyStrategy: withHistory(),\n })\n const vault = await db.openVault(options.vault)\n const serialized = await vault.dump()\n\n // Make sure the parent directory exists. If the user passed\n // `./backups/2026/demo.json` and `./backups/2026` doesn't\n // exist yet, we create it. This is the common case for\n // scripted rotations dropping into a date-based folder.\n await fs.mkdir(path.dirname(absolutePath), { recursive: true })\n await fs.writeFile(absolutePath, serialized, 'utf8')\n\n return {\n path: absolutePath,\n bytes: Buffer.byteLength(serialized, 'utf8'),\n }\n } finally {\n db?.close()\n }\n}\n"],"mappings":"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","names":["createNoydb","createNoydb","withTeam","createNoydb","jsonFile","jsonFile","createNoydb","withTeam","fs","path","createNoydb","jsonFile","path","jsonFile","createNoydb","fs"]}
package/dist/index.js CHANGED
@@ -792,6 +792,7 @@ function fail(start, message) {
792
792
  }
793
793
 
794
794
  // src/commands/rotate.ts
795
+ import { withTeam } from "@noy-db/hub/team";
795
796
  import { createNoydb as createNoydb2 } from "@noy-db/hub";
796
797
  import { jsonFile } from "@noy-db/to-file";
797
798
 
@@ -837,7 +838,9 @@ async function rotate(options) {
837
838
  db = await createDb({
838
839
  store: buildAdapter(options.dir),
839
840
  user: options.user,
840
- secret
841
+ secret,
842
+ // Key rotation is the opt-in team capability (#267).
843
+ teamStrategy: withTeam()
841
844
  });
842
845
  const vault = await db.openVault(options.vault);
843
846
  const targets = options.collections && options.collections.length > 0 ? options.collections : await vault.collections();
@@ -854,6 +857,7 @@ async function rotate(options) {
854
857
  }
855
858
 
856
859
  // src/commands/add-user.ts
860
+ import { withTeam as withTeam2 } from "@noy-db/hub/team";
857
861
  import { createNoydb as createNoydb3 } from "@noy-db/hub";
858
862
  import { jsonFile as jsonFile2 } from "@noy-db/to-file";
859
863
  async function addUser(options) {
@@ -882,7 +886,10 @@ async function addUser(options) {
882
886
  db = await createDb({
883
887
  store: buildAdapter(options.dir),
884
888
  user: options.callerUser,
885
- secret: callerSecret
889
+ secret: callerSecret,
890
+ // Multi-user grant is the opt-in team capability (#267) — the CLI
891
+ // add-user command is by definition a multi-user operation.
892
+ teamStrategy: withTeam2()
886
893
  });
887
894
  const grantOpts = {
888
895
  userId: options.newUserId,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/wizard/run.ts","../src/wizard/render.ts","../src/wizard/detect.ts","../src/wizard/augment.ts","../src/wizard/i18n/en.ts","../src/wizard/i18n/th.ts","../src/wizard/i18n/index.ts","../src/commands/add.ts","../src/commands/verify.ts","../src/commands/rotate.ts","../src/commands/shared.ts","../src/commands/add-user.ts","../src/commands/backup.ts"],"sourcesContent":["/**\n * The wizard entry point — `runWizard()`.\n *\n * Two modes:\n *\n * 1. **Interactive (default).** Uses `@clack/prompts` to ask the user\n * for project name, adapter, and sample-data inclusion. Cancellation\n * at any prompt aborts cleanly with a non-zero exit code.\n *\n * 2. **Non-interactive (`yes: true`).** Skips every prompt and uses the\n * values supplied in `WizardOptions`. Missing values become defaults.\n * This is the path tests take — no terminal needed, fully scriptable.\n *\n * The function never spawns child processes (no `npm install` etc.). It\n * only writes files and returns. The shell wrapper around `npm create` is\n * responsible for installing — we keep this layer pure so it's trivially\n * testable and so adding a `--no-install` flag later is a no-op.\n */\n\nimport { promises as fs } from 'node:fs'\nimport path from 'node:path'\nimport * as p from '@clack/prompts'\nimport pc from 'picocolors'\nimport { renderTemplate, templateDir, type RenderTokens } from './render.js'\nimport type {\n WizardAdapter,\n WizardOptions,\n WizardResult,\n WizardFreshResult,\n WizardAugmentResult,\n WizardTemplate,\n} from './types.js'\nimport { detectNuxtProject } from './detect.js'\nimport { augmentNuxtConfig, writeAugmentedConfig } from './augment.js'\nimport { detectLocale, loadMessages, type WizardMessages } from './i18n/index.js'\n\n/**\n * Default invoice records the wizard injects when `sampleData: true`.\n * Kept here (not in the template) so the same records can be reused by\n * the `noy-db add` command for collections with `--seed`. Three records\n * is enough to demo a query that returns more than one row.\n */\nconst DEFAULT_SEED = [\n {\n id: 'inv-001',\n client: 'Acme Holdings',\n amount: 1500,\n status: 'open',\n dueDate: '2026-05-01',\n },\n {\n id: 'inv-002',\n client: 'Globex Inc',\n amount: 4200,\n status: 'paid',\n dueDate: '2026-04-15',\n },\n {\n id: 'inv-003',\n client: 'Initech LLC',\n amount: 850,\n status: 'overdue',\n dueDate: '2026-03-20',\n },\n]\n\nfunction adapterLabels(msg: WizardMessages): Record<WizardAdapter, string> {\n return {\n browser: msg.adapterBrowserLabel,\n file: msg.adapterFileLabel,\n memory: msg.adapterMemoryLabel,\n }\n}\n\n/**\n * Validates the project name. Rules are intentionally narrow:\n * - non-empty\n * - lowercase letters, digits, hyphens, dots, underscores only\n * - cannot start with a hyphen, dot, or underscore\n * - max 214 chars (npm package-name limit)\n *\n * The narrow rule set means whatever the user types is also a valid npm\n * package name, so the generated `package.json#name` field is always safe.\n */\nexport function validateProjectName(name: string): string | null {\n if (!name || name.trim() === '') return 'Project name cannot be empty'\n if (name.length > 214) return 'Project name must be 214 characters or fewer'\n if (!/^[a-z0-9][a-z0-9._-]*$/.test(name)) {\n return 'Project name must start with a lowercase letter or digit and contain only lowercase letters, digits, hyphens, dots, or underscores'\n }\n return null\n}\n\n/**\n * Main entry point. Detects whether `cwd` is an existing Nuxt 4\n * project and routes to one of two modes:\n *\n * - **Fresh mode** (the original behavior): prompts for\n * project name, creates a new directory, renders the Nuxt 4\n * starter template. Returns a `WizardFreshResult`.\n *\n * - **Augment mode** (new in, ): patches the existing\n * `nuxt.config.ts` via magicast to add `@noy-db/in-nuxt` to the\n * modules array and a `noydb:` config key. Shows a unified\n * diff and asks for confirmation before writing. Supports\n * `--dry-run`. Returns a `WizardAugmentResult`.\n *\n * The auto-detection rule: if cwd has both a `nuxt.config.ts`\n * (or `.js`/`.mjs`) AND a `package.json` that lists `nuxt` in any\n * dependency section, augment mode fires. Otherwise fresh mode.\n * Users can force fresh mode via `forceFresh: true` (CLI:\n * `--force-fresh`) when they want to create a sub-project inside\n * an existing Nuxt workspace.\n *\n * Both modes refuse to clobber existing work: fresh mode rejects\n * non-empty target dirs; augment mode rejects unsupported config\n * shapes (opaque exports, non-array modules, etc.).\n */\nexport async function runWizard(options: WizardOptions = {}): Promise<WizardResult> {\n const cwd = options.cwd ?? process.cwd()\n const yes = options.yes ?? false\n\n // Resolve the message bundle once at the top so every downstream\n // helper sees a consistent locale. Explicit `options.locale` wins;\n // otherwise we fall back to POSIX env-var detection (LC_ALL → LANG).\n // Tests pin the locale to keep snapshots deterministic.\n const msg = loadMessages(options.locale ?? detectLocale())\n\n // ── Detect existing Nuxt project ───────────────────────────────────\n // Runs BEFORE any prompts so the interactive flow branches cleanly\n // into fresh vs augment without asking the user questions that\n // don't apply to their mode. `forceFresh` short-circuits the\n // detection — CI tests use this to scaffold into a temp dir that\n // happens to sit under an existing Nuxt project.\n const detection = options.forceFresh\n ? null\n : await detectNuxtProject(cwd)\n\n if (detection?.existing && detection.configPath) {\n return runAugmentMode(options, cwd, detection.configPath, msg)\n }\n\n return runFreshMode(options, cwd, yes, msg)\n}\n\n/**\n * The original fresh-project path, factored out of the\n * main entry so the augment branch above can coexist. Behavior\n * is unchanged from except the return shape now includes\n * the `kind: 'fresh'` discriminator.\n */\nasync function runFreshMode(\n options: WizardOptions,\n cwd: string,\n yes: boolean,\n msg: WizardMessages,\n): Promise<WizardFreshResult> {\n // ── Resolve answers ────────────────────────────────────────────────\n // In non-interactive mode every prompt is short-circuited; in\n // interactive mode we only prompt for fields the caller didn't supply.\n const projectName = yes\n ? options.projectName ?? 'my-noy-db-app'\n : await promptProjectName(options.projectName, msg)\n\n const adapter: WizardAdapter = yes\n ? options.adapter ?? 'browser'\n : await promptAdapter(options.adapter, msg)\n\n const sampleData: boolean = yes\n ? options.sampleData ?? true\n : await promptSampleData(options.sampleData, msg)\n\n const template: WizardTemplate = yes\n ? options.template ?? 'nuxt-default'\n : await promptTemplate(options.template, msg)\n\n const syncAdapter: WizardAdapter | 'none' = yes\n ? options.syncAdapter ?? 'none'\n : await promptSyncAdapter(options.syncAdapter, adapter, msg)\n\n // ── Validate target directory ──────────────────────────────────────\n // We refuse to write into a non-empty directory. Empty + missing are\n // both fine — fs.mkdir { recursive: true } handles both. The `Cancelled`\n // exit is intentional: cleaner than throwing an Error from a wizard.\n const projectPath = path.resolve(cwd, projectName)\n await assertWritableTarget(projectPath)\n\n // ── Render the template ────────────────────────────────────────────\n const tokens: RenderTokens = {\n PROJECT_NAME: projectName,\n ADAPTER: adapter,\n DEVTOOLS: 'true',\n SEED_INVOICES: sampleData\n ? JSON.stringify(DEFAULT_SEED, null, 2).replace(/\\n/g, '\\n ')\n : '[]',\n }\n\n await fs.mkdir(projectPath, { recursive: true })\n const files = await renderTemplate(\n templateDir(template),\n projectPath,\n tokens,\n )\n\n if (!yes) {\n // Print a friendly summary so the user knows what happened. We don't\n // run `npm install` ourselves — the user picks the package manager.\n p.note(\n [\n `${pc.bold('cd')} ${projectName}`,\n `${pc.bold('pnpm install')} ${pc.dim('(or npm/yarn/bun)')}`,\n `${pc.bold('pnpm dev')}`,\n ].join('\\n'),\n msg.freshNextStepsTitle,\n )\n p.outro(pc.green(msg.freshOutroDone))\n }\n\n return {\n kind: 'fresh',\n options: {\n projectName,\n adapter,\n sampleData,\n cwd,\n template,\n syncAdapter,\n },\n projectPath,\n files,\n }\n}\n\n/**\n * The new augment-existing-project path. Runs magicast\n * on the detected nuxt.config, shows a unified diff, asks for\n * confirmation, and writes. Supports `--dry-run` to see the diff\n * without touching disk.\n *\n * Three outcomes:\n * - **written**: the file was patched successfully\n * - **already-configured**: both target mutations are already\n * present (idempotent no-op)\n * - **cancelled**: user said no at the confirmation prompt\n * - **dry-run**: `options.dryRun` was set, we showed the diff\n * and returned without writing\n * - **unsupported-shape**: the config file uses a shape we can't\n * safely mutate (opaque export, non-array modules, etc.)\n */\nasync function runAugmentMode(\n options: WizardOptions,\n cwd: string,\n configPath: string,\n msg: WizardMessages,\n): Promise<WizardAugmentResult> {\n const yes = options.yes ?? false\n const dryRun = options.dryRun ?? false\n\n if (!yes) {\n p.note(\n [\n `${pc.dim(msg.augmentDetectedPrefix)}`,\n ` ${pc.cyan(configPath)}`,\n '',\n msg.augmentDescription,\n ].join('\\n'),\n msg.augmentModeTitle,\n )\n }\n\n // In augment mode we only need ONE prompt from the user: which\n // adapter to wire into the `noydb: { adapter }` key. Everything\n // else is decided by the config we're patching.\n const adapter: WizardAdapter = yes\n ? options.adapter ?? 'browser'\n : await promptAdapter(options.adapter, msg)\n\n const result = await augmentNuxtConfig({\n configPath,\n adapter,\n dryRun,\n })\n\n if (result.kind === 'already-configured') {\n if (!yes) {\n p.note(\n `${pc.yellow(msg.augmentNothingToDo)} ${result.reason}`,\n msg.augmentAlreadyConfiguredTitle,\n )\n p.outro(pc.green(msg.augmentAlreadyOutro))\n }\n return {\n kind: 'augment',\n configPath,\n adapter,\n changed: false,\n reason: 'already-configured',\n }\n }\n\n if (result.kind === 'unsupported-shape') {\n if (!yes) {\n p.cancel(`${pc.red(msg.augmentUnsupportedPrefix)} ${result.reason}`)\n }\n return {\n kind: 'augment',\n configPath,\n adapter,\n changed: false,\n reason: 'unsupported-shape',\n }\n }\n\n // result.kind === 'proposed-change' — print the diff and either\n // write (after confirmation) or bail in dry-run mode.\n if (!yes || dryRun) {\n p.note(renderDiff(result.diff), msg.augmentProposedChangesTitle)\n }\n\n if (dryRun) {\n if (!yes) p.outro(pc.green(msg.augmentDryRunOutro))\n return {\n kind: 'augment',\n configPath,\n adapter,\n changed: false,\n reason: 'dry-run',\n diff: result.diff,\n }\n }\n\n let shouldWrite = yes\n if (!yes) {\n const confirmed = await p.confirm({\n message: msg.augmentApplyConfirm,\n initialValue: true,\n })\n if (p.isCancel(confirmed) || confirmed !== true) {\n p.cancel(msg.augmentAborted)\n return {\n kind: 'augment',\n configPath,\n adapter,\n changed: false,\n reason: 'cancelled',\n diff: result.diff,\n }\n }\n shouldWrite = true\n }\n\n if (shouldWrite) {\n await writeAugmentedConfig(configPath, result.newCode)\n if (!yes) {\n p.note(\n [\n pc.dim(msg.augmentInstallIntro),\n '',\n `${pc.bold('pnpm add')} @noy-db/in-nuxt @noy-db/in-pinia @noy-db/hub @noy-db/to-browser-idb @pinia/nuxt pinia`,\n pc.dim(msg.augmentInstallPmHint),\n ].join('\\n'),\n msg.augmentNextStepTitle,\n )\n p.outro(pc.green(msg.augmentDoneOutro))\n }\n }\n\n return {\n kind: 'augment',\n configPath,\n adapter,\n changed: true,\n reason: 'written',\n diff: result.diff,\n }\n}\n\n/**\n * Clean up a unified diff for terminal display. Strips the `===`\n * separators the `diff` package emits, keeps a reasonable max\n * width, and drops the Index/------ preamble that's only useful\n * for `patch -p1`. The result is a short, colorized-ready string\n * that fits inside a clack `note()` block.\n */\nfunction renderDiff(diff: string): string {\n const lines = diff.split('\\n')\n const keep: string[] = []\n for (const line of lines) {\n if (line.startsWith('Index:')) continue\n if (line.startsWith('=')) continue\n if (line.startsWith('---') || line.startsWith('+++')) continue\n if (line.startsWith('+')) keep.push(pc.green(line))\n else if (line.startsWith('-')) keep.push(pc.red(line))\n else if (line.startsWith('@@')) keep.push(pc.dim(line))\n else keep.push(line)\n }\n return keep.join('\\n').trim()\n}\n\n// ─── Prompt helpers ──────────────────────────────────────────────────────\n\nasync function promptProjectName(initial: string | undefined, msg: WizardMessages): Promise<string> {\n if (initial) {\n const err = validateProjectName(initial)\n if (err) throw new Error(err)\n return initial\n }\n const result = await p.text({\n message: msg.promptProjectName,\n placeholder: msg.promptProjectNamePlaceholder,\n initialValue: 'my-noy-db-app',\n validate: (v) => validateProjectName(v ?? '') ?? undefined,\n })\n if (p.isCancel(result)) {\n p.cancel(msg.cancelled)\n process.exit(1)\n }\n return result\n}\n\nasync function promptAdapter(initial: WizardAdapter | undefined, msg: WizardMessages): Promise<WizardAdapter> {\n if (initial) return initial\n const labels = adapterLabels(msg)\n const result = await p.select<WizardAdapter>({\n message: msg.promptAdapter,\n options: (['browser', 'file', 'memory'] as const).map((value) => ({\n value,\n label: labels[value],\n })),\n initialValue: 'browser',\n })\n if (p.isCancel(result)) {\n p.cancel(msg.cancelled)\n process.exit(1)\n }\n return result\n}\n\nasync function promptSyncAdapter(\n initial: WizardAdapter | 'none' | undefined,\n primary: WizardAdapter,\n _msg: WizardMessages,\n): Promise<WizardAdapter | 'none'> {\n if (initial !== undefined) return initial\n const choices = (['none', 'memory', 'file', 'browser'] as const).filter(\n (x) => x !== primary,\n )\n const labels: Record<WizardAdapter | 'none', string> = {\n none: 'No sync — local only',\n memory: 'In-memory (testing / CI)',\n file: 'JSON files (USB / shared drive)',\n browser: 'IndexedDB (other tabs / PWAs)',\n }\n const result = await p.select<WizardAdapter | 'none'>({\n message: 'Add a sync target?',\n options: choices.map((value) => ({ value, label: labels[value] })),\n initialValue: 'none',\n })\n if (p.isCancel(result)) {\n process.exit(1)\n }\n return result\n}\n\nasync function promptTemplate(\n initial: WizardTemplate | undefined,\n _msg: WizardMessages,\n): Promise<WizardTemplate> {\n if (initial) return initial\n const result = await p.select<WizardTemplate>({\n message: 'Pick a starter template',\n options: [\n {\n value: 'nuxt-default',\n label: 'Nuxt 4 + Pinia',\n hint: 'SSR + file-based routing (recommended default)',\n },\n {\n value: 'vite-vue',\n label: 'Vite + Vue 3 + Pinia',\n hint: 'Client-side SPA, shorter install graph',\n },\n {\n value: 'vanilla',\n label: 'Vite + TypeScript',\n hint: 'No framework, smallest footprint',\n },\n {\n value: 'electron',\n label: 'Electron + Vue 3',\n hint: 'Desktop app with to-file adapter — USB workflow',\n },\n ],\n initialValue: 'nuxt-default',\n })\n if (p.isCancel(result)) {\n process.exit(1)\n }\n return result\n}\n\nasync function promptSampleData(initial: boolean | undefined, msg: WizardMessages): Promise<boolean> {\n if (typeof initial === 'boolean') return initial\n const result = await p.confirm({\n message: msg.promptSampleData,\n initialValue: true,\n })\n if (p.isCancel(result)) {\n p.cancel(msg.cancelled)\n process.exit(1)\n }\n return result\n}\n\nasync function assertWritableTarget(target: string): Promise<void> {\n try {\n const entries = await fs.readdir(target)\n if (entries.length > 0) {\n throw new Error(\n `Target directory '${target}' already exists and is not empty. Refusing to overwrite — pick a different project name or remove the directory first.`,\n )\n }\n } catch (err: unknown) {\n // ENOENT is the happy path — the directory doesn't exist yet.\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return\n throw err\n }\n}\n","/**\n * Template loader and renderer.\n *\n * Templates live under `packages/create-noy-db/templates/<template-name>/`\n * as a literal directory tree. Files are read verbatim from disk; the\n * renderer walks the tree, substitutes a small set of placeholder tokens,\n * and writes the result to the target directory.\n *\n * The placeholder syntax is intentionally minimal: `{{TOKEN}}` (uppercase,\n * no spaces, no nested expressions). This avoids dragging in handlebars\n * or any templating library, keeps the bundle small, and means there's\n * exactly one rule to learn when authoring a template.\n *\n * Files whose names start with `_` are renamed to `.` on copy. This is\n * how we ship `_gitignore` (`.gitignore`) without npm trying to interpret\n * it as part of the package contents.\n */\n\nimport { promises as fs } from 'node:fs'\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\n/**\n * Tokens the renderer recognizes. Authoring rule for templates: every\n * placeholder must be a member of this set, otherwise template authors\n * will hit silent no-ops at render time.\n */\nexport interface RenderTokens {\n /** The user-supplied project name. Used for `package.json#name` etc. */\n PROJECT_NAME: string\n /** The chosen adapter (browser/file/memory). */\n ADAPTER: string\n /** \"true\" or \"false\" — written into `nuxt.config.ts` for `noydb.devtools`. */\n DEVTOOLS: string\n /** A literal `[...]` block of seed records, or `[]` if `sampleData` is false. */\n SEED_INVOICES: string\n}\n\n/**\n * Walks `src` recursively and copies every file into `dest`, substituting\n * `{{TOKEN}}` placeholders along the way.\n *\n * Returns the relative paths of every file written, sorted alphabetically.\n * Tests use this to assert that the expected file set was produced without\n * having to walk `dest` themselves.\n *\n * The function is intentionally synchronous-feeling (uses `await` inside\n * a depth-first walk) so the order of writes is deterministic — easier\n * to reason about when something fails halfway through.\n */\nexport async function renderTemplate(\n src: string,\n dest: string,\n tokens: RenderTokens,\n): Promise<string[]> {\n const written: string[] = []\n await walk(src, dest, '', tokens, written)\n written.sort()\n return written\n}\n\nasync function walk(\n srcRoot: string,\n destRoot: string,\n rel: string,\n tokens: RenderTokens,\n written: string[],\n): Promise<void> {\n const srcDir = path.join(srcRoot, rel)\n const entries = await fs.readdir(srcDir, { withFileTypes: true })\n\n for (const entry of entries) {\n const srcEntry = path.join(srcDir, entry.name)\n // _gitignore → .gitignore, _npmrc → .npmrc, etc.\n // npm strips files starting with `.` from the published tarball, so\n // shipping them as `_<name>` is the standard workaround.\n const destName = entry.name.startsWith('_')\n ? `.${entry.name.slice(1)}`\n : entry.name\n const destRel = rel ? path.join(rel, destName) : destName\n const destEntry = path.join(destRoot, destRel)\n\n if (entry.isDirectory()) {\n await fs.mkdir(destEntry, { recursive: true })\n await walk(srcRoot, destRoot, path.join(rel, entry.name), tokens, written)\n continue\n }\n\n const raw = await fs.readFile(srcEntry, 'utf8')\n const rendered = applyTokens(raw, tokens)\n await fs.mkdir(path.dirname(destEntry), { recursive: true })\n await fs.writeFile(destEntry, rendered, 'utf8')\n written.push(destRel)\n }\n}\n\n/**\n * Substitutes every `{{KEY}}` in `input` with the corresponding value\n * from `tokens`. Unknown keys are left untouched (so the user can spot\n * them visually in the generated output if a template author makes a\n * typo). Empty values are allowed.\n */\nexport function applyTokens(input: string, tokens: RenderTokens): string {\n const bag = tokens as unknown as Record<string, string>\n return input.replace(/\\{\\{(\\w+)\\}\\}/g, (match, key: string) => {\n const value = bag[key]\n return value === undefined ? match : value\n })\n}\n\n/**\n * Resolves the absolute path of a template directory shipped inside the\n * package. The package layout is:\n *\n * packages/create-noy-db/\n * dist/\n * wizard/render.js ← we are here at runtime (after tsup build)\n * templates/\n * nuxt-default/ ← we want to point at this\n *\n * In the published tarball the same relative path holds: `dist/` and\n * `templates/` are siblings under the package root. Computing the path\n * relative to `import.meta.url` lets us avoid hardcoding any assumption\n * about whether we're running from source, from `dist/`, or from a\n * globally installed copy.\n */\nexport function templateDir(name: string): string {\n // From `dist/wizard/render.js` we go up two levels to the package root,\n // then into `templates/<name>`.\n const here = fileURLToPath(import.meta.url)\n const packageRoot = path.resolve(path.dirname(here), '..', '..')\n return path.join(packageRoot, 'templates', name)\n}\n","/**\n * Detect whether the wizard is being invoked inside an existing\n * Nuxt 4 project that should be augmented in place, vs a blank\n * directory where the wizard should create a fresh starter.\n *\n * The detection rule is intentionally narrow so we don't trip on\n * random `package.json` files sitting in the parent tree: a project\n * counts as \"existing Nuxt 4\" only when the SAME directory has\n * BOTH `nuxt.config.ts` (or `.js`) AND a `package.json` that lists\n * `nuxt` in any of the dependency sections. Either alone is\n * ambiguous:\n *\n * - `nuxt.config.ts` without a package.json could be a stray\n * template scratch file the user moved here.\n * - `package.json` with `nuxt` but no config file could be a\n * half-deleted project or a pnpm workspace root that pulls\n * nuxt in transitively for docs.\n *\n * Requiring both avoids both false positives.\n *\n * We deliberately do NOT walk upward to find a parent Nuxt project.\n * The user's cwd is load-bearing — if they wanted to augment a\n * parent dir, they should cd there first. Walking up would make\n * \"run the wizard to create a fresh app inside my existing\n * monorepo\" silently augment the monorepo root instead.\n */\n\nimport { promises as fs } from 'node:fs'\nimport path from 'node:path'\n\nexport interface NuxtDetection {\n /** Whether cwd contains an existing Nuxt 4 project. */\n readonly existing: boolean\n /** Absolute path of the detected config file, if any. */\n readonly configPath: string | null\n /** Absolute path of the detected package.json, if any. */\n readonly packageJsonPath: string | null\n /** Reason strings explaining the detection outcome — useful for diagnostic messages. */\n readonly reasons: readonly string[]\n}\n\n/**\n * Inspect `cwd` for an existing Nuxt 4 project. Returns a detailed\n * result object so the caller can both branch on `existing` and\n * surface the specific reasons to the user.\n *\n * Pure in terms of filesystem reads — no writes, no network, no\n * caching. Callers who need cached detection should memoize on\n * their side.\n */\nexport async function detectNuxtProject(cwd: string): Promise<NuxtDetection> {\n const reasons: string[] = []\n\n // Step 1: look for a Nuxt config file. Both extensions are\n // valid; Nuxt itself accepts either.\n const configCandidates = ['nuxt.config.ts', 'nuxt.config.js', 'nuxt.config.mjs']\n let configPath: string | null = null\n for (const name of configCandidates) {\n const candidate = path.join(cwd, name)\n if (await pathExists(candidate)) {\n configPath = candidate\n reasons.push(`Found ${name}`)\n break\n }\n }\n if (!configPath) {\n reasons.push('No nuxt.config.{ts,js,mjs} in cwd')\n return {\n existing: false,\n configPath: null,\n packageJsonPath: null,\n reasons,\n }\n }\n\n // Step 2: look for a package.json in the same directory.\n const pkgPath = path.join(cwd, 'package.json')\n if (!(await pathExists(pkgPath))) {\n reasons.push('Config file present but no package.json — ambiguous, skipping')\n return {\n existing: false,\n configPath,\n packageJsonPath: null,\n reasons,\n }\n }\n\n // Step 3: verify `nuxt` is in one of the dependency sections.\n // We're lenient about WHICH section (dependencies, devDependencies,\n // peerDependencies) because real-world projects put it in all of\n // them depending on the tooling.\n let pkg: Record<string, unknown>\n try {\n pkg = JSON.parse(await fs.readFile(pkgPath, 'utf8')) as Record<string, unknown>\n } catch (err) {\n reasons.push(`package.json is not valid JSON: ${(err as Error).message}`)\n return {\n existing: false,\n configPath,\n packageJsonPath: pkgPath,\n reasons,\n }\n }\n\n const depSections = ['dependencies', 'devDependencies', 'peerDependencies'] as const\n let nuxtVersion: string | undefined\n for (const section of depSections) {\n const deps = pkg[section]\n if (deps && typeof deps === 'object' && 'nuxt' in deps) {\n nuxtVersion = (deps as Record<string, string>)['nuxt']\n reasons.push(`Found nuxt@${nuxtVersion} in ${section}`)\n break\n }\n }\n if (!nuxtVersion) {\n reasons.push('Config file present, but package.json does not list `nuxt` as a dependency')\n return {\n existing: false,\n configPath,\n packageJsonPath: pkgPath,\n reasons,\n }\n }\n\n return {\n existing: true,\n configPath,\n packageJsonPath: pkgPath,\n reasons,\n }\n}\n\n/** Cheap fs.access wrapper that returns a boolean instead of throwing. */\nasync function pathExists(target: string): Promise<boolean> {\n try {\n await fs.access(target)\n return true\n } catch {\n return false\n }\n}\n","/**\n * Augment an existing Nuxt 4 project with `@noy-db/in-nuxt`.\n *\n * The entry point is `augmentNuxtConfig()`, which reads a\n * `nuxt.config.ts` (or `.js` / `.mjs`) via magicast, mutates the\n * AST to:\n *\n * 1. Add `'@noy-db/in-nuxt'` to the `modules` array (creating the\n * array if it doesn't exist)\n * 2. Add the `noydb: { adapter, pinia: true, devtools: true }`\n * config key (creating it if it doesn't exist)\n *\n * then generates the new source code, computes a unified diff\n * against the original, and either writes the result (with\n * optional user confirmation) or returns the diff unchanged for\n * `--dry-run`.\n *\n * ## Idempotency\n *\n * Re-running the wizard on an already-augmented project is a\n * no-op: we check whether the target values are already present\n * before mutating, and if both are, we short-circuit before\n * calling `generateCode()`. The caller sees\n * `{ changed: false, reason: 'already configured' }` and can\n * exit cleanly.\n *\n * ## Edge cases\n *\n * - `export default defineNuxtConfig({...})` — the common case,\n * handled directly.\n * - `export default {...}` — plain object literal, also handled.\n * - `export default someVar` — opaque reference, we bail with a\n * clear error telling the user to edit manually. We don't try\n * to chase the variable.\n * - `modules` declared as an object instead of an array — rare\n * but possible in some Nuxt tooling. We bail.\n * - `modules` already contains `'@noy-db/in-nuxt'` — idempotent skip.\n * - `noydb` key already present — we preserve the existing value\n * UNLESS `force: true` is passed (not yet surfaced in the CLI;\n * reserved for a future `--overwrite-config` flag).\n *\n * ## Why magicast instead of a regex/string-patch\n *\n * Regex-based config patching produces correct-looking code 90%\n * of the time and silently wrong code the other 10%. Anything\n * non-trivial (nested options, comments, multi-line strings,\n * conditional exports) breaks. Magicast walks a real Babel AST,\n * preserves unrelated formatting, and round-trips the file\n * cleanly — including comments, trailing commas, and property\n * order.\n */\n\nimport { promises as fs } from 'node:fs'\nimport { loadFile, generateCode, builders } from 'magicast'\nimport { createPatch } from 'diff'\nimport type { WizardAdapter } from './types.js'\n\nexport interface AugmentOptions {\n /** Absolute path of the nuxt.config.{ts,js,mjs} to patch. */\n configPath: string\n /** Adapter string to write into `noydb: { adapter }`. */\n adapter: WizardAdapter\n /**\n * When true, skip the write and return the would-be result.\n * The CLI's `--dry-run` flag threads through to here.\n */\n dryRun?: boolean\n}\n\nexport type AugmentResult =\n | {\n readonly kind: 'already-configured'\n readonly configPath: string\n readonly reason: string\n }\n | {\n readonly kind: 'proposed-change'\n readonly configPath: string\n readonly originalCode: string\n readonly newCode: string\n /** Unified diff string suitable for printing to the terminal. */\n readonly diff: string\n /** `true` when `options.dryRun` was set — the caller skips the write. */\n readonly dryRun: boolean\n }\n | {\n readonly kind: 'unsupported-shape'\n readonly configPath: string\n readonly reason: string\n }\n\n/**\n * Parse, mutate, and compute the diff. Does NOT write the file —\n * that's the caller's responsibility, after (optionally) prompting\n * the user for confirmation.\n *\n * The split exists so the CLI can interleave the prompt between\n * \"show the diff\" and \"write the file\", and so tests can inspect\n * the proposed change without any filesystem side effect beyond\n * the initial read.\n */\nexport async function augmentNuxtConfig(\n options: AugmentOptions,\n): Promise<AugmentResult> {\n const originalCode = await fs.readFile(options.configPath, 'utf8')\n const mod = await loadFile(options.configPath)\n\n // The entry point is whatever `export default` points at. In a\n // Nuxt config this is either `defineNuxtConfig({...})` or a\n // plain object literal.\n const exported = mod.exports.default as unknown\n if (exported === undefined || exported === null) {\n return {\n kind: 'unsupported-shape',\n configPath: options.configPath,\n reason: `${options.configPath} has no default export. Expected \\`export default defineNuxtConfig({...})\\` or \\`export default {...}\\`.`,\n }\n }\n\n // Both the `defineNuxtConfig({...})` call and the plain-object\n // case expose the config as a proxified object. For the function\n // call, magicast's proxy makes `exported.$args[0]` the config\n // object; for a plain object it's `exported` itself. We test\n // both shapes and pick the first one that has `modules` or can\n // accept a `modules` write.\n //\n // This looks a bit like JavaScript duck-typing because it IS\n // duck-typing — magicast's proxy tolerates either shape and the\n // API surface is identical once you reach the config object.\n const config = resolveConfigObject(exported)\n if (!config) {\n return {\n kind: 'unsupported-shape',\n configPath: options.configPath,\n reason: `Could not find the config object in ${options.configPath}. ` +\n `Expected \\`export default defineNuxtConfig({ modules: [], ... })\\` or a plain object literal.`,\n }\n }\n\n const skipReasons: string[] = []\n\n // --- Modules array -------------------------------------------------\n // Ensure `modules` is an array; create it if missing. Magicast's\n // proxy handles the array-vs-missing distinction via typeof checks\n // on the array's length property.\n const modulesRaw: unknown = config.modules\n let modulesWasMissing = false\n if (modulesRaw === undefined) {\n modulesWasMissing = true\n config.modules = []\n } else if (typeof modulesRaw !== 'object' || !isProxyArray(modulesRaw)) {\n return {\n kind: 'unsupported-shape',\n configPath: options.configPath,\n reason: `\\`modules\\` in ${options.configPath} is not an array literal. ` +\n `Edit it manually and re-run the wizard if you want to continue.`,\n }\n }\n\n // Push '@noy-db/in-nuxt' if it's not already listed. We compare by\n // stringification because magicast's proxy yields primitive\n // strings for literal entries.\n const modules = config.modules as string[]\n const alreadyHasModule = Array.from(modules).some((m) => String(m) === '@noy-db/in-nuxt')\n if (alreadyHasModule) {\n skipReasons.push('`@noy-db/in-nuxt` already in modules')\n } else {\n // Insert as a literal string so magicast writes `'@noy-db/in-nuxt'`\n // rather than an object wrapper.\n modules.push('@noy-db/in-nuxt')\n }\n\n // --- `noydb` config key --------------------------------------------\n // If the user already has a noydb key, preserve it — they might\n // have custom options we don't want to clobber. If it doesn't\n // exist, add it with the adapter the wizard collected.\n const noydbRaw: unknown = config.noydb\n if (noydbRaw !== undefined) {\n skipReasons.push('`noydb` key already set')\n } else {\n // `builders.raw` parses the literal into an AST node, so the\n // output is a real object expression in the generated code\n // instead of a stringified opaque blob.\n config.noydb = builders.raw(\n `{ adapter: '${options.adapter}', pinia: true, devtools: true }`,\n )\n }\n\n // If nothing changed, short-circuit. `modulesWasMissing` is a\n // change even if we didn't push anything, because we wrote an\n // empty array into the config — but that's an unusual case so\n // we lump it in with the regular \"changed\" path.\n if (skipReasons.length === 2 && !modulesWasMissing) {\n return {\n kind: 'already-configured',\n configPath: options.configPath,\n reason: skipReasons.join('; '),\n }\n }\n\n // Generate the new source, then compute a unified diff against\n // the original. `createPatch` is from the `diff` package — same\n // one `jest-diff` and `vitest` use under the hood. The empty\n // header strings keep the diff output compact for terminal\n // display; a full unified diff header isn't needed since we're\n // going to print the diff inline, not pipe it to `patch -p1`.\n const generated = generateCode(mod).code\n const diff = createPatch(\n options.configPath,\n originalCode,\n generated,\n '',\n '',\n { context: 3 },\n )\n\n return {\n kind: 'proposed-change',\n configPath: options.configPath,\n originalCode,\n newCode: generated,\n diff,\n dryRun: options.dryRun === true,\n }\n}\n\n/**\n * Write the augmented config to disk. Call this after the user\n * has seen and approved the diff from `augmentNuxtConfig()`. The\n * split between \"compute\" and \"write\" is deliberate — see the\n * module docstring.\n */\nexport async function writeAugmentedConfig(\n configPath: string,\n newCode: string,\n): Promise<void> {\n await fs.writeFile(configPath, newCode, 'utf8')\n}\n\n// ─── Internal helpers ─────────────────────────────────────────────────\n\n/**\n * Given the proxified default export, return the config object\n * itself. Handles both `defineNuxtConfig({...})` (proxified function\n * call whose `$args[0]` is the config) and `{...}` (plain object\n * proxy). Returns null for shapes we don't recognize.\n *\n * Important: magicast's `$args` is a PROXY, not a real array, so\n * `Array.isArray($args)` returns false. We access `$args[0]`\n * directly and check whether the result is a usable object.\n */\nfunction resolveConfigObject(\n exported: unknown,\n): Record<string, unknown> | null {\n if (!exported || typeof exported !== 'object') return null\n const proxy = exported as Record<string, unknown> & {\n $type?: string\n $args?: Record<number, unknown>\n }\n\n // defineNuxtConfig(...) call — magicast exposes the args through\n // a proxied $args accessor. The config literal is at $args[0].\n if (proxy.$type === 'function-call' && proxy.$args) {\n const firstArg = proxy.$args[0]\n if (firstArg && typeof firstArg === 'object') {\n return firstArg as Record<string, unknown>\n }\n return null\n }\n\n // Plain object literal export — the proxy IS the config.\n if (proxy.$type === 'object' || proxy.$type === undefined) {\n return proxy\n }\n\n return null\n}\n\n/**\n * Check whether a magicast-proxied value is an array. The proxy\n * reports `$type === 'array'` for array literals; plain objects\n * and strings come back with different tags.\n */\nfunction isProxyArray(value: unknown): boolean {\n if (!value || typeof value !== 'object') return false\n const proxy = value as { $type?: string }\n return proxy.$type === 'array'\n}\n","/**\n * English (`en`) wizard messages — the default locale.\n *\n * Editing note: if you change a key's value here, update the\n * matching key in every other locale bundle (`th.ts`, future\n * additions). The key-parity test in `__tests__/i18n.test.ts`\n * is the safety net: it fails if any locale is missing a key\n * or has an extra one, so a forgotten update surfaces in CI\n * rather than in a user report.\n */\n\nimport type { WizardMessages } from './types.js'\n\nexport const en: WizardMessages = {\n wizardIntro:\n 'A wizard for noy-db — None Of Your DataBase.\\n' +\n 'Generates a fresh Nuxt 4 + Pinia + encrypted-store starter.',\n\n promptProjectName: 'Project name',\n promptProjectNamePlaceholder: 'my-noy-db-app',\n promptAdapter: 'Storage adapter',\n adapterBrowserLabel:\n 'browser — localStorage / IndexedDB (recommended for web apps)',\n adapterFileLabel:\n 'file — JSON files on disk (Electron / Tauri / USB workflows)',\n adapterMemoryLabel:\n 'memory — no persistence (ideal for tests and demos)',\n promptSampleData: 'Include sample invoice records?',\n\n freshNextStepsTitle: 'Next steps',\n freshOutroDone: '✔ Done — happy encrypting!',\n\n augmentModeTitle: 'Augment mode',\n augmentDetectedPrefix: 'Detected existing Nuxt 4 project:',\n augmentDescription:\n 'The wizard will add @noy-db/in-nuxt to your modules array\\n' +\n 'and a noydb: config key. You can review the diff before\\n' +\n 'anything is written to disk.',\n\n augmentProposedChangesTitle: 'Proposed changes',\n augmentApplyConfirm: 'Apply these changes?',\n\n augmentAlreadyConfiguredTitle: 'Already configured',\n augmentNothingToDo: 'Nothing to do:',\n augmentAlreadyOutro: '✔ Your Nuxt config is already wired up.',\n augmentAborted: 'Aborted — your config is unchanged.',\n augmentDryRunOutro: '✔ Dry run — no files were modified.',\n augmentNextStepTitle: 'Next step',\n augmentInstallIntro:\n 'Install the @noy-db packages your config now depends on:',\n augmentInstallPmHint: '(or use npm/yarn/bun as appropriate)',\n augmentDoneOutro: '✔ Config updated — happy encrypting!',\n augmentUnsupportedPrefix: 'Cannot safely patch this config:',\n\n cancelled: 'Cancelled.',\n}\n","/**\n * Thai (`th`) wizard messages.\n *\n * Editing note: this bundle MUST stay in key-parity with `en.ts`.\n * The key-parity test in `__tests__/i18n.test.ts` fails if any key\n * is missing or extra here, so a forgotten translation surfaces in\n * CI rather than as a runtime crash for a Thai-speaking user.\n *\n * Translation conventions:\n * - Technical identifiers (`modules`, `noydb:`, `nuxt.config.ts`,\n * `Nuxt 4`, `Pinia`, adapter names like `browser`/`file`/`memory`)\n * stay in English. This matches how Thai developers actually\n * read and write code — translating them would just add a\n * mental round-trip.\n * - Product name \"noy-db\" and the \"None Of Your DataBase\" tagline\n * also stay in English; they're brand strings, not prose.\n * - Validation/error strings are NOT in this file — they stay in\n * English so bug reports look the same in any locale (see the\n * docstring on `WizardMessages` in `types.ts` for the rationale).\n */\n\nimport type { WizardMessages } from './types.js'\n\nexport const th: WizardMessages = {\n wizardIntro:\n 'ตัวช่วยสร้างสำหรับ noy-db — None Of Your DataBase\\n' +\n 'สร้างโปรเจกต์เริ่มต้น Nuxt 4 + Pinia พร้อมที่เก็บข้อมูลแบบเข้ารหัส',\n\n promptProjectName: 'ชื่อโปรเจกต์',\n promptProjectNamePlaceholder: 'my-noy-db-app',\n promptAdapter: 'อะแดปเตอร์จัดเก็บข้อมูล',\n adapterBrowserLabel:\n 'browser — localStorage / IndexedDB (แนะนำสำหรับเว็บแอป)',\n adapterFileLabel:\n 'file — ไฟล์ JSON บนดิสก์ (Electron / Tauri / USB)',\n adapterMemoryLabel:\n 'memory — ไม่บันทึกข้อมูล (เหมาะสำหรับการทดสอบและตัวอย่าง)',\n promptSampleData: 'เพิ่มข้อมูลตัวอย่างใบแจ้งหนี้หรือไม่?',\n\n freshNextStepsTitle: 'ขั้นตอนถัดไป',\n freshOutroDone: '✔ เสร็จเรียบร้อย — ขอให้สนุกกับการเข้ารหัส!',\n\n augmentModeTitle: 'โหมดเสริมโปรเจกต์เดิม',\n augmentDetectedPrefix: 'พบโปรเจกต์ Nuxt 4 ที่มีอยู่แล้ว:',\n augmentDescription:\n 'ตัวช่วยจะเพิ่ม @noy-db/in-nuxt เข้าใน modules\\n' +\n 'และเพิ่มคีย์ noydb: ในไฟล์ config คุณสามารถดู diff\\n' +\n 'ก่อนที่จะเขียนไฟล์ลงดิสก์ได้',\n\n augmentProposedChangesTitle: 'รายการเปลี่ยนแปลงที่จะทำ',\n augmentApplyConfirm: 'ยืนยันการเปลี่ยนแปลงเหล่านี้?',\n\n augmentAlreadyConfiguredTitle: 'ตั้งค่าไว้แล้ว',\n augmentNothingToDo: 'ไม่มีอะไรต้องทำ:',\n augmentAlreadyOutro: '✔ ไฟล์ Nuxt config ของคุณตั้งค่าครบแล้ว',\n augmentAborted: 'ยกเลิก — ไฟล์ config ของคุณไม่ถูกแก้ไข',\n augmentDryRunOutro: '✔ Dry run — ไม่มีไฟล์ใดถูกแก้ไข',\n augmentNextStepTitle: 'ขั้นตอนถัดไป',\n augmentInstallIntro:\n 'ติดตั้งแพ็กเกจ @noy-db ที่ config ของคุณต้องใช้:',\n augmentInstallPmHint: '(หรือใช้ npm/yarn/bun ตามความเหมาะสม)',\n augmentDoneOutro: '✔ อัปเดต config เรียบร้อย — ขอให้สนุกกับการเข้ารหัส!',\n augmentUnsupportedPrefix: 'ไม่สามารถแก้ไข config นี้ได้อย่างปลอดภัย:',\n\n cancelled: 'ยกเลิกแล้ว',\n}\n","/**\n * i18n entrypoint for the `@noy-db/create` wizard.\n *\n * Three responsibilities:\n *\n * 1. Re-export `Locale` and `WizardMessages` so callers don't\n * need to know about the bundle layout.\n * 2. `detectLocale(env)` — pure function that maps Unix-style\n * `LC_ALL` / `LANG` / `LANGUAGE` env vars to a supported\n * `Locale`. Returns `'en'` for anything we don't recognise.\n * 3. `loadMessages(locale)` — synchronous lookup that returns\n * the message bundle for a locale. Synchronous (not dynamic\n * `import()`) on purpose: bundles are tiny (< 2 KB each), the\n * wizard reads them on every prompt, and async would force\n * every caller to be async. tsup tree-shakes unused locales\n * out of the bin only if we use top-level `import`s.\n *\n * ## Why env-var detection instead of `Intl.DateTimeFormat().resolvedOptions().locale`\n *\n * The Intl approach reads the JS engine's *display* locale, which\n * on most CI runners and Docker images is `en-US` regardless of\n * the user's actual setup. The Unix env vars (`LC_ALL`, `LANG`)\n * are how shells, terminals, and CLI tools have negotiated locale\n * for 30+ years — that's what a Thai-speaking dev's terminal will\n * actually have set. Following that convention also means power\n * users can override per-invocation with `LANG=th_TH.UTF-8 npm\n * create @noy-db`, no flag required.\n */\n\nimport { en } from './en.js'\nimport { th } from './th.js'\nimport type { Locale, WizardMessages } from './types.js'\n\nexport type { Locale, WizardMessages } from './types.js'\n\nconst BUNDLES: Record<Locale, WizardMessages> = { en, th }\n\n/** Every locale we ship a bundle for. Used by tests and `--lang` validation. */\nexport const SUPPORTED_LOCALES: readonly Locale[] = ['en', 'th'] as const\n\n/**\n * Resolve a locale code to its message bundle. Falls back to `en`\n * if the requested locale isn't shipped — defensive, since\n * `Locale` is a union type and TS already prevents this at compile\n * time, but `--lang` parsing comes from user input at runtime.\n */\nexport function loadMessages(locale: Locale): WizardMessages {\n return BUNDLES[locale] ?? BUNDLES.en\n}\n\n/**\n * Auto-detect a locale from POSIX env vars. Returns `'en'` when\n * nothing is set or when the value doesn't match a supported\n * locale — never throws.\n *\n * Inspection order matches the POSIX spec:\n * 1. `LC_ALL` (overrides everything)\n * 2. `LC_MESSAGES` (the category we actually care about)\n * 3. `LANG` (system default)\n * 4. `LANGUAGE` (GNU extension, comma-separated preference list)\n *\n * The first non-empty value wins. We then strip the encoding\n * suffix (`th_TH.UTF-8` → `th_TH`) and the region (`th_TH` → `th`)\n * before matching against `SUPPORTED_LOCALES`.\n */\nexport function detectLocale(env: NodeJS.ProcessEnv = process.env): Locale {\n const candidates = [\n env.LC_ALL,\n env.LC_MESSAGES,\n env.LANG,\n // LANGUAGE is a comma-separated preference list — take the first\n // entry. We deliberately do NOT walk the whole list; the wizard\n // ships exactly two locales, so a \"best fit\" walk would be\n // overkill and would obscure unexpected behaviour.\n env.LANGUAGE?.split(':')[0]?.split(',')[0],\n ]\n\n for (const raw of candidates) {\n if (!raw) continue\n const normalised = raw\n .split('.')[0]! // strip encoding: th_TH.UTF-8 → th_TH\n .split('@')[0]! // strip modifier: en_US@euro → en_US\n .toLowerCase()\n .split('_')[0]! // strip region: th_th → th\n\n if ((SUPPORTED_LOCALES as readonly string[]).includes(normalised)) {\n return normalised as Locale\n }\n }\n\n return 'en'\n}\n\n/**\n * Parse a `--lang` CLI argument into a `Locale`. Throws a clear\n * error for unsupported values — the caller (parse-args) catches\n * and reformats into a usage message.\n */\nexport function parseLocaleFlag(value: string): Locale {\n const normalised = value.toLowerCase().trim()\n if ((SUPPORTED_LOCALES as readonly string[]).includes(normalised)) {\n return normalised as Locale\n }\n throw new Error(\n `Unsupported --lang value: \"${value}\". Supported: ${SUPPORTED_LOCALES.join(', ')}`,\n )\n}\n","/**\n * `noy-db add <collection>` — scaffold a new collection inside an existing\n * Nuxt 4 project that already has `@noy-db/in-nuxt` configured.\n *\n * The command writes two files:\n *\n * 1. `app/stores/<collection>.ts` — a `defineNoydbStore<T>()` call with\n * a placeholder `T` interface and one example field. The user fills\n * in the real shape after the file is created.\n *\n * 2. `app/pages/<collection>.vue` — a minimal CRUD page that lists,\n * adds, and deletes records. The store ID and collection name are\n * derived from the argument; everything else is boilerplate.\n *\n * The command refuses to overwrite existing files. If either target\n * already exists it logs which one and exits non-zero — the user has to\n * delete or move the file first. There's no `--force` because forcing an\n * overwrite of generated UI code is almost always a footgun in disguise.\n */\n\nimport { promises as fs } from 'node:fs'\nimport path from 'node:path'\n\nexport interface AddCollectionOptions {\n /** The collection name. Must be a lowercase identifier. */\n name: string\n /** Project root. Defaults to `process.cwd()`. */\n cwd?: string\n /** Compartment id to embed in the generated store. Defaults to `default`. */\n vault?: string\n}\n\n/**\n * Result returned to callers (the bin entry uses this to format output;\n * tests assert on the file paths).\n */\nexport interface AddCollectionResult {\n /** Files written, in the order they were created. */\n files: string[]\n}\n\n/**\n * Lowercase identifier check, narrower than the project-name check —\n * collection names become Vue component names, Pinia store IDs, AND TS\n * symbols, so we keep them simple: letters, digits, hyphens, must start\n * with a letter.\n */\nexport function validateCollectionName(name: string): string | null {\n if (!name) return 'Collection name is required'\n if (!/^[a-z][a-z0-9-]*$/.test(name)) {\n return 'Collection name must start with a lowercase letter and contain only lowercase letters, digits, or hyphens'\n }\n return null\n}\n\nexport async function addCollection(\n options: AddCollectionOptions,\n): Promise<AddCollectionResult> {\n const err = validateCollectionName(options.name)\n if (err) throw new Error(err)\n\n const cwd = options.cwd ?? process.cwd()\n const vault = options.vault ?? 'default'\n const name = options.name\n\n // Convert kebab-case to PascalCase for the TS interface name and the\n // Vue helper variable.\n const PascalName = name\n .split('-')\n .map((s) => (s.length === 0 ? s : (s[0]?.toUpperCase() ?? '') + s.slice(1)))\n .join('')\n const useFnName = `use${PascalName}`\n\n const storePath = path.join(cwd, 'app', 'stores', `${name}.ts`)\n const pagePath = path.join(cwd, 'app', 'pages', `${name}.vue`)\n\n // Refuse to overwrite. Doing both checks before writing either file\n // means a partial write is impossible — either both files land or\n // neither does.\n for (const target of [storePath, pagePath]) {\n if (await pathExists(target)) {\n throw new Error(`Refusing to overwrite existing file: ${target}`)\n }\n }\n\n await fs.mkdir(path.dirname(storePath), { recursive: true })\n await fs.mkdir(path.dirname(pagePath), { recursive: true })\n\n await fs.writeFile(storePath, renderStore(name, PascalName, useFnName, vault), 'utf8')\n await fs.writeFile(pagePath, renderPage(name, PascalName, useFnName), 'utf8')\n\n return { files: [storePath, pagePath] }\n}\n\nasync function pathExists(p: string): Promise<boolean> {\n try {\n await fs.access(p)\n return true\n } catch {\n return false\n }\n}\n\n/**\n * Renders the `app/stores/<name>.ts` file. The interface is intentionally\n * minimal — `id` and `name` are the only fields. The user is expected to\n * extend it after generation; we'd rather not lock them into a domain\n * shape we guessed.\n */\nfunction renderStore(\n name: string,\n PascalName: string,\n useFnName: string,\n vault: string,\n): string {\n return `// Generated by \\`noy-db add ${name}\\`.\n//\n// Edit the ${PascalName} interface to match your domain, then call\n// \\`${useFnName}()\\` from any component.\n//\n// defineNoydbStore is auto-imported by @noy-db/in-nuxt. The vault\n// id is the tenant/company namespace — change it if you have multiple.\n\nexport interface ${PascalName} {\n id: string\n name: string\n}\n\nexport const ${useFnName} = defineNoydbStore<${PascalName}>('${name}', {\n vault: '${vault}',\n})\n`\n}\n\n/**\n * Renders the `app/pages/<name>.vue` page — list + add + delete. Picks\n * the simplest possible template-renderer pattern (`v-for`, no fancy\n * components) so the generated file is easy to read and modify.\n */\nfunction renderPage(name: string, PascalName: string, useFnName: string): string {\n return `<!--\n Generated by \\`noy-db add ${name}\\`.\n Visit /${name} in your dev server.\n-->\n<script setup lang=\"ts\">\nconst ${name} = ${useFnName}()\nawait ${name}.$ready\n\nfunction addOne() {\n ${name}.add({\n id: crypto.randomUUID(),\n name: 'New ${PascalName}',\n })\n}\n\nfunction removeOne(id: string) {\n ${name}.remove(id)\n}\n</script>\n\n<template>\n <main>\n <h1>${PascalName}</h1>\n <button @click=\"addOne\">Add ${PascalName}</button>\n <ul>\n <li v-for=\"item in ${name}.items\" :key=\"item.id\">\n {{ item.name }}\n <button @click=\"removeOne(item.id)\">Delete</button>\n </li>\n </ul>\n </main>\n</template>\n`\n}\n","/**\n * `noy-db verify` — end-to-end integrity check.\n *\n * Opens an in-memory NOYDB instance, writes a record, reads it back,\n * decrypts it, and asserts the round-trip is byte-identical. The check\n * exercises the full crypto path (PBKDF2 → KEK → DEK → AES-GCM) without\n * touching any user data on disk.\n *\n * Why an in-memory check is the right scope:\n * - It validates that @noy-db/core, @noy-db/memory, and the user's\n * installed Node version all agree on Web Crypto. That's the most\n * common silent failure for first-time installers.\n * - It cannot accidentally corrupt user data because there isn't any.\n * - It runs in well under one second, so users actually run it.\n *\n * What this command does NOT do (intentionally):\n * - Open the user's actual vault file/dynamo/s3/browser store.\n * That requires the user's passphrase — not something we want a CLI\n * `verify` command to prompt for. The full passphrase-driven verify\n * belongs in `nuxi noydb verify` once the auth story for CLIs lands\n * in. For now `noy-db verify` is the dependency-graph smoke test.\n */\n\nimport { createNoydb } from '@noy-db/hub'\nimport { memory } from '@noy-db/to-memory'\n\nexport interface VerifyResult {\n /** `true` if the round-trip succeeded; `false` if anything diverged. */\n ok: boolean\n /** Human-readable status. Always set, even on success. */\n message: string\n /** Wall-clock time the integrity check took, in ms. */\n durationMs: number\n}\n\n/**\n * Runs the end-to-end check. Pure function — no console output, no\n * `process.exit`. The bin wrapper handles formatting and exit codes so\n * the function is trivial to call from tests.\n */\nexport async function verifyIntegrity(): Promise<VerifyResult> {\n const start = performance.now()\n try {\n const db = await createNoydb({\n store: memory(),\n user: 'noy-db-verify',\n // The passphrase here is throwaway — the in-memory adapter never\n // persists anything, and the KEK is destroyed when we call close()\n // a few lines down. We use a non-trivial value just to exercise\n // PBKDF2 properly.\n secret: 'noy-db-verify-passphrase-2026',\n })\n const company = await db.openVault('verify-co')\n const collection = company.collection<{ id: string; n: number }>('verify')\n\n // Round-trip a single record. We pick a value that's small enough\n // to print on failure but large enough to ensure encryption isn't\n // accidentally a no-op.\n const original = { id: 'verify-1', n: 42 }\n await collection.put('verify-1', original)\n const got = await collection.get('verify-1')\n if (!got || got.id !== original.id || got.n !== original.n) {\n return fail(start, `Round-trip mismatch: got ${JSON.stringify(got)}`)\n }\n\n // Make sure the query DSL works too — this catches the case where\n // the user's @noy-db/core install is at (no query DSL) but the\n // CLI was updated to.\n const found = collection.query().where('n', '==', 42).toArray()\n if (found.length !== 1) {\n return fail(start, `Query DSL mismatch: expected 1 result, got ${found.length}`)\n }\n\n db.close()\n\n return {\n ok: true,\n message: 'noy-db integrity check passed',\n durationMs: Math.round(performance.now() - start),\n }\n } catch (err) {\n return fail(start, `Integrity check threw: ${(err as Error).message}`)\n }\n}\n\nfunction fail(start: number, message: string): VerifyResult {\n return {\n ok: false,\n message,\n durationMs: Math.round(performance.now() - start),\n }\n}\n","/**\n * `noy-db rotate` — rotate the DEKs for one or more collections in\n * a vault.\n *\n * What it does\n * ------------\n * For each target collection:\n *\n * 1. Generate a fresh DEK\n * 2. Decrypt every record with the old DEK\n * 3. Re-encrypt every record with the new DEK\n * 4. Re-wrap the new DEK into every remaining user's keyring\n *\n * The old DEKs become unreachable as soon as the keyring files are\n * updated. This is the \"just rotate\" path — nobody is revoked,\n * everybody keeps their current permissions, but the key material\n * is replaced.\n *\n * Why expose this as a CLI command\n * --------------------------------\n * Two real-world scenarios:\n *\n * 1. **Suspected key leak.** An operator lost a laptop, a\n * developer accidentally pasted a passphrase into a Slack\n * channel, a USB stick went missing. Even if you think the\n * passphrase is safe, rotating is cheap insurance.\n *\n * 2. **Scheduled rotation.** Some compliance regimes require\n * periodic key rotation regardless of exposure. A CLI makes\n * this scriptable from cron or a CI job.\n *\n * This module is test-first: all inputs are plain options, the\n * passphrase reader is injected, and the Noydb factory is\n * injectable. The production bin is a thin wrapper that defaults\n * those injections to their real implementations.\n */\n\nimport { createNoydb, type Noydb, type NoydbStore } from '@noy-db/hub'\nimport { jsonFile } from '@noy-db/to-file'\nimport type { ReadPassphrase } from './shared.js'\nimport { defaultReadPassphrase } from './shared.js'\n\nexport interface RotateOptions {\n /** Directory containing the vault data (file adapter only). */\n dir: string\n /** Vault (tenant) name to rotate keys in. */\n vault: string\n /** The user id of the operator running the rotate. */\n user: string\n /**\n * Explicit list of collections to rotate. When undefined, the\n * rotation targets every collection the user has a DEK for —\n * resolved at run time by reading the vault snapshot.\n */\n collections?: string[]\n /** Injected passphrase reader. Defaults to the clack implementation. */\n readPassphrase?: ReadPassphrase\n /**\n * Injected Noydb factory. Production code leaves this undefined\n * and gets `createNoydb`; tests pass a constructor that builds\n * against an in-memory adapter.\n */\n createDb?: typeof createNoydb\n /**\n * Injected adapter factory. Production code leaves this undefined\n * and gets `jsonFile`; tests pass one that returns the shared\n * in-memory adapter their fixture used.\n */\n buildAdapter?: (dir: string) => NoydbStore\n}\n\nexport interface RotateResult {\n /** The collections that were actually rotated. */\n rotated: string[]\n}\n\n/**\n * Run the rotate flow against a file-adapter vault. Returns\n * the list of collections that were rotated so callers can display\n * it to the user.\n *\n * Throws `Error` on any auth/adapter/rotate failure. The bin\n * catches these and prints a friendly message; direct callers\n * (tests) can inspect the error message to assert specific\n * failure modes.\n */\nexport async function rotate(options: RotateOptions): Promise<RotateResult> {\n const readPassphrase = options.readPassphrase ?? defaultReadPassphrase\n const buildAdapter = options.buildAdapter ?? ((dir) => jsonFile({ dir }))\n const createDb = options.createDb ?? createNoydb\n\n // Read the passphrase BEFORE opening the database. This way a\n // cancelled prompt (Ctrl-C at the password entry) leaves the\n // adapter completely untouched — no files opened, no locks held.\n const secret = await readPassphrase(`Passphrase for ${options.user}`)\n\n let db: Noydb | null = null\n try {\n db = await createDb({\n store: buildAdapter(options.dir),\n user: options.user,\n secret,\n })\n\n // Resolve \"all collections\" by asking the vault. This\n // happens BEFORE rotate() is called, so the list is stable\n // across the operation — adding a new collection mid-rotate\n // would be a race we're not guarding against (single-writer\n // assumption applies).\n const vault = await db.openVault(options.vault)\n const targets = options.collections && options.collections.length > 0\n ? options.collections\n : await vault.collections()\n\n if (targets.length === 0) {\n throw new Error(\n `Vault \"${options.vault}\" has no collections to rotate.`,\n )\n }\n\n await db.rotate(options.vault, targets)\n return { rotated: targets }\n } finally {\n // Always close the DB on exit — success or failure. Close()\n // clears the KEK and DEKs from process memory, which is the\n // final line of defense if the passphrase somehow leaked\n // into a log line above this block.\n db?.close()\n }\n}\n","/**\n * Shared primitives for the interactive `noy-db` subcommands that\n * need to unlock a real vault.\n *\n * Three things live here:\n *\n * 1. `ReadPassphrase` — a tiny interface for \"prompt the user for\n * a passphrase\", with a test-friendly default. Subcommands take\n * this as an injected dependency so tests can short-circuit\n * the prompt without spawning a pty.\n *\n * 2. `defaultReadPassphrase` — the production implementation,\n * built on `@clack/prompts` `password()`. Never echoes the\n * value to the terminal, never logs it, clears it from the\n * returned promise after the caller consumes it.\n *\n * 3. `assertRole` — narrow unknown string input to the Role type\n * with a consistent error message.\n *\n * ## Why pull this out\n *\n * `rotate`, `addUser`, and `backup` all need the same \"prompt for\n * a passphrase\" shape and the same \"open a file adapter and get\n * back a Noydb instance\" shape. Duplicating it in three files would\n * drift over time; centralizing means one place to audit the\n * passphrase-handling contract (never log, never persist, clear\n * local variables after use).\n */\n\nimport { password, isCancel, cancel } from '@clack/prompts'\nimport type { Role } from '@noy-db/hub'\n\nconst VALID_ROLES = ['owner', 'admin', 'operator', 'viewer', 'client'] as const\n\n/**\n * Asynchronous passphrase reader. Production code passes\n * `defaultReadPassphrase`; tests pass a stub that returns a fixed\n * string without touching stdin.\n *\n * The `label` is shown to the user as the prompt message. It\n * should never contain the expected passphrase or any secret.\n */\nexport type ReadPassphrase = (label: string) => Promise<string>\n\n/**\n * Clack-based passphrase prompt. Cancellation (Ctrl-C) aborts the\n * process with exit code 1 — prompts are always the first thing to\n * fire in a subcommand, so aborting here doesn't leave the system\n * in a half-mutated state.\n */\nexport const defaultReadPassphrase: ReadPassphrase = async (label) => {\n const value = await password({\n message: label,\n // Basic sanity: reject empty strings up front. We don't enforce\n // length here because the caller's KEK-derivation step will\n // reject weak passphrases with its own, richer error.\n validate: (v) => (v.length === 0 ? 'Passphrase cannot be empty' : undefined),\n })\n if (isCancel(value)) {\n cancel('Cancelled.')\n process.exit(1)\n }\n return value\n}\n\n/**\n * Narrow an unknown string to the `Role` type from @noy-db/core.\n * Used by the `add user` subcommand to validate the role argument\n * before passing it to `noydb.grant()`.\n */\nexport function assertRole(input: string): Role {\n if (!(VALID_ROLES as readonly string[]).includes(input)) {\n throw new Error(\n `Invalid role \"${input}\" — must be one of: ${VALID_ROLES.join(', ')}`,\n )\n }\n return input as Role\n}\n\n/**\n * Split a comma-separated collection list into an array of names,\n * trimming whitespace and dropping empties. Returns null if the\n * input itself is empty or undefined — the caller decides whether\n * that means \"all collections\" or \"error\".\n */\nexport function parseCollectionList(input: string | undefined): string[] | null {\n if (!input) return null\n const parts = input\n .split(',')\n .map((s) => s.trim())\n .filter((s) => s.length > 0)\n return parts.length > 0 ? parts : null\n}\n","/**\n * `noy-db add user <id> <role>` — grant a new user access to a\n * vault.\n *\n * What it does\n * ------------\n * Wraps `noydb.grant()` in the CLI's auth-prompt ritual:\n *\n * 1. Prompt the caller for their own passphrase (to unlock the\n * caller's keyring and derive the wrapping key).\n * 2. Prompt for the new user's passphrase.\n * 3. Prompt for confirmation of the new passphrase.\n * 4. Reject on mismatch.\n * 5. Call `noydb.grant(vault, { userId, role, passphrase, permissions })`.\n *\n * For owner/admin/viewer roles, every collection is granted\n * automatically (the core keyring.ts grant logic handles that via\n * the `permissions` field). For operator/client, the caller must\n * pass a `--collections` list because those roles need explicit\n * per-collection permissions.\n *\n * ## What this does NOT do\n *\n * - No email/invite flow — is about local-CLI key management,\n * not out-of-band user enrollment.\n * - No rollback on partial failure — `grant()` is atomic at the\n * core level (keyring file writes last, after DEK wrapping), so\n * partial-state-on-crash is already handled.\n */\n\nimport { createNoydb, type Noydb, type NoydbStore, type Role } from '@noy-db/hub'\nimport { jsonFile } from '@noy-db/to-file'\nimport type { ReadPassphrase } from './shared.js'\nimport { defaultReadPassphrase } from './shared.js'\n\nexport interface AddUserOptions {\n /** Directory containing the vault data (file adapter only). */\n dir: string\n /** Vault (tenant) name to grant access to. */\n vault: string\n /** The user id of the caller running the grant. */\n callerUser: string\n /** The new user's id (must not already exist in the vault keyring). */\n newUserId: string\n /** The new user's display name — shown in UI and audit logs. Defaults to `newUserId`. */\n newUserDisplayName?: string\n /** The new user's role. */\n role: Role\n /**\n * Per-collection permissions. Required when `role` is operator or\n * client; ignored for owner/admin/viewer (they get everything\n * via the core's resolvePermissions logic).\n *\n * Shape: `{ invoices: 'rw', clients: 'ro' }`. CLI callers pass\n * `--collections invoices:rw,clients:ro` and the argv parser\n * converts it to this shape.\n */\n permissions?: Record<string, 'rw' | 'ro'>\n /** Injected passphrase reader. Defaults to the clack implementation. */\n readPassphrase?: ReadPassphrase\n /** Injected Noydb factory. */\n createDb?: typeof createNoydb\n /** Injected adapter factory. */\n buildAdapter?: (dir: string) => NoydbStore\n}\n\nexport interface AddUserResult {\n /** The userId that was granted access. */\n userId: string\n /** The role they were granted. */\n role: Role\n}\n\n/**\n * Run the grant flow. Two passphrase prompts: caller's, then new\n * user's (twice for confirmation). Calls `noydb.grant()` with the\n * collected values.\n */\nexport async function addUser(options: AddUserOptions): Promise<AddUserResult> {\n const readPassphrase = options.readPassphrase ?? defaultReadPassphrase\n const buildAdapter = options.buildAdapter ?? ((dir) => jsonFile({ dir }))\n const createDb = options.createDb ?? createNoydb\n\n // Operator/client roles NEED explicit permissions. Reject here\n // rather than in the middle of the grant, so the caller sees the\n // problem before any I/O happens.\n if (\n (options.role === 'operator' || options.role === 'client') &&\n (!options.permissions || Object.keys(options.permissions).length === 0)\n ) {\n throw new Error(\n `Role \"${options.role}\" requires explicit --collections — e.g. --collections invoices:rw,clients:ro`,\n )\n }\n\n const callerSecret = await readPassphrase(\n `Your passphrase (${options.callerUser})`,\n )\n const newSecret = await readPassphrase(\n `New passphrase for ${options.newUserId}`,\n )\n const confirmSecret = await readPassphrase(\n `Confirm passphrase for ${options.newUserId}`,\n )\n\n if (newSecret !== confirmSecret) {\n throw new Error(`Passphrases do not match — grant aborted.`)\n }\n\n let db: Noydb | null = null\n try {\n db = await createDb({\n store: buildAdapter(options.dir),\n user: options.callerUser,\n secret: callerSecret,\n })\n\n // Build the grant options. Only include `permissions` when the\n // caller actually supplied them — otherwise the core's\n // resolvePermissions fills in the role defaults. The spread\n // (rather than post-assignment) keeps the object literal\n // compatible with `GrantOptions`'s readonly `permissions`.\n const grantOpts: Parameters<Noydb['grant']>[1] = {\n userId: options.newUserId,\n displayName: options.newUserDisplayName ?? options.newUserId,\n role: options.role,\n passphrase: newSecret,\n ...(options.permissions ? { permissions: options.permissions } : {}),\n }\n\n await db.grant(options.vault, grantOpts)\n\n return {\n userId: options.newUserId,\n role: options.role,\n }\n } finally {\n db?.close()\n }\n}\n","/**\n * `noy-db backup <target>` — dump a vault to a local file.\n *\n * What it does\n * ------------\n * Wraps `vault.dump()` in the CLI's auth-prompt ritual, then\n * writes the serialized backup to the requested path. As of,\n * `dump()` already produces a verifiable backup (embedded\n * ledgerHead, full `_ledger` / `_ledger_deltas` snapshots) — the\n * CLI just moves bytes; the integrity guarantees come from core.\n *\n * ## Target URI support\n *\n * ships **`file://` only** (or a plain filesystem path).\n * The issue spec originally called for `s3://` as well, but\n * wiring @aws-sdk into @noy-db/create would defeat the\n * zero-runtime-deps story for the CLI package. S3 backup is\n * deferred to a follow-up that can live in @noy-db/s3-cli or a\n * similar optional companion package.\n *\n * Accepted forms:\n * - `file:///absolute/path.json`\n * - `file://./relative/path.json`\n * - `/absolute/path.json` (treated as `file://`)\n * - `./relative/path.json` (treated as `file://`)\n *\n * ## What this does NOT do\n *\n * - No encryption of the backup BEYOND what noy-db already does.\n * The dumped file is a valid noy-db backup, which means\n * individual records are still encrypted but the keyring is\n * included (wrapped with each user's KEK). Anyone who loads\n * the backup still needs the correct passphrase to read.\n * - No restore — that's a separate subcommand tracked as a\n * follow-up. For now users can restore via\n * `vault.load(backupString)` from their own app code.\n */\n\nimport { promises as fs } from 'node:fs'\nimport path from 'node:path'\nimport { createNoydb, type Noydb, type NoydbStore } from '@noy-db/hub'\nimport { withHistory } from '@noy-db/hub/history'\nimport { jsonFile } from '@noy-db/to-file'\nimport type { ReadPassphrase } from './shared.js'\nimport { defaultReadPassphrase } from './shared.js'\n\nexport interface BackupOptions {\n /** Directory containing the vault data (file adapter only). */\n dir: string\n /** Vault (tenant) name to back up. */\n vault: string\n /** The user id of the operator running the backup. */\n user: string\n /**\n * Where to write the backup. Accepts a `file://` URI or a plain\n * filesystem path. Relative paths resolve against `process.cwd()`.\n */\n target: string\n /** Injected passphrase reader. */\n readPassphrase?: ReadPassphrase\n /** Injected Noydb factory. */\n createDb?: typeof createNoydb\n /** Injected adapter factory. */\n buildAdapter?: (dir: string) => NoydbStore\n}\n\nexport interface BackupResult {\n /** Absolute filesystem path the backup was written to. */\n path: string\n /** Size of the serialized backup in bytes. */\n bytes: number\n}\n\n/**\n * Parse a backup target into an absolute filesystem path. Rejects\n * unsupported URI schemes (s3://, https://, etc.) early so the\n * caller doesn't silently write to the wrong place.\n */\nexport function resolveBackupTarget(target: string, cwd: string = process.cwd()): string {\n // Strip the `file://` prefix if present. The rest of the string\n // is treated as a filesystem path. We accept both `file:///abs`\n // (three slashes, absolute) and `file://./rel` (two slashes,\n // relative) because real-world users write both.\n let raw = target\n if (target.startsWith('file://')) {\n raw = target.slice('file://'.length)\n } else if (target.includes('://')) {\n // Any other scheme is unsupported.\n throw new Error(\n `Unsupported backup target scheme: \"${target.split('://')[0]}://\". ` +\n `Only file:// and plain filesystem paths are supported in. ` +\n `S3 backups will land in a follow-up.`,\n )\n }\n return path.resolve(cwd, raw)\n}\n\nexport async function backup(options: BackupOptions): Promise<BackupResult> {\n const readPassphrase = options.readPassphrase ?? defaultReadPassphrase\n const buildAdapter = options.buildAdapter ?? ((dir) => jsonFile({ dir }))\n const createDb = options.createDb ?? createNoydb\n\n // Resolve the target FIRST so a bad URI fails before any\n // passphrase is collected. This keeps the UX clean: a typo in\n // `s3://bucket/x` rejects without asking for a secret the user\n // would then have to type again.\n const absolutePath = resolveBackupTarget(options.target)\n\n const secret = await readPassphrase(`Passphrase for ${options.user}`)\n\n let db: Noydb | null = null\n try {\n db = await createDb({\n store: buildAdapter(options.dir),\n user: options.user,\n secret,\n // Opt into the history strategy so `vault.dump()` embeds\n // `ledgerHead` for tamper-evident verification at restore.\n // Source vaults that were never written with history simply\n // have an empty ledger; the embedded head is omitted in that\n // case and the backup is treated as legacy on load.\n historyStrategy: withHistory(),\n })\n const vault = await db.openVault(options.vault)\n const serialized = await vault.dump()\n\n // Make sure the parent directory exists. If the user passed\n // `./backups/2026/demo.json` and `./backups/2026` doesn't\n // exist yet, we create it. This is the common case for\n // scripted rotations dropping into a date-based folder.\n await fs.mkdir(path.dirname(absolutePath), { recursive: true })\n await fs.writeFile(absolutePath, serialized, 'utf8')\n\n return {\n path: absolutePath,\n bytes: Buffer.byteLength(serialized, 'utf8'),\n }\n } finally {\n db?.close()\n }\n}\n"],"mappings":"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","names":["fs","path","fs","path","fs","path","fs","fs","path","pathExists","p","createNoydb","isCancel","cancel","createNoydb","createNoydb","jsonFile","jsonFile","createNoydb","fs","path","createNoydb","jsonFile","path","jsonFile","createNoydb","fs"]}
1
+ {"version":3,"sources":["../src/wizard/run.ts","../src/wizard/render.ts","../src/wizard/detect.ts","../src/wizard/augment.ts","../src/wizard/i18n/en.ts","../src/wizard/i18n/th.ts","../src/wizard/i18n/index.ts","../src/commands/add.ts","../src/commands/verify.ts","../src/commands/rotate.ts","../src/commands/shared.ts","../src/commands/add-user.ts","../src/commands/backup.ts"],"sourcesContent":["/**\n * The wizard entry point — `runWizard()`.\n *\n * Two modes:\n *\n * 1. **Interactive (default).** Uses `@clack/prompts` to ask the user\n * for project name, adapter, and sample-data inclusion. Cancellation\n * at any prompt aborts cleanly with a non-zero exit code.\n *\n * 2. **Non-interactive (`yes: true`).** Skips every prompt and uses the\n * values supplied in `WizardOptions`. Missing values become defaults.\n * This is the path tests take — no terminal needed, fully scriptable.\n *\n * The function never spawns child processes (no `npm install` etc.). It\n * only writes files and returns. The shell wrapper around `npm create` is\n * responsible for installing — we keep this layer pure so it's trivially\n * testable and so adding a `--no-install` flag later is a no-op.\n */\n\nimport { promises as fs } from 'node:fs'\nimport path from 'node:path'\nimport * as p from '@clack/prompts'\nimport pc from 'picocolors'\nimport { renderTemplate, templateDir, type RenderTokens } from './render.js'\nimport type {\n WizardAdapter,\n WizardOptions,\n WizardResult,\n WizardFreshResult,\n WizardAugmentResult,\n WizardTemplate,\n} from './types.js'\nimport { detectNuxtProject } from './detect.js'\nimport { augmentNuxtConfig, writeAugmentedConfig } from './augment.js'\nimport { detectLocale, loadMessages, type WizardMessages } from './i18n/index.js'\n\n/**\n * Default invoice records the wizard injects when `sampleData: true`.\n * Kept here (not in the template) so the same records can be reused by\n * the `noy-db add` command for collections with `--seed`. Three records\n * is enough to demo a query that returns more than one row.\n */\nconst DEFAULT_SEED = [\n {\n id: 'inv-001',\n client: 'Acme Holdings',\n amount: 1500,\n status: 'open',\n dueDate: '2026-05-01',\n },\n {\n id: 'inv-002',\n client: 'Globex Inc',\n amount: 4200,\n status: 'paid',\n dueDate: '2026-04-15',\n },\n {\n id: 'inv-003',\n client: 'Initech LLC',\n amount: 850,\n status: 'overdue',\n dueDate: '2026-03-20',\n },\n]\n\nfunction adapterLabels(msg: WizardMessages): Record<WizardAdapter, string> {\n return {\n browser: msg.adapterBrowserLabel,\n file: msg.adapterFileLabel,\n memory: msg.adapterMemoryLabel,\n }\n}\n\n/**\n * Validates the project name. Rules are intentionally narrow:\n * - non-empty\n * - lowercase letters, digits, hyphens, dots, underscores only\n * - cannot start with a hyphen, dot, or underscore\n * - max 214 chars (npm package-name limit)\n *\n * The narrow rule set means whatever the user types is also a valid npm\n * package name, so the generated `package.json#name` field is always safe.\n */\nexport function validateProjectName(name: string): string | null {\n if (!name || name.trim() === '') return 'Project name cannot be empty'\n if (name.length > 214) return 'Project name must be 214 characters or fewer'\n if (!/^[a-z0-9][a-z0-9._-]*$/.test(name)) {\n return 'Project name must start with a lowercase letter or digit and contain only lowercase letters, digits, hyphens, dots, or underscores'\n }\n return null\n}\n\n/**\n * Main entry point. Detects whether `cwd` is an existing Nuxt 4\n * project and routes to one of two modes:\n *\n * - **Fresh mode** (the original behavior): prompts for\n * project name, creates a new directory, renders the Nuxt 4\n * starter template. Returns a `WizardFreshResult`.\n *\n * - **Augment mode** (new in, ): patches the existing\n * `nuxt.config.ts` via magicast to add `@noy-db/in-nuxt` to the\n * modules array and a `noydb:` config key. Shows a unified\n * diff and asks for confirmation before writing. Supports\n * `--dry-run`. Returns a `WizardAugmentResult`.\n *\n * The auto-detection rule: if cwd has both a `nuxt.config.ts`\n * (or `.js`/`.mjs`) AND a `package.json` that lists `nuxt` in any\n * dependency section, augment mode fires. Otherwise fresh mode.\n * Users can force fresh mode via `forceFresh: true` (CLI:\n * `--force-fresh`) when they want to create a sub-project inside\n * an existing Nuxt workspace.\n *\n * Both modes refuse to clobber existing work: fresh mode rejects\n * non-empty target dirs; augment mode rejects unsupported config\n * shapes (opaque exports, non-array modules, etc.).\n */\nexport async function runWizard(options: WizardOptions = {}): Promise<WizardResult> {\n const cwd = options.cwd ?? process.cwd()\n const yes = options.yes ?? false\n\n // Resolve the message bundle once at the top so every downstream\n // helper sees a consistent locale. Explicit `options.locale` wins;\n // otherwise we fall back to POSIX env-var detection (LC_ALL → LANG).\n // Tests pin the locale to keep snapshots deterministic.\n const msg = loadMessages(options.locale ?? detectLocale())\n\n // ── Detect existing Nuxt project ───────────────────────────────────\n // Runs BEFORE any prompts so the interactive flow branches cleanly\n // into fresh vs augment without asking the user questions that\n // don't apply to their mode. `forceFresh` short-circuits the\n // detection — CI tests use this to scaffold into a temp dir that\n // happens to sit under an existing Nuxt project.\n const detection = options.forceFresh\n ? null\n : await detectNuxtProject(cwd)\n\n if (detection?.existing && detection.configPath) {\n return runAugmentMode(options, cwd, detection.configPath, msg)\n }\n\n return runFreshMode(options, cwd, yes, msg)\n}\n\n/**\n * The original fresh-project path, factored out of the\n * main entry so the augment branch above can coexist. Behavior\n * is unchanged from except the return shape now includes\n * the `kind: 'fresh'` discriminator.\n */\nasync function runFreshMode(\n options: WizardOptions,\n cwd: string,\n yes: boolean,\n msg: WizardMessages,\n): Promise<WizardFreshResult> {\n // ── Resolve answers ────────────────────────────────────────────────\n // In non-interactive mode every prompt is short-circuited; in\n // interactive mode we only prompt for fields the caller didn't supply.\n const projectName = yes\n ? options.projectName ?? 'my-noy-db-app'\n : await promptProjectName(options.projectName, msg)\n\n const adapter: WizardAdapter = yes\n ? options.adapter ?? 'browser'\n : await promptAdapter(options.adapter, msg)\n\n const sampleData: boolean = yes\n ? options.sampleData ?? true\n : await promptSampleData(options.sampleData, msg)\n\n const template: WizardTemplate = yes\n ? options.template ?? 'nuxt-default'\n : await promptTemplate(options.template, msg)\n\n const syncAdapter: WizardAdapter | 'none' = yes\n ? options.syncAdapter ?? 'none'\n : await promptSyncAdapter(options.syncAdapter, adapter, msg)\n\n // ── Validate target directory ──────────────────────────────────────\n // We refuse to write into a non-empty directory. Empty + missing are\n // both fine — fs.mkdir { recursive: true } handles both. The `Cancelled`\n // exit is intentional: cleaner than throwing an Error from a wizard.\n const projectPath = path.resolve(cwd, projectName)\n await assertWritableTarget(projectPath)\n\n // ── Render the template ────────────────────────────────────────────\n const tokens: RenderTokens = {\n PROJECT_NAME: projectName,\n ADAPTER: adapter,\n DEVTOOLS: 'true',\n SEED_INVOICES: sampleData\n ? JSON.stringify(DEFAULT_SEED, null, 2).replace(/\\n/g, '\\n ')\n : '[]',\n }\n\n await fs.mkdir(projectPath, { recursive: true })\n const files = await renderTemplate(\n templateDir(template),\n projectPath,\n tokens,\n )\n\n if (!yes) {\n // Print a friendly summary so the user knows what happened. We don't\n // run `npm install` ourselves — the user picks the package manager.\n p.note(\n [\n `${pc.bold('cd')} ${projectName}`,\n `${pc.bold('pnpm install')} ${pc.dim('(or npm/yarn/bun)')}`,\n `${pc.bold('pnpm dev')}`,\n ].join('\\n'),\n msg.freshNextStepsTitle,\n )\n p.outro(pc.green(msg.freshOutroDone))\n }\n\n return {\n kind: 'fresh',\n options: {\n projectName,\n adapter,\n sampleData,\n cwd,\n template,\n syncAdapter,\n },\n projectPath,\n files,\n }\n}\n\n/**\n * The new augment-existing-project path. Runs magicast\n * on the detected nuxt.config, shows a unified diff, asks for\n * confirmation, and writes. Supports `--dry-run` to see the diff\n * without touching disk.\n *\n * Three outcomes:\n * - **written**: the file was patched successfully\n * - **already-configured**: both target mutations are already\n * present (idempotent no-op)\n * - **cancelled**: user said no at the confirmation prompt\n * - **dry-run**: `options.dryRun` was set, we showed the diff\n * and returned without writing\n * - **unsupported-shape**: the config file uses a shape we can't\n * safely mutate (opaque export, non-array modules, etc.)\n */\nasync function runAugmentMode(\n options: WizardOptions,\n cwd: string,\n configPath: string,\n msg: WizardMessages,\n): Promise<WizardAugmentResult> {\n const yes = options.yes ?? false\n const dryRun = options.dryRun ?? false\n\n if (!yes) {\n p.note(\n [\n `${pc.dim(msg.augmentDetectedPrefix)}`,\n ` ${pc.cyan(configPath)}`,\n '',\n msg.augmentDescription,\n ].join('\\n'),\n msg.augmentModeTitle,\n )\n }\n\n // In augment mode we only need ONE prompt from the user: which\n // adapter to wire into the `noydb: { adapter }` key. Everything\n // else is decided by the config we're patching.\n const adapter: WizardAdapter = yes\n ? options.adapter ?? 'browser'\n : await promptAdapter(options.adapter, msg)\n\n const result = await augmentNuxtConfig({\n configPath,\n adapter,\n dryRun,\n })\n\n if (result.kind === 'already-configured') {\n if (!yes) {\n p.note(\n `${pc.yellow(msg.augmentNothingToDo)} ${result.reason}`,\n msg.augmentAlreadyConfiguredTitle,\n )\n p.outro(pc.green(msg.augmentAlreadyOutro))\n }\n return {\n kind: 'augment',\n configPath,\n adapter,\n changed: false,\n reason: 'already-configured',\n }\n }\n\n if (result.kind === 'unsupported-shape') {\n if (!yes) {\n p.cancel(`${pc.red(msg.augmentUnsupportedPrefix)} ${result.reason}`)\n }\n return {\n kind: 'augment',\n configPath,\n adapter,\n changed: false,\n reason: 'unsupported-shape',\n }\n }\n\n // result.kind === 'proposed-change' — print the diff and either\n // write (after confirmation) or bail in dry-run mode.\n if (!yes || dryRun) {\n p.note(renderDiff(result.diff), msg.augmentProposedChangesTitle)\n }\n\n if (dryRun) {\n if (!yes) p.outro(pc.green(msg.augmentDryRunOutro))\n return {\n kind: 'augment',\n configPath,\n adapter,\n changed: false,\n reason: 'dry-run',\n diff: result.diff,\n }\n }\n\n let shouldWrite = yes\n if (!yes) {\n const confirmed = await p.confirm({\n message: msg.augmentApplyConfirm,\n initialValue: true,\n })\n if (p.isCancel(confirmed) || confirmed !== true) {\n p.cancel(msg.augmentAborted)\n return {\n kind: 'augment',\n configPath,\n adapter,\n changed: false,\n reason: 'cancelled',\n diff: result.diff,\n }\n }\n shouldWrite = true\n }\n\n if (shouldWrite) {\n await writeAugmentedConfig(configPath, result.newCode)\n if (!yes) {\n p.note(\n [\n pc.dim(msg.augmentInstallIntro),\n '',\n `${pc.bold('pnpm add')} @noy-db/in-nuxt @noy-db/in-pinia @noy-db/hub @noy-db/to-browser-idb @pinia/nuxt pinia`,\n pc.dim(msg.augmentInstallPmHint),\n ].join('\\n'),\n msg.augmentNextStepTitle,\n )\n p.outro(pc.green(msg.augmentDoneOutro))\n }\n }\n\n return {\n kind: 'augment',\n configPath,\n adapter,\n changed: true,\n reason: 'written',\n diff: result.diff,\n }\n}\n\n/**\n * Clean up a unified diff for terminal display. Strips the `===`\n * separators the `diff` package emits, keeps a reasonable max\n * width, and drops the Index/------ preamble that's only useful\n * for `patch -p1`. The result is a short, colorized-ready string\n * that fits inside a clack `note()` block.\n */\nfunction renderDiff(diff: string): string {\n const lines = diff.split('\\n')\n const keep: string[] = []\n for (const line of lines) {\n if (line.startsWith('Index:')) continue\n if (line.startsWith('=')) continue\n if (line.startsWith('---') || line.startsWith('+++')) continue\n if (line.startsWith('+')) keep.push(pc.green(line))\n else if (line.startsWith('-')) keep.push(pc.red(line))\n else if (line.startsWith('@@')) keep.push(pc.dim(line))\n else keep.push(line)\n }\n return keep.join('\\n').trim()\n}\n\n// ─── Prompt helpers ──────────────────────────────────────────────────────\n\nasync function promptProjectName(initial: string | undefined, msg: WizardMessages): Promise<string> {\n if (initial) {\n const err = validateProjectName(initial)\n if (err) throw new Error(err)\n return initial\n }\n const result = await p.text({\n message: msg.promptProjectName,\n placeholder: msg.promptProjectNamePlaceholder,\n initialValue: 'my-noy-db-app',\n validate: (v) => validateProjectName(v ?? '') ?? undefined,\n })\n if (p.isCancel(result)) {\n p.cancel(msg.cancelled)\n process.exit(1)\n }\n return result\n}\n\nasync function promptAdapter(initial: WizardAdapter | undefined, msg: WizardMessages): Promise<WizardAdapter> {\n if (initial) return initial\n const labels = adapterLabels(msg)\n const result = await p.select<WizardAdapter>({\n message: msg.promptAdapter,\n options: (['browser', 'file', 'memory'] as const).map((value) => ({\n value,\n label: labels[value],\n })),\n initialValue: 'browser',\n })\n if (p.isCancel(result)) {\n p.cancel(msg.cancelled)\n process.exit(1)\n }\n return result\n}\n\nasync function promptSyncAdapter(\n initial: WizardAdapter | 'none' | undefined,\n primary: WizardAdapter,\n _msg: WizardMessages,\n): Promise<WizardAdapter | 'none'> {\n if (initial !== undefined) return initial\n const choices = (['none', 'memory', 'file', 'browser'] as const).filter(\n (x) => x !== primary,\n )\n const labels: Record<WizardAdapter | 'none', string> = {\n none: 'No sync — local only',\n memory: 'In-memory (testing / CI)',\n file: 'JSON files (USB / shared drive)',\n browser: 'IndexedDB (other tabs / PWAs)',\n }\n const result = await p.select<WizardAdapter | 'none'>({\n message: 'Add a sync target?',\n options: choices.map((value) => ({ value, label: labels[value] })),\n initialValue: 'none',\n })\n if (p.isCancel(result)) {\n process.exit(1)\n }\n return result\n}\n\nasync function promptTemplate(\n initial: WizardTemplate | undefined,\n _msg: WizardMessages,\n): Promise<WizardTemplate> {\n if (initial) return initial\n const result = await p.select<WizardTemplate>({\n message: 'Pick a starter template',\n options: [\n {\n value: 'nuxt-default',\n label: 'Nuxt 4 + Pinia',\n hint: 'SSR + file-based routing (recommended default)',\n },\n {\n value: 'vite-vue',\n label: 'Vite + Vue 3 + Pinia',\n hint: 'Client-side SPA, shorter install graph',\n },\n {\n value: 'vanilla',\n label: 'Vite + TypeScript',\n hint: 'No framework, smallest footprint',\n },\n {\n value: 'electron',\n label: 'Electron + Vue 3',\n hint: 'Desktop app with to-file adapter — USB workflow',\n },\n ],\n initialValue: 'nuxt-default',\n })\n if (p.isCancel(result)) {\n process.exit(1)\n }\n return result\n}\n\nasync function promptSampleData(initial: boolean | undefined, msg: WizardMessages): Promise<boolean> {\n if (typeof initial === 'boolean') return initial\n const result = await p.confirm({\n message: msg.promptSampleData,\n initialValue: true,\n })\n if (p.isCancel(result)) {\n p.cancel(msg.cancelled)\n process.exit(1)\n }\n return result\n}\n\nasync function assertWritableTarget(target: string): Promise<void> {\n try {\n const entries = await fs.readdir(target)\n if (entries.length > 0) {\n throw new Error(\n `Target directory '${target}' already exists and is not empty. Refusing to overwrite — pick a different project name or remove the directory first.`,\n )\n }\n } catch (err: unknown) {\n // ENOENT is the happy path — the directory doesn't exist yet.\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return\n throw err\n }\n}\n","/**\n * Template loader and renderer.\n *\n * Templates live under `packages/create-noy-db/templates/<template-name>/`\n * as a literal directory tree. Files are read verbatim from disk; the\n * renderer walks the tree, substitutes a small set of placeholder tokens,\n * and writes the result to the target directory.\n *\n * The placeholder syntax is intentionally minimal: `{{TOKEN}}` (uppercase,\n * no spaces, no nested expressions). This avoids dragging in handlebars\n * or any templating library, keeps the bundle small, and means there's\n * exactly one rule to learn when authoring a template.\n *\n * Files whose names start with `_` are renamed to `.` on copy. This is\n * how we ship `_gitignore` (`.gitignore`) without npm trying to interpret\n * it as part of the package contents.\n */\n\nimport { promises as fs } from 'node:fs'\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\n/**\n * Tokens the renderer recognizes. Authoring rule for templates: every\n * placeholder must be a member of this set, otherwise template authors\n * will hit silent no-ops at render time.\n */\nexport interface RenderTokens {\n /** The user-supplied project name. Used for `package.json#name` etc. */\n PROJECT_NAME: string\n /** The chosen adapter (browser/file/memory). */\n ADAPTER: string\n /** \"true\" or \"false\" — written into `nuxt.config.ts` for `noydb.devtools`. */\n DEVTOOLS: string\n /** A literal `[...]` block of seed records, or `[]` if `sampleData` is false. */\n SEED_INVOICES: string\n}\n\n/**\n * Walks `src` recursively and copies every file into `dest`, substituting\n * `{{TOKEN}}` placeholders along the way.\n *\n * Returns the relative paths of every file written, sorted alphabetically.\n * Tests use this to assert that the expected file set was produced without\n * having to walk `dest` themselves.\n *\n * The function is intentionally synchronous-feeling (uses `await` inside\n * a depth-first walk) so the order of writes is deterministic — easier\n * to reason about when something fails halfway through.\n */\nexport async function renderTemplate(\n src: string,\n dest: string,\n tokens: RenderTokens,\n): Promise<string[]> {\n const written: string[] = []\n await walk(src, dest, '', tokens, written)\n written.sort()\n return written\n}\n\nasync function walk(\n srcRoot: string,\n destRoot: string,\n rel: string,\n tokens: RenderTokens,\n written: string[],\n): Promise<void> {\n const srcDir = path.join(srcRoot, rel)\n const entries = await fs.readdir(srcDir, { withFileTypes: true })\n\n for (const entry of entries) {\n const srcEntry = path.join(srcDir, entry.name)\n // _gitignore → .gitignore, _npmrc → .npmrc, etc.\n // npm strips files starting with `.` from the published tarball, so\n // shipping them as `_<name>` is the standard workaround.\n const destName = entry.name.startsWith('_')\n ? `.${entry.name.slice(1)}`\n : entry.name\n const destRel = rel ? path.join(rel, destName) : destName\n const destEntry = path.join(destRoot, destRel)\n\n if (entry.isDirectory()) {\n await fs.mkdir(destEntry, { recursive: true })\n await walk(srcRoot, destRoot, path.join(rel, entry.name), tokens, written)\n continue\n }\n\n const raw = await fs.readFile(srcEntry, 'utf8')\n const rendered = applyTokens(raw, tokens)\n await fs.mkdir(path.dirname(destEntry), { recursive: true })\n await fs.writeFile(destEntry, rendered, 'utf8')\n written.push(destRel)\n }\n}\n\n/**\n * Substitutes every `{{KEY}}` in `input` with the corresponding value\n * from `tokens`. Unknown keys are left untouched (so the user can spot\n * them visually in the generated output if a template author makes a\n * typo). Empty values are allowed.\n */\nexport function applyTokens(input: string, tokens: RenderTokens): string {\n const bag = tokens as unknown as Record<string, string>\n return input.replace(/\\{\\{(\\w+)\\}\\}/g, (match, key: string) => {\n const value = bag[key]\n return value === undefined ? match : value\n })\n}\n\n/**\n * Resolves the absolute path of a template directory shipped inside the\n * package. The package layout is:\n *\n * packages/create-noy-db/\n * dist/\n * wizard/render.js ← we are here at runtime (after tsup build)\n * templates/\n * nuxt-default/ ← we want to point at this\n *\n * In the published tarball the same relative path holds: `dist/` and\n * `templates/` are siblings under the package root. Computing the path\n * relative to `import.meta.url` lets us avoid hardcoding any assumption\n * about whether we're running from source, from `dist/`, or from a\n * globally installed copy.\n */\nexport function templateDir(name: string): string {\n // From `dist/wizard/render.js` we go up two levels to the package root,\n // then into `templates/<name>`.\n const here = fileURLToPath(import.meta.url)\n const packageRoot = path.resolve(path.dirname(here), '..', '..')\n return path.join(packageRoot, 'templates', name)\n}\n","/**\n * Detect whether the wizard is being invoked inside an existing\n * Nuxt 4 project that should be augmented in place, vs a blank\n * directory where the wizard should create a fresh starter.\n *\n * The detection rule is intentionally narrow so we don't trip on\n * random `package.json` files sitting in the parent tree: a project\n * counts as \"existing Nuxt 4\" only when the SAME directory has\n * BOTH `nuxt.config.ts` (or `.js`) AND a `package.json` that lists\n * `nuxt` in any of the dependency sections. Either alone is\n * ambiguous:\n *\n * - `nuxt.config.ts` without a package.json could be a stray\n * template scratch file the user moved here.\n * - `package.json` with `nuxt` but no config file could be a\n * half-deleted project or a pnpm workspace root that pulls\n * nuxt in transitively for docs.\n *\n * Requiring both avoids both false positives.\n *\n * We deliberately do NOT walk upward to find a parent Nuxt project.\n * The user's cwd is load-bearing — if they wanted to augment a\n * parent dir, they should cd there first. Walking up would make\n * \"run the wizard to create a fresh app inside my existing\n * monorepo\" silently augment the monorepo root instead.\n */\n\nimport { promises as fs } from 'node:fs'\nimport path from 'node:path'\n\nexport interface NuxtDetection {\n /** Whether cwd contains an existing Nuxt 4 project. */\n readonly existing: boolean\n /** Absolute path of the detected config file, if any. */\n readonly configPath: string | null\n /** Absolute path of the detected package.json, if any. */\n readonly packageJsonPath: string | null\n /** Reason strings explaining the detection outcome — useful for diagnostic messages. */\n readonly reasons: readonly string[]\n}\n\n/**\n * Inspect `cwd` for an existing Nuxt 4 project. Returns a detailed\n * result object so the caller can both branch on `existing` and\n * surface the specific reasons to the user.\n *\n * Pure in terms of filesystem reads — no writes, no network, no\n * caching. Callers who need cached detection should memoize on\n * their side.\n */\nexport async function detectNuxtProject(cwd: string): Promise<NuxtDetection> {\n const reasons: string[] = []\n\n // Step 1: look for a Nuxt config file. Both extensions are\n // valid; Nuxt itself accepts either.\n const configCandidates = ['nuxt.config.ts', 'nuxt.config.js', 'nuxt.config.mjs']\n let configPath: string | null = null\n for (const name of configCandidates) {\n const candidate = path.join(cwd, name)\n if (await pathExists(candidate)) {\n configPath = candidate\n reasons.push(`Found ${name}`)\n break\n }\n }\n if (!configPath) {\n reasons.push('No nuxt.config.{ts,js,mjs} in cwd')\n return {\n existing: false,\n configPath: null,\n packageJsonPath: null,\n reasons,\n }\n }\n\n // Step 2: look for a package.json in the same directory.\n const pkgPath = path.join(cwd, 'package.json')\n if (!(await pathExists(pkgPath))) {\n reasons.push('Config file present but no package.json — ambiguous, skipping')\n return {\n existing: false,\n configPath,\n packageJsonPath: null,\n reasons,\n }\n }\n\n // Step 3: verify `nuxt` is in one of the dependency sections.\n // We're lenient about WHICH section (dependencies, devDependencies,\n // peerDependencies) because real-world projects put it in all of\n // them depending on the tooling.\n let pkg: Record<string, unknown>\n try {\n pkg = JSON.parse(await fs.readFile(pkgPath, 'utf8')) as Record<string, unknown>\n } catch (err) {\n reasons.push(`package.json is not valid JSON: ${(err as Error).message}`)\n return {\n existing: false,\n configPath,\n packageJsonPath: pkgPath,\n reasons,\n }\n }\n\n const depSections = ['dependencies', 'devDependencies', 'peerDependencies'] as const\n let nuxtVersion: string | undefined\n for (const section of depSections) {\n const deps = pkg[section]\n if (deps && typeof deps === 'object' && 'nuxt' in deps) {\n nuxtVersion = (deps as Record<string, string>)['nuxt']\n reasons.push(`Found nuxt@${nuxtVersion} in ${section}`)\n break\n }\n }\n if (!nuxtVersion) {\n reasons.push('Config file present, but package.json does not list `nuxt` as a dependency')\n return {\n existing: false,\n configPath,\n packageJsonPath: pkgPath,\n reasons,\n }\n }\n\n return {\n existing: true,\n configPath,\n packageJsonPath: pkgPath,\n reasons,\n }\n}\n\n/** Cheap fs.access wrapper that returns a boolean instead of throwing. */\nasync function pathExists(target: string): Promise<boolean> {\n try {\n await fs.access(target)\n return true\n } catch {\n return false\n }\n}\n","/**\n * Augment an existing Nuxt 4 project with `@noy-db/in-nuxt`.\n *\n * The entry point is `augmentNuxtConfig()`, which reads a\n * `nuxt.config.ts` (or `.js` / `.mjs`) via magicast, mutates the\n * AST to:\n *\n * 1. Add `'@noy-db/in-nuxt'` to the `modules` array (creating the\n * array if it doesn't exist)\n * 2. Add the `noydb: { adapter, pinia: true, devtools: true }`\n * config key (creating it if it doesn't exist)\n *\n * then generates the new source code, computes a unified diff\n * against the original, and either writes the result (with\n * optional user confirmation) or returns the diff unchanged for\n * `--dry-run`.\n *\n * ## Idempotency\n *\n * Re-running the wizard on an already-augmented project is a\n * no-op: we check whether the target values are already present\n * before mutating, and if both are, we short-circuit before\n * calling `generateCode()`. The caller sees\n * `{ changed: false, reason: 'already configured' }` and can\n * exit cleanly.\n *\n * ## Edge cases\n *\n * - `export default defineNuxtConfig({...})` — the common case,\n * handled directly.\n * - `export default {...}` — plain object literal, also handled.\n * - `export default someVar` — opaque reference, we bail with a\n * clear error telling the user to edit manually. We don't try\n * to chase the variable.\n * - `modules` declared as an object instead of an array — rare\n * but possible in some Nuxt tooling. We bail.\n * - `modules` already contains `'@noy-db/in-nuxt'` — idempotent skip.\n * - `noydb` key already present — we preserve the existing value\n * UNLESS `force: true` is passed (not yet surfaced in the CLI;\n * reserved for a future `--overwrite-config` flag).\n *\n * ## Why magicast instead of a regex/string-patch\n *\n * Regex-based config patching produces correct-looking code 90%\n * of the time and silently wrong code the other 10%. Anything\n * non-trivial (nested options, comments, multi-line strings,\n * conditional exports) breaks. Magicast walks a real Babel AST,\n * preserves unrelated formatting, and round-trips the file\n * cleanly — including comments, trailing commas, and property\n * order.\n */\n\nimport { promises as fs } from 'node:fs'\nimport { loadFile, generateCode, builders } from 'magicast'\nimport { createPatch } from 'diff'\nimport type { WizardAdapter } from './types.js'\n\nexport interface AugmentOptions {\n /** Absolute path of the nuxt.config.{ts,js,mjs} to patch. */\n configPath: string\n /** Adapter string to write into `noydb: { adapter }`. */\n adapter: WizardAdapter\n /**\n * When true, skip the write and return the would-be result.\n * The CLI's `--dry-run` flag threads through to here.\n */\n dryRun?: boolean\n}\n\nexport type AugmentResult =\n | {\n readonly kind: 'already-configured'\n readonly configPath: string\n readonly reason: string\n }\n | {\n readonly kind: 'proposed-change'\n readonly configPath: string\n readonly originalCode: string\n readonly newCode: string\n /** Unified diff string suitable for printing to the terminal. */\n readonly diff: string\n /** `true` when `options.dryRun` was set — the caller skips the write. */\n readonly dryRun: boolean\n }\n | {\n readonly kind: 'unsupported-shape'\n readonly configPath: string\n readonly reason: string\n }\n\n/**\n * Parse, mutate, and compute the diff. Does NOT write the file —\n * that's the caller's responsibility, after (optionally) prompting\n * the user for confirmation.\n *\n * The split exists so the CLI can interleave the prompt between\n * \"show the diff\" and \"write the file\", and so tests can inspect\n * the proposed change without any filesystem side effect beyond\n * the initial read.\n */\nexport async function augmentNuxtConfig(\n options: AugmentOptions,\n): Promise<AugmentResult> {\n const originalCode = await fs.readFile(options.configPath, 'utf8')\n const mod = await loadFile(options.configPath)\n\n // The entry point is whatever `export default` points at. In a\n // Nuxt config this is either `defineNuxtConfig({...})` or a\n // plain object literal.\n const exported = mod.exports.default as unknown\n if (exported === undefined || exported === null) {\n return {\n kind: 'unsupported-shape',\n configPath: options.configPath,\n reason: `${options.configPath} has no default export. Expected \\`export default defineNuxtConfig({...})\\` or \\`export default {...}\\`.`,\n }\n }\n\n // Both the `defineNuxtConfig({...})` call and the plain-object\n // case expose the config as a proxified object. For the function\n // call, magicast's proxy makes `exported.$args[0]` the config\n // object; for a plain object it's `exported` itself. We test\n // both shapes and pick the first one that has `modules` or can\n // accept a `modules` write.\n //\n // This looks a bit like JavaScript duck-typing because it IS\n // duck-typing — magicast's proxy tolerates either shape and the\n // API surface is identical once you reach the config object.\n const config = resolveConfigObject(exported)\n if (!config) {\n return {\n kind: 'unsupported-shape',\n configPath: options.configPath,\n reason: `Could not find the config object in ${options.configPath}. ` +\n `Expected \\`export default defineNuxtConfig({ modules: [], ... })\\` or a plain object literal.`,\n }\n }\n\n const skipReasons: string[] = []\n\n // --- Modules array -------------------------------------------------\n // Ensure `modules` is an array; create it if missing. Magicast's\n // proxy handles the array-vs-missing distinction via typeof checks\n // on the array's length property.\n const modulesRaw: unknown = config.modules\n let modulesWasMissing = false\n if (modulesRaw === undefined) {\n modulesWasMissing = true\n config.modules = []\n } else if (typeof modulesRaw !== 'object' || !isProxyArray(modulesRaw)) {\n return {\n kind: 'unsupported-shape',\n configPath: options.configPath,\n reason: `\\`modules\\` in ${options.configPath} is not an array literal. ` +\n `Edit it manually and re-run the wizard if you want to continue.`,\n }\n }\n\n // Push '@noy-db/in-nuxt' if it's not already listed. We compare by\n // stringification because magicast's proxy yields primitive\n // strings for literal entries.\n const modules = config.modules as string[]\n const alreadyHasModule = Array.from(modules).some((m) => String(m) === '@noy-db/in-nuxt')\n if (alreadyHasModule) {\n skipReasons.push('`@noy-db/in-nuxt` already in modules')\n } else {\n // Insert as a literal string so magicast writes `'@noy-db/in-nuxt'`\n // rather than an object wrapper.\n modules.push('@noy-db/in-nuxt')\n }\n\n // --- `noydb` config key --------------------------------------------\n // If the user already has a noydb key, preserve it — they might\n // have custom options we don't want to clobber. If it doesn't\n // exist, add it with the adapter the wizard collected.\n const noydbRaw: unknown = config.noydb\n if (noydbRaw !== undefined) {\n skipReasons.push('`noydb` key already set')\n } else {\n // `builders.raw` parses the literal into an AST node, so the\n // output is a real object expression in the generated code\n // instead of a stringified opaque blob.\n config.noydb = builders.raw(\n `{ adapter: '${options.adapter}', pinia: true, devtools: true }`,\n )\n }\n\n // If nothing changed, short-circuit. `modulesWasMissing` is a\n // change even if we didn't push anything, because we wrote an\n // empty array into the config — but that's an unusual case so\n // we lump it in with the regular \"changed\" path.\n if (skipReasons.length === 2 && !modulesWasMissing) {\n return {\n kind: 'already-configured',\n configPath: options.configPath,\n reason: skipReasons.join('; '),\n }\n }\n\n // Generate the new source, then compute a unified diff against\n // the original. `createPatch` is from the `diff` package — same\n // one `jest-diff` and `vitest` use under the hood. The empty\n // header strings keep the diff output compact for terminal\n // display; a full unified diff header isn't needed since we're\n // going to print the diff inline, not pipe it to `patch -p1`.\n const generated = generateCode(mod).code\n const diff = createPatch(\n options.configPath,\n originalCode,\n generated,\n '',\n '',\n { context: 3 },\n )\n\n return {\n kind: 'proposed-change',\n configPath: options.configPath,\n originalCode,\n newCode: generated,\n diff,\n dryRun: options.dryRun === true,\n }\n}\n\n/**\n * Write the augmented config to disk. Call this after the user\n * has seen and approved the diff from `augmentNuxtConfig()`. The\n * split between \"compute\" and \"write\" is deliberate — see the\n * module docstring.\n */\nexport async function writeAugmentedConfig(\n configPath: string,\n newCode: string,\n): Promise<void> {\n await fs.writeFile(configPath, newCode, 'utf8')\n}\n\n// ─── Internal helpers ─────────────────────────────────────────────────\n\n/**\n * Given the proxified default export, return the config object\n * itself. Handles both `defineNuxtConfig({...})` (proxified function\n * call whose `$args[0]` is the config) and `{...}` (plain object\n * proxy). Returns null for shapes we don't recognize.\n *\n * Important: magicast's `$args` is a PROXY, not a real array, so\n * `Array.isArray($args)` returns false. We access `$args[0]`\n * directly and check whether the result is a usable object.\n */\nfunction resolveConfigObject(\n exported: unknown,\n): Record<string, unknown> | null {\n if (!exported || typeof exported !== 'object') return null\n const proxy = exported as Record<string, unknown> & {\n $type?: string\n $args?: Record<number, unknown>\n }\n\n // defineNuxtConfig(...) call — magicast exposes the args through\n // a proxied $args accessor. The config literal is at $args[0].\n if (proxy.$type === 'function-call' && proxy.$args) {\n const firstArg = proxy.$args[0]\n if (firstArg && typeof firstArg === 'object') {\n return firstArg as Record<string, unknown>\n }\n return null\n }\n\n // Plain object literal export — the proxy IS the config.\n if (proxy.$type === 'object' || proxy.$type === undefined) {\n return proxy\n }\n\n return null\n}\n\n/**\n * Check whether a magicast-proxied value is an array. The proxy\n * reports `$type === 'array'` for array literals; plain objects\n * and strings come back with different tags.\n */\nfunction isProxyArray(value: unknown): boolean {\n if (!value || typeof value !== 'object') return false\n const proxy = value as { $type?: string }\n return proxy.$type === 'array'\n}\n","/**\n * English (`en`) wizard messages — the default locale.\n *\n * Editing note: if you change a key's value here, update the\n * matching key in every other locale bundle (`th.ts`, future\n * additions). The key-parity test in `__tests__/i18n.test.ts`\n * is the safety net: it fails if any locale is missing a key\n * or has an extra one, so a forgotten update surfaces in CI\n * rather than in a user report.\n */\n\nimport type { WizardMessages } from './types.js'\n\nexport const en: WizardMessages = {\n wizardIntro:\n 'A wizard for noy-db — None Of Your DataBase.\\n' +\n 'Generates a fresh Nuxt 4 + Pinia + encrypted-store starter.',\n\n promptProjectName: 'Project name',\n promptProjectNamePlaceholder: 'my-noy-db-app',\n promptAdapter: 'Storage adapter',\n adapterBrowserLabel:\n 'browser — localStorage / IndexedDB (recommended for web apps)',\n adapterFileLabel:\n 'file — JSON files on disk (Electron / Tauri / USB workflows)',\n adapterMemoryLabel:\n 'memory — no persistence (ideal for tests and demos)',\n promptSampleData: 'Include sample invoice records?',\n\n freshNextStepsTitle: 'Next steps',\n freshOutroDone: '✔ Done — happy encrypting!',\n\n augmentModeTitle: 'Augment mode',\n augmentDetectedPrefix: 'Detected existing Nuxt 4 project:',\n augmentDescription:\n 'The wizard will add @noy-db/in-nuxt to your modules array\\n' +\n 'and a noydb: config key. You can review the diff before\\n' +\n 'anything is written to disk.',\n\n augmentProposedChangesTitle: 'Proposed changes',\n augmentApplyConfirm: 'Apply these changes?',\n\n augmentAlreadyConfiguredTitle: 'Already configured',\n augmentNothingToDo: 'Nothing to do:',\n augmentAlreadyOutro: '✔ Your Nuxt config is already wired up.',\n augmentAborted: 'Aborted — your config is unchanged.',\n augmentDryRunOutro: '✔ Dry run — no files were modified.',\n augmentNextStepTitle: 'Next step',\n augmentInstallIntro:\n 'Install the @noy-db packages your config now depends on:',\n augmentInstallPmHint: '(or use npm/yarn/bun as appropriate)',\n augmentDoneOutro: '✔ Config updated — happy encrypting!',\n augmentUnsupportedPrefix: 'Cannot safely patch this config:',\n\n cancelled: 'Cancelled.',\n}\n","/**\n * Thai (`th`) wizard messages.\n *\n * Editing note: this bundle MUST stay in key-parity with `en.ts`.\n * The key-parity test in `__tests__/i18n.test.ts` fails if any key\n * is missing or extra here, so a forgotten translation surfaces in\n * CI rather than as a runtime crash for a Thai-speaking user.\n *\n * Translation conventions:\n * - Technical identifiers (`modules`, `noydb:`, `nuxt.config.ts`,\n * `Nuxt 4`, `Pinia`, adapter names like `browser`/`file`/`memory`)\n * stay in English. This matches how Thai developers actually\n * read and write code — translating them would just add a\n * mental round-trip.\n * - Product name \"noy-db\" and the \"None Of Your DataBase\" tagline\n * also stay in English; they're brand strings, not prose.\n * - Validation/error strings are NOT in this file — they stay in\n * English so bug reports look the same in any locale (see the\n * docstring on `WizardMessages` in `types.ts` for the rationale).\n */\n\nimport type { WizardMessages } from './types.js'\n\nexport const th: WizardMessages = {\n wizardIntro:\n 'ตัวช่วยสร้างสำหรับ noy-db — None Of Your DataBase\\n' +\n 'สร้างโปรเจกต์เริ่มต้น Nuxt 4 + Pinia พร้อมที่เก็บข้อมูลแบบเข้ารหัส',\n\n promptProjectName: 'ชื่อโปรเจกต์',\n promptProjectNamePlaceholder: 'my-noy-db-app',\n promptAdapter: 'อะแดปเตอร์จัดเก็บข้อมูล',\n adapterBrowserLabel:\n 'browser — localStorage / IndexedDB (แนะนำสำหรับเว็บแอป)',\n adapterFileLabel:\n 'file — ไฟล์ JSON บนดิสก์ (Electron / Tauri / USB)',\n adapterMemoryLabel:\n 'memory — ไม่บันทึกข้อมูล (เหมาะสำหรับการทดสอบและตัวอย่าง)',\n promptSampleData: 'เพิ่มข้อมูลตัวอย่างใบแจ้งหนี้หรือไม่?',\n\n freshNextStepsTitle: 'ขั้นตอนถัดไป',\n freshOutroDone: '✔ เสร็จเรียบร้อย — ขอให้สนุกกับการเข้ารหัส!',\n\n augmentModeTitle: 'โหมดเสริมโปรเจกต์เดิม',\n augmentDetectedPrefix: 'พบโปรเจกต์ Nuxt 4 ที่มีอยู่แล้ว:',\n augmentDescription:\n 'ตัวช่วยจะเพิ่ม @noy-db/in-nuxt เข้าใน modules\\n' +\n 'และเพิ่มคีย์ noydb: ในไฟล์ config คุณสามารถดู diff\\n' +\n 'ก่อนที่จะเขียนไฟล์ลงดิสก์ได้',\n\n augmentProposedChangesTitle: 'รายการเปลี่ยนแปลงที่จะทำ',\n augmentApplyConfirm: 'ยืนยันการเปลี่ยนแปลงเหล่านี้?',\n\n augmentAlreadyConfiguredTitle: 'ตั้งค่าไว้แล้ว',\n augmentNothingToDo: 'ไม่มีอะไรต้องทำ:',\n augmentAlreadyOutro: '✔ ไฟล์ Nuxt config ของคุณตั้งค่าครบแล้ว',\n augmentAborted: 'ยกเลิก — ไฟล์ config ของคุณไม่ถูกแก้ไข',\n augmentDryRunOutro: '✔ Dry run — ไม่มีไฟล์ใดถูกแก้ไข',\n augmentNextStepTitle: 'ขั้นตอนถัดไป',\n augmentInstallIntro:\n 'ติดตั้งแพ็กเกจ @noy-db ที่ config ของคุณต้องใช้:',\n augmentInstallPmHint: '(หรือใช้ npm/yarn/bun ตามความเหมาะสม)',\n augmentDoneOutro: '✔ อัปเดต config เรียบร้อย — ขอให้สนุกกับการเข้ารหัส!',\n augmentUnsupportedPrefix: 'ไม่สามารถแก้ไข config นี้ได้อย่างปลอดภัย:',\n\n cancelled: 'ยกเลิกแล้ว',\n}\n","/**\n * i18n entrypoint for the `@noy-db/create` wizard.\n *\n * Three responsibilities:\n *\n * 1. Re-export `Locale` and `WizardMessages` so callers don't\n * need to know about the bundle layout.\n * 2. `detectLocale(env)` — pure function that maps Unix-style\n * `LC_ALL` / `LANG` / `LANGUAGE` env vars to a supported\n * `Locale`. Returns `'en'` for anything we don't recognise.\n * 3. `loadMessages(locale)` — synchronous lookup that returns\n * the message bundle for a locale. Synchronous (not dynamic\n * `import()`) on purpose: bundles are tiny (< 2 KB each), the\n * wizard reads them on every prompt, and async would force\n * every caller to be async. tsup tree-shakes unused locales\n * out of the bin only if we use top-level `import`s.\n *\n * ## Why env-var detection instead of `Intl.DateTimeFormat().resolvedOptions().locale`\n *\n * The Intl approach reads the JS engine's *display* locale, which\n * on most CI runners and Docker images is `en-US` regardless of\n * the user's actual setup. The Unix env vars (`LC_ALL`, `LANG`)\n * are how shells, terminals, and CLI tools have negotiated locale\n * for 30+ years — that's what a Thai-speaking dev's terminal will\n * actually have set. Following that convention also means power\n * users can override per-invocation with `LANG=th_TH.UTF-8 npm\n * create @noy-db`, no flag required.\n */\n\nimport { en } from './en.js'\nimport { th } from './th.js'\nimport type { Locale, WizardMessages } from './types.js'\n\nexport type { Locale, WizardMessages } from './types.js'\n\nconst BUNDLES: Record<Locale, WizardMessages> = { en, th }\n\n/** Every locale we ship a bundle for. Used by tests and `--lang` validation. */\nexport const SUPPORTED_LOCALES: readonly Locale[] = ['en', 'th'] as const\n\n/**\n * Resolve a locale code to its message bundle. Falls back to `en`\n * if the requested locale isn't shipped — defensive, since\n * `Locale` is a union type and TS already prevents this at compile\n * time, but `--lang` parsing comes from user input at runtime.\n */\nexport function loadMessages(locale: Locale): WizardMessages {\n return BUNDLES[locale] ?? BUNDLES.en\n}\n\n/**\n * Auto-detect a locale from POSIX env vars. Returns `'en'` when\n * nothing is set or when the value doesn't match a supported\n * locale — never throws.\n *\n * Inspection order matches the POSIX spec:\n * 1. `LC_ALL` (overrides everything)\n * 2. `LC_MESSAGES` (the category we actually care about)\n * 3. `LANG` (system default)\n * 4. `LANGUAGE` (GNU extension, comma-separated preference list)\n *\n * The first non-empty value wins. We then strip the encoding\n * suffix (`th_TH.UTF-8` → `th_TH`) and the region (`th_TH` → `th`)\n * before matching against `SUPPORTED_LOCALES`.\n */\nexport function detectLocale(env: NodeJS.ProcessEnv = process.env): Locale {\n const candidates = [\n env.LC_ALL,\n env.LC_MESSAGES,\n env.LANG,\n // LANGUAGE is a comma-separated preference list — take the first\n // entry. We deliberately do NOT walk the whole list; the wizard\n // ships exactly two locales, so a \"best fit\" walk would be\n // overkill and would obscure unexpected behaviour.\n env.LANGUAGE?.split(':')[0]?.split(',')[0],\n ]\n\n for (const raw of candidates) {\n if (!raw) continue\n const normalised = raw\n .split('.')[0]! // strip encoding: th_TH.UTF-8 → th_TH\n .split('@')[0]! // strip modifier: en_US@euro → en_US\n .toLowerCase()\n .split('_')[0]! // strip region: th_th → th\n\n if ((SUPPORTED_LOCALES as readonly string[]).includes(normalised)) {\n return normalised as Locale\n }\n }\n\n return 'en'\n}\n\n/**\n * Parse a `--lang` CLI argument into a `Locale`. Throws a clear\n * error for unsupported values — the caller (parse-args) catches\n * and reformats into a usage message.\n */\nexport function parseLocaleFlag(value: string): Locale {\n const normalised = value.toLowerCase().trim()\n if ((SUPPORTED_LOCALES as readonly string[]).includes(normalised)) {\n return normalised as Locale\n }\n throw new Error(\n `Unsupported --lang value: \"${value}\". Supported: ${SUPPORTED_LOCALES.join(', ')}`,\n )\n}\n","/**\n * `noy-db add <collection>` — scaffold a new collection inside an existing\n * Nuxt 4 project that already has `@noy-db/in-nuxt` configured.\n *\n * The command writes two files:\n *\n * 1. `app/stores/<collection>.ts` — a `defineNoydbStore<T>()` call with\n * a placeholder `T` interface and one example field. The user fills\n * in the real shape after the file is created.\n *\n * 2. `app/pages/<collection>.vue` — a minimal CRUD page that lists,\n * adds, and deletes records. The store ID and collection name are\n * derived from the argument; everything else is boilerplate.\n *\n * The command refuses to overwrite existing files. If either target\n * already exists it logs which one and exits non-zero — the user has to\n * delete or move the file first. There's no `--force` because forcing an\n * overwrite of generated UI code is almost always a footgun in disguise.\n */\n\nimport { promises as fs } from 'node:fs'\nimport path from 'node:path'\n\nexport interface AddCollectionOptions {\n /** The collection name. Must be a lowercase identifier. */\n name: string\n /** Project root. Defaults to `process.cwd()`. */\n cwd?: string\n /** Compartment id to embed in the generated store. Defaults to `default`. */\n vault?: string\n}\n\n/**\n * Result returned to callers (the bin entry uses this to format output;\n * tests assert on the file paths).\n */\nexport interface AddCollectionResult {\n /** Files written, in the order they were created. */\n files: string[]\n}\n\n/**\n * Lowercase identifier check, narrower than the project-name check —\n * collection names become Vue component names, Pinia store IDs, AND TS\n * symbols, so we keep them simple: letters, digits, hyphens, must start\n * with a letter.\n */\nexport function validateCollectionName(name: string): string | null {\n if (!name) return 'Collection name is required'\n if (!/^[a-z][a-z0-9-]*$/.test(name)) {\n return 'Collection name must start with a lowercase letter and contain only lowercase letters, digits, or hyphens'\n }\n return null\n}\n\nexport async function addCollection(\n options: AddCollectionOptions,\n): Promise<AddCollectionResult> {\n const err = validateCollectionName(options.name)\n if (err) throw new Error(err)\n\n const cwd = options.cwd ?? process.cwd()\n const vault = options.vault ?? 'default'\n const name = options.name\n\n // Convert kebab-case to PascalCase for the TS interface name and the\n // Vue helper variable.\n const PascalName = name\n .split('-')\n .map((s) => (s.length === 0 ? s : (s[0]?.toUpperCase() ?? '') + s.slice(1)))\n .join('')\n const useFnName = `use${PascalName}`\n\n const storePath = path.join(cwd, 'app', 'stores', `${name}.ts`)\n const pagePath = path.join(cwd, 'app', 'pages', `${name}.vue`)\n\n // Refuse to overwrite. Doing both checks before writing either file\n // means a partial write is impossible — either both files land or\n // neither does.\n for (const target of [storePath, pagePath]) {\n if (await pathExists(target)) {\n throw new Error(`Refusing to overwrite existing file: ${target}`)\n }\n }\n\n await fs.mkdir(path.dirname(storePath), { recursive: true })\n await fs.mkdir(path.dirname(pagePath), { recursive: true })\n\n await fs.writeFile(storePath, renderStore(name, PascalName, useFnName, vault), 'utf8')\n await fs.writeFile(pagePath, renderPage(name, PascalName, useFnName), 'utf8')\n\n return { files: [storePath, pagePath] }\n}\n\nasync function pathExists(p: string): Promise<boolean> {\n try {\n await fs.access(p)\n return true\n } catch {\n return false\n }\n}\n\n/**\n * Renders the `app/stores/<name>.ts` file. The interface is intentionally\n * minimal — `id` and `name` are the only fields. The user is expected to\n * extend it after generation; we'd rather not lock them into a domain\n * shape we guessed.\n */\nfunction renderStore(\n name: string,\n PascalName: string,\n useFnName: string,\n vault: string,\n): string {\n return `// Generated by \\`noy-db add ${name}\\`.\n//\n// Edit the ${PascalName} interface to match your domain, then call\n// \\`${useFnName}()\\` from any component.\n//\n// defineNoydbStore is auto-imported by @noy-db/in-nuxt. The vault\n// id is the tenant/company namespace — change it if you have multiple.\n\nexport interface ${PascalName} {\n id: string\n name: string\n}\n\nexport const ${useFnName} = defineNoydbStore<${PascalName}>('${name}', {\n vault: '${vault}',\n})\n`\n}\n\n/**\n * Renders the `app/pages/<name>.vue` page — list + add + delete. Picks\n * the simplest possible template-renderer pattern (`v-for`, no fancy\n * components) so the generated file is easy to read and modify.\n */\nfunction renderPage(name: string, PascalName: string, useFnName: string): string {\n return `<!--\n Generated by \\`noy-db add ${name}\\`.\n Visit /${name} in your dev server.\n-->\n<script setup lang=\"ts\">\nconst ${name} = ${useFnName}()\nawait ${name}.$ready\n\nfunction addOne() {\n ${name}.add({\n id: crypto.randomUUID(),\n name: 'New ${PascalName}',\n })\n}\n\nfunction removeOne(id: string) {\n ${name}.remove(id)\n}\n</script>\n\n<template>\n <main>\n <h1>${PascalName}</h1>\n <button @click=\"addOne\">Add ${PascalName}</button>\n <ul>\n <li v-for=\"item in ${name}.items\" :key=\"item.id\">\n {{ item.name }}\n <button @click=\"removeOne(item.id)\">Delete</button>\n </li>\n </ul>\n </main>\n</template>\n`\n}\n","/**\n * `noy-db verify` — end-to-end integrity check.\n *\n * Opens an in-memory NOYDB instance, writes a record, reads it back,\n * decrypts it, and asserts the round-trip is byte-identical. The check\n * exercises the full crypto path (PBKDF2 → KEK → DEK → AES-GCM) without\n * touching any user data on disk.\n *\n * Why an in-memory check is the right scope:\n * - It validates that @noy-db/core, @noy-db/memory, and the user's\n * installed Node version all agree on Web Crypto. That's the most\n * common silent failure for first-time installers.\n * - It cannot accidentally corrupt user data because there isn't any.\n * - It runs in well under one second, so users actually run it.\n *\n * What this command does NOT do (intentionally):\n * - Open the user's actual vault file/dynamo/s3/browser store.\n * That requires the user's passphrase — not something we want a CLI\n * `verify` command to prompt for. The full passphrase-driven verify\n * belongs in `nuxi noydb verify` once the auth story for CLIs lands\n * in. For now `noy-db verify` is the dependency-graph smoke test.\n */\n\nimport { createNoydb } from '@noy-db/hub'\nimport { memory } from '@noy-db/to-memory'\n\nexport interface VerifyResult {\n /** `true` if the round-trip succeeded; `false` if anything diverged. */\n ok: boolean\n /** Human-readable status. Always set, even on success. */\n message: string\n /** Wall-clock time the integrity check took, in ms. */\n durationMs: number\n}\n\n/**\n * Runs the end-to-end check. Pure function — no console output, no\n * `process.exit`. The bin wrapper handles formatting and exit codes so\n * the function is trivial to call from tests.\n */\nexport async function verifyIntegrity(): Promise<VerifyResult> {\n const start = performance.now()\n try {\n const db = await createNoydb({\n store: memory(),\n user: 'noy-db-verify',\n // The passphrase here is throwaway — the in-memory adapter never\n // persists anything, and the KEK is destroyed when we call close()\n // a few lines down. We use a non-trivial value just to exercise\n // PBKDF2 properly.\n secret: 'noy-db-verify-passphrase-2026',\n })\n const company = await db.openVault('verify-co')\n const collection = company.collection<{ id: string; n: number }>('verify')\n\n // Round-trip a single record. We pick a value that's small enough\n // to print on failure but large enough to ensure encryption isn't\n // accidentally a no-op.\n const original = { id: 'verify-1', n: 42 }\n await collection.put('verify-1', original)\n const got = await collection.get('verify-1')\n if (!got || got.id !== original.id || got.n !== original.n) {\n return fail(start, `Round-trip mismatch: got ${JSON.stringify(got)}`)\n }\n\n // Make sure the query DSL works too — this catches the case where\n // the user's @noy-db/core install is at (no query DSL) but the\n // CLI was updated to.\n const found = collection.query().where('n', '==', 42).toArray()\n if (found.length !== 1) {\n return fail(start, `Query DSL mismatch: expected 1 result, got ${found.length}`)\n }\n\n db.close()\n\n return {\n ok: true,\n message: 'noy-db integrity check passed',\n durationMs: Math.round(performance.now() - start),\n }\n } catch (err) {\n return fail(start, `Integrity check threw: ${(err as Error).message}`)\n }\n}\n\nfunction fail(start: number, message: string): VerifyResult {\n return {\n ok: false,\n message,\n durationMs: Math.round(performance.now() - start),\n }\n}\n","/**\n * `noy-db rotate` — rotate the DEKs for one or more collections in\n * a vault.\n *\n * What it does\n * ------------\n * For each target collection:\n *\n * 1. Generate a fresh DEK\n * 2. Decrypt every record with the old DEK\n * 3. Re-encrypt every record with the new DEK\n * 4. Re-wrap the new DEK into every remaining user's keyring\n *\n * The old DEKs become unreachable as soon as the keyring files are\n * updated. This is the \"just rotate\" path — nobody is revoked,\n * everybody keeps their current permissions, but the key material\n * is replaced.\n *\n * Why expose this as a CLI command\n * --------------------------------\n * Two real-world scenarios:\n *\n * 1. **Suspected key leak.** An operator lost a laptop, a\n * developer accidentally pasted a passphrase into a Slack\n * channel, a USB stick went missing. Even if you think the\n * passphrase is safe, rotating is cheap insurance.\n *\n * 2. **Scheduled rotation.** Some compliance regimes require\n * periodic key rotation regardless of exposure. A CLI makes\n * this scriptable from cron or a CI job.\n *\n * This module is test-first: all inputs are plain options, the\n * passphrase reader is injected, and the Noydb factory is\n * injectable. The production bin is a thin wrapper that defaults\n * those injections to their real implementations.\n */\n\nimport { withTeam } from '@noy-db/hub/team'\nimport { createNoydb, type Noydb, type NoydbStore } from '@noy-db/hub'\nimport { jsonFile } from '@noy-db/to-file'\nimport type { ReadPassphrase } from './shared.js'\nimport { defaultReadPassphrase } from './shared.js'\n\nexport interface RotateOptions {\n /** Directory containing the vault data (file adapter only). */\n dir: string\n /** Vault (tenant) name to rotate keys in. */\n vault: string\n /** The user id of the operator running the rotate. */\n user: string\n /**\n * Explicit list of collections to rotate. When undefined, the\n * rotation targets every collection the user has a DEK for —\n * resolved at run time by reading the vault snapshot.\n */\n collections?: string[]\n /** Injected passphrase reader. Defaults to the clack implementation. */\n readPassphrase?: ReadPassphrase\n /**\n * Injected Noydb factory. Production code leaves this undefined\n * and gets `createNoydb`; tests pass a constructor that builds\n * against an in-memory adapter.\n */\n createDb?: typeof createNoydb\n /**\n * Injected adapter factory. Production code leaves this undefined\n * and gets `jsonFile`; tests pass one that returns the shared\n * in-memory adapter their fixture used.\n */\n buildAdapter?: (dir: string) => NoydbStore\n}\n\nexport interface RotateResult {\n /** The collections that were actually rotated. */\n rotated: string[]\n}\n\n/**\n * Run the rotate flow against a file-adapter vault. Returns\n * the list of collections that were rotated so callers can display\n * it to the user.\n *\n * Throws `Error` on any auth/adapter/rotate failure. The bin\n * catches these and prints a friendly message; direct callers\n * (tests) can inspect the error message to assert specific\n * failure modes.\n */\nexport async function rotate(options: RotateOptions): Promise<RotateResult> {\n const readPassphrase = options.readPassphrase ?? defaultReadPassphrase\n const buildAdapter = options.buildAdapter ?? ((dir) => jsonFile({ dir }))\n const createDb = options.createDb ?? createNoydb\n\n // Read the passphrase BEFORE opening the database. This way a\n // cancelled prompt (Ctrl-C at the password entry) leaves the\n // adapter completely untouched — no files opened, no locks held.\n const secret = await readPassphrase(`Passphrase for ${options.user}`)\n\n let db: Noydb | null = null\n try {\n db = await createDb({\n store: buildAdapter(options.dir),\n user: options.user,\n secret,\n // Key rotation is the opt-in team capability (#267).\n teamStrategy: withTeam(),\n })\n\n // Resolve \"all collections\" by asking the vault. This\n // happens BEFORE rotate() is called, so the list is stable\n // across the operation — adding a new collection mid-rotate\n // would be a race we're not guarding against (single-writer\n // assumption applies).\n const vault = await db.openVault(options.vault)\n const targets = options.collections && options.collections.length > 0\n ? options.collections\n : await vault.collections()\n\n if (targets.length === 0) {\n throw new Error(\n `Vault \"${options.vault}\" has no collections to rotate.`,\n )\n }\n\n await db.rotate(options.vault, targets)\n return { rotated: targets }\n } finally {\n // Always close the DB on exit — success or failure. Close()\n // clears the KEK and DEKs from process memory, which is the\n // final line of defense if the passphrase somehow leaked\n // into a log line above this block.\n db?.close()\n }\n}\n","/**\n * Shared primitives for the interactive `noy-db` subcommands that\n * need to unlock a real vault.\n *\n * Three things live here:\n *\n * 1. `ReadPassphrase` — a tiny interface for \"prompt the user for\n * a passphrase\", with a test-friendly default. Subcommands take\n * this as an injected dependency so tests can short-circuit\n * the prompt without spawning a pty.\n *\n * 2. `defaultReadPassphrase` — the production implementation,\n * built on `@clack/prompts` `password()`. Never echoes the\n * value to the terminal, never logs it, clears it from the\n * returned promise after the caller consumes it.\n *\n * 3. `assertRole` — narrow unknown string input to the Role type\n * with a consistent error message.\n *\n * ## Why pull this out\n *\n * `rotate`, `addUser`, and `backup` all need the same \"prompt for\n * a passphrase\" shape and the same \"open a file adapter and get\n * back a Noydb instance\" shape. Duplicating it in three files would\n * drift over time; centralizing means one place to audit the\n * passphrase-handling contract (never log, never persist, clear\n * local variables after use).\n */\n\nimport { password, isCancel, cancel } from '@clack/prompts'\nimport type { Role } from '@noy-db/hub'\n\nconst VALID_ROLES = ['owner', 'admin', 'operator', 'viewer', 'client'] as const\n\n/**\n * Asynchronous passphrase reader. Production code passes\n * `defaultReadPassphrase`; tests pass a stub that returns a fixed\n * string without touching stdin.\n *\n * The `label` is shown to the user as the prompt message. It\n * should never contain the expected passphrase or any secret.\n */\nexport type ReadPassphrase = (label: string) => Promise<string>\n\n/**\n * Clack-based passphrase prompt. Cancellation (Ctrl-C) aborts the\n * process with exit code 1 — prompts are always the first thing to\n * fire in a subcommand, so aborting here doesn't leave the system\n * in a half-mutated state.\n */\nexport const defaultReadPassphrase: ReadPassphrase = async (label) => {\n const value = await password({\n message: label,\n // Basic sanity: reject empty strings up front. We don't enforce\n // length here because the caller's KEK-derivation step will\n // reject weak passphrases with its own, richer error.\n validate: (v) => (v.length === 0 ? 'Passphrase cannot be empty' : undefined),\n })\n if (isCancel(value)) {\n cancel('Cancelled.')\n process.exit(1)\n }\n return value\n}\n\n/**\n * Narrow an unknown string to the `Role` type from @noy-db/core.\n * Used by the `add user` subcommand to validate the role argument\n * before passing it to `noydb.grant()`.\n */\nexport function assertRole(input: string): Role {\n if (!(VALID_ROLES as readonly string[]).includes(input)) {\n throw new Error(\n `Invalid role \"${input}\" — must be one of: ${VALID_ROLES.join(', ')}`,\n )\n }\n return input as Role\n}\n\n/**\n * Split a comma-separated collection list into an array of names,\n * trimming whitespace and dropping empties. Returns null if the\n * input itself is empty or undefined — the caller decides whether\n * that means \"all collections\" or \"error\".\n */\nexport function parseCollectionList(input: string | undefined): string[] | null {\n if (!input) return null\n const parts = input\n .split(',')\n .map((s) => s.trim())\n .filter((s) => s.length > 0)\n return parts.length > 0 ? parts : null\n}\n","/**\n * `noy-db add user <id> <role>` — grant a new user access to a\n * vault.\n *\n * What it does\n * ------------\n * Wraps `noydb.grant()` in the CLI's auth-prompt ritual:\n *\n * 1. Prompt the caller for their own passphrase (to unlock the\n * caller's keyring and derive the wrapping key).\n * 2. Prompt for the new user's passphrase.\n * 3. Prompt for confirmation of the new passphrase.\n * 4. Reject on mismatch.\n * 5. Call `noydb.grant(vault, { userId, role, passphrase, permissions })`.\n *\n * For owner/admin/viewer roles, every collection is granted\n * automatically (the core keyring.ts grant logic handles that via\n * the `permissions` field). For operator/client, the caller must\n * pass a `--collections` list because those roles need explicit\n * per-collection permissions.\n *\n * ## What this does NOT do\n *\n * - No email/invite flow — is about local-CLI key management,\n * not out-of-band user enrollment.\n * - No rollback on partial failure — `grant()` is atomic at the\n * core level (keyring file writes last, after DEK wrapping), so\n * partial-state-on-crash is already handled.\n */\n\nimport { withTeam } from '@noy-db/hub/team'\nimport { createNoydb, type Noydb, type NoydbStore, type Role } from '@noy-db/hub'\nimport { jsonFile } from '@noy-db/to-file'\nimport type { ReadPassphrase } from './shared.js'\nimport { defaultReadPassphrase } from './shared.js'\n\nexport interface AddUserOptions {\n /** Directory containing the vault data (file adapter only). */\n dir: string\n /** Vault (tenant) name to grant access to. */\n vault: string\n /** The user id of the caller running the grant. */\n callerUser: string\n /** The new user's id (must not already exist in the vault keyring). */\n newUserId: string\n /** The new user's display name — shown in UI and audit logs. Defaults to `newUserId`. */\n newUserDisplayName?: string\n /** The new user's role. */\n role: Role\n /**\n * Per-collection permissions. Required when `role` is operator or\n * client; ignored for owner/admin/viewer (they get everything\n * via the core's resolvePermissions logic).\n *\n * Shape: `{ invoices: 'rw', clients: 'ro' }`. CLI callers pass\n * `--collections invoices:rw,clients:ro` and the argv parser\n * converts it to this shape.\n */\n permissions?: Record<string, 'rw' | 'ro'>\n /** Injected passphrase reader. Defaults to the clack implementation. */\n readPassphrase?: ReadPassphrase\n /** Injected Noydb factory. */\n createDb?: typeof createNoydb\n /** Injected adapter factory. */\n buildAdapter?: (dir: string) => NoydbStore\n}\n\nexport interface AddUserResult {\n /** The userId that was granted access. */\n userId: string\n /** The role they were granted. */\n role: Role\n}\n\n/**\n * Run the grant flow. Two passphrase prompts: caller's, then new\n * user's (twice for confirmation). Calls `noydb.grant()` with the\n * collected values.\n */\nexport async function addUser(options: AddUserOptions): Promise<AddUserResult> {\n const readPassphrase = options.readPassphrase ?? defaultReadPassphrase\n const buildAdapter = options.buildAdapter ?? ((dir) => jsonFile({ dir }))\n const createDb = options.createDb ?? createNoydb\n\n // Operator/client roles NEED explicit permissions. Reject here\n // rather than in the middle of the grant, so the caller sees the\n // problem before any I/O happens.\n if (\n (options.role === 'operator' || options.role === 'client') &&\n (!options.permissions || Object.keys(options.permissions).length === 0)\n ) {\n throw new Error(\n `Role \"${options.role}\" requires explicit --collections — e.g. --collections invoices:rw,clients:ro`,\n )\n }\n\n const callerSecret = await readPassphrase(\n `Your passphrase (${options.callerUser})`,\n )\n const newSecret = await readPassphrase(\n `New passphrase for ${options.newUserId}`,\n )\n const confirmSecret = await readPassphrase(\n `Confirm passphrase for ${options.newUserId}`,\n )\n\n if (newSecret !== confirmSecret) {\n throw new Error(`Passphrases do not match — grant aborted.`)\n }\n\n let db: Noydb | null = null\n try {\n db = await createDb({\n store: buildAdapter(options.dir),\n user: options.callerUser,\n secret: callerSecret,\n // Multi-user grant is the opt-in team capability (#267) — the CLI\n // add-user command is by definition a multi-user operation.\n teamStrategy: withTeam(),\n })\n\n // Build the grant options. Only include `permissions` when the\n // caller actually supplied them — otherwise the core's\n // resolvePermissions fills in the role defaults. The spread\n // (rather than post-assignment) keeps the object literal\n // compatible with `GrantOptions`'s readonly `permissions`.\n const grantOpts: Parameters<Noydb['grant']>[1] = {\n userId: options.newUserId,\n displayName: options.newUserDisplayName ?? options.newUserId,\n role: options.role,\n passphrase: newSecret,\n ...(options.permissions ? { permissions: options.permissions } : {}),\n }\n\n await db.grant(options.vault, grantOpts)\n\n return {\n userId: options.newUserId,\n role: options.role,\n }\n } finally {\n db?.close()\n }\n}\n","/**\n * `noy-db backup <target>` — dump a vault to a local file.\n *\n * What it does\n * ------------\n * Wraps `vault.dump()` in the CLI's auth-prompt ritual, then\n * writes the serialized backup to the requested path. As of,\n * `dump()` already produces a verifiable backup (embedded\n * ledgerHead, full `_ledger` / `_ledger_deltas` snapshots) — the\n * CLI just moves bytes; the integrity guarantees come from core.\n *\n * ## Target URI support\n *\n * ships **`file://` only** (or a plain filesystem path).\n * The issue spec originally called for `s3://` as well, but\n * wiring @aws-sdk into @noy-db/create would defeat the\n * zero-runtime-deps story for the CLI package. S3 backup is\n * deferred to a follow-up that can live in @noy-db/s3-cli or a\n * similar optional companion package.\n *\n * Accepted forms:\n * - `file:///absolute/path.json`\n * - `file://./relative/path.json`\n * - `/absolute/path.json` (treated as `file://`)\n * - `./relative/path.json` (treated as `file://`)\n *\n * ## What this does NOT do\n *\n * - No encryption of the backup BEYOND what noy-db already does.\n * The dumped file is a valid noy-db backup, which means\n * individual records are still encrypted but the keyring is\n * included (wrapped with each user's KEK). Anyone who loads\n * the backup still needs the correct passphrase to read.\n * - No restore — that's a separate subcommand tracked as a\n * follow-up. For now users can restore via\n * `vault.load(backupString)` from their own app code.\n */\n\nimport { promises as fs } from 'node:fs'\nimport path from 'node:path'\nimport { createNoydb, type Noydb, type NoydbStore } from '@noy-db/hub'\nimport { withHistory } from '@noy-db/hub/history'\nimport { jsonFile } from '@noy-db/to-file'\nimport type { ReadPassphrase } from './shared.js'\nimport { defaultReadPassphrase } from './shared.js'\n\nexport interface BackupOptions {\n /** Directory containing the vault data (file adapter only). */\n dir: string\n /** Vault (tenant) name to back up. */\n vault: string\n /** The user id of the operator running the backup. */\n user: string\n /**\n * Where to write the backup. Accepts a `file://` URI or a plain\n * filesystem path. Relative paths resolve against `process.cwd()`.\n */\n target: string\n /** Injected passphrase reader. */\n readPassphrase?: ReadPassphrase\n /** Injected Noydb factory. */\n createDb?: typeof createNoydb\n /** Injected adapter factory. */\n buildAdapter?: (dir: string) => NoydbStore\n}\n\nexport interface BackupResult {\n /** Absolute filesystem path the backup was written to. */\n path: string\n /** Size of the serialized backup in bytes. */\n bytes: number\n}\n\n/**\n * Parse a backup target into an absolute filesystem path. Rejects\n * unsupported URI schemes (s3://, https://, etc.) early so the\n * caller doesn't silently write to the wrong place.\n */\nexport function resolveBackupTarget(target: string, cwd: string = process.cwd()): string {\n // Strip the `file://` prefix if present. The rest of the string\n // is treated as a filesystem path. We accept both `file:///abs`\n // (three slashes, absolute) and `file://./rel` (two slashes,\n // relative) because real-world users write both.\n let raw = target\n if (target.startsWith('file://')) {\n raw = target.slice('file://'.length)\n } else if (target.includes('://')) {\n // Any other scheme is unsupported.\n throw new Error(\n `Unsupported backup target scheme: \"${target.split('://')[0]}://\". ` +\n `Only file:// and plain filesystem paths are supported in. ` +\n `S3 backups will land in a follow-up.`,\n )\n }\n return path.resolve(cwd, raw)\n}\n\nexport async function backup(options: BackupOptions): Promise<BackupResult> {\n const readPassphrase = options.readPassphrase ?? defaultReadPassphrase\n const buildAdapter = options.buildAdapter ?? ((dir) => jsonFile({ dir }))\n const createDb = options.createDb ?? createNoydb\n\n // Resolve the target FIRST so a bad URI fails before any\n // passphrase is collected. This keeps the UX clean: a typo in\n // `s3://bucket/x` rejects without asking for a secret the user\n // would then have to type again.\n const absolutePath = resolveBackupTarget(options.target)\n\n const secret = await readPassphrase(`Passphrase for ${options.user}`)\n\n let db: Noydb | null = null\n try {\n db = await createDb({\n store: buildAdapter(options.dir),\n user: options.user,\n secret,\n // Opt into the history strategy so `vault.dump()` embeds\n // `ledgerHead` for tamper-evident verification at restore.\n // Source vaults that were never written with history simply\n // have an empty ledger; the embedded head is omitted in that\n // case and the backup is treated as legacy on load.\n historyStrategy: withHistory(),\n })\n const vault = await db.openVault(options.vault)\n const serialized = await vault.dump()\n\n // Make sure the parent directory exists. If the user passed\n // `./backups/2026/demo.json` and `./backups/2026` doesn't\n // exist yet, we create it. This is the common case for\n // scripted rotations dropping into a date-based folder.\n await fs.mkdir(path.dirname(absolutePath), { recursive: true })\n await fs.writeFile(absolutePath, serialized, 'utf8')\n\n return {\n path: absolutePath,\n bytes: Buffer.byteLength(serialized, 'utf8'),\n }\n } finally {\n db?.close()\n }\n}\n"],"mappings":"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","names":["fs","path","fs","path","fs","path","fs","fs","path","pathExists","p","createNoydb","isCancel","cancel","createNoydb","withTeam","createNoydb","jsonFile","jsonFile","createNoydb","withTeam","fs","path","createNoydb","jsonFile","path","jsonFile","createNoydb","fs"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-noy-db",
3
- "version": "0.3.0-pre.2",
3
+ "version": "0.3.0-pre.4",
4
4
  "description": "Wizard + CLI tool for noy-db: scaffold a fresh Nuxt 4 + Pinia encrypted store, or add collections to an existing project. Invoke via `npm create @noy-db`.",
5
5
  "license": "MIT",
6
6
  "author": "vLannaAi <vicio@lanna.ai>",
@@ -40,9 +40,9 @@
40
40
  "picocolors": "^1.1.1",
41
41
  "magicast": "^0.3.5",
42
42
  "diff": "^7.0.0",
43
- "@noy-db/hub": "0.3.0-pre.2",
44
- "@noy-db/to-memory": "0.3.0-pre.2",
45
- "@noy-db/to-file": "0.3.0-pre.2"
43
+ "@noy-db/to-memory": "0.3.0-pre.4",
44
+ "@noy-db/hub": "0.3.0-pre.4",
45
+ "@noy-db/to-file": "0.3.0-pre.4"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/node": "^22.0.0",