@forinda/kickjs-cli 8.10.2 → 8.12.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 (90) hide show
  1. package/dist/{agent-docs-As_KjQBC.mjs → agent-docs-CuD2-X3w.mjs} +30 -26
  2. package/dist/{agent-docs-ZgfnGL-j.mjs → agent-docs-ykxoPqXX.mjs} +23 -19
  3. package/dist/agent-docs-ykxoPqXX.mjs.map +1 -0
  4. package/dist/{build-BfC9Kkt-.mjs → build-C-AsmCh1.mjs} +2 -2
  5. package/dist/{build-BuBCsnzr.mjs → build-DQnV0GCD.mjs} +3 -3
  6. package/dist/{build-BuBCsnzr.mjs.map → build-DQnV0GCD.mjs.map} +1 -1
  7. package/dist/{builtins-DXASTS7y.mjs → builtins-BsYCQU80.mjs} +2 -2
  8. package/dist/{builtins-397XZB9_.mjs → builtins-Cjp9FT1m.mjs} +392 -185
  9. package/dist/cli.mjs +2 -2
  10. package/dist/config-DMFwBdWg.mjs +12 -0
  11. package/dist/config-DbnBakh8.mjs +13 -0
  12. package/dist/config-DbnBakh8.mjs.map +1 -0
  13. package/dist/{doctor-DGWvBOvj.mjs → doctor-rEbeoAoO.mjs} +282 -75
  14. package/dist/doctor-rEbeoAoO.mjs.map +1 -0
  15. package/dist/{fullstack-x16HfCRz.mjs → fullstack-DdSScENL.mjs} +7 -7
  16. package/dist/{fullstack-BqYdhTDI.mjs → fullstack-XhuwRQEb.mjs} +8 -8
  17. package/dist/{fullstack-BqYdhTDI.mjs.map → fullstack-XhuwRQEb.mjs.map} +1 -1
  18. package/dist/index.d.mts +29 -7
  19. package/dist/index.d.mts.map +1 -1
  20. package/dist/index.mjs +2 -2
  21. package/dist/package-manager-C8ekNwsa.mjs +11 -0
  22. package/dist/package-manager-DvXi-NGm.mjs +12 -0
  23. package/dist/package-manager-DvXi-NGm.mjs.map +1 -0
  24. package/dist/{plugin-HazJu53p.mjs → plugin--9hxxgsD.mjs} +2 -2
  25. package/dist/{plugin-Dh0LYWl5.mjs → plugin-CvlUt9NG.mjs} +2 -2
  26. package/dist/project-BN1DnrN3.mjs +134 -0
  27. package/dist/project-BN1DnrN3.mjs.map +1 -0
  28. package/dist/project-DBmrFgdd.mjs +133 -0
  29. package/dist/{project-root-DGKTJDH3.mjs → project-root-BXmtd-te.mjs} +2 -2
  30. package/dist/{project-root-DlgCWwc_.mjs → project-root-FMt83NHV.mjs} +3 -3
  31. package/dist/{project-root-DlgCWwc_.mjs.map → project-root-FMt83NHV.mjs.map} +1 -1
  32. package/dist/{prompts-D4nwf5mP.mjs → prompts-DJBDNwHt.mjs} +2 -2
  33. package/dist/{prompts-D4nwf5mP.mjs.map → prompts-DJBDNwHt.mjs.map} +1 -1
  34. package/dist/{rolldown-runtime-7c5UUG6D.mjs → rolldown-runtime-C4d34LEE.mjs} +1 -1
  35. package/dist/{run-plugins-Dx-QENlm.mjs → run-plugins-9tjbMqS6.mjs} +87 -87
  36. package/dist/run-plugins-9tjbMqS6.mjs.map +1 -0
  37. package/dist/typegen-AGdhxyRc.mjs +107 -0
  38. package/dist/typegen-AGdhxyRc.mjs.map +1 -0
  39. package/dist/typegen-Lf0UPhdl.mjs +106 -0
  40. package/dist/{types-CtrnAiNl.mjs → types-B5dY2mTz.mjs} +1 -1
  41. package/package.json +3 -3
  42. package/templates/base/files/README.md +2 -2
  43. package/templates/base/files/kick.config.ts +1 -1
  44. package/templates/base/files/src/modules/hello/hello.controller.ts +0 -5
  45. package/templates/base/files/src/modules/hello/hello.service.ts +0 -4
  46. package/templates/base/files/tsconfig.json +3 -1
  47. package/templates/feature-devtools/feature.json +2 -6
  48. package/templates/feature-devtools/files/src/devtools.d.ts +3 -0
  49. package/templates/feature-queue/feature.json +39 -3
  50. package/templates/feature-queue/files/_dot_env.example.append +4 -2
  51. package/templates/feature-queue/files/src/adapters/queue.ts +16 -0
  52. package/templates/feature-storage/feature.json +1 -1
  53. package/templates/schema-valibot/files/src/config/index.ts +5 -2
  54. package/templates/schema-yup/files/src/config/index.ts +5 -2
  55. package/templates/schema-zod/files/src/config/index.ts +5 -2
  56. package/templates/template-inertia/feature.json +9 -1
  57. package/templates/template-inertia/files/_dot_env.append +2 -0
  58. package/templates/template-inertia/files/_dot_env.example.append +6 -2
  59. package/templates/template-inertia/files/_dot_env.test.append +1 -0
  60. package/templates/template-inertia/files/_dot_env.test.example.append +1 -0
  61. package/templates/template-inertia/files/src/client/Layout.tsx +2 -0
  62. package/templates/template-inertia/files/src/client/Seo.tsx +35 -0
  63. package/templates/template-inertia/files/src/index.ts +11 -5
  64. package/templates/template-inertia/files/src/modules/index.ts +2 -1
  65. package/templates/template-inertia/files/src/modules/pages/pages.controller.ts +18 -6
  66. package/templates/template-inertia/files/src/modules/pages/pages.test.ts +34 -0
  67. package/templates/template-inertia/files/src/modules/seo/seo.controller.ts +19 -0
  68. package/templates/template-inertia/files/src/modules/seo/seo.module.ts +12 -0
  69. package/templates/template-inertia/files/tsconfig.json +3 -1
  70. package/templates/template-inertia/files/vite.config.ts +3 -2
  71. package/templates/web-kick/files/web/_dot_oxfmtrc.json +9 -0
  72. package/templates/web-kick/files/web/src/App.tsx +1 -7
  73. package/dist/agent-docs-ZgfnGL-j.mjs.map +0 -1
  74. package/dist/config-Ck9aQVpE.mjs +0 -12
  75. package/dist/config-CsOfUFfq.mjs +0 -13
  76. package/dist/config-CsOfUFfq.mjs.map +0 -1
  77. package/dist/doctor-DGWvBOvj.mjs.map +0 -1
  78. package/dist/package-manager--IUl91nq.mjs +0 -11
  79. package/dist/package-manager-DBmD5nSr.mjs +0 -12
  80. package/dist/package-manager-DBmD5nSr.mjs.map +0 -1
  81. package/dist/project-C98JovY4.mjs +0 -115
  82. package/dist/project-C98JovY4.mjs.map +0 -1
  83. package/dist/project-TbTVQCnh.mjs +0 -114
  84. package/dist/run-plugins-Dx-QENlm.mjs.map +0 -1
  85. package/dist/typegen-DJQ6WBwr.mjs +0 -106
  86. package/dist/typegen-DSaawXaQ.mjs +0 -107
  87. package/dist/typegen-DSaawXaQ.mjs.map +0 -1
  88. package/templates/feature-queue/files/_dot_env.append +0 -2
  89. package/templates/template-inertia/files/public/robots.txt +0 -4
  90. /package/templates/feature-storage/files/src/{storage.ts → adapters/storage.ts} +0 -0
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs-cli v8.10.2
2
+ * @forinda/kickjs-cli v8.12.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * @license MIT
10
10
  */
