@forinda/kickjs-cli 1.1.2 → 1.2.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.js CHANGED
@@ -5,7 +5,7 @@ var __name = (target, value) => __defProp(target, "name", { value, configurable:
5
5
  // src/cli.ts
6
6
  import { Command } from "commander";
7
7
  import { readFileSync as readFileSync2 } from "fs";
8
- import { dirname as dirname3, join as join16 } from "path";
8
+ import { dirname as dirname3, join as join18 } from "path";
9
9
  import { fileURLToPath as fileURLToPath2 } from "url";
10
10
 
11
11
  // src/commands/init.ts
@@ -61,13 +61,15 @@ async function initProject(options) {
61
61
  };
62
62
  if (template !== "minimal") {
63
63
  baseDeps["@forinda/kickjs-swagger"] = KICKJS_VERSION;
64
+ baseDeps["@forinda/kickjs-devtools"] = KICKJS_VERSION;
64
65
  }
65
66
  if (template === "graphql") {
66
67
  baseDeps["@forinda/kickjs-graphql"] = KICKJS_VERSION;
67
68
  baseDeps["graphql"] = "^16.11.0";
68
69
  }
69
- if (template === "microservice") {
70
+ if (template === "cqrs") {
70
71
  baseDeps["@forinda/kickjs-queue"] = KICKJS_VERSION;
72
+ baseDeps["@forinda/kickjs-ws"] = KICKJS_VERSION;
71
73
  baseDeps["@forinda/kickjs-otel"] = KICKJS_VERSION;
72
74
  }
73
75
  if (template === "ddd") {
@@ -166,12 +168,45 @@ export default defineConfig({
166
168
  printWidth: 100,
167
169
  tabWidth: 2
168
170
  }, null, 2));
171
+ await writeFileSafe(join(dir, ".editorconfig"), `# https://editorconfig.org
172
+ root = true
173
+
174
+ [*]
175
+ indent_style = space
176
+ indent_size = 2
177
+ end_of_line = lf
178
+ charset = utf-8
179
+ trim_trailing_whitespace = true
180
+ insert_final_newline = true
181
+
182
+ [*.md]
183
+ trim_trailing_whitespace = false
184
+ `);
169
185
  await writeFileSafe(join(dir, ".gitignore"), `node_modules/
170
186
  dist/
171
187
  .env
172
188
  coverage/
173
189
  .DS_Store
174
190
  *.tsbuildinfo
191
+ `);
192
+ await writeFileSafe(join(dir, ".gitattributes"), `# Auto-detect text files and normalise line endings to LF
193
+ * text=auto eol=lf
194
+
195
+ # Explicitly mark generated / binary files
196
+ *.png binary
197
+ *.jpg binary
198
+ *.jpeg binary
199
+ *.gif binary
200
+ *.ico binary
201
+ *.woff binary
202
+ *.woff2 binary
203
+ *.ttf binary
204
+ *.eot binary
205
+
206
+ # Lock files \u2014 treat as generated
207
+ pnpm-lock.yaml -diff linguist-generated
208
+ yarn.lock -diff linguist-generated
209
+ package-lock.json -diff linguist-generated
175
210
  `);
176
211
  await writeFileSafe(join(dir, ".env"), `PORT=3000
177
212
  NODE_ENV=development
@@ -275,19 +310,36 @@ export default defineConfig({
275
310
  rest: "kick g module user",
276
311
  graphql: "kick g resolver user",
277
312
  ddd: "kick g module user --repo drizzle",
278
- microservice: "kick g module user && kick g job email",
313
+ cqrs: "kick g module user --pattern cqrs",
279
314
  minimal: "# add your routes to src/index.ts"
280
315
  };
281
316
  console.log(` ${genHint[template] ?? genHint.rest}`);
282
317
  console.log(" kick dev");
283
318
  console.log();
284
319
  console.log(" Commands:");
285
- console.log(" kick dev Start dev server with Vite HMR");
286
- console.log(" kick build Production build via Vite");
287
- console.log(" kick start Run production build");
288
- console.log(` kick g module X Generate a DDD module`);
289
- if (template === "graphql") console.log(" kick g resolver X Generate a GraphQL resolver");
290
- if (template === "microservice") console.log(" kick g job X Generate a queue job processor");
320
+ console.log(" kick dev Start dev server with Vite HMR");
321
+ console.log(" kick build Production build via Vite");
322
+ console.log(" kick start Run production build");
323
+ console.log();
324
+ console.log(" Generators:");
325
+ console.log(" kick g module <name> Full DDD module (controller, DTOs, use-cases, repo)");
326
+ console.log(" kick g scaffold <n> <f..> CRUD module from field definitions");
327
+ console.log(" kick g controller <name> Standalone controller");
328
+ console.log(" kick g service <name> @Service() class");
329
+ console.log(" kick g middleware <name> Express middleware");
330
+ console.log(" kick g guard <name> Route guard (auth, roles, etc.)");
331
+ console.log(" kick g adapter <name> AppAdapter with lifecycle hooks");
332
+ console.log(" kick g dto <name> Zod DTO schema");
333
+ if (template === "graphql") console.log(" kick g resolver <name> GraphQL resolver");
334
+ if (template === "cqrs") console.log(" kick g job <name> Queue job processor");
335
+ console.log(" kick g config Generate kick.config.ts");
336
+ console.log();
337
+ console.log(" Add packages:");
338
+ console.log(" kick add <pkg> Install a KickJS package + peers");
339
+ console.log(" kick add --list Show all available packages");
340
+ console.log();
341
+ console.log(" Available: auth, swagger, graphql, drizzle, prisma, ws,");
342
+ console.log(" cron, queue, mailer, otel, multi-tenant, notifications, testing");
291
343
  console.log();
292
344
  }
293
345
  __name(initProject, "initProject");
@@ -315,12 +367,13 @@ bootstrap({
315
367
  ],
316
368
  })
317
369
  `;
318
- case "microservice":
370
+ case "cqrs":
319
371
  return `import 'reflect-metadata'
320
372
  import { bootstrap } from '@forinda/kickjs-http'
321
373
  import { DevToolsAdapter } from '@forinda/kickjs-devtools'
322
374
  import { SwaggerAdapter } from '@forinda/kickjs-swagger'
323
375
  import { OtelAdapter } from '@forinda/kickjs-otel'
376
+ // import { WsAdapter } from '@forinda/kickjs-ws'
324
377
  // import { QueueAdapter, BullMQProvider } from '@forinda/kickjs-queue'
325
378
  import { modules } from './modules'
326
379
 
@@ -332,6 +385,8 @@ bootstrap({
332
385
  new SwaggerAdapter({
333
386
  info: { title: '${name}', version: '${cliPkg.version}' },
334
387
  }),
388
+ // Uncomment for WebSocket support:
389
+ // new WsAdapter(),
335
390
  // Uncomment when Redis is available:
336
391
  // new QueueAdapter({
337
392
  // provider: new BullMQProvider({ host: 'localhost', port: 6379 }),
@@ -403,7 +458,7 @@ async function confirm(question, defaultYes = true) {
403
458
  }
404
459
  __name(confirm, "confirm");
405
460
  function registerInitCommand(program) {
406
- 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").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 | microservice | minimal").action(async (name, opts) => {
461
+ 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").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").action(async (name, opts) => {
407
462
  console.log();
408
463
  if (!name) {
409
464
  name = await ask("Project name", "my-api");
@@ -451,14 +506,14 @@ function registerInitCommand(program) {
451
506
  "REST API (Express + Swagger)",
452
507
  "GraphQL API (GraphQL + GraphiQL)",
453
508
  "DDD (Domain-Driven Design modules)",
454
- "Microservice (REST + Queue worker)",
509
+ "CQRS (Commands, Queries, Events + WS/Queue)",
455
510
  "Minimal (bare Express)"
456
511
  ], 0);
457
512
  const templateMap = {
458
513
  "REST API (Express + Swagger)": "rest",
459
514
  "GraphQL API (GraphQL + GraphiQL)": "graphql",
460
515
  "DDD (Domain-Driven Design modules)": "ddd",
461
- "Microservice (REST + Queue worker)": "microservice",
516
+ "CQRS (Commands, Queries, Events + WS/Queue)": "cqrs",
462
517
  "Minimal (bare Express)": "minimal"
463
518
  };
464
519
  template = templateMap[template] ?? "rest";
@@ -496,10 +551,11 @@ function registerInitCommand(program) {
496
551
  __name(registerInitCommand, "registerInitCommand");
497
552
 
498
553
  // src/commands/generate.ts
499
- import { resolve as resolve2 } from "path";
554
+ import { resolve as resolve4 } from "path";
500
555
 
501
556
  // src/generators/module.ts
502
557
  import { join as join2 } from "path";
558
+ import { createInterface as createInterface2 } from "readline";
503
559
 
504
560
  // src/utils/naming.ts
505
561
  function toPascalCase(name) {
@@ -536,9 +592,25 @@ __name(pluralizePascal, "pluralizePascal");
536
592
  import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
537
593
 
538
594
  // src/generators/templates/module-index.ts
595
+ function repoMaps(pascal, kebab, repo) {
596
+ const repoClassMap = {
597
+ inmemory: `InMemory${pascal}Repository`,
598
+ drizzle: `Drizzle${pascal}Repository`,
599
+ prisma: `Prisma${pascal}Repository`
600
+ };
601
+ const repoFileMap = {
602
+ inmemory: `in-memory-${kebab}`,
603
+ drizzle: `drizzle-${kebab}`,
604
+ prisma: `prisma-${kebab}`
605
+ };
606
+ return {
607
+ repoClass: repoClassMap[repo] ?? repoClassMap.inmemory,
608
+ repoFile: repoFileMap[repo] ?? repoFileMap.inmemory
609
+ };
610
+ }
611
+ __name(repoMaps, "repoMaps");
539
612
  function generateModuleIndex(pascal, kebab, plural, repo) {
540
- const repoClass = repo === "inmemory" ? `InMemory${pascal}Repository` : `Drizzle${pascal}Repository`;
541
- const repoFile = repo === "inmemory" ? `in-memory-${kebab}` : `drizzle-${kebab}`;
613
+ const { repoClass, repoFile } = repoMaps(pascal, kebab, repo);
542
614
  return `/**
543
615
  * ${pascal} Module
544
616
  *
@@ -591,6 +663,65 @@ export class ${pascal}Module implements AppModule {
591
663
  `;
592
664
  }
593
665
  __name(generateModuleIndex, "generateModuleIndex");
666
+ function generateRestModuleIndex(pascal, kebab, plural, repo) {
667
+ const { repoClass, repoFile } = repoMaps(pascal, kebab, repo);
668
+ return `/**
669
+ * ${pascal} Module
670
+ *
671
+ * REST module with a flat folder structure.
672
+ * Controller delegates to service, service wraps the repository.
673
+ *
674
+ * Structure:
675
+ * ${kebab}.controller.ts \u2014 HTTP routes (CRUD)
676
+ * ${kebab}.service.ts \u2014 Business logic
677
+ * ${kebab}.repository.ts \u2014 Repository interface
678
+ * ${repoFile}.repository.ts \u2014 Repository implementation
679
+ * dtos/ \u2014 Request/response schemas
680
+ */
681
+ import { Container, type AppModule, type ModuleRoutes } from '@forinda/kickjs-core'
682
+ import { buildRoutes } from '@forinda/kickjs-http'
683
+ import { ${pascal.toUpperCase()}_REPOSITORY } from './${kebab}.repository'
684
+ import { ${repoClass} } from './${repoFile}.repository'
685
+ import { ${pascal}Controller } from './${kebab}.controller'
686
+
687
+ // Eagerly load decorated classes so @Service()/@Repository() decorators register in the DI container
688
+ import.meta.glob(['./**/*.service.ts', './**/*.repository.ts', '!./**/*.test.ts'], { eager: true })
689
+
690
+ export class ${pascal}Module implements AppModule {
691
+ register(container: Container): void {
692
+ container.registerFactory(${pascal.toUpperCase()}_REPOSITORY, () =>
693
+ container.resolve(${repoClass}),
694
+ )
695
+ }
696
+
697
+ routes(): ModuleRoutes {
698
+ return {
699
+ path: '/${plural}',
700
+ router: buildRoutes(${pascal}Controller),
701
+ controller: ${pascal}Controller,
702
+ }
703
+ }
704
+ }
705
+ `;
706
+ }
707
+ __name(generateRestModuleIndex, "generateRestModuleIndex");
708
+ function generateMinimalModuleIndex(pascal, kebab, plural) {
709
+ return `import { type AppModule, type ModuleRoutes } from '@forinda/kickjs-core'
710
+ import { buildRoutes } from '@forinda/kickjs-http'
711
+ import { ${pascal}Controller } from './${kebab}.controller'
712
+
713
+ export class ${pascal}Module implements AppModule {
714
+ routes(): ModuleRoutes {
715
+ return {
716
+ path: '/${plural}',
717
+ router: buildRoutes(${pascal}Controller),
718
+ controller: ${pascal}Controller,
719
+ }
720
+ }
721
+ }
722
+ `;
723
+ }
724
+ __name(generateMinimalModuleIndex, "generateMinimalModuleIndex");
594
725
 
595
726
  // src/generators/templates/controller.ts
596
727
  function generateController(pascal, kebab, plural, pluralPascal) {
@@ -656,6 +787,62 @@ export class ${pascal}Controller {
656
787
  `;
657
788
  }
658
789
  __name(generateController, "generateController");
790
+ function generateRestController(pascal, kebab, plural, pluralPascal) {
791
+ const camel = pascal.charAt(0).toLowerCase() + pascal.slice(1);
792
+ return `import { Controller, Get, Post, Put, Delete, Autowired, ApiQueryParams } from '@forinda/kickjs-core'
793
+ import type { RequestContext } from '@forinda/kickjs-http'
794
+ import { ApiTags } from '@forinda/kickjs-swagger'
795
+ import { ${pascal}Service } from './${kebab}.service'
796
+ import { create${pascal}Schema } from './dtos/create-${kebab}.dto'
797
+ import { update${pascal}Schema } from './dtos/update-${kebab}.dto'
798
+ import { ${pascal.toUpperCase()}_QUERY_CONFIG } from './${kebab}.constants'
799
+
800
+ @Controller()
801
+ export class ${pascal}Controller {
802
+ @Autowired() private ${camel}Service!: ${pascal}Service
803
+
804
+ @Get('/')
805
+ @ApiTags('${pascal}')
806
+ @ApiQueryParams(${pascal.toUpperCase()}_QUERY_CONFIG)
807
+ async list(ctx: RequestContext) {
808
+ return ctx.paginate(
809
+ (parsed) => this.${camel}Service.findPaginated(parsed),
810
+ ${pascal.toUpperCase()}_QUERY_CONFIG,
811
+ )
812
+ }
813
+
814
+ @Get('/:id')
815
+ @ApiTags('${pascal}')
816
+ async getById(ctx: RequestContext) {
817
+ const result = await this.${camel}Service.findById(ctx.params.id)
818
+ if (!result) return ctx.notFound('${pascal} not found')
819
+ ctx.json(result)
820
+ }
821
+
822
+ @Post('/', { body: create${pascal}Schema, name: 'Create${pascal}' })
823
+ @ApiTags('${pascal}')
824
+ async create(ctx: RequestContext) {
825
+ const result = await this.${camel}Service.create(ctx.body)
826
+ ctx.created(result)
827
+ }
828
+
829
+ @Put('/:id', { body: update${pascal}Schema, name: 'Update${pascal}' })
830
+ @ApiTags('${pascal}')
831
+ async update(ctx: RequestContext) {
832
+ const result = await this.${camel}Service.update(ctx.params.id, ctx.body)
833
+ ctx.json(result)
834
+ }
835
+
836
+ @Delete('/:id')
837
+ @ApiTags('${pascal}')
838
+ async remove(ctx: RequestContext) {
839
+ await this.${camel}Service.delete(ctx.params.id)
840
+ ctx.noContent()
841
+ }
842
+ }
843
+ `;
844
+ }
845
+ __name(generateRestController, "generateRestController");
659
846
 
660
847
  // src/generators/templates/constants.ts
661
848
  function generateConstants(pascal) {
@@ -819,20 +1006,19 @@ export class Delete${pascal}UseCase {
819
1006
  __name(generateUseCases, "generateUseCases");
820
1007
 
821
1008
  // src/generators/templates/repository.ts
822
- function generateRepositoryInterface(pascal, kebab) {
1009
+ function generateRepositoryInterface(pascal, kebab, dtoPrefix = "../../application/dtos") {
823
1010
  return `/**
824
1011
  * ${pascal} Repository Interface
825
1012
  *
826
- * Domain layer \u2014 defines the contract for data access.
827
- * The interface lives in the domain layer; implementations live in infrastructure.
828
- * This inversion of dependencies keeps the domain pure and testable.
1013
+ * Defines the contract for data access.
1014
+ * The interface declares what operations are available;
1015
+ * implementations (in-memory, Drizzle, Prisma) fulfill the contract.
829
1016
  *
830
- * To swap implementations (e.g. in-memory -> Drizzle -> Prisma),
831
- * change the factory in the module's register() method.
1017
+ * To swap implementations, change the factory in the module's register() method.
832
1018
  */
833
- import type { ${pascal}ResponseDTO } from '../../application/dtos/${kebab}-response.dto'
834
- import type { Create${pascal}DTO } from '../../application/dtos/create-${kebab}.dto'
835
- import type { Update${pascal}DTO } from '../../application/dtos/update-${kebab}.dto'
1019
+ import type { ${pascal}ResponseDTO } from '${dtoPrefix}/${kebab}-response.dto'
1020
+ import type { Create${pascal}DTO } from '${dtoPrefix}/create-${kebab}.dto'
1021
+ import type { Update${pascal}DTO } from '${dtoPrefix}/update-${kebab}.dto'
836
1022
  import type { ParsedQuery } from '@forinda/kickjs-http'
837
1023
 
838
1024
  export interface I${pascal}Repository {
@@ -848,11 +1034,11 @@ export const ${pascal.toUpperCase()}_REPOSITORY = Symbol('I${pascal}Repository')
848
1034
  `;
849
1035
  }
850
1036
  __name(generateRepositoryInterface, "generateRepositoryInterface");
851
- function generateInMemoryRepository(pascal, kebab) {
1037
+ function generateInMemoryRepository(pascal, kebab, repoPrefix = "../../domain/repositories", dtoPrefix = "../../application/dtos") {
852
1038
  return `/**
853
1039
  * In-Memory ${pascal} Repository
854
1040
  *
855
- * Infrastructure layer \u2014 implements the repository interface using a Map.
1041
+ * Implements the repository interface using a Map.
856
1042
  * Useful for prototyping and testing. Replace with a database implementation
857
1043
  * (Drizzle, Prisma, etc.) for production use.
858
1044
  *
@@ -861,10 +1047,10 @@ function generateInMemoryRepository(pascal, kebab) {
861
1047
  import { randomUUID } from 'node:crypto'
862
1048
  import { Repository, HttpException } from '@forinda/kickjs-core'
863
1049
  import type { ParsedQuery } from '@forinda/kickjs-http'
864
- import type { I${pascal}Repository } from '../../domain/repositories/${kebab}.repository'
865
- import type { ${pascal}ResponseDTO } from '../../application/dtos/${kebab}-response.dto'
866
- import type { Create${pascal}DTO } from '../../application/dtos/create-${kebab}.dto'
867
- import type { Update${pascal}DTO } from '../../application/dtos/update-${kebab}.dto'
1050
+ import type { I${pascal}Repository } from '${repoPrefix}/${kebab}.repository'
1051
+ import type { ${pascal}ResponseDTO } from '${dtoPrefix}/${kebab}-response.dto'
1052
+ import type { Create${pascal}DTO } from '${dtoPrefix}/create-${kebab}.dto'
1053
+ import type { Update${pascal}DTO } from '${dtoPrefix}/update-${kebab}.dto'
868
1054
 
869
1055
  @Repository()
870
1056
  export class InMemory${pascal}Repository implements I${pascal}Repository {
@@ -912,6 +1098,162 @@ export class InMemory${pascal}Repository implements I${pascal}Repository {
912
1098
  `;
913
1099
  }
914
1100
  __name(generateInMemoryRepository, "generateInMemoryRepository");
1101
+ function generateDrizzleRepository(pascal, kebab, repoPrefix = "../../domain/repositories", dtoPrefix = "../../application/dtos") {
1102
+ return `/**
1103
+ * Drizzle ${pascal} Repository
1104
+ *
1105
+ * Implements the repository interface using Drizzle ORM.
1106
+ * Requires a Drizzle database instance injected via the DI container.
1107
+ *
1108
+ * TODO: Update the schema import to match your Drizzle schema file.
1109
+ * TODO: Replace 'db' injection token with your actual database token.
1110
+ *
1111
+ * @Repository() registers this class in the DI container as a singleton.
1112
+ */
1113
+ import { eq, sql } from 'drizzle-orm'
1114
+ import { Repository, HttpException, Autowired } from '@forinda/kickjs-core'
1115
+ import type { ParsedQuery } from '@forinda/kickjs-http'
1116
+ import type { I${pascal}Repository } from '${repoPrefix}/${kebab}.repository'
1117
+ import type { ${pascal}ResponseDTO } from '${dtoPrefix}/${kebab}-response.dto'
1118
+ import type { Create${pascal}DTO } from '${dtoPrefix}/create-${kebab}.dto'
1119
+ import type { Update${pascal}DTO } from '${dtoPrefix}/update-${kebab}.dto'
1120
+
1121
+ // TODO: Import your Drizzle schema table \u2014 e.g.:
1122
+ // import { ${kebab}s } from '@/db/schema'
1123
+
1124
+ // TODO: Import your Drizzle DB injection token \u2014 e.g.:
1125
+ // import { DRIZZLE_DB } from '@/db/drizzle.provider'
1126
+
1127
+ @Repository()
1128
+ export class Drizzle${pascal}Repository implements I${pascal}Repository {
1129
+ // TODO: Uncomment and configure your Drizzle DB injection:
1130
+ // @Autowired(DRIZZLE_DB) private db!: DrizzleDB
1131
+
1132
+ async findById(id: string): Promise<${pascal}ResponseDTO | null> {
1133
+ // TODO: Implement with Drizzle
1134
+ // const [row] = await this.db.select().from(${kebab}s).where(eq(${kebab}s.id, id))
1135
+ // return row ?? null
1136
+ throw new Error('Drizzle ${pascal} repository not yet implemented \u2014 update schema imports and queries')
1137
+ }
1138
+
1139
+ async findAll(): Promise<${pascal}ResponseDTO[]> {
1140
+ // TODO: Implement with Drizzle
1141
+ // return this.db.select().from(${kebab}s)
1142
+ throw new Error('Drizzle ${pascal} repository not yet implemented')
1143
+ }
1144
+
1145
+ async findPaginated(parsed: ParsedQuery): Promise<{ data: ${pascal}ResponseDTO[]; total: number }> {
1146
+ // TODO: Implement with Drizzle
1147
+ // const data = await this.db.select().from(${kebab}s)
1148
+ // .limit(parsed.pagination.limit)
1149
+ // .offset(parsed.pagination.offset)
1150
+ // const [{ count }] = await this.db.select({ count: sql\`count(*)\` }).from(${kebab}s)
1151
+ // return { data, total: Number(count) }
1152
+ throw new Error('Drizzle ${pascal} repository not yet implemented')
1153
+ }
1154
+
1155
+ async create(dto: Create${pascal}DTO): Promise<${pascal}ResponseDTO> {
1156
+ // TODO: Implement with Drizzle
1157
+ // const [row] = await this.db.insert(${kebab}s).values(dto).returning()
1158
+ // return row
1159
+ throw new Error('Drizzle ${pascal} repository not yet implemented')
1160
+ }
1161
+
1162
+ async update(id: string, dto: Update${pascal}DTO): Promise<${pascal}ResponseDTO> {
1163
+ // TODO: Implement with Drizzle
1164
+ // const [row] = await this.db.update(${kebab}s).set(dto).where(eq(${kebab}s.id, id)).returning()
1165
+ // if (!row) throw HttpException.notFound('${pascal} not found')
1166
+ // return row
1167
+ throw new Error('Drizzle ${pascal} repository not yet implemented')
1168
+ }
1169
+
1170
+ async delete(id: string): Promise<void> {
1171
+ // TODO: Implement with Drizzle
1172
+ // const result = await this.db.delete(${kebab}s).where(eq(${kebab}s.id, id))
1173
+ // if (!result.rowCount) throw HttpException.notFound('${pascal} not found')
1174
+ throw new Error('Drizzle ${pascal} repository not yet implemented')
1175
+ }
1176
+ }
1177
+ `;
1178
+ }
1179
+ __name(generateDrizzleRepository, "generateDrizzleRepository");
1180
+ function generatePrismaRepository(pascal, kebab, repoPrefix = "../../domain/repositories", dtoPrefix = "../../application/dtos") {
1181
+ const camel = kebab.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
1182
+ return `/**
1183
+ * Prisma ${pascal} Repository
1184
+ *
1185
+ * Implements the repository interface using Prisma Client.
1186
+ * Requires a PrismaClient instance injected via the DI container.
1187
+ *
1188
+ * TODO: Ensure your Prisma schema has a '${pascal}' model defined.
1189
+ * TODO: Replace 'PRISMA_CLIENT' with your actual Prisma injection token.
1190
+ *
1191
+ * @Repository() registers this class in the DI container as a singleton.
1192
+ */
1193
+ import { Repository, HttpException, Autowired } from '@forinda/kickjs-core'
1194
+ import type { ParsedQuery } from '@forinda/kickjs-http'
1195
+ import type { I${pascal}Repository } from '${repoPrefix}/${kebab}.repository'
1196
+ import type { ${pascal}ResponseDTO } from '${dtoPrefix}/${kebab}-response.dto'
1197
+ import type { Create${pascal}DTO } from '${dtoPrefix}/create-${kebab}.dto'
1198
+ import type { Update${pascal}DTO } from '${dtoPrefix}/update-${kebab}.dto'
1199
+
1200
+ // TODO: Import your Prisma injection token \u2014 e.g.:
1201
+ // import { PRISMA_CLIENT } from '@/db/prisma.provider'
1202
+ // import type { PrismaClient } from '@prisma/client'
1203
+
1204
+ @Repository()
1205
+ export class Prisma${pascal}Repository implements I${pascal}Repository {
1206
+ // TODO: Uncomment and configure your Prisma injection:
1207
+ // @Autowired(PRISMA_CLIENT) private prisma!: PrismaClient
1208
+
1209
+ async findById(id: string): Promise<${pascal}ResponseDTO | null> {
1210
+ // TODO: Implement with Prisma
1211
+ // return this.prisma.${camel}.findUnique({ where: { id } })
1212
+ throw new Error('Prisma ${pascal} repository not yet implemented \u2014 update Prisma imports and queries')
1213
+ }
1214
+
1215
+ async findAll(): Promise<${pascal}ResponseDTO[]> {
1216
+ // TODO: Implement with Prisma
1217
+ // return this.prisma.${camel}.findMany()
1218
+ throw new Error('Prisma ${pascal} repository not yet implemented')
1219
+ }
1220
+
1221
+ async findPaginated(parsed: ParsedQuery): Promise<{ data: ${pascal}ResponseDTO[]; total: number }> {
1222
+ // TODO: Implement with Prisma
1223
+ // const [data, total] = await Promise.all([
1224
+ // this.prisma.${camel}.findMany({
1225
+ // skip: parsed.pagination.offset,
1226
+ // take: parsed.pagination.limit,
1227
+ // }),
1228
+ // this.prisma.${camel}.count(),
1229
+ // ])
1230
+ // return { data, total }
1231
+ throw new Error('Prisma ${pascal} repository not yet implemented')
1232
+ }
1233
+
1234
+ async create(dto: Create${pascal}DTO): Promise<${pascal}ResponseDTO> {
1235
+ // TODO: Implement with Prisma
1236
+ // return this.prisma.${camel}.create({ data: dto })
1237
+ throw new Error('Prisma ${pascal} repository not yet implemented')
1238
+ }
1239
+
1240
+ async update(id: string, dto: Update${pascal}DTO): Promise<${pascal}ResponseDTO> {
1241
+ // TODO: Implement with Prisma
1242
+ // const row = await this.prisma.${camel}.update({ where: { id }, data: dto })
1243
+ // if (!row) throw HttpException.notFound('${pascal} not found')
1244
+ // return row
1245
+ throw new Error('Prisma ${pascal} repository not yet implemented')
1246
+ }
1247
+
1248
+ async delete(id: string): Promise<void> {
1249
+ // TODO: Implement with Prisma
1250
+ // await this.prisma.${camel}.delete({ where: { id } })
1251
+ throw new Error('Prisma ${pascal} repository not yet implemented')
1252
+ }
1253
+ }
1254
+ `;
1255
+ }
1256
+ __name(generatePrismaRepository, "generatePrismaRepository");
915
1257
 
916
1258
  // src/generators/templates/domain.ts
917
1259
  function generateDomainService(pascal, kebab) {
@@ -1110,9 +1452,9 @@ describe('${pascal}Controller', () => {
1110
1452
  `;
1111
1453
  }
1112
1454
  __name(generateControllerTest, "generateControllerTest");
1113
- function generateRepositoryTest(pascal, kebab, plural) {
1455
+ function generateRepositoryTest(pascal, kebab, plural, repoImport = `../infrastructure/repositories/in-memory-${kebab}.repository`) {
1114
1456
  return `import { describe, it, expect, beforeEach } from 'vitest'
1115
- import { InMemory${pascal}Repository } from '../infrastructure/repositories/in-memory-${kebab}.repository'
1457
+ import { InMemory${pascal}Repository } from '${repoImport}'
1116
1458
 
1117
1459
  describe('InMemory${pascal}Repository', () => {
1118
1460
  let repo: InMemory${pascal}Repository
@@ -1156,42 +1498,574 @@ describe('InMemory${pascal}Repository', () => {
1156
1498
  pagination: { page: 1, limit: 2, offset: 0 },
1157
1499
  })
1158
1500
 
1159
- expect(result.data).toHaveLength(2)
1160
- expect(result.total).toBe(3)
1161
- })
1162
-
1163
- it('should update a ${kebab}', async () => {
1164
- const created = await repo.create({ name: 'Original' })
1165
- const updated = await repo.update(created.id, { name: 'Updated' })
1166
- expect(updated.name).toBe('Updated')
1167
- })
1501
+ expect(result.data).toHaveLength(2)
1502
+ expect(result.total).toBe(3)
1503
+ })
1504
+
1505
+ it('should update a ${kebab}', async () => {
1506
+ const created = await repo.create({ name: 'Original' })
1507
+ const updated = await repo.update(created.id, { name: 'Updated' })
1508
+ expect(updated.name).toBe('Updated')
1509
+ })
1510
+
1511
+ it('should delete a ${kebab}', async () => {
1512
+ const created = await repo.create({ name: 'To Delete' })
1513
+ await repo.delete(created.id)
1514
+ const found = await repo.findById(created.id)
1515
+ expect(found).toBeNull()
1516
+ })
1517
+ })
1518
+ `;
1519
+ }
1520
+ __name(generateRepositoryTest, "generateRepositoryTest");
1521
+
1522
+ // src/generators/templates/rest-service.ts
1523
+ function generateRestService(pascal, kebab) {
1524
+ return `import { Service, Inject, HttpException } from '@forinda/kickjs-core'
1525
+ import type { ParsedQuery } from '@forinda/kickjs-http'
1526
+ import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from './${kebab}.repository'
1527
+ import type { ${pascal}ResponseDTO } from './dtos/${kebab}-response.dto'
1528
+ import type { Create${pascal}DTO } from './dtos/create-${kebab}.dto'
1529
+ import type { Update${pascal}DTO } from './dtos/update-${kebab}.dto'
1530
+
1531
+ @Service()
1532
+ export class ${pascal}Service {
1533
+ constructor(
1534
+ @Inject(${pascal.toUpperCase()}_REPOSITORY) private readonly repo: I${pascal}Repository,
1535
+ ) {}
1536
+
1537
+ async findById(id: string): Promise<${pascal}ResponseDTO | null> {
1538
+ return this.repo.findById(id)
1539
+ }
1540
+
1541
+ async findAll(): Promise<${pascal}ResponseDTO[]> {
1542
+ return this.repo.findAll()
1543
+ }
1544
+
1545
+ async findPaginated(parsed: ParsedQuery) {
1546
+ return this.repo.findPaginated(parsed)
1547
+ }
1548
+
1549
+ async create(dto: Create${pascal}DTO): Promise<${pascal}ResponseDTO> {
1550
+ return this.repo.create(dto)
1551
+ }
1552
+
1553
+ async update(id: string, dto: Update${pascal}DTO): Promise<${pascal}ResponseDTO> {
1554
+ return this.repo.update(id, dto)
1555
+ }
1556
+
1557
+ async delete(id: string): Promise<void> {
1558
+ await this.repo.delete(id)
1559
+ }
1560
+ }
1561
+ `;
1562
+ }
1563
+ __name(generateRestService, "generateRestService");
1564
+ function generateRestConstants(pascal) {
1565
+ return `import type { QueryFieldConfig } from '@forinda/kickjs-http'
1566
+
1567
+ export const ${pascal.toUpperCase()}_QUERY_CONFIG: QueryFieldConfig = {
1568
+ filterable: ['name'],
1569
+ sortable: ['name', 'createdAt'],
1570
+ searchable: ['name'],
1571
+ }
1572
+ `;
1573
+ }
1574
+ __name(generateRestConstants, "generateRestConstants");
1575
+
1576
+ // src/generators/templates/cqrs.ts
1577
+ function generateCqrsModuleIndex(pascal, kebab, plural, repo) {
1578
+ const repoClassMap = {
1579
+ inmemory: `InMemory${pascal}Repository`,
1580
+ drizzle: `Drizzle${pascal}Repository`,
1581
+ prisma: `Prisma${pascal}Repository`
1582
+ };
1583
+ const repoFileMap = {
1584
+ inmemory: `in-memory-${kebab}`,
1585
+ drizzle: `drizzle-${kebab}`,
1586
+ prisma: `prisma-${kebab}`
1587
+ };
1588
+ const repoClass = repoClassMap[repo] ?? repoClassMap.inmemory;
1589
+ const repoFile = repoFileMap[repo] ?? repoFileMap.inmemory;
1590
+ return `/**
1591
+ * ${pascal} Module \u2014 CQRS Pattern
1592
+ *
1593
+ * Separates read (queries) and write (commands) operations.
1594
+ * Events are emitted after state changes and can be handled via
1595
+ * WebSocket broadcasts, queue jobs, or ETL pipelines.
1596
+ *
1597
+ * Structure:
1598
+ * commands/ \u2014 Write operations (create, update, delete)
1599
+ * queries/ \u2014 Read operations (get, list)
1600
+ * events/ \u2014 Domain events + handlers (WS broadcast, queue dispatch)
1601
+ * dtos/ \u2014 Request/response schemas
1602
+ */
1603
+ import { Container, type AppModule, type ModuleRoutes } from '@forinda/kickjs-core'
1604
+ import { buildRoutes } from '@forinda/kickjs-http'
1605
+ import { ${pascal.toUpperCase()}_REPOSITORY } from './${kebab}.repository'
1606
+ import { ${repoClass} } from './${repoFile}.repository'
1607
+ import { ${pascal}Controller } from './${kebab}.controller'
1608
+
1609
+ // Eagerly load decorated classes
1610
+ import.meta.glob(
1611
+ [
1612
+ './commands/**/*.ts',
1613
+ './queries/**/*.ts',
1614
+ './events/**/*.ts',
1615
+ '!./**/*.test.ts',
1616
+ ],
1617
+ { eager: true },
1618
+ )
1619
+
1620
+ export class ${pascal}Module implements AppModule {
1621
+ register(container: Container): void {
1622
+ container.registerFactory(${pascal.toUpperCase()}_REPOSITORY, () =>
1623
+ container.resolve(${repoClass}),
1624
+ )
1625
+ }
1626
+
1627
+ routes(): ModuleRoutes {
1628
+ return {
1629
+ path: '/${plural}',
1630
+ router: buildRoutes(${pascal}Controller),
1631
+ controller: ${pascal}Controller,
1632
+ }
1633
+ }
1634
+ }
1635
+ `;
1636
+ }
1637
+ __name(generateCqrsModuleIndex, "generateCqrsModuleIndex");
1638
+ function generateCqrsController(pascal, kebab, plural, pluralPascal) {
1639
+ const camel = pascal.charAt(0).toLowerCase() + pascal.slice(1);
1640
+ return `import { Controller, Get, Post, Put, Delete, Autowired, ApiQueryParams } from '@forinda/kickjs-core'
1641
+ import type { RequestContext } from '@forinda/kickjs-http'
1642
+ import { ApiTags } from '@forinda/kickjs-swagger'
1643
+ import { Create${pascal}Command } from './commands/create-${kebab}.command'
1644
+ import { Update${pascal}Command } from './commands/update-${kebab}.command'
1645
+ import { Delete${pascal}Command } from './commands/delete-${kebab}.command'
1646
+ import { Get${pascal}Query } from './queries/get-${kebab}.query'
1647
+ import { List${pluralPascal}Query } from './queries/list-${plural}.query'
1648
+ import { create${pascal}Schema } from './dtos/create-${kebab}.dto'
1649
+ import { update${pascal}Schema } from './dtos/update-${kebab}.dto'
1650
+ import { ${pascal.toUpperCase()}_QUERY_CONFIG } from './${kebab}.constants'
1651
+
1652
+ @Controller()
1653
+ export class ${pascal}Controller {
1654
+ @Autowired() private create${pascal}Command!: Create${pascal}Command
1655
+ @Autowired() private update${pascal}Command!: Update${pascal}Command
1656
+ @Autowired() private delete${pascal}Command!: Delete${pascal}Command
1657
+ @Autowired() private get${pascal}Query!: Get${pascal}Query
1658
+ @Autowired() private list${pluralPascal}Query!: List${pluralPascal}Query
1659
+
1660
+ @Get('/')
1661
+ @ApiTags('${pascal}')
1662
+ @ApiQueryParams(${pascal.toUpperCase()}_QUERY_CONFIG)
1663
+ async list(ctx: RequestContext) {
1664
+ return ctx.paginate(
1665
+ (parsed) => this.list${pluralPascal}Query.execute(parsed),
1666
+ ${pascal.toUpperCase()}_QUERY_CONFIG,
1667
+ )
1668
+ }
1669
+
1670
+ @Get('/:id')
1671
+ @ApiTags('${pascal}')
1672
+ async getById(ctx: RequestContext) {
1673
+ const result = await this.get${pascal}Query.execute(ctx.params.id)
1674
+ if (!result) return ctx.notFound('${pascal} not found')
1675
+ ctx.json(result)
1676
+ }
1677
+
1678
+ @Post('/', { body: create${pascal}Schema, name: 'Create${pascal}' })
1679
+ @ApiTags('${pascal}')
1680
+ async create(ctx: RequestContext) {
1681
+ const result = await this.create${pascal}Command.execute(ctx.body)
1682
+ ctx.created(result)
1683
+ }
1684
+
1685
+ @Put('/:id', { body: update${pascal}Schema, name: 'Update${pascal}' })
1686
+ @ApiTags('${pascal}')
1687
+ async update(ctx: RequestContext) {
1688
+ const result = await this.update${pascal}Command.execute(ctx.params.id, ctx.body)
1689
+ ctx.json(result)
1690
+ }
1691
+
1692
+ @Delete('/:id')
1693
+ @ApiTags('${pascal}')
1694
+ async remove(ctx: RequestContext) {
1695
+ await this.delete${pascal}Command.execute(ctx.params.id)
1696
+ ctx.noContent()
1697
+ }
1698
+ }
1699
+ `;
1700
+ }
1701
+ __name(generateCqrsController, "generateCqrsController");
1702
+ function generateCqrsCommands(pascal, kebab) {
1703
+ return [
1704
+ {
1705
+ file: `create-${kebab}.command.ts`,
1706
+ content: `import { Service, Inject } from '@forinda/kickjs-core'
1707
+ import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../${kebab}.repository'
1708
+ import type { Create${pascal}DTO } from '../dtos/create-${kebab}.dto'
1709
+ import type { ${pascal}ResponseDTO } from '../dtos/${kebab}-response.dto'
1710
+ import { ${pascal}Events } from '../events/${kebab}.events'
1711
+
1712
+ @Service()
1713
+ export class Create${pascal}Command {
1714
+ constructor(
1715
+ @Inject(${pascal.toUpperCase()}_REPOSITORY) private readonly repo: I${pascal}Repository,
1716
+ @Inject(${pascal}Events) private readonly events: ${pascal}Events,
1717
+ ) {}
1718
+
1719
+ async execute(dto: Create${pascal}DTO): Promise<${pascal}ResponseDTO> {
1720
+ const result = await this.repo.create(dto)
1721
+ this.events.emit('${kebab}.created', result)
1722
+ return result
1723
+ }
1724
+ }
1725
+ `
1726
+ },
1727
+ {
1728
+ file: `update-${kebab}.command.ts`,
1729
+ content: `import { Service, Inject } from '@forinda/kickjs-core'
1730
+ import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../${kebab}.repository'
1731
+ import type { Update${pascal}DTO } from '../dtos/update-${kebab}.dto'
1732
+ import type { ${pascal}ResponseDTO } from '../dtos/${kebab}-response.dto'
1733
+ import { ${pascal}Events } from '../events/${kebab}.events'
1734
+
1735
+ @Service()
1736
+ export class Update${pascal}Command {
1737
+ constructor(
1738
+ @Inject(${pascal.toUpperCase()}_REPOSITORY) private readonly repo: I${pascal}Repository,
1739
+ @Inject(${pascal}Events) private readonly events: ${pascal}Events,
1740
+ ) {}
1741
+
1742
+ async execute(id: string, dto: Update${pascal}DTO): Promise<${pascal}ResponseDTO> {
1743
+ const result = await this.repo.update(id, dto)
1744
+ this.events.emit('${kebab}.updated', result)
1745
+ return result
1746
+ }
1747
+ }
1748
+ `
1749
+ },
1750
+ {
1751
+ file: `delete-${kebab}.command.ts`,
1752
+ content: `import { Service, Inject } from '@forinda/kickjs-core'
1753
+ import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../${kebab}.repository'
1754
+ import { ${pascal}Events } from '../events/${kebab}.events'
1755
+
1756
+ @Service()
1757
+ export class Delete${pascal}Command {
1758
+ constructor(
1759
+ @Inject(${pascal.toUpperCase()}_REPOSITORY) private readonly repo: I${pascal}Repository,
1760
+ @Inject(${pascal}Events) private readonly events: ${pascal}Events,
1761
+ ) {}
1762
+
1763
+ async execute(id: string): Promise<void> {
1764
+ await this.repo.delete(id)
1765
+ this.events.emit('${kebab}.deleted', { id })
1766
+ }
1767
+ }
1768
+ `
1769
+ }
1770
+ ];
1771
+ }
1772
+ __name(generateCqrsCommands, "generateCqrsCommands");
1773
+ function generateCqrsQueries(pascal, kebab, plural, pluralPascal) {
1774
+ return [
1775
+ {
1776
+ file: `get-${kebab}.query.ts`,
1777
+ content: `import { Service, Inject } from '@forinda/kickjs-core'
1778
+ import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../${kebab}.repository'
1779
+ import type { ${pascal}ResponseDTO } from '../dtos/${kebab}-response.dto'
1780
+
1781
+ @Service()
1782
+ export class Get${pascal}Query {
1783
+ constructor(
1784
+ @Inject(${pascal.toUpperCase()}_REPOSITORY) private readonly repo: I${pascal}Repository,
1785
+ ) {}
1786
+
1787
+ async execute(id: string): Promise<${pascal}ResponseDTO | null> {
1788
+ return this.repo.findById(id)
1789
+ }
1790
+ }
1791
+ `
1792
+ },
1793
+ {
1794
+ file: `list-${plural}.query.ts`,
1795
+ content: `import { Service, Inject } from '@forinda/kickjs-core'
1796
+ import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../${kebab}.repository'
1797
+ import type { ParsedQuery } from '@forinda/kickjs-http'
1798
+
1799
+ @Service()
1800
+ export class List${pluralPascal}Query {
1801
+ constructor(
1802
+ @Inject(${pascal.toUpperCase()}_REPOSITORY) private readonly repo: I${pascal}Repository,
1803
+ ) {}
1804
+
1805
+ async execute(parsed: ParsedQuery) {
1806
+ return this.repo.findPaginated(parsed)
1807
+ }
1808
+ }
1809
+ `
1810
+ }
1811
+ ];
1812
+ }
1813
+ __name(generateCqrsQueries, "generateCqrsQueries");
1814
+ function generateCqrsEvents(pascal, kebab) {
1815
+ return [
1816
+ {
1817
+ file: `${kebab}.events.ts`,
1818
+ content: `import { Service } from '@forinda/kickjs-core'
1819
+ import { EventEmitter } from 'node:events'
1820
+ import type { ${pascal}ResponseDTO } from '../dtos/${kebab}-response.dto'
1821
+
1822
+ /**
1823
+ * ${pascal} domain event types.
1824
+ *
1825
+ * These events are emitted by commands after state changes.
1826
+ * Subscribe to them in event handlers for side effects:
1827
+ * - WebSocket broadcasts (real-time UI updates)
1828
+ * - Queue jobs (async processing, ETL pipelines)
1829
+ * - Audit logging
1830
+ * - Cache invalidation
1831
+ */
1832
+ export interface ${pascal}EventMap {
1833
+ '${kebab}.created': ${pascal}ResponseDTO
1834
+ '${kebab}.updated': ${pascal}ResponseDTO
1835
+ '${kebab}.deleted': { id: string }
1836
+ }
1837
+
1838
+ @Service()
1839
+ export class ${pascal}Events {
1840
+ private emitter = new EventEmitter()
1841
+
1842
+ emit<K extends keyof ${pascal}EventMap>(event: K, data: ${pascal}EventMap[K]): void {
1843
+ this.emitter.emit(event, data)
1844
+ }
1845
+
1846
+ on<K extends keyof ${pascal}EventMap>(event: K, handler: (data: ${pascal}EventMap[K]) => void): void {
1847
+ this.emitter.on(event, handler)
1848
+ }
1849
+
1850
+ off<K extends keyof ${pascal}EventMap>(event: K, handler: (data: ${pascal}EventMap[K]) => void): void {
1851
+ this.emitter.off(event, handler)
1852
+ }
1853
+ }
1854
+ `
1855
+ },
1856
+ {
1857
+ file: `on-${kebab}-change.handler.ts`,
1858
+ content: `import { Service, Autowired } from '@forinda/kickjs-core'
1859
+ import { ${pascal}Events } from './${kebab}.events'
1860
+
1861
+ /**
1862
+ * ${pascal} Change Event Handler
1863
+ *
1864
+ * Reacts to domain events emitted by commands.
1865
+ * Wire up side effects here:
1866
+ *
1867
+ * 1. WebSocket broadcast \u2014 notify connected clients in real-time
1868
+ * import { WsGateway } from '@forinda/kickjs-ws'
1869
+ * this.ws.broadcast('${kebab}-channel', { event, data })
1870
+ *
1871
+ * 2. Queue dispatch \u2014 offload heavy processing to background workers
1872
+ * import { QueueService } from '@forinda/kickjs-queue'
1873
+ * this.queue.add('${kebab}-etl', { action: event, payload: data })
1874
+ *
1875
+ * 3. ETL pipeline \u2014 transform and load data to external systems
1876
+ * await this.etlPipeline.process(data)
1877
+ */
1878
+ @Service()
1879
+ export class On${pascal}ChangeHandler {
1880
+ @Autowired() private events!: ${pascal}Events
1881
+
1882
+ // Uncomment to inject WebSocket and Queue services:
1883
+ // @Autowired() private ws!: WsGateway
1884
+ // @Autowired() private queue!: QueueService
1885
+
1886
+ onInit(): void {
1887
+ this.events.on('${kebab}.created', (data) => {
1888
+ console.log('[${pascal}] Created:', data.id)
1889
+ // TODO: Broadcast via WebSocket
1890
+ // this.ws.broadcast('${kebab}-channel', { event: '${kebab}.created', data })
1891
+ // TODO: Dispatch to queue for async processing / ETL
1892
+ // this.queue.add('${kebab}-etl', { action: 'create', payload: data })
1893
+ })
1894
+
1895
+ this.events.on('${kebab}.updated', (data) => {
1896
+ console.log('[${pascal}] Updated:', data.id)
1897
+ // TODO: Broadcast via WebSocket
1898
+ // this.ws.broadcast('${kebab}-channel', { event: '${kebab}.updated', data })
1899
+ })
1168
1900
 
1169
- it('should delete a ${kebab}', async () => {
1170
- const created = await repo.create({ name: 'To Delete' })
1171
- await repo.delete(created.id)
1172
- const found = await repo.findById(created.id)
1173
- expect(found).toBeNull()
1174
- })
1175
- })
1176
- `;
1901
+ this.events.on('${kebab}.deleted', (data) => {
1902
+ console.log('[${pascal}] Deleted:', data.id)
1903
+ // TODO: Broadcast via WebSocket
1904
+ // this.ws.broadcast('${kebab}-channel', { event: '${kebab}.deleted', data })
1905
+ })
1906
+ }
1177
1907
  }
1178
- __name(generateRepositoryTest, "generateRepositoryTest");
1908
+ `
1909
+ }
1910
+ ];
1911
+ }
1912
+ __name(generateCqrsEvents, "generateCqrsEvents");
1179
1913
 
1180
1914
  // src/generators/module.ts
1915
+ function promptUser(question) {
1916
+ const rl = createInterface2({
1917
+ input: process.stdin,
1918
+ output: process.stdout
1919
+ });
1920
+ return new Promise((resolve8) => {
1921
+ rl.question(question, (answer) => {
1922
+ rl.close();
1923
+ resolve8(answer.trim().toLowerCase());
1924
+ });
1925
+ });
1926
+ }
1927
+ __name(promptUser, "promptUser");
1181
1928
  async function generateModule(options) {
1182
- const { name, modulesDir, noEntity, noTests, repo = "inmemory", minimal } = options;
1929
+ const { name, modulesDir, noEntity, noTests, repo = "inmemory", force } = options;
1930
+ let pattern = options.pattern ?? "ddd";
1931
+ if (options.minimal) pattern = "minimal";
1183
1932
  const kebab = toKebabCase(name);
1184
1933
  const pascal = toPascalCase(name);
1185
- const camel = toCamelCase(name);
1186
1934
  const plural = pluralize(kebab);
1187
1935
  const pluralPascal = pluralizePascal(pascal);
1188
1936
  const moduleDir = join2(modulesDir, plural);
1189
1937
  const files = [];
1938
+ let overwriteAll = force ?? false;
1190
1939
  const write = /* @__PURE__ */ __name(async (relativePath, content) => {
1191
1940
  const fullPath = join2(moduleDir, relativePath);
1941
+ if (!overwriteAll && await fileExists(fullPath)) {
1942
+ const answer = await promptUser(` File already exists: ${relativePath}
1943
+ Overwrite? (y/n/a = yes/no/all) `);
1944
+ if (answer === "a") {
1945
+ overwriteAll = true;
1946
+ } else if (answer !== "y") {
1947
+ console.log(` Skipped: ${relativePath}`);
1948
+ return;
1949
+ }
1950
+ }
1192
1951
  await writeFileSafe(fullPath, content);
1193
1952
  files.push(fullPath);
1194
1953
  }, "write");
1954
+ const ctx = {
1955
+ kebab,
1956
+ pascal,
1957
+ plural,
1958
+ pluralPascal,
1959
+ moduleDir,
1960
+ repo,
1961
+ noEntity: noEntity ?? false,
1962
+ noTests: noTests ?? false,
1963
+ write,
1964
+ files
1965
+ };
1966
+ switch (pattern) {
1967
+ case "minimal":
1968
+ await generateMinimalFiles(ctx);
1969
+ break;
1970
+ case "rest":
1971
+ await generateRestFiles(ctx);
1972
+ break;
1973
+ case "cqrs":
1974
+ await generateCqrsFiles(ctx);
1975
+ break;
1976
+ case "graphql":
1977
+ case "ddd":
1978
+ default:
1979
+ await generateDddFiles(ctx);
1980
+ break;
1981
+ }
1982
+ await autoRegisterModule(modulesDir, pascal, plural);
1983
+ return files;
1984
+ }
1985
+ __name(generateModule, "generateModule");
1986
+ async function generateMinimalFiles(ctx) {
1987
+ const { pascal, kebab, plural, write } = ctx;
1988
+ await write("index.ts", generateMinimalModuleIndex(pascal, kebab, plural));
1989
+ await write(`${kebab}.controller.ts`, `import { Controller, Get } from '@forinda/kickjs-core'
1990
+ import type { RequestContext } from '@forinda/kickjs-http'
1991
+
1992
+ @Controller()
1993
+ export class ${pascal}Controller {
1994
+ @Get('/')
1995
+ async list(ctx: RequestContext) {
1996
+ ctx.json({ message: '${pascal} list' })
1997
+ }
1998
+ }
1999
+ `);
2000
+ }
2001
+ __name(generateMinimalFiles, "generateMinimalFiles");
2002
+ async function generateRestFiles(ctx) {
2003
+ const { pascal, kebab, plural, pluralPascal, repo, noTests, write } = ctx;
2004
+ await write("index.ts", generateRestModuleIndex(pascal, kebab, plural, repo));
2005
+ await write(`${kebab}.constants.ts`, generateRestConstants(pascal));
2006
+ await write(`${kebab}.controller.ts`, generateRestController(pascal, kebab, plural, pluralPascal));
2007
+ await write(`${kebab}.service.ts`, generateRestService(pascal, kebab));
2008
+ await write(`dtos/create-${kebab}.dto.ts`, generateCreateDTO(pascal, kebab));
2009
+ await write(`dtos/update-${kebab}.dto.ts`, generateUpdateDTO(pascal, kebab));
2010
+ await write(`dtos/${kebab}-response.dto.ts`, generateResponseDTO(pascal, kebab));
2011
+ await write(`${kebab}.repository.ts`, generateRepositoryInterface(pascal, kebab, "./dtos"));
2012
+ const repoFileMap = {
2013
+ inmemory: `in-memory-${kebab}`,
2014
+ drizzle: `drizzle-${kebab}`,
2015
+ prisma: `prisma-${kebab}`
2016
+ };
2017
+ const repoGeneratorMap = {
2018
+ inmemory: /* @__PURE__ */ __name(() => generateInMemoryRepository(pascal, kebab, ".", "./dtos"), "inmemory"),
2019
+ drizzle: /* @__PURE__ */ __name(() => generateDrizzleRepository(pascal, kebab, ".", "./dtos"), "drizzle"),
2020
+ prisma: /* @__PURE__ */ __name(() => generatePrismaRepository(pascal, kebab, ".", "./dtos"), "prisma")
2021
+ };
2022
+ await write(`${repoFileMap[repo]}.repository.ts`, repoGeneratorMap[repo]());
2023
+ if (!noTests) {
2024
+ await write(`__tests__/${kebab}.controller.test.ts`, generateControllerTest(pascal, kebab, plural));
2025
+ await write(`__tests__/${kebab}.repository.test.ts`, generateRepositoryTest(pascal, kebab, plural, `../${repoFileMap.inmemory}.repository`));
2026
+ }
2027
+ }
2028
+ __name(generateRestFiles, "generateRestFiles");
2029
+ async function generateCqrsFiles(ctx) {
2030
+ const { pascal, kebab, plural, pluralPascal, repo, noTests, write } = ctx;
2031
+ await write("index.ts", generateCqrsModuleIndex(pascal, kebab, plural, repo));
2032
+ await write(`${kebab}.constants.ts`, generateRestConstants(pascal));
2033
+ await write(`${kebab}.controller.ts`, generateCqrsController(pascal, kebab, plural, pluralPascal));
2034
+ await write(`dtos/create-${kebab}.dto.ts`, generateCreateDTO(pascal, kebab));
2035
+ await write(`dtos/update-${kebab}.dto.ts`, generateUpdateDTO(pascal, kebab));
2036
+ await write(`dtos/${kebab}-response.dto.ts`, generateResponseDTO(pascal, kebab));
2037
+ const commands = generateCqrsCommands(pascal, kebab);
2038
+ for (const cmd of commands) {
2039
+ await write(`commands/${cmd.file}`, cmd.content);
2040
+ }
2041
+ const queries = generateCqrsQueries(pascal, kebab, plural, pluralPascal);
2042
+ for (const q of queries) {
2043
+ await write(`queries/${q.file}`, q.content);
2044
+ }
2045
+ const events = generateCqrsEvents(pascal, kebab);
2046
+ for (const e of events) {
2047
+ await write(`events/${e.file}`, e.content);
2048
+ }
2049
+ await write(`${kebab}.repository.ts`, generateRepositoryInterface(pascal, kebab, "./dtos"));
2050
+ const repoFileMap = {
2051
+ inmemory: `in-memory-${kebab}`,
2052
+ drizzle: `drizzle-${kebab}`,
2053
+ prisma: `prisma-${kebab}`
2054
+ };
2055
+ const repoGeneratorMap = {
2056
+ inmemory: /* @__PURE__ */ __name(() => generateInMemoryRepository(pascal, kebab, ".", "./dtos"), "inmemory"),
2057
+ drizzle: /* @__PURE__ */ __name(() => generateDrizzleRepository(pascal, kebab, ".", "./dtos"), "drizzle"),
2058
+ prisma: /* @__PURE__ */ __name(() => generatePrismaRepository(pascal, kebab, ".", "./dtos"), "prisma")
2059
+ };
2060
+ await write(`${repoFileMap[repo]}.repository.ts`, repoGeneratorMap[repo]());
2061
+ if (!noTests) {
2062
+ await write(`__tests__/${kebab}.controller.test.ts`, generateControllerTest(pascal, kebab, plural));
2063
+ await write(`__tests__/${kebab}.repository.test.ts`, generateRepositoryTest(pascal, kebab, plural, `../${repoFileMap.inmemory}.repository`));
2064
+ }
2065
+ }
2066
+ __name(generateCqrsFiles, "generateCqrsFiles");
2067
+ async function generateDddFiles(ctx) {
2068
+ const { pascal, kebab, plural, pluralPascal, repo, noEntity, noTests, write } = ctx;
1195
2069
  await write("index.ts", generateModuleIndex(pascal, kebab, plural, repo));
1196
2070
  await write("constants.ts", generateConstants(pascal));
1197
2071
  await write(`presentation/${kebab}.controller.ts`, generateController(pascal, kebab, plural, pluralPascal));
@@ -1204,10 +2078,18 @@ async function generateModule(options) {
1204
2078
  }
1205
2079
  await write(`domain/repositories/${kebab}.repository.ts`, generateRepositoryInterface(pascal, kebab));
1206
2080
  await write(`domain/services/${kebab}-domain.service.ts`, generateDomainService(pascal, kebab));
1207
- if (repo === "inmemory") {
1208
- await write(`infrastructure/repositories/in-memory-${kebab}.repository.ts`, generateInMemoryRepository(pascal, kebab));
1209
- }
1210
- if (!noEntity && !minimal) {
2081
+ const repoFileMap = {
2082
+ inmemory: `in-memory-${kebab}`,
2083
+ drizzle: `drizzle-${kebab}`,
2084
+ prisma: `prisma-${kebab}`
2085
+ };
2086
+ const repoGeneratorMap = {
2087
+ inmemory: /* @__PURE__ */ __name(() => generateInMemoryRepository(pascal, kebab), "inmemory"),
2088
+ drizzle: /* @__PURE__ */ __name(() => generateDrizzleRepository(pascal, kebab), "drizzle"),
2089
+ prisma: /* @__PURE__ */ __name(() => generatePrismaRepository(pascal, kebab), "prisma")
2090
+ };
2091
+ await write(`infrastructure/repositories/${repoFileMap[repo]}.repository.ts`, repoGeneratorMap[repo]());
2092
+ if (!noEntity) {
1211
2093
  await write(`domain/entities/${kebab}.entity.ts`, generateEntity(pascal, kebab));
1212
2094
  await write(`domain/value-objects/${kebab}-id.vo.ts`, generateValueObject(pascal, kebab));
1213
2095
  }
@@ -1215,10 +2097,8 @@ async function generateModule(options) {
1215
2097
  await write(`__tests__/${kebab}.controller.test.ts`, generateControllerTest(pascal, kebab, plural));
1216
2098
  await write(`__tests__/${kebab}.repository.test.ts`, generateRepositoryTest(pascal, kebab, plural));
1217
2099
  }
1218
- await autoRegisterModule(modulesDir, pascal, plural);
1219
- return files;
1220
2100
  }
1221
- __name(generateModule, "generateModule");
2101
+ __name(generateDddFiles, "generateDddFiles");
1222
2102
  async function autoRegisterModule(modulesDir, pascal, plural) {
1223
2103
  const indexPath = join2(modulesDir, "index.ts");
1224
2104
  const exists = await fileExists(indexPath);
@@ -1354,13 +2234,64 @@ export class ${pascal}Adapter implements AppAdapter {
1354
2234
  __name(generateAdapter, "generateAdapter");
1355
2235
 
1356
2236
  // src/generators/middleware.ts
1357
- import { join as join4 } from "path";
2237
+ import { join as join5 } from "path";
2238
+
2239
+ // src/utils/resolve-out-dir.ts
2240
+ import { resolve as resolve2, join as join4 } from "path";
2241
+ var DDD_FOLDER_MAP = {
2242
+ controller: "presentation",
2243
+ service: "domain/services",
2244
+ dto: "application/dtos",
2245
+ guard: "presentation/guards",
2246
+ middleware: "middleware"
2247
+ };
2248
+ var FLAT_FOLDER_MAP = {
2249
+ controller: "",
2250
+ service: "",
2251
+ dto: "dtos",
2252
+ guard: "guards",
2253
+ middleware: "middleware"
2254
+ };
2255
+ var CQRS_FOLDER_MAP = {
2256
+ controller: "",
2257
+ service: "",
2258
+ dto: "dtos",
2259
+ guard: "guards",
2260
+ middleware: "middleware",
2261
+ command: "commands",
2262
+ query: "queries",
2263
+ event: "events"
2264
+ };
2265
+ function resolveOutDir(options) {
2266
+ const { type, outDir, moduleName, modulesDir = "src/modules", defaultDir, pattern = "ddd" } = options;
2267
+ if (outDir) return resolve2(outDir);
2268
+ if (moduleName) {
2269
+ const folderMap = pattern === "ddd" ? DDD_FOLDER_MAP : pattern === "cqrs" ? CQRS_FOLDER_MAP : FLAT_FOLDER_MAP;
2270
+ const kebab = toKebabCase(moduleName);
2271
+ const plural = pluralize(kebab);
2272
+ const subfolder = folderMap[type] ?? "";
2273
+ const base = join4(modulesDir, plural);
2274
+ return resolve2(subfolder ? join4(base, subfolder) : base);
2275
+ }
2276
+ return resolve2(defaultDir);
2277
+ }
2278
+ __name(resolveOutDir, "resolveOutDir");
2279
+
2280
+ // src/generators/middleware.ts
1358
2281
  async function generateMiddleware(options) {
1359
- const { name, outDir } = options;
2282
+ const { name, moduleName, modulesDir, pattern } = options;
2283
+ const outDir = resolveOutDir({
2284
+ type: "middleware",
2285
+ outDir: options.outDir,
2286
+ moduleName,
2287
+ modulesDir,
2288
+ defaultDir: "src/middleware",
2289
+ pattern
2290
+ });
1360
2291
  const kebab = toKebabCase(name);
1361
2292
  const camel = toCamelCase(name);
1362
2293
  const files = [];
1363
- const filePath = join4(outDir, `${kebab}.middleware.ts`);
2294
+ const filePath = join5(outDir, `${kebab}.middleware.ts`);
1364
2295
  await writeFileSafe(filePath, `import type { Request, Response, NextFunction } from 'express'
1365
2296
 
1366
2297
  export interface ${toPascalCase(name)}Options {
@@ -1392,14 +2323,22 @@ export function ${camel}(options: ${toPascalCase(name)}Options = {}) {
1392
2323
  __name(generateMiddleware, "generateMiddleware");
1393
2324
 
1394
2325
  // src/generators/guard.ts
1395
- import { join as join5 } from "path";
2326
+ import { join as join6 } from "path";
1396
2327
  async function generateGuard(options) {
1397
- const { name, outDir } = options;
2328
+ const { name, moduleName, modulesDir, pattern } = options;
2329
+ const outDir = resolveOutDir({
2330
+ type: "guard",
2331
+ outDir: options.outDir,
2332
+ moduleName,
2333
+ modulesDir,
2334
+ defaultDir: "src/guards",
2335
+ pattern
2336
+ });
1398
2337
  const kebab = toKebabCase(name);
1399
2338
  const camel = toCamelCase(name);
1400
2339
  const pascal = toPascalCase(name);
1401
2340
  const files = [];
1402
- const filePath = join5(outDir, `${kebab}.guard.ts`);
2341
+ const filePath = join6(outDir, `${kebab}.guard.ts`);
1403
2342
  await writeFileSafe(filePath, `import { Container, HttpException } from '@forinda/kickjs-core'
1404
2343
  import type { RequestContext } from '@forinda/kickjs-http'
1405
2344
 
@@ -1443,13 +2382,21 @@ export async function ${camel}Guard(ctx: RequestContext, next: () => void): Prom
1443
2382
  __name(generateGuard, "generateGuard");
1444
2383
 
1445
2384
  // src/generators/service.ts
1446
- import { join as join6 } from "path";
2385
+ import { join as join7 } from "path";
1447
2386
  async function generateService(options) {
1448
- const { name, outDir } = options;
2387
+ const { name, moduleName, modulesDir, pattern } = options;
2388
+ const outDir = resolveOutDir({
2389
+ type: "service",
2390
+ outDir: options.outDir,
2391
+ moduleName,
2392
+ modulesDir,
2393
+ defaultDir: "src/services",
2394
+ pattern
2395
+ });
1449
2396
  const kebab = toKebabCase(name);
1450
2397
  const pascal = toPascalCase(name);
1451
2398
  const files = [];
1452
- const filePath = join6(outDir, `${kebab}.service.ts`);
2399
+ const filePath = join7(outDir, `${kebab}.service.ts`);
1453
2400
  await writeFileSafe(filePath, `import { Service } from '@forinda/kickjs-core'
1454
2401
 
1455
2402
  @Service()
@@ -1466,13 +2413,21 @@ export class ${pascal}Service {
1466
2413
  __name(generateService, "generateService");
1467
2414
 
1468
2415
  // src/generators/controller.ts
1469
- import { join as join7 } from "path";
2416
+ import { join as join8 } from "path";
1470
2417
  async function generateController2(options) {
1471
- const { name, outDir } = options;
2418
+ const { name, moduleName, modulesDir, pattern } = options;
2419
+ const outDir = resolveOutDir({
2420
+ type: "controller",
2421
+ outDir: options.outDir,
2422
+ moduleName,
2423
+ modulesDir,
2424
+ defaultDir: "src/controllers",
2425
+ pattern
2426
+ });
1472
2427
  const kebab = toKebabCase(name);
1473
2428
  const pascal = toPascalCase(name);
1474
2429
  const files = [];
1475
- const filePath = join7(outDir, `${kebab}.controller.ts`);
2430
+ const filePath = join8(outDir, `${kebab}.controller.ts`);
1476
2431
  await writeFileSafe(filePath, `import { Controller, Get, Post, Autowired } from '@forinda/kickjs-core'
1477
2432
  import type { RequestContext } from '@forinda/kickjs-http'
1478
2433
 
@@ -1497,14 +2452,22 @@ export class ${pascal}Controller {
1497
2452
  __name(generateController2, "generateController");
1498
2453
 
1499
2454
  // src/generators/dto.ts
1500
- import { join as join8 } from "path";
2455
+ import { join as join9 } from "path";
1501
2456
  async function generateDto(options) {
1502
- const { name, outDir } = options;
2457
+ const { name, moduleName, modulesDir, pattern } = options;
2458
+ const outDir = resolveOutDir({
2459
+ type: "dto",
2460
+ outDir: options.outDir,
2461
+ moduleName,
2462
+ modulesDir,
2463
+ defaultDir: "src/dtos",
2464
+ pattern
2465
+ });
1503
2466
  const kebab = toKebabCase(name);
1504
2467
  const pascal = toPascalCase(name);
1505
2468
  const camel = toCamelCase(name);
1506
2469
  const files = [];
1507
- const filePath = join8(outDir, `${kebab}.dto.ts`);
2470
+ const filePath = join9(outDir, `${kebab}.dto.ts`);
1508
2471
  await writeFileSafe(filePath, `import { z } from 'zod'
1509
2472
 
1510
2473
  export const ${camel}Schema = z.object({
@@ -1520,24 +2483,24 @@ export type ${pascal}DTO = z.infer<typeof ${camel}Schema>
1520
2483
  __name(generateDto, "generateDto");
1521
2484
 
1522
2485
  // src/generators/config.ts
1523
- import { join as join9 } from "path";
2486
+ import { join as join10 } from "path";
1524
2487
  import { existsSync as existsSync2 } from "fs";
1525
- import { createInterface as createInterface2 } from "readline";
2488
+ import { createInterface as createInterface3 } from "readline";
1526
2489
  async function confirm2(message) {
1527
- const rl = createInterface2({
2490
+ const rl = createInterface3({
1528
2491
  input: process.stdin,
1529
2492
  output: process.stdout
1530
2493
  });
1531
- return new Promise((resolve6) => {
2494
+ return new Promise((resolve8) => {
1532
2495
  rl.question(` ${message} (y/N) `, (answer) => {
1533
2496
  rl.close();
1534
- resolve6(answer.trim().toLowerCase() === "y");
2497
+ resolve8(answer.trim().toLowerCase() === "y");
1535
2498
  });
1536
2499
  });
1537
2500
  }
1538
2501
  __name(confirm2, "confirm");
1539
2502
  async function generateConfig(options) {
1540
- const filePath = join9(options.outDir, "kick.config.ts");
2503
+ const filePath = join10(options.outDir, "kick.config.ts");
1541
2504
  const modulesDir = options.modulesDir ?? "src/modules";
1542
2505
  const defaultRepo = options.defaultRepo ?? "inmemory";
1543
2506
  if (existsSync2(filePath) && !options.force) {
@@ -1585,7 +2548,7 @@ export default defineConfig({
1585
2548
  __name(generateConfig, "generateConfig");
1586
2549
 
1587
2550
  // src/generators/resolver.ts
1588
- import { join as join10 } from "path";
2551
+ import { join as join11 } from "path";
1589
2552
  async function generateResolver(options) {
1590
2553
  const { name, outDir } = options;
1591
2554
  const pascal = toPascalCase(name);
@@ -1593,7 +2556,7 @@ async function generateResolver(options) {
1593
2556
  const camel = toCamelCase(name);
1594
2557
  const files = [];
1595
2558
  const write = /* @__PURE__ */ __name(async (relativePath, content) => {
1596
- const fullPath = join10(outDir, relativePath);
2559
+ const fullPath = join11(outDir, relativePath);
1597
2560
  await writeFileSafe(fullPath, content);
1598
2561
  files.push(fullPath);
1599
2562
  }, "write");
@@ -1663,7 +2626,7 @@ export const ${camel}TypeDefs = \`
1663
2626
  __name(generateResolver, "generateResolver");
1664
2627
 
1665
2628
  // src/generators/job.ts
1666
- import { join as join11 } from "path";
2629
+ import { join as join12 } from "path";
1667
2630
  async function generateJob(options) {
1668
2631
  const { name, outDir } = options;
1669
2632
  const pascal = toPascalCase(name);
@@ -1672,7 +2635,7 @@ async function generateJob(options) {
1672
2635
  const queueName = options.queue ?? `${kebab}-queue`;
1673
2636
  const files = [];
1674
2637
  const write = /* @__PURE__ */ __name(async (relativePath, content) => {
1675
- const fullPath = join11(outDir, relativePath);
2638
+ const fullPath = join12(outDir, relativePath);
1676
2639
  await writeFileSafe(fullPath, content);
1677
2640
  files.push(fullPath);
1678
2641
  }, "write");
@@ -1715,7 +2678,7 @@ export class ${pascal}Job {
1715
2678
  __name(generateJob, "generateJob");
1716
2679
 
1717
2680
  // src/generators/scaffold.ts
1718
- import { join as join12 } from "path";
2681
+ import { join as join13 } from "path";
1719
2682
  import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
1720
2683
  var TYPE_MAP = {
1721
2684
  string: {
@@ -1811,10 +2774,10 @@ async function generateScaffold(options) {
1811
2774
  const camel = toCamelCase(name);
1812
2775
  const plural = pluralize(kebab);
1813
2776
  const pluralPascal = pluralizePascal(pascal);
1814
- const moduleDir = join12(modulesDir, plural);
2777
+ const moduleDir = join13(modulesDir, plural);
1815
2778
  const files = [];
1816
2779
  const write = /* @__PURE__ */ __name(async (relativePath, content) => {
1817
- const fullPath = join12(moduleDir, relativePath);
2780
+ const fullPath = join13(moduleDir, relativePath);
1818
2781
  await writeFileSafe(fullPath, content);
1819
2782
  files.push(fullPath);
1820
2783
  }, "write");
@@ -2220,7 +3183,7 @@ export class Delete${pascal}UseCase {
2220
3183
  }
2221
3184
  __name(genUseCases, "genUseCases");
2222
3185
  async function autoRegisterModule2(modulesDir, pascal, plural) {
2223
- const indexPath = join12(modulesDir, "index.ts");
3186
+ const indexPath = join13(modulesDir, "index.ts");
2224
3187
  const exists = await fileExists(indexPath);
2225
3188
  if (!exists) {
2226
3189
  await writeFileSafe(indexPath, `import type { AppModuleClass } from '@forinda/kickjs-core'
@@ -2251,6 +3214,90 @@ export const modules: AppModuleClass[] = [${pascal}Module]
2251
3214
  }
2252
3215
  __name(autoRegisterModule2, "autoRegisterModule");
2253
3216
 
3217
+ // src/generators/test.ts
3218
+ import { join as join14, resolve as resolve3 } from "path";
3219
+ async function generateTest(options) {
3220
+ const { name, moduleName, modulesDir } = options;
3221
+ const kebab = toKebabCase(name);
3222
+ const pascal = toPascalCase(name);
3223
+ const files = [];
3224
+ let outDir;
3225
+ if (options.outDir) {
3226
+ outDir = resolve3(options.outDir);
3227
+ } else if (moduleName) {
3228
+ const modKebab = toKebabCase(moduleName);
3229
+ const modPlural = pluralize(modKebab);
3230
+ const modDir = modulesDir ?? "src/modules";
3231
+ outDir = resolve3(join14(modDir, modPlural, "__tests__"));
3232
+ } else {
3233
+ outDir = resolve3("src/__tests__");
3234
+ }
3235
+ const filePath = join14(outDir, `${kebab}.test.ts`);
3236
+ await writeFileSafe(filePath, `import { describe, it, expect, beforeEach } from 'vitest'
3237
+ import { Container } from '@forinda/kickjs-core'
3238
+
3239
+ describe('${pascal}', () => {
3240
+ beforeEach(() => {
3241
+ Container.reset()
3242
+ })
3243
+
3244
+ it('should be defined', () => {
3245
+ // TODO: Import and test your class/function here
3246
+ expect(true).toBe(true)
3247
+ })
3248
+
3249
+ it('should handle the happy path', async () => {
3250
+ // TODO: Set up test data and assertions
3251
+ expect(true).toBe(true)
3252
+ })
3253
+
3254
+ it('should handle edge cases', async () => {
3255
+ // TODO: Test error handling, empty inputs, etc.
3256
+ expect(true).toBe(true)
3257
+ })
3258
+ })
3259
+ `);
3260
+ files.push(filePath);
3261
+ return files;
3262
+ }
3263
+ __name(generateTest, "generateTest");
3264
+
3265
+ // src/config.ts
3266
+ import { readFile as readFile4, access as access2 } from "fs/promises";
3267
+ import { join as join15 } from "path";
3268
+ var CONFIG_FILES = [
3269
+ "kick.config.ts",
3270
+ "kick.config.js",
3271
+ "kick.config.mjs",
3272
+ "kick.config.json"
3273
+ ];
3274
+ async function loadKickConfig(cwd) {
3275
+ for (const filename of CONFIG_FILES) {
3276
+ const filepath = join15(cwd, filename);
3277
+ try {
3278
+ await access2(filepath);
3279
+ } catch {
3280
+ continue;
3281
+ }
3282
+ if (filename.endsWith(".json")) {
3283
+ const content = await readFile4(filepath, "utf-8");
3284
+ return JSON.parse(content);
3285
+ }
3286
+ try {
3287
+ const { pathToFileURL: pathToFileURL2 } = await import("url");
3288
+ const mod = await import(pathToFileURL2(filepath).href);
3289
+ return mod.default ?? mod;
3290
+ } catch (err) {
3291
+ if (filename.endsWith(".ts")) {
3292
+ 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.`);
3293
+ }
3294
+ continue;
3295
+ }
3296
+ }
3297
+ return null;
3298
+ }
3299
+ __name(loadKickConfig, "loadKickConfig");
3300
+
2254
3301
  // src/commands/generate.ts
2255
3302
  function printGenerated(files) {
2256
3303
  const cwd = process.cwd();
@@ -2262,86 +3309,195 @@ function printGenerated(files) {
2262
3309
  console.log();
2263
3310
  }
2264
3311
  __name(printGenerated, "printGenerated");
3312
+ var GENERATORS = [
3313
+ {
3314
+ name: "module <name>",
3315
+ description: "Full DDD module (controller, DTOs, use-cases, repo)"
3316
+ },
3317
+ {
3318
+ name: "scaffold <name> <fields...>",
3319
+ description: "CRUD module from field definitions"
3320
+ },
3321
+ {
3322
+ name: "controller <name>",
3323
+ description: "@Controller() class [-m module]"
3324
+ },
3325
+ {
3326
+ name: "service <name>",
3327
+ description: "@Service() singleton [-m module]"
3328
+ },
3329
+ {
3330
+ name: "middleware <name>",
3331
+ description: "Express middleware function [-m module]"
3332
+ },
3333
+ {
3334
+ name: "guard <name>",
3335
+ description: "Route guard (auth, roles, etc.) [-m module]"
3336
+ },
3337
+ {
3338
+ name: "dto <name>",
3339
+ description: "Zod DTO schema [-m module]"
3340
+ },
3341
+ {
3342
+ name: "adapter <name>",
3343
+ description: "AppAdapter with lifecycle hooks (app-level only)"
3344
+ },
3345
+ {
3346
+ name: "test <name>",
3347
+ description: "Vitest test scaffold [-m module]"
3348
+ },
3349
+ {
3350
+ name: "resolver <name>",
3351
+ description: "GraphQL @Resolver class"
3352
+ },
3353
+ {
3354
+ name: "job <name>",
3355
+ description: "Queue @Job processor"
3356
+ },
3357
+ {
3358
+ name: "config",
3359
+ description: "Generate kick.config.ts"
3360
+ }
3361
+ ];
3362
+ function printGeneratorList() {
3363
+ console.log("\n Available generators:\n");
3364
+ const maxName = Math.max(...GENERATORS.map((g) => g.name.length));
3365
+ for (const g of GENERATORS) {
3366
+ console.log(` kick g ${g.name.padEnd(maxName + 2)} ${g.description}`);
3367
+ }
3368
+ console.log();
3369
+ }
3370
+ __name(printGeneratorList, "printGeneratorList");
2265
3371
  function registerGenerateCommand(program) {
2266
- const gen = program.command("generate").alias("g").description("Generate code scaffolds");
2267
- gen.command("module <name>").description("Generate a full DDD module with all layers").option("--no-entity", "Skip entity and value object generation").option("--no-tests", "Skip test file generation").option("--repo <type>", "Repository implementation: inmemory | drizzle", "inmemory").option("--minimal", "Only generate index.ts and controller").option("--modules-dir <dir>", "Modules directory", "src/modules").action(async (name, opts) => {
3372
+ const gen = program.command("generate").alias("g").description("Generate code scaffolds").option("--list", "List all available generators").action((opts) => {
3373
+ if (opts.list) {
3374
+ printGeneratorList();
3375
+ } else {
3376
+ gen.help();
3377
+ }
3378
+ });
3379
+ gen.command("module <name>").description("Generate a module (structure depends on project pattern)").option("--no-entity", "Skip entity and value object generation").option("--no-tests", "Skip test file generation").option("--repo <type>", "Repository implementation: inmemory | drizzle | prisma").option("--pattern <pattern>", "Override project pattern: rest | ddd | cqrs | minimal").option("--minimal", "Shorthand for --pattern minimal").option("--modules-dir <dir>", "Modules directory").option("-f, --force", "Overwrite existing files without prompting").action(async (name, opts) => {
3380
+ const config = await loadKickConfig(process.cwd());
3381
+ const modulesDir = opts.modulesDir ?? config?.modulesDir ?? "src/modules";
3382
+ const repo = opts.repo ?? config?.defaultRepo ?? "inmemory";
3383
+ const pattern = opts.pattern ?? config?.pattern ?? "ddd";
2268
3384
  const files = await generateModule({
2269
3385
  name,
2270
- modulesDir: resolve2(opts.modulesDir),
3386
+ modulesDir: resolve4(modulesDir),
2271
3387
  noEntity: opts.entity === false,
2272
3388
  noTests: opts.tests === false,
2273
- repo: opts.repo,
2274
- minimal: opts.minimal
3389
+ repo,
3390
+ minimal: opts.minimal,
3391
+ force: opts.force,
3392
+ pattern
2275
3393
  });
2276
3394
  printGenerated(files);
2277
3395
  });
2278
3396
  gen.command("adapter <name>").description("Generate an AppAdapter with lifecycle hooks and middleware support").option("-o, --out <dir>", "Output directory", "src/adapters").action(async (name, opts) => {
2279
3397
  const files = await generateAdapter({
2280
3398
  name,
2281
- outDir: resolve2(opts.out)
3399
+ outDir: resolve4(opts.out)
2282
3400
  });
2283
3401
  printGenerated(files);
2284
3402
  });
2285
- gen.command("middleware <name>").description("Generate an Express middleware function").option("-o, --out <dir>", "Output directory", "src/middleware").action(async (name, opts) => {
3403
+ gen.command("middleware <name>").description("Generate an Express middleware function\n Use -m to scope it to a module: kick g middleware auth -m users").option("-o, --out <dir>", "Output directory (overrides --module)").option("-m, --module <module>", "Place inside a module folder").action(async (name, opts) => {
3404
+ const config = await loadKickConfig(process.cwd());
3405
+ const modulesDir = config?.modulesDir ?? "src/modules";
2286
3406
  const files = await generateMiddleware({
2287
3407
  name,
2288
- outDir: resolve2(opts.out)
3408
+ outDir: opts.out,
3409
+ moduleName: opts.module,
3410
+ modulesDir,
3411
+ pattern: config?.pattern
2289
3412
  });
2290
3413
  printGenerated(files);
2291
3414
  });
2292
- gen.command("guard <name>").description("Generate a route guard (auth, roles, etc.)").option("-o, --out <dir>", "Output directory", "src/guards").action(async (name, opts) => {
3415
+ gen.command("guard <name>").description("Generate a route guard (auth, roles, etc.)\n Use -m to scope it to a module: kick g guard admin -m users").option("-o, --out <dir>", "Output directory (overrides --module)").option("-m, --module <module>", "Place inside a module folder").action(async (name, opts) => {
3416
+ const config = await loadKickConfig(process.cwd());
3417
+ const modulesDir = config?.modulesDir ?? "src/modules";
2293
3418
  const files = await generateGuard({
2294
3419
  name,
2295
- outDir: resolve2(opts.out)
3420
+ outDir: opts.out,
3421
+ moduleName: opts.module,
3422
+ modulesDir,
3423
+ pattern: config?.pattern
2296
3424
  });
2297
3425
  printGenerated(files);
2298
3426
  });
2299
- gen.command("service <name>").description("Generate a @Service() class").option("-o, --out <dir>", "Output directory", "src/services").action(async (name, opts) => {
3427
+ gen.command("service <name>").description("Generate a @Service() class\n Use -m to scope it to a module: kick g service payment -m orders").option("-o, --out <dir>", "Output directory (overrides --module)").option("-m, --module <module>", "Place inside a module folder").action(async (name, opts) => {
3428
+ const config = await loadKickConfig(process.cwd());
3429
+ const modulesDir = config?.modulesDir ?? "src/modules";
2300
3430
  const files = await generateService({
2301
3431
  name,
2302
- outDir: resolve2(opts.out)
3432
+ outDir: opts.out,
3433
+ moduleName: opts.module,
3434
+ modulesDir,
3435
+ pattern: config?.pattern
2303
3436
  });
2304
3437
  printGenerated(files);
2305
3438
  });
2306
- gen.command("controller <name>").description("Generate a @Controller() class with basic routes").option("-o, --out <dir>", "Output directory", "src/controllers").action(async (name, opts) => {
3439
+ gen.command("controller <name>").description("Generate a @Controller() class with basic routes\n Use -m to scope it to a module: kick g controller auth -m users").option("-o, --out <dir>", "Output directory (overrides --module)").option("-m, --module <module>", "Place inside a module folder").action(async (name, opts) => {
3440
+ const config = await loadKickConfig(process.cwd());
3441
+ const modulesDir = config?.modulesDir ?? "src/modules";
2307
3442
  const files = await generateController2({
2308
3443
  name,
2309
- outDir: resolve2(opts.out)
3444
+ outDir: opts.out,
3445
+ moduleName: opts.module,
3446
+ modulesDir,
3447
+ pattern: config?.pattern
2310
3448
  });
2311
3449
  printGenerated(files);
2312
3450
  });
2313
- gen.command("dto <name>").description("Generate a Zod DTO schema").option("-o, --out <dir>", "Output directory", "src/dtos").action(async (name, opts) => {
3451
+ gen.command("dto <name>").description("Generate a Zod DTO schema\n Use -m to scope it to a module: kick g dto create-user -m users").option("-o, --out <dir>", "Output directory (overrides --module)").option("-m, --module <module>", "Place inside a module folder").action(async (name, opts) => {
3452
+ const config = await loadKickConfig(process.cwd());
3453
+ const modulesDir = config?.modulesDir ?? "src/modules";
2314
3454
  const files = await generateDto({
2315
3455
  name,
2316
- outDir: resolve2(opts.out)
3456
+ outDir: opts.out,
3457
+ moduleName: opts.module,
3458
+ modulesDir,
3459
+ pattern: config?.pattern
3460
+ });
3461
+ printGenerated(files);
3462
+ });
3463
+ gen.command("test <name>").description("Generate a Vitest test scaffold\n Use -m to scope it to a module: kick g test user-service -m users").option("-o, --out <dir>", "Output directory (overrides --module)").option("-m, --module <module>", "Place inside a module's __tests__/ folder").action(async (name, opts) => {
3464
+ const config = await loadKickConfig(process.cwd());
3465
+ const modulesDir = config?.modulesDir ?? "src/modules";
3466
+ const files = await generateTest({
3467
+ name,
3468
+ outDir: opts.out,
3469
+ moduleName: opts.module,
3470
+ modulesDir
2317
3471
  });
2318
3472
  printGenerated(files);
2319
3473
  });
2320
3474
  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) => {
2321
3475
  const files = await generateResolver({
2322
3476
  name,
2323
- outDir: resolve2(opts.out)
3477
+ outDir: resolve4(opts.out)
2324
3478
  });
2325
3479
  printGenerated(files);
2326
3480
  });
2327
3481
  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) => {
2328
3482
  const files = await generateJob({
2329
3483
  name,
2330
- outDir: resolve2(opts.out),
3484
+ outDir: resolve4(opts.out),
2331
3485
  queue: opts.queue
2332
3486
  });
2333
3487
  printGenerated(files);
2334
3488
  });
2335
- gen.command("scaffold <name> [fields...]").description("Generate a full CRUD module from field definitions\n Example: kick g scaffold Post title:string body:text published:boolean?\n Types: string, text, number, int, float, boolean, date, email, url, uuid, json, enum:a,b,c\n Append ? for optional fields: description:text?").option("--no-entity", "Skip entity and value object generation").option("--no-tests", "Skip test file generation").option("--modules-dir <dir>", "Modules directory", "src/modules").action(async (name, rawFields, opts) => {
3489
+ gen.command("scaffold <name> [fields...]").description("Generate a full CRUD module from field definitions\n Example: kick g scaffold Post title:string body:text published:boolean?\n Types: string, text, number, int, float, boolean, date, email, url, uuid, json, enum:a,b,c\n Append ? for optional fields: description:text?").option("--no-entity", "Skip entity and value object generation").option("--no-tests", "Skip test file generation").option("--modules-dir <dir>", "Modules directory").action(async (name, rawFields, opts) => {
2336
3490
  if (rawFields.length === 0) {
2337
3491
  console.error("\n Error: At least one field is required.\n Usage: kick g scaffold <name> <field:type> [field:type...]\n Example: kick g scaffold Post title:string body:text published:boolean\n");
2338
3492
  process.exit(1);
2339
3493
  }
3494
+ const config = await loadKickConfig(process.cwd());
3495
+ const modulesDir = opts.modulesDir ?? config?.modulesDir ?? "src/modules";
2340
3496
  const fields = parseFields(rawFields);
2341
3497
  const files = await generateScaffold({
2342
3498
  name,
2343
3499
  fields,
2344
- modulesDir: resolve2(opts.modulesDir),
3500
+ modulesDir: resolve4(modulesDir),
2345
3501
  noEntity: opts.entity === false,
2346
3502
  noTests: opts.tests === false
2347
3503
  });
@@ -2352,9 +3508,9 @@ function registerGenerateCommand(program) {
2352
3508
  }
2353
3509
  printGenerated(files);
2354
3510
  });
2355
- gen.command("config").description("Generate a kick.config.ts at the project root").option("--modules-dir <dir>", "Modules directory path", "src/modules").option("--repo <type>", "Default repository type: inmemory | drizzle", "inmemory").option("-f, --force", "Overwrite existing kick.config.ts without prompting").action(async (opts) => {
3511
+ gen.command("config").description("Generate a kick.config.ts at the project root").option("--modules-dir <dir>", "Modules directory path", "src/modules").option("--repo <type>", "Default repository type: inmemory | drizzle | prisma", "inmemory").option("-f, --force", "Overwrite existing kick.config.ts without prompting").action(async (opts) => {
2356
3512
  const files = await generateConfig({
2357
- outDir: resolve2("."),
3513
+ outDir: resolve4("."),
2358
3514
  modulesDir: opts.modulesDir,
2359
3515
  defaultRepo: opts.repo,
2360
3516
  force: opts.force
@@ -2366,7 +3522,7 @@ __name(registerGenerateCommand, "registerGenerateCommand");
2366
3522
 
2367
3523
  // src/commands/run.ts
2368
3524
  import { cpSync, existsSync as existsSync3, mkdirSync } from "fs";
2369
- import { resolve as resolve3, join as join14 } from "path";
3525
+ import { resolve as resolve5, join as join16 } from "path";
2370
3526
 
2371
3527
  // src/utils/shell.ts
2372
3528
  import { execSync as execSync2 } from "child_process";
@@ -2378,42 +3534,6 @@ function runShellCommand(command, cwd) {
2378
3534
  }
2379
3535
  __name(runShellCommand, "runShellCommand");
2380
3536
 
2381
- // src/config.ts
2382
- import { readFile as readFile4, access as access2 } from "fs/promises";
2383
- import { join as join13 } from "path";
2384
- var CONFIG_FILES = [
2385
- "kick.config.ts",
2386
- "kick.config.js",
2387
- "kick.config.mjs",
2388
- "kick.config.json"
2389
- ];
2390
- async function loadKickConfig(cwd) {
2391
- for (const filename of CONFIG_FILES) {
2392
- const filepath = join13(cwd, filename);
2393
- try {
2394
- await access2(filepath);
2395
- } catch {
2396
- continue;
2397
- }
2398
- if (filename.endsWith(".json")) {
2399
- const content = await readFile4(filepath, "utf-8");
2400
- return JSON.parse(content);
2401
- }
2402
- try {
2403
- const { pathToFileURL: pathToFileURL2 } = await import("url");
2404
- const mod = await import(pathToFileURL2(filepath).href);
2405
- return mod.default ?? mod;
2406
- } catch (err) {
2407
- if (filename.endsWith(".ts")) {
2408
- 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.`);
2409
- }
2410
- continue;
2411
- }
2412
- }
2413
- return null;
2414
- }
2415
- __name(loadKickConfig, "loadKickConfig");
2416
-
2417
3537
  // src/commands/run.ts
2418
3538
  function registerRunCommands(program) {
2419
3539
  program.command("dev").description("Start development server with Vite HMR (zero-downtime reload)").option("-e, --entry <file>", "Entry file", "src/index.ts").option("-p, --port <port>", "Port number").action((opts) => {
@@ -2440,9 +3560,9 @@ function registerRunCommands(program) {
2440
3560
  console.log("\n Copying directories to dist...");
2441
3561
  for (const entry of copyDirs) {
2442
3562
  const src = typeof entry === "string" ? entry : entry.src;
2443
- const dest = typeof entry === "string" ? join14("dist", entry) : entry.dest ?? join14("dist", src);
2444
- const srcPath = resolve3(src);
2445
- const destPath = resolve3(dest);
3563
+ const dest = typeof entry === "string" ? join16("dist", entry) : entry.dest ?? join16("dist", src);
3564
+ const srcPath = resolve5(src);
3565
+ const destPath = resolve5(dest);
2446
3566
  if (!existsSync3(srcPath)) {
2447
3567
  console.log(` \u26A0 Skipped ${src} (not found)`);
2448
3568
  continue;
@@ -2691,7 +3811,7 @@ __name(registerInspectCommand, "registerInspectCommand");
2691
3811
  // src/commands/add.ts
2692
3812
  import { execSync as execSync3 } from "child_process";
2693
3813
  import { existsSync as existsSync4 } from "fs";
2694
- import { resolve as resolve4 } from "path";
3814
+ import { resolve as resolve6 } from "path";
2695
3815
  var PACKAGE_REGISTRY = {
2696
3816
  // Core (already installed by kick new)
2697
3817
  core: {
@@ -2841,24 +3961,34 @@ var PACKAGE_REGISTRY = {
2841
3961
  }
2842
3962
  };
2843
3963
  function detectPackageManager() {
2844
- if (existsSync4(resolve4("pnpm-lock.yaml"))) return "pnpm";
2845
- if (existsSync4(resolve4("yarn.lock"))) return "yarn";
3964
+ if (existsSync4(resolve6("pnpm-lock.yaml"))) return "pnpm";
3965
+ if (existsSync4(resolve6("yarn.lock"))) return "yarn";
2846
3966
  return "npm";
2847
3967
  }
2848
3968
  __name(detectPackageManager, "detectPackageManager");
3969
+ function printPackageList() {
3970
+ console.log("\n Available KickJS packages:\n");
3971
+ const maxName = Math.max(...Object.keys(PACKAGE_REGISTRY).map((k) => k.length));
3972
+ for (const [name, info] of Object.entries(PACKAGE_REGISTRY)) {
3973
+ const padded = name.padEnd(maxName + 2);
3974
+ const peers = info.peers.length ? ` (+ ${info.peers.join(", ")})` : "";
3975
+ console.log(` ${padded} ${info.description}${peers}`);
3976
+ }
3977
+ console.log("\n Usage: kick add graphql drizzle otel");
3978
+ console.log(" kick add queue:bullmq");
3979
+ console.log();
3980
+ }
3981
+ __name(printPackageList, "printPackageList");
3982
+ function registerListCommand(program) {
3983
+ program.command("list").alias("ls").description("List all available KickJS packages").action(() => {
3984
+ printPackageList();
3985
+ });
3986
+ }
3987
+ __name(registerListCommand, "registerListCommand");
2849
3988
  function registerAddCommand(program) {
2850
3989
  program.command("add [packages...]").description("Add KickJS packages with their required dependencies").option("--pm <manager>", "Package manager override").option("-D, --dev", "Install as dev dependency").option("--list", "List all available packages").action(async (packages, opts) => {
2851
3990
  if (opts.list || packages.length === 0) {
2852
- console.log("\n Available KickJS packages:\n");
2853
- const maxName = Math.max(...Object.keys(PACKAGE_REGISTRY).map((k) => k.length));
2854
- for (const [name, info] of Object.entries(PACKAGE_REGISTRY)) {
2855
- const padded = name.padEnd(maxName + 2);
2856
- const peers = info.peers.length ? ` (+ ${info.peers.join(", ")})` : "";
2857
- console.log(` ${padded} ${info.description}${peers}`);
2858
- }
2859
- console.log("\n Usage: kick add graphql drizzle otel");
2860
- console.log(" kick add queue:bullmq");
2861
- console.log();
3991
+ printPackageList();
2862
3992
  return;
2863
3993
  }
2864
3994
  const pm = opts.pm ?? detectPackageManager();
@@ -2926,14 +4056,14 @@ function registerAddCommand(program) {
2926
4056
  __name(registerAddCommand, "registerAddCommand");
2927
4057
 
2928
4058
  // src/commands/tinker.ts
2929
- import { resolve as resolve5, join as join15 } from "path";
4059
+ import { resolve as resolve7, join as join17 } from "path";
2930
4060
  import { existsSync as existsSync5 } from "fs";
2931
4061
  import { pathToFileURL } from "url";
2932
4062
  import { fork } from "child_process";
2933
4063
  function registerTinkerCommand(program) {
2934
4064
  program.command("tinker").description("Interactive REPL with DI container and services loaded").option("-e, --entry <file>", "Entry file to load", "src/index.ts").action(async (opts) => {
2935
4065
  const cwd = process.cwd();
2936
- const entryPath = resolve5(cwd, opts.entry);
4066
+ const entryPath = resolve7(cwd, opts.entry);
2937
4067
  if (!existsSync5(entryPath)) {
2938
4068
  console.error(`
2939
4069
  Error: ${opts.entry} not found.
@@ -2946,7 +4076,7 @@ function registerTinkerCommand(program) {
2946
4076
  process.exit(1);
2947
4077
  }
2948
4078
  const tinkerScript = generateTinkerScript(entryPath, opts.entry);
2949
- const tmpFile = join15(cwd, ".kick-tinker.mjs");
4079
+ const tmpFile = join17(cwd, ".kick-tinker.mjs");
2950
4080
  const { writeFileSync, unlinkSync } = await import("fs");
2951
4081
  writeFileSync(tmpFile, tinkerScript, "utf-8");
2952
4082
  try {
@@ -2955,8 +4085,8 @@ function registerTinkerCommand(program) {
2955
4085
  execPath: tsxBin,
2956
4086
  stdio: "inherit"
2957
4087
  });
2958
- await new Promise((resolve6) => {
2959
- child.on("exit", () => resolve6());
4088
+ await new Promise((resolve8) => {
4089
+ child.on("exit", () => resolve8());
2960
4090
  });
2961
4091
  } finally {
2962
4092
  try {
@@ -3029,9 +4159,9 @@ __name(generateTinkerScript, "generateTinkerScript");
3029
4159
  function findBin(startDir, name) {
3030
4160
  let dir = startDir;
3031
4161
  while (true) {
3032
- const candidate = join15(dir, "node_modules", ".bin", name);
4162
+ const candidate = join17(dir, "node_modules", ".bin", name);
3033
4163
  if (existsSync5(candidate)) return candidate;
3034
- const parent = resolve5(dir, "..");
4164
+ const parent = resolve7(dir, "..");
3035
4165
  if (parent === dir) break;
3036
4166
  dir = parent;
3037
4167
  }
@@ -3041,7 +4171,7 @@ __name(findBin, "findBin");
3041
4171
 
3042
4172
  // src/cli.ts
3043
4173
  var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
3044
- var pkg = JSON.parse(readFileSync2(join16(__dirname2, "..", "package.json"), "utf-8"));
4174
+ var pkg = JSON.parse(readFileSync2(join18(__dirname2, "..", "package.json"), "utf-8"));
3045
4175
  async function main() {
3046
4176
  const program = new Command();
3047
4177
  program.name("kick").description("KickJS \u2014 A production-grade, decorator-driven Node.js framework").version(pkg.version);
@@ -3052,6 +4182,7 @@ async function main() {
3052
4182
  registerInfoCommand(program);
3053
4183
  registerInspectCommand(program);
3054
4184
  registerAddCommand(program);
4185
+ registerListCommand(program);
3055
4186
  registerTinkerCommand(program);
3056
4187
  registerCustomCommands(program, config);
3057
4188
  program.showHelpAfterError();