servcraft 0.1.7 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -1,21 +1,110 @@
1
1
  #!/usr/bin/env node
2
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
+ }) : x)(function(x) {
5
+ if (typeof require !== "undefined") return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
2
8
 
3
9
  // src/cli/index.ts
4
- import { Command as Command6 } from "commander";
10
+ import { Command as Command11 } from "commander";
5
11
 
6
12
  // src/cli/commands/init.ts
7
13
  import { Command } from "commander";
8
- import path2 from "path";
14
+ import path3 from "path";
9
15
  import fs2 from "fs/promises";
10
16
  import ora from "ora";
11
17
  import inquirer from "inquirer";
12
- import chalk2 from "chalk";
18
+ import chalk3 from "chalk";
13
19
  import { execSync } from "child_process";
14
20
 
15
21
  // src/cli/utils/helpers.ts
16
22
  import fs from "fs/promises";
17
- import path from "path";
23
+ import path2 from "path";
24
+ import chalk2 from "chalk";
25
+
26
+ // src/cli/utils/dry-run.ts
18
27
  import chalk from "chalk";
28
+ import path from "path";
29
+ var DryRunManager = class _DryRunManager {
30
+ static instance;
31
+ enabled = false;
32
+ operations = [];
33
+ constructor() {
34
+ }
35
+ static getInstance() {
36
+ if (!_DryRunManager.instance) {
37
+ _DryRunManager.instance = new _DryRunManager();
38
+ }
39
+ return _DryRunManager.instance;
40
+ }
41
+ enable() {
42
+ this.enabled = true;
43
+ this.operations = [];
44
+ }
45
+ disable() {
46
+ this.enabled = false;
47
+ this.operations = [];
48
+ }
49
+ isEnabled() {
50
+ return this.enabled;
51
+ }
52
+ addOperation(operation) {
53
+ if (this.enabled) {
54
+ this.operations.push(operation);
55
+ }
56
+ }
57
+ getOperations() {
58
+ return [...this.operations];
59
+ }
60
+ printSummary() {
61
+ if (!this.enabled || this.operations.length === 0) {
62
+ return;
63
+ }
64
+ console.log(chalk.bold.yellow("\n\u{1F4CB} Dry Run - Preview of changes:\n"));
65
+ console.log(chalk.gray("No files will be written. Remove --dry-run to apply changes.\n"));
66
+ const createOps = this.operations.filter((op) => op.type === "create");
67
+ const modifyOps = this.operations.filter((op) => op.type === "modify");
68
+ const deleteOps = this.operations.filter((op) => op.type === "delete");
69
+ if (createOps.length > 0) {
70
+ console.log(chalk.green.bold(`
71
+ \u2713 Files to be created (${createOps.length}):`));
72
+ createOps.forEach((op) => {
73
+ const size = op.content ? `${op.content.length} bytes` : "unknown size";
74
+ console.log(` ${chalk.green("+")} ${chalk.cyan(op.path)} ${chalk.gray(`(${size})`)}`);
75
+ });
76
+ }
77
+ if (modifyOps.length > 0) {
78
+ console.log(chalk.yellow.bold(`
79
+ ~ Files to be modified (${modifyOps.length}):`));
80
+ modifyOps.forEach((op) => {
81
+ console.log(` ${chalk.yellow("~")} ${chalk.cyan(op.path)}`);
82
+ });
83
+ }
84
+ if (deleteOps.length > 0) {
85
+ console.log(chalk.red.bold(`
86
+ - Files to be deleted (${deleteOps.length}):`));
87
+ deleteOps.forEach((op) => {
88
+ console.log(` ${chalk.red("-")} ${chalk.cyan(op.path)}`);
89
+ });
90
+ }
91
+ console.log(chalk.gray("\n" + "\u2500".repeat(60)));
92
+ console.log(
93
+ chalk.bold(` Total operations: ${this.operations.length}`) + chalk.gray(
94
+ ` (${createOps.length} create, ${modifyOps.length} modify, ${deleteOps.length} delete)`
95
+ )
96
+ );
97
+ console.log(chalk.gray("\u2500".repeat(60)));
98
+ console.log(chalk.yellow("\n\u26A0 This was a dry run. No files were created or modified."));
99
+ console.log(chalk.gray(" Remove --dry-run to apply these changes.\n"));
100
+ }
101
+ // Helper to format file path relative to cwd
102
+ relativePath(filePath) {
103
+ return path.relative(process.cwd(), filePath);
104
+ }
105
+ };
106
+
107
+ // src/cli/utils/helpers.ts
19
108
  function toPascalCase(str) {
20
109
  return str.split(/[-_\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
21
110
  }
@@ -47,39 +136,54 @@ async function ensureDir(dirPath) {
47
136
  await fs.mkdir(dirPath, { recursive: true });
48
137
  }
49
138
  async function writeFile(filePath, content) {
50
- await ensureDir(path.dirname(filePath));
139
+ const dryRun = DryRunManager.getInstance();
140
+ if (dryRun.isEnabled()) {
141
+ dryRun.addOperation({
142
+ type: "create",
143
+ path: dryRun.relativePath(filePath),
144
+ content,
145
+ size: content.length
146
+ });
147
+ return;
148
+ }
149
+ await ensureDir(path2.dirname(filePath));
51
150
  await fs.writeFile(filePath, content, "utf-8");
52
151
  }
53
152
  function success(message) {
54
- console.log(chalk.green("\u2713"), message);
153
+ console.log(chalk2.green("\u2713"), message);
55
154
  }
56
155
  function error(message) {
57
- console.error(chalk.red("\u2717"), message);
156
+ console.error(chalk2.red("\u2717"), message);
58
157
  }
59
158
  function warn(message) {
60
- console.log(chalk.yellow("\u26A0"), message);
159
+ console.log(chalk2.yellow("\u26A0"), message);
61
160
  }
62
161
  function info(message) {
63
- console.log(chalk.blue("\u2139"), message);
162
+ console.log(chalk2.blue("\u2139"), message);
64
163
  }
65
164
  function getProjectRoot() {
66
165
  return process.cwd();
67
166
  }
68
167
  function getSourceDir() {
69
- return path.join(getProjectRoot(), "src");
168
+ return path2.join(getProjectRoot(), "src");
70
169
  }
71
170
  function getModulesDir() {
72
- return path.join(getSourceDir(), "modules");
171
+ return path2.join(getSourceDir(), "modules");
73
172
  }
74
173
 
75
174
  // src/cli/commands/init.ts
76
- var initCommand = new 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(
175
+ var initCommand = new 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(
77
176
  async (name, cmdOptions) => {
177
+ const dryRun = DryRunManager.getInstance();
178
+ if (cmdOptions?.dryRun) {
179
+ dryRun.enable();
180
+ console.log(chalk3.yellow("\n\u26A0 DRY RUN MODE - No files will be written\n"));
181
+ }
78
182
  console.log(
79
- chalk2.blue(`
183
+ chalk3.blue(`
80
184
  \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
81
185
  \u2551 \u2551
82
- \u2551 ${chalk2.bold("\u{1F680} Servcraft Project Generator")} \u2551
186
+ \u2551 ${chalk3.bold("\u{1F680} Servcraft Project Generator")} \u2551
83
187
  \u2551 \u2551
84
188
  \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
85
189
  `)
@@ -174,7 +278,7 @@ var initCommand = new Command("init").alias("new").description("Initialize a new
174
278
  orm: db === "mongodb" ? "mongoose" : db === "none" ? "none" : "prisma"
175
279
  };
176
280
  }
177
- const projectDir = path2.resolve(process.cwd(), options.name);
281
+ const projectDir = path3.resolve(process.cwd(), options.name);
178
282
  const spinner = ora("Creating project...").start();
179
283
  try {
180
284
  try {
@@ -188,21 +292,21 @@ var initCommand = new Command("init").alias("new").description("Initialize a new
188
292
  spinner.text = "Generating project files...";
189
293
  const packageJson = generatePackageJson(options);
190
294
  await writeFile(
191
- path2.join(projectDir, "package.json"),
295
+ path3.join(projectDir, "package.json"),
192
296
  JSON.stringify(packageJson, null, 2)
193
297
  );
194
298
  if (options.language === "typescript") {
195
- await writeFile(path2.join(projectDir, "tsconfig.json"), generateTsConfig(options));
196
- await writeFile(path2.join(projectDir, "tsup.config.ts"), generateTsupConfig(options));
299
+ await writeFile(path3.join(projectDir, "tsconfig.json"), generateTsConfig(options));
300
+ await writeFile(path3.join(projectDir, "tsup.config.ts"), generateTsupConfig(options));
197
301
  } else {
198
- await writeFile(path2.join(projectDir, "jsconfig.json"), generateJsConfig(options));
302
+ await writeFile(path3.join(projectDir, "jsconfig.json"), generateJsConfig(options));
199
303
  }
200
- await writeFile(path2.join(projectDir, ".env.example"), generateEnvExample(options));
201
- await writeFile(path2.join(projectDir, ".env"), generateEnvExample(options));
202
- await writeFile(path2.join(projectDir, ".gitignore"), generateGitignore());
203
- await writeFile(path2.join(projectDir, "Dockerfile"), generateDockerfile(options));
304
+ await writeFile(path3.join(projectDir, ".env.example"), generateEnvExample(options));
305
+ await writeFile(path3.join(projectDir, ".env"), generateEnvExample(options));
306
+ await writeFile(path3.join(projectDir, ".gitignore"), generateGitignore());
307
+ await writeFile(path3.join(projectDir, "Dockerfile"), generateDockerfile(options));
204
308
  await writeFile(
205
- path2.join(projectDir, "docker-compose.yml"),
309
+ path3.join(projectDir, "docker-compose.yml"),
206
310
  generateDockerCompose(options)
207
311
  );
208
312
  const ext = options.language === "typescript" ? "ts" : options.moduleSystem === "esm" ? "js" : "cjs";
@@ -223,59 +327,63 @@ var initCommand = new Command("init").alias("new").description("Initialize a new
223
327
  dirs.push("src/database/models");
224
328
  }
225
329
  for (const dir of dirs) {
226
- await ensureDir(path2.join(projectDir, dir));
330
+ await ensureDir(path3.join(projectDir, dir));
227
331
  }
228
- await writeFile(path2.join(projectDir, `src/index.${ext}`), generateEntryFile(options));
332
+ await writeFile(path3.join(projectDir, `src/index.${ext}`), generateEntryFile(options));
229
333
  await writeFile(
230
- path2.join(projectDir, `src/core/server.${ext}`),
334
+ path3.join(projectDir, `src/core/server.${ext}`),
231
335
  generateServerFile(options)
232
336
  );
233
337
  await writeFile(
234
- path2.join(projectDir, `src/core/logger.${ext}`),
338
+ path3.join(projectDir, `src/core/logger.${ext}`),
235
339
  generateLoggerFile(options)
236
340
  );
237
341
  await writeFile(
238
- path2.join(projectDir, `src/config/index.${ext}`),
342
+ path3.join(projectDir, `src/config/index.${ext}`),
239
343
  generateConfigFile(options)
240
344
  );
241
345
  await writeFile(
242
- path2.join(projectDir, `src/middleware/index.${ext}`),
346
+ path3.join(projectDir, `src/middleware/index.${ext}`),
243
347
  generateMiddlewareFile(options)
244
348
  );
245
349
  await writeFile(
246
- path2.join(projectDir, `src/utils/index.${ext}`),
350
+ path3.join(projectDir, `src/utils/index.${ext}`),
247
351
  generateUtilsFile(options)
248
352
  );
249
353
  await writeFile(
250
- path2.join(projectDir, `src/types/index.${ext}`),
354
+ path3.join(projectDir, `src/types/index.${ext}`),
251
355
  generateTypesFile(options)
252
356
  );
253
357
  if (options.orm === "prisma") {
254
358
  await writeFile(
255
- path2.join(projectDir, "prisma/schema.prisma"),
359
+ path3.join(projectDir, "prisma/schema.prisma"),
256
360
  generatePrismaSchema(options)
257
361
  );
258
362
  } else if (options.orm === "mongoose") {
259
363
  await writeFile(
260
- path2.join(projectDir, `src/database/connection.${ext}`),
364
+ path3.join(projectDir, `src/database/connection.${ext}`),
261
365
  generateMongooseConnection(options)
262
366
  );
263
367
  await writeFile(
264
- path2.join(projectDir, `src/database/models/user.model.${ext}`),
368
+ path3.join(projectDir, `src/database/models/user.model.${ext}`),
265
369
  generateMongooseUserModel(options)
266
370
  );
267
371
  }
268
372
  spinner.succeed("Project files generated!");
269
- const installSpinner = ora("Installing dependencies...").start();
270
- try {
271
- execSync("npm install", { cwd: projectDir, stdio: "pipe" });
272
- installSpinner.succeed("Dependencies installed!");
273
- } catch {
274
- installSpinner.warn("Failed to install dependencies automatically");
275
- warn(' Run "npm install" manually in the project directory');
373
+ if (!cmdOptions?.dryRun) {
374
+ const installSpinner = ora("Installing dependencies...").start();
375
+ try {
376
+ execSync("npm install", { cwd: projectDir, stdio: "pipe" });
377
+ installSpinner.succeed("Dependencies installed!");
378
+ } catch {
379
+ installSpinner.warn("Failed to install dependencies automatically");
380
+ warn(' Run "npm install" manually in the project directory');
381
+ }
382
+ }
383
+ if (!cmdOptions?.dryRun) {
384
+ console.log("\n" + chalk3.green("\u2728 Project created successfully!"));
276
385
  }
277
- console.log("\n" + chalk2.green("\u2728 Project created successfully!"));
278
- console.log("\n" + chalk2.bold("\u{1F4C1} Project structure:"));
386
+ console.log("\n" + chalk3.bold("\u{1F4C1} Project structure:"));
279
387
  console.log(`
280
388
  ${options.name}/
281
389
  \u251C\u2500\u2500 src/
@@ -290,19 +398,22 @@ var initCommand = new Command("init").alias("new").description("Initialize a new
290
398
  \u251C\u2500\u2500 docker-compose.yml
291
399
  \u2514\u2500\u2500 package.json
292
400
  `);
293
- console.log(chalk2.bold("\u{1F680} Get started:"));
401
+ console.log(chalk3.bold("\u{1F680} Get started:"));
294
402
  console.log(`
295
- ${chalk2.cyan(`cd ${options.name}`)}
296
- ${options.database !== "none" ? chalk2.cyan("npm run db:push # Setup database") : ""}
297
- ${chalk2.cyan("npm run dev # Start development server")}
403
+ ${chalk3.cyan(`cd ${options.name}`)}
404
+ ${options.database !== "none" ? chalk3.cyan("npm run db:push # Setup database") : ""}
405
+ ${chalk3.cyan("npm run dev # Start development server")}
298
406
  `);
299
- console.log(chalk2.bold("\u{1F4DA} Available commands:"));
407
+ console.log(chalk3.bold("\u{1F4DA} Available commands:"));
300
408
  console.log(`
301
- ${chalk2.yellow("servcraft generate module <name>")} Generate a new module
302
- ${chalk2.yellow("servcraft generate controller <name>")} Generate a controller
303
- ${chalk2.yellow("servcraft generate service <name>")} Generate a service
304
- ${chalk2.yellow("servcraft add auth")} Add authentication module
409
+ ${chalk3.yellow("servcraft generate module <name>")} Generate a new module
410
+ ${chalk3.yellow("servcraft generate controller <name>")} Generate a controller
411
+ ${chalk3.yellow("servcraft generate service <name>")} Generate a service
412
+ ${chalk3.yellow("servcraft add auth")} Add authentication module
305
413
  `);
414
+ if (cmdOptions?.dryRun) {
415
+ dryRun.printSummary();
416
+ }
306
417
  } catch (err) {
307
418
  spinner.fail("Failed to create project");
308
419
  error(err instanceof Error ? err.message : String(err));
@@ -1094,9 +1205,10 @@ declare module 'fastify' {
1094
1205
 
1095
1206
  // src/cli/commands/generate.ts
1096
1207
  import { Command as Command2 } from "commander";
1097
- import path3 from "path";
1208
+ import path4 from "path";
1098
1209
  import ora2 from "ora";
1099
1210
  import inquirer2 from "inquirer";
1211
+ import chalk5 from "chalk";
1100
1212
 
1101
1213
  // src/cli/utils/field-parser.ts
1102
1214
  var tsTypeMap = {
@@ -1240,6 +1352,108 @@ function parseFields(fieldsStr) {
1240
1352
  return fieldsStr.split(/\s+/).filter(Boolean).map(parseField);
1241
1353
  }
1242
1354
 
1355
+ // src/cli/utils/error-handler.ts
1356
+ import chalk4 from "chalk";
1357
+ var ServCraftError = class extends Error {
1358
+ suggestions;
1359
+ docsLink;
1360
+ constructor(message, suggestions = [], docsLink) {
1361
+ super(message);
1362
+ this.name = "ServCraftError";
1363
+ this.suggestions = suggestions;
1364
+ this.docsLink = docsLink;
1365
+ }
1366
+ };
1367
+ var ErrorTypes = {
1368
+ MODULE_NOT_FOUND: (moduleName) => new ServCraftError(
1369
+ `Module "${moduleName}" not found`,
1370
+ [
1371
+ `Run ${chalk4.cyan("servcraft list")} to see available modules`,
1372
+ `Check the spelling of the module name`,
1373
+ `Visit ${chalk4.blue("https://github.com/Le-Sourcier/servcraft#modules")} for module list`
1374
+ ],
1375
+ "https://github.com/Le-Sourcier/servcraft#add-pre-built-modules"
1376
+ ),
1377
+ MODULE_ALREADY_EXISTS: (moduleName) => new ServCraftError(`Module "${moduleName}" already exists`, [
1378
+ `Use ${chalk4.cyan("servcraft add " + moduleName + " --force")} to overwrite`,
1379
+ `Use ${chalk4.cyan("servcraft add " + moduleName + " --update")} to update`,
1380
+ `Use ${chalk4.cyan("servcraft add " + moduleName + " --skip-existing")} to skip`
1381
+ ]),
1382
+ NOT_IN_PROJECT: () => new ServCraftError(
1383
+ "Not in a ServCraft project directory",
1384
+ [
1385
+ `Run ${chalk4.cyan("servcraft init")} to create a new project`,
1386
+ `Navigate to your ServCraft project directory`,
1387
+ `Check if ${chalk4.yellow("package.json")} exists`
1388
+ ],
1389
+ "https://github.com/Le-Sourcier/servcraft#initialize-project"
1390
+ ),
1391
+ FILE_ALREADY_EXISTS: (fileName) => new ServCraftError(`File "${fileName}" already exists`, [
1392
+ `Use ${chalk4.cyan("--force")} flag to overwrite`,
1393
+ `Choose a different name`,
1394
+ `Delete the existing file first`
1395
+ ]),
1396
+ INVALID_DATABASE: (database) => new ServCraftError(`Invalid database type: "${database}"`, [
1397
+ `Valid options: ${chalk4.cyan("postgresql, mysql, sqlite, mongodb, none")}`,
1398
+ `Use ${chalk4.cyan("servcraft init --db postgresql")} for PostgreSQL`
1399
+ ]),
1400
+ INVALID_VALIDATOR: (validator) => new ServCraftError(`Invalid validator type: "${validator}"`, [
1401
+ `Valid options: ${chalk4.cyan("zod, joi, yup")}`,
1402
+ `Default is ${chalk4.cyan("zod")}`
1403
+ ]),
1404
+ MISSING_DEPENDENCY: (dependency, command) => new ServCraftError(`Missing dependency: "${dependency}"`, [
1405
+ `Run ${chalk4.cyan(command)} to install`,
1406
+ `Check your ${chalk4.yellow("package.json")}`
1407
+ ]),
1408
+ INVALID_FIELD_FORMAT: (field) => new ServCraftError(`Invalid field format: "${field}"`, [
1409
+ `Expected format: ${chalk4.cyan("name:type")}`,
1410
+ `Example: ${chalk4.cyan("name:string age:number isActive:boolean")}`,
1411
+ `Supported types: string, number, boolean, date`
1412
+ ]),
1413
+ GIT_NOT_INITIALIZED: () => new ServCraftError("Git repository not initialized", [
1414
+ `Run ${chalk4.cyan("git init")} to initialize git`,
1415
+ `This is required for some ServCraft features`
1416
+ ])
1417
+ };
1418
+ function displayError(error2) {
1419
+ console.error("\n" + chalk4.red.bold("\u2717 Error: ") + chalk4.red(error2.message));
1420
+ if (error2 instanceof ServCraftError) {
1421
+ if (error2.suggestions.length > 0) {
1422
+ console.log("\n" + chalk4.yellow.bold("\u{1F4A1} Suggestions:"));
1423
+ error2.suggestions.forEach((suggestion) => {
1424
+ console.log(chalk4.yellow(" \u2022 ") + suggestion);
1425
+ });
1426
+ }
1427
+ if (error2.docsLink) {
1428
+ console.log(
1429
+ "\n" + chalk4.blue.bold("\u{1F4DA} Documentation: ") + chalk4.blue.underline(error2.docsLink)
1430
+ );
1431
+ }
1432
+ }
1433
+ console.log();
1434
+ }
1435
+ function validateProject() {
1436
+ try {
1437
+ const fs12 = __require("fs");
1438
+ if (!fs12.existsSync("package.json")) {
1439
+ return ErrorTypes.NOT_IN_PROJECT();
1440
+ }
1441
+ const packageJson = JSON.parse(fs12.readFileSync("package.json", "utf-8"));
1442
+ if (!packageJson.dependencies?.fastify) {
1443
+ return new ServCraftError("This does not appear to be a ServCraft project", [
1444
+ `ServCraft projects require Fastify`,
1445
+ `Run ${chalk4.cyan("servcraft init")} to create a new project`
1446
+ ]);
1447
+ }
1448
+ return null;
1449
+ } catch {
1450
+ return new ServCraftError("Failed to validate project", [
1451
+ `Ensure you are in the project root directory`,
1452
+ `Check if ${chalk4.yellow("package.json")} is valid`
1453
+ ]);
1454
+ }
1455
+ }
1456
+
1243
1457
  // src/cli/templates/controller.ts
1244
1458
  function controllerTemplate(name, pascalName, camelName) {
1245
1459
  return `import type { FastifyRequest, FastifyReply } from 'fastify';
@@ -1928,11 +2142,371 @@ ${indexLines.join("\n")}
1928
2142
  `;
1929
2143
  }
1930
2144
 
2145
+ // src/cli/templates/controller-test.ts
2146
+ function controllerTestTemplate(name, pascalName, camelName) {
2147
+ return `import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2148
+ import { build } from '../../app.js';
2149
+ import { FastifyInstance } from 'fastify';
2150
+
2151
+ describe('${pascalName}Controller', () => {
2152
+ let app: FastifyInstance;
2153
+
2154
+ beforeAll(async () => {
2155
+ app = await build();
2156
+ await app.ready();
2157
+ });
2158
+
2159
+ afterAll(async () => {
2160
+ await app.close();
2161
+ });
2162
+
2163
+ describe('GET /${name}', () => {
2164
+ it('should return list of ${name}', async () => {
2165
+ const response = await app.inject({
2166
+ method: 'GET',
2167
+ url: '/${name}',
2168
+ });
2169
+
2170
+ expect(response.statusCode).toBe(200);
2171
+ expect(response.json()).toHaveProperty('data');
2172
+ });
2173
+ });
2174
+
2175
+ describe('GET /${name}/:id', () => {
2176
+ it('should return a single ${camelName}', async () => {
2177
+ // TODO: Create test ${camelName} first
2178
+ const response = await app.inject({
2179
+ method: 'GET',
2180
+ url: '/${name}/1',
2181
+ });
2182
+
2183
+ expect(response.statusCode).toBe(200);
2184
+ expect(response.json()).toHaveProperty('data');
2185
+ });
2186
+
2187
+ it('should return 404 for non-existent ${camelName}', async () => {
2188
+ const response = await app.inject({
2189
+ method: 'GET',
2190
+ url: '/${name}/999999',
2191
+ });
2192
+
2193
+ expect(response.statusCode).toBe(404);
2194
+ });
2195
+ });
2196
+
2197
+ describe('POST /${name}', () => {
2198
+ it('should create a new ${camelName}', async () => {
2199
+ const response = await app.inject({
2200
+ method: 'POST',
2201
+ url: '/${name}',
2202
+ payload: {
2203
+ // TODO: Add required fields
2204
+ },
2205
+ });
2206
+
2207
+ expect(response.statusCode).toBe(201);
2208
+ expect(response.json()).toHaveProperty('data');
2209
+ });
2210
+
2211
+ it('should return 400 for invalid data', async () => {
2212
+ const response = await app.inject({
2213
+ method: 'POST',
2214
+ url: '/${name}',
2215
+ payload: {},
2216
+ });
2217
+
2218
+ expect(response.statusCode).toBe(400);
2219
+ });
2220
+ });
2221
+
2222
+ describe('PUT /${name}/:id', () => {
2223
+ it('should update a ${camelName}', async () => {
2224
+ // TODO: Create test ${camelName} first
2225
+ const response = await app.inject({
2226
+ method: 'PUT',
2227
+ url: '/${name}/1',
2228
+ payload: {
2229
+ // TODO: Add fields to update
2230
+ },
2231
+ });
2232
+
2233
+ expect(response.statusCode).toBe(200);
2234
+ expect(response.json()).toHaveProperty('data');
2235
+ });
2236
+ });
2237
+
2238
+ describe('DELETE /${name}/:id', () => {
2239
+ it('should delete a ${camelName}', async () => {
2240
+ // TODO: Create test ${camelName} first
2241
+ const response = await app.inject({
2242
+ method: 'DELETE',
2243
+ url: '/${name}/1',
2244
+ });
2245
+
2246
+ expect(response.statusCode).toBe(204);
2247
+ });
2248
+ });
2249
+ });
2250
+ `;
2251
+ }
2252
+
2253
+ // src/cli/templates/service-test.ts
2254
+ function serviceTestTemplate(name, pascalName, camelName) {
2255
+ return `import { describe, it, expect, beforeEach } from 'vitest';
2256
+ import { ${pascalName}Service } from '../${name}.service.js';
2257
+
2258
+ describe('${pascalName}Service', () => {
2259
+ let service: ${pascalName}Service;
2260
+
2261
+ beforeEach(() => {
2262
+ service = new ${pascalName}Service();
2263
+ });
2264
+
2265
+ describe('getAll', () => {
2266
+ it('should return all ${name}', async () => {
2267
+ const result = await service.getAll();
2268
+
2269
+ expect(result).toBeDefined();
2270
+ expect(Array.isArray(result)).toBe(true);
2271
+ });
2272
+
2273
+ it('should apply pagination', async () => {
2274
+ const result = await service.getAll({ page: 1, limit: 10 });
2275
+
2276
+ expect(result).toBeDefined();
2277
+ expect(result.length).toBeLessThanOrEqual(10);
2278
+ });
2279
+ });
2280
+
2281
+ describe('getById', () => {
2282
+ it('should return a ${camelName} by id', async () => {
2283
+ // TODO: Create test ${camelName} first
2284
+ const id = '1';
2285
+ const result = await service.getById(id);
2286
+
2287
+ expect(result).toBeDefined();
2288
+ expect(result.id).toBe(id);
2289
+ });
2290
+
2291
+ it('should return null for non-existent id', async () => {
2292
+ const result = await service.getById('999999');
2293
+
2294
+ expect(result).toBeNull();
2295
+ });
2296
+ });
2297
+
2298
+ describe('create', () => {
2299
+ it('should create a new ${camelName}', async () => {
2300
+ const data = {
2301
+ // TODO: Add required fields
2302
+ };
2303
+
2304
+ const result = await service.create(data);
2305
+
2306
+ expect(result).toBeDefined();
2307
+ expect(result.id).toBeDefined();
2308
+ });
2309
+
2310
+ it('should throw error for invalid data', async () => {
2311
+ await expect(service.create({} as any)).rejects.toThrow();
2312
+ });
2313
+ });
2314
+
2315
+ describe('update', () => {
2316
+ it('should update a ${camelName}', async () => {
2317
+ // TODO: Create test ${camelName} first
2318
+ const id = '1';
2319
+ const updates = {
2320
+ // TODO: Add fields to update
2321
+ };
2322
+
2323
+ const result = await service.update(id, updates);
2324
+
2325
+ expect(result).toBeDefined();
2326
+ expect(result.id).toBe(id);
2327
+ });
2328
+
2329
+ it('should return null for non-existent id', async () => {
2330
+ const result = await service.update('999999', {});
2331
+
2332
+ expect(result).toBeNull();
2333
+ });
2334
+ });
2335
+
2336
+ describe('delete', () => {
2337
+ it('should delete a ${camelName}', async () => {
2338
+ // TODO: Create test ${camelName} first
2339
+ const id = '1';
2340
+ const result = await service.delete(id);
2341
+
2342
+ expect(result).toBe(true);
2343
+ });
2344
+
2345
+ it('should return false for non-existent id', async () => {
2346
+ const result = await service.delete('999999');
2347
+
2348
+ expect(result).toBe(false);
2349
+ });
2350
+ });
2351
+ });
2352
+ `;
2353
+ }
2354
+
2355
+ // src/cli/templates/integration-test.ts
2356
+ function integrationTestTemplate(name, pascalName, camelName) {
2357
+ return `import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
2358
+ import { build } from '../../app.js';
2359
+ import { FastifyInstance } from 'fastify';
2360
+ import { prisma } from '../../lib/prisma.js';
2361
+
2362
+ describe('${pascalName} Integration Tests', () => {
2363
+ let app: FastifyInstance;
2364
+
2365
+ beforeAll(async () => {
2366
+ app = await build();
2367
+ await app.ready();
2368
+ });
2369
+
2370
+ afterAll(async () => {
2371
+ await app.close();
2372
+ await prisma.$disconnect();
2373
+ });
2374
+
2375
+ beforeEach(async () => {
2376
+ // Clean up test data
2377
+ // await prisma.${camelName}.deleteMany();
2378
+ });
2379
+
2380
+ describe('Full CRUD workflow', () => {
2381
+ it('should create, read, update, and delete a ${camelName}', async () => {
2382
+ // Create
2383
+ const createResponse = await app.inject({
2384
+ method: 'POST',
2385
+ url: '/${name}',
2386
+ payload: {
2387
+ // TODO: Add required fields
2388
+ },
2389
+ });
2390
+
2391
+ expect(createResponse.statusCode).toBe(201);
2392
+ const created = createResponse.json().data;
2393
+ expect(created.id).toBeDefined();
2394
+
2395
+ // Read
2396
+ const readResponse = await app.inject({
2397
+ method: 'GET',
2398
+ url: \`/${name}/\${created.id}\`,
2399
+ });
2400
+
2401
+ expect(readResponse.statusCode).toBe(200);
2402
+ const read = readResponse.json().data;
2403
+ expect(read.id).toBe(created.id);
2404
+
2405
+ // Update
2406
+ const updateResponse = await app.inject({
2407
+ method: 'PUT',
2408
+ url: \`/${name}/\${created.id}\`,
2409
+ payload: {
2410
+ // TODO: Add fields to update
2411
+ },
2412
+ });
2413
+
2414
+ expect(updateResponse.statusCode).toBe(200);
2415
+ const updated = updateResponse.json().data;
2416
+ expect(updated.id).toBe(created.id);
2417
+
2418
+ // Delete
2419
+ const deleteResponse = await app.inject({
2420
+ method: 'DELETE',
2421
+ url: \`/${name}/\${created.id}\`,
2422
+ });
2423
+
2424
+ expect(deleteResponse.statusCode).toBe(204);
2425
+
2426
+ // Verify deletion
2427
+ const verifyResponse = await app.inject({
2428
+ method: 'GET',
2429
+ url: \`/${name}/\${created.id}\`,
2430
+ });
2431
+
2432
+ expect(verifyResponse.statusCode).toBe(404);
2433
+ });
2434
+ });
2435
+
2436
+ describe('List and pagination', () => {
2437
+ it('should list ${name} with pagination', async () => {
2438
+ // Create multiple ${name}
2439
+ const count = 5;
2440
+ for (let i = 0; i < count; i++) {
2441
+ await app.inject({
2442
+ method: 'POST',
2443
+ url: '/${name}',
2444
+ payload: {
2445
+ // TODO: Add required fields
2446
+ },
2447
+ });
2448
+ }
2449
+
2450
+ // Test pagination
2451
+ const response = await app.inject({
2452
+ method: 'GET',
2453
+ url: '/${name}?page=1&limit=3',
2454
+ });
2455
+
2456
+ expect(response.statusCode).toBe(200);
2457
+ const result = response.json();
2458
+ expect(result.data).toBeDefined();
2459
+ expect(result.data.length).toBeLessThanOrEqual(3);
2460
+ expect(result.total).toBeGreaterThanOrEqual(count);
2461
+ });
2462
+ });
2463
+
2464
+ describe('Validation', () => {
2465
+ it('should validate required fields on create', async () => {
2466
+ const response = await app.inject({
2467
+ method: 'POST',
2468
+ url: '/${name}',
2469
+ payload: {},
2470
+ });
2471
+
2472
+ expect(response.statusCode).toBe(400);
2473
+ expect(response.json()).toHaveProperty('error');
2474
+ });
2475
+
2476
+ it('should validate data types', async () => {
2477
+ const response = await app.inject({
2478
+ method: 'POST',
2479
+ url: '/${name}',
2480
+ payload: {
2481
+ // TODO: Add invalid field types
2482
+ },
2483
+ });
2484
+
2485
+ expect(response.statusCode).toBe(400);
2486
+ });
2487
+ });
2488
+ });
2489
+ `;
2490
+ }
2491
+
1931
2492
  // src/cli/commands/generate.ts
2493
+ function enableDryRunIfNeeded(options) {
2494
+ const dryRun = DryRunManager.getInstance();
2495
+ if (options.dryRun) {
2496
+ dryRun.enable();
2497
+ console.log(chalk5.yellow("\n\u26A0 DRY RUN MODE - No files will be written\n"));
2498
+ }
2499
+ }
2500
+ function showDryRunSummary(options) {
2501
+ if (options.dryRun) {
2502
+ DryRunManager.getInstance().printSummary();
2503
+ }
2504
+ }
1932
2505
  var generateCommand = new Command2("generate").alias("g").description("Generate resources (module, controller, service, etc.)");
1933
2506
  generateCommand.command("module <name> [fields...]").alias("m").description(
1934
2507
  "Generate a complete module with controller, service, repository, types, schemas, and routes"
1935
- ).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) => {
2508
+ ).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) => {
2509
+ enableDryRunIfNeeded(options);
1936
2510
  let fields = [];
1937
2511
  if (options.interactive) {
1938
2512
  fields = await promptForFields();
@@ -1947,7 +2521,7 @@ generateCommand.command("module <name> [fields...]").alias("m").description(
1947
2521
  const pluralName = pluralize(kebabName);
1948
2522
  const tableName = pluralize(kebabName.replace(/-/g, "_"));
1949
2523
  const validatorType = options.validator || "zod";
1950
- const moduleDir = path3.join(getModulesDir(), kebabName);
2524
+ const moduleDir = path4.join(getModulesDir(), kebabName);
1951
2525
  if (await fileExists(moduleDir)) {
1952
2526
  spinner.stop();
1953
2527
  error(`Module "${kebabName}" already exists`);
@@ -1986,7 +2560,22 @@ generateCommand.command("module <name> [fields...]").alias("m").description(
1986
2560
  });
1987
2561
  }
1988
2562
  for (const file of files) {
1989
- await writeFile(path3.join(moduleDir, file.name), file.content);
2563
+ await writeFile(path4.join(moduleDir, file.name), file.content);
2564
+ }
2565
+ if (options.withTests) {
2566
+ const testDir = path4.join(moduleDir, "__tests__");
2567
+ await writeFile(
2568
+ path4.join(testDir, `${kebabName}.controller.test.ts`),
2569
+ controllerTestTemplate(kebabName, pascalName, camelName)
2570
+ );
2571
+ await writeFile(
2572
+ path4.join(testDir, `${kebabName}.service.test.ts`),
2573
+ serviceTestTemplate(kebabName, pascalName, camelName)
2574
+ );
2575
+ await writeFile(
2576
+ path4.join(testDir, `${kebabName}.integration.test.ts`),
2577
+ integrationTestTemplate(kebabName, pascalName, camelName)
2578
+ );
1990
2579
  }
1991
2580
  spinner.succeed(`Module "${pascalName}" generated successfully!`);
1992
2581
  if (options.prisma || hasFields) {
@@ -2011,6 +2600,11 @@ generateCommand.command("module <name> [fields...]").alias("m").description(
2011
2600
  }
2012
2601
  console.log("\n\u{1F4C1} Files created:");
2013
2602
  files.forEach((f) => success(` src/modules/${kebabName}/${f.name}`));
2603
+ if (options.withTests) {
2604
+ success(` src/modules/${kebabName}/__tests__/${kebabName}.controller.test.ts`);
2605
+ success(` src/modules/${kebabName}/__tests__/${kebabName}.service.test.ts`);
2606
+ success(` src/modules/${kebabName}/__tests__/${kebabName}.integration.test.ts`);
2607
+ }
2014
2608
  console.log("\n\u{1F4CC} Next steps:");
2015
2609
  if (!hasFields) {
2016
2610
  info(` 1. Update the types in ${kebabName}.types.ts`);
@@ -2024,42 +2618,46 @@ generateCommand.command("module <name> [fields...]").alias("m").description(
2024
2618
  info(` ${hasFields ? "3" : "4"}. Add the Prisma model to schema.prisma`);
2025
2619
  info(` ${hasFields ? "4" : "5"}. Run: npm run db:migrate`);
2026
2620
  }
2621
+ showDryRunSummary(options);
2027
2622
  } catch (err) {
2028
2623
  spinner.fail("Failed to generate module");
2029
2624
  error(err instanceof Error ? err.message : String(err));
2030
2625
  }
2031
2626
  });
2032
- generateCommand.command("controller <name>").alias("c").description("Generate a controller").option("-m, --module <module>", "Target module name").action(async (name, options) => {
2627
+ 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) => {
2628
+ enableDryRunIfNeeded(options);
2033
2629
  const spinner = ora2("Generating controller...").start();
2034
2630
  try {
2035
2631
  const kebabName = toKebabCase(name);
2036
2632
  const pascalName = toPascalCase(name);
2037
2633
  const camelName = toCamelCase(name);
2038
2634
  const moduleName = options.module ? toKebabCase(options.module) : kebabName;
2039
- const moduleDir = path3.join(getModulesDir(), moduleName);
2040
- const filePath = path3.join(moduleDir, `${kebabName}.controller.ts`);
2635
+ const moduleDir = path4.join(getModulesDir(), moduleName);
2636
+ const filePath = path4.join(moduleDir, `${kebabName}.controller.ts`);
2041
2637
  if (await fileExists(filePath)) {
2042
2638
  spinner.stop();
2043
- error(`Controller "${kebabName}" already exists`);
2639
+ displayError(ErrorTypes.FILE_ALREADY_EXISTS(`${kebabName}.controller.ts`));
2044
2640
  return;
2045
2641
  }
2046
2642
  await writeFile(filePath, controllerTemplate(kebabName, pascalName, camelName));
2047
2643
  spinner.succeed(`Controller "${pascalName}Controller" generated!`);
2048
2644
  success(` src/modules/${moduleName}/${kebabName}.controller.ts`);
2645
+ showDryRunSummary(options);
2049
2646
  } catch (err) {
2050
2647
  spinner.fail("Failed to generate controller");
2051
2648
  error(err instanceof Error ? err.message : String(err));
2052
2649
  }
2053
2650
  });
2054
- generateCommand.command("service <name>").alias("s").description("Generate a service").option("-m, --module <module>", "Target module name").action(async (name, options) => {
2651
+ 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) => {
2652
+ enableDryRunIfNeeded(options);
2055
2653
  const spinner = ora2("Generating service...").start();
2056
2654
  try {
2057
2655
  const kebabName = toKebabCase(name);
2058
2656
  const pascalName = toPascalCase(name);
2059
2657
  const camelName = toCamelCase(name);
2060
2658
  const moduleName = options.module ? toKebabCase(options.module) : kebabName;
2061
- const moduleDir = path3.join(getModulesDir(), moduleName);
2062
- const filePath = path3.join(moduleDir, `${kebabName}.service.ts`);
2659
+ const moduleDir = path4.join(getModulesDir(), moduleName);
2660
+ const filePath = path4.join(moduleDir, `${kebabName}.service.ts`);
2063
2661
  if (await fileExists(filePath)) {
2064
2662
  spinner.stop();
2065
2663
  error(`Service "${kebabName}" already exists`);
@@ -2068,12 +2666,14 @@ generateCommand.command("service <name>").alias("s").description("Generate a ser
2068
2666
  await writeFile(filePath, serviceTemplate(kebabName, pascalName, camelName));
2069
2667
  spinner.succeed(`Service "${pascalName}Service" generated!`);
2070
2668
  success(` src/modules/${moduleName}/${kebabName}.service.ts`);
2669
+ showDryRunSummary(options);
2071
2670
  } catch (err) {
2072
2671
  spinner.fail("Failed to generate service");
2073
2672
  error(err instanceof Error ? err.message : String(err));
2074
2673
  }
2075
2674
  });
2076
- generateCommand.command("repository <name>").alias("r").description("Generate a repository").option("-m, --module <module>", "Target module name").action(async (name, options) => {
2675
+ 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) => {
2676
+ enableDryRunIfNeeded(options);
2077
2677
  const spinner = ora2("Generating repository...").start();
2078
2678
  try {
2079
2679
  const kebabName = toKebabCase(name);
@@ -2081,8 +2681,8 @@ generateCommand.command("repository <name>").alias("r").description("Generate a
2081
2681
  const camelName = toCamelCase(name);
2082
2682
  const pluralName = pluralize(kebabName);
2083
2683
  const moduleName = options.module ? toKebabCase(options.module) : kebabName;
2084
- const moduleDir = path3.join(getModulesDir(), moduleName);
2085
- const filePath = path3.join(moduleDir, `${kebabName}.repository.ts`);
2684
+ const moduleDir = path4.join(getModulesDir(), moduleName);
2685
+ const filePath = path4.join(moduleDir, `${kebabName}.repository.ts`);
2086
2686
  if (await fileExists(filePath)) {
2087
2687
  spinner.stop();
2088
2688
  error(`Repository "${kebabName}" already exists`);
@@ -2091,19 +2691,21 @@ generateCommand.command("repository <name>").alias("r").description("Generate a
2091
2691
  await writeFile(filePath, repositoryTemplate(kebabName, pascalName, camelName, pluralName));
2092
2692
  spinner.succeed(`Repository "${pascalName}Repository" generated!`);
2093
2693
  success(` src/modules/${moduleName}/${kebabName}.repository.ts`);
2694
+ showDryRunSummary(options);
2094
2695
  } catch (err) {
2095
2696
  spinner.fail("Failed to generate repository");
2096
2697
  error(err instanceof Error ? err.message : String(err));
2097
2698
  }
2098
2699
  });
2099
- generateCommand.command("types <name>").alias("t").description("Generate types/interfaces").option("-m, --module <module>", "Target module name").action(async (name, options) => {
2700
+ 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) => {
2701
+ enableDryRunIfNeeded(options);
2100
2702
  const spinner = ora2("Generating types...").start();
2101
2703
  try {
2102
2704
  const kebabName = toKebabCase(name);
2103
2705
  const pascalName = toPascalCase(name);
2104
2706
  const moduleName = options.module ? toKebabCase(options.module) : kebabName;
2105
- const moduleDir = path3.join(getModulesDir(), moduleName);
2106
- const filePath = path3.join(moduleDir, `${kebabName}.types.ts`);
2707
+ const moduleDir = path4.join(getModulesDir(), moduleName);
2708
+ const filePath = path4.join(moduleDir, `${kebabName}.types.ts`);
2107
2709
  if (await fileExists(filePath)) {
2108
2710
  spinner.stop();
2109
2711
  error(`Types file "${kebabName}.types.ts" already exists`);
@@ -2112,20 +2714,22 @@ generateCommand.command("types <name>").alias("t").description("Generate types/i
2112
2714
  await writeFile(filePath, typesTemplate(kebabName, pascalName));
2113
2715
  spinner.succeed(`Types for "${pascalName}" generated!`);
2114
2716
  success(` src/modules/${moduleName}/${kebabName}.types.ts`);
2717
+ showDryRunSummary(options);
2115
2718
  } catch (err) {
2116
2719
  spinner.fail("Failed to generate types");
2117
2720
  error(err instanceof Error ? err.message : String(err));
2118
2721
  }
2119
2722
  });
2120
- generateCommand.command("schema <name>").alias("v").description("Generate validation schemas").option("-m, --module <module>", "Target module name").action(async (name, options) => {
2723
+ 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) => {
2724
+ enableDryRunIfNeeded(options);
2121
2725
  const spinner = ora2("Generating schemas...").start();
2122
2726
  try {
2123
2727
  const kebabName = toKebabCase(name);
2124
2728
  const pascalName = toPascalCase(name);
2125
2729
  const camelName = toCamelCase(name);
2126
2730
  const moduleName = options.module ? toKebabCase(options.module) : kebabName;
2127
- const moduleDir = path3.join(getModulesDir(), moduleName);
2128
- const filePath = path3.join(moduleDir, `${kebabName}.schemas.ts`);
2731
+ const moduleDir = path4.join(getModulesDir(), moduleName);
2732
+ const filePath = path4.join(moduleDir, `${kebabName}.schemas.ts`);
2129
2733
  if (await fileExists(filePath)) {
2130
2734
  spinner.stop();
2131
2735
  error(`Schemas file "${kebabName}.schemas.ts" already exists`);
@@ -2134,12 +2738,14 @@ generateCommand.command("schema <name>").alias("v").description("Generate valida
2134
2738
  await writeFile(filePath, schemasTemplate(kebabName, pascalName, camelName));
2135
2739
  spinner.succeed(`Schemas for "${pascalName}" generated!`);
2136
2740
  success(` src/modules/${moduleName}/${kebabName}.schemas.ts`);
2741
+ showDryRunSummary(options);
2137
2742
  } catch (err) {
2138
2743
  spinner.fail("Failed to generate schemas");
2139
2744
  error(err instanceof Error ? err.message : String(err));
2140
2745
  }
2141
2746
  });
2142
- generateCommand.command("routes <name>").description("Generate routes").option("-m, --module <module>", "Target module name").action(async (name, options) => {
2747
+ 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) => {
2748
+ enableDryRunIfNeeded(options);
2143
2749
  const spinner = ora2("Generating routes...").start();
2144
2750
  try {
2145
2751
  const kebabName = toKebabCase(name);
@@ -2147,8 +2753,8 @@ generateCommand.command("routes <name>").description("Generate routes").option("
2147
2753
  const camelName = toCamelCase(name);
2148
2754
  const pluralName = pluralize(kebabName);
2149
2755
  const moduleName = options.module ? toKebabCase(options.module) : kebabName;
2150
- const moduleDir = path3.join(getModulesDir(), moduleName);
2151
- const filePath = path3.join(moduleDir, `${kebabName}.routes.ts`);
2756
+ const moduleDir = path4.join(getModulesDir(), moduleName);
2757
+ const filePath = path4.join(moduleDir, `${kebabName}.routes.ts`);
2152
2758
  if (await fileExists(filePath)) {
2153
2759
  spinner.stop();
2154
2760
  error(`Routes file "${kebabName}.routes.ts" already exists`);
@@ -2157,6 +2763,7 @@ generateCommand.command("routes <name>").description("Generate routes").option("
2157
2763
  await writeFile(filePath, routesTemplate(kebabName, pascalName, camelName, pluralName));
2158
2764
  spinner.succeed(`Routes for "${pascalName}" generated!`);
2159
2765
  success(` src/modules/${moduleName}/${kebabName}.routes.ts`);
2766
+ showDryRunSummary(options);
2160
2767
  } catch (err) {
2161
2768
  spinner.fail("Failed to generate routes");
2162
2769
  error(err instanceof Error ? err.message : String(err));
@@ -2235,21 +2842,21 @@ async function promptForFields() {
2235
2842
 
2236
2843
  // src/cli/commands/add-module.ts
2237
2844
  import { Command as Command3 } from "commander";
2238
- import path6 from "path";
2845
+ import path7 from "path";
2239
2846
  import ora3 from "ora";
2240
- import chalk4 from "chalk";
2847
+ import chalk7 from "chalk";
2241
2848
  import * as fs5 from "fs/promises";
2242
2849
 
2243
2850
  // src/cli/utils/env-manager.ts
2244
2851
  import * as fs3 from "fs/promises";
2245
- import * as path4 from "path";
2852
+ import * as path5 from "path";
2246
2853
  import { existsSync } from "fs";
2247
2854
  var EnvManager = class {
2248
2855
  envPath;
2249
2856
  envExamplePath;
2250
2857
  constructor(projectRoot) {
2251
- this.envPath = path4.join(projectRoot, ".env");
2252
- this.envExamplePath = path4.join(projectRoot, ".env.example");
2858
+ this.envPath = path5.join(projectRoot, ".env");
2859
+ this.envExamplePath = path5.join(projectRoot, ".env.example");
2253
2860
  }
2254
2861
  /**
2255
2862
  * Add environment variables to .env file
@@ -2900,15 +3507,15 @@ var EnvManager = class {
2900
3507
 
2901
3508
  // src/cli/utils/template-manager.ts
2902
3509
  import * as fs4 from "fs/promises";
2903
- import * as path5 from "path";
3510
+ import * as path6 from "path";
2904
3511
  import { createHash } from "crypto";
2905
3512
  import { existsSync as existsSync2 } from "fs";
2906
3513
  var TemplateManager = class {
2907
3514
  templatesDir;
2908
3515
  manifestsDir;
2909
3516
  constructor(projectRoot) {
2910
- this.templatesDir = path5.join(projectRoot, ".servcraft", "templates");
2911
- this.manifestsDir = path5.join(projectRoot, ".servcraft", "manifests");
3517
+ this.templatesDir = path6.join(projectRoot, ".servcraft", "templates");
3518
+ this.manifestsDir = path6.join(projectRoot, ".servcraft", "manifests");
2912
3519
  }
2913
3520
  /**
2914
3521
  * Initialize template system
@@ -2922,10 +3529,10 @@ var TemplateManager = class {
2922
3529
  */
2923
3530
  async saveTemplate(moduleName, files) {
2924
3531
  await this.initialize();
2925
- const moduleTemplateDir = path5.join(this.templatesDir, moduleName);
3532
+ const moduleTemplateDir = path6.join(this.templatesDir, moduleName);
2926
3533
  await fs4.mkdir(moduleTemplateDir, { recursive: true });
2927
3534
  for (const [fileName, content] of Object.entries(files)) {
2928
- const filePath = path5.join(moduleTemplateDir, fileName);
3535
+ const filePath = path6.join(moduleTemplateDir, fileName);
2929
3536
  await fs4.writeFile(filePath, content, "utf-8");
2930
3537
  }
2931
3538
  }
@@ -2934,7 +3541,7 @@ var TemplateManager = class {
2934
3541
  */
2935
3542
  async getTemplate(moduleName, fileName) {
2936
3543
  try {
2937
- const filePath = path5.join(this.templatesDir, moduleName, fileName);
3544
+ const filePath = path6.join(this.templatesDir, moduleName, fileName);
2938
3545
  return await fs4.readFile(filePath, "utf-8");
2939
3546
  } catch {
2940
3547
  return null;
@@ -2959,7 +3566,7 @@ var TemplateManager = class {
2959
3566
  installedAt: /* @__PURE__ */ new Date(),
2960
3567
  updatedAt: /* @__PURE__ */ new Date()
2961
3568
  };
2962
- const manifestPath = path5.join(this.manifestsDir, `${moduleName}.json`);
3569
+ const manifestPath = path6.join(this.manifestsDir, `${moduleName}.json`);
2963
3570
  await fs4.writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
2964
3571
  }
2965
3572
  /**
@@ -2967,7 +3574,7 @@ var TemplateManager = class {
2967
3574
  */
2968
3575
  async getManifest(moduleName) {
2969
3576
  try {
2970
- const manifestPath = path5.join(this.manifestsDir, `${moduleName}.json`);
3577
+ const manifestPath = path6.join(this.manifestsDir, `${moduleName}.json`);
2971
3578
  const content = await fs4.readFile(manifestPath, "utf-8");
2972
3579
  return JSON.parse(content);
2973
3580
  } catch {
@@ -2996,7 +3603,7 @@ var TemplateManager = class {
2996
3603
  }
2997
3604
  const results = [];
2998
3605
  for (const [fileName, fileInfo] of Object.entries(manifest.files)) {
2999
- const filePath = path5.join(moduleDir, fileName);
3606
+ const filePath = path6.join(moduleDir, fileName);
3000
3607
  if (!existsSync2(filePath)) {
3001
3608
  results.push({
3002
3609
  fileName,
@@ -3022,7 +3629,7 @@ var TemplateManager = class {
3022
3629
  */
3023
3630
  async createBackup(moduleName, moduleDir) {
3024
3631
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").substring(0, 19);
3025
- const backupDir = path5.join(path5.dirname(moduleDir), `${moduleName}.backup-${timestamp}`);
3632
+ const backupDir = path6.join(path6.dirname(moduleDir), `${moduleName}.backup-${timestamp}`);
3026
3633
  await this.copyDirectory(moduleDir, backupDir);
3027
3634
  return backupDir;
3028
3635
  }
@@ -3033,8 +3640,8 @@ var TemplateManager = class {
3033
3640
  await fs4.mkdir(dest, { recursive: true });
3034
3641
  const entries = await fs4.readdir(src, { withFileTypes: true });
3035
3642
  for (const entry of entries) {
3036
- const srcPath = path5.join(src, entry.name);
3037
- const destPath = path5.join(dest, entry.name);
3643
+ const srcPath = path6.join(src, entry.name);
3644
+ const destPath = path6.join(dest, entry.name);
3038
3645
  if (entry.isDirectory()) {
3039
3646
  await this.copyDirectory(srcPath, destPath);
3040
3647
  } else {
@@ -3135,23 +3742,23 @@ var TemplateManager = class {
3135
3742
  }
3136
3743
  manifest.files = fileHashes;
3137
3744
  manifest.updatedAt = /* @__PURE__ */ new Date();
3138
- const manifestPath = path5.join(this.manifestsDir, `${moduleName}.json`);
3745
+ const manifestPath = path6.join(this.manifestsDir, `${moduleName}.json`);
3139
3746
  await fs4.writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
3140
3747
  }
3141
3748
  };
3142
3749
 
3143
3750
  // src/cli/utils/interactive-prompt.ts
3144
3751
  import inquirer3 from "inquirer";
3145
- import chalk3 from "chalk";
3752
+ import chalk6 from "chalk";
3146
3753
  var InteractivePrompt = class {
3147
3754
  /**
3148
3755
  * Ask what to do when module already exists
3149
3756
  */
3150
3757
  static async askModuleExists(moduleName, hasModifications) {
3151
- console.log(chalk3.yellow(`
3758
+ console.log(chalk6.yellow(`
3152
3759
  \u26A0\uFE0F Module "${moduleName}" already exists`));
3153
3760
  if (hasModifications) {
3154
- console.log(chalk3.yellow(" Some files have been modified by you.\n"));
3761
+ console.log(chalk6.yellow(" Some files have been modified by you.\n"));
3155
3762
  }
3156
3763
  const { action } = await inquirer3.prompt([
3157
3764
  {
@@ -3189,12 +3796,12 @@ var InteractivePrompt = class {
3189
3796
  * Ask what to do with a specific file
3190
3797
  */
3191
3798
  static async askFileAction(fileName, isModified, yourLines, newLines) {
3192
- console.log(chalk3.cyan(`
3799
+ console.log(chalk6.cyan(`
3193
3800
  \u{1F4C1} ${fileName}`));
3194
3801
  console.log(
3195
- chalk3.gray(` Your version: ${yourLines} lines${isModified ? " (modified)" : ""}`)
3802
+ chalk6.gray(` Your version: ${yourLines} lines${isModified ? " (modified)" : ""}`)
3196
3803
  );
3197
- console.log(chalk3.gray(` New version: ${newLines} lines
3804
+ console.log(chalk6.gray(` New version: ${newLines} lines
3198
3805
  `));
3199
3806
  const { action } = await inquirer3.prompt([
3200
3807
  {
@@ -3246,7 +3853,7 @@ var InteractivePrompt = class {
3246
3853
  * Display diff and ask to continue
3247
3854
  */
3248
3855
  static async showDiffAndAsk(diff) {
3249
- console.log(chalk3.cyan("\n\u{1F4CA} Differences:\n"));
3856
+ console.log(chalk6.cyan("\n\u{1F4CA} Differences:\n"));
3250
3857
  console.log(diff);
3251
3858
  return await this.confirm("\nDo you want to proceed with this change?", true);
3252
3859
  }
@@ -3254,41 +3861,41 @@ var InteractivePrompt = class {
3254
3861
  * Display merge conflicts
3255
3862
  */
3256
3863
  static displayConflicts(conflicts) {
3257
- console.log(chalk3.red("\n\u26A0\uFE0F Merge Conflicts Detected:\n"));
3864
+ console.log(chalk6.red("\n\u26A0\uFE0F Merge Conflicts Detected:\n"));
3258
3865
  conflicts.forEach((conflict, i) => {
3259
- console.log(chalk3.yellow(` ${i + 1}. ${conflict}`));
3866
+ console.log(chalk6.yellow(` ${i + 1}. ${conflict}`));
3260
3867
  });
3261
- console.log(chalk3.gray("\n Conflict markers have been added to the file:"));
3262
- console.log(chalk3.gray(" <<<<<<< YOUR VERSION"));
3263
- console.log(chalk3.gray(" ... your code ..."));
3264
- console.log(chalk3.gray(" ======="));
3265
- console.log(chalk3.gray(" ... new code ..."));
3266
- console.log(chalk3.gray(" >>>>>>> NEW VERSION\n"));
3868
+ console.log(chalk6.gray("\n Conflict markers have been added to the file:"));
3869
+ console.log(chalk6.gray(" <<<<<<< YOUR VERSION"));
3870
+ console.log(chalk6.gray(" ... your code ..."));
3871
+ console.log(chalk6.gray(" ======="));
3872
+ console.log(chalk6.gray(" ... new code ..."));
3873
+ console.log(chalk6.gray(" >>>>>>> NEW VERSION\n"));
3267
3874
  }
3268
3875
  /**
3269
3876
  * Show backup location
3270
3877
  */
3271
3878
  static showBackupCreated(backupPath) {
3272
- console.log(chalk3.green(`
3273
- \u2713 Backup created: ${chalk3.cyan(backupPath)}`));
3879
+ console.log(chalk6.green(`
3880
+ \u2713 Backup created: ${chalk6.cyan(backupPath)}`));
3274
3881
  }
3275
3882
  /**
3276
3883
  * Show merge summary
3277
3884
  */
3278
3885
  static showMergeSummary(stats) {
3279
- console.log(chalk3.bold("\n\u{1F4CA} Merge Summary:\n"));
3886
+ console.log(chalk6.bold("\n\u{1F4CA} Merge Summary:\n"));
3280
3887
  if (stats.merged > 0) {
3281
- console.log(chalk3.green(` \u2713 Merged: ${stats.merged} file(s)`));
3888
+ console.log(chalk6.green(` \u2713 Merged: ${stats.merged} file(s)`));
3282
3889
  }
3283
3890
  if (stats.kept > 0) {
3284
- console.log(chalk3.blue(` \u2192 Kept: ${stats.kept} file(s)`));
3891
+ console.log(chalk6.blue(` \u2192 Kept: ${stats.kept} file(s)`));
3285
3892
  }
3286
3893
  if (stats.overwritten > 0) {
3287
- console.log(chalk3.yellow(` \u26A0 Overwritten: ${stats.overwritten} file(s)`));
3894
+ console.log(chalk6.yellow(` \u26A0 Overwritten: ${stats.overwritten} file(s)`));
3288
3895
  }
3289
3896
  if (stats.conflicts > 0) {
3290
- console.log(chalk3.red(` \u26A0 Conflicts: ${stats.conflicts} file(s)`));
3291
- console.log(chalk3.gray("\n Please resolve conflicts manually before committing.\n"));
3897
+ console.log(chalk6.red(` \u26A0 Conflicts: ${stats.conflicts} file(s)`));
3898
+ console.log(chalk6.gray("\n Please resolve conflicts manually before committing.\n"));
3292
3899
  }
3293
3900
  }
3294
3901
  /**
@@ -3460,31 +4067,40 @@ var AVAILABLE_MODULES = {
3460
4067
  var addModuleCommand = new Command3("add").description("Add a pre-built module to your project").argument(
3461
4068
  "[module]",
3462
4069
  "Module to add (auth, users, email, audit, upload, cache, notifications, settings)"
3463
- ).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(
4070
+ ).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(
3464
4071
  async (moduleName, options) => {
3465
4072
  if (options?.list || !moduleName) {
3466
- console.log(chalk4.bold("\n\u{1F4E6} Available Modules:\n"));
4073
+ console.log(chalk7.bold("\n\u{1F4E6} Available Modules:\n"));
3467
4074
  for (const [key, mod] of Object.entries(AVAILABLE_MODULES)) {
3468
- console.log(` ${chalk4.cyan(key.padEnd(15))} ${mod.name}`);
3469
- console.log(` ${" ".repeat(15)} ${chalk4.gray(mod.description)}
4075
+ console.log(` ${chalk7.cyan(key.padEnd(15))} ${mod.name}`);
4076
+ console.log(` ${" ".repeat(15)} ${chalk7.gray(mod.description)}
3470
4077
  `);
3471
4078
  }
3472
- console.log(chalk4.bold("Usage:"));
3473
- console.log(` ${chalk4.yellow("servcraft add auth")} Add authentication module`);
3474
- console.log(` ${chalk4.yellow("servcraft add users")} Add user management module`);
3475
- console.log(` ${chalk4.yellow("servcraft add email")} Add email service module
4079
+ console.log(chalk7.bold("Usage:"));
4080
+ console.log(` ${chalk7.yellow("servcraft add auth")} Add authentication module`);
4081
+ console.log(` ${chalk7.yellow("servcraft add users")} Add user management module`);
4082
+ console.log(` ${chalk7.yellow("servcraft add email")} Add email service module
3476
4083
  `);
3477
4084
  return;
3478
4085
  }
4086
+ const dryRun = DryRunManager.getInstance();
4087
+ if (options?.dryRun) {
4088
+ dryRun.enable();
4089
+ console.log(chalk7.yellow("\n\u26A0 DRY RUN MODE - No files will be written\n"));
4090
+ }
3479
4091
  const module = AVAILABLE_MODULES[moduleName];
3480
4092
  if (!module) {
3481
- error(`Unknown module: ${moduleName}`);
3482
- info('Run "servcraft add --list" to see available modules');
4093
+ displayError(ErrorTypes.MODULE_NOT_FOUND(moduleName));
4094
+ return;
4095
+ }
4096
+ const projectError = validateProject();
4097
+ if (projectError) {
4098
+ displayError(projectError);
3483
4099
  return;
3484
4100
  }
3485
4101
  const spinner = ora3(`Adding ${module.name} module...`).start();
3486
4102
  try {
3487
- const moduleDir = path6.join(getModulesDir(), moduleName);
4103
+ const moduleDir = path7.join(getModulesDir(), moduleName);
3488
4104
  const templateManager = new TemplateManager(process.cwd());
3489
4105
  const moduleExists = await fileExists(moduleDir);
3490
4106
  if (moduleExists) {
@@ -3552,16 +4168,16 @@ var addModuleCommand = new Command3("add").description("Add a pre-built module t
3552
4168
  info("\n\u{1F4DD} Created new .env file");
3553
4169
  }
3554
4170
  if (result.added.length > 0) {
3555
- console.log(chalk4.bold("\n\u2705 Added to .env:"));
4171
+ console.log(chalk7.bold("\n\u2705 Added to .env:"));
3556
4172
  result.added.forEach((key) => success(` ${key}`));
3557
4173
  }
3558
4174
  if (result.skipped.length > 0) {
3559
- console.log(chalk4.bold("\n\u23ED\uFE0F Already in .env (skipped):"));
4175
+ console.log(chalk7.bold("\n\u23ED\uFE0F Already in .env (skipped):"));
3560
4176
  result.skipped.forEach((key) => info(` ${key}`));
3561
4177
  }
3562
4178
  const requiredVars = envSections.flatMap((section) => section.variables).filter((v) => v.required && !v.value).map((v) => v.key);
3563
4179
  if (requiredVars.length > 0) {
3564
- console.log(chalk4.bold("\n\u26A0\uFE0F Required configuration:"));
4180
+ console.log(chalk7.bold("\n\u26A0\uFE0F Required configuration:"));
3565
4181
  requiredVars.forEach((key) => warn(` ${key} - Please configure this variable`));
3566
4182
  }
3567
4183
  } catch (err) {
@@ -3569,10 +4185,15 @@ var addModuleCommand = new Command3("add").description("Add a pre-built module t
3569
4185
  error(err instanceof Error ? err.message : String(err));
3570
4186
  }
3571
4187
  }
3572
- console.log("\n\u{1F4CC} Next steps:");
3573
- info(" 1. Configure environment variables in .env (if needed)");
3574
- info(" 2. Register the module in your main app file");
3575
- info(" 3. Run database migrations if needed");
4188
+ if (!options?.dryRun) {
4189
+ console.log("\n\u{1F4CC} Next steps:");
4190
+ info(" 1. Configure environment variables in .env (if needed)");
4191
+ info(" 2. Register the module in your main app file");
4192
+ info(" 3. Run database migrations if needed");
4193
+ }
4194
+ if (options?.dryRun) {
4195
+ dryRun.printSummary();
4196
+ }
3576
4197
  } catch (err) {
3577
4198
  spinner.fail("Failed to add module");
3578
4199
  error(err instanceof Error ? err.message : String(err));
@@ -3623,7 +4244,7 @@ export * from './auth.schemas.js';
3623
4244
  `
3624
4245
  };
3625
4246
  for (const [name, content] of Object.entries(files)) {
3626
- await writeFile(path6.join(dir, name), content);
4247
+ await writeFile(path7.join(dir, name), content);
3627
4248
  }
3628
4249
  }
3629
4250
  async function generateUsersModule(dir) {
@@ -3663,7 +4284,7 @@ export * from './user.schemas.js';
3663
4284
  `
3664
4285
  };
3665
4286
  for (const [name, content] of Object.entries(files)) {
3666
- await writeFile(path6.join(dir, name), content);
4287
+ await writeFile(path7.join(dir, name), content);
3667
4288
  }
3668
4289
  }
3669
4290
  async function generateEmailModule(dir) {
@@ -3720,7 +4341,7 @@ export { EmailService, emailService } from './email.service.js';
3720
4341
  `
3721
4342
  };
3722
4343
  for (const [name, content] of Object.entries(files)) {
3723
- await writeFile(path6.join(dir, name), content);
4344
+ await writeFile(path7.join(dir, name), content);
3724
4345
  }
3725
4346
  }
3726
4347
  async function generateAuditModule(dir) {
@@ -3764,7 +4385,7 @@ export { AuditService, auditService } from './audit.service.js';
3764
4385
  `
3765
4386
  };
3766
4387
  for (const [name, content] of Object.entries(files)) {
3767
- await writeFile(path6.join(dir, name), content);
4388
+ await writeFile(path7.join(dir, name), content);
3768
4389
  }
3769
4390
  }
3770
4391
  async function generateUploadModule(dir) {
@@ -3790,7 +4411,7 @@ export interface UploadOptions {
3790
4411
  `
3791
4412
  };
3792
4413
  for (const [name, content] of Object.entries(files)) {
3793
- await writeFile(path6.join(dir, name), content);
4414
+ await writeFile(path7.join(dir, name), content);
3794
4415
  }
3795
4416
  }
3796
4417
  async function generateCacheModule(dir) {
@@ -3836,7 +4457,7 @@ export { CacheService, cacheService } from './cache.service.js';
3836
4457
  `
3837
4458
  };
3838
4459
  for (const [name, content] of Object.entries(files)) {
3839
- await writeFile(path6.join(dir, name), content);
4460
+ await writeFile(path7.join(dir, name), content);
3840
4461
  }
3841
4462
  }
3842
4463
  async function generateGenericModule(dir, name) {
@@ -3850,18 +4471,18 @@ export interface ${name.charAt(0).toUpperCase() + name.slice(1)}Data {
3850
4471
  `
3851
4472
  };
3852
4473
  for (const [fileName, content] of Object.entries(files)) {
3853
- await writeFile(path6.join(dir, fileName), content);
4474
+ await writeFile(path7.join(dir, fileName), content);
3854
4475
  }
3855
4476
  }
3856
4477
  async function findServercraftModules() {
3857
- const scriptDir = path6.dirname(new URL(import.meta.url).pathname);
4478
+ const scriptDir = path7.dirname(new URL(import.meta.url).pathname);
3858
4479
  const possiblePaths = [
3859
4480
  // Local node_modules (when servcraft is a dependency)
3860
- path6.join(process.cwd(), "node_modules", "servcraft", "src", "modules"),
4481
+ path7.join(process.cwd(), "node_modules", "servcraft", "src", "modules"),
3861
4482
  // From dist/cli/index.js -> src/modules (npx or global install)
3862
- path6.resolve(scriptDir, "..", "..", "src", "modules"),
4483
+ path7.resolve(scriptDir, "..", "..", "src", "modules"),
3863
4484
  // From src/cli/commands/add-module.ts -> src/modules (development)
3864
- path6.resolve(scriptDir, "..", "..", "modules")
4485
+ path7.resolve(scriptDir, "..", "..", "modules")
3865
4486
  ];
3866
4487
  for (const p of possiblePaths) {
3867
4488
  try {
@@ -3885,7 +4506,7 @@ async function generateModuleFiles(moduleName, moduleDir) {
3885
4506
  const sourceDirName = moduleNameMap[moduleName] || moduleName;
3886
4507
  const servercraftModulesDir = await findServercraftModules();
3887
4508
  if (servercraftModulesDir) {
3888
- const sourceModuleDir = path6.join(servercraftModulesDir, sourceDirName);
4509
+ const sourceModuleDir = path7.join(servercraftModulesDir, sourceDirName);
3889
4510
  if (await fileExists(sourceModuleDir)) {
3890
4511
  await copyModuleFromSource(sourceModuleDir, moduleDir);
3891
4512
  return;
@@ -3917,8 +4538,8 @@ async function generateModuleFiles(moduleName, moduleDir) {
3917
4538
  async function copyModuleFromSource(sourceDir, targetDir) {
3918
4539
  const entries = await fs5.readdir(sourceDir, { withFileTypes: true });
3919
4540
  for (const entry of entries) {
3920
- const sourcePath = path6.join(sourceDir, entry.name);
3921
- const targetPath = path6.join(targetDir, entry.name);
4541
+ const sourcePath = path7.join(sourceDir, entry.name);
4542
+ const targetPath = path7.join(targetDir, entry.name);
3922
4543
  if (entry.isDirectory()) {
3923
4544
  await fs5.mkdir(targetPath, { recursive: true });
3924
4545
  await copyModuleFromSource(sourcePath, targetPath);
@@ -3931,7 +4552,7 @@ async function getModuleFiles(moduleName, moduleDir) {
3931
4552
  const files = {};
3932
4553
  const entries = await fs5.readdir(moduleDir);
3933
4554
  for (const entry of entries) {
3934
- const filePath = path6.join(moduleDir, entry);
4555
+ const filePath = path7.join(moduleDir, entry);
3935
4556
  const stat2 = await fs5.stat(filePath);
3936
4557
  if (stat2.isFile() && entry.endsWith(".ts")) {
3937
4558
  const content = await fs5.readFile(filePath, "utf-8");
@@ -3942,14 +4563,14 @@ async function getModuleFiles(moduleName, moduleDir) {
3942
4563
  }
3943
4564
  async function showDiffForModule(templateManager, moduleName, moduleDir) {
3944
4565
  const modifiedFiles = await templateManager.getModifiedFiles(moduleName, moduleDir);
3945
- console.log(chalk4.cyan(`
4566
+ console.log(chalk7.cyan(`
3946
4567
  \u{1F4CA} Changes in module "${moduleName}":
3947
4568
  `));
3948
4569
  for (const file of modifiedFiles) {
3949
4570
  if (file.isModified) {
3950
- console.log(chalk4.yellow(`
4571
+ console.log(chalk7.yellow(`
3951
4572
  \u{1F4C4} ${file.fileName}:`));
3952
- const currentPath = path6.join(moduleDir, file.fileName);
4573
+ const currentPath = path7.join(moduleDir, file.fileName);
3953
4574
  const currentContent = await fs5.readFile(currentPath, "utf-8");
3954
4575
  const originalContent = await templateManager.getTemplate(moduleName, file.fileName);
3955
4576
  if (originalContent) {
@@ -3962,11 +4583,11 @@ async function showDiffForModule(templateManager, moduleName, moduleDir) {
3962
4583
  async function performSmartMerge(templateManager, moduleName, moduleDir, _displayName) {
3963
4584
  const spinner = ora3("Analyzing files for merge...").start();
3964
4585
  const newFiles = {};
3965
- const templateDir = path6.join(templateManager["templatesDir"], moduleName);
4586
+ const templateDir = path7.join(templateManager["templatesDir"], moduleName);
3966
4587
  try {
3967
4588
  const entries = await fs5.readdir(templateDir);
3968
4589
  for (const entry of entries) {
3969
- const content = await fs5.readFile(path6.join(templateDir, entry), "utf-8");
4590
+ const content = await fs5.readFile(path7.join(templateDir, entry), "utf-8");
3970
4591
  newFiles[entry] = content;
3971
4592
  }
3972
4593
  } catch {
@@ -3984,7 +4605,7 @@ async function performSmartMerge(templateManager, moduleName, moduleDir, _displa
3984
4605
  };
3985
4606
  for (const fileInfo of modifiedFiles) {
3986
4607
  const fileName = fileInfo.fileName;
3987
- const filePath = path6.join(moduleDir, fileName);
4608
+ const filePath = path7.join(moduleDir, fileName);
3988
4609
  const newContent = newFiles[fileName];
3989
4610
  if (!newContent) {
3990
4611
  continue;
@@ -4056,7 +4677,7 @@ async function performSmartMerge(templateManager, moduleName, moduleDir, _displa
4056
4677
  import { Command as Command4 } from "commander";
4057
4678
  import { execSync as execSync2, spawn } from "child_process";
4058
4679
  import ora4 from "ora";
4059
- import chalk5 from "chalk";
4680
+ import chalk8 from "chalk";
4060
4681
  var dbCommand = new Command4("db").description("Database management commands");
4061
4682
  dbCommand.command("migrate").description("Run database migrations").option("-n, --name <name>", "Migration name").action(async (options) => {
4062
4683
  const spinner = ora4("Running migrations...").start();
@@ -4113,7 +4734,7 @@ dbCommand.command("seed").description("Run database seed").action(async () => {
4113
4734
  });
4114
4735
  dbCommand.command("reset").description("Reset database (drop all data and re-run migrations)").option("-f, --force", "Skip confirmation").action(async (options) => {
4115
4736
  if (!options.force) {
4116
- console.log(chalk5.yellow("\n\u26A0\uFE0F WARNING: This will delete all data in your database!\n"));
4737
+ console.log(chalk8.yellow("\n\u26A0\uFE0F WARNING: This will delete all data in your database!\n"));
4117
4738
  const readline = await import("readline");
4118
4739
  const rl = readline.createInterface({
4119
4740
  input: process.stdin,
@@ -4147,14 +4768,14 @@ dbCommand.command("status").description("Show migration status").action(async ()
4147
4768
 
4148
4769
  // src/cli/commands/docs.ts
4149
4770
  import { Command as Command5 } from "commander";
4150
- import path8 from "path";
4771
+ import path9 from "path";
4151
4772
  import fs7 from "fs/promises";
4152
4773
  import ora6 from "ora";
4153
- import chalk6 from "chalk";
4774
+ import chalk9 from "chalk";
4154
4775
 
4155
4776
  // src/cli/utils/docs-generator.ts
4156
4777
  import fs6 from "fs/promises";
4157
- import path7 from "path";
4778
+ import path8 from "path";
4158
4779
  import ora5 from "ora";
4159
4780
 
4160
4781
  // src/core/server.ts
@@ -4875,11 +5496,11 @@ function validateQuery(schema, data) {
4875
5496
  function formatZodErrors(error2) {
4876
5497
  const errors = {};
4877
5498
  for (const issue of error2.issues) {
4878
- const path9 = issue.path.join(".") || "root";
4879
- if (!errors[path9]) {
4880
- errors[path9] = [];
5499
+ const path12 = issue.path.join(".") || "root";
5500
+ if (!errors[path12]) {
5501
+ errors[path12] = [];
4881
5502
  }
4882
- errors[path9].push(issue.message);
5503
+ errors[path12].push(issue.message);
4883
5504
  }
4884
5505
  return errors;
4885
5506
  }
@@ -5712,8 +6333,8 @@ async function generateDocs(outputPath = "openapi.json", silent = false) {
5712
6333
  await registerUserModule(app, authService);
5713
6334
  await app.ready();
5714
6335
  const spec = app.swagger();
5715
- const absoluteOutput = path7.resolve(outputPath);
5716
- await fs6.mkdir(path7.dirname(absoluteOutput), { recursive: true });
6336
+ const absoluteOutput = path8.resolve(outputPath);
6337
+ await fs6.mkdir(path8.dirname(absoluteOutput), { recursive: true });
5717
6338
  await fs6.writeFile(absoluteOutput, JSON.stringify(spec, null, 2), "utf8");
5718
6339
  spinner?.succeed(`OpenAPI spec generated at ${absoluteOutput}`);
5719
6340
  await app.close();
@@ -5758,7 +6379,7 @@ docsCommand.command("export").description("Export documentation to Postman, Inso
5758
6379
  const spinner = ora6("Exporting documentation...").start();
5759
6380
  try {
5760
6381
  const projectRoot = getProjectRoot();
5761
- const specPath = path8.join(projectRoot, "openapi.json");
6382
+ const specPath = path9.join(projectRoot, "openapi.json");
5762
6383
  try {
5763
6384
  await fs7.access(specPath);
5764
6385
  } catch {
@@ -5785,7 +6406,7 @@ docsCommand.command("export").description("Export documentation to Postman, Inso
5785
6406
  default:
5786
6407
  throw new Error(`Unknown format: ${options.format}`);
5787
6408
  }
5788
- const outPath = path8.join(projectRoot, options.output || defaultName);
6409
+ const outPath = path9.join(projectRoot, options.output || defaultName);
5789
6410
  await fs7.writeFile(outPath, output);
5790
6411
  spinner.succeed(`Exported to: ${options.output || defaultName}`);
5791
6412
  if (options.format === "postman") {
@@ -5798,8 +6419,8 @@ docsCommand.command("export").description("Export documentation to Postman, Inso
5798
6419
  });
5799
6420
  docsCommand.command("status").description("Show documentation status").action(async () => {
5800
6421
  const projectRoot = getProjectRoot();
5801
- console.log(chalk6.bold("\n\u{1F4CA} Documentation Status\n"));
5802
- const specPath = path8.join(projectRoot, "openapi.json");
6422
+ console.log(chalk9.bold("\n\u{1F4CA} Documentation Status\n"));
6423
+ const specPath = path9.join(projectRoot, "openapi.json");
5803
6424
  try {
5804
6425
  const stat2 = await fs7.stat(specPath);
5805
6426
  success(
@@ -5914,13 +6535,736 @@ function formatDate(date) {
5914
6535
  });
5915
6536
  }
5916
6537
 
6538
+ // src/cli/commands/list.ts
6539
+ import { Command as Command6 } from "commander";
6540
+ import chalk10 from "chalk";
6541
+ import fs8 from "fs/promises";
6542
+ var AVAILABLE_MODULES2 = {
6543
+ // Core
6544
+ auth: {
6545
+ name: "Authentication",
6546
+ description: "JWT authentication with access/refresh tokens",
6547
+ category: "Core"
6548
+ },
6549
+ users: {
6550
+ name: "User Management",
6551
+ description: "User CRUD with RBAC (roles & permissions)",
6552
+ category: "Core"
6553
+ },
6554
+ email: {
6555
+ name: "Email Service",
6556
+ description: "SMTP email with templates (Handlebars)",
6557
+ category: "Core"
6558
+ },
6559
+ // Security
6560
+ mfa: {
6561
+ name: "MFA/TOTP",
6562
+ description: "Two-factor authentication with QR codes",
6563
+ category: "Security"
6564
+ },
6565
+ oauth: {
6566
+ name: "OAuth",
6567
+ description: "Social login (Google, GitHub, Facebook, Twitter, Apple)",
6568
+ category: "Security"
6569
+ },
6570
+ "rate-limit": {
6571
+ name: "Rate Limiting",
6572
+ description: "Advanced rate limiting with multiple algorithms",
6573
+ category: "Security"
6574
+ },
6575
+ // Data & Storage
6576
+ cache: {
6577
+ name: "Redis Cache",
6578
+ description: "Redis caching with TTL & invalidation",
6579
+ category: "Data & Storage"
6580
+ },
6581
+ upload: {
6582
+ name: "File Upload",
6583
+ description: "File upload with local/S3/Cloudinary storage",
6584
+ category: "Data & Storage"
6585
+ },
6586
+ search: {
6587
+ name: "Search",
6588
+ description: "Full-text search with Elasticsearch/Meilisearch",
6589
+ category: "Data & Storage"
6590
+ },
6591
+ // Communication
6592
+ notification: {
6593
+ name: "Notifications",
6594
+ description: "Email, SMS, Push notifications",
6595
+ category: "Communication"
6596
+ },
6597
+ webhook: {
6598
+ name: "Webhooks",
6599
+ description: "Outgoing webhooks with HMAC signatures & retry",
6600
+ category: "Communication"
6601
+ },
6602
+ websocket: {
6603
+ name: "WebSockets",
6604
+ description: "Real-time communication with Socket.io",
6605
+ category: "Communication"
6606
+ },
6607
+ // Background Processing
6608
+ queue: {
6609
+ name: "Queue/Jobs",
6610
+ description: "Background jobs with Bull/BullMQ & cron scheduling",
6611
+ category: "Background Processing"
6612
+ },
6613
+ "media-processing": {
6614
+ name: "Media Processing",
6615
+ description: "Image/video processing with FFmpeg",
6616
+ category: "Background Processing"
6617
+ },
6618
+ // Monitoring & Analytics
6619
+ audit: {
6620
+ name: "Audit Logs",
6621
+ description: "Activity logging and audit trail",
6622
+ category: "Monitoring & Analytics"
6623
+ },
6624
+ analytics: {
6625
+ name: "Analytics/Metrics",
6626
+ description: "Prometheus metrics & event tracking",
6627
+ category: "Monitoring & Analytics"
6628
+ },
6629
+ // Internationalization
6630
+ i18n: {
6631
+ name: "i18n/Localization",
6632
+ description: "Multi-language support with 7+ locales",
6633
+ category: "Internationalization"
6634
+ },
6635
+ // API Management
6636
+ "feature-flag": {
6637
+ name: "Feature Flags",
6638
+ description: "A/B testing & progressive rollout",
6639
+ category: "API Management"
6640
+ },
6641
+ "api-versioning": {
6642
+ name: "API Versioning",
6643
+ description: "Multiple API versions support",
6644
+ category: "API Management"
6645
+ },
6646
+ // Payments
6647
+ payment: {
6648
+ name: "Payments",
6649
+ description: "Payment processing (Stripe, PayPal, Mobile Money)",
6650
+ category: "Payments"
6651
+ }
6652
+ };
6653
+ async function getInstalledModules() {
6654
+ try {
6655
+ const modulesDir = getModulesDir();
6656
+ const entries = await fs8.readdir(modulesDir, { withFileTypes: true });
6657
+ return entries.filter((e) => e.isDirectory()).map((e) => e.name);
6658
+ } catch {
6659
+ return [];
6660
+ }
6661
+ }
6662
+ function isServercraftProject() {
6663
+ try {
6664
+ getProjectRoot();
6665
+ return true;
6666
+ } catch {
6667
+ return false;
6668
+ }
6669
+ }
6670
+ var listCommand = new Command6("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(
6671
+ async (options) => {
6672
+ const installedModules = await getInstalledModules();
6673
+ const isProject = isServercraftProject();
6674
+ if (options.json) {
6675
+ const output = {
6676
+ available: Object.entries(AVAILABLE_MODULES2).map(([key, mod]) => ({
6677
+ id: key,
6678
+ ...mod,
6679
+ installed: installedModules.includes(key)
6680
+ }))
6681
+ };
6682
+ if (isProject) {
6683
+ output.installed = installedModules;
6684
+ }
6685
+ console.log(JSON.stringify(output, null, 2));
6686
+ return;
6687
+ }
6688
+ const byCategory = {};
6689
+ for (const [key, mod] of Object.entries(AVAILABLE_MODULES2)) {
6690
+ if (options.category && mod.category.toLowerCase() !== options.category.toLowerCase()) {
6691
+ continue;
6692
+ }
6693
+ if (!byCategory[mod.category]) {
6694
+ byCategory[mod.category] = [];
6695
+ }
6696
+ byCategory[mod.category]?.push({
6697
+ id: key,
6698
+ name: mod.name,
6699
+ description: mod.description,
6700
+ installed: installedModules.includes(key)
6701
+ });
6702
+ }
6703
+ if (options.installed) {
6704
+ if (!isProject) {
6705
+ console.log(chalk10.yellow("\n\u26A0 Not in a Servcraft project directory\n"));
6706
+ return;
6707
+ }
6708
+ console.log(chalk10.bold("\n\u{1F4E6} Installed Modules:\n"));
6709
+ if (installedModules.length === 0) {
6710
+ console.log(chalk10.gray(" No modules installed yet.\n"));
6711
+ console.log(` Run ${chalk10.cyan("servcraft add <module>")} to add a module.
6712
+ `);
6713
+ return;
6714
+ }
6715
+ for (const modId of installedModules) {
6716
+ const mod = AVAILABLE_MODULES2[modId];
6717
+ if (mod) {
6718
+ console.log(` ${chalk10.green("\u2713")} ${chalk10.cyan(modId.padEnd(18))} ${mod.name}`);
6719
+ } else {
6720
+ console.log(
6721
+ ` ${chalk10.green("\u2713")} ${chalk10.cyan(modId.padEnd(18))} ${chalk10.gray("(custom module)")}`
6722
+ );
6723
+ }
6724
+ }
6725
+ console.log(`
6726
+ Total: ${chalk10.bold(installedModules.length)} module(s) installed
6727
+ `);
6728
+ return;
6729
+ }
6730
+ console.log(chalk10.bold("\n\u{1F4E6} Available Modules\n"));
6731
+ if (isProject) {
6732
+ console.log(
6733
+ chalk10.gray(` ${chalk10.green("\u2713")} = installed ${chalk10.dim("\u25CB")} = not installed
6734
+ `)
6735
+ );
6736
+ }
6737
+ for (const [category, modules] of Object.entries(byCategory)) {
6738
+ console.log(chalk10.bold.blue(` ${category}`));
6739
+ console.log(chalk10.gray(" " + "\u2500".repeat(40)));
6740
+ for (const mod of modules) {
6741
+ const status = isProject ? mod.installed ? chalk10.green("\u2713") : chalk10.dim("\u25CB") : " ";
6742
+ const nameColor = mod.installed ? chalk10.green : chalk10.cyan;
6743
+ console.log(` ${status} ${nameColor(mod.id.padEnd(18))} ${mod.name}`);
6744
+ console.log(` ${chalk10.gray(mod.description)}`);
6745
+ }
6746
+ console.log();
6747
+ }
6748
+ const totalAvailable = Object.keys(AVAILABLE_MODULES2).length;
6749
+ const totalInstalled = installedModules.filter((m) => AVAILABLE_MODULES2[m]).length;
6750
+ console.log(chalk10.gray("\u2500".repeat(50)));
6751
+ console.log(
6752
+ ` ${chalk10.bold(totalAvailable)} modules available` + (isProject ? ` | ${chalk10.green.bold(totalInstalled)} installed` : "")
6753
+ );
6754
+ console.log();
6755
+ console.log(chalk10.bold(" Usage:"));
6756
+ console.log(` ${chalk10.yellow("servcraft add <module>")} Add a module`);
6757
+ console.log(` ${chalk10.yellow("servcraft list --installed")} Show installed only`);
6758
+ console.log(` ${chalk10.yellow("servcraft list --category Security")} Filter by category`);
6759
+ console.log();
6760
+ }
6761
+ );
6762
+
6763
+ // src/cli/commands/remove.ts
6764
+ import { Command as Command7 } from "commander";
6765
+ import path10 from "path";
6766
+ import ora7 from "ora";
6767
+ import chalk11 from "chalk";
6768
+ import fs9 from "fs/promises";
6769
+ import inquirer4 from "inquirer";
6770
+ var removeCommand = new Command7("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) => {
6771
+ const projectError = validateProject();
6772
+ if (projectError) {
6773
+ displayError(projectError);
6774
+ return;
6775
+ }
6776
+ console.log(chalk11.bold.cyan("\n\u{1F5D1}\uFE0F ServCraft Module Removal\n"));
6777
+ const moduleDir = path10.join(getModulesDir(), moduleName);
6778
+ try {
6779
+ const exists = await fs9.access(moduleDir).then(() => true).catch(() => false);
6780
+ if (!exists) {
6781
+ displayError(
6782
+ new ServCraftError(`Module "${moduleName}" is not installed`, [
6783
+ `Run ${chalk11.cyan("servcraft list --installed")} to see installed modules`,
6784
+ `Check the spelling of the module name`
6785
+ ])
6786
+ );
6787
+ return;
6788
+ }
6789
+ const files = await fs9.readdir(moduleDir);
6790
+ const fileCount = files.length;
6791
+ if (!options?.yes) {
6792
+ console.log(chalk11.yellow(`\u26A0 This will remove the "${moduleName}" module:`));
6793
+ console.log(chalk11.gray(` Directory: ${moduleDir}`));
6794
+ console.log(chalk11.gray(` Files: ${fileCount} file(s)`));
6795
+ console.log();
6796
+ const { confirm } = await inquirer4.prompt([
6797
+ {
6798
+ type: "confirm",
6799
+ name: "confirm",
6800
+ message: "Are you sure you want to remove this module?",
6801
+ default: false
6802
+ }
6803
+ ]);
6804
+ if (!confirm) {
6805
+ console.log(chalk11.yellow("\n\u2716 Removal cancelled\n"));
6806
+ return;
6807
+ }
6808
+ }
6809
+ const spinner = ora7("Removing module...").start();
6810
+ await fs9.rm(moduleDir, { recursive: true, force: true });
6811
+ spinner.succeed(`Module "${moduleName}" removed successfully!`);
6812
+ console.log("\n" + chalk11.bold("\u2713 Removed:"));
6813
+ success(` src/modules/${moduleName}/ (${fileCount} files)`);
6814
+ if (!options?.keepEnv) {
6815
+ console.log("\n" + chalk11.bold("\u{1F4CC} Manual cleanup needed:"));
6816
+ info(" 1. Remove environment variables related to this module from .env");
6817
+ info(" 2. Remove module imports from your main app file");
6818
+ info(" 3. Remove related database migrations if any");
6819
+ info(" 4. Update your routes if they reference this module");
6820
+ } else {
6821
+ console.log("\n" + chalk11.bold("\u{1F4CC} Manual cleanup needed:"));
6822
+ info(" 1. Environment variables were kept (--keep-env flag)");
6823
+ info(" 2. Remove module imports from your main app file");
6824
+ info(" 3. Update your routes if they reference this module");
6825
+ }
6826
+ console.log();
6827
+ } catch (err) {
6828
+ error(err instanceof Error ? err.message : String(err));
6829
+ console.log();
6830
+ }
6831
+ });
6832
+
6833
+ // src/cli/commands/doctor.ts
6834
+ import { Command as Command8 } from "commander";
6835
+ import chalk12 from "chalk";
6836
+ import fs10 from "fs/promises";
6837
+ async function checkNodeVersion() {
6838
+ const version = process.version;
6839
+ const major = parseInt(version.slice(1).split(".")[0] || "0", 10);
6840
+ if (major >= 18) {
6841
+ return { name: "Node.js", status: "pass", message: `${version} \u2713` };
6842
+ }
6843
+ return {
6844
+ name: "Node.js",
6845
+ status: "fail",
6846
+ message: `${version} (< 18)`,
6847
+ suggestion: "Upgrade to Node.js 18+"
6848
+ };
6849
+ }
6850
+ async function checkPackageJson() {
6851
+ const checks = [];
6852
+ try {
6853
+ const content = await fs10.readFile("package.json", "utf-8");
6854
+ const pkg = JSON.parse(content);
6855
+ checks.push({ name: "package.json", status: "pass", message: "Found" });
6856
+ if (pkg.dependencies?.fastify) {
6857
+ checks.push({ name: "Fastify", status: "pass", message: "Installed" });
6858
+ } else {
6859
+ checks.push({
6860
+ name: "Fastify",
6861
+ status: "fail",
6862
+ message: "Missing",
6863
+ suggestion: "npm install fastify"
6864
+ });
6865
+ }
6866
+ } catch {
6867
+ checks.push({
6868
+ name: "package.json",
6869
+ status: "fail",
6870
+ message: "Not found",
6871
+ suggestion: "Run servcraft init"
6872
+ });
6873
+ }
6874
+ return checks;
6875
+ }
6876
+ async function checkDirectories() {
6877
+ const checks = [];
6878
+ const dirs = ["src", "node_modules", ".git", ".env"];
6879
+ for (const dir of dirs) {
6880
+ try {
6881
+ await fs10.access(dir);
6882
+ checks.push({ name: dir, status: "pass", message: "Exists" });
6883
+ } catch {
6884
+ const isCritical = dir === "src" || dir === "node_modules";
6885
+ checks.push({
6886
+ name: dir,
6887
+ status: isCritical ? "fail" : "warn",
6888
+ message: "Not found",
6889
+ suggestion: dir === "node_modules" ? "npm install" : dir === ".env" ? "Create .env file" : void 0
6890
+ });
6891
+ }
6892
+ }
6893
+ return checks;
6894
+ }
6895
+ var doctorCommand = new Command8("doctor").description("Diagnose project configuration and dependencies").action(async () => {
6896
+ console.log(chalk12.bold.cyan("\n\u{1F50D} ServCraft Doctor\n"));
6897
+ const allChecks = [];
6898
+ allChecks.push(await checkNodeVersion());
6899
+ allChecks.push(...await checkPackageJson());
6900
+ allChecks.push(...await checkDirectories());
6901
+ allChecks.forEach((check) => {
6902
+ const icon = check.status === "pass" ? chalk12.green("\u2713") : check.status === "warn" ? chalk12.yellow("\u26A0") : chalk12.red("\u2717");
6903
+ const color = check.status === "pass" ? chalk12.green : check.status === "warn" ? chalk12.yellow : chalk12.red;
6904
+ console.log(`${icon} ${check.name.padEnd(20)} ${color(check.message)}`);
6905
+ if (check.suggestion) {
6906
+ console.log(chalk12.gray(` \u2192 ${check.suggestion}`));
6907
+ }
6908
+ });
6909
+ const pass = allChecks.filter((c) => c.status === "pass").length;
6910
+ const warn2 = allChecks.filter((c) => c.status === "warn").length;
6911
+ const fail = allChecks.filter((c) => c.status === "fail").length;
6912
+ console.log(chalk12.gray("\n" + "\u2500".repeat(60)));
6913
+ console.log(
6914
+ `
6915
+ ${chalk12.green(pass + " passed")} | ${chalk12.yellow(warn2 + " warnings")} | ${chalk12.red(fail + " failed")}
6916
+ `
6917
+ );
6918
+ if (fail === 0 && warn2 === 0) {
6919
+ console.log(chalk12.green.bold("\u2728 Everything looks good!\n"));
6920
+ } else if (fail > 0) {
6921
+ console.log(chalk12.red.bold("\u2717 Fix critical issues before using ServCraft.\n"));
6922
+ } else {
6923
+ console.log(chalk12.yellow.bold("\u26A0 Some warnings, but should work.\n"));
6924
+ }
6925
+ });
6926
+
6927
+ // src/cli/commands/update.ts
6928
+ import { Command as Command9 } from "commander";
6929
+ import chalk13 from "chalk";
6930
+ import fs11 from "fs/promises";
6931
+ import path11 from "path";
6932
+ import { fileURLToPath } from "url";
6933
+ import inquirer5 from "inquirer";
6934
+ var __filename2 = fileURLToPath(import.meta.url);
6935
+ var __dirname2 = path11.dirname(__filename2);
6936
+ var AVAILABLE_MODULES3 = [
6937
+ "auth",
6938
+ "users",
6939
+ "email",
6940
+ "mfa",
6941
+ "oauth",
6942
+ "rate-limit",
6943
+ "cache",
6944
+ "upload",
6945
+ "search",
6946
+ "notification",
6947
+ "webhook",
6948
+ "websocket",
6949
+ "queue",
6950
+ "payment",
6951
+ "i18n",
6952
+ "feature-flag",
6953
+ "analytics",
6954
+ "media-processing",
6955
+ "api-versioning",
6956
+ "audit",
6957
+ "swagger",
6958
+ "validation"
6959
+ ];
6960
+ async function getInstalledModules2() {
6961
+ try {
6962
+ const modulesDir = getModulesDir();
6963
+ const entries = await fs11.readdir(modulesDir, { withFileTypes: true });
6964
+ const installedModules = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).filter((name) => AVAILABLE_MODULES3.includes(name));
6965
+ return installedModules;
6966
+ } catch {
6967
+ return [];
6968
+ }
6969
+ }
6970
+ async function copyModuleFiles(moduleName, _projectRoot) {
6971
+ const cliRoot = path11.resolve(__dirname2, "../../../");
6972
+ const sourceModulePath = path11.join(cliRoot, "src", "modules", moduleName);
6973
+ const targetModulesDir = getModulesDir();
6974
+ const targetModulePath = path11.join(targetModulesDir, moduleName);
6975
+ try {
6976
+ await fs11.access(sourceModulePath);
6977
+ } catch {
6978
+ throw new Error(`Module source not found: ${moduleName}`);
6979
+ }
6980
+ await fs11.cp(sourceModulePath, targetModulePath, { recursive: true });
6981
+ }
6982
+ async function updateModule(moduleName, options) {
6983
+ const projectError = validateProject();
6984
+ if (projectError) {
6985
+ displayError(projectError);
6986
+ return;
6987
+ }
6988
+ const projectRoot = getProjectRoot();
6989
+ const installedModules = await getInstalledModules2();
6990
+ if (!installedModules.includes(moduleName)) {
6991
+ console.log(chalk13.yellow(`
6992
+ \u26A0 Module "${moduleName}" is not installed
6993
+ `));
6994
+ console.log(
6995
+ chalk13.gray(`Run ${chalk13.cyan(`servcraft add ${moduleName}`)} to install it first.
6996
+ `)
6997
+ );
6998
+ return;
6999
+ }
7000
+ if (options.check) {
7001
+ console.log(chalk13.cyan(`
7002
+ \u{1F4E6} Checking updates for "${moduleName}"...
7003
+ `));
7004
+ console.log(chalk13.gray("Note: Version tracking will be implemented in a future release."));
7005
+ console.log(chalk13.gray("Currently, update will always reinstall the latest version.\n"));
7006
+ return;
7007
+ }
7008
+ const { confirmed } = await inquirer5.prompt([
7009
+ {
7010
+ type: "confirm",
7011
+ name: "confirmed",
7012
+ message: `Update "${moduleName}" module? This will overwrite existing files.`,
7013
+ default: false
7014
+ }
7015
+ ]);
7016
+ if (!confirmed) {
7017
+ console.log(chalk13.yellow("\n\u26A0 Update cancelled\n"));
7018
+ return;
7019
+ }
7020
+ console.log(chalk13.cyan(`
7021
+ \u{1F504} Updating "${moduleName}" module...
7022
+ `));
7023
+ try {
7024
+ await copyModuleFiles(moduleName, projectRoot);
7025
+ console.log(chalk13.green(`\u2714 Module "${moduleName}" updated successfully!
7026
+ `));
7027
+ console.log(
7028
+ chalk13.gray("Note: Remember to review any breaking changes in the documentation.\n")
7029
+ );
7030
+ } catch (error2) {
7031
+ if (error2 instanceof Error) {
7032
+ console.error(chalk13.red(`
7033
+ \u2717 Failed to update module: ${error2.message}
7034
+ `));
7035
+ }
7036
+ }
7037
+ }
7038
+ async function updateAllModules(options) {
7039
+ const projectError = validateProject();
7040
+ if (projectError) {
7041
+ displayError(projectError);
7042
+ return;
7043
+ }
7044
+ const installedModules = await getInstalledModules2();
7045
+ if (installedModules.length === 0) {
7046
+ console.log(chalk13.yellow("\n\u26A0 No modules installed\n"));
7047
+ console.log(chalk13.gray(`Run ${chalk13.cyan("servcraft list")} to see available modules.
7048
+ `));
7049
+ return;
7050
+ }
7051
+ if (options.check) {
7052
+ console.log(chalk13.cyan("\n\u{1F4E6} Checking updates for all modules...\n"));
7053
+ console.log(chalk13.bold("Installed modules:"));
7054
+ installedModules.forEach((mod) => {
7055
+ console.log(` \u2022 ${chalk13.cyan(mod)}`);
7056
+ });
7057
+ console.log();
7058
+ console.log(chalk13.gray("Note: Version tracking will be implemented in a future release."));
7059
+ console.log(chalk13.gray("Currently, update will always reinstall the latest version.\n"));
7060
+ return;
7061
+ }
7062
+ console.log(chalk13.cyan(`
7063
+ \u{1F4E6} Found ${installedModules.length} installed module(s):
7064
+ `));
7065
+ installedModules.forEach((mod) => {
7066
+ console.log(` \u2022 ${chalk13.cyan(mod)}`);
7067
+ });
7068
+ console.log();
7069
+ const { confirmed } = await inquirer5.prompt([
7070
+ {
7071
+ type: "confirm",
7072
+ name: "confirmed",
7073
+ message: "Update all modules? This will overwrite existing files.",
7074
+ default: false
7075
+ }
7076
+ ]);
7077
+ if (!confirmed) {
7078
+ console.log(chalk13.yellow("\n\u26A0 Update cancelled\n"));
7079
+ return;
7080
+ }
7081
+ console.log(chalk13.cyan("\n\u{1F504} Updating all modules...\n"));
7082
+ const projectRoot = getProjectRoot();
7083
+ let successCount = 0;
7084
+ let failCount = 0;
7085
+ for (const moduleName of installedModules) {
7086
+ try {
7087
+ await copyModuleFiles(moduleName, projectRoot);
7088
+ console.log(chalk13.green(`\u2714 Updated: ${moduleName}`));
7089
+ successCount++;
7090
+ } catch {
7091
+ console.error(chalk13.red(`\u2717 Failed: ${moduleName}`));
7092
+ failCount++;
7093
+ }
7094
+ }
7095
+ console.log();
7096
+ console.log(
7097
+ chalk13.bold(
7098
+ `
7099
+ \u2714 Update complete: ${chalk13.green(successCount)} succeeded, ${chalk13.red(failCount)} failed
7100
+ `
7101
+ )
7102
+ );
7103
+ if (successCount > 0) {
7104
+ console.log(
7105
+ chalk13.gray("Note: Remember to review any breaking changes in the documentation.\n")
7106
+ );
7107
+ }
7108
+ }
7109
+ var updateCommand = new Command9("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) => {
7110
+ if (moduleName) {
7111
+ await updateModule(moduleName, { check: options?.check });
7112
+ } else {
7113
+ await updateAllModules({ check: options?.check });
7114
+ }
7115
+ });
7116
+
7117
+ // src/cli/commands/completion.ts
7118
+ import { Command as Command10 } from "commander";
7119
+ var bashScript = `
7120
+ # servcraft bash completion script
7121
+ _servcraft_completions() {
7122
+ local cur prev words cword
7123
+ _init_completion || return
7124
+
7125
+ # Main commands
7126
+ local commands="init add generate list remove doctor update completion docs --version --help"
7127
+
7128
+ # Generate subcommands
7129
+ local generate_subcommands="module controller service repository types schema routes m c s r t"
7130
+
7131
+ case "\${words[1]}" in
7132
+ generate|g)
7133
+ if [[ \${cword} -eq 2 ]]; then
7134
+ COMPREPLY=( $(compgen -W "\${generate_subcommands}" -- "\${cur}") )
7135
+ fi
7136
+ ;;
7137
+ add|remove|rm|update)
7138
+ if [[ \${cword} -eq 2 ]]; then
7139
+ # Get available modules
7140
+ local modules="auth cache rate-limit notification payment oauth mfa queue websocket upload"
7141
+ COMPREPLY=( $(compgen -W "\${modules}" -- "\${cur}") )
7142
+ fi
7143
+ ;;
7144
+ completion)
7145
+ if [[ \${cword} -eq 2 ]]; then
7146
+ COMPREPLY=( $(compgen -W "bash zsh" -- "\${cur}") )
7147
+ fi
7148
+ ;;
7149
+ *)
7150
+ if [[ \${cword} -eq 1 ]]; then
7151
+ COMPREPLY=( $(compgen -W "\${commands}" -- "\${cur}") )
7152
+ fi
7153
+ ;;
7154
+ esac
7155
+ }
7156
+
7157
+ complete -F _servcraft_completions servcraft
7158
+ `;
7159
+ var zshScript = `
7160
+ #compdef servcraft
7161
+
7162
+ _servcraft() {
7163
+ local context state state_descr line
7164
+ typeset -A opt_args
7165
+
7166
+ _arguments -C \\
7167
+ '1: :_servcraft_commands' \\
7168
+ '*::arg:->args'
7169
+
7170
+ case $state in
7171
+ args)
7172
+ case $line[1] in
7173
+ generate|g)
7174
+ _servcraft_generate
7175
+ ;;
7176
+ add|remove|rm|update)
7177
+ _servcraft_modules
7178
+ ;;
7179
+ completion)
7180
+ _arguments '1: :(bash zsh)'
7181
+ ;;
7182
+ esac
7183
+ ;;
7184
+ esac
7185
+ }
7186
+
7187
+ _servcraft_commands() {
7188
+ local commands
7189
+ commands=(
7190
+ 'init:Initialize a new ServCraft project'
7191
+ 'add:Add a pre-built module to your project'
7192
+ 'generate:Generate code files (controller, service, etc.)'
7193
+ 'list:List available and installed modules'
7194
+ 'remove:Remove an installed module'
7195
+ 'doctor:Diagnose project configuration'
7196
+ 'update:Update installed modules'
7197
+ 'completion:Generate shell completion scripts'
7198
+ 'docs:Open documentation'
7199
+ '--version:Show version'
7200
+ '--help:Show help'
7201
+ )
7202
+ _describe 'command' commands
7203
+ }
7204
+
7205
+ _servcraft_generate() {
7206
+ local subcommands
7207
+ subcommands=(
7208
+ 'module:Generate a complete module (controller + service + routes)'
7209
+ 'controller:Generate a controller'
7210
+ 'service:Generate a service'
7211
+ 'repository:Generate a repository'
7212
+ 'types:Generate TypeScript types'
7213
+ 'schema:Generate validation schema'
7214
+ 'routes:Generate routes file'
7215
+ 'm:Alias for module'
7216
+ 'c:Alias for controller'
7217
+ 's:Alias for service'
7218
+ 'r:Alias for repository'
7219
+ 't:Alias for types'
7220
+ )
7221
+ _describe 'subcommand' subcommands
7222
+ }
7223
+
7224
+ _servcraft_modules() {
7225
+ local modules
7226
+ modules=(
7227
+ 'auth:Authentication & Authorization'
7228
+ 'cache:Redis caching'
7229
+ 'rate-limit:Rate limiting'
7230
+ 'notification:Email/SMS notifications'
7231
+ 'payment:Payment integration'
7232
+ 'oauth:OAuth providers'
7233
+ 'mfa:Multi-factor authentication'
7234
+ 'queue:Background jobs'
7235
+ 'websocket:WebSocket support'
7236
+ 'upload:File upload handling'
7237
+ )
7238
+ _describe 'module' modules
7239
+ }
7240
+
7241
+ _servcraft "$@"
7242
+ `;
7243
+ var completionCommand = new Command10("completion").description("Generate shell completion scripts").argument("<shell>", "Shell type (bash or zsh)").action((shell) => {
7244
+ const shellLower = shell.toLowerCase();
7245
+ if (shellLower === "bash") {
7246
+ console.log(bashScript);
7247
+ } else if (shellLower === "zsh") {
7248
+ console.log(zshScript);
7249
+ } else {
7250
+ console.error(`Unsupported shell: ${shell}`);
7251
+ console.error("Supported shells: bash, zsh");
7252
+ process.exit(1);
7253
+ }
7254
+ });
7255
+
5917
7256
  // src/cli/index.ts
5918
- var program = new Command6();
7257
+ var program = new Command11();
5919
7258
  program.name("servcraft").description("Servcraft - A modular Node.js backend framework CLI").version("0.1.0");
5920
7259
  program.addCommand(initCommand);
5921
7260
  program.addCommand(generateCommand);
5922
7261
  program.addCommand(addModuleCommand);
5923
7262
  program.addCommand(dbCommand);
5924
7263
  program.addCommand(docsCommand);
7264
+ program.addCommand(listCommand);
7265
+ program.addCommand(removeCommand);
7266
+ program.addCommand(doctorCommand);
7267
+ program.addCommand(updateCommand);
7268
+ program.addCommand(completionCommand);
5925
7269
  program.parse();
5926
7270
  //# sourceMappingURL=index.js.map