zuro-cli 0.0.2-beta.13 → 0.0.2-beta.15
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/README.md +2 -1
- package/dist/chunk-R3MGV5UR.mjs +322 -0
- package/dist/chunk-R3MGV5UR.mjs.map +1 -0
- package/dist/chunk-VMOTWTER.mjs +37 -0
- package/dist/chunk-VMOTWTER.mjs.map +1 -0
- package/dist/chunk-YBAO5SKK.mjs +87 -0
- package/dist/chunk-YBAO5SKK.mjs.map +1 -0
- package/dist/database.handler-5OUD6XJZ.mjs +32 -0
- package/dist/database.handler-5OUD6XJZ.mjs.map +1 -0
- package/dist/docs.handler-JL3ZIVJQ.mjs +12 -0
- package/dist/docs.handler-JL3ZIVJQ.mjs.map +1 -0
- package/dist/index.js +1706 -535
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1247 -514
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -6,6 +6,13 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
6
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
7
|
var __getProtoOf = Object.getPrototypeOf;
|
|
8
8
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __esm = (fn, res) => function __init() {
|
|
10
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
11
|
+
};
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
9
16
|
var __copyProps = (to, from, except, desc) => {
|
|
10
17
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
18
|
for (let key of __getOwnPropNames(from))
|
|
@@ -23,6 +30,449 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
30
|
mod
|
|
24
31
|
));
|
|
25
32
|
|
|
33
|
+
// src/utils/code-inject.ts
|
|
34
|
+
function escapeRegex(value) {
|
|
35
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
36
|
+
}
|
|
37
|
+
function appendImport(source, line) {
|
|
38
|
+
if (source.includes(line)) {
|
|
39
|
+
return { source, inserted: true };
|
|
40
|
+
}
|
|
41
|
+
const importRegex = /^import .+ from .+;?\s*$/gm;
|
|
42
|
+
let lastImportIndex = 0;
|
|
43
|
+
let match;
|
|
44
|
+
while ((match = importRegex.exec(source)) !== null) {
|
|
45
|
+
lastImportIndex = match.index + match[0].length;
|
|
46
|
+
}
|
|
47
|
+
if (lastImportIndex <= 0) {
|
|
48
|
+
return { source, inserted: false };
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
source: source.slice(0, lastImportIndex) + `
|
|
52
|
+
${line}` + source.slice(lastImportIndex),
|
|
53
|
+
inserted: true
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
var init_code_inject = __esm({
|
|
57
|
+
"src/utils/code-inject.ts"() {
|
|
58
|
+
"use strict";
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// src/handlers/database.handler.ts
|
|
63
|
+
var database_handler_exports = {};
|
|
64
|
+
__export(database_handler_exports, {
|
|
65
|
+
DEFAULT_DATABASE_URLS: () => DEFAULT_DATABASE_URLS,
|
|
66
|
+
backupDatabaseFiles: () => backupDatabaseFiles,
|
|
67
|
+
databaseLabel: () => databaseLabel,
|
|
68
|
+
detectInstalledDatabaseDialect: () => detectInstalledDatabaseDialect,
|
|
69
|
+
ensureSchemaExport: () => ensureSchemaExport,
|
|
70
|
+
getDatabaseSelection: () => getDatabaseSelection,
|
|
71
|
+
getDatabaseSetupHint: () => getDatabaseSetupHint,
|
|
72
|
+
isDatabaseModule: () => isDatabaseModule,
|
|
73
|
+
isDrizzleDatabaseModule: () => isDrizzleDatabaseModule,
|
|
74
|
+
parseDatabaseDialect: () => parseDatabaseDialect,
|
|
75
|
+
printDatabaseHints: () => printDatabaseHints,
|
|
76
|
+
promptDatabaseConfig: () => promptDatabaseConfig,
|
|
77
|
+
validateDatabaseUrl: () => validateDatabaseUrl
|
|
78
|
+
});
|
|
79
|
+
function parseDatabaseDialect(value) {
|
|
80
|
+
const normalized = value?.trim().toLowerCase();
|
|
81
|
+
if (!normalized) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
if (normalized === "pg" || normalized === "postgres" || normalized === "postgresql" || normalized === "database-pg" || normalized === "drizzle-pg" || normalized === "drizzle-postgres" || normalized === "database-drizzle-pg") {
|
|
85
|
+
return "database-pg";
|
|
86
|
+
}
|
|
87
|
+
if (normalized === "mysql" || normalized === "database-mysql" || normalized === "drizzle-mysql" || normalized === "database-drizzle-mysql") {
|
|
88
|
+
return "database-mysql";
|
|
89
|
+
}
|
|
90
|
+
if (normalized === "database-prisma-pg" || normalized === "prisma-pg" || normalized === "prisma-postgres" || normalized === "prisma-postgresql" || normalized === "database-prisma") {
|
|
91
|
+
return "database-prisma-pg";
|
|
92
|
+
}
|
|
93
|
+
if (normalized === "database-prisma-mysql" || normalized === "prisma-mysql") {
|
|
94
|
+
return "database-prisma-mysql";
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
function isDatabaseModule(moduleName) {
|
|
99
|
+
return moduleName === "database-pg" || moduleName === "database-mysql" || moduleName === "database-prisma-pg" || moduleName === "database-prisma-mysql";
|
|
100
|
+
}
|
|
101
|
+
function isDrizzleDatabaseModule(moduleName) {
|
|
102
|
+
return moduleName === "database-pg" || moduleName === "database-mysql";
|
|
103
|
+
}
|
|
104
|
+
function getDatabaseSelection(moduleName) {
|
|
105
|
+
if (moduleName === "database-pg") {
|
|
106
|
+
return { orm: "drizzle", dialect: "postgresql" };
|
|
107
|
+
}
|
|
108
|
+
if (moduleName === "database-mysql") {
|
|
109
|
+
return { orm: "drizzle", dialect: "mysql" };
|
|
110
|
+
}
|
|
111
|
+
if (moduleName === "database-prisma-pg") {
|
|
112
|
+
return { orm: "prisma", dialect: "postgresql" };
|
|
113
|
+
}
|
|
114
|
+
return { orm: "prisma", dialect: "mysql" };
|
|
115
|
+
}
|
|
116
|
+
function getDatabaseModule(orm, dialect) {
|
|
117
|
+
return DATABASE_MODULE_MAP[orm][dialect];
|
|
118
|
+
}
|
|
119
|
+
function parsePrismaProvider(schemaContent) {
|
|
120
|
+
const match = schemaContent.match(/provider\s*=\s*"([^"]+)"/);
|
|
121
|
+
if (!match) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
const provider = match[1]?.trim().toLowerCase();
|
|
125
|
+
if (provider === "mysql") {
|
|
126
|
+
return "mysql";
|
|
127
|
+
}
|
|
128
|
+
if (provider === "postgresql" || provider === "postgres") {
|
|
129
|
+
return "postgresql";
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
function validateDatabaseUrl(rawUrl, moduleName) {
|
|
134
|
+
const { dialect } = getDatabaseSelection(moduleName);
|
|
135
|
+
const dbUrl = rawUrl.trim();
|
|
136
|
+
if (!dbUrl) {
|
|
137
|
+
throw new Error("Database URL cannot be empty.");
|
|
138
|
+
}
|
|
139
|
+
let parsed;
|
|
140
|
+
try {
|
|
141
|
+
parsed = new URL(dbUrl);
|
|
142
|
+
} catch {
|
|
143
|
+
throw new Error(`Invalid database URL: '${dbUrl}'.`);
|
|
144
|
+
}
|
|
145
|
+
const protocol = parsed.protocol.toLowerCase();
|
|
146
|
+
if (dialect === "postgresql" && protocol !== "postgresql:" && protocol !== "postgres:") {
|
|
147
|
+
throw new Error("PostgreSQL URL must start with postgres:// or postgresql://");
|
|
148
|
+
}
|
|
149
|
+
if (dialect === "mysql" && protocol !== "mysql:") {
|
|
150
|
+
throw new Error("MySQL URL must start with mysql://");
|
|
151
|
+
}
|
|
152
|
+
return dbUrl;
|
|
153
|
+
}
|
|
154
|
+
async function detectInstalledDatabaseDialect(projectRoot, srcDir) {
|
|
155
|
+
const dbIndexPath = import_path7.default.join(projectRoot, srcDir, "db", "index.ts");
|
|
156
|
+
const prismaSchemaPath = import_path7.default.join(projectRoot, "prisma", "schema.prisma");
|
|
157
|
+
if (import_fs_extra6.default.existsSync(dbIndexPath)) {
|
|
158
|
+
const content = await import_fs_extra6.default.readFile(dbIndexPath, "utf-8");
|
|
159
|
+
if (content.includes("drizzle-orm/node-postgres") || content.includes(`from "pg"`)) {
|
|
160
|
+
return "database-pg";
|
|
161
|
+
}
|
|
162
|
+
if (content.includes("drizzle-orm/mysql2") || content.includes(`from "mysql2`)) {
|
|
163
|
+
return "database-mysql";
|
|
164
|
+
}
|
|
165
|
+
if (content.includes(`from "@prisma/client"`)) {
|
|
166
|
+
if (import_fs_extra6.default.existsSync(prismaSchemaPath)) {
|
|
167
|
+
const schemaContent = await import_fs_extra6.default.readFile(prismaSchemaPath, "utf-8");
|
|
168
|
+
const dialect = parsePrismaProvider(schemaContent);
|
|
169
|
+
if (dialect === "mysql") {
|
|
170
|
+
return "database-prisma-mysql";
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return "database-prisma-pg";
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (import_fs_extra6.default.existsSync(prismaSchemaPath)) {
|
|
177
|
+
const schemaContent = await import_fs_extra6.default.readFile(prismaSchemaPath, "utf-8");
|
|
178
|
+
const dialect = parsePrismaProvider(schemaContent);
|
|
179
|
+
if (dialect === "mysql") {
|
|
180
|
+
return "database-prisma-mysql";
|
|
181
|
+
}
|
|
182
|
+
return "database-prisma-pg";
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
async function backupDatabaseFiles(projectRoot, srcDir) {
|
|
187
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
188
|
+
const backupRoot = import_path7.default.join(projectRoot, ".zuro", "backups", `database-${timestamp}`);
|
|
189
|
+
const candidates = [
|
|
190
|
+
import_path7.default.join(projectRoot, srcDir, "db", "index.ts"),
|
|
191
|
+
import_path7.default.join(projectRoot, "drizzle.config.ts"),
|
|
192
|
+
import_path7.default.join(projectRoot, "prisma", "schema.prisma")
|
|
193
|
+
];
|
|
194
|
+
let copied = false;
|
|
195
|
+
for (const filePath of candidates) {
|
|
196
|
+
if (!import_fs_extra6.default.existsSync(filePath)) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const relativePath = import_path7.default.relative(projectRoot, filePath);
|
|
200
|
+
const backupPath = import_path7.default.join(backupRoot, relativePath);
|
|
201
|
+
await import_fs_extra6.default.ensureDir(import_path7.default.dirname(backupPath));
|
|
202
|
+
await import_fs_extra6.default.copyFile(filePath, backupPath);
|
|
203
|
+
copied = true;
|
|
204
|
+
}
|
|
205
|
+
return copied ? backupRoot : null;
|
|
206
|
+
}
|
|
207
|
+
function databaseLabel(moduleName) {
|
|
208
|
+
const selection = getDatabaseSelection(moduleName);
|
|
209
|
+
const ormLabel = selection.orm === "drizzle" ? "Drizzle" : "Prisma";
|
|
210
|
+
const dialectLabel = selection.dialect === "postgresql" ? "PostgreSQL" : "MySQL";
|
|
211
|
+
return `${ormLabel} (${dialectLabel})`;
|
|
212
|
+
}
|
|
213
|
+
function getDatabaseSetupHint(moduleName, dbUrl) {
|
|
214
|
+
try {
|
|
215
|
+
const parsed = new URL(dbUrl);
|
|
216
|
+
const dbName = parsed.pathname.replace(/^\/+/, "") || "mydb";
|
|
217
|
+
if (moduleName === "database-pg") {
|
|
218
|
+
return `createdb ${dbName}`;
|
|
219
|
+
}
|
|
220
|
+
return `mysql -e "CREATE DATABASE IF NOT EXISTS ${dbName};"`;
|
|
221
|
+
} catch {
|
|
222
|
+
return moduleName === "database-pg" ? "createdb <database_name>" : `mysql -e "CREATE DATABASE IF NOT EXISTS <database_name>;"`;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
async function ensureSchemaExport(projectRoot, srcDir, schemaFileName) {
|
|
226
|
+
const schemaIndexPath = import_path7.default.join(projectRoot, srcDir, "db", "schema", "index.ts");
|
|
227
|
+
if (!await import_fs_extra6.default.pathExists(schemaIndexPath)) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const exportLine = `export * from "./${schemaFileName}";`;
|
|
231
|
+
const content = await import_fs_extra6.default.readFile(schemaIndexPath, "utf-8");
|
|
232
|
+
const normalized = content.replace(/\r\n/g, "\n");
|
|
233
|
+
const exportPattern = new RegExp(
|
|
234
|
+
`^\\s*export\\s*\\*\\s*from\\s*["']\\./${escapeRegex(schemaFileName)}["'];?\\s*$`,
|
|
235
|
+
"m"
|
|
236
|
+
);
|
|
237
|
+
if (exportPattern.test(normalized)) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
let next = normalized.replace(/^\s*export\s*\{\s*\};?\s*$/m, "").trimEnd();
|
|
241
|
+
if (next.length > 0) {
|
|
242
|
+
next += "\n\n";
|
|
243
|
+
}
|
|
244
|
+
next += `${exportLine}
|
|
245
|
+
`;
|
|
246
|
+
await import_fs_extra6.default.writeFile(schemaIndexPath, next);
|
|
247
|
+
}
|
|
248
|
+
async function promptDatabaseConfig(initialModuleName, projectRoot, srcDir) {
|
|
249
|
+
let resolvedModuleName;
|
|
250
|
+
if (initialModuleName === "database") {
|
|
251
|
+
const ormResponse = await (0, import_prompts2.default)({
|
|
252
|
+
type: "select",
|
|
253
|
+
name: "orm",
|
|
254
|
+
message: "Which ORM?",
|
|
255
|
+
choices: [
|
|
256
|
+
{ title: "Drizzle", value: "drizzle" },
|
|
257
|
+
{ title: "Prisma", value: "prisma" }
|
|
258
|
+
],
|
|
259
|
+
initial: 0
|
|
260
|
+
});
|
|
261
|
+
if (!ormResponse.orm) {
|
|
262
|
+
console.log(import_chalk4.default.yellow("Operation cancelled."));
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
const variantResponse = await (0, import_prompts2.default)({
|
|
266
|
+
type: "select",
|
|
267
|
+
name: "dialect",
|
|
268
|
+
message: "Which database dialect?",
|
|
269
|
+
choices: [
|
|
270
|
+
{ title: "PostgreSQL", value: "postgresql" },
|
|
271
|
+
{ title: "MySQL", value: "mysql" }
|
|
272
|
+
]
|
|
273
|
+
});
|
|
274
|
+
if (!variantResponse.dialect) {
|
|
275
|
+
console.log(import_chalk4.default.yellow("Operation cancelled."));
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
resolvedModuleName = getDatabaseModule(ormResponse.orm, variantResponse.dialect);
|
|
279
|
+
} else {
|
|
280
|
+
const parsed = parseDatabaseDialect(initialModuleName);
|
|
281
|
+
if (!parsed) {
|
|
282
|
+
throw new Error(`Unsupported database module '${initialModuleName}'.`);
|
|
283
|
+
}
|
|
284
|
+
resolvedModuleName = parsed;
|
|
285
|
+
}
|
|
286
|
+
const { orm: selectedOrm, dialect: selectedDialect } = getDatabaseSelection(resolvedModuleName);
|
|
287
|
+
let databaseBackupPath = null;
|
|
288
|
+
const installedDialect = await detectInstalledDatabaseDialect(projectRoot, srcDir);
|
|
289
|
+
if (installedDialect && installedDialect !== resolvedModuleName) {
|
|
290
|
+
console.log(
|
|
291
|
+
import_chalk4.default.yellow(
|
|
292
|
+
`
|
|
293
|
+
\u26A0 Existing database setup detected: ${databaseLabel(installedDialect)}.`
|
|
294
|
+
)
|
|
295
|
+
);
|
|
296
|
+
console.log(
|
|
297
|
+
import_chalk4.default.yellow(
|
|
298
|
+
` Switching to ${databaseLabel(resolvedModuleName)} will overwrite db files and drizzle config.
|
|
299
|
+
`
|
|
300
|
+
)
|
|
301
|
+
);
|
|
302
|
+
const switchResponse = await (0, import_prompts2.default)({
|
|
303
|
+
type: "confirm",
|
|
304
|
+
name: "proceed",
|
|
305
|
+
message: "Continue and switch database dialect?",
|
|
306
|
+
initial: false
|
|
307
|
+
});
|
|
308
|
+
if (!switchResponse.proceed) {
|
|
309
|
+
console.log(import_chalk4.default.yellow("Operation cancelled."));
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
databaseBackupPath = await backupDatabaseFiles(projectRoot, srcDir);
|
|
313
|
+
}
|
|
314
|
+
const defaultUrl = DEFAULT_DATABASE_URLS[resolvedModuleName];
|
|
315
|
+
console.log(import_chalk4.default.dim(` Tip: Leave blank to use ${defaultUrl}
|
|
316
|
+
`));
|
|
317
|
+
const response = await (0, import_prompts2.default)({
|
|
318
|
+
type: "text",
|
|
319
|
+
name: "dbUrl",
|
|
320
|
+
message: "Database URL",
|
|
321
|
+
initial: ""
|
|
322
|
+
});
|
|
323
|
+
if (response.dbUrl === void 0) {
|
|
324
|
+
console.log(import_chalk4.default.yellow("Operation cancelled."));
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
const enteredUrl = response.dbUrl?.trim() || "";
|
|
328
|
+
const usedDefaultDbUrl = enteredUrl.length === 0;
|
|
329
|
+
const customDbUrl = validateDatabaseUrl(enteredUrl || defaultUrl, resolvedModuleName);
|
|
330
|
+
return {
|
|
331
|
+
resolvedModuleName,
|
|
332
|
+
selectedOrm,
|
|
333
|
+
selectedDialect,
|
|
334
|
+
customDbUrl,
|
|
335
|
+
usedDefaultDbUrl,
|
|
336
|
+
databaseBackupPath
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
function printDatabaseHints(moduleName, customDbUrl, usedDefaultDbUrl, databaseBackupPath) {
|
|
340
|
+
if (databaseBackupPath) {
|
|
341
|
+
console.log(import_chalk4.default.blue(`\u2139 Backup created at: ${databaseBackupPath}
|
|
342
|
+
`));
|
|
343
|
+
}
|
|
344
|
+
if (usedDefaultDbUrl) {
|
|
345
|
+
console.log(import_chalk4.default.yellow("\u2139 Review DATABASE_URL in .env if your local DB config differs."));
|
|
346
|
+
}
|
|
347
|
+
const setupHint = getDatabaseSetupHint(
|
|
348
|
+
moduleName,
|
|
349
|
+
customDbUrl || DEFAULT_DATABASE_URLS[moduleName]
|
|
350
|
+
);
|
|
351
|
+
console.log(import_chalk4.default.yellow(`\u2139 Ensure DB exists: ${setupHint}`));
|
|
352
|
+
if (isDrizzleDatabaseModule(moduleName)) {
|
|
353
|
+
console.log(import_chalk4.default.yellow("\u2139 Run migrations: npx drizzle-kit generate && npx drizzle-kit migrate"));
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
console.log(import_chalk4.default.yellow("\u2139 Run migrations: npx prisma migrate dev --name init"));
|
|
357
|
+
console.log(import_chalk4.default.yellow("\u2139 Generate client: npx prisma generate"));
|
|
358
|
+
}
|
|
359
|
+
var import_path7, import_fs_extra6, import_prompts2, import_chalk4, DATABASE_MODULE_MAP, DEFAULT_DATABASE_URLS;
|
|
360
|
+
var init_database_handler = __esm({
|
|
361
|
+
"src/handlers/database.handler.ts"() {
|
|
362
|
+
"use strict";
|
|
363
|
+
import_path7 = __toESM(require("path"));
|
|
364
|
+
import_fs_extra6 = __toESM(require("fs-extra"));
|
|
365
|
+
import_prompts2 = __toESM(require("prompts"));
|
|
366
|
+
import_chalk4 = __toESM(require("chalk"));
|
|
367
|
+
init_code_inject();
|
|
368
|
+
DATABASE_MODULE_MAP = {
|
|
369
|
+
drizzle: {
|
|
370
|
+
postgresql: "database-pg",
|
|
371
|
+
mysql: "database-mysql"
|
|
372
|
+
},
|
|
373
|
+
prisma: {
|
|
374
|
+
postgresql: "database-prisma-pg",
|
|
375
|
+
mysql: "database-prisma-mysql"
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
DEFAULT_DATABASE_URLS = {
|
|
379
|
+
"database-pg": "postgresql://postgres@localhost:5432/mydb",
|
|
380
|
+
"database-mysql": "mysql://root@localhost:3306/mydb",
|
|
381
|
+
"database-prisma-pg": "postgresql://postgres@localhost:5432/mydb",
|
|
382
|
+
"database-prisma-mysql": "mysql://root@localhost:3306/mydb"
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// src/handlers/docs.handler.ts
|
|
388
|
+
var docs_handler_exports = {};
|
|
389
|
+
__export(docs_handler_exports, {
|
|
390
|
+
injectDocsRoutes: () => injectDocsRoutes,
|
|
391
|
+
isDocsModuleInstalled: () => isDocsModuleInstalled,
|
|
392
|
+
printDocsHints: () => printDocsHints
|
|
393
|
+
});
|
|
394
|
+
async function isDocsModuleInstalled(projectRoot, srcDir) {
|
|
395
|
+
return await import_fs_extra7.default.pathExists(import_path8.default.join(projectRoot, srcDir, "lib", "openapi.ts"));
|
|
396
|
+
}
|
|
397
|
+
async function injectDocsRoutes(projectRoot, srcDir) {
|
|
398
|
+
const routeIndexPath = import_path8.default.join(projectRoot, srcDir, "routes", "index.ts");
|
|
399
|
+
const routeImport = `import docsRoutes from "./docs.routes";`;
|
|
400
|
+
const routeMountPattern = /rootRouter\.use\(\s*["']\/docs["']\s*,\s*docsRoutes\s*\)/;
|
|
401
|
+
if (await import_fs_extra7.default.pathExists(routeIndexPath)) {
|
|
402
|
+
let routeContent = await import_fs_extra7.default.readFile(routeIndexPath, "utf-8");
|
|
403
|
+
let routeModified = false;
|
|
404
|
+
const importResult = appendImport(routeContent, routeImport);
|
|
405
|
+
if (!importResult.inserted) {
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
if (importResult.source !== routeContent) {
|
|
409
|
+
routeContent = importResult.source;
|
|
410
|
+
routeModified = true;
|
|
411
|
+
}
|
|
412
|
+
if (!routeMountPattern.test(routeContent)) {
|
|
413
|
+
const routeSetup = `
|
|
414
|
+
// API docs
|
|
415
|
+
rootRouter.use("/docs", docsRoutes);
|
|
416
|
+
`;
|
|
417
|
+
const exportMatch = routeContent.match(/export default rootRouter;?\s*$/m);
|
|
418
|
+
if (!exportMatch || exportMatch.index === void 0) {
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
routeContent = routeContent.slice(0, exportMatch.index) + routeSetup + "\n" + routeContent.slice(exportMatch.index);
|
|
422
|
+
routeModified = true;
|
|
423
|
+
}
|
|
424
|
+
if (routeModified) {
|
|
425
|
+
await import_fs_extra7.default.writeFile(routeIndexPath, routeContent);
|
|
426
|
+
}
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
const appPath = import_path8.default.join(projectRoot, srcDir, "app.ts");
|
|
430
|
+
if (!await import_fs_extra7.default.pathExists(appPath)) {
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
let appContent = await import_fs_extra7.default.readFile(appPath, "utf-8");
|
|
434
|
+
let appModified = false;
|
|
435
|
+
const appImportResult = appendImport(appContent, `import docsRoutes from "./routes/docs.routes";`);
|
|
436
|
+
if (!appImportResult.inserted) {
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
if (appImportResult.source !== appContent) {
|
|
440
|
+
appContent = appImportResult.source;
|
|
441
|
+
appModified = true;
|
|
442
|
+
}
|
|
443
|
+
const hasMount = /app\.use\(\s*["']\/api\/docs["']\s*,\s*docsRoutes\s*\)/.test(appContent);
|
|
444
|
+
if (!hasMount) {
|
|
445
|
+
const setup = `
|
|
446
|
+
// API docs
|
|
447
|
+
app.use("/api/docs", docsRoutes);
|
|
448
|
+
`;
|
|
449
|
+
const exportMatch = appContent.match(/export default app;?\s*$/m);
|
|
450
|
+
if (!exportMatch || exportMatch.index === void 0) {
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
appContent = appContent.slice(0, exportMatch.index) + setup + "\n" + appContent.slice(exportMatch.index);
|
|
454
|
+
appModified = true;
|
|
455
|
+
}
|
|
456
|
+
if (appModified) {
|
|
457
|
+
await import_fs_extra7.default.writeFile(appPath, appContent);
|
|
458
|
+
}
|
|
459
|
+
return true;
|
|
460
|
+
}
|
|
461
|
+
function printDocsHints() {
|
|
462
|
+
const chalk9 = require("chalk");
|
|
463
|
+
console.log(chalk9.yellow("\u2139 API docs available at: /api/docs"));
|
|
464
|
+
console.log(chalk9.yellow("\u2139 OpenAPI spec available at: /api/docs/openapi.json"));
|
|
465
|
+
}
|
|
466
|
+
var import_path8, import_fs_extra7;
|
|
467
|
+
var init_docs_handler = __esm({
|
|
468
|
+
"src/handlers/docs.handler.ts"() {
|
|
469
|
+
"use strict";
|
|
470
|
+
import_path8 = __toESM(require("path"));
|
|
471
|
+
import_fs_extra7 = __toESM(require("fs-extra"));
|
|
472
|
+
init_code_inject();
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
|
|
26
476
|
// src/index.ts
|
|
27
477
|
var import_commander = require("commander");
|
|
28
478
|
|
|
@@ -32,6 +482,7 @@ var import_chalk2 = __toESM(require("chalk"));
|
|
|
32
482
|
var import_fs_extra4 = __toESM(require("fs-extra"));
|
|
33
483
|
var import_path4 = __toESM(require("path"));
|
|
34
484
|
var import_prompts = __toESM(require("prompts"));
|
|
485
|
+
var import_child_process = require("child_process");
|
|
35
486
|
|
|
36
487
|
// src/utils/registry.ts
|
|
37
488
|
var import_node_crypto = require("crypto");
|
|
@@ -348,23 +799,39 @@ var ENV_CONFIGS = {
|
|
|
348
799
|
{ name: "DATABASE_URL", schema: "z.string().url()" }
|
|
349
800
|
]
|
|
350
801
|
},
|
|
351
|
-
|
|
802
|
+
"database-prisma-pg": {
|
|
352
803
|
envVars: {
|
|
353
|
-
|
|
354
|
-
BETTER_AUTH_URL: "http://localhost:3000"
|
|
804
|
+
DATABASE_URL: "postgresql://postgres@localhost:5432/mydb"
|
|
355
805
|
},
|
|
356
806
|
schemaFields: [
|
|
357
|
-
{ name: "
|
|
358
|
-
{ name: "BETTER_AUTH_URL", schema: "z.string().url()" }
|
|
807
|
+
{ name: "DATABASE_URL", schema: "z.string().url()" }
|
|
359
808
|
]
|
|
360
809
|
},
|
|
361
|
-
|
|
810
|
+
"database-prisma-mysql": {
|
|
362
811
|
envVars: {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
812
|
+
DATABASE_URL: "mysql://root@localhost:3306/mydb"
|
|
813
|
+
},
|
|
814
|
+
schemaFields: [
|
|
815
|
+
{ name: "DATABASE_URL", schema: "z.string().url()" }
|
|
816
|
+
]
|
|
817
|
+
},
|
|
818
|
+
auth: {
|
|
819
|
+
envVars: {
|
|
820
|
+
BETTER_AUTH_SECRET: "your-secret-key-at-least-32-characters-long",
|
|
821
|
+
BETTER_AUTH_URL: "http://localhost:3000"
|
|
822
|
+
},
|
|
823
|
+
schemaFields: [
|
|
824
|
+
{ name: "BETTER_AUTH_SECRET", schema: "z.string().min(32)" },
|
|
825
|
+
{ name: "BETTER_AUTH_URL", schema: "z.string().url()" }
|
|
826
|
+
]
|
|
827
|
+
},
|
|
828
|
+
mailer: {
|
|
829
|
+
envVars: {
|
|
830
|
+
SMTP_HOST: "smtp.example.com",
|
|
831
|
+
SMTP_PORT: "587",
|
|
832
|
+
SMTP_USER: "your-email@example.com",
|
|
833
|
+
SMTP_PASS: "your-password",
|
|
834
|
+
MAIL_FROM: "noreply@example.com"
|
|
368
835
|
},
|
|
369
836
|
schemaFields: [
|
|
370
837
|
{ name: "SMTP_HOST", schema: "z.string().min(1)" },
|
|
@@ -383,6 +850,16 @@ var ENV_CONFIGS = {
|
|
|
383
850
|
{ name: "RESEND_API_KEY", schema: "z.string().min(1)" },
|
|
384
851
|
{ name: "MAIL_FROM", schema: "z.string().min(1)" }
|
|
385
852
|
]
|
|
853
|
+
},
|
|
854
|
+
"rate-limiter": {
|
|
855
|
+
envVars: {
|
|
856
|
+
RATE_LIMIT_WINDOW_MS: "900000",
|
|
857
|
+
RATE_LIMIT_MAX: "100"
|
|
858
|
+
},
|
|
859
|
+
schemaFields: [
|
|
860
|
+
{ name: "RATE_LIMIT_WINDOW_MS", schema: "z.coerce.number().default(900000)" },
|
|
861
|
+
{ name: "RATE_LIMIT_MAX", schema: "z.coerce.number().default(100)" }
|
|
862
|
+
]
|
|
386
863
|
}
|
|
387
864
|
};
|
|
388
865
|
|
|
@@ -404,6 +881,19 @@ function sanitizeConfig(input) {
|
|
|
404
881
|
if (typeof raw.srcDir === "string") {
|
|
405
882
|
config.srcDir = raw.srcDir;
|
|
406
883
|
}
|
|
884
|
+
if (raw.database && typeof raw.database === "object") {
|
|
885
|
+
const dbRaw = raw.database;
|
|
886
|
+
const database = {};
|
|
887
|
+
if (dbRaw.orm === "drizzle" || dbRaw.orm === "prisma") {
|
|
888
|
+
database.orm = dbRaw.orm;
|
|
889
|
+
}
|
|
890
|
+
if (dbRaw.dialect === "postgresql" || dbRaw.dialect === "mysql") {
|
|
891
|
+
database.dialect = dbRaw.dialect;
|
|
892
|
+
}
|
|
893
|
+
if (database.orm || database.dialect) {
|
|
894
|
+
config.database = database;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
407
897
|
return config;
|
|
408
898
|
}
|
|
409
899
|
function getConfigPath(cwd) {
|
|
@@ -496,6 +986,62 @@ bun.lockb
|
|
|
496
986
|
await import_fs_extra4.default.writeFile(prettierIgnorePath, ignoreContent);
|
|
497
987
|
}
|
|
498
988
|
}
|
|
989
|
+
async function setupGitignore(targetDir) {
|
|
990
|
+
const gitignorePath = import_path4.default.join(targetDir, ".gitignore");
|
|
991
|
+
if (await import_fs_extra4.default.pathExists(gitignorePath)) {
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
const gitignoreContent = `# dependencies
|
|
995
|
+
node_modules
|
|
996
|
+
|
|
997
|
+
# build output
|
|
998
|
+
dist
|
|
999
|
+
build
|
|
1000
|
+
|
|
1001
|
+
# environment variables
|
|
1002
|
+
.env
|
|
1003
|
+
.env.*
|
|
1004
|
+
!.env.example
|
|
1005
|
+
|
|
1006
|
+
# logs
|
|
1007
|
+
*.log
|
|
1008
|
+
npm-debug.log*
|
|
1009
|
+
pnpm-debug.log*
|
|
1010
|
+
|
|
1011
|
+
# coverage
|
|
1012
|
+
coverage
|
|
1013
|
+
|
|
1014
|
+
# OS files
|
|
1015
|
+
.DS_Store
|
|
1016
|
+
Thumbs.db
|
|
1017
|
+
|
|
1018
|
+
# IDE
|
|
1019
|
+
.vscode
|
|
1020
|
+
.idea
|
|
1021
|
+
*.swp
|
|
1022
|
+
*.swo
|
|
1023
|
+
`;
|
|
1024
|
+
await import_fs_extra4.default.writeFile(gitignorePath, gitignoreContent);
|
|
1025
|
+
}
|
|
1026
|
+
function tryGitInit(targetDir) {
|
|
1027
|
+
try {
|
|
1028
|
+
(0, import_child_process.execSync)("git rev-parse --is-inside-work-tree", {
|
|
1029
|
+
cwd: targetDir,
|
|
1030
|
+
stdio: "ignore"
|
|
1031
|
+
});
|
|
1032
|
+
return;
|
|
1033
|
+
} catch {
|
|
1034
|
+
}
|
|
1035
|
+
try {
|
|
1036
|
+
(0, import_child_process.execSync)("git init", { cwd: targetDir, stdio: "ignore" });
|
|
1037
|
+
(0, import_child_process.execSync)("git add -A", { cwd: targetDir, stdio: "ignore" });
|
|
1038
|
+
(0, import_child_process.execSync)('git commit -m "Initial commit from zuro-cli"', {
|
|
1039
|
+
cwd: targetDir,
|
|
1040
|
+
stdio: "ignore"
|
|
1041
|
+
});
|
|
1042
|
+
} catch {
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
499
1045
|
async function init() {
|
|
500
1046
|
const cwd = process.cwd();
|
|
501
1047
|
const isExistingProject = await import_fs_extra4.default.pathExists(import_path4.default.join(cwd, "package.json"));
|
|
@@ -647,8 +1193,16 @@ async function init() {
|
|
|
647
1193
|
currentStep = "prettier setup";
|
|
648
1194
|
await setupPrettier(targetDir);
|
|
649
1195
|
}
|
|
1196
|
+
currentStep = "gitignore setup";
|
|
1197
|
+
spinner.text = "Setting up .gitignore...";
|
|
1198
|
+
await setupGitignore(targetDir);
|
|
650
1199
|
currentStep = "config write";
|
|
651
1200
|
await writeZuroConfig(targetDir, zuroConfig);
|
|
1201
|
+
if (!isExistingProject) {
|
|
1202
|
+
currentStep = "git init";
|
|
1203
|
+
spinner.text = "Initializing git repository...";
|
|
1204
|
+
tryGitInit(targetDir);
|
|
1205
|
+
}
|
|
652
1206
|
spinner.succeed(import_chalk2.default.green("Project initialized successfully!"));
|
|
653
1207
|
console.log(`
|
|
654
1208
|
${import_chalk2.default.bold("Next steps:")}`);
|
|
@@ -672,10 +1226,9 @@ ${import_chalk2.default.bold("Retry:")}`);
|
|
|
672
1226
|
}
|
|
673
1227
|
|
|
674
1228
|
// src/commands/add.ts
|
|
675
|
-
var import_prompts2 = __toESM(require("prompts"));
|
|
676
1229
|
var import_ora2 = __toESM(require("ora"));
|
|
677
|
-
var
|
|
678
|
-
var
|
|
1230
|
+
var import_path13 = __toESM(require("path"));
|
|
1231
|
+
var import_fs_extra12 = __toESM(require("fs-extra"));
|
|
679
1232
|
var import_node_crypto2 = require("crypto");
|
|
680
1233
|
|
|
681
1234
|
// src/utils/dependency.ts
|
|
@@ -691,7 +1244,8 @@ var BLOCK_SIGNATURES = {
|
|
|
691
1244
|
logger: "lib/logger.ts",
|
|
692
1245
|
auth: "lib/auth.ts",
|
|
693
1246
|
mailer: "lib/mailer.ts",
|
|
694
|
-
docs: "lib/openapi.ts"
|
|
1247
|
+
docs: "lib/openapi.ts",
|
|
1248
|
+
"rate-limiter": "middleware/rate-limiter.ts"
|
|
695
1249
|
};
|
|
696
1250
|
var resolveDependencies = async (moduleDependencies, cwd) => {
|
|
697
1251
|
if (!moduleDependencies || moduleDependencies.length === 0) {
|
|
@@ -701,9 +1255,17 @@ var resolveDependencies = async (moduleDependencies, cwd) => {
|
|
|
701
1255
|
const srcDir = config?.srcDir || "src";
|
|
702
1256
|
for (const dep of moduleDependencies) {
|
|
703
1257
|
if (dep === "database") {
|
|
1258
|
+
const configuredOrm = config?.database?.orm;
|
|
1259
|
+
const configuredDialect = config?.database?.dialect;
|
|
704
1260
|
const pgExists = import_fs_extra5.default.existsSync(import_path5.default.join(cwd, srcDir, BLOCK_SIGNATURES["database-pg"]));
|
|
705
1261
|
const mysqlExists = import_fs_extra5.default.existsSync(import_path5.default.join(cwd, srcDir, BLOCK_SIGNATURES["database-mysql"]));
|
|
706
|
-
|
|
1262
|
+
const prismaSchemaExists = import_fs_extra5.default.existsSync(import_path5.default.join(cwd, "prisma", "schema.prisma"));
|
|
1263
|
+
const hasDbFiles = pgExists || mysqlExists || prismaSchemaExists;
|
|
1264
|
+
const hasDbConfig = Boolean(configuredOrm && configuredDialect);
|
|
1265
|
+
if (hasDbConfig && hasDbFiles) {
|
|
1266
|
+
continue;
|
|
1267
|
+
}
|
|
1268
|
+
if (!hasDbConfig && hasDbFiles) {
|
|
707
1269
|
continue;
|
|
708
1270
|
}
|
|
709
1271
|
console.log(import_chalk3.default.blue(`\u2139 Dependency '${dep}' is missing. Triggering install...`));
|
|
@@ -722,12 +1284,8 @@ var resolveDependencies = async (moduleDependencies, cwd) => {
|
|
|
722
1284
|
}
|
|
723
1285
|
};
|
|
724
1286
|
|
|
725
|
-
// src/
|
|
726
|
-
var
|
|
727
|
-
var DEFAULT_DATABASE_URLS = {
|
|
728
|
-
"database-pg": "postgresql://postgres@localhost:5432/mydb",
|
|
729
|
-
"database-mysql": "mysql://root@localhost:3306/mydb"
|
|
730
|
-
};
|
|
1287
|
+
// src/utils/paths.ts
|
|
1288
|
+
var import_path6 = __toESM(require("path"));
|
|
731
1289
|
function resolveSafeTargetPath2(projectRoot, srcDir, file) {
|
|
732
1290
|
const targetPath = import_path6.default.resolve(projectRoot, srcDir, file.target);
|
|
733
1291
|
const normalizedRoot = import_path6.default.resolve(projectRoot);
|
|
@@ -736,231 +1294,27 @@ function resolveSafeTargetPath2(projectRoot, srcDir, file) {
|
|
|
736
1294
|
}
|
|
737
1295
|
return targetPath;
|
|
738
1296
|
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
function parseDatabaseDialect(value) {
|
|
752
|
-
const normalized = value?.trim().toLowerCase();
|
|
753
|
-
if (!normalized) {
|
|
754
|
-
return null;
|
|
755
|
-
}
|
|
756
|
-
if (normalized === "pg" || normalized === "postgres" || normalized === "postgresql" || normalized === "database-pg") {
|
|
757
|
-
return "database-pg";
|
|
758
|
-
}
|
|
759
|
-
if (normalized === "mysql" || normalized === "database-mysql") {
|
|
760
|
-
return "database-mysql";
|
|
761
|
-
}
|
|
762
|
-
return null;
|
|
763
|
-
}
|
|
764
|
-
function isDatabaseModule(moduleName) {
|
|
765
|
-
return moduleName === "database-pg" || moduleName === "database-mysql";
|
|
766
|
-
}
|
|
767
|
-
function validateDatabaseUrl(rawUrl, moduleName) {
|
|
768
|
-
const dbUrl = rawUrl.trim();
|
|
769
|
-
if (!dbUrl) {
|
|
770
|
-
throw new Error("Database URL cannot be empty.");
|
|
771
|
-
}
|
|
772
|
-
let parsed;
|
|
773
|
-
try {
|
|
774
|
-
parsed = new URL(dbUrl);
|
|
775
|
-
} catch {
|
|
776
|
-
throw new Error(`Invalid database URL: '${dbUrl}'.`);
|
|
777
|
-
}
|
|
778
|
-
const protocol = parsed.protocol.toLowerCase();
|
|
779
|
-
if (moduleName === "database-pg" && protocol !== "postgresql:" && protocol !== "postgres:") {
|
|
780
|
-
throw new Error("PostgreSQL URL must start with postgres:// or postgresql://");
|
|
781
|
-
}
|
|
782
|
-
if (moduleName === "database-mysql" && protocol !== "mysql:") {
|
|
783
|
-
throw new Error("MySQL URL must start with mysql://");
|
|
784
|
-
}
|
|
785
|
-
return dbUrl;
|
|
786
|
-
}
|
|
787
|
-
async function detectInstalledDatabaseDialect(projectRoot, srcDir) {
|
|
788
|
-
const dbIndexPath = import_path6.default.join(projectRoot, srcDir, "db", "index.ts");
|
|
789
|
-
if (!import_fs_extra6.default.existsSync(dbIndexPath)) {
|
|
790
|
-
return null;
|
|
791
|
-
}
|
|
792
|
-
const content = await import_fs_extra6.default.readFile(dbIndexPath, "utf-8");
|
|
793
|
-
if (content.includes("drizzle-orm/node-postgres") || content.includes(`from "pg"`)) {
|
|
794
|
-
return "database-pg";
|
|
795
|
-
}
|
|
796
|
-
if (content.includes("drizzle-orm/mysql2") || content.includes(`from "mysql2`)) {
|
|
797
|
-
return "database-mysql";
|
|
798
|
-
}
|
|
799
|
-
return null;
|
|
800
|
-
}
|
|
801
|
-
async function backupDatabaseFiles(projectRoot, srcDir) {
|
|
802
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
803
|
-
const backupRoot = import_path6.default.join(projectRoot, ".zuro", "backups", `database-${timestamp}`);
|
|
804
|
-
const candidates = [
|
|
805
|
-
import_path6.default.join(projectRoot, srcDir, "db", "index.ts"),
|
|
806
|
-
import_path6.default.join(projectRoot, "drizzle.config.ts")
|
|
807
|
-
];
|
|
808
|
-
let copied = false;
|
|
809
|
-
for (const filePath of candidates) {
|
|
810
|
-
if (!import_fs_extra6.default.existsSync(filePath)) {
|
|
811
|
-
continue;
|
|
812
|
-
}
|
|
813
|
-
const relativePath = import_path6.default.relative(projectRoot, filePath);
|
|
814
|
-
const backupPath = import_path6.default.join(backupRoot, relativePath);
|
|
815
|
-
await import_fs_extra6.default.ensureDir(import_path6.default.dirname(backupPath));
|
|
816
|
-
await import_fs_extra6.default.copyFile(filePath, backupPath);
|
|
817
|
-
copied = true;
|
|
818
|
-
}
|
|
819
|
-
return copied ? backupRoot : null;
|
|
820
|
-
}
|
|
821
|
-
function databaseLabel(moduleName) {
|
|
822
|
-
return moduleName === "database-pg" ? "PostgreSQL" : "MySQL";
|
|
823
|
-
}
|
|
824
|
-
function getDatabaseSetupHint(moduleName, dbUrl) {
|
|
825
|
-
try {
|
|
826
|
-
const parsed = new URL(dbUrl);
|
|
827
|
-
const dbName = parsed.pathname.replace(/^\/+/, "") || "mydb";
|
|
828
|
-
if (moduleName === "database-pg") {
|
|
829
|
-
return `createdb ${dbName}`;
|
|
830
|
-
}
|
|
831
|
-
return `mysql -e "CREATE DATABASE IF NOT EXISTS ${dbName};"`;
|
|
832
|
-
} catch {
|
|
833
|
-
return moduleName === "database-pg" ? "createdb <database_name>" : `mysql -e "CREATE DATABASE IF NOT EXISTS <database_name>;"`;
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
function getModuleDocsPath(moduleName) {
|
|
837
|
-
if (isDatabaseModule(moduleName)) {
|
|
838
|
-
return "database";
|
|
839
|
-
}
|
|
840
|
-
return moduleName;
|
|
841
|
-
}
|
|
842
|
-
function escapeRegex(value) {
|
|
843
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
844
|
-
}
|
|
845
|
-
function appendImport(source, line) {
|
|
846
|
-
if (source.includes(line)) {
|
|
847
|
-
return { source, inserted: true };
|
|
848
|
-
}
|
|
849
|
-
const importRegex = /^import .+ from .+;?\s*$/gm;
|
|
850
|
-
let lastImportIndex = 0;
|
|
851
|
-
let match;
|
|
852
|
-
while ((match = importRegex.exec(source)) !== null) {
|
|
853
|
-
lastImportIndex = match.index + match[0].length;
|
|
854
|
-
}
|
|
855
|
-
if (lastImportIndex <= 0) {
|
|
856
|
-
return { source, inserted: false };
|
|
857
|
-
}
|
|
858
|
-
return {
|
|
859
|
-
source: source.slice(0, lastImportIndex) + `
|
|
860
|
-
${line}` + source.slice(lastImportIndex),
|
|
861
|
-
inserted: true
|
|
862
|
-
};
|
|
863
|
-
}
|
|
864
|
-
async function hasEnvVariable(projectRoot, key) {
|
|
865
|
-
const envPath = import_path6.default.join(projectRoot, ".env");
|
|
866
|
-
if (!await import_fs_extra6.default.pathExists(envPath)) {
|
|
867
|
-
return false;
|
|
868
|
-
}
|
|
869
|
-
const content = await import_fs_extra6.default.readFile(envPath, "utf-8");
|
|
870
|
-
const pattern = new RegExp(`^${escapeRegex(key)}=`, "m");
|
|
871
|
-
return pattern.test(content);
|
|
872
|
-
}
|
|
873
|
-
async function isLikelyEmptyDirectory(cwd) {
|
|
874
|
-
const entries = await import_fs_extra6.default.readdir(cwd);
|
|
875
|
-
const ignored = /* @__PURE__ */ new Set([".ds_store", "thumbs.db"]);
|
|
876
|
-
return entries.filter((entry) => !ignored.has(entry.toLowerCase())).length === 0;
|
|
877
|
-
}
|
|
878
|
-
async function ensureSchemaExport(projectRoot, srcDir, schemaFileName) {
|
|
879
|
-
const schemaIndexPath = import_path6.default.join(projectRoot, srcDir, "db", "schema", "index.ts");
|
|
880
|
-
if (!await import_fs_extra6.default.pathExists(schemaIndexPath)) {
|
|
881
|
-
return;
|
|
882
|
-
}
|
|
883
|
-
const exportLine = `export * from "./${schemaFileName}";`;
|
|
884
|
-
const content = await import_fs_extra6.default.readFile(schemaIndexPath, "utf-8");
|
|
885
|
-
const normalized = content.replace(/\r\n/g, "\n");
|
|
886
|
-
const exportPattern = new RegExp(
|
|
887
|
-
`^\\s*export\\s*\\*\\s*from\\s*["']\\./${escapeRegex(schemaFileName)}["'];?\\s*$`,
|
|
888
|
-
"m"
|
|
889
|
-
);
|
|
890
|
-
if (exportPattern.test(normalized)) {
|
|
891
|
-
return;
|
|
892
|
-
}
|
|
893
|
-
let next = normalized.replace(/^\s*export\s*\{\s*\};?\s*$/m, "").trimEnd();
|
|
894
|
-
if (next.length > 0) {
|
|
895
|
-
next += "\n\n";
|
|
896
|
-
}
|
|
897
|
-
next += `${exportLine}
|
|
898
|
-
`;
|
|
899
|
-
await import_fs_extra6.default.writeFile(schemaIndexPath, next);
|
|
900
|
-
}
|
|
901
|
-
async function isDocsModuleInstalled(projectRoot, srcDir) {
|
|
902
|
-
return await import_fs_extra6.default.pathExists(import_path6.default.join(projectRoot, srcDir, "lib", "openapi.ts"));
|
|
903
|
-
}
|
|
1297
|
+
|
|
1298
|
+
// src/commands/add.ts
|
|
1299
|
+
init_code_inject();
|
|
1300
|
+
var import_chalk8 = __toESM(require("chalk"));
|
|
1301
|
+
init_database_handler();
|
|
1302
|
+
|
|
1303
|
+
// src/handlers/auth.handler.ts
|
|
1304
|
+
var import_path9 = __toESM(require("path"));
|
|
1305
|
+
var import_fs_extra8 = __toESM(require("fs-extra"));
|
|
1306
|
+
var import_prompts3 = __toESM(require("prompts"));
|
|
1307
|
+
var import_chalk5 = __toESM(require("chalk"));
|
|
1308
|
+
init_code_inject();
|
|
904
1309
|
async function isAuthModuleInstalled(projectRoot, srcDir) {
|
|
905
|
-
return await
|
|
906
|
-
}
|
|
907
|
-
async function injectErrorHandler(projectRoot, srcDir) {
|
|
908
|
-
const appPath = import_path6.default.join(projectRoot, srcDir, "app.ts");
|
|
909
|
-
if (!import_fs_extra6.default.existsSync(appPath)) {
|
|
910
|
-
return false;
|
|
911
|
-
}
|
|
912
|
-
let content = await import_fs_extra6.default.readFile(appPath, "utf-8");
|
|
913
|
-
const errorImport = `import { errorHandler, notFoundHandler } from "./middleware/error-handler";`;
|
|
914
|
-
const hasErrorImport = content.includes(errorImport);
|
|
915
|
-
const hasNotFoundUse = /app\.use\(\s*notFoundHandler\s*\)/.test(content);
|
|
916
|
-
const hasErrorUse = /app\.use\(\s*errorHandler\s*\)/.test(content);
|
|
917
|
-
let modified = false;
|
|
918
|
-
let importInserted = hasErrorImport;
|
|
919
|
-
if (!hasErrorImport) {
|
|
920
|
-
const importRegex = /^import .+ from .+;?\s*$/gm;
|
|
921
|
-
let lastImportIndex = 0;
|
|
922
|
-
let match;
|
|
923
|
-
while ((match = importRegex.exec(content)) !== null) {
|
|
924
|
-
lastImportIndex = match.index + match[0].length;
|
|
925
|
-
}
|
|
926
|
-
if (lastImportIndex > 0) {
|
|
927
|
-
content = content.slice(0, lastImportIndex) + `
|
|
928
|
-
${errorImport}` + content.slice(lastImportIndex);
|
|
929
|
-
modified = true;
|
|
930
|
-
importInserted = true;
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
let setupInserted = hasNotFoundUse && hasErrorUse;
|
|
934
|
-
if (!setupInserted) {
|
|
935
|
-
const setupLines = [];
|
|
936
|
-
if (!hasNotFoundUse) {
|
|
937
|
-
setupLines.push("app.use(notFoundHandler);");
|
|
938
|
-
}
|
|
939
|
-
if (!hasErrorUse) {
|
|
940
|
-
setupLines.push("app.use(errorHandler);");
|
|
941
|
-
}
|
|
942
|
-
const errorSetup = `
|
|
943
|
-
// Error handling (must be last)
|
|
944
|
-
${setupLines.join("\n")}
|
|
945
|
-
`;
|
|
946
|
-
const exportMatch = content.match(/export default app;?\s*$/m);
|
|
947
|
-
if (exportMatch && exportMatch.index !== void 0) {
|
|
948
|
-
content = content.slice(0, exportMatch.index) + errorSetup + "\n" + content.slice(exportMatch.index);
|
|
949
|
-
modified = true;
|
|
950
|
-
setupInserted = true;
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
if (modified) {
|
|
954
|
-
await import_fs_extra6.default.writeFile(appPath, content);
|
|
955
|
-
}
|
|
956
|
-
return importInserted && setupInserted;
|
|
1310
|
+
return await import_fs_extra8.default.pathExists(import_path9.default.join(projectRoot, srcDir, "lib", "auth.ts"));
|
|
957
1311
|
}
|
|
958
1312
|
async function injectAuthRoutes(projectRoot, srcDir) {
|
|
959
|
-
const appPath =
|
|
960
|
-
if (!await
|
|
1313
|
+
const appPath = import_path9.default.join(projectRoot, srcDir, "app.ts");
|
|
1314
|
+
if (!await import_fs_extra8.default.pathExists(appPath)) {
|
|
961
1315
|
return false;
|
|
962
1316
|
}
|
|
963
|
-
let appContent = await
|
|
1317
|
+
let appContent = await import_fs_extra8.default.readFile(appPath, "utf-8");
|
|
964
1318
|
const authHandlerImport = `import { toNodeHandler } from "better-auth/node";`;
|
|
965
1319
|
const authImport = `import { auth } from "./lib/auth";`;
|
|
966
1320
|
const routeIndexUserImport = `import userRoutes from "./user.routes";`;
|
|
@@ -995,9 +1349,9 @@ async function injectAuthRoutes(projectRoot, srcDir) {
|
|
|
995
1349
|
appContent = appContent.slice(0, insertionIndex) + authMountLine + appContent.slice(insertionIndex);
|
|
996
1350
|
appModified = true;
|
|
997
1351
|
}
|
|
998
|
-
const routeIndexPath =
|
|
999
|
-
if (await
|
|
1000
|
-
let routeContent = await
|
|
1352
|
+
const routeIndexPath = import_path9.default.join(projectRoot, srcDir, "routes", "index.ts");
|
|
1353
|
+
if (await import_fs_extra8.default.pathExists(routeIndexPath)) {
|
|
1354
|
+
let routeContent = await import_fs_extra8.default.readFile(routeIndexPath, "utf-8");
|
|
1001
1355
|
let routeModified = false;
|
|
1002
1356
|
const userImportResult = appendImport(routeContent, routeIndexUserImport);
|
|
1003
1357
|
if (!userImportResult.inserted) {
|
|
@@ -1021,7 +1375,7 @@ rootRouter.use("/users", userRoutes);
|
|
|
1021
1375
|
routeModified = true;
|
|
1022
1376
|
}
|
|
1023
1377
|
if (routeModified) {
|
|
1024
|
-
await
|
|
1378
|
+
await import_fs_extra8.default.writeFile(routeIndexPath, routeContent);
|
|
1025
1379
|
}
|
|
1026
1380
|
} else {
|
|
1027
1381
|
const hasUserRoute = /app\.use\(\s*["']\/api\/users["']\s*,\s*userRoutes\s*\)/.test(appContent);
|
|
@@ -1047,17 +1401,766 @@ app.use("/api/users", userRoutes);
|
|
|
1047
1401
|
}
|
|
1048
1402
|
}
|
|
1049
1403
|
if (appModified) {
|
|
1050
|
-
await
|
|
1404
|
+
await import_fs_extra8.default.writeFile(appPath, appContent);
|
|
1051
1405
|
}
|
|
1052
1406
|
return true;
|
|
1053
1407
|
}
|
|
1054
|
-
async function
|
|
1055
|
-
const
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1408
|
+
async function injectAuthDocs(projectRoot, srcDir) {
|
|
1409
|
+
const openApiPath = import_path9.default.join(projectRoot, srcDir, "lib", "openapi.ts");
|
|
1410
|
+
if (!await import_fs_extra8.default.pathExists(openApiPath)) {
|
|
1411
|
+
return false;
|
|
1412
|
+
}
|
|
1413
|
+
const authMarker = "// ZURO_AUTH_DOCS";
|
|
1414
|
+
let content = await import_fs_extra8.default.readFile(openApiPath, "utf-8");
|
|
1415
|
+
if (content.includes(authMarker)) {
|
|
1416
|
+
return true;
|
|
1417
|
+
}
|
|
1418
|
+
const moduleDocsEndMarker = "// ZURO_DOCS_MODULES_END";
|
|
1419
|
+
if (!content.includes(moduleDocsEndMarker)) {
|
|
1420
|
+
return false;
|
|
1421
|
+
}
|
|
1422
|
+
const authBlock = `
|
|
1423
|
+
const authSignUpSchema = z.object({
|
|
1424
|
+
email: z.string().email().openapi({ example: "dev@company.com" }),
|
|
1425
|
+
password: z.string().min(8).openapi({ example: "strong-password" }),
|
|
1426
|
+
name: z.string().min(1).optional().openapi({ example: "Dev User" }),
|
|
1427
|
+
});
|
|
1428
|
+
|
|
1429
|
+
const authSignInSchema = z.object({
|
|
1430
|
+
email: z.string().email().openapi({ example: "dev@company.com" }),
|
|
1431
|
+
password: z.string().min(8).openapi({ example: "strong-password" }),
|
|
1432
|
+
});
|
|
1433
|
+
|
|
1434
|
+
const authUserSchema = z.object({
|
|
1435
|
+
id: z.string().openapi({ example: "user_123" }),
|
|
1436
|
+
email: z.string().email().openapi({ example: "dev@company.com" }),
|
|
1437
|
+
name: z.string().nullable().openapi({ example: "Dev User" }),
|
|
1438
|
+
});
|
|
1439
|
+
|
|
1440
|
+
${authMarker}
|
|
1441
|
+
registry.registerPath({
|
|
1442
|
+
method: "post",
|
|
1443
|
+
path: "/api/auth/sign-up/email",
|
|
1444
|
+
tags: ["Auth"],
|
|
1445
|
+
summary: "Register using email and password",
|
|
1446
|
+
request: {
|
|
1447
|
+
body: {
|
|
1448
|
+
content: {
|
|
1449
|
+
"application/json": {
|
|
1450
|
+
schema: authSignUpSchema,
|
|
1451
|
+
},
|
|
1452
|
+
},
|
|
1453
|
+
},
|
|
1454
|
+
},
|
|
1455
|
+
responses: {
|
|
1456
|
+
200: { description: "Registration successful" },
|
|
1457
|
+
},
|
|
1458
|
+
});
|
|
1459
|
+
|
|
1460
|
+
registry.registerPath({
|
|
1461
|
+
method: "post",
|
|
1462
|
+
path: "/api/auth/sign-in/email",
|
|
1463
|
+
tags: ["Auth"],
|
|
1464
|
+
summary: "Sign in using email and password",
|
|
1465
|
+
request: {
|
|
1466
|
+
body: {
|
|
1467
|
+
content: {
|
|
1468
|
+
"application/json": {
|
|
1469
|
+
schema: authSignInSchema,
|
|
1470
|
+
},
|
|
1471
|
+
},
|
|
1472
|
+
},
|
|
1473
|
+
},
|
|
1474
|
+
responses: {
|
|
1475
|
+
200: { description: "Sign in successful" },
|
|
1476
|
+
401: { description: "Invalid credentials" },
|
|
1477
|
+
},
|
|
1478
|
+
});
|
|
1479
|
+
|
|
1480
|
+
registry.registerPath({
|
|
1481
|
+
method: "post",
|
|
1482
|
+
path: "/api/auth/sign-out",
|
|
1483
|
+
tags: ["Auth"],
|
|
1484
|
+
summary: "Sign out current user",
|
|
1485
|
+
responses: {
|
|
1486
|
+
200: { description: "Sign out successful" },
|
|
1487
|
+
},
|
|
1488
|
+
});
|
|
1489
|
+
|
|
1490
|
+
registry.registerPath({
|
|
1491
|
+
method: "get",
|
|
1492
|
+
path: "/api/users/me",
|
|
1493
|
+
tags: ["Auth"],
|
|
1494
|
+
summary: "Get current authenticated user",
|
|
1495
|
+
security: [{ bearerAuth: [] }],
|
|
1496
|
+
responses: {
|
|
1497
|
+
200: {
|
|
1498
|
+
description: "Current user",
|
|
1499
|
+
content: {
|
|
1500
|
+
"application/json": {
|
|
1501
|
+
schema: z.object({ user: authUserSchema }),
|
|
1502
|
+
},
|
|
1503
|
+
},
|
|
1504
|
+
},
|
|
1505
|
+
401: { description: "Not authenticated" },
|
|
1506
|
+
},
|
|
1507
|
+
});
|
|
1508
|
+
`;
|
|
1509
|
+
content = content.replace(moduleDocsEndMarker, `${authBlock}
|
|
1510
|
+
${moduleDocsEndMarker}`);
|
|
1511
|
+
await import_fs_extra8.default.writeFile(openApiPath, content);
|
|
1512
|
+
return true;
|
|
1513
|
+
}
|
|
1514
|
+
async function promptAuthConfig(projectRoot, srcDir, options) {
|
|
1515
|
+
const { isDocsModuleInstalled: isDocsModuleInstalled2 } = await Promise.resolve().then(() => (init_docs_handler(), docs_handler_exports));
|
|
1516
|
+
const docsInstalled = await isDocsModuleInstalled2(projectRoot, srcDir);
|
|
1517
|
+
let shouldInstallDocsForAuth = false;
|
|
1518
|
+
if (!docsInstalled) {
|
|
1519
|
+
if (options.yes) {
|
|
1520
|
+
shouldInstallDocsForAuth = true;
|
|
1521
|
+
} else {
|
|
1522
|
+
const docsResponse = await (0, import_prompts3.default)({
|
|
1523
|
+
type: "confirm",
|
|
1524
|
+
name: "installDocs",
|
|
1525
|
+
message: "Install API docs module (Scalar + OpenAPI) too?",
|
|
1526
|
+
initial: true
|
|
1527
|
+
});
|
|
1528
|
+
if (docsResponse.installDocs === void 0) {
|
|
1529
|
+
console.log(import_chalk5.default.yellow("Operation cancelled."));
|
|
1530
|
+
return null;
|
|
1531
|
+
}
|
|
1532
|
+
shouldInstallDocsForAuth = docsResponse.installDocs;
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
const { detectInstalledDatabaseDialect: detectInstalledDatabaseDialect2 } = await Promise.resolve().then(() => (init_database_handler(), database_handler_exports));
|
|
1536
|
+
const authDatabaseDialect = await detectInstalledDatabaseDialect2(projectRoot, srcDir);
|
|
1537
|
+
return { shouldInstallDocsForAuth, authDatabaseDialect };
|
|
1538
|
+
}
|
|
1539
|
+
function printAuthHints(generatedAuthSecret) {
|
|
1540
|
+
if (generatedAuthSecret) {
|
|
1541
|
+
console.log(import_chalk5.default.yellow("\u2139 BETTER_AUTH_SECRET was generated automatically."));
|
|
1542
|
+
} else {
|
|
1543
|
+
console.log(import_chalk5.default.yellow("\u2139 Review BETTER_AUTH_SECRET and BETTER_AUTH_URL in .env."));
|
|
1544
|
+
}
|
|
1545
|
+
console.log(import_chalk5.default.yellow("\u2139 Run migrations: npx drizzle-kit generate && npx drizzle-kit migrate"));
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
// src/handlers/mailer.handler.ts
|
|
1549
|
+
var import_prompts4 = __toESM(require("prompts"));
|
|
1550
|
+
var import_chalk6 = __toESM(require("chalk"));
|
|
1551
|
+
async function promptMailerConfig() {
|
|
1552
|
+
const providerResponse = await (0, import_prompts4.default)({
|
|
1553
|
+
type: "select",
|
|
1554
|
+
name: "provider",
|
|
1555
|
+
message: "Which email provider?",
|
|
1556
|
+
choices: [
|
|
1557
|
+
{ title: "SMTP (Nodemailer)", description: "Gmail, Mailtrap, any SMTP server", value: "smtp" },
|
|
1558
|
+
{ title: "Resend", description: "API-based, easiest setup", value: "resend" }
|
|
1559
|
+
]
|
|
1560
|
+
});
|
|
1561
|
+
if (providerResponse.provider === void 0) {
|
|
1562
|
+
console.log(import_chalk6.default.yellow("Operation cancelled."));
|
|
1563
|
+
return null;
|
|
1564
|
+
}
|
|
1565
|
+
const mailerProvider = providerResponse.provider;
|
|
1566
|
+
let customSmtpVars;
|
|
1567
|
+
let usedDefaultSmtp = false;
|
|
1568
|
+
console.log(import_chalk6.default.dim(" Tip: Leave fields blank to use placeholder values and configure later\n"));
|
|
1569
|
+
if (mailerProvider === "smtp") {
|
|
1570
|
+
const smtpResponse = await (0, import_prompts4.default)([
|
|
1571
|
+
{
|
|
1572
|
+
type: "text",
|
|
1573
|
+
name: "host",
|
|
1574
|
+
message: "SMTP Host",
|
|
1575
|
+
initial: ""
|
|
1576
|
+
},
|
|
1577
|
+
{
|
|
1578
|
+
type: "text",
|
|
1579
|
+
name: "port",
|
|
1580
|
+
message: "SMTP Port",
|
|
1581
|
+
initial: "587"
|
|
1582
|
+
},
|
|
1583
|
+
{
|
|
1584
|
+
type: "text",
|
|
1585
|
+
name: "user",
|
|
1586
|
+
message: "SMTP User",
|
|
1587
|
+
initial: ""
|
|
1588
|
+
},
|
|
1589
|
+
{
|
|
1590
|
+
type: "password",
|
|
1591
|
+
name: "pass",
|
|
1592
|
+
message: "SMTP Password"
|
|
1593
|
+
},
|
|
1594
|
+
{
|
|
1595
|
+
type: "text",
|
|
1596
|
+
name: "from",
|
|
1597
|
+
message: "Mail From address",
|
|
1598
|
+
initial: ""
|
|
1599
|
+
}
|
|
1600
|
+
]);
|
|
1601
|
+
if (smtpResponse.host === void 0) {
|
|
1602
|
+
console.log(import_chalk6.default.yellow("Operation cancelled."));
|
|
1603
|
+
return null;
|
|
1604
|
+
}
|
|
1605
|
+
const host = smtpResponse.host?.trim() || "";
|
|
1606
|
+
const user = smtpResponse.user?.trim() || "";
|
|
1607
|
+
const pass = smtpResponse.pass?.trim() || "";
|
|
1608
|
+
const from = smtpResponse.from?.trim() || "";
|
|
1609
|
+
const port = smtpResponse.port?.trim() || "587";
|
|
1610
|
+
usedDefaultSmtp = !host && !user;
|
|
1611
|
+
if (!usedDefaultSmtp) {
|
|
1612
|
+
customSmtpVars = {
|
|
1613
|
+
SMTP_HOST: host || "smtp.example.com",
|
|
1614
|
+
SMTP_PORT: port,
|
|
1615
|
+
SMTP_USER: user || "your-email@example.com",
|
|
1616
|
+
SMTP_PASS: pass || "your-password",
|
|
1617
|
+
MAIL_FROM: from || "noreply@example.com"
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
} else {
|
|
1621
|
+
const resendResponse = await (0, import_prompts4.default)([
|
|
1622
|
+
{
|
|
1623
|
+
type: "text",
|
|
1624
|
+
name: "apiKey",
|
|
1625
|
+
message: "Resend API Key",
|
|
1626
|
+
initial: ""
|
|
1627
|
+
},
|
|
1628
|
+
{
|
|
1629
|
+
type: "text",
|
|
1630
|
+
name: "from",
|
|
1631
|
+
message: "Mail From address",
|
|
1632
|
+
initial: ""
|
|
1633
|
+
}
|
|
1634
|
+
]);
|
|
1635
|
+
if (resendResponse.apiKey === void 0) {
|
|
1636
|
+
console.log(import_chalk6.default.yellow("Operation cancelled."));
|
|
1637
|
+
return null;
|
|
1638
|
+
}
|
|
1639
|
+
const apiKey = resendResponse.apiKey?.trim() || "";
|
|
1640
|
+
const from = resendResponse.from?.trim() || "";
|
|
1641
|
+
usedDefaultSmtp = !apiKey;
|
|
1642
|
+
if (!usedDefaultSmtp) {
|
|
1643
|
+
customSmtpVars = {
|
|
1644
|
+
RESEND_API_KEY: apiKey || "re_your_api_key",
|
|
1645
|
+
MAIL_FROM: from || "onboarding@resend.dev"
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
return { mailerProvider, customSmtpVars, usedDefaultSmtp };
|
|
1650
|
+
}
|
|
1651
|
+
function printMailerHints(usedDefaultSmtp) {
|
|
1652
|
+
if (usedDefaultSmtp) {
|
|
1653
|
+
console.log(import_chalk6.default.yellow("\u2139 Placeholder SMTP values added to .env \u2014 update them before sending emails."));
|
|
1654
|
+
} else {
|
|
1655
|
+
console.log(import_chalk6.default.yellow("\u2139 Review SMTP configuration in .env to ensure values are correct."));
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
// src/commands/add.ts
|
|
1660
|
+
init_docs_handler();
|
|
1661
|
+
|
|
1662
|
+
// src/handlers/error-handler.handler.ts
|
|
1663
|
+
var import_path10 = __toESM(require("path"));
|
|
1664
|
+
var import_fs_extra9 = __toESM(require("fs-extra"));
|
|
1665
|
+
async function injectErrorHandler(projectRoot, srcDir) {
|
|
1666
|
+
const appPath = import_path10.default.join(projectRoot, srcDir, "app.ts");
|
|
1667
|
+
if (!import_fs_extra9.default.existsSync(appPath)) {
|
|
1668
|
+
return false;
|
|
1669
|
+
}
|
|
1670
|
+
let content = await import_fs_extra9.default.readFile(appPath, "utf-8");
|
|
1671
|
+
const errorImport = `import { errorHandler, notFoundHandler } from "./middleware/error-handler";`;
|
|
1672
|
+
const hasErrorImport = content.includes(errorImport);
|
|
1673
|
+
const hasNotFoundUse = /app\.use\(\s*notFoundHandler\s*\)/.test(content);
|
|
1674
|
+
const hasErrorUse = /app\.use\(\s*errorHandler\s*\)/.test(content);
|
|
1675
|
+
let modified = false;
|
|
1676
|
+
let importInserted = hasErrorImport;
|
|
1677
|
+
if (!hasErrorImport) {
|
|
1678
|
+
const importRegex = /^import .+ from .+;?\s*$/gm;
|
|
1679
|
+
let lastImportIndex = 0;
|
|
1680
|
+
let match;
|
|
1681
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
1682
|
+
lastImportIndex = match.index + match[0].length;
|
|
1683
|
+
}
|
|
1684
|
+
if (lastImportIndex > 0) {
|
|
1685
|
+
content = content.slice(0, lastImportIndex) + `
|
|
1686
|
+
${errorImport}` + content.slice(lastImportIndex);
|
|
1687
|
+
modified = true;
|
|
1688
|
+
importInserted = true;
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
let setupInserted = hasNotFoundUse && hasErrorUse;
|
|
1692
|
+
if (!setupInserted) {
|
|
1693
|
+
const setupLines = [];
|
|
1694
|
+
if (!hasNotFoundUse) {
|
|
1695
|
+
setupLines.push("app.use(notFoundHandler);");
|
|
1696
|
+
}
|
|
1697
|
+
if (!hasErrorUse) {
|
|
1698
|
+
setupLines.push("app.use(errorHandler);");
|
|
1699
|
+
}
|
|
1700
|
+
const errorSetup = `
|
|
1701
|
+
// Error handling (must be last)
|
|
1702
|
+
${setupLines.join("\n")}
|
|
1703
|
+
`;
|
|
1704
|
+
const exportMatch = content.match(/export default app;?\s*$/m);
|
|
1705
|
+
if (exportMatch && exportMatch.index !== void 0) {
|
|
1706
|
+
content = content.slice(0, exportMatch.index) + errorSetup + "\n" + content.slice(exportMatch.index);
|
|
1707
|
+
modified = true;
|
|
1708
|
+
setupInserted = true;
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
if (modified) {
|
|
1712
|
+
await import_fs_extra9.default.writeFile(appPath, content);
|
|
1713
|
+
}
|
|
1714
|
+
return importInserted && setupInserted;
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
// src/handlers/rate-limiter.handler.ts
|
|
1718
|
+
var import_path11 = __toESM(require("path"));
|
|
1719
|
+
var import_fs_extra10 = __toESM(require("fs-extra"));
|
|
1720
|
+
async function injectRateLimiter(projectRoot, srcDir) {
|
|
1721
|
+
const appPath = import_path11.default.join(projectRoot, srcDir, "app.ts");
|
|
1722
|
+
if (!import_fs_extra10.default.existsSync(appPath)) {
|
|
1723
|
+
return false;
|
|
1724
|
+
}
|
|
1725
|
+
let content = await import_fs_extra10.default.readFile(appPath, "utf-8");
|
|
1726
|
+
const rateLimiterImport = `import { rateLimiter } from "./middleware/rate-limiter";`;
|
|
1727
|
+
const hasImport = content.includes(rateLimiterImport);
|
|
1728
|
+
const hasUse = /app\.use\(\s*rateLimiter\s*\)/.test(content);
|
|
1729
|
+
if (hasImport && hasUse) {
|
|
1730
|
+
return true;
|
|
1731
|
+
}
|
|
1732
|
+
let modified = false;
|
|
1733
|
+
if (!hasImport) {
|
|
1734
|
+
const importRegex = /^import .+ from .+;?\s*$/gm;
|
|
1735
|
+
let lastImportIndex = 0;
|
|
1736
|
+
let match;
|
|
1737
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
1738
|
+
lastImportIndex = match.index + match[0].length;
|
|
1739
|
+
}
|
|
1740
|
+
if (lastImportIndex > 0) {
|
|
1741
|
+
content = content.slice(0, lastImportIndex) + `
|
|
1742
|
+
${rateLimiterImport}` + content.slice(lastImportIndex);
|
|
1743
|
+
modified = true;
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
if (!hasUse) {
|
|
1747
|
+
const jsonMiddleware = /^(\s*app\.use\(\s*express\.json\(\)\s*\);?\s*)$/m;
|
|
1748
|
+
const jsonMatch = content.match(jsonMiddleware);
|
|
1749
|
+
if (jsonMatch && jsonMatch.index !== void 0) {
|
|
1750
|
+
const insertAt = jsonMatch.index + jsonMatch[0].length;
|
|
1751
|
+
content = content.slice(0, insertAt) + `
|
|
1752
|
+
app.use(rateLimiter);` + content.slice(insertAt);
|
|
1753
|
+
modified = true;
|
|
1754
|
+
} else {
|
|
1755
|
+
const routeMount = /^\s*app\.use\(\s*["']\/api["']/m;
|
|
1756
|
+
const routeMatch = content.match(routeMount);
|
|
1757
|
+
if (routeMatch && routeMatch.index !== void 0) {
|
|
1758
|
+
content = content.slice(0, routeMatch.index) + `app.use(rateLimiter);
|
|
1759
|
+
` + content.slice(routeMatch.index);
|
|
1760
|
+
modified = true;
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
if (modified) {
|
|
1765
|
+
await import_fs_extra10.default.writeFile(appPath, content);
|
|
1766
|
+
}
|
|
1767
|
+
return true;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
// src/handlers/uploads.handler.ts
|
|
1771
|
+
var import_path12 = __toESM(require("path"));
|
|
1772
|
+
var import_fs_extra11 = __toESM(require("fs-extra"));
|
|
1773
|
+
var import_prompts5 = __toESM(require("prompts"));
|
|
1774
|
+
var import_chalk7 = __toESM(require("chalk"));
|
|
1775
|
+
init_code_inject();
|
|
1776
|
+
var UPLOAD_PRESETS = {
|
|
1777
|
+
image: {
|
|
1778
|
+
mimeTypes: ["image/jpeg", "image/png", "image/webp", "image/gif"],
|
|
1779
|
+
maxFileSize: 5 * 1024 * 1024,
|
|
1780
|
+
maxFiles: 1
|
|
1781
|
+
},
|
|
1782
|
+
document: {
|
|
1783
|
+
mimeTypes: [
|
|
1784
|
+
"application/pdf",
|
|
1785
|
+
"text/plain",
|
|
1786
|
+
"application/msword",
|
|
1787
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
1788
|
+
],
|
|
1789
|
+
maxFileSize: 10 * 1024 * 1024,
|
|
1790
|
+
maxFiles: 3
|
|
1791
|
+
},
|
|
1792
|
+
video: {
|
|
1793
|
+
mimeTypes: ["video/mp4", "video/quicktime", "video/webm"],
|
|
1794
|
+
maxFileSize: 100 * 1024 * 1024,
|
|
1795
|
+
maxFiles: 1
|
|
1796
|
+
},
|
|
1797
|
+
mixed: {
|
|
1798
|
+
mimeTypes: [
|
|
1799
|
+
"image/jpeg",
|
|
1800
|
+
"image/png",
|
|
1801
|
+
"image/webp",
|
|
1802
|
+
"application/pdf",
|
|
1803
|
+
"text/plain",
|
|
1804
|
+
"video/mp4"
|
|
1805
|
+
],
|
|
1806
|
+
maxFileSize: 25 * 1024 * 1024,
|
|
1807
|
+
maxFiles: 5
|
|
1808
|
+
}
|
|
1809
|
+
};
|
|
1810
|
+
function getUploadEnvSchemaFields(provider) {
|
|
1811
|
+
const shared = [
|
|
1812
|
+
{ name: "UPLOAD_PROVIDER", schema: `z.enum(["s3", "r2", "cloudinary"])` },
|
|
1813
|
+
{ name: "UPLOAD_MODE", schema: `z.enum(["proxy", "direct", "large"])` },
|
|
1814
|
+
{ name: "UPLOAD_AUTH_MODE", schema: `z.enum(["required", "optional", "none"])` },
|
|
1815
|
+
{ name: "UPLOAD_FILE_ACCESS", schema: `z.enum(["private", "public"])` },
|
|
1816
|
+
{ name: "UPLOAD_FILE_PRESET", schema: `z.enum(["image", "document", "video", "mixed"])` },
|
|
1817
|
+
{ name: "UPLOAD_KEY_PREFIX", schema: "z.string().min(1)" },
|
|
1818
|
+
{ name: "UPLOAD_ALLOWED_MIME", schema: "z.string().min(1)" },
|
|
1819
|
+
{ name: "UPLOAD_MAX_FILE_SIZE", schema: "z.coerce.number().positive()" },
|
|
1820
|
+
{ name: "UPLOAD_MAX_FILES", schema: "z.coerce.number().int().positive()" },
|
|
1821
|
+
{ name: "UPLOAD_DIRECT_URL_TTL_SECONDS", schema: "z.coerce.number().int().positive().default(900)" },
|
|
1822
|
+
{ name: "UPLOAD_ACCESS_URL_TTL_SECONDS", schema: "z.coerce.number().int().positive().default(300)" },
|
|
1823
|
+
{ name: "UPLOAD_MULTIPART_PART_SIZE", schema: "z.coerce.number().int().positive().default(5242880)" }
|
|
1824
|
+
];
|
|
1825
|
+
if (provider === "cloudinary") {
|
|
1826
|
+
return [
|
|
1827
|
+
...shared,
|
|
1828
|
+
{ name: "CLOUDINARY_CLOUD_NAME", schema: "z.string().min(1)" },
|
|
1829
|
+
{ name: "CLOUDINARY_API_KEY", schema: "z.string().min(1)" },
|
|
1830
|
+
{ name: "CLOUDINARY_API_SECRET", schema: "z.string().min(1)" },
|
|
1831
|
+
{ name: "CLOUDINARY_FOLDER", schema: 'z.string().min(1).default("uploads")' },
|
|
1832
|
+
{ name: "CLOUDINARY_UPLOAD_PRESET", schema: 'z.string().default("")' }
|
|
1833
|
+
];
|
|
1834
|
+
}
|
|
1835
|
+
return [
|
|
1836
|
+
...shared,
|
|
1837
|
+
{ name: "UPLOAD_BUCKET", schema: "z.string().min(1)" },
|
|
1838
|
+
{ name: "UPLOAD_REGION", schema: "z.string().min(1)" },
|
|
1839
|
+
{ name: "UPLOAD_ENDPOINT", schema: 'z.string().default("")' },
|
|
1840
|
+
{ name: "UPLOAD_ACCESS_KEY_ID", schema: "z.string().min(1)" },
|
|
1841
|
+
{ name: "UPLOAD_SECRET_ACCESS_KEY", schema: "z.string().min(1)" },
|
|
1842
|
+
{ name: "UPLOAD_PUBLIC_BASE_URL", schema: 'z.string().default("")' }
|
|
1843
|
+
];
|
|
1844
|
+
}
|
|
1845
|
+
async function isAuthInstalled(projectRoot, srcDir) {
|
|
1846
|
+
return import_fs_extra11.default.pathExists(import_path12.default.join(projectRoot, srcDir, "lib", "auth.ts"));
|
|
1847
|
+
}
|
|
1848
|
+
function hasDrizzleDatabase(config) {
|
|
1849
|
+
return config?.database?.orm === "drizzle";
|
|
1850
|
+
}
|
|
1851
|
+
async function promptCredentials(provider) {
|
|
1852
|
+
console.log(import_chalk7.default.dim(" Tip: Leave fields blank to use placeholders and configure later.\n"));
|
|
1853
|
+
if (provider === "cloudinary") {
|
|
1854
|
+
const response2 = await (0, import_prompts5.default)([
|
|
1855
|
+
{
|
|
1856
|
+
type: "text",
|
|
1857
|
+
name: "cloudName",
|
|
1858
|
+
message: "Cloudinary cloud name",
|
|
1859
|
+
initial: ""
|
|
1860
|
+
},
|
|
1861
|
+
{
|
|
1862
|
+
type: "text",
|
|
1863
|
+
name: "apiKey",
|
|
1864
|
+
message: "Cloudinary API key",
|
|
1865
|
+
initial: ""
|
|
1866
|
+
},
|
|
1867
|
+
{
|
|
1868
|
+
type: "password",
|
|
1869
|
+
name: "apiSecret",
|
|
1870
|
+
message: "Cloudinary API secret"
|
|
1871
|
+
},
|
|
1872
|
+
{
|
|
1873
|
+
type: "text",
|
|
1874
|
+
name: "folder",
|
|
1875
|
+
message: "Cloudinary folder",
|
|
1876
|
+
initial: "uploads"
|
|
1877
|
+
},
|
|
1878
|
+
{
|
|
1879
|
+
type: "text",
|
|
1880
|
+
name: "uploadPreset",
|
|
1881
|
+
message: "Cloudinary upload preset (optional)",
|
|
1882
|
+
initial: ""
|
|
1883
|
+
}
|
|
1884
|
+
]);
|
|
1885
|
+
if (response2.cloudName === void 0) {
|
|
1886
|
+
console.log(import_chalk7.default.yellow("Operation cancelled."));
|
|
1887
|
+
return null;
|
|
1888
|
+
}
|
|
1889
|
+
const values2 = {
|
|
1890
|
+
CLOUDINARY_CLOUD_NAME: response2.cloudName?.trim() || "your-cloud-name",
|
|
1891
|
+
CLOUDINARY_API_KEY: response2.apiKey?.trim() || "your-api-key",
|
|
1892
|
+
CLOUDINARY_API_SECRET: response2.apiSecret?.trim() || "your-api-secret",
|
|
1893
|
+
CLOUDINARY_FOLDER: response2.folder?.trim() || "uploads",
|
|
1894
|
+
CLOUDINARY_UPLOAD_PRESET: response2.uploadPreset?.trim() || ""
|
|
1895
|
+
};
|
|
1896
|
+
return values2;
|
|
1897
|
+
}
|
|
1898
|
+
const response = await (0, import_prompts5.default)([
|
|
1899
|
+
{
|
|
1900
|
+
type: "text",
|
|
1901
|
+
name: "bucket",
|
|
1902
|
+
message: `${provider.toUpperCase()} bucket name`,
|
|
1903
|
+
initial: ""
|
|
1904
|
+
},
|
|
1905
|
+
{
|
|
1906
|
+
type: "text",
|
|
1907
|
+
name: "region",
|
|
1908
|
+
message: `${provider.toUpperCase()} region`,
|
|
1909
|
+
initial: provider === "r2" ? "auto" : "us-east-1"
|
|
1910
|
+
},
|
|
1911
|
+
{
|
|
1912
|
+
type: "text",
|
|
1913
|
+
name: "endpoint",
|
|
1914
|
+
message: provider === "r2" ? "R2 S3 endpoint" : "Custom S3 endpoint (optional)",
|
|
1915
|
+
initial: provider === "r2" ? "https://<account-id>.r2.cloudflarestorage.com" : ""
|
|
1916
|
+
},
|
|
1917
|
+
{
|
|
1918
|
+
type: "text",
|
|
1919
|
+
name: "accessKeyId",
|
|
1920
|
+
message: "Access key ID",
|
|
1921
|
+
initial: ""
|
|
1922
|
+
},
|
|
1923
|
+
{
|
|
1924
|
+
type: "password",
|
|
1925
|
+
name: "secretAccessKey",
|
|
1926
|
+
message: "Secret access key"
|
|
1927
|
+
},
|
|
1928
|
+
{
|
|
1929
|
+
type: "text",
|
|
1930
|
+
name: "publicBaseUrl",
|
|
1931
|
+
message: "Public base URL (optional)",
|
|
1932
|
+
initial: ""
|
|
1933
|
+
}
|
|
1934
|
+
]);
|
|
1935
|
+
if (response.bucket === void 0) {
|
|
1936
|
+
console.log(import_chalk7.default.yellow("Operation cancelled."));
|
|
1937
|
+
return null;
|
|
1938
|
+
}
|
|
1939
|
+
const values = {
|
|
1940
|
+
UPLOAD_BUCKET: response.bucket?.trim() || `your-${provider}-bucket`,
|
|
1941
|
+
UPLOAD_REGION: response.region?.trim() || (provider === "r2" ? "auto" : "us-east-1"),
|
|
1942
|
+
UPLOAD_ENDPOINT: response.endpoint?.trim() || (provider === "r2" ? "https://<account-id>.r2.cloudflarestorage.com" : ""),
|
|
1943
|
+
UPLOAD_ACCESS_KEY_ID: response.accessKeyId?.trim() || "your-access-key-id",
|
|
1944
|
+
UPLOAD_SECRET_ACCESS_KEY: response.secretAccessKey?.trim() || "your-secret-access-key",
|
|
1945
|
+
UPLOAD_PUBLIC_BASE_URL: response.publicBaseUrl?.trim() || ""
|
|
1946
|
+
};
|
|
1947
|
+
return values;
|
|
1948
|
+
}
|
|
1949
|
+
function buildSharedEnvVars(provider, mode, authMode, access, preset, maxFileSize, maxFiles) {
|
|
1950
|
+
return {
|
|
1951
|
+
UPLOAD_PROVIDER: provider,
|
|
1952
|
+
UPLOAD_MODE: mode,
|
|
1953
|
+
UPLOAD_AUTH_MODE: authMode,
|
|
1954
|
+
UPLOAD_FILE_ACCESS: access,
|
|
1955
|
+
UPLOAD_FILE_PRESET: preset,
|
|
1956
|
+
UPLOAD_KEY_PREFIX: "uploads",
|
|
1957
|
+
UPLOAD_ALLOWED_MIME: UPLOAD_PRESETS[preset].mimeTypes.join(","),
|
|
1958
|
+
UPLOAD_MAX_FILE_SIZE: String(maxFileSize),
|
|
1959
|
+
UPLOAD_MAX_FILES: String(maxFiles),
|
|
1960
|
+
UPLOAD_DIRECT_URL_TTL_SECONDS: "900",
|
|
1961
|
+
UPLOAD_ACCESS_URL_TTL_SECONDS: "300",
|
|
1962
|
+
UPLOAD_MULTIPART_PART_SIZE: "5242880"
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
async function promptUploadsConfig(projectRoot, srcDir) {
|
|
1966
|
+
const projectConfig = await readZuroConfig(projectRoot);
|
|
1967
|
+
const authInstalled = await isAuthInstalled(projectRoot, srcDir);
|
|
1968
|
+
const drizzleInstalled = hasDrizzleDatabase(projectConfig);
|
|
1969
|
+
const initial = await (0, import_prompts5.default)([
|
|
1970
|
+
{
|
|
1971
|
+
type: "select",
|
|
1972
|
+
name: "provider",
|
|
1973
|
+
message: "Which upload provider?",
|
|
1974
|
+
choices: [
|
|
1975
|
+
{ title: "S3", value: "s3" },
|
|
1976
|
+
{ title: "R2", value: "r2" },
|
|
1977
|
+
{ title: "Cloudinary", value: "cloudinary" }
|
|
1978
|
+
],
|
|
1979
|
+
initial: 0
|
|
1980
|
+
},
|
|
1981
|
+
{
|
|
1982
|
+
type: "select",
|
|
1983
|
+
name: "mode",
|
|
1984
|
+
message: "Which upload mode?",
|
|
1985
|
+
choices: [
|
|
1986
|
+
{ title: "Proxy", description: "API server receives the file and uploads it", value: "proxy" },
|
|
1987
|
+
{ title: "Direct", description: "Client uploads directly with signed params/URLs", value: "direct" },
|
|
1988
|
+
{ title: "Large", description: "Multipart upload flow for large files", value: "large" }
|
|
1989
|
+
],
|
|
1990
|
+
initial: 1
|
|
1991
|
+
},
|
|
1992
|
+
{
|
|
1993
|
+
type: "select",
|
|
1994
|
+
name: "authMode",
|
|
1995
|
+
message: "Who can upload?",
|
|
1996
|
+
choices: [
|
|
1997
|
+
{ title: "Authenticated only", value: "required" },
|
|
1998
|
+
{ title: "Optional auth", value: "optional" },
|
|
1999
|
+
{ title: "Public", value: "none" }
|
|
2000
|
+
],
|
|
2001
|
+
initial: authInstalled ? 0 : 2
|
|
2002
|
+
},
|
|
2003
|
+
{
|
|
2004
|
+
type: "select",
|
|
2005
|
+
name: "access",
|
|
2006
|
+
message: "How should uploaded files be accessed?",
|
|
2007
|
+
choices: [
|
|
2008
|
+
{ title: "Private", value: "private" },
|
|
2009
|
+
{ title: "Public", value: "public" }
|
|
2010
|
+
],
|
|
2011
|
+
initial: 0
|
|
2012
|
+
},
|
|
2013
|
+
{
|
|
2014
|
+
type: "select",
|
|
2015
|
+
name: "preset",
|
|
2016
|
+
message: "Which file preset?",
|
|
2017
|
+
choices: [
|
|
2018
|
+
{ title: "Image", value: "image" },
|
|
2019
|
+
{ title: "Document", value: "document" },
|
|
2020
|
+
{ title: "Video", value: "video" },
|
|
2021
|
+
{ title: "Mixed", value: "mixed" }
|
|
2022
|
+
],
|
|
2023
|
+
initial: 0
|
|
2024
|
+
},
|
|
2025
|
+
{
|
|
2026
|
+
type: "confirm",
|
|
2027
|
+
name: "useDefaults",
|
|
2028
|
+
message: "Use recommended upload limits for this preset?",
|
|
2029
|
+
initial: true
|
|
2030
|
+
}
|
|
2031
|
+
]);
|
|
2032
|
+
if (initial.provider === void 0) {
|
|
2033
|
+
console.log(import_chalk7.default.yellow("Operation cancelled."));
|
|
2034
|
+
return null;
|
|
2035
|
+
}
|
|
2036
|
+
const provider = initial.provider;
|
|
2037
|
+
const mode = initial.mode;
|
|
2038
|
+
const authMode = initial.authMode;
|
|
2039
|
+
const access = initial.access;
|
|
2040
|
+
const preset = initial.preset;
|
|
2041
|
+
if (provider === "cloudinary" && mode === "large") {
|
|
2042
|
+
console.log(import_chalk7.default.yellow("\nCloudinary large multipart scaffolding is not available in this module yet."));
|
|
2043
|
+
console.log(import_chalk7.default.yellow("Use S3 or R2 for large uploads, or pick Proxy/Direct for Cloudinary.\n"));
|
|
2044
|
+
return null;
|
|
2045
|
+
}
|
|
2046
|
+
if (provider === "cloudinary" && (preset === "document" || preset === "mixed")) {
|
|
2047
|
+
const warning = await (0, import_prompts5.default)({
|
|
2048
|
+
type: "confirm",
|
|
2049
|
+
name: "continue",
|
|
2050
|
+
message: "Cloudinary is media-first. Continue with Cloudinary for non-media/general files?",
|
|
2051
|
+
initial: false
|
|
2052
|
+
});
|
|
2053
|
+
if (!warning.continue) {
|
|
2054
|
+
console.log(import_chalk7.default.yellow("Operation cancelled."));
|
|
2055
|
+
return null;
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
const presetDefaults = UPLOAD_PRESETS[preset];
|
|
2059
|
+
let maxFileSize = presetDefaults.maxFileSize;
|
|
2060
|
+
let maxFiles = presetDefaults.maxFiles;
|
|
2061
|
+
if (!initial.useDefaults) {
|
|
2062
|
+
const custom = await (0, import_prompts5.default)([
|
|
2063
|
+
{
|
|
2064
|
+
type: "number",
|
|
2065
|
+
name: "maxFileSizeMb",
|
|
2066
|
+
message: "Max file size (MB)",
|
|
2067
|
+
initial: Math.max(1, Math.round(presetDefaults.maxFileSize / (1024 * 1024))),
|
|
2068
|
+
min: 1
|
|
2069
|
+
},
|
|
2070
|
+
{
|
|
2071
|
+
type: "number",
|
|
2072
|
+
name: "maxFiles",
|
|
2073
|
+
message: "Max files per request",
|
|
2074
|
+
initial: presetDefaults.maxFiles,
|
|
2075
|
+
min: 1,
|
|
2076
|
+
max: 20
|
|
2077
|
+
}
|
|
2078
|
+
]);
|
|
2079
|
+
if (custom.maxFileSizeMb === void 0) {
|
|
2080
|
+
console.log(import_chalk7.default.yellow("Operation cancelled."));
|
|
2081
|
+
return null;
|
|
2082
|
+
}
|
|
2083
|
+
maxFileSize = Number(custom.maxFileSizeMb) * 1024 * 1024;
|
|
2084
|
+
maxFiles = Number(custom.maxFiles);
|
|
2085
|
+
}
|
|
2086
|
+
let useDatabaseMetadata = false;
|
|
2087
|
+
let shouldInstallDatabase = false;
|
|
2088
|
+
const metadataPrompt = await (0, import_prompts5.default)({
|
|
2089
|
+
type: "select",
|
|
2090
|
+
name: "metadata",
|
|
2091
|
+
message: "Upload metadata storage?",
|
|
2092
|
+
choices: [
|
|
2093
|
+
{ title: drizzleInstalled ? "Database" : "Install database + track uploads", value: "db" },
|
|
2094
|
+
{ title: "No metadata", value: "none" }
|
|
2095
|
+
],
|
|
2096
|
+
initial: 0
|
|
2097
|
+
});
|
|
2098
|
+
if (metadataPrompt.metadata === void 0) {
|
|
2099
|
+
console.log(import_chalk7.default.yellow("Operation cancelled."));
|
|
2100
|
+
return null;
|
|
2101
|
+
}
|
|
2102
|
+
if (metadataPrompt.metadata === "db") {
|
|
2103
|
+
useDatabaseMetadata = true;
|
|
2104
|
+
if (!projectConfig?.database) {
|
|
2105
|
+
shouldInstallDatabase = true;
|
|
2106
|
+
} else if (!drizzleInstalled) {
|
|
2107
|
+
console.log(import_chalk7.default.yellow("\nUploads metadata currently supports Drizzle-based database setups only."));
|
|
2108
|
+
console.log(import_chalk7.default.yellow("Install or switch to a Drizzle database, or continue without metadata.\n"));
|
|
2109
|
+
return null;
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
if (access === "private" && !useDatabaseMetadata) {
|
|
2113
|
+
const warning = await (0, import_prompts5.default)({
|
|
2114
|
+
type: "confirm",
|
|
2115
|
+
name: "continue",
|
|
2116
|
+
message: "Private uploads work best with metadata. Continue without database tracking?",
|
|
2117
|
+
initial: false
|
|
2118
|
+
});
|
|
2119
|
+
if (!warning.continue) {
|
|
2120
|
+
console.log(import_chalk7.default.yellow("Operation cancelled."));
|
|
2121
|
+
return null;
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
let shouldInstallAuth = false;
|
|
2125
|
+
if (authMode !== "none" && !authInstalled) {
|
|
2126
|
+
const authPrompt = await (0, import_prompts5.default)({
|
|
2127
|
+
type: "confirm",
|
|
2128
|
+
name: "installAuth",
|
|
2129
|
+
message: "Uploads auth requires the auth module. Install auth now?",
|
|
2130
|
+
initial: true
|
|
2131
|
+
});
|
|
2132
|
+
if (!authPrompt.installAuth) {
|
|
2133
|
+
console.log(import_chalk7.default.yellow("Operation cancelled."));
|
|
2134
|
+
return null;
|
|
2135
|
+
}
|
|
2136
|
+
shouldInstallAuth = true;
|
|
2137
|
+
}
|
|
2138
|
+
const providerEnv = await promptCredentials(provider);
|
|
2139
|
+
if (!providerEnv) {
|
|
2140
|
+
return null;
|
|
2141
|
+
}
|
|
2142
|
+
return {
|
|
2143
|
+
provider,
|
|
2144
|
+
mode,
|
|
2145
|
+
authMode,
|
|
2146
|
+
access,
|
|
2147
|
+
preset,
|
|
2148
|
+
useDatabaseMetadata,
|
|
2149
|
+
shouldInstallAuth,
|
|
2150
|
+
shouldInstallDatabase,
|
|
2151
|
+
envVars: {
|
|
2152
|
+
...buildSharedEnvVars(provider, mode, authMode, access, preset, maxFileSize, maxFiles),
|
|
2153
|
+
...providerEnv
|
|
2154
|
+
}
|
|
2155
|
+
};
|
|
2156
|
+
}
|
|
2157
|
+
async function injectUploadsRoutes(projectRoot, srcDir) {
|
|
2158
|
+
const routeIndexPath = import_path12.default.join(projectRoot, srcDir, "routes", "index.ts");
|
|
2159
|
+
const routeImport = `import uploadsRoutes from "./uploads.routes";`;
|
|
2160
|
+
const routeMountPattern = /rootRouter\.use\(\s*["']\/uploads["']\s*,\s*uploadsRoutes\s*\)/;
|
|
2161
|
+
if (await import_fs_extra11.default.pathExists(routeIndexPath)) {
|
|
2162
|
+
let routeContent = await import_fs_extra11.default.readFile(routeIndexPath, "utf-8");
|
|
2163
|
+
let routeModified = false;
|
|
1061
2164
|
const importResult = appendImport(routeContent, routeImport);
|
|
1062
2165
|
if (!importResult.inserted) {
|
|
1063
2166
|
return false;
|
|
@@ -1068,8 +2171,8 @@ async function injectDocsRoutes(projectRoot, srcDir) {
|
|
|
1068
2171
|
}
|
|
1069
2172
|
if (!routeMountPattern.test(routeContent)) {
|
|
1070
2173
|
const routeSetup = `
|
|
1071
|
-
//
|
|
1072
|
-
rootRouter.use("/
|
|
2174
|
+
// Upload routes
|
|
2175
|
+
rootRouter.use("/uploads", uploadsRoutes);
|
|
1073
2176
|
`;
|
|
1074
2177
|
const exportMatch = routeContent.match(/export default rootRouter;?\s*$/m);
|
|
1075
2178
|
if (!exportMatch || exportMatch.index === void 0) {
|
|
@@ -1079,17 +2182,17 @@ rootRouter.use("/docs", docsRoutes);
|
|
|
1079
2182
|
routeModified = true;
|
|
1080
2183
|
}
|
|
1081
2184
|
if (routeModified) {
|
|
1082
|
-
await
|
|
2185
|
+
await import_fs_extra11.default.writeFile(routeIndexPath, routeContent);
|
|
1083
2186
|
}
|
|
1084
2187
|
return true;
|
|
1085
2188
|
}
|
|
1086
|
-
const appPath =
|
|
1087
|
-
if (!await
|
|
2189
|
+
const appPath = import_path12.default.join(projectRoot, srcDir, "app.ts");
|
|
2190
|
+
if (!await import_fs_extra11.default.pathExists(appPath)) {
|
|
1088
2191
|
return false;
|
|
1089
2192
|
}
|
|
1090
|
-
let appContent = await
|
|
2193
|
+
let appContent = await import_fs_extra11.default.readFile(appPath, "utf-8");
|
|
1091
2194
|
let appModified = false;
|
|
1092
|
-
const appImportResult = appendImport(appContent, `import
|
|
2195
|
+
const appImportResult = appendImport(appContent, `import uploadsRoutes from "./routes/uploads.routes";`);
|
|
1093
2196
|
if (!appImportResult.inserted) {
|
|
1094
2197
|
return false;
|
|
1095
2198
|
}
|
|
@@ -1097,11 +2200,11 @@ rootRouter.use("/docs", docsRoutes);
|
|
|
1097
2200
|
appContent = appImportResult.source;
|
|
1098
2201
|
appModified = true;
|
|
1099
2202
|
}
|
|
1100
|
-
const hasMount = /app\.use\(\s*["']\/api\/
|
|
2203
|
+
const hasMount = /app\.use\(\s*["']\/api\/uploads["']\s*,\s*uploadsRoutes\s*\)/.test(appContent);
|
|
1101
2204
|
if (!hasMount) {
|
|
1102
2205
|
const setup = `
|
|
1103
|
-
//
|
|
1104
|
-
app.use("/api/
|
|
2206
|
+
// Upload routes
|
|
2207
|
+
app.use("/api/uploads", uploadsRoutes);
|
|
1105
2208
|
`;
|
|
1106
2209
|
const exportMatch = appContent.match(/export default app;?\s*$/m);
|
|
1107
2210
|
if (!exportMatch || exportMatch.index === void 0) {
|
|
@@ -1111,116 +2214,224 @@ app.use("/api/docs", docsRoutes);
|
|
|
1111
2214
|
appModified = true;
|
|
1112
2215
|
}
|
|
1113
2216
|
if (appModified) {
|
|
1114
|
-
await
|
|
2217
|
+
await import_fs_extra11.default.writeFile(appPath, appContent);
|
|
1115
2218
|
}
|
|
1116
2219
|
return true;
|
|
1117
2220
|
}
|
|
1118
|
-
async function
|
|
1119
|
-
|
|
1120
|
-
|
|
2221
|
+
async function isUploadsModuleInstalled(projectRoot, srcDir) {
|
|
2222
|
+
return import_fs_extra11.default.pathExists(import_path12.default.join(projectRoot, srcDir, "routes", "uploads.routes.ts"));
|
|
2223
|
+
}
|
|
2224
|
+
async function detectInstalledUploadsMode(projectRoot) {
|
|
2225
|
+
const envPath = import_path12.default.join(projectRoot, ".env");
|
|
2226
|
+
if (!await import_fs_extra11.default.pathExists(envPath)) {
|
|
2227
|
+
return null;
|
|
2228
|
+
}
|
|
2229
|
+
const content = await import_fs_extra11.default.readFile(envPath, "utf-8");
|
|
2230
|
+
const match = content.match(/^UPLOAD_MODE=(proxy|direct|large)$/m);
|
|
2231
|
+
if (!match) {
|
|
2232
|
+
return null;
|
|
2233
|
+
}
|
|
2234
|
+
return match[1];
|
|
2235
|
+
}
|
|
2236
|
+
async function injectUploadsDocs(projectRoot, srcDir, mode) {
|
|
2237
|
+
const openApiPath = import_path12.default.join(projectRoot, srcDir, "lib", "openapi.ts");
|
|
2238
|
+
if (!await import_fs_extra11.default.pathExists(openApiPath)) {
|
|
1121
2239
|
return false;
|
|
1122
2240
|
}
|
|
1123
|
-
const
|
|
1124
|
-
let content = await
|
|
1125
|
-
if (content.includes(
|
|
2241
|
+
const marker = "// ZURO_UPLOADS_DOCS";
|
|
2242
|
+
let content = await import_fs_extra11.default.readFile(openApiPath, "utf-8");
|
|
2243
|
+
if (content.includes(marker)) {
|
|
1126
2244
|
return true;
|
|
1127
2245
|
}
|
|
1128
2246
|
const moduleDocsEndMarker = "// ZURO_DOCS_MODULES_END";
|
|
1129
2247
|
if (!content.includes(moduleDocsEndMarker)) {
|
|
1130
2248
|
return false;
|
|
1131
2249
|
}
|
|
1132
|
-
const
|
|
1133
|
-
const
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
});
|
|
1138
|
-
|
|
1139
|
-
const authSignInSchema = z.object({
|
|
1140
|
-
email: z.string().email().openapi({ example: "dev@company.com" }),
|
|
1141
|
-
password: z.string().min(8).openapi({ example: "strong-password" }),
|
|
2250
|
+
const commonBlock = `
|
|
2251
|
+
const uploadAccessSchema = z.object({
|
|
2252
|
+
key: z.string().openapi({ example: "uploads/users/user_123/2026/03/06/example.png" }),
|
|
2253
|
+
resourceType: z.string().optional().openapi({ example: "image" }),
|
|
2254
|
+
providerAssetId: z.string().optional().openapi({ example: "uploads/users/user_123/example" }),
|
|
1142
2255
|
});
|
|
1143
2256
|
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
email: z.string().email().openapi({ example: "dev@company.com" }),
|
|
1147
|
-
name: z.string().nullable().openapi({ example: "Dev User" }),
|
|
1148
|
-
});
|
|
1149
|
-
|
|
1150
|
-
${authMarker}
|
|
1151
|
-
registry.registerPath({
|
|
2257
|
+
`;
|
|
2258
|
+
const directBlock = mode === "direct" ? `registry.registerPath({
|
|
1152
2259
|
method: "post",
|
|
1153
|
-
path: "/api/
|
|
1154
|
-
tags: ["
|
|
1155
|
-
summary: "
|
|
2260
|
+
path: "/api/uploads/presign",
|
|
2261
|
+
tags: ["Uploads"],
|
|
2262
|
+
summary: "Create a signed direct upload request",
|
|
1156
2263
|
request: {
|
|
1157
2264
|
body: {
|
|
1158
2265
|
content: {
|
|
1159
2266
|
"application/json": {
|
|
1160
|
-
schema:
|
|
2267
|
+
schema: z.object({
|
|
2268
|
+
originalName: z.string().min(1),
|
|
2269
|
+
mimeType: z.string().min(1),
|
|
2270
|
+
bytes: z.number().positive(),
|
|
2271
|
+
}),
|
|
1161
2272
|
},
|
|
1162
2273
|
},
|
|
1163
2274
|
},
|
|
1164
2275
|
},
|
|
1165
2276
|
responses: {
|
|
1166
|
-
200: { description: "
|
|
2277
|
+
200: { description: "Signed upload created" },
|
|
1167
2278
|
},
|
|
1168
2279
|
});
|
|
1169
2280
|
|
|
1170
2281
|
registry.registerPath({
|
|
1171
2282
|
method: "post",
|
|
1172
|
-
path: "/api/
|
|
1173
|
-
tags: ["
|
|
1174
|
-
summary: "
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
content: {
|
|
1178
|
-
"application/json": {
|
|
1179
|
-
schema: authSignInSchema,
|
|
1180
|
-
},
|
|
1181
|
-
},
|
|
1182
|
-
},
|
|
2283
|
+
path: "/api/uploads/complete",
|
|
2284
|
+
tags: ["Uploads"],
|
|
2285
|
+
summary: "Finalize a direct upload and persist metadata",
|
|
2286
|
+
responses: {
|
|
2287
|
+
201: { description: "Upload finalized" },
|
|
1183
2288
|
},
|
|
2289
|
+
});
|
|
2290
|
+
` : "";
|
|
2291
|
+
const proxyBlock = mode === "proxy" ? `registry.registerPath({
|
|
2292
|
+
method: "post",
|
|
2293
|
+
path: "/api/uploads",
|
|
2294
|
+
tags: ["Uploads"],
|
|
2295
|
+
summary: "Upload a file through the API server",
|
|
1184
2296
|
responses: {
|
|
1185
|
-
|
|
1186
|
-
|
|
2297
|
+
201: { description: "File uploaded" },
|
|
2298
|
+
},
|
|
2299
|
+
});
|
|
2300
|
+
` : "";
|
|
2301
|
+
const largeBlock = mode === "large" ? `registry.registerPath({
|
|
2302
|
+
method: "post",
|
|
2303
|
+
path: "/api/uploads/multipart/init",
|
|
2304
|
+
tags: ["Uploads"],
|
|
2305
|
+
summary: "Start a multipart upload session",
|
|
2306
|
+
responses: {
|
|
2307
|
+
200: { description: "Multipart upload initialized" },
|
|
1187
2308
|
},
|
|
1188
2309
|
});
|
|
1189
2310
|
|
|
1190
2311
|
registry.registerPath({
|
|
1191
2312
|
method: "post",
|
|
1192
|
-
path: "/api/
|
|
1193
|
-
tags: ["
|
|
1194
|
-
summary: "
|
|
2313
|
+
path: "/api/uploads/multipart/complete",
|
|
2314
|
+
tags: ["Uploads"],
|
|
2315
|
+
summary: "Complete a multipart upload",
|
|
1195
2316
|
responses: {
|
|
1196
|
-
|
|
2317
|
+
201: { description: "Multipart upload completed" },
|
|
1197
2318
|
},
|
|
1198
2319
|
});
|
|
1199
2320
|
|
|
1200
2321
|
registry.registerPath({
|
|
1201
|
-
method: "
|
|
1202
|
-
path: "/api/
|
|
1203
|
-
tags: ["
|
|
1204
|
-
summary: "
|
|
1205
|
-
security: [{ bearerAuth: [] }],
|
|
2322
|
+
method: "post",
|
|
2323
|
+
path: "/api/uploads/multipart/abort",
|
|
2324
|
+
tags: ["Uploads"],
|
|
2325
|
+
summary: "Abort a multipart upload",
|
|
1206
2326
|
responses: {
|
|
1207
|
-
|
|
1208
|
-
|
|
2327
|
+
204: { description: "Multipart upload aborted" },
|
|
2328
|
+
},
|
|
2329
|
+
});
|
|
2330
|
+
` : "";
|
|
2331
|
+
const sharedOps = `registry.registerPath({
|
|
2332
|
+
method: "post",
|
|
2333
|
+
path: "/api/uploads/access-url",
|
|
2334
|
+
tags: ["Uploads"],
|
|
2335
|
+
summary: "Create an access URL for an uploaded file",
|
|
2336
|
+
request: {
|
|
2337
|
+
body: {
|
|
1209
2338
|
content: {
|
|
1210
2339
|
"application/json": {
|
|
1211
|
-
schema:
|
|
2340
|
+
schema: uploadAccessSchema,
|
|
1212
2341
|
},
|
|
1213
2342
|
},
|
|
1214
2343
|
},
|
|
1215
|
-
|
|
2344
|
+
},
|
|
2345
|
+
responses: {
|
|
2346
|
+
200: { description: "Access URL generated" },
|
|
2347
|
+
},
|
|
2348
|
+
});
|
|
2349
|
+
|
|
2350
|
+
registry.registerPath({
|
|
2351
|
+
method: "delete",
|
|
2352
|
+
path: "/api/uploads",
|
|
2353
|
+
tags: ["Uploads"],
|
|
2354
|
+
summary: "Delete an uploaded file",
|
|
2355
|
+
request: {
|
|
2356
|
+
body: {
|
|
2357
|
+
content: {
|
|
2358
|
+
"application/json": {
|
|
2359
|
+
schema: uploadAccessSchema,
|
|
2360
|
+
},
|
|
2361
|
+
},
|
|
2362
|
+
},
|
|
2363
|
+
},
|
|
2364
|
+
responses: {
|
|
2365
|
+
204: { description: "Upload deleted" },
|
|
1216
2366
|
},
|
|
1217
2367
|
});
|
|
1218
2368
|
`;
|
|
1219
|
-
|
|
2369
|
+
const block = `
|
|
2370
|
+
${commonBlock}${marker}
|
|
2371
|
+
${proxyBlock}${directBlock}${largeBlock}${sharedOps}`;
|
|
2372
|
+
content = content.replace(moduleDocsEndMarker, `${block}
|
|
1220
2373
|
${moduleDocsEndMarker}`);
|
|
1221
|
-
|
|
2374
|
+
const tagInsert = /(\{\s*name:\s*"Auth".+\},\s*\n\s*\],)/;
|
|
2375
|
+
if (tagInsert.test(content) && !content.includes(`{ name: "Uploads", description: "File uploads and asset access" }`)) {
|
|
2376
|
+
content = content.replace(
|
|
2377
|
+
tagInsert,
|
|
2378
|
+
`{ name: "Auth", description: "Authentication and session endpoints" },
|
|
2379
|
+
{ name: "Uploads", description: "File uploads and asset access" },
|
|
2380
|
+
],`
|
|
2381
|
+
);
|
|
2382
|
+
}
|
|
2383
|
+
await import_fs_extra11.default.writeFile(openApiPath, content);
|
|
1222
2384
|
return true;
|
|
1223
2385
|
}
|
|
2386
|
+
function printUploadHints(result) {
|
|
2387
|
+
console.log(import_chalk7.default.yellow("\u2139 Upload routes are mounted at: /api/uploads"));
|
|
2388
|
+
console.log(import_chalk7.default.yellow(`\u2139 Provider: ${result.provider} \xB7 Mode: ${result.mode} \xB7 Access: ${result.access}`));
|
|
2389
|
+
if (result.mode === "proxy") {
|
|
2390
|
+
console.log(import_chalk7.default.yellow("\u2139 Reuse uploadSingle()/uploadArray() from src/lib/uploads/proxy.ts in your own form + file routes."));
|
|
2391
|
+
}
|
|
2392
|
+
if (result.provider === "r2") {
|
|
2393
|
+
console.log(import_chalk7.default.yellow("\u2139 R2 presigned URLs use the R2 S3 API endpoint, not your custom domain."));
|
|
2394
|
+
}
|
|
2395
|
+
if (result.useDatabaseMetadata) {
|
|
2396
|
+
console.log(import_chalk7.default.yellow("\u2139 Upload metadata is stored in db/schema/uploads.ts."));
|
|
2397
|
+
} else {
|
|
2398
|
+
console.log(import_chalk7.default.yellow("\u2139 No upload metadata table was added. Private file ownership checks fall back to key prefixes."));
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
// src/commands/add.ts
|
|
2403
|
+
function resolvePackageManager(projectRoot) {
|
|
2404
|
+
if (import_fs_extra12.default.existsSync(import_path13.default.join(projectRoot, "pnpm-lock.yaml"))) {
|
|
2405
|
+
return "pnpm";
|
|
2406
|
+
}
|
|
2407
|
+
if (import_fs_extra12.default.existsSync(import_path13.default.join(projectRoot, "bun.lockb")) || import_fs_extra12.default.existsSync(import_path13.default.join(projectRoot, "bun.lock"))) {
|
|
2408
|
+
return "bun";
|
|
2409
|
+
}
|
|
2410
|
+
if (import_fs_extra12.default.existsSync(import_path13.default.join(projectRoot, "yarn.lock"))) {
|
|
2411
|
+
return "yarn";
|
|
2412
|
+
}
|
|
2413
|
+
return "npm";
|
|
2414
|
+
}
|
|
2415
|
+
function getModuleDocsPath(moduleName) {
|
|
2416
|
+
if (isDatabaseModule(moduleName)) {
|
|
2417
|
+
return "database";
|
|
2418
|
+
}
|
|
2419
|
+
return moduleName;
|
|
2420
|
+
}
|
|
2421
|
+
async function hasEnvVariable(projectRoot, key) {
|
|
2422
|
+
const envPath = import_path13.default.join(projectRoot, ".env");
|
|
2423
|
+
if (!await import_fs_extra12.default.pathExists(envPath)) {
|
|
2424
|
+
return false;
|
|
2425
|
+
}
|
|
2426
|
+
const content = await import_fs_extra12.default.readFile(envPath, "utf-8");
|
|
2427
|
+
const pattern = new RegExp(`^${escapeRegex(key)}=`, "m");
|
|
2428
|
+
return pattern.test(content);
|
|
2429
|
+
}
|
|
2430
|
+
async function isLikelyEmptyDirectory(cwd) {
|
|
2431
|
+
const entries = await import_fs_extra12.default.readdir(cwd);
|
|
2432
|
+
const ignored = /* @__PURE__ */ new Set([".ds_store", "thumbs.db"]);
|
|
2433
|
+
return entries.filter((entry) => !ignored.has(entry.toLowerCase())).length === 0;
|
|
2434
|
+
}
|
|
1224
2435
|
var add = async (moduleName, options = {}) => {
|
|
1225
2436
|
const projectRoot = process.cwd();
|
|
1226
2437
|
const projectConfig = await readZuroConfig(projectRoot);
|
|
@@ -1241,188 +2452,42 @@ var add = async (moduleName, options = {}) => {
|
|
|
1241
2452
|
let customDbUrl;
|
|
1242
2453
|
let usedDefaultDbUrl = false;
|
|
1243
2454
|
let databaseBackupPath = null;
|
|
2455
|
+
let selectedDatabaseOrm = null;
|
|
2456
|
+
let selectedDatabaseDialect = null;
|
|
1244
2457
|
let generatedAuthSecret = false;
|
|
1245
2458
|
let authDatabaseDialect = null;
|
|
1246
2459
|
let customSmtpVars;
|
|
1247
2460
|
let usedDefaultSmtp = false;
|
|
1248
2461
|
let mailerProvider = "smtp";
|
|
1249
2462
|
let shouldInstallDocsForAuth = false;
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
console.log(import_chalk4.default.yellow("Operation cancelled."));
|
|
1262
|
-
return;
|
|
1263
|
-
}
|
|
1264
|
-
resolvedModuleName = variantResponse.variant;
|
|
1265
|
-
}
|
|
1266
|
-
if (isDatabaseModule(resolvedModuleName)) {
|
|
1267
|
-
const installedDialect = await detectInstalledDatabaseDialect(projectRoot, srcDir);
|
|
1268
|
-
if (installedDialect && installedDialect !== resolvedModuleName) {
|
|
1269
|
-
console.log(
|
|
1270
|
-
import_chalk4.default.yellow(
|
|
1271
|
-
`
|
|
1272
|
-
\u26A0 Existing database setup detected: ${databaseLabel(installedDialect)}.`
|
|
1273
|
-
)
|
|
1274
|
-
);
|
|
1275
|
-
console.log(
|
|
1276
|
-
import_chalk4.default.yellow(
|
|
1277
|
-
` Switching to ${databaseLabel(resolvedModuleName)} will overwrite db files and drizzle config.
|
|
1278
|
-
`
|
|
1279
|
-
)
|
|
1280
|
-
);
|
|
1281
|
-
const switchResponse = await (0, import_prompts2.default)({
|
|
1282
|
-
type: "confirm",
|
|
1283
|
-
name: "proceed",
|
|
1284
|
-
message: "Continue and switch database dialect?",
|
|
1285
|
-
initial: false
|
|
1286
|
-
});
|
|
1287
|
-
if (!switchResponse.proceed) {
|
|
1288
|
-
console.log(import_chalk4.default.yellow("Operation cancelled."));
|
|
1289
|
-
return;
|
|
1290
|
-
}
|
|
1291
|
-
databaseBackupPath = await backupDatabaseFiles(projectRoot, srcDir);
|
|
1292
|
-
}
|
|
1293
|
-
const defaultUrl = DEFAULT_DATABASE_URLS[resolvedModuleName];
|
|
1294
|
-
console.log(import_chalk4.default.dim(` Tip: Leave blank to use ${defaultUrl}
|
|
1295
|
-
`));
|
|
1296
|
-
const response = await (0, import_prompts2.default)({
|
|
1297
|
-
type: "text",
|
|
1298
|
-
name: "dbUrl",
|
|
1299
|
-
message: "Database URL",
|
|
1300
|
-
initial: ""
|
|
1301
|
-
});
|
|
1302
|
-
if (response.dbUrl === void 0) {
|
|
1303
|
-
console.log(import_chalk4.default.yellow("Operation cancelled."));
|
|
1304
|
-
return;
|
|
1305
|
-
}
|
|
1306
|
-
const enteredUrl = response.dbUrl?.trim() || "";
|
|
1307
|
-
usedDefaultDbUrl = enteredUrl.length === 0;
|
|
1308
|
-
customDbUrl = validateDatabaseUrl(enteredUrl || defaultUrl, resolvedModuleName);
|
|
2463
|
+
let uploadConfig = null;
|
|
2464
|
+
let uploadDatabaseDialect = null;
|
|
2465
|
+
if (resolvedModuleName === "database" || isDatabaseModule(resolvedModuleName)) {
|
|
2466
|
+
const result = await promptDatabaseConfig(resolvedModuleName, projectRoot, srcDir);
|
|
2467
|
+
if (!result) return;
|
|
2468
|
+
resolvedModuleName = result.resolvedModuleName;
|
|
2469
|
+
selectedDatabaseOrm = result.selectedOrm;
|
|
2470
|
+
selectedDatabaseDialect = result.selectedDialect;
|
|
2471
|
+
customDbUrl = result.customDbUrl;
|
|
2472
|
+
usedDefaultDbUrl = result.usedDefaultDbUrl;
|
|
2473
|
+
databaseBackupPath = result.databaseBackupPath;
|
|
1309
2474
|
}
|
|
1310
2475
|
if (resolvedModuleName === "mailer") {
|
|
1311
|
-
const
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
{ title: "SMTP (Nodemailer)", description: "Gmail, Mailtrap, any SMTP server", value: "smtp" },
|
|
1317
|
-
{ title: "Resend", description: "API-based, easiest setup", value: "resend" }
|
|
1318
|
-
]
|
|
1319
|
-
});
|
|
1320
|
-
if (providerResponse.provider === void 0) {
|
|
1321
|
-
console.log(import_chalk4.default.yellow("Operation cancelled."));
|
|
1322
|
-
return;
|
|
1323
|
-
}
|
|
1324
|
-
mailerProvider = providerResponse.provider;
|
|
1325
|
-
console.log(import_chalk4.default.dim(" Tip: Leave fields blank to use placeholder values and configure later\n"));
|
|
1326
|
-
if (mailerProvider === "smtp") {
|
|
1327
|
-
const smtpResponse = await (0, import_prompts2.default)([
|
|
1328
|
-
{
|
|
1329
|
-
type: "text",
|
|
1330
|
-
name: "host",
|
|
1331
|
-
message: "SMTP Host",
|
|
1332
|
-
initial: ""
|
|
1333
|
-
},
|
|
1334
|
-
{
|
|
1335
|
-
type: "text",
|
|
1336
|
-
name: "port",
|
|
1337
|
-
message: "SMTP Port",
|
|
1338
|
-
initial: "587"
|
|
1339
|
-
},
|
|
1340
|
-
{
|
|
1341
|
-
type: "text",
|
|
1342
|
-
name: "user",
|
|
1343
|
-
message: "SMTP User",
|
|
1344
|
-
initial: ""
|
|
1345
|
-
},
|
|
1346
|
-
{
|
|
1347
|
-
type: "password",
|
|
1348
|
-
name: "pass",
|
|
1349
|
-
message: "SMTP Password"
|
|
1350
|
-
},
|
|
1351
|
-
{
|
|
1352
|
-
type: "text",
|
|
1353
|
-
name: "from",
|
|
1354
|
-
message: "Mail From address",
|
|
1355
|
-
initial: ""
|
|
1356
|
-
}
|
|
1357
|
-
]);
|
|
1358
|
-
if (smtpResponse.host === void 0) {
|
|
1359
|
-
console.log(import_chalk4.default.yellow("Operation cancelled."));
|
|
1360
|
-
return;
|
|
1361
|
-
}
|
|
1362
|
-
const host = smtpResponse.host?.trim() || "";
|
|
1363
|
-
const user = smtpResponse.user?.trim() || "";
|
|
1364
|
-
const pass = smtpResponse.pass?.trim() || "";
|
|
1365
|
-
const from = smtpResponse.from?.trim() || "";
|
|
1366
|
-
const port = smtpResponse.port?.trim() || "587";
|
|
1367
|
-
usedDefaultSmtp = !host && !user;
|
|
1368
|
-
if (!usedDefaultSmtp) {
|
|
1369
|
-
customSmtpVars = {
|
|
1370
|
-
SMTP_HOST: host || "smtp.example.com",
|
|
1371
|
-
SMTP_PORT: port,
|
|
1372
|
-
SMTP_USER: user || "your-email@example.com",
|
|
1373
|
-
SMTP_PASS: pass || "your-password",
|
|
1374
|
-
MAIL_FROM: from || "noreply@example.com"
|
|
1375
|
-
};
|
|
1376
|
-
}
|
|
1377
|
-
} else {
|
|
1378
|
-
const resendResponse = await (0, import_prompts2.default)([
|
|
1379
|
-
{
|
|
1380
|
-
type: "text",
|
|
1381
|
-
name: "apiKey",
|
|
1382
|
-
message: "Resend API Key",
|
|
1383
|
-
initial: ""
|
|
1384
|
-
},
|
|
1385
|
-
{
|
|
1386
|
-
type: "text",
|
|
1387
|
-
name: "from",
|
|
1388
|
-
message: "Mail From address",
|
|
1389
|
-
initial: ""
|
|
1390
|
-
}
|
|
1391
|
-
]);
|
|
1392
|
-
if (resendResponse.apiKey === void 0) {
|
|
1393
|
-
console.log(import_chalk4.default.yellow("Operation cancelled."));
|
|
1394
|
-
return;
|
|
1395
|
-
}
|
|
1396
|
-
const apiKey = resendResponse.apiKey?.trim() || "";
|
|
1397
|
-
const from = resendResponse.from?.trim() || "";
|
|
1398
|
-
usedDefaultSmtp = !apiKey;
|
|
1399
|
-
if (!usedDefaultSmtp) {
|
|
1400
|
-
customSmtpVars = {
|
|
1401
|
-
RESEND_API_KEY: apiKey || "re_your_api_key",
|
|
1402
|
-
MAIL_FROM: from || "onboarding@resend.dev"
|
|
1403
|
-
};
|
|
1404
|
-
}
|
|
1405
|
-
}
|
|
2476
|
+
const result = await promptMailerConfig();
|
|
2477
|
+
if (!result) return;
|
|
2478
|
+
mailerProvider = result.mailerProvider;
|
|
2479
|
+
customSmtpVars = result.customSmtpVars;
|
|
2480
|
+
usedDefaultSmtp = result.usedDefaultSmtp;
|
|
1406
2481
|
}
|
|
1407
2482
|
if (resolvedModuleName === "auth") {
|
|
1408
|
-
const
|
|
1409
|
-
if (!
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
message: "Install API docs module (Scalar + OpenAPI) too?",
|
|
1417
|
-
initial: true
|
|
1418
|
-
});
|
|
1419
|
-
if (docsResponse.installDocs === void 0) {
|
|
1420
|
-
console.log(import_chalk4.default.yellow("Operation cancelled."));
|
|
1421
|
-
return;
|
|
1422
|
-
}
|
|
1423
|
-
shouldInstallDocsForAuth = docsResponse.installDocs;
|
|
1424
|
-
}
|
|
1425
|
-
}
|
|
2483
|
+
const result = await promptAuthConfig(projectRoot, srcDir, options);
|
|
2484
|
+
if (!result) return;
|
|
2485
|
+
shouldInstallDocsForAuth = result.shouldInstallDocsForAuth;
|
|
2486
|
+
authDatabaseDialect = result.authDatabaseDialect;
|
|
2487
|
+
}
|
|
2488
|
+
if (resolvedModuleName === "uploads") {
|
|
2489
|
+
uploadConfig = await promptUploadsConfig(projectRoot, srcDir);
|
|
2490
|
+
if (!uploadConfig) return;
|
|
1426
2491
|
}
|
|
1427
2492
|
const pm = resolvePackageManager(projectRoot);
|
|
1428
2493
|
const spinner = (0, import_ora2.default)(`Checking registry for ${resolvedModuleName}...`).start();
|
|
@@ -1438,10 +2503,41 @@ var add = async (moduleName, options = {}) => {
|
|
|
1438
2503
|
spinner.fail(`Module '${resolvedModuleName}' not found.`);
|
|
1439
2504
|
return;
|
|
1440
2505
|
}
|
|
1441
|
-
spinner.succeed(`Found module: ${
|
|
2506
|
+
spinner.succeed(`Found module: ${import_chalk8.default.cyan(resolvedModuleName)}`);
|
|
1442
2507
|
const moduleDeps = module2.moduleDependencies || [];
|
|
1443
2508
|
currentStep = "module dependency resolution";
|
|
1444
2509
|
await resolveDependencies(moduleDeps, projectRoot);
|
|
2510
|
+
if (resolvedModuleName === "uploads" && uploadConfig) {
|
|
2511
|
+
if (uploadConfig.shouldInstallDatabase) {
|
|
2512
|
+
console.log(import_chalk8.default.blue("\n\u2139 Upload metadata needs a Drizzle database. Installing database module..."));
|
|
2513
|
+
await add("database");
|
|
2514
|
+
}
|
|
2515
|
+
if (uploadConfig.shouldInstallAuth) {
|
|
2516
|
+
console.log(import_chalk8.default.blue("\n\u2139 Upload auth needs the auth module. Installing auth module..."));
|
|
2517
|
+
await add("auth", { yes: true });
|
|
2518
|
+
}
|
|
2519
|
+
uploadDatabaseDialect = await detectInstalledDatabaseDialect(projectRoot, srcDir);
|
|
2520
|
+
if (uploadConfig.useDatabaseMetadata) {
|
|
2521
|
+
if (uploadDatabaseDialect === "database-prisma-pg" || uploadDatabaseDialect === "database-prisma-mysql") {
|
|
2522
|
+
spinner.fail("Uploads metadata currently supports Drizzle-based database setup only.");
|
|
2523
|
+
console.log(import_chalk8.default.yellow("\u2139 Install a Drizzle database, or rerun uploads without metadata."));
|
|
2524
|
+
return;
|
|
2525
|
+
}
|
|
2526
|
+
if (!uploadDatabaseDialect) {
|
|
2527
|
+
spinner.fail("Could not detect a database setup for uploads metadata.");
|
|
2528
|
+
console.log(import_chalk8.default.yellow("\u2139 Install the database module first, then rerun uploads."));
|
|
2529
|
+
return;
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
if (resolvedModuleName === "auth") {
|
|
2534
|
+
authDatabaseDialect = await detectInstalledDatabaseDialect(projectRoot, srcDir);
|
|
2535
|
+
if (authDatabaseDialect === "database-prisma-pg" || authDatabaseDialect === "database-prisma-mysql") {
|
|
2536
|
+
spinner.fail("Auth module currently supports Drizzle-based database setup only.");
|
|
2537
|
+
console.log(import_chalk8.default.yellow("\u2139 Install auth after switching database ORM to Drizzle."));
|
|
2538
|
+
return;
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
1445
2541
|
currentStep = "dependency installation";
|
|
1446
2542
|
spinner.start("Installing dependencies...");
|
|
1447
2543
|
let runtimeDeps = module2.dependencies || [];
|
|
@@ -1455,14 +2551,20 @@ var add = async (moduleName, options = {}) => {
|
|
|
1455
2551
|
devDeps = ["@types/nodemailer"];
|
|
1456
2552
|
}
|
|
1457
2553
|
}
|
|
2554
|
+
if (resolvedModuleName === "uploads" && uploadConfig) {
|
|
2555
|
+
runtimeDeps = ["multer"];
|
|
2556
|
+
devDeps = ["@types/multer"];
|
|
2557
|
+
if (uploadConfig.provider === "cloudinary") {
|
|
2558
|
+
runtimeDeps.push("cloudinary");
|
|
2559
|
+
} else {
|
|
2560
|
+
runtimeDeps.push("@aws-sdk/client-s3", "@aws-sdk/s3-request-presigner");
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
1458
2563
|
await installDependencies(pm, runtimeDeps, projectRoot);
|
|
1459
2564
|
await installDependencies(pm, devDeps, projectRoot, { dev: true });
|
|
1460
2565
|
spinner.succeed("Dependencies installed");
|
|
1461
2566
|
currentStep = "module scaffolding";
|
|
1462
2567
|
spinner.start("Scaffolding files...");
|
|
1463
|
-
if (resolvedModuleName === "auth") {
|
|
1464
|
-
authDatabaseDialect = await detectInstalledDatabaseDialect(projectRoot, srcDir);
|
|
1465
|
-
}
|
|
1466
2568
|
for (const file of module2.files) {
|
|
1467
2569
|
let fetchPath = file.path;
|
|
1468
2570
|
let expectedSha256 = file.sha256;
|
|
@@ -1477,12 +2579,37 @@ var add = async (moduleName, options = {}) => {
|
|
|
1477
2579
|
expectedSha256 = void 0;
|
|
1478
2580
|
expectedSize = void 0;
|
|
1479
2581
|
}
|
|
2582
|
+
if (resolvedModuleName === "uploads" && uploadConfig) {
|
|
2583
|
+
if (file.target === "lib/uploads/provider.ts") {
|
|
2584
|
+
fetchPath = uploadConfig.provider === "cloudinary" ? "express/lib/uploads/provider.cloudinary.ts" : "express/lib/uploads/provider.s3.ts";
|
|
2585
|
+
expectedSha256 = void 0;
|
|
2586
|
+
expectedSize = void 0;
|
|
2587
|
+
}
|
|
2588
|
+
if (file.target === "lib/uploads/metadata.ts") {
|
|
2589
|
+
fetchPath = uploadConfig.useDatabaseMetadata ? "express/lib/uploads/metadata.db.ts" : "express/lib/uploads/metadata.noop.ts";
|
|
2590
|
+
expectedSha256 = void 0;
|
|
2591
|
+
expectedSize = void 0;
|
|
2592
|
+
}
|
|
2593
|
+
if (file.target === "middleware/upload-auth.ts") {
|
|
2594
|
+
fetchPath = `express/middleware/upload-auth.${uploadConfig.authMode}.ts`;
|
|
2595
|
+
expectedSha256 = void 0;
|
|
2596
|
+
expectedSize = void 0;
|
|
2597
|
+
}
|
|
2598
|
+
if (file.target === "db/schema/uploads.ts") {
|
|
2599
|
+
if (!uploadConfig.useDatabaseMetadata) {
|
|
2600
|
+
continue;
|
|
2601
|
+
}
|
|
2602
|
+
fetchPath = uploadDatabaseDialect === "database-mysql" ? "express/db/schema/uploads.mysql.ts" : "express/db/schema/uploads.ts";
|
|
2603
|
+
expectedSha256 = void 0;
|
|
2604
|
+
expectedSize = void 0;
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
1480
2607
|
let content = await fetchFile(fetchPath, {
|
|
1481
2608
|
baseUrl: registryContext.fileBaseUrl,
|
|
1482
2609
|
expectedSha256,
|
|
1483
2610
|
expectedSize
|
|
1484
2611
|
});
|
|
1485
|
-
if (
|
|
2612
|
+
if (isDrizzleDatabaseModule(resolvedModuleName) && file.target === "../drizzle.config.ts") {
|
|
1486
2613
|
const normalizedSrcDir = srcDir.replace(/\\/g, "/");
|
|
1487
2614
|
content = content.replace(
|
|
1488
2615
|
/schema:\s*["'][^"']+["']/,
|
|
@@ -1490,10 +2617,10 @@ var add = async (moduleName, options = {}) => {
|
|
|
1490
2617
|
);
|
|
1491
2618
|
}
|
|
1492
2619
|
const targetPath = resolveSafeTargetPath2(projectRoot, srcDir, file);
|
|
1493
|
-
await
|
|
1494
|
-
await
|
|
2620
|
+
await import_fs_extra12.default.ensureDir(import_path13.default.dirname(targetPath));
|
|
2621
|
+
await import_fs_extra12.default.writeFile(targetPath, content);
|
|
1495
2622
|
}
|
|
1496
|
-
const schemaExports = module2.files.map((file) => file.target.replace(/\\/g, "/")).filter((target) => /^db\/schema\/[^/]+\.ts$/.test(target)).map((target) =>
|
|
2623
|
+
const schemaExports = module2.files.map((file) => file.target.replace(/\\/g, "/")).filter((target) => /^db\/schema\/[^/]+\.ts$/.test(target)).map((target) => import_path13.default.posix.basename(target, ".ts")).filter((name) => name !== "index");
|
|
1497
2624
|
for (const schemaFileName of schemaExports) {
|
|
1498
2625
|
await ensureSchemaExport(projectRoot, srcDir, schemaFileName);
|
|
1499
2626
|
}
|
|
@@ -1526,6 +2653,15 @@ var add = async (moduleName, options = {}) => {
|
|
|
1526
2653
|
spinner.warn("Could not find app.ts - error handler needs manual setup");
|
|
1527
2654
|
}
|
|
1528
2655
|
}
|
|
2656
|
+
if (resolvedModuleName === "rate-limiter") {
|
|
2657
|
+
spinner.start("Configuring rate limiter in app.ts...");
|
|
2658
|
+
const injected = await injectRateLimiter(projectRoot, srcDir);
|
|
2659
|
+
if (injected) {
|
|
2660
|
+
spinner.succeed("Rate limiter configured in app.ts");
|
|
2661
|
+
} else {
|
|
2662
|
+
spinner.warn("Could not find app.ts - rate limiter needs manual setup");
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
1529
2665
|
if (resolvedModuleName === "docs") {
|
|
1530
2666
|
spinner.start("Configuring docs routes...");
|
|
1531
2667
|
const injected = await injectDocsRoutes(projectRoot, srcDir);
|
|
@@ -1544,6 +2680,38 @@ var add = async (moduleName, options = {}) => {
|
|
|
1544
2680
|
spinner.warn("Could not update API docs automatically");
|
|
1545
2681
|
}
|
|
1546
2682
|
}
|
|
2683
|
+
const uploadsInstalled = await isUploadsModuleInstalled(projectRoot, srcDir);
|
|
2684
|
+
if (uploadsInstalled) {
|
|
2685
|
+
const uploadMode = await detectInstalledUploadsMode(projectRoot);
|
|
2686
|
+
if (uploadMode) {
|
|
2687
|
+
spinner.start("Adding uploads endpoints to API docs...");
|
|
2688
|
+
const uploadsDocsInjected = await injectUploadsDocs(projectRoot, srcDir, uploadMode);
|
|
2689
|
+
if (uploadsDocsInjected) {
|
|
2690
|
+
spinner.succeed("Uploads endpoints added to API docs");
|
|
2691
|
+
} else {
|
|
2692
|
+
spinner.warn("Could not update API docs automatically");
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
if (resolvedModuleName === "uploads" && uploadConfig) {
|
|
2698
|
+
spinner.start("Configuring upload routes...");
|
|
2699
|
+
const injected = await injectUploadsRoutes(projectRoot, srcDir);
|
|
2700
|
+
if (injected) {
|
|
2701
|
+
spinner.succeed("Upload routes configured");
|
|
2702
|
+
} else {
|
|
2703
|
+
spinner.warn("Could not configure upload routes automatically");
|
|
2704
|
+
}
|
|
2705
|
+
const docsInstalled = await isDocsModuleInstalled(projectRoot, srcDir);
|
|
2706
|
+
if (docsInstalled) {
|
|
2707
|
+
spinner.start("Adding uploads endpoints to API docs...");
|
|
2708
|
+
const docsInjected = await injectUploadsDocs(projectRoot, srcDir, uploadConfig.mode);
|
|
2709
|
+
if (docsInjected) {
|
|
2710
|
+
spinner.succeed("Uploads endpoints added to API docs");
|
|
2711
|
+
} else {
|
|
2712
|
+
spinner.warn("Could not update API docs automatically");
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
1547
2715
|
}
|
|
1548
2716
|
let envConfigKey = resolvedModuleName;
|
|
1549
2717
|
if (resolvedModuleName === "mailer" && mailerProvider === "resend") {
|
|
@@ -1573,57 +2741,60 @@ var add = async (moduleName, options = {}) => {
|
|
|
1573
2741
|
await updateEnvSchema(projectRoot, srcDir, envConfig.schemaFields);
|
|
1574
2742
|
spinner.succeed("Environment configured");
|
|
1575
2743
|
}
|
|
1576
|
-
|
|
2744
|
+
if (resolvedModuleName === "uploads" && uploadConfig) {
|
|
2745
|
+
currentStep = "environment configuration";
|
|
2746
|
+
spinner.start("Updating environment configuration...");
|
|
2747
|
+
await updateEnvFile(projectRoot, uploadConfig.envVars, true);
|
|
2748
|
+
await updateEnvSchema(projectRoot, srcDir, getUploadEnvSchemaFields(uploadConfig.provider));
|
|
2749
|
+
spinner.succeed("Environment configured");
|
|
2750
|
+
}
|
|
2751
|
+
if (isDatabaseModule(resolvedModuleName)) {
|
|
2752
|
+
const selected = getDatabaseSelection(resolvedModuleName);
|
|
2753
|
+
const orm = selectedDatabaseOrm ?? selected.orm;
|
|
2754
|
+
const dialect = selectedDatabaseDialect ?? selected.dialect;
|
|
2755
|
+
const latestConfig = await readZuroConfig(projectRoot);
|
|
2756
|
+
if (latestConfig) {
|
|
2757
|
+
await writeZuroConfig(projectRoot, {
|
|
2758
|
+
...latestConfig,
|
|
2759
|
+
database: {
|
|
2760
|
+
orm,
|
|
2761
|
+
dialect
|
|
2762
|
+
}
|
|
2763
|
+
});
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2766
|
+
console.log(import_chalk8.default.green(`
|
|
1577
2767
|
\u2714 ${resolvedModuleName} added successfully!
|
|
1578
2768
|
`));
|
|
1579
|
-
if (databaseBackupPath) {
|
|
1580
|
-
console.log(import_chalk4.default.blue(`\u2139 Backup created at: ${databaseBackupPath}
|
|
1581
|
-
`));
|
|
1582
|
-
}
|
|
1583
2769
|
const docsPath = getModuleDocsPath(resolvedModuleName);
|
|
1584
2770
|
const docsUrl = `https://zuro-cli.devbybriyan.com/docs/${docsPath}`;
|
|
1585
|
-
console.log(
|
|
2771
|
+
console.log(import_chalk8.default.blue(`\u2139 Docs: ${docsUrl}`));
|
|
1586
2772
|
if (isDatabaseModule(resolvedModuleName)) {
|
|
1587
|
-
|
|
1588
|
-
console.log(import_chalk4.default.yellow("\u2139 Review DATABASE_URL in .env if your local DB config differs."));
|
|
1589
|
-
}
|
|
1590
|
-
const setupHint = getDatabaseSetupHint(
|
|
1591
|
-
resolvedModuleName,
|
|
1592
|
-
customDbUrl || DEFAULT_DATABASE_URLS[resolvedModuleName]
|
|
1593
|
-
);
|
|
1594
|
-
console.log(import_chalk4.default.yellow(`\u2139 Ensure DB exists: ${setupHint}`));
|
|
1595
|
-
console.log(import_chalk4.default.yellow("\u2139 Run migrations: npx drizzle-kit generate && npx drizzle-kit migrate"));
|
|
2773
|
+
printDatabaseHints(resolvedModuleName, customDbUrl, usedDefaultDbUrl, databaseBackupPath);
|
|
1596
2774
|
}
|
|
1597
2775
|
if (resolvedModuleName === "auth") {
|
|
1598
|
-
|
|
1599
|
-
console.log(import_chalk4.default.yellow("\u2139 BETTER_AUTH_SECRET was generated automatically."));
|
|
1600
|
-
} else {
|
|
1601
|
-
console.log(import_chalk4.default.yellow("\u2139 Review BETTER_AUTH_SECRET and BETTER_AUTH_URL in .env."));
|
|
1602
|
-
}
|
|
1603
|
-
console.log(import_chalk4.default.yellow("\u2139 Run migrations: npx drizzle-kit generate && npx drizzle-kit migrate"));
|
|
2776
|
+
printAuthHints(generatedAuthSecret);
|
|
1604
2777
|
}
|
|
1605
2778
|
if (resolvedModuleName === "mailer") {
|
|
1606
|
-
|
|
1607
|
-
console.log(import_chalk4.default.yellow("\u2139 Placeholder SMTP values added to .env \u2014 update them before sending emails."));
|
|
1608
|
-
} else {
|
|
1609
|
-
console.log(import_chalk4.default.yellow("\u2139 Review SMTP configuration in .env to ensure values are correct."));
|
|
1610
|
-
}
|
|
2779
|
+
printMailerHints(usedDefaultSmtp);
|
|
1611
2780
|
}
|
|
1612
2781
|
if (resolvedModuleName === "docs") {
|
|
1613
|
-
|
|
1614
|
-
|
|
2782
|
+
printDocsHints();
|
|
2783
|
+
}
|
|
2784
|
+
if (resolvedModuleName === "uploads" && uploadConfig) {
|
|
2785
|
+
printUploadHints(uploadConfig);
|
|
1615
2786
|
}
|
|
1616
2787
|
if (resolvedModuleName === "auth" && shouldInstallDocsForAuth) {
|
|
1617
|
-
console.log(
|
|
2788
|
+
console.log(import_chalk8.default.blue("\n\u2139 Installing API docs module..."));
|
|
1618
2789
|
await add("docs", { yes: true });
|
|
1619
2790
|
}
|
|
1620
2791
|
} catch (error) {
|
|
1621
|
-
spinner.fail(
|
|
2792
|
+
spinner.fail(import_chalk8.default.red(`Failed during ${currentStep}.`));
|
|
1622
2793
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1623
|
-
console.error(
|
|
2794
|
+
console.error(import_chalk8.default.red(errorMessage));
|
|
1624
2795
|
console.log(`
|
|
1625
|
-
${
|
|
1626
|
-
console.log(
|
|
2796
|
+
${import_chalk8.default.bold("Retry:")}`);
|
|
2797
|
+
console.log(import_chalk8.default.cyan(` npx zuro-cli add ${resolvedModuleName}`));
|
|
1627
2798
|
}
|
|
1628
2799
|
};
|
|
1629
2800
|
|