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/README.md CHANGED
@@ -1,40 +1,44 @@
1
1
  # zuro-cli
2
2
 
3
- The backend builder for busy developers.
3
+ A CLI for scaffolding backend foundations and modules in your project.
4
4
 
5
- ## Usage
5
+ ## init
6
6
 
7
- ```bash
8
- npx zuro-cli@beta init
9
- ```
7
+ Use the `init` command to create a production-ready Express + TypeScript backend foundation.
10
8
 
11
- Add modules:
9
+ The `init` command installs core dependencies, creates base files, and prepares your project for module-based backend development.
12
10
 
13
11
  ```bash
14
- npx zuro-cli@beta add database
15
- npx zuro-cli@beta add auth
12
+ npx zuro-cli init
16
13
  ```
17
14
 
18
- ## Docs
19
-
20
- - https://zuro-cli.devbybriyan.com/docs
15
+ ## add
21
16
 
22
- ## Registry
17
+ Use the `add` command to add modules to your project.
23
18
 
24
- Default registry:
25
-
26
- - https://registry.devbybriyan.com/channels/stable.json
27
-
28
- Override for local testing:
19
+ The `add` command scaffolds module files, installs required dependencies, and updates relevant project setup.
29
20
 
30
21
  ```bash
31
- export ZURO_REGISTRY_URL=http://127.0.0.1:8787
22
+ npx zuro-cli add [module]
32
23
  ```
33
24
 
34
- ## Development
25
+ Example:
35
26
 
36
27
  ```bash
37
- corepack pnpm install
38
- corepack pnpm --filter zuro-cli build
39
- corepack pnpm --filter zuro-cli lint
28
+ npx zuro-cli add auth
40
29
  ```
