@forinda/kickjs-cli 2.0.0 → 2.1.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.
Files changed (116) hide show
  1. package/bin.js +8 -0
  2. package/dist/cli.mjs +5351 -0
  3. package/dist/index.d.mts +246 -0
  4. package/dist/index.d.mts.map +1 -0
  5. package/dist/index.mjs +3532 -0
  6. package/dist/index.mjs.map +1 -0
  7. package/package.json +31 -13
  8. package/dist/cli.d.ts +0 -2
  9. package/dist/cli.d.ts.map +0 -1
  10. package/dist/cli.js +0 -1552
  11. package/dist/commands/add.d.ts +0 -5
  12. package/dist/commands/add.d.ts.map +0 -1
  13. package/dist/commands/custom.d.ts +0 -56
  14. package/dist/commands/custom.d.ts.map +0 -1
  15. package/dist/commands/generate.d.ts +0 -3
  16. package/dist/commands/generate.d.ts.map +0 -1
  17. package/dist/commands/info.d.ts +0 -3
  18. package/dist/commands/info.d.ts.map +0 -1
  19. package/dist/commands/init.d.ts +0 -3
  20. package/dist/commands/init.d.ts.map +0 -1
  21. package/dist/commands/inspect.d.ts +0 -3
  22. package/dist/commands/inspect.d.ts.map +0 -1
  23. package/dist/commands/remove.d.ts +0 -3
  24. package/dist/commands/remove.d.ts.map +0 -1
  25. package/dist/commands/run.d.ts +0 -3
  26. package/dist/commands/run.d.ts.map +0 -1
  27. package/dist/commands/tinker.d.ts +0 -3
  28. package/dist/commands/tinker.d.ts.map +0 -1
  29. package/dist/config-D9faxBLQ.js +0 -3108
  30. package/dist/config.d.ts +0 -131
  31. package/dist/config.d.ts.map +0 -1
  32. package/dist/generators/adapter.d.ts +0 -7
  33. package/dist/generators/adapter.d.ts.map +0 -1
  34. package/dist/generators/config.d.ts +0 -9
  35. package/dist/generators/config.d.ts.map +0 -1
  36. package/dist/generators/controller.d.ts +0 -11
  37. package/dist/generators/controller.d.ts.map +0 -1
  38. package/dist/generators/dto.d.ts +0 -11
  39. package/dist/generators/dto.d.ts.map +0 -1
  40. package/dist/generators/guard.d.ts +0 -11
  41. package/dist/generators/guard.d.ts.map +0 -1
  42. package/dist/generators/job.d.ts +0 -8
  43. package/dist/generators/job.d.ts.map +0 -1
  44. package/dist/generators/middleware.d.ts +0 -11
  45. package/dist/generators/middleware.d.ts.map +0 -1
  46. package/dist/generators/module.d.ts +0 -33
  47. package/dist/generators/module.d.ts.map +0 -1
  48. package/dist/generators/patterns/cqrs.d.ts +0 -3
  49. package/dist/generators/patterns/cqrs.d.ts.map +0 -1
  50. package/dist/generators/patterns/ddd.d.ts +0 -3
  51. package/dist/generators/patterns/ddd.d.ts.map +0 -1
  52. package/dist/generators/patterns/index.d.ts +0 -6
  53. package/dist/generators/patterns/index.d.ts.map +0 -1
  54. package/dist/generators/patterns/minimal.d.ts +0 -3
  55. package/dist/generators/patterns/minimal.d.ts.map +0 -1
  56. package/dist/generators/patterns/rest.d.ts +0 -3
  57. package/dist/generators/patterns/rest.d.ts.map +0 -1
  58. package/dist/generators/patterns/types.d.ts +0 -15
  59. package/dist/generators/patterns/types.d.ts.map +0 -1
  60. package/dist/generators/project.d.ts +0 -14
  61. package/dist/generators/project.d.ts.map +0 -1
  62. package/dist/generators/remove-module.d.ts +0 -12
  63. package/dist/generators/remove-module.d.ts.map +0 -1
  64. package/dist/generators/resolver.d.ts +0 -7
  65. package/dist/generators/resolver.d.ts.map +0 -1
  66. package/dist/generators/scaffold.d.ts +0 -20
  67. package/dist/generators/scaffold.d.ts.map +0 -1
  68. package/dist/generators/service.d.ts +0 -11
  69. package/dist/generators/service.d.ts.map +0 -1
  70. package/dist/generators/templates/constants.d.ts +0 -3
  71. package/dist/generators/templates/constants.d.ts.map +0 -1
  72. package/dist/generators/templates/controller.d.ts +0 -6
  73. package/dist/generators/templates/controller.d.ts.map +0 -1
  74. package/dist/generators/templates/cqrs.d.ts +0 -23
  75. package/dist/generators/templates/cqrs.d.ts.map +0 -1
  76. package/dist/generators/templates/domain.d.ts +0 -5
  77. package/dist/generators/templates/domain.d.ts.map +0 -1
  78. package/dist/generators/templates/drizzle/index.d.ts +0 -4
  79. package/dist/generators/templates/drizzle/index.d.ts.map +0 -1
  80. package/dist/generators/templates/dtos.d.ts +0 -5
  81. package/dist/generators/templates/dtos.d.ts.map +0 -1
  82. package/dist/generators/templates/index.d.ts +0 -14
  83. package/dist/generators/templates/index.d.ts.map +0 -1
  84. package/dist/generators/templates/module-index.d.ts +0 -13
  85. package/dist/generators/templates/module-index.d.ts.map +0 -1
  86. package/dist/generators/templates/prisma/index.d.ts +0 -3
  87. package/dist/generators/templates/prisma/index.d.ts.map +0 -1
  88. package/dist/generators/templates/project-app.d.ts +0 -9
  89. package/dist/generators/templates/project-app.d.ts.map +0 -1
  90. package/dist/generators/templates/project-config.d.ts +0 -23
  91. package/dist/generators/templates/project-config.d.ts.map +0 -1
  92. package/dist/generators/templates/project-docs.d.ts +0 -9
  93. package/dist/generators/templates/project-docs.d.ts.map +0 -1
  94. package/dist/generators/templates/repository.d.ts +0 -5
  95. package/dist/generators/templates/repository.d.ts.map +0 -1
  96. package/dist/generators/templates/rest-service.d.ts +0 -6
  97. package/dist/generators/templates/rest-service.d.ts.map +0 -1
  98. package/dist/generators/templates/tests.d.ts +0 -4
  99. package/dist/generators/templates/tests.d.ts.map +0 -1
  100. package/dist/generators/templates/types.d.ts +0 -20
  101. package/dist/generators/templates/types.d.ts.map +0 -1
  102. package/dist/generators/templates/use-cases.d.ts +0 -6
  103. package/dist/generators/templates/use-cases.d.ts.map +0 -1
  104. package/dist/generators/test.d.ts +0 -9
  105. package/dist/generators/test.d.ts.map +0 -1
  106. package/dist/index.d.ts +0 -12
  107. package/dist/index.d.ts.map +0 -1
  108. package/dist/index.js +0 -17
  109. package/dist/utils/fs.d.ts +0 -11
  110. package/dist/utils/fs.d.ts.map +0 -1
  111. package/dist/utils/naming.d.ts +0 -18
  112. package/dist/utils/naming.d.ts.map +0 -1
  113. package/dist/utils/resolve-out-dir.d.ts +0 -25
  114. package/dist/utils/resolve-out-dir.d.ts.map +0 -1
  115. package/dist/utils/shell.d.ts +0 -3
  116. package/dist/utils/shell.d.ts.map +0 -1
