@forinda/kickjs-cli 6.4.0 → 6.6.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 (41) hide show
  1. package/dist/agent-docs-CqRWMHZ-.mjs +12 -0
  2. package/dist/agent-docs-CqRWMHZ-.mjs.map +1 -0
  3. package/dist/{build-CBRWV98i.mjs → build-C6c_h-27.mjs} +3 -3
  4. package/dist/{build-CBRWV98i.mjs.map → build-C6c_h-27.mjs.map} +1 -1
  5. package/dist/{builtins-1-t8nFZe.mjs → builtins-BvKA9kzi.mjs} +2 -2
  6. package/dist/cli.mjs +60 -24
  7. package/dist/{config-C6Na808n.mjs → config-BOArOJm-.mjs} +3 -3
  8. package/dist/{config-C6Na808n.mjs.map → config-BOArOJm-.mjs.map} +1 -1
  9. package/dist/doctor-BJWjI_wt.mjs +860 -0
  10. package/dist/doctor-BJWjI_wt.mjs.map +1 -0
  11. package/dist/fullstack-BgwfRTCN.mjs +138 -0
  12. package/dist/fullstack-BgwfRTCN.mjs.map +1 -0
  13. package/dist/fullstack-CHO-Us7Y.mjs +137 -0
  14. package/dist/index.d.mts +7 -0
  15. package/dist/index.d.mts.map +1 -1
  16. package/dist/index.mjs +2 -2
  17. package/dist/index.mjs.map +1 -1
  18. package/dist/{plugin-DlIRWEfP.mjs → plugin-IyD9yXBm.mjs} +3 -3
  19. package/dist/{plugin-DlIRWEfP.mjs.map → plugin-IyD9yXBm.mjs.map} +1 -1
  20. package/dist/project-QWNGOwFX.mjs +390 -0
  21. package/dist/project-QWNGOwFX.mjs.map +1 -0
  22. package/dist/{project-docs-Dr75Phev.mjs → project-docs-BSNKPfLT.mjs} +37 -11
  23. package/dist/project-docs-BSNKPfLT.mjs.map +1 -0
  24. package/dist/{project-root-BKYv-vPf.mjs → project-root-2PIpqSzM.mjs} +3 -3
  25. package/dist/{project-root-BKYv-vPf.mjs.map → project-root-2PIpqSzM.mjs.map} +1 -1
  26. package/dist/prompts-CeLdg6XF.mjs +12 -0
  27. package/dist/prompts-CeLdg6XF.mjs.map +1 -0
  28. package/dist/{rolldown-runtime-DLwMHNw-.mjs → rolldown-runtime-CCuSuZBc.mjs} +1 -1
  29. package/dist/{run-plugins-DaN0MHlX.mjs → run-plugins-B-_RJ-JV.mjs} +60 -54
  30. package/dist/run-plugins-B-_RJ-JV.mjs.map +1 -0
  31. package/dist/{typegen-DJGvcWVf.mjs → typegen-BTfDyerr.mjs} +17 -17
  32. package/dist/typegen-BTfDyerr.mjs.map +1 -0
  33. package/dist/{types-CmiWgylI.mjs → types-SeFJ8umm.mjs} +1 -1
  34. package/package.json +2 -2
  35. package/dist/agent-docs-lDM1Nx6J.mjs +0 -12
  36. package/dist/agent-docs-lDM1Nx6J.mjs.map +0 -1
  37. package/dist/doctor-DlWMC_2O.mjs +0 -1234
  38. package/dist/doctor-DlWMC_2O.mjs.map +0 -1
  39. package/dist/project-docs-Dr75Phev.mjs.map +0 -1
  40. package/dist/run-plugins-DaN0MHlX.mjs.map +0 -1
  41. package/dist/typegen-DJGvcWVf.mjs.map +0 -1
