@forinda/kickjs-cli 4.0.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/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs-cli v4.0.0
2
+ * @forinda/kickjs-cli v4.1.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -38,11 +38,60 @@ let _dryRun = false;
38
38
  function setDryRun(enabled) {
39
39
  _dryRun = enabled;
40
40
  }
41
- /** Write a file, creating parent directories if needed. Skips writing in dry run mode. */
41
+ /** Extensions prettier can format. Anything else is written verbatim. */
42
+ const FORMATTABLE = new Set([
43
+ ".ts",
44
+ ".tsx",
45
+ ".js",
46
+ ".jsx",
47
+ ".mjs",
48
+ ".cjs",
49
+ ".json",
50
+ ".md"
51
+ ]);
52
+ /**
53
+ * Write a file, creating parent directories if needed.
54
+ *
55
+ * After write, runs prettier against the file when:
56
+ * - format-on-write is enabled (default)
57
+ * - the extension is in {@link FORMATTABLE}
58
+ * - prettier resolves from the user's project (or our own cwd)
59
+ *
60
+ * Failures (missing prettier, unparseable source, prettier crash) are
61
+ * swallowed silently — formatting is a polish step, not a correctness
62
+ * gate. The pre-existing pre-commit hook still catches anything we
63
+ * couldn't format.
64
+ *
65
+ * Skips writing entirely in dry run mode.
66
+ */
42
67
  async function writeFileSafe(filePath, content) {
43
68
  if (_dryRun) return;
44
69
  await mkdir(dirname(filePath), { recursive: true });
45
70
  await writeFile(filePath, content, "utf-8");
71
+ if (FORMATTABLE.has(extname(filePath))) await formatFile(filePath, content).catch(() => {});
72
+ }
73
+ let _prettier = void 0;
74
+ /** Resolve prettier from the user's project; cache the result (or null) for the process. */
75
+ function resolvePrettier(cwd) {
76
+ if (_prettier !== void 0) return _prettier;
77
+ try {
78
+ _prettier = createRequire(join(cwd, "package.json"))("prettier");
79
+ } catch {
80
+ _prettier = null;
81
+ }
82
+ return _prettier;
83
+ }
84
+ async function formatFile(filePath, content) {
85
+ const prettier = resolvePrettier(process.cwd());
86
+ if (!prettier) return;
87
+ if ((await prettier.getFileInfo(filePath, { resolveConfig: true })).ignored) return;
88
+ const config = await prettier.resolveConfig(filePath) ?? {};
89
+ const formatted = await prettier.format(content, {
90
+ ...config,
91
+ filepath: filePath
92
+ });
93
+ if (formatted === content) return;
94
+ await writeFile(filePath, formatted, "utf-8");
46
95
  }
47
96
  /** Check if a file exists */
48
97
  async function fileExists(filePath) {
@@ -115,7 +164,7 @@ function generatePackageJson(name, template, kickjsVersion, packages = []) {
115
164
  "unplugin-swc": "^1.5.9",
116
165
  vite: "^8.0.3",
117
166
  vitest: "^4.1.2",
118
- typescript: "^5.9.2",
167
+ typescript: "^6.0.3",
119
168
  prettier: "^3.8.1"
120
169
  }
121
170
  }, null, 2);
@@ -709,404 +758,223 @@ Copy \`.env.example\` to \`.env\` and configure:
709
758
  - [CLI Reference](https://forinda.github.io/kick-js/api/cli.html)
710
759
  `;
711
760
  }
