servcraft 0.1.6 → 0.2.0

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,316 @@
1
1
  #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
7
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
8
+ }) : x)(function(x) {
9
+ if (typeof require !== "undefined") return require.apply(this, arguments);
10
+ throw Error('Dynamic require of "' + x + '" is not supported');
11
+ });
12
+ var __esm = (fn, res) => function __init() {
13
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
14
+ };
15
+ var __export = (target, all) => {
16
+ for (var name in all)
17
+ __defProp(target, name, { get: all[name], enumerable: true });
18
+ };
19
+ var __copyProps = (to, from, except, desc) => {
20
+ if (from && typeof from === "object" || typeof from === "function") {
21
+ for (let key of __getOwnPropNames(from))
22
+ if (!__hasOwnProp.call(to, key) && key !== except)
23
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
24
+ }
25
+ return to;
26
+ };
27
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
+
29
+ // node_modules/tsup/assets/esm_shims.js
30
+ import path from "path";
31
+ import { fileURLToPath } from "url";
32
+ var init_esm_shims = __esm({
33
+ "node_modules/tsup/assets/esm_shims.js"() {
34
+ "use strict";
35
+ }
36
+ });
37
+
38
+ // src/cli/utils/error-handler.ts
39
+ var error_handler_exports = {};
40
+ __export(error_handler_exports, {
41
+ ErrorTypes: () => ErrorTypes,
42
+ ServCraftError: () => ServCraftError,
43
+ displayError: () => displayError,
44
+ handleSystemError: () => handleSystemError,
45
+ validateProject: () => validateProject
46
+ });
47
+ import chalk4 from "chalk";
48
+ function displayError(error2) {
49
+ console.error("\n" + chalk4.red.bold("\u2717 Error: ") + chalk4.red(error2.message));
50
+ if (error2 instanceof ServCraftError) {
51
+ if (error2.suggestions.length > 0) {
52
+ console.log("\n" + chalk4.yellow.bold("\u{1F4A1} Suggestions:"));
53
+ error2.suggestions.forEach((suggestion) => {
54
+ console.log(chalk4.yellow(" \u2022 ") + suggestion);
55
+ });
56
+ }
57
+ if (error2.docsLink) {
58
+ console.log("\n" + chalk4.blue.bold("\u{1F4DA} Documentation: ") + chalk4.blue.underline(error2.docsLink));
59
+ }
60
+ }
61
+ console.log();
62
+ }
63
+ function handleSystemError(err) {
64
+ switch (err.code) {
65
+ case "ENOENT":
66
+ return new ServCraftError(
67
+ `File or directory not found: ${err.path}`,
68
+ [`Check if the path exists`, `Create the directory first`]
69
+ );
70
+ case "EACCES":
71
+ case "EPERM":
72
+ return new ServCraftError(
73
+ `Permission denied: ${err.path}`,
74
+ [
75
+ `Check file permissions`,
76
+ `Try running with elevated privileges (not recommended)`,
77
+ `Change ownership of the directory`
78
+ ]
79
+ );
80
+ case "EEXIST":
81
+ return new ServCraftError(
82
+ `File or directory already exists: ${err.path}`,
83
+ [`Use a different name`, `Remove the existing file first`, `Use ${chalk4.cyan("--force")} to overwrite`]
84
+ );
85
+ case "ENOTDIR":
86
+ return new ServCraftError(
87
+ `Not a directory: ${err.path}`,
88
+ [`Check the path`, `A file exists where a directory is expected`]
89
+ );
90
+ case "EISDIR":
91
+ return new ServCraftError(
92
+ `Is a directory: ${err.path}`,
93
+ [`Cannot perform this operation on a directory`, `Did you mean to target a file?`]
94
+ );
95
+ default:
96
+ return new ServCraftError(
97
+ err.message,
98
+ [`Check system error code: ${err.code}`, `Review the error details above`]
99
+ );
100
+ }
101
+ }
102
+ function validateProject() {
103
+ try {
104
+ const fs10 = __require("fs");
105
+ const path12 = __require("path");
106
+ if (!fs10.existsSync("package.json")) {
107
+ return ErrorTypes.NOT_IN_PROJECT();
108
+ }
109
+ const packageJson = JSON.parse(fs10.readFileSync("package.json", "utf-8"));
110
+ if (!packageJson.dependencies?.fastify) {
111
+ return new ServCraftError(
112
+ "This does not appear to be a ServCraft project",
113
+ [
114
+ `ServCraft projects require Fastify`,
115
+ `Run ${chalk4.cyan("servcraft init")} to create a new project`
116
+ ]
117
+ );
118
+ }
119
+ return null;
120
+ } catch (err) {
121
+ return new ServCraftError(
122
+ "Failed to validate project",
123
+ [`Ensure you are in the project root directory`, `Check if ${chalk4.yellow("package.json")} is valid`]
124
+ );
125
+ }
126
+ }
127
+ var ServCraftError, ErrorTypes;
128
+ var init_error_handler = __esm({
129
+ "src/cli/utils/error-handler.ts"() {
130
+ "use strict";
131
+ init_esm_shims();
132
+ ServCraftError = class extends Error {
133
+ suggestions;
134
+ docsLink;
135
+ constructor(message, suggestions = [], docsLink) {
136
+ super(message);
137
+ this.name = "ServCraftError";
138
+ this.suggestions = suggestions;
139
+ this.docsLink = docsLink;
140
+ }
141
+ };
142
+ ErrorTypes = {
143
+ MODULE_NOT_FOUND: (moduleName) => new ServCraftError(
144
+ `Module "${moduleName}" not found`,
145
+ [
146
+ `Run ${chalk4.cyan("servcraft list")} to see available modules`,
147
+ `Check the spelling of the module name`,
148
+ `Visit ${chalk4.blue("https://github.com/Le-Sourcier/servcraft#modules")} for module list`
149
+ ],
150
+ "https://github.com/Le-Sourcier/servcraft#add-pre-built-modules"
151
+ ),
152
+ MODULE_ALREADY_EXISTS: (moduleName) => new ServCraftError(
153
+ `Module "${moduleName}" already exists`,
154
+ [
155
+ `Use ${chalk4.cyan("servcraft add " + moduleName + " --force")} to overwrite`,
156
+ `Use ${chalk4.cyan("servcraft add " + moduleName + " --update")} to update`,
157
+ `Use ${chalk4.cyan("servcraft add " + moduleName + " --skip-existing")} to skip`
158
+ ]
159
+ ),
160
+ NOT_IN_PROJECT: () => new ServCraftError(
161
+ "Not in a ServCraft project directory",
162
+ [
163
+ `Run ${chalk4.cyan("servcraft init")} to create a new project`,
164
+ `Navigate to your ServCraft project directory`,
165
+ `Check if ${chalk4.yellow("package.json")} exists`
166
+ ],
167
+ "https://github.com/Le-Sourcier/servcraft#initialize-project"
168
+ ),
169
+ FILE_ALREADY_EXISTS: (fileName) => new ServCraftError(
170
+ `File "${fileName}" already exists`,
171
+ [
172
+ `Use ${chalk4.cyan("--force")} flag to overwrite`,
173
+ `Choose a different name`,
174
+ `Delete the existing file first`
175
+ ]
176
+ ),
177
+ INVALID_DATABASE: (database) => new ServCraftError(
178
+ `Invalid database type: "${database}"`,
179
+ [
180
+ `Valid options: ${chalk4.cyan("postgresql, mysql, sqlite, mongodb, none")}`,
181
+ `Use ${chalk4.cyan("servcraft init --db postgresql")} for PostgreSQL`
182
+ ]
183
+ ),
184
+ INVALID_VALIDATOR: (validator) => new ServCraftError(
185
+ `Invalid validator type: "${validator}"`,
186
+ [`Valid options: ${chalk4.cyan("zod, joi, yup")}`, `Default is ${chalk4.cyan("zod")}`]
187
+ ),
188
+ MISSING_DEPENDENCY: (dependency, command) => new ServCraftError(
189
+ `Missing dependency: "${dependency}"`,
190
+ [`Run ${chalk4.cyan(command)} to install`, `Check your ${chalk4.yellow("package.json")}`]
191
+ ),
192
+ INVALID_FIELD_FORMAT: (field) => new ServCraftError(
193
+ `Invalid field format: "${field}"`,
194
+ [
195
+ `Expected format: ${chalk4.cyan("name:type")}`,
196
+ `Example: ${chalk4.cyan("name:string age:number isActive:boolean")}`,
197
+ `Supported types: string, number, boolean, date`
198
+ ]
199
+ ),
200
+ GIT_NOT_INITIALIZED: () => new ServCraftError(
201
+ "Git repository not initialized",
202
+ [
203
+ `Run ${chalk4.cyan("git init")} to initialize git`,
204
+ `This is required for some ServCraft features`
205
+ ]
206
+ )
207
+ };
208
+ }
209
+ });
2
210
 
3
211
  // src/cli/index.ts
4
- import { Command as Command5 } from "commander";
212
+ init_esm_shims();
213
+ import { Command as Command9 } from "commander";
5
214
 
6
215
  // src/cli/commands/init.ts
216
+ init_esm_shims();
7
217
  import { Command } from "commander";
8
- import path2 from "path";
218
+ import path4 from "path";
9
219
  import fs2 from "fs/promises";
10
220
  import ora from "ora";
11
221
  import inquirer from "inquirer";
12
- import chalk2 from "chalk";
222
+ import chalk3 from "chalk";
13
223
  import { execSync } from "child_process";
14
224
 
15
225
  // src/cli/utils/helpers.ts
226
+ init_esm_shims();
16
227
  import fs from "fs/promises";
17
- import path from "path";
228
+ import path3 from "path";
229
+ import chalk2 from "chalk";
230
+
231
+ // src/cli/utils/dry-run.ts
232
+ init_esm_shims();
18
233
  import chalk from "chalk";
