@forinda/kickjs-cli 2.0.1 → 2.2.0

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