@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.
@@ -0,0 +1,724 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/bin/create.ts
4
+ import * as p2 from "@clack/prompts";
5
+ import pc2 from "picocolors";
6
+
7
+ // src/wizard/run.ts
8
+ import { promises as fs4 } from "fs";
9
+ import path3 from "path";
10
+ import * as p from "@clack/prompts";
11
+ import pc from "picocolors";
12
+
13
+ // src/wizard/render.ts
14
+ import { promises as fs } from "fs";
15
+ import path from "path";
16
+ import { fileURLToPath } from "url";
17
+ async function renderTemplate(src, dest, tokens) {
18
+ const written = [];
19
+ await walk(src, dest, "", tokens, written);
20
+ written.sort();
21
+ return written;
22
+ }
23
+ async function walk(srcRoot, destRoot, rel, tokens, written) {
24
+ const srcDir = path.join(srcRoot, rel);
25
+ const entries = await fs.readdir(srcDir, { withFileTypes: true });
26
+ for (const entry of entries) {
27
+ const srcEntry = path.join(srcDir, entry.name);
28
+ const destName = entry.name.startsWith("_") ? `.${entry.name.slice(1)}` : entry.name;
29
+ const destRel = rel ? path.join(rel, destName) : destName;
30
+ const destEntry = path.join(destRoot, destRel);
31
+ if (entry.isDirectory()) {
32
+ await fs.mkdir(destEntry, { recursive: true });
33
+ await walk(srcRoot, destRoot, path.join(rel, entry.name), tokens, written);
34
+ continue;
35
+ }
36
+ const raw = await fs.readFile(srcEntry, "utf8");
37
+ const rendered = applyTokens(raw, tokens);
38
+ await fs.mkdir(path.dirname(destEntry), { recursive: true });
39
+ await fs.writeFile(destEntry, rendered, "utf8");
40
+ written.push(destRel);
41
+ }
42
+ }
43
+ function applyTokens(input, tokens) {
44
+ const bag = tokens;
45
+ return input.replace(/\{\{(\w+)\}\}/g, (match, key) => {
46
+ const value = bag[key];
47
+ return value === void 0 ? match : value;
48
+ });
49
+ }
50
+ function templateDir(name) {
51
+ const here = fileURLToPath(import.meta.url);
52
+ const packageRoot = path.resolve(path.dirname(here), "..", "..");
53
+ return path.join(packageRoot, "templates", name);
54
+ }
55
+
56
+ // src/wizard/detect.ts
57
+ import { promises as fs2 } from "fs";
58
+ import path2 from "path";
59
+ async function detectNuxtProject(cwd) {
60
+ const reasons = [];
61
+ const configCandidates = ["nuxt.config.ts", "nuxt.config.js", "nuxt.config.mjs"];
62
+ let configPath = null;
63
+ for (const name of configCandidates) {
64
+ const candidate = path2.join(cwd, name);
65
+ if (await pathExists(candidate)) {
66
+ configPath = candidate;
67
+ reasons.push(`Found ${name}`);
68
+ break;
69
+ }
70
+ }
71
+ if (!configPath) {
72
+ reasons.push("No nuxt.config.{ts,js,mjs} in cwd");
73
+ return {
74
+ existing: false,
75
+ configPath: null,
76
+ packageJsonPath: null,
77
+ reasons
78
+ };
79
+ }
80
+ const pkgPath = path2.join(cwd, "package.json");
81
+ if (!await pathExists(pkgPath)) {
82
+ reasons.push("Config file present but no package.json \u2014 ambiguous, skipping");
83
+ return {
84
+ existing: false,
85
+ configPath,
86
+ packageJsonPath: null,
87
+ reasons
88
+ };
89
+ }
90
+ let pkg;
91
+ try {
92
+ pkg = JSON.parse(await fs2.readFile(pkgPath, "utf8"));
93
+ } catch (err) {
94
+ reasons.push(`package.json is not valid JSON: ${err.message}`);
95
+ return {
96
+ existing: false,
97
+ configPath,
98
+ packageJsonPath: pkgPath,
99
+ reasons
100
+ };
101
+ }
102
+ const depSections = ["dependencies", "devDependencies", "peerDependencies"];
103
+ let nuxtVersion;
104
+ for (const section of depSections) {
105
+ const deps = pkg[section];
106
+ if (deps && typeof deps === "object" && "nuxt" in deps) {
107
+ nuxtVersion = deps["nuxt"];
108
+ reasons.push(`Found nuxt@${nuxtVersion} in ${section}`);
109
+ break;
110
+ }
111
+ }
112
+ if (!nuxtVersion) {
113
+ reasons.push("Config file present, but package.json does not list `nuxt` as a dependency");
114
+ return {
115
+ existing: false,
116
+ configPath,
117
+ packageJsonPath: pkgPath,
118
+ reasons
119
+ };
120
+ }
121
+ return {
122
+ existing: true,
123
+ configPath,
124
+ packageJsonPath: pkgPath,
125
+ reasons
126
+ };
127
+ }
128
+ async function pathExists(target) {
129
+ try {
130
+ await fs2.access(target);
131
+ return true;
132
+ } catch {
133
+ return false;
134
+ }
135
+ }
136
+
137
+ // src/wizard/augment.ts
138
+ import { promises as fs3 } from "fs";
139
+ import { loadFile, generateCode, builders } from "magicast";
140
+ import { createPatch } from "diff";
141
+ async function augmentNuxtConfig(options) {
142
+ const originalCode = await fs3.readFile(options.configPath, "utf8");
143
+ const mod = await loadFile(options.configPath);
144
+ const exported = mod.exports.default;
145
+ if (exported === void 0 || exported === null) {
146
+ return {
147
+ kind: "unsupported-shape",
148
+ configPath: options.configPath,
149
+ reason: `${options.configPath} has no default export. Expected \`export default defineNuxtConfig({...})\` or \`export default {...}\`.`
150
+ };
151
+ }
152
+ const config = resolveConfigObject(exported);
153
+ if (!config) {
154
+ return {
155
+ kind: "unsupported-shape",
156
+ configPath: options.configPath,
157
+ reason: `Could not find the config object in ${options.configPath}. Expected \`export default defineNuxtConfig({ modules: [], ... })\` or a plain object literal.`
158
+ };
159
+ }
160
+ const skipReasons = [];
161
+ const modulesRaw = config.modules;
162
+ let modulesWasMissing = false;
163
+ if (modulesRaw === void 0) {
164
+ modulesWasMissing = true;
165
+ config.modules = [];
166
+ } else if (typeof modulesRaw !== "object" || !isProxyArray(modulesRaw)) {
167
+ return {
168
+ kind: "unsupported-shape",
169
+ configPath: options.configPath,
170
+ reason: `\`modules\` in ${options.configPath} is not an array literal. Edit it manually and re-run the wizard if you want to continue.`
171
+ };
172
+ }
173
+ const modules = config.modules;
174
+ const alreadyHasModule = Array.from(modules).some((m) => String(m) === "@noy-db/nuxt");
175
+ if (alreadyHasModule) {
176
+ skipReasons.push("`@noy-db/nuxt` already in modules");
177
+ } else {
178
+ modules.push("@noy-db/nuxt");
179
+ }
180
+ const noydbRaw = config.noydb;
181
+ if (noydbRaw !== void 0) {
182
+ skipReasons.push("`noydb` key already set");
183
+ } else {
184
+ config.noydb = builders.raw(
185
+ `{ adapter: '${options.adapter}', pinia: true, devtools: true }`
186
+ );
187
+ }
188
+ if (skipReasons.length === 2 && !modulesWasMissing) {
189
+ return {
190
+ kind: "already-configured",
191
+ configPath: options.configPath,
192
+ reason: skipReasons.join("; ")
193
+ };
194
+ }
195
+ const generated = generateCode(mod).code;
196
+ const diff = createPatch(
197
+ options.configPath,
198
+ originalCode,
199
+ generated,
200
+ "",
201
+ "",
202
+ { context: 3 }
203
+ );
204
+ return {
205
+ kind: "proposed-change",
206
+ configPath: options.configPath,
207
+ originalCode,
208
+ newCode: generated,
209
+ diff,
210
+ dryRun: options.dryRun === true
211
+ };
212
+ }
213
+ async function writeAugmentedConfig(configPath, newCode) {
214
+ await fs3.writeFile(configPath, newCode, "utf8");
215
+ }
216
+ function resolveConfigObject(exported) {
217
+ if (!exported || typeof exported !== "object") return null;
218
+ const proxy = exported;
219
+ if (proxy.$type === "function-call" && proxy.$args) {
220
+ const firstArg = proxy.$args[0];
221
+ if (firstArg && typeof firstArg === "object") {
222
+ return firstArg;
223
+ }
224
+ return null;
225
+ }
226
+ if (proxy.$type === "object" || proxy.$type === void 0) {
227
+ return proxy;
228
+ }
229
+ return null;
230
+ }
231
+ function isProxyArray(value) {
232
+ if (!value || typeof value !== "object") return false;
233
+ const proxy = value;
234
+ return proxy.$type === "array";
235
+ }
236
+
237
+ // src/wizard/i18n/en.ts
238
+ var en = {
239
+ wizardIntro: "A wizard for noy-db \u2014 None Of Your DataBase.\nGenerates a fresh Nuxt 4 + Pinia + encrypted-store starter.",
240
+ promptProjectName: "Project name",
241
+ promptProjectNamePlaceholder: "my-noy-db-app",
242
+ promptAdapter: "Storage adapter",
243
+ adapterBrowserLabel: "browser \u2014 localStorage / IndexedDB (recommended for web apps)",
244
+ adapterFileLabel: "file \u2014 JSON files on disk (Electron / Tauri / USB workflows)",
245
+ adapterMemoryLabel: "memory \u2014 no persistence (ideal for tests and demos)",
246
+ promptSampleData: "Include sample invoice records?",
247
+ freshNextStepsTitle: "Next steps",
248
+ freshOutroDone: "\u2714 Done \u2014 happy encrypting!",
249
+ augmentModeTitle: "Augment mode",
250
+ augmentDetectedPrefix: "Detected existing Nuxt 4 project:",
251
+ augmentDescription: "The wizard will add @noy-db/nuxt to your modules array\nand a noydb: config key. You can review the diff before\nanything is written to disk.",
252
+ augmentProposedChangesTitle: "Proposed changes",
253
+ augmentApplyConfirm: "Apply these changes?",
254
+ augmentAlreadyConfiguredTitle: "Already configured",
255
+ augmentNothingToDo: "Nothing to do:",
256
+ augmentAlreadyOutro: "\u2714 Your Nuxt config is already wired up.",
257
+ augmentAborted: "Aborted \u2014 your config is unchanged.",
258
+ augmentDryRunOutro: "\u2714 Dry run \u2014 no files were modified.",
259
+ augmentNextStepTitle: "Next step",
260
+ augmentInstallIntro: "Install the @noy-db packages your config now depends on:",
261
+ augmentInstallPmHint: "(or use npm/yarn/bun as appropriate)",
262
+ augmentDoneOutro: "\u2714 Config updated \u2014 happy encrypting!",
263
+ augmentUnsupportedPrefix: "Cannot safely patch this config:",
264
+ cancelled: "Cancelled."
265
+ };
266
+
267
+ // src/wizard/i18n/th.ts
268
+ var th = {
269
+ wizardIntro: "\u0E15\u0E31\u0E27\u0E0A\u0E48\u0E27\u0E22\u0E2A\u0E23\u0E49\u0E32\u0E07\u0E2A\u0E33\u0E2B\u0E23\u0E31\u0E1A noy-db \u2014 None Of Your DataBase\n\u0E2A\u0E23\u0E49\u0E32\u0E07\u0E42\u0E1B\u0E23\u0E40\u0E08\u0E01\u0E15\u0E4C\u0E40\u0E23\u0E34\u0E48\u0E21\u0E15\u0E49\u0E19 Nuxt 4 + Pinia \u0E1E\u0E23\u0E49\u0E2D\u0E21\u0E17\u0E35\u0E48\u0E40\u0E01\u0E47\u0E1A\u0E02\u0E49\u0E2D\u0E21\u0E39\u0E25\u0E41\u0E1A\u0E1A\u0E40\u0E02\u0E49\u0E32\u0E23\u0E2B\u0E31\u0E2A",
270
+ promptProjectName: "\u0E0A\u0E37\u0E48\u0E2D\u0E42\u0E1B\u0E23\u0E40\u0E08\u0E01\u0E15\u0E4C",
271
+ promptProjectNamePlaceholder: "my-noy-db-app",
272
+ promptAdapter: "\u0E2D\u0E30\u0E41\u0E14\u0E1B\u0E40\u0E15\u0E2D\u0E23\u0E4C\u0E08\u0E31\u0E14\u0E40\u0E01\u0E47\u0E1A\u0E02\u0E49\u0E2D\u0E21\u0E39\u0E25",
273
+ adapterBrowserLabel: "browser \u2014 localStorage / IndexedDB (\u0E41\u0E19\u0E30\u0E19\u0E33\u0E2A\u0E33\u0E2B\u0E23\u0E31\u0E1A\u0E40\u0E27\u0E47\u0E1A\u0E41\u0E2D\u0E1B)",
274
+ adapterFileLabel: "file \u2014 \u0E44\u0E1F\u0E25\u0E4C JSON \u0E1A\u0E19\u0E14\u0E34\u0E2A\u0E01\u0E4C (Electron / Tauri / USB)",
275
+ adapterMemoryLabel: "memory \u2014 \u0E44\u0E21\u0E48\u0E1A\u0E31\u0E19\u0E17\u0E36\u0E01\u0E02\u0E49\u0E2D\u0E21\u0E39\u0E25 (\u0E40\u0E2B\u0E21\u0E32\u0E30\u0E2A\u0E33\u0E2B\u0E23\u0E31\u0E1A\u0E01\u0E32\u0E23\u0E17\u0E14\u0E2A\u0E2D\u0E1A\u0E41\u0E25\u0E30\u0E15\u0E31\u0E27\u0E2D\u0E22\u0E48\u0E32\u0E07)",
276
+ promptSampleData: "\u0E40\u0E1E\u0E34\u0E48\u0E21\u0E02\u0E49\u0E2D\u0E21\u0E39\u0E25\u0E15\u0E31\u0E27\u0E2D\u0E22\u0E48\u0E32\u0E07\u0E43\u0E1A\u0E41\u0E08\u0E49\u0E07\u0E2B\u0E19\u0E35\u0E49\u0E2B\u0E23\u0E37\u0E2D\u0E44\u0E21\u0E48?",
277
+ freshNextStepsTitle: "\u0E02\u0E31\u0E49\u0E19\u0E15\u0E2D\u0E19\u0E16\u0E31\u0E14\u0E44\u0E1B",
278
+ freshOutroDone: "\u2714 \u0E40\u0E2A\u0E23\u0E47\u0E08\u0E40\u0E23\u0E35\u0E22\u0E1A\u0E23\u0E49\u0E2D\u0E22 \u2014 \u0E02\u0E2D\u0E43\u0E2B\u0E49\u0E2A\u0E19\u0E38\u0E01\u0E01\u0E31\u0E1A\u0E01\u0E32\u0E23\u0E40\u0E02\u0E49\u0E32\u0E23\u0E2B\u0E31\u0E2A!",
279
+ augmentModeTitle: "\u0E42\u0E2B\u0E21\u0E14\u0E40\u0E2A\u0E23\u0E34\u0E21\u0E42\u0E1B\u0E23\u0E40\u0E08\u0E01\u0E15\u0E4C\u0E40\u0E14\u0E34\u0E21",
280
+ augmentDetectedPrefix: "\u0E1E\u0E1A\u0E42\u0E1B\u0E23\u0E40\u0E08\u0E01\u0E15\u0E4C Nuxt 4 \u0E17\u0E35\u0E48\u0E21\u0E35\u0E2D\u0E22\u0E39\u0E48\u0E41\u0E25\u0E49\u0E27:",
281
+ augmentDescription: "\u0E15\u0E31\u0E27\u0E0A\u0E48\u0E27\u0E22\u0E08\u0E30\u0E40\u0E1E\u0E34\u0E48\u0E21 @noy-db/nuxt \u0E40\u0E02\u0E49\u0E32\u0E43\u0E19 modules\n\u0E41\u0E25\u0E30\u0E40\u0E1E\u0E34\u0E48\u0E21\u0E04\u0E35\u0E22\u0E4C noydb: \u0E43\u0E19\u0E44\u0E1F\u0E25\u0E4C config \u0E04\u0E38\u0E13\u0E2A\u0E32\u0E21\u0E32\u0E23\u0E16\u0E14\u0E39 diff\n\u0E01\u0E48\u0E2D\u0E19\u0E17\u0E35\u0E48\u0E08\u0E30\u0E40\u0E02\u0E35\u0E22\u0E19\u0E44\u0E1F\u0E25\u0E4C\u0E25\u0E07\u0E14\u0E34\u0E2A\u0E01\u0E4C\u0E44\u0E14\u0E49",
282
+ augmentProposedChangesTitle: "\u0E23\u0E32\u0E22\u0E01\u0E32\u0E23\u0E40\u0E1B\u0E25\u0E35\u0E48\u0E22\u0E19\u0E41\u0E1B\u0E25\u0E07\u0E17\u0E35\u0E48\u0E08\u0E30\u0E17\u0E33",
283
+ augmentApplyConfirm: "\u0E22\u0E37\u0E19\u0E22\u0E31\u0E19\u0E01\u0E32\u0E23\u0E40\u0E1B\u0E25\u0E35\u0E48\u0E22\u0E19\u0E41\u0E1B\u0E25\u0E07\u0E40\u0E2B\u0E25\u0E48\u0E32\u0E19\u0E35\u0E49?",
284
+ augmentAlreadyConfiguredTitle: "\u0E15\u0E31\u0E49\u0E07\u0E04\u0E48\u0E32\u0E44\u0E27\u0E49\u0E41\u0E25\u0E49\u0E27",
285
+ augmentNothingToDo: "\u0E44\u0E21\u0E48\u0E21\u0E35\u0E2D\u0E30\u0E44\u0E23\u0E15\u0E49\u0E2D\u0E07\u0E17\u0E33:",
286
+ augmentAlreadyOutro: "\u2714 \u0E44\u0E1F\u0E25\u0E4C Nuxt config \u0E02\u0E2D\u0E07\u0E04\u0E38\u0E13\u0E15\u0E31\u0E49\u0E07\u0E04\u0E48\u0E32\u0E04\u0E23\u0E1A\u0E41\u0E25\u0E49\u0E27",
287
+ augmentAborted: "\u0E22\u0E01\u0E40\u0E25\u0E34\u0E01 \u2014 \u0E44\u0E1F\u0E25\u0E4C config \u0E02\u0E2D\u0E07\u0E04\u0E38\u0E13\u0E44\u0E21\u0E48\u0E16\u0E39\u0E01\u0E41\u0E01\u0E49\u0E44\u0E02",
288
+ augmentDryRunOutro: "\u2714 Dry run \u2014 \u0E44\u0E21\u0E48\u0E21\u0E35\u0E44\u0E1F\u0E25\u0E4C\u0E43\u0E14\u0E16\u0E39\u0E01\u0E41\u0E01\u0E49\u0E44\u0E02",
289
+ augmentNextStepTitle: "\u0E02\u0E31\u0E49\u0E19\u0E15\u0E2D\u0E19\u0E16\u0E31\u0E14\u0E44\u0E1B",
290
+ augmentInstallIntro: "\u0E15\u0E34\u0E14\u0E15\u0E31\u0E49\u0E07\u0E41\u0E1E\u0E47\u0E01\u0E40\u0E01\u0E08 @noy-db \u0E17\u0E35\u0E48 config \u0E02\u0E2D\u0E07\u0E04\u0E38\u0E13\u0E15\u0E49\u0E2D\u0E07\u0E43\u0E0A\u0E49:",
291
+ augmentInstallPmHint: "(\u0E2B\u0E23\u0E37\u0E2D\u0E43\u0E0A\u0E49 npm/yarn/bun \u0E15\u0E32\u0E21\u0E04\u0E27\u0E32\u0E21\u0E40\u0E2B\u0E21\u0E32\u0E30\u0E2A\u0E21)",
292
+ augmentDoneOutro: "\u2714 \u0E2D\u0E31\u0E1B\u0E40\u0E14\u0E15 config \u0E40\u0E23\u0E35\u0E22\u0E1A\u0E23\u0E49\u0E2D\u0E22 \u2014 \u0E02\u0E2D\u0E43\u0E2B\u0E49\u0E2A\u0E19\u0E38\u0E01\u0E01\u0E31\u0E1A\u0E01\u0E32\u0E23\u0E40\u0E02\u0E49\u0E32\u0E23\u0E2B\u0E31\u0E2A!",
293
+ augmentUnsupportedPrefix: "\u0E44\u0E21\u0E48\u0E2A\u0E32\u0E21\u0E32\u0E23\u0E16\u0E41\u0E01\u0E49\u0E44\u0E02 config \u0E19\u0E35\u0E49\u0E44\u0E14\u0E49\u0E2D\u0E22\u0E48\u0E32\u0E07\u0E1B\u0E25\u0E2D\u0E14\u0E20\u0E31\u0E22:",
294
+ cancelled: "\u0E22\u0E01\u0E40\u0E25\u0E34\u0E01\u0E41\u0E25\u0E49\u0E27"
295
+ };
296
+
297
+ // src/wizard/i18n/index.ts
298
+ var BUNDLES = { en, th };
299
+ var SUPPORTED_LOCALES = ["en", "th"];
300
+ function loadMessages(locale) {
301
+ return BUNDLES[locale] ?? BUNDLES.en;
302
+ }
303
+ function detectLocale(env = process.env) {
304
+ const candidates = [
305
+ env.LC_ALL,
306
+ env.LC_MESSAGES,
307
+ env.LANG,
308
+ // LANGUAGE is a comma-separated preference list — take the first
309
+ // entry. We deliberately do NOT walk the whole list; the wizard
310
+ // ships exactly two locales, so a "best fit" walk would be
311
+ // overkill and would obscure unexpected behaviour.
312
+ env.LANGUAGE?.split(":")[0]?.split(",")[0]
313
+ ];
314
+ for (const raw of candidates) {
315
+ if (!raw) continue;
316
+ const normalised = raw.split(".")[0].split("@")[0].toLowerCase().split("_")[0];
317
+ if (SUPPORTED_LOCALES.includes(normalised)) {
318
+ return normalised;
319
+ }
320
+ }
321
+ return "en";
322
+ }
323
+ function parseLocaleFlag(value) {
324
+ const normalised = value.toLowerCase().trim();
325
+ if (SUPPORTED_LOCALES.includes(normalised)) {
326
+ return normalised;
327
+ }
328
+ throw new Error(
329
+ `Unsupported --lang value: "${value}". Supported: ${SUPPORTED_LOCALES.join(", ")}`
330
+ );
331
+ }
332
+
333
+ // src/wizard/run.ts
334
+ var DEFAULT_SEED = [
335
+ {
336
+ id: "inv-001",
337
+ client: "Acme Holdings",
338
+ amount: 1500,
339
+ status: "open",
340
+ dueDate: "2026-05-01"
341
+ },
342
+ {
343
+ id: "inv-002",
344
+ client: "Globex Inc",
345
+ amount: 4200,
346
+ status: "paid",
347
+ dueDate: "2026-04-15"
348
+ },
349
+ {
350
+ id: "inv-003",
351
+ client: "Initech LLC",
352
+ amount: 850,
353
+ status: "overdue",
354
+ dueDate: "2026-03-20"
355
+ }
356
+ ];
357
+ function adapterLabels(msg) {
358
+ return {
359
+ browser: msg.adapterBrowserLabel,
360
+ file: msg.adapterFileLabel,
361
+ memory: msg.adapterMemoryLabel
362
+ };
363
+ }
364
+ function validateProjectName(name) {
365
+ if (!name || name.trim() === "") return "Project name cannot be empty";
366
+ if (name.length > 214) return "Project name must be 214 characters or fewer";
367
+ if (!/^[a-z0-9][a-z0-9._-]*$/.test(name)) {
368
+ return "Project name must start with a lowercase letter or digit and contain only lowercase letters, digits, hyphens, dots, or underscores";
369
+ }
370
+ return null;
371
+ }
372
+ async function runWizard(options = {}) {
373
+ const cwd = options.cwd ?? process.cwd();
374
+ const yes = options.yes ?? false;
375
+ const msg = loadMessages(options.locale ?? detectLocale());
376
+ const detection = options.forceFresh ? null : await detectNuxtProject(cwd);
377
+ if (detection?.existing && detection.configPath) {
378
+ return runAugmentMode(options, cwd, detection.configPath, msg);
379
+ }
380
+ return runFreshMode(options, cwd, yes, msg);
381
+ }
382
+ async function runFreshMode(options, cwd, yes, msg) {
383
+ const projectName = yes ? options.projectName ?? "my-noy-db-app" : await promptProjectName(options.projectName, msg);
384
+ const adapter = yes ? options.adapter ?? "browser" : await promptAdapter(options.adapter, msg);
385
+ const sampleData = yes ? options.sampleData ?? true : await promptSampleData(options.sampleData, msg);
386
+ const projectPath = path3.resolve(cwd, projectName);
387
+ await assertWritableTarget(projectPath);
388
+ const tokens = {
389
+ PROJECT_NAME: projectName,
390
+ ADAPTER: adapter,
391
+ DEVTOOLS: "true",
392
+ SEED_INVOICES: sampleData ? JSON.stringify(DEFAULT_SEED, null, 2).replace(/\n/g, "\n ") : "[]"
393
+ };
394
+ await fs4.mkdir(projectPath, { recursive: true });
395
+ const files = await renderTemplate(
396
+ templateDir("nuxt-default"),
397
+ projectPath,
398
+ tokens
399
+ );
400
+ if (!yes) {
401
+ p.note(
402
+ [
403
+ `${pc.bold("cd")} ${projectName}`,
404
+ `${pc.bold("pnpm install")} ${pc.dim("(or npm/yarn/bun)")}`,
405
+ `${pc.bold("pnpm dev")}`
406
+ ].join("\n"),
407
+ msg.freshNextStepsTitle
408
+ );
409
+ p.outro(pc.green(msg.freshOutroDone));
410
+ }
411
+ return {
412
+ kind: "fresh",
413
+ options: {
414
+ projectName,
415
+ adapter,
416
+ sampleData,
417
+ cwd
418
+ },
419
+ projectPath,
420
+ files
421
+ };
422
+ }
423
+ async function runAugmentMode(options, cwd, configPath, msg) {
424
+ const yes = options.yes ?? false;
425
+ const dryRun = options.dryRun ?? false;
426
+ if (!yes) {
427
+ p.note(
428
+ [
429
+ `${pc.dim(msg.augmentDetectedPrefix)}`,
430
+ ` ${pc.cyan(configPath)}`,
431
+ "",
432
+ msg.augmentDescription
433
+ ].join("\n"),
434
+ msg.augmentModeTitle
435
+ );
436
+ }
437
+ const adapter = yes ? options.adapter ?? "browser" : await promptAdapter(options.adapter, msg);
438
+ const result = await augmentNuxtConfig({
439
+ configPath,
440
+ adapter,
441
+ dryRun
442
+ });
443
+ if (result.kind === "already-configured") {
444
+ if (!yes) {
445
+ p.note(
446
+ `${pc.yellow(msg.augmentNothingToDo)} ${result.reason}`,
447
+ msg.augmentAlreadyConfiguredTitle
448
+ );
449
+ p.outro(pc.green(msg.augmentAlreadyOutro));
450
+ }
451
+ return {
452
+ kind: "augment",
453
+ configPath,
454
+ adapter,
455
+ changed: false,
456
+ reason: "already-configured"
457
+ };
458
+ }
459
+ if (result.kind === "unsupported-shape") {
460
+ if (!yes) {
461
+ p.cancel(`${pc.red(msg.augmentUnsupportedPrefix)} ${result.reason}`);
462
+ }
463
+ return {
464
+ kind: "augment",
465
+ configPath,
466
+ adapter,
467
+ changed: false,
468
+ reason: "unsupported-shape"
469
+ };
470
+ }
471
+ if (!yes || dryRun) {
472
+ p.note(renderDiff(result.diff), msg.augmentProposedChangesTitle);
473
+ }
474
+ if (dryRun) {
475
+ if (!yes) p.outro(pc.green(msg.augmentDryRunOutro));
476
+ return {
477
+ kind: "augment",
478
+ configPath,
479
+ adapter,
480
+ changed: false,
481
+ reason: "dry-run",
482
+ diff: result.diff
483
+ };
484
+ }
485
+ let shouldWrite = yes;
486
+ if (!yes) {
487
+ const confirmed = await p.confirm({
488
+ message: msg.augmentApplyConfirm,
489
+ initialValue: true
490
+ });
491
+ if (p.isCancel(confirmed) || confirmed !== true) {
492
+ p.cancel(msg.augmentAborted);
493
+ return {
494
+ kind: "augment",
495
+ configPath,
496
+ adapter,
497
+ changed: false,
498
+ reason: "cancelled",
499
+ diff: result.diff
500
+ };
501
+ }
502
+ shouldWrite = true;
503
+ }
504
+ if (shouldWrite) {
505
+ await writeAugmentedConfig(configPath, result.newCode);
506
+ if (!yes) {
507
+ p.note(
508
+ [
509
+ pc.dim(msg.augmentInstallIntro),
510
+ "",
511
+ `${pc.bold("pnpm add")} @noy-db/nuxt @noy-db/pinia @noy-db/core @noy-db/browser @pinia/nuxt pinia`,
512
+ pc.dim(msg.augmentInstallPmHint)
513
+ ].join("\n"),
514
+ msg.augmentNextStepTitle
515
+ );
516
+ p.outro(pc.green(msg.augmentDoneOutro));
517
+ }
518
+ }
519
+ return {
520
+ kind: "augment",
521
+ configPath,
522
+ adapter,
523
+ changed: true,
524
+ reason: "written",
525
+ diff: result.diff
526
+ };
527
+ }
528
+ function renderDiff(diff) {
529
+ const lines = diff.split("\n");
530
+ const keep = [];
531
+ for (const line of lines) {
532
+ if (line.startsWith("Index:")) continue;
533
+ if (line.startsWith("=")) continue;
534
+ if (line.startsWith("---") || line.startsWith("+++")) continue;
535
+ if (line.startsWith("+")) keep.push(pc.green(line));
536
+ else if (line.startsWith("-")) keep.push(pc.red(line));
537
+ else if (line.startsWith("@@")) keep.push(pc.dim(line));
538
+ else keep.push(line);
539
+ }
540
+ return keep.join("\n").trim();
541
+ }
542
+ async function promptProjectName(initial, msg) {
543
+ if (initial) {
544
+ const err = validateProjectName(initial);
545
+ if (err) throw new Error(err);
546
+ return initial;
547
+ }
548
+ const result = await p.text({
549
+ message: msg.promptProjectName,
550
+ placeholder: msg.promptProjectNamePlaceholder,
551
+ initialValue: "my-noy-db-app",
552
+ validate: (v) => validateProjectName(v ?? "") ?? void 0
553
+ });
554
+ if (p.isCancel(result)) {
555
+ p.cancel(msg.cancelled);
556
+ process.exit(1);
557
+ }
558
+ return result;
559
+ }
560
+ async function promptAdapter(initial, msg) {
561
+ if (initial) return initial;
562
+ const labels = adapterLabels(msg);
563
+ const result = await p.select({
564
+ message: msg.promptAdapter,
565
+ options: ["browser", "file", "memory"].map((value) => ({
566
+ value,
567
+ label: labels[value]
568
+ })),
569
+ initialValue: "browser"
570
+ });
571
+ if (p.isCancel(result)) {
572
+ p.cancel(msg.cancelled);
573
+ process.exit(1);
574
+ }
575
+ return result;
576
+ }
577
+ async function promptSampleData(initial, msg) {
578
+ if (typeof initial === "boolean") return initial;
579
+ const result = await p.confirm({
580
+ message: msg.promptSampleData,
581
+ initialValue: true
582
+ });
583
+ if (p.isCancel(result)) {
584
+ p.cancel(msg.cancelled);
585
+ process.exit(1);
586
+ }
587
+ return result;
588
+ }
589
+ async function assertWritableTarget(target) {
590
+ try {
591
+ const entries = await fs4.readdir(target);
592
+ if (entries.length > 0) {
593
+ throw new Error(
594
+ `Target directory '${target}' already exists and is not empty. Refusing to overwrite \u2014 pick a different project name or remove the directory first.`
595
+ );
596
+ }
597
+ } catch (err) {
598
+ if (err.code === "ENOENT") return;
599
+ throw err;
600
+ }
601
+ }
602
+
603
+ // src/bin/parse-args.ts
604
+ function parseArgs(argv) {
605
+ const options = {};
606
+ let help = false;
607
+ for (let i = 0; i < argv.length; i++) {
608
+ const arg = argv[i];
609
+ if (arg === "-h" || arg === "--help") {
610
+ help = true;
611
+ continue;
612
+ }
613
+ if (arg === "-y" || arg === "--yes") {
614
+ options.yes = true;
615
+ continue;
616
+ }
617
+ if (arg === "--no-sample-data") {
618
+ options.sampleData = false;
619
+ continue;
620
+ }
621
+ if (arg === "--dry-run") {
622
+ options.dryRun = true;
623
+ continue;
624
+ }
625
+ if (arg === "--force-fresh") {
626
+ options.forceFresh = true;
627
+ continue;
628
+ }
629
+ if (arg === "--lang") {
630
+ const next = argv[++i];
631
+ if (!next) {
632
+ throw new Error("--lang requires a value (en or th)");
633
+ }
634
+ options.locale = parseLocaleFlag(next);
635
+ continue;
636
+ }
637
+ if (arg === "--adapter") {
638
+ const next = argv[++i];
639
+ if (next !== "browser" && next !== "file" && next !== "memory") {
640
+ throw new Error(
641
+ `--adapter must be one of: browser, file, memory (got: ${next ?? "(missing)"})`
642
+ );
643
+ }
644
+ options.adapter = next;
645
+ continue;
646
+ }
647
+ if (arg && arg.startsWith("-")) {
648
+ throw new Error(`Unknown option: ${arg}`);
649
+ }
650
+ if (arg && !options.projectName) {
651
+ options.projectName = arg;
652
+ continue;
653
+ }
654
+ if (arg) {
655
+ throw new Error(`Unexpected positional argument: ${arg}`);
656
+ }
657
+ }
658
+ return { options, help };
659
+ }
660
+
661
+ // src/bin/create.ts
662
+ var HELP = `Usage: npm create @noy-db [project-name] [options]
663
+
664
+ The wizard auto-detects whether cwd is an existing Nuxt 4 project:
665
+
666
+ - If nuxt.config.{ts,js,mjs} + a package.json with \`nuxt\` in
667
+ deps are both present, it enters AUGMENT mode \u2014 patches the
668
+ existing config via magicast, shows a unified diff, and asks
669
+ for confirmation before writing.
670
+
671
+ - Otherwise it enters FRESH mode \u2014 creates a new project
672
+ directory with a full Nuxt 4 + Pinia + @noy-db/nuxt starter.
673
+
674
+ Options:
675
+ -y, --yes Skip prompts; use defaults for missing values
676
+ --adapter <name> Adapter: browser (default) | file | memory
677
+ --no-sample-data (fresh mode) Skip the seed invoice records
678
+ --dry-run (augment mode) Show the diff without writing
679
+ --force-fresh Force fresh-project mode even when cwd looks
680
+ like an existing Nuxt project
681
+ --lang <code> UI language: en (default) | th
682
+ When omitted, auto-detected from LC_ALL/LANG
683
+ -h, --help Show this message and exit
684
+
685
+ Examples:
686
+ # Fresh project in a new directory
687
+ npm create @noy-db my-app
688
+ npm create @noy-db my-app --yes --adapter file
689
+
690
+ # Augment an existing Nuxt 4 project (run from its root)
691
+ cd ~/my-existing-nuxt-app
692
+ npm create @noy-db # preview the diff and confirm
693
+ npm create @noy-db --dry-run # print the diff, no write
694
+ npm create @noy-db --yes # non-interactive, write immediately
695
+ `;
696
+ async function main() {
697
+ let parsed;
698
+ try {
699
+ parsed = parseArgs(process.argv.slice(2));
700
+ } catch (err) {
701
+ process.stderr.write(`${pc2.red("error:")} ${err.message}
702
+
703
+ ${HELP}`);
704
+ process.exit(2);
705
+ return;
706
+ }
707
+ if (parsed.help) {
708
+ process.stdout.write(HELP);
709
+ return;
710
+ }
711
+ if (!parsed.options.yes) {
712
+ const msg = loadMessages(parsed.options.locale ?? detectLocale());
713
+ p2.intro(pc2.bgCyan(pc2.black(" @noy-db/create ")));
714
+ p2.note(msg.wizardIntro);
715
+ }
716
+ try {
717
+ await runWizard(parsed.options);
718
+ } catch (err) {
719
+ p2.cancel(err.message);
720
+ process.exit(1);
721
+ }
722
+ }
723
+ void main();
724
+ //# sourceMappingURL=create.js.map