712
- /** Generate CLAUDE.md with AI development guide */
713
- function generateClaude(name, template, pm) {
714
- return `# CLAUDE.md — ${name} Development Guide
715
-
716
- > **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.
717
-
718
- ## Project Overview
719
-
720
- This is a **${{
721
- rest: "REST API",
722
- graphql: "GraphQL API",
723
- ddd: "Domain-Driven Design",
724
- cqrs: "CQRS + Event-Driven",
725
- minimal: "Minimal Express"
726
- }[template] ?? "REST API"}** application built with [KickJS](https://forinda.github.io/kick-js/) — a decorator-driven Node.js framework on Express 5 and TypeScript.
727
-
728
- ## Quick Commands
729
-
730
- \`\`\`bash
731
- ${pm} install # Install dependencies
732
- kick dev # Start dev server with HMR
733
- kick build # Production build via Vite
734
- kick start # Run production build
735
- ${pm} run test # Run tests with Vitest
736
- ${pm} run typecheck # TypeScript type checking
737
- ${pm} run format # Format code with Prettier
738
- \`\`\`
739
-
740
- ## Project Structure
741
-
742
- \`\`\`
743
- src/
744
- ├── index.ts # Application bootstrap
745
- ├── modules/ # Feature modules (DDD/CQRS pattern)
746
- │ └── index.ts # Module registry
747
- ${template === "graphql" ? "├── resolvers/ # GraphQL resolvers\n" : ""}└── ...
748
- \`\`\`
749
-
750
- ## Package Manager
751
-
752
- - Always use **${pm}** for this project
753
- - Run \`${pm} install\` to sync dependencies
754
- - Never mix package managers (npm/yarn/pnpm)
755
-
756
- ## Code Style
757
-
758
- - **Prettier** no semicolons, single quotes, trailing commas, 100 char width
759
- - **TypeScript strict mode** — all types required
760
- - Format before committing: \`${pm} run format\`
761
- - Type check with: \`${pm} run typecheck\`
762
-
763
- ## Key Patterns
764
-
765
- ### Controllers
766
-
767
- Use decorators to define routes. Annotate \`ctx\` with \`Ctx<KickRoutes.X['method']>\`
768
- to get fully-typed \`ctx.params\`, \`ctx.body\`, and \`ctx.query\` from the
769
- generated \`KickRoutes\` namespace (refreshed on \`kick dev\` and \`kick typegen\`).
770
-
771
- \`\`\`ts
772
- import { Controller, Get, Post, type Ctx } from '@forinda/kickjs'
773
-
774
- @Controller()
775
- export class UserController {
776
- @Get('/')
777
- async findAll(ctx: Ctx<KickRoutes.UserController['findAll']>) {
778
- return ctx.json({ users: [] })
779
- }
780
-
781
- @Post('/')
782
- async create(ctx: Ctx<KickRoutes.UserController['create']>) {
783
- const data = ctx.body
784
- return ctx.created({ user: data })
785
- }
786
- }
787
- \`\`\`
788
-
789
- ### Services
790
-
791
- Inject dependencies with \`@Service()\` and \`@Autowired()\`:
792
-
793
- \`\`\`ts
794
- import { Service, Autowired } from '@forinda/kickjs'
795
-
796
- @Service()
797
- export class UserService {
798
- @Autowired()
799
- private userRepository!: UserRepository
800
-
801
- async findAll() {
802
- return this.userRepository.findAll()
803
- }
804
- }
805
- \`\`\`
806
-
807
- ### Modules
808
-
809
- Modules implement \`AppModule\` and wire controllers via \`buildRoutes()\`.
810
-
811
- > **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.
812
-
813
- \`\`\`ts
814
- // src/modules/users/users.module.ts (named <feature>.module.ts)
815
- import { type AppModule, type ModuleRoutes, buildRoutes } from '@forinda/kickjs'
816
- import { UserController } from './user.controller'
817
-
818
- export class UserModule implements AppModule {
819
- routes(): ModuleRoutes {
820
- return {
821
- path: '/users',
822
- router: buildRoutes(UserController),
823
- controller: UserController,
824
- }
825
- }
826
- }
827
- \`\`\`
828
-
829
- Register all modules in \`src/modules/index.ts\`:
830
-
831
- \`\`\`ts
832
- import type { AppModuleClass } from '@forinda/kickjs'
833
- import { UserModule } from './user/user.module'
834
-
835
- export const modules: AppModuleClass[] = [UserModule]
836
- \`\`\`
837
-
838
- ### RequestContext
839
-
840
- Every controller method receives a \`ctx\` (alias \`Ctx<TRoute>\` or the
841
- loose \`RequestContext\`):
842
-
843
- \`\`\`ts
844
- ctx.body // Request body (parsed JSON)
845
- ctx.params // Route params
846
- ctx.query // Query string
847
- ctx.headers // Request headers
848
- ctx.requestId // Auto-generated request ID
849
- ctx.session // Session data (if session middleware enabled)
850
- ctx.file // Uploaded file (single)
851
- ctx.files // Uploaded files (multiple)
852
-
853
- // Pagination helpers
854
- ctx.qs(config) // Parse query with filters/sort/pagination
855
- ctx.paginate(handler) // Auto-paginated response
856
-
857
- // Response helpers
858
- ctx.json(data) // 200 OK with JSON
859
- ctx.created(data) // 201 Created
860
- ctx.noContent() // 204 No Content
861
- ctx.notFound() // 404 Not Found
862
- ctx.badRequest(msg) // 400 Bad Request
863
- \`\`\`
864
-
865
- > **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>.
866
-
867
- ## CLI Generators
868
-
869
- Generate code with the \`kick\` CLI:
870
-
871
- \`\`\`bash
872
- kick g module <name> # Full module (controller, service, DTOs, repo)
873
- kick g scaffold <name> <fields> # CRUD module from field definitions
874
- kick g controller <name> # Standalone controller
875
- kick g service <name> # Service class
876
- kick g middleware <name> # Express middleware
877
- kick g guard <name> # Route guard (auth, roles)
878
- kick g adapter <name> # AppAdapter with lifecycle hooks
879
- kick g dto <name> # Zod DTO schema
880
- ${template === "graphql" ? "kick g resolver <name> # GraphQL resolver\n" : ""}${template === "cqrs" ? "kick g job <name> # Queue job processor\n" : ""}\`\`\`
881
-
882
- ## Adding Packages
761
+ /**
762
+ * Generate CLAUDE.md.
763
+ *
764
+ * v4 update: this file is intentionally thin. AGENTS.md is the
765
+ * canonical, multi-agent project reference (Claude / Copilot /
766
+ * Codex / Gemini / etc.) — duplicating it here meant two files
767
+ * drifting out of sync after every framework change. The generated
768
+ * CLAUDE.md now redirects there + adds Claude-specific affordances
769
+ * only.
770
+ */
771
+ function generateClaude(name, _template, pm) {
772
+ return `# CLAUDE.md — ${name}
773
+
774
+ **Read \`./AGENTS.md\` first.** It is the canonical, multi-agent
775
+ reference for this project (Claude, Copilot, Codex, Gemini, etc.) —
776
+ project conventions, structure, decorator patterns, env wiring, CLI
777
+ generators, every gotcha.
778
+
779
+ **Then read \`./kickjs-skills.md\`.** That file is the task-oriented
780
+ skill index short, rigid recipes keyed to triggers ("add-module",
781
+ "write-controller-test", "bootstrap-export", "deny-list", …). Use it
782
+ as the playbook when executing common KickJS workflows.
783
+
784
+ This file is a thin Claude-specific layer on top of those two; when
785
+ they disagree on anything substantive, treat \`AGENTS.md\` as
786
+ authoritative and flag the discrepancy.
787
+
788
+ ## Why two files
789
+
790
+ \`AGENTS.md\` is what every agent reads. \`CLAUDE.md\` is what
791
+ Claude Code automatically loads as project context on each
792
+ conversation. Keeping CLAUDE.md slim avoids two files drifting; the
793
+ redirect above ensures Claude pulls the canonical content without
794
+ us copy-pasting.
795
+
796
+ ## Claude-specific notes
797
+
798
+ - **Slash commands** — \`/help\` for Claude Code commands; \`/init\`
799
+ to refresh project memory if AGENTS.md changes substantially.
800
+ - **Feedback** — file issues at <https://github.com/anthropics/claude-code/issues>.
801
+ - **Persistent memory** Claude maintains user/feedback/project/
802
+ reference memories under \`.claude/memory/\`. If you ask for
803
+ something that contradicts a remembered preference, Claude flags
804
+ it before acting; corrections update memory automatically.
805
+ - **Long-running tasks** — \`/loop\` and \`/schedule\` for recurring
806
+ or background work. Useful for "wait for the deploy then open a
807
+ cleanup PR" or "every Monday triage the issue board" patterns.
808
+
809
+ ## Quick reference (full version in AGENTS.md)
883
810
 
884
811
  \`\`\`bash
885
- kick add auth # JWT, API key, OAuth strategies
886
- kick add swagger # OpenAPI docs from decorators
887
- kick add ws # WebSocket support
888
- kick add queue # Background jobs (BullMQ/RabbitMQ/Kafka)
889
- kick add mailer # Email (SMTP, Resend, SES)
890
- kick add cron # Scheduled tasks
891
- kick add prisma # Prisma ORM adapter
892
- kick add drizzle # Drizzle ORM adapter
893
- kick add otel # OpenTelemetry tracing
894
- kick add --list # Show all available packages
895
- \`\`\`
896
-
897
- ## Environment Configuration
898
-
899
- The project's typed env schema lives in **\`src/config/index.ts\`** —
900
- extend the base schema there with your application-specific keys, and
901
- the schema is auto-registered with kickjs at module load. The companion
902
- \`src/index.ts\` imports it as a side effect (\`import './config'\`) **before**
903
- \`bootstrap()\` runs, so every \`@Service\`, \`@Controller\`, \`@Value\`, and
904
- \`ConfigService\` resolution sees the validated extended values.
905
-
906
- > **Do not delete \`import './config'\` from \`src/index.ts\`.** It is the
907
- > registration step that wires \`ConfigService\` to your env schema.
908
- > Without it, \`config.get('YOUR_KEY')\` returns \`undefined\` for every
909
- > user-defined key and \`@Value('YOUR_KEY')\` only works because of a
910
- > raw \`process.env\` fallback (Zod coercion + defaults are skipped).
911
-
912
- Edit \`.env\` for variable values. Access them with \`@Value()\`:
913
-
914
- \`\`\`ts
915
- import { Value } from '@forinda/kickjs'
916
-
917
- @Service()
918
- export class ApiService {
919
- @Value('API_KEY')
920
- private apiKey!: string
921
-
922
- @Value('PORT', 3000) // With default
923
- private port!: number
924
- }
925
- \`\`\`
926
-
927
- Or use \`ConfigService\`:
928
-
929
- \`\`\`ts
930
- import { Service, Autowired, ConfigService } from '@forinda/kickjs'
931
-
932
- @Service()
933
- export class AppService {
934
- @Autowired()
935
- private config!: ConfigService
936
-
937
- getPort() {
938
- // typed: number, Zod-coerced from baseEnvSchema
939
- return this.config.get('PORT')
940
- }
941
- }
942
- \`\`\`
943
-
944
- Hot-reload of \`.env\` changes during dev is wired up automatically via
945
- \`envWatchPlugin()\` in \`vite.config.ts\` — edit \`.env\`, the dev server
946
- reloads, and the next \`config.get()\` re-parses with the new values.
947
-
948
- ### Standalone Env Utilities (No DI Required)
949
-
950
- These functions work anywhere — scripts, CLI tools, plain files, outside \`@Service\`/\`@Controller\`:
951
-
952
- \`\`\`ts
953
- import { defineEnv, loadEnv, getEnv, reloadEnv, resetEnvCache, baseEnvSchema } from '@forinda/kickjs/config'
954
- import { z } from 'zod'
955
-
956
- // Define and parse schema
957
- const schema = defineEnv((base) =>
958
- base.extend({ DATABASE_URL: z.string().url() })
959
- )
960
- const env = loadEnv(schema) // Parse + validate process.env
961
- console.log(env.PORT) // 3000 (coerced to number)
962
- console.log(env.DATABASE_URL) // validated URL string
963
-
964
- // Get single value
965
- const port = getEnv('PORT') // typed after kick typegen
966
-
967
- // Reload after .env changes (HMR calls this automatically)
968
- reloadEnv()
969
-
970
- // Reset cache in tests that swap schemas
971
- resetEnvCache()
972
- \`\`\`
973
-
974
- | Function | Purpose |
975
- |----------|---------|
976
- | \`defineEnv(fn)\` | Extend base schema with custom Zod keys |
977
- | \`loadEnv(schema?)\` | Parse \`process.env\`, validate, cache, return typed object |
978
- | \`getEnv(key, schema?)\` | Get single validated env value |
979
- | \`reloadEnv()\` | Re-read \`.env\` from disk, re-parse with same schema |
980
- | \`resetEnvCache()\` | Clear parsed cache AND registered schema (for tests) |
981
- | \`baseEnvSchema\` | Base Zod schema: \`PORT\`, \`NODE_ENV\`, \`LOG_LEVEL\` |
982
-
983
- ## Standalone Utilities (No DI Required)
984
-
985
- These utilities work outside decorated classes:
986
-
987
- ### Logger
988
-
989
- \`\`\`ts
990
- import { Logger, createLogger } from '@forinda/kickjs'
991
-
992
- const log = Logger.for('MyScript') // Static factory
993
- log.info('Processing started')
994
- log.error('Something failed')
995
-
996
- const log2 = createLogger('Worker') // Function form
997
- \`\`\`
998
-
999
- ### Injection Tokens
1000
-
1001
- \`\`\`ts
1002
- import { createToken } from '@forinda/kickjs'
1003
-
1004
- // Type-safe DI tokens for factory/interface binding
1005
- const DB_URL = createToken<string>('config.database.url')
1006
- const FEATURE_FLAGS = createToken<FeatureFlags>('app.features')
812
+ ${pm} install # Install dependencies
813
+ kick dev # Dev server with HMR + typegen
814
+ kick build && kick start # Production
815
+ ${pm} run test # Vitest
816
+ ${pm} run typecheck # tsc --noEmit
817
+ ${pm} run format # Prettier
1007
818
  \`\`\`
1008
819
 
1009
- ### Reactivity
820
+ ## v4 framework reminders
1010
821
 
1011
- \`\`\`ts
1012
- import { ref, computed, watch, reactive } from '@forinda/kickjs'
1013
-
1014
- const count = ref(0)
1015
- const doubled = computed(() => count.value * 2)
1016
- const stop = watch(() => count.value, (val) => console.log(val))
1017
- count.value++ // logs 1
1018
- \`\`\`
822
+ When generating or modifying code in this project, stay aligned with the v4 conventions documented in \`AGENTS.md\`:
1019
823
 
1020
- ### HTTP Errors
824
+ - **Adapters**: \`defineAdapter()\` factory — never \`class implements AppAdapter\`.
825
+ - **Plugins**: \`definePlugin()\` factory — never plain function returning \`KickPlugin\`.
826
+ - **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.
827
+ - **Decorators**: \`@Controller()\` (no path arg — mount prefix comes from \`routes().path\`).
828
+ - **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.
829
+ - **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).
830
+ - **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.
831
+ - **Context Contributors** (\`defineContextDecorator\`) over \`@Middleware()\` for ctx-population work.
832
+ - **Repos under tests**: \`Container.create()\` for isolation — never \`new Container()\` or \`getInstance().reset()\`.
833
+ - **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.
834
+ - **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\`.
835
+ - **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\`.
1021
836
 
1022
- \`\`\`ts
1023
- import { HttpException, HttpStatus } from '@forinda/kickjs'
1024
-
1025
- throw new HttpException(HttpStatus.NOT_FOUND, 'User not found')
1026
- \`\`\`
1027
-
1028
- ## Testing
1029
-
1030
- Tests live in \`src/**/*.test.ts\`:
1031
-
1032
- \`\`\`ts
1033
- import { describe, it, expect, beforeEach } from 'vitest'
1034
- import { Container } from '@forinda/kickjs'
1035
- import { createTestApp } from '@forinda/kickjs-testing'
1036
-
1037
- describe('UserController', () => {
1038
- beforeEach(() => Container.reset())
1039
-
1040
- it('should return users', async () => {
1041
- const app = await createTestApp([UserModule])
1042
- const res = await app.get('/users')
1043
- expect(res.status).toBe(200)
1044
- })
1045
- })
1046
- \`\`\`
1047
-
1048
- Run tests:
1049
- - \`${pm} run test\` — run all tests
1050
- - \`${pm} run test:watch\` — watch mode
1051
-
1052
- ## Decorators Reference
1053
-
1054
- ### Route Decorators
1055
- - \`@Controller()\` — mark a class as an HTTP controller (path comes from \`routes().path\`)
1056
- - \`@Get('/'), @Post('/'), @Put('/'), @Delete('/'), @Patch('/')\` — HTTP methods
1057
- - \`@Middleware(fn)\` — attach middleware
1058
- - \`@Public()\` — skip authentication (requires @forinda/kickjs-auth)
1059
- - \`@Roles('admin', 'user')\` — role-based access control
1060
-
1061
- ### DI Decorators
1062
- - \`@Service()\` — singleton service (DI-registered)
1063
- - \`@Repository()\` — repository (semantic alias for @Service)
1064
- - \`@Autowired()\` — property injection
1065
- - \`@Inject('token')\` — token-based injection
1066
- - \`@Value('ENV_VAR')\` — inject config value
1067
-
1068
- ${template === "cqrs" ? `### CQRS/Event Decorators
1069
- - \`@Job('job-name')\` — queue job handler
1070
- - \`@Process('queue-name')\` — queue processor
1071
- - \`@Cron('0 * * * *')\` — cron schedule
1072
- - \`@WsController('/path')\` — WebSocket controller
1073
- - \`@Subscribe('event')\` — WebSocket event handler
1074
-
1075
- ` : ""}${template === "graphql" ? `### GraphQL Decorators
1076
- - \`@Resolver()\` — GraphQL resolver
1077
- - \`@Query()\` — GraphQL query
1078
- - \`@Mutation()\` — GraphQL mutation
1079
- - \`@Arg('name')\` — resolver argument
1080
-
1081
- ` : ""}## Common Pitfalls
1082
-
1083
- 1. **Decorators fire at import time** — make sure to import module classes in \`src/modules/index.ts\`
1084
- 2. **Tests need \`Container.reset()\`** — call in \`beforeEach\` to isolate DI state
1085
- 3. **Always use \`ctx.body\`** — never \`req.body\` directly
1086
- 4. **DI requires \`reflect-metadata\`** — already imported in \`src/index.ts\`
1087
- 5. **Vite HMR requires proper cleanup** — adapters should implement \`shutdown()\`
1088
- 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).
1089
-
1090
- ## Learn More
1091
-
1092
- - [KickJS Documentation](https://forinda.github.io/kick-js/)
1093
- - [API Reference](https://forinda.github.io/kick-js/api/)
1094
- - [CLI Commands](https://forinda.github.io/kick-js/guide/cli-commands.html)
1095
- - [Decorators Guide](https://forinda.github.io/kick-js/guide/decorators.html)
837
+ For everything else (controllers, services, modules, RequestContext API, generators, CLI commands, package additions, env wiring, troubleshooting) → \`AGENTS.md\`.
1096
838
  `;
1097
839
  }
1098
840
  /** Generate AGENTS.md with AI agent guide */
