@forinda/kickjs-cli 5.11.0 → 6.0.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 (37) hide show
  1. package/dist/agent-docs-Dmku65k2.mjs +12 -0
  2. package/dist/agent-docs-Dmku65k2.mjs.map +1 -0
  3. package/dist/{builtins-DOQNq7JT.mjs → builtins-G49e_Qsj.mjs} +2 -2
  4. package/dist/cli.mjs +151 -1376
  5. package/dist/config-DdtRfl33.mjs +13 -0
  6. package/dist/config-DdtRfl33.mjs.map +1 -0
  7. package/dist/doctor-SUUDEI1J.mjs +1221 -0
  8. package/dist/doctor-SUUDEI1J.mjs.map +1 -0
  9. package/dist/index.d.mts +663 -853
  10. package/dist/index.d.mts.map +1 -1
  11. package/dist/index.mjs +2 -2
  12. package/dist/{plugin-CcuuVb2P.mjs → plugin-Dg0Lk2Lp.mjs} +3 -3
  13. package/dist/{plugin-CcuuVb2P.mjs.map → plugin-Dg0Lk2Lp.mjs.map} +1 -1
  14. package/dist/{project-docs-CjnHf0Wd.mjs → project-docs-C-dA6-TO.mjs} +6 -36
  15. package/dist/project-docs-C-dA6-TO.mjs.map +1 -0
  16. package/dist/{project-root-SRoIXPwv.mjs → project-root-so4F5DRN.mjs} +3 -3
  17. package/dist/{project-root-SRoIXPwv.mjs.map → project-root-so4F5DRN.mjs.map} +1 -1
  18. package/dist/{rolldown-runtime-BOORVFz_.mjs → rolldown-runtime-DrKbExWn.mjs} +1 -1
  19. package/dist/run-plugins-B2_AT35s.mjs +636 -0
  20. package/dist/run-plugins-B2_AT35s.mjs.map +1 -0
  21. package/dist/typegen-5MX2F5iL.mjs +114 -0
  22. package/dist/typegen-5MX2F5iL.mjs.map +1 -0
  23. package/dist/types-C5PH0h7Z.mjs +11 -0
  24. package/package.json +13 -13
  25. package/dist/agent-docs-BuCTT-9g.mjs +0 -12
  26. package/dist/agent-docs-BuCTT-9g.mjs.map +0 -1
  27. package/dist/config-DW9HQc2u.mjs +0 -13
  28. package/dist/config-DW9HQc2u.mjs.map +0 -1
  29. package/dist/doctor-Cin9GYv9.mjs +0 -2076
  30. package/dist/doctor-Cin9GYv9.mjs.map +0 -1
  31. package/dist/project-docs-CjnHf0Wd.mjs.map +0 -1
  32. package/dist/run-plugins-fsoovaEF.mjs +0 -976
  33. package/dist/run-plugins-fsoovaEF.mjs.map +0 -1
  34. package/dist/typegen-CSLwAvgI.mjs +0 -114
  35. package/dist/typegen-CSLwAvgI.mjs.map +0 -1
  36. package/dist/types-BZ1L4hvm.mjs +0 -12
  37. package/dist/types-BZ1L4hvm.mjs.map +0 -1
