zuro-cli 0.0.2-beta.12 → 0.0.2-beta.14
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/chunk-VMOTWTER.mjs +37 -0
- package/dist/chunk-VMOTWTER.mjs.map +1 -0
- package/dist/chunk-W36ZIR4Y.mjs +216 -0
- package/dist/chunk-W36ZIR4Y.mjs.map +1 -0
- package/dist/chunk-YBAO5SKK.mjs +87 -0
- package/dist/chunk-YBAO5SKK.mjs.map +1 -0
- package/dist/database.handler-D7EVXRJX.mjs +28 -0
- package/dist/database.handler-D7EVXRJX.mjs.map +1 -0
- package/dist/docs.handler-JL3ZIVJQ.mjs +12 -0
- package/dist/docs.handler-JL3ZIVJQ.mjs.map +1 -0
- package/dist/index.js +928 -429
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +582 -417
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
ensureSchemaExport,
|
|
4
|
+
isDatabaseModule,
|
|
5
|
+
parseDatabaseDialect,
|
|
6
|
+
printDatabaseHints,
|
|
7
|
+
promptDatabaseConfig
|
|
8
|
+
} from "./chunk-W36ZIR4Y.mjs";
|
|
9
|
+
import {
|
|
10
|
+
injectDocsRoutes,
|
|
11
|
+
isDocsModuleInstalled,
|
|
12
|
+
printDocsHints
|
|
13
|
+
} from "./chunk-YBAO5SKK.mjs";
|
|
14
|
+
import {
|
|
15
|
+
appendImport,
|
|
16
|
+
escapeRegex
|
|
17
|
+
} from "./chunk-VMOTWTER.mjs";
|
|
2
18
|
|
|
3
19
|
// src/index.ts
|
|
4
20
|
import { Command } from "commander";
|
|
@@ -9,6 +25,7 @@ import chalk2 from "chalk";
|
|
|
9
25
|
import fs4 from "fs-extra";
|
|
10
26
|
import path4 from "path";
|
|
11
27
|
import prompts from "prompts";
|
|
28
|
+
import { execSync } from "child_process";
|
|
12
29
|
|
|
13
30
|
// src/utils/registry.ts
|
|
14
31
|
import { createHash } from "crypto";
|
|
@@ -360,6 +377,16 @@ var ENV_CONFIGS = {
|
|
|
360
377
|
{ name: "RESEND_API_KEY", schema: "z.string().min(1)" },
|
|
361
378
|
{ name: "MAIL_FROM", schema: "z.string().min(1)" }
|
|
362
379
|
]
|
|
380
|
+
},
|
|
381
|
+
"rate-limiter": {
|
|
382
|
+
envVars: {
|
|
383
|
+
RATE_LIMIT_WINDOW_MS: "900000",
|
|
384
|
+
RATE_LIMIT_MAX: "100"
|
|
385
|
+
},
|
|
386
|
+
schemaFields: [
|
|
387
|
+
{ name: "RATE_LIMIT_WINDOW_MS", schema: "z.coerce.number().default(900000)" },
|
|
388
|
+
{ name: "RATE_LIMIT_MAX", schema: "z.coerce.number().default(100)" }
|
|
389
|
+
]
|
|
363
390
|
}
|
|
364
391
|
};
|
|
365
392
|
|
|
@@ -473,6 +500,62 @@ bun.lockb
|
|
|
473
500
|
await fs4.writeFile(prettierIgnorePath, ignoreContent);
|
|
474
501
|
}
|
|
475
502
|
}
|
|
503
|
+
async function setupGitignore(targetDir) {
|
|
504
|
+
const gitignorePath = path4.join(targetDir, ".gitignore");
|
|
505
|
+
if (await fs4.pathExists(gitignorePath)) {
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
const gitignoreContent = `# dependencies
|
|
509
|
+
node_modules
|
|
510
|
+
|
|
511
|
+
# build output
|
|
512
|
+
dist
|
|
513
|
+
build
|
|
514
|
+
|
|
515
|
+
# environment variables
|
|
516
|
+
.env
|
|
517
|
+
.env.*
|
|
518
|
+
!.env.example
|
|
519
|
+
|
|
520
|
+
# logs
|
|
521
|
+
*.log
|
|
522
|
+
npm-debug.log*
|
|
523
|
+
pnpm-debug.log*
|
|
524
|
+
|
|
525
|
+
# coverage
|
|
526
|
+
coverage
|
|
527
|
+
|
|
528
|
+
# OS files
|
|
529
|
+
.DS_Store
|
|
530
|
+
Thumbs.db
|
|
531
|
+
|
|
532
|
+
# IDE
|
|
533
|
+
.vscode
|
|
534
|
+
.idea
|
|
535
|
+
*.swp
|
|
536
|
+
*.swo
|
|
537
|
+
`;
|
|
538
|
+
await fs4.writeFile(gitignorePath, gitignoreContent);
|
|
539
|
+
}
|
|
540
|
+
function tryGitInit(targetDir) {
|
|
541
|
+
try {
|
|
542
|
+
execSync("git rev-parse --is-inside-work-tree", {
|
|
543
|
+
cwd: targetDir,
|
|
544
|
+
stdio: "ignore"
|
|
545
|
+
});
|
|
546
|
+
return;
|
|
547
|
+
} catch {
|
|
548
|
+
}
|
|
549
|
+
try {
|
|
550
|
+
execSync("git init", { cwd: targetDir, stdio: "ignore" });
|
|
551
|
+
execSync("git add -A", { cwd: targetDir, stdio: "ignore" });
|
|
552
|
+
execSync('git commit -m "Initial commit from zuro-cli"', {
|
|
553
|
+
cwd: targetDir,
|
|
554
|
+
stdio: "ignore"
|
|
555
|
+
});
|
|
556
|
+
} catch {
|
|
557
|
+
}
|
|
558
|
+
}
|
|
476
559
|
async function init() {
|
|
477
560
|
const cwd = process.cwd();
|
|
478
561
|
const isExistingProject = await fs4.pathExists(path4.join(cwd, "package.json"));
|
|
@@ -624,8 +707,16 @@ async function init() {
|
|
|
624
707
|
currentStep = "prettier setup";
|
|
625
708
|
await setupPrettier(targetDir);
|
|
626
709
|
}
|
|
710
|
+
currentStep = "gitignore setup";
|
|
711
|
+
spinner.text = "Setting up .gitignore...";
|
|
712
|
+
await setupGitignore(targetDir);
|
|
627
713
|
currentStep = "config write";
|
|
628
714
|
await writeZuroConfig(targetDir, zuroConfig);
|
|
715
|
+
if (!isExistingProject) {
|
|
716
|
+
currentStep = "git init";
|
|
717
|
+
spinner.text = "Initializing git repository...";
|
|
718
|
+
tryGitInit(targetDir);
|
|
719
|
+
}
|
|
629
720
|
spinner.succeed(chalk2.green("Project initialized successfully!"));
|
|
630
721
|
console.log(`
|
|
631
722
|
${chalk2.bold("Next steps:")}`);
|
|
@@ -649,10 +740,9 @@ ${chalk2.bold("Retry:")}`);
|
|
|
649
740
|
}
|
|
650
741
|
|
|
651
742
|
// src/commands/add.ts
|
|
652
|
-
import prompts2 from "prompts";
|
|
653
743
|
import ora2 from "ora";
|
|
654
|
-
import
|
|
655
|
-
import
|
|
744
|
+
import path10 from "path";
|
|
745
|
+
import fs9 from "fs-extra";
|
|
656
746
|
import { randomBytes } from "crypto";
|
|
657
747
|
|
|
658
748
|
// src/utils/dependency.ts
|
|
@@ -667,7 +757,9 @@ var BLOCK_SIGNATURES = {
|
|
|
667
757
|
"error-handler": "lib/errors.ts",
|
|
668
758
|
logger: "lib/logger.ts",
|
|
669
759
|
auth: "lib/auth.ts",
|
|
670
|
-
mailer: "lib/mailer.ts"
|
|
760
|
+
mailer: "lib/mailer.ts",
|
|
761
|
+
docs: "lib/openapi.ts",
|
|
762
|
+
"rate-limiter": "middleware/rate-limiter.ts"
|
|
671
763
|
};
|
|
672
764
|
var resolveDependencies = async (moduleDependencies, cwd) => {
|
|
673
765
|
if (!moduleDependencies || moduleDependencies.length === 0) {
|
|
@@ -698,12 +790,8 @@ var resolveDependencies = async (moduleDependencies, cwd) => {
|
|
|
698
790
|
}
|
|
699
791
|
};
|
|
700
792
|
|
|
701
|
-
// src/
|
|
702
|
-
import
|
|
703
|
-
var DEFAULT_DATABASE_URLS = {
|
|
704
|
-
"database-pg": "postgresql://postgres@localhost:5432/mydb",
|
|
705
|
-
"database-mysql": "mysql://root@localhost:3306/mydb"
|
|
706
|
-
};
|
|
793
|
+
// src/utils/paths.ts
|
|
794
|
+
import path6 from "path";
|
|
707
795
|
function resolveSafeTargetPath2(projectRoot, srcDir, file) {
|
|
708
796
|
const targetPath = path6.resolve(projectRoot, srcDir, file.target);
|
|
709
797
|
const normalizedRoot = path6.resolve(projectRoot);
|
|
@@ -712,202 +800,20 @@ function resolveSafeTargetPath2(projectRoot, srcDir, file) {
|
|
|
712
800
|
}
|
|
713
801
|
return targetPath;
|
|
714
802
|
}
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
return "
|
|
726
|
-
}
|
|
727
|
-
function parseDatabaseDialect(value) {
|
|
728
|
-
const normalized = value?.trim().toLowerCase();
|
|
729
|
-
if (!normalized) {
|
|
730
|
-
return null;
|
|
731
|
-
}
|
|
732
|
-
if (normalized === "pg" || normalized === "postgres" || normalized === "postgresql" || normalized === "database-pg") {
|
|
733
|
-
return "database-pg";
|
|
734
|
-
}
|
|
735
|
-
if (normalized === "mysql" || normalized === "database-mysql") {
|
|
736
|
-
return "database-mysql";
|
|
737
|
-
}
|
|
738
|
-
return null;
|
|
739
|
-
}
|
|
740
|
-
function isDatabaseModule(moduleName) {
|
|
741
|
-
return moduleName === "database-pg" || moduleName === "database-mysql";
|
|
742
|
-
}
|
|
743
|
-
function validateDatabaseUrl(rawUrl, moduleName) {
|
|
744
|
-
const dbUrl = rawUrl.trim();
|
|
745
|
-
if (!dbUrl) {
|
|
746
|
-
throw new Error("Database URL cannot be empty.");
|
|
747
|
-
}
|
|
748
|
-
let parsed;
|
|
749
|
-
try {
|
|
750
|
-
parsed = new URL(dbUrl);
|
|
751
|
-
} catch {
|
|
752
|
-
throw new Error(`Invalid database URL: '${dbUrl}'.`);
|
|
753
|
-
}
|
|
754
|
-
const protocol = parsed.protocol.toLowerCase();
|
|
755
|
-
if (moduleName === "database-pg" && protocol !== "postgresql:" && protocol !== "postgres:") {
|
|
756
|
-
throw new Error("PostgreSQL URL must start with postgres:// or postgresql://");
|
|
757
|
-
}
|
|
758
|
-
if (moduleName === "database-mysql" && protocol !== "mysql:") {
|
|
759
|
-
throw new Error("MySQL URL must start with mysql://");
|
|
760
|
-
}
|
|
761
|
-
return dbUrl;
|
|
762
|
-
}
|
|
763
|
-
async function detectInstalledDatabaseDialect(projectRoot, srcDir) {
|
|
764
|
-
const dbIndexPath = path6.join(projectRoot, srcDir, "db", "index.ts");
|
|
765
|
-
if (!fs6.existsSync(dbIndexPath)) {
|
|
766
|
-
return null;
|
|
767
|
-
}
|
|
768
|
-
const content = await fs6.readFile(dbIndexPath, "utf-8");
|
|
769
|
-
if (content.includes("drizzle-orm/node-postgres") || content.includes(`from "pg"`)) {
|
|
770
|
-
return "database-pg";
|
|
771
|
-
}
|
|
772
|
-
if (content.includes("drizzle-orm/mysql2") || content.includes(`from "mysql2`)) {
|
|
773
|
-
return "database-mysql";
|
|
774
|
-
}
|
|
775
|
-
return null;
|
|
776
|
-
}
|
|
777
|
-
async function backupDatabaseFiles(projectRoot, srcDir) {
|
|
778
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
779
|
-
const backupRoot = path6.join(projectRoot, ".zuro", "backups", `database-${timestamp}`);
|
|
780
|
-
const candidates = [
|
|
781
|
-
path6.join(projectRoot, srcDir, "db", "index.ts"),
|
|
782
|
-
path6.join(projectRoot, "drizzle.config.ts")
|
|
783
|
-
];
|
|
784
|
-
let copied = false;
|
|
785
|
-
for (const filePath of candidates) {
|
|
786
|
-
if (!fs6.existsSync(filePath)) {
|
|
787
|
-
continue;
|
|
788
|
-
}
|
|
789
|
-
const relativePath = path6.relative(projectRoot, filePath);
|
|
790
|
-
const backupPath = path6.join(backupRoot, relativePath);
|
|
791
|
-
await fs6.ensureDir(path6.dirname(backupPath));
|
|
792
|
-
await fs6.copyFile(filePath, backupPath);
|
|
793
|
-
copied = true;
|
|
794
|
-
}
|
|
795
|
-
return copied ? backupRoot : null;
|
|
796
|
-
}
|
|
797
|
-
function databaseLabel(moduleName) {
|
|
798
|
-
return moduleName === "database-pg" ? "PostgreSQL" : "MySQL";
|
|
799
|
-
}
|
|
800
|
-
function getDatabaseSetupHint(moduleName, dbUrl) {
|
|
801
|
-
try {
|
|
802
|
-
const parsed = new URL(dbUrl);
|
|
803
|
-
const dbName = parsed.pathname.replace(/^\/+/, "") || "mydb";
|
|
804
|
-
if (moduleName === "database-pg") {
|
|
805
|
-
return `createdb ${dbName}`;
|
|
806
|
-
}
|
|
807
|
-
return `mysql -e "CREATE DATABASE IF NOT EXISTS ${dbName};"`;
|
|
808
|
-
} catch {
|
|
809
|
-
return moduleName === "database-pg" ? "createdb <database_name>" : `mysql -e "CREATE DATABASE IF NOT EXISTS <database_name>;"`;
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
function getModuleDocsPath(moduleName) {
|
|
813
|
-
if (isDatabaseModule(moduleName)) {
|
|
814
|
-
return "database";
|
|
815
|
-
}
|
|
816
|
-
return moduleName;
|
|
817
|
-
}
|
|
818
|
-
function escapeRegex(value) {
|
|
819
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
820
|
-
}
|
|
821
|
-
async function hasEnvVariable(projectRoot, key) {
|
|
822
|
-
const envPath = path6.join(projectRoot, ".env");
|
|
823
|
-
if (!await fs6.pathExists(envPath)) {
|
|
824
|
-
return false;
|
|
825
|
-
}
|
|
826
|
-
const content = await fs6.readFile(envPath, "utf-8");
|
|
827
|
-
const pattern = new RegExp(`^${escapeRegex(key)}=`, "m");
|
|
828
|
-
return pattern.test(content);
|
|
829
|
-
}
|
|
830
|
-
async function isLikelyEmptyDirectory(cwd) {
|
|
831
|
-
const entries = await fs6.readdir(cwd);
|
|
832
|
-
const ignored = /* @__PURE__ */ new Set([".ds_store", "thumbs.db"]);
|
|
833
|
-
return entries.filter((entry) => !ignored.has(entry.toLowerCase())).length === 0;
|
|
834
|
-
}
|
|
835
|
-
async function ensureSchemaExport(projectRoot, srcDir, schemaFileName) {
|
|
836
|
-
const schemaIndexPath = path6.join(projectRoot, srcDir, "db", "schema", "index.ts");
|
|
837
|
-
if (!await fs6.pathExists(schemaIndexPath)) {
|
|
838
|
-
return;
|
|
839
|
-
}
|
|
840
|
-
const exportLine = `export * from "./${schemaFileName}";`;
|
|
841
|
-
const content = await fs6.readFile(schemaIndexPath, "utf-8");
|
|
842
|
-
const normalized = content.replace(/\r\n/g, "\n");
|
|
843
|
-
const exportPattern = new RegExp(
|
|
844
|
-
`^\\s*export\\s*\\*\\s*from\\s*["']\\./${escapeRegex(schemaFileName)}["'];?\\s*$`,
|
|
845
|
-
"m"
|
|
846
|
-
);
|
|
847
|
-
if (exportPattern.test(normalized)) {
|
|
848
|
-
return;
|
|
849
|
-
}
|
|
850
|
-
let next = normalized.replace(/^\s*export\s*\{\s*\};?\s*$/m, "").trimEnd();
|
|
851
|
-
if (next.length > 0) {
|
|
852
|
-
next += "\n\n";
|
|
853
|
-
}
|
|
854
|
-
next += `${exportLine}
|
|
855
|
-
`;
|
|
856
|
-
await fs6.writeFile(schemaIndexPath, next);
|
|
857
|
-
}
|
|
858
|
-
async function injectErrorHandler(projectRoot, srcDir) {
|
|
859
|
-
const appPath = path6.join(projectRoot, srcDir, "app.ts");
|
|
860
|
-
if (!fs6.existsSync(appPath)) {
|
|
861
|
-
return false;
|
|
862
|
-
}
|
|
863
|
-
let content = await fs6.readFile(appPath, "utf-8");
|
|
864
|
-
const errorImport = `import { errorHandler, notFoundHandler } from "./middleware/error-handler";`;
|
|
865
|
-
const hasErrorImport = content.includes(errorImport);
|
|
866
|
-
const hasNotFoundUse = /app\.use\(\s*notFoundHandler\s*\)/.test(content);
|
|
867
|
-
const hasErrorUse = /app\.use\(\s*errorHandler\s*\)/.test(content);
|
|
868
|
-
let modified = false;
|
|
869
|
-
let importInserted = hasErrorImport;
|
|
870
|
-
if (!hasErrorImport) {
|
|
871
|
-
const importRegex = /^import .+ from .+;?\s*$/gm;
|
|
872
|
-
let lastImportIndex = 0;
|
|
873
|
-
let match;
|
|
874
|
-
while ((match = importRegex.exec(content)) !== null) {
|
|
875
|
-
lastImportIndex = match.index + match[0].length;
|
|
876
|
-
}
|
|
877
|
-
if (lastImportIndex > 0) {
|
|
878
|
-
content = content.slice(0, lastImportIndex) + `
|
|
879
|
-
${errorImport}` + content.slice(lastImportIndex);
|
|
880
|
-
modified = true;
|
|
881
|
-
importInserted = true;
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
let setupInserted = hasNotFoundUse && hasErrorUse;
|
|
885
|
-
if (!setupInserted) {
|
|
886
|
-
const setupLines = [];
|
|
887
|
-
if (!hasNotFoundUse) {
|
|
888
|
-
setupLines.push("app.use(notFoundHandler);");
|
|
889
|
-
}
|
|
890
|
-
if (!hasErrorUse) {
|
|
891
|
-
setupLines.push("app.use(errorHandler);");
|
|
892
|
-
}
|
|
893
|
-
const errorSetup = `
|
|
894
|
-
// Error handling (must be last)
|
|
895
|
-
${setupLines.join("\n")}
|
|
896
|
-
`;
|
|
897
|
-
const exportMatch = content.match(/export default app;?\s*$/m);
|
|
898
|
-
if (exportMatch && exportMatch.index !== void 0) {
|
|
899
|
-
content = content.slice(0, exportMatch.index) + errorSetup + "\n" + content.slice(exportMatch.index);
|
|
900
|
-
modified = true;
|
|
901
|
-
setupInserted = true;
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
if (modified) {
|
|
905
|
-
await fs6.writeFile(appPath, content);
|
|
906
|
-
}
|
|
907
|
-
return importInserted && setupInserted;
|
|
803
|
+
|
|
804
|
+
// src/commands/add.ts
|
|
805
|
+
import chalk6 from "chalk";
|
|
806
|
+
|
|
807
|
+
// src/handlers/auth.handler.ts
|
|
808
|
+
import path7 from "path";
|
|
809
|
+
import fs6 from "fs-extra";
|
|
810
|
+
import prompts2 from "prompts";
|
|
811
|
+
import chalk4 from "chalk";
|
|
812
|
+
async function isAuthModuleInstalled(projectRoot, srcDir) {
|
|
813
|
+
return await fs6.pathExists(path7.join(projectRoot, srcDir, "lib", "auth.ts"));
|
|
908
814
|
}
|
|
909
815
|
async function injectAuthRoutes(projectRoot, srcDir) {
|
|
910
|
-
const appPath =
|
|
816
|
+
const appPath = path7.join(projectRoot, srcDir, "app.ts");
|
|
911
817
|
if (!await fs6.pathExists(appPath)) {
|
|
912
818
|
return false;
|
|
913
819
|
}
|
|
@@ -917,25 +823,6 @@ async function injectAuthRoutes(projectRoot, srcDir) {
|
|
|
917
823
|
const routeIndexUserImport = `import userRoutes from "./user.routes";`;
|
|
918
824
|
const appUserImport = `import userRoutes from "./routes/user.routes";`;
|
|
919
825
|
let appModified = false;
|
|
920
|
-
const appendImport = (source, line) => {
|
|
921
|
-
if (source.includes(line)) {
|
|
922
|
-
return { source, inserted: true };
|
|
923
|
-
}
|
|
924
|
-
const importRegex = /^import .+ from .+;?\s*$/gm;
|
|
925
|
-
let lastImportIndex = 0;
|
|
926
|
-
let match;
|
|
927
|
-
while ((match = importRegex.exec(source)) !== null) {
|
|
928
|
-
lastImportIndex = match.index + match[0].length;
|
|
929
|
-
}
|
|
930
|
-
if (lastImportIndex <= 0) {
|
|
931
|
-
return { source, inserted: false };
|
|
932
|
-
}
|
|
933
|
-
return {
|
|
934
|
-
source: source.slice(0, lastImportIndex) + `
|
|
935
|
-
${line}` + source.slice(lastImportIndex),
|
|
936
|
-
inserted: true
|
|
937
|
-
};
|
|
938
|
-
};
|
|
939
826
|
for (const importLine of [authHandlerImport, authImport]) {
|
|
940
827
|
const next = appendImport(appContent, importLine);
|
|
941
828
|
if (!next.inserted) {
|
|
@@ -965,7 +852,7 @@ ${line}` + source.slice(lastImportIndex),
|
|
|
965
852
|
appContent = appContent.slice(0, insertionIndex) + authMountLine + appContent.slice(insertionIndex);
|
|
966
853
|
appModified = true;
|
|
967
854
|
}
|
|
968
|
-
const routeIndexPath =
|
|
855
|
+
const routeIndexPath = path7.join(projectRoot, srcDir, "routes", "index.ts");
|
|
969
856
|
if (await fs6.pathExists(routeIndexPath)) {
|
|
970
857
|
let routeContent = await fs6.readFile(routeIndexPath, "utf-8");
|
|
971
858
|
let routeModified = false;
|
|
@@ -1021,7 +908,399 @@ app.use("/api/users", userRoutes);
|
|
|
1021
908
|
}
|
|
1022
909
|
return true;
|
|
1023
910
|
}
|
|
1024
|
-
|
|
911
|
+
async function injectAuthDocs(projectRoot, srcDir) {
|
|
912
|
+
const openApiPath = path7.join(projectRoot, srcDir, "lib", "openapi.ts");
|
|
913
|
+
if (!await fs6.pathExists(openApiPath)) {
|
|
914
|
+
return false;
|
|
915
|
+
}
|
|
916
|
+
const authMarker = "// ZURO_AUTH_DOCS";
|
|
917
|
+
let content = await fs6.readFile(openApiPath, "utf-8");
|
|
918
|
+
if (content.includes(authMarker)) {
|
|
919
|
+
return true;
|
|
920
|
+
}
|
|
921
|
+
const moduleDocsEndMarker = "// ZURO_DOCS_MODULES_END";
|
|
922
|
+
if (!content.includes(moduleDocsEndMarker)) {
|
|
923
|
+
return false;
|
|
924
|
+
}
|
|
925
|
+
const authBlock = `
|
|
926
|
+
const authSignUpSchema = z.object({
|
|
927
|
+
email: z.string().email().openapi({ example: "dev@company.com" }),
|
|
928
|
+
password: z.string().min(8).openapi({ example: "strong-password" }),
|
|
929
|
+
name: z.string().min(1).optional().openapi({ example: "Dev User" }),
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
const authSignInSchema = z.object({
|
|
933
|
+
email: z.string().email().openapi({ example: "dev@company.com" }),
|
|
934
|
+
password: z.string().min(8).openapi({ example: "strong-password" }),
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
const authUserSchema = z.object({
|
|
938
|
+
id: z.string().openapi({ example: "user_123" }),
|
|
939
|
+
email: z.string().email().openapi({ example: "dev@company.com" }),
|
|
940
|
+
name: z.string().nullable().openapi({ example: "Dev User" }),
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
${authMarker}
|
|
944
|
+
registry.registerPath({
|
|
945
|
+
method: "post",
|
|
946
|
+
path: "/api/auth/sign-up/email",
|
|
947
|
+
tags: ["Auth"],
|
|
948
|
+
summary: "Register using email and password",
|
|
949
|
+
request: {
|
|
950
|
+
body: {
|
|
951
|
+
content: {
|
|
952
|
+
"application/json": {
|
|
953
|
+
schema: authSignUpSchema,
|
|
954
|
+
},
|
|
955
|
+
},
|
|
956
|
+
},
|
|
957
|
+
},
|
|
958
|
+
responses: {
|
|
959
|
+
200: { description: "Registration successful" },
|
|
960
|
+
},
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
registry.registerPath({
|
|
964
|
+
method: "post",
|
|
965
|
+
path: "/api/auth/sign-in/email",
|
|
966
|
+
tags: ["Auth"],
|
|
967
|
+
summary: "Sign in using email and password",
|
|
968
|
+
request: {
|
|
969
|
+
body: {
|
|
970
|
+
content: {
|
|
971
|
+
"application/json": {
|
|
972
|
+
schema: authSignInSchema,
|
|
973
|
+
},
|
|
974
|
+
},
|
|
975
|
+
},
|
|
976
|
+
},
|
|
977
|
+
responses: {
|
|
978
|
+
200: { description: "Sign in successful" },
|
|
979
|
+
401: { description: "Invalid credentials" },
|
|
980
|
+
},
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
registry.registerPath({
|
|
984
|
+
method: "post",
|
|
985
|
+
path: "/api/auth/sign-out",
|
|
986
|
+
tags: ["Auth"],
|
|
987
|
+
summary: "Sign out current user",
|
|
988
|
+
responses: {
|
|
989
|
+
200: { description: "Sign out successful" },
|
|
990
|
+
},
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
registry.registerPath({
|
|
994
|
+
method: "get",
|
|
995
|
+
path: "/api/users/me",
|
|
996
|
+
tags: ["Auth"],
|
|
997
|
+
summary: "Get current authenticated user",
|
|
998
|
+
security: [{ bearerAuth: [] }],
|
|
999
|
+
responses: {
|
|
1000
|
+
200: {
|
|
1001
|
+
description: "Current user",
|
|
1002
|
+
content: {
|
|
1003
|
+
"application/json": {
|
|
1004
|
+
schema: z.object({ user: authUserSchema }),
|
|
1005
|
+
},
|
|
1006
|
+
},
|
|
1007
|
+
},
|
|
1008
|
+
401: { description: "Not authenticated" },
|
|
1009
|
+
},
|
|
1010
|
+
});
|
|
1011
|
+
`;
|
|
1012
|
+
content = content.replace(moduleDocsEndMarker, `${authBlock}
|
|
1013
|
+
${moduleDocsEndMarker}`);
|
|
1014
|
+
await fs6.writeFile(openApiPath, content);
|
|
1015
|
+
return true;
|
|
1016
|
+
}
|
|
1017
|
+
async function promptAuthConfig(projectRoot, srcDir, options) {
|
|
1018
|
+
const { isDocsModuleInstalled: isDocsModuleInstalled2 } = await import("./docs.handler-JL3ZIVJQ.mjs");
|
|
1019
|
+
const docsInstalled = await isDocsModuleInstalled2(projectRoot, srcDir);
|
|
1020
|
+
let shouldInstallDocsForAuth = false;
|
|
1021
|
+
if (!docsInstalled) {
|
|
1022
|
+
if (options.yes) {
|
|
1023
|
+
shouldInstallDocsForAuth = true;
|
|
1024
|
+
} else {
|
|
1025
|
+
const docsResponse = await prompts2({
|
|
1026
|
+
type: "confirm",
|
|
1027
|
+
name: "installDocs",
|
|
1028
|
+
message: "Install API docs module (Scalar + OpenAPI) too?",
|
|
1029
|
+
initial: true
|
|
1030
|
+
});
|
|
1031
|
+
if (docsResponse.installDocs === void 0) {
|
|
1032
|
+
console.log(chalk4.yellow("Operation cancelled."));
|
|
1033
|
+
return null;
|
|
1034
|
+
}
|
|
1035
|
+
shouldInstallDocsForAuth = docsResponse.installDocs;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
const { detectInstalledDatabaseDialect } = await import("./database.handler-D7EVXRJX.mjs");
|
|
1039
|
+
const authDatabaseDialect = await detectInstalledDatabaseDialect(projectRoot, srcDir);
|
|
1040
|
+
return { shouldInstallDocsForAuth, authDatabaseDialect };
|
|
1041
|
+
}
|
|
1042
|
+
function printAuthHints(generatedAuthSecret) {
|
|
1043
|
+
if (generatedAuthSecret) {
|
|
1044
|
+
console.log(chalk4.yellow("\u2139 BETTER_AUTH_SECRET was generated automatically."));
|
|
1045
|
+
} else {
|
|
1046
|
+
console.log(chalk4.yellow("\u2139 Review BETTER_AUTH_SECRET and BETTER_AUTH_URL in .env."));
|
|
1047
|
+
}
|
|
1048
|
+
console.log(chalk4.yellow("\u2139 Run migrations: npx drizzle-kit generate && npx drizzle-kit migrate"));
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// src/handlers/mailer.handler.ts
|
|
1052
|
+
import prompts3 from "prompts";
|
|
1053
|
+
import chalk5 from "chalk";
|
|
1054
|
+
async function promptMailerConfig() {
|
|
1055
|
+
const providerResponse = await prompts3({
|
|
1056
|
+
type: "select",
|
|
1057
|
+
name: "provider",
|
|
1058
|
+
message: "Which email provider?",
|
|
1059
|
+
choices: [
|
|
1060
|
+
{ title: "SMTP (Nodemailer)", description: "Gmail, Mailtrap, any SMTP server", value: "smtp" },
|
|
1061
|
+
{ title: "Resend", description: "API-based, easiest setup", value: "resend" }
|
|
1062
|
+
]
|
|
1063
|
+
});
|
|
1064
|
+
if (providerResponse.provider === void 0) {
|
|
1065
|
+
console.log(chalk5.yellow("Operation cancelled."));
|
|
1066
|
+
return null;
|
|
1067
|
+
}
|
|
1068
|
+
const mailerProvider = providerResponse.provider;
|
|
1069
|
+
let customSmtpVars;
|
|
1070
|
+
let usedDefaultSmtp = false;
|
|
1071
|
+
console.log(chalk5.dim(" Tip: Leave fields blank to use placeholder values and configure later\n"));
|
|
1072
|
+
if (mailerProvider === "smtp") {
|
|
1073
|
+
const smtpResponse = await prompts3([
|
|
1074
|
+
{
|
|
1075
|
+
type: "text",
|
|
1076
|
+
name: "host",
|
|
1077
|
+
message: "SMTP Host",
|
|
1078
|
+
initial: ""
|
|
1079
|
+
},
|
|
1080
|
+
{
|
|
1081
|
+
type: "text",
|
|
1082
|
+
name: "port",
|
|
1083
|
+
message: "SMTP Port",
|
|
1084
|
+
initial: "587"
|
|
1085
|
+
},
|
|
1086
|
+
{
|
|
1087
|
+
type: "text",
|
|
1088
|
+
name: "user",
|
|
1089
|
+
message: "SMTP User",
|
|
1090
|
+
initial: ""
|
|
1091
|
+
},
|
|
1092
|
+
{
|
|
1093
|
+
type: "password",
|
|
1094
|
+
name: "pass",
|
|
1095
|
+
message: "SMTP Password"
|
|
1096
|
+
},
|
|
1097
|
+
{
|
|
1098
|
+
type: "text",
|
|
1099
|
+
name: "from",
|
|
1100
|
+
message: "Mail From address",
|
|
1101
|
+
initial: ""
|
|
1102
|
+
}
|
|
1103
|
+
]);
|
|
1104
|
+
if (smtpResponse.host === void 0) {
|
|
1105
|
+
console.log(chalk5.yellow("Operation cancelled."));
|
|
1106
|
+
return null;
|
|
1107
|
+
}
|
|
1108
|
+
const host = smtpResponse.host?.trim() || "";
|
|
1109
|
+
const user = smtpResponse.user?.trim() || "";
|
|
1110
|
+
const pass = smtpResponse.pass?.trim() || "";
|
|
1111
|
+
const from = smtpResponse.from?.trim() || "";
|
|
1112
|
+
const port = smtpResponse.port?.trim() || "587";
|
|
1113
|
+
usedDefaultSmtp = !host && !user;
|
|
1114
|
+
if (!usedDefaultSmtp) {
|
|
1115
|
+
customSmtpVars = {
|
|
1116
|
+
SMTP_HOST: host || "smtp.example.com",
|
|
1117
|
+
SMTP_PORT: port,
|
|
1118
|
+
SMTP_USER: user || "your-email@example.com",
|
|
1119
|
+
SMTP_PASS: pass || "your-password",
|
|
1120
|
+
MAIL_FROM: from || "noreply@example.com"
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
} else {
|
|
1124
|
+
const resendResponse = await prompts3([
|
|
1125
|
+
{
|
|
1126
|
+
type: "text",
|
|
1127
|
+
name: "apiKey",
|
|
1128
|
+
message: "Resend API Key",
|
|
1129
|
+
initial: ""
|
|
1130
|
+
},
|
|
1131
|
+
{
|
|
1132
|
+
type: "text",
|
|
1133
|
+
name: "from",
|
|
1134
|
+
message: "Mail From address",
|
|
1135
|
+
initial: ""
|
|
1136
|
+
}
|
|
1137
|
+
]);
|
|
1138
|
+
if (resendResponse.apiKey === void 0) {
|
|
1139
|
+
console.log(chalk5.yellow("Operation cancelled."));
|
|
1140
|
+
return null;
|
|
1141
|
+
}
|
|
1142
|
+
const apiKey = resendResponse.apiKey?.trim() || "";
|
|
1143
|
+
const from = resendResponse.from?.trim() || "";
|
|
1144
|
+
usedDefaultSmtp = !apiKey;
|
|
1145
|
+
if (!usedDefaultSmtp) {
|
|
1146
|
+
customSmtpVars = {
|
|
1147
|
+
RESEND_API_KEY: apiKey || "re_your_api_key",
|
|
1148
|
+
MAIL_FROM: from || "onboarding@resend.dev"
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
return { mailerProvider, customSmtpVars, usedDefaultSmtp };
|
|
1153
|
+
}
|
|
1154
|
+
function printMailerHints(usedDefaultSmtp) {
|
|
1155
|
+
if (usedDefaultSmtp) {
|
|
1156
|
+
console.log(chalk5.yellow("\u2139 Placeholder SMTP values added to .env \u2014 update them before sending emails."));
|
|
1157
|
+
} else {
|
|
1158
|
+
console.log(chalk5.yellow("\u2139 Review SMTP configuration in .env to ensure values are correct."));
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// src/handlers/error-handler.handler.ts
|
|
1163
|
+
import path8 from "path";
|
|
1164
|
+
import fs7 from "fs-extra";
|
|
1165
|
+
async function injectErrorHandler(projectRoot, srcDir) {
|
|
1166
|
+
const appPath = path8.join(projectRoot, srcDir, "app.ts");
|
|
1167
|
+
if (!fs7.existsSync(appPath)) {
|
|
1168
|
+
return false;
|
|
1169
|
+
}
|
|
1170
|
+
let content = await fs7.readFile(appPath, "utf-8");
|
|
1171
|
+
const errorImport = `import { errorHandler, notFoundHandler } from "./middleware/error-handler";`;
|
|
1172
|
+
const hasErrorImport = content.includes(errorImport);
|
|
1173
|
+
const hasNotFoundUse = /app\.use\(\s*notFoundHandler\s*\)/.test(content);
|
|
1174
|
+
const hasErrorUse = /app\.use\(\s*errorHandler\s*\)/.test(content);
|
|
1175
|
+
let modified = false;
|
|
1176
|
+
let importInserted = hasErrorImport;
|
|
1177
|
+
if (!hasErrorImport) {
|
|
1178
|
+
const importRegex = /^import .+ from .+;?\s*$/gm;
|
|
1179
|
+
let lastImportIndex = 0;
|
|
1180
|
+
let match;
|
|
1181
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
1182
|
+
lastImportIndex = match.index + match[0].length;
|
|
1183
|
+
}
|
|
1184
|
+
if (lastImportIndex > 0) {
|
|
1185
|
+
content = content.slice(0, lastImportIndex) + `
|
|
1186
|
+
${errorImport}` + content.slice(lastImportIndex);
|
|
1187
|
+
modified = true;
|
|
1188
|
+
importInserted = true;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
let setupInserted = hasNotFoundUse && hasErrorUse;
|
|
1192
|
+
if (!setupInserted) {
|
|
1193
|
+
const setupLines = [];
|
|
1194
|
+
if (!hasNotFoundUse) {
|
|
1195
|
+
setupLines.push("app.use(notFoundHandler);");
|
|
1196
|
+
}
|
|
1197
|
+
if (!hasErrorUse) {
|
|
1198
|
+
setupLines.push("app.use(errorHandler);");
|
|
1199
|
+
}
|
|
1200
|
+
const errorSetup = `
|
|
1201
|
+
// Error handling (must be last)
|
|
1202
|
+
${setupLines.join("\n")}
|
|
1203
|
+
`;
|
|
1204
|
+
const exportMatch = content.match(/export default app;?\s*$/m);
|
|
1205
|
+
if (exportMatch && exportMatch.index !== void 0) {
|
|
1206
|
+
content = content.slice(0, exportMatch.index) + errorSetup + "\n" + content.slice(exportMatch.index);
|
|
1207
|
+
modified = true;
|
|
1208
|
+
setupInserted = true;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
if (modified) {
|
|
1212
|
+
await fs7.writeFile(appPath, content);
|
|
1213
|
+
}
|
|
1214
|
+
return importInserted && setupInserted;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// src/handlers/rate-limiter.handler.ts
|
|
1218
|
+
import path9 from "path";
|
|
1219
|
+
import fs8 from "fs-extra";
|
|
1220
|
+
async function injectRateLimiter(projectRoot, srcDir) {
|
|
1221
|
+
const appPath = path9.join(projectRoot, srcDir, "app.ts");
|
|
1222
|
+
if (!fs8.existsSync(appPath)) {
|
|
1223
|
+
return false;
|
|
1224
|
+
}
|
|
1225
|
+
let content = await fs8.readFile(appPath, "utf-8");
|
|
1226
|
+
const rateLimiterImport = `import { rateLimiter } from "./middleware/rate-limiter";`;
|
|
1227
|
+
const hasImport = content.includes(rateLimiterImport);
|
|
1228
|
+
const hasUse = /app\.use\(\s*rateLimiter\s*\)/.test(content);
|
|
1229
|
+
if (hasImport && hasUse) {
|
|
1230
|
+
return true;
|
|
1231
|
+
}
|
|
1232
|
+
let modified = false;
|
|
1233
|
+
if (!hasImport) {
|
|
1234
|
+
const importRegex = /^import .+ from .+;?\s*$/gm;
|
|
1235
|
+
let lastImportIndex = 0;
|
|
1236
|
+
let match;
|
|
1237
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
1238
|
+
lastImportIndex = match.index + match[0].length;
|
|
1239
|
+
}
|
|
1240
|
+
if (lastImportIndex > 0) {
|
|
1241
|
+
content = content.slice(0, lastImportIndex) + `
|
|
1242
|
+
${rateLimiterImport}` + content.slice(lastImportIndex);
|
|
1243
|
+
modified = true;
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
if (!hasUse) {
|
|
1247
|
+
const jsonMiddleware = /^(\s*app\.use\(\s*express\.json\(\)\s*\);?\s*)$/m;
|
|
1248
|
+
const jsonMatch = content.match(jsonMiddleware);
|
|
1249
|
+
if (jsonMatch && jsonMatch.index !== void 0) {
|
|
1250
|
+
const insertAt = jsonMatch.index + jsonMatch[0].length;
|
|
1251
|
+
content = content.slice(0, insertAt) + `
|
|
1252
|
+
app.use(rateLimiter);` + content.slice(insertAt);
|
|
1253
|
+
modified = true;
|
|
1254
|
+
} else {
|
|
1255
|
+
const routeMount = /^\s*app\.use\(\s*["']\/api["']/m;
|
|
1256
|
+
const routeMatch = content.match(routeMount);
|
|
1257
|
+
if (routeMatch && routeMatch.index !== void 0) {
|
|
1258
|
+
content = content.slice(0, routeMatch.index) + `app.use(rateLimiter);
|
|
1259
|
+
` + content.slice(routeMatch.index);
|
|
1260
|
+
modified = true;
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
if (modified) {
|
|
1265
|
+
await fs8.writeFile(appPath, content);
|
|
1266
|
+
}
|
|
1267
|
+
return true;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// src/commands/add.ts
|
|
1271
|
+
function resolvePackageManager(projectRoot) {
|
|
1272
|
+
if (fs9.existsSync(path10.join(projectRoot, "pnpm-lock.yaml"))) {
|
|
1273
|
+
return "pnpm";
|
|
1274
|
+
}
|
|
1275
|
+
if (fs9.existsSync(path10.join(projectRoot, "bun.lockb")) || fs9.existsSync(path10.join(projectRoot, "bun.lock"))) {
|
|
1276
|
+
return "bun";
|
|
1277
|
+
}
|
|
1278
|
+
if (fs9.existsSync(path10.join(projectRoot, "yarn.lock"))) {
|
|
1279
|
+
return "yarn";
|
|
1280
|
+
}
|
|
1281
|
+
return "npm";
|
|
1282
|
+
}
|
|
1283
|
+
function getModuleDocsPath(moduleName) {
|
|
1284
|
+
if (isDatabaseModule(moduleName)) {
|
|
1285
|
+
return "database";
|
|
1286
|
+
}
|
|
1287
|
+
return moduleName;
|
|
1288
|
+
}
|
|
1289
|
+
async function hasEnvVariable(projectRoot, key) {
|
|
1290
|
+
const envPath = path10.join(projectRoot, ".env");
|
|
1291
|
+
if (!await fs9.pathExists(envPath)) {
|
|
1292
|
+
return false;
|
|
1293
|
+
}
|
|
1294
|
+
const content = await fs9.readFile(envPath, "utf-8");
|
|
1295
|
+
const pattern = new RegExp(`^${escapeRegex(key)}=`, "m");
|
|
1296
|
+
return pattern.test(content);
|
|
1297
|
+
}
|
|
1298
|
+
async function isLikelyEmptyDirectory(cwd) {
|
|
1299
|
+
const entries = await fs9.readdir(cwd);
|
|
1300
|
+
const ignored = /* @__PURE__ */ new Set([".ds_store", "thumbs.db"]);
|
|
1301
|
+
return entries.filter((entry) => !ignored.has(entry.toLowerCase())).length === 0;
|
|
1302
|
+
}
|
|
1303
|
+
var add = async (moduleName, options = {}) => {
|
|
1025
1304
|
const projectRoot = process.cwd();
|
|
1026
1305
|
const projectConfig = await readZuroConfig(projectRoot);
|
|
1027
1306
|
if (!projectConfig) {
|
|
@@ -1046,162 +1325,27 @@ var add = async (moduleName) => {
|
|
|
1046
1325
|
let customSmtpVars;
|
|
1047
1326
|
let usedDefaultSmtp = false;
|
|
1048
1327
|
let mailerProvider = "smtp";
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
]
|
|
1058
|
-
});
|
|
1059
|
-
if (!variantResponse.variant) {
|
|
1060
|
-
console.log(chalk4.yellow("Operation cancelled."));
|
|
1061
|
-
return;
|
|
1062
|
-
}
|
|
1063
|
-
resolvedModuleName = variantResponse.variant;
|
|
1064
|
-
}
|
|
1065
|
-
if (isDatabaseModule(resolvedModuleName)) {
|
|
1066
|
-
const installedDialect = await detectInstalledDatabaseDialect(projectRoot, srcDir);
|
|
1067
|
-
if (installedDialect && installedDialect !== resolvedModuleName) {
|
|
1068
|
-
console.log(
|
|
1069
|
-
chalk4.yellow(
|
|
1070
|
-
`
|
|
1071
|
-
\u26A0 Existing database setup detected: ${databaseLabel(installedDialect)}.`
|
|
1072
|
-
)
|
|
1073
|
-
);
|
|
1074
|
-
console.log(
|
|
1075
|
-
chalk4.yellow(
|
|
1076
|
-
` Switching to ${databaseLabel(resolvedModuleName)} will overwrite db files and drizzle config.
|
|
1077
|
-
`
|
|
1078
|
-
)
|
|
1079
|
-
);
|
|
1080
|
-
const switchResponse = await prompts2({
|
|
1081
|
-
type: "confirm",
|
|
1082
|
-
name: "proceed",
|
|
1083
|
-
message: "Continue and switch database dialect?",
|
|
1084
|
-
initial: false
|
|
1085
|
-
});
|
|
1086
|
-
if (!switchResponse.proceed) {
|
|
1087
|
-
console.log(chalk4.yellow("Operation cancelled."));
|
|
1088
|
-
return;
|
|
1089
|
-
}
|
|
1090
|
-
databaseBackupPath = await backupDatabaseFiles(projectRoot, srcDir);
|
|
1091
|
-
}
|
|
1092
|
-
const defaultUrl = DEFAULT_DATABASE_URLS[resolvedModuleName];
|
|
1093
|
-
console.log(chalk4.dim(` Tip: Leave blank to use ${defaultUrl}
|
|
1094
|
-
`));
|
|
1095
|
-
const response = await prompts2({
|
|
1096
|
-
type: "text",
|
|
1097
|
-
name: "dbUrl",
|
|
1098
|
-
message: "Database URL",
|
|
1099
|
-
initial: ""
|
|
1100
|
-
});
|
|
1101
|
-
if (response.dbUrl === void 0) {
|
|
1102
|
-
console.log(chalk4.yellow("Operation cancelled."));
|
|
1103
|
-
return;
|
|
1104
|
-
}
|
|
1105
|
-
const enteredUrl = response.dbUrl?.trim() || "";
|
|
1106
|
-
usedDefaultDbUrl = enteredUrl.length === 0;
|
|
1107
|
-
customDbUrl = validateDatabaseUrl(enteredUrl || defaultUrl, resolvedModuleName);
|
|
1328
|
+
let shouldInstallDocsForAuth = false;
|
|
1329
|
+
if (resolvedModuleName === "database" || isDatabaseModule(resolvedModuleName)) {
|
|
1330
|
+
const result = await promptDatabaseConfig(resolvedModuleName, projectRoot, srcDir);
|
|
1331
|
+
if (!result) return;
|
|
1332
|
+
resolvedModuleName = result.resolvedModuleName;
|
|
1333
|
+
customDbUrl = result.customDbUrl;
|
|
1334
|
+
usedDefaultDbUrl = result.usedDefaultDbUrl;
|
|
1335
|
+
databaseBackupPath = result.databaseBackupPath;
|
|
1108
1336
|
}
|
|
1109
1337
|
if (resolvedModuleName === "mailer") {
|
|
1110
|
-
const
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
return;
|
|
1122
|
-
}
|
|
1123
|
-
mailerProvider = providerResponse.provider;
|
|
1124
|
-
console.log(chalk4.dim(" Tip: Leave fields blank to use placeholder values and configure later\n"));
|
|
1125
|
-
if (mailerProvider === "smtp") {
|
|
1126
|
-
const smtpResponse = await prompts2([
|
|
1127
|
-
{
|
|
1128
|
-
type: "text",
|
|
1129
|
-
name: "host",
|
|
1130
|
-
message: "SMTP Host",
|
|
1131
|
-
initial: ""
|
|
1132
|
-
},
|
|
1133
|
-
{
|
|
1134
|
-
type: "text",
|
|
1135
|
-
name: "port",
|
|
1136
|
-
message: "SMTP Port",
|
|
1137
|
-
initial: "587"
|
|
1138
|
-
},
|
|
1139
|
-
{
|
|
1140
|
-
type: "text",
|
|
1141
|
-
name: "user",
|
|
1142
|
-
message: "SMTP User",
|
|
1143
|
-
initial: ""
|
|
1144
|
-
},
|
|
1145
|
-
{
|
|
1146
|
-
type: "password",
|
|
1147
|
-
name: "pass",
|
|
1148
|
-
message: "SMTP Password"
|
|
1149
|
-
},
|
|
1150
|
-
{
|
|
1151
|
-
type: "text",
|
|
1152
|
-
name: "from",
|
|
1153
|
-
message: "Mail From address",
|
|
1154
|
-
initial: ""
|
|
1155
|
-
}
|
|
1156
|
-
]);
|
|
1157
|
-
if (smtpResponse.host === void 0) {
|
|
1158
|
-
console.log(chalk4.yellow("Operation cancelled."));
|
|
1159
|
-
return;
|
|
1160
|
-
}
|
|
1161
|
-
const host = smtpResponse.host?.trim() || "";
|
|
1162
|
-
const user = smtpResponse.user?.trim() || "";
|
|
1163
|
-
const pass = smtpResponse.pass?.trim() || "";
|
|
1164
|
-
const from = smtpResponse.from?.trim() || "";
|
|
1165
|
-
const port = smtpResponse.port?.trim() || "587";
|
|
1166
|
-
usedDefaultSmtp = !host && !user;
|
|
1167
|
-
if (!usedDefaultSmtp) {
|
|
1168
|
-
customSmtpVars = {
|
|
1169
|
-
SMTP_HOST: host || "smtp.example.com",
|
|
1170
|
-
SMTP_PORT: port,
|
|
1171
|
-
SMTP_USER: user || "your-email@example.com",
|
|
1172
|
-
SMTP_PASS: pass || "your-password",
|
|
1173
|
-
MAIL_FROM: from || "noreply@example.com"
|
|
1174
|
-
};
|
|
1175
|
-
}
|
|
1176
|
-
} else {
|
|
1177
|
-
const resendResponse = await prompts2([
|
|
1178
|
-
{
|
|
1179
|
-
type: "text",
|
|
1180
|
-
name: "apiKey",
|
|
1181
|
-
message: "Resend API Key",
|
|
1182
|
-
initial: ""
|
|
1183
|
-
},
|
|
1184
|
-
{
|
|
1185
|
-
type: "text",
|
|
1186
|
-
name: "from",
|
|
1187
|
-
message: "Mail From address",
|
|
1188
|
-
initial: ""
|
|
1189
|
-
}
|
|
1190
|
-
]);
|
|
1191
|
-
if (resendResponse.apiKey === void 0) {
|
|
1192
|
-
console.log(chalk4.yellow("Operation cancelled."));
|
|
1193
|
-
return;
|
|
1194
|
-
}
|
|
1195
|
-
const apiKey = resendResponse.apiKey?.trim() || "";
|
|
1196
|
-
const from = resendResponse.from?.trim() || "";
|
|
1197
|
-
usedDefaultSmtp = !apiKey;
|
|
1198
|
-
if (!usedDefaultSmtp) {
|
|
1199
|
-
customSmtpVars = {
|
|
1200
|
-
RESEND_API_KEY: apiKey || "re_your_api_key",
|
|
1201
|
-
MAIL_FROM: from || "onboarding@resend.dev"
|
|
1202
|
-
};
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1338
|
+
const result = await promptMailerConfig();
|
|
1339
|
+
if (!result) return;
|
|
1340
|
+
mailerProvider = result.mailerProvider;
|
|
1341
|
+
customSmtpVars = result.customSmtpVars;
|
|
1342
|
+
usedDefaultSmtp = result.usedDefaultSmtp;
|
|
1343
|
+
}
|
|
1344
|
+
if (resolvedModuleName === "auth") {
|
|
1345
|
+
const result = await promptAuthConfig(projectRoot, srcDir, options);
|
|
1346
|
+
if (!result) return;
|
|
1347
|
+
shouldInstallDocsForAuth = result.shouldInstallDocsForAuth;
|
|
1348
|
+
authDatabaseDialect = result.authDatabaseDialect;
|
|
1205
1349
|
}
|
|
1206
1350
|
const pm = resolvePackageManager(projectRoot);
|
|
1207
1351
|
const spinner = ora2(`Checking registry for ${resolvedModuleName}...`).start();
|
|
@@ -1217,7 +1361,7 @@ var add = async (moduleName) => {
|
|
|
1217
1361
|
spinner.fail(`Module '${resolvedModuleName}' not found.`);
|
|
1218
1362
|
return;
|
|
1219
1363
|
}
|
|
1220
|
-
spinner.succeed(`Found module: ${
|
|
1364
|
+
spinner.succeed(`Found module: ${chalk6.cyan(resolvedModuleName)}`);
|
|
1221
1365
|
const moduleDeps = module.moduleDependencies || [];
|
|
1222
1366
|
currentStep = "module dependency resolution";
|
|
1223
1367
|
await resolveDependencies(moduleDeps, projectRoot);
|
|
@@ -1239,9 +1383,6 @@ var add = async (moduleName) => {
|
|
|
1239
1383
|
spinner.succeed("Dependencies installed");
|
|
1240
1384
|
currentStep = "module scaffolding";
|
|
1241
1385
|
spinner.start("Scaffolding files...");
|
|
1242
|
-
if (resolvedModuleName === "auth") {
|
|
1243
|
-
authDatabaseDialect = await detectInstalledDatabaseDialect(projectRoot, srcDir);
|
|
1244
|
-
}
|
|
1245
1386
|
for (const file of module.files) {
|
|
1246
1387
|
let fetchPath = file.path;
|
|
1247
1388
|
let expectedSha256 = file.sha256;
|
|
@@ -1269,10 +1410,10 @@ var add = async (moduleName) => {
|
|
|
1269
1410
|
);
|
|
1270
1411
|
}
|
|
1271
1412
|
const targetPath = resolveSafeTargetPath2(projectRoot, srcDir, file);
|
|
1272
|
-
await
|
|
1273
|
-
await
|
|
1413
|
+
await fs9.ensureDir(path10.dirname(targetPath));
|
|
1414
|
+
await fs9.writeFile(targetPath, content);
|
|
1274
1415
|
}
|
|
1275
|
-
const schemaExports = module.files.map((file) => file.target.replace(/\\/g, "/")).filter((target) => /^db\/schema\/[^/]+\.ts$/.test(target)).map((target) =>
|
|
1416
|
+
const schemaExports = module.files.map((file) => file.target.replace(/\\/g, "/")).filter((target) => /^db\/schema\/[^/]+\.ts$/.test(target)).map((target) => path10.posix.basename(target, ".ts")).filter((name) => name !== "index");
|
|
1276
1417
|
for (const schemaFileName of schemaExports) {
|
|
1277
1418
|
await ensureSchemaExport(projectRoot, srcDir, schemaFileName);
|
|
1278
1419
|
}
|
|
@@ -1285,6 +1426,16 @@ var add = async (moduleName) => {
|
|
|
1285
1426
|
} else {
|
|
1286
1427
|
spinner.warn("Could not configure routes automatically");
|
|
1287
1428
|
}
|
|
1429
|
+
const docsInstalled = await isDocsModuleInstalled(projectRoot, srcDir);
|
|
1430
|
+
if (docsInstalled) {
|
|
1431
|
+
spinner.start("Adding auth endpoints to API docs...");
|
|
1432
|
+
const authDocsInjected = await injectAuthDocs(projectRoot, srcDir);
|
|
1433
|
+
if (authDocsInjected) {
|
|
1434
|
+
spinner.succeed("Auth endpoints added to API docs");
|
|
1435
|
+
} else {
|
|
1436
|
+
spinner.warn("Could not update API docs automatically");
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1288
1439
|
}
|
|
1289
1440
|
if (resolvedModuleName === "error-handler") {
|
|
1290
1441
|
spinner.start("Configuring error handler in app.ts...");
|
|
@@ -1295,6 +1446,34 @@ var add = async (moduleName) => {
|
|
|
1295
1446
|
spinner.warn("Could not find app.ts - error handler needs manual setup");
|
|
1296
1447
|
}
|
|
1297
1448
|
}
|
|
1449
|
+
if (resolvedModuleName === "rate-limiter") {
|
|
1450
|
+
spinner.start("Configuring rate limiter in app.ts...");
|
|
1451
|
+
const injected = await injectRateLimiter(projectRoot, srcDir);
|
|
1452
|
+
if (injected) {
|
|
1453
|
+
spinner.succeed("Rate limiter configured in app.ts");
|
|
1454
|
+
} else {
|
|
1455
|
+
spinner.warn("Could not find app.ts - rate limiter needs manual setup");
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
if (resolvedModuleName === "docs") {
|
|
1459
|
+
spinner.start("Configuring docs routes...");
|
|
1460
|
+
const injected = await injectDocsRoutes(projectRoot, srcDir);
|
|
1461
|
+
if (injected) {
|
|
1462
|
+
spinner.succeed("Docs routes configured");
|
|
1463
|
+
} else {
|
|
1464
|
+
spinner.warn("Could not configure docs routes automatically");
|
|
1465
|
+
}
|
|
1466
|
+
const authInstalled = await isAuthModuleInstalled(projectRoot, srcDir);
|
|
1467
|
+
if (authInstalled) {
|
|
1468
|
+
spinner.start("Adding auth endpoints to API docs...");
|
|
1469
|
+
const authDocsInjected = await injectAuthDocs(projectRoot, srcDir);
|
|
1470
|
+
if (authDocsInjected) {
|
|
1471
|
+
spinner.succeed("Auth endpoints added to API docs");
|
|
1472
|
+
} else {
|
|
1473
|
+
spinner.warn("Could not update API docs automatically");
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1298
1477
|
let envConfigKey = resolvedModuleName;
|
|
1299
1478
|
if (resolvedModuleName === "mailer" && mailerProvider === "resend") {
|
|
1300
1479
|
envConfigKey = "mailer-resend";
|
|
@@ -1323,49 +1502,35 @@ var add = async (moduleName) => {
|
|
|
1323
1502
|
await updateEnvSchema(projectRoot, srcDir, envConfig.schemaFields);
|
|
1324
1503
|
spinner.succeed("Environment configured");
|
|
1325
1504
|
}
|
|
1326
|
-
console.log(
|
|
1505
|
+
console.log(chalk6.green(`
|
|
1327
1506
|
\u2714 ${resolvedModuleName} added successfully!
|
|
1328
1507
|
`));
|
|
1329
|
-
if (databaseBackupPath) {
|
|
1330
|
-
console.log(chalk4.blue(`\u2139 Backup created at: ${databaseBackupPath}
|
|
1331
|
-
`));
|
|
1332
|
-
}
|
|
1333
1508
|
const docsPath = getModuleDocsPath(resolvedModuleName);
|
|
1334
1509
|
const docsUrl = `https://zuro-cli.devbybriyan.com/docs/${docsPath}`;
|
|
1335
|
-
console.log(
|
|
1510
|
+
console.log(chalk6.blue(`\u2139 Docs: ${docsUrl}`));
|
|
1336
1511
|
if (isDatabaseModule(resolvedModuleName)) {
|
|
1337
|
-
|
|
1338
|
-
console.log(chalk4.yellow("\u2139 Review DATABASE_URL in .env if your local DB config differs."));
|
|
1339
|
-
}
|
|
1340
|
-
const setupHint = getDatabaseSetupHint(
|
|
1341
|
-
resolvedModuleName,
|
|
1342
|
-
customDbUrl || DEFAULT_DATABASE_URLS[resolvedModuleName]
|
|
1343
|
-
);
|
|
1344
|
-
console.log(chalk4.yellow(`\u2139 Ensure DB exists: ${setupHint}`));
|
|
1345
|
-
console.log(chalk4.yellow("\u2139 Run migrations: npx drizzle-kit generate && npx drizzle-kit migrate"));
|
|
1512
|
+
printDatabaseHints(resolvedModuleName, customDbUrl, usedDefaultDbUrl, databaseBackupPath);
|
|
1346
1513
|
}
|
|
1347
1514
|
if (resolvedModuleName === "auth") {
|
|
1348
|
-
|
|
1349
|
-
console.log(chalk4.yellow("\u2139 BETTER_AUTH_SECRET was generated automatically."));
|
|
1350
|
-
} else {
|
|
1351
|
-
console.log(chalk4.yellow("\u2139 Review BETTER_AUTH_SECRET and BETTER_AUTH_URL in .env."));
|
|
1352
|
-
}
|
|
1353
|
-
console.log(chalk4.yellow("\u2139 Run migrations: npx drizzle-kit generate && npx drizzle-kit migrate"));
|
|
1515
|
+
printAuthHints(generatedAuthSecret);
|
|
1354
1516
|
}
|
|
1355
1517
|
if (resolvedModuleName === "mailer") {
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1518
|
+
printMailerHints(usedDefaultSmtp);
|
|
1519
|
+
}
|
|
1520
|
+
if (resolvedModuleName === "docs") {
|
|
1521
|
+
printDocsHints();
|
|
1522
|
+
}
|
|
1523
|
+
if (resolvedModuleName === "auth" && shouldInstallDocsForAuth) {
|
|
1524
|
+
console.log(chalk6.blue("\n\u2139 Installing API docs module..."));
|
|
1525
|
+
await add("docs", { yes: true });
|
|
1361
1526
|
}
|
|
1362
1527
|
} catch (error) {
|
|
1363
|
-
spinner.fail(
|
|
1528
|
+
spinner.fail(chalk6.red(`Failed during ${currentStep}.`));
|
|
1364
1529
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1365
|
-
console.error(
|
|
1530
|
+
console.error(chalk6.red(errorMessage));
|
|
1366
1531
|
console.log(`
|
|
1367
|
-
${
|
|
1368
|
-
console.log(
|
|
1532
|
+
${chalk6.bold("Retry:")}`);
|
|
1533
|
+
console.log(chalk6.cyan(` npx zuro-cli add ${resolvedModuleName}`));
|
|
1369
1534
|
}
|
|
1370
1535
|
};
|
|
1371
1536
|
|
|
@@ -1373,6 +1538,6 @@ ${chalk4.bold("Retry:")}`);
|
|
|
1373
1538
|
var program = new Command();
|
|
1374
1539
|
program.name("zuro-cli").description("Zuro CLI tool").version("0.0.1");
|
|
1375
1540
|
program.command("init").description("Initialize a new Zuro project").action(init);
|
|
1376
|
-
program.command("add <module>").description("Add a module to your project").action(add);
|
|
1541
|
+
program.command("add <module>").description("Add a module to your project").action((module, options) => add(module, options));
|
|
1377
1542
|
program.parse(process.argv);
|
|
1378
1543
|
//# sourceMappingURL=index.mjs.map
|