1099
841
  function generateAgents(name, template, pm) {
1100
842
  return `# AGENTS.md — AI Agent Guide for ${name}
1101
843
 
1102
- This guide helps AI agents (Claude, Copilot, etc.) work effectively on this KickJS application.
844
+ This guide is the **canonical, multi-agent reference** for this KickJS
845
+ application — Claude, Copilot, Codex, Gemini, etc. all read it first.
846
+ Per-agent files (\`CLAUDE.md\`, \`GEMINI.md\`, etc.) are thin layers that
847
+ add tool-specific affordances on top.
1103
848
 
1104
849
  ## Before You Start
1105
850
 
1106
- 1. Read \`CLAUDE.md\` for project conventions and commands
1107
- 2. Run \`${pm} install\` to install dependencies
1108
- 3. Run \`kick dev\` to verify the app starts
1109
- 4. Read the [KickJS documentation](https://forinda.github.io/kick-js/) for framework details
851
+ 1. Run \`${pm} install\` to install dependencies
852
+ 2. Run \`kick dev\` to verify the app starts
853
+ 3. Read the [KickJS documentation](https://forinda.github.io/kick-js/) for framework details
854
+
855
+ ## v4 Conventions (don't skip)
856
+
857
+ KickJS v4 made a handful of structural changes from v3. Internalise these
858
+ before generating or modifying code — they are the source of most agent
859
+ mistakes:
860
+
861
+ - **Adapters** — \`defineAdapter()\` factory. Never write \`class Foo implements AppAdapter\`.
862
+
863
+ \`\`\`ts
864
+ export const MyAdapter = defineAdapter<MyOptions>({
865
+ name: 'MyAdapter',
866
+ defaults: { ... },
867
+ build: (config) => ({
868
+ beforeMount({ app }) { /* ... */ },
869
+ afterStart({ server }) { /* ... */ },
870
+ }),
871
+ })
872
+ \`\`\`
873
+
874
+ - **Plugins** — \`definePlugin()\` factory. Same shape, never plain function returning \`KickPlugin\`.
875
+
876
+ - **DI tokens** — slash-delimited \`<scope>/<area>/<key>\`, lower-case, no \`:\` separators:
877
+
878
+ \`\`\`ts
879
+ const USERS_REPO = createToken<UsersRepo>('app/users/repository')
880
+ const DB = createToken<Database>('app/db/connection')
881
+ \`\`\`
882
+
883
+ The \`kick/\` prefix is reserved for first-party packages; this project
884
+ owns its own scope (\`app/\`, your domain name, etc.).
885
+
886
+ - **\`@Controller()\`** takes **no path argument**. Mount prefix comes from
887
+ the module's \`routes()\` return value, not the decorator. \`@Controller('/users')\`
888
+ is a v3 leftover; the linter and codegen reject it.
889
+
890
+ - **Env wiring** — \`src/config/index.ts\` calls \`loadEnv(envSchema)\` as a
891
+ side effect. \`src/index.ts\` MUST have \`import './config'\` as its **first**
892
+ import (before \`bootstrap()\`). Without it, \`ConfigService.get('YOUR_KEY')\`
893
+ returns \`undefined\` and \`@Value()\` only works via raw \`process.env\` fallback
894
+ (Zod coercion + defaults silently skipped).
895
+
896
+ - **Module entry files MUST be named \`<name>.module.ts\`** — see the Vite
897
+ HMR contract at the top of "Module Pattern" below. The CLI enforces this;
898
+ hand-rolled files must too.
899
+
900
+ - **Assets** — drop new template files into \`src/templates/<namespace>/\`
901
+ (or wherever \`kick.config.ts\` points). The dev watcher auto-rebuilds the
902
+ \`KickAssets\` augmentation; \`assets.x.y()\` re-walks on next call. No restart,
903
+ no manual build step.
904
+
905
+ - **Context over \`@Middleware()\`** — when a middleware's only job is to
906
+ populate \`ctx.set('key', value)\`, use \`defineHttpContextDecorator()\`
907
+ (HTTP) or \`defineContextDecorator()\` (transport-agnostic) instead.
908
+ Typed via \`ContextMeta\`, ordered via \`dependsOn\`, validated at boot.
909
+ Reserve \`@Middleware()\` for response short-circuit / stream mutation /
910
+ pre-route-matching work.
911
+
912
+ Two ground rules around the data flow — both stem from the fact that
913
+ every per-request stage gets its OWN \`RequestContext\` instance, all
914
+ reading/writing the SAME \`AsyncLocalStorage\`-backed Map:
915
+ - **\`resolve\` and \`onError\` must RETURN the value.** The runner
916
+ writes it via \`ctx.set(reg.key, value)\` on your behalf. Direct
917
+ property assignment (\`ctx.tenant = …\`) sticks to the contributor
918
+ instance only — the handler instance never sees it.
919
+ - **Read across instances via \`ctx.set\` / \`ctx.get\`** (or
920
+ \`requestStore.getStore()?.values.get('key')\` from a service that
921
+ has no \`ctx\` reference). \`ctx.req\` works because the underlying
922
+ Express request is shared; bespoke property assignments don't.
923
+
924
+ - **Test isolation** — default to \`Container.create()\` for fresh DI state.
925
+ Never \`new Container()\` and never \`getInstance().reset()\` — both leak
926
+ registrations between tests.
927
+
928
+ \`\`\`ts
929
+ const container = Container.create()
930
+ // ... register test-scoped providers, run, discard
931
+ \`\`\`
932
+
933
+ - **Bootstrap export** — \`src/index.ts\` MUST end with
934
+ \`export const app = await bootstrap({ ... })\`. The Vite plugin imports
935
+ the named \`app\` symbol to drive HMR module swaps; testing helpers
936
+ (\`createTestApp\`) and the OpenAPI introspector also rely on it. Drop
937
+ the \`export\` and \`kick dev\` will silently fall back to a full restart
938
+ on every save while \`createTestApp\` complains about a missing handle.
939
+
940
+ - **Keep \`src/index.ts\` thin** — collect plugins, modules, middleware, and
941
+ adapters in dedicated folders and re-export aggregated arrays. Do **not**
942
+ inline registration in the entry file:
943
+
944
+ \`\`\`ts
945
+ // src/modules/index.ts
946
+ export const modules: AppModuleClass[] = [HelloModule, UsersModule, ...]
947
+
948
+ // src/middleware/index.ts
949
+ export const middleware = [helmet(), cors(), requestId(), ...]
950
+
951
+ // src/plugins/index.ts
952
+ export const plugins = [MetricsPlugin(), AuditPlugin()]
953
+
954
+ // src/adapters/index.ts
955
+ export const adapters = [SwaggerAdapter({ ... }), DevToolsAdapter()]
956
+ \`\`\`
957
+
958
+ \`\`\`ts
959
+ // src/index.ts — stays small; one import per category
960
+ import 'reflect-metadata'
961
+ import './config'
962
+ import { bootstrap } from '@forinda/kickjs'
963
+ import { modules } from './modules'
964
+ import { middleware } from './middleware'
965
+ import { plugins } from './plugins'
966
+ import { adapters } from './adapters'
967
+
968
+ export const app = await bootstrap({ modules, middleware, plugins, adapters })
969
+ \`\`\`
970
+
971
+ This keeps the entry file diff-friendly, scales to dozens of modules
972
+ without git churn, and lets each domain own its own registration list.
973
+ The generators (\`kick g module\`, \`kick g middleware\`, \`kick g plugin\`,
974
+ \`kick g adapter\`) follow this layout — manual additions should too.
975
+
976
+ Everything else (controllers, services, modules, RequestContext API, generators,
977
+ package additions, env access patterns, troubleshooting) is detailed below.
1110
978
 
1111
979
  ## Where to Find Things
1112
980
 
@@ -1117,6 +985,7 @@ This guide helps AI agents (Claude, Copilot, etc.) work effectively on this Kick
1117
985
  | Entry point | \`src/index.ts\` |
1118
986
  | Module registry | \`src/modules/index.ts\` |
1119
987
  | Feature modules | \`src/modules/<module-name>/\` |
988
+ | **Module entry file** | \`src/modules/<name>/<name>.module.ts\` (filename suffix is required — see Vite HMR contract below) |
1120
989
  ${template === "graphql" ? "| GraphQL resolvers | `src/resolvers/` |\n" : ""}| Env values | \`.env\` |
1121
990
  | Env schema (Zod) | \`src/config/index.ts\` |
1122
991
  | TypeScript config | \`tsconfig.json\` |
@@ -1312,14 +1181,13 @@ import { Container } from '@forinda/kickjs'
1312
1181
  import { createTestApp } from '@forinda/kickjs-testing'
1313
1182
 
1314
1183
  describe('UserController', () => {
1315
- beforeEach(() => {
1316
- Container.reset() // Important: isolate DI state
1317
- })
1318
-
1319
1184
  it('should return users', async () => {
1320
- const app = await createTestApp([UserModule])
1185
+ // Container.create() isolated DI state per test, never new Container()
1186
+ // and never getInstance().reset() (both leak registrations between tests).
1187
+ const container = Container.create()
1188
+ const app = await createTestApp([UserModule], { container })
1321
1189
  const res = await app.get('/users')
1322
-
1190
+
1323
1191
  expect(res.status).toBe(200)
1324
1192
  expect(res.body).toHaveProperty('users')
1325
1193
  })
@@ -1383,7 +1251,7 @@ These work anywhere — scripts, plain files, outside \`@Service\`/\`@Controller
1383
1251
  |---------|--------|---------|
1384
1252
  | \`Logger.for(name)\` | \`@forinda/kickjs\` | \`const log = Logger.for('MyScript')\` |
1385
1253
  | \`createLogger(name)\` | \`@forinda/kickjs\` | \`const log = createLogger('Worker')\` |
1386
- | \`createToken<T>(name)\` | \`@forinda/kickjs\` | \`const TOKEN = createToken<string>('db.url')\` |
1254
+ | \`createToken<T>(name)\` | \`@forinda/kickjs\` | \`const TOKEN = createToken<string>('app/db/url')\` |
1387
1255
  | \`ref(value)\` | \`@forinda/kickjs\` | \`const count = ref(0)\` |
1388
1256
  | \`computed(fn)\` | \`@forinda/kickjs\` | \`const doubled = computed(() => count.value * 2)\` |
1389
1257
  | \`watch(source, cb)\` | \`@forinda/kickjs\` | \`watch(() => count.value, (v) => log(v))\` |
@@ -1453,42 +1321,299 @@ ${template === "graphql" ? `### GraphQL
1453
1321
 
1454
1322
  1. **Forgot to register module** — Add to \`src/modules/index.ts\` exports array
1455
1323
  2. **DI not working** — Ensure \`reflect-metadata\` is imported in \`src/index.ts\`
1456
- 3. **Tests failing randomly** — Missing \`Container.reset()\` in \`beforeEach\`
1324
+ 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()\`
1457
1325
  4. **Routes not found** — Check controller path and module registration
1458
1326
  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.
1459
1327
  6. **Decorators not working** — Check \`tsconfig.json\` has \`experimentalDecorators: true\`
1460
1328
  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.
1461
1329
  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.
1462
1330
  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).
1331
+ 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({ ... })\`.
1332
+ 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\`.
1333
+
1334
+ ## CLI Commands Reference
1335
+
1336
+ | Command | Description |
1337
+ |---------|-------------|
1338
+ | \`kick dev\` | Dev server with HMR |
1339
+ | \`kick dev:debug\` | Dev server with debugger |
1340
+ | \`kick build\` | Production build |
1341
+ | \`kick start\` | Run production build |
1342
+ | \`kick g module <names...>\` | Generate one or more modules |
1343
+ | \`kick g scaffold <name> <fields>\` | Generate CRUD |
1344
+ | \`kick g controller <name>\` | Generate controller |
1345
+ | \`kick g service <name>\` | Generate service |
1346
+ | \`kick g middleware <name>\` | Generate middleware |
1347
+ | \`kick add <package>\` | Add KickJS package |
1348
+ | \`kick add --list\` | List available packages |
1349
+ | \`kick rm module <names...>\` | Remove one or more modules |
1350
+
1351
+ > **Note:** When using \`kick new\` in scripts or CI, pass \`-t\` (or \`--template\`) and \`-r\` (or \`--repo\`) flags to bypass interactive prompts:
1352
+ > \`\`\`bash
1353
+ > kick new my-api -t ddd -r prisma --pm ${pm} --no-git --no-install -f
1354
+ > \`\`\`
1355
+
1356
+ ## Learn More
1357
+
1358
+ - [KickJS Docs](https://forinda.github.io/kick-js/)
1359
+ - [CLI Reference](https://forinda.github.io/kick-js/api/cli.html)
1360
+ - [Decorators Guide](https://forinda.github.io/kick-js/guide/decorators.html)
1361
+ - [DI System](https://forinda.github.io/kick-js/guide/dependency-injection.html)
1362
+ - [Testing](https://forinda.github.io/kick-js/api/testing.html)
1363
+ `;
1364
+ }
1365
+ /**
1366
+ * Generate `kickjs-skills.md` — task-oriented "skill" recipes for AI
1367
+ * agents (Claude superpowers, Copilot, etc.). Where AGENTS.md is the
1368
+ * narrative reference, this file lists short, rigid workflows the agent
1369
+ * should follow when it sees the corresponding trigger.
1370
+ */
1371
+ function generateKickJsSkills(name, _template, pm) {
1372
+ return `# kickjs-skills.md — Task Skills for AI Agents (${name})
1373
+
1374
+ This file is the agent-facing **skills index** for KickJS work in this
1375
+ repo. Each block below is a short, rigid workflow keyed to a specific
1376
+ trigger ("user wants to add a module", "tests are leaking state", etc.).
1377
+
1378
+ - Reference docs (narrative, exhaustive) → \`AGENTS.md\`.
1379
+ - Tool-specific notes → \`CLAUDE.md\`, \`GEMINI.md\`, etc.
1380
+ - **This file** → step-by-step recipes the agent should *execute*.
1381
+
1382
+ Re-run \`kick g agents -f --only skills\` after framework upgrades to refresh.
1383
+
1384
+ ---
1385
+
1386
+ ## Skill: add-module
1387
+
1388
+ \`\`\`yaml
1389
+ name: kickjs-add-module
1390
+ description: Use when the user asks to add a new feature module (controller + service + repo + DTOs).
1391
+ \`\`\`
1392
+
1393
+ **Trigger phrases**: "add a users module", "scaffold tasks", "new feature for X".
1394
+
1395
+ **Steps**:
1396
+ 1. Run \`kick g module <name>\` (use plural form if the project pluralizes — check \`kick.config.ts\`).
1397
+ 2. Verify the new folder under \`src/modules/<name>/\` contains \`<name>.module.ts\` (filename suffix is mandatory for HMR).
1398
+ 3. Confirm the module appears in \`src/modules/index.ts\` exports — generator does this automatically; verify if you bypassed it.
1399
+ 4. Open \`<name>.dto.ts\` and tighten the Zod schemas to real fields (the generator emits placeholders).
1400
+ 5. Run \`${pm} run typecheck\` and \`${pm} run test\` before claiming done.
1401
+
1402
+ **Red flags** (stop and ask):
1403
+ - File created as \`<name>.ts\` instead of \`<name>.module.ts\` — Vite won't HMR it.
1404
+ - Module not registered in \`src/modules/index.ts\`.
1405
+ - \`@Controller('/path')\` with a path argument — that's a v3 pattern; remove it (mount comes from \`routes().path\`).
1406
+
1407
+ ---
1408
+
1409
+ ## Skill: add-adapter
1410
+
1411
+ \`\`\`yaml
1412
+ name: kickjs-add-adapter
1413
+ description: Use when wiring a new lifecycle integration (Swagger, DevTools, Auth, custom).
1414
+ \`\`\`
1415
+
1416
+ **Steps**:
1417
+ 1. \`kick g adapter <name>\` to scaffold the boilerplate, OR install via \`kick add <package>\` for first-party adapters.
1418
+ 2. The generated file uses \`defineAdapter()\` — never \`class implements AppAdapter\`.
1419
+ 3. Add the adapter instance to \`src/adapters/index.ts\` (don't inline in \`src/index.ts\`).
1420
+ 4. If the adapter contributes to \`ctx.set/get\`, prefer \`AppAdapter.contributors?()\` over a wrapping middleware.
1421
+ 5. Verify with \`kick dev\` that the adapter's lifecycle logs fire.
1422
+
1423
+ **Red flags**:
1424
+ - Inlining the adapter list directly in \`src/index.ts\` (entry file should stay thin).
1425
+ - Returning a plain object instead of going through \`defineAdapter()\` — type inference for \`config\` will be wrong.
1426
+
1427
+ ---
1428
+
1429
+ ## Skill: write-controller-test
1430
+
1431
+ \`\`\`yaml
1432
+ name: kickjs-write-controller-test
1433
+ description: Use when adding a Vitest test that exercises an HTTP route or DI graph.
1434
+ \`\`\`
1435
+
1436
+ **Template** (copy/paste, adjust):
1437
+
1438
+ \`\`\`ts
1439
+ import { describe, it, expect } from 'vitest'
1440
+ import { Container } from '@forinda/kickjs'
1441
+ import { createTestApp } from '@forinda/kickjs-testing'
1442
+
1443
+ describe('UserController', () => {
1444
+ it('returns users', async () => {
1445
+ const container = Container.create() // isolated DI per test
1446
+ const app = await createTestApp([UserModule], { container })
1447
+ const res = await app.get('/users')
1448
+ expect(res.status).toBe(200)
1449
+ })
1450
+ })
1451
+ \`\`\`
1452
+
1453
+ **Red flags**:
1454
+ - \`new Container()\` — wrong; use \`Container.create()\`.
1455
+ - \`Container.getInstance().reset()\` — wrong; same fix.
1456
+ - Sharing a container across \`it()\` blocks — leaks registrations.
1457
+
1458
+ ---
1459
+
1460
+ ## Skill: env-wiring-check
1461
+
1462
+ \`\`\`yaml
1463
+ name: kickjs-env-wiring-check
1464
+ description: Use when ConfigService.get('SOME_KEY') returns undefined or @Value silently falls back to process.env.
1465
+ \`\`\`
1466
+
1467
+ **Diagnosis**:
1468
+ 1. Open \`src/index.ts\`. The **first non-\`reflect-metadata\`** import MUST be \`import './config'\`.
1469
+ 2. Open \`src/config/index.ts\`. It MUST call \`loadEnv(envSchema)\` as a top-level side effect.
1470
+ 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).
1471
+
1472
+ **Fix**: add the key to the schema; ensure both side-effect imports above are present.
1473
+
1474
+ ---
1475
+
1476
+ ## Skill: bootstrap-export
1477
+
1478
+ \`\`\`yaml
1479
+ name: kickjs-bootstrap-export
1480
+ description: Use when HMR is silently doing full restarts on every save, or createTestApp can't find the app handle.
1481
+ \`\`\`
1482
+
1483
+ **Check** \`src/index.ts\`'s last line:
1484
+
1485
+ \`\`\`ts
1486
+ // CORRECT
1487
+ export const app = await bootstrap({ ... })
1488
+
1489
+ // WRONG (HMR degrades to full restart, createTestApp loses the handle)
1490
+ await bootstrap({ ... })
1491
+ \`\`\`
1492
+
1493
+ The Vite plugin imports the named \`app\` symbol; testing helpers do too.
1494
+
1495
+ ---
1496
+
1497
+ ## Skill: thin-entry-file
1498
+
1499
+ \`\`\`yaml
1500
+ name: kickjs-thin-entry-file
1501
+ description: Use when src/index.ts is accumulating module/middleware/plugin/adapter literals.
1502
+ \`\`\`
1503
+
1504
+ **Refactor target**:
1505
+
1506
+ \`\`\`ts
1507
+ // src/modules/index.ts
1508
+ export const modules: AppModuleClass[] = [HelloModule, UsersModule, ...]
1509
+
1510
+ // src/middleware/index.ts
1511
+ export const middleware = [helmet(), cors(), requestId(), ...]
1512
+
1513
+ // src/plugins/index.ts
1514
+ export const plugins = [MetricsPlugin(), ...]
1515
+
1516
+ // src/adapters/index.ts
1517
+ export const adapters = [SwaggerAdapter({ ... }), DevToolsAdapter()]
1518
+
1519
+ // src/index.ts — stays small
1520
+ import 'reflect-metadata'
1521
+ import './config'
1522
+ import { bootstrap } from '@forinda/kickjs'
1523
+ import { modules } from './modules'
1524
+ import { middleware } from './middleware'
1525
+ import { plugins } from './plugins'
1526
+ import { adapters } from './adapters'
1527
+ export const app = await bootstrap({ modules, middleware, plugins, adapters })
1528
+ \`\`\`
1529
+
1530
+ **Red flags**: any \`new SomeAdapter()\` or \`SomePlugin()\` literal inside \`bootstrap({ ... })\` instead of imported from a category folder.
1531
+
1532
+ ---
1533
+
1534
+ ## Skill: context-contributor
1535
+
1536
+ \`\`\`yaml
1537
+ name: kickjs-context-contributor
1538
+ description: Use when a middleware's only job is to set ctx values consumed elsewhere — replace with defineHttpContextDecorator (HTTP) or defineContextDecorator (transport-agnostic).
1539
+ \`\`\`
1540
+
1541
+ **Pattern** (HTTP — most common):
1542
+
1543
+ \`\`\`ts
1544
+ import { defineHttpContextDecorator, type RequestContext } from '@forinda/kickjs'
1545
+
1546
+ const LoadTenant = defineHttpContextDecorator({
1547
+ key: 'tenant',
1548
+ deps: { repo: TENANT_REPO },
1549
+ resolve: (ctx, { repo }) => repo.findById(ctx.req.headers['x-tenant-id'] as string),
1550
+ })
1551
+
1552
+ const LoadProject = defineHttpContextDecorator({
1553
+ key: 'project',
1554
+ dependsOn: ['tenant'],
1555
+ resolve: (ctx) => projectsRepo.find(ctx.get('tenant')!.id, ctx.params.id),
1556
+ })
1557
+
1558
+ @LoadTenant
1559
+ @LoadProject
1560
+ @Get('/projects/:id')
1561
+ getProject(ctx: RequestContext) { ctx.json(ctx.get('project')) }
1562
+ \`\`\`
1563
+
1564
+ 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\`).
1565
+
1566
+ Precedence high → low: **method > class > module > adapter > global**.
1567
+ Cycles or unmet \`dependsOn\` keys throw \`MissingContributorError\` at boot.
1568
+
1569
+ **Critical rules — all stem from the same shared-via-ALS instance model**:
1570
+ - 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\`).
1571
+ - **\`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.
1572
+ - \`ctx.set('tenant', x)\` then \`ctx.get('tenant')\` works across instances. \`ctx.req.headers[...]\` works (the underlying Express request is shared).
1573
+ - Services can read contributor output without a \`ctx\` reference via \`requestStore.getStore()?.values.get('tenant')\` — same Map, no DI plumbing needed.
1574
+
1575
+ **Don't use this for**: response short-circuit, stream mutation, or
1576
+ pre-route-matching work — keep \`@Middleware()\` for those.
1577
+
1578
+ ---
1579
+
1580
+ ## Skill: refresh-agent-docs
1463
1581
 
1464
- ## CLI Commands Reference
1582
+ \`\`\`yaml
1583
+ name: kickjs-refresh-agent-docs
1584
+ description: Use after a KickJS version bump to sync AGENTS.md / CLAUDE.md / kickjs-skills.md with the latest CLI templates.
1585
+ \`\`\`
1465
1586
 
1466
- | Command | Description |
1467
- |---------|-------------|
1468
- | \`kick dev\` | Dev server with HMR |
1469
- | \`kick dev:debug\` | Dev server with debugger |
1470
- | \`kick build\` | Production build |
1471
- | \`kick start\` | Run production build |
1472
- | \`kick g module <names...>\` | Generate one or more modules |
1473
- | \`kick g scaffold <name> <fields>\` | Generate CRUD |
1474
- | \`kick g controller <name>\` | Generate controller |
1475
- | \`kick g service <name>\` | Generate service |
1476
- | \`kick g middleware <name>\` | Generate middleware |
1477
- | \`kick add <package>\` | Add KickJS package |
1478
- | \`kick add --list\` | List available packages |
1479
- | \`kick rm module <names...>\` | Remove one or more modules |
1587
+ **Steps**:
1588
+ 1. \`kick g agents -f --only both\` — overwrites \`AGENTS.md\` and \`CLAUDE.md\`.
1589
+ 2. \`kick g agents -f --only skills\` refreshes \`kickjs-skills.md\` (this file).
1590
+ 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.
1591
+ 4. Commit as \`docs(agents): sync from CLI vX.Y\`.
1480
1592
 
1481
- > **Note:** When using \`kick new\` in scripts or CI, pass \`-t\` (or \`--template\`) and \`-r\` (or \`--repo\`) flags to bypass interactive prompts:
1482
- > \`\`\`bash
1483
- > kick new my-api -t ddd -r prisma --pm ${pm} --no-git --no-install -f
1484
- > \`\`\`
1593
+ ---
1594
+
1595
+ ## Skill: deny-list
1596
+
1597
+ \`\`\`yaml
1598
+ name: kickjs-deny-list
1599
+ description: Patterns to refuse outright when the user asks for them — they break v4 invariants.
1600
+ \`\`\`
1601
+
1602
+ - \`class implements AppAdapter\` → use \`defineAdapter()\`.
1603
+ - \`class implements KickPlugin\` / function returning \`KickPlugin\` → use \`definePlugin()\`.
1604
+ - \`@Controller('/path')\` with a path argument → drop the path; set the mount via \`routes().path\`.
1605
+ - \`new Container()\` or \`Container.getInstance().reset()\` in tests → use \`Container.create()\`.
1606
+ - DI tokens with \`:\` separator (\`'app:db:url'\`) or in PascalCase → use slash-delimited lower-case (\`'app/db/url'\`).
1607
+ - \`bootstrap({ ... })\` without \`export const app = ...\` → always export.
1608
+ - Module file named \`<name>.ts\` (no \`.module\` suffix) → rename to \`<name>.module.ts\`.
1609
+
1610
+ ---
1485
1611
 
1486
1612
  ## Learn More
1487
1613
 
1488
1614
  - [KickJS Docs](https://forinda.github.io/kick-js/)
1489
- - [CLI Reference](https://forinda.github.io/kick-js/api/cli.html)
1490
- - [Decorators Guide](https://forinda.github.io/kick-js/guide/decorators.html)
1491
- - [DI System](https://forinda.github.io/kick-js/guide/dependency-injection.html)
1615
+ - [Decorators](https://forinda.github.io/kick-js/guide/decorators.html)
1616
+ - [Context Decorators](https://forinda.github.io/kick-js/guide/context-decorators.html)
1492
1617
  - [Testing](https://forinda.github.io/kick-js/api/testing.html)
1493
1618
  `;
1494
1619
  }
