@forinda/kickjs-cli 5.1.0 → 5.2.1

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.
Files changed (27) hide show
  1. package/dist/{builtins-caRjFvKz.mjs → builtins-B0dptoXq.mjs} +626 -241
  2. package/dist/builtins-B0dptoXq.mjs.map +1 -0
  3. package/dist/{builtins-Cb_d-b1S.mjs → builtins-N3mDa6bM.mjs} +695 -273
  4. package/dist/cli.mjs +9 -9
  5. package/dist/{config-8bAt-mLl.mjs → config-Bc6ERRTE.mjs} +35 -5
  6. package/dist/{config-C_LQNClP.mjs → config-CRi3zCxk.mjs} +36 -6
  7. package/dist/config-CRi3zCxk.mjs.map +1 -0
  8. package/dist/{generator-extension-CYY-RI21.mjs → generator-extension-C-HwKvFf.mjs} +76 -39
  9. package/dist/generator-extension-C-HwKvFf.mjs.map +1 -0
  10. package/dist/index.d.mts +282 -1
  11. package/dist/index.d.mts.map +1 -1
  12. package/dist/index.mjs +4 -4
  13. package/dist/{plugin-D8K5fG-O.mjs → plugin-DfomEcef.mjs} +4 -4
  14. package/dist/{plugin-D8K5fG-O.mjs.map → plugin-DfomEcef.mjs.map} +1 -1
  15. package/dist/{plugin-Bgfg7qMk.mjs → plugin-b7ig7Uxv.mjs} +2 -2
  16. package/dist/{rolldown-runtime-BM29JyaJ.mjs → rolldown-runtime-CV_zlh2d.mjs} +1 -1
  17. package/dist/{run-plugins-BXvMFPhJ.mjs → run-plugins-D9abb5Nx.mjs} +2 -2
  18. package/dist/{typegen-BNz_RQTb.mjs → typegen-B9S81bOx.mjs} +48 -225
  19. package/dist/typegen-B9S81bOx.mjs.map +1 -0
  20. package/dist/{typegen-8ZeA1B-X.mjs → typegen-BKUAdp_3.mjs} +47 -228
  21. package/dist/{types-BBUo1vXh.mjs → types-CU89yUxU.mjs} +2 -2
  22. package/dist/{types-BBUo1vXh.mjs.map → types-CU89yUxU.mjs.map} +1 -1
  23. package/package.json +6 -4
  24. package/dist/builtins-caRjFvKz.mjs.map +0 -1
  25. package/dist/config-C_LQNClP.mjs.map +0 -1
  26. package/dist/generator-extension-CYY-RI21.mjs.map +0 -1
  27. package/dist/typegen-8ZeA1B-X.mjs.map +0 -1
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs-cli v5.1.0
2
+ * @forinda/kickjs-cli v5.2.1
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -8,19 +8,292 @@
8
8
  *
9
9
  * @license MIT
10
10
  */
