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