zuro-cli 0.0.2-beta.3 → 0.0.2-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +422 -150
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +422 -150
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -202,18 +202,23 @@ async function ensurePackageManagerAvailable(pm) {
|
|
|
202
202
|
);
|
|
203
203
|
}
|
|
204
204
|
}
|
|
205
|
-
async function initPackageJson(cwd, force = false, packageName = "zuro-app", srcDir = "src") {
|
|
205
|
+
async function initPackageJson(cwd, force = false, packageName = "zuro-app", srcDir = "src", options = {}) {
|
|
206
206
|
const pkgPath = import_path.default.join(cwd, "package.json");
|
|
207
207
|
if (force || !await import_fs_extra.default.pathExists(pkgPath)) {
|
|
208
|
+
const scripts = {
|
|
209
|
+
"dev": `tsx watch ${srcDir}/server.ts`,
|
|
210
|
+
"build": "tsc",
|
|
211
|
+
"start": "node dist/server.js"
|
|
212
|
+
};
|
|
213
|
+
if (options.enablePrettier) {
|
|
214
|
+
scripts["format"] = "prettier --write .";
|
|
215
|
+
scripts["format:check"] = "prettier --check .";
|
|
216
|
+
}
|
|
208
217
|
await import_fs_extra.default.writeJson(pkgPath, {
|
|
209
218
|
name: normalizePackageName(packageName),
|
|
210
219
|
version: "0.0.1",
|
|
211
220
|
private: true,
|
|
212
|
-
scripts
|
|
213
|
-
"dev": `tsx watch ${srcDir}/server.ts`,
|
|
214
|
-
"build": "tsc",
|
|
215
|
-
"start": "node dist/server.js"
|
|
216
|
-
}
|
|
221
|
+
scripts
|
|
217
222
|
}, { spaces: 2 });
|
|
218
223
|
}
|
|
219
224
|
}
|
|
@@ -250,8 +255,9 @@ async function installDependencies(pm, deps, cwd, options = {}) {
|
|
|
250
255
|
var import_fs_extra2 = __toESM(require("fs-extra"));
|
|
251
256
|
var import_path2 = __toESM(require("path"));
|
|
252
257
|
var import_os = __toESM(require("os"));
|
|
253
|
-
var updateEnvFile = async (cwd, variables, createIfMissing = true) => {
|
|
258
|
+
var updateEnvFile = async (cwd, variables, createIfMissing = true, options = {}) => {
|
|
254
259
|
const envPath = import_path2.default.join(cwd, ".env");
|
|
260
|
+
const overwriteExisting = options.overwriteExisting ?? false;
|
|
255
261
|
let content = "";
|
|
256
262
|
if (import_fs_extra2.default.existsSync(envPath)) {
|
|
257
263
|
content = await import_fs_extra2.default.readFile(envPath, "utf-8");
|
|
@@ -260,14 +266,25 @@ var updateEnvFile = async (cwd, variables, createIfMissing = true) => {
|
|
|
260
266
|
}
|
|
261
267
|
let modified = false;
|
|
262
268
|
for (const [key, value] of Object.entries(variables)) {
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
269
|
+
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
270
|
+
const regex = new RegExp(`^${escapedKey}=.*$`, "m");
|
|
271
|
+
if (regex.test(content)) {
|
|
272
|
+
if (!overwriteExisting) {
|
|
273
|
+
continue;
|
|
267
274
|
}
|
|
268
|
-
|
|
269
|
-
|
|
275
|
+
const nextLine = `${key}=${value}`;
|
|
276
|
+
const updated = content.replace(regex, nextLine);
|
|
277
|
+
if (updated !== content) {
|
|
278
|
+
content = updated;
|
|
279
|
+
modified = true;
|
|
280
|
+
}
|
|
281
|
+
continue;
|
|
270
282
|
}
|
|
283
|
+
if (content && !content.endsWith("\n")) {
|
|
284
|
+
content += import_os.default.EOL;
|
|
285
|
+
}
|
|
286
|
+
content += `${key}=${value}${import_os.default.EOL}`;
|
|
287
|
+
modified = true;
|
|
271
288
|
}
|
|
272
289
|
if (modified || !import_fs_extra2.default.existsSync(envPath)) {
|
|
273
290
|
await import_fs_extra2.default.writeFile(envPath, content);
|
|
@@ -390,6 +407,13 @@ function showNonZuroProjectMessage() {
|
|
|
390
407
|
console.log("- a fresh/empty directory, or");
|
|
391
408
|
console.log("- an existing project already managed by Zuro CLI.");
|
|
392
409
|
}
|
|
410
|
+
function showInitFirstMessage() {
|
|
411
|
+
console.log(import_chalk.default.yellow("No Zuro project found in this directory."));
|
|
412
|
+
console.log("");
|
|
413
|
+
console.log(import_chalk.default.yellow("Run init first, then add modules."));
|
|
414
|
+
console.log("");
|
|
415
|
+
console.log(import_chalk.default.cyan("npx zuro-cli init"));
|
|
416
|
+
}
|
|
393
417
|
|
|
394
418
|
// src/commands/init.ts
|
|
395
419
|
function resolveSafeTargetPath(projectRoot, srcDir, file) {
|
|
@@ -419,6 +443,33 @@ async function ensureSafeTargetDirectory(targetDir, cwd, projectName) {
|
|
|
419
443
|
});
|
|
420
444
|
return response.proceed === true;
|
|
421
445
|
}
|
|
446
|
+
async function setupPrettier(targetDir) {
|
|
447
|
+
const prettierConfigPath = import_path4.default.join(targetDir, ".prettierrc");
|
|
448
|
+
const prettierIgnorePath = import_path4.default.join(targetDir, ".prettierignore");
|
|
449
|
+
if (!await import_fs_extra4.default.pathExists(prettierConfigPath)) {
|
|
450
|
+
const prettierConfig = {
|
|
451
|
+
semi: true,
|
|
452
|
+
singleQuote: false,
|
|
453
|
+
trailingComma: "es5",
|
|
454
|
+
printWidth: 100,
|
|
455
|
+
tabWidth: 2
|
|
456
|
+
};
|
|
457
|
+
await import_fs_extra4.default.writeJson(prettierConfigPath, prettierConfig, { spaces: 2 });
|
|
458
|
+
}
|
|
459
|
+
if (!await import_fs_extra4.default.pathExists(prettierIgnorePath)) {
|
|
460
|
+
const ignoreContent = `node_modules
|
|
461
|
+
dist
|
|
462
|
+
build
|
|
463
|
+
coverage
|
|
464
|
+
.next
|
|
465
|
+
pnpm-lock.yaml
|
|
466
|
+
package-lock.json
|
|
467
|
+
bun.lock
|
|
468
|
+
bun.lockb
|
|
469
|
+
`;
|
|
470
|
+
await import_fs_extra4.default.writeFile(prettierIgnorePath, ignoreContent);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
422
473
|
async function init() {
|
|
423
474
|
const cwd = process.cwd();
|
|
424
475
|
const isExistingProject = await import_fs_extra4.default.pathExists(import_path4.default.join(cwd, "package.json"));
|
|
@@ -431,6 +482,7 @@ async function init() {
|
|
|
431
482
|
let pm = "npm";
|
|
432
483
|
let srcDir = "src";
|
|
433
484
|
let projectName = import_path4.default.basename(cwd);
|
|
485
|
+
let enablePrettier = false;
|
|
434
486
|
if (isExistingProject) {
|
|
435
487
|
console.log(import_chalk2.default.blue("\u2139 Existing project detected."));
|
|
436
488
|
projectName = import_path4.default.basename(cwd);
|
|
@@ -468,6 +520,12 @@ async function init() {
|
|
|
468
520
|
{ title: "bun", value: "bun" }
|
|
469
521
|
],
|
|
470
522
|
initial: 0
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
type: "confirm",
|
|
526
|
+
name: "prettier",
|
|
527
|
+
message: "Setup Prettier?",
|
|
528
|
+
initial: true
|
|
471
529
|
}
|
|
472
530
|
]);
|
|
473
531
|
if (response.pm === void 0) {
|
|
@@ -475,6 +533,7 @@ async function init() {
|
|
|
475
533
|
return;
|
|
476
534
|
}
|
|
477
535
|
pm = response.pm;
|
|
536
|
+
enablePrettier = response.prettier === true;
|
|
478
537
|
srcDir = "src";
|
|
479
538
|
if (!response.path || response.path.trim() === "") {
|
|
480
539
|
projectName = import_path4.default.basename(cwd);
|
|
@@ -513,7 +572,7 @@ async function init() {
|
|
|
513
572
|
spinner.text = "Initializing project...";
|
|
514
573
|
const hasPackageJson = await import_fs_extra4.default.pathExists(import_path4.default.join(targetDir, "package.json"));
|
|
515
574
|
if (!hasPackageJson) {
|
|
516
|
-
await initPackageJson(targetDir, true, projectName, srcDir);
|
|
575
|
+
await initPackageJson(targetDir, true, projectName, srcDir, { enablePrettier });
|
|
517
576
|
}
|
|
518
577
|
currentStep = "dependency installation";
|
|
519
578
|
spinner.text = `Installing dependencies using ${pm}...`;
|
|
@@ -530,6 +589,9 @@ async function init() {
|
|
|
530
589
|
}
|
|
531
590
|
await installDependencies(pm, runtimeDeps, targetDir);
|
|
532
591
|
await installDependencies(pm, devDeps, targetDir, { dev: true });
|
|
592
|
+
if (enablePrettier) {
|
|
593
|
+
await installDependencies(pm, ["prettier"], targetDir, { dev: true });
|
|
594
|
+
}
|
|
533
595
|
currentStep = "module file generation";
|
|
534
596
|
spinner.text = "Fetching core module files...";
|
|
535
597
|
for (const file of coreModule.files) {
|
|
@@ -555,6 +617,10 @@ async function init() {
|
|
|
555
617
|
}
|
|
556
618
|
currentStep = "environment file setup";
|
|
557
619
|
await createInitialEnv(targetDir);
|
|
620
|
+
if (enablePrettier) {
|
|
621
|
+
currentStep = "prettier setup";
|
|
622
|
+
await setupPrettier(targetDir);
|
|
623
|
+
}
|
|
558
624
|
currentStep = "config write";
|
|
559
625
|
await writeZuroConfig(targetDir, zuroConfig);
|
|
560
626
|
spinner.succeed(import_chalk2.default.green("Project initialized successfully!"));
|
|
@@ -584,6 +650,7 @@ var import_prompts2 = __toESM(require("prompts"));
|
|
|
584
650
|
var import_ora2 = __toESM(require("ora"));
|
|
585
651
|
var import_path6 = __toESM(require("path"));
|
|
586
652
|
var import_fs_extra6 = __toESM(require("fs-extra"));
|
|
653
|
+
var import_node_crypto2 = require("crypto");
|
|
587
654
|
|
|
588
655
|
// src/utils/dependency.ts
|
|
589
656
|
var import_fs_extra5 = __toESM(require("fs-extra"));
|
|
@@ -629,6 +696,10 @@ var resolveDependencies = async (moduleDependencies, cwd) => {
|
|
|
629
696
|
|
|
630
697
|
// src/commands/add.ts
|
|
631
698
|
var import_chalk4 = __toESM(require("chalk"));
|
|
699
|
+
var DEFAULT_DATABASE_URLS = {
|
|
700
|
+
"database-pg": "postgresql://postgres@localhost:5432/mydb",
|
|
701
|
+
"database-mysql": "mysql://root@localhost:3306/mydb"
|
|
702
|
+
};
|
|
632
703
|
function resolveSafeTargetPath2(projectRoot, srcDir, file) {
|
|
633
704
|
const targetPath = import_path6.default.resolve(projectRoot, srcDir, file.target);
|
|
634
705
|
const normalizedRoot = import_path6.default.resolve(projectRoot);
|
|
@@ -637,37 +708,176 @@ function resolveSafeTargetPath2(projectRoot, srcDir, file) {
|
|
|
637
708
|
}
|
|
638
709
|
return targetPath;
|
|
639
710
|
}
|
|
711
|
+
function resolvePackageManager(projectRoot) {
|
|
712
|
+
if (import_fs_extra6.default.existsSync(import_path6.default.join(projectRoot, "pnpm-lock.yaml"))) {
|
|
713
|
+
return "pnpm";
|
|
714
|
+
}
|
|
715
|
+
if (import_fs_extra6.default.existsSync(import_path6.default.join(projectRoot, "bun.lockb")) || import_fs_extra6.default.existsSync(import_path6.default.join(projectRoot, "bun.lock"))) {
|
|
716
|
+
return "bun";
|
|
717
|
+
}
|
|
718
|
+
if (import_fs_extra6.default.existsSync(import_path6.default.join(projectRoot, "yarn.lock"))) {
|
|
719
|
+
return "yarn";
|
|
720
|
+
}
|
|
721
|
+
return "npm";
|
|
722
|
+
}
|
|
723
|
+
function parseDatabaseDialect(value) {
|
|
724
|
+
const normalized = value?.trim().toLowerCase();
|
|
725
|
+
if (!normalized) {
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
if (normalized === "pg" || normalized === "postgres" || normalized === "postgresql" || normalized === "database-pg") {
|
|
729
|
+
return "database-pg";
|
|
730
|
+
}
|
|
731
|
+
if (normalized === "mysql" || normalized === "database-mysql") {
|
|
732
|
+
return "database-mysql";
|
|
733
|
+
}
|
|
734
|
+
return null;
|
|
735
|
+
}
|
|
736
|
+
function isDatabaseModule(moduleName) {
|
|
737
|
+
return moduleName === "database-pg" || moduleName === "database-mysql";
|
|
738
|
+
}
|
|
739
|
+
function validateDatabaseUrl(rawUrl, moduleName) {
|
|
740
|
+
const dbUrl = rawUrl.trim();
|
|
741
|
+
if (!dbUrl) {
|
|
742
|
+
throw new Error("Database URL cannot be empty.");
|
|
743
|
+
}
|
|
744
|
+
let parsed;
|
|
745
|
+
try {
|
|
746
|
+
parsed = new URL(dbUrl);
|
|
747
|
+
} catch {
|
|
748
|
+
throw new Error(`Invalid database URL: '${dbUrl}'.`);
|
|
749
|
+
}
|
|
750
|
+
const protocol = parsed.protocol.toLowerCase();
|
|
751
|
+
if (moduleName === "database-pg" && protocol !== "postgresql:" && protocol !== "postgres:") {
|
|
752
|
+
throw new Error("PostgreSQL URL must start with postgres:// or postgresql://");
|
|
753
|
+
}
|
|
754
|
+
if (moduleName === "database-mysql" && protocol !== "mysql:") {
|
|
755
|
+
throw new Error("MySQL URL must start with mysql://");
|
|
756
|
+
}
|
|
757
|
+
return dbUrl;
|
|
758
|
+
}
|
|
759
|
+
async function detectInstalledDatabaseDialect(projectRoot, srcDir) {
|
|
760
|
+
const dbIndexPath = import_path6.default.join(projectRoot, srcDir, "db", "index.ts");
|
|
761
|
+
if (!import_fs_extra6.default.existsSync(dbIndexPath)) {
|
|
762
|
+
return null;
|
|
763
|
+
}
|
|
764
|
+
const content = await import_fs_extra6.default.readFile(dbIndexPath, "utf-8");
|
|
765
|
+
if (content.includes("drizzle-orm/node-postgres") || content.includes(`from "pg"`)) {
|
|
766
|
+
return "database-pg";
|
|
767
|
+
}
|
|
768
|
+
if (content.includes("drizzle-orm/mysql2") || content.includes(`from "mysql2`)) {
|
|
769
|
+
return "database-mysql";
|
|
770
|
+
}
|
|
771
|
+
return null;
|
|
772
|
+
}
|
|
773
|
+
async function backupDatabaseFiles(projectRoot, srcDir) {
|
|
774
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
775
|
+
const backupRoot = import_path6.default.join(projectRoot, ".zuro", "backups", `database-${timestamp}`);
|
|
776
|
+
const candidates = [
|
|
777
|
+
import_path6.default.join(projectRoot, srcDir, "db", "index.ts"),
|
|
778
|
+
import_path6.default.join(projectRoot, "drizzle.config.ts")
|
|
779
|
+
];
|
|
780
|
+
let copied = false;
|
|
781
|
+
for (const filePath of candidates) {
|
|
782
|
+
if (!import_fs_extra6.default.existsSync(filePath)) {
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
const relativePath = import_path6.default.relative(projectRoot, filePath);
|
|
786
|
+
const backupPath = import_path6.default.join(backupRoot, relativePath);
|
|
787
|
+
await import_fs_extra6.default.ensureDir(import_path6.default.dirname(backupPath));
|
|
788
|
+
await import_fs_extra6.default.copyFile(filePath, backupPath);
|
|
789
|
+
copied = true;
|
|
790
|
+
}
|
|
791
|
+
return copied ? backupRoot : null;
|
|
792
|
+
}
|
|
793
|
+
function databaseLabel(moduleName) {
|
|
794
|
+
return moduleName === "database-pg" ? "PostgreSQL" : "MySQL";
|
|
795
|
+
}
|
|
796
|
+
function getDatabaseSetupHint(moduleName, dbUrl) {
|
|
797
|
+
try {
|
|
798
|
+
const parsed = new URL(dbUrl);
|
|
799
|
+
const dbName = parsed.pathname.replace(/^\/+/, "") || "mydb";
|
|
800
|
+
if (moduleName === "database-pg") {
|
|
801
|
+
return `createdb ${dbName}`;
|
|
802
|
+
}
|
|
803
|
+
return `mysql -e "CREATE DATABASE IF NOT EXISTS ${dbName};"`;
|
|
804
|
+
} catch {
|
|
805
|
+
return moduleName === "database-pg" ? "createdb <database_name>" : `mysql -e "CREATE DATABASE IF NOT EXISTS <database_name>;"`;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
function getModuleDocsPath(moduleName) {
|
|
809
|
+
if (isDatabaseModule(moduleName)) {
|
|
810
|
+
return "database";
|
|
811
|
+
}
|
|
812
|
+
return moduleName;
|
|
813
|
+
}
|
|
814
|
+
function escapeRegex(value) {
|
|
815
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
816
|
+
}
|
|
817
|
+
async function hasEnvVariable(projectRoot, key) {
|
|
818
|
+
const envPath = import_path6.default.join(projectRoot, ".env");
|
|
819
|
+
if (!await import_fs_extra6.default.pathExists(envPath)) {
|
|
820
|
+
return false;
|
|
821
|
+
}
|
|
822
|
+
const content = await import_fs_extra6.default.readFile(envPath, "utf-8");
|
|
823
|
+
const pattern = new RegExp(`^${escapeRegex(key)}=`, "m");
|
|
824
|
+
return pattern.test(content);
|
|
825
|
+
}
|
|
826
|
+
async function isLikelyEmptyDirectory(cwd) {
|
|
827
|
+
const entries = await import_fs_extra6.default.readdir(cwd);
|
|
828
|
+
const ignored = /* @__PURE__ */ new Set([".ds_store", "thumbs.db"]);
|
|
829
|
+
return entries.filter((entry) => !ignored.has(entry.toLowerCase())).length === 0;
|
|
830
|
+
}
|
|
640
831
|
async function injectErrorHandler(projectRoot, srcDir) {
|
|
641
832
|
const appPath = import_path6.default.join(projectRoot, srcDir, "app.ts");
|
|
642
833
|
if (!import_fs_extra6.default.existsSync(appPath)) {
|
|
643
834
|
return false;
|
|
644
835
|
}
|
|
645
836
|
let content = await import_fs_extra6.default.readFile(appPath, "utf-8");
|
|
646
|
-
if (content.includes("errorHandler")) {
|
|
647
|
-
return true;
|
|
648
|
-
}
|
|
649
837
|
const errorImport = `import { errorHandler, notFoundHandler } from "./middleware/error-handler";`;
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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) + `
|
|
658
852
|
${errorImport}` + content.slice(lastImportIndex);
|
|
853
|
+
modified = true;
|
|
854
|
+
importInserted = true;
|
|
855
|
+
}
|
|
659
856
|
}
|
|
660
|
-
|
|
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 = `
|
|
661
867
|
// Error handling (must be last)
|
|
662
|
-
|
|
663
|
-
app.use(errorHandler);
|
|
868
|
+
${setupLines.join("\n")}
|
|
664
869
|
`;
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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
|
+
}
|
|
668
876
|
}
|
|
669
|
-
|
|
670
|
-
|
|
877
|
+
if (modified) {
|
|
878
|
+
await import_fs_extra6.default.writeFile(appPath, content);
|
|
879
|
+
}
|
|
880
|
+
return importInserted && setupInserted;
|
|
671
881
|
}
|
|
672
882
|
async function injectAuthRoutes(projectRoot, srcDir) {
|
|
673
883
|
const appPath = import_path6.default.join(projectRoot, srcDir, "app.ts");
|
|
@@ -675,48 +885,83 @@ async function injectAuthRoutes(projectRoot, srcDir) {
|
|
|
675
885
|
return false;
|
|
676
886
|
}
|
|
677
887
|
let content = await import_fs_extra6.default.readFile(appPath, "utf-8");
|
|
678
|
-
if (content.includes("routes/auth.routes")) {
|
|
679
|
-
return true;
|
|
680
|
-
}
|
|
681
888
|
const authImport = `import authRoutes from "./routes/auth.routes";`;
|
|
682
889
|
const userImport = `import userRoutes from "./routes/user.routes";`;
|
|
683
|
-
const
|
|
890
|
+
const hasAuthImport = content.includes(authImport);
|
|
891
|
+
const hasUserImport = content.includes(userImport);
|
|
892
|
+
const hasAuthRoute = /app\.use\(\s*authRoutes\s*\)/.test(content);
|
|
893
|
+
const hasUserRoute = /app\.use\(\s*["']\/api\/users["']\s*,\s*userRoutes\s*\)/.test(content);
|
|
894
|
+
let modified = false;
|
|
895
|
+
let importsReady = hasAuthImport && hasUserImport;
|
|
896
|
+
if (!importsReady) {
|
|
897
|
+
const importRegex = /^import .+ from .+;?\s*$/gm;
|
|
898
|
+
let lastImportIndex = 0;
|
|
899
|
+
let match;
|
|
900
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
901
|
+
lastImportIndex = match.index + match[0].length;
|
|
902
|
+
}
|
|
903
|
+
if (lastImportIndex > 0) {
|
|
904
|
+
const missingImports = [];
|
|
905
|
+
if (!hasAuthImport) {
|
|
906
|
+
missingImports.push(authImport);
|
|
907
|
+
}
|
|
908
|
+
if (!hasUserImport) {
|
|
909
|
+
missingImports.push(userImport);
|
|
910
|
+
}
|
|
911
|
+
content = content.slice(0, lastImportIndex) + `
|
|
912
|
+
${missingImports.join("\n")}` + content.slice(lastImportIndex);
|
|
913
|
+
modified = true;
|
|
914
|
+
importsReady = true;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
let setupReady = hasAuthRoute && hasUserRoute;
|
|
918
|
+
if (!setupReady) {
|
|
919
|
+
const setupLines = [];
|
|
920
|
+
if (!hasAuthRoute) {
|
|
921
|
+
setupLines.push("app.use(authRoutes);");
|
|
922
|
+
}
|
|
923
|
+
if (!hasUserRoute) {
|
|
924
|
+
setupLines.push('app.use("/api/users", userRoutes);');
|
|
925
|
+
}
|
|
926
|
+
const routeSetup = `
|
|
684
927
|
// Auth routes
|
|
685
|
-
|
|
686
|
-
app.use("/api/users", userRoutes);
|
|
928
|
+
${setupLines.join("\n")}
|
|
687
929
|
`;
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
if (lastImportIndex > 0) {
|
|
695
|
-
content = content.slice(0, lastImportIndex) + `
|
|
696
|
-
${authImport}
|
|
697
|
-
${userImport}` + content.slice(lastImportIndex);
|
|
930
|
+
const exportMatch = content.match(/export default app;?\s*$/m);
|
|
931
|
+
if (exportMatch && exportMatch.index !== void 0) {
|
|
932
|
+
content = content.slice(0, exportMatch.index) + routeSetup + "\n" + content.slice(exportMatch.index);
|
|
933
|
+
modified = true;
|
|
934
|
+
setupReady = true;
|
|
935
|
+
}
|
|
698
936
|
}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
content = content.slice(0, exportMatch.index) + routeSetup + "\n" + content.slice(exportMatch.index);
|
|
937
|
+
if (modified) {
|
|
938
|
+
await import_fs_extra6.default.writeFile(appPath, content);
|
|
702
939
|
}
|
|
703
|
-
|
|
704
|
-
return true;
|
|
940
|
+
return importsReady && setupReady;
|
|
705
941
|
}
|
|
706
942
|
var add = async (moduleName) => {
|
|
707
943
|
const projectRoot = process.cwd();
|
|
708
944
|
const projectConfig = await readZuroConfig(projectRoot);
|
|
709
945
|
if (!projectConfig) {
|
|
946
|
+
if (await isLikelyEmptyDirectory(projectRoot)) {
|
|
947
|
+
showInitFirstMessage();
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
710
950
|
showNonZuroProjectMessage();
|
|
711
951
|
return;
|
|
712
952
|
}
|
|
713
|
-
|
|
953
|
+
const srcDir = projectConfig.srcDir || "src";
|
|
954
|
+
let resolvedModuleName = moduleName;
|
|
955
|
+
const parsedDialect = parseDatabaseDialect(moduleName);
|
|
956
|
+
if (parsedDialect) {
|
|
957
|
+
resolvedModuleName = parsedDialect;
|
|
958
|
+
}
|
|
714
959
|
let customDbUrl;
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
if (
|
|
960
|
+
let usedDefaultDbUrl = false;
|
|
961
|
+
let databaseBackupPath = null;
|
|
962
|
+
let generatedAuthSecret = false;
|
|
963
|
+
let authDatabaseDialect = null;
|
|
964
|
+
if (resolvedModuleName === "database") {
|
|
720
965
|
const variantResponse = await (0, import_prompts2.default)({
|
|
721
966
|
type: "select",
|
|
722
967
|
name: "variant",
|
|
@@ -727,22 +972,39 @@ var add = async (moduleName) => {
|
|
|
727
972
|
]
|
|
728
973
|
});
|
|
729
974
|
if (!variantResponse.variant) {
|
|
730
|
-
|
|
975
|
+
console.log(import_chalk4.default.yellow("Operation cancelled."));
|
|
976
|
+
return;
|
|
731
977
|
}
|
|
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;
|
|
978
|
+
resolvedModuleName = variantResponse.variant;
|
|
743
979
|
}
|
|
744
|
-
if ((
|
|
745
|
-
const
|
|
980
|
+
if (isDatabaseModule(resolvedModuleName)) {
|
|
981
|
+
const installedDialect = await detectInstalledDatabaseDialect(projectRoot, srcDir);
|
|
982
|
+
if (installedDialect && installedDialect !== resolvedModuleName) {
|
|
983
|
+
console.log(
|
|
984
|
+
import_chalk4.default.yellow(
|
|
985
|
+
`
|
|
986
|
+
\u26A0 Existing database setup detected: ${databaseLabel(installedDialect)}.`
|
|
987
|
+
)
|
|
988
|
+
);
|
|
989
|
+
console.log(
|
|
990
|
+
import_chalk4.default.yellow(
|
|
991
|
+
` Switching to ${databaseLabel(resolvedModuleName)} will overwrite db files and drizzle config.
|
|
992
|
+
`
|
|
993
|
+
)
|
|
994
|
+
);
|
|
995
|
+
const switchResponse = await (0, import_prompts2.default)({
|
|
996
|
+
type: "confirm",
|
|
997
|
+
name: "proceed",
|
|
998
|
+
message: "Continue and switch database dialect?",
|
|
999
|
+
initial: false
|
|
1000
|
+
});
|
|
1001
|
+
if (!switchResponse.proceed) {
|
|
1002
|
+
console.log(import_chalk4.default.yellow("Operation cancelled."));
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
databaseBackupPath = await backupDatabaseFiles(projectRoot, srcDir);
|
|
1006
|
+
}
|
|
1007
|
+
const defaultUrl = DEFAULT_DATABASE_URLS[resolvedModuleName];
|
|
746
1008
|
console.log(import_chalk4.default.dim(` Tip: Leave blank to use ${defaultUrl}
|
|
747
1009
|
`));
|
|
748
1010
|
const response = await (0, import_prompts2.default)({
|
|
@@ -751,44 +1013,69 @@ var add = async (moduleName) => {
|
|
|
751
1013
|
message: "Database URL",
|
|
752
1014
|
initial: ""
|
|
753
1015
|
});
|
|
754
|
-
|
|
1016
|
+
if (response.dbUrl === void 0) {
|
|
1017
|
+
console.log(import_chalk4.default.yellow("Operation cancelled."));
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
const enteredUrl = response.dbUrl?.trim() || "";
|
|
1021
|
+
usedDefaultDbUrl = enteredUrl.length === 0;
|
|
1022
|
+
customDbUrl = validateDatabaseUrl(enteredUrl || defaultUrl, resolvedModuleName);
|
|
755
1023
|
}
|
|
756
|
-
const
|
|
1024
|
+
const pm = resolvePackageManager(projectRoot);
|
|
1025
|
+
const spinner = (0, import_ora2.default)(`Checking registry for ${resolvedModuleName}...`).start();
|
|
1026
|
+
let currentStep = "package manager preflight";
|
|
757
1027
|
try {
|
|
1028
|
+
spinner.text = `Checking ${pm} availability...`;
|
|
1029
|
+
await ensurePackageManagerAvailable(pm);
|
|
1030
|
+
currentStep = "registry fetch";
|
|
1031
|
+
spinner.text = `Checking registry for ${resolvedModuleName}...`;
|
|
758
1032
|
const registryContext = await fetchRegistry();
|
|
759
|
-
const module2 = registryContext.manifest.modules[
|
|
1033
|
+
const module2 = registryContext.manifest.modules[resolvedModuleName];
|
|
760
1034
|
if (!module2) {
|
|
761
|
-
spinner.fail(`Module '${
|
|
1035
|
+
spinner.fail(`Module '${resolvedModuleName}' not found.`);
|
|
762
1036
|
return;
|
|
763
1037
|
}
|
|
764
|
-
spinner.succeed(`Found module: ${import_chalk4.default.cyan(
|
|
1038
|
+
spinner.succeed(`Found module: ${import_chalk4.default.cyan(resolvedModuleName)}`);
|
|
765
1039
|
const moduleDeps = module2.moduleDependencies || [];
|
|
1040
|
+
currentStep = "module dependency resolution";
|
|
766
1041
|
await resolveDependencies(moduleDeps, projectRoot);
|
|
1042
|
+
currentStep = "dependency installation";
|
|
767
1043
|
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
1044
|
await installDependencies(pm, module2.dependencies || [], projectRoot);
|
|
777
1045
|
await installDependencies(pm, module2.devDependencies || [], projectRoot, { dev: true });
|
|
778
1046
|
spinner.succeed("Dependencies installed");
|
|
1047
|
+
currentStep = "module scaffolding";
|
|
779
1048
|
spinner.start("Scaffolding files...");
|
|
1049
|
+
if (resolvedModuleName === "auth") {
|
|
1050
|
+
authDatabaseDialect = await detectInstalledDatabaseDialect(projectRoot, srcDir);
|
|
1051
|
+
}
|
|
780
1052
|
for (const file of module2.files) {
|
|
781
|
-
|
|
1053
|
+
let fetchPath = file.path;
|
|
1054
|
+
let expectedSha256 = file.sha256;
|
|
1055
|
+
let expectedSize = file.size;
|
|
1056
|
+
if (resolvedModuleName === "auth" && file.target === "db/schema/auth.ts" && authDatabaseDialect === "database-mysql") {
|
|
1057
|
+
fetchPath = "express/db/schema/auth.mysql.ts";
|
|
1058
|
+
expectedSha256 = void 0;
|
|
1059
|
+
expectedSize = void 0;
|
|
1060
|
+
}
|
|
1061
|
+
let content = await fetchFile(fetchPath, {
|
|
782
1062
|
baseUrl: registryContext.fileBaseUrl,
|
|
783
|
-
expectedSha256
|
|
784
|
-
expectedSize
|
|
1063
|
+
expectedSha256,
|
|
1064
|
+
expectedSize
|
|
785
1065
|
});
|
|
1066
|
+
if (isDatabaseModule(resolvedModuleName) && file.target === "../drizzle.config.ts") {
|
|
1067
|
+
const normalizedSrcDir = srcDir.replace(/\\/g, "/");
|
|
1068
|
+
content = content.replace(
|
|
1069
|
+
/schema:\s*["'][^"']+["']/,
|
|
1070
|
+
`schema: "./${normalizedSrcDir}/db/schema/*"`
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
786
1073
|
const targetPath = resolveSafeTargetPath2(projectRoot, srcDir, file);
|
|
787
1074
|
await import_fs_extra6.default.ensureDir(import_path6.default.dirname(targetPath));
|
|
788
1075
|
await import_fs_extra6.default.writeFile(targetPath, content);
|
|
789
1076
|
}
|
|
790
1077
|
spinner.succeed("Files generated");
|
|
791
|
-
if (
|
|
1078
|
+
if (resolvedModuleName === "auth") {
|
|
792
1079
|
spinner.start("Configuring routes in app.ts...");
|
|
793
1080
|
const injected = await injectAuthRoutes(projectRoot, srcDir);
|
|
794
1081
|
if (injected) {
|
|
@@ -797,7 +1084,7 @@ var add = async (moduleName) => {
|
|
|
797
1084
|
spinner.warn("Could not find app.ts - routes need manual setup");
|
|
798
1085
|
}
|
|
799
1086
|
}
|
|
800
|
-
if (
|
|
1087
|
+
if (resolvedModuleName === "error-handler") {
|
|
801
1088
|
spinner.start("Configuring error handler in app.ts...");
|
|
802
1089
|
const injected = await injectErrorHandler(projectRoot, srcDir);
|
|
803
1090
|
if (injected) {
|
|
@@ -806,78 +1093,63 @@ var add = async (moduleName) => {
|
|
|
806
1093
|
spinner.warn("Could not find app.ts - error handler needs manual setup");
|
|
807
1094
|
}
|
|
808
1095
|
}
|
|
809
|
-
const envConfig = ENV_CONFIGS[
|
|
1096
|
+
const envConfig = ENV_CONFIGS[resolvedModuleName];
|
|
810
1097
|
if (envConfig) {
|
|
1098
|
+
currentStep = "environment configuration";
|
|
811
1099
|
spinner.start("Updating environment configuration...");
|
|
812
1100
|
const envVars = { ...envConfig.envVars };
|
|
813
|
-
if (customDbUrl && (
|
|
1101
|
+
if (customDbUrl && isDatabaseModule(resolvedModuleName)) {
|
|
814
1102
|
envVars.DATABASE_URL = customDbUrl;
|
|
815
1103
|
}
|
|
816
|
-
|
|
1104
|
+
if (resolvedModuleName === "auth") {
|
|
1105
|
+
const hasExistingSecret = await hasEnvVariable(projectRoot, "BETTER_AUTH_SECRET");
|
|
1106
|
+
if (!hasExistingSecret) {
|
|
1107
|
+
envVars.BETTER_AUTH_SECRET = (0, import_node_crypto2.randomBytes)(32).toString("hex");
|
|
1108
|
+
generatedAuthSecret = true;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
await updateEnvFile(projectRoot, envVars, true, {
|
|
1112
|
+
overwriteExisting: isDatabaseModule(resolvedModuleName)
|
|
1113
|
+
});
|
|
817
1114
|
await updateEnvSchema(projectRoot, srcDir, envConfig.schemaFields);
|
|
818
1115
|
spinner.succeed("Environment configured");
|
|
819
1116
|
}
|
|
820
1117
|
console.log(import_chalk4.default.green(`
|
|
821
|
-
\u2714 ${
|
|
1118
|
+
\u2714 ${resolvedModuleName} added successfully!
|
|
822
1119
|
`));
|
|
823
|
-
if (
|
|
824
|
-
console.log(import_chalk4.default.
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
1120
|
+
if (databaseBackupPath) {
|
|
1121
|
+
console.log(import_chalk4.default.blue(`\u2139 Backup created at: ${databaseBackupPath}
|
|
1122
|
+
`));
|
|
1123
|
+
}
|
|
1124
|
+
const docsPath = getModuleDocsPath(resolvedModuleName);
|
|
1125
|
+
const docsUrl = `https://zuro-cli.devbybriyan.com/docs/${docsPath}`;
|
|
1126
|
+
console.log(import_chalk4.default.blue(`\u2139 Docs: ${docsUrl}`));
|
|
1127
|
+
if (isDatabaseModule(resolvedModuleName)) {
|
|
1128
|
+
if (usedDefaultDbUrl) {
|
|
1129
|
+
console.log(import_chalk4.default.yellow("\u2139 Review DATABASE_URL in .env if your local DB config differs."));
|
|
1130
|
+
}
|
|
1131
|
+
const setupHint = getDatabaseSetupHint(
|
|
1132
|
+
resolvedModuleName,
|
|
1133
|
+
customDbUrl || DEFAULT_DATABASE_URLS[resolvedModuleName]
|
|
828
1134
|
);
|
|
829
|
-
console.log(import_chalk4.default.yellow(
|
|
830
|
-
console.log(import_chalk4.default.
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
} else if (moduleName === "error-handler") {
|
|
838
|
-
console.log(import_chalk4.default.bold("\u{1F4CB} Usage:\n"));
|
|
839
|
-
console.log(import_chalk4.default.yellow("Throw errors in your controllers:"));
|
|
840
|
-
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"));
|
|
841
|
-
console.log(import_chalk4.default.white(` import { UnauthorizedError, NotFoundError } from "./lib/errors";`));
|
|
842
|
-
console.log("");
|
|
843
|
-
console.log(import_chalk4.default.white(` throw new UnauthorizedError("Invalid credentials");`));
|
|
844
|
-
console.log(import_chalk4.default.white(` throw new NotFoundError("User not found");`));
|
|
845
|
-
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"));
|
|
846
|
-
console.log(import_chalk4.default.yellow("Available error classes:"));
|
|
847
|
-
console.log(import_chalk4.default.dim(" BadRequestError (400)"));
|
|
848
|
-
console.log(import_chalk4.default.dim(" UnauthorizedError (401)"));
|
|
849
|
-
console.log(import_chalk4.default.dim(" ForbiddenError (403)"));
|
|
850
|
-
console.log(import_chalk4.default.dim(" NotFoundError (404)"));
|
|
851
|
-
console.log(import_chalk4.default.dim(" ConflictError (409)"));
|
|
852
|
-
console.log(import_chalk4.default.dim(" ValidationError (422)"));
|
|
853
|
-
console.log(import_chalk4.default.dim(" InternalServerError (500)\n"));
|
|
854
|
-
console.log(import_chalk4.default.yellow("Wrap async handlers:"));
|
|
855
|
-
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"));
|
|
856
|
-
console.log(import_chalk4.default.white(` import { asyncHandler } from "./middleware/error-handler";`));
|
|
857
|
-
console.log("");
|
|
858
|
-
console.log(import_chalk4.default.white(` router.get("/users", asyncHandler(async (req, res) => {`));
|
|
859
|
-
console.log(import_chalk4.default.white(" // errors auto-caught"));
|
|
860
|
-
console.log(import_chalk4.default.white(" }));"));
|
|
861
|
-
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 (moduleName.includes("database")) {
|
|
863
|
-
console.log(import_chalk4.default.bold("\u{1F4CB} Next Steps:\n"));
|
|
864
|
-
let stepNum = 1;
|
|
865
|
-
if (!customDbUrl) {
|
|
866
|
-
console.log(import_chalk4.default.yellow(`${stepNum}. Update DATABASE_URL in .env:`));
|
|
867
|
-
console.log(
|
|
868
|
-
import_chalk4.default.dim(" We added a placeholder. Update with your actual database credentials.\n")
|
|
869
|
-
);
|
|
870
|
-
stepNum++;
|
|
1135
|
+
console.log(import_chalk4.default.yellow(`\u2139 Ensure DB exists: ${setupHint}`));
|
|
1136
|
+
console.log(import_chalk4.default.yellow("\u2139 Run migrations: npx drizzle-kit generate && npx drizzle-kit migrate"));
|
|
1137
|
+
}
|
|
1138
|
+
if (resolvedModuleName === "auth") {
|
|
1139
|
+
if (generatedAuthSecret) {
|
|
1140
|
+
console.log(import_chalk4.default.yellow("\u2139 BETTER_AUTH_SECRET was generated automatically."));
|
|
1141
|
+
} else {
|
|
1142
|
+
console.log(import_chalk4.default.yellow("\u2139 Review BETTER_AUTH_SECRET and BETTER_AUTH_URL in .env."));
|
|
871
1143
|
}
|
|
872
|
-
console.log(import_chalk4.default.yellow(
|
|
873
|
-
console.log(import_chalk4.default.dim(" Add table files and export from index.ts\n"));
|
|
874
|
-
stepNum++;
|
|
875
|
-
console.log(import_chalk4.default.yellow(`${stepNum}. Run migrations:`));
|
|
876
|
-
console.log(import_chalk4.default.cyan(" npx drizzle-kit generate"));
|
|
877
|
-
console.log(import_chalk4.default.cyan(" npx drizzle-kit migrate\n"));
|
|
1144
|
+
console.log(import_chalk4.default.yellow("\u2139 Run migrations: npx drizzle-kit generate && npx drizzle-kit migrate"));
|
|
878
1145
|
}
|
|
879
1146
|
} catch (error) {
|
|
880
|
-
spinner.fail(`Failed
|
|
1147
|
+
spinner.fail(import_chalk4.default.red(`Failed during ${currentStep}.`));
|
|
1148
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1149
|
+
console.error(import_chalk4.default.red(errorMessage));
|
|
1150
|
+
console.log(`
|
|
1151
|
+
${import_chalk4.default.bold("Retry:")}`);
|
|
1152
|
+
console.log(import_chalk4.default.cyan(` npx zuro-cli add ${resolvedModuleName}`));
|
|
881
1153
|
}
|
|
882
1154
|
};
|
|
883
1155
|
|