@forinda/kickjs-cli 2.2.1 → 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.1
2
+ * @forinda/kickjs-cli v2.2.3
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -55,7 +55,7 @@ async function fileExists(filePath) {
55
55
  function generatePackageJson(name, template, kickjsVersion) {
56
56
  const baseDeps = {
57
57
  "@forinda/kickjs": kickjsVersion,
58
- "@forinda/kickjs-config": kickjsVersion,
58
+ dotenv: "^17.3.1",
59
59
  express: "^5.1.0",
60
60
  "reflect-metadata": "^0.2.2",
61
61
  zod: "^4.3.6",
@@ -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 */
@@ -535,11 +579,7 @@ function generateReadme(name, template, pm) {
535
579
  cqrs: "CQRS + Event-Driven",
536
580
  minimal: "Minimal"
537
581
  };
538
- const packages = [
539
- "@forinda/kickjs",
540
- "@forinda/kickjs-vite",
541
- "@forinda/kickjs-config"
542
- ];
582
+ const packages = ["@forinda/kickjs", "@forinda/kickjs-vite"];
543
583
  if (template !== "minimal") packages.push("@forinda/kickjs-swagger", "@forinda/kickjs-devtools");
544
584
  if (template === "graphql") packages.push("@forinda/kickjs-graphql");
545
585
  if (template === "cqrs") packages.push("@forinda/kickjs-queue", "@forinda/kickjs-ws", "@forinda/kickjs-otel");
@@ -775,10 +815,23 @@ kick add --list # Show all available packages
775
815
 
776
816
  ## Environment Configuration
777
817
 
778
- 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()\`:
779
832
 
780
833
  \`\`\`ts
781
- import { Value } from '@forinda/kickjs-config'
834
+ import { Value } from '@forinda/kickjs'
782
835
 
783
836
  @Service()
784
837
  export class ApiService {
@@ -793,7 +846,7 @@ export class ApiService {
793
846
  Or use \`ConfigService\`:
794
847
 
795
848
  \`\`\`ts
796
- import { ConfigService } from '@forinda/kickjs-config'
849
+ import { Service, Autowired, ConfigService } from '@forinda/kickjs'
797
850
 
798
851
  @Service()
799
852
  export class AppService {
@@ -801,11 +854,16 @@ export class AppService {
801
854
  private config!: ConfigService
802
855
 
803
856
  getPort() {
804
- return this.config.get('PORT', 3000)
857
+ // typed: number, Zod-coerced from baseEnvSchema
858
+ return this.config.get('PORT')
805
859
  }
806
860
  }
807
861
  \`\`\`
808
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
+
809
867
  ## Testing
810
868
 
811
869
  Tests live in \`src/**/*.test.ts\`:
@@ -867,6 +925,7 @@ ${template === "cqrs" ? `### CQRS/Event Decorators
867
925
  3. **Always use \`ctx.body\`** — never \`req.body\` directly
868
926
  4. **DI requires \`reflect-metadata\`** — already imported in \`src/index.ts\`
869
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).
870
929
 
871
930
  ## Learn More
872
931
 
@@ -898,7 +957,8 @@ This guide helps AI agents (Claude, Copilot, etc.) work effectively on this Kick
898
957
  | Entry point | \`src/index.ts\` |
899
958
  | Module registry | \`src/modules/index.ts\` |
900
959
  | Feature modules | \`src/modules/<module-name>/\` |
901
- ${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\` |
902
962
  | TypeScript config | \`tsconfig.json\` |
903
963
  | Vite config (HMR) | \`vite.config.ts\` |
904
964
  | Vitest config | \`vitest.config.ts\` |
@@ -1108,26 +1168,37 @@ Run tests:
1108
1168
 
1109
1169
  ## Environment Variables
1110
1170
 
1111
- 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.
1112
1177
 
1113
- 1. **@Value() decorator** (recommended):
1178
+ Access patterns:
1179
+
1180
+ 1. **@Value() decorator** (recommended for known-at-construction keys):
1114
1181
  \`\`\`ts
1115
1182
  @Value('DATABASE_URL')
1116
1183
  private dbUrl!: string
1117
1184
  \`\`\`
1118
1185
 
1119
- 2. **ConfigService** (for dynamic access):
1186
+ 2. **ConfigService** (recommended for dynamic / method-scoped access):
1120
1187
  \`\`\`ts
1121
1188
  @Autowired()
1122
1189
  private config!: ConfigService
1123
1190
 
1124
- const port = this.config.get('PORT', 3000)
1191
+ const port = this.config.get('PORT') // typed: number
1125
1192
  \`\`\`
1126
1193
 
1127
- 3. **Direct access** (avoid in app code):
1128
- \`\`\`ts
1129
- process.env.PORT
1130
- \`\`\`
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.
1131
1202
 
1132
1203
  ## Key Decorators
1133
1204
 
@@ -1174,6 +1245,7 @@ ${template === "graphql" ? `### GraphQL
1174
1245
  4. **Routes not found** — Check controller path and module registration
1175
1246
  5. **HMR not working** — Verify \`vite.config.ts\` has \`hmr: true\`
1176
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.
1177
1249
 
1178
1250
  ## CLI Commands Reference
1179
1251
 
@@ -1226,7 +1298,7 @@ async function initProject(options) {
1226
1298
  await writeFileSafe(join(dir, ".gitattributes"), generateGitAttributes());
1227
1299
  await writeFileSafe(join(dir, ".env"), generateEnv());
1228
1300
  await writeFileSafe(join(dir, ".env.example"), generateEnvExample());
1229
- await writeFileSafe(join(dir, "src/env.ts"), generateEnvFile());
1301
+ await writeFileSafe(join(dir, "src/config/index.ts"), generateEnvFile());
1230
1302
  await writeFileSafe(join(dir, "src/index.ts"), generateEntryFile(name, template, cliPkg.version));
1231
1303
  await writeFileSafe(join(dir, "src/modules/index.ts"), generateModulesIndex());
1232
1304
  await writeFileSafe(join(dir, "src/modules/hello/hello.service.ts"), generateHelloService());
@@ -4864,31 +4936,49 @@ function extractInjectsFromSource(source, filePath, cwd) {
4864
4936
  return out;
4865
4937
  }
4866
4938
  /**
4867
- * Look for an env schema file at `<cwd>/<envFile>`. Returns a
4868
- * `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
4869
4958
  * `defineEnv(...)` call and a default export — the two markers we
4870
4959
  * need before it's safe to emit `import type schema from '...'` in
4871
- * the generator.
4872
- *
4873
- * Returns `null` for any other state (file missing, no defineEnv, no
4874
- * default export) so the generator skips env typing silently. Users
4875
- * who want env typing must opt in by writing `src/env.ts` to the
4876
- * 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.
4877
4963
  */
4878
4964
  async function detectEnvFile(cwd, envFile) {
4879
- const abs = resolve(cwd, envFile);
4880
- let source;
4881
- try {
4882
- source = await readFile(abs, "utf-8");
4883
- } catch {
4884
- 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
+ };
4885
4980
  }
4886
- if (!/\bdefineEnv\s*\(/.test(source)) return null;
4887
- if (!/export\s+default\b/.test(source)) return null;
4888
- return {
4889
- filePath: abs,
4890
- relativePath: toRelative(abs, cwd)
4891
- };
4981
+ return null;
4892
4982
  }
4893
4983
  /** Detect duplicate class names across files */
4894
4984
  function findCollisions(classes) {
@@ -6111,9 +6201,9 @@ const PACKAGE_REGISTRY = {
6111
6201
  dev: true
6112
6202
  },
6113
6203
  config: {
6114
- pkg: "@forinda/kickjs-config",
6204
+ pkg: "dotenv",
6115
6205
  peers: [],
6116
- description: "Zod-based env validation"
6206
+ description: "Optional .env file loader (kickjs ConfigService now ships in @forinda/kickjs)"
6117
6207
  },
6118
6208
  cli: {
6119
6209
  pkg: "@forinda/kickjs-cli",
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs-cli v2.2.1
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 */
@@ -2577,7 +2618,7 @@ export type ${pascal}DTO = z.infer<typeof ${camel}Schema>
2577
2618
  function generatePackageJson(name, template, kickjsVersion) {
2578
2619
  const baseDeps = {
2579
2620
  "@forinda/kickjs": kickjsVersion,
2580
- "@forinda/kickjs-config": kickjsVersion,
2621
+ dotenv: "^17.3.1",
2581
2622
  express: "^5.1.0",
2582
2623
  "reflect-metadata": "^0.2.2",
2583
2624
  zod: "^4.3.6",
@@ -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: {
@@ -2799,11 +2843,7 @@ function generateReadme(name, template, pm) {
2799
2843
  cqrs: "CQRS + Event-Driven",
2800
2844
  minimal: "Minimal"
2801
2845
  };
2802
- const packages = [
2803
- "@forinda/kickjs",
2804
- "@forinda/kickjs-vite",
2805
- "@forinda/kickjs-config"
2806
- ];
2846
+ const packages = ["@forinda/kickjs", "@forinda/kickjs-vite"];
2807
2847
  if (template !== "minimal") packages.push("@forinda/kickjs-swagger", "@forinda/kickjs-devtools");
2808
2848
  if (template === "graphql") packages.push("@forinda/kickjs-graphql");
2809
2849
  if (template === "cqrs") packages.push("@forinda/kickjs-queue", "@forinda/kickjs-ws", "@forinda/kickjs-otel");
@@ -3039,10 +3079,23 @@ kick add --list # Show all available packages
3039
3079
 
3040
3080
  ## Environment Configuration
3041
3081
 
3042
- 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()\`:
3043
3096
 
3044
3097
  \`\`\`ts
3045
- import { Value } from '@forinda/kickjs-config'
3098
+ import { Value } from '@forinda/kickjs'
3046
3099
 
3047
3100
  @Service()
3048
3101
  export class ApiService {
@@ -3057,7 +3110,7 @@ export class ApiService {
3057
3110
  Or use \`ConfigService\`:
3058
3111
 
3059
3112
  \`\`\`ts
3060
- import { ConfigService } from '@forinda/kickjs-config'
3113
+ import { Service, Autowired, ConfigService } from '@forinda/kickjs'
3061
3114
 
3062
3115
  @Service()
3063
3116
  export class AppService {
@@ -3065,11 +3118,16 @@ export class AppService {
3065
3118
  private config!: ConfigService
3066
3119
 
3067
3120
  getPort() {
3068
- return this.config.get('PORT', 3000)
3121
+ // typed: number, Zod-coerced from baseEnvSchema
3122
+ return this.config.get('PORT')
3069
3123
  }
3070
3124
  }
3071
3125
  \`\`\`
3072
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
+
3073
3131
  ## Testing
3074
3132
 
3075
3133
  Tests live in \`src/**/*.test.ts\`:
@@ -3131,6 +3189,7 @@ ${template === "cqrs" ? `### CQRS/Event Decorators
3131
3189
  3. **Always use \`ctx.body\`** — never \`req.body\` directly
3132
3190
  4. **DI requires \`reflect-metadata\`** — already imported in \`src/index.ts\`
3133
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).
3134
3193
 
3135
3194
  ## Learn More
3136
3195
 
@@ -3162,7 +3221,8 @@ This guide helps AI agents (Claude, Copilot, etc.) work effectively on this Kick
3162
3221
  | Entry point | \`src/index.ts\` |
3163
3222
  | Module registry | \`src/modules/index.ts\` |
3164
3223
  | Feature modules | \`src/modules/<module-name>/\` |
3165
- ${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\` |
3166
3226
  | TypeScript config | \`tsconfig.json\` |
3167
3227
  | Vite config (HMR) | \`vite.config.ts\` |
3168
3228
  | Vitest config | \`vitest.config.ts\` |
@@ -3372,26 +3432,37 @@ Run tests:
3372
3432
 
3373
3433
  ## Environment Variables
3374
3434
 
3375
- 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:
3376
3443
 
3377
- 1. **@Value() decorator** (recommended):
3444
+ 1. **@Value() decorator** (recommended for known-at-construction keys):
3378
3445
  \`\`\`ts
3379
3446
  @Value('DATABASE_URL')
3380
3447
  private dbUrl!: string
3381
3448
  \`\`\`
3382
3449
 
3383
- 2. **ConfigService** (for dynamic access):
3450
+ 2. **ConfigService** (recommended for dynamic / method-scoped access):
3384
3451
  \`\`\`ts
3385
3452
  @Autowired()
3386
3453
  private config!: ConfigService
3387
3454
 
3388
- const port = this.config.get('PORT', 3000)
3455
+ const port = this.config.get('PORT') // typed: number
3389
3456
  \`\`\`
3390
3457
 
3391
- 3. **Direct access** (avoid in app code):
3392
- \`\`\`ts
3393
- process.env.PORT
3394
- \`\`\`
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.
3395
3466
 
3396
3467
  ## Key Decorators
3397
3468
 
@@ -3438,6 +3509,7 @@ ${template === "graphql" ? `### GraphQL
3438
3509
  4. **Routes not found** — Check controller path and module registration
3439
3510
  5. **HMR not working** — Verify \`vite.config.ts\` has \`hmr: true\`
3440
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.
3441
3513
 
3442
3514
  ## CLI Commands Reference
3443
3515
 
@@ -3490,7 +3562,7 @@ async function initProject(options) {
3490
3562
  await writeFileSafe(join(dir, ".gitattributes"), generateGitAttributes());
3491
3563
  await writeFileSafe(join(dir, ".env"), generateEnv());
3492
3564
  await writeFileSafe(join(dir, ".env.example"), generateEnvExample());
3493
- await writeFileSafe(join(dir, "src/env.ts"), generateEnvFile());
3565
+ await writeFileSafe(join(dir, "src/config/index.ts"), generateEnvFile());
3494
3566
  await writeFileSafe(join(dir, "src/index.ts"), generateEntryFile(name, template, cliPkg.version));
3495
3567
  await writeFileSafe(join(dir, "src/modules/index.ts"), generateModulesIndex());
3496
3568
  await writeFileSafe(join(dir, "src/modules/hello/hello.service.ts"), generateHelloService());
@@ -3536,7 +3608,7 @@ async function initProject(options) {
3536
3608
  }
3537
3609
  }
3538
3610
  try {
3539
- const { runTypegen } = await import("./typegen-UejiKdXA.mjs");
3611
+ const { runTypegen } = await import("./typegen-BncsvEr-.mjs");
3540
3612
  await runTypegen({
3541
3613
  cwd: dir,
3542
3614
  allowDuplicates: true,