package/dist/index.mjs ADDED
@@ -0,0 +1,3532 @@
1
+ /**
2
+ * @forinda/kickjs-cli v2.1.0
3
+ *
4
+ * Copyright (c) Felix Orinda
5
+ *
6
+ * This source code is licensed under the MIT license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ *
9
+ * @license MIT
10
+ */
11
+ import { dirname, join, resolve } from "node:path";
12
+ import { createInterface } from "node:readline";
13
+ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
14
+ import { execSync } from "node:child_process";
15
+ import { readFileSync } from "node:fs";
16
+ import { fileURLToPath } from "node:url";
17
+ /** Write a file, creating parent directories if needed. Skips writing in dry run mode. */
18
+ async function writeFileSafe(filePath, content) {
19
+ await mkdir(dirname(filePath), { recursive: true });
20
+ await writeFile(filePath, content, "utf-8");
21
+ }
22
+ /** Check if a file exists */
23
+ async function fileExists(filePath) {
24
+ try {
25
+ await access(filePath);
26
+ return true;
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+ //#endregion
32
+ //#region src/utils/naming.ts
33
+ /** Convert a name to PascalCase */
34
+ function toPascalCase(name) {
35
+ return name.replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^(.)/, (c) => c.toUpperCase());
36
+ }
37
+ /** Convert a name to camelCase */
38
+ function toCamelCase(name) {
39
+ const pascal = toPascalCase(name);
40
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
41
+ }
42
+ /** Convert a name to kebab-case */
43
+ function toKebabCase(name) {
44
+ return name.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
45
+ }
46
+ /**
47
+ * Pluralize a kebab-case name for directory/file names.
48
+ * If already plural (ends in 's'), returns as-is.
49
+ */
50
+ function pluralize(name) {
51
+ if (name.endsWith("s")) return name;
52
+ if (name.endsWith("x") || name.endsWith("z")) return name + "es";
53
+ if (name.endsWith("sh") || name.endsWith("ch")) return name + "es";
54
+ if (name.endsWith("y") && !/[aeiou]y$/.test(name)) return name.slice(0, -1) + "ies";
55
+ return name + "s";
56
+ }
57
+ /**
58
+ * Pluralize a PascalCase name for class identifiers.
59
+ * If already plural (ends in 's'), returns as-is.
60
+ * Used for `List${pluralPascal}UseCase` to avoid `ListUserssUseCase`.
61
+ */
62
+ function pluralizePascal(name) {
63
+ if (name.endsWith("s")) return name;
64
+ if (name.endsWith("x") || name.endsWith("z")) return name + "es";
65
+ if (name.endsWith("sh") || name.endsWith("ch")) return name + "es";
66
+ if (name.endsWith("y") && !/[aeiou]y$/i.test(name)) return name.slice(0, -1) + "ies";
67
+ return name + "s";
68
+ }
69
+ //#endregion
70
+ //#region src/generators/templates/module-index.ts
71
+ const repoLabelMap = {
72
+ inmemory: "in-memory",
73
+ drizzle: "Drizzle",
74
+ prisma: "Prisma"
75
+ };
76
+ function toPascalRepoType(repo) {
77
+ return repo.charAt(0).toUpperCase() + repo.slice(1).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
78
+ }
79
+ function toKebabRepoType(repo) {
80
+ return repo.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
81
+ }
82
+ function repoLabel(repo) {
83
+ return repoLabelMap[repo] ?? toPascalRepoType(repo);
84
+ }
85
+ function repoMaps(pascal, kebab, repo) {
86
+ const repoClassMap = {
87
+ inmemory: `InMemory${pascal}Repository`,
88
+ drizzle: `Drizzle${pascal}Repository`,
89
+ prisma: `Prisma${pascal}Repository`
90
+ };
91
+ const repoFileMap = {
92
+ inmemory: `in-memory-${kebab}`,
93
+ drizzle: `drizzle-${kebab}`,
94
+ prisma: `prisma-${kebab}`
95
+ };
96
+ return {
97
+ repoClass: repoClassMap[repo] ?? `${toPascalRepoType(repo)}${pascal}Repository`,
98
+ repoFile: repoFileMap[repo] ?? `${toKebabRepoType(repo)}-${kebab}`
99
+ };
100
+ }
101
+ /** DDD module index — nested folders, use-cases, domain services */
102
+ function generateModuleIndex(ctx) {
103
+ const { pascal, kebab, plural = "", repo } = ctx;
104
+ const { repoClass, repoFile } = repoMaps(pascal, kebab, repo);
105
+ return `/**
106
+ * ${pascal} Module
107
+ *
108
+ * Self-contained feature module following Domain-Driven Design (DDD).
109
+ * Registers dependencies in the DI container and declares HTTP routes.
110
+ *
111
+ * Structure:
112
+ * presentation/ — HTTP controllers (entry points)
113
+ * application/ — Use cases (orchestration) and DTOs (validation)
114
+ * domain/ — Entities, value objects, repository interfaces, domain services
115
+ * infrastructure/ — Repository implementations (currently ${repoLabel(repo)})
116
+ */
117
+ import { Container, type AppModule, type ModuleRoutes } from '@forinda/kickjs'
118
+ import { buildRoutes } from '@forinda/kickjs'
119
+ import { ${pascal.toUpperCase()}_REPOSITORY } from './domain/repositories/${kebab}.repository'
120
+ import { ${repoClass} } from './infrastructure/repositories/${repoFile}.repository'
121
+ import { ${pascal}Controller } from './presentation/${kebab}.controller'
122
+
123
+ // Eagerly load decorated classes so @Service()/@Repository() decorators register in the DI container
124
+ import.meta.glob(
125
+ ['./domain/services/**/*.ts', './application/use-cases/**/*.ts', '!./**/*.test.ts'],
126
+ { eager: true },
127
+ )
128
+
129
+ export class ${pascal}Module implements AppModule {
130
+ /**
131
+ * Register module dependencies in the DI container.
132
+ * Bind repository interface tokens to their implementations here.
133
+ * Currently wired to ${repoLabel(repo)}. To swap implementations, change the factory target.
134
+ */
135
+ register(container: Container): void {
136
+ container.registerFactory(${pascal.toUpperCase()}_REPOSITORY, () =>
137
+ container.resolve(${repoClass}),
138
+ )
139
+ }
140
+
141
+ /**
142
+ * Declare HTTP routes for this module.
143
+ * The path is prefixed with the global apiPrefix and version (e.g. /api/v1/${plural}).
144
+ * Passing 'controller' enables automatic OpenAPI spec generation via SwaggerAdapter.
145
+ */
146
+ routes(): ModuleRoutes {
147
+ return {
148
+ path: '/${plural}',
149
+ router: buildRoutes(${pascal}Controller),
150
+ controller: ${pascal}Controller,
151
+ }
152
+ }
153
+ }
154
+ `;
155
+ }
156
+ /** REST module index — flat folder, service + controller, no use-cases */
157
+ function generateRestModuleIndex(ctx) {
158
+ const { pascal, kebab, plural = "", repo } = ctx;
159
+ const { repoClass, repoFile } = repoMaps(pascal, kebab, repo);
160
+ return `/**
161
+ * ${pascal} Module
162
+ *
163
+ * REST module with a flat folder structure.
164
+ * Controller delegates to service, service wraps the repository.
165
+ *
166
+ * Structure:
167
+ * ${kebab}.controller.ts — HTTP routes (CRUD)
168
+ * ${kebab}.service.ts — Business logic
169
+ * ${kebab}.repository.ts — Repository interface
170
+ * ${repoFile}.repository.ts — Repository implementation
171
+ * dtos/ — Request/response schemas
172
+ */
173
+ import { Container, type AppModule, type ModuleRoutes } from '@forinda/kickjs'
174
+ import { buildRoutes } from '@forinda/kickjs'
175
+ import { ${pascal.toUpperCase()}_REPOSITORY } from './${kebab}.repository'
176
+ import { ${repoClass} } from './${repoFile}.repository'
177
+ import { ${pascal}Controller } from './${kebab}.controller'
178
+
179
+ // Eagerly load decorated classes so @Service()/@Repository() decorators register in the DI container
180
+ import.meta.glob(['./**/*.service.ts', './**/*.repository.ts', '!./**/*.test.ts'], { eager: true })
181
+
182
+ export class ${pascal}Module implements AppModule {
183
+ register(container: Container): void {
184
+ container.registerFactory(${pascal.toUpperCase()}_REPOSITORY, () =>
185
+ container.resolve(${repoClass}),
186
+ )
187
+ }
188
+
189
+ routes(): ModuleRoutes {
190
+ return {
191
+ path: '/${plural}',
192
+ router: buildRoutes(${pascal}Controller),
193
+ controller: ${pascal}Controller,
194
+ }
195
+ }
196
+ }
197
+ `;
198
+ }
199
+ /** Minimal module index — just controller, no service/repo */
200
+ function generateMinimalModuleIndex(ctx) {
201
+ const { pascal, kebab, plural = "" } = ctx;
202
+ return `import { type AppModule, type ModuleRoutes } from '@forinda/kickjs'
203
+ import { buildRoutes } from '@forinda/kickjs'
204
+ import { ${pascal}Controller } from './${kebab}.controller'
205
+
206
+ export class ${pascal}Module implements AppModule {
207
+ routes(): ModuleRoutes {
208
+ return {
209
+ path: '/${plural}',
210
+ router: buildRoutes(${pascal}Controller),
211
+ controller: ${pascal}Controller,
212
+ }
213
+ }
214
+ }
215
+ `;
216
+ }
217
+ //#endregion
218
+ //#region src/generators/templates/controller.ts
219
+ /** DDD controller — injects use-cases, nested import paths */
220
+ function generateController$1(ctx) {
221
+ const { pascal, kebab, plural = "", pluralPascal = "" } = ctx;
222
+ return `import { Controller, Get, Post, Put, Delete, Autowired, ApiQueryParams } from '@forinda/kickjs'
223
+ import type { RequestContext } from '@forinda/kickjs'
224
+ import { ApiTags } from '@forinda/kickjs-swagger'
225
+ import { Create${pascal}UseCase } from '../application/use-cases/create-${kebab}.use-case'
226
+ import { Get${pascal}UseCase } from '../application/use-cases/get-${kebab}.use-case'
227
+ import { List${pluralPascal}UseCase } from '../application/use-cases/list-${plural}.use-case'
228
+ import { Update${pascal}UseCase } from '../application/use-cases/update-${kebab}.use-case'
229
+ import { Delete${pascal}UseCase } from '../application/use-cases/delete-${kebab}.use-case'
230
+ import { create${pascal}Schema } from '../application/dtos/create-${kebab}.dto'
231
+ import { update${pascal}Schema } from '../application/dtos/update-${kebab}.dto'
232
+ import { ${pascal.toUpperCase()}_QUERY_CONFIG } from '../constants'
233
+
234
+ @Controller()
235
+ export class ${pascal}Controller {
236
+ @Autowired() private create${pascal}UseCase!: Create${pascal}UseCase
237
+ @Autowired() private get${pascal}UseCase!: Get${pascal}UseCase
238
+ @Autowired() private list${pluralPascal}UseCase!: List${pluralPascal}UseCase
239
+ @Autowired() private update${pascal}UseCase!: Update${pascal}UseCase
240
+ @Autowired() private delete${pascal}UseCase!: Delete${pascal}UseCase
241
+
242
+ @Get('/')
243
+ @ApiTags('${pascal}')
244
+ @ApiQueryParams(${pascal.toUpperCase()}_QUERY_CONFIG)
245
+ async list(ctx: RequestContext) {
246
+ return ctx.paginate(
247
+ (parsed) => this.list${pluralPascal}UseCase.execute(parsed),
248
+ ${pascal.toUpperCase()}_QUERY_CONFIG,
249
+ )
250
+ }
251
+
252
+ @Get('/:id')
253
+ @ApiTags('${pascal}')
254
+ async getById(ctx: RequestContext) {
255
+ const result = await this.get${pascal}UseCase.execute(ctx.params.id)
256
+ if (!result) return ctx.notFound('${pascal} not found')
257
+ ctx.json(result)
258
+ }
259
+
260
+ @Post('/', { body: create${pascal}Schema, name: 'Create${pascal}' })
261
+ @ApiTags('${pascal}')
262
+ async create(ctx: RequestContext) {
263
+ const result = await this.create${pascal}UseCase.execute(ctx.body)
264
+ ctx.created(result)
265
+ }
266
+
267
+ @Put('/:id', { body: update${pascal}Schema, name: 'Update${pascal}' })
268
+ @ApiTags('${pascal}')
269
+ async update(ctx: RequestContext) {
270
+ const result = await this.update${pascal}UseCase.execute(ctx.params.id, ctx.body)
271
+ ctx.json(result)
272
+ }
273
+
274
+ @Delete('/:id')
275
+ @ApiTags('${pascal}')
276
+ async remove(ctx: RequestContext) {
277
+ await this.delete${pascal}UseCase.execute(ctx.params.id)
278
+ ctx.noContent()
279
+ }
280
+ }
281
+ `;
282
+ }
283
+ /** REST controller — injects service directly, flat import paths */
284
+ function generateRestController(ctx) {
285
+ const { pascal, kebab, plural = "", pluralPascal = "" } = ctx;
286
+ const camel = pascal.charAt(0).toLowerCase() + pascal.slice(1);
287
+ return `import { Controller, Get, Post, Put, Delete, Autowired, ApiQueryParams } from '@forinda/kickjs'
288
+ import type { RequestContext } from '@forinda/kickjs'
289
+ import { ApiTags } from '@forinda/kickjs-swagger'
290
+ import { ${pascal}Service } from './${kebab}.service'
291
+ import { create${pascal}Schema } from './dtos/create-${kebab}.dto'
292
+ import { update${pascal}Schema } from './dtos/update-${kebab}.dto'
293
+ import { ${pascal.toUpperCase()}_QUERY_CONFIG } from './${kebab}.constants'
294
+
295
+ @Controller()
296
+ export class ${pascal}Controller {
297
+ @Autowired() private ${camel}Service!: ${pascal}Service
298
+
299
+ @Get('/')
300
+ @ApiTags('${pascal}')
301
+ @ApiQueryParams(${pascal.toUpperCase()}_QUERY_CONFIG)
302
+ async list(ctx: RequestContext) {
303
+ return ctx.paginate(
304
+ (parsed) => this.${camel}Service.findPaginated(parsed),
305
+ ${pascal.toUpperCase()}_QUERY_CONFIG,
306
+ )
307
+ }
308
+
309
+ @Get('/:id')
310
+ @ApiTags('${pascal}')
311
+ async getById(ctx: RequestContext) {
312
+ const result = await this.${camel}Service.findById(ctx.params.id)
313
+ if (!result) return ctx.notFound('${pascal} not found')
314
+ ctx.json(result)
315
+ }
316
+
317
+ @Post('/', { body: create${pascal}Schema, name: 'Create${pascal}' })
318
+ @ApiTags('${pascal}')
319
+ async create(ctx: RequestContext) {
320
+ const result = await this.${camel}Service.create(ctx.body)
321
+ ctx.created(result)
322
+ }
323
+
324
+ @Put('/:id', { body: update${pascal}Schema, name: 'Update${pascal}' })
325
+ @ApiTags('${pascal}')
326
+ async update(ctx: RequestContext) {
327
+ const result = await this.${camel}Service.update(ctx.params.id, ctx.body)
328
+ ctx.json(result)
329
+ }
330
+
331
+ @Delete('/:id')
332
+ @ApiTags('${pascal}')
333
+ async remove(ctx: RequestContext) {
334
+ await this.${camel}Service.delete(ctx.params.id)
335
+ ctx.noContent()
336
+ }
337
+ }
338
+ `;
339
+ }
340
+ //#endregion
341
+ //#region src/generators/templates/constants.ts
342
+ function generateConstants(ctx) {
343
+ const { pascal } = ctx;
344
+ return `import type { QueryParamsConfig } from '@forinda/kickjs'
345
+
346
+ export const ${pascal.toUpperCase()}_QUERY_CONFIG: QueryParamsConfig = {
347
+ filterable: ['name'],
348
+ sortable: ['name', 'createdAt'],
349
+ searchable: ['name'],
350
+ }
351
+ `;
352
+ }
353
+ //#endregion
354
+ //#region src/generators/templates/dtos.ts
355
+ function generateCreateDTO(ctx) {
356
+ const { pascal, kebab } = ctx;
357
+ return `import { z } from 'zod'
358
+
359
+ /**
360
+ * Create ${pascal} DTO — Zod schema for validating POST request bodies.
361
+ * This schema is passed to @Post('/', { body: create${pascal}Schema }) for automatic validation.
362
+ * It also generates OpenAPI request body docs when SwaggerAdapter is used.
363
+ *
364
+ * Add more fields as needed. Supported Zod types:
365
+ * z.string(), z.number(), z.boolean(), z.enum([...]),
366
+ * z.array(), z.object(), .optional(), .default(), .transform()
367
+ */
368
+ export const create${pascal}Schema = z.object({
369
+ name: z.string().min(1, 'Name is required').max(200),
370
+ })
371
+
372
+ export type Create${pascal}DTO = z.infer<typeof create${pascal}Schema>
373
+ `;
374
+ }
375
+ function generateUpdateDTO(ctx) {
376
+ const { pascal, kebab } = ctx;
377
+ return `import { z } from 'zod'
378
+
379
+ export const update${pascal}Schema = z.object({
380
+ name: z.string().min(1).max(200).optional(),
381
+ })
382
+
383
+ export type Update${pascal}DTO = z.infer<typeof update${pascal}Schema>
384
+ `;
385
+ }
386
+ function generateResponseDTO(ctx) {
387
+ const { pascal, kebab } = ctx;
388
+ return `export interface ${pascal}ResponseDTO {
389
+ id: string
390
+ name: string
391
+ createdAt: string
392
+ updatedAt: string
393
+ }
394
+ `;
395
+ }
396
+ //#endregion
397
+ //#region src/generators/templates/use-cases.ts
398
+ function generateUseCases(ctx) {
399
+ const { pascal, kebab, plural = "", pluralPascal = "" } = ctx;
400
+ return [
401
+ {
402
+ file: `create-${kebab}.use-case.ts`,
403
+ content: `/**
404
+ * Create ${pascal} Use Case
405
+ *
406
+ * Application layer — orchestrates a single business operation.
407
+ * Use cases are thin: validate input (via DTO), call domain/repo, return response.
408
+ * Keep business rules in the domain service, not here.
409
+ */
410
+ import { Service, Inject } from '@forinda/kickjs'
411
+ import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../../domain/repositories/${kebab}.repository'
412
+ import type { Create${pascal}DTO } from '../dtos/create-${kebab}.dto'
413
+ import type { ${pascal}ResponseDTO } from '../dtos/${kebab}-response.dto'
414
+
415
+ @Service()
416
+ export class Create${pascal}UseCase {
417
+ constructor(
418
+ @Inject(${pascal.toUpperCase()}_REPOSITORY) private readonly repo: I${pascal}Repository,
419
+ ) {}
420
+
421
+ async execute(dto: Create${pascal}DTO): Promise<${pascal}ResponseDTO> {
422
+ return this.repo.create(dto)
423
+ }
424
+ }
425
+ `
426
+ },
427
+ {
428
+ file: `get-${kebab}.use-case.ts`,
429
+ content: `import { Service, Inject } from '@forinda/kickjs'
430
+ import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../../domain/repositories/${kebab}.repository'
431
+ import type { ${pascal}ResponseDTO } from '../dtos/${kebab}-response.dto'
432
+
433
+ @Service()
434
+ export class Get${pascal}UseCase {
435
+ constructor(
436
+ @Inject(${pascal.toUpperCase()}_REPOSITORY) private readonly repo: I${pascal}Repository,
437
+ ) {}
438
+
439
+ async execute(id: string): Promise<${pascal}ResponseDTO | null> {
440
+ return this.repo.findById(id)
441
+ }
442
+ }
443
+ `
444
+ },
445
+ {
446
+ file: `list-${plural}.use-case.ts`,
447
+ content: `import { Service, Inject } from '@forinda/kickjs'
448
+ import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../../domain/repositories/${kebab}.repository'
449
+ import type { ParsedQuery } from '@forinda/kickjs'
450
+
451
+ @Service()
452
+ export class List${pluralPascal}UseCase {
453
+ constructor(
454
+ @Inject(${pascal.toUpperCase()}_REPOSITORY) private readonly repo: I${pascal}Repository,
455
+ ) {}
456
+
457
+ async execute(parsed: ParsedQuery) {
458
+ return this.repo.findPaginated(parsed)
459
+ }
460
+ }
461
+ `
462
+ },
463
+ {
464
+ file: `update-${kebab}.use-case.ts`,
465
+ content: `import { Service, Inject } from '@forinda/kickjs'
466
+ import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../../domain/repositories/${kebab}.repository'
467
+ import type { Update${pascal}DTO } from '../dtos/update-${kebab}.dto'
468
+ import type { ${pascal}ResponseDTO } from '../dtos/${kebab}-response.dto'
469
+
470
+ @Service()
471
+ export class Update${pascal}UseCase {
472
+ constructor(
473
+ @Inject(${pascal.toUpperCase()}_REPOSITORY) private readonly repo: I${pascal}Repository,
474
+ ) {}
475
+
476
+ async execute(id: string, dto: Update${pascal}DTO): Promise<${pascal}ResponseDTO> {
477
+ return this.repo.update(id, dto)
478
+ }
479
+ }
480
+ `
481
+ },
482
+ {
483
+ file: `delete-${kebab}.use-case.ts`,
484
+ content: `import { Service, Inject } from '@forinda/kickjs'
485
+ import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../../domain/repositories/${kebab}.repository'
486
+
487
+ @Service()
488
+ export class Delete${pascal}UseCase {
489
+ constructor(
490
+ @Inject(${pascal.toUpperCase()}_REPOSITORY) private readonly repo: I${pascal}Repository,
491
+ ) {}
492
+
493
+ async execute(id: string): Promise<void> {
494
+ await this.repo.delete(id)
495
+ }
496
+ }
497
+ `
498
+ }
499
+ ];
500
+ }
501
+ //#endregion
502
+ //#region src/generators/templates/repository.ts
503
+ function generateRepositoryInterface(ctx) {
504
+ const { pascal, kebab, dtoPrefix = "../../application/dtos" } = ctx;
505
+ return `/**
506
+ * ${pascal} Repository Interface
507
+ *
508
+ * Defines the contract for data access.
509
+ * The interface declares what operations are available;
510
+ * implementations (in-memory, Drizzle, Prisma) fulfill the contract.
511
+ *
512
+ * To swap implementations, change the factory in the module's register() method.
513
+ */
514
+ import type { ${pascal}ResponseDTO } from '${dtoPrefix}/${kebab}-response.dto'
515
+ import type { Create${pascal}DTO } from '${dtoPrefix}/create-${kebab}.dto'
516
+ import type { Update${pascal}DTO } from '${dtoPrefix}/update-${kebab}.dto'
517
+ import type { ParsedQuery } from '@forinda/kickjs'
518
+
519
+ export interface I${pascal}Repository {
520
+ findById(id: string): Promise<${pascal}ResponseDTO | null>
521
+ findAll(): Promise<${pascal}ResponseDTO[]>
522
+ findPaginated(parsed: ParsedQuery): Promise<{ data: ${pascal}ResponseDTO[]; total: number }>
523
+ create(dto: Create${pascal}DTO): Promise<${pascal}ResponseDTO>
524
+ update(id: string, dto: Update${pascal}DTO): Promise<${pascal}ResponseDTO>
525
+ delete(id: string): Promise<void>
526
+ }
527
+
528
+ export const ${pascal.toUpperCase()}_REPOSITORY = Symbol('I${pascal}Repository')
529
+ `;
530
+ }
531
+ function generateInMemoryRepository(ctx) {
532
+ const { pascal, kebab, repoPrefix = "../../domain/repositories", dtoPrefix = "../../application/dtos" } = ctx;
533
+ return `/**
534
+ * In-Memory ${pascal} Repository
535
+ *
536
+ * Implements the repository interface using a Map.
537
+ * Useful for prototyping and testing. Replace with a database implementation
538
+ * (Drizzle, Prisma, etc.) for production use.
539
+ *
540
+ * @Repository() registers this class in the DI container as a singleton.
541
+ */
542
+ import { randomUUID } from 'node:crypto'
543
+ import { Repository, HttpException } from '@forinda/kickjs'
544
+ import type { ParsedQuery } from '@forinda/kickjs'
545
+ import type { I${pascal}Repository } from '${repoPrefix}/${kebab}.repository'
546
+ import type { ${pascal}ResponseDTO } from '${dtoPrefix}/${kebab}-response.dto'
547
+ import type { Create${pascal}DTO } from '${dtoPrefix}/create-${kebab}.dto'
548
+ import type { Update${pascal}DTO } from '${dtoPrefix}/update-${kebab}.dto'
549
+
550
+ @Repository()
551
+ export class InMemory${pascal}Repository implements I${pascal}Repository {
552
+ private store = new Map<string, ${pascal}ResponseDTO>()
553
+
554
+ async findById(id: string): Promise<${pascal}ResponseDTO | null> {
555
+ return this.store.get(id) ?? null
556
+ }
557
+
558
+ async findAll(): Promise<${pascal}ResponseDTO[]> {
559
+ return Array.from(this.store.values())
560
+ }
561
+
562
+ async findPaginated(parsed: ParsedQuery): Promise<{ data: ${pascal}ResponseDTO[]; total: number }> {
563
+ const all = Array.from(this.store.values())
564
+ const data = all.slice(parsed.pagination.offset, parsed.pagination.offset + parsed.pagination.limit)
565
+ return { data, total: all.length }
566
+ }
567
+
568
+ async create(dto: Create${pascal}DTO): Promise<${pascal}ResponseDTO> {
569
+ const now = new Date().toISOString()
570
+ const entity: ${pascal}ResponseDTO = {
571
+ id: randomUUID(),
572
+ name: dto.name,
573
+ createdAt: now,
574
+ updatedAt: now,
575
+ }
576
+ this.store.set(entity.id, entity)
577
+ return entity
578
+ }
579
+
580
+ async update(id: string, dto: Update${pascal}DTO): Promise<${pascal}ResponseDTO> {
581
+ const existing = this.store.get(id)
582
+ if (!existing) throw HttpException.notFound('${pascal} not found')
583
+ const updated = { ...existing, ...dto, updatedAt: new Date().toISOString() }
584
+ this.store.set(id, updated)
585
+ return updated
586
+ }
587
+
588
+ async delete(id: string): Promise<void> {
589
+ if (!this.store.has(id)) throw HttpException.notFound('${pascal} not found')
590
+ this.store.delete(id)
591
+ }
592
+ }
593
+ `;
594
+ }
595
+ function generateCustomRepository(ctx) {
596
+ const { pascal, kebab, repoType = "", repoPrefix = "../../domain/repositories", dtoPrefix = "../../application/dtos" } = ctx;
597
+ const repoTypePascal = repoType.charAt(0).toUpperCase() + repoType.slice(1).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
598
+ return `/**
599
+ * ${repoTypePascal} ${pascal} Repository
600
+ *
601
+ * Stub implementation for a custom '${repoType}' repository.
602
+ * Implements the repository interface using an in-memory Map as a placeholder.
603
+ *
604
+ * TODO: Replace the in-memory Map with your ${repoType} data-access logic.
605
+ * See I${pascal}Repository for the interface contract.
606
+ *
607
+ * @Repository() registers this class in the DI container as a singleton.
608
+ */
609
+ import { randomUUID } from 'node:crypto'
610
+ import { Repository, HttpException } from '@forinda/kickjs'
611
+ import type { ParsedQuery } from '@forinda/kickjs'
612
+ import type { I${pascal}Repository } from '${repoPrefix}/${kebab}.repository'
613
+ import type { ${pascal}ResponseDTO } from '${dtoPrefix}/${kebab}-response.dto'
614
+ import type { Create${pascal}DTO } from '${dtoPrefix}/create-${kebab}.dto'
615
+ import type { Update${pascal}DTO } from '${dtoPrefix}/update-${kebab}.dto'
616
+
617
+ @Repository()
618
+ export class ${repoTypePascal}${pascal}Repository implements I${pascal}Repository {
619
+ // TODO: Replace with your ${repoType} client/connection
620
+ private store = new Map<string, ${pascal}ResponseDTO>()
621
+
622
+ async findById(id: string): Promise<${pascal}ResponseDTO | null> {
623
+ // TODO: Implement with ${repoType}
624
+ return this.store.get(id) ?? null
625
+ }
626
+
627
+ async findAll(): Promise<${pascal}ResponseDTO[]> {
628
+ // TODO: Implement with ${repoType}
629
+ return Array.from(this.store.values())
630
+ }
631
+
632
+ async findPaginated(parsed: ParsedQuery): Promise<{ data: ${pascal}ResponseDTO[]; total: number }> {
633
+ // TODO: Implement with ${repoType}
634
+ const all = Array.from(this.store.values())
635
+ const data = all.slice(parsed.pagination.offset, parsed.pagination.offset + parsed.pagination.limit)
636
+ return { data, total: all.length }
637
+ }
638
+
639
+ async create(dto: Create${pascal}DTO): Promise<${pascal}ResponseDTO> {
640
+ // TODO: Implement with ${repoType}
641
+ const now = new Date().toISOString()
642
+ const entity: ${pascal}ResponseDTO = {
643
+ id: randomUUID(),
644
+ name: dto.name,
645
+ createdAt: now,
646
+ updatedAt: now,
647
+ }
648
+ this.store.set(entity.id, entity)
649
+ return entity
650
+ }
651
+
652
+ async update(id: string, dto: Update${pascal}DTO): Promise<${pascal}ResponseDTO> {
653
+ // TODO: Implement with ${repoType}
654
+ const existing = this.store.get(id)
655
+ if (!existing) throw HttpException.notFound('${pascal} not found')
656
+ const updated = { ...existing, ...dto, updatedAt: new Date().toISOString() }
657
+ this.store.set(id, updated)
658
+ return updated
659
+ }
660
+
661
+ async delete(id: string): Promise<void> {
662
+ // TODO: Implement with ${repoType}
663
+ if (!this.store.has(id)) throw HttpException.notFound('${pascal} not found')
664
+ this.store.delete(id)
665
+ }
666
+ }
667
+ `;
668
+ }
669
+ //#endregion
670
+ //#region src/generators/templates/domain.ts
671
+ function generateDomainService(ctx) {
672
+ const { pascal, kebab } = ctx;
673
+ return `/**
674
+ * ${pascal} Domain Service
675
+ *
676
+ * Domain layer — contains business rules that don't belong to a single entity.
677
+ * Use this for cross-entity logic, validation rules, and domain invariants.
678
+ * Keep it free of HTTP/framework concerns.
679
+ */
680
+ import { Service, Inject, HttpException } from '@forinda/kickjs'
681
+ import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../repositories/${kebab}.repository'
682
+
683
+ @Service()
684
+ export class ${pascal}DomainService {
685
+ constructor(
686
+ @Inject(${pascal.toUpperCase()}_REPOSITORY) private readonly repo: I${pascal}Repository,
687
+ ) {}
688
+
689
+ async ensureExists(id: string): Promise<void> {
690
+ const entity = await this.repo.findById(id)
691
+ if (!entity) {
692
+ throw HttpException.notFound('${pascal} not found')
693
+ }
694
+ }
695
+ }
696
+ `;
697
+ }
698
+ function generateEntity(ctx) {
699
+ const { pascal, kebab } = ctx;
700
+ return `/**
701
+ * ${pascal} Entity
702
+ *
703
+ * Domain layer — the core business object.
704
+ * Uses a private constructor with static factory methods (create, reconstitute)
705
+ * to enforce invariants. Properties are accessed via getters to maintain encapsulation.
706
+ *
707
+ * Patterns used:
708
+ * - Private constructor: prevents direct instantiation
709
+ * - create(): factory for new entities (generates ID, sets timestamps)
710
+ * - reconstitute(): factory for rebuilding from persistence (no side effects)
711
+ * - changeName(): mutation method that enforces business rules
712
+ */
713
+ import { ${pascal}Id } from '../value-objects/${kebab}-id.vo'
714
+
715
+ interface ${pascal}Props {
716
+ id: ${pascal}Id
717
+ name: string
718
+ createdAt: Date
719
+ updatedAt: Date
720
+ }
721
+
722
+ export class ${pascal} {
723
+ private constructor(private props: ${pascal}Props) {}
724
+
725
+ static create(params: { name: string }): ${pascal} {
726
+ const now = new Date()
727
+ return new ${pascal}({
728
+ id: ${pascal}Id.create(),
729
+ name: params.name,
730
+ createdAt: now,
731
+ updatedAt: now,
732
+ })
733
+ }
734
+
735
+ static reconstitute(props: ${pascal}Props): ${pascal} {
736
+ return new ${pascal}(props)
737
+ }
738
+
739
+ get id(): ${pascal}Id {
740
+ return this.props.id
741
+ }
742
+ get name(): string {
743
+ return this.props.name
744
+ }
745
+ get createdAt(): Date {
746
+ return this.props.createdAt
747
+ }
748
+ get updatedAt(): Date {
749
+ return this.props.updatedAt
750
+ }
751
+
752
+ changeName(name: string): void {
753
+ if (!name || name.trim().length === 0) {
754
+ throw new Error('Name cannot be empty')
755
+ }
756
+ this.props.name = name.trim()
757
+ this.props.updatedAt = new Date()
758
+ }
759
+
760
+ toJSON() {
761
+ return {
762
+ id: this.props.id.toString(),
763
+ name: this.props.name,
764
+ createdAt: this.props.createdAt.toISOString(),
765
+ updatedAt: this.props.updatedAt.toISOString(),
766
+ }
767
+ }
768
+ }
769
+ `;
770
+ }
771
+ function generateValueObject(ctx) {
772
+ const { pascal, kebab } = ctx;
773
+ return `/**
774
+ * ${pascal} ID Value Object
775
+ *
776
+ * Domain layer — wraps a primitive ID with type safety and validation.
777
+ * Value objects are immutable and compared by value, not reference.
778
+ *
779
+ * ${pascal}Id.create() — generate a new UUID
780
+ * ${pascal}Id.from(id) — wrap an existing ID string (validates non-empty)
781
+ * id.equals(other) — compare two IDs by value
782
+ */
783
+ import { randomUUID } from 'node:crypto'
784
+
785
+ export class ${pascal}Id {
786
+ private constructor(private readonly value: string) {}
787
+
788
+ static create(): ${pascal}Id {
789
+ return new ${pascal}Id(randomUUID())
790
+ }
791
+
792
+ static from(id: string): ${pascal}Id {
793
+ if (!id || id.trim().length === 0) {
794
+ throw new Error('${pascal}Id cannot be empty')
795
+ }
796
+ return new ${pascal}Id(id)
797
+ }
798
+
799
+ toString(): string {
800
+ return this.value
801
+ }
802
+
803
+ equals(other: ${pascal}Id): boolean {
804
+ return this.value === other.value
805
+ }
806
+ }
807
+ `;
808
+ }
809
+ //#endregion
810
+ //#region src/generators/templates/tests.ts
811
+ function generateControllerTest(ctx) {
812
+ const { pascal, kebab, plural = "" } = ctx;
813
+ return `import { describe, it, expect, beforeEach } from 'vitest'
814
+ import { Container } from '@forinda/kickjs'
815
+
816
+ describe('${pascal}Controller', () => {
817
+ beforeEach(() => {
818
+ Container.reset()
819
+ })
820
+
821
+ it('should be defined', () => {
822
+ expect(true).toBe(true)
823
+ })
824
+
825
+ describe('POST /${plural}', () => {
826
+ it('should create a new ${kebab}', async () => {
827
+ // TODO: Set up test module, call create endpoint, assert 201
828
+ expect(true).toBe(true)
829
+ })
830
+ })
831
+
832
+ describe('GET /${plural}', () => {
833
+ it('should return paginated ${plural}', async () => {
834
+ // TODO: Set up test module, call list endpoint, assert { data, meta }
835
+ expect(true).toBe(true)
836
+ })
837
+ })
838
+
839
+ describe('GET /${plural}/:id', () => {
840
+ it('should return a ${kebab} by id', async () => {
841
+ // TODO: Create a ${kebab}, then fetch by id, assert match
842
+ expect(true).toBe(true)
843
+ })
844
+
845
+ it('should return 404 for non-existent ${kebab}', async () => {
846
+ // TODO: Fetch non-existent id, assert 404
847
+ expect(true).toBe(true)
848
+ })
849
+ })
850
+
851
+ describe('PUT /${plural}/:id', () => {
852
+ it('should update an existing ${kebab}', async () => {
853
+ // TODO: Create, update, assert changes
854
+ expect(true).toBe(true)
855
+ })
856
+ })
857
+
858
+ describe('DELETE /${plural}/:id', () => {
859
+ it('should delete a ${kebab}', async () => {
860
+ // TODO: Create, delete, assert gone
861
+ expect(true).toBe(true)
862
+ })
863
+ })
864
+ })
865
+ `;
866
+ }
867
+ function generateRepositoryTest(ctx) {
868
+ const { pascal, kebab, plural = "", repoPrefix = `../infrastructure/repositories/in-memory-${kebab}.repository` } = ctx;
869
+ return `import { describe, it, expect, beforeEach } from 'vitest'
870
+ import { InMemory${pascal}Repository } from '${repoPrefix}'
871
+
872
+ describe('InMemory${pascal}Repository', () => {
873
+ let repo: InMemory${pascal}Repository
874
+
875
+ beforeEach(() => {
876
+ repo = new InMemory${pascal}Repository()
877
+ })
878
+
879
+ it('should create and retrieve a ${kebab}', async () => {
880
+ const created = await repo.create({ name: 'Test ${pascal}' })
881
+ expect(created).toBeDefined()
882
+ expect(created.name).toBe('Test ${pascal}')
883
+ expect(created.id).toBeDefined()
884
+
885
+ const found = await repo.findById(created.id)
886
+ expect(found).toEqual(created)
887
+ })
888
+
889
+ it('should return null for non-existent id', async () => {
890
+ const found = await repo.findById('non-existent')
891
+ expect(found).toBeNull()
892
+ })
893
+
894
+ it('should list all ${plural}', async () => {
895
+ await repo.create({ name: '${pascal} 1' })
896
+ await repo.create({ name: '${pascal} 2' })
897
+
898
+ const all = await repo.findAll()
899
+ expect(all).toHaveLength(2)
900
+ })
901
+
902
+ it('should return paginated results', async () => {
903
+ await repo.create({ name: '${pascal} 1' })
904
+ await repo.create({ name: '${pascal} 2' })
905
+ await repo.create({ name: '${pascal} 3' })
906
+
907
+ const result = await repo.findPaginated({
908
+ filters: [],
909
+ sort: [],
910
+ search: '',
911
+ pagination: { page: 1, limit: 2, offset: 0 },
912
+ })
913
+
914
+ expect(result.data).toHaveLength(2)
915
+ expect(result.total).toBe(3)
916
+ })
917
+
918
+ it('should update a ${kebab}', async () => {
919
+ const created = await repo.create({ name: 'Original' })
920
+ const updated = await repo.update(created.id, { name: 'Updated' })
921
+ expect(updated.name).toBe('Updated')
922
+ })
923
+
924
+ it('should delete a ${kebab}', async () => {
925
+ const created = await repo.create({ name: 'To Delete' })
926
+ await repo.delete(created.id)
927
+ const found = await repo.findById(created.id)
928
+ expect(found).toBeNull()
929
+ })
930
+ })
931
+ `;
932
+ }
933
+ //#endregion
934
+ //#region src/generators/templates/rest-service.ts
935
+ /** REST service — wraps repository with CRUD methods, replaces use-cases for flat pattern */
936
+ function generateRestService(ctx) {
937
+ const { pascal, kebab } = ctx;
938
+ return `import { Service, Inject, HttpException } from '@forinda/kickjs'
939
+ import type { ParsedQuery } from '@forinda/kickjs'
940
+ import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from './${kebab}.repository'
941
+ import type { ${pascal}ResponseDTO } from './dtos/${kebab}-response.dto'
942
+ import type { Create${pascal}DTO } from './dtos/create-${kebab}.dto'
943
+ import type { Update${pascal}DTO } from './dtos/update-${kebab}.dto'
944
+
945
+ @Service()
946
+ export class ${pascal}Service {
947
+ constructor(
948
+ @Inject(${pascal.toUpperCase()}_REPOSITORY) private readonly repo: I${pascal}Repository,
949
+ ) {}
950
+
951
+ async findById(id: string): Promise<${pascal}ResponseDTO | null> {
952
+ return this.repo.findById(id)
953
+ }
954
+
955
+ async findAll(): Promise<${pascal}ResponseDTO[]> {
956
+ return this.repo.findAll()
957
+ }
958
+
959
+ async findPaginated(parsed: ParsedQuery) {
960
+ return this.repo.findPaginated(parsed)
961
+ }
962
+
963
+ async create(dto: Create${pascal}DTO): Promise<${pascal}ResponseDTO> {
964
+ return this.repo.create(dto)
965
+ }
966
+
967
+ async update(id: string, dto: Update${pascal}DTO): Promise<${pascal}ResponseDTO> {
968
+ return this.repo.update(id, dto)
969
+ }
970
+
971
+ async delete(id: string): Promise<void> {
972
+ await this.repo.delete(id)
973
+ }
974
+ }
975
+ `;
976
+ }
977
+ /** REST constants — query config for flat pattern */
978
+ function generateRestConstants(ctx) {
979
+ const { pascal } = ctx;
980
+ return `import type { QueryFieldConfig } from '@forinda/kickjs'
981
+
982
+ export const ${pascal.toUpperCase()}_QUERY_CONFIG: QueryFieldConfig = {
983
+ filterable: ['name'],
984
+ sortable: ['name', 'createdAt'],
985
+ searchable: ['name'],
986
+ }
987
+ `;
988
+ }
989
+ //#endregion
990
+ //#region src/generators/templates/cqrs.ts
991
+ /** CQRS module index — commands, queries, events, WebSocket + queue integration */
992
+ function generateCqrsModuleIndex(ctx) {
993
+ const { pascal, kebab, plural = "", repo } = ctx;
994
+ const repoClassMap = {
995
+ inmemory: `InMemory${pascal}Repository`,
996
+ drizzle: `Drizzle${pascal}Repository`,
997
+ prisma: `Prisma${pascal}Repository`
998
+ };
999
+ const repoFileMap = {
1000
+ inmemory: `in-memory-${kebab}`,
1001
+ drizzle: `drizzle-${kebab}`,
1002
+ prisma: `prisma-${kebab}`
1003
+ };
1004
+ const repoClass = repoClassMap[repo] ?? repoClassMap.inmemory;
1005
+ const repoFile = repoFileMap[repo] ?? repoFileMap.inmemory;
1006
+ return `/**
1007
+ * ${pascal} Module — CQRS Pattern
1008
+ *
1009
+ * Separates read (queries) and write (commands) operations.
1010
+ * Events are emitted after state changes and can be handled via
1011
+ * WebSocket broadcasts, queue jobs, or ETL pipelines.
1012
+ *
1013
+ * Structure:
1014
+ * commands/ — Write operations (create, update, delete)
1015
+ * queries/ — Read operations (get, list)
1016
+ * events/ — Domain events + handlers (WS broadcast, queue dispatch)
1017
+ * dtos/ — Request/response schemas
1018
+ */
1019
+ import { Container, type AppModule, type ModuleRoutes } from '@forinda/kickjs'
1020
+ import { buildRoutes } from '@forinda/kickjs'
1021
+ import { ${pascal.toUpperCase()}_REPOSITORY } from './${kebab}.repository'
1022
+ import { ${repoClass} } from './${repoFile}.repository'
1023
+ import { ${pascal}Controller } from './${kebab}.controller'
1024
+
1025
+ // Eagerly load decorated classes
1026
+ import.meta.glob(
1027
+ [
1028
+ './commands/**/*.ts',
1029
+ './queries/**/*.ts',
1030
+ './events/**/*.ts',
1031
+ '!./**/*.test.ts',
1032
+ ],
1033
+ { eager: true },
1034
+ )
1035
+
1036
+ export class ${pascal}Module implements AppModule {
1037
+ register(container: Container): void {
1038
+ container.registerFactory(${pascal.toUpperCase()}_REPOSITORY, () =>
1039
+ container.resolve(${repoClass}),
1040
+ )
1041
+ }
1042
+
1043
+ routes(): ModuleRoutes {
1044
+ return {
1045
+ path: '/${plural}',
1046
+ router: buildRoutes(${pascal}Controller),
1047
+ controller: ${pascal}Controller,
1048
+ }
1049
+ }
1050
+ }
1051
+ `;
1052
+ }
1053
+ /** CQRS controller — dispatches to command/query handlers */
1054
+ function generateCqrsController(ctx) {
1055
+ const { pascal, kebab, plural = "", pluralPascal = "" } = ctx;
1056
+ return `import { Controller, Get, Post, Put, Delete, Autowired, ApiQueryParams } from '@forinda/kickjs'
1057
+ import type { RequestContext } from '@forinda/kickjs'
1058
+ import { ApiTags } from '@forinda/kickjs-swagger'
1059
+ import { Create${pascal}Command } from './commands/create-${kebab}.command'
1060
+ import { Update${pascal}Command } from './commands/update-${kebab}.command'
1061
+ import { Delete${pascal}Command } from './commands/delete-${kebab}.command'
1062
+ import { Get${pascal}Query } from './queries/get-${kebab}.query'
1063
+ import { List${pluralPascal}Query } from './queries/list-${plural}.query'
1064
+ import { create${pascal}Schema } from './dtos/create-${kebab}.dto'
1065
+ import { update${pascal}Schema } from './dtos/update-${kebab}.dto'
1066
+ import { ${pascal.toUpperCase()}_QUERY_CONFIG } from './${kebab}.constants'
1067
+
1068
+ @Controller()
1069
+ export class ${pascal}Controller {
1070
+ @Autowired() private create${pascal}Command!: Create${pascal}Command
1071
+ @Autowired() private update${pascal}Command!: Update${pascal}Command
1072
+ @Autowired() private delete${pascal}Command!: Delete${pascal}Command
1073
+ @Autowired() private get${pascal}Query!: Get${pascal}Query
1074
+ @Autowired() private list${pluralPascal}Query!: List${pluralPascal}Query
1075
+
1076
+ @Get('/')
1077
+ @ApiTags('${pascal}')
1078
+ @ApiQueryParams(${pascal.toUpperCase()}_QUERY_CONFIG)
1079
+ async list(ctx: RequestContext) {
1080
+ return ctx.paginate(
1081
+ (parsed) => this.list${pluralPascal}Query.execute(parsed),
1082
+ ${pascal.toUpperCase()}_QUERY_CONFIG,
1083
+ )
1084
+ }
1085
+
1086
+ @Get('/:id')
1087
+ @ApiTags('${pascal}')
1088
+ async getById(ctx: RequestContext) {
1089
+ const result = await this.get${pascal}Query.execute(ctx.params.id)
1090
+ if (!result) return ctx.notFound('${pascal} not found')
1091
+ ctx.json(result)
1092
+ }
1093
+
1094
+ @Post('/', { body: create${pascal}Schema, name: 'Create${pascal}' })
1095
+ @ApiTags('${pascal}')
1096
+ async create(ctx: RequestContext) {
1097
+ const result = await this.create${pascal}Command.execute(ctx.body)
1098
+ ctx.created(result)
1099
+ }
1100
+
1101
+ @Put('/:id', { body: update${pascal}Schema, name: 'Update${pascal}' })
1102
+ @ApiTags('${pascal}')
1103
+ async update(ctx: RequestContext) {
1104
+ const result = await this.update${pascal}Command.execute(ctx.params.id, ctx.body)
1105
+ ctx.json(result)
1106
+ }
1107
+
1108
+ @Delete('/:id')
1109
+ @ApiTags('${pascal}')
1110
+ async remove(ctx: RequestContext) {
1111
+ await this.delete${pascal}Command.execute(ctx.params.id)
1112
+ ctx.noContent()
1113
+ }
1114
+ }
1115
+ `;
1116
+ }
1117
+ /** CQRS commands — write operations that emit events */
1118
+ function generateCqrsCommands(ctx) {
1119
+ const { pascal, kebab } = ctx;
1120
+ return [
1121
+ {
1122
+ file: `create-${kebab}.command.ts`,
1123
+ content: `import { Service, Inject } from '@forinda/kickjs'
1124
+ import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../${kebab}.repository'
1125
+ import type { Create${pascal}DTO } from '../dtos/create-${kebab}.dto'
1126
+ import type { ${pascal}ResponseDTO } from '../dtos/${kebab}-response.dto'
1127
+ import { ${pascal}Events } from '../events/${kebab}.events'
1128
+
1129
+ @Service()
1130
+ export class Create${pascal}Command {
1131
+ constructor(
1132
+ @Inject(${pascal.toUpperCase()}_REPOSITORY) private readonly repo: I${pascal}Repository,
1133
+ @Inject(${pascal}Events) private readonly events: ${pascal}Events,
1134
+ ) {}
1135
+
1136
+ async execute(dto: Create${pascal}DTO): Promise<${pascal}ResponseDTO> {
1137
+ const result = await this.repo.create(dto)
1138
+ this.events.emit('${kebab}.created', result)
1139
+ return result
1140
+ }
1141
+ }
1142
+ `
1143
+ },
1144
+ {
1145
+ file: `update-${kebab}.command.ts`,
1146
+ content: `import { Service, Inject } from '@forinda/kickjs'
1147
+ import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../${kebab}.repository'
1148
+ import type { Update${pascal}DTO } from '../dtos/update-${kebab}.dto'
1149
+ import type { ${pascal}ResponseDTO } from '../dtos/${kebab}-response.dto'
1150
+ import { ${pascal}Events } from '../events/${kebab}.events'
1151
+
1152
+ @Service()
1153
+ export class Update${pascal}Command {
1154
+ constructor(
1155
+ @Inject(${pascal.toUpperCase()}_REPOSITORY) private readonly repo: I${pascal}Repository,
1156
+ @Inject(${pascal}Events) private readonly events: ${pascal}Events,
1157
+ ) {}
1158
+
1159
+ async execute(id: string, dto: Update${pascal}DTO): Promise<${pascal}ResponseDTO> {
1160
+ const result = await this.repo.update(id, dto)
1161
+ this.events.emit('${kebab}.updated', result)
1162
+ return result
1163
+ }
1164
+ }
1165
+ `
1166
+ },
1167
+ {
1168
+ file: `delete-${kebab}.command.ts`,
1169
+ content: `import { Service, Inject } from '@forinda/kickjs'
1170
+ import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../${kebab}.repository'
1171
+ import { ${pascal}Events } from '../events/${kebab}.events'
1172
+
1173
+ @Service()
1174
+ export class Delete${pascal}Command {
1175
+ constructor(
1176
+ @Inject(${pascal.toUpperCase()}_REPOSITORY) private readonly repo: I${pascal}Repository,
1177
+ @Inject(${pascal}Events) private readonly events: ${pascal}Events,
1178
+ ) {}
1179
+
1180
+ async execute(id: string): Promise<void> {
1181
+ await this.repo.delete(id)
1182
+ this.events.emit('${kebab}.deleted', { id })
1183
+ }
1184
+ }
1185
+ `
1186
+ }
1187
+ ];
1188
+ }
1189
+ /** CQRS queries — read operations */
1190
+ function generateCqrsQueries(ctx) {
1191
+ const { pascal, kebab, plural = "", pluralPascal = "" } = ctx;
1192
+ return [{
1193
+ file: `get-${kebab}.query.ts`,
1194
+ content: `import { Service, Inject } from '@forinda/kickjs'
1195
+ import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../${kebab}.repository'
1196
+ import type { ${pascal}ResponseDTO } from '../dtos/${kebab}-response.dto'
1197
+
1198
+ @Service()
1199
+ export class Get${pascal}Query {
1200
+ constructor(
1201
+ @Inject(${pascal.toUpperCase()}_REPOSITORY) private readonly repo: I${pascal}Repository,
1202
+ ) {}
1203
+
1204
+ async execute(id: string): Promise<${pascal}ResponseDTO | null> {
1205
+ return this.repo.findById(id)
1206
+ }
1207
+ }
1208
+ `
1209
+ }, {
1210
+ file: `list-${plural}.query.ts`,
1211
+ content: `import { Service, Inject } from '@forinda/kickjs'
1212
+ import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../${kebab}.repository'
1213
+ import type { ParsedQuery } from '@forinda/kickjs'
1214
+
1215
+ @Service()
1216
+ export class List${pluralPascal}Query {
1217
+ constructor(
1218
+ @Inject(${pascal.toUpperCase()}_REPOSITORY) private readonly repo: I${pascal}Repository,
1219
+ ) {}
1220
+
1221
+ async execute(parsed: ParsedQuery) {
1222
+ return this.repo.findPaginated(parsed)
1223
+ }
1224
+ }
1225
+ `
1226
+ }];
1227
+ }
1228
+ /** CQRS events — domain event emitter + handler with WS/queue integration */
1229
+ function generateCqrsEvents(ctx) {
1230
+ const { pascal, kebab } = ctx;
1231
+ return [{
1232
+ file: `${kebab}.events.ts`,
1233
+ content: `import { Service } from '@forinda/kickjs'
1234
+ import { EventEmitter } from 'node:events'
1235
+ import type { ${pascal}ResponseDTO } from '../dtos/${kebab}-response.dto'
1236
+
1237
+ /**
1238
+ * ${pascal} domain event types.
1239
+ *
1240
+ * These events are emitted by commands after state changes.
1241
+ * Subscribe to them in event handlers for side effects:
1242
+ * - WebSocket broadcasts (real-time UI updates)
1243
+ * - Queue jobs (async processing, ETL pipelines)
1244
+ * - Audit logging
1245
+ * - Cache invalidation
1246
+ */
1247
+ export interface ${pascal}EventMap {
1248
+ '${kebab}.created': ${pascal}ResponseDTO
1249
+ '${kebab}.updated': ${pascal}ResponseDTO
1250
+ '${kebab}.deleted': { id: string }
1251
+ }
1252
+
1253
+ @Service()
1254
+ export class ${pascal}Events {
1255
+ private emitter = new EventEmitter()
1256
+
1257
+ emit<K extends keyof ${pascal}EventMap>(event: K, data: ${pascal}EventMap[K]): void {
1258
+ this.emitter.emit(event, data)
1259
+ }
1260
+
1261
+ on<K extends keyof ${pascal}EventMap>(event: K, handler: (data: ${pascal}EventMap[K]) => void): void {
1262
+ this.emitter.on(event, handler)
1263
+ }
1264
+
1265
+ off<K extends keyof ${pascal}EventMap>(event: K, handler: (data: ${pascal}EventMap[K]) => void): void {
1266
+ this.emitter.off(event, handler)
1267
+ }
1268
+ }
1269
+ `
1270
+ }, {
1271
+ file: `on-${kebab}-change.handler.ts`,
1272
+ content: `import { Service, Autowired } from '@forinda/kickjs'
1273
+ import { ${pascal}Events } from './${kebab}.events'
1274
+
1275
+ /**
1276
+ * ${pascal} Change Event Handler
1277
+ *
1278
+ * Reacts to domain events emitted by commands.
1279
+ * Wire up side effects here:
1280
+ *
1281
+ * 1. WebSocket broadcast — notify connected clients in real-time
1282
+ * import { WsGateway } from '@forinda/kickjs-ws'
1283
+ * this.ws.broadcast('${kebab}-channel', { event, data })
1284
+ *
1285
+ * 2. Queue dispatch — offload heavy processing to background workers
1286
+ * import { QueueService } from '@forinda/kickjs-queue'
1287
+ * this.queue.add('${kebab}-etl', { action: event, payload: data })
1288
+ *
1289
+ * 3. ETL pipeline — transform and load data to external systems
1290
+ * await this.etlPipeline.process(data)
1291
+ */
1292
+ @Service()
1293
+ export class On${pascal}ChangeHandler {
1294
+ @Autowired() private events!: ${pascal}Events
1295
+
1296
+ // Uncomment to inject WebSocket and Queue services:
1297
+ // @Autowired() private ws!: WsGateway
1298
+ // @Autowired() private queue!: QueueService
1299
+
1300
+ onInit(): void {
1301
+ this.events.on('${kebab}.created', (data) => {
1302
+ console.log('[${pascal}] Created:', data.id)
1303
+ // TODO: Broadcast via WebSocket
1304
+ // this.ws.broadcast('${kebab}-channel', { event: '${kebab}.created', data })
1305
+ // TODO: Dispatch to queue for async processing / ETL
1306
+ // this.queue.add('${kebab}-etl', { action: 'create', payload: data })
1307
+ })
1308
+
1309
+ this.events.on('${kebab}.updated', (data) => {
1310
+ console.log('[${pascal}] Updated:', data.id)
1311
+ // TODO: Broadcast via WebSocket
1312
+ // this.ws.broadcast('${kebab}-channel', { event: '${kebab}.updated', data })
1313
+ })
1314
+
1315
+ this.events.on('${kebab}.deleted', (data) => {
1316
+ console.log('[${pascal}] Deleted:', data.id)
1317
+ // TODO: Broadcast via WebSocket
1318
+ // this.ws.broadcast('${kebab}-channel', { event: '${kebab}.deleted', data })
1319
+ })
1320
+ }
1321
+ }
1322
+ `
1323
+ }];
1324
+ }
1325
+ //#endregion
1326
+ //#region src/generators/templates/drizzle/index.ts
1327
+ function generateDrizzleRepository(ctx) {
1328
+ const { pascal, kebab, repoPrefix = "../../domain/repositories", dtoPrefix = "../../application/dtos" } = ctx;
1329
+ return `/**
1330
+ * Drizzle ${pascal} Repository
1331
+ *
1332
+ * Implements the repository interface using Drizzle ORM.
1333
+ * Uses buildFromColumns() with Column objects for type-safe query building.
1334
+ *
1335
+ * TODO: Update the schema import to match your Drizzle schema file.
1336
+ * TODO: Replace DRIZZLE_DB injection token with your actual database token.
1337
+ *
1338
+ * @Repository() registers this class in the DI container as a singleton.
1339
+ */
1340
+ import { eq, ne, gt, gte, lt, lte, ilike, inArray, between, and, or, asc, desc, count, sql } from 'drizzle-orm'
1341
+ import { Repository, HttpException, Inject } from '@forinda/kickjs'
1342
+ import { DRIZZLE_DB, DrizzleQueryAdapter } from '@forinda/kickjs-drizzle'
1343
+ import type { ParsedQuery } from '@forinda/kickjs'
1344
+ import type { I${pascal}Repository } from '${repoPrefix}/${kebab}.repository'
1345
+ import type { ${pascal}ResponseDTO } from '${dtoPrefix}/${kebab}-response.dto'
1346
+ import type { Create${pascal}DTO } from '${dtoPrefix}/create-${kebab}.dto'
1347
+ import type { Update${pascal}DTO } from '${dtoPrefix}/update-${kebab}.dto'
1348
+ import { ${pascal.toUpperCase()}_QUERY_CONFIG } from '../../constants'
1349
+
1350
+ // TODO: Import your Drizzle schema table — e.g.:
1351
+ // import { ${kebab}s } from '@/db/schema'
1352
+
1353
+ const queryAdapter = new DrizzleQueryAdapter({
1354
+ eq, ne, gt, gte, lt, lte, ilike, inArray, between, and, or, asc, desc,
1355
+ })
1356
+
1357
+ @Repository()
1358
+ export class Drizzle${pascal}Repository implements I${pascal}Repository {
1359
+ constructor(@Inject(DRIZZLE_DB) private db: any) {}
1360
+
1361
+ async findById(id: string): Promise<${pascal}ResponseDTO | null> {
1362
+ // TODO: Implement with Drizzle
1363
+ // const row = this.db.select().from(${kebab}s).where(eq(${kebab}s.id, id)).get()
1364
+ // return row ?? null
1365
+ throw new Error('Drizzle ${pascal} repository not yet implemented — update schema imports and queries')
1366
+ }
1367
+
1368
+ async findAll(): Promise<${pascal}ResponseDTO[]> {
1369
+ // TODO: Implement with Drizzle
1370
+ // return this.db.select().from(${kebab}s).all()
1371
+ throw new Error('Drizzle ${pascal} repository not yet implemented')
1372
+ }
1373
+
1374
+ async findPaginated(parsed: ParsedQuery): Promise<{ data: ${pascal}ResponseDTO[]; total: number }> {
1375
+ // TODO: Use buildFromColumns() with your query config for type-safe filtering
1376
+ // const query = queryAdapter.buildFromColumns(parsed, ${pascal.toUpperCase()}_QUERY_CONFIG)
1377
+ //
1378
+ // const data = this.db
1379
+ // .select().from(${kebab}s).$dynamic()
1380
+ // .where(query.where).orderBy(...query.orderBy)
1381
+ // .limit(query.limit).offset(query.offset).all()
1382
+ //
1383
+ // const totalResult = this.db
1384
+ // .select({ count: count() }).from(${kebab}s)
1385
+ // .$dynamic().where(query.where).get()
1386
+ //
1387
+ // return { data, total: totalResult?.count ?? 0 }
1388
+ throw new Error('Drizzle ${pascal} repository not yet implemented')
1389
+ }
1390
+
1391
+ async create(dto: Create${pascal}DTO): Promise<${pascal}ResponseDTO> {
1392
+ // TODO: Implement with Drizzle
1393
+ // return this.db.insert(${kebab}s).values(dto).returning().get()
1394
+ throw new Error('Drizzle ${pascal} repository not yet implemented')
1395
+ }
1396
+
1397
+ async update(id: string, dto: Update${pascal}DTO): Promise<${pascal}ResponseDTO> {
1398
+ // TODO: Implement with Drizzle
1399
+ // const row = this.db.update(${kebab}s).set(dto).where(eq(${kebab}s.id, id)).returning().get()
1400
+ // if (!row) throw HttpException.notFound('${pascal} not found')
1401
+ // return row
1402
+ throw new Error('Drizzle ${pascal} repository not yet implemented')
1403
+ }
1404
+
1405
+ async delete(id: string): Promise<void> {
1406
+ // TODO: Implement with Drizzle
1407
+ // this.db.delete(${kebab}s).where(eq(${kebab}s.id, id)).run()
1408
+ throw new Error('Drizzle ${pascal} repository not yet implemented')
1409
+ }
1410
+ }
1411
+ `;
1412
+ }
1413
+ function generateDrizzleConstants(ctx) {
1414
+ const { pascal, kebab } = ctx;
1415
+ return `import type { DrizzleQueryParamsConfig } from '@forinda/kickjs-drizzle'
1416
+ // TODO: Import your schema table and reference actual columns for type safety
1417
+ // import { ${kebab}s } from '@/db/schema'
1418
+
1419
+ export const ${pascal.toUpperCase()}_QUERY_CONFIG: DrizzleQueryParamsConfig = {
1420
+ columns: {
1421
+ // Replace with actual Drizzle Column references for type-safe filtering:
1422
+ // name: ${kebab}s.name,
1423
+ // status: ${kebab}s.status,
1424
+ },
1425
+ sortable: {
1426
+ // name: ${kebab}s.name,
1427
+ // createdAt: ${kebab}s.createdAt,
1428
+ },
1429
+ searchColumns: [
1430
+ // ${kebab}s.name,
1431
+ ],
1432
+ }
1433
+ `;
1434
+ }
1435
+ //#endregion
1436
+ //#region src/generators/templates/prisma/index.ts
1437
+ function generatePrismaRepository(ctx) {
1438
+ const { pascal, kebab, repoPrefix = "../../domain/repositories", dtoPrefix = "../../application/dtos" } = ctx;
1439
+ const camel = kebab.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
1440
+ return `/**
1441
+ * Prisma ${pascal} Repository
1442
+ *
1443
+ * Implements the repository interface using Prisma Client.
1444
+ * Requires a PrismaClient instance injected via the DI container.
1445
+ *
1446
+ * Ensure your Prisma schema has a '${pascal}' model defined.
1447
+ *
1448
+ * For full Prisma field-level type safety, replace PrismaModelDelegate with your PrismaClient:
1449
+ * @Inject(PRISMA_CLIENT) private prisma!: PrismaClient
1450
+ *
1451
+ * @Repository() registers this class in the DI container as a singleton.
1452
+ */
1453
+ import { Repository, HttpException, Inject } from '@forinda/kickjs'
1454
+ import { PRISMA_CLIENT, type PrismaModelDelegate } from '@forinda/kickjs-prisma'
1455
+ import type { ParsedQuery } from '@forinda/kickjs'
1456
+ import type { I${pascal}Repository } from '${repoPrefix}/${kebab}.repository'
1457
+ import type { ${pascal}ResponseDTO } from '${dtoPrefix}/${kebab}-response.dto'
1458
+ import type { Create${pascal}DTO } from '${dtoPrefix}/create-${kebab}.dto'
1459
+ import type { Update${pascal}DTO } from '${dtoPrefix}/update-${kebab}.dto'
1460
+
1461
+ @Repository()
1462
+ export class Prisma${pascal}Repository implements I${pascal}Repository {
1463
+ @Inject(PRISMA_CLIENT) private prisma!: { ${camel}: PrismaModelDelegate }
1464
+
1465
+ async findById(id: string): Promise<${pascal}ResponseDTO | null> {
1466
+ return this.prisma.${camel}.findUnique({ where: { id } }) as Promise<${pascal}ResponseDTO | null>
1467
+ }
1468
+
1469
+ async findAll(): Promise<${pascal}ResponseDTO[]> {
1470
+ return this.prisma.${camel}.findMany() as Promise<${pascal}ResponseDTO[]>
1471
+ }
1472
+
1473
+ async findPaginated(parsed: ParsedQuery): Promise<{ data: ${pascal}ResponseDTO[]; total: number }> {
1474
+ const [data, total] = await Promise.all([
1475
+ this.prisma.${camel}.findMany({
1476
+ skip: parsed.pagination.offset,
1477
+ take: parsed.pagination.limit,
1478
+ }) as Promise<${pascal}ResponseDTO[]>,
1479
+ this.prisma.${camel}.count(),
1480
+ ])
1481
+ return { data, total }
1482
+ }
1483
+
1484
+ async create(dto: Create${pascal}DTO): Promise<${pascal}ResponseDTO> {
1485
+ return this.prisma.${camel}.create({ data: dto as Record<string, unknown> }) as Promise<${pascal}ResponseDTO>
1486
+ }
1487
+
1488
+ async update(id: string, dto: Update${pascal}DTO): Promise<${pascal}ResponseDTO> {
1489
+ const existing = await this.prisma.${camel}.findUnique({ where: { id } })
1490
+ if (!existing) throw HttpException.notFound('${pascal} not found')
1491
+ return this.prisma.${camel}.update({ where: { id }, data: dto as Record<string, unknown> }) as Promise<${pascal}ResponseDTO>
1492
+ }
1493
+
1494
+ async delete(id: string): Promise<void> {
1495
+ await this.prisma.${camel}.deleteMany({ where: { id } })
1496
+ }
1497
+ }
1498
+ `;
1499
+ }
1500
+ //#endregion
1501
+ //#region src/generators/templates/project-app.ts
1502
+ /**
1503
+ * Generate src/index.ts entry file with template-specific bootstrap.
1504
+ *
1505
+ * All templates export the app for the Vite plugin (dev mode).
1506
+ * In production, bootstrap() auto-starts the HTTP server when
1507
+ * `globalThis.__kickjs_httpServer` is not set.
1508
+ */
1509
+ function generateEntryFile(name, template, version) {
1510
+ switch (template) {
1511
+ case "graphql": return `import 'reflect-metadata'
1512
+ import { bootstrap } from '@forinda/kickjs'
1513
+ import { DevToolsAdapter } from '@forinda/kickjs-devtools'
1514
+ import { GraphQLAdapter } from '@forinda/kickjs-graphql'
1515
+ import { modules } from './modules'
1516
+
1517
+ // Import your resolvers here
1518
+ // import { UserResolver } from './resolvers/user.resolver'
1519
+
1520
+ // Export the app for the Vite plugin (dev mode)
1521
+ export const app = await bootstrap({
1522
+ modules,
1523
+ adapters: [
1524
+ new DevToolsAdapter(),
1525
+ new GraphQLAdapter({
1526
+ resolvers: [/* UserResolver */],
1527
+ // Add custom type definitions here:
1528
+ // typeDefs: userTypeDefs,
1529
+ }),
1530
+ ],
1531
+ })
1532
+ `;
1533
+ case "cqrs": return `import 'reflect-metadata'
1534
+ import { bootstrap } from '@forinda/kickjs'
1535
+ import { DevToolsAdapter } from '@forinda/kickjs-devtools'
1536
+ import { SwaggerAdapter } from '@forinda/kickjs-swagger'
1537
+ import { OtelAdapter } from '@forinda/kickjs-otel'
1538
+ // import { WsAdapter } from '@forinda/kickjs-ws'
1539
+ // import { QueueAdapter, BullMQProvider } from '@forinda/kickjs-queue'
1540
+ import { modules } from './modules'
1541
+
1542
+ // Export the app for the Vite plugin (dev mode)
1543
+ export const app = await bootstrap({
1544
+ modules,
1545
+ adapters: [
1546
+ new OtelAdapter({ serviceName: '${name}' }),
1547
+ new DevToolsAdapter(),
1548
+ new SwaggerAdapter({
1549
+ info: { title: '${name}', version: '${version}' },
1550
+ }),
1551
+ // Uncomment for WebSocket support:
1552
+ // new WsAdapter(),
1553
+ // Uncomment when Redis is available:
1554
+ // new QueueAdapter({
1555
+ // provider: new BullMQProvider({ host: 'localhost', port: 6379 }),
1556
+ // }),
1557
+ ],
1558
+ })
1559
+ `;
1560
+ case "minimal": return `import 'reflect-metadata'
1561
+ import { bootstrap } from '@forinda/kickjs'
1562
+ import { modules } from './modules'
1563
+
1564
+ // Export the app for the Vite plugin (dev mode)
1565
+ export const app = await bootstrap({ modules })
1566
+ `;
1567
+ default: return `import 'reflect-metadata'
1568
+ import express from 'express'
1569
+ import {
1570
+ bootstrap,
1571
+ requestId,
1572
+ requestLogger,
1573
+ helmet,
1574
+ cors,
1575
+ } from '@forinda/kickjs'
1576
+ import { DevToolsAdapter } from '@forinda/kickjs-devtools'
1577
+ import { SwaggerAdapter } from '@forinda/kickjs-swagger'
1578
+ import { modules } from './modules'
1579
+
1580
+ // Export the app for the Vite plugin (dev mode)
1581
+ export const app = await bootstrap({
1582
+ modules,
1583
+ adapters: [
1584
+ new DevToolsAdapter(),
1585
+ new SwaggerAdapter({
1586
+ info: { title: '${name}', version: '${version}' },
1587
+ }),
1588
+ ],
1589
+ middleware: [
1590
+ helmet(),
1591
+ cors({ origin: '*' }),
1592
+ requestId(),
1593
+ requestLogger(),
1594
+ express.json(),
1595
+ ],
1596
+ })
1597
+ `;
1598
+ }
1599
+ }
1600
+ /** Generate src/modules/index.ts module registry */
1601
+ function generateModulesIndex() {
1602
+ return `import type { AppModuleClass } from '@forinda/kickjs'
1603
+ import { HelloModule } from './hello/hello.module'
1604
+
1605
+ // Remove HelloModule and run: kick g module <name>
1606
+ export const modules: AppModuleClass[] = [HelloModule]
1607
+ `;
1608
+ }
1609
+ /** Generate src/modules/hello/hello.service.ts */
1610
+ function generateHelloService() {
1611
+ return `import { Service } from '@forinda/kickjs'
1612
+
1613
+ @Service()
1614
+ export class HelloService {
1615
+ greet(name: string) {
1616
+ return { message: \`Hello \${name} from KickJS!\`, timestamp: new Date().toISOString() }
1617
+ }
1618
+
1619
+ healthCheck() {
1620
+ return { status: 'ok', uptime: process.uptime() }
1621
+ }
1622
+ }
1623
+ `;
1624
+ }
1625
+ /** Generate src/modules/hello/hello.controller.ts */
1626
+ function generateHelloController() {
1627
+ return `import { Controller, Get, Autowired, type RequestContext } from '@forinda/kickjs'
1628
+ import { HelloService } from './hello.service'
1629
+
1630
+ @Controller()
1631
+ export class HelloController {
1632
+ @Autowired() private helloService!: HelloService
1633
+
1634
+ @Get('/')
1635
+ index(ctx: RequestContext) {
1636
+ ctx.json(this.helloService.greet('World'))
1637
+ }
1638
+
1639
+ @Get('/health')
1640
+ health(ctx: RequestContext) {
1641
+ ctx.json(this.helloService.healthCheck())
1642
+ }
1643
+ }
1644
+ `;
1645
+ }
1646
+ /** Generate src/modules/hello/hello.module.ts */
1647
+ function generateHelloModule() {
1648
+ return `import { type AppModule, type ModuleRoutes, buildRoutes } from '@forinda/kickjs'
1649
+ import { HelloController } from './hello.controller'
1650
+
1651
+ export class HelloModule implements AppModule {
1652
+ routes(): ModuleRoutes {
1653
+ return {
1654
+ path: '/hello',
1655
+ router: buildRoutes(HelloController),
1656
+ controller: HelloController,
1657
+ }
1658
+ }
1659
+ }
1660
+ `;
1661
+ }
1662
+ /** Generate kick.config.ts CLI configuration */
1663
+ function generateKickConfig(template, defaultRepo = "inmemory") {
1664
+ return `import { defineConfig } from '@forinda/kickjs-cli'
1665
+
1666
+ export default defineConfig({
1667
+ pattern: '${template}',
1668
+ modules: {
1669
+ dir: 'src/modules',
1670
+ repo: ${[
1671
+ "drizzle",
1672
+ "inmemory",
1673
+ "prisma"
1674
+ ].includes(defaultRepo) ? `'${defaultRepo}'` : `{ name: '${defaultRepo}' }`},
1675
+ pluralize: true,
1676
+ },
1677
+
1678
+ commands: [
1679
+ {
1680
+ name: 'test',
1681
+ description: 'Run tests with Vitest',
1682
+ steps: 'npx vitest run',
1683
+ },
1684
+ {
1685
+ name: 'format',
1686
+ description: 'Format code with Prettier',
1687
+ steps: 'npx prettier --write src/',
1688
+ },
1689
+ {
1690
+ name: 'format:check',
1691
+ description: 'Check formatting without writing',
1692
+ steps: 'npx prettier --check src/',
1693
+ },
1694
+ {
1695
+ name: 'check',
1696
+ description: 'Run typecheck + format check',
1697
+ steps: ['npx tsc --noEmit', 'npx prettier --check src/'],
1698
+ aliases: ['verify', 'ci'],
1699
+ },
1700
+ ],
1701
+ })
1702
+ `;
1703
+ }
1704
+ //#endregion
1705
+ //#region src/generators/patterns/minimal.ts
1706
+ async function generateMinimalFiles(ctx) {
1707
+ const { pascal, kebab, plural, write } = ctx;
1708
+ await write("index.ts", generateMinimalModuleIndex({
1709
+ pascal,
1710
+ kebab,
1711
+ plural
1712
+ }));
1713
+ await write(`${kebab}.controller.ts`, `import { Controller, Get } from '@forinda/kickjs'
1714
+ import type { RequestContext } from '@forinda/kickjs'
1715
+
1716
+ @Controller()
1717
+ export class ${pascal}Controller {
1718
+ @Get('/')
1719
+ async list(ctx: RequestContext) {
1720
+ ctx.json({ message: '${pascal} list' })
1721
+ }
1722
+ }
1723
+ `);
1724
+ }
1725
+ //#endregion
1726
+ //#region src/generators/patterns/rest.ts
1727
+ async function generateRestFiles(ctx) {
1728
+ const { pascal, kebab, plural, pluralPascal, repo, noTests, prismaClientPath, write } = ctx;
1729
+ await write("index.ts", generateRestModuleIndex({
1730
+ pascal,
1731
+ kebab,
1732
+ plural,
1733
+ repo
1734
+ }));
1735
+ await write(`${kebab}.constants.ts`, generateRestConstants({
1736
+ pascal,
1737
+ kebab
1738
+ }));
1739
+ await write(`${kebab}.controller.ts`, generateRestController({
1740
+ pascal,
1741
+ kebab,
1742
+ plural,
1743
+ pluralPascal
1744
+ }));
1745
+ await write(`${kebab}.service.ts`, generateRestService({
1746
+ pascal,
1747
+ kebab
1748
+ }));
1749
+ await write(`dtos/create-${kebab}.dto.ts`, generateCreateDTO({
1750
+ pascal,
1751
+ kebab
1752
+ }));
1753
+ await write(`dtos/update-${kebab}.dto.ts`, generateUpdateDTO({
1754
+ pascal,
1755
+ kebab
1756
+ }));
1757
+ await write(`dtos/${kebab}-response.dto.ts`, generateResponseDTO({
1758
+ pascal,
1759
+ kebab
1760
+ }));
1761
+ await write(`${kebab}.repository.ts`, generateRepositoryInterface({
1762
+ pascal,
1763
+ kebab,
1764
+ dtoPrefix: "./dtos"
1765
+ }));
1766
+ const builtinRepoFileMap = {
1767
+ inmemory: `in-memory-${kebab}`,
1768
+ drizzle: `drizzle-${kebab}`,
1769
+ prisma: `prisma-${kebab}`
1770
+ };
1771
+ const builtinRepoGeneratorMap = {
1772
+ inmemory: () => generateInMemoryRepository({
1773
+ pascal,
1774
+ kebab,
1775
+ repoPrefix: ".",
1776
+ dtoPrefix: "./dtos"
1777
+ }),
1778
+ drizzle: () => generateDrizzleRepository({
1779
+ pascal,
1780
+ kebab,
1781
+ repoPrefix: ".",
1782
+ dtoPrefix: "./dtos"
1783
+ }),
1784
+ prisma: () => generatePrismaRepository({
1785
+ pascal,
1786
+ kebab,
1787
+ repoPrefix: ".",
1788
+ dtoPrefix: "./dtos",
1789
+ prismaClientPath
1790
+ })
1791
+ };
1792
+ const repoFile = builtinRepoFileMap[repo] ?? `${toKebabCase(repo)}-${kebab}`;
1793
+ const repoGenerator = builtinRepoGeneratorMap[repo] ?? (() => generateCustomRepository({
1794
+ pascal,
1795
+ kebab,
1796
+ repoType: repo,
1797
+ repoPrefix: ".",
1798
+ dtoPrefix: "./dtos"
1799
+ }));
1800
+ await write(`${repoFile}.repository.ts`, repoGenerator());
1801
+ if (!noTests) {
1802
+ if (repo !== "inmemory") await write(`in-memory-${kebab}.repository.ts`, generateInMemoryRepository({
1803
+ pascal,
1804
+ kebab,
1805
+ repoPrefix: ".",
1806
+ dtoPrefix: "./dtos"
1807
+ }));
1808
+ await write(`__tests__/${kebab}.controller.test.ts`, generateControllerTest({
1809
+ pascal,
1810
+ kebab,
1811
+ plural
1812
+ }));
1813
+ await write(`__tests__/${kebab}.repository.test.ts`, generateRepositoryTest({
1814
+ pascal,
1815
+ kebab,
1816
+ plural,
1817
+ repoPrefix: `../${builtinRepoFileMap.inmemory ?? `in-memory-${kebab}`}.repository`
1818
+ }));
1819
+ }
1820
+ }
1821
+ //#endregion
1822
+ //#region src/generators/patterns/cqrs.ts
1823
+ async function generateCqrsFiles(ctx) {
1824
+ const { pascal, kebab, plural, pluralPascal, repo, noTests, prismaClientPath, write } = ctx;
1825
+ await write("index.ts", generateCqrsModuleIndex({
1826
+ pascal,
1827
+ kebab,
1828
+ plural,
1829
+ repo
1830
+ }));
1831
+ await write(`${kebab}.constants.ts`, generateRestConstants({
1832
+ pascal,
1833
+ kebab
1834
+ }));
1835
+ await write(`${kebab}.controller.ts`, generateCqrsController({
1836
+ pascal,
1837
+ kebab,
1838
+ plural,
1839
+ pluralPascal
1840
+ }));
1841
+ await write(`dtos/create-${kebab}.dto.ts`, generateCreateDTO({
1842
+ pascal,
1843
+ kebab
1844
+ }));
1845
+ await write(`dtos/update-${kebab}.dto.ts`, generateUpdateDTO({
1846
+ pascal,
1847
+ kebab
1848
+ }));
1849
+ await write(`dtos/${kebab}-response.dto.ts`, generateResponseDTO({
1850
+ pascal,
1851
+ kebab
1852
+ }));
1853
+ const commands = generateCqrsCommands({
1854
+ pascal,
1855
+ kebab
1856
+ });
1857
+ for (const cmd of commands) await write(`commands/${cmd.file}`, cmd.content);
1858
+ const queries = generateCqrsQueries({
1859
+ pascal,
1860
+ kebab,
1861
+ plural,
1862
+ pluralPascal
1863
+ });
1864
+ for (const q of queries) await write(`queries/${q.file}`, q.content);
1865
+ const events = generateCqrsEvents({
1866
+ pascal,
1867
+ kebab
1868
+ });
1869
+ for (const e of events) await write(`events/${e.file}`, e.content);
1870
+ await write(`${kebab}.repository.ts`, generateRepositoryInterface({
1871
+ pascal,
1872
+ kebab,
1873
+ dtoPrefix: "./dtos"
1874
+ }));
1875
+ const builtinRepoFileMap = {
1876
+ inmemory: `in-memory-${kebab}`,
1877
+ drizzle: `drizzle-${kebab}`,
1878
+ prisma: `prisma-${kebab}`
1879
+ };
1880
+ const builtinRepoGeneratorMap = {
1881
+ inmemory: () => generateInMemoryRepository({
1882
+ pascal,
1883
+ kebab,
1884
+ repoPrefix: ".",
1885
+ dtoPrefix: "./dtos"
1886
+ }),
1887
+ drizzle: () => generateDrizzleRepository({
1888
+ pascal,
1889
+ kebab,
1890
+ repoPrefix: ".",
1891
+ dtoPrefix: "./dtos"
1892
+ }),
1893
+ prisma: () => generatePrismaRepository({
1894
+ pascal,
1895
+ kebab,
1896
+ repoPrefix: ".",
1897
+ dtoPrefix: "./dtos",
1898
+ prismaClientPath
1899
+ })
1900
+ };
1901
+ const repoFile = builtinRepoFileMap[repo] ?? `${toKebabCase(repo)}-${kebab}`;
1902
+ const repoGenerator = builtinRepoGeneratorMap[repo] ?? (() => generateCustomRepository({
1903
+ pascal,
1904
+ kebab,
1905
+ repoType: repo,
1906
+ repoPrefix: ".",
1907
+ dtoPrefix: "./dtos"
1908
+ }));
1909
+ await write(`${repoFile}.repository.ts`, repoGenerator());
1910
+ if (!noTests) {
1911
+ if (repo !== "inmemory") await write(`in-memory-${kebab}.repository.ts`, generateInMemoryRepository({
1912
+ pascal,
1913
+ kebab,
1914
+ repoPrefix: ".",
1915
+ dtoPrefix: "./dtos"
1916
+ }));
1917
+ await write(`__tests__/${kebab}.controller.test.ts`, generateControllerTest({
1918
+ pascal,
1919
+ kebab,
1920
+ plural
1921
+ }));
1922
+ await write(`__tests__/${kebab}.repository.test.ts`, generateRepositoryTest({
1923
+ pascal,
1924
+ kebab,
1925
+ plural,
1926
+ repoPrefix: `../${builtinRepoFileMap.inmemory ?? `in-memory-${kebab}`}.repository`
1927
+ }));
1928
+ }
1929
+ }
1930
+ //#endregion
1931
+ //#region src/generators/patterns/ddd.ts
1932
+ async function generateDddFiles(ctx) {
1933
+ const { pascal, kebab, plural, pluralPascal, repo, noEntity, noTests, prismaClientPath, write } = ctx;
1934
+ await write("index.ts", generateModuleIndex({
1935
+ pascal,
1936
+ kebab,
1937
+ plural,
1938
+ repo
1939
+ }));
1940
+ await write("constants.ts", repo === "drizzle" ? generateDrizzleConstants({
1941
+ pascal,
1942
+ kebab
1943
+ }) : generateConstants({
1944
+ pascal,
1945
+ kebab
1946
+ }));
1947
+ await write(`presentation/${kebab}.controller.ts`, generateController$1({
1948
+ pascal,
1949
+ kebab,
1950
+ plural,
1951
+ pluralPascal
1952
+ }));
1953
+ await write(`application/dtos/create-${kebab}.dto.ts`, generateCreateDTO({
1954
+ pascal,
1955
+ kebab
1956
+ }));
1957
+ await write(`application/dtos/update-${kebab}.dto.ts`, generateUpdateDTO({
1958
+ pascal,
1959
+ kebab
1960
+ }));
1961
+ await write(`application/dtos/${kebab}-response.dto.ts`, generateResponseDTO({
1962
+ pascal,
1963
+ kebab
1964
+ }));
1965
+ const useCases = generateUseCases({
1966
+ pascal,
1967
+ kebab,
1968
+ plural,
1969
+ pluralPascal
1970
+ });
1971
+ for (const uc of useCases) await write(`application/use-cases/${uc.file}`, uc.content);
1972
+ await write(`domain/repositories/${kebab}.repository.ts`, generateRepositoryInterface({
1973
+ pascal,
1974
+ kebab
1975
+ }));
1976
+ await write(`domain/services/${kebab}-domain.service.ts`, generateDomainService({
1977
+ pascal,
1978
+ kebab
1979
+ }));
1980
+ const builtinRepoFileMap = {
1981
+ inmemory: `in-memory-${kebab}`,
1982
+ drizzle: `drizzle-${kebab}`,
1983
+ prisma: `prisma-${kebab}`
1984
+ };
1985
+ const builtinRepoGeneratorMap = {
1986
+ inmemory: () => generateInMemoryRepository({
1987
+ pascal,
1988
+ kebab
1989
+ }),
1990
+ drizzle: () => generateDrizzleRepository({
1991
+ pascal,
1992
+ kebab
1993
+ }),
1994
+ prisma: () => generatePrismaRepository({
1995
+ pascal,
1996
+ kebab,
1997
+ prismaClientPath
1998
+ })
1999
+ };
2000
+ const repoFile = builtinRepoFileMap[repo] ?? `${toKebabCase(repo)}-${kebab}`;
2001
+ const repoGenerator = builtinRepoGeneratorMap[repo] ?? (() => generateCustomRepository({
2002
+ pascal,
2003
+ kebab,
2004
+ repoType: repo
2005
+ }));
2006
+ await write(`infrastructure/repositories/${repoFile}.repository.ts`, repoGenerator());
2007
+ if (!noEntity) {
2008
+ await write(`domain/entities/${kebab}.entity.ts`, generateEntity({
2009
+ pascal,
2010
+ kebab
2011
+ }));
2012
+ await write(`domain/value-objects/${kebab}-id.vo.ts`, generateValueObject({
2013
+ pascal,
2014
+ kebab
2015
+ }));
2016
+ }
2017
+ if (!noTests) {
2018
+ if (repo !== "inmemory") await write(`infrastructure/repositories/in-memory-${kebab}.repository.ts`, generateInMemoryRepository({
2019
+ pascal,
2020
+ kebab
2021
+ }));
2022
+ await write(`__tests__/${kebab}.controller.test.ts`, generateControllerTest({
2023
+ pascal,
2024
+ kebab,
2025
+ plural
2026
+ }));
2027
+ await write(`__tests__/${kebab}.repository.test.ts`, generateRepositoryTest({
2028
+ pascal,
2029
+ kebab,
2030
+ plural
2031
+ }));
2032
+ }
2033
+ }
2034
+ //#endregion
2035
+ //#region src/generators/module.ts
2036
+ /** Prompt the user for a single-line answer via stdin */
2037
+ function promptUser(question) {
2038
+ const rl = createInterface({
2039
+ input: process.stdin,
2040
+ output: process.stdout
2041
+ });
2042
+ return new Promise((resolve) => {
2043
+ rl.question(question, (answer) => {
2044
+ rl.close();
2045
+ resolve(answer.trim().toLowerCase());
2046
+ });
2047
+ });
2048
+ }
2049
+ /**
2050
+ * Generate a module — structure depends on the project pattern.
2051
+ *
2052
+ * Patterns:
2053
+ * rest — flat folder: controller + service + DTOs + repo
2054
+ * ddd — nested DDD: presentation/ application/ domain/ infrastructure/
2055
+ * graphql — flat folder: resolver + service + DTOs + repo (future)
2056
+ * cqrs — commands, queries, events with WS/queue integration
2057
+ * minimal — just controller + module index
2058
+ */
2059
+ async function generateModule(options) {
2060
+ const { name, modulesDir, noEntity, noTests, repo = "inmemory", force, dryRun } = options;
2061
+ const shouldPluralize = options.pluralize !== false;
2062
+ let pattern = options.pattern ?? "ddd";
2063
+ if (options.minimal) pattern = "minimal";
2064
+ const kebab = toKebabCase(name);
2065
+ const pascal = toPascalCase(name);
2066
+ const plural = shouldPluralize ? pluralize(kebab) : kebab;
2067
+ const pluralPascal = shouldPluralize ? pluralizePascal(pascal) : pascal;
2068
+ const moduleDir = join(modulesDir, plural);
2069
+ const files = [];
2070
+ let overwriteAll = force ?? false;
2071
+ const write = async (relativePath, content) => {
2072
+ const fullPath = join(moduleDir, relativePath);
2073
+ if (dryRun) {
2074
+ files.push(fullPath);
2075
+ return;
2076
+ }
2077
+ if (!overwriteAll && await fileExists(fullPath)) {
2078
+ const answer = await promptUser(` File already exists: ${relativePath}\n Overwrite? (y/n/a = yes/no/all) `);
2079
+ if (answer === "a") overwriteAll = true;
2080
+ else if (answer !== "y") {
2081
+ console.log(` Skipped: ${relativePath}`);
2082
+ return;
2083
+ }
2084
+ }
2085
+ await writeFileSafe(fullPath, content);
2086
+ files.push(fullPath);
2087
+ };
2088
+ const ctx = {
2089
+ kebab,
2090
+ pascal,
2091
+ plural,
2092
+ pluralPascal,
2093
+ moduleDir,
2094
+ repo,
2095
+ noEntity: noEntity ?? false,
2096
+ noTests: noTests ?? false,
2097
+ prismaClientPath: options.prismaClientPath ?? "@prisma/client",
2098
+ write,
2099
+ files
2100
+ };
2101
+ switch (pattern) {
2102
+ case "minimal":
2103
+ await generateMinimalFiles(ctx);
2104
+ break;
2105
+ case "rest":
2106
+ await generateRestFiles(ctx);
2107
+ break;
2108
+ case "cqrs":
2109
+ await generateCqrsFiles(ctx);
2110
+ break;
2111
+ default:
2112
+ await generateDddFiles(ctx);
2113
+ break;
2114
+ }
2115
+ if (!dryRun) await autoRegisterModule(modulesDir, pascal, plural);
2116
+ return files;
2117
+ }
2118
+ /** Add the new module to src/modules/index.ts */
2119
+ async function autoRegisterModule(modulesDir, pascal, plural) {
2120
+ const indexPath = join(modulesDir, "index.ts");
2121
+ if (!await fileExists(indexPath)) {
2122
+ await writeFileSafe(indexPath, `import type { AppModuleClass } from '@forinda/kickjs'
2123
+ import { ${pascal}Module } from './${plural}'
2124
+
2125
+ export const modules: AppModuleClass[] = [${pascal}Module]
2126
+ `);
2127
+ return;
2128
+ }
2129
+ let content = await readFile(indexPath, "utf-8");
2130
+ const importLine = `import { ${pascal}Module } from './${plural}'`;
2131
+ if (!content.includes(`${pascal}Module`)) {
2132
+ const lastImportIdx = content.lastIndexOf("import ");
2133
+ if (lastImportIdx !== -1) {
2134
+ const lineEnd = content.indexOf("\n", lastImportIdx);
2135
+ content = content.slice(0, lineEnd + 1) + importLine + "\n" + content.slice(lineEnd + 1);
2136
+ } else content = importLine + "\n" + content;
2137
+ content = content.replace(/(=\s*\[)([\s\S]*?)(])/, (_match, open, existing, close) => {
2138
+ const trimmed = existing.trim();
2139
+ if (!trimmed) return `${open}${pascal}Module${close}`;
2140
+ const needsComma = trimmed.endsWith(",") ? "" : ",";
2141
+ return `${open}${existing.trimEnd()}${needsComma} ${pascal}Module${close}`;
2142
+ });
2143
+ }
2144
+ await writeFile(indexPath, content, "utf-8");
2145
+ }
2146
+ //#endregion
2147
+ //#region src/generators/adapter.ts
2148
+ async function generateAdapter(options) {
2149
+ const { name, outDir } = options;
2150
+ const kebab = toKebabCase(name);
2151
+ const pascal = toPascalCase(name);
2152
+ const files = [];
2153
+ const filePath = join(outDir, `${kebab}.adapter.ts`);
2154
+ await writeFileSafe(filePath, `import type { AppAdapter, AdapterContext, AdapterMiddleware } from '@forinda/kickjs'
2155
+
2156
+ export interface ${pascal}AdapterOptions {
2157
+ // Add your adapter configuration here
2158
+ }
2159
+
2160
+ /**
2161
+ * ${pascal} adapter.
2162
+ *
2163
+ * Hooks into the Application lifecycle to add middleware, routes,
2164
+ * or external service connections.
2165
+ *
2166
+ * Usage:
2167
+ * bootstrap({
2168
+ * adapters: [new ${pascal}Adapter({ ... })],
2169
+ * })
2170
+ */
2171
+ export class ${pascal}Adapter implements AppAdapter {
2172
+ name = '${pascal}Adapter'
2173
+
2174
+ constructor(private options: ${pascal}AdapterOptions = {}) {}
2175
+
2176
+ /**
2177
+ * Return middleware entries that the Application will mount.
2178
+ * Use \`phase\` to control where in the pipeline they run:
2179
+ * 'beforeGlobal' | 'afterGlobal' | 'beforeRoutes' | 'afterRoutes'
2180
+ */
2181
+ middleware(): AdapterMiddleware[] {
2182
+ return [
2183
+ // Example: add a custom header to all responses
2184
+ // {
2185
+ // phase: 'beforeGlobal',
2186
+ // handler: (_req: any, res: any, next: any) => {
2187
+ // res.setHeader('X-${pascal}', 'true')
2188
+ // next()
2189
+ // },
2190
+ // },
2191
+ // Example: scope middleware to a specific path
2192
+ // {
2193
+ // phase: 'beforeRoutes',
2194
+ // path: '/api/v1/admin',
2195
+ // handler: myAdminMiddleware(),
2196
+ // },
2197
+ ]
2198
+ }
2199
+
2200
+ /**
2201
+ * Called before global middleware.
2202
+ * Use this to mount routes that bypass the middleware stack
2203
+ * (health checks, docs UI, static assets).
2204
+ */
2205
+ beforeMount({ app }: AdapterContext): void {
2206
+ // Example: mount a status route
2207
+ // app.get('/${kebab}/status', (_req, res) => {
2208
+ // res.json({ status: 'ok' })
2209
+ // })
2210
+ }
2211
+
2212
+ /**
2213
+ * Called after modules and routes are registered, before the server starts.
2214
+ * Use this for late-stage DI registrations or config validation.
2215
+ */
2216
+ beforeStart({ container }: AdapterContext): void {
2217
+ // Example: register a service in the DI container
2218
+ // container.registerInstance(MY_TOKEN, new MyService(this.options))
2219
+ }
2220
+
2221
+ /**
2222
+ * Called after the HTTP server is listening.
2223
+ * Use this to attach to the raw http.Server (Socket.IO, gRPC, etc).
2224
+ */
2225
+ afterStart({ server, container }: AdapterContext): void {
2226
+ // Example: attach Socket.IO
2227
+ // const io = new Server(server)
2228
+ // container.registerInstance(SOCKET_IO, io)
2229
+ }
2230
+
2231
+ /**
2232
+ * Called on graceful shutdown. Clean up connections.
2233
+ */
2234
+ async shutdown(): Promise<void> {
2235
+ // Example: close a connection pool
2236
+ // await this.pool.end()
2237
+ }
2238
+ }
2239
+ `);
2240
+ files.push(filePath);
2241
+ return files;
2242
+ }
2243
+ //#endregion
2244
+ //#region src/utils/resolve-out-dir.ts
2245
+ /**
2246
+ * DDD folder mapping — nested layered architecture.
2247
+ */
2248
+ const DDD_FOLDER_MAP = {
2249
+ controller: "presentation",
2250
+ service: "domain/services",
2251
+ dto: "application/dtos",
2252
+ guard: "presentation/guards",
2253
+ middleware: "middleware"
2254
+ };
2255
+ /**
2256
+ * Flat folder mapping — REST/GraphQL/minimal patterns.
2257
+ * Files live at the module root or in minimal subdirectories.
2258
+ */
2259
+ const FLAT_FOLDER_MAP = {
2260
+ controller: "",
2261
+ service: "",
2262
+ dto: "dtos",
2263
+ guard: "guards",
2264
+ middleware: "middleware"
2265
+ };
2266
+ /**
2267
+ * CQRS folder mapping — commands, queries, events.
2268
+ */
2269
+ const CQRS_FOLDER_MAP = {
2270
+ controller: "",
2271
+ service: "",
2272
+ dto: "dtos",
2273
+ guard: "guards",
2274
+ middleware: "middleware",
2275
+ command: "commands",
2276
+ query: "queries",
2277
+ event: "events"
2278
+ };
2279
+ /**
2280
+ * Resolve the output directory for a generator artifact.
2281
+ *
2282
+ * Priority:
2283
+ * 1. Explicit --out flag (always wins)
2284
+ * 2. --module flag → maps into module's folder (DDD or flat based on pattern)
2285
+ * 3. Standalone default directory
2286
+ */
2287
+ function resolveOutDir(options) {
2288
+ const { type, outDir, moduleName, modulesDir = "src/modules", defaultDir, pattern = "ddd" } = options;
2289
+ if (outDir) return resolve(outDir);
2290
+ if (moduleName) {
2291
+ const folderMap = pattern === "ddd" ? DDD_FOLDER_MAP : pattern === "cqrs" ? CQRS_FOLDER_MAP : FLAT_FOLDER_MAP;
2292
+ const plural = pluralize(toKebabCase(moduleName));
2293
+ const subfolder = folderMap[type] ?? "";
2294
+ const base = join(modulesDir, plural);
2295
+ return resolve(subfolder ? join(base, subfolder) : base);
2296
+ }
2297
+ return resolve(defaultDir);
2298
+ }
2299
+ //#endregion
2300
+ //#region src/generators/middleware.ts
2301
+ async function generateMiddleware(options) {
2302
+ const { name, moduleName, modulesDir, pattern } = options;
2303
+ const outDir = resolveOutDir({
2304
+ type: "middleware",
2305
+ outDir: options.outDir,
2306
+ moduleName,
2307
+ modulesDir,
2308
+ defaultDir: "src/middleware",
2309
+ pattern
2310
+ });
2311
+ const kebab = toKebabCase(name);
2312
+ const camel = toCamelCase(name);
2313
+ const files = [];
2314
+ const filePath = join(outDir, `${kebab}.middleware.ts`);
2315
+ await writeFileSafe(filePath, `import type { Request, Response, NextFunction } from 'express'
2316
+
2317
+ export interface ${toPascalCase(name)}Options {
2318
+ // Add configuration options here
2319
+ }
2320
+
2321
+ /**
2322
+ * ${toPascalCase(name)} middleware.
2323
+ *
2324
+ * Usage in bootstrap:
2325
+ * middleware: [${camel}()]
2326
+ *
2327
+ * Usage with adapter:
2328
+ * middleware() { return [{ handler: ${camel}(), phase: 'afterGlobal' }] }
2329
+ *
2330
+ * Usage with @Middleware decorator:
2331
+ * @Middleware(${camel}())
2332
+ */
2333
+ export function ${camel}(options: ${toPascalCase(name)}Options = {}) {
2334
+ return (req: Request, res: Response, next: NextFunction) => {
2335
+ // Implement your middleware logic here
2336
+ next()
2337
+ }
2338
+ }
2339
+ `);
2340
+ files.push(filePath);
2341
+ return files;
2342
+ }
2343
+ //#endregion
2344
+ //#region src/generators/guard.ts
2345
+ async function generateGuard(options) {
2346
+ const { name, moduleName, modulesDir, pattern } = options;
2347
+ const outDir = resolveOutDir({
2348
+ type: "guard",
2349
+ outDir: options.outDir,
2350
+ moduleName,
2351
+ modulesDir,
2352
+ defaultDir: "src/guards",
2353
+ pattern
2354
+ });
2355
+ const kebab = toKebabCase(name);
2356
+ const camel = toCamelCase(name);
2357
+ const pascal = toPascalCase(name);
2358
+ const files = [];
2359
+ const filePath = join(outDir, `${kebab}.guard.ts`);
2360
+ await writeFileSafe(filePath, `import { Container, HttpException } from '@forinda/kickjs'
2361
+ import type { RequestContext } from '@forinda/kickjs'
2362
+
2363
+ /**
2364
+ * ${pascal} guard.
2365
+ *
2366
+ * Guards protect routes by checking conditions before the handler runs.
2367
+ * Return early with an error response to block access.
2368
+ *
2369
+ * Usage:
2370
+ * @Middleware(${camel}Guard)
2371
+ * @Get('/protected')
2372
+ * async handler(ctx: RequestContext) { ... }
2373
+ */
2374
+ export async function ${camel}Guard(ctx: RequestContext, next: () => void): Promise<void> {
2375
+ // Example: check for an authorization header
2376
+ const header = ctx.headers.authorization
2377
+ if (!header?.startsWith('Bearer ')) {
2378
+ ctx.res.status(401).json({ message: 'Missing or invalid authorization header' })
2379
+ return
2380
+ }
2381
+
2382
+ const token = header.slice(7)
2383
+
2384
+ try {
2385
+ // Verify the token using a service from the DI container
2386
+ // const container = Container.getInstance()
2387
+ // const authService = container.resolve(AuthService)
2388
+ // const payload = authService.verifyToken(token)
2389
+ // ctx.set('auth', payload)
2390
+
2391
+ next()
2392
+ } catch {
2393
+ ctx.res.status(401).json({ message: 'Invalid or expired token' })
2394
+ }
2395
+ }
2396
+ `);
2397
+ files.push(filePath);
2398
+ return files;
2399
+ }
2400
+ //#endregion
2401
+ //#region src/generators/service.ts
2402
+ async function generateService(options) {
2403
+ const { name, moduleName, modulesDir, pattern } = options;
2404
+ const outDir = resolveOutDir({
2405
+ type: "service",
2406
+ outDir: options.outDir,
2407
+ moduleName,
2408
+ modulesDir,
2409
+ defaultDir: "src/services",
2410
+ pattern
2411
+ });
2412
+ const kebab = toKebabCase(name);
2413
+ const pascal = toPascalCase(name);
2414
+ const files = [];
2415
+ const filePath = join(outDir, `${kebab}.service.ts`);
2416
+ await writeFileSafe(filePath, `import { Service } from '@forinda/kickjs'
2417
+
2418
+ @Service()
2419
+ export class ${pascal}Service {
2420
+ // Inject dependencies via constructor
2421
+ // constructor(
2422
+ // @Inject(MY_REPO) private readonly repo: IMyRepository,
2423
+ // ) {}
2424
+ }
2425
+ `);
2426
+ files.push(filePath);
2427
+ return files;
2428
+ }
2429
+ //#endregion
2430
+ //#region src/generators/controller.ts
2431
+ async function generateController(options) {
2432
+ const { name, moduleName, modulesDir, pattern } = options;
2433
+ const outDir = resolveOutDir({
2434
+ type: "controller",
2435
+ outDir: options.outDir,
2436
+ moduleName,
2437
+ modulesDir,
2438
+ defaultDir: "src/controllers",
2439
+ pattern
2440
+ });
2441
+ const kebab = toKebabCase(name);
2442
+ const pascal = toPascalCase(name);
2443
+ const files = [];
2444
+ const filePath = join(outDir, `${kebab}.controller.ts`);
2445
+ await writeFileSafe(filePath, `import { Controller, Get, Post, Autowired } from '@forinda/kickjs'
2446
+ import type { RequestContext } from '@forinda/kickjs'
2447
+
2448
+ @Controller()
2449
+ export class ${pascal}Controller {
2450
+ // @Autowired() private myService!: MyService
2451
+
2452
+ @Get('/')
2453
+ async list(ctx: RequestContext) {
2454
+ ctx.json({ message: '${pascal} list' })
2455
+ }
2456
+
2457
+ @Post('/')
2458
+ async create(ctx: RequestContext) {
2459
+ ctx.created({ message: '${pascal} created', data: ctx.body })
2460
+ }
2461
+ }
2462
+ `);
2463
+ files.push(filePath);
2464
+ return files;
2465
+ }
2466
+ //#endregion
2467
+ //#region src/generators/dto.ts
2468
+ async function generateDto(options) {
2469
+ const { name, moduleName, modulesDir, pattern } = options;
2470
+ const outDir = resolveOutDir({
2471
+ type: "dto",
2472
+ outDir: options.outDir,
2473
+ moduleName,
2474
+ modulesDir,
2475
+ defaultDir: "src/dtos",
2476
+ pattern
2477
+ });
2478
+ const kebab = toKebabCase(name);
2479
+ const pascal = toPascalCase(name);
2480
+ const camel = toCamelCase(name);
2481
+ const files = [];
2482
+ const filePath = join(outDir, `${kebab}.dto.ts`);
2483
+ await writeFileSafe(filePath, `import { z } from 'zod'
2484
+
2485
+ export const ${camel}Schema = z.object({
2486
+ // Define your schema fields here
2487
+ name: z.string().min(1).max(200),
2488
+ })
2489
+
2490
+ export type ${pascal}DTO = z.infer<typeof ${camel}Schema>
2491
+ `);
2492
+ files.push(filePath);
2493
+ return files;
2494
+ }
2495
+ //#endregion
2496
+ //#region src/generators/templates/project-config.ts
2497
+ /** Generate package.json with template-aware dependencies */
2498
+ function generatePackageJson(name, template, kickjsVersion) {
2499
+ const baseDeps = {
2500
+ "@forinda/kickjs": kickjsVersion,
2501
+ "@forinda/kickjs-config": kickjsVersion,
2502
+ express: "^5.1.0",
2503
+ "reflect-metadata": "^0.2.2",
2504
+ zod: "^4.3.6",
2505
+ pino: "^10.3.1",
2506
+ "pino-pretty": "^13.1.3"
2507
+ };
2508
+ if (template !== "minimal") {
2509
+ baseDeps["@forinda/kickjs-swagger"] = kickjsVersion;
2510
+ baseDeps["@forinda/kickjs-devtools"] = kickjsVersion;
2511
+ }
2512
+ if (template === "graphql") {
2513
+ baseDeps["@forinda/kickjs-graphql"] = kickjsVersion;
2514
+ baseDeps["graphql"] = "^16.11.0";
2515
+ }
2516
+ if (template === "cqrs") {
2517
+ baseDeps["@forinda/kickjs-queue"] = kickjsVersion;
2518
+ baseDeps["@forinda/kickjs-ws"] = kickjsVersion;
2519
+ baseDeps["@forinda/kickjs-otel"] = kickjsVersion;
2520
+ }
2521
+ if (template === "ddd") baseDeps["@forinda/kickjs-swagger"] = kickjsVersion;
2522
+ return JSON.stringify({
2523
+ name,
2524
+ version: kickjsVersion.replace("^", ""),
2525
+ type: "module",
2526
+ scripts: {
2527
+ dev: "vite",
2528
+ "dev:debug": "kick dev:debug",
2529
+ build: "kick build",
2530
+ start: "kick start",
2531
+ test: "vitest run",
2532
+ "test:watch": "vitest",
2533
+ typecheck: "tsc --noEmit",
2534
+ typegen: "kick typegen",
2535
+ lint: "eslint src/",
2536
+ format: "prettier --write src/"
2537
+ },
2538
+ dependencies: baseDeps,
2539
+ devDependencies: {
2540
+ "@forinda/kickjs-cli": kickjsVersion,
2541
+ "@forinda/kickjs-vite": kickjsVersion,
2542
+ "@swc/core": "^1.15.21",
2543
+ "@types/express": "^5.0.6",
2544
+ "@types/node": "^25.0.0",
2545
+ "unplugin-swc": "^1.5.9",
2546
+ vite: "^8.0.3",
2547
+ vitest: "^4.1.2",
2548
+ typescript: "^5.9.2",
2549
+ prettier: "^3.8.1"
2550
+ }
2551
+ }, null, 2);
2552
+ }
2553
+ /**
2554
+ * Generate vite.config.ts with the KickJS Vite plugin.
2555
+ *
2556
+ * The plugin handles:
2557
+ * - SSR environment setup for backend Node.js code
2558
+ * - Virtual module generation (virtual:kickjs/app)
2559
+ * - Module auto-discovery (scans *.module.ts files)
2560
+ * - HMR with selective container invalidation
2561
+ * - Express mounting via configureServer() post-hook
2562
+ * - httpServer piping to adapters (WsAdapter, Socket.IO, etc.)
2563
+ */
2564
+ function generateViteConfig() {
2565
+ return `import { defineConfig } from 'vite'
2566
+ import { resolve } from 'path'
2567
+ import swc from 'unplugin-swc'
2568
+ import { kickjsVitePlugin } from '@forinda/kickjs-vite'
2569
+
2570
+ export default defineConfig({
2571
+ oxc: false,
2572
+ plugins: [
2573
+ swc.vite(),
2574
+ kickjsVitePlugin({ entry: 'src/index.ts' }),
2575
+ ],
2576
+ resolve: {
2577
+ alias: {
2578
+ '@': resolve(__dirname, 'src'),
2579
+ },
2580
+ },
2581
+ ssr: {
2582
+ // Don't bundle pino — its worker-thread transport needs Node.js resolution
2583
+ // to find pino-pretty at runtime for colored log output
2584
+ external: ['pino', 'pino-pretty'],
2585
+ },
2586
+ build: {
2587
+ target: 'node20',
2588
+ ssr: true,
2589
+ outDir: 'dist',
2590
+ sourcemap: true,
2591
+ rollupOptions: {
2592
+ input: resolve(__dirname, 'src/index.ts'),
2593
+ output: { format: 'esm' },
2594
+ },
2595
+ },
2596
+ })
2597
+ `;
2598
+ }
2599
+ /** Generate tsconfig.json with decorator support */
2600
+ function generateTsConfig() {
2601
+ return JSON.stringify({
2602
+ compilerOptions: {
2603
+ target: "ES2022",
2604
+ module: "ESNext",
2605
+ moduleResolution: "bundler",
2606
+ lib: ["ES2022"],
2607
+ types: ["node", "vite/client"],
2608
+ strict: true,
2609
+ esModuleInterop: true,
2610
+ skipLibCheck: true,
2611
+ sourceMap: true,
2612
+ declaration: true,
2613
+ experimentalDecorators: true,
2614
+ emitDecoratorMetadata: true,
2615
+ outDir: "dist",
2616
+ rootDir: "src",
2617
+ paths: { "@/*": ["./src/*"] }
2618
+ },
2619
+ include: ["src"]
2620
+ }, null, 2);
2621
+ }
2622
+ /** Generate .prettierrc with project formatting rules */
2623
+ function generatePrettierConfig() {
2624
+ return JSON.stringify({
2625
+ semi: false,
2626
+ singleQuote: true,
2627
+ trailingComma: "all",
2628
+ printWidth: 100,
2629
+ tabWidth: 2
2630
+ }, null, 2);
2631
+ }
2632
+ /** Generate .editorconfig for consistent editor settings */
2633
+ function generateEditorConfig() {
2634
+ return `# https://editorconfig.org
2635
+ root = true
2636
+
2637
+ [*]
2638
+ indent_style = space
2639
+ indent_size = 2
2640
+ end_of_line = lf
2641
+ charset = utf-8
2642
+ trim_trailing_whitespace = true
2643
+ insert_final_newline = true
2644
+
2645
+ [*.md]
2646
+ trim_trailing_whitespace = false
2647
+ `;
2648
+ }
2649
+ /** Generate .gitignore with common Node.js patterns */
2650
+ function generateGitIgnore() {
2651
+ return `node_modules/
2652
+ dist/
2653
+ .env
2654
+ coverage/
2655
+ .DS_Store
2656
+ *.tsbuildinfo
2657
+ `;
2658
+ }
2659
+ /** Generate .gitattributes for consistent line endings */
2660
+ function generateGitAttributes() {
2661
+ return `# Auto-detect text files and normalise line endings to LF
2662
+ * text=auto eol=lf
2663
+
2664
+ # Explicitly mark generated / binary files
2665
+ *.png binary
2666
+ *.jpg binary
2667
+ *.jpeg binary
2668
+ *.gif binary
2669
+ *.ico binary
2670
+ *.woff binary
2671
+ *.woff2 binary
2672
+ *.ttf binary
2673
+ *.eot binary
2674
+
2675
+ # Lock files — treat as generated
2676
+ pnpm-lock.yaml -diff linguist-generated
2677
+ yarn.lock -diff linguist-generated
2678
+ package-lock.json -diff linguist-generated
2679
+ `;
2680
+ }
2681
+ /** Generate .env file with default environment variables */
2682
+ function generateEnv() {
2683
+ return `PORT=3000
2684
+ NODE_ENV=development
2685
+ `;
2686
+ }
2687
+ /** Generate .env.example file as a template */
2688
+ function generateEnvExample() {
2689
+ return `PORT=3000
2690
+ NODE_ENV=development
2691
+ `;
2692
+ }
2693
+ /** Generate vitest.config.ts for test configuration */
2694
+ function generateVitestConfig() {
2695
+ return `import { defineConfig } from 'vitest/config'
2696
+ import swc from 'unplugin-swc'
2697
+
2698
+ export default defineConfig({
2699
+ plugins: [swc.vite()],
2700
+ test: {
2701
+ globals: true,
2702
+ environment: 'node',
2703
+ include: ['src/**/*.test.ts'],
2704
+ },
2705
+ })
2706
+ `;
2707
+ }
2708
+ //#endregion
2709
+ //#region src/generators/templates/project-docs.ts
2710
+ /** Generate README.md with project documentation */
2711
+ function generateReadme(name, template, pm) {
2712
+ const templateLabels = {
2713
+ rest: "REST API",
2714
+ graphql: "GraphQL API",
2715
+ ddd: "Domain-Driven Design",
2716
+ cqrs: "CQRS + Event-Driven",
2717
+ minimal: "Minimal"
2718
+ };
2719
+ const packages = [
2720
+ "@forinda/kickjs",
2721
+ "@forinda/kickjs-vite",
2722
+ "@forinda/kickjs-config"
2723
+ ];
2724
+ if (template !== "minimal") packages.push("@forinda/kickjs-swagger", "@forinda/kickjs-devtools");
2725
+ if (template === "graphql") packages.push("@forinda/kickjs-graphql");
2726
+ if (template === "cqrs") packages.push("@forinda/kickjs-queue", "@forinda/kickjs-ws", "@forinda/kickjs-otel");
2727
+ return `# ${name}
2728
+
2729
+ A **${templateLabels[template] ?? "REST API"}** built with [KickJS](https://forinda.github.io/kick-js/) — a decorator-driven Node.js framework on Express 5 and TypeScript.
2730
+
2731
+ ## Getting Started
2732
+
2733
+ \`\`\`bash
2734
+ ${pm} install
2735
+ kick dev
2736
+ \`\`\`
2737
+
2738
+ ## Scripts
2739
+
2740
+ | Command | Description |
2741
+ |---|---|
2742
+ | \`kick dev\` | Start dev server with Vite HMR |
2743
+ | \`kick build\` | Production build |
2744
+ | \`kick start\` | Run production build |
2745
+ | \`${pm} run test\` | Run tests with Vitest |
2746
+ | \`kick g module <name>\` | Generate a DDD module |
2747
+ | \`kick g scaffold <name> <fields...>\` | Generate CRUD from field definitions |
2748
+ | \`kick add <package>\` | Add a KickJS package |
2749
+
2750
+ ## Project Structure
2751
+
2752
+ \`\`\`
2753
+ src/
2754
+ ├── index.ts # Application entry point
2755
+ ├── modules/ # Feature modules (controllers, services, repos)
2756
+ │ └── index.ts # Module registry
2757
+ └── ...
2758
+ \`\`\`
2759
+
2760
+ ## Packages
2761
+
2762
+ ${packages.map((p) => `- \`${p}\``).join("\n")}
2763
+
2764
+ ## Adding Features
2765
+
2766
+ \`\`\`bash
2767
+ kick add auth # Authentication (JWT, API key, OAuth)
2768
+ kick add swagger # OpenAPI documentation
2769
+ kick add ws # WebSocket support
2770
+ kick add queue # Background job processing
2771
+ kick add mailer # Email sending
2772
+ kick add cron # Scheduled tasks
2773
+ kick add --list # Show all available packages
2774
+ \`\`\`
2775
+
2776
+ ## Environment Variables
2777
+
2778
+ Copy \`.env.example\` to \`.env\` and configure:
2779
+
2780
+ | Variable | Default | Description |
2781
+ |---|---|---|
2782
+ | \`PORT\` | \`3000\` | Server port |
2783
+ | \`NODE_ENV\` | \`development\` | Environment |
2784
+
2785
+ ## Learn More
2786
+
2787
+ - [KickJS Documentation](https://forinda.github.io/kick-js/)
2788
+ - [CLI Reference](https://forinda.github.io/kick-js/api/cli.html)
2789
+ `;
2790
+ }
2791
+ /** Generate CLAUDE.md with AI development guide */
2792
+ function generateClaude(name, template, pm) {
2793
+ return `# CLAUDE.md — ${name} Development Guide
2794
+
2795
+ ## Project Overview
2796
+
2797
+ This is a **${{
2798
+ rest: "REST API",
2799
+ graphql: "GraphQL API",
2800
+ ddd: "Domain-Driven Design",
2801
+ cqrs: "CQRS + Event-Driven",
2802
+ minimal: "Minimal Express"
2803
+ }[template] ?? "REST API"}** application built with [KickJS](https://forinda.github.io/kick-js/) — a decorator-driven Node.js framework on Express 5 and TypeScript.
2804
+
2805
+ ## Quick Commands
2806
+
2807
+ \`\`\`bash
2808
+ ${pm} install # Install dependencies
2809
+ kick dev # Start dev server with HMR
2810
+ kick build # Production build via Vite
2811
+ kick start # Run production build
2812
+ ${pm} run test # Run tests with Vitest
2813
+ ${pm} run typecheck # TypeScript type checking
2814
+ ${pm} run format # Format code with Prettier
2815
+ \`\`\`
2816
+
2817
+ ## Project Structure
2818
+
2819
+ \`\`\`
2820
+ src/
2821
+ ├── index.ts # Application bootstrap
2822
+ ├── modules/ # Feature modules (DDD/CQRS pattern)
2823
+ │ └── index.ts # Module registry
2824
+ ${template === "graphql" ? "├── resolvers/ # GraphQL resolvers\n" : ""}└── ...
2825
+ \`\`\`
2826
+
2827
+ ## Package Manager
2828
+
2829
+ - Always use **${pm}** for this project
2830
+ - Run \`${pm} install\` to sync dependencies
2831
+ - Never mix package managers (npm/yarn/pnpm)
2832
+
2833
+ ## Code Style
2834
+
2835
+ - **Prettier** — no semicolons, single quotes, trailing commas, 100 char width
2836
+ - **TypeScript strict mode** — all types required
2837
+ - Format before committing: \`${pm} run format\`
2838
+ - Type check with: \`${pm} run typecheck\`
2839
+
2840
+ ## Key Patterns
2841
+
2842
+ ### Controllers
2843
+
2844
+ Use decorators to define routes:
2845
+
2846
+ \`\`\`ts
2847
+ import { Controller, Get, Post, RequestContext } from '@forinda/kickjs'
2848
+
2849
+ @Controller('/users')
2850
+ export class UserController {
2851
+ @Get('/')
2852
+ async findAll(ctx: RequestContext) {
2853
+ return ctx.json({ users: [] })
2854
+ }
2855
+
2856
+ @Post('/')
2857
+ async create(ctx: RequestContext) {
2858
+ const data = ctx.body
2859
+ return ctx.created({ user: data })
2860
+ }
2861
+ }
2862
+ \`\`\`
2863
+
2864
+ ### Services
2865
+
2866
+ Inject dependencies with \`@Service()\` and \`@Autowired()\`:
2867
+
2868
+ \`\`\`ts
2869
+ import { Service, Autowired } from '@forinda/kickjs'
2870
+
2871
+ @Service()
2872
+ export class UserService {
2873
+ @Autowired()
2874
+ private userRepository!: UserRepository
2875
+
2876
+ async findAll() {
2877
+ return this.userRepository.findAll()
2878
+ }
2879
+ }
2880
+ \`\`\`
2881
+
2882
+ ### Modules
2883
+
2884
+ Register controllers and providers in modules:
2885
+
2886
+ \`\`\`ts
2887
+ import { Module } from '@forinda/kickjs'
2888
+ import { UserController } from './user.controller'
2889
+ import { UserService } from './user.service'
2890
+
2891
+ @Module({
2892
+ controllers: [UserController],
2893
+ providers: [UserService],
2894
+ })
2895
+ export class UserModule {}
2896
+ \`\`\`
2897
+
2898
+ ### RequestContext
2899
+
2900
+ Every controller method receives \`ctx: RequestContext\`:
2901
+
2902
+ \`\`\`ts
2903
+ ctx.body // Request body (parsed JSON)
2904
+ ctx.params // Route params
2905
+ ctx.query // Query string
2906
+ ctx.headers // Request headers
2907
+ ctx.requestId // Auto-generated request ID
2908
+ ctx.session // Session data (if session middleware enabled)
2909
+ ctx.file // Uploaded file (single)
2910
+ ctx.files // Uploaded files (multiple)
2911
+
2912
+ // Pagination helpers
2913
+ ctx.qs(config) // Parse query with filters/sort/pagination
2914
+ ctx.paginate(handler) // Auto-paginated response
2915
+
2916
+ // Response helpers
2917
+ ctx.json(data) // 200 OK with JSON
2918
+ ctx.created(data) // 201 Created
2919
+ ctx.noContent() // 204 No Content
2920
+ ctx.notFound() // 404 Not Found
2921
+ ctx.badRequest(msg) // 400 Bad Request
2922
+ \`\`\`
2923
+
2924
+ ## CLI Generators
2925
+
2926
+ Generate code with the \`kick\` CLI:
2927
+
2928
+ \`\`\`bash
2929
+ kick g module <name> # Full module (controller, service, DTOs, repo)
2930
+ kick g scaffold <name> <fields> # CRUD module from field definitions
2931
+ kick g controller <name> # Standalone controller
2932
+ kick g service <name> # Service class
2933
+ kick g middleware <name> # Express middleware
2934
+ kick g guard <name> # Route guard (auth, roles)
2935
+ kick g adapter <name> # AppAdapter with lifecycle hooks
2936
+ kick g dto <name> # Zod DTO schema
2937
+ ${template === "graphql" ? "kick g resolver <name> # GraphQL resolver\n" : ""}${template === "cqrs" ? "kick g job <name> # Queue job processor\n" : ""}\`\`\`
2938
+
2939
+ ## Adding Packages
2940
+
2941
+ \`\`\`bash
2942
+ kick add auth # JWT, API key, OAuth strategies
2943
+ kick add swagger # OpenAPI docs from decorators
2944
+ kick add ws # WebSocket support
2945
+ kick add queue # Background jobs (BullMQ/RabbitMQ/Kafka)
2946
+ kick add mailer # Email (SMTP, Resend, SES)
2947
+ kick add cron # Scheduled tasks
2948
+ kick add prisma # Prisma ORM adapter
2949
+ kick add drizzle # Drizzle ORM adapter
2950
+ kick add otel # OpenTelemetry tracing
2951
+ kick add --list # Show all available packages
2952
+ \`\`\`
2953
+
2954
+ ## Environment Configuration
2955
+
2956
+ Edit \`.env\` for environment variables. Access them with \`@Value()\` decorator:
2957
+
2958
+ \`\`\`ts
2959
+ import { Value } from '@forinda/kickjs-config'
2960
+
2961
+ @Service()
2962
+ export class ApiService {
2963
+ @Value('API_KEY')
2964
+ private apiKey!: string
2965
+
2966
+ @Value('PORT', 3000) // With default
2967
+ private port!: number
2968
+ }
2969
+ \`\`\`
2970
+
2971
+ Or use \`ConfigService\`:
2972
+
2973
+ \`\`\`ts
2974
+ import { ConfigService } from '@forinda/kickjs-config'
2975
+
2976
+ @Service()
2977
+ export class AppService {
2978
+ @Autowired()
2979
+ private config!: ConfigService
2980
+
2981
+ getPort() {
2982
+ return this.config.get('PORT', 3000)
2983
+ }
2984
+ }
2985
+ \`\`\`
2986
+
2987
+ ## Testing
2988
+
2989
+ Tests live in \`src/**/*.test.ts\`:
2990
+
2991
+ \`\`\`ts
2992
+ import { describe, it, expect, beforeEach } from 'vitest'
2993
+ import { Container } from '@forinda/kickjs'
2994
+ import { createTestApp } from '@forinda/kickjs-testing'
2995
+
2996
+ describe('UserController', () => {
2997
+ beforeEach(() => Container.reset())
2998
+
2999
+ it('should return users', async () => {
3000
+ const app = await createTestApp([UserModule])
3001
+ const res = await app.get('/users')
3002
+ expect(res.status).toBe(200)
3003
+ })
3004
+ })
3005
+ \`\`\`
3006
+
3007
+ Run tests:
3008
+ - \`${pm} run test\` — run all tests
3009
+ - \`${pm} run test:watch\` — watch mode
3010
+
3011
+ ## Decorators Reference
3012
+
3013
+ ### Route Decorators
3014
+ - \`@Controller('/path')\` — define controller prefix
3015
+ - \`@Get('/'), @Post('/'), @Put('/'), @Delete('/'), @Patch('/')\` — HTTP methods
3016
+ - \`@Middleware(fn)\` — attach middleware
3017
+ - \`@Public()\` — skip authentication (requires @forinda/kickjs-auth)
3018
+ - \`@Roles('admin', 'user')\` — role-based access control
3019
+
3020
+ ### DI Decorators
3021
+ - \`@Module({ controllers, providers, imports })\` — define module
3022
+ - \`@Service()\` — singleton service (DI-registered)
3023
+ - \`@Repository()\` — repository (semantic alias for @Service)
3024
+ - \`@Autowired()\` — property injection
3025
+ - \`@Inject('token')\` — token-based injection
3026
+ - \`@Value('ENV_VAR')\` — inject config value
3027
+
3028
+ ${template === "cqrs" ? `### CQRS/Event Decorators
3029
+ - \`@Job('job-name')\` — queue job handler
3030
+ - \`@Process('queue-name')\` — queue processor
3031
+ - \`@Cron('0 * * * *')\` — cron schedule
3032
+ - \`@WsController('/path')\` — WebSocket controller
3033
+ - \`@Subscribe('event')\` — WebSocket event handler
3034
+
3035
+ ` : ""}${template === "graphql" ? `### GraphQL Decorators
3036
+ - \`@Resolver()\` — GraphQL resolver
3037
+ - \`@Query()\` — GraphQL query
3038
+ - \`@Mutation()\` — GraphQL mutation
3039
+ - \`@Arg('name')\` — resolver argument
3040
+
3041
+ ` : ""}## Common Pitfalls
3042
+
3043
+ 1. **Decorators fire at import time** — make sure to import module classes in \`src/modules/index.ts\`
3044
+ 2. **Tests need \`Container.reset()\`** — call in \`beforeEach\` to isolate DI state
3045
+ 3. **Always use \`ctx.body\`** — never \`req.body\` directly
3046
+ 4. **DI requires \`reflect-metadata\`** — already imported in \`src/index.ts\`
3047
+ 5. **Vite HMR requires proper cleanup** — adapters should implement \`shutdown()\`
3048
+
3049
+ ## Learn More
3050
+
3051
+ - [KickJS Documentation](https://forinda.github.io/kick-js/)
3052
+ - [API Reference](https://forinda.github.io/kick-js/api/)
3053
+ - [CLI Commands](https://forinda.github.io/kick-js/guide/cli-commands.html)
3054
+ - [Decorators Guide](https://forinda.github.io/kick-js/guide/decorators.html)
3055
+ `;
3056
+ }
3057
+ /** Generate AGENTS.md with AI agent guide */
3058
+ function generateAgents(name, template, pm) {
3059
+ return `# AGENTS.md — AI Agent Guide for ${name}
3060
+
3061
+ This guide helps AI agents (Claude, Copilot, etc.) work effectively on this KickJS application.
3062
+
3063
+ ## Before You Start
3064
+
3065
+ 1. Read \`CLAUDE.md\` for project conventions and commands
3066
+ 2. Run \`${pm} install\` to install dependencies
3067
+ 3. Run \`kick dev\` to verify the app starts
3068
+ 4. Read the [KickJS documentation](https://forinda.github.io/kick-js/) for framework details
3069
+
3070
+ ## Where to Find Things
3071
+
3072
+ ### Application Structure
3073
+
3074
+ | What | Where |
3075
+ |------|-------|
3076
+ | Entry point | \`src/index.ts\` |
3077
+ | Module registry | \`src/modules/index.ts\` |
3078
+ | Feature modules | \`src/modules/<module-name>/\` |
3079
+ ${template === "graphql" ? "| GraphQL resolvers | `src/resolvers/` |\n" : ""}| Environment config | \`.env\` |
3080
+ | TypeScript config | \`tsconfig.json\` |
3081
+ | Vite config (HMR) | \`vite.config.ts\` |
3082
+ | Vitest config | \`vitest.config.ts\` |
3083
+ | Prettier config | \`.prettierrc\` |
3084
+ | CLI config | \`kick.config.ts\` |
3085
+
3086
+ ### Module Pattern (${template.toUpperCase()})
3087
+
3088
+ Each module in \`src/modules/<name>/\` typically contains:
3089
+
3090
+ ${template === "ddd" ? `\`\`\`
3091
+ <name>/
3092
+ ├── <name>.controller.ts # HTTP routes (@Controller)
3093
+ ├── <name>.service.ts # Business logic (@Service)
3094
+ ├── <name>.repository.ts # Data access (@Repository)
3095
+ ├── <name>.dto.ts # Request/response schemas (Zod)
3096
+ ├── <name>.entity.ts # Domain entity (optional)
3097
+ └── <name>.module.ts # Module definition (@Module)
3098
+ \`\`\`
3099
+ ` : template === "cqrs" ? `\`\`\`
3100
+ <name>/
3101
+ ├── commands/ # Write operations
3102
+ │ ├── create-<name>.command.ts
3103
+ │ └── create-<name>.handler.ts
3104
+ ├── queries/ # Read operations
3105
+ │ ├── get-<name>.query.ts
3106
+ │ └── get-<name>.handler.ts
3107
+ ├── events/ # Domain events
3108
+ │ └── <name>-created.event.ts
3109
+ ├── <name>.controller.ts # HTTP routes
3110
+ ├── <name>.repository.ts # Data access
3111
+ └── <name>.module.ts # Module definition
3112
+ \`\`\`
3113
+ ` : template === "graphql" ? `\`\`\`
3114
+ resolvers/
3115
+ ├── <name>.resolver.ts # @Resolver, @Query, @Mutation
3116
+ ├── <name>.types.ts # GraphQL type definitions
3117
+ └── <name>.service.ts # Business logic
3118
+ \`\`\`
3119
+ ` : template === "rest" ? `\`\`\`
3120
+ <name>/
3121
+ ├── <name>.controller.ts # HTTP routes (@Controller)
3122
+ ├── <name>.service.ts # Business logic (@Service)
3123
+ ├── <name>.dto.ts # Request/response schemas (Zod)
3124
+ └── <name>.module.ts # Module definition (@Module)
3125
+ \`\`\`
3126
+ ` : `\`\`\`
3127
+ src/
3128
+ ├── index.ts # Add routes here
3129
+ └── ... # Custom structure
3130
+ \`\`\`
3131
+ `}
3132
+
3133
+ ## Checklist: Adding a Feature
3134
+
3135
+ ### New Module (Recommended)
3136
+
3137
+ Use the CLI generator for consistency:
3138
+
3139
+ \`\`\`bash
3140
+ kick g module <name> # Generate full module
3141
+ # or
3142
+ kick g scaffold <name> <fields> # Generate CRUD from fields
3143
+ \`\`\`
3144
+
3145
+ Then:
3146
+ - [ ] Review generated files in \`src/modules/<name>/\`
3147
+ - [ ] Verify module is registered in \`src/modules/index.ts\`
3148
+ - [ ] Update DTOs in \`<name>.dto.ts\` if needed
3149
+ - [ ] Implement business logic in \`<name>.service.ts\`
3150
+ - [ ] Run \`kick dev\` to test with HMR
3151
+ - [ ] Write tests in \`<name>.test.ts\`
3152
+
3153
+ ### Manual Controller
3154
+
3155
+ If not using generators:
3156
+
3157
+ - [ ] Create \`src/modules/<name>/<name>.controller.ts\`
3158
+ - [ ] Add \`@Controller('/path')\` decorator
3159
+ - [ ] Add route handlers with \`@Get()\`, \`@Post()\`, etc.
3160
+ - [ ] Create module file with \`@Module({ controllers: [NameController] })\`
3161
+ - [ ] Register module in \`src/modules/index.ts\`
3162
+ - [ ] Test with \`kick dev\`
3163
+
3164
+ ### Manual Service
3165
+
3166
+ - [ ] Create \`src/modules/<name>/<name>.service.ts\`
3167
+ - [ ] Add \`@Service()\` decorator
3168
+ - [ ] Inject dependencies with \`@Autowired()\`
3169
+ - [ ] Register in module \`providers\` array
3170
+ - [ ] Write unit tests
3171
+
3172
+ ### New Middleware
3173
+
3174
+ - [ ] Create \`src/middleware/<name>.middleware.ts\`
3175
+ - [ ] Export middleware function (Express format)
3176
+ - [ ] Register in \`src/index.ts\` or attach to routes with \`@Middleware()\`
3177
+ - [ ] Test with sample requests
3178
+
3179
+ ### Adding a Package
3180
+
3181
+ Use \`kick add\` to install KickJS packages with correct peer dependencies:
3182
+
3183
+ - [ ] Run \`kick add <package>\` (e.g., \`kick add auth\`)
3184
+ - [ ] Follow package-specific setup in terminal output
3185
+ - [ ] Update \`src/index.ts\` to register adapter (if needed)
3186
+ - [ ] Configure environment variables in \`.env\`
3187
+ - [ ] Test integration with \`kick dev\`
3188
+
3189
+ ## Common Tasks
3190
+
3191
+ ### Generate CRUD Module
3192
+
3193
+ \`\`\`bash
3194
+ kick g scaffold user name:string email:string age:number
3195
+ \`\`\`
3196
+
3197
+ This creates a full CRUD module with:
3198
+ - Controller with GET, POST, PUT, DELETE routes
3199
+ - Service with business logic
3200
+ - Repository with data access
3201
+ - DTOs with Zod validation
3202
+
3203
+ ### Add Authentication
3204
+
3205
+ \`\`\`bash
3206
+ kick add auth
3207
+ \`\`\`
3208
+
3209
+ Then configure in \`src/index.ts\`:
3210
+
3211
+ \`\`\`ts
3212
+ import { AuthAdapter, JwtStrategy } from '@forinda/kickjs-auth'
3213
+
3214
+ bootstrap({
3215
+ modules,
3216
+ adapters: [
3217
+ new AuthAdapter({
3218
+ strategies: [new JwtStrategy({ secret: process.env.JWT_SECRET! })],
3219
+ }),
3220
+ ],
3221
+ })
3222
+ \`\`\`
3223
+
3224
+ ### Add Database (Prisma)
3225
+
3226
+ \`\`\`bash
3227
+ kick add prisma
3228
+ ${pm} install prisma @prisma/client
3229
+ npx prisma init
3230
+ # Edit prisma/schema.prisma
3231
+ npx prisma migrate dev --name init
3232
+ kick g module user --repo prisma
3233
+ \`\`\`
3234
+
3235
+ ### Add WebSocket Support
3236
+
3237
+ \`\`\`bash
3238
+ kick add ws
3239
+ \`\`\`
3240
+
3241
+ Then add adapter in \`src/index.ts\`:
3242
+
3243
+ \`\`\`ts
3244
+ import { WsAdapter } from '@forinda/kickjs-ws'
3245
+
3246
+ bootstrap({
3247
+ modules,
3248
+ adapters: [new WsAdapter()],
3249
+ })
3250
+ \`\`\`
3251
+
3252
+ Create WebSocket controller:
3253
+
3254
+ \`\`\`bash
3255
+ kick g controller chat --ws
3256
+ \`\`\`
3257
+
3258
+ ## Testing Guidelines
3259
+
3260
+ All tests use Vitest:
3261
+
3262
+ \`\`\`ts
3263
+ import { describe, it, expect, beforeEach } from 'vitest'
3264
+ import { Container } from '@forinda/kickjs'
3265
+ import { createTestApp } from '@forinda/kickjs-testing'
3266
+
3267
+ describe('UserController', () => {
3268
+ beforeEach(() => {
3269
+ Container.reset() // Important: isolate DI state
3270
+ })
3271
+
3272
+ it('should return users', async () => {
3273
+ const app = await createTestApp([UserModule])
3274
+ const res = await app.get('/users')
3275
+
3276
+ expect(res.status).toBe(200)
3277
+ expect(res.body).toHaveProperty('users')
3278
+ })
3279
+ })
3280
+ \`\`\`
3281
+
3282
+ Run tests:
3283
+ - \`${pm} run test\` — run all tests once
3284
+ - \`${pm} run test:watch\` — watch mode
3285
+ - Individual file: \`${pm} run test src/modules/user/user.test.ts\`
3286
+
3287
+ ## Environment Variables
3288
+
3289
+ Managed via \`.env\` file. Access with:
3290
+
3291
+ 1. **@Value() decorator** (recommended):
3292
+ \`\`\`ts
3293
+ @Value('DATABASE_URL')
3294
+ private dbUrl!: string
3295
+ \`\`\`
3296
+
3297
+ 2. **ConfigService** (for dynamic access):
3298
+ \`\`\`ts
3299
+ @Autowired()
3300
+ private config!: ConfigService
3301
+
3302
+ const port = this.config.get('PORT', 3000)
3303
+ \`\`\`
3304
+
3305
+ 3. **Direct access** (avoid in app code):
3306
+ \`\`\`ts
3307
+ process.env.PORT
3308
+ \`\`\`
3309
+
3310
+ ## Key Decorators
3311
+
3312
+ ### HTTP Routes
3313
+ | Decorator | Purpose |
3314
+ |-----------|---------|
3315
+ | \`@Controller('/path')\` | Define route prefix |
3316
+ | \`@Get('/'), @Post('/')\` | HTTP method handlers |
3317
+ | \`@Middleware(fn)\` | Attach middleware |
3318
+ | \`@Public()\` | Skip auth (requires auth adapter) |
3319
+ | \`@Roles('admin')\` | Role-based access |
3320
+
3321
+ ### Dependency Injection
3322
+ | Decorator | Purpose |
3323
+ |-----------|---------|
3324
+ | \`@Module({})\` | Define feature module |
3325
+ | \`@Service()\` | Register singleton service |
3326
+ | \`@Repository()\` | Register repository |
3327
+ | \`@Autowired()\` | Property injection |
3328
+ | \`@Inject('token')\` | Token-based injection |
3329
+ | \`@Value('VAR')\` | Inject env variable |
3330
+
3331
+ ${template === "graphql" ? `### GraphQL
3332
+ | Decorator | Purpose |
3333
+ |-----------|---------|
3334
+ | \`@Resolver()\` | GraphQL resolver class |
3335
+ | \`@Query()\` | Query handler |
3336
+ | \`@Mutation()\` | Mutation handler |
3337
+ | \`@Arg('name')\` | Resolver argument |
3338
+
3339
+ ` : ""}${template === "cqrs" ? `### Background Jobs
3340
+ | Decorator | Purpose |
3341
+ |-----------|---------|
3342
+ | \`@Job('name')\` | Queue job handler |
3343
+ | \`@Process('queue')\` | Queue processor |
3344
+ | \`@Cron('0 * * * *')\` | Cron schedule |
3345
+ | \`@WsController()\` | WebSocket controller |
3346
+
3347
+ ` : ""}## Common Pitfalls
3348
+
3349
+ 1. **Forgot to register module** — Add to \`src/modules/index.ts\` exports array
3350
+ 2. **DI not working** — Ensure \`reflect-metadata\` is imported in \`src/index.ts\`
3351
+ 3. **Tests failing randomly** — Missing \`Container.reset()\` in \`beforeEach\`
3352
+ 4. **Routes not found** — Check controller path and module registration
3353
+ 5. **HMR not working** — Verify \`vite.config.ts\` has \`hmr: true\`
3354
+ 6. **Decorators not working** — Check \`tsconfig.json\` has \`experimentalDecorators: true\`
3355
+
3356
+ ## CLI Commands Reference
3357
+
3358
+ | Command | Description |
3359
+ |---------|-------------|
3360
+ | \`kick dev\` | Dev server with HMR |
3361
+ | \`kick dev:debug\` | Dev server with debugger |
3362
+ | \`kick build\` | Production build |
3363
+ | \`kick start\` | Run production build |
3364
+ | \`kick g module <names...>\` | Generate one or more modules |
3365
+ | \`kick g scaffold <name> <fields>\` | Generate CRUD |
3366
+ | \`kick g controller <name>\` | Generate controller |
3367
+ | \`kick g service <name>\` | Generate service |
3368
+ | \`kick g middleware <name>\` | Generate middleware |
3369
+ | \`kick add <package>\` | Add KickJS package |
3370
+ | \`kick add --list\` | List available packages |
3371
+ | \`kick rm module <names...>\` | Remove one or more modules |
3372
+
3373
+ > **Note:** When using \`kick new\` in scripts or CI, pass \`-t\` (or \`--template\`) and \`-r\` (or \`--repo\`) flags to bypass interactive prompts:
3374
+ > \`\`\`bash
3375
+ > kick new my-api -t ddd -r prisma --pm ${pm} --no-git --no-install -f
3376
+ > \`\`\`
3377
+
3378
+ ## Learn More
3379
+
3380
+ - [KickJS Docs](https://forinda.github.io/kick-js/)
3381
+ - [CLI Reference](https://forinda.github.io/kick-js/api/cli.html)
3382
+ - [Decorators Guide](https://forinda.github.io/kick-js/guide/decorators.html)
3383
+ - [DI System](https://forinda.github.io/kick-js/guide/dependency-injection.html)
3384
+ - [Testing](https://forinda.github.io/kick-js/api/testing.html)
3385
+ `;
3386
+ }
3387
+ //#endregion
3388
+ //#region src/generators/project.ts
3389
+ const __dirname = dirname(fileURLToPath(import.meta.url));
3390
+ const cliPkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
3391
+ const KICKJS_VERSION = `^${cliPkg.version}`;
3392
+ /** Scaffold a new KickJS project */
3393
+ async function initProject(options) {
3394
+ const { name, directory, packageManager = "pnpm", template = "rest", defaultRepo = "inmemory" } = options;
3395
+ const dir = directory;
3396
+ const log = (msg) => console.log(` ${msg}`);
3397
+ console.log(`\n Creating KickJS project: ${name}\n`);
3398
+ await writeFileSafe(join(dir, "package.json"), generatePackageJson(name, template, KICKJS_VERSION));
3399
+ await writeFileSafe(join(dir, "vite.config.ts"), generateViteConfig());
3400
+ await writeFileSafe(join(dir, "tsconfig.json"), generateTsConfig());
3401
+ await writeFileSafe(join(dir, ".prettierrc"), generatePrettierConfig());
3402
+ await writeFileSafe(join(dir, ".editorconfig"), generateEditorConfig());
3403
+ await writeFileSafe(join(dir, ".gitignore"), generateGitIgnore());
3404
+ await writeFileSafe(join(dir, ".gitattributes"), generateGitAttributes());
3405
+ await writeFileSafe(join(dir, ".env"), generateEnv());
3406
+ await writeFileSafe(join(dir, ".env.example"), generateEnvExample());
3407
+ await writeFileSafe(join(dir, "src/index.ts"), generateEntryFile(name, template, cliPkg.version));
3408
+ await writeFileSafe(join(dir, "src/modules/index.ts"), generateModulesIndex());
3409
+ await writeFileSafe(join(dir, "src/modules/hello/hello.service.ts"), generateHelloService());
3410
+ await writeFileSafe(join(dir, "src/modules/hello/hello.controller.ts"), generateHelloController());
3411
+ await writeFileSafe(join(dir, "src/modules/hello/hello.module.ts"), generateHelloModule());
3412
+ if (template === "graphql") await writeFileSafe(join(dir, "src/resolvers/.gitkeep"), "");
3413
+ await writeFileSafe(join(dir, "kick.config.ts"), generateKickConfig(template, defaultRepo));
3414
+ await writeFileSafe(join(dir, "vitest.config.ts"), generateVitestConfig());
3415
+ await writeFileSafe(join(dir, "README.md"), generateReadme(name, template, packageManager));
3416
+ await writeFileSafe(join(dir, "CLAUDE.md"), generateClaude(name, template, packageManager));
3417
+ await writeFileSafe(join(dir, "AGENTS.md"), generateAgents(name, template, packageManager));
3418
+ if (options.initGit) try {
3419
+ execSync("git init", {
3420
+ cwd: dir,
3421
+ stdio: "pipe"
3422
+ });
3423
+ execSync("git branch -M main", {
3424
+ cwd: dir,
3425
+ stdio: "pipe"
3426
+ });
3427
+ execSync("git add -A", {
3428
+ cwd: dir,
3429
+ stdio: "pipe"
3430
+ });
3431
+ execSync("git commit -m \"chore: initial commit from kick new\"", {
3432
+ cwd: dir,
3433
+ stdio: "pipe"
3434
+ });
3435
+ log("Git repository initialized");
3436
+ } catch {
3437
+ log("Warning: git init failed (git may not be installed)");
3438
+ }
3439
+ if (options.installDeps) {
3440
+ console.log(`\n Installing dependencies with ${packageManager}...\n`);
3441
+ try {
3442
+ execSync(`${packageManager} install`, {
3443
+ cwd: dir,
3444
+ stdio: "inherit"
3445
+ });
3446
+ console.log("\n Dependencies installed successfully!");
3447
+ } catch {
3448
+ console.log(`\n Warning: ${packageManager} install failed. Run it manually.`);
3449
+ }
3450
+ }
3451
+ console.log("\n Project scaffolded successfully!");
3452
+ console.log();
3453
+ const needsCd = dir !== process.cwd();
3454
+ log("Next steps:");
3455
+ if (needsCd) log(` cd ${name}`);
3456
+ if (!options.installDeps) log(` ${packageManager} install`);
3457
+ const genHint = {
3458
+ rest: "kick g module user",
3459
+ graphql: "kick g resolver user",
3460
+ ddd: "kick g module user --repo drizzle",
3461
+ cqrs: "kick g module user --pattern cqrs",
3462
+ minimal: "# add your routes to src/index.ts"
3463
+ };
3464
+ log(` ${genHint[template] ?? genHint.rest}`);
3465
+ log(" kick dev");
3466
+ log("");
3467
+ log("Commands:");
3468
+ log(" kick dev Start dev server with Vite HMR");
3469
+ log(" kick build Production build via Vite");
3470
+ log(" kick start Run production build");
3471
+ log("");
3472
+ log("Generators:");
3473
+ log(" kick g module <name> Full DDD module (controller, DTOs, use-cases, repo)");
3474
+ log(" kick g scaffold <n> <f..> CRUD module from field definitions");
3475
+ log(" kick g controller <name> Standalone controller");
3476
+ log(" kick g service <name> @Service() class");
3477
+ log(" kick g middleware <name> Express middleware");
3478
+ log(" kick g guard <name> Route guard (auth, roles, etc.)");
3479
+ log(" kick g adapter <name> AppAdapter with lifecycle hooks");
3480
+ log(" kick g dto <name> Zod DTO schema");
3481
+ if (template === "graphql") log(" kick g resolver <name> GraphQL resolver");
3482
+ if (template === "cqrs") log(" kick g job <name> Queue job processor");
3483
+ log(" kick g config Generate kick.config.ts");
3484
+ log("");
3485
+ log("Add packages:");
3486
+ log(" kick add <pkg> Install a KickJS package + peers");
3487
+ log(" kick add --list Show all available packages");
3488
+ log("");
3489
+ log("Available: auth, swagger, graphql, drizzle, prisma, ws,");
3490
+ log(" cron, queue, mailer, otel, multi-tenant, notifications, testing");
3491
+ log("");
3492
+ }
3493
+ //#endregion
3494
+ //#region src/config.ts
3495
+ /** Helper to define a type-safe kick.config.ts */
3496
+ function defineConfig(config) {
3497
+ return config;
3498
+ }
3499
+ const CONFIG_FILES = [
3500
+ "kick.config.ts",
3501
+ "kick.config.js",
3502
+ "kick.config.mjs",
3503
+ "kick.config.json"
3504
+ ];
3505
+ /** Load kick.config.* from the project root */
3506
+ async function loadKickConfig(cwd) {
3507
+ for (const filename of CONFIG_FILES) {
3508
+ const filepath = join(cwd, filename);
3509
+ try {
3510
+ await access(filepath);
3511
+ } catch {
3512
+ continue;
3513
+ }
3514
+ if (filename.endsWith(".json")) {
3515
+ const content = await readFile(filepath, "utf-8");
3516
+ return JSON.parse(content);
3517
+ }
3518
+ try {
3519
+ const { pathToFileURL } = await import("node:url");
3520
+ const mod = await import(pathToFileURL(filepath).href);
3521
+ return mod.default ?? mod;
3522
+ } catch (err) {
3523
+ if (filename.endsWith(".ts")) console.warn(`Warning: Failed to load ${filename}. TypeScript config files require a runtime loader (e.g. tsx, ts-node) or use kick.config.js/.mjs instead.`);
3524
+ continue;
3525
+ }
3526
+ }
3527
+ return null;
3528
+ }
3529
+ //#endregion
3530
+ export { defineConfig, generateAdapter, generateController, generateDto, generateGuard, generateMiddleware, generateModule, generateService, initProject, loadKickConfig, pluralize, toCamelCase, toKebabCase, toPascalCase };
3531
+
3532
+ //# sourceMappingURL=index.mjs.map