@forinda/kickjs-cli 3.2.0 → 4.1.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.
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs-cli v3.2.0
2
+ * @forinda/kickjs-cli v4.1.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -8,18 +8,68 @@
8
8
  *
9
9
  * @license MIT
10
10
  */
11
- import { dirname, join, resolve } from "node:path";
11
+ import { createRequire } from "node:module";
12
+ import { dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
12
13
  import { access, mkdir, readFile, writeFile } from "node:fs/promises";
13
14
  import * as clack from "@clack/prompts";
14
15
  import pc from "picocolors";
15
16
  import pkg from "pluralize";
16
17
  import { execSync } from "node:child_process";
17
- import { readFileSync } from "node:fs";
18
+ import { existsSync, readFileSync } from "node:fs";
18
19
  import { fileURLToPath } from "node:url";
19
- /** Write a file, creating parent directories if needed. Skips writing in dry run mode. */
20
+ /** Extensions prettier can format. Anything else is written verbatim. */
21
+ const FORMATTABLE = new Set([
22
+ ".ts",
23
+ ".tsx",
24
+ ".js",
25
+ ".jsx",
26
+ ".mjs",
27
+ ".cjs",
28
+ ".json",
29
+ ".md"
30
+ ]);
31
+ /**
32
+ * Write a file, creating parent directories if needed.
33
+ *
34
+ * After write, runs prettier against the file when:
35
+ * - format-on-write is enabled (default)
36
+ * - the extension is in {@link FORMATTABLE}
37
+ * - prettier resolves from the user's project (or our own cwd)
38
+ *
39
+ * Failures (missing prettier, unparseable source, prettier crash) are
40
+ * swallowed silently — formatting is a polish step, not a correctness
41
+ * gate. The pre-existing pre-commit hook still catches anything we
42
+ * couldn't format.
43
+ *
44
+ * Skips writing entirely in dry run mode.
45
+ */
20
46
  async function writeFileSafe(filePath, content) {
21
47
  await mkdir(dirname(filePath), { recursive: true });
22
48
  await writeFile(filePath, content, "utf-8");
49
+ if (FORMATTABLE.has(extname(filePath))) await formatFile(filePath, content).catch(() => {});
50
+ }
51
+ let _prettier = void 0;
52
+ /** Resolve prettier from the user's project; cache the result (or null) for the process. */
53
+ function resolvePrettier(cwd) {
54
+ if (_prettier !== void 0) return _prettier;
55
+ try {
56
+ _prettier = createRequire(join(cwd, "package.json"))("prettier");
57
+ } catch {
58
+ _prettier = null;
59
+ }
60
+ return _prettier;
61
+ }
62
+ async function formatFile(filePath, content) {
63
+ const prettier = resolvePrettier(process.cwd());
64
+ if (!prettier) return;
65
+ if ((await prettier.getFileInfo(filePath, { resolveConfig: true })).ignored) return;
66
+ const config = await prettier.resolveConfig(filePath) ?? {};
67
+ const formatted = await prettier.format(content, {
68
+ ...config,
69
+ filepath: filePath
70
+ });
71
+ if (formatted === content) return;
72
+ await writeFile(filePath, formatted, "utf-8");
23
73
  }
24
74
  /** Check if a file exists */
25
75
  async function fileExists(filePath) {
@@ -551,7 +601,7 @@ export interface I${pascal}Repository {
551
601
  * \`@Inject(${pascal.toUpperCase()}_REPOSITORY)\` both return the typed
552
602
  * interface — no manual generic, no \`any\` cast.
553
603
  */
554
- export const ${pascal.toUpperCase()}_REPOSITORY = createToken<I${pascal}Repository>('${pascal}/Repository')
604
+ export const ${pascal.toUpperCase()}_REPOSITORY = createToken<I${pascal}Repository>('app/${kebab}/repository')
555
605
  `;
556
606
  }
557
607
  function generateInMemoryRepository(ctx) {
@@ -1543,15 +1593,15 @@ function generateEntryFile(name, template, version, packages = []) {
1543
1593
  const gqlAdapters = [];
1544
1594
  if (packages.includes("devtools")) {
1545
1595
  gqlImports.push(`import { DevToolsAdapter } from '@forinda/kickjs-devtools'`);
1546
- gqlAdapters.push(` new DevToolsAdapter(),`);
1596
+ gqlAdapters.push(` DevToolsAdapter(),`);
1547
1597
  }
1548
1598
  if (packages.includes("otel")) {
1549
1599
  gqlImports.push(`import { OtelAdapter } from '@forinda/kickjs-otel'`);
1550
- gqlAdapters.push(` new OtelAdapter({ serviceName: '${name}' }),`);
1600
+ gqlAdapters.push(` OtelAdapter({ serviceName: '${name}' }),`);
1551
1601
  }
1552
1602
  if (packages.includes("swagger")) {
1553
1603
  gqlImports.push(`import { SwaggerAdapter } from '@forinda/kickjs-swagger'`);
1554
- gqlAdapters.push(` new SwaggerAdapter({ info: { title: '${name}', version: '${version}' } }),`);
1604
+ gqlAdapters.push(` SwaggerAdapter({ info: { title: '${name}', version: '${version}' } }),`);
1555
1605
  }
1556
1606
  return `import 'reflect-metadata'
1557
1607
  // Side-effect import — registers the extended env schema with kickjs
@@ -1584,15 +1634,15 @@ ${gqlAdapters.length ? gqlAdapters.join("\n") + "\n" : ""} new GraphQLAdapter
1584
1634
  const cqrsAdapters = [];
1585
1635
  if (packages.includes("otel")) {
1586
1636
  cqrsImports.push(`import { OtelAdapter } from '@forinda/kickjs-otel'`);
1587
- cqrsAdapters.push(` new OtelAdapter({ serviceName: '${name}' }),`);
1637
+ cqrsAdapters.push(` OtelAdapter({ serviceName: '${name}' }),`);
1588
1638
  }
1589
1639
  if (packages.includes("devtools")) {
1590
1640
  cqrsImports.push(`import { DevToolsAdapter } from '@forinda/kickjs-devtools'`);
1591
- cqrsAdapters.push(` new DevToolsAdapter(),`);
1641
+ cqrsAdapters.push(` DevToolsAdapter(),`);
1592
1642
  }
1593
1643
  if (packages.includes("swagger")) {
1594
1644
  cqrsImports.push(`import { SwaggerAdapter } from '@forinda/kickjs-swagger'`);
1595
- cqrsAdapters.push(` new SwaggerAdapter({\n info: { title: '${name}', version: '${version}' },\n }),`);
1645
+ cqrsAdapters.push(` SwaggerAdapter({\n info: { title: '${name}', version: '${version}' },\n }),`);
1596
1646
  }
1597
1647
  if (packages.includes("graphql")) {
1598
1648
  cqrsImports.push(`import { GraphQLAdapter } from '@forinda/kickjs-graphql'`);
@@ -1611,7 +1661,7 @@ ${cqrsImports.length ? cqrsImports.join("\n") + "\n" : ""}import { modules } fro
1611
1661
 
1612
1662
  // Export the app for the Vite plugin (dev mode)
1613
1663
  export const app = await bootstrap({
1614
- modules,${cqrsImports.length ? `\n adapters: [\n${cqrsAdapters.join("\n")}\n // Uncomment for WebSocket support:\n // new WsAdapter(),\n // Uncomment when Redis is available:\n // new QueueAdapter({\n // provider: new BullMQProvider({ host: 'localhost', port: 6379 }),\n // }),\n ],` : `\n adapters: [\n // Uncomment for WebSocket support:\n // new WsAdapter(),\n // Uncomment when Redis is available:\n // new QueueAdapter({\n // provider: new BullMQProvider({ host: 'localhost', port: 6379 }),\n // }),\n ],`}
1664
+ modules,${cqrsImports.length ? `\n adapters: [\n${cqrsAdapters.join("\n")}\n // Uncomment for WebSocket support:\n // WsAdapter(),\n // Uncomment when Redis is available:\n // QueueAdapter({\n // provider: new BullMQProvider({ host: 'localhost', port: 6379 }),\n // }),\n ],` : `\n adapters: [\n // Uncomment for WebSocket support:\n // WsAdapter(),\n // Uncomment when Redis is available:\n // QueueAdapter({\n // provider: new BullMQProvider({ host: 'localhost', port: 6379 }),\n // }),\n ],`}
1615
1665
  })
1616
1666
  `;
1617
1667
  }
@@ -1620,15 +1670,15 @@ export const app = await bootstrap({
1620
1670
  const adapters = [];
1621
1671
  if (packages.includes("swagger")) {
1622
1672
  imports.push(`import { SwaggerAdapter } from '@forinda/kickjs-swagger'`);
1623
- adapters.push(` new SwaggerAdapter({ info: { title: '${name}', version: '${version}' } }),`);
1673
+ adapters.push(` SwaggerAdapter({ info: { title: '${name}', version: '${version}' } }),`);
1624
1674
  }
1625
1675
  if (packages.includes("devtools")) {
1626
1676
  imports.push(`import { DevToolsAdapter } from '@forinda/kickjs-devtools'`);
1627
- adapters.push(` new DevToolsAdapter(),`);
1677
+ adapters.push(` DevToolsAdapter(),`);
1628
1678
  }
1629
1679
  if (packages.includes("otel")) {
1630
1680
  imports.push(`import { OtelAdapter } from '@forinda/kickjs-otel'`);
1631
- adapters.push(` new OtelAdapter({ serviceName: '${name}' }),`);
1681
+ adapters.push(` OtelAdapter({ serviceName: '${name}' }),`);
1632
1682
  }
1633
1683
  if (packages.includes("graphql")) {
1634
1684
  imports.push(`import { GraphQLAdapter } from '@forinda/kickjs-graphql'`);
@@ -1652,15 +1702,15 @@ export const app = await bootstrap({ modules${adapters.length ? `,\n adapters:
1652
1702
  const restAdapters = [];
1653
1703
  if (packages.includes("devtools")) {
1654
1704
  restImports.push(`import { DevToolsAdapter } from '@forinda/kickjs-devtools'`);
1655
- restAdapters.push(` new DevToolsAdapter(),`);
1705
+ restAdapters.push(` DevToolsAdapter(),`);
1656
1706
  }
1657
1707
  if (packages.includes("swagger")) {
1658
1708
  restImports.push(`import { SwaggerAdapter } from '@forinda/kickjs-swagger'`);
1659
- restAdapters.push(` new SwaggerAdapter({\n info: { title: '${name}', version: '${version}' },\n }),`);
1709
+ restAdapters.push(` SwaggerAdapter({\n info: { title: '${name}', version: '${version}' },\n }),`);
1660
1710
  }
1661
1711
  if (packages.includes("otel")) {
1662
1712
  restImports.push(`import { OtelAdapter } from '@forinda/kickjs-otel'`);
1663
- restAdapters.push(` new OtelAdapter({ serviceName: '${name}' }),`);
1713
+ restAdapters.push(` OtelAdapter({ serviceName: '${name}' }),`);
1664
1714
  }
1665
1715
  return `import 'reflect-metadata'
1666
1716
  // Side-effect import — registers the extended env schema with kickjs
@@ -2311,97 +2361,114 @@ export const modules: AppModuleClass[] = [${pascal}Module]
2311
2361
  }
2312
2362
  //#endregion
2313
2363
  //#region src/generators/adapter.ts
2364
+ /**
2365
+ * Scaffold a `defineAdapter()` factory under `src/adapters/<name>.adapter.ts`.
2366
+ *
2367
+ * v4 dropped the `class implements AppAdapter` pattern in favour of the
2368
+ * `defineAdapter()` factory (architecture.md §21.3.4). The generated
2369
+ * template uses the new factory shape so adopters get a working
2370
+ * adapter with all four lifecycle hooks (beforeMount, beforeStart,
2371
+ * afterStart, shutdown), a typed config object with defaults, and the
2372
+ * factory's call / `.scoped()` / `.async()` surfaces — without
2373
+ * writing a single class.
2374
+ */
2314
2375
  async function generateAdapter(options) {
2315
2376
  const { name, outDir } = options;
2316
2377
  const kebab = toKebabCase(name);
2317
2378
  const pascal = toPascalCase(name);
2318
2379
  const files = [];
2319
2380
  const filePath = join(outDir, `${kebab}.adapter.ts`);
2320
- await writeFileSafe(filePath, `import type { AppAdapter, AdapterContext, AdapterMiddleware } from '@forinda/kickjs'
2381
+ await writeFileSafe(filePath, `import { defineAdapter, type AdapterContext, type AdapterMiddleware } from '@forinda/kickjs'
2321
2382
 
2322
- export interface ${pascal}AdapterOptions {
2323
- // Add your adapter configuration here
2383
+ /**
2384
+ * Configuration for the ${pascal} adapter.
2385
+ *
2386
+ * Adapters typically take a small config object so callers can tune
2387
+ * behaviour at bootstrap time. Keep the shape narrow — anything
2388
+ * derived from the environment should be read inside the build
2389
+ * function via getEnv(), not forced onto the caller.
2390
+ */
2391
+ export interface ${pascal}AdapterConfig {
2392
+ // Add your adapter configuration here, e.g.:
2393
+ // enabled?: boolean
2394
+ // apiKey?: string
2324
2395
  }
2325
2396
 
2326
2397
  /**
2327
- * ${pascal} adapter.
2398
+ * ${pascal} adapter — built via \`defineAdapter()\` so callers get the
2399
+ * factory's call / \`.scoped()\` / \`.async()\` surfaces for free.
2328
2400
  *
2329
2401
  * Hooks into the Application lifecycle to add middleware, routes,
2330
2402
  * or external service connections.
2331
2403
  *
2332
- * Usage:
2333
- * bootstrap({
2334
- * adapters: [new ${pascal}Adapter({ ... })],
2335
- * })
2404
+ * @example
2405
+ * \`\`\`ts
2406
+ * import { bootstrap } from '@forinda/kickjs'
2407
+ * import { ${pascal}Adapter } from './adapters/${kebab}.adapter'
2408
+ *
2409
+ * bootstrap({
2410
+ * modules,
2411
+ * adapters: [${pascal}Adapter({ /* config overrides *\\/ })],
2412
+ * })
2413
+ * \`\`\`
2336
2414
  */
2337
- export class ${pascal}Adapter implements AppAdapter {
2338
- name = '${pascal}Adapter'
2339
-
2340
- constructor(private options: ${pascal}AdapterOptions = {}) {}
2341
-
2342
- /**
2343
- * Return middleware entries that the Application will mount.
2344
- * Use \`phase\` to control where in the pipeline they run:
2345
- * 'beforeGlobal' | 'afterGlobal' | 'beforeRoutes' | 'afterRoutes'
2346
- */
2347
- middleware(): AdapterMiddleware[] {
2348
- return [
2349
- // Example: add a custom header to all responses
2350
- // {
2351
- // phase: 'beforeGlobal',
2352
- // handler: (_req: any, res: any, next: any) => {
2353
- // res.setHeader('X-${pascal}', 'true')
2354
- // next()
2355
- // },
2356
- // },
2357
- // Example: scope middleware to a specific path
2358
- // {
2359
- // phase: 'beforeRoutes',
2360
- // path: '/api/v1/admin',
2361
- // handler: myAdminMiddleware(),
2362
- // },
2363
- ]
2364
- }
2415
+ export const ${pascal}Adapter = defineAdapter<${pascal}AdapterConfig>({
2416
+ name: '${pascal}Adapter',
2417
+ defaults: {
2418
+ // Default config values go here
2419
+ },
2420
+ build: (_config, { name: _name }) => ({
2421
+ /**
2422
+ * Return middleware entries that the Application will mount.
2423
+ * \`phase\` controls where in the pipeline they run:
2424
+ * 'beforeGlobal' | 'afterGlobal' | 'beforeRoutes' | 'afterRoutes'.
2425
+ */
2426
+ middleware(): AdapterMiddleware[] {
2427
+ return [
2428
+ // Example: add a custom header to all responses
2429
+ // {
2430
+ // phase: 'beforeGlobal',
2431
+ // handler: (_req, res, next) => {
2432
+ // res.setHeader('X-${pascal}', 'true')
2433
+ // next()
2434
+ // },
2435
+ // },
2436
+ ]
2437
+ },
2365
2438
 
2366
- /**
2367
- * Called before global middleware.
2368
- * Use this to mount routes that bypass the middleware stack
2369
- * (health checks, docs UI, static assets).
2370
- */
2371
- beforeMount({ app }: AdapterContext): void {
2372
- // Example: mount a status route
2373
- // app.get('/${kebab}/status', (_req, res) => {
2374
- // res.json({ status: 'ok' })
2375
- // })
2376
- }
2439
+ /**
2440
+ * Called before global middleware. Use this to mount routes that
2441
+ * bypass the middleware stack (health checks, docs UI, static
2442
+ * assets).
2443
+ */
2444
+ beforeMount(_ctx: AdapterContext): void {
2445
+ // Example:
2446
+ // _ctx.app.get('/${kebab}/status', (_req, res) => res.json({ status: 'ok' }))
2447
+ },
2377
2448
 
2378
- /**
2379
- * Called after modules and routes are registered, before the server starts.
2380
- * Use this for late-stage DI registrations or config validation.
2381
- */
2382
- beforeStart({ container }: AdapterContext): void {
2383
- // Example: register a service in the DI container
2384
- // container.registerInstance(MY_TOKEN, new MyService(this.options))
2385
- }
2449
+ /**
2450
+ * Called after modules and routes are registered, before the
2451
+ * server starts. Use this for late-stage DI registrations or
2452
+ * config validation.
2453
+ */
2454
+ beforeStart(_ctx: AdapterContext): void {
2455
+ // Example: _ctx.container.bindToken(MY_TOKEN, new MyService(_config))
2456
+ },
2386
2457
 
2387
- /**
2388
- * Called after the HTTP server is listening.
2389
- * Use this to attach to the raw http.Server (Socket.IO, gRPC, etc).
2390
- */
2391
- afterStart({ server, container }: AdapterContext): void {
2392
- // Example: attach Socket.IO
2393
- // const io = new Server(server)
2394
- // container.registerInstance(SOCKET_IO, io)
2395
- }
2458
+ /**
2459
+ * Called after the HTTP server is listening. Use this to attach
2460
+ * to the raw http.Server (Socket.IO, gRPC, etc).
2461
+ */
2462
+ afterStart(_ctx: AdapterContext): void {
2463
+ // Example: const io = new Server(_ctx.server)
2464
+ },
2396
2465
 
2397
- /**
2398
- * Called on graceful shutdown. Clean up connections.
2399
- */
2400
- async shutdown(): Promise<void> {
2401
- // Example: close a connection pool
2402
- // await this.pool.end()
2403
- }
2404
- }
2466
+ /** Called on graceful shutdown. Clean up connections. */
2467
+ async shutdown(): Promise<void> {
2468
+ // Example: await this.pool.end()
2469
+ },
2470
+ }),
2471
+ })
2405
2472
  `);
2406
2473
  files.push(filePath);
2407
2474
  return files;
@@ -2730,7 +2797,7 @@ function generatePackageJson(name, template, kickjsVersion, packages = []) {
2730
2797
  "unplugin-swc": "^1.5.9",
2731
2798
  vite: "^8.0.3",
2732
2799
  vitest: "^4.1.2",
2733
- typescript: "^5.9.2",
2800
+ typescript: "^6.0.3",
2734
2801
  prettier: "^3.8.1"
2735
2802
  }
2736
2803
  }, null, 2);
@@ -2976,404 +3043,223 @@ Copy \`.env.example\` to \`.env\` and configure:
2976
3043
  - [CLI Reference](https://forinda.github.io/kick-js/api/cli.html)
2977
3044
  `;
2978
3045
  }