@@ -0,0 +1,860 @@
1
+ /**
2
+ * @forinda/kickjs-cli v6.6.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{l as e,s as t}from"./project-docs-BSNKPfLT.mjs";import{a as n,n as r,r as i,t as a,u as o}from"./prompts-CeLdg6XF.mjs";import{i as s,s as c}from"./project-QWNGOwFX.mjs";import{i as l}from"./config-BOArOJm-.mjs";import{t as u}from"./project-root-2PIpqSzM.mjs";import{createRequire as d}from"node:module";import f,{dirname as p,join as m,relative as h,resolve as g}from"node:path";import{existsSync as _,readFileSync as v,readdirSync as y,statSync as b}from"node:fs";import{readFile as x,writeFile as S}from"node:fs/promises";import C from"pluralize";import{pathToFileURL as ee}from"node:url";import{defineGenerator as te}from"@forinda/kickjs-cli-kit";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 C.plural(e)}function O(e){return C.plural(e)}function k(e){return e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`)}function A(e){return e.charAt(0).toUpperCase()+e.slice(1).replace(/-([a-z])/g,(e,t)=>t.toUpperCase())}function ne(e){return e.replace(/([a-z])([A-Z])/g,`$1-$2`).toLowerCase()}function re(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]??`${ne(n)}-${t}`}}function j(e){return e??`define`}function M(e){let{pascal:t,kebab:n,plural:r=``,repo:i,style:a}=e,{repoClass:o,repoFile:s}=re(t,n,i),c=j(a),l=`/**
12
+ * ${t} Module
13
+ *
14
+ * REST module with a flat folder structure.
15
+ * Controller delegates to service, service wraps the repository.
16
+ *
17
+ * Structure:
18
+ * ${n}.controller.ts — HTTP routes (CRUD)
19
+ * ${n}.service.ts — Business logic
20
+ * ${n}.repository.ts — Repository interface
21
+ * ${s}.repository.ts — Repository implementation
22
+ * dtos/ — Request/response schemas
23
+ */`,u=`import { ${t.toUpperCase()}_REPOSITORY } from './${n}.repository'
24
+ import { ${o} } from './${s}.repository'
25
+ import { ${t}Controller } from './${n}.controller'
26
+
27
+ // Eagerly load decorated classes so @Controller()/@Service()/@Repository() decorators
28
+ // register in the DI container. Recursive globs (./**/) so the module keeps working
29
+ // however you nest files (e.g. moving controllers into a controllers/ sub-folder).
30
+ import.meta.glob(
31
+ ['./**/*.controller.ts', './**/*.service.ts', './**/*.repository.ts', '!./**/*.test.ts'],
32
+ { eager: true },
33
+ )`,d=` /**
34
+ * Declare HTTP routes for this module. Return value shape:
35
+ *
36
+ * - \`path\` — URL prefix for this route set.
37
+ * - \`controller\` — Controller class (also drives OpenAPI).
38
+ * - \`version\` — Optional. Overrides the app-wide API version.
39
+ *
40
+ * Return an **array** to mount multiple route sets — admin
41
+ * surfaces, side-by-side v1 + v2 controllers, etc:
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
+ register(container: Container): void {
53
+ container.registerFactory(${t.toUpperCase()}_REPOSITORY, () =>
54
+ container.resolve(${o}),
55
+ )
56
+ }
57
+
58
+ ${d.replace(/^ {4}/gm,` `).replace(/^ {6}/gm,` `)}
59
+ routes(): ModuleRoutes {
60
+ return {
61
+ path: '/${r}',
62
+ controller: ${t}Controller,
63
+ }
64
+ }
65
+ }
66
+ `:`${l}
67
+ import { defineModule } from '@forinda/kickjs'
68
+ ${u}
69
+
70
+ export const ${t}Module = defineModule({
71
+ name: '${t}Module',
72
+ build: () => ({
73
+ register(container) {
74
+ container.registerFactory(${t.toUpperCase()}_REPOSITORY, () =>
75
+ container.resolve(${o}),
76
+ )
77
+ },
78
+
79
+ ${d}
80
+ routes() {
81
+ return {
82
+ path: '/${r}',
83
+ controller: ${t}Controller,
84
+ }
85
+ },
86
+ }),
87
+ })
88
+ `}function ie(e){let{pascal:t,kebab:n,plural:r=``,style:i}=e,a=j(i),o=` /**
89
+ * Declare HTTP routes. Return value shape:
90
+ *
91
+ * - \`path\` — URL prefix for this route set.
92
+ * - \`controller\` — Controller class (also drives OpenAPI).
93
+ * - \`version\` — Optional. Overrides the app-wide API version.
94
+ *
95
+ * Return an array to mount multiple route sets:
96
+ *
97
+ * return [
98
+ * { path: '/${r}', version: 1, controller: ${t}V1Controller },
99
+ * { path: '/${r}', version: 2, controller: ${t}V2Controller },
100
+ * ]
101
+ */`;return a===`class`?`import { type AppModule, type ModuleRoutes } from '@forinda/kickjs'
102
+ import { ${t}Controller } from './${n}.controller'
103
+
104
+ export class ${t}Module implements AppModule {
105
+ ${o.replace(/^ {4}/gm,` `).replace(/^ {6}/gm,` `)}
106
+ routes(): ModuleRoutes {
107
+ return {
108
+ path: '/${r}',
109
+ controller: ${t}Controller,
110
+ }
111
+ }
112
+ }
113
+ `:`import { defineModule } from '@forinda/kickjs'
114
+ import { ${t}Controller } from './${n}.controller'
115
+
116
+ export const ${t}Module = defineModule({
117
+ name: '${t}Module',
118
+ build: () => ({
119
+ ${o}
120
+ routes() {
121
+ return {
122
+ path: '/${r}',
123
+ controller: ${t}Controller,
124
+ }
125
+ },
126
+ }),
127
+ })
128
+ `}function N(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'
129
+ import { ApiTags } from '@forinda/kickjs-swagger'
130
+ import { ${t}Service } from './${n}.service'
131
+ import { create${t}Schema } from './dtos/create-${n}.dto'
132
+ import { update${t}Schema } from './dtos/update-${n}.dto'
133
+ import { ${t.toUpperCase()}_QUERY_CONFIG } from './${n}.constants'
134
+
135
+ // Each handler annotates its \`ctx\` with \`Ctx<KickRoutes.${t}Controller['<method>']>\`
136
+ // so \`ctx.params\`, \`ctx.body\`, and \`ctx.query\` are typed end-to-end.
137
+ // The \`KickRoutes\` namespace is generated by \`kick typegen\` (auto-run on
138
+ // \`kick dev\`) — see https://kickjs.app/guide/typegen.
139
+
140
+ @Controller()
141
+ export class ${t}Controller {
142
+ @Autowired() private readonly ${r}Service!: ${t}Service
143
+
144
+ @Get('/')
145
+ @ApiTags('${t}')
146
+ @ApiQueryParams(${t.toUpperCase()}_QUERY_CONFIG)
147
+ async list(ctx: Ctx<KickRoutes.${t}Controller['list']>) {
148
+ return ctx.paginate(
149
+ (parsed) => this.${r}Service.findPaginated(parsed),
150
+ ${t.toUpperCase()}_QUERY_CONFIG,
151
+ )
152
+ }
153
+
154
+ @Get('/:id')
155
+ @ApiTags('${t}')
156
+ async getById(ctx: Ctx<KickRoutes.${t}Controller['getById']>) {
157
+ const result = await this.${r}Service.findById(ctx.params.id)
158
+ if (!result) return ctx.notFound('${t} not found')
159
+ ctx.json(result)
160
+ }
161
+
162
+ @Post('/', { body: create${t}Schema, name: 'Create${t}' })
163
+ @ApiTags('${t}')
164
+ async create(ctx: Ctx<KickRoutes.${t}Controller['create']>) {
165
+ const result = await this.${r}Service.create(ctx.body)
166
+ ctx.created(result)
167
+ }
168
+
169
+ @Put('/:id', { body: update${t}Schema, name: 'Update${t}' })
170
+ @ApiTags('${t}')
171
+ async update(ctx: Ctx<KickRoutes.${t}Controller['update']>) {
172
+ const result = await this.${r}Service.update(ctx.params.id, ctx.body)
173
+ ctx.json(result)
174
+ }
175
+
176
+ @Delete('/:id')
177
+ @ApiTags('${t}')
178
+ async remove(ctx: Ctx<KickRoutes.${t}Controller['remove']>) {
179
+ await this.${r}Service.delete(ctx.params.id)
180
+ ctx.noContent()
181
+ }
182
+ }
183
+ `}function P(e){let{pascal:t}=e;return`import { z } from 'zod'
184
+
185
+ /**
186
+ * Create ${t} DTO — Zod schema for validating POST request bodies.
187
+ * This schema is passed to @Post('/', { body: create${t}Schema }) for automatic validation.
188
+ * It also generates OpenAPI request body docs when SwaggerAdapter is used.
189
+ *
190
+ * Add more fields as needed. Supported Zod types:
191
+ * z.string(), z.number(), z.boolean(), z.enum([...]),
192
+ * z.array(), z.object(), .optional(), .default(), .transform()
193
+ */
194
+ export const create${t}Schema = z.object({
195
+ name: z.string().min(1, 'Name is required').max(200),
196
+ })
197
+
198
+ export type Create${t}DTO = z.infer<typeof create${t}Schema>
199
+ `}function ae(e){let{pascal:t}=e;return`import { z } from 'zod'
200
+
201
+ export const update${t}Schema = z.object({
202
+ name: z.string().min(1).max(200).optional(),
203
+ })
204
+
205
+ export type Update${t}DTO = z.infer<typeof update${t}Schema>
206
+ `}function oe(e){let{pascal:t}=e;return`export interface ${t}ResponseDTO {
207
+ id: string
208
+ name: string
209
+ createdAt: string
210
+ updatedAt: string
211
+ }
212
+ `}function F(e){let{pascal:t,kebab:n,dtoPrefix:r=`../../application/dtos`,tokenScope:i=`app`}=e;return`/**
213
+ * ${t} Repository Interface
214
+ *
215
+ * Defines the contract for data access.
216
+ * The interface declares what operations are available;
217
+ * implementations (in-memory, Drizzle, Prisma) fulfill the contract.
218
+ *
219
+ * To swap implementations, change the factory in the module's register() method.
220
+ */
221
+ import { createToken } from '@forinda/kickjs'
222
+ import type { ${t}ResponseDTO } from '${r}/${n}-response.dto'
223
+ import type { Create${t}DTO } from '${r}/create-${n}.dto'
224
+ import type { Update${t}DTO } from '${r}/update-${n}.dto'
225
+ import type { ParsedQuery } from '@forinda/kickjs'
226
+
227
+ export interface I${t}Repository {
228
+ findById(id: string): Promise<${t}ResponseDTO | null>
229
+ findAll(): Promise<${t}ResponseDTO[]>
230
+ findPaginated(parsed: ParsedQuery): Promise<{ data: ${t}ResponseDTO[]; total: number }>
231
+ create(dto: Create${t}DTO): Promise<${t}ResponseDTO>
232
+ update(id: string, dto: Update${t}DTO): Promise<${t}ResponseDTO>
233
+ delete(id: string): Promise<void>
234
+ }
235
+
236
+ /**
237
+ * Collision-safe DI token bound to \`I${t}Repository\`.
238
+ * \`container.resolve(${t.toUpperCase()}_REPOSITORY)\` and
239
+ * \`@Inject(${t.toUpperCase()}_REPOSITORY)\` both return the typed
240
+ * interface — no manual generic, no \`any\` cast.
241
+ *
242
+ * The \`'${i}/'\` prefix matches the project scope so
243
+ * \`kick-lint\`'s \`token-reserved-prefix\` rule never fires —
244
+ * adopters must NOT use the reserved \`'kick/'\` namespace.
245
+ */
246
+ export const ${t.toUpperCase()}_REPOSITORY = createToken<I${t}Repository>('${i}/${t}/repository')
247
+ `}function I(e){let{pascal:t,kebab:n,repoPrefix:r=`../../domain/repositories`,dtoPrefix:i=`../../application/dtos`}=e;return`/**
248
+ * In-Memory ${t} Repository
249
+ *
250
+ * Implements the repository interface using a Map.
251
+ * Useful for prototyping and testing. Replace with a database implementation
252
+ * (Drizzle, Prisma, etc.) for production use.
253
+ *
254
+ * @Repository() registers this class in the DI container as a singleton.
255
+ */
256
+ import { randomUUID } from 'node:crypto'
257
+ import { Repository, HttpException } from '@forinda/kickjs'
258
+ import type { ParsedQuery } from '@forinda/kickjs'
259
+ import type { I${t}Repository } from '${r}/${n}.repository'
260
+ import type { ${t}ResponseDTO } from '${i}/${n}-response.dto'
261
+ import type { Create${t}DTO } from '${i}/create-${n}.dto'
262
+ import type { Update${t}DTO } from '${i}/update-${n}.dto'
263
+
264
+ @Repository()
265
+ export class InMemory${t}Repository implements I${t}Repository {
266
+ private store = new Map<string, ${t}ResponseDTO>()
267
+
268
+ async findById(id: string): Promise<${t}ResponseDTO | null> {
269
+ return this.store.get(id) ?? null
270
+ }
271
+
272
+ async findAll(): Promise<${t}ResponseDTO[]> {
273
+ return Array.from(this.store.values())
274
+ }
275
+
276
+ async findPaginated(parsed: ParsedQuery): Promise<{ data: ${t}ResponseDTO[]; total: number }> {
277
+ const all = Array.from(this.store.values())
278
+ const data = all.slice(parsed.pagination.offset, parsed.pagination.offset + parsed.pagination.limit)
279
+ return { data, total: all.length }
280
+ }
281
+
282
+ async create(dto: Create${t}DTO): Promise<${t}ResponseDTO> {
283
+ const now = new Date().toISOString()
284
+ const entity: ${t}ResponseDTO = {
285
+ id: randomUUID(),
286
+ ...dto,
287
+ createdAt: now,
288
+ updatedAt: now,
289
+ }
290
+ this.store.set(entity.id, entity)
291
+ return entity
292
+ }
293
+
294
+ async update(id: string, dto: Update${t}DTO): Promise<${t}ResponseDTO> {
295
+ const existing = this.store.get(id)
296
+ if (!existing) throw HttpException.notFound('${t} not found')
297
+ const updated = { ...existing, ...dto, updatedAt: new Date().toISOString() }
298
+ this.store.set(id, updated)
299
+ return updated
300
+ }
301
+
302
+ async delete(id: string): Promise<void> {
303
+ if (!this.store.has(id)) throw HttpException.notFound('${t} not found')
304
+ this.store.delete(id)
305
+ }
306
+ }
307
+ `}function L(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`/**
308
+ * ${o} ${t} Repository
309
+ *
310
+ * Stub implementation for a custom '${r}' repository.
311
+ * Implements the repository interface using an in-memory Map as a placeholder.
312
+ *
313
+ * TODO: Replace the in-memory Map with your ${r} data-access logic.
314
+ * See I${t}Repository for the interface contract.
315
+ *
316
+ * @Repository() registers this class in the DI container as a singleton.
317
+ */
318
+ import { randomUUID } from 'node:crypto'
319
+ import { Repository, HttpException } from '@forinda/kickjs'
320
+ import type { ParsedQuery } from '@forinda/kickjs'
321
+ import type { I${t}Repository } from '${i}/${n}.repository'
322
+ import type { ${t}ResponseDTO } from '${a}/${n}-response.dto'
323
+ import type { Create${t}DTO } from '${a}/create-${n}.dto'
324
+ import type { Update${t}DTO } from '${a}/update-${n}.dto'
325
+
326
+ @Repository()
327
+ export class ${o}${t}Repository implements I${t}Repository {
328
+ // TODO: Replace with your ${r} client/connection
329
+ private store = new Map<string, ${t}ResponseDTO>()
330
+
331
+ async findById(id: string): Promise<${t}ResponseDTO | null> {
332
+ // TODO: Implement with ${r}
333
+ return this.store.get(id) ?? null
334
+ }
335
+
336
+ async findAll(): Promise<${t}ResponseDTO[]> {
337
+ // TODO: Implement with ${r}
338
+ return Array.from(this.store.values())
339
+ }
340
+
341
+ async findPaginated(parsed: ParsedQuery): Promise<{ data: ${t}ResponseDTO[]; total: number }> {
342
+ // TODO: Implement with ${r}
343
+ const all = Array.from(this.store.values())
344
+ const data = all.slice(parsed.pagination.offset, parsed.pagination.offset + parsed.pagination.limit)
345
+ return { data, total: all.length }
346
+ }
347
+
348
+ async create(dto: Create${t}DTO): Promise<${t}ResponseDTO> {
349
+ // TODO: Implement with ${r}
350
+ const now = new Date().toISOString()
351
+ const entity: ${t}ResponseDTO = {
352
+ id: randomUUID(),
353
+ ...dto,
354
+ createdAt: now,
355
+ updatedAt: now,
356
+ }
357
+ this.store.set(entity.id, entity)
358
+ return entity
359
+ }
360
+
361
+ async update(id: string, dto: Update${t}DTO): Promise<${t}ResponseDTO> {
362
+ // TODO: Implement with ${r}
363
+ const existing = this.store.get(id)
364
+ if (!existing) throw HttpException.notFound('${t} not found')
365
+ const updated = { ...existing, ...dto, updatedAt: new Date().toISOString() }
366
+ this.store.set(id, updated)
367
+ return updated
368
+ }
369
+
370
+ async delete(id: string): Promise<void> {
371
+ // TODO: Implement with ${r}
372
+ if (!this.store.has(id)) throw HttpException.notFound('${t} not found')
373
+ this.store.delete(id)
374
+ }
375
+ }
376
+ `}function se(e){let{pascal:t,kebab:n,plural:r=``}=e;return`import { describe, it, expect, beforeEach } from 'vitest'
377
+ import { Container } from '@forinda/kickjs'
378
+
379
+ describe('${t}Controller', () => {
380
+ beforeEach(() => {
381
+ Container.reset()
382
+ })
383
+
384
+ it('should be defined', () => {
385
+ expect(true).toBe(true)
386
+ })
387
+
388
+ describe('POST /${r}', () => {
389
+ it('should create a new ${n}', async () => {
390
+ // TODO: Set up test module, call create endpoint, assert 201
391
+ expect(true).toBe(true)
392
+ })
393
+ })
394
+
395
+ describe('GET /${r}', () => {
396
+ it('should return paginated ${r}', async () => {
397
+ // TODO: Set up test module, call list endpoint, assert { data, meta }
398
+ expect(true).toBe(true)
399
+ })
400
+ })
401
+
402
+ describe('GET /${r}/:id', () => {
403
+ it('should return a ${n} by id', async () => {
404
+ // TODO: Create a ${n}, then fetch by id, assert match
405
+ expect(true).toBe(true)
406
+ })
407
+
408
+ it('should return 404 for non-existent ${n}', async () => {
409
+ // TODO: Fetch non-existent id, assert 404
410
+ expect(true).toBe(true)
411
+ })
412
+ })
413
+
414
+ describe('PUT /${r}/:id', () => {
415
+ it('should update an existing ${n}', async () => {
416
+ // TODO: Create, update, assert changes
417
+ expect(true).toBe(true)
418
+ })
419
+ })
420
+
421
+ describe('DELETE /${r}/:id', () => {
422
+ it('should delete a ${n}', async () => {
423
+ // TODO: Create, delete, assert gone
424
+ expect(true).toBe(true)
425
+ })
426
+ })
427
+ })
428
+ `}function ce(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'
429
+ import { InMemory${t}Repository } from '${i}'
430
+
431
+ describe('InMemory${t}Repository', () => {
432
+ let repo: InMemory${t}Repository
433
+
434
+ beforeEach(() => {
435
+ repo = new InMemory${t}Repository()
436
+ })
437
+
438
+ it('should create and retrieve a ${n}', async () => {
439
+ const created = await repo.create({ name: 'Test ${t}' })
440
+ expect(created).toBeDefined()
441
+ expect(created.name).toBe('Test ${t}')
442
+ expect(created.id).toBeDefined()
443
+
444
+ const found = await repo.findById(created.id)
445
+ expect(found).toEqual(created)
446
+ })
447
+
448
+ it('should return null for non-existent id', async () => {
449
+ const found = await repo.findById('non-existent')
450
+ expect(found).toBeNull()
451
+ })
452
+
453
+ it('should list all ${r}', async () => {
454
+ await repo.create({ name: '${t} 1' })
455
+ await repo.create({ name: '${t} 2' })
456
+
457
+ const all = await repo.findAll()
458
+ expect(all).toHaveLength(2)
459
+ })
460
+
461
+ it('should return paginated results', async () => {
462
+ await repo.create({ name: '${t} 1' })
463
+ await repo.create({ name: '${t} 2' })
464
+ await repo.create({ name: '${t} 3' })
465
+
466
+ const result = await repo.findPaginated({
467
+ filters: [],
468
+ sort: [],
469
+ search: '',
470
+ pagination: { page: 1, limit: 2, offset: 0 },
471
+ })
472
+
473
+ expect(result.data).toHaveLength(2)
474
+ expect(result.total).toBe(3)
475
+ })
476
+
477
+ it('should update a ${n}', async () => {
478
+ const created = await repo.create({ name: 'Original' })
479
+ const updated = await repo.update(created.id, { name: 'Updated' })
480
+ expect(updated.name).toBe('Updated')
481
+ })
482
+
483
+ it('should delete a ${n}', async () => {
484
+ const created = await repo.create({ name: 'To Delete' })
485
+ await repo.delete(created.id)
486
+ const found = await repo.findById(created.id)
487
+ expect(found).toBeNull()
488
+ })
489
+ })
490
+ `}function R(e){let{pascal:t,kebab:n}=e;return`import { Service, Inject, HttpException } from '@forinda/kickjs'
491
+ import type { ParsedQuery } from '@forinda/kickjs'
492
+ import { ${t.toUpperCase()}_REPOSITORY, type I${t}Repository } from './${n}.repository'
493
+ import type { ${t}ResponseDTO } from './dtos/${n}-response.dto'
494
+ import type { Create${t}DTO } from './dtos/create-${n}.dto'
495
+ import type { Update${t}DTO } from './dtos/update-${n}.dto'
496
+
497
+ @Service()
498
+ export class ${t}Service {
499
+ constructor(
500
+ @Inject(${t.toUpperCase()}_REPOSITORY) private readonly repo: I${t}Repository,
501
+ ) {}
502
+
503
+ async findById(id: string): Promise<${t}ResponseDTO | null> {
504
+ return this.repo.findById(id)
505
+ }
506
+
507
+ async findAll(): Promise<${t}ResponseDTO[]> {
508
+ return this.repo.findAll()
509
+ }
510
+
511
+ async findPaginated(parsed: ParsedQuery) {
512
+ return this.repo.findPaginated(parsed)
513
+ }
514
+
515
+ async create(dto: Create${t}DTO): Promise<${t}ResponseDTO> {
516
+ return this.repo.create(dto)
517
+ }
518
+
519
+ async update(id: string, dto: Update${t}DTO): Promise<${t}ResponseDTO> {
520
+ return this.repo.update(id, dto)
521
+ }
522
+
523
+ async delete(id: string): Promise<void> {
524
+ await this.repo.delete(id)
525
+ }
526
+ }
527
+ `}function z(e){let{pascal:t}=e;return`import type { QueryFieldConfig } from '@forinda/kickjs'
528
+
529
+ export const ${t.toUpperCase()}_QUERY_CONFIG: QueryFieldConfig = {
530
+ filterable: ['name'],
531
+ sortable: ['name', 'createdAt'],
532
+ searchable: ['name'],
533
+ }
534
+ `}async function le(e){let{pascal:t,kebab:n,plural:r,style:i,write:a}=e;await a(`${n}.module.ts`,ie({pascal:t,kebab:n,plural:r,style:i})),await a(`${n}.controller.ts`,`import { Controller, Get, type Ctx } from '@forinda/kickjs'
535
+
536
+ // \`Ctx<KickRoutes.${t}Controller['<method>']>\` is generated by
537
+ // \`kick typegen\` (auto-run on \`kick dev\`).
538
+
539
+ @Controller()
540
+ export class ${t}Controller {
541
+ @Get('/')
542
+ async list(ctx: Ctx<KickRoutes.${t}Controller['list']>) {
543
+ ctx.json({ message: '${t} list' })
544
+ }
545
+ }
546
+ `)}async function ue(e){let{pascal:t,kebab:n,plural:r,pluralPascal:i,repo:a,noTests:o,tokenScope:s,style:c,write:l}=e;await l(`${n}.module.ts`,M({pascal:t,kebab:n,plural:r,repo:a,style:c})),await l(`${n}.constants.ts`,z({pascal:t,kebab:n})),await l(`${n}.controller.ts`,N({pascal:t,kebab:n,plural:r,pluralPascal:i})),await l(`${n}.service.ts`,R({pascal:t,kebab:n})),await l(`dtos/create-${n}.dto.ts`,P({pascal:t,kebab:n})),await l(`dtos/update-${n}.dto.ts`,ae({pascal:t,kebab:n})),await l(`dtos/${n}-response.dto.ts`,oe({pascal:t,kebab:n})),await l(`${n}.repository.ts`,F({pascal:t,kebab:n,dtoPrefix:`./dtos`,tokenScope:s}));let u=a===`inmemory`,d=u?`in-memory-${n}`:`${E(a)}-${n}`,f=u?I({pascal:t,kebab:n,repoPrefix:`.`,dtoPrefix:`./dtos`}):L({pascal:t,kebab:n,repoType:a,repoPrefix:`.`,dtoPrefix:`./dtos`});await l(`${d}.repository.ts`,f),o||(a!==`inmemory`&&await l(`in-memory-${n}.repository.ts`,I({pascal:t,kebab:n,repoPrefix:`.`,dtoPrefix:`./dtos`})),await l(`__tests__/${n}.controller.test.ts`,se({pascal:t,kebab:n,plural:r})),await l(`__tests__/${n}.repository.test.ts`,ce({pascal:t,kebab:n,plural:r,repoPrefix:`../in-memory-${n}.repository`})))}function de(e){return e?typeof e==`string`?e:e.name:`inmemory`}async function fe(n){let{name:r,modulesDir:s,noEntity:c,noTests:l,repo:u=`inmemory`,force:d,dryRun:f}=n,p=n.pluralize!==!1,h=n.pattern??`rest`;n.minimal&&(h=`minimal`);let g=E(r),_=w(r),v=p?D(g):g,y=p?O(_):_,b=m(s,v),x=[],S=d??!1,C={kebab:g,pascal:_,plural:v,pluralPascal:y,moduleDir:b,repo:u,noEntity:c??!1,noTests:l??!1,prismaClientPath:n.prismaClientPath??`@prisma/client`,tokenScope:n.tokenScope??`app`,style:n.style??`define`,write:async(n,r)=>{let s=m(b,n);if(f){x.push(s);return}if(!S&&await t(s)&&!await a({message:`File exists: ${o.dim(n)}. Overwrite?`,initialValue:!1})){i.warn(`Skipped: ${n}`);return}await e(s,r),x.push(s)},files:x};switch(h){case`minimal`:await le(C);break;default:await ue(C);break}return f||await B(s,_,v,g,C.style),x}async function B(n,r,i,a,o=`define`){let s=m(n,`index.ts`),c=await t(s),l=`./${i}/${a}.module`,u=o===`class`?`${r}Module`:`${r}Module()`;if(!c){await e(s,o===`class`?`import type { AppModuleEntry } from '@forinda/kickjs'
547
+ import { ${r}Module } from '${l}'
548
+
549
+ export const modules: AppModuleEntry[] = [${u}]
550
+ `:`import { defineModules } from '@forinda/kickjs'
551
+ import { ${r}Module } from '${l}'
552
+
553
+ export const modules = defineModules().mount(${u})
554
+ `);return}let d=await x(s,`utf-8`),f=`import { ${r}Module } from '${l}'`,p=k(l);if(!RegExp(`^import\\s*\\{[^}]*\\b${k(r)}Module\\b[^}]*\\}\\s*from\\s*['"]${p}['"]`,`m`).test(d)){let e=d.lastIndexOf(`import `);if(e!==-1){let t=d.indexOf(`
555
+ `,e);d=d.slice(0,t+1)+f+`
556
+ `+d.slice(t+1)}else d=f+`
557
+ `+d}let h=H(d);if(h){let e=d.slice(h.rhsStart,h.rhsEnd+1);RegExp(`\\b${k(r)}Module\\b`).test(e)||(d=V(d,u))}else d=V(d,u);await S(s,d,`utf-8`)}function V(e,t){let n=H(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 H(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=me(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=W(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=W(e,t);if(n===-1)break;i=n+1}return i}function U(e,t){let n=e.slice(t,t+2);if(n===`//`){for(t+=2;t<e.length&&e[t]!==`
558
+ `;)t++;return t}if(n===`/*`){for(t+=2;t+1<e.length&&!(e[t]===`*`&&e[t+1]===`/`);)t++;return t+2}return t}function me(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=U(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 W(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=U(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 he(t){let{name:n,outDir:r}=t,i=E(n),a=w(n),o=[],s=m(r,`${i}.adapter.ts`);return await e(s,`import {
559
+ defineAdapter,
560
+ type AdapterContext,
561
+ type AdapterMiddleware,
562
+ type ContributorRegistrations,
563
+ type Constructor,
564
+ } from '@forinda/kickjs'
565
+
566
+ /**
567
+ * Configuration for the ${a} adapter.
568
+ *
569
+ * Adapters typically take a small config object so callers can tune
570
+ * behaviour at bootstrap time. Keep the shape narrow — anything
571
+ * derived from the environment should be read inside the build
572
+ * function via getEnv(), not forced onto the caller.
573
+ */
574
+ export interface ${a}AdapterConfig {
575
+ // Add your adapter configuration here, e.g.:
576
+ // enabled?: boolean
577
+ // apiKey?: string
578
+ }
579
+
580
+ /**
581
+ * ${a} adapter — built via \`defineAdapter()\` so callers get the
582
+ * factory's call / \`.scoped()\` / \`.async()\` surfaces for free.
583
+ *
584
+ * Hooks into the Application lifecycle to add middleware, routes,
585
+ * Context Contributors, or external service connections.
586
+ *
587
+ * Every lifecycle hook below is OPTIONAL. The scaffold emits all of
588
+ * them so adopters can browse what's available and delete what they
589
+ * don't need — \`build()\` returning \`{}\` is also valid for an adapter
590
+ * that only contributes config defaults.
591
+ *
592
+ * @example
593
+ * \`\`\`ts
594
+ * import { bootstrap } from '@forinda/kickjs'
595
+ * import { ${a}Adapter } from './adapters/${i}.adapter'
596
+ *
597
+ * bootstrap({
598
+ * modules,
599
+ * adapters: [${a}Adapter({ /* config overrides *\\/ })],
600
+ * })
601
+ * \`\`\`
602
+ */
603
+ export const ${a}Adapter = defineAdapter<${a}AdapterConfig>({
604
+ name: '${a}Adapter',
605
+ defaults: {
606
+ // Default config values go here. The adopter's overrides shallow-merge
607
+ // on top of these before \`build()\` runs.
608
+ },
609
+ build: (_config, { name: _name }) => {
610
+ // Closures inside \`build()\` are how each adapter instance owns its
611
+ // own state (database client, Map, timer handle, …). The same
612
+ // \`_config\` is visible to every hook below.
613
+
614
+ return {
615
+ /**
616
+ * Express middleware entries the Application mounts at named phases.
617
+ *
618
+ * \`phase\` controls where each handler sits in the pipeline:
619
+ * 'beforeGlobal' | 'afterGlobal' | 'beforeRoutes' | 'afterRoutes'.
620
+ *
621
+ * \`path\` (optional) scopes the entry to a path prefix.
622
+ *
623
+ * Delete this hook entirely if you don't add middleware.
624
+ */
625
+ middleware(): AdapterMiddleware[] {
626
+ return [
627
+ // Example: add a custom header to all responses
628
+ // {
629
+ // phase: 'beforeGlobal',
630
+ // handler: (_req, res, next) => {
631
+ // res.setHeader('X-${a}', 'true')
632
+ // next()
633
+ // },
634
+ // },
635
+ // Example: scope a rate limiter to one path prefix
636
+ // {
637
+ // phase: 'beforeRoutes',
638
+ // path: '/api/v1/auth',
639
+ // handler: rateLimit({ max: 10 }),
640
+ // },
641
+ ]
642
+ },
643
+
644
+ /**
645
+ * Runs BEFORE global middleware. Mount routes that should bypass the
646
+ * middleware stack — health checks, docs UI, static assets, OAuth
647
+ * callbacks. Anything you want reachable even if a global middleware
648
+ * later in the chain rejects requests.
649
+ *
650
+ * Delete this hook if you have no early routes.
651
+ */
652
+ beforeMount(_ctx: AdapterContext): void {
653
+ // Example:
654
+ // _ctx.app.get('/${i}/status', (_req, res) => res.json({ status: 'ok' }))
655
+ },
656
+
657
+ /**
658
+ * Fires once per controller class as the router mounts. Use this to
659
+ * collect route metadata for OpenAPI specs, dependency graphs, route
660
+ * inventories, devtools dashboards.
661
+ *
662
+ * Delete this hook unless your adapter introspects the route registry.
663
+ */
664
+ onRouteMount(_controllerClass: Constructor, _mountPath: string): void {
665
+ // Example (Swagger-style): collect routes for the spec.
666
+ // openApiSpec.addController(_controllerClass, _mountPath)
667
+ },
668
+
669
+ /**
670
+ * Runs AFTER modules + routes are wired, BEFORE the server starts.
671
+ * Right place for late-stage DI registrations or final config validation.
672
+ *
673
+ * Delete this hook if there's nothing to wire post-modules.
674
+ */
675
+ beforeStart(_ctx: AdapterContext): void {
676
+ // Example: _ctx.container.registerInstance(MY_TOKEN, new MyService(_config))
677
+ },
678
+
679
+ /**
680
+ * Runs AFTER the HTTP server is listening. The raw \`http.Server\` is
681
+ * available on \`ctx.server\` — attach upgrade handlers (Socket.IO,
682
+ * gRPC, GraphQL subscriptions), warm caches, log a banner.
683
+ *
684
+ * Delete this hook if you don't need the running server reference.
685
+ */
686
+ afterStart(_ctx: AdapterContext): void {
687
+ // Example: const io = new Server(_ctx.server)
688
+ },
689
+
690
+ /**
691
+ * Returns Context Contributors to merge into every route's pipeline
692
+ * at the \`'adapter'\` precedence level. Per-route handlers can
693
+ * override the value at the method / class / module level.
694
+ *
695
+ * Delete this hook unless your adapter ships typed per-request values
696
+ * (auth user, tenant, locale, feature flags, geo, etc).
697
+ */
698
+ contributors(): ContributorRegistrations {
699
+ return [
700
+ // Example:
701
+ // import { defineHttpContextDecorator } from '@forinda/kickjs'
702
+ // declare module '@forinda/kickjs' { interface ContextMeta { ${i}: { id: string } } }
703
+ // const Load${a} = defineHttpContextDecorator({
704
+ // key: '${i}',
705
+ // resolve: (ctx) => ({ id: ctx.req.headers['x-${i}-id'] as string }),
706
+ // })
707
+ // return [Load${a}.registration]
708
+ ]
709
+ },
710
+
711
+ /**
712
+ * Runs on graceful shutdown (SIGINT/SIGTERM). Clean up long-lived
713
+ * resources the adapter owns: close connections, flush buffers,
714
+ * cancel timers. The framework runs every adapter's \`shutdown\`
715
+ * concurrently via \`Promise.allSettled\` — one failure won't block
716
+ * sibling adapters.
717
+ *
718
+ * Delete this hook if your adapter holds no resources.
719
+ */
720
+ async shutdown(): Promise<void> {
721
+ // Example: await this.pool.end()
722
+ // Example: clearInterval(this.heartbeatTimer)
723
+ },
724
+ }
725
+ },
726
+ })
727
+ `),o.push(s),o}const ge={controller:``,service:``,dto:`dtos`,guard:`guards`,middleware:`middleware`,contributor:`contributors`};function G(e){let{type:t,outDir:n,moduleName:r,modulesDir:i=`src/modules`,defaultDir:a,shouldPluralize:o=!0}=e;if(n)return g(n);if(r){let e=ge,n=E(r),a=o?D(n):n,s=e[t]??``,c=m(i,a);return g(s?m(c,s):c)}return g(a)}async function _e(t){let{name:n,moduleName:r,modulesDir:i,pattern:a}=t,o=G({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=m(o,`${s}.middleware.ts`);return await e(u,`import type { Request, Response, NextFunction } from 'express'
728
+
729
+ export interface ${w(n)}Options {
730
+ // Add configuration options here. The factory below closes over the
731
+ // resolved options object; pass them at the call site —
732
+ // \`${c}({ foo: 'bar' })\` — and the closure preserves them across
733
+ // every request.
734
+ }
735
+
736
+ /**
737
+ * ${w(n)} middleware.
738
+ *
739
+ * Usage in bootstrap (fires on every request):
740
+ * middleware: [${c}()]
741
+ *
742
+ * Usage with adapter — phase controls *when* the handler runs:
743
+ *
744
+ * middleware() {
745
+ * return [{ handler: ${c}(), phase: 'afterGlobal' }]
746
+ * }
747
+ *
748
+ * Phase semantics (see \`MiddlewarePhase\` JSDoc for the full contract):
749
+ * - 'beforeGlobal' / 'afterGlobal' / 'beforeRoutes' — fire on every
750
+ * request, before module routes run.
751
+ * - 'afterRoutes' — fires ONLY when no route matched (404 fall-through)
752
+ * OR a route handler called \`next()\` without ending the response.
753
+ * Controllers that call \`ctx.json(…)\` end the chain and skip this
754
+ * phase. For per-response work (logging, metrics) attach to
755
+ * \`res.on('finish', …)\` from an earlier-phase middleware instead.
756
+ *
757
+ * Optional path scope — string, RegExp, or array of either:
758
+ * middleware() {
759
+ * return [{
760
+ * handler: ${c}({ region: 'eu' }),
761
+ * phase: 'afterGlobal',
762
+ * path: ['/api', /^\\/admin/],
763
+ * }]
764
+ * }
765
+ *
766
+ * Usage with @Middleware decorator:
767
+ * @Middleware(${c}())
768
+ */
769
+ export function ${c}(options: ${w(n)}Options = {}) {
770
+ return (req: Request, res: Response, next: NextFunction) => {
771
+ // Implement your middleware logic here. \`options\` is captured by
772
+ // closure — log or read it anywhere in this handler body.
773
+ void options
774
+ next()
775
+ }
776
+ }
777
+ `),l.push(u),l}async function ve(t){let{name:n,moduleName:r,modulesDir:i,pattern:a}=t,o=G({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=m(o,`${s}.guard.ts`);return await e(d,`import { Container, HttpException } from '@forinda/kickjs'
778
+ import type { RequestContext } from '@forinda/kickjs'
779
+
780
+ /**
781
+ * ${l} guard.
782
+ *
783
+ * Guards protect routes by checking conditions before the handler runs.
784
+ * Return early with an error response to block access.
785
+ *
786
+ * Usage:
787
+ * @Middleware(${c}Guard)
788
+ * @Get('/protected')
789
+ * async handler(ctx: RequestContext) { ... }
790
+ */
791
+ export async function ${c}Guard(ctx: RequestContext, next: () => void): Promise<void> {
792
+ // Example: check for an authorization header
793
+ const header = ctx.headers.authorization
794
+ if (!header?.startsWith('Bearer ')) {
795
+ ctx.res.status(401).json({ message: 'Missing or invalid authorization header' })
796
+ return
797
+ }
798
+
799
+ const token = header.slice(7)
800
+
801
+ try {
802
+ // Verify the token using a service from the DI container
803
+ // const container = Container.getInstance()
804
+ // const authService = container.resolve(AuthService)
805
+ // const payload = authService.verifyToken(token)
806
+ // ctx.set('auth', payload)
807
+
808
+ next()
809
+ } catch {
810
+ ctx.res.status(401).json({ message: 'Invalid or expired token' })
811
+ }
812
+ }
813
+ `),u.push(d),u}async function ye(t){let{name:n,moduleName:r,modulesDir:i,pattern:a}=t,o=G({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=m(o,`${s}.service.ts`);return await e(u,`import { Service } from '@forinda/kickjs'
814
+
815
+ @Service()
816
+ export class ${c}Service {
817
+ // Inject dependencies via constructor
818
+ // constructor(
819
+ // @Inject(MY_REPO) private readonly repo: IMyRepository,
820
+ // ) {}
821
+ }
822
+ `),l.push(u),l}async function be(t){let{name:n,moduleName:r,modulesDir:i,pattern:a}=t,o=G({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=m(o,`${s}.controller.ts`);return await e(u,`import { Controller, Get, Post, type Ctx } from '@forinda/kickjs'
823
+
824
+ // \`Ctx<KickRoutes.${c}Controller['<method>']>\` is generated by
825
+ // \`kick typegen\` (auto-run on \`kick dev\`). After the first run, your IDE
826
+ // will autocomplete \`ctx.params\`, \`ctx.body\`, and \`ctx.query\`.
827
+ // See https://kickjs.app/guide/typegen for details.
828
+
829
+ @Controller()
830
+ export class ${c}Controller {
831
+ // @Autowired() private readonly myService!: MyService
832
+
833
+ @Get('/')
834
+ async list(ctx: Ctx<KickRoutes.${c}Controller['list']>) {
835
+ ctx.json({ message: '${c} list' })
836
+ }
837
+
838
+ @Post('/')
839
+ async create(ctx: Ctx<KickRoutes.${c}Controller['create']>) {
840
+ ctx.created({ message: '${c} created', data: ctx.body })
841
+ }
842
+ }
843
+ `),l.push(u),l}async function xe(t){let{name:n,moduleName:r,modulesDir:i,pattern:a}=t,o=G({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=m(o,`${s}.dto.ts`);return await e(d,`import { z } from 'zod'
844
+
845
+ export const ${l}Schema = z.object({
846
+ // Define your schema fields here
847
+ name: z.string().min(1).max(200),
848
+ })
849
+
850
+ export type ${c}DTO = z.infer<typeof ${l}Schema>
851
+ `),u.push(d),u}function Se(e){let t=new Map;return{report(n,r){let i=r instanceof Error?r.message:String(r);t.get(n)!==i&&(t.set(n,i),e(` kick typegen: ${n} pass failed (${i}) — types in .kickjs/types may be stale`))},clear(e){t.delete(e)}}}const Ce=`__kickjs_typegen_owner`;function we(e){let{cwd:t,config:n}=e,r=e.debounceMs??100,i=e.pipeline??{runTypegen:async e=>(await import(`./typegen-BTfDyerr.mjs`).then(e=>e.n)).runTypegen(e),runAllPluginTypegens:async e=>(await import(`./run-plugins-B-_RJ-JV.mjs`)).runAllPluginTypegens(e),writeTypegenArtifacts:async(e,t,n)=>(await import(`./typegen-BTfDyerr.mjs`).then(e=>e.n)).writeTypegenArtifacts(e,t,n),buildAssets:async(e,t)=>(await import(`./build-C6c_h-27.mjs`).then(e=>e.n)).buildAssets(e,t)},a=n?.typegen?.schemaValidator??`zod`,o=n?.typegen?.envFile,s=f.resolve(t,n?.typegen?.outDir??`.kickjs/types`),c=n?.assetMap?Object.values(n.assetMap).map(e=>e?.src).filter(e=>typeof e==`string`&&e.length>0).map(e=>f.resolve(t,e)):[],l=!!n?.assetMap&&Object.keys(n.assetMap).length>0,u=e=>e.replaceAll(`\\`,`/`),d=c.map(u),p=e=>{let t=u(e);return d.some(e=>t===e||t.startsWith(`${e}/`))},m=Se(e.emitWarning),h=null,g=!1,_=new Set,v=new Set,y=!1,b=!1;function x(r,c){i.runTypegen({cwd:t,silent:!0,allowDuplicates:!0,schemaValidator:a,envFile:o,srcDir:n?.typegen?.srcDir,outDir:n?.typegen?.outDir,assetMap:n?.assetMap,changedFiles:r,runPlugins:!1}).then(()=>m.clear(`scan`)).catch(e=>m.report(`scan`,e)),i.runAllPluginTypegens({cwd:t,config:n,silent:!0,changedFiles:r}).then(e=>i.writeTypegenArtifacts(s,e,!0)).then(()=>m.clear(`plugins`)).catch(e=>m.report(`plugins`,e)).finally(()=>e.onPassComplete?.()),c&&n&&i.buildAssets(n,{cwd:t,silent:!0}).catch(()=>{})}function S(){let e=y?void 0:{changed:[..._],removed:[...v]},t=b;_.clear(),v.clear(),y=!1,b=!1,x(e,t)}return{assetSrcRoots:c,handleWatchEvent(e,t){if(!g&&!u(t).includes(`/.kickjs/`)){if(e===`unlinkDir`)y=!0,l&&(b=!0);else{if(t.endsWith(`.d.ts`))return;let n=/\.(ts|tsx|mts|cts)$/.test(t),r=p(t);if(!n&&!r)return;r&&l&&(b=!0),n&&(e===`unlink`?(v.add(t),_.delete(t)):(_.add(t),v.delete(t)))}h&&clearTimeout(h),h=setTimeout(S,r)}},runOnce(){g||x(void 0,l)},dispose(){g=!0,h&&clearTimeout(h),h=null}}}function Te(e){return E(e).replace(/-/g,`_`)}function K(e){let t=e.cwd??process.cwd(),n=e.projectRoot??u(t),r=e.pluralize??!0,i=w(e.name),a=T(e.name),o=E(e.name),s=Te(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 Ee(e,t){return g(e.cwd,t)}async function De(e){return import(ee(e).href)}const q=new Map;async function J(e){let t=q.get(e);if(t)return t;let n=Oe(e);return q.set(e,n),n}async function Oe(e){let t=g(e,`package.json`);if(!_(t))return{generators:[],loaded:[],failed:[]};let n=ke(JSON.parse(await x(t,`utf-8`))),r=d(g(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 x(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=g(p(t),s);if(!_(c)){o.push({source:e,reason:`kickjs.generators points to missing file: ${s}`});continue}let l;try{l=await De(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(!Ae(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 ke(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 Ae(e){if(!e||typeof e!=`object`)return!1;let t=e;return typeof t.name==`string`&&typeof t.files==`function`}async function je(e,t=[]){let n=e.cwd??process.cwd(),r=t.find(t=>t.spec.name===e.generatorName);if(r)return Y(r.spec,r.source,e,n);let i=Ne(await J(n),e.generatorName);return i?Y(i.spec,i.source,e,n):null}async function Me(e,t=[]){let n=await J(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 Ne(e,t){return e.generators.find(e=>e.spec.name===t)}async function Y(t,n,r,i){let a=K({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=Ee(a,t.path);await e(n,t.content),s.push(n)}return{files:s,source:n}}function Pe(e){return e}function Fe(e){return e}function X(e){try{return JSON.parse(v(e,`utf-8`))}catch{return null}}function Z(e){try{return v(e,`utf-8`)}catch{return null}}function Ie(e){let t=Z(m(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=Le(e,r.extends);if(t){let e=X(t)??{};r.compilerOptions={...e.compilerOptions,...r.compilerOptions}}}return r}function Le(e,t){if(t.startsWith(`.`)){let n=g(e,t);return _(n)?n:null}let n=m(e,`node_modules`,t);return _(n)?n:null}function Re(e){return e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`)}function ze(){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.
852
+ Install a supported version via nvm / fnm / volta.`}:{name:`Node version`,status:`pass`,message:e}}function Be(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 Ve(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}const He={express:[],fastify:[`fastify`,`@fastify/middie`],h3:[`h3`]};function Ue(e){if(!e.pkg||e.runtime===`express`)return null;let t={...e.pkg.dependencies,...e.pkg.peerDependencies,...e.pkg.devDependencies},n=He[e.runtime].filter(e=>!t[e]),r=`runtime engine (${e.runtime})`;return n.length>0?{name:r,status:`fail`,fix:`Resolved runtime '${e.runtime}' is missing engine peer(s): ${n.join(`, `)}.\nInstall: pnpm add ${n.join(` `)}`}:{name:r,status:`pass`}}function We(e){if(!e.pkg||!Ge(e.cwd))return null;let t=s[e.runtime],n=`upload driver (${e.runtime})`;return t.prod?{...e.pkg.dependencies,...e.pkg.peerDependencies,...e.pkg.devDependencies}[t.prod]?{name:n,status:`pass`,message:t.prod}:{name:n,status:`fail`,fix:`This project uses file uploads on the '${e.runtime}' runtime, which needs '${t.prod}'.\nInstall it: kick add upload (or pnpm add ${t.prod})`}:{name:n,status:`pass`,message:`native multipart`}}const Q=2e3;function Ge(e){let t=m(e,`src`);if(!_(t))return!1;let n=/@FileUpload\b|\bupload\.(single|array|none)\s*\(/,r=[t],i=0;for(;r.length>0&&i<Q;){let e=r.pop(),t;try{t=y(e,{withFileTypes:!0})}catch{continue}for(let a of t){if(i>=Q)break;let t=m(e,a.name);if(a.isDirectory()){a.name!==`node_modules`&&r.push(t);continue}if(/\.(ts|tsx|mts|cts)$/.test(a.name)&&(i++,n.test(Z(t)??``)))return!0}}return!1}function Ke(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.
853
+ Install it: pnpm add reflect-metadata
854
+ Then import it at the top of src/index.ts:
855
+
856
+ import 'reflect-metadata'
857
+ // ... rest of bootstrap`}}function qe(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 Je(e){let t=[`src/env.ts`,`src/env/index.ts`,`src/config/env.ts`,`src/config/index.ts`].map(t=>m(e.cwd,t)).filter(e=>_(e)).filter(e=>/\bloadEnv\s*\(/.test(Z(e)??``));if(t.length===0)return null;let n=[`src/index.ts`,`src/main.ts`].map(t=>m(e.cwd,t)).find(e=>_(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=Z(n)??``,i=p(n),a=[];for(let e of t){let t=h(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+)?['"]${Re(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=>h(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 \`${h(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 ${h(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 $(e,t=Ye){let n=0,r=0,i=[e];for(;i.length>0&&r<t;){let e=i.pop(),a;try{a=y(e,{withFileTypes:!0})}catch{continue}for(let o of a){if(r>=t)break;r++;let a=m(e,o.name);if(o.isDirectory()){i.push(a);continue}try{let e=b(a).mtimeMs;e>n&&(n=e)}catch{}}}return n}const Ye=2e3;function Xe(e){let t=m(e.cwd,`.kickjs`,`types`);if(!_(t))return null;let n=$(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 Ze=[()=>ze(),Be,Ve,Ue,We,Ke,qe,Je,Xe];async function Qe(e,t={}){let n={cwd:e,pkg:X(m(e,`package.json`)),tsconfig:Ie(e),runtime:t.runtime??`express`},r=[...Ze,...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 $e(e){switch(e){case`pass`:return o.green(`✔`);case`warn`:return o.yellow(`⚠`);case`fail`:return o.red(`✖`)}}function et(e){let t=$e(e.status),n=e.message?` ${o.dim(`(${e.message})`)}`:``;return`${t} ${e.name}${n}`}function tt(e){return e.split(`
858
+ `).map(e=>` ${o.dim(`→`)} ${e}`).join(`
859
+ `)}function nt(e){return e?.doctor?.checks??[]}function rt(e){e.command(`doctor`).description(`Pre-flight checks for your KickJS project (dev environment health)`).action(async()=>{let e=process.cwd(),t=nt(await l(e)),a=await c(e);r(`KickJS Doctor`);let s=await Qe(e,{extraChecks:t,runtime:a});for(let e of s)i.message(et(e)),e.fix&&e.status!==`pass`&&i.message(tt(e.fix));let u=s.filter(e=>e.status===`pass`).length,d=s.filter(e=>e.status===`warn`).length,f=s.filter(e=>e.status===`fail`).length,p=[o.green(`${u} passed`),d>0?o.yellow(`${d} warning${d===1?``:`s`}`):`${d} warnings`,f>0?o.red(`${f} error${f===1?``:`s`}`):`${f} errors`].join(`, `);f>0?(n(`${p} — fix the errors above before running the app`),process.exit(1)):n(d>0?`${p} — review the warnings`:o.green(`${p} — your environment looks good`))})}export{O as A,L as C,M as D,N as E,E as M,w as N,k as O,R as S,F as T,B as _,je as a,de as b,Ce as c,be as d,ye as f,he as g,G as h,Me as i,T as j,D as k,we as l,_e as m,Pe as n,K as o,ve as p,rt as r,te as s,Fe as t,xe as u,H as v,I as w,z as x,fe as y};
860
+ //# sourceMappingURL=doctor-BJWjI_wt.mjs.map