234
+ import path2 from "path";
235
+ var DryRunManager = class _DryRunManager {
236
+ static instance;
237
+ enabled = false;
238
+ operations = [];
239
+ constructor() {
240
+ }
241
+ static getInstance() {
242
+ if (!_DryRunManager.instance) {
243
+ _DryRunManager.instance = new _DryRunManager();
244
+ }
245
+ return _DryRunManager.instance;
246
+ }
247
+ enable() {
248
+ this.enabled = true;
249
+ this.operations = [];
250
+ }
251
+ disable() {
252
+ this.enabled = false;
253
+ this.operations = [];
254
+ }
255
+ isEnabled() {
256
+ return this.enabled;
257
+ }
258
+ addOperation(operation) {
259
+ if (this.enabled) {
260
+ this.operations.push(operation);
261
+ }
262
+ }
263
+ getOperations() {
264
+ return [...this.operations];
265
+ }
266
+ printSummary() {
267
+ if (!this.enabled || this.operations.length === 0) {
268
+ return;
269
+ }
270
+ console.log(chalk.bold.yellow("\n\u{1F4CB} Dry Run - Preview of changes:\n"));
271
+ console.log(chalk.gray("No files will be written. Remove --dry-run to apply changes.\n"));
272
+ const createOps = this.operations.filter((op) => op.type === "create");
273
+ const modifyOps = this.operations.filter((op) => op.type === "modify");
274
+ const deleteOps = this.operations.filter((op) => op.type === "delete");
275
+ if (createOps.length > 0) {
276
+ console.log(chalk.green.bold(`
277
+ \u2713 Files to be created (${createOps.length}):`));
278
+ createOps.forEach((op) => {
279
+ const size = op.content ? `${op.content.length} bytes` : "unknown size";
280
+ console.log(` ${chalk.green("+")} ${chalk.cyan(op.path)} ${chalk.gray(`(${size})`)}`);
281
+ });
282
+ }
283
+ if (modifyOps.length > 0) {
284
+ console.log(chalk.yellow.bold(`
285
+ ~ Files to be modified (${modifyOps.length}):`));
286
+ modifyOps.forEach((op) => {
287
+ console.log(` ${chalk.yellow("~")} ${chalk.cyan(op.path)}`);
288
+ });
289
+ }
290
+ if (deleteOps.length > 0) {
291
+ console.log(chalk.red.bold(`
292
+ - Files to be deleted (${deleteOps.length}):`));
293
+ deleteOps.forEach((op) => {
294
+ console.log(` ${chalk.red("-")} ${chalk.cyan(op.path)}`);
295
+ });
296
+ }
297
+ console.log(chalk.gray("\n" + "\u2500".repeat(60)));
298
+ console.log(
299
+ chalk.bold(` Total operations: ${this.operations.length}`) + chalk.gray(
300
+ ` (${createOps.length} create, ${modifyOps.length} modify, ${deleteOps.length} delete)`
301
+ )
302
+ );
303
+ console.log(chalk.gray("\u2500".repeat(60)));
304
+ console.log(chalk.yellow("\n\u26A0 This was a dry run. No files were created or modified."));
305
+ console.log(chalk.gray(" Remove --dry-run to apply these changes.\n"));
306
+ }
307
+ // Helper to format file path relative to cwd
308
+ relativePath(filePath) {
309
+ return path2.relative(process.cwd(), filePath);
310
+ }
311
+ };
312
+
313
+ // src/cli/utils/helpers.ts
19
314
  function toPascalCase(str) {
20
315
  return str.split(/[-_\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
21
316
  }
@@ -47,39 +342,54 @@ async function ensureDir(dirPath) {
47
342
  await fs.mkdir(dirPath, { recursive: true });
48
343
  }
49
344
  async function writeFile(filePath, content) {
50
- await ensureDir(path.dirname(filePath));
345
+ const dryRun = DryRunManager.getInstance();
346
+ if (dryRun.isEnabled()) {
347
+ dryRun.addOperation({
348
+ type: "create",
349
+ path: dryRun.relativePath(filePath),
350
+ content,
351
+ size: content.length
352
+ });
353
+ return;
354
+ }
355
+ await ensureDir(path3.dirname(filePath));
51
356
  await fs.writeFile(filePath, content, "utf-8");
52
357
  }
53
358
  function success(message) {
54
- console.log(chalk.green("\u2713"), message);
359
+ console.log(chalk2.green("\u2713"), message);
55
360
  }
56
361
  function error(message) {
57
- console.error(chalk.red("\u2717"), message);
362
+ console.error(chalk2.red("\u2717"), message);
58
363
  }
59
364
  function warn(message) {
60
- console.log(chalk.yellow("\u26A0"), message);
365
+ console.log(chalk2.yellow("\u26A0"), message);
61
366
  }
62
367
  function info(message) {
63
- console.log(chalk.blue("\u2139"), message);
368
+ console.log(chalk2.blue("\u2139"), message);
64
369
  }
65
370
  function getProjectRoot() {
66
371
  return process.cwd();
67
372
  }
68
373
  function getSourceDir() {
69
- return path.join(getProjectRoot(), "src");
374
+ return path3.join(getProjectRoot(), "src");
70
375
  }
71
376
  function getModulesDir() {
72
- return path.join(getSourceDir(), "modules");
377
+ return path3.join(getSourceDir(), "modules");
73
378
  }
74
379
 
75
380
  // 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("--db <database>", "Database type (postgresql, mysql, sqlite, mongodb, none)").action(
381
+ 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
382
  async (name, cmdOptions) => {
383
+ const dryRun = DryRunManager.getInstance();
384
+ if (cmdOptions?.dryRun) {
385
+ dryRun.enable();
386
+ console.log(chalk3.yellow("\n\u26A0 DRY RUN MODE - No files will be written\n"));
387
+ }
78
388
  console.log(
79
- chalk2.blue(`
389
+ chalk3.blue(`
80
390
  \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
391
  \u2551 \u2551
82
- \u2551 ${chalk2.bold("\u{1F680} Servcraft Project Generator")} \u2551
392
+ \u2551 ${chalk3.bold("\u{1F680} Servcraft Project Generator")} \u2551
83
393
  \u2551 \u2551
84
394
  \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
395
  `)
@@ -90,6 +400,7 @@ var initCommand = new Command("init").alias("new").description("Initialize a new
90
400
  options = {
91
401
  name: name || "my-servcraft-app",
92
402
  language: cmdOptions.javascript ? "javascript" : "typescript",
403
+ moduleSystem: cmdOptions.commonjs ? "commonjs" : "esm",
93
404
  database: db,
94
405
  orm: db === "mongodb" ? "mongoose" : db === "none" ? "none" : "prisma",
95
406
  validator: "zod",
@@ -119,6 +430,16 @@ var initCommand = new Command("init").alias("new").description("Initialize a new
119
430
  ],
120
431
  default: "typescript"
121
432
  },
433
+ {
434
+ type: "list",
435
+ name: "moduleSystem",
436
+ message: "Select module system:",
437
+ choices: [
438
+ { name: "ESM (import/export) - Recommended", value: "esm" },
439
+ { name: "CommonJS (require/module.exports)", value: "commonjs" }
440
+ ],
441
+ default: "esm"
442
+ },
122
443
  {
123
444
  type: "list",
124
445
  name: "database",
@@ -163,7 +484,7 @@ var initCommand = new Command("init").alias("new").description("Initialize a new
163
484
  orm: db === "mongodb" ? "mongoose" : db === "none" ? "none" : "prisma"
164
485
  };
165
486
  }
166
- const projectDir = path2.resolve(process.cwd(), options.name);
487
+ const projectDir = path4.resolve(process.cwd(), options.name);
167
488
  const spinner = ora("Creating project...").start();
168
489
  try {
169
490
  try {
@@ -177,24 +498,24 @@ var initCommand = new Command("init").alias("new").description("Initialize a new
177
498
  spinner.text = "Generating project files...";
178
499
  const packageJson = generatePackageJson(options);
179
500
  await writeFile(
180
- path2.join(projectDir, "package.json"),
501
+ path4.join(projectDir, "package.json"),
181
502
  JSON.stringify(packageJson, null, 2)
182
503
  );
183
504
  if (options.language === "typescript") {
184
- await writeFile(path2.join(projectDir, "tsconfig.json"), generateTsConfig());
185
- await writeFile(path2.join(projectDir, "tsup.config.ts"), generateTsupConfig());
505
+ await writeFile(path4.join(projectDir, "tsconfig.json"), generateTsConfig(options));
506
+ await writeFile(path4.join(projectDir, "tsup.config.ts"), generateTsupConfig(options));
186
507
  } else {
187
- await writeFile(path2.join(projectDir, "jsconfig.json"), generateJsConfig());
508
+ await writeFile(path4.join(projectDir, "jsconfig.json"), generateJsConfig(options));
188
509
  }
189
- await writeFile(path2.join(projectDir, ".env.example"), generateEnvExample(options));
190
- await writeFile(path2.join(projectDir, ".env"), generateEnvExample(options));
191
- await writeFile(path2.join(projectDir, ".gitignore"), generateGitignore());
192
- await writeFile(path2.join(projectDir, "Dockerfile"), generateDockerfile(options));
510
+ await writeFile(path4.join(projectDir, ".env.example"), generateEnvExample(options));
511
+ await writeFile(path4.join(projectDir, ".env"), generateEnvExample(options));
512
+ await writeFile(path4.join(projectDir, ".gitignore"), generateGitignore());
513
+ await writeFile(path4.join(projectDir, "Dockerfile"), generateDockerfile(options));
193
514
  await writeFile(
194
- path2.join(projectDir, "docker-compose.yml"),
515
+ path4.join(projectDir, "docker-compose.yml"),
195
516
  generateDockerCompose(options)
196
517
  );
197
- const ext = options.language === "typescript" ? "ts" : "js";
518
+ const ext = options.language === "typescript" ? "ts" : options.moduleSystem === "esm" ? "js" : "cjs";
198
519
  const dirs = [
199
520
  "src/core",
200
521
  "src/config",
@@ -212,43 +533,63 @@ var initCommand = new Command("init").alias("new").description("Initialize a new
212
533
  dirs.push("src/database/models");
213
534
  }
214
535
  for (const dir of dirs) {
215
- await ensureDir(path2.join(projectDir, dir));
536
+ await ensureDir(path4.join(projectDir, dir));
216
537
  }
217
- await writeFile(path2.join(projectDir, `src/index.${ext}`), generateEntryFile(options));
538
+ await writeFile(path4.join(projectDir, `src/index.${ext}`), generateEntryFile(options));
218
539
  await writeFile(
219
- path2.join(projectDir, `src/core/server.${ext}`),
540
+ path4.join(projectDir, `src/core/server.${ext}`),
220
541
  generateServerFile(options)
221
542
  );
222
543
  await writeFile(
223
- path2.join(projectDir, `src/core/logger.${ext}`),
544
+ path4.join(projectDir, `src/core/logger.${ext}`),
224
545
  generateLoggerFile(options)
225
546
  );
547
+ await writeFile(
548
+ path4.join(projectDir, `src/config/index.${ext}`),
549
+ generateConfigFile(options)
550
+ );
551
+ await writeFile(
552
+ path4.join(projectDir, `src/middleware/index.${ext}`),
553
+ generateMiddlewareFile(options)
554
+ );
555
+ await writeFile(
556
+ path4.join(projectDir, `src/utils/index.${ext}`),
557
+ generateUtilsFile(options)
558
+ );
559
+ await writeFile(
560
+ path4.join(projectDir, `src/types/index.${ext}`),
561
+ generateTypesFile(options)
562
+ );
226
563
  if (options.orm === "prisma") {
227
564
  await writeFile(
228
- path2.join(projectDir, "prisma/schema.prisma"),
565
+ path4.join(projectDir, "prisma/schema.prisma"),
229
566
  generatePrismaSchema(options)
230
567
  );
231
568
  } else if (options.orm === "mongoose") {
232
569
  await writeFile(
233
- path2.join(projectDir, `src/database/connection.${ext}`),
570
+ path4.join(projectDir, `src/database/connection.${ext}`),
234
571
  generateMongooseConnection(options)
235
572
  );
236
573
  await writeFile(
237
- path2.join(projectDir, `src/database/models/user.model.${ext}`),
574
+ path4.join(projectDir, `src/database/models/user.model.${ext}`),
238
575
  generateMongooseUserModel(options)
239
576
  );
240
577
  }
241
578
  spinner.succeed("Project files generated!");
242
- const installSpinner = ora("Installing dependencies...").start();
243
- try {
244
- execSync("npm install", { cwd: projectDir, stdio: "pipe" });
245
- installSpinner.succeed("Dependencies installed!");
246
- } catch {
247
- installSpinner.warn("Failed to install dependencies automatically");
248
- warn(' Run "npm install" manually in the project directory');
579
+ if (!cmdOptions?.dryRun) {
580
+ const installSpinner = ora("Installing dependencies...").start();
581
+ try {
582
+ execSync("npm install", { cwd: projectDir, stdio: "pipe" });
583
+ installSpinner.succeed("Dependencies installed!");
584
+ } catch {
585
+ installSpinner.warn("Failed to install dependencies automatically");
586
+ warn(' Run "npm install" manually in the project directory');
587
+ }
249
588
  }
250
- console.log("\n" + chalk2.green("\u2728 Project created successfully!"));
251
- console.log("\n" + chalk2.bold("\u{1F4C1} Project structure:"));
589
+ if (!cmdOptions?.dryRun) {
590
+ console.log("\n" + chalk3.green("\u2728 Project created successfully!"));
591
+ }
592
+ console.log("\n" + chalk3.bold("\u{1F4C1} Project structure:"));
252
593
  console.log(`
253
594
  ${options.name}/
254
595
  \u251C\u2500\u2500 src/
@@ -263,19 +604,22 @@ var initCommand = new Command("init").alias("new").description("Initialize a new
263
604
  \u251C\u2500\u2500 docker-compose.yml
264
605
  \u2514\u2500\u2500 package.json
265
606
  `);
266
- console.log(chalk2.bold("\u{1F680} Get started:"));
607
+ console.log(chalk3.bold("\u{1F680} Get started:"));
267
608
  console.log(`
268
- ${chalk2.cyan(`cd ${options.name}`)}
269
- ${options.database !== "none" ? chalk2.cyan("npm run db:push # Setup database") : ""}
270
- ${chalk2.cyan("npm run dev # Start development server")}
609
+ ${chalk3.cyan(`cd ${options.name}`)}
610
+ ${options.database !== "none" ? chalk3.cyan("npm run db:push # Setup database") : ""}
611
+ ${chalk3.cyan("npm run dev # Start development server")}
271
612
  `);
272
- console.log(chalk2.bold("\u{1F4DA} Available commands:"));
613
+ console.log(chalk3.bold("\u{1F4DA} Available commands:"));
273
614
  console.log(`
274
- ${chalk2.yellow("servcraft generate module <name>")} Generate a new module
275
- ${chalk2.yellow("servcraft generate controller <name>")} Generate a controller
276
- ${chalk2.yellow("servcraft generate service <name>")} Generate a service
277
- ${chalk2.yellow("servcraft add auth")} Add authentication module
615
+ ${chalk3.yellow("servcraft generate module <name>")} Generate a new module
616
+ ${chalk3.yellow("servcraft generate controller <name>")} Generate a controller
617
+ ${chalk3.yellow("servcraft generate service <name>")} Generate a service
618
+ ${chalk3.yellow("servcraft add auth")} Add authentication module
278
619
  `);
620
+ if (cmdOptions?.dryRun) {
621
+ dryRun.printSummary();
622
+ }
279
623
  } catch (err) {
280
624
  spinner.fail("Failed to create project");
281
625
  error(err instanceof Error ? err.message : String(err));
@@ -284,18 +628,35 @@ var initCommand = new Command("init").alias("new").description("Initialize a new
284
628
  );
285
629
  function generatePackageJson(options) {
286
630
  const isTS = options.language === "typescript";
631
+ const isESM = options.moduleSystem === "esm";
632
+ let devCommand;
633
+ if (isTS) {
634
+ devCommand = "tsx watch src/index.ts";
635
+ } else if (isESM) {
636
+ devCommand = "node --watch src/index.js";
637
+ } else {
638
+ devCommand = "node --watch src/index.cjs";
639
+ }
640
+ let startCommand;
641
+ if (isTS) {
642
+ startCommand = isESM ? "node dist/index.js" : "node dist/index.cjs";
643
+ } else if (isESM) {
644
+ startCommand = "node src/index.js";
645
+ } else {
646
+ startCommand = "node src/index.cjs";
647
+ }
287
648
  const pkg = {
288
649
  name: options.name,
289
650
  version: "0.1.0",
290
651
  description: "A Servcraft application",
291
- main: isTS ? "dist/index.js" : "src/index.js",
292
- type: "module",
652
+ main: isTS ? isESM ? "dist/index.js" : "dist/index.cjs" : isESM ? "src/index.js" : "src/index.cjs",
653
+ ...isESM && { type: "module" },
293
654
  scripts: {
294
- dev: isTS ? "tsx watch src/index.ts" : "node --watch src/index.js",
655
+ dev: devCommand,
295
656
  build: isTS ? "tsup" : 'echo "No build needed for JS"',
296
- start: isTS ? "node dist/index.js" : "node src/index.js",
657
+ start: startCommand,
297
658
  test: "vitest",
298
- lint: isTS ? "eslint src --ext .ts" : "eslint src --ext .js"
659
+ lint: isTS ? "eslint src --ext .ts" : "eslint src --ext .js,.cjs"
299
660
  },
300
661
  dependencies: {
301
662
  fastify: "^4.28.1",
@@ -357,13 +718,14 @@ function generatePackageJson(options) {
357
718
  }
358
719
  return pkg;
359
720
  }
360
- function generateTsConfig() {
721
+ function generateTsConfig(options) {
722
+ const isESM = options.moduleSystem === "esm";
361
723
  return JSON.stringify(
362
724
  {
363
725
  compilerOptions: {
364
726
  target: "ES2022",
365
- module: "NodeNext",
366
- moduleResolution: "NodeNext",
727
+ module: isESM ? "NodeNext" : "CommonJS",
728
+ moduleResolution: isESM ? "NodeNext" : "Node",
367
729
  lib: ["ES2022"],
368
730
  outDir: "./dist",
369
731
  rootDir: "./src",
@@ -382,12 +744,13 @@ function generateTsConfig() {
382
744
  2
383
745
  );
384
746
  }
385
- function generateJsConfig() {
747
+ function generateJsConfig(options) {
748
+ const isESM = options.moduleSystem === "esm";
386
749
  return JSON.stringify(
387
750
  {
388
751
  compilerOptions: {
389
- module: "NodeNext",
390
- moduleResolution: "NodeNext",
752
+ module: isESM ? "NodeNext" : "CommonJS",
753
+ moduleResolution: isESM ? "NodeNext" : "Node",
391
754
  target: "ES2022",
392
755
  checkJs: true
393
756
  },
@@ -398,12 +761,13 @@ function generateJsConfig() {
398
761
  2
399
762
  );
400
763
  }
401
- function generateTsupConfig() {
764
+ function generateTsupConfig(options) {
765
+ const isESM = options.moduleSystem === "esm";
402
766
  return `import { defineConfig } from 'tsup';
403
767
 
404
768
  export default defineConfig({
405
769
  entry: ['src/index.ts'],
406
- format: ['esm'],
770
+ format: ['${isESM ? "esm" : "cjs"}'],
407
771
  dts: true,
408
772
  clean: true,
409
773
  sourcemap: true,
@@ -412,7 +776,7 @@ export default defineConfig({
412
776
  `;
413
777
  }
414
778
  function generateEnvExample(options) {
415
- let env = `# Server
779
+ let env2 = `# Server
416
780
  NODE_ENV=development
417
781
  PORT=3000
418
782
  HOST=0.0.0.0
@@ -430,31 +794,31 @@ RATE_LIMIT_MAX=100
430
794
  LOG_LEVEL=info
431
795
  `;
432
796
  if (options.database === "postgresql") {
433
- env += `
797
+ env2 += `
434
798
  # Database (PostgreSQL)
435
799
  DATABASE_PROVIDER=postgresql
436
800
  DATABASE_URL="postgresql://user:password@localhost:5432/mydb?schema=public"
437
801
  `;
438
802
  } else if (options.database === "mysql") {
439
- env += `
803
+ env2 += `
440
804
  # Database (MySQL)
441
805
  DATABASE_PROVIDER=mysql
442
806
  DATABASE_URL="mysql://user:password@localhost:3306/mydb"
443
807
  `;
444
808
  } else if (options.database === "sqlite") {
445
- env += `
809
+ env2 += `
446
810
  # Database (SQLite)
447
811
  DATABASE_PROVIDER=sqlite
448
812
  DATABASE_URL="file:./dev.db"
449
813
  `;
450
814
  } else if (options.database === "mongodb") {
451
- env += `
815
+ env2 += `
452
816
  # Database (MongoDB)
453
817
  MONGODB_URI="mongodb://localhost:27017/mydb"
454
818
  `;
455
819
  }
456
820
  if (options.features.includes("email")) {
457
- env += `
821
+ env2 += `
458
822
  # Email
459
823
  SMTP_HOST=smtp.example.com
460
824
  SMTP_PORT=587
@@ -464,12 +828,12 @@ SMTP_FROM="App <noreply@example.com>"
464
828
  `;
465
829
  }
466
830
  if (options.features.includes("redis")) {
467
- env += `
831
+ env2 += `
468
832
  # Redis
469
833
  REDIS_URL=redis://localhost:6379
470
834
  `;
471
835
  }
472
- return env;
836
+ return env2;
473
837
  }
474
838
  function generateGitignore() {
475
839
  return `node_modules/
@@ -598,7 +962,12 @@ model User {
598
962
  }
599
963
  function generateEntryFile(options) {
600
964
  const isTS = options.language === "typescript";
601
- return `${isTS ? "import 'dotenv/config';\nimport { createServer } from './core/server.js';\nimport { logger } from './core/logger.js';" : "require('dotenv').config();\nconst { createServer } = require('./core/server.js');\nconst { logger } = require('./core/logger.js');"}
965
+ const isESM = options.moduleSystem === "esm";
966
+ const fileExt = isTS ? "js" : isESM ? "js" : "cjs";
967
+ if (isESM || isTS) {
968
+ return `import 'dotenv/config';
969
+ import { createServer } from './core/server.${fileExt}';
970
+ import { logger } from './core/logger.${fileExt}';
602
971
 
603
972
  async function main()${isTS ? ": Promise<void>" : ""} {
604
973
  const server = createServer();
@@ -613,16 +982,31 @@ async function main()${isTS ? ": Promise<void>" : ""} {
613
982
 
614
983
  main();
615
984
  `;
985
+ } else {
986
+ return `require('dotenv').config();
987
+ const { createServer } = require('./core/server.cjs');
988
+ const { logger } = require('./core/logger.cjs');
989
+
990
+ async function main() {
991
+ const server = createServer();
992
+
993
+ try {
994
+ await server.start();
995
+ } catch (error) {
996
+ logger.error({ err: error }, 'Failed to start server');
997
+ process.exit(1);
998
+ }
999
+ }
1000
+
1001
+ main();
1002
+ `;
1003
+ }
616
1004
  }
617
1005
  function generateServerFile(options) {
618
1006
  const isTS = options.language === "typescript";
619
- return `${isTS ? `import Fastify from 'fastify';
620
- import type { FastifyInstance } from 'fastify';
621
- import { logger } from './logger.js';` : `const Fastify = require('fastify');
622
- const { logger } = require('./logger.js');`}
623
-
624
- ${isTS ? "export function createServer(): { instance: FastifyInstance; start: () => Promise<void> }" : "function createServer()"} {
625
- const app = Fastify({ logger });
1007
+ const isESM = options.moduleSystem === "esm";
1008
+ const fileExt = isTS ? "js" : isESM ? "js" : "cjs";
1009
+ const serverBody = ` const app = Fastify({ logger });
626
1010
 
627
1011
  // Health check
628
1012
  app.get('/health', async () => ({
@@ -649,33 +1033,58 @@ ${isTS ? "export function createServer(): { instance: FastifyInstance; start: ()
649
1033
  logger.info(\`Server listening on \${host}:\${port}\`);
650
1034
  },
651
1035
  };
652
- }
1036
+ }`;
1037
+ if (isESM || isTS) {
1038
+ return `import Fastify from 'fastify';
1039
+ ${isTS ? "import type { FastifyInstance } from 'fastify';" : ""}
1040
+ import { logger } from './logger.${fileExt}';
1041
+
1042
+ ${isTS ? "export function createServer(): { instance: FastifyInstance; start: () => Promise<void> }" : "export function createServer()"} {
1043
+ ${serverBody}
1044
+ `;
1045
+ } else {
1046
+ return `const Fastify = require('fastify');
1047
+ const { logger } = require('./logger.cjs');
1048
+
1049
+ function createServer() {
1050
+ ${serverBody}
653
1051
 
654
- ${isTS ? "" : "module.exports = { createServer };"}
1052
+ module.exports = { createServer };
655
1053
  `;
1054
+ }
656
1055
  }
657
1056
  function generateLoggerFile(options) {
658
1057
  const isTS = options.language === "typescript";
659
- return `${isTS ? "import pino from 'pino';\nimport type { Logger } from 'pino';" : "const pino = require('pino');"}
660
-
661
- ${isTS ? "export const logger: Logger" : "const logger"} = pino({
1058
+ const isESM = options.moduleSystem === "esm";
1059
+ const loggerBody = `pino({
662
1060
  level: process.env.LOG_LEVEL || 'info',
663
1061
  transport: process.env.NODE_ENV !== 'production' ? {
664
1062
  target: 'pino-pretty',
665
1063
  options: { colorize: true },
666
1064
  } : undefined,
667
- });
1065
+ })`;
1066
+ if (isESM || isTS) {
1067
+ return `import pino from 'pino';
1068
+ ${isTS ? "import type { Logger } from 'pino';" : ""}
1069
+
1070
+ export const logger${isTS ? ": Logger" : ""} = ${loggerBody};
1071
+ `;
1072
+ } else {
1073
+ return `const pino = require('pino');
1074
+
1075
+ const logger = ${loggerBody};
668
1076
 
669
- ${isTS ? "" : "module.exports = { logger };"}
1077
+ module.exports = { logger };
670
1078
  `;
1079
+ }
671
1080
  }
672
1081
  function generateMongooseConnection(options) {
673
1082
  const isTS = options.language === "typescript";
674
- return `${isTS ? "import mongoose from 'mongoose';\nimport { logger } from '../core/logger.js';" : "const mongoose = require('mongoose');\nconst { logger } = require('../core/logger.js');"}
675
-
676
- const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/mydb';
1083
+ const isESM = options.moduleSystem === "esm";
1084
+ const fileExt = isTS ? "js" : isESM ? "js" : "cjs";
1085
+ const connectionBody = `const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/mydb';
677
1086
 
678
- ${isTS ? "export async function connectDatabase(): Promise<typeof mongoose>" : "async function connectDatabase()"} {
1087
+ async function connectDatabase()${isTS ? ": Promise<typeof mongoose>" : ""} {
679
1088
  try {
680
1089
  const conn = await mongoose.connect(MONGODB_URI);
681
1090
  logger.info(\`MongoDB connected: \${conn.connection.host}\`);
@@ -686,35 +1095,36 @@ ${isTS ? "export async function connectDatabase(): Promise<typeof mongoose>" : "
686
1095
  }
687
1096
  }
688
1097
 
689
- ${isTS ? "export async function disconnectDatabase(): Promise<void>" : "async function disconnectDatabase()"} {
1098
+ async function disconnectDatabase()${isTS ? ": Promise<void>" : ""} {
690
1099
  try {
691
1100
  await mongoose.disconnect();
692
1101
  logger.info('MongoDB disconnected');
693
1102
  } catch (error) {
694
1103
  logger.error({ err: error }, 'MongoDB disconnect failed');
695
1104
  }
696
- }
1105
+ }`;
1106
+ if (isESM || isTS) {
1107
+ return `import mongoose from 'mongoose';
1108
+ import { logger } from '../core/logger.${fileExt}';
1109
+
1110
+ ${connectionBody}
1111
+
1112
+ export { connectDatabase, disconnectDatabase, mongoose };
1113
+ `;
1114
+ } else {
1115
+ return `const mongoose = require('mongoose');
1116
+ const { logger } = require('../core/logger.cjs');
697
1117
 
698
- ${isTS ? "export { mongoose };" : "module.exports = { connectDatabase, disconnectDatabase, mongoose };"}
1118
+ ${connectionBody}
1119
+
1120
+ module.exports = { connectDatabase, disconnectDatabase, mongoose };
699
1121
  `;
1122
+ }
700
1123
  }
701
1124
  function generateMongooseUserModel(options) {
702
1125
  const isTS = options.language === "typescript";
703
- return `${isTS ? "import mongoose, { Schema, Document } from 'mongoose';\nimport bcrypt from 'bcryptjs';" : "const mongoose = require('mongoose');\nconst bcrypt = require('bcryptjs');\nconst { Schema } = mongoose;"}
704
-
705
- ${isTS ? `export interface IUser extends Document {
706
- email: string;
707
- password: string;
708
- name?: string;
709
- role: 'user' | 'admin';
710
- status: 'active' | 'inactive' | 'suspended';
711
- emailVerified: boolean;
712
- createdAt: Date;
713
- updatedAt: Date;
714
- comparePassword(candidatePassword: string): Promise<boolean>;
715
- }` : ""}
716
-
717
- const userSchema = new Schema${isTS ? "<IUser>" : ""}({
1126
+ const isESM = options.moduleSystem === "esm";
1127
+ const schemaBody = `const userSchema = new Schema${isTS ? "<IUser>" : ""}({
718
1128
  email: {
719
1129
  type: String,
720
1130
  required: true,
@@ -765,19 +1175,250 @@ userSchema.pre('save', async function(next) {
765
1175
  // Compare password method
766
1176
  userSchema.methods.comparePassword = async function(candidatePassword${isTS ? ": string" : ""})${isTS ? ": Promise<boolean>" : ""} {
767
1177
  return bcrypt.compare(candidatePassword, this.password);
768
- };
1178
+ };`;
1179
+ const tsInterface = isTS ? `
1180
+ export interface IUser extends Document {
1181
+ email: string;
1182
+ password: string;
1183
+ name?: string;
1184
+ role: 'user' | 'admin';
1185
+ status: 'active' | 'inactive' | 'suspended';
1186
+ emailVerified: boolean;
1187
+ createdAt: Date;
1188
+ updatedAt: Date;
1189
+ comparePassword(candidatePassword: string): Promise<boolean>;
1190
+ }
1191
+ ` : "";
1192
+ if (isESM || isTS) {
1193
+ return `import mongoose${isTS ? ", { Schema, Document }" : ""} from 'mongoose';
1194
+ import bcrypt from 'bcryptjs';
1195
+ ${!isTS ? "const { Schema } = mongoose;" : ""}
1196
+ ${tsInterface}
1197
+ ${schemaBody}
1198
+
1199
+ export const User = mongoose.model${isTS ? "<IUser>" : ""}('User', userSchema);
1200
+ `;
1201
+ } else {
1202
+ return `const mongoose = require('mongoose');
1203
+ const bcrypt = require('bcryptjs');
1204
+ const { Schema } = mongoose;
1205
+
1206
+ ${schemaBody}
1207
+
1208
+ const User = mongoose.model('User', userSchema);
1209
+
1210
+ module.exports = { User };
1211
+ `;
1212
+ }
1213
+ }
1214
+ function generateConfigFile(options) {
1215
+ const isTS = options.language === "typescript";
1216
+ const isESM = options.moduleSystem === "esm";
1217
+ const configBody = `{
1218
+ env: process.env.NODE_ENV || 'development',
1219
+ port: parseInt(process.env.PORT || '3000', 10),
1220
+ host: process.env.HOST || '0.0.0.0',
1221
+
1222
+ jwt: {
1223
+ secret: process.env.JWT_SECRET || 'your-secret-key',
1224
+ accessExpiresIn: process.env.JWT_ACCESS_EXPIRES_IN || '15m',
1225
+ refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d',
1226
+ },
1227
+
1228
+ cors: {
1229
+ origin: process.env.CORS_ORIGIN || 'http://localhost:3000',
1230
+ },
1231
+
1232
+ rateLimit: {
1233
+ max: parseInt(process.env.RATE_LIMIT_MAX || '100', 10),
1234
+ },
1235
+
1236
+ log: {
1237
+ level: process.env.LOG_LEVEL || 'info',
1238
+ },
1239
+ }${isTS ? " as const" : ""}`;
1240
+ if (isESM || isTS) {
1241
+ return `import 'dotenv/config';
1242
+
1243
+ export const config = ${configBody};
1244
+ `;
1245
+ } else {
1246
+ return `require('dotenv').config();
1247
+
1248
+ const config = ${configBody};
1249
+
1250
+ module.exports = { config };
1251
+ `;
1252
+ }
1253
+ }
1254
+ function generateMiddlewareFile(options) {
1255
+ const isTS = options.language === "typescript";
1256
+ const isESM = options.moduleSystem === "esm";
1257
+ const fileExt = isTS ? "js" : isESM ? "js" : "cjs";
1258
+ const middlewareBody = `/**
1259
+ * Error handler middleware
1260
+ */
1261
+ function errorHandler(error${isTS ? ": Error" : ""}, request${isTS ? ": FastifyRequest" : ""}, reply${isTS ? ": FastifyReply" : ""})${isTS ? ": void" : ""} {
1262
+ logger.error({ err: error, url: request.url, method: request.method }, 'Request error');
1263
+
1264
+ const statusCode = (error${isTS ? " as any" : ""}).statusCode || 500;
1265
+ const message = statusCode === 500 ? 'Internal Server Error' : error.message;
1266
+
1267
+ reply.status(statusCode).send({
1268
+ success: false,
1269
+ error: message,
1270
+ ...(process.env.NODE_ENV === 'development' && { stack: error.stack }),
1271
+ });
1272
+ }
1273
+
1274
+ /**
1275
+ * Request logging middleware
1276
+ */
1277
+ function requestLogger(request${isTS ? ": FastifyRequest" : ""}, reply${isTS ? ": FastifyReply" : ""}, done${isTS ? ": () => void" : ""})${isTS ? ": void" : ""} {
1278
+ logger.info({ url: request.url, method: request.method, ip: request.ip }, 'Incoming request');
1279
+ done();
1280
+ }`;
1281
+ if (isESM || isTS) {
1282
+ return `${isTS ? "import type { FastifyRequest, FastifyReply } from 'fastify';" : ""}
1283
+ import { logger } from '../core/logger.${fileExt}';
1284
+
1285
+ ${middlewareBody}
1286
+
1287
+ export { errorHandler, requestLogger };
1288
+ `;
1289
+ } else {
1290
+ return `const { logger } = require('../core/logger.cjs');
1291
+
1292
+ ${middlewareBody}
1293
+
1294
+ module.exports = { errorHandler, requestLogger };
1295
+ `;
1296
+ }
1297
+ }
1298
+ function generateUtilsFile(options) {
1299
+ const isTS = options.language === "typescript";
1300
+ const isESM = options.moduleSystem === "esm";
1301
+ const utilsBody = `/**
1302
+ * Standard API response helper
1303
+ */
1304
+ function apiResponse${isTS ? "<T>" : ""}(data${isTS ? ": T" : ""}, message = "Success")${isTS ? ": { success: boolean; message: string; data: T }" : ""} {
1305
+ return {
1306
+ success: true,
1307
+ message,
1308
+ data,
1309
+ };
1310
+ }
1311
+
1312
+ /**
1313
+ * Error response helper
1314
+ */
1315
+ function errorResponse(message${isTS ? ": string" : ""}, code${isTS ? "?: string" : ""})${isTS ? ": { success: boolean; error: string; code?: string }" : ""} {
1316
+ return {
1317
+ success: false,
1318
+ error: message,
1319
+ ...(code && { code }),
1320
+ };
1321
+ }
1322
+
1323
+ /**
1324
+ * Pagination helper
1325
+ */
1326
+ function paginate${isTS ? "<T>" : ""}(data${isTS ? ": T[]" : ""}, page${isTS ? ": number" : ""}, limit${isTS ? ": number" : ""}, total${isTS ? ": number" : ""})${isTS ? ": PaginationResult<T>" : ""} {
1327
+ const totalPages = Math.ceil(total / limit);
1328
+
1329
+ return {
1330
+ data,
1331
+ pagination: {
1332
+ page,
1333
+ limit,
1334
+ total,
1335
+ totalPages,
1336
+ hasNextPage: page < totalPages,
1337
+ hasPrevPage: page > 1,
1338
+ },
1339
+ };
1340
+ }`;
1341
+ const tsInterface = isTS ? `
1342
+ /**
1343
+ * Pagination result type
1344
+ */
1345
+ export interface PaginationResult<T> {
1346
+ data: T[];
1347
+ pagination: {
1348
+ page: number;
1349
+ limit: number;
1350
+ total: number;
1351
+ totalPages: number;
1352
+ hasNextPage: boolean;
1353
+ hasPrevPage: boolean;
1354
+ };
1355
+ }
1356
+ ` : "";
1357
+ if (isESM || isTS) {
1358
+ return `${tsInterface}
1359
+ ${utilsBody}
1360
+
1361
+ export { apiResponse, errorResponse, paginate };
1362
+ `;
1363
+ } else {
1364
+ return `${utilsBody}
1365
+
1366
+ module.exports = { apiResponse, errorResponse, paginate };
1367
+ `;
1368
+ }
1369
+ }
1370
+ function generateTypesFile(options) {
1371
+ if (options.language !== "typescript") {
1372
+ return "// Types file - not needed for JavaScript\n";
1373
+ }
1374
+ return `/**
1375
+ * Common type definitions
1376
+ */
1377
+
1378
+ export interface ApiResponse<T = unknown> {
1379
+ success: boolean;
1380
+ message?: string;
1381
+ data?: T;
1382
+ error?: string;
1383
+ code?: string;
1384
+ }
1385
+
1386
+ export interface PaginatedResponse<T> {
1387
+ data: T[];
1388
+ pagination: {
1389
+ page: number;
1390
+ limit: number;
1391
+ total: number;
1392
+ totalPages: number;
1393
+ hasNextPage: boolean;
1394
+ hasPrevPage: boolean;
1395
+ };
1396
+ }
1397
+
1398
+ export interface RequestUser {
1399
+ id: string;
1400
+ email: string;
1401
+ role: string;
1402
+ }
769
1403
 
770
- ${isTS ? "export const User = mongoose.model<IUser>('User', userSchema);" : "const User = mongoose.model('User', userSchema);\nmodule.exports = { User };"}
1404
+ declare module 'fastify' {
1405
+ interface FastifyRequest {
1406
+ user?: RequestUser;
1407
+ }
1408
+ }
771
1409
  `;
772
1410
  }
773
1411
 
774
1412
  // src/cli/commands/generate.ts
1413
+ init_esm_shims();
775
1414
  import { Command as Command2 } from "commander";
776
- import path3 from "path";
1415
+ import path5 from "path";
777
1416
  import ora2 from "ora";
778
1417
  import inquirer2 from "inquirer";
1418
+ import chalk5 from "chalk";
779
1419
 
780
1420
  // src/cli/utils/field-parser.ts
1421
+ init_esm_shims();
781
1422
  var tsTypeMap = {
782
1423
  string: "string",
783
1424
  number: "number",
@@ -919,7 +1560,11 @@ function parseFields(fieldsStr) {
919
1560
  return fieldsStr.split(/\s+/).filter(Boolean).map(parseField);
920
1561
  }
921
1562
 
1563
+ // src/cli/commands/generate.ts
1564
+ init_error_handler();
1565
+
922
1566
  // src/cli/templates/controller.ts
1567
+ init_esm_shims();
923
1568
  function controllerTemplate(name, pascalName, camelName) {
924
1569
  return `import type { FastifyRequest, FastifyReply } from 'fastify';
925
1570
  import type { ${pascalName}Service } from './${name}.service.js';
@@ -989,6 +1634,7 @@ export function create${pascalName}Controller(${camelName}Service: ${pascalName}
989
1634
  }
990
1635
 
991
1636
  // src/cli/templates/service.ts
1637
+ init_esm_shims();
992
1638
  function serviceTemplate(name, pascalName, camelName) {
993
1639
  return `import type { PaginatedResult, PaginationParams } from '../../types/index.js';
994
1640
  import { NotFoundError, ConflictError } from '../../utils/errors.js';
@@ -1049,6 +1695,7 @@ export function create${pascalName}Service(repository?: ${pascalName}Repository)
1049
1695
  }
1050
1696
 
1051
1697
  // src/cli/templates/repository.ts
1698
+ init_esm_shims();
1052
1699
  function repositoryTemplate(name, pascalName, camelName, pluralName) {
1053
1700
  return `import { randomUUID } from 'crypto';
1054
1701
  import type { PaginatedResult, PaginationParams } from '../../types/index.js';
@@ -1155,6 +1802,7 @@ export function create${pascalName}Repository(): ${pascalName}Repository {
1155
1802
  }
1156
1803
 
1157
1804
  // src/cli/templates/types.ts
1805
+ init_esm_shims();
1158
1806
  function typesTemplate(name, pascalName) {
1159
1807
  return `import type { BaseEntity } from '../../types/index.js';
1160
1808
 
@@ -1184,6 +1832,7 @@ export interface ${pascalName}Filters {
1184
1832
  }
1185
1833
 
1186
1834
  // src/cli/templates/schemas.ts
1835
+ init_esm_shims();
1187
1836
  function schemasTemplate(name, pascalName, camelName) {
1188
1837
  return `import { z } from 'zod';
1189
1838
 
@@ -1212,6 +1861,7 @@ export type ${pascalName}QueryInput = z.infer<typeof ${camelName}QuerySchema>;
1212
1861
  }
1213
1862
 
1214
1863
  // src/cli/templates/routes.ts
1864
+ init_esm_shims();
1215
1865
  function routesTemplate(name, pascalName, camelName, pluralName) {
1216
1866
  return `import type { FastifyInstance } from 'fastify';
1217
1867
  import type { ${pascalName}Controller } from './${name}.controller.js';
@@ -1264,6 +1914,7 @@ export function register${pascalName}Routes(
1264
1914
  }
1265
1915
 
1266
1916
  // src/cli/templates/module-index.ts
1917
+ init_esm_shims();
1267
1918
  function moduleIndexTemplate(name, pascalName, camelName) {
1268
1919
  return `import type { FastifyInstance } from 'fastify';
1269
1920
  import { logger } from '../../core/logger.js';
@@ -1299,6 +1950,7 @@ export * from './${name}.schemas.js';
1299
1950
  }
1300
1951
 
1301
1952
  // src/cli/templates/prisma-model.ts
1953
+ init_esm_shims();
1302
1954
  function prismaModelTemplate(name, pascalName, tableName) {
1303
1955
  return `
1304
1956
  // Add this model to your prisma/schema.prisma file
@@ -1318,6 +1970,7 @@ model ${pascalName} {
1318
1970
  }
1319
1971
 
1320
1972
  // src/cli/templates/dynamic-types.ts
1973
+ init_esm_shims();
1321
1974
  function dynamicTypesTemplate(name, pascalName, fields) {
1322
1975
  const fieldLines = fields.map((field) => {
1323
1976
  const tsType = tsTypeMap[field.type];
@@ -1362,6 +2015,7 @@ ${fields.filter((f) => ["string", "enum", "boolean"].includes(f.type)).map((f) =
1362
2015
  }
1363
2016
 
1364
2017
  // src/cli/templates/dynamic-schemas.ts
2018
+ init_esm_shims();
1365
2019
  function dynamicSchemasTemplate(name, pascalName, camelName, fields, validator = "zod") {
1366
2020
  switch (validator) {
1367
2021
  case "joi":
@@ -1540,6 +2194,7 @@ function getJsType(field) {
1540
2194
  }
1541
2195
 
1542
2196
  // src/cli/templates/dynamic-prisma.ts
2197
+ init_esm_shims();
1543
2198
  function dynamicPrismaTemplate(modelName, tableName, fields) {
1544
2199
  const fieldLines = [];
1545
2200
  for (const field of fields) {
@@ -1608,10 +2263,23 @@ ${indexLines.join("\n")}
1608
2263
  }
1609
2264
 
1610
2265
  // src/cli/commands/generate.ts
2266
+ function enableDryRunIfNeeded(options) {
2267
+ const dryRun = DryRunManager.getInstance();
2268
+ if (options.dryRun) {
2269
+ dryRun.enable();
2270
+ console.log(chalk5.yellow("\n\u26A0 DRY RUN MODE - No files will be written\n"));
2271
+ }
2272
+ }
2273
+ function showDryRunSummary(options) {
2274
+ if (options.dryRun) {
2275
+ DryRunManager.getInstance().printSummary();
2276
+ }
2277
+ }
1611
2278
  var generateCommand = new Command2("generate").alias("g").description("Generate resources (module, controller, service, etc.)");
1612
2279
  generateCommand.command("module <name> [fields...]").alias("m").description(
1613
2280
  "Generate a complete module with controller, service, repository, types, schemas, and routes"
1614
- ).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) => {
2281
+ ).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("--dry-run", "Preview changes without writing files").action(async (name, fieldsArgs, options) => {
2282
+ enableDryRunIfNeeded(options);
1615
2283
  let fields = [];
1616
2284
  if (options.interactive) {
1617
2285
  fields = await promptForFields();
@@ -1626,7 +2294,7 @@ generateCommand.command("module <name> [fields...]").alias("m").description(
1626
2294
  const pluralName = pluralize(kebabName);
1627
2295
  const tableName = pluralize(kebabName.replace(/-/g, "_"));
1628
2296
  const validatorType = options.validator || "zod";
1629
- const moduleDir = path3.join(getModulesDir(), kebabName);
2297
+ const moduleDir = path5.join(getModulesDir(), kebabName);
1630
2298
  if (await fileExists(moduleDir)) {
1631
2299
  spinner.stop();
1632
2300
  error(`Module "${kebabName}" already exists`);
@@ -1665,7 +2333,7 @@ generateCommand.command("module <name> [fields...]").alias("m").description(
1665
2333
  });
1666
2334
  }
1667
2335
  for (const file of files) {
1668
- await writeFile(path3.join(moduleDir, file.name), file.content);
2336
+ await writeFile(path5.join(moduleDir, file.name), file.content);
1669
2337
  }
1670
2338
  spinner.succeed(`Module "${pascalName}" generated successfully!`);
1671
2339
  if (options.prisma || hasFields) {
@@ -1703,42 +2371,46 @@ generateCommand.command("module <name> [fields...]").alias("m").description(
1703
2371
  info(` ${hasFields ? "3" : "4"}. Add the Prisma model to schema.prisma`);
1704
2372
  info(` ${hasFields ? "4" : "5"}. Run: npm run db:migrate`);
1705
2373
  }
2374
+ showDryRunSummary(options);
1706
2375
  } catch (err) {
1707
2376
  spinner.fail("Failed to generate module");
1708
2377
  error(err instanceof Error ? err.message : String(err));
1709
2378
  }
1710
2379
  });
1711
- generateCommand.command("controller <name>").alias("c").description("Generate a controller").option("-m, --module <module>", "Target module name").action(async (name, options) => {
2380
+ 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) => {
2381
+ enableDryRunIfNeeded(options);
1712
2382
  const spinner = ora2("Generating controller...").start();
1713
2383
  try {
1714
2384
  const kebabName = toKebabCase(name);
1715
2385
  const pascalName = toPascalCase(name);
1716
2386
  const camelName = toCamelCase(name);
1717
2387
  const moduleName = options.module ? toKebabCase(options.module) : kebabName;
1718
- const moduleDir = path3.join(getModulesDir(), moduleName);
1719
- const filePath = path3.join(moduleDir, `${kebabName}.controller.ts`);
2388
+ const moduleDir = path5.join(getModulesDir(), moduleName);
2389
+ const filePath = path5.join(moduleDir, `${kebabName}.controller.ts`);
1720
2390
  if (await fileExists(filePath)) {
1721
2391
  spinner.stop();
1722
- error(`Controller "${kebabName}" already exists`);
2392
+ displayError(ErrorTypes.FILE_ALREADY_EXISTS(`${kebabName}.controller.ts`));
1723
2393
  return;
1724
2394
  }
1725
2395
  await writeFile(filePath, controllerTemplate(kebabName, pascalName, camelName));
1726
2396
  spinner.succeed(`Controller "${pascalName}Controller" generated!`);
1727
2397
  success(` src/modules/${moduleName}/${kebabName}.controller.ts`);
2398
+ showDryRunSummary(options);
1728
2399
  } catch (err) {
1729
2400
  spinner.fail("Failed to generate controller");
1730
2401
  error(err instanceof Error ? err.message : String(err));
1731
2402
  }
1732
2403
  });
1733
- generateCommand.command("service <name>").alias("s").description("Generate a service").option("-m, --module <module>", "Target module name").action(async (name, options) => {
2404
+ 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) => {
2405
+ enableDryRunIfNeeded(options);
1734
2406
  const spinner = ora2("Generating service...").start();
1735
2407
  try {
1736
2408
  const kebabName = toKebabCase(name);
1737
2409
  const pascalName = toPascalCase(name);
1738
2410
  const camelName = toCamelCase(name);
1739
2411
  const moduleName = options.module ? toKebabCase(options.module) : kebabName;
1740
- const moduleDir = path3.join(getModulesDir(), moduleName);
1741
- const filePath = path3.join(moduleDir, `${kebabName}.service.ts`);
2412
+ const moduleDir = path5.join(getModulesDir(), moduleName);
2413
+ const filePath = path5.join(moduleDir, `${kebabName}.service.ts`);
1742
2414
  if (await fileExists(filePath)) {
1743
2415
  spinner.stop();
1744
2416
  error(`Service "${kebabName}" already exists`);
@@ -1747,12 +2419,14 @@ generateCommand.command("service <name>").alias("s").description("Generate a ser
1747
2419
  await writeFile(filePath, serviceTemplate(kebabName, pascalName, camelName));
1748
2420
  spinner.succeed(`Service "${pascalName}Service" generated!`);
1749
2421
  success(` src/modules/${moduleName}/${kebabName}.service.ts`);
2422
+ showDryRunSummary(options);
1750
2423
  } catch (err) {
1751
2424
  spinner.fail("Failed to generate service");
1752
2425
  error(err instanceof Error ? err.message : String(err));
1753
2426
  }
1754
2427
  });
1755
- generateCommand.command("repository <name>").alias("r").description("Generate a repository").option("-m, --module <module>", "Target module name").action(async (name, options) => {
2428
+ 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) => {
2429
+ enableDryRunIfNeeded(options);
1756
2430
  const spinner = ora2("Generating repository...").start();
1757
2431
  try {
1758
2432
  const kebabName = toKebabCase(name);
@@ -1760,8 +2434,8 @@ generateCommand.command("repository <name>").alias("r").description("Generate a
1760
2434
  const camelName = toCamelCase(name);
1761
2435
  const pluralName = pluralize(kebabName);
1762
2436
  const moduleName = options.module ? toKebabCase(options.module) : kebabName;
1763
- const moduleDir = path3.join(getModulesDir(), moduleName);
1764
- const filePath = path3.join(moduleDir, `${kebabName}.repository.ts`);
2437
+ const moduleDir = path5.join(getModulesDir(), moduleName);
2438
+ const filePath = path5.join(moduleDir, `${kebabName}.repository.ts`);
1765
2439
  if (await fileExists(filePath)) {
1766
2440
  spinner.stop();
1767
2441
  error(`Repository "${kebabName}" already exists`);
@@ -1770,19 +2444,21 @@ generateCommand.command("repository <name>").alias("r").description("Generate a
1770
2444
  await writeFile(filePath, repositoryTemplate(kebabName, pascalName, camelName, pluralName));
1771
2445
  spinner.succeed(`Repository "${pascalName}Repository" generated!`);
1772
2446
  success(` src/modules/${moduleName}/${kebabName}.repository.ts`);
2447
+ showDryRunSummary(options);
1773
2448
  } catch (err) {
1774
2449
  spinner.fail("Failed to generate repository");
1775
2450
  error(err instanceof Error ? err.message : String(err));
1776
2451
  }
1777
2452
  });
1778
- generateCommand.command("types <name>").alias("t").description("Generate types/interfaces").option("-m, --module <module>", "Target module name").action(async (name, options) => {
2453
+ 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) => {
2454
+ enableDryRunIfNeeded(options);
1779
2455
  const spinner = ora2("Generating types...").start();
1780
2456
  try {
1781
2457
  const kebabName = toKebabCase(name);
1782
2458
  const pascalName = toPascalCase(name);
1783
2459
  const moduleName = options.module ? toKebabCase(options.module) : kebabName;
1784
- const moduleDir = path3.join(getModulesDir(), moduleName);
1785
- const filePath = path3.join(moduleDir, `${kebabName}.types.ts`);
2460
+ const moduleDir = path5.join(getModulesDir(), moduleName);
2461
+ const filePath = path5.join(moduleDir, `${kebabName}.types.ts`);
1786
2462
  if (await fileExists(filePath)) {
1787
2463
  spinner.stop();
1788
2464
  error(`Types file "${kebabName}.types.ts" already exists`);
@@ -1791,20 +2467,22 @@ generateCommand.command("types <name>").alias("t").description("Generate types/i
1791
2467
  await writeFile(filePath, typesTemplate(kebabName, pascalName));
1792
2468
  spinner.succeed(`Types for "${pascalName}" generated!`);
1793
2469
  success(` src/modules/${moduleName}/${kebabName}.types.ts`);
2470
+ showDryRunSummary(options);
1794
2471
  } catch (err) {
1795
2472
  spinner.fail("Failed to generate types");
1796
2473
  error(err instanceof Error ? err.message : String(err));
1797
2474
  }
1798
2475
  });
1799
- generateCommand.command("schema <name>").alias("v").description("Generate validation schemas").option("-m, --module <module>", "Target module name").action(async (name, options) => {
2476
+ 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) => {
2477
+ enableDryRunIfNeeded(options);
1800
2478
  const spinner = ora2("Generating schemas...").start();
1801
2479
  try {
1802
2480
  const kebabName = toKebabCase(name);
1803
2481
  const pascalName = toPascalCase(name);
1804
2482
  const camelName = toCamelCase(name);
1805
2483
  const moduleName = options.module ? toKebabCase(options.module) : kebabName;
1806
- const moduleDir = path3.join(getModulesDir(), moduleName);
1807
- const filePath = path3.join(moduleDir, `${kebabName}.schemas.ts`);
2484
+ const moduleDir = path5.join(getModulesDir(), moduleName);
2485
+ const filePath = path5.join(moduleDir, `${kebabName}.schemas.ts`);
1808
2486
  if (await fileExists(filePath)) {
1809
2487
  spinner.stop();
1810
2488
  error(`Schemas file "${kebabName}.schemas.ts" already exists`);
@@ -1813,12 +2491,14 @@ generateCommand.command("schema <name>").alias("v").description("Generate valida
1813
2491
  await writeFile(filePath, schemasTemplate(kebabName, pascalName, camelName));
1814
2492
  spinner.succeed(`Schemas for "${pascalName}" generated!`);
1815
2493
  success(` src/modules/${moduleName}/${kebabName}.schemas.ts`);
2494
+ showDryRunSummary(options);
1816
2495
  } catch (err) {
1817
2496
  spinner.fail("Failed to generate schemas");
1818
2497
  error(err instanceof Error ? err.message : String(err));
1819
2498
  }
1820
2499
  });
1821
- generateCommand.command("routes <name>").description("Generate routes").option("-m, --module <module>", "Target module name").action(async (name, options) => {
2500
+ 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) => {
2501
+ enableDryRunIfNeeded(options);
1822
2502
  const spinner = ora2("Generating routes...").start();
1823
2503
  try {
1824
2504
  const kebabName = toKebabCase(name);
@@ -1826,8 +2506,8 @@ generateCommand.command("routes <name>").description("Generate routes").option("
1826
2506
  const camelName = toCamelCase(name);
1827
2507
  const pluralName = pluralize(kebabName);
1828
2508
  const moduleName = options.module ? toKebabCase(options.module) : kebabName;
1829
- const moduleDir = path3.join(getModulesDir(), moduleName);
1830
- const filePath = path3.join(moduleDir, `${kebabName}.routes.ts`);
2509
+ const moduleDir = path5.join(getModulesDir(), moduleName);
2510
+ const filePath = path5.join(moduleDir, `${kebabName}.routes.ts`);
1831
2511
  if (await fileExists(filePath)) {
1832
2512
  spinner.stop();
1833
2513
  error(`Routes file "${kebabName}.routes.ts" already exists`);
@@ -1836,6 +2516,7 @@ generateCommand.command("routes <name>").description("Generate routes").option("
1836
2516
  await writeFile(filePath, routesTemplate(kebabName, pascalName, camelName, pluralName));
1837
2517
  spinner.succeed(`Routes for "${pascalName}" generated!`);
1838
2518
  success(` src/modules/${moduleName}/${kebabName}.routes.ts`);
2519
+ showDryRunSummary(options);
1839
2520
  } catch (err) {
1840
2521
  spinner.fail("Failed to generate routes");
1841
2522
  error(err instanceof Error ? err.message : String(err));
@@ -1913,22 +2594,24 @@ async function promptForFields() {
1913
2594
  }
1914
2595
 
1915
2596
  // src/cli/commands/add-module.ts
2597
+ init_esm_shims();
1916
2598
  import { Command as Command3 } from "commander";
1917
- import path6 from "path";
2599
+ import path8 from "path";
1918
2600
  import ora3 from "ora";
1919
- import chalk4 from "chalk";
2601
+ import chalk7 from "chalk";
1920
2602
  import * as fs5 from "fs/promises";
1921
2603
 
1922
2604
  // src/cli/utils/env-manager.ts
2605
+ init_esm_shims();
1923
2606
  import * as fs3 from "fs/promises";
1924
- import * as path4 from "path";
2607
+ import * as path6 from "path";
1925
2608
  import { existsSync } from "fs";
1926
2609
  var EnvManager = class {
1927
2610
  envPath;
1928
2611
  envExamplePath;
1929
2612
  constructor(projectRoot) {
1930
- this.envPath = path4.join(projectRoot, ".env");
1931
- this.envExamplePath = path4.join(projectRoot, ".env.example");
2613
+ this.envPath = path6.join(projectRoot, ".env");
2614
+ this.envExamplePath = path6.join(projectRoot, ".env.example");
1932
2615
  }
1933
2616
  /**
1934
2617
  * Add environment variables to .env file
@@ -1936,12 +2619,12 @@ var EnvManager = class {
1936
2619
  async addVariables(sections) {
1937
2620
  const added = [];
1938
2621
  const skipped = [];
1939
- let created = false;
2622
+ let created2 = false;
1940
2623
  let envContent = "";
1941
2624
  if (existsSync(this.envPath)) {
1942
2625
  envContent = await fs3.readFile(this.envPath, "utf-8");
1943
2626
  } else {
1944
- created = true;
2627
+ created2 = true;
1945
2628
  }
1946
2629
  const existingKeys = this.parseExistingKeys(envContent);
1947
2630
  let newContent = envContent;
@@ -1972,7 +2655,7 @@ var EnvManager = class {
1972
2655
  if (existsSync(this.envExamplePath)) {
1973
2656
  await this.updateEnvExample(sections);
1974
2657
  }
1975
- return { added, skipped, created };
2658
+ return { added, skipped, created: created2 };
1976
2659
  }
1977
2660
  /**
1978
2661
  * Update .env.example file
@@ -2578,16 +3261,17 @@ var EnvManager = class {
2578
3261
  };
2579
3262
 
2580
3263
  // src/cli/utils/template-manager.ts
3264
+ init_esm_shims();
2581
3265
  import * as fs4 from "fs/promises";
2582
- import * as path5 from "path";
3266
+ import * as path7 from "path";
2583
3267
  import { createHash } from "crypto";
2584
3268
  import { existsSync as existsSync2 } from "fs";
2585
3269
  var TemplateManager = class {
2586
3270
  templatesDir;
2587
3271
  manifestsDir;
2588
3272
  constructor(projectRoot) {
2589
- this.templatesDir = path5.join(projectRoot, ".servcraft", "templates");
2590
- this.manifestsDir = path5.join(projectRoot, ".servcraft", "manifests");
3273
+ this.templatesDir = path7.join(projectRoot, ".servcraft", "templates");
3274
+ this.manifestsDir = path7.join(projectRoot, ".servcraft", "manifests");
2591
3275
  }
2592
3276
  /**
2593
3277
  * Initialize template system
@@ -2601,10 +3285,10 @@ var TemplateManager = class {
2601
3285
  */
2602
3286
  async saveTemplate(moduleName, files) {
2603
3287
  await this.initialize();
2604
- const moduleTemplateDir = path5.join(this.templatesDir, moduleName);
3288
+ const moduleTemplateDir = path7.join(this.templatesDir, moduleName);
2605
3289
  await fs4.mkdir(moduleTemplateDir, { recursive: true });
2606
3290
  for (const [fileName, content] of Object.entries(files)) {
2607
- const filePath = path5.join(moduleTemplateDir, fileName);
3291
+ const filePath = path7.join(moduleTemplateDir, fileName);
2608
3292
  await fs4.writeFile(filePath, content, "utf-8");
2609
3293
  }
2610
3294
  }
@@ -2613,7 +3297,7 @@ var TemplateManager = class {
2613
3297
  */
2614
3298
  async getTemplate(moduleName, fileName) {
2615
3299
  try {
2616
- const filePath = path5.join(this.templatesDir, moduleName, fileName);
3300
+ const filePath = path7.join(this.templatesDir, moduleName, fileName);
2617
3301
  return await fs4.readFile(filePath, "utf-8");
2618
3302
  } catch {
2619
3303
  return null;
@@ -2638,7 +3322,7 @@ var TemplateManager = class {
2638
3322
  installedAt: /* @__PURE__ */ new Date(),
2639
3323
  updatedAt: /* @__PURE__ */ new Date()
2640
3324
  };
2641
- const manifestPath = path5.join(this.manifestsDir, `${moduleName}.json`);
3325
+ const manifestPath = path7.join(this.manifestsDir, `${moduleName}.json`);
2642
3326
  await fs4.writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
2643
3327
  }
2644
3328
  /**
@@ -2646,7 +3330,7 @@ var TemplateManager = class {
2646
3330
  */
2647
3331
  async getManifest(moduleName) {
2648
3332
  try {
2649
- const manifestPath = path5.join(this.manifestsDir, `${moduleName}.json`);
3333
+ const manifestPath = path7.join(this.manifestsDir, `${moduleName}.json`);
2650
3334
  const content = await fs4.readFile(manifestPath, "utf-8");
2651
3335
  return JSON.parse(content);
2652
3336
  } catch {
@@ -2675,7 +3359,7 @@ var TemplateManager = class {
2675
3359
  }
2676
3360
  const results = [];
2677
3361
  for (const [fileName, fileInfo] of Object.entries(manifest.files)) {
2678
- const filePath = path5.join(moduleDir, fileName);
3362
+ const filePath = path7.join(moduleDir, fileName);
2679
3363
  if (!existsSync2(filePath)) {
2680
3364
  results.push({
2681
3365
  fileName,
@@ -2701,7 +3385,7 @@ var TemplateManager = class {
2701
3385
  */
2702
3386
  async createBackup(moduleName, moduleDir) {
2703
3387
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").substring(0, 19);
2704
- const backupDir = path5.join(path5.dirname(moduleDir), `${moduleName}.backup-${timestamp}`);
3388
+ const backupDir = path7.join(path7.dirname(moduleDir), `${moduleName}.backup-${timestamp}`);
2705
3389
  await this.copyDirectory(moduleDir, backupDir);
2706
3390
  return backupDir;
2707
3391
  }
@@ -2712,8 +3396,8 @@ var TemplateManager = class {
2712
3396
  await fs4.mkdir(dest, { recursive: true });
2713
3397
  const entries = await fs4.readdir(src, { withFileTypes: true });
2714
3398
  for (const entry of entries) {
2715
- const srcPath = path5.join(src, entry.name);
2716
- const destPath = path5.join(dest, entry.name);
3399
+ const srcPath = path7.join(src, entry.name);
3400
+ const destPath = path7.join(dest, entry.name);
2717
3401
  if (entry.isDirectory()) {
2718
3402
  await this.copyDirectory(srcPath, destPath);
2719
3403
  } else {
@@ -2814,23 +3498,24 @@ var TemplateManager = class {
2814
3498
  }
2815
3499
  manifest.files = fileHashes;
2816
3500
  manifest.updatedAt = /* @__PURE__ */ new Date();
2817
- const manifestPath = path5.join(this.manifestsDir, `${moduleName}.json`);
3501
+ const manifestPath = path7.join(this.manifestsDir, `${moduleName}.json`);
2818
3502
  await fs4.writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
2819
3503
  }
2820
3504
  };
2821
3505
 
2822
3506
  // src/cli/utils/interactive-prompt.ts
3507
+ init_esm_shims();
2823
3508
  import inquirer3 from "inquirer";
2824
- import chalk3 from "chalk";
3509
+ import chalk6 from "chalk";
2825
3510
  var InteractivePrompt = class {
2826
3511
  /**
2827
3512
  * Ask what to do when module already exists
2828
3513
  */
2829
3514
  static async askModuleExists(moduleName, hasModifications) {
2830
- console.log(chalk3.yellow(`
3515
+ console.log(chalk6.yellow(`
2831
3516
  \u26A0\uFE0F Module "${moduleName}" already exists`));
2832
3517
  if (hasModifications) {
2833
- console.log(chalk3.yellow(" Some files have been modified by you.\n"));
3518
+ console.log(chalk6.yellow(" Some files have been modified by you.\n"));
2834
3519
  }
2835
3520
  const { action } = await inquirer3.prompt([
2836
3521
  {
@@ -2868,12 +3553,12 @@ var InteractivePrompt = class {
2868
3553
  * Ask what to do with a specific file
2869
3554
  */
2870
3555
  static async askFileAction(fileName, isModified, yourLines, newLines) {
2871
- console.log(chalk3.cyan(`
3556
+ console.log(chalk6.cyan(`
2872
3557
  \u{1F4C1} ${fileName}`));
2873
3558
  console.log(
2874
- chalk3.gray(` Your version: ${yourLines} lines${isModified ? " (modified)" : ""}`)
3559
+ chalk6.gray(` Your version: ${yourLines} lines${isModified ? " (modified)" : ""}`)
2875
3560
  );
2876
- console.log(chalk3.gray(` New version: ${newLines} lines
3561
+ console.log(chalk6.gray(` New version: ${newLines} lines
2877
3562
  `));
2878
3563
  const { action } = await inquirer3.prompt([
2879
3564
  {
@@ -2925,7 +3610,7 @@ var InteractivePrompt = class {
2925
3610
  * Display diff and ask to continue
2926
3611
  */
2927
3612
  static async showDiffAndAsk(diff) {
2928
- console.log(chalk3.cyan("\n\u{1F4CA} Differences:\n"));
3613
+ console.log(chalk6.cyan("\n\u{1F4CA} Differences:\n"));
2929
3614
  console.log(diff);
2930
3615
  return await this.confirm("\nDo you want to proceed with this change?", true);
2931
3616
  }
@@ -2933,41 +3618,41 @@ var InteractivePrompt = class {
2933
3618
  * Display merge conflicts
2934
3619
  */
2935
3620
  static displayConflicts(conflicts) {
2936
- console.log(chalk3.red("\n\u26A0\uFE0F Merge Conflicts Detected:\n"));
3621
+ console.log(chalk6.red("\n\u26A0\uFE0F Merge Conflicts Detected:\n"));
2937
3622
  conflicts.forEach((conflict, i) => {
2938
- console.log(chalk3.yellow(` ${i + 1}. ${conflict}`));
3623
+ console.log(chalk6.yellow(` ${i + 1}. ${conflict}`));
2939
3624
  });
2940
- console.log(chalk3.gray("\n Conflict markers have been added to the file:"));
2941
- console.log(chalk3.gray(" <<<<<<< YOUR VERSION"));
2942
- console.log(chalk3.gray(" ... your code ..."));
2943
- console.log(chalk3.gray(" ======="));
2944
- console.log(chalk3.gray(" ... new code ..."));
2945
- console.log(chalk3.gray(" >>>>>>> NEW VERSION\n"));
3625
+ console.log(chalk6.gray("\n Conflict markers have been added to the file:"));
3626
+ console.log(chalk6.gray(" <<<<<<< YOUR VERSION"));
3627
+ console.log(chalk6.gray(" ... your code ..."));
3628
+ console.log(chalk6.gray(" ======="));
3629
+ console.log(chalk6.gray(" ... new code ..."));
3630
+ console.log(chalk6.gray(" >>>>>>> NEW VERSION\n"));
2946
3631
  }
2947
3632
  /**
2948
3633
  * Show backup location
2949
3634
  */
2950
3635
  static showBackupCreated(backupPath) {
2951
- console.log(chalk3.green(`
2952
- \u2713 Backup created: ${chalk3.cyan(backupPath)}`));
3636
+ console.log(chalk6.green(`
3637
+ \u2713 Backup created: ${chalk6.cyan(backupPath)}`));
2953
3638
  }
2954
3639
  /**
2955
3640
  * Show merge summary
2956
3641
  */
2957
3642
  static showMergeSummary(stats) {
2958
- console.log(chalk3.bold("\n\u{1F4CA} Merge Summary:\n"));
3643
+ console.log(chalk6.bold("\n\u{1F4CA} Merge Summary:\n"));
2959
3644
  if (stats.merged > 0) {
2960
- console.log(chalk3.green(` \u2713 Merged: ${stats.merged} file(s)`));
3645
+ console.log(chalk6.green(` \u2713 Merged: ${stats.merged} file(s)`));
2961
3646
  }
2962
3647
  if (stats.kept > 0) {
2963
- console.log(chalk3.blue(` \u2192 Kept: ${stats.kept} file(s)`));
3648
+ console.log(chalk6.blue(` \u2192 Kept: ${stats.kept} file(s)`));
2964
3649
  }
2965
3650
  if (stats.overwritten > 0) {
2966
- console.log(chalk3.yellow(` \u26A0 Overwritten: ${stats.overwritten} file(s)`));
3651
+ console.log(chalk6.yellow(` \u26A0 Overwritten: ${stats.overwritten} file(s)`));
2967
3652
  }
2968
3653
  if (stats.conflicts > 0) {
2969
- console.log(chalk3.red(` \u26A0 Conflicts: ${stats.conflicts} file(s)`));
2970
- console.log(chalk3.gray("\n Please resolve conflicts manually before committing.\n"));
3654
+ console.log(chalk6.red(` \u26A0 Conflicts: ${stats.conflicts} file(s)`));
3655
+ console.log(chalk6.gray("\n Please resolve conflicts manually before committing.\n"));
2971
3656
  }
2972
3657
  }
2973
3658
  /**
@@ -3005,6 +3690,7 @@ var InteractivePrompt = class {
3005
3690
  };
3006
3691
 
3007
3692
  // src/cli/commands/add-module.ts
3693
+ init_error_handler();
3008
3694
  var AVAILABLE_MODULES = {
3009
3695
  auth: {
3010
3696
  name: "Authentication",
@@ -3139,31 +3825,40 @@ var AVAILABLE_MODULES = {
3139
3825
  var addModuleCommand = new Command3("add").description("Add a pre-built module to your project").argument(
3140
3826
  "[module]",
3141
3827
  "Module to add (auth, users, email, audit, upload, cache, notifications, settings)"
3142
- ).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(
3828
+ ).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(
3143
3829
  async (moduleName, options) => {
3144
3830
  if (options?.list || !moduleName) {
3145
- console.log(chalk4.bold("\n\u{1F4E6} Available Modules:\n"));
3831
+ console.log(chalk7.bold("\n\u{1F4E6} Available Modules:\n"));
3146
3832
  for (const [key, mod] of Object.entries(AVAILABLE_MODULES)) {
3147
- console.log(` ${chalk4.cyan(key.padEnd(15))} ${mod.name}`);
3148
- console.log(` ${" ".repeat(15)} ${chalk4.gray(mod.description)}
3833
+ console.log(` ${chalk7.cyan(key.padEnd(15))} ${mod.name}`);
3834
+ console.log(` ${" ".repeat(15)} ${chalk7.gray(mod.description)}
3149
3835
  `);
3150
3836
  }
3151
- console.log(chalk4.bold("Usage:"));
3152
- console.log(` ${chalk4.yellow("servcraft add auth")} Add authentication module`);
3153
- console.log(` ${chalk4.yellow("servcraft add users")} Add user management module`);
3154
- console.log(` ${chalk4.yellow("servcraft add email")} Add email service module
3837
+ console.log(chalk7.bold("Usage:"));
3838
+ console.log(` ${chalk7.yellow("servcraft add auth")} Add authentication module`);
3839
+ console.log(` ${chalk7.yellow("servcraft add users")} Add user management module`);
3840
+ console.log(` ${chalk7.yellow("servcraft add email")} Add email service module
3155
3841
  `);
3156
3842
  return;
3157
3843
  }
3844
+ const dryRun = DryRunManager.getInstance();
3845
+ if (options?.dryRun) {
3846
+ dryRun.enable();
3847
+ console.log(chalk7.yellow("\n\u26A0 DRY RUN MODE - No files will be written\n"));
3848
+ }
3158
3849
  const module = AVAILABLE_MODULES[moduleName];
3159
3850
  if (!module) {
3160
- error(`Unknown module: ${moduleName}`);
3161
- info('Run "servcraft add --list" to see available modules');
3851
+ displayError(ErrorTypes.MODULE_NOT_FOUND(moduleName));
3852
+ return;
3853
+ }
3854
+ const projectError = validateProject();
3855
+ if (projectError) {
3856
+ displayError(projectError);
3162
3857
  return;
3163
3858
  }
3164
3859
  const spinner = ora3(`Adding ${module.name} module...`).start();
3165
3860
  try {
3166
- const moduleDir = path6.join(getModulesDir(), moduleName);
3861
+ const moduleDir = path8.join(getModulesDir(), moduleName);
3167
3862
  const templateManager = new TemplateManager(process.cwd());
3168
3863
  const moduleExists = await fileExists(moduleDir);
3169
3864
  if (moduleExists) {
@@ -3231,16 +3926,16 @@ var addModuleCommand = new Command3("add").description("Add a pre-built module t
3231
3926
  info("\n\u{1F4DD} Created new .env file");
3232
3927
  }
3233
3928
  if (result.added.length > 0) {
3234
- console.log(chalk4.bold("\n\u2705 Added to .env:"));
3929
+ console.log(chalk7.bold("\n\u2705 Added to .env:"));
3235
3930
  result.added.forEach((key) => success(` ${key}`));
3236
3931
  }
3237
3932
  if (result.skipped.length > 0) {
3238
- console.log(chalk4.bold("\n\u23ED\uFE0F Already in .env (skipped):"));
3933
+ console.log(chalk7.bold("\n\u23ED\uFE0F Already in .env (skipped):"));
3239
3934
  result.skipped.forEach((key) => info(` ${key}`));
3240
3935
  }
3241
3936
  const requiredVars = envSections.flatMap((section) => section.variables).filter((v) => v.required && !v.value).map((v) => v.key);
3242
3937
  if (requiredVars.length > 0) {
3243
- console.log(chalk4.bold("\n\u26A0\uFE0F Required configuration:"));
3938
+ console.log(chalk7.bold("\n\u26A0\uFE0F Required configuration:"));
3244
3939
  requiredVars.forEach((key) => warn(` ${key} - Please configure this variable`));
3245
3940
  }
3246
3941
  } catch (err) {
@@ -3248,10 +3943,15 @@ var addModuleCommand = new Command3("add").description("Add a pre-built module t
3248
3943
  error(err instanceof Error ? err.message : String(err));
3249
3944
  }
3250
3945
  }
3251
- console.log("\n\u{1F4CC} Next steps:");
3252
- info(" 1. Configure environment variables in .env (if needed)");
3253
- info(" 2. Register the module in your main app file");
3254
- info(" 3. Run database migrations if needed");
3946
+ if (!options?.dryRun) {
3947
+ console.log("\n\u{1F4CC} Next steps:");
3948
+ info(" 1. Configure environment variables in .env (if needed)");
3949
+ info(" 2. Register the module in your main app file");
3950
+ info(" 3. Run database migrations if needed");
3951
+ }
3952
+ if (options?.dryRun) {
3953
+ dryRun.printSummary();
3954
+ }
3255
3955
  } catch (err) {
3256
3956
  spinner.fail("Failed to add module");
3257
3957
  error(err instanceof Error ? err.message : String(err));
@@ -3302,7 +4002,7 @@ export * from './auth.schemas.js';
3302
4002
  `
3303
4003
  };
3304
4004
  for (const [name, content] of Object.entries(files)) {
3305
- await writeFile(path6.join(dir, name), content);
4005
+ await writeFile(path8.join(dir, name), content);
3306
4006
  }
3307
4007
  }
3308
4008
  async function generateUsersModule(dir) {
@@ -3342,7 +4042,7 @@ export * from './user.schemas.js';
3342
4042
  `
3343
4043
  };
3344
4044
  for (const [name, content] of Object.entries(files)) {
3345
- await writeFile(path6.join(dir, name), content);
4045
+ await writeFile(path8.join(dir, name), content);
3346
4046
  }
3347
4047
  }
3348
4048
  async function generateEmailModule(dir) {
@@ -3399,7 +4099,7 @@ export { EmailService, emailService } from './email.service.js';
3399
4099
  `
3400
4100
  };
3401
4101
  for (const [name, content] of Object.entries(files)) {
3402
- await writeFile(path6.join(dir, name), content);
4102
+ await writeFile(path8.join(dir, name), content);
3403
4103
  }
3404
4104
  }
3405
4105
  async function generateAuditModule(dir) {
@@ -3443,7 +4143,7 @@ export { AuditService, auditService } from './audit.service.js';
3443
4143
  `
3444
4144
  };
3445
4145
  for (const [name, content] of Object.entries(files)) {
3446
- await writeFile(path6.join(dir, name), content);
4146
+ await writeFile(path8.join(dir, name), content);
3447
4147
  }
3448
4148
  }
3449
4149
  async function generateUploadModule(dir) {
@@ -3469,7 +4169,7 @@ export interface UploadOptions {
3469
4169
  `
3470
4170
  };
3471
4171
  for (const [name, content] of Object.entries(files)) {
3472
- await writeFile(path6.join(dir, name), content);
4172
+ await writeFile(path8.join(dir, name), content);
3473
4173
  }
3474
4174
  }
3475
4175
  async function generateCacheModule(dir) {
@@ -3515,7 +4215,7 @@ export { CacheService, cacheService } from './cache.service.js';
3515
4215
  `
3516
4216
  };
3517
4217
  for (const [name, content] of Object.entries(files)) {
3518
- await writeFile(path6.join(dir, name), content);
4218
+ await writeFile(path8.join(dir, name), content);
3519
4219
  }
3520
4220
  }
3521
4221
  async function generateGenericModule(dir, name) {
@@ -3529,18 +4229,18 @@ export interface ${name.charAt(0).toUpperCase() + name.slice(1)}Data {
3529
4229
  `
3530
4230
  };
3531
4231
  for (const [fileName, content] of Object.entries(files)) {
3532
- await writeFile(path6.join(dir, fileName), content);
4232
+ await writeFile(path8.join(dir, fileName), content);
3533
4233
  }
3534
4234
  }
3535
4235
  async function findServercraftModules() {
3536
- const scriptDir = path6.dirname(new URL(import.meta.url).pathname);
4236
+ const scriptDir = path8.dirname(new URL(import.meta.url).pathname);
3537
4237
  const possiblePaths = [
3538
4238
  // Local node_modules (when servcraft is a dependency)
3539
- path6.join(process.cwd(), "node_modules", "servcraft", "src", "modules"),
4239
+ path8.join(process.cwd(), "node_modules", "servcraft", "src", "modules"),
3540
4240
  // From dist/cli/index.js -> src/modules (npx or global install)
3541
- path6.resolve(scriptDir, "..", "..", "src", "modules"),
4241
+ path8.resolve(scriptDir, "..", "..", "src", "modules"),
3542
4242
  // From src/cli/commands/add-module.ts -> src/modules (development)
3543
- path6.resolve(scriptDir, "..", "..", "modules")
4243
+ path8.resolve(scriptDir, "..", "..", "modules")
3544
4244
  ];
3545
4245
  for (const p of possiblePaths) {
3546
4246
  try {
@@ -3555,7 +4255,7 @@ async function findServercraftModules() {
3555
4255
  }
3556
4256
  async function generateModuleFiles(moduleName, moduleDir) {
3557
4257
  const moduleNameMap = {
3558
- "users": "user",
4258
+ users: "user",
3559
4259
  "rate-limit": "rate-limit",
3560
4260
  "feature-flag": "feature-flag",
3561
4261
  "api-versioning": "api-versioning",
@@ -3564,7 +4264,7 @@ async function generateModuleFiles(moduleName, moduleDir) {
3564
4264
  const sourceDirName = moduleNameMap[moduleName] || moduleName;
3565
4265
  const servercraftModulesDir = await findServercraftModules();
3566
4266
  if (servercraftModulesDir) {
3567
- const sourceModuleDir = path6.join(servercraftModulesDir, sourceDirName);
4267
+ const sourceModuleDir = path8.join(servercraftModulesDir, sourceDirName);
3568
4268
  if (await fileExists(sourceModuleDir)) {
3569
4269
  await copyModuleFromSource(sourceModuleDir, moduleDir);
3570
4270
  return;
@@ -3596,8 +4296,8 @@ async function generateModuleFiles(moduleName, moduleDir) {
3596
4296
  async function copyModuleFromSource(sourceDir, targetDir) {
3597
4297
  const entries = await fs5.readdir(sourceDir, { withFileTypes: true });
3598
4298
  for (const entry of entries) {
3599
- const sourcePath = path6.join(sourceDir, entry.name);
3600
- const targetPath = path6.join(targetDir, entry.name);
4299
+ const sourcePath = path8.join(sourceDir, entry.name);
4300
+ const targetPath = path8.join(targetDir, entry.name);
3601
4301
  if (entry.isDirectory()) {
3602
4302
  await fs5.mkdir(targetPath, { recursive: true });
3603
4303
  await copyModuleFromSource(sourcePath, targetPath);
@@ -3610,7 +4310,7 @@ async function getModuleFiles(moduleName, moduleDir) {
3610
4310
  const files = {};
3611
4311
  const entries = await fs5.readdir(moduleDir);
3612
4312
  for (const entry of entries) {
3613
- const filePath = path6.join(moduleDir, entry);
4313
+ const filePath = path8.join(moduleDir, entry);
3614
4314
  const stat2 = await fs5.stat(filePath);
3615
4315
  if (stat2.isFile() && entry.endsWith(".ts")) {
3616
4316
  const content = await fs5.readFile(filePath, "utf-8");
@@ -3621,14 +4321,14 @@ async function getModuleFiles(moduleName, moduleDir) {
3621
4321
  }
3622
4322
  async function showDiffForModule(templateManager, moduleName, moduleDir) {
3623
4323
  const modifiedFiles = await templateManager.getModifiedFiles(moduleName, moduleDir);
3624
- console.log(chalk4.cyan(`
4324
+ console.log(chalk7.cyan(`
3625
4325
  \u{1F4CA} Changes in module "${moduleName}":
3626
4326
  `));
3627
4327
  for (const file of modifiedFiles) {
3628
4328
  if (file.isModified) {
3629
- console.log(chalk4.yellow(`
4329
+ console.log(chalk7.yellow(`
3630
4330
  \u{1F4C4} ${file.fileName}:`));
3631
- const currentPath = path6.join(moduleDir, file.fileName);
4331
+ const currentPath = path8.join(moduleDir, file.fileName);
3632
4332
  const currentContent = await fs5.readFile(currentPath, "utf-8");
3633
4333
  const originalContent = await templateManager.getTemplate(moduleName, file.fileName);
3634
4334
  if (originalContent) {
@@ -3641,11 +4341,11 @@ async function showDiffForModule(templateManager, moduleName, moduleDir) {
3641
4341
  async function performSmartMerge(templateManager, moduleName, moduleDir, _displayName) {
3642
4342
  const spinner = ora3("Analyzing files for merge...").start();
3643
4343
  const newFiles = {};
3644
- const templateDir = path6.join(templateManager["templatesDir"], moduleName);
4344
+ const templateDir = path8.join(templateManager["templatesDir"], moduleName);
3645
4345
  try {
3646
4346
  const entries = await fs5.readdir(templateDir);
3647
4347
  for (const entry of entries) {
3648
- const content = await fs5.readFile(path6.join(templateDir, entry), "utf-8");
4348
+ const content = await fs5.readFile(path8.join(templateDir, entry), "utf-8");
3649
4349
  newFiles[entry] = content;
3650
4350
  }
3651
4351
  } catch {
@@ -3663,7 +4363,7 @@ async function performSmartMerge(templateManager, moduleName, moduleDir, _displa
3663
4363
  };
3664
4364
  for (const fileInfo of modifiedFiles) {
3665
4365
  const fileName = fileInfo.fileName;
3666
- const filePath = path6.join(moduleDir, fileName);
4366
+ const filePath = path8.join(moduleDir, fileName);
3667
4367
  const newContent = newFiles[fileName];
3668
4368
  if (!newContent) {
3669
4369
  continue;
@@ -3732,10 +4432,11 @@ async function performSmartMerge(templateManager, moduleName, moduleDir, _displa
3732
4432
  }
3733
4433
 
3734
4434
  // src/cli/commands/db.ts
4435
+ init_esm_shims();
3735
4436
  import { Command as Command4 } from "commander";
3736
4437
  import { execSync as execSync2, spawn } from "child_process";
3737
4438
  import ora4 from "ora";
3738
- import chalk5 from "chalk";
4439
+ import chalk8 from "chalk";
3739
4440
  var dbCommand = new Command4("db").description("Database management commands");
3740
4441
  dbCommand.command("migrate").description("Run database migrations").option("-n, --name <name>", "Migration name").action(async (options) => {
3741
4442
  const spinner = ora4("Running migrations...").start();
@@ -3792,7 +4493,7 @@ dbCommand.command("seed").description("Run database seed").action(async () => {
3792
4493
  });
3793
4494
  dbCommand.command("reset").description("Reset database (drop all data and re-run migrations)").option("-f, --force", "Skip confirmation").action(async (options) => {
3794
4495
  if (!options.force) {
3795
- console.log(chalk5.yellow("\n\u26A0\uFE0F WARNING: This will delete all data in your database!\n"));
4496
+ console.log(chalk8.yellow("\n\u26A0\uFE0F WARNING: This will delete all data in your database!\n"));
3796
4497
  const readline = await import("readline");
3797
4498
  const rl = readline.createInterface({
3798
4499
  input: process.stdin,
@@ -3824,12 +4525,2149 @@ dbCommand.command("status").description("Show migration status").action(async ()
3824
4525
  }
3825
4526
  });
3826
4527
 
4528
+ // src/cli/commands/docs.ts
4529
+ init_esm_shims();
4530
+ import { Command as Command5 } from "commander";
4531
+ import path10 from "path";
4532
+ import fs7 from "fs/promises";
4533
+ import ora6 from "ora";
4534
+ import chalk9 from "chalk";
4535
+
4536
+ // src/cli/utils/docs-generator.ts
4537
+ init_esm_shims();
4538
+ import fs6 from "fs/promises";
4539
+ import path9 from "path";
4540
+ import ora5 from "ora";
4541
+
4542
+ // src/core/server.ts
4543
+ init_esm_shims();
4544
+ import Fastify from "fastify";
4545
+
4546
+ // src/core/logger.ts
4547
+ init_esm_shims();
4548
+ import pino from "pino";
4549
+ var defaultConfig = {
4550
+ level: process.env.LOG_LEVEL || "info",
4551
+ pretty: process.env.NODE_ENV !== "production",
4552
+ name: "servcraft"
4553
+ };
4554
+ function createLogger(config2 = {}) {
4555
+ const mergedConfig = { ...defaultConfig, ...config2 };
4556
+ const transport = mergedConfig.pretty ? {
4557
+ target: "pino-pretty",
4558
+ options: {
4559
+ colorize: true,
4560
+ translateTime: "SYS:standard",
4561
+ ignore: "pid,hostname"
4562
+ }
4563
+ } : void 0;
4564
+ return pino({
4565
+ name: mergedConfig.name,
4566
+ level: mergedConfig.level,
4567
+ transport,
4568
+ formatters: {
4569
+ level: (label) => ({ level: label })
4570
+ },
4571
+ timestamp: pino.stdTimeFunctions.isoTime
4572
+ });
4573
+ }
4574
+ var logger = createLogger();
4575
+
4576
+ // src/core/server.ts
4577
+ var defaultConfig2 = {
4578
+ port: parseInt(process.env.PORT || "3000", 10),
4579
+ host: process.env.HOST || "0.0.0.0",
4580
+ trustProxy: true,
4581
+ bodyLimit: 1048576,
4582
+ // 1MB
4583
+ requestTimeout: 3e4
4584
+ // 30s
4585
+ };
4586
+ var Server = class {
4587
+ app;
4588
+ config;
4589
+ logger;
4590
+ isShuttingDown = false;
4591
+ constructor(config2 = {}) {
4592
+ this.config = { ...defaultConfig2, ...config2 };
4593
+ this.logger = this.config.logger || logger;
4594
+ const fastifyOptions = {
4595
+ logger: this.logger,
4596
+ trustProxy: this.config.trustProxy,
4597
+ bodyLimit: this.config.bodyLimit,
4598
+ requestTimeout: this.config.requestTimeout
4599
+ };
4600
+ this.app = Fastify(fastifyOptions);
4601
+ this.setupHealthCheck();
4602
+ this.setupGracefulShutdown();
4603
+ }
4604
+ get instance() {
4605
+ return this.app;
4606
+ }
4607
+ setupHealthCheck() {
4608
+ this.app.get("/health", async (_request, reply) => {
4609
+ const healthcheck = {
4610
+ status: "ok",
4611
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4612
+ uptime: process.uptime(),
4613
+ memory: process.memoryUsage(),
4614
+ version: process.env.npm_package_version || "0.1.0"
4615
+ };
4616
+ return reply.status(200).send(healthcheck);
4617
+ });
4618
+ this.app.get("/ready", async (_request, reply) => {
4619
+ if (this.isShuttingDown) {
4620
+ return reply.status(503).send({ status: "shutting_down" });
4621
+ }
4622
+ return reply.status(200).send({ status: "ready" });
4623
+ });
4624
+ }
4625
+ setupGracefulShutdown() {
4626
+ const signals = ["SIGINT", "SIGTERM", "SIGQUIT"];
4627
+ signals.forEach((signal) => {
4628
+ process.on(signal, async () => {
4629
+ this.logger.info(`Received ${signal}, starting graceful shutdown...`);
4630
+ await this.shutdown();
4631
+ });
4632
+ });
4633
+ process.on("uncaughtException", async (error2) => {
4634
+ this.logger.error({ err: error2 }, "Uncaught exception");
4635
+ await this.shutdown(1);
4636
+ });
4637
+ process.on("unhandledRejection", async (reason) => {
4638
+ this.logger.error({ err: reason }, "Unhandled rejection");
4639
+ await this.shutdown(1);
4640
+ });
4641
+ }
4642
+ async shutdown(exitCode = 0) {
4643
+ if (this.isShuttingDown) {
4644
+ return;
4645
+ }
4646
+ this.isShuttingDown = true;
4647
+ this.logger.info("Graceful shutdown initiated...");
4648
+ const shutdownTimeout = setTimeout(() => {
4649
+ this.logger.error("Graceful shutdown timeout, forcing exit");
4650
+ process.exit(1);
4651
+ }, 3e4);
4652
+ try {
4653
+ await this.app.close();
4654
+ this.logger.info("Server closed successfully");
4655
+ clearTimeout(shutdownTimeout);
4656
+ process.exit(exitCode);
4657
+ } catch (error2) {
4658
+ this.logger.error({ err: error2 }, "Error during shutdown");
4659
+ clearTimeout(shutdownTimeout);
4660
+ process.exit(1);
4661
+ }
4662
+ }
4663
+ async start() {
4664
+ try {
4665
+ await this.app.listen({
4666
+ port: this.config.port,
4667
+ host: this.config.host
4668
+ });
4669
+ this.logger.info(`Server listening on ${this.config.host}:${this.config.port}`);
4670
+ } catch (error2) {
4671
+ this.logger.error({ err: error2 }, "Failed to start server");
4672
+ throw error2;
4673
+ }
4674
+ }
4675
+ };
4676
+ function createServer(config2 = {}) {
4677
+ return new Server(config2);
4678
+ }
4679
+
4680
+ // src/middleware/index.ts
4681
+ init_esm_shims();
4682
+
4683
+ // src/middleware/error-handler.ts
4684
+ init_esm_shims();
4685
+
4686
+ // src/utils/errors.ts
4687
+ init_esm_shims();
4688
+ var AppError = class _AppError extends Error {
4689
+ statusCode;
4690
+ isOperational;
4691
+ errors;
4692
+ constructor(message, statusCode = 500, isOperational = true, errors) {
4693
+ super(message);
4694
+ this.statusCode = statusCode;
4695
+ this.isOperational = isOperational;
4696
+ this.errors = errors;
4697
+ Object.setPrototypeOf(this, _AppError.prototype);
4698
+ Error.captureStackTrace(this, this.constructor);
4699
+ }
4700
+ };
4701
+ var NotFoundError = class _NotFoundError extends AppError {
4702
+ constructor(resource = "Resource") {
4703
+ super(`${resource} not found`, 404);
4704
+ Object.setPrototypeOf(this, _NotFoundError.prototype);
4705
+ }
4706
+ };
4707
+ var UnauthorizedError = class _UnauthorizedError extends AppError {
4708
+ constructor(message = "Unauthorized") {
4709
+ super(message, 401);
4710
+ Object.setPrototypeOf(this, _UnauthorizedError.prototype);
4711
+ }
4712
+ };
4713
+ var ForbiddenError = class _ForbiddenError extends AppError {
4714
+ constructor(message = "Forbidden") {
4715
+ super(message, 403);
4716
+ Object.setPrototypeOf(this, _ForbiddenError.prototype);
4717
+ }
4718
+ };
4719
+ var BadRequestError = class _BadRequestError extends AppError {
4720
+ constructor(message = "Bad request", errors) {
4721
+ super(message, 400, true, errors);
4722
+ Object.setPrototypeOf(this, _BadRequestError.prototype);
4723
+ }
4724
+ };
4725
+ var ConflictError = class _ConflictError extends AppError {
4726
+ constructor(message = "Resource already exists") {
4727
+ super(message, 409);
4728
+ Object.setPrototypeOf(this, _ConflictError.prototype);
4729
+ }
4730
+ };
4731
+ var ValidationError = class _ValidationError extends AppError {
4732
+ constructor(errors) {
4733
+ super("Validation failed", 422, true, errors);
4734
+ Object.setPrototypeOf(this, _ValidationError.prototype);
4735
+ }
4736
+ };
4737
+ function isAppError(error2) {
4738
+ return error2 instanceof AppError;
4739
+ }
4740
+
4741
+ // src/config/index.ts
4742
+ init_esm_shims();
4743
+
4744
+ // src/config/env.ts
4745
+ init_esm_shims();
4746
+ import { z } from "zod";
4747
+ import dotenv from "dotenv";
4748
+ dotenv.config();
4749
+ var envSchema = z.object({
4750
+ // Server
4751
+ NODE_ENV: z.enum(["development", "staging", "production", "test"]).default("development"),
4752
+ PORT: z.string().transform(Number).default("3000"),
4753
+ HOST: z.string().default("0.0.0.0"),
4754
+ // Database
4755
+ DATABASE_URL: z.string().optional(),
4756
+ // JWT
4757
+ JWT_SECRET: z.string().min(32).optional(),
4758
+ JWT_ACCESS_EXPIRES_IN: z.string().default("15m"),
4759
+ JWT_REFRESH_EXPIRES_IN: z.string().default("7d"),
4760
+ // Security
4761
+ CORS_ORIGIN: z.string().default("*"),
4762
+ RATE_LIMIT_MAX: z.string().transform(Number).default("100"),
4763
+ RATE_LIMIT_WINDOW_MS: z.string().transform(Number).default("60000"),
4764
+ // Email
4765
+ SMTP_HOST: z.string().optional(),
4766
+ SMTP_PORT: z.string().transform(Number).optional(),
4767
+ SMTP_USER: z.string().optional(),
4768
+ SMTP_PASS: z.string().optional(),
4769
+ SMTP_FROM: z.string().optional(),
4770
+ // Redis (optional)
4771
+ REDIS_URL: z.string().optional(),
4772
+ // Logging
4773
+ LOG_LEVEL: z.enum(["fatal", "error", "warn", "info", "debug", "trace"]).default("info")
4774
+ });
4775
+ function validateEnv() {
4776
+ const parsed = envSchema.safeParse(process.env);
4777
+ if (!parsed.success) {
4778
+ logger.error({ errors: parsed.error.flatten().fieldErrors }, "Invalid environment variables");
4779
+ throw new Error("Invalid environment variables");
4780
+ }
4781
+ return parsed.data;
4782
+ }
4783
+ var env = validateEnv();
4784
+ function isProduction() {
4785
+ return env.NODE_ENV === "production";
4786
+ }
4787
+
4788
+ // src/config/index.ts
4789
+ function parseCorsOrigin(origin) {
4790
+ if (origin === "*") return "*";
4791
+ if (origin.includes(",")) {
4792
+ return origin.split(",").map((o) => o.trim());
4793
+ }
4794
+ return origin;
4795
+ }
4796
+ function createConfig() {
4797
+ return {
4798
+ env,
4799
+ server: {
4800
+ port: env.PORT,
4801
+ host: env.HOST
4802
+ },
4803
+ jwt: {
4804
+ secret: env.JWT_SECRET || "change-me-in-production-please-32chars",
4805
+ accessExpiresIn: env.JWT_ACCESS_EXPIRES_IN,
4806
+ refreshExpiresIn: env.JWT_REFRESH_EXPIRES_IN
4807
+ },
4808
+ security: {
4809
+ corsOrigin: parseCorsOrigin(env.CORS_ORIGIN),
4810
+ rateLimit: {
4811
+ max: env.RATE_LIMIT_MAX,
4812
+ windowMs: env.RATE_LIMIT_WINDOW_MS
4813
+ }
4814
+ },
4815
+ email: {
4816
+ host: env.SMTP_HOST,
4817
+ port: env.SMTP_PORT,
4818
+ user: env.SMTP_USER,
4819
+ pass: env.SMTP_PASS,
4820
+ from: env.SMTP_FROM
4821
+ },
4822
+ database: {
4823
+ url: env.DATABASE_URL
4824
+ },
4825
+ redis: {
4826
+ url: env.REDIS_URL
4827
+ }
4828
+ };
4829
+ }
4830
+ var config = createConfig();
4831
+
4832
+ // src/middleware/error-handler.ts
4833
+ function registerErrorHandler(app) {
4834
+ app.setErrorHandler(
4835
+ (error2, request, reply) => {
4836
+ logger.error(
4837
+ {
4838
+ err: error2,
4839
+ requestId: request.id,
4840
+ method: request.method,
4841
+ url: request.url
4842
+ },
4843
+ "Request error"
4844
+ );
4845
+ if (isAppError(error2)) {
4846
+ return reply.status(error2.statusCode).send({
4847
+ success: false,
4848
+ message: error2.message,
4849
+ errors: error2.errors,
4850
+ ...isProduction() ? {} : { stack: error2.stack }
4851
+ });
4852
+ }
4853
+ if ("validation" in error2 && error2.validation) {
4854
+ const errors = {};
4855
+ for (const err of error2.validation) {
4856
+ const field = err.instancePath?.replace("/", "") || "body";
4857
+ if (!errors[field]) {
4858
+ errors[field] = [];
4859
+ }
4860
+ errors[field].push(err.message || "Invalid value");
4861
+ }
4862
+ return reply.status(400).send({
4863
+ success: false,
4864
+ message: "Validation failed",
4865
+ errors
4866
+ });
4867
+ }
4868
+ if ("statusCode" in error2 && typeof error2.statusCode === "number") {
4869
+ return reply.status(error2.statusCode).send({
4870
+ success: false,
4871
+ message: error2.message,
4872
+ ...isProduction() ? {} : { stack: error2.stack }
4873
+ });
4874
+ }
4875
+ return reply.status(500).send({
4876
+ success: false,
4877
+ message: isProduction() ? "Internal server error" : error2.message,
4878
+ ...isProduction() ? {} : { stack: error2.stack }
4879
+ });
4880
+ }
4881
+ );
4882
+ app.setNotFoundHandler((request, reply) => {
4883
+ return reply.status(404).send({
4884
+ success: false,
4885
+ message: `Route ${request.method} ${request.url} not found`
4886
+ });
4887
+ });
4888
+ }
4889
+
4890
+ // src/middleware/security.ts
4891
+ init_esm_shims();
4892
+ import helmet from "@fastify/helmet";
4893
+ import cors from "@fastify/cors";
4894
+ import rateLimit from "@fastify/rate-limit";
4895
+ var defaultOptions = {
4896
+ helmet: true,
4897
+ cors: true,
4898
+ rateLimit: true
4899
+ };
4900
+ async function registerSecurity(app, options = {}) {
4901
+ const opts = { ...defaultOptions, ...options };
4902
+ if (opts.helmet) {
4903
+ await app.register(helmet, {
4904
+ contentSecurityPolicy: {
4905
+ directives: {
4906
+ defaultSrc: ["'self'"],
4907
+ styleSrc: ["'self'", "'unsafe-inline'"],
4908
+ scriptSrc: ["'self'"],
4909
+ imgSrc: ["'self'", "data:", "https:"]
4910
+ }
4911
+ },
4912
+ crossOriginEmbedderPolicy: false
4913
+ });
4914
+ logger.debug("Helmet security headers enabled");
4915
+ }
4916
+ if (opts.cors) {
4917
+ await app.register(cors, {
4918
+ origin: config.security.corsOrigin,
4919
+ credentials: true,
4920
+ methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
4921
+ allowedHeaders: ["Content-Type", "Authorization", "X-Requested-With"],
4922
+ exposedHeaders: ["X-Total-Count", "X-Page", "X-Limit"],
4923
+ maxAge: 86400
4924
+ // 24 hours
4925
+ });
4926
+ logger.debug({ origin: config.security.corsOrigin }, "CORS enabled");
4927
+ }
4928
+ if (opts.rateLimit) {
4929
+ await app.register(rateLimit, {
4930
+ max: config.security.rateLimit.max,
4931
+ timeWindow: config.security.rateLimit.windowMs,
4932
+ errorResponseBuilder: (_request, context) => ({
4933
+ success: false,
4934
+ message: "Too many requests, please try again later",
4935
+ retryAfter: context.after
4936
+ }),
4937
+ keyGenerator: (request) => {
4938
+ return request.headers["x-forwarded-for"]?.toString().split(",")[0] || request.ip || "unknown";
4939
+ }
4940
+ });
4941
+ logger.debug(
4942
+ {
4943
+ max: config.security.rateLimit.max,
4944
+ windowMs: config.security.rateLimit.windowMs
4945
+ },
4946
+ "Rate limiting enabled"
4947
+ );
4948
+ }
4949
+ }
4950
+
4951
+ // src/modules/swagger/index.ts
4952
+ init_esm_shims();
4953
+
4954
+ // src/modules/swagger/swagger.service.ts
4955
+ init_esm_shims();
4956
+ import swagger from "@fastify/swagger";
4957
+ import swaggerUi from "@fastify/swagger-ui";
4958
+ var defaultConfig3 = {
4959
+ title: "Servcraft API",
4960
+ description: "API documentation generated by Servcraft",
4961
+ version: "1.0.0",
4962
+ tags: [
4963
+ { name: "Auth", description: "Authentication endpoints" },
4964
+ { name: "Users", description: "User management endpoints" },
4965
+ { name: "Health", description: "Health check endpoints" }
4966
+ ]
4967
+ };
4968
+ async function registerSwagger(app, customConfig) {
4969
+ const swaggerConfig = { ...defaultConfig3, ...customConfig };
4970
+ await app.register(swagger, {
4971
+ openapi: {
4972
+ openapi: "3.0.3",
4973
+ info: {
4974
+ title: swaggerConfig.title,
4975
+ description: swaggerConfig.description,
4976
+ version: swaggerConfig.version,
4977
+ contact: swaggerConfig.contact,
4978
+ license: swaggerConfig.license
4979
+ },
4980
+ servers: swaggerConfig.servers || [
4981
+ {
4982
+ url: `http://localhost:${config.server.port}`,
4983
+ description: "Development server"
4984
+ }
4985
+ ],
4986
+ tags: swaggerConfig.tags,
4987
+ components: {
4988
+ securitySchemes: {
4989
+ bearerAuth: {
4990
+ type: "http",
4991
+ scheme: "bearer",
4992
+ bearerFormat: "JWT",
4993
+ description: "Enter your JWT token"
4994
+ }
4995
+ }
4996
+ }
4997
+ }
4998
+ });
4999
+ await app.register(swaggerUi, {
5000
+ routePrefix: "/docs",
5001
+ uiConfig: {
5002
+ docExpansion: "list",
5003
+ deepLinking: true,
5004
+ displayRequestDuration: true,
5005
+ filter: true,
5006
+ showExtensions: true,
5007
+ showCommonExtensions: true
5008
+ },
5009
+ staticCSP: true,
5010
+ transformStaticCSP: (header) => header
5011
+ });
5012
+ logger.info("Swagger documentation registered at /docs");
5013
+ }
5014
+
5015
+ // src/modules/swagger/schema-builder.ts
5016
+ init_esm_shims();
5017
+
5018
+ // src/modules/auth/index.ts
5019
+ init_esm_shims();
5020
+ import jwt from "@fastify/jwt";
5021
+ import cookie from "@fastify/cookie";
5022
+
5023
+ // src/modules/auth/auth.service.ts
5024
+ init_esm_shims();
5025
+ import bcrypt from "bcryptjs";
5026
+ import { Redis } from "ioredis";
5027
+ var AuthService = class {
5028
+ app;
5029
+ SALT_ROUNDS = 12;
5030
+ redis = null;
5031
+ BLACKLIST_PREFIX = "auth:blacklist:";
5032
+ BLACKLIST_TTL = 7 * 24 * 60 * 60;
5033
+ // 7 days in seconds
5034
+ constructor(app, redisUrl) {
5035
+ this.app = app;
5036
+ if (redisUrl || process.env.REDIS_URL) {
5037
+ try {
5038
+ this.redis = new Redis(redisUrl || process.env.REDIS_URL || "redis://localhost:6379");
5039
+ this.redis.on("connect", () => {
5040
+ logger.info("Auth service connected to Redis for token blacklist");
5041
+ });
5042
+ this.redis.on("error", (error2) => {
5043
+ logger.error({ err: error2 }, "Redis connection error in Auth service");
5044
+ });
5045
+ } catch (error2) {
5046
+ logger.warn({ err: error2 }, "Failed to connect to Redis, using in-memory blacklist");
5047
+ this.redis = null;
5048
+ }
5049
+ } else {
5050
+ logger.warn(
5051
+ "No REDIS_URL provided, using in-memory token blacklist (not recommended for production)"
5052
+ );
5053
+ }
5054
+ }
5055
+ async hashPassword(password) {
5056
+ return bcrypt.hash(password, this.SALT_ROUNDS);
5057
+ }
5058
+ async verifyPassword(password, hash) {
5059
+ return bcrypt.compare(password, hash);
5060
+ }
5061
+ generateTokenPair(user) {
5062
+ const accessPayload = {
5063
+ sub: user.id,
5064
+ email: user.email,
5065
+ role: user.role,
5066
+ type: "access"
5067
+ };
5068
+ const refreshPayload = {
5069
+ sub: user.id,
5070
+ email: user.email,
5071
+ role: user.role,
5072
+ type: "refresh"
5073
+ };
5074
+ const accessToken = this.app.jwt.sign(accessPayload, {
5075
+ expiresIn: config.jwt.accessExpiresIn
5076
+ });
5077
+ const refreshToken = this.app.jwt.sign(refreshPayload, {
5078
+ expiresIn: config.jwt.refreshExpiresIn
5079
+ });
5080
+ const expiresIn = this.parseExpiration(config.jwt.accessExpiresIn);
5081
+ return { accessToken, refreshToken, expiresIn };
5082
+ }
5083
+ parseExpiration(expiration) {
5084
+ const match = expiration.match(/^(\d+)([smhd])$/);
5085
+ if (!match) return 900;
5086
+ const value = parseInt(match[1] || "0", 10);
5087
+ const unit = match[2];
5088
+ switch (unit) {
5089
+ case "s":
5090
+ return value;
5091
+ case "m":
5092
+ return value * 60;
5093
+ case "h":
5094
+ return value * 3600;
5095
+ case "d":
5096
+ return value * 86400;
5097
+ default:
5098
+ return 900;
5099
+ }
5100
+ }
5101
+ async verifyAccessToken(token) {
5102
+ try {
5103
+ if (await this.isTokenBlacklisted(token)) {
5104
+ throw new UnauthorizedError("Token has been revoked");
5105
+ }
5106
+ const payload = this.app.jwt.verify(token);
5107
+ if (payload.type !== "access") {
5108
+ throw new UnauthorizedError("Invalid token type");
5109
+ }
5110
+ return payload;
5111
+ } catch (error2) {
5112
+ if (error2 instanceof UnauthorizedError) throw error2;
5113
+ logger.debug({ err: error2 }, "Token verification failed");
5114
+ throw new UnauthorizedError("Invalid or expired token");
5115
+ }
5116
+ }
5117
+ async verifyRefreshToken(token) {
5118
+ try {
5119
+ if (await this.isTokenBlacklisted(token)) {
5120
+ throw new UnauthorizedError("Token has been revoked");
5121
+ }
5122
+ const payload = this.app.jwt.verify(token);
5123
+ if (payload.type !== "refresh") {
5124
+ throw new UnauthorizedError("Invalid token type");
5125
+ }
5126
+ return payload;
5127
+ } catch (error2) {
5128
+ if (error2 instanceof UnauthorizedError) throw error2;
5129
+ logger.debug({ err: error2 }, "Refresh token verification failed");
5130
+ throw new UnauthorizedError("Invalid or expired refresh token");
5131
+ }
5132
+ }
5133
+ /**
5134
+ * Blacklist a token (JWT revocation)
5135
+ * Uses Redis if available, falls back to in-memory Set
5136
+ */
5137
+ async blacklistToken(token) {
5138
+ if (this.redis) {
5139
+ try {
5140
+ const key = `${this.BLACKLIST_PREFIX}${token}`;
5141
+ await this.redis.setex(key, this.BLACKLIST_TTL, "1");
5142
+ logger.debug("Token blacklisted in Redis");
5143
+ } catch (error2) {
5144
+ logger.error({ err: error2 }, "Failed to blacklist token in Redis");
5145
+ throw new Error("Failed to revoke token");
5146
+ }
5147
+ } else {
5148
+ logger.warn("Using in-memory blacklist - not suitable for multi-instance deployments");
5149
+ }
5150
+ }
5151
+ /**
5152
+ * Check if a token is blacklisted
5153
+ * Uses Redis if available, falls back to always returning false
5154
+ */
5155
+ async isTokenBlacklisted(token) {
5156
+ if (this.redis) {
5157
+ try {
5158
+ const key = `${this.BLACKLIST_PREFIX}${token}`;
5159
+ const result = await this.redis.exists(key);
5160
+ return result === 1;
5161
+ } catch (error2) {
5162
+ logger.error({ err: error2 }, "Failed to check token blacklist in Redis");
5163
+ return false;
5164
+ }
5165
+ }
5166
+ return false;
5167
+ }
5168
+ /**
5169
+ * Get count of blacklisted tokens (Redis only)
5170
+ */
5171
+ async getBlacklistCount() {
5172
+ if (this.redis) {
5173
+ try {
5174
+ const keys = await this.redis.keys(`${this.BLACKLIST_PREFIX}*`);
5175
+ return keys.length;
5176
+ } catch (error2) {
5177
+ logger.error({ err: error2 }, "Failed to get blacklist count from Redis");
5178
+ return 0;
5179
+ }
5180
+ }
5181
+ return 0;
5182
+ }
5183
+ /**
5184
+ * Close Redis connection
5185
+ */
5186
+ async close() {
5187
+ if (this.redis) {
5188
+ await this.redis.quit();
5189
+ logger.info("Auth service Redis connection closed");
5190
+ }
5191
+ }
5192
+ // OAuth support methods - to be implemented with user repository
5193
+ async findUserByEmail(_email) {
5194
+ return null;
5195
+ }
5196
+ async createUserFromOAuth(data) {
5197
+ const user = {
5198
+ id: `oauth_${Date.now()}`,
5199
+ email: data.email,
5200
+ role: "user"
5201
+ };
5202
+ logger.info({ email: data.email }, "Created user from OAuth");
5203
+ return user;
5204
+ }
5205
+ async generateTokensForUser(userId) {
5206
+ const user = {
5207
+ id: userId,
5208
+ email: "",
5209
+ // Would be fetched from database in production
5210
+ role: "user"
5211
+ };
5212
+ return this.generateTokenPair(user);
5213
+ }
5214
+ async verifyPasswordById(userId, _password) {
5215
+ logger.debug({ userId }, "Password verification requested");
5216
+ return false;
5217
+ }
5218
+ };
5219
+ function createAuthService(app) {
5220
+ return new AuthService(app);
5221
+ }
5222
+
5223
+ // src/modules/auth/auth.controller.ts
5224
+ init_esm_shims();
5225
+
5226
+ // src/modules/auth/schemas.ts
5227
+ init_esm_shims();
5228
+ import { z as z2 } from "zod";
5229
+ var loginSchema = z2.object({
5230
+ email: z2.string().email("Invalid email address"),
5231
+ password: z2.string().min(1, "Password is required")
5232
+ });
5233
+ var registerSchema = z2.object({
5234
+ email: z2.string().email("Invalid email address"),
5235
+ password: z2.string().min(8, "Password must be at least 8 characters").regex(/[A-Z]/, "Password must contain at least one uppercase letter").regex(/[a-z]/, "Password must contain at least one lowercase letter").regex(/[0-9]/, "Password must contain at least one number"),
5236
+ name: z2.string().min(2, "Name must be at least 2 characters").optional()
5237
+ });
5238
+ var refreshTokenSchema = z2.object({
5239
+ refreshToken: z2.string().min(1, "Refresh token is required")
5240
+ });
5241
+ var passwordResetRequestSchema = z2.object({
5242
+ email: z2.string().email("Invalid email address")
5243
+ });
5244
+ var passwordResetConfirmSchema = z2.object({
5245
+ token: z2.string().min(1, "Token is required"),
5246
+ password: z2.string().min(8, "Password must be at least 8 characters").regex(/[A-Z]/, "Password must contain at least one uppercase letter").regex(/[a-z]/, "Password must contain at least one lowercase letter").regex(/[0-9]/, "Password must contain at least one number")
5247
+ });
5248
+ var changePasswordSchema = z2.object({
5249
+ currentPassword: z2.string().min(1, "Current password is required"),
5250
+ newPassword: z2.string().min(8, "Password must be at least 8 characters").regex(/[A-Z]/, "Password must contain at least one uppercase letter").regex(/[a-z]/, "Password must contain at least one lowercase letter").regex(/[0-9]/, "Password must contain at least one number")
5251
+ });
5252
+
5253
+ // src/utils/response.ts
5254
+ init_esm_shims();
5255
+ function success2(reply, data, statusCode = 200) {
5256
+ const response = {
5257
+ success: true,
5258
+ data
5259
+ };
5260
+ return reply.status(statusCode).send(response);
5261
+ }
5262
+ function created(reply, data) {
5263
+ return success2(reply, data, 201);
5264
+ }
5265
+ function noContent(reply) {
5266
+ return reply.status(204).send();
5267
+ }
5268
+
5269
+ // src/modules/validation/validator.ts
5270
+ init_esm_shims();
5271
+ import { z as z3 } from "zod";
5272
+ function validateBody(schema, data) {
5273
+ const result = schema.safeParse(data);
5274
+ if (!result.success) {
5275
+ throw new ValidationError(formatZodErrors(result.error));
5276
+ }
5277
+ return result.data;
5278
+ }
5279
+ function validateQuery(schema, data) {
5280
+ const result = schema.safeParse(data);
5281
+ if (!result.success) {
5282
+ throw new ValidationError(formatZodErrors(result.error));
5283
+ }
5284
+ return result.data;
5285
+ }
5286
+ function formatZodErrors(error2) {
5287
+ const errors = {};
5288
+ for (const issue of error2.issues) {
5289
+ const path12 = issue.path.join(".") || "root";
5290
+ if (!errors[path12]) {
5291
+ errors[path12] = [];
5292
+ }
5293
+ errors[path12].push(issue.message);
5294
+ }
5295
+ return errors;
5296
+ }
5297
+ var idParamSchema = z3.object({
5298
+ id: z3.string().uuid("Invalid ID format")
5299
+ });
5300
+ var paginationSchema = z3.object({
5301
+ page: z3.string().transform(Number).optional().default("1"),
5302
+ limit: z3.string().transform(Number).optional().default("20"),
5303
+ sortBy: z3.string().optional(),
5304
+ sortOrder: z3.enum(["asc", "desc"]).optional().default("asc")
5305
+ });
5306
+ var searchSchema = z3.object({
5307
+ q: z3.string().min(1, "Search query is required").optional(),
5308
+ search: z3.string().min(1).optional()
5309
+ });
5310
+ var emailSchema = z3.string().email("Invalid email address");
5311
+ var passwordSchema = z3.string().min(8, "Password must be at least 8 characters").regex(/[A-Z]/, "Password must contain at least one uppercase letter").regex(/[a-z]/, "Password must contain at least one lowercase letter").regex(/[0-9]/, "Password must contain at least one number").regex(/[^A-Za-z0-9]/, "Password must contain at least one special character");
5312
+ var urlSchema = z3.string().url("Invalid URL format");
5313
+ var phoneSchema = z3.string().regex(/^\+?[1-9]\d{1,14}$/, "Invalid phone number format");
5314
+ var dateSchema = z3.coerce.date();
5315
+ var futureDateSchema = z3.coerce.date().refine((date) => date > /* @__PURE__ */ new Date(), "Date must be in the future");
5316
+ var pastDateSchema = z3.coerce.date().refine((date) => date < /* @__PURE__ */ new Date(), "Date must be in the past");
5317
+
5318
+ // src/modules/auth/auth.controller.ts
5319
+ var AuthController = class {
5320
+ constructor(authService, userService) {
5321
+ this.authService = authService;
5322
+ this.userService = userService;
5323
+ }
5324
+ async register(request, reply) {
5325
+ const data = validateBody(registerSchema, request.body);
5326
+ const existingUser = await this.userService.findByEmail(data.email);
5327
+ if (existingUser) {
5328
+ throw new BadRequestError("Email already registered");
5329
+ }
5330
+ const hashedPassword = await this.authService.hashPassword(data.password);
5331
+ const user = await this.userService.create({
5332
+ email: data.email,
5333
+ password: hashedPassword,
5334
+ name: data.name
5335
+ });
5336
+ const tokens = this.authService.generateTokenPair({
5337
+ id: user.id,
5338
+ email: user.email,
5339
+ role: user.role
5340
+ });
5341
+ created(reply, {
5342
+ user: {
5343
+ id: user.id,
5344
+ email: user.email,
5345
+ name: user.name,
5346
+ role: user.role
5347
+ },
5348
+ ...tokens
5349
+ });
5350
+ }
5351
+ async login(request, reply) {
5352
+ const data = validateBody(loginSchema, request.body);
5353
+ const user = await this.userService.findByEmail(data.email);
5354
+ if (!user) {
5355
+ throw new UnauthorizedError("Invalid credentials");
5356
+ }
5357
+ if (user.status !== "active") {
5358
+ throw new UnauthorizedError("Account is not active");
5359
+ }
5360
+ const isValidPassword = await this.authService.verifyPassword(data.password, user.password);
5361
+ if (!isValidPassword) {
5362
+ throw new UnauthorizedError("Invalid credentials");
5363
+ }
5364
+ await this.userService.updateLastLogin(user.id);
5365
+ const tokens = this.authService.generateTokenPair({
5366
+ id: user.id,
5367
+ email: user.email,
5368
+ role: user.role
5369
+ });
5370
+ success2(reply, {
5371
+ user: {
5372
+ id: user.id,
5373
+ email: user.email,
5374
+ name: user.name,
5375
+ role: user.role
5376
+ },
5377
+ ...tokens
5378
+ });
5379
+ }
5380
+ async refresh(request, reply) {
5381
+ const data = validateBody(refreshTokenSchema, request.body);
5382
+ const payload = await this.authService.verifyRefreshToken(data.refreshToken);
5383
+ const user = await this.userService.findById(payload.sub);
5384
+ if (!user || user.status !== "active") {
5385
+ throw new UnauthorizedError("User not found or inactive");
5386
+ }
5387
+ await this.authService.blacklistToken(data.refreshToken);
5388
+ const tokens = this.authService.generateTokenPair({
5389
+ id: user.id,
5390
+ email: user.email,
5391
+ role: user.role
5392
+ });
5393
+ success2(reply, tokens);
5394
+ }
5395
+ async logout(request, reply) {
5396
+ const authHeader = request.headers.authorization;
5397
+ if (authHeader?.startsWith("Bearer ")) {
5398
+ const token = authHeader.substring(7);
5399
+ await this.authService.blacklistToken(token);
5400
+ }
5401
+ success2(reply, { message: "Logged out successfully" });
5402
+ }
5403
+ async me(request, reply) {
5404
+ const authRequest = request;
5405
+ const user = await this.userService.findById(authRequest.user.id);
5406
+ if (!user) {
5407
+ throw new UnauthorizedError("User not found");
5408
+ }
5409
+ success2(reply, {
5410
+ id: user.id,
5411
+ email: user.email,
5412
+ name: user.name,
5413
+ role: user.role,
5414
+ status: user.status,
5415
+ createdAt: user.createdAt
5416
+ });
5417
+ }
5418
+ async changePassword(request, reply) {
5419
+ const authRequest = request;
5420
+ const data = validateBody(changePasswordSchema, request.body);
5421
+ const user = await this.userService.findById(authRequest.user.id);
5422
+ if (!user) {
5423
+ throw new UnauthorizedError("User not found");
5424
+ }
5425
+ const isValidPassword = await this.authService.verifyPassword(
5426
+ data.currentPassword,
5427
+ user.password
5428
+ );
5429
+ if (!isValidPassword) {
5430
+ throw new BadRequestError("Current password is incorrect");
5431
+ }
5432
+ const hashedPassword = await this.authService.hashPassword(data.newPassword);
5433
+ await this.userService.updatePassword(user.id, hashedPassword);
5434
+ success2(reply, { message: "Password changed successfully" });
5435
+ }
5436
+ };
5437
+ function createAuthController(authService, userService) {
5438
+ return new AuthController(authService, userService);
5439
+ }
5440
+
5441
+ // src/modules/auth/auth.routes.ts
5442
+ init_esm_shims();
5443
+
5444
+ // src/modules/auth/auth.middleware.ts
5445
+ init_esm_shims();
5446
+ function createAuthMiddleware(authService) {
5447
+ return async function authenticate(request, _reply) {
5448
+ const authHeader = request.headers.authorization;
5449
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
5450
+ throw new UnauthorizedError("Missing or invalid authorization header");
5451
+ }
5452
+ const token = authHeader.substring(7);
5453
+ const payload = await authService.verifyAccessToken(token);
5454
+ request.user = {
5455
+ id: payload.sub,
5456
+ email: payload.email,
5457
+ role: payload.role
5458
+ };
5459
+ };
5460
+ }
5461
+ function createRoleMiddleware(allowedRoles) {
5462
+ return async function authorize(request, _reply) {
5463
+ const user = request.user;
5464
+ if (!user) {
5465
+ throw new UnauthorizedError("Authentication required");
5466
+ }
5467
+ if (!allowedRoles.includes(user.role)) {
5468
+ throw new ForbiddenError("Insufficient permissions");
5469
+ }
5470
+ };
5471
+ }
5472
+
5473
+ // src/modules/auth/auth.routes.ts
5474
+ function registerAuthRoutes(app, controller, authService) {
5475
+ const authenticate = createAuthMiddleware(authService);
5476
+ app.post("/auth/register", controller.register.bind(controller));
5477
+ app.post("/auth/login", controller.login.bind(controller));
5478
+ app.post("/auth/refresh", controller.refresh.bind(controller));
5479
+ app.post("/auth/logout", { preHandler: [authenticate] }, controller.logout.bind(controller));
5480
+ app.get("/auth/me", { preHandler: [authenticate] }, controller.me.bind(controller));
5481
+ app.post(
5482
+ "/auth/change-password",
5483
+ { preHandler: [authenticate] },
5484
+ controller.changePassword.bind(controller)
5485
+ );
5486
+ }
5487
+
5488
+ // src/modules/user/user.service.ts
5489
+ init_esm_shims();
5490
+
5491
+ // src/modules/user/user.repository.ts
5492
+ init_esm_shims();
5493
+
5494
+ // src/database/prisma.ts
5495
+ init_esm_shims();
5496
+ import { PrismaClient } from "@prisma/client";
5497
+ var prismaClientSingleton = () => {
5498
+ return new PrismaClient({
5499
+ log: isProduction() ? ["error"] : ["query", "info", "warn", "error"],
5500
+ errorFormat: isProduction() ? "minimal" : "pretty"
5501
+ });
5502
+ };
5503
+ var prisma = globalThis.__prisma ?? prismaClientSingleton();
5504
+ if (!isProduction()) {
5505
+ globalThis.__prisma = prisma;
5506
+ }
5507
+
5508
+ // src/utils/pagination.ts
5509
+ init_esm_shims();
5510
+ var DEFAULT_PAGE = 1;
5511
+ var DEFAULT_LIMIT = 20;
5512
+ var MAX_LIMIT = 100;
5513
+ function parsePaginationParams(query) {
5514
+ const page = Math.max(1, parseInt(String(query.page || DEFAULT_PAGE), 10));
5515
+ const limit = Math.min(
5516
+ MAX_LIMIT,
5517
+ Math.max(1, parseInt(String(query.limit || DEFAULT_LIMIT), 10))
5518
+ );
5519
+ const sortBy = typeof query.sortBy === "string" ? query.sortBy : void 0;
5520
+ const sortOrder = query.sortOrder === "desc" ? "desc" : "asc";
5521
+ return { page, limit, sortBy, sortOrder };
5522
+ }
5523
+ function createPaginatedResult(data, total, params) {
5524
+ const totalPages = Math.ceil(total / params.limit);
5525
+ return {
5526
+ data,
5527
+ meta: {
5528
+ total,
5529
+ page: params.page,
5530
+ limit: params.limit,
5531
+ totalPages,
5532
+ hasNextPage: params.page < totalPages,
5533
+ hasPrevPage: params.page > 1
5534
+ }
5535
+ };
5536
+ }
5537
+ function getSkip(params) {
5538
+ return (params.page - 1) * params.limit;
5539
+ }
5540
+
5541
+ // src/modules/user/user.repository.ts
5542
+ import { UserRole, UserStatus } from "@prisma/client";
5543
+ var UserRepository = class {
5544
+ /**
5545
+ * Find user by ID
5546
+ */
5547
+ async findById(id) {
5548
+ const user = await prisma.user.findUnique({
5549
+ where: { id }
5550
+ });
5551
+ if (!user) return null;
5552
+ return this.mapPrismaUserToUser(user);
5553
+ }
5554
+ /**
5555
+ * Find user by email (case-insensitive)
5556
+ */
5557
+ async findByEmail(email) {
5558
+ const user = await prisma.user.findUnique({
5559
+ where: { email: email.toLowerCase() }
5560
+ });
5561
+ if (!user) return null;
5562
+ return this.mapPrismaUserToUser(user);
5563
+ }
5564
+ /**
5565
+ * Find multiple users with pagination and filters
5566
+ */
5567
+ async findMany(params, filters) {
5568
+ const where = this.buildWhereClause(filters);
5569
+ const orderBy = this.buildOrderBy(params);
5570
+ const [data, total] = await Promise.all([
5571
+ prisma.user.findMany({
5572
+ where,
5573
+ orderBy,
5574
+ skip: getSkip(params),
5575
+ take: params.limit
5576
+ }),
5577
+ prisma.user.count({ where })
5578
+ ]);
5579
+ const mappedUsers = data.map((user) => this.mapPrismaUserToUser(user));
5580
+ return createPaginatedResult(mappedUsers, total, params);
5581
+ }
5582
+ /**
5583
+ * Create a new user
5584
+ */
5585
+ async create(data) {
5586
+ const user = await prisma.user.create({
5587
+ data: {
5588
+ email: data.email.toLowerCase(),
5589
+ password: data.password,
5590
+ name: data.name,
5591
+ role: this.mapRoleToEnum(data.role || "user"),
5592
+ status: UserStatus.ACTIVE,
5593
+ emailVerified: false
5594
+ }
5595
+ });
5596
+ return this.mapPrismaUserToUser(user);
5597
+ }
5598
+ /**
5599
+ * Update user data
5600
+ */
5601
+ async update(id, data) {
5602
+ try {
5603
+ const user = await prisma.user.update({
5604
+ where: { id },
5605
+ data: {
5606
+ ...data.email && { email: data.email.toLowerCase() },
5607
+ ...data.name !== void 0 && { name: data.name },
5608
+ ...data.role && { role: this.mapRoleToEnum(data.role) },
5609
+ ...data.status && { status: this.mapStatusToEnum(data.status) },
5610
+ ...data.emailVerified !== void 0 && { emailVerified: data.emailVerified },
5611
+ ...data.metadata && { metadata: data.metadata }
5612
+ }
5613
+ });
5614
+ return this.mapPrismaUserToUser(user);
5615
+ } catch {
5616
+ return null;
5617
+ }
5618
+ }
5619
+ /**
5620
+ * Update user password
5621
+ */
5622
+ async updatePassword(id, password) {
5623
+ try {
5624
+ const user = await prisma.user.update({
5625
+ where: { id },
5626
+ data: { password }
5627
+ });
5628
+ return this.mapPrismaUserToUser(user);
5629
+ } catch {
5630
+ return null;
5631
+ }
5632
+ }
5633
+ /**
5634
+ * Update last login timestamp
5635
+ */
5636
+ async updateLastLogin(id) {
5637
+ try {
5638
+ const user = await prisma.user.update({
5639
+ where: { id },
5640
+ data: { lastLoginAt: /* @__PURE__ */ new Date() }
5641
+ });
5642
+ return this.mapPrismaUserToUser(user);
5643
+ } catch {
5644
+ return null;
5645
+ }
5646
+ }
5647
+ /**
5648
+ * Delete user by ID
5649
+ */
5650
+ async delete(id) {
5651
+ try {
5652
+ await prisma.user.delete({
5653
+ where: { id }
5654
+ });
5655
+ return true;
5656
+ } catch {
5657
+ return false;
5658
+ }
5659
+ }
5660
+ /**
5661
+ * Count users with optional filters
5662
+ */
5663
+ async count(filters) {
5664
+ const where = this.buildWhereClause(filters);
5665
+ return prisma.user.count({ where });
5666
+ }
5667
+ /**
5668
+ * Helper to clear all users (for testing only)
5669
+ * WARNING: This deletes all users from the database
5670
+ */
5671
+ async clear() {
5672
+ await prisma.user.deleteMany();
5673
+ }
5674
+ /**
5675
+ * Build Prisma where clause from filters
5676
+ */
5677
+ buildWhereClause(filters) {
5678
+ if (!filters) return {};
5679
+ return {
5680
+ ...filters.status && { status: this.mapStatusToEnum(filters.status) },
5681
+ ...filters.role && { role: this.mapRoleToEnum(filters.role) },
5682
+ ...filters.emailVerified !== void 0 && { emailVerified: filters.emailVerified },
5683
+ ...filters.search && {
5684
+ OR: [
5685
+ { email: { contains: filters.search, mode: "insensitive" } },
5686
+ { name: { contains: filters.search, mode: "insensitive" } }
5687
+ ]
5688
+ }
5689
+ };
5690
+ }
5691
+ /**
5692
+ * Build Prisma orderBy clause from pagination params
5693
+ */
5694
+ buildOrderBy(params) {
5695
+ if (!params.sortBy) {
5696
+ return { createdAt: "desc" };
5697
+ }
5698
+ return {
5699
+ [params.sortBy]: params.sortOrder || "asc"
5700
+ };
5701
+ }
5702
+ /**
5703
+ * Map Prisma User to application User type
5704
+ */
5705
+ mapPrismaUserToUser(prismaUser) {
5706
+ return {
5707
+ id: prismaUser.id,
5708
+ email: prismaUser.email,
5709
+ password: prismaUser.password,
5710
+ name: prismaUser.name ?? void 0,
5711
+ role: this.mapEnumToRole(prismaUser.role),
5712
+ status: this.mapEnumToStatus(prismaUser.status),
5713
+ emailVerified: prismaUser.emailVerified,
5714
+ lastLoginAt: prismaUser.lastLoginAt ?? void 0,
5715
+ metadata: prismaUser.metadata,
5716
+ createdAt: prismaUser.createdAt,
5717
+ updatedAt: prismaUser.updatedAt
5718
+ };
5719
+ }
5720
+ /**
5721
+ * Map application role to Prisma enum
5722
+ */
5723
+ mapRoleToEnum(role) {
5724
+ const roleMap = {
5725
+ user: UserRole.USER,
5726
+ moderator: UserRole.MODERATOR,
5727
+ admin: UserRole.ADMIN,
5728
+ super_admin: UserRole.SUPER_ADMIN
5729
+ };
5730
+ return roleMap[role] || UserRole.USER;
5731
+ }
5732
+ /**
5733
+ * Map Prisma enum to application role
5734
+ */
5735
+ mapEnumToRole(role) {
5736
+ const roleMap = {
5737
+ [UserRole.USER]: "user",
5738
+ [UserRole.MODERATOR]: "moderator",
5739
+ [UserRole.ADMIN]: "admin",
5740
+ [UserRole.SUPER_ADMIN]: "super_admin"
5741
+ };
5742
+ return roleMap[role];
5743
+ }
5744
+ /**
5745
+ * Map application status to Prisma enum
5746
+ */
5747
+ mapStatusToEnum(status) {
5748
+ const statusMap = {
5749
+ active: UserStatus.ACTIVE,
5750
+ inactive: UserStatus.INACTIVE,
5751
+ suspended: UserStatus.SUSPENDED,
5752
+ banned: UserStatus.BANNED
5753
+ };
5754
+ return statusMap[status] || UserStatus.ACTIVE;
5755
+ }
5756
+ /**
5757
+ * Map Prisma enum to application status
5758
+ */
5759
+ mapEnumToStatus(status) {
5760
+ const statusMap = {
5761
+ [UserStatus.ACTIVE]: "active",
5762
+ [UserStatus.INACTIVE]: "inactive",
5763
+ [UserStatus.SUSPENDED]: "suspended",
5764
+ [UserStatus.BANNED]: "banned"
5765
+ };
5766
+ return statusMap[status];
5767
+ }
5768
+ };
5769
+ function createUserRepository() {
5770
+ return new UserRepository();
5771
+ }
5772
+
5773
+ // src/modules/user/types.ts
5774
+ init_esm_shims();
5775
+ var DEFAULT_ROLE_PERMISSIONS = {
5776
+ user: ["profile:read", "profile:update"],
5777
+ moderator: [
5778
+ "profile:read",
5779
+ "profile:update",
5780
+ "users:read",
5781
+ "content:read",
5782
+ "content:update",
5783
+ "content:delete"
5784
+ ],
5785
+ admin: [
5786
+ "profile:read",
5787
+ "profile:update",
5788
+ "users:read",
5789
+ "users:update",
5790
+ "users:delete",
5791
+ "content:manage",
5792
+ "settings:read"
5793
+ ],
5794
+ super_admin: ["*:manage"]
5795
+ // All permissions
5796
+ };
5797
+
5798
+ // src/modules/user/user.service.ts
5799
+ var UserService = class {
5800
+ constructor(repository) {
5801
+ this.repository = repository;
5802
+ }
5803
+ async findById(id) {
5804
+ return this.repository.findById(id);
5805
+ }
5806
+ async findByEmail(email) {
5807
+ return this.repository.findByEmail(email);
5808
+ }
5809
+ async findMany(params, filters) {
5810
+ const result = await this.repository.findMany(params, filters);
5811
+ return {
5812
+ ...result,
5813
+ data: result.data.map(({ password: _password, ...user }) => user)
5814
+ };
5815
+ }
5816
+ async create(data) {
5817
+ const existing = await this.repository.findByEmail(data.email);
5818
+ if (existing) {
5819
+ throw new ConflictError("User with this email already exists");
5820
+ }
5821
+ const user = await this.repository.create(data);
5822
+ logger.info({ userId: user.id, email: user.email }, "User created");
5823
+ return user;
5824
+ }
5825
+ async update(id, data) {
5826
+ const user = await this.repository.findById(id);
5827
+ if (!user) {
5828
+ throw new NotFoundError("User");
5829
+ }
5830
+ if (data.email && data.email !== user.email) {
5831
+ const existing = await this.repository.findByEmail(data.email);
5832
+ if (existing) {
5833
+ throw new ConflictError("Email already in use");
5834
+ }
5835
+ }
5836
+ const updatedUser = await this.repository.update(id, data);
5837
+ if (!updatedUser) {
5838
+ throw new NotFoundError("User");
5839
+ }
5840
+ logger.info({ userId: id }, "User updated");
5841
+ return updatedUser;
5842
+ }
5843
+ async updatePassword(id, hashedPassword) {
5844
+ const user = await this.repository.updatePassword(id, hashedPassword);
5845
+ if (!user) {
5846
+ throw new NotFoundError("User");
5847
+ }
5848
+ logger.info({ userId: id }, "User password updated");
5849
+ return user;
5850
+ }
5851
+ async updateLastLogin(id) {
5852
+ const user = await this.repository.updateLastLogin(id);
5853
+ if (!user) {
5854
+ throw new NotFoundError("User");
5855
+ }
5856
+ return user;
5857
+ }
5858
+ async delete(id) {
5859
+ const user = await this.repository.findById(id);
5860
+ if (!user) {
5861
+ throw new NotFoundError("User");
5862
+ }
5863
+ await this.repository.delete(id);
5864
+ logger.info({ userId: id }, "User deleted");
5865
+ }
5866
+ async suspend(id) {
5867
+ return this.update(id, { status: "suspended" });
5868
+ }
5869
+ async ban(id) {
5870
+ return this.update(id, { status: "banned" });
5871
+ }
5872
+ async activate(id) {
5873
+ return this.update(id, { status: "active" });
5874
+ }
5875
+ async verifyEmail(id) {
5876
+ return this.update(id, { emailVerified: true });
5877
+ }
5878
+ async changeRole(id, role) {
5879
+ return this.update(id, { role });
5880
+ }
5881
+ // RBAC helpers
5882
+ hasPermission(role, permission) {
5883
+ const permissions = DEFAULT_ROLE_PERMISSIONS[role] || [];
5884
+ if (permissions.includes("*:manage")) {
5885
+ return true;
5886
+ }
5887
+ if (permissions.includes(permission)) {
5888
+ return true;
5889
+ }
5890
+ const [resource] = permission.split(":");
5891
+ const managePermission = `${resource}:manage`;
5892
+ if (permissions.includes(managePermission)) {
5893
+ return true;
5894
+ }
5895
+ return false;
5896
+ }
5897
+ getPermissions(role) {
5898
+ return DEFAULT_ROLE_PERMISSIONS[role] || [];
5899
+ }
5900
+ };
5901
+ function createUserService(repository) {
5902
+ return new UserService(repository || createUserRepository());
5903
+ }
5904
+
5905
+ // src/modules/auth/types.ts
5906
+ init_esm_shims();
5907
+
5908
+ // src/modules/auth/index.ts
5909
+ async function registerAuthModule(app) {
5910
+ await app.register(jwt, {
5911
+ secret: config.jwt.secret,
5912
+ sign: {
5913
+ algorithm: "HS256"
5914
+ }
5915
+ });
5916
+ await app.register(cookie, {
5917
+ secret: config.jwt.secret,
5918
+ hook: "onRequest"
5919
+ });
5920
+ const authService = createAuthService(app);
5921
+ const userService = createUserService();
5922
+ const authController = createAuthController(authService, userService);
5923
+ registerAuthRoutes(app, authController, authService);
5924
+ logger.info("Auth module registered");
5925
+ }
5926
+
5927
+ // src/modules/user/index.ts
5928
+ init_esm_shims();
5929
+
5930
+ // src/modules/user/user.controller.ts
5931
+ init_esm_shims();
5932
+
5933
+ // src/modules/user/schemas.ts
5934
+ init_esm_shims();
5935
+ import { z as z4 } from "zod";
5936
+ var userStatusEnum = z4.enum(["active", "inactive", "suspended", "banned"]);
5937
+ var userRoleEnum = z4.enum(["user", "admin", "moderator", "super_admin"]);
5938
+ var createUserSchema = z4.object({
5939
+ email: z4.string().email("Invalid email address"),
5940
+ password: z4.string().min(8, "Password must be at least 8 characters").regex(/[A-Z]/, "Password must contain at least one uppercase letter").regex(/[a-z]/, "Password must contain at least one lowercase letter").regex(/[0-9]/, "Password must contain at least one number"),
5941
+ name: z4.string().min(2, "Name must be at least 2 characters").optional(),
5942
+ role: userRoleEnum.optional().default("user")
5943
+ });
5944
+ var updateUserSchema = z4.object({
5945
+ email: z4.string().email("Invalid email address").optional(),
5946
+ name: z4.string().min(2, "Name must be at least 2 characters").optional(),
5947
+ role: userRoleEnum.optional(),
5948
+ status: userStatusEnum.optional(),
5949
+ emailVerified: z4.boolean().optional(),
5950
+ metadata: z4.record(z4.unknown()).optional()
5951
+ });
5952
+ var updateProfileSchema = z4.object({
5953
+ name: z4.string().min(2, "Name must be at least 2 characters").optional(),
5954
+ metadata: z4.record(z4.unknown()).optional()
5955
+ });
5956
+ var userQuerySchema = z4.object({
5957
+ page: z4.string().transform(Number).optional(),
5958
+ limit: z4.string().transform(Number).optional(),
5959
+ sortBy: z4.string().optional(),
5960
+ sortOrder: z4.enum(["asc", "desc"]).optional(),
5961
+ status: userStatusEnum.optional(),
5962
+ role: userRoleEnum.optional(),
5963
+ search: z4.string().optional(),
5964
+ emailVerified: z4.string().transform((val) => val === "true").optional()
5965
+ });
5966
+
5967
+ // src/modules/user/user.controller.ts
5968
+ function omitPassword(user) {
5969
+ const { password, ...userData } = user;
5970
+ void password;
5971
+ return userData;
5972
+ }
5973
+ var UserController = class {
5974
+ constructor(userService) {
5975
+ this.userService = userService;
5976
+ }
5977
+ async list(request, reply) {
5978
+ const query = validateQuery(userQuerySchema, request.query);
5979
+ const pagination = parsePaginationParams(query);
5980
+ const filters = {
5981
+ status: query.status,
5982
+ role: query.role,
5983
+ search: query.search,
5984
+ emailVerified: query.emailVerified
5985
+ };
5986
+ const result = await this.userService.findMany(pagination, filters);
5987
+ success2(reply, result);
5988
+ }
5989
+ async getById(request, reply) {
5990
+ const user = await this.userService.findById(request.params.id);
5991
+ if (!user) {
5992
+ return reply.status(404).send({
5993
+ success: false,
5994
+ message: "User not found"
5995
+ });
5996
+ }
5997
+ success2(reply, omitPassword(user));
5998
+ }
5999
+ async update(request, reply) {
6000
+ const data = validateBody(updateUserSchema, request.body);
6001
+ const user = await this.userService.update(request.params.id, data);
6002
+ success2(reply, omitPassword(user));
6003
+ }
6004
+ async delete(request, reply) {
6005
+ const authRequest = request;
6006
+ if (authRequest.user.id === request.params.id) {
6007
+ throw new ForbiddenError("Cannot delete your own account");
6008
+ }
6009
+ await this.userService.delete(request.params.id);
6010
+ noContent(reply);
6011
+ }
6012
+ async suspend(request, reply) {
6013
+ const authRequest = request;
6014
+ if (authRequest.user.id === request.params.id) {
6015
+ throw new ForbiddenError("Cannot suspend your own account");
6016
+ }
6017
+ const user = await this.userService.suspend(request.params.id);
6018
+ const userData = omitPassword(user);
6019
+ success2(reply, userData);
6020
+ }
6021
+ async ban(request, reply) {
6022
+ const authRequest = request;
6023
+ if (authRequest.user.id === request.params.id) {
6024
+ throw new ForbiddenError("Cannot ban your own account");
6025
+ }
6026
+ const user = await this.userService.ban(request.params.id);
6027
+ const userData = omitPassword(user);
6028
+ success2(reply, userData);
6029
+ }
6030
+ async activate(request, reply) {
6031
+ const user = await this.userService.activate(request.params.id);
6032
+ const userData = omitPassword(user);
6033
+ success2(reply, userData);
6034
+ }
6035
+ // Profile routes (for authenticated user)
6036
+ async getProfile(request, reply) {
6037
+ const authRequest = request;
6038
+ const user = await this.userService.findById(authRequest.user.id);
6039
+ if (!user) {
6040
+ return reply.status(404).send({
6041
+ success: false,
6042
+ message: "User not found"
6043
+ });
6044
+ }
6045
+ const userData = omitPassword(user);
6046
+ success2(reply, userData);
6047
+ }
6048
+ async updateProfile(request, reply) {
6049
+ const authRequest = request;
6050
+ const data = validateBody(updateProfileSchema, request.body);
6051
+ const user = await this.userService.update(authRequest.user.id, data);
6052
+ const userData = omitPassword(user);
6053
+ success2(reply, userData);
6054
+ }
6055
+ };
6056
+ function createUserController(userService) {
6057
+ return new UserController(userService);
6058
+ }
6059
+
6060
+ // src/modules/user/user.routes.ts
6061
+ init_esm_shims();
6062
+ var idParamsSchema = {
6063
+ type: "object",
6064
+ properties: {
6065
+ id: { type: "string" }
6066
+ },
6067
+ required: ["id"]
6068
+ };
6069
+ function registerUserRoutes(app, controller, authService) {
6070
+ const authenticate = createAuthMiddleware(authService);
6071
+ const isAdmin = createRoleMiddleware(["admin", "super_admin"]);
6072
+ const isModerator = createRoleMiddleware(["moderator", "admin", "super_admin"]);
6073
+ app.get("/profile", { preHandler: [authenticate] }, controller.getProfile.bind(controller));
6074
+ app.patch("/profile", { preHandler: [authenticate] }, controller.updateProfile.bind(controller));
6075
+ app.get("/users", { preHandler: [authenticate, isModerator] }, controller.list.bind(controller));
6076
+ app.get(
6077
+ "/users/:id",
6078
+ { preHandler: [authenticate, isModerator], schema: { params: idParamsSchema } },
6079
+ async (request, reply) => {
6080
+ return controller.getById(request, reply);
6081
+ }
6082
+ );
6083
+ app.patch(
6084
+ "/users/:id",
6085
+ { preHandler: [authenticate, isAdmin], schema: { params: idParamsSchema } },
6086
+ async (request, reply) => {
6087
+ return controller.update(request, reply);
6088
+ }
6089
+ );
6090
+ app.delete(
6091
+ "/users/:id",
6092
+ { preHandler: [authenticate, isAdmin], schema: { params: idParamsSchema } },
6093
+ async (request, reply) => {
6094
+ return controller.delete(request, reply);
6095
+ }
6096
+ );
6097
+ app.post(
6098
+ "/users/:id/suspend",
6099
+ { preHandler: [authenticate, isAdmin], schema: { params: idParamsSchema } },
6100
+ async (request, reply) => {
6101
+ return controller.suspend(request, reply);
6102
+ }
6103
+ );
6104
+ app.post(
6105
+ "/users/:id/ban",
6106
+ { preHandler: [authenticate, isAdmin], schema: { params: idParamsSchema } },
6107
+ async (request, reply) => {
6108
+ return controller.ban(request, reply);
6109
+ }
6110
+ );
6111
+ app.post(
6112
+ "/users/:id/activate",
6113
+ { preHandler: [authenticate, isAdmin], schema: { params: idParamsSchema } },
6114
+ async (request, reply) => {
6115
+ return controller.activate(request, reply);
6116
+ }
6117
+ );
6118
+ }
6119
+
6120
+ // src/modules/user/index.ts
6121
+ async function registerUserModule(app, authService) {
6122
+ const repository = createUserRepository();
6123
+ const userService = createUserService(repository);
6124
+ const userController = createUserController(userService);
6125
+ registerUserRoutes(app, userController, authService);
6126
+ logger.info("User module registered");
6127
+ }
6128
+
6129
+ // src/cli/utils/docs-generator.ts
6130
+ async function generateDocs(outputPath = "openapi.json", silent = false) {
6131
+ const spinner = silent ? null : ora5("Generating OpenAPI documentation...").start();
6132
+ try {
6133
+ const server = createServer({
6134
+ port: config.server.port,
6135
+ host: config.server.host
6136
+ });
6137
+ const app = server.instance;
6138
+ registerErrorHandler(app);
6139
+ await registerSecurity(app);
6140
+ await registerSwagger(app, {
6141
+ title: "Servcraft API",
6142
+ description: "API documentation generated by Servcraft",
6143
+ version: "1.0.0"
6144
+ });
6145
+ await registerAuthModule(app);
6146
+ const authService = createAuthService(app);
6147
+ await registerUserModule(app, authService);
6148
+ await app.ready();
6149
+ const spec = app.swagger();
6150
+ const absoluteOutput = path9.resolve(outputPath);
6151
+ await fs6.mkdir(path9.dirname(absoluteOutput), { recursive: true });
6152
+ await fs6.writeFile(absoluteOutput, JSON.stringify(spec, null, 2), "utf8");
6153
+ spinner?.succeed(`OpenAPI spec generated at ${absoluteOutput}`);
6154
+ await app.close();
6155
+ return absoluteOutput;
6156
+ } catch (error2) {
6157
+ spinner?.fail("Failed to generate OpenAPI documentation");
6158
+ throw error2;
6159
+ }
6160
+ }
6161
+
6162
+ // src/cli/commands/docs.ts
6163
+ var docsCommand = new Command5("docs").description("API documentation commands");
6164
+ docsCommand.command("generate").alias("gen").description("Generate OpenAPI/Swagger documentation").option("-o, --output <file>", "Output file path", "openapi.json").option("-f, --format <format>", "Output format: json, yaml", "json").action(async (options) => {
6165
+ try {
6166
+ const outputPath = await generateDocs(options.output, false);
6167
+ if (options.format === "yaml") {
6168
+ const jsonContent = await fs7.readFile(outputPath, "utf-8");
6169
+ const spec = JSON.parse(jsonContent);
6170
+ const yamlPath = outputPath.replace(".json", ".yaml");
6171
+ await fs7.writeFile(yamlPath, jsonToYaml(spec));
6172
+ success(`YAML documentation generated: ${yamlPath}`);
6173
+ }
6174
+ console.log("\n\u{1F4DA} Documentation URLs:");
6175
+ info(" Swagger UI: http://localhost:3000/docs");
6176
+ info(" OpenAPI JSON: http://localhost:3000/docs/json");
6177
+ } catch (err) {
6178
+ error(err instanceof Error ? err.message : String(err));
6179
+ }
6180
+ });
6181
+ docsCommand.option("-o, --output <path>", "Output file path", "openapi.json").action(async (options) => {
6182
+ if (options.output) {
6183
+ try {
6184
+ const outputPath = await generateDocs(options.output);
6185
+ success(`Documentation written to ${outputPath}`);
6186
+ } catch (err) {
6187
+ error(err instanceof Error ? err.message : String(err));
6188
+ process.exitCode = 1;
6189
+ }
6190
+ }
6191
+ });
6192
+ docsCommand.command("export").description("Export documentation to Postman, Insomnia, or YAML").option("-f, --format <format>", "Export format: postman, insomnia, yaml", "postman").option("-o, --output <file>", "Output file path").action(async (options) => {
6193
+ const spinner = ora6("Exporting documentation...").start();
6194
+ try {
6195
+ const projectRoot = getProjectRoot();
6196
+ const specPath = path10.join(projectRoot, "openapi.json");
6197
+ try {
6198
+ await fs7.access(specPath);
6199
+ } catch {
6200
+ spinner.text = "Generating OpenAPI spec first...";
6201
+ await generateDocs("openapi.json", true);
6202
+ }
6203
+ const specContent = await fs7.readFile(specPath, "utf-8");
6204
+ const spec = JSON.parse(specContent);
6205
+ let output;
6206
+ let defaultName;
6207
+ switch (options.format) {
6208
+ case "postman":
6209
+ output = JSON.stringify(convertToPostman(spec), null, 2);
6210
+ defaultName = "postman_collection.json";
6211
+ break;
6212
+ case "insomnia":
6213
+ output = JSON.stringify(convertToInsomnia(spec), null, 2);
6214
+ defaultName = "insomnia_collection.json";
6215
+ break;
6216
+ case "yaml":
6217
+ output = jsonToYaml(spec);
6218
+ defaultName = "openapi.yaml";
6219
+ break;
6220
+ default:
6221
+ throw new Error(`Unknown format: ${options.format}`);
6222
+ }
6223
+ const outPath = path10.join(projectRoot, options.output || defaultName);
6224
+ await fs7.writeFile(outPath, output);
6225
+ spinner.succeed(`Exported to: ${options.output || defaultName}`);
6226
+ if (options.format === "postman") {
6227
+ info("\n Import in Postman: File > Import > Select file");
6228
+ }
6229
+ } catch (err) {
6230
+ spinner.fail("Export failed");
6231
+ error(err instanceof Error ? err.message : String(err));
6232
+ }
6233
+ });
6234
+ docsCommand.command("status").description("Show documentation status").action(async () => {
6235
+ const projectRoot = getProjectRoot();
6236
+ console.log(chalk9.bold("\n\u{1F4CA} Documentation Status\n"));
6237
+ const specPath = path10.join(projectRoot, "openapi.json");
6238
+ try {
6239
+ const stat2 = await fs7.stat(specPath);
6240
+ success(
6241
+ `openapi.json exists (${formatBytes(stat2.size)}, modified ${formatDate(stat2.mtime)})`
6242
+ );
6243
+ const content = await fs7.readFile(specPath, "utf-8");
6244
+ const spec = JSON.parse(content);
6245
+ const pathCount = Object.keys(spec.paths || {}).length;
6246
+ info(` ${pathCount} endpoints documented`);
6247
+ } catch {
6248
+ warn('openapi.json not found - run "servcraft docs generate"');
6249
+ }
6250
+ console.log("\n\u{1F4CC} Commands:");
6251
+ info(" servcraft docs generate Generate OpenAPI spec");
6252
+ info(" servcraft docs export Export to Postman/Insomnia");
6253
+ });
6254
+ function jsonToYaml(obj, indent = 0) {
6255
+ const spaces = " ".repeat(indent);
6256
+ if (obj === null || obj === void 0) return "null";
6257
+ if (typeof obj === "string") {
6258
+ if (obj.includes("\n") || obj.includes(":") || obj.includes("#")) {
6259
+ return `"${obj.replace(/"/g, '\\"')}"`;
6260
+ }
6261
+ return obj || '""';
6262
+ }
6263
+ if (typeof obj === "number" || typeof obj === "boolean") return String(obj);
6264
+ if (Array.isArray(obj)) {
6265
+ if (obj.length === 0) return "[]";
6266
+ return obj.map((item) => `${spaces}- ${jsonToYaml(item, indent + 1).trimStart()}`).join("\n");
6267
+ }
6268
+ if (typeof obj === "object") {
6269
+ const entries = Object.entries(obj);
6270
+ if (entries.length === 0) return "{}";
6271
+ return entries.map(([key, value]) => {
6272
+ const valueStr = jsonToYaml(value, indent + 1);
6273
+ if (typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length > 0) {
6274
+ return `${spaces}${key}:
6275
+ ${valueStr}`;
6276
+ }
6277
+ return `${spaces}${key}: ${valueStr}`;
6278
+ }).join("\n");
6279
+ }
6280
+ return String(obj);
6281
+ }
6282
+ function convertToPostman(spec) {
6283
+ const baseUrl = spec.servers?.[0]?.url || "http://localhost:3000";
6284
+ const items = [];
6285
+ for (const [pathUrl, methods] of Object.entries(spec.paths || {})) {
6286
+ for (const [method, details] of Object.entries(methods)) {
6287
+ items.push({
6288
+ name: details.summary || `${method.toUpperCase()} ${pathUrl}`,
6289
+ request: {
6290
+ method: method.toUpperCase(),
6291
+ header: [
6292
+ { key: "Content-Type", value: "application/json" },
6293
+ { key: "Authorization", value: "Bearer {{token}}" }
6294
+ ],
6295
+ url: {
6296
+ raw: `{{baseUrl}}${pathUrl}`,
6297
+ host: ["{{baseUrl}}"],
6298
+ path: pathUrl.split("/").filter(Boolean)
6299
+ },
6300
+ ...details.requestBody ? { body: { mode: "raw", raw: "{}" } } : {}
6301
+ }
6302
+ });
6303
+ }
6304
+ }
6305
+ return {
6306
+ info: {
6307
+ name: spec.info.title,
6308
+ schema: "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
6309
+ },
6310
+ item: items,
6311
+ variable: [
6312
+ { key: "baseUrl", value: baseUrl },
6313
+ { key: "token", value: "" }
6314
+ ]
6315
+ };
6316
+ }
6317
+ function convertToInsomnia(spec) {
6318
+ const baseUrl = spec.servers?.[0]?.url || "http://localhost:3000";
6319
+ const resources = [
6320
+ { _type: "environment", name: "Base Environment", data: { baseUrl, token: "" } }
6321
+ ];
6322
+ for (const [pathUrl, methods] of Object.entries(spec.paths || {})) {
6323
+ for (const [method, details] of Object.entries(methods)) {
6324
+ resources.push({
6325
+ _type: "request",
6326
+ name: details.summary || `${method.toUpperCase()} ${pathUrl}`,
6327
+ method: method.toUpperCase(),
6328
+ url: `{{ baseUrl }}${pathUrl}`,
6329
+ headers: [
6330
+ { name: "Content-Type", value: "application/json" },
6331
+ { name: "Authorization", value: "Bearer {{ token }}" }
6332
+ ]
6333
+ });
6334
+ }
6335
+ }
6336
+ return { _type: "export", __export_format: 4, resources };
6337
+ }
6338
+ function formatBytes(bytes) {
6339
+ if (bytes < 1024) return `${bytes} B`;
6340
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
6341
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
6342
+ }
6343
+ function formatDate(date) {
6344
+ return date.toLocaleDateString("en-US", {
6345
+ month: "short",
6346
+ day: "numeric",
6347
+ hour: "2-digit",
6348
+ minute: "2-digit"
6349
+ });
6350
+ }
6351
+
6352
+ // src/cli/commands/list.ts
6353
+ init_esm_shims();
6354
+ import { Command as Command6 } from "commander";
6355
+ import chalk10 from "chalk";
6356
+ import fs8 from "fs/promises";
6357
+ var AVAILABLE_MODULES2 = {
6358
+ // Core
6359
+ auth: {
6360
+ name: "Authentication",
6361
+ description: "JWT authentication with access/refresh tokens",
6362
+ category: "Core"
6363
+ },
6364
+ users: {
6365
+ name: "User Management",
6366
+ description: "User CRUD with RBAC (roles & permissions)",
6367
+ category: "Core"
6368
+ },
6369
+ email: {
6370
+ name: "Email Service",
6371
+ description: "SMTP email with templates (Handlebars)",
6372
+ category: "Core"
6373
+ },
6374
+ // Security
6375
+ mfa: {
6376
+ name: "MFA/TOTP",
6377
+ description: "Two-factor authentication with QR codes",
6378
+ category: "Security"
6379
+ },
6380
+ oauth: {
6381
+ name: "OAuth",
6382
+ description: "Social login (Google, GitHub, Facebook, Twitter, Apple)",
6383
+ category: "Security"
6384
+ },
6385
+ "rate-limit": {
6386
+ name: "Rate Limiting",
6387
+ description: "Advanced rate limiting with multiple algorithms",
6388
+ category: "Security"
6389
+ },
6390
+ // Data & Storage
6391
+ cache: {
6392
+ name: "Redis Cache",
6393
+ description: "Redis caching with TTL & invalidation",
6394
+ category: "Data & Storage"
6395
+ },
6396
+ upload: {
6397
+ name: "File Upload",
6398
+ description: "File upload with local/S3/Cloudinary storage",
6399
+ category: "Data & Storage"
6400
+ },
6401
+ search: {
6402
+ name: "Search",
6403
+ description: "Full-text search with Elasticsearch/Meilisearch",
6404
+ category: "Data & Storage"
6405
+ },
6406
+ // Communication
6407
+ notification: {
6408
+ name: "Notifications",
6409
+ description: "Email, SMS, Push notifications",
6410
+ category: "Communication"
6411
+ },
6412
+ webhook: {
6413
+ name: "Webhooks",
6414
+ description: "Outgoing webhooks with HMAC signatures & retry",
6415
+ category: "Communication"
6416
+ },
6417
+ websocket: {
6418
+ name: "WebSockets",
6419
+ description: "Real-time communication with Socket.io",
6420
+ category: "Communication"
6421
+ },
6422
+ // Background Processing
6423
+ queue: {
6424
+ name: "Queue/Jobs",
6425
+ description: "Background jobs with Bull/BullMQ & cron scheduling",
6426
+ category: "Background Processing"
6427
+ },
6428
+ "media-processing": {
6429
+ name: "Media Processing",
6430
+ description: "Image/video processing with FFmpeg",
6431
+ category: "Background Processing"
6432
+ },
6433
+ // Monitoring & Analytics
6434
+ audit: {
6435
+ name: "Audit Logs",
6436
+ description: "Activity logging and audit trail",
6437
+ category: "Monitoring & Analytics"
6438
+ },
6439
+ analytics: {
6440
+ name: "Analytics/Metrics",
6441
+ description: "Prometheus metrics & event tracking",
6442
+ category: "Monitoring & Analytics"
6443
+ },
6444
+ // Internationalization
6445
+ i18n: {
6446
+ name: "i18n/Localization",
6447
+ description: "Multi-language support with 7+ locales",
6448
+ category: "Internationalization"
6449
+ },
6450
+ // API Management
6451
+ "feature-flag": {
6452
+ name: "Feature Flags",
6453
+ description: "A/B testing & progressive rollout",
6454
+ category: "API Management"
6455
+ },
6456
+ "api-versioning": {
6457
+ name: "API Versioning",
6458
+ description: "Multiple API versions support",
6459
+ category: "API Management"
6460
+ },
6461
+ // Payments
6462
+ payment: {
6463
+ name: "Payments",
6464
+ description: "Payment processing (Stripe, PayPal, Mobile Money)",
6465
+ category: "Payments"
6466
+ }
6467
+ };
6468
+ async function getInstalledModules() {
6469
+ try {
6470
+ const modulesDir = getModulesDir();
6471
+ const entries = await fs8.readdir(modulesDir, { withFileTypes: true });
6472
+ return entries.filter((e) => e.isDirectory()).map((e) => e.name);
6473
+ } catch {
6474
+ return [];
6475
+ }
6476
+ }
6477
+ function isServercraftProject() {
6478
+ try {
6479
+ getProjectRoot();
6480
+ return true;
6481
+ } catch {
6482
+ return false;
6483
+ }
6484
+ }
6485
+ 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(
6486
+ async (options) => {
6487
+ const installedModules = await getInstalledModules();
6488
+ const isProject = isServercraftProject();
6489
+ if (options.json) {
6490
+ const output = {
6491
+ available: Object.entries(AVAILABLE_MODULES2).map(([key, mod]) => ({
6492
+ id: key,
6493
+ ...mod,
6494
+ installed: installedModules.includes(key)
6495
+ }))
6496
+ };
6497
+ if (isProject) {
6498
+ output.installed = installedModules;
6499
+ }
6500
+ console.log(JSON.stringify(output, null, 2));
6501
+ return;
6502
+ }
6503
+ const byCategory = {};
6504
+ for (const [key, mod] of Object.entries(AVAILABLE_MODULES2)) {
6505
+ if (options.category && mod.category.toLowerCase() !== options.category.toLowerCase()) {
6506
+ continue;
6507
+ }
6508
+ if (!byCategory[mod.category]) {
6509
+ byCategory[mod.category] = [];
6510
+ }
6511
+ byCategory[mod.category].push({
6512
+ id: key,
6513
+ name: mod.name,
6514
+ description: mod.description,
6515
+ installed: installedModules.includes(key)
6516
+ });
6517
+ }
6518
+ if (options.installed) {
6519
+ if (!isProject) {
6520
+ console.log(chalk10.yellow("\n\u26A0 Not in a Servcraft project directory\n"));
6521
+ return;
6522
+ }
6523
+ console.log(chalk10.bold("\n\u{1F4E6} Installed Modules:\n"));
6524
+ if (installedModules.length === 0) {
6525
+ console.log(chalk10.gray(" No modules installed yet.\n"));
6526
+ console.log(` Run ${chalk10.cyan("servcraft add <module>")} to add a module.
6527
+ `);
6528
+ return;
6529
+ }
6530
+ for (const modId of installedModules) {
6531
+ const mod = AVAILABLE_MODULES2[modId];
6532
+ if (mod) {
6533
+ console.log(` ${chalk10.green("\u2713")} ${chalk10.cyan(modId.padEnd(18))} ${mod.name}`);
6534
+ } else {
6535
+ console.log(
6536
+ ` ${chalk10.green("\u2713")} ${chalk10.cyan(modId.padEnd(18))} ${chalk10.gray("(custom module)")}`
6537
+ );
6538
+ }
6539
+ }
6540
+ console.log(`
6541
+ Total: ${chalk10.bold(installedModules.length)} module(s) installed
6542
+ `);
6543
+ return;
6544
+ }
6545
+ console.log(chalk10.bold("\n\u{1F4E6} Available Modules\n"));
6546
+ if (isProject) {
6547
+ console.log(
6548
+ chalk10.gray(` ${chalk10.green("\u2713")} = installed ${chalk10.dim("\u25CB")} = not installed
6549
+ `)
6550
+ );
6551
+ }
6552
+ for (const [category, modules] of Object.entries(byCategory)) {
6553
+ console.log(chalk10.bold.blue(` ${category}`));
6554
+ console.log(chalk10.gray(" " + "\u2500".repeat(40)));
6555
+ for (const mod of modules) {
6556
+ const status = isProject ? mod.installed ? chalk10.green("\u2713") : chalk10.dim("\u25CB") : " ";
6557
+ const nameColor = mod.installed ? chalk10.green : chalk10.cyan;
6558
+ console.log(` ${status} ${nameColor(mod.id.padEnd(18))} ${mod.name}`);
6559
+ console.log(` ${chalk10.gray(mod.description)}`);
6560
+ }
6561
+ console.log();
6562
+ }
6563
+ const totalAvailable = Object.keys(AVAILABLE_MODULES2).length;
6564
+ const totalInstalled = installedModules.filter((m) => AVAILABLE_MODULES2[m]).length;
6565
+ console.log(chalk10.gray("\u2500".repeat(50)));
6566
+ console.log(
6567
+ ` ${chalk10.bold(totalAvailable)} modules available` + (isProject ? ` | ${chalk10.green.bold(totalInstalled)} installed` : "")
6568
+ );
6569
+ console.log();
6570
+ console.log(chalk10.bold(" Usage:"));
6571
+ console.log(` ${chalk10.yellow("servcraft add <module>")} Add a module`);
6572
+ console.log(` ${chalk10.yellow("servcraft list --installed")} Show installed only`);
6573
+ console.log(` ${chalk10.yellow("servcraft list --category Security")} Filter by category`);
6574
+ console.log();
6575
+ }
6576
+ );
6577
+
6578
+ // src/cli/commands/remove.ts
6579
+ init_esm_shims();
6580
+ import { Command as Command7 } from "commander";
6581
+ import path11 from "path";
6582
+ import ora7 from "ora";
6583
+ import chalk11 from "chalk";
6584
+ import fs9 from "fs/promises";
6585
+ import inquirer4 from "inquirer";
6586
+ init_error_handler();
6587
+ 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) => {
6588
+ const projectError = validateProject();
6589
+ if (projectError) {
6590
+ displayError(projectError);
6591
+ return;
6592
+ }
6593
+ console.log(chalk11.bold.cyan("\n\u{1F5D1}\uFE0F ServCraft Module Removal\n"));
6594
+ const moduleDir = path11.join(getModulesDir(), moduleName);
6595
+ try {
6596
+ const exists = await fs9.access(moduleDir).then(() => true).catch(() => false);
6597
+ if (!exists) {
6598
+ displayError(
6599
+ new (init_error_handler(), __toCommonJS(error_handler_exports)).ServCraftError(
6600
+ `Module "${moduleName}" is not installed`,
6601
+ [
6602
+ `Run ${chalk11.cyan("servcraft list --installed")} to see installed modules`,
6603
+ `Check the spelling of the module name`
6604
+ ]
6605
+ )
6606
+ );
6607
+ return;
6608
+ }
6609
+ const files = await fs9.readdir(moduleDir);
6610
+ const fileCount = files.length;
6611
+ if (!options?.yes) {
6612
+ console.log(chalk11.yellow(`\u26A0 This will remove the "${moduleName}" module:`));
6613
+ console.log(chalk11.gray(` Directory: ${moduleDir}`));
6614
+ console.log(chalk11.gray(` Files: ${fileCount} file(s)`));
6615
+ console.log();
6616
+ const { confirm } = await inquirer4.prompt([
6617
+ {
6618
+ type: "confirm",
6619
+ name: "confirm",
6620
+ message: "Are you sure you want to remove this module?",
6621
+ default: false
6622
+ }
6623
+ ]);
6624
+ if (!confirm) {
6625
+ console.log(chalk11.yellow("\n\u2716 Removal cancelled\n"));
6626
+ return;
6627
+ }
6628
+ }
6629
+ const spinner = ora7("Removing module...").start();
6630
+ await fs9.rm(moduleDir, { recursive: true, force: true });
6631
+ spinner.succeed(`Module "${moduleName}" removed successfully!`);
6632
+ console.log("\n" + chalk11.bold("\u2713 Removed:"));
6633
+ success(` src/modules/${moduleName}/ (${fileCount} files)`);
6634
+ if (!options?.keepEnv) {
6635
+ console.log("\n" + chalk11.bold("\u{1F4CC} Manual cleanup needed:"));
6636
+ info(" 1. Remove environment variables related to this module from .env");
6637
+ info(" 2. Remove module imports from your main app file");
6638
+ info(" 3. Remove related database migrations if any");
6639
+ info(" 4. Update your routes if they reference this module");
6640
+ } else {
6641
+ console.log("\n" + chalk11.bold("\u{1F4CC} Manual cleanup needed:"));
6642
+ info(" 1. Environment variables were kept (--keep-env flag)");
6643
+ info(" 2. Remove module imports from your main app file");
6644
+ info(" 3. Update your routes if they reference this module");
6645
+ }
6646
+ console.log();
6647
+ } catch (err) {
6648
+ error(err instanceof Error ? err.message : String(err));
6649
+ console.log();
6650
+ }
6651
+ });
6652
+
6653
+ // src/cli/commands/doctor.ts
6654
+ init_esm_shims();
6655
+ import { Command as Command8 } from "commander";
6656
+ import chalk12 from "chalk";
6657
+ var doctorCommand = new Command8("doctor").description("Diagnose project configuration and dependencies").action(async () => {
6658
+ console.log(chalk12.bold.cyan("\nServCraft Doctor - Coming soon!\n"));
6659
+ });
6660
+
3827
6661
  // src/cli/index.ts
3828
- var program = new Command5();
6662
+ var program = new Command9();
3829
6663
  program.name("servcraft").description("Servcraft - A modular Node.js backend framework CLI").version("0.1.0");
3830
6664
  program.addCommand(initCommand);
3831
6665
  program.addCommand(generateCommand);
3832
6666
  program.addCommand(addModuleCommand);
3833
6667
  program.addCommand(dbCommand);
6668
+ program.addCommand(docsCommand);
6669
+ program.addCommand(listCommand);
6670
+ program.addCommand(removeCommand);
6671
+ program.addCommand(doctorCommand);
3834
6672
  program.parse();
3835
6673
  //# sourceMappingURL=index.js.map