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