@forinda/kickjs-cli 2.2.2 → 2.2.3

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.
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs-cli v2.2.2
2
+ * @forinda/kickjs-cli v2.2.3
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -122,13 +122,16 @@ function generateViteConfig() {
122
122
  return `import { defineConfig } from 'vite'
123
123
  import { resolve } from 'node:path'
124
124
  import swc from 'unplugin-swc'
125
- import { kickjsVitePlugin } from '@forinda/kickjs-vite'
125
+ import { kickjsVitePlugin, envWatchPlugin } from '@forinda/kickjs-vite'
126
126
 
127
127
  export default defineConfig({
128
128
  oxc: false,
129
129
  plugins: [
130
130
  swc.vite(),
131
131
  kickjsVitePlugin({ entry: 'src/index.ts' }),
132
+ // Watches .env files and triggers a full reload on change so the
133
+ // dev server picks up env tweaks without a manual restart.
134
+ envWatchPlugin(),
132
135
  ],
133
136
  resolve: {
134
137
  alias: {
@@ -278,6 +281,11 @@ export default defineConfig({
278
281
  function generateEntryFile(name, template, version) {
279
282
  switch (template) {
280
283
  case "graphql": return `import 'reflect-metadata'
284
+ // Side-effect import — registers the extended env schema with kickjs
285
+ // **before** any controller / service / @Value gets resolved. Without
286
+ // this line ConfigService.get('YOUR_KEY') returns undefined because the
287
+ // cached schema would still be the base shape. See guide/configuration.
288
+ import './config'
281
289
  import { bootstrap } from '@forinda/kickjs'
282
290
  import { DevToolsAdapter } from '@forinda/kickjs-devtools'
283
291
  import { GraphQLAdapter } from '@forinda/kickjs-graphql'
@@ -300,6 +308,11 @@ export const app = await bootstrap({
300
308
  })
301
309
  `;
302
310
  case "cqrs": return `import 'reflect-metadata'
311
+ // Side-effect import — registers the extended env schema with kickjs
312
+ // **before** any controller / service / @Value gets resolved. Without
313
+ // this line ConfigService.get('YOUR_KEY') returns undefined because the
314
+ // cached schema would still be the base shape. See guide/configuration.
315
+ import './config'
303
316
  import { bootstrap } from '@forinda/kickjs'
304
317
  import { DevToolsAdapter } from '@forinda/kickjs-devtools'
305
318
  import { SwaggerAdapter } from '@forinda/kickjs-swagger'
@@ -327,6 +340,11 @@ export const app = await bootstrap({
327
340
  })
328
341
  `;
329
342
  case "minimal": return `import 'reflect-metadata'
343
+ // Side-effect import — registers the extended env schema with kickjs
344
+ // **before** any controller / service / @Value gets resolved. Without
345
+ // this line ConfigService.get('YOUR_KEY') returns undefined because the
346
+ // cached schema would still be the base shape. See guide/configuration.
347
+ import './config'
330
348
  import { bootstrap } from '@forinda/kickjs'
331
349
  import { modules } from './modules'
332
350
 
@@ -334,6 +352,11 @@ import { modules } from './modules'
334
352
  export const app = await bootstrap({ modules })
335
353
  `;
336
354
  default: return `import 'reflect-metadata'
355
+ // Side-effect import — registers the extended env schema with kickjs
356
+ // **before** any controller / service / @Value gets resolved. Without
357
+ // this line ConfigService.get('YOUR_KEY') returns undefined because the
358
+ // cached schema would still be the base shape. See guide/configuration.
359
+ import './config'
337
360
  import express from 'express'
338
361
  import {
339
362
  bootstrap,
@@ -376,10 +399,17 @@ export const modules: AppModuleClass[] = [HelloModule]
376
399
  `;
377
400
  }
378
401
  /**
379
- * Generate `src/env.ts` — the project's typed env schema.
402
+ * Generate `src/config/index.ts` — the project's typed env schema.
380
403
  *
381
404
  * Default-exports a `defineEnv(...)` schema so `kick typegen` can
382
- * infer it into the global `KickEnv` registry. After typegen runs:
405
+ * infer it into the global `KickEnv` registry, and *also* calls
406
+ * `loadEnv(envSchema)` as a module-load side effect so `ConfigService`
407
+ * and `@Value()` see the extended shape from the very first DI
408
+ * resolution. The companion `src/index.ts` template adds
409
+ * `import './config'` immediately after `reflect-metadata` so the
410
+ * registration runs before `bootstrap()` constructs anything.
411
+ *
412
+ * After typegen runs:
383
413
  *
384
414
  * @Value('DATABASE_URL') private url!: Env<'DATABASE_URL'>
385
415
  * process.env.DATABASE_URL // typed as string
@@ -387,7 +417,7 @@ export const modules: AppModuleClass[] = [HelloModule]
387
417
  * Both autocomplete and type-check at compile time.
388
418
  */
389
419
  function generateEnvFile() {
390
- return `import { defineEnv } from '@forinda/kickjs/config'
420
+ return `import { defineEnv, loadEnv } from '@forinda/kickjs/config'
391
421
  import { z } from 'zod'
392
422
 
393
423
  /**
@@ -403,11 +433,25 @@ import { z } from 'zod'
403
433
  * JWT_SECRET: z.string().min(32),
404
434
  * REDIS_URL: z.string().url().optional(),
405
435
  */
406
- export default defineEnv((base) =>
436
+ const envSchema = defineEnv((base) =>
407
437
  base.extend({
408
438
  // DATABASE_URL: z.string().url(),
409
439
  }),
410
440
  )
441
+
442
+ /**
443
+ * IMPORTANT — side effect: register the schema with kickjs's env cache
444
+ * **at module-load time**. \`ConfigService\` and \`@Value()\` both consume
445
+ * this cache, and they will fall back to the base schema (or undefined)
446
+ * if no extended schema has been registered before they're resolved.
447
+ *
448
+ * As long as \`src/index.ts\` imports this file (\`import './env'\`) at the
449
+ * top — before \`bootstrap()\` runs — every controller and service in the
450
+ * app sees the typed extended values.
451
+ */
452
+ export const env = loadEnv(envSchema)
453
+
454
+ export default envSchema
411
455
  `;
412
456
  }
413
457
  /** Generate src/modules/hello/hello.service.ts */
@@ -771,7 +815,20 @@ kick add --list # Show all available packages
771
815
 
772
816
  ## Environment Configuration
773
817
 
774
- Edit \`.env\` for environment variables. Access them with \`@Value()\` decorator:
818
+ The project's typed env schema lives in **\`src/config/index.ts\`**
819
+ extend the base schema there with your application-specific keys, and
820
+ the schema is auto-registered with kickjs at module load. The companion
821
+ \`src/index.ts\` imports it as a side effect (\`import './config'\`) **before**
822
+ \`bootstrap()\` runs, so every \`@Service\`, \`@Controller\`, \`@Value\`, and
823
+ \`ConfigService\` resolution sees the validated extended values.
824
+
825
+ > **Do not delete \`import './config'\` from \`src/index.ts\`.** It is the
826
+ > registration step that wires \`ConfigService\` to your env schema.
827
+ > Without it, \`config.get('YOUR_KEY')\` returns \`undefined\` for every
828
+ > user-defined key and \`@Value('YOUR_KEY')\` only works because of a
829
+ > raw \`process.env\` fallback (Zod coercion + defaults are skipped).
830
+
831
+ Edit \`.env\` for variable values. Access them with \`@Value()\`:
775
832
 
776
833
  \`\`\`ts
777
834
  import { Value } from '@forinda/kickjs'
@@ -789,7 +846,7 @@ export class ApiService {
789
846
  Or use \`ConfigService\`:
790
847
 
791
848
  \`\`\`ts
792
- import { ConfigService } from '@forinda/kickjs'
849
+ import { Service, Autowired, ConfigService } from '@forinda/kickjs'
793
850
 
794
851
  @Service()
795
852
  export class AppService {
@@ -797,11 +854,16 @@ export class AppService {
797
854
  private config!: ConfigService
798
855
 
799
856
  getPort() {
800
- return this.config.get('PORT', 3000)
857
+ // typed: number, Zod-coerced from baseEnvSchema
858
+ return this.config.get('PORT')
801
859
  }
802
860
  }
803
861
  \`\`\`
804
862
 
863
+ Hot-reload of \`.env\` changes during dev is wired up automatically via
864
+ \`envWatchPlugin()\` in \`vite.config.ts\` — edit \`.env\`, the dev server
865
+ reloads, and the next \`config.get()\` re-parses with the new values.
866
+
805
867
  ## Testing
806
868
 
807
869
  Tests live in \`src/**/*.test.ts\`:
@@ -863,6 +925,7 @@ ${template === "cqrs" ? `### CQRS/Event Decorators
863
925
  3. **Always use \`ctx.body\`** — never \`req.body\` directly
864
926
  4. **DI requires \`reflect-metadata\`** — already imported in \`src/index.ts\`
865
927
  5. **Vite HMR requires proper cleanup** — adapters should implement \`shutdown()\`
928
+ 6. **Never delete \`import './config'\` from \`src/index.ts\`** — that side-effect import registers the env schema with kickjs. Without it \`ConfigService.get('YOUR_KEY')\` returns \`undefined\` for every user-defined key. \`@Value('YOUR_KEY')\` *appears* to keep working but only via a raw \`process.env\` fallback (Zod coercion + schema defaults are silently skipped).
866
929
 
867
930
  ## Learn More
868
931
 
@@ -894,7 +957,8 @@ This guide helps AI agents (Claude, Copilot, etc.) work effectively on this Kick
894
957
  | Entry point | \`src/index.ts\` |
895
958
  | Module registry | \`src/modules/index.ts\` |
896
959
  | Feature modules | \`src/modules/<module-name>/\` |
897
- ${template === "graphql" ? "| GraphQL resolvers | `src/resolvers/` |\n" : ""}| Environment config | \`.env\` |
960
+ ${template === "graphql" ? "| GraphQL resolvers | `src/resolvers/` |\n" : ""}| Env values | \`.env\` |
961
+ | Env schema (Zod) | \`src/config/index.ts\` |
898
962
  | TypeScript config | \`tsconfig.json\` |
899
963
  | Vite config (HMR) | \`vite.config.ts\` |
900
964
  | Vitest config | \`vitest.config.ts\` |
@@ -1104,26 +1168,37 @@ Run tests:
1104
1168
 
1105
1169
  ## Environment Variables
1106
1170
 
1107
- Managed via \`.env\` file. Access with:
1171
+ Schema is declared in \`src/config/index.ts\` (extends the base
1172
+ \`PORT\`/\`NODE_ENV\`/\`LOG_LEVEL\` shape via \`defineEnv\`) and registered
1173
+ with kickjs at module load. \`src/index.ts\` imports it via
1174
+ \`import './config'\` **before** \`bootstrap()\` so the cache is populated
1175
+ in time for DI. Add new keys to the schema, drop their values into
1176
+ \`.env\`, and they're typed everywhere.
1177
+
1178
+ Access patterns:
1108
1179
 
1109
- 1. **@Value() decorator** (recommended):
1180
+ 1. **@Value() decorator** (recommended for known-at-construction keys):
1110
1181
  \`\`\`ts
1111
1182
  @Value('DATABASE_URL')
1112
1183
  private dbUrl!: string
1113
1184
  \`\`\`
1114
1185
 
1115
- 2. **ConfigService** (for dynamic access):
1186
+ 2. **ConfigService** (recommended for dynamic / method-scoped access):
1116
1187
  \`\`\`ts
1117
1188
  @Autowired()
1118
1189
  private config!: ConfigService
1119
1190
 
1120
- const port = this.config.get('PORT', 3000)
1191
+ const port = this.config.get('PORT') // typed: number
1121
1192
  \`\`\`
1122
1193
 
1123
- 3. **Direct access** (avoid in app code):
1124
- \`\`\`ts
1125
- process.env.PORT
1126
- \`\`\`
1194
+ 3. **Direct \`process.env\`** avoid in app code; bypasses Zod
1195
+ coercion and the typed \`KickEnv\` registry.
1196
+
1197
+ > **Pitfall**: never delete \`import './config'\` from \`src/index.ts\`.
1198
+ > If the schema is not registered before DI runs, \`config.get()\`
1199
+ > returns \`undefined\` for user keys (the base shape only) and
1200
+ > \`@Value()\` only works because of its raw \`process.env\` fallback —
1201
+ > Zod coercion + schema defaults are silently skipped.
1127
1202
 
1128
1203
  ## Key Decorators
1129
1204
 
@@ -1170,6 +1245,7 @@ ${template === "graphql" ? `### GraphQL
1170
1245
  4. **Routes not found** — Check controller path and module registration
1171
1246
  5. **HMR not working** — Verify \`vite.config.ts\` has \`hmr: true\`
1172
1247
  6. **Decorators not working** — Check \`tsconfig.json\` has \`experimentalDecorators: true\`
1248
+ 7. **\`config.get('YOUR_KEY')\` returns \`undefined\`** — \`src/index.ts\` is missing \`import './config'\`. That side-effect import registers the env schema with kickjs (\`loadEnv(envSchema)\` runs at module load). Without it, \`ConfigService\` falls back to the base schema (\`PORT\`/\`NODE_ENV\`/\`LOG_LEVEL\` only) and every user-defined key reads as \`undefined\`. \`@Value()\` may *appear* to work because of a raw \`process.env\` fallback, but Zod coercion and schema defaults are silently skipped — investigate \`src/index.ts\` and \`src/config/index.ts\` first.
1173
1249
 
1174
1250
  ## CLI Commands Reference
1175
1251
 
@@ -1222,7 +1298,7 @@ async function initProject(options) {
1222
1298
  await writeFileSafe(join(dir, ".gitattributes"), generateGitAttributes());
1223
1299
  await writeFileSafe(join(dir, ".env"), generateEnv());
1224
1300
  await writeFileSafe(join(dir, ".env.example"), generateEnvExample());
1225
- await writeFileSafe(join(dir, "src/env.ts"), generateEnvFile());
1301
+ await writeFileSafe(join(dir, "src/config/index.ts"), generateEnvFile());
1226
1302
  await writeFileSafe(join(dir, "src/index.ts"), generateEntryFile(name, template, cliPkg.version));
1227
1303
  await writeFileSafe(join(dir, "src/modules/index.ts"), generateModulesIndex());
1228
1304
  await writeFileSafe(join(dir, "src/modules/hello/hello.service.ts"), generateHelloService());
@@ -4860,31 +4936,49 @@ function extractInjectsFromSource(source, filePath, cwd) {
4860
4936
  return out;
4861
4937
  }
4862
4938
  /**
4863
- * Look for an env schema file at `<cwd>/<envFile>`. Returns a
4864
- * `DiscoveredEnv` if the file exists and contains both a
4939
+ * Default search order for the env schema file. Newer projects keep
4940
+ * the schema under `src/config/` so the framework's "config" concept
4941
+ * has a single home; older scaffolds dropped it at `src/env.ts` (kept
4942
+ * here for back-compat). The first match wins.
4943
+ */
4944
+ const DEFAULT_ENV_FILE_CANDIDATES = [
4945
+ "src/config/index.ts",
4946
+ "src/config/env.ts",
4947
+ "src/config.ts",
4948
+ "src/env.ts"
4949
+ ];
4950
+ /**
4951
+ * Look for an env schema file. When `envFile` is the string default
4952
+ * (`'src/env.ts'`) or omitted, every entry in `DEFAULT_ENV_FILE_CANDIDATES`
4953
+ * is tried in order. When the caller passes an explicit path, only that
4954
+ * path is tried (so projects can opt out of the search by setting
4955
+ * `kick.config.ts → typegen.envFile`).
4956
+ *
4957
+ * Returns a `DiscoveredEnv` if the file exists and contains both a
4865
4958
  * `defineEnv(...)` call and a default export — the two markers we
4866
4959
  * need before it's safe to emit `import type schema from '...'` in
4867
- * the generator.
4868
- *
4869
- * Returns `null` for any other state (file missing, no defineEnv, no
4870
- * default export) so the generator skips env typing silently. Users
4871
- * who want env typing must opt in by writing `src/env.ts` to the
4872
- * documented shape.
4960
+ * the generator. Returns `null` for any other state (no candidate
4961
+ * found, no defineEnv, no default export) so the generator skips env
4962
+ * typing silently.
4873
4963
  */
4874
4964
  async function detectEnvFile(cwd, envFile) {
4875
- const abs = resolve(cwd, envFile);
4876
- let source;
4877
- try {
4878
- source = await readFile(abs, "utf-8");
4879
- } catch {
4880
- return null;
4965
+ const candidates = envFile === "src/env.ts" ? DEFAULT_ENV_FILE_CANDIDATES : [envFile];
4966
+ for (const candidate of candidates) {
4967
+ const abs = resolve(cwd, candidate);
4968
+ let source;
4969
+ try {
4970
+ source = await readFile(abs, "utf-8");
4971
+ } catch {
4972
+ continue;
4973
+ }
4974
+ if (!/\bdefineEnv\s*\(/.test(source)) continue;
4975
+ if (!/export\s+default\b/.test(source)) continue;
4976
+ return {
4977
+ filePath: abs,
4978
+ relativePath: toRelative(abs, cwd)
4979
+ };
4881
4980
  }
4882
- if (!/\bdefineEnv\s*\(/.test(source)) return null;
4883
- if (!/export\s+default\b/.test(source)) return null;
4884
- return {
4885
- filePath: abs,
4886
- relativePath: toRelative(abs, cwd)
4887
- };
4981
+ return null;
4888
4982
  }
4889
4983
  /** Detect duplicate class names across files */
4890
4984
  function findCollisions(classes) {
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs-cli v2.2.2
2
+ * @forinda/kickjs-cli v2.2.3
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -1528,6 +1528,11 @@ export class Prisma${pascal}Repository implements I${pascal}Repository {
1528
1528
  function generateEntryFile(name, template, version) {
1529
1529
  switch (template) {
1530
1530
  case "graphql": return `import 'reflect-metadata'
1531
+ // Side-effect import — registers the extended env schema with kickjs
1532
+ // **before** any controller / service / @Value gets resolved. Without
1533
+ // this line ConfigService.get('YOUR_KEY') returns undefined because the
1534
+ // cached schema would still be the base shape. See guide/configuration.
1535
+ import './config'
1531
1536
  import { bootstrap } from '@forinda/kickjs'
1532
1537
  import { DevToolsAdapter } from '@forinda/kickjs-devtools'
1533
1538
  import { GraphQLAdapter } from '@forinda/kickjs-graphql'
@@ -1550,6 +1555,11 @@ export const app = await bootstrap({
1550
1555
  })
1551
1556
  `;
1552
1557
  case "cqrs": return `import 'reflect-metadata'
1558
+ // Side-effect import — registers the extended env schema with kickjs
1559
+ // **before** any controller / service / @Value gets resolved. Without
1560
+ // this line ConfigService.get('YOUR_KEY') returns undefined because the
1561
+ // cached schema would still be the base shape. See guide/configuration.
1562
+ import './config'
1553
1563
  import { bootstrap } from '@forinda/kickjs'
1554
1564
  import { DevToolsAdapter } from '@forinda/kickjs-devtools'
1555
1565
  import { SwaggerAdapter } from '@forinda/kickjs-swagger'
@@ -1577,6 +1587,11 @@ export const app = await bootstrap({
1577
1587
  })
1578
1588
  `;
1579
1589
  case "minimal": return `import 'reflect-metadata'
1590
+ // Side-effect import — registers the extended env schema with kickjs
1591
+ // **before** any controller / service / @Value gets resolved. Without
1592
+ // this line ConfigService.get('YOUR_KEY') returns undefined because the
1593
+ // cached schema would still be the base shape. See guide/configuration.
1594
+ import './config'
1580
1595
  import { bootstrap } from '@forinda/kickjs'
1581
1596
  import { modules } from './modules'
1582
1597
 
@@ -1584,6 +1599,11 @@ import { modules } from './modules'
1584
1599
  export const app = await bootstrap({ modules })
1585
1600
  `;
1586
1601
  default: return `import 'reflect-metadata'
1602
+ // Side-effect import — registers the extended env schema with kickjs
1603
+ // **before** any controller / service / @Value gets resolved. Without
1604
+ // this line ConfigService.get('YOUR_KEY') returns undefined because the
1605
+ // cached schema would still be the base shape. See guide/configuration.
1606
+ import './config'
1587
1607
  import express from 'express'
1588
1608
  import {
1589
1609
  bootstrap,
@@ -1626,10 +1646,17 @@ export const modules: AppModuleClass[] = [HelloModule]
1626
1646
  `;
1627
1647
  }
1628
1648
  /**
1629
- * Generate `src/env.ts` — the project's typed env schema.
1649
+ * Generate `src/config/index.ts` — the project's typed env schema.
1630
1650
  *
1631
1651
  * Default-exports a `defineEnv(...)` schema so `kick typegen` can
1632
- * infer it into the global `KickEnv` registry. After typegen runs:
1652
+ * infer it into the global `KickEnv` registry, and *also* calls
1653
+ * `loadEnv(envSchema)` as a module-load side effect so `ConfigService`
1654
+ * and `@Value()` see the extended shape from the very first DI
1655
+ * resolution. The companion `src/index.ts` template adds
1656
+ * `import './config'` immediately after `reflect-metadata` so the
1657
+ * registration runs before `bootstrap()` constructs anything.
1658
+ *
1659
+ * After typegen runs:
1633
1660
  *
1634
1661
  * @Value('DATABASE_URL') private url!: Env<'DATABASE_URL'>
1635
1662
  * process.env.DATABASE_URL // typed as string
@@ -1637,7 +1664,7 @@ export const modules: AppModuleClass[] = [HelloModule]
1637
1664
  * Both autocomplete and type-check at compile time.
1638
1665
  */
1639
1666
  function generateEnvFile() {
1640
- return `import { defineEnv } from '@forinda/kickjs/config'
1667
+ return `import { defineEnv, loadEnv } from '@forinda/kickjs/config'
1641
1668
  import { z } from 'zod'
1642
1669
 
1643
1670
  /**
@@ -1653,11 +1680,25 @@ import { z } from 'zod'
1653
1680
  * JWT_SECRET: z.string().min(32),
1654
1681
  * REDIS_URL: z.string().url().optional(),
1655
1682
  */
1656
- export default defineEnv((base) =>
1683
+ const envSchema = defineEnv((base) =>
1657
1684
  base.extend({
1658
1685
  // DATABASE_URL: z.string().url(),
1659
1686
  }),
1660
1687
  )
1688
+
1689
+ /**
1690
+ * IMPORTANT — side effect: register the schema with kickjs's env cache
1691
+ * **at module-load time**. \`ConfigService\` and \`@Value()\` both consume
1692
+ * this cache, and they will fall back to the base schema (or undefined)
1693
+ * if no extended schema has been registered before they're resolved.
1694
+ *
1695
+ * As long as \`src/index.ts\` imports this file (\`import './env'\`) at the
1696
+ * top — before \`bootstrap()\` runs — every controller and service in the
1697
+ * app sees the typed extended values.
1698
+ */
1699
+ export const env = loadEnv(envSchema)
1700
+
1701
+ export default envSchema
1661
1702
  `;
1662
1703
  }
1663
1704
  /** Generate src/modules/hello/hello.service.ts */
@@ -2644,13 +2685,16 @@ function generateViteConfig() {
2644
2685
  return `import { defineConfig } from 'vite'
2645
2686
  import { resolve } from 'node:path'
2646
2687
  import swc from 'unplugin-swc'
2647
- import { kickjsVitePlugin } from '@forinda/kickjs-vite'
2688
+ import { kickjsVitePlugin, envWatchPlugin } from '@forinda/kickjs-vite'
2648
2689
 
2649
2690
  export default defineConfig({
2650
2691
  oxc: false,
2651
2692
  plugins: [
2652
2693
  swc.vite(),
2653
2694
  kickjsVitePlugin({ entry: 'src/index.ts' }),
2695
+ // Watches .env files and triggers a full reload on change so the
2696
+ // dev server picks up env tweaks without a manual restart.
2697
+ envWatchPlugin(),
2654
2698
  ],
2655
2699
  resolve: {
2656
2700
  alias: {
@@ -3035,7 +3079,20 @@ kick add --list # Show all available packages
3035
3079
 
3036
3080
  ## Environment Configuration
3037
3081
 
3038
- Edit \`.env\` for environment variables. Access them with \`@Value()\` decorator:
3082
+ The project's typed env schema lives in **\`src/config/index.ts\`**
3083
+ extend the base schema there with your application-specific keys, and
3084
+ the schema is auto-registered with kickjs at module load. The companion
3085
+ \`src/index.ts\` imports it as a side effect (\`import './config'\`) **before**
3086
+ \`bootstrap()\` runs, so every \`@Service\`, \`@Controller\`, \`@Value\`, and
3087
+ \`ConfigService\` resolution sees the validated extended values.
3088
+
3089
+ > **Do not delete \`import './config'\` from \`src/index.ts\`.** It is the
3090
+ > registration step that wires \`ConfigService\` to your env schema.
3091
+ > Without it, \`config.get('YOUR_KEY')\` returns \`undefined\` for every
3092
+ > user-defined key and \`@Value('YOUR_KEY')\` only works because of a
3093
+ > raw \`process.env\` fallback (Zod coercion + defaults are skipped).
3094
+
3095
+ Edit \`.env\` for variable values. Access them with \`@Value()\`:
3039
3096
 
3040
3097
  \`\`\`ts
3041
3098
  import { Value } from '@forinda/kickjs'
@@ -3053,7 +3110,7 @@ export class ApiService {
3053
3110
  Or use \`ConfigService\`:
3054
3111
 
3055
3112
  \`\`\`ts
3056
- import { ConfigService } from '@forinda/kickjs'
3113
+ import { Service, Autowired, ConfigService } from '@forinda/kickjs'
3057
3114
 
3058
3115
  @Service()
3059
3116
  export class AppService {
@@ -3061,11 +3118,16 @@ export class AppService {
3061
3118
  private config!: ConfigService
3062
3119
 
3063
3120
  getPort() {
3064
- return this.config.get('PORT', 3000)
3121
+ // typed: number, Zod-coerced from baseEnvSchema
3122
+ return this.config.get('PORT')
3065
3123
  }
3066
3124
  }
3067
3125
  \`\`\`
3068
3126
 
3127
+ Hot-reload of \`.env\` changes during dev is wired up automatically via
3128
+ \`envWatchPlugin()\` in \`vite.config.ts\` — edit \`.env\`, the dev server
3129
+ reloads, and the next \`config.get()\` re-parses with the new values.
3130
+
3069
3131
  ## Testing
3070
3132
 
3071
3133
  Tests live in \`src/**/*.test.ts\`:
@@ -3127,6 +3189,7 @@ ${template === "cqrs" ? `### CQRS/Event Decorators
3127
3189
  3. **Always use \`ctx.body\`** — never \`req.body\` directly
3128
3190
  4. **DI requires \`reflect-metadata\`** — already imported in \`src/index.ts\`
3129
3191
  5. **Vite HMR requires proper cleanup** — adapters should implement \`shutdown()\`
3192
+ 6. **Never delete \`import './config'\` from \`src/index.ts\`** — that side-effect import registers the env schema with kickjs. Without it \`ConfigService.get('YOUR_KEY')\` returns \`undefined\` for every user-defined key. \`@Value('YOUR_KEY')\` *appears* to keep working but only via a raw \`process.env\` fallback (Zod coercion + schema defaults are silently skipped).
3130
3193
 
3131
3194
  ## Learn More
3132
3195
 
@@ -3158,7 +3221,8 @@ This guide helps AI agents (Claude, Copilot, etc.) work effectively on this Kick
3158
3221
  | Entry point | \`src/index.ts\` |
3159
3222
  | Module registry | \`src/modules/index.ts\` |
3160
3223
  | Feature modules | \`src/modules/<module-name>/\` |
3161
- ${template === "graphql" ? "| GraphQL resolvers | `src/resolvers/` |\n" : ""}| Environment config | \`.env\` |
3224
+ ${template === "graphql" ? "| GraphQL resolvers | `src/resolvers/` |\n" : ""}| Env values | \`.env\` |
3225
+ | Env schema (Zod) | \`src/config/index.ts\` |
3162
3226
  | TypeScript config | \`tsconfig.json\` |
3163
3227
  | Vite config (HMR) | \`vite.config.ts\` |
3164
3228
  | Vitest config | \`vitest.config.ts\` |
@@ -3368,26 +3432,37 @@ Run tests:
3368
3432
 
3369
3433
  ## Environment Variables
3370
3434
 
3371
- Managed via \`.env\` file. Access with:
3435
+ Schema is declared in \`src/config/index.ts\` (extends the base
3436
+ \`PORT\`/\`NODE_ENV\`/\`LOG_LEVEL\` shape via \`defineEnv\`) and registered
3437
+ with kickjs at module load. \`src/index.ts\` imports it via
3438
+ \`import './config'\` **before** \`bootstrap()\` so the cache is populated
3439
+ in time for DI. Add new keys to the schema, drop their values into
3440
+ \`.env\`, and they're typed everywhere.
3441
+
3442
+ Access patterns:
3372
3443
 
3373
- 1. **@Value() decorator** (recommended):
3444
+ 1. **@Value() decorator** (recommended for known-at-construction keys):
3374
3445
  \`\`\`ts
3375
3446
  @Value('DATABASE_URL')
3376
3447
  private dbUrl!: string
3377
3448
  \`\`\`
3378
3449
 
3379
- 2. **ConfigService** (for dynamic access):
3450
+ 2. **ConfigService** (recommended for dynamic / method-scoped access):
3380
3451
  \`\`\`ts
3381
3452
  @Autowired()
3382
3453
  private config!: ConfigService
3383
3454
 
3384
- const port = this.config.get('PORT', 3000)
3455
+ const port = this.config.get('PORT') // typed: number
3385
3456
  \`\`\`
3386
3457
 
3387
- 3. **Direct access** (avoid in app code):
3388
- \`\`\`ts
3389
- process.env.PORT
3390
- \`\`\`
3458
+ 3. **Direct \`process.env\`** avoid in app code; bypasses Zod
3459
+ coercion and the typed \`KickEnv\` registry.
3460
+
3461
+ > **Pitfall**: never delete \`import './config'\` from \`src/index.ts\`.
3462
+ > If the schema is not registered before DI runs, \`config.get()\`
3463
+ > returns \`undefined\` for user keys (the base shape only) and
3464
+ > \`@Value()\` only works because of its raw \`process.env\` fallback —
3465
+ > Zod coercion + schema defaults are silently skipped.
3391
3466
 
3392
3467
  ## Key Decorators
3393
3468
 
@@ -3434,6 +3509,7 @@ ${template === "graphql" ? `### GraphQL
3434
3509
  4. **Routes not found** — Check controller path and module registration
3435
3510
  5. **HMR not working** — Verify \`vite.config.ts\` has \`hmr: true\`
3436
3511
  6. **Decorators not working** — Check \`tsconfig.json\` has \`experimentalDecorators: true\`
3512
+ 7. **\`config.get('YOUR_KEY')\` returns \`undefined\`** — \`src/index.ts\` is missing \`import './config'\`. That side-effect import registers the env schema with kickjs (\`loadEnv(envSchema)\` runs at module load). Without it, \`ConfigService\` falls back to the base schema (\`PORT\`/\`NODE_ENV\`/\`LOG_LEVEL\` only) and every user-defined key reads as \`undefined\`. \`@Value()\` may *appear* to work because of a raw \`process.env\` fallback, but Zod coercion and schema defaults are silently skipped — investigate \`src/index.ts\` and \`src/config/index.ts\` first.
3437
3513
 
3438
3514
  ## CLI Commands Reference
3439
3515
 
@@ -3486,7 +3562,7 @@ async function initProject(options) {
3486
3562
  await writeFileSafe(join(dir, ".gitattributes"), generateGitAttributes());
3487
3563
  await writeFileSafe(join(dir, ".env"), generateEnv());
3488
3564
  await writeFileSafe(join(dir, ".env.example"), generateEnvExample());
3489
- await writeFileSafe(join(dir, "src/env.ts"), generateEnvFile());
3565
+ await writeFileSafe(join(dir, "src/config/index.ts"), generateEnvFile());
3490
3566
  await writeFileSafe(join(dir, "src/index.ts"), generateEntryFile(name, template, cliPkg.version));
3491
3567
  await writeFileSafe(join(dir, "src/modules/index.ts"), generateModulesIndex());
3492
3568
  await writeFileSafe(join(dir, "src/modules/hello/hello.service.ts"), generateHelloService());
@@ -3532,7 +3608,7 @@ async function initProject(options) {
3532
3608
  }
3533
3609
  }
3534
3610
  try {
3535
- const { runTypegen } = await import("./typegen-CDS6VbOd.mjs");
3611
+ const { runTypegen } = await import("./typegen-BncsvEr-.mjs");
3536
3612
  await runTypegen({
3537
3613
  cwd: dir,
3538
3614
  allowDuplicates: true,