zuro-cli 0.0.2-beta.13 → 0.0.2-beta.14
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/chunk-VMOTWTER.mjs +37 -0
- package/dist/chunk-VMOTWTER.mjs.map +1 -0
- package/dist/chunk-W36ZIR4Y.mjs +216 -0
- package/dist/chunk-W36ZIR4Y.mjs.map +1 -0
- package/dist/chunk-YBAO5SKK.mjs +87 -0
- package/dist/chunk-YBAO5SKK.mjs.map +1 -0
- package/dist/database.handler-D7EVXRJX.mjs +28 -0
- package/dist/database.handler-D7EVXRJX.mjs.map +1 -0
- package/dist/docs.handler-JL3ZIVJQ.mjs +12 -0
- package/dist/docs.handler-JL3ZIVJQ.mjs.map +1 -0
- package/dist/index.js +800 -536
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +436 -506
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6,6 +6,13 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
6
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
7
|
var __getProtoOf = Object.getPrototypeOf;
|
|
8
8
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __esm = (fn, res) => function __init() {
|
|
10
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
11
|
+
};
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
9
16
|
var __copyProps = (to, from, except, desc) => {
|
|
10
17
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
18
|
for (let key of __getOwnPropNames(from))
|
|
@@ -23,6 +30,343 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
30
|
mod
|
|
24
31
|
));
|
|
25
32
|
|
|
33
|
+
// src/utils/code-inject.ts
|
|
34
|
+
function escapeRegex(value) {
|
|
35
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
36
|
+
}
|
|
37
|
+
function appendImport(source, line) {
|
|
38
|
+
if (source.includes(line)) {
|
|
39
|
+
return { source, inserted: true };
|
|
40
|
+
}
|
|
41
|
+
const importRegex = /^import .+ from .+;?\s*$/gm;
|
|
42
|
+
let lastImportIndex = 0;
|
|
43
|
+
let match;
|
|
44
|
+
while ((match = importRegex.exec(source)) !== null) {
|
|
45
|
+
lastImportIndex = match.index + match[0].length;
|
|
46
|
+
}
|
|
47
|
+
if (lastImportIndex <= 0) {
|
|
48
|
+
return { source, inserted: false };
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
source: source.slice(0, lastImportIndex) + `
|
|
52
|
+
${line}` + source.slice(lastImportIndex),
|
|
53
|
+
inserted: true
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
var init_code_inject = __esm({
|
|
57
|
+
"src/utils/code-inject.ts"() {
|
|
58
|
+
"use strict";
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// src/handlers/database.handler.ts
|
|
63
|
+
var database_handler_exports = {};
|
|
64
|
+
__export(database_handler_exports, {
|
|
65
|
+
DEFAULT_DATABASE_URLS: () => DEFAULT_DATABASE_URLS,
|
|
66
|
+
backupDatabaseFiles: () => backupDatabaseFiles,
|
|
67
|
+
databaseLabel: () => databaseLabel,
|
|
68
|
+
detectInstalledDatabaseDialect: () => detectInstalledDatabaseDialect,
|
|
69
|
+
ensureSchemaExport: () => ensureSchemaExport,
|
|
70
|
+
getDatabaseSetupHint: () => getDatabaseSetupHint,
|
|
71
|
+
isDatabaseModule: () => isDatabaseModule,
|
|
72
|
+
parseDatabaseDialect: () => parseDatabaseDialect,
|
|
73
|
+
printDatabaseHints: () => printDatabaseHints,
|
|
74
|
+
promptDatabaseConfig: () => promptDatabaseConfig,
|
|
75
|
+
validateDatabaseUrl: () => validateDatabaseUrl
|
|
76
|
+
});
|
|
77
|
+
function parseDatabaseDialect(value) {
|
|
78
|
+
const normalized = value?.trim().toLowerCase();
|
|
79
|
+
if (!normalized) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
if (normalized === "pg" || normalized === "postgres" || normalized === "postgresql" || normalized === "database-pg") {
|
|
83
|
+
return "database-pg";
|
|
84
|
+
}
|
|
85
|
+
if (normalized === "mysql" || normalized === "database-mysql") {
|
|
86
|
+
return "database-mysql";
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
function isDatabaseModule(moduleName) {
|
|
91
|
+
return moduleName === "database-pg" || moduleName === "database-mysql";
|
|
92
|
+
}
|
|
93
|
+
function validateDatabaseUrl(rawUrl, moduleName) {
|
|
94
|
+
const dbUrl = rawUrl.trim();
|
|
95
|
+
if (!dbUrl) {
|
|
96
|
+
throw new Error("Database URL cannot be empty.");
|
|
97
|
+
}
|
|
98
|
+
let parsed;
|
|
99
|
+
try {
|
|
100
|
+
parsed = new URL(dbUrl);
|
|
101
|
+
} catch {
|
|
102
|
+
throw new Error(`Invalid database URL: '${dbUrl}'.`);
|
|
103
|
+
}
|
|
104
|
+
const protocol = parsed.protocol.toLowerCase();
|
|
105
|
+
if (moduleName === "database-pg" && protocol !== "postgresql:" && protocol !== "postgres:") {
|
|
106
|
+
throw new Error("PostgreSQL URL must start with postgres:// or postgresql://");
|
|
107
|
+
}
|
|
108
|
+
if (moduleName === "database-mysql" && protocol !== "mysql:") {
|
|
109
|
+
throw new Error("MySQL URL must start with mysql://");
|
|
110
|
+
}
|
|
111
|
+
return dbUrl;
|
|
112
|
+
}
|
|
113
|
+
async function detectInstalledDatabaseDialect(projectRoot, srcDir) {
|
|
114
|
+
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";
|
|
121
|
+
}
|
|
122
|
+
if (content.includes("drizzle-orm/mysql2") || content.includes(`from "mysql2`)) {
|
|
123
|
+
return "database-mysql";
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
async function backupDatabaseFiles(projectRoot, srcDir) {
|
|
128
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
129
|
+
const backupRoot = import_path7.default.join(projectRoot, ".zuro", "backups", `database-${timestamp}`);
|
|
130
|
+
const candidates = [
|
|
131
|
+
import_path7.default.join(projectRoot, srcDir, "db", "index.ts"),
|
|
132
|
+
import_path7.default.join(projectRoot, "drizzle.config.ts")
|
|
133
|
+
];
|
|
134
|
+
let copied = false;
|
|
135
|
+
for (const filePath of candidates) {
|
|
136
|
+
if (!import_fs_extra6.default.existsSync(filePath)) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const relativePath = import_path7.default.relative(projectRoot, filePath);
|
|
140
|
+
const backupPath = import_path7.default.join(backupRoot, relativePath);
|
|
141
|
+
await import_fs_extra6.default.ensureDir(import_path7.default.dirname(backupPath));
|
|
142
|
+
await import_fs_extra6.default.copyFile(filePath, backupPath);
|
|
143
|
+
copied = true;
|
|
144
|
+
}
|
|
145
|
+
return copied ? backupRoot : null;
|
|
146
|
+
}
|
|
147
|
+
function databaseLabel(moduleName) {
|
|
148
|
+
return moduleName === "database-pg" ? "PostgreSQL" : "MySQL";
|
|
149
|
+
}
|
|
150
|
+
function getDatabaseSetupHint(moduleName, dbUrl) {
|
|
151
|
+
try {
|
|
152
|
+
const parsed = new URL(dbUrl);
|
|
153
|
+
const dbName = parsed.pathname.replace(/^\/+/, "") || "mydb";
|
|
154
|
+
if (moduleName === "database-pg") {
|
|
155
|
+
return `createdb ${dbName}`;
|
|
156
|
+
}
|
|
157
|
+
return `mysql -e "CREATE DATABASE IF NOT EXISTS ${dbName};"`;
|
|
158
|
+
} catch {
|
|
159
|
+
return moduleName === "database-pg" ? "createdb <database_name>" : `mysql -e "CREATE DATABASE IF NOT EXISTS <database_name>;"`;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async function ensureSchemaExport(projectRoot, srcDir, schemaFileName) {
|
|
163
|
+
const schemaIndexPath = import_path7.default.join(projectRoot, srcDir, "db", "schema", "index.ts");
|
|
164
|
+
if (!await import_fs_extra6.default.pathExists(schemaIndexPath)) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const exportLine = `export * from "./${schemaFileName}";`;
|
|
168
|
+
const content = await import_fs_extra6.default.readFile(schemaIndexPath, "utf-8");
|
|
169
|
+
const normalized = content.replace(/\r\n/g, "\n");
|
|
170
|
+
const exportPattern = new RegExp(
|
|
171
|
+
`^\\s*export\\s*\\*\\s*from\\s*["']\\./${escapeRegex(schemaFileName)}["'];?\\s*$`,
|
|
172
|
+
"m"
|
|
173
|
+
);
|
|
174
|
+
if (exportPattern.test(normalized)) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
let next = normalized.replace(/^\s*export\s*\{\s*\};?\s*$/m, "").trimEnd();
|
|
178
|
+
if (next.length > 0) {
|
|
179
|
+
next += "\n\n";
|
|
180
|
+
}
|
|
181
|
+
next += `${exportLine}
|
|
182
|
+
`;
|
|
183
|
+
await import_fs_extra6.default.writeFile(schemaIndexPath, next);
|
|
184
|
+
}
|
|
185
|
+
async function promptDatabaseConfig(initialModuleName, projectRoot, srcDir) {
|
|
186
|
+
let resolvedModuleName;
|
|
187
|
+
if (initialModuleName === "database") {
|
|
188
|
+
const variantResponse = await (0, import_prompts2.default)({
|
|
189
|
+
type: "select",
|
|
190
|
+
name: "variant",
|
|
191
|
+
message: "Which database dialect?",
|
|
192
|
+
choices: [
|
|
193
|
+
{ title: "PostgreSQL", value: "database-pg" },
|
|
194
|
+
{ title: "MySQL", value: "database-mysql" }
|
|
195
|
+
]
|
|
196
|
+
});
|
|
197
|
+
if (!variantResponse.variant) {
|
|
198
|
+
console.log(import_chalk4.default.yellow("Operation cancelled."));
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
resolvedModuleName = variantResponse.variant;
|
|
202
|
+
} else {
|
|
203
|
+
resolvedModuleName = initialModuleName;
|
|
204
|
+
}
|
|
205
|
+
let databaseBackupPath = null;
|
|
206
|
+
const installedDialect = await detectInstalledDatabaseDialect(projectRoot, srcDir);
|
|
207
|
+
if (installedDialect && installedDialect !== resolvedModuleName) {
|
|
208
|
+
console.log(
|
|
209
|
+
import_chalk4.default.yellow(
|
|
210
|
+
`
|
|
211
|
+
\u26A0 Existing database setup detected: ${databaseLabel(installedDialect)}.`
|
|
212
|
+
)
|
|
213
|
+
);
|
|
214
|
+
console.log(
|
|
215
|
+
import_chalk4.default.yellow(
|
|
216
|
+
` Switching to ${databaseLabel(resolvedModuleName)} will overwrite db files and drizzle config.
|
|
217
|
+
`
|
|
218
|
+
)
|
|
219
|
+
);
|
|
220
|
+
const switchResponse = await (0, import_prompts2.default)({
|
|
221
|
+
type: "confirm",
|
|
222
|
+
name: "proceed",
|
|
223
|
+
message: "Continue and switch database dialect?",
|
|
224
|
+
initial: false
|
|
225
|
+
});
|
|
226
|
+
if (!switchResponse.proceed) {
|
|
227
|
+
console.log(import_chalk4.default.yellow("Operation cancelled."));
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
databaseBackupPath = await backupDatabaseFiles(projectRoot, srcDir);
|
|
231
|
+
}
|
|
232
|
+
const defaultUrl = DEFAULT_DATABASE_URLS[resolvedModuleName];
|
|
233
|
+
console.log(import_chalk4.default.dim(` Tip: Leave blank to use ${defaultUrl}
|
|
234
|
+
`));
|
|
235
|
+
const response = await (0, import_prompts2.default)({
|
|
236
|
+
type: "text",
|
|
237
|
+
name: "dbUrl",
|
|
238
|
+
message: "Database URL",
|
|
239
|
+
initial: ""
|
|
240
|
+
});
|
|
241
|
+
if (response.dbUrl === void 0) {
|
|
242
|
+
console.log(import_chalk4.default.yellow("Operation cancelled."));
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
const enteredUrl = response.dbUrl?.trim() || "";
|
|
246
|
+
const usedDefaultDbUrl = enteredUrl.length === 0;
|
|
247
|
+
const customDbUrl = validateDatabaseUrl(enteredUrl || defaultUrl, resolvedModuleName);
|
|
248
|
+
return { resolvedModuleName, customDbUrl, usedDefaultDbUrl, databaseBackupPath };
|
|
249
|
+
}
|
|
250
|
+
function printDatabaseHints(moduleName, customDbUrl, usedDefaultDbUrl, databaseBackupPath) {
|
|
251
|
+
if (databaseBackupPath) {
|
|
252
|
+
console.log(import_chalk4.default.blue(`\u2139 Backup created at: ${databaseBackupPath}
|
|
253
|
+
`));
|
|
254
|
+
}
|
|
255
|
+
if (usedDefaultDbUrl) {
|
|
256
|
+
console.log(import_chalk4.default.yellow("\u2139 Review DATABASE_URL in .env if your local DB config differs."));
|
|
257
|
+
}
|
|
258
|
+
const setupHint = getDatabaseSetupHint(
|
|
259
|
+
moduleName,
|
|
260
|
+
customDbUrl || DEFAULT_DATABASE_URLS[moduleName]
|
|
261
|
+
);
|
|
262
|
+
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"));
|
|
264
|
+
}
|
|
265
|
+
var import_path7, import_fs_extra6, import_prompts2, import_chalk4, DEFAULT_DATABASE_URLS;
|
|
266
|
+
var init_database_handler = __esm({
|
|
267
|
+
"src/handlers/database.handler.ts"() {
|
|
268
|
+
"use strict";
|
|
269
|
+
import_path7 = __toESM(require("path"));
|
|
270
|
+
import_fs_extra6 = __toESM(require("fs-extra"));
|
|
271
|
+
import_prompts2 = __toESM(require("prompts"));
|
|
272
|
+
import_chalk4 = __toESM(require("chalk"));
|
|
273
|
+
init_code_inject();
|
|
274
|
+
DEFAULT_DATABASE_URLS = {
|
|
275
|
+
"database-pg": "postgresql://postgres@localhost:5432/mydb",
|
|
276
|
+
"database-mysql": "mysql://root@localhost:3306/mydb"
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// src/handlers/docs.handler.ts
|
|
282
|
+
var docs_handler_exports = {};
|
|
283
|
+
__export(docs_handler_exports, {
|
|
284
|
+
injectDocsRoutes: () => injectDocsRoutes,
|
|
285
|
+
isDocsModuleInstalled: () => isDocsModuleInstalled,
|
|
286
|
+
printDocsHints: () => printDocsHints
|
|
287
|
+
});
|
|
288
|
+
async function isDocsModuleInstalled(projectRoot, srcDir) {
|
|
289
|
+
return await import_fs_extra7.default.pathExists(import_path8.default.join(projectRoot, srcDir, "lib", "openapi.ts"));
|
|
290
|
+
}
|
|
291
|
+
async function injectDocsRoutes(projectRoot, srcDir) {
|
|
292
|
+
const routeIndexPath = import_path8.default.join(projectRoot, srcDir, "routes", "index.ts");
|
|
293
|
+
const routeImport = `import docsRoutes from "./docs.routes";`;
|
|
294
|
+
const routeMountPattern = /rootRouter\.use\(\s*["']\/docs["']\s*,\s*docsRoutes\s*\)/;
|
|
295
|
+
if (await import_fs_extra7.default.pathExists(routeIndexPath)) {
|
|
296
|
+
let routeContent = await import_fs_extra7.default.readFile(routeIndexPath, "utf-8");
|
|
297
|
+
let routeModified = false;
|
|
298
|
+
const importResult = appendImport(routeContent, routeImport);
|
|
299
|
+
if (!importResult.inserted) {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
if (importResult.source !== routeContent) {
|
|
303
|
+
routeContent = importResult.source;
|
|
304
|
+
routeModified = true;
|
|
305
|
+
}
|
|
306
|
+
if (!routeMountPattern.test(routeContent)) {
|
|
307
|
+
const routeSetup = `
|
|
308
|
+
// API docs
|
|
309
|
+
rootRouter.use("/docs", docsRoutes);
|
|
310
|
+
`;
|
|
311
|
+
const exportMatch = routeContent.match(/export default rootRouter;?\s*$/m);
|
|
312
|
+
if (!exportMatch || exportMatch.index === void 0) {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
routeContent = routeContent.slice(0, exportMatch.index) + routeSetup + "\n" + routeContent.slice(exportMatch.index);
|
|
316
|
+
routeModified = true;
|
|
317
|
+
}
|
|
318
|
+
if (routeModified) {
|
|
319
|
+
await import_fs_extra7.default.writeFile(routeIndexPath, routeContent);
|
|
320
|
+
}
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
const appPath = import_path8.default.join(projectRoot, srcDir, "app.ts");
|
|
324
|
+
if (!await import_fs_extra7.default.pathExists(appPath)) {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
let appContent = await import_fs_extra7.default.readFile(appPath, "utf-8");
|
|
328
|
+
let appModified = false;
|
|
329
|
+
const appImportResult = appendImport(appContent, `import docsRoutes from "./routes/docs.routes";`);
|
|
330
|
+
if (!appImportResult.inserted) {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
if (appImportResult.source !== appContent) {
|
|
334
|
+
appContent = appImportResult.source;
|
|
335
|
+
appModified = true;
|
|
336
|
+
}
|
|
337
|
+
const hasMount = /app\.use\(\s*["']\/api\/docs["']\s*,\s*docsRoutes\s*\)/.test(appContent);
|
|
338
|
+
if (!hasMount) {
|
|
339
|
+
const setup = `
|
|
340
|
+
// API docs
|
|
341
|
+
app.use("/api/docs", docsRoutes);
|
|
342
|
+
`;
|
|
343
|
+
const exportMatch = appContent.match(/export default app;?\s*$/m);
|
|
344
|
+
if (!exportMatch || exportMatch.index === void 0) {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
appContent = appContent.slice(0, exportMatch.index) + setup + "\n" + appContent.slice(exportMatch.index);
|
|
348
|
+
appModified = true;
|
|
349
|
+
}
|
|
350
|
+
if (appModified) {
|
|
351
|
+
await import_fs_extra7.default.writeFile(appPath, appContent);
|
|
352
|
+
}
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
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"));
|
|
359
|
+
}
|
|
360
|
+
var import_path8, import_fs_extra7;
|
|
361
|
+
var init_docs_handler = __esm({
|
|
362
|
+
"src/handlers/docs.handler.ts"() {
|
|
363
|
+
"use strict";
|
|
364
|
+
import_path8 = __toESM(require("path"));
|
|
365
|
+
import_fs_extra7 = __toESM(require("fs-extra"));
|
|
366
|
+
init_code_inject();
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
|
|
26
370
|
// src/index.ts
|
|
27
371
|
var import_commander = require("commander");
|
|
28
372
|
|
|
@@ -32,6 +376,7 @@ var import_chalk2 = __toESM(require("chalk"));
|
|
|
32
376
|
var import_fs_extra4 = __toESM(require("fs-extra"));
|
|
33
377
|
var import_path4 = __toESM(require("path"));
|
|
34
378
|
var import_prompts = __toESM(require("prompts"));
|
|
379
|
+
var import_child_process = require("child_process");
|
|
35
380
|
|
|
36
381
|
// src/utils/registry.ts
|
|
37
382
|
var import_node_crypto = require("crypto");
|
|
@@ -383,6 +728,16 @@ var ENV_CONFIGS = {
|
|
|
383
728
|
{ name: "RESEND_API_KEY", schema: "z.string().min(1)" },
|
|
384
729
|
{ name: "MAIL_FROM", schema: "z.string().min(1)" }
|
|
385
730
|
]
|
|
731
|
+
},
|
|
732
|
+
"rate-limiter": {
|
|
733
|
+
envVars: {
|
|
734
|
+
RATE_LIMIT_WINDOW_MS: "900000",
|
|
735
|
+
RATE_LIMIT_MAX: "100"
|
|
736
|
+
},
|
|
737
|
+
schemaFields: [
|
|
738
|
+
{ name: "RATE_LIMIT_WINDOW_MS", schema: "z.coerce.number().default(900000)" },
|
|
739
|
+
{ name: "RATE_LIMIT_MAX", schema: "z.coerce.number().default(100)" }
|
|
740
|
+
]
|
|
386
741
|
}
|
|
387
742
|
};
|
|
388
743
|
|
|
@@ -496,29 +851,85 @@ bun.lockb
|
|
|
496
851
|
await import_fs_extra4.default.writeFile(prettierIgnorePath, ignoreContent);
|
|
497
852
|
}
|
|
498
853
|
}
|
|
499
|
-
async function
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
const existingZuroConfig = await readZuroConfig(cwd);
|
|
503
|
-
if (isExistingProject && !existingZuroConfig) {
|
|
504
|
-
showNonZuroProjectMessage();
|
|
854
|
+
async function setupGitignore(targetDir) {
|
|
855
|
+
const gitignorePath = import_path4.default.join(targetDir, ".gitignore");
|
|
856
|
+
if (await import_fs_extra4.default.pathExists(gitignorePath)) {
|
|
505
857
|
return;
|
|
506
858
|
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
859
|
+
const gitignoreContent = `# dependencies
|
|
860
|
+
node_modules
|
|
861
|
+
|
|
862
|
+
# build output
|
|
863
|
+
dist
|
|
864
|
+
build
|
|
865
|
+
|
|
866
|
+
# environment variables
|
|
867
|
+
.env
|
|
868
|
+
.env.*
|
|
869
|
+
!.env.example
|
|
870
|
+
|
|
871
|
+
# logs
|
|
872
|
+
*.log
|
|
873
|
+
npm-debug.log*
|
|
874
|
+
pnpm-debug.log*
|
|
875
|
+
|
|
876
|
+
# coverage
|
|
877
|
+
coverage
|
|
878
|
+
|
|
879
|
+
# OS files
|
|
880
|
+
.DS_Store
|
|
881
|
+
Thumbs.db
|
|
882
|
+
|
|
883
|
+
# IDE
|
|
884
|
+
.vscode
|
|
885
|
+
.idea
|
|
886
|
+
*.swp
|
|
887
|
+
*.swo
|
|
888
|
+
`;
|
|
889
|
+
await import_fs_extra4.default.writeFile(gitignorePath, gitignoreContent);
|
|
890
|
+
}
|
|
891
|
+
function tryGitInit(targetDir) {
|
|
892
|
+
try {
|
|
893
|
+
(0, import_child_process.execSync)("git rev-parse --is-inside-work-tree", {
|
|
894
|
+
cwd: targetDir,
|
|
895
|
+
stdio: "ignore"
|
|
896
|
+
});
|
|
897
|
+
return;
|
|
898
|
+
} catch {
|
|
899
|
+
}
|
|
900
|
+
try {
|
|
901
|
+
(0, import_child_process.execSync)("git init", { cwd: targetDir, stdio: "ignore" });
|
|
902
|
+
(0, import_child_process.execSync)("git add -A", { cwd: targetDir, stdio: "ignore" });
|
|
903
|
+
(0, import_child_process.execSync)('git commit -m "Initial commit from zuro-cli"', {
|
|
904
|
+
cwd: targetDir,
|
|
905
|
+
stdio: "ignore"
|
|
906
|
+
});
|
|
907
|
+
} catch {
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
async function init() {
|
|
911
|
+
const cwd = process.cwd();
|
|
912
|
+
const isExistingProject = await import_fs_extra4.default.pathExists(import_path4.default.join(cwd, "package.json"));
|
|
913
|
+
const existingZuroConfig = await readZuroConfig(cwd);
|
|
914
|
+
if (isExistingProject && !existingZuroConfig) {
|
|
915
|
+
showNonZuroProjectMessage();
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
let targetDir = cwd;
|
|
919
|
+
let pm = "npm";
|
|
920
|
+
let srcDir = "src";
|
|
921
|
+
let projectName = import_path4.default.basename(cwd);
|
|
922
|
+
let enablePrettier = false;
|
|
923
|
+
if (isExistingProject) {
|
|
924
|
+
console.log(import_chalk2.default.blue("\u2139 Existing project detected."));
|
|
925
|
+
projectName = import_path4.default.basename(cwd);
|
|
926
|
+
if (await import_fs_extra4.default.pathExists(import_path4.default.join(cwd, "pnpm-lock.yaml"))) {
|
|
927
|
+
pm = "pnpm";
|
|
928
|
+
} else if (await import_fs_extra4.default.pathExists(import_path4.default.join(cwd, "bun.lockb"))) {
|
|
929
|
+
pm = "bun";
|
|
930
|
+
} else if (await import_fs_extra4.default.pathExists(import_path4.default.join(cwd, "yarn.lock"))) {
|
|
931
|
+
pm = "yarn";
|
|
932
|
+
}
|
|
522
933
|
const response = await (0, import_prompts.default)({
|
|
523
934
|
type: "text",
|
|
524
935
|
name: "srcDir",
|
|
@@ -647,8 +1058,16 @@ async function init() {
|
|
|
647
1058
|
currentStep = "prettier setup";
|
|
648
1059
|
await setupPrettier(targetDir);
|
|
649
1060
|
}
|
|
1061
|
+
currentStep = "gitignore setup";
|
|
1062
|
+
spinner.text = "Setting up .gitignore...";
|
|
1063
|
+
await setupGitignore(targetDir);
|
|
650
1064
|
currentStep = "config write";
|
|
651
1065
|
await writeZuroConfig(targetDir, zuroConfig);
|
|
1066
|
+
if (!isExistingProject) {
|
|
1067
|
+
currentStep = "git init";
|
|
1068
|
+
spinner.text = "Initializing git repository...";
|
|
1069
|
+
tryGitInit(targetDir);
|
|
1070
|
+
}
|
|
652
1071
|
spinner.succeed(import_chalk2.default.green("Project initialized successfully!"));
|
|
653
1072
|
console.log(`
|
|
654
1073
|
${import_chalk2.default.bold("Next steps:")}`);
|
|
@@ -672,10 +1091,9 @@ ${import_chalk2.default.bold("Retry:")}`);
|
|
|
672
1091
|
}
|
|
673
1092
|
|
|
674
1093
|
// src/commands/add.ts
|
|
675
|
-
var import_prompts2 = __toESM(require("prompts"));
|
|
676
1094
|
var import_ora2 = __toESM(require("ora"));
|
|
677
|
-
var
|
|
678
|
-
var
|
|
1095
|
+
var import_path12 = __toESM(require("path"));
|
|
1096
|
+
var import_fs_extra11 = __toESM(require("fs-extra"));
|
|
679
1097
|
var import_node_crypto2 = require("crypto");
|
|
680
1098
|
|
|
681
1099
|
// src/utils/dependency.ts
|
|
@@ -691,7 +1109,8 @@ var BLOCK_SIGNATURES = {
|
|
|
691
1109
|
logger: "lib/logger.ts",
|
|
692
1110
|
auth: "lib/auth.ts",
|
|
693
1111
|
mailer: "lib/mailer.ts",
|
|
694
|
-
docs: "lib/openapi.ts"
|
|
1112
|
+
docs: "lib/openapi.ts",
|
|
1113
|
+
"rate-limiter": "middleware/rate-limiter.ts"
|
|
695
1114
|
};
|
|
696
1115
|
var resolveDependencies = async (moduleDependencies, cwd) => {
|
|
697
1116
|
if (!moduleDependencies || moduleDependencies.length === 0) {
|
|
@@ -722,12 +1141,8 @@ var resolveDependencies = async (moduleDependencies, cwd) => {
|
|
|
722
1141
|
}
|
|
723
1142
|
};
|
|
724
1143
|
|
|
725
|
-
// src/
|
|
726
|
-
var
|
|
727
|
-
var DEFAULT_DATABASE_URLS = {
|
|
728
|
-
"database-pg": "postgresql://postgres@localhost:5432/mydb",
|
|
729
|
-
"database-mysql": "mysql://root@localhost:3306/mydb"
|
|
730
|
-
};
|
|
1144
|
+
// src/utils/paths.ts
|
|
1145
|
+
var import_path6 = __toESM(require("path"));
|
|
731
1146
|
function resolveSafeTargetPath2(projectRoot, srcDir, file) {
|
|
732
1147
|
const targetPath = import_path6.default.resolve(projectRoot, srcDir, file.target);
|
|
733
1148
|
const normalizedRoot = import_path6.default.resolve(projectRoot);
|
|
@@ -736,231 +1151,27 @@ function resolveSafeTargetPath2(projectRoot, srcDir, file) {
|
|
|
736
1151
|
}
|
|
737
1152
|
return targetPath;
|
|
738
1153
|
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
function parseDatabaseDialect(value) {
|
|
752
|
-
const normalized = value?.trim().toLowerCase();
|
|
753
|
-
if (!normalized) {
|
|
754
|
-
return null;
|
|
755
|
-
}
|
|
756
|
-
if (normalized === "pg" || normalized === "postgres" || normalized === "postgresql" || normalized === "database-pg") {
|
|
757
|
-
return "database-pg";
|
|
758
|
-
}
|
|
759
|
-
if (normalized === "mysql" || normalized === "database-mysql") {
|
|
760
|
-
return "database-mysql";
|
|
761
|
-
}
|
|
762
|
-
return null;
|
|
763
|
-
}
|
|
764
|
-
function isDatabaseModule(moduleName) {
|
|
765
|
-
return moduleName === "database-pg" || moduleName === "database-mysql";
|
|
766
|
-
}
|
|
767
|
-
function validateDatabaseUrl(rawUrl, moduleName) {
|
|
768
|
-
const dbUrl = rawUrl.trim();
|
|
769
|
-
if (!dbUrl) {
|
|
770
|
-
throw new Error("Database URL cannot be empty.");
|
|
771
|
-
}
|
|
772
|
-
let parsed;
|
|
773
|
-
try {
|
|
774
|
-
parsed = new URL(dbUrl);
|
|
775
|
-
} catch {
|
|
776
|
-
throw new Error(`Invalid database URL: '${dbUrl}'.`);
|
|
777
|
-
}
|
|
778
|
-
const protocol = parsed.protocol.toLowerCase();
|
|
779
|
-
if (moduleName === "database-pg" && protocol !== "postgresql:" && protocol !== "postgres:") {
|
|
780
|
-
throw new Error("PostgreSQL URL must start with postgres:// or postgresql://");
|
|
781
|
-
}
|
|
782
|
-
if (moduleName === "database-mysql" && protocol !== "mysql:") {
|
|
783
|
-
throw new Error("MySQL URL must start with mysql://");
|
|
784
|
-
}
|
|
785
|
-
return dbUrl;
|
|
786
|
-
}
|
|
787
|
-
async function detectInstalledDatabaseDialect(projectRoot, srcDir) {
|
|
788
|
-
const dbIndexPath = import_path6.default.join(projectRoot, srcDir, "db", "index.ts");
|
|
789
|
-
if (!import_fs_extra6.default.existsSync(dbIndexPath)) {
|
|
790
|
-
return null;
|
|
791
|
-
}
|
|
792
|
-
const content = await import_fs_extra6.default.readFile(dbIndexPath, "utf-8");
|
|
793
|
-
if (content.includes("drizzle-orm/node-postgres") || content.includes(`from "pg"`)) {
|
|
794
|
-
return "database-pg";
|
|
795
|
-
}
|
|
796
|
-
if (content.includes("drizzle-orm/mysql2") || content.includes(`from "mysql2`)) {
|
|
797
|
-
return "database-mysql";
|
|
798
|
-
}
|
|
799
|
-
return null;
|
|
800
|
-
}
|
|
801
|
-
async function backupDatabaseFiles(projectRoot, srcDir) {
|
|
802
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
803
|
-
const backupRoot = import_path6.default.join(projectRoot, ".zuro", "backups", `database-${timestamp}`);
|
|
804
|
-
const candidates = [
|
|
805
|
-
import_path6.default.join(projectRoot, srcDir, "db", "index.ts"),
|
|
806
|
-
import_path6.default.join(projectRoot, "drizzle.config.ts")
|
|
807
|
-
];
|
|
808
|
-
let copied = false;
|
|
809
|
-
for (const filePath of candidates) {
|
|
810
|
-
if (!import_fs_extra6.default.existsSync(filePath)) {
|
|
811
|
-
continue;
|
|
812
|
-
}
|
|
813
|
-
const relativePath = import_path6.default.relative(projectRoot, filePath);
|
|
814
|
-
const backupPath = import_path6.default.join(backupRoot, relativePath);
|
|
815
|
-
await import_fs_extra6.default.ensureDir(import_path6.default.dirname(backupPath));
|
|
816
|
-
await import_fs_extra6.default.copyFile(filePath, backupPath);
|
|
817
|
-
copied = true;
|
|
818
|
-
}
|
|
819
|
-
return copied ? backupRoot : null;
|
|
820
|
-
}
|
|
821
|
-
function databaseLabel(moduleName) {
|
|
822
|
-
return moduleName === "database-pg" ? "PostgreSQL" : "MySQL";
|
|
823
|
-
}
|
|
824
|
-
function getDatabaseSetupHint(moduleName, dbUrl) {
|
|
825
|
-
try {
|
|
826
|
-
const parsed = new URL(dbUrl);
|
|
827
|
-
const dbName = parsed.pathname.replace(/^\/+/, "") || "mydb";
|
|
828
|
-
if (moduleName === "database-pg") {
|
|
829
|
-
return `createdb ${dbName}`;
|
|
830
|
-
}
|
|
831
|
-
return `mysql -e "CREATE DATABASE IF NOT EXISTS ${dbName};"`;
|
|
832
|
-
} catch {
|
|
833
|
-
return moduleName === "database-pg" ? "createdb <database_name>" : `mysql -e "CREATE DATABASE IF NOT EXISTS <database_name>;"`;
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
function getModuleDocsPath(moduleName) {
|
|
837
|
-
if (isDatabaseModule(moduleName)) {
|
|
838
|
-
return "database";
|
|
839
|
-
}
|
|
840
|
-
return moduleName;
|
|
841
|
-
}
|
|
842
|
-
function escapeRegex(value) {
|
|
843
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
844
|
-
}
|
|
845
|
-
function appendImport(source, line) {
|
|
846
|
-
if (source.includes(line)) {
|
|
847
|
-
return { source, inserted: true };
|
|
848
|
-
}
|
|
849
|
-
const importRegex = /^import .+ from .+;?\s*$/gm;
|
|
850
|
-
let lastImportIndex = 0;
|
|
851
|
-
let match;
|
|
852
|
-
while ((match = importRegex.exec(source)) !== null) {
|
|
853
|
-
lastImportIndex = match.index + match[0].length;
|
|
854
|
-
}
|
|
855
|
-
if (lastImportIndex <= 0) {
|
|
856
|
-
return { source, inserted: false };
|
|
857
|
-
}
|
|
858
|
-
return {
|
|
859
|
-
source: source.slice(0, lastImportIndex) + `
|
|
860
|
-
${line}` + source.slice(lastImportIndex),
|
|
861
|
-
inserted: true
|
|
862
|
-
};
|
|
863
|
-
}
|
|
864
|
-
async function hasEnvVariable(projectRoot, key) {
|
|
865
|
-
const envPath = import_path6.default.join(projectRoot, ".env");
|
|
866
|
-
if (!await import_fs_extra6.default.pathExists(envPath)) {
|
|
867
|
-
return false;
|
|
868
|
-
}
|
|
869
|
-
const content = await import_fs_extra6.default.readFile(envPath, "utf-8");
|
|
870
|
-
const pattern = new RegExp(`^${escapeRegex(key)}=`, "m");
|
|
871
|
-
return pattern.test(content);
|
|
872
|
-
}
|
|
873
|
-
async function isLikelyEmptyDirectory(cwd) {
|
|
874
|
-
const entries = await import_fs_extra6.default.readdir(cwd);
|
|
875
|
-
const ignored = /* @__PURE__ */ new Set([".ds_store", "thumbs.db"]);
|
|
876
|
-
return entries.filter((entry) => !ignored.has(entry.toLowerCase())).length === 0;
|
|
877
|
-
}
|
|
878
|
-
async function ensureSchemaExport(projectRoot, srcDir, schemaFileName) {
|
|
879
|
-
const schemaIndexPath = import_path6.default.join(projectRoot, srcDir, "db", "schema", "index.ts");
|
|
880
|
-
if (!await import_fs_extra6.default.pathExists(schemaIndexPath)) {
|
|
881
|
-
return;
|
|
882
|
-
}
|
|
883
|
-
const exportLine = `export * from "./${schemaFileName}";`;
|
|
884
|
-
const content = await import_fs_extra6.default.readFile(schemaIndexPath, "utf-8");
|
|
885
|
-
const normalized = content.replace(/\r\n/g, "\n");
|
|
886
|
-
const exportPattern = new RegExp(
|
|
887
|
-
`^\\s*export\\s*\\*\\s*from\\s*["']\\./${escapeRegex(schemaFileName)}["'];?\\s*$`,
|
|
888
|
-
"m"
|
|
889
|
-
);
|
|
890
|
-
if (exportPattern.test(normalized)) {
|
|
891
|
-
return;
|
|
892
|
-
}
|
|
893
|
-
let next = normalized.replace(/^\s*export\s*\{\s*\};?\s*$/m, "").trimEnd();
|
|
894
|
-
if (next.length > 0) {
|
|
895
|
-
next += "\n\n";
|
|
896
|
-
}
|
|
897
|
-
next += `${exportLine}
|
|
898
|
-
`;
|
|
899
|
-
await import_fs_extra6.default.writeFile(schemaIndexPath, next);
|
|
900
|
-
}
|
|
901
|
-
async function isDocsModuleInstalled(projectRoot, srcDir) {
|
|
902
|
-
return await import_fs_extra6.default.pathExists(import_path6.default.join(projectRoot, srcDir, "lib", "openapi.ts"));
|
|
903
|
-
}
|
|
1154
|
+
|
|
1155
|
+
// src/commands/add.ts
|
|
1156
|
+
init_code_inject();
|
|
1157
|
+
var import_chalk7 = __toESM(require("chalk"));
|
|
1158
|
+
init_database_handler();
|
|
1159
|
+
|
|
1160
|
+
// src/handlers/auth.handler.ts
|
|
1161
|
+
var import_path9 = __toESM(require("path"));
|
|
1162
|
+
var import_fs_extra8 = __toESM(require("fs-extra"));
|
|
1163
|
+
var import_prompts3 = __toESM(require("prompts"));
|
|
1164
|
+
var import_chalk5 = __toESM(require("chalk"));
|
|
1165
|
+
init_code_inject();
|
|
904
1166
|
async function isAuthModuleInstalled(projectRoot, srcDir) {
|
|
905
|
-
return await
|
|
906
|
-
}
|
|
907
|
-
async function injectErrorHandler(projectRoot, srcDir) {
|
|
908
|
-
const appPath = import_path6.default.join(projectRoot, srcDir, "app.ts");
|
|
909
|
-
if (!import_fs_extra6.default.existsSync(appPath)) {
|
|
910
|
-
return false;
|
|
911
|
-
}
|
|
912
|
-
let content = await import_fs_extra6.default.readFile(appPath, "utf-8");
|
|
913
|
-
const errorImport = `import { errorHandler, notFoundHandler } from "./middleware/error-handler";`;
|
|
914
|
-
const hasErrorImport = content.includes(errorImport);
|
|
915
|
-
const hasNotFoundUse = /app\.use\(\s*notFoundHandler\s*\)/.test(content);
|
|
916
|
-
const hasErrorUse = /app\.use\(\s*errorHandler\s*\)/.test(content);
|
|
917
|
-
let modified = false;
|
|
918
|
-
let importInserted = hasErrorImport;
|
|
919
|
-
if (!hasErrorImport) {
|
|
920
|
-
const importRegex = /^import .+ from .+;?\s*$/gm;
|
|
921
|
-
let lastImportIndex = 0;
|
|
922
|
-
let match;
|
|
923
|
-
while ((match = importRegex.exec(content)) !== null) {
|
|
924
|
-
lastImportIndex = match.index + match[0].length;
|
|
925
|
-
}
|
|
926
|
-
if (lastImportIndex > 0) {
|
|
927
|
-
content = content.slice(0, lastImportIndex) + `
|
|
928
|
-
${errorImport}` + content.slice(lastImportIndex);
|
|
929
|
-
modified = true;
|
|
930
|
-
importInserted = true;
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
let setupInserted = hasNotFoundUse && hasErrorUse;
|
|
934
|
-
if (!setupInserted) {
|
|
935
|
-
const setupLines = [];
|
|
936
|
-
if (!hasNotFoundUse) {
|
|
937
|
-
setupLines.push("app.use(notFoundHandler);");
|
|
938
|
-
}
|
|
939
|
-
if (!hasErrorUse) {
|
|
940
|
-
setupLines.push("app.use(errorHandler);");
|
|
941
|
-
}
|
|
942
|
-
const errorSetup = `
|
|
943
|
-
// Error handling (must be last)
|
|
944
|
-
${setupLines.join("\n")}
|
|
945
|
-
`;
|
|
946
|
-
const exportMatch = content.match(/export default app;?\s*$/m);
|
|
947
|
-
if (exportMatch && exportMatch.index !== void 0) {
|
|
948
|
-
content = content.slice(0, exportMatch.index) + errorSetup + "\n" + content.slice(exportMatch.index);
|
|
949
|
-
modified = true;
|
|
950
|
-
setupInserted = true;
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
if (modified) {
|
|
954
|
-
await import_fs_extra6.default.writeFile(appPath, content);
|
|
955
|
-
}
|
|
956
|
-
return importInserted && setupInserted;
|
|
1167
|
+
return await import_fs_extra8.default.pathExists(import_path9.default.join(projectRoot, srcDir, "lib", "auth.ts"));
|
|
957
1168
|
}
|
|
958
1169
|
async function injectAuthRoutes(projectRoot, srcDir) {
|
|
959
|
-
const appPath =
|
|
960
|
-
if (!await
|
|
1170
|
+
const appPath = import_path9.default.join(projectRoot, srcDir, "app.ts");
|
|
1171
|
+
if (!await import_fs_extra8.default.pathExists(appPath)) {
|
|
961
1172
|
return false;
|
|
962
1173
|
}
|
|
963
|
-
let appContent = await
|
|
1174
|
+
let appContent = await import_fs_extra8.default.readFile(appPath, "utf-8");
|
|
964
1175
|
const authHandlerImport = `import { toNodeHandler } from "better-auth/node";`;
|
|
965
1176
|
const authImport = `import { auth } from "./lib/auth";`;
|
|
966
1177
|
const routeIndexUserImport = `import userRoutes from "./user.routes";`;
|
|
@@ -995,9 +1206,9 @@ async function injectAuthRoutes(projectRoot, srcDir) {
|
|
|
995
1206
|
appContent = appContent.slice(0, insertionIndex) + authMountLine + appContent.slice(insertionIndex);
|
|
996
1207
|
appModified = true;
|
|
997
1208
|
}
|
|
998
|
-
const routeIndexPath =
|
|
999
|
-
if (await
|
|
1000
|
-
let routeContent = await
|
|
1209
|
+
const routeIndexPath = import_path9.default.join(projectRoot, srcDir, "routes", "index.ts");
|
|
1210
|
+
if (await import_fs_extra8.default.pathExists(routeIndexPath)) {
|
|
1211
|
+
let routeContent = await import_fs_extra8.default.readFile(routeIndexPath, "utf-8");
|
|
1001
1212
|
let routeModified = false;
|
|
1002
1213
|
const userImportResult = appendImport(routeContent, routeIndexUserImport);
|
|
1003
1214
|
if (!userImportResult.inserted) {
|
|
@@ -1021,7 +1232,7 @@ rootRouter.use("/users", userRoutes);
|
|
|
1021
1232
|
routeModified = true;
|
|
1022
1233
|
}
|
|
1023
1234
|
if (routeModified) {
|
|
1024
|
-
await
|
|
1235
|
+
await import_fs_extra8.default.writeFile(routeIndexPath, routeContent);
|
|
1025
1236
|
}
|
|
1026
1237
|
} else {
|
|
1027
1238
|
const hasUserRoute = /app\.use\(\s*["']\/api\/users["']\s*,\s*userRoutes\s*\)/.test(appContent);
|
|
@@ -1047,81 +1258,17 @@ app.use("/api/users", userRoutes);
|
|
|
1047
1258
|
}
|
|
1048
1259
|
}
|
|
1049
1260
|
if (appModified) {
|
|
1050
|
-
await
|
|
1051
|
-
}
|
|
1052
|
-
return true;
|
|
1053
|
-
}
|
|
1054
|
-
async function injectDocsRoutes(projectRoot, srcDir) {
|
|
1055
|
-
const routeIndexPath = import_path6.default.join(projectRoot, srcDir, "routes", "index.ts");
|
|
1056
|
-
const routeImport = `import docsRoutes from "./docs.routes";`;
|
|
1057
|
-
const routeMountPattern = /rootRouter\.use\(\s*["']\/docs["']\s*,\s*docsRoutes\s*\)/;
|
|
1058
|
-
if (await import_fs_extra6.default.pathExists(routeIndexPath)) {
|
|
1059
|
-
let routeContent = await import_fs_extra6.default.readFile(routeIndexPath, "utf-8");
|
|
1060
|
-
let routeModified = false;
|
|
1061
|
-
const importResult = appendImport(routeContent, routeImport);
|
|
1062
|
-
if (!importResult.inserted) {
|
|
1063
|
-
return false;
|
|
1064
|
-
}
|
|
1065
|
-
if (importResult.source !== routeContent) {
|
|
1066
|
-
routeContent = importResult.source;
|
|
1067
|
-
routeModified = true;
|
|
1068
|
-
}
|
|
1069
|
-
if (!routeMountPattern.test(routeContent)) {
|
|
1070
|
-
const routeSetup = `
|
|
1071
|
-
// API docs
|
|
1072
|
-
rootRouter.use("/docs", docsRoutes);
|
|
1073
|
-
`;
|
|
1074
|
-
const exportMatch = routeContent.match(/export default rootRouter;?\s*$/m);
|
|
1075
|
-
if (!exportMatch || exportMatch.index === void 0) {
|
|
1076
|
-
return false;
|
|
1077
|
-
}
|
|
1078
|
-
routeContent = routeContent.slice(0, exportMatch.index) + routeSetup + "\n" + routeContent.slice(exportMatch.index);
|
|
1079
|
-
routeModified = true;
|
|
1080
|
-
}
|
|
1081
|
-
if (routeModified) {
|
|
1082
|
-
await import_fs_extra6.default.writeFile(routeIndexPath, routeContent);
|
|
1083
|
-
}
|
|
1084
|
-
return true;
|
|
1085
|
-
}
|
|
1086
|
-
const appPath = import_path6.default.join(projectRoot, srcDir, "app.ts");
|
|
1087
|
-
if (!await import_fs_extra6.default.pathExists(appPath)) {
|
|
1088
|
-
return false;
|
|
1089
|
-
}
|
|
1090
|
-
let appContent = await import_fs_extra6.default.readFile(appPath, "utf-8");
|
|
1091
|
-
let appModified = false;
|
|
1092
|
-
const appImportResult = appendImport(appContent, `import docsRoutes from "./routes/docs.routes";`);
|
|
1093
|
-
if (!appImportResult.inserted) {
|
|
1094
|
-
return false;
|
|
1095
|
-
}
|
|
1096
|
-
if (appImportResult.source !== appContent) {
|
|
1097
|
-
appContent = appImportResult.source;
|
|
1098
|
-
appModified = true;
|
|
1099
|
-
}
|
|
1100
|
-
const hasMount = /app\.use\(\s*["']\/api\/docs["']\s*,\s*docsRoutes\s*\)/.test(appContent);
|
|
1101
|
-
if (!hasMount) {
|
|
1102
|
-
const setup = `
|
|
1103
|
-
// API docs
|
|
1104
|
-
app.use("/api/docs", docsRoutes);
|
|
1105
|
-
`;
|
|
1106
|
-
const exportMatch = appContent.match(/export default app;?\s*$/m);
|
|
1107
|
-
if (!exportMatch || exportMatch.index === void 0) {
|
|
1108
|
-
return false;
|
|
1109
|
-
}
|
|
1110
|
-
appContent = appContent.slice(0, exportMatch.index) + setup + "\n" + appContent.slice(exportMatch.index);
|
|
1111
|
-
appModified = true;
|
|
1112
|
-
}
|
|
1113
|
-
if (appModified) {
|
|
1114
|
-
await import_fs_extra6.default.writeFile(appPath, appContent);
|
|
1261
|
+
await import_fs_extra8.default.writeFile(appPath, appContent);
|
|
1115
1262
|
}
|
|
1116
1263
|
return true;
|
|
1117
1264
|
}
|
|
1118
1265
|
async function injectAuthDocs(projectRoot, srcDir) {
|
|
1119
|
-
const openApiPath =
|
|
1120
|
-
if (!await
|
|
1266
|
+
const openApiPath = import_path9.default.join(projectRoot, srcDir, "lib", "openapi.ts");
|
|
1267
|
+
if (!await import_fs_extra8.default.pathExists(openApiPath)) {
|
|
1121
1268
|
return false;
|
|
1122
1269
|
}
|
|
1123
1270
|
const authMarker = "// ZURO_AUTH_DOCS";
|
|
1124
|
-
let content = await
|
|
1271
|
+
let content = await import_fs_extra8.default.readFile(openApiPath, "utf-8");
|
|
1125
1272
|
if (content.includes(authMarker)) {
|
|
1126
1273
|
return true;
|
|
1127
1274
|
}
|
|
@@ -1218,9 +1365,298 @@ registry.registerPath({
|
|
|
1218
1365
|
`;
|
|
1219
1366
|
content = content.replace(moduleDocsEndMarker, `${authBlock}
|
|
1220
1367
|
${moduleDocsEndMarker}`);
|
|
1221
|
-
await
|
|
1368
|
+
await import_fs_extra8.default.writeFile(openApiPath, content);
|
|
1369
|
+
return true;
|
|
1370
|
+
}
|
|
1371
|
+
async function promptAuthConfig(projectRoot, srcDir, options) {
|
|
1372
|
+
const { isDocsModuleInstalled: isDocsModuleInstalled2 } = await Promise.resolve().then(() => (init_docs_handler(), docs_handler_exports));
|
|
1373
|
+
const docsInstalled = await isDocsModuleInstalled2(projectRoot, srcDir);
|
|
1374
|
+
let shouldInstallDocsForAuth = false;
|
|
1375
|
+
if (!docsInstalled) {
|
|
1376
|
+
if (options.yes) {
|
|
1377
|
+
shouldInstallDocsForAuth = true;
|
|
1378
|
+
} else {
|
|
1379
|
+
const docsResponse = await (0, import_prompts3.default)({
|
|
1380
|
+
type: "confirm",
|
|
1381
|
+
name: "installDocs",
|
|
1382
|
+
message: "Install API docs module (Scalar + OpenAPI) too?",
|
|
1383
|
+
initial: true
|
|
1384
|
+
});
|
|
1385
|
+
if (docsResponse.installDocs === void 0) {
|
|
1386
|
+
console.log(import_chalk5.default.yellow("Operation cancelled."));
|
|
1387
|
+
return null;
|
|
1388
|
+
}
|
|
1389
|
+
shouldInstallDocsForAuth = docsResponse.installDocs;
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
const { detectInstalledDatabaseDialect: detectInstalledDatabaseDialect2 } = await Promise.resolve().then(() => (init_database_handler(), database_handler_exports));
|
|
1393
|
+
const authDatabaseDialect = await detectInstalledDatabaseDialect2(projectRoot, srcDir);
|
|
1394
|
+
return { shouldInstallDocsForAuth, authDatabaseDialect };
|
|
1395
|
+
}
|
|
1396
|
+
function printAuthHints(generatedAuthSecret) {
|
|
1397
|
+
if (generatedAuthSecret) {
|
|
1398
|
+
console.log(import_chalk5.default.yellow("\u2139 BETTER_AUTH_SECRET was generated automatically."));
|
|
1399
|
+
} else {
|
|
1400
|
+
console.log(import_chalk5.default.yellow("\u2139 Review BETTER_AUTH_SECRET and BETTER_AUTH_URL in .env."));
|
|
1401
|
+
}
|
|
1402
|
+
console.log(import_chalk5.default.yellow("\u2139 Run migrations: npx drizzle-kit generate && npx drizzle-kit migrate"));
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
// src/handlers/mailer.handler.ts
|
|
1406
|
+
var import_prompts4 = __toESM(require("prompts"));
|
|
1407
|
+
var import_chalk6 = __toESM(require("chalk"));
|
|
1408
|
+
async function promptMailerConfig() {
|
|
1409
|
+
const providerResponse = await (0, import_prompts4.default)({
|
|
1410
|
+
type: "select",
|
|
1411
|
+
name: "provider",
|
|
1412
|
+
message: "Which email provider?",
|
|
1413
|
+
choices: [
|
|
1414
|
+
{ title: "SMTP (Nodemailer)", description: "Gmail, Mailtrap, any SMTP server", value: "smtp" },
|
|
1415
|
+
{ title: "Resend", description: "API-based, easiest setup", value: "resend" }
|
|
1416
|
+
]
|
|
1417
|
+
});
|
|
1418
|
+
if (providerResponse.provider === void 0) {
|
|
1419
|
+
console.log(import_chalk6.default.yellow("Operation cancelled."));
|
|
1420
|
+
return null;
|
|
1421
|
+
}
|
|
1422
|
+
const mailerProvider = providerResponse.provider;
|
|
1423
|
+
let customSmtpVars;
|
|
1424
|
+
let usedDefaultSmtp = false;
|
|
1425
|
+
console.log(import_chalk6.default.dim(" Tip: Leave fields blank to use placeholder values and configure later\n"));
|
|
1426
|
+
if (mailerProvider === "smtp") {
|
|
1427
|
+
const smtpResponse = await (0, import_prompts4.default)([
|
|
1428
|
+
{
|
|
1429
|
+
type: "text",
|
|
1430
|
+
name: "host",
|
|
1431
|
+
message: "SMTP Host",
|
|
1432
|
+
initial: ""
|
|
1433
|
+
},
|
|
1434
|
+
{
|
|
1435
|
+
type: "text",
|
|
1436
|
+
name: "port",
|
|
1437
|
+
message: "SMTP Port",
|
|
1438
|
+
initial: "587"
|
|
1439
|
+
},
|
|
1440
|
+
{
|
|
1441
|
+
type: "text",
|
|
1442
|
+
name: "user",
|
|
1443
|
+
message: "SMTP User",
|
|
1444
|
+
initial: ""
|
|
1445
|
+
},
|
|
1446
|
+
{
|
|
1447
|
+
type: "password",
|
|
1448
|
+
name: "pass",
|
|
1449
|
+
message: "SMTP Password"
|
|
1450
|
+
},
|
|
1451
|
+
{
|
|
1452
|
+
type: "text",
|
|
1453
|
+
name: "from",
|
|
1454
|
+
message: "Mail From address",
|
|
1455
|
+
initial: ""
|
|
1456
|
+
}
|
|
1457
|
+
]);
|
|
1458
|
+
if (smtpResponse.host === void 0) {
|
|
1459
|
+
console.log(import_chalk6.default.yellow("Operation cancelled."));
|
|
1460
|
+
return null;
|
|
1461
|
+
}
|
|
1462
|
+
const host = smtpResponse.host?.trim() || "";
|
|
1463
|
+
const user = smtpResponse.user?.trim() || "";
|
|
1464
|
+
const pass = smtpResponse.pass?.trim() || "";
|
|
1465
|
+
const from = smtpResponse.from?.trim() || "";
|
|
1466
|
+
const port = smtpResponse.port?.trim() || "587";
|
|
1467
|
+
usedDefaultSmtp = !host && !user;
|
|
1468
|
+
if (!usedDefaultSmtp) {
|
|
1469
|
+
customSmtpVars = {
|
|
1470
|
+
SMTP_HOST: host || "smtp.example.com",
|
|
1471
|
+
SMTP_PORT: port,
|
|
1472
|
+
SMTP_USER: user || "your-email@example.com",
|
|
1473
|
+
SMTP_PASS: pass || "your-password",
|
|
1474
|
+
MAIL_FROM: from || "noreply@example.com"
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
} else {
|
|
1478
|
+
const resendResponse = await (0, import_prompts4.default)([
|
|
1479
|
+
{
|
|
1480
|
+
type: "text",
|
|
1481
|
+
name: "apiKey",
|
|
1482
|
+
message: "Resend API Key",
|
|
1483
|
+
initial: ""
|
|
1484
|
+
},
|
|
1485
|
+
{
|
|
1486
|
+
type: "text",
|
|
1487
|
+
name: "from",
|
|
1488
|
+
message: "Mail From address",
|
|
1489
|
+
initial: ""
|
|
1490
|
+
}
|
|
1491
|
+
]);
|
|
1492
|
+
if (resendResponse.apiKey === void 0) {
|
|
1493
|
+
console.log(import_chalk6.default.yellow("Operation cancelled."));
|
|
1494
|
+
return null;
|
|
1495
|
+
}
|
|
1496
|
+
const apiKey = resendResponse.apiKey?.trim() || "";
|
|
1497
|
+
const from = resendResponse.from?.trim() || "";
|
|
1498
|
+
usedDefaultSmtp = !apiKey;
|
|
1499
|
+
if (!usedDefaultSmtp) {
|
|
1500
|
+
customSmtpVars = {
|
|
1501
|
+
RESEND_API_KEY: apiKey || "re_your_api_key",
|
|
1502
|
+
MAIL_FROM: from || "onboarding@resend.dev"
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
return { mailerProvider, customSmtpVars, usedDefaultSmtp };
|
|
1507
|
+
}
|
|
1508
|
+
function printMailerHints(usedDefaultSmtp) {
|
|
1509
|
+
if (usedDefaultSmtp) {
|
|
1510
|
+
console.log(import_chalk6.default.yellow("\u2139 Placeholder SMTP values added to .env \u2014 update them before sending emails."));
|
|
1511
|
+
} else {
|
|
1512
|
+
console.log(import_chalk6.default.yellow("\u2139 Review SMTP configuration in .env to ensure values are correct."));
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
// src/commands/add.ts
|
|
1517
|
+
init_docs_handler();
|
|
1518
|
+
|
|
1519
|
+
// src/handlers/error-handler.handler.ts
|
|
1520
|
+
var import_path10 = __toESM(require("path"));
|
|
1521
|
+
var import_fs_extra9 = __toESM(require("fs-extra"));
|
|
1522
|
+
async function injectErrorHandler(projectRoot, srcDir) {
|
|
1523
|
+
const appPath = import_path10.default.join(projectRoot, srcDir, "app.ts");
|
|
1524
|
+
if (!import_fs_extra9.default.existsSync(appPath)) {
|
|
1525
|
+
return false;
|
|
1526
|
+
}
|
|
1527
|
+
let content = await import_fs_extra9.default.readFile(appPath, "utf-8");
|
|
1528
|
+
const errorImport = `import { errorHandler, notFoundHandler } from "./middleware/error-handler";`;
|
|
1529
|
+
const hasErrorImport = content.includes(errorImport);
|
|
1530
|
+
const hasNotFoundUse = /app\.use\(\s*notFoundHandler\s*\)/.test(content);
|
|
1531
|
+
const hasErrorUse = /app\.use\(\s*errorHandler\s*\)/.test(content);
|
|
1532
|
+
let modified = false;
|
|
1533
|
+
let importInserted = hasErrorImport;
|
|
1534
|
+
if (!hasErrorImport) {
|
|
1535
|
+
const importRegex = /^import .+ from .+;?\s*$/gm;
|
|
1536
|
+
let lastImportIndex = 0;
|
|
1537
|
+
let match;
|
|
1538
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
1539
|
+
lastImportIndex = match.index + match[0].length;
|
|
1540
|
+
}
|
|
1541
|
+
if (lastImportIndex > 0) {
|
|
1542
|
+
content = content.slice(0, lastImportIndex) + `
|
|
1543
|
+
${errorImport}` + content.slice(lastImportIndex);
|
|
1544
|
+
modified = true;
|
|
1545
|
+
importInserted = true;
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
let setupInserted = hasNotFoundUse && hasErrorUse;
|
|
1549
|
+
if (!setupInserted) {
|
|
1550
|
+
const setupLines = [];
|
|
1551
|
+
if (!hasNotFoundUse) {
|
|
1552
|
+
setupLines.push("app.use(notFoundHandler);");
|
|
1553
|
+
}
|
|
1554
|
+
if (!hasErrorUse) {
|
|
1555
|
+
setupLines.push("app.use(errorHandler);");
|
|
1556
|
+
}
|
|
1557
|
+
const errorSetup = `
|
|
1558
|
+
// Error handling (must be last)
|
|
1559
|
+
${setupLines.join("\n")}
|
|
1560
|
+
`;
|
|
1561
|
+
const exportMatch = content.match(/export default app;?\s*$/m);
|
|
1562
|
+
if (exportMatch && exportMatch.index !== void 0) {
|
|
1563
|
+
content = content.slice(0, exportMatch.index) + errorSetup + "\n" + content.slice(exportMatch.index);
|
|
1564
|
+
modified = true;
|
|
1565
|
+
setupInserted = true;
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
if (modified) {
|
|
1569
|
+
await import_fs_extra9.default.writeFile(appPath, content);
|
|
1570
|
+
}
|
|
1571
|
+
return importInserted && setupInserted;
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
// src/handlers/rate-limiter.handler.ts
|
|
1575
|
+
var import_path11 = __toESM(require("path"));
|
|
1576
|
+
var import_fs_extra10 = __toESM(require("fs-extra"));
|
|
1577
|
+
async function injectRateLimiter(projectRoot, srcDir) {
|
|
1578
|
+
const appPath = import_path11.default.join(projectRoot, srcDir, "app.ts");
|
|
1579
|
+
if (!import_fs_extra10.default.existsSync(appPath)) {
|
|
1580
|
+
return false;
|
|
1581
|
+
}
|
|
1582
|
+
let content = await import_fs_extra10.default.readFile(appPath, "utf-8");
|
|
1583
|
+
const rateLimiterImport = `import { rateLimiter } from "./middleware/rate-limiter";`;
|
|
1584
|
+
const hasImport = content.includes(rateLimiterImport);
|
|
1585
|
+
const hasUse = /app\.use\(\s*rateLimiter\s*\)/.test(content);
|
|
1586
|
+
if (hasImport && hasUse) {
|
|
1587
|
+
return true;
|
|
1588
|
+
}
|
|
1589
|
+
let modified = false;
|
|
1590
|
+
if (!hasImport) {
|
|
1591
|
+
const importRegex = /^import .+ from .+;?\s*$/gm;
|
|
1592
|
+
let lastImportIndex = 0;
|
|
1593
|
+
let match;
|
|
1594
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
1595
|
+
lastImportIndex = match.index + match[0].length;
|
|
1596
|
+
}
|
|
1597
|
+
if (lastImportIndex > 0) {
|
|
1598
|
+
content = content.slice(0, lastImportIndex) + `
|
|
1599
|
+
${rateLimiterImport}` + content.slice(lastImportIndex);
|
|
1600
|
+
modified = true;
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
if (!hasUse) {
|
|
1604
|
+
const jsonMiddleware = /^(\s*app\.use\(\s*express\.json\(\)\s*\);?\s*)$/m;
|
|
1605
|
+
const jsonMatch = content.match(jsonMiddleware);
|
|
1606
|
+
if (jsonMatch && jsonMatch.index !== void 0) {
|
|
1607
|
+
const insertAt = jsonMatch.index + jsonMatch[0].length;
|
|
1608
|
+
content = content.slice(0, insertAt) + `
|
|
1609
|
+
app.use(rateLimiter);` + content.slice(insertAt);
|
|
1610
|
+
modified = true;
|
|
1611
|
+
} else {
|
|
1612
|
+
const routeMount = /^\s*app\.use\(\s*["']\/api["']/m;
|
|
1613
|
+
const routeMatch = content.match(routeMount);
|
|
1614
|
+
if (routeMatch && routeMatch.index !== void 0) {
|
|
1615
|
+
content = content.slice(0, routeMatch.index) + `app.use(rateLimiter);
|
|
1616
|
+
` + content.slice(routeMatch.index);
|
|
1617
|
+
modified = true;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
if (modified) {
|
|
1622
|
+
await import_fs_extra10.default.writeFile(appPath, content);
|
|
1623
|
+
}
|
|
1222
1624
|
return true;
|
|
1223
1625
|
}
|
|
1626
|
+
|
|
1627
|
+
// src/commands/add.ts
|
|
1628
|
+
function resolvePackageManager(projectRoot) {
|
|
1629
|
+
if (import_fs_extra11.default.existsSync(import_path12.default.join(projectRoot, "pnpm-lock.yaml"))) {
|
|
1630
|
+
return "pnpm";
|
|
1631
|
+
}
|
|
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"))) {
|
|
1633
|
+
return "bun";
|
|
1634
|
+
}
|
|
1635
|
+
if (import_fs_extra11.default.existsSync(import_path12.default.join(projectRoot, "yarn.lock"))) {
|
|
1636
|
+
return "yarn";
|
|
1637
|
+
}
|
|
1638
|
+
return "npm";
|
|
1639
|
+
}
|
|
1640
|
+
function getModuleDocsPath(moduleName) {
|
|
1641
|
+
if (isDatabaseModule(moduleName)) {
|
|
1642
|
+
return "database";
|
|
1643
|
+
}
|
|
1644
|
+
return moduleName;
|
|
1645
|
+
}
|
|
1646
|
+
async function hasEnvVariable(projectRoot, key) {
|
|
1647
|
+
const envPath = import_path12.default.join(projectRoot, ".env");
|
|
1648
|
+
if (!await import_fs_extra11.default.pathExists(envPath)) {
|
|
1649
|
+
return false;
|
|
1650
|
+
}
|
|
1651
|
+
const content = await import_fs_extra11.default.readFile(envPath, "utf-8");
|
|
1652
|
+
const pattern = new RegExp(`^${escapeRegex(key)}=`, "m");
|
|
1653
|
+
return pattern.test(content);
|
|
1654
|
+
}
|
|
1655
|
+
async function isLikelyEmptyDirectory(cwd) {
|
|
1656
|
+
const entries = await import_fs_extra11.default.readdir(cwd);
|
|
1657
|
+
const ignored = /* @__PURE__ */ new Set([".ds_store", "thumbs.db"]);
|
|
1658
|
+
return entries.filter((entry) => !ignored.has(entry.toLowerCase())).length === 0;
|
|
1659
|
+
}
|
|
1224
1660
|
var add = async (moduleName, options = {}) => {
|
|
1225
1661
|
const projectRoot = process.cwd();
|
|
1226
1662
|
const projectConfig = await readZuroConfig(projectRoot);
|
|
@@ -1247,182 +1683,26 @@ var add = async (moduleName, options = {}) => {
|
|
|
1247
1683
|
let usedDefaultSmtp = false;
|
|
1248
1684
|
let mailerProvider = "smtp";
|
|
1249
1685
|
let shouldInstallDocsForAuth = false;
|
|
1250
|
-
if (resolvedModuleName === "database") {
|
|
1251
|
-
const
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
{ title: "MySQL", value: "database-mysql" }
|
|
1258
|
-
]
|
|
1259
|
-
});
|
|
1260
|
-
if (!variantResponse.variant) {
|
|
1261
|
-
console.log(import_chalk4.default.yellow("Operation cancelled."));
|
|
1262
|
-
return;
|
|
1263
|
-
}
|
|
1264
|
-
resolvedModuleName = variantResponse.variant;
|
|
1265
|
-
}
|
|
1266
|
-
if (isDatabaseModule(resolvedModuleName)) {
|
|
1267
|
-
const installedDialect = await detectInstalledDatabaseDialect(projectRoot, srcDir);
|
|
1268
|
-
if (installedDialect && installedDialect !== resolvedModuleName) {
|
|
1269
|
-
console.log(
|
|
1270
|
-
import_chalk4.default.yellow(
|
|
1271
|
-
`
|
|
1272
|
-
\u26A0 Existing database setup detected: ${databaseLabel(installedDialect)}.`
|
|
1273
|
-
)
|
|
1274
|
-
);
|
|
1275
|
-
console.log(
|
|
1276
|
-
import_chalk4.default.yellow(
|
|
1277
|
-
` Switching to ${databaseLabel(resolvedModuleName)} will overwrite db files and drizzle config.
|
|
1278
|
-
`
|
|
1279
|
-
)
|
|
1280
|
-
);
|
|
1281
|
-
const switchResponse = await (0, import_prompts2.default)({
|
|
1282
|
-
type: "confirm",
|
|
1283
|
-
name: "proceed",
|
|
1284
|
-
message: "Continue and switch database dialect?",
|
|
1285
|
-
initial: false
|
|
1286
|
-
});
|
|
1287
|
-
if (!switchResponse.proceed) {
|
|
1288
|
-
console.log(import_chalk4.default.yellow("Operation cancelled."));
|
|
1289
|
-
return;
|
|
1290
|
-
}
|
|
1291
|
-
databaseBackupPath = await backupDatabaseFiles(projectRoot, srcDir);
|
|
1292
|
-
}
|
|
1293
|
-
const defaultUrl = DEFAULT_DATABASE_URLS[resolvedModuleName];
|
|
1294
|
-
console.log(import_chalk4.default.dim(` Tip: Leave blank to use ${defaultUrl}
|
|
1295
|
-
`));
|
|
1296
|
-
const response = await (0, import_prompts2.default)({
|
|
1297
|
-
type: "text",
|
|
1298
|
-
name: "dbUrl",
|
|
1299
|
-
message: "Database URL",
|
|
1300
|
-
initial: ""
|
|
1301
|
-
});
|
|
1302
|
-
if (response.dbUrl === void 0) {
|
|
1303
|
-
console.log(import_chalk4.default.yellow("Operation cancelled."));
|
|
1304
|
-
return;
|
|
1305
|
-
}
|
|
1306
|
-
const enteredUrl = response.dbUrl?.trim() || "";
|
|
1307
|
-
usedDefaultDbUrl = enteredUrl.length === 0;
|
|
1308
|
-
customDbUrl = validateDatabaseUrl(enteredUrl || defaultUrl, resolvedModuleName);
|
|
1686
|
+
if (resolvedModuleName === "database" || isDatabaseModule(resolvedModuleName)) {
|
|
1687
|
+
const result = await promptDatabaseConfig(resolvedModuleName, projectRoot, srcDir);
|
|
1688
|
+
if (!result) return;
|
|
1689
|
+
resolvedModuleName = result.resolvedModuleName;
|
|
1690
|
+
customDbUrl = result.customDbUrl;
|
|
1691
|
+
usedDefaultDbUrl = result.usedDefaultDbUrl;
|
|
1692
|
+
databaseBackupPath = result.databaseBackupPath;
|
|
1309
1693
|
}
|
|
1310
1694
|
if (resolvedModuleName === "mailer") {
|
|
1311
|
-
const
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
{ title: "SMTP (Nodemailer)", description: "Gmail, Mailtrap, any SMTP server", value: "smtp" },
|
|
1317
|
-
{ title: "Resend", description: "API-based, easiest setup", value: "resend" }
|
|
1318
|
-
]
|
|
1319
|
-
});
|
|
1320
|
-
if (providerResponse.provider === void 0) {
|
|
1321
|
-
console.log(import_chalk4.default.yellow("Operation cancelled."));
|
|
1322
|
-
return;
|
|
1323
|
-
}
|
|
1324
|
-
mailerProvider = providerResponse.provider;
|
|
1325
|
-
console.log(import_chalk4.default.dim(" Tip: Leave fields blank to use placeholder values and configure later\n"));
|
|
1326
|
-
if (mailerProvider === "smtp") {
|
|
1327
|
-
const smtpResponse = await (0, import_prompts2.default)([
|
|
1328
|
-
{
|
|
1329
|
-
type: "text",
|
|
1330
|
-
name: "host",
|
|
1331
|
-
message: "SMTP Host",
|
|
1332
|
-
initial: ""
|
|
1333
|
-
},
|
|
1334
|
-
{
|
|
1335
|
-
type: "text",
|
|
1336
|
-
name: "port",
|
|
1337
|
-
message: "SMTP Port",
|
|
1338
|
-
initial: "587"
|
|
1339
|
-
},
|
|
1340
|
-
{
|
|
1341
|
-
type: "text",
|
|
1342
|
-
name: "user",
|
|
1343
|
-
message: "SMTP User",
|
|
1344
|
-
initial: ""
|
|
1345
|
-
},
|
|
1346
|
-
{
|
|
1347
|
-
type: "password",
|
|
1348
|
-
name: "pass",
|
|
1349
|
-
message: "SMTP Password"
|
|
1350
|
-
},
|
|
1351
|
-
{
|
|
1352
|
-
type: "text",
|
|
1353
|
-
name: "from",
|
|
1354
|
-
message: "Mail From address",
|
|
1355
|
-
initial: ""
|
|
1356
|
-
}
|
|
1357
|
-
]);
|
|
1358
|
-
if (smtpResponse.host === void 0) {
|
|
1359
|
-
console.log(import_chalk4.default.yellow("Operation cancelled."));
|
|
1360
|
-
return;
|
|
1361
|
-
}
|
|
1362
|
-
const host = smtpResponse.host?.trim() || "";
|
|
1363
|
-
const user = smtpResponse.user?.trim() || "";
|
|
1364
|
-
const pass = smtpResponse.pass?.trim() || "";
|
|
1365
|
-
const from = smtpResponse.from?.trim() || "";
|
|
1366
|
-
const port = smtpResponse.port?.trim() || "587";
|
|
1367
|
-
usedDefaultSmtp = !host && !user;
|
|
1368
|
-
if (!usedDefaultSmtp) {
|
|
1369
|
-
customSmtpVars = {
|
|
1370
|
-
SMTP_HOST: host || "smtp.example.com",
|
|
1371
|
-
SMTP_PORT: port,
|
|
1372
|
-
SMTP_USER: user || "your-email@example.com",
|
|
1373
|
-
SMTP_PASS: pass || "your-password",
|
|
1374
|
-
MAIL_FROM: from || "noreply@example.com"
|
|
1375
|
-
};
|
|
1376
|
-
}
|
|
1377
|
-
} else {
|
|
1378
|
-
const resendResponse = await (0, import_prompts2.default)([
|
|
1379
|
-
{
|
|
1380
|
-
type: "text",
|
|
1381
|
-
name: "apiKey",
|
|
1382
|
-
message: "Resend API Key",
|
|
1383
|
-
initial: ""
|
|
1384
|
-
},
|
|
1385
|
-
{
|
|
1386
|
-
type: "text",
|
|
1387
|
-
name: "from",
|
|
1388
|
-
message: "Mail From address",
|
|
1389
|
-
initial: ""
|
|
1390
|
-
}
|
|
1391
|
-
]);
|
|
1392
|
-
if (resendResponse.apiKey === void 0) {
|
|
1393
|
-
console.log(import_chalk4.default.yellow("Operation cancelled."));
|
|
1394
|
-
return;
|
|
1395
|
-
}
|
|
1396
|
-
const apiKey = resendResponse.apiKey?.trim() || "";
|
|
1397
|
-
const from = resendResponse.from?.trim() || "";
|
|
1398
|
-
usedDefaultSmtp = !apiKey;
|
|
1399
|
-
if (!usedDefaultSmtp) {
|
|
1400
|
-
customSmtpVars = {
|
|
1401
|
-
RESEND_API_KEY: apiKey || "re_your_api_key",
|
|
1402
|
-
MAIL_FROM: from || "onboarding@resend.dev"
|
|
1403
|
-
};
|
|
1404
|
-
}
|
|
1405
|
-
}
|
|
1695
|
+
const result = await promptMailerConfig();
|
|
1696
|
+
if (!result) return;
|
|
1697
|
+
mailerProvider = result.mailerProvider;
|
|
1698
|
+
customSmtpVars = result.customSmtpVars;
|
|
1699
|
+
usedDefaultSmtp = result.usedDefaultSmtp;
|
|
1406
1700
|
}
|
|
1407
1701
|
if (resolvedModuleName === "auth") {
|
|
1408
|
-
const
|
|
1409
|
-
if (!
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
} else {
|
|
1413
|
-
const docsResponse = await (0, import_prompts2.default)({
|
|
1414
|
-
type: "confirm",
|
|
1415
|
-
name: "installDocs",
|
|
1416
|
-
message: "Install API docs module (Scalar + OpenAPI) too?",
|
|
1417
|
-
initial: true
|
|
1418
|
-
});
|
|
1419
|
-
if (docsResponse.installDocs === void 0) {
|
|
1420
|
-
console.log(import_chalk4.default.yellow("Operation cancelled."));
|
|
1421
|
-
return;
|
|
1422
|
-
}
|
|
1423
|
-
shouldInstallDocsForAuth = docsResponse.installDocs;
|
|
1424
|
-
}
|
|
1425
|
-
}
|
|
1702
|
+
const result = await promptAuthConfig(projectRoot, srcDir, options);
|
|
1703
|
+
if (!result) return;
|
|
1704
|
+
shouldInstallDocsForAuth = result.shouldInstallDocsForAuth;
|
|
1705
|
+
authDatabaseDialect = result.authDatabaseDialect;
|
|
1426
1706
|
}
|
|
1427
1707
|
const pm = resolvePackageManager(projectRoot);
|
|
1428
1708
|
const spinner = (0, import_ora2.default)(`Checking registry for ${resolvedModuleName}...`).start();
|
|
@@ -1438,7 +1718,7 @@ var add = async (moduleName, options = {}) => {
|
|
|
1438
1718
|
spinner.fail(`Module '${resolvedModuleName}' not found.`);
|
|
1439
1719
|
return;
|
|
1440
1720
|
}
|
|
1441
|
-
spinner.succeed(`Found module: ${
|
|
1721
|
+
spinner.succeed(`Found module: ${import_chalk7.default.cyan(resolvedModuleName)}`);
|
|
1442
1722
|
const moduleDeps = module2.moduleDependencies || [];
|
|
1443
1723
|
currentStep = "module dependency resolution";
|
|
1444
1724
|
await resolveDependencies(moduleDeps, projectRoot);
|
|
@@ -1460,9 +1740,6 @@ var add = async (moduleName, options = {}) => {
|
|
|
1460
1740
|
spinner.succeed("Dependencies installed");
|
|
1461
1741
|
currentStep = "module scaffolding";
|
|
1462
1742
|
spinner.start("Scaffolding files...");
|
|
1463
|
-
if (resolvedModuleName === "auth") {
|
|
1464
|
-
authDatabaseDialect = await detectInstalledDatabaseDialect(projectRoot, srcDir);
|
|
1465
|
-
}
|
|
1466
1743
|
for (const file of module2.files) {
|
|
1467
1744
|
let fetchPath = file.path;
|
|
1468
1745
|
let expectedSha256 = file.sha256;
|
|
@@ -1490,10 +1767,10 @@ var add = async (moduleName, options = {}) => {
|
|
|
1490
1767
|
);
|
|
1491
1768
|
}
|
|
1492
1769
|
const targetPath = resolveSafeTargetPath2(projectRoot, srcDir, file);
|
|
1493
|
-
await
|
|
1494
|
-
await
|
|
1770
|
+
await import_fs_extra11.default.ensureDir(import_path12.default.dirname(targetPath));
|
|
1771
|
+
await import_fs_extra11.default.writeFile(targetPath, content);
|
|
1495
1772
|
}
|
|
1496
|
-
const schemaExports = module2.files.map((file) => file.target.replace(/\\/g, "/")).filter((target) => /^db\/schema\/[^/]+\.ts$/.test(target)).map((target) =>
|
|
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");
|
|
1497
1774
|
for (const schemaFileName of schemaExports) {
|
|
1498
1775
|
await ensureSchemaExport(projectRoot, srcDir, schemaFileName);
|
|
1499
1776
|
}
|
|
@@ -1526,6 +1803,15 @@ var add = async (moduleName, options = {}) => {
|
|
|
1526
1803
|
spinner.warn("Could not find app.ts - error handler needs manual setup");
|
|
1527
1804
|
}
|
|
1528
1805
|
}
|
|
1806
|
+
if (resolvedModuleName === "rate-limiter") {
|
|
1807
|
+
spinner.start("Configuring rate limiter in app.ts...");
|
|
1808
|
+
const injected = await injectRateLimiter(projectRoot, srcDir);
|
|
1809
|
+
if (injected) {
|
|
1810
|
+
spinner.succeed("Rate limiter configured in app.ts");
|
|
1811
|
+
} else {
|
|
1812
|
+
spinner.warn("Could not find app.ts - rate limiter needs manual setup");
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1529
1815
|
if (resolvedModuleName === "docs") {
|
|
1530
1816
|
spinner.start("Configuring docs routes...");
|
|
1531
1817
|
const injected = await injectDocsRoutes(projectRoot, srcDir);
|
|
@@ -1573,57 +1859,35 @@ var add = async (moduleName, options = {}) => {
|
|
|
1573
1859
|
await updateEnvSchema(projectRoot, srcDir, envConfig.schemaFields);
|
|
1574
1860
|
spinner.succeed("Environment configured");
|
|
1575
1861
|
}
|
|
1576
|
-
console.log(
|
|
1862
|
+
console.log(import_chalk7.default.green(`
|
|
1577
1863
|
\u2714 ${resolvedModuleName} added successfully!
|
|
1578
1864
|
`));
|
|
1579
|
-
if (databaseBackupPath) {
|
|
1580
|
-
console.log(import_chalk4.default.blue(`\u2139 Backup created at: ${databaseBackupPath}
|
|
1581
|
-
`));
|
|
1582
|
-
}
|
|
1583
1865
|
const docsPath = getModuleDocsPath(resolvedModuleName);
|
|
1584
1866
|
const docsUrl = `https://zuro-cli.devbybriyan.com/docs/${docsPath}`;
|
|
1585
|
-
console.log(
|
|
1867
|
+
console.log(import_chalk7.default.blue(`\u2139 Docs: ${docsUrl}`));
|
|
1586
1868
|
if (isDatabaseModule(resolvedModuleName)) {
|
|
1587
|
-
|
|
1588
|
-
console.log(import_chalk4.default.yellow("\u2139 Review DATABASE_URL in .env if your local DB config differs."));
|
|
1589
|
-
}
|
|
1590
|
-
const setupHint = getDatabaseSetupHint(
|
|
1591
|
-
resolvedModuleName,
|
|
1592
|
-
customDbUrl || DEFAULT_DATABASE_URLS[resolvedModuleName]
|
|
1593
|
-
);
|
|
1594
|
-
console.log(import_chalk4.default.yellow(`\u2139 Ensure DB exists: ${setupHint}`));
|
|
1595
|
-
console.log(import_chalk4.default.yellow("\u2139 Run migrations: npx drizzle-kit generate && npx drizzle-kit migrate"));
|
|
1869
|
+
printDatabaseHints(resolvedModuleName, customDbUrl, usedDefaultDbUrl, databaseBackupPath);
|
|
1596
1870
|
}
|
|
1597
1871
|
if (resolvedModuleName === "auth") {
|
|
1598
|
-
|
|
1599
|
-
console.log(import_chalk4.default.yellow("\u2139 BETTER_AUTH_SECRET was generated automatically."));
|
|
1600
|
-
} else {
|
|
1601
|
-
console.log(import_chalk4.default.yellow("\u2139 Review BETTER_AUTH_SECRET and BETTER_AUTH_URL in .env."));
|
|
1602
|
-
}
|
|
1603
|
-
console.log(import_chalk4.default.yellow("\u2139 Run migrations: npx drizzle-kit generate && npx drizzle-kit migrate"));
|
|
1872
|
+
printAuthHints(generatedAuthSecret);
|
|
1604
1873
|
}
|
|
1605
1874
|
if (resolvedModuleName === "mailer") {
|
|
1606
|
-
|
|
1607
|
-
console.log(import_chalk4.default.yellow("\u2139 Placeholder SMTP values added to .env \u2014 update them before sending emails."));
|
|
1608
|
-
} else {
|
|
1609
|
-
console.log(import_chalk4.default.yellow("\u2139 Review SMTP configuration in .env to ensure values are correct."));
|
|
1610
|
-
}
|
|
1875
|
+
printMailerHints(usedDefaultSmtp);
|
|
1611
1876
|
}
|
|
1612
1877
|
if (resolvedModuleName === "docs") {
|
|
1613
|
-
|
|
1614
|
-
console.log(import_chalk4.default.yellow("\u2139 OpenAPI spec available at: /api/docs/openapi.json"));
|
|
1878
|
+
printDocsHints();
|
|
1615
1879
|
}
|
|
1616
1880
|
if (resolvedModuleName === "auth" && shouldInstallDocsForAuth) {
|
|
1617
|
-
console.log(
|
|
1881
|
+
console.log(import_chalk7.default.blue("\n\u2139 Installing API docs module..."));
|
|
1618
1882
|
await add("docs", { yes: true });
|
|
1619
1883
|
}
|
|
1620
1884
|
} catch (error) {
|
|
1621
|
-
spinner.fail(
|
|
1885
|
+
spinner.fail(import_chalk7.default.red(`Failed during ${currentStep}.`));
|
|
1622
1886
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1623
|
-
console.error(
|
|
1887
|
+
console.error(import_chalk7.default.red(errorMessage));
|
|
1624
1888
|
console.log(`
|
|
1625
|
-
${
|
|
1626
|
-
console.log(
|
|
1889
|
+
${import_chalk7.default.bold("Retry:")}`);
|
|
1890
|
+
console.log(import_chalk7.default.cyan(` npx zuro-cli add ${resolvedModuleName}`));
|
|
1627
1891
|
}
|
|
1628
1892
|
};
|
|
1629
1893
|
|