@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
package/dist/index.js
ADDED
|
@@ -0,0 +1,902 @@
|
|
|
1
|
+
// src/wizard/run.ts
|
|
2
|
+
import { promises as fs4 } from "fs";
|
|
3
|
+
import path3 from "path";
|
|
4
|
+
import * as p from "@clack/prompts";
|
|
5
|
+
import pc from "picocolors";
|
|
6
|
+
|
|
7
|
+
// src/wizard/render.ts
|
|
8
|
+
import { promises as fs } from "fs";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import { fileURLToPath } from "url";
|
|
11
|
+
async function renderTemplate(src, dest, tokens) {
|
|
12
|
+
const written = [];
|
|
13
|
+
await walk(src, dest, "", tokens, written);
|
|
14
|
+
written.sort();
|
|
15
|
+
return written;
|
|
16
|
+
}
|
|
17
|
+
async function walk(srcRoot, destRoot, rel, tokens, written) {
|
|
18
|
+
const srcDir = path.join(srcRoot, rel);
|
|
19
|
+
const entries = await fs.readdir(srcDir, { withFileTypes: true });
|
|
20
|
+
for (const entry of entries) {
|
|
21
|
+
const srcEntry = path.join(srcDir, entry.name);
|
|
22
|
+
const destName = entry.name.startsWith("_") ? `.${entry.name.slice(1)}` : entry.name;
|
|
23
|
+
const destRel = rel ? path.join(rel, destName) : destName;
|
|
24
|
+
const destEntry = path.join(destRoot, destRel);
|
|
25
|
+
if (entry.isDirectory()) {
|
|
26
|
+
await fs.mkdir(destEntry, { recursive: true });
|
|
27
|
+
await walk(srcRoot, destRoot, path.join(rel, entry.name), tokens, written);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const raw = await fs.readFile(srcEntry, "utf8");
|
|
31
|
+
const rendered = applyTokens(raw, tokens);
|
|
32
|
+
await fs.mkdir(path.dirname(destEntry), { recursive: true });
|
|
33
|
+
await fs.writeFile(destEntry, rendered, "utf8");
|
|
34
|
+
written.push(destRel);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function applyTokens(input, tokens) {
|
|
38
|
+
const bag = tokens;
|
|
39
|
+
return input.replace(/\{\{(\w+)\}\}/g, (match, key) => {
|
|
40
|
+
const value = bag[key];
|
|
41
|
+
return value === void 0 ? match : value;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function templateDir(name) {
|
|
45
|
+
const here = fileURLToPath(import.meta.url);
|
|
46
|
+
const packageRoot = path.resolve(path.dirname(here), "..", "..");
|
|
47
|
+
return path.join(packageRoot, "templates", name);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/wizard/detect.ts
|
|
51
|
+
import { promises as fs2 } from "fs";
|
|
52
|
+
import path2 from "path";
|
|
53
|
+
async function detectNuxtProject(cwd) {
|
|
54
|
+
const reasons = [];
|
|
55
|
+
const configCandidates = ["nuxt.config.ts", "nuxt.config.js", "nuxt.config.mjs"];
|
|
56
|
+
let configPath = null;
|
|
57
|
+
for (const name of configCandidates) {
|
|
58
|
+
const candidate = path2.join(cwd, name);
|
|
59
|
+
if (await pathExists(candidate)) {
|
|
60
|
+
configPath = candidate;
|
|
61
|
+
reasons.push(`Found ${name}`);
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (!configPath) {
|
|
66
|
+
reasons.push("No nuxt.config.{ts,js,mjs} in cwd");
|
|
67
|
+
return {
|
|
68
|
+
existing: false,
|
|
69
|
+
configPath: null,
|
|
70
|
+
packageJsonPath: null,
|
|
71
|
+
reasons
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const pkgPath = path2.join(cwd, "package.json");
|
|
75
|
+
if (!await pathExists(pkgPath)) {
|
|
76
|
+
reasons.push("Config file present but no package.json \u2014 ambiguous, skipping");
|
|
77
|
+
return {
|
|
78
|
+
existing: false,
|
|
79
|
+
configPath,
|
|
80
|
+
packageJsonPath: null,
|
|
81
|
+
reasons
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
let pkg;
|
|
85
|
+
try {
|
|
86
|
+
pkg = JSON.parse(await fs2.readFile(pkgPath, "utf8"));
|
|
87
|
+
} catch (err) {
|
|
88
|
+
reasons.push(`package.json is not valid JSON: ${err.message}`);
|
|
89
|
+
return {
|
|
90
|
+
existing: false,
|
|
91
|
+
configPath,
|
|
92
|
+
packageJsonPath: pkgPath,
|
|
93
|
+
reasons
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const depSections = ["dependencies", "devDependencies", "peerDependencies"];
|
|
97
|
+
let nuxtVersion;
|
|
98
|
+
for (const section of depSections) {
|
|
99
|
+
const deps = pkg[section];
|
|
100
|
+
if (deps && typeof deps === "object" && "nuxt" in deps) {
|
|
101
|
+
nuxtVersion = deps["nuxt"];
|
|
102
|
+
reasons.push(`Found nuxt@${nuxtVersion} in ${section}`);
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (!nuxtVersion) {
|
|
107
|
+
reasons.push("Config file present, but package.json does not list `nuxt` as a dependency");
|
|
108
|
+
return {
|
|
109
|
+
existing: false,
|
|
110
|
+
configPath,
|
|
111
|
+
packageJsonPath: pkgPath,
|
|
112
|
+
reasons
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
existing: true,
|
|
117
|
+
configPath,
|
|
118
|
+
packageJsonPath: pkgPath,
|
|
119
|
+
reasons
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
async function pathExists(target) {
|
|
123
|
+
try {
|
|
124
|
+
await fs2.access(target);
|
|
125
|
+
return true;
|
|
126
|
+
} catch {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/wizard/augment.ts
|
|
132
|
+
import { promises as fs3 } from "fs";
|
|
133
|
+
import { loadFile, generateCode, builders } from "magicast";
|
|
134
|
+
import { createPatch } from "diff";
|
|
135
|
+
async function augmentNuxtConfig(options) {
|
|
136
|
+
const originalCode = await fs3.readFile(options.configPath, "utf8");
|
|
137
|
+
const mod = await loadFile(options.configPath);
|
|
138
|
+
const exported = mod.exports.default;
|
|
139
|
+
if (exported === void 0 || exported === null) {
|
|
140
|
+
return {
|
|
141
|
+
kind: "unsupported-shape",
|
|
142
|
+
configPath: options.configPath,
|
|
143
|
+
reason: `${options.configPath} has no default export. Expected \`export default defineNuxtConfig({...})\` or \`export default {...}\`.`
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
const config = resolveConfigObject(exported);
|
|
147
|
+
if (!config) {
|
|
148
|
+
return {
|
|
149
|
+
kind: "unsupported-shape",
|
|
150
|
+
configPath: options.configPath,
|
|
151
|
+
reason: `Could not find the config object in ${options.configPath}. Expected \`export default defineNuxtConfig({ modules: [], ... })\` or a plain object literal.`
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const skipReasons = [];
|
|
155
|
+
const modulesRaw = config.modules;
|
|
156
|
+
let modulesWasMissing = false;
|
|
157
|
+
if (modulesRaw === void 0) {
|
|
158
|
+
modulesWasMissing = true;
|
|
159
|
+
config.modules = [];
|
|
160
|
+
} else if (typeof modulesRaw !== "object" || !isProxyArray(modulesRaw)) {
|
|
161
|
+
return {
|
|
162
|
+
kind: "unsupported-shape",
|
|
163
|
+
configPath: options.configPath,
|
|
164
|
+
reason: `\`modules\` in ${options.configPath} is not an array literal. Edit it manually and re-run the wizard if you want to continue.`
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
const modules = config.modules;
|
|
168
|
+
const alreadyHasModule = Array.from(modules).some((m) => String(m) === "@noy-db/nuxt");
|
|
169
|
+
if (alreadyHasModule) {
|
|
170
|
+
skipReasons.push("`@noy-db/nuxt` already in modules");
|
|
171
|
+
} else {
|
|
172
|
+
modules.push("@noy-db/nuxt");
|
|
173
|
+
}
|
|
174
|
+
const noydbRaw = config.noydb;
|
|
175
|
+
if (noydbRaw !== void 0) {
|
|
176
|
+
skipReasons.push("`noydb` key already set");
|
|
177
|
+
} else {
|
|
178
|
+
config.noydb = builders.raw(
|
|
179
|
+
`{ adapter: '${options.adapter}', pinia: true, devtools: true }`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
if (skipReasons.length === 2 && !modulesWasMissing) {
|
|
183
|
+
return {
|
|
184
|
+
kind: "already-configured",
|
|
185
|
+
configPath: options.configPath,
|
|
186
|
+
reason: skipReasons.join("; ")
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
const generated = generateCode(mod).code;
|
|
190
|
+
const diff = createPatch(
|
|
191
|
+
options.configPath,
|
|
192
|
+
originalCode,
|
|
193
|
+
generated,
|
|
194
|
+
"",
|
|
195
|
+
"",
|
|
196
|
+
{ context: 3 }
|
|
197
|
+
);
|
|
198
|
+
return {
|
|
199
|
+
kind: "proposed-change",
|
|
200
|
+
configPath: options.configPath,
|
|
201
|
+
originalCode,
|
|
202
|
+
newCode: generated,
|
|
203
|
+
diff,
|
|
204
|
+
dryRun: options.dryRun === true
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
async function writeAugmentedConfig(configPath, newCode) {
|
|
208
|
+
await fs3.writeFile(configPath, newCode, "utf8");
|
|
209
|
+
}
|
|
210
|
+
function resolveConfigObject(exported) {
|
|
211
|
+
if (!exported || typeof exported !== "object") return null;
|
|
212
|
+
const proxy = exported;
|
|
213
|
+
if (proxy.$type === "function-call" && proxy.$args) {
|
|
214
|
+
const firstArg = proxy.$args[0];
|
|
215
|
+
if (firstArg && typeof firstArg === "object") {
|
|
216
|
+
return firstArg;
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
if (proxy.$type === "object" || proxy.$type === void 0) {
|
|
221
|
+
return proxy;
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
function isProxyArray(value) {
|
|
226
|
+
if (!value || typeof value !== "object") return false;
|
|
227
|
+
const proxy = value;
|
|
228
|
+
return proxy.$type === "array";
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// src/wizard/i18n/en.ts
|
|
232
|
+
var en = {
|
|
233
|
+
wizardIntro: "A wizard for noy-db \u2014 None Of Your DataBase.\nGenerates a fresh Nuxt 4 + Pinia + encrypted-store starter.",
|
|
234
|
+
promptProjectName: "Project name",
|
|
235
|
+
promptProjectNamePlaceholder: "my-noy-db-app",
|
|
236
|
+
promptAdapter: "Storage adapter",
|
|
237
|
+
adapterBrowserLabel: "browser \u2014 localStorage / IndexedDB (recommended for web apps)",
|
|
238
|
+
adapterFileLabel: "file \u2014 JSON files on disk (Electron / Tauri / USB workflows)",
|
|
239
|
+
adapterMemoryLabel: "memory \u2014 no persistence (ideal for tests and demos)",
|
|
240
|
+
promptSampleData: "Include sample invoice records?",
|
|
241
|
+
freshNextStepsTitle: "Next steps",
|
|
242
|
+
freshOutroDone: "\u2714 Done \u2014 happy encrypting!",
|
|
243
|
+
augmentModeTitle: "Augment mode",
|
|
244
|
+
augmentDetectedPrefix: "Detected existing Nuxt 4 project:",
|
|
245
|
+
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.",
|
|
246
|
+
augmentProposedChangesTitle: "Proposed changes",
|
|
247
|
+
augmentApplyConfirm: "Apply these changes?",
|
|
248
|
+
augmentAlreadyConfiguredTitle: "Already configured",
|
|
249
|
+
augmentNothingToDo: "Nothing to do:",
|
|
250
|
+
augmentAlreadyOutro: "\u2714 Your Nuxt config is already wired up.",
|
|
251
|
+
augmentAborted: "Aborted \u2014 your config is unchanged.",
|
|
252
|
+
augmentDryRunOutro: "\u2714 Dry run \u2014 no files were modified.",
|
|
253
|
+
augmentNextStepTitle: "Next step",
|
|
254
|
+
augmentInstallIntro: "Install the @noy-db packages your config now depends on:",
|
|
255
|
+
augmentInstallPmHint: "(or use npm/yarn/bun as appropriate)",
|
|
256
|
+
augmentDoneOutro: "\u2714 Config updated \u2014 happy encrypting!",
|
|
257
|
+
augmentUnsupportedPrefix: "Cannot safely patch this config:",
|
|
258
|
+
cancelled: "Cancelled."
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// src/wizard/i18n/th.ts
|
|
262
|
+
var th = {
|
|
263
|
+
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",
|
|
264
|
+
promptProjectName: "\u0E0A\u0E37\u0E48\u0E2D\u0E42\u0E1B\u0E23\u0E40\u0E08\u0E01\u0E15\u0E4C",
|
|
265
|
+
promptProjectNamePlaceholder: "my-noy-db-app",
|
|
266
|
+
promptAdapter: "\u0E2D\u0E30\u0E41\u0E14\u0E1B\u0E40\u0E15\u0E2D\u0E23\u0E4C\u0E08\u0E31\u0E14\u0E40\u0E01\u0E47\u0E1A\u0E02\u0E49\u0E2D\u0E21\u0E39\u0E25",
|
|
267
|
+
adapterBrowserLabel: "browser \u2014 localStorage / IndexedDB (\u0E41\u0E19\u0E30\u0E19\u0E33\u0E2A\u0E33\u0E2B\u0E23\u0E31\u0E1A\u0E40\u0E27\u0E47\u0E1A\u0E41\u0E2D\u0E1B)",
|
|
268
|
+
adapterFileLabel: "file \u2014 \u0E44\u0E1F\u0E25\u0E4C JSON \u0E1A\u0E19\u0E14\u0E34\u0E2A\u0E01\u0E4C (Electron / Tauri / USB)",
|
|
269
|
+
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)",
|
|
270
|
+
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?",
|
|
271
|
+
freshNextStepsTitle: "\u0E02\u0E31\u0E49\u0E19\u0E15\u0E2D\u0E19\u0E16\u0E31\u0E14\u0E44\u0E1B",
|
|
272
|
+
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!",
|
|
273
|
+
augmentModeTitle: "\u0E42\u0E2B\u0E21\u0E14\u0E40\u0E2A\u0E23\u0E34\u0E21\u0E42\u0E1B\u0E23\u0E40\u0E08\u0E01\u0E15\u0E4C\u0E40\u0E14\u0E34\u0E21",
|
|
274
|
+
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:",
|
|
275
|
+
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",
|
|
276
|
+
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",
|
|
277
|
+
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?",
|
|
278
|
+
augmentAlreadyConfiguredTitle: "\u0E15\u0E31\u0E49\u0E07\u0E04\u0E48\u0E32\u0E44\u0E27\u0E49\u0E41\u0E25\u0E49\u0E27",
|
|
279
|
+
augmentNothingToDo: "\u0E44\u0E21\u0E48\u0E21\u0E35\u0E2D\u0E30\u0E44\u0E23\u0E15\u0E49\u0E2D\u0E07\u0E17\u0E33:",
|
|
280
|
+
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",
|
|
281
|
+
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",
|
|
282
|
+
augmentDryRunOutro: "\u2714 Dry run \u2014 \u0E44\u0E21\u0E48\u0E21\u0E35\u0E44\u0E1F\u0E25\u0E4C\u0E43\u0E14\u0E16\u0E39\u0E01\u0E41\u0E01\u0E49\u0E44\u0E02",
|
|
283
|
+
augmentNextStepTitle: "\u0E02\u0E31\u0E49\u0E19\u0E15\u0E2D\u0E19\u0E16\u0E31\u0E14\u0E44\u0E1B",
|
|
284
|
+
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:",
|
|
285
|
+
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)",
|
|
286
|
+
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!",
|
|
287
|
+
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:",
|
|
288
|
+
cancelled: "\u0E22\u0E01\u0E40\u0E25\u0E34\u0E01\u0E41\u0E25\u0E49\u0E27"
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
// src/wizard/i18n/index.ts
|
|
292
|
+
var BUNDLES = { en, th };
|
|
293
|
+
var SUPPORTED_LOCALES = ["en", "th"];
|
|
294
|
+
function loadMessages(locale) {
|
|
295
|
+
return BUNDLES[locale] ?? BUNDLES.en;
|
|
296
|
+
}
|
|
297
|
+
function detectLocale(env = process.env) {
|
|
298
|
+
const candidates = [
|
|
299
|
+
env.LC_ALL,
|
|
300
|
+
env.LC_MESSAGES,
|
|
301
|
+
env.LANG,
|
|
302
|
+
// LANGUAGE is a comma-separated preference list — take the first
|
|
303
|
+
// entry. We deliberately do NOT walk the whole list; the wizard
|
|
304
|
+
// ships exactly two locales, so a "best fit" walk would be
|
|
305
|
+
// overkill and would obscure unexpected behaviour.
|
|
306
|
+
env.LANGUAGE?.split(":")[0]?.split(",")[0]
|
|
307
|
+
];
|
|
308
|
+
for (const raw of candidates) {
|
|
309
|
+
if (!raw) continue;
|
|
310
|
+
const normalised = raw.split(".")[0].split("@")[0].toLowerCase().split("_")[0];
|
|
311
|
+
if (SUPPORTED_LOCALES.includes(normalised)) {
|
|
312
|
+
return normalised;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return "en";
|
|
316
|
+
}
|
|
317
|
+
function parseLocaleFlag(value) {
|
|
318
|
+
const normalised = value.toLowerCase().trim();
|
|
319
|
+
if (SUPPORTED_LOCALES.includes(normalised)) {
|
|
320
|
+
return normalised;
|
|
321
|
+
}
|
|
322
|
+
throw new Error(
|
|
323
|
+
`Unsupported --lang value: "${value}". Supported: ${SUPPORTED_LOCALES.join(", ")}`
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// src/wizard/run.ts
|
|
328
|
+
var DEFAULT_SEED = [
|
|
329
|
+
{
|
|
330
|
+
id: "inv-001",
|
|
331
|
+
client: "Acme Holdings",
|
|
332
|
+
amount: 1500,
|
|
333
|
+
status: "open",
|
|
334
|
+
dueDate: "2026-05-01"
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
id: "inv-002",
|
|
338
|
+
client: "Globex Inc",
|
|
339
|
+
amount: 4200,
|
|
340
|
+
status: "paid",
|
|
341
|
+
dueDate: "2026-04-15"
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
id: "inv-003",
|
|
345
|
+
client: "Initech LLC",
|
|
346
|
+
amount: 850,
|
|
347
|
+
status: "overdue",
|
|
348
|
+
dueDate: "2026-03-20"
|
|
349
|
+
}
|
|
350
|
+
];
|
|
351
|
+
function adapterLabels(msg) {
|
|
352
|
+
return {
|
|
353
|
+
browser: msg.adapterBrowserLabel,
|
|
354
|
+
file: msg.adapterFileLabel,
|
|
355
|
+
memory: msg.adapterMemoryLabel
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
function validateProjectName(name) {
|
|
359
|
+
if (!name || name.trim() === "") return "Project name cannot be empty";
|
|
360
|
+
if (name.length > 214) return "Project name must be 214 characters or fewer";
|
|
361
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/.test(name)) {
|
|
362
|
+
return "Project name must start with a lowercase letter or digit and contain only lowercase letters, digits, hyphens, dots, or underscores";
|
|
363
|
+
}
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
async function runWizard(options = {}) {
|
|
367
|
+
const cwd = options.cwd ?? process.cwd();
|
|
368
|
+
const yes = options.yes ?? false;
|
|
369
|
+
const msg = loadMessages(options.locale ?? detectLocale());
|
|
370
|
+
const detection = options.forceFresh ? null : await detectNuxtProject(cwd);
|
|
371
|
+
if (detection?.existing && detection.configPath) {
|
|
372
|
+
return runAugmentMode(options, cwd, detection.configPath, msg);
|
|
373
|
+
}
|
|
374
|
+
return runFreshMode(options, cwd, yes, msg);
|
|
375
|
+
}
|
|
376
|
+
async function runFreshMode(options, cwd, yes, msg) {
|
|
377
|
+
const projectName = yes ? options.projectName ?? "my-noy-db-app" : await promptProjectName(options.projectName, msg);
|
|
378
|
+
const adapter = yes ? options.adapter ?? "browser" : await promptAdapter(options.adapter, msg);
|
|
379
|
+
const sampleData = yes ? options.sampleData ?? true : await promptSampleData(options.sampleData, msg);
|
|
380
|
+
const projectPath = path3.resolve(cwd, projectName);
|
|
381
|
+
await assertWritableTarget(projectPath);
|
|
382
|
+
const tokens = {
|
|
383
|
+
PROJECT_NAME: projectName,
|
|
384
|
+
ADAPTER: adapter,
|
|
385
|
+
DEVTOOLS: "true",
|
|
386
|
+
SEED_INVOICES: sampleData ? JSON.stringify(DEFAULT_SEED, null, 2).replace(/\n/g, "\n ") : "[]"
|
|
387
|
+
};
|
|
388
|
+
await fs4.mkdir(projectPath, { recursive: true });
|
|
389
|
+
const files = await renderTemplate(
|
|
390
|
+
templateDir("nuxt-default"),
|
|
391
|
+
projectPath,
|
|
392
|
+
tokens
|
|
393
|
+
);
|
|
394
|
+
if (!yes) {
|
|
395
|
+
p.note(
|
|
396
|
+
[
|
|
397
|
+
`${pc.bold("cd")} ${projectName}`,
|
|
398
|
+
`${pc.bold("pnpm install")} ${pc.dim("(or npm/yarn/bun)")}`,
|
|
399
|
+
`${pc.bold("pnpm dev")}`
|
|
400
|
+
].join("\n"),
|
|
401
|
+
msg.freshNextStepsTitle
|
|
402
|
+
);
|
|
403
|
+
p.outro(pc.green(msg.freshOutroDone));
|
|
404
|
+
}
|
|
405
|
+
return {
|
|
406
|
+
kind: "fresh",
|
|
407
|
+
options: {
|
|
408
|
+
projectName,
|
|
409
|
+
adapter,
|
|
410
|
+
sampleData,
|
|
411
|
+
cwd
|
|
412
|
+
},
|
|
413
|
+
projectPath,
|
|
414
|
+
files
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
async function runAugmentMode(options, cwd, configPath, msg) {
|
|
418
|
+
const yes = options.yes ?? false;
|
|
419
|
+
const dryRun = options.dryRun ?? false;
|
|
420
|
+
if (!yes) {
|
|
421
|
+
p.note(
|
|
422
|
+
[
|
|
423
|
+
`${pc.dim(msg.augmentDetectedPrefix)}`,
|
|
424
|
+
` ${pc.cyan(configPath)}`,
|
|
425
|
+
"",
|
|
426
|
+
msg.augmentDescription
|
|
427
|
+
].join("\n"),
|
|
428
|
+
msg.augmentModeTitle
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
const adapter = yes ? options.adapter ?? "browser" : await promptAdapter(options.adapter, msg);
|
|
432
|
+
const result = await augmentNuxtConfig({
|
|
433
|
+
configPath,
|
|
434
|
+
adapter,
|
|
435
|
+
dryRun
|
|
436
|
+
});
|
|
437
|
+
if (result.kind === "already-configured") {
|
|
438
|
+
if (!yes) {
|
|
439
|
+
p.note(
|
|
440
|
+
`${pc.yellow(msg.augmentNothingToDo)} ${result.reason}`,
|
|
441
|
+
msg.augmentAlreadyConfiguredTitle
|
|
442
|
+
);
|
|
443
|
+
p.outro(pc.green(msg.augmentAlreadyOutro));
|
|
444
|
+
}
|
|
445
|
+
return {
|
|
446
|
+
kind: "augment",
|
|
447
|
+
configPath,
|
|
448
|
+
adapter,
|
|
449
|
+
changed: false,
|
|
450
|
+
reason: "already-configured"
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
if (result.kind === "unsupported-shape") {
|
|
454
|
+
if (!yes) {
|
|
455
|
+
p.cancel(`${pc.red(msg.augmentUnsupportedPrefix)} ${result.reason}`);
|
|
456
|
+
}
|
|
457
|
+
return {
|
|
458
|
+
kind: "augment",
|
|
459
|
+
configPath,
|
|
460
|
+
adapter,
|
|
461
|
+
changed: false,
|
|
462
|
+
reason: "unsupported-shape"
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
if (!yes || dryRun) {
|
|
466
|
+
p.note(renderDiff(result.diff), msg.augmentProposedChangesTitle);
|
|
467
|
+
}
|
|
468
|
+
if (dryRun) {
|
|
469
|
+
if (!yes) p.outro(pc.green(msg.augmentDryRunOutro));
|
|
470
|
+
return {
|
|
471
|
+
kind: "augment",
|
|
472
|
+
configPath,
|
|
473
|
+
adapter,
|
|
474
|
+
changed: false,
|
|
475
|
+
reason: "dry-run",
|
|
476
|
+
diff: result.diff
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
let shouldWrite = yes;
|
|
480
|
+
if (!yes) {
|
|
481
|
+
const confirmed = await p.confirm({
|
|
482
|
+
message: msg.augmentApplyConfirm,
|
|
483
|
+
initialValue: true
|
|
484
|
+
});
|
|
485
|
+
if (p.isCancel(confirmed) || confirmed !== true) {
|
|
486
|
+
p.cancel(msg.augmentAborted);
|
|
487
|
+
return {
|
|
488
|
+
kind: "augment",
|
|
489
|
+
configPath,
|
|
490
|
+
adapter,
|
|
491
|
+
changed: false,
|
|
492
|
+
reason: "cancelled",
|
|
493
|
+
diff: result.diff
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
shouldWrite = true;
|
|
497
|
+
}
|
|
498
|
+
if (shouldWrite) {
|
|
499
|
+
await writeAugmentedConfig(configPath, result.newCode);
|
|
500
|
+
if (!yes) {
|
|
501
|
+
p.note(
|
|
502
|
+
[
|
|
503
|
+
pc.dim(msg.augmentInstallIntro),
|
|
504
|
+
"",
|
|
505
|
+
`${pc.bold("pnpm add")} @noy-db/nuxt @noy-db/pinia @noy-db/core @noy-db/browser @pinia/nuxt pinia`,
|
|
506
|
+
pc.dim(msg.augmentInstallPmHint)
|
|
507
|
+
].join("\n"),
|
|
508
|
+
msg.augmentNextStepTitle
|
|
509
|
+
);
|
|
510
|
+
p.outro(pc.green(msg.augmentDoneOutro));
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return {
|
|
514
|
+
kind: "augment",
|
|
515
|
+
configPath,
|
|
516
|
+
adapter,
|
|
517
|
+
changed: true,
|
|
518
|
+
reason: "written",
|
|
519
|
+
diff: result.diff
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
function renderDiff(diff) {
|
|
523
|
+
const lines = diff.split("\n");
|
|
524
|
+
const keep = [];
|
|
525
|
+
for (const line of lines) {
|
|
526
|
+
if (line.startsWith("Index:")) continue;
|
|
527
|
+
if (line.startsWith("=")) continue;
|
|
528
|
+
if (line.startsWith("---") || line.startsWith("+++")) continue;
|
|
529
|
+
if (line.startsWith("+")) keep.push(pc.green(line));
|
|
530
|
+
else if (line.startsWith("-")) keep.push(pc.red(line));
|
|
531
|
+
else if (line.startsWith("@@")) keep.push(pc.dim(line));
|
|
532
|
+
else keep.push(line);
|
|
533
|
+
}
|
|
534
|
+
return keep.join("\n").trim();
|
|
535
|
+
}
|
|
536
|
+
async function promptProjectName(initial, msg) {
|
|
537
|
+
if (initial) {
|
|
538
|
+
const err = validateProjectName(initial);
|
|
539
|
+
if (err) throw new Error(err);
|
|
540
|
+
return initial;
|
|
541
|
+
}
|
|
542
|
+
const result = await p.text({
|
|
543
|
+
message: msg.promptProjectName,
|
|
544
|
+
placeholder: msg.promptProjectNamePlaceholder,
|
|
545
|
+
initialValue: "my-noy-db-app",
|
|
546
|
+
validate: (v) => validateProjectName(v ?? "") ?? void 0
|
|
547
|
+
});
|
|
548
|
+
if (p.isCancel(result)) {
|
|
549
|
+
p.cancel(msg.cancelled);
|
|
550
|
+
process.exit(1);
|
|
551
|
+
}
|
|
552
|
+
return result;
|
|
553
|
+
}
|
|
554
|
+
async function promptAdapter(initial, msg) {
|
|
555
|
+
if (initial) return initial;
|
|
556
|
+
const labels = adapterLabels(msg);
|
|
557
|
+
const result = await p.select({
|
|
558
|
+
message: msg.promptAdapter,
|
|
559
|
+
options: ["browser", "file", "memory"].map((value) => ({
|
|
560
|
+
value,
|
|
561
|
+
label: labels[value]
|
|
562
|
+
})),
|
|
563
|
+
initialValue: "browser"
|
|
564
|
+
});
|
|
565
|
+
if (p.isCancel(result)) {
|
|
566
|
+
p.cancel(msg.cancelled);
|
|
567
|
+
process.exit(1);
|
|
568
|
+
}
|
|
569
|
+
return result;
|
|
570
|
+
}
|
|
571
|
+
async function promptSampleData(initial, msg) {
|
|
572
|
+
if (typeof initial === "boolean") return initial;
|
|
573
|
+
const result = await p.confirm({
|
|
574
|
+
message: msg.promptSampleData,
|
|
575
|
+
initialValue: true
|
|
576
|
+
});
|
|
577
|
+
if (p.isCancel(result)) {
|
|
578
|
+
p.cancel(msg.cancelled);
|
|
579
|
+
process.exit(1);
|
|
580
|
+
}
|
|
581
|
+
return result;
|
|
582
|
+
}
|
|
583
|
+
async function assertWritableTarget(target) {
|
|
584
|
+
try {
|
|
585
|
+
const entries = await fs4.readdir(target);
|
|
586
|
+
if (entries.length > 0) {
|
|
587
|
+
throw new Error(
|
|
588
|
+
`Target directory '${target}' already exists and is not empty. Refusing to overwrite \u2014 pick a different project name or remove the directory first.`
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
} catch (err) {
|
|
592
|
+
if (err.code === "ENOENT") return;
|
|
593
|
+
throw err;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// src/commands/add.ts
|
|
598
|
+
import { promises as fs5 } from "fs";
|
|
599
|
+
import path4 from "path";
|
|
600
|
+
function validateCollectionName(name) {
|
|
601
|
+
if (!name) return "Collection name is required";
|
|
602
|
+
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
|
|
603
|
+
return "Collection name must start with a lowercase letter and contain only lowercase letters, digits, or hyphens";
|
|
604
|
+
}
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
async function addCollection(options) {
|
|
608
|
+
const err = validateCollectionName(options.name);
|
|
609
|
+
if (err) throw new Error(err);
|
|
610
|
+
const cwd = options.cwd ?? process.cwd();
|
|
611
|
+
const compartment = options.compartment ?? "default";
|
|
612
|
+
const name = options.name;
|
|
613
|
+
const PascalName = name.split("-").map((s) => s.length === 0 ? s : (s[0]?.toUpperCase() ?? "") + s.slice(1)).join("");
|
|
614
|
+
const useFnName = `use${PascalName}`;
|
|
615
|
+
const storePath = path4.join(cwd, "app", "stores", `${name}.ts`);
|
|
616
|
+
const pagePath = path4.join(cwd, "app", "pages", `${name}.vue`);
|
|
617
|
+
for (const target of [storePath, pagePath]) {
|
|
618
|
+
if (await pathExists2(target)) {
|
|
619
|
+
throw new Error(`Refusing to overwrite existing file: ${target}`);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
await fs5.mkdir(path4.dirname(storePath), { recursive: true });
|
|
623
|
+
await fs5.mkdir(path4.dirname(pagePath), { recursive: true });
|
|
624
|
+
await fs5.writeFile(storePath, renderStore(name, PascalName, useFnName, compartment), "utf8");
|
|
625
|
+
await fs5.writeFile(pagePath, renderPage(name, PascalName, useFnName), "utf8");
|
|
626
|
+
return { files: [storePath, pagePath] };
|
|
627
|
+
}
|
|
628
|
+
async function pathExists2(p2) {
|
|
629
|
+
try {
|
|
630
|
+
await fs5.access(p2);
|
|
631
|
+
return true;
|
|
632
|
+
} catch {
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
function renderStore(name, PascalName, useFnName, compartment) {
|
|
637
|
+
return `// Generated by \`noy-db add ${name}\`.
|
|
638
|
+
//
|
|
639
|
+
// Edit the ${PascalName} interface to match your domain, then call
|
|
640
|
+
// \`${useFnName}()\` from any component.
|
|
641
|
+
//
|
|
642
|
+
// defineNoydbStore is auto-imported by @noy-db/nuxt. The compartment
|
|
643
|
+
// id is the tenant/company namespace \u2014 change it if you have multiple.
|
|
644
|
+
|
|
645
|
+
export interface ${PascalName} {
|
|
646
|
+
id: string
|
|
647
|
+
name: string
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
export const ${useFnName} = defineNoydbStore<${PascalName}>('${name}', {
|
|
651
|
+
compartment: '${compartment}',
|
|
652
|
+
})
|
|
653
|
+
`;
|
|
654
|
+
}
|
|
655
|
+
function renderPage(name, PascalName, useFnName) {
|
|
656
|
+
return `<!--
|
|
657
|
+
Generated by \`noy-db add ${name}\`.
|
|
658
|
+
Visit /${name} in your dev server.
|
|
659
|
+
-->
|
|
660
|
+
<script setup lang="ts">
|
|
661
|
+
const ${name} = ${useFnName}()
|
|
662
|
+
await ${name}.$ready
|
|
663
|
+
|
|
664
|
+
function addOne() {
|
|
665
|
+
${name}.add({
|
|
666
|
+
id: crypto.randomUUID(),
|
|
667
|
+
name: 'New ${PascalName}',
|
|
668
|
+
})
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function removeOne(id: string) {
|
|
672
|
+
${name}.remove(id)
|
|
673
|
+
}
|
|
674
|
+
</script>
|
|
675
|
+
|
|
676
|
+
<template>
|
|
677
|
+
<main>
|
|
678
|
+
<h1>${PascalName}</h1>
|
|
679
|
+
<button @click="addOne">Add ${PascalName}</button>
|
|
680
|
+
<ul>
|
|
681
|
+
<li v-for="item in ${name}.items" :key="item.id">
|
|
682
|
+
{{ item.name }}
|
|
683
|
+
<button @click="removeOne(item.id)">Delete</button>
|
|
684
|
+
</li>
|
|
685
|
+
</ul>
|
|
686
|
+
</main>
|
|
687
|
+
</template>
|
|
688
|
+
`;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// src/commands/verify.ts
|
|
692
|
+
import { createNoydb } from "@noy-db/core";
|
|
693
|
+
import { memory } from "@noy-db/memory";
|
|
694
|
+
async function verifyIntegrity() {
|
|
695
|
+
const start = performance.now();
|
|
696
|
+
try {
|
|
697
|
+
const db = await createNoydb({
|
|
698
|
+
adapter: memory(),
|
|
699
|
+
user: "noy-db-verify",
|
|
700
|
+
// The passphrase here is throwaway — the in-memory adapter never
|
|
701
|
+
// persists anything, and the KEK is destroyed when we call close()
|
|
702
|
+
// a few lines down. We use a non-trivial value just to exercise
|
|
703
|
+
// PBKDF2 properly.
|
|
704
|
+
secret: "noy-db-verify-passphrase-2026"
|
|
705
|
+
});
|
|
706
|
+
const company = await db.openCompartment("verify-co");
|
|
707
|
+
const collection = company.collection("verify");
|
|
708
|
+
const original = { id: "verify-1", n: 42 };
|
|
709
|
+
await collection.put("verify-1", original);
|
|
710
|
+
const got = await collection.get("verify-1");
|
|
711
|
+
if (!got || got.id !== original.id || got.n !== original.n) {
|
|
712
|
+
return fail(start, `Round-trip mismatch: got ${JSON.stringify(got)}`);
|
|
713
|
+
}
|
|
714
|
+
const found = collection.query().where("n", "==", 42).toArray();
|
|
715
|
+
if (found.length !== 1) {
|
|
716
|
+
return fail(start, `Query DSL mismatch: expected 1 result, got ${found.length}`);
|
|
717
|
+
}
|
|
718
|
+
db.close();
|
|
719
|
+
return {
|
|
720
|
+
ok: true,
|
|
721
|
+
message: "noy-db integrity check passed",
|
|
722
|
+
durationMs: Math.round(performance.now() - start)
|
|
723
|
+
};
|
|
724
|
+
} catch (err) {
|
|
725
|
+
return fail(start, `Integrity check threw: ${err.message}`);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
function fail(start, message) {
|
|
729
|
+
return {
|
|
730
|
+
ok: false,
|
|
731
|
+
message,
|
|
732
|
+
durationMs: Math.round(performance.now() - start)
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// src/commands/rotate.ts
|
|
737
|
+
import { createNoydb as createNoydb2 } from "@noy-db/core";
|
|
738
|
+
import { jsonFile } from "@noy-db/file";
|
|
739
|
+
|
|
740
|
+
// src/commands/shared.ts
|
|
741
|
+
import { password, isCancel as isCancel2, cancel as cancel2 } from "@clack/prompts";
|
|
742
|
+
var VALID_ROLES = ["owner", "admin", "operator", "viewer", "client"];
|
|
743
|
+
var defaultReadPassphrase = async (label) => {
|
|
744
|
+
const value = await password({
|
|
745
|
+
message: label,
|
|
746
|
+
// Basic sanity: reject empty strings up front. We don't enforce
|
|
747
|
+
// length here because the caller's KEK-derivation step will
|
|
748
|
+
// reject weak passphrases with its own, richer error.
|
|
749
|
+
validate: (v) => v.length === 0 ? "Passphrase cannot be empty" : void 0
|
|
750
|
+
});
|
|
751
|
+
if (isCancel2(value)) {
|
|
752
|
+
cancel2("Cancelled.");
|
|
753
|
+
process.exit(1);
|
|
754
|
+
}
|
|
755
|
+
return value;
|
|
756
|
+
};
|
|
757
|
+
function assertRole(input) {
|
|
758
|
+
if (!VALID_ROLES.includes(input)) {
|
|
759
|
+
throw new Error(
|
|
760
|
+
`Invalid role "${input}" \u2014 must be one of: ${VALID_ROLES.join(", ")}`
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
return input;
|
|
764
|
+
}
|
|
765
|
+
function parseCollectionList(input) {
|
|
766
|
+
if (!input) return null;
|
|
767
|
+
const parts = input.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
768
|
+
return parts.length > 0 ? parts : null;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// src/commands/rotate.ts
|
|
772
|
+
async function rotate(options) {
|
|
773
|
+
const readPassphrase = options.readPassphrase ?? defaultReadPassphrase;
|
|
774
|
+
const buildAdapter = options.buildAdapter ?? ((dir) => jsonFile({ dir }));
|
|
775
|
+
const createDb = options.createDb ?? createNoydb2;
|
|
776
|
+
const secret = await readPassphrase(`Passphrase for ${options.user}`);
|
|
777
|
+
let db = null;
|
|
778
|
+
try {
|
|
779
|
+
db = await createDb({
|
|
780
|
+
adapter: buildAdapter(options.dir),
|
|
781
|
+
user: options.user,
|
|
782
|
+
secret
|
|
783
|
+
});
|
|
784
|
+
const compartment = await db.openCompartment(options.compartment);
|
|
785
|
+
const targets = options.collections && options.collections.length > 0 ? options.collections : await compartment.collections();
|
|
786
|
+
if (targets.length === 0) {
|
|
787
|
+
throw new Error(
|
|
788
|
+
`Compartment "${options.compartment}" has no collections to rotate.`
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
await db.rotate(options.compartment, targets);
|
|
792
|
+
return { rotated: targets };
|
|
793
|
+
} finally {
|
|
794
|
+
db?.close();
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// src/commands/add-user.ts
|
|
799
|
+
import { createNoydb as createNoydb3 } from "@noy-db/core";
|
|
800
|
+
import { jsonFile as jsonFile2 } from "@noy-db/file";
|
|
801
|
+
async function addUser(options) {
|
|
802
|
+
const readPassphrase = options.readPassphrase ?? defaultReadPassphrase;
|
|
803
|
+
const buildAdapter = options.buildAdapter ?? ((dir) => jsonFile2({ dir }));
|
|
804
|
+
const createDb = options.createDb ?? createNoydb3;
|
|
805
|
+
if ((options.role === "operator" || options.role === "client") && (!options.permissions || Object.keys(options.permissions).length === 0)) {
|
|
806
|
+
throw new Error(
|
|
807
|
+
`Role "${options.role}" requires explicit --collections \u2014 e.g. --collections invoices:rw,clients:ro`
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
const callerSecret = await readPassphrase(
|
|
811
|
+
`Your passphrase (${options.callerUser})`
|
|
812
|
+
);
|
|
813
|
+
const newSecret = await readPassphrase(
|
|
814
|
+
`New passphrase for ${options.newUserId}`
|
|
815
|
+
);
|
|
816
|
+
const confirmSecret = await readPassphrase(
|
|
817
|
+
`Confirm passphrase for ${options.newUserId}`
|
|
818
|
+
);
|
|
819
|
+
if (newSecret !== confirmSecret) {
|
|
820
|
+
throw new Error(`Passphrases do not match \u2014 grant aborted.`);
|
|
821
|
+
}
|
|
822
|
+
let db = null;
|
|
823
|
+
try {
|
|
824
|
+
db = await createDb({
|
|
825
|
+
adapter: buildAdapter(options.dir),
|
|
826
|
+
user: options.callerUser,
|
|
827
|
+
secret: callerSecret
|
|
828
|
+
});
|
|
829
|
+
const grantOpts = {
|
|
830
|
+
userId: options.newUserId,
|
|
831
|
+
displayName: options.newUserDisplayName ?? options.newUserId,
|
|
832
|
+
role: options.role,
|
|
833
|
+
passphrase: newSecret,
|
|
834
|
+
...options.permissions ? { permissions: options.permissions } : {}
|
|
835
|
+
};
|
|
836
|
+
await db.grant(options.compartment, grantOpts);
|
|
837
|
+
return {
|
|
838
|
+
userId: options.newUserId,
|
|
839
|
+
role: options.role
|
|
840
|
+
};
|
|
841
|
+
} finally {
|
|
842
|
+
db?.close();
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// src/commands/backup.ts
|
|
847
|
+
import { promises as fs6 } from "fs";
|
|
848
|
+
import path5 from "path";
|
|
849
|
+
import { createNoydb as createNoydb4 } from "@noy-db/core";
|
|
850
|
+
import { jsonFile as jsonFile3 } from "@noy-db/file";
|
|
851
|
+
function resolveBackupTarget(target, cwd = process.cwd()) {
|
|
852
|
+
let raw = target;
|
|
853
|
+
if (target.startsWith("file://")) {
|
|
854
|
+
raw = target.slice("file://".length);
|
|
855
|
+
} else if (target.includes("://")) {
|
|
856
|
+
throw new Error(
|
|
857
|
+
`Unsupported backup target scheme: "${target.split("://")[0]}://". Only file:// and plain filesystem paths are supported in v0.5. S3 backups will land in a follow-up.`
|
|
858
|
+
);
|
|
859
|
+
}
|
|
860
|
+
return path5.resolve(cwd, raw);
|
|
861
|
+
}
|
|
862
|
+
async function backup(options) {
|
|
863
|
+
const readPassphrase = options.readPassphrase ?? defaultReadPassphrase;
|
|
864
|
+
const buildAdapter = options.buildAdapter ?? ((dir) => jsonFile3({ dir }));
|
|
865
|
+
const createDb = options.createDb ?? createNoydb4;
|
|
866
|
+
const absolutePath = resolveBackupTarget(options.target);
|
|
867
|
+
const secret = await readPassphrase(`Passphrase for ${options.user}`);
|
|
868
|
+
let db = null;
|
|
869
|
+
try {
|
|
870
|
+
db = await createDb({
|
|
871
|
+
adapter: buildAdapter(options.dir),
|
|
872
|
+
user: options.user,
|
|
873
|
+
secret
|
|
874
|
+
});
|
|
875
|
+
const compartment = await db.openCompartment(options.compartment);
|
|
876
|
+
const serialized = await compartment.dump();
|
|
877
|
+
await fs6.mkdir(path5.dirname(absolutePath), { recursive: true });
|
|
878
|
+
await fs6.writeFile(absolutePath, serialized, "utf8");
|
|
879
|
+
return {
|
|
880
|
+
path: absolutePath,
|
|
881
|
+
bytes: Buffer.byteLength(serialized, "utf8")
|
|
882
|
+
};
|
|
883
|
+
} finally {
|
|
884
|
+
db?.close();
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
export {
|
|
888
|
+
SUPPORTED_LOCALES,
|
|
889
|
+
addCollection,
|
|
890
|
+
addUser,
|
|
891
|
+
assertRole,
|
|
892
|
+
backup,
|
|
893
|
+
detectLocale,
|
|
894
|
+
loadMessages,
|
|
895
|
+
parseCollectionList,
|
|
896
|
+
parseLocaleFlag,
|
|
897
|
+
resolveBackupTarget,
|
|
898
|
+
rotate,
|
|
899
|
+
runWizard,
|
|
900
|
+
verifyIntegrity
|
|
901
|
+
};
|
|
902
|
+
//# sourceMappingURL=index.js.map
|