servcraft 0.1.7 → 0.3.1
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/.github/workflows/ci.yml +9 -4
- package/README.md +63 -2
- package/ROADMAP.md +86 -41
- package/dist/cli/index.cjs +1510 -172
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +1516 -172
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/add-module.ts +36 -7
- package/src/cli/commands/completion.ts +146 -0
- package/src/cli/commands/doctor.ts +123 -0
- package/src/cli/commands/generate.ts +73 -1
- package/src/cli/commands/init.ts +29 -10
- package/src/cli/commands/list.ts +274 -0
- package/src/cli/commands/remove.ts +102 -0
- package/src/cli/commands/update.ts +221 -0
- package/src/cli/index.ts +20 -0
- package/src/cli/templates/controller-test.ts +110 -0
- package/src/cli/templates/integration-test.ts +139 -0
- package/src/cli/templates/service-test.ts +100 -0
- package/src/cli/utils/dry-run.ts +155 -0
- package/src/cli/utils/error-handler.ts +184 -0
- package/src/cli/utils/helpers.ts +13 -0
- package/tests/cli/add.test.ts +32 -0
- package/tests/cli/completion.test.ts +35 -0
- package/tests/cli/doctor.test.ts +23 -0
- package/tests/cli/dry-run.test.ts +39 -0
- package/tests/cli/errors.test.ts +29 -0
- package/tests/cli/generate.test.ts +39 -0
- package/tests/cli/init.test.ts +63 -0
- package/tests/cli/list.test.ts +25 -0
- package/tests/cli/remove.test.ts +28 -0
- package/tests/cli/update.test.ts +34 -0
package/dist/cli/index.cjs
CHANGED
|
@@ -28,21 +28,104 @@ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${_
|
|
|
28
28
|
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
29
29
|
|
|
30
30
|
// src/cli/index.ts
|
|
31
|
-
var
|
|
31
|
+
var import_commander11 = require("commander");
|
|
32
32
|
|
|
33
33
|
// src/cli/commands/init.ts
|
|
34
34
|
var import_commander = require("commander");
|
|
35
|
-
var
|
|
35
|
+
var import_path3 = __toESM(require("path"), 1);
|
|
36
36
|
var import_promises2 = __toESM(require("fs/promises"), 1);
|
|
37
37
|
var import_ora = __toESM(require("ora"), 1);
|
|
38
38
|
var import_inquirer = __toESM(require("inquirer"), 1);
|
|
39
|
-
var
|
|
39
|
+
var import_chalk3 = __toESM(require("chalk"), 1);
|
|
40
40
|
var import_child_process = require("child_process");
|
|
41
41
|
|
|
42
42
|
// src/cli/utils/helpers.ts
|
|
43
43
|
var import_promises = __toESM(require("fs/promises"), 1);
|
|
44
|
-
var
|
|
44
|
+
var import_path2 = __toESM(require("path"), 1);
|
|
45
|
+
var import_chalk2 = __toESM(require("chalk"), 1);
|
|
46
|
+
|
|
47
|
+
// src/cli/utils/dry-run.ts
|
|
45
48
|
var import_chalk = __toESM(require("chalk"), 1);
|
|
49
|
+
var import_path = __toESM(require("path"), 1);
|
|
50
|
+
var DryRunManager = class _DryRunManager {
|
|
51
|
+
static instance;
|
|
52
|
+
enabled = false;
|
|
53
|
+
operations = [];
|
|
54
|
+
constructor() {
|
|
55
|
+
}
|
|
56
|
+
static getInstance() {
|
|
57
|
+
if (!_DryRunManager.instance) {
|
|
58
|
+
_DryRunManager.instance = new _DryRunManager();
|
|
59
|
+
}
|
|
60
|
+
return _DryRunManager.instance;
|
|
61
|
+
}
|
|
62
|
+
enable() {
|
|
63
|
+
this.enabled = true;
|
|
64
|
+
this.operations = [];
|
|
65
|
+
}
|
|
66
|
+
disable() {
|
|
67
|
+
this.enabled = false;
|
|
68
|
+
this.operations = [];
|
|
69
|
+
}
|
|
70
|
+
isEnabled() {
|
|
71
|
+
return this.enabled;
|
|
72
|
+
}
|
|
73
|
+
addOperation(operation) {
|
|
74
|
+
if (this.enabled) {
|
|
75
|
+
this.operations.push(operation);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
getOperations() {
|
|
79
|
+
return [...this.operations];
|
|
80
|
+
}
|
|
81
|
+
printSummary() {
|
|
82
|
+
if (!this.enabled || this.operations.length === 0) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
console.log(import_chalk.default.bold.yellow("\n\u{1F4CB} Dry Run - Preview of changes:\n"));
|
|
86
|
+
console.log(import_chalk.default.gray("No files will be written. Remove --dry-run to apply changes.\n"));
|
|
87
|
+
const createOps = this.operations.filter((op) => op.type === "create");
|
|
88
|
+
const modifyOps = this.operations.filter((op) => op.type === "modify");
|
|
89
|
+
const deleteOps = this.operations.filter((op) => op.type === "delete");
|
|
90
|
+
if (createOps.length > 0) {
|
|
91
|
+
console.log(import_chalk.default.green.bold(`
|
|
92
|
+
\u2713 Files to be created (${createOps.length}):`));
|
|
93
|
+
createOps.forEach((op) => {
|
|
94
|
+
const size = op.content ? `${op.content.length} bytes` : "unknown size";
|
|
95
|
+
console.log(` ${import_chalk.default.green("+")} ${import_chalk.default.cyan(op.path)} ${import_chalk.default.gray(`(${size})`)}`);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
if (modifyOps.length > 0) {
|
|
99
|
+
console.log(import_chalk.default.yellow.bold(`
|
|
100
|
+
~ Files to be modified (${modifyOps.length}):`));
|
|
101
|
+
modifyOps.forEach((op) => {
|
|
102
|
+
console.log(` ${import_chalk.default.yellow("~")} ${import_chalk.default.cyan(op.path)}`);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
if (deleteOps.length > 0) {
|
|
106
|
+
console.log(import_chalk.default.red.bold(`
|
|
107
|
+
- Files to be deleted (${deleteOps.length}):`));
|
|
108
|
+
deleteOps.forEach((op) => {
|
|
109
|
+
console.log(` ${import_chalk.default.red("-")} ${import_chalk.default.cyan(op.path)}`);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
console.log(import_chalk.default.gray("\n" + "\u2500".repeat(60)));
|
|
113
|
+
console.log(
|
|
114
|
+
import_chalk.default.bold(` Total operations: ${this.operations.length}`) + import_chalk.default.gray(
|
|
115
|
+
` (${createOps.length} create, ${modifyOps.length} modify, ${deleteOps.length} delete)`
|
|
116
|
+
)
|
|
117
|
+
);
|
|
118
|
+
console.log(import_chalk.default.gray("\u2500".repeat(60)));
|
|
119
|
+
console.log(import_chalk.default.yellow("\n\u26A0 This was a dry run. No files were created or modified."));
|
|
120
|
+
console.log(import_chalk.default.gray(" Remove --dry-run to apply these changes.\n"));
|
|
121
|
+
}
|
|
122
|
+
// Helper to format file path relative to cwd
|
|
123
|
+
relativePath(filePath) {
|
|
124
|
+
return import_path.default.relative(process.cwd(), filePath);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// src/cli/utils/helpers.ts
|
|
46
129
|
function toPascalCase(str) {
|
|
47
130
|
return str.split(/[-_\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
|
|
48
131
|
}
|
|
@@ -74,39 +157,54 @@ async function ensureDir(dirPath) {
|
|
|
74
157
|
await import_promises.default.mkdir(dirPath, { recursive: true });
|
|
75
158
|
}
|
|
76
159
|
async function writeFile(filePath, content) {
|
|
77
|
-
|
|
160
|
+
const dryRun = DryRunManager.getInstance();
|
|
161
|
+
if (dryRun.isEnabled()) {
|
|
162
|
+
dryRun.addOperation({
|
|
163
|
+
type: "create",
|
|
164
|
+
path: dryRun.relativePath(filePath),
|
|
165
|
+
content,
|
|
166
|
+
size: content.length
|
|
167
|
+
});
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
await ensureDir(import_path2.default.dirname(filePath));
|
|
78
171
|
await import_promises.default.writeFile(filePath, content, "utf-8");
|
|
79
172
|
}
|
|
80
173
|
function success(message) {
|
|
81
|
-
console.log(
|
|
174
|
+
console.log(import_chalk2.default.green("\u2713"), message);
|
|
82
175
|
}
|
|
83
176
|
function error(message) {
|
|
84
|
-
console.error(
|
|
177
|
+
console.error(import_chalk2.default.red("\u2717"), message);
|
|
85
178
|
}
|
|
86
179
|
function warn(message) {
|
|
87
|
-
console.log(
|
|
180
|
+
console.log(import_chalk2.default.yellow("\u26A0"), message);
|
|
88
181
|
}
|
|
89
182
|
function info(message) {
|
|
90
|
-
console.log(
|
|
183
|
+
console.log(import_chalk2.default.blue("\u2139"), message);
|
|
91
184
|
}
|
|
92
185
|
function getProjectRoot() {
|
|
93
186
|
return process.cwd();
|
|
94
187
|
}
|
|
95
188
|
function getSourceDir() {
|
|
96
|
-
return
|
|
189
|
+
return import_path2.default.join(getProjectRoot(), "src");
|
|
97
190
|
}
|
|
98
191
|
function getModulesDir() {
|
|
99
|
-
return
|
|
192
|
+
return import_path2.default.join(getSourceDir(), "modules");
|
|
100
193
|
}
|
|
101
194
|
|
|
102
195
|
// src/cli/commands/init.ts
|
|
103
|
-
var initCommand = new import_commander.Command("init").alias("new").description("Initialize a new Servcraft project").argument("[name]", "Project name").option("-y, --yes", "Skip prompts and use defaults").option("--ts, --typescript", "Use TypeScript (default)").option("--js, --javascript", "Use JavaScript").option("--esm", "Use ES Modules (import/export) - default").option("--cjs, --commonjs", "Use CommonJS (require/module.exports)").option("--db <database>", "Database type (postgresql, mysql, sqlite, mongodb, none)").action(
|
|
196
|
+
var initCommand = new import_commander.Command("init").alias("new").description("Initialize a new Servcraft project").argument("[name]", "Project name").option("-y, --yes", "Skip prompts and use defaults").option("--ts, --typescript", "Use TypeScript (default)").option("--js, --javascript", "Use JavaScript").option("--esm", "Use ES Modules (import/export) - default").option("--cjs, --commonjs", "Use CommonJS (require/module.exports)").option("--db <database>", "Database type (postgresql, mysql, sqlite, mongodb, none)").option("--dry-run", "Preview changes without writing files").action(
|
|
104
197
|
async (name, cmdOptions) => {
|
|
198
|
+
const dryRun = DryRunManager.getInstance();
|
|
199
|
+
if (cmdOptions?.dryRun) {
|
|
200
|
+
dryRun.enable();
|
|
201
|
+
console.log(import_chalk3.default.yellow("\n\u26A0 DRY RUN MODE - No files will be written\n"));
|
|
202
|
+
}
|
|
105
203
|
console.log(
|
|
106
|
-
|
|
204
|
+
import_chalk3.default.blue(`
|
|
107
205
|
\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
|
|
108
206
|
\u2551 \u2551
|
|
109
|
-
\u2551 ${
|
|
207
|
+
\u2551 ${import_chalk3.default.bold("\u{1F680} Servcraft Project Generator")} \u2551
|
|
110
208
|
\u2551 \u2551
|
|
111
209
|
\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
|
|
112
210
|
`)
|
|
@@ -201,7 +299,7 @@ var initCommand = new import_commander.Command("init").alias("new").description(
|
|
|
201
299
|
orm: db === "mongodb" ? "mongoose" : db === "none" ? "none" : "prisma"
|
|
202
300
|
};
|
|
203
301
|
}
|
|
204
|
-
const projectDir =
|
|
302
|
+
const projectDir = import_path3.default.resolve(process.cwd(), options.name);
|
|
205
303
|
const spinner = (0, import_ora.default)("Creating project...").start();
|
|
206
304
|
try {
|
|
207
305
|
try {
|
|
@@ -215,21 +313,21 @@ var initCommand = new import_commander.Command("init").alias("new").description(
|
|
|
215
313
|
spinner.text = "Generating project files...";
|
|
216
314
|
const packageJson = generatePackageJson(options);
|
|
217
315
|
await writeFile(
|
|
218
|
-
|
|
316
|
+
import_path3.default.join(projectDir, "package.json"),
|
|
219
317
|
JSON.stringify(packageJson, null, 2)
|
|
220
318
|
);
|
|
221
319
|
if (options.language === "typescript") {
|
|
222
|
-
await writeFile(
|
|
223
|
-
await writeFile(
|
|
320
|
+
await writeFile(import_path3.default.join(projectDir, "tsconfig.json"), generateTsConfig(options));
|
|
321
|
+
await writeFile(import_path3.default.join(projectDir, "tsup.config.ts"), generateTsupConfig(options));
|
|
224
322
|
} else {
|
|
225
|
-
await writeFile(
|
|
323
|
+
await writeFile(import_path3.default.join(projectDir, "jsconfig.json"), generateJsConfig(options));
|
|
226
324
|
}
|
|
227
|
-
await writeFile(
|
|
228
|
-
await writeFile(
|
|
229
|
-
await writeFile(
|
|
230
|
-
await writeFile(
|
|
325
|
+
await writeFile(import_path3.default.join(projectDir, ".env.example"), generateEnvExample(options));
|
|
326
|
+
await writeFile(import_path3.default.join(projectDir, ".env"), generateEnvExample(options));
|
|
327
|
+
await writeFile(import_path3.default.join(projectDir, ".gitignore"), generateGitignore());
|
|
328
|
+
await writeFile(import_path3.default.join(projectDir, "Dockerfile"), generateDockerfile(options));
|
|
231
329
|
await writeFile(
|
|
232
|
-
|
|
330
|
+
import_path3.default.join(projectDir, "docker-compose.yml"),
|
|
233
331
|
generateDockerCompose(options)
|
|
234
332
|
);
|
|
235
333
|
const ext = options.language === "typescript" ? "ts" : options.moduleSystem === "esm" ? "js" : "cjs";
|
|
@@ -250,59 +348,63 @@ var initCommand = new import_commander.Command("init").alias("new").description(
|
|
|
250
348
|
dirs.push("src/database/models");
|
|
251
349
|
}
|
|
252
350
|
for (const dir of dirs) {
|
|
253
|
-
await ensureDir(
|
|
351
|
+
await ensureDir(import_path3.default.join(projectDir, dir));
|
|
254
352
|
}
|
|
255
|
-
await writeFile(
|
|
353
|
+
await writeFile(import_path3.default.join(projectDir, `src/index.${ext}`), generateEntryFile(options));
|
|
256
354
|
await writeFile(
|
|
257
|
-
|
|
355
|
+
import_path3.default.join(projectDir, `src/core/server.${ext}`),
|
|
258
356
|
generateServerFile(options)
|
|
259
357
|
);
|
|
260
358
|
await writeFile(
|
|
261
|
-
|
|
359
|
+
import_path3.default.join(projectDir, `src/core/logger.${ext}`),
|
|
262
360
|
generateLoggerFile(options)
|
|
263
361
|
);
|
|
264
362
|
await writeFile(
|
|
265
|
-
|
|
363
|
+
import_path3.default.join(projectDir, `src/config/index.${ext}`),
|
|
266
364
|
generateConfigFile(options)
|
|
267
365
|
);
|
|
268
366
|
await writeFile(
|
|
269
|
-
|
|
367
|
+
import_path3.default.join(projectDir, `src/middleware/index.${ext}`),
|
|
270
368
|
generateMiddlewareFile(options)
|
|
271
369
|
);
|
|
272
370
|
await writeFile(
|
|
273
|
-
|
|
371
|
+
import_path3.default.join(projectDir, `src/utils/index.${ext}`),
|
|
274
372
|
generateUtilsFile(options)
|
|
275
373
|
);
|
|
276
374
|
await writeFile(
|
|
277
|
-
|
|
375
|
+
import_path3.default.join(projectDir, `src/types/index.${ext}`),
|
|
278
376
|
generateTypesFile(options)
|
|
279
377
|
);
|
|
280
378
|
if (options.orm === "prisma") {
|
|
281
379
|
await writeFile(
|
|
282
|
-
|
|
380
|
+
import_path3.default.join(projectDir, "prisma/schema.prisma"),
|
|
283
381
|
generatePrismaSchema(options)
|
|
284
382
|
);
|
|
285
383
|
} else if (options.orm === "mongoose") {
|
|
286
384
|
await writeFile(
|
|
287
|
-
|
|
385
|
+
import_path3.default.join(projectDir, `src/database/connection.${ext}`),
|
|
288
386
|
generateMongooseConnection(options)
|
|
289
387
|
);
|
|
290
388
|
await writeFile(
|
|
291
|
-
|
|
389
|
+
import_path3.default.join(projectDir, `src/database/models/user.model.${ext}`),
|
|
292
390
|
generateMongooseUserModel(options)
|
|
293
391
|
);
|
|
294
392
|
}
|
|
295
393
|
spinner.succeed("Project files generated!");
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
394
|
+
if (!cmdOptions?.dryRun) {
|
|
395
|
+
const installSpinner = (0, import_ora.default)("Installing dependencies...").start();
|
|
396
|
+
try {
|
|
397
|
+
(0, import_child_process.execSync)("npm install", { cwd: projectDir, stdio: "pipe" });
|
|
398
|
+
installSpinner.succeed("Dependencies installed!");
|
|
399
|
+
} catch {
|
|
400
|
+
installSpinner.warn("Failed to install dependencies automatically");
|
|
401
|
+
warn(' Run "npm install" manually in the project directory');
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (!cmdOptions?.dryRun) {
|
|
405
|
+
console.log("\n" + import_chalk3.default.green("\u2728 Project created successfully!"));
|
|
303
406
|
}
|
|
304
|
-
console.log("\n" +
|
|
305
|
-
console.log("\n" + import_chalk2.default.bold("\u{1F4C1} Project structure:"));
|
|
407
|
+
console.log("\n" + import_chalk3.default.bold("\u{1F4C1} Project structure:"));
|
|
306
408
|
console.log(`
|
|
307
409
|
${options.name}/
|
|
308
410
|
\u251C\u2500\u2500 src/
|
|
@@ -317,19 +419,22 @@ var initCommand = new import_commander.Command("init").alias("new").description(
|
|
|
317
419
|
\u251C\u2500\u2500 docker-compose.yml
|
|
318
420
|
\u2514\u2500\u2500 package.json
|
|
319
421
|
`);
|
|
320
|
-
console.log(
|
|
422
|
+
console.log(import_chalk3.default.bold("\u{1F680} Get started:"));
|
|
321
423
|
console.log(`
|
|
322
|
-
${
|
|
323
|
-
${options.database !== "none" ?
|
|
324
|
-
${
|
|
424
|
+
${import_chalk3.default.cyan(`cd ${options.name}`)}
|
|
425
|
+
${options.database !== "none" ? import_chalk3.default.cyan("npm run db:push # Setup database") : ""}
|
|
426
|
+
${import_chalk3.default.cyan("npm run dev # Start development server")}
|
|
325
427
|
`);
|
|
326
|
-
console.log(
|
|
428
|
+
console.log(import_chalk3.default.bold("\u{1F4DA} Available commands:"));
|
|
327
429
|
console.log(`
|
|
328
|
-
${
|
|
329
|
-
${
|
|
330
|
-
${
|
|
331
|
-
${
|
|
430
|
+
${import_chalk3.default.yellow("servcraft generate module <name>")} Generate a new module
|
|
431
|
+
${import_chalk3.default.yellow("servcraft generate controller <name>")} Generate a controller
|
|
432
|
+
${import_chalk3.default.yellow("servcraft generate service <name>")} Generate a service
|
|
433
|
+
${import_chalk3.default.yellow("servcraft add auth")} Add authentication module
|
|
332
434
|
`);
|
|
435
|
+
if (cmdOptions?.dryRun) {
|
|
436
|
+
dryRun.printSummary();
|
|
437
|
+
}
|
|
333
438
|
} catch (err) {
|
|
334
439
|
spinner.fail("Failed to create project");
|
|
335
440
|
error(err instanceof Error ? err.message : String(err));
|
|
@@ -1121,9 +1226,10 @@ declare module 'fastify' {
|
|
|
1121
1226
|
|
|
1122
1227
|
// src/cli/commands/generate.ts
|
|
1123
1228
|
var import_commander2 = require("commander");
|
|
1124
|
-
var
|
|
1229
|
+
var import_path4 = __toESM(require("path"), 1);
|
|
1125
1230
|
var import_ora2 = __toESM(require("ora"), 1);
|
|
1126
1231
|
var import_inquirer2 = __toESM(require("inquirer"), 1);
|
|
1232
|
+
var import_chalk5 = __toESM(require("chalk"), 1);
|
|
1127
1233
|
|
|
1128
1234
|
// src/cli/utils/field-parser.ts
|
|
1129
1235
|
var tsTypeMap = {
|
|
@@ -1267,6 +1373,108 @@ function parseFields(fieldsStr) {
|
|
|
1267
1373
|
return fieldsStr.split(/\s+/).filter(Boolean).map(parseField);
|
|
1268
1374
|
}
|
|
1269
1375
|
|
|
1376
|
+
// src/cli/utils/error-handler.ts
|
|
1377
|
+
var import_chalk4 = __toESM(require("chalk"), 1);
|
|
1378
|
+
var ServCraftError = class extends Error {
|
|
1379
|
+
suggestions;
|
|
1380
|
+
docsLink;
|
|
1381
|
+
constructor(message, suggestions = [], docsLink) {
|
|
1382
|
+
super(message);
|
|
1383
|
+
this.name = "ServCraftError";
|
|
1384
|
+
this.suggestions = suggestions;
|
|
1385
|
+
this.docsLink = docsLink;
|
|
1386
|
+
}
|
|
1387
|
+
};
|
|
1388
|
+
var ErrorTypes = {
|
|
1389
|
+
MODULE_NOT_FOUND: (moduleName) => new ServCraftError(
|
|
1390
|
+
`Module "${moduleName}" not found`,
|
|
1391
|
+
[
|
|
1392
|
+
`Run ${import_chalk4.default.cyan("servcraft list")} to see available modules`,
|
|
1393
|
+
`Check the spelling of the module name`,
|
|
1394
|
+
`Visit ${import_chalk4.default.blue("https://github.com/Le-Sourcier/servcraft#modules")} for module list`
|
|
1395
|
+
],
|
|
1396
|
+
"https://github.com/Le-Sourcier/servcraft#add-pre-built-modules"
|
|
1397
|
+
),
|
|
1398
|
+
MODULE_ALREADY_EXISTS: (moduleName) => new ServCraftError(`Module "${moduleName}" already exists`, [
|
|
1399
|
+
`Use ${import_chalk4.default.cyan("servcraft add " + moduleName + " --force")} to overwrite`,
|
|
1400
|
+
`Use ${import_chalk4.default.cyan("servcraft add " + moduleName + " --update")} to update`,
|
|
1401
|
+
`Use ${import_chalk4.default.cyan("servcraft add " + moduleName + " --skip-existing")} to skip`
|
|
1402
|
+
]),
|
|
1403
|
+
NOT_IN_PROJECT: () => new ServCraftError(
|
|
1404
|
+
"Not in a ServCraft project directory",
|
|
1405
|
+
[
|
|
1406
|
+
`Run ${import_chalk4.default.cyan("servcraft init")} to create a new project`,
|
|
1407
|
+
`Navigate to your ServCraft project directory`,
|
|
1408
|
+
`Check if ${import_chalk4.default.yellow("package.json")} exists`
|
|
1409
|
+
],
|
|
1410
|
+
"https://github.com/Le-Sourcier/servcraft#initialize-project"
|
|
1411
|
+
),
|
|
1412
|
+
FILE_ALREADY_EXISTS: (fileName) => new ServCraftError(`File "${fileName}" already exists`, [
|
|
1413
|
+
`Use ${import_chalk4.default.cyan("--force")} flag to overwrite`,
|
|
1414
|
+
`Choose a different name`,
|
|
1415
|
+
`Delete the existing file first`
|
|
1416
|
+
]),
|
|
1417
|
+
INVALID_DATABASE: (database) => new ServCraftError(`Invalid database type: "${database}"`, [
|
|
1418
|
+
`Valid options: ${import_chalk4.default.cyan("postgresql, mysql, sqlite, mongodb, none")}`,
|
|
1419
|
+
`Use ${import_chalk4.default.cyan("servcraft init --db postgresql")} for PostgreSQL`
|
|
1420
|
+
]),
|
|
1421
|
+
INVALID_VALIDATOR: (validator) => new ServCraftError(`Invalid validator type: "${validator}"`, [
|
|
1422
|
+
`Valid options: ${import_chalk4.default.cyan("zod, joi, yup")}`,
|
|
1423
|
+
`Default is ${import_chalk4.default.cyan("zod")}`
|
|
1424
|
+
]),
|
|
1425
|
+
MISSING_DEPENDENCY: (dependency, command) => new ServCraftError(`Missing dependency: "${dependency}"`, [
|
|
1426
|
+
`Run ${import_chalk4.default.cyan(command)} to install`,
|
|
1427
|
+
`Check your ${import_chalk4.default.yellow("package.json")}`
|
|
1428
|
+
]),
|
|
1429
|
+
INVALID_FIELD_FORMAT: (field) => new ServCraftError(`Invalid field format: "${field}"`, [
|
|
1430
|
+
`Expected format: ${import_chalk4.default.cyan("name:type")}`,
|
|
1431
|
+
`Example: ${import_chalk4.default.cyan("name:string age:number isActive:boolean")}`,
|
|
1432
|
+
`Supported types: string, number, boolean, date`
|
|
1433
|
+
]),
|
|
1434
|
+
GIT_NOT_INITIALIZED: () => new ServCraftError("Git repository not initialized", [
|
|
1435
|
+
`Run ${import_chalk4.default.cyan("git init")} to initialize git`,
|
|
1436
|
+
`This is required for some ServCraft features`
|
|
1437
|
+
])
|
|
1438
|
+
};
|
|
1439
|
+
function displayError(error2) {
|
|
1440
|
+
console.error("\n" + import_chalk4.default.red.bold("\u2717 Error: ") + import_chalk4.default.red(error2.message));
|
|
1441
|
+
if (error2 instanceof ServCraftError) {
|
|
1442
|
+
if (error2.suggestions.length > 0) {
|
|
1443
|
+
console.log("\n" + import_chalk4.default.yellow.bold("\u{1F4A1} Suggestions:"));
|
|
1444
|
+
error2.suggestions.forEach((suggestion) => {
|
|
1445
|
+
console.log(import_chalk4.default.yellow(" \u2022 ") + suggestion);
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
if (error2.docsLink) {
|
|
1449
|
+
console.log(
|
|
1450
|
+
"\n" + import_chalk4.default.blue.bold("\u{1F4DA} Documentation: ") + import_chalk4.default.blue.underline(error2.docsLink)
|
|
1451
|
+
);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
console.log();
|
|
1455
|
+
}
|
|
1456
|
+
function validateProject() {
|
|
1457
|
+
try {
|
|
1458
|
+
const fs12 = require("fs");
|
|
1459
|
+
if (!fs12.existsSync("package.json")) {
|
|
1460
|
+
return ErrorTypes.NOT_IN_PROJECT();
|
|
1461
|
+
}
|
|
1462
|
+
const packageJson = JSON.parse(fs12.readFileSync("package.json", "utf-8"));
|
|
1463
|
+
if (!packageJson.dependencies?.fastify) {
|
|
1464
|
+
return new ServCraftError("This does not appear to be a ServCraft project", [
|
|
1465
|
+
`ServCraft projects require Fastify`,
|
|
1466
|
+
`Run ${import_chalk4.default.cyan("servcraft init")} to create a new project`
|
|
1467
|
+
]);
|
|
1468
|
+
}
|
|
1469
|
+
return null;
|
|
1470
|
+
} catch {
|
|
1471
|
+
return new ServCraftError("Failed to validate project", [
|
|
1472
|
+
`Ensure you are in the project root directory`,
|
|
1473
|
+
`Check if ${import_chalk4.default.yellow("package.json")} is valid`
|
|
1474
|
+
]);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1270
1478
|
// src/cli/templates/controller.ts
|
|
1271
1479
|
function controllerTemplate(name, pascalName, camelName) {
|
|
1272
1480
|
return `import type { FastifyRequest, FastifyReply } from 'fastify';
|
|
@@ -1955,11 +2163,371 @@ ${indexLines.join("\n")}
|
|
|
1955
2163
|
`;
|
|
1956
2164
|
}
|
|
1957
2165
|
|
|
2166
|
+
// src/cli/templates/controller-test.ts
|
|
2167
|
+
function controllerTestTemplate(name, pascalName, camelName) {
|
|
2168
|
+
return `import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2169
|
+
import { build } from '../../app.js';
|
|
2170
|
+
import { FastifyInstance } from 'fastify';
|
|
2171
|
+
|
|
2172
|
+
describe('${pascalName}Controller', () => {
|
|
2173
|
+
let app: FastifyInstance;
|
|
2174
|
+
|
|
2175
|
+
beforeAll(async () => {
|
|
2176
|
+
app = await build();
|
|
2177
|
+
await app.ready();
|
|
2178
|
+
});
|
|
2179
|
+
|
|
2180
|
+
afterAll(async () => {
|
|
2181
|
+
await app.close();
|
|
2182
|
+
});
|
|
2183
|
+
|
|
2184
|
+
describe('GET /${name}', () => {
|
|
2185
|
+
it('should return list of ${name}', async () => {
|
|
2186
|
+
const response = await app.inject({
|
|
2187
|
+
method: 'GET',
|
|
2188
|
+
url: '/${name}',
|
|
2189
|
+
});
|
|
2190
|
+
|
|
2191
|
+
expect(response.statusCode).toBe(200);
|
|
2192
|
+
expect(response.json()).toHaveProperty('data');
|
|
2193
|
+
});
|
|
2194
|
+
});
|
|
2195
|
+
|
|
2196
|
+
describe('GET /${name}/:id', () => {
|
|
2197
|
+
it('should return a single ${camelName}', async () => {
|
|
2198
|
+
// TODO: Create test ${camelName} first
|
|
2199
|
+
const response = await app.inject({
|
|
2200
|
+
method: 'GET',
|
|
2201
|
+
url: '/${name}/1',
|
|
2202
|
+
});
|
|
2203
|
+
|
|
2204
|
+
expect(response.statusCode).toBe(200);
|
|
2205
|
+
expect(response.json()).toHaveProperty('data');
|
|
2206
|
+
});
|
|
2207
|
+
|
|
2208
|
+
it('should return 404 for non-existent ${camelName}', async () => {
|
|
2209
|
+
const response = await app.inject({
|
|
2210
|
+
method: 'GET',
|
|
2211
|
+
url: '/${name}/999999',
|
|
2212
|
+
});
|
|
2213
|
+
|
|
2214
|
+
expect(response.statusCode).toBe(404);
|
|
2215
|
+
});
|
|
2216
|
+
});
|
|
2217
|
+
|
|
2218
|
+
describe('POST /${name}', () => {
|
|
2219
|
+
it('should create a new ${camelName}', async () => {
|
|
2220
|
+
const response = await app.inject({
|
|
2221
|
+
method: 'POST',
|
|
2222
|
+
url: '/${name}',
|
|
2223
|
+
payload: {
|
|
2224
|
+
// TODO: Add required fields
|
|
2225
|
+
},
|
|
2226
|
+
});
|
|
2227
|
+
|
|
2228
|
+
expect(response.statusCode).toBe(201);
|
|
2229
|
+
expect(response.json()).toHaveProperty('data');
|
|
2230
|
+
});
|
|
2231
|
+
|
|
2232
|
+
it('should return 400 for invalid data', async () => {
|
|
2233
|
+
const response = await app.inject({
|
|
2234
|
+
method: 'POST',
|
|
2235
|
+
url: '/${name}',
|
|
2236
|
+
payload: {},
|
|
2237
|
+
});
|
|
2238
|
+
|
|
2239
|
+
expect(response.statusCode).toBe(400);
|
|
2240
|
+
});
|
|
2241
|
+
});
|
|
2242
|
+
|
|
2243
|
+
describe('PUT /${name}/:id', () => {
|
|
2244
|
+
it('should update a ${camelName}', async () => {
|
|
2245
|
+
// TODO: Create test ${camelName} first
|
|
2246
|
+
const response = await app.inject({
|
|
2247
|
+
method: 'PUT',
|
|
2248
|
+
url: '/${name}/1',
|
|
2249
|
+
payload: {
|
|
2250
|
+
// TODO: Add fields to update
|
|
2251
|
+
},
|
|
2252
|
+
});
|
|
2253
|
+
|
|
2254
|
+
expect(response.statusCode).toBe(200);
|
|
2255
|
+
expect(response.json()).toHaveProperty('data');
|
|
2256
|
+
});
|
|
2257
|
+
});
|
|
2258
|
+
|
|
2259
|
+
describe('DELETE /${name}/:id', () => {
|
|
2260
|
+
it('should delete a ${camelName}', async () => {
|
|
2261
|
+
// TODO: Create test ${camelName} first
|
|
2262
|
+
const response = await app.inject({
|
|
2263
|
+
method: 'DELETE',
|
|
2264
|
+
url: '/${name}/1',
|
|
2265
|
+
});
|
|
2266
|
+
|
|
2267
|
+
expect(response.statusCode).toBe(204);
|
|
2268
|
+
});
|
|
2269
|
+
});
|
|
2270
|
+
});
|
|
2271
|
+
`;
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
// src/cli/templates/service-test.ts
|
|
2275
|
+
function serviceTestTemplate(name, pascalName, camelName) {
|
|
2276
|
+
return `import { describe, it, expect, beforeEach } from 'vitest';
|
|
2277
|
+
import { ${pascalName}Service } from '../${name}.service.js';
|
|
2278
|
+
|
|
2279
|
+
describe('${pascalName}Service', () => {
|
|
2280
|
+
let service: ${pascalName}Service;
|
|
2281
|
+
|
|
2282
|
+
beforeEach(() => {
|
|
2283
|
+
service = new ${pascalName}Service();
|
|
2284
|
+
});
|
|
2285
|
+
|
|
2286
|
+
describe('getAll', () => {
|
|
2287
|
+
it('should return all ${name}', async () => {
|
|
2288
|
+
const result = await service.getAll();
|
|
2289
|
+
|
|
2290
|
+
expect(result).toBeDefined();
|
|
2291
|
+
expect(Array.isArray(result)).toBe(true);
|
|
2292
|
+
});
|
|
2293
|
+
|
|
2294
|
+
it('should apply pagination', async () => {
|
|
2295
|
+
const result = await service.getAll({ page: 1, limit: 10 });
|
|
2296
|
+
|
|
2297
|
+
expect(result).toBeDefined();
|
|
2298
|
+
expect(result.length).toBeLessThanOrEqual(10);
|
|
2299
|
+
});
|
|
2300
|
+
});
|
|
2301
|
+
|
|
2302
|
+
describe('getById', () => {
|
|
2303
|
+
it('should return a ${camelName} by id', async () => {
|
|
2304
|
+
// TODO: Create test ${camelName} first
|
|
2305
|
+
const id = '1';
|
|
2306
|
+
const result = await service.getById(id);
|
|
2307
|
+
|
|
2308
|
+
expect(result).toBeDefined();
|
|
2309
|
+
expect(result.id).toBe(id);
|
|
2310
|
+
});
|
|
2311
|
+
|
|
2312
|
+
it('should return null for non-existent id', async () => {
|
|
2313
|
+
const result = await service.getById('999999');
|
|
2314
|
+
|
|
2315
|
+
expect(result).toBeNull();
|
|
2316
|
+
});
|
|
2317
|
+
});
|
|
2318
|
+
|
|
2319
|
+
describe('create', () => {
|
|
2320
|
+
it('should create a new ${camelName}', async () => {
|
|
2321
|
+
const data = {
|
|
2322
|
+
// TODO: Add required fields
|
|
2323
|
+
};
|
|
2324
|
+
|
|
2325
|
+
const result = await service.create(data);
|
|
2326
|
+
|
|
2327
|
+
expect(result).toBeDefined();
|
|
2328
|
+
expect(result.id).toBeDefined();
|
|
2329
|
+
});
|
|
2330
|
+
|
|
2331
|
+
it('should throw error for invalid data', async () => {
|
|
2332
|
+
await expect(service.create({} as any)).rejects.toThrow();
|
|
2333
|
+
});
|
|
2334
|
+
});
|
|
2335
|
+
|
|
2336
|
+
describe('update', () => {
|
|
2337
|
+
it('should update a ${camelName}', async () => {
|
|
2338
|
+
// TODO: Create test ${camelName} first
|
|
2339
|
+
const id = '1';
|
|
2340
|
+
const updates = {
|
|
2341
|
+
// TODO: Add fields to update
|
|
2342
|
+
};
|
|
2343
|
+
|
|
2344
|
+
const result = await service.update(id, updates);
|
|
2345
|
+
|
|
2346
|
+
expect(result).toBeDefined();
|
|
2347
|
+
expect(result.id).toBe(id);
|
|
2348
|
+
});
|
|
2349
|
+
|
|
2350
|
+
it('should return null for non-existent id', async () => {
|
|
2351
|
+
const result = await service.update('999999', {});
|
|
2352
|
+
|
|
2353
|
+
expect(result).toBeNull();
|
|
2354
|
+
});
|
|
2355
|
+
});
|
|
2356
|
+
|
|
2357
|
+
describe('delete', () => {
|
|
2358
|
+
it('should delete a ${camelName}', async () => {
|
|
2359
|
+
// TODO: Create test ${camelName} first
|
|
2360
|
+
const id = '1';
|
|
2361
|
+
const result = await service.delete(id);
|
|
2362
|
+
|
|
2363
|
+
expect(result).toBe(true);
|
|
2364
|
+
});
|
|
2365
|
+
|
|
2366
|
+
it('should return false for non-existent id', async () => {
|
|
2367
|
+
const result = await service.delete('999999');
|
|
2368
|
+
|
|
2369
|
+
expect(result).toBe(false);
|
|
2370
|
+
});
|
|
2371
|
+
});
|
|
2372
|
+
});
|
|
2373
|
+
`;
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
// src/cli/templates/integration-test.ts
|
|
2377
|
+
function integrationTestTemplate(name, pascalName, camelName) {
|
|
2378
|
+
return `import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
|
|
2379
|
+
import { build } from '../../app.js';
|
|
2380
|
+
import { FastifyInstance } from 'fastify';
|
|
2381
|
+
import { prisma } from '../../lib/prisma.js';
|
|
2382
|
+
|
|
2383
|
+
describe('${pascalName} Integration Tests', () => {
|
|
2384
|
+
let app: FastifyInstance;
|
|
2385
|
+
|
|
2386
|
+
beforeAll(async () => {
|
|
2387
|
+
app = await build();
|
|
2388
|
+
await app.ready();
|
|
2389
|
+
});
|
|
2390
|
+
|
|
2391
|
+
afterAll(async () => {
|
|
2392
|
+
await app.close();
|
|
2393
|
+
await prisma.$disconnect();
|
|
2394
|
+
});
|
|
2395
|
+
|
|
2396
|
+
beforeEach(async () => {
|
|
2397
|
+
// Clean up test data
|
|
2398
|
+
// await prisma.${camelName}.deleteMany();
|
|
2399
|
+
});
|
|
2400
|
+
|
|
2401
|
+
describe('Full CRUD workflow', () => {
|
|
2402
|
+
it('should create, read, update, and delete a ${camelName}', async () => {
|
|
2403
|
+
// Create
|
|
2404
|
+
const createResponse = await app.inject({
|
|
2405
|
+
method: 'POST',
|
|
2406
|
+
url: '/${name}',
|
|
2407
|
+
payload: {
|
|
2408
|
+
// TODO: Add required fields
|
|
2409
|
+
},
|
|
2410
|
+
});
|
|
2411
|
+
|
|
2412
|
+
expect(createResponse.statusCode).toBe(201);
|
|
2413
|
+
const created = createResponse.json().data;
|
|
2414
|
+
expect(created.id).toBeDefined();
|
|
2415
|
+
|
|
2416
|
+
// Read
|
|
2417
|
+
const readResponse = await app.inject({
|
|
2418
|
+
method: 'GET',
|
|
2419
|
+
url: \`/${name}/\${created.id}\`,
|
|
2420
|
+
});
|
|
2421
|
+
|
|
2422
|
+
expect(readResponse.statusCode).toBe(200);
|
|
2423
|
+
const read = readResponse.json().data;
|
|
2424
|
+
expect(read.id).toBe(created.id);
|
|
2425
|
+
|
|
2426
|
+
// Update
|
|
2427
|
+
const updateResponse = await app.inject({
|
|
2428
|
+
method: 'PUT',
|
|
2429
|
+
url: \`/${name}/\${created.id}\`,
|
|
2430
|
+
payload: {
|
|
2431
|
+
// TODO: Add fields to update
|
|
2432
|
+
},
|
|
2433
|
+
});
|
|
2434
|
+
|
|
2435
|
+
expect(updateResponse.statusCode).toBe(200);
|
|
2436
|
+
const updated = updateResponse.json().data;
|
|
2437
|
+
expect(updated.id).toBe(created.id);
|
|
2438
|
+
|
|
2439
|
+
// Delete
|
|
2440
|
+
const deleteResponse = await app.inject({
|
|
2441
|
+
method: 'DELETE',
|
|
2442
|
+
url: \`/${name}/\${created.id}\`,
|
|
2443
|
+
});
|
|
2444
|
+
|
|
2445
|
+
expect(deleteResponse.statusCode).toBe(204);
|
|
2446
|
+
|
|
2447
|
+
// Verify deletion
|
|
2448
|
+
const verifyResponse = await app.inject({
|
|
2449
|
+
method: 'GET',
|
|
2450
|
+
url: \`/${name}/\${created.id}\`,
|
|
2451
|
+
});
|
|
2452
|
+
|
|
2453
|
+
expect(verifyResponse.statusCode).toBe(404);
|
|
2454
|
+
});
|
|
2455
|
+
});
|
|
2456
|
+
|
|
2457
|
+
describe('List and pagination', () => {
|
|
2458
|
+
it('should list ${name} with pagination', async () => {
|
|
2459
|
+
// Create multiple ${name}
|
|
2460
|
+
const count = 5;
|
|
2461
|
+
for (let i = 0; i < count; i++) {
|
|
2462
|
+
await app.inject({
|
|
2463
|
+
method: 'POST',
|
|
2464
|
+
url: '/${name}',
|
|
2465
|
+
payload: {
|
|
2466
|
+
// TODO: Add required fields
|
|
2467
|
+
},
|
|
2468
|
+
});
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
// Test pagination
|
|
2472
|
+
const response = await app.inject({
|
|
2473
|
+
method: 'GET',
|
|
2474
|
+
url: '/${name}?page=1&limit=3',
|
|
2475
|
+
});
|
|
2476
|
+
|
|
2477
|
+
expect(response.statusCode).toBe(200);
|
|
2478
|
+
const result = response.json();
|
|
2479
|
+
expect(result.data).toBeDefined();
|
|
2480
|
+
expect(result.data.length).toBeLessThanOrEqual(3);
|
|
2481
|
+
expect(result.total).toBeGreaterThanOrEqual(count);
|
|
2482
|
+
});
|
|
2483
|
+
});
|
|
2484
|
+
|
|
2485
|
+
describe('Validation', () => {
|
|
2486
|
+
it('should validate required fields on create', async () => {
|
|
2487
|
+
const response = await app.inject({
|
|
2488
|
+
method: 'POST',
|
|
2489
|
+
url: '/${name}',
|
|
2490
|
+
payload: {},
|
|
2491
|
+
});
|
|
2492
|
+
|
|
2493
|
+
expect(response.statusCode).toBe(400);
|
|
2494
|
+
expect(response.json()).toHaveProperty('error');
|
|
2495
|
+
});
|
|
2496
|
+
|
|
2497
|
+
it('should validate data types', async () => {
|
|
2498
|
+
const response = await app.inject({
|
|
2499
|
+
method: 'POST',
|
|
2500
|
+
url: '/${name}',
|
|
2501
|
+
payload: {
|
|
2502
|
+
// TODO: Add invalid field types
|
|
2503
|
+
},
|
|
2504
|
+
});
|
|
2505
|
+
|
|
2506
|
+
expect(response.statusCode).toBe(400);
|
|
2507
|
+
});
|
|
2508
|
+
});
|
|
2509
|
+
});
|
|
2510
|
+
`;
|
|
2511
|
+
}
|
|
2512
|
+
|
|
1958
2513
|
// src/cli/commands/generate.ts
|
|
2514
|
+
function enableDryRunIfNeeded(options) {
|
|
2515
|
+
const dryRun = DryRunManager.getInstance();
|
|
2516
|
+
if (options.dryRun) {
|
|
2517
|
+
dryRun.enable();
|
|
2518
|
+
console.log(import_chalk5.default.yellow("\n\u26A0 DRY RUN MODE - No files will be written\n"));
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
function showDryRunSummary(options) {
|
|
2522
|
+
if (options.dryRun) {
|
|
2523
|
+
DryRunManager.getInstance().printSummary();
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
1959
2526
|
var generateCommand = new import_commander2.Command("generate").alias("g").description("Generate resources (module, controller, service, etc.)");
|
|
1960
2527
|
generateCommand.command("module <name> [fields...]").alias("m").description(
|
|
1961
2528
|
"Generate a complete module with controller, service, repository, types, schemas, and routes"
|
|
1962
|
-
).option("--no-routes", "Skip routes generation").option("--no-repository", "Skip repository generation").option("--prisma", "Generate Prisma model suggestion").option("--validator <type>", "Validator type: zod, joi, yup", "zod").option("-i, --interactive", "Interactive mode to define fields").action(async (name, fieldsArgs, options) => {
|
|
2529
|
+
).option("--no-routes", "Skip routes generation").option("--no-repository", "Skip repository generation").option("--prisma", "Generate Prisma model suggestion").option("--validator <type>", "Validator type: zod, joi, yup", "zod").option("-i, --interactive", "Interactive mode to define fields").option("--with-tests", "Generate test files (__tests__ directory)").option("--dry-run", "Preview changes without writing files").action(async (name, fieldsArgs, options) => {
|
|
2530
|
+
enableDryRunIfNeeded(options);
|
|
1963
2531
|
let fields = [];
|
|
1964
2532
|
if (options.interactive) {
|
|
1965
2533
|
fields = await promptForFields();
|
|
@@ -1974,7 +2542,7 @@ generateCommand.command("module <name> [fields...]").alias("m").description(
|
|
|
1974
2542
|
const pluralName = pluralize(kebabName);
|
|
1975
2543
|
const tableName = pluralize(kebabName.replace(/-/g, "_"));
|
|
1976
2544
|
const validatorType = options.validator || "zod";
|
|
1977
|
-
const moduleDir =
|
|
2545
|
+
const moduleDir = import_path4.default.join(getModulesDir(), kebabName);
|
|
1978
2546
|
if (await fileExists(moduleDir)) {
|
|
1979
2547
|
spinner.stop();
|
|
1980
2548
|
error(`Module "${kebabName}" already exists`);
|
|
@@ -2013,7 +2581,22 @@ generateCommand.command("module <name> [fields...]").alias("m").description(
|
|
|
2013
2581
|
});
|
|
2014
2582
|
}
|
|
2015
2583
|
for (const file of files) {
|
|
2016
|
-
await writeFile(
|
|
2584
|
+
await writeFile(import_path4.default.join(moduleDir, file.name), file.content);
|
|
2585
|
+
}
|
|
2586
|
+
if (options.withTests) {
|
|
2587
|
+
const testDir = import_path4.default.join(moduleDir, "__tests__");
|
|
2588
|
+
await writeFile(
|
|
2589
|
+
import_path4.default.join(testDir, `${kebabName}.controller.test.ts`),
|
|
2590
|
+
controllerTestTemplate(kebabName, pascalName, camelName)
|
|
2591
|
+
);
|
|
2592
|
+
await writeFile(
|
|
2593
|
+
import_path4.default.join(testDir, `${kebabName}.service.test.ts`),
|
|
2594
|
+
serviceTestTemplate(kebabName, pascalName, camelName)
|
|
2595
|
+
);
|
|
2596
|
+
await writeFile(
|
|
2597
|
+
import_path4.default.join(testDir, `${kebabName}.integration.test.ts`),
|
|
2598
|
+
integrationTestTemplate(kebabName, pascalName, camelName)
|
|
2599
|
+
);
|
|
2017
2600
|
}
|
|
2018
2601
|
spinner.succeed(`Module "${pascalName}" generated successfully!`);
|
|
2019
2602
|
if (options.prisma || hasFields) {
|
|
@@ -2038,6 +2621,11 @@ generateCommand.command("module <name> [fields...]").alias("m").description(
|
|
|
2038
2621
|
}
|
|
2039
2622
|
console.log("\n\u{1F4C1} Files created:");
|
|
2040
2623
|
files.forEach((f) => success(` src/modules/${kebabName}/${f.name}`));
|
|
2624
|
+
if (options.withTests) {
|
|
2625
|
+
success(` src/modules/${kebabName}/__tests__/${kebabName}.controller.test.ts`);
|
|
2626
|
+
success(` src/modules/${kebabName}/__tests__/${kebabName}.service.test.ts`);
|
|
2627
|
+
success(` src/modules/${kebabName}/__tests__/${kebabName}.integration.test.ts`);
|
|
2628
|
+
}
|
|
2041
2629
|
console.log("\n\u{1F4CC} Next steps:");
|
|
2042
2630
|
if (!hasFields) {
|
|
2043
2631
|
info(` 1. Update the types in ${kebabName}.types.ts`);
|
|
@@ -2051,42 +2639,46 @@ generateCommand.command("module <name> [fields...]").alias("m").description(
|
|
|
2051
2639
|
info(` ${hasFields ? "3" : "4"}. Add the Prisma model to schema.prisma`);
|
|
2052
2640
|
info(` ${hasFields ? "4" : "5"}. Run: npm run db:migrate`);
|
|
2053
2641
|
}
|
|
2642
|
+
showDryRunSummary(options);
|
|
2054
2643
|
} catch (err) {
|
|
2055
2644
|
spinner.fail("Failed to generate module");
|
|
2056
2645
|
error(err instanceof Error ? err.message : String(err));
|
|
2057
2646
|
}
|
|
2058
2647
|
});
|
|
2059
|
-
generateCommand.command("controller <name>").alias("c").description("Generate a controller").option("-m, --module <module>", "Target module name").action(async (name, options) => {
|
|
2648
|
+
generateCommand.command("controller <name>").alias("c").description("Generate a controller").option("-m, --module <module>", "Target module name").option("--dry-run", "Preview changes without writing files").action(async (name, options) => {
|
|
2649
|
+
enableDryRunIfNeeded(options);
|
|
2060
2650
|
const spinner = (0, import_ora2.default)("Generating controller...").start();
|
|
2061
2651
|
try {
|
|
2062
2652
|
const kebabName = toKebabCase(name);
|
|
2063
2653
|
const pascalName = toPascalCase(name);
|
|
2064
2654
|
const camelName = toCamelCase(name);
|
|
2065
2655
|
const moduleName = options.module ? toKebabCase(options.module) : kebabName;
|
|
2066
|
-
const moduleDir =
|
|
2067
|
-
const filePath =
|
|
2656
|
+
const moduleDir = import_path4.default.join(getModulesDir(), moduleName);
|
|
2657
|
+
const filePath = import_path4.default.join(moduleDir, `${kebabName}.controller.ts`);
|
|
2068
2658
|
if (await fileExists(filePath)) {
|
|
2069
2659
|
spinner.stop();
|
|
2070
|
-
|
|
2660
|
+
displayError(ErrorTypes.FILE_ALREADY_EXISTS(`${kebabName}.controller.ts`));
|
|
2071
2661
|
return;
|
|
2072
2662
|
}
|
|
2073
2663
|
await writeFile(filePath, controllerTemplate(kebabName, pascalName, camelName));
|
|
2074
2664
|
spinner.succeed(`Controller "${pascalName}Controller" generated!`);
|
|
2075
2665
|
success(` src/modules/${moduleName}/${kebabName}.controller.ts`);
|
|
2666
|
+
showDryRunSummary(options);
|
|
2076
2667
|
} catch (err) {
|
|
2077
2668
|
spinner.fail("Failed to generate controller");
|
|
2078
2669
|
error(err instanceof Error ? err.message : String(err));
|
|
2079
2670
|
}
|
|
2080
2671
|
});
|
|
2081
|
-
generateCommand.command("service <name>").alias("s").description("Generate a service").option("-m, --module <module>", "Target module name").action(async (name, options) => {
|
|
2672
|
+
generateCommand.command("service <name>").alias("s").description("Generate a service").option("-m, --module <module>", "Target module name").option("--dry-run", "Preview changes without writing files").action(async (name, options) => {
|
|
2673
|
+
enableDryRunIfNeeded(options);
|
|
2082
2674
|
const spinner = (0, import_ora2.default)("Generating service...").start();
|
|
2083
2675
|
try {
|
|
2084
2676
|
const kebabName = toKebabCase(name);
|
|
2085
2677
|
const pascalName = toPascalCase(name);
|
|
2086
2678
|
const camelName = toCamelCase(name);
|
|
2087
2679
|
const moduleName = options.module ? toKebabCase(options.module) : kebabName;
|
|
2088
|
-
const moduleDir =
|
|
2089
|
-
const filePath =
|
|
2680
|
+
const moduleDir = import_path4.default.join(getModulesDir(), moduleName);
|
|
2681
|
+
const filePath = import_path4.default.join(moduleDir, `${kebabName}.service.ts`);
|
|
2090
2682
|
if (await fileExists(filePath)) {
|
|
2091
2683
|
spinner.stop();
|
|
2092
2684
|
error(`Service "${kebabName}" already exists`);
|
|
@@ -2095,12 +2687,14 @@ generateCommand.command("service <name>").alias("s").description("Generate a ser
|
|
|
2095
2687
|
await writeFile(filePath, serviceTemplate(kebabName, pascalName, camelName));
|
|
2096
2688
|
spinner.succeed(`Service "${pascalName}Service" generated!`);
|
|
2097
2689
|
success(` src/modules/${moduleName}/${kebabName}.service.ts`);
|
|
2690
|
+
showDryRunSummary(options);
|
|
2098
2691
|
} catch (err) {
|
|
2099
2692
|
spinner.fail("Failed to generate service");
|
|
2100
2693
|
error(err instanceof Error ? err.message : String(err));
|
|
2101
2694
|
}
|
|
2102
2695
|
});
|
|
2103
|
-
generateCommand.command("repository <name>").alias("r").description("Generate a repository").option("-m, --module <module>", "Target module name").action(async (name, options) => {
|
|
2696
|
+
generateCommand.command("repository <name>").alias("r").description("Generate a repository").option("-m, --module <module>", "Target module name").option("--dry-run", "Preview changes without writing files").action(async (name, options) => {
|
|
2697
|
+
enableDryRunIfNeeded(options);
|
|
2104
2698
|
const spinner = (0, import_ora2.default)("Generating repository...").start();
|
|
2105
2699
|
try {
|
|
2106
2700
|
const kebabName = toKebabCase(name);
|
|
@@ -2108,8 +2702,8 @@ generateCommand.command("repository <name>").alias("r").description("Generate a
|
|
|
2108
2702
|
const camelName = toCamelCase(name);
|
|
2109
2703
|
const pluralName = pluralize(kebabName);
|
|
2110
2704
|
const moduleName = options.module ? toKebabCase(options.module) : kebabName;
|
|
2111
|
-
const moduleDir =
|
|
2112
|
-
const filePath =
|
|
2705
|
+
const moduleDir = import_path4.default.join(getModulesDir(), moduleName);
|
|
2706
|
+
const filePath = import_path4.default.join(moduleDir, `${kebabName}.repository.ts`);
|
|
2113
2707
|
if (await fileExists(filePath)) {
|
|
2114
2708
|
spinner.stop();
|
|
2115
2709
|
error(`Repository "${kebabName}" already exists`);
|
|
@@ -2118,19 +2712,21 @@ generateCommand.command("repository <name>").alias("r").description("Generate a
|
|
|
2118
2712
|
await writeFile(filePath, repositoryTemplate(kebabName, pascalName, camelName, pluralName));
|
|
2119
2713
|
spinner.succeed(`Repository "${pascalName}Repository" generated!`);
|
|
2120
2714
|
success(` src/modules/${moduleName}/${kebabName}.repository.ts`);
|
|
2715
|
+
showDryRunSummary(options);
|
|
2121
2716
|
} catch (err) {
|
|
2122
2717
|
spinner.fail("Failed to generate repository");
|
|
2123
2718
|
error(err instanceof Error ? err.message : String(err));
|
|
2124
2719
|
}
|
|
2125
2720
|
});
|
|
2126
|
-
generateCommand.command("types <name>").alias("t").description("Generate types/interfaces").option("-m, --module <module>", "Target module name").action(async (name, options) => {
|
|
2721
|
+
generateCommand.command("types <name>").alias("t").description("Generate types/interfaces").option("-m, --module <module>", "Target module name").option("--dry-run", "Preview changes without writing files").action(async (name, options) => {
|
|
2722
|
+
enableDryRunIfNeeded(options);
|
|
2127
2723
|
const spinner = (0, import_ora2.default)("Generating types...").start();
|
|
2128
2724
|
try {
|
|
2129
2725
|
const kebabName = toKebabCase(name);
|
|
2130
2726
|
const pascalName = toPascalCase(name);
|
|
2131
2727
|
const moduleName = options.module ? toKebabCase(options.module) : kebabName;
|
|
2132
|
-
const moduleDir =
|
|
2133
|
-
const filePath =
|
|
2728
|
+
const moduleDir = import_path4.default.join(getModulesDir(), moduleName);
|
|
2729
|
+
const filePath = import_path4.default.join(moduleDir, `${kebabName}.types.ts`);
|
|
2134
2730
|
if (await fileExists(filePath)) {
|
|
2135
2731
|
spinner.stop();
|
|
2136
2732
|
error(`Types file "${kebabName}.types.ts" already exists`);
|
|
@@ -2139,20 +2735,22 @@ generateCommand.command("types <name>").alias("t").description("Generate types/i
|
|
|
2139
2735
|
await writeFile(filePath, typesTemplate(kebabName, pascalName));
|
|
2140
2736
|
spinner.succeed(`Types for "${pascalName}" generated!`);
|
|
2141
2737
|
success(` src/modules/${moduleName}/${kebabName}.types.ts`);
|
|
2738
|
+
showDryRunSummary(options);
|
|
2142
2739
|
} catch (err) {
|
|
2143
2740
|
spinner.fail("Failed to generate types");
|
|
2144
2741
|
error(err instanceof Error ? err.message : String(err));
|
|
2145
2742
|
}
|
|
2146
2743
|
});
|
|
2147
|
-
generateCommand.command("schema <name>").alias("v").description("Generate validation schemas").option("-m, --module <module>", "Target module name").action(async (name, options) => {
|
|
2744
|
+
generateCommand.command("schema <name>").alias("v").description("Generate validation schemas").option("-m, --module <module>", "Target module name").option("--dry-run", "Preview changes without writing files").action(async (name, options) => {
|
|
2745
|
+
enableDryRunIfNeeded(options);
|
|
2148
2746
|
const spinner = (0, import_ora2.default)("Generating schemas...").start();
|
|
2149
2747
|
try {
|
|
2150
2748
|
const kebabName = toKebabCase(name);
|
|
2151
2749
|
const pascalName = toPascalCase(name);
|
|
2152
2750
|
const camelName = toCamelCase(name);
|
|
2153
2751
|
const moduleName = options.module ? toKebabCase(options.module) : kebabName;
|
|
2154
|
-
const moduleDir =
|
|
2155
|
-
const filePath =
|
|
2752
|
+
const moduleDir = import_path4.default.join(getModulesDir(), moduleName);
|
|
2753
|
+
const filePath = import_path4.default.join(moduleDir, `${kebabName}.schemas.ts`);
|
|
2156
2754
|
if (await fileExists(filePath)) {
|
|
2157
2755
|
spinner.stop();
|
|
2158
2756
|
error(`Schemas file "${kebabName}.schemas.ts" already exists`);
|
|
@@ -2161,12 +2759,14 @@ generateCommand.command("schema <name>").alias("v").description("Generate valida
|
|
|
2161
2759
|
await writeFile(filePath, schemasTemplate(kebabName, pascalName, camelName));
|
|
2162
2760
|
spinner.succeed(`Schemas for "${pascalName}" generated!`);
|
|
2163
2761
|
success(` src/modules/${moduleName}/${kebabName}.schemas.ts`);
|
|
2762
|
+
showDryRunSummary(options);
|
|
2164
2763
|
} catch (err) {
|
|
2165
2764
|
spinner.fail("Failed to generate schemas");
|
|
2166
2765
|
error(err instanceof Error ? err.message : String(err));
|
|
2167
2766
|
}
|
|
2168
2767
|
});
|
|
2169
|
-
generateCommand.command("routes <name>").description("Generate routes").option("-m, --module <module>", "Target module name").action(async (name, options) => {
|
|
2768
|
+
generateCommand.command("routes <name>").description("Generate routes").option("-m, --module <module>", "Target module name").option("--dry-run", "Preview changes without writing files").action(async (name, options) => {
|
|
2769
|
+
enableDryRunIfNeeded(options);
|
|
2170
2770
|
const spinner = (0, import_ora2.default)("Generating routes...").start();
|
|
2171
2771
|
try {
|
|
2172
2772
|
const kebabName = toKebabCase(name);
|
|
@@ -2174,8 +2774,8 @@ generateCommand.command("routes <name>").description("Generate routes").option("
|
|
|
2174
2774
|
const camelName = toCamelCase(name);
|
|
2175
2775
|
const pluralName = pluralize(kebabName);
|
|
2176
2776
|
const moduleName = options.module ? toKebabCase(options.module) : kebabName;
|
|
2177
|
-
const moduleDir =
|
|
2178
|
-
const filePath =
|
|
2777
|
+
const moduleDir = import_path4.default.join(getModulesDir(), moduleName);
|
|
2778
|
+
const filePath = import_path4.default.join(moduleDir, `${kebabName}.routes.ts`);
|
|
2179
2779
|
if (await fileExists(filePath)) {
|
|
2180
2780
|
spinner.stop();
|
|
2181
2781
|
error(`Routes file "${kebabName}.routes.ts" already exists`);
|
|
@@ -2184,6 +2784,7 @@ generateCommand.command("routes <name>").description("Generate routes").option("
|
|
|
2184
2784
|
await writeFile(filePath, routesTemplate(kebabName, pascalName, camelName, pluralName));
|
|
2185
2785
|
spinner.succeed(`Routes for "${pascalName}" generated!`);
|
|
2186
2786
|
success(` src/modules/${moduleName}/${kebabName}.routes.ts`);
|
|
2787
|
+
showDryRunSummary(options);
|
|
2187
2788
|
} catch (err) {
|
|
2188
2789
|
spinner.fail("Failed to generate routes");
|
|
2189
2790
|
error(err instanceof Error ? err.message : String(err));
|
|
@@ -2262,21 +2863,21 @@ async function promptForFields() {
|
|
|
2262
2863
|
|
|
2263
2864
|
// src/cli/commands/add-module.ts
|
|
2264
2865
|
var import_commander3 = require("commander");
|
|
2265
|
-
var
|
|
2866
|
+
var import_path5 = __toESM(require("path"), 1);
|
|
2266
2867
|
var import_ora3 = __toESM(require("ora"), 1);
|
|
2267
|
-
var
|
|
2868
|
+
var import_chalk7 = __toESM(require("chalk"), 1);
|
|
2268
2869
|
var fs5 = __toESM(require("fs/promises"), 1);
|
|
2269
2870
|
|
|
2270
2871
|
// src/cli/utils/env-manager.ts
|
|
2271
2872
|
var fs3 = __toESM(require("fs/promises"), 1);
|
|
2272
|
-
var
|
|
2873
|
+
var path5 = __toESM(require("path"), 1);
|
|
2273
2874
|
var import_fs = require("fs");
|
|
2274
2875
|
var EnvManager = class {
|
|
2275
2876
|
envPath;
|
|
2276
2877
|
envExamplePath;
|
|
2277
2878
|
constructor(projectRoot) {
|
|
2278
|
-
this.envPath =
|
|
2279
|
-
this.envExamplePath =
|
|
2879
|
+
this.envPath = path5.join(projectRoot, ".env");
|
|
2880
|
+
this.envExamplePath = path5.join(projectRoot, ".env.example");
|
|
2280
2881
|
}
|
|
2281
2882
|
/**
|
|
2282
2883
|
* Add environment variables to .env file
|
|
@@ -2927,15 +3528,15 @@ var EnvManager = class {
|
|
|
2927
3528
|
|
|
2928
3529
|
// src/cli/utils/template-manager.ts
|
|
2929
3530
|
var fs4 = __toESM(require("fs/promises"), 1);
|
|
2930
|
-
var
|
|
3531
|
+
var path6 = __toESM(require("path"), 1);
|
|
2931
3532
|
var import_crypto = require("crypto");
|
|
2932
3533
|
var import_fs2 = require("fs");
|
|
2933
3534
|
var TemplateManager = class {
|
|
2934
3535
|
templatesDir;
|
|
2935
3536
|
manifestsDir;
|
|
2936
3537
|
constructor(projectRoot) {
|
|
2937
|
-
this.templatesDir =
|
|
2938
|
-
this.manifestsDir =
|
|
3538
|
+
this.templatesDir = path6.join(projectRoot, ".servcraft", "templates");
|
|
3539
|
+
this.manifestsDir = path6.join(projectRoot, ".servcraft", "manifests");
|
|
2939
3540
|
}
|
|
2940
3541
|
/**
|
|
2941
3542
|
* Initialize template system
|
|
@@ -2949,10 +3550,10 @@ var TemplateManager = class {
|
|
|
2949
3550
|
*/
|
|
2950
3551
|
async saveTemplate(moduleName, files) {
|
|
2951
3552
|
await this.initialize();
|
|
2952
|
-
const moduleTemplateDir =
|
|
3553
|
+
const moduleTemplateDir = path6.join(this.templatesDir, moduleName);
|
|
2953
3554
|
await fs4.mkdir(moduleTemplateDir, { recursive: true });
|
|
2954
3555
|
for (const [fileName, content] of Object.entries(files)) {
|
|
2955
|
-
const filePath =
|
|
3556
|
+
const filePath = path6.join(moduleTemplateDir, fileName);
|
|
2956
3557
|
await fs4.writeFile(filePath, content, "utf-8");
|
|
2957
3558
|
}
|
|
2958
3559
|
}
|
|
@@ -2961,7 +3562,7 @@ var TemplateManager = class {
|
|
|
2961
3562
|
*/
|
|
2962
3563
|
async getTemplate(moduleName, fileName) {
|
|
2963
3564
|
try {
|
|
2964
|
-
const filePath =
|
|
3565
|
+
const filePath = path6.join(this.templatesDir, moduleName, fileName);
|
|
2965
3566
|
return await fs4.readFile(filePath, "utf-8");
|
|
2966
3567
|
} catch {
|
|
2967
3568
|
return null;
|
|
@@ -2986,7 +3587,7 @@ var TemplateManager = class {
|
|
|
2986
3587
|
installedAt: /* @__PURE__ */ new Date(),
|
|
2987
3588
|
updatedAt: /* @__PURE__ */ new Date()
|
|
2988
3589
|
};
|
|
2989
|
-
const manifestPath =
|
|
3590
|
+
const manifestPath = path6.join(this.manifestsDir, `${moduleName}.json`);
|
|
2990
3591
|
await fs4.writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
|
|
2991
3592
|
}
|
|
2992
3593
|
/**
|
|
@@ -2994,7 +3595,7 @@ var TemplateManager = class {
|
|
|
2994
3595
|
*/
|
|
2995
3596
|
async getManifest(moduleName) {
|
|
2996
3597
|
try {
|
|
2997
|
-
const manifestPath =
|
|
3598
|
+
const manifestPath = path6.join(this.manifestsDir, `${moduleName}.json`);
|
|
2998
3599
|
const content = await fs4.readFile(manifestPath, "utf-8");
|
|
2999
3600
|
return JSON.parse(content);
|
|
3000
3601
|
} catch {
|
|
@@ -3023,7 +3624,7 @@ var TemplateManager = class {
|
|
|
3023
3624
|
}
|
|
3024
3625
|
const results = [];
|
|
3025
3626
|
for (const [fileName, fileInfo] of Object.entries(manifest.files)) {
|
|
3026
|
-
const filePath =
|
|
3627
|
+
const filePath = path6.join(moduleDir, fileName);
|
|
3027
3628
|
if (!(0, import_fs2.existsSync)(filePath)) {
|
|
3028
3629
|
results.push({
|
|
3029
3630
|
fileName,
|
|
@@ -3049,7 +3650,7 @@ var TemplateManager = class {
|
|
|
3049
3650
|
*/
|
|
3050
3651
|
async createBackup(moduleName, moduleDir) {
|
|
3051
3652
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").substring(0, 19);
|
|
3052
|
-
const backupDir =
|
|
3653
|
+
const backupDir = path6.join(path6.dirname(moduleDir), `${moduleName}.backup-${timestamp}`);
|
|
3053
3654
|
await this.copyDirectory(moduleDir, backupDir);
|
|
3054
3655
|
return backupDir;
|
|
3055
3656
|
}
|
|
@@ -3060,8 +3661,8 @@ var TemplateManager = class {
|
|
|
3060
3661
|
await fs4.mkdir(dest, { recursive: true });
|
|
3061
3662
|
const entries = await fs4.readdir(src, { withFileTypes: true });
|
|
3062
3663
|
for (const entry of entries) {
|
|
3063
|
-
const srcPath =
|
|
3064
|
-
const destPath =
|
|
3664
|
+
const srcPath = path6.join(src, entry.name);
|
|
3665
|
+
const destPath = path6.join(dest, entry.name);
|
|
3065
3666
|
if (entry.isDirectory()) {
|
|
3066
3667
|
await this.copyDirectory(srcPath, destPath);
|
|
3067
3668
|
} else {
|
|
@@ -3162,23 +3763,23 @@ var TemplateManager = class {
|
|
|
3162
3763
|
}
|
|
3163
3764
|
manifest.files = fileHashes;
|
|
3164
3765
|
manifest.updatedAt = /* @__PURE__ */ new Date();
|
|
3165
|
-
const manifestPath =
|
|
3766
|
+
const manifestPath = path6.join(this.manifestsDir, `${moduleName}.json`);
|
|
3166
3767
|
await fs4.writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
|
|
3167
3768
|
}
|
|
3168
3769
|
};
|
|
3169
3770
|
|
|
3170
3771
|
// src/cli/utils/interactive-prompt.ts
|
|
3171
3772
|
var import_inquirer3 = __toESM(require("inquirer"), 1);
|
|
3172
|
-
var
|
|
3773
|
+
var import_chalk6 = __toESM(require("chalk"), 1);
|
|
3173
3774
|
var InteractivePrompt = class {
|
|
3174
3775
|
/**
|
|
3175
3776
|
* Ask what to do when module already exists
|
|
3176
3777
|
*/
|
|
3177
3778
|
static async askModuleExists(moduleName, hasModifications) {
|
|
3178
|
-
console.log(
|
|
3779
|
+
console.log(import_chalk6.default.yellow(`
|
|
3179
3780
|
\u26A0\uFE0F Module "${moduleName}" already exists`));
|
|
3180
3781
|
if (hasModifications) {
|
|
3181
|
-
console.log(
|
|
3782
|
+
console.log(import_chalk6.default.yellow(" Some files have been modified by you.\n"));
|
|
3182
3783
|
}
|
|
3183
3784
|
const { action } = await import_inquirer3.default.prompt([
|
|
3184
3785
|
{
|
|
@@ -3216,12 +3817,12 @@ var InteractivePrompt = class {
|
|
|
3216
3817
|
* Ask what to do with a specific file
|
|
3217
3818
|
*/
|
|
3218
3819
|
static async askFileAction(fileName, isModified, yourLines, newLines) {
|
|
3219
|
-
console.log(
|
|
3820
|
+
console.log(import_chalk6.default.cyan(`
|
|
3220
3821
|
\u{1F4C1} ${fileName}`));
|
|
3221
3822
|
console.log(
|
|
3222
|
-
|
|
3823
|
+
import_chalk6.default.gray(` Your version: ${yourLines} lines${isModified ? " (modified)" : ""}`)
|
|
3223
3824
|
);
|
|
3224
|
-
console.log(
|
|
3825
|
+
console.log(import_chalk6.default.gray(` New version: ${newLines} lines
|
|
3225
3826
|
`));
|
|
3226
3827
|
const { action } = await import_inquirer3.default.prompt([
|
|
3227
3828
|
{
|
|
@@ -3273,7 +3874,7 @@ var InteractivePrompt = class {
|
|
|
3273
3874
|
* Display diff and ask to continue
|
|
3274
3875
|
*/
|
|
3275
3876
|
static async showDiffAndAsk(diff) {
|
|
3276
|
-
console.log(
|
|
3877
|
+
console.log(import_chalk6.default.cyan("\n\u{1F4CA} Differences:\n"));
|
|
3277
3878
|
console.log(diff);
|
|
3278
3879
|
return await this.confirm("\nDo you want to proceed with this change?", true);
|
|
3279
3880
|
}
|
|
@@ -3281,41 +3882,41 @@ var InteractivePrompt = class {
|
|
|
3281
3882
|
* Display merge conflicts
|
|
3282
3883
|
*/
|
|
3283
3884
|
static displayConflicts(conflicts) {
|
|
3284
|
-
console.log(
|
|
3885
|
+
console.log(import_chalk6.default.red("\n\u26A0\uFE0F Merge Conflicts Detected:\n"));
|
|
3285
3886
|
conflicts.forEach((conflict, i) => {
|
|
3286
|
-
console.log(
|
|
3887
|
+
console.log(import_chalk6.default.yellow(` ${i + 1}. ${conflict}`));
|
|
3287
3888
|
});
|
|
3288
|
-
console.log(
|
|
3289
|
-
console.log(
|
|
3290
|
-
console.log(
|
|
3291
|
-
console.log(
|
|
3292
|
-
console.log(
|
|
3293
|
-
console.log(
|
|
3889
|
+
console.log(import_chalk6.default.gray("\n Conflict markers have been added to the file:"));
|
|
3890
|
+
console.log(import_chalk6.default.gray(" <<<<<<< YOUR VERSION"));
|
|
3891
|
+
console.log(import_chalk6.default.gray(" ... your code ..."));
|
|
3892
|
+
console.log(import_chalk6.default.gray(" ======="));
|
|
3893
|
+
console.log(import_chalk6.default.gray(" ... new code ..."));
|
|
3894
|
+
console.log(import_chalk6.default.gray(" >>>>>>> NEW VERSION\n"));
|
|
3294
3895
|
}
|
|
3295
3896
|
/**
|
|
3296
3897
|
* Show backup location
|
|
3297
3898
|
*/
|
|
3298
3899
|
static showBackupCreated(backupPath) {
|
|
3299
|
-
console.log(
|
|
3300
|
-
\u2713 Backup created: ${
|
|
3900
|
+
console.log(import_chalk6.default.green(`
|
|
3901
|
+
\u2713 Backup created: ${import_chalk6.default.cyan(backupPath)}`));
|
|
3301
3902
|
}
|
|
3302
3903
|
/**
|
|
3303
3904
|
* Show merge summary
|
|
3304
3905
|
*/
|
|
3305
3906
|
static showMergeSummary(stats) {
|
|
3306
|
-
console.log(
|
|
3907
|
+
console.log(import_chalk6.default.bold("\n\u{1F4CA} Merge Summary:\n"));
|
|
3307
3908
|
if (stats.merged > 0) {
|
|
3308
|
-
console.log(
|
|
3909
|
+
console.log(import_chalk6.default.green(` \u2713 Merged: ${stats.merged} file(s)`));
|
|
3309
3910
|
}
|
|
3310
3911
|
if (stats.kept > 0) {
|
|
3311
|
-
console.log(
|
|
3912
|
+
console.log(import_chalk6.default.blue(` \u2192 Kept: ${stats.kept} file(s)`));
|
|
3312
3913
|
}
|
|
3313
3914
|
if (stats.overwritten > 0) {
|
|
3314
|
-
console.log(
|
|
3915
|
+
console.log(import_chalk6.default.yellow(` \u26A0 Overwritten: ${stats.overwritten} file(s)`));
|
|
3315
3916
|
}
|
|
3316
3917
|
if (stats.conflicts > 0) {
|
|
3317
|
-
console.log(
|
|
3318
|
-
console.log(
|
|
3918
|
+
console.log(import_chalk6.default.red(` \u26A0 Conflicts: ${stats.conflicts} file(s)`));
|
|
3919
|
+
console.log(import_chalk6.default.gray("\n Please resolve conflicts manually before committing.\n"));
|
|
3319
3920
|
}
|
|
3320
3921
|
}
|
|
3321
3922
|
/**
|
|
@@ -3487,31 +4088,40 @@ var AVAILABLE_MODULES = {
|
|
|
3487
4088
|
var addModuleCommand = new import_commander3.Command("add").description("Add a pre-built module to your project").argument(
|
|
3488
4089
|
"[module]",
|
|
3489
4090
|
"Module to add (auth, users, email, audit, upload, cache, notifications, settings)"
|
|
3490
|
-
).option("-l, --list", "List available modules").option("-f, --force", "Force overwrite existing module").option("-u, --update", "Update existing module (smart merge)").option("--skip-existing", "Skip if module already exists").action(
|
|
4091
|
+
).option("-l, --list", "List available modules").option("-f, --force", "Force overwrite existing module").option("-u, --update", "Update existing module (smart merge)").option("--skip-existing", "Skip if module already exists").option("--dry-run", "Preview changes without writing files").action(
|
|
3491
4092
|
async (moduleName, options) => {
|
|
3492
4093
|
if (options?.list || !moduleName) {
|
|
3493
|
-
console.log(
|
|
4094
|
+
console.log(import_chalk7.default.bold("\n\u{1F4E6} Available Modules:\n"));
|
|
3494
4095
|
for (const [key, mod] of Object.entries(AVAILABLE_MODULES)) {
|
|
3495
|
-
console.log(` ${
|
|
3496
|
-
console.log(` ${" ".repeat(15)} ${
|
|
4096
|
+
console.log(` ${import_chalk7.default.cyan(key.padEnd(15))} ${mod.name}`);
|
|
4097
|
+
console.log(` ${" ".repeat(15)} ${import_chalk7.default.gray(mod.description)}
|
|
3497
4098
|
`);
|
|
3498
4099
|
}
|
|
3499
|
-
console.log(
|
|
3500
|
-
console.log(` ${
|
|
3501
|
-
console.log(` ${
|
|
3502
|
-
console.log(` ${
|
|
4100
|
+
console.log(import_chalk7.default.bold("Usage:"));
|
|
4101
|
+
console.log(` ${import_chalk7.default.yellow("servcraft add auth")} Add authentication module`);
|
|
4102
|
+
console.log(` ${import_chalk7.default.yellow("servcraft add users")} Add user management module`);
|
|
4103
|
+
console.log(` ${import_chalk7.default.yellow("servcraft add email")} Add email service module
|
|
3503
4104
|
`);
|
|
3504
4105
|
return;
|
|
3505
4106
|
}
|
|
4107
|
+
const dryRun = DryRunManager.getInstance();
|
|
4108
|
+
if (options?.dryRun) {
|
|
4109
|
+
dryRun.enable();
|
|
4110
|
+
console.log(import_chalk7.default.yellow("\n\u26A0 DRY RUN MODE - No files will be written\n"));
|
|
4111
|
+
}
|
|
3506
4112
|
const module2 = AVAILABLE_MODULES[moduleName];
|
|
3507
4113
|
if (!module2) {
|
|
3508
|
-
|
|
3509
|
-
|
|
4114
|
+
displayError(ErrorTypes.MODULE_NOT_FOUND(moduleName));
|
|
4115
|
+
return;
|
|
4116
|
+
}
|
|
4117
|
+
const projectError = validateProject();
|
|
4118
|
+
if (projectError) {
|
|
4119
|
+
displayError(projectError);
|
|
3510
4120
|
return;
|
|
3511
4121
|
}
|
|
3512
4122
|
const spinner = (0, import_ora3.default)(`Adding ${module2.name} module...`).start();
|
|
3513
4123
|
try {
|
|
3514
|
-
const moduleDir =
|
|
4124
|
+
const moduleDir = import_path5.default.join(getModulesDir(), moduleName);
|
|
3515
4125
|
const templateManager = new TemplateManager(process.cwd());
|
|
3516
4126
|
const moduleExists = await fileExists(moduleDir);
|
|
3517
4127
|
if (moduleExists) {
|
|
@@ -3579,16 +4189,16 @@ var addModuleCommand = new import_commander3.Command("add").description("Add a p
|
|
|
3579
4189
|
info("\n\u{1F4DD} Created new .env file");
|
|
3580
4190
|
}
|
|
3581
4191
|
if (result.added.length > 0) {
|
|
3582
|
-
console.log(
|
|
4192
|
+
console.log(import_chalk7.default.bold("\n\u2705 Added to .env:"));
|
|
3583
4193
|
result.added.forEach((key) => success(` ${key}`));
|
|
3584
4194
|
}
|
|
3585
4195
|
if (result.skipped.length > 0) {
|
|
3586
|
-
console.log(
|
|
4196
|
+
console.log(import_chalk7.default.bold("\n\u23ED\uFE0F Already in .env (skipped):"));
|
|
3587
4197
|
result.skipped.forEach((key) => info(` ${key}`));
|
|
3588
4198
|
}
|
|
3589
4199
|
const requiredVars = envSections.flatMap((section) => section.variables).filter((v) => v.required && !v.value).map((v) => v.key);
|
|
3590
4200
|
if (requiredVars.length > 0) {
|
|
3591
|
-
console.log(
|
|
4201
|
+
console.log(import_chalk7.default.bold("\n\u26A0\uFE0F Required configuration:"));
|
|
3592
4202
|
requiredVars.forEach((key) => warn(` ${key} - Please configure this variable`));
|
|
3593
4203
|
}
|
|
3594
4204
|
} catch (err) {
|
|
@@ -3596,10 +4206,15 @@ var addModuleCommand = new import_commander3.Command("add").description("Add a p
|
|
|
3596
4206
|
error(err instanceof Error ? err.message : String(err));
|
|
3597
4207
|
}
|
|
3598
4208
|
}
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
4209
|
+
if (!options?.dryRun) {
|
|
4210
|
+
console.log("\n\u{1F4CC} Next steps:");
|
|
4211
|
+
info(" 1. Configure environment variables in .env (if needed)");
|
|
4212
|
+
info(" 2. Register the module in your main app file");
|
|
4213
|
+
info(" 3. Run database migrations if needed");
|
|
4214
|
+
}
|
|
4215
|
+
if (options?.dryRun) {
|
|
4216
|
+
dryRun.printSummary();
|
|
4217
|
+
}
|
|
3603
4218
|
} catch (err) {
|
|
3604
4219
|
spinner.fail("Failed to add module");
|
|
3605
4220
|
error(err instanceof Error ? err.message : String(err));
|
|
@@ -3650,7 +4265,7 @@ export * from './auth.schemas.js';
|
|
|
3650
4265
|
`
|
|
3651
4266
|
};
|
|
3652
4267
|
for (const [name, content] of Object.entries(files)) {
|
|
3653
|
-
await writeFile(
|
|
4268
|
+
await writeFile(import_path5.default.join(dir, name), content);
|
|
3654
4269
|
}
|
|
3655
4270
|
}
|
|
3656
4271
|
async function generateUsersModule(dir) {
|
|
@@ -3690,7 +4305,7 @@ export * from './user.schemas.js';
|
|
|
3690
4305
|
`
|
|
3691
4306
|
};
|
|
3692
4307
|
for (const [name, content] of Object.entries(files)) {
|
|
3693
|
-
await writeFile(
|
|
4308
|
+
await writeFile(import_path5.default.join(dir, name), content);
|
|
3694
4309
|
}
|
|
3695
4310
|
}
|
|
3696
4311
|
async function generateEmailModule(dir) {
|
|
@@ -3747,7 +4362,7 @@ export { EmailService, emailService } from './email.service.js';
|
|
|
3747
4362
|
`
|
|
3748
4363
|
};
|
|
3749
4364
|
for (const [name, content] of Object.entries(files)) {
|
|
3750
|
-
await writeFile(
|
|
4365
|
+
await writeFile(import_path5.default.join(dir, name), content);
|
|
3751
4366
|
}
|
|
3752
4367
|
}
|
|
3753
4368
|
async function generateAuditModule(dir) {
|
|
@@ -3791,7 +4406,7 @@ export { AuditService, auditService } from './audit.service.js';
|
|
|
3791
4406
|
`
|
|
3792
4407
|
};
|
|
3793
4408
|
for (const [name, content] of Object.entries(files)) {
|
|
3794
|
-
await writeFile(
|
|
4409
|
+
await writeFile(import_path5.default.join(dir, name), content);
|
|
3795
4410
|
}
|
|
3796
4411
|
}
|
|
3797
4412
|
async function generateUploadModule(dir) {
|
|
@@ -3817,7 +4432,7 @@ export interface UploadOptions {
|
|
|
3817
4432
|
`
|
|
3818
4433
|
};
|
|
3819
4434
|
for (const [name, content] of Object.entries(files)) {
|
|
3820
|
-
await writeFile(
|
|
4435
|
+
await writeFile(import_path5.default.join(dir, name), content);
|
|
3821
4436
|
}
|
|
3822
4437
|
}
|
|
3823
4438
|
async function generateCacheModule(dir) {
|
|
@@ -3863,7 +4478,7 @@ export { CacheService, cacheService } from './cache.service.js';
|
|
|
3863
4478
|
`
|
|
3864
4479
|
};
|
|
3865
4480
|
for (const [name, content] of Object.entries(files)) {
|
|
3866
|
-
await writeFile(
|
|
4481
|
+
await writeFile(import_path5.default.join(dir, name), content);
|
|
3867
4482
|
}
|
|
3868
4483
|
}
|
|
3869
4484
|
async function generateGenericModule(dir, name) {
|
|
@@ -3877,18 +4492,18 @@ export interface ${name.charAt(0).toUpperCase() + name.slice(1)}Data {
|
|
|
3877
4492
|
`
|
|
3878
4493
|
};
|
|
3879
4494
|
for (const [fileName, content] of Object.entries(files)) {
|
|
3880
|
-
await writeFile(
|
|
4495
|
+
await writeFile(import_path5.default.join(dir, fileName), content);
|
|
3881
4496
|
}
|
|
3882
4497
|
}
|
|
3883
4498
|
async function findServercraftModules() {
|
|
3884
|
-
const scriptDir =
|
|
4499
|
+
const scriptDir = import_path5.default.dirname(new URL(importMetaUrl).pathname);
|
|
3885
4500
|
const possiblePaths = [
|
|
3886
4501
|
// Local node_modules (when servcraft is a dependency)
|
|
3887
|
-
|
|
4502
|
+
import_path5.default.join(process.cwd(), "node_modules", "servcraft", "src", "modules"),
|
|
3888
4503
|
// From dist/cli/index.js -> src/modules (npx or global install)
|
|
3889
|
-
|
|
4504
|
+
import_path5.default.resolve(scriptDir, "..", "..", "src", "modules"),
|
|
3890
4505
|
// From src/cli/commands/add-module.ts -> src/modules (development)
|
|
3891
|
-
|
|
4506
|
+
import_path5.default.resolve(scriptDir, "..", "..", "modules")
|
|
3892
4507
|
];
|
|
3893
4508
|
for (const p of possiblePaths) {
|
|
3894
4509
|
try {
|
|
@@ -3912,7 +4527,7 @@ async function generateModuleFiles(moduleName, moduleDir) {
|
|
|
3912
4527
|
const sourceDirName = moduleNameMap[moduleName] || moduleName;
|
|
3913
4528
|
const servercraftModulesDir = await findServercraftModules();
|
|
3914
4529
|
if (servercraftModulesDir) {
|
|
3915
|
-
const sourceModuleDir =
|
|
4530
|
+
const sourceModuleDir = import_path5.default.join(servercraftModulesDir, sourceDirName);
|
|
3916
4531
|
if (await fileExists(sourceModuleDir)) {
|
|
3917
4532
|
await copyModuleFromSource(sourceModuleDir, moduleDir);
|
|
3918
4533
|
return;
|
|
@@ -3944,8 +4559,8 @@ async function generateModuleFiles(moduleName, moduleDir) {
|
|
|
3944
4559
|
async function copyModuleFromSource(sourceDir, targetDir) {
|
|
3945
4560
|
const entries = await fs5.readdir(sourceDir, { withFileTypes: true });
|
|
3946
4561
|
for (const entry of entries) {
|
|
3947
|
-
const sourcePath =
|
|
3948
|
-
const targetPath =
|
|
4562
|
+
const sourcePath = import_path5.default.join(sourceDir, entry.name);
|
|
4563
|
+
const targetPath = import_path5.default.join(targetDir, entry.name);
|
|
3949
4564
|
if (entry.isDirectory()) {
|
|
3950
4565
|
await fs5.mkdir(targetPath, { recursive: true });
|
|
3951
4566
|
await copyModuleFromSource(sourcePath, targetPath);
|
|
@@ -3958,7 +4573,7 @@ async function getModuleFiles(moduleName, moduleDir) {
|
|
|
3958
4573
|
const files = {};
|
|
3959
4574
|
const entries = await fs5.readdir(moduleDir);
|
|
3960
4575
|
for (const entry of entries) {
|
|
3961
|
-
const filePath =
|
|
4576
|
+
const filePath = import_path5.default.join(moduleDir, entry);
|
|
3962
4577
|
const stat2 = await fs5.stat(filePath);
|
|
3963
4578
|
if (stat2.isFile() && entry.endsWith(".ts")) {
|
|
3964
4579
|
const content = await fs5.readFile(filePath, "utf-8");
|
|
@@ -3969,14 +4584,14 @@ async function getModuleFiles(moduleName, moduleDir) {
|
|
|
3969
4584
|
}
|
|
3970
4585
|
async function showDiffForModule(templateManager, moduleName, moduleDir) {
|
|
3971
4586
|
const modifiedFiles = await templateManager.getModifiedFiles(moduleName, moduleDir);
|
|
3972
|
-
console.log(
|
|
4587
|
+
console.log(import_chalk7.default.cyan(`
|
|
3973
4588
|
\u{1F4CA} Changes in module "${moduleName}":
|
|
3974
4589
|
`));
|
|
3975
4590
|
for (const file of modifiedFiles) {
|
|
3976
4591
|
if (file.isModified) {
|
|
3977
|
-
console.log(
|
|
4592
|
+
console.log(import_chalk7.default.yellow(`
|
|
3978
4593
|
\u{1F4C4} ${file.fileName}:`));
|
|
3979
|
-
const currentPath =
|
|
4594
|
+
const currentPath = import_path5.default.join(moduleDir, file.fileName);
|
|
3980
4595
|
const currentContent = await fs5.readFile(currentPath, "utf-8");
|
|
3981
4596
|
const originalContent = await templateManager.getTemplate(moduleName, file.fileName);
|
|
3982
4597
|
if (originalContent) {
|
|
@@ -3989,11 +4604,11 @@ async function showDiffForModule(templateManager, moduleName, moduleDir) {
|
|
|
3989
4604
|
async function performSmartMerge(templateManager, moduleName, moduleDir, _displayName) {
|
|
3990
4605
|
const spinner = (0, import_ora3.default)("Analyzing files for merge...").start();
|
|
3991
4606
|
const newFiles = {};
|
|
3992
|
-
const templateDir =
|
|
4607
|
+
const templateDir = import_path5.default.join(templateManager["templatesDir"], moduleName);
|
|
3993
4608
|
try {
|
|
3994
4609
|
const entries = await fs5.readdir(templateDir);
|
|
3995
4610
|
for (const entry of entries) {
|
|
3996
|
-
const content = await fs5.readFile(
|
|
4611
|
+
const content = await fs5.readFile(import_path5.default.join(templateDir, entry), "utf-8");
|
|
3997
4612
|
newFiles[entry] = content;
|
|
3998
4613
|
}
|
|
3999
4614
|
} catch {
|
|
@@ -4011,7 +4626,7 @@ async function performSmartMerge(templateManager, moduleName, moduleDir, _displa
|
|
|
4011
4626
|
};
|
|
4012
4627
|
for (const fileInfo of modifiedFiles) {
|
|
4013
4628
|
const fileName = fileInfo.fileName;
|
|
4014
|
-
const filePath =
|
|
4629
|
+
const filePath = import_path5.default.join(moduleDir, fileName);
|
|
4015
4630
|
const newContent = newFiles[fileName];
|
|
4016
4631
|
if (!newContent) {
|
|
4017
4632
|
continue;
|
|
@@ -4083,7 +4698,7 @@ async function performSmartMerge(templateManager, moduleName, moduleDir, _displa
|
|
|
4083
4698
|
var import_commander4 = require("commander");
|
|
4084
4699
|
var import_child_process2 = require("child_process");
|
|
4085
4700
|
var import_ora4 = __toESM(require("ora"), 1);
|
|
4086
|
-
var
|
|
4701
|
+
var import_chalk8 = __toESM(require("chalk"), 1);
|
|
4087
4702
|
var dbCommand = new import_commander4.Command("db").description("Database management commands");
|
|
4088
4703
|
dbCommand.command("migrate").description("Run database migrations").option("-n, --name <name>", "Migration name").action(async (options) => {
|
|
4089
4704
|
const spinner = (0, import_ora4.default)("Running migrations...").start();
|
|
@@ -4140,7 +4755,7 @@ dbCommand.command("seed").description("Run database seed").action(async () => {
|
|
|
4140
4755
|
});
|
|
4141
4756
|
dbCommand.command("reset").description("Reset database (drop all data and re-run migrations)").option("-f, --force", "Skip confirmation").action(async (options) => {
|
|
4142
4757
|
if (!options.force) {
|
|
4143
|
-
console.log(
|
|
4758
|
+
console.log(import_chalk8.default.yellow("\n\u26A0\uFE0F WARNING: This will delete all data in your database!\n"));
|
|
4144
4759
|
const readline = await import("readline");
|
|
4145
4760
|
const rl = readline.createInterface({
|
|
4146
4761
|
input: process.stdin,
|
|
@@ -4174,14 +4789,14 @@ dbCommand.command("status").description("Show migration status").action(async ()
|
|
|
4174
4789
|
|
|
4175
4790
|
// src/cli/commands/docs.ts
|
|
4176
4791
|
var import_commander5 = require("commander");
|
|
4177
|
-
var
|
|
4792
|
+
var import_path7 = __toESM(require("path"), 1);
|
|
4178
4793
|
var import_promises4 = __toESM(require("fs/promises"), 1);
|
|
4179
4794
|
var import_ora6 = __toESM(require("ora"), 1);
|
|
4180
|
-
var
|
|
4795
|
+
var import_chalk9 = __toESM(require("chalk"), 1);
|
|
4181
4796
|
|
|
4182
4797
|
// src/cli/utils/docs-generator.ts
|
|
4183
4798
|
var import_promises3 = __toESM(require("fs/promises"), 1);
|
|
4184
|
-
var
|
|
4799
|
+
var import_path6 = __toESM(require("path"), 1);
|
|
4185
4800
|
var import_ora5 = __toESM(require("ora"), 1);
|
|
4186
4801
|
|
|
4187
4802
|
// src/core/server.ts
|
|
@@ -4902,11 +5517,11 @@ function validateQuery(schema, data) {
|
|
|
4902
5517
|
function formatZodErrors(error2) {
|
|
4903
5518
|
const errors = {};
|
|
4904
5519
|
for (const issue of error2.issues) {
|
|
4905
|
-
const
|
|
4906
|
-
if (!errors[
|
|
4907
|
-
errors[
|
|
5520
|
+
const path12 = issue.path.join(".") || "root";
|
|
5521
|
+
if (!errors[path12]) {
|
|
5522
|
+
errors[path12] = [];
|
|
4908
5523
|
}
|
|
4909
|
-
errors[
|
|
5524
|
+
errors[path12].push(issue.message);
|
|
4910
5525
|
}
|
|
4911
5526
|
return errors;
|
|
4912
5527
|
}
|
|
@@ -5739,8 +6354,8 @@ async function generateDocs(outputPath = "openapi.json", silent = false) {
|
|
|
5739
6354
|
await registerUserModule(app, authService);
|
|
5740
6355
|
await app.ready();
|
|
5741
6356
|
const spec = app.swagger();
|
|
5742
|
-
const absoluteOutput =
|
|
5743
|
-
await import_promises3.default.mkdir(
|
|
6357
|
+
const absoluteOutput = import_path6.default.resolve(outputPath);
|
|
6358
|
+
await import_promises3.default.mkdir(import_path6.default.dirname(absoluteOutput), { recursive: true });
|
|
5744
6359
|
await import_promises3.default.writeFile(absoluteOutput, JSON.stringify(spec, null, 2), "utf8");
|
|
5745
6360
|
spinner?.succeed(`OpenAPI spec generated at ${absoluteOutput}`);
|
|
5746
6361
|
await app.close();
|
|
@@ -5785,7 +6400,7 @@ docsCommand.command("export").description("Export documentation to Postman, Inso
|
|
|
5785
6400
|
const spinner = (0, import_ora6.default)("Exporting documentation...").start();
|
|
5786
6401
|
try {
|
|
5787
6402
|
const projectRoot = getProjectRoot();
|
|
5788
|
-
const specPath =
|
|
6403
|
+
const specPath = import_path7.default.join(projectRoot, "openapi.json");
|
|
5789
6404
|
try {
|
|
5790
6405
|
await import_promises4.default.access(specPath);
|
|
5791
6406
|
} catch {
|
|
@@ -5812,7 +6427,7 @@ docsCommand.command("export").description("Export documentation to Postman, Inso
|
|
|
5812
6427
|
default:
|
|
5813
6428
|
throw new Error(`Unknown format: ${options.format}`);
|
|
5814
6429
|
}
|
|
5815
|
-
const outPath =
|
|
6430
|
+
const outPath = import_path7.default.join(projectRoot, options.output || defaultName);
|
|
5816
6431
|
await import_promises4.default.writeFile(outPath, output);
|
|
5817
6432
|
spinner.succeed(`Exported to: ${options.output || defaultName}`);
|
|
5818
6433
|
if (options.format === "postman") {
|
|
@@ -5825,8 +6440,8 @@ docsCommand.command("export").description("Export documentation to Postman, Inso
|
|
|
5825
6440
|
});
|
|
5826
6441
|
docsCommand.command("status").description("Show documentation status").action(async () => {
|
|
5827
6442
|
const projectRoot = getProjectRoot();
|
|
5828
|
-
console.log(
|
|
5829
|
-
const specPath =
|
|
6443
|
+
console.log(import_chalk9.default.bold("\n\u{1F4CA} Documentation Status\n"));
|
|
6444
|
+
const specPath = import_path7.default.join(projectRoot, "openapi.json");
|
|
5830
6445
|
try {
|
|
5831
6446
|
const stat2 = await import_promises4.default.stat(specPath);
|
|
5832
6447
|
success(
|
|
@@ -5941,13 +6556,736 @@ function formatDate(date) {
|
|
|
5941
6556
|
});
|
|
5942
6557
|
}
|
|
5943
6558
|
|
|
6559
|
+
// src/cli/commands/list.ts
|
|
6560
|
+
var import_commander6 = require("commander");
|
|
6561
|
+
var import_chalk10 = __toESM(require("chalk"), 1);
|
|
6562
|
+
var import_promises5 = __toESM(require("fs/promises"), 1);
|
|
6563
|
+
var AVAILABLE_MODULES2 = {
|
|
6564
|
+
// Core
|
|
6565
|
+
auth: {
|
|
6566
|
+
name: "Authentication",
|
|
6567
|
+
description: "JWT authentication with access/refresh tokens",
|
|
6568
|
+
category: "Core"
|
|
6569
|
+
},
|
|
6570
|
+
users: {
|
|
6571
|
+
name: "User Management",
|
|
6572
|
+
description: "User CRUD with RBAC (roles & permissions)",
|
|
6573
|
+
category: "Core"
|
|
6574
|
+
},
|
|
6575
|
+
email: {
|
|
6576
|
+
name: "Email Service",
|
|
6577
|
+
description: "SMTP email with templates (Handlebars)",
|
|
6578
|
+
category: "Core"
|
|
6579
|
+
},
|
|
6580
|
+
// Security
|
|
6581
|
+
mfa: {
|
|
6582
|
+
name: "MFA/TOTP",
|
|
6583
|
+
description: "Two-factor authentication with QR codes",
|
|
6584
|
+
category: "Security"
|
|
6585
|
+
},
|
|
6586
|
+
oauth: {
|
|
6587
|
+
name: "OAuth",
|
|
6588
|
+
description: "Social login (Google, GitHub, Facebook, Twitter, Apple)",
|
|
6589
|
+
category: "Security"
|
|
6590
|
+
},
|
|
6591
|
+
"rate-limit": {
|
|
6592
|
+
name: "Rate Limiting",
|
|
6593
|
+
description: "Advanced rate limiting with multiple algorithms",
|
|
6594
|
+
category: "Security"
|
|
6595
|
+
},
|
|
6596
|
+
// Data & Storage
|
|
6597
|
+
cache: {
|
|
6598
|
+
name: "Redis Cache",
|
|
6599
|
+
description: "Redis caching with TTL & invalidation",
|
|
6600
|
+
category: "Data & Storage"
|
|
6601
|
+
},
|
|
6602
|
+
upload: {
|
|
6603
|
+
name: "File Upload",
|
|
6604
|
+
description: "File upload with local/S3/Cloudinary storage",
|
|
6605
|
+
category: "Data & Storage"
|
|
6606
|
+
},
|
|
6607
|
+
search: {
|
|
6608
|
+
name: "Search",
|
|
6609
|
+
description: "Full-text search with Elasticsearch/Meilisearch",
|
|
6610
|
+
category: "Data & Storage"
|
|
6611
|
+
},
|
|
6612
|
+
// Communication
|
|
6613
|
+
notification: {
|
|
6614
|
+
name: "Notifications",
|
|
6615
|
+
description: "Email, SMS, Push notifications",
|
|
6616
|
+
category: "Communication"
|
|
6617
|
+
},
|
|
6618
|
+
webhook: {
|
|
6619
|
+
name: "Webhooks",
|
|
6620
|
+
description: "Outgoing webhooks with HMAC signatures & retry",
|
|
6621
|
+
category: "Communication"
|
|
6622
|
+
},
|
|
6623
|
+
websocket: {
|
|
6624
|
+
name: "WebSockets",
|
|
6625
|
+
description: "Real-time communication with Socket.io",
|
|
6626
|
+
category: "Communication"
|
|
6627
|
+
},
|
|
6628
|
+
// Background Processing
|
|
6629
|
+
queue: {
|
|
6630
|
+
name: "Queue/Jobs",
|
|
6631
|
+
description: "Background jobs with Bull/BullMQ & cron scheduling",
|
|
6632
|
+
category: "Background Processing"
|
|
6633
|
+
},
|
|
6634
|
+
"media-processing": {
|
|
6635
|
+
name: "Media Processing",
|
|
6636
|
+
description: "Image/video processing with FFmpeg",
|
|
6637
|
+
category: "Background Processing"
|
|
6638
|
+
},
|
|
6639
|
+
// Monitoring & Analytics
|
|
6640
|
+
audit: {
|
|
6641
|
+
name: "Audit Logs",
|
|
6642
|
+
description: "Activity logging and audit trail",
|
|
6643
|
+
category: "Monitoring & Analytics"
|
|
6644
|
+
},
|
|
6645
|
+
analytics: {
|
|
6646
|
+
name: "Analytics/Metrics",
|
|
6647
|
+
description: "Prometheus metrics & event tracking",
|
|
6648
|
+
category: "Monitoring & Analytics"
|
|
6649
|
+
},
|
|
6650
|
+
// Internationalization
|
|
6651
|
+
i18n: {
|
|
6652
|
+
name: "i18n/Localization",
|
|
6653
|
+
description: "Multi-language support with 7+ locales",
|
|
6654
|
+
category: "Internationalization"
|
|
6655
|
+
},
|
|
6656
|
+
// API Management
|
|
6657
|
+
"feature-flag": {
|
|
6658
|
+
name: "Feature Flags",
|
|
6659
|
+
description: "A/B testing & progressive rollout",
|
|
6660
|
+
category: "API Management"
|
|
6661
|
+
},
|
|
6662
|
+
"api-versioning": {
|
|
6663
|
+
name: "API Versioning",
|
|
6664
|
+
description: "Multiple API versions support",
|
|
6665
|
+
category: "API Management"
|
|
6666
|
+
},
|
|
6667
|
+
// Payments
|
|
6668
|
+
payment: {
|
|
6669
|
+
name: "Payments",
|
|
6670
|
+
description: "Payment processing (Stripe, PayPal, Mobile Money)",
|
|
6671
|
+
category: "Payments"
|
|
6672
|
+
}
|
|
6673
|
+
};
|
|
6674
|
+
async function getInstalledModules() {
|
|
6675
|
+
try {
|
|
6676
|
+
const modulesDir = getModulesDir();
|
|
6677
|
+
const entries = await import_promises5.default.readdir(modulesDir, { withFileTypes: true });
|
|
6678
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
6679
|
+
} catch {
|
|
6680
|
+
return [];
|
|
6681
|
+
}
|
|
6682
|
+
}
|
|
6683
|
+
function isServercraftProject() {
|
|
6684
|
+
try {
|
|
6685
|
+
getProjectRoot();
|
|
6686
|
+
return true;
|
|
6687
|
+
} catch {
|
|
6688
|
+
return false;
|
|
6689
|
+
}
|
|
6690
|
+
}
|
|
6691
|
+
var listCommand = new import_commander6.Command("list").alias("ls").description("List available and installed modules").option("-a, --available", "Show only available modules").option("-i, --installed", "Show only installed modules").option("-c, --category <category>", "Filter by category").option("--json", "Output as JSON").action(
|
|
6692
|
+
async (options) => {
|
|
6693
|
+
const installedModules = await getInstalledModules();
|
|
6694
|
+
const isProject = isServercraftProject();
|
|
6695
|
+
if (options.json) {
|
|
6696
|
+
const output = {
|
|
6697
|
+
available: Object.entries(AVAILABLE_MODULES2).map(([key, mod]) => ({
|
|
6698
|
+
id: key,
|
|
6699
|
+
...mod,
|
|
6700
|
+
installed: installedModules.includes(key)
|
|
6701
|
+
}))
|
|
6702
|
+
};
|
|
6703
|
+
if (isProject) {
|
|
6704
|
+
output.installed = installedModules;
|
|
6705
|
+
}
|
|
6706
|
+
console.log(JSON.stringify(output, null, 2));
|
|
6707
|
+
return;
|
|
6708
|
+
}
|
|
6709
|
+
const byCategory = {};
|
|
6710
|
+
for (const [key, mod] of Object.entries(AVAILABLE_MODULES2)) {
|
|
6711
|
+
if (options.category && mod.category.toLowerCase() !== options.category.toLowerCase()) {
|
|
6712
|
+
continue;
|
|
6713
|
+
}
|
|
6714
|
+
if (!byCategory[mod.category]) {
|
|
6715
|
+
byCategory[mod.category] = [];
|
|
6716
|
+
}
|
|
6717
|
+
byCategory[mod.category]?.push({
|
|
6718
|
+
id: key,
|
|
6719
|
+
name: mod.name,
|
|
6720
|
+
description: mod.description,
|
|
6721
|
+
installed: installedModules.includes(key)
|
|
6722
|
+
});
|
|
6723
|
+
}
|
|
6724
|
+
if (options.installed) {
|
|
6725
|
+
if (!isProject) {
|
|
6726
|
+
console.log(import_chalk10.default.yellow("\n\u26A0 Not in a Servcraft project directory\n"));
|
|
6727
|
+
return;
|
|
6728
|
+
}
|
|
6729
|
+
console.log(import_chalk10.default.bold("\n\u{1F4E6} Installed Modules:\n"));
|
|
6730
|
+
if (installedModules.length === 0) {
|
|
6731
|
+
console.log(import_chalk10.default.gray(" No modules installed yet.\n"));
|
|
6732
|
+
console.log(` Run ${import_chalk10.default.cyan("servcraft add <module>")} to add a module.
|
|
6733
|
+
`);
|
|
6734
|
+
return;
|
|
6735
|
+
}
|
|
6736
|
+
for (const modId of installedModules) {
|
|
6737
|
+
const mod = AVAILABLE_MODULES2[modId];
|
|
6738
|
+
if (mod) {
|
|
6739
|
+
console.log(` ${import_chalk10.default.green("\u2713")} ${import_chalk10.default.cyan(modId.padEnd(18))} ${mod.name}`);
|
|
6740
|
+
} else {
|
|
6741
|
+
console.log(
|
|
6742
|
+
` ${import_chalk10.default.green("\u2713")} ${import_chalk10.default.cyan(modId.padEnd(18))} ${import_chalk10.default.gray("(custom module)")}`
|
|
6743
|
+
);
|
|
6744
|
+
}
|
|
6745
|
+
}
|
|
6746
|
+
console.log(`
|
|
6747
|
+
Total: ${import_chalk10.default.bold(installedModules.length)} module(s) installed
|
|
6748
|
+
`);
|
|
6749
|
+
return;
|
|
6750
|
+
}
|
|
6751
|
+
console.log(import_chalk10.default.bold("\n\u{1F4E6} Available Modules\n"));
|
|
6752
|
+
if (isProject) {
|
|
6753
|
+
console.log(
|
|
6754
|
+
import_chalk10.default.gray(` ${import_chalk10.default.green("\u2713")} = installed ${import_chalk10.default.dim("\u25CB")} = not installed
|
|
6755
|
+
`)
|
|
6756
|
+
);
|
|
6757
|
+
}
|
|
6758
|
+
for (const [category, modules] of Object.entries(byCategory)) {
|
|
6759
|
+
console.log(import_chalk10.default.bold.blue(` ${category}`));
|
|
6760
|
+
console.log(import_chalk10.default.gray(" " + "\u2500".repeat(40)));
|
|
6761
|
+
for (const mod of modules) {
|
|
6762
|
+
const status = isProject ? mod.installed ? import_chalk10.default.green("\u2713") : import_chalk10.default.dim("\u25CB") : " ";
|
|
6763
|
+
const nameColor = mod.installed ? import_chalk10.default.green : import_chalk10.default.cyan;
|
|
6764
|
+
console.log(` ${status} ${nameColor(mod.id.padEnd(18))} ${mod.name}`);
|
|
6765
|
+
console.log(` ${import_chalk10.default.gray(mod.description)}`);
|
|
6766
|
+
}
|
|
6767
|
+
console.log();
|
|
6768
|
+
}
|
|
6769
|
+
const totalAvailable = Object.keys(AVAILABLE_MODULES2).length;
|
|
6770
|
+
const totalInstalled = installedModules.filter((m) => AVAILABLE_MODULES2[m]).length;
|
|
6771
|
+
console.log(import_chalk10.default.gray("\u2500".repeat(50)));
|
|
6772
|
+
console.log(
|
|
6773
|
+
` ${import_chalk10.default.bold(totalAvailable)} modules available` + (isProject ? ` | ${import_chalk10.default.green.bold(totalInstalled)} installed` : "")
|
|
6774
|
+
);
|
|
6775
|
+
console.log();
|
|
6776
|
+
console.log(import_chalk10.default.bold(" Usage:"));
|
|
6777
|
+
console.log(` ${import_chalk10.default.yellow("servcraft add <module>")} Add a module`);
|
|
6778
|
+
console.log(` ${import_chalk10.default.yellow("servcraft list --installed")} Show installed only`);
|
|
6779
|
+
console.log(` ${import_chalk10.default.yellow("servcraft list --category Security")} Filter by category`);
|
|
6780
|
+
console.log();
|
|
6781
|
+
}
|
|
6782
|
+
);
|
|
6783
|
+
|
|
6784
|
+
// src/cli/commands/remove.ts
|
|
6785
|
+
var import_commander7 = require("commander");
|
|
6786
|
+
var import_path8 = __toESM(require("path"), 1);
|
|
6787
|
+
var import_ora7 = __toESM(require("ora"), 1);
|
|
6788
|
+
var import_chalk11 = __toESM(require("chalk"), 1);
|
|
6789
|
+
var import_promises6 = __toESM(require("fs/promises"), 1);
|
|
6790
|
+
var import_inquirer4 = __toESM(require("inquirer"), 1);
|
|
6791
|
+
var removeCommand = new import_commander7.Command("remove").alias("rm").description("Remove an installed module from your project").argument("<module>", "Module to remove").option("-y, --yes", "Skip confirmation prompt").option("--keep-env", "Keep environment variables").action(async (moduleName, options) => {
|
|
6792
|
+
const projectError = validateProject();
|
|
6793
|
+
if (projectError) {
|
|
6794
|
+
displayError(projectError);
|
|
6795
|
+
return;
|
|
6796
|
+
}
|
|
6797
|
+
console.log(import_chalk11.default.bold.cyan("\n\u{1F5D1}\uFE0F ServCraft Module Removal\n"));
|
|
6798
|
+
const moduleDir = import_path8.default.join(getModulesDir(), moduleName);
|
|
6799
|
+
try {
|
|
6800
|
+
const exists = await import_promises6.default.access(moduleDir).then(() => true).catch(() => false);
|
|
6801
|
+
if (!exists) {
|
|
6802
|
+
displayError(
|
|
6803
|
+
new ServCraftError(`Module "${moduleName}" is not installed`, [
|
|
6804
|
+
`Run ${import_chalk11.default.cyan("servcraft list --installed")} to see installed modules`,
|
|
6805
|
+
`Check the spelling of the module name`
|
|
6806
|
+
])
|
|
6807
|
+
);
|
|
6808
|
+
return;
|
|
6809
|
+
}
|
|
6810
|
+
const files = await import_promises6.default.readdir(moduleDir);
|
|
6811
|
+
const fileCount = files.length;
|
|
6812
|
+
if (!options?.yes) {
|
|
6813
|
+
console.log(import_chalk11.default.yellow(`\u26A0 This will remove the "${moduleName}" module:`));
|
|
6814
|
+
console.log(import_chalk11.default.gray(` Directory: ${moduleDir}`));
|
|
6815
|
+
console.log(import_chalk11.default.gray(` Files: ${fileCount} file(s)`));
|
|
6816
|
+
console.log();
|
|
6817
|
+
const { confirm } = await import_inquirer4.default.prompt([
|
|
6818
|
+
{
|
|
6819
|
+
type: "confirm",
|
|
6820
|
+
name: "confirm",
|
|
6821
|
+
message: "Are you sure you want to remove this module?",
|
|
6822
|
+
default: false
|
|
6823
|
+
}
|
|
6824
|
+
]);
|
|
6825
|
+
if (!confirm) {
|
|
6826
|
+
console.log(import_chalk11.default.yellow("\n\u2716 Removal cancelled\n"));
|
|
6827
|
+
return;
|
|
6828
|
+
}
|
|
6829
|
+
}
|
|
6830
|
+
const spinner = (0, import_ora7.default)("Removing module...").start();
|
|
6831
|
+
await import_promises6.default.rm(moduleDir, { recursive: true, force: true });
|
|
6832
|
+
spinner.succeed(`Module "${moduleName}" removed successfully!`);
|
|
6833
|
+
console.log("\n" + import_chalk11.default.bold("\u2713 Removed:"));
|
|
6834
|
+
success(` src/modules/${moduleName}/ (${fileCount} files)`);
|
|
6835
|
+
if (!options?.keepEnv) {
|
|
6836
|
+
console.log("\n" + import_chalk11.default.bold("\u{1F4CC} Manual cleanup needed:"));
|
|
6837
|
+
info(" 1. Remove environment variables related to this module from .env");
|
|
6838
|
+
info(" 2. Remove module imports from your main app file");
|
|
6839
|
+
info(" 3. Remove related database migrations if any");
|
|
6840
|
+
info(" 4. Update your routes if they reference this module");
|
|
6841
|
+
} else {
|
|
6842
|
+
console.log("\n" + import_chalk11.default.bold("\u{1F4CC} Manual cleanup needed:"));
|
|
6843
|
+
info(" 1. Environment variables were kept (--keep-env flag)");
|
|
6844
|
+
info(" 2. Remove module imports from your main app file");
|
|
6845
|
+
info(" 3. Update your routes if they reference this module");
|
|
6846
|
+
}
|
|
6847
|
+
console.log();
|
|
6848
|
+
} catch (err) {
|
|
6849
|
+
error(err instanceof Error ? err.message : String(err));
|
|
6850
|
+
console.log();
|
|
6851
|
+
}
|
|
6852
|
+
});
|
|
6853
|
+
|
|
6854
|
+
// src/cli/commands/doctor.ts
|
|
6855
|
+
var import_commander8 = require("commander");
|
|
6856
|
+
var import_chalk12 = __toESM(require("chalk"), 1);
|
|
6857
|
+
var import_promises7 = __toESM(require("fs/promises"), 1);
|
|
6858
|
+
async function checkNodeVersion() {
|
|
6859
|
+
const version = process.version;
|
|
6860
|
+
const major = parseInt(version.slice(1).split(".")[0] || "0", 10);
|
|
6861
|
+
if (major >= 18) {
|
|
6862
|
+
return { name: "Node.js", status: "pass", message: `${version} \u2713` };
|
|
6863
|
+
}
|
|
6864
|
+
return {
|
|
6865
|
+
name: "Node.js",
|
|
6866
|
+
status: "fail",
|
|
6867
|
+
message: `${version} (< 18)`,
|
|
6868
|
+
suggestion: "Upgrade to Node.js 18+"
|
|
6869
|
+
};
|
|
6870
|
+
}
|
|
6871
|
+
async function checkPackageJson() {
|
|
6872
|
+
const checks = [];
|
|
6873
|
+
try {
|
|
6874
|
+
const content = await import_promises7.default.readFile("package.json", "utf-8");
|
|
6875
|
+
const pkg = JSON.parse(content);
|
|
6876
|
+
checks.push({ name: "package.json", status: "pass", message: "Found" });
|
|
6877
|
+
if (pkg.dependencies?.fastify) {
|
|
6878
|
+
checks.push({ name: "Fastify", status: "pass", message: "Installed" });
|
|
6879
|
+
} else {
|
|
6880
|
+
checks.push({
|
|
6881
|
+
name: "Fastify",
|
|
6882
|
+
status: "fail",
|
|
6883
|
+
message: "Missing",
|
|
6884
|
+
suggestion: "npm install fastify"
|
|
6885
|
+
});
|
|
6886
|
+
}
|
|
6887
|
+
} catch {
|
|
6888
|
+
checks.push({
|
|
6889
|
+
name: "package.json",
|
|
6890
|
+
status: "fail",
|
|
6891
|
+
message: "Not found",
|
|
6892
|
+
suggestion: "Run servcraft init"
|
|
6893
|
+
});
|
|
6894
|
+
}
|
|
6895
|
+
return checks;
|
|
6896
|
+
}
|
|
6897
|
+
async function checkDirectories() {
|
|
6898
|
+
const checks = [];
|
|
6899
|
+
const dirs = ["src", "node_modules", ".git", ".env"];
|
|
6900
|
+
for (const dir of dirs) {
|
|
6901
|
+
try {
|
|
6902
|
+
await import_promises7.default.access(dir);
|
|
6903
|
+
checks.push({ name: dir, status: "pass", message: "Exists" });
|
|
6904
|
+
} catch {
|
|
6905
|
+
const isCritical = dir === "src" || dir === "node_modules";
|
|
6906
|
+
checks.push({
|
|
6907
|
+
name: dir,
|
|
6908
|
+
status: isCritical ? "fail" : "warn",
|
|
6909
|
+
message: "Not found",
|
|
6910
|
+
suggestion: dir === "node_modules" ? "npm install" : dir === ".env" ? "Create .env file" : void 0
|
|
6911
|
+
});
|
|
6912
|
+
}
|
|
6913
|
+
}
|
|
6914
|
+
return checks;
|
|
6915
|
+
}
|
|
6916
|
+
var doctorCommand = new import_commander8.Command("doctor").description("Diagnose project configuration and dependencies").action(async () => {
|
|
6917
|
+
console.log(import_chalk12.default.bold.cyan("\n\u{1F50D} ServCraft Doctor\n"));
|
|
6918
|
+
const allChecks = [];
|
|
6919
|
+
allChecks.push(await checkNodeVersion());
|
|
6920
|
+
allChecks.push(...await checkPackageJson());
|
|
6921
|
+
allChecks.push(...await checkDirectories());
|
|
6922
|
+
allChecks.forEach((check) => {
|
|
6923
|
+
const icon = check.status === "pass" ? import_chalk12.default.green("\u2713") : check.status === "warn" ? import_chalk12.default.yellow("\u26A0") : import_chalk12.default.red("\u2717");
|
|
6924
|
+
const color = check.status === "pass" ? import_chalk12.default.green : check.status === "warn" ? import_chalk12.default.yellow : import_chalk12.default.red;
|
|
6925
|
+
console.log(`${icon} ${check.name.padEnd(20)} ${color(check.message)}`);
|
|
6926
|
+
if (check.suggestion) {
|
|
6927
|
+
console.log(import_chalk12.default.gray(` \u2192 ${check.suggestion}`));
|
|
6928
|
+
}
|
|
6929
|
+
});
|
|
6930
|
+
const pass = allChecks.filter((c) => c.status === "pass").length;
|
|
6931
|
+
const warn2 = allChecks.filter((c) => c.status === "warn").length;
|
|
6932
|
+
const fail = allChecks.filter((c) => c.status === "fail").length;
|
|
6933
|
+
console.log(import_chalk12.default.gray("\n" + "\u2500".repeat(60)));
|
|
6934
|
+
console.log(
|
|
6935
|
+
`
|
|
6936
|
+
${import_chalk12.default.green(pass + " passed")} | ${import_chalk12.default.yellow(warn2 + " warnings")} | ${import_chalk12.default.red(fail + " failed")}
|
|
6937
|
+
`
|
|
6938
|
+
);
|
|
6939
|
+
if (fail === 0 && warn2 === 0) {
|
|
6940
|
+
console.log(import_chalk12.default.green.bold("\u2728 Everything looks good!\n"));
|
|
6941
|
+
} else if (fail > 0) {
|
|
6942
|
+
console.log(import_chalk12.default.red.bold("\u2717 Fix critical issues before using ServCraft.\n"));
|
|
6943
|
+
} else {
|
|
6944
|
+
console.log(import_chalk12.default.yellow.bold("\u26A0 Some warnings, but should work.\n"));
|
|
6945
|
+
}
|
|
6946
|
+
});
|
|
6947
|
+
|
|
6948
|
+
// src/cli/commands/update.ts
|
|
6949
|
+
var import_commander9 = require("commander");
|
|
6950
|
+
var import_chalk13 = __toESM(require("chalk"), 1);
|
|
6951
|
+
var import_promises8 = __toESM(require("fs/promises"), 1);
|
|
6952
|
+
var import_path9 = __toESM(require("path"), 1);
|
|
6953
|
+
var import_url = require("url");
|
|
6954
|
+
var import_inquirer5 = __toESM(require("inquirer"), 1);
|
|
6955
|
+
var __filename2 = (0, import_url.fileURLToPath)(importMetaUrl);
|
|
6956
|
+
var __dirname = import_path9.default.dirname(__filename2);
|
|
6957
|
+
var AVAILABLE_MODULES3 = [
|
|
6958
|
+
"auth",
|
|
6959
|
+
"users",
|
|
6960
|
+
"email",
|
|
6961
|
+
"mfa",
|
|
6962
|
+
"oauth",
|
|
6963
|
+
"rate-limit",
|
|
6964
|
+
"cache",
|
|
6965
|
+
"upload",
|
|
6966
|
+
"search",
|
|
6967
|
+
"notification",
|
|
6968
|
+
"webhook",
|
|
6969
|
+
"websocket",
|
|
6970
|
+
"queue",
|
|
6971
|
+
"payment",
|
|
6972
|
+
"i18n",
|
|
6973
|
+
"feature-flag",
|
|
6974
|
+
"analytics",
|
|
6975
|
+
"media-processing",
|
|
6976
|
+
"api-versioning",
|
|
6977
|
+
"audit",
|
|
6978
|
+
"swagger",
|
|
6979
|
+
"validation"
|
|
6980
|
+
];
|
|
6981
|
+
async function getInstalledModules2() {
|
|
6982
|
+
try {
|
|
6983
|
+
const modulesDir = getModulesDir();
|
|
6984
|
+
const entries = await import_promises8.default.readdir(modulesDir, { withFileTypes: true });
|
|
6985
|
+
const installedModules = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).filter((name) => AVAILABLE_MODULES3.includes(name));
|
|
6986
|
+
return installedModules;
|
|
6987
|
+
} catch {
|
|
6988
|
+
return [];
|
|
6989
|
+
}
|
|
6990
|
+
}
|
|
6991
|
+
async function copyModuleFiles(moduleName, _projectRoot) {
|
|
6992
|
+
const cliRoot = import_path9.default.resolve(__dirname, "../../../");
|
|
6993
|
+
const sourceModulePath = import_path9.default.join(cliRoot, "src", "modules", moduleName);
|
|
6994
|
+
const targetModulesDir = getModulesDir();
|
|
6995
|
+
const targetModulePath = import_path9.default.join(targetModulesDir, moduleName);
|
|
6996
|
+
try {
|
|
6997
|
+
await import_promises8.default.access(sourceModulePath);
|
|
6998
|
+
} catch {
|
|
6999
|
+
throw new Error(`Module source not found: ${moduleName}`);
|
|
7000
|
+
}
|
|
7001
|
+
await import_promises8.default.cp(sourceModulePath, targetModulePath, { recursive: true });
|
|
7002
|
+
}
|
|
7003
|
+
async function updateModule(moduleName, options) {
|
|
7004
|
+
const projectError = validateProject();
|
|
7005
|
+
if (projectError) {
|
|
7006
|
+
displayError(projectError);
|
|
7007
|
+
return;
|
|
7008
|
+
}
|
|
7009
|
+
const projectRoot = getProjectRoot();
|
|
7010
|
+
const installedModules = await getInstalledModules2();
|
|
7011
|
+
if (!installedModules.includes(moduleName)) {
|
|
7012
|
+
console.log(import_chalk13.default.yellow(`
|
|
7013
|
+
\u26A0 Module "${moduleName}" is not installed
|
|
7014
|
+
`));
|
|
7015
|
+
console.log(
|
|
7016
|
+
import_chalk13.default.gray(`Run ${import_chalk13.default.cyan(`servcraft add ${moduleName}`)} to install it first.
|
|
7017
|
+
`)
|
|
7018
|
+
);
|
|
7019
|
+
return;
|
|
7020
|
+
}
|
|
7021
|
+
if (options.check) {
|
|
7022
|
+
console.log(import_chalk13.default.cyan(`
|
|
7023
|
+
\u{1F4E6} Checking updates for "${moduleName}"...
|
|
7024
|
+
`));
|
|
7025
|
+
console.log(import_chalk13.default.gray("Note: Version tracking will be implemented in a future release."));
|
|
7026
|
+
console.log(import_chalk13.default.gray("Currently, update will always reinstall the latest version.\n"));
|
|
7027
|
+
return;
|
|
7028
|
+
}
|
|
7029
|
+
const { confirmed } = await import_inquirer5.default.prompt([
|
|
7030
|
+
{
|
|
7031
|
+
type: "confirm",
|
|
7032
|
+
name: "confirmed",
|
|
7033
|
+
message: `Update "${moduleName}" module? This will overwrite existing files.`,
|
|
7034
|
+
default: false
|
|
7035
|
+
}
|
|
7036
|
+
]);
|
|
7037
|
+
if (!confirmed) {
|
|
7038
|
+
console.log(import_chalk13.default.yellow("\n\u26A0 Update cancelled\n"));
|
|
7039
|
+
return;
|
|
7040
|
+
}
|
|
7041
|
+
console.log(import_chalk13.default.cyan(`
|
|
7042
|
+
\u{1F504} Updating "${moduleName}" module...
|
|
7043
|
+
`));
|
|
7044
|
+
try {
|
|
7045
|
+
await copyModuleFiles(moduleName, projectRoot);
|
|
7046
|
+
console.log(import_chalk13.default.green(`\u2714 Module "${moduleName}" updated successfully!
|
|
7047
|
+
`));
|
|
7048
|
+
console.log(
|
|
7049
|
+
import_chalk13.default.gray("Note: Remember to review any breaking changes in the documentation.\n")
|
|
7050
|
+
);
|
|
7051
|
+
} catch (error2) {
|
|
7052
|
+
if (error2 instanceof Error) {
|
|
7053
|
+
console.error(import_chalk13.default.red(`
|
|
7054
|
+
\u2717 Failed to update module: ${error2.message}
|
|
7055
|
+
`));
|
|
7056
|
+
}
|
|
7057
|
+
}
|
|
7058
|
+
}
|
|
7059
|
+
async function updateAllModules(options) {
|
|
7060
|
+
const projectError = validateProject();
|
|
7061
|
+
if (projectError) {
|
|
7062
|
+
displayError(projectError);
|
|
7063
|
+
return;
|
|
7064
|
+
}
|
|
7065
|
+
const installedModules = await getInstalledModules2();
|
|
7066
|
+
if (installedModules.length === 0) {
|
|
7067
|
+
console.log(import_chalk13.default.yellow("\n\u26A0 No modules installed\n"));
|
|
7068
|
+
console.log(import_chalk13.default.gray(`Run ${import_chalk13.default.cyan("servcraft list")} to see available modules.
|
|
7069
|
+
`));
|
|
7070
|
+
return;
|
|
7071
|
+
}
|
|
7072
|
+
if (options.check) {
|
|
7073
|
+
console.log(import_chalk13.default.cyan("\n\u{1F4E6} Checking updates for all modules...\n"));
|
|
7074
|
+
console.log(import_chalk13.default.bold("Installed modules:"));
|
|
7075
|
+
installedModules.forEach((mod) => {
|
|
7076
|
+
console.log(` \u2022 ${import_chalk13.default.cyan(mod)}`);
|
|
7077
|
+
});
|
|
7078
|
+
console.log();
|
|
7079
|
+
console.log(import_chalk13.default.gray("Note: Version tracking will be implemented in a future release."));
|
|
7080
|
+
console.log(import_chalk13.default.gray("Currently, update will always reinstall the latest version.\n"));
|
|
7081
|
+
return;
|
|
7082
|
+
}
|
|
7083
|
+
console.log(import_chalk13.default.cyan(`
|
|
7084
|
+
\u{1F4E6} Found ${installedModules.length} installed module(s):
|
|
7085
|
+
`));
|
|
7086
|
+
installedModules.forEach((mod) => {
|
|
7087
|
+
console.log(` \u2022 ${import_chalk13.default.cyan(mod)}`);
|
|
7088
|
+
});
|
|
7089
|
+
console.log();
|
|
7090
|
+
const { confirmed } = await import_inquirer5.default.prompt([
|
|
7091
|
+
{
|
|
7092
|
+
type: "confirm",
|
|
7093
|
+
name: "confirmed",
|
|
7094
|
+
message: "Update all modules? This will overwrite existing files.",
|
|
7095
|
+
default: false
|
|
7096
|
+
}
|
|
7097
|
+
]);
|
|
7098
|
+
if (!confirmed) {
|
|
7099
|
+
console.log(import_chalk13.default.yellow("\n\u26A0 Update cancelled\n"));
|
|
7100
|
+
return;
|
|
7101
|
+
}
|
|
7102
|
+
console.log(import_chalk13.default.cyan("\n\u{1F504} Updating all modules...\n"));
|
|
7103
|
+
const projectRoot = getProjectRoot();
|
|
7104
|
+
let successCount = 0;
|
|
7105
|
+
let failCount = 0;
|
|
7106
|
+
for (const moduleName of installedModules) {
|
|
7107
|
+
try {
|
|
7108
|
+
await copyModuleFiles(moduleName, projectRoot);
|
|
7109
|
+
console.log(import_chalk13.default.green(`\u2714 Updated: ${moduleName}`));
|
|
7110
|
+
successCount++;
|
|
7111
|
+
} catch {
|
|
7112
|
+
console.error(import_chalk13.default.red(`\u2717 Failed: ${moduleName}`));
|
|
7113
|
+
failCount++;
|
|
7114
|
+
}
|
|
7115
|
+
}
|
|
7116
|
+
console.log();
|
|
7117
|
+
console.log(
|
|
7118
|
+
import_chalk13.default.bold(
|
|
7119
|
+
`
|
|
7120
|
+
\u2714 Update complete: ${import_chalk13.default.green(successCount)} succeeded, ${import_chalk13.default.red(failCount)} failed
|
|
7121
|
+
`
|
|
7122
|
+
)
|
|
7123
|
+
);
|
|
7124
|
+
if (successCount > 0) {
|
|
7125
|
+
console.log(
|
|
7126
|
+
import_chalk13.default.gray("Note: Remember to review any breaking changes in the documentation.\n")
|
|
7127
|
+
);
|
|
7128
|
+
}
|
|
7129
|
+
}
|
|
7130
|
+
var updateCommand = new import_commander9.Command("update").description("Update installed modules to latest version").argument("[module]", "Specific module to update").option("--check", "Check for updates without applying").option("-y, --yes", "Skip confirmation prompt").action(async (moduleName, options) => {
|
|
7131
|
+
if (moduleName) {
|
|
7132
|
+
await updateModule(moduleName, { check: options?.check });
|
|
7133
|
+
} else {
|
|
7134
|
+
await updateAllModules({ check: options?.check });
|
|
7135
|
+
}
|
|
7136
|
+
});
|
|
7137
|
+
|
|
7138
|
+
// src/cli/commands/completion.ts
|
|
7139
|
+
var import_commander10 = require("commander");
|
|
7140
|
+
var bashScript = `
|
|
7141
|
+
# servcraft bash completion script
|
|
7142
|
+
_servcraft_completions() {
|
|
7143
|
+
local cur prev words cword
|
|
7144
|
+
_init_completion || return
|
|
7145
|
+
|
|
7146
|
+
# Main commands
|
|
7147
|
+
local commands="init add generate list remove doctor update completion docs --version --help"
|
|
7148
|
+
|
|
7149
|
+
# Generate subcommands
|
|
7150
|
+
local generate_subcommands="module controller service repository types schema routes m c s r t"
|
|
7151
|
+
|
|
7152
|
+
case "\${words[1]}" in
|
|
7153
|
+
generate|g)
|
|
7154
|
+
if [[ \${cword} -eq 2 ]]; then
|
|
7155
|
+
COMPREPLY=( $(compgen -W "\${generate_subcommands}" -- "\${cur}") )
|
|
7156
|
+
fi
|
|
7157
|
+
;;
|
|
7158
|
+
add|remove|rm|update)
|
|
7159
|
+
if [[ \${cword} -eq 2 ]]; then
|
|
7160
|
+
# Get available modules
|
|
7161
|
+
local modules="auth cache rate-limit notification payment oauth mfa queue websocket upload"
|
|
7162
|
+
COMPREPLY=( $(compgen -W "\${modules}" -- "\${cur}") )
|
|
7163
|
+
fi
|
|
7164
|
+
;;
|
|
7165
|
+
completion)
|
|
7166
|
+
if [[ \${cword} -eq 2 ]]; then
|
|
7167
|
+
COMPREPLY=( $(compgen -W "bash zsh" -- "\${cur}") )
|
|
7168
|
+
fi
|
|
7169
|
+
;;
|
|
7170
|
+
*)
|
|
7171
|
+
if [[ \${cword} -eq 1 ]]; then
|
|
7172
|
+
COMPREPLY=( $(compgen -W "\${commands}" -- "\${cur}") )
|
|
7173
|
+
fi
|
|
7174
|
+
;;
|
|
7175
|
+
esac
|
|
7176
|
+
}
|
|
7177
|
+
|
|
7178
|
+
complete -F _servcraft_completions servcraft
|
|
7179
|
+
`;
|
|
7180
|
+
var zshScript = `
|
|
7181
|
+
#compdef servcraft
|
|
7182
|
+
|
|
7183
|
+
_servcraft() {
|
|
7184
|
+
local context state state_descr line
|
|
7185
|
+
typeset -A opt_args
|
|
7186
|
+
|
|
7187
|
+
_arguments -C \\
|
|
7188
|
+
'1: :_servcraft_commands' \\
|
|
7189
|
+
'*::arg:->args'
|
|
7190
|
+
|
|
7191
|
+
case $state in
|
|
7192
|
+
args)
|
|
7193
|
+
case $line[1] in
|
|
7194
|
+
generate|g)
|
|
7195
|
+
_servcraft_generate
|
|
7196
|
+
;;
|
|
7197
|
+
add|remove|rm|update)
|
|
7198
|
+
_servcraft_modules
|
|
7199
|
+
;;
|
|
7200
|
+
completion)
|
|
7201
|
+
_arguments '1: :(bash zsh)'
|
|
7202
|
+
;;
|
|
7203
|
+
esac
|
|
7204
|
+
;;
|
|
7205
|
+
esac
|
|
7206
|
+
}
|
|
7207
|
+
|
|
7208
|
+
_servcraft_commands() {
|
|
7209
|
+
local commands
|
|
7210
|
+
commands=(
|
|
7211
|
+
'init:Initialize a new ServCraft project'
|
|
7212
|
+
'add:Add a pre-built module to your project'
|
|
7213
|
+
'generate:Generate code files (controller, service, etc.)'
|
|
7214
|
+
'list:List available and installed modules'
|
|
7215
|
+
'remove:Remove an installed module'
|
|
7216
|
+
'doctor:Diagnose project configuration'
|
|
7217
|
+
'update:Update installed modules'
|
|
7218
|
+
'completion:Generate shell completion scripts'
|
|
7219
|
+
'docs:Open documentation'
|
|
7220
|
+
'--version:Show version'
|
|
7221
|
+
'--help:Show help'
|
|
7222
|
+
)
|
|
7223
|
+
_describe 'command' commands
|
|
7224
|
+
}
|
|
7225
|
+
|
|
7226
|
+
_servcraft_generate() {
|
|
7227
|
+
local subcommands
|
|
7228
|
+
subcommands=(
|
|
7229
|
+
'module:Generate a complete module (controller + service + routes)'
|
|
7230
|
+
'controller:Generate a controller'
|
|
7231
|
+
'service:Generate a service'
|
|
7232
|
+
'repository:Generate a repository'
|
|
7233
|
+
'types:Generate TypeScript types'
|
|
7234
|
+
'schema:Generate validation schema'
|
|
7235
|
+
'routes:Generate routes file'
|
|
7236
|
+
'm:Alias for module'
|
|
7237
|
+
'c:Alias for controller'
|
|
7238
|
+
's:Alias for service'
|
|
7239
|
+
'r:Alias for repository'
|
|
7240
|
+
't:Alias for types'
|
|
7241
|
+
)
|
|
7242
|
+
_describe 'subcommand' subcommands
|
|
7243
|
+
}
|
|
7244
|
+
|
|
7245
|
+
_servcraft_modules() {
|
|
7246
|
+
local modules
|
|
7247
|
+
modules=(
|
|
7248
|
+
'auth:Authentication & Authorization'
|
|
7249
|
+
'cache:Redis caching'
|
|
7250
|
+
'rate-limit:Rate limiting'
|
|
7251
|
+
'notification:Email/SMS notifications'
|
|
7252
|
+
'payment:Payment integration'
|
|
7253
|
+
'oauth:OAuth providers'
|
|
7254
|
+
'mfa:Multi-factor authentication'
|
|
7255
|
+
'queue:Background jobs'
|
|
7256
|
+
'websocket:WebSocket support'
|
|
7257
|
+
'upload:File upload handling'
|
|
7258
|
+
)
|
|
7259
|
+
_describe 'module' modules
|
|
7260
|
+
}
|
|
7261
|
+
|
|
7262
|
+
_servcraft "$@"
|
|
7263
|
+
`;
|
|
7264
|
+
var completionCommand = new import_commander10.Command("completion").description("Generate shell completion scripts").argument("<shell>", "Shell type (bash or zsh)").action((shell) => {
|
|
7265
|
+
const shellLower = shell.toLowerCase();
|
|
7266
|
+
if (shellLower === "bash") {
|
|
7267
|
+
console.log(bashScript);
|
|
7268
|
+
} else if (shellLower === "zsh") {
|
|
7269
|
+
console.log(zshScript);
|
|
7270
|
+
} else {
|
|
7271
|
+
console.error(`Unsupported shell: ${shell}`);
|
|
7272
|
+
console.error("Supported shells: bash, zsh");
|
|
7273
|
+
process.exit(1);
|
|
7274
|
+
}
|
|
7275
|
+
});
|
|
7276
|
+
|
|
5944
7277
|
// src/cli/index.ts
|
|
5945
|
-
var program = new
|
|
7278
|
+
var program = new import_commander11.Command();
|
|
5946
7279
|
program.name("servcraft").description("Servcraft - A modular Node.js backend framework CLI").version("0.1.0");
|
|
5947
7280
|
program.addCommand(initCommand);
|
|
5948
7281
|
program.addCommand(generateCommand);
|
|
5949
7282
|
program.addCommand(addModuleCommand);
|
|
5950
7283
|
program.addCommand(dbCommand);
|
|
5951
7284
|
program.addCommand(docsCommand);
|
|
7285
|
+
program.addCommand(listCommand);
|
|
7286
|
+
program.addCommand(removeCommand);
|
|
7287
|
+
program.addCommand(doctorCommand);
|
|
7288
|
+
program.addCommand(updateCommand);
|
|
7289
|
+
program.addCommand(completionCommand);
|
|
5952
7290
|
program.parse();
|
|
5953
7291
|
//# sourceMappingURL=index.cjs.map
|