30
+
31
+ Available modules include:
32
+
33
+ - `database`
34
+ - `auth`
35
+ - `validator`
36
+ - `error-handler`
37
+
38
+ ## Documentation
39
+
40
+ Visit https://zuro-cli.devbybriyan.com/docs to view the documentation.
41
+
42
+ ## License
43
+
44
+ Licensed under the MIT license.
package/dist/index.js CHANGED
@@ -250,8 +250,9 @@ async function installDependencies(pm, deps, cwd, options = {}) {
250
250
  var import_fs_extra2 = __toESM(require("fs-extra"));
251
251
  var import_path2 = __toESM(require("path"));
252
252
  var import_os = __toESM(require("os"));
253
- var updateEnvFile = async (cwd, variables, createIfMissing = true) => {
253
+ var updateEnvFile = async (cwd, variables, createIfMissing = true, options = {}) => {
254
254
  const envPath = import_path2.default.join(cwd, ".env");
255
+ const overwriteExisting = options.overwriteExisting ?? false;
255
256
  let content = "";
256
257
  if (import_fs_extra2.default.existsSync(envPath)) {
257
258
  content = await import_fs_extra2.default.readFile(envPath, "utf-8");
@@ -260,14 +261,25 @@ var updateEnvFile = async (cwd, variables, createIfMissing = true) => {
260
261
  }
261
262
  let modified = false;
262
263
  for (const [key, value] of Object.entries(variables)) {
263
- const regex = new RegExp(`^${key}=`, "m");
264
- if (!regex.test(content)) {
265
- if (content && !content.endsWith("\n")) {
266
- content += import_os.default.EOL;
264
+ const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
265
+ const regex = new RegExp(`^${escapedKey}=.*$`, "m");
266
+ if (regex.test(content)) {
267
+ if (!overwriteExisting) {
268
+ continue;
267
269
  }
268
- content += `${key}=${value}${import_os.default.EOL}`;
269
- modified = true;
270
+ const nextLine = `${key}=${value}`;
271
+ const updated = content.replace(regex, nextLine);
272
+ if (updated !== content) {
273
+ content = updated;
274
+ modified = true;
275
+ }
276
+ continue;
270
277
  }
278
+ if (content && !content.endsWith("\n")) {
279
+ content += import_os.default.EOL;
280
+ }
281
+ content += `${key}=${value}${import_os.default.EOL}`;
282
+ modified = true;
271
283
  }
272
284
  if (modified || !import_fs_extra2.default.existsSync(envPath)) {
273
285
  await import_fs_extra2.default.writeFile(envPath, content);
@@ -584,6 +596,7 @@ var import_prompts2 = __toESM(require("prompts"));
584
596
  var import_ora2 = __toESM(require("ora"));
585
597
  var import_path6 = __toESM(require("path"));
586
598
  var import_fs_extra6 = __toESM(require("fs-extra"));
599
+ var import_node_crypto2 = require("crypto");
587
600
 
588
601
  // src/utils/dependency.ts
589
602
  var import_fs_extra5 = __toESM(require("fs-extra"));
@@ -629,6 +642,10 @@ var resolveDependencies = async (moduleDependencies, cwd) => {
629
642
 
630
643
  // src/commands/add.ts
631
644
  var import_chalk4 = __toESM(require("chalk"));
645
+ var DEFAULT_DATABASE_URLS = {
646
+ "database-pg": "postgresql://postgres@localhost:5432/mydb",
647
+ "database-mysql": "mysql://root@localhost:3306/mydb"
648
+ };
632
649
  function resolveSafeTargetPath2(projectRoot, srcDir, file) {
633
650
  const targetPath = import_path6.default.resolve(projectRoot, srcDir, file.target);
634
651
  const normalizedRoot = import_path6.default.resolve(projectRoot);
@@ -637,37 +654,165 @@ function resolveSafeTargetPath2(projectRoot, srcDir, file) {
637
654
  }
638
655
  return targetPath;
639
656
  }
657
+ function resolvePackageManager(projectRoot) {
658
+ if (import_fs_extra6.default.existsSync(import_path6.default.join(projectRoot, "pnpm-lock.yaml"))) {
659
+ return "pnpm";
660
+ }
661
+ 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"))) {
662
+ return "bun";
663
+ }
664
+ if (import_fs_extra6.default.existsSync(import_path6.default.join(projectRoot, "yarn.lock"))) {
665
+ return "yarn";
666
+ }
667
+ return "npm";
668
+ }
669
+ function parseDatabaseDialect(value) {
670
+ const normalized = value?.trim().toLowerCase();
671
+ if (!normalized) {
672
+ return null;
673
+ }
674
+ if (normalized === "pg" || normalized === "postgres" || normalized === "postgresql" || normalized === "database-pg") {
675
+ return "database-pg";
676
+ }
677
+ if (normalized === "mysql" || normalized === "database-mysql") {
678
+ return "database-mysql";
679
+ }
680
+ return null;
681
+ }
682
+ function isDatabaseModule(moduleName) {
683
+ return moduleName === "database-pg" || moduleName === "database-mysql";
684
+ }
685
+ function validateDatabaseUrl(rawUrl, moduleName) {
686
+ const dbUrl = rawUrl.trim();
687
+ if (!dbUrl) {
688
+ throw new Error("Database URL cannot be empty.");
689
+ }
690
+ let parsed;
691
+ try {
692
+ parsed = new URL(dbUrl);
693
+ } catch {
694
+ throw new Error(`Invalid database URL: '${dbUrl}'.`);
695
+ }
696
+ const protocol = parsed.protocol.toLowerCase();
697
+ if (moduleName === "database-pg" && protocol !== "postgresql:" && protocol !== "postgres:") {
698
+ throw new Error("PostgreSQL URL must start with postgres:// or postgresql://");
699
+ }
700
+ if (moduleName === "database-mysql" && protocol !== "mysql:") {
701
+ throw new Error("MySQL URL must start with mysql://");
702
+ }
703
+ return dbUrl;
704
+ }
705
+ async function detectInstalledDatabaseDialect(projectRoot, srcDir) {
706
+ const dbIndexPath = import_path6.default.join(projectRoot, srcDir, "db", "index.ts");
707
+ if (!import_fs_extra6.default.existsSync(dbIndexPath)) {
708
+ return null;
709
+ }
710
+ const content = await import_fs_extra6.default.readFile(dbIndexPath, "utf-8");
711
+ if (content.includes("drizzle-orm/node-postgres") || content.includes(`from "pg"`)) {
712
+ return "database-pg";
713
+ }
714
+ if (content.includes("drizzle-orm/mysql2") || content.includes(`from "mysql2`)) {
715
+ return "database-mysql";
716
+ }
717
+ return null;
718
+ }
719
+ async function backupDatabaseFiles(projectRoot, srcDir) {
720
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
721
+ const backupRoot = import_path6.default.join(projectRoot, ".zuro", "backups", `database-${timestamp}`);
722
+ const candidates = [
723
+ import_path6.default.join(projectRoot, srcDir, "db", "index.ts"),
724
+ import_path6.default.join(projectRoot, "drizzle.config.ts")
725
+ ];
726
+ let copied = false;
727
+ for (const filePath of candidates) {
728
+ if (!import_fs_extra6.default.existsSync(filePath)) {
729
+ continue;
730
+ }
731
+ const relativePath = import_path6.default.relative(projectRoot, filePath);
732
+ const backupPath = import_path6.default.join(backupRoot, relativePath);
733
+ await import_fs_extra6.default.ensureDir(import_path6.default.dirname(backupPath));
734
+ await import_fs_extra6.default.copyFile(filePath, backupPath);
735
+ copied = true;
736
+ }
737
+ return copied ? backupRoot : null;
738
+ }
739
+ function databaseLabel(moduleName) {
740
+ return moduleName === "database-pg" ? "PostgreSQL" : "MySQL";
741
+ }
742
+ function getDatabaseSetupHint(moduleName, dbUrl) {
743
+ try {
744
+ const parsed = new URL(dbUrl);
745
+ const dbName = parsed.pathname.replace(/^\/+/, "") || "mydb";
746
+ if (moduleName === "database-pg") {
747
+ return `createdb ${dbName}`;
748
+ }
749
+ return `mysql -e "CREATE DATABASE IF NOT EXISTS ${dbName};"`;
750
+ } catch {
751
+ return moduleName === "database-pg" ? "createdb <database_name>" : `mysql -e "CREATE DATABASE IF NOT EXISTS <database_name>;"`;
752
+ }
753
+ }
754
+ function escapeRegex(value) {
755
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
756
+ }
757
+ async function hasEnvVariable(projectRoot, key) {
758
+ const envPath = import_path6.default.join(projectRoot, ".env");
759
+ if (!await import_fs_extra6.default.pathExists(envPath)) {
760
+ return false;
761
+ }
762
+ const content = await import_fs_extra6.default.readFile(envPath, "utf-8");
763
+ const pattern = new RegExp(`^${escapeRegex(key)}=`, "m");
764
+ return pattern.test(content);
765
+ }
640
766
  async function injectErrorHandler(projectRoot, srcDir) {
641
767
  const appPath = import_path6.default.join(projectRoot, srcDir, "app.ts");
642
768
  if (!import_fs_extra6.default.existsSync(appPath)) {
643
769
  return false;
644
770
  }
645
771
  let content = await import_fs_extra6.default.readFile(appPath, "utf-8");
646
- if (content.includes("errorHandler")) {
647
- return true;
648
- }
649
772
  const errorImport = `import { errorHandler, notFoundHandler } from "./middleware/error-handler";`;
650
- const importRegex = /^import .+ from .+;?\s*$/gm;
651
- let lastImportIndex = 0;
652
- let match;
653
- while ((match = importRegex.exec(content)) !== null) {
654
- lastImportIndex = match.index + match[0].length;
655
- }
656
- if (lastImportIndex > 0) {
657
- content = content.slice(0, lastImportIndex) + `
773
+ const hasErrorImport = content.includes(errorImport);
774
+ const hasNotFoundUse = /app\.use\(\s*notFoundHandler\s*\)/.test(content);
775
+ const hasErrorUse = /app\.use\(\s*errorHandler\s*\)/.test(content);
776
+ let modified = false;
777
+ let importInserted = hasErrorImport;
778
+ if (!hasErrorImport) {
779
+ const importRegex = /^import .+ from .+;?\s*$/gm;
780
+ let lastImportIndex = 0;
781
+ let match;
782
+ while ((match = importRegex.exec(content)) !== null) {
783
+ lastImportIndex = match.index + match[0].length;
784
+ }
785
+ if (lastImportIndex > 0) {
786
+ content = content.slice(0, lastImportIndex) + `
658
787
  ${errorImport}` + content.slice(lastImportIndex);
788
+ modified = true;
789
+ importInserted = true;
790
+ }
659
791
  }
660
- const errorSetup = `
792
+ let setupInserted = hasNotFoundUse && hasErrorUse;
793
+ if (!setupInserted) {
794
+ const setupLines = [];
795
+ if (!hasNotFoundUse) {
796
+ setupLines.push("app.use(notFoundHandler);");
797
+ }
798
+ if (!hasErrorUse) {
799
+ setupLines.push("app.use(errorHandler);");
800
+ }
801
+ const errorSetup = `
661
802
  // Error handling (must be last)
662
- app.use(notFoundHandler);
663
- app.use(errorHandler);
803
+ ${setupLines.join("\n")}
664
804
  `;
665
- const exportMatch = content.match(/export default app;?\s*$/m);
666
- if (exportMatch && exportMatch.index !== void 0) {
667
- content = content.slice(0, exportMatch.index) + errorSetup + "\n" + content.slice(exportMatch.index);
805
+ const exportMatch = content.match(/export default app;?\s*$/m);
806
+ if (exportMatch && exportMatch.index !== void 0) {
807
+ content = content.slice(0, exportMatch.index) + errorSetup + "\n" + content.slice(exportMatch.index);
808
+ modified = true;
809
+ setupInserted = true;
810
+ }
668
811
  }
669
- await import_fs_extra6.default.writeFile(appPath, content);
670
- return true;
812
+ if (modified) {
813
+ await import_fs_extra6.default.writeFile(appPath, content);
814
+ }
815
+ return importInserted && setupInserted;
671
816
  }
672
817
  async function injectAuthRoutes(projectRoot, srcDir) {
673
818
  const appPath = import_path6.default.join(projectRoot, srcDir, "app.ts");
@@ -675,33 +820,59 @@ async function injectAuthRoutes(projectRoot, srcDir) {
675
820
  return false;
676
821
  }
677
822
  let content = await import_fs_extra6.default.readFile(appPath, "utf-8");
678
- if (content.includes("routes/auth.routes")) {
679
- return true;
680
- }
681
823
  const authImport = `import authRoutes from "./routes/auth.routes";`;
682
824
  const userImport = `import userRoutes from "./routes/user.routes";`;
683
- const routeSetup = `
825
+ const hasAuthImport = content.includes(authImport);
826
+ const hasUserImport = content.includes(userImport);
827
+ const hasAuthRoute = /app\.use\(\s*authRoutes\s*\)/.test(content);
828
+ const hasUserRoute = /app\.use\(\s*["']\/api\/users["']\s*,\s*userRoutes\s*\)/.test(content);
829
+ let modified = false;
830
+ let importsReady = hasAuthImport && hasUserImport;
831
+ if (!importsReady) {
832
+ const importRegex = /^import .+ from .+;?\s*$/gm;
833
+ let lastImportIndex = 0;
834
+ let match;
835
+ while ((match = importRegex.exec(content)) !== null) {
836
+ lastImportIndex = match.index + match[0].length;
837
+ }
838
+ if (lastImportIndex > 0) {
839
+ const missingImports = [];
840
+ if (!hasAuthImport) {
841
+ missingImports.push(authImport);
842
+ }
843
+ if (!hasUserImport) {
844
+ missingImports.push(userImport);
845
+ }
846
+ content = content.slice(0, lastImportIndex) + `
847
+ ${missingImports.join("\n")}` + content.slice(lastImportIndex);
848
+ modified = true;
849
+ importsReady = true;
850
+ }
851
+ }
852
+ let setupReady = hasAuthRoute && hasUserRoute;
853
+ if (!setupReady) {
854
+ const setupLines = [];
855
+ if (!hasAuthRoute) {
856
+ setupLines.push("app.use(authRoutes);");
857
+ }
858
+ if (!hasUserRoute) {
859
+ setupLines.push('app.use("/api/users", userRoutes);');
860
+ }
861
+ const routeSetup = `
684
862
  // Auth routes
685
- app.use(authRoutes);
686
- app.use("/api/users", userRoutes);
863
+ ${setupLines.join("\n")}
687
864
  `;
688
- const importRegex = /^import .+ from .+;?\s*$/gm;
689
- let lastImportIndex = 0;
690
- let match;
691
- while ((match = importRegex.exec(content)) !== null) {
692
- lastImportIndex = match.index + match[0].length;
693
- }
694
- if (lastImportIndex > 0) {
695
- content = content.slice(0, lastImportIndex) + `
696
- ${authImport}
697
- ${userImport}` + content.slice(lastImportIndex);
865
+ const exportMatch = content.match(/export default app;?\s*$/m);
866
+ if (exportMatch && exportMatch.index !== void 0) {
867
+ content = content.slice(0, exportMatch.index) + routeSetup + "\n" + content.slice(exportMatch.index);
868
+ modified = true;
869
+ setupReady = true;
870
+ }
698
871
  }
699
- const exportMatch = content.match(/export default app;?\s*$/m);
700
- if (exportMatch && exportMatch.index !== void 0) {
701
- content = content.slice(0, exportMatch.index) + routeSetup + "\n" + content.slice(exportMatch.index);
872
+ if (modified) {
873
+ await import_fs_extra6.default.writeFile(appPath, content);
702
874
  }
703
- await import_fs_extra6.default.writeFile(appPath, content);
704
- return true;
875
+ return importsReady && setupReady;
705
876
  }
706
877
  var add = async (moduleName) => {
707
878
  const projectRoot = process.cwd();
@@ -710,13 +881,18 @@ var add = async (moduleName) => {
710
881
  showNonZuroProjectMessage();
711
882
  return;
712
883
  }
713
- let srcDir = projectConfig.srcDir || "src";
884
+ const srcDir = projectConfig.srcDir || "src";
885
+ let resolvedModuleName = moduleName;
886
+ const parsedDialect = parseDatabaseDialect(moduleName);
887
+ if (parsedDialect) {
888
+ resolvedModuleName = parsedDialect;
889
+ }
714
890
  let customDbUrl;
715
- const DEFAULT_URLS = {
716
- "database-pg": "postgresql://root@localhost:5432/mydb",
717
- "database-mysql": "mysql://root@localhost:3306/mydb"
718
- };
719
- if (moduleName === "database") {
891
+ let usedDefaultDbUrl = false;
892
+ let databaseBackupPath = null;
893
+ let generatedAuthSecret = false;
894
+ let authDatabaseDialect = null;
895
+ if (resolvedModuleName === "database") {
720
896
  const variantResponse = await (0, import_prompts2.default)({
721
897
  type: "select",
722
898
  name: "variant",
@@ -727,22 +903,39 @@ var add = async (moduleName) => {
727
903
  ]
728
904
  });
729
905
  if (!variantResponse.variant) {
730
- process.exit(0);
906
+ console.log(import_chalk4.default.yellow("Operation cancelled."));
907
+ return;
731
908
  }
732
- moduleName = variantResponse.variant;
733
- const defaultUrl = DEFAULT_URLS[moduleName];
734
- console.log(import_chalk4.default.dim(` Tip: Leave blank to use ${defaultUrl}
735
- `));
736
- const urlResponse = await (0, import_prompts2.default)({
737
- type: "text",
738
- name: "dbUrl",
739
- message: "Database URL",
740
- initial: ""
741
- });
742
- customDbUrl = urlResponse.dbUrl?.trim() || defaultUrl;
909
+ resolvedModuleName = variantResponse.variant;
743
910
  }
744
- if ((moduleName === "database-pg" || moduleName === "database-mysql") && customDbUrl === void 0) {
745
- const defaultUrl = DEFAULT_URLS[moduleName];
911
+ if (isDatabaseModule(resolvedModuleName)) {
912
+ const installedDialect = await detectInstalledDatabaseDialect(projectRoot, srcDir);
913
+ if (installedDialect && installedDialect !== resolvedModuleName) {
914
+ console.log(
915
+ import_chalk4.default.yellow(
916
+ `
917
+ \u26A0 Existing database setup detected: ${databaseLabel(installedDialect)}.`
918
+ )
919
+ );
920
+ console.log(
921
+ import_chalk4.default.yellow(
922
+ ` Switching to ${databaseLabel(resolvedModuleName)} will overwrite db files and drizzle config.
923
+ `
924
+ )
925
+ );
926
+ const switchResponse = await (0, import_prompts2.default)({
927
+ type: "confirm",
928
+ name: "proceed",
929
+ message: "Continue and switch database dialect?",
930
+ initial: false
931
+ });
932
+ if (!switchResponse.proceed) {
933
+ console.log(import_chalk4.default.yellow("Operation cancelled."));
934
+ return;
935
+ }
936
+ databaseBackupPath = await backupDatabaseFiles(projectRoot, srcDir);
937
+ }
938
+ const defaultUrl = DEFAULT_DATABASE_URLS[resolvedModuleName];
746
939
  console.log(import_chalk4.default.dim(` Tip: Leave blank to use ${defaultUrl}
747
940
  `));
748
941
  const response = await (0, import_prompts2.default)({
@@ -751,44 +944,69 @@ var add = async (moduleName) => {
751
944
  message: "Database URL",
752
945
  initial: ""
753
946
  });
754
- customDbUrl = response.dbUrl?.trim() || defaultUrl;
947
+ if (response.dbUrl === void 0) {
948
+ console.log(import_chalk4.default.yellow("Operation cancelled."));
949
+ return;
950
+ }
951
+ const enteredUrl = response.dbUrl?.trim() || "";
952
+ usedDefaultDbUrl = enteredUrl.length === 0;
953
+ customDbUrl = validateDatabaseUrl(enteredUrl || defaultUrl, resolvedModuleName);
755
954
  }
756
- const spinner = (0, import_ora2.default)(`Checking registry for ${moduleName}...`).start();
955
+ const pm = resolvePackageManager(projectRoot);
956
+ const spinner = (0, import_ora2.default)(`Checking registry for ${resolvedModuleName}...`).start();
957
+ let currentStep = "package manager preflight";
757
958
  try {
959
+ spinner.text = `Checking ${pm} availability...`;
960
+ await ensurePackageManagerAvailable(pm);
961
+ currentStep = "registry fetch";
962
+ spinner.text = `Checking registry for ${resolvedModuleName}...`;
758
963
  const registryContext = await fetchRegistry();
759
- const module2 = registryContext.manifest.modules[moduleName];
964
+ const module2 = registryContext.manifest.modules[resolvedModuleName];
760
965
  if (!module2) {
761
- spinner.fail(`Module '${moduleName}' not found.`);
966
+ spinner.fail(`Module '${resolvedModuleName}' not found.`);
762
967
  return;
763
968
  }
764
- spinner.succeed(`Found module: ${import_chalk4.default.cyan(moduleName)}`);
969
+ spinner.succeed(`Found module: ${import_chalk4.default.cyan(resolvedModuleName)}`);
765
970
  const moduleDeps = module2.moduleDependencies || [];
971
+ currentStep = "module dependency resolution";
766
972
  await resolveDependencies(moduleDeps, projectRoot);
973
+ currentStep = "dependency installation";
767
974
  spinner.start("Installing dependencies...");
768
- let pm = "npm";
769
- if (import_fs_extra6.default.existsSync(import_path6.default.join(projectRoot, "pnpm-lock.yaml"))) {
770
- pm = "pnpm";
771
- } else if (import_fs_extra6.default.existsSync(import_path6.default.join(projectRoot, "bun.lockb"))) {
772
- pm = "bun";
773
- } else if (import_fs_extra6.default.existsSync(import_path6.default.join(projectRoot, "yarn.lock"))) {
774
- pm = "yarn";
775
- }
776
975
  await installDependencies(pm, module2.dependencies || [], projectRoot);
777
976
  await installDependencies(pm, module2.devDependencies || [], projectRoot, { dev: true });
778
977
  spinner.succeed("Dependencies installed");
978
+ currentStep = "module scaffolding";
779
979
  spinner.start("Scaffolding files...");
980
+ if (resolvedModuleName === "auth") {
981
+ authDatabaseDialect = await detectInstalledDatabaseDialect(projectRoot, srcDir);
982
+ }
780
983
  for (const file of module2.files) {
781
- const content = await fetchFile(file.path, {
984
+ let fetchPath = file.path;
985
+ let expectedSha256 = file.sha256;
986
+ let expectedSize = file.size;
987
+ if (resolvedModuleName === "auth" && file.target === "db/schema/auth.ts" && authDatabaseDialect === "database-mysql") {
988
+ fetchPath = "express/db/schema/auth.mysql.ts";
989
+ expectedSha256 = void 0;
990
+ expectedSize = void 0;
991
+ }
992
+ let content = await fetchFile(fetchPath, {
782
993
  baseUrl: registryContext.fileBaseUrl,
783
- expectedSha256: file.sha256,
784
- expectedSize: file.size
994
+ expectedSha256,
995
+ expectedSize
785
996
  });
997
+ if (isDatabaseModule(resolvedModuleName) && file.target === "../drizzle.config.ts") {
998
+ const normalizedSrcDir = srcDir.replace(/\\/g, "/");
999
+ content = content.replace(
1000
+ /schema:\s*["'][^"']+["']/,
1001
+ `schema: "./${normalizedSrcDir}/db/schema/*"`
1002
+ );
1003
+ }
786
1004
  const targetPath = resolveSafeTargetPath2(projectRoot, srcDir, file);
787
1005
  await import_fs_extra6.default.ensureDir(import_path6.default.dirname(targetPath));
788
1006
  await import_fs_extra6.default.writeFile(targetPath, content);
789
1007
  }
790
1008
  spinner.succeed("Files generated");
791
- if (moduleName === "auth") {
1009
+ if (resolvedModuleName === "auth") {
792
1010
  spinner.start("Configuring routes in app.ts...");
793
1011
  const injected = await injectAuthRoutes(projectRoot, srcDir);
794
1012
  if (injected) {
@@ -797,7 +1015,7 @@ var add = async (moduleName) => {
797
1015
  spinner.warn("Could not find app.ts - routes need manual setup");
798
1016
  }
799
1017
  }
800
- if (moduleName === "error-handler") {
1018
+ if (resolvedModuleName === "error-handler") {
801
1019
  spinner.start("Configuring error handler in app.ts...");
802
1020
  const injected = await injectErrorHandler(projectRoot, srcDir);
803
1021
  if (injected) {
@@ -806,26 +1024,42 @@ var add = async (moduleName) => {
806
1024
  spinner.warn("Could not find app.ts - error handler needs manual setup");
807
1025
  }
808
1026
  }
809
- const envConfig = ENV_CONFIGS[moduleName];
1027
+ const envConfig = ENV_CONFIGS[resolvedModuleName];
810
1028
  if (envConfig) {
1029
+ currentStep = "environment configuration";
811
1030
  spinner.start("Updating environment configuration...");
812
1031
  const envVars = { ...envConfig.envVars };
813
- if (customDbUrl && (moduleName === "database-pg" || moduleName === "database-mysql")) {
1032
+ if (customDbUrl && isDatabaseModule(resolvedModuleName)) {
814
1033
  envVars.DATABASE_URL = customDbUrl;
815
1034
  }
816
- await updateEnvFile(projectRoot, envVars);
1035
+ if (resolvedModuleName === "auth") {
1036
+ const hasExistingSecret = await hasEnvVariable(projectRoot, "BETTER_AUTH_SECRET");
1037
+ if (!hasExistingSecret) {
1038
+ envVars.BETTER_AUTH_SECRET = (0, import_node_crypto2.randomBytes)(32).toString("hex");
1039
+ generatedAuthSecret = true;
1040
+ }
1041
+ }
1042
+ await updateEnvFile(projectRoot, envVars, true, {
1043
+ overwriteExisting: isDatabaseModule(resolvedModuleName)
1044
+ });
817
1045
  await updateEnvSchema(projectRoot, srcDir, envConfig.schemaFields);
818
1046
  spinner.succeed("Environment configured");
819
1047
  }
820
1048
  console.log(import_chalk4.default.green(`
821
- \u2714 ${moduleName} added successfully!
1049
+ \u2714 ${resolvedModuleName} added successfully!
1050
+ `));
1051
+ if (databaseBackupPath) {
1052
+ console.log(import_chalk4.default.blue(`\u2139 Backup created at: ${databaseBackupPath}
822
1053
  `));
823
- if (moduleName === "auth") {
1054
+ }
1055
+ if (resolvedModuleName === "auth") {
824
1056
  console.log(import_chalk4.default.bold("\u{1F4CB} Next Steps:\n"));
825
- console.log(import_chalk4.default.yellow("1. Update your .env file:"));
826
- console.log(
827
- import_chalk4.default.dim(" We added placeholder values. Update BETTER_AUTH_SECRET with a secure key.\n")
828
- );
1057
+ if (generatedAuthSecret) {
1058
+ console.log(import_chalk4.default.yellow("1. BETTER_AUTH_SECRET generated automatically."));
1059
+ } else {
1060
+ console.log(import_chalk4.default.yellow("1. Review your auth env values in .env."));
1061
+ }
1062
+ console.log(import_chalk4.default.dim(" Make sure BETTER_AUTH_URL matches your API origin (for example http://localhost:3000).\n"));
829
1063
  console.log(import_chalk4.default.yellow("2. Run database migrations:"));
830
1064
  console.log(import_chalk4.default.cyan(" npx drizzle-kit generate"));
831
1065
  console.log(import_chalk4.default.cyan(" npx drizzle-kit migrate\n"));
@@ -834,7 +1068,7 @@ var add = async (moduleName) => {
834
1068
  console.log(import_chalk4.default.dim(" POST /auth/sign-in/email - Login"));
835
1069
  console.log(import_chalk4.default.dim(" POST /auth/sign-out - Logout"));
836
1070
  console.log(import_chalk4.default.dim(" GET /api/users/me - Current user\n"));
837
- } else if (moduleName === "error-handler") {
1071
+ } else if (resolvedModuleName === "error-handler") {
838
1072
  console.log(import_chalk4.default.bold("\u{1F4CB} Usage:\n"));
839
1073
  console.log(import_chalk4.default.yellow("Throw errors in your controllers:"));
840
1074
  console.log(import_chalk4.default.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"));
@@ -859,25 +1093,38 @@ var add = async (moduleName) => {
859
1093
  console.log(import_chalk4.default.white(" // errors auto-caught"));
860
1094
  console.log(import_chalk4.default.white(" }));"));
861
1095
  console.log(import_chalk4.default.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"));
862
- } else if (moduleName.includes("database")) {
1096
+ } else if (isDatabaseModule(resolvedModuleName)) {
863
1097
  console.log(import_chalk4.default.bold("\u{1F4CB} Next Steps:\n"));
864
1098
  let stepNum = 1;
865
- if (!customDbUrl) {
1099
+ if (usedDefaultDbUrl) {
866
1100
  console.log(import_chalk4.default.yellow(`${stepNum}. Update DATABASE_URL in .env:`));
867
1101
  console.log(
868
- import_chalk4.default.dim(" We added a placeholder. Update with your actual database credentials.\n")
1102
+ import_chalk4.default.dim(" We added a local default. Update it if your DB host/user/password differ.\n")
869
1103
  );
870
1104
  stepNum++;
871
1105
  }
872
- console.log(import_chalk4.default.yellow(`${stepNum}. Create schemas in src/db/schema/:`));
1106
+ console.log(import_chalk4.default.yellow(`${stepNum}. Create schemas in ${srcDir}/db/schema/:`));
873
1107
  console.log(import_chalk4.default.dim(" Add table files and export from index.ts\n"));
1108
+ stepNum++;
1109
+ const setupHint = getDatabaseSetupHint(
1110
+ resolvedModuleName,
1111
+ customDbUrl || DEFAULT_DATABASE_URLS[resolvedModuleName]
1112
+ );
1113
+ console.log(import_chalk4.default.yellow(`${stepNum}. Ensure the database exists:`));
1114
+ console.log(import_chalk4.default.cyan(` ${setupHint}
1115
+ `));
874
1116
  stepNum++;
875
1117
  console.log(import_chalk4.default.yellow(`${stepNum}. Run migrations:`));
876
1118
  console.log(import_chalk4.default.cyan(" npx drizzle-kit generate"));
877
1119
  console.log(import_chalk4.default.cyan(" npx drizzle-kit migrate\n"));
878
1120
  }
879
1121
  } catch (error) {
880
- spinner.fail(`Failed to add module: ${error.message}`);
1122
+ spinner.fail(import_chalk4.default.red(`Failed during ${currentStep}.`));
1123
+ const errorMessage = error instanceof Error ? error.message : String(error);
1124
+ console.error(import_chalk4.default.red(errorMessage));
1125
+ console.log(`
1126
+ ${import_chalk4.default.bold("Retry:")}`);
1127
+ console.log(import_chalk4.default.cyan(` npx zuro-cli add ${resolvedModuleName}`));
881
1128
  }
882
1129
  };
883
1130