zuro-cli 0.0.2-beta.1 → 0.0.2-beta.11

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
@@ -189,18 +189,36 @@ async function fetchFile(filePath, options) {
189
189
  var import_fs_extra = __toESM(require("fs-extra"));
190
190
  var import_path = __toESM(require("path"));
191
191
  var import_execa = require("execa");
192
- async function initPackageJson(cwd, force = false) {
192
+ function normalizePackageName(name) {
193
+ const normalized = name.trim().toLowerCase().replace(/[^a-z0-9._-]/g, "-").replace(/-+/g, "-").replace(/^[._-]+|[._-]+$/g, "");
194
+ return normalized || "zuro-app";
195
+ }
196
+ async function ensurePackageManagerAvailable(pm) {
197
+ try {
198
+ await (0, import_execa.execa)(pm, ["--version"], { stdio: "ignore" });
199
+ } catch {
200
+ throw new Error(
201
+ `Package manager '${pm}' is not installed or not available in PATH. Install it or choose npm.`
202
+ );
203
+ }
204
+ }
205
+ async function initPackageJson(cwd, force = false, packageName = "zuro-app", srcDir = "src", options = {}) {
193
206
  const pkgPath = import_path.default.join(cwd, "package.json");
194
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
+ }
195
217
  await import_fs_extra.default.writeJson(pkgPath, {
196
- name: "zuro-app",
218
+ name: normalizePackageName(packageName),
197
219
  version: "0.0.1",
198
220
  private: true,
199
- scripts: {
200
- "dev": "tsx watch src/server.ts",
201
- "build": "tsc",
202
- "start": "node dist/server.js"
203
- }
221
+ scripts
204
222
  }, { spaces: 2 });
205
223
  }
206
224
  }
