zuro-cli 0.0.2-beta.14 → 0.0.2-beta.16
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 +2 -1
- package/dist/{chunk-W36ZIR4Y.mjs → chunk-R3MGV5UR.mjs} +130 -24
- package/dist/chunk-R3MGV5UR.mjs.map +1 -0
- package/dist/{database.handler-D7EVXRJX.mjs → database.handler-5OUD6XJZ.mjs} +6 -2
- package/dist/index.js +829 -50
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +701 -26
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-W36ZIR4Y.mjs.map +0 -1
- /package/dist/{database.handler-D7EVXRJX.mjs.map → database.handler-5OUD6XJZ.mjs.map} +0 -0
package/dist/index.mjs
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
detectInstalledDatabaseDialect,
|
|
3
4
|
ensureSchemaExport,
|
|
5
|
+
getDatabaseSelection,
|
|
4
6
|
isDatabaseModule,
|
|
7
|
+
isDrizzleDatabaseModule,
|
|
5
8
|
parseDatabaseDialect,
|
|
6
9
|
printDatabaseHints,
|
|
7
10
|
promptDatabaseConfig
|
|
8
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-R3MGV5UR.mjs";
|
|
9
12
|
import {
|
|
10
13
|
injectDocsRoutes,
|
|
11
14
|
isDocsModuleInstalled,
|
|
@@ -342,6 +345,22 @@ var ENV_CONFIGS = {
|
|
|
342
345
|
{ name: "DATABASE_URL", schema: "z.string().url()" }
|
|
343
346
|
]
|
|
344
347
|
},
|
|
348
|
+
"database-prisma-pg": {
|
|
349
|
+
envVars: {
|
|
350
|
+
DATABASE_URL: "postgresql://postgres@localhost:5432/mydb"
|
|
351
|
+
},
|
|
352
|
+
schemaFields: [
|
|
353
|
+
{ name: "DATABASE_URL", schema: "z.string().url()" }
|
|
354
|
+
]
|
|
355
|
+
},
|
|
356
|
+
"database-prisma-mysql": {
|
|
357
|
+
envVars: {
|
|
358
|
+
DATABASE_URL: "mysql://root@localhost:3306/mydb"
|
|
359
|
+
},
|
|
360
|
+
schemaFields: [
|
|
361
|
+
{ name: "DATABASE_URL", schema: "z.string().url()" }
|
|
362
|
+
]
|
|
363
|
+
},
|
|
345
364
|
auth: {
|
|
346
365
|
envVars: {
|
|
347
366
|
BETTER_AUTH_SECRET: "your-secret-key-at-least-32-characters-long",
|
|
@@ -408,6 +427,19 @@ function sanitizeConfig(input) {
|
|
|
408
427
|
if (typeof raw.srcDir === "string") {
|
|
409
428
|
config.srcDir = raw.srcDir;
|
|
410
429
|
}
|
|
430
|
+
if (raw.database && typeof raw.database === "object") {
|
|
431
|
+
const dbRaw = raw.database;
|
|
432
|
+
const database = {};
|
|
433
|
+
if (dbRaw.orm === "drizzle" || dbRaw.orm === "prisma") {
|
|
434
|
+
database.orm = dbRaw.orm;
|
|
435
|
+
}
|
|
436
|
+
if (dbRaw.dialect === "postgresql" || dbRaw.dialect === "mysql") {
|
|
437
|
+
database.dialect = dbRaw.dialect;
|
|
438
|
+
}
|
|
439
|
+
if (database.orm || database.dialect) {
|
|
440
|
+
config.database = database;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
411
443
|
return config;
|
|
412
444
|
}
|
|
413
445
|
function getConfigPath(cwd) {
|
|
@@ -741,8 +773,8 @@ ${chalk2.bold("Retry:")}`);
|
|
|
741
773
|
|
|
742
774
|
// src/commands/add.ts
|
|
743
775
|
import ora2 from "ora";
|
|
744
|
-
import
|
|
745
|
-
import
|
|
776
|
+
import path11 from "path";
|
|
777
|
+
import fs10 from "fs-extra";
|
|
746
778
|
import { randomBytes } from "crypto";
|
|
747
779
|
|
|
748
780
|
// src/utils/dependency.ts
|
|
@@ -769,9 +801,17 @@ var resolveDependencies = async (moduleDependencies, cwd) => {
|
|
|
769
801
|
const srcDir = config?.srcDir || "src";
|
|
770
802
|
for (const dep of moduleDependencies) {
|
|
771
803
|
if (dep === "database") {
|
|
804
|
+
const configuredOrm = config?.database?.orm;
|
|
805
|
+
const configuredDialect = config?.database?.dialect;
|
|
772
806
|
const pgExists = fs5.existsSync(path5.join(cwd, srcDir, BLOCK_SIGNATURES["database-pg"]));
|
|
773
807
|
const mysqlExists = fs5.existsSync(path5.join(cwd, srcDir, BLOCK_SIGNATURES["database-mysql"]));
|
|
774
|
-
|
|
808
|
+
const prismaSchemaExists = fs5.existsSync(path5.join(cwd, "prisma", "schema.prisma"));
|
|
809
|
+
const hasDbFiles = pgExists || mysqlExists || prismaSchemaExists;
|
|
810
|
+
const hasDbConfig = Boolean(configuredOrm && configuredDialect);
|
|
811
|
+
if (hasDbConfig && hasDbFiles) {
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
if (!hasDbConfig && hasDbFiles) {
|
|
775
815
|
continue;
|
|
776
816
|
}
|
|
777
817
|
console.log(chalk3.blue(`\u2139 Dependency '${dep}' is missing. Triggering install...`));
|
|
@@ -802,7 +842,7 @@ function resolveSafeTargetPath2(projectRoot, srcDir, file) {
|
|
|
802
842
|
}
|
|
803
843
|
|
|
804
844
|
// src/commands/add.ts
|
|
805
|
-
import
|
|
845
|
+
import chalk7 from "chalk";
|
|
806
846
|
|
|
807
847
|
// src/handlers/auth.handler.ts
|
|
808
848
|
import path7 from "path";
|
|
@@ -1035,8 +1075,8 @@ async function promptAuthConfig(projectRoot, srcDir, options) {
|
|
|
1035
1075
|
shouldInstallDocsForAuth = docsResponse.installDocs;
|
|
1036
1076
|
}
|
|
1037
1077
|
}
|
|
1038
|
-
const { detectInstalledDatabaseDialect } = await import("./database.handler-
|
|
1039
|
-
const authDatabaseDialect = await
|
|
1078
|
+
const { detectInstalledDatabaseDialect: detectInstalledDatabaseDialect2 } = await import("./database.handler-5OUD6XJZ.mjs");
|
|
1079
|
+
const authDatabaseDialect = await detectInstalledDatabaseDialect2(projectRoot, srcDir);
|
|
1040
1080
|
return { shouldInstallDocsForAuth, authDatabaseDialect };
|
|
1041
1081
|
}
|
|
1042
1082
|
function printAuthHints(generatedAuthSecret) {
|
|
@@ -1267,15 +1307,513 @@ app.use(rateLimiter);` + content.slice(insertAt);
|
|
|
1267
1307
|
return true;
|
|
1268
1308
|
}
|
|
1269
1309
|
|
|
1310
|
+
// src/handlers/uploads.handler.ts
|
|
1311
|
+
import path10 from "path";
|
|
1312
|
+
import fs9 from "fs-extra";
|
|
1313
|
+
import prompts4 from "prompts";
|
|
1314
|
+
import chalk6 from "chalk";
|
|
1315
|
+
var UPLOAD_PRESETS = {
|
|
1316
|
+
image: {
|
|
1317
|
+
mimeTypes: ["image/jpeg", "image/png", "image/webp", "image/gif"],
|
|
1318
|
+
maxFileSize: 5 * 1024 * 1024,
|
|
1319
|
+
maxFiles: 1
|
|
1320
|
+
},
|
|
1321
|
+
document: {
|
|
1322
|
+
mimeTypes: [
|
|
1323
|
+
"application/pdf",
|
|
1324
|
+
"text/plain",
|
|
1325
|
+
"application/msword",
|
|
1326
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
1327
|
+
],
|
|
1328
|
+
maxFileSize: 10 * 1024 * 1024,
|
|
1329
|
+
maxFiles: 3
|
|
1330
|
+
},
|
|
1331
|
+
video: {
|
|
1332
|
+
mimeTypes: ["video/mp4", "video/quicktime", "video/webm"],
|
|
1333
|
+
maxFileSize: 100 * 1024 * 1024,
|
|
1334
|
+
maxFiles: 1
|
|
1335
|
+
}
|
|
1336
|
+
};
|
|
1337
|
+
function getUploadEnvSchemaFields(provider) {
|
|
1338
|
+
const shared = [
|
|
1339
|
+
{ name: "UPLOAD_PROVIDER", schema: `z.enum(["s3", "r2", "cloudinary"])` },
|
|
1340
|
+
{ name: "UPLOAD_MODE", schema: `z.enum(["proxy", "direct", "large"])` },
|
|
1341
|
+
{ name: "UPLOAD_AUTH_MODE", schema: `z.enum(["required", "none"])` },
|
|
1342
|
+
{ name: "UPLOAD_FILE_ACCESS", schema: `z.enum(["private", "public"])` },
|
|
1343
|
+
{ name: "UPLOAD_FILE_PRESET", schema: `z.enum(["image", "document", "video"])` },
|
|
1344
|
+
{ name: "UPLOAD_KEY_PREFIX", schema: "z.string().min(1)" },
|
|
1345
|
+
{ name: "UPLOAD_ALLOWED_MIME", schema: "z.string().min(1)" },
|
|
1346
|
+
{ name: "UPLOAD_MAX_FILE_SIZE", schema: "z.coerce.number().positive()" },
|
|
1347
|
+
{ name: "UPLOAD_MAX_FILES", schema: "z.coerce.number().int().positive()" },
|
|
1348
|
+
{ name: "UPLOAD_DIRECT_URL_TTL_SECONDS", schema: "z.coerce.number().int().positive().default(900)" },
|
|
1349
|
+
{ name: "UPLOAD_ACCESS_URL_TTL_SECONDS", schema: "z.coerce.number().int().positive().default(300)" },
|
|
1350
|
+
{ name: "UPLOAD_MULTIPART_PART_SIZE", schema: "z.coerce.number().int().positive().default(5242880)" }
|
|
1351
|
+
];
|
|
1352
|
+
if (provider === "cloudinary") {
|
|
1353
|
+
return [
|
|
1354
|
+
...shared,
|
|
1355
|
+
{ name: "CLOUDINARY_CLOUD_NAME", schema: "z.string().min(1)" },
|
|
1356
|
+
{ name: "CLOUDINARY_API_KEY", schema: "z.string().min(1)" },
|
|
1357
|
+
{ name: "CLOUDINARY_API_SECRET", schema: "z.string().min(1)" },
|
|
1358
|
+
{ name: "CLOUDINARY_FOLDER", schema: 'z.string().min(1).default("uploads")' },
|
|
1359
|
+
{ name: "CLOUDINARY_UPLOAD_PRESET", schema: 'z.string().default("")' }
|
|
1360
|
+
];
|
|
1361
|
+
}
|
|
1362
|
+
return [
|
|
1363
|
+
...shared,
|
|
1364
|
+
{ name: "UPLOAD_BUCKET", schema: "z.string().min(1)" },
|
|
1365
|
+
{ name: "UPLOAD_REGION", schema: "z.string().min(1)" },
|
|
1366
|
+
{ name: "UPLOAD_ENDPOINT", schema: 'z.string().default("")' },
|
|
1367
|
+
{ name: "UPLOAD_ACCESS_KEY_ID", schema: "z.string().min(1)" },
|
|
1368
|
+
{ name: "UPLOAD_SECRET_ACCESS_KEY", schema: "z.string().min(1)" },
|
|
1369
|
+
{ name: "UPLOAD_PUBLIC_BASE_URL", schema: 'z.string().default("")' }
|
|
1370
|
+
];
|
|
1371
|
+
}
|
|
1372
|
+
async function isAuthInstalled(projectRoot, srcDir) {
|
|
1373
|
+
return fs9.pathExists(path10.join(projectRoot, srcDir, "lib", "auth.ts"));
|
|
1374
|
+
}
|
|
1375
|
+
function hasDrizzleDatabase(config) {
|
|
1376
|
+
return config?.database?.orm === "drizzle";
|
|
1377
|
+
}
|
|
1378
|
+
function getProviderEnvDefaults(provider) {
|
|
1379
|
+
if (provider === "cloudinary") {
|
|
1380
|
+
return {
|
|
1381
|
+
CLOUDINARY_CLOUD_NAME: "your-cloud-name",
|
|
1382
|
+
CLOUDINARY_API_KEY: "your-api-key",
|
|
1383
|
+
CLOUDINARY_API_SECRET: "your-api-secret",
|
|
1384
|
+
CLOUDINARY_FOLDER: "uploads",
|
|
1385
|
+
CLOUDINARY_UPLOAD_PRESET: ""
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
return {
|
|
1389
|
+
UPLOAD_BUCKET: `your-${provider}-bucket`,
|
|
1390
|
+
UPLOAD_REGION: provider === "r2" ? "auto" : "us-east-1",
|
|
1391
|
+
UPLOAD_ENDPOINT: provider === "r2" ? "https://<account-id>.r2.cloudflarestorage.com" : "",
|
|
1392
|
+
UPLOAD_ACCESS_KEY_ID: "your-access-key-id",
|
|
1393
|
+
UPLOAD_SECRET_ACCESS_KEY: "your-secret-access-key",
|
|
1394
|
+
UPLOAD_PUBLIC_BASE_URL: ""
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
function buildSharedEnvVars(provider, mode, authMode, access, preset, maxFileSize, maxFiles) {
|
|
1398
|
+
return {
|
|
1399
|
+
UPLOAD_PROVIDER: provider,
|
|
1400
|
+
UPLOAD_MODE: mode,
|
|
1401
|
+
UPLOAD_AUTH_MODE: authMode,
|
|
1402
|
+
UPLOAD_FILE_ACCESS: access,
|
|
1403
|
+
UPLOAD_FILE_PRESET: preset,
|
|
1404
|
+
UPLOAD_KEY_PREFIX: "uploads",
|
|
1405
|
+
UPLOAD_ALLOWED_MIME: UPLOAD_PRESETS[preset].mimeTypes.join(","),
|
|
1406
|
+
UPLOAD_MAX_FILE_SIZE: String(maxFileSize),
|
|
1407
|
+
UPLOAD_MAX_FILES: String(maxFiles),
|
|
1408
|
+
UPLOAD_DIRECT_URL_TTL_SECONDS: "900",
|
|
1409
|
+
UPLOAD_ACCESS_URL_TTL_SECONDS: "300",
|
|
1410
|
+
UPLOAD_MULTIPART_PART_SIZE: "5242880"
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
async function promptUploadsConfig(projectRoot, srcDir) {
|
|
1414
|
+
const projectConfig = await readZuroConfig(projectRoot);
|
|
1415
|
+
const authInstalled = await isAuthInstalled(projectRoot, srcDir);
|
|
1416
|
+
const drizzleInstalled = hasDrizzleDatabase(projectConfig);
|
|
1417
|
+
const initial = await prompts4([
|
|
1418
|
+
{
|
|
1419
|
+
type: "select",
|
|
1420
|
+
name: "provider",
|
|
1421
|
+
message: "Which upload provider?",
|
|
1422
|
+
choices: [
|
|
1423
|
+
{ title: "S3", value: "s3" },
|
|
1424
|
+
{ title: "R2", value: "r2" },
|
|
1425
|
+
{ title: "Cloudinary", value: "cloudinary" }
|
|
1426
|
+
],
|
|
1427
|
+
initial: 0
|
|
1428
|
+
},
|
|
1429
|
+
{
|
|
1430
|
+
type: "select",
|
|
1431
|
+
name: "mode",
|
|
1432
|
+
message: "Which upload mode?",
|
|
1433
|
+
choices: [
|
|
1434
|
+
{ title: "Proxy", description: "API server receives the file and uploads it", value: "proxy" },
|
|
1435
|
+
{ title: "Direct", description: "Client uploads directly with signed params/URLs", value: "direct" },
|
|
1436
|
+
{ title: "Large", description: "Multipart upload flow for large files", value: "large" }
|
|
1437
|
+
],
|
|
1438
|
+
initial: 1
|
|
1439
|
+
},
|
|
1440
|
+
{
|
|
1441
|
+
type: "select",
|
|
1442
|
+
name: "authMode",
|
|
1443
|
+
message: "Who can upload?",
|
|
1444
|
+
choices: [
|
|
1445
|
+
{ title: "Authenticated only", value: "required" },
|
|
1446
|
+
{ title: "Public", value: "none" }
|
|
1447
|
+
],
|
|
1448
|
+
initial: authInstalled ? 0 : 1
|
|
1449
|
+
},
|
|
1450
|
+
{
|
|
1451
|
+
type: "select",
|
|
1452
|
+
name: "access",
|
|
1453
|
+
message: "How should uploaded files be accessed?",
|
|
1454
|
+
choices: [
|
|
1455
|
+
{ title: "Private", value: "private" },
|
|
1456
|
+
{ title: "Public", value: "public" }
|
|
1457
|
+
],
|
|
1458
|
+
initial: 0
|
|
1459
|
+
},
|
|
1460
|
+
{
|
|
1461
|
+
type: "select",
|
|
1462
|
+
name: "preset",
|
|
1463
|
+
message: "Which file preset?",
|
|
1464
|
+
choices: [
|
|
1465
|
+
{ title: "Image", value: "image" },
|
|
1466
|
+
{ title: "Document", value: "document" },
|
|
1467
|
+
{ title: "Video", value: "video" }
|
|
1468
|
+
],
|
|
1469
|
+
initial: 0
|
|
1470
|
+
}
|
|
1471
|
+
]);
|
|
1472
|
+
if (initial.provider === void 0) {
|
|
1473
|
+
console.log(chalk6.yellow("Operation cancelled."));
|
|
1474
|
+
return null;
|
|
1475
|
+
}
|
|
1476
|
+
const provider = initial.provider;
|
|
1477
|
+
const mode = initial.mode;
|
|
1478
|
+
const authMode = initial.authMode;
|
|
1479
|
+
const access = initial.access;
|
|
1480
|
+
const preset = initial.preset;
|
|
1481
|
+
if (provider === "cloudinary" && mode === "large") {
|
|
1482
|
+
console.log(chalk6.yellow("\nCloudinary large multipart scaffolding is not available in this module yet."));
|
|
1483
|
+
console.log(chalk6.yellow("Use S3 or R2 for large uploads, or pick Proxy/Direct for Cloudinary.\n"));
|
|
1484
|
+
return null;
|
|
1485
|
+
}
|
|
1486
|
+
if (provider === "cloudinary" && preset === "document") {
|
|
1487
|
+
const warning = await prompts4({
|
|
1488
|
+
type: "confirm",
|
|
1489
|
+
name: "continue",
|
|
1490
|
+
message: "Cloudinary is media-first. Continue with Cloudinary for non-media/general files?",
|
|
1491
|
+
initial: false
|
|
1492
|
+
});
|
|
1493
|
+
if (!warning.continue) {
|
|
1494
|
+
console.log(chalk6.yellow("Operation cancelled."));
|
|
1495
|
+
return null;
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
const presetDefaults = UPLOAD_PRESETS[preset];
|
|
1499
|
+
let useDatabaseMetadata = false;
|
|
1500
|
+
let shouldInstallDatabase = false;
|
|
1501
|
+
const metadataPrompt = await prompts4({
|
|
1502
|
+
type: "confirm",
|
|
1503
|
+
name: "metadata",
|
|
1504
|
+
message: drizzleInstalled ? "Store upload metadata in your database?" : "Install database and store upload metadata?",
|
|
1505
|
+
initial: true
|
|
1506
|
+
});
|
|
1507
|
+
if (metadataPrompt.metadata === void 0) {
|
|
1508
|
+
console.log(chalk6.yellow("Operation cancelled."));
|
|
1509
|
+
return null;
|
|
1510
|
+
}
|
|
1511
|
+
if (metadataPrompt.metadata === true) {
|
|
1512
|
+
useDatabaseMetadata = true;
|
|
1513
|
+
if (!projectConfig?.database) {
|
|
1514
|
+
shouldInstallDatabase = true;
|
|
1515
|
+
} else if (!drizzleInstalled) {
|
|
1516
|
+
console.log(chalk6.yellow("\nUploads metadata currently supports Drizzle-based database setups only."));
|
|
1517
|
+
console.log(chalk6.yellow("Install or switch to a Drizzle database, or continue without metadata.\n"));
|
|
1518
|
+
return null;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
if (access === "private" && !useDatabaseMetadata) {
|
|
1522
|
+
const warning = await prompts4({
|
|
1523
|
+
type: "confirm",
|
|
1524
|
+
name: "continue",
|
|
1525
|
+
message: "Private uploads work best with metadata. Continue without database tracking?",
|
|
1526
|
+
initial: false
|
|
1527
|
+
});
|
|
1528
|
+
if (!warning.continue) {
|
|
1529
|
+
console.log(chalk6.yellow("Operation cancelled."));
|
|
1530
|
+
return null;
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
let shouldInstallAuth = false;
|
|
1534
|
+
if (authMode !== "none" && !authInstalled) {
|
|
1535
|
+
const authPrompt = await prompts4({
|
|
1536
|
+
type: "confirm",
|
|
1537
|
+
name: "installAuth",
|
|
1538
|
+
message: "Uploads auth requires the auth module. Install auth now?",
|
|
1539
|
+
initial: true
|
|
1540
|
+
});
|
|
1541
|
+
if (!authPrompt.installAuth) {
|
|
1542
|
+
console.log(chalk6.yellow("Operation cancelled."));
|
|
1543
|
+
return null;
|
|
1544
|
+
}
|
|
1545
|
+
shouldInstallAuth = true;
|
|
1546
|
+
}
|
|
1547
|
+
return {
|
|
1548
|
+
provider,
|
|
1549
|
+
mode,
|
|
1550
|
+
authMode,
|
|
1551
|
+
access,
|
|
1552
|
+
preset,
|
|
1553
|
+
useDatabaseMetadata,
|
|
1554
|
+
shouldInstallAuth,
|
|
1555
|
+
shouldInstallDatabase,
|
|
1556
|
+
envVars: {
|
|
1557
|
+
...buildSharedEnvVars(provider, mode, authMode, access, preset, presetDefaults.maxFileSize, presetDefaults.maxFiles),
|
|
1558
|
+
...getProviderEnvDefaults(provider)
|
|
1559
|
+
}
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1562
|
+
async function injectUploadsRoutes(projectRoot, srcDir) {
|
|
1563
|
+
const routeIndexPath = path10.join(projectRoot, srcDir, "routes", "index.ts");
|
|
1564
|
+
const routeImport = `import uploadsRoutes from "./uploads.routes";`;
|
|
1565
|
+
const routeMountPattern = /rootRouter\.use\(\s*["']\/uploads["']\s*,\s*uploadsRoutes\s*\)/;
|
|
1566
|
+
if (await fs9.pathExists(routeIndexPath)) {
|
|
1567
|
+
let routeContent = await fs9.readFile(routeIndexPath, "utf-8");
|
|
1568
|
+
let routeModified = false;
|
|
1569
|
+
const importResult = appendImport(routeContent, routeImport);
|
|
1570
|
+
if (!importResult.inserted) {
|
|
1571
|
+
return false;
|
|
1572
|
+
}
|
|
1573
|
+
if (importResult.source !== routeContent) {
|
|
1574
|
+
routeContent = importResult.source;
|
|
1575
|
+
routeModified = true;
|
|
1576
|
+
}
|
|
1577
|
+
if (!routeMountPattern.test(routeContent)) {
|
|
1578
|
+
const routeSetup = `
|
|
1579
|
+
// Upload routes
|
|
1580
|
+
rootRouter.use("/uploads", uploadsRoutes);
|
|
1581
|
+
`;
|
|
1582
|
+
const exportMatch = routeContent.match(/export default rootRouter;?\s*$/m);
|
|
1583
|
+
if (!exportMatch || exportMatch.index === void 0) {
|
|
1584
|
+
return false;
|
|
1585
|
+
}
|
|
1586
|
+
routeContent = routeContent.slice(0, exportMatch.index) + routeSetup + "\n" + routeContent.slice(exportMatch.index);
|
|
1587
|
+
routeModified = true;
|
|
1588
|
+
}
|
|
1589
|
+
if (routeModified) {
|
|
1590
|
+
await fs9.writeFile(routeIndexPath, routeContent);
|
|
1591
|
+
}
|
|
1592
|
+
return true;
|
|
1593
|
+
}
|
|
1594
|
+
const appPath = path10.join(projectRoot, srcDir, "app.ts");
|
|
1595
|
+
if (!await fs9.pathExists(appPath)) {
|
|
1596
|
+
return false;
|
|
1597
|
+
}
|
|
1598
|
+
let appContent = await fs9.readFile(appPath, "utf-8");
|
|
1599
|
+
let appModified = false;
|
|
1600
|
+
const appImportResult = appendImport(appContent, `import uploadsRoutes from "./routes/uploads.routes";`);
|
|
1601
|
+
if (!appImportResult.inserted) {
|
|
1602
|
+
return false;
|
|
1603
|
+
}
|
|
1604
|
+
if (appImportResult.source !== appContent) {
|
|
1605
|
+
appContent = appImportResult.source;
|
|
1606
|
+
appModified = true;
|
|
1607
|
+
}
|
|
1608
|
+
const hasMount = /app\.use\(\s*["']\/api\/uploads["']\s*,\s*uploadsRoutes\s*\)/.test(appContent);
|
|
1609
|
+
if (!hasMount) {
|
|
1610
|
+
const setup = `
|
|
1611
|
+
// Upload routes
|
|
1612
|
+
app.use("/api/uploads", uploadsRoutes);
|
|
1613
|
+
`;
|
|
1614
|
+
const exportMatch = appContent.match(/export default app;?\s*$/m);
|
|
1615
|
+
if (!exportMatch || exportMatch.index === void 0) {
|
|
1616
|
+
return false;
|
|
1617
|
+
}
|
|
1618
|
+
appContent = appContent.slice(0, exportMatch.index) + setup + "\n" + appContent.slice(exportMatch.index);
|
|
1619
|
+
appModified = true;
|
|
1620
|
+
}
|
|
1621
|
+
if (appModified) {
|
|
1622
|
+
await fs9.writeFile(appPath, appContent);
|
|
1623
|
+
}
|
|
1624
|
+
return true;
|
|
1625
|
+
}
|
|
1626
|
+
async function isUploadsModuleInstalled(projectRoot, srcDir) {
|
|
1627
|
+
return fs9.pathExists(path10.join(projectRoot, srcDir, "routes", "uploads.routes.ts"));
|
|
1628
|
+
}
|
|
1629
|
+
async function detectInstalledUploadsMode(projectRoot) {
|
|
1630
|
+
const envPath = path10.join(projectRoot, ".env");
|
|
1631
|
+
if (!await fs9.pathExists(envPath)) {
|
|
1632
|
+
return null;
|
|
1633
|
+
}
|
|
1634
|
+
const content = await fs9.readFile(envPath, "utf-8");
|
|
1635
|
+
const match = content.match(/^UPLOAD_MODE=(proxy|direct|large)$/m);
|
|
1636
|
+
if (!match) {
|
|
1637
|
+
return null;
|
|
1638
|
+
}
|
|
1639
|
+
return match[1];
|
|
1640
|
+
}
|
|
1641
|
+
async function injectUploadsDocs(projectRoot, srcDir, mode) {
|
|
1642
|
+
const openApiPath = path10.join(projectRoot, srcDir, "lib", "openapi.ts");
|
|
1643
|
+
if (!await fs9.pathExists(openApiPath)) {
|
|
1644
|
+
return false;
|
|
1645
|
+
}
|
|
1646
|
+
const marker = "// ZURO_UPLOADS_DOCS";
|
|
1647
|
+
let content = await fs9.readFile(openApiPath, "utf-8");
|
|
1648
|
+
if (content.includes(marker)) {
|
|
1649
|
+
return true;
|
|
1650
|
+
}
|
|
1651
|
+
const moduleDocsEndMarker = "// ZURO_DOCS_MODULES_END";
|
|
1652
|
+
if (!content.includes(moduleDocsEndMarker)) {
|
|
1653
|
+
return false;
|
|
1654
|
+
}
|
|
1655
|
+
const commonBlock = `
|
|
1656
|
+
const uploadAccessSchema = z.object({
|
|
1657
|
+
key: z.string().openapi({ example: "uploads/users/user_123/2026/03/06/example.png" }),
|
|
1658
|
+
resourceType: z.string().optional().openapi({ example: "image" }),
|
|
1659
|
+
providerAssetId: z.string().optional().openapi({ example: "uploads/users/user_123/example" }),
|
|
1660
|
+
});
|
|
1661
|
+
|
|
1662
|
+
`;
|
|
1663
|
+
const directBlock = mode === "direct" ? `registry.registerPath({
|
|
1664
|
+
method: "post",
|
|
1665
|
+
path: "/api/uploads/presign",
|
|
1666
|
+
tags: ["Uploads"],
|
|
1667
|
+
summary: "Create a signed direct upload request",
|
|
1668
|
+
request: {
|
|
1669
|
+
body: {
|
|
1670
|
+
content: {
|
|
1671
|
+
"application/json": {
|
|
1672
|
+
schema: z.object({
|
|
1673
|
+
originalName: z.string().min(1),
|
|
1674
|
+
mimeType: z.string().min(1),
|
|
1675
|
+
bytes: z.number().positive(),
|
|
1676
|
+
}),
|
|
1677
|
+
},
|
|
1678
|
+
},
|
|
1679
|
+
},
|
|
1680
|
+
},
|
|
1681
|
+
responses: {
|
|
1682
|
+
200: { description: "Signed upload created" },
|
|
1683
|
+
},
|
|
1684
|
+
});
|
|
1685
|
+
|
|
1686
|
+
registry.registerPath({
|
|
1687
|
+
method: "post",
|
|
1688
|
+
path: "/api/uploads/complete",
|
|
1689
|
+
tags: ["Uploads"],
|
|
1690
|
+
summary: "Finalize a direct upload and persist metadata",
|
|
1691
|
+
responses: {
|
|
1692
|
+
201: { description: "Upload finalized" },
|
|
1693
|
+
},
|
|
1694
|
+
});
|
|
1695
|
+
` : "";
|
|
1696
|
+
const proxyBlock = mode === "proxy" ? `registry.registerPath({
|
|
1697
|
+
method: "post",
|
|
1698
|
+
path: "/api/uploads",
|
|
1699
|
+
tags: ["Uploads"],
|
|
1700
|
+
summary: "Upload a file through the API server",
|
|
1701
|
+
responses: {
|
|
1702
|
+
201: { description: "File uploaded" },
|
|
1703
|
+
},
|
|
1704
|
+
});
|
|
1705
|
+
` : "";
|
|
1706
|
+
const largeBlock = mode === "large" ? `registry.registerPath({
|
|
1707
|
+
method: "post",
|
|
1708
|
+
path: "/api/uploads/multipart/init",
|
|
1709
|
+
tags: ["Uploads"],
|
|
1710
|
+
summary: "Start a multipart upload session",
|
|
1711
|
+
responses: {
|
|
1712
|
+
200: { description: "Multipart upload initialized" },
|
|
1713
|
+
},
|
|
1714
|
+
});
|
|
1715
|
+
|
|
1716
|
+
registry.registerPath({
|
|
1717
|
+
method: "post",
|
|
1718
|
+
path: "/api/uploads/multipart/complete",
|
|
1719
|
+
tags: ["Uploads"],
|
|
1720
|
+
summary: "Complete a multipart upload",
|
|
1721
|
+
responses: {
|
|
1722
|
+
201: { description: "Multipart upload completed" },
|
|
1723
|
+
},
|
|
1724
|
+
});
|
|
1725
|
+
|
|
1726
|
+
registry.registerPath({
|
|
1727
|
+
method: "post",
|
|
1728
|
+
path: "/api/uploads/multipart/abort",
|
|
1729
|
+
tags: ["Uploads"],
|
|
1730
|
+
summary: "Abort a multipart upload",
|
|
1731
|
+
responses: {
|
|
1732
|
+
204: { description: "Multipart upload aborted" },
|
|
1733
|
+
},
|
|
1734
|
+
});
|
|
1735
|
+
` : "";
|
|
1736
|
+
const sharedOps = `registry.registerPath({
|
|
1737
|
+
method: "post",
|
|
1738
|
+
path: "/api/uploads/access-url",
|
|
1739
|
+
tags: ["Uploads"],
|
|
1740
|
+
summary: "Create an access URL for an uploaded file",
|
|
1741
|
+
request: {
|
|
1742
|
+
body: {
|
|
1743
|
+
content: {
|
|
1744
|
+
"application/json": {
|
|
1745
|
+
schema: uploadAccessSchema,
|
|
1746
|
+
},
|
|
1747
|
+
},
|
|
1748
|
+
},
|
|
1749
|
+
},
|
|
1750
|
+
responses: {
|
|
1751
|
+
200: { description: "Access URL generated" },
|
|
1752
|
+
},
|
|
1753
|
+
});
|
|
1754
|
+
|
|
1755
|
+
registry.registerPath({
|
|
1756
|
+
method: "delete",
|
|
1757
|
+
path: "/api/uploads",
|
|
1758
|
+
tags: ["Uploads"],
|
|
1759
|
+
summary: "Delete an uploaded file",
|
|
1760
|
+
request: {
|
|
1761
|
+
body: {
|
|
1762
|
+
content: {
|
|
1763
|
+
"application/json": {
|
|
1764
|
+
schema: uploadAccessSchema,
|
|
1765
|
+
},
|
|
1766
|
+
},
|
|
1767
|
+
},
|
|
1768
|
+
},
|
|
1769
|
+
responses: {
|
|
1770
|
+
204: { description: "Upload deleted" },
|
|
1771
|
+
},
|
|
1772
|
+
});
|
|
1773
|
+
`;
|
|
1774
|
+
const block = `
|
|
1775
|
+
${commonBlock}${marker}
|
|
1776
|
+
${proxyBlock}${directBlock}${largeBlock}${sharedOps}`;
|
|
1777
|
+
content = content.replace(moduleDocsEndMarker, `${block}
|
|
1778
|
+
${moduleDocsEndMarker}`);
|
|
1779
|
+
const tagInsert = /(\{\s*name:\s*"Auth".+\},\s*\n\s*\],)/;
|
|
1780
|
+
if (tagInsert.test(content) && !content.includes(`{ name: "Uploads", description: "File uploads and asset access" }`)) {
|
|
1781
|
+
content = content.replace(
|
|
1782
|
+
tagInsert,
|
|
1783
|
+
`{ name: "Auth", description: "Authentication and session endpoints" },
|
|
1784
|
+
{ name: "Uploads", description: "File uploads and asset access" },
|
|
1785
|
+
],`
|
|
1786
|
+
);
|
|
1787
|
+
}
|
|
1788
|
+
await fs9.writeFile(openApiPath, content);
|
|
1789
|
+
return true;
|
|
1790
|
+
}
|
|
1791
|
+
function printUploadHints(result) {
|
|
1792
|
+
console.log(chalk6.yellow("\u2139 Upload routes are mounted at: /api/uploads"));
|
|
1793
|
+
console.log(chalk6.yellow(`\u2139 Provider: ${result.provider} \xB7 Mode: ${result.mode} \xB7 Access: ${result.access}`));
|
|
1794
|
+
console.log(chalk6.yellow("\u2139 Fill the generated upload env vars in .env before testing uploads."));
|
|
1795
|
+
if (result.mode === "proxy") {
|
|
1796
|
+
console.log(chalk6.yellow("\u2139 Reuse uploadSingle()/uploadArray() from src/lib/uploads/proxy.ts in your own form + file routes."));
|
|
1797
|
+
}
|
|
1798
|
+
if (result.provider === "r2") {
|
|
1799
|
+
console.log(chalk6.yellow("\u2139 R2 presigned URLs use the R2 S3 API endpoint, not your custom domain."));
|
|
1800
|
+
}
|
|
1801
|
+
if (result.useDatabaseMetadata) {
|
|
1802
|
+
console.log(chalk6.yellow("\u2139 Upload metadata is stored in db/schema/uploads.ts."));
|
|
1803
|
+
} else {
|
|
1804
|
+
console.log(chalk6.yellow("\u2139 No upload metadata table was added. Private file ownership checks fall back to key prefixes."));
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1270
1808
|
// src/commands/add.ts
|
|
1271
1809
|
function resolvePackageManager(projectRoot) {
|
|
1272
|
-
if (
|
|
1810
|
+
if (fs10.existsSync(path11.join(projectRoot, "pnpm-lock.yaml"))) {
|
|
1273
1811
|
return "pnpm";
|
|
1274
1812
|
}
|
|
1275
|
-
if (
|
|
1813
|
+
if (fs10.existsSync(path11.join(projectRoot, "bun.lockb")) || fs10.existsSync(path11.join(projectRoot, "bun.lock"))) {
|
|
1276
1814
|
return "bun";
|
|
1277
1815
|
}
|
|
1278
|
-
if (
|
|
1816
|
+
if (fs10.existsSync(path11.join(projectRoot, "yarn.lock"))) {
|
|
1279
1817
|
return "yarn";
|
|
1280
1818
|
}
|
|
1281
1819
|
return "npm";
|
|
@@ -1287,16 +1825,16 @@ function getModuleDocsPath(moduleName) {
|
|
|
1287
1825
|
return moduleName;
|
|
1288
1826
|
}
|
|
1289
1827
|
async function hasEnvVariable(projectRoot, key) {
|
|
1290
|
-
const envPath =
|
|
1291
|
-
if (!await
|
|
1828
|
+
const envPath = path11.join(projectRoot, ".env");
|
|
1829
|
+
if (!await fs10.pathExists(envPath)) {
|
|
1292
1830
|
return false;
|
|
1293
1831
|
}
|
|
1294
|
-
const content = await
|
|
1832
|
+
const content = await fs10.readFile(envPath, "utf-8");
|
|
1295
1833
|
const pattern = new RegExp(`^${escapeRegex(key)}=`, "m");
|
|
1296
1834
|
return pattern.test(content);
|
|
1297
1835
|
}
|
|
1298
1836
|
async function isLikelyEmptyDirectory(cwd) {
|
|
1299
|
-
const entries = await
|
|
1837
|
+
const entries = await fs10.readdir(cwd);
|
|
1300
1838
|
const ignored = /* @__PURE__ */ new Set([".ds_store", "thumbs.db"]);
|
|
1301
1839
|
return entries.filter((entry) => !ignored.has(entry.toLowerCase())).length === 0;
|
|
1302
1840
|
}
|
|
@@ -1320,16 +1858,22 @@ var add = async (moduleName, options = {}) => {
|
|
|
1320
1858
|
let customDbUrl;
|
|
1321
1859
|
let usedDefaultDbUrl = false;
|
|
1322
1860
|
let databaseBackupPath = null;
|
|
1861
|
+
let selectedDatabaseOrm = null;
|
|
1862
|
+
let selectedDatabaseDialect = null;
|
|
1323
1863
|
let generatedAuthSecret = false;
|
|
1324
1864
|
let authDatabaseDialect = null;
|
|
1325
1865
|
let customSmtpVars;
|
|
1326
1866
|
let usedDefaultSmtp = false;
|
|
1327
1867
|
let mailerProvider = "smtp";
|
|
1328
1868
|
let shouldInstallDocsForAuth = false;
|
|
1869
|
+
let uploadConfig = null;
|
|
1870
|
+
let uploadDatabaseDialect = null;
|
|
1329
1871
|
if (resolvedModuleName === "database" || isDatabaseModule(resolvedModuleName)) {
|
|
1330
1872
|
const result = await promptDatabaseConfig(resolvedModuleName, projectRoot, srcDir);
|
|
1331
1873
|
if (!result) return;
|
|
1332
1874
|
resolvedModuleName = result.resolvedModuleName;
|
|
1875
|
+
selectedDatabaseOrm = result.selectedOrm;
|
|
1876
|
+
selectedDatabaseDialect = result.selectedDialect;
|
|
1333
1877
|
customDbUrl = result.customDbUrl;
|
|
1334
1878
|
usedDefaultDbUrl = result.usedDefaultDbUrl;
|
|
1335
1879
|
databaseBackupPath = result.databaseBackupPath;
|
|
@@ -1347,6 +1891,10 @@ var add = async (moduleName, options = {}) => {
|
|
|
1347
1891
|
shouldInstallDocsForAuth = result.shouldInstallDocsForAuth;
|
|
1348
1892
|
authDatabaseDialect = result.authDatabaseDialect;
|
|
1349
1893
|
}
|
|
1894
|
+
if (resolvedModuleName === "uploads") {
|
|
1895
|
+
uploadConfig = await promptUploadsConfig(projectRoot, srcDir);
|
|
1896
|
+
if (!uploadConfig) return;
|
|
1897
|
+
}
|
|
1350
1898
|
const pm = resolvePackageManager(projectRoot);
|
|
1351
1899
|
const spinner = ora2(`Checking registry for ${resolvedModuleName}...`).start();
|
|
1352
1900
|
let currentStep = "package manager preflight";
|
|
@@ -1361,10 +1909,46 @@ var add = async (moduleName, options = {}) => {
|
|
|
1361
1909
|
spinner.fail(`Module '${resolvedModuleName}' not found.`);
|
|
1362
1910
|
return;
|
|
1363
1911
|
}
|
|
1364
|
-
spinner.succeed(`Found module: ${
|
|
1912
|
+
spinner.succeed(`Found module: ${chalk7.cyan(resolvedModuleName)}`);
|
|
1365
1913
|
const moduleDeps = module.moduleDependencies || [];
|
|
1366
1914
|
currentStep = "module dependency resolution";
|
|
1367
1915
|
await resolveDependencies(moduleDeps, projectRoot);
|
|
1916
|
+
if (resolvedModuleName === "uploads" && uploadConfig) {
|
|
1917
|
+
const errorHandlerInstalled = fs10.existsSync(path11.join(projectRoot, srcDir, "lib", "errors.ts"));
|
|
1918
|
+
if (!errorHandlerInstalled) {
|
|
1919
|
+
console.log(chalk7.blue("\n\u2139 Uploads needs the error-handler module. Installing error-handler..."));
|
|
1920
|
+
await add("error-handler");
|
|
1921
|
+
}
|
|
1922
|
+
if (uploadConfig.shouldInstallDatabase) {
|
|
1923
|
+
console.log(chalk7.blue("\n\u2139 Upload metadata needs a Drizzle database. Installing database module..."));
|
|
1924
|
+
await add("database");
|
|
1925
|
+
}
|
|
1926
|
+
if (uploadConfig.shouldInstallAuth) {
|
|
1927
|
+
console.log(chalk7.blue("\n\u2139 Upload auth needs the auth module. Installing auth module..."));
|
|
1928
|
+
await add("auth", { yes: true });
|
|
1929
|
+
}
|
|
1930
|
+
uploadDatabaseDialect = await detectInstalledDatabaseDialect(projectRoot, srcDir);
|
|
1931
|
+
if (uploadConfig.useDatabaseMetadata) {
|
|
1932
|
+
if (uploadDatabaseDialect === "database-prisma-pg" || uploadDatabaseDialect === "database-prisma-mysql") {
|
|
1933
|
+
spinner.fail("Uploads metadata currently supports Drizzle-based database setup only.");
|
|
1934
|
+
console.log(chalk7.yellow("\u2139 Install a Drizzle database, or rerun uploads without metadata."));
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1937
|
+
if (!uploadDatabaseDialect) {
|
|
1938
|
+
spinner.fail("Could not detect a database setup for uploads metadata.");
|
|
1939
|
+
console.log(chalk7.yellow("\u2139 Install the database module first, then rerun uploads."));
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
if (resolvedModuleName === "auth") {
|
|
1945
|
+
authDatabaseDialect = await detectInstalledDatabaseDialect(projectRoot, srcDir);
|
|
1946
|
+
if (authDatabaseDialect === "database-prisma-pg" || authDatabaseDialect === "database-prisma-mysql") {
|
|
1947
|
+
spinner.fail("Auth module currently supports Drizzle-based database setup only.");
|
|
1948
|
+
console.log(chalk7.yellow("\u2139 Install auth after switching database ORM to Drizzle."));
|
|
1949
|
+
return;
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1368
1952
|
currentStep = "dependency installation";
|
|
1369
1953
|
spinner.start("Installing dependencies...");
|
|
1370
1954
|
let runtimeDeps = module.dependencies || [];
|
|
@@ -1378,6 +1962,15 @@ var add = async (moduleName, options = {}) => {
|
|
|
1378
1962
|
devDeps = ["@types/nodemailer"];
|
|
1379
1963
|
}
|
|
1380
1964
|
}
|
|
1965
|
+
if (resolvedModuleName === "uploads" && uploadConfig) {
|
|
1966
|
+
runtimeDeps = ["multer"];
|
|
1967
|
+
devDeps = ["@types/multer"];
|
|
1968
|
+
if (uploadConfig.provider === "cloudinary") {
|
|
1969
|
+
runtimeDeps.push("cloudinary");
|
|
1970
|
+
} else {
|
|
1971
|
+
runtimeDeps.push("@aws-sdk/client-s3", "@aws-sdk/s3-request-presigner");
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1381
1974
|
await installDependencies(pm, runtimeDeps, projectRoot);
|
|
1382
1975
|
await installDependencies(pm, devDeps, projectRoot, { dev: true });
|
|
1383
1976
|
spinner.succeed("Dependencies installed");
|
|
@@ -1397,12 +1990,37 @@ var add = async (moduleName, options = {}) => {
|
|
|
1397
1990
|
expectedSha256 = void 0;
|
|
1398
1991
|
expectedSize = void 0;
|
|
1399
1992
|
}
|
|
1993
|
+
if (resolvedModuleName === "uploads" && uploadConfig) {
|
|
1994
|
+
if (file.target === "lib/uploads/provider.ts") {
|
|
1995
|
+
fetchPath = uploadConfig.provider === "cloudinary" ? "express/lib/uploads/provider.cloudinary.ts" : "express/lib/uploads/provider.s3.ts";
|
|
1996
|
+
expectedSha256 = void 0;
|
|
1997
|
+
expectedSize = void 0;
|
|
1998
|
+
}
|
|
1999
|
+
if (file.target === "lib/uploads/metadata.ts") {
|
|
2000
|
+
fetchPath = uploadConfig.useDatabaseMetadata ? "express/lib/uploads/metadata.db.ts" : "express/lib/uploads/metadata.noop.ts";
|
|
2001
|
+
expectedSha256 = void 0;
|
|
2002
|
+
expectedSize = void 0;
|
|
2003
|
+
}
|
|
2004
|
+
if (file.target === "middleware/upload-auth.ts") {
|
|
2005
|
+
fetchPath = `express/middleware/upload-auth.${uploadConfig.authMode}.ts`;
|
|
2006
|
+
expectedSha256 = void 0;
|
|
2007
|
+
expectedSize = void 0;
|
|
2008
|
+
}
|
|
2009
|
+
if (file.target === "db/schema/uploads.ts") {
|
|
2010
|
+
if (!uploadConfig.useDatabaseMetadata) {
|
|
2011
|
+
continue;
|
|
2012
|
+
}
|
|
2013
|
+
fetchPath = uploadDatabaseDialect === "database-mysql" ? "express/db/schema/uploads.mysql.ts" : "express/db/schema/uploads.ts";
|
|
2014
|
+
expectedSha256 = void 0;
|
|
2015
|
+
expectedSize = void 0;
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
1400
2018
|
let content = await fetchFile(fetchPath, {
|
|
1401
2019
|
baseUrl: registryContext.fileBaseUrl,
|
|
1402
2020
|
expectedSha256,
|
|
1403
2021
|
expectedSize
|
|
1404
2022
|
});
|
|
1405
|
-
if (
|
|
2023
|
+
if (isDrizzleDatabaseModule(resolvedModuleName) && file.target === "../drizzle.config.ts") {
|
|
1406
2024
|
const normalizedSrcDir = srcDir.replace(/\\/g, "/");
|
|
1407
2025
|
content = content.replace(
|
|
1408
2026
|
/schema:\s*["'][^"']+["']/,
|
|
@@ -1410,10 +2028,10 @@ var add = async (moduleName, options = {}) => {
|
|
|
1410
2028
|
);
|
|
1411
2029
|
}
|
|
1412
2030
|
const targetPath = resolveSafeTargetPath2(projectRoot, srcDir, file);
|
|
1413
|
-
await
|
|
1414
|
-
await
|
|
2031
|
+
await fs10.ensureDir(path11.dirname(targetPath));
|
|
2032
|
+
await fs10.writeFile(targetPath, content);
|
|
1415
2033
|
}
|
|
1416
|
-
const schemaExports = module.files.map((file) => file.target.replace(/\\/g, "/")).filter((target) => /^db\/schema\/[^/]+\.ts$/.test(target)).map((target) =>
|
|
2034
|
+
const schemaExports = module.files.map((file) => file.target.replace(/\\/g, "/")).filter((target) => /^db\/schema\/[^/]+\.ts$/.test(target)).map((target) => path11.posix.basename(target, ".ts")).filter((name) => name !== "index");
|
|
1417
2035
|
for (const schemaFileName of schemaExports) {
|
|
1418
2036
|
await ensureSchemaExport(projectRoot, srcDir, schemaFileName);
|
|
1419
2037
|
}
|
|
@@ -1473,6 +2091,38 @@ var add = async (moduleName, options = {}) => {
|
|
|
1473
2091
|
spinner.warn("Could not update API docs automatically");
|
|
1474
2092
|
}
|
|
1475
2093
|
}
|
|
2094
|
+
const uploadsInstalled = await isUploadsModuleInstalled(projectRoot, srcDir);
|
|
2095
|
+
if (uploadsInstalled) {
|
|
2096
|
+
const uploadMode = await detectInstalledUploadsMode(projectRoot);
|
|
2097
|
+
if (uploadMode) {
|
|
2098
|
+
spinner.start("Adding uploads endpoints to API docs...");
|
|
2099
|
+
const uploadsDocsInjected = await injectUploadsDocs(projectRoot, srcDir, uploadMode);
|
|
2100
|
+
if (uploadsDocsInjected) {
|
|
2101
|
+
spinner.succeed("Uploads endpoints added to API docs");
|
|
2102
|
+
} else {
|
|
2103
|
+
spinner.warn("Could not update API docs automatically");
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
if (resolvedModuleName === "uploads" && uploadConfig) {
|
|
2109
|
+
spinner.start("Configuring upload routes...");
|
|
2110
|
+
const injected = await injectUploadsRoutes(projectRoot, srcDir);
|
|
2111
|
+
if (injected) {
|
|
2112
|
+
spinner.succeed("Upload routes configured");
|
|
2113
|
+
} else {
|
|
2114
|
+
spinner.warn("Could not configure upload routes automatically");
|
|
2115
|
+
}
|
|
2116
|
+
const docsInstalled = await isDocsModuleInstalled(projectRoot, srcDir);
|
|
2117
|
+
if (docsInstalled) {
|
|
2118
|
+
spinner.start("Adding uploads endpoints to API docs...");
|
|
2119
|
+
const docsInjected = await injectUploadsDocs(projectRoot, srcDir, uploadConfig.mode);
|
|
2120
|
+
if (docsInjected) {
|
|
2121
|
+
spinner.succeed("Uploads endpoints added to API docs");
|
|
2122
|
+
} else {
|
|
2123
|
+
spinner.warn("Could not update API docs automatically");
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
1476
2126
|
}
|
|
1477
2127
|
let envConfigKey = resolvedModuleName;
|
|
1478
2128
|
if (resolvedModuleName === "mailer" && mailerProvider === "resend") {
|
|
@@ -1502,12 +2152,34 @@ var add = async (moduleName, options = {}) => {
|
|
|
1502
2152
|
await updateEnvSchema(projectRoot, srcDir, envConfig.schemaFields);
|
|
1503
2153
|
spinner.succeed("Environment configured");
|
|
1504
2154
|
}
|
|
1505
|
-
|
|
2155
|
+
if (resolvedModuleName === "uploads" && uploadConfig) {
|
|
2156
|
+
currentStep = "environment configuration";
|
|
2157
|
+
spinner.start("Updating environment configuration...");
|
|
2158
|
+
await updateEnvFile(projectRoot, uploadConfig.envVars, true);
|
|
2159
|
+
await updateEnvSchema(projectRoot, srcDir, getUploadEnvSchemaFields(uploadConfig.provider));
|
|
2160
|
+
spinner.succeed("Environment configured");
|
|
2161
|
+
}
|
|
2162
|
+
if (isDatabaseModule(resolvedModuleName)) {
|
|
2163
|
+
const selected = getDatabaseSelection(resolvedModuleName);
|
|
2164
|
+
const orm = selectedDatabaseOrm ?? selected.orm;
|
|
2165
|
+
const dialect = selectedDatabaseDialect ?? selected.dialect;
|
|
2166
|
+
const latestConfig = await readZuroConfig(projectRoot);
|
|
2167
|
+
if (latestConfig) {
|
|
2168
|
+
await writeZuroConfig(projectRoot, {
|
|
2169
|
+
...latestConfig,
|
|
2170
|
+
database: {
|
|
2171
|
+
orm,
|
|
2172
|
+
dialect
|
|
2173
|
+
}
|
|
2174
|
+
});
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
console.log(chalk7.green(`
|
|
1506
2178
|
\u2714 ${resolvedModuleName} added successfully!
|
|
1507
2179
|
`));
|
|
1508
2180
|
const docsPath = getModuleDocsPath(resolvedModuleName);
|
|
1509
2181
|
const docsUrl = `https://zuro-cli.devbybriyan.com/docs/${docsPath}`;
|
|
1510
|
-
console.log(
|
|
2182
|
+
console.log(chalk7.blue(`\u2139 Docs: ${docsUrl}`));
|
|
1511
2183
|
if (isDatabaseModule(resolvedModuleName)) {
|
|
1512
2184
|
printDatabaseHints(resolvedModuleName, customDbUrl, usedDefaultDbUrl, databaseBackupPath);
|
|
1513
2185
|
}
|
|
@@ -1520,17 +2192,20 @@ var add = async (moduleName, options = {}) => {
|
|
|
1520
2192
|
if (resolvedModuleName === "docs") {
|
|
1521
2193
|
printDocsHints();
|
|
1522
2194
|
}
|
|
2195
|
+
if (resolvedModuleName === "uploads" && uploadConfig) {
|
|
2196
|
+
printUploadHints(uploadConfig);
|
|
2197
|
+
}
|
|
1523
2198
|
if (resolvedModuleName === "auth" && shouldInstallDocsForAuth) {
|
|
1524
|
-
console.log(
|
|
2199
|
+
console.log(chalk7.blue("\n\u2139 Installing API docs module..."));
|
|
1525
2200
|
await add("docs", { yes: true });
|
|
1526
2201
|
}
|
|
1527
2202
|
} catch (error) {
|
|
1528
|
-
spinner.fail(
|
|
2203
|
+
spinner.fail(chalk7.red(`Failed during ${currentStep}.`));
|
|
1529
2204
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1530
|
-
console.error(
|
|
2205
|
+
console.error(chalk7.red(errorMessage));
|
|
1531
2206
|
console.log(`
|
|
1532
|
-
${
|
|
1533
|
-
console.log(
|
|
2207
|
+
${chalk7.bold("Retry:")}`);
|
|
2208
|
+
console.log(chalk7.cyan(` npx zuro-cli add ${resolvedModuleName}`));
|
|
1534
2209
|
}
|
|
1535
2210
|
};
|
|
1536
2211
|
|