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/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
- auth: {
802
+ "database-prisma-pg": {
352
803
  envVars: {
353
- BETTER_AUTH_SECRET: "your-secret-key-at-least-32-characters-long",
354
- BETTER_AUTH_URL: "http://localhost:3000"
804
+ DATABASE_URL: "postgresql://postgres@localhost:5432/mydb"
355
805
  },
356
806
  schemaFields: [
357
- { name: "BETTER_AUTH_SECRET", schema: "z.string().min(32)" },
358
- { name: "BETTER_AUTH_URL", schema: "z.string().url()" }
807
+ { name: "DATABASE_URL", schema: "z.string().url()" }
359
808
  ]
360
809
  },
361
- mailer: {
810
+ "database-prisma-mysql": {
362
811
  envVars: {
363
- SMTP_HOST: "smtp.example.com",
364
- SMTP_PORT: "587",
365
- SMTP_USER: "your-email@example.com",
366
- SMTP_PASS: "your-password",
367
- MAIL_FROM: "noreply@example.com"
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 import_path6 = __toESM(require("path"));
678
- var import_fs_extra6 = __toESM(require("fs-extra"));
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
- if (pgExists || mysqlExists) {
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/commands/add.ts
726
- var import_chalk4 = __toESM(require("chalk"));
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
- function resolvePackageManager(projectRoot) {
740
- if (import_fs_extra6.default.existsSync(import_path6.default.join(projectRoot, "pnpm-lock.yaml"))) {
741
- return "pnpm";
742
- }
743
- if (import_fs_extra6.default.existsSync(import_path6.default.join(projectRoot, "bun.lockb")) || import_fs_extra6.default.existsSync(import_path6.default.join(projectRoot, "bun.lock"))) {
744
- return "bun";
745
- }
746
- if (import_fs_extra6.default.existsSync(import_path6.default.join(projectRoot, "yarn.lock"))) {
747
- return "yarn";
748
- }
749
- return "npm";
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 import_fs_extra6.default.pathExists(import_path6.default.join(projectRoot, srcDir, "lib", "auth.ts"));
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 = import_path6.default.join(projectRoot, srcDir, "app.ts");
960
- if (!await import_fs_extra6.default.pathExists(appPath)) {
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 import_fs_extra6.default.readFile(appPath, "utf-8");
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 = import_path6.default.join(projectRoot, srcDir, "routes", "index.ts");
999
- if (await import_fs_extra6.default.pathExists(routeIndexPath)) {
1000
- let routeContent = await import_fs_extra6.default.readFile(routeIndexPath, "utf-8");
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 import_fs_extra6.default.writeFile(routeIndexPath, routeContent);
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 import_fs_extra6.default.writeFile(appPath, appContent);
1404
+ await import_fs_extra8.default.writeFile(appPath, appContent);
1051
1405
  }
1052
1406
  return true;
1053
1407
  }
1054
- async function injectDocsRoutes(projectRoot, srcDir) {
1055
- const routeIndexPath = import_path6.default.join(projectRoot, srcDir, "routes", "index.ts");
1056
- const routeImport = `import docsRoutes from "./docs.routes";`;
1057
- const routeMountPattern = /rootRouter\.use\(\s*["']\/docs["']\s*,\s*docsRoutes\s*\)/;
1058
- if (await import_fs_extra6.default.pathExists(routeIndexPath)) {
1059
- let routeContent = await import_fs_extra6.default.readFile(routeIndexPath, "utf-8");
1060
- let routeModified = false;
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
- // API docs
1072
- rootRouter.use("/docs", docsRoutes);
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 import_fs_extra6.default.writeFile(routeIndexPath, routeContent);
2185
+ await import_fs_extra11.default.writeFile(routeIndexPath, routeContent);
1083
2186
  }
1084
2187
  return true;
1085
2188
  }
1086
- const appPath = import_path6.default.join(projectRoot, srcDir, "app.ts");
1087
- if (!await import_fs_extra6.default.pathExists(appPath)) {
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 import_fs_extra6.default.readFile(appPath, "utf-8");
2193
+ let appContent = await import_fs_extra11.default.readFile(appPath, "utf-8");
1091
2194
  let appModified = false;
1092
- const appImportResult = appendImport(appContent, `import docsRoutes from "./routes/docs.routes";`);
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\/docs["']\s*,\s*docsRoutes\s*\)/.test(appContent);
2203
+ const hasMount = /app\.use\(\s*["']\/api\/uploads["']\s*,\s*uploadsRoutes\s*\)/.test(appContent);
1101
2204
  if (!hasMount) {
1102
2205
  const setup = `
1103
- // API docs
1104
- app.use("/api/docs", docsRoutes);
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 import_fs_extra6.default.writeFile(appPath, appContent);
2217
+ await import_fs_extra11.default.writeFile(appPath, appContent);
1115
2218
  }
1116
2219
  return true;
1117
2220
  }
1118
- async function injectAuthDocs(projectRoot, srcDir) {
1119
- const openApiPath = import_path6.default.join(projectRoot, srcDir, "lib", "openapi.ts");
1120
- if (!await import_fs_extra6.default.pathExists(openApiPath)) {
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 authMarker = "// ZURO_AUTH_DOCS";
1124
- let content = await import_fs_extra6.default.readFile(openApiPath, "utf-8");
1125
- if (content.includes(authMarker)) {
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 authBlock = `
1133
- const authSignUpSchema = z.object({
1134
- email: z.string().email().openapi({ example: "dev@company.com" }),
1135
- password: z.string().min(8).openapi({ example: "strong-password" }),
1136
- name: z.string().min(1).optional().openapi({ example: "Dev User" }),
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
- const authUserSchema = z.object({
1145
- id: z.string().openapi({ example: "user_123" }),
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/auth/sign-up/email",
1154
- tags: ["Auth"],
1155
- summary: "Register using email and password",
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: authSignUpSchema,
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: "Registration successful" },
2277
+ 200: { description: "Signed upload created" },
1167
2278
  },
1168
2279
  });
1169
2280
 
1170
2281
  registry.registerPath({
1171
2282
  method: "post",
1172
- path: "/api/auth/sign-in/email",
1173
- tags: ["Auth"],
1174
- summary: "Sign in using email and password",
1175
- request: {
1176
- body: {
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
- 200: { description: "Sign in successful" },
1186
- 401: { description: "Invalid credentials" },
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/auth/sign-out",
1193
- tags: ["Auth"],
1194
- summary: "Sign out current user",
2313
+ path: "/api/uploads/multipart/complete",
2314
+ tags: ["Uploads"],
2315
+ summary: "Complete a multipart upload",
1195
2316
  responses: {
1196
- 200: { description: "Sign out successful" },
2317
+ 201: { description: "Multipart upload completed" },
1197
2318
  },
1198
2319
  });
1199
2320
 
1200
2321
  registry.registerPath({
1201
- method: "get",
1202
- path: "/api/users/me",
1203
- tags: ["Auth"],
1204
- summary: "Get current authenticated user",
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
- 200: {
1208
- description: "Current user",
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: z.object({ user: authUserSchema }),
2340
+ schema: uploadAccessSchema,
1212
2341
  },
1213
2342
  },
1214
2343
  },
1215
- 401: { description: "Not authenticated" },
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
- content = content.replace(moduleDocsEndMarker, `${authBlock}
2369
+ const block = `
2370
+ ${commonBlock}${marker}
2371
+ ${proxyBlock}${directBlock}${largeBlock}${sharedOps}`;
2372
+ content = content.replace(moduleDocsEndMarker, `${block}
1220
2373
  ${moduleDocsEndMarker}`);
1221
- await import_fs_extra6.default.writeFile(openApiPath, content);
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
- if (resolvedModuleName === "database") {
1251
- const variantResponse = await (0, import_prompts2.default)({
1252
- type: "select",
1253
- name: "variant",
1254
- message: "Which database dialect?",
1255
- choices: [
1256
- { title: "PostgreSQL", value: "database-pg" },
1257
- { title: "MySQL", value: "database-mysql" }
1258
- ]
1259
- });
1260
- if (!variantResponse.variant) {
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 providerResponse = await (0, import_prompts2.default)({
1312
- type: "select",
1313
- name: "provider",
1314
- message: "Which email provider?",
1315
- choices: [
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 docsInstalled = await isDocsModuleInstalled(projectRoot, srcDir);
1409
- if (!docsInstalled) {
1410
- if (options.yes) {
1411
- shouldInstallDocsForAuth = true;
1412
- } else {
1413
- const docsResponse = await (0, import_prompts2.default)({
1414
- type: "confirm",
1415
- name: "installDocs",
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: ${import_chalk4.default.cyan(resolvedModuleName)}`);
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 (isDatabaseModule(resolvedModuleName) && file.target === "../drizzle.config.ts") {
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 import_fs_extra6.default.ensureDir(import_path6.default.dirname(targetPath));
1494
- await import_fs_extra6.default.writeFile(targetPath, content);
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) => import_path6.default.posix.basename(target, ".ts")).filter((name) => name !== "index");
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
- console.log(import_chalk4.default.green(`
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(import_chalk4.default.blue(`\u2139 Docs: ${docsUrl}`));
2771
+ console.log(import_chalk8.default.blue(`\u2139 Docs: ${docsUrl}`));
1586
2772
  if (isDatabaseModule(resolvedModuleName)) {
1587
- if (usedDefaultDbUrl) {
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
- if (generatedAuthSecret) {
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
- if (usedDefaultSmtp) {
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
- console.log(import_chalk4.default.yellow("\u2139 API docs available at: /api/docs"));
1614
- console.log(import_chalk4.default.yellow("\u2139 OpenAPI spec available at: /api/docs/openapi.json"));
2782
+ printDocsHints();
2783
+ }
2784
+ if (resolvedModuleName === "uploads" && uploadConfig) {
2785
+ printUploadHints(uploadConfig);
1615
2786
  }
1616
2787
  if (resolvedModuleName === "auth" && shouldInstallDocsForAuth) {
1617
- console.log(import_chalk4.default.blue("\n\u2139 Installing API docs module..."));
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(import_chalk4.default.red(`Failed during ${currentStep}.`));
2792
+ spinner.fail(import_chalk8.default.red(`Failed during ${currentStep}.`));
1622
2793
  const errorMessage = error instanceof Error ? error.message : String(error);
1623
- console.error(import_chalk4.default.red(errorMessage));
2794
+ console.error(import_chalk8.default.red(errorMessage));
1624
2795
  console.log(`
1625
- ${import_chalk4.default.bold("Retry:")}`);
1626
- console.log(import_chalk4.default.cyan(` npx zuro-cli add ${resolvedModuleName}`));
2796
+ ${import_chalk8.default.bold("Retry:")}`);
2797
+ console.log(import_chalk8.default.cyan(` npx zuro-cli add ${resolvedModuleName}`));
1627
2798
  }
1628
2799
  };
1629
2800