zuro-cli 0.0.2-beta.13 → 0.0.2-beta.14

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,343 @@ 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
+ getDatabaseSetupHint: () => getDatabaseSetupHint,
71
+ isDatabaseModule: () => isDatabaseModule,
72
+ parseDatabaseDialect: () => parseDatabaseDialect,
73
+ printDatabaseHints: () => printDatabaseHints,
74
+ promptDatabaseConfig: () => promptDatabaseConfig,
75
+ validateDatabaseUrl: () => validateDatabaseUrl
76
+ });
77
+ function parseDatabaseDialect(value) {
78
+ const normalized = value?.trim().toLowerCase();
79
+ if (!normalized) {
80
+ return null;
81
+ }
82
+ if (normalized === "pg" || normalized === "postgres" || normalized === "postgresql" || normalized === "database-pg") {
83
+ return "database-pg";
84
+ }
85
+ if (normalized === "mysql" || normalized === "database-mysql") {
86
+ return "database-mysql";
87
+ }
88
+ return null;
89
+ }
90
+ function isDatabaseModule(moduleName) {
91
+ return moduleName === "database-pg" || moduleName === "database-mysql";
92
+ }
93
+ function validateDatabaseUrl(rawUrl, moduleName) {
94
+ const dbUrl = rawUrl.trim();
95
+ if (!dbUrl) {
96
+ throw new Error("Database URL cannot be empty.");
97
+ }
98
+ let parsed;
99
+ try {
100
+ parsed = new URL(dbUrl);
101
+ } catch {
102
+ throw new Error(`Invalid database URL: '${dbUrl}'.`);
103
+ }
104
+ const protocol = parsed.protocol.toLowerCase();
105
+ if (moduleName === "database-pg" && protocol !== "postgresql:" && protocol !== "postgres:") {
106
+ throw new Error("PostgreSQL URL must start with postgres:// or postgresql://");
107
+ }
108
+ if (moduleName === "database-mysql" && protocol !== "mysql:") {
109
+ throw new Error("MySQL URL must start with mysql://");
110
+ }
111
+ return dbUrl;
112
+ }
113
+ async function detectInstalledDatabaseDialect(projectRoot, srcDir) {
114
+ const dbIndexPath = import_path7.default.join(projectRoot, srcDir, "db", "index.ts");
115
+ if (!import_fs_extra6.default.existsSync(dbIndexPath)) {
116
+ return null;
117
+ }
118
+ const content = await import_fs_extra6.default.readFile(dbIndexPath, "utf-8");
119
+ if (content.includes("drizzle-orm/node-postgres") || content.includes(`from "pg"`)) {
120
+ return "database-pg";
121
+ }
122
+ if (content.includes("drizzle-orm/mysql2") || content.includes(`from "mysql2`)) {
123
+ return "database-mysql";
124
+ }
125
+ return null;
126
+ }
127
+ async function backupDatabaseFiles(projectRoot, srcDir) {
128
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
129
+ const backupRoot = import_path7.default.join(projectRoot, ".zuro", "backups", `database-${timestamp}`);
130
+ const candidates = [
131
+ import_path7.default.join(projectRoot, srcDir, "db", "index.ts"),
132
+ import_path7.default.join(projectRoot, "drizzle.config.ts")
133
+ ];
134
+ let copied = false;
135
+ for (const filePath of candidates) {
136
+ if (!import_fs_extra6.default.existsSync(filePath)) {
137
+ continue;
138
+ }
139
+ const relativePath = import_path7.default.relative(projectRoot, filePath);
140
+ const backupPath = import_path7.default.join(backupRoot, relativePath);
141
+ await import_fs_extra6.default.ensureDir(import_path7.default.dirname(backupPath));
142
+ await import_fs_extra6.default.copyFile(filePath, backupPath);
143
+ copied = true;
144
+ }
145
+ return copied ? backupRoot : null;
146
+ }
147
+ function databaseLabel(moduleName) {
148
+ return moduleName === "database-pg" ? "PostgreSQL" : "MySQL";
149
+ }
150
+ function getDatabaseSetupHint(moduleName, dbUrl) {
151
+ try {
152
+ const parsed = new URL(dbUrl);
153
+ const dbName = parsed.pathname.replace(/^\/+/, "") || "mydb";
154
+ if (moduleName === "database-pg") {
155
+ return `createdb ${dbName}`;
156
+ }
157
+ return `mysql -e "CREATE DATABASE IF NOT EXISTS ${dbName};"`;
158
+ } catch {
159
+ return moduleName === "database-pg" ? "createdb <database_name>" : `mysql -e "CREATE DATABASE IF NOT EXISTS <database_name>;"`;
160
+ }
161
+ }
162
+ async function ensureSchemaExport(projectRoot, srcDir, schemaFileName) {
163
+ const schemaIndexPath = import_path7.default.join(projectRoot, srcDir, "db", "schema", "index.ts");
164
+ if (!await import_fs_extra6.default.pathExists(schemaIndexPath)) {
165
+ return;
166
+ }
167
+ const exportLine = `export * from "./${schemaFileName}";`;
168
+ const content = await import_fs_extra6.default.readFile(schemaIndexPath, "utf-8");
169
+ const normalized = content.replace(/\r\n/g, "\n");
170
+ const exportPattern = new RegExp(
171
+ `^\\s*export\\s*\\*\\s*from\\s*["']\\./${escapeRegex(schemaFileName)}["'];?\\s*$`,
172
+ "m"
173
+ );
174
+ if (exportPattern.test(normalized)) {
175
+ return;
176
+ }
177
+ let next = normalized.replace(/^\s*export\s*\{\s*\};?\s*$/m, "").trimEnd();
178
+ if (next.length > 0) {
179
+ next += "\n\n";
180
+ }
181
+ next += `${exportLine}
182
+ `;
183
+ await import_fs_extra6.default.writeFile(schemaIndexPath, next);
184
+ }
185
+ async function promptDatabaseConfig(initialModuleName, projectRoot, srcDir) {
186
+ let resolvedModuleName;
187
+ if (initialModuleName === "database") {
188
+ const variantResponse = await (0, import_prompts2.default)({
189
+ type: "select",
190
+ name: "variant",
191
+ message: "Which database dialect?",
192
+ choices: [
193
+ { title: "PostgreSQL", value: "database-pg" },
194
+ { title: "MySQL", value: "database-mysql" }
195
+ ]
196
+ });
197
+ if (!variantResponse.variant) {
198
+ console.log(import_chalk4.default.yellow("Operation cancelled."));
199
+ return null;
200
+ }
201
+ resolvedModuleName = variantResponse.variant;
202
+ } else {
203
+ resolvedModuleName = initialModuleName;
204
+ }
205
+ let databaseBackupPath = null;
206
+ const installedDialect = await detectInstalledDatabaseDialect(projectRoot, srcDir);
207
+ if (installedDialect && installedDialect !== resolvedModuleName) {
208
+ console.log(
209
+ import_chalk4.default.yellow(
210
+ `
211
+ \u26A0 Existing database setup detected: ${databaseLabel(installedDialect)}.`
212
+ )
213
+ );
214
+ console.log(
215
+ import_chalk4.default.yellow(
216
+ ` Switching to ${databaseLabel(resolvedModuleName)} will overwrite db files and drizzle config.
217
+ `
218
+ )
219
+ );
220
+ const switchResponse = await (0, import_prompts2.default)({
221
+ type: "confirm",
222
+ name: "proceed",
223
+ message: "Continue and switch database dialect?",
224
+ initial: false
225
+ });
226
+ if (!switchResponse.proceed) {
227
+ console.log(import_chalk4.default.yellow("Operation cancelled."));
228
+ return null;
229
+ }
230
+ databaseBackupPath = await backupDatabaseFiles(projectRoot, srcDir);
231
+ }
232
+ const defaultUrl = DEFAULT_DATABASE_URLS[resolvedModuleName];
233
+ console.log(import_chalk4.default.dim(` Tip: Leave blank to use ${defaultUrl}
234
+ `));
235
+ const response = await (0, import_prompts2.default)({
236
+ type: "text",
237
+ name: "dbUrl",
238
+ message: "Database URL",
239
+ initial: ""
240
+ });
241
+ if (response.dbUrl === void 0) {
242
+ console.log(import_chalk4.default.yellow("Operation cancelled."));
243
+ return null;
244
+ }
245
+ const enteredUrl = response.dbUrl?.trim() || "";
246
+ const usedDefaultDbUrl = enteredUrl.length === 0;
247
+ const customDbUrl = validateDatabaseUrl(enteredUrl || defaultUrl, resolvedModuleName);
248
+ return { resolvedModuleName, customDbUrl, usedDefaultDbUrl, databaseBackupPath };
249
+ }
250
+ function printDatabaseHints(moduleName, customDbUrl, usedDefaultDbUrl, databaseBackupPath) {
251
+ if (databaseBackupPath) {
252
+ console.log(import_chalk4.default.blue(`\u2139 Backup created at: ${databaseBackupPath}
253
+ `));
254
+ }
255
+ if (usedDefaultDbUrl) {
256
+ console.log(import_chalk4.default.yellow("\u2139 Review DATABASE_URL in .env if your local DB config differs."));
257
+ }
258
+ const setupHint = getDatabaseSetupHint(
259
+ moduleName,
260
+ customDbUrl || DEFAULT_DATABASE_URLS[moduleName]
261
+ );
262
+ console.log(import_chalk4.default.yellow(`\u2139 Ensure DB exists: ${setupHint}`));
263
+ console.log(import_chalk4.default.yellow("\u2139 Run migrations: npx drizzle-kit generate && npx drizzle-kit migrate"));
264
+ }
265
+ var import_path7, import_fs_extra6, import_prompts2, import_chalk4, DEFAULT_DATABASE_URLS;
266
+ var init_database_handler = __esm({
267
+ "src/handlers/database.handler.ts"() {
268
+ "use strict";
269
+ import_path7 = __toESM(require("path"));
270
+ import_fs_extra6 = __toESM(require("fs-extra"));
271
+ import_prompts2 = __toESM(require("prompts"));
272
+ import_chalk4 = __toESM(require("chalk"));
273
+ init_code_inject();
274
+ DEFAULT_DATABASE_URLS = {
275
+ "database-pg": "postgresql://postgres@localhost:5432/mydb",
276
+ "database-mysql": "mysql://root@localhost:3306/mydb"
277
+ };
278
+ }
279
+ });
280
+
281
+ // src/handlers/docs.handler.ts
282
+ var docs_handler_exports = {};
283
+ __export(docs_handler_exports, {
284
+ injectDocsRoutes: () => injectDocsRoutes,
285
+ isDocsModuleInstalled: () => isDocsModuleInstalled,
286
+ printDocsHints: () => printDocsHints
287
+ });
288
+ async function isDocsModuleInstalled(projectRoot, srcDir) {
289
+ return await import_fs_extra7.default.pathExists(import_path8.default.join(projectRoot, srcDir, "lib", "openapi.ts"));
290
+ }
291
+ async function injectDocsRoutes(projectRoot, srcDir) {
292
+ const routeIndexPath = import_path8.default.join(projectRoot, srcDir, "routes", "index.ts");
293
+ const routeImport = `import docsRoutes from "./docs.routes";`;
294
+ const routeMountPattern = /rootRouter\.use\(\s*["']\/docs["']\s*,\s*docsRoutes\s*\)/;
295
+ if (await import_fs_extra7.default.pathExists(routeIndexPath)) {
296
+ let routeContent = await import_fs_extra7.default.readFile(routeIndexPath, "utf-8");
297
+ let routeModified = false;
298
+ const importResult = appendImport(routeContent, routeImport);
299
+ if (!importResult.inserted) {
300
+ return false;
301
+ }
302
+ if (importResult.source !== routeContent) {
303
+ routeContent = importResult.source;
304
+ routeModified = true;
305
+ }
306
+ if (!routeMountPattern.test(routeContent)) {
307
+ const routeSetup = `
308
+ // API docs
309
+ rootRouter.use("/docs", docsRoutes);
310
+ `;
311
+ const exportMatch = routeContent.match(/export default rootRouter;?\s*$/m);
312
+ if (!exportMatch || exportMatch.index === void 0) {
313
+ return false;
314
+ }
315
+ routeContent = routeContent.slice(0, exportMatch.index) + routeSetup + "\n" + routeContent.slice(exportMatch.index);
316
+ routeModified = true;
317
+ }
318
+ if (routeModified) {
319
+ await import_fs_extra7.default.writeFile(routeIndexPath, routeContent);
320
+ }
321
+ return true;
322
+ }
323
+ const appPath = import_path8.default.join(projectRoot, srcDir, "app.ts");
324
+ if (!await import_fs_extra7.default.pathExists(appPath)) {
325
+ return false;
326
+ }
327
+ let appContent = await import_fs_extra7.default.readFile(appPath, "utf-8");
328
+ let appModified = false;
329
+ const appImportResult = appendImport(appContent, `import docsRoutes from "./routes/docs.routes";`);
330
+ if (!appImportResult.inserted) {
331
+ return false;
332
+ }
333
+ if (appImportResult.source !== appContent) {
334
+ appContent = appImportResult.source;
335
+ appModified = true;
336
+ }
337
+ const hasMount = /app\.use\(\s*["']\/api\/docs["']\s*,\s*docsRoutes\s*\)/.test(appContent);
338
+ if (!hasMount) {
339
+ const setup = `
340
+ // API docs
341
+ app.use("/api/docs", docsRoutes);
342
+ `;
343
+ const exportMatch = appContent.match(/export default app;?\s*$/m);
344
+ if (!exportMatch || exportMatch.index === void 0) {
345
+ return false;
346
+ }
347
+ appContent = appContent.slice(0, exportMatch.index) + setup + "\n" + appContent.slice(exportMatch.index);
348
+ appModified = true;
349
+ }
350
+ if (appModified) {
351
+ await import_fs_extra7.default.writeFile(appPath, appContent);
352
+ }
353
+ return true;
354
+ }
355
+ function printDocsHints() {
356
+ const chalk8 = require("chalk");
357
+ console.log(chalk8.yellow("\u2139 API docs available at: /api/docs"));
358
+ console.log(chalk8.yellow("\u2139 OpenAPI spec available at: /api/docs/openapi.json"));
359
+ }
360
+ var import_path8, import_fs_extra7;
361
+ var init_docs_handler = __esm({
362
+ "src/handlers/docs.handler.ts"() {
363
+ "use strict";
364
+ import_path8 = __toESM(require("path"));
365
+ import_fs_extra7 = __toESM(require("fs-extra"));
366
+ init_code_inject();
367
+ }
368
+ });
369
+
26
370
  // src/index.ts
27
371
  var import_commander = require("commander");
28
372
 
@@ -32,6 +376,7 @@ var import_chalk2 = __toESM(require("chalk"));
32
376
  var import_fs_extra4 = __toESM(require("fs-extra"));
33
377
  var import_path4 = __toESM(require("path"));
34
378
  var import_prompts = __toESM(require("prompts"));
379
+ var import_child_process = require("child_process");
35
380
 
36
381
  // src/utils/registry.ts
37
382
  var import_node_crypto = require("crypto");
@@ -383,6 +728,16 @@ var ENV_CONFIGS = {
383
728
  { name: "RESEND_API_KEY", schema: "z.string().min(1)" },
384
729
  { name: "MAIL_FROM", schema: "z.string().min(1)" }
385
730
  ]
731
+ },
732
+ "rate-limiter": {
733
+ envVars: {
734
+ RATE_LIMIT_WINDOW_MS: "900000",
735
+ RATE_LIMIT_MAX: "100"
736
+ },
737
+ schemaFields: [
738
+ { name: "RATE_LIMIT_WINDOW_MS", schema: "z.coerce.number().default(900000)" },
739
+ { name: "RATE_LIMIT_MAX", schema: "z.coerce.number().default(100)" }
740
+ ]
386
741
  }
387
742
  };
388
743
 
@@ -496,29 +851,85 @@ bun.lockb
496
851
  await import_fs_extra4.default.writeFile(prettierIgnorePath, ignoreContent);
497
852
  }
498
853
  }
