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.js CHANGED
@@ -67,8 +67,10 @@ __export(database_handler_exports, {
67
67
  databaseLabel: () => databaseLabel,
68
68
  detectInstalledDatabaseDialect: () => detectInstalledDatabaseDialect,
69
69
  ensureSchemaExport: () => ensureSchemaExport,
70
+ getDatabaseSelection: () => getDatabaseSelection,
70
71
  getDatabaseSetupHint: () => getDatabaseSetupHint,
71
72
  isDatabaseModule: () => isDatabaseModule,
73
+ isDrizzleDatabaseModule: () => isDrizzleDatabaseModule,
72
74
  parseDatabaseDialect: () => parseDatabaseDialect,
73
75
  printDatabaseHints: () => printDatabaseHints,
74
76
  promptDatabaseConfig: () => promptDatabaseConfig,
@@ -79,18 +81,57 @@ function parseDatabaseDialect(value) {
79
81
  if (!normalized) {
80
82
  return null;
81
83
  }
82
- if (normalized === "pg" || normalized === "postgres" || normalized === "postgresql" || normalized === "database-pg") {
84
+ if (normalized === "pg" || normalized === "postgres" || normalized === "postgresql" || normalized === "database-pg" || normalized === "drizzle-pg" || normalized === "drizzle-postgres" || normalized === "database-drizzle-pg") {
83
85
  return "database-pg";
84
86
  }
85
- if (normalized === "mysql" || normalized === "database-mysql") {
87
+ if (normalized === "mysql" || normalized === "database-mysql" || normalized === "drizzle-mysql" || normalized === "database-drizzle-mysql") {
86
88
  return "database-mysql";
87
89
  }
90
+ if (normalized === "database-prisma-pg" || normalized === "prisma-pg" || normalized === "prisma-postgres" || normalized === "prisma-postgresql" || normalized === "database-prisma") {
91
+ return "database-prisma-pg";
92
+ }
93
+ if (normalized === "database-prisma-mysql" || normalized === "prisma-mysql") {
94
+ return "database-prisma-mysql";
95
+ }
88
96
  return null;
89
97
  }
90
98
  function isDatabaseModule(moduleName) {
99
+ return moduleName === "database-pg" || moduleName === "database-mysql" || moduleName === "database-prisma-pg" || moduleName === "database-prisma-mysql";
100
+ }
101
+ function isDrizzleDatabaseModule(moduleName) {
91
102
  return moduleName === "database-pg" || moduleName === "database-mysql";
92
103
  }
104
+ function getDatabaseSelection(moduleName) {
105
+ if (moduleName === "database-pg") {
106
+ return { orm: "drizzle", dialect: "postgresql" };
107
+ }
108
+ if (moduleName === "database-mysql") {
109
+ return { orm: "drizzle", dialect: "mysql" };
110
+ }
111
+ if (moduleName === "database-prisma-pg") {
112
+ return { orm: "prisma", dialect: "postgresql" };
113
+ }
114
+ return { orm: "prisma", dialect: "mysql" };
115
+ }
116
+ function getDatabaseModule(orm, dialect) {
117
+ return DATABASE_MODULE_MAP[orm][dialect];
118
+ }
119
+ function parsePrismaProvider(schemaContent) {
120
+ const match = schemaContent.match(/provider\s*=\s*"([^"]+)"/);
121
+ if (!match) {
122
+ return null;
123
+ }
124
+ const provider = match[1]?.trim().toLowerCase();
125
+ if (provider === "mysql") {
126
+ return "mysql";
127
+ }
128
+ if (provider === "postgresql" || provider === "postgres") {
129
+ return "postgresql";
130
+ }
131
+ return null;
132
+ }
93
133
  function validateDatabaseUrl(rawUrl, moduleName) {
134
+ const { dialect } = getDatabaseSelection(moduleName);
94
135
  const dbUrl = rawUrl.trim();
95
136
  if (!dbUrl) {
96
137
  throw new Error("Database URL cannot be empty.");
@@ -102,25 +143,43 @@ function validateDatabaseUrl(rawUrl, moduleName) {
102
143
  throw new Error(`Invalid database URL: '${dbUrl}'.`);
103
144
  }
104
145
  const protocol = parsed.protocol.toLowerCase();
105
- if (moduleName === "database-pg" && protocol !== "postgresql:" && protocol !== "postgres:") {
146
+ if (dialect === "postgresql" && protocol !== "postgresql:" && protocol !== "postgres:") {
106
147
  throw new Error("PostgreSQL URL must start with postgres:// or postgresql://");
107
148
  }
108
- if (moduleName === "database-mysql" && protocol !== "mysql:") {
149
+ if (dialect === "mysql" && protocol !== "mysql:") {
109
150
  throw new Error("MySQL URL must start with mysql://");
110
151
  }
111
152
  return dbUrl;
112
153
  }
113
154
  async function detectInstalledDatabaseDialect(projectRoot, srcDir) {
114
155
  const dbIndexPath = import_path7.default.join(projectRoot, srcDir, "db", "index.ts");
115
- if (!import_fs_extra6.default.existsSync(dbIndexPath)) {
116
- return null;
117
- }
118
- const content = await import_fs_extra6.default.readFile(dbIndexPath, "utf-8");
119
- if (content.includes("drizzle-orm/node-postgres") || content.includes(`from "pg"`)) {
120
- return "database-pg";
156
+ const prismaSchemaPath = import_path7.default.join(projectRoot, "prisma", "schema.prisma");
157
+ if (import_fs_extra6.default.existsSync(dbIndexPath)) {
158
+ const content = await import_fs_extra6.default.readFile(dbIndexPath, "utf-8");
159
+ if (content.includes("drizzle-orm/node-postgres") || content.includes(`from "pg"`)) {
160
+ return "database-pg";
161
+ }
162
+ if (content.includes("drizzle-orm/mysql2") || content.includes(`from "mysql2`)) {
163
+ return "database-mysql";
164
+ }
165
+ if (content.includes(`from "@prisma/client"`)) {
166
+ if (import_fs_extra6.default.existsSync(prismaSchemaPath)) {
167
+ const schemaContent = await import_fs_extra6.default.readFile(prismaSchemaPath, "utf-8");
168
+ const dialect = parsePrismaProvider(schemaContent);
169
+ if (dialect === "mysql") {
170
+ return "database-prisma-mysql";
171
+ }
172
+ }
173
+ return "database-prisma-pg";
174
+ }
121
175
  }
122
- if (content.includes("drizzle-orm/mysql2") || content.includes(`from "mysql2`)) {
123
- return "database-mysql";
176
+ if (import_fs_extra6.default.existsSync(prismaSchemaPath)) {
177
+ const schemaContent = await import_fs_extra6.default.readFile(prismaSchemaPath, "utf-8");
178
+ const dialect = parsePrismaProvider(schemaContent);
179
+ if (dialect === "mysql") {
180
+ return "database-prisma-mysql";
181
+ }
182
+ return "database-prisma-pg";
124
183
  }
125
184
  return null;
126
185
  }
@@ -129,7 +188,8 @@ async function backupDatabaseFiles(projectRoot, srcDir) {
129
188
  const backupRoot = import_path7.default.join(projectRoot, ".zuro", "backups", `database-${timestamp}`);
130
189
  const candidates = [
131
190
  import_path7.default.join(projectRoot, srcDir, "db", "index.ts"),
132
- import_path7.default.join(projectRoot, "drizzle.config.ts")
191
+ import_path7.default.join(projectRoot, "drizzle.config.ts"),
192
+ import_path7.default.join(projectRoot, "prisma", "schema.prisma")
133
193
  ];
134
194
  let copied = false;
135
195
  for (const filePath of candidates) {
@@ -145,7 +205,10 @@ async function backupDatabaseFiles(projectRoot, srcDir) {
145
205
  return copied ? backupRoot : null;
146
206
  }
147
207
  function databaseLabel(moduleName) {
148
- return moduleName === "database-pg" ? "PostgreSQL" : "MySQL";
208
+ const selection = getDatabaseSelection(moduleName);
209
+ const ormLabel = selection.orm === "drizzle" ? "Drizzle" : "Prisma";
210
+ const dialectLabel = selection.dialect === "postgresql" ? "PostgreSQL" : "MySQL";
211
+ return `${ormLabel} (${dialectLabel})`;
149
212
  }
150
213
  function getDatabaseSetupHint(moduleName, dbUrl) {
151
214
  try {
@@ -185,23 +248,42 @@ async function ensureSchemaExport(projectRoot, srcDir, schemaFileName) {
185
248
  async function promptDatabaseConfig(initialModuleName, projectRoot, srcDir) {
186
249
  let resolvedModuleName;
187
250
  if (initialModuleName === "database") {
251
+ const ormResponse = await (0, import_prompts2.default)({
252
+ type: "select",
253
+ name: "orm",
254
+ message: "Which ORM?",
255
+ choices: [
256
+ { title: "Drizzle", value: "drizzle" },
257
+ { title: "Prisma", value: "prisma" }
258
+ ],
259
+ initial: 0
260
+ });
261
+ if (!ormResponse.orm) {
262
+ console.log(import_chalk4.default.yellow("Operation cancelled."));
263
+ return null;
264
+ }
188
265
  const variantResponse = await (0, import_prompts2.default)({
189
266
  type: "select",
190
- name: "variant",
267
+ name: "dialect",
191
268
  message: "Which database dialect?",
192
269
  choices: [
193
- { title: "PostgreSQL", value: "database-pg" },
194
- { title: "MySQL", value: "database-mysql" }
270
+ { title: "PostgreSQL", value: "postgresql" },
271
+ { title: "MySQL", value: "mysql" }
195
272
  ]
196
273
  });
197
- if (!variantResponse.variant) {
274
+ if (!variantResponse.dialect) {
198
275
  console.log(import_chalk4.default.yellow("Operation cancelled."));
199
276
  return null;
200
277
  }
201
- resolvedModuleName = variantResponse.variant;
278
+ resolvedModuleName = getDatabaseModule(ormResponse.orm, variantResponse.dialect);
202
279
  } else {
203
- resolvedModuleName = initialModuleName;
280
+ const parsed = parseDatabaseDialect(initialModuleName);
281
+ if (!parsed) {
282
+ throw new Error(`Unsupported database module '${initialModuleName}'.`);
283
+ }
284
+ resolvedModuleName = parsed;
204
285
  }
286
+ const { orm: selectedOrm, dialect: selectedDialect } = getDatabaseSelection(resolvedModuleName);
205
287
  let databaseBackupPath = null;
206
288
  const installedDialect = await detectInstalledDatabaseDialect(projectRoot, srcDir);
207
289
  if (installedDialect && installedDialect !== resolvedModuleName) {
@@ -245,7 +327,14 @@ async function promptDatabaseConfig(initialModuleName, projectRoot, srcDir) {
245
327
  const enteredUrl = response.dbUrl?.trim() || "";
246
328
  const usedDefaultDbUrl = enteredUrl.length === 0;
247
329
  const customDbUrl = validateDatabaseUrl(enteredUrl || defaultUrl, resolvedModuleName);
248
- return { resolvedModuleName, customDbUrl, usedDefaultDbUrl, databaseBackupPath };
330
+ return {
331
+ resolvedModuleName,
332
+ selectedOrm,
333
+ selectedDialect,
334
+ customDbUrl,
335
+ usedDefaultDbUrl,
336
+ databaseBackupPath
337
+ };
249
338
  }
250
339
  function printDatabaseHints(moduleName, customDbUrl, usedDefaultDbUrl, databaseBackupPath) {
251
340
  if (databaseBackupPath) {
@@ -260,9 +349,14 @@ function printDatabaseHints(moduleName, customDbUrl, usedDefaultDbUrl, databaseB
260
349
  customDbUrl || DEFAULT_DATABASE_URLS[moduleName]
261
350
  );
262
351
  console.log(import_chalk4.default.yellow(`\u2139 Ensure DB exists: ${setupHint}`));
263
- console.log(import_chalk4.default.yellow("\u2139 Run migrations: npx drizzle-kit generate && npx drizzle-kit migrate"));
352
+ if (isDrizzleDatabaseModule(moduleName)) {
353
+ console.log(import_chalk4.default.yellow("\u2139 Run migrations: npx drizzle-kit generate && npx drizzle-kit migrate"));
354
+ return;
355
+ }
356
+ console.log(import_chalk4.default.yellow("\u2139 Run migrations: npx prisma migrate dev --name init"));
357
+ console.log(import_chalk4.default.yellow("\u2139 Generate client: npx prisma generate"));
264
358
  }
265
- var import_path7, import_fs_extra6, import_prompts2, import_chalk4, DEFAULT_DATABASE_URLS;
359
+ var import_path7, import_fs_extra6, import_prompts2, import_chalk4, DATABASE_MODULE_MAP, DEFAULT_DATABASE_URLS;
266
360
  var init_database_handler = __esm({
267
361
  "src/handlers/database.handler.ts"() {
268
362
  "use strict";
@@ -271,9 +365,21 @@ var init_database_handler = __esm({
271
365
  import_prompts2 = __toESM(require("prompts"));
272
366
  import_chalk4 = __toESM(require("chalk"));
273
367
  init_code_inject();
368
+ DATABASE_MODULE_MAP = {
369
+ drizzle: {
370
+ postgresql: "database-pg",
371
+ mysql: "database-mysql"
372
+ },
373
+ prisma: {
374
+ postgresql: "database-prisma-pg",
375
+ mysql: "database-prisma-mysql"
376
+ }
377
+ };
274
378
  DEFAULT_DATABASE_URLS = {
275
379
  "database-pg": "postgresql://postgres@localhost:5432/mydb",
276
- "database-mysql": "mysql://root@localhost:3306/mydb"
380
+ "database-mysql": "mysql://root@localhost:3306/mydb",
381
+ "database-prisma-pg": "postgresql://postgres@localhost:5432/mydb",
382
+ "database-prisma-mysql": "mysql://root@localhost:3306/mydb"
277
383
  };
278
384
  }
279
385
  });
@@ -353,9 +459,9 @@ app.use("/api/docs", docsRoutes);
353
459
  return true;
354
460
  }
355
461
  function printDocsHints() {
356
- const chalk8 = require("chalk");
357
- console.log(chalk8.yellow("\u2139 API docs available at: /api/docs"));
358
- console.log(chalk8.yellow("\u2139 OpenAPI spec available at: /api/docs/openapi.json"));
462
+ const chalk9 = require("chalk");
463
+ console.log(chalk9.yellow("\u2139 API docs available at: /api/docs"));
464
+ console.log(chalk9.yellow("\u2139 OpenAPI spec available at: /api/docs/openapi.json"));
359
465
  }
360
466
  var import_path8, import_fs_extra7;
361
467
  var init_docs_handler = __esm({
@@ -693,6 +799,22 @@ var ENV_CONFIGS = {
693
799
  { name: "DATABASE_URL", schema: "z.string().url()" }
694
800
  ]
695
801
  },
802
+ "database-prisma-pg": {
803
+ envVars: {
804
+ DATABASE_URL: "postgresql://postgres@localhost:5432/mydb"
805
+ },
806
+ schemaFields: [
807
+ { name: "DATABASE_URL", schema: "z.string().url()" }
808
+ ]
809
+ },
810
+ "database-prisma-mysql": {
811
+ envVars: {
812
+ DATABASE_URL: "mysql://root@localhost:3306/mydb"
813
+ },
814
+ schemaFields: [
815
+ { name: "DATABASE_URL", schema: "z.string().url()" }
816
+ ]
817
+ },
696
818
  auth: {
697
819
  envVars: {
698
820
  BETTER_AUTH_SECRET: "your-secret-key-at-least-32-characters-long",
@@ -759,6 +881,19 @@ function sanitizeConfig(input) {
759
881
  if (typeof raw.srcDir === "string") {
760
882
  config.srcDir = raw.srcDir;
761
883
  }
884
+ if (raw.database && typeof raw.database === "object") {
885
+ const dbRaw = raw.database;
886
+ const database = {};
887
+ if (dbRaw.orm === "drizzle" || dbRaw.orm === "prisma") {
888
+ database.orm = dbRaw.orm;
889
+ }
890
+ if (dbRaw.dialect === "postgresql" || dbRaw.dialect === "mysql") {
891
+ database.dialect = dbRaw.dialect;
892
+ }
893
+ if (database.orm || database.dialect) {
894
+ config.database = database;
895
+ }
896
+ }
762
897
  return config;
763
898
  }
764
899
  function getConfigPath(cwd) {
@@ -1092,8 +1227,8 @@ ${import_chalk2.default.bold("Retry:")}`);
1092
1227
 
1093
1228
  // src/commands/add.ts
1094
1229
  var import_ora2 = __toESM(require("ora"));
1095
- var import_path12 = __toESM(require("path"));
1096
- var import_fs_extra11 = __toESM(require("fs-extra"));
1230
+ var import_path13 = __toESM(require("path"));
1231
+ var import_fs_extra12 = __toESM(require("fs-extra"));
1097
1232
  var import_node_crypto2 = require("crypto");
1098
1233
 
1099
1234
  // src/utils/dependency.ts
@@ -1120,9 +1255,17 @@ var resolveDependencies = async (moduleDependencies, cwd) => {
1120
1255
  const srcDir = config?.srcDir || "src";
1121
1256
  for (const dep of moduleDependencies) {
1122
1257
  if (dep === "database") {
1258
+ const configuredOrm = config?.database?.orm;
1259
+ const configuredDialect = config?.database?.dialect;
1123
1260
  const pgExists = import_fs_extra5.default.existsSync(import_path5.default.join(cwd, srcDir, BLOCK_SIGNATURES["database-pg"]));
1124
1261
  const mysqlExists = import_fs_extra5.default.existsSync(import_path5.default.join(cwd, srcDir, BLOCK_SIGNATURES["database-mysql"]));
1125
- if (pgExists || mysqlExists) {
1262
+ const prismaSchemaExists = import_fs_extra5.default.existsSync(import_path5.default.join(cwd, "prisma", "schema.prisma"));
1263
+ const hasDbFiles = pgExists || mysqlExists || prismaSchemaExists;
1264
+ const hasDbConfig = Boolean(configuredOrm && configuredDialect);
1265
+ if (hasDbConfig && hasDbFiles) {
1266
+ continue;
1267
+ }
1268
+ if (!hasDbConfig && hasDbFiles) {
1126
1269
  continue;
1127
1270
  }
1128
1271
  console.log(import_chalk3.default.blue(`\u2139 Dependency '${dep}' is missing. Triggering install...`));
@@ -1154,7 +1297,7 @@ function resolveSafeTargetPath2(projectRoot, srcDir, file) {
1154
1297
 
1155
1298
  // src/commands/add.ts
1156
1299
  init_code_inject();
1157
- var import_chalk7 = __toESM(require("chalk"));
1300
+ var import_chalk8 = __toESM(require("chalk"));
1158
1301
  init_database_handler();
1159
1302
 
1160
1303
  // src/handlers/auth.handler.ts
@@ -1624,15 +1767,514 @@ app.use(rateLimiter);` + content.slice(insertAt);
1624
1767
  return true;
1625
1768
  }
1626
1769
 
1770
+ // src/handlers/uploads.handler.ts
1771
+ var import_path12 = __toESM(require("path"));
1772
+ var import_fs_extra11 = __toESM(require("fs-extra"));
1773
+ var import_prompts5 = __toESM(require("prompts"));
1774
+ var import_chalk7 = __toESM(require("chalk"));
1775
+ init_code_inject();
1776
+ var UPLOAD_PRESETS = {
1777
+ image: {
1778
+ mimeTypes: ["image/jpeg", "image/png", "image/webp", "image/gif"],
1779
+ maxFileSize: 5 * 1024 * 1024,
1780
+ maxFiles: 1
1781
+ },
1782
+ document: {
1783
+ mimeTypes: [
1784
+ "application/pdf",
1785
+ "text/plain",
1786
+ "application/msword",
1787
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
1788
+ ],
1789
+ maxFileSize: 10 * 1024 * 1024,
1790
+ maxFiles: 3
1791
+ },
1792
+ video: {
1793
+ mimeTypes: ["video/mp4", "video/quicktime", "video/webm"],
1794
+ maxFileSize: 100 * 1024 * 1024,
1795
+ maxFiles: 1
1796
+ }
1797
+ };
1798
+ function getUploadEnvSchemaFields(provider) {
1799
+ const shared = [
1800
+ { name: "UPLOAD_PROVIDER", schema: `z.enum(["s3", "r2", "cloudinary"])` },
1801
+ { name: "UPLOAD_MODE", schema: `z.enum(["proxy", "direct", "large"])` },
1802
+ { name: "UPLOAD_AUTH_MODE", schema: `z.enum(["required", "none"])` },
1803
+ { name: "UPLOAD_FILE_ACCESS", schema: `z.enum(["private", "public"])` },
1804
+ { name: "UPLOAD_FILE_PRESET", schema: `z.enum(["image", "document", "video"])` },
1805
+ { name: "UPLOAD_KEY_PREFIX", schema: "z.string().min(1)" },
1806
+ { name: "UPLOAD_ALLOWED_MIME", schema: "z.string().min(1)" },
1807
+ { name: "UPLOAD_MAX_FILE_SIZE", schema: "z.coerce.number().positive()" },
1808
+ { name: "UPLOAD_MAX_FILES", schema: "z.coerce.number().int().positive()" },
1809
+ { name: "UPLOAD_DIRECT_URL_TTL_SECONDS", schema: "z.coerce.number().int().positive().default(900)" },
1810
+ { name: "UPLOAD_ACCESS_URL_TTL_SECONDS", schema: "z.coerce.number().int().positive().default(300)" },
1811
+ { name: "UPLOAD_MULTIPART_PART_SIZE", schema: "z.coerce.number().int().positive().default(5242880)" }
1812
+ ];
1813
+ if (provider === "cloudinary") {
1814
+ return [
1815
+ ...shared,
1816
+ { name: "CLOUDINARY_CLOUD_NAME", schema: "z.string().min(1)" },
1817
+ { name: "CLOUDINARY_API_KEY", schema: "z.string().min(1)" },
1818
+ { name: "CLOUDINARY_API_SECRET", schema: "z.string().min(1)" },
1819
+ { name: "CLOUDINARY_FOLDER", schema: 'z.string().min(1).default("uploads")' },
1820
+ { name: "CLOUDINARY_UPLOAD_PRESET", schema: 'z.string().default("")' }
1821
+ ];
1822
+ }
1823
+ return [
1824
+ ...shared,
1825
+ { name: "UPLOAD_BUCKET", schema: "z.string().min(1)" },
1826
+ { name: "UPLOAD_REGION", schema: "z.string().min(1)" },
1827
+ { name: "UPLOAD_ENDPOINT", schema: 'z.string().default("")' },
1828
+ { name: "UPLOAD_ACCESS_KEY_ID", schema: "z.string().min(1)" },
1829
+ { name: "UPLOAD_SECRET_ACCESS_KEY", schema: "z.string().min(1)" },
1830
+ { name: "UPLOAD_PUBLIC_BASE_URL", schema: 'z.string().default("")' }
1831
+ ];
1832
+ }
1833
+ async function isAuthInstalled(projectRoot, srcDir) {
1834
+ return import_fs_extra11.default.pathExists(import_path12.default.join(projectRoot, srcDir, "lib", "auth.ts"));
1835
+ }
1836
+ function hasDrizzleDatabase(config) {
1837
+ return config?.database?.orm === "drizzle";
1838
+ }
1839
+ function getProviderEnvDefaults(provider) {
1840
+ if (provider === "cloudinary") {
1841
+ return {
1842
+ CLOUDINARY_CLOUD_NAME: "your-cloud-name",
1843
+ CLOUDINARY_API_KEY: "your-api-key",
1844
+ CLOUDINARY_API_SECRET: "your-api-secret",
1845
+ CLOUDINARY_FOLDER: "uploads",
1846
+ CLOUDINARY_UPLOAD_PRESET: ""
1847
+ };
1848
+ }
1849
+ return {
1850
+ UPLOAD_BUCKET: `your-${provider}-bucket`,
1851
+ UPLOAD_REGION: provider === "r2" ? "auto" : "us-east-1",
1852
+ UPLOAD_ENDPOINT: provider === "r2" ? "https://<account-id>.r2.cloudflarestorage.com" : "",
1853
+ UPLOAD_ACCESS_KEY_ID: "your-access-key-id",
1854
+ UPLOAD_SECRET_ACCESS_KEY: "your-secret-access-key",
1855
+ UPLOAD_PUBLIC_BASE_URL: ""
1856
+ };
1857
+ }
1858
+ function buildSharedEnvVars(provider, mode, authMode, access, preset, maxFileSize, maxFiles) {
1859
+ return {
1860
+ UPLOAD_PROVIDER: provider,
1861
+ UPLOAD_MODE: mode,
1862
+ UPLOAD_AUTH_MODE: authMode,
1863
+ UPLOAD_FILE_ACCESS: access,
1864
+ UPLOAD_FILE_PRESET: preset,
1865
+ UPLOAD_KEY_PREFIX: "uploads",
1866
+ UPLOAD_ALLOWED_MIME: UPLOAD_PRESETS[preset].mimeTypes.join(","),
1867
+ UPLOAD_MAX_FILE_SIZE: String(maxFileSize),
1868
+ UPLOAD_MAX_FILES: String(maxFiles),
1869
+ UPLOAD_DIRECT_URL_TTL_SECONDS: "900",
1870
+ UPLOAD_ACCESS_URL_TTL_SECONDS: "300",
1871
+ UPLOAD_MULTIPART_PART_SIZE: "5242880"
1872
+ };
1873
+ }
1874
+ async function promptUploadsConfig(projectRoot, srcDir) {
1875
+ const projectConfig = await readZuroConfig(projectRoot);
1876
+ const authInstalled = await isAuthInstalled(projectRoot, srcDir);
1877
+ const drizzleInstalled = hasDrizzleDatabase(projectConfig);
1878
+ const initial = await (0, import_prompts5.default)([
1879
+ {
1880
+ type: "select",
1881
+ name: "provider",
1882
+ message: "Which upload provider?",
1883
+ choices: [
1884
+ { title: "S3", value: "s3" },
1885
+ { title: "R2", value: "r2" },
1886
+ { title: "Cloudinary", value: "cloudinary" }
1887
+ ],
1888
+ initial: 0
1889
+ },
1890
+ {
1891
+ type: "select",
1892
+ name: "mode",
1893
+ message: "Which upload mode?",
1894
+ choices: [
1895
+ { title: "Proxy", description: "API server receives the file and uploads it", value: "proxy" },
1896
+ { title: "Direct", description: "Client uploads directly with signed params/URLs", value: "direct" },
1897
+ { title: "Large", description: "Multipart upload flow for large files", value: "large" }
1898
+ ],
1899
+ initial: 1
1900
+ },
1901
+ {
1902
+ type: "select",
1903
+ name: "authMode",
1904
+ message: "Who can upload?",
1905
+ choices: [
1906
+ { title: "Authenticated only", value: "required" },
1907
+ { title: "Public", value: "none" }
1908
+ ],
1909
+ initial: authInstalled ? 0 : 1
1910
+ },
1911
+ {
1912
+ type: "select",
1913
+ name: "access",
1914
+ message: "How should uploaded files be accessed?",
1915
+ choices: [
1916
+ { title: "Private", value: "private" },
1917
+ { title: "Public", value: "public" }
1918
+ ],
1919
+ initial: 0
1920
+ },
1921
+ {
1922
+ type: "select",
1923
+ name: "preset",
1924
+ message: "Which file preset?",
1925
+ choices: [
1926
+ { title: "Image", value: "image" },
1927
+ { title: "Document", value: "document" },
1928
+ { title: "Video", value: "video" }
1929
+ ],
1930
+ initial: 0
1931
+ }
1932
+ ]);
1933
+ if (initial.provider === void 0) {
1934
+ console.log(import_chalk7.default.yellow("Operation cancelled."));
1935
+ return null;
1936
+ }
1937
+ const provider = initial.provider;
1938
+ const mode = initial.mode;
1939
+ const authMode = initial.authMode;
1940
+ const access = initial.access;
1941
+ const preset = initial.preset;
1942
+ if (provider === "cloudinary" && mode === "large") {
1943
+ console.log(import_chalk7.default.yellow("\nCloudinary large multipart scaffolding is not available in this module yet."));
1944
+ console.log(import_chalk7.default.yellow("Use S3 or R2 for large uploads, or pick Proxy/Direct for Cloudinary.\n"));
1945
+ return null;
1946
+ }
1947
+ if (provider === "cloudinary" && preset === "document") {
1948
+ const warning = await (0, import_prompts5.default)({
1949
+ type: "confirm",
1950
+ name: "continue",
1951
+ message: "Cloudinary is media-first. Continue with Cloudinary for non-media/general files?",
1952
+ initial: false
1953
+ });
1954
+ if (!warning.continue) {
1955
+ console.log(import_chalk7.default.yellow("Operation cancelled."));
1956
+ return null;
1957
+ }
1958
+ }
1959
+ const presetDefaults = UPLOAD_PRESETS[preset];
1960
+ let useDatabaseMetadata = false;
1961
+ let shouldInstallDatabase = false;
1962
+ const metadataPrompt = await (0, import_prompts5.default)({
1963
+ type: "confirm",
1964
+ name: "metadata",
1965
+ message: drizzleInstalled ? "Store upload metadata in your database?" : "Install database and store upload metadata?",
1966
+ initial: true
1967
+ });
1968
+ if (metadataPrompt.metadata === void 0) {
1969
+ console.log(import_chalk7.default.yellow("Operation cancelled."));
1970
+ return null;
1971
+ }
1972
+ if (metadataPrompt.metadata === true) {
1973
+ useDatabaseMetadata = true;
1974
+ if (!projectConfig?.database) {
1975
+ shouldInstallDatabase = true;
1976
+ } else if (!drizzleInstalled) {
1977
+ console.log(import_chalk7.default.yellow("\nUploads metadata currently supports Drizzle-based database setups only."));
1978
+ console.log(import_chalk7.default.yellow("Install or switch to a Drizzle database, or continue without metadata.\n"));
1979
+ return null;
1980
+ }
1981
+ }
1982
+ if (access === "private" && !useDatabaseMetadata) {
1983
+ const warning = await (0, import_prompts5.default)({
1984
+ type: "confirm",
1985
+ name: "continue",
1986
+ message: "Private uploads work best with metadata. Continue without database tracking?",
1987
+ initial: false
1988
+ });
1989
+ if (!warning.continue) {
1990
+ console.log(import_chalk7.default.yellow("Operation cancelled."));
1991
+ return null;
1992
+ }
1993
+ }
1994
+ let shouldInstallAuth = false;
1995
+ if (authMode !== "none" && !authInstalled) {
1996
+ const authPrompt = await (0, import_prompts5.default)({
1997
+ type: "confirm",
1998
+ name: "installAuth",
1999
+ message: "Uploads auth requires the auth module. Install auth now?",
2000
+ initial: true
2001
+ });
2002
+ if (!authPrompt.installAuth) {
2003
+ console.log(import_chalk7.default.yellow("Operation cancelled."));
2004
+ return null;
2005
+ }
2006
+ shouldInstallAuth = true;
2007
+ }
2008
+ return {
2009
+ provider,
2010
+ mode,
2011
+ authMode,
2012
+ access,
2013
+ preset,
2014
+ useDatabaseMetadata,
2015
+ shouldInstallAuth,
2016
+ shouldInstallDatabase,
2017
+ envVars: {
2018
+ ...buildSharedEnvVars(provider, mode, authMode, access, preset, presetDefaults.maxFileSize, presetDefaults.maxFiles),
2019
+ ...getProviderEnvDefaults(provider)
2020
+ }
2021
+ };
2022
+ }
2023
+ async function injectUploadsRoutes(projectRoot, srcDir) {
2024
+ const routeIndexPath = import_path12.default.join(projectRoot, srcDir, "routes", "index.ts");
2025
+ const routeImport = `import uploadsRoutes from "./uploads.routes";`;
2026
+ const routeMountPattern = /rootRouter\.use\(\s*["']\/uploads["']\s*,\s*uploadsRoutes\s*\)/;
2027
+ if (await import_fs_extra11.default.pathExists(routeIndexPath)) {
2028
+ let routeContent = await import_fs_extra11.default.readFile(routeIndexPath, "utf-8");
2029
+ let routeModified = false;
2030
+ const importResult = appendImport(routeContent, routeImport);
2031
+ if (!importResult.inserted) {
2032
+ return false;
2033
+ }
2034
+ if (importResult.source !== routeContent) {
2035
+ routeContent = importResult.source;
2036
+ routeModified = true;
2037
+ }
2038
+ if (!routeMountPattern.test(routeContent)) {
2039
+ const routeSetup = `
2040
+ // Upload routes
2041
+ rootRouter.use("/uploads", uploadsRoutes);
2042
+ `;
2043
+ const exportMatch = routeContent.match(/export default rootRouter;?\s*$/m);
2044
+ if (!exportMatch || exportMatch.index === void 0) {
2045
+ return false;
2046
+ }
2047
+ routeContent = routeContent.slice(0, exportMatch.index) + routeSetup + "\n" + routeContent.slice(exportMatch.index);
2048
+ routeModified = true;
2049
+ }
2050
+ if (routeModified) {
2051
+ await import_fs_extra11.default.writeFile(routeIndexPath, routeContent);
2052
+ }
2053
+ return true;
2054
+ }
2055
+ const appPath = import_path12.default.join(projectRoot, srcDir, "app.ts");
2056
+ if (!await import_fs_extra11.default.pathExists(appPath)) {
2057
+ return false;
2058
+ }
2059
+ let appContent = await import_fs_extra11.default.readFile(appPath, "utf-8");
2060
+ let appModified = false;
2061
+ const appImportResult = appendImport(appContent, `import uploadsRoutes from "./routes/uploads.routes";`);
2062
+ if (!appImportResult.inserted) {
2063
+ return false;
2064
+ }
2065
+ if (appImportResult.source !== appContent) {
2066
+ appContent = appImportResult.source;
2067
+ appModified = true;
2068
+ }
2069
+ const hasMount = /app\.use\(\s*["']\/api\/uploads["']\s*,\s*uploadsRoutes\s*\)/.test(appContent);
2070
+ if (!hasMount) {
2071
+ const setup = `
2072
+ // Upload routes
2073
+ app.use("/api/uploads", uploadsRoutes);
2074
+ `;
2075
+ const exportMatch = appContent.match(/export default app;?\s*$/m);
2076
+ if (!exportMatch || exportMatch.index === void 0) {
2077
+ return false;
2078
+ }
2079
+ appContent = appContent.slice(0, exportMatch.index) + setup + "\n" + appContent.slice(exportMatch.index);
2080
+ appModified = true;
2081
+ }
2082
+ if (appModified) {
2083
+ await import_fs_extra11.default.writeFile(appPath, appContent);
2084
+ }
2085
+ return true;
2086
+ }
2087
+ async function isUploadsModuleInstalled(projectRoot, srcDir) {
2088
+ return import_fs_extra11.default.pathExists(import_path12.default.join(projectRoot, srcDir, "routes", "uploads.routes.ts"));
2089
+ }
2090
+ async function detectInstalledUploadsMode(projectRoot) {
2091
+ const envPath = import_path12.default.join(projectRoot, ".env");
2092
+ if (!await import_fs_extra11.default.pathExists(envPath)) {
2093
+ return null;
2094
+ }
2095
+ const content = await import_fs_extra11.default.readFile(envPath, "utf-8");
2096
+ const match = content.match(/^UPLOAD_MODE=(proxy|direct|large)$/m);
2097
+ if (!match) {
2098
+ return null;
2099
+ }
2100
+ return match[1];
2101
+ }
2102
+ async function injectUploadsDocs(projectRoot, srcDir, mode) {
2103
+ const openApiPath = import_path12.default.join(projectRoot, srcDir, "lib", "openapi.ts");
2104
+ if (!await import_fs_extra11.default.pathExists(openApiPath)) {
2105
+ return false;
2106
+ }
2107
+ const marker = "// ZURO_UPLOADS_DOCS";
2108
+ let content = await import_fs_extra11.default.readFile(openApiPath, "utf-8");
2109
+ if (content.includes(marker)) {
2110
+ return true;
2111
+ }
2112
+ const moduleDocsEndMarker = "// ZURO_DOCS_MODULES_END";
2113
+ if (!content.includes(moduleDocsEndMarker)) {
2114
+ return false;
2115
+ }
2116
+ const commonBlock = `
2117
+ const uploadAccessSchema = z.object({
2118
+ key: z.string().openapi({ example: "uploads/users/user_123/2026/03/06/example.png" }),
2119
+ resourceType: z.string().optional().openapi({ example: "image" }),
2120
+ providerAssetId: z.string().optional().openapi({ example: "uploads/users/user_123/example" }),
2121
+ });
2122
+
2123
+ `;
2124
+ const directBlock = mode === "direct" ? `registry.registerPath({
2125
+ method: "post",
2126
+ path: "/api/uploads/presign",
2127
+ tags: ["Uploads"],
2128
+ summary: "Create a signed direct upload request",
2129
+ request: {
2130
+ body: {
2131
+ content: {
2132
+ "application/json": {
2133
+ schema: z.object({
2134
+ originalName: z.string().min(1),
2135
+ mimeType: z.string().min(1),
2136
+ bytes: z.number().positive(),
2137
+ }),
2138
+ },
2139
+ },
2140
+ },
2141
+ },
2142
+ responses: {
2143
+ 200: { description: "Signed upload created" },
2144
+ },
2145
+ });
2146
+
2147
+ registry.registerPath({
2148
+ method: "post",
2149
+ path: "/api/uploads/complete",
2150
+ tags: ["Uploads"],
2151
+ summary: "Finalize a direct upload and persist metadata",
2152
+ responses: {
2153
+ 201: { description: "Upload finalized" },
2154
+ },
2155
+ });
2156
+ ` : "";
2157
+ const proxyBlock = mode === "proxy" ? `registry.registerPath({
2158
+ method: "post",
2159
+ path: "/api/uploads",
2160
+ tags: ["Uploads"],
2161
+ summary: "Upload a file through the API server",
2162
+ responses: {
2163
+ 201: { description: "File uploaded" },
2164
+ },
2165
+ });
2166
+ ` : "";
2167
+ const largeBlock = mode === "large" ? `registry.registerPath({
2168
+ method: "post",
2169
+ path: "/api/uploads/multipart/init",
2170
+ tags: ["Uploads"],
2171
+ summary: "Start a multipart upload session",
2172
+ responses: {
2173
+ 200: { description: "Multipart upload initialized" },
2174
+ },
2175
+ });
2176
+
2177
+ registry.registerPath({
2178
+ method: "post",
2179
+ path: "/api/uploads/multipart/complete",
2180
+ tags: ["Uploads"],
2181
+ summary: "Complete a multipart upload",
2182
+ responses: {
2183
+ 201: { description: "Multipart upload completed" },
2184
+ },
2185
+ });
2186
+
2187
+ registry.registerPath({
2188
+ method: "post",
2189
+ path: "/api/uploads/multipart/abort",
2190
+ tags: ["Uploads"],
2191
+ summary: "Abort a multipart upload",
2192
+ responses: {
2193
+ 204: { description: "Multipart upload aborted" },
2194
+ },
2195
+ });
2196
+ ` : "";
2197
+ const sharedOps = `registry.registerPath({
2198
+ method: "post",
2199
+ path: "/api/uploads/access-url",
2200
+ tags: ["Uploads"],
2201
+ summary: "Create an access URL for an uploaded file",
2202
+ request: {
2203
+ body: {
2204
+ content: {
2205
+ "application/json": {
2206
+ schema: uploadAccessSchema,
2207
+ },
2208
+ },
2209
+ },
2210
+ },
2211
+ responses: {
2212
+ 200: { description: "Access URL generated" },
2213
+ },
2214
+ });
2215
+
2216
+ registry.registerPath({
2217
+ method: "delete",
2218
+ path: "/api/uploads",
2219
+ tags: ["Uploads"],
2220
+ summary: "Delete an uploaded file",
2221
+ request: {
2222
+ body: {
2223
+ content: {
2224
+ "application/json": {
2225
+ schema: uploadAccessSchema,
2226
+ },
2227
+ },
2228
+ },
2229
+ },
2230
+ responses: {
2231
+ 204: { description: "Upload deleted" },
2232
+ },
2233
+ });
2234
+ `;
2235
+ const block = `
2236
+ ${commonBlock}${marker}
2237
+ ${proxyBlock}${directBlock}${largeBlock}${sharedOps}`;
2238
+ content = content.replace(moduleDocsEndMarker, `${block}
2239
+ ${moduleDocsEndMarker}`);
2240
+ const tagInsert = /(\{\s*name:\s*"Auth".+\},\s*\n\s*\],)/;
2241
+ if (tagInsert.test(content) && !content.includes(`{ name: "Uploads", description: "File uploads and asset access" }`)) {
2242
+ content = content.replace(
2243
+ tagInsert,
2244
+ `{ name: "Auth", description: "Authentication and session endpoints" },
2245
+ { name: "Uploads", description: "File uploads and asset access" },
2246
+ ],`
2247
+ );
2248
+ }
2249
+ await import_fs_extra11.default.writeFile(openApiPath, content);
2250
+ return true;
2251
+ }
2252
+ function printUploadHints(result) {
2253
+ console.log(import_chalk7.default.yellow("\u2139 Upload routes are mounted at: /api/uploads"));
2254
+ console.log(import_chalk7.default.yellow(`\u2139 Provider: ${result.provider} \xB7 Mode: ${result.mode} \xB7 Access: ${result.access}`));
2255
+ console.log(import_chalk7.default.yellow("\u2139 Fill the generated upload env vars in .env before testing uploads."));
2256
+ if (result.mode === "proxy") {
2257
+ console.log(import_chalk7.default.yellow("\u2139 Reuse uploadSingle()/uploadArray() from src/lib/uploads/proxy.ts in your own form + file routes."));
2258
+ }
2259
+ if (result.provider === "r2") {
2260
+ console.log(import_chalk7.default.yellow("\u2139 R2 presigned URLs use the R2 S3 API endpoint, not your custom domain."));
2261
+ }
2262
+ if (result.useDatabaseMetadata) {
2263
+ console.log(import_chalk7.default.yellow("\u2139 Upload metadata is stored in db/schema/uploads.ts."));
2264
+ } else {
2265
+ console.log(import_chalk7.default.yellow("\u2139 No upload metadata table was added. Private file ownership checks fall back to key prefixes."));
2266
+ }
2267
+ }
2268
+
1627
2269
  // src/commands/add.ts
1628
2270
  function resolvePackageManager(projectRoot) {
1629
- if (import_fs_extra11.default.existsSync(import_path12.default.join(projectRoot, "pnpm-lock.yaml"))) {
2271
+ if (import_fs_extra12.default.existsSync(import_path13.default.join(projectRoot, "pnpm-lock.yaml"))) {
1630
2272
  return "pnpm";
1631
2273
  }
1632
- if (import_fs_extra11.default.existsSync(import_path12.default.join(projectRoot, "bun.lockb")) || import_fs_extra11.default.existsSync(import_path12.default.join(projectRoot, "bun.lock"))) {
2274
+ if (import_fs_extra12.default.existsSync(import_path13.default.join(projectRoot, "bun.lockb")) || import_fs_extra12.default.existsSync(import_path13.default.join(projectRoot, "bun.lock"))) {
1633
2275
  return "bun";
1634
2276
  }
1635
- if (import_fs_extra11.default.existsSync(import_path12.default.join(projectRoot, "yarn.lock"))) {
2277
+ if (import_fs_extra12.default.existsSync(import_path13.default.join(projectRoot, "yarn.lock"))) {
1636
2278
  return "yarn";
1637
2279
  }
1638
2280
  return "npm";
@@ -1644,16 +2286,16 @@ function getModuleDocsPath(moduleName) {
1644
2286
  return moduleName;
1645
2287
  }
1646
2288
  async function hasEnvVariable(projectRoot, key) {
1647
- const envPath = import_path12.default.join(projectRoot, ".env");
1648
- if (!await import_fs_extra11.default.pathExists(envPath)) {
2289
+ const envPath = import_path13.default.join(projectRoot, ".env");
2290
+ if (!await import_fs_extra12.default.pathExists(envPath)) {
1649
2291
  return false;
1650
2292
  }
1651
- const content = await import_fs_extra11.default.readFile(envPath, "utf-8");
2293
+ const content = await import_fs_extra12.default.readFile(envPath, "utf-8");
1652
2294
  const pattern = new RegExp(`^${escapeRegex(key)}=`, "m");
1653
2295
  return pattern.test(content);
1654
2296
  }
1655
2297
  async function isLikelyEmptyDirectory(cwd) {
1656
- const entries = await import_fs_extra11.default.readdir(cwd);
2298
+ const entries = await import_fs_extra12.default.readdir(cwd);
1657
2299
  const ignored = /* @__PURE__ */ new Set([".ds_store", "thumbs.db"]);
1658
2300
  return entries.filter((entry) => !ignored.has(entry.toLowerCase())).length === 0;
1659
2301
  }
@@ -1677,16 +2319,22 @@ var add = async (moduleName, options = {}) => {
1677
2319
  let customDbUrl;
1678
2320
  let usedDefaultDbUrl = false;
1679
2321
  let databaseBackupPath = null;
2322
+ let selectedDatabaseOrm = null;
2323
+ let selectedDatabaseDialect = null;
1680
2324
  let generatedAuthSecret = false;
1681
2325
  let authDatabaseDialect = null;
1682
2326
  let customSmtpVars;
1683
2327
  let usedDefaultSmtp = false;
1684
2328
  let mailerProvider = "smtp";
1685
2329
  let shouldInstallDocsForAuth = false;
2330
+ let uploadConfig = null;
2331
+ let uploadDatabaseDialect = null;
1686
2332
  if (resolvedModuleName === "database" || isDatabaseModule(resolvedModuleName)) {
1687
2333
  const result = await promptDatabaseConfig(resolvedModuleName, projectRoot, srcDir);
1688
2334
  if (!result) return;
1689
2335
  resolvedModuleName = result.resolvedModuleName;
2336
+ selectedDatabaseOrm = result.selectedOrm;
2337
+ selectedDatabaseDialect = result.selectedDialect;
1690
2338
  customDbUrl = result.customDbUrl;
1691
2339
  usedDefaultDbUrl = result.usedDefaultDbUrl;
1692
2340
  databaseBackupPath = result.databaseBackupPath;
@@ -1704,6 +2352,10 @@ var add = async (moduleName, options = {}) => {
1704
2352
  shouldInstallDocsForAuth = result.shouldInstallDocsForAuth;
1705
2353
  authDatabaseDialect = result.authDatabaseDialect;
1706
2354
  }
2355
+ if (resolvedModuleName === "uploads") {
2356
+ uploadConfig = await promptUploadsConfig(projectRoot, srcDir);
2357
+ if (!uploadConfig) return;
2358
+ }
1707
2359
  const pm = resolvePackageManager(projectRoot);
1708
2360
  const spinner = (0, import_ora2.default)(`Checking registry for ${resolvedModuleName}...`).start();
1709
2361
  let currentStep = "package manager preflight";
@@ -1718,10 +2370,46 @@ var add = async (moduleName, options = {}) => {
1718
2370
  spinner.fail(`Module '${resolvedModuleName}' not found.`);
1719
2371
  return;
1720
2372
  }
1721
- spinner.succeed(`Found module: ${import_chalk7.default.cyan(resolvedModuleName)}`);
2373
+ spinner.succeed(`Found module: ${import_chalk8.default.cyan(resolvedModuleName)}`);
1722
2374
  const moduleDeps = module2.moduleDependencies || [];
1723
2375
  currentStep = "module dependency resolution";
1724
2376
  await resolveDependencies(moduleDeps, projectRoot);
2377
+ if (resolvedModuleName === "uploads" && uploadConfig) {
2378
+ const errorHandlerInstalled = import_fs_extra12.default.existsSync(import_path13.default.join(projectRoot, srcDir, "lib", "errors.ts"));
2379
+ if (!errorHandlerInstalled) {
2380
+ console.log(import_chalk8.default.blue("\n\u2139 Uploads needs the error-handler module. Installing error-handler..."));
2381
+ await add("error-handler");
2382
+ }
2383
+ if (uploadConfig.shouldInstallDatabase) {
2384
+ console.log(import_chalk8.default.blue("\n\u2139 Upload metadata needs a Drizzle database. Installing database module..."));
2385
+ await add("database");
2386
+ }
2387
+ if (uploadConfig.shouldInstallAuth) {
2388
+ console.log(import_chalk8.default.blue("\n\u2139 Upload auth needs the auth module. Installing auth module..."));
2389
+ await add("auth", { yes: true });
2390
+ }
2391
+ uploadDatabaseDialect = await detectInstalledDatabaseDialect(projectRoot, srcDir);
2392
+ if (uploadConfig.useDatabaseMetadata) {
2393
+ if (uploadDatabaseDialect === "database-prisma-pg" || uploadDatabaseDialect === "database-prisma-mysql") {
2394
+ spinner.fail("Uploads metadata currently supports Drizzle-based database setup only.");
2395
+ console.log(import_chalk8.default.yellow("\u2139 Install a Drizzle database, or rerun uploads without metadata."));
2396
+ return;
2397
+ }
2398
+ if (!uploadDatabaseDialect) {
2399
+ spinner.fail("Could not detect a database setup for uploads metadata.");
2400
+ console.log(import_chalk8.default.yellow("\u2139 Install the database module first, then rerun uploads."));
2401
+ return;
2402
+ }
2403
+ }
2404
+ }
2405
+ if (resolvedModuleName === "auth") {
2406
+ authDatabaseDialect = await detectInstalledDatabaseDialect(projectRoot, srcDir);
2407
+ if (authDatabaseDialect === "database-prisma-pg" || authDatabaseDialect === "database-prisma-mysql") {
2408
+ spinner.fail("Auth module currently supports Drizzle-based database setup only.");
2409
+ console.log(import_chalk8.default.yellow("\u2139 Install auth after switching database ORM to Drizzle."));
2410
+ return;
2411
+ }
2412
+ }
1725
2413
  currentStep = "dependency installation";
1726
2414
  spinner.start("Installing dependencies...");
1727
2415
  let runtimeDeps = module2.dependencies || [];
@@ -1735,6 +2423,15 @@ var add = async (moduleName, options = {}) => {
1735
2423
  devDeps = ["@types/nodemailer"];
1736
2424
  }
1737
2425
  }
2426
+ if (resolvedModuleName === "uploads" && uploadConfig) {
2427
+ runtimeDeps = ["multer"];
2428
+ devDeps = ["@types/multer"];
2429
+ if (uploadConfig.provider === "cloudinary") {
2430
+ runtimeDeps.push("cloudinary");
2431
+ } else {
2432
+ runtimeDeps.push("@aws-sdk/client-s3", "@aws-sdk/s3-request-presigner");
2433
+ }
2434
+ }
1738
2435
  await installDependencies(pm, runtimeDeps, projectRoot);
1739
2436
  await installDependencies(pm, devDeps, projectRoot, { dev: true });
1740
2437
  spinner.succeed("Dependencies installed");
@@ -1754,12 +2451,37 @@ var add = async (moduleName, options = {}) => {
1754
2451
  expectedSha256 = void 0;
1755
2452
  expectedSize = void 0;
1756
2453
  }
2454
+ if (resolvedModuleName === "uploads" && uploadConfig) {
2455
+ if (file.target === "lib/uploads/provider.ts") {
2456
+ fetchPath = uploadConfig.provider === "cloudinary" ? "express/lib/uploads/provider.cloudinary.ts" : "express/lib/uploads/provider.s3.ts";
2457
+ expectedSha256 = void 0;
2458
+ expectedSize = void 0;
2459
+ }
2460
+ if (file.target === "lib/uploads/metadata.ts") {
2461
+ fetchPath = uploadConfig.useDatabaseMetadata ? "express/lib/uploads/metadata.db.ts" : "express/lib/uploads/metadata.noop.ts";
2462
+ expectedSha256 = void 0;
2463
+ expectedSize = void 0;
2464
+ }
2465
+ if (file.target === "middleware/upload-auth.ts") {
2466
+ fetchPath = `express/middleware/upload-auth.${uploadConfig.authMode}.ts`;
2467
+ expectedSha256 = void 0;
2468
+ expectedSize = void 0;
2469
+ }
2470
+ if (file.target === "db/schema/uploads.ts") {
2471
+ if (!uploadConfig.useDatabaseMetadata) {
2472
+ continue;
2473
+ }
2474
+ fetchPath = uploadDatabaseDialect === "database-mysql" ? "express/db/schema/uploads.mysql.ts" : "express/db/schema/uploads.ts";
2475
+ expectedSha256 = void 0;
2476
+ expectedSize = void 0;
2477
+ }
2478
+ }
1757
2479
  let content = await fetchFile(fetchPath, {
1758
2480
  baseUrl: registryContext.fileBaseUrl,
1759
2481
  expectedSha256,
1760
2482
  expectedSize
1761
2483
  });
1762
- if (isDatabaseModule(resolvedModuleName) && file.target === "../drizzle.config.ts") {
2484
+ if (isDrizzleDatabaseModule(resolvedModuleName) && file.target === "../drizzle.config.ts") {
1763
2485
  const normalizedSrcDir = srcDir.replace(/\\/g, "/");
1764
2486
  content = content.replace(
1765
2487
  /schema:\s*["'][^"']+["']/,
@@ -1767,10 +2489,10 @@ var add = async (moduleName, options = {}) => {
1767
2489
  );
1768
2490
  }
1769
2491
  const targetPath = resolveSafeTargetPath2(projectRoot, srcDir, file);
1770
- await import_fs_extra11.default.ensureDir(import_path12.default.dirname(targetPath));
1771
- await import_fs_extra11.default.writeFile(targetPath, content);
2492
+ await import_fs_extra12.default.ensureDir(import_path13.default.dirname(targetPath));
2493
+ await import_fs_extra12.default.writeFile(targetPath, content);
1772
2494
  }
1773
- const schemaExports = module2.files.map((file) => file.target.replace(/\\/g, "/")).filter((target) => /^db\/schema\/[^/]+\.ts$/.test(target)).map((target) => import_path12.default.posix.basename(target, ".ts")).filter((name) => name !== "index");
2495
+ const schemaExports = module2.files.map((file) => file.target.replace(/\\/g, "/")).filter((target) => /^db\/schema\/[^/]+\.ts$/.test(target)).map((target) => import_path13.default.posix.basename(target, ".ts")).filter((name) => name !== "index");
1774
2496
  for (const schemaFileName of schemaExports) {
1775
2497
  await ensureSchemaExport(projectRoot, srcDir, schemaFileName);
1776
2498
  }
@@ -1830,6 +2552,38 @@ var add = async (moduleName, options = {}) => {
1830
2552
  spinner.warn("Could not update API docs automatically");
1831
2553
  }
1832
2554
  }
2555
+ const uploadsInstalled = await isUploadsModuleInstalled(projectRoot, srcDir);
2556
+ if (uploadsInstalled) {
2557
+ const uploadMode = await detectInstalledUploadsMode(projectRoot);
2558
+ if (uploadMode) {
2559
+ spinner.start("Adding uploads endpoints to API docs...");
2560
+ const uploadsDocsInjected = await injectUploadsDocs(projectRoot, srcDir, uploadMode);
2561
+ if (uploadsDocsInjected) {
2562
+ spinner.succeed("Uploads endpoints added to API docs");
2563
+ } else {
2564
+ spinner.warn("Could not update API docs automatically");
2565
+ }
2566
+ }
2567
+ }
2568
+ }
2569
+ if (resolvedModuleName === "uploads" && uploadConfig) {
2570
+ spinner.start("Configuring upload routes...");
2571
+ const injected = await injectUploadsRoutes(projectRoot, srcDir);
2572
+ if (injected) {
2573
+ spinner.succeed("Upload routes configured");
2574
+ } else {
2575
+ spinner.warn("Could not configure upload routes automatically");
2576
+ }
2577
+ const docsInstalled = await isDocsModuleInstalled(projectRoot, srcDir);
2578
+ if (docsInstalled) {
2579
+ spinner.start("Adding uploads endpoints to API docs...");
2580
+ const docsInjected = await injectUploadsDocs(projectRoot, srcDir, uploadConfig.mode);
2581
+ if (docsInjected) {
2582
+ spinner.succeed("Uploads endpoints added to API docs");
2583
+ } else {
2584
+ spinner.warn("Could not update API docs automatically");
2585
+ }
2586
+ }
1833
2587
  }
1834
2588
  let envConfigKey = resolvedModuleName;
1835
2589
  if (resolvedModuleName === "mailer" && mailerProvider === "resend") {
@@ -1859,12 +2613,34 @@ var add = async (moduleName, options = {}) => {
1859
2613
  await updateEnvSchema(projectRoot, srcDir, envConfig.schemaFields);
1860
2614
  spinner.succeed("Environment configured");
1861
2615
  }
1862
- console.log(import_chalk7.default.green(`
2616
+ if (resolvedModuleName === "uploads" && uploadConfig) {
2617
+ currentStep = "environment configuration";
2618
+ spinner.start("Updating environment configuration...");
2619
+ await updateEnvFile(projectRoot, uploadConfig.envVars, true);
2620
+ await updateEnvSchema(projectRoot, srcDir, getUploadEnvSchemaFields(uploadConfig.provider));
2621
+ spinner.succeed("Environment configured");
2622
+ }
2623
+ if (isDatabaseModule(resolvedModuleName)) {
2624
+ const selected = getDatabaseSelection(resolvedModuleName);
2625
+ const orm = selectedDatabaseOrm ?? selected.orm;
2626
+ const dialect = selectedDatabaseDialect ?? selected.dialect;
2627
+ const latestConfig = await readZuroConfig(projectRoot);
2628
+ if (latestConfig) {
2629
+ await writeZuroConfig(projectRoot, {
2630
+ ...latestConfig,
2631
+ database: {
2632
+ orm,
2633
+ dialect
2634
+ }
2635
+ });
2636
+ }
2637
+ }
2638
+ console.log(import_chalk8.default.green(`
1863
2639
  \u2714 ${resolvedModuleName} added successfully!
1864
2640
  `));
1865
2641
  const docsPath = getModuleDocsPath(resolvedModuleName);
1866
2642
  const docsUrl = `https://zuro-cli.devbybriyan.com/docs/${docsPath}`;
1867
- console.log(import_chalk7.default.blue(`\u2139 Docs: ${docsUrl}`));
2643
+ console.log(import_chalk8.default.blue(`\u2139 Docs: ${docsUrl}`));
1868
2644
  if (isDatabaseModule(resolvedModuleName)) {
1869
2645
  printDatabaseHints(resolvedModuleName, customDbUrl, usedDefaultDbUrl, databaseBackupPath);
1870
2646
  }
@@ -1877,17 +2653,20 @@ var add = async (moduleName, options = {}) => {
1877
2653
  if (resolvedModuleName === "docs") {
1878
2654
  printDocsHints();
1879
2655
  }
2656
+ if (resolvedModuleName === "uploads" && uploadConfig) {
2657
+ printUploadHints(uploadConfig);
2658
+ }
1880
2659
  if (resolvedModuleName === "auth" && shouldInstallDocsForAuth) {
1881
- console.log(import_chalk7.default.blue("\n\u2139 Installing API docs module..."));
2660
+ console.log(import_chalk8.default.blue("\n\u2139 Installing API docs module..."));
1882
2661
  await add("docs", { yes: true });
1883
2662
  }
1884
2663
  } catch (error) {
1885
- spinner.fail(import_chalk7.default.red(`Failed during ${currentStep}.`));
2664
+ spinner.fail(import_chalk8.default.red(`Failed during ${currentStep}.`));
1886
2665
  const errorMessage = error instanceof Error ? error.message : String(error);
1887
- console.error(import_chalk7.default.red(errorMessage));
2666
+ console.error(import_chalk8.default.red(errorMessage));
1888
2667
  console.log(`
1889
- ${import_chalk7.default.bold("Retry:")}`);
1890
- console.log(import_chalk7.default.cyan(` npx zuro-cli add ${resolvedModuleName}`));
2668
+ ${import_chalk8.default.bold("Retry:")}`);
2669
+ console.log(import_chalk8.default.cyan(` npx zuro-cli add ${resolvedModuleName}`));
1891
2670
  }
1892
2671
  };
1893
2672