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