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.mjs CHANGED
@@ -1,4 +1,20 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ ensureSchemaExport,
4
+ isDatabaseModule,
5
+ parseDatabaseDialect,
6
+ printDatabaseHints,
7
+ promptDatabaseConfig
8
+ } from "./chunk-W36ZIR4Y.mjs";
9
+ import {
10
+ injectDocsRoutes,
11
+ isDocsModuleInstalled,
12
+ printDocsHints
13
+ } from "./chunk-YBAO5SKK.mjs";
14
+ import {
15
+ appendImport,
16
+ escapeRegex
17
+ } from "./chunk-VMOTWTER.mjs";
2
18
 
3
19
  // src/index.ts
4
20
  import { Command } from "commander";
@@ -9,6 +25,7 @@ import chalk2 from "chalk";
9
25
  import fs4 from "fs-extra";
10
26
  import path4 from "path";
11
27
  import prompts from "prompts";
28
+ import { execSync } from "child_process";
12
29
 
13
30
  // src/utils/registry.ts
14
31
  import { createHash } from "crypto";
@@ -360,6 +377,16 @@ var ENV_CONFIGS = {
360
377
  { name: "RESEND_API_KEY", schema: "z.string().min(1)" },
361
378
  { name: "MAIL_FROM", schema: "z.string().min(1)" }
362
379
  ]
380
+ },
381
+ "rate-limiter": {
382
+ envVars: {
383
+ RATE_LIMIT_WINDOW_MS: "900000",
384
+ RATE_LIMIT_MAX: "100"
385
+ },
386
+ schemaFields: [
387
+ { name: "RATE_LIMIT_WINDOW_MS", schema: "z.coerce.number().default(900000)" },
388
+ { name: "RATE_LIMIT_MAX", schema: "z.coerce.number().default(100)" }
389
+ ]
363
390
  }
364
391
  };
365
392
 
@@ -473,6 +500,62 @@ bun.lockb
473
500
  await fs4.writeFile(prettierIgnorePath, ignoreContent);
474
501
  }
475
502
  }
