@forinda/kickjs-cli 4.1.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs-cli v4.1.0
2
+ * @forinda/kickjs-cli v5.0.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -108,15 +108,9 @@ async function fileExists(filePath) {
108
108
  const PACKAGE_DEPS = {
109
109
  auth: "@forinda/kickjs-auth",
110
110
  swagger: "@forinda/kickjs-swagger",
111
- otel: "@forinda/kickjs-otel",
112
111
  ws: "@forinda/kickjs-ws",
113
112
  queue: "@forinda/kickjs-queue",
114
- cron: "@forinda/kickjs-cron",
115
- mailer: "@forinda/kickjs-mailer",
116
- graphql: "@forinda/kickjs-graphql",
117
- devtools: "@forinda/kickjs-devtools",
118
- notifications: "@forinda/kickjs-notifications",
119
- "multi-tenant": "@forinda/kickjs-multi-tenant"
113
+ devtools: "@forinda/kickjs-devtools"
120
114
  };
121
115
  /** Generate package.json with template-aware dependencies */
122
116
  function generatePackageJson(name, template, kickjsVersion, packages = []) {
@@ -129,15 +123,10 @@ function generatePackageJson(name, template, kickjsVersion, packages = []) {
129
123
  pino: "^10.3.1",
130
124
  "pino-pretty": "^13.1.3"
131
125
  };
132
- if (template === "graphql") {
133
- baseDeps["@forinda/kickjs-graphql"] = kickjsVersion;
134
- baseDeps["graphql"] = "^16.11.0";
135
- }
136
126
  for (const pkg of packages) {
137
127
  const dep = PACKAGE_DEPS[pkg];
138
128
  if (dep && !baseDeps[dep]) baseDeps[dep] = kickjsVersion;
139
129
  }
140
- if (packages.includes("graphql") && !baseDeps["graphql"]) baseDeps["graphql"] = "^16.11.0";
141
130
  return JSON.stringify({
142
131
  name,
143
132
  version: kickjsVersion.replace("^", ""),
@@ -342,54 +331,9 @@ export default defineConfig({
342
331
  */
343
332
  function generateEntryFile(name, template, version, packages = []) {
344
333
  switch (template) {
345
- case "graphql": {
346
- const gqlImports = [];
347
- const gqlAdapters = [];
348
- if (packages.includes("devtools")) {
349
- gqlImports.push(`import { DevToolsAdapter } from '@forinda/kickjs-devtools'`);
350
- gqlAdapters.push(` DevToolsAdapter(),`);
351
- }
352
- if (packages.includes("otel")) {
353
- gqlImports.push(`import { OtelAdapter } from '@forinda/kickjs-otel'`);
354
- gqlAdapters.push(` OtelAdapter({ serviceName: '${name}' }),`);
355
- }
356
- if (packages.includes("swagger")) {
357
- gqlImports.push(`import { SwaggerAdapter } from '@forinda/kickjs-swagger'`);
358
- gqlAdapters.push(` SwaggerAdapter({ info: { title: '${name}', version: '${version}' } }),`);
359
- }
360
- return `import 'reflect-metadata'
361
- // Side-effect import — registers the extended env schema with kickjs
362
- // **before** any controller / service / @Value gets resolved. Without
363
- // this line ConfigService.get('YOUR_KEY') returns undefined because the
364
- // cached schema would still be the base shape. See guide/configuration.
365
- import './config'
366
- import { bootstrap } from '@forinda/kickjs'
367
- import { GraphQLAdapter } from '@forinda/kickjs-graphql'
368
- ${gqlImports.length ? gqlImports.join("\n") + "\n" : ""}import { modules } from './modules'
369
-
370
- // Import your resolvers here
371
- // import { UserResolver } from './resolvers/user.resolver'
372
-
373
- // Export the app for the Vite plugin (dev mode)
374
- export const app = await bootstrap({
375
- modules,
376
- adapters: [
377
- ${gqlAdapters.length ? gqlAdapters.join("\n") + "\n" : ""} new GraphQLAdapter({
378
- resolvers: [/* UserResolver */],
379
- // Add custom type definitions here:
380
- // typeDefs: userTypeDefs,
381
- }),
382
- ],
383
- })
384
- `;
385
- }
386
334
  case "cqrs": {
387
335
  const cqrsImports = [];
388
336
  const cqrsAdapters = [];
389
- if (packages.includes("otel")) {
390
- cqrsImports.push(`import { OtelAdapter } from '@forinda/kickjs-otel'`);
391
- cqrsAdapters.push(` OtelAdapter({ serviceName: '${name}' }),`);
392
- }
393
337
  if (packages.includes("devtools")) {
394
338
  cqrsImports.push(`import { DevToolsAdapter } from '@forinda/kickjs-devtools'`);
395
339
  cqrsAdapters.push(` DevToolsAdapter(),`);
@@ -398,10 +342,6 @@ ${gqlAdapters.length ? gqlAdapters.join("\n") + "\n" : ""} new GraphQLAdapter
398
342
  cqrsImports.push(`import { SwaggerAdapter } from '@forinda/kickjs-swagger'`);
399
343
  cqrsAdapters.push(` SwaggerAdapter({\n info: { title: '${name}', version: '${version}' },\n }),`);
400
344
  }
401
- if (packages.includes("graphql")) {
402
- cqrsImports.push(`import { GraphQLAdapter } from '@forinda/kickjs-graphql'`);
403
- cqrsAdapters.push(` new GraphQLAdapter({ resolvers: [] }),`);
404
- }
405
345
  return `import 'reflect-metadata'
406
346
  // Side-effect import — registers the extended env schema with kickjs
407
347
  // **before** any controller / service / @Value gets resolved. Without
@@ -430,14 +370,6 @@ export const app = await bootstrap({
430
370
  imports.push(`import { DevToolsAdapter } from '@forinda/kickjs-devtools'`);
431
371
  adapters.push(` DevToolsAdapter(),`);
432
372
  }
433
- if (packages.includes("otel")) {
434
- imports.push(`import { OtelAdapter } from '@forinda/kickjs-otel'`);
435
- adapters.push(` OtelAdapter({ serviceName: '${name}' }),`);
436
- }
437
- if (packages.includes("graphql")) {
438
- imports.push(`import { GraphQLAdapter } from '@forinda/kickjs-graphql'`);
439
- adapters.push(` new GraphQLAdapter({ resolvers: [] }),`);
440
- }
441
373
  return `import 'reflect-metadata'
442
374
  // Side-effect import — registers the extended env schema with kickjs
443
375
  // **before** any controller / service / @Value gets resolved. Without
@@ -462,10 +394,6 @@ export const app = await bootstrap({ modules${adapters.length ? `,\n adapters:
462
394
  restImports.push(`import { SwaggerAdapter } from '@forinda/kickjs-swagger'`);
463
395
  restAdapters.push(` SwaggerAdapter({\n info: { title: '${name}', version: '${version}' },\n }),`);
464
396
  }
465
- if (packages.includes("otel")) {
466
- restImports.push(`import { OtelAdapter } from '@forinda/kickjs-otel'`);
467
- restAdapters.push(` OtelAdapter({ serviceName: '${name}' }),`);
468
- }
469
397
  return `import 'reflect-metadata'
470
398
  // Side-effect import — registers the extended env schema with kickjs
471
399
  // **before** any controller / service / @Value gets resolved. Without
@@ -685,15 +613,13 @@ export default defineConfig({
685
613
  function generateReadme(name, template, pm) {
686
614
  const templateLabels = {
687
615
  rest: "REST API",
688
- graphql: "GraphQL API",
689
616
  ddd: "Domain-Driven Design",
690
617
  cqrs: "CQRS + Event-Driven",
691
618
  minimal: "Minimal"
692
619
  };
693
620
  const packages = ["@forinda/kickjs", "@forinda/kickjs-vite"];
694
621
  if (template !== "minimal") packages.push("@forinda/kickjs-swagger", "@forinda/kickjs-devtools");
695
- if (template === "graphql") packages.push("@forinda/kickjs-graphql");
696
- if (template === "cqrs") packages.push("@forinda/kickjs-queue", "@forinda/kickjs-ws", "@forinda/kickjs-otel");
622
+ if (template === "cqrs") packages.push("@forinda/kickjs-queue", "@forinda/kickjs-ws");
697
623
  return `# ${name}
698
624
 
699
625
  A **${templateLabels[template] ?? "REST API"}** built with [KickJS](https://forinda.github.io/kick-js/) — a decorator-driven Node.js framework on Express 5 and TypeScript.
@@ -738,11 +664,11 @@ kick add auth # Authentication (JWT, API key, OAuth)
738
664
  kick add swagger # OpenAPI documentation
739
665
  kick add ws # WebSocket support
740
666
  kick add queue # Background job processing
741
- kick add mailer # Email sending
742
- kick add cron # Scheduled tasks
743
667
  kick add --list # Show all available packages
744
668
  \`\`\`
745
669
 
670
+ For email, scheduled tasks, multi-tenancy, OpenTelemetry, GraphQL, and notifications use the BYO recipes in the [KickJS guides](https://forinda.github.io/kick-js/guide/) — they wire the upstream library through \`defineAdapter()\` / \`definePlugin()\` directly, so you keep control of the integration.
671
+
746
672
  ## Environment Variables
747
673
 
748
674
  Copy \`.env.example\` to \`.env\` and configure:
@@ -917,8 +843,8 @@ mistakes:
917
843
  property assignment (\`ctx.tenant = …\`) sticks to the contributor
918
844
  instance only — the handler instance never sees it.
919
845
  - **Read across instances via \`ctx.set\` / \`ctx.get\`** (or
920
- \`requestStore.getStore()?.values.get('key')\` from a service that
921
- has no \`ctx\` reference). \`ctx.req\` works because the underlying
846
+ \`getRequestValue(key)\` from a service that has no \`ctx\` reference
847
+ typed via \`MetaValue<K>\`). \`ctx.req\` works because the underlying
922
848
  Express request is shared; bespoke property assignments don't.
923
849
 
924
850
  - **Test isolation** — default to \`Container.create()\` for fresh DI state.
@@ -986,7 +912,7 @@ package additions, env access patterns, troubleshooting) is detailed below.
986
912
  | Module registry | \`src/modules/index.ts\` |
987
913
  | Feature modules | \`src/modules/<module-name>/\` |
988
914
  | **Module entry file** | \`src/modules/<name>/<name>.module.ts\` (filename suffix is required — see Vite HMR contract below) |
989
- ${template === "graphql" ? "| GraphQL resolvers | `src/resolvers/` |\n" : ""}| Env values | \`.env\` |
915
+ | Env values | \`.env\` |
990
916
  | Env schema (Zod) | \`src/config/index.ts\` |
991
917
  | TypeScript config | \`tsconfig.json\` |
992
918
  | Vite config (HMR) | \`vite.config.ts\` |
@@ -1023,12 +949,6 @@ ${template === "ddd" ? `\`\`\`
1023
949
  ├── <name>.repository.ts # Data access
1024
950
  └── <name>.module.ts # Module definition (implements AppModule)
1025
951
  \`\`\`
1026
- ` : template === "graphql" ? `\`\`\`
1027
- resolvers/
1028
- ├── <name>.resolver.ts # @Resolver, @Query, @Mutation
1029
- ├── <name>.types.ts # GraphQL type definitions
1030
- └── <name>.service.ts # Business logic
1031
- \`\`\`
1032
952
  ` : template === "rest" ? `\`\`\`
1033
953
  <name>/
1034
954
  ├── <name>.controller.ts # HTTP routes (@Controller)
@@ -1301,15 +1221,7 @@ fast). The \`onError\` hook is async-permitted.
1301
1221
 
1302
1222
  Full guide: <https://forinda.github.io/kick-js/guide/context-decorators>.
1303
1223
 
1304
- ${template === "graphql" ? `### GraphQL
1305
- | Decorator | Purpose |
1306
- |-----------|---------|
1307
- | \`@Resolver()\` | GraphQL resolver class |
1308
- | \`@Query()\` | Query handler |
1309
- | \`@Mutation()\` | Mutation handler |
1310
- | \`@Arg('name')\` | Resolver argument |
1311
-
1312
- ` : ""}${template === "cqrs" ? `### Background Jobs
1224
+ ${template === "cqrs" ? `### Background Jobs
1313
1225
  | Decorator | Purpose |
1314
1226
  |-----------|---------|
1315
1227
  | \`@Job('name')\` | Queue job handler |
@@ -1567,10 +1479,11 @@ Precedence high → low: **method > class > module > adapter > global**.
1567
1479
  Cycles or unmet \`dependsOn\` keys throw \`MissingContributorError\` at boot.
1568
1480
 
1569
1481
  **Critical rules — all stem from the same shared-via-ALS instance model**:
1570
- - Every per-request stage (middleware → contributors → handler) gets its OWN \`RequestContext\` instance, but they all read/write the SAME \`AsyncLocalStorage\`-backed Map (\`requestStore.getStore().values\`).
1482
+ - Every per-request stage (middleware → contributors → handler) gets its OWN \`RequestContext\` instance, but they all read/write the SAME \`AsyncLocalStorage\`-backed bag.
1571
1483
  - **\`resolve\` and \`onError\` must RETURN the value** — the runner writes it via \`ctx.set(key, value)\`. Direct property assignment (\`ctx.tenant = …\`) sticks to one instance only and the handler instance never sees it.
1572
1484
  - \`ctx.set('tenant', x)\` then \`ctx.get('tenant')\` works across instances. \`ctx.req.headers[...]\` works (the underlying Express request is shared).
1573
- - Services can read contributor output without a \`ctx\` reference via \`requestStore.getStore()?.values.get('tenant')\` same Map, no DI plumbing needed.
1485
+ - Services with no \`ctx\` reference: \`getRequestValue('tenant')\` returns \`MetaValue<'tenant'> | undefined\` (typed via the augmented \`ContextMeta\`). For \`requestId\` use \`getRequestStore()\`.
1486
+ - **No \`setRequestValue\` — writes flow through \`ctx.set\` or a contributor's return value.** Avoids "spooky action at a distance" where any service can pollute the per-request bag.
1574
1487
 
1575
1488
  **Don't use this for**: response short-circuit, stream mutation, or
1576
1489
  pre-route-matching work — keep \`@Middleware()\` for those.
@@ -1643,7 +1556,6 @@ async function initProject(options) {
1643
1556
  await writeFileSafe(join(dir, "src/modules/hello/hello.service.ts"), generateHelloService());
1644
1557
  await writeFileSafe(join(dir, "src/modules/hello/hello.controller.ts"), generateHelloController());
1645
1558
  await writeFileSafe(join(dir, "src/modules/hello/hello.module.ts"), generateHelloModule());
1646
- if (template === "graphql") await writeFileSafe(join(dir, "src/resolvers/.gitkeep"), "");
1647
1559
  await writeFileSafe(join(dir, "kick.config.ts"), generateKickConfig(template, defaultRepo, packageManager));
1648
1560
  await writeFileSafe(join(dir, "vitest.config.ts"), generateVitestConfig());
1649
1561
  await writeFileSafe(join(dir, "README.md"), generateReadme(name, template, packageManager));
@@ -1699,7 +1611,6 @@ async function initProject(options) {
1699
1611
  if (!options.installDeps) log(` ${packageManager} install`);
1700
1612
  const genHint = {
1701
1613
  rest: "kick g module user",
1702
- graphql: "kick g resolver user",
1703
1614
  ddd: "kick g module user --repo drizzle",
1704
1615
  cqrs: "kick g module user --pattern cqrs",
1705
1616
  minimal: "# add your routes to src/index.ts"
@@ -1721,7 +1632,6 @@ async function initProject(options) {
1721
1632
  log(" kick g guard <name> Route guard (auth, roles, etc.)");
1722
1633
  log(" kick g adapter <name> AppAdapter with lifecycle hooks");
1723
1634
  log(" kick g dto <name> Zod DTO schema");
1724
- if (template === "graphql") log(" kick g resolver <name> GraphQL resolver");
1725
1635
  if (template === "cqrs") log(" kick g job <name> Queue job processor");
1726
1636
  log(" kick g config Generate kick.config.ts");
1727
1637
  log("");
@@ -1729,8 +1639,7 @@ async function initProject(options) {
1729
1639
  log(" kick add <pkg> Install a KickJS package + peers");
1730
1640
  log(" kick add --list Show all available packages");
1731
1641
  log("");
1732
- log("Available: auth, swagger, graphql, drizzle, prisma, ws, cron,");
1733
- log(" queue, mailer, otel, devtools, multi-tenant, notifications, mcp, testing");
1642
+ log("Available: auth, swagger, drizzle, prisma, ws, queue, devtools, mcp, testing");
1734
1643
  log("");
1735
1644
  }
1736
1645
  //#endregion
@@ -1816,11 +1725,6 @@ const OPTIONAL_PACKAGES = [
1816
1725
  label: "Swagger",
1817
1726
  hint: "OpenAPI docs"
1818
1727
  },
1819
- {
1820
- value: "otel",
1821
- label: "OpenTelemetry",
1822
- hint: "tracing & metrics"
1823
- },
1824
1728
  {
1825
1729
  value: "ws",
1826
1730
  label: "WebSocket",
@@ -1831,39 +1735,14 @@ const OPTIONAL_PACKAGES = [
1831
1735
  label: "Queue",
1832
1736
  hint: "BullMQ/RabbitMQ/Kafka"
1833
1737
  },
1834
- {
1835
- value: "cron",
1836
- label: "Cron",
1837
- hint: "scheduled jobs"
1838
- },
1839
- {
1840
- value: "mailer",
1841
- label: "Mailer",
1842
- hint: "SMTP, Resend, SES"
1843
- },
1844
- {
1845
- value: "graphql",
1846
- label: "GraphQL",
1847
- hint: "resolvers, GraphiQL"
1848
- },
1849
1738
  {
1850
1739
  value: "devtools",
1851
1740
  label: "DevTools",
1852
1741
  hint: "debug dashboard"
1853
- },
1854
- {
1855
- value: "notifications",
1856
- label: "Notifications",
1857
- hint: "email, Slack, Discord"
1858
- },
1859
- {
1860
- value: "multi-tenant",
1861
- label: "Multi-Tenant",
1862
- hint: "tenant resolution"
1863
1742
  }
1864
1743
  ];
1865
1744
  function registerInitCommand(program) {
1866
- 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 | graphql | 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,otel)").action(async (name, opts) => {
1745
+ 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) => {
1867
1746
  intro("KickJS — Create a new project");
1868
1747
  if (!name) name = await text({
1869
1748
  message: "Project name",
@@ -1907,11 +1786,6 @@ function registerInitCommand(program) {
1907
1786
  label: "REST API",
1908
1787
  hint: "Express + Swagger"
1909
1788
  },
1910
- {
1911
- value: "graphql",
1912
- label: "GraphQL API",
1913
- hint: "GraphQL + GraphiQL"
1914
- },
1915
1789
  {
1916
1790
  value: "ddd",
1917
1791
  label: "DDD",
@@ -4060,7 +3934,6 @@ function resolveRepoType(config) {
4060
3934
  * Patterns:
4061
3935
  * rest — flat folder: controller + service + DTOs + repo
4062
3936
  * ddd — nested DDD: presentation/ application/ domain/ infrastructure/
4063
- * graphql — flat folder: resolver + service + DTOs + repo (future)
4064
3937
  * cqrs — commands, queries, events with WS/queue integration
4065
3938
  * minimal — just controller + module index
4066
3939
  */
@@ -4173,7 +4046,13 @@ async function generateAdapter(options) {
4173
4046
  const pascal = toPascalCase(name);
4174
4047
  const files = [];
4175
4048
  const filePath = join(outDir, `${kebab}.adapter.ts`);
4176
- await writeFileSafe(filePath, `import { defineAdapter, type AdapterContext, type AdapterMiddleware } from '@forinda/kickjs'
4049
+ await writeFileSafe(filePath, `import {
4050
+ defineAdapter,
4051
+ type AdapterContext,
4052
+ type AdapterMiddleware,
4053
+ type ContributorRegistrations,
4054
+ type Constructor,
4055
+ } from '@forinda/kickjs'
4177
4056
 
4178
4057
  /**
4179
4058
  * Configuration for the ${pascal} adapter.
@@ -4194,7 +4073,12 @@ export interface ${pascal}AdapterConfig {
4194
4073
  * factory's call / \`.scoped()\` / \`.async()\` surfaces for free.
4195
4074
  *
4196
4075
  * Hooks into the Application lifecycle to add middleware, routes,
4197
- * or external service connections.
4076
+ * Context Contributors, or external service connections.
4077
+ *
4078
+ * Every lifecycle hook below is OPTIONAL. The scaffold emits all of
4079
+ * them so adopters can browse what's available and delete what they
4080
+ * don't need — \`build()\` returning \`{}\` is also valid for an adapter
4081
+ * that only contributes config defaults.
4198
4082
  *
4199
4083
  * @example
4200
4084
  * \`\`\`ts
@@ -4210,59 +4094,126 @@ export interface ${pascal}AdapterConfig {
4210
4094
  export const ${pascal}Adapter = defineAdapter<${pascal}AdapterConfig>({
4211
4095
  name: '${pascal}Adapter',
4212
4096
  defaults: {
4213
- // Default config values go here
4097
+ // Default config values go here. The adopter's overrides shallow-merge
4098
+ // on top of these before \`build()\` runs.
4214
4099
  },
4215
- build: (_config, { name: _name }) => ({
4216
- /**
4217
- * Return middleware entries that the Application will mount.
4218
- * \`phase\` controls where in the pipeline they run:
4219
- * 'beforeGlobal' | 'afterGlobal' | 'beforeRoutes' | 'afterRoutes'.
4220
- */
4221
- middleware(): AdapterMiddleware[] {
4222
- return [
4223
- // Example: add a custom header to all responses
4224
- // {
4225
- // phase: 'beforeGlobal',
4226
- // handler: (_req, res, next) => {
4227
- // res.setHeader('X-${pascal}', 'true')
4228
- // next()
4229
- // },
4230
- // },
4231
- ]
4232
- },
4233
-
4234
- /**
4235
- * Called before global middleware. Use this to mount routes that
4236
- * bypass the middleware stack (health checks, docs UI, static
4237
- * assets).
4238
- */
4239
- beforeMount(_ctx: AdapterContext): void {
4240
- // Example:
4241
- // _ctx.app.get('/${kebab}/status', (_req, res) => res.json({ status: 'ok' }))
4242
- },
4243
-
4244
- /**
4245
- * Called after modules and routes are registered, before the
4246
- * server starts. Use this for late-stage DI registrations or
4247
- * config validation.
4248
- */
4249
- beforeStart(_ctx: AdapterContext): void {
4250
- // Example: _ctx.container.bindToken(MY_TOKEN, new MyService(_config))
4251
- },
4100
+ build: (_config, { name: _name }) => {
4101
+ // Closures inside \`build()\` are how each adapter instance owns its
4102
+ // own state (database client, Map, timer handle, …). The same
4103
+ // \`_config\` is visible to every hook below.
4252
4104
 
4253
- /**
4254
- * Called after the HTTP server is listening. Use this to attach
4255
- * to the raw http.Server (Socket.IO, gRPC, etc).
4256
- */
4257
- afterStart(_ctx: AdapterContext): void {
4258
- // Example: const io = new Server(_ctx.server)
4259
- },
4260
-
4261
- /** Called on graceful shutdown. Clean up connections. */
4262
- async shutdown(): Promise<void> {
4263
- // Example: await this.pool.end()
4264
- },
4265
- }),
4105
+ return {
4106
+ /**
4107
+ * Express middleware entries the Application mounts at named phases.
4108
+ *
4109
+ * \`phase\` controls where each handler sits in the pipeline:
4110
+ * 'beforeGlobal' | 'afterGlobal' | 'beforeRoutes' | 'afterRoutes'.
4111
+ *
4112
+ * \`path\` (optional) scopes the entry to a path prefix.
4113
+ *
4114
+ * Delete this hook entirely if you don't add middleware.
4115
+ */
4116
+ middleware(): AdapterMiddleware[] {
4117
+ return [
4118
+ // Example: add a custom header to all responses
4119
+ // {
4120
+ // phase: 'beforeGlobal',
4121
+ // handler: (_req, res, next) => {
4122
+ // res.setHeader('X-${pascal}', 'true')
4123
+ // next()
4124
+ // },
4125
+ // },
4126
+ // Example: scope a rate limiter to one path prefix
4127
+ // {
4128
+ // phase: 'beforeRoutes',
4129
+ // path: '/api/v1/auth',
4130
+ // handler: rateLimit({ max: 10 }),
4131
+ // },
4132
+ ]
4133
+ },
4134
+
4135
+ /**
4136
+ * Runs BEFORE global middleware. Mount routes that should bypass the
4137
+ * middleware stack — health checks, docs UI, static assets, OAuth
4138
+ * callbacks. Anything you want reachable even if a global middleware
4139
+ * later in the chain rejects requests.
4140
+ *
4141
+ * Delete this hook if you have no early routes.
4142
+ */
4143
+ beforeMount(_ctx: AdapterContext): void {
4144
+ // Example:
4145
+ // _ctx.app.get('/${kebab}/status', (_req, res) => res.json({ status: 'ok' }))
4146
+ },
4147
+
4148
+ /**
4149
+ * Fires once per controller class as the router mounts. Use this to
4150
+ * collect route metadata for OpenAPI specs, dependency graphs, route
4151
+ * inventories, devtools dashboards.
4152
+ *
4153
+ * Delete this hook unless your adapter introspects the route registry.
4154
+ */
4155
+ onRouteMount(_controllerClass: Constructor, _mountPath: string): void {
4156
+ // Example (Swagger-style): collect routes for the spec.
4157
+ // openApiSpec.addController(_controllerClass, _mountPath)
4158
+ },
4159
+
4160
+ /**
4161
+ * Runs AFTER modules + routes are wired, BEFORE the server starts.
4162
+ * Right place for late-stage DI registrations or final config validation.
4163
+ *
4164
+ * Delete this hook if there's nothing to wire post-modules.
4165
+ */
4166
+ beforeStart(_ctx: AdapterContext): void {
4167
+ // Example: _ctx.container.registerInstance(MY_TOKEN, new MyService(_config))
4168
+ },
4169
+
4170
+ /**
4171
+ * Runs AFTER the HTTP server is listening. The raw \`http.Server\` is
4172
+ * available on \`ctx.server\` — attach upgrade handlers (Socket.IO,
4173
+ * gRPC, GraphQL subscriptions), warm caches, log a banner.
4174
+ *
4175
+ * Delete this hook if you don't need the running server reference.
4176
+ */
4177
+ afterStart(_ctx: AdapterContext): void {
4178
+ // Example: const io = new Server(_ctx.server)
4179
+ },
4180
+
4181
+ /**
4182
+ * Returns Context Contributors to merge into every route's pipeline
4183
+ * at the \`'adapter'\` precedence level. Per-route handlers can
4184
+ * override the value at the method / class / module level.
4185
+ *
4186
+ * Delete this hook unless your adapter ships typed per-request values
4187
+ * (auth user, tenant, locale, feature flags, geo, etc).
4188
+ */
4189
+ contributors(): ContributorRegistrations {
4190
+ return [
4191
+ // Example:
4192
+ // import { defineHttpContextDecorator } from '@forinda/kickjs'
4193
+ // declare module '@forinda/kickjs' { interface ContextMeta { ${kebab}: { id: string } } }
4194
+ // const Load${pascal} = defineHttpContextDecorator({
4195
+ // key: '${kebab}',
4196
+ // resolve: (ctx) => ({ id: ctx.req.headers['x-${kebab}-id'] as string }),
4197
+ // })
4198
+ // return [Load${pascal}.registration]
4199
+ ]
4200
+ },
4201
+
4202
+ /**
4203
+ * Runs on graceful shutdown (SIGINT/SIGTERM). Clean up long-lived
4204
+ * resources the adapter owns: close connections, flush buffers,
4205
+ * cancel timers. The framework runs every adapter's \`shutdown\`
4206
+ * concurrently via \`Promise.allSettled\` — one failure won't block
4207
+ * sibling adapters.
4208
+ *
4209
+ * Delete this hook if your adapter holds no resources.
4210
+ */
4211
+ async shutdown(): Promise<void> {
4212
+ // Example: await this.pool.end()
4213
+ // Example: clearInterval(this.heartbeatTimer)
4214
+ },
4215
+ }
4216
+ },
4266
4217
  })
4267
4218
  `);
4268
4219
  files.push(filePath);
@@ -4290,6 +4241,7 @@ async function generatePlugin(options) {
4290
4241
  type AppAdapter,
4291
4242
  type AppModuleClass,
4292
4243
  type Container,
4244
+ type ContributorRegistrations,
4293
4245
  } from '@forinda/kickjs'
4294
4246
 
4295
4247
  /**
@@ -4321,8 +4273,9 @@ export interface ${pascal}PluginConfig {
4321
4273
  * 2. \`modules()\` — plugin modules load before user modules.
4322
4274
  * 3. \`adapters()\` — plugin adapters mount before user adapters.
4323
4275
  * 4. \`middleware()\` — plugin middleware runs before user middleware.
4324
- * 5. \`onReady(container)\` runs after the app has fully bootstrapped.
4325
- * 6. \`shutdown()\` — runs on graceful shutdown.
4276
+ * 5. \`contributors()\` Context Contributors merged into every route.
4277
+ * 6. \`onReady(container)\` — runs after the app has fully bootstrapped.
4278
+ * 7. \`shutdown()\` — runs on graceful shutdown.
4326
4279
  *
4327
4280
  * @example
4328
4281
  * \`\`\`ts
@@ -4383,6 +4336,27 @@ export const ${pascal}Plugin = definePlugin<${pascal}PluginConfig>({
4383
4336
  ]
4384
4337
  },
4385
4338
 
4339
+ /**
4340
+ * Return Context Contributors to merge into every route's pipeline.
4341
+ * Plugins contribute at the same \`'adapter'\` precedence level as
4342
+ * adapters — overrideable per-route at the method / class / module
4343
+ * level. See https://forinda.github.io/kick-js/guide/context-decorators
4344
+ *
4345
+ * Delete this hook if your plugin doesn't ship typed per-request values.
4346
+ */
4347
+ contributors(): ContributorRegistrations {
4348
+ return [
4349
+ // Example:
4350
+ // import { defineHttpContextDecorator } from '@forinda/kickjs'
4351
+ // declare module '@forinda/kickjs' { interface ContextMeta { ${kebab}: { foo: string } } }
4352
+ // const Load${pascal} = defineHttpContextDecorator({
4353
+ // key: '${kebab}',
4354
+ // resolve: (ctx) => ({ foo: ctx.req.headers['x-${kebab}'] as string }),
4355
+ // })
4356
+ // return [Load${pascal}.registration]
4357
+ ]
4358
+ },
4359
+
4386
4360
  /**
4387
4361
  * Called after the application has fully bootstrapped. Use this
4388
4362
  * for post-startup work like logging, health checks, or warming
@@ -4839,7 +4813,6 @@ function escapesRoot$1(path, root) {
4839
4813
  //#region src/generators/agent-docs.ts
4840
4814
  const VALID_TEMPLATES = new Set([
4841
4815
  "rest",
4842
- "graphql",
4843
4816
  "ddd",
4844
4817
  "cqrs",
4845
4818
  "minimal"
@@ -5151,82 +5124,6 @@ export class AuthService {
5151
5124
  `;
5152
5125
  }
5153
5126
  //#endregion
5154
- //#region src/generators/resolver.ts
5155
- async function generateResolver(options) {
5156
- const { name, outDir } = options;
5157
- const pascal = toPascalCase(name);
5158
- const kebab = toKebabCase(name);
5159
- const camel = toCamelCase(name);
5160
- const files = [];
5161
- const write = async (relativePath, content) => {
5162
- const fullPath = join(outDir, relativePath);
5163
- await writeFileSafe(fullPath, content);
5164
- files.push(fullPath);
5165
- };
5166
- await write(`${kebab}.resolver.ts`, `import { Service } from '@forinda/kickjs'
5167
- import { Resolver, Query, Mutation, Arg } from '@forinda/kickjs-graphql'
5168
-
5169
- /**
5170
- * ${pascal} GraphQL Resolver
5171
- *
5172
- * Decorators:
5173
- * @Resolver(typeName?) — marks this class as a GraphQL resolver
5174
- * @Query(name?, { returnType?, description? }) — defines a query field
5175
- * @Mutation(name?, { returnType?, description? }) — defines a mutation field
5176
- * @Arg(name, type?) — marks a method parameter as a GraphQL argument
5177
- */
5178
- @Service()
5179
- @Resolver('${pascal}')
5180
- export class ${pascal}Resolver {
5181
- private items: Array<{ id: string; name: string }> = []
5182
-
5183
- @Query('${camel}s', { returnType: '[${pascal}]', description: 'List all ${camel}s' })
5184
- findAll() {
5185
- return this.items
5186
- }
5187
-
5188
- @Query('${camel}', { returnType: '${pascal}', description: 'Get a ${camel} by ID' })
5189
- findById(@Arg('id', 'ID!') id: string) {
5190
- return this.items.find((item) => item.id === id) ?? null
5191
- }
5192
-
5193
- @Mutation('create${pascal}', { returnType: '${pascal}', description: 'Create a new ${camel}' })
5194
- create(@Arg('name', 'String!') name: string) {
5195
- const item = { id: String(this.items.length + 1), name }
5196
- this.items.push(item)
5197
- return item
5198
- }
5199
-
5200
- @Mutation('update${pascal}', { returnType: '${pascal}', description: 'Update a ${camel}' })
5201
- update(@Arg('id', 'ID!') id: string, @Arg('name', 'String!') name: string) {
5202
- const item = this.items.find((i) => i.id === id)
5203
- if (item) item.name = name
5204
- return item
5205
- }
5206
-
5207
- @Mutation('delete${pascal}', { returnType: 'Boolean', description: 'Delete a ${camel}' })
5208
- remove(@Arg('id', 'ID!') id: string) {
5209
- const idx = this.items.findIndex((i) => i.id === id)
5210
- if (idx === -1) return false
5211
- this.items.splice(idx, 1)
5212
- return true
5213
- }
5214
- }
5215
- `);
5216
- await write(`${kebab}.typedefs.ts`, `/**
5217
- * ${pascal} GraphQL type definitions.
5218
- * Pass to GraphQLAdapter's typeDefs option to register custom types.
5219
- */
5220
- export const ${camel}TypeDefs = \`
5221
- type ${pascal} {
5222
- id: ID!
5223
- name: String!
5224
- }
5225
- \`
5226
- `);
5227
- return files;
5228
- }
5229
- //#endregion
5230
5127
  //#region src/generators/job.ts
5231
5128
  async function generateJob(options) {
5232
5129
  const { name, outDir } = options;
@@ -6389,10 +6286,8 @@ function extractAugmentationsFromSource(source, filePath, cwd) {
6389
6286
  const closeBrace = findBalancedBrace(source, bracePos);
6390
6287
  if (closeBrace >= 0) {
6391
6288
  const body = source.slice(bracePos + 1, closeBrace);
6392
- const descMatch = /\bdescription\s*:\s*['"`]([^'"`]+)['"`]/.exec(body);
6393
- const exampleMatch = /\bexample\s*:\s*['"`]([^'"`]+)['"`]/.exec(body);
6394
- description = descMatch ? descMatch[1] : null;
6395
- example = exampleMatch ? exampleMatch[1] : null;
6289
+ description = readStringField(body, "description");
6290
+ example = readStringField(body, "example");
6396
6291
  }
6397
6292
  }
6398
6293
  }
@@ -6407,6 +6302,46 @@ function extractAugmentationsFromSource(source, filePath, cwd) {
6407
6302
  return out;
6408
6303
  }
6409
6304
  /**
6305
+ * Pull a string-valued field out of a JS object-literal body, respecting
6306
+ * the opening quote so the value isn't truncated at the first foreign
6307
+ * quote character. Handles backslash escapes inside the literal.
6308
+ *
6309
+ * Why a custom parser instead of one regex per delimiter: real-world
6310
+ * `defineAugmentation` calls embed all three quote characters at once
6311
+ * — backtick template literals carrying TS shapes like
6312
+ * `'free' | 'pro'` (single quotes) AND `\`ctx.get(...)\`` (escaped
6313
+ * backticks). A character-class regex like `[^'"`]+` truncates on the
6314
+ * first foreign quote it sees. This walker scans char-by-char from
6315
+ * the matched delimiter and only stops on the matching one.
6316
+ */
6317
+ function readStringField(body, field) {
6318
+ const m = new RegExp(`\\b${field}\\s*:\\s*(['"\`])`, "g").exec(body);
6319
+ if (!m) return null;
6320
+ const quote = m[1];
6321
+ const start = m.index + m[0].length;
6322
+ let i = start;
6323
+ let raw = null;
6324
+ while (i < body.length) {
6325
+ const ch = body[i];
6326
+ if (ch === "\\") {
6327
+ i += 2;
6328
+ continue;
6329
+ }
6330
+ if (ch === quote) {
6331
+ raw = body.slice(start, i);
6332
+ break;
6333
+ }
6334
+ i++;
6335
+ }
6336
+ if (raw === null) return null;
6337
+ return raw.replace(/\\(.)/g, (_m, c) => {
6338
+ if (c === "n") return "\n";
6339
+ if (c === "t") return " ";
6340
+ if (c === "r") return "\r";
6341
+ return c;
6342
+ });
6343
+ }
6344
+ /**
6410
6345
  * Default search order for the env schema file. Newer projects keep
6411
6346
  * the schema under `src/config/` so the framework's "config" concept
6412
6347
  * has a single home; older scaffolds dropped it at `src/env.ts` (kept
@@ -7417,10 +7352,6 @@ const GENERATORS = [
7417
7352
  name: "test <name>",
7418
7353
  description: "Vitest test scaffold [-m module]"
7419
7354
  },
7420
- {
7421
- name: "resolver <name>",
7422
- description: "GraphQL @Resolver class"
7423
- },
7424
7355
  {
7425
7356
  name: "job <name>",
7426
7357
  description: "Queue @Job processor"
@@ -7622,14 +7553,6 @@ function registerGenerateCommand(program) {
7622
7553
  pluralize: mc.pluralize ?? true
7623
7554
  }), dryRun);
7624
7555
  });
7625
- gen.command("resolver <name>").description("Generate a GraphQL @Resolver class with @Query and @Mutation methods").option("-o, --out <dir>", "Output directory", "src/resolvers").action(async (name, opts, cmd) => {
7626
- const dryRun = isDryRun(cmd);
7627
- setDryRun(dryRun);
7628
- printGenerated(await generateResolver({
7629
- name,
7630
- outDir: resolve(opts.out)
7631
- }), dryRun);
7632
- });
7633
7556
  gen.command("job <name>").description("Generate a @Job queue processor with @Process handlers").option("-o, --out <dir>", "Output directory", "src/jobs").option("-q, --queue <name>", "Queue name (default: <name>-queue)").action(async (name, opts, cmd) => {
7634
7557
  const dryRun = isDryRun(cmd);
7635
7558
  setDryRun(dryRun);
@@ -7699,7 +7622,7 @@ function registerGenerateCommand(program) {
7699
7622
  force: opts.force
7700
7623
  }), dryRun);
7701
7624
  });
7702
- gen.command("agents").alias("agent-docs").alias("ai-docs").description("Regenerate AGENTS.md + CLAUDE.md + kickjs-skills.md (sync after framework upgrades)").option("--only <which>", "Limit scope: agents | claude | skills | both (agents+claude) | all (default: all)", "all").option("--name <name>", "Project name (defaults to package.json name)").option("--pm <pm>", "Package manager (defaults to package.json packageManager)").option("--template <template>", "Template: rest | graphql | ddd | cqrs | minimal").option("-f, --force", "Overwrite existing files without prompting").action(async (opts, cmd) => {
7625
+ gen.command("agents").alias("agent-docs").alias("ai-docs").description("Regenerate AGENTS.md + CLAUDE.md + kickjs-skills.md (sync after framework upgrades)").option("--only <which>", "Limit scope: agents | claude | skills | both (agents+claude) | all (default: all)", "all").option("--name <name>", "Project name (defaults to package.json name)").option("--pm <pm>", "Package manager (defaults to package.json packageManager)").option("--template <template>", "Template: rest | ddd | cqrs | minimal").option("-f, --force", "Overwrite existing files without prompting").action(async (opts, cmd) => {
7703
7626
  const dryRun = isDryRun(cmd);
7704
7627
  setDryRun(dryRun);
7705
7628
  const only = opts.only ?? "all";
@@ -8282,11 +8205,6 @@ const PACKAGE_REGISTRY = {
8282
8205
  peers: [],
8283
8206
  description: "OpenAPI spec + Swagger UI + ReDoc"
8284
8207
  },
8285
- graphql: {
8286
- pkg: "@forinda/kickjs-graphql",
8287
- peers: ["graphql"],
8288
- description: "GraphQL resolvers + GraphiQL"
8289
- },
8290
8208
  drizzle: {
8291
8209
  pkg: "@forinda/kickjs-drizzle",
8292
8210
  peers: ["drizzle-orm"],
@@ -8302,11 +8220,6 @@ const PACKAGE_REGISTRY = {
8302
8220
  peers: ["socket.io"],
8303
8221
  description: "WebSocket with @WsController decorators"
8304
8222
  },
8305
- otel: {
8306
- pkg: "@forinda/kickjs-otel",
8307
- peers: ["@opentelemetry/api"],
8308
- description: "OpenTelemetry tracing + metrics"
8309
- },
8310
8223
  devtools: {
8311
8224
  pkg: "@forinda/kickjs-devtools",
8312
8225
  peers: [],
@@ -8318,16 +8231,6 @@ const PACKAGE_REGISTRY = {
8318
8231
  peers: ["jsonwebtoken"],
8319
8232
  description: "Authentication — JWT, API key, and custom strategies"
8320
8233
  },
8321
- mailer: {
8322
- pkg: "@forinda/kickjs-mailer",
8323
- peers: ["nodemailer"],
8324
- description: "Email sending — SMTP, Resend, SES, or custom provider"
8325
- },
8326
- cron: {
8327
- pkg: "@forinda/kickjs-cron",
8328
- peers: ["croner"],
8329
- description: "Cron job scheduling (production-grade with croner)"
8330
- },
8331
8234
  queue: {
8332
8235
  pkg: "@forinda/kickjs-queue",
8333
8236
  peers: [],
@@ -8348,16 +8251,6 @@ const PACKAGE_REGISTRY = {
8348
8251
  peers: ["kafkajs"],
8349
8252
  description: "Queue with Kafka"
8350
8253
  },
8351
- "multi-tenant": {
8352
- pkg: "@forinda/kickjs-multi-tenant",
8353
- peers: [],
8354
- description: "Tenant resolution middleware"
8355
- },
8356
- notifications: {
8357
- pkg: "@forinda/kickjs-notifications",
8358
- peers: [],
8359
- description: "Multi-channel notifications — email, Slack, Discord, webhook"
8360
- },
8361
8254
  mcp: {
8362
8255
  pkg: "@forinda/kickjs-mcp",
8363
8256
  peers: ["@modelcontextprotocol/sdk"],
@@ -8411,7 +8304,7 @@ function printPackageList() {
8411
8304
  const peers = info.peers.length ? ` (+ ${info.peers.join(", ")})` : "";
8412
8305
  console.log(` ${padded} ${info.description}${peers}`);
8413
8306
  }
8414
- console.log("\n Usage: kick add graphql drizzle otel");
8307
+ console.log("\n Usage: kick add auth drizzle swagger");
8415
8308
  console.log(" kick add queue:bullmq");
8416
8309
  console.log();
8417
8310
  }