@@ -1524,6 +1649,7 @@ async function initProject(options) {
1524
1649
  await writeFileSafe(join(dir, "README.md"), generateReadme(name, template, packageManager));
1525
1650
  await writeFileSafe(join(dir, "CLAUDE.md"), generateClaude(name, template, packageManager));
1526
1651
  await writeFileSafe(join(dir, "AGENTS.md"), generateAgents(name, template, packageManager));
1652
+ await writeFileSafe(join(dir, "kickjs-skills.md"), generateKickJsSkills(name, template, packageManager));
1527
1653
  if (options.installDeps) {
1528
1654
  console.log(`\n Installing dependencies with ${packageManager}...\n`);
1529
1655
  try {
@@ -2612,7 +2738,7 @@ export interface I${pascal}Repository {
2612
2738
  * \`@Inject(${pascal.toUpperCase()}_REPOSITORY)\` both return the typed
2613
2739
  * interface — no manual generic, no \`any\` cast.
2614
2740
  */
2615
- export const ${pascal.toUpperCase()}_REPOSITORY = createToken<I${pascal}Repository>('${pascal}/Repository')
2741
+ export const ${pascal.toUpperCase()}_REPOSITORY = createToken<I${pascal}Repository>('app/${kebab}/repository')
2616
2742
  `;
2617
2743
  }
2618
2744
  function generateInMemoryRepository(ctx) {
@@ -4030,97 +4156,114 @@ export const modules: AppModuleClass[] = [${pascal}Module]
4030
4156
  }
4031
4157
  //#endregion
4032
4158
  //#region src/generators/adapter.ts
4159
+ /**
4160
+ * Scaffold a `defineAdapter()` factory under `src/adapters/<name>.adapter.ts`.
4161
+ *
4162
+ * v4 dropped the `class implements AppAdapter` pattern in favour of the
4163
+ * `defineAdapter()` factory (architecture.md §21.3.4). The generated
4164
+ * template uses the new factory shape so adopters get a working
4165
+ * adapter with all four lifecycle hooks (beforeMount, beforeStart,
4166
+ * afterStart, shutdown), a typed config object with defaults, and the
4167
+ * factory's call / `.scoped()` / `.async()` surfaces — without
4168
+ * writing a single class.
4169
+ */
4033
4170
  async function generateAdapter(options) {
4034
4171
  const { name, outDir } = options;
4035
4172
  const kebab = toKebabCase(name);
4036
4173
  const pascal = toPascalCase(name);
4037
4174
  const files = [];
4038
4175
  const filePath = join(outDir, `${kebab}.adapter.ts`);
4039
- await writeFileSafe(filePath, `import type { AppAdapter, AdapterContext, AdapterMiddleware } from '@forinda/kickjs'
4176
+ await writeFileSafe(filePath, `import { defineAdapter, type AdapterContext, type AdapterMiddleware } from '@forinda/kickjs'
4040
4177
 
4041
- export interface ${pascal}AdapterOptions {
4042
- // Add your adapter configuration here
4178
+ /**
4179
+ * Configuration for the ${pascal} adapter.
4180
+ *
4181
+ * Adapters typically take a small config object so callers can tune
4182
+ * behaviour at bootstrap time. Keep the shape narrow — anything
4183
+ * derived from the environment should be read inside the build
4184
+ * function via getEnv(), not forced onto the caller.
4185
+ */
4186
+ export interface ${pascal}AdapterConfig {
4187
+ // Add your adapter configuration here, e.g.:
4188
+ // enabled?: boolean
4189
+ // apiKey?: string
4043
4190
  }
4044
4191
 
4045
4192
  /**
4046
- * ${pascal} adapter.
4193
+ * ${pascal} adapter — built via \`defineAdapter()\` so callers get the
4194
+ * factory's call / \`.scoped()\` / \`.async()\` surfaces for free.
4047
4195
  *
4048
4196
  * Hooks into the Application lifecycle to add middleware, routes,
4049
4197
  * or external service connections.
4050
4198
  *
4051
- * Usage:
4052
- * bootstrap({
4053
- * adapters: [new ${pascal}Adapter({ ... })],
4054
- * })
4199
+ * @example
4200
+ * \`\`\`ts
4201
+ * import { bootstrap } from '@forinda/kickjs'
4202
+ * import { ${pascal}Adapter } from './adapters/${kebab}.adapter'
4203
+ *
4204
+ * bootstrap({
4205
+ * modules,
4206
+ * adapters: [${pascal}Adapter({ /* config overrides *\\/ })],
4207
+ * })
4208
+ * \`\`\`
4055
4209
  */
4056
- export class ${pascal}Adapter implements AppAdapter {
4057
- name = '${pascal}Adapter'
4058
-
4059
- constructor(private options: ${pascal}AdapterOptions = {}) {}
4060
-
4061
- /**
4062
- * Return middleware entries that the Application will mount.
4063
- * Use \`phase\` to control where in the pipeline they run:
4064
- * 'beforeGlobal' | 'afterGlobal' | 'beforeRoutes' | 'afterRoutes'
4065
- */
4066
- middleware(): AdapterMiddleware[] {
4067
- return [
4068
- // Example: add a custom header to all responses
4069
- // {
4070
- // phase: 'beforeGlobal',
4071
- // handler: (_req: any, res: any, next: any) => {
4072
- // res.setHeader('X-${pascal}', 'true')
4073
- // next()
4074
- // },
4075
- // },
4076
- // Example: scope middleware to a specific path
4077
- // {
4078
- // phase: 'beforeRoutes',
4079
- // path: '/api/v1/admin',
4080
- // handler: myAdminMiddleware(),
4081
- // },
4082
- ]
4083
- }
4210
+ export const ${pascal}Adapter = defineAdapter<${pascal}AdapterConfig>({
4211
+ name: '${pascal}Adapter',
4212
+ defaults: {
4213
+ // Default config values go here
4214
+ },
4215
+ build: (_config, { name: _name }) => ({
4216
+ /**
4217
+ * Return middleware entries that the Application will mount.
4218
+ * \`phase\` controls where in the pipeline they run:
4219
+ * 'beforeGlobal' | 'afterGlobal' | 'beforeRoutes' | 'afterRoutes'.
4220
+ */
4221
+ middleware(): AdapterMiddleware[] {
4222
+ return [
4223
+ // Example: add a custom header to all responses
4224
+ // {
4225
+ // phase: 'beforeGlobal',
4226
+ // handler: (_req, res, next) => {
4227
+ // res.setHeader('X-${pascal}', 'true')
4228
+ // next()
4229
+ // },
4230
+ // },
4231
+ ]
4232
+ },
4084
4233
 
4085
- /**
4086
- * Called before global middleware.
4087
- * Use this to mount routes that bypass the middleware stack
4088
- * (health checks, docs UI, static assets).
4089
- */
4090
- beforeMount({ app }: AdapterContext): void {
4091
- // Example: mount a status route
4092
- // app.get('/${kebab}/status', (_req, res) => {
4093
- // res.json({ status: 'ok' })
4094
- // })
4095
- }
4234
+ /**
4235
+ * Called before global middleware. Use this to mount routes that
4236
+ * bypass the middleware stack (health checks, docs UI, static
4237
+ * assets).
4238
+ */
4239
+ beforeMount(_ctx: AdapterContext): void {
4240
+ // Example:
4241
+ // _ctx.app.get('/${kebab}/status', (_req, res) => res.json({ status: 'ok' }))
4242
+ },
4096
4243
 
4097
- /**
4098
- * Called after modules and routes are registered, before the server starts.
4099
- * Use this for late-stage DI registrations or config validation.
4100
- */
4101
- beforeStart({ container }: AdapterContext): void {
4102
- // Example: register a service in the DI container
4103
- // container.registerInstance(MY_TOKEN, new MyService(this.options))
4104
- }
4244
+ /**
4245
+ * Called after modules and routes are registered, before the
4246
+ * server starts. Use this for late-stage DI registrations or
4247
+ * config validation.
4248
+ */
4249
+ beforeStart(_ctx: AdapterContext): void {
4250
+ // Example: _ctx.container.bindToken(MY_TOKEN, new MyService(_config))
4251
+ },
4105
4252
 
4106
- /**
4107
- * Called after the HTTP server is listening.
4108
- * Use this to attach to the raw http.Server (Socket.IO, gRPC, etc).
4109
- */
4110
- afterStart({ server, container }: AdapterContext): void {
4111
- // Example: attach Socket.IO
4112
- // const io = new Server(server)
4113
- // container.registerInstance(SOCKET_IO, io)
4114
- }
4253
+ /**
4254
+ * Called after the HTTP server is listening. Use this to attach
4255
+ * to the raw http.Server (Socket.IO, gRPC, etc).
4256
+ */
4257
+ afterStart(_ctx: AdapterContext): void {
4258
+ // Example: const io = new Server(_ctx.server)
4259
+ },
4115
4260
 
4116
- /**
4117
- * Called on graceful shutdown. Clean up connections.
4118
- */
4119
- async shutdown(): Promise<void> {
4120
- // Example: close a connection pool
4121
- // await this.pool.end()
4122
- }
4123
- }
4261
+ /** Called on graceful shutdown. Clean up connections. */
4262
+ async shutdown(): Promise<void> {
4263
+ // Example: await this.pool.end()
4264
+ },
4265
+ }),
4266
+ })
4124
4267
  `);
4125
4268
  files.push(filePath);
4126
4269
  return files;
@@ -4128,80 +4271,84 @@ export class ${pascal}Adapter implements AppAdapter {
4128
4271
  //#endregion
4129
4272
  //#region src/generators/plugin.ts
4130
4273
  /**
4131
- * Scaffold a `KickPlugin` under `src/plugins/<name>.plugin.ts`.
4274
+ * Scaffold a `definePlugin()` factory under `src/plugins/<name>.plugin.ts`.
4132
4275
  *
4133
- * Plugins are the canonical place to wire DI bindings, load extra
4134
- * modules, add middleware, or attach startup hooks without writing a
4135
- * full adapter. The generated template implements every optional
4136
- * `KickPlugin` hook with commented examples so users can uncomment
4137
- * the ones they need and delete the rest.
4276
+ * v4 standardised on the `definePlugin()` factory pattern (architecture
4277
+ * §21.2.2) same surface as `defineAdapter()`, so adopters learn one
4278
+ * mental model. The generated template uses the factory shape with a
4279
+ * typed config object, defaults block, and a build function returning
4280
+ * the underlying KickPlugin hooks.
4138
4281
  */
4139
4282
  async function generatePlugin(options) {
4140
4283
  const { name, outDir } = options;
4141
4284
  const kebab = toKebabCase(name);
4142
4285
  const pascal = toPascalCase(name);
4143
- const factoryName = `${toCamelCase(name)}Plugin`;
4144
4286
  const files = [];
4145
4287
  const filePath = join(outDir, `${kebab}.plugin.ts`);
4146
- await writeFileSafe(filePath, `import type { KickPlugin, Container, AppAdapter, AppModuleClass } from '@forinda/kickjs'
4288
+ await writeFileSafe(filePath, `import {
4289
+ definePlugin,
4290
+ type AppAdapter,
4291
+ type AppModuleClass,
4292
+ type Container,
4293
+ } from '@forinda/kickjs'
4147
4294
 
4148
4295
  /**
4149
- * Options for the ${pascal} plugin.
4296
+ * Configuration for the ${pascal} plugin.
4150
4297
  *
4151
- * Plugins typically take a small options object in their factory so
4152
- * callers can configure them inline at bootstrap time. Keep the
4153
- * shape narrow — anything derived from the environment should be
4154
- * read via \`getEnv\` inside the plugin itself, not forced onto the
4155
- * caller.
4298
+ * Plugins typically take a small config object so callers can tune
4299
+ * behaviour at bootstrap time. Keep the shape narrow — anything
4300
+ * derived from the environment should be read inside the build
4301
+ * function via getEnv(), not forced onto the caller.
4156
4302
  */
4157
- export interface ${pascal}PluginOptions {
4158
- // Add your plugin options here, for example:
4303
+ export interface ${pascal}PluginConfig {
4304
+ // Add your plugin config here, e.g.:
4159
4305
  // enabled?: boolean
4160
4306
  // apiKey?: string
4161
4307
  }
4162
4308
 
4163
4309
  /**
4164
- * ${pascal} plugin.
4310
+ * ${pascal} plugin — built via \`definePlugin()\` so callers get the
4311
+ * factory's call / \`.scoped()\` / \`.async()\` surfaces for free.
4165
4312
  *
4166
- * A \`KickPlugin\` bundles DI bindings, modules, adapters, and
4167
- * middleware into one object that can be added to \`bootstrap({ plugins })\`.
4168
- * Every hook is optional — delete the ones you don't need and keep
4169
- * only the surface your plugin actually uses.
4313
+ * A plugin bundles DI bindings, modules, adapters, and middleware
4314
+ * into one object that can be added to \`bootstrap({ plugins })\`.
4170
4315
  *
4171
- * Lifecycle order:
4316
+ * Lifecycle order (each hook is optional — delete the ones you don't
4317
+ * need and keep only the surface your plugin actually uses):
4172
4318
  *
4173
- * 1. \`register(container)\` — runs before user modules load. Use
4319
+ * 1. \`register(container)\` — runs before user modules load. Use
4174
4320
  * it to bind services that modules depend on.
4175
- * 2. \`modules()\` — plugin modules load before user modules.
4176
- * 3. \`adapters()\` — plugin adapters are added before user adapters.
4177
- * 4. \`middleware()\` — plugin middleware runs before user middleware.
4178
- * 5. \`onReady(container)\` — runs after the app has fully bootstrapped.
4179
- * 6. \`shutdown()\` — runs on graceful shutdown.
4321
+ * 2. \`modules()\` — plugin modules load before user modules.
4322
+ * 3. \`adapters()\` — plugin adapters mount before user adapters.
4323
+ * 4. \`middleware()\` — plugin middleware runs before user middleware.
4324
+ * 5. \`onReady(container)\` — runs after the app has fully bootstrapped.
4325
+ * 6. \`shutdown()\` — runs on graceful shutdown.
4180
4326
  *
4181
4327
  * @example
4182
4328
  * \`\`\`ts
4183
4329
  * import { bootstrap } from '@forinda/kickjs'
4184
- * import { ${factoryName} } from './plugins/${kebab}.plugin'
4330
+ * import { ${pascal}Plugin } from './plugins/${kebab}.plugin'
4185
4331
  *
4186
4332
  * export const app = await bootstrap({
4187
4333
  * modules,
4188
- * plugins: [${factoryName}({})],
4334
+ * plugins: [${pascal}Plugin({ /* config overrides *\\/ })],
4189
4335
  * })
4190
4336
  * \`\`\`
4191
4337
  */
4192
- export function ${factoryName}(options: ${pascal}PluginOptions = {}): KickPlugin {
4193
- return {
4194
- name: '${kebab}',
4195
-
4338
+ export const ${pascal}Plugin = definePlugin<${pascal}PluginConfig>({
4339
+ name: '${pascal}Plugin',
4340
+ defaults: {
4341
+ // Default config values go here
4342
+ },
4343
+ build: (_config, { name: _name }) => ({
4196
4344
  /**
4197
4345
  * Register DI bindings before modules load.
4198
4346
  * Use \`container.registerInstance(TOKEN, value)\` for singletons
4199
4347
  * and \`container.registerFactory(TOKEN, () => ...)\` for lazy
4200
4348
  * constructions.
4201
4349
  */
4202
- register(container: Container): void {
4203
- // Example: bind a configured service to a DI token
4204
- // container.registerInstance(MY_TOKEN, new MyService(options))
4350
+ register(_container: Container): void {
4351
+ // Example: _container.registerInstance(MY_TOKEN, new MyService(_config))
4205
4352
  },
4206
4353
 
4207
4354
  /**
@@ -4217,11 +4364,11 @@ export function ${factoryName}(options: ${pascal}PluginOptions = {}): KickPlugin
4217
4364
 
4218
4365
  /**
4219
4366
  * Return adapter instances to be added to the application.
4220
- * Plugin adapters are added before user adapters.
4367
+ * Plugin adapters mount before user adapters.
4221
4368
  */
4222
4369
  adapters(): AppAdapter[] {
4223
4370
  return [
4224
- // new MyAdapter({ ... }),
4371
+ // MyAdapter({ ... }),
4225
4372
  ]
4226
4373
  },
4227
4374
 
@@ -4229,10 +4376,10 @@ export function ${factoryName}(options: ${pascal}PluginOptions = {}): KickPlugin
4229
4376
  * Return Express middleware entries to be added to the global
4230
4377
  * pipeline. Plugin middleware runs before user-defined middleware.
4231
4378
  */
4232
- middleware(): any[] {
4379
+ middleware(): unknown[] {
4233
4380
  return [
4234
4381
  // helmet(),
4235
- // myCustomMiddleware(options),
4382
+ // myCustomMiddleware(_config),
4236
4383
  ]
4237
4384
  },
4238
4385
 
@@ -4241,9 +4388,9 @@ export function ${factoryName}(options: ${pascal}PluginOptions = {}): KickPlugin
4241
4388
  * for post-startup work like logging, health checks, or warming
4242
4389
  * a cache. Runs once per process.
4243
4390
  */
4244
- async onReady(container: Container): Promise<void> {
4245
- // const logger = container.resolve(Logger)
4246
- // logger.info('${pascal} plugin ready')
4391
+ async onReady(_container: Container): Promise<void> {
4392
+ // const log = _container.resolve(Logger)
4393
+ // log.info('${pascal} plugin ready')
4247
4394
  },
4248
4395
 
4249
4396
  /**
@@ -4251,10 +4398,10 @@ export function ${factoryName}(options: ${pascal}PluginOptions = {}): KickPlugin
4251
4398
  * resources this plugin owns (connections, timers, subscriptions).
4252
4399
  */
4253
4400
  async shutdown(): Promise<void> {
4254
- // await this.connection?.close()
4401
+ // Example: await this.connection?.close()
4255
4402
  },
4256
- }
4257
- }
4403
+ }),
4404
+ })
4258
4405
  `);
4259
4406
  files.push(filePath);
4260
4407
  return files;
@@ -4487,94 +4634,276 @@ export class ${pascal}Controller {
4487
4634
  ctx.created({ message: '${pascal} created', data: ctx.body })
4488
4635
  }
4489
4636
  }
4490
- `);
4491
- files.push(filePath);
4492
- return files;
4637
+ `);
4638
+ files.push(filePath);
4639
+ return files;
4640
+ }
4641
+ //#endregion
4642
+ //#region src/generators/dto.ts
4643
+ async function generateDto(options) {
4644
+ const { name, moduleName, modulesDir, pattern } = options;
4645
+ const outDir = resolveOutDir({
4646
+ type: "dto",
4647
+ outDir: options.outDir,
4648
+ moduleName,
4649
+ modulesDir,
4650
+ defaultDir: "src/dtos",
4651
+ pattern,
4652
+ shouldPluralize: options.pluralize ?? true
4653
+ });
4654
+ const kebab = toKebabCase(name);
4655
+ const pascal = toPascalCase(name);
4656
+ const camel = toCamelCase(name);
4657
+ const files = [];
4658
+ const filePath = join(outDir, `${kebab}.dto.ts`);
4659
+ await writeFileSafe(filePath, `import { z } from 'zod'
4660
+
4661
+ export const ${camel}Schema = z.object({
4662
+ // Define your schema fields here
4663
+ name: z.string().min(1).max(200),
4664
+ })
4665
+
4666
+ export type ${pascal}DTO = z.infer<typeof ${camel}Schema>
4667
+ `);
4668
+ files.push(filePath);
4669
+ return files;
4670
+ }
4671
+ //#endregion
4672
+ //#region src/generators/config.ts
4673
+ async function generateConfig(options) {
4674
+ const filePath = join(options.outDir, "kick.config.ts");
4675
+ const modulesDir = options.modulesDir ?? "src/modules";
4676
+ const defaultRepo = options.defaultRepo ?? "inmemory";
4677
+ if (existsSync(filePath) && !options.force) {
4678
+ if (!await confirm({
4679
+ message: "kick.config.ts already exists. Overwrite?",
4680
+ initialValue: false
4681
+ })) {
4682
+ console.log("\n Skipped — existing kick.config.ts preserved.");
4683
+ return [];
4684
+ }
4685
+ }
4686
+ await writeFileSafe(filePath, `import { defineConfig } from '@forinda/kickjs-cli'
4687
+
4688
+ export default defineConfig({
4689
+ modules: {
4690
+ dir: '${modulesDir}',
4691
+ repo: '${defaultRepo}',
4692
+ pluralize: true,
4693
+ },
4694
+
4695
+ typegen: {
4696
+ schemaValidator: 'zod',
4697
+ },
4698
+
4699
+ commands: [
4700
+ {
4701
+ name: 'test',
4702
+ description: 'Run tests with Vitest',
4703
+ steps: 'npx vitest run',
4704
+ },
4705
+ {
4706
+ name: 'format',
4707
+ description: 'Format code with Prettier',
4708
+ steps: 'npx prettier --write src/',
4709
+ },
4710
+ {
4711
+ name: 'format:check',
4712
+ description: 'Check formatting without writing',
4713
+ steps: 'npx prettier --check src/',
4714
+ },
4715
+ {
4716
+ name: 'ci:check',
4717
+ description: 'Run typecheck + format check',
4718
+ steps: ['npx tsc --noEmit', 'npx prettier --check src/'],
4719
+ aliases: ['verify'],
4720
+ },
4721
+ ],
4722
+ })
4723
+ `);
4724
+ return [filePath];
4725
+ }
4726
+ //#endregion
4727
+ //#region src/config.ts
4728
+ const PACKAGE_MANAGERS = [
4729
+ "pnpm",
4730
+ "npm",
4731
+ "yarn",
4732
+ "bun"
4733
+ ];
4734
+ const BUILTIN_REPO_TYPES = [
4735
+ "drizzle",
4736
+ "inmemory",
4737
+ "prisma"
4738
+ ];
4739
+ /** Resolve module config from `modules.*` block. */
4740
+ function resolveModuleConfig(config) {
4741
+ if (!config) return {};
4742
+ const mc = {
4743
+ dir: config.modules?.dir,
4744
+ repo: config.modules?.repo,
4745
+ schemaDir: config.modules?.schemaDir,
4746
+ pluralize: config.modules?.pluralize,
4747
+ prismaClientPath: config.modules?.prismaClientPath
4748
+ };
4749
+ if (mc.repo && typeof mc.repo === "string" && !BUILTIN_REPO_TYPES.includes(mc.repo)) console.warn(` Warning: modules.repo '${mc.repo}' is not a built-in type (${BUILTIN_REPO_TYPES.join(", ")}). It will generate a stub repository. Use { name: '${mc.repo}' } to silence this warning.`);
4750
+ return mc;
4751
+ }
4752
+ const CONFIG_FILES = [
4753
+ "kick.config.ts",
4754
+ "kick.config.js",
4755
+ "kick.config.mjs",
4756
+ "kick.config.json"
4757
+ ];
4758
+ /** Load kick.config.* from the project root */
4759
+ async function loadKickConfig(cwd) {
4760
+ for (const filename of CONFIG_FILES) {
4761
+ const filepath = join(cwd, filename);
4762
+ try {
4763
+ await access(filepath);
4764
+ } catch {
4765
+ continue;
4766
+ }
4767
+ if (filename.endsWith(".json")) {
4768
+ const content = await readFile(filepath, "utf-8");
4769
+ return JSON.parse(content);
4770
+ }
4771
+ try {
4772
+ const { pathToFileURL } = await import("node:url");
4773
+ const mod = await import(pathToFileURL(filepath).href);
4774
+ const config = mod.default ?? mod;
4775
+ const warnings = validateAssetMap(config, cwd);
4776
+ for (const warning of warnings) console.warn(` Warning: ${warning}`);
4777
+ return config;
4778
+ } catch (err) {
4779
+ 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.`);
4780
+ continue;
4781
+ }
4782
+ }
4783
+ return null;
4784
+ }
4785
+ /**
4786
+ * Validate `assetMap` entries on a loaded config. Returns a list of
4787
+ * human-readable warnings; the caller decides how to surface them
4788
+ * (typically `console.warn`). Never throws — `kick g` and other
4789
+ * unrelated commands should keep working even when the assetMap is
4790
+ * misconfigured.
4791
+ *
4792
+ * Checks:
4793
+ *
4794
+ * - Each entry's `src` is a non-empty string.
4795
+ * - The `src` directory exists on disk (otherwise the typegen + build
4796
+ * steps will fail later with cryptic errors).
4797
+ * - `dest` doesn't escape the project root (defensive — a `dest:
4798
+ * '../../etc'` typo could write files outside the workspace).
4799
+ * - The namespace key is a non-empty string and doesn't include a
4800
+ * `/` (would conflict with the `<namespace>/<key>` manifest format).
4801
+ */
4802
+ function validateAssetMap(config, cwd) {
4803
+ const warnings = [];
4804
+ if (!config?.assetMap) return warnings;
4805
+ const root = resolve(cwd);
4806
+ for (const [namespace, entry] of Object.entries(config.assetMap)) {
4807
+ if (!namespace || namespace.includes("/")) {
4808
+ warnings.push(`assetMap key '${namespace}' is invalid — must be a non-empty string without '/'`);
4809
+ continue;
4810
+ }
4811
+ if (typeof entry?.src !== "string" || entry.src.length === 0) {
4812
+ warnings.push(`assetMap.${namespace} is missing a non-empty 'src' field`);
4813
+ continue;
4814
+ }
4815
+ if (!existsSync(resolve(cwd, entry.src))) warnings.push(`assetMap.${namespace}.src ('${entry.src}') does not exist — typegen + build will fail`);
4816
+ if (entry.dest) {
4817
+ if (escapesRoot$1(resolve(cwd, entry.dest), root)) warnings.push(`assetMap.${namespace}.dest ('${entry.dest}') resolves outside the project root — refusing to copy`);
4818
+ }
4819
+ }
4820
+ return warnings;
4821
+ }
4822
+ /**
4823
+ * Returns true when `path` (absolute) resolves outside of `root`
4824
+ * (also absolute). Uses `path.relative` for accuracy:
4825
+ *
4826
+ * - The result is empty when paths are identical (inside).
4827
+ * - It starts with `..` when the path traverses outside the root.
4828
+ * - It's absolute (Windows: cross-drive) when there's no relative
4829
+ * path between them.
4830
+ *
4831
+ * Avoids the prefix-match pitfalls of `startsWith` (e.g. `/app`
4832
+ * matching `/app2/...`, or case-mismatches on macOS / Windows).
4833
+ */
4834
+ function escapesRoot$1(path, root) {
4835
+ const rel = relative(root, path);
4836
+ return rel === "" ? false : rel.startsWith("..") || isAbsolute(rel);
4493
4837
  }
4494
4838
  //#endregion
4495
- //#region src/generators/dto.ts
4496
- async function generateDto(options) {
4497
- const { name, moduleName, modulesDir, pattern } = options;
4498
- const outDir = resolveOutDir({
4499
- type: "dto",
4500
- outDir: options.outDir,
4501
- moduleName,
4502
- modulesDir,
4503
- defaultDir: "src/dtos",
4504
- pattern,
4505
- shouldPluralize: options.pluralize ?? true
4506
- });
4507
- const kebab = toKebabCase(name);
4508
- const pascal = toPascalCase(name);
4509
- const camel = toCamelCase(name);
4510
- const files = [];
4511
- const filePath = join(outDir, `${kebab}.dto.ts`);
4512
- await writeFileSafe(filePath, `import { z } from 'zod'
4513
-
4514
- export const ${camel}Schema = z.object({
4515
- // Define your schema fields here
4516
- name: z.string().min(1).max(200),
4517
- })
4518
-
4519
- export type ${pascal}DTO = z.infer<typeof ${camel}Schema>
4520
- `);
4521
- files.push(filePath);
4522
- return files;
4839
+ //#region src/generators/agent-docs.ts
4840
+ const VALID_TEMPLATES = new Set([
4841
+ "rest",
4842
+ "graphql",
4843
+ "ddd",
4844
+ "cqrs",
4845
+ "minimal"
4846
+ ]);
4847
+ function detectName(outDir, override) {
4848
+ if (override) return override;
4849
+ try {
4850
+ const pkg = JSON.parse(readFileSync(join(outDir, "package.json"), "utf-8"));
4851
+ if (pkg.name) return pkg.name.replace(/^@[^/]+\//, "");
4852
+ } catch {}
4853
+ return outDir.split("/").filter(Boolean).pop() ?? "app";
4523
4854
  }
4524
- //#endregion
4525
- //#region src/generators/config.ts
4526
- async function generateConfig(options) {
4527
- const filePath = join(options.outDir, "kick.config.ts");
4528
- const modulesDir = options.modulesDir ?? "src/modules";
4529
- const defaultRepo = options.defaultRepo ?? "inmemory";
4530
- if (existsSync(filePath) && !options.force) {
4531
- if (!await confirm({
4532
- message: "kick.config.ts already exists. Overwrite?",
4533
- initialValue: false
4534
- })) {
4535
- console.log("\n Skipped existing kick.config.ts preserved.");
4536
- return [];
4855
+ function detectPm(outDir, override) {
4856
+ if (override) return override;
4857
+ try {
4858
+ const pkg = JSON.parse(readFileSync(join(outDir, "package.json"), "utf-8"));
4859
+ if (pkg.packageManager) return pkg.packageManager.split("@")[0];
4860
+ } catch {}
4861
+ return "pnpm";
4862
+ }
4863
+ async function detectTemplate(outDir, override) {
4864
+ if (override) return override;
4865
+ try {
4866
+ const pattern = (await loadKickConfig(outDir))?.pattern;
4867
+ if (pattern && VALID_TEMPLATES.has(pattern)) return pattern;
4868
+ } catch {}
4869
+ return "ddd";
4870
+ }
4871
+ async function generateAgentDocs(options) {
4872
+ const only = options.only ?? "all";
4873
+ const name = detectName(options.outDir, options.name);
4874
+ const pm = detectPm(options.outDir, options.pm);
4875
+ const template = await detectTemplate(options.outDir, options.template);
4876
+ const wantsAgents = only === "agents" || only === "both" || only === "all";
4877
+ const wantsClaude = only === "claude" || only === "both" || only === "all";
4878
+ const wantsSkills = only === "skills" || only === "all";
4879
+ const targets = [];
4880
+ if (wantsAgents) targets.push({
4881
+ file: join(options.outDir, "AGENTS.md"),
4882
+ render: () => generateAgents(name, template, pm)
4883
+ });
4884
+ if (wantsClaude) targets.push({
4885
+ file: join(options.outDir, "CLAUDE.md"),
4886
+ render: () => generateClaude(name, template, pm)
4887
+ });
4888
+ if (wantsSkills) targets.push({
4889
+ file: join(options.outDir, "kickjs-skills.md"),
4890
+ render: () => generateKickJsSkills(name, template, pm)
4891
+ });
4892
+ const written = [];
4893
+ for (const { file, render } of targets) {
4894
+ if (existsSync(file) && !options.force) {
4895
+ if (!await confirm({
4896
+ message: `${file.replace(options.outDir + "/", "")} already exists. Overwrite?`,
4897
+ initialValue: false
4898
+ })) {
4899
+ console.log(` Skipped — existing ${file.replace(options.outDir + "/", "")} preserved.`);
4900
+ continue;
4901
+ }
4537
4902
  }
4903
+ await writeFileSafe(file, render());
4904
+ written.push(file);
4538
4905
  }
4539
- await writeFileSafe(filePath, `import { defineConfig } from '@forinda/kickjs-cli'
4540
-
4541
- export default defineConfig({
4542
- modules: {
4543
- dir: '${modulesDir}',
4544
- repo: '${defaultRepo}',
4545
- pluralize: true,
4546
- },
4547
-
4548
- typegen: {
4549
- schemaValidator: 'zod',
4550
- },
4551
-
4552
- commands: [
4553
- {
4554
- name: 'test',
4555
- description: 'Run tests with Vitest',
4556
- steps: 'npx vitest run',
4557
- },
4558
- {
4559
- name: 'format',
4560
- description: 'Format code with Prettier',
4561
- steps: 'npx prettier --write src/',
4562
- },
4563
- {
4564
- name: 'format:check',
4565
- description: 'Check formatting without writing',
4566
- steps: 'npx prettier --check src/',
4567
- },
4568
- {
4569
- name: 'ci:check',
4570
- description: 'Run typecheck + format check',
4571
- steps: ['npx tsc --noEmit', 'npx prettier --check src/'],
4572
- aliases: ['verify'],
4573
- },
4574
- ],
4575
- })
4576
- `);
4577
- return [filePath];
4906
+ return written;
4578
4907
  }
4579
4908
  //#endregion
4580
4909
  //#region src/generators/auth-scaffold.ts
@@ -5386,7 +5715,7 @@ export interface I${pascal}Repository {
5386
5715
  * \`@Inject(${pascal.toUpperCase()}_REPOSITORY)\` both return the typed
5387
5716
  * interface — no manual generic, no \`any\` cast.
5388
5717
  */
5389
- export const ${pascal.toUpperCase()}_REPOSITORY = createToken<I${pascal}Repository>('${pascal}/Repository')
5718
+ export const ${pascal.toUpperCase()}_REPOSITORY = createToken<I${pascal}Repository>('app/${kebab}/repository')
5390
5719
  `;
5391
5720
  }
5392
5721
  function genDomainService(pascal, kebab) {
@@ -5542,118 +5871,6 @@ describe('${pascal}', () => {
5542
5871
  return files;
5543
5872
  }
5544
5873
  //#endregion
5545
- //#region src/config.ts
5546
- const PACKAGE_MANAGERS = [
5547
- "pnpm",
5548
- "npm",
5549
- "yarn",
5550
- "bun"
5551
- ];
5552
- const BUILTIN_REPO_TYPES = [
5553
- "drizzle",
5554
- "inmemory",
5555
- "prisma"
5556
- ];
5557
- /** Resolve module config from `modules.*` block. */
5558
- function resolveModuleConfig(config) {
5559
- if (!config) return {};
5560
- const mc = {
5561
- dir: config.modules?.dir,
5562
- repo: config.modules?.repo,
5563
- schemaDir: config.modules?.schemaDir,
5564
- pluralize: config.modules?.pluralize,
5565
- prismaClientPath: config.modules?.prismaClientPath
5566
- };
5567
- if (mc.repo && typeof mc.repo === "string" && !BUILTIN_REPO_TYPES.includes(mc.repo)) console.warn(` Warning: modules.repo '${mc.repo}' is not a built-in type (${BUILTIN_REPO_TYPES.join(", ")}). It will generate a stub repository. Use { name: '${mc.repo}' } to silence this warning.`);
5568
- return mc;
5569
- }
5570
- const CONFIG_FILES = [
5571
- "kick.config.ts",
5572
- "kick.config.js",
5573
- "kick.config.mjs",
5574
- "kick.config.json"
5575
- ];
5576
- /** Load kick.config.* from the project root */
5577
- async function loadKickConfig(cwd) {
5578
- for (const filename of CONFIG_FILES) {
5579
- const filepath = join(cwd, filename);
5580
- try {
5581
- await access(filepath);
5582
- } catch {
5583
- continue;
5584
- }
5585
- if (filename.endsWith(".json")) {
5586
- const content = await readFile(filepath, "utf-8");
5587
- return JSON.parse(content);
5588
- }
5589
- try {
5590
- const { pathToFileURL } = await import("node:url");
5591
- const mod = await import(pathToFileURL(filepath).href);
5592
- const config = mod.default ?? mod;
5593
- const warnings = validateAssetMap(config, cwd);
5594
- for (const warning of warnings) console.warn(` Warning: ${warning}`);
5595
- return config;
5596
- } catch (err) {
5597
- 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.`);
5598
- continue;
5599
- }
5600
- }
5601
- return null;
5602
- }
5603
- /**
5604
- * Validate `assetMap` entries on a loaded config. Returns a list of
5605
- * human-readable warnings; the caller decides how to surface them
5606
- * (typically `console.warn`). Never throws — `kick g` and other
5607
- * unrelated commands should keep working even when the assetMap is
5608
- * misconfigured.
5609
- *
5610
- * Checks:
5611
- *
5612
- * - Each entry's `src` is a non-empty string.
5613
- * - The `src` directory exists on disk (otherwise the typegen + build
5614
- * steps will fail later with cryptic errors).
5615
- * - `dest` doesn't escape the project root (defensive — a `dest:
5616
- * '../../etc'` typo could write files outside the workspace).
5617
- * - The namespace key is a non-empty string and doesn't include a
5618
- * `/` (would conflict with the `<namespace>/<key>` manifest format).
5619
- */
5620
- function validateAssetMap(config, cwd) {
5621
- const warnings = [];
5622
- if (!config?.assetMap) return warnings;
5623
- const root = resolve(cwd);
5624
- for (const [namespace, entry] of Object.entries(config.assetMap)) {
5625
- if (!namespace || namespace.includes("/")) {
5626
- warnings.push(`assetMap key '${namespace}' is invalid — must be a non-empty string without '/'`);
5627
- continue;
5628
- }
5629
- if (typeof entry?.src !== "string" || entry.src.length === 0) {
5630
- warnings.push(`assetMap.${namespace} is missing a non-empty 'src' field`);
5631
- continue;
5632
- }
5633
- if (!existsSync(resolve(cwd, entry.src))) warnings.push(`assetMap.${namespace}.src ('${entry.src}') does not exist — typegen + build will fail`);
5634
- if (entry.dest) {
5635
- if (escapesRoot$1(resolve(cwd, entry.dest), root)) warnings.push(`assetMap.${namespace}.dest ('${entry.dest}') resolves outside the project root — refusing to copy`);
5636
- }
5637
- }
5638
- return warnings;
5639
- }
5640
- /**
5641
- * Returns true when `path` (absolute) resolves outside of `root`
5642
- * (also absolute). Uses `path.relative` for accuracy:
5643
- *
5644
- * - The result is empty when paths are identical (inside).
5645
- * - It starts with `..` when the path traverses outside the root.
5646
- * - It's absolute (Windows: cross-drive) when there's no relative
5647
- * path between them.
5648
- *
5649
- * Avoids the prefix-match pitfalls of `startsWith` (e.g. `/app`
5650
- * matching `/app2/...`, or case-mismatches on macOS / Windows).
5651
- */
5652
- function escapesRoot$1(path, root) {
5653
- const rel = relative(root, path);
5654
- return rel === "" ? false : rel.startsWith("..") || isAbsolute(rel);
5655
- }
5656
- //#endregion
5657
5874
  //#region src/typegen/scanner.ts
5658
5875
  /** Decorators that mark a class as DI-managed */
5659
5876
  const DECORATOR_NAMES = [
@@ -6831,8 +7048,12 @@ export {}
6831
7048
  const blocks = [];
6832
7049
  for (const item of [...byName.values()].sort((a, b) => a.name.localeCompare(b.name))) {
6833
7050
  const docLines = [];
6834
- if (item.description) docLines.push(` * ${item.description}`);
6835
- if (item.example) docLines.push(` * @example`, ` * \`\`\`ts`, ` * ${item.example}`, ` * \`\`\``);
7051
+ if (item.description) for (const line of item.description.split("\n")) docLines.push(` * ${line}`);
7052
+ if (item.example) {
7053
+ docLines.push(` * @example`, ` * \`\`\`ts`);
7054
+ for (const line of item.example.split("\n")) docLines.push(` * ${line}`);
7055
+ docLines.push(` * \`\`\``);
7056
+ }
6836
7057
  docLines.push(` * @see ${item.relativePath}`);
6837
7058
  blocks.push([
6838
7059
  "/**",
@@ -7118,6 +7339,13 @@ async function safeRun(opts, silent) {
7118
7339
  }
7119
7340
  //#endregion
7120
7341
  //#region src/commands/generate.ts
7342
+ const AGENT_DOCS_ONLY_VALUES = [
7343
+ "agents",
7344
+ "claude",
7345
+ "skills",
7346
+ "both",
7347
+ "all"
7348
+ ];
7121
7349
  /** Check if --dry-run was passed on the parent generate command */
7122
7350
  function isDryRun(cmd) {
7123
7351
  return (cmd.parent?.opts())?.dryRun ?? false;
@@ -7200,6 +7428,10 @@ const GENERATORS = [
7200
7428
  {
7201
7429
  name: "config",
7202
7430
  description: "Generate kick.config.ts"
7431
+ },
7432
+ {
7433
+ name: "agents",
7434
+ description: "Regenerate AGENTS.md + CLAUDE.md + kickjs-skills.md from upstream templates"
7203
7435
  }
7204
7436
  ];
7205
7437
  async function printGeneratorList() {
@@ -7467,6 +7699,24 @@ function registerGenerateCommand(program) {
7467
7699
  force: opts.force
7468
7700
  }), dryRun);
7469
7701
  });
7702
+ gen.command("agents").alias("agent-docs").alias("ai-docs").description("Regenerate AGENTS.md + CLAUDE.md + kickjs-skills.md (sync after framework upgrades)").option("--only <which>", "Limit scope: agents | claude | skills | both (agents+claude) | all (default: all)", "all").option("--name <name>", "Project name (defaults to package.json name)").option("--pm <pm>", "Package manager (defaults to package.json packageManager)").option("--template <template>", "Template: rest | graphql | ddd | cqrs | minimal").option("-f, --force", "Overwrite existing files without prompting").action(async (opts, cmd) => {
7703
+ const dryRun = isDryRun(cmd);
7704
+ setDryRun(dryRun);
7705
+ const only = opts.only ?? "all";
7706
+ if (!AGENT_DOCS_ONLY_VALUES.includes(only)) {
7707
+ console.error(` Invalid --only value: ${only}. Expected: ${AGENT_DOCS_ONLY_VALUES.join(" | ")}`);
7708
+ process.exitCode = 1;
7709
+ return;
7710
+ }
7711
+ printGenerated(await generateAgentDocs({
7712
+ outDir: resolve("."),
7713
+ only,
7714
+ name: opts.name,
7715
+ pm: opts.pm,
7716
+ template: opts.template,
7717
+ force: opts.force
7718
+ }), dryRun);
7719
+ });
7470
7720
  }
7471
7721
  //#endregion
7472
7722
  //#region src/utils/shell.ts
@@ -7673,11 +7923,15 @@ async function startDevServer(_entry, port) {
7673
7923
  configFile: resolve("vite.config.ts"),
7674
7924
  server: { port: port ? parseInt(port, 10) : void 0 }
7675
7925
  });
7926
+ const assetSrcRoots = devConfig?.assetMap ? Object.values(devConfig.assetMap).map((entry) => entry?.src).filter((src) => typeof src === "string" && src.length > 0).map((src) => resolve(cwd, src)) : [];
7927
+ const isAssetFile = (file) => assetSrcRoots.some((root) => file === root || file.startsWith(`${root}/`));
7676
7928
  let typegenTimer = null;
7677
7929
  const scheduleTypegen = (file) => {
7678
- if (!/\.(ts|tsx|mts|cts)$/.test(file)) return;
7679
7930
  if (file.includes(".kickjs")) return;
7680
7931
  if (file.endsWith(".d.ts")) return;
7932
+ const isTs = /\.(ts|tsx|mts|cts)$/.test(file);
7933
+ const isAsset = isAssetFile(file);
7934
+ if (!isTs && !isAsset) return;
7681
7935
  if (typegenTimer) clearTimeout(typegenTimer);
7682
7936
  typegenTimer = setTimeout(() => {
7683
7937
  runTypegen({
@@ -7695,6 +7949,7 @@ async function startDevServer(_entry, port) {
7695
7949
  server.watcher.on("add", scheduleTypegen);
7696
7950
  server.watcher.on("unlink", scheduleTypegen);
7697
7951
  server.watcher.on("change", scheduleTypegen);
7952
+ if (assetSrcRoots.length > 0) server.watcher.add(assetSrcRoots);
7698
7953
  await server.listen();
7699
7954
  server.printUrls();
7700
7955
  console.log(`\n KickJS dev server running (Vite + @forinda/kickjs-vite)\n`);