zuro-cli 0.0.2-beta.1 → 0.0.2-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -22
- package/dist/index.js +565 -162
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +565 -162
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -166,18 +166,36 @@ async function fetchFile(filePath, options) {
|
|
|
166
166
|
import fs from "fs-extra";
|
|
167
167
|
import path from "path";
|
|
168
168
|
import { execa } from "execa";
|
|
169
|
-
|
|
169
|
+
function normalizePackageName(name) {
|
|
170
|
+
const normalized = name.trim().toLowerCase().replace(/[^a-z0-9._-]/g, "-").replace(/-+/g, "-").replace(/^[._-]+|[._-]+$/g, "");
|
|
171
|
+
return normalized || "zuro-app";
|
|
172
|
+
}
|
|
173
|
+
async function ensurePackageManagerAvailable(pm) {
|
|
174
|
+
try {
|
|
175
|
+
await execa(pm, ["--version"], { stdio: "ignore" });
|
|
176
|
+
} catch {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`Package manager '${pm}' is not installed or not available in PATH. Install it or choose npm.`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
async function initPackageJson(cwd, force = false, packageName = "zuro-app", srcDir = "src", options = {}) {
|
|
170
183
|
const pkgPath = path.join(cwd, "package.json");
|
|
171
184
|
if (force || !await fs.pathExists(pkgPath)) {
|
|
185
|
+
const scripts = {
|
|
186
|
+
"dev": `tsx watch ${srcDir}/server.ts`,
|
|
187
|
+
"build": "tsc",
|
|
188
|
+
"start": "node dist/server.js"
|
|
189
|
+
};
|
|
190
|
+
if (options.enablePrettier) {
|
|
191
|
+
scripts["format"] = "prettier --write .";
|
|
192
|
+
scripts["format:check"] = "prettier --check .";
|
|
193
|
+
}
|
|
172
194
|
await fs.writeJson(pkgPath, {
|
|
173
|
-
name:
|
|
195
|
+
name: normalizePackageName(packageName),
|
|
174
196
|
version: "0.0.1",
|
|
175
197
|
private: true,
|
|
176
|
-
scripts
|
|
177
|
-
"dev": "tsx watch src/server.ts",
|
|
178
|
-
"build": "tsc",
|
|
179
|
-
"start": "node dist/server.js"
|
|
180
|
-
}
|
|
198
|
+
scripts
|
|
181
199
|
}, { spaces: 2 });
|
|
182
200
|
}
|
|
183
201
|
}
|
|
@@ -214,8 +232,9 @@ async function installDependencies(pm, deps, cwd, options = {}) {
|
|
|
214
232
|
import fs2 from "fs-extra";
|
|
215
233
|
import path2 from "path";
|
|
216
234
|
import os from "os";
|
|
217
|
-
var updateEnvFile = async (cwd, variables, createIfMissing = true) => {
|
|
235
|
+
var updateEnvFile = async (cwd, variables, createIfMissing = true, options = {}) => {
|
|
218
236
|
const envPath = path2.join(cwd, ".env");
|
|
237
|
+
const overwriteExisting = options.overwriteExisting ?? false;
|
|
219
238
|
let content = "";
|
|
220
239
|
if (fs2.existsSync(envPath)) {
|
|
221
240
|
content = await fs2.readFile(envPath, "utf-8");
|
|
@@ -224,14 +243,25 @@ var updateEnvFile = async (cwd, variables, createIfMissing = true) => {
|
|
|
224
243
|
}
|
|
225
244
|
let modified = false;
|
|
226
245
|
for (const [key, value] of Object.entries(variables)) {
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
246
|
+
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
247
|
+
const regex = new RegExp(`^${escapedKey}=.*$`, "m");
|
|
248
|
+
if (regex.test(content)) {
|
|
249
|
+
if (!overwriteExisting) {
|
|
250
|
+
continue;
|
|
231
251
|
}
|
|
232
|
-
|
|
233
|
-
|
|
252
|
+
const nextLine = `${key}=${value}`;
|
|
253
|
+
const updated = content.replace(regex, nextLine);
|
|
254
|
+
if (updated !== content) {
|
|
255
|
+
content = updated;
|
|
256
|
+
modified = true;
|
|
257
|
+
}
|
|
258
|
+
continue;
|
|
234
259
|
}
|
|
260
|
+
if (content && !content.endsWith("\n")) {
|
|
261
|
+
content += os.EOL;
|
|
262
|
+
}
|
|
263
|
+
content += `${key}=${value}${os.EOL}`;
|
|
264
|
+
modified = true;
|
|
235
265
|
}
|
|
236
266
|
if (modified || !fs2.existsSync(envPath)) {
|
|
237
267
|
await fs2.writeFile(envPath, content);
|
|
@@ -354,6 +384,13 @@ function showNonZuroProjectMessage() {
|
|
|
354
384
|
console.log("- a fresh/empty directory, or");
|
|
355
385
|
console.log("- an existing project already managed by Zuro CLI.");
|
|
356
386
|
}
|
|
387
|
+
function showInitFirstMessage() {
|
|
388
|
+
console.log(chalk.yellow("No Zuro project found in this directory."));
|
|
389
|
+
console.log("");
|
|
390
|
+
console.log(chalk.yellow("Run init first, then add modules."));
|
|
391
|
+
console.log("");
|
|
392
|
+
console.log(chalk.cyan("npx zuro-cli init"));
|
|
393
|
+
}
|
|
357
394
|
|
|
358
395
|
// src/commands/init.ts
|
|
359
396
|
function resolveSafeTargetPath(projectRoot, srcDir, file) {
|
|
@@ -368,6 +405,48 @@ function resolveSafeTargetPath(projectRoot, srcDir, file) {
|
|
|
368
405
|
targetPath
|
|
369
406
|
};
|
|
370
407
|
}
|
|
408
|
+
async function ensureSafeTargetDirectory(targetDir, cwd, projectName) {
|
|
409
|
+
await fs4.ensureDir(targetDir);
|
|
410
|
+
const entries = await fs4.readdir(targetDir);
|
|
411
|
+
if (entries.length === 0) {
|
|
412
|
+
return true;
|
|
413
|
+
}
|
|
414
|
+
const isCurrentFolder = targetDir === cwd;
|
|
415
|
+
const response = await prompts({
|
|
416
|
+
type: "confirm",
|
|
417
|
+
name: "proceed",
|
|
418
|
+
message: isCurrentFolder ? `Current folder '${projectName}' is not empty. Continue anyway?` : `Target folder '${projectName}' already exists and is not empty. Continue anyway?`,
|
|
419
|
+
initial: false
|
|
420
|
+
});
|
|
421
|
+
return response.proceed === true;
|
|
422
|
+
}
|
|
423
|
+
async function setupPrettier(targetDir) {
|
|
424
|
+
const prettierConfigPath = path4.join(targetDir, ".prettierrc");
|
|
425
|
+
const prettierIgnorePath = path4.join(targetDir, ".prettierignore");
|
|
426
|
+
if (!await fs4.pathExists(prettierConfigPath)) {
|
|
427
|
+
const prettierConfig = {
|
|
428
|
+
semi: true,
|
|
429
|
+
singleQuote: false,
|
|
430
|
+
trailingComma: "es5",
|
|
431
|
+
printWidth: 100,
|
|
432
|
+
tabWidth: 2
|
|
433
|
+
};
|
|
434
|
+
await fs4.writeJson(prettierConfigPath, prettierConfig, { spaces: 2 });
|
|
435
|
+
}
|
|
436
|
+
if (!await fs4.pathExists(prettierIgnorePath)) {
|
|
437
|
+
const ignoreContent = `node_modules
|
|
438
|
+
dist
|
|
439
|
+
build
|
|
440
|
+
coverage
|
|
441
|
+
.next
|
|
442
|
+
pnpm-lock.yaml
|
|
443
|
+
package-lock.json
|
|
444
|
+
bun.lock
|
|
445
|
+
bun.lockb
|
|
446
|
+
`;
|
|
447
|
+
await fs4.writeFile(prettierIgnorePath, ignoreContent);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
371
450
|
async function init() {
|
|
372
451
|
const cwd = process.cwd();
|
|
373
452
|
const isExistingProject = await fs4.pathExists(path4.join(cwd, "package.json"));
|
|
@@ -380,6 +459,7 @@ async function init() {
|
|
|
380
459
|
let pm = "npm";
|
|
381
460
|
let srcDir = "src";
|
|
382
461
|
let projectName = path4.basename(cwd);
|
|
462
|
+
let enablePrettier = false;
|
|
383
463
|
if (isExistingProject) {
|
|
384
464
|
console.log(chalk2.blue("\u2139 Existing project detected."));
|
|
385
465
|
projectName = path4.basename(cwd);
|
|
@@ -417,6 +497,12 @@ async function init() {
|
|
|
417
497
|
{ title: "bun", value: "bun" }
|
|
418
498
|
],
|
|
419
499
|
initial: 0
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
type: "confirm",
|
|
503
|
+
name: "prettier",
|
|
504
|
+
message: "Setup Prettier?",
|
|
505
|
+
initial: true
|
|
420
506
|
}
|
|
421
507
|
]);
|
|
422
508
|
if (response.pm === void 0) {
|
|
@@ -424,6 +510,7 @@ async function init() {
|
|
|
424
510
|
return;
|
|
425
511
|
}
|
|
426
512
|
pm = response.pm;
|
|
513
|
+
enablePrettier = response.prettier === true;
|
|
427
514
|
srcDir = "src";
|
|
428
515
|
if (!response.path || response.path.trim() === "") {
|
|
429
516
|
projectName = path4.basename(cwd);
|
|
@@ -432,7 +519,11 @@ async function init() {
|
|
|
432
519
|
} else {
|
|
433
520
|
projectName = response.path.trim();
|
|
434
521
|
targetDir = path4.resolve(cwd, projectName);
|
|
435
|
-
|
|
522
|
+
}
|
|
523
|
+
const isSafeTarget = await ensureSafeTargetDirectory(targetDir, cwd, projectName);
|
|
524
|
+
if (!isSafeTarget) {
|
|
525
|
+
console.log(chalk2.red("Operation cancelled."));
|
|
526
|
+
return;
|
|
436
527
|
}
|
|
437
528
|
}
|
|
438
529
|
const existingConfig = targetDir === cwd ? existingZuroConfig : await readZuroConfig(targetDir);
|
|
@@ -441,20 +532,26 @@ async function init() {
|
|
|
441
532
|
pm,
|
|
442
533
|
srcDir: srcDir || existingConfig?.srcDir || "src"
|
|
443
534
|
};
|
|
444
|
-
await writeZuroConfig(targetDir, zuroConfig);
|
|
445
535
|
const spinner = ora("Connecting to Zuro Registry...").start();
|
|
536
|
+
let currentStep = "package manager preflight";
|
|
446
537
|
try {
|
|
538
|
+
spinner.text = `Checking ${pm} availability...`;
|
|
539
|
+
await ensurePackageManagerAvailable(pm);
|
|
540
|
+
currentStep = "registry fetch";
|
|
541
|
+
spinner.text = "Connecting to Zuro Registry...";
|
|
447
542
|
const registryContext = await fetchRegistry();
|
|
448
543
|
const coreModule = registryContext.manifest.modules.core;
|
|
449
544
|
if (!coreModule) {
|
|
450
545
|
spinner.fail("Core module not found in registry.");
|
|
451
546
|
return;
|
|
452
547
|
}
|
|
548
|
+
currentStep = "project initialization";
|
|
453
549
|
spinner.text = "Initializing project...";
|
|
454
550
|
const hasPackageJson = await fs4.pathExists(path4.join(targetDir, "package.json"));
|
|
455
551
|
if (!hasPackageJson) {
|
|
456
|
-
await initPackageJson(targetDir, true);
|
|
552
|
+
await initPackageJson(targetDir, true, projectName, srcDir, { enablePrettier });
|
|
457
553
|
}
|
|
554
|
+
currentStep = "dependency installation";
|
|
458
555
|
spinner.text = `Installing dependencies using ${pm}...`;
|
|
459
556
|
let runtimeDeps = [];
|
|
460
557
|
let devDeps = [];
|
|
@@ -469,6 +566,10 @@ async function init() {
|
|
|
469
566
|
}
|
|
470
567
|
await installDependencies(pm, runtimeDeps, targetDir);
|
|
471
568
|
await installDependencies(pm, devDeps, targetDir, { dev: true });
|
|
569
|
+
if (enablePrettier) {
|
|
570
|
+
await installDependencies(pm, ["prettier"], targetDir, { dev: true });
|
|
571
|
+
}
|
|
572
|
+
currentStep = "module file generation";
|
|
472
573
|
spinner.text = "Fetching core module files...";
|
|
473
574
|
for (const file of coreModule.files) {
|
|
474
575
|
const { relativeTargetPath, targetPath } = resolveSafeTargetPath(targetDir, srcDir, file);
|
|
@@ -491,7 +592,14 @@ async function init() {
|
|
|
491
592
|
await fs4.ensureDir(path4.dirname(targetPath));
|
|
492
593
|
await fs4.writeFile(targetPath, content);
|
|
493
594
|
}
|
|
595
|
+
currentStep = "environment file setup";
|
|
494
596
|
await createInitialEnv(targetDir);
|
|
597
|
+
if (enablePrettier) {
|
|
598
|
+
currentStep = "prettier setup";
|
|
599
|
+
await setupPrettier(targetDir);
|
|
600
|
+
}
|
|
601
|
+
currentStep = "config write";
|
|
602
|
+
await writeZuroConfig(targetDir, zuroConfig);
|
|
495
603
|
spinner.succeed(chalk2.green("Project initialized successfully!"));
|
|
496
604
|
console.log(`
|
|
497
605
|
${chalk2.bold("Next steps:")}`);
|
|
@@ -502,8 +610,15 @@ ${chalk2.bold("Next steps:")}`);
|
|
|
502
610
|
console.log(`
|
|
503
611
|
${chalk2.dim("Add modules: zuro-cli add database, zuro-cli add auth")}`);
|
|
504
612
|
} catch (error) {
|
|
505
|
-
spinner.fail(chalk2.red(
|
|
506
|
-
|
|
613
|
+
spinner.fail(chalk2.red(`Failed during ${currentStep}.`));
|
|
614
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
615
|
+
console.error(chalk2.red(errorMessage));
|
|
616
|
+
console.log(`
|
|
617
|
+
${chalk2.bold("Retry:")}`);
|
|
618
|
+
if (targetDir !== cwd) {
|
|
619
|
+
console.log(chalk2.cyan(` cd ${projectName}`));
|
|
620
|
+
}
|
|
621
|
+
console.log(chalk2.cyan(" npx zuro-cli init"));
|
|
507
622
|
}
|
|
508
623
|
}
|
|
509
624
|
|
|
@@ -512,6 +627,7 @@ import prompts2 from "prompts";
|
|
|
512
627
|
import ora2 from "ora";
|
|
513
628
|
import path6 from "path";
|
|
514
629
|
import fs6 from "fs-extra";
|
|
630
|
+
import { randomBytes } from "crypto";
|
|
515
631
|
|
|
516
632
|
// src/utils/dependency.ts
|
|
517
633
|
import fs5 from "fs-extra";
|
|
@@ -557,6 +673,10 @@ var resolveDependencies = async (moduleDependencies, cwd) => {
|
|
|
557
673
|
|
|
558
674
|
// src/commands/add.ts
|
|
559
675
|
import chalk4 from "chalk";
|
|
676
|
+
var DEFAULT_DATABASE_URLS = {
|
|
677
|
+
"database-pg": "postgresql://postgres@localhost:5432/mydb",
|
|
678
|
+
"database-mysql": "mysql://root@localhost:3306/mydb"
|
|
679
|
+
};
|
|
560
680
|
function resolveSafeTargetPath2(projectRoot, srcDir, file) {
|
|
561
681
|
const targetPath = path6.resolve(projectRoot, srcDir, file.target);
|
|
562
682
|
const normalizedRoot = path6.resolve(projectRoot);
|
|
@@ -565,86 +685,338 @@ function resolveSafeTargetPath2(projectRoot, srcDir, file) {
|
|
|
565
685
|
}
|
|
566
686
|
return targetPath;
|
|
567
687
|
}
|
|
688
|
+
function resolvePackageManager(projectRoot) {
|
|
689
|
+
if (fs6.existsSync(path6.join(projectRoot, "pnpm-lock.yaml"))) {
|
|
690
|
+
return "pnpm";
|
|
691
|
+
}
|
|
692
|
+
if (fs6.existsSync(path6.join(projectRoot, "bun.lockb")) || fs6.existsSync(path6.join(projectRoot, "bun.lock"))) {
|
|
693
|
+
return "bun";
|
|
694
|
+
}
|
|
695
|
+
if (fs6.existsSync(path6.join(projectRoot, "yarn.lock"))) {
|
|
696
|
+
return "yarn";
|
|
697
|
+
}
|
|
698
|
+
return "npm";
|
|
699
|
+
}
|
|
700
|
+
function parseDatabaseDialect(value) {
|
|
701
|
+
const normalized = value?.trim().toLowerCase();
|
|
702
|
+
if (!normalized) {
|
|
703
|
+
return null;
|
|
704
|
+
}
|
|
705
|
+
if (normalized === "pg" || normalized === "postgres" || normalized === "postgresql" || normalized === "database-pg") {
|
|
706
|
+
return "database-pg";
|
|
707
|
+
}
|
|
708
|
+
if (normalized === "mysql" || normalized === "database-mysql") {
|
|
709
|
+
return "database-mysql";
|
|
710
|
+
}
|
|
711
|
+
return null;
|
|
712
|
+
}
|
|
713
|
+
function isDatabaseModule(moduleName) {
|
|
714
|
+
return moduleName === "database-pg" || moduleName === "database-mysql";
|
|
715
|
+
}
|
|
716
|
+
function validateDatabaseUrl(rawUrl, moduleName) {
|
|
717
|
+
const dbUrl = rawUrl.trim();
|
|
718
|
+
if (!dbUrl) {
|
|
719
|
+
throw new Error("Database URL cannot be empty.");
|
|
720
|
+
}
|
|
721
|
+
let parsed;
|
|
722
|
+
try {
|
|
723
|
+
parsed = new URL(dbUrl);
|
|
724
|
+
} catch {
|
|
725
|
+
throw new Error(`Invalid database URL: '${dbUrl}'.`);
|
|
726
|
+
}
|
|
727
|
+
const protocol = parsed.protocol.toLowerCase();
|
|
728
|
+
if (moduleName === "database-pg" && protocol !== "postgresql:" && protocol !== "postgres:") {
|
|
729
|
+
throw new Error("PostgreSQL URL must start with postgres:// or postgresql://");
|
|
730
|
+
}
|
|
731
|
+
if (moduleName === "database-mysql" && protocol !== "mysql:") {
|
|
732
|
+
throw new Error("MySQL URL must start with mysql://");
|
|
733
|
+
}
|
|
734
|
+
return dbUrl;
|
|
735
|
+
}
|
|
736
|
+
async function detectInstalledDatabaseDialect(projectRoot, srcDir) {
|
|
737
|
+
const dbIndexPath = path6.join(projectRoot, srcDir, "db", "index.ts");
|
|
738
|
+
if (!fs6.existsSync(dbIndexPath)) {
|
|
739
|
+
return null;
|
|
740
|
+
}
|
|
741
|
+
const content = await fs6.readFile(dbIndexPath, "utf-8");
|
|
742
|
+
if (content.includes("drizzle-orm/node-postgres") || content.includes(`from "pg"`)) {
|
|
743
|
+
return "database-pg";
|
|
744
|
+
}
|
|
745
|
+
if (content.includes("drizzle-orm/mysql2") || content.includes(`from "mysql2`)) {
|
|
746
|
+
return "database-mysql";
|
|
747
|
+
}
|
|
748
|
+
return null;
|
|
749
|
+
}
|
|
750
|
+
async function backupDatabaseFiles(projectRoot, srcDir) {
|
|
751
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
752
|
+
const backupRoot = path6.join(projectRoot, ".zuro", "backups", `database-${timestamp}`);
|
|
753
|
+
const candidates = [
|
|
754
|
+
path6.join(projectRoot, srcDir, "db", "index.ts"),
|
|
755
|
+
path6.join(projectRoot, "drizzle.config.ts")
|
|
756
|
+
];
|
|
757
|
+
let copied = false;
|
|
758
|
+
for (const filePath of candidates) {
|
|
759
|
+
if (!fs6.existsSync(filePath)) {
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
const relativePath = path6.relative(projectRoot, filePath);
|
|
763
|
+
const backupPath = path6.join(backupRoot, relativePath);
|
|
764
|
+
await fs6.ensureDir(path6.dirname(backupPath));
|
|
765
|
+
await fs6.copyFile(filePath, backupPath);
|
|
766
|
+
copied = true;
|
|
767
|
+
}
|
|
768
|
+
return copied ? backupRoot : null;
|
|
769
|
+
}
|
|
770
|
+
function databaseLabel(moduleName) {
|
|
771
|
+
return moduleName === "database-pg" ? "PostgreSQL" : "MySQL";
|
|
772
|
+
}
|
|
773
|
+
function getDatabaseSetupHint(moduleName, dbUrl) {
|
|
774
|
+
try {
|
|
775
|
+
const parsed = new URL(dbUrl);
|
|
776
|
+
const dbName = parsed.pathname.replace(/^\/+/, "") || "mydb";
|
|
777
|
+
if (moduleName === "database-pg") {
|
|
778
|
+
return `createdb ${dbName}`;
|
|
779
|
+
}
|
|
780
|
+
return `mysql -e "CREATE DATABASE IF NOT EXISTS ${dbName};"`;
|
|
781
|
+
} catch {
|
|
782
|
+
return moduleName === "database-pg" ? "createdb <database_name>" : `mysql -e "CREATE DATABASE IF NOT EXISTS <database_name>;"`;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
function getModuleDocsPath(moduleName) {
|
|
786
|
+
if (isDatabaseModule(moduleName)) {
|
|
787
|
+
return "database";
|
|
788
|
+
}
|
|
789
|
+
return moduleName;
|
|
790
|
+
}
|
|
791
|
+
function escapeRegex(value) {
|
|
792
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
793
|
+
}
|
|
794
|
+
async function hasEnvVariable(projectRoot, key) {
|
|
795
|
+
const envPath = path6.join(projectRoot, ".env");
|
|
796
|
+
if (!await fs6.pathExists(envPath)) {
|
|
797
|
+
return false;
|
|
798
|
+
}
|
|
799
|
+
const content = await fs6.readFile(envPath, "utf-8");
|
|
800
|
+
const pattern = new RegExp(`^${escapeRegex(key)}=`, "m");
|
|
801
|
+
return pattern.test(content);
|
|
802
|
+
}
|
|
803
|
+
async function isLikelyEmptyDirectory(cwd) {
|
|
804
|
+
const entries = await fs6.readdir(cwd);
|
|
805
|
+
const ignored = /* @__PURE__ */ new Set([".ds_store", "thumbs.db"]);
|
|
806
|
+
return entries.filter((entry) => !ignored.has(entry.toLowerCase())).length === 0;
|
|
807
|
+
}
|
|
808
|
+
async function ensureSchemaExport(projectRoot, srcDir, schemaFileName) {
|
|
809
|
+
const schemaIndexPath = path6.join(projectRoot, srcDir, "db", "schema", "index.ts");
|
|
810
|
+
if (!await fs6.pathExists(schemaIndexPath)) {
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
const exportLine = `export * from "./${schemaFileName}";`;
|
|
814
|
+
const content = await fs6.readFile(schemaIndexPath, "utf-8");
|
|
815
|
+
const normalized = content.replace(/\r\n/g, "\n");
|
|
816
|
+
const exportPattern = new RegExp(
|
|
817
|
+
`^\\s*export\\s*\\*\\s*from\\s*["']\\./${escapeRegex(schemaFileName)}["'];?\\s*$`,
|
|
818
|
+
"m"
|
|
819
|
+
);
|
|
820
|
+
if (exportPattern.test(normalized)) {
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
let next = normalized.replace(/^\s*export\s*\{\s*\};?\s*$/m, "").trimEnd();
|
|
824
|
+
if (next.length > 0) {
|
|
825
|
+
next += "\n\n";
|
|
826
|
+
}
|
|
827
|
+
next += `${exportLine}
|
|
828
|
+
`;
|
|
829
|
+
await fs6.writeFile(schemaIndexPath, next);
|
|
830
|
+
}
|
|
568
831
|
async function injectErrorHandler(projectRoot, srcDir) {
|
|
569
832
|
const appPath = path6.join(projectRoot, srcDir, "app.ts");
|
|
570
833
|
if (!fs6.existsSync(appPath)) {
|
|
571
834
|
return false;
|
|
572
835
|
}
|
|
573
836
|
let content = await fs6.readFile(appPath, "utf-8");
|
|
574
|
-
if (content.includes("errorHandler")) {
|
|
575
|
-
return true;
|
|
576
|
-
}
|
|
577
837
|
const errorImport = `import { errorHandler, notFoundHandler } from "./middleware/error-handler";`;
|
|
578
|
-
const
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
838
|
+
const hasErrorImport = content.includes(errorImport);
|
|
839
|
+
const hasNotFoundUse = /app\.use\(\s*notFoundHandler\s*\)/.test(content);
|
|
840
|
+
const hasErrorUse = /app\.use\(\s*errorHandler\s*\)/.test(content);
|
|
841
|
+
let modified = false;
|
|
842
|
+
let importInserted = hasErrorImport;
|
|
843
|
+
if (!hasErrorImport) {
|
|
844
|
+
const importRegex = /^import .+ from .+;?\s*$/gm;
|
|
845
|
+
let lastImportIndex = 0;
|
|
846
|
+
let match;
|
|
847
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
848
|
+
lastImportIndex = match.index + match[0].length;
|
|
849
|
+
}
|
|
850
|
+
if (lastImportIndex > 0) {
|
|
851
|
+
content = content.slice(0, lastImportIndex) + `
|
|
586
852
|
${errorImport}` + content.slice(lastImportIndex);
|
|
853
|
+
modified = true;
|
|
854
|
+
importInserted = true;
|
|
855
|
+
}
|
|
587
856
|
}
|
|
588
|
-
|
|
857
|
+
let setupInserted = hasNotFoundUse && hasErrorUse;
|
|
858
|
+
if (!setupInserted) {
|
|
859
|
+
const setupLines = [];
|
|
860
|
+
if (!hasNotFoundUse) {
|
|
861
|
+
setupLines.push("app.use(notFoundHandler);");
|
|
862
|
+
}
|
|
863
|
+
if (!hasErrorUse) {
|
|
864
|
+
setupLines.push("app.use(errorHandler);");
|
|
865
|
+
}
|
|
866
|
+
const errorSetup = `
|
|
589
867
|
// Error handling (must be last)
|
|
590
|
-
|
|
591
|
-
app.use(errorHandler);
|
|
868
|
+
${setupLines.join("\n")}
|
|
592
869
|
`;
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
870
|
+
const exportMatch = content.match(/export default app;?\s*$/m);
|
|
871
|
+
if (exportMatch && exportMatch.index !== void 0) {
|
|
872
|
+
content = content.slice(0, exportMatch.index) + errorSetup + "\n" + content.slice(exportMatch.index);
|
|
873
|
+
modified = true;
|
|
874
|
+
setupInserted = true;
|
|
875
|
+
}
|
|
596
876
|
}
|
|
597
|
-
|
|
598
|
-
|
|
877
|
+
if (modified) {
|
|
878
|
+
await fs6.writeFile(appPath, content);
|
|
879
|
+
}
|
|
880
|
+
return importInserted && setupInserted;
|
|
599
881
|
}
|
|
600
882
|
async function injectAuthRoutes(projectRoot, srcDir) {
|
|
601
883
|
const appPath = path6.join(projectRoot, srcDir, "app.ts");
|
|
602
|
-
if (!fs6.
|
|
884
|
+
if (!await fs6.pathExists(appPath)) {
|
|
603
885
|
return false;
|
|
604
886
|
}
|
|
605
|
-
let
|
|
606
|
-
|
|
607
|
-
|
|
887
|
+
let appContent = await fs6.readFile(appPath, "utf-8");
|
|
888
|
+
const authHandlerImport = `import { toNodeHandler } from "better-auth/node";`;
|
|
889
|
+
const authImport = `import { auth } from "./lib/auth";`;
|
|
890
|
+
const routeIndexUserImport = `import userRoutes from "./user.routes";`;
|
|
891
|
+
const appUserImport = `import userRoutes from "./routes/user.routes";`;
|
|
892
|
+
let appModified = false;
|
|
893
|
+
const appendImport = (source, line) => {
|
|
894
|
+
if (source.includes(line)) {
|
|
895
|
+
return { source, inserted: true };
|
|
896
|
+
}
|
|
897
|
+
const importRegex = /^import .+ from .+;?\s*$/gm;
|
|
898
|
+
let lastImportIndex = 0;
|
|
899
|
+
let match;
|
|
900
|
+
while ((match = importRegex.exec(source)) !== null) {
|
|
901
|
+
lastImportIndex = match.index + match[0].length;
|
|
902
|
+
}
|
|
903
|
+
if (lastImportIndex <= 0) {
|
|
904
|
+
return { source, inserted: false };
|
|
905
|
+
}
|
|
906
|
+
return {
|
|
907
|
+
source: source.slice(0, lastImportIndex) + `
|
|
908
|
+
${line}` + source.slice(lastImportIndex),
|
|
909
|
+
inserted: true
|
|
910
|
+
};
|
|
911
|
+
};
|
|
912
|
+
for (const importLine of [authHandlerImport, authImport]) {
|
|
913
|
+
const next = appendImport(appContent, importLine);
|
|
914
|
+
if (!next.inserted) {
|
|
915
|
+
return false;
|
|
916
|
+
}
|
|
917
|
+
if (next.source !== appContent) {
|
|
918
|
+
appContent = next.source;
|
|
919
|
+
appModified = true;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
const hasAuthMount = /toNodeHandler\(\s*auth\s*\)/.test(appContent) && /\/api\/auth/.test(appContent);
|
|
923
|
+
if (!hasAuthMount) {
|
|
924
|
+
const authMountLine = "app.all(/^\\/api\\/auth(?:\\/.*)?$/, toNodeHandler(auth));\n";
|
|
925
|
+
const jsonIndex = appContent.search(/^\s*app\.use\(\s*express\.json\(\)\s*\);\s*$/m);
|
|
926
|
+
let insertionIndex = jsonIndex;
|
|
927
|
+
if (insertionIndex < 0) {
|
|
928
|
+
const healthIndex = appContent.search(/^\s*app\.get\(\s*["']\/health["']\s*,/m);
|
|
929
|
+
insertionIndex = healthIndex;
|
|
930
|
+
}
|
|
931
|
+
if (insertionIndex < 0) {
|
|
932
|
+
const exportMatch = appContent.match(/export default app;?\s*$/m);
|
|
933
|
+
insertionIndex = exportMatch?.index ?? -1;
|
|
934
|
+
}
|
|
935
|
+
if (insertionIndex < 0) {
|
|
936
|
+
return false;
|
|
937
|
+
}
|
|
938
|
+
appContent = appContent.slice(0, insertionIndex) + authMountLine + appContent.slice(insertionIndex);
|
|
939
|
+
appModified = true;
|
|
608
940
|
}
|
|
609
|
-
const
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
941
|
+
const routeIndexPath = path6.join(projectRoot, srcDir, "routes", "index.ts");
|
|
942
|
+
if (await fs6.pathExists(routeIndexPath)) {
|
|
943
|
+
let routeContent = await fs6.readFile(routeIndexPath, "utf-8");
|
|
944
|
+
let routeModified = false;
|
|
945
|
+
const userImportResult = appendImport(routeContent, routeIndexUserImport);
|
|
946
|
+
if (!userImportResult.inserted) {
|
|
947
|
+
return false;
|
|
948
|
+
}
|
|
949
|
+
if (userImportResult.source !== routeContent) {
|
|
950
|
+
routeContent = userImportResult.source;
|
|
951
|
+
routeModified = true;
|
|
952
|
+
}
|
|
953
|
+
const hasUserRoute = /rootRouter\.use\(\s*["']\/users["']\s*,\s*userRoutes\s*\)/.test(routeContent);
|
|
954
|
+
if (!hasUserRoute) {
|
|
955
|
+
const routeSetup = `
|
|
956
|
+
// User routes
|
|
957
|
+
rootRouter.use("/users", userRoutes);
|
|
958
|
+
`;
|
|
959
|
+
const exportMatch = routeContent.match(/export default rootRouter;?\s*$/m);
|
|
960
|
+
if (!exportMatch || exportMatch.index === void 0) {
|
|
961
|
+
return false;
|
|
962
|
+
}
|
|
963
|
+
routeContent = routeContent.slice(0, exportMatch.index) + routeSetup + "\n" + routeContent.slice(exportMatch.index);
|
|
964
|
+
routeModified = true;
|
|
965
|
+
}
|
|
966
|
+
if (routeModified) {
|
|
967
|
+
await fs6.writeFile(routeIndexPath, routeContent);
|
|
968
|
+
}
|
|
969
|
+
} else {
|
|
970
|
+
const hasUserRoute = /app\.use\(\s*["']\/api\/users["']\s*,\s*userRoutes\s*\)/.test(appContent);
|
|
971
|
+
if (!hasUserRoute) {
|
|
972
|
+
const exportMatch = appContent.match(/export default app;?\s*$/m);
|
|
973
|
+
if (!exportMatch || exportMatch.index === void 0) {
|
|
974
|
+
return false;
|
|
975
|
+
}
|
|
976
|
+
const routeSetup = `
|
|
977
|
+
// User routes
|
|
614
978
|
app.use("/api/users", userRoutes);
|
|
615
979
|
`;
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
if (
|
|
629
|
-
|
|
630
|
-
}
|
|
631
|
-
await fs6.writeFile(appPath, content);
|
|
980
|
+
appContent = appContent.slice(0, exportMatch.index) + routeSetup + "\n" + appContent.slice(exportMatch.index);
|
|
981
|
+
appModified = true;
|
|
982
|
+
}
|
|
983
|
+
const userImportResult = appendImport(appContent, appUserImport);
|
|
984
|
+
if (!userImportResult.inserted) {
|
|
985
|
+
return false;
|
|
986
|
+
}
|
|
987
|
+
if (userImportResult.source !== appContent) {
|
|
988
|
+
appContent = userImportResult.source;
|
|
989
|
+
appModified = true;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
if (appModified) {
|
|
993
|
+
await fs6.writeFile(appPath, appContent);
|
|
994
|
+
}
|
|
632
995
|
return true;
|
|
633
996
|
}
|
|
634
997
|
var add = async (moduleName) => {
|
|
635
998
|
const projectRoot = process.cwd();
|
|
636
999
|
const projectConfig = await readZuroConfig(projectRoot);
|
|
637
1000
|
if (!projectConfig) {
|
|
1001
|
+
if (await isLikelyEmptyDirectory(projectRoot)) {
|
|
1002
|
+
showInitFirstMessage();
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
638
1005
|
showNonZuroProjectMessage();
|
|
639
1006
|
return;
|
|
640
1007
|
}
|
|
641
|
-
|
|
1008
|
+
const srcDir = projectConfig.srcDir || "src";
|
|
1009
|
+
let resolvedModuleName = moduleName;
|
|
1010
|
+
const parsedDialect = parseDatabaseDialect(moduleName);
|
|
1011
|
+
if (parsedDialect) {
|
|
1012
|
+
resolvedModuleName = parsedDialect;
|
|
1013
|
+
}
|
|
642
1014
|
let customDbUrl;
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
if (
|
|
1015
|
+
let usedDefaultDbUrl = false;
|
|
1016
|
+
let databaseBackupPath = null;
|
|
1017
|
+
let generatedAuthSecret = false;
|
|
1018
|
+
let authDatabaseDialect = null;
|
|
1019
|
+
if (resolvedModuleName === "database") {
|
|
648
1020
|
const variantResponse = await prompts2({
|
|
649
1021
|
type: "select",
|
|
650
1022
|
name: "variant",
|
|
@@ -655,22 +1027,39 @@ var add = async (moduleName) => {
|
|
|
655
1027
|
]
|
|
656
1028
|
});
|
|
657
1029
|
if (!variantResponse.variant) {
|
|
658
|
-
|
|
1030
|
+
console.log(chalk4.yellow("Operation cancelled."));
|
|
1031
|
+
return;
|
|
659
1032
|
}
|
|
660
|
-
|
|
661
|
-
const defaultUrl = DEFAULT_URLS[moduleName];
|
|
662
|
-
console.log(chalk4.dim(` Tip: Leave blank to use ${defaultUrl}
|
|
663
|
-
`));
|
|
664
|
-
const urlResponse = await prompts2({
|
|
665
|
-
type: "text",
|
|
666
|
-
name: "dbUrl",
|
|
667
|
-
message: "Database URL",
|
|
668
|
-
initial: ""
|
|
669
|
-
});
|
|
670
|
-
customDbUrl = urlResponse.dbUrl?.trim() || defaultUrl;
|
|
1033
|
+
resolvedModuleName = variantResponse.variant;
|
|
671
1034
|
}
|
|
672
|
-
if ((
|
|
673
|
-
const
|
|
1035
|
+
if (isDatabaseModule(resolvedModuleName)) {
|
|
1036
|
+
const installedDialect = await detectInstalledDatabaseDialect(projectRoot, srcDir);
|
|
1037
|
+
if (installedDialect && installedDialect !== resolvedModuleName) {
|
|
1038
|
+
console.log(
|
|
1039
|
+
chalk4.yellow(
|
|
1040
|
+
`
|
|
1041
|
+
\u26A0 Existing database setup detected: ${databaseLabel(installedDialect)}.`
|
|
1042
|
+
)
|
|
1043
|
+
);
|
|
1044
|
+
console.log(
|
|
1045
|
+
chalk4.yellow(
|
|
1046
|
+
` Switching to ${databaseLabel(resolvedModuleName)} will overwrite db files and drizzle config.
|
|
1047
|
+
`
|
|
1048
|
+
)
|
|
1049
|
+
);
|
|
1050
|
+
const switchResponse = await prompts2({
|
|
1051
|
+
type: "confirm",
|
|
1052
|
+
name: "proceed",
|
|
1053
|
+
message: "Continue and switch database dialect?",
|
|
1054
|
+
initial: false
|
|
1055
|
+
});
|
|
1056
|
+
if (!switchResponse.proceed) {
|
|
1057
|
+
console.log(chalk4.yellow("Operation cancelled."));
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
databaseBackupPath = await backupDatabaseFiles(projectRoot, srcDir);
|
|
1061
|
+
}
|
|
1062
|
+
const defaultUrl = DEFAULT_DATABASE_URLS[resolvedModuleName];
|
|
674
1063
|
console.log(chalk4.dim(` Tip: Leave blank to use ${defaultUrl}
|
|
675
1064
|
`));
|
|
676
1065
|
const response = await prompts2({
|
|
@@ -679,53 +1068,82 @@ var add = async (moduleName) => {
|
|
|
679
1068
|
message: "Database URL",
|
|
680
1069
|
initial: ""
|
|
681
1070
|
});
|
|
682
|
-
|
|
1071
|
+
if (response.dbUrl === void 0) {
|
|
1072
|
+
console.log(chalk4.yellow("Operation cancelled."));
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
const enteredUrl = response.dbUrl?.trim() || "";
|
|
1076
|
+
usedDefaultDbUrl = enteredUrl.length === 0;
|
|
1077
|
+
customDbUrl = validateDatabaseUrl(enteredUrl || defaultUrl, resolvedModuleName);
|
|
683
1078
|
}
|
|
684
|
-
const
|
|
1079
|
+
const pm = resolvePackageManager(projectRoot);
|
|
1080
|
+
const spinner = ora2(`Checking registry for ${resolvedModuleName}...`).start();
|
|
1081
|
+
let currentStep = "package manager preflight";
|
|
685
1082
|
try {
|
|
1083
|
+
spinner.text = `Checking ${pm} availability...`;
|
|
1084
|
+
await ensurePackageManagerAvailable(pm);
|
|
1085
|
+
currentStep = "registry fetch";
|
|
1086
|
+
spinner.text = `Checking registry for ${resolvedModuleName}...`;
|
|
686
1087
|
const registryContext = await fetchRegistry();
|
|
687
|
-
const module = registryContext.manifest.modules[
|
|
1088
|
+
const module = registryContext.manifest.modules[resolvedModuleName];
|
|
688
1089
|
if (!module) {
|
|
689
|
-
spinner.fail(`Module '${
|
|
1090
|
+
spinner.fail(`Module '${resolvedModuleName}' not found.`);
|
|
690
1091
|
return;
|
|
691
1092
|
}
|
|
692
|
-
spinner.succeed(`Found module: ${chalk4.cyan(
|
|
1093
|
+
spinner.succeed(`Found module: ${chalk4.cyan(resolvedModuleName)}`);
|
|
693
1094
|
const moduleDeps = module.moduleDependencies || [];
|
|
1095
|
+
currentStep = "module dependency resolution";
|
|
694
1096
|
await resolveDependencies(moduleDeps, projectRoot);
|
|
1097
|
+
currentStep = "dependency installation";
|
|
695
1098
|
spinner.start("Installing dependencies...");
|
|
696
|
-
let pm = "npm";
|
|
697
|
-
if (fs6.existsSync(path6.join(projectRoot, "pnpm-lock.yaml"))) {
|
|
698
|
-
pm = "pnpm";
|
|
699
|
-
} else if (fs6.existsSync(path6.join(projectRoot, "bun.lockb"))) {
|
|
700
|
-
pm = "bun";
|
|
701
|
-
} else if (fs6.existsSync(path6.join(projectRoot, "yarn.lock"))) {
|
|
702
|
-
pm = "yarn";
|
|
703
|
-
}
|
|
704
1099
|
await installDependencies(pm, module.dependencies || [], projectRoot);
|
|
705
1100
|
await installDependencies(pm, module.devDependencies || [], projectRoot, { dev: true });
|
|
706
1101
|
spinner.succeed("Dependencies installed");
|
|
1102
|
+
currentStep = "module scaffolding";
|
|
707
1103
|
spinner.start("Scaffolding files...");
|
|
1104
|
+
if (resolvedModuleName === "auth") {
|
|
1105
|
+
authDatabaseDialect = await detectInstalledDatabaseDialect(projectRoot, srcDir);
|
|
1106
|
+
}
|
|
708
1107
|
for (const file of module.files) {
|
|
709
|
-
|
|
1108
|
+
let fetchPath = file.path;
|
|
1109
|
+
let expectedSha256 = file.sha256;
|
|
1110
|
+
let expectedSize = file.size;
|
|
1111
|
+
if (resolvedModuleName === "auth" && file.target === "db/schema/auth.ts" && authDatabaseDialect === "database-mysql") {
|
|
1112
|
+
fetchPath = "express/db/schema/auth.mysql.ts";
|
|
1113
|
+
expectedSha256 = void 0;
|
|
1114
|
+
expectedSize = void 0;
|
|
1115
|
+
}
|
|
1116
|
+
let content = await fetchFile(fetchPath, {
|
|
710
1117
|
baseUrl: registryContext.fileBaseUrl,
|
|
711
|
-
expectedSha256
|
|
712
|
-
expectedSize
|
|
1118
|
+
expectedSha256,
|
|
1119
|
+
expectedSize
|
|
713
1120
|
});
|
|
1121
|
+
if (isDatabaseModule(resolvedModuleName) && file.target === "../drizzle.config.ts") {
|
|
1122
|
+
const normalizedSrcDir = srcDir.replace(/\\/g, "/");
|
|
1123
|
+
content = content.replace(
|
|
1124
|
+
/schema:\s*["'][^"']+["']/,
|
|
1125
|
+
`schema: "./${normalizedSrcDir}/db/schema/*"`
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
714
1128
|
const targetPath = resolveSafeTargetPath2(projectRoot, srcDir, file);
|
|
715
1129
|
await fs6.ensureDir(path6.dirname(targetPath));
|
|
716
1130
|
await fs6.writeFile(targetPath, content);
|
|
717
1131
|
}
|
|
1132
|
+
const schemaExports = module.files.map((file) => file.target.replace(/\\/g, "/")).filter((target) => /^db\/schema\/[^/]+\.ts$/.test(target)).map((target) => path6.posix.basename(target, ".ts")).filter((name) => name !== "index");
|
|
1133
|
+
for (const schemaFileName of schemaExports) {
|
|
1134
|
+
await ensureSchemaExport(projectRoot, srcDir, schemaFileName);
|
|
1135
|
+
}
|
|
718
1136
|
spinner.succeed("Files generated");
|
|
719
|
-
if (
|
|
720
|
-
spinner.start("Configuring routes
|
|
1137
|
+
if (resolvedModuleName === "auth") {
|
|
1138
|
+
spinner.start("Configuring routes...");
|
|
721
1139
|
const injected = await injectAuthRoutes(projectRoot, srcDir);
|
|
722
1140
|
if (injected) {
|
|
723
|
-
spinner.succeed("Routes configured
|
|
1141
|
+
spinner.succeed("Routes configured");
|
|
724
1142
|
} else {
|
|
725
|
-
spinner.warn("Could not
|
|
1143
|
+
spinner.warn("Could not configure routes automatically");
|
|
726
1144
|
}
|
|
727
1145
|
}
|
|
728
|
-
if (
|
|
1146
|
+
if (resolvedModuleName === "error-handler") {
|
|
729
1147
|
spinner.start("Configuring error handler in app.ts...");
|
|
730
1148
|
const injected = await injectErrorHandler(projectRoot, srcDir);
|
|
731
1149
|
if (injected) {
|
|
@@ -734,78 +1152,63 @@ var add = async (moduleName) => {
|
|
|
734
1152
|
spinner.warn("Could not find app.ts - error handler needs manual setup");
|
|
735
1153
|
}
|
|
736
1154
|
}
|
|
737
|
-
const envConfig = ENV_CONFIGS[
|
|
1155
|
+
const envConfig = ENV_CONFIGS[resolvedModuleName];
|
|
738
1156
|
if (envConfig) {
|
|
1157
|
+
currentStep = "environment configuration";
|
|
739
1158
|
spinner.start("Updating environment configuration...");
|
|
740
1159
|
const envVars = { ...envConfig.envVars };
|
|
741
|
-
if (customDbUrl && (
|
|
1160
|
+
if (customDbUrl && isDatabaseModule(resolvedModuleName)) {
|
|
742
1161
|
envVars.DATABASE_URL = customDbUrl;
|
|
743
1162
|
}
|
|
744
|
-
|
|
1163
|
+
if (resolvedModuleName === "auth") {
|
|
1164
|
+
const hasExistingSecret = await hasEnvVariable(projectRoot, "BETTER_AUTH_SECRET");
|
|
1165
|
+
if (!hasExistingSecret) {
|
|
1166
|
+
envVars.BETTER_AUTH_SECRET = randomBytes(32).toString("hex");
|
|
1167
|
+
generatedAuthSecret = true;
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
await updateEnvFile(projectRoot, envVars, true, {
|
|
1171
|
+
overwriteExisting: isDatabaseModule(resolvedModuleName)
|
|
1172
|
+
});
|
|
745
1173
|
await updateEnvSchema(projectRoot, srcDir, envConfig.schemaFields);
|
|
746
1174
|
spinner.succeed("Environment configured");
|
|
747
1175
|
}
|
|
748
1176
|
console.log(chalk4.green(`
|
|
749
|
-
\u2714 ${
|
|
1177
|
+
\u2714 ${resolvedModuleName} added successfully!
|
|
750
1178
|
`));
|
|
751
|
-
if (
|
|
752
|
-
console.log(chalk4.
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
1179
|
+
if (databaseBackupPath) {
|
|
1180
|
+
console.log(chalk4.blue(`\u2139 Backup created at: ${databaseBackupPath}
|
|
1181
|
+
`));
|
|
1182
|
+
}
|
|
1183
|
+
const docsPath = getModuleDocsPath(resolvedModuleName);
|
|
1184
|
+
const docsUrl = `https://zuro-cli.devbybriyan.com/docs/${docsPath}`;
|
|
1185
|
+
console.log(chalk4.blue(`\u2139 Docs: ${docsUrl}`));
|
|
1186
|
+
if (isDatabaseModule(resolvedModuleName)) {
|
|
1187
|
+
if (usedDefaultDbUrl) {
|
|
1188
|
+
console.log(chalk4.yellow("\u2139 Review DATABASE_URL in .env if your local DB config differs."));
|
|
1189
|
+
}
|
|
1190
|
+
const setupHint = getDatabaseSetupHint(
|
|
1191
|
+
resolvedModuleName,
|
|
1192
|
+
customDbUrl || DEFAULT_DATABASE_URLS[resolvedModuleName]
|
|
756
1193
|
);
|
|
757
|
-
console.log(chalk4.yellow(
|
|
758
|
-
console.log(chalk4.
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
} else if (moduleName === "error-handler") {
|
|
766
|
-
console.log(chalk4.bold("\u{1F4CB} Usage:\n"));
|
|
767
|
-
console.log(chalk4.yellow("Throw errors in your controllers:"));
|
|
768
|
-
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"));
|
|
769
|
-
console.log(chalk4.white(` import { UnauthorizedError, NotFoundError } from "./lib/errors";`));
|
|
770
|
-
console.log("");
|
|
771
|
-
console.log(chalk4.white(` throw new UnauthorizedError("Invalid credentials");`));
|
|
772
|
-
console.log(chalk4.white(` throw new NotFoundError("User not found");`));
|
|
773
|
-
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"));
|
|
774
|
-
console.log(chalk4.yellow("Available error classes:"));
|
|
775
|
-
console.log(chalk4.dim(" BadRequestError (400)"));
|
|
776
|
-
console.log(chalk4.dim(" UnauthorizedError (401)"));
|
|
777
|
-
console.log(chalk4.dim(" ForbiddenError (403)"));
|
|
778
|
-
console.log(chalk4.dim(" NotFoundError (404)"));
|
|
779
|
-
console.log(chalk4.dim(" ConflictError (409)"));
|
|
780
|
-
console.log(chalk4.dim(" ValidationError (422)"));
|
|
781
|
-
console.log(chalk4.dim(" InternalServerError (500)\n"));
|
|
782
|
-
console.log(chalk4.yellow("Wrap async handlers:"));
|
|
783
|
-
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"));
|
|
784
|
-
console.log(chalk4.white(` import { asyncHandler } from "./middleware/error-handler";`));
|
|
785
|
-
console.log("");
|
|
786
|
-
console.log(chalk4.white(` router.get("/users", asyncHandler(async (req, res) => {`));
|
|
787
|
-
console.log(chalk4.white(" // errors auto-caught"));
|
|
788
|
-
console.log(chalk4.white(" }));"));
|
|
789
|
-
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"));
|
|
790
|
-
} else if (moduleName.includes("database")) {
|
|
791
|
-
console.log(chalk4.bold("\u{1F4CB} Next Steps:\n"));
|
|
792
|
-
let stepNum = 1;
|
|
793
|
-
if (!customDbUrl) {
|
|
794
|
-
console.log(chalk4.yellow(`${stepNum}. Update DATABASE_URL in .env:`));
|
|
795
|
-
console.log(
|
|
796
|
-
chalk4.dim(" We added a placeholder. Update with your actual database credentials.\n")
|
|
797
|
-
);
|
|
798
|
-
stepNum++;
|
|
1194
|
+
console.log(chalk4.yellow(`\u2139 Ensure DB exists: ${setupHint}`));
|
|
1195
|
+
console.log(chalk4.yellow("\u2139 Run migrations: npx drizzle-kit generate && npx drizzle-kit migrate"));
|
|
1196
|
+
}
|
|
1197
|
+
if (resolvedModuleName === "auth") {
|
|
1198
|
+
if (generatedAuthSecret) {
|
|
1199
|
+
console.log(chalk4.yellow("\u2139 BETTER_AUTH_SECRET was generated automatically."));
|
|
1200
|
+
} else {
|
|
1201
|
+
console.log(chalk4.yellow("\u2139 Review BETTER_AUTH_SECRET and BETTER_AUTH_URL in .env."));
|
|
799
1202
|
}
|
|
800
|
-
console.log(chalk4.yellow(
|
|
801
|
-
console.log(chalk4.dim(" Add table files and export from index.ts\n"));
|
|
802
|
-
stepNum++;
|
|
803
|
-
console.log(chalk4.yellow(`${stepNum}. Run migrations:`));
|
|
804
|
-
console.log(chalk4.cyan(" npx drizzle-kit generate"));
|
|
805
|
-
console.log(chalk4.cyan(" npx drizzle-kit migrate\n"));
|
|
1203
|
+
console.log(chalk4.yellow("\u2139 Run migrations: npx drizzle-kit generate && npx drizzle-kit migrate"));
|
|
806
1204
|
}
|
|
807
1205
|
} catch (error) {
|
|
808
|
-
spinner.fail(`Failed
|
|
1206
|
+
spinner.fail(chalk4.red(`Failed during ${currentStep}.`));
|
|
1207
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1208
|
+
console.error(chalk4.red(errorMessage));
|
|
1209
|
+
console.log(`
|
|
1210
|
+
${chalk4.bold("Retry:")}`);
|
|
1211
|
+
console.log(chalk4.cyan(` npx zuro-cli add ${resolvedModuleName}`));
|
|
809
1212
|
}
|
|
810
1213
|
};
|
|
811
1214
|
|