11
- import{i as e,o as t,t as n}from"./package-manager-DBmD5nSr.mjs";import{a as r,n as i,r as a,t as o,u as s}from"./prompts-D4nwf5mP.mjs";import{s as c}from"./config-CsOfUFfq.mjs";import{c as l,i as u}from"./project-C98JovY4.mjs";import{t as d}from"./project-root-DlgCWwc_.mjs";import{createRequire as f}from"node:module";import p,{dirname as m,join as h,relative as g,resolve as _}from"node:path";import{existsSync as v,readFileSync as y,readdirSync as b,statSync as x}from"node:fs";import{readFile as S,writeFile as C}from"node:fs/promises";import w from"pluralize";import{pathToFileURL as ee}from"node:url";import{defineGenerator as te}from"@forinda/kickjs-cli-kit";function T(e){return e.replace(/[-_\s]+(.)?/g,(e,t)=>t?t.toUpperCase():``).replace(/^(.)/,e=>e.toUpperCase())}function E(e){let t=T(e);return t.charAt(0).toLowerCase()+t.slice(1)}function D(e){return e.replace(/([a-z])([A-Z])/g,`$1-$2`).replace(/[\s_]+/g,`-`).toLowerCase()}function O(e){return w.plural(e)}function k(e){return w.plural(e)}function A(e){return e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`)}const ne={inmemory:`in-memory`,drizzle:`Drizzle`,prisma:`Prisma`};function re(e){return e.charAt(0).toUpperCase()+e.slice(1).replace(/-([a-z])/g,(e,t)=>t.toUpperCase())}function ie(e){return ne[e]??re(e)}function ae(e){return`create${e}Repository`}function j(e){return e??`define`}function M(e){let{pascal:t,kebab:n,plural:r=``,repo:i,style:a}=e,o=ae(t),s=j(a),c=`/**
11
+ import{i as e,o as t,t as n}from"./package-manager-DvXi-NGm.mjs";import{a as r,n as i,r as a,t as o,u as s}from"./prompts-DJBDNwHt.mjs";import{s as c}from"./config-DbnBakh8.mjs";import{c as l,i as u}from"./project-BN1DnrN3.mjs";import{t as d}from"./project-root-FMt83NHV.mjs";import{createRequire as f}from"node:module";import p,{dirname as m,join as h,relative as g,resolve as _}from"node:path";import{existsSync as v,readFileSync as y,readdirSync as b,statSync as x}from"node:fs";import{readFile as S}from"node:fs/promises";import C from"pluralize";import{pathToFileURL as w}from"node:url";import{defineGenerator as ee}from"@forinda/kickjs-cli-kit";C.addUncountableRule(`auth`);function T(e){return e.replace(/[-_\s]+(.)?/g,(e,t)=>t?t.toUpperCase():``).replace(/^(.)/,e=>e.toUpperCase())}function E(e){let t=T(e);return t.charAt(0).toLowerCase()+t.slice(1)}function D(e){return e.replace(/([a-z])([A-Z])/g,`$1-$2`).replace(/[\s_]+/g,`-`).toLowerCase()}function O(e){return C.plural(e)}function k(e){return C.plural(e)}function A(e){return e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`)}const te={inmemory:`in-memory`,drizzle:`Drizzle`,prisma:`Prisma`};function ne(e){return e.charAt(0).toUpperCase()+e.slice(1).replace(/-([a-z])/g,(e,t)=>t.toUpperCase())}function re(e){return te[e]??ne(e)}function ie(e){return`create${e}Repository`}function j(e){return e??`define`}function M(e){let{pascal:t,kebab:n,plural:r=``,repo:i,style:a}=e,o=e.routePath??r,s=ie(t),c=j(a),l=`/**
12
12
  * ${t} Module
13
13
  *
14
14
  * REST module with a flat folder structure.
@@ -21,10 +21,10 @@ import{i as e,o as t,t as n}from"./package-manager-DBmD5nSr.mjs";import{a as r,n
21
21
  * dtos/ — Request/response schemas
22
22
  *
23
23
  * The repository is backed by an in-memory Map so this module works as
24
- * generated. Swap in ${i===`inmemory`?`a database`:ie(i)} by replacing the factory body in
24
+ * generated. Swap in ${i===`inmemory`?`a database`:re(i)} by replacing the factory body in
25
25
  * ${n}.repository.ts — the contract is whatever that factory returns, so
26
26
  * nothing else has to change.
27
- */`,l=`import { ${t.toUpperCase()}_REPOSITORY, ${o} } from './${n}.repository'
27
+ */`,u=`import { ${t.toUpperCase()}_REPOSITORY, ${s} } from './${n}.repository'
28
28
  import { ${t}Controller } from './${n}.controller'
29
29
 
30
30
  // Eagerly load every module file so decorators (@Controller / @Service /
@@ -33,7 +33,7 @@ import { ${t}Controller } from './${n}.controller'
33
33
  // to emit, so a hand-written \`*.usecase.ts\` or \`*.policy.ts\` silently never
34
34
  // registered and failed later as \`No provider for X\` (#609). Recursive (./**/)
35
35
  // so nesting keeps working.
36
- import.meta.glob(['./**/*.ts', '!./**/*.test.ts', '!./**/*.d.ts'], { eager: true })`,u=` /**
36
+ import.meta.glob(['./**/*.ts', '!./**/*.test.ts', '!./**/*.d.ts'], { eager: true })`,d=` /**
37
37
  * Declare HTTP routes for this module. Return value shape:
38
38
  *
39
39
  * - \`path\` — URL prefix for this route set.
@@ -44,49 +44,49 @@ import.meta.glob(['./**/*.ts', '!./**/*.test.ts', '!./**/*.d.ts'], { eager: true
44
44
  * surfaces, side-by-side v1 + v2 controllers, etc:
45
45
  *
46
46
  * return [
47
- * { path: '/${r}', version: 1, controller: ${t}V1Controller },
48
- * { path: '/${r}', version: 2, controller: ${t}V2Controller },
47
+ * { path: '/${o}', version: 1, controller: ${t}V1Controller },
48
+ * { path: '/${o}', version: 2, controller: ${t}V2Controller },
49
49
  * ]
50
- */`;return s===`class`?`${c}
50
+ */`;return c===`class`?`${l}
51
51
  import { Container, type AppModule, type ModuleRoutes } from '@forinda/kickjs'
52
- ${l}
52
+ ${u}
53
53
 
54
54
  export class ${t}Module implements AppModule {
55
55
  register(container: Container): void {
56
- container.registerFactory(${t.toUpperCase()}_REPOSITORY, () => ${o}())
56
+ container.registerFactory(${t.toUpperCase()}_REPOSITORY, () => ${s}())
57
57
  }
58
58
 
59
- ${u.replace(/^ {4}/gm,` `).replace(/^ {6}/gm,` `)}
59
+ ${d.replace(/^ {4}/gm,` `).replace(/^ {6}/gm,` `)}
60
60
  routes(): ModuleRoutes {
61
61
  return {
62
- path: '/${r}',
62
+ path: '/${o}',
63
63
  controller: ${t}Controller,
64
64
  }
65
65
  }
66
66
  }
67
- `:`${c}
67
+ `:`${l}
68
68
  import { defineModule } from '@forinda/kickjs'
69
- ${l}
69
+ ${u}
70
70
 
71
71
  export const ${t}Module = defineModule({
72
72
  name: '${t}Module',
73
73
  build: () => ({
74
74
  register(container) {
75
75
  container.registerFactory(${t.toUpperCase()}_REPOSITORY, () =>
76
- ${o}(),
76
+ ${s}(),
77
77
  )
78
78
  },
79
79
 
80
- ${u}
80
+ ${d}
81
81
  routes() {
82
82
  return {
83
- path: '/${r}',
83
+ path: '/${o}',
84
84
  controller: ${t}Controller,
85
85
  }
86
86
  },
87
87
  }),
88
88
  })
89
- `}function oe(e){let{pascal:t,kebab:n,plural:r=``,style:i}=e,a=j(i),o=` /**
89
+ `}function ae(e){let{pascal:t,kebab:n,plural:r=``,style:i}=e,a=e.routePath??r,o=j(i),s=` /**
90
90
  * Declare HTTP routes. Return value shape:
91
91
  *
92
92
  * - \`path\` — URL prefix for this route set.
@@ -96,17 +96,17 @@ ${u}
96
96
  * Return an array to mount multiple route sets:
97
97
  *
98
98
  * return [
99
- * { path: '/${r}', version: 1, controller: ${t}V1Controller },
100
- * { path: '/${r}', version: 2, controller: ${t}V2Controller },
99
+ * { path: '/${a}', version: 1, controller: ${t}V1Controller },
100
+ * { path: '/${a}', version: 2, controller: ${t}V2Controller },
101
101
  * ]
102
- */`;return a===`class`?`import { type AppModule, type ModuleRoutes } from '@forinda/kickjs'
102
+ */`;return o===`class`?`import { type AppModule, type ModuleRoutes } from '@forinda/kickjs'
103
103
  import { ${t}Controller } from './${n}.controller'
104
104
 
105
105
  export class ${t}Module implements AppModule {
106
- ${o.replace(/^ {4}/gm,` `).replace(/^ {6}/gm,` `)}
106
+ ${s.replace(/^ {4}/gm,` `).replace(/^ {6}/gm,` `)}
107
107
  routes(): ModuleRoutes {
108
108
  return {
109
- path: '/${r}',
109
+ path: '/${a}',
110
110
  controller: ${t}Controller,
111
111
  }
112
112
  }
@@ -117,10 +117,10 @@ import { ${t}Controller } from './${n}.controller'
117
117
  export const ${t}Module = defineModule({
118
118
  name: '${t}Module',
119
119
  build: () => ({
120
- ${o}
120
+ ${s}
121
121
  routes() {
122
122
  return {
123
- path: '/${r}',
123
+ path: '/${a}',
124
124
  controller: ${t}Controller,
125
125
  }
126
126
  },
@@ -131,7 +131,7 @@ ${o}
131
131
  Controller,
132
132
  Get,
133
133
  Post,
134
- Put,
134
+ Patch,
135
135
  Delete,
136
136
  Autowired,
137
137
  ApiQueryParams,
@@ -157,10 +157,18 @@ import { ${t.toUpperCase()}_QUERY_CONFIG } from './${n}.constants'
157
157
  // \`ctx.problem.*\` for error branches — those send immediately and return
158
158
  // nothing, so they stay out of the success type.
159
159
 
160
+ // Require a signed-in user for these routes: https://kickjs.app/guide/authentication
160
161
  @Controller()
161
162
  export class ${t}Controller {
162
163
  @Autowired() private readonly ${a}Service!: ${t}Service
163
164
 
165
+ // Offset pages (?page=&limit=). For keyset pages (?cursor=&limit=) — stable
166
+ // under concurrent inserts, no deep OFFSET scans — swap the body for:
167
+ // return ctx.paginateCursor(
168
+ // (q) => this.${a}Service.findAfter({ ...q, limit: q.limit + 1 }),
169
+ // ${t.toUpperCase()}_QUERY_CONFIG,
170
+ // )
171
+ // https://kickjs.app/guide/query-parsing#cursor-pagination
164
172
  @Get('/')
165
173
  ${i} @ApiQueryParams(${t.toUpperCase()}_QUERY_CONFIG)
166
174
  async list(ctx: Ctx<KickRoutes.${t}Controller['list']>) {
@@ -184,10 +192,14 @@ ${i} async getById(ctx: Ctx<KickRoutes.${t}Controller['getById']>) {
184
192
 
185
193
  @Post('/', { body: create${t}Schema, name: 'Create${t}' })
186
194
  ${i} async create(ctx: Ctx<KickRoutes.${t}Controller['create']>) {
187
- return reply.created(await this.${a}Service.create(ctx.body))
195
+ const created = await this.${a}Service.create(ctx.body)
196
+ // 201 + Location: where the new ${n} lives, built from the getById route.
197
+ return reply.created(created, {
198
+ location: ctx.urlFor('${t}Controller.getById', { id: created.id }),
199
+ })
188
200
  }
189
201
 
190
- @Put('/:id', { body: update${t}Schema, name: 'Update${t}' })
202
+ @Patch('/:id', { body: update${t}Schema, name: 'Update${t}' })
191
203
  ${i} async update(ctx: Ctx<KickRoutes.${t}Controller['update']>) {
192
204
  return this.${a}Service.update(ctx.params.id, ctx.body)
193
205
  }
@@ -201,7 +213,7 @@ ${i} async remove(ctx: Ctx<KickRoutes.${t}Controller['remove']>) {
201
213
  `}const P={zod:{import:`import { z } from 'zod'`,required:`z.string().min(1, { error: 'Name is required' }).max(200)`,optional:`z.string().min(1).max(200).optional()`,infer:e=>`z.infer<typeof ${e}>`,hint:[` * z.string(), z.number(), z.boolean(), z.enum([...]),`,` * z.array(), z.object(), .optional(), .default(), .transform()`].join(`
202
214
  `)},valibot:{import:`import * as v from 'valibot'`,required:`v.pipe(v.string(), v.minLength(1, 'Name is required'), v.maxLength(200))`,optional:`v.optional(v.pipe(v.string(), v.minLength(1), v.maxLength(200)))`,infer:e=>`v.InferOutput<typeof ${e}>`,hint:[` * v.string(), v.number(), v.boolean(), v.picklist([...]),`,` * v.array(), v.object(), v.optional(), and v.pipe(...) to chain checks`].join(`
203
215
  `)},yup:{import:`import * as yup from 'yup'`,required:`yup.string().required('Name is required').min(1).max(200)`,optional:`yup.string().min(1).max(200).optional()`,infer:e=>`yup.InferType<typeof ${e}>`,hint:[` * yup.string(), yup.number(), yup.boolean(), .oneOf([...]),`,` * yup.array(), yup.object(), .required(), .optional(), .default()`].join(`
204
- `)}};function F(e,t){return`${e===`zod`?`z.object`:e===`valibot`?`v.object`:`yup.object`}({\n ${t},\n})`}function se(e){let{pascal:t}=e,n=e.schemaLib??`zod`,r=P[n],i=`create${t}Schema`,a=n===`zod`?`Zod`:n===`valibot`?`Valibot`:`Yup`;return`${r.import}
216
+ `)}};function F(e,t){return`${e===`zod`?`z.object`:e===`valibot`?`v.object`:`yup.object`}({\n ${t},\n})`}function oe(e){let{pascal:t}=e,n=e.schemaLib??`zod`,r=P[n],i=`create${t}Schema`,a=n===`zod`?`Zod`:n===`valibot`?`Valibot`:`Yup`;return`${r.import}
205
217
 
206
218
  /**
207
219
  * Create ${t} DTO — ${a} schema for validating POST request bodies.
@@ -214,12 +226,12 @@ ${r.hint}
214
226
  export const ${i} = ${F(n,`name: ${r.required}`)}
215
227
 
216
228
  export type Create${t}DTO = ${r.infer(i)}
217
- `}function ce(e){let{pascal:t}=e,n=e.schemaLib??`zod`,r=P[n],i=`update${t}Schema`;return`${r.import}
229
+ `}function se(e){let{pascal:t}=e,n=e.schemaLib??`zod`,r=P[n],i=`update${t}Schema`;return`${r.import}
218
230
 
219
231
  export const ${i} = ${F(n,`name: ${r.optional}`)}
220
232
 
221
233
  export type Update${t}DTO = ${r.infer(i)}
222
- `}function le(e){let{pascal:t}=e;return`export interface ${t}ResponseDTO {
234
+ `}function ce(e){let{pascal:t}=e;return`export interface ${t}ResponseDTO {
223
235
  id: string
224
236
  name: string
225
237
  createdAt: string
@@ -227,6 +239,22 @@ export type Update${t}DTO = ${r.infer(i)}
227
239
  }
228
240
  `}function I(e){let{pascal:t,kebab:n,repoType:r,dtoPrefix:i=`./dtos`,tokenScope:a=`app`}=e,o=r&&r!==`inmemory`?r:``,s=`create${t}Repository`,c=` const store = new Map<string, ${t}ResponseDTO>()
229
241
 
242
+ /**
243
+ * Filtered, searched and sorted. \`byId\` breaks sort-key ties by id — what a
244
+ * cursor needs; without it ties (and an unsorted list) keep insertion order.
245
+ */
246
+ function select(parsed: Omit<ParsedQuery, 'pagination'>, byId = false): ${t}ResponseDTO[] {
247
+ // The parser already limited fields to ${t.toUpperCase()}_QUERY_CONFIG.
248
+ const q = parsed.search.toLowerCase()
249
+ return [...store.values()]
250
+ .filter(
251
+ (row) =>
252
+ parsed.filters.every((f) => matches(field(row, f.field), f)) &&
253
+ (!q || SEARCHABLE.some((name) => String(field(row, name) ?? '').toLowerCase().includes(q))),
254
+ )
255
+ .sort((a, b) => order(a, b, parsed.sort, byId))
256
+ }
257
+
230
258
  return {
231
259
  async findById(id: string): Promise<${t}ResponseDTO | null> {
232
260
  return store.get(id) ?? null
@@ -239,9 +267,28 @@ export type Update${t}DTO = ${r.infer(i)}
239
267
  async findPaginated(
240
268
  parsed: ParsedQuery,
241
269
  ): Promise<{ data: ${t}ResponseDTO[]; total: number }> {
242
- const all = [...store.values()]
270
+ const rows = select(parsed)
243
271
  const { offset, limit } = parsed.pagination
244
- return { data: all.slice(offset, offset + limit), total: all.length }
272
+ return { data: rows.slice(offset, offset + limit), total: rows.length }
273
+ },
274
+
275
+ /**
276
+ * Keyset page for \`ctx.paginateCursor\`: the rows strictly after
277
+ * \`cursor\` in sort order (id breaks ties), at most \`limit\` of them —
278
+ * the caller asks for one more than it shows, to learn whether a next
279
+ * page exists. A database does the same with
280
+ * \`WHERE (sortField, id) > (cursor.sortField, cursor.id)\`.
281
+ */
282
+ async findAfter(
283
+ query: Omit<ParsedQuery, 'pagination'> & {
284
+ cursor: Readonly<Record<string, unknown>> | null
285
+ limit: number
286
+ },
287
+ ): Promise<${t}ResponseDTO[]> {
288
+ const { cursor, limit, sort } = query
289
+ const rows = select(query, true)
290
+ const after = cursor ? rows.filter((row) => order(row, cursor, sort, true) > 0) : rows
291
+ return after.slice(0, limit)
245
292
  },
246
293
 
247
294
  async create(dto: Create${t}DTO): Promise<${t}ResponseDTO> {
@@ -277,11 +324,71 @@ export type Update${t}DTO = ${r.infer(i)}
277
324
  */
278
325
  import { randomUUID } from 'node:crypto'
279
326
  import { createToken, HttpException } from '@forinda/kickjs'
280
- import type { ParsedQuery } from '@forinda/kickjs'
327
+ import type { FilterItem, ParsedQuery, SortItem } from '@forinda/kickjs'
328
+ import { ${t.toUpperCase()}_QUERY_CONFIG } from './${n}.constants'
281
329
  import type { ${t}ResponseDTO } from '${i}/${n}-response.dto'
282
330
  import type { Create${t}DTO } from '${i}/create-${n}.dto'
283
331
  import type { Update${t}DTO } from '${i}/update-${n}.dto'
284
332
 
333
+ const SEARCHABLE = (${t.toUpperCase()}_QUERY_CONFIG.searchable ?? []) as string[]
334
+
335
+ function field(row: object, name: string): unknown {
336
+ return (row as Record<string, unknown>)[name]
337
+ }
338
+
339
+ /**
340
+ * A stored number compares by value (the query's value is text); everything
341
+ * else, ISO dates included, as text — so a string field keeps '001' ≠ '1'.
342
+ */
343
+ function compare(stored: unknown, other: unknown): number {
344
+ if (typeof stored === 'number') return stored - Number(other)
345
+ return String(stored ?? '').localeCompare(String(other ?? ''))
346
+ }
347
+
348
+ /**
349
+ * Sort order of two rows (or a row and a cursor position): each sort field in
350
+ * turn, then — with \`byId\` — id, in the direction of the last sort field.
351
+ */
352
+ function order(a: object, b: object, sort: readonly SortItem[], byId: boolean): number {
353
+ for (const { field: name, direction } of sort) {
354
+ const result = compare(field(a, name), field(b, name))
355
+ if (result) return direction === 'desc' ? -result : result
356
+ }
357
+ if (!byId) return 0
358
+ const result = compare(field(a, 'id'), field(b, 'id'))
359
+ return sort.at(-1)?.direction === 'desc' ? -result : result
360
+ }
361
+
362
+ /** One \`?filter=field:op:value\` — \`in\` and \`between\` take comma-separated values. */
363
+ function matches(value: unknown, { operator, value: raw }: FilterItem): boolean {
364
+ const text = String(value ?? '').toLowerCase()
365
+ const list = raw.split(',')
366
+ switch (operator) {
367
+ case 'eq':
368
+ return compare(value, raw) === 0
369
+ case 'neq':
370
+ return compare(value, raw) !== 0
371
+ case 'gt':
372
+ return compare(value, raw) > 0
373
+ case 'gte':
374
+ return compare(value, raw) >= 0
375
+ case 'lt':
376
+ return compare(value, raw) < 0
377
+ case 'lte':
378
+ return compare(value, raw) <= 0
379
+ case 'between':
380
+ return compare(value, list[0]) >= 0 && compare(value, list[1]) <= 0
381
+ case 'in':
382
+ return list.some((v) => compare(value, v) === 0)
383
+ case 'contains':
384
+ return text.includes(raw.toLowerCase())
385
+ case 'starts':
386
+ return text.startsWith(raw.toLowerCase())
387
+ case 'ends':
388
+ return text.endsWith(raw.toLowerCase())
389
+ }
390
+ }
391
+
285
392
  export function ${s}() {
286
393
  ${c}
287
394
  }
@@ -300,7 +407,7 @@ export type ${t}Repository = ReturnType<typeof ${s}>
300
407
  * adopters must NOT use the reserved \`'kick/'\` namespace.
301
408
  */
302
409
  export const ${t.toUpperCase()}_REPOSITORY = createToken<${t}Repository>('${a}/${t}/repository')
303
- `}function ue(e){let{pascal:t,kebab:n,plural:r=``,style:i=`define`,testHarness:a=!1}=e,o=i===`define`?`${t}Module()`:`${t}Module`;return`${a?`import { describe, it, expect, beforeEach } from 'vitest'
410
+ `}function le(e){let{pascal:t,kebab:n,plural:r=``,style:i=`define`,testHarness:a=!1,testImport:o=`vitest`}=e,s=e.routePath??r,c=i===`define`?`${t}Module()`:`${t}Module`;return`${a?`import { describe, it, expect, beforeEach } from '${o}'
304
411
  import request from 'supertest'
305
412
  import { Container } from '@forinda/kickjs'
306
413
  import { createTestApp } from '@forinda/kickjs-testing'
@@ -316,11 +423,11 @@ import { ${t}Module } from '../${n}.module'
316
423
  * pass the same values in \`boot()\` below and update this to match, so the test
317
424
  * exercises the paths production actually serves:
318
425
  *
319
- * defaultVersion: false → '/api/${r}'
320
- * apiPrefix: '/v1' → '/v1/v1/${r}'
426
+ * defaultVersion: false → '/api/${s}'
427
+ * apiPrefix: '/v1' → '/v1/v1/${s}'
321
428
  */
322
- const BASE = '/api/v1/${r}'
323
- `:`import { describe, it, beforeEach } from 'vitest'
429
+ const BASE = '/api/v1/${s}'
430
+ `:`import { describe, it, beforeEach } from '${o}'
324
431
  import { Container } from '@forinda/kickjs'
325
432
 
326
433
  // Install \`@forinda/kickjs-testing\` and \`supertest\` to boot this module in a
@@ -342,7 +449,7 @@ ${a?` /**
342
449
  */
343
450
  async function boot() {
344
451
  const { app, container } = await createTestApp({
345
- modules: [${o}],
452
+ modules: [${c}],
346
453
  // Mirror your bootstrap() here if it overrides either, and update BASE:
347
454
  // apiPrefix: '/api',
348
455
  // defaultVersion: 1,
@@ -350,7 +457,7 @@ ${a?` /**
350
457
  return { app, container, agent: request(app.handle.bind(app)) }
351
458
  }
352
459
 
353
- describe('GET /${r}', () => {
460
+ describe('GET /${s}', () => {
354
461
  it('returns an empty page before anything is created', async () => {
355
462
  const { agent } = await boot()
356
463
  const res = await agent.get(BASE)
@@ -359,8 +466,18 @@ ${a?` /**
359
466
  expect(res.body).toMatchObject({ data: [], meta: { page: 1 } })
360
467
  })
361
468
 
469
+ it('filters and sorts through the query string', async () => {
470
+ const { agent } = await boot()
471
+ for (const name of ['Alpha', 'Beta', 'Gamma']) await agent.post(BASE).send({ name })
472
+ const res = await agent.get(\`\${BASE}?filter=name:neq:Beta&sort=name:desc\`)
473
+
474
+ expect(res.status).toBe(200)
475
+ expect(res.body.data.map((row: { name: string }) => row.name)).toEqual(['Gamma', 'Alpha'])
476
+ expect(res.body.meta.total).toBe(2)
477
+ })
478
+
362
479
  it.todo('returns created ${r}, paginated')
363
- })`:` describe('GET /${r}', () => {
480
+ })`:` describe('GET /${s}', () => {
364
481
  // Boot the module with createTestApp and assert { data, meta }.
365
482
  it.todo('returns an empty page before anything is created')
366
483
  it.todo('returns created ${r}, paginated')
@@ -370,38 +487,45 @@ ${a?` /**
370
487
  // reporter lists it as outstanding instead of counting it as a pass. Replace
371
488
  // the todo with a real test as you implement each endpoint.
372
489
 
373
- describe('POST /${r}', () => {
374
- // Post a valid body, assert 201 and the created ${n} in the response.
375
- it.todo('creates a ${n}')
490
+ describe('POST /${s}', () => {
491
+ ${a?` it('creates a ${n} and points Location at it', async () => {
492
+ const { agent } = await boot()
493
+ const res = await agent.post(BASE).send({ name: 'Test ${t}' })
494
+
495
+ expect(res.status).toBe(201)
496
+ expect(res.body).toMatchObject({ name: 'Test ${t}' })
497
+ expect(res.headers.location).toBe(\`\${BASE}/\${res.body.id}\`)
498
+ })`:` // Post a valid body, assert 201, the created ${n}, and its Location header.
499
+ it.todo('creates a ${n}')`}
376
500
  // Post an invalid body, assert 422 and the validation detail.
377
501
  it.todo('rejects an invalid body')
378
502
  })
379
503
 
380
- describe('GET /${r}/:id', () => {
504
+ describe('GET /${s}/:id', () => {
381
505
  // Create one, fetch it by id, assert the payload matches.
382
506
  it.todo('returns a ${n} by id')
383
507
  // Fetch an id that does not exist, assert 404.
384
508
  it.todo('returns 404 for an unknown id')
385
509
  })
386
510
 
387
- describe('PUT /${r}/:id', () => {
511
+ describe('PATCH /${s}/:id', () => {
388
512
  // Create, update, assert the change is reflected on a subsequent read.
389
513
  it.todo('updates an existing ${n}')
390
514
  })
391
515
 
392
- describe('DELETE /${r}/:id', () => {
516
+ describe('DELETE /${s}/:id', () => {
393
517
  // Create, delete, assert a subsequent read is 404.
394
518
  it.todo('deletes a ${n}')
395
519
  })
396
520
  })
397
- `}function de(e){let{pascal:t,kebab:n,plural:r=``,repoPrefix:i=`../${n}.repository`}=e,a=`create${t}Repository`;return`import { describe, it, expect, beforeEach } from 'vitest'
398
- import { ${a}, type ${t}Repository } from '${i}'
521
+ `}function ue(e){let{pascal:t,kebab:n,plural:r=``,repoPrefix:i=`../${n}.repository`,testImport:a=`vitest`}=e,o=`create${t}Repository`;return`import { describe, it, expect, beforeEach } from '${a}'
522
+ import { ${o}, type ${t}Repository } from '${i}'
399
523
 
400
524
  describe('${t} repository', () => {
401
525
  let repo: ${t}Repository
402
526
 
403
527
  beforeEach(() => {
404
- repo = ${a}()
528
+ repo = ${o}()
405
529
  })
406
530
 
407
531
  it('should create and retrieve a ${n}', async () => {
@@ -443,6 +567,83 @@ describe('${t} repository', () => {
443
567
  expect(result.total).toBe(3)
444
568
  })
445
569
 
570
+ it('should filter, search and sort before paginating', async () => {
571
+ await repo.create({ name: 'Banana' })
572
+ await repo.create({ name: 'apple' })
573
+ await repo.create({ name: 'Cherry' })
574
+ const pagination = { page: 1, limit: 10, offset: 0 }
575
+
576
+ const sorted = await repo.findPaginated({
577
+ filters: [],
578
+ sort: [{ field: 'name', direction: 'desc' }],
579
+ search: '',
580
+ pagination,
581
+ })
582
+ expect(sorted.data.map((row) => row.name)).toEqual(['Cherry', 'Banana', 'apple'])
583
+
584
+ const filtered = await repo.findPaginated({
585
+ filters: [{ field: 'name', operator: 'neq', value: 'apple' }],
586
+ sort: [],
587
+ search: '',
588
+ pagination: { page: 1, limit: 1, offset: 0 },
589
+ })
590
+ expect(filtered.data).toHaveLength(1)
591
+ expect(filtered.total).toBe(2)
592
+
593
+ const searched = await repo.findPaginated({ filters: [], sort: [], search: 'AN', pagination })
594
+ expect(searched.data.map((row) => row.name)).toEqual(['Banana'])
595
+ })
596
+
597
+ it('compares a text field as text, not as a number', async () => {
598
+ await repo.create({ name: '001' })
599
+ await repo.create({ name: '10' })
600
+ await repo.create({ name: '9' })
601
+ const pagination = { page: 1, limit: 10, offset: 0 }
602
+
603
+ const exact = await repo.findPaginated({
604
+ filters: [{ field: 'name', operator: 'eq', value: '1' }],
605
+ sort: [],
606
+ search: '',
607
+ pagination,
608
+ })
609
+ expect(exact.total).toBe(0)
610
+
611
+ const sorted = await repo.findPaginated({
612
+ filters: [],
613
+ sort: [{ field: 'name', direction: 'asc' }],
614
+ search: '',
615
+ pagination,
616
+ })
617
+ expect(sorted.data.map((row) => row.name)).toEqual(['001', '10', '9'])
618
+ })
619
+
620
+ it('pages through rows with findAfter, stable when sort keys tie', async () => {
621
+ // Three rows share 'a', so a page boundary falls inside the tie: only the
622
+ // id tie-breaker keeps the third 'a' from being skipped or repeated.
623
+ for (const name of ['b', 'a', 'b', 'a', 'a']) await repo.create({ name })
624
+ const query = { filters: [], sort: [{ field: 'name', direction: 'asc' as const }], search: '' }
625
+ const limit = 2
626
+
627
+ const pages: string[][] = []
628
+ const ids: string[] = []
629
+ let cursor: Record<string, unknown> | null = null
630
+ let hasNext = true
631
+ while (hasNext) {
632
+ // Ask for one more than a page, as ctx.paginateCursor's fetcher does.
633
+ const rows = await repo.findAfter({ ...query, cursor, limit: limit + 1 })
634
+ hasNext = rows.length > limit
635
+ const page = rows.slice(0, limit)
636
+ pages.push(page.map((row) => row.name))
637
+ ids.push(...page.map((row) => row.id))
638
+ const last = page.at(-1)
639
+ cursor = last ? { name: last.name, id: last.id } : null
640
+ }
641
+
642
+ expect(pages.map((page) => page.length)).toEqual([2, 2, 1])
643
+ expect(pages.flat()).toEqual(['a', 'a', 'a', 'b', 'b'])
644
+ expect(new Set(ids).size).toBe(5)
645
+ })
646
+
446
647
  it('should update a ${n}', async () => {
447
648
  const created = await repo.create({ name: 'Original' })
448
649
  const updated = await repo.update(created.id, { name: 'Updated' })
@@ -456,7 +657,7 @@ describe('${t} repository', () => {
456
657
  expect(found).toBeNull()
457
658
  })
458
659
  })
459
- `}function L(e){let{pascal:t,kebab:n}=e;return`import { Service, Inject, HttpException } from '@forinda/kickjs'
660
+ `}function L(e){let{pascal:t,kebab:n}=e;return`import { Service, Inject } from '@forinda/kickjs'
460
661
  import type { ParsedQuery } from '@forinda/kickjs'
461
662
  import { ${t.toUpperCase()}_REPOSITORY, type ${t}Repository } from './${n}.repository'
462
663
  import type { ${t}ResponseDTO } from './dtos/${n}-response.dto'
@@ -481,6 +682,10 @@ export class ${t}Service {
481
682
  return this.repo.findPaginated(parsed)
482
683
  }
483
684
 
685
+ async findAfter(query: Parameters<${t}Repository['findAfter']>[0]) {
686
+ return this.repo.findAfter(query)
687
+ }
688
+
484
689
  async create(dto: Create${t}DTO): Promise<${t}ResponseDTO> {
485
690
  return this.repo.create(dto)
486
691
  }
@@ -493,14 +698,14 @@ export class ${t}Service {
493
698
  await this.repo.delete(id)
494
699
  }
495
700
  }
496
- `}function R(e){let{pascal:t}=e;return`import type { QueryFieldConfig } from '@forinda/kickjs'
701
+ `}const de=/* @__PURE__ */ new Set([`string`,`text`,`email`,`url`]),fe=/* @__PURE__ */ new Set([`json`]);function R(e){let{pascal:t,fields:n=[{name:`name`,type:`string`}]}=e,r=n.filter(e=>!fe.has(e.type)).map(e=>e.name),i=n.filter(e=>de.has(e.type)).map(e=>e.name),a=e=>`[${e.map(e=>`'${e}'`).join(`, `)}]`;return`import type { QueryFieldConfig } from '@forinda/kickjs'
497
702
 
498
703
  export const ${t.toUpperCase()}_QUERY_CONFIG: QueryFieldConfig = {
499
- filterable: ['name'],
500
- sortable: ['name', 'createdAt'],
501
- searchable: ['name'],
704
+ filterable: ${a(r)},
705
+ sortable: ${a([...r.filter(e=>e!==`createdAt`),`createdAt`])},
706
+ searchable: ${a(i)},
502
707
  }
503
- `}async function fe(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, Post, Put, Delete, reply, type Ctx } from '@forinda/kickjs'
708
+ `}async function pe(e){let{pascal:t,kebab:n,plural:r,routePath:i,style:a,write:o}=e;await o(`${n}.module.ts`,ae({pascal:t,kebab:n,plural:r,routePath:i,style:a})),await o(`${n}.controller.ts`,`import { Controller, Get, Post, Patch, Delete, reply, type Ctx } from '@forinda/kickjs'
504
709
 
505
710
  // \`Ctx<KickRoutes.${t}Controller['<method>']>\` is generated by
506
711
  // \`kick typegen\` (auto-run on \`kick dev\`).
@@ -513,6 +718,7 @@ export const ${t.toUpperCase()}_QUERY_CONFIG: QueryFieldConfig = {
513
718
  // "Minimal" is about the file count, not the route surface — you get the whole
514
719
  // CRUD shape here in one file.
515
720
 
721
+ // Require a signed-in user for these routes: https://kickjs.app/guide/authentication
516
722
  @Controller()
517
723
  export class ${t}Controller {
518
724
  @Get('/')
@@ -530,7 +736,7 @@ export class ${t}Controller {
530
736
  return reply.created({ data: ctx.body })
531
737
  }
532
738
 
533
- @Put('/:id')
739
+ @Patch('/:id')
534
740
  async update(ctx: Ctx<KickRoutes.${t}Controller['update']>) {
535
741
  return { id: ctx.params.id, data: ctx.body }
536
742
  }
@@ -540,7 +746,7 @@ export class ${t}Controller {
540
746
  return reply.noContent()
541
747
  }
542
748
  }
543
- `)}async function pe(e){let{pascal:t,kebab:n,plural:r,pluralPascal:i,repo:a,noTests:o,tokenScope:s,style:c,testHarness:l,write:u}=e,d=e.swagger??!1,f=e.schemaLib??`zod`;await u(`${n}.module.ts`,M({pascal:t,kebab:n,plural:r,repo:a,style:c})),await u(`${n}.constants.ts`,R({pascal:t,kebab:n})),await u(`${n}.controller.ts`,N({pascal:t,kebab:n,plural:r,pluralPascal:i,swagger:d})),await u(`${n}.service.ts`,L({pascal:t,kebab:n})),await u(`dtos/create-${n}.dto.ts`,se({pascal:t,kebab:n,schemaLib:f})),await u(`dtos/update-${n}.dto.ts`,ce({pascal:t,kebab:n,schemaLib:f})),await u(`dtos/${n}-response.dto.ts`,le({pascal:t,kebab:n})),await u(`${n}.repository.ts`,I({pascal:t,kebab:n,repoType:a,dtoPrefix:`./dtos`,tokenScope:s})),o||(await u(`__tests__/${n}.controller.test.ts`,ue({pascal:t,kebab:n,plural:r,style:c,testHarness:l})),await u(`__tests__/${n}.repository.test.ts`,de({pascal:t,kebab:n,plural:r,repoPrefix:`../${n}.repository`})))}function me(e){return e?typeof e==`string`?e:e.name:`inmemory`}async function he(n){let{name:r,modulesDir:i,noEntity:c,noTests:l,repo:u=`inmemory`,force:d,dryRun:f}=n,p=n.pluralize!==!1,m=n.pattern??`rest`;n.minimal&&(m=`minimal`);let g=D(r),_=T(r),v=p?O(g):g,y=p?k(_):_,b=h(i,v),x=[],S=d??!1,C={kebab:g,pascal:_,plural:v,pluralPascal:y,moduleDir:b,repo:u,noEntity:c??!1,noTests:l??!1,tokenScope:n.tokenScope??`app`,swagger:n.swagger??!1,schemaLib:n.schemaLib??`zod`,testHarness:n.testHarness??!1,style:n.style??`define`,write:async(n,r)=>{let i=h(b,n);f?x.push(i):!S&&await e(i)&&!await o({message:`File exists: ${s.dim(n)}. Overwrite?`,initialValue:!1})?a.warn(`Skipped: ${n}`):(await t(i,r),x.push(i))},files:x};switch(m){case`minimal`:await fe(C);break;default:await pe(C)}return f||await z(i,_,v,g,C.style),x}async function z(n,r,i,a,o=`define`){let s=h(n,`index.ts`),c=await e(s),l=`./${i}/${a}.module`,u=o===`class`?`${r}Module`:`${r}Module()`;if(!c){let e=o===`class`?`import type { AppModuleEntry } from '@forinda/kickjs'
749
+ `)}async function me(e){let{pascal:t,kebab:n,plural:r,pluralPascal:i,routePath:a,repo:o,noTests:s,tokenScope:c,style:l,testHarness:u,testImport:d,write:f}=e,p=e.swagger??!1,m=e.schemaLib??`zod`;await f(`${n}.module.ts`,M({pascal:t,kebab:n,plural:r,routePath:a,repo:o,style:l})),await f(`${n}.constants.ts`,R({pascal:t,kebab:n})),await f(`${n}.controller.ts`,N({pascal:t,kebab:n,plural:r,pluralPascal:i,swagger:p})),await f(`${n}.service.ts`,L({pascal:t,kebab:n})),await f(`dtos/create-${n}.dto.ts`,oe({pascal:t,kebab:n,schemaLib:m})),await f(`dtos/update-${n}.dto.ts`,se({pascal:t,kebab:n,schemaLib:m})),await f(`dtos/${n}-response.dto.ts`,ce({pascal:t,kebab:n})),await f(`${n}.repository.ts`,I({pascal:t,kebab:n,repoType:o,dtoPrefix:`./dtos`,tokenScope:c})),s||(await f(`__tests__/${n}.controller.test.ts`,le({pascal:t,kebab:n,plural:r,routePath:a,style:l,testHarness:u,testImport:d})),await f(`__tests__/${n}.repository.test.ts`,ue({pascal:t,kebab:n,plural:r,repoPrefix:`../${n}.repository`,testImport:d})))}function he(e){return e?typeof e==`string`?e:e.name:`inmemory`}async function ge(n){let{name:r,modulesDir:i,noEntity:c,noTests:l,repo:u=`inmemory`,force:d,dryRun:f}=n,p=n.pluralize===!0,m=n.pattern??`rest`;n.minimal&&(m=`minimal`);let g=D(r),_=T(r),v=p?O(g):g,y=p?k(_):_,b=n.pluralizeRoutes===!1?g:O(g),x=h(i,v),S=[],C=d??!1,w={kebab:g,pascal:_,plural:v,pluralPascal:y,routePath:b,moduleDir:x,repo:u,noEntity:c??!1,noTests:l??!1,tokenScope:n.tokenScope??`app`,swagger:n.swagger??!1,schemaLib:n.schemaLib??`zod`,testHarness:n.testHarness??!1,testImport:n.testImport,style:n.style??`define`,write:async(n,r)=>{let i=h(x,n);f?S.push(i):!C&&await e(i)&&!await o({message:`File exists: ${s.dim(n)}. Overwrite?`,initialValue:!1})?a.warn(`Skipped: ${n}`):(await t(i,r),S.push(i))},files:S};switch(m){case`minimal`:await pe(w);break;default:await me(w)}return f||await z(i,_,v,g,w.style),S}async function z(n,r,i,a,o=`define`){let s=h(n,`index.ts`),c=await e(s),l=`./${i}/${a}.module`,u=o===`class`?`${r}Module`:`${r}Module()`;if(!c){let e=o===`class`?`import type { AppModuleEntry } from '@forinda/kickjs'
544
750
  import { ${r}Module } from '${l}'
545
751
 
546
752
  export const modules: AppModuleEntry[] = [${u}]
@@ -551,8 +757,8 @@ export const modules = defineModules().mount(${u})
551
757
  `;await t(s,e);return}let d=await S(s,`utf-8`),f=`import { ${r}Module } from '${l}'`,p=A(l);if(!RegExp(`^import\\s*\\{[^}]*\\b${A(r)}Module\\b[^}]*\\}\\s*from\\s*['"]${p}['"]`,`m`).test(d)){let e=d.lastIndexOf(`import `);if(e!==-1){let t=d.indexOf(`
552
758
  `,e);d=d.slice(0,t+1)+f+`
553
759
  `+d.slice(t+1)}else d=f+`
554
- `+d}let m=V(d);if(m){let e=d.slice(m.rhsStart,m.rhsEnd+1);RegExp(`\\b${A(r)}Module\\b`).test(e)||(d=B(d,u))}else d=B(d,u);await C(s,d,`utf-8`)}function B(e,t){let n=V(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 V(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=_e(e,n);return t===-1?null:{shape:`array`,rhsStart:n,rhsEnd:t}}if(e.slice(n,n+13)===`defineModules`){let t=ge(e,n);return t===-1?null:{shape:`chain`,rhsStart:n,rhsEnd:t-1,chainEnd:t}}return null}function ge(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=U(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=U(e,t);if(n===-1)break;i=n+1}return i}function H(e,t){let n=e.slice(t,t+2);if(n===`//`){for(t+=2;t<e.length&&e[t]!==`
555
- `;)t++;return t}if(n===`/*`){for(t+=2;t+1<e.length&&(e[t]!==`*`||e[t+1]!==`/`);)t++;return t+2}return t}function _e(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=H(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++}else{if(i===`[`)n++;else if(i===`]`&&(n--,n===0))return r;r++}}return-1}function U(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=H(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++}else{if(i===`(`)n++;else if(i===`)`&&(n--,n===0))return r;r++}}return-1}function W(e,t,n,r){return`/**
760
+ `+d}let m=V(d);if(m){let e=d.slice(m.rhsStart,m.rhsEnd+1);RegExp(`\\b${A(r)}Module\\b`).test(e)||(d=B(d,u))}else d=B(d,u);await t(s,d)}function B(e,t){let n=V(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 V(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=ve(e,n);return t===-1?null:{shape:`array`,rhsStart:n,rhsEnd:t}}if(e.slice(n,n+13)===`defineModules`){let t=_e(e,n);return t===-1?null:{shape:`chain`,rhsStart:n,rhsEnd:t-1,chainEnd:t}}return null}function _e(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=U(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=U(e,t);if(n===-1)break;i=n+1}return i}function H(e,t){let n=e.slice(t,t+2);if(n===`//`){for(t+=2;t<e.length&&e[t]!==`
761
+ `;)t++;return t}if(n===`/*`){for(t+=2;t+1<e.length&&(e[t]!==`*`||e[t+1]!==`/`);)t++;return t+2}return t}function ve(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=H(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++}else{if(i===`[`)n++;else if(i===`]`&&(n--,n===0))return r;r++}}return-1}function U(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=H(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++}else{if(i===`(`)n++;else if(i===`)`&&(n--,n===0))return r;r++}}return-1}function W(e,t,n,r){return`/**
556
762
  * Snapshot for the DevTools topology view (\`/_debug\`).
557
763
  *
558
764
  * Must be CHEAP — the topology endpoint polls on a short interval, so
@@ -592,7 +798,7 @@ export const modules = defineModules().mount(${u})
592
798
  // ]
593
799
  // },`.split(`
594
800
  `).map(e=>r+e).join(`
595
- `)}async function ve(e){let{name:n,outDir:r}=e,i=D(n),a=T(n),o=[],s=h(r,`${i}.adapter.ts`);return await t(s,`import {
801
+ `)}async function ye(e){let{name:n,outDir:r}=e,i=D(n),a=T(n),o=[],s=h(r,`${i}.adapter.ts`);return await t(s,`import {
596
802
  defineAdapter,
597
803
  type AdapterContext,
598
804
  type AdapterMiddleware,
@@ -801,8 +1007,8 @@ ${W(i,a,`adapter`,` `)}
801
1007
  }
802
1008
  },
803
1009
  })
804
- `),o.push(s),o}const ye={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 _(n);if(r){let e=ye,n=D(r),a=o?O(n):n,s=e[t]??``,c=h(i,a);return _(s?h(c,s):c)}return _(a)}async function be(e){let{name:n,moduleName:r,modulesDir:i,pattern:a}=e,o=e.runtime===`express`,s=e.runtime?`this project runs on ${e.runtime} and has no
805
- // \`express\``:"this project declares no `runtime` in kick.config.ts, so it\n // may have no `express`",c=G({type:`middleware`,outDir:e.outDir,moduleName:r,modulesDir:i,defaultDir:`src/middleware`,pattern:a,shouldPluralize:e.pluralize??!0}),l=D(n),u=E(n),d=[],f=h(c,`${l}.middleware.ts`);return await t(f,`${o?`import type { Request, Response, NextFunction } from 'express'`:`import type { IncomingMessage, ServerResponse } from 'node:http'`}
1010
+ `),o.push(s),o}const be={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=!1}=e;if(n)return _(n);if(r){let e=be,n=D(r),a=o?O(n):n,s=e[t]??``,c=h(i,a);return _(s?h(c,s):c)}return _(a)}async function xe(e){let{name:n,moduleName:r,modulesDir:i,pattern:a}=e,o=e.runtime===`express`,s=e.runtime?`this project runs on ${e.runtime} and has no
1011
+ // \`express\``:"this project declares no `runtime` in kick.config.ts, so it\n // may have no `express`",c=G({type:`middleware`,outDir:e.outDir,moduleName:r,modulesDir:i,defaultDir:`src/middleware`,pattern:a,shouldPluralize:e.pluralize??!1}),l=D(n),u=E(n),d=[],f=h(c,`${l}.middleware.ts`);return await t(f,`${o?`import type { Request, Response, NextFunction } from 'express'`:`import type { IncomingMessage, ServerResponse } from 'node:http'`}
806
1012
 
807
1013
  export interface ${T(n)}Options {
808
1014
  // Add configuration options here. The factory below closes over the
@@ -861,7 +1067,7 @@ ${o?" // Typed from `express` because that is this project's runtime\n // (`ru
861
1067
  next()
862
1068
  }
863
1069
  }
864
- `),d.push(f),d}async function xe(e){let{name:n,moduleName:r,modulesDir:i,pattern:a}=e,o=G({type:`guard`,outDir:e.outDir,moduleName:r,modulesDir:i,defaultDir:`src/guards`,pattern:a,shouldPluralize:e.pluralize??!0}),s=D(n),c=E(n),l=T(n),u=[],d=h(o,`${s}.guard.ts`);return await t(d,`import { Container, HttpException } from '@forinda/kickjs'
1070
+ `),d.push(f),d}async function Se(e){let{name:n,moduleName:r,modulesDir:i,pattern:a}=e,o=G({type:`guard`,outDir:e.outDir,moduleName:r,modulesDir:i,defaultDir:`src/guards`,pattern:a,shouldPluralize:e.pluralize??!1}),s=D(n),c=E(n),l=T(n),u=[],d=h(o,`${s}.guard.ts`);return await t(d,`import { Container, HttpException } from '@forinda/kickjs'
865
1071
  import type { RequestContext } from '@forinda/kickjs'
866
1072
 
867
1073
  /**
@@ -901,7 +1107,7 @@ export async function ${c}Guard(ctx: RequestContext, next: () => void): Promise<
901
1107
  ctx.problem.unauthorized({ detail: 'Invalid or expired token' })
902
1108
  }
903
1109
  }
904
- `),u.push(d),u}async function Se(e){let{name:n,moduleName:r,modulesDir:i,pattern:a}=e,o=G({type:`service`,outDir:e.outDir,moduleName:r,modulesDir:i,defaultDir:`src/services`,pattern:a,shouldPluralize:e.pluralize??!0}),s=D(n),c=T(n),l=[],u=h(o,`${s}.service.ts`);return await t(u,`import { Service } from '@forinda/kickjs'
1110
+ `),u.push(d),u}async function Ce(e){let{name:n,moduleName:r,modulesDir:i,pattern:a}=e,o=G({type:`service`,outDir:e.outDir,moduleName:r,modulesDir:i,defaultDir:`src/services`,pattern:a,shouldPluralize:e.pluralize??!1}),s=D(n),c=T(n),l=[],u=h(o,`${s}.service.ts`);return await t(u,`import { Service } from '@forinda/kickjs'
905
1111
 
906
1112
  @Service()
907
1113
  export class ${c}Service {
@@ -910,7 +1116,7 @@ export class ${c}Service {
910
1116
  // @Inject(MY_REPO) private readonly repo: IMyRepository,
911
1117
  // ) {}
912
1118
  }
913
- `),l.push(u),l}async function Ce(e){let{name:n,moduleName:r,modulesDir:i,pattern:a}=e,o=G({type:`controller`,outDir:e.outDir,moduleName:r,modulesDir:i,defaultDir:`src/controllers`,pattern:a,shouldPluralize:e.pluralize??!0}),s=D(n),c=T(n),l=[],u=h(o,`${s}.controller.ts`);return await t(u,`import { Controller, Get, Post, Put, Delete, reply, type Ctx } from '@forinda/kickjs'
1119
+ `),l.push(u),l}async function we(e){let{name:n,moduleName:r,modulesDir:i,pattern:a}=e,o=G({type:`controller`,outDir:e.outDir,moduleName:r,modulesDir:i,defaultDir:`src/controllers`,pattern:a,shouldPluralize:e.pluralize??!1}),s=D(n),c=T(n),l=[],u=h(o,`${s}.controller.ts`);return await t(u,`import { Controller, Get, Post, Patch, Delete, reply, type Ctx } from '@forinda/kickjs'
914
1120
 
915
1121
  // \`Ctx<KickRoutes.${c}Controller['<method>']>\` is generated by
916
1122
  // \`kick typegen\` (auto-run on \`kick dev\`). After the first run, your IDE
@@ -923,6 +1129,7 @@ export class ${c}Service {
923
1129
  //
924
1130
  // The bodies below are placeholders: wire them to a service or repository.
925
1131
 
1132
+ // Require a signed-in user for these routes: https://kickjs.app/guide/authentication
926
1133
  @Controller()
927
1134
  export class ${c}Controller {
928
1135
  // @Autowired() private readonly myService!: MyService
@@ -942,7 +1149,7 @@ export class ${c}Controller {
942
1149
  return reply.created({ data: ctx.body })
943
1150
  }
944
1151
 
945
- @Put('/:id')
1152
+ @Patch('/:id')
946
1153
  async update(ctx: Ctx<KickRoutes.${c}Controller['update']>) {
947
1154
  return { id: ctx.params.id, data: ctx.body }
948
1155
  }
@@ -952,21 +1159,21 @@ export class ${c}Controller {
952
1159
  return reply.noContent()
953
1160
  }
954
1161
  }
955
- `),l.push(u),l}async function we(e){let{name:n,moduleName:r,modulesDir:i,pattern:a}=e,o=G({type:`dto`,outDir:e.outDir,moduleName:r,modulesDir:i,defaultDir:`src/dtos`,pattern:a,shouldPluralize:e.pluralize??!0}),s=D(n),c=T(n),l=E(n),u=e.schemaLib??`zod`,d=P[u],f=[],p=h(o,`${s}.dto.ts`);return await t(p,`${d.import}
1162
+ `),l.push(u),l}async function Te(e){let{name:n,moduleName:r,modulesDir:i,pattern:a}=e,o=G({type:`dto`,outDir:e.outDir,moduleName:r,modulesDir:i,defaultDir:`src/dtos`,pattern:a,shouldPluralize:e.pluralize??!1}),s=D(n),c=T(n),l=E(n),u=e.schemaLib??`zod`,d=P[u],f=[],p=h(o,`${s}.dto.ts`);return await t(p,`${d.import}
956
1163
 
957
1164
  export const ${l}Schema = ${F(u,`// Define your schema fields here\n name: ${d.required}`)}
958
1165
 
959
1166
  export type ${c}DTO = ${d.infer(`${l}Schema`)}
960
- `),f.push(p),f}function Te(){let e=(process.env.LOG_LEVEL??process.env.KICKJS_LOG_LEVEL??``).toLowerCase();return e===`debug`||e===`trace`}var Ee=class extends Error{drifted;constructor(e){let t=e.map(e=>` ${e.id} → ${e.outFile}`).join(`
961
- `);super(`kick typegen --check: ${e.length} generated file(s) are out of date:\n${t}\n Run \`kick typegen\` and commit the result.`),this.name=`TypegenDriftError`,this.drifted=e}};function De(e){return e}function Oe(e){let t=/* @__PURE__ */ 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 ke=`__kickjs_typegen_owner`;function Ae(e){let{cwd:t,config:n}=e,r=e.debounceMs??100,i=e.pipeline??{runTypegen:async e=>(await import(`./typegen-DSaawXaQ.mjs`).then(e=>e.n)).runTypegen(e),runAllPluginTypegens:async e=>(await import(`./run-plugins-Dx-QENlm.mjs`).then(e=>e.n)).runAllPluginTypegens(e),writeTypegenArtifacts:async(e,t,n)=>(await import(`./typegen-DSaawXaQ.mjs`).then(e=>e.n)).writeTypegenArtifacts(e,t,n),buildAssets:async(e,t)=>(await import(`./build-BuBCsnzr.mjs`).then(e=>e.n)).buildAssets(e,t)},a=n?.typegen?.schemaValidator??`zod`,o=n?.typegen?.envFile,s=p.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=>p.resolve(t,e)):[],l=!!n?.assetMap&&Object.keys(n.assetMap).length>0,u=e=>e.replaceAll(`\\`,`/`),d=c.map(u),f=e=>{let t=u(e);return d.some(e=>t===e||t.startsWith(`${e}/`))},m=Oe(e.emitWarning),h=null,g=!1,_=/* @__PURE__ */ new Set,v=/* @__PURE__ */ 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,watch:!0}).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=f(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 je(e){return D(e).replace(/-/g,`_`)}function K(e){let t=e.cwd??process.cwd(),n=e.projectRoot??d(t),r=e.pluralize??!0,i=T(e.name),a=E(e.name),o=D(e.name),s=je(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=O(o);c.pluralKebab=e,c.pluralPascal=T(e),c.pluralCamel=E(e)}return c}function Me(e,t){return _(e.cwd,t)}async function Ne(e){return import(ee(e).href)}const q=/* @__PURE__ */ new Map;async function J(e){let t=q.get(e);if(t)return t;let n=Pe(e);return q.set(e,n),n}async function Pe(e){let t=_(e,`package.json`);if(!v(t))return{generators:[],loaded:[],failed:[]};let n=Fe(JSON.parse(await S(t,`utf-8`))),r=f(_(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 S(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(t),s);if(!v(c)){o.push({source:e,reason:`kickjs.generators points to missing file: ${s}`});continue}let l;try{l=await Ne(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`});else{for(let t of u)Ie(t)?i.push({source:e,spec:t}):o.push({source:e,reason:`manifest entry is not a valid GeneratorSpec (missing name/files)`});a.push(e)}}return{generators:i,loaded:a,failed:o}}function Fe(e){let t=/* @__PURE__ */ 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 Ie(e){if(!e||typeof e!=`object`)return!1;let t=e;return typeof t.name==`string`&&typeof t.files==`function`}async function Le(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=ze(await J(n),e.generatorName);return i?Y(i.spec,i.source,e,n):null}async function Re(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 ze(e,t){return e.generators.find(e=>e.spec.name===t)}async function Y(e,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 e.files(a),s=[];for(let e of o){let n=Me(a,e.path);await t(n,e.content),s.push(n)}return{files:s,source:n}}function Be(e){return e}function Ve(e){return e}function He(e){try{return JSON.parse(y(e,`utf-8`))}catch{return null}}function X(e){try{return y(e,`utf-8`)}catch{return null}}function Ue(e){let t=e.replace(/\/\*[\s\S]*?\*\//g,``).replace(/(^|[^:"'\\])\/\/.*$/gm,`$1`).replace(/,(\s*[}\]])/g,`$1`);try{return JSON.parse(t)}catch{return null}}function We(e){let t=h(e,`tsconfig.json`);if(v(t))return Z(t,/* @__PURE__ */ new Set)}function Z(e,t){if(t.has(e)||t.size>=16)return null;t.add(e);let n=X(e);if(n===null)return null;let r=Ue(n);if(typeof r!=`object`||!r)return null;let i=Array.isArray(r.extends)?r.extends:typeof r.extends==`string`?[r.extends]:[],a={};for(let n of i){if(typeof n!=`string`)continue;let r=Ge(m(e),n);if(!r)continue;let i=Z(r,t);i&&Object.assign(a,i.compilerOptions)}return r.compilerOptions={...a,...r.compilerOptions},r}function Ge(e,t){if(t.startsWith(`.`)||t.startsWith(`/`))return Q(_(e,t));let n=e;for(;;){let e=Q(h(n,`node_modules`,t));if(e)return e;let r=m(n);if(r===n)break;n=r}return null}function Q(e){if(v(e)){try{if(x(e).isDirectory()){let t=h(e,`tsconfig.json`);return v(t)?t:null}}catch{return null}return e}let t=`${e}.json`;return v(t)?t:null}function Ke(e){return e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`)}function qe(){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.
962
- Install a supported version via nvm / fnm / volta.`}:{name:`Node version`,status:`pass`,message:e}}function Je(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 Ye(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: ${n(e.cwd)} add express`}:t.express?{name:`express installed`,status:`pass`,message:t.express}:null}const Xe={express:[],fastify:[`fastify`,`@fastify/middie`],h3:[`h3`]};function Ze(e){if(!e.pkg||e.runtime===`express`)return null;let t={...e.pkg.dependencies,...e.pkg.peerDependencies,...e.pkg.devDependencies},r=Xe[e.runtime].filter(e=>!t[e]),i=`runtime engine (${e.runtime})`;return r.length>0?{name:i,status:`fail`,fix:`Resolved runtime '${e.runtime}' is missing engine peer(s): ${r.join(`, `)}.\nInstall: ${n(e.cwd)} add ${r.join(` `)}`}:e.runtime===`h3`&&Qe(e.cwd,t.h3)===1?{name:i,status:`fail`,message:`h3 v1`,fix:`This version of @forinda/kickjs runs on h3 v2 only. Upgrade: ${n(e.cwd)} add h3@^2 — no code change for apps that only use KickJS APIs.`}:{name:i,status:`pass`}}function Qe(e,t){let n=t;for(let t=_(e);;t=m(t)){try{n=JSON.parse(y(h(t,`node_modules`,`h3`,`package.json`),`utf8`)).version;break}catch{}if(m(t)===t)break}let r=/(\d+)/.exec(n)?.[1];return r===void 0?void 0:Number(r)}function $e(e){if(!e.pkg||!et(e.cwd))return null;let t=u[e.runtime],r=`upload driver (${e.runtime})`;return t.prod?{...e.pkg.dependencies,...e.pkg.peerDependencies,...e.pkg.devDependencies}[t.prod]?{name:r,status:`pass`,message:t.prod}:{name:r,status:`fail`,fix:`This project uses file uploads on the '${e.runtime}' runtime, which needs '${t.prod}'.\nInstall it: kick add upload (or ${n(e.cwd)} add ${t.prod})`}:{name:r,status:`pass`,message:`native multipart`}}const $=2e3;function et(e){let t=h(e,`src`);if(!v(t))return!1;let n=/@FileUpload\b|\bupload\.(single|array|none)\s*\(/,r=[t],i=0;for(;r.length>0&&i<$;){let e=r.pop(),t;try{t=b(e,{withFileTypes:!0})}catch{continue}for(let a of t){if(i>=$)break;let t=h(e,a.name);if(a.isDirectory())a.name!==`node_modules`&&r.push(t);else if(/\.(ts|tsx|mts|cts)$/.test(a.name)&&(i++,n.test(X(t)??``)))return!0}}return!1}function tt(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.\nInstall it: ${n(e.cwd)} add reflect-metadata\nThen import it at the top of src/index.ts:\n\n import 'reflect-metadata'\n // ... rest of bootstrap`}}function nt(e){if(e.tsconfig===void 0)return[{name:`tsconfig.json present`,status:`fail`,fix:"Create a tsconfig.json with `experimentalDecorators: true` and `emitDecoratorMetadata: true`. `kick new` scaffolds one automatically."}];if(e.tsconfig===null)return[{name:`tsconfig.json readable`,status:`warn`,message:`could not parse tsconfig.json (or a config it extends)`,fix:"Check tsconfig.json for a syntax error, and that every path in its `extends` chain resolves. Decorator options could not be verified."}];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 rt(e){let t=[`src/env.ts`,`src/env/index.ts`,`src/config/env.ts`,`src/config/index.ts`].map(t=>h(e.cwd,t)).filter(e=>v(e)).filter(e=>/\bloadEnv\s*\(/.test(X(e)??``));if(t.length===0)return null;let n=[`src/index.ts`,`src/main.ts`].map(t=>h(e.cwd,t)).find(e=>v(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=X(n)??``,i=m(n),a=[];for(let e of t){let t=g(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+)?['"]${Ke(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=>g(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 \`${g(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 ${g(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 it(e,t=at){let n=0,r=0,i=[e];for(;i.length>0&&r<t;){let e=i.pop(),a;try{a=b(e,{withFileTypes:!0})}catch{continue}for(let o of a){if(r>=t)break;r++;let a=h(e,o.name);if(o.isDirectory())i.push(a);else try{let e=x(a).mtimeMs;e>n&&(n=e)}catch{}}}return n}const at=2e3;function ot(e){let t=h(e.cwd,`.kickjs`,`types`);if(!v(t))return null;let n=it(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 st=[`vitest.config.ts`,`vitest.config.js`,`vitest.config.mts`,`vite.config.ts`,`jest.config.ts`,`jest.config.js`],ct=[`.env.test`,`.env.test.local`],lt=[`.env`,`.env.local`];function ut(e){let t=e.pkg?.scripts;if(t&&typeof t==`object`)for(let[e,n]of Object.entries(t)){if(!e.startsWith(`test`)||typeof n!=`string`)continue;let t=/KICKJS_ENV_FILE=(\S+)/.exec(n);if(t?.[1])return t[1]}return process.env.KICKJS_ENV_FILE??null}function dt(e){if(!st.some(t=>v(h(e.cwd,t))))return null;let t=lt.filter(t=>v(h(e.cwd,t)));if(t.length===0)return null;let n=ct.filter(t=>v(h(e.cwd,t)));if(n.length>0){let t=ut(e);return t&&t!==`off`&&t!==`none`?{name:`test env isolation`,status:`warn`,message:`${n.join(`, `)} present, but KICKJS_ENV_FILE=${t} overrides it`,fix:`KICKJS_ENV_FILE is checked before test-mode isolation, so ${n[0]} is\nignored and "${t}" is read instead. Drop the override to use the\ntest files, or set KICKJS_ENV_FILE=off if env comes from the runner.`}:{name:`test env isolation`,status:`pass`,message:`${n.join(`, `)} present`}}return{name:`test env isolation`,status:`warn`,message:`${t.join(`, `)} present, no .env.test`,fix:`Under NODE_ENV=test (or vitest's VITEST) KickJS reads .env.test / .env.test.local
1167
+ `),f.push(p),f}function Ee(){let e=(process.env.LOG_LEVEL??process.env.KICKJS_LOG_LEVEL??``).toLowerCase();return e===`debug`||e===`trace`}var De=class extends Error{drifted;constructor(e){let t=e.map(e=>` ${e.id} → ${e.outFile}`).join(`
1168
+ `);super(`kick typegen --check: ${e.length} generated file(s) are out of date:\n${t}\n Run \`kick typegen\` and commit the result.`),this.name=`TypegenDriftError`,this.drifted=e}};function Oe(e){return e}function ke(e){let t=/* @__PURE__ */ 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 Ae=`__kickjs_typegen_owner`;function je(e){let{cwd:t,config:n}=e,r=e.debounceMs??100,i=e.pipeline??{runTypegen:async e=>(await import(`./typegen-AGdhxyRc.mjs`).then(e=>e.n)).runTypegen(e),runAllPluginTypegens:async e=>(await import(`./run-plugins-9tjbMqS6.mjs`).then(e=>e.n)).runAllPluginTypegens(e),writeTypegenArtifacts:async(e,t,n)=>(await import(`./typegen-AGdhxyRc.mjs`).then(e=>e.n)).writeTypegenArtifacts(e,t,n),buildAssets:async(e,t)=>(await import(`./build-DQnV0GCD.mjs`).then(e=>e.n)).buildAssets(e,t)},a=n?.typegen?.schemaValidator??`zod`,o=n?.typegen?.envFile,s=p.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=>p.resolve(t,e)):[],l=!!n?.assetMap&&Object.keys(n.assetMap).length>0,u=e=>e.replaceAll(`\\`,`/`),d=c.map(u),f=e=>{let t=u(e);return d.some(e=>t===e||t.startsWith(`${e}/`))},m=ke(e.emitWarning),h=null,g=!1,_=/* @__PURE__ */ new Set,v=/* @__PURE__ */ 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,watch:!0}).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=f(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 Me(e){return D(e).replace(/-/g,`_`)}function K(e){let t=e.cwd??process.cwd(),n=e.projectRoot??d(t),r=e.pluralize??!1,i=T(e.name),a=E(e.name),o=D(e.name),s=Me(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=O(o);c.pluralKebab=e,c.pluralPascal=T(e),c.pluralCamel=E(e)}return c}function Ne(e,t){return _(e.cwd,t)}async function Pe(e){return import(w(e).href)}const q=/* @__PURE__ */ new Map;async function J(e){let t=q.get(e);if(t)return t;let n=Fe(e);return q.set(e,n),n}async function Fe(e){let t=_(e,`package.json`);if(!v(t))return{generators:[],loaded:[],failed:[]};let n=Ie(JSON.parse(await S(t,`utf-8`))),r=f(_(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 S(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(t),s);if(!v(c)){o.push({source:e,reason:`kickjs.generators points to missing file: ${s}`});continue}let l;try{l=await Pe(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`});else{for(let t of u)Le(t)?i.push({source:e,spec:t}):o.push({source:e,reason:`manifest entry is not a valid GeneratorSpec (missing name/files)`});a.push(e)}}return{generators:i,loaded:a,failed:o}}function Ie(e){let t=/* @__PURE__ */ 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 Le(e){if(!e||typeof e!=`object`)return!1;let t=e;return typeof t.name==`string`&&typeof t.files==`function`}async function Re(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=Be(await J(n),e.generatorName);return i?Y(i.spec,i.source,e,n):null}async function ze(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 Be(e,t){return e.generators.find(e=>e.spec.name===t)}async function Y(e,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 e.files(a),s=[];for(let e of o){let n=Ne(a,e.path);await t(n,e.content),s.push(n)}return{files:s,source:n}}function Ve(e){return e}function He(e){return e}function Ue(e){try{return JSON.parse(y(e,`utf-8`))}catch{return null}}function X(e){try{return y(e,`utf-8`)}catch{return null}}function We(e){let t=e.replace(/\/\*[\s\S]*?\*\//g,``).replace(/(^|[^:"'\\])\/\/.*$/gm,`$1`).replace(/,(\s*[}\]])/g,`$1`);try{return JSON.parse(t)}catch{return null}}function Ge(e){let t=h(e,`tsconfig.json`);if(v(t))return Z(t,/* @__PURE__ */ new Set)}function Z(e,t){if(t.has(e)||t.size>=16)return null;t.add(e);let n=X(e);if(n===null)return null;let r=We(n);if(typeof r!=`object`||!r)return null;let i=Array.isArray(r.extends)?r.extends:typeof r.extends==`string`?[r.extends]:[],a={};for(let n of i){if(typeof n!=`string`)continue;let r=Ke(m(e),n);if(!r)continue;let i=Z(r,t);i&&Object.assign(a,i.compilerOptions)}return r.compilerOptions={...a,...r.compilerOptions},r}function Ke(e,t){if(t.startsWith(`.`)||t.startsWith(`/`))return Q(_(e,t));let n=e;for(;;){let e=Q(h(n,`node_modules`,t));if(e)return e;let r=m(n);if(r===n)break;n=r}return null}function Q(e){if(v(e)){try{if(x(e).isDirectory()){let t=h(e,`tsconfig.json`);return v(t)?t:null}}catch{return null}return e}let t=`${e}.json`;return v(t)?t:null}function qe(e){return e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`)}function Je(){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.
1169
+ Install a supported version via nvm / fnm / volta.`}:{name:`Node version`,status:`pass`,message:e}}function Ye(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 Xe(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: ${n(e.cwd)} add express`}:t.express?{name:`express installed`,status:`pass`,message:t.express}:null}const Ze={express:[],fastify:[`fastify`,`@fastify/middie`],h3:[`h3`]};function Qe(e){if(!e.pkg||e.runtime===`express`)return null;let t={...e.pkg.dependencies,...e.pkg.peerDependencies,...e.pkg.devDependencies},r=Ze[e.runtime].filter(e=>!t[e]),i=`runtime engine (${e.runtime})`;return r.length>0?{name:i,status:`fail`,fix:`Resolved runtime '${e.runtime}' is missing engine peer(s): ${r.join(`, `)}.\nInstall: ${n(e.cwd)} add ${r.join(` `)}`}:e.runtime===`h3`&&$e(e.cwd,t.h3)===1?{name:i,status:`fail`,message:`h3 v1`,fix:`This version of @forinda/kickjs runs on h3 v2 only. Upgrade: ${n(e.cwd)} add h3@^2 — no code change for apps that only use KickJS APIs.`}:{name:i,status:`pass`}}function $e(e,t){let n=t;for(let t=_(e);;t=m(t)){try{n=JSON.parse(y(h(t,`node_modules`,`h3`,`package.json`),`utf8`)).version;break}catch{}if(m(t)===t)break}let r=/(\d+)/.exec(n)?.[1];return r===void 0?void 0:Number(r)}function et(e){if(!e.pkg||!tt(e.cwd))return null;let t=u[e.runtime],r=`upload driver (${e.runtime})`;return t.prod?{...e.pkg.dependencies,...e.pkg.peerDependencies,...e.pkg.devDependencies}[t.prod]?{name:r,status:`pass`,message:t.prod}:{name:r,status:`fail`,fix:`This project uses file uploads on the '${e.runtime}' runtime, which needs '${t.prod}'.\nInstall it: kick add upload (or ${n(e.cwd)} add ${t.prod})`}:{name:r,status:`pass`,message:`native multipart`}}const $=2e3;function tt(e){let t=h(e,`src`);if(!v(t))return!1;let n=/@FileUpload\b|\bupload\.(single|array|none)\s*\(/,r=[t],i=0;for(;r.length>0&&i<$;){let e=r.pop(),t;try{t=b(e,{withFileTypes:!0})}catch{continue}for(let a of t){if(i>=$)break;let t=h(e,a.name);if(a.isDirectory())a.name!==`node_modules`&&r.push(t);else if(/\.(ts|tsx|mts|cts)$/.test(a.name)&&(i++,n.test(X(t)??``)))return!0}}return!1}function nt(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.\nInstall it: ${n(e.cwd)} add reflect-metadata\nThen import it at the top of src/index.ts:\n\n import 'reflect-metadata'\n // ... rest of bootstrap`}}function rt(e){if(e.tsconfig===void 0)return[{name:`tsconfig.json present`,status:`fail`,fix:"Create a tsconfig.json with `experimentalDecorators: true` and `emitDecoratorMetadata: true`. `kick new` scaffolds one automatically."}];if(e.tsconfig===null)return[{name:`tsconfig.json readable`,status:`warn`,message:`could not parse tsconfig.json (or a config it extends)`,fix:"Check tsconfig.json for a syntax error, and that every path in its `extends` chain resolves. Decorator options could not be verified."}];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 it(e){let t=[`src/env.ts`,`src/env/index.ts`,`src/config/env.ts`,`src/config/index.ts`].map(t=>h(e.cwd,t)).filter(e=>v(e)).filter(e=>/\bloadEnv\s*\(/.test(X(e)??``));if(t.length===0)return null;let n=[`src/index.ts`,`src/main.ts`].map(t=>h(e.cwd,t)).find(e=>v(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=X(n)??``,i=m(n),a=[];for(let e of t){let t=g(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+)?['"]${qe(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=>g(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 \`${g(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 ${g(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 at(e,t=ot){let n=0,r=0,i=[e];for(;i.length>0&&r<t;){let e=i.pop(),a;try{a=b(e,{withFileTypes:!0})}catch{continue}for(let o of a){if(r>=t)break;r++;let a=h(e,o.name);if(o.isDirectory())i.push(a);else try{let e=x(a).mtimeMs;e>n&&(n=e)}catch{}}}return n}const ot=2e3;function st(e){let t=h(e.cwd,`.kickjs`,`types`);if(!v(t))return null;let n=at(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 ct=[`vitest.config.ts`,`vitest.config.js`,`vitest.config.mts`,`vite.config.ts`,`jest.config.ts`,`jest.config.js`],lt=[`.env.test`,`.env.test.local`],ut=[`.env`,`.env.local`];function dt(e){let t=e.pkg?.scripts;if(t&&typeof t==`object`)for(let[e,n]of Object.entries(t)){if(!e.startsWith(`test`)||typeof n!=`string`)continue;let t=/KICKJS_ENV_FILE=(\S+)/.exec(n);if(t?.[1])return t[1]}return process.env.KICKJS_ENV_FILE??null}function ft(e){if(!ct.some(t=>v(h(e.cwd,t))))return null;let t=ut.filter(t=>v(h(e.cwd,t)));if(t.length===0)return null;let n=lt.filter(t=>v(h(e.cwd,t)));if(n.length>0){let t=dt(e);return t&&t!==`off`&&t!==`none`?{name:`test env isolation`,status:`warn`,message:`${n.join(`, `)} present, but KICKJS_ENV_FILE=${t} overrides it`,fix:`KICKJS_ENV_FILE is checked before test-mode isolation, so ${n[0]} is\nignored and "${t}" is read instead. Drop the override to use the\ntest files, or set KICKJS_ENV_FILE=off if env comes from the runner.`}:{name:`test env isolation`,status:`pass`,message:`${n.join(`, `)} present`}}return{name:`test env isolation`,status:`warn`,message:`${t.join(`, `)} present, no .env.test`,fix:`Under NODE_ENV=test (or vitest's VITEST) KickJS reads .env.test / .env.test.local
963
1170
  INSTEAD of ${t.join(` and `)} — no layering, no fallback. Without one of\nthose files, every var your test config does not pin resolves from your
964
1171
  development env — including DATABASE_URL and any third-party credentials.
965
1172
 
966
1173
  Create .env.test (cp .env.test.example .env.test, if the project has one)
967
1174
  holding the whole environment the suite should run against
968
1175
  (a var you leave out is then missing, not inherited), or set KICKJS_ENV_FILE=off
969
- and supply env from the test runner.`}}const ft=[()=>qe(),Je,Ye,Ze,$e,tt,nt,rt,dt,ot];async function pt(e,t={}){let n={cwd:e,pkg:He(h(e,`package.json`)),tsconfig:We(e),runtime:t.runtime??`express`},r=[...ft,...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 mt(e){switch(e){case`pass`:return s.green(`✔`);case`warn`:return s.yellow(`⚠`);case`fail`:return s.red(`✖`)}}function ht(e){let t=mt(e.status),n=e.message?` ${s.dim(`(${e.message})`)}`:``;return`${t} ${e.name}${n}`}function gt(e){return e.split(`
1176
+ and supply env from the test runner.`}}const pt=[()=>Je(),Ye,Xe,Qe,et,nt,rt,it,ft,st];async function mt(e,t={}){let n={cwd:e,pkg:Ue(h(e,`package.json`)),tsconfig:Ge(e),runtime:t.runtime??`express`},r=[...pt,...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 ht(e){switch(e){case`pass`:return s.green(`✔`);case`warn`:return s.yellow(`⚠`);case`fail`:return s.red(`✖`)}}function gt(e){let t=ht(e.status),n=e.message?` ${s.dim(`(${e.message})`)}`:``;return`${t} ${e.name}${n}`}function _t(e){return e.split(`
970
1177
  `).map(e=>` ${s.dim(`→`)} ${e}`).join(`
971
- `)}function _t(e){return e?.doctor?.checks??[]}function vt(e){e.command(`doctor`).description(`Pre-flight checks for your KickJS project (dev environment health)`).action(async()=>{let e=process.cwd(),t=_t(await c(e)),n=await l(e);i(`KickJS Doctor`);let o=await pt(e,{extraChecks:t,runtime:n});for(let e of o)a.message(ht(e)),e.fix&&e.status!==`pass`&&a.message(gt(e.fix));let u=o.filter(e=>e.status===`pass`).length,d=o.filter(e=>e.status===`warn`).length,f=o.filter(e=>e.status===`fail`).length,p=[s.green(`${u} passed`),d>0?s.yellow(`${d} warning${d===1?``:`s`}`):`${d} warnings`,f>0?s.red(`${f} error${f===1?``:`s`}`):`${f} errors`].join(`, `);f>0?(r(`${p} — fix the errors above before running the app`),process.exit(1)):r(d>0?`${p} — review the warnings`:s.green(`${p} — your environment looks good`))})}export{A,he as C,I as D,L as E,T as F,k as M,E as N,N as O,D as P,V as S,R as T,be as _,Le as a,W as b,ke as c,De as d,Te as f,xe as g,Se as h,Re as i,O as j,M as k,Ae as l,Ce as m,Be as n,K as o,we as p,vt as r,te as s,Ve as t,Ee as u,G as v,me as w,z as x,ve as y};
972
- //# sourceMappingURL=doctor-DGWvBOvj.mjs.map
1178
+ `)}function vt(e){return e?.doctor?.checks??[]}function yt(e){e.command(`doctor`).description(`Pre-flight checks for your KickJS project (dev environment health)`).action(async()=>{let e=process.cwd(),t=vt(await c(e)),n=await l(e);i(`KickJS Doctor`);let o=await mt(e,{extraChecks:t,runtime:n});for(let e of o)a.message(gt(e)),e.fix&&e.status!==`pass`&&a.message(_t(e.fix));let u=o.filter(e=>e.status===`pass`).length,d=o.filter(e=>e.status===`warn`).length,f=o.filter(e=>e.status===`fail`).length,p=[s.green(`${u} passed`),d>0?s.yellow(`${d} warning${d===1?``:`s`}`):`${d} warnings`,f>0?s.red(`${f} error${f===1?``:`s`}`):`${f} errors`].join(`, `);f>0?(r(`${p} — fix the errors above before running the app`),process.exit(1)):r(d>0?`${p} — review the warnings`:s.green(`${p} — your environment looks good`))})}export{A,ge as C,I as D,L as E,T as F,k as M,E as N,N as O,D as P,V as S,R as T,xe as _,Re as a,W as b,Ae as c,Oe as d,Ee as f,Se as g,Ce as h,ze as i,O as j,M as k,je as l,we as m,Ve as n,K as o,Te as p,yt as r,ee as s,He as t,De as u,G as v,he as w,z as x,ye as y};
1179
+ //# sourceMappingURL=doctor-rEbeoAoO.mjs.map