@@ -1,2076 +0,0 @@
1
- /**
2
- * @forinda/kickjs-cli v5.11.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{b as e,c as t,d as n,g as r,l as i,o as a,s as o,v as s}from"./project-docs-CjnHf0Wd.mjs";import{i as c}from"./config-DW9HQc2u.mjs";import{t as l}from"./project-root-SRoIXPwv.mjs";import{createRequire as u}from"node:module";import{dirname as d,join as f,relative as p,resolve as m}from"node:path";import{existsSync as h,readFileSync as g,readdirSync as _,statSync as v}from"node:fs";import{readFile as y,writeFile as b}from"node:fs/promises";import x from"pluralize";import{execFileSync as S,execSync as C}from"node:child_process";import{fileURLToPath as ee,pathToFileURL as te}from"node:url";function w(e){return e.replace(/[-_\s]+(.)?/g,(e,t)=>t?t.toUpperCase():``).replace(/^(.)/,e=>e.toUpperCase())}function T(e){let t=w(e);return t.charAt(0).toLowerCase()+t.slice(1)}function E(e){return e.replace(/([a-z])([A-Z])/g,`$1-$2`).replace(/[\s_]+/g,`-`).toLowerCase()}function D(e){return x.plural(e)}function O(e){return x.plural(e)}function k(e){return e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`)}const ne={inmemory:`in-memory`,drizzle:`Drizzle`,prisma:`Prisma`};function A(e){return e.charAt(0).toUpperCase()+e.slice(1).replace(/-([a-z])/g,(e,t)=>t.toUpperCase())}function re(e){return e.replace(/([a-z])([A-Z])/g,`$1-$2`).toLowerCase()}function j(e){return ne[e]??A(e)}function M(e,t,n){let r={inmemory:`InMemory${e}Repository`,drizzle:`Drizzle${e}Repository`,prisma:`Prisma${e}Repository`},i={inmemory:`in-memory-${t}`,drizzle:`drizzle-${t}`,prisma:`prisma-${t}`};return{repoClass:r[n]??`${A(n)}${e}Repository`,repoFile:i[n]??`${re(n)}-${t}`}}function N(e){return e??`define`}function ie(e){let{pascal:t,kebab:n,plural:r=``,repo:i,style:a}=e,{repoClass:o,repoFile:s}=M(t,n,i),c=N(a),l=`/**
12
- * ${t} Module
13
- *
14
- * Self-contained feature module following Domain-Driven Design (DDD).
15
- * Registers dependencies in the DI container and declares HTTP routes.
16
- *
17
- * Structure:
18
- * presentation/ — HTTP controllers (entry points)
19
- * application/ — Use cases (orchestration) and DTOs (validation)
20
- * domain/ — Entities, value objects, repository interfaces, domain services
21
- * infrastructure/ — Repository implementations (currently ${j(i)})
22
- */`,u=`import { ${t.toUpperCase()}_REPOSITORY } from './domain/repositories/${n}.repository'
23
- import { ${o} } from './infrastructure/repositories/${s}.repository'
24
- import { ${t}Controller } from './presentation/${n}.controller'
25
-
26
- // Eagerly load decorated classes so @Service()/@Repository() decorators register in the DI container
27
- import.meta.glob(
28
- ['./domain/services/**/*.ts', './application/use-cases/**/*.ts', '!./**/*.test.ts'],
29
- { eager: true },
30
- )`,d=` /**
31
- * Declare HTTP routes for this module. Return value shape:
32
- *
33
- * - \`path\` — URL prefix for this route set, mounted under
34
- * \`/{apiPrefix}/v{version}{path}\`.
35
- * - \`controller\` — Controller class. Used both for the route
36
- * handler bindings and OpenAPI spec generation.
37
- * - \`version\` — Optional. Overrides the app-wide API version
38
- * for this route set only.
39
- *
40
- * Return an **array** to mount multiple route sets under the
41
- * same module (e.g. side-by-side v1 + v2 controllers):
42
- *
43
- * return [
44
- * { path: '/${r}', version: 1, controller: ${t}V1Controller },
45
- * { path: '/${r}', version: 2, controller: ${t}V2Controller },
46
- * ]
47
- */`;return c===`class`?`${l}
48
- import { Container, type AppModule, type ModuleRoutes } from '@forinda/kickjs'
49
- ${u}
50
-
51
- export class ${t}Module implements AppModule {
52
- /**
53
- * Register module dependencies in the DI container.
54
- * Bind repository interface tokens to their implementations here.
55
- * Currently wired to ${j(i)}. To swap implementations, change the factory target.
56
- */
57
- register(container: Container): void {
58
- container.registerFactory(${t.toUpperCase()}_REPOSITORY, () =>
59
- container.resolve(${o}),
60
- )
61
- }
62
-
63
- ${d.replace(/^ {4}/gm,` `).replace(/^ {6}/gm,` `)}
64
- routes(): ModuleRoutes {
65
- return {
66
- path: '/${r}',
67
- controller: ${t}Controller,
68
- }
69
- }
70
- }
71
- `:`${l}
72
- import { defineModule } from '@forinda/kickjs'
73
- ${u}
74
-
75
- export const ${t}Module = defineModule({
76
- name: '${t}Module',
77
- build: () => ({
78
- /**
79
- * Register module dependencies in the DI container.
80
- * Bind repository interface tokens to their implementations here.
81
- * Currently wired to ${j(i)}. To swap implementations, change the factory target.
82
- */
83
- register(container) {
84
- container.registerFactory(${t.toUpperCase()}_REPOSITORY, () =>
85
- container.resolve(${o}),
86
- )
87
- },
88
-
89
- ${d}
90
- routes() {
91
- return {
92
- path: '/${r}',
93
- controller: ${t}Controller,
94
- }
95
- },
96
- }),
97
- })
98
- `}function ae(e){let{pascal:t,kebab:n,plural:r=``,repo:i,style:a}=e,{repoClass:o,repoFile:s}=M(t,n,i),c=N(a),l=`/**
99
- * ${t} Module
100
- *
101
- * REST module with a flat folder structure.
102
- * Controller delegates to service, service wraps the repository.
103
- *
104
- * Structure:
105
- * ${n}.controller.ts — HTTP routes (CRUD)
106
- * ${n}.service.ts — Business logic
107
- * ${n}.repository.ts — Repository interface
108
- * ${s}.repository.ts — Repository implementation
109
- * dtos/ — Request/response schemas
110
- */`,u=`import { ${t.toUpperCase()}_REPOSITORY } from './${n}.repository'
111
- import { ${o} } from './${s}.repository'
112
- import { ${t}Controller } from './${n}.controller'
113
-
114
- // Eagerly load decorated classes so @Service()/@Repository() decorators register in the DI container
115
- import.meta.glob(['./**/*.service.ts', './**/*.repository.ts', '!./**/*.test.ts'], { eager: true })`,d=` /**
116
- * Declare HTTP routes for this module. Return value shape:
117
- *
118
- * - \`path\` — URL prefix for this route set.
119
- * - \`controller\` — Controller class (also drives OpenAPI).
120
- * - \`version\` — Optional. Overrides the app-wide API version.
121
- *
122
- * Return an **array** to mount multiple route sets — admin
123
- * surfaces, side-by-side v1 + v2 controllers, etc:
124
- *
125
- * return [
126
- * { path: '/${r}', version: 1, controller: ${t}V1Controller },
127
- * { path: '/${r}', version: 2, controller: ${t}V2Controller },
128
- * ]
129
- */`;return c===`class`?`${l}
130
- import { Container, type AppModule, type ModuleRoutes } from '@forinda/kickjs'
131
- ${u}
132
-
133
- export class ${t}Module implements AppModule {
134
- register(container: Container): void {
135
- container.registerFactory(${t.toUpperCase()}_REPOSITORY, () =>
136
- container.resolve(${o}),
137
- )
138
- }
139
-
140
- ${d.replace(/^ {4}/gm,` `).replace(/^ {6}/gm,` `)}
141
- routes(): ModuleRoutes {
142
- return {
143
- path: '/${r}',
144
- controller: ${t}Controller,
145
- }
146
- }
147
- }
148
- `:`${l}
149
- import { defineModule } from '@forinda/kickjs'
150
- ${u}
151
-
152
- export const ${t}Module = defineModule({
153
- name: '${t}Module',
154
- build: () => ({
155
- register(container) {
156
- container.registerFactory(${t.toUpperCase()}_REPOSITORY, () =>
157
- container.resolve(${o}),
158
- )
159
- },
160
-
161
- ${d}
162
- routes() {
163
- return {
164
- path: '/${r}',
165
- controller: ${t}Controller,
166
- }
167
- },
168
- }),
169
- })
170
- `}function oe(e){let{pascal:t,kebab:n,plural:r=``,style:i}=e,a=N(i),o=` /**
171
- * Declare HTTP routes. Return value shape:
172
- *
173
- * - \`path\` — URL prefix for this route set.
174
- * - \`controller\` — Controller class (also drives OpenAPI).
175
- * - \`version\` — Optional. Overrides the app-wide API version.
176
- *
177
- * Return an array to mount multiple route sets:
178
- *
179
- * return [
180
- * { path: '/${r}', version: 1, controller: ${t}V1Controller },
181
- * { path: '/${r}', version: 2, controller: ${t}V2Controller },
182
- * ]
183
- */`;return a===`class`?`import { type AppModule, type ModuleRoutes } from '@forinda/kickjs'
184
- import { ${t}Controller } from './${n}.controller'
185
-
186
- export class ${t}Module implements AppModule {
187
- ${o.replace(/^ {4}/gm,` `).replace(/^ {6}/gm,` `)}
188
- routes(): ModuleRoutes {
189
- return {
190
- path: '/${r}',
191
- controller: ${t}Controller,
192
- }
193
- }
194
- }
195
- `:`import { defineModule } from '@forinda/kickjs'
196
- import { ${t}Controller } from './${n}.controller'
197
-
198
- export const ${t}Module = defineModule({
199
- name: '${t}Module',
200
- build: () => ({
201
- ${o}
202
- routes() {
203
- return {
204
- path: '/${r}',
205
- controller: ${t}Controller,
206
- }
207
- },
208
- }),
209
- })
210
- `}function se(e){let{pascal:t,kebab:n,plural:r=``,pluralPascal:i=``}=e;return`import { Controller, Get, Post, Put, Delete, Autowired, ApiQueryParams, type Ctx } from '@forinda/kickjs'
211
- import { ApiTags } from '@forinda/kickjs-swagger'
212
- import { Create${t}UseCase } from '../application/use-cases/create-${n}.use-case'
213
- import { Get${t}UseCase } from '../application/use-cases/get-${n}.use-case'
214
- import { List${i}UseCase } from '../application/use-cases/list-${r}.use-case'
215
- import { Update${t}UseCase } from '../application/use-cases/update-${n}.use-case'
216
- import { Delete${t}UseCase } from '../application/use-cases/delete-${n}.use-case'
217
- import { create${t}Schema } from '../application/dtos/create-${n}.dto'
218
- import { update${t}Schema } from '../application/dtos/update-${n}.dto'
219
- import { ${t.toUpperCase()}_QUERY_CONFIG } from '../constants'
220
-
221
- // Each handler annotates its \`ctx\` with \`Ctx<KickRoutes.${t}Controller['<method>']>\`
222
- // so \`ctx.params\`, \`ctx.body\`, and \`ctx.query\` are typed end-to-end.
223
- // The \`KickRoutes\` namespace is generated by \`kick typegen\` (auto-run on
224
- // \`kick dev\`) — see https://forinda.github.io/kick-js/guide/typegen.
225
-
226
- @Controller()
227
- export class ${t}Controller {
228
- @Autowired() private readonly create${t}UseCase!: Create${t}UseCase
229
- @Autowired() private readonly get${t}UseCase!: Get${t}UseCase
230
- @Autowired() private readonly list${i}UseCase!: List${i}UseCase
231
- @Autowired() private readonly update${t}UseCase!: Update${t}UseCase
232
- @Autowired() private readonly delete${t}UseCase!: Delete${t}UseCase
233
-
234
- @Get('/')
235
- @ApiTags('${t}')
236
- @ApiQueryParams(${t.toUpperCase()}_QUERY_CONFIG)
237
- async list(ctx: Ctx<KickRoutes.${t}Controller['list']>) {
238
- return ctx.paginate(
239
- (parsed) => this.list${i}UseCase.execute(parsed),
240
- ${t.toUpperCase()}_QUERY_CONFIG,
241
- )
242
- }
243
-
244
- @Get('/:id')
245
- @ApiTags('${t}')
246
- async getById(ctx: Ctx<KickRoutes.${t}Controller['getById']>) {
247
- const result = await this.get${t}UseCase.execute(ctx.params.id)
248
- if (!result) return ctx.notFound('${t} not found')
249
- ctx.json(result)
250
- }
251
-
252
- @Post('/', { body: create${t}Schema, name: 'Create${t}' })
253
- @ApiTags('${t}')
254
- async create(ctx: Ctx<KickRoutes.${t}Controller['create']>) {
255
- const result = await this.create${t}UseCase.execute(ctx.body)
256
- ctx.created(result)
257
- }
258
-
259
- @Put('/:id', { body: update${t}Schema, name: 'Update${t}' })
260
- @ApiTags('${t}')
261
- async update(ctx: Ctx<KickRoutes.${t}Controller['update']>) {
262
- const result = await this.update${t}UseCase.execute(ctx.params.id, ctx.body)
263
- ctx.json(result)
264
- }
265
-
266
- @Delete('/:id')
267
- @ApiTags('${t}')
268
- async remove(ctx: Ctx<KickRoutes.${t}Controller['remove']>) {
269
- await this.delete${t}UseCase.execute(ctx.params.id)
270
- ctx.noContent()
271
- }
272
- }
273
- `}function ce(e){let{pascal:t,kebab:n}=e,r=t.charAt(0).toLowerCase()+t.slice(1);return`import { Controller, Get, Post, Put, Delete, Autowired, ApiQueryParams, type Ctx } from '@forinda/kickjs'
274
- import { ApiTags } from '@forinda/kickjs-swagger'
275
- import { ${t}Service } from './${n}.service'
276
- import { create${t}Schema } from './dtos/create-${n}.dto'
277
- import { update${t}Schema } from './dtos/update-${n}.dto'
278
- import { ${t.toUpperCase()}_QUERY_CONFIG } from './${n}.constants'
279
-
280
- // Each handler annotates its \`ctx\` with \`Ctx<KickRoutes.${t}Controller['<method>']>\`
281
- // so \`ctx.params\`, \`ctx.body\`, and \`ctx.query\` are typed end-to-end.
282
- // The \`KickRoutes\` namespace is generated by \`kick typegen\` (auto-run on
283
- // \`kick dev\`) — see https://forinda.github.io/kick-js/guide/typegen.
284
-
285
- @Controller()
286
- export class ${t}Controller {
287
- @Autowired() private readonly ${r}Service!: ${t}Service
288
-
289
- @Get('/')
290
- @ApiTags('${t}')
291
- @ApiQueryParams(${t.toUpperCase()}_QUERY_CONFIG)
292
- async list(ctx: Ctx<KickRoutes.${t}Controller['list']>) {
293
- return ctx.paginate(
294
- (parsed) => this.${r}Service.findPaginated(parsed),
295
- ${t.toUpperCase()}_QUERY_CONFIG,
296
- )
297
- }
298
-
299
- @Get('/:id')
300
- @ApiTags('${t}')
301
- async getById(ctx: Ctx<KickRoutes.${t}Controller['getById']>) {
302
- const result = await this.${r}Service.findById(ctx.params.id)
303
- if (!result) return ctx.notFound('${t} not found')
304
- ctx.json(result)
305
- }
306
-
307
- @Post('/', { body: create${t}Schema, name: 'Create${t}' })
308
- @ApiTags('${t}')
309
- async create(ctx: Ctx<KickRoutes.${t}Controller['create']>) {
310
- const result = await this.${r}Service.create(ctx.body)
311
- ctx.created(result)
312
- }
313
-
314
- @Put('/:id', { body: update${t}Schema, name: 'Update${t}' })
315
- @ApiTags('${t}')
316
- async update(ctx: Ctx<KickRoutes.${t}Controller['update']>) {
317
- const result = await this.${r}Service.update(ctx.params.id, ctx.body)
318
- ctx.json(result)
319
- }
320
-
321
- @Delete('/:id')
322
- @ApiTags('${t}')
323
- async remove(ctx: Ctx<KickRoutes.${t}Controller['remove']>) {
324
- await this.${r}Service.delete(ctx.params.id)
325
- ctx.noContent()
326
- }
327
- }
328
- `}function le(e){let{pascal:t}=e;return`import type { QueryParamsConfig } from '@forinda/kickjs'
329
-
330
- export const ${t.toUpperCase()}_QUERY_CONFIG: QueryParamsConfig = {
331
- filterable: ['name'],
332
- sortable: ['name', 'createdAt'],
333
- searchable: ['name'],
334
- }
335
- `}function P(e){let{pascal:t}=e;return`import { z } from 'zod'
336
-
337
- /**
338
- * Create ${t} DTO — Zod schema for validating POST request bodies.
339
- * This schema is passed to @Post('/', { body: create${t}Schema }) for automatic validation.
340
- * It also generates OpenAPI request body docs when SwaggerAdapter is used.
341
- *
342
- * Add more fields as needed. Supported Zod types:
343
- * z.string(), z.number(), z.boolean(), z.enum([...]),
344
- * z.array(), z.object(), .optional(), .default(), .transform()
345
- */
346
- export const create${t}Schema = z.object({
347
- name: z.string().min(1, 'Name is required').max(200),
348
- })
349
-
350
- export type Create${t}DTO = z.infer<typeof create${t}Schema>
351
- `}function F(e){let{pascal:t}=e;return`import { z } from 'zod'
352
-
353
- export const update${t}Schema = z.object({
354
- name: z.string().min(1).max(200).optional(),
355
- })
356
-
357
- export type Update${t}DTO = z.infer<typeof update${t}Schema>
358
- `}function I(e){let{pascal:t}=e;return`export interface ${t}ResponseDTO {
359
- id: string
360
- name: string
361
- createdAt: string
362
- updatedAt: string
363
- }
364
- `}function ue(e){let{pascal:t,kebab:n,plural:r=``,pluralPascal:i=``}=e;return[{file:`create-${n}.use-case.ts`,content:`/**
365
- * Create ${t} Use Case
366
- *
367
- * Application layer — orchestrates a single business operation.
368
- * Use cases are thin: validate input (via DTO), call domain/repo, return response.
369
- * Keep business rules in the domain service, not here.
370
- */
371
- import { Service, Inject } from '@forinda/kickjs'
372
- import { ${t.toUpperCase()}_REPOSITORY, type I${t}Repository } from '../../domain/repositories/${n}.repository'
373
- import type { Create${t}DTO } from '../dtos/create-${n}.dto'
374
- import type { ${t}ResponseDTO } from '../dtos/${n}-response.dto'
375
-
376
- @Service()
377
- export class Create${t}UseCase {
378
- constructor(
379
- @Inject(${t.toUpperCase()}_REPOSITORY) private readonly repo: I${t}Repository,
380
- ) {}
381
-
382
- async execute(dto: Create${t}DTO): Promise<${t}ResponseDTO> {
383
- return this.repo.create(dto)
384
- }
385
- }
386
- `},{file:`get-${n}.use-case.ts`,content:`import { Service, Inject } from '@forinda/kickjs'
387
- import { ${t.toUpperCase()}_REPOSITORY, type I${t}Repository } from '../../domain/repositories/${n}.repository'
388
- import type { ${t}ResponseDTO } from '../dtos/${n}-response.dto'
389
-
390
- @Service()
391
- export class Get${t}UseCase {
392
- constructor(
393
- @Inject(${t.toUpperCase()}_REPOSITORY) private readonly repo: I${t}Repository,
394
- ) {}
395
-
396
- async execute(id: string): Promise<${t}ResponseDTO | null> {
397
- return this.repo.findById(id)
398
- }
399
- }
400
- `},{file:`list-${r}.use-case.ts`,content:`import { Service, Inject } from '@forinda/kickjs'
401
- import { ${t.toUpperCase()}_REPOSITORY, type I${t}Repository } from '../../domain/repositories/${n}.repository'
402
- import type { ParsedQuery } from '@forinda/kickjs'
403
-
404
- @Service()
405
- export class List${i}UseCase {
406
- constructor(
407
- @Inject(${t.toUpperCase()}_REPOSITORY) private readonly repo: I${t}Repository,
408
- ) {}
409
-
410
- async execute(parsed: ParsedQuery) {
411
- return this.repo.findPaginated(parsed)
412
- }
413
- }
414
- `},{file:`update-${n}.use-case.ts`,content:`import { Service, Inject } from '@forinda/kickjs'
415
- import { ${t.toUpperCase()}_REPOSITORY, type I${t}Repository } from '../../domain/repositories/${n}.repository'
416
- import type { Update${t}DTO } from '../dtos/update-${n}.dto'
417
- import type { ${t}ResponseDTO } from '../dtos/${n}-response.dto'
418
-
419
- @Service()
420
- export class Update${t}UseCase {
421
- constructor(
422
- @Inject(${t.toUpperCase()}_REPOSITORY) private readonly repo: I${t}Repository,
423
- ) {}
424
-
425
- async execute(id: string, dto: Update${t}DTO): Promise<${t}ResponseDTO> {
426
- return this.repo.update(id, dto)
427
- }
428
- }
429
- `},{file:`delete-${n}.use-case.ts`,content:`import { Service, Inject } from '@forinda/kickjs'
430
- import { ${t.toUpperCase()}_REPOSITORY, type I${t}Repository } from '../../domain/repositories/${n}.repository'
431
-
432
- @Service()
433
- export class Delete${t}UseCase {
434
- constructor(
435
- @Inject(${t.toUpperCase()}_REPOSITORY) private readonly repo: I${t}Repository,
436
- ) {}
437
-
438
- async execute(id: string): Promise<void> {
439
- await this.repo.delete(id)
440
- }
441
- }
442
- `}]}function L(e){let{pascal:t,kebab:n,dtoPrefix:r=`../../application/dtos`,tokenScope:i=`app`}=e;return`/**
443
- * ${t} Repository Interface
444
- *
445
- * Defines the contract for data access.
446
- * The interface declares what operations are available;
447
- * implementations (in-memory, Drizzle, Prisma) fulfill the contract.
448
- *
449
- * To swap implementations, change the factory in the module's register() method.
450
- */
451
- import { createToken } from '@forinda/kickjs'
452
- import type { ${t}ResponseDTO } from '${r}/${n}-response.dto'
453
- import type { Create${t}DTO } from '${r}/create-${n}.dto'
454
- import type { Update${t}DTO } from '${r}/update-${n}.dto'
455
- import type { ParsedQuery } from '@forinda/kickjs'
456
-
457
- export interface I${t}Repository {
458
- findById(id: string): Promise<${t}ResponseDTO | null>
459
- findAll(): Promise<${t}ResponseDTO[]>
460
- findPaginated(parsed: ParsedQuery): Promise<{ data: ${t}ResponseDTO[]; total: number }>
461
- create(dto: Create${t}DTO): Promise<${t}ResponseDTO>
462
- update(id: string, dto: Update${t}DTO): Promise<${t}ResponseDTO>
463
- delete(id: string): Promise<void>
464
- }
465
-
466
- /**
467
- * Collision-safe DI token bound to \`I${t}Repository\`.
468
- * \`container.resolve(${t.toUpperCase()}_REPOSITORY)\` and
469
- * \`@Inject(${t.toUpperCase()}_REPOSITORY)\` both return the typed
470
- * interface — no manual generic, no \`any\` cast.
471
- *
472
- * The \`'${i}/'\` prefix matches the project scope so
473
- * \`kick-lint\`'s \`token-reserved-prefix\` rule never fires —
474
- * adopters must NOT use the reserved \`'kick/'\` namespace.
475
- */
476
- export const ${t.toUpperCase()}_REPOSITORY = createToken<I${t}Repository>('${i}/${t}/repository')
477
- `}function R(e){let{pascal:t,kebab:n,repoPrefix:r=`../../domain/repositories`,dtoPrefix:i=`../../application/dtos`}=e;return`/**
478
- * In-Memory ${t} Repository
479
- *
480
- * Implements the repository interface using a Map.
481
- * Useful for prototyping and testing. Replace with a database implementation
482
- * (Drizzle, Prisma, etc.) for production use.
483
- *
484
- * @Repository() registers this class in the DI container as a singleton.
485
- */
486
- import { randomUUID } from 'node:crypto'
487
- import { Repository, HttpException } from '@forinda/kickjs'
488
- import type { ParsedQuery } from '@forinda/kickjs'
489
- import type { I${t}Repository } from '${r}/${n}.repository'
490
- import type { ${t}ResponseDTO } from '${i}/${n}-response.dto'
491
- import type { Create${t}DTO } from '${i}/create-${n}.dto'
492
- import type { Update${t}DTO } from '${i}/update-${n}.dto'
493
-
494
- @Repository()
495
- export class InMemory${t}Repository implements I${t}Repository {
496
- private store = new Map<string, ${t}ResponseDTO>()
497
-
498
- async findById(id: string): Promise<${t}ResponseDTO | null> {
499
- return this.store.get(id) ?? null
500
- }
501
-
502
- async findAll(): Promise<${t}ResponseDTO[]> {
503
- return Array.from(this.store.values())
504
- }
505
-
506
- async findPaginated(parsed: ParsedQuery): Promise<{ data: ${t}ResponseDTO[]; total: number }> {
507
- const all = Array.from(this.store.values())
508
- const data = all.slice(parsed.pagination.offset, parsed.pagination.offset + parsed.pagination.limit)
509
- return { data, total: all.length }
510
- }
511
-
512
- async create(dto: Create${t}DTO): Promise<${t}ResponseDTO> {
513
- const now = new Date().toISOString()
514
- const entity: ${t}ResponseDTO = {
515
- id: randomUUID(),
516
- name: dto.name,
517
- createdAt: now,
518
- updatedAt: now,
519
- }
520
- this.store.set(entity.id, entity)
521
- return entity
522
- }
523
-
524
- async update(id: string, dto: Update${t}DTO): Promise<${t}ResponseDTO> {
525
- const existing = this.store.get(id)
526
- if (!existing) throw HttpException.notFound('${t} not found')
527
- const updated = { ...existing, ...dto, updatedAt: new Date().toISOString() }
528
- this.store.set(id, updated)
529
- return updated
530
- }
531
-
532
- async delete(id: string): Promise<void> {
533
- if (!this.store.has(id)) throw HttpException.notFound('${t} not found')
534
- this.store.delete(id)
535
- }
536
- }
537
- `}function z(e){let{pascal:t,kebab:n,repoType:r=``,repoPrefix:i=`../../domain/repositories`,dtoPrefix:a=`../../application/dtos`}=e,o=r.charAt(0).toUpperCase()+r.slice(1).replace(/-([a-z])/g,(e,t)=>t.toUpperCase());return`/**
538
- * ${o} ${t} Repository
539
- *
540
- * Stub implementation for a custom '${r}' repository.
541
- * Implements the repository interface using an in-memory Map as a placeholder.
542
- *
543
- * TODO: Replace the in-memory Map with your ${r} data-access logic.
544
- * See I${t}Repository for the interface contract.
545
- *
546
- * @Repository() registers this class in the DI container as a singleton.
547
- */
548
- import { randomUUID } from 'node:crypto'
549
- import { Repository, HttpException } from '@forinda/kickjs'
550
- import type { ParsedQuery } from '@forinda/kickjs'
551
- import type { I${t}Repository } from '${i}/${n}.repository'
552
- import type { ${t}ResponseDTO } from '${a}/${n}-response.dto'
553
- import type { Create${t}DTO } from '${a}/create-${n}.dto'
554
- import type { Update${t}DTO } from '${a}/update-${n}.dto'
555
-
556
- @Repository()
557
- export class ${o}${t}Repository implements I${t}Repository {
558
- // TODO: Replace with your ${r} client/connection
559
- private store = new Map<string, ${t}ResponseDTO>()
560
-
561
- async findById(id: string): Promise<${t}ResponseDTO | null> {
562
- // TODO: Implement with ${r}
563
- return this.store.get(id) ?? null
564
- }
565
-
566
- async findAll(): Promise<${t}ResponseDTO[]> {
567
- // TODO: Implement with ${r}
568
- return Array.from(this.store.values())
569
- }
570
-
571
- async findPaginated(parsed: ParsedQuery): Promise<{ data: ${t}ResponseDTO[]; total: number }> {
572
- // TODO: Implement with ${r}
573
- const all = Array.from(this.store.values())
574
- const data = all.slice(parsed.pagination.offset, parsed.pagination.offset + parsed.pagination.limit)
575
- return { data, total: all.length }
576
- }
577
-
578
- async create(dto: Create${t}DTO): Promise<${t}ResponseDTO> {
579
- // TODO: Implement with ${r}
580
- const now = new Date().toISOString()
581
- const entity: ${t}ResponseDTO = {
582
- id: randomUUID(),
583
- name: dto.name,
584
- createdAt: now,
585
- updatedAt: now,
586
- }
587
- this.store.set(entity.id, entity)
588
- return entity
589
- }
590
-
591
- async update(id: string, dto: Update${t}DTO): Promise<${t}ResponseDTO> {
592
- // TODO: Implement with ${r}
593
- const existing = this.store.get(id)
594
- if (!existing) throw HttpException.notFound('${t} not found')
595
- const updated = { ...existing, ...dto, updatedAt: new Date().toISOString() }
596
- this.store.set(id, updated)
597
- return updated
598
- }
599
-
600
- async delete(id: string): Promise<void> {
601
- // TODO: Implement with ${r}
602
- if (!this.store.has(id)) throw HttpException.notFound('${t} not found')
603
- this.store.delete(id)
604
- }
605
- }
606
- `}function de(e){let{pascal:t,kebab:n}=e;return`/**
607
- * ${t} Domain Service
608
- *
609
- * Domain layer — contains business rules that don't belong to a single entity.
610
- * Use this for cross-entity logic, validation rules, and domain invariants.
611
- * Keep it free of HTTP/framework concerns.
612
- */
613
- import { Service, Inject, HttpException } from '@forinda/kickjs'
614
- import { ${t.toUpperCase()}_REPOSITORY, type I${t}Repository } from '../repositories/${n}.repository'
615
-
616
- @Service()
617
- export class ${t}DomainService {
618
- constructor(
619
- @Inject(${t.toUpperCase()}_REPOSITORY) private readonly repo: I${t}Repository,
620
- ) {}
621
-
622
- async ensureExists(id: string): Promise<void> {
623
- const entity = await this.repo.findById(id)
624
- if (!entity) {
625
- throw HttpException.notFound('${t} not found')
626
- }
627
- }
628
- }
629
- `}function fe(e){let{pascal:t,kebab:n}=e;return`/**
630
- * ${t} Entity
631
- *
632
- * Domain layer — the core business object.
633
- * Uses a private constructor with static factory methods (create, reconstitute)
634
- * to enforce invariants. Properties are accessed via getters to maintain encapsulation.
635
- *
636
- * Patterns used:
637
- * - Private constructor: prevents direct instantiation
638
- * - create(): factory for new entities (generates ID, sets timestamps)
639
- * - reconstitute(): factory for rebuilding from persistence (no side effects)
640
- * - changeName(): mutation method that enforces business rules
641
- */
642
- import { ${t}Id } from '../value-objects/${n}-id.vo'
643
-
644
- interface ${t}Props {
645
- id: ${t}Id
646
- name: string
647
- createdAt: Date
648
- updatedAt: Date
649
- }
650
-
651
- export class ${t} {
652
- private constructor(private props: ${t}Props) {}
653
-
654
- static create(params: { name: string }): ${t} {
655
- const now = new Date()
656
- return new ${t}({
657
- id: ${t}Id.create(),
658
- name: params.name,
659
- createdAt: now,
660
- updatedAt: now,
661
- })
662
- }
663
-
664
- static reconstitute(props: ${t}Props): ${t} {
665
- return new ${t}(props)
666
- }
667
-
668
- get id(): ${t}Id {
669
- return this.props.id
670
- }
671
- get name(): string {
672
- return this.props.name
673
- }
674
- get createdAt(): Date {
675
- return this.props.createdAt
676
- }
677
- get updatedAt(): Date {
678
- return this.props.updatedAt
679
- }
680
-
681
- changeName(name: string): void {
682
- if (!name || name.trim().length === 0) {
683
- throw new Error('Name cannot be empty')
684
- }
685
- this.props.name = name.trim()
686
- this.props.updatedAt = new Date()
687
- }
688
-
689
- toJSON() {
690
- return {
691
- id: this.props.id.toString(),
692
- name: this.props.name,
693
- createdAt: this.props.createdAt.toISOString(),
694
- updatedAt: this.props.updatedAt.toISOString(),
695
- }
696
- }
697
- }
698
- `}function pe(e){let{pascal:t}=e;return`/**
699
- * ${t} ID Value Object
700
- *
701
- * Domain layer — wraps a primitive ID with type safety and validation.
702
- * Value objects are immutable and compared by value, not reference.
703
- *
704
- * ${t}Id.create() — generate a new UUID
705
- * ${t}Id.from(id) — wrap an existing ID string (validates non-empty)
706
- * id.equals(other) — compare two IDs by value
707
- */
708
- import { randomUUID } from 'node:crypto'
709
-
710
- export class ${t}Id {
711
- private constructor(private readonly value: string) {}
712
-
713
- static create(): ${t}Id {
714
- return new ${t}Id(randomUUID())
715
- }
716
-
717
- static from(id: string): ${t}Id {
718
- if (!id || id.trim().length === 0) {
719
- throw new Error('${t}Id cannot be empty')
720
- }
721
- return new ${t}Id(id)
722
- }
723
-
724
- toString(): string {
725
- return this.value
726
- }
727
-
728
- equals(other: ${t}Id): boolean {
729
- return this.value === other.value
730
- }
731
- }
732
- `}function B(e){let{pascal:t,kebab:n,plural:r=``}=e;return`import { describe, it, expect, beforeEach } from 'vitest'
733
- import { Container } from '@forinda/kickjs'
734
-
735
- describe('${t}Controller', () => {
736
- beforeEach(() => {
737
- Container.reset()
738
- })
739
-
740
- it('should be defined', () => {
741
- expect(true).toBe(true)
742
- })
743
-
744
- describe('POST /${r}', () => {
745
- it('should create a new ${n}', async () => {
746
- // TODO: Set up test module, call create endpoint, assert 201
747
- expect(true).toBe(true)
748
- })
749
- })
750
-
751
- describe('GET /${r}', () => {
752
- it('should return paginated ${r}', async () => {
753
- // TODO: Set up test module, call list endpoint, assert { data, meta }
754
- expect(true).toBe(true)
755
- })
756
- })
757
-
758
- describe('GET /${r}/:id', () => {
759
- it('should return a ${n} by id', async () => {
760
- // TODO: Create a ${n}, then fetch by id, assert match
761
- expect(true).toBe(true)
762
- })
763
-
764
- it('should return 404 for non-existent ${n}', async () => {
765
- // TODO: Fetch non-existent id, assert 404
766
- expect(true).toBe(true)
767
- })
768
- })
769
-
770
- describe('PUT /${r}/:id', () => {
771
- it('should update an existing ${n}', async () => {
772
- // TODO: Create, update, assert changes
773
- expect(true).toBe(true)
774
- })
775
- })
776
-
777
- describe('DELETE /${r}/:id', () => {
778
- it('should delete a ${n}', async () => {
779
- // TODO: Create, delete, assert gone
780
- expect(true).toBe(true)
781
- })
782
- })
783
- })
784
- `}function V(e){let{pascal:t,kebab:n,plural:r=``,repoPrefix:i=`../infrastructure/repositories/in-memory-${n}.repository`}=e;return`import { describe, it, expect, beforeEach } from 'vitest'
785
- import { InMemory${t}Repository } from '${i}'
786
-
787
- describe('InMemory${t}Repository', () => {
788
- let repo: InMemory${t}Repository
789
-
790
- beforeEach(() => {
791
- repo = new InMemory${t}Repository()
792
- })
793
-
794
- it('should create and retrieve a ${n}', async () => {
795
- const created = await repo.create({ name: 'Test ${t}' })
796
- expect(created).toBeDefined()
797
- expect(created.name).toBe('Test ${t}')
798
- expect(created.id).toBeDefined()
799
-
800
- const found = await repo.findById(created.id)
801
- expect(found).toEqual(created)
802
- })
803
-
804
- it('should return null for non-existent id', async () => {
805
- const found = await repo.findById('non-existent')
806
- expect(found).toBeNull()
807
- })
808
-
809
- it('should list all ${r}', async () => {
810
- await repo.create({ name: '${t} 1' })
811
- await repo.create({ name: '${t} 2' })
812
-
813
- const all = await repo.findAll()
814
- expect(all).toHaveLength(2)
815
- })
816
-
817
- it('should return paginated results', async () => {
818
- await repo.create({ name: '${t} 1' })
819
- await repo.create({ name: '${t} 2' })
820
- await repo.create({ name: '${t} 3' })
821
-
822
- const result = await repo.findPaginated({
823
- filters: [],
824
- sort: [],
825
- search: '',
826
- pagination: { page: 1, limit: 2, offset: 0 },
827
- })
828
-
829
- expect(result.data).toHaveLength(2)
830
- expect(result.total).toBe(3)
831
- })
832
-
833
- it('should update a ${n}', async () => {
834
- const created = await repo.create({ name: 'Original' })
835
- const updated = await repo.update(created.id, { name: 'Updated' })
836
- expect(updated.name).toBe('Updated')
837
- })
838
-
839
- it('should delete a ${n}', async () => {
840
- const created = await repo.create({ name: 'To Delete' })
841
- await repo.delete(created.id)
842
- const found = await repo.findById(created.id)
843
- expect(found).toBeNull()
844
- })
845
- })
846
- `}function me(e){let{pascal:t,kebab:n}=e;return`import { Service, Inject, HttpException } from '@forinda/kickjs'
847
- import type { ParsedQuery } from '@forinda/kickjs'
848
- import { ${t.toUpperCase()}_REPOSITORY, type I${t}Repository } from './${n}.repository'
849
- import type { ${t}ResponseDTO } from './dtos/${n}-response.dto'
850
- import type { Create${t}DTO } from './dtos/create-${n}.dto'
851
- import type { Update${t}DTO } from './dtos/update-${n}.dto'
852
-
853
- @Service()
854
- export class ${t}Service {
855
- constructor(
856
- @Inject(${t.toUpperCase()}_REPOSITORY) private readonly repo: I${t}Repository,
857
- ) {}
858
-
859
- async findById(id: string): Promise<${t}ResponseDTO | null> {
860
- return this.repo.findById(id)
861
- }
862
-
863
- async findAll(): Promise<${t}ResponseDTO[]> {
864
- return this.repo.findAll()
865
- }
866
-
867
- async findPaginated(parsed: ParsedQuery) {
868
- return this.repo.findPaginated(parsed)
869
- }
870
-
871
- async create(dto: Create${t}DTO): Promise<${t}ResponseDTO> {
872
- return this.repo.create(dto)
873
- }
874
-
875
- async update(id: string, dto: Update${t}DTO): Promise<${t}ResponseDTO> {
876
- return this.repo.update(id, dto)
877
- }
878
-
879
- async delete(id: string): Promise<void> {
880
- await this.repo.delete(id)
881
- }
882
- }
883
- `}function H(e){let{pascal:t}=e;return`import type { QueryFieldConfig } from '@forinda/kickjs'
884
-
885
- export const ${t.toUpperCase()}_QUERY_CONFIG: QueryFieldConfig = {
886
- filterable: ['name'],
887
- sortable: ['name', 'createdAt'],
888
- searchable: ['name'],
889
- }
890
- `}function he(e){let{pascal:t,kebab:n,plural:r=``,repo:i,style:a}=e,o={inmemory:`InMemory${t}Repository`,drizzle:`Drizzle${t}Repository`,prisma:`Prisma${t}Repository`},s={inmemory:`in-memory-${n}`,drizzle:`drizzle-${n}`,prisma:`prisma-${n}`},c=o[i]??o.inmemory,l=s[i]??s.inmemory,u=a??`define`,d=`/**
891
- * ${t} Module — CQRS Pattern
892
- *
893
- * Separates read (queries) and write (commands) operations.
894
- * Events are emitted after state changes and can be handled via
895
- * WebSocket broadcasts, queue jobs, or ETL pipelines.
896
- *
897
- * Structure:
898
- * commands/ — Write operations (create, update, delete)
899
- * queries/ — Read operations (get, list)
900
- * events/ — Domain events + handlers (WS broadcast, queue dispatch)
901
- * dtos/ — Request/response schemas
902
- */`,f=`import { ${t.toUpperCase()}_REPOSITORY } from './${n}.repository'
903
- import { ${c} } from './${l}.repository'
904
- import { ${t}Controller } from './${n}.controller'
905
-
906
- // Eagerly load decorated classes
907
- import.meta.glob(
908
- [
909
- './commands/**/*.ts',
910
- './queries/**/*.ts',
911
- './events/**/*.ts',
912
- '!./**/*.test.ts',
913
- ],
914
- { eager: true },
915
- )`,p=` /**
916
- * Declare HTTP routes for this CQRS module. Return value shape:
917
- *
918
- * - \`path\` — URL prefix for this route set.
919
- * - \`controller\` — Controller class (also drives OpenAPI).
920
- * - \`version\` — Optional. Overrides the app-wide API version.
921
- *
922
- * Return an array to mount multiple route sets:
923
- *
924
- * return [
925
- * { path: '/${r}', version: 1, controller: ${t}V1Controller },
926
- * { path: '/${r}', version: 2, controller: ${t}V2Controller },
927
- * ]
928
- */`;return u===`class`?`${d}
929
- import { Container, type AppModule, type ModuleRoutes } from '@forinda/kickjs'
930
- ${f}
931
-
932
- export class ${t}Module implements AppModule {
933
- register(container: Container): void {
934
- container.registerFactory(${t.toUpperCase()}_REPOSITORY, () =>
935
- container.resolve(${c}),
936
- )
937
- }
938
-
939
- ${p.replace(/^ {4}/gm,` `).replace(/^ {6}/gm,` `)}
940
- routes(): ModuleRoutes {
941
- return {
942
- path: '/${r}',
943
- controller: ${t}Controller,
944
- }
945
- }
946
- }
947
- `:`${d}
948
- import { defineModule } from '@forinda/kickjs'
949
- ${f}
950
-
951
- export const ${t}Module = defineModule({
952
- name: '${t}Module',
953
- build: () => ({
954
- register(container) {
955
- container.registerFactory(${t.toUpperCase()}_REPOSITORY, () =>
956
- container.resolve(${c}),
957
- )
958
- },
959
-
960
- ${p}
961
- routes() {
962
- return {
963
- path: '/${r}',
964
- controller: ${t}Controller,
965
- }
966
- },
967
- }),
968
- })
969
- `}function ge(e){let{pascal:t,kebab:n,plural:r=``,pluralPascal:i=``}=e;return`import { Controller, Get, Post, Put, Delete, Autowired, ApiQueryParams, type Ctx } from '@forinda/kickjs'
970
- import { ApiTags } from '@forinda/kickjs-swagger'
971
- import { Create${t}Command } from './commands/create-${n}.command'
972
- import { Update${t}Command } from './commands/update-${n}.command'
973
- import { Delete${t}Command } from './commands/delete-${n}.command'
974
- import { Get${t}Query } from './queries/get-${n}.query'
975
- import { List${i}Query } from './queries/list-${r}.query'
976
- import { create${t}Schema } from './dtos/create-${n}.dto'
977
- import { update${t}Schema } from './dtos/update-${n}.dto'
978
- import { ${t.toUpperCase()}_QUERY_CONFIG } from './${n}.constants'
979
-
980
- // Each handler annotates its \`ctx\` with \`Ctx<KickRoutes.${t}Controller['<method>']>\`
981
- // so \`ctx.params\`, \`ctx.body\`, and \`ctx.query\` are typed end-to-end.
982
- // The \`KickRoutes\` namespace is generated by \`kick typegen\` (auto-run on
983
- // \`kick dev\`) — see https://forinda.github.io/kick-js/guide/typegen.
984
-
985
- @Controller()
986
- export class ${t}Controller {
987
- @Autowired() private readonly create${t}Command!: Create${t}Command
988
- @Autowired() private readonly update${t}Command!: Update${t}Command
989
- @Autowired() private readonly delete${t}Command!: Delete${t}Command
990
- @Autowired() private readonly get${t}Query!: Get${t}Query
991
- @Autowired() private readonly list${i}Query!: List${i}Query
992
-
993
- @Get('/')
994
- @ApiTags('${t}')
995
- @ApiQueryParams(${t.toUpperCase()}_QUERY_CONFIG)
996
- async list(ctx: Ctx<KickRoutes.${t}Controller['list']>) {
997
- return ctx.paginate(
998
- (parsed) => this.list${i}Query.execute(parsed),
999
- ${t.toUpperCase()}_QUERY_CONFIG,
1000
- )
1001
- }
1002
-
1003
- @Get('/:id')
1004
- @ApiTags('${t}')
1005
- async getById(ctx: Ctx<KickRoutes.${t}Controller['getById']>) {
1006
- const result = await this.get${t}Query.execute(ctx.params.id)
1007
- if (!result) return ctx.notFound('${t} not found')
1008
- ctx.json(result)
1009
- }
1010
-
1011
- @Post('/', { body: create${t}Schema, name: 'Create${t}' })
1012
- @ApiTags('${t}')
1013
- async create(ctx: Ctx<KickRoutes.${t}Controller['create']>) {
1014
- const result = await this.create${t}Command.execute(ctx.body)
1015
- ctx.created(result)
1016
- }
1017
-
1018
- @Put('/:id', { body: update${t}Schema, name: 'Update${t}' })
1019
- @ApiTags('${t}')
1020
- async update(ctx: Ctx<KickRoutes.${t}Controller['update']>) {
1021
- const result = await this.update${t}Command.execute(ctx.params.id, ctx.body)
1022
- ctx.json(result)
1023
- }
1024
-
1025
- @Delete('/:id')
1026
- @ApiTags('${t}')
1027
- async remove(ctx: Ctx<KickRoutes.${t}Controller['remove']>) {
1028
- await this.delete${t}Command.execute(ctx.params.id)
1029
- ctx.noContent()
1030
- }
1031
- }
1032
- `}function _e(e){let{pascal:t,kebab:n}=e;return[{file:`create-${n}.command.ts`,content:`import { Service, Inject } from '@forinda/kickjs'
1033
- import { ${t.toUpperCase()}_REPOSITORY, type I${t}Repository } from '../${n}.repository'
1034
- import type { Create${t}DTO } from '../dtos/create-${n}.dto'
1035
- import type { ${t}ResponseDTO } from '../dtos/${n}-response.dto'
1036
- import { ${t}Events } from '../events/${n}.events'
1037
-
1038
- @Service()
1039
- export class Create${t}Command {
1040
- constructor(
1041
- @Inject(${t.toUpperCase()}_REPOSITORY) private readonly repo: I${t}Repository,
1042
- @Inject(${t}Events) private readonly events: ${t}Events,
1043
- ) {}
1044
-
1045
- async execute(dto: Create${t}DTO): Promise<${t}ResponseDTO> {
1046
- const result = await this.repo.create(dto)
1047
- this.events.emit('${n}.created', result)
1048
- return result
1049
- }
1050
- }
1051
- `},{file:`update-${n}.command.ts`,content:`import { Service, Inject } from '@forinda/kickjs'
1052
- import { ${t.toUpperCase()}_REPOSITORY, type I${t}Repository } from '../${n}.repository'
1053
- import type { Update${t}DTO } from '../dtos/update-${n}.dto'
1054
- import type { ${t}ResponseDTO } from '../dtos/${n}-response.dto'
1055
- import { ${t}Events } from '../events/${n}.events'
1056
-
1057
- @Service()
1058
- export class Update${t}Command {
1059
- constructor(
1060
- @Inject(${t.toUpperCase()}_REPOSITORY) private readonly repo: I${t}Repository,
1061
- @Inject(${t}Events) private readonly events: ${t}Events,
1062
- ) {}
1063
-
1064
- async execute(id: string, dto: Update${t}DTO): Promise<${t}ResponseDTO> {
1065
- const result = await this.repo.update(id, dto)
1066
- this.events.emit('${n}.updated', result)
1067
- return result
1068
- }
1069
- }
1070
- `},{file:`delete-${n}.command.ts`,content:`import { Service, Inject } from '@forinda/kickjs'
1071
- import { ${t.toUpperCase()}_REPOSITORY, type I${t}Repository } from '../${n}.repository'
1072
- import { ${t}Events } from '../events/${n}.events'
1073
-
1074
- @Service()
1075
- export class Delete${t}Command {
1076
- constructor(
1077
- @Inject(${t.toUpperCase()}_REPOSITORY) private readonly repo: I${t}Repository,
1078
- @Inject(${t}Events) private readonly events: ${t}Events,
1079
- ) {}
1080
-
1081
- async execute(id: string): Promise<void> {
1082
- await this.repo.delete(id)
1083
- this.events.emit('${n}.deleted', { id })
1084
- }
1085
- }
1086
- `}]}function ve(e){let{pascal:t,kebab:n,plural:r=``,pluralPascal:i=``}=e;return[{file:`get-${n}.query.ts`,content:`import { Service, Inject } from '@forinda/kickjs'
1087
- import { ${t.toUpperCase()}_REPOSITORY, type I${t}Repository } from '../${n}.repository'
1088
- import type { ${t}ResponseDTO } from '../dtos/${n}-response.dto'
1089
-
1090
- @Service()
1091
- export class Get${t}Query {
1092
- constructor(
1093
- @Inject(${t.toUpperCase()}_REPOSITORY) private readonly repo: I${t}Repository,
1094
- ) {}
1095
-
1096
- async execute(id: string): Promise<${t}ResponseDTO | null> {
1097
- return this.repo.findById(id)
1098
- }
1099
- }
1100
- `},{file:`list-${r}.query.ts`,content:`import { Service, Inject } from '@forinda/kickjs'
1101
- import { ${t.toUpperCase()}_REPOSITORY, type I${t}Repository } from '../${n}.repository'
1102
- import type { ParsedQuery } from '@forinda/kickjs'
1103
-
1104
- @Service()
1105
- export class List${i}Query {
1106
- constructor(
1107
- @Inject(${t.toUpperCase()}_REPOSITORY) private readonly repo: I${t}Repository,
1108
- ) {}
1109
-
1110
- async execute(parsed: ParsedQuery) {
1111
- return this.repo.findPaginated(parsed)
1112
- }
1113
- }
1114
- `}]}function ye(e){let{pascal:t,kebab:n}=e;return[{file:`${n}.events.ts`,content:`import { Service } from '@forinda/kickjs'
1115
- import { EventEmitter } from 'node:events'
1116
- import type { ${t}ResponseDTO } from '../dtos/${n}-response.dto'
1117
-
1118
- /**
1119
- * ${t} domain event types.
1120
- *
1121
- * These events are emitted by commands after state changes.
1122
- * Subscribe to them in event handlers for side effects:
1123
- * - WebSocket broadcasts (real-time UI updates)
1124
- * - Queue jobs (async processing, ETL pipelines)
1125
- * - Audit logging
1126
- * - Cache invalidation
1127
- */
1128
- export interface ${t}EventMap {
1129
- '${n}.created': ${t}ResponseDTO
1130
- '${n}.updated': ${t}ResponseDTO
1131
- '${n}.deleted': { id: string }
1132
- }
1133
-
1134
- @Service()
1135
- export class ${t}Events {
1136
- private emitter = new EventEmitter()
1137
-
1138
- emit<K extends keyof ${t}EventMap>(event: K, data: ${t}EventMap[K]): void {
1139
- this.emitter.emit(event, data)
1140
- }
1141
-
1142
- on<K extends keyof ${t}EventMap>(event: K, handler: (data: ${t}EventMap[K]) => void): void {
1143
- this.emitter.on(event, handler)
1144
- }
1145
-
1146
- off<K extends keyof ${t}EventMap>(event: K, handler: (data: ${t}EventMap[K]) => void): void {
1147
- this.emitter.off(event, handler)
1148
- }
1149
- }
1150
- `},{file:`on-${n}-change.handler.ts`,content:`import { Service, Autowired } from '@forinda/kickjs'
1151
- import { ${t}Events } from './${n}.events'
1152
-
1153
- /**
1154
- * ${t} Change Event Handler
1155
- *
1156
- * Reacts to domain events emitted by commands.
1157
- * Wire up side effects here:
1158
- *
1159
- * 1. WebSocket broadcast — notify connected clients in real-time
1160
- * import { WsGateway } from '@forinda/kickjs-ws'
1161
- * this.ws.broadcast('${n}-channel', { event, data })
1162
- *
1163
- * 2. Queue dispatch — offload heavy processing to background workers
1164
- * import { QueueService } from '@forinda/kickjs-queue'
1165
- * this.queue.add('${n}-etl', { action: event, payload: data })
1166
- *
1167
- * 3. ETL pipeline — transform and load data to external systems
1168
- * await this.etlPipeline.process(data)
1169
- */
1170
- @Service()
1171
- export class On${t}ChangeHandler {
1172
- @Autowired() private events!: ${t}Events
1173
-
1174
- // Uncomment to inject WebSocket and Queue services:
1175
- // @Autowired() private ws!: WsGateway
1176
- // @Autowired() private queue!: QueueService
1177
-
1178
- onInit(): void {
1179
- this.events.on('${n}.created', (data) => {
1180
- console.log('[${t}] Created:', data.id)
1181
- // TODO: Broadcast via WebSocket
1182
- // this.ws.broadcast('${n}-channel', { event: '${n}.created', data })
1183
- // TODO: Dispatch to queue for async processing / ETL
1184
- // this.queue.add('${n}-etl', { action: 'create', payload: data })
1185
- })
1186
-
1187
- this.events.on('${n}.updated', (data) => {
1188
- console.log('[${t}] Updated:', data.id)
1189
- // TODO: Broadcast via WebSocket
1190
- // this.ws.broadcast('${n}-channel', { event: '${n}.updated', data })
1191
- })
1192
-
1193
- this.events.on('${n}.deleted', (data) => {
1194
- console.log('[${t}] Deleted:', data.id)
1195
- // TODO: Broadcast via WebSocket
1196
- // this.ws.broadcast('${n}-channel', { event: '${n}.deleted', data })
1197
- })
1198
- }
1199
- }
1200
- `}]}function U(e){let{pascal:t,kebab:n,repoPrefix:r=`../../domain/repositories`,dtoPrefix:i=`../../application/dtos`}=e;return`/**
1201
- * Drizzle ${t} Repository
1202
- *
1203
- * Implements the repository interface using Drizzle ORM.
1204
- * Uses buildFromColumns() with Column objects for type-safe query building.
1205
- *
1206
- * TODO: Update the schema import to match your Drizzle schema file.
1207
- * TODO: Replace DRIZZLE_DB injection token with your actual database token.
1208
- *
1209
- * @Repository() registers this class in the DI container as a singleton.
1210
- */
1211
- import { eq, ne, gt, gte, lt, lte, ilike, inArray, between, and, or, asc, desc, count, sql } from 'drizzle-orm'
1212
- import { Repository, HttpException, Inject } from '@forinda/kickjs'
1213
- import { DRIZZLE_DB, DrizzleQueryAdapter } from '@forinda/kickjs-drizzle'
1214
- import type { ParsedQuery } from '@forinda/kickjs'
1215
- import type { I${t}Repository } from '${r}/${n}.repository'
1216
- import type { ${t}ResponseDTO } from '${i}/${n}-response.dto'
1217
- import type { Create${t}DTO } from '${i}/create-${n}.dto'
1218
- import type { Update${t}DTO } from '${i}/update-${n}.dto'
1219
- import { ${t.toUpperCase()}_QUERY_CONFIG } from '../../constants'
1220
-
1221
- // TODO: Import your Drizzle schema table — e.g.:
1222
- // import { ${n}s } from '@/db/schema'
1223
-
1224
- const queryAdapter = new DrizzleQueryAdapter({
1225
- eq, ne, gt, gte, lt, lte, ilike, inArray, between, and, or, asc, desc,
1226
- })
1227
-
1228
- @Repository()
1229
- export class Drizzle${t}Repository implements I${t}Repository {
1230
- constructor(@Inject(DRIZZLE_DB) private db: any) {}
1231
-
1232
- async findById(id: string): Promise<${t}ResponseDTO | null> {
1233
- // TODO: Implement with Drizzle
1234
- // const row = this.db.select().from(${n}s).where(eq(${n}s.id, id)).get()
1235
- // return row ?? null
1236
- throw new Error('Drizzle ${t} repository not yet implemented — update schema imports and queries')
1237
- }
1238
-
1239
- async findAll(): Promise<${t}ResponseDTO[]> {
1240
- // TODO: Implement with Drizzle
1241
- // return this.db.select().from(${n}s).all()
1242
- throw new Error('Drizzle ${t} repository not yet implemented')
1243
- }
1244
-
1245
- async findPaginated(parsed: ParsedQuery): Promise<{ data: ${t}ResponseDTO[]; total: number }> {
1246
- // TODO: Use buildFromColumns() with your query config for type-safe filtering
1247
- // const query = queryAdapter.buildFromColumns(parsed, ${t.toUpperCase()}_QUERY_CONFIG)
1248
- //
1249
- // const data = this.db
1250
- // .select().from(${n}s).$dynamic()
1251
- // .where(query.where).orderBy(...query.orderBy)
1252
- // .limit(query.limit).offset(query.offset).all()
1253
- //
1254
- // const totalResult = this.db
1255
- // .select({ count: count() }).from(${n}s)
1256
- // .$dynamic().where(query.where).get()
1257
- //
1258
- // return { data, total: totalResult?.count ?? 0 }
1259
- throw new Error('Drizzle ${t} repository not yet implemented')
1260
- }
1261
-
1262
- async create(dto: Create${t}DTO): Promise<${t}ResponseDTO> {
1263
- // TODO: Implement with Drizzle
1264
- // return this.db.insert(${n}s).values(dto).returning().get()
1265
- throw new Error('Drizzle ${t} repository not yet implemented')
1266
- }
1267
-
1268
- async update(id: string, dto: Update${t}DTO): Promise<${t}ResponseDTO> {
1269
- // TODO: Implement with Drizzle
1270
- // const row = this.db.update(${n}s).set(dto).where(eq(${n}s.id, id)).returning().get()
1271
- // if (!row) throw HttpException.notFound('${t} not found')
1272
- // return row
1273
- throw new Error('Drizzle ${t} repository not yet implemented')
1274
- }
1275
-
1276
- async delete(id: string): Promise<void> {
1277
- // TODO: Implement with Drizzle
1278
- // this.db.delete(${n}s).where(eq(${n}s.id, id)).run()
1279
- throw new Error('Drizzle ${t} repository not yet implemented')
1280
- }
1281
- }
1282
- `}function be(e){let{pascal:t,kebab:n}=e;return`import type { DrizzleQueryParamsConfig } from '@forinda/kickjs-drizzle'
1283
- // TODO: Import your schema table and reference actual columns for type safety
1284
- // import { ${n}s } from '@/db/schema'
1285
-
1286
- export const ${t.toUpperCase()}_QUERY_CONFIG: DrizzleQueryParamsConfig = {
1287
- columns: {
1288
- // Replace with actual Drizzle Column references for type-safe filtering:
1289
- // name: ${n}s.name,
1290
- // status: ${n}s.status,
1291
- },
1292
- sortable: {
1293
- // name: ${n}s.name,
1294
- // createdAt: ${n}s.createdAt,
1295
- },
1296
- searchColumns: [
1297
- // ${n}s.name,
1298
- ],
1299
- }
1300
- `}function W(e){let{pascal:t,kebab:n,repoPrefix:r=`../../domain/repositories`,dtoPrefix:i=`../../application/dtos`}=e,a=n.replace(/-([a-z])/g,(e,t)=>t.toUpperCase());return`/**
1301
- * Prisma ${t} Repository
1302
- *
1303
- * Implements the repository interface using Prisma Client.
1304
- * Requires a PrismaClient instance injected via the DI container.
1305
- *
1306
- * Ensure your Prisma schema has a '${t}' model defined.
1307
- *
1308
- * For full Prisma field-level type safety, replace PrismaModelDelegate with your PrismaClient:
1309
- * @Inject(PRISMA_CLIENT) private prisma!: PrismaClient
1310
- *
1311
- * @Repository() registers this class in the DI container as a singleton.
1312
- */
1313
- import { Repository, HttpException, Inject } from '@forinda/kickjs'
1314
- import { PRISMA_CLIENT, type PrismaModelDelegate } from '@forinda/kickjs-prisma'
1315
- import type { ParsedQuery } from '@forinda/kickjs'
1316
- import type { I${t}Repository } from '${r}/${n}.repository'
1317
- import type { ${t}ResponseDTO } from '${i}/${n}-response.dto'
1318
- import type { Create${t}DTO } from '${i}/create-${n}.dto'
1319
- import type { Update${t}DTO } from '${i}/update-${n}.dto'
1320
-
1321
- @Repository()
1322
- export class Prisma${t}Repository implements I${t}Repository {
1323
- @Inject(PRISMA_CLIENT) private prisma!: { ${a}: PrismaModelDelegate }
1324
-
1325
- async findById(id: string): Promise<${t}ResponseDTO | null> {
1326
- return this.prisma.${a}.findUnique({ where: { id } }) as Promise<${t}ResponseDTO | null>
1327
- }
1328
-
1329
- async findAll(): Promise<${t}ResponseDTO[]> {
1330
- return this.prisma.${a}.findMany() as Promise<${t}ResponseDTO[]>
1331
- }
1332
-
1333
- async findPaginated(parsed: ParsedQuery): Promise<{ data: ${t}ResponseDTO[]; total: number }> {
1334
- const [data, total] = await Promise.all([
1335
- this.prisma.${a}.findMany({
1336
- skip: parsed.pagination.offset,
1337
- take: parsed.pagination.limit,
1338
- }) as Promise<${t}ResponseDTO[]>,
1339
- this.prisma.${a}.count(),
1340
- ])
1341
- return { data, total }
1342
- }
1343
-
1344
- async create(dto: Create${t}DTO): Promise<${t}ResponseDTO> {
1345
- return this.prisma.${a}.create({ data: dto as Record<string, unknown> }) as Promise<${t}ResponseDTO>
1346
- }
1347
-
1348
- async update(id: string, dto: Update${t}DTO): Promise<${t}ResponseDTO> {
1349
- const existing = await this.prisma.${a}.findUnique({ where: { id } })
1350
- if (!existing) throw HttpException.notFound('${t} not found')
1351
- return this.prisma.${a}.update({ where: { id }, data: dto as Record<string, unknown> }) as Promise<${t}ResponseDTO>
1352
- }
1353
-
1354
- async delete(id: string): Promise<void> {
1355
- await this.prisma.${a}.deleteMany({ where: { id } })
1356
- }
1357
- }
1358
- `}function xe(e,t,n,r=[]){switch(t){case`cqrs`:{let t=[],i=[];return r.includes(`devtools`)&&(t.push(`import { DevToolsAdapter } from '@forinda/kickjs-devtools'`),i.push(` DevToolsAdapter(),`)),r.includes(`swagger`)&&(t.push(`import { SwaggerAdapter } from '@forinda/kickjs-swagger'`),i.push(` SwaggerAdapter({\n info: { title: '${e}', version: '${n}' },\n }),`)),`import 'reflect-metadata'
1359
- // Side-effect import — registers the extended env schema with kickjs
1360
- // **before** any controller / service / @Value gets resolved. Without
1361
- // this line ConfigService.get('YOUR_KEY') returns undefined because the
1362
- // cached schema would still be the base shape. See guide/configuration.
1363
- import './config'
1364
- import { bootstrap } from '@forinda/kickjs'
1365
- // import { WsAdapter } from '@forinda/kickjs-ws'
1366
- // import { QueueAdapter, BullMQProvider } from '@forinda/kickjs-queue'
1367
- ${t.length?t.join(`
1368
- `)+`
1369
- `:``}import { modules } from './modules'
1370
-
1371
- // Export the app for the Vite plugin (dev mode)
1372
- export const app = await bootstrap({
1373
- modules,${t.length?`\n adapters: [\n${i.join(`
1374
- `)}\n // Uncomment for WebSocket support:\n // WsAdapter(),\n // Uncomment when Redis is available:\n // QueueAdapter({\n // provider: new BullMQProvider({ host: 'localhost', port: 6379 }),\n // }),\n ],`:`
1375
- adapters: [
1376
- // Uncomment for WebSocket support:
1377
- // WsAdapter(),
1378
- // Uncomment when Redis is available:
1379
- // QueueAdapter({
1380
- // provider: new BullMQProvider({ host: 'localhost', port: 6379 }),
1381
- // }),
1382
- ],`}
1383
- })
1384
- `}case`minimal`:{let t=[],i=[];return r.includes(`swagger`)&&(t.push(`import { SwaggerAdapter } from '@forinda/kickjs-swagger'`),i.push(` SwaggerAdapter({ info: { title: '${e}', version: '${n}' } }),`)),r.includes(`devtools`)&&(t.push(`import { DevToolsAdapter } from '@forinda/kickjs-devtools'`),i.push(` DevToolsAdapter(),`)),`import 'reflect-metadata'
1385
- // Side-effect import — registers the extended env schema with kickjs
1386
- // **before** any controller / service / @Value gets resolved. Without
1387
- // this line ConfigService.get('YOUR_KEY') returns undefined because the
1388
- // cached schema would still be the base shape. See guide/configuration.
1389
- import './config'
1390
- import { bootstrap } from '@forinda/kickjs'
1391
- ${t.length?t.join(`
1392
- `)+`
1393
- `:``}import { modules } from './modules'
1394
-
1395
- // Export the app for the Vite plugin (dev mode)
1396
- export const app = await bootstrap({ modules${i.length?`,\n adapters: [\n${i.join(`
1397
- `)}\n ]`:``} })
1398
- `}default:{let t=[],i=[];return r.includes(`devtools`)&&(t.push(`import { DevToolsAdapter } from '@forinda/kickjs-devtools'`),i.push(` DevToolsAdapter(),`)),r.includes(`swagger`)&&(t.push(`import { SwaggerAdapter } from '@forinda/kickjs-swagger'`),i.push(` SwaggerAdapter({\n info: { title: '${e}', version: '${n}' },\n }),`)),`import 'reflect-metadata'
1399
- // Side-effect import — registers the extended env schema with kickjs
1400
- // **before** any controller / service / @Value gets resolved. Without
1401
- // this line ConfigService.get('YOUR_KEY') returns undefined because the
1402
- // cached schema would still be the base shape. See guide/configuration.
1403
- import './config'
1404
- import express from 'express'
1405
- import {
1406
- bootstrap,
1407
- requestId,
1408
- requestLogger,
1409
- helmet,
1410
- cors,
1411
- } from '@forinda/kickjs'
1412
- ${t.length?t.join(`
1413
- `)+`
1414
- `:``}import { modules } from './modules'
1415
-
1416
- // Export the app for the Vite plugin (dev mode)
1417
- export const app = await bootstrap({
1418
- modules,${i.length?`\n adapters: [\n${i.join(`
1419
- `)}\n ],`:``}
1420
- middleware: [
1421
- helmet(),
1422
- cors({ origin: '*' }),
1423
- requestId(),
1424
- requestLogger(),
1425
- express.json(),
1426
- ],
1427
- })
1428
- `}}}function Se(){return`import { defineModules } from '@forinda/kickjs'
1429
- import { HelloModule } from './hello/hello.module'
1430
-
1431
- // Remove HelloModule and run: kick g module <name>
1432
- // \`defineModules()\` returns a chainable list — \`kick g module\` appends
1433
- // \`.mount(NewModule())\` to the chain on every generation.
1434
- export const modules = defineModules().mount(HelloModule())
1435
- `}function Ce(e=`zod`){return e===`valibot`?`import { loadEnvFromSchema } from '@forinda/kickjs/config'
1436
- import { fromValibot } from '@forinda/kickjs-schema/valibot'
1437
- import * as v from 'valibot'
1438
-
1439
- /**
1440
- * Project environment schema (Valibot).
1441
- *
1442
- * \`fromValibot\` wraps the Valibot schema as a \`KickSchema\` so the
1443
- * env loader, validate middleware, and swagger spec generator all see
1444
- * the same shape. The default export is the contract \`kick typegen\`
1445
- * reads to populate \`KickEnv\` via \`InferSchemaOutput<typeof _envSchema>\`
1446
- * — that's what makes \`@Value('FOO')\` autocomplete and
1447
- * \`process.env.FOO\` typed.
1448
- *
1449
- * @example
1450
- * DATABASE_URL: v.pipe(v.string(), v.url()),
1451
- * JWT_SECRET: v.pipe(v.string(), v.minLength(32)),
1452
- * REDIS_URL: v.optional(v.pipe(v.string(), v.url())),
1453
- */
1454
- const envSchema = fromValibot(
1455
- v.object({
1456
- PORT: v.optional(v.pipe(v.string(), v.transform(Number)), '3000'),
1457
- NODE_ENV: v.optional(v.picklist(['development', 'production', 'test']), 'development'),
1458
- LOG_LEVEL: v.optional(v.string(), 'info'),
1459
- // DATABASE_URL: v.pipe(v.string(), v.url()),
1460
- }),
1461
- )
1462
-
1463
- /**
1464
- * IMPORTANT — side effect: register the schema with kickjs's env cache
1465
- * **at module-load time**. \`ConfigService\` and \`@Value()\` both consume
1466
- * this cache, and they will fall back to the base schema (or undefined)
1467
- * if no extended schema has been registered before they're resolved.
1468
- *
1469
- * As long as \`src/index.ts\` imports this file (\`import './config'\`) at
1470
- * the top — before \`bootstrap()\` runs — every controller and service
1471
- * in the app sees the typed extended values.
1472
- */
1473
- export const env = loadEnvFromSchema(envSchema)
1474
-
1475
- export default envSchema
1476
- `:e===`yup`?`import { loadEnvFromSchema } from '@forinda/kickjs/config'
1477
- import { fromYup } from '@forinda/kickjs-schema/yup'
1478
- import * as yup from 'yup'
1479
-
1480
- /**
1481
- * Project environment schema (Yup).
1482
- *
1483
- * \`fromYup\` wraps the Yup schema as a \`KickSchema\` so the env loader,
1484
- * validate middleware, and swagger spec generator all see the same
1485
- * shape. The default export is the contract \`kick typegen\` reads to
1486
- * populate \`KickEnv\` via \`InferSchemaOutput<typeof _envSchema>\`.
1487
- *
1488
- * Note: Yup's \`.url()\` defaults to http/https; database connection
1489
- * strings like \`postgres://\` use \`.matches(/^[a-z]+:\\/\\/.+/i)\` or
1490
- * a plain \`.string().required()\`.
1491
- *
1492
- * @example
1493
- * DATABASE_URL: yup.string().required(),
1494
- * JWT_SECRET: yup.string().min(32).required(),
1495
- * REDIS_URL: yup.string().url().optional(),
1496
- */
1497
- const envSchema = fromYup(
1498
- yup.object({
1499
- PORT: yup.number().default(3000),
1500
- NODE_ENV: yup
1501
- .string()
1502
- .oneOf(['development', 'production', 'test'])
1503
- .default('development'),
1504
- LOG_LEVEL: yup.string().default('info'),
1505
- // DATABASE_URL: yup.string().required(),
1506
- }),
1507
- )
1508
-
1509
- /**
1510
- * IMPORTANT — side effect: register the schema with kickjs's env cache
1511
- * **at module-load time**. \`ConfigService\` and \`@Value()\` both consume
1512
- * this cache, and they will fall back to the base schema (or undefined)
1513
- * if no extended schema has been registered before they're resolved.
1514
- *
1515
- * As long as \`src/index.ts\` imports this file (\`import './config'\`) at
1516
- * the top — before \`bootstrap()\` runs — every controller and service
1517
- * in the app sees the typed extended values.
1518
- */
1519
- export const env = loadEnvFromSchema(envSchema)
1520
-
1521
- export default envSchema
1522
- `:`import { loadEnvFromSchema } from '@forinda/kickjs/config'
1523
- import { fromZod } from '@forinda/kickjs-schema/zod'
1524
- import { z } from 'zod'
1525
-
1526
- /**
1527
- * Project environment schema (Zod).
1528
- *
1529
- * \`fromZod\` wraps the Zod schema as a \`KickSchema\` so the env loader,
1530
- * validate middleware, and swagger spec generator all see the same
1531
- * shape. The default export is the contract \`kick typegen\` reads to
1532
- * populate \`KickEnv\` via \`InferSchemaOutput<typeof _envSchema>\` —
1533
- * that's what makes \`@Value('FOO')\` autocomplete and
1534
- * \`process.env.FOO\` typed.
1535
- *
1536
- * @example
1537
- * DATABASE_URL: z.string().url(),
1538
- * JWT_SECRET: z.string().min(32),
1539
- * REDIS_URL: z.string().url().optional(),
1540
- */
1541
- const envSchema = fromZod(
1542
- z.object({
1543
- PORT: z.coerce.number().default(3000),
1544
- NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
1545
- LOG_LEVEL: z.string().default('info'),
1546
- // DATABASE_URL: z.string().url(),
1547
- }),
1548
- )
1549
-
1550
- /**
1551
- * IMPORTANT — side effect: register the schema with kickjs's env cache
1552
- * **at module-load time**. \`ConfigService\` and \`@Value()\` both consume
1553
- * this cache, and they will fall back to the base schema (or undefined)
1554
- * if no extended schema has been registered before they're resolved.
1555
- *
1556
- * As long as \`src/index.ts\` imports this file (\`import './config'\`) at
1557
- * the top — before \`bootstrap()\` runs — every controller and service
1558
- * in the app sees the typed extended values.
1559
- */
1560
- export const env = loadEnvFromSchema(envSchema)
1561
-
1562
- export default envSchema
1563
- `}function we(){return`import { Service } from '@forinda/kickjs'
1564
-
1565
- @Service()
1566
- export class HelloService {
1567
- greet(name: string) {
1568
- return { message: \`Hello \${name} from KickJS!\`, timestamp: new Date().toISOString() }
1569
- }
1570
-
1571
- healthCheck() {
1572
- return { status: 'ok', uptime: process.uptime() }
1573
- }
1574
- }
1575
- `}function Te(){return`import { Controller, Get, Autowired, type Ctx } from '@forinda/kickjs'
1576
- import { HelloService } from './hello.service'
1577
-
1578
- // \`Ctx<KickRoutes.HelloController['<method>']>\` is generated by
1579
- // \`kick typegen\` (auto-run on \`kick dev\`). The first run after a fresh
1580
- // scaffold creates \`.kickjs/types/routes.ts\` so this file typechecks.
1581
- // See https://forinda.github.io/kick-js/guide/typegen.
1582
-
1583
- @Controller()
1584
- export class HelloController {
1585
- @Autowired() private readonly helloService!: HelloService
1586
-
1587
- @Get('/')
1588
- index(ctx: Ctx<KickRoutes.HelloController['index']>) {
1589
- ctx.json(this.helloService.greet('World'))
1590
- }
1591
-
1592
- @Get('/health')
1593
- health(ctx: Ctx<KickRoutes.HelloController['health']>) {
1594
- ctx.json(this.helloService.healthCheck())
1595
- }
1596
- }
1597
- `}function Ee(){return`import { defineModule } from '@forinda/kickjs'
1598
- import { HelloController } from './hello.controller'
1599
-
1600
- export const HelloModule = defineModule({
1601
- name: 'HelloModule',
1602
- build: () => ({
1603
- // \`register(container)\` is optional — only implement it when you need
1604
- // to bind a token to a concrete implementation, e.g.
1605
- // register(container) {
1606
- // container.registerFactory(USER_REPOSITORY, () => container.resolve(InMemoryUserRepository))
1607
- // }
1608
- // The HelloService uses @Service() so the decorator handles registration.
1609
-
1610
- routes() {
1611
- return {
1612
- path: '/hello',
1613
- controller: HelloController,
1614
- }
1615
- },
1616
- }),
1617
- })
1618
- `}function De(e,t=`inmemory`,n=`pnpm`){return`import { defineConfig } from '@forinda/kickjs-cli'
1619
-
1620
- export default defineConfig({
1621
- pattern: '${e}',
1622
- // Pinned so \`kick add\` and other dep-installing commands always use the
1623
- // project's intended package manager, regardless of which lockfile exists.
1624
- packageManager: '${n}',
1625
- modules: {
1626
- dir: 'src/modules',
1627
- repo: ${[`drizzle`,`inmemory`,`prisma`].includes(t)?`'${t}'`:`{ name: '${t}' }`},
1628
- pluralize: true,
1629
- },
1630
-
1631
- // \`kick typegen\` populates \`.kickjs/types/\` so \`Ctx<KickRoutes.X['method']>\`
1632
- // resolves to fully-typed params/body/query. Auto-runs on \`kick dev\`.
1633
- // \`'kickjs-schema'\` routes inference through \`InferSchemaOutput\` so the
1634
- // typegen works for any wrapped schema (Zod / Valibot / Yup). Switch
1635
- // to \`'zod'\` if you ship Zod schemas without \`fromZod()\` wrapping, or
1636
- // set \`schemaValidator: false\` to skip schema-driven body typing.
1637
- typegen: {
1638
- schemaValidator: 'kickjs-schema',
1639
- },
1640
-
1641
- commands: [
1642
- {
1643
- name: 'test',
1644
- description: 'Run tests with Vitest',
1645
- steps: 'npx vitest run',
1646
- },
1647
- {
1648
- name: 'format',
1649
- description: 'Format code with Prettier',
1650
- steps: 'npx prettier --write src/',
1651
- },
1652
- {
1653
- name: 'format:check',
1654
- description: 'Check formatting without writing',
1655
- steps: 'npx prettier --check src/',
1656
- },
1657
- {
1658
- name: 'ci:check',
1659
- description: 'Run typecheck + format check',
1660
- steps: ['npx tsc --noEmit', 'npx prettier --check src/'],
1661
- aliases: ['verify'],
1662
- },
1663
- ],
1664
- })
1665
- `}async function Oe(e){let{pascal:t,kebab:n,plural:r,style:i,write:a}=e;await a(`${n}.module.ts`,oe({pascal:t,kebab:n,plural:r,style:i})),await a(`${n}.controller.ts`,`import { Controller, Get, type Ctx } from '@forinda/kickjs'
1666
-
1667
- // \`Ctx<KickRoutes.${t}Controller['<method>']>\` is generated by
1668
- // \`kick typegen\` (auto-run on \`kick dev\`).
1669
-
1670
- @Controller()
1671
- export class ${t}Controller {
1672
- @Get('/')
1673
- async list(ctx: Ctx<KickRoutes.${t}Controller['list']>) {
1674
- ctx.json({ message: '${t} list' })
1675
- }
1676
- }
1677
- `)}async function ke(e){let{pascal:t,kebab:n,plural:r,pluralPascal:i,repo:a,noTests:o,prismaClientPath:s,tokenScope:c,style:l,write:u}=e;await u(`${n}.module.ts`,ae({pascal:t,kebab:n,plural:r,repo:a,style:l})),await u(`${n}.constants.ts`,H({pascal:t,kebab:n})),await u(`${n}.controller.ts`,ce({pascal:t,kebab:n,plural:r,pluralPascal:i})),await u(`${n}.service.ts`,me({pascal:t,kebab:n})),await u(`dtos/create-${n}.dto.ts`,P({pascal:t,kebab:n})),await u(`dtos/update-${n}.dto.ts`,F({pascal:t,kebab:n})),await u(`dtos/${n}-response.dto.ts`,I({pascal:t,kebab:n})),await u(`${n}.repository.ts`,L({pascal:t,kebab:n,dtoPrefix:`./dtos`,tokenScope:c}));let d={inmemory:`in-memory-${n}`,drizzle:`drizzle-${n}`,prisma:`prisma-${n}`},f={inmemory:()=>R({pascal:t,kebab:n,repoPrefix:`.`,dtoPrefix:`./dtos`}),drizzle:()=>U({pascal:t,kebab:n,repoPrefix:`.`,dtoPrefix:`./dtos`}),prisma:()=>W({pascal:t,kebab:n,repoPrefix:`.`,dtoPrefix:`./dtos`,prismaClientPath:s})},p=d[a]??`${E(a)}-${n}`,m=f[a]??(()=>z({pascal:t,kebab:n,repoType:a,repoPrefix:`.`,dtoPrefix:`./dtos`}));await u(`${p}.repository.ts`,m()),o||(a!==`inmemory`&&await u(`in-memory-${n}.repository.ts`,R({pascal:t,kebab:n,repoPrefix:`.`,dtoPrefix:`./dtos`})),await u(`__tests__/${n}.controller.test.ts`,B({pascal:t,kebab:n,plural:r})),await u(`__tests__/${n}.repository.test.ts`,V({pascal:t,kebab:n,plural:r,repoPrefix:`../${d.inmemory??`in-memory-${n}`}.repository`})))}async function Ae(e){let{pascal:t,kebab:n,plural:r,pluralPascal:i,repo:a,noTests:o,prismaClientPath:s,tokenScope:c,style:l,write:u}=e;await u(`${n}.module.ts`,he({pascal:t,kebab:n,plural:r,repo:a,style:l})),await u(`${n}.constants.ts`,H({pascal:t,kebab:n})),await u(`${n}.controller.ts`,ge({pascal:t,kebab:n,plural:r,pluralPascal:i})),await u(`dtos/create-${n}.dto.ts`,P({pascal:t,kebab:n})),await u(`dtos/update-${n}.dto.ts`,F({pascal:t,kebab:n})),await u(`dtos/${n}-response.dto.ts`,I({pascal:t,kebab:n}));let d=_e({pascal:t,kebab:n});for(let e of d)await u(`commands/${e.file}`,e.content);let f=ve({pascal:t,kebab:n,plural:r,pluralPascal:i});for(let e of f)await u(`queries/${e.file}`,e.content);let p=ye({pascal:t,kebab:n});for(let e of p)await u(`events/${e.file}`,e.content);await u(`${n}.repository.ts`,L({pascal:t,kebab:n,dtoPrefix:`./dtos`,tokenScope:c}));let m={inmemory:`in-memory-${n}`,drizzle:`drizzle-${n}`,prisma:`prisma-${n}`},h={inmemory:()=>R({pascal:t,kebab:n,repoPrefix:`.`,dtoPrefix:`./dtos`}),drizzle:()=>U({pascal:t,kebab:n,repoPrefix:`.`,dtoPrefix:`./dtos`}),prisma:()=>W({pascal:t,kebab:n,repoPrefix:`.`,dtoPrefix:`./dtos`,prismaClientPath:s})},g=m[a]??`${E(a)}-${n}`,_=h[a]??(()=>z({pascal:t,kebab:n,repoType:a,repoPrefix:`.`,dtoPrefix:`./dtos`}));await u(`${g}.repository.ts`,_()),o||(a!==`inmemory`&&await u(`in-memory-${n}.repository.ts`,R({pascal:t,kebab:n,repoPrefix:`.`,dtoPrefix:`./dtos`})),await u(`__tests__/${n}.controller.test.ts`,B({pascal:t,kebab:n,plural:r})),await u(`__tests__/${n}.repository.test.ts`,V({pascal:t,kebab:n,plural:r,repoPrefix:`../${m.inmemory??`in-memory-${n}`}.repository`})))}async function je(e){let{pascal:t,kebab:n,plural:r,pluralPascal:i,repo:a,noEntity:o,noTests:s,prismaClientPath:c,tokenScope:l,style:u,write:d}=e;await d(`${n}.module.ts`,ie({pascal:t,kebab:n,plural:r,repo:a,style:u})),await d(`constants.ts`,a===`drizzle`?be({pascal:t,kebab:n}):le({pascal:t,kebab:n})),await d(`presentation/${n}.controller.ts`,se({pascal:t,kebab:n,plural:r,pluralPascal:i})),await d(`application/dtos/create-${n}.dto.ts`,P({pascal:t,kebab:n})),await d(`application/dtos/update-${n}.dto.ts`,F({pascal:t,kebab:n})),await d(`application/dtos/${n}-response.dto.ts`,I({pascal:t,kebab:n}));let f=ue({pascal:t,kebab:n,plural:r,pluralPascal:i});for(let e of f)await d(`application/use-cases/${e.file}`,e.content);await d(`domain/repositories/${n}.repository.ts`,L({pascal:t,kebab:n,tokenScope:l})),await d(`domain/services/${n}-domain.service.ts`,de({pascal:t,kebab:n}));let p={inmemory:`in-memory-${n}`,drizzle:`drizzle-${n}`,prisma:`prisma-${n}`},m={inmemory:()=>R({pascal:t,kebab:n}),drizzle:()=>U({pascal:t,kebab:n}),prisma:()=>W({pascal:t,kebab:n,prismaClientPath:c})},h=p[a]??`${E(a)}-${n}`,g=m[a]??(()=>z({pascal:t,kebab:n,repoType:a}));await d(`infrastructure/repositories/${h}.repository.ts`,g()),o||(await d(`domain/entities/${n}.entity.ts`,fe({pascal:t,kebab:n})),await d(`domain/value-objects/${n}-id.vo.ts`,pe({pascal:t,kebab:n}))),s||(a!==`inmemory`&&await d(`infrastructure/repositories/in-memory-${n}.repository.ts`,R({pascal:t,kebab:n})),await d(`__tests__/${n}.controller.test.ts`,B({pascal:t,kebab:n,plural:r})),await d(`__tests__/${n}.repository.test.ts`,V({pascal:t,kebab:n,plural:r})))}function Me(e){return e?typeof e==`string`?e:e.name:`inmemory`}async function Ne(t){let{name:n,modulesDir:a,noEntity:c,noTests:l,repo:u=`inmemory`,force:d,dryRun:p}=t,m=t.pluralize!==!1,h=t.pattern??`ddd`;t.minimal&&(h=`minimal`);let g=E(n),_=w(n),v=m?D(g):g,y=m?O(_):_,b=f(a,v),x=[],S=d??!1,C={kebab:g,pascal:_,plural:v,pluralPascal:y,moduleDir:b,repo:u,noEntity:c??!1,noTests:l??!1,prismaClientPath:t.prismaClientPath??`@prisma/client`,tokenScope:t.tokenScope??`app`,style:t.style??`define`,write:async(t,n)=>{let a=f(b,t);if(p){x.push(a);return}if(!S&&await s(a)&&!await o({message:`File exists: ${r.dim(t)}. Overwrite?`,initialValue:!1})){i.warn(`Skipped: ${t}`);return}await e(a,n),x.push(a)},files:x};switch(h){case`minimal`:await Oe(C);break;case`rest`:await ke(C);break;case`cqrs`:await Ae(C);break;default:await je(C);break}return p||await G(a,_,v,g,C.style),x}async function G(t,n,r,i,a=`define`){let o=f(t,`index.ts`),c=await s(o),l=`./${r}/${i}.module`,u=a===`class`?`${n}Module`:`${n}Module()`;if(!c){await e(o,a===`class`?`import type { AppModuleEntry } from '@forinda/kickjs'
1678
- import { ${n}Module } from '${l}'
1679
-
1680
- export const modules: AppModuleEntry[] = [${u}]
1681
- `:`import { defineModules } from '@forinda/kickjs'
1682
- import { ${n}Module } from '${l}'
1683
-
1684
- export const modules = defineModules().mount(${u})
1685
- `);return}let d=await y(o,`utf-8`),p=`import { ${n}Module } from '${l}'`,m=k(l);if(!RegExp(`^import\\s*\\{[^}]*\\b${k(n)}Module\\b[^}]*\\}\\s*from\\s*['"]${m}['"]`,`m`).test(d)){let e=d.lastIndexOf(`import `);if(e!==-1){let t=d.indexOf(`
1686
- `,e);d=d.slice(0,t+1)+p+`
1687
- `+d.slice(t+1)}else d=p+`
1688
- `+d}let h=q(d);if(h){let e=d.slice(h.rhsStart,h.rhsEnd+1);RegExp(`\\b${k(n)}Module\\b`).test(e)||(d=K(d,u))}else d=K(d,u);await b(o,d,`utf-8`)}function K(e,t){let n=q(e);if(!n)return e;if(n.shape===`array`){let r=e.slice(n.rhsStart+1,n.rhsEnd),i=r.trim(),a;if(!i)a=`[${t}]`;else{let e=i.endsWith(`,`)?``:`,`;a=`[${r.trimEnd()}${e} ${t}]`}return e.slice(0,n.rhsStart)+a+e.slice(n.rhsEnd+1)}return`${e.slice(0,n.chainEnd)}\n .mount(${t})${e.slice(n.chainEnd)}`}function q(e){let t=/export\s+const\s+modules\b[^=]*=/.exec(e);if(!t)return null;let n=t.index+t[0].length;for(;n<e.length&&/\s/.test(e[n]??``);)n++;if(e[n]===`[`){let t=Fe(e,n);return t===-1?null:{shape:`array`,rhsStart:n,rhsEnd:t}}if(e.slice(n,n+13)===`defineModules`){let t=Pe(e,n);return t===-1?null:{shape:`chain`,rhsStart:n,rhsEnd:t-1,chainEnd:t}}return null}function Pe(e,t=0){let n=/defineModules\s*\(/g;n.lastIndex=t;let r=n.exec(e);if(!r)return-1;let i=r.index+r[0].length-1;if(e[i]!==`(`||(i=Y(e,i),i===-1))return-1;for(i++;;){let t=i;for(;t<e.length&&/\s/.test(e[t]??``);)t++;if(e[t]!==`.`||e.slice(t,t+6)!==`.mount`)break;for(t+=6;t<e.length&&/\s/.test(e[t]??``);)t++;if(e[t]!==`(`)break;let n=Y(e,t);if(n===-1)break;i=n+1}return i}function J(e,t){let n=e.slice(t,t+2);if(n===`//`){for(t+=2;t<e.length&&e[t]!==`
1689
- `;)t++;return t}if(n===`/*`){for(t+=2;t+1<e.length&&!(e[t]===`*`&&e[t+1]===`/`);)t++;return t+2}return t}function Fe(e,t){if(e[t]!==`[`)return-1;let n=1,r=t+1;for(;r<e.length;){let t=e.slice(r,r+2);if(t===`//`||t===`/*`){r=J(e,r);continue}let i=e[r]??``;if(i===`'`||i===`"`||i==="`"){let t=i;for(r++;r<e.length&&e[r]!==t;)e[r]===`\\`&&r++,r++;r<e.length&&r++;continue}if(i===`[`)n++;else if(i===`]`&&(n--,n===0))return r;r++}return-1}function Y(e,t){if(e[t]!==`(`)return-1;let n=1,r=t+1;for(;r<e.length;){let t=e.slice(r,r+2);if(t===`//`||t===`/*`){r=J(e,r);continue}let i=e[r]??``;if(i===`'`||i===`"`||i==="`"){let t=i;for(r++;r<e.length&&e[r]!==t;)e[r]===`\\`&&r++,r++;r<e.length&&r++;continue}if(i===`(`)n++;else if(i===`)`&&(n--,n===0))return r;r++}return-1}async function Ie(t){let{name:n,outDir:r}=t,i=E(n),a=w(n),o=[],s=f(r,`${i}.adapter.ts`);return await e(s,`import {
1690
- defineAdapter,
1691
- type AdapterContext,
1692
- type AdapterMiddleware,
1693
- type ContributorRegistrations,
1694
- type Constructor,
1695
- } from '@forinda/kickjs'
1696
-
1697
- /**
1698
- * Configuration for the ${a} adapter.
1699
- *
1700
- * Adapters typically take a small config object so callers can tune
1701
- * behaviour at bootstrap time. Keep the shape narrow — anything
1702
- * derived from the environment should be read inside the build
1703
- * function via getEnv(), not forced onto the caller.
1704
- */
1705
- export interface ${a}AdapterConfig {
1706
- // Add your adapter configuration here, e.g.:
1707
- // enabled?: boolean
1708
- // apiKey?: string
1709
- }
1710
-
1711
- /**
1712
- * ${a} adapter — built via \`defineAdapter()\` so callers get the
1713
- * factory's call / \`.scoped()\` / \`.async()\` surfaces for free.
1714
- *
1715
- * Hooks into the Application lifecycle to add middleware, routes,
1716
- * Context Contributors, or external service connections.
1717
- *
1718
- * Every lifecycle hook below is OPTIONAL. The scaffold emits all of
1719
- * them so adopters can browse what's available and delete what they
1720
- * don't need — \`build()\` returning \`{}\` is also valid for an adapter
1721
- * that only contributes config defaults.
1722
- *
1723
- * @example
1724
- * \`\`\`ts
1725
- * import { bootstrap } from '@forinda/kickjs'
1726
- * import { ${a}Adapter } from './adapters/${i}.adapter'
1727
- *
1728
- * bootstrap({
1729
- * modules,
1730
- * adapters: [${a}Adapter({ /* config overrides *\\/ })],
1731
- * })
1732
- * \`\`\`
1733
- */
1734
- export const ${a}Adapter = defineAdapter<${a}AdapterConfig>({
1735
- name: '${a}Adapter',
1736
- defaults: {
1737
- // Default config values go here. The adopter's overrides shallow-merge
1738
- // on top of these before \`build()\` runs.
1739
- },
1740
- build: (_config, { name: _name }) => {
1741
- // Closures inside \`build()\` are how each adapter instance owns its
1742
- // own state (database client, Map, timer handle, …). The same
1743
- // \`_config\` is visible to every hook below.
1744
-
1745
- return {
1746
- /**
1747
- * Express middleware entries the Application mounts at named phases.
1748
- *
1749
- * \`phase\` controls where each handler sits in the pipeline:
1750
- * 'beforeGlobal' | 'afterGlobal' | 'beforeRoutes' | 'afterRoutes'.
1751
- *
1752
- * \`path\` (optional) scopes the entry to a path prefix.
1753
- *
1754
- * Delete this hook entirely if you don't add middleware.
1755
- */
1756
- middleware(): AdapterMiddleware[] {
1757
- return [
1758
- // Example: add a custom header to all responses
1759
- // {
1760
- // phase: 'beforeGlobal',
1761
- // handler: (_req, res, next) => {
1762
- // res.setHeader('X-${a}', 'true')
1763
- // next()
1764
- // },
1765
- // },
1766
- // Example: scope a rate limiter to one path prefix
1767
- // {
1768
- // phase: 'beforeRoutes',
1769
- // path: '/api/v1/auth',
1770
- // handler: rateLimit({ max: 10 }),
1771
- // },
1772
- ]
1773
- },
1774
-
1775
- /**
1776
- * Runs BEFORE global middleware. Mount routes that should bypass the
1777
- * middleware stack — health checks, docs UI, static assets, OAuth
1778
- * callbacks. Anything you want reachable even if a global middleware
1779
- * later in the chain rejects requests.
1780
- *
1781
- * Delete this hook if you have no early routes.
1782
- */
1783
- beforeMount(_ctx: AdapterContext): void {
1784
- // Example:
1785
- // _ctx.app.get('/${i}/status', (_req, res) => res.json({ status: 'ok' }))
1786
- },
1787
-
1788
- /**
1789
- * Fires once per controller class as the router mounts. Use this to
1790
- * collect route metadata for OpenAPI specs, dependency graphs, route
1791
- * inventories, devtools dashboards.
1792
- *
1793
- * Delete this hook unless your adapter introspects the route registry.
1794
- */
1795
- onRouteMount(_controllerClass: Constructor, _mountPath: string): void {
1796
- // Example (Swagger-style): collect routes for the spec.
1797
- // openApiSpec.addController(_controllerClass, _mountPath)
1798
- },
1799
-
1800
- /**
1801
- * Runs AFTER modules + routes are wired, BEFORE the server starts.
1802
- * Right place for late-stage DI registrations or final config validation.
1803
- *
1804
- * Delete this hook if there's nothing to wire post-modules.
1805
- */
1806
- beforeStart(_ctx: AdapterContext): void {
1807
- // Example: _ctx.container.registerInstance(MY_TOKEN, new MyService(_config))
1808
- },
1809
-
1810
- /**
1811
- * Runs AFTER the HTTP server is listening. The raw \`http.Server\` is
1812
- * available on \`ctx.server\` — attach upgrade handlers (Socket.IO,
1813
- * gRPC, GraphQL subscriptions), warm caches, log a banner.
1814
- *
1815
- * Delete this hook if you don't need the running server reference.
1816
- */
1817
- afterStart(_ctx: AdapterContext): void {
1818
- // Example: const io = new Server(_ctx.server)
1819
- },
1820
-
1821
- /**
1822
- * Returns Context Contributors to merge into every route's pipeline
1823
- * at the \`'adapter'\` precedence level. Per-route handlers can
1824
- * override the value at the method / class / module level.
1825
- *
1826
- * Delete this hook unless your adapter ships typed per-request values
1827
- * (auth user, tenant, locale, feature flags, geo, etc).
1828
- */
1829
- contributors(): ContributorRegistrations {
1830
- return [
1831
- // Example:
1832
- // import { defineHttpContextDecorator } from '@forinda/kickjs'
1833
- // declare module '@forinda/kickjs' { interface ContextMeta { ${i}: { id: string } } }
1834
- // const Load${a} = defineHttpContextDecorator({
1835
- // key: '${i}',
1836
- // resolve: (ctx) => ({ id: ctx.req.headers['x-${i}-id'] as string }),
1837
- // })
1838
- // return [Load${a}.registration]
1839
- ]
1840
- },
1841
-
1842
- /**
1843
- * Runs on graceful shutdown (SIGINT/SIGTERM). Clean up long-lived
1844
- * resources the adapter owns: close connections, flush buffers,
1845
- * cancel timers. The framework runs every adapter's \`shutdown\`
1846
- * concurrently via \`Promise.allSettled\` — one failure won't block
1847
- * sibling adapters.
1848
- *
1849
- * Delete this hook if your adapter holds no resources.
1850
- */
1851
- async shutdown(): Promise<void> {
1852
- // Example: await this.pool.end()
1853
- // Example: clearInterval(this.heartbeatTimer)
1854
- },
1855
- }
1856
- },
1857
- })
1858
- `),o.push(s),o}const Le={controller:`presentation`,service:`domain/services`,dto:`application/dtos`,guard:`presentation/guards`,middleware:`middleware`,contributor:`presentation/contributors`},Re={controller:``,service:``,dto:`dtos`,guard:`guards`,middleware:`middleware`,contributor:`contributors`},ze={controller:``,service:``,dto:`dtos`,guard:`guards`,middleware:`middleware`,contributor:`contributors`,command:`commands`,query:`queries`,event:`events`};function X(e){let{type:t,outDir:n,moduleName:r,modulesDir:i=`src/modules`,defaultDir:a,pattern:o=`ddd`,shouldPluralize:s=!0}=e;if(n)return m(n);if(r){let e=o===`ddd`?Le:o===`cqrs`?ze:Re,n=E(r),a=s?D(n):n,c=e[t]??``,l=f(i,a);return m(c?f(l,c):l)}return m(a)}async function Be(t){let{name:n,moduleName:r,modulesDir:i,pattern:a}=t,o=X({type:`middleware`,outDir:t.outDir,moduleName:r,modulesDir:i,defaultDir:`src/middleware`,pattern:a,shouldPluralize:t.pluralize??!0}),s=E(n),c=T(n),l=[],u=f(o,`${s}.middleware.ts`);return await e(u,`import type { Request, Response, NextFunction } from 'express'
1859
-
1860
- export interface ${w(n)}Options {
1861
- // Add configuration options here. The factory below closes over the
1862
- // resolved options object; pass them at the call site —
1863
- // \`${c}({ foo: 'bar' })\` — and the closure preserves them across
1864
- // every request.
1865
- }
1866
-
1867
- /**
1868
- * ${w(n)} middleware.
1869
- *
1870
- * Usage in bootstrap (fires on every request):
1871
- * middleware: [${c}()]
1872
- *
1873
- * Usage with adapter — phase controls *when* the handler runs:
1874
- *
1875
- * middleware() {
1876
- * return [{ handler: ${c}(), phase: 'afterGlobal' }]
1877
- * }
1878
- *
1879
- * Phase semantics (see \`MiddlewarePhase\` JSDoc for the full contract):
1880
- * - 'beforeGlobal' / 'afterGlobal' / 'beforeRoutes' — fire on every
1881
- * request, before module routes run.
1882
- * - 'afterRoutes' — fires ONLY when no route matched (404 fall-through)
1883
- * OR a route handler called \`next()\` without ending the response.
1884
- * Controllers that call \`ctx.json(…)\` end the chain and skip this
1885
- * phase. For per-response work (logging, metrics) attach to
1886
- * \`res.on('finish', …)\` from an earlier-phase middleware instead.
1887
- *
1888
- * Optional path scope — string, RegExp, or array of either:
1889
- * middleware() {
1890
- * return [{
1891
- * handler: ${c}({ region: 'eu' }),
1892
- * phase: 'afterGlobal',
1893
- * path: ['/api', /^\\/admin/],
1894
- * }]
1895
- * }
1896
- *
1897
- * Usage with @Middleware decorator:
1898
- * @Middleware(${c}())
1899
- */
1900
- export function ${c}(options: ${w(n)}Options = {}) {
1901
- return (req: Request, res: Response, next: NextFunction) => {
1902
- // Implement your middleware logic here. \`options\` is captured by
1903
- // closure — log or read it anywhere in this handler body.
1904
- void options
1905
- next()
1906
- }
1907
- }
1908
- `),l.push(u),l}async function Ve(t){let{name:n,moduleName:r,modulesDir:i,pattern:a}=t,o=X({type:`guard`,outDir:t.outDir,moduleName:r,modulesDir:i,defaultDir:`src/guards`,pattern:a,shouldPluralize:t.pluralize??!0}),s=E(n),c=T(n),l=w(n),u=[],d=f(o,`${s}.guard.ts`);return await e(d,`import { Container, HttpException } from '@forinda/kickjs'
1909
- import type { RequestContext } from '@forinda/kickjs'
1910
-
1911
- /**
1912
- * ${l} guard.
1913
- *
1914
- * Guards protect routes by checking conditions before the handler runs.
1915
- * Return early with an error response to block access.
1916
- *
1917
- * Usage:
1918
- * @Middleware(${c}Guard)
1919
- * @Get('/protected')
1920
- * async handler(ctx: RequestContext) { ... }
1921
- */
1922
- export async function ${c}Guard(ctx: RequestContext, next: () => void): Promise<void> {
1923
- // Example: check for an authorization header
1924
- const header = ctx.headers.authorization
1925
- if (!header?.startsWith('Bearer ')) {
1926
- ctx.res.status(401).json({ message: 'Missing or invalid authorization header' })
1927
- return
1928
- }
1929
-
1930
- const token = header.slice(7)
1931
-
1932
- try {
1933
- // Verify the token using a service from the DI container
1934
- // const container = Container.getInstance()
1935
- // const authService = container.resolve(AuthService)
1936
- // const payload = authService.verifyToken(token)
1937
- // ctx.set('auth', payload)
1938
-
1939
- next()
1940
- } catch {
1941
- ctx.res.status(401).json({ message: 'Invalid or expired token' })
1942
- }
1943
- }
1944
- `),u.push(d),u}async function He(t){let{name:n,moduleName:r,modulesDir:i,pattern:a}=t,o=X({type:`service`,outDir:t.outDir,moduleName:r,modulesDir:i,defaultDir:`src/services`,pattern:a,shouldPluralize:t.pluralize??!0}),s=E(n),c=w(n),l=[],u=f(o,`${s}.service.ts`);return await e(u,`import { Service } from '@forinda/kickjs'
1945
-
1946
- @Service()
1947
- export class ${c}Service {
1948
- // Inject dependencies via constructor
1949
- // constructor(
1950
- // @Inject(MY_REPO) private readonly repo: IMyRepository,
1951
- // ) {}
1952
- }
1953
- `),l.push(u),l}async function Ue(t){let{name:n,moduleName:r,modulesDir:i,pattern:a}=t,o=X({type:`controller`,outDir:t.outDir,moduleName:r,modulesDir:i,defaultDir:`src/controllers`,pattern:a,shouldPluralize:t.pluralize??!0}),s=E(n),c=w(n),l=[],u=f(o,`${s}.controller.ts`);return await e(u,`import { Controller, Get, Post, type Ctx } from '@forinda/kickjs'
1954
-
1955
- // \`Ctx<KickRoutes.${c}Controller['<method>']>\` is generated by
1956
- // \`kick typegen\` (auto-run on \`kick dev\`). After the first run, your IDE
1957
- // will autocomplete \`ctx.params\`, \`ctx.body\`, and \`ctx.query\`.
1958
- // See https://forinda.github.io/kick-js/guide/typegen for details.
1959
-
1960
- @Controller()
1961
- export class ${c}Controller {
1962
- // @Autowired() private readonly myService!: MyService
1963
-
1964
- @Get('/')
1965
- async list(ctx: Ctx<KickRoutes.${c}Controller['list']>) {
1966
- ctx.json({ message: '${c} list' })
1967
- }
1968
-
1969
- @Post('/')
1970
- async create(ctx: Ctx<KickRoutes.${c}Controller['create']>) {
1971
- ctx.created({ message: '${c} created', data: ctx.body })
1972
- }
1973
- }
1974
- `),l.push(u),l}async function We(t){let{name:n,moduleName:r,modulesDir:i,pattern:a}=t,o=X({type:`dto`,outDir:t.outDir,moduleName:r,modulesDir:i,defaultDir:`src/dtos`,pattern:a,shouldPluralize:t.pluralize??!0}),s=E(n),c=w(n),l=T(n),u=[],d=f(o,`${s}.dto.ts`);return await e(d,`import { z } from 'zod'
1975
-
1976
- export const ${l}Schema = z.object({
1977
- // Define your schema fields here
1978
- name: z.string().min(1).max(200),
1979
- })
1980
-
1981
- export type ${c}DTO = z.infer<typeof ${l}Schema>
1982
- `),u.push(d),u}const Ge={swagger:`@forinda/kickjs-swagger`,ws:`@forinda/kickjs-ws`,queue:`@forinda/kickjs-queue`,devtools:`@forinda/kickjs-devtools`},Ke={zod:{name:`zod`,range:`^4.3.6`},valibot:{name:`valibot`,range:`^1.4.1`},yup:{name:`yup`,range:`^1.7.1`}};function Z(e,t){let n=e[t];if(!n)throw Error(`generatePackageJson: missing resolved version for ${t}. Add it to SIBLING_PACKAGES in generators/project.ts.`);return n}function qe(e,t,n,r=[],i=`zod`){let a=Ke[i],o={"@forinda/kickjs":Z(n,`@forinda/kickjs`),"@forinda/kickjs-schema":Z(n,`@forinda/kickjs-schema`),dotenv:`^17.3.1`,express:`^5.1.0`,"reflect-metadata":`^0.2.2`,[a.name]:a.range};for(let e of r){let t=Ge[e];t&&!o[t]&&(o[t]=Z(n,t))}return JSON.stringify({name:e,version:`0.0.0`,type:`module`,scripts:{dev:`vite`,"dev:debug":`kick dev:debug`,build:`kick build`,start:`kick start`,test:`vitest run`,"test:watch":`vitest`,typecheck:`tsc --noEmit`,typegen:`kick typegen`,lint:`eslint src/`,format:`prettier --write src/`},dependencies:o,devDependencies:{"@forinda/kickjs-cli":Z(n,`@forinda/kickjs-cli`),"@forinda/kickjs-vite":Z(n,`@forinda/kickjs-vite`),"@swc/core":`^1.15.21`,"@types/express":`^5.0.6`,"@types/node":`^25.0.0`,"unplugin-swc":`^1.5.9`,vite:`^8.0.3`,vitest:`^4.1.2`,typescript:`^6.0.3`,prettier:`^3.8.1`}},null,2)}function Je(){return`import { defineConfig } from 'vite'
1983
- import { resolve } from 'node:path'
1984
- import swc from 'unplugin-swc'
1985
- import { kickjsVitePlugin, envWatchPlugin } from '@forinda/kickjs-vite'
1986
-
1987
- export default defineConfig({
1988
- oxc: false,
1989
- plugins: [
1990
- swc.vite(),
1991
- kickjsVitePlugin({ entry: 'src/index.ts' }),
1992
- // Watches .env files and triggers a full reload on change so the
1993
- // dev server picks up env tweaks without a manual restart.
1994
- envWatchPlugin(),
1995
- ],
1996
- resolve: {
1997
- alias: {
1998
- '@': resolve(__dirname, 'src'),
1999
- },
2000
- },
2001
- build: {
2002
- target: 'node20',
2003
- ssr: true,
2004
- outDir: 'dist',
2005
- sourcemap: true,
2006
- rollupOptions: {
2007
- input: resolve(__dirname, 'src/index.ts'),
2008
- output: { format: 'esm' },
2009
- },
2010
- },
2011
- })
2012
- `}function Ye(){return JSON.stringify({compilerOptions:{target:`ES2022`,module:`ESNext`,moduleResolution:`bundler`,lib:[`ES2022`],types:[`node`,`vite/client`],strict:!0,esModuleInterop:!0,skipLibCheck:!0,sourceMap:!0,declaration:!0,experimentalDecorators:!0,emitDecoratorMetadata:!0,outDir:`dist`,paths:{"@/*":[`./src/*`]}},include:[`src`,`.kickjs/types/**/*.d.ts`,`.kickjs/types/**/*.ts`]},null,2)}function Xe(){return JSON.stringify({semi:!1,singleQuote:!0,trailingComma:`all`,printWidth:100,tabWidth:2},null,2)}function Ze(){return`# https://editorconfig.org
2013
- root = true
2014
-
2015
- [*]
2016
- indent_style = space
2017
- indent_size = 2
2018
- end_of_line = lf
2019
- charset = utf-8
2020
- trim_trailing_whitespace = true
2021
- insert_final_newline = true
2022
-
2023
- [*.md]
2024
- trim_trailing_whitespace = false
2025
- `}function Qe(){return`node_modules/
2026
- dist/
2027
- .env
2028
- coverage/
2029
- .DS_Store
2030
- *.tsbuildinfo
2031
- .kickjs/
2032
- `}function $e(){return`# Auto-detect text files and normalise line endings to LF
2033
- * text=auto eol=lf
2034
-
2035
- # Explicitly mark generated / binary files
2036
- *.png binary
2037
- *.jpg binary
2038
- *.jpeg binary
2039
- *.gif binary
2040
- *.ico binary
2041
- *.woff binary
2042
- *.woff2 binary
2043
- *.ttf binary
2044
- *.eot binary
2045
-
2046
- # Lock files — treat as generated
2047
- pnpm-lock.yaml -diff linguist-generated
2048
- yarn.lock -diff linguist-generated
2049
- package-lock.json -diff linguist-generated
2050
- `}function et(){return`PORT=3000
2051
- NODE_ENV=development
2052
- `}function tt(){return`PORT=3000
2053
- NODE_ENV=development
2054
- `}function nt(){return`import { defineConfig } from 'vitest/config'
2055
- import swc from 'unplugin-swc'
2056
-
2057
- export default defineConfig({
2058
- plugins: [swc.vite()],
2059
- test: {
2060
- globals: true,
2061
- environment: 'node',
2062
- include: ['src/**/*.test.ts'],
2063
- },
2064
- })
2065
- `}const rt=d(ee(import.meta.url)),Q=JSON.parse(g(f(rt,`..`,`package.json`),`utf-8`)),it=`^${Q.version}`,at=[`@forinda/kickjs`,`@forinda/kickjs-cli`,`@forinda/kickjs-schema`,`@forinda/kickjs-vite`,`@forinda/kickjs-swagger`,`@forinda/kickjs-ws`,`@forinda/kickjs-queue`,`@forinda/kickjs-devtools`,`@forinda/kickjs-testing`];async function ot(){let e=await Promise.all(at.map(async e=>{try{let t=S(`npm`,[`view`,e,`version`],{encoding:`utf-8`,timeout:5e3,stdio:[`ignore`,`pipe`,`ignore`]}).toString().trim();if(t&&/^\d+\.\d+\.\d+/.test(t))return[e,`^${t}`]}catch{}return[e,it]}));return Object.fromEntries(e)}async function st(t){let{name:n,directory:r,packageManager:i=`pnpm`,template:o=`rest`,defaultRepo:s=`inmemory`,packages:c=[],schemaLib:l=`zod`}=t,u=r,d=e=>console.log(` ${e}`);console.log(`\n Creating KickJS project: ${n}\n`),d(`Resolving package versions...`);let p=await ot();await e(f(u,`package.json`),qe(n,o,p,c,l)),await e(f(u,`vite.config.ts`),Je()),await e(f(u,`tsconfig.json`),Ye()),await e(f(u,`.prettierrc`),Xe()),await e(f(u,`.editorconfig`),Ze()),await e(f(u,`.gitignore`),Qe()),await e(f(u,`.gitattributes`),$e()),await e(f(u,`.env`),et()),await e(f(u,`.env.example`),tt()),await e(f(u,`src/config/index.ts`),Ce(l)),await e(f(u,`src/index.ts`),xe(n,o,Q.version,c)),await e(f(u,`src/modules/index.ts`),Se()),await e(f(u,`src/modules/hello/hello.service.ts`),we()),await e(f(u,`src/modules/hello/hello.controller.ts`),Te()),await e(f(u,`src/modules/hello/hello.module.ts`),Ee()),await e(f(u,`kick.config.ts`),De(o,s,i)),await e(f(u,`vitest.config.ts`),nt()),await e(f(u,`README.md`),a(n,o,i));let{generateAgentDocs:m}=await import(`./agent-docs-BuCTT-9g.mjs`).then(e=>e.t);if(await m({outDir:u,name:n,pm:i,template:o,only:`all`,force:!0}),t.installDeps){console.log(`\n Installing dependencies with ${i}...\n`);try{C(`${i} install`,{cwd:u,stdio:`inherit`}),console.log(`
2066
- Dependencies installed successfully!`)}catch{console.log(`\n Warning: ${i} install failed. Run it manually.`)}}try{let{runTypegen:e}=await import(`./typegen-CSLwAvgI.mjs`).then(e=>e.n);await e({cwd:u,allowDuplicates:!0,silent:!0})}catch{}if(t.initGit)try{C(`git init`,{cwd:u,stdio:`pipe`}),C(`git branch -M main`,{cwd:u,stdio:`pipe`}),C(`git add -A`,{cwd:u,stdio:`pipe`}),C(`git commit -m "chore: initial commit from kick new"`,{cwd:u,stdio:`pipe`}),d(`Git repository initialized`)}catch{d(`Warning: git init failed (git may not be installed)`)}console.log(`
2067
- Project scaffolded successfully!`),console.log();let h=u!==process.cwd();d(`Next steps:`),h&&d(` cd ${n}`),t.installDeps||d(` ${i} install`);let g={rest:`kick g module user`,ddd:`kick g module user --repo drizzle`,cqrs:`kick g module user --pattern cqrs`,minimal:`# add your routes to src/index.ts`};d(` ${g[o]??g.rest}`),d(` kick dev`),d(``),d(`Commands:`),d(` kick dev Start dev server with Vite HMR`),d(` kick build Production build via Vite`),d(` kick start Run production build`),d(``),d(`Generators:`),d(` kick g module <name> Full DDD module (controller, DTOs, use-cases, repo)`),d(` kick g scaffold <n> <f..> CRUD module from field definitions`),d(` kick g controller <name> Standalone controller`),d(` kick g service <name> @Service() class`),d(` kick g middleware <name> Express middleware`),d(` kick g guard <name> Route guard (auth, roles, etc.)`),d(` kick g adapter <name> AppAdapter with lifecycle hooks`),d(` kick g dto <name> Zod DTO schema`),o===`cqrs`&&d(` kick g job <name> Queue job processor`),d(` kick g config Generate kick.config.ts`),d(``),d(`Add packages:`),d(` kick add <pkg> Install a KickJS package + peers`),d(` kick add --list Show all available packages`),d(``),d(`Available: auth, swagger, drizzle, prisma, ws, queue, devtools, mcp, testing`),d(``)}function ct(e){return e}function lt(e){return E(e).replace(/-/g,`_`)}function ut(e){let t=e.cwd??process.cwd(),n=e.projectRoot??l(t),r=e.pluralize??!0,i=w(e.name),a=T(e.name),o=E(e.name),s=lt(e.name),c={name:e.name,pascal:i,camel:a,kebab:o,snake:s,modulesDir:e.modulesDir??`src/modules`,cwd:t,projectRoot:n,args:e.args??[],flags:e.flags??{}};if(r){let e=D(o);c.pluralKebab=e,c.pluralPascal=w(e),c.pluralCamel=T(e)}return c}function dt(e,t){return m(e.cwd,t)}async function ft(e){return import(te(e).href)}const pt=new Map;async function mt(e){let t=pt.get(e);if(t)return t;let n=ht(e);return pt.set(e,n),n}async function ht(e){let t=m(e,`package.json`);if(!h(t))return{generators:[],loaded:[],failed:[]};let n=gt(JSON.parse(await y(t,`utf-8`))),r=u(m(e,`package.json`)),i=[],a=[],o=[];for(let e of n){let t;try{t=r.resolve(`${e}/package.json`)}catch{continue}let n;try{n=JSON.parse(await y(t,`utf-8`))}catch(t){o.push({source:e,reason:`failed to parse package.json: ${t}`});continue}if(!n.kickjs?.generators)continue;let s=n.kickjs.generators,c=m(d(t),s);if(!h(c)){o.push({source:e,reason:`kickjs.generators points to missing file: ${s}`});continue}let l;try{l=await ft(c)}catch(t){o.push({source:e,reason:`failed to import manifest: ${t}`});continue}let u=l.default;if(!Array.isArray(u)){o.push({source:e,reason:`manifest's default export is not an array of GeneratorSpec`});continue}for(let t of u){if(!_t(t)){o.push({source:e,reason:`manifest entry is not a valid GeneratorSpec (missing name/files)`});continue}i.push({source:e,spec:t})}a.push(e)}return{generators:i,loaded:a,failed:o}}function gt(e){let t=new Set;for(let n of[e.dependencies,e.devDependencies,e.peerDependencies])if(n)for(let e of Object.keys(n))t.add(e);return Array.from(t)}function _t(e){if(!e||typeof e!=`object`)return!1;let t=e;return typeof t.name==`string`&&typeof t.files==`function`}async function vt(e,t=[]){let n=e.cwd??process.cwd(),r=t.find(t=>t.spec.name===e.generatorName);if(r)return xt(r.spec,r.source,e,n);let i=bt(await mt(n),e.generatorName);return i?xt(i.spec,i.source,e,n):null}async function yt(e,t=[]){let n=await mt(e),r=new Set(t.map(e=>e.spec.name)),i=n.generators.filter(e=>!r.has(e.spec.name));return{generators:[...t,...i],loaded:n.loaded,failed:n.failed}}function bt(e,t){return e.generators.find(e=>e.spec.name===t)}async function xt(t,n,r,i){let a=ut({name:r.itemName,args:r.args,flags:r.flags,modulesDir:r.modulesDir,pluralize:r.pluralize,cwd:i,projectRoot:r.projectRoot}),o=await t.files(a),s=[];for(let t of o){let n=dt(a,t.path);await e(n,t.content),s.push(n)}return{files:s,source:n}}function St(e){return e}function Ct(e){return e}function wt(e){try{return JSON.parse(g(e,`utf-8`))}catch{return null}}function $(e){try{return g(e,`utf-8`)}catch{return null}}function Tt(e){let t=$(f(e,`tsconfig.json`));if(!t)return null;let n=t.replace(/\/\*[\s\S]*?\*\//g,``).replace(/\/\/.*$/gm,``),r;try{r=JSON.parse(n)}catch{return null}if(typeof r?.extends==`string`){let t=Et(e,r.extends);if(t){let e=wt(t)??{};r.compilerOptions={...e.compilerOptions,...r.compilerOptions}}}return r}function Et(e,t){if(t.startsWith(`.`)){let n=m(e,t);return h(n)?n:null}let n=f(e,`node_modules`,t);return h(n)?n:null}function Dt(e){return e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`)}function Ot(){let e=process.version,t=Number.parseInt(e.replace(/^v/,``).split(`.`)[0],10);return Number.isNaN(t)||t<20?{name:`Node version`,status:`fail`,message:e,fix:`KickJS requires Node 20 or newer.
2068
- Install a supported version via nvm / fnm / volta.`}:{name:`Node version`,status:`pass`,message:e}}function kt(e){if(!e.pkg)return{name:`@forinda/kickjs installed`,status:`warn`,message:`no package.json`};let t={...e.pkg.dependencies,...e.pkg.peerDependencies};return t[`@forinda/kickjs`]?{name:`@forinda/kickjs installed`,status:`pass`,message:t[`@forinda/kickjs`]}:{name:`@forinda/kickjs installed`,status:`fail`,fix:"This directory does not look like a KickJS project — `@forinda/kickjs` is not in your package.json. Run `kick doctor` from the project root, or scaffold a fresh project with `kick new <name>`."}}function At(e){if(!e.pkg)return null;let t={...e.pkg.dependencies,...e.pkg.peerDependencies};return t[`@forinda/kickjs`]&&!t.express?{name:`express installed`,status:`fail`,fix:"`@forinda/kickjs` declares `express` as a required peer dependency, but your package.json does not include it. Install: pnpm add express"}:t.express?{name:`express installed`,status:`pass`,message:t.express}:null}function jt(e){if(!e.pkg)return{name:`reflect-metadata installed`,status:`warn`,message:`no package.json`};let t={...e.pkg.dependencies,...e.pkg.peerDependencies,...e.pkg.devDependencies};return t[`reflect-metadata`]?{name:`reflect-metadata installed`,status:`pass`,message:t[`reflect-metadata`]}:{name:`reflect-metadata installed`,status:`fail`,fix:`KickJS decorators require the reflect-metadata polyfill.
2069
- Install it: pnpm add reflect-metadata
2070
- Then import it at the top of src/index.ts:
2071
-
2072
- import 'reflect-metadata'
2073
- // ... rest of bootstrap`}}function Mt(e){if(!e.tsconfig)return[{name:`tsconfig.json present`,status:`fail`,fix:"Create a tsconfig.json with `experimentalDecorators: true` and `emitDecoratorMetadata: true`. `kick new` scaffolds one automatically."}];let t=e.tsconfig.compilerOptions??{},n=[];return n.push(t.experimentalDecorators===!0?{name:`tsconfig: experimentalDecorators`,status:`pass`}:{name:`tsconfig: experimentalDecorators`,status:`fail`,fix:'Add `"experimentalDecorators": true` to compilerOptions in tsconfig.json. Without it, @Service / @Controller / @Get etc. don\'t register any metadata at compile time.'}),n.push(t.emitDecoratorMetadata===!0?{name:`tsconfig: emitDecoratorMetadata`,status:`pass`}:{name:`tsconfig: emitDecoratorMetadata`,status:`fail`,fix:'Add `"emitDecoratorMetadata": true` to compilerOptions in tsconfig.json. The DI container uses this metadata for constructor-parameter injection.'}),n}function Nt(e){let t=[`src/env.ts`,`src/env/index.ts`,`src/config/env.ts`,`src/config/index.ts`].map(t=>f(e.cwd,t)).filter(e=>h(e)).filter(e=>/\bloadEnv\s*\(/.test($(e)??``));if(t.length===0)return null;let n=[`src/index.ts`,`src/main.ts`].map(t=>f(e.cwd,t)).find(e=>h(e));if(!n)return{name:`env wiring`,status:`warn`,message:`env-init file exists but no src/index.ts or src/main.ts found`};let r=$(n)??``,i=d(n),a=[];for(let e of t){let t=p(i,e).replace(/\\/g,`/`).replace(/\.ts$/,``),n=t.startsWith(`.`)?t:`./`+t,r=n.replace(/\/index$/,``);a.push(n,r);let o=e.replace(/\\/g,`/`).match(/\/src\/(.+?)(?:\.ts)?$/);if(o){let e=`@/`+o[1],t=e.replace(/\/index$/,``);a.push(e,t)}}let o=-1;for(let e of new Set(a)){let t=RegExp(`^import\\s+(?:.*?from\\s+)?['"]${Dt(e)}['"]`,`m`),n=r.match(t);n&&n.index!==void 0&&(o===-1||n.index<o)&&(o=n.index)}let s=r.search(/\bbootstrap\s*\(/),c=t.map(t=>p(e.cwd,t).replace(/\\/g,`/`)).join(`, `);return o===-1?{name:`env wiring`,status:`fail`,message:c,fix:`An env-init file (${c}) calls \`loadEnv(...)\` but \`${p(e.cwd,n).replace(/\\/g,`/`)}\` doesn't import it.\nWithout this, ConfigService.get('X') returns undefined while @Value('X') works via process.env fallback — a half-broken config you won't notice until something is missing.\n\nFix: add a side-effect import at the top of ${p(e.cwd,n).replace(/\\/g,`/`)} (above bootstrap()), pointing at one of the detected files. For example:\n\n import './env'\n // or\n import './config'\n // or, with the @/ alias:\n import '@/config/env'`}:s!==-1&&o>s?{name:`env wiring`,status:`warn`,message:`env-init imported AFTER bootstrap() — should be before`,fix:`Move the env import above the bootstrap() call so the schema runs before any service reads from ConfigService.`}:{name:`env wiring`,status:`pass`}}function Pt(e,t=Ft){let n=0,r=0,i=[e];for(;i.length>0&&r<t;){let e=i.pop(),a;try{a=_(e,{withFileTypes:!0})}catch{continue}for(let o of a){if(r>=t)break;r++;let a=f(e,o.name);if(o.isDirectory()){i.push(a);continue}try{let e=v(a).mtimeMs;e>n&&(n=e)}catch{}}}return n}const Ft=2e3;function It(e){let t=f(e.cwd,`.kickjs`,`types`);if(!h(t))return null;let n=Pt(t);if(n===0)return null;let r=Date.now()-n,i=Math.floor(r/6e4);return i>60?{name:`typegen freshness`,status:`warn`,message:`last updated ${i} minutes ago`,fix:"Re-run `kick typegen` (or `kick dev`, which runs it on every reload) so generated types match the current code."}:{name:`typegen freshness`,status:`pass`,message:i===0?`just now`:`${i}m ago`}}const Lt=[()=>Ot(),kt,At,jt,Mt,Nt,It];async function Rt(e,t={}){let n={cwd:e,pkg:wt(f(e,`package.json`)),tsconfig:Tt(e)},r=[...Lt,...t.extraChecks??[]],i=[];for(let e of r){let t;try{t=await e(n)}catch(t){i.push({name:e.name||`doctor check`,status:`fail`,message:t instanceof Error?t.message:String(t)});continue}t!=null&&(Array.isArray(t)?i.push(...t):i.push(t))}return i}function zt(e){switch(e){case`pass`:return r.green(`✔`);case`warn`:return r.yellow(`⚠`);case`fail`:return r.red(`✖`)}}function Bt(e){let t=zt(e.status),n=e.message?` ${r.dim(`(${e.message})`)}`:``;return`${t} ${e.name}${n}`}function Vt(e){return e.split(`
2074
- `).map(e=>` ${r.dim(`→`)} ${e}`).join(`
2075
- `)}function Ht(e){return e?.doctor?.checks??[]}function Ut(e){e.command(`doctor`).description(`Pre-flight checks for your KickJS project (dev environment health)`).action(async()=>{let e=process.cwd(),a=Ht(await c(e));t(`KickJS Doctor`);let o=await Rt(e,{extraChecks:a});for(let e of o)i.message(Bt(e)),e.fix&&e.status!==`pass`&&i.message(Vt(e.fix));let s=o.filter(e=>e.status===`pass`).length,l=o.filter(e=>e.status===`warn`).length,u=o.filter(e=>e.status===`fail`).length,d=[r.green(`${s} passed`),l>0?r.yellow(`${l} warning${l===1?``:`s`}`):`${l} warnings`,u>0?r.red(`${u} error${u===1?``:`s`}`):`${u} errors`].join(`, `);u>0?(n(`${d} — fix the errors above before running the app`),process.exit(1)):n(l>0?`${d} — review the warnings`:r.green(`${d} — your environment looks good`))})}export{T as C,O as S,w as T,q as _,vt as a,k as b,st as c,He as d,Ve as f,G as g,Ie as h,yt as i,We as l,X as m,St as n,ut as o,Be as p,Ut as r,ct as s,Ct as t,Ue as u,Ne as v,E as w,D as x,Me as y};
2076
- //# sourceMappingURL=doctor-Cin9GYv9.mjs.map