@@ -237,8 +255,9 @@ async function installDependencies(pm, deps, cwd, options = {}) {
237
255
  var import_fs_extra2 = __toESM(require("fs-extra"));
238
256
  var import_path2 = __toESM(require("path"));
239
257
  var import_os = __toESM(require("os"));
240
- var updateEnvFile = async (cwd, variables, createIfMissing = true) => {
258
+ var updateEnvFile = async (cwd, variables, createIfMissing = true, options = {}) => {
241
259
  const envPath = import_path2.default.join(cwd, ".env");
260
+ const overwriteExisting = options.overwriteExisting ?? false;
242
261
  let content = "";
243
262
  if (import_fs_extra2.default.existsSync(envPath)) {
244
263
  content = await import_fs_extra2.default.readFile(envPath, "utf-8");
@@ -247,14 +266,25 @@ var updateEnvFile = async (cwd, variables, createIfMissing = true) => {
247
266
  }
248
267
  let modified = false;
249
268
  for (const [key, value] of Object.entries(variables)) {
250
- const regex = new RegExp(`^${key}=`, "m");
251
- if (!regex.test(content)) {
252
- if (content && !content.endsWith("\n")) {
253
- 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;
254
274
  }
255
- content += `${key}=${value}${import_os.default.EOL}`;
256
- 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;
257
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;
258
288
  }
259
289
  if (modified || !import_fs_extra2.default.existsSync(envPath)) {
260
290
  await import_fs_extra2.default.writeFile(envPath, content);
@@ -377,6 +407,13 @@ function showNonZuroProjectMessage() {
377
407
  console.log("- a fresh/empty directory, or");
378
408
  console.log("- an existing project already managed by Zuro CLI.");
379
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
+ }
380
417
 
381
418
  // src/commands/init.ts
382
419
  function resolveSafeTargetPath(projectRoot, srcDir, file) {
@@ -391,6 +428,48 @@ function resolveSafeTargetPath(projectRoot, srcDir, file) {
391
428
  targetPath
392
429
  };
393
430
  }
431
+ async function ensureSafeTargetDirectory(targetDir, cwd, projectName) {
432
+ await import_fs_extra4.default.ensureDir(targetDir);
433
+ const entries = await import_fs_extra4.default.readdir(targetDir);
434
+ if (entries.length === 0) {
435
+ return true;
436
+ }
437
+ const isCurrentFolder = targetDir === cwd;
438
+ const response = await (0, import_prompts.default)({
439
+ type: "confirm",
440
+ name: "proceed",
441
+ message: isCurrentFolder ? `Current folder '${projectName}' is not empty. Continue anyway?` : `Target folder '${projectName}' already exists and is not empty. Continue anyway?`,
442
+ initial: false
443
+ });
444
+ return response.proceed === true;
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
+ }
394
473
  async function init() {
395
474
  const cwd = process.cwd();
396
475
  const isExistingProject = await import_fs_extra4.default.pathExists(import_path4.default.join(cwd, "package.json"));
@@ -403,6 +482,7 @@ async function init() {
403
482
  let pm = "npm";
404
483
  let srcDir = "src";
405
484
  let projectName = import_path4.default.basename(cwd);
485
+ let enablePrettier = false;
406
486
  if (isExistingProject) {
407
487
  console.log(import_chalk2.default.blue("\u2139 Existing project detected."));
408
488
  projectName = import_path4.default.basename(cwd);
@@ -440,6 +520,12 @@ async function init() {
440
520
  { title: "bun", value: "bun" }
441
521
  ],
442
522
  initial: 0
523
+ },
524
+ {
525
+ type: "confirm",
526
+ name: "prettier",
527
+ message: "Setup Prettier?",
528
+ initial: true
443
529
  }
444
530
  ]);
445
531
  if (response.pm === void 0) {
@@ -447,6 +533,7 @@ async function init() {
447
533
  return;
448
534
  }
449
535
  pm = response.pm;
536
+ enablePrettier = response.prettier === true;
450
537
  srcDir = "src";
451
538
  if (!response.path || response.path.trim() === "") {
452
539
  projectName = import_path4.default.basename(cwd);
@@ -455,7 +542,11 @@ async function init() {
455
542
  } else {
456
543
  projectName = response.path.trim();
457
544
  targetDir = import_path4.default.resolve(cwd, projectName);
458
- await import_fs_extra4.default.ensureDir(targetDir);
545
+ }
546
+ const isSafeTarget = await ensureSafeTargetDirectory(targetDir, cwd, projectName);
547
+ if (!isSafeTarget) {
548
+ console.log(import_chalk2.default.red("Operation cancelled."));
549
+ return;
459
550
  }
460
551
  }
461
552
  const existingConfig = targetDir === cwd ? existingZuroConfig : await readZuroConfig(targetDir);
@@ -464,20 +555,26 @@ async function init() {
464
555
  pm,
465
556
  srcDir: srcDir || existingConfig?.srcDir || "src"
466
557
  };
467
- await writeZuroConfig(targetDir, zuroConfig);
468
558
  const spinner = (0, import_ora.default)("Connecting to Zuro Registry...").start();
559
+ let currentStep = "package manager preflight";
469
560
  try {
561
+ spinner.text = `Checking ${pm} availability...`;
562
+ await ensurePackageManagerAvailable(pm);
563
+ currentStep = "registry fetch";
564
+ spinner.text = "Connecting to Zuro Registry...";
470
565
  const registryContext = await fetchRegistry();
471
566
  const coreModule = registryContext.manifest.modules.core;
472
567
  if (!coreModule) {
473
568
  spinner.fail("Core module not found in registry.");
474
569
  return;
475
570
  }
571
+ currentStep = "project initialization";
476
572
  spinner.text = "Initializing project...";
477
573
  const hasPackageJson = await import_fs_extra4.default.pathExists(import_path4.default.join(targetDir, "package.json"));
478
574
  if (!hasPackageJson) {
479
- await initPackageJson(targetDir, true);
575
+ await initPackageJson(targetDir, true, projectName, srcDir, { enablePrettier });
480
576
  }
577
+ currentStep = "dependency installation";
481
578
  spinner.text = `Installing dependencies using ${pm}...`;
482
579
  let runtimeDeps = [];
483
580
  let devDeps = [];
@@ -492,6 +589,10 @@ async function init() {
492
589
  }
493
590
  await installDependencies(pm, runtimeDeps, targetDir);
494
591
  await installDependencies(pm, devDeps, targetDir, { dev: true });
592
+ if (enablePrettier) {
593
+ await installDependencies(pm, ["prettier"], targetDir, { dev: true });
594
+ }
595
+ currentStep = "module file generation";
495
596
  spinner.text = "Fetching core module files...";
496
597
  for (const file of coreModule.files) {
497
598
  const { relativeTargetPath, targetPath } = resolveSafeTargetPath(targetDir, srcDir, file);
@@ -514,7 +615,14 @@ async function init() {
514
615
  await import_fs_extra4.default.ensureDir(import_path4.default.dirname(targetPath));
515
616
  await import_fs_extra4.default.writeFile(targetPath, content);
516
617
  }
618
+ currentStep = "environment file setup";
517
619
  await createInitialEnv(targetDir);
620
+ if (enablePrettier) {
621
+ currentStep = "prettier setup";
622
+ await setupPrettier(targetDir);
623
+ }
624
+ currentStep = "config write";
625
+ await writeZuroConfig(targetDir, zuroConfig);
518
626
  spinner.succeed(import_chalk2.default.green("Project initialized successfully!"));
519
627
  console.log(`
520
628
  ${import_chalk2.default.bold("Next steps:")}`);
@@ -525,8 +633,15 @@ ${import_chalk2.default.bold("Next steps:")}`);
525
633
  console.log(`
526
634
  ${import_chalk2.default.dim("Add modules: zuro-cli add database, zuro-cli add auth")}`);
527
635
  } catch (error) {
528
- spinner.fail(import_chalk2.default.red("Failed to initialize project."));
529
- console.error(error);
636
+ spinner.fail(import_chalk2.default.red(`Failed during ${currentStep}.`));
637
+ const errorMessage = error instanceof Error ? error.message : String(error);
638
+ console.error(import_chalk2.default.red(errorMessage));
639
+ console.log(`
640
+ ${import_chalk2.default.bold("Retry:")}`);
641
+ if (targetDir !== cwd) {
642
+ console.log(import_chalk2.default.cyan(` cd ${projectName}`));
643
+ }
644
+ console.log(import_chalk2.default.cyan(" npx zuro-cli init"));
530
645
  }
531
646
  }
532
647
 
@@ -535,6 +650,7 @@ var import_prompts2 = __toESM(require("prompts"));
535
650
  var import_ora2 = __toESM(require("ora"));
536
651
  var import_path6 = __toESM(require("path"));
537
652
  var import_fs_extra6 = __toESM(require("fs-extra"));
653
+ var import_node_crypto2 = require("crypto");
538
654
 
539
655
  // src/utils/dependency.ts
540
656
  var import_fs_extra5 = __toESM(require("fs-extra"));
@@ -580,6 +696,10 @@ var resolveDependencies = async (moduleDependencies, cwd) => {
580
696
 
581
697
  // src/commands/add.ts
582
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
+ };
583
703
  function resolveSafeTargetPath2(projectRoot, srcDir, file) {
584
704
  const targetPath = import_path6.default.resolve(projectRoot, srcDir, file.target);
585
705
  const normalizedRoot = import_path6.default.resolve(projectRoot);
@@ -588,86 +708,338 @@ function resolveSafeTargetPath2(projectRoot, srcDir, file) {
588
708
  }
589
709
  return targetPath;
590
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
+ }
831
+ async function ensureSchemaExport(projectRoot, srcDir, schemaFileName) {
832
+ const schemaIndexPath = import_path6.default.join(projectRoot, srcDir, "db", "schema", "index.ts");
833
+ if (!await import_fs_extra6.default.pathExists(schemaIndexPath)) {
834
+ return;
835
+ }
836
+ const exportLine = `export * from "./${schemaFileName}";`;
837
+ const content = await import_fs_extra6.default.readFile(schemaIndexPath, "utf-8");
838
+ const normalized = content.replace(/\r\n/g, "\n");
839
+ const exportPattern = new RegExp(
840
+ `^\\s*export\\s*\\*\\s*from\\s*["']\\./${escapeRegex(schemaFileName)}["'];?\\s*$`,
841
+ "m"
842
+ );
843
+ if (exportPattern.test(normalized)) {
844
+ return;
845
+ }
846
+ let next = normalized.replace(/^\s*export\s*\{\s*\};?\s*$/m, "").trimEnd();
847
+ if (next.length > 0) {
848
+ next += "\n\n";
849
+ }
850
+ next += `${exportLine}
851
+ `;
852
+ await import_fs_extra6.default.writeFile(schemaIndexPath, next);
853
+ }
591
854
  async function injectErrorHandler(projectRoot, srcDir) {
592
855
  const appPath = import_path6.default.join(projectRoot, srcDir, "app.ts");
593
856
  if (!import_fs_extra6.default.existsSync(appPath)) {
594
857
  return false;
595
858
  }
596
859
  let content = await import_fs_extra6.default.readFile(appPath, "utf-8");
597
- if (content.includes("errorHandler")) {
598
- return true;
599
- }
600
860
  const errorImport = `import { errorHandler, notFoundHandler } from "./middleware/error-handler";`;
601
- const importRegex = /^import .+ from .+;?\s*$/gm;
602
- let lastImportIndex = 0;
603
- let match;
604
- while ((match = importRegex.exec(content)) !== null) {
605
- lastImportIndex = match.index + match[0].length;
606
- }
607
- if (lastImportIndex > 0) {
608
- content = content.slice(0, lastImportIndex) + `
861
+ const hasErrorImport = content.includes(errorImport);
862
+ const hasNotFoundUse = /app\.use\(\s*notFoundHandler\s*\)/.test(content);
863
+ const hasErrorUse = /app\.use\(\s*errorHandler\s*\)/.test(content);
864
+ let modified = false;
865
+ let importInserted = hasErrorImport;
866
+ if (!hasErrorImport) {
867
+ const importRegex = /^import .+ from .+;?\s*$/gm;
868
+ let lastImportIndex = 0;
869
+ let match;
870
+ while ((match = importRegex.exec(content)) !== null) {
871
+ lastImportIndex = match.index + match[0].length;
872
+ }
873
+ if (lastImportIndex > 0) {
874
+ content = content.slice(0, lastImportIndex) + `
609
875
  ${errorImport}` + content.slice(lastImportIndex);
876
+ modified = true;
877
+ importInserted = true;
878
+ }
610
879
  }
611
- const errorSetup = `
880
+ let setupInserted = hasNotFoundUse && hasErrorUse;
881
+ if (!setupInserted) {
882
+ const setupLines = [];
883
+ if (!hasNotFoundUse) {
884
+ setupLines.push("app.use(notFoundHandler);");
885
+ }
886
+ if (!hasErrorUse) {
887
+ setupLines.push("app.use(errorHandler);");
888
+ }
889
+ const errorSetup = `
612
890
  // Error handling (must be last)
613
- app.use(notFoundHandler);
614
- app.use(errorHandler);
891
+ ${setupLines.join("\n")}
615
892
  `;
616
- const exportMatch = content.match(/export default app;?\s*$/m);
617
- if (exportMatch && exportMatch.index !== void 0) {
618
- content = content.slice(0, exportMatch.index) + errorSetup + "\n" + content.slice(exportMatch.index);
893
+ const exportMatch = content.match(/export default app;?\s*$/m);
894
+ if (exportMatch && exportMatch.index !== void 0) {
895
+ content = content.slice(0, exportMatch.index) + errorSetup + "\n" + content.slice(exportMatch.index);
896
+ modified = true;
897
+ setupInserted = true;
898
+ }
619
899
  }
620
- await import_fs_extra6.default.writeFile(appPath, content);
621
- return true;
900
+ if (modified) {
901
+ await import_fs_extra6.default.writeFile(appPath, content);
902
+ }
903
+ return importInserted && setupInserted;
622
904
  }
623
905
  async function injectAuthRoutes(projectRoot, srcDir) {
624
906
  const appPath = import_path6.default.join(projectRoot, srcDir, "app.ts");
625
- if (!import_fs_extra6.default.existsSync(appPath)) {
907
+ if (!await import_fs_extra6.default.pathExists(appPath)) {
626
908
  return false;
627
909
  }
628
- let content = await import_fs_extra6.default.readFile(appPath, "utf-8");
629
- if (content.includes("routes/auth.routes")) {
630
- return true;
910
+ let appContent = await import_fs_extra6.default.readFile(appPath, "utf-8");
911
+ const authHandlerImport = `import { toNodeHandler } from "better-auth/node";`;
912
+ const authImport = `import { auth } from "./lib/auth";`;
913
+ const routeIndexUserImport = `import userRoutes from "./user.routes";`;
914
+ const appUserImport = `import userRoutes from "./routes/user.routes";`;
915
+ let appModified = false;
916
+ const appendImport = (source, line) => {
917
+ if (source.includes(line)) {
918
+ return { source, inserted: true };
919
+ }
920
+ const importRegex = /^import .+ from .+;?\s*$/gm;
921
+ let lastImportIndex = 0;
922
+ let match;
923
+ while ((match = importRegex.exec(source)) !== null) {
924
+ lastImportIndex = match.index + match[0].length;
925
+ }
926
+ if (lastImportIndex <= 0) {
927
+ return { source, inserted: false };
928
+ }
929
+ return {
930
+ source: source.slice(0, lastImportIndex) + `
931
+ ${line}` + source.slice(lastImportIndex),
932
+ inserted: true
933
+ };
934
+ };
935
+ for (const importLine of [authHandlerImport, authImport]) {
936
+ const next = appendImport(appContent, importLine);
937
+ if (!next.inserted) {
938
+ return false;
939
+ }
940
+ if (next.source !== appContent) {
941
+ appContent = next.source;
942
+ appModified = true;
943
+ }
944
+ }
945
+ const hasAuthMount = /toNodeHandler\(\s*auth\s*\)/.test(appContent) && /\/api\/auth/.test(appContent);
946
+ if (!hasAuthMount) {
947
+ const authMountLine = "app.all(/^\\/api\\/auth(?:\\/.*)?$/, toNodeHandler(auth));\n";
948
+ const jsonIndex = appContent.search(/^\s*app\.use\(\s*express\.json\(\)\s*\);\s*$/m);
949
+ let insertionIndex = jsonIndex;
950
+ if (insertionIndex < 0) {
951
+ const healthIndex = appContent.search(/^\s*app\.get\(\s*["']\/health["']\s*,/m);
952
+ insertionIndex = healthIndex;
953
+ }
954
+ if (insertionIndex < 0) {
955
+ const exportMatch = appContent.match(/export default app;?\s*$/m);
956
+ insertionIndex = exportMatch?.index ?? -1;
957
+ }
958
+ if (insertionIndex < 0) {
959
+ return false;
960
+ }
961
+ appContent = appContent.slice(0, insertionIndex) + authMountLine + appContent.slice(insertionIndex);
962
+ appModified = true;
631
963
  }
632
- const authImport = `import authRoutes from "./routes/auth.routes";`;
633
- const userImport = `import userRoutes from "./routes/user.routes";`;
634
- const routeSetup = `
635
- // Auth routes
636
- app.use(authRoutes);
964
+ const routeIndexPath = import_path6.default.join(projectRoot, srcDir, "routes", "index.ts");
965
+ if (await import_fs_extra6.default.pathExists(routeIndexPath)) {
966
+ let routeContent = await import_fs_extra6.default.readFile(routeIndexPath, "utf-8");
967
+ let routeModified = false;
968
+ const userImportResult = appendImport(routeContent, routeIndexUserImport);
969
+ if (!userImportResult.inserted) {
970
+ return false;
971
+ }
972
+ if (userImportResult.source !== routeContent) {
973
+ routeContent = userImportResult.source;
974
+ routeModified = true;
975
+ }
976
+ const hasUserRoute = /rootRouter\.use\(\s*["']\/users["']\s*,\s*userRoutes\s*\)/.test(routeContent);
977
+ if (!hasUserRoute) {
978
+ const routeSetup = `
979
+ // User routes
980
+ rootRouter.use("/users", userRoutes);
981
+ `;
982
+ const exportMatch = routeContent.match(/export default rootRouter;?\s*$/m);
983
+ if (!exportMatch || exportMatch.index === void 0) {
984
+ return false;
985
+ }
986
+ routeContent = routeContent.slice(0, exportMatch.index) + routeSetup + "\n" + routeContent.slice(exportMatch.index);
987
+ routeModified = true;
988
+ }
989
+ if (routeModified) {
990
+ await import_fs_extra6.default.writeFile(routeIndexPath, routeContent);
991
+ }
992
+ } else {
993
+ const hasUserRoute = /app\.use\(\s*["']\/api\/users["']\s*,\s*userRoutes\s*\)/.test(appContent);
994
+ if (!hasUserRoute) {
995
+ const exportMatch = appContent.match(/export default app;?\s*$/m);
996
+ if (!exportMatch || exportMatch.index === void 0) {
997
+ return false;
998
+ }
999
+ const routeSetup = `
1000
+ // User routes
637
1001
  app.use("/api/users", userRoutes);
638
1002
  `;
639
- const importRegex = /^import .+ from .+;?\s*$/gm;
640
- let lastImportIndex = 0;
641
- let match;
642
- while ((match = importRegex.exec(content)) !== null) {
643
- lastImportIndex = match.index + match[0].length;
644
- }
645
- if (lastImportIndex > 0) {
646
- content = content.slice(0, lastImportIndex) + `
647
- ${authImport}
648
- ${userImport}` + content.slice(lastImportIndex);
649
- }
650
- const exportMatch = content.match(/export default app;?\s*$/m);
651
- if (exportMatch && exportMatch.index !== void 0) {
652
- content = content.slice(0, exportMatch.index) + routeSetup + "\n" + content.slice(exportMatch.index);
653
- }
654
- await import_fs_extra6.default.writeFile(appPath, content);
1003
+ appContent = appContent.slice(0, exportMatch.index) + routeSetup + "\n" + appContent.slice(exportMatch.index);
1004
+ appModified = true;
1005
+ }
1006
+ const userImportResult = appendImport(appContent, appUserImport);
1007
+ if (!userImportResult.inserted) {
1008
+ return false;
1009
+ }
1010
+ if (userImportResult.source !== appContent) {
1011
+ appContent = userImportResult.source;
1012
+ appModified = true;
1013
+ }
1014
+ }
1015
+ if (appModified) {
1016
+ await import_fs_extra6.default.writeFile(appPath, appContent);
1017
+ }
655
1018
  return true;
656
1019
  }
657
1020
  var add = async (moduleName) => {
658
1021
  const projectRoot = process.cwd();
659
1022
  const projectConfig = await readZuroConfig(projectRoot);
660
1023
  if (!projectConfig) {
1024
+ if (await isLikelyEmptyDirectory(projectRoot)) {
1025
+ showInitFirstMessage();
1026
+ return;
1027
+ }
661
1028
  showNonZuroProjectMessage();
662
1029
  return;
663
1030
  }
664
- let srcDir = projectConfig.srcDir || "src";
1031
+ const srcDir = projectConfig.srcDir || "src";
1032
+ let resolvedModuleName = moduleName;
1033
+ const parsedDialect = parseDatabaseDialect(moduleName);
1034
+ if (parsedDialect) {
1035
+ resolvedModuleName = parsedDialect;
1036
+ }
665
1037
  let customDbUrl;
666
- const DEFAULT_URLS = {
667
- "database-pg": "postgresql://root@localhost:5432/mydb",
668
- "database-mysql": "mysql://root@localhost:3306/mydb"
669
- };
670
- if (moduleName === "database") {
1038
+ let usedDefaultDbUrl = false;
1039
+ let databaseBackupPath = null;
1040
+ let generatedAuthSecret = false;
1041
+ let authDatabaseDialect = null;
1042
+ if (resolvedModuleName === "database") {
671
1043
  const variantResponse = await (0, import_prompts2.default)({
672
1044
  type: "select",
673
1045
  name: "variant",
@@ -678,22 +1050,39 @@ var add = async (moduleName) => {
678
1050
  ]
679
1051
  });
680
1052
  if (!variantResponse.variant) {
681
- process.exit(0);
1053
+ console.log(import_chalk4.default.yellow("Operation cancelled."));
1054
+ return;
682
1055
  }
683
- moduleName = variantResponse.variant;
684
- const defaultUrl = DEFAULT_URLS[moduleName];
685
- console.log(import_chalk4.default.dim(` Tip: Leave blank to use ${defaultUrl}
686
- `));
687
- const urlResponse = await (0, import_prompts2.default)({
688
- type: "text",
689
- name: "dbUrl",
690
- message: "Database URL",
691
- initial: ""
692
- });
693
- customDbUrl = urlResponse.dbUrl?.trim() || defaultUrl;
1056
+ resolvedModuleName = variantResponse.variant;
694
1057
  }
695
- if ((moduleName === "database-pg" || moduleName === "database-mysql") && customDbUrl === void 0) {
696
- const defaultUrl = DEFAULT_URLS[moduleName];
1058
+ if (isDatabaseModule(resolvedModuleName)) {
1059
+ const installedDialect = await detectInstalledDatabaseDialect(projectRoot, srcDir);
1060
+ if (installedDialect && installedDialect !== resolvedModuleName) {
1061
+ console.log(
1062
+ import_chalk4.default.yellow(
1063
+ `
1064
+ \u26A0 Existing database setup detected: ${databaseLabel(installedDialect)}.`
1065
+ )
1066
+ );
1067
+ console.log(
1068
+ import_chalk4.default.yellow(
1069
+ ` Switching to ${databaseLabel(resolvedModuleName)} will overwrite db files and drizzle config.
1070
+ `
1071
+ )
1072
+ );
1073
+ const switchResponse = await (0, import_prompts2.default)({
1074
+ type: "confirm",
1075
+ name: "proceed",
1076
+ message: "Continue and switch database dialect?",
1077
+ initial: false
1078
+ });
1079
+ if (!switchResponse.proceed) {
1080
+ console.log(import_chalk4.default.yellow("Operation cancelled."));
1081
+ return;
1082
+ }
1083
+ databaseBackupPath = await backupDatabaseFiles(projectRoot, srcDir);
1084
+ }
1085
+ const defaultUrl = DEFAULT_DATABASE_URLS[resolvedModuleName];
697
1086
  console.log(import_chalk4.default.dim(` Tip: Leave blank to use ${defaultUrl}
698
1087
  `));
699
1088
  const response = await (0, import_prompts2.default)({
@@ -702,53 +1091,82 @@ var add = async (moduleName) => {
702
1091
  message: "Database URL",
703
1092
  initial: ""
704
1093
  });
705
- customDbUrl = response.dbUrl?.trim() || defaultUrl;
1094
+ if (response.dbUrl === void 0) {
1095
+ console.log(import_chalk4.default.yellow("Operation cancelled."));
1096
+ return;
1097
+ }
1098
+ const enteredUrl = response.dbUrl?.trim() || "";
1099
+ usedDefaultDbUrl = enteredUrl.length === 0;
1100
+ customDbUrl = validateDatabaseUrl(enteredUrl || defaultUrl, resolvedModuleName);
706
1101
  }
707
- const spinner = (0, import_ora2.default)(`Checking registry for ${moduleName}...`).start();
1102
+ const pm = resolvePackageManager(projectRoot);
1103
+ const spinner = (0, import_ora2.default)(`Checking registry for ${resolvedModuleName}...`).start();
1104
+ let currentStep = "package manager preflight";
708
1105
  try {
1106
+ spinner.text = `Checking ${pm} availability...`;
1107
+ await ensurePackageManagerAvailable(pm);
1108
+ currentStep = "registry fetch";
1109
+ spinner.text = `Checking registry for ${resolvedModuleName}...`;
709
1110
  const registryContext = await fetchRegistry();
710
- const module2 = registryContext.manifest.modules[moduleName];
1111
+ const module2 = registryContext.manifest.modules[resolvedModuleName];
711
1112
  if (!module2) {
712
- spinner.fail(`Module '${moduleName}' not found.`);
1113
+ spinner.fail(`Module '${resolvedModuleName}' not found.`);
713
1114
  return;
714
1115
  }
715
- spinner.succeed(`Found module: ${import_chalk4.default.cyan(moduleName)}`);
1116
+ spinner.succeed(`Found module: ${import_chalk4.default.cyan(resolvedModuleName)}`);
716
1117
  const moduleDeps = module2.moduleDependencies || [];
1118
+ currentStep = "module dependency resolution";
717
1119
  await resolveDependencies(moduleDeps, projectRoot);
1120
+ currentStep = "dependency installation";
718
1121
  spinner.start("Installing dependencies...");
719
- let pm = "npm";
720
- if (import_fs_extra6.default.existsSync(import_path6.default.join(projectRoot, "pnpm-lock.yaml"))) {
721
- pm = "pnpm";
722
- } else if (import_fs_extra6.default.existsSync(import_path6.default.join(projectRoot, "bun.lockb"))) {
723
- pm = "bun";
724
- } else if (import_fs_extra6.default.existsSync(import_path6.default.join(projectRoot, "yarn.lock"))) {
725
- pm = "yarn";
726
- }
727
1122
  await installDependencies(pm, module2.dependencies || [], projectRoot);
728
1123
  await installDependencies(pm, module2.devDependencies || [], projectRoot, { dev: true });
729
1124
  spinner.succeed("Dependencies installed");
1125
+ currentStep = "module scaffolding";
730
1126
  spinner.start("Scaffolding files...");
1127
+ if (resolvedModuleName === "auth") {
1128
+ authDatabaseDialect = await detectInstalledDatabaseDialect(projectRoot, srcDir);
1129
+ }
731
1130
  for (const file of module2.files) {
732
- const content = await fetchFile(file.path, {
1131
+ let fetchPath = file.path;
1132
+ let expectedSha256 = file.sha256;
1133
+ let expectedSize = file.size;
1134
+ if (resolvedModuleName === "auth" && file.target === "db/schema/auth.ts" && authDatabaseDialect === "database-mysql") {
1135
+ fetchPath = "express/db/schema/auth.mysql.ts";
1136
+ expectedSha256 = void 0;
1137
+ expectedSize = void 0;
1138
+ }
1139
+ let content = await fetchFile(fetchPath, {
733
1140
  baseUrl: registryContext.fileBaseUrl,
734
- expectedSha256: file.sha256,
735
- expectedSize: file.size
1141
+ expectedSha256,
1142
+ expectedSize
736
1143
  });
1144
+ if (isDatabaseModule(resolvedModuleName) && file.target === "../drizzle.config.ts") {
1145
+ const normalizedSrcDir = srcDir.replace(/\\/g, "/");
1146
+ content = content.replace(
1147
+ /schema:\s*["'][^"']+["']/,
1148
+ `schema: "./${normalizedSrcDir}/db/schema/*"`
1149
+ );
1150
+ }
737
1151
  const targetPath = resolveSafeTargetPath2(projectRoot, srcDir, file);
738
1152
  await import_fs_extra6.default.ensureDir(import_path6.default.dirname(targetPath));
739
1153
  await import_fs_extra6.default.writeFile(targetPath, content);
740
1154
  }
1155
+ const schemaExports = module2.files.map((file) => file.target.replace(/\\/g, "/")).filter((target) => /^db\/schema\/[^/]+\.ts$/.test(target)).map((target) => import_path6.default.posix.basename(target, ".ts")).filter((name) => name !== "index");
1156
+ for (const schemaFileName of schemaExports) {
1157
+ await ensureSchemaExport(projectRoot, srcDir, schemaFileName);
1158
+ }
741
1159
  spinner.succeed("Files generated");
742
- if (moduleName === "auth") {
743
- spinner.start("Configuring routes in app.ts...");
1160
+ if (resolvedModuleName === "auth") {
1161
+ spinner.start("Configuring routes...");
744
1162
  const injected = await injectAuthRoutes(projectRoot, srcDir);
745
1163
  if (injected) {
746
- spinner.succeed("Routes configured in app.ts");
1164
+ spinner.succeed("Routes configured");
747
1165
  } else {
748
- spinner.warn("Could not find app.ts - routes need manual setup");
1166
+ spinner.warn("Could not configure routes automatically");
749
1167
  }
750
1168
  }
751
- if (moduleName === "error-handler") {
1169
+ if (resolvedModuleName === "error-handler") {
752
1170
  spinner.start("Configuring error handler in app.ts...");
753
1171
  const injected = await injectErrorHandler(projectRoot, srcDir);
754
1172
  if (injected) {
@@ -757,78 +1175,63 @@ var add = async (moduleName) => {
757
1175
  spinner.warn("Could not find app.ts - error handler needs manual setup");
758
1176
  }
759
1177
  }
760
- const envConfig = ENV_CONFIGS[moduleName];
1178
+ const envConfig = ENV_CONFIGS[resolvedModuleName];
761
1179
  if (envConfig) {
1180
+ currentStep = "environment configuration";
762
1181
  spinner.start("Updating environment configuration...");
763
1182
  const envVars = { ...envConfig.envVars };
764
- if (customDbUrl && (moduleName === "database-pg" || moduleName === "database-mysql")) {
1183
+ if (customDbUrl && isDatabaseModule(resolvedModuleName)) {
765
1184
  envVars.DATABASE_URL = customDbUrl;
766
1185
  }
767
- await updateEnvFile(projectRoot, envVars);
1186
+ if (resolvedModuleName === "auth") {
1187
+ const hasExistingSecret = await hasEnvVariable(projectRoot, "BETTER_AUTH_SECRET");
1188
+ if (!hasExistingSecret) {
1189
+ envVars.BETTER_AUTH_SECRET = (0, import_node_crypto2.randomBytes)(32).toString("hex");
1190
+ generatedAuthSecret = true;
1191
+ }
1192
+ }
1193
+ await updateEnvFile(projectRoot, envVars, true, {
1194
+ overwriteExisting: isDatabaseModule(resolvedModuleName)
1195
+ });
768
1196
  await updateEnvSchema(projectRoot, srcDir, envConfig.schemaFields);
769
1197
  spinner.succeed("Environment configured");
770
1198
  }
771
1199
  console.log(import_chalk4.default.green(`
772
- \u2714 ${moduleName} added successfully!
1200
+ \u2714 ${resolvedModuleName} added successfully!
773
1201
  `));
774
- if (moduleName === "auth") {
775
- console.log(import_chalk4.default.bold("\u{1F4CB} Next Steps:\n"));
776
- console.log(import_chalk4.default.yellow("1. Update your .env file:"));
777
- console.log(
778
- import_chalk4.default.dim(" We added placeholder values. Update BETTER_AUTH_SECRET with a secure key.\n")
1202
+ if (databaseBackupPath) {
1203
+ console.log(import_chalk4.default.blue(`\u2139 Backup created at: ${databaseBackupPath}
1204
+ `));
1205
+ }
1206
+ const docsPath = getModuleDocsPath(resolvedModuleName);
1207
+ const docsUrl = `https://zuro-cli.devbybriyan.com/docs/${docsPath}`;
1208
+ console.log(import_chalk4.default.blue(`\u2139 Docs: ${docsUrl}`));
1209
+ if (isDatabaseModule(resolvedModuleName)) {
1210
+ if (usedDefaultDbUrl) {
1211
+ console.log(import_chalk4.default.yellow("\u2139 Review DATABASE_URL in .env if your local DB config differs."));
1212
+ }
1213
+ const setupHint = getDatabaseSetupHint(
1214
+ resolvedModuleName,
1215
+ customDbUrl || DEFAULT_DATABASE_URLS[resolvedModuleName]
779
1216
  );
780
- console.log(import_chalk4.default.yellow("2. Run database migrations:"));
781
- console.log(import_chalk4.default.cyan(" npx drizzle-kit generate"));
782
- console.log(import_chalk4.default.cyan(" npx drizzle-kit migrate\n"));
783
- console.log(import_chalk4.default.yellow("3. Available endpoints:"));
784
- console.log(import_chalk4.default.dim(" POST /auth/sign-up/email - Register"));
785
- console.log(import_chalk4.default.dim(" POST /auth/sign-in/email - Login"));
786
- console.log(import_chalk4.default.dim(" POST /auth/sign-out - Logout"));
787
- console.log(import_chalk4.default.dim(" GET /api/users/me - Current user\n"));
788
- } else if (moduleName === "error-handler") {
789
- console.log(import_chalk4.default.bold("\u{1F4CB} Usage:\n"));
790
- console.log(import_chalk4.default.yellow("Throw errors in your controllers:"));
791
- 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"));
792
- console.log(import_chalk4.default.white(` import { UnauthorizedError, NotFoundError } from "./lib/errors";`));
793
- console.log("");
794
- console.log(import_chalk4.default.white(` throw new UnauthorizedError("Invalid credentials");`));
795
- console.log(import_chalk4.default.white(` throw new NotFoundError("User not found");`));
796
- 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"));
797
- console.log(import_chalk4.default.yellow("Available error classes:"));
798
- console.log(import_chalk4.default.dim(" BadRequestError (400)"));
799
- console.log(import_chalk4.default.dim(" UnauthorizedError (401)"));
800
- console.log(import_chalk4.default.dim(" ForbiddenError (403)"));
801
- console.log(import_chalk4.default.dim(" NotFoundError (404)"));
802
- console.log(import_chalk4.default.dim(" ConflictError (409)"));
803
- console.log(import_chalk4.default.dim(" ValidationError (422)"));
804
- console.log(import_chalk4.default.dim(" InternalServerError (500)\n"));
805
- console.log(import_chalk4.default.yellow("Wrap async handlers:"));
806
- 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"));
807
- console.log(import_chalk4.default.white(` import { asyncHandler } from "./middleware/error-handler";`));
808
- console.log("");
809
- console.log(import_chalk4.default.white(` router.get("/users", asyncHandler(async (req, res) => {`));
810
- console.log(import_chalk4.default.white(" // errors auto-caught"));
811
- console.log(import_chalk4.default.white(" }));"));
812
- 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"));
813
- } else if (moduleName.includes("database")) {
814
- console.log(import_chalk4.default.bold("\u{1F4CB} Next Steps:\n"));
815
- let stepNum = 1;
816
- if (!customDbUrl) {
817
- console.log(import_chalk4.default.yellow(`${stepNum}. Update DATABASE_URL in .env:`));
818
- console.log(
819
- import_chalk4.default.dim(" We added a placeholder. Update with your actual database credentials.\n")
820
- );
821
- stepNum++;
1217
+ console.log(import_chalk4.default.yellow(`\u2139 Ensure DB exists: ${setupHint}`));
1218
+ console.log(import_chalk4.default.yellow("\u2139 Run migrations: npx drizzle-kit generate && npx drizzle-kit migrate"));
1219
+ }
1220
+ if (resolvedModuleName === "auth") {
1221
+ if (generatedAuthSecret) {
1222
+ console.log(import_chalk4.default.yellow("\u2139 BETTER_AUTH_SECRET was generated automatically."));
1223
+ } else {
1224
+ console.log(import_chalk4.default.yellow("\u2139 Review BETTER_AUTH_SECRET and BETTER_AUTH_URL in .env."));
822
1225
  }
823
- console.log(import_chalk4.default.yellow(`${stepNum}. Create schemas in src/db/schema/:`));
824
- console.log(import_chalk4.default.dim(" Add table files and export from index.ts\n"));
825
- stepNum++;
826
- console.log(import_chalk4.default.yellow(`${stepNum}. Run migrations:`));
827
- console.log(import_chalk4.default.cyan(" npx drizzle-kit generate"));
828
- console.log(import_chalk4.default.cyan(" npx drizzle-kit migrate\n"));
1226
+ console.log(import_chalk4.default.yellow("\u2139 Run migrations: npx drizzle-kit generate && npx drizzle-kit migrate"));
829
1227
  }
830
1228
  } catch (error) {
831
- spinner.fail(`Failed to add module: ${error.message}`);
1229
+ spinner.fail(import_chalk4.default.red(`Failed during ${currentStep}.`));
1230
+ const errorMessage = error instanceof Error ? error.message : String(error);
1231
+ console.error(import_chalk4.default.red(errorMessage));
1232
+ console.log(`
1233
+ ${import_chalk4.default.bold("Retry:")}`);
1234
+ console.log(import_chalk4.default.cyan(` npx zuro-cli add ${resolvedModuleName}`));
832
1235
  }
833
1236
  };
834
1237