2979
- /** Generate CLAUDE.md with AI development guide */
2980
- function generateClaude(name, template, pm) {
2981
- return `# CLAUDE.md — ${name} Development Guide
2982
-
2983
- > **Read \`AGENTS.md\` first.** It is the canonical, multi-agent reference for this project (Claude, Copilot, Codex, Gemini, etc.). This file contains the same project context distilled for Claude, plus Claude-specific notes. When the two disagree on anything substantive, treat \`AGENTS.md\` as authoritative and flag the discrepancy.
2984
-
2985
- ## Project Overview
2986
-
2987
- This is a **${{
2988
- rest: "REST API",
2989
- graphql: "GraphQL API",
2990
- ddd: "Domain-Driven Design",
2991
- cqrs: "CQRS + Event-Driven",
2992
- minimal: "Minimal Express"
2993
- }[template] ?? "REST API"}** application built with [KickJS](https://forinda.github.io/kick-js/) — a decorator-driven Node.js framework on Express 5 and TypeScript.
2994
-
2995
- ## Quick Commands
2996
-
2997
- \`\`\`bash
2998
- ${pm} install # Install dependencies
2999
- kick dev # Start dev server with HMR
3000
- kick build # Production build via Vite
3001
- kick start # Run production build
3002
- ${pm} run test # Run tests with Vitest
3003
- ${pm} run typecheck # TypeScript type checking
3004
- ${pm} run format # Format code with Prettier
3005
- \`\`\`
3006
-
3007
- ## Project Structure
3008
-
3009
- \`\`\`
3010
- src/
3011
- ├── index.ts # Application bootstrap
3012
- ├── modules/ # Feature modules (DDD/CQRS pattern)
3013
- │ └── index.ts # Module registry
3014
- ${template === "graphql" ? "├── resolvers/ # GraphQL resolvers\n" : ""}└── ...
3015
- \`\`\`
3016
-
3017
- ## Package Manager
3018
-
3019
- - Always use **${pm}** for this project
3020
- - Run \`${pm} install\` to sync dependencies
3021
- - Never mix package managers (npm/yarn/pnpm)
3022
-
3023
- ## Code Style
3024
-
3025
- - **Prettier** no semicolons, single quotes, trailing commas, 100 char width
3026
- - **TypeScript strict mode** — all types required
3027
- - Format before committing: \`${pm} run format\`
3028
- - Type check with: \`${pm} run typecheck\`
3029
-
3030
- ## Key Patterns
3031
-
3032
- ### Controllers
3033
-
3034
- Use decorators to define routes. Annotate \`ctx\` with \`Ctx<KickRoutes.X['method']>\`
3035
- to get fully-typed \`ctx.params\`, \`ctx.body\`, and \`ctx.query\` from the
3036
- generated \`KickRoutes\` namespace (refreshed on \`kick dev\` and \`kick typegen\`).
3037
-
3038
- \`\`\`ts
3039
- import { Controller, Get, Post, type Ctx } from '@forinda/kickjs'
3040
-
3041
- @Controller('/users')
3042
- export class UserController {
3043
- @Get('/')
3044
- async findAll(ctx: Ctx<KickRoutes.UserController['findAll']>) {
3045
- return ctx.json({ users: [] })
3046
- }
3047
-
3048
- @Post('/')
3049
- async create(ctx: Ctx<KickRoutes.UserController['create']>) {
3050
- const data = ctx.body
3051
- return ctx.created({ user: data })
3052
- }
3053
- }
3054
- \`\`\`
3055
-
3056
- ### Services
3057
-
3058
- Inject dependencies with \`@Service()\` and \`@Autowired()\`:
3059
-
3060
- \`\`\`ts
3061
- import { Service, Autowired } from '@forinda/kickjs'
3062
-
3063
- @Service()
3064
- export class UserService {
3065
- @Autowired()
3066
- private userRepository!: UserRepository
3067
-
3068
- async findAll() {
3069
- return this.userRepository.findAll()
3070
- }
3071
- }
3072
- \`\`\`
3073
-
3074
- ### Modules
3075
-
3076
- Modules implement \`AppModule\` and wire controllers via \`buildRoutes()\`.
3077
-
3078
- > **Naming matters.** Module files **must** be named \`<name>.module.ts\` and live under \`src/modules/\`. The Vite plugin auto-discovers files matching \`*.module.[tj]sx?\` for HMR — a misnamed file (e.g., \`projects.ts\`) won't trigger a graceful module rebuild on save and will require a full server restart. The CLI generator (\`kick g module <name>\`) follows this convention automatically.
3079
-
3080
- \`\`\`ts
3081
- // src/modules/users/users.module.ts (named <feature>.module.ts)
3082
- import { type AppModule, type ModuleRoutes, buildRoutes } from '@forinda/kickjs'
3083
- import { UserController } from './user.controller'
3084
-
3085
- export class UserModule implements AppModule {
3086
- routes(): ModuleRoutes {
3087
- return {
3088
- path: '/users',
3089
- router: buildRoutes(UserController),
3090
- controller: UserController,
3091
- }
3092
- }
3093
- }
3094
- \`\`\`
3095
-
3096
- Register all modules in \`src/modules/index.ts\`:
3097
-
3098
- \`\`\`ts
3099
- import type { AppModuleClass } from '@forinda/kickjs'
3100
- import { UserModule } from './user/user.module'
3101
-
3102
- export const modules: AppModuleClass[] = [UserModule]
3103
- \`\`\`
3104
-
3105
- ### RequestContext
3106
-
3107
- Every controller method receives a \`ctx\` (alias \`Ctx<TRoute>\` or the
3108
- loose \`RequestContext\`):
3109
-
3110
- \`\`\`ts
3111
- ctx.body // Request body (parsed JSON)
3112
- ctx.params // Route params
3113
- ctx.query // Query string
3114
- ctx.headers // Request headers
3115
- ctx.requestId // Auto-generated request ID
3116
- ctx.session // Session data (if session middleware enabled)
3117
- ctx.file // Uploaded file (single)
3118
- ctx.files // Uploaded files (multiple)
3119
-
3120
- // Pagination helpers
3121
- ctx.qs(config) // Parse query with filters/sort/pagination
3122
- ctx.paginate(handler) // Auto-paginated response
3123
-
3124
- // Response helpers
3125
- ctx.json(data) // 200 OK with JSON
3126
- ctx.created(data) // 201 Created
3127
- ctx.noContent() // 204 No Content
3128
- ctx.notFound() // 404 Not Found
3129
- ctx.badRequest(msg) // 400 Bad Request
3130
- \`\`\`
3131
-
3132
- > **Context decorators** — when a middleware's only job is to populate \`ctx.set/get\` for the handler to read, prefer \`defineContextDecorator()\` over \`@Middleware()\`. Typed via \`ContextMeta\`, supports \`dependsOn\` ordering, validates the pipeline at boot. Full pattern reference in \`AGENTS.md\` and at <https://forinda.github.io/kick-js/guide/context-decorators>.
3133
-
3134
- ## CLI Generators
3135
-
3136
- Generate code with the \`kick\` CLI:
3137
-
3138
- \`\`\`bash
3139
- kick g module <name> # Full module (controller, service, DTOs, repo)
3140
- kick g scaffold <name> <fields> # CRUD module from field definitions
3141
- kick g controller <name> # Standalone controller
3142
- kick g service <name> # Service class
3143
- kick g middleware <name> # Express middleware
3144
- kick g guard <name> # Route guard (auth, roles)
3145
- kick g adapter <name> # AppAdapter with lifecycle hooks
3146
- kick g dto <name> # Zod DTO schema
3147
- ${template === "graphql" ? "kick g resolver <name> # GraphQL resolver\n" : ""}${template === "cqrs" ? "kick g job <name> # Queue job processor\n" : ""}\`\`\`
3148
-
3149
- ## Adding Packages
3046
+ /**
3047
+ * Generate CLAUDE.md.
3048
+ *
3049
+ * v4 update: this file is intentionally thin. AGENTS.md is the
3050
+ * canonical, multi-agent project reference (Claude / Copilot /
3051
+ * Codex / Gemini / etc.) — duplicating it here meant two files
3052
+ * drifting out of sync after every framework change. The generated
3053
+ * CLAUDE.md now redirects there + adds Claude-specific affordances
3054
+ * only.
3055
+ */
3056
+ function generateClaude(name, _template, pm) {
3057
+ return `# CLAUDE.md — ${name}
3058
+
3059
+ **Read \`./AGENTS.md\` first.** It is the canonical, multi-agent
3060
+ reference for this project (Claude, Copilot, Codex, Gemini, etc.) —
3061
+ project conventions, structure, decorator patterns, env wiring, CLI
3062
+ generators, every gotcha.
3063
+
3064
+ **Then read \`./kickjs-skills.md\`.** That file is the task-oriented
3065
+ skill index short, rigid recipes keyed to triggers ("add-module",
3066
+ "write-controller-test", "bootstrap-export", "deny-list", …). Use it
3067
+ as the playbook when executing common KickJS workflows.
3068
+
3069
+ This file is a thin Claude-specific layer on top of those two; when
3070
+ they disagree on anything substantive, treat \`AGENTS.md\` as
3071
+ authoritative and flag the discrepancy.
3072
+
3073
+ ## Why two files
3074
+
3075
+ \`AGENTS.md\` is what every agent reads. \`CLAUDE.md\` is what
3076
+ Claude Code automatically loads as project context on each
3077
+ conversation. Keeping CLAUDE.md slim avoids two files drifting; the
3078
+ redirect above ensures Claude pulls the canonical content without
3079
+ us copy-pasting.
3080
+
3081
+ ## Claude-specific notes
3082
+
3083
+ - **Slash commands** — \`/help\` for Claude Code commands; \`/init\`
3084
+ to refresh project memory if AGENTS.md changes substantially.
3085
+ - **Feedback** — file issues at <https://github.com/anthropics/claude-code/issues>.
3086
+ - **Persistent memory** Claude maintains user/feedback/project/
3087
+ reference memories under \`.claude/memory/\`. If you ask for
3088
+ something that contradicts a remembered preference, Claude flags
3089
+ it before acting; corrections update memory automatically.
3090
+ - **Long-running tasks** — \`/loop\` and \`/schedule\` for recurring
3091
+ or background work. Useful for "wait for the deploy then open a
3092
+ cleanup PR" or "every Monday triage the issue board" patterns.
3093
+
3094
+ ## Quick reference (full version in AGENTS.md)
3150
3095
 
3151
3096
  \`\`\`bash
3152
- kick add auth # JWT, API key, OAuth strategies
3153
- kick add swagger # OpenAPI docs from decorators
3154
- kick add ws # WebSocket support
3155
- kick add queue # Background jobs (BullMQ/RabbitMQ/Kafka)
3156
- kick add mailer # Email (SMTP, Resend, SES)
3157
- kick add cron # Scheduled tasks
3158
- kick add prisma # Prisma ORM adapter
3159
- kick add drizzle # Drizzle ORM adapter
3160
- kick add otel # OpenTelemetry tracing
3161
- kick add --list # Show all available packages
3162
- \`\`\`
3163
-
3164
- ## Environment Configuration
3165
-
3166
- The project's typed env schema lives in **\`src/config/index.ts\`** —
3167
- extend the base schema there with your application-specific keys, and
3168
- the schema is auto-registered with kickjs at module load. The companion
3169
- \`src/index.ts\` imports it as a side effect (\`import './config'\`) **before**
3170
- \`bootstrap()\` runs, so every \`@Service\`, \`@Controller\`, \`@Value\`, and
3171
- \`ConfigService\` resolution sees the validated extended values.
3172
-
3173
- > **Do not delete \`import './config'\` from \`src/index.ts\`.** It is the
3174
- > registration step that wires \`ConfigService\` to your env schema.
3175
- > Without it, \`config.get('YOUR_KEY')\` returns \`undefined\` for every
3176
- > user-defined key and \`@Value('YOUR_KEY')\` only works because of a
3177
- > raw \`process.env\` fallback (Zod coercion + defaults are skipped).
3178
-
3179
- Edit \`.env\` for variable values. Access them with \`@Value()\`:
3180
-
3181
- \`\`\`ts
3182
- import { Value } from '@forinda/kickjs'
3183
-
3184
- @Service()
3185
- export class ApiService {
3186
- @Value('API_KEY')
3187
- private apiKey!: string
3188
-
3189
- @Value('PORT', 3000) // With default
3190
- private port!: number
3191
- }
3192
- \`\`\`
3193
-
3194
- Or use \`ConfigService\`:
3195
-
3196
- \`\`\`ts
3197
- import { Service, Autowired, ConfigService } from '@forinda/kickjs'
3198
-
3199
- @Service()
3200
- export class AppService {
3201
- @Autowired()
3202
- private config!: ConfigService
3203
-
3204
- getPort() {
3205
- // typed: number, Zod-coerced from baseEnvSchema
3206
- return this.config.get('PORT')
3207
- }
3208
- }
3209
- \`\`\`
3210
-
3211
- Hot-reload of \`.env\` changes during dev is wired up automatically via
3212
- \`envWatchPlugin()\` in \`vite.config.ts\` — edit \`.env\`, the dev server
3213
- reloads, and the next \`config.get()\` re-parses with the new values.
3214
-
3215
- ### Standalone Env Utilities (No DI Required)
3216
-
3217
- These functions work anywhere — scripts, CLI tools, plain files, outside \`@Service\`/\`@Controller\`:
3218
-
3219
- \`\`\`ts
3220
- import { defineEnv, loadEnv, getEnv, reloadEnv, resetEnvCache, baseEnvSchema } from '@forinda/kickjs/config'
3221
- import { z } from 'zod'
3222
-
3223
- // Define and parse schema
3224
- const schema = defineEnv((base) =>
3225
- base.extend({ DATABASE_URL: z.string().url() })
3226
- )
3227
- const env = loadEnv(schema) // Parse + validate process.env
3228
- console.log(env.PORT) // 3000 (coerced to number)
3229
- console.log(env.DATABASE_URL) // validated URL string
3230
-
3231
- // Get single value
3232
- const port = getEnv('PORT') // typed after kick typegen
3233
-
3234
- // Reload after .env changes (HMR calls this automatically)
3235
- reloadEnv()
3236
-
3237
- // Reset cache in tests that swap schemas
3238
- resetEnvCache()
3239
- \`\`\`
3240
-
3241
- | Function | Purpose |
3242
- |----------|---------|
3243
- | \`defineEnv(fn)\` | Extend base schema with custom Zod keys |
3244
- | \`loadEnv(schema?)\` | Parse \`process.env\`, validate, cache, return typed object |
3245
- | \`getEnv(key, schema?)\` | Get single validated env value |
3246
- | \`reloadEnv()\` | Re-read \`.env\` from disk, re-parse with same schema |
3247
- | \`resetEnvCache()\` | Clear parsed cache AND registered schema (for tests) |
3248
- | \`baseEnvSchema\` | Base Zod schema: \`PORT\`, \`NODE_ENV\`, \`LOG_LEVEL\` |
3249
-
3250
- ## Standalone Utilities (No DI Required)
3251
-
3252
- These utilities work outside decorated classes:
3253
-
3254
- ### Logger
3255
-
3256
- \`\`\`ts
3257
- import { Logger, createLogger } from '@forinda/kickjs'
3258
-
3259
- const log = Logger.for('MyScript') // Static factory
3260
- log.info('Processing started')
3261
- log.error('Something failed')
3262
-
3263
- const log2 = createLogger('Worker') // Function form
3264
- \`\`\`
3265
-
3266
- ### Injection Tokens
3267
-
3268
- \`\`\`ts
3269
- import { createToken } from '@forinda/kickjs'
3270
-
3271
- // Type-safe DI tokens for factory/interface binding
3272
- const DB_URL = createToken<string>('config.database.url')
3273
- const FEATURE_FLAGS = createToken<FeatureFlags>('app.features')
3274
- \`\`\`
3275
-
3276
- ### Reactivity
3277
-
3278
- \`\`\`ts
3279
- import { ref, computed, watch, reactive } from '@forinda/kickjs'
3280
-
3281
- const count = ref(0)
3282
- const doubled = computed(() => count.value * 2)
3283
- const stop = watch(() => count.value, (val) => console.log(val))
3284
- count.value++ // logs 1
3285
- \`\`\`
3286
-
3287
- ### HTTP Errors
3288
-
3289
- \`\`\`ts
3290
- import { HttpException, HttpStatus } from '@forinda/kickjs'
3291
-
3292
- throw new HttpException(HttpStatus.NOT_FOUND, 'User not found')
3293
- \`\`\`
3294
-
3295
- ## Testing
3296
-
3297
- Tests live in \`src/**/*.test.ts\`:
3298
-
3299
- \`\`\`ts
3300
- import { describe, it, expect, beforeEach } from 'vitest'
3301
- import { Container } from '@forinda/kickjs'
3302
- import { createTestApp } from '@forinda/kickjs-testing'
3303
-
3304
- describe('UserController', () => {
3305
- beforeEach(() => Container.reset())
3306
-
3307
- it('should return users', async () => {
3308
- const app = await createTestApp([UserModule])
3309
- const res = await app.get('/users')
3310
- expect(res.status).toBe(200)
3311
- })
3312
- })
3097
+ ${pm} install # Install dependencies
3098
+ kick dev # Dev server with HMR + typegen
3099
+ kick build && kick start # Production
3100
+ ${pm} run test # Vitest
3101
+ ${pm} run typecheck # tsc --noEmit
3102
+ ${pm} run format # Prettier
3313
3103
  \`\`\`
3314
3104
 
3315
- Run tests:
3316
- - \`${pm} run test\` — run all tests
3317
- - \`${pm} run test:watch\` — watch mode
3318
-
3319
- ## Decorators Reference
3320
-
3321
- ### Route Decorators
3322
- - \`@Controller('/path')\` — define controller prefix
3323
- - \`@Get('/'), @Post('/'), @Put('/'), @Delete('/'), @Patch('/')\` — HTTP methods
3324
- - \`@Middleware(fn)\` — attach middleware
3325
- - \`@Public()\` — skip authentication (requires @forinda/kickjs-auth)
3326
- - \`@Roles('admin', 'user')\` — role-based access control
3327
-
3328
- ### DI Decorators
3329
- - \`@Service()\` — singleton service (DI-registered)
3330
- - \`@Repository()\` — repository (semantic alias for @Service)
3331
- - \`@Autowired()\` — property injection
3332
- - \`@Inject('token')\` — token-based injection
3333
- - \`@Value('ENV_VAR')\` — inject config value
3334
-
3335
- ${template === "cqrs" ? `### CQRS/Event Decorators
3336
- - \`@Job('job-name')\` — queue job handler
3337
- - \`@Process('queue-name')\` — queue processor
3338
- - \`@Cron('0 * * * *')\` — cron schedule
3339
- - \`@WsController('/path')\` — WebSocket controller
3340
- - \`@Subscribe('event')\` — WebSocket event handler
3341
-
3342
- ` : ""}${template === "graphql" ? `### GraphQL Decorators
3343
- - \`@Resolver()\` — GraphQL resolver
3344
- - \`@Query()\` — GraphQL query
3345
- - \`@Mutation()\` — GraphQL mutation
3346
- - \`@Arg('name')\` — resolver argument
3105
+ ## v4 framework reminders
3347
3106
 
3348
- ` : ""}## Common Pitfalls
3107
+ When generating or modifying code in this project, stay aligned with the v4 conventions documented in \`AGENTS.md\`:
3349
3108
 
3350
- 1. **Decorators fire at import time** make sure to import module classes in \`src/modules/index.ts\`
3351
- 2. **Tests need \`Container.reset()\`**call in \`beforeEach\` to isolate DI state
3352
- 3. **Always use \`ctx.body\`** never \`req.body\` directly
3353
- 4. **DI requires \`reflect-metadata\`**already imported in \`src/index.ts\`
3354
- 5. **Vite HMR requires proper cleanup**adapters should implement \`shutdown()\`
3355
- 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).
3109
+ - **Adapters**: \`defineAdapter()\` factorynever \`class implements AppAdapter\`.
3110
+ - **Plugins**: \`definePlugin()\` factory never plain function returning \`KickPlugin\`.
3111
+ - **DI tokens**: slash-delimited \`<scope>/<area>/<key>\` (e.g. \`'app/users/repository'\`). First-party uses the reserved \`'kick/'\` prefix; this project owns its own scope.
3112
+ - **Decorators**: \`@Controller()\` (no path arg mount prefix comes from \`routes().path\`).
3113
+ - **Module entry file** MUST be named \`<name>.module.ts\` and live under \`src/modules/<name>/\`. The Vite plugin auto-discovers \`*.module.[tj]sx?\` for graceful HMR a misnamed \`projects.ts\` silently degrades every save into a full restart.
3114
+ - **Env**: schema lives in \`src/config/index.ts\`; \`import './config'\` MUST be the first import in \`src/index.ts\` (side-effect registers the schema before any \`@Value\` resolves).
3115
+ - **Assets**: drop new template files into \`src/templates/<namespace>/\`; the dev watcher auto-rebuilds the \`KickAssets\` augmentation + \`assets.x.y()\` re-walks on next call. No restart, no manual build.
3116
+ - **Context Contributors** (\`defineContextDecorator\`) over \`@Middleware()\` for ctx-population work.
3117
+ - **Repos under tests**: \`Container.create()\` for isolation — never \`new Container()\` or \`getInstance().reset()\`.
3118
+ - **Bootstrap export**: \`src/index.ts\` must end with \`export const app = await bootstrap({ ... })\`. The Vite plugin and \`createTestApp\` import the named \`app\`; without the export, HMR silently degrades to full restarts.
3119
+ - **Thin entry file**: aggregate \`modules\`, \`middleware\`, \`plugins\`, \`adapters\` in their own folders (\`src/modules/index.ts\`, \`src/middleware/index.ts\`, …) and pass them by name to \`bootstrap()\` — never inline the lists in \`src/index.ts\`.
3120
+ - **Refresh these files**: \`kick g agents -f\` regenerates \`AGENTS.md\` + \`CLAUDE.md\` from the latest CLI templates. Hand-edited content is overwritten — keep customisation in \`AGENTS.local.md\`.
3356
3121
 
3357
- ## Learn More
3358
-
3359
- - [KickJS Documentation](https://forinda.github.io/kick-js/)
3360
- - [API Reference](https://forinda.github.io/kick-js/api/)
3361
- - [CLI Commands](https://forinda.github.io/kick-js/guide/cli-commands.html)
3362
- - [Decorators Guide](https://forinda.github.io/kick-js/guide/decorators.html)
3122
+ For everything else (controllers, services, modules, RequestContext API, generators, CLI commands, package additions, env wiring, troubleshooting) → \`AGENTS.md\`.
3363
3123
  `;
