zuro-cli 0.0.2-beta.2 → 0.0.2-beta.4

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
@@ -227,8 +227,9 @@ async function installDependencies(pm, deps, cwd, options = {}) {
227
227
  import fs2 from "fs-extra";
228
228
  import path2 from "path";
229
229
  import os from "os";
230
- var updateEnvFile = async (cwd, variables, createIfMissing = true) => {
230
+ var updateEnvFile = async (cwd, variables, createIfMissing = true, options = {}) => {
231
231
  const envPath = path2.join(cwd, ".env");
232
+ const overwriteExisting = options.overwriteExisting ?? false;
232
233
  let content = "";
233
234
  if (fs2.existsSync(envPath)) {
234
235
  content = await fs2.readFile(envPath, "utf-8");
@@ -237,14 +238,25 @@ var updateEnvFile = async (cwd, variables, createIfMissing = true) => {
237
238
  }
238
239
  let modified = false;
239
240
  for (const [key, value] of Object.entries(variables)) {
240
- const regex = new RegExp(`^${key}=`, "m");
241
- if (!regex.test(content)) {
242
- if (content && !content.endsWith("\n")) {
243
- content += os.EOL;
241
+ const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
242
+ const regex = new RegExp(`^${escapedKey}=.*$`, "m");
243
+ if (regex.test(content)) {
244
+ if (!overwriteExisting) {
245
+ continue;
244
246
  }
245
- content += `${key}=${value}${os.EOL}`;
246
- modified = true;
247
+ const nextLine = `${key}=${value}`;
248
+ const updated = content.replace(regex, nextLine);
249
+ if (updated !== content) {
250
+ content = updated;
251
+ modified = true;
252
+ }
253
+ continue;
247
254
  }
255
+ if (content && !content.endsWith("\n")) {
256
+ content += os.EOL;
257
+ }
258
+ content += `${key}=${value}${os.EOL}`;
259
+ modified = true;
248
260
  }
249
261
  if (modified || !fs2.existsSync(envPath)) {
250
262
  await fs2.writeFile(envPath, content);
@@ -561,6 +573,7 @@ import prompts2 from "prompts";
561
573
  import ora2 from "ora";
562
574
  import path6 from "path";
563
575
  import fs6 from "fs-extra";
576
+ import { randomBytes } from "crypto";
564
577
 
565
578
  // src/utils/dependency.ts
566
579
  import fs5 from "fs-extra";
@@ -606,6 +619,10 @@ var resolveDependencies = async (moduleDependencies, cwd) => {
606
619
 
607
620
  // src/commands/add.ts
608
621
  import chalk4 from "chalk";
622
+ var DEFAULT_DATABASE_URLS = {
623
+ "database-pg": "postgresql://postgres@localhost:5432/mydb",
624
+ "database-mysql": "mysql://root@localhost:3306/mydb"
625
+ };
609
626
  function resolveSafeTargetPath2(projectRoot, srcDir, file) {
610
627
  const targetPath = path6.resolve(projectRoot, srcDir, file.target);
611
628
  const normalizedRoot = path6.resolve(projectRoot);
@@ -614,37 +631,165 @@ function resolveSafeTargetPath2(projectRoot, srcDir, file) {
614
631
  }
615
632
  return targetPath;
616
633
  }
634
+ function resolvePackageManager(projectRoot) {
635
+ if (fs6.existsSync(path6.join(projectRoot, "pnpm-lock.yaml"))) {
636
+ return "pnpm";
637
+ }
638
+ if (fs6.existsSync(path6.join(projectRoot, "bun.lockb")) || fs6.existsSync(path6.join(projectRoot, "bun.lock"))) {
639
+ return "bun";
640
+ }
641
+ if (fs6.existsSync(path6.join(projectRoot, "yarn.lock"))) {
642
+ return "yarn";
643
+ }
644
+ return "npm";
645
+ }
646
+ function parseDatabaseDialect(value) {
647
+ const normalized = value?.trim().toLowerCase();
648
+ if (!normalized) {
649
+ return null;
650
+ }
651
+ if (normalized === "pg" || normalized === "postgres" || normalized === "postgresql" || normalized === "database-pg") {
652
+ return "database-pg";
653
+ }
654
+ if (normalized === "mysql" || normalized === "database-mysql") {
655
+ return "database-mysql";
656
+ }
657
+ return null;
658
+ }
659
+ function isDatabaseModule(moduleName) {
660
+ return moduleName === "database-pg" || moduleName === "database-mysql";
661
+ }
662
+ function validateDatabaseUrl(rawUrl, moduleName) {
663
+ const dbUrl = rawUrl.trim();
664
+ if (!dbUrl) {
665
+ throw new Error("Database URL cannot be empty.");
666
+ }
667
+ let parsed;
668
+ try {
669
+ parsed = new URL(dbUrl);
670
+ } catch {
671
+ throw new Error(`Invalid database URL: '${dbUrl}'.`);
672
+ }
673
+ const protocol = parsed.protocol.toLowerCase();
674
+ if (moduleName === "database-pg" && protocol !== "postgresql:" && protocol !== "postgres:") {
675
+ throw new Error("PostgreSQL URL must start with postgres:// or postgresql://");
676
+ }
677
+ if (moduleName === "database-mysql" && protocol !== "mysql:") {
678
+ throw new Error("MySQL URL must start with mysql://");
679
+ }
680
+ return dbUrl;
681
+ }
682
+ async function detectInstalledDatabaseDialect(projectRoot, srcDir) {
683
+ const dbIndexPath = path6.join(projectRoot, srcDir, "db", "index.ts");
684
+ if (!fs6.existsSync(dbIndexPath)) {
685
+ return null;
686
+ }
687
+ const content = await fs6.readFile(dbIndexPath, "utf-8");
688
+ if (content.includes("drizzle-orm/node-postgres") || content.includes(`from "pg"`)) {
689
+ return "database-pg";
690
+ }
691
+ if (content.includes("drizzle-orm/mysql2") || content.includes(`from "mysql2`)) {
692
+ return "database-mysql";
693
+ }
694
+ return null;
695
+ }
696
+ async function backupDatabaseFiles(projectRoot, srcDir) {
697
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
698
+ const backupRoot = path6.join(projectRoot, ".zuro", "backups", `database-${timestamp}`);
699
+ const candidates = [
700
+ path6.join(projectRoot, srcDir, "db", "index.ts"),
701
+ path6.join(projectRoot, "drizzle.config.ts")
702
+ ];
703
+ let copied = false;
704
+ for (const filePath of candidates) {
705
+ if (!fs6.existsSync(filePath)) {
706
+ continue;
707
+ }
708
+ const relativePath = path6.relative(projectRoot, filePath);
709
+ const backupPath = path6.join(backupRoot, relativePath);
710
+ await fs6.ensureDir(path6.dirname(backupPath));
711
+ await fs6.copyFile(filePath, backupPath);
712
+ copied = true;
713
+ }
714
+ return copied ? backupRoot : null;
715
+ }
716
+ function databaseLabel(moduleName) {
717
+ return moduleName === "database-pg" ? "PostgreSQL" : "MySQL";
718
+ }
719
+ function getDatabaseSetupHint(moduleName, dbUrl) {
720
+ try {
721
+ const parsed = new URL(dbUrl);
722
+ const dbName = parsed.pathname.replace(/^\/+/, "") || "mydb";
723
+ if (moduleName === "database-pg") {
724
+ return `createdb ${dbName}`;
725
+ }
726
+ return `mysql -e "CREATE DATABASE IF NOT EXISTS ${dbName};"`;
727
+ } catch {
728
+ return moduleName === "database-pg" ? "createdb <database_name>" : `mysql -e "CREATE DATABASE IF NOT EXISTS <database_name>;"`;
729
+ }
730
+ }
731
+ function escapeRegex(value) {
732
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
733
+ }
734
+ async function hasEnvVariable(projectRoot, key) {
735
+ const envPath = path6.join(projectRoot, ".env");
736
+ if (!await fs6.pathExists(envPath)) {
737
+ return false;
738
+ }
739
+ const content = await fs6.readFile(envPath, "utf-8");
740
+ const pattern = new RegExp(`^${escapeRegex(key)}=`, "m");
741
+ return pattern.test(content);
742
+ }
617
743
  async function injectErrorHandler(projectRoot, srcDir) {
618
744
  const appPath = path6.join(projectRoot, srcDir, "app.ts");
619
745
  if (!fs6.existsSync(appPath)) {
620
746
  return false;
621
747
  }
622
748
  let content = await fs6.readFile(appPath, "utf-8");
623
- if (content.includes("errorHandler")) {
624
- return true;
625
- }
626
749
  const errorImport = `import { errorHandler, notFoundHandler } from "./middleware/error-handler";`;
627
- const importRegex = /^import .+ from .+;?\s*$/gm;
628
- let lastImportIndex = 0;
629
- let match;
630
- while ((match = importRegex.exec(content)) !== null) {
631
- lastImportIndex = match.index + match[0].length;
632
- }
633
- if (lastImportIndex > 0) {
634
- content = content.slice(0, lastImportIndex) + `
750
+ const hasErrorImport = content.includes(errorImport);
751
+ const hasNotFoundUse = /app\.use\(\s*notFoundHandler\s*\)/.test(content);
752
+ const hasErrorUse = /app\.use\(\s*errorHandler\s*\)/.test(content);
753
+ let modified = false;
754
+ let importInserted = hasErrorImport;
755
+ if (!hasErrorImport) {
756
+ const importRegex = /^import .+ from .+;?\s*$/gm;
757
+ let lastImportIndex = 0;
758
+ let match;
759
+ while ((match = importRegex.exec(content)) !== null) {
760
+ lastImportIndex = match.index + match[0].length;
761
+ }
762
+ if (lastImportIndex > 0) {
763
+ content = content.slice(0, lastImportIndex) + `
635
764
  ${errorImport}` + content.slice(lastImportIndex);
765
+ modified = true;
766
+ importInserted = true;
767
+ }
636
768
  }
637
- const errorSetup = `
769
+ let setupInserted = hasNotFoundUse && hasErrorUse;
770
+ if (!setupInserted) {
771
+ const setupLines = [];
772
+ if (!hasNotFoundUse) {
773
+ setupLines.push("app.use(notFoundHandler);");
774
+ }
775
+ if (!hasErrorUse) {
776
+ setupLines.push("app.use(errorHandler);");
777
+ }
778
+ const errorSetup = `
638
779
  // Error handling (must be last)
639
- app.use(notFoundHandler);
640
- app.use(errorHandler);
780
+ ${setupLines.join("\n")}
641
781
  `;
642
- const exportMatch = content.match(/export default app;?\s*$/m);
643
- if (exportMatch && exportMatch.index !== void 0) {
644
- content = content.slice(0, exportMatch.index) + errorSetup + "\n" + content.slice(exportMatch.index);
782
+ const exportMatch = content.match(/export default app;?\s*$/m);
783
+ if (exportMatch && exportMatch.index !== void 0) {
784
+ content = content.slice(0, exportMatch.index) + errorSetup + "\n" + content.slice(exportMatch.index);
785
+ modified = true;
786
+ setupInserted = true;
787
+ }
645
788
  }
646
- await fs6.writeFile(appPath, content);
647
- return true;
789
+ if (modified) {
790
+ await fs6.writeFile(appPath, content);
791
+ }
792
+ return importInserted && setupInserted;
648
793
  }
649
794
  async function injectAuthRoutes(projectRoot, srcDir) {
650
795
  const appPath = path6.join(projectRoot, srcDir, "app.ts");
@@ -652,33 +797,59 @@ async function injectAuthRoutes(projectRoot, srcDir) {
652
797
  return false;
653
798
  }
654
799
  let content = await fs6.readFile(appPath, "utf-8");
655
- if (content.includes("routes/auth.routes")) {
656
- return true;
657
- }
658
800
  const authImport = `import authRoutes from "./routes/auth.routes";`;
659
801
  const userImport = `import userRoutes from "./routes/user.routes";`;
660
- const routeSetup = `
802
+ const hasAuthImport = content.includes(authImport);
803
+ const hasUserImport = content.includes(userImport);
804
+ const hasAuthRoute = /app\.use\(\s*authRoutes\s*\)/.test(content);
805
+ const hasUserRoute = /app\.use\(\s*["']\/api\/users["']\s*,\s*userRoutes\s*\)/.test(content);
806
+ let modified = false;
807
+ let importsReady = hasAuthImport && hasUserImport;
808
+ if (!importsReady) {
809
+ const importRegex = /^import .+ from .+;?\s*$/gm;
810
+ let lastImportIndex = 0;
811
+ let match;
812
+ while ((match = importRegex.exec(content)) !== null) {
813
+ lastImportIndex = match.index + match[0].length;
814
+ }
815
+ if (lastImportIndex > 0) {
816
+ const missingImports = [];
817
+ if (!hasAuthImport) {
818
+ missingImports.push(authImport);
819
+ }
820
+ if (!hasUserImport) {
821
+ missingImports.push(userImport);
822
+ }
823
+ content = content.slice(0, lastImportIndex) + `
824
+ ${missingImports.join("\n")}` + content.slice(lastImportIndex);
825
+ modified = true;
826
+ importsReady = true;
827
+ }
828
+ }
829
+ let setupReady = hasAuthRoute && hasUserRoute;
830
+ if (!setupReady) {
831
+ const setupLines = [];
832
+ if (!hasAuthRoute) {
833
+ setupLines.push("app.use(authRoutes);");
834
+ }
835
+ if (!hasUserRoute) {
836
+ setupLines.push('app.use("/api/users", userRoutes);');
837
+ }
838
+ const routeSetup = `
661
839
  // Auth routes
662
- app.use(authRoutes);
663
- app.use("/api/users", userRoutes);
840
+ ${setupLines.join("\n")}
664
841
  `;
665
- const importRegex = /^import .+ from .+;?\s*$/gm;
666
- let lastImportIndex = 0;
667
- let match;
668
- while ((match = importRegex.exec(content)) !== null) {
669
- lastImportIndex = match.index + match[0].length;
670
- }
671
- if (lastImportIndex > 0) {
672
- content = content.slice(0, lastImportIndex) + `
673
- ${authImport}
674
- ${userImport}` + content.slice(lastImportIndex);
842
+ const exportMatch = content.match(/export default app;?\s*$/m);
843
+ if (exportMatch && exportMatch.index !== void 0) {
844
+ content = content.slice(0, exportMatch.index) + routeSetup + "\n" + content.slice(exportMatch.index);
845
+ modified = true;
846
+ setupReady = true;
847
+ }
675
848
  }
676
- const exportMatch = content.match(/export default app;?\s*$/m);
677
- if (exportMatch && exportMatch.index !== void 0) {
678
- content = content.slice(0, exportMatch.index) + routeSetup + "\n" + content.slice(exportMatch.index);
849
+ if (modified) {
850
+ await fs6.writeFile(appPath, content);
679
851
  }
680
- await fs6.writeFile(appPath, content);
681
- return true;
852
+ return importsReady && setupReady;
682
853
  }
683
854
  var add = async (moduleName) => {
684
855
  const projectRoot = process.cwd();
@@ -687,13 +858,18 @@ var add = async (moduleName) => {
687
858
  showNonZuroProjectMessage();
688
859
  return;
689
860
  }
690
- let srcDir = projectConfig.srcDir || "src";
861
+ const srcDir = projectConfig.srcDir || "src";
862
+ let resolvedModuleName = moduleName;
863
+ const parsedDialect = parseDatabaseDialect(moduleName);
864
+ if (parsedDialect) {
865
+ resolvedModuleName = parsedDialect;
866
+ }
691
867
  let customDbUrl;
692
- const DEFAULT_URLS = {
693
- "database-pg": "postgresql://root@localhost:5432/mydb",
694
- "database-mysql": "mysql://root@localhost:3306/mydb"
695
- };
696
- if (moduleName === "database") {
868
+ let usedDefaultDbUrl = false;
869
+ let databaseBackupPath = null;
870
+ let generatedAuthSecret = false;
871
+ let authDatabaseDialect = null;
872
+ if (resolvedModuleName === "database") {
697
873
  const variantResponse = await prompts2({
698
874
  type: "select",
699
875
  name: "variant",
@@ -704,22 +880,39 @@ var add = async (moduleName) => {
704
880
  ]
705
881
  });
706
882
  if (!variantResponse.variant) {
707
- process.exit(0);
883
+ console.log(chalk4.yellow("Operation cancelled."));
884
+ return;
708
885
  }
709
- moduleName = variantResponse.variant;
710
- const defaultUrl = DEFAULT_URLS[moduleName];
711
- console.log(chalk4.dim(` Tip: Leave blank to use ${defaultUrl}
712
- `));
713
- const urlResponse = await prompts2({
714
- type: "text",
715
- name: "dbUrl",
716
- message: "Database URL",
717
- initial: ""
718
- });
719
- customDbUrl = urlResponse.dbUrl?.trim() || defaultUrl;
886
+ resolvedModuleName = variantResponse.variant;
720
887
  }
721
- if ((moduleName === "database-pg" || moduleName === "database-mysql") && customDbUrl === void 0) {
722
- const defaultUrl = DEFAULT_URLS[moduleName];
888
+ if (isDatabaseModule(resolvedModuleName)) {
889
+ const installedDialect = await detectInstalledDatabaseDialect(projectRoot, srcDir);
890
+ if (installedDialect && installedDialect !== resolvedModuleName) {
891
+ console.log(
892
+ chalk4.yellow(
893
+ `
894
+ \u26A0 Existing database setup detected: ${databaseLabel(installedDialect)}.`
895
+ )
896
+ );
897
+ console.log(
898
+ chalk4.yellow(
899
+ ` Switching to ${databaseLabel(resolvedModuleName)} will overwrite db files and drizzle config.
900
+ `
901
+ )
902
+ );
903
+ const switchResponse = await prompts2({
904
+ type: "confirm",
905
+ name: "proceed",
906
+ message: "Continue and switch database dialect?",
907
+ initial: false
908
+ });
909
+ if (!switchResponse.proceed) {
910
+ console.log(chalk4.yellow("Operation cancelled."));
911
+ return;
912
+ }
913
+ databaseBackupPath = await backupDatabaseFiles(projectRoot, srcDir);
914
+ }
915
+ const defaultUrl = DEFAULT_DATABASE_URLS[resolvedModuleName];
723
916
  console.log(chalk4.dim(` Tip: Leave blank to use ${defaultUrl}
724
917
  `));
725
918
  const response = await prompts2({
@@ -728,44 +921,69 @@ var add = async (moduleName) => {
728
921
  message: "Database URL",
729
922
  initial: ""
730
923
  });
731
- customDbUrl = response.dbUrl?.trim() || defaultUrl;
924
+ if (response.dbUrl === void 0) {
925
+ console.log(chalk4.yellow("Operation cancelled."));
926
+ return;
927
+ }
928
+ const enteredUrl = response.dbUrl?.trim() || "";
929
+ usedDefaultDbUrl = enteredUrl.length === 0;
930
+ customDbUrl = validateDatabaseUrl(enteredUrl || defaultUrl, resolvedModuleName);
732
931
  }
733
- const spinner = ora2(`Checking registry for ${moduleName}...`).start();
932
+ const pm = resolvePackageManager(projectRoot);
933
+ const spinner = ora2(`Checking registry for ${resolvedModuleName}...`).start();
934
+ let currentStep = "package manager preflight";
734
935
  try {
936
+ spinner.text = `Checking ${pm} availability...`;
937
+ await ensurePackageManagerAvailable(pm);
938
+ currentStep = "registry fetch";
939
+ spinner.text = `Checking registry for ${resolvedModuleName}...`;
735
940
  const registryContext = await fetchRegistry();
736
- const module = registryContext.manifest.modules[moduleName];
941
+ const module = registryContext.manifest.modules[resolvedModuleName];
737
942
  if (!module) {
738
- spinner.fail(`Module '${moduleName}' not found.`);
943
+ spinner.fail(`Module '${resolvedModuleName}' not found.`);
739
944
  return;
740
945
  }
741
- spinner.succeed(`Found module: ${chalk4.cyan(moduleName)}`);
946
+ spinner.succeed(`Found module: ${chalk4.cyan(resolvedModuleName)}`);
742
947
  const moduleDeps = module.moduleDependencies || [];
948
+ currentStep = "module dependency resolution";
743
949
  await resolveDependencies(moduleDeps, projectRoot);
950
+ currentStep = "dependency installation";
744
951
  spinner.start("Installing dependencies...");
745
- let pm = "npm";
746
- if (fs6.existsSync(path6.join(projectRoot, "pnpm-lock.yaml"))) {
747
- pm = "pnpm";
748
- } else if (fs6.existsSync(path6.join(projectRoot, "bun.lockb"))) {
749
- pm = "bun";
750
- } else if (fs6.existsSync(path6.join(projectRoot, "yarn.lock"))) {
751
- pm = "yarn";
752
- }
753
952
  await installDependencies(pm, module.dependencies || [], projectRoot);
754
953
  await installDependencies(pm, module.devDependencies || [], projectRoot, { dev: true });
755
954
  spinner.succeed("Dependencies installed");
955
+ currentStep = "module scaffolding";
756
956
  spinner.start("Scaffolding files...");
957
+ if (resolvedModuleName === "auth") {
958
+ authDatabaseDialect = await detectInstalledDatabaseDialect(projectRoot, srcDir);
959
+ }
757
960
  for (const file of module.files) {
758
- const content = await fetchFile(file.path, {
961
+ let fetchPath = file.path;
962
+ let expectedSha256 = file.sha256;
963
+ let expectedSize = file.size;
964
+ if (resolvedModuleName === "auth" && file.target === "db/schema/auth.ts" && authDatabaseDialect === "database-mysql") {
965
+ fetchPath = "express/db/schema/auth.mysql.ts";
966
+ expectedSha256 = void 0;
967
+ expectedSize = void 0;
968
+ }
969
+ let content = await fetchFile(fetchPath, {
759
970
  baseUrl: registryContext.fileBaseUrl,
760
- expectedSha256: file.sha256,
761
- expectedSize: file.size
971
+ expectedSha256,
972
+ expectedSize
762
973
  });
974
+ if (isDatabaseModule(resolvedModuleName) && file.target === "../drizzle.config.ts") {
975
+ const normalizedSrcDir = srcDir.replace(/\\/g, "/");
976
+ content = content.replace(
977
+ /schema:\s*["'][^"']+["']/,
978
+ `schema: "./${normalizedSrcDir}/db/schema/*"`
979
+ );
980
+ }
763
981
  const targetPath = resolveSafeTargetPath2(projectRoot, srcDir, file);
764
982
  await fs6.ensureDir(path6.dirname(targetPath));
765
983
  await fs6.writeFile(targetPath, content);
766
984
  }
767
985
  spinner.succeed("Files generated");
768
- if (moduleName === "auth") {
986
+ if (resolvedModuleName === "auth") {
769
987
  spinner.start("Configuring routes in app.ts...");
770
988
  const injected = await injectAuthRoutes(projectRoot, srcDir);
771
989
  if (injected) {
@@ -774,7 +992,7 @@ var add = async (moduleName) => {
774
992
  spinner.warn("Could not find app.ts - routes need manual setup");
775
993
  }
776
994
  }
777
- if (moduleName === "error-handler") {
995
+ if (resolvedModuleName === "error-handler") {
778
996
  spinner.start("Configuring error handler in app.ts...");
779
997
  const injected = await injectErrorHandler(projectRoot, srcDir);
780
998
  if (injected) {
@@ -783,26 +1001,42 @@ var add = async (moduleName) => {
783
1001
  spinner.warn("Could not find app.ts - error handler needs manual setup");
784
1002
  }
785
1003
  }
786
- const envConfig = ENV_CONFIGS[moduleName];
1004
+ const envConfig = ENV_CONFIGS[resolvedModuleName];
787
1005
  if (envConfig) {
1006
+ currentStep = "environment configuration";
788
1007
  spinner.start("Updating environment configuration...");
789
1008
  const envVars = { ...envConfig.envVars };
790
- if (customDbUrl && (moduleName === "database-pg" || moduleName === "database-mysql")) {
1009
+ if (customDbUrl && isDatabaseModule(resolvedModuleName)) {
791
1010
  envVars.DATABASE_URL = customDbUrl;
792
1011
  }
793
- await updateEnvFile(projectRoot, envVars);
1012
+ if (resolvedModuleName === "auth") {
1013
+ const hasExistingSecret = await hasEnvVariable(projectRoot, "BETTER_AUTH_SECRET");
1014
+ if (!hasExistingSecret) {
1015
+ envVars.BETTER_AUTH_SECRET = randomBytes(32).toString("hex");
1016
+ generatedAuthSecret = true;
1017
+ }
1018
+ }
1019
+ await updateEnvFile(projectRoot, envVars, true, {
1020
+ overwriteExisting: isDatabaseModule(resolvedModuleName)
1021
+ });
794
1022
  await updateEnvSchema(projectRoot, srcDir, envConfig.schemaFields);
795
1023
  spinner.succeed("Environment configured");
796
1024
  }
797
1025
  console.log(chalk4.green(`
798
- \u2714 ${moduleName} added successfully!
1026
+ \u2714 ${resolvedModuleName} added successfully!
1027
+ `));
1028
+ if (databaseBackupPath) {
1029
+ console.log(chalk4.blue(`\u2139 Backup created at: ${databaseBackupPath}
799
1030
  `));
800
- if (moduleName === "auth") {
1031
+ }
1032
+ if (resolvedModuleName === "auth") {
801
1033
  console.log(chalk4.bold("\u{1F4CB} Next Steps:\n"));
802
- console.log(chalk4.yellow("1. Update your .env file:"));
803
- console.log(
804
- chalk4.dim(" We added placeholder values. Update BETTER_AUTH_SECRET with a secure key.\n")
805
- );
1034
+ if (generatedAuthSecret) {
1035
+ console.log(chalk4.yellow("1. BETTER_AUTH_SECRET generated automatically."));
1036
+ } else {
1037
+ console.log(chalk4.yellow("1. Review your auth env values in .env."));
1038
+ }
1039
+ console.log(chalk4.dim(" Make sure BETTER_AUTH_URL matches your API origin (for example http://localhost:3000).\n"));
806
1040
  console.log(chalk4.yellow("2. Run database migrations:"));
807
1041
  console.log(chalk4.cyan(" npx drizzle-kit generate"));
808
1042
  console.log(chalk4.cyan(" npx drizzle-kit migrate\n"));
@@ -811,7 +1045,7 @@ var add = async (moduleName) => {
811
1045
  console.log(chalk4.dim(" POST /auth/sign-in/email - Login"));
812
1046
  console.log(chalk4.dim(" POST /auth/sign-out - Logout"));
813
1047
  console.log(chalk4.dim(" GET /api/users/me - Current user\n"));
814
- } else if (moduleName === "error-handler") {
1048
+ } else if (resolvedModuleName === "error-handler") {
815
1049
  console.log(chalk4.bold("\u{1F4CB} Usage:\n"));
816
1050
  console.log(chalk4.yellow("Throw errors in your controllers:"));
817
1051
  console.log(chalk4.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
@@ -836,25 +1070,38 @@ var add = async (moduleName) => {
836
1070
  console.log(chalk4.white(" // errors auto-caught"));
837
1071
  console.log(chalk4.white(" }));"));
838
1072
  console.log(chalk4.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
839
- } else if (moduleName.includes("database")) {
1073
+ } else if (isDatabaseModule(resolvedModuleName)) {
840
1074
  console.log(chalk4.bold("\u{1F4CB} Next Steps:\n"));
841
1075
  let stepNum = 1;
842
- if (!customDbUrl) {
1076
+ if (usedDefaultDbUrl) {
843
1077
  console.log(chalk4.yellow(`${stepNum}. Update DATABASE_URL in .env:`));
844
1078
  console.log(
845
- chalk4.dim(" We added a placeholder. Update with your actual database credentials.\n")
1079
+ chalk4.dim(" We added a local default. Update it if your DB host/user/password differ.\n")
846
1080
  );
847
1081
  stepNum++;
848
1082
  }
849
- console.log(chalk4.yellow(`${stepNum}. Create schemas in src/db/schema/:`));
1083
+ console.log(chalk4.yellow(`${stepNum}. Create schemas in ${srcDir}/db/schema/:`));
850
1084
  console.log(chalk4.dim(" Add table files and export from index.ts\n"));
1085
+ stepNum++;
1086
+ const setupHint = getDatabaseSetupHint(
1087
+ resolvedModuleName,
1088
+ customDbUrl || DEFAULT_DATABASE_URLS[resolvedModuleName]
1089
+ );
1090
+ console.log(chalk4.yellow(`${stepNum}. Ensure the database exists:`));
1091
+ console.log(chalk4.cyan(` ${setupHint}
1092
+ `));
851
1093
  stepNum++;
852
1094
  console.log(chalk4.yellow(`${stepNum}. Run migrations:`));
853
1095
  console.log(chalk4.cyan(" npx drizzle-kit generate"));
854
1096
  console.log(chalk4.cyan(" npx drizzle-kit migrate\n"));
855
1097
  }
856
1098
  } catch (error) {
857
- spinner.fail(`Failed to add module: ${error.message}`);
1099
+ spinner.fail(chalk4.red(`Failed during ${currentStep}.`));
1100
+ const errorMessage = error instanceof Error ? error.message : String(error);
1101
+ console.error(chalk4.red(errorMessage));
1102
+ console.log(`
1103
+ ${chalk4.bold("Retry:")}`);
1104
+ console.log(chalk4.cyan(` npx zuro-cli add ${resolvedModuleName}`));
858
1105
  }
859
1106
  };
860
1107