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