3364
3124
  }
3365
3125
  /** Generate AGENTS.md with AI agent guide */
3366
3126
  function generateAgents(name, template, pm) {
3367
3127
  return `# AGENTS.md — AI Agent Guide for ${name}
3368
3128
 
3369
- This guide helps AI agents (Claude, Copilot, etc.) work effectively on this KickJS application.
3129
+ This guide is the **canonical, multi-agent reference** for this KickJS
3130
+ application — Claude, Copilot, Codex, Gemini, etc. all read it first.
3131
+ Per-agent files (\`CLAUDE.md\`, \`GEMINI.md\`, etc.) are thin layers that
3132
+ add tool-specific affordances on top.
3370
3133
 
3371
3134
  ## Before You Start
3372
3135
 
3373
- 1. Read \`CLAUDE.md\` for project conventions and commands
3374
- 2. Run \`${pm} install\` to install dependencies
3375
- 3. Run \`kick dev\` to verify the app starts
3376
- 4. Read the [KickJS documentation](https://forinda.github.io/kick-js/) for framework details
3136
+ 1. Run \`${pm} install\` to install dependencies
3137
+ 2. Run \`kick dev\` to verify the app starts
3138
+ 3. Read the [KickJS documentation](https://forinda.github.io/kick-js/) for framework details
3139
+
3140
+ ## v4 Conventions (don't skip)
3141
+
3142
+ KickJS v4 made a handful of structural changes from v3. Internalise these
3143
+ before generating or modifying code — they are the source of most agent
3144
+ mistakes:
3145
+
3146
+ - **Adapters** — \`defineAdapter()\` factory. Never write \`class Foo implements AppAdapter\`.
3147
+
3148
+ \`\`\`ts
3149
+ export const MyAdapter = defineAdapter<MyOptions>({
3150
+ name: 'MyAdapter',
3151
+ defaults: { ... },
3152
+ build: (config) => ({
3153
+ beforeMount({ app }) { /* ... */ },
3154
+ afterStart({ server }) { /* ... */ },
3155
+ }),
3156
+ })
3157
+ \`\`\`
3158
+
3159
+ - **Plugins** — \`definePlugin()\` factory. Same shape, never plain function returning \`KickPlugin\`.
3160
+
3161
+ - **DI tokens** — slash-delimited \`<scope>/<area>/<key>\`, lower-case, no \`:\` separators:
3162
+
3163
+ \`\`\`ts
3164
+ const USERS_REPO = createToken<UsersRepo>('app/users/repository')
3165
+ const DB = createToken<Database>('app/db/connection')
3166
+ \`\`\`
3167
+
3168
+ The \`kick/\` prefix is reserved for first-party packages; this project
3169
+ owns its own scope (\`app/\`, your domain name, etc.).
3170
+
3171
+ - **\`@Controller()\`** takes **no path argument**. Mount prefix comes from
3172
+ the module's \`routes()\` return value, not the decorator. \`@Controller('/users')\`
3173
+ is a v3 leftover; the linter and codegen reject it.
3174
+
3175
+ - **Env wiring** — \`src/config/index.ts\` calls \`loadEnv(envSchema)\` as a
3176
+ side effect. \`src/index.ts\` MUST have \`import './config'\` as its **first**
3177
+ import (before \`bootstrap()\`). Without it, \`ConfigService.get('YOUR_KEY')\`
3178
+ returns \`undefined\` and \`@Value()\` only works via raw \`process.env\` fallback
3179
+ (Zod coercion + defaults silently skipped).
3180
+
3181
+ - **Module entry files MUST be named \`<name>.module.ts\`** — see the Vite
3182
+ HMR contract at the top of "Module Pattern" below. The CLI enforces this;
3183
+ hand-rolled files must too.
3184
+
3185
+ - **Assets** — drop new template files into \`src/templates/<namespace>/\`
3186
+ (or wherever \`kick.config.ts\` points). The dev watcher auto-rebuilds the
3187
+ \`KickAssets\` augmentation; \`assets.x.y()\` re-walks on next call. No restart,
3188
+ no manual build step.
3189
+
3190
+ - **Context over \`@Middleware()\`** — when a middleware's only job is to
3191
+ populate \`ctx.set('key', value)\`, use \`defineHttpContextDecorator()\`
3192
+ (HTTP) or \`defineContextDecorator()\` (transport-agnostic) instead.
3193
+ Typed via \`ContextMeta\`, ordered via \`dependsOn\`, validated at boot.
3194
+ Reserve \`@Middleware()\` for response short-circuit / stream mutation /
3195
+ pre-route-matching work.
3196
+
3197
+ Two ground rules around the data flow — both stem from the fact that
3198
+ every per-request stage gets its OWN \`RequestContext\` instance, all
3199
+ reading/writing the SAME \`AsyncLocalStorage\`-backed Map:
3200
+ - **\`resolve\` and \`onError\` must RETURN the value.** The runner
3201
+ writes it via \`ctx.set(reg.key, value)\` on your behalf. Direct
3202
+ property assignment (\`ctx.tenant = …\`) sticks to the contributor
3203
+ instance only — the handler instance never sees it.
3204
+ - **Read across instances via \`ctx.set\` / \`ctx.get\`** (or
3205
+ \`requestStore.getStore()?.values.get('key')\` from a service that
3206
+ has no \`ctx\` reference). \`ctx.req\` works because the underlying
3207
+ Express request is shared; bespoke property assignments don't.
3208
+
3209
+ - **Test isolation** — default to \`Container.create()\` for fresh DI state.
3210
+ Never \`new Container()\` and never \`getInstance().reset()\` — both leak
3211
+ registrations between tests.
3212
+
3213
+ \`\`\`ts
3214
+ const container = Container.create()
3215
+ // ... register test-scoped providers, run, discard
3216
+ \`\`\`
3217
+
3218
+ - **Bootstrap export** — \`src/index.ts\` MUST end with
3219
+ \`export const app = await bootstrap({ ... })\`. The Vite plugin imports
3220
+ the named \`app\` symbol to drive HMR module swaps; testing helpers
3221
+ (\`createTestApp\`) and the OpenAPI introspector also rely on it. Drop
3222
+ the \`export\` and \`kick dev\` will silently fall back to a full restart
3223
+ on every save while \`createTestApp\` complains about a missing handle.
3224
+
3225
+ - **Keep \`src/index.ts\` thin** — collect plugins, modules, middleware, and
3226
+ adapters in dedicated folders and re-export aggregated arrays. Do **not**
3227
+ inline registration in the entry file:
3228
+
3229
+ \`\`\`ts
3230
+ // src/modules/index.ts
3231
+ export const modules: AppModuleClass[] = [HelloModule, UsersModule, ...]
3232
+
3233
+ // src/middleware/index.ts
3234
+ export const middleware = [helmet(), cors(), requestId(), ...]
3235
+
3236
+ // src/plugins/index.ts
3237
+ export const plugins = [MetricsPlugin(), AuditPlugin()]
3238
+
3239
+ // src/adapters/index.ts
3240
+ export const adapters = [SwaggerAdapter({ ... }), DevToolsAdapter()]
3241
+ \`\`\`
3242
+
3243
+ \`\`\`ts
3244
+ // src/index.ts — stays small; one import per category
3245
+ import 'reflect-metadata'
3246
+ import './config'
3247
+ import { bootstrap } from '@forinda/kickjs'
3248
+ import { modules } from './modules'
3249
+ import { middleware } from './middleware'
3250
+ import { plugins } from './plugins'
3251
+ import { adapters } from './adapters'
3252
+
3253
+ export const app = await bootstrap({ modules, middleware, plugins, adapters })
3254
+ \`\`\`
3255
+
3256
+ This keeps the entry file diff-friendly, scales to dozens of modules
3257
+ without git churn, and lets each domain own its own registration list.
3258
+ The generators (\`kick g module\`, \`kick g middleware\`, \`kick g plugin\`,
3259
+ \`kick g adapter\`) follow this layout — manual additions should too.
3260
+
3261
+ Everything else (controllers, services, modules, RequestContext API, generators,
3262
+ package additions, env access patterns, troubleshooting) is detailed below.
3377
3263
 
3378
3264
  ## Where to Find Things
3379
3265
 
@@ -3384,6 +3270,7 @@ This guide helps AI agents (Claude, Copilot, etc.) work effectively on this Kick
3384
3270
  | Entry point | \`src/index.ts\` |
3385
3271
  | Module registry | \`src/modules/index.ts\` |
3386
3272
  | Feature modules | \`src/modules/<module-name>/\` |
3273
+ | **Module entry file** | \`src/modules/<name>/<name>.module.ts\` (filename suffix is required — see Vite HMR contract below) |
3387
3274
  ${template === "graphql" ? "| GraphQL resolvers | `src/resolvers/` |\n" : ""}| Env values | \`.env\` |
3388
3275
  | Env schema (Zod) | \`src/config/index.ts\` |
3389
3276
  | TypeScript config | \`tsconfig.json\` |
@@ -3466,7 +3353,7 @@ Then:
3466
3353
  If not using generators:
3467
3354
 
3468
3355
  - [ ] Create \`src/modules/<name>/<name>.controller.ts\`
3469
- - [ ] Add \`@Controller('/path')\` decorator
3356
+ - [ ] Add \`@Controller()\` decorator
3470
3357
  - [ ] Add route handlers with \`@Get()\`, \`@Post()\`, etc.
3471
3358
  - [ ] Create module file implementing \`AppModule\` with \`routes()\` returning \`{ path, router: buildRoutes(Controller), controller }\`
3472
3359
  - [ ] Register module in \`src/modules/index.ts\` (\`AppModuleClass[]\` array)
@@ -3528,8 +3415,8 @@ import { AuthAdapter, JwtStrategy } from '@forinda/kickjs-auth'
3528
3415
  bootstrap({
3529
3416
  modules,
3530
3417
  adapters: [
3531
- new AuthAdapter({
3532
- strategies: [new JwtStrategy({ secret: process.env.JWT_SECRET! })],
3418
+ AuthAdapter({
3419
+ strategies: [JwtStrategy({ secret: process.env.JWT_SECRET! })],
3533
3420
  }),
3534
3421
  ],
3535
3422
  })
@@ -3559,7 +3446,7 @@ import { WsAdapter } from '@forinda/kickjs-ws'
3559
3446
 
3560
3447
  bootstrap({
3561
3448
  modules,
3562
- adapters: [new WsAdapter()],
3449
+ adapters: [WsAdapter()],
3563
3450
  })
3564
3451
  \`\`\`
3565
3452
 
@@ -3579,14 +3466,13 @@ import { Container } from '@forinda/kickjs'
3579
3466
  import { createTestApp } from '@forinda/kickjs-testing'
3580
3467
 
3581
3468
  describe('UserController', () => {
3582
- beforeEach(() => {
3583
- Container.reset() // Important: isolate DI state
3584
- })
3585
-
3586
3469
  it('should return users', async () => {
3587
- const app = await createTestApp([UserModule])
3470
+ // Container.create() isolated DI state per test, never new Container()
3471
+ // and never getInstance().reset() (both leak registrations between tests).
3472
+ const container = Container.create()
3473
+ const app = await createTestApp([UserModule], { container })
3588
3474
  const res = await app.get('/users')
3589
-
3475
+
3590
3476
  expect(res.status).toBe(200)
3591
3477
  expect(res.body).toHaveProperty('users')
3592
3478
  })
@@ -3650,7 +3536,7 @@ These work anywhere — scripts, plain files, outside \`@Service\`/\`@Controller
3650
3536
  |---------|--------|---------|
3651
3537
  | \`Logger.for(name)\` | \`@forinda/kickjs\` | \`const log = Logger.for('MyScript')\` |
3652
3538
  | \`createLogger(name)\` | \`@forinda/kickjs\` | \`const log = createLogger('Worker')\` |
3653
- | \`createToken<T>(name)\` | \`@forinda/kickjs\` | \`const TOKEN = createToken<string>('db.url')\` |
3539
+ | \`createToken<T>(name)\` | \`@forinda/kickjs\` | \`const TOKEN = createToken<string>('app/db/url')\` |
3654
3540
  | \`ref(value)\` | \`@forinda/kickjs\` | \`const count = ref(0)\` |
3655
3541
  | \`computed(fn)\` | \`@forinda/kickjs\` | \`const doubled = computed(() => count.value * 2)\` |
3656
3542
  | \`watch(source, cb)\` | \`@forinda/kickjs\` | \`watch(() => count.value, (v) => log(v))\` |
@@ -3663,7 +3549,7 @@ These work anywhere — scripts, plain files, outside \`@Service\`/\`@Controller
3663
3549
  ### HTTP Routes
3664
3550
  | Decorator | Purpose |
3665
3551
  |-----------|---------|
3666
- | \`@Controller('/path')\` | Define route prefix |
3552
+ | \`@Controller()\` | Define route prefix |
3667
3553
  | \`@Get('/'), @Post('/')\` | HTTP method handlers |
3668
3554
  | \`@Middleware(fn)\` | Attach middleware |
3669
3555
  | \`@Public()\` | Skip auth (requires auth adapter) |
@@ -3720,13 +3606,15 @@ ${template === "graphql" ? `### GraphQL
3720
3606
 
3721
3607
  1. **Forgot to register module** — Add to \`src/modules/index.ts\` exports array
3722
3608
  2. **DI not working** — Ensure \`reflect-metadata\` is imported in \`src/index.ts\`
3723
- 3. **Tests failing randomly** — Missing \`Container.reset()\` in \`beforeEach\`
3609
+ 3. **Tests failing randomly** — Sharing the global container between tests. Default to \`Container.create()\` per test (or per \`beforeEach\`) instead of \`new Container()\` / \`getInstance().reset()\`
3724
3610
  4. **Routes not found** — Check controller path and module registration
3725
3611
  5. **HMR not working** — Two checks: (a) \`vite.config.ts\` has \`hmr: true\`; (b) module file is named \`<name>.module.ts\` (or \`.tsx\`/\`.js\`/\`.jsx\`) and lives under \`src/modules/\`. The Vite plugin auto-discovers \`*.module.[tj]sx?\` for graceful HMR — a misnamed module file (e.g., \`projects.ts\`) silently degrades to a full restart on every save.
3726
3612
  6. **Decorators not working** — Check \`tsconfig.json\` has \`experimentalDecorators: true\`
3727
3613
  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.
3728
3614
  8. **Used \`@Middleware()\` to compute a value for \`ctx\`** — prefer \`defineContextDecorator()\` (see Context Decorators above). It's typed via \`ContextMeta\`, supports \`dependsOn\` for ordering, and validates the pipeline at boot. \`@Middleware()\` is for response short-circuiting, stream mutation, and pre-route-matching work.
3729
3615
  9. **Context contributor's \`dependsOn\` key not produced anywhere** — boot throws \`MissingContributorError\` naming the dependent and the route. Either remove the dep or register a contributor that produces the key (at any precedence level: method/class/module/adapter/global).
3616
+ 10. **\`bootstrap()\` not exported** — \`src/index.ts\` calls \`await bootstrap({ ... })\` but discards the return value (no \`export const app = ...\`). Vite HMR can't locate the running instance, so module saves degrade to full restarts; \`createTestApp\`/\`@forinda/kickjs-testing\` consumers can't import the handle either. Always: \`export const app = await bootstrap({ ... })\`.
3617
+ 11. **Refresh AGENTS.md / CLAUDE.md after a framework upgrade** — these files are scaffolded by the CLI and don't auto-update. Run \`kick g agents -f\` (or \`kick g agent-docs -f\`) to regenerate from the latest CLI templates after \`kick add\` / version bumps. Hand-edited sections will be overwritten — keep customisation in a separate file like \`AGENTS.local.md\`.
3730
3618
 
3731
3619
  ## CLI Commands Reference
3732
3620
 
@@ -3759,6 +3647,261 @@ ${template === "graphql" ? `### GraphQL
3759
3647
  - [Testing](https://forinda.github.io/kick-js/api/testing.html)
3760
3648
  `;
3761
3649
  }
3650
+ /**
3651
+ * Generate `kickjs-skills.md` — task-oriented "skill" recipes for AI
3652
+ * agents (Claude superpowers, Copilot, etc.). Where AGENTS.md is the
3653
+ * narrative reference, this file lists short, rigid workflows the agent
3654
+ * should follow when it sees the corresponding trigger.
3655
+ */
3656
+ function generateKickJsSkills(name, _template, pm) {
3657
+ return `# kickjs-skills.md — Task Skills for AI Agents (${name})
3658
+
3659
+ This file is the agent-facing **skills index** for KickJS work in this
3660
+ repo. Each block below is a short, rigid workflow keyed to a specific
3661
+ trigger ("user wants to add a module", "tests are leaking state", etc.).
3662
+
3663
+ - Reference docs (narrative, exhaustive) → \`AGENTS.md\`.
3664
+ - Tool-specific notes → \`CLAUDE.md\`, \`GEMINI.md\`, etc.
3665
+ - **This file** → step-by-step recipes the agent should *execute*.
3666
+
3667
+ Re-run \`kick g agents -f --only skills\` after framework upgrades to refresh.
3668
+
3669
+ ---
3670
+
3671
+ ## Skill: add-module
3672
+
3673
+ \`\`\`yaml
3674
+ name: kickjs-add-module
3675
+ description: Use when the user asks to add a new feature module (controller + service + repo + DTOs).
3676
+ \`\`\`
3677
+
3678
+ **Trigger phrases**: "add a users module", "scaffold tasks", "new feature for X".
3679
+
3680
+ **Steps**:
3681
+ 1. Run \`kick g module <name>\` (use plural form if the project pluralizes — check \`kick.config.ts\`).
3682
+ 2. Verify the new folder under \`src/modules/<name>/\` contains \`<name>.module.ts\` (filename suffix is mandatory for HMR).
3683
+ 3. Confirm the module appears in \`src/modules/index.ts\` exports — generator does this automatically; verify if you bypassed it.
3684
+ 4. Open \`<name>.dto.ts\` and tighten the Zod schemas to real fields (the generator emits placeholders).
3685
+ 5. Run \`${pm} run typecheck\` and \`${pm} run test\` before claiming done.
3686
+
3687
+ **Red flags** (stop and ask):
3688
+ - File created as \`<name>.ts\` instead of \`<name>.module.ts\` — Vite won't HMR it.
3689
+ - Module not registered in \`src/modules/index.ts\`.
3690
+ - \`@Controller('/path')\` with a path argument — that's a v3 pattern; remove it (mount comes from \`routes().path\`).
3691
+
3692
+ ---
3693
+
3694
+ ## Skill: add-adapter
3695
+
3696
+ \`\`\`yaml
3697
+ name: kickjs-add-adapter
3698
+ description: Use when wiring a new lifecycle integration (Swagger, DevTools, Auth, custom).
3699
+ \`\`\`
3700
+
3701
+ **Steps**:
3702
+ 1. \`kick g adapter <name>\` to scaffold the boilerplate, OR install via \`kick add <package>\` for first-party adapters.
3703
+ 2. The generated file uses \`defineAdapter()\` — never \`class implements AppAdapter\`.
3704
+ 3. Add the adapter instance to \`src/adapters/index.ts\` (don't inline in \`src/index.ts\`).
3705
+ 4. If the adapter contributes to \`ctx.set/get\`, prefer \`AppAdapter.contributors?()\` over a wrapping middleware.
3706
+ 5. Verify with \`kick dev\` that the adapter's lifecycle logs fire.
3707
+
3708
+ **Red flags**:
3709
+ - Inlining the adapter list directly in \`src/index.ts\` (entry file should stay thin).
3710
+ - Returning a plain object instead of going through \`defineAdapter()\` — type inference for \`config\` will be wrong.
3711
+
3712
+ ---
3713
+
3714
+ ## Skill: write-controller-test
3715
+
3716
+ \`\`\`yaml
3717
+ name: kickjs-write-controller-test
3718
+ description: Use when adding a Vitest test that exercises an HTTP route or DI graph.
3719
+ \`\`\`
3720
+
3721
+ **Template** (copy/paste, adjust):
3722
+
3723
+ \`\`\`ts
3724
+ import { describe, it, expect } from 'vitest'
3725
+ import { Container } from '@forinda/kickjs'
3726
+ import { createTestApp } from '@forinda/kickjs-testing'
3727
+
3728
+ describe('UserController', () => {
3729
+ it('returns users', async () => {
3730
+ const container = Container.create() // isolated DI per test
3731
+ const app = await createTestApp([UserModule], { container })
3732
+ const res = await app.get('/users')
3733
+ expect(res.status).toBe(200)
3734
+ })
3735
+ })
3736
+ \`\`\`
3737
+
3738
+ **Red flags**:
3739
+ - \`new Container()\` — wrong; use \`Container.create()\`.
3740
+ - \`Container.getInstance().reset()\` — wrong; same fix.
3741
+ - Sharing a container across \`it()\` blocks — leaks registrations.
3742
+
3743
+ ---
3744
+
3745
+ ## Skill: env-wiring-check
3746
+
3747
+ \`\`\`yaml
3748
+ name: kickjs-env-wiring-check
3749
+ description: Use when ConfigService.get('SOME_KEY') returns undefined or @Value silently falls back to process.env.
3750
+ \`\`\`
3751
+
3752
+ **Diagnosis**:
3753
+ 1. Open \`src/index.ts\`. The **first non-\`reflect-metadata\`** import MUST be \`import './config'\`.
3754
+ 2. Open \`src/config/index.ts\`. It MUST call \`loadEnv(envSchema)\` as a top-level side effect.
3755
+ 3. The new key MUST be declared in the Zod schema there. \`@Value('NEW_KEY')\` won't work without a schema entry (it'll fall back to raw \`process.env\` and skip Zod coercion silently).
3756
+
3757
+ **Fix**: add the key to the schema; ensure both side-effect imports above are present.
3758
+
3759
+ ---
3760
+
3761
+ ## Skill: bootstrap-export
3762
+
3763
+ \`\`\`yaml
3764
+ name: kickjs-bootstrap-export
3765
+ description: Use when HMR is silently doing full restarts on every save, or createTestApp can't find the app handle.
3766
+ \`\`\`
3767
+
3768
+ **Check** \`src/index.ts\`'s last line:
3769
+
3770
+ \`\`\`ts
3771
+ // CORRECT
3772
+ export const app = await bootstrap({ ... })
3773
+
3774
+ // WRONG (HMR degrades to full restart, createTestApp loses the handle)
3775
+ await bootstrap({ ... })
3776
+ \`\`\`
3777
+
3778
+ The Vite plugin imports the named \`app\` symbol; testing helpers do too.
3779
+
3780
+ ---
3781
+
3782
+ ## Skill: thin-entry-file
3783
+
3784
+ \`\`\`yaml
3785
+ name: kickjs-thin-entry-file
3786
+ description: Use when src/index.ts is accumulating module/middleware/plugin/adapter literals.
3787
+ \`\`\`
3788
+
3789
+ **Refactor target**:
3790
+
3791
+ \`\`\`ts
3792
+ // src/modules/index.ts
3793
+ export const modules: AppModuleClass[] = [HelloModule, UsersModule, ...]
3794
+
3795
+ // src/middleware/index.ts
3796
+ export const middleware = [helmet(), cors(), requestId(), ...]
3797
+
3798
+ // src/plugins/index.ts
3799
+ export const plugins = [MetricsPlugin(), ...]
3800
+
3801
+ // src/adapters/index.ts
3802
+ export const adapters = [SwaggerAdapter({ ... }), DevToolsAdapter()]
3803
+
3804
+ // src/index.ts — stays small
3805
+ import 'reflect-metadata'
3806
+ import './config'
3807
+ import { bootstrap } from '@forinda/kickjs'
3808
+ import { modules } from './modules'
3809
+ import { middleware } from './middleware'
3810
+ import { plugins } from './plugins'
3811
+ import { adapters } from './adapters'
3812
+ export const app = await bootstrap({ modules, middleware, plugins, adapters })
3813
+ \`\`\`
3814
+
3815
+ **Red flags**: any \`new SomeAdapter()\` or \`SomePlugin()\` literal inside \`bootstrap({ ... })\` instead of imported from a category folder.
3816
+
3817
+ ---
3818
+
3819
+ ## Skill: context-contributor
3820
+
3821
+ \`\`\`yaml
3822
+ name: kickjs-context-contributor
3823
+ description: Use when a middleware's only job is to set ctx values consumed elsewhere — replace with defineHttpContextDecorator (HTTP) or defineContextDecorator (transport-agnostic).
3824
+ \`\`\`
3825
+
3826
+ **Pattern** (HTTP — most common):
3827
+
3828
+ \`\`\`ts
3829
+ import { defineHttpContextDecorator, type RequestContext } from '@forinda/kickjs'
3830
+
3831
+ const LoadTenant = defineHttpContextDecorator({
3832
+ key: 'tenant',
3833
+ deps: { repo: TENANT_REPO },
3834
+ resolve: (ctx, { repo }) => repo.findById(ctx.req.headers['x-tenant-id'] as string),
3835
+ })
3836
+
3837
+ const LoadProject = defineHttpContextDecorator({
3838
+ key: 'project',
3839
+ dependsOn: ['tenant'],
3840
+ resolve: (ctx) => projectsRepo.find(ctx.get('tenant')!.id, ctx.params.id),
3841
+ })
3842
+
3843
+ @LoadTenant
3844
+ @LoadProject
3845
+ @Get('/projects/:id')
3846
+ getProject(ctx: RequestContext) { ctx.json(ctx.get('project')) }
3847
+ \`\`\`
3848
+
3849
+ Use \`defineContextDecorator\` (no Http prefix) when authoring a contributor that must run across HTTP, WebSocket, queue, and cron transports — \`Ctx\` defaults to the smaller \`ExecutionContext\` surface (\`get\` / \`set\` / \`requestId\` only, no \`req\`).
3850
+
3851
+ Precedence high → low: **method > class > module > adapter > global**.
3852
+ Cycles or unmet \`dependsOn\` keys throw \`MissingContributorError\` at boot.
3853
+
3854
+ **Critical rules — all stem from the same shared-via-ALS instance model**:
3855
+ - Every per-request stage (middleware → contributors → handler) gets its OWN \`RequestContext\` instance, but they all read/write the SAME \`AsyncLocalStorage\`-backed Map (\`requestStore.getStore().values\`).
3856
+ - **\`resolve\` and \`onError\` must RETURN the value** — the runner writes it via \`ctx.set(key, value)\`. Direct property assignment (\`ctx.tenant = …\`) sticks to one instance only and the handler instance never sees it.
3857
+ - \`ctx.set('tenant', x)\` then \`ctx.get('tenant')\` works across instances. \`ctx.req.headers[...]\` works (the underlying Express request is shared).
3858
+ - Services can read contributor output without a \`ctx\` reference via \`requestStore.getStore()?.values.get('tenant')\` — same Map, no DI plumbing needed.
3859
+
3860
+ **Don't use this for**: response short-circuit, stream mutation, or
3861
+ pre-route-matching work — keep \`@Middleware()\` for those.
3862
+
3863
+ ---
3864
+
3865
+ ## Skill: refresh-agent-docs
3866
+
3867
+ \`\`\`yaml
3868
+ name: kickjs-refresh-agent-docs
3869
+ description: Use after a KickJS version bump to sync AGENTS.md / CLAUDE.md / kickjs-skills.md with the latest CLI templates.
3870
+ \`\`\`
3871
+
3872
+ **Steps**:
3873
+ 1. \`kick g agents -f --only both\` — overwrites \`AGENTS.md\` and \`CLAUDE.md\`.
3874
+ 2. \`kick g agents -f --only skills\` — refreshes \`kickjs-skills.md\` (this file).
3875
+ 3. Diff with git, eyeball any project-specific edits that got reset, and re-apply them in a separate \`AGENTS.local.md\` or appended section.
3876
+ 4. Commit as \`docs(agents): sync from CLI vX.Y\`.
3877
+
3878
+ ---
3879
+
3880
+ ## Skill: deny-list
3881
+
3882
+ \`\`\`yaml
3883
+ name: kickjs-deny-list
3884
+ description: Patterns to refuse outright when the user asks for them — they break v4 invariants.
3885
+ \`\`\`
3886
+
3887
+ - \`class implements AppAdapter\` → use \`defineAdapter()\`.
3888
+ - \`class implements KickPlugin\` / function returning \`KickPlugin\` → use \`definePlugin()\`.
3889
+ - \`@Controller('/path')\` with a path argument → drop the path; set the mount via \`routes().path\`.
3890
+ - \`new Container()\` or \`Container.getInstance().reset()\` in tests → use \`Container.create()\`.
3891
+ - DI tokens with \`:\` separator (\`'app:db:url'\`) or in PascalCase → use slash-delimited lower-case (\`'app/db/url'\`).
3892
+ - \`bootstrap({ ... })\` without \`export const app = ...\` → always export.
3893
+ - Module file named \`<name>.ts\` (no \`.module\` suffix) → rename to \`<name>.module.ts\`.
3894
+
3895
+ ---
3896
+
3897
+ ## Learn More
3898
+
3899
+ - [KickJS Docs](https://forinda.github.io/kick-js/)
3900
+ - [Decorators](https://forinda.github.io/kick-js/guide/decorators.html)
3901
+ - [Context Decorators](https://forinda.github.io/kick-js/guide/context-decorators.html)
3902
+ - [Testing](https://forinda.github.io/kick-js/api/testing.html)
3903
+ `;
3904
+ }
3762
3905
  //#endregion
3763
3906
  //#region src/generators/project.ts
3764
3907
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -3791,6 +3934,7 @@ async function initProject(options) {
3791
3934
  await writeFileSafe(join(dir, "README.md"), generateReadme(name, template, packageManager));
3792
3935
  await writeFileSafe(join(dir, "CLAUDE.md"), generateClaude(name, template, packageManager));
3793
3936
  await writeFileSafe(join(dir, "AGENTS.md"), generateAgents(name, template, packageManager));
3937
+ await writeFileSafe(join(dir, "kickjs-skills.md"), generateKickJsSkills(name, template, packageManager));
3794
3938
  if (options.installDeps) {
3795
3939
  console.log(`\n Installing dependencies with ${packageManager}...\n`);
3796
3940
  try {
@@ -3804,7 +3948,7 @@ async function initProject(options) {
3804
3948
  }
3805
3949
  }
3806
3950
  try {
3807
- const { runTypegen } = await import("./typegen-C30frihW.mjs");
3951
+ const { runTypegen } = await import("./typegen-C-H8pg-y.mjs");
3808
3952
  await runTypegen({
3809
3953
  cwd: dir,
3810
3954
  allowDuplicates: true,
@@ -3902,7 +4046,10 @@ async function loadKickConfig(cwd) {
3902
4046
  try {
3903
4047
  const { pathToFileURL } = await import("node:url");
3904
4048
  const mod = await import(pathToFileURL(filepath).href);
3905
- return mod.default ?? mod;
4049
+ const config = mod.default ?? mod;
4050
+ const warnings = validateAssetMap(config, cwd);
4051
+ for (const warning of warnings) console.warn(` Warning: ${warning}`);
4052
+ return config;
3906
4053
  } catch (err) {
3907
4054
  if (filename.endsWith(".ts")) console.warn(`Warning: Failed to load ${filename}. TypeScript config files require a runtime loader (e.g. tsx, ts-node) or use kick.config.js/.mjs instead.`);
3908
4055
  continue;
@@ -3910,7 +4057,126 @@ async function loadKickConfig(cwd) {
3910
4057
  }
3911
4058
  return null;
3912
4059
  }
4060
+ /**
4061
+ * Validate `assetMap` entries on a loaded config. Returns a list of
4062
+ * human-readable warnings; the caller decides how to surface them
4063
+ * (typically `console.warn`). Never throws — `kick g` and other
4064
+ * unrelated commands should keep working even when the assetMap is
4065
+ * misconfigured.
4066
+ *
4067
+ * Checks:
4068
+ *
4069
+ * - Each entry's `src` is a non-empty string.
4070
+ * - The `src` directory exists on disk (otherwise the typegen + build
4071
+ * steps will fail later with cryptic errors).
4072
+ * - `dest` doesn't escape the project root (defensive — a `dest:
4073
+ * '../../etc'` typo could write files outside the workspace).
4074
+ * - The namespace key is a non-empty string and doesn't include a
4075
+ * `/` (would conflict with the `<namespace>/<key>` manifest format).
4076
+ */
4077
+ function validateAssetMap(config, cwd) {
4078
+ const warnings = [];
4079
+ if (!config?.assetMap) return warnings;
4080
+ const root = resolve(cwd);
4081
+ for (const [namespace, entry] of Object.entries(config.assetMap)) {
4082
+ if (!namespace || namespace.includes("/")) {
4083
+ warnings.push(`assetMap key '${namespace}' is invalid — must be a non-empty string without '/'`);
4084
+ continue;
4085
+ }
4086
+ if (typeof entry?.src !== "string" || entry.src.length === 0) {
4087
+ warnings.push(`assetMap.${namespace} is missing a non-empty 'src' field`);
4088
+ continue;
4089
+ }
4090
+ if (!existsSync(resolve(cwd, entry.src))) warnings.push(`assetMap.${namespace}.src ('${entry.src}') does not exist — typegen + build will fail`);
4091
+ if (entry.dest) {
4092
+ if (escapesRoot(resolve(cwd, entry.dest), root)) warnings.push(`assetMap.${namespace}.dest ('${entry.dest}') resolves outside the project root — refusing to copy`);
4093
+ }
4094
+ }
4095
+ return warnings;
4096
+ }
4097
+ /**
4098
+ * Returns true when `path` (absolute) resolves outside of `root`
4099
+ * (also absolute). Uses `path.relative` for accuracy:
4100
+ *
4101
+ * - The result is empty when paths are identical (inside).
4102
+ * - It starts with `..` when the path traverses outside the root.
4103
+ * - It's absolute (Windows: cross-drive) when there's no relative
4104
+ * path between them.
4105
+ *
4106
+ * Avoids the prefix-match pitfalls of `startsWith` (e.g. `/app`
4107
+ * matching `/app2/...`, or case-mismatches on macOS / Windows).
4108
+ */
4109
+ function escapesRoot(path, root) {
4110
+ const rel = relative(root, path);
4111
+ return rel === "" ? false : rel.startsWith("..") || isAbsolute(rel);
4112
+ }
4113
+ //#endregion
4114
+ //#region src/generator-extension/define.ts
4115
+ /**
4116
+ * Identity factory — returns the spec verbatim. Exists for type
4117
+ * inference and forward-compatibility (future fields can be added with
4118
+ * defaults).
4119
+ *
4120
+ * @example
4121
+ * ```ts
4122
+ * import { defineGenerator } from '@forinda/kickjs-cli'
4123
+ *
4124
+ * export default [
4125
+ * defineGenerator({
4126
+ * name: 'command',
4127
+ * description: 'Generate a CQRS command + handler',
4128
+ * files: (ctx) => [
4129
+ * {
4130
+ * path: `src/modules/${ctx.kebab}/commands/${ctx.kebab}.command.ts`,
4131
+ * content: `// command for ${ctx.pascal}`,
4132
+ * },
4133
+ * ],
4134
+ * }),
4135
+ * ]
4136
+ * ```
4137
+ */
4138
+ function defineGenerator(spec) {
4139
+ return spec;
4140
+ }
4141
+ //#endregion
4142
+ //#region src/generator-extension/context.ts
4143
+ /** Convert any string to snake_case (`UserPost` / `user-post` → `user_post`). */
4144
+ function toSnakeCase(name) {
4145
+ return toKebabCase(name).replace(/-/g, "_");
4146
+ }
4147
+ /**
4148
+ * Build a {@link GeneratorContext} from the raw name + invocation
4149
+ * arguments. Centralises the case-transformation logic so every plugin
4150
+ * generator sees the same shape regardless of how the name was typed
4151
+ * on the command line (`Post` vs `post` vs `user_post`).
4152
+ */
4153
+ function buildGeneratorContext(input) {
4154
+ const cwd = input.cwd ?? process.cwd();
4155
+ const usePlural = input.pluralize ?? true;
4156
+ const pascal = toPascalCase(input.name);
4157
+ const camel = toCamelCase(input.name);
4158
+ const kebab = toKebabCase(input.name);
4159
+ const snake = toSnakeCase(input.name);
4160
+ const ctx = {
4161
+ name: input.name,
4162
+ pascal,
4163
+ camel,
4164
+ kebab,
4165
+ snake,
4166
+ modulesDir: input.modulesDir ?? "src/modules",
4167
+ cwd,
4168
+ args: input.args ?? [],
4169
+ flags: input.flags ?? {}
4170
+ };
4171
+ if (usePlural) {
4172
+ const pluralKebab = pluralize(kebab);
4173
+ ctx.pluralKebab = pluralKebab;
4174
+ ctx.pluralPascal = toPascalCase(pluralKebab);
4175
+ ctx.pluralCamel = toCamelCase(pluralKebab);
4176
+ }
4177
+ return ctx;
4178
+ }
3913
4179
  //#endregion
3914
- export { defineConfig, generateAdapter, generateController, generateDto, generateGuard, generateMiddleware, generateModule, generateService, initProject, loadKickConfig, pluralize, toCamelCase, toKebabCase, toPascalCase };
4180
+ export { buildGeneratorContext, defineConfig, defineGenerator, generateAdapter, generateController, generateDto, generateGuard, generateMiddleware, generateModule, generateService, initProject, loadKickConfig, pluralize, toCamelCase, toKebabCase, toPascalCase };
3915
4181
 
3916
4182
  //# sourceMappingURL=index.mjs.map