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.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 (
|
|
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 (
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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 (
|
|
123
|
-
|
|
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
|
-
|
|
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: "
|
|
267
|
+
name: "dialect",
|
|
191
268
|
message: "Which database dialect?",
|
|
192
269
|
choices: [
|
|
193
|
-
{ title: "PostgreSQL", value: "
|
|
194
|
-
{ title: "MySQL", value: "
|
|
270
|
+
{ title: "PostgreSQL", value: "postgresql" },
|
|
271
|
+
{ title: "MySQL", value: "mysql" }
|
|
195
272
|
]
|
|
196
273
|
});
|
|
197
|
-
if (!variantResponse.
|
|
274
|
+
if (!variantResponse.dialect) {
|
|
198
275
|
console.log(import_chalk4.default.yellow("Operation cancelled."));
|
|
199
276
|
return null;
|
|
200
277
|
}
|
|
201
|
-
resolvedModuleName = variantResponse.
|
|
278
|
+
resolvedModuleName = getDatabaseModule(ormResponse.orm, variantResponse.dialect);
|
|
202
279
|
} else {
|
|
203
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
357
|
-
console.log(
|
|
358
|
-
console.log(
|
|
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
|
|
1096
|
-
var
|
|
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
|
-
|
|
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
|
|
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 (
|
|
2271
|
+
if (import_fs_extra12.default.existsSync(import_path13.default.join(projectRoot, "pnpm-lock.yaml"))) {
|
|
1630
2272
|
return "pnpm";
|
|
1631
2273
|
}
|
|
1632
|
-
if (
|
|
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 (
|
|
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 =
|
|
1648
|
-
if (!await
|
|
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
|
|
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
|
|
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: ${
|
|
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 (
|
|
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
|
|
1771
|
-
await
|
|
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) =>
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
2664
|
+
spinner.fail(import_chalk8.default.red(`Failed during ${currentStep}.`));
|
|
1886
2665
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1887
|
-
console.error(
|
|
2666
|
+
console.error(import_chalk8.default.red(errorMessage));
|
|
1888
2667
|
console.log(`
|
|
1889
|
-
${
|
|
1890
|
-
console.log(
|
|
2668
|
+
${import_chalk8.default.bold("Retry:")}`);
|
|
2669
|
+
console.log(import_chalk8.default.cyan(` npx zuro-cli add ${resolvedModuleName}`));
|
|
1891
2670
|
}
|
|
1892
2671
|
};
|
|
1893
2672
|
|