@noy-db/create 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +283 -0
- package/dist/bin/create.d.ts +1 -0
- package/dist/bin/create.js +724 -0
- package/dist/bin/create.js.map +1 -0
- package/dist/bin/noy-db.d.ts +1 -0
- package/dist/bin/noy-db.js +548 -0
- package/dist/bin/noy-db.js.map +1 -0
- package/dist/index.d.ts +665 -0
- package/dist/index.js +902 -0
- package/dist/index.js.map +1 -0
- package/package.json +70 -0
- package/templates/nuxt-default/README.md +38 -0
- package/templates/nuxt-default/_gitignore +32 -0
- package/templates/nuxt-default/app/app.vue +37 -0
- package/templates/nuxt-default/app/pages/index.vue +21 -0
- package/templates/nuxt-default/app/pages/invoices.vue +62 -0
- package/templates/nuxt-default/app/stores/invoices.ts +23 -0
- package/templates/nuxt-default/nuxt.config.ts +30 -0
- package/templates/nuxt-default/package.json +28 -0
- package/templates/nuxt-default/tsconfig.json +3 -0
|
@@ -0,0 +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} 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 v0.3.1 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 v0.5, #37): patches the existing\n * `nuxt.config.ts` via magicast to add `@noy-db/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 v0.3.1 fresh-project path, factored out of the\n * main entry so the augment branch above can coexist. Behavior\n * is unchanged from v0.3.1 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 // ── 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('nuxt-default'),\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 },\n projectPath,\n files,\n }\n}\n\n/**\n * The new v0.5 augment-existing-project path (#37). 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/nuxt @noy-db/pinia @noy-db/core @noy-db/browser @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 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/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/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/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/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/nuxt')\n if (alreadyHasModule) {\n skipReasons.push('`@noy-db/nuxt` already in modules')\n } else {\n // Insert as a literal string so magicast writes `'@noy-db/nuxt'`\n // rather than an object wrapper.\n modules.push('@noy-db/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/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/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/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 compartment?: 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 compartment = options.compartment ?? '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, compartment), '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 compartment: 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/nuxt. The compartment\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 compartment: '${compartment}',\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 compartment 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 v0.4. For now `noy-db verify` is the dependency-graph smoke test.\n */\n\nimport { createNoydb } from '@noy-db/core'\nimport { memory } from '@noy-db/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 adapter: 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.openCompartment('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 v0.2 (no query DSL) but the\n // CLI was updated to v0.3.\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 compartment.\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 NoydbAdapter } from '@noy-db/core'\nimport { jsonFile } from '@noy-db/file'\nimport type { ReadPassphrase } from './shared.js'\nimport { defaultReadPassphrase } from './shared.js'\n\nexport interface RotateOptions {\n /** Directory containing the compartment data (file adapter only). */\n dir: string\n /** Compartment (tenant) name to rotate keys in. */\n compartment: 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 compartment 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) => NoydbAdapter\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 compartment. 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 adapter: buildAdapter(options.dir),\n user: options.user,\n secret,\n })\n\n // Resolve \"all collections\" by asking the compartment. 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 compartment = await db.openCompartment(options.compartment)\n const targets = options.collections && options.collections.length > 0\n ? options.collections\n : await compartment.collections()\n\n if (targets.length === 0) {\n throw new Error(\n `Compartment \"${options.compartment}\" has no collections to rotate.`,\n )\n }\n\n await db.rotate(options.compartment, 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 compartment.\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/core'\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 * compartment.\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(compartment, { 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 — v0.5 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 NoydbAdapter, type Role } from '@noy-db/core'\nimport { jsonFile } from '@noy-db/file'\nimport type { ReadPassphrase } from './shared.js'\nimport { defaultReadPassphrase } from './shared.js'\n\nexport interface AddUserOptions {\n /** Directory containing the compartment data (file adapter only). */\n dir: string\n /** Compartment (tenant) name to grant access to. */\n compartment: 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 compartment 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) => NoydbAdapter\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 adapter: 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.compartment, 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 compartment to a local file.\n *\n * What it does\n * ------------\n * Wraps `compartment.dump()` in the CLI's auth-prompt ritual, then\n * writes the serialized backup to the requested path. As of v0.4,\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 * v0.5.0 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 * `compartment.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 NoydbAdapter } from '@noy-db/core'\nimport { jsonFile } from '@noy-db/file'\nimport type { ReadPassphrase } from './shared.js'\nimport { defaultReadPassphrase } from './shared.js'\n\nexport interface BackupOptions {\n /** Directory containing the compartment data (file adapter only). */\n dir: string\n /** Compartment (tenant) name to back up. */\n compartment: 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) => NoydbAdapter\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 v0.5. ` +\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 adapter: buildAdapter(options.dir),\n user: options.user,\n secret,\n })\n const compartment = await db.openCompartment(options.compartment)\n const serialized = await compartment.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"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@noy-db/create",
|
|
3
|
+
"version": "0.5.0",
|
|
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
|
+
"license": "MIT",
|
|
6
|
+
"author": "vLannaAi <vicio@lanna.ai>",
|
|
7
|
+
"homepage": "https://github.com/vLannaAi/noy-db/tree/main/packages/create-noy-db#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/vLannaAi/noy-db.git",
|
|
11
|
+
"directory": "packages/create-noy-db"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/vLannaAi/noy-db/issues"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"bin": {
|
|
18
|
+
"create": "./dist/bin/create.js",
|
|
19
|
+
"noy-db": "./dist/bin/noy-db.js"
|
|
20
|
+
},
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"default": "./dist/index.js"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"main": "./dist/index.js",
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"templates",
|
|
32
|
+
"README.md",
|
|
33
|
+
"LICENSE"
|
|
34
|
+
],
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=20.0.0"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@clack/prompts": "^0.9.1",
|
|
40
|
+
"picocolors": "^1.1.1",
|
|
41
|
+
"magicast": "^0.3.5",
|
|
42
|
+
"diff": "^7.0.0",
|
|
43
|
+
"@noy-db/core": "0.5.0",
|
|
44
|
+
"@noy-db/file": "0.5.0",
|
|
45
|
+
"@noy-db/memory": "0.5.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^22.0.0",
|
|
49
|
+
"@types/diff": "^7.0.0"
|
|
50
|
+
},
|
|
51
|
+
"keywords": [
|
|
52
|
+
"noy-db",
|
|
53
|
+
"create",
|
|
54
|
+
"scaffolder",
|
|
55
|
+
"wizard",
|
|
56
|
+
"cli",
|
|
57
|
+
"nuxt",
|
|
58
|
+
"nuxt4",
|
|
59
|
+
"pinia",
|
|
60
|
+
"vue",
|
|
61
|
+
"encryption",
|
|
62
|
+
"zero-knowledge"
|
|
63
|
+
],
|
|
64
|
+
"scripts": {
|
|
65
|
+
"build": "tsup && node scripts/postbuild.mjs",
|
|
66
|
+
"test": "vitest run",
|
|
67
|
+
"lint": "eslint src/",
|
|
68
|
+
"typecheck": "tsc --noEmit"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# {{PROJECT_NAME}}
|
|
2
|
+
|
|
3
|
+
A Nuxt 4 + Pinia + [noy-db](https://github.com/vLannaAi/noy-db) starter, scaffolded by `create-noy-db`.
|
|
4
|
+
|
|
5
|
+
## Stack
|
|
6
|
+
|
|
7
|
+
- **Nuxt 4** — fullstack Vue framework
|
|
8
|
+
- **Pinia** — reactive state management
|
|
9
|
+
- **@noy-db/nuxt** — Nuxt module for noy-db (auto-imports, SSR-safe runtime, devtools tab)
|
|
10
|
+
- **@noy-db/pinia** — `defineNoydbStore` — drop-in `defineStore` replacement that wires a Pinia store to an encrypted compartment + collection
|
|
11
|
+
- **@noy-db/{{ADAPTER}}** — storage adapter
|
|
12
|
+
|
|
13
|
+
Everything stored is encrypted with AES-256-GCM before it touches the adapter. The adapter only ever sees ciphertext.
|
|
14
|
+
|
|
15
|
+
## Getting started
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pnpm install # or npm/yarn/bun
|
|
19
|
+
pnpm dev # nuxt dev on http://localhost:3000
|
|
20
|
+
pnpm build # production build
|
|
21
|
+
pnpm preview # preview the production build
|
|
22
|
+
pnpm verify # run the noy-db integrity check
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Adding a collection
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx noy-db add clients
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
This scaffolds `app/stores/clients.ts` and `app/pages/clients.vue`. Edit the generated `Client` interface to match your domain, then visit `/clients` in your dev server.
|
|
32
|
+
|
|
33
|
+
## Documentation
|
|
34
|
+
|
|
35
|
+
- [noy-db getting started](https://github.com/vLannaAi/noy-db/blob/main/docs/getting-started.md)
|
|
36
|
+
- [End-user features](https://github.com/vLannaAi/noy-db/blob/main/docs/end-user-features.md)
|
|
37
|
+
- [Architecture](https://github.com/vLannaAi/noy-db/blob/main/docs/architecture.md)
|
|
38
|
+
- [Roadmap](https://github.com/vLannaAi/noy-db/blob/main/ROADMAP.md)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Nuxt build outputs
|
|
2
|
+
.nuxt
|
|
3
|
+
.output
|
|
4
|
+
.data
|
|
5
|
+
.cache
|
|
6
|
+
.vite
|
|
7
|
+
|
|
8
|
+
# Node
|
|
9
|
+
node_modules
|
|
10
|
+
*.log
|
|
11
|
+
.npm
|
|
12
|
+
|
|
13
|
+
# Env files
|
|
14
|
+
.env
|
|
15
|
+
.env.local
|
|
16
|
+
.env.*.local
|
|
17
|
+
|
|
18
|
+
# OS
|
|
19
|
+
.DS_Store
|
|
20
|
+
Thumbs.db
|
|
21
|
+
|
|
22
|
+
# IDE
|
|
23
|
+
.vscode/*
|
|
24
|
+
!.vscode/extensions.json
|
|
25
|
+
.idea
|
|
26
|
+
*.suo
|
|
27
|
+
*.ntvs*
|
|
28
|
+
*.njsproj
|
|
29
|
+
*.sln
|
|
30
|
+
|
|
31
|
+
# Test coverage
|
|
32
|
+
coverage
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Top-level layout. Add navigation here as you grow the app.
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<template>
|
|
6
|
+
<div>
|
|
7
|
+
<header>
|
|
8
|
+
<nav>
|
|
9
|
+
<NuxtLink to="/">Home</NuxtLink>
|
|
10
|
+
<NuxtLink to="/invoices">Invoices</NuxtLink>
|
|
11
|
+
</nav>
|
|
12
|
+
</header>
|
|
13
|
+
<main>
|
|
14
|
+
<NuxtPage />
|
|
15
|
+
</main>
|
|
16
|
+
</div>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<style>
|
|
20
|
+
nav {
|
|
21
|
+
display: flex;
|
|
22
|
+
gap: 1rem;
|
|
23
|
+
padding: 1rem;
|
|
24
|
+
border-bottom: 1px solid #e5e5e5;
|
|
25
|
+
}
|
|
26
|
+
nav a {
|
|
27
|
+
text-decoration: none;
|
|
28
|
+
color: #0070f3;
|
|
29
|
+
}
|
|
30
|
+
nav a.router-link-active {
|
|
31
|
+
font-weight: bold;
|
|
32
|
+
}
|
|
33
|
+
main {
|
|
34
|
+
padding: 1rem;
|
|
35
|
+
font-family: system-ui, sans-serif;
|
|
36
|
+
}
|
|
37
|
+
</style>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Landing page for the generated noy-db app.
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<template>
|
|
6
|
+
<section>
|
|
7
|
+
<h1>{{PROJECT_NAME}}</h1>
|
|
8
|
+
<p>
|
|
9
|
+
Encrypted, offline-first, zero-knowledge document store powered by
|
|
10
|
+
<a href="https://github.com/vLannaAi/noy-db">noy-db</a>.
|
|
11
|
+
</p>
|
|
12
|
+
<p>
|
|
13
|
+
Visit <NuxtLink to="/invoices">/invoices</NuxtLink> to see the demo
|
|
14
|
+
invoices store.
|
|
15
|
+
</p>
|
|
16
|
+
<p>
|
|
17
|
+
Run <code>noy-db verify</code> at any time to check the integrity of
|
|
18
|
+
your noy-db install.
|
|
19
|
+
</p>
|
|
20
|
+
</section>
|
|
21
|
+
</template>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { DEFAULT_INVOICES } from '~/stores/invoices'
|
|
3
|
+
|
|
4
|
+
const invoices = useInvoices()
|
|
5
|
+
await invoices.$ready
|
|
6
|
+
|
|
7
|
+
// Seed on first run: if the store is empty and there are defaults, populate.
|
|
8
|
+
// This runs once on component mount; later visits see whatever the user
|
|
9
|
+
// has edited.
|
|
10
|
+
if (invoices.items.length === 0 && DEFAULT_INVOICES.length > 0) {
|
|
11
|
+
for (const inv of DEFAULT_INVOICES) {
|
|
12
|
+
invoices.add(inv)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Reactive query: re-runs whenever the underlying collection changes.
|
|
17
|
+
const open = invoices.query()
|
|
18
|
+
.where('status', '==', 'open')
|
|
19
|
+
.live()
|
|
20
|
+
|
|
21
|
+
function addDraft() {
|
|
22
|
+
invoices.add({
|
|
23
|
+
id: crypto.randomUUID(),
|
|
24
|
+
client: 'New Client',
|
|
25
|
+
amount: Math.round(Math.random() * 10000),
|
|
26
|
+
status: 'draft',
|
|
27
|
+
dueDate: new Date().toISOString().slice(0, 10),
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function remove(id: string) {
|
|
32
|
+
invoices.remove(id)
|
|
33
|
+
}
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<template>
|
|
37
|
+
<section>
|
|
38
|
+
<h1>Invoices</h1>
|
|
39
|
+
<button @click="addDraft">+ New draft</button>
|
|
40
|
+
<p><strong>{{ open.length }}</strong> open invoice(s)</p>
|
|
41
|
+
<ul>
|
|
42
|
+
<li v-for="inv in invoices.items" :key="inv.id">
|
|
43
|
+
<strong>{{ inv.client }}</strong> — {{ inv.amount }} ({{ inv.status }})
|
|
44
|
+
<button @click="remove(inv.id)">Delete</button>
|
|
45
|
+
</li>
|
|
46
|
+
</ul>
|
|
47
|
+
<p v-if="invoices.items.length === 0">
|
|
48
|
+
No invoices yet — click "New draft" to add one.
|
|
49
|
+
</p>
|
|
50
|
+
</section>
|
|
51
|
+
</template>
|
|
52
|
+
|
|
53
|
+
<style scoped>
|
|
54
|
+
button {
|
|
55
|
+
cursor: pointer;
|
|
56
|
+
padding: 0.25rem 0.75rem;
|
|
57
|
+
margin: 0.25rem;
|
|
58
|
+
}
|
|
59
|
+
li {
|
|
60
|
+
padding: 0.5rem 0;
|
|
61
|
+
}
|
|
62
|
+
</style>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Generated by `create-noy-db`.
|
|
2
|
+
//
|
|
3
|
+
// `defineNoydbStore` is auto-imported by @noy-db/nuxt. The store wires
|
|
4
|
+
// a Pinia store to a noy-db compartment + collection. Encryption,
|
|
5
|
+
// keyring management, and adapter wiring are invisible to components.
|
|
6
|
+
|
|
7
|
+
export interface Invoice {
|
|
8
|
+
id: string
|
|
9
|
+
client: string
|
|
10
|
+
amount: number
|
|
11
|
+
status: 'draft' | 'open' | 'paid' | 'overdue'
|
|
12
|
+
dueDate: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const useInvoices = defineNoydbStore<Invoice>('invoices', {
|
|
16
|
+
compartment: 'demo-co',
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Default seed records rendered into the store on first mount (see
|
|
21
|
+
* `app/pages/invoices.vue`). An empty array means the page starts empty.
|
|
22
|
+
*/
|
|
23
|
+
export const DEFAULT_INVOICES: Invoice[] = {{SEED_INVOICES}}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Generated by `create-noy-db`.
|
|
2
|
+
//
|
|
3
|
+
// The `noydb:` key is fully typed via @noy-db/nuxt's module augmentation
|
|
4
|
+
// of @nuxt/schema. Hover any field for documentation.
|
|
5
|
+
|
|
6
|
+
export default defineNuxtConfig({
|
|
7
|
+
compatibilityDate: '2026-04-01',
|
|
8
|
+
|
|
9
|
+
modules: [
|
|
10
|
+
'@pinia/nuxt',
|
|
11
|
+
'@noy-db/nuxt',
|
|
12
|
+
],
|
|
13
|
+
|
|
14
|
+
noydb: {
|
|
15
|
+
adapter: '{{ADAPTER}}',
|
|
16
|
+
pinia: true,
|
|
17
|
+
devtools: {{DEVTOOLS}},
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
devtools: {
|
|
21
|
+
enabled: process.env['NODE_ENV'] !== 'production',
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
telemetry: false,
|
|
25
|
+
|
|
26
|
+
typescript: {
|
|
27
|
+
strict: true,
|
|
28
|
+
typeCheck: false,
|
|
29
|
+
},
|
|
30
|
+
})
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PROJECT_NAME}}",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "nuxt dev",
|
|
8
|
+
"build": "nuxt build",
|
|
9
|
+
"preview": "nuxt preview",
|
|
10
|
+
"verify": "noy-db verify"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@noy-db/browser": "^0.5.0",
|
|
14
|
+
"@noy-db/core": "^0.5.0",
|
|
15
|
+
"@noy-db/file": "^0.5.0",
|
|
16
|
+
"@noy-db/memory": "^0.5.0",
|
|
17
|
+
"@noy-db/nuxt": "^0.5.0",
|
|
18
|
+
"@noy-db/pinia": "^0.5.0",
|
|
19
|
+
"@noy-db/vue": "^0.5.0",
|
|
20
|
+
"@pinia/nuxt": "^0.11.0",
|
|
21
|
+
"nuxt": "^4.4.2",
|
|
22
|
+
"pinia": "^3.0.1",
|
|
23
|
+
"vue": "^3.5.32"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@noy-db/create": "^0.5.0"
|
|
27
|
+
}
|
|
28
|
+
}
|