zuro-cli 0.0.2-beta.3 → 0.0.2-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -202,18 +202,23 @@ async function ensurePackageManagerAvailable(pm) {
202
202
  );
203
203
  }
204
204
  }
205
- async function initPackageJson(cwd, force = false, packageName = "zuro-app", srcDir = "src") {
205
+ async function initPackageJson(cwd, force = false, packageName = "zuro-app", srcDir = "src", options = {}) {
206
206
  const pkgPath = import_path.default.join(cwd, "package.json");
207
207
  if (force || !await import_fs_extra.default.pathExists(pkgPath)) {
208
+ const scripts = {
209
+ "dev": `tsx watch ${srcDir}/server.ts`,
210
+ "build": "tsc",
211
+ "start": "node dist/server.js"
212
+ };
213
+ if (options.enablePrettier) {
214
+ scripts["format"] = "prettier --write .";
215
+ scripts["format:check"] = "prettier --check .";
216
+ }
208
217
  await import_fs_extra.default.writeJson(pkgPath, {
209
218
  name: normalizePackageName(packageName),
210
219
  version: "0.0.1",
211
220
  private: true,
212
- scripts: {
213
- "dev": `tsx watch ${srcDir}/server.ts`,
214
- "build": "tsc",
215
- "start": "node dist/server.js"
216
- }
221
+ scripts
217
222
  }, { spaces: 2 });
218
223
  }
219
224
  }
@@ -250,8 +255,9 @@ async function installDependencies(pm, deps, cwd, options = {}) {
250
255
  var import_fs_extra2 = __toESM(require("fs-extra"));
251
256
  var import_path2 = __toESM(require("path"));
252
257
  var import_os = __toESM(require("os"));
253
- var updateEnvFile = async (cwd, variables, createIfMissing = true) => {
258
+ var updateEnvFile = async (cwd, variables, createIfMissing = true, options = {}) => {
254
259
  const envPath = import_path2.default.join(cwd, ".env");
260
+ const overwriteExisting = options.overwriteExisting ?? false;
255
261
  let content = "";
256
262
  if (import_fs_extra2.default.existsSync(envPath)) {
257
263
  content = await import_fs_extra2.default.readFile(envPath, "utf-8");
@@ -260,14 +266,25 @@ var updateEnvFile = async (cwd, variables, createIfMissing = true) => {
260
266
  }
261
267
  let modified = false;
262
268
  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;
269
+ const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
270
+ const regex = new RegExp(`^${escapedKey}=.*$`, "m");
271
+ if (regex.test(content)) {
272
+ if (!overwriteExisting) {
273
+ continue;
267
274
  }
268
- content += `${key}=${value}${import_os.default.EOL}`;
269
- modified = true;
275
+ const nextLine = `${key}=${value}`;
276
+ const updated = content.replace(regex, nextLine);
277
+ if (updated !== content) {
278
+ content = updated;
279
+ modified = true;
280
+ }
281
+ continue;
270
282
  }
283
+ if (content && !content.endsWith("\n")) {
284
+ content += import_os.default.EOL;
285
+ }
286
+ content += `${key}=${value}${import_os.default.EOL}`;
287
+ modified = true;
271
288
  }
272
289
  if (modified || !import_fs_extra2.default.existsSync(envPath)) {
273
290
  await import_fs_extra2.default.writeFile(envPath, content);
@@ -390,6 +407,13 @@ function showNonZuroProjectMessage() {
390
407
  console.log("- a fresh/empty directory, or");
391
408
  console.log("- an existing project already managed by Zuro CLI.");
392
409
  }
410
+ function showInitFirstMessage() {
411
+ console.log(import_chalk.default.yellow("No Zuro project found in this directory."));
412
+ console.log("");
413
+ console.log(import_chalk.default.yellow("Run init first, then add modules."));
414
+ console.log("");
415
+ console.log(import_chalk.default.cyan("npx zuro-cli init"));
416
+ }
393
417
 
394
418
  // src/commands/init.ts
395
419
  function resolveSafeTargetPath(projectRoot, srcDir, file) {
@@ -419,6 +443,33 @@ async function ensureSafeTargetDirectory(targetDir, cwd, projectName) {
419
443
  });
420
444
  return response.proceed === true;
421
445
  }
446
+ async function setupPrettier(targetDir) {
447
+ const prettierConfigPath = import_path4.default.join(targetDir, ".prettierrc");
448
+ const prettierIgnorePath = import_path4.default.join(targetDir, ".prettierignore");
449
+ if (!await import_fs_extra4.default.pathExists(prettierConfigPath)) {
450
+ const prettierConfig = {
451
+ semi: true,
452
+ singleQuote: false,
453
+ trailingComma: "es5",
454
+ printWidth: 100,
455
+ tabWidth: 2
456
+ };
457
+ await import_fs_extra4.default.writeJson(prettierConfigPath, prettierConfig, { spaces: 2 });
458
+ }
459
+ if (!await import_fs_extra4.default.pathExists(prettierIgnorePath)) {
460
+ const ignoreContent = `node_modules
461
+ dist
462
+ build
463
+ coverage
464
+ .next
465
+ pnpm-lock.yaml
466
+ package-lock.json
467
+ bun.lock
468
+ bun.lockb
469
+ `;
470
+ await import_fs_extra4.default.writeFile(prettierIgnorePath, ignoreContent);
471
+ }
472
+ }
422
473
  async function init() {
423
474
  const cwd = process.cwd();
424
475
  const isExistingProject = await import_fs_extra4.default.pathExists(import_path4.default.join(cwd, "package.json"));
@@ -431,6 +482,7 @@ async function init() {
431
482
  let pm = "npm";
432
483
  let srcDir = "src";
433
484
  let projectName = import_path4.default.basename(cwd);
485
+ let enablePrettier = false;
434
486
  if (isExistingProject) {
435
487
  console.log(import_chalk2.default.blue("\u2139 Existing project detected."));
436
488
  projectName = import_path4.default.basename(cwd);
@@ -468,6 +520,12 @@ async function init() {
468
520
  { title: "bun", value: "bun" }
469
521
  ],
470
522
  initial: 0
523
+ },
524
+ {
525
+ type: "confirm",
526
+ name: "prettier",
527
+ message: "Setup Prettier?",
528
+ initial: true
471
529
  }
472
530
  ]);
473
531
  if (response.pm === void 0) {
@@ -475,6 +533,7 @@ async function init() {
475
533
  return;
476
534
  }
477
535
  pm = response.pm;
536
+ enablePrettier = response.prettier === true;
478
537
  srcDir = "src";
479
538
  if (!response.path || response.path.trim() === "") {
480
539
  projectName = import_path4.default.basename(cwd);
@@ -513,7 +572,7 @@ async function init() {
513
572
  spinner.text = "Initializing project...";
514
573
  const hasPackageJson = await import_fs_extra4.default.pathExists(import_path4.default.join(targetDir, "package.json"));
515
574
  if (!hasPackageJson) {
516
- await initPackageJson(targetDir, true, projectName, srcDir);
575
+ await initPackageJson(targetDir, true, projectName, srcDir, { enablePrettier });
517
576
  }
518
577
  currentStep = "dependency installation";
519
578
  spinner.text = `Installing dependencies using ${pm}...`;
@@ -530,6 +589,9 @@ async function init() {
530
589
  }
531
590
  await installDependencies(pm, runtimeDeps, targetDir);
532
591
  await installDependencies(pm, devDeps, targetDir, { dev: true });
592
+ if (enablePrettier) {
593
+ await installDependencies(pm, ["prettier"], targetDir, { dev: true });
594
+ }
533
595
  currentStep = "module file generation";
534
596
  spinner.text = "Fetching core module files...";
535
597
  for (const file of coreModule.files) {
@@ -555,6 +617,10 @@ async function init() {
555
617
  }
556
618
  currentStep = "environment file setup";
557
619
  await createInitialEnv(targetDir);
620
+ if (enablePrettier) {
621
+ currentStep = "prettier setup";
622
+ await setupPrettier(targetDir);
623
+ }
558
624
  currentStep = "config write";
559
625
  await writeZuroConfig(targetDir, zuroConfig);
560
626
  spinner.succeed(import_chalk2.default.green("Project initialized successfully!"));
@@ -584,6 +650,7 @@ var import_prompts2 = __toESM(require("prompts"));
584
650
  var import_ora2 = __toESM(require("ora"));
585
651
  var import_path6 = __toESM(require("path"));
586
652
  var import_fs_extra6 = __toESM(require("fs-extra"));
653
+ var import_node_crypto2 = require("crypto");
587
654
 
588
655
  // src/utils/dependency.ts
589
656
  var import_fs_extra5 = __toESM(require("fs-extra"));
@@ -629,6 +696,10 @@ var resolveDependencies = async (moduleDependencies, cwd) => {
629
696
 
630
697
  // src/commands/add.ts
631
698
  var import_chalk4 = __toESM(require("chalk"));
699
+ var DEFAULT_DATABASE_URLS = {
700
+ "database-pg": "postgresql://postgres@localhost:5432/mydb",
701
+ "database-mysql": "mysql://root@localhost:3306/mydb"
702
+ };
632
703
  function resolveSafeTargetPath2(projectRoot, srcDir, file) {
633
704
  const targetPath = import_path6.default.resolve(projectRoot, srcDir, file.target);
634
705
  const normalizedRoot = import_path6.default.resolve(projectRoot);
@@ -637,37 +708,176 @@ function resolveSafeTargetPath2(projectRoot, srcDir, file) {
637
708
  }
638
709
  return targetPath;
639
710
  }
711
+ function resolvePackageManager(projectRoot) {
712
+ if (import_fs_extra6.default.existsSync(import_path6.default.join(projectRoot, "pnpm-lock.yaml"))) {
713
+ return "pnpm";
714
+ }
715
+ 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"))) {
716
+ return "bun";
717
+ }
718
+ if (import_fs_extra6.default.existsSync(import_path6.default.join(projectRoot, "yarn.lock"))) {
719
+ return "yarn";
720
+ }
721
+ return "npm";
722
+ }
723
+ function parseDatabaseDialect(value) {
724
+ const normalized = value?.trim().toLowerCase();
725
+ if (!normalized) {
726
+ return null;
727
+ }
728
+ if (normalized === "pg" || normalized === "postgres" || normalized === "postgresql" || normalized === "database-pg") {
729
+ return "database-pg";
730
+ }
731
+ if (normalized === "mysql" || normalized === "database-mysql") {
732
+ return "database-mysql";
733
+ }
734
+ return null;
735
+ }
736
+ function isDatabaseModule(moduleName) {
737
+ return moduleName === "database-pg" || moduleName === "database-mysql";
738
+ }
739
+ function validateDatabaseUrl(rawUrl, moduleName) {
740
+ const dbUrl = rawUrl.trim();
741
+ if (!dbUrl) {
742
+ throw new Error("Database URL cannot be empty.");
743
+ }
744
+ let parsed;
745
+ try {
746
+ parsed = new URL(dbUrl);
747
+ } catch {
748
+ throw new Error(`Invalid database URL: '${dbUrl}'.`);
749
+ }
750
+ const protocol = parsed.protocol.toLowerCase();
751
+ if (moduleName === "database-pg" && protocol !== "postgresql:" && protocol !== "postgres:") {
752
+ throw new Error("PostgreSQL URL must start with postgres:// or postgresql://");
753
+ }
754
+ if (moduleName === "database-mysql" && protocol !== "mysql:") {
755
+ throw new Error("MySQL URL must start with mysql://");
756
+ }
757
+ return dbUrl;
758
+ }
759
+ async function detectInstalledDatabaseDialect(projectRoot, srcDir) {
760
+ const dbIndexPath = import_path6.default.join(projectRoot, srcDir, "db", "index.ts");
761
+ if (!import_fs_extra6.default.existsSync(dbIndexPath)) {
762
+ return null;
763
+ }
764
+ const content = await import_fs_extra6.default.readFile(dbIndexPath, "utf-8");
765
+ if (content.includes("drizzle-orm/node-postgres") || content.includes(`from "pg"`)) {
766
+ return "database-pg";
767
+ }
768
+ if (content.includes("drizzle-orm/mysql2") || content.includes(`from "mysql2`)) {
769
+ return "database-mysql";
770
+ }
771
+ return null;
772
+ }
773
+ async function backupDatabaseFiles(projectRoot, srcDir) {
774
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
775
+ const backupRoot = import_path6.default.join(projectRoot, ".zuro", "backups", `database-${timestamp}`);
776
+ const candidates = [
777
+ import_path6.default.join(projectRoot, srcDir, "db", "index.ts"),
778
+ import_path6.default.join(projectRoot, "drizzle.config.ts")
779
+ ];
780
+ let copied = false;
781
+ for (const filePath of candidates) {
782
+ if (!import_fs_extra6.default.existsSync(filePath)) {
783
+ continue;
784
+ }
785
+ const relativePath = import_path6.default.relative(projectRoot, filePath);
786
+ const backupPath = import_path6.default.join(backupRoot, relativePath);
787
+ await import_fs_extra6.default.ensureDir(import_path6.default.dirname(backupPath));
788
+ await import_fs_extra6.default.copyFile(filePath, backupPath);
789
+ copied = true;
790
+ }
791
+ return copied ? backupRoot : null;
792
+ }
793
+ function databaseLabel(moduleName) {
794
+ return moduleName === "database-pg" ? "PostgreSQL" : "MySQL";
795
+ }
796
+ function getDatabaseSetupHint(moduleName, dbUrl) {
797
+ try {
798
+ const parsed = new URL(dbUrl);
799
+ const dbName = parsed.pathname.replace(/^\/+/, "") || "mydb";
800
+ if (moduleName === "database-pg") {
801
+ return `createdb ${dbName}`;
802
+ }
803
+ return `mysql -e "CREATE DATABASE IF NOT EXISTS ${dbName};"`;
804
+ } catch {
805
+ return moduleName === "database-pg" ? "createdb <database_name>" : `mysql -e "CREATE DATABASE IF NOT EXISTS <database_name>;"`;
806
+ }
807
+ }
808
+ function getModuleDocsPath(moduleName) {
809
+ if (isDatabaseModule(moduleName)) {
810
+ return "database";
811
+ }
812
+ return moduleName;
813
+ }
814
+ function escapeRegex(value) {
815
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
816
+ }
817
+ async function hasEnvVariable(projectRoot, key) {
818
+ const envPath = import_path6.default.join(projectRoot, ".env");
819
+ if (!await import_fs_extra6.default.pathExists(envPath)) {
820
+ return false;
821
+ }
822
+ const content = await import_fs_extra6.default.readFile(envPath, "utf-8");
823
+ const pattern = new RegExp(`^${escapeRegex(key)}=`, "m");
824
+ return pattern.test(content);
825
+ }
826
+ async function isLikelyEmptyDirectory(cwd) {
827
+ const entries = await import_fs_extra6.default.readdir(cwd);
828
+ const ignored = /* @__PURE__ */ new Set([".ds_store", "thumbs.db"]);
829
+ return entries.filter((entry) => !ignored.has(entry.toLowerCase())).length === 0;
830
+ }
640
831
  async function injectErrorHandler(projectRoot, srcDir) {
641
832
  const appPath = import_path6.default.join(projectRoot, srcDir, "app.ts");
642
833
  if (!import_fs_extra6.default.existsSync(appPath)) {
643
834
  return false;
644
835
  }
645
836
  let content = await import_fs_extra6.default.readFile(appPath, "utf-8");
646
- if (content.includes("errorHandler")) {
647
- return true;
648
- }
649
837
  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) + `
838
+ const hasErrorImport = content.includes(errorImport);
839
+ const hasNotFoundUse = /app\.use\(\s*notFoundHandler\s*\)/.test(content);
840
+ const hasErrorUse = /app\.use\(\s*errorHandler\s*\)/.test(content);
841
+ let modified = false;
842
+ let importInserted = hasErrorImport;
843
+ if (!hasErrorImport) {
844
+ const importRegex = /^import .+ from .+;?\s*$/gm;
845
+ let lastImportIndex = 0;
846
+ let match;
847
+ while ((match = importRegex.exec(content)) !== null) {
848
+ lastImportIndex = match.index + match[0].length;
849
+ }
850
+ if (lastImportIndex > 0) {
851
+ content = content.slice(0, lastImportIndex) + `
658
852
  ${errorImport}` + content.slice(lastImportIndex);
853
+ modified = true;
854
+ importInserted = true;
855
+ }
659
856
  }
660
- const errorSetup = `
857
+ let setupInserted = hasNotFoundUse && hasErrorUse;
858
+ if (!setupInserted) {
859
+ const setupLines = [];
860
+ if (!hasNotFoundUse) {
861
+ setupLines.push("app.use(notFoundHandler);");
862
+ }
863
+ if (!hasErrorUse) {
864
+ setupLines.push("app.use(errorHandler);");
865
+ }
866
+ const errorSetup = `
661
867
  // Error handling (must be last)
662
- app.use(notFoundHandler);
663
- app.use(errorHandler);
868
+ ${setupLines.join("\n")}
664
869
  `;
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);
870
+ const exportMatch = content.match(/export default app;?\s*$/m);
871
+ if (exportMatch && exportMatch.index !== void 0) {
872
+ content = content.slice(0, exportMatch.index) + errorSetup + "\n" + content.slice(exportMatch.index);
873
+ modified = true;
874
+ setupInserted = true;
875
+ }
668
876
  }
669
- await import_fs_extra6.default.writeFile(appPath, content);
670
- return true;
877
+ if (modified) {
878
+ await import_fs_extra6.default.writeFile(appPath, content);
879
+ }
880
+ return importInserted && setupInserted;
671
881
  }
672
882
  async function injectAuthRoutes(projectRoot, srcDir) {
673
883
  const appPath = import_path6.default.join(projectRoot, srcDir, "app.ts");
@@ -675,48 +885,83 @@ async function injectAuthRoutes(projectRoot, srcDir) {
675
885
  return false;
676
886
  }
677
887
  let content = await import_fs_extra6.default.readFile(appPath, "utf-8");
678
- if (content.includes("routes/auth.routes")) {
679
- return true;
680
- }
681
888
  const authImport = `import authRoutes from "./routes/auth.routes";`;
682
889
  const userImport = `import userRoutes from "./routes/user.routes";`;
683
- const routeSetup = `
890
+ const hasAuthImport = content.includes(authImport);
891
+ const hasUserImport = content.includes(userImport);
892
+ const hasAuthRoute = /app\.use\(\s*authRoutes\s*\)/.test(content);
893
+ const hasUserRoute = /app\.use\(\s*["']\/api\/users["']\s*,\s*userRoutes\s*\)/.test(content);
894
+ let modified = false;
895
+ let importsReady = hasAuthImport && hasUserImport;
896
+ if (!importsReady) {
897
+ const importRegex = /^import .+ from .+;?\s*$/gm;
898
+ let lastImportIndex = 0;
899
+ let match;
900
+ while ((match = importRegex.exec(content)) !== null) {
901
+ lastImportIndex = match.index + match[0].length;
902
+ }
903
+ if (lastImportIndex > 0) {
904
+ const missingImports = [];
905
+ if (!hasAuthImport) {
906
+ missingImports.push(authImport);
907
+ }
908
+ if (!hasUserImport) {
909
+ missingImports.push(userImport);
910
+ }
911
+ content = content.slice(0, lastImportIndex) + `
912
+ ${missingImports.join("\n")}` + content.slice(lastImportIndex);
913
+ modified = true;
914
+ importsReady = true;
915
+ }
916
+ }
917
+ let setupReady = hasAuthRoute && hasUserRoute;
918
+ if (!setupReady) {
919
+ const setupLines = [];
920
+ if (!hasAuthRoute) {
921
+ setupLines.push("app.use(authRoutes);");
922
+ }
923
+ if (!hasUserRoute) {
924
+ setupLines.push('app.use("/api/users", userRoutes);');
925
+ }
926
+ const routeSetup = `
684
927
  // Auth routes
685
- app.use(authRoutes);
686
- app.use("/api/users", userRoutes);
928
+ ${setupLines.join("\n")}
687
929
  `;
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);
930
+ const exportMatch = content.match(/export default app;?\s*$/m);
931
+ if (exportMatch && exportMatch.index !== void 0) {
932
+ content = content.slice(0, exportMatch.index) + routeSetup + "\n" + content.slice(exportMatch.index);
933
+ modified = true;
934
+ setupReady = true;
935
+ }
698
936
  }
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);
937
+ if (modified) {
938
+ await import_fs_extra6.default.writeFile(appPath, content);
702
939
  }
703
- await import_fs_extra6.default.writeFile(appPath, content);
704
- return true;
940
+ return importsReady && setupReady;
705
941
  }
706
942
  var add = async (moduleName) => {
707
943
  const projectRoot = process.cwd();
708
944
  const projectConfig = await readZuroConfig(projectRoot);
709
945
  if (!projectConfig) {
946
+ if (await isLikelyEmptyDirectory(projectRoot)) {
947
+ showInitFirstMessage();
948
+ return;
949
+ }
710
950
  showNonZuroProjectMessage();
711
951
  return;
712
952
  }
713
- let srcDir = projectConfig.srcDir || "src";
953
+ const srcDir = projectConfig.srcDir || "src";
954
+ let resolvedModuleName = moduleName;
955
+ const parsedDialect = parseDatabaseDialect(moduleName);
956
+ if (parsedDialect) {
957
+ resolvedModuleName = parsedDialect;
958
+ }
714
959
  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") {
960
+ let usedDefaultDbUrl = false;
961
+ let databaseBackupPath = null;
962
+ let generatedAuthSecret = false;
963
+ let authDatabaseDialect = null;
964
+ if (resolvedModuleName === "database") {
720
965
  const variantResponse = await (0, import_prompts2.default)({
721
966
  type: "select",
722
967
  name: "variant",
@@ -727,22 +972,39 @@ var add = async (moduleName) => {
727
972
  ]
728
973
  });
729
974
  if (!variantResponse.variant) {
730
- process.exit(0);
975
+ console.log(import_chalk4.default.yellow("Operation cancelled."));
976
+ return;
731
977
  }
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;
978
+ resolvedModuleName = variantResponse.variant;
743
979
  }
744
- if ((moduleName === "database-pg" || moduleName === "database-mysql") && customDbUrl === void 0) {
745
- const defaultUrl = DEFAULT_URLS[moduleName];
980
+ if (isDatabaseModule(resolvedModuleName)) {
981
+ const installedDialect = await detectInstalledDatabaseDialect(projectRoot, srcDir);
982
+ if (installedDialect && installedDialect !== resolvedModuleName) {
983
+ console.log(
984
+ import_chalk4.default.yellow(
985
+ `
986
+ \u26A0 Existing database setup detected: ${databaseLabel(installedDialect)}.`
987
+ )
988
+ );
989
+ console.log(
990
+ import_chalk4.default.yellow(
991
+ ` Switching to ${databaseLabel(resolvedModuleName)} will overwrite db files and drizzle config.
992
+ `
993
+ )
994
+ );
995
+ const switchResponse = await (0, import_prompts2.default)({
996
+ type: "confirm",
997
+ name: "proceed",
998
+ message: "Continue and switch database dialect?",
999
+ initial: false
1000
+ });
1001
+ if (!switchResponse.proceed) {
1002
+ console.log(import_chalk4.default.yellow("Operation cancelled."));
1003
+ return;
1004
+ }
1005
+ databaseBackupPath = await backupDatabaseFiles(projectRoot, srcDir);
1006
+ }
1007
+ const defaultUrl = DEFAULT_DATABASE_URLS[resolvedModuleName];
746
1008
  console.log(import_chalk4.default.dim(` Tip: Leave blank to use ${defaultUrl}
747
1009
  `));
748
1010
  const response = await (0, import_prompts2.default)({
@@ -751,44 +1013,69 @@ var add = async (moduleName) => {
751
1013
  message: "Database URL",
752
1014
  initial: ""
753
1015
  });
754
- customDbUrl = response.dbUrl?.trim() || defaultUrl;
1016
+ if (response.dbUrl === void 0) {
1017
+ console.log(import_chalk4.default.yellow("Operation cancelled."));
1018
+ return;
1019
+ }
1020
+ const enteredUrl = response.dbUrl?.trim() || "";
1021
+ usedDefaultDbUrl = enteredUrl.length === 0;
1022
+ customDbUrl = validateDatabaseUrl(enteredUrl || defaultUrl, resolvedModuleName);
755
1023
  }
756
- const spinner = (0, import_ora2.default)(`Checking registry for ${moduleName}...`).start();
1024
+ const pm = resolvePackageManager(projectRoot);
1025
+ const spinner = (0, import_ora2.default)(`Checking registry for ${resolvedModuleName}...`).start();
1026
+ let currentStep = "package manager preflight";
757
1027
  try {
1028
+ spinner.text = `Checking ${pm} availability...`;
1029
+ await ensurePackageManagerAvailable(pm);
1030
+ currentStep = "registry fetch";
1031
+ spinner.text = `Checking registry for ${resolvedModuleName}...`;
758
1032
  const registryContext = await fetchRegistry();
759
- const module2 = registryContext.manifest.modules[moduleName];
1033
+ const module2 = registryContext.manifest.modules[resolvedModuleName];
760
1034
  if (!module2) {
761
- spinner.fail(`Module '${moduleName}' not found.`);
1035
+ spinner.fail(`Module '${resolvedModuleName}' not found.`);
762
1036
  return;
763
1037
  }
764
- spinner.succeed(`Found module: ${import_chalk4.default.cyan(moduleName)}`);
1038
+ spinner.succeed(`Found module: ${import_chalk4.default.cyan(resolvedModuleName)}`);
765
1039
  const moduleDeps = module2.moduleDependencies || [];
1040
+ currentStep = "module dependency resolution";
766
1041
  await resolveDependencies(moduleDeps, projectRoot);
1042
+ currentStep = "dependency installation";
767
1043
  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
1044
  await installDependencies(pm, module2.dependencies || [], projectRoot);
777
1045
  await installDependencies(pm, module2.devDependencies || [], projectRoot, { dev: true });
778
1046
  spinner.succeed("Dependencies installed");
1047
+ currentStep = "module scaffolding";
779
1048
  spinner.start("Scaffolding files...");
1049
+ if (resolvedModuleName === "auth") {
1050
+ authDatabaseDialect = await detectInstalledDatabaseDialect(projectRoot, srcDir);
1051
+ }
780
1052
  for (const file of module2.files) {
781
- const content = await fetchFile(file.path, {
1053
+ let fetchPath = file.path;
1054
+ let expectedSha256 = file.sha256;
1055
+ let expectedSize = file.size;
1056
+ if (resolvedModuleName === "auth" && file.target === "db/schema/auth.ts" && authDatabaseDialect === "database-mysql") {
1057
+ fetchPath = "express/db/schema/auth.mysql.ts";
1058
+ expectedSha256 = void 0;
1059
+ expectedSize = void 0;
1060
+ }
1061
+ let content = await fetchFile(fetchPath, {
782
1062
  baseUrl: registryContext.fileBaseUrl,
783
- expectedSha256: file.sha256,
784
- expectedSize: file.size
1063
+ expectedSha256,
1064
+ expectedSize
785
1065
  });
1066
+ if (isDatabaseModule(resolvedModuleName) && file.target === "../drizzle.config.ts") {
1067
+ const normalizedSrcDir = srcDir.replace(/\\/g, "/");
1068
+ content = content.replace(
1069
+ /schema:\s*["'][^"']+["']/,
1070
+ `schema: "./${normalizedSrcDir}/db/schema/*"`
1071
+ );
1072
+ }
786
1073
  const targetPath = resolveSafeTargetPath2(projectRoot, srcDir, file);
787
1074
  await import_fs_extra6.default.ensureDir(import_path6.default.dirname(targetPath));
788
1075
  await import_fs_extra6.default.writeFile(targetPath, content);
789
1076
  }
790
1077
  spinner.succeed("Files generated");
791
- if (moduleName === "auth") {
1078
+ if (resolvedModuleName === "auth") {
792
1079
  spinner.start("Configuring routes in app.ts...");
793
1080
  const injected = await injectAuthRoutes(projectRoot, srcDir);
794
1081
  if (injected) {
@@ -797,7 +1084,7 @@ var add = async (moduleName) => {
797
1084
  spinner.warn("Could not find app.ts - routes need manual setup");
798
1085
  }
799
1086
  }
800
- if (moduleName === "error-handler") {
1087
+ if (resolvedModuleName === "error-handler") {
801
1088
  spinner.start("Configuring error handler in app.ts...");
802
1089
  const injected = await injectErrorHandler(projectRoot, srcDir);
803
1090
  if (injected) {
@@ -806,78 +1093,63 @@ var add = async (moduleName) => {
806
1093
  spinner.warn("Could not find app.ts - error handler needs manual setup");
807
1094
  }
808
1095
  }
809
- const envConfig = ENV_CONFIGS[moduleName];
1096
+ const envConfig = ENV_CONFIGS[resolvedModuleName];
810
1097
  if (envConfig) {
1098
+ currentStep = "environment configuration";
811
1099
  spinner.start("Updating environment configuration...");
812
1100
  const envVars = { ...envConfig.envVars };
813
- if (customDbUrl && (moduleName === "database-pg" || moduleName === "database-mysql")) {
1101
+ if (customDbUrl && isDatabaseModule(resolvedModuleName)) {
814
1102
  envVars.DATABASE_URL = customDbUrl;
815
1103
  }
816
- await updateEnvFile(projectRoot, envVars);
1104
+ if (resolvedModuleName === "auth") {
1105
+ const hasExistingSecret = await hasEnvVariable(projectRoot, "BETTER_AUTH_SECRET");
1106
+ if (!hasExistingSecret) {
1107
+ envVars.BETTER_AUTH_SECRET = (0, import_node_crypto2.randomBytes)(32).toString("hex");
1108
+ generatedAuthSecret = true;
1109
+ }
1110
+ }
1111
+ await updateEnvFile(projectRoot, envVars, true, {
1112
+ overwriteExisting: isDatabaseModule(resolvedModuleName)
1113
+ });
817
1114
  await updateEnvSchema(projectRoot, srcDir, envConfig.schemaFields);
818
1115
  spinner.succeed("Environment configured");
819
1116
  }
820
1117
  console.log(import_chalk4.default.green(`
821
- \u2714 ${moduleName} added successfully!
1118
+ \u2714 ${resolvedModuleName} added successfully!
822
1119
  `));
823
- if (moduleName === "auth") {
824
- 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")
1120
+ if (databaseBackupPath) {
1121
+ console.log(import_chalk4.default.blue(`\u2139 Backup created at: ${databaseBackupPath}
1122
+ `));
1123
+ }
1124
+ const docsPath = getModuleDocsPath(resolvedModuleName);
1125
+ const docsUrl = `https://zuro-cli.devbybriyan.com/docs/${docsPath}`;
1126
+ console.log(import_chalk4.default.blue(`\u2139 Docs: ${docsUrl}`));
1127
+ if (isDatabaseModule(resolvedModuleName)) {
1128
+ if (usedDefaultDbUrl) {
1129
+ console.log(import_chalk4.default.yellow("\u2139 Review DATABASE_URL in .env if your local DB config differs."));
1130
+ }
1131
+ const setupHint = getDatabaseSetupHint(
1132
+ resolvedModuleName,
1133
+ customDbUrl || DEFAULT_DATABASE_URLS[resolvedModuleName]
828
1134
  );
829
- console.log(import_chalk4.default.yellow("2. Run database migrations:"));
830
- console.log(import_chalk4.default.cyan(" npx drizzle-kit generate"));
831
- console.log(import_chalk4.default.cyan(" npx drizzle-kit migrate\n"));
832
- console.log(import_chalk4.default.yellow("3. Available endpoints:"));
833
- console.log(import_chalk4.default.dim(" POST /auth/sign-up/email - Register"));
834
- console.log(import_chalk4.default.dim(" POST /auth/sign-in/email - Login"));
835
- console.log(import_chalk4.default.dim(" POST /auth/sign-out - Logout"));
836
- console.log(import_chalk4.default.dim(" GET /api/users/me - Current user\n"));
837
- } else if (moduleName === "error-handler") {
838
- console.log(import_chalk4.default.bold("\u{1F4CB} Usage:\n"));
839
- console.log(import_chalk4.default.yellow("Throw errors in your controllers:"));
840
- 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"));
841
- console.log(import_chalk4.default.white(` import { UnauthorizedError, NotFoundError } from "./lib/errors";`));
842
- console.log("");
843
- console.log(import_chalk4.default.white(` throw new UnauthorizedError("Invalid credentials");`));
844
- console.log(import_chalk4.default.white(` throw new NotFoundError("User not found");`));
845
- 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"));
846
- console.log(import_chalk4.default.yellow("Available error classes:"));
847
- console.log(import_chalk4.default.dim(" BadRequestError (400)"));
848
- console.log(import_chalk4.default.dim(" UnauthorizedError (401)"));
849
- console.log(import_chalk4.default.dim(" ForbiddenError (403)"));
850
- console.log(import_chalk4.default.dim(" NotFoundError (404)"));
851
- console.log(import_chalk4.default.dim(" ConflictError (409)"));
852
- console.log(import_chalk4.default.dim(" ValidationError (422)"));
853
- console.log(import_chalk4.default.dim(" InternalServerError (500)\n"));
854
- console.log(import_chalk4.default.yellow("Wrap async handlers:"));
855
- 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"));
856
- console.log(import_chalk4.default.white(` import { asyncHandler } from "./middleware/error-handler";`));
857
- console.log("");
858
- console.log(import_chalk4.default.white(` router.get("/users", asyncHandler(async (req, res) => {`));
859
- console.log(import_chalk4.default.white(" // errors auto-caught"));
860
- console.log(import_chalk4.default.white(" }));"));
861
- 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")) {
863
- console.log(import_chalk4.default.bold("\u{1F4CB} Next Steps:\n"));
864
- let stepNum = 1;
865
- if (!customDbUrl) {
866
- console.log(import_chalk4.default.yellow(`${stepNum}. Update DATABASE_URL in .env:`));
867
- console.log(
868
- import_chalk4.default.dim(" We added a placeholder. Update with your actual database credentials.\n")
869
- );
870
- stepNum++;
1135
+ console.log(import_chalk4.default.yellow(`\u2139 Ensure DB exists: ${setupHint}`));
1136
+ console.log(import_chalk4.default.yellow("\u2139 Run migrations: npx drizzle-kit generate && npx drizzle-kit migrate"));
1137
+ }
1138
+ if (resolvedModuleName === "auth") {
1139
+ if (generatedAuthSecret) {
1140
+ console.log(import_chalk4.default.yellow("\u2139 BETTER_AUTH_SECRET was generated automatically."));
1141
+ } else {
1142
+ console.log(import_chalk4.default.yellow("\u2139 Review BETTER_AUTH_SECRET and BETTER_AUTH_URL in .env."));
871
1143
  }
872
- console.log(import_chalk4.default.yellow(`${stepNum}. Create schemas in src/db/schema/:`));
873
- console.log(import_chalk4.default.dim(" Add table files and export from index.ts\n"));
874
- stepNum++;
875
- console.log(import_chalk4.default.yellow(`${stepNum}. Run migrations:`));
876
- console.log(import_chalk4.default.cyan(" npx drizzle-kit generate"));
877
- console.log(import_chalk4.default.cyan(" npx drizzle-kit migrate\n"));
1144
+ console.log(import_chalk4.default.yellow("\u2139 Run migrations: npx drizzle-kit generate && npx drizzle-kit migrate"));
878
1145
  }
879
1146
  } catch (error) {
880
- spinner.fail(`Failed to add module: ${error.message}`);
1147
+ spinner.fail(import_chalk4.default.red(`Failed during ${currentStep}.`));
1148
+ const errorMessage = error instanceof Error ? error.message : String(error);
1149
+ console.error(import_chalk4.default.red(errorMessage));
1150
+ console.log(`
1151
+ ${import_chalk4.default.bold("Retry:")}`);
1152
+ console.log(import_chalk4.default.cyan(` npx zuro-cli add ${resolvedModuleName}`));
881
1153
  }
882
1154
  };
883
1155