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.
@@ -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 import_commander6 = require("commander");
31
+ var import_commander11 = require("commander");
32
32
 
33
33
  // src/cli/commands/init.ts
34
34
  var import_commander = require("commander");
35
- var import_path2 = __toESM(require("path"), 1);
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 import_chalk2 = __toESM(require("chalk"), 1);
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 import_path = __toESM(require("path"), 1);
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
- await ensureDir(import_path.default.dirname(filePath));
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(import_chalk.default.green("\u2713"), message);
174
+ console.log(import_chalk2.default.green("\u2713"), message);
82
175
  }
83
176
  function error(message) {
84
- console.error(import_chalk.default.red("\u2717"), message);
177
+ console.error(import_chalk2.default.red("\u2717"), message);
85
178
  }
86
179
  function warn(message) {
87
- console.log(import_chalk.default.yellow("\u26A0"), message);
180
+ console.log(import_chalk2.default.yellow("\u26A0"), message);
88
181
  }
89
182
  function info(message) {
90
- console.log(import_chalk.default.blue("\u2139"), message);
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 import_path.default.join(getProjectRoot(), "src");
189
+ return import_path2.default.join(getProjectRoot(), "src");
97
190
  }
98
191
  function getModulesDir() {
99
- return import_path.default.join(getSourceDir(), "modules");
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
- import_chalk2.default.blue(`
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 ${import_chalk2.default.bold("\u{1F680} Servcraft Project Generator")} \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 = import_path2.default.resolve(process.cwd(), options.name);
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
- import_path2.default.join(projectDir, "package.json"),
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(import_path2.default.join(projectDir, "tsconfig.json"), generateTsConfig(options));
223
- await writeFile(import_path2.default.join(projectDir, "tsup.config.ts"), generateTsupConfig(options));
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(import_path2.default.join(projectDir, "jsconfig.json"), generateJsConfig(options));
323
+ await writeFile(import_path3.default.join(projectDir, "jsconfig.json"), generateJsConfig(options));
226
324
  }
227
- await writeFile(import_path2.default.join(projectDir, ".env.example"), generateEnvExample(options));
228
- await writeFile(import_path2.default.join(projectDir, ".env"), generateEnvExample(options));
229
- await writeFile(import_path2.default.join(projectDir, ".gitignore"), generateGitignore());
230
- await writeFile(import_path2.default.join(projectDir, "Dockerfile"), generateDockerfile(options));
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
- import_path2.default.join(projectDir, "docker-compose.yml"),
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(import_path2.default.join(projectDir, dir));
351
+ await ensureDir(import_path3.default.join(projectDir, dir));
254
352
  }
255
- await writeFile(import_path2.default.join(projectDir, `src/index.${ext}`), generateEntryFile(options));
353
+ await writeFile(import_path3.default.join(projectDir, `src/index.${ext}`), generateEntryFile(options));
256
354
  await writeFile(
257
- import_path2.default.join(projectDir, `src/core/server.${ext}`),
355
+ import_path3.default.join(projectDir, `src/core/server.${ext}`),
258
356
  generateServerFile(options)
259
357
  );
260
358
  await writeFile(
261
- import_path2.default.join(projectDir, `src/core/logger.${ext}`),
359
+ import_path3.default.join(projectDir, `src/core/logger.${ext}`),
262
360
  generateLoggerFile(options)
263
361
  );
264
362
  await writeFile(
265
- import_path2.default.join(projectDir, `src/config/index.${ext}`),
363
+ import_path3.default.join(projectDir, `src/config/index.${ext}`),
266
364
  generateConfigFile(options)
267
365
  );
268
366
  await writeFile(
269
- import_path2.default.join(projectDir, `src/middleware/index.${ext}`),
367
+ import_path3.default.join(projectDir, `src/middleware/index.${ext}`),
270
368
  generateMiddlewareFile(options)
271
369
  );
272
370
  await writeFile(
273
- import_path2.default.join(projectDir, `src/utils/index.${ext}`),
371
+ import_path3.default.join(projectDir, `src/utils/index.${ext}`),
274
372
  generateUtilsFile(options)
275
373
  );
276
374
  await writeFile(
277
- import_path2.default.join(projectDir, `src/types/index.${ext}`),
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
- import_path2.default.join(projectDir, "prisma/schema.prisma"),
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
- import_path2.default.join(projectDir, `src/database/connection.${ext}`),
385
+ import_path3.default.join(projectDir, `src/database/connection.${ext}`),
288
386
  generateMongooseConnection(options)
289
387
  );
290
388
  await writeFile(
291
- import_path2.default.join(projectDir, `src/database/models/user.model.${ext}`),
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
- const installSpinner = (0, import_ora.default)("Installing dependencies...").start();
297
- try {
298
- (0, import_child_process.execSync)("npm install", { cwd: projectDir, stdio: "pipe" });
299
- installSpinner.succeed("Dependencies installed!");
300
- } catch {
301
- installSpinner.warn("Failed to install dependencies automatically");
302
- warn(' Run "npm install" manually in the project directory');
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" + import_chalk2.default.green("\u2728 Project created successfully!"));
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(import_chalk2.default.bold("\u{1F680} Get started:"));
422
+ console.log(import_chalk3.default.bold("\u{1F680} Get started:"));
321
423
  console.log(`
322
- ${import_chalk2.default.cyan(`cd ${options.name}`)}
323
- ${options.database !== "none" ? import_chalk2.default.cyan("npm run db:push # Setup database") : ""}
324
- ${import_chalk2.default.cyan("npm run dev # Start development server")}
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(import_chalk2.default.bold("\u{1F4DA} Available commands:"));
428
+ console.log(import_chalk3.default.bold("\u{1F4DA} Available commands:"));
327
429
  console.log(`
328
- ${import_chalk2.default.yellow("servcraft generate module <name>")} Generate a new module
329
- ${import_chalk2.default.yellow("servcraft generate controller <name>")} Generate a controller
330
- ${import_chalk2.default.yellow("servcraft generate service <name>")} Generate a service
331
- ${import_chalk2.default.yellow("servcraft add auth")} Add authentication module
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 import_path3 = __toESM(require("path"), 1);
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 = import_path3.default.join(getModulesDir(), kebabName);
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(import_path3.default.join(moduleDir, file.name), file.content);
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 = import_path3.default.join(getModulesDir(), moduleName);
2067
- const filePath = import_path3.default.join(moduleDir, `${kebabName}.controller.ts`);
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
- error(`Controller "${kebabName}" already exists`);
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 = import_path3.default.join(getModulesDir(), moduleName);
2089
- const filePath = import_path3.default.join(moduleDir, `${kebabName}.service.ts`);
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 = import_path3.default.join(getModulesDir(), moduleName);
2112
- const filePath = import_path3.default.join(moduleDir, `${kebabName}.repository.ts`);
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 = import_path3.default.join(getModulesDir(), moduleName);
2133
- const filePath = import_path3.default.join(moduleDir, `${kebabName}.types.ts`);
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 = import_path3.default.join(getModulesDir(), moduleName);
2155
- const filePath = import_path3.default.join(moduleDir, `${kebabName}.schemas.ts`);
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 = import_path3.default.join(getModulesDir(), moduleName);
2178
- const filePath = import_path3.default.join(moduleDir, `${kebabName}.routes.ts`);
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 import_path4 = __toESM(require("path"), 1);
2866
+ var import_path5 = __toESM(require("path"), 1);
2266
2867
  var import_ora3 = __toESM(require("ora"), 1);
2267
- var import_chalk4 = __toESM(require("chalk"), 1);
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 path4 = __toESM(require("path"), 1);
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 = path4.join(projectRoot, ".env");
2279
- this.envExamplePath = path4.join(projectRoot, ".env.example");
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 path5 = __toESM(require("path"), 1);
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 = path5.join(projectRoot, ".servcraft", "templates");
2938
- this.manifestsDir = path5.join(projectRoot, ".servcraft", "manifests");
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 = path5.join(this.templatesDir, moduleName);
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 = path5.join(moduleTemplateDir, fileName);
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 = path5.join(this.templatesDir, moduleName, fileName);
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 = path5.join(this.manifestsDir, `${moduleName}.json`);
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 = path5.join(this.manifestsDir, `${moduleName}.json`);
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 = path5.join(moduleDir, fileName);
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 = path5.join(path5.dirname(moduleDir), `${moduleName}.backup-${timestamp}`);
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 = path5.join(src, entry.name);
3064
- const destPath = path5.join(dest, entry.name);
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 = path5.join(this.manifestsDir, `${moduleName}.json`);
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 import_chalk3 = __toESM(require("chalk"), 1);
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(import_chalk3.default.yellow(`
3779
+ console.log(import_chalk6.default.yellow(`
3179
3780
  \u26A0\uFE0F Module "${moduleName}" already exists`));
3180
3781
  if (hasModifications) {
3181
- console.log(import_chalk3.default.yellow(" Some files have been modified by you.\n"));
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(import_chalk3.default.cyan(`
3820
+ console.log(import_chalk6.default.cyan(`
3220
3821
  \u{1F4C1} ${fileName}`));
3221
3822
  console.log(
3222
- import_chalk3.default.gray(` Your version: ${yourLines} lines${isModified ? " (modified)" : ""}`)
3823
+ import_chalk6.default.gray(` Your version: ${yourLines} lines${isModified ? " (modified)" : ""}`)
3223
3824
  );
3224
- console.log(import_chalk3.default.gray(` New version: ${newLines} lines
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(import_chalk3.default.cyan("\n\u{1F4CA} Differences:\n"));
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(import_chalk3.default.red("\n\u26A0\uFE0F Merge Conflicts Detected:\n"));
3885
+ console.log(import_chalk6.default.red("\n\u26A0\uFE0F Merge Conflicts Detected:\n"));
3285
3886
  conflicts.forEach((conflict, i) => {
3286
- console.log(import_chalk3.default.yellow(` ${i + 1}. ${conflict}`));
3887
+ console.log(import_chalk6.default.yellow(` ${i + 1}. ${conflict}`));
3287
3888
  });
3288
- console.log(import_chalk3.default.gray("\n Conflict markers have been added to the file:"));
3289
- console.log(import_chalk3.default.gray(" <<<<<<< YOUR VERSION"));
3290
- console.log(import_chalk3.default.gray(" ... your code ..."));
3291
- console.log(import_chalk3.default.gray(" ======="));
3292
- console.log(import_chalk3.default.gray(" ... new code ..."));
3293
- console.log(import_chalk3.default.gray(" >>>>>>> NEW VERSION\n"));
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(import_chalk3.default.green(`
3300
- \u2713 Backup created: ${import_chalk3.default.cyan(backupPath)}`));
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(import_chalk3.default.bold("\n\u{1F4CA} Merge Summary:\n"));
3907
+ console.log(import_chalk6.default.bold("\n\u{1F4CA} Merge Summary:\n"));
3307
3908
  if (stats.merged > 0) {
3308
- console.log(import_chalk3.default.green(` \u2713 Merged: ${stats.merged} file(s)`));
3909
+ console.log(import_chalk6.default.green(` \u2713 Merged: ${stats.merged} file(s)`));
3309
3910
  }
3310
3911
  if (stats.kept > 0) {
3311
- console.log(import_chalk3.default.blue(` \u2192 Kept: ${stats.kept} file(s)`));
3912
+ console.log(import_chalk6.default.blue(` \u2192 Kept: ${stats.kept} file(s)`));
3312
3913
  }
3313
3914
  if (stats.overwritten > 0) {
3314
- console.log(import_chalk3.default.yellow(` \u26A0 Overwritten: ${stats.overwritten} file(s)`));
3915
+ console.log(import_chalk6.default.yellow(` \u26A0 Overwritten: ${stats.overwritten} file(s)`));
3315
3916
  }
3316
3917
  if (stats.conflicts > 0) {
3317
- console.log(import_chalk3.default.red(` \u26A0 Conflicts: ${stats.conflicts} file(s)`));
3318
- console.log(import_chalk3.default.gray("\n Please resolve conflicts manually before committing.\n"));
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(import_chalk4.default.bold("\n\u{1F4E6} Available Modules:\n"));
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(` ${import_chalk4.default.cyan(key.padEnd(15))} ${mod.name}`);
3496
- console.log(` ${" ".repeat(15)} ${import_chalk4.default.gray(mod.description)}
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(import_chalk4.default.bold("Usage:"));
3500
- console.log(` ${import_chalk4.default.yellow("servcraft add auth")} Add authentication module`);
3501
- console.log(` ${import_chalk4.default.yellow("servcraft add users")} Add user management module`);
3502
- console.log(` ${import_chalk4.default.yellow("servcraft add email")} Add email service module
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
- error(`Unknown module: ${moduleName}`);
3509
- info('Run "servcraft add --list" to see available modules');
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 = import_path4.default.join(getModulesDir(), moduleName);
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(import_chalk4.default.bold("\n\u2705 Added to .env:"));
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(import_chalk4.default.bold("\n\u23ED\uFE0F Already in .env (skipped):"));
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(import_chalk4.default.bold("\n\u26A0\uFE0F Required configuration:"));
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
- console.log("\n\u{1F4CC} Next steps:");
3600
- info(" 1. Configure environment variables in .env (if needed)");
3601
- info(" 2. Register the module in your main app file");
3602
- info(" 3. Run database migrations if needed");
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(import_path4.default.join(dir, name), content);
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(import_path4.default.join(dir, name), content);
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(import_path4.default.join(dir, name), content);
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(import_path4.default.join(dir, name), content);
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(import_path4.default.join(dir, name), content);
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(import_path4.default.join(dir, name), content);
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(import_path4.default.join(dir, fileName), content);
4495
+ await writeFile(import_path5.default.join(dir, fileName), content);
3881
4496
  }
3882
4497
  }
3883
4498
  async function findServercraftModules() {
3884
- const scriptDir = import_path4.default.dirname(new URL(importMetaUrl).pathname);
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
- import_path4.default.join(process.cwd(), "node_modules", "servcraft", "src", "modules"),
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
- import_path4.default.resolve(scriptDir, "..", "..", "src", "modules"),
4504
+ import_path5.default.resolve(scriptDir, "..", "..", "src", "modules"),
3890
4505
  // From src/cli/commands/add-module.ts -> src/modules (development)
3891
- import_path4.default.resolve(scriptDir, "..", "..", "modules")
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 = import_path4.default.join(servercraftModulesDir, sourceDirName);
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 = import_path4.default.join(sourceDir, entry.name);
3948
- const targetPath = import_path4.default.join(targetDir, entry.name);
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 = import_path4.default.join(moduleDir, entry);
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(import_chalk4.default.cyan(`
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(import_chalk4.default.yellow(`
4592
+ console.log(import_chalk7.default.yellow(`
3978
4593
  \u{1F4C4} ${file.fileName}:`));
3979
- const currentPath = import_path4.default.join(moduleDir, file.fileName);
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 = import_path4.default.join(templateManager["templatesDir"], moduleName);
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(import_path4.default.join(templateDir, entry), "utf-8");
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 = import_path4.default.join(moduleDir, fileName);
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 import_chalk5 = __toESM(require("chalk"), 1);
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(import_chalk5.default.yellow("\n\u26A0\uFE0F WARNING: This will delete all data in your database!\n"));
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 import_path6 = __toESM(require("path"), 1);
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 import_chalk6 = __toESM(require("chalk"), 1);
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 import_path5 = __toESM(require("path"), 1);
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 path9 = issue.path.join(".") || "root";
4906
- if (!errors[path9]) {
4907
- errors[path9] = [];
5520
+ const path12 = issue.path.join(".") || "root";
5521
+ if (!errors[path12]) {
5522
+ errors[path12] = [];
4908
5523
  }
4909
- errors[path9].push(issue.message);
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 = import_path5.default.resolve(outputPath);
5743
- await import_promises3.default.mkdir(import_path5.default.dirname(absoluteOutput), { recursive: true });
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 = import_path6.default.join(projectRoot, "openapi.json");
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 = import_path6.default.join(projectRoot, options.output || defaultName);
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(import_chalk6.default.bold("\n\u{1F4CA} Documentation Status\n"));
5829
- const specPath = import_path6.default.join(projectRoot, "openapi.json");
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 import_commander6.Command();
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