11
- import { A as httpMethodColor, C as intro, D as select, E as outro, F as writeFileSafe, M as severityColor, N as fileExists, O as spinner, P as setDryRun, S as confirm, T as multiSelect, _ as pluralize, a as initProject, b as toKebabCase, c as generateKickJsSkills, d as generateService, f as generateGuard, g as resolveRepoType, h as generateModule, j as pc, k as text, l as generateDto, m as generateAdapter, n as tryDispatchPluginGenerator, o as generateAgents, p as generateMiddleware, s as generateClaude, t as listPluginGenerators, u as generateController, v as pluralizePascal, w as log, x as toPascalCase, y as toCamelCase } from "./generator-extension-CYY-RI21.mjs";
12
- import { a as resolveModuleConfig, i as loadKickConfig, t as PACKAGE_MANAGERS } from "./config-C_LQNClP.mjs";
13
- import { n as defineCliPlugin } from "./types-BBUo1vXh.mjs";
14
- import { n as mergeCliPlugins } from "./plugin-D8K5fG-O.mjs";
15
- import { a as discoverAssets, i as TokenCollisionError, o as renderAssetTypes, r as watchTypegen, t as runTypegen$1 } from "./typegen-8ZeA1B-X.mjs";
16
- import path, { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
11
+ import { A as httpMethodColor, C as intro, D as select, E as outro, F as writeFileSafe, M as severityColor, N as fileExists, O as spinner, P as setDryRun, S as confirm, T as multiSelect, _ as pluralize, a as initProject, b as toKebabCase, c as generateKickJsSkills, d as generateService, f as generateGuard, g as resolveRepoType, h as generateModule, j as pc, k as text, l as generateDto, m as generateAdapter, n as tryDispatchPluginGenerator, o as generateAgents, p as generateMiddleware, s as generateClaude, t as listPluginGenerators, u as generateController, v as pluralizePascal, w as log, x as toPascalCase, y as toCamelCase } from "./generator-extension-C-HwKvFf.mjs";
12
+ import { a as resolveModuleConfig, i as loadKickConfig, o as resolveTokenScope, t as PACKAGE_MANAGERS } from "./config-CRi3zCxk.mjs";
13
+ import { n as defineCliPlugin } from "./types-CU89yUxU.mjs";
14
+ import { n as mergeCliPlugins } from "./plugin-DfomEcef.mjs";
15
+ import { a as discoverAssets, i as TokenCollisionError, o as renderAssetTypes, r as watchTypegen, s as scanProject, t as runTypegen$1 } from "./typegen-B9S81bOx.mjs";
16
+ import path, { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
17
+ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
17
18
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
18
19
  import { execSync, fork, spawn, spawnSync } from "node:child_process";
19
- import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
20
20
  import { pathToFileURL } from "node:url";
21
21
  import { glob } from "glob";
22
+ import { groupAssetKeys } from "@forinda/kickjs";
22
23
  import { arch, platform, release } from "node:os";
23
24
  import { generate, migrateDown, migrateLatest, migrateRollback, migrateStatus, migrateUp, renderSchemaSource, resolveDbConfig } from "@forinda/kickjs-db";
25
+ //#region src/commands/add.ts
26
+ /** Registry of KickJS packages and their required peer dependencies */
27
+ const PACKAGE_REGISTRY = {
28
+ kickjs: {
29
+ pkg: "@forinda/kickjs",
30
+ peers: ["express"],
31
+ description: "Unified framework: DI, decorators, routing, middleware",
32
+ core: true
33
+ },
34
+ vite: {
35
+ pkg: "@forinda/kickjs-vite",
36
+ peers: ["vite"],
37
+ description: "Vite plugin: dev server, HMR, module discovery",
38
+ dev: true,
39
+ core: true
40
+ },
41
+ cli: {
42
+ pkg: "@forinda/kickjs-cli",
43
+ peers: [],
44
+ description: "CLI tool and code generators",
45
+ dev: true,
46
+ core: true
47
+ },
48
+ swagger: {
49
+ pkg: "@forinda/kickjs-swagger",
50
+ peers: [],
51
+ description: "OpenAPI spec + Swagger UI + ReDoc"
52
+ },
53
+ db: {
54
+ pkg: "@forinda/kickjs-db",
55
+ peers: [],
56
+ description: "kick/db core — schema DSL, migrations, KickDbClient, customType"
57
+ },
58
+ "db-pg": {
59
+ pkg: "@forinda/kickjs-db-pg",
60
+ peers: ["pg"],
61
+ description: "kick/db PostgreSQL dialect + adapter (pgDialect, pgAdapter)"
62
+ },
63
+ drizzle: {
64
+ pkg: "@forinda/kickjs-drizzle",
65
+ peers: ["drizzle-orm"],
66
+ description: "Drizzle ORM adapter + query builder"
67
+ },
68
+ prisma: {
69
+ pkg: "@forinda/kickjs-prisma",
70
+ peers: ["@prisma/client"],
71
+ description: "Prisma adapter + query builder"
72
+ },
73
+ ws: {
74
+ pkg: "@forinda/kickjs-ws",
75
+ peers: ["socket.io"],
76
+ description: "WebSocket with @WsController decorators"
77
+ },
78
+ devtools: {
79
+ pkg: "@forinda/kickjs-devtools",
80
+ peers: [],
81
+ description: "Development dashboard — routes, DI, metrics, health",
82
+ dev: true
83
+ },
84
+ auth: {
85
+ pkg: "@forinda/kickjs-auth",
86
+ peers: ["jsonwebtoken"],
87
+ description: "Authentication — JWT, API key, and custom strategies"
88
+ },
89
+ queue: {
90
+ pkg: "@forinda/kickjs-queue",
91
+ peers: [],
92
+ description: "Queue adapter (BullMQ/RabbitMQ/Kafka)"
93
+ },
94
+ "queue:bullmq": {
95
+ pkg: "@forinda/kickjs-queue",
96
+ peers: ["bullmq", "ioredis"],
97
+ description: "Queue with BullMQ + Redis"
98
+ },
99
+ "queue:rabbitmq": {
100
+ pkg: "@forinda/kickjs-queue",
101
+ peers: ["amqplib"],
102
+ description: "Queue with RabbitMQ"
103
+ },
104
+ "queue:kafka": {
105
+ pkg: "@forinda/kickjs-queue",
106
+ peers: ["kafkajs"],
107
+ description: "Queue with Kafka"
108
+ },
109
+ mcp: {
110
+ pkg: "@forinda/kickjs-mcp",
111
+ peers: ["@modelcontextprotocol/sdk"],
112
+ description: "Model Context Protocol server — expose @Controller endpoints as AI tools"
113
+ },
114
+ testing: {
115
+ pkg: "@forinda/kickjs-testing",
116
+ peers: [],
117
+ description: "Test utilities and TestModule builder",
118
+ dev: true
119
+ }
120
+ };
121
+ /**
122
+ * Walk up from `fromDir` to filesystem root, returning the first
123
+ * directory that contains `name`. Lets monorepo sub-packages pick up
124
+ * lockfiles and `packageManager` fields living at the workspace root.
125
+ */
126
+ function findUp(name, fromDir = process.cwd()) {
127
+ let current = fromDir;
128
+ while (true) {
129
+ if (existsSync(resolve(current, name))) return current;
130
+ const parent = dirname(current);
131
+ if (parent === current) return null;
132
+ current = parent;
133
+ }
134
+ }
135
+ function detectFromLockfile() {
136
+ if (findUp("pnpm-lock.yaml")) return "pnpm";
137
+ if (findUp("yarn.lock")) return "yarn";
138
+ if (findUp("bun.lockb") || findUp("bun.lock")) return "bun";
139
+ if (findUp("package-lock.json")) return "npm";
140
+ return null;
141
+ }
142
+ /**
143
+ * Read `packageManager` from the nearest ancestor `package.json` that
144
+ * declares the field (corepack convention: `"pnpm@10.0.0"`). Climbs so
145
+ * monorepo sub-packages inherit the workspace pm even when their own
146
+ * package.json omits the field.
147
+ */
148
+ function packageManagerFromPackageJson() {
149
+ let dir = process.cwd();
150
+ while (dir) {
151
+ const pkgPath = resolve(dir, "package.json");
152
+ if (existsSync(pkgPath)) try {
153
+ const field = JSON.parse(readFileSync(pkgPath, "utf-8")).packageManager;
154
+ if (typeof field === "string") {
155
+ const name = field.split("@")[0];
156
+ if (PACKAGE_MANAGERS.includes(name)) return name;
157
+ }
158
+ } catch {}
159
+ const parent = dirname(dir);
160
+ if (parent === dir) return null;
161
+ dir = parent;
162
+ }
163
+ return null;
164
+ }
165
+ /**
166
+ * Resolve which package manager to use, in priority order:
167
+ * 1. `--pm` CLI flag
168
+ * 2. `packageManager` in kick.config
169
+ * 3. `packageManager` in nearest ancestor package.json (corepack)
170
+ * 4. Nearest ancestor lockfile (pnpm-lock.yaml → yarn.lock → bun.lock → package-lock.json)
171
+ * 5. `'npm'` fallback
172
+ *
173
+ * Returns the chosen pm plus the source for callers that want to log
174
+ * the resolution path.
175
+ */
176
+ async function resolvePackageManagerWithSource(flagPm) {
177
+ if (flagPm && PACKAGE_MANAGERS.includes(flagPm)) return {
178
+ pm: flagPm,
179
+ source: "flag"
180
+ };
181
+ const config = await loadKickConfig(process.cwd());
182
+ if (config?.packageManager && PACKAGE_MANAGERS.includes(config.packageManager)) return {
183
+ pm: config.packageManager,
184
+ source: "config"
185
+ };
186
+ const fromPkg = packageManagerFromPackageJson();
187
+ if (fromPkg) return {
188
+ pm: fromPkg,
189
+ source: "package.json"
190
+ };
191
+ const fromLock = detectFromLockfile();
192
+ if (fromLock) return {
193
+ pm: fromLock,
194
+ source: "lockfile"
195
+ };
196
+ return {
197
+ pm: "npm",
198
+ source: "default"
199
+ };
200
+ }
201
+ /** Convenience wrapper for callers that don't care about the source. */
202
+ async function resolvePackageManager(flagPm) {
203
+ const { pm } = await resolvePackageManagerWithSource(flagPm);
204
+ return pm;
205
+ }
206
+ /**
207
+ * Print the package catalog. By default shows just the three core
208
+ * packages every project always has — the optional list churns
209
+ * (packages added, deprecated, removed) and a long enumeration in CLI
210
+ * output / docs goes stale within a release. Pass `all = true` to dump
211
+ * everything; that's what `kick add --list --all` triggers when an
212
+ * adopter genuinely wants the live catalog.
213
+ */
214
+ function printPackageList(all = false) {
215
+ const entries = Object.entries(PACKAGE_REGISTRY);
216
+ const maxName = Math.max(...entries.map(([k]) => k.length));
217
+ const core = entries.filter(([, info]) => info.core);
218
+ const optional = entries.filter(([, info]) => !info.core);
219
+ const formatRow = ([name, info]) => {
220
+ const padded = name.padEnd(maxName + 2);
221
+ const peers = info.peers.length ? ` (+ ${info.peers.join(", ")})` : "";
222
+ return ` ${padded} ${info.description}${peers}`;
223
+ };
224
+ console.log("\n Core packages (always installed by `kick new`):\n");
225
+ for (const row of core) console.log(formatRow(row));
226
+ if (all) {
227
+ console.log("\n Optional packages (add as needed):\n");
228
+ for (const row of optional) console.log(formatRow(row));
229
+ } else {
230
+ console.log(`\n Plus ${optional.length} optional packages (auth, swagger, db, queue, …).`);
231
+ console.log(" Run `kick add --list --all` for the full catalog.");
232
+ }
233
+ console.log("\n Usage: kick add auth drizzle swagger");
234
+ console.log(" kick add queue:bullmq");
235
+ console.log();
236
+ }
237
+ function registerListCommand(program) {
238
+ program.command("list").alias("ls").description("List KickJS packages (core only; pair with --all for the full catalog)").option("--all", "Include the full optional catalog").action((opts) => {
239
+ printPackageList(Boolean(opts.all));
240
+ });
241
+ }
242
+ function registerAddCommand(program) {
243
+ program.command("add [packages...]").description("Add KickJS packages with their required dependencies").option("--pm <manager>", "Package manager override").option("-D, --dev", "Install as dev dependency").option("--list", "List packages (core only by default; pair with --all)").option("--all", "When listing, include the full optional catalog").action(async (packages, opts) => {
244
+ if (opts.list || packages.length === 0) {
245
+ printPackageList(Boolean(opts.all));
246
+ return;
247
+ }
248
+ const { pm, source } = await resolvePackageManagerWithSource(opts.pm);
249
+ console.log(`\n Using ${pm} (resolved from ${source})`);
250
+ const forceDevFlag = opts.dev;
251
+ const prodDeps = /* @__PURE__ */ new Set();
252
+ const devDeps = /* @__PURE__ */ new Set();
253
+ const unknown = [];
254
+ for (const name of packages) {
255
+ const entry = PACKAGE_REGISTRY[name];
256
+ if (!entry) {
257
+ unknown.push(name);
258
+ continue;
259
+ }
260
+ const target = forceDevFlag || entry.dev ? devDeps : prodDeps;
261
+ target.add(entry.pkg);
262
+ for (const peer of entry.peers) target.add(peer);
263
+ }
264
+ if (unknown.length > 0) {
265
+ console.log(`\n Unknown packages: ${unknown.join(", ")}`);
266
+ console.log(" Run \"kick add --list\" to see available packages.\n");
267
+ if (prodDeps.size === 0 && devDeps.size === 0) return;
268
+ }
269
+ if (prodDeps.size > 0) {
270
+ const deps = Array.from(prodDeps);
271
+ const cmd = `${pm} add ${deps.join(" ")}`;
272
+ console.log(`\n Installing ${deps.length} dependency(ies):`);
273
+ for (const dep of deps) console.log(` + ${dep}`);
274
+ console.log();
275
+ try {
276
+ execSync(cmd, { stdio: "inherit" });
277
+ } catch {
278
+ console.log(`\n Installation failed. Run manually:\n ${cmd}\n`);
279
+ }
280
+ }
281
+ if (devDeps.size > 0) {
282
+ const deps = Array.from(devDeps);
283
+ const cmd = `${pm} add -D ${deps.join(" ")}`;
284
+ console.log(`\n Installing ${deps.length} dev dependency(ies):`);
285
+ for (const dep of deps) console.log(` + ${dep} (dev)`);
286
+ console.log();
287
+ try {
288
+ execSync(cmd, { stdio: "inherit" });
289
+ } catch {
290
+ console.log(`\n Installation failed. Run manually:\n ${cmd}\n`);
291
+ }
292
+ }
293
+ console.log(" Done!\n");
294
+ });
295
+ }
296
+ //#endregion
24
297
  //#region src/commands/init.ts
25
298
  /** All optional packages available for selection */
26
299
  const OPTIONAL_PACKAGES = [
@@ -51,9 +324,11 @@ const OPTIONAL_PACKAGES = [
51
324
  }
52
325
  ];
53
326
  function registerInitCommand(program) {
54
- program.command("new [name]").alias("init").description("Create a new KickJS project (use \".\" for current directory)").option("-d, --directory <dir>", "Target directory (defaults to project name)").option("--pm <manager>", "Package manager: pnpm | npm | yarn | bun").option("--git", "Initialize git repository").option("--no-git", "Skip git initialization").option("--install", "Install dependencies after scaffolding").option("--no-install", "Skip dependency installation").option("-f, --force", "Remove existing files without prompting").option("-t, --template <type>", "Project template: rest | ddd | cqrs | minimal").option("-r, --repo <type>", "Default repository: prisma | drizzle | inmemory | custom").option("--packages <packages>", "Comma-separated packages to include (e.g. auth,swagger,ws,queue)").action(async (name, opts) => {
327
+ program.command("new [name]").alias("init").description("Create a new KickJS project (use \".\" for current directory)").option("-d, --directory <dir>", "Target directory (defaults to project name)").option("--pm <manager>", "Package manager: pnpm | npm | yarn | bun").option("--git", "Initialize git repository").option("--no-git", "Skip git initialization").option("--install", "Install dependencies after scaffolding").option("--no-install", "Skip dependency installation").option("-f, --force", "Remove existing files without prompting").option("-t, --template <type>", "Project template: rest | ddd | cqrs | minimal").option("-r, --repo <type>", "Default repository: prisma | drizzle | inmemory | custom").option("--packages <packages>", "Comma-separated packages to include (e.g. auth,swagger,ws,queue)").option("-y, --yes", "Pick safe defaults for every prompt (template=minimal, repo=inmemory, no extras, git+install on)").option("--non-interactive", "alias for --yes").action(async (name, opts) => {
55
328
  intro("KickJS — Create a new project");
56
- if (!name) name = await text({
329
+ const yes = Boolean(opts.yes || opts.nonInteractive);
330
+ if (!name) if (yes) name = "my-api";
331
+ else name = await text({
57
332
  message: "Project name",
58
333
  placeholder: "my-api",
59
334
  defaultValue: "my-api"
@@ -67,7 +342,11 @@ function registerInitCommand(program) {
67
342
  const entries = readdirSync(directory);
68
343
  if (entries.length > 0) {
69
344
  if (opts.force) log.warn(`Clearing existing files in ${directory}`);
70
- else {
345
+ else if (yes) {
346
+ log.warn(`Directory "${name}" is not empty. Pass --force to clear it.`);
347
+ outro("Aborted.");
348
+ return;
349
+ } else {
71
350
  log.warn(`Directory "${name}" is not empty:`);
72
351
  const shown = entries.slice(0, 5);
73
352
  for (const entry of shown) log.message(` - ${entry}`);
@@ -87,7 +366,8 @@ function registerInitCommand(program) {
87
366
  }
88
367
  }
89
368
  let template = opts.template;
90
- if (!template) template = await select({
369
+ if (!template) if (yes) template = "minimal";
370
+ else template = await select({
91
371
  message: "Project template",
92
372
  options: [
93
373
  {
@@ -113,7 +393,8 @@ function registerInitCommand(program) {
113
393
  ]
114
394
  });
115
395
  let packageManager = opts.pm;
116
- if (!packageManager) packageManager = await select({
396
+ if (!packageManager) if (yes) packageManager = await resolvePackageManager(void 0);
397
+ else packageManager = await select({
117
398
  message: "Package manager",
118
399
  options: [
119
400
  {
@@ -135,7 +416,8 @@ function registerInitCommand(program) {
135
416
  ]
136
417
  });
137
418
  let defaultRepo = opts.repo;
138
- if (!defaultRepo) {
419
+ if (!defaultRepo) if (yes) defaultRepo = "inmemory";
420
+ else {
139
421
  defaultRepo = await select({
140
422
  message: "Default repository/ORM",
141
423
  options: [
@@ -168,19 +450,20 @@ function registerInitCommand(program) {
168
450
  const raw = opts.packages.trim().toLowerCase();
169
451
  if (raw === "" || raw === "none" || raw === "false") selectedPackages = [];
170
452
  else selectedPackages = opts.packages.split(",").map((p) => p.trim()).filter(Boolean);
171
- } else selectedPackages = await multiSelect({
453
+ } else if (yes) selectedPackages = [];
454
+ else selectedPackages = await multiSelect({
172
455
  message: "Select packages to include",
173
456
  options: [...OPTIONAL_PACKAGES],
174
457
  required: false
175
458
  });
176
459
  let initGit;
177
- if (opts.git === void 0) initGit = await confirm({
460
+ if (opts.git === void 0) initGit = yes ? true : await confirm({
178
461
  message: "Initialize git repository?",
179
462
  initialValue: true
180
463
  });
181
464
  else initGit = opts.git;
182
465
  let installDeps;
183
- if (opts.install === void 0) installDeps = await confirm({
466
+ if (opts.install === void 0) installDeps = yes ? true : await confirm({
184
467
  message: "Install dependencies?",
185
468
  initialValue: true
186
469
  });
@@ -428,7 +711,7 @@ function detectName(outDir, override) {
428
711
  const pkg = JSON.parse(readFileSync(join(outDir, "package.json"), "utf-8"));
429
712
  if (pkg.name) return pkg.name.replace(/^@[^/]+\//, "");
430
713
  } catch {}
431
- return outDir.split("/").filter(Boolean).pop() ?? "app";
714
+ return outDir.split("/").findLast(Boolean) ?? "app";
432
715
  }
433
716
  function detectPm(outDir, override) {
434
717
  if (override) return override;
@@ -892,7 +1175,7 @@ function parseFields(raw) {
892
1175
  });
893
1176
  }
894
1177
  async function generateScaffold(options) {
895
- const { name, fields, modulesDir, noEntity, noTests, repo = "inmemory" } = options;
1178
+ const { name, fields, modulesDir, noEntity, noTests: _noTests, repo = "inmemory", tokenScope = "app" } = options;
896
1179
  const shouldPluralize = options.pluralize !== false;
897
1180
  const kebab = toKebabCase(name);
898
1181
  const pascal = toPascalCase(name);
@@ -914,7 +1197,7 @@ async function generateScaffold(options) {
914
1197
  await write(`application/dtos/${kebab}-response.dto.ts`, genResponseDTO(pascal, fields));
915
1198
  const useCases = genUseCases(pascal, kebab, plural, pluralPascal);
916
1199
  for (const uc of useCases) await write(`application/use-cases/${uc.file}`, uc.content);
917
- await write(`domain/repositories/${kebab}.repository.ts`, genRepositoryInterface(pascal, kebab));
1200
+ await write(`domain/repositories/${kebab}.repository.ts`, genRepositoryInterface(pascal, kebab, tokenScope));
918
1201
  await write(`domain/services/${kebab}-domain.service.ts`, genDomainService(pascal, kebab));
919
1202
  if (repo === "inmemory") await write(`infrastructure/repositories/in-memory-${kebab}.repository.ts`, genInMemoryRepository(pascal, kebab, fields));
920
1203
  if (!noEntity) {
@@ -1195,7 +1478,7 @@ export class ${pascal}Controller {
1195
1478
  }
1196
1479
  `;
1197
1480
  }
1198
- function genRepositoryInterface(pascal, kebab) {
1481
+ function genRepositoryInterface(pascal, kebab, tokenScope) {
1199
1482
  return `import { createToken } from '@forinda/kickjs'
1200
1483
  import type { ${pascal}ResponseDTO } from '../../application/dtos/${kebab}-response.dto'
1201
1484
  import type { Create${pascal}DTO } from '../../application/dtos/create-${kebab}.dto'
@@ -1216,8 +1499,12 @@ export interface I${pascal}Repository {
1216
1499
  * \`container.resolve(${pascal.toUpperCase()}_REPOSITORY)\` and
1217
1500
  * \`@Inject(${pascal.toUpperCase()}_REPOSITORY)\` both return the typed
1218
1501
  * interface — no manual generic, no \`any\` cast.
1502
+ *
1503
+ * The \`'${tokenScope}/'\` prefix matches the project scope so
1504
+ * \`kick-lint\`'s \`token-reserved-prefix\` rule never fires —
1505
+ * adopters must NOT use the reserved \`'kick/'\` namespace.
1219
1506
  */
1220
- export const ${pascal.toUpperCase()}_REPOSITORY = createToken<I${pascal}Repository>('app/${kebab}/repository')
1507
+ export const ${pascal.toUpperCase()}_REPOSITORY = createToken<I${pascal}Repository>('${tokenScope}/${kebab}/repository')
1221
1508
  `;
1222
1509
  }
1223
1510
  function genDomainService(pascal, kebab) {
@@ -1497,6 +1784,7 @@ async function runModuleGeneration(names, opts, dryRun) {
1497
1784
  const repo = opts.repo ?? resolveRepoType(mc.repo);
1498
1785
  const pattern = opts.pattern ?? config?.pattern ?? "ddd";
1499
1786
  const shouldPluralize = opts.pluralize === false ? false : mc.pluralize ?? true;
1787
+ const tokenScope = resolveTokenScope(config, process.cwd());
1500
1788
  const allFiles = [];
1501
1789
  for (const name of names) {
1502
1790
  const files = await generateModule({
@@ -1510,7 +1798,8 @@ async function runModuleGeneration(names, opts, dryRun) {
1510
1798
  pattern,
1511
1799
  dryRun,
1512
1800
  pluralize: shouldPluralize,
1513
- prismaClientPath: mc.prismaClientPath
1801
+ prismaClientPath: mc.prismaClientPath,
1802
+ tokenScope
1514
1803
  });
1515
1804
  allFiles.push(...files);
1516
1805
  }
@@ -1673,16 +1962,19 @@ function registerGenerateCommand(program) {
1673
1962
  console.error("\n Error: At least one field is required.\n Usage: kick g scaffold <name> <field:type> [field:type...]\n Example: kick g scaffold Post title:string body:text:optional published:boolean:optional\n Optional: append :optional (shell-safe, no quoting needed)\n");
1674
1963
  process.exit(1);
1675
1964
  }
1676
- const mc = resolveModuleConfig(await loadKickConfig(process.cwd()));
1965
+ const config = await loadKickConfig(process.cwd());
1966
+ const mc = resolveModuleConfig(config);
1677
1967
  const modulesDir = opts.modulesDir ?? mc.dir ?? "src/modules";
1678
1968
  const fields = parseFields(rawFields);
1969
+ const tokenScope = resolveTokenScope(config, process.cwd());
1679
1970
  const files = await generateScaffold({
1680
1971
  name,
1681
1972
  fields,
1682
1973
  modulesDir: resolve(modulesDir),
1683
1974
  noEntity: opts.entity === false,
1684
1975
  noTests: opts.tests === false,
1685
- pluralize: opts.pluralize === false ? false : mc.pluralize ?? true
1976
+ pluralize: opts.pluralize === false ? false : mc.pluralize ?? true,
1977
+ tokenScope
1686
1978
  });
1687
1979
  console.log(`\n Scaffolded ${name} with ${fields.length} field(s):`);
1688
1980
  for (const f of fields) console.log(` ${f.name}: ${f.type}${f.optional ? " (optional)" : ""}`);
@@ -1771,6 +2063,17 @@ const BANNER_PREFIX = "/* AUTO-GENERATED by kick typegen — do not edit. Plugin
1771
2063
  async function runTypegen(opts) {
1772
2064
  const typesDirAbs = path.resolve(opts.cwd, TYPES_DIR);
1773
2065
  await mkdir(typesDirAbs, { recursive: true });
2066
+ const scanCache = /* @__PURE__ */ new Map();
2067
+ const scanFn = opts.scan ?? scanProject;
2068
+ const getScanResult = (scanOpts) => {
2069
+ const key = stableScanKey(scanOpts);
2070
+ let pending = scanCache.get(key);
2071
+ if (!pending) {
2072
+ pending = scanFn(scanOpts);
2073
+ scanCache.set(key, pending);
2074
+ }
2075
+ return pending;
2076
+ };
1774
2077
  const ctx = {
1775
2078
  cwd: opts.cwd,
1776
2079
  config: opts.config,
@@ -1782,6 +2085,7 @@ async function runTypegen(opts) {
1782
2085
  await mkdir(path.dirname(abs), { recursive: true });
1783
2086
  await writeFile(abs, contents, "utf8");
1784
2087
  },
2088
+ getScanResult,
1785
2089
  log: console
1786
2090
  };
1787
2091
  const results = [];
@@ -1794,7 +2098,8 @@ async function runTypegen(opts) {
1794
2098
  });
1795
2099
  continue;
1796
2100
  }
1797
- const file = path.join(typesDirAbs, `${plugin.id.replace(/\//g, "__")}.d.ts`);
2101
+ const ext = plugin.outExtension ?? ".d.ts";
2102
+ const file = path.join(typesDirAbs, `${plugin.id.replace(/\//g, "__")}${ext}`);
1798
2103
  const next = `${BANNER_PREFIX}${plugin.id} */\n\n` + out + "\n";
1799
2104
  let prev = "";
1800
2105
  if (existsSync(file)) prev = await readFile(file, "utf8");
@@ -1816,6 +2121,24 @@ async function runTypegen(opts) {
1816
2121
  }
1817
2122
  return results;
1818
2123
  }
2124
+ /**
2125
+ * Order-independent cache key for `ScanOptions`. Builds the key from
2126
+ * the known fields in a fixed order so semantically equal options
2127
+ * always produce the same key regardless of how the caller built the
2128
+ * object literal. Arrays (extensions / exclude) are sorted before
2129
+ * joining so `['.ts', '.tsx']` and `['.tsx', '.ts']` collide.
2130
+ */
2131
+ function stableScanKey(opts) {
2132
+ const extensions = (opts.extensions ?? []).slice().toSorted().join(",");
2133
+ const exclude = (opts.exclude ?? []).slice().toSorted().join(",");
2134
+ return [
2135
+ `root=${opts.root}`,
2136
+ `cwd=${opts.cwd}`,
2137
+ `extensions=${extensions}`,
2138
+ `exclude=${exclude}`,
2139
+ `envFile=${opts.envFile ?? ""}`
2140
+ ].join("|");
2141
+ }
1819
2142
  //#endregion
1820
2143
  //#region src/typegen/disable-filter.ts
1821
2144
  /**
@@ -1937,18 +2260,15 @@ async function processEntry(namespace, entry, cwd, distAbs) {
1937
2260
  });
1938
2261
  mkdirSync(destAbs, { recursive: true });
1939
2262
  const manifestSlice = {};
1940
- const keyOwner = /* @__PURE__ */ new Map();
1941
- for (const relPath of matches.sort()) {
2263
+ const { pairs, collisionGroupsResolved } = groupAssetKeys(namespace, [...matches].toSorted(), { strategy: entry.keys ?? "auto" });
2264
+ for (const { rel: relPath, key } of pairs) {
1942
2265
  const srcFile = join(srcAbs, relPath);
1943
2266
  const destFile = join(destAbs, relPath);
1944
2267
  mkdirSync(dirname(destFile), { recursive: true });
1945
2268
  cpSync(srcFile, destFile);
1946
- const logicalKey = `${namespace}/${stripExt(relPath)}`;
1947
- const previous = keyOwner.get(logicalKey);
1948
- if (previous) console.warn(` ⚠ assetMap collision in '${namespace}': '${previous}' and '${relPath}' both flatten to key '${logicalKey}'. Last-alphabetical wins ('${relPath}'). Rename one of them or set assetMap.${namespace}.glob to filter by extension.`);
1949
- keyOwner.set(logicalKey, relPath);
1950
- manifestSlice[logicalKey] = toManifestRelative(distAbs, destFile);
2269
+ manifestSlice[key] = toManifestRelative(distAbs, destFile);
1951
2270
  }
2271
+ if (collisionGroupsResolved > 0) console.log(` ℹ assetMap.${namespace}: auto-resolved ${collisionGroupsResolved} basename collision(s) by keeping extensions (set 'keys: "strip"' to opt back into legacy last-write-wins behaviour, or 'keys: "with-extension"' to keep all keys verbose).`);
1952
2272
  return {
1953
2273
  entrySummary: {
1954
2274
  namespace,
@@ -1959,11 +2279,6 @@ async function processEntry(namespace, entry, cwd, distAbs) {
1959
2279
  manifestSlice
1960
2280
  };
1961
2281
  }
1962
- /** Strip the final extension from a file path (`mails/welcome.ejs` → `mails/welcome`). */
1963
- function stripExt(path) {
1964
- const ext = extname(path);
1965
- return ext ? path.slice(0, -ext.length) : path;
1966
- }
1967
2282
  /**
1968
2283
  * Make `destFile` relative to the manifest's directory + force POSIX
1969
2284
  * separators so the manifest is byte-stable across platforms.
@@ -2005,8 +2320,21 @@ function isDirectorySync(path) {
2005
2320
  * This function just creates the Vite server, listens, and handles shutdown.
2006
2321
  * Vite owns the HTTP port — Express runs as post-middleware on Vite's server.
2007
2322
  */
2008
- async function startDevServer(_entry, port) {
2323
+ /**
2324
+ * Resolve whether the dev server's chokidar should poll instead of
2325
+ * relying on `fs.watch` events. CLI flag wins over env var; default
2326
+ * is event-based (faster, lower CPU). Polling is the right choice in
2327
+ * Docker bind mounts, WSL crossing the WSL/Windows boundary, NFS,
2328
+ * and some old Linux kernels where new-file events get dropped.
2329
+ */
2330
+ function resolvePolling(flag) {
2331
+ if (typeof flag === "boolean") return flag;
2332
+ const env = process.env.KICKJS_WATCH_POLLING;
2333
+ return env === "1" || env === "true";
2334
+ }
2335
+ async function startDevServer(_entry, port, opts = {}) {
2009
2336
  if (port) process.env.PORT = port;
2337
+ const polling = resolvePolling(opts.polling);
2010
2338
  const cwd = process.cwd();
2011
2339
  const devConfig = await loadKickConfig(cwd);
2012
2340
  const schemaValidator = devConfig?.typegen?.schemaValidator ?? "zod";
@@ -2019,7 +2347,8 @@ async function startDevServer(_entry, port) {
2019
2347
  envFile,
2020
2348
  srcDir: devConfig?.typegen?.srcDir,
2021
2349
  outDir: devConfig?.typegen?.outDir,
2022
- assetMap: devConfig?.assetMap
2350
+ assetMap: devConfig?.assetMap,
2351
+ runPlugins: false
2023
2352
  });
2024
2353
  } catch (err) {
2025
2354
  console.warn(` kick typegen: skipped (${err?.message ?? err})`);
@@ -2032,7 +2361,13 @@ async function startDevServer(_entry, port) {
2032
2361
  const { createServer } = await import(pathToFileURL(createRequire(resolve("package.json")).resolve("vite")).href);
2033
2362
  const server = await createServer({
2034
2363
  configFile: resolve("vite.config.ts"),
2035
- server: { port: port ? parseInt(port, 10) : void 0 }
2364
+ server: {
2365
+ port: port ? parseInt(port, 10) : void 0,
2366
+ ...polling ? { watch: {
2367
+ usePolling: true,
2368
+ interval: 100
2369
+ } } : {}
2370
+ }
2036
2371
  });
2037
2372
  const assetSrcRoots = devConfig?.assetMap ? Object.values(devConfig.assetMap).map((entry) => entry?.src).filter((src) => typeof src === "string" && src.length > 0).map((src) => resolve(cwd, src)) : [];
2038
2373
  const isAssetFile = (file) => assetSrcRoots.some((root) => file === root || file.startsWith(`${root}/`));
@@ -2053,7 +2388,8 @@ async function startDevServer(_entry, port) {
2053
2388
  envFile,
2054
2389
  srcDir: devConfig?.typegen?.srcDir,
2055
2390
  outDir: devConfig?.typegen?.outDir,
2056
- assetMap: devConfig?.assetMap
2391
+ assetMap: devConfig?.assetMap,
2392
+ runPlugins: false
2057
2393
  }).catch(() => {});
2058
2394
  runAllPluginTypegens({
2059
2395
  cwd,
@@ -2078,9 +2414,9 @@ async function startDevServer(_entry, port) {
2078
2414
  process.on("SIGTERM", shutdown);
2079
2415
  }
2080
2416
  function registerRunCommands(program) {
2081
- program.command("dev").description("Start development server with Vite HMR (zero-downtime reload)").option("-e, --entry <file>", "Entry file", "src/index.ts").option("-p, --port <port>", "Port number").action(async (opts) => {
2417
+ program.command("dev").description("Start development server with Vite HMR (zero-downtime reload)").option("-e, --entry <file>", "Entry file", "src/index.ts").option("-p, --port <port>", "Port number").option("--polling", "Force chokidar to poll for file changes (Docker / WSL / NFS / older kernels)").action(async (opts) => {
2082
2418
  try {
2083
- await startDevServer(opts.entry, opts.port);
2419
+ await startDevServer(opts.entry, opts.port, { polling: opts.polling });
2084
2420
  } catch (err) {
2085
2421
  if (err.code === "ERR_MODULE_NOT_FOUND" && err.message?.includes("vite")) console.error("\n Error: vite is not installed.\n Run: pnpm add -D vite unplugin-swc\n");
2086
2422
  else console.error("\n Dev server failed:", err.message ?? err);
@@ -2173,7 +2509,7 @@ function registerInfoCommand(program) {
2173
2509
  }
2174
2510
  //#endregion
2175
2511
  //#region src/commands/inspect.ts
2176
- const { bold, dim, green, red, yellow, cyan, blue } = pc;
2512
+ const { bold, dim, green, red, yellow, blue } = pc;
2177
2513
  function formatUptime(seconds) {
2178
2514
  const d = Math.floor(seconds / 86400);
2179
2515
  const h = Math.floor(seconds % 86400 / 3600);
@@ -2282,198 +2618,6 @@ function registerInspectCommand(program) {
2282
2618
  });
2283
2619
  }
2284
2620
  //#endregion
2285
- //#region src/commands/add.ts
2286
- /** Registry of KickJS packages and their required peer dependencies */
2287
- const PACKAGE_REGISTRY = {
2288
- kickjs: {
2289
- pkg: "@forinda/kickjs",
2290
- peers: ["express"],
2291
- description: "Unified framework: DI, decorators, routing, middleware"
2292
- },
2293
- vite: {
2294
- pkg: "@forinda/kickjs-vite",
2295
- peers: ["vite"],
2296
- description: "Vite plugin: dev server, HMR, module discovery",
2297
- dev: true
2298
- },
2299
- config: {
2300
- pkg: "dotenv",
2301
- peers: [],
2302
- description: "Optional .env file loader (kickjs ConfigService now ships in @forinda/kickjs)"
2303
- },
2304
- cli: {
2305
- pkg: "@forinda/kickjs-cli",
2306
- peers: [],
2307
- description: "CLI tool and code generators",
2308
- dev: true
2309
- },
2310
- swagger: {
2311
- pkg: "@forinda/kickjs-swagger",
2312
- peers: [],
2313
- description: "OpenAPI spec + Swagger UI + ReDoc"
2314
- },
2315
- drizzle: {
2316
- pkg: "@forinda/kickjs-drizzle",
2317
- peers: ["drizzle-orm"],
2318
- description: "Drizzle ORM adapter + query builder"
2319
- },
2320
- prisma: {
2321
- pkg: "@forinda/kickjs-prisma",
2322
- peers: ["@prisma/client"],
2323
- description: "Prisma adapter + query builder"
2324
- },
2325
- ws: {
2326
- pkg: "@forinda/kickjs-ws",
2327
- peers: ["socket.io"],
2328
- description: "WebSocket with @WsController decorators"
2329
- },
2330
- devtools: {
2331
- pkg: "@forinda/kickjs-devtools",
2332
- peers: [],
2333
- description: "Development dashboard — routes, DI, metrics, health",
2334
- dev: true
2335
- },
2336
- auth: {
2337
- pkg: "@forinda/kickjs-auth",
2338
- peers: ["jsonwebtoken"],
2339
- description: "Authentication — JWT, API key, and custom strategies"
2340
- },
2341
- queue: {
2342
- pkg: "@forinda/kickjs-queue",
2343
- peers: [],
2344
- description: "Queue adapter (BullMQ/RabbitMQ/Kafka)"
2345
- },
2346
- "queue:bullmq": {
2347
- pkg: "@forinda/kickjs-queue",
2348
- peers: ["bullmq", "ioredis"],
2349
- description: "Queue with BullMQ + Redis"
2350
- },
2351
- "queue:rabbitmq": {
2352
- pkg: "@forinda/kickjs-queue",
2353
- peers: ["amqplib"],
2354
- description: "Queue with RabbitMQ"
2355
- },
2356
- "queue:kafka": {
2357
- pkg: "@forinda/kickjs-queue",
2358
- peers: ["kafkajs"],
2359
- description: "Queue with Kafka"
2360
- },
2361
- mcp: {
2362
- pkg: "@forinda/kickjs-mcp",
2363
- peers: ["@modelcontextprotocol/sdk"],
2364
- description: "Model Context Protocol server — expose @Controller endpoints as AI tools"
2365
- },
2366
- testing: {
2367
- pkg: "@forinda/kickjs-testing",
2368
- peers: [],
2369
- description: "Test utilities and TestModule builder",
2370
- dev: true
2371
- }
2372
- };
2373
- function detectPackageManager() {
2374
- if (existsSync(resolve("pnpm-lock.yaml"))) return "pnpm";
2375
- if (existsSync(resolve("yarn.lock"))) return "yarn";
2376
- if (existsSync(resolve("bun.lockb")) || existsSync(resolve("bun.lock"))) return "bun";
2377
- return "npm";
2378
- }
2379
- /** Read `packageManager` from package.json (corepack convention: "pnpm@10.0.0") */
2380
- function packageManagerFromPackageJson() {
2381
- try {
2382
- const field = JSON.parse(readFileSync(resolve("package.json"), "utf-8")).packageManager;
2383
- if (typeof field !== "string") return null;
2384
- const name = field.split("@")[0];
2385
- return PACKAGE_MANAGERS.includes(name) ? name : null;
2386
- } catch {
2387
- return null;
2388
- }
2389
- }
2390
- /**
2391
- * Resolve which package manager to use, in priority order:
2392
- * 1. `--pm` CLI flag
2393
- * 2. `packageManager` in kick.config
2394
- * 3. `packageManager` in package.json (corepack)
2395
- * 4. Lockfile detection
2396
- * 5. `'npm'`
2397
- */
2398
- async function resolvePackageManager(flagPm) {
2399
- if (flagPm && PACKAGE_MANAGERS.includes(flagPm)) return flagPm;
2400
- const config = await loadKickConfig(process.cwd());
2401
- if (config?.packageManager && PACKAGE_MANAGERS.includes(config.packageManager)) return config.packageManager;
2402
- const fromPkg = packageManagerFromPackageJson();
2403
- if (fromPkg) return fromPkg;
2404
- return detectPackageManager();
2405
- }
2406
- function printPackageList() {
2407
- console.log("\n Available KickJS packages:\n");
2408
- const maxName = Math.max(...Object.keys(PACKAGE_REGISTRY).map((k) => k.length));
2409
- for (const [name, info] of Object.entries(PACKAGE_REGISTRY)) {
2410
- const padded = name.padEnd(maxName + 2);
2411
- const peers = info.peers.length ? ` (+ ${info.peers.join(", ")})` : "";
2412
- console.log(` ${padded} ${info.description}${peers}`);
2413
- }
2414
- console.log("\n Usage: kick add auth drizzle swagger");
2415
- console.log(" kick add queue:bullmq");
2416
- console.log();
2417
- }
2418
- function registerListCommand(program) {
2419
- program.command("list").alias("ls").description("List all available KickJS packages").action(() => {
2420
- printPackageList();
2421
- });
2422
- }
2423
- function registerAddCommand(program) {
2424
- program.command("add [packages...]").description("Add KickJS packages with their required dependencies").option("--pm <manager>", "Package manager override").option("-D, --dev", "Install as dev dependency").option("--list", "List all available packages").action(async (packages, opts) => {
2425
- if (opts.list || packages.length === 0) {
2426
- printPackageList();
2427
- return;
2428
- }
2429
- const pm = await resolvePackageManager(opts.pm);
2430
- const forceDevFlag = opts.dev;
2431
- const prodDeps = /* @__PURE__ */ new Set();
2432
- const devDeps = /* @__PURE__ */ new Set();
2433
- const unknown = [];
2434
- for (const name of packages) {
2435
- const entry = PACKAGE_REGISTRY[name];
2436
- if (!entry) {
2437
- unknown.push(name);
2438
- continue;
2439
- }
2440
- const target = forceDevFlag || entry.dev ? devDeps : prodDeps;
2441
- target.add(entry.pkg);
2442
- for (const peer of entry.peers) target.add(peer);
2443
- }
2444
- if (unknown.length > 0) {
2445
- console.log(`\n Unknown packages: ${unknown.join(", ")}`);
2446
- console.log(" Run \"kick add --list\" to see available packages.\n");
2447
- if (prodDeps.size === 0 && devDeps.size === 0) return;
2448
- }
2449
- if (prodDeps.size > 0) {
2450
- const deps = Array.from(prodDeps);
2451
- const cmd = `${pm} add ${deps.join(" ")}`;
2452
- console.log(`\n Installing ${deps.length} dependency(ies):`);
2453
- for (const dep of deps) console.log(` + ${dep}`);
2454
- console.log();
2455
- try {
2456
- execSync(cmd, { stdio: "inherit" });
2457
- } catch {
2458
- console.log(`\n Installation failed. Run manually:\n ${cmd}\n`);
2459
- }
2460
- }
2461
- if (devDeps.size > 0) {
2462
- const deps = Array.from(devDeps);
2463
- const cmd = `${pm} add -D ${deps.join(" ")}`;
2464
- console.log(`\n Installing ${deps.length} dev dependency(ies):`);
2465
- for (const dep of deps) console.log(` + ${dep} (dev)`);
2466
- console.log();
2467
- try {
2468
- execSync(cmd, { stdio: "inherit" });
2469
- } catch {
2470
- console.log(`\n Installation failed. Run manually:\n ${cmd}\n`);
2471
- }
2472
- }
2473
- console.log(" Done!\n");
2474
- });
2475
- }
2476
- //#endregion
2477
2621
  //#region src/explain/known-issues.ts
2478
2622
  function includesAll(haystack, needles) {
2479
2623
  const lower = haystack.toLowerCase();
@@ -3280,8 +3424,8 @@ function registerTypegenCommand(program) {
3280
3424
  const cwd = process.cwd();
3281
3425
  const config = await loadKickConfig(cwd);
3282
3426
  if (opts.list) {
3283
- const { mergeCliPlugins } = await import("./plugin-D8K5fG-O.mjs").then((n) => n.t);
3284
- const { builtinCliPlugins } = await import("./builtins-caRjFvKz.mjs");
3427
+ const { mergeCliPlugins } = await import("./plugin-DfomEcef.mjs").then((n) => n.t);
3428
+ const { builtinCliPlugins } = await import("./builtins-B0dptoXq.mjs");
3285
3429
  const merged = mergeCliPlugins([...builtinCliPlugins, ...config?.plugins ?? []], config?.commands ?? []);
3286
3430
  const disabled = new Set(config?.typegen?.disable ?? []);
3287
3431
  if (merged.typegens.length === 0) {
@@ -3307,7 +3451,8 @@ function registerTypegenCommand(program) {
3307
3451
  allowDuplicates: opts.allowDuplicates,
3308
3452
  schemaValidator,
3309
3453
  envFile,
3310
- assetMap: config?.assetMap
3454
+ assetMap: config?.assetMap,
3455
+ runPlugins: false
3311
3456
  };
3312
3457
  try {
3313
3458
  if (opts.watch) {
@@ -3727,6 +3872,238 @@ const kickAssetsTypegen = () => ({
3727
3872
  }
3728
3873
  });
3729
3874
  //#endregion
3875
+ //#region src/typegen/render/routes.ts
3876
+ const ROUTES_HEADER = `/* eslint-disable */
3877
+ // AUTO-GENERATED by \`kick typegen\`. DO NOT EDIT.
3878
+ // Re-run with \`kick typegen\` or rely on \`kick dev\` to refresh.
3879
+ `;
3880
+ /**
3881
+ * Render the `KickRoutes` global namespace augmentation. Each interface
3882
+ * inside corresponds to a controller class; each property is a single
3883
+ * route method on that controller, conforming to `RouteShape`.
3884
+ *
3885
+ * Fills `params` from URL patterns, `query` from `@ApiQueryParams`, and
3886
+ * `body`/`query`/`params` (when schema-validated) from the configured
3887
+ * schema validator. `response` is emitted as `unknown`.
3888
+ */
3889
+ function renderRoutes(routes, routesOutFile, schemaValidator) {
3890
+ if (routes.length === 0) return `${ROUTES_HEADER}
3891
+ // (no routes discovered yet — annotate a controller method with
3892
+ // @Get/@Post/@Put/@Delete/@Patch and re-run \`kick typegen\`)
3893
+ declare global {
3894
+ // eslint-disable-next-line @typescript-eslint/no-namespace
3895
+ namespace KickRoutes {}
3896
+ }
3897
+
3898
+ export {}
3899
+ `;
3900
+ const byController = /* @__PURE__ */ new Map();
3901
+ for (const r of routes) {
3902
+ const arr = byController.get(r.controller) ?? [];
3903
+ arr.push(r);
3904
+ byController.set(r.controller, arr);
3905
+ }
3906
+ const schemaImports = /* @__PURE__ */ new Map();
3907
+ const renderField = (schema, routeFilePath) => {
3908
+ const alias = planSchemaImport(schema, routeFilePath, routesOutFile, schemaValidator, schemaImports);
3909
+ return alias ? `import('zod').infer<typeof ${alias}>` : null;
3910
+ };
3911
+ const interfaces = [];
3912
+ for (const [controller, methods] of byController) {
3913
+ const lines = [` interface ${controller} {`];
3914
+ for (const m of methods) {
3915
+ const urlParamsType = m.pathParams.length > 0 ? `{ ${m.pathParams.map((p) => `${p}: string`).join("; ")} }` : "{}";
3916
+ const bodySchemaType = renderField(m.bodySchema, m.filePath);
3917
+ const querySchemaType = renderField(m.querySchema, m.filePath);
3918
+ const paramsType = renderField(m.paramsSchema, m.filePath) ?? urlParamsType;
3919
+ const bodyType = bodySchemaType ?? "unknown";
3920
+ const queryType = querySchemaType ?? renderQueryShape(m);
3921
+ const docLines = renderQueryDocLines(m);
3922
+ lines.push(` /**`, ` * ${m.httpMethod} ${m.path}`, ...docLines.map((d) => ` * ${d}`), ` */`, ` ${m.method}: {`, ` params: ${paramsType}`, ` body: ${bodyType}`, ` query: ${queryType}`, ` response: unknown`, ` }`);
3923
+ }
3924
+ lines.push(" }");
3925
+ interfaces.push(lines.join("\n"));
3926
+ }
3927
+ return `${ROUTES_HEADER}${renderSchemaImports(schemaImports)}
3928
+ declare global {
3929
+ // eslint-disable-next-line @typescript-eslint/no-namespace
3930
+ namespace KickRoutes {
3931
+ ${interfaces.join("\n")}
3932
+ }
3933
+ }
3934
+
3935
+ export {}
3936
+ `;
3937
+ }
3938
+ function renderQueryShape(m) {
3939
+ if (m.queryFilterable === null) return "unknown";
3940
+ const sortable = m.querySortable ?? [];
3941
+ return `{ filter?: string | string[]; sort?: ${sortable.length > 0 ? sortable.flatMap((f) => [`'${f}'`, `'-${f}'`]).join(" | ") : "string"}; q?: string; page?: string; limit?: string }`;
3942
+ }
3943
+ function renderQueryDocLines(m) {
3944
+ const lines = [];
3945
+ if (m.queryFilterable && m.queryFilterable.length > 0) lines.push(`Filterable: ${m.queryFilterable.join(", ")}`);
3946
+ if (m.querySortable && m.querySortable.length > 0) lines.push(`Sortable: ${m.querySortable.join(", ")}`);
3947
+ if (m.querySearchable && m.querySearchable.length > 0) lines.push(`Searchable: ${m.querySearchable.join(", ")}`);
3948
+ return lines;
3949
+ }
3950
+ /**
3951
+ * Plan a schema import for hoisting at the top of `routes.ts`. Returns
3952
+ * the alias the in-namespace code should use, or `null` if the schema
3953
+ * cannot be referenced (no validator configured, or source unresolvable).
3954
+ */
3955
+ function planSchemaImport(schema, routeFilePath, routesOutFile, schemaValidator, imports) {
3956
+ if (!schema || schemaValidator !== "zod") return null;
3957
+ if (schema.source === null) return null;
3958
+ const specifier = resolveSchemaImportSpecifier(schema.source, routeFilePath, routesOutFile);
3959
+ if (specifier === "unknown") return null;
3960
+ const key = `${specifier}::${schema.identifier}`;
3961
+ let alias = imports.get(key)?.specifier;
3962
+ if (!alias) {
3963
+ alias = `_S${imports.size}`;
3964
+ imports.set(key, {
3965
+ identifier: schema.identifier,
3966
+ specifier: alias
3967
+ });
3968
+ } else alias = imports.get(key).specifier;
3969
+ return alias;
3970
+ }
3971
+ function renderSchemaImports(imports) {
3972
+ if (imports.size === 0) return "";
3973
+ const lines = [];
3974
+ for (const [key, value] of imports) {
3975
+ const [path] = key.split("::");
3976
+ lines.push(`import type { ${value.identifier} as ${value.specifier} } from '${path}'`);
3977
+ }
3978
+ return lines.join("\n") + "\n";
3979
+ }
3980
+ /**
3981
+ * Compute the import specifier the generated `routes.d.ts` should use
3982
+ * to reach a schema declared either in the controller file (empty
3983
+ * string) or imported from elsewhere (relative path or bare module).
3984
+ */
3985
+ function resolveSchemaImportSpecifier(source, routeFilePath, routesOutFile) {
3986
+ if (source === null) return "unknown";
3987
+ const routesDir = dirname(routesOutFile);
3988
+ if (source === "") {
3989
+ let rel = relative(routesDir, routeFilePath).split(sep).join("/");
3990
+ rel = rel.replace(/\.(ts|tsx|mts|cts)$/i, "");
3991
+ if (!rel.startsWith(".")) rel = "./" + rel;
3992
+ return rel;
3993
+ }
3994
+ if (!source.startsWith(".") && !source.startsWith("/")) return source;
3995
+ let rel = relative(routesDir, resolve(dirname(routeFilePath), source)).split(sep).join("/");
3996
+ rel = rel.replace(/\.(ts|tsx|mts|cts)$/i, "");
3997
+ if (!rel.startsWith(".")) rel = "./" + rel;
3998
+ return rel;
3999
+ }
4000
+ //#endregion
4001
+ //#region src/typegen/builtin/routes.ts
4002
+ const kickRoutesTypegen = () => ({
4003
+ id: "kick/routes",
4004
+ outExtension: ".ts",
4005
+ inputs: ["src/**/*.controller.ts", "src/**/*.module.ts"],
4006
+ async generate(ctx) {
4007
+ const scan = await ctx.getScanResult({
4008
+ root: resolveSrcDir$1(ctx),
4009
+ cwd: ctx.cwd,
4010
+ envFile: resolveEnvFile$1(ctx)
4011
+ });
4012
+ const schemaValidator = ctx.config?.typegen?.schemaValidator ?? "zod";
4013
+ const outFile = path.resolve(ctx.cwd, ".kickjs/types/kick__routes.ts");
4014
+ return renderRoutes(scan.routes, outFile, schemaValidator);
4015
+ }
4016
+ });
4017
+ function resolveSrcDir$1(ctx) {
4018
+ return path.resolve(ctx.cwd, ctx.config?.typegen?.srcDir ?? "src");
4019
+ }
4020
+ function resolveEnvFile$1(ctx) {
4021
+ const cfg = ctx.config?.typegen?.envFile;
4022
+ if (cfg === false) return void 0;
4023
+ return cfg;
4024
+ }
4025
+ //#endregion
4026
+ //#region src/typegen/render/env.ts
4027
+ const ENV_HEADER = `/* eslint-disable */
4028
+ // AUTO-GENERATED by \`kick typegen\`. DO NOT EDIT.
4029
+ // Re-run with \`kick typegen\` or rely on \`kick dev\` to refresh.
4030
+ `;
4031
+ /**
4032
+ * Render the `KickEnv` + `NodeJS.ProcessEnv` augmentation file from a
4033
+ * detected env schema. Returns `null` when no env file was discovered,
4034
+ * so the caller can skip emission entirely (rather than emitting an
4035
+ * empty augmentation that would shadow `KickEnv` to a useless `{}`).
4036
+ */
4037
+ function renderEnv(env, envOutFile) {
4038
+ if (!env) return null;
4039
+ let rel = relative(dirname(envOutFile), env.filePath).split(sep).join("/");
4040
+ rel = rel.replace(/\.(ts|tsx|mts|cts)$/i, "");
4041
+ if (!rel.startsWith(".")) rel = "./" + rel;
4042
+ return `${ENV_HEADER}
4043
+ // Importing the schema as a type lets us infer its shape without
4044
+ // pulling in any runtime code. \`Awaited<>\` strips an accidental
4045
+ // Promise wrap on dynamic-imported defaults.
4046
+ import type _envSchema from '${rel}'
4047
+
4048
+ // Local type alias — interfaces can only \`extend\` an identifier,
4049
+ // not an inline import expression, so we resolve the schema's
4050
+ // inferred shape into a named type first.
4051
+ type _KickEnvShape = import('zod').infer<typeof _envSchema>
4052
+
4053
+ declare global {
4054
+ /**
4055
+ * Typed environment registry. Augmented from \`${env.relativePath}\`
4056
+ * so \`@Value('PORT')\`, \`Env<'PORT'>\`, and \`process.env.PORT\` are
4057
+ * all type-safe and autocomplete.
4058
+ */
4059
+ interface KickEnv extends _KickEnvShape {}
4060
+
4061
+ // eslint-disable-next-line @typescript-eslint/no-namespace
4062
+ namespace NodeJS {
4063
+ /**
4064
+ * Narrow \`process.env\` so known keys exist as \`string\` (the raw
4065
+ * pre-Zod-coercion form). \`@Value\` and the \`ConfigService\` apply
4066
+ * the schema's transforms internally; access \`process.env\` directly
4067
+ * only when you need the raw string. Unknown keys still resolve to
4068
+ * \`string | undefined\` via the base @types/node declaration.
4069
+ */
4070
+ interface ProcessEnv extends Record<keyof KickEnv, string> {}
4071
+ }
4072
+ }
4073
+
4074
+ export {}
4075
+ `;
4076
+ }
4077
+ //#endregion
4078
+ //#region src/typegen/builtin/env.ts
4079
+ const kickEnvTypegen = () => ({
4080
+ id: "kick/env",
4081
+ outExtension: ".ts",
4082
+ inputs: [
4083
+ "src/env.ts",
4084
+ "src/**/env.ts",
4085
+ "src/**/*.env.ts"
4086
+ ],
4087
+ async generate(ctx) {
4088
+ const envFile = resolveEnvFile(ctx);
4089
+ if (envFile === false) return null;
4090
+ const scan = await ctx.getScanResult({
4091
+ root: resolveSrcDir(ctx),
4092
+ cwd: ctx.cwd,
4093
+ envFile
4094
+ });
4095
+ if (!scan.env) return null;
4096
+ const outFile = path.resolve(ctx.cwd, ".kickjs/types/kick__env.ts");
4097
+ return renderEnv(scan.env, outFile);
4098
+ }
4099
+ });
4100
+ function resolveSrcDir(ctx) {
4101
+ return path.resolve(ctx.cwd, ctx.config?.typegen?.srcDir ?? "src");
4102
+ }
4103
+ function resolveEnvFile(ctx) {
4104
+ return ctx.config?.typegen?.envFile;
4105
+ }
4106
+ //#endregion
3730
4107
  //#region src/plugin/builtins.ts
3731
4108
  const builtinCliPlugins = [
3732
4109
  defineCliPlugin({
@@ -3789,9 +4166,17 @@ const builtinCliPlugins = [
3789
4166
  defineCliPlugin({
3790
4167
  name: "kick/assets",
3791
4168
  typegens: [kickAssetsTypegen()]
4169
+ }),
4170
+ defineCliPlugin({
4171
+ name: "kick/routes",
4172
+ typegens: [kickRoutesTypegen()]
4173
+ }),
4174
+ defineCliPlugin({
4175
+ name: "kick/env",
4176
+ typegens: [kickEnvTypegen()]
3792
4177
  })
3793
4178
  ];
3794
4179
  //#endregion
3795
4180
  export { builtinCliPlugins, applyDisableFilter as n, runAllPluginTypegens as t };
3796
4181
 
3797
- //# sourceMappingURL=builtins-caRjFvKz.mjs.map
4182
+ //# sourceMappingURL=builtins-B0dptoXq.mjs.map