503
+ async function setupGitignore(targetDir) {
504
+ const gitignorePath = path4.join(targetDir, ".gitignore");
505
+ if (await fs4.pathExists(gitignorePath)) {
506
+ return;
507
+ }
508
+ const gitignoreContent = `# dependencies
509
+ node_modules
510
+
511
+ # build output
512
+ dist
513
+ build
514
+
515
+ # environment variables
516
+ .env
517
+ .env.*
518
+ !.env.example
519
+
520
+ # logs
521
+ *.log
522
+ npm-debug.log*
523
+ pnpm-debug.log*
524
+
525
+ # coverage
526
+ coverage
527
+
528
+ # OS files
529
+ .DS_Store
530
+ Thumbs.db
531
+
532
+ # IDE
533
+ .vscode
534
+ .idea
535
+ *.swp
536
+ *.swo
537
+ `;
538
+ await fs4.writeFile(gitignorePath, gitignoreContent);
539
+ }
540
+ function tryGitInit(targetDir) {
541
+ try {
542
+ execSync("git rev-parse --is-inside-work-tree", {
543
+ cwd: targetDir,
544
+ stdio: "ignore"
545
+ });
546
+ return;
547
+ } catch {
548
+ }
549
+ try {
550
+ execSync("git init", { cwd: targetDir, stdio: "ignore" });
551
+ execSync("git add -A", { cwd: targetDir, stdio: "ignore" });
552
+ execSync('git commit -m "Initial commit from zuro-cli"', {
553
+ cwd: targetDir,
554
+ stdio: "ignore"
555
+ });
556
+ } catch {
557
+ }
558
+ }
476
559
  async function init() {
477
560
  const cwd = process.cwd();
478
561
  const isExistingProject = await fs4.pathExists(path4.join(cwd, "package.json"));
@@ -624,8 +707,16 @@ async function init() {
624
707
  currentStep = "prettier setup";
625
708
  await setupPrettier(targetDir);
626
709
  }
710
+ currentStep = "gitignore setup";
711
+ spinner.text = "Setting up .gitignore...";
712
+ await setupGitignore(targetDir);
627
713
  currentStep = "config write";
628
714
  await writeZuroConfig(targetDir, zuroConfig);
715
+ if (!isExistingProject) {
716
+ currentStep = "git init";
717
+ spinner.text = "Initializing git repository...";
718
+ tryGitInit(targetDir);
719
+ }
629
720
  spinner.succeed(chalk2.green("Project initialized successfully!"));
630
721
  console.log(`
631
722
  ${chalk2.bold("Next steps:")}`);
@@ -649,10 +740,9 @@ ${chalk2.bold("Retry:")}`);
649
740
  }
650
741
 
651
742
  // src/commands/add.ts
652
- import prompts2 from "prompts";
653
743
  import ora2 from "ora";
654
- import path6 from "path";
655
- import fs6 from "fs-extra";
744
+ import path10 from "path";
745
+ import fs9 from "fs-extra";
656
746
  import { randomBytes } from "crypto";
657
747
 
658
748
  // src/utils/dependency.ts
@@ -667,7 +757,9 @@ var BLOCK_SIGNATURES = {
667
757
  "error-handler": "lib/errors.ts",
668
758
  logger: "lib/logger.ts",
669
759
  auth: "lib/auth.ts",
670
- mailer: "lib/mailer.ts"
760
+ mailer: "lib/mailer.ts",
761
+ docs: "lib/openapi.ts",
762
+ "rate-limiter": "middleware/rate-limiter.ts"
671
763
  };
672
764
  var resolveDependencies = async (moduleDependencies, cwd) => {
673
765
  if (!moduleDependencies || moduleDependencies.length === 0) {
@@ -698,12 +790,8 @@ var resolveDependencies = async (moduleDependencies, cwd) => {
698
790
  }
699
791
  };
700
792
 
701
- // src/commands/add.ts
702
- import chalk4 from "chalk";
703
- var DEFAULT_DATABASE_URLS = {
704
- "database-pg": "postgresql://postgres@localhost:5432/mydb",
705
- "database-mysql": "mysql://root@localhost:3306/mydb"
706
- };
793
+ // src/utils/paths.ts
794
+ import path6 from "path";
707
795
  function resolveSafeTargetPath2(projectRoot, srcDir, file) {
708
796
  const targetPath = path6.resolve(projectRoot, srcDir, file.target);
709
797
  const normalizedRoot = path6.resolve(projectRoot);
@@ -712,202 +800,20 @@ function resolveSafeTargetPath2(projectRoot, srcDir, file) {
712
800
  }
713
801
  return targetPath;
714
802
  }
715
- function resolvePackageManager(projectRoot) {
716
- if (fs6.existsSync(path6.join(projectRoot, "pnpm-lock.yaml"))) {
717
- return "pnpm";
718
- }
719
- if (fs6.existsSync(path6.join(projectRoot, "bun.lockb")) || fs6.existsSync(path6.join(projectRoot, "bun.lock"))) {
720
- return "bun";
721
- }
722
- if (fs6.existsSync(path6.join(projectRoot, "yarn.lock"))) {
723
- return "yarn";
724
- }
725
- return "npm";
726
- }
727
- function parseDatabaseDialect(value) {
728
- const normalized = value?.trim().toLowerCase();
729
- if (!normalized) {
730
- return null;
731
- }
732
- if (normalized === "pg" || normalized === "postgres" || normalized === "postgresql" || normalized === "database-pg") {
733
- return "database-pg";
734
- }
735
- if (normalized === "mysql" || normalized === "database-mysql") {
736
- return "database-mysql";
737
- }
738
- return null;
739
- }
740
- function isDatabaseModule(moduleName) {
741
- return moduleName === "database-pg" || moduleName === "database-mysql";
742
- }
743
- function validateDatabaseUrl(rawUrl, moduleName) {
744
- const dbUrl = rawUrl.trim();
745
- if (!dbUrl) {
746
- throw new Error("Database URL cannot be empty.");
747
- }
748
- let parsed;
749
- try {
750
- parsed = new URL(dbUrl);
751
- } catch {
752
- throw new Error(`Invalid database URL: '${dbUrl}'.`);
753
- }
754
- const protocol = parsed.protocol.toLowerCase();
755
- if (moduleName === "database-pg" && protocol !== "postgresql:" && protocol !== "postgres:") {
756
- throw new Error("PostgreSQL URL must start with postgres:// or postgresql://");
757
- }
758
- if (moduleName === "database-mysql" && protocol !== "mysql:") {
759
- throw new Error("MySQL URL must start with mysql://");
760
- }
761
- return dbUrl;
762
- }
763
- async function detectInstalledDatabaseDialect(projectRoot, srcDir) {
764
- const dbIndexPath = path6.join(projectRoot, srcDir, "db", "index.ts");
765
- if (!fs6.existsSync(dbIndexPath)) {
766
- return null;
767
- }
768
- const content = await fs6.readFile(dbIndexPath, "utf-8");
769
- if (content.includes("drizzle-orm/node-postgres") || content.includes(`from "pg"`)) {
770
- return "database-pg";
771
- }
772
- if (content.includes("drizzle-orm/mysql2") || content.includes(`from "mysql2`)) {
773
- return "database-mysql";
774
- }
775
- return null;
776
- }
777
- async function backupDatabaseFiles(projectRoot, srcDir) {
778
- const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
779
- const backupRoot = path6.join(projectRoot, ".zuro", "backups", `database-${timestamp}`);
780
- const candidates = [
781
- path6.join(projectRoot, srcDir, "db", "index.ts"),
782
- path6.join(projectRoot, "drizzle.config.ts")
783
- ];
784
- let copied = false;
785
- for (const filePath of candidates) {
786
- if (!fs6.existsSync(filePath)) {
787
- continue;
788
- }
789
- const relativePath = path6.relative(projectRoot, filePath);
790
- const backupPath = path6.join(backupRoot, relativePath);
791
- await fs6.ensureDir(path6.dirname(backupPath));
792
- await fs6.copyFile(filePath, backupPath);
793
- copied = true;
794
- }
795
- return copied ? backupRoot : null;
796
- }
797
- function databaseLabel(moduleName) {
798
- return moduleName === "database-pg" ? "PostgreSQL" : "MySQL";
799
- }
800
- function getDatabaseSetupHint(moduleName, dbUrl) {
801
- try {
802
- const parsed = new URL(dbUrl);
803
- const dbName = parsed.pathname.replace(/^\/+/, "") || "mydb";
804
- if (moduleName === "database-pg") {
805
- return `createdb ${dbName}`;
806
- }
807
- return `mysql -e "CREATE DATABASE IF NOT EXISTS ${dbName};"`;
808
- } catch {
809
- return moduleName === "database-pg" ? "createdb <database_name>" : `mysql -e "CREATE DATABASE IF NOT EXISTS <database_name>;"`;
810
- }
811
- }
812
- function getModuleDocsPath(moduleName) {
813
- if (isDatabaseModule(moduleName)) {
814
- return "database";
815
- }
816
- return moduleName;
817
- }
818
- function escapeRegex(value) {
819
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
820
- }
821
- async function hasEnvVariable(projectRoot, key) {
822
- const envPath = path6.join(projectRoot, ".env");
823
- if (!await fs6.pathExists(envPath)) {
824
- return false;
825
- }
826
- const content = await fs6.readFile(envPath, "utf-8");
827
- const pattern = new RegExp(`^${escapeRegex(key)}=`, "m");
828
- return pattern.test(content);
829
- }
830
- async function isLikelyEmptyDirectory(cwd) {
831
- const entries = await fs6.readdir(cwd);
832
- const ignored = /* @__PURE__ */ new Set([".ds_store", "thumbs.db"]);
833
- return entries.filter((entry) => !ignored.has(entry.toLowerCase())).length === 0;
834
- }
835
- async function ensureSchemaExport(projectRoot, srcDir, schemaFileName) {
836
- const schemaIndexPath = path6.join(projectRoot, srcDir, "db", "schema", "index.ts");
837
- if (!await fs6.pathExists(schemaIndexPath)) {
838
- return;
839
- }
840
- const exportLine = `export * from "./${schemaFileName}";`;
841
- const content = await fs6.readFile(schemaIndexPath, "utf-8");
842
- const normalized = content.replace(/\r\n/g, "\n");
843
- const exportPattern = new RegExp(
844
- `^\\s*export\\s*\\*\\s*from\\s*["']\\./${escapeRegex(schemaFileName)}["'];?\\s*$`,
845
- "m"
846
- );
847
- if (exportPattern.test(normalized)) {
848
- return;
849
- }
850
- let next = normalized.replace(/^\s*export\s*\{\s*\};?\s*$/m, "").trimEnd();
851
- if (next.length > 0) {
852
- next += "\n\n";
853
- }
854
- next += `${exportLine}
855
- `;
856
- await fs6.writeFile(schemaIndexPath, next);
857
- }
858
- async function injectErrorHandler(projectRoot, srcDir) {
859
- const appPath = path6.join(projectRoot, srcDir, "app.ts");
860
- if (!fs6.existsSync(appPath)) {
861
- return false;
862
- }
863
- let content = await fs6.readFile(appPath, "utf-8");
864
- const errorImport = `import { errorHandler, notFoundHandler } from "./middleware/error-handler";`;
865
- const hasErrorImport = content.includes(errorImport);
866
- const hasNotFoundUse = /app\.use\(\s*notFoundHandler\s*\)/.test(content);
867
- const hasErrorUse = /app\.use\(\s*errorHandler\s*\)/.test(content);
868
- let modified = false;
869
- let importInserted = hasErrorImport;
870
- if (!hasErrorImport) {
871
- const importRegex = /^import .+ from .+;?\s*$/gm;
872
- let lastImportIndex = 0;
873
- let match;
874
- while ((match = importRegex.exec(content)) !== null) {
875
- lastImportIndex = match.index + match[0].length;
876
- }
877
- if (lastImportIndex > 0) {
878
- content = content.slice(0, lastImportIndex) + `
879
- ${errorImport}` + content.slice(lastImportIndex);
880
- modified = true;
881
- importInserted = true;
882
- }
883
- }
884
- let setupInserted = hasNotFoundUse && hasErrorUse;
885
- if (!setupInserted) {
886
- const setupLines = [];
887
- if (!hasNotFoundUse) {
888
- setupLines.push("app.use(notFoundHandler);");
889
- }
890
- if (!hasErrorUse) {
891
- setupLines.push("app.use(errorHandler);");
892
- }
893
- const errorSetup = `
894
- // Error handling (must be last)
895
- ${setupLines.join("\n")}
896
- `;
897
- const exportMatch = content.match(/export default app;?\s*$/m);
898
- if (exportMatch && exportMatch.index !== void 0) {
899
- content = content.slice(0, exportMatch.index) + errorSetup + "\n" + content.slice(exportMatch.index);
900
- modified = true;
901
- setupInserted = true;
902
- }
903
- }
904
- if (modified) {
905
- await fs6.writeFile(appPath, content);
906
- }
907
- return importInserted && setupInserted;
803
+
804
+ // src/commands/add.ts
805
+ import chalk6 from "chalk";
806
+
807
+ // src/handlers/auth.handler.ts
808
+ import path7 from "path";
809
+ import fs6 from "fs-extra";
810
+ import prompts2 from "prompts";
811
+ import chalk4 from "chalk";
812
+ async function isAuthModuleInstalled(projectRoot, srcDir) {
813
+ return await fs6.pathExists(path7.join(projectRoot, srcDir, "lib", "auth.ts"));
908
814
  }
909
815
  async function injectAuthRoutes(projectRoot, srcDir) {
910
- const appPath = path6.join(projectRoot, srcDir, "app.ts");
816
+ const appPath = path7.join(projectRoot, srcDir, "app.ts");
911
817
  if (!await fs6.pathExists(appPath)) {
912
818
  return false;
913
819
  }
@@ -917,25 +823,6 @@ async function injectAuthRoutes(projectRoot, srcDir) {
917
823
  const routeIndexUserImport = `import userRoutes from "./user.routes";`;
918
824
  const appUserImport = `import userRoutes from "./routes/user.routes";`;
919
825
  let appModified = false;
920
- const appendImport = (source, line) => {
921
- if (source.includes(line)) {
922
- return { source, inserted: true };
923
- }
924
- const importRegex = /^import .+ from .+;?\s*$/gm;
925
- let lastImportIndex = 0;
926
- let match;
927
- while ((match = importRegex.exec(source)) !== null) {
928
- lastImportIndex = match.index + match[0].length;
929
- }
930
- if (lastImportIndex <= 0) {
931
- return { source, inserted: false };
932
- }
933
- return {
934
- source: source.slice(0, lastImportIndex) + `
935
- ${line}` + source.slice(lastImportIndex),
936
- inserted: true
937
- };
938
- };
939
826
  for (const importLine of [authHandlerImport, authImport]) {
940
827
  const next = appendImport(appContent, importLine);
941
828
  if (!next.inserted) {
@@ -965,7 +852,7 @@ ${line}` + source.slice(lastImportIndex),
965
852
  appContent = appContent.slice(0, insertionIndex) + authMountLine + appContent.slice(insertionIndex);
966
853
  appModified = true;
967
854
  }
968
- const routeIndexPath = path6.join(projectRoot, srcDir, "routes", "index.ts");
855
+ const routeIndexPath = path7.join(projectRoot, srcDir, "routes", "index.ts");
969
856
  if (await fs6.pathExists(routeIndexPath)) {
970
857
  let routeContent = await fs6.readFile(routeIndexPath, "utf-8");
971
858
  let routeModified = false;
@@ -1021,7 +908,399 @@ app.use("/api/users", userRoutes);
1021
908
  }
1022
909
  return true;
1023
910
  }
1024
- var add = async (moduleName) => {
911
+ async function injectAuthDocs(projectRoot, srcDir) {
912
+ const openApiPath = path7.join(projectRoot, srcDir, "lib", "openapi.ts");
913
+ if (!await fs6.pathExists(openApiPath)) {
914
+ return false;
915
+ }
916
+ const authMarker = "// ZURO_AUTH_DOCS";
917
+ let content = await fs6.readFile(openApiPath, "utf-8");
918
+ if (content.includes(authMarker)) {
919
+ return true;
920
+ }
921
+ const moduleDocsEndMarker = "// ZURO_DOCS_MODULES_END";
922
+ if (!content.includes(moduleDocsEndMarker)) {
923
+ return false;
924
+ }
925
+ const authBlock = `
926
+ const authSignUpSchema = z.object({
927
+ email: z.string().email().openapi({ example: "dev@company.com" }),
928
+ password: z.string().min(8).openapi({ example: "strong-password" }),
929
+ name: z.string().min(1).optional().openapi({ example: "Dev User" }),
930
+ });
931
+
932
+ const authSignInSchema = z.object({
933
+ email: z.string().email().openapi({ example: "dev@company.com" }),
934
+ password: z.string().min(8).openapi({ example: "strong-password" }),
935
+ });
936
+
937
+ const authUserSchema = z.object({
938
+ id: z.string().openapi({ example: "user_123" }),
939
+ email: z.string().email().openapi({ example: "dev@company.com" }),
940
+ name: z.string().nullable().openapi({ example: "Dev User" }),
941
+ });
942
+
943
+ ${authMarker}
944
+ registry.registerPath({
945
+ method: "post",
946
+ path: "/api/auth/sign-up/email",
947
+ tags: ["Auth"],
948
+ summary: "Register using email and password",
949
+ request: {
950
+ body: {
951
+ content: {
952
+ "application/json": {
953
+ schema: authSignUpSchema,
954
+ },
955
+ },
956
+ },
957
+ },
958
+ responses: {
959
+ 200: { description: "Registration successful" },
960
+ },
961
+ });
962
+
963
+ registry.registerPath({
964
+ method: "post",
965
+ path: "/api/auth/sign-in/email",
966
+ tags: ["Auth"],
967
+ summary: "Sign in using email and password",
968
+ request: {
969
+ body: {
970
+ content: {
971
+ "application/json": {
972
+ schema: authSignInSchema,
973
+ },
974
+ },
975
+ },
976
+ },
977
+ responses: {
978
+ 200: { description: "Sign in successful" },
979
+ 401: { description: "Invalid credentials" },
980
+ },
981
+ });
982
+
983
+ registry.registerPath({
984
+ method: "post",
985
+ path: "/api/auth/sign-out",
986
+ tags: ["Auth"],
987
+ summary: "Sign out current user",
988
+ responses: {
989
+ 200: { description: "Sign out successful" },
990
+ },
991
+ });
992
+
993
+ registry.registerPath({
994
+ method: "get",
995
+ path: "/api/users/me",
996
+ tags: ["Auth"],
997
+ summary: "Get current authenticated user",
998
+ security: [{ bearerAuth: [] }],
999
+ responses: {
1000
+ 200: {
1001
+ description: "Current user",
1002
+ content: {
1003
+ "application/json": {
1004
+ schema: z.object({ user: authUserSchema }),
1005
+ },
1006
+ },
1007
+ },
1008
+ 401: { description: "Not authenticated" },
1009
+ },
1010
+ });
1011
+ `;
1012
+ content = content.replace(moduleDocsEndMarker, `${authBlock}
1013
+ ${moduleDocsEndMarker}`);
1014
+ await fs6.writeFile(openApiPath, content);
1015
+ return true;
1016
+ }
1017
+ async function promptAuthConfig(projectRoot, srcDir, options) {
1018
+ const { isDocsModuleInstalled: isDocsModuleInstalled2 } = await import("./docs.handler-JL3ZIVJQ.mjs");
1019
+ const docsInstalled = await isDocsModuleInstalled2(projectRoot, srcDir);
1020
+ let shouldInstallDocsForAuth = false;
1021
+ if (!docsInstalled) {
1022
+ if (options.yes) {
1023
+ shouldInstallDocsForAuth = true;
1024
+ } else {
1025
+ const docsResponse = await prompts2({
1026
+ type: "confirm",
1027
+ name: "installDocs",
1028
+ message: "Install API docs module (Scalar + OpenAPI) too?",
1029
+ initial: true
1030
+ });
1031
+ if (docsResponse.installDocs === void 0) {
1032
+ console.log(chalk4.yellow("Operation cancelled."));
1033
+ return null;
1034
+ }
1035
+ shouldInstallDocsForAuth = docsResponse.installDocs;
1036
+ }
1037
+ }
1038
+ const { detectInstalledDatabaseDialect } = await import("./database.handler-D7EVXRJX.mjs");
1039
+ const authDatabaseDialect = await detectInstalledDatabaseDialect(projectRoot, srcDir);
1040
+ return { shouldInstallDocsForAuth, authDatabaseDialect };
1041
+ }
1042
+ function printAuthHints(generatedAuthSecret) {
1043
+ if (generatedAuthSecret) {
1044
+ console.log(chalk4.yellow("\u2139 BETTER_AUTH_SECRET was generated automatically."));
1045
+ } else {
1046
+ console.log(chalk4.yellow("\u2139 Review BETTER_AUTH_SECRET and BETTER_AUTH_URL in .env."));
1047
+ }
1048
+ console.log(chalk4.yellow("\u2139 Run migrations: npx drizzle-kit generate && npx drizzle-kit migrate"));
1049
+ }
1050
+
1051
+ // src/handlers/mailer.handler.ts
1052
+ import prompts3 from "prompts";
1053
+ import chalk5 from "chalk";
1054
+ async function promptMailerConfig() {
1055
+ const providerResponse = await prompts3({
1056
+ type: "select",
1057
+ name: "provider",
1058
+ message: "Which email provider?",
1059
+ choices: [
1060
+ { title: "SMTP (Nodemailer)", description: "Gmail, Mailtrap, any SMTP server", value: "smtp" },
1061
+ { title: "Resend", description: "API-based, easiest setup", value: "resend" }
1062
+ ]
1063
+ });
1064
+ if (providerResponse.provider === void 0) {
1065
+ console.log(chalk5.yellow("Operation cancelled."));
1066
+ return null;
1067
+ }
1068
+ const mailerProvider = providerResponse.provider;
1069
+ let customSmtpVars;
1070
+ let usedDefaultSmtp = false;
1071
+ console.log(chalk5.dim(" Tip: Leave fields blank to use placeholder values and configure later\n"));
1072
+ if (mailerProvider === "smtp") {
1073
+ const smtpResponse = await prompts3([
1074
+ {
1075
+ type: "text",
1076
+ name: "host",
1077
+ message: "SMTP Host",
1078
+ initial: ""
1079
+ },
1080
+ {
1081
+ type: "text",
1082
+ name: "port",
1083
+ message: "SMTP Port",
1084
+ initial: "587"
1085
+ },
1086
+ {
1087
+ type: "text",
1088
+ name: "user",
1089
+ message: "SMTP User",
1090
+ initial: ""
1091
+ },
1092
+ {
1093
+ type: "password",
1094
+ name: "pass",
1095
+ message: "SMTP Password"
1096
+ },
1097
+ {
1098
+ type: "text",
1099
+ name: "from",
1100
+ message: "Mail From address",
1101
+ initial: ""
1102
+ }
1103
+ ]);
1104
+ if (smtpResponse.host === void 0) {
1105
+ console.log(chalk5.yellow("Operation cancelled."));
1106
+ return null;
1107
+ }
1108
+ const host = smtpResponse.host?.trim() || "";
1109
+ const user = smtpResponse.user?.trim() || "";
1110
+ const pass = smtpResponse.pass?.trim() || "";
1111
+ const from = smtpResponse.from?.trim() || "";
1112
+ const port = smtpResponse.port?.trim() || "587";
1113
+ usedDefaultSmtp = !host && !user;
1114
+ if (!usedDefaultSmtp) {
1115
+ customSmtpVars = {
1116
+ SMTP_HOST: host || "smtp.example.com",
1117
+ SMTP_PORT: port,
1118
+ SMTP_USER: user || "your-email@example.com",
1119
+ SMTP_PASS: pass || "your-password",
1120
+ MAIL_FROM: from || "noreply@example.com"
1121
+ };
1122
+ }
1123
+ } else {
1124
+ const resendResponse = await prompts3([
1125
+ {
1126
+ type: "text",
1127
+ name: "apiKey",
1128
+ message: "Resend API Key",
1129
+ initial: ""
1130
+ },
1131
+ {
1132
+ type: "text",
1133
+ name: "from",
1134
+ message: "Mail From address",
1135
+ initial: ""
1136
+ }
1137
+ ]);
1138
+ if (resendResponse.apiKey === void 0) {
1139
+ console.log(chalk5.yellow("Operation cancelled."));
1140
+ return null;
1141
+ }
1142
+ const apiKey = resendResponse.apiKey?.trim() || "";
1143
+ const from = resendResponse.from?.trim() || "";
1144
+ usedDefaultSmtp = !apiKey;
1145
+ if (!usedDefaultSmtp) {
1146
+ customSmtpVars = {
1147
+ RESEND_API_KEY: apiKey || "re_your_api_key",
1148
+ MAIL_FROM: from || "onboarding@resend.dev"
1149
+ };
1150
+ }
1151
+ }
1152
+ return { mailerProvider, customSmtpVars, usedDefaultSmtp };
1153
+ }
1154
+ function printMailerHints(usedDefaultSmtp) {
1155
+ if (usedDefaultSmtp) {
1156
+ console.log(chalk5.yellow("\u2139 Placeholder SMTP values added to .env \u2014 update them before sending emails."));
1157
+ } else {
1158
+ console.log(chalk5.yellow("\u2139 Review SMTP configuration in .env to ensure values are correct."));
1159
+ }
1160
+ }
1161
+
1162
+ // src/handlers/error-handler.handler.ts
1163
+ import path8 from "path";
1164
+ import fs7 from "fs-extra";
1165
+ async function injectErrorHandler(projectRoot, srcDir) {
1166
+ const appPath = path8.join(projectRoot, srcDir, "app.ts");
1167
+ if (!fs7.existsSync(appPath)) {
1168
+ return false;
1169
+ }
1170
+ let content = await fs7.readFile(appPath, "utf-8");
1171
+ const errorImport = `import { errorHandler, notFoundHandler } from "./middleware/error-handler";`;
1172
+ const hasErrorImport = content.includes(errorImport);
1173
+ const hasNotFoundUse = /app\.use\(\s*notFoundHandler\s*\)/.test(content);
1174
+ const hasErrorUse = /app\.use\(\s*errorHandler\s*\)/.test(content);
1175
+ let modified = false;
1176
+ let importInserted = hasErrorImport;
1177
+ if (!hasErrorImport) {
1178
+ const importRegex = /^import .+ from .+;?\s*$/gm;
1179
+ let lastImportIndex = 0;
1180
+ let match;
1181
+ while ((match = importRegex.exec(content)) !== null) {
1182
+ lastImportIndex = match.index + match[0].length;
1183
+ }
1184
+ if (lastImportIndex > 0) {
1185
+ content = content.slice(0, lastImportIndex) + `
1186
+ ${errorImport}` + content.slice(lastImportIndex);
1187
+ modified = true;
1188
+ importInserted = true;
1189
+ }
1190
+ }
1191
+ let setupInserted = hasNotFoundUse && hasErrorUse;
1192
+ if (!setupInserted) {
1193
+ const setupLines = [];
1194
+ if (!hasNotFoundUse) {
1195
+ setupLines.push("app.use(notFoundHandler);");
1196
+ }
1197
+ if (!hasErrorUse) {
1198
+ setupLines.push("app.use(errorHandler);");
1199
+ }
1200
+ const errorSetup = `
1201
+ // Error handling (must be last)
1202
+ ${setupLines.join("\n")}
1203
+ `;
1204
+ const exportMatch = content.match(/export default app;?\s*$/m);
1205
+ if (exportMatch && exportMatch.index !== void 0) {
1206
+ content = content.slice(0, exportMatch.index) + errorSetup + "\n" + content.slice(exportMatch.index);
1207
+ modified = true;
1208
+ setupInserted = true;
1209
+ }
1210
+ }
1211
+ if (modified) {
1212
+ await fs7.writeFile(appPath, content);
1213
+ }
1214
+ return importInserted && setupInserted;
1215
+ }
1216
+
1217
+ // src/handlers/rate-limiter.handler.ts
1218
+ import path9 from "path";
1219
+ import fs8 from "fs-extra";
1220
+ async function injectRateLimiter(projectRoot, srcDir) {
1221
+ const appPath = path9.join(projectRoot, srcDir, "app.ts");
1222
+ if (!fs8.existsSync(appPath)) {
1223
+ return false;
1224
+ }
1225
+ let content = await fs8.readFile(appPath, "utf-8");
1226
+ const rateLimiterImport = `import { rateLimiter } from "./middleware/rate-limiter";`;
1227
+ const hasImport = content.includes(rateLimiterImport);
1228
+ const hasUse = /app\.use\(\s*rateLimiter\s*\)/.test(content);
1229
+ if (hasImport && hasUse) {
1230
+ return true;
1231
+ }
1232
+ let modified = false;
1233
+ if (!hasImport) {
1234
+ const importRegex = /^import .+ from .+;?\s*$/gm;
1235
+ let lastImportIndex = 0;
1236
+ let match;
1237
+ while ((match = importRegex.exec(content)) !== null) {
1238
+ lastImportIndex = match.index + match[0].length;
1239
+ }
1240
+ if (lastImportIndex > 0) {
1241
+ content = content.slice(0, lastImportIndex) + `
1242
+ ${rateLimiterImport}` + content.slice(lastImportIndex);
1243
+ modified = true;
1244
+ }
1245
+ }
1246
+ if (!hasUse) {
1247
+ const jsonMiddleware = /^(\s*app\.use\(\s*express\.json\(\)\s*\);?\s*)$/m;
1248
+ const jsonMatch = content.match(jsonMiddleware);
1249
+ if (jsonMatch && jsonMatch.index !== void 0) {
1250
+ const insertAt = jsonMatch.index + jsonMatch[0].length;
1251
+ content = content.slice(0, insertAt) + `
1252
+ app.use(rateLimiter);` + content.slice(insertAt);
1253
+ modified = true;
1254
+ } else {
1255
+ const routeMount = /^\s*app\.use\(\s*["']\/api["']/m;
1256
+ const routeMatch = content.match(routeMount);
1257
+ if (routeMatch && routeMatch.index !== void 0) {
1258
+ content = content.slice(0, routeMatch.index) + `app.use(rateLimiter);
1259
+ ` + content.slice(routeMatch.index);
1260
+ modified = true;
1261
+ }
1262
+ }
1263
+ }
1264
+ if (modified) {
1265
+ await fs8.writeFile(appPath, content);
1266
+ }
1267
+ return true;
1268
+ }
1269
+
1270
+ // src/commands/add.ts
1271
+ function resolvePackageManager(projectRoot) {
1272
+ if (fs9.existsSync(path10.join(projectRoot, "pnpm-lock.yaml"))) {
1273
+ return "pnpm";
1274
+ }
1275
+ if (fs9.existsSync(path10.join(projectRoot, "bun.lockb")) || fs9.existsSync(path10.join(projectRoot, "bun.lock"))) {
1276
+ return "bun";
1277
+ }
1278
+ if (fs9.existsSync(path10.join(projectRoot, "yarn.lock"))) {
1279
+ return "yarn";
1280
+ }
1281
+ return "npm";
1282
+ }
1283
+ function getModuleDocsPath(moduleName) {
1284
+ if (isDatabaseModule(moduleName)) {
1285
+ return "database";
1286
+ }
1287
+ return moduleName;
1288
+ }
1289
+ async function hasEnvVariable(projectRoot, key) {
1290
+ const envPath = path10.join(projectRoot, ".env");
1291
+ if (!await fs9.pathExists(envPath)) {
1292
+ return false;
1293
+ }
1294
+ const content = await fs9.readFile(envPath, "utf-8");
1295
+ const pattern = new RegExp(`^${escapeRegex(key)}=`, "m");
1296
+ return pattern.test(content);
1297
+ }
1298
+ async function isLikelyEmptyDirectory(cwd) {
1299
+ const entries = await fs9.readdir(cwd);
1300
+ const ignored = /* @__PURE__ */ new Set([".ds_store", "thumbs.db"]);
1301
+ return entries.filter((entry) => !ignored.has(entry.toLowerCase())).length === 0;
1302
+ }
1303
+ var add = async (moduleName, options = {}) => {
1025
1304
  const projectRoot = process.cwd();
1026
1305
  const projectConfig = await readZuroConfig(projectRoot);
1027
1306
  if (!projectConfig) {
@@ -1046,162 +1325,27 @@ var add = async (moduleName) => {
1046
1325
  let customSmtpVars;
1047
1326
  let usedDefaultSmtp = false;
1048
1327
  let mailerProvider = "smtp";
1049
- if (resolvedModuleName === "database") {
1050
- const variantResponse = await prompts2({
1051
- type: "select",
1052
- name: "variant",
1053
- message: "Which database dialect?",
1054
- choices: [
1055
- { title: "PostgreSQL", value: "database-pg" },
1056
- { title: "MySQL", value: "database-mysql" }
1057
- ]
1058
- });
1059
- if (!variantResponse.variant) {
1060
- console.log(chalk4.yellow("Operation cancelled."));
1061
- return;
1062
- }
1063
- resolvedModuleName = variantResponse.variant;
1064
- }
1065
- if (isDatabaseModule(resolvedModuleName)) {
1066
- const installedDialect = await detectInstalledDatabaseDialect(projectRoot, srcDir);
1067
- if (installedDialect && installedDialect !== resolvedModuleName) {
1068
- console.log(
1069
- chalk4.yellow(
1070
- `
1071
- \u26A0 Existing database setup detected: ${databaseLabel(installedDialect)}.`
1072
- )
1073
- );
1074
- console.log(
1075
- chalk4.yellow(
1076
- ` Switching to ${databaseLabel(resolvedModuleName)} will overwrite db files and drizzle config.
1077
- `
1078
- )
1079
- );
1080
- const switchResponse = await prompts2({
1081
- type: "confirm",
1082
- name: "proceed",
1083
- message: "Continue and switch database dialect?",
1084
- initial: false
1085
- });
1086
- if (!switchResponse.proceed) {
1087
- console.log(chalk4.yellow("Operation cancelled."));
1088
- return;
1089
- }
1090
- databaseBackupPath = await backupDatabaseFiles(projectRoot, srcDir);
1091
- }
1092
- const defaultUrl = DEFAULT_DATABASE_URLS[resolvedModuleName];
1093
- console.log(chalk4.dim(` Tip: Leave blank to use ${defaultUrl}
1094
- `));
1095
- const response = await prompts2({
1096
- type: "text",
1097
- name: "dbUrl",
1098
- message: "Database URL",
1099
- initial: ""
1100
- });
1101
- if (response.dbUrl === void 0) {
1102
- console.log(chalk4.yellow("Operation cancelled."));
1103
- return;
1104
- }
1105
- const enteredUrl = response.dbUrl?.trim() || "";
1106
- usedDefaultDbUrl = enteredUrl.length === 0;
1107
- customDbUrl = validateDatabaseUrl(enteredUrl || defaultUrl, resolvedModuleName);
1328
+ let shouldInstallDocsForAuth = false;
1329
+ if (resolvedModuleName === "database" || isDatabaseModule(resolvedModuleName)) {
1330
+ const result = await promptDatabaseConfig(resolvedModuleName, projectRoot, srcDir);
1331
+ if (!result) return;
1332
+ resolvedModuleName = result.resolvedModuleName;
1333
+ customDbUrl = result.customDbUrl;
1334
+ usedDefaultDbUrl = result.usedDefaultDbUrl;
1335
+ databaseBackupPath = result.databaseBackupPath;
1108
1336
  }
1109
1337
  if (resolvedModuleName === "mailer") {
1110
- const providerResponse = await prompts2({
1111
- type: "select",
1112
- name: "provider",
1113
- message: "Which email provider?",
1114
- choices: [
1115
- { title: "SMTP (Nodemailer)", description: "Gmail, Mailtrap, any SMTP server", value: "smtp" },
1116
- { title: "Resend", description: "API-based, easiest setup", value: "resend" }
1117
- ]
1118
- });
1119
- if (providerResponse.provider === void 0) {
1120
- console.log(chalk4.yellow("Operation cancelled."));
1121
- return;
1122
- }
1123
- mailerProvider = providerResponse.provider;
1124
- console.log(chalk4.dim(" Tip: Leave fields blank to use placeholder values and configure later\n"));
1125
- if (mailerProvider === "smtp") {
1126
- const smtpResponse = await prompts2([
1127
- {
1128
- type: "text",
1129
- name: "host",
1130
- message: "SMTP Host",
1131
- initial: ""
1132
- },
1133
- {
1134
- type: "text",
1135
- name: "port",
1136
- message: "SMTP Port",
1137
- initial: "587"
1138
- },
1139
- {
1140
- type: "text",
1141
- name: "user",
1142
- message: "SMTP User",
1143
- initial: ""
1144
- },
1145
- {
1146
- type: "password",
1147
- name: "pass",
1148
- message: "SMTP Password"
1149
- },
1150
- {
1151
- type: "text",
1152
- name: "from",
1153
- message: "Mail From address",
1154
- initial: ""
1155
- }
1156
- ]);
1157
- if (smtpResponse.host === void 0) {
1158
- console.log(chalk4.yellow("Operation cancelled."));
1159
- return;
1160
- }
1161
- const host = smtpResponse.host?.trim() || "";
1162
- const user = smtpResponse.user?.trim() || "";
1163
- const pass = smtpResponse.pass?.trim() || "";
1164
- const from = smtpResponse.from?.trim() || "";
1165
- const port = smtpResponse.port?.trim() || "587";
1166
- usedDefaultSmtp = !host && !user;
1167
- if (!usedDefaultSmtp) {
1168
- customSmtpVars = {
1169
- SMTP_HOST: host || "smtp.example.com",
1170
- SMTP_PORT: port,
1171
- SMTP_USER: user || "your-email@example.com",
1172
- SMTP_PASS: pass || "your-password",
1173
- MAIL_FROM: from || "noreply@example.com"
1174
- };
1175
- }
1176
- } else {
1177
- const resendResponse = await prompts2([
1178
- {
1179
- type: "text",
1180
- name: "apiKey",
1181
- message: "Resend API Key",
1182
- initial: ""
1183
- },
1184
- {
1185
- type: "text",
1186
- name: "from",
1187
- message: "Mail From address",
1188
- initial: ""
1189
- }
1190
- ]);
1191
- if (resendResponse.apiKey === void 0) {
1192
- console.log(chalk4.yellow("Operation cancelled."));
1193
- return;
1194
- }
1195
- const apiKey = resendResponse.apiKey?.trim() || "";
1196
- const from = resendResponse.from?.trim() || "";
1197
- usedDefaultSmtp = !apiKey;
1198
- if (!usedDefaultSmtp) {
1199
- customSmtpVars = {
1200
- RESEND_API_KEY: apiKey || "re_your_api_key",
1201
- MAIL_FROM: from || "onboarding@resend.dev"
1202
- };
1203
- }
1204
- }
1338
+ const result = await promptMailerConfig();
1339
+ if (!result) return;
1340
+ mailerProvider = result.mailerProvider;
1341
+ customSmtpVars = result.customSmtpVars;
1342
+ usedDefaultSmtp = result.usedDefaultSmtp;
1343
+ }
1344
+ if (resolvedModuleName === "auth") {
1345
+ const result = await promptAuthConfig(projectRoot, srcDir, options);
1346
+ if (!result) return;
1347
+ shouldInstallDocsForAuth = result.shouldInstallDocsForAuth;
1348
+ authDatabaseDialect = result.authDatabaseDialect;
1205
1349
  }
1206
1350
  const pm = resolvePackageManager(projectRoot);
1207
1351
  const spinner = ora2(`Checking registry for ${resolvedModuleName}...`).start();
@@ -1217,7 +1361,7 @@ var add = async (moduleName) => {
1217
1361
  spinner.fail(`Module '${resolvedModuleName}' not found.`);
1218
1362
  return;
1219
1363
  }
1220
- spinner.succeed(`Found module: ${chalk4.cyan(resolvedModuleName)}`);
1364
+ spinner.succeed(`Found module: ${chalk6.cyan(resolvedModuleName)}`);
1221
1365
  const moduleDeps = module.moduleDependencies || [];
1222
1366
  currentStep = "module dependency resolution";
1223
1367
  await resolveDependencies(moduleDeps, projectRoot);
@@ -1239,9 +1383,6 @@ var add = async (moduleName) => {
1239
1383
  spinner.succeed("Dependencies installed");
1240
1384
  currentStep = "module scaffolding";
1241
1385
  spinner.start("Scaffolding files...");
1242
- if (resolvedModuleName === "auth") {
1243
- authDatabaseDialect = await detectInstalledDatabaseDialect(projectRoot, srcDir);
1244
- }
1245
1386
  for (const file of module.files) {
1246
1387
  let fetchPath = file.path;
1247
1388
  let expectedSha256 = file.sha256;
@@ -1269,10 +1410,10 @@ var add = async (moduleName) => {
1269
1410
  );
1270
1411
  }
1271
1412
  const targetPath = resolveSafeTargetPath2(projectRoot, srcDir, file);
1272
- await fs6.ensureDir(path6.dirname(targetPath));
1273
- await fs6.writeFile(targetPath, content);
1413
+ await fs9.ensureDir(path10.dirname(targetPath));
1414
+ await fs9.writeFile(targetPath, content);
1274
1415
  }
1275
- const schemaExports = module.files.map((file) => file.target.replace(/\\/g, "/")).filter((target) => /^db\/schema\/[^/]+\.ts$/.test(target)).map((target) => path6.posix.basename(target, ".ts")).filter((name) => name !== "index");
1416
+ const schemaExports = module.files.map((file) => file.target.replace(/\\/g, "/")).filter((target) => /^db\/schema\/[^/]+\.ts$/.test(target)).map((target) => path10.posix.basename(target, ".ts")).filter((name) => name !== "index");
1276
1417
  for (const schemaFileName of schemaExports) {
1277
1418
  await ensureSchemaExport(projectRoot, srcDir, schemaFileName);
1278
1419
  }
@@ -1285,6 +1426,16 @@ var add = async (moduleName) => {
1285
1426
  } else {
1286
1427
  spinner.warn("Could not configure routes automatically");
1287
1428
  }
1429
+ const docsInstalled = await isDocsModuleInstalled(projectRoot, srcDir);
1430
+ if (docsInstalled) {
1431
+ spinner.start("Adding auth endpoints to API docs...");
1432
+ const authDocsInjected = await injectAuthDocs(projectRoot, srcDir);
1433
+ if (authDocsInjected) {
1434
+ spinner.succeed("Auth endpoints added to API docs");
1435
+ } else {
1436
+ spinner.warn("Could not update API docs automatically");
1437
+ }
1438
+ }
1288
1439
  }
1289
1440
  if (resolvedModuleName === "error-handler") {
1290
1441
  spinner.start("Configuring error handler in app.ts...");
@@ -1295,6 +1446,34 @@ var add = async (moduleName) => {
1295
1446
  spinner.warn("Could not find app.ts - error handler needs manual setup");
1296
1447
  }
1297
1448
  }
1449
+ if (resolvedModuleName === "rate-limiter") {
1450
+ spinner.start("Configuring rate limiter in app.ts...");
1451
+ const injected = await injectRateLimiter(projectRoot, srcDir);
1452
+ if (injected) {
1453
+ spinner.succeed("Rate limiter configured in app.ts");
1454
+ } else {
1455
+ spinner.warn("Could not find app.ts - rate limiter needs manual setup");
1456
+ }
1457
+ }
1458
+ if (resolvedModuleName === "docs") {
1459
+ spinner.start("Configuring docs routes...");
1460
+ const injected = await injectDocsRoutes(projectRoot, srcDir);
1461
+ if (injected) {
1462
+ spinner.succeed("Docs routes configured");
1463
+ } else {
1464
+ spinner.warn("Could not configure docs routes automatically");
1465
+ }
1466
+ const authInstalled = await isAuthModuleInstalled(projectRoot, srcDir);
1467
+ if (authInstalled) {
1468
+ spinner.start("Adding auth endpoints to API docs...");
1469
+ const authDocsInjected = await injectAuthDocs(projectRoot, srcDir);
1470
+ if (authDocsInjected) {
1471
+ spinner.succeed("Auth endpoints added to API docs");
1472
+ } else {
1473
+ spinner.warn("Could not update API docs automatically");
1474
+ }
1475
+ }
1476
+ }
1298
1477
  let envConfigKey = resolvedModuleName;
1299
1478
  if (resolvedModuleName === "mailer" && mailerProvider === "resend") {
1300
1479
  envConfigKey = "mailer-resend";
@@ -1323,49 +1502,35 @@ var add = async (moduleName) => {
1323
1502
  await updateEnvSchema(projectRoot, srcDir, envConfig.schemaFields);
1324
1503
  spinner.succeed("Environment configured");
1325
1504
  }
1326
- console.log(chalk4.green(`
1505
+ console.log(chalk6.green(`
1327
1506
  \u2714 ${resolvedModuleName} added successfully!
1328
1507
  `));
1329
- if (databaseBackupPath) {
1330
- console.log(chalk4.blue(`\u2139 Backup created at: ${databaseBackupPath}
1331
- `));
1332
- }
1333
1508
  const docsPath = getModuleDocsPath(resolvedModuleName);
1334
1509
  const docsUrl = `https://zuro-cli.devbybriyan.com/docs/${docsPath}`;
1335
- console.log(chalk4.blue(`\u2139 Docs: ${docsUrl}`));
1510
+ console.log(chalk6.blue(`\u2139 Docs: ${docsUrl}`));
1336
1511
  if (isDatabaseModule(resolvedModuleName)) {
1337
- if (usedDefaultDbUrl) {
1338
- console.log(chalk4.yellow("\u2139 Review DATABASE_URL in .env if your local DB config differs."));
1339
- }
1340
- const setupHint = getDatabaseSetupHint(
1341
- resolvedModuleName,
1342
- customDbUrl || DEFAULT_DATABASE_URLS[resolvedModuleName]
1343
- );
1344
- console.log(chalk4.yellow(`\u2139 Ensure DB exists: ${setupHint}`));
1345
- console.log(chalk4.yellow("\u2139 Run migrations: npx drizzle-kit generate && npx drizzle-kit migrate"));
1512
+ printDatabaseHints(resolvedModuleName, customDbUrl, usedDefaultDbUrl, databaseBackupPath);
1346
1513
  }
1347
1514
  if (resolvedModuleName === "auth") {
1348
- if (generatedAuthSecret) {
1349
- console.log(chalk4.yellow("\u2139 BETTER_AUTH_SECRET was generated automatically."));
1350
- } else {
1351
- console.log(chalk4.yellow("\u2139 Review BETTER_AUTH_SECRET and BETTER_AUTH_URL in .env."));
1352
- }
1353
- console.log(chalk4.yellow("\u2139 Run migrations: npx drizzle-kit generate && npx drizzle-kit migrate"));
1515
+ printAuthHints(generatedAuthSecret);
1354
1516
  }
1355
1517
  if (resolvedModuleName === "mailer") {
1356
- if (usedDefaultSmtp) {
1357
- console.log(chalk4.yellow("\u2139 Placeholder SMTP values added to .env \u2014 update them before sending emails."));
1358
- } else {
1359
- console.log(chalk4.yellow("\u2139 Review SMTP configuration in .env to ensure values are correct."));
1360
- }
1518
+ printMailerHints(usedDefaultSmtp);
1519
+ }
1520
+ if (resolvedModuleName === "docs") {
1521
+ printDocsHints();
1522
+ }
1523
+ if (resolvedModuleName === "auth" && shouldInstallDocsForAuth) {
1524
+ console.log(chalk6.blue("\n\u2139 Installing API docs module..."));
1525
+ await add("docs", { yes: true });
1361
1526
  }
1362
1527
  } catch (error) {
1363
- spinner.fail(chalk4.red(`Failed during ${currentStep}.`));
1528
+ spinner.fail(chalk6.red(`Failed during ${currentStep}.`));
1364
1529
  const errorMessage = error instanceof Error ? error.message : String(error);
1365
- console.error(chalk4.red(errorMessage));
1530
+ console.error(chalk6.red(errorMessage));
1366
1531
  console.log(`
1367
- ${chalk4.bold("Retry:")}`);
1368
- console.log(chalk4.cyan(` npx zuro-cli add ${resolvedModuleName}`));
1532
+ ${chalk6.bold("Retry:")}`);
1533
+ console.log(chalk6.cyan(` npx zuro-cli add ${resolvedModuleName}`));
1369
1534
  }
1370
1535
  };
1371
1536
 
@@ -1373,6 +1538,6 @@ ${chalk4.bold("Retry:")}`);
1373
1538
  var program = new Command();
1374
1539
  program.name("zuro-cli").description("Zuro CLI tool").version("0.0.1");
1375
1540
  program.command("init").description("Initialize a new Zuro project").action(init);
1376
- program.command("add <module>").description("Add a module to your project").action(add);
1541
+ program.command("add <module>").description("Add a module to your project").action((module, options) => add(module, options));
1377
1542
  program.parse(process.argv);
1378
1543
  //# sourceMappingURL=index.mjs.map