499
- async function init() {
500
- const cwd = process.cwd();
501
- const isExistingProject = await import_fs_extra4.default.pathExists(import_path4.default.join(cwd, "package.json"));
502
- const existingZuroConfig = await readZuroConfig(cwd);
503
- if (isExistingProject && !existingZuroConfig) {
504
- showNonZuroProjectMessage();
854
+ async function setupGitignore(targetDir) {
855
+ const gitignorePath = import_path4.default.join(targetDir, ".gitignore");
856
+ if (await import_fs_extra4.default.pathExists(gitignorePath)) {
505
857
  return;
506
858
  }
507
- let targetDir = cwd;
508
- let pm = "npm";
509
- let srcDir = "src";
510
- let projectName = import_path4.default.basename(cwd);
511
- let enablePrettier = false;
512
- if (isExistingProject) {
513
- console.log(import_chalk2.default.blue("\u2139 Existing project detected."));
514
- projectName = import_path4.default.basename(cwd);
515
- if (await import_fs_extra4.default.pathExists(import_path4.default.join(cwd, "pnpm-lock.yaml"))) {
516
- pm = "pnpm";
517
- } else if (await import_fs_extra4.default.pathExists(import_path4.default.join(cwd, "bun.lockb"))) {
518
- pm = "bun";
519
- } else if (await import_fs_extra4.default.pathExists(import_path4.default.join(cwd, "yarn.lock"))) {
520
- pm = "yarn";
521
- }
859
+ const gitignoreContent = `# dependencies
860
+ node_modules
861
+
862
+ # build output
863
+ dist
864
+ build
865
+
866
+ # environment variables
867
+ .env
868
+ .env.*
869
+ !.env.example
870
+
871
+ # logs
872
+ *.log
873
+ npm-debug.log*
874
+ pnpm-debug.log*
875
+
876
+ # coverage
877
+ coverage
878
+
879
+ # OS files
880
+ .DS_Store
881
+ Thumbs.db
882
+
883
+ # IDE
884
+ .vscode
885
+ .idea
886
+ *.swp
887
+ *.swo
888
+ `;
889
+ await import_fs_extra4.default.writeFile(gitignorePath, gitignoreContent);
890
+ }
891
+ function tryGitInit(targetDir) {
892
+ try {
893
+ (0, import_child_process.execSync)("git rev-parse --is-inside-work-tree", {
894
+ cwd: targetDir,
895
+ stdio: "ignore"
896
+ });
897
+ return;
898
+ } catch {
899
+ }
900
+ try {
901
+ (0, import_child_process.execSync)("git init", { cwd: targetDir, stdio: "ignore" });
902
+ (0, import_child_process.execSync)("git add -A", { cwd: targetDir, stdio: "ignore" });
903
+ (0, import_child_process.execSync)('git commit -m "Initial commit from zuro-cli"', {
904
+ cwd: targetDir,
905
+ stdio: "ignore"
906
+ });
907
+ } catch {
908
+ }
909
+ }
910
+ async function init() {
911
+ const cwd = process.cwd();
912
+ const isExistingProject = await import_fs_extra4.default.pathExists(import_path4.default.join(cwd, "package.json"));
913
+ const existingZuroConfig = await readZuroConfig(cwd);
914
+ if (isExistingProject && !existingZuroConfig) {
915
+ showNonZuroProjectMessage();
916
+ return;
917
+ }
918
+ let targetDir = cwd;
919
+ let pm = "npm";
920
+ let srcDir = "src";
921
+ let projectName = import_path4.default.basename(cwd);
922
+ let enablePrettier = false;
923
+ if (isExistingProject) {
924
+ console.log(import_chalk2.default.blue("\u2139 Existing project detected."));
925
+ projectName = import_path4.default.basename(cwd);
926
+ if (await import_fs_extra4.default.pathExists(import_path4.default.join(cwd, "pnpm-lock.yaml"))) {
927
+ pm = "pnpm";
928
+ } else if (await import_fs_extra4.default.pathExists(import_path4.default.join(cwd, "bun.lockb"))) {
929
+ pm = "bun";
930
+ } else if (await import_fs_extra4.default.pathExists(import_path4.default.join(cwd, "yarn.lock"))) {
931
+ pm = "yarn";
932
+ }
522
933
  const response = await (0, import_prompts.default)({
523
934
  type: "text",
524
935
  name: "srcDir",
@@ -647,8 +1058,16 @@ async function init() {
647
1058
  currentStep = "prettier setup";
648
1059
  await setupPrettier(targetDir);
649
1060
  }
1061
+ currentStep = "gitignore setup";
1062
+ spinner.text = "Setting up .gitignore...";
1063
+ await setupGitignore(targetDir);
650
1064
  currentStep = "config write";
651
1065
  await writeZuroConfig(targetDir, zuroConfig);
1066
+ if (!isExistingProject) {
1067
+ currentStep = "git init";
1068
+ spinner.text = "Initializing git repository...";
1069
+ tryGitInit(targetDir);
1070
+ }
652
1071
  spinner.succeed(import_chalk2.default.green("Project initialized successfully!"));
653
1072
  console.log(`
654
1073
  ${import_chalk2.default.bold("Next steps:")}`);
@@ -672,10 +1091,9 @@ ${import_chalk2.default.bold("Retry:")}`);
672
1091
  }
673
1092
 
674
1093
  // src/commands/add.ts
675
- var import_prompts2 = __toESM(require("prompts"));
676
1094
  var import_ora2 = __toESM(require("ora"));
677
- var import_path6 = __toESM(require("path"));
678
- var import_fs_extra6 = __toESM(require("fs-extra"));
1095
+ var import_path12 = __toESM(require("path"));
1096
+ var import_fs_extra11 = __toESM(require("fs-extra"));
679
1097
  var import_node_crypto2 = require("crypto");
680
1098
 
681
1099
  // src/utils/dependency.ts
@@ -691,7 +1109,8 @@ var BLOCK_SIGNATURES = {
691
1109
  logger: "lib/logger.ts",
692
1110
  auth: "lib/auth.ts",
693
1111
  mailer: "lib/mailer.ts",
694
- docs: "lib/openapi.ts"
1112
+ docs: "lib/openapi.ts",
1113
+ "rate-limiter": "middleware/rate-limiter.ts"
695
1114
  };
696
1115
  var resolveDependencies = async (moduleDependencies, cwd) => {
697
1116
  if (!moduleDependencies || moduleDependencies.length === 0) {
@@ -722,12 +1141,8 @@ var resolveDependencies = async (moduleDependencies, cwd) => {
722
1141
  }
723
1142
  };
724
1143
 
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
- };
1144
+ // src/utils/paths.ts
1145
+ var import_path6 = __toESM(require("path"));
731
1146
  function resolveSafeTargetPath2(projectRoot, srcDir, file) {
732
1147
  const targetPath = import_path6.default.resolve(projectRoot, srcDir, file.target);
733
1148
  const normalizedRoot = import_path6.default.resolve(projectRoot);
@@ -736,231 +1151,27 @@ function resolveSafeTargetPath2(projectRoot, srcDir, file) {
736
1151
  }
737
1152
  return targetPath;
738
1153
  }
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
- }
1154
+
1155
+ // src/commands/add.ts
1156
+ init_code_inject();
1157
+ var import_chalk7 = __toESM(require("chalk"));
1158
+ init_database_handler();
1159
+
1160
+ // src/handlers/auth.handler.ts
1161
+ var import_path9 = __toESM(require("path"));
1162
+ var import_fs_extra8 = __toESM(require("fs-extra"));
1163
+ var import_prompts3 = __toESM(require("prompts"));
1164
+ var import_chalk5 = __toESM(require("chalk"));
1165
+ init_code_inject();
904
1166
  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;
1167
+ return await import_fs_extra8.default.pathExists(import_path9.default.join(projectRoot, srcDir, "lib", "auth.ts"));
957
1168
  }
958
1169
  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)) {
1170
+ const appPath = import_path9.default.join(projectRoot, srcDir, "app.ts");
1171
+ if (!await import_fs_extra8.default.pathExists(appPath)) {
961
1172
  return false;
962
1173
  }
963
- let appContent = await import_fs_extra6.default.readFile(appPath, "utf-8");
1174
+ let appContent = await import_fs_extra8.default.readFile(appPath, "utf-8");
964
1175
  const authHandlerImport = `import { toNodeHandler } from "better-auth/node";`;
965
1176
  const authImport = `import { auth } from "./lib/auth";`;
966
1177
  const routeIndexUserImport = `import userRoutes from "./user.routes";`;
@@ -995,9 +1206,9 @@ async function injectAuthRoutes(projectRoot, srcDir) {
995
1206
  appContent = appContent.slice(0, insertionIndex) + authMountLine + appContent.slice(insertionIndex);
996
1207
  appModified = true;
997
1208
  }
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");
1209
+ const routeIndexPath = import_path9.default.join(projectRoot, srcDir, "routes", "index.ts");
1210
+ if (await import_fs_extra8.default.pathExists(routeIndexPath)) {
1211
+ let routeContent = await import_fs_extra8.default.readFile(routeIndexPath, "utf-8");
1001
1212
  let routeModified = false;
1002
1213
  const userImportResult = appendImport(routeContent, routeIndexUserImport);
1003
1214
  if (!userImportResult.inserted) {
@@ -1021,7 +1232,7 @@ rootRouter.use("/users", userRoutes);
1021
1232
  routeModified = true;
1022
1233
  }
1023
1234
  if (routeModified) {
1024
- await import_fs_extra6.default.writeFile(routeIndexPath, routeContent);
1235
+ await import_fs_extra8.default.writeFile(routeIndexPath, routeContent);
1025
1236
  }
1026
1237
  } else {
1027
1238
  const hasUserRoute = /app\.use\(\s*["']\/api\/users["']\s*,\s*userRoutes\s*\)/.test(appContent);
@@ -1047,81 +1258,17 @@ app.use("/api/users", userRoutes);
1047
1258
  }
1048
1259
  }
1049
1260
  if (appModified) {
1050
- await import_fs_extra6.default.writeFile(appPath, appContent);
1051
- }
1052
- return true;
1053
- }
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;
1061
- const importResult = appendImport(routeContent, routeImport);
1062
- if (!importResult.inserted) {
1063
- return false;
1064
- }
1065
- if (importResult.source !== routeContent) {
1066
- routeContent = importResult.source;
1067
- routeModified = true;
1068
- }
1069
- if (!routeMountPattern.test(routeContent)) {
1070
- const routeSetup = `
1071
- // API docs
1072
- rootRouter.use("/docs", docsRoutes);
1073
- `;
1074
- const exportMatch = routeContent.match(/export default rootRouter;?\s*$/m);
1075
- if (!exportMatch || exportMatch.index === void 0) {
1076
- return false;
1077
- }
1078
- routeContent = routeContent.slice(0, exportMatch.index) + routeSetup + "\n" + routeContent.slice(exportMatch.index);
1079
- routeModified = true;
1080
- }
1081
- if (routeModified) {
1082
- await import_fs_extra6.default.writeFile(routeIndexPath, routeContent);
1083
- }
1084
- return true;
1085
- }
1086
- const appPath = import_path6.default.join(projectRoot, srcDir, "app.ts");
1087
- if (!await import_fs_extra6.default.pathExists(appPath)) {
1088
- return false;
1089
- }
1090
- let appContent = await import_fs_extra6.default.readFile(appPath, "utf-8");
1091
- let appModified = false;
1092
- const appImportResult = appendImport(appContent, `import docsRoutes from "./routes/docs.routes";`);
1093
- if (!appImportResult.inserted) {
1094
- return false;
1095
- }
1096
- if (appImportResult.source !== appContent) {
1097
- appContent = appImportResult.source;
1098
- appModified = true;
1099
- }
1100
- const hasMount = /app\.use\(\s*["']\/api\/docs["']\s*,\s*docsRoutes\s*\)/.test(appContent);
1101
- if (!hasMount) {
1102
- const setup = `
1103
- // API docs
1104
- app.use("/api/docs", docsRoutes);
1105
- `;
1106
- const exportMatch = appContent.match(/export default app;?\s*$/m);
1107
- if (!exportMatch || exportMatch.index === void 0) {
1108
- return false;
1109
- }
1110
- appContent = appContent.slice(0, exportMatch.index) + setup + "\n" + appContent.slice(exportMatch.index);
1111
- appModified = true;
1112
- }
1113
- if (appModified) {
1114
- await import_fs_extra6.default.writeFile(appPath, appContent);
1261
+ await import_fs_extra8.default.writeFile(appPath, appContent);
1115
1262
  }
1116
1263
  return true;
1117
1264
  }
1118
1265
  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)) {
1266
+ const openApiPath = import_path9.default.join(projectRoot, srcDir, "lib", "openapi.ts");
1267
+ if (!await import_fs_extra8.default.pathExists(openApiPath)) {
1121
1268
  return false;
1122
1269
  }
1123
1270
  const authMarker = "// ZURO_AUTH_DOCS";
1124
- let content = await import_fs_extra6.default.readFile(openApiPath, "utf-8");
1271
+ let content = await import_fs_extra8.default.readFile(openApiPath, "utf-8");
1125
1272
  if (content.includes(authMarker)) {
1126
1273
  return true;
1127
1274
  }
@@ -1218,9 +1365,298 @@ registry.registerPath({
1218
1365
  `;
1219
1366
  content = content.replace(moduleDocsEndMarker, `${authBlock}
1220
1367
  ${moduleDocsEndMarker}`);
1221
- await import_fs_extra6.default.writeFile(openApiPath, content);
1368
+ await import_fs_extra8.default.writeFile(openApiPath, content);
1369
+ return true;
1370
+ }
1371
+ async function promptAuthConfig(projectRoot, srcDir, options) {
1372
+ const { isDocsModuleInstalled: isDocsModuleInstalled2 } = await Promise.resolve().then(() => (init_docs_handler(), docs_handler_exports));
1373
+ const docsInstalled = await isDocsModuleInstalled2(projectRoot, srcDir);
1374
+ let shouldInstallDocsForAuth = false;
1375
+ if (!docsInstalled) {
1376
+ if (options.yes) {
1377
+ shouldInstallDocsForAuth = true;
1378
+ } else {
1379
+ const docsResponse = await (0, import_prompts3.default)({
1380
+ type: "confirm",
1381
+ name: "installDocs",
1382
+ message: "Install API docs module (Scalar + OpenAPI) too?",
1383
+ initial: true
1384
+ });
1385
+ if (docsResponse.installDocs === void 0) {
1386
+ console.log(import_chalk5.default.yellow("Operation cancelled."));
1387
+ return null;
1388
+ }
1389
+ shouldInstallDocsForAuth = docsResponse.installDocs;
1390
+ }
1391
+ }
1392
+ const { detectInstalledDatabaseDialect: detectInstalledDatabaseDialect2 } = await Promise.resolve().then(() => (init_database_handler(), database_handler_exports));
1393
+ const authDatabaseDialect = await detectInstalledDatabaseDialect2(projectRoot, srcDir);
1394
+ return { shouldInstallDocsForAuth, authDatabaseDialect };
1395
+ }
1396
+ function printAuthHints(generatedAuthSecret) {
1397
+ if (generatedAuthSecret) {
1398
+ console.log(import_chalk5.default.yellow("\u2139 BETTER_AUTH_SECRET was generated automatically."));
1399
+ } else {
1400
+ console.log(import_chalk5.default.yellow("\u2139 Review BETTER_AUTH_SECRET and BETTER_AUTH_URL in .env."));
1401
+ }
1402
+ console.log(import_chalk5.default.yellow("\u2139 Run migrations: npx drizzle-kit generate && npx drizzle-kit migrate"));
1403
+ }
1404
+
1405
+ // src/handlers/mailer.handler.ts
1406
+ var import_prompts4 = __toESM(require("prompts"));
1407
+ var import_chalk6 = __toESM(require("chalk"));
1408
+ async function promptMailerConfig() {
1409
+ const providerResponse = await (0, import_prompts4.default)({
1410
+ type: "select",
1411
+ name: "provider",
1412
+ message: "Which email provider?",
1413
+ choices: [
1414
+ { title: "SMTP (Nodemailer)", description: "Gmail, Mailtrap, any SMTP server", value: "smtp" },
1415
+ { title: "Resend", description: "API-based, easiest setup", value: "resend" }
1416
+ ]
1417
+ });
1418
+ if (providerResponse.provider === void 0) {
1419
+ console.log(import_chalk6.default.yellow("Operation cancelled."));
1420
+ return null;
1421
+ }
1422
+ const mailerProvider = providerResponse.provider;
1423
+ let customSmtpVars;
1424
+ let usedDefaultSmtp = false;
1425
+ console.log(import_chalk6.default.dim(" Tip: Leave fields blank to use placeholder values and configure later\n"));
1426
+ if (mailerProvider === "smtp") {
1427
+ const smtpResponse = await (0, import_prompts4.default)([
1428
+ {
1429
+ type: "text",
1430
+ name: "host",
1431
+ message: "SMTP Host",
1432
+ initial: ""
1433
+ },
1434
+ {
1435
+ type: "text",
1436
+ name: "port",
1437
+ message: "SMTP Port",
1438
+ initial: "587"
1439
+ },
1440
+ {
1441
+ type: "text",
1442
+ name: "user",
1443
+ message: "SMTP User",
1444
+ initial: ""
1445
+ },
1446
+ {
1447
+ type: "password",
1448
+ name: "pass",
1449
+ message: "SMTP Password"
1450
+ },
1451
+ {
1452
+ type: "text",
1453
+ name: "from",
1454
+ message: "Mail From address",
1455
+ initial: ""
1456
+ }
1457
+ ]);
1458
+ if (smtpResponse.host === void 0) {
1459
+ console.log(import_chalk6.default.yellow("Operation cancelled."));
1460
+ return null;
1461
+ }
1462
+ const host = smtpResponse.host?.trim() || "";
1463
+ const user = smtpResponse.user?.trim() || "";
1464
+ const pass = smtpResponse.pass?.trim() || "";
1465
+ const from = smtpResponse.from?.trim() || "";
1466
+ const port = smtpResponse.port?.trim() || "587";
1467
+ usedDefaultSmtp = !host && !user;
1468
+ if (!usedDefaultSmtp) {
1469
+ customSmtpVars = {
1470
+ SMTP_HOST: host || "smtp.example.com",
1471
+ SMTP_PORT: port,
1472
+ SMTP_USER: user || "your-email@example.com",
1473
+ SMTP_PASS: pass || "your-password",
1474
+ MAIL_FROM: from || "noreply@example.com"
1475
+ };
1476
+ }
1477
+ } else {
1478
+ const resendResponse = await (0, import_prompts4.default)([
1479
+ {
1480
+ type: "text",
1481
+ name: "apiKey",
1482
+ message: "Resend API Key",
1483
+ initial: ""
1484
+ },
1485
+ {
1486
+ type: "text",
1487
+ name: "from",
1488
+ message: "Mail From address",
1489
+ initial: ""
1490
+ }
1491
+ ]);
1492
+ if (resendResponse.apiKey === void 0) {
1493
+ console.log(import_chalk6.default.yellow("Operation cancelled."));
1494
+ return null;
1495
+ }
1496
+ const apiKey = resendResponse.apiKey?.trim() || "";
1497
+ const from = resendResponse.from?.trim() || "";
1498
+ usedDefaultSmtp = !apiKey;
1499
+ if (!usedDefaultSmtp) {
1500
+ customSmtpVars = {
1501
+ RESEND_API_KEY: apiKey || "re_your_api_key",
1502
+ MAIL_FROM: from || "onboarding@resend.dev"
1503
+ };
1504
+ }
1505
+ }
1506
+ return { mailerProvider, customSmtpVars, usedDefaultSmtp };
1507
+ }
1508
+ function printMailerHints(usedDefaultSmtp) {
1509
+ if (usedDefaultSmtp) {
1510
+ console.log(import_chalk6.default.yellow("\u2139 Placeholder SMTP values added to .env \u2014 update them before sending emails."));
1511
+ } else {
1512
+ console.log(import_chalk6.default.yellow("\u2139 Review SMTP configuration in .env to ensure values are correct."));
1513
+ }
1514
+ }
1515
+
1516
+ // src/commands/add.ts
1517
+ init_docs_handler();
1518
+
1519
+ // src/handlers/error-handler.handler.ts
1520
+ var import_path10 = __toESM(require("path"));
1521
+ var import_fs_extra9 = __toESM(require("fs-extra"));
1522
+ async function injectErrorHandler(projectRoot, srcDir) {
1523
+ const appPath = import_path10.default.join(projectRoot, srcDir, "app.ts");
1524
+ if (!import_fs_extra9.default.existsSync(appPath)) {
1525
+ return false;
1526
+ }
1527
+ let content = await import_fs_extra9.default.readFile(appPath, "utf-8");
1528
+ const errorImport = `import { errorHandler, notFoundHandler } from "./middleware/error-handler";`;
1529
+ const hasErrorImport = content.includes(errorImport);
1530
+ const hasNotFoundUse = /app\.use\(\s*notFoundHandler\s*\)/.test(content);
1531
+ const hasErrorUse = /app\.use\(\s*errorHandler\s*\)/.test(content);
1532
+ let modified = false;
1533
+ let importInserted = hasErrorImport;
1534
+ if (!hasErrorImport) {
1535
+ const importRegex = /^import .+ from .+;?\s*$/gm;
1536
+ let lastImportIndex = 0;
1537
+ let match;
1538
+ while ((match = importRegex.exec(content)) !== null) {
1539
+ lastImportIndex = match.index + match[0].length;
1540
+ }
1541
+ if (lastImportIndex > 0) {
1542
+ content = content.slice(0, lastImportIndex) + `
1543
+ ${errorImport}` + content.slice(lastImportIndex);
1544
+ modified = true;
1545
+ importInserted = true;
1546
+ }
1547
+ }
1548
+ let setupInserted = hasNotFoundUse && hasErrorUse;
1549
+ if (!setupInserted) {
1550
+ const setupLines = [];
1551
+ if (!hasNotFoundUse) {
1552
+ setupLines.push("app.use(notFoundHandler);");
1553
+ }
1554
+ if (!hasErrorUse) {
1555
+ setupLines.push("app.use(errorHandler);");
1556
+ }
1557
+ const errorSetup = `
1558
+ // Error handling (must be last)
1559
+ ${setupLines.join("\n")}
1560
+ `;
1561
+ const exportMatch = content.match(/export default app;?\s*$/m);
1562
+ if (exportMatch && exportMatch.index !== void 0) {
1563
+ content = content.slice(0, exportMatch.index) + errorSetup + "\n" + content.slice(exportMatch.index);
1564
+ modified = true;
1565
+ setupInserted = true;
1566
+ }
1567
+ }
1568
+ if (modified) {
1569
+ await import_fs_extra9.default.writeFile(appPath, content);
1570
+ }
1571
+ return importInserted && setupInserted;
1572
+ }
1573
+
1574
+ // src/handlers/rate-limiter.handler.ts
1575
+ var import_path11 = __toESM(require("path"));
1576
+ var import_fs_extra10 = __toESM(require("fs-extra"));
1577
+ async function injectRateLimiter(projectRoot, srcDir) {
1578
+ const appPath = import_path11.default.join(projectRoot, srcDir, "app.ts");
1579
+ if (!import_fs_extra10.default.existsSync(appPath)) {
1580
+ return false;
1581
+ }
1582
+ let content = await import_fs_extra10.default.readFile(appPath, "utf-8");
1583
+ const rateLimiterImport = `import { rateLimiter } from "./middleware/rate-limiter";`;
1584
+ const hasImport = content.includes(rateLimiterImport);
1585
+ const hasUse = /app\.use\(\s*rateLimiter\s*\)/.test(content);
1586
+ if (hasImport && hasUse) {
1587
+ return true;
1588
+ }
1589
+ let modified = false;
1590
+ if (!hasImport) {
1591
+ const importRegex = /^import .+ from .+;?\s*$/gm;
1592
+ let lastImportIndex = 0;
1593
+ let match;
1594
+ while ((match = importRegex.exec(content)) !== null) {
1595
+ lastImportIndex = match.index + match[0].length;
1596
+ }
1597
+ if (lastImportIndex > 0) {
1598
+ content = content.slice(0, lastImportIndex) + `
1599
+ ${rateLimiterImport}` + content.slice(lastImportIndex);
1600
+ modified = true;
1601
+ }
1602
+ }
1603
+ if (!hasUse) {
1604
+ const jsonMiddleware = /^(\s*app\.use\(\s*express\.json\(\)\s*\);?\s*)$/m;
1605
+ const jsonMatch = content.match(jsonMiddleware);
1606
+ if (jsonMatch && jsonMatch.index !== void 0) {
1607
+ const insertAt = jsonMatch.index + jsonMatch[0].length;
1608
+ content = content.slice(0, insertAt) + `
1609
+ app.use(rateLimiter);` + content.slice(insertAt);
1610
+ modified = true;
1611
+ } else {
1612
+ const routeMount = /^\s*app\.use\(\s*["']\/api["']/m;
1613
+ const routeMatch = content.match(routeMount);
1614
+ if (routeMatch && routeMatch.index !== void 0) {
1615
+ content = content.slice(0, routeMatch.index) + `app.use(rateLimiter);
1616
+ ` + content.slice(routeMatch.index);
1617
+ modified = true;
1618
+ }
1619
+ }
1620
+ }
1621
+ if (modified) {
1622
+ await import_fs_extra10.default.writeFile(appPath, content);
1623
+ }
1222
1624
  return true;
1223
1625
  }
1626
+
1627
+ // src/commands/add.ts
1628
+ function resolvePackageManager(projectRoot) {
1629
+ if (import_fs_extra11.default.existsSync(import_path12.default.join(projectRoot, "pnpm-lock.yaml"))) {
1630
+ return "pnpm";
1631
+ }
1632
+ if (import_fs_extra11.default.existsSync(import_path12.default.join(projectRoot, "bun.lockb")) || import_fs_extra11.default.existsSync(import_path12.default.join(projectRoot, "bun.lock"))) {
1633
+ return "bun";
1634
+ }
1635
+ if (import_fs_extra11.default.existsSync(import_path12.default.join(projectRoot, "yarn.lock"))) {
1636
+ return "yarn";
1637
+ }
1638
+ return "npm";
1639
+ }
1640
+ function getModuleDocsPath(moduleName) {
1641
+ if (isDatabaseModule(moduleName)) {
1642
+ return "database";
1643
+ }
1644
+ return moduleName;
1645
+ }
1646
+ async function hasEnvVariable(projectRoot, key) {
1647
+ const envPath = import_path12.default.join(projectRoot, ".env");
1648
+ if (!await import_fs_extra11.default.pathExists(envPath)) {
1649
+ return false;
1650
+ }
1651
+ const content = await import_fs_extra11.default.readFile(envPath, "utf-8");
1652
+ const pattern = new RegExp(`^${escapeRegex(key)}=`, "m");
1653
+ return pattern.test(content);
1654
+ }
1655
+ async function isLikelyEmptyDirectory(cwd) {
1656
+ const entries = await import_fs_extra11.default.readdir(cwd);
1657
+ const ignored = /* @__PURE__ */ new Set([".ds_store", "thumbs.db"]);
1658
+ return entries.filter((entry) => !ignored.has(entry.toLowerCase())).length === 0;
1659
+ }
1224
1660
  var add = async (moduleName, options = {}) => {
1225
1661
  const projectRoot = process.cwd();
1226
1662
  const projectConfig = await readZuroConfig(projectRoot);
@@ -1247,182 +1683,26 @@ var add = async (moduleName, options = {}) => {
1247
1683
  let usedDefaultSmtp = false;
1248
1684
  let mailerProvider = "smtp";
1249
1685
  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);
1686
+ if (resolvedModuleName === "database" || isDatabaseModule(resolvedModuleName)) {
1687
+ const result = await promptDatabaseConfig(resolvedModuleName, projectRoot, srcDir);
1688
+ if (!result) return;
1689
+ resolvedModuleName = result.resolvedModuleName;
1690
+ customDbUrl = result.customDbUrl;
1691
+ usedDefaultDbUrl = result.usedDefaultDbUrl;
1692
+ databaseBackupPath = result.databaseBackupPath;
1309
1693
  }
1310
1694
  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
- }
1695
+ const result = await promptMailerConfig();
1696
+ if (!result) return;
1697
+ mailerProvider = result.mailerProvider;
1698
+ customSmtpVars = result.customSmtpVars;
1699
+ usedDefaultSmtp = result.usedDefaultSmtp;
1406
1700
  }
1407
1701
  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
- }
1702
+ const result = await promptAuthConfig(projectRoot, srcDir, options);
1703
+ if (!result) return;
1704
+ shouldInstallDocsForAuth = result.shouldInstallDocsForAuth;
1705
+ authDatabaseDialect = result.authDatabaseDialect;
1426
1706
  }
1427
1707
  const pm = resolvePackageManager(projectRoot);
1428
1708
  const spinner = (0, import_ora2.default)(`Checking registry for ${resolvedModuleName}...`).start();
@@ -1438,7 +1718,7 @@ var add = async (moduleName, options = {}) => {
1438
1718
  spinner.fail(`Module '${resolvedModuleName}' not found.`);
1439
1719
  return;
1440
1720
  }
1441
- spinner.succeed(`Found module: ${import_chalk4.default.cyan(resolvedModuleName)}`);
1721
+ spinner.succeed(`Found module: ${import_chalk7.default.cyan(resolvedModuleName)}`);
1442
1722
  const moduleDeps = module2.moduleDependencies || [];
1443
1723
  currentStep = "module dependency resolution";
1444
1724
  await resolveDependencies(moduleDeps, projectRoot);
@@ -1460,9 +1740,6 @@ var add = async (moduleName, options = {}) => {
1460
1740
  spinner.succeed("Dependencies installed");
1461
1741
  currentStep = "module scaffolding";
1462
1742
  spinner.start("Scaffolding files...");
1463
- if (resolvedModuleName === "auth") {
1464
- authDatabaseDialect = await detectInstalledDatabaseDialect(projectRoot, srcDir);
1465
- }
1466
1743
  for (const file of module2.files) {
1467
1744
  let fetchPath = file.path;
1468
1745
  let expectedSha256 = file.sha256;
@@ -1490,10 +1767,10 @@ var add = async (moduleName, options = {}) => {
1490
1767
  );
1491
1768
  }
1492
1769
  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);
1770
+ await import_fs_extra11.default.ensureDir(import_path12.default.dirname(targetPath));
1771
+ await import_fs_extra11.default.writeFile(targetPath, content);
1495
1772
  }
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");
1773
+ const schemaExports = module2.files.map((file) => file.target.replace(/\\/g, "/")).filter((target) => /^db\/schema\/[^/]+\.ts$/.test(target)).map((target) => import_path12.default.posix.basename(target, ".ts")).filter((name) => name !== "index");
1497
1774
  for (const schemaFileName of schemaExports) {
1498
1775
  await ensureSchemaExport(projectRoot, srcDir, schemaFileName);
1499
1776
  }
@@ -1526,6 +1803,15 @@ var add = async (moduleName, options = {}) => {
1526
1803
  spinner.warn("Could not find app.ts - error handler needs manual setup");
1527
1804
  }
1528
1805
  }
1806
+ if (resolvedModuleName === "rate-limiter") {
1807
+ spinner.start("Configuring rate limiter in app.ts...");
1808
+ const injected = await injectRateLimiter(projectRoot, srcDir);
1809
+ if (injected) {
1810
+ spinner.succeed("Rate limiter configured in app.ts");
1811
+ } else {
1812
+ spinner.warn("Could not find app.ts - rate limiter needs manual setup");
1813
+ }
1814
+ }
1529
1815
  if (resolvedModuleName === "docs") {
1530
1816
  spinner.start("Configuring docs routes...");
1531
1817
  const injected = await injectDocsRoutes(projectRoot, srcDir);
@@ -1573,57 +1859,35 @@ var add = async (moduleName, options = {}) => {
1573
1859
  await updateEnvSchema(projectRoot, srcDir, envConfig.schemaFields);
1574
1860
  spinner.succeed("Environment configured");
1575
1861
  }
1576
- console.log(import_chalk4.default.green(`
1862
+ console.log(import_chalk7.default.green(`
1577
1863
  \u2714 ${resolvedModuleName} added successfully!
1578
1864
  `));
1579
- if (databaseBackupPath) {
1580
- console.log(import_chalk4.default.blue(`\u2139 Backup created at: ${databaseBackupPath}
1581
- `));
1582
- }
1583
1865
  const docsPath = getModuleDocsPath(resolvedModuleName);
1584
1866
  const docsUrl = `https://zuro-cli.devbybriyan.com/docs/${docsPath}`;
1585
- console.log(import_chalk4.default.blue(`\u2139 Docs: ${docsUrl}`));
1867
+ console.log(import_chalk7.default.blue(`\u2139 Docs: ${docsUrl}`));
1586
1868
  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"));
1869
+ printDatabaseHints(resolvedModuleName, customDbUrl, usedDefaultDbUrl, databaseBackupPath);
1596
1870
  }
1597
1871
  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"));
1872
+ printAuthHints(generatedAuthSecret);
1604
1873
  }
1605
1874
  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
- }
1875
+ printMailerHints(usedDefaultSmtp);
1611
1876
  }
1612
1877
  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"));
1878
+ printDocsHints();
1615
1879
  }
1616
1880
  if (resolvedModuleName === "auth" && shouldInstallDocsForAuth) {
1617
- console.log(import_chalk4.default.blue("\n\u2139 Installing API docs module..."));
1881
+ console.log(import_chalk7.default.blue("\n\u2139 Installing API docs module..."));
1618
1882
  await add("docs", { yes: true });
1619
1883
  }
1620
1884
  } catch (error) {
1621
- spinner.fail(import_chalk4.default.red(`Failed during ${currentStep}.`));
1885
+ spinner.fail(import_chalk7.default.red(`Failed during ${currentStep}.`));
1622
1886
  const errorMessage = error instanceof Error ? error.message : String(error);
1623
- console.error(import_chalk4.default.red(errorMessage));
1887
+ console.error(import_chalk7.default.red(errorMessage));
1624
1888
  console.log(`
1625
- ${import_chalk4.default.bold("Retry:")}`);
1626
- console.log(import_chalk4.default.cyan(` npx zuro-cli add ${resolvedModuleName}`));
1889
+ ${import_chalk7.default.bold("Retry:")}`);
1890
+ console.log(import_chalk7.default.cyan(` npx zuro-cli add ${resolvedModuleName}`));
1627
1891
  }
1628
1892
  };
1629
1893