@forinda/kickjs-cli 2.2.5 → 2.3.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 v2.2.5
2
+ * @forinda/kickjs-cli v2.3.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -9,11 +9,11 @@
9
9
  * @license MIT
10
10
  */
11
11
  import { Command } from "commander";
12
- import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs";
12
+ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
13
13
  import { basename, dirname, join, relative, resolve, sep } from "node:path";
14
14
  import { fileURLToPath, pathToFileURL } from "node:url";
15
15
  import { createInterface } from "node:readline";
16
- import { execSync, fork } from "node:child_process";
16
+ import { execSync, fork, spawn } from "node:child_process";
17
17
  import { access, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
18
18
  import pkg from "pluralize";
19
19
  import { arch, platform, release } from "node:os";
@@ -3654,6 +3654,140 @@ export class ${pascal}Adapter implements AppAdapter {
3654
3654
  return files;
3655
3655
  }
3656
3656
  //#endregion
3657
+ //#region src/generators/plugin.ts
3658
+ /**
3659
+ * Scaffold a `KickPlugin` under `src/plugins/<name>.plugin.ts`.
3660
+ *
3661
+ * Plugins are the canonical place to wire DI bindings, load extra
3662
+ * modules, add middleware, or attach startup hooks without writing a
3663
+ * full adapter. The generated template implements every optional
3664
+ * `KickPlugin` hook with commented examples so users can uncomment
3665
+ * the ones they need and delete the rest.
3666
+ */
3667
+ async function generatePlugin(options) {
3668
+ const { name, outDir } = options;
3669
+ const kebab = toKebabCase(name);
3670
+ const pascal = toPascalCase(name);
3671
+ const factoryName = `${toCamelCase(name)}Plugin`;
3672
+ const files = [];
3673
+ const filePath = join(outDir, `${kebab}.plugin.ts`);
3674
+ await writeFileSafe(filePath, `import type { KickPlugin, Container, AppAdapter, AppModuleClass } from '@forinda/kickjs'
3675
+
3676
+ /**
3677
+ * Options for the ${pascal} plugin.
3678
+ *
3679
+ * Plugins typically take a small options object in their factory so
3680
+ * callers can configure them inline at bootstrap time. Keep the
3681
+ * shape narrow — anything derived from the environment should be
3682
+ * read via \`getEnv\` inside the plugin itself, not forced onto the
3683
+ * caller.
3684
+ */
3685
+ export interface ${pascal}PluginOptions {
3686
+ // Add your plugin options here, for example:
3687
+ // enabled?: boolean
3688
+ // apiKey?: string
3689
+ }
3690
+
3691
+ /**
3692
+ * ${pascal} plugin.
3693
+ *
3694
+ * A \`KickPlugin\` bundles DI bindings, modules, adapters, and
3695
+ * middleware into one object that can be added to \`bootstrap({ plugins })\`.
3696
+ * Every hook is optional — delete the ones you don't need and keep
3697
+ * only the surface your plugin actually uses.
3698
+ *
3699
+ * Lifecycle order:
3700
+ *
3701
+ * 1. \`register(container)\` — runs before user modules load. Use
3702
+ * it to bind services that modules depend on.
3703
+ * 2. \`modules()\` — plugin modules load before user modules.
3704
+ * 3. \`adapters()\` — plugin adapters are added before user adapters.
3705
+ * 4. \`middleware()\` — plugin middleware runs before user middleware.
3706
+ * 5. \`onReady(container)\` — runs after the app has fully bootstrapped.
3707
+ * 6. \`shutdown()\` — runs on graceful shutdown.
3708
+ *
3709
+ * @example
3710
+ * \`\`\`ts
3711
+ * import { bootstrap } from '@forinda/kickjs'
3712
+ * import { ${factoryName} } from './plugins/${kebab}.plugin'
3713
+ *
3714
+ * export const app = await bootstrap({
3715
+ * modules,
3716
+ * plugins: [${factoryName}({ /* options */ })],
3717
+ * })
3718
+ * \`\`\`
3719
+ */
3720
+ export function ${factoryName}(options: ${pascal}PluginOptions = {}): KickPlugin {
3721
+ return {
3722
+ name: '${kebab}',
3723
+
3724
+ /**
3725
+ * Register DI bindings before modules load.
3726
+ * Use \`container.registerInstance(TOKEN, value)\` for singletons
3727
+ * and \`container.registerFactory(TOKEN, () => ...)\` for lazy
3728
+ * constructions.
3729
+ */
3730
+ register(container: Container): void {
3731
+ // Example: bind a configured service to a DI token
3732
+ // container.registerInstance(MY_TOKEN, new MyService(options))
3733
+ },
3734
+
3735
+ /**
3736
+ * Return module classes this plugin contributes to the app.
3737
+ * These load before user modules, so plugin controllers and
3738
+ * services are available for user code to \`@Autowired\`.
3739
+ */
3740
+ modules(): AppModuleClass[] {
3741
+ return [
3742
+ // ExampleModule,
3743
+ ]
3744
+ },
3745
+
3746
+ /**
3747
+ * Return adapter instances to be added to the application.
3748
+ * Plugin adapters are added before user adapters.
3749
+ */
3750
+ adapters(): AppAdapter[] {
3751
+ return [
3752
+ // new MyAdapter({ ... }),
3753
+ ]
3754
+ },
3755
+
3756
+ /**
3757
+ * Return Express middleware entries to be added to the global
3758
+ * pipeline. Plugin middleware runs before user-defined middleware.
3759
+ */
3760
+ middleware(): any[] {
3761
+ return [
3762
+ // helmet(),
3763
+ // myCustomMiddleware(options),
3764
+ ]
3765
+ },
3766
+
3767
+ /**
3768
+ * Called after the application has fully bootstrapped. Use this
3769
+ * for post-startup work like logging, health checks, or warming
3770
+ * a cache. Runs once per process.
3771
+ */
3772
+ async onReady(container: Container): Promise<void> {
3773
+ // const logger = container.resolve(Logger)
3774
+ // logger.info('${pascal} plugin ready')
3775
+ },
3776
+
3777
+ /**
3778
+ * Called during graceful shutdown. Clean up any long-lived
3779
+ * resources this plugin owns (connections, timers, subscriptions).
3780
+ */
3781
+ async shutdown(): Promise<void> {
3782
+ // await this.connection?.close()
3783
+ },
3784
+ }
3785
+ }
3786
+ `);
3787
+ files.push(filePath);
3788
+ return files;
3789
+ }
3790
+ //#endregion
3657
3791
  //#region src/utils/resolve-out-dir.ts
3658
3792
  /**
3659
3793
  * DDD folder mapping — nested layered architecture.
@@ -5835,6 +5969,14 @@ function registerGenerateCommand(program) {
5835
5969
  outDir: resolve(opts.out)
5836
5970
  }), dryRun);
5837
5971
  });
5972
+ gen.command("plugin <name>").description("Generate a KickPlugin with DI, modules, adapters, middleware, and lifecycle hooks").option("-o, --out <dir>", "Output directory", "src/plugins").action(async (name, opts, cmd) => {
5973
+ const dryRun = isDryRun(cmd);
5974
+ setDryRun(dryRun);
5975
+ printGenerated(await generatePlugin({
5976
+ name,
5977
+ outDir: resolve(opts.out)
5978
+ }), dryRun);
5979
+ });
5838
5980
  gen.command("middleware <name>").description("Generate an Express middleware function\n Use -m to scope it to a module: kick g middleware auth -m users").option("-o, --out <dir>", "Output directory (overrides --module)").option("-m, --module <module>", "Place inside a module folder").action(async (name, opts, cmd) => {
5839
5981
  const dryRun = isDryRun(cmd);
5840
5982
  setDryRun(dryRun);
@@ -6440,6 +6582,11 @@ const PACKAGE_REGISTRY = {
6440
6582
  peers: [],
6441
6583
  description: "Multi-channel notifications — email, Slack, Discord, webhook"
6442
6584
  },
6585
+ mcp: {
6586
+ pkg: "@forinda/kickjs-mcp",
6587
+ peers: ["@modelcontextprotocol/sdk"],
6588
+ description: "Model Context Protocol server — expose @Controller endpoints as AI tools"
6589
+ },
6443
6590
  testing: {
6444
6591
  pkg: "@forinda/kickjs-testing",
6445
6592
  peers: [],
@@ -6523,6 +6670,616 @@ function registerAddCommand(program) {
6523
6670
  });
6524
6671
  }
6525
6672
  //#endregion
6673
+ //#region src/explain/known-issues.ts
6674
+ function includesAll(haystack, needles) {
6675
+ const lower = haystack.toLowerCase();
6676
+ return needles.every((n) => lower.includes(n.toLowerCase()));
6677
+ }
6678
+ function includesAny(haystack, needles) {
6679
+ const lower = haystack.toLowerCase();
6680
+ return needles.some((n) => lower.includes(n.toLowerCase()));
6681
+ }
6682
+ const KNOWN_ISSUES = [
6683
+ { match(input, _ctx) {
6684
+ const hasConfigGetUndefined = includesAll(input, ["config", "get"]) && includesAny(input, ["undefined", "null"]);
6685
+ const hasValueUndefined = input.includes("@Value") && includesAny(input, ["undefined", "is not defined"]);
6686
+ if (!hasConfigGetUndefined && !hasValueUndefined) return null;
6687
+ return {
6688
+ confidence: hasConfigGetUndefined && hasValueUndefined ? 90 : 75,
6689
+ diagnosis: {
6690
+ id: "env-schema-not-registered",
6691
+ title: "ConfigService.get() returns undefined for user-defined keys",
6692
+ explanation: "Your src/index.ts is missing `import \"./config\"`. That side-effect import\nregisters the env schema with kickjs at module-load time. Without it,\nConfigService falls back to the base schema (PORT/NODE_ENV/LOG_LEVEL only)\nand every user-defined key reads as undefined. @Value() may *appear* to\nwork via a raw process.env fallback, but Zod coercion and schema defaults\nare silently skipped.",
6693
+ fix: "Add this line to src/index.ts near the top, before bootstrap() runs:",
6694
+ codeBefore: "import 'reflect-metadata'\nimport { bootstrap } from '@forinda/kickjs'\nimport { modules } from './modules'\n",
6695
+ codeAfter: "import 'reflect-metadata'\nimport './config' // ← add this — registers env schema\nimport { bootstrap } from '@forinda/kickjs'\nimport { modules } from './modules'\n",
6696
+ docs: "https://forinda.github.io/kick-js/guide/configuration.html#wiring-the-schema-at-startup"
6697
+ }
6698
+ };
6699
+ } },
6700
+ { match(input, _ctx) {
6701
+ const hasTestContext = includesAny(input, [
6702
+ "vitest",
6703
+ "test",
6704
+ "spec",
6705
+ "__tests__",
6706
+ ".test."
6707
+ ]);
6708
+ if (!includesAny(input, [
6709
+ "already registered",
6710
+ "already exists",
6711
+ "duplicate",
6712
+ "has been registered"
6713
+ ])) return null;
6714
+ return {
6715
+ confidence: hasTestContext ? 85 : 60,
6716
+ diagnosis: {
6717
+ id: "container-not-reset-in-tests",
6718
+ title: "DI container leaks between test cases",
6719
+ explanation: "KickJS decorators register classes on the global Container at import time.\nWhen vitest re-imports your modules across tests, the same class can be\nregistered twice and the container throws. The fix is to wipe the\ncontainer between tests so each case starts fresh.",
6720
+ fix: "Add Container.reset() to a beforeEach hook in the failing test file:",
6721
+ codeAfter: "import { describe, it, beforeEach } from 'vitest'\nimport { Container } from '@forinda/kickjs'\n\ndescribe('UserController', () => {\n beforeEach(() => Container.reset())\n\n it('does the thing', async () => { /* ... */ })\n})",
6722
+ docs: "https://forinda.github.io/kick-js/guide/testing.html"
6723
+ }
6724
+ };
6725
+ } },
6726
+ { match(input, _ctx) {
6727
+ if (!(input.includes("@Module") || includesAll(input, ["Module", "is not a function"]) || includesAll(input, ["Module", "no exported member"]))) return null;
6728
+ return {
6729
+ confidence: 80,
6730
+ diagnosis: {
6731
+ id: "module-decorator-not-found",
6732
+ title: "KickJS does not have a @Module decorator (different pattern from NestJS)",
6733
+ explanation: "NestJS uses @Module({ controllers, providers }). KickJS uses an interface\npattern instead: a class implements AppModule and exposes routes() that\nreturns the controller wiring. This was a deliberate choice — modules\nbecome explicit values rather than metadata, which makes them easier to\ncompose, test, and serialize.",
6734
+ fix: "Replace the @Module decorator with an AppModule class:",
6735
+ codeBefore: "import { Module } from '@forinda/kickjs' // ← does not exist\nimport { UserController } from './user.controller'\n\n@Module({\n controllers: [UserController],\n})\nexport class UserModule {}",
6736
+ codeAfter: "import { type AppModule, type ModuleRoutes, buildRoutes } from '@forinda/kickjs'\nimport { UserController } from './user.controller'\n\nexport class UserModule implements AppModule {\n routes(): ModuleRoutes {\n return {\n path: '/users',\n router: buildRoutes(UserController),\n controller: UserController,\n }\n }\n}",
6737
+ docs: "https://forinda.github.io/kick-js/guide/project-structure.html"
6738
+ }
6739
+ };
6740
+ } },
6741
+ { match(input, _ctx) {
6742
+ if (!/KickRoutes\s*\[\s*['"](GET|POST|PUT|PATCH|DELETE)/i.test(input)) return null;
6743
+ return {
6744
+ confidence: 95,
6745
+ diagnosis: {
6746
+ id: "legacy-kick-routes-bracket-syntax",
6747
+ title: "KickRoutes['POST /users'] is the legacy v1 syntax",
6748
+ explanation: "KickJS v2 changed the typegen output from a flat string-keyed map to a\nnamespaced shape: KickRoutes.UserController[\"create\"] instead of\nKickRoutes[\"POST /users\"]. The new form is per-controller, per-method,\nand matches the actual class names so refactors propagate via\nrename-symbol instead of grep.",
6749
+ fix: "Update the Ctx<...> type parameter to use the namespace form:",
6750
+ codeBefore: "@Post('/', { body: createUserSchema })\ncreate(ctx: Ctx<KickRoutes['POST /users']>) { /* ... */ }",
6751
+ codeAfter: "@Post('/', { body: createUserSchema, name: 'CreateUser' })\ncreate(ctx: Ctx<KickRoutes.UserController['create']>) { /* ... */ }",
6752
+ docs: "https://forinda.github.io/kick-js/guide/typegen.html"
6753
+ }
6754
+ };
6755
+ } },
6756
+ { match(input, _ctx) {
6757
+ const hasCluster = includesAny(input, [
6758
+ "cluster",
6759
+ "workers",
6760
+ "two ports",
6761
+ "duplicate server"
6762
+ ]);
6763
+ const hasDevSignal = includesAny(input, [
6764
+ "kick dev",
6765
+ "vite",
6766
+ "eaddrinuse",
6767
+ "5173",
6768
+ "5174",
6769
+ "two servers"
6770
+ ]);
6771
+ if (!hasCluster || !hasDevSignal) return null;
6772
+ return {
6773
+ confidence: 85,
6774
+ diagnosis: {
6775
+ id: "cluster-in-vite-dev",
6776
+ title: "Cluster mode is incompatible with `kick dev` (Vite owns the server)",
6777
+ explanation: "In dev mode, Vite owns the HTTP server. If your bootstrap passes\ncluster: { workers: N }, the framework forks N workers, each of which\nspins up its own Vite instance on a separate port. The fix landed in\nv2.2.5: McpAdapter (and bootstrap()) now detects Vite dev mode and\nsilently skips cluster, with a warning. If you see this on an older\nversion, upgrade or guard the cluster option behind NODE_ENV.",
6778
+ fix: "Either upgrade to v2.2.5+ or gate cluster mode on production:",
6779
+ codeAfter: "export const app = await bootstrap({\n modules,\n cluster: process.env.NODE_ENV === 'production' ? { workers: 4 } : false,\n})",
6780
+ docs: "https://forinda.github.io/kick-js/guide/cluster.html"
6781
+ }
6782
+ };
6783
+ } },
6784
+ { match(input, _ctx) {
6785
+ if (!includesAny(input, [
6786
+ "reflect-metadata",
6787
+ "Reflect.getMetadata is not a function",
6788
+ "Reflect.defineMetadata",
6789
+ "design:type",
6790
+ "design:paramtypes"
6791
+ ])) return null;
6792
+ return {
6793
+ confidence: 90,
6794
+ diagnosis: {
6795
+ id: "reflect-metadata-missing",
6796
+ title: "reflect-metadata is not loaded — DI cannot read decorator types",
6797
+ explanation: "The DI container reads constructor parameter types via the\nreflect-metadata polyfill. The polyfill must be imported once,\nbefore any decorator runs. Most projects do this at the top of\nsrc/index.ts; missing the import causes obscure \"design:paramtypes\"\nor \"Reflect.getMetadata is not a function\" errors at runtime.",
6798
+ fix: "Add the import at the very top of src/index.ts:",
6799
+ codeAfter: "import 'reflect-metadata' // ← must be the FIRST import\nimport './config'\nimport { bootstrap } from '@forinda/kickjs'\nimport { modules } from './modules'\n\nexport const app = await bootstrap({ modules })",
6800
+ docs: "https://forinda.github.io/kick-js/guide/dependency-injection.html"
6801
+ }
6802
+ };
6803
+ } },
6804
+ { match(input, _ctx) {
6805
+ if (!includesAny(input, [
6806
+ "404",
6807
+ "cannot get",
6808
+ "cannot post",
6809
+ "no route"
6810
+ ])) return null;
6811
+ return {
6812
+ confidence: 50,
6813
+ diagnosis: {
6814
+ id: "module-not-registered",
6815
+ title: "A 404 may indicate a module is not in the modules array",
6816
+ explanation: "KickJS only mounts modules listed in `src/modules/index.ts`. If you\ngenerated a module via `kick g module foo` but the routes don't appear,\nthe most likely cause is that the module is missing from the exported\narray. The CLI usually wires this automatically, but a hand-edit can\ndrop the entry.",
6817
+ fix: "Open src/modules/index.ts and verify the module is in the array:",
6818
+ codeAfter: "import type { AppModuleClass } from '@forinda/kickjs'\nimport { UserModule } from './users/user.module'\nimport { TaskModule } from './tasks/task.module' // ← was this missing?\n\nexport const modules: AppModuleClass[] = [UserModule, TaskModule]",
6819
+ docs: "https://forinda.github.io/kick-js/guide/project-structure.html"
6820
+ }
6821
+ };
6822
+ } }
6823
+ ];
6824
+ /**
6825
+ * Run every matcher against the input and return the highest-confidence
6826
+ * hit, or `null` if no matcher cleared the 40-confidence threshold.
6827
+ */
6828
+ function findBestMatch(input, ctx) {
6829
+ let best = null;
6830
+ for (const issue of KNOWN_ISSUES) {
6831
+ let match = null;
6832
+ try {
6833
+ match = issue.match(input, ctx);
6834
+ } catch {
6835
+ continue;
6836
+ }
6837
+ if (!match || match.confidence < 40) continue;
6838
+ if (!best || match.confidence > best.confidence) best = match;
6839
+ }
6840
+ return best;
6841
+ }
6842
+ //#endregion
6843
+ //#region src/explain/ai-fallback.ts
6844
+ /**
6845
+ * Ask the configured LLM for a diagnosis of `options.input`.
6846
+ *
6847
+ * Returns a discriminated result; callers should never assume the
6848
+ * LLM was reachable or produced valid output. The function catches
6849
+ * every expected failure mode and maps it to a friendly `unavailable`
6850
+ * or `error` result — the CLI can then decide how to present it.
6851
+ */
6852
+ async function askAi(options) {
6853
+ const provider = options.provider ?? "openai";
6854
+ const apiKey = process.env.OPENAI_API_KEY;
6855
+ if (provider === "openai" && !apiKey) return {
6856
+ kind: "unavailable",
6857
+ reason: "OPENAI_API_KEY environment variable is not set",
6858
+ suggestion: "Set OPENAI_API_KEY in your shell, e.g.\n export OPENAI_API_KEY=\"sk-...\"\n\nThen re-run `kick explain --ai \"<your error>\"`."
6859
+ };
6860
+ let aiModule;
6861
+ try {
6862
+ aiModule = await import("@forinda/kickjs-ai");
6863
+ } catch {
6864
+ return {
6865
+ kind: "unavailable",
6866
+ reason: "@forinda/kickjs-ai is not installed",
6867
+ suggestion: "Install the AI package to enable the LLM fallback:\n kick add ai\n\nOr manually:\n pnpm add @forinda/kickjs-ai"
6868
+ };
6869
+ }
6870
+ const { OpenAIProvider } = aiModule;
6871
+ const instance = new OpenAIProvider({
6872
+ apiKey,
6873
+ defaultChatModel: options.model ?? "gpt-4o-mini"
6874
+ });
6875
+ const systemPrompt = buildSystemPrompt(options.cwd);
6876
+ const userPrompt = `Error or stack trace:\n\n${options.input.trim()}`;
6877
+ try {
6878
+ const diagnosis = parseDiagnosisFromResponse((await instance.chat({ messages: [{
6879
+ role: "system",
6880
+ content: systemPrompt
6881
+ }, {
6882
+ role: "user",
6883
+ content: userPrompt
6884
+ }] })).content);
6885
+ if (!diagnosis) return {
6886
+ kind: "error",
6887
+ message: "The LLM responded but the payload was not valid JSON in the expected shape. Try again, or file an issue with the error text."
6888
+ };
6889
+ return {
6890
+ kind: "ok",
6891
+ diagnosis
6892
+ };
6893
+ } catch (err) {
6894
+ return {
6895
+ kind: "error",
6896
+ message: `LLM request failed: ${err instanceof Error ? err.message : String(err)}`
6897
+ };
6898
+ }
6899
+ }
6900
+ /**
6901
+ * Build the system prompt that tells the LLM what KickJS is and how
6902
+ * to structure its response. The prompt is deliberately prescriptive:
6903
+ * the caller needs a JSON payload it can render via the same formatter
6904
+ * the known-issues path uses, so freeform text doesn't work.
6905
+ *
6906
+ * Keep this prompt short — every token counts at inference time and
6907
+ * the CLI is often called interactively.
6908
+ */
6909
+ function buildSystemPrompt(cwd) {
6910
+ return [
6911
+ "You are a diagnostic assistant for KickJS, a decorator-driven Node.js",
6912
+ "framework built on Express 5 and TypeScript. KickJS projects use:",
6913
+ " - @Controller, @Get, @Post, @Autowired, @Service, @Value decorators",
6914
+ " - An AppModule interface with a routes() method (NOT a @Module decorator)",
6915
+ " - Zod schemas as both runtime validators and OpenAPI sources",
6916
+ " - Ctx<KickRoutes.ControllerName['method']> for typed request context",
6917
+ " - src/config/index.ts with defineEnv/loadEnv for env schema",
6918
+ " - A side-effect `import \"./config\"` in src/index.ts to register the schema",
6919
+ " - Container.reset() in beforeEach for DI test isolation",
6920
+ "",
6921
+ "When the user gives you an error message or stack trace, produce a",
6922
+ "structured diagnosis that helps them fix the bug. You MUST respond",
6923
+ "with a single JSON object (no surrounding prose, no markdown fences)",
6924
+ "matching this shape:",
6925
+ "",
6926
+ "{",
6927
+ " \"id\": \"<kebab-case-identifier>\",",
6928
+ " \"title\": \"<one-line problem summary>\",",
6929
+ " \"explanation\": \"<multi-line explanation of what is wrong>\",",
6930
+ " \"fix\": \"<multi-line instructions for fixing the problem>\",",
6931
+ " \"codeBefore\": \"<optional: broken code snippet>\",",
6932
+ " \"codeAfter\": \"<optional: corrected code snippet>\",",
6933
+ " \"docs\": \"<optional: KickJS doc URL that discusses this topic>\"",
6934
+ "}",
6935
+ "",
6936
+ "The KickJS docs live at https://forinda.github.io/kick-js/ — prefer",
6937
+ "that domain for any doc links you suggest.",
6938
+ cwd ? `The project is located at ${cwd}.` : ""
6939
+ ].filter((line) => line.length > 0).join("\n");
6940
+ }
6941
+ /**
6942
+ * Extract a `Diagnosis` object from the LLM response content.
6943
+ *
6944
+ * Tries three strategies in order:
6945
+ * 1. Parse the whole content as JSON directly
6946
+ * 2. Strip a surrounding markdown fence (```json ... ```)
6947
+ * 3. Find the first balanced `{ ... }` block and parse that
6948
+ *
6949
+ * Returns null if none of the strategies produce a valid object with
6950
+ * at least the required fields (id, title, explanation, fix).
6951
+ */
6952
+ function parseDiagnosisFromResponse(content) {
6953
+ const attempts = [
6954
+ content,
6955
+ stripMarkdownFence(content),
6956
+ extractFirstJsonObject(content)
6957
+ ].filter((s) => s !== null);
6958
+ for (const attempt of attempts) try {
6959
+ const parsed = JSON.parse(attempt);
6960
+ if (isValidDiagnosis(parsed)) return parsed;
6961
+ } catch {
6962
+ continue;
6963
+ }
6964
+ return null;
6965
+ }
6966
+ function stripMarkdownFence(text) {
6967
+ const match = text.match(/```(?:json)?\s*\n([\s\S]*?)```/);
6968
+ return match ? match[1]?.trim() ?? null : null;
6969
+ }
6970
+ function extractFirstJsonObject(text) {
6971
+ const start = text.indexOf("{");
6972
+ if (start === -1) return null;
6973
+ let depth = 0;
6974
+ let inString = false;
6975
+ let escape = false;
6976
+ for (let i = start; i < text.length; i++) {
6977
+ const ch = text[i];
6978
+ if (escape) {
6979
+ escape = false;
6980
+ continue;
6981
+ }
6982
+ if (ch === "\\" && inString) {
6983
+ escape = true;
6984
+ continue;
6985
+ }
6986
+ if (ch === "\"") {
6987
+ inString = !inString;
6988
+ continue;
6989
+ }
6990
+ if (inString) continue;
6991
+ if (ch === "{") depth++;
6992
+ if (ch === "}") {
6993
+ depth--;
6994
+ if (depth === 0) return text.slice(start, i + 1);
6995
+ }
6996
+ }
6997
+ return null;
6998
+ }
6999
+ function isValidDiagnosis(value) {
7000
+ if (value === null || typeof value !== "object") return false;
7001
+ const v = value;
7002
+ return typeof v.id === "string" && typeof v.title === "string" && typeof v.explanation === "string" && typeof v.fix === "string";
7003
+ }
7004
+ //#endregion
7005
+ //#region src/commands/explain.ts
7006
+ /**
7007
+ * `kick explain` — explain a KickJS error and suggest a fix.
7008
+ *
7009
+ * The command takes an error message (positional arg, --message flag,
7010
+ * or stdin), runs it through a registry of known KickJS pitfalls, and
7011
+ * prints the highest-confidence diagnosis with a code fix and a doc
7012
+ * link. If no matcher hits, it prints a "no match" message — the
7013
+ * --ai flag (planned) will fall back to an LLM call against the
7014
+ * registered AiProvider.
7015
+ *
7016
+ * The known-issues registry lives in src/explain/known-issues.ts and
7017
+ * is the single source of truth for KickJS-specific advice. Adding a
7018
+ * new entry takes ~30 lines and gives every user a permanent fix path.
7019
+ *
7020
+ * @example
7021
+ * ```bash
7022
+ * # As a positional arg
7023
+ * kick explain "config.get('DATABASE_URL') returned undefined"
7024
+ *
7025
+ * # Via stdin (pipe a stack trace)
7026
+ * pnpm test 2>&1 | kick explain
7027
+ *
7028
+ * # Via --message flag
7029
+ * kick explain --message "Reflect.getMetadata is not a function"
7030
+ * ```
7031
+ */
7032
+ function registerExplainCommand(program) {
7033
+ program.command("explain [message]").description("Explain a KickJS error and suggest a fix").option("-m, --message <text>", "Error message to explain (alternative to positional arg)").option("--ai", "Fall back to LLM if no known-issue matches (requires @forinda/kickjs-ai)").option("--model <name>", "Model name for the --ai fallback", "gpt-4o-mini").option("--json", "Output the diagnosis as JSON for tooling integration").action(async (positional, opts) => {
7034
+ const input = await resolveInput(positional, opts.message);
7035
+ if (!input || input.trim().length === 0) {
7036
+ process.stderr.write("Error: no input provided.\n\nPass a message as a positional arg, --message flag, or pipe via stdin:\n kick explain \"config.get returned undefined\"\n pnpm test 2>&1 | kick explain\n");
7037
+ process.exit(1);
7038
+ }
7039
+ const ctx = buildExplainContext();
7040
+ const match = findBestMatch(input, ctx);
7041
+ if (opts.json && match) {
7042
+ process.stdout.write(JSON.stringify({
7043
+ matched: true,
7044
+ ...match
7045
+ }, null, 2) + "\n");
7046
+ return;
7047
+ }
7048
+ if (match) {
7049
+ printDiagnosis(input, match.diagnosis, match.confidence);
7050
+ return;
7051
+ }
7052
+ if (!opts.ai) {
7053
+ if (opts.json) {
7054
+ process.stdout.write(JSON.stringify({ matched: false }, null, 2) + "\n");
7055
+ process.exit(2);
7056
+ }
7057
+ printNoMatch(input, false);
7058
+ process.exit(2);
7059
+ }
7060
+ const result = await askAi({
7061
+ input,
7062
+ model: opts.model,
7063
+ cwd: ctx.cwd
7064
+ });
7065
+ if (opts.json) {
7066
+ process.stdout.write(JSON.stringify(aiResultToJson(result), null, 2) + "\n");
7067
+ process.exit(result.kind === "ok" ? 0 : 2);
7068
+ }
7069
+ printAiResult(input, result);
7070
+ process.exit(result.kind === "ok" ? 0 : 2);
7071
+ });
7072
+ }
7073
+ /** Serialize an AskAiResult for `--json` output. */
7074
+ function aiResultToJson(result) {
7075
+ if (result.kind === "ok") return {
7076
+ matched: true,
7077
+ source: "ai",
7078
+ diagnosis: result.diagnosis
7079
+ };
7080
+ if (result.kind === "unavailable") return {
7081
+ matched: false,
7082
+ aiUnavailable: true,
7083
+ reason: result.reason
7084
+ };
7085
+ return {
7086
+ matched: false,
7087
+ aiError: true,
7088
+ error: result.message
7089
+ };
7090
+ }
7091
+ /** Render an AskAiResult to stdout using the same formatting as local matches. */
7092
+ function printAiResult(input, result) {
7093
+ if (result.kind === "ok") {
7094
+ printDiagnosis(input, result.diagnosis, -1, true);
7095
+ return;
7096
+ }
7097
+ if (result.kind === "unavailable") {
7098
+ process.stdout.write(`\n Explaining: ${truncate(input.trim(), 200)}\n\n`);
7099
+ process.stdout.write(` AI fallback unavailable: ${result.reason}\n\n`);
7100
+ process.stdout.write(`${indent(result.suggestion, " ")}\n\n`);
7101
+ return;
7102
+ }
7103
+ process.stdout.write(`\n Explaining: ${truncate(input.trim(), 200)}\n\n`);
7104
+ process.stdout.write(` AI fallback error: ${result.message}\n\n`);
7105
+ }
7106
+ /**
7107
+ * Resolve the error text from positional arg, --message flag, or stdin.
7108
+ *
7109
+ * Precedence: positional > flag > stdin. We only read stdin if neither
7110
+ * of the first two were provided AND stdin is not a TTY (i.e. something
7111
+ * is being piped in). Reading from a real TTY would hang waiting for
7112
+ * the user to type, which is never what they want.
7113
+ */
7114
+ async function resolveInput(positional, flag) {
7115
+ if (positional && positional.trim().length > 0) return positional;
7116
+ if (flag && flag.trim().length > 0) return flag;
7117
+ if (process.stdin.isTTY) return "";
7118
+ return readStdinAll();
7119
+ }
7120
+ function readStdinAll() {
7121
+ return new Promise((resolve, reject) => {
7122
+ let buffer = "";
7123
+ process.stdin.setEncoding("utf8");
7124
+ process.stdin.on("data", (chunk) => {
7125
+ buffer += chunk;
7126
+ });
7127
+ process.stdin.on("end", () => resolve(buffer));
7128
+ process.stdin.on("error", reject);
7129
+ });
7130
+ }
7131
+ /**
7132
+ * Build a small context object the matchers can use to check project
7133
+ * state — e.g. "does this project have a src/config/index.ts?".
7134
+ *
7135
+ * Kept intentionally minimal to avoid pulling the full kick.config
7136
+ * loader into a fast-path command. Matchers should treat this as
7137
+ * best-effort and degrade gracefully when ctx is undefined.
7138
+ */
7139
+ function buildExplainContext() {
7140
+ const cwd = process.cwd();
7141
+ return {
7142
+ cwd,
7143
+ hasFile: (path) => existsSync(resolve(cwd, path))
7144
+ };
7145
+ }
7146
+ function printDiagnosis(input, d, confidence, aiLabel = false) {
7147
+ const inputSnippet = truncate(input.trim(), 200);
7148
+ const label = aiLabel ? "AI-generated — verify before applying" : labelConfidence(confidence);
7149
+ process.stdout.write(`\n Explaining: ${inputSnippet}\n`);
7150
+ process.stdout.write(`\n Match: ${d.id} (${label})\n`);
7151
+ process.stdout.write(` Title: ${d.title}\n`);
7152
+ process.stdout.write(`\n Diagnosis:\n${indent(d.explanation, " ")}\n`);
7153
+ process.stdout.write(`\n Fix:\n${indent(d.fix, " ")}\n`);
7154
+ if (d.codeBefore) process.stdout.write(`\n Before:\n${indent(d.codeBefore, " ")}\n`);
7155
+ if (d.codeAfter) process.stdout.write(`\n After:\n${indent(d.codeAfter, " ")}\n`);
7156
+ if (d.docs) process.stdout.write(`\n Docs: ${d.docs}\n`);
7157
+ process.stdout.write("\n");
7158
+ }
7159
+ function printNoMatch(input, aiRequested) {
7160
+ const snippet = truncate(input.trim(), 200);
7161
+ process.stdout.write(`\n Explaining: ${snippet}\n\n`);
7162
+ if (aiRequested) process.stdout.write(" No known-issue matched, and --ai fallback is not yet wired.\n When @forinda/kickjs-ai ships its provider implementations,\n this command will call the configured LLM with the error +\n project context and return a structured fix.\n\n");
7163
+ else process.stdout.write(" No known-issue matched. Things you can try:\n\n 1. Check the framework docs for the error keywords:\n https://forinda.github.io/kick-js/\n\n 2. Re-run with --ai to fall back to an LLM (requires\n @forinda/kickjs-ai with a configured provider):\n kick explain --ai \"<your error>\"\n\n 3. File an issue with the error text:\n https://github.com/forinda/kick-js/issues/new\n\n");
7164
+ }
7165
+ function indent(text, prefix) {
7166
+ return text.split("\n").map((line) => `${prefix}${line}`).join("\n");
7167
+ }
7168
+ function truncate(text, max) {
7169
+ if (text.length <= max) return text;
7170
+ return text.slice(0, max - 1) + "…";
7171
+ }
7172
+ function labelConfidence(score) {
7173
+ if (score >= 90) return "high confidence";
7174
+ if (score >= 70) return "good match";
7175
+ if (score >= 50) return "medium confidence";
7176
+ return "low confidence — verify manually";
7177
+ }
7178
+ //#endregion
7179
+ //#region src/commands/mcp.ts
7180
+ /**
7181
+ * `kick mcp` — Model Context Protocol commands.
7182
+ *
7183
+ * Two subcommands:
7184
+ * - `kick mcp` (default → `start`): runs the built application as an
7185
+ * MCP server over stdio. The user's app must already wire `McpAdapter`
7186
+ * from `@forinda/kickjs-mcp` into its bootstrap. The CLI just spawns
7187
+ * the built entry as a subprocess with `KICK_MCP_STDIO=1`, which the
7188
+ * adapter detects and uses to switch its transport from
7189
+ * StreamableHTTP to stdio. The subprocess inherits stdin/stdout/stderr
7190
+ * so the MCP wire protocol flows directly between the parent process
7191
+ * (the MCP client — Claude Code, Cursor, etc.) and the child app.
7192
+ * - `kick mcp init`: generates a `.mcp.json` config file pointing at
7193
+ * this project, ready to drop into a Claude Code / Cursor workspace.
7194
+ *
7195
+ * Logs MUST go to stderr in stdio mode — anything written to stdout
7196
+ * corrupts the JSON-RPC protocol stream. Pino's default stream is
7197
+ * stderr already, so this works out of the box for KickJS apps using
7198
+ * the framework's bundled logger.
7199
+ */
7200
+ function registerMcpCommand(program) {
7201
+ const mcp = program.command("mcp").description("Model Context Protocol commands (start | init)");
7202
+ mcp.command("start", { isDefault: true }).description("Run the built application as an MCP server over stdio").option("-e, --entry <file>", "Entry file", "dist/index.js").option("--node-arg <arg...>", "Extra arguments to pass to node").action(runMcpServer);
7203
+ mcp.command("init").description("Generate .mcp.json for Claude Code / Cursor / Zed").option("-n, --name <name>", "Server name (defaults to package.json name)").option("-o, --out <file>", "Output file", ".mcp.json").option("-f, --force", "Overwrite an existing entry without prompting").option("--global", "Write to ~/.mcp.json instead of the project root").action(initMcpConfig);
7204
+ }
7205
+ function runMcpServer(opts) {
7206
+ const entry = resolve(opts.entry);
7207
+ if (!existsSync(entry)) {
7208
+ process.stderr.write(`Error: entry file not found: ${entry}\n\nBuild the app first with \`kick build\`, or pass a custom entry:\n kick mcp -e dist/server.js\n`);
7209
+ process.exit(1);
7210
+ }
7211
+ const nodeArgs = [...opts.nodeArg ?? [], entry];
7212
+ const child = spawn(process.execPath, nodeArgs, {
7213
+ stdio: "inherit",
7214
+ env: {
7215
+ ...process.env,
7216
+ KICK_MCP_STDIO: "1",
7217
+ NODE_ENV: process.env.NODE_ENV ?? "production"
7218
+ }
7219
+ });
7220
+ child.on("error", (err) => {
7221
+ process.stderr.write(`Failed to start MCP server: ${err.message}\n`);
7222
+ process.exit(1);
7223
+ });
7224
+ child.on("exit", (code, signal) => {
7225
+ if (signal) {
7226
+ process.kill(process.pid, signal);
7227
+ return;
7228
+ }
7229
+ process.exit(code ?? 0);
7230
+ });
7231
+ const forward = (signal) => {
7232
+ if (!child.killed) child.kill(signal);
7233
+ };
7234
+ process.on("SIGINT", () => forward("SIGINT"));
7235
+ process.on("SIGTERM", () => forward("SIGTERM"));
7236
+ }
7237
+ function initMcpConfig(opts) {
7238
+ const cwd = process.cwd();
7239
+ const projectName = readPackageName(cwd) ?? basename(cwd);
7240
+ const serverName = opts.name ?? projectName;
7241
+ const outPath = opts.global ? resolve(process.env.HOME ?? ".", ".mcp.json") : resolve(cwd, opts.out);
7242
+ const entry = {
7243
+ command: "kick",
7244
+ args: ["mcp"],
7245
+ cwd
7246
+ };
7247
+ let config = { mcpServers: {} };
7248
+ if (existsSync(outPath)) try {
7249
+ const raw = readFileSync(outPath, "utf8");
7250
+ const parsed = JSON.parse(raw);
7251
+ if (parsed && typeof parsed === "object" && parsed.mcpServers) config = { mcpServers: { ...parsed.mcpServers } };
7252
+ } catch (err) {
7253
+ const message = err instanceof Error ? err.message : String(err);
7254
+ process.stderr.write(`Error: existing ${outPath} is not valid JSON (${message}).\nFix the file or pass --force to overwrite the entry.\n`);
7255
+ process.exit(1);
7256
+ }
7257
+ if (config.mcpServers[serverName] && !opts.force) {
7258
+ process.stderr.write(`Error: an entry for "${serverName}" already exists in ${outPath}.\nPass --force to overwrite it, or use --name to pick a different key.\n`);
7259
+ process.exit(1);
7260
+ }
7261
+ config.mcpServers[serverName] = entry;
7262
+ writeFileSync(outPath, JSON.stringify(config, null, 2) + "\n", "utf8");
7263
+ process.stdout.write(`\n ✓ Wrote MCP server entry "${serverName}" to ${outPath}\n\n To activate it:\n 1. Build your app: kick build\n 2. Restart your MCP client (Claude Code, Cursor, Zed)\n 3. The server should appear in the client's tool picker\n\n`);
7264
+ }
7265
+ /**
7266
+ * Read the `name` field from the project's `package.json`. Returns
7267
+ * null if the file is missing or unparseable — callers fall back to
7268
+ * the directory name in that case.
7269
+ */
7270
+ function readPackageName(cwd) {
7271
+ const pkgPath = resolve(cwd, "package.json");
7272
+ if (!existsSync(pkgPath)) return null;
7273
+ try {
7274
+ const raw = readFileSync(pkgPath, "utf8");
7275
+ const parsed = JSON.parse(raw);
7276
+ if (typeof parsed.name === "string") return parsed.name;
7277
+ return null;
7278
+ } catch {
7279
+ return null;
7280
+ }
7281
+ }
7282
+ //#endregion
6526
7283
  //#region src/commands/tinker.ts
6527
7284
  function registerTinkerCommand(program) {
6528
7285
  program.command("tinker").description("Interactive REPL with DI container and services loaded").option("-e, --entry <file>", "Entry file to load", "src/index.ts").action(async (opts) => {
@@ -6773,6 +7530,8 @@ async function main() {
6773
7530
  registerInspectCommand(program);
6774
7531
  registerAddCommand(program);
6775
7532
  registerListCommand(program);
7533
+ registerExplainCommand(program);
7534
+ registerMcpCommand(program);
6776
7535
  registerTinkerCommand(program);
6777
7536
  registerRemoveCommand(program);
6778
7537
  registerTypegenCommand(program);
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs-cli v2.2.5
2
+ * @forinda/kickjs-cli v2.3.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -3706,7 +3706,7 @@ async function initProject(options) {
3706
3706
  }
3707
3707
  }
3708
3708
  try {
3709
- const { runTypegen } = await import("./typegen-CTXqSva4.mjs");
3709
+ const { runTypegen } = await import("./typegen-CsTiIxmu.mjs");
3710
3710
  await runTypegen({
3711
3711
  cwd: dir,
3712
3712
  allowDuplicates: true,
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs-cli v2.2.5
2
+ * @forinda/kickjs-cli v2.3.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -901,4 +901,4 @@ async function runTypegen(opts = {}) {
901
901
  //#endregion
902
902
  export { runTypegen };
903
903
 
904
- //# sourceMappingURL=typegen-CTXqSva4.mjs.map
904
+ //# sourceMappingURL=typegen-CsTiIxmu.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"typegen-CTXqSva4.mjs","names":[],"sources":["../src/typegen/scanner.ts","../src/typegen/generator.ts","../src/typegen/index.ts"],"sourcesContent":["/**\n * Static scanner for KickJS decorated classes and DI tokens.\n *\n * Walks `src/**\\/*.ts` (excluding tests and node_modules) and extracts:\n *\n * - Decorated classes (`@Service`, `@Controller`, `@Repository`, etc.)\n * - `createToken<T>('name')` definitions\n * - `@Inject('literal')` calls\n *\n * The output feeds the type generator, which emits `.kickjs/types/*.d.ts`\n * files used by the user's tsc to make `container.resolve()` and module\n * discovery type-safe.\n *\n * This is intentionally regex-based (not AST-based) to avoid the\n * ts-morph / typescript compiler dependency. Pattern from\n * `packages/vite/src/module-discovery.ts` which already uses regex\n * to detect `*.module.ts` exports.\n *\n * ## Collision detection\n *\n * Two classes with the same name across different files is a collision.\n * The scanner records all collisions in `ScanResult.collisions` so the\n * caller (generator) can decide whether to hard-error or auto-namespace.\n *\n * @module @forinda/kickjs-cli/typegen/scanner\n */\n\nimport type { Dirent } from 'node:fs'\nimport { readdir, readFile } from 'node:fs/promises'\nimport { join, relative, resolve, sep } from 'node:path'\n\n/** Decorators that mark a class as DI-managed */\nexport const DECORATOR_NAMES = [\n 'Service',\n 'Controller',\n 'Repository',\n 'Injectable',\n 'Component',\n 'Module',\n] as const\n\nexport type DecoratorName = (typeof DECORATOR_NAMES)[number]\n\n/** A single discovered decorated class */\nexport interface DiscoveredClass {\n /** Class name (e.g., 'UserService') */\n className: string\n /** Decorator that marked it (e.g., 'Service') */\n decorator: DecoratorName\n /** Absolute file path */\n filePath: string\n /** Path relative to scan root, with forward slashes */\n relativePath: string\n /** True if exported as `default` */\n isDefault: boolean\n}\n\n/** A single route handler discovered on a controller class */\nexport interface DiscoveredRoute {\n /** Owning controller class name (e.g. 'UserController') */\n controller: string\n /** Handler method name on the controller (e.g. 'getUser') */\n method: string\n /** HTTP verb (uppercase) */\n httpMethod: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'\n /** Route path including parameter placeholders (e.g. '/:id/posts/:postId') */\n path: string\n /** URL path parameter names extracted from `:placeholder` segments */\n pathParams: string[]\n /**\n * Whitelisted query field names extracted from `@ApiQueryParams({...})`.\n * `null` means no `@ApiQueryParams` was found on this method (so the\n * generator emits an unconstrained `query` shape). An empty array means\n * the decorator existed but no fields could be statically extracted\n * (e.g. an opaque imported config).\n */\n queryFilterable: string[] | null\n querySortable: string[] | null\n querySearchable: string[] | null\n /**\n * Schema identifiers referenced from the route decorator's second arg\n * (e.g. `@Post('/', { body: createTaskSchema })`). `null` means no\n * such reference; the value carries the identifier and the resolved\n * import source (relative module path) if known.\n */\n bodySchema: SchemaRef | null\n querySchema: SchemaRef | null\n paramsSchema: SchemaRef | null\n /** Absolute file path of the controller */\n filePath: string\n /** Path relative to scan root, with forward slashes */\n relativePath: string\n}\n\n/** A statically-resolved schema identifier reference */\nexport interface SchemaRef {\n /** The identifier as written (e.g. `createTaskSchema`) */\n identifier: string\n /**\n * Resolved module specifier (relative path or bare module name) where\n * the identifier is defined. `null` means the source could not be\n * statically determined (the generator falls back to `unknown`).\n */\n source: string | null\n}\n\n/** A `createToken<T>('name')` call discovered in source */\nexport interface DiscoveredToken {\n /** The literal string passed to `createToken()` */\n name: string\n /** The const variable name on the LHS, if any */\n variable: string | null\n /** Absolute file path */\n filePath: string\n /** Path relative to scan root, with forward slashes */\n relativePath: string\n}\n\n/** An `@Inject('literal')` call discovered in source */\nexport interface DiscoveredInject {\n /** The literal string passed to `@Inject()` */\n name: string\n /** Absolute file path */\n filePath: string\n /** Path relative to scan root, with forward slashes */\n relativePath: string\n}\n\n/** A name collision — same class name in two or more files */\nexport interface ClassCollision {\n /** The colliding class name */\n className: string\n /** All files declaring the class */\n classes: DiscoveredClass[]\n}\n\n/**\n * Information about a discovered env schema file. The typegen\n * generator uses this to emit a `KickEnv` + `NodeJS.ProcessEnv`\n * augmentation that flows through to `@Value` and `process.env`.\n *\n * `null` means no env file was found at the configured location.\n */\nexport interface DiscoveredEnv {\n /** Absolute path to the env schema file */\n filePath: string\n /** Path relative to scan root, with forward slashes */\n relativePath: string\n}\n\n/** Aggregated scanner output */\nexport interface ScanResult {\n classes: DiscoveredClass[]\n routes: DiscoveredRoute[]\n tokens: DiscoveredToken[]\n injects: DiscoveredInject[]\n collisions: ClassCollision[]\n /** Discovered env schema file (or null if none found at the configured path) */\n env: DiscoveredEnv | null\n}\n\n/** Options for the scanner */\nexport interface ScanOptions {\n /** Root directory to scan (e.g., absolute path to `src`) */\n root: string\n /** Project root used to compute relative paths (e.g., process.cwd()) */\n cwd: string\n /** Glob-like extensions to scan */\n extensions?: string[]\n /** Substrings that exclude a path (matched against relative path) */\n exclude?: string[]\n /**\n * Path to the env schema file, relative to `cwd`. Defaults to\n * `'src/env.ts'`. The file must contain a `defineEnv(...)` call\n * with a default export for the typegen to emit a typed `KickEnv`\n * augmentation. If the file does not exist or doesn't match the\n * expected shape, env typing is skipped silently.\n */\n envFile?: string\n}\n\nconst DEFAULT_EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts']\nconst DEFAULT_EXCLUDES = ['node_modules', '.kickjs', 'dist', 'build', '.test.', '.spec.', '.d.ts']\n\n/**\n * Match a class-level decorator immediately followed by an exported\n * class declaration. Captures decorator name and class name.\n */\nconst DECORATED_CLASS_REGEX = new RegExp(\n String.raw`@(${DECORATOR_NAMES.join('|')})\\s*\\([^)]*\\)` +\n String.raw`(?:\\s*@[A-Z]\\w*(?:\\s*\\([^)]*\\))?)*` +\n String.raw`\\s*export\\s+(default\\s+)?(?:abstract\\s+)?class\\s+(\\w+)`,\n 'g',\n)\n\n/**\n * Match a `createToken<T>('name')` call with optional `export const X =`\n * or `const X =` prefix. Tolerates whitespace and the type parameter\n * being absent (`createToken('name')`).\n */\nconst CREATE_TOKEN_REGEX =\n /(?:export\\s+)?const\\s+(\\w+)\\s*(?::\\s*[^=]+)?=\\s*createToken\\s*(?:<[^>]*>)?\\s*\\(\\s*['\"`]([^'\"`]+)['\"`]\\s*\\)/g\n\n/**\n * Match a bare `createToken<T>('name')` call (no const assignment) so\n * we still pick up dynamically-used tokens.\n */\nconst BARE_CREATE_TOKEN_REGEX = /createToken\\s*(?:<[^>]*>)?\\s*\\(\\s*['\"`]([^'\"`]+)['\"`]\\s*\\)/g\n\n/** Match `@Inject('literal')` — only literals; computed args are skipped */\nconst INJECT_LITERAL_REGEX = /@Inject\\s*\\(\\s*['\"`]([^'\"`]+)['\"`]\\s*\\)/g\n\n/** HTTP route decorator names recognised by the scanner */\nconst HTTP_DECORATORS = ['Get', 'Post', 'Put', 'Delete', 'Patch'] as const\n\n/**\n * Match a route decorator immediately followed by a method declaration.\n * Captures the HTTP verb, path literal (or empty), and method name.\n *\n * Tolerates:\n * - Optional second arg to the route decorator (`@Get('/path', { ... })`)\n * - Stacked decorators between the route and the method (`@Get('/') @Use(...)`)\n * - Path-less decorators (`@Get()` → defaults to `/`)\n * - `async` modifier on the method\n *\n * Run within a class body slice (see extractRoutesFromSource) so the\n * captured method name is unambiguously a method on that class.\n */\nconst ROUTE_METHOD_REGEX = new RegExp(\n String.raw`@(${HTTP_DECORATORS.join('|')})\\s*\\(` +\n String.raw`(?:\\s*['\"\\`]([^'\"\\`]*)['\"\\`])?[^)]*\\)` +\n String.raw`(?:\\s*@[A-Z]\\w*(?:\\s*\\([^)]*\\))?)*` +\n String.raw`\\s*(?:public\\s+|private\\s+|protected\\s+)?(?:async\\s+)?` +\n String.raw`([a-zA-Z_]\\w*)\\s*\\(`,\n 'g',\n)\n\n/** Extract `:placeholder` segments from an Express route path */\nfunction extractPathParams(path: string): string[] {\n const matches = path.match(/:([a-zA-Z_]\\w*)/g) ?? []\n return matches.map((m) => m.slice(1))\n}\n\n/**\n * Given the matched text of a route decorator + method declaration, return\n * the substring inside the route decorator's argument list (between the\n * outermost `(` and `)`). Returns `null` if no parens are found.\n *\n * Example input:\n * `@Post('/', { body: createTaskSchema, name: 'CreateTask' }) async create(`\n * Returns:\n * `'/', { body: createTaskSchema, name: 'CreateTask' }`\n */\nfunction extractRouteOptionsArg(matchedText: string): string | null {\n const open = matchedText.indexOf('(')\n if (open < 0) return null\n let depth = 1\n for (let i = open + 1; i < matchedText.length; i++) {\n const ch = matchedText[i]\n if (ch === '(') depth++\n else if (ch === ')') {\n depth--\n if (depth === 0) return matchedText.slice(open + 1, i)\n }\n }\n return null\n}\n\n/**\n * Extract a bare identifier value from a single field in an object literal\n * embedded in a string. Returns `null` if the field is missing or its value\n * isn't a bare identifier (e.g. an inline object, function call, etc.).\n *\n * Example: `extractObjectFieldIdentifier(\"'/' , { body: createTaskSchema }\", 'body')`\n * returns `'createTaskSchema'`.\n */\nfunction extractObjectFieldIdentifier(text: string, field: string): string | null {\n // Look for `field: <identifier>` not followed by `(` (function call) or `{` (inline object)\n const re = new RegExp(String.raw`\\b${field}\\s*:\\s*([A-Za-z_$][\\w$]*)`, 'g')\n const m = re.exec(text)\n if (!m) return null\n return m[1]\n}\n\n/**\n * Resolve a bare identifier to its module source by inspecting the file's\n * top-level imports and same-file `const` declarations.\n *\n * - `import { X } from './path'` → returns `'./path'`\n * - `import X from './path'` (default import) → returns `'./path'`\n * - `import * as X from './path'` → returns `'./path'`\n * - `const X = z.object(...)` (same file) → returns `null` (caller emits a self-import)\n *\n * Returns `null` when the identifier cannot be resolved.\n */\nfunction resolveImportSource(source: string, identifier: string): string | null {\n // Named import: `import { X, Y as Z } from './path'`\n const namedRe = new RegExp(\n String.raw`import\\s*(?:type\\s+)?\\{[^}]*\\b${identifier}\\b[^}]*\\}\\s*from\\s*['\"\\`]([^'\"\\`]+)['\"\\`]`,\n )\n const named = namedRe.exec(source)\n if (named) return named[1]\n\n // Default import: `import X from './path'`\n const defaultRe = new RegExp(\n String.raw`import\\s+(?:type\\s+)?${identifier}\\s+from\\s*['\"\\`]([^'\"\\`]+)['\"\\`]`,\n )\n const def = defaultRe.exec(source)\n if (def) return def[1]\n\n // Namespace import: `import * as X from './path'`\n const nsRe = new RegExp(\n String.raw`import\\s*\\*\\s*as\\s+${identifier}\\s+from\\s*['\"\\`]([^'\"\\`]+)['\"\\`]`,\n )\n const ns = nsRe.exec(source)\n if (ns) return ns[1]\n\n // Same-file const declaration — return empty string as a sentinel meaning\n // \"current file\". The generator turns this into a self-relative reference.\n const constRe = new RegExp(String.raw`(?:^|\\n)\\s*(?:export\\s+)?const\\s+${identifier}\\b`)\n if (constRe.test(source)) return ''\n\n return null\n}\n\n/**\n * Extract whitelist arrays from an `@ApiQueryParams(...)` decorator\n * within `decoratorBlock`. Handles two forms:\n *\n * - Inline literal: `@ApiQueryParams({ filterable: ['a', 'b'], ... })`\n * - Const reference: `@ApiQueryParams(SOME_CONFIG)` — looks up\n * `const SOME_CONFIG = { ... }` in the same file (`fullSource`).\n *\n * Returns `null` if no `@ApiQueryParams` is present. Returns\n * `{ filterable: [], sortable: [], searchable: [] }` if the decorator\n * is present but no fields could be statically extracted (opaque\n * imports, column-object configs, function calls, etc.).\n */\nfunction extractApiQueryParams(\n decoratorBlock: string,\n fullSource: string,\n): { filterable: string[]; sortable: string[]; searchable: string[] } | null {\n const apiMatch = /@ApiQueryParams\\s*\\(\\s*([\\s\\S]*?)\\s*\\)\\s*$/.exec(decoratorBlock)\n if (!apiMatch) {\n // Try without anchoring to the end (decorator may not be the last in the block)\n const loose = /@ApiQueryParams\\s*\\(([\\s\\S]*?)\\)/.exec(decoratorBlock)\n if (!loose) return null\n return parseApiQueryParamsArg(loose[1].trim(), fullSource)\n }\n return parseApiQueryParamsArg(apiMatch[1].trim(), fullSource)\n}\n\nfunction parseApiQueryParamsArg(\n arg: string,\n fullSource: string,\n): { filterable: string[]; sortable: string[]; searchable: string[] } {\n // Inline literal — starts with `{`\n if (arg.startsWith('{')) {\n return parseInlineConfigLiteral(arg)\n }\n // Const reference — bare identifier (possibly with type assertion)\n const idMatch = /^([A-Za-z_]\\w*)/.exec(arg)\n if (idMatch) {\n const ident = idMatch[1]\n // Look for `const IDENT = { ... }` in the same source file\n const constRe = new RegExp(\n String.raw`const\\s+${ident}\\s*(?::\\s*[^=]+)?=\\s*(\\{[\\s\\S]*?\\n\\})`,\n 'm',\n )\n const constMatch = constRe.exec(fullSource)\n if (constMatch) {\n return parseInlineConfigLiteral(constMatch[1])\n }\n }\n // Fallback: decorator present but extraction failed\n return { filterable: [], sortable: [], searchable: [] }\n}\n\n/** Extract a string array literal for one config key from an inline object literal */\nfunction extractStringArray(literal: string, key: string): string[] {\n const re = new RegExp(String.raw`${key}\\s*:\\s*\\[([\\s\\S]*?)\\]`)\n const m = re.exec(literal)\n if (!m) return []\n return Array.from(m[1].matchAll(/['\"`]([^'\"`]+)['\"`]/g)).map((x) => x[1])\n}\n\n/** Parse an inline `{ filterable: [...], sortable: [...], searchable: [...] }` literal */\nfunction parseInlineConfigLiteral(literal: string): {\n filterable: string[]\n sortable: string[]\n searchable: string[]\n} {\n return {\n filterable: extractStringArray(literal, 'filterable'),\n sortable: extractStringArray(literal, 'sortable'),\n searchable: extractStringArray(literal, 'searchable'),\n }\n}\n\n/** Recursively walk a directory and yield matching file paths */\nasync function walk(dir: string, opts: ScanOptions): Promise<string[]> {\n const exts = opts.extensions ?? DEFAULT_EXTENSIONS\n const excludes = opts.exclude ?? DEFAULT_EXCLUDES\n const out: string[] = []\n\n let entries: Dirent[]\n try {\n entries = (await readdir(dir, { withFileTypes: true, encoding: 'utf-8' })) as Dirent[]\n } catch {\n return out\n }\n\n for (const entry of entries) {\n const full = join(dir, entry.name)\n const rel = relative(opts.cwd, full)\n\n if (excludes.some((ex) => rel.includes(ex))) continue\n\n if (entry.isDirectory()) {\n out.push(...(await walk(full, opts)))\n } else if (entry.isFile()) {\n if (exts.some((ext) => entry.name.endsWith(ext))) {\n out.push(full)\n }\n }\n }\n\n return out\n}\n\n/** Compute the forward-slash relative path used in scanner output */\nfunction toRelative(filePath: string, cwd: string): string {\n return relative(cwd, filePath).split(sep).join('/')\n}\n\n/** Extract decorated classes from a single source file */\nexport function extractClassesFromSource(\n source: string,\n filePath: string,\n cwd: string,\n): DiscoveredClass[] {\n const out: DiscoveredClass[] = []\n const relPath = toRelative(filePath, cwd)\n\n DECORATED_CLASS_REGEX.lastIndex = 0\n let match: RegExpExecArray | null\n while ((match = DECORATED_CLASS_REGEX.exec(source)) !== null) {\n const [, decorator, defaultMarker, className] = match\n out.push({\n className,\n decorator: decorator as DecoratorName,\n filePath,\n relativePath: relPath,\n isDefault: Boolean(defaultMarker),\n })\n }\n\n return out\n}\n\n/** Extract `createToken('name')` definitions from a single source file */\nexport function extractTokensFromSource(\n source: string,\n filePath: string,\n cwd: string,\n): DiscoveredToken[] {\n const out: DiscoveredToken[] = []\n const relPath = toRelative(filePath, cwd)\n const seen = new Set<string>()\n\n // First pass: const-bound tokens (preferred — we get the variable name)\n CREATE_TOKEN_REGEX.lastIndex = 0\n let match: RegExpExecArray | null\n while ((match = CREATE_TOKEN_REGEX.exec(source)) !== null) {\n const [full, variable, name] = match\n seen.add(full)\n out.push({ name, variable, filePath, relativePath: relPath })\n }\n\n // Second pass: bare calls not captured above (rare but possible)\n BARE_CREATE_TOKEN_REGEX.lastIndex = 0\n while ((match = BARE_CREATE_TOKEN_REGEX.exec(source)) !== null) {\n if (seen.has(match[0])) continue\n out.push({\n name: match[1],\n variable: null,\n filePath,\n relativePath: relPath,\n })\n }\n\n return out\n}\n\n/**\n * Extract route handlers from a source file.\n *\n * For each decorated class in `classesInFile`, slices the source from\n * the class declaration to the next class (or EOF) and runs the route\n * decorator regex within that slice. The result is a list of routes\n * tagged with their owning controller.\n *\n * Heuristic note: this assumes classes are not nested. KickJS controllers\n * are top-level by convention so this holds in practice.\n */\nexport function extractRoutesFromSource(\n source: string,\n filePath: string,\n cwd: string,\n classesInFile: DiscoveredClass[],\n): DiscoveredRoute[] {\n const out: DiscoveredRoute[] = []\n if (classesInFile.length === 0) return out\n const relPath = toRelative(filePath, cwd)\n\n // Locate each class declaration's offset in the source\n const positions: Array<{ cls: DiscoveredClass; start: number }> = []\n for (const cls of classesInFile) {\n const re = new RegExp(String.raw`class\\s+${cls.className}\\b`)\n const m = re.exec(source)\n if (m?.index !== undefined) {\n positions.push({ cls, start: m.index })\n }\n }\n positions.sort((a, b) => a.start - b.start)\n\n for (let i = 0; i < positions.length; i++) {\n const { cls, start } = positions[i]\n const end = i + 1 < positions.length ? positions[i + 1].start : source.length\n const block = source.slice(start, end)\n\n ROUTE_METHOD_REGEX.lastIndex = 0\n let match: RegExpExecArray | null\n while ((match = ROUTE_METHOD_REGEX.exec(block)) !== null) {\n const [matchedText, verb, pathLiteral, methodName] = match\n const path = pathLiteral && pathLiteral.length > 0 ? pathLiteral : '/'\n\n // The route regex already greedily matched any stacked decorators\n // BETWEEN the route decorator and the method declaration. Inspect\n // the matched substring for an `@ApiQueryParams(...)` call.\n const apiQp = extractApiQueryParams(matchedText, source)\n\n // The route decorator's second argument carries body/query/params\n // schema references. Extract them from the leading slice of the\n // matched text (the part before any stacked decorators).\n const routeArgs = extractRouteOptionsArg(matchedText)\n const bodyId = routeArgs ? extractObjectFieldIdentifier(routeArgs, 'body') : null\n const queryId = routeArgs ? extractObjectFieldIdentifier(routeArgs, 'query') : null\n const paramsId = routeArgs ? extractObjectFieldIdentifier(routeArgs, 'params') : null\n\n out.push({\n controller: cls.className,\n method: methodName,\n httpMethod: verb.toUpperCase() as DiscoveredRoute['httpMethod'],\n path,\n pathParams: extractPathParams(path),\n queryFilterable: apiQp?.filterable ?? null,\n querySortable: apiQp?.sortable ?? null,\n querySearchable: apiQp?.searchable ?? null,\n bodySchema: bodyId\n ? { identifier: bodyId, source: resolveImportSource(source, bodyId) }\n : null,\n querySchema: queryId\n ? { identifier: queryId, source: resolveImportSource(source, queryId) }\n : null,\n paramsSchema: paramsId\n ? { identifier: paramsId, source: resolveImportSource(source, paramsId) }\n : null,\n filePath,\n relativePath: relPath,\n })\n }\n }\n\n return out\n}\n\n/** Extract `@Inject('literal')` calls from a single source file */\nexport function extractInjectsFromSource(\n source: string,\n filePath: string,\n cwd: string,\n): DiscoveredInject[] {\n const out: DiscoveredInject[] = []\n const relPath = toRelative(filePath, cwd)\n\n INJECT_LITERAL_REGEX.lastIndex = 0\n let match: RegExpExecArray | null\n while ((match = INJECT_LITERAL_REGEX.exec(source)) !== null) {\n out.push({ name: match[1], filePath, relativePath: relPath })\n }\n\n return out\n}\n\n/**\n * Default search order for the env schema file. Newer projects keep\n * the schema under `src/config/` so the framework's \"config\" concept\n * has a single home; older scaffolds dropped it at `src/env.ts` (kept\n * here for back-compat). The first match wins.\n */\nconst DEFAULT_ENV_FILE_CANDIDATES = [\n 'src/config/index.ts',\n 'src/config/env.ts',\n 'src/config.ts',\n 'src/env.ts',\n] as const\n\n/**\n * Look for an env schema file. When `envFile` is the string default\n * (`'src/env.ts'`) or omitted, every entry in `DEFAULT_ENV_FILE_CANDIDATES`\n * is tried in order. When the caller passes an explicit path, only that\n * path is tried (so projects can opt out of the search by setting\n * `kick.config.ts → typegen.envFile`).\n *\n * Returns a `DiscoveredEnv` if the file exists and contains both a\n * `defineEnv(...)` call and a default export — the two markers we\n * need before it's safe to emit `import type schema from '...'` in\n * the generator. Returns `null` for any other state (no candidate\n * found, no defineEnv, no default export) so the generator skips env\n * typing silently.\n */\nexport async function detectEnvFile(cwd: string, envFile: string): Promise<DiscoveredEnv | null> {\n // The CLI passes the literal default `'src/env.ts'` when the user\n // hasn't overridden it. Treat that as \"use the search list\" rather\n // than pinning to one path, so newer scaffolds at src/config/ keep\n // working without forcing every project to set typegen.envFile.\n const candidates: readonly string[] =\n envFile === 'src/env.ts' ? DEFAULT_ENV_FILE_CANDIDATES : [envFile]\n\n for (const candidate of candidates) {\n const abs = resolve(cwd, candidate)\n let source: string\n try {\n source = await readFile(abs, 'utf-8')\n } catch {\n continue\n }\n // Cheap heuristic: defineEnv(...) call AND a default export.\n // We don't try to evaluate the file — the generator emits an\n // `import type schema from '...'` and lets the user's tsc do the\n // actual schema-to-type inference.\n if (!/\\bdefineEnv\\s*\\(/.test(source)) continue\n if (!/export\\s+default\\b/.test(source)) continue\n return {\n filePath: abs,\n relativePath: toRelative(abs, cwd),\n }\n }\n\n return null\n}\n\n/** Detect duplicate class names across files */\nexport function findCollisions(classes: DiscoveredClass[]): ClassCollision[] {\n const groups = new Map<string, DiscoveredClass[]>()\n for (const cls of classes) {\n const arr = groups.get(cls.className) ?? []\n arr.push(cls)\n groups.set(cls.className, arr)\n }\n\n const collisions: ClassCollision[] = []\n for (const [className, group] of groups) {\n // Two declarations of the same class name in different files = collision.\n // Multiple decorators on the same file/class are NOT a collision.\n const distinctFiles = new Set(group.map((c) => c.filePath))\n if (distinctFiles.size > 1) {\n collisions.push({ className, classes: group })\n }\n }\n\n // Deterministic order\n collisions.sort((a, b) => a.className.localeCompare(b.className))\n return collisions\n}\n\n/**\n * Scan a project for decorated classes, createToken definitions, and\n * `@Inject` literal usages.\n */\nexport async function scanProject(opts: ScanOptions): Promise<ScanResult> {\n const root = resolve(opts.root)\n const files = await walk(root, opts)\n\n const classes: DiscoveredClass[] = []\n const routes: DiscoveredRoute[] = []\n const tokens: DiscoveredToken[] = []\n const injects: DiscoveredInject[] = []\n\n // Two passes: first collect all classes, then a second pass extracts\n // routes per file using the per-file class list as scoping context.\n // This keeps class discovery and route discovery independent.\n const sources = new Map<string, string>()\n for (const file of files) {\n let source: string\n try {\n source = await readFile(file, 'utf-8')\n } catch {\n continue\n }\n sources.set(file, source)\n classes.push(...extractClassesFromSource(source, file, opts.cwd))\n tokens.push(...extractTokensFromSource(source, file, opts.cwd))\n injects.push(...extractInjectsFromSource(source, file, opts.cwd))\n }\n\n for (const [file, source] of sources) {\n const classesInFile = classes.filter((c) => c.filePath === file)\n routes.push(...extractRoutesFromSource(source, file, opts.cwd, classesInFile))\n }\n\n // Deterministic ordering for stable .d.ts output\n classes.sort((a, b) => {\n if (a.className !== b.className) return a.className.localeCompare(b.className)\n return a.relativePath.localeCompare(b.relativePath)\n })\n tokens.sort(\n (a, b) => a.name.localeCompare(b.name) || a.relativePath.localeCompare(b.relativePath),\n )\n injects.sort(\n (a, b) => a.name.localeCompare(b.name) || a.relativePath.localeCompare(b.relativePath),\n )\n routes.sort(\n (a, b) => a.controller.localeCompare(b.controller) || a.method.localeCompare(b.method),\n )\n\n const collisions = findCollisions(classes)\n const env = await detectEnvFile(opts.cwd, opts.envFile ?? 'src/env.ts')\n\n return { classes, routes, tokens, injects, collisions, env }\n}\n","/**\n * Generates `.d.ts` files inside `.kickjs/types/` from the discovered\n * decorated classes and DI tokens. Pattern modeled on React Router's\n * `.react-router/types/` directory.\n *\n * Outputs:\n * - `.kickjs/types/registry.d.ts` — module augmentation for `KickJsRegistry`\n * that gives `container.resolve('UserService')` the right return type.\n * - `.kickjs/types/services.d.ts` — string-literal union of all known\n * service-style tokens for tooling autocomplete.\n * - `.kickjs/types/modules.d.ts` — string-literal union of discovered\n * module class names.\n * - `.kickjs/types/index.d.ts` — re-exports the above (single import target).\n * - `.kickjs/.gitignore` — gitignores the whole folder so generated files\n * never get committed.\n *\n * ## Collision behaviour\n *\n * If `findCollisions()` returns any duplicate class names:\n * - **Default (`allowDuplicates: false`)** — `generateTypes` throws a\n * `TokenCollisionError` with a clear message listing every conflicting\n * file. The caller (CLI) prints it and exits non-zero. Nothing is\n * written to disk.\n * - **`allowDuplicates: true`** — colliding classes are auto-namespaced\n * by their relative file path so the registry keys become e.g.\n * `'modules/users/UserService'` instead of `'UserService'`. Non-colliding\n * classes still get bare `'ClassName'` keys (smart default).\n *\n * @module @forinda/kickjs-cli/typegen/generator\n */\n\nimport { mkdir, writeFile } from 'node:fs/promises'\nimport { dirname, join, relative, resolve, sep } from 'node:path'\nimport type {\n ClassCollision,\n DiscoveredClass,\n DiscoveredEnv,\n DiscoveredInject,\n DiscoveredRoute,\n DiscoveredToken,\n} from './scanner'\n\n/** Header written to every generated file */\nconst HEADER = `/* eslint-disable */\n// AUTO-GENERATED by \\`kick typegen\\`. DO NOT EDIT.\n// Re-run with \\`kick typegen\\` or rely on \\`kick dev\\` to refresh.\n`\n\n/** Decorators whose classes participate in the DI registry augmentation */\nconst REGISTRY_DECORATORS = new Set(['Service', 'Repository', 'Injectable', 'Component'])\n\n/** Thrown by `generateTypes` when collisions are found and not allowed */\nexport class TokenCollisionError extends Error {\n readonly collisions: ClassCollision[]\n constructor(collisions: ClassCollision[]) {\n super(formatCollisionMessage(collisions))\n this.name = 'TokenCollisionError'\n this.collisions = collisions\n }\n}\n\n/** Build a human-readable message describing every collision */\nfunction formatCollisionMessage(collisions: ClassCollision[]): string {\n const lines: string[] = ['kick typegen: token collision detected']\n for (const c of collisions) {\n lines.push('')\n lines.push(` ${c.classes.length} classes named '${c.className}':`)\n for (const cls of c.classes) {\n lines.push(` - ${cls.relativePath}`)\n }\n }\n lines.push('')\n lines.push('Resolutions:')\n lines.push(' (a) Rename one of the classes')\n lines.push(\n \" (b) Use createToken<T>('namespaced/Name') and import the token explicitly — see @forinda/kickjs\",\n )\n lines.push(' (c) Pass --allow-duplicates to namespace the registry keys automatically')\n lines.push(\" (e.g. 'modules/users/UserService' instead of 'UserService')\")\n return lines.join('\\n')\n}\n\n/** Compute the module specifier (without extension) used inside `import('...')` */\nfunction importSpecifierFor(targetFile: string, fromFile: string): string {\n const fromDir = dirname(fromFile)\n let rel = relative(fromDir, targetFile).split(sep).join('/')\n rel = rel.replace(/\\.(ts|tsx|mts|cts)$/i, '')\n if (!rel.startsWith('.')) rel = './' + rel\n return rel\n}\n\n/**\n * Build the namespaced registry key for a colliding class.\n * Strips the `src/` prefix and the file extension, then appends the\n * class name. Example: `src/modules/users/user.service.ts` + `UserService`\n * → `modules/users/UserService`.\n */\nfunction namespacedKeyFor(cls: DiscoveredClass): string {\n const rel = cls.relativePath.replace(/^src\\//, '').replace(/\\.(ts|tsx|mts|cts)$/i, '')\n // Drop the trailing filename if it's just the class in kebab/snake form —\n // keep the directory path as the namespace.\n const parts = rel.split('/')\n parts.pop()\n const ns = parts.join('/')\n return ns ? `${ns}/${cls.className}` : cls.className\n}\n\n/**\n * Render the `KickJsRegistry` module augmentation. Each entry maps a\n * string token to the imported class type.\n *\n * Default-exported classes are imported as `import('...').default`.\n *\n * `collidingNames` lists class names that should be auto-namespaced;\n * everything else gets a bare key.\n */\nfunction renderRegistry(\n classes: DiscoveredClass[],\n outFile: string,\n collidingNames: Set<string>,\n): string {\n const seen = new Set<string>()\n const entries: string[] = []\n\n for (const c of classes) {\n if (!REGISTRY_DECORATORS.has(c.decorator)) continue\n\n const key = collidingNames.has(c.className) ? namespacedKeyFor(c) : c.className\n if (seen.has(key)) continue\n seen.add(key)\n\n const spec = importSpecifierFor(c.filePath, outFile)\n const ref = c.isDefault ? `import('${spec}').default` : `import('${spec}').${c.className}`\n entries.push(` '${key}': ${ref}`)\n }\n\n const body = entries.length\n ? entries.join('\\n')\n : ' // (no services discovered yet — run `kick g service <name>` to add one)'\n\n return `${HEADER}\ndeclare module '@forinda/kickjs' {\n interface KickJsRegistry {\n${body}\n }\n}\n\nexport {}\n`\n}\n\n/** Render a string-literal union type containing the given names */\nfunction renderUnion(typeName: string, names: string[], emptyComment: string): string {\n if (names.length === 0) {\n return `${HEADER}\n// ${emptyComment}\nexport type ${typeName} = never\n`\n }\n const sorted = [...new Set(names)].sort()\n return `${HEADER}\nexport type ${typeName} =\n${sorted.map((n) => ` | '${n}'`).join('\\n')}\n`\n}\n\n/** Render the barrel index that re-exports the union types */\nfunction renderIndex(includeEnv: boolean): string {\n const envImport = includeEnv ? \"import './env'\\n\" : ''\n return `${HEADER}\nexport type { ServiceToken } from './services'\nexport type { ModuleToken } from './modules'\n\n// The registry, routes, and env augmentations are loaded as side-effects —\n// importing this file (or having it on tsconfig include) is enough for\n// \\`container.resolve()\\`, \\`Ctx<KickRoutes.UserController['getUser']>\\`,\n// and \\`@Value('PORT')\\` to resolve.\nimport './registry'\nimport './routes'\n${envImport}`\n}\n\n/**\n * Render the `query` field's TypeScript type for a single route.\n *\n * - When `@ApiQueryParams` is absent (`queryFilterable === null`), emits\n * `unknown` so the user gets nothing extra.\n * - When the decorator is present, emits an object literal whose keys\n * are the standard query string keys (`filter`, `sort`, `q`, `page`,\n * `limit`). `sort` is narrowed to a string-literal union of allowed\n * field names with optional `-` direction prefix.\n */\nfunction renderQueryShape(m: DiscoveredRoute): string {\n if (m.queryFilterable === null) return 'unknown'\n const sortable = m.querySortable ?? []\n const sortType =\n sortable.length > 0 ? sortable.flatMap((f) => [`'${f}'`, `'-${f}'`]).join(' | ') : 'string'\n return `{ filter?: string | string[]; sort?: ${sortType}; q?: string; page?: string; limit?: string }`\n}\n\n/** Render JSDoc lines summarising the @ApiQueryParams whitelist */\nfunction renderQueryDocLines(m: DiscoveredRoute): string[] {\n const lines: string[] = []\n if (m.queryFilterable && m.queryFilterable.length > 0) {\n lines.push(`Filterable: ${m.queryFilterable.join(', ')}`)\n }\n if (m.querySortable && m.querySortable.length > 0) {\n lines.push(`Sortable: ${m.querySortable.join(', ')}`)\n }\n if (m.querySearchable && m.querySearchable.length > 0) {\n lines.push(`Searchable: ${m.querySearchable.join(', ')}`)\n }\n return lines\n}\n\n/**\n * Plan a schema import for hoisting at the top of `routes.ts`. Returns\n * the alias the in-namespace code should use, or `null` if the schema\n * cannot be referenced (no validator configured, or source unresolvable).\n *\n * Aliases are unique per (alias-counter) so two schemas named\n * `createTaskSchema` from different modules don't collide.\n */\nfunction planSchemaImport(\n schema: { identifier: string; source: string | null } | null,\n routeFilePath: string,\n routesOutFile: string,\n schemaValidator: 'zod' | false,\n imports: Map<string, { identifier: string; specifier: string }>,\n): string | null {\n if (!schema || schemaValidator !== 'zod') return null\n if (schema.source === null) return null\n const specifier = resolveSchemaImportSpecifier(schema.source, routeFilePath, routesOutFile)\n if (specifier === 'unknown') return null\n const key = `${specifier}::${schema.identifier}`\n let alias = imports.get(key)?.specifier\n if (!alias) {\n alias = `_S${imports.size}`\n imports.set(key, { identifier: schema.identifier, specifier: alias })\n } else {\n alias = imports.get(key)!.specifier\n }\n return alias\n}\n\n/** Build the `import type { ... } from '...'` lines for hoisted schema imports */\nfunction renderSchemaImports(\n imports: Map<string, { identifier: string; specifier: string }>,\n): string {\n if (imports.size === 0) return ''\n const lines: string[] = []\n for (const [key, value] of imports) {\n const [path] = key.split('::')\n lines.push(`import type { ${value.identifier} as ${value.specifier} } from '${path}'`)\n }\n return lines.join('\\n') + '\\n'\n}\n\n/**\n * Compute the import specifier the generated `routes.d.ts` should use to\n * reach a schema declared either in the controller file (empty string)\n * or imported from elsewhere (relative path or bare module name).\n *\n * - Bare module names (`zod`, `@scope/pkg`) are returned as-is.\n * - Relative paths (`./users.dto`, `../shared/schema`) are resolved\n * against the controller's file path, then re-relativised against the\n * directory containing `routes.d.ts`.\n * - Empty string (same-file schema) becomes a relative path from the\n * `routes.d.ts` directory back to the controller file.\n */\nfunction resolveSchemaImportSpecifier(\n source: string | null,\n routeFilePath: string,\n routesOutFile: string,\n): string {\n if (source === null) return 'unknown'\n const routesDir = dirname(routesOutFile)\n\n // Same-file schema — point at the controller file itself\n if (source === '') {\n let rel = relative(routesDir, routeFilePath).split(sep).join('/')\n rel = rel.replace(/\\.(ts|tsx|mts|cts)$/i, '')\n if (!rel.startsWith('.')) rel = './' + rel\n return rel\n }\n\n // Bare module name (no leading `.` and not absolute) → keep as-is\n if (!source.startsWith('.') && !source.startsWith('/')) {\n return source\n }\n\n // Relative import → resolve against the controller's directory, then\n // re-relativise against the routes.d.ts directory\n const controllerDir = dirname(routeFilePath)\n const absoluteTarget = resolve(controllerDir, source)\n let rel = relative(routesDir, absoluteTarget).split(sep).join('/')\n rel = rel.replace(/\\.(ts|tsx|mts|cts)$/i, '')\n if (!rel.startsWith('.')) rel = './' + rel\n return rel\n}\n\n/**\n * Render the `KickEnv` + `NodeJS.ProcessEnv` augmentation file from a\n * detected env schema. Mirrors the routes.ts pattern: emits as a `.ts`\n * file (not `.d.ts`) so the top-level `import type schema from '...'`\n * actually resolves under `moduleResolution: 'bundler'`.\n *\n * Returns `null` when no env file was discovered, so the caller can\n * skip writing the file altogether (rather than emitting an empty\n * augmentation that would shadow `KickEnv` to a useless `{}`).\n */\nfunction renderEnv(env: DiscoveredEnv | null, envOutFile: string): string | null {\n if (!env) return null\n // Compute the relative import path from .kickjs/types/env.ts back\n // to the user's env schema file, stripping the extension so TS can\n // resolve it.\n const envOutDir = dirname(envOutFile)\n let rel = relative(envOutDir, env.filePath).split(sep).join('/')\n rel = rel.replace(/\\.(ts|tsx|mts|cts)$/i, '')\n if (!rel.startsWith('.')) rel = './' + rel\n\n return `${HEADER}\n// Importing the schema as a type lets us infer its shape without\n// pulling in any runtime code. \\`Awaited<>\\` strips an accidental\n// Promise wrap on dynamic-imported defaults.\nimport type _envSchema from '${rel}'\n\n// Local type alias — interfaces can only \\`extend\\` an identifier,\n// not an inline import expression, so we resolve the schema's\n// inferred shape into a named type first.\ntype _KickEnvShape = import('zod').infer<typeof _envSchema>\n\ndeclare global {\n /**\n * Typed environment registry. Augmented from \\`${env.relativePath}\\`\n * so \\`@Value('PORT')\\`, \\`Env<'PORT'>\\`, and \\`process.env.PORT\\` are\n * all type-safe and autocomplete.\n */\n interface KickEnv extends _KickEnvShape {}\n\n // eslint-disable-next-line @typescript-eslint/no-namespace\n namespace NodeJS {\n /**\n * Narrow \\`process.env\\` so known keys exist as \\`string\\` (the raw\n * pre-Zod-coercion form). \\`@Value\\` and the \\`ConfigService\\` apply\n * the schema's transforms internally; access \\`process.env\\` directly\n * only when you need the raw string. Unknown keys still resolve to\n * \\`string | undefined\\` via the base @types/node declaration.\n */\n interface ProcessEnv extends Record<keyof KickEnv, string> {}\n }\n}\n\nexport {}\n`\n}\n\n/**\n * Render the `KickRoutes` global namespace augmentation. Each interface\n * inside corresponds to a controller class; each property is a single\n * route method on that controller, conforming to `RouteShape`.\n *\n * Fills `params` from URL patterns, `query` from `@ApiQueryParams`, and\n * `body`/`query`/`params` (when schema-validated) from the configured\n * schema validator. `response` is emitted as `unknown`.\n */\nfunction renderRoutes(\n routes: DiscoveredRoute[],\n routesOutFile: string,\n schemaValidator: 'zod' | false,\n): string {\n if (routes.length === 0) {\n return `${HEADER}\n// (no routes discovered yet — annotate a controller method with\n// @Get/@Post/@Put/@Delete/@Patch and re-run \\`kick typegen\\`)\ndeclare global {\n // eslint-disable-next-line @typescript-eslint/no-namespace\n namespace KickRoutes {}\n}\n\nexport {}\n`\n }\n\n // Group routes by controller for emission\n const byController = new Map<string, DiscoveredRoute[]>()\n for (const r of routes) {\n const arr = byController.get(r.controller) ?? []\n arr.push(r)\n byController.set(r.controller, arr)\n }\n\n // Hoisted schema imports — collected during interface rendering, then\n // emitted at the top of `routes.ts` so the in-namespace type references\n // resolve correctly. (Inline `import('...').X` inside `.d.ts` files\n // silently degrades to `unknown` with `moduleResolution: 'bundler'`.)\n const schemaImports = new Map<string, { identifier: string; specifier: string }>()\n\n const renderField = (\n schema: { identifier: string; source: string | null } | null,\n routeFilePath: string,\n ): string | null => {\n const alias = planSchemaImport(\n schema,\n routeFilePath,\n routesOutFile,\n schemaValidator,\n schemaImports,\n )\n return alias ? `import('zod').infer<typeof ${alias}>` : null\n }\n\n const interfaces: string[] = []\n for (const [controller, methods] of byController) {\n const lines: string[] = [` interface ${controller} {`]\n for (const m of methods) {\n // Empty `{}` (rather than `Record<string, never>`) so that accessing\n // an unknown property on a paramless route is a type error in strict\n // mode. `Record<string, never>` returns `never` for any access which\n // unfortunately is assignable to anything and silently passes.\n const urlParamsType =\n m.pathParams.length > 0 ? `{ ${m.pathParams.map((p) => `${p}: string`).join('; ')} }` : '{}'\n\n // Schema-driven types win over the URL-pattern / `unknown` defaults\n // when the user has wired a schema in the route decorator.\n const bodySchemaType = renderField(m.bodySchema, m.filePath)\n const querySchemaType = renderField(m.querySchema, m.filePath)\n const paramsSchemaType = renderField(m.paramsSchema, m.filePath)\n\n const paramsType = paramsSchemaType ?? urlParamsType\n const bodyType = bodySchemaType ?? 'unknown'\n const queryType = querySchemaType ?? renderQueryShape(m)\n const docLines = renderQueryDocLines(m)\n lines.push(\n ` /**`,\n ` * ${m.httpMethod} ${m.path}`,\n ...docLines.map((d) => ` * ${d}`),\n ` */`,\n ` ${m.method}: {`,\n ` params: ${paramsType}`,\n ` body: ${bodyType}`,\n ` query: ${queryType}`,\n ` response: unknown`,\n ` }`,\n )\n }\n lines.push(' }')\n interfaces.push(lines.join('\\n'))\n }\n\n const importBlock = renderSchemaImports(schemaImports)\n const interfaceBlock = interfaces.join('\\n')\n\n return `${HEADER}${importBlock}\ndeclare global {\n // eslint-disable-next-line @typescript-eslint/no-namespace\n namespace KickRoutes {\n${interfaceBlock}\n }\n}\n\nexport {}\n`\n}\n\n/** Result of a typegen run — useful for logging and tests */\nexport interface GenerateResult {\n /** Number of registry entries written */\n registryEntries: number\n /** Number of service tokens (classes + createToken + @Inject literals) */\n serviceTokens: number\n /** Number of module tokens written */\n moduleTokens: number\n /** Number of route entries written into KickRoutes */\n routeEntries: number\n /** Whether a typed env augmentation was emitted */\n envWritten: boolean\n /** Files that were written */\n written: string[]\n /** Number of collisions that were auto-namespaced (only > 0 with allowDuplicates) */\n resolvedCollisions: number\n}\n\n/** Options for `generateTypes` */\nexport interface GenerateOptions {\n /** Discovered classes from the scanner */\n classes: DiscoveredClass[]\n /** Discovered route handlers from the scanner */\n routes?: DiscoveredRoute[]\n /** Discovered `createToken('name')` calls */\n tokens?: DiscoveredToken[]\n /** Discovered `@Inject('literal')` calls */\n injects?: DiscoveredInject[]\n /** Detected duplicate class names from the scanner */\n collisions?: ClassCollision[]\n /** Discovered env schema file (or null if none) */\n env?: DiscoveredEnv | null\n /** Output directory (typically `<cwd>/.kickjs/types`) */\n outDir: string\n /**\n * When `true`, colliding class names are auto-namespaced by file path\n * instead of throwing. Default: `false`.\n */\n allowDuplicates?: boolean\n /**\n * Schema validator the project uses. When `'zod'`, the generator\n * emits `z.infer<typeof import('...').schema>` for any route whose\n * decorator declared a body/query/params schema identifier that\n * could be statically resolved. When `false` (or omitted), schemas\n * are ignored and `body`/`query`/`params` keep their `unknown`\n * placeholders.\n *\n * Future: `'joi'`, `'yup'`, `'json-schema'`, custom adapters.\n */\n schemaValidator?: 'zod' | false\n}\n\n/** Write all generated `.d.ts` files to `outDir` */\nexport async function generateTypes(opts: GenerateOptions): Promise<GenerateResult> {\n const {\n classes,\n routes = [],\n tokens = [],\n injects = [],\n collisions = [],\n env = null,\n outDir,\n allowDuplicates = false,\n schemaValidator = false,\n } = opts\n\n if (collisions.length > 0 && !allowDuplicates) {\n throw new TokenCollisionError(collisions)\n }\n\n await mkdir(outDir, { recursive: true })\n\n const registryFile = join(outDir, 'registry.d.ts')\n const servicesFile = join(outDir, 'services.d.ts')\n const modulesFile = join(outDir, 'modules.d.ts')\n // routes.ts (NOT .d.ts) — TypeScript silently degrades top-level\n // imports inside `.d.ts` files to `unknown` when the user's tsconfig\n // has `moduleResolution: 'bundler'`. Emitting as a regular `.ts` file\n // makes the schema imports resolve correctly so `z.infer<typeof X>`\n // produces a proper type. The file contains only type declarations\n // so it has zero runtime impact.\n const routesFile = join(outDir, 'routes.ts')\n // env.ts (same .ts vs .d.ts story as routes.ts)\n const envFile = join(outDir, 'env.ts')\n const indexFile = join(outDir, 'index.d.ts')\n\n const collidingNames = new Set(collisions.map((c) => c.className))\n const registryContent = renderRegistry(classes, registryFile, collidingNames)\n\n // ServiceToken union — combines class names, createToken literals, and\n // @Inject literals so tooling autocomplete sees every known token.\n const classTokens = classes\n .filter((c) => REGISTRY_DECORATORS.has(c.decorator))\n .map((c) => (collidingNames.has(c.className) ? namespacedKeyFor(c) : c.className))\n const tokenLiterals = tokens.map((t) => t.name)\n const injectLiterals = injects.map((i) => i.name)\n const allServices = [...classTokens, ...tokenLiterals, ...injectLiterals]\n\n const modules = classes.filter((c) => c.decorator === 'Module').map((c) => c.className)\n\n const servicesContent = renderUnion(\n 'ServiceToken',\n allServices,\n '(no tokens discovered — declare with createToken<T>() or `kick g service <name>`)',\n )\n const modulesContent = renderUnion(\n 'ModuleToken',\n modules,\n '(no @Module classes discovered — `kick g module <name>` to add one)',\n )\n const routesContent = renderRoutes(routes, routesFile, schemaValidator)\n const envContent = renderEnv(env, envFile)\n const indexContent = renderIndex(envContent !== null)\n\n await writeFile(registryFile, registryContent, 'utf-8')\n await writeFile(servicesFile, servicesContent, 'utf-8')\n await writeFile(modulesFile, modulesContent, 'utf-8')\n await writeFile(routesFile, routesContent, 'utf-8')\n await writeFile(indexFile, indexContent, 'utf-8')\n\n const written = [registryFile, servicesFile, modulesFile, routesFile, indexFile]\n if (envContent) {\n await writeFile(envFile, envContent, 'utf-8')\n written.push(envFile)\n }\n\n // Write `.gitignore` at the .kickjs root (one level up from outDir)\n const kickjsRoot = dirname(outDir)\n await writeFile(join(kickjsRoot, '.gitignore'), '# Auto-generated by kick typegen\\n*\\n', 'utf-8')\n\n return {\n registryEntries: classTokens.length,\n serviceTokens: new Set(allServices).size,\n moduleTokens: modules.length,\n routeEntries: routes.length,\n envWritten: envContent !== null,\n written,\n resolvedCollisions: collisions.length,\n }\n}\n","/**\n * Public entry point for the KickJS typegen module.\n *\n * Used by:\n * - `kick typegen` (one-shot or watch mode)\n * - `kick dev` (auto-runs once before Vite starts; refreshes when files change)\n *\n * @module @forinda/kickjs-cli/typegen\n */\n\nimport { resolve } from 'node:path'\nimport { scanProject, type ScanResult } from './scanner'\nimport { generateTypes, type GenerateResult, TokenCollisionError } from './generator'\n\nexport type {\n DiscoveredClass,\n DiscoveredToken,\n DiscoveredInject,\n DiscoveredEnv,\n ClassCollision,\n ScanResult,\n} from './scanner'\nexport type { GenerateResult } from './generator'\nexport { TokenCollisionError } from './generator'\n\n/** Options for `runTypegen` */\nexport interface RunTypegenOptions {\n /** Project root (defaults to `process.cwd()`) */\n cwd?: string\n /** Source directory to scan (defaults to `src`) */\n srcDir?: string\n /** Output directory (defaults to `.kickjs/types`) */\n outDir?: string\n /** Suppress console output */\n silent?: boolean\n /**\n * When `true`, duplicate class names are auto-namespaced by file path\n * instead of throwing. `kick dev` enables this so the dev server is\n * never blocked by an in-progress rename. CLI default is `false` so\n * `kick typegen` (and CI) catches collisions early. */\n allowDuplicates?: boolean\n /**\n * Schema validator used to derive `body`/`query`/`params` types from\n * route metadata. Currently only `'zod'` is supported; `false` (the\n * default) leaves these fields as `unknown`. Loaded from\n * `kick.config.ts` `typegen.schemaValidator` when invoked via the CLI.\n */\n schemaValidator?: 'zod' | false\n /**\n * Path to the env schema file (relative to `cwd`). The file must\n * default-export a `defineEnv(...)` schema for the typed `KickEnv`\n * augmentation to be emitted. Defaults to `'src/env.ts'`. Set to\n * `false` to disable env typing entirely.\n */\n envFile?: string | false\n}\n\n/** Resolve options to absolute paths */\nfunction resolveOptions(opts: RunTypegenOptions): {\n cwd: string\n srcDir: string\n outDir: string\n silent: boolean\n allowDuplicates: boolean\n schemaValidator: 'zod' | false\n envFile: string | false\n} {\n const cwd = opts.cwd ?? process.cwd()\n return {\n cwd,\n srcDir: resolve(cwd, opts.srcDir ?? 'src'),\n outDir: resolve(cwd, opts.outDir ?? '.kickjs/types'),\n silent: opts.silent ?? false,\n allowDuplicates: opts.allowDuplicates ?? false,\n schemaValidator: opts.schemaValidator ?? false,\n envFile: opts.envFile ?? 'src/env.ts',\n }\n}\n\n/**\n * Run a single typegen pass: scan source files, generate `.d.ts` files.\n *\n * Returns the discovered scan result alongside the generation result so\n * callers (`kick dev`, devtools) can log them or feed them to other tools.\n *\n * Throws `TokenCollisionError` if duplicate class names are found and\n * `allowDuplicates` is false.\n */\nexport async function runTypegen(opts: RunTypegenOptions = {}): Promise<{\n scan: ScanResult\n result: GenerateResult\n}> {\n const { cwd, srcDir, outDir, silent, allowDuplicates, schemaValidator, envFile } =\n resolveOptions(opts)\n\n const start = Date.now()\n const scan = await scanProject({\n root: srcDir,\n cwd,\n // Pass through unless explicitly disabled\n envFile: envFile === false ? undefined : envFile,\n })\n const result = await generateTypes({\n classes: scan.classes,\n routes: scan.routes,\n tokens: scan.tokens,\n injects: scan.injects,\n collisions: scan.collisions,\n env: envFile === false ? null : scan.env,\n outDir,\n allowDuplicates,\n schemaValidator,\n })\n const elapsed = Date.now() - start\n\n if (!silent) {\n const where = outDir.replace(cwd + '/', '')\n const collisionNote =\n result.resolvedCollisions > 0 ? `, ${result.resolvedCollisions} collisions namespaced` : ''\n const envNote = result.envWritten ? ', env typed' : ''\n console.log(\n ` kick typegen → ${result.serviceTokens} services, ${result.routeEntries} routes, ${result.moduleTokens} modules${envNote}${collisionNote} → ${where} (${elapsed}ms)`,\n )\n }\n\n return { scan, result }\n}\n\n/**\n * Watch mode for `kick typegen --watch`.\n *\n * Uses Node's built-in `fs.watch` (recursive, available on Linux 22+ and\n * macOS 19+). Falls back gracefully if recursive watch is not supported.\n *\n * Debounces re-runs by 100ms so a bulk file change (e.g. `kick g module`\n * creating 5 files at once) emits one regen, not five.\n *\n * In watch mode collisions are reported but never thrown — the watcher\n * keeps running so the user can fix the rename and the next scan\n * recovers automatically.\n *\n * Returns a `stop()` function that closes the watcher.\n */\nexport async function watchTypegen(opts: RunTypegenOptions = {}): Promise<() => void> {\n const resolved = resolveOptions(opts)\n const { srcDir, silent } = resolved\n // Watch mode always tolerates collisions — otherwise an in-progress\n // rename would crash the dev loop. The error is still printed.\n const runOpts: RunTypegenOptions = { ...resolved, allowDuplicates: true }\n\n // Initial run\n await safeRun(runOpts, silent)\n\n const { watch } = await import('node:fs')\n\n let timer: ReturnType<typeof setTimeout> | null = null\n const trigger = (filename: string | null) => {\n // Only react to TypeScript source changes; ignore everything else\n if (!filename) return\n if (!/\\.(ts|tsx|mts|cts)$/.test(filename)) return\n if (filename.includes('.kickjs')) return\n if (filename.endsWith('.d.ts')) return\n\n if (timer) clearTimeout(timer)\n timer = setTimeout(() => {\n safeRun(runOpts, silent)\n }, 100)\n }\n\n let watcher: ReturnType<typeof watch>\n try {\n watcher = watch(srcDir, { recursive: true }, (_event, filename) => {\n trigger(filename)\n })\n } catch (err: any) {\n if (!silent) {\n console.warn(\n ` kick typegen: watch mode unavailable (${err?.message ?? err}). Falling back to polling.`,\n )\n }\n // Polling fallback — re-scan every 2s\n const interval = setInterval(() => {\n safeRun({ ...runOpts, silent: true }, true)\n }, 2000)\n return () => clearInterval(interval)\n }\n\n return () => {\n if (timer) clearTimeout(timer)\n watcher.close()\n }\n}\n\n/** Run typegen swallowing errors so the watcher loop never dies */\nasync function safeRun(opts: RunTypegenOptions, silent: boolean): Promise<void> {\n try {\n await runTypegen(opts)\n } catch (err) {\n if (silent) return\n if (err instanceof TokenCollisionError) {\n console.error('\\n' + err.message + '\\n')\n } else {\n const msg = err instanceof Error ? err.message : String(err)\n console.error(` kick typegen failed: ${msg}`)\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"typegen-CsTiIxmu.mjs","names":[],"sources":["../src/typegen/scanner.ts","../src/typegen/generator.ts","../src/typegen/index.ts"],"sourcesContent":["/**\n * Static scanner for KickJS decorated classes and DI tokens.\n *\n * Walks `src/**\\/*.ts` (excluding tests and node_modules) and extracts:\n *\n * - Decorated classes (`@Service`, `@Controller`, `@Repository`, etc.)\n * - `createToken<T>('name')` definitions\n * - `@Inject('literal')` calls\n *\n * The output feeds the type generator, which emits `.kickjs/types/*.d.ts`\n * files used by the user's tsc to make `container.resolve()` and module\n * discovery type-safe.\n *\n * This is intentionally regex-based (not AST-based) to avoid the\n * ts-morph / typescript compiler dependency. Pattern from\n * `packages/vite/src/module-discovery.ts` which already uses regex\n * to detect `*.module.ts` exports.\n *\n * ## Collision detection\n *\n * Two classes with the same name across different files is a collision.\n * The scanner records all collisions in `ScanResult.collisions` so the\n * caller (generator) can decide whether to hard-error or auto-namespace.\n *\n * @module @forinda/kickjs-cli/typegen/scanner\n */\n\nimport type { Dirent } from 'node:fs'\nimport { readdir, readFile } from 'node:fs/promises'\nimport { join, relative, resolve, sep } from 'node:path'\n\n/** Decorators that mark a class as DI-managed */\nexport const DECORATOR_NAMES = [\n 'Service',\n 'Controller',\n 'Repository',\n 'Injectable',\n 'Component',\n 'Module',\n] as const\n\nexport type DecoratorName = (typeof DECORATOR_NAMES)[number]\n\n/** A single discovered decorated class */\nexport interface DiscoveredClass {\n /** Class name (e.g., 'UserService') */\n className: string\n /** Decorator that marked it (e.g., 'Service') */\n decorator: DecoratorName\n /** Absolute file path */\n filePath: string\n /** Path relative to scan root, with forward slashes */\n relativePath: string\n /** True if exported as `default` */\n isDefault: boolean\n}\n\n/** A single route handler discovered on a controller class */\nexport interface DiscoveredRoute {\n /** Owning controller class name (e.g. 'UserController') */\n controller: string\n /** Handler method name on the controller (e.g. 'getUser') */\n method: string\n /** HTTP verb (uppercase) */\n httpMethod: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'\n /** Route path including parameter placeholders (e.g. '/:id/posts/:postId') */\n path: string\n /** URL path parameter names extracted from `:placeholder` segments */\n pathParams: string[]\n /**\n * Whitelisted query field names extracted from `@ApiQueryParams({...})`.\n * `null` means no `@ApiQueryParams` was found on this method (so the\n * generator emits an unconstrained `query` shape). An empty array means\n * the decorator existed but no fields could be statically extracted\n * (e.g. an opaque imported config).\n */\n queryFilterable: string[] | null\n querySortable: string[] | null\n querySearchable: string[] | null\n /**\n * Schema identifiers referenced from the route decorator's second arg\n * (e.g. `@Post('/', { body: createTaskSchema })`). `null` means no\n * such reference; the value carries the identifier and the resolved\n * import source (relative module path) if known.\n */\n bodySchema: SchemaRef | null\n querySchema: SchemaRef | null\n paramsSchema: SchemaRef | null\n /** Absolute file path of the controller */\n filePath: string\n /** Path relative to scan root, with forward slashes */\n relativePath: string\n}\n\n/** A statically-resolved schema identifier reference */\nexport interface SchemaRef {\n /** The identifier as written (e.g. `createTaskSchema`) */\n identifier: string\n /**\n * Resolved module specifier (relative path or bare module name) where\n * the identifier is defined. `null` means the source could not be\n * statically determined (the generator falls back to `unknown`).\n */\n source: string | null\n}\n\n/** A `createToken<T>('name')` call discovered in source */\nexport interface DiscoveredToken {\n /** The literal string passed to `createToken()` */\n name: string\n /** The const variable name on the LHS, if any */\n variable: string | null\n /** Absolute file path */\n filePath: string\n /** Path relative to scan root, with forward slashes */\n relativePath: string\n}\n\n/** An `@Inject('literal')` call discovered in source */\nexport interface DiscoveredInject {\n /** The literal string passed to `@Inject()` */\n name: string\n /** Absolute file path */\n filePath: string\n /** Path relative to scan root, with forward slashes */\n relativePath: string\n}\n\n/** A name collision — same class name in two or more files */\nexport interface ClassCollision {\n /** The colliding class name */\n className: string\n /** All files declaring the class */\n classes: DiscoveredClass[]\n}\n\n/**\n * Information about a discovered env schema file. The typegen\n * generator uses this to emit a `KickEnv` + `NodeJS.ProcessEnv`\n * augmentation that flows through to `@Value` and `process.env`.\n *\n * `null` means no env file was found at the configured location.\n */\nexport interface DiscoveredEnv {\n /** Absolute path to the env schema file */\n filePath: string\n /** Path relative to scan root, with forward slashes */\n relativePath: string\n}\n\n/** Aggregated scanner output */\nexport interface ScanResult {\n classes: DiscoveredClass[]\n routes: DiscoveredRoute[]\n tokens: DiscoveredToken[]\n injects: DiscoveredInject[]\n collisions: ClassCollision[]\n /** Discovered env schema file (or null if none found at the configured path) */\n env: DiscoveredEnv | null\n}\n\n/** Options for the scanner */\nexport interface ScanOptions {\n /** Root directory to scan (e.g., absolute path to `src`) */\n root: string\n /** Project root used to compute relative paths (e.g., process.cwd()) */\n cwd: string\n /** Glob-like extensions to scan */\n extensions?: string[]\n /** Substrings that exclude a path (matched against relative path) */\n exclude?: string[]\n /**\n * Path to the env schema file, relative to `cwd`. Defaults to\n * `'src/env.ts'`. The file must contain a `defineEnv(...)` call\n * with a default export for the typegen to emit a typed `KickEnv`\n * augmentation. If the file does not exist or doesn't match the\n * expected shape, env typing is skipped silently.\n */\n envFile?: string\n}\n\nconst DEFAULT_EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts']\nconst DEFAULT_EXCLUDES = ['node_modules', '.kickjs', 'dist', 'build', '.test.', '.spec.', '.d.ts']\n\n/**\n * Match a class-level decorator immediately followed by an exported\n * class declaration. Captures decorator name and class name.\n */\nconst DECORATED_CLASS_REGEX = new RegExp(\n String.raw`@(${DECORATOR_NAMES.join('|')})\\s*\\([^)]*\\)` +\n String.raw`(?:\\s*@[A-Z]\\w*(?:\\s*\\([^)]*\\))?)*` +\n String.raw`\\s*export\\s+(default\\s+)?(?:abstract\\s+)?class\\s+(\\w+)`,\n 'g',\n)\n\n/**\n * Match a `createToken<T>('name')` call with optional `export const X =`\n * or `const X =` prefix. Tolerates whitespace and the type parameter\n * being absent (`createToken('name')`).\n */\nconst CREATE_TOKEN_REGEX =\n /(?:export\\s+)?const\\s+(\\w+)\\s*(?::\\s*[^=]+)?=\\s*createToken\\s*(?:<[^>]*>)?\\s*\\(\\s*['\"`]([^'\"`]+)['\"`]\\s*\\)/g\n\n/**\n * Match a bare `createToken<T>('name')` call (no const assignment) so\n * we still pick up dynamically-used tokens.\n */\nconst BARE_CREATE_TOKEN_REGEX = /createToken\\s*(?:<[^>]*>)?\\s*\\(\\s*['\"`]([^'\"`]+)['\"`]\\s*\\)/g\n\n/** Match `@Inject('literal')` — only literals; computed args are skipped */\nconst INJECT_LITERAL_REGEX = /@Inject\\s*\\(\\s*['\"`]([^'\"`]+)['\"`]\\s*\\)/g\n\n/** HTTP route decorator names recognised by the scanner */\nconst HTTP_DECORATORS = ['Get', 'Post', 'Put', 'Delete', 'Patch'] as const\n\n/**\n * Match a route decorator immediately followed by a method declaration.\n * Captures the HTTP verb, path literal (or empty), and method name.\n *\n * Tolerates:\n * - Optional second arg to the route decorator (`@Get('/path', { ... })`)\n * - Stacked decorators between the route and the method (`@Get('/') @Use(...)`)\n * - Path-less decorators (`@Get()` → defaults to `/`)\n * - `async` modifier on the method\n *\n * Run within a class body slice (see extractRoutesFromSource) so the\n * captured method name is unambiguously a method on that class.\n */\nconst ROUTE_METHOD_REGEX = new RegExp(\n String.raw`@(${HTTP_DECORATORS.join('|')})\\s*\\(` +\n String.raw`(?:\\s*['\"\\`]([^'\"\\`]*)['\"\\`])?[^)]*\\)` +\n String.raw`(?:\\s*@[A-Z]\\w*(?:\\s*\\([^)]*\\))?)*` +\n String.raw`\\s*(?:public\\s+|private\\s+|protected\\s+)?(?:async\\s+)?` +\n String.raw`([a-zA-Z_]\\w*)\\s*\\(`,\n 'g',\n)\n\n/** Extract `:placeholder` segments from an Express route path */\nfunction extractPathParams(path: string): string[] {\n const matches = path.match(/:([a-zA-Z_]\\w*)/g) ?? []\n return matches.map((m) => m.slice(1))\n}\n\n/**\n * Given the matched text of a route decorator + method declaration, return\n * the substring inside the route decorator's argument list (between the\n * outermost `(` and `)`). Returns `null` if no parens are found.\n *\n * Example input:\n * `@Post('/', { body: createTaskSchema, name: 'CreateTask' }) async create(`\n * Returns:\n * `'/', { body: createTaskSchema, name: 'CreateTask' }`\n */\nfunction extractRouteOptionsArg(matchedText: string): string | null {\n const open = matchedText.indexOf('(')\n if (open < 0) return null\n let depth = 1\n for (let i = open + 1; i < matchedText.length; i++) {\n const ch = matchedText[i]\n if (ch === '(') depth++\n else if (ch === ')') {\n depth--\n if (depth === 0) return matchedText.slice(open + 1, i)\n }\n }\n return null\n}\n\n/**\n * Extract a bare identifier value from a single field in an object literal\n * embedded in a string. Returns `null` if the field is missing or its value\n * isn't a bare identifier (e.g. an inline object, function call, etc.).\n *\n * Example: `extractObjectFieldIdentifier(\"'/' , { body: createTaskSchema }\", 'body')`\n * returns `'createTaskSchema'`.\n */\nfunction extractObjectFieldIdentifier(text: string, field: string): string | null {\n // Look for `field: <identifier>` not followed by `(` (function call) or `{` (inline object)\n const re = new RegExp(String.raw`\\b${field}\\s*:\\s*([A-Za-z_$][\\w$]*)`, 'g')\n const m = re.exec(text)\n if (!m) return null\n return m[1]\n}\n\n/**\n * Resolve a bare identifier to its module source by inspecting the file's\n * top-level imports and same-file `const` declarations.\n *\n * - `import { X } from './path'` → returns `'./path'`\n * - `import X from './path'` (default import) → returns `'./path'`\n * - `import * as X from './path'` → returns `'./path'`\n * - `const X = z.object(...)` (same file) → returns `null` (caller emits a self-import)\n *\n * Returns `null` when the identifier cannot be resolved.\n */\nfunction resolveImportSource(source: string, identifier: string): string | null {\n // Named import: `import { X, Y as Z } from './path'`\n const namedRe = new RegExp(\n String.raw`import\\s*(?:type\\s+)?\\{[^}]*\\b${identifier}\\b[^}]*\\}\\s*from\\s*['\"\\`]([^'\"\\`]+)['\"\\`]`,\n )\n const named = namedRe.exec(source)\n if (named) return named[1]\n\n // Default import: `import X from './path'`\n const defaultRe = new RegExp(\n String.raw`import\\s+(?:type\\s+)?${identifier}\\s+from\\s*['\"\\`]([^'\"\\`]+)['\"\\`]`,\n )\n const def = defaultRe.exec(source)\n if (def) return def[1]\n\n // Namespace import: `import * as X from './path'`\n const nsRe = new RegExp(\n String.raw`import\\s*\\*\\s*as\\s+${identifier}\\s+from\\s*['\"\\`]([^'\"\\`]+)['\"\\`]`,\n )\n const ns = nsRe.exec(source)\n if (ns) return ns[1]\n\n // Same-file const declaration — return empty string as a sentinel meaning\n // \"current file\". The generator turns this into a self-relative reference.\n const constRe = new RegExp(String.raw`(?:^|\\n)\\s*(?:export\\s+)?const\\s+${identifier}\\b`)\n if (constRe.test(source)) return ''\n\n return null\n}\n\n/**\n * Extract whitelist arrays from an `@ApiQueryParams(...)` decorator\n * within `decoratorBlock`. Handles two forms:\n *\n * - Inline literal: `@ApiQueryParams({ filterable: ['a', 'b'], ... })`\n * - Const reference: `@ApiQueryParams(SOME_CONFIG)` — looks up\n * `const SOME_CONFIG = { ... }` in the same file (`fullSource`).\n *\n * Returns `null` if no `@ApiQueryParams` is present. Returns\n * `{ filterable: [], sortable: [], searchable: [] }` if the decorator\n * is present but no fields could be statically extracted (opaque\n * imports, column-object configs, function calls, etc.).\n */\nfunction extractApiQueryParams(\n decoratorBlock: string,\n fullSource: string,\n): { filterable: string[]; sortable: string[]; searchable: string[] } | null {\n const apiMatch = /@ApiQueryParams\\s*\\(\\s*([\\s\\S]*?)\\s*\\)\\s*$/.exec(decoratorBlock)\n if (!apiMatch) {\n // Try without anchoring to the end (decorator may not be the last in the block)\n const loose = /@ApiQueryParams\\s*\\(([\\s\\S]*?)\\)/.exec(decoratorBlock)\n if (!loose) return null\n return parseApiQueryParamsArg(loose[1].trim(), fullSource)\n }\n return parseApiQueryParamsArg(apiMatch[1].trim(), fullSource)\n}\n\nfunction parseApiQueryParamsArg(\n arg: string,\n fullSource: string,\n): { filterable: string[]; sortable: string[]; searchable: string[] } {\n // Inline literal — starts with `{`\n if (arg.startsWith('{')) {\n return parseInlineConfigLiteral(arg)\n }\n // Const reference — bare identifier (possibly with type assertion)\n const idMatch = /^([A-Za-z_]\\w*)/.exec(arg)\n if (idMatch) {\n const ident = idMatch[1]\n // Look for `const IDENT = { ... }` in the same source file\n const constRe = new RegExp(\n String.raw`const\\s+${ident}\\s*(?::\\s*[^=]+)?=\\s*(\\{[\\s\\S]*?\\n\\})`,\n 'm',\n )\n const constMatch = constRe.exec(fullSource)\n if (constMatch) {\n return parseInlineConfigLiteral(constMatch[1])\n }\n }\n // Fallback: decorator present but extraction failed\n return { filterable: [], sortable: [], searchable: [] }\n}\n\n/** Extract a string array literal for one config key from an inline object literal */\nfunction extractStringArray(literal: string, key: string): string[] {\n const re = new RegExp(String.raw`${key}\\s*:\\s*\\[([\\s\\S]*?)\\]`)\n const m = re.exec(literal)\n if (!m) return []\n return Array.from(m[1].matchAll(/['\"`]([^'\"`]+)['\"`]/g)).map((x) => x[1])\n}\n\n/** Parse an inline `{ filterable: [...], sortable: [...], searchable: [...] }` literal */\nfunction parseInlineConfigLiteral(literal: string): {\n filterable: string[]\n sortable: string[]\n searchable: string[]\n} {\n return {\n filterable: extractStringArray(literal, 'filterable'),\n sortable: extractStringArray(literal, 'sortable'),\n searchable: extractStringArray(literal, 'searchable'),\n }\n}\n\n/** Recursively walk a directory and yield matching file paths */\nasync function walk(dir: string, opts: ScanOptions): Promise<string[]> {\n const exts = opts.extensions ?? DEFAULT_EXTENSIONS\n const excludes = opts.exclude ?? DEFAULT_EXCLUDES\n const out: string[] = []\n\n let entries: Dirent[]\n try {\n entries = (await readdir(dir, { withFileTypes: true, encoding: 'utf-8' })) as Dirent[]\n } catch {\n return out\n }\n\n for (const entry of entries) {\n const full = join(dir, entry.name)\n const rel = relative(opts.cwd, full)\n\n if (excludes.some((ex) => rel.includes(ex))) continue\n\n if (entry.isDirectory()) {\n out.push(...(await walk(full, opts)))\n } else if (entry.isFile()) {\n if (exts.some((ext) => entry.name.endsWith(ext))) {\n out.push(full)\n }\n }\n }\n\n return out\n}\n\n/** Compute the forward-slash relative path used in scanner output */\nfunction toRelative(filePath: string, cwd: string): string {\n return relative(cwd, filePath).split(sep).join('/')\n}\n\n/** Extract decorated classes from a single source file */\nexport function extractClassesFromSource(\n source: string,\n filePath: string,\n cwd: string,\n): DiscoveredClass[] {\n const out: DiscoveredClass[] = []\n const relPath = toRelative(filePath, cwd)\n\n DECORATED_CLASS_REGEX.lastIndex = 0\n let match: RegExpExecArray | null\n while ((match = DECORATED_CLASS_REGEX.exec(source)) !== null) {\n const [, decorator, defaultMarker, className] = match\n out.push({\n className,\n decorator: decorator as DecoratorName,\n filePath,\n relativePath: relPath,\n isDefault: Boolean(defaultMarker),\n })\n }\n\n return out\n}\n\n/** Extract `createToken('name')` definitions from a single source file */\nexport function extractTokensFromSource(\n source: string,\n filePath: string,\n cwd: string,\n): DiscoveredToken[] {\n const out: DiscoveredToken[] = []\n const relPath = toRelative(filePath, cwd)\n const seen = new Set<string>()\n\n // First pass: const-bound tokens (preferred — we get the variable name)\n CREATE_TOKEN_REGEX.lastIndex = 0\n let match: RegExpExecArray | null\n while ((match = CREATE_TOKEN_REGEX.exec(source)) !== null) {\n const [full, variable, name] = match\n seen.add(full)\n out.push({ name, variable, filePath, relativePath: relPath })\n }\n\n // Second pass: bare calls not captured above (rare but possible)\n BARE_CREATE_TOKEN_REGEX.lastIndex = 0\n while ((match = BARE_CREATE_TOKEN_REGEX.exec(source)) !== null) {\n if (seen.has(match[0])) continue\n out.push({\n name: match[1],\n variable: null,\n filePath,\n relativePath: relPath,\n })\n }\n\n return out\n}\n\n/**\n * Extract route handlers from a source file.\n *\n * For each decorated class in `classesInFile`, slices the source from\n * the class declaration to the next class (or EOF) and runs the route\n * decorator regex within that slice. The result is a list of routes\n * tagged with their owning controller.\n *\n * Heuristic note: this assumes classes are not nested. KickJS controllers\n * are top-level by convention so this holds in practice.\n */\nexport function extractRoutesFromSource(\n source: string,\n filePath: string,\n cwd: string,\n classesInFile: DiscoveredClass[],\n): DiscoveredRoute[] {\n const out: DiscoveredRoute[] = []\n if (classesInFile.length === 0) return out\n const relPath = toRelative(filePath, cwd)\n\n // Locate each class declaration's offset in the source\n const positions: Array<{ cls: DiscoveredClass; start: number }> = []\n for (const cls of classesInFile) {\n const re = new RegExp(String.raw`class\\s+${cls.className}\\b`)\n const m = re.exec(source)\n if (m?.index !== undefined) {\n positions.push({ cls, start: m.index })\n }\n }\n positions.sort((a, b) => a.start - b.start)\n\n for (let i = 0; i < positions.length; i++) {\n const { cls, start } = positions[i]\n const end = i + 1 < positions.length ? positions[i + 1].start : source.length\n const block = source.slice(start, end)\n\n ROUTE_METHOD_REGEX.lastIndex = 0\n let match: RegExpExecArray | null\n while ((match = ROUTE_METHOD_REGEX.exec(block)) !== null) {\n const [matchedText, verb, pathLiteral, methodName] = match\n const path = pathLiteral && pathLiteral.length > 0 ? pathLiteral : '/'\n\n // The route regex already greedily matched any stacked decorators\n // BETWEEN the route decorator and the method declaration. Inspect\n // the matched substring for an `@ApiQueryParams(...)` call.\n const apiQp = extractApiQueryParams(matchedText, source)\n\n // The route decorator's second argument carries body/query/params\n // schema references. Extract them from the leading slice of the\n // matched text (the part before any stacked decorators).\n const routeArgs = extractRouteOptionsArg(matchedText)\n const bodyId = routeArgs ? extractObjectFieldIdentifier(routeArgs, 'body') : null\n const queryId = routeArgs ? extractObjectFieldIdentifier(routeArgs, 'query') : null\n const paramsId = routeArgs ? extractObjectFieldIdentifier(routeArgs, 'params') : null\n\n out.push({\n controller: cls.className,\n method: methodName,\n httpMethod: verb.toUpperCase() as DiscoveredRoute['httpMethod'],\n path,\n pathParams: extractPathParams(path),\n queryFilterable: apiQp?.filterable ?? null,\n querySortable: apiQp?.sortable ?? null,\n querySearchable: apiQp?.searchable ?? null,\n bodySchema: bodyId\n ? { identifier: bodyId, source: resolveImportSource(source, bodyId) }\n : null,\n querySchema: queryId\n ? { identifier: queryId, source: resolveImportSource(source, queryId) }\n : null,\n paramsSchema: paramsId\n ? { identifier: paramsId, source: resolveImportSource(source, paramsId) }\n : null,\n filePath,\n relativePath: relPath,\n })\n }\n }\n\n return out\n}\n\n/** Extract `@Inject('literal')` calls from a single source file */\nexport function extractInjectsFromSource(\n source: string,\n filePath: string,\n cwd: string,\n): DiscoveredInject[] {\n const out: DiscoveredInject[] = []\n const relPath = toRelative(filePath, cwd)\n\n INJECT_LITERAL_REGEX.lastIndex = 0\n let match: RegExpExecArray | null\n while ((match = INJECT_LITERAL_REGEX.exec(source)) !== null) {\n out.push({ name: match[1], filePath, relativePath: relPath })\n }\n\n return out\n}\n\n/**\n * Default search order for the env schema file. Newer projects keep\n * the schema under `src/config/` so the framework's \"config\" concept\n * has a single home; older scaffolds dropped it at `src/env.ts` (kept\n * here for back-compat). The first match wins.\n */\nconst DEFAULT_ENV_FILE_CANDIDATES = [\n 'src/config/index.ts',\n 'src/config/env.ts',\n 'src/config.ts',\n 'src/env.ts',\n] as const\n\n/**\n * Look for an env schema file. When `envFile` is the string default\n * (`'src/env.ts'`) or omitted, every entry in `DEFAULT_ENV_FILE_CANDIDATES`\n * is tried in order. When the caller passes an explicit path, only that\n * path is tried (so projects can opt out of the search by setting\n * `kick.config.ts → typegen.envFile`).\n *\n * Returns a `DiscoveredEnv` if the file exists and contains both a\n * `defineEnv(...)` call and a default export — the two markers we\n * need before it's safe to emit `import type schema from '...'` in\n * the generator. Returns `null` for any other state (no candidate\n * found, no defineEnv, no default export) so the generator skips env\n * typing silently.\n */\nexport async function detectEnvFile(cwd: string, envFile: string): Promise<DiscoveredEnv | null> {\n // The CLI passes the literal default `'src/env.ts'` when the user\n // hasn't overridden it. Treat that as \"use the search list\" rather\n // than pinning to one path, so newer scaffolds at src/config/ keep\n // working without forcing every project to set typegen.envFile.\n const candidates: readonly string[] =\n envFile === 'src/env.ts' ? DEFAULT_ENV_FILE_CANDIDATES : [envFile]\n\n for (const candidate of candidates) {\n const abs = resolve(cwd, candidate)\n let source: string\n try {\n source = await readFile(abs, 'utf-8')\n } catch {\n continue\n }\n // Cheap heuristic: defineEnv(...) call AND a default export.\n // We don't try to evaluate the file — the generator emits an\n // `import type schema from '...'` and lets the user's tsc do the\n // actual schema-to-type inference.\n if (!/\\bdefineEnv\\s*\\(/.test(source)) continue\n if (!/export\\s+default\\b/.test(source)) continue\n return {\n filePath: abs,\n relativePath: toRelative(abs, cwd),\n }\n }\n\n return null\n}\n\n/** Detect duplicate class names across files */\nexport function findCollisions(classes: DiscoveredClass[]): ClassCollision[] {\n const groups = new Map<string, DiscoveredClass[]>()\n for (const cls of classes) {\n const arr = groups.get(cls.className) ?? []\n arr.push(cls)\n groups.set(cls.className, arr)\n }\n\n const collisions: ClassCollision[] = []\n for (const [className, group] of groups) {\n // Two declarations of the same class name in different files = collision.\n // Multiple decorators on the same file/class are NOT a collision.\n const distinctFiles = new Set(group.map((c) => c.filePath))\n if (distinctFiles.size > 1) {\n collisions.push({ className, classes: group })\n }\n }\n\n // Deterministic order\n collisions.sort((a, b) => a.className.localeCompare(b.className))\n return collisions\n}\n\n/**\n * Scan a project for decorated classes, createToken definitions, and\n * `@Inject` literal usages.\n */\nexport async function scanProject(opts: ScanOptions): Promise<ScanResult> {\n const root = resolve(opts.root)\n const files = await walk(root, opts)\n\n const classes: DiscoveredClass[] = []\n const routes: DiscoveredRoute[] = []\n const tokens: DiscoveredToken[] = []\n const injects: DiscoveredInject[] = []\n\n // Two passes: first collect all classes, then a second pass extracts\n // routes per file using the per-file class list as scoping context.\n // This keeps class discovery and route discovery independent.\n const sources = new Map<string, string>()\n for (const file of files) {\n let source: string\n try {\n source = await readFile(file, 'utf-8')\n } catch {\n continue\n }\n sources.set(file, source)\n classes.push(...extractClassesFromSource(source, file, opts.cwd))\n tokens.push(...extractTokensFromSource(source, file, opts.cwd))\n injects.push(...extractInjectsFromSource(source, file, opts.cwd))\n }\n\n for (const [file, source] of sources) {\n const classesInFile = classes.filter((c) => c.filePath === file)\n routes.push(...extractRoutesFromSource(source, file, opts.cwd, classesInFile))\n }\n\n // Deterministic ordering for stable .d.ts output\n classes.sort((a, b) => {\n if (a.className !== b.className) return a.className.localeCompare(b.className)\n return a.relativePath.localeCompare(b.relativePath)\n })\n tokens.sort(\n (a, b) => a.name.localeCompare(b.name) || a.relativePath.localeCompare(b.relativePath),\n )\n injects.sort(\n (a, b) => a.name.localeCompare(b.name) || a.relativePath.localeCompare(b.relativePath),\n )\n routes.sort(\n (a, b) => a.controller.localeCompare(b.controller) || a.method.localeCompare(b.method),\n )\n\n const collisions = findCollisions(classes)\n const env = await detectEnvFile(opts.cwd, opts.envFile ?? 'src/env.ts')\n\n return { classes, routes, tokens, injects, collisions, env }\n}\n","/**\n * Generates `.d.ts` files inside `.kickjs/types/` from the discovered\n * decorated classes and DI tokens. Pattern modeled on React Router's\n * `.react-router/types/` directory.\n *\n * Outputs:\n * - `.kickjs/types/registry.d.ts` — module augmentation for `KickJsRegistry`\n * that gives `container.resolve('UserService')` the right return type.\n * - `.kickjs/types/services.d.ts` — string-literal union of all known\n * service-style tokens for tooling autocomplete.\n * - `.kickjs/types/modules.d.ts` — string-literal union of discovered\n * module class names.\n * - `.kickjs/types/index.d.ts` — re-exports the above (single import target).\n * - `.kickjs/.gitignore` — gitignores the whole folder so generated files\n * never get committed.\n *\n * ## Collision behaviour\n *\n * If `findCollisions()` returns any duplicate class names:\n * - **Default (`allowDuplicates: false`)** — `generateTypes` throws a\n * `TokenCollisionError` with a clear message listing every conflicting\n * file. The caller (CLI) prints it and exits non-zero. Nothing is\n * written to disk.\n * - **`allowDuplicates: true`** — colliding classes are auto-namespaced\n * by their relative file path so the registry keys become e.g.\n * `'modules/users/UserService'` instead of `'UserService'`. Non-colliding\n * classes still get bare `'ClassName'` keys (smart default).\n *\n * @module @forinda/kickjs-cli/typegen/generator\n */\n\nimport { mkdir, writeFile } from 'node:fs/promises'\nimport { dirname, join, relative, resolve, sep } from 'node:path'\nimport type {\n ClassCollision,\n DiscoveredClass,\n DiscoveredEnv,\n DiscoveredInject,\n DiscoveredRoute,\n DiscoveredToken,\n} from './scanner'\n\n/** Header written to every generated file */\nconst HEADER = `/* eslint-disable */\n// AUTO-GENERATED by \\`kick typegen\\`. DO NOT EDIT.\n// Re-run with \\`kick typegen\\` or rely on \\`kick dev\\` to refresh.\n`\n\n/** Decorators whose classes participate in the DI registry augmentation */\nconst REGISTRY_DECORATORS = new Set(['Service', 'Repository', 'Injectable', 'Component'])\n\n/** Thrown by `generateTypes` when collisions are found and not allowed */\nexport class TokenCollisionError extends Error {\n readonly collisions: ClassCollision[]\n constructor(collisions: ClassCollision[]) {\n super(formatCollisionMessage(collisions))\n this.name = 'TokenCollisionError'\n this.collisions = collisions\n }\n}\n\n/** Build a human-readable message describing every collision */\nfunction formatCollisionMessage(collisions: ClassCollision[]): string {\n const lines: string[] = ['kick typegen: token collision detected']\n for (const c of collisions) {\n lines.push('')\n lines.push(` ${c.classes.length} classes named '${c.className}':`)\n for (const cls of c.classes) {\n lines.push(` - ${cls.relativePath}`)\n }\n }\n lines.push('')\n lines.push('Resolutions:')\n lines.push(' (a) Rename one of the classes')\n lines.push(\n \" (b) Use createToken<T>('namespaced/Name') and import the token explicitly — see @forinda/kickjs\",\n )\n lines.push(' (c) Pass --allow-duplicates to namespace the registry keys automatically')\n lines.push(\" (e.g. 'modules/users/UserService' instead of 'UserService')\")\n return lines.join('\\n')\n}\n\n/** Compute the module specifier (without extension) used inside `import('...')` */\nfunction importSpecifierFor(targetFile: string, fromFile: string): string {\n const fromDir = dirname(fromFile)\n let rel = relative(fromDir, targetFile).split(sep).join('/')\n rel = rel.replace(/\\.(ts|tsx|mts|cts)$/i, '')\n if (!rel.startsWith('.')) rel = './' + rel\n return rel\n}\n\n/**\n * Build the namespaced registry key for a colliding class.\n * Strips the `src/` prefix and the file extension, then appends the\n * class name. Example: `src/modules/users/user.service.ts` + `UserService`\n * → `modules/users/UserService`.\n */\nfunction namespacedKeyFor(cls: DiscoveredClass): string {\n const rel = cls.relativePath.replace(/^src\\//, '').replace(/\\.(ts|tsx|mts|cts)$/i, '')\n // Drop the trailing filename if it's just the class in kebab/snake form —\n // keep the directory path as the namespace.\n const parts = rel.split('/')\n parts.pop()\n const ns = parts.join('/')\n return ns ? `${ns}/${cls.className}` : cls.className\n}\n\n/**\n * Render the `KickJsRegistry` module augmentation. Each entry maps a\n * string token to the imported class type.\n *\n * Default-exported classes are imported as `import('...').default`.\n *\n * `collidingNames` lists class names that should be auto-namespaced;\n * everything else gets a bare key.\n */\nfunction renderRegistry(\n classes: DiscoveredClass[],\n outFile: string,\n collidingNames: Set<string>,\n): string {\n const seen = new Set<string>()\n const entries: string[] = []\n\n for (const c of classes) {\n if (!REGISTRY_DECORATORS.has(c.decorator)) continue\n\n const key = collidingNames.has(c.className) ? namespacedKeyFor(c) : c.className\n if (seen.has(key)) continue\n seen.add(key)\n\n const spec = importSpecifierFor(c.filePath, outFile)\n const ref = c.isDefault ? `import('${spec}').default` : `import('${spec}').${c.className}`\n entries.push(` '${key}': ${ref}`)\n }\n\n const body = entries.length\n ? entries.join('\\n')\n : ' // (no services discovered yet — run `kick g service <name>` to add one)'\n\n return `${HEADER}\ndeclare module '@forinda/kickjs' {\n interface KickJsRegistry {\n${body}\n }\n}\n\nexport {}\n`\n}\n\n/** Render a string-literal union type containing the given names */\nfunction renderUnion(typeName: string, names: string[], emptyComment: string): string {\n if (names.length === 0) {\n return `${HEADER}\n// ${emptyComment}\nexport type ${typeName} = never\n`\n }\n const sorted = [...new Set(names)].sort()\n return `${HEADER}\nexport type ${typeName} =\n${sorted.map((n) => ` | '${n}'`).join('\\n')}\n`\n}\n\n/** Render the barrel index that re-exports the union types */\nfunction renderIndex(includeEnv: boolean): string {\n const envImport = includeEnv ? \"import './env'\\n\" : ''\n return `${HEADER}\nexport type { ServiceToken } from './services'\nexport type { ModuleToken } from './modules'\n\n// The registry, routes, and env augmentations are loaded as side-effects —\n// importing this file (or having it on tsconfig include) is enough for\n// \\`container.resolve()\\`, \\`Ctx<KickRoutes.UserController['getUser']>\\`,\n// and \\`@Value('PORT')\\` to resolve.\nimport './registry'\nimport './routes'\n${envImport}`\n}\n\n/**\n * Render the `query` field's TypeScript type for a single route.\n *\n * - When `@ApiQueryParams` is absent (`queryFilterable === null`), emits\n * `unknown` so the user gets nothing extra.\n * - When the decorator is present, emits an object literal whose keys\n * are the standard query string keys (`filter`, `sort`, `q`, `page`,\n * `limit`). `sort` is narrowed to a string-literal union of allowed\n * field names with optional `-` direction prefix.\n */\nfunction renderQueryShape(m: DiscoveredRoute): string {\n if (m.queryFilterable === null) return 'unknown'\n const sortable = m.querySortable ?? []\n const sortType =\n sortable.length > 0 ? sortable.flatMap((f) => [`'${f}'`, `'-${f}'`]).join(' | ') : 'string'\n return `{ filter?: string | string[]; sort?: ${sortType}; q?: string; page?: string; limit?: string }`\n}\n\n/** Render JSDoc lines summarising the @ApiQueryParams whitelist */\nfunction renderQueryDocLines(m: DiscoveredRoute): string[] {\n const lines: string[] = []\n if (m.queryFilterable && m.queryFilterable.length > 0) {\n lines.push(`Filterable: ${m.queryFilterable.join(', ')}`)\n }\n if (m.querySortable && m.querySortable.length > 0) {\n lines.push(`Sortable: ${m.querySortable.join(', ')}`)\n }\n if (m.querySearchable && m.querySearchable.length > 0) {\n lines.push(`Searchable: ${m.querySearchable.join(', ')}`)\n }\n return lines\n}\n\n/**\n * Plan a schema import for hoisting at the top of `routes.ts`. Returns\n * the alias the in-namespace code should use, or `null` if the schema\n * cannot be referenced (no validator configured, or source unresolvable).\n *\n * Aliases are unique per (alias-counter) so two schemas named\n * `createTaskSchema` from different modules don't collide.\n */\nfunction planSchemaImport(\n schema: { identifier: string; source: string | null } | null,\n routeFilePath: string,\n routesOutFile: string,\n schemaValidator: 'zod' | false,\n imports: Map<string, { identifier: string; specifier: string }>,\n): string | null {\n if (!schema || schemaValidator !== 'zod') return null\n if (schema.source === null) return null\n const specifier = resolveSchemaImportSpecifier(schema.source, routeFilePath, routesOutFile)\n if (specifier === 'unknown') return null\n const key = `${specifier}::${schema.identifier}`\n let alias = imports.get(key)?.specifier\n if (!alias) {\n alias = `_S${imports.size}`\n imports.set(key, { identifier: schema.identifier, specifier: alias })\n } else {\n alias = imports.get(key)!.specifier\n }\n return alias\n}\n\n/** Build the `import type { ... } from '...'` lines for hoisted schema imports */\nfunction renderSchemaImports(\n imports: Map<string, { identifier: string; specifier: string }>,\n): string {\n if (imports.size === 0) return ''\n const lines: string[] = []\n for (const [key, value] of imports) {\n const [path] = key.split('::')\n lines.push(`import type { ${value.identifier} as ${value.specifier} } from '${path}'`)\n }\n return lines.join('\\n') + '\\n'\n}\n\n/**\n * Compute the import specifier the generated `routes.d.ts` should use to\n * reach a schema declared either in the controller file (empty string)\n * or imported from elsewhere (relative path or bare module name).\n *\n * - Bare module names (`zod`, `@scope/pkg`) are returned as-is.\n * - Relative paths (`./users.dto`, `../shared/schema`) are resolved\n * against the controller's file path, then re-relativised against the\n * directory containing `routes.d.ts`.\n * - Empty string (same-file schema) becomes a relative path from the\n * `routes.d.ts` directory back to the controller file.\n */\nfunction resolveSchemaImportSpecifier(\n source: string | null,\n routeFilePath: string,\n routesOutFile: string,\n): string {\n if (source === null) return 'unknown'\n const routesDir = dirname(routesOutFile)\n\n // Same-file schema — point at the controller file itself\n if (source === '') {\n let rel = relative(routesDir, routeFilePath).split(sep).join('/')\n rel = rel.replace(/\\.(ts|tsx|mts|cts)$/i, '')\n if (!rel.startsWith('.')) rel = './' + rel\n return rel\n }\n\n // Bare module name (no leading `.` and not absolute) → keep as-is\n if (!source.startsWith('.') && !source.startsWith('/')) {\n return source\n }\n\n // Relative import → resolve against the controller's directory, then\n // re-relativise against the routes.d.ts directory\n const controllerDir = dirname(routeFilePath)\n const absoluteTarget = resolve(controllerDir, source)\n let rel = relative(routesDir, absoluteTarget).split(sep).join('/')\n rel = rel.replace(/\\.(ts|tsx|mts|cts)$/i, '')\n if (!rel.startsWith('.')) rel = './' + rel\n return rel\n}\n\n/**\n * Render the `KickEnv` + `NodeJS.ProcessEnv` augmentation file from a\n * detected env schema. Mirrors the routes.ts pattern: emits as a `.ts`\n * file (not `.d.ts`) so the top-level `import type schema from '...'`\n * actually resolves under `moduleResolution: 'bundler'`.\n *\n * Returns `null` when no env file was discovered, so the caller can\n * skip writing the file altogether (rather than emitting an empty\n * augmentation that would shadow `KickEnv` to a useless `{}`).\n */\nfunction renderEnv(env: DiscoveredEnv | null, envOutFile: string): string | null {\n if (!env) return null\n // Compute the relative import path from .kickjs/types/env.ts back\n // to the user's env schema file, stripping the extension so TS can\n // resolve it.\n const envOutDir = dirname(envOutFile)\n let rel = relative(envOutDir, env.filePath).split(sep).join('/')\n rel = rel.replace(/\\.(ts|tsx|mts|cts)$/i, '')\n if (!rel.startsWith('.')) rel = './' + rel\n\n return `${HEADER}\n// Importing the schema as a type lets us infer its shape without\n// pulling in any runtime code. \\`Awaited<>\\` strips an accidental\n// Promise wrap on dynamic-imported defaults.\nimport type _envSchema from '${rel}'\n\n// Local type alias — interfaces can only \\`extend\\` an identifier,\n// not an inline import expression, so we resolve the schema's\n// inferred shape into a named type first.\ntype _KickEnvShape = import('zod').infer<typeof _envSchema>\n\ndeclare global {\n /**\n * Typed environment registry. Augmented from \\`${env.relativePath}\\`\n * so \\`@Value('PORT')\\`, \\`Env<'PORT'>\\`, and \\`process.env.PORT\\` are\n * all type-safe and autocomplete.\n */\n interface KickEnv extends _KickEnvShape {}\n\n // eslint-disable-next-line @typescript-eslint/no-namespace\n namespace NodeJS {\n /**\n * Narrow \\`process.env\\` so known keys exist as \\`string\\` (the raw\n * pre-Zod-coercion form). \\`@Value\\` and the \\`ConfigService\\` apply\n * the schema's transforms internally; access \\`process.env\\` directly\n * only when you need the raw string. Unknown keys still resolve to\n * \\`string | undefined\\` via the base @types/node declaration.\n */\n interface ProcessEnv extends Record<keyof KickEnv, string> {}\n }\n}\n\nexport {}\n`\n}\n\n/**\n * Render the `KickRoutes` global namespace augmentation. Each interface\n * inside corresponds to a controller class; each property is a single\n * route method on that controller, conforming to `RouteShape`.\n *\n * Fills `params` from URL patterns, `query` from `@ApiQueryParams`, and\n * `body`/`query`/`params` (when schema-validated) from the configured\n * schema validator. `response` is emitted as `unknown`.\n */\nfunction renderRoutes(\n routes: DiscoveredRoute[],\n routesOutFile: string,\n schemaValidator: 'zod' | false,\n): string {\n if (routes.length === 0) {\n return `${HEADER}\n// (no routes discovered yet — annotate a controller method with\n// @Get/@Post/@Put/@Delete/@Patch and re-run \\`kick typegen\\`)\ndeclare global {\n // eslint-disable-next-line @typescript-eslint/no-namespace\n namespace KickRoutes {}\n}\n\nexport {}\n`\n }\n\n // Group routes by controller for emission\n const byController = new Map<string, DiscoveredRoute[]>()\n for (const r of routes) {\n const arr = byController.get(r.controller) ?? []\n arr.push(r)\n byController.set(r.controller, arr)\n }\n\n // Hoisted schema imports — collected during interface rendering, then\n // emitted at the top of `routes.ts` so the in-namespace type references\n // resolve correctly. (Inline `import('...').X` inside `.d.ts` files\n // silently degrades to `unknown` with `moduleResolution: 'bundler'`.)\n const schemaImports = new Map<string, { identifier: string; specifier: string }>()\n\n const renderField = (\n schema: { identifier: string; source: string | null } | null,\n routeFilePath: string,\n ): string | null => {\n const alias = planSchemaImport(\n schema,\n routeFilePath,\n routesOutFile,\n schemaValidator,\n schemaImports,\n )\n return alias ? `import('zod').infer<typeof ${alias}>` : null\n }\n\n const interfaces: string[] = []\n for (const [controller, methods] of byController) {\n const lines: string[] = [` interface ${controller} {`]\n for (const m of methods) {\n // Empty `{}` (rather than `Record<string, never>`) so that accessing\n // an unknown property on a paramless route is a type error in strict\n // mode. `Record<string, never>` returns `never` for any access which\n // unfortunately is assignable to anything and silently passes.\n const urlParamsType =\n m.pathParams.length > 0 ? `{ ${m.pathParams.map((p) => `${p}: string`).join('; ')} }` : '{}'\n\n // Schema-driven types win over the URL-pattern / `unknown` defaults\n // when the user has wired a schema in the route decorator.\n const bodySchemaType = renderField(m.bodySchema, m.filePath)\n const querySchemaType = renderField(m.querySchema, m.filePath)\n const paramsSchemaType = renderField(m.paramsSchema, m.filePath)\n\n const paramsType = paramsSchemaType ?? urlParamsType\n const bodyType = bodySchemaType ?? 'unknown'\n const queryType = querySchemaType ?? renderQueryShape(m)\n const docLines = renderQueryDocLines(m)\n lines.push(\n ` /**`,\n ` * ${m.httpMethod} ${m.path}`,\n ...docLines.map((d) => ` * ${d}`),\n ` */`,\n ` ${m.method}: {`,\n ` params: ${paramsType}`,\n ` body: ${bodyType}`,\n ` query: ${queryType}`,\n ` response: unknown`,\n ` }`,\n )\n }\n lines.push(' }')\n interfaces.push(lines.join('\\n'))\n }\n\n const importBlock = renderSchemaImports(schemaImports)\n const interfaceBlock = interfaces.join('\\n')\n\n return `${HEADER}${importBlock}\ndeclare global {\n // eslint-disable-next-line @typescript-eslint/no-namespace\n namespace KickRoutes {\n${interfaceBlock}\n }\n}\n\nexport {}\n`\n}\n\n/** Result of a typegen run — useful for logging and tests */\nexport interface GenerateResult {\n /** Number of registry entries written */\n registryEntries: number\n /** Number of service tokens (classes + createToken + @Inject literals) */\n serviceTokens: number\n /** Number of module tokens written */\n moduleTokens: number\n /** Number of route entries written into KickRoutes */\n routeEntries: number\n /** Whether a typed env augmentation was emitted */\n envWritten: boolean\n /** Files that were written */\n written: string[]\n /** Number of collisions that were auto-namespaced (only > 0 with allowDuplicates) */\n resolvedCollisions: number\n}\n\n/** Options for `generateTypes` */\nexport interface GenerateOptions {\n /** Discovered classes from the scanner */\n classes: DiscoveredClass[]\n /** Discovered route handlers from the scanner */\n routes?: DiscoveredRoute[]\n /** Discovered `createToken('name')` calls */\n tokens?: DiscoveredToken[]\n /** Discovered `@Inject('literal')` calls */\n injects?: DiscoveredInject[]\n /** Detected duplicate class names from the scanner */\n collisions?: ClassCollision[]\n /** Discovered env schema file (or null if none) */\n env?: DiscoveredEnv | null\n /** Output directory (typically `<cwd>/.kickjs/types`) */\n outDir: string\n /**\n * When `true`, colliding class names are auto-namespaced by file path\n * instead of throwing. Default: `false`.\n */\n allowDuplicates?: boolean\n /**\n * Schema validator the project uses. When `'zod'`, the generator\n * emits `z.infer<typeof import('...').schema>` for any route whose\n * decorator declared a body/query/params schema identifier that\n * could be statically resolved. When `false` (or omitted), schemas\n * are ignored and `body`/`query`/`params` keep their `unknown`\n * placeholders.\n *\n * Future: `'joi'`, `'yup'`, `'json-schema'`, custom adapters.\n */\n schemaValidator?: 'zod' | false\n}\n\n/** Write all generated `.d.ts` files to `outDir` */\nexport async function generateTypes(opts: GenerateOptions): Promise<GenerateResult> {\n const {\n classes,\n routes = [],\n tokens = [],\n injects = [],\n collisions = [],\n env = null,\n outDir,\n allowDuplicates = false,\n schemaValidator = false,\n } = opts\n\n if (collisions.length > 0 && !allowDuplicates) {\n throw new TokenCollisionError(collisions)\n }\n\n await mkdir(outDir, { recursive: true })\n\n const registryFile = join(outDir, 'registry.d.ts')\n const servicesFile = join(outDir, 'services.d.ts')\n const modulesFile = join(outDir, 'modules.d.ts')\n // routes.ts (NOT .d.ts) — TypeScript silently degrades top-level\n // imports inside `.d.ts` files to `unknown` when the user's tsconfig\n // has `moduleResolution: 'bundler'`. Emitting as a regular `.ts` file\n // makes the schema imports resolve correctly so `z.infer<typeof X>`\n // produces a proper type. The file contains only type declarations\n // so it has zero runtime impact.\n const routesFile = join(outDir, 'routes.ts')\n // env.ts (same .ts vs .d.ts story as routes.ts)\n const envFile = join(outDir, 'env.ts')\n const indexFile = join(outDir, 'index.d.ts')\n\n const collidingNames = new Set(collisions.map((c) => c.className))\n const registryContent = renderRegistry(classes, registryFile, collidingNames)\n\n // ServiceToken union — combines class names, createToken literals, and\n // @Inject literals so tooling autocomplete sees every known token.\n const classTokens = classes\n .filter((c) => REGISTRY_DECORATORS.has(c.decorator))\n .map((c) => (collidingNames.has(c.className) ? namespacedKeyFor(c) : c.className))\n const tokenLiterals = tokens.map((t) => t.name)\n const injectLiterals = injects.map((i) => i.name)\n const allServices = [...classTokens, ...tokenLiterals, ...injectLiterals]\n\n const modules = classes.filter((c) => c.decorator === 'Module').map((c) => c.className)\n\n const servicesContent = renderUnion(\n 'ServiceToken',\n allServices,\n '(no tokens discovered — declare with createToken<T>() or `kick g service <name>`)',\n )\n const modulesContent = renderUnion(\n 'ModuleToken',\n modules,\n '(no @Module classes discovered — `kick g module <name>` to add one)',\n )\n const routesContent = renderRoutes(routes, routesFile, schemaValidator)\n const envContent = renderEnv(env, envFile)\n const indexContent = renderIndex(envContent !== null)\n\n await writeFile(registryFile, registryContent, 'utf-8')\n await writeFile(servicesFile, servicesContent, 'utf-8')\n await writeFile(modulesFile, modulesContent, 'utf-8')\n await writeFile(routesFile, routesContent, 'utf-8')\n await writeFile(indexFile, indexContent, 'utf-8')\n\n const written = [registryFile, servicesFile, modulesFile, routesFile, indexFile]\n if (envContent) {\n await writeFile(envFile, envContent, 'utf-8')\n written.push(envFile)\n }\n\n // Write `.gitignore` at the .kickjs root (one level up from outDir)\n const kickjsRoot = dirname(outDir)\n await writeFile(join(kickjsRoot, '.gitignore'), '# Auto-generated by kick typegen\\n*\\n', 'utf-8')\n\n return {\n registryEntries: classTokens.length,\n serviceTokens: new Set(allServices).size,\n moduleTokens: modules.length,\n routeEntries: routes.length,\n envWritten: envContent !== null,\n written,\n resolvedCollisions: collisions.length,\n }\n}\n","/**\n * Public entry point for the KickJS typegen module.\n *\n * Used by:\n * - `kick typegen` (one-shot or watch mode)\n * - `kick dev` (auto-runs once before Vite starts; refreshes when files change)\n *\n * @module @forinda/kickjs-cli/typegen\n */\n\nimport { resolve } from 'node:path'\nimport { scanProject, type ScanResult } from './scanner'\nimport { generateTypes, type GenerateResult, TokenCollisionError } from './generator'\n\nexport type {\n DiscoveredClass,\n DiscoveredToken,\n DiscoveredInject,\n DiscoveredEnv,\n ClassCollision,\n ScanResult,\n} from './scanner'\nexport type { GenerateResult } from './generator'\nexport { TokenCollisionError } from './generator'\n\n/** Options for `runTypegen` */\nexport interface RunTypegenOptions {\n /** Project root (defaults to `process.cwd()`) */\n cwd?: string\n /** Source directory to scan (defaults to `src`) */\n srcDir?: string\n /** Output directory (defaults to `.kickjs/types`) */\n outDir?: string\n /** Suppress console output */\n silent?: boolean\n /**\n * When `true`, duplicate class names are auto-namespaced by file path\n * instead of throwing. `kick dev` enables this so the dev server is\n * never blocked by an in-progress rename. CLI default is `false` so\n * `kick typegen` (and CI) catches collisions early. */\n allowDuplicates?: boolean\n /**\n * Schema validator used to derive `body`/`query`/`params` types from\n * route metadata. Currently only `'zod'` is supported; `false` (the\n * default) leaves these fields as `unknown`. Loaded from\n * `kick.config.ts` `typegen.schemaValidator` when invoked via the CLI.\n */\n schemaValidator?: 'zod' | false\n /**\n * Path to the env schema file (relative to `cwd`). The file must\n * default-export a `defineEnv(...)` schema for the typed `KickEnv`\n * augmentation to be emitted. Defaults to `'src/env.ts'`. Set to\n * `false` to disable env typing entirely.\n */\n envFile?: string | false\n}\n\n/** Resolve options to absolute paths */\nfunction resolveOptions(opts: RunTypegenOptions): {\n cwd: string\n srcDir: string\n outDir: string\n silent: boolean\n allowDuplicates: boolean\n schemaValidator: 'zod' | false\n envFile: string | false\n} {\n const cwd = opts.cwd ?? process.cwd()\n return {\n cwd,\n srcDir: resolve(cwd, opts.srcDir ?? 'src'),\n outDir: resolve(cwd, opts.outDir ?? '.kickjs/types'),\n silent: opts.silent ?? false,\n allowDuplicates: opts.allowDuplicates ?? false,\n schemaValidator: opts.schemaValidator ?? false,\n envFile: opts.envFile ?? 'src/env.ts',\n }\n}\n\n/**\n * Run a single typegen pass: scan source files, generate `.d.ts` files.\n *\n * Returns the discovered scan result alongside the generation result so\n * callers (`kick dev`, devtools) can log them or feed them to other tools.\n *\n * Throws `TokenCollisionError` if duplicate class names are found and\n * `allowDuplicates` is false.\n */\nexport async function runTypegen(opts: RunTypegenOptions = {}): Promise<{\n scan: ScanResult\n result: GenerateResult\n}> {\n const { cwd, srcDir, outDir, silent, allowDuplicates, schemaValidator, envFile } =\n resolveOptions(opts)\n\n const start = Date.now()\n const scan = await scanProject({\n root: srcDir,\n cwd,\n // Pass through unless explicitly disabled\n envFile: envFile === false ? undefined : envFile,\n })\n const result = await generateTypes({\n classes: scan.classes,\n routes: scan.routes,\n tokens: scan.tokens,\n injects: scan.injects,\n collisions: scan.collisions,\n env: envFile === false ? null : scan.env,\n outDir,\n allowDuplicates,\n schemaValidator,\n })\n const elapsed = Date.now() - start\n\n if (!silent) {\n const where = outDir.replace(cwd + '/', '')\n const collisionNote =\n result.resolvedCollisions > 0 ? `, ${result.resolvedCollisions} collisions namespaced` : ''\n const envNote = result.envWritten ? ', env typed' : ''\n console.log(\n ` kick typegen → ${result.serviceTokens} services, ${result.routeEntries} routes, ${result.moduleTokens} modules${envNote}${collisionNote} → ${where} (${elapsed}ms)`,\n )\n }\n\n return { scan, result }\n}\n\n/**\n * Watch mode for `kick typegen --watch`.\n *\n * Uses Node's built-in `fs.watch` (recursive, available on Linux 22+ and\n * macOS 19+). Falls back gracefully if recursive watch is not supported.\n *\n * Debounces re-runs by 100ms so a bulk file change (e.g. `kick g module`\n * creating 5 files at once) emits one regen, not five.\n *\n * In watch mode collisions are reported but never thrown — the watcher\n * keeps running so the user can fix the rename and the next scan\n * recovers automatically.\n *\n * Returns a `stop()` function that closes the watcher.\n */\nexport async function watchTypegen(opts: RunTypegenOptions = {}): Promise<() => void> {\n const resolved = resolveOptions(opts)\n const { srcDir, silent } = resolved\n // Watch mode always tolerates collisions — otherwise an in-progress\n // rename would crash the dev loop. The error is still printed.\n const runOpts: RunTypegenOptions = { ...resolved, allowDuplicates: true }\n\n // Initial run\n await safeRun(runOpts, silent)\n\n const { watch } = await import('node:fs')\n\n let timer: ReturnType<typeof setTimeout> | null = null\n const trigger = (filename: string | null) => {\n // Only react to TypeScript source changes; ignore everything else\n if (!filename) return\n if (!/\\.(ts|tsx|mts|cts)$/.test(filename)) return\n if (filename.includes('.kickjs')) return\n if (filename.endsWith('.d.ts')) return\n\n if (timer) clearTimeout(timer)\n timer = setTimeout(() => {\n safeRun(runOpts, silent)\n }, 100)\n }\n\n let watcher: ReturnType<typeof watch>\n try {\n watcher = watch(srcDir, { recursive: true }, (_event, filename) => {\n trigger(filename)\n })\n } catch (err: any) {\n if (!silent) {\n console.warn(\n ` kick typegen: watch mode unavailable (${err?.message ?? err}). Falling back to polling.`,\n )\n }\n // Polling fallback — re-scan every 2s\n const interval = setInterval(() => {\n safeRun({ ...runOpts, silent: true }, true)\n }, 2000)\n return () => clearInterval(interval)\n }\n\n return () => {\n if (timer) clearTimeout(timer)\n watcher.close()\n }\n}\n\n/** Run typegen swallowing errors so the watcher loop never dies */\nasync function safeRun(opts: RunTypegenOptions, silent: boolean): Promise<void> {\n try {\n await runTypegen(opts)\n } catch (err) {\n if (silent) return\n if (err instanceof TokenCollisionError) {\n console.error('\\n' + err.message + '\\n')\n } else {\n const msg = err instanceof Error ? err.message : String(err)\n console.error(` kick typegen failed: ${msg}`)\n }\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forinda/kickjs-cli",
3
- "version": "2.2.5",
3
+ "version": "2.3.0",
4
4
  "description": "CLI for KickJS — project scaffolding, DDD module generation, dev/build/start",
5
5
  "keywords": [
6
6
  "kickjs",
@@ -84,7 +84,8 @@
84
84
  "@types/pluralize": "^0.0.33",
85
85
  "typescript": "^5.9.2",
86
86
  "vite": "^8.0.3",
87
- "vitest": "^4.1.2"
87
+ "vitest": "^4.1.2",
88
+ "@forinda/kickjs-ai": "2.3.0"
88
89
  },
89
90
  "publishConfig": {
90
91
  "access": "public"