zuro-cli 0.0.2-beta.12 → 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,6 +851,62 @@ bun.lockb
496
851
  await import_fs_extra4.default.writeFile(prettierIgnorePath, ignoreContent);
497
852
  }
498
853
  }
854
+ async function setupGitignore(targetDir) {
855
+ const gitignorePath = import_path4.default.join(targetDir, ".gitignore");
856
+ if (await import_fs_extra4.default.pathExists(gitignorePath)) {
857
+ return;
858
+ }
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
+ }
499
910
  async function init() {
500
911
  const cwd = process.cwd();
501
912
  const isExistingProject = await import_fs_extra4.default.pathExists(import_path4.default.join(cwd, "package.json"));
@@ -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
@@ -690,7 +1108,9 @@ var BLOCK_SIGNATURES = {
690
1108
  "error-handler": "lib/errors.ts",
691
1109
  logger: "lib/logger.ts",
692
1110
  auth: "lib/auth.ts",
693
- mailer: "lib/mailer.ts"
1111
+ mailer: "lib/mailer.ts",
1112
+ docs: "lib/openapi.ts",
1113
+ "rate-limiter": "middleware/rate-limiter.ts"
694
1114
  };
695
1115
  var resolveDependencies = async (moduleDependencies, cwd) => {
696
1116
  if (!moduleDependencies || moduleDependencies.length === 0) {
@@ -721,12 +1141,8 @@ var resolveDependencies = async (moduleDependencies, cwd) => {
721
1141
  }
722
1142
  };
723
1143
 
724
- // src/commands/add.ts
725
- var import_chalk4 = __toESM(require("chalk"));
726
- var DEFAULT_DATABASE_URLS = {
727
- "database-pg": "postgresql://postgres@localhost:5432/mydb",
728
- "database-mysql": "mysql://root@localhost:3306/mydb"
729
- };
1144
+ // src/utils/paths.ts
1145
+ var import_path6 = __toESM(require("path"));
730
1146
  function resolveSafeTargetPath2(projectRoot, srcDir, file) {
731
1147
  const targetPath = import_path6.default.resolve(projectRoot, srcDir, file.target);
732
1148
  const normalizedRoot = import_path6.default.resolve(projectRoot);
@@ -735,155 +1151,380 @@ function resolveSafeTargetPath2(projectRoot, srcDir, file) {
735
1151
  }
736
1152
  return targetPath;
737
1153
  }
738
- function resolvePackageManager(projectRoot) {
739
- if (import_fs_extra6.default.existsSync(import_path6.default.join(projectRoot, "pnpm-lock.yaml"))) {
740
- return "pnpm";
741
- }
742
- 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"))) {
743
- return "bun";
744
- }
745
- if (import_fs_extra6.default.existsSync(import_path6.default.join(projectRoot, "yarn.lock"))) {
746
- return "yarn";
747
- }
748
- return "npm";
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();
1166
+ async function isAuthModuleInstalled(projectRoot, srcDir) {
1167
+ return await import_fs_extra8.default.pathExists(import_path9.default.join(projectRoot, srcDir, "lib", "auth.ts"));
749
1168
  }
750
- function parseDatabaseDialect(value) {
751
- const normalized = value?.trim().toLowerCase();
752
- if (!normalized) {
753
- return null;
1169
+ async function injectAuthRoutes(projectRoot, srcDir) {
1170
+ const appPath = import_path9.default.join(projectRoot, srcDir, "app.ts");
1171
+ if (!await import_fs_extra8.default.pathExists(appPath)) {
1172
+ return false;
754
1173
  }
755
- if (normalized === "pg" || normalized === "postgres" || normalized === "postgresql" || normalized === "database-pg") {
756
- return "database-pg";
1174
+ let appContent = await import_fs_extra8.default.readFile(appPath, "utf-8");
1175
+ const authHandlerImport = `import { toNodeHandler } from "better-auth/node";`;
1176
+ const authImport = `import { auth } from "./lib/auth";`;
1177
+ const routeIndexUserImport = `import userRoutes from "./user.routes";`;
1178
+ const appUserImport = `import userRoutes from "./routes/user.routes";`;
1179
+ let appModified = false;
1180
+ for (const importLine of [authHandlerImport, authImport]) {
1181
+ const next = appendImport(appContent, importLine);
1182
+ if (!next.inserted) {
1183
+ return false;
1184
+ }
1185
+ if (next.source !== appContent) {
1186
+ appContent = next.source;
1187
+ appModified = true;
1188
+ }
757
1189
  }
758
- if (normalized === "mysql" || normalized === "database-mysql") {
759
- return "database-mysql";
760
- }
761
- return null;
762
- }
763
- function isDatabaseModule(moduleName) {
764
- return moduleName === "database-pg" || moduleName === "database-mysql";
765
- }
766
- function validateDatabaseUrl(rawUrl, moduleName) {
767
- const dbUrl = rawUrl.trim();
768
- if (!dbUrl) {
769
- throw new Error("Database URL cannot be empty.");
770
- }
771
- let parsed;
772
- try {
773
- parsed = new URL(dbUrl);
774
- } catch {
775
- throw new Error(`Invalid database URL: '${dbUrl}'.`);
1190
+ const hasAuthMount = /toNodeHandler\(\s*auth\s*\)/.test(appContent) && /\/api\/auth/.test(appContent);
1191
+ if (!hasAuthMount) {
1192
+ const authMountLine = "app.all(/^\\/api\\/auth(?:\\/.*)?$/, toNodeHandler(auth));\n";
1193
+ const jsonIndex = appContent.search(/^\s*app\.use\(\s*express\.json\(\)\s*\);\s*$/m);
1194
+ let insertionIndex = jsonIndex;
1195
+ if (insertionIndex < 0) {
1196
+ const healthIndex = appContent.search(/^\s*app\.get\(\s*["']\/health["']\s*,/m);
1197
+ insertionIndex = healthIndex;
1198
+ }
1199
+ if (insertionIndex < 0) {
1200
+ const exportMatch = appContent.match(/export default app;?\s*$/m);
1201
+ insertionIndex = exportMatch?.index ?? -1;
1202
+ }
1203
+ if (insertionIndex < 0) {
1204
+ return false;
1205
+ }
1206
+ appContent = appContent.slice(0, insertionIndex) + authMountLine + appContent.slice(insertionIndex);
1207
+ appModified = true;
776
1208
  }
777
- const protocol = parsed.protocol.toLowerCase();
778
- if (moduleName === "database-pg" && protocol !== "postgresql:" && protocol !== "postgres:") {
779
- throw new Error("PostgreSQL URL must start with postgres:// or postgresql://");
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");
1212
+ let routeModified = false;
1213
+ const userImportResult = appendImport(routeContent, routeIndexUserImport);
1214
+ if (!userImportResult.inserted) {
1215
+ return false;
1216
+ }
1217
+ if (userImportResult.source !== routeContent) {
1218
+ routeContent = userImportResult.source;
1219
+ routeModified = true;
1220
+ }
1221
+ const hasUserRoute = /rootRouter\.use\(\s*["']\/users["']\s*,\s*userRoutes\s*\)/.test(routeContent);
1222
+ if (!hasUserRoute) {
1223
+ const routeSetup = `
1224
+ // User routes
1225
+ rootRouter.use("/users", userRoutes);
1226
+ `;
1227
+ const exportMatch = routeContent.match(/export default rootRouter;?\s*$/m);
1228
+ if (!exportMatch || exportMatch.index === void 0) {
1229
+ return false;
1230
+ }
1231
+ routeContent = routeContent.slice(0, exportMatch.index) + routeSetup + "\n" + routeContent.slice(exportMatch.index);
1232
+ routeModified = true;
1233
+ }
1234
+ if (routeModified) {
1235
+ await import_fs_extra8.default.writeFile(routeIndexPath, routeContent);
1236
+ }
1237
+ } else {
1238
+ const hasUserRoute = /app\.use\(\s*["']\/api\/users["']\s*,\s*userRoutes\s*\)/.test(appContent);
1239
+ if (!hasUserRoute) {
1240
+ const exportMatch = appContent.match(/export default app;?\s*$/m);
1241
+ if (!exportMatch || exportMatch.index === void 0) {
1242
+ return false;
1243
+ }
1244
+ const routeSetup = `
1245
+ // User routes
1246
+ app.use("/api/users", userRoutes);
1247
+ `;
1248
+ appContent = appContent.slice(0, exportMatch.index) + routeSetup + "\n" + appContent.slice(exportMatch.index);
1249
+ appModified = true;
1250
+ }
1251
+ const userImportResult = appendImport(appContent, appUserImport);
1252
+ if (!userImportResult.inserted) {
1253
+ return false;
1254
+ }
1255
+ if (userImportResult.source !== appContent) {
1256
+ appContent = userImportResult.source;
1257
+ appModified = true;
1258
+ }
780
1259
  }
781
- if (moduleName === "database-mysql" && protocol !== "mysql:") {
782
- throw new Error("MySQL URL must start with mysql://");
1260
+ if (appModified) {
1261
+ await import_fs_extra8.default.writeFile(appPath, appContent);
783
1262
  }
784
- return dbUrl;
1263
+ return true;
785
1264
  }
786
- async function detectInstalledDatabaseDialect(projectRoot, srcDir) {
787
- const dbIndexPath = import_path6.default.join(projectRoot, srcDir, "db", "index.ts");
788
- if (!import_fs_extra6.default.existsSync(dbIndexPath)) {
789
- return null;
1265
+ async function injectAuthDocs(projectRoot, srcDir) {
1266
+ const openApiPath = import_path9.default.join(projectRoot, srcDir, "lib", "openapi.ts");
1267
+ if (!await import_fs_extra8.default.pathExists(openApiPath)) {
1268
+ return false;
790
1269
  }
791
- const content = await import_fs_extra6.default.readFile(dbIndexPath, "utf-8");
792
- if (content.includes("drizzle-orm/node-postgres") || content.includes(`from "pg"`)) {
793
- return "database-pg";
1270
+ const authMarker = "// ZURO_AUTH_DOCS";
1271
+ let content = await import_fs_extra8.default.readFile(openApiPath, "utf-8");
1272
+ if (content.includes(authMarker)) {
1273
+ return true;
794
1274
  }
795
- if (content.includes("drizzle-orm/mysql2") || content.includes(`from "mysql2`)) {
796
- return "database-mysql";
1275
+ const moduleDocsEndMarker = "// ZURO_DOCS_MODULES_END";
1276
+ if (!content.includes(moduleDocsEndMarker)) {
1277
+ return false;
797
1278
  }
798
- return null;
1279
+ const authBlock = `
1280
+ const authSignUpSchema = z.object({
1281
+ email: z.string().email().openapi({ example: "dev@company.com" }),
1282
+ password: z.string().min(8).openapi({ example: "strong-password" }),
1283
+ name: z.string().min(1).optional().openapi({ example: "Dev User" }),
1284
+ });
1285
+
1286
+ const authSignInSchema = z.object({
1287
+ email: z.string().email().openapi({ example: "dev@company.com" }),
1288
+ password: z.string().min(8).openapi({ example: "strong-password" }),
1289
+ });
1290
+
1291
+ const authUserSchema = z.object({
1292
+ id: z.string().openapi({ example: "user_123" }),
1293
+ email: z.string().email().openapi({ example: "dev@company.com" }),
1294
+ name: z.string().nullable().openapi({ example: "Dev User" }),
1295
+ });
1296
+
1297
+ ${authMarker}
1298
+ registry.registerPath({
1299
+ method: "post",
1300
+ path: "/api/auth/sign-up/email",
1301
+ tags: ["Auth"],
1302
+ summary: "Register using email and password",
1303
+ request: {
1304
+ body: {
1305
+ content: {
1306
+ "application/json": {
1307
+ schema: authSignUpSchema,
1308
+ },
1309
+ },
1310
+ },
1311
+ },
1312
+ responses: {
1313
+ 200: { description: "Registration successful" },
1314
+ },
1315
+ });
1316
+
1317
+ registry.registerPath({
1318
+ method: "post",
1319
+ path: "/api/auth/sign-in/email",
1320
+ tags: ["Auth"],
1321
+ summary: "Sign in using email and password",
1322
+ request: {
1323
+ body: {
1324
+ content: {
1325
+ "application/json": {
1326
+ schema: authSignInSchema,
1327
+ },
1328
+ },
1329
+ },
1330
+ },
1331
+ responses: {
1332
+ 200: { description: "Sign in successful" },
1333
+ 401: { description: "Invalid credentials" },
1334
+ },
1335
+ });
1336
+
1337
+ registry.registerPath({
1338
+ method: "post",
1339
+ path: "/api/auth/sign-out",
1340
+ tags: ["Auth"],
1341
+ summary: "Sign out current user",
1342
+ responses: {
1343
+ 200: { description: "Sign out successful" },
1344
+ },
1345
+ });
1346
+
1347
+ registry.registerPath({
1348
+ method: "get",
1349
+ path: "/api/users/me",
1350
+ tags: ["Auth"],
1351
+ summary: "Get current authenticated user",
1352
+ security: [{ bearerAuth: [] }],
1353
+ responses: {
1354
+ 200: {
1355
+ description: "Current user",
1356
+ content: {
1357
+ "application/json": {
1358
+ schema: z.object({ user: authUserSchema }),
1359
+ },
1360
+ },
1361
+ },
1362
+ 401: { description: "Not authenticated" },
1363
+ },
1364
+ });
1365
+ `;
1366
+ content = content.replace(moduleDocsEndMarker, `${authBlock}
1367
+ ${moduleDocsEndMarker}`);
1368
+ await import_fs_extra8.default.writeFile(openApiPath, content);
1369
+ return true;
799
1370
  }
800
- async function backupDatabaseFiles(projectRoot, srcDir) {
801
- const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
802
- const backupRoot = import_path6.default.join(projectRoot, ".zuro", "backups", `database-${timestamp}`);
803
- const candidates = [
804
- import_path6.default.join(projectRoot, srcDir, "db", "index.ts"),
805
- import_path6.default.join(projectRoot, "drizzle.config.ts")
806
- ];
807
- let copied = false;
808
- for (const filePath of candidates) {
809
- if (!import_fs_extra6.default.existsSync(filePath)) {
810
- continue;
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;
811
1390
  }
812
- const relativePath = import_path6.default.relative(projectRoot, filePath);
813
- const backupPath = import_path6.default.join(backupRoot, relativePath);
814
- await import_fs_extra6.default.ensureDir(import_path6.default.dirname(backupPath));
815
- await import_fs_extra6.default.copyFile(filePath, backupPath);
816
- copied = true;
817
1391
  }
818
- return copied ? backupRoot : null;
819
- }
820
- function databaseLabel(moduleName) {
821
- return moduleName === "database-pg" ? "PostgreSQL" : "MySQL";
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 };
822
1395
  }
823
- function getDatabaseSetupHint(moduleName, dbUrl) {
824
- try {
825
- const parsed = new URL(dbUrl);
826
- const dbName = parsed.pathname.replace(/^\/+/, "") || "mydb";
827
- if (moduleName === "database-pg") {
828
- return `createdb ${dbName}`;
829
- }
830
- return `mysql -e "CREATE DATABASE IF NOT EXISTS ${dbName};"`;
831
- } catch {
832
- return moduleName === "database-pg" ? "createdb <database_name>" : `mysql -e "CREATE DATABASE IF NOT EXISTS <database_name>;"`;
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."));
833
1401
  }
1402
+ console.log(import_chalk5.default.yellow("\u2139 Run migrations: npx drizzle-kit generate && npx drizzle-kit migrate"));
834
1403
  }
835
- function getModuleDocsPath(moduleName) {
836
- if (isDatabaseModule(moduleName)) {
837
- return "database";
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;
838
1421
  }
839
- return moduleName;
840
- }
841
- function escapeRegex(value) {
842
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
843
- }
844
- async function hasEnvVariable(projectRoot, key) {
845
- const envPath = import_path6.default.join(projectRoot, ".env");
846
- if (!await import_fs_extra6.default.pathExists(envPath)) {
847
- return false;
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
+ }
848
1505
  }
849
- const content = await import_fs_extra6.default.readFile(envPath, "utf-8");
850
- const pattern = new RegExp(`^${escapeRegex(key)}=`, "m");
851
- return pattern.test(content);
852
- }
853
- async function isLikelyEmptyDirectory(cwd) {
854
- const entries = await import_fs_extra6.default.readdir(cwd);
855
- const ignored = /* @__PURE__ */ new Set([".ds_store", "thumbs.db"]);
856
- return entries.filter((entry) => !ignored.has(entry.toLowerCase())).length === 0;
1506
+ return { mailerProvider, customSmtpVars, usedDefaultSmtp };
857
1507
  }
858
- async function ensureSchemaExport(projectRoot, srcDir, schemaFileName) {
859
- const schemaIndexPath = import_path6.default.join(projectRoot, srcDir, "db", "schema", "index.ts");
860
- if (!await import_fs_extra6.default.pathExists(schemaIndexPath)) {
861
- return;
862
- }
863
- const exportLine = `export * from "./${schemaFileName}";`;
864
- const content = await import_fs_extra6.default.readFile(schemaIndexPath, "utf-8");
865
- const normalized = content.replace(/\r\n/g, "\n");
866
- const exportPattern = new RegExp(
867
- `^\\s*export\\s*\\*\\s*from\\s*["']\\./${escapeRegex(schemaFileName)}["'];?\\s*$`,
868
- "m"
869
- );
870
- if (exportPattern.test(normalized)) {
871
- return;
872
- }
873
- let next = normalized.replace(/^\s*export\s*\{\s*\};?\s*$/m, "").trimEnd();
874
- if (next.length > 0) {
875
- next += "\n\n";
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."));
876
1513
  }
877
- next += `${exportLine}
878
- `;
879
- await import_fs_extra6.default.writeFile(schemaIndexPath, next);
880
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"));
881
1522
  async function injectErrorHandler(projectRoot, srcDir) {
882
- const appPath = import_path6.default.join(projectRoot, srcDir, "app.ts");
883
- if (!import_fs_extra6.default.existsSync(appPath)) {
1523
+ const appPath = import_path10.default.join(projectRoot, srcDir, "app.ts");
1524
+ if (!import_fs_extra9.default.existsSync(appPath)) {
884
1525
  return false;
885
1526
  }
886
- let content = await import_fs_extra6.default.readFile(appPath, "utf-8");
1527
+ let content = await import_fs_extra9.default.readFile(appPath, "utf-8");
887
1528
  const errorImport = `import { errorHandler, notFoundHandler } from "./middleware/error-handler";`;
888
1529
  const hasErrorImport = content.includes(errorImport);
889
1530
  const hasNotFoundUse = /app\.use\(\s*notFoundHandler\s*\)/.test(content);
@@ -925,126 +1566,98 @@ ${setupLines.join("\n")}
925
1566
  }
926
1567
  }
927
1568
  if (modified) {
928
- await import_fs_extra6.default.writeFile(appPath, content);
1569
+ await import_fs_extra9.default.writeFile(appPath, content);
929
1570
  }
930
1571
  return importInserted && setupInserted;
931
1572
  }
932
- async function injectAuthRoutes(projectRoot, srcDir) {
933
- const appPath = import_path6.default.join(projectRoot, srcDir, "app.ts");
934
- if (!await import_fs_extra6.default.pathExists(appPath)) {
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)) {
935
1580
  return false;
936
1581
  }
937
- let appContent = await import_fs_extra6.default.readFile(appPath, "utf-8");
938
- const authHandlerImport = `import { toNodeHandler } from "better-auth/node";`;
939
- const authImport = `import { auth } from "./lib/auth";`;
940
- const routeIndexUserImport = `import userRoutes from "./user.routes";`;
941
- const appUserImport = `import userRoutes from "./routes/user.routes";`;
942
- let appModified = false;
943
- const appendImport = (source, line) => {
944
- if (source.includes(line)) {
945
- return { source, inserted: true };
946
- }
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) {
947
1591
  const importRegex = /^import .+ from .+;?\s*$/gm;
948
1592
  let lastImportIndex = 0;
949
1593
  let match;
950
- while ((match = importRegex.exec(source)) !== null) {
1594
+ while ((match = importRegex.exec(content)) !== null) {
951
1595
  lastImportIndex = match.index + match[0].length;
952
1596
  }
953
- if (lastImportIndex <= 0) {
954
- return { source, inserted: false };
955
- }
956
- return {
957
- source: source.slice(0, lastImportIndex) + `
958
- ${line}` + source.slice(lastImportIndex),
959
- inserted: true
960
- };
961
- };
962
- for (const importLine of [authHandlerImport, authImport]) {
963
- const next = appendImport(appContent, importLine);
964
- if (!next.inserted) {
965
- return false;
966
- }
967
- if (next.source !== appContent) {
968
- appContent = next.source;
969
- appModified = true;
970
- }
971
- }
972
- const hasAuthMount = /toNodeHandler\(\s*auth\s*\)/.test(appContent) && /\/api\/auth/.test(appContent);
973
- if (!hasAuthMount) {
974
- const authMountLine = "app.all(/^\\/api\\/auth(?:\\/.*)?$/, toNodeHandler(auth));\n";
975
- const jsonIndex = appContent.search(/^\s*app\.use\(\s*express\.json\(\)\s*\);\s*$/m);
976
- let insertionIndex = jsonIndex;
977
- if (insertionIndex < 0) {
978
- const healthIndex = appContent.search(/^\s*app\.get\(\s*["']\/health["']\s*,/m);
979
- insertionIndex = healthIndex;
980
- }
981
- if (insertionIndex < 0) {
982
- const exportMatch = appContent.match(/export default app;?\s*$/m);
983
- insertionIndex = exportMatch?.index ?? -1;
984
- }
985
- if (insertionIndex < 0) {
986
- return false;
1597
+ if (lastImportIndex > 0) {
1598
+ content = content.slice(0, lastImportIndex) + `
1599
+ ${rateLimiterImport}` + content.slice(lastImportIndex);
1600
+ modified = true;
987
1601
  }
988
- appContent = appContent.slice(0, insertionIndex) + authMountLine + appContent.slice(insertionIndex);
989
- appModified = true;
990
1602
  }
991
- const routeIndexPath = import_path6.default.join(projectRoot, srcDir, "routes", "index.ts");
992
- if (await import_fs_extra6.default.pathExists(routeIndexPath)) {
993
- let routeContent = await import_fs_extra6.default.readFile(routeIndexPath, "utf-8");
994
- let routeModified = false;
995
- const userImportResult = appendImport(routeContent, routeIndexUserImport);
996
- if (!userImportResult.inserted) {
997
- return false;
998
- }
999
- if (userImportResult.source !== routeContent) {
1000
- routeContent = userImportResult.source;
1001
- routeModified = true;
1002
- }
1003
- const hasUserRoute = /rootRouter\.use\(\s*["']\/users["']\s*,\s*userRoutes\s*\)/.test(routeContent);
1004
- if (!hasUserRoute) {
1005
- const routeSetup = `
1006
- // User routes
1007
- rootRouter.use("/users", userRoutes);
1008
- `;
1009
- const exportMatch = routeContent.match(/export default rootRouter;?\s*$/m);
1010
- if (!exportMatch || exportMatch.index === void 0) {
1011
- return false;
1012
- }
1013
- routeContent = routeContent.slice(0, exportMatch.index) + routeSetup + "\n" + routeContent.slice(exportMatch.index);
1014
- routeModified = true;
1015
- }
1016
- if (routeModified) {
1017
- await import_fs_extra6.default.writeFile(routeIndexPath, routeContent);
1018
- }
1019
- } else {
1020
- const hasUserRoute = /app\.use\(\s*["']\/api\/users["']\s*,\s*userRoutes\s*\)/.test(appContent);
1021
- if (!hasUserRoute) {
1022
- const exportMatch = appContent.match(/export default app;?\s*$/m);
1023
- if (!exportMatch || exportMatch.index === void 0) {
1024
- return false;
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;
1025
1618
  }
1026
- const routeSetup = `
1027
- // User routes
1028
- app.use("/api/users", userRoutes);
1029
- `;
1030
- appContent = appContent.slice(0, exportMatch.index) + routeSetup + "\n" + appContent.slice(exportMatch.index);
1031
- appModified = true;
1032
- }
1033
- const userImportResult = appendImport(appContent, appUserImport);
1034
- if (!userImportResult.inserted) {
1035
- return false;
1036
- }
1037
- if (userImportResult.source !== appContent) {
1038
- appContent = userImportResult.source;
1039
- appModified = true;
1040
1619
  }
1041
1620
  }
1042
- if (appModified) {
1043
- await import_fs_extra6.default.writeFile(appPath, appContent);
1621
+ if (modified) {
1622
+ await import_fs_extra10.default.writeFile(appPath, content);
1044
1623
  }
1045
1624
  return true;
1046
1625
  }
1047
- var add = async (moduleName) => {
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
+ }
1660
+ var add = async (moduleName, options = {}) => {
1048
1661
  const projectRoot = process.cwd();
1049
1662
  const projectConfig = await readZuroConfig(projectRoot);
1050
1663
  if (!projectConfig) {
@@ -1069,162 +1682,27 @@ var add = async (moduleName) => {
1069
1682
  let customSmtpVars;
1070
1683
  let usedDefaultSmtp = false;
1071
1684
  let mailerProvider = "smtp";
1072
- if (resolvedModuleName === "database") {
1073
- const variantResponse = await (0, import_prompts2.default)({
1074
- type: "select",
1075
- name: "variant",
1076
- message: "Which database dialect?",
1077
- choices: [
1078
- { title: "PostgreSQL", value: "database-pg" },
1079
- { title: "MySQL", value: "database-mysql" }
1080
- ]
1081
- });
1082
- if (!variantResponse.variant) {
1083
- console.log(import_chalk4.default.yellow("Operation cancelled."));
1084
- return;
1085
- }
1086
- resolvedModuleName = variantResponse.variant;
1087
- }
1088
- if (isDatabaseModule(resolvedModuleName)) {
1089
- const installedDialect = await detectInstalledDatabaseDialect(projectRoot, srcDir);
1090
- if (installedDialect && installedDialect !== resolvedModuleName) {
1091
- console.log(
1092
- import_chalk4.default.yellow(
1093
- `
1094
- \u26A0 Existing database setup detected: ${databaseLabel(installedDialect)}.`
1095
- )
1096
- );
1097
- console.log(
1098
- import_chalk4.default.yellow(
1099
- ` Switching to ${databaseLabel(resolvedModuleName)} will overwrite db files and drizzle config.
1100
- `
1101
- )
1102
- );
1103
- const switchResponse = await (0, import_prompts2.default)({
1104
- type: "confirm",
1105
- name: "proceed",
1106
- message: "Continue and switch database dialect?",
1107
- initial: false
1108
- });
1109
- if (!switchResponse.proceed) {
1110
- console.log(import_chalk4.default.yellow("Operation cancelled."));
1111
- return;
1112
- }
1113
- databaseBackupPath = await backupDatabaseFiles(projectRoot, srcDir);
1114
- }
1115
- const defaultUrl = DEFAULT_DATABASE_URLS[resolvedModuleName];
1116
- console.log(import_chalk4.default.dim(` Tip: Leave blank to use ${defaultUrl}
1117
- `));
1118
- const response = await (0, import_prompts2.default)({
1119
- type: "text",
1120
- name: "dbUrl",
1121
- message: "Database URL",
1122
- initial: ""
1123
- });
1124
- if (response.dbUrl === void 0) {
1125
- console.log(import_chalk4.default.yellow("Operation cancelled."));
1126
- return;
1127
- }
1128
- const enteredUrl = response.dbUrl?.trim() || "";
1129
- usedDefaultDbUrl = enteredUrl.length === 0;
1130
- customDbUrl = validateDatabaseUrl(enteredUrl || defaultUrl, resolvedModuleName);
1685
+ let shouldInstallDocsForAuth = false;
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;
1131
1693
  }
1132
1694
  if (resolvedModuleName === "mailer") {
1133
- const providerResponse = await (0, import_prompts2.default)({
1134
- type: "select",
1135
- name: "provider",
1136
- message: "Which email provider?",
1137
- choices: [
1138
- { title: "SMTP (Nodemailer)", description: "Gmail, Mailtrap, any SMTP server", value: "smtp" },
1139
- { title: "Resend", description: "API-based, easiest setup", value: "resend" }
1140
- ]
1141
- });
1142
- if (providerResponse.provider === void 0) {
1143
- console.log(import_chalk4.default.yellow("Operation cancelled."));
1144
- return;
1145
- }
1146
- mailerProvider = providerResponse.provider;
1147
- console.log(import_chalk4.default.dim(" Tip: Leave fields blank to use placeholder values and configure later\n"));
1148
- if (mailerProvider === "smtp") {
1149
- const smtpResponse = await (0, import_prompts2.default)([
1150
- {
1151
- type: "text",
1152
- name: "host",
1153
- message: "SMTP Host",
1154
- initial: ""
1155
- },
1156
- {
1157
- type: "text",
1158
- name: "port",
1159
- message: "SMTP Port",
1160
- initial: "587"
1161
- },
1162
- {
1163
- type: "text",
1164
- name: "user",
1165
- message: "SMTP User",
1166
- initial: ""
1167
- },
1168
- {
1169
- type: "password",
1170
- name: "pass",
1171
- message: "SMTP Password"
1172
- },
1173
- {
1174
- type: "text",
1175
- name: "from",
1176
- message: "Mail From address",
1177
- initial: ""
1178
- }
1179
- ]);
1180
- if (smtpResponse.host === void 0) {
1181
- console.log(import_chalk4.default.yellow("Operation cancelled."));
1182
- return;
1183
- }
1184
- const host = smtpResponse.host?.trim() || "";
1185
- const user = smtpResponse.user?.trim() || "";
1186
- const pass = smtpResponse.pass?.trim() || "";
1187
- const from = smtpResponse.from?.trim() || "";
1188
- const port = smtpResponse.port?.trim() || "587";
1189
- usedDefaultSmtp = !host && !user;
1190
- if (!usedDefaultSmtp) {
1191
- customSmtpVars = {
1192
- SMTP_HOST: host || "smtp.example.com",
1193
- SMTP_PORT: port,
1194
- SMTP_USER: user || "your-email@example.com",
1195
- SMTP_PASS: pass || "your-password",
1196
- MAIL_FROM: from || "noreply@example.com"
1197
- };
1198
- }
1199
- } else {
1200
- const resendResponse = await (0, import_prompts2.default)([
1201
- {
1202
- type: "text",
1203
- name: "apiKey",
1204
- message: "Resend API Key",
1205
- initial: ""
1206
- },
1207
- {
1208
- type: "text",
1209
- name: "from",
1210
- message: "Mail From address",
1211
- initial: ""
1212
- }
1213
- ]);
1214
- if (resendResponse.apiKey === void 0) {
1215
- console.log(import_chalk4.default.yellow("Operation cancelled."));
1216
- return;
1217
- }
1218
- const apiKey = resendResponse.apiKey?.trim() || "";
1219
- const from = resendResponse.from?.trim() || "";
1220
- usedDefaultSmtp = !apiKey;
1221
- if (!usedDefaultSmtp) {
1222
- customSmtpVars = {
1223
- RESEND_API_KEY: apiKey || "re_your_api_key",
1224
- MAIL_FROM: from || "onboarding@resend.dev"
1225
- };
1226
- }
1227
- }
1695
+ const result = await promptMailerConfig();
1696
+ if (!result) return;
1697
+ mailerProvider = result.mailerProvider;
1698
+ customSmtpVars = result.customSmtpVars;
1699
+ usedDefaultSmtp = result.usedDefaultSmtp;
1700
+ }
1701
+ if (resolvedModuleName === "auth") {
1702
+ const result = await promptAuthConfig(projectRoot, srcDir, options);
1703
+ if (!result) return;
1704
+ shouldInstallDocsForAuth = result.shouldInstallDocsForAuth;
1705
+ authDatabaseDialect = result.authDatabaseDialect;
1228
1706
  }
1229
1707
  const pm = resolvePackageManager(projectRoot);
1230
1708
  const spinner = (0, import_ora2.default)(`Checking registry for ${resolvedModuleName}...`).start();
@@ -1240,7 +1718,7 @@ var add = async (moduleName) => {
1240
1718
  spinner.fail(`Module '${resolvedModuleName}' not found.`);
1241
1719
  return;
1242
1720
  }
1243
- spinner.succeed(`Found module: ${import_chalk4.default.cyan(resolvedModuleName)}`);
1721
+ spinner.succeed(`Found module: ${import_chalk7.default.cyan(resolvedModuleName)}`);
1244
1722
  const moduleDeps = module2.moduleDependencies || [];
1245
1723
  currentStep = "module dependency resolution";
1246
1724
  await resolveDependencies(moduleDeps, projectRoot);
@@ -1262,9 +1740,6 @@ var add = async (moduleName) => {
1262
1740
  spinner.succeed("Dependencies installed");
1263
1741
  currentStep = "module scaffolding";
1264
1742
  spinner.start("Scaffolding files...");
1265
- if (resolvedModuleName === "auth") {
1266
- authDatabaseDialect = await detectInstalledDatabaseDialect(projectRoot, srcDir);
1267
- }
1268
1743
  for (const file of module2.files) {
1269
1744
  let fetchPath = file.path;
1270
1745
  let expectedSha256 = file.sha256;
@@ -1292,10 +1767,10 @@ var add = async (moduleName) => {
1292
1767
  );
1293
1768
  }
1294
1769
  const targetPath = resolveSafeTargetPath2(projectRoot, srcDir, file);
1295
- await import_fs_extra6.default.ensureDir(import_path6.default.dirname(targetPath));
1296
- 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);
1297
1772
  }
1298
- 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");
1299
1774
  for (const schemaFileName of schemaExports) {
1300
1775
  await ensureSchemaExport(projectRoot, srcDir, schemaFileName);
1301
1776
  }
@@ -1308,6 +1783,16 @@ var add = async (moduleName) => {
1308
1783
  } else {
1309
1784
  spinner.warn("Could not configure routes automatically");
1310
1785
  }
1786
+ const docsInstalled = await isDocsModuleInstalled(projectRoot, srcDir);
1787
+ if (docsInstalled) {
1788
+ spinner.start("Adding auth endpoints to API docs...");
1789
+ const authDocsInjected = await injectAuthDocs(projectRoot, srcDir);
1790
+ if (authDocsInjected) {
1791
+ spinner.succeed("Auth endpoints added to API docs");
1792
+ } else {
1793
+ spinner.warn("Could not update API docs automatically");
1794
+ }
1795
+ }
1311
1796
  }
1312
1797
  if (resolvedModuleName === "error-handler") {
1313
1798
  spinner.start("Configuring error handler in app.ts...");
@@ -1318,6 +1803,34 @@ var add = async (moduleName) => {
1318
1803
  spinner.warn("Could not find app.ts - error handler needs manual setup");
1319
1804
  }
1320
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
+ }
1815
+ if (resolvedModuleName === "docs") {
1816
+ spinner.start("Configuring docs routes...");
1817
+ const injected = await injectDocsRoutes(projectRoot, srcDir);
1818
+ if (injected) {
1819
+ spinner.succeed("Docs routes configured");
1820
+ } else {
1821
+ spinner.warn("Could not configure docs routes automatically");
1822
+ }
1823
+ const authInstalled = await isAuthModuleInstalled(projectRoot, srcDir);
1824
+ if (authInstalled) {
1825
+ spinner.start("Adding auth endpoints to API docs...");
1826
+ const authDocsInjected = await injectAuthDocs(projectRoot, srcDir);
1827
+ if (authDocsInjected) {
1828
+ spinner.succeed("Auth endpoints added to API docs");
1829
+ } else {
1830
+ spinner.warn("Could not update API docs automatically");
1831
+ }
1832
+ }
1833
+ }
1321
1834
  let envConfigKey = resolvedModuleName;
1322
1835
  if (resolvedModuleName === "mailer" && mailerProvider === "resend") {
1323
1836
  envConfigKey = "mailer-resend";
@@ -1346,49 +1859,35 @@ var add = async (moduleName) => {
1346
1859
  await updateEnvSchema(projectRoot, srcDir, envConfig.schemaFields);
1347
1860
  spinner.succeed("Environment configured");
1348
1861
  }
1349
- console.log(import_chalk4.default.green(`
1862
+ console.log(import_chalk7.default.green(`
1350
1863
  \u2714 ${resolvedModuleName} added successfully!
1351
1864
  `));
1352
- if (databaseBackupPath) {
1353
- console.log(import_chalk4.default.blue(`\u2139 Backup created at: ${databaseBackupPath}
1354
- `));
1355
- }
1356
1865
  const docsPath = getModuleDocsPath(resolvedModuleName);
1357
1866
  const docsUrl = `https://zuro-cli.devbybriyan.com/docs/${docsPath}`;
1358
- console.log(import_chalk4.default.blue(`\u2139 Docs: ${docsUrl}`));
1867
+ console.log(import_chalk7.default.blue(`\u2139 Docs: ${docsUrl}`));
1359
1868
  if (isDatabaseModule(resolvedModuleName)) {
1360
- if (usedDefaultDbUrl) {
1361
- console.log(import_chalk4.default.yellow("\u2139 Review DATABASE_URL in .env if your local DB config differs."));
1362
- }
1363
- const setupHint = getDatabaseSetupHint(
1364
- resolvedModuleName,
1365
- customDbUrl || DEFAULT_DATABASE_URLS[resolvedModuleName]
1366
- );
1367
- console.log(import_chalk4.default.yellow(`\u2139 Ensure DB exists: ${setupHint}`));
1368
- console.log(import_chalk4.default.yellow("\u2139 Run migrations: npx drizzle-kit generate && npx drizzle-kit migrate"));
1869
+ printDatabaseHints(resolvedModuleName, customDbUrl, usedDefaultDbUrl, databaseBackupPath);
1369
1870
  }
1370
1871
  if (resolvedModuleName === "auth") {
1371
- if (generatedAuthSecret) {
1372
- console.log(import_chalk4.default.yellow("\u2139 BETTER_AUTH_SECRET was generated automatically."));
1373
- } else {
1374
- console.log(import_chalk4.default.yellow("\u2139 Review BETTER_AUTH_SECRET and BETTER_AUTH_URL in .env."));
1375
- }
1376
- console.log(import_chalk4.default.yellow("\u2139 Run migrations: npx drizzle-kit generate && npx drizzle-kit migrate"));
1872
+ printAuthHints(generatedAuthSecret);
1377
1873
  }
1378
1874
  if (resolvedModuleName === "mailer") {
1379
- if (usedDefaultSmtp) {
1380
- console.log(import_chalk4.default.yellow("\u2139 Placeholder SMTP values added to .env \u2014 update them before sending emails."));
1381
- } else {
1382
- console.log(import_chalk4.default.yellow("\u2139 Review SMTP configuration in .env to ensure values are correct."));
1383
- }
1875
+ printMailerHints(usedDefaultSmtp);
1876
+ }
1877
+ if (resolvedModuleName === "docs") {
1878
+ printDocsHints();
1879
+ }
1880
+ if (resolvedModuleName === "auth" && shouldInstallDocsForAuth) {
1881
+ console.log(import_chalk7.default.blue("\n\u2139 Installing API docs module..."));
1882
+ await add("docs", { yes: true });
1384
1883
  }
1385
1884
  } catch (error) {
1386
- spinner.fail(import_chalk4.default.red(`Failed during ${currentStep}.`));
1885
+ spinner.fail(import_chalk7.default.red(`Failed during ${currentStep}.`));
1387
1886
  const errorMessage = error instanceof Error ? error.message : String(error);
1388
- console.error(import_chalk4.default.red(errorMessage));
1887
+ console.error(import_chalk7.default.red(errorMessage));
1389
1888
  console.log(`
1390
- ${import_chalk4.default.bold("Retry:")}`);
1391
- 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}`));
1392
1891
  }
1393
1892
  };
1394
1893
 
@@ -1396,6 +1895,6 @@ ${import_chalk4.default.bold("Retry:")}`);
1396
1895
  var program = new import_commander.Command();
1397
1896
  program.name("zuro-cli").description("Zuro CLI tool").version("0.0.1");
1398
1897
  program.command("init").description("Initialize a new Zuro project").action(init);
1399
- program.command("add <module>").description("Add a module to your project").action(add);
1898
+ program.command("add <module>").description("Add a module to your project").action((module2, options) => add(module2, options));
1400
1899
  program.parse(process.argv);
1401
1900
  //# sourceMappingURL=index.js.map