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/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-W36ZIR4Y.mjs";
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 path10 from "path";
745
- import fs9 from "fs-extra";
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
- if (pgExists || mysqlExists) {
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 chalk6 from "chalk";
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-D7EVXRJX.mjs");
1039
- const authDatabaseDialect = await detectInstalledDatabaseDialect(projectRoot, srcDir);
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 (fs9.existsSync(path10.join(projectRoot, "pnpm-lock.yaml"))) {
1810
+ if (fs10.existsSync(path11.join(projectRoot, "pnpm-lock.yaml"))) {
1273
1811
  return "pnpm";
1274
1812
  }
1275
- if (fs9.existsSync(path10.join(projectRoot, "bun.lockb")) || fs9.existsSync(path10.join(projectRoot, "bun.lock"))) {
1813
+ if (fs10.existsSync(path11.join(projectRoot, "bun.lockb")) || fs10.existsSync(path11.join(projectRoot, "bun.lock"))) {
1276
1814
  return "bun";
1277
1815
  }
1278
- if (fs9.existsSync(path10.join(projectRoot, "yarn.lock"))) {
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 = path10.join(projectRoot, ".env");
1291
- if (!await fs9.pathExists(envPath)) {
1828
+ const envPath = path11.join(projectRoot, ".env");
1829
+ if (!await fs10.pathExists(envPath)) {
1292
1830
  return false;
1293
1831
  }
1294
- const content = await fs9.readFile(envPath, "utf-8");
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 fs9.readdir(cwd);
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: ${chalk6.cyan(resolvedModuleName)}`);
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 (isDatabaseModule(resolvedModuleName) && file.target === "../drizzle.config.ts") {
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 fs9.ensureDir(path10.dirname(targetPath));
1414
- await fs9.writeFile(targetPath, content);
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) => path10.posix.basename(target, ".ts")).filter((name) => name !== "index");
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
- console.log(chalk6.green(`
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(chalk6.blue(`\u2139 Docs: ${docsUrl}`));
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(chalk6.blue("\n\u2139 Installing API docs module..."));
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(chalk6.red(`Failed during ${currentStep}.`));
2203
+ spinner.fail(chalk7.red(`Failed during ${currentStep}.`));
1529
2204
  const errorMessage = error instanceof Error ? error.message : String(error);
1530
- console.error(chalk6.red(errorMessage));
2205
+ console.error(chalk7.red(errorMessage));
1531
2206
  console.log(`
1532
- ${chalk6.bold("Retry:")}`);
1533
- console.log(chalk6.cyan(` npx zuro-cli add ${resolvedModuleName}`));
2207
+ ${chalk7.bold("Retry:")}`);
2208
+ console.log(chalk7.cyan(` npx zuro-cli add ${resolvedModuleName}`));
1534
2209
  }
1535
2210
  };
1536
2211