@forinda/kickjs-cli 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1065 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+
4
+ // src/generators/module.ts
5
+ import { join } from "path";
6
+
7
+ // src/utils/fs.ts
8
+ import { writeFile, mkdir, access, readFile } from "fs/promises";
9
+ import { dirname } from "path";
10
+ async function writeFileSafe(filePath, content) {
11
+ await mkdir(dirname(filePath), {
12
+ recursive: true
13
+ });
14
+ await writeFile(filePath, content, "utf-8");
15
+ }
16
+ __name(writeFileSafe, "writeFileSafe");
17
+ async function fileExists(filePath) {
18
+ try {
19
+ await access(filePath);
20
+ return true;
21
+ } catch {
22
+ return false;
23
+ }
24
+ }
25
+ __name(fileExists, "fileExists");
26
+
27
+ // src/utils/naming.ts
28
+ function toPascalCase(name) {
29
+ return name.replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^(.)/, (c) => c.toUpperCase());
30
+ }
31
+ __name(toPascalCase, "toPascalCase");
32
+ function toCamelCase(name) {
33
+ const pascal = toPascalCase(name);
34
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
35
+ }
36
+ __name(toCamelCase, "toCamelCase");
37
+ function toKebabCase(name) {
38
+ return name.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
39
+ }
40
+ __name(toKebabCase, "toKebabCase");
41
+ function pluralize(name) {
42
+ if (name.endsWith("s")) return name;
43
+ if (name.endsWith("x") || name.endsWith("z")) return name + "es";
44
+ if (name.endsWith("sh") || name.endsWith("ch")) return name + "es";
45
+ if (name.endsWith("y") && !/[aeiou]y$/.test(name)) return name.slice(0, -1) + "ies";
46
+ return name + "s";
47
+ }
48
+ __name(pluralize, "pluralize");
49
+ function pluralizePascal(name) {
50
+ if (name.endsWith("s")) return name;
51
+ if (name.endsWith("x") || name.endsWith("z")) return name + "es";
52
+ if (name.endsWith("sh") || name.endsWith("ch")) return name + "es";
53
+ if (name.endsWith("y") && !/[aeiou]y$/i.test(name)) return name.slice(0, -1) + "ies";
54
+ return name + "s";
55
+ }
56
+ __name(pluralizePascal, "pluralizePascal");
57
+
58
+ // src/generators/module.ts
59
+ import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
60
+ async function generateModule(options) {
61
+ const { name, modulesDir, noEntity, noTests, repo = "inmemory", minimal } = options;
62
+ const kebab = toKebabCase(name);
63
+ const pascal = toPascalCase(name);
64
+ const camel = toCamelCase(name);
65
+ const plural = pluralize(kebab);
66
+ const pluralPascal = pluralizePascal(pascal);
67
+ const moduleDir = join(modulesDir, plural);
68
+ const files = [];
69
+ const write = /* @__PURE__ */ __name(async (relativePath, content) => {
70
+ const fullPath = join(moduleDir, relativePath);
71
+ await writeFileSafe(fullPath, content);
72
+ files.push(fullPath);
73
+ }, "write");
74
+ await write("index.ts", `/**
75
+ * ${pascal} Module
76
+ *
77
+ * Self-contained feature module following Domain-Driven Design (DDD).
78
+ * Registers dependencies in the DI container and declares HTTP routes.
79
+ *
80
+ * Structure:
81
+ * presentation/ \u2014 HTTP controllers (entry points)
82
+ * application/ \u2014 Use cases (orchestration) and DTOs (validation)
83
+ * domain/ \u2014 Entities, value objects, repository interfaces, domain services
84
+ * infrastructure/ \u2014 Repository implementations (in-memory, Drizzle, Prisma, etc.)
85
+ */
86
+ import { Container, type AppModule, type ModuleRoutes } from '@forinda/kickjs-core'
87
+ import { buildRoutes } from '@forinda/kickjs-http'
88
+ import { ${pascal.toUpperCase()}_REPOSITORY } from './domain/repositories/${kebab}.repository'
89
+ import { ${repo === "inmemory" ? `InMemory${pascal}Repository` : `Drizzle${pascal}Repository`} } from './infrastructure/repositories/${repo === "inmemory" ? `in-memory-${kebab}` : `drizzle-${kebab}`}.repository'
90
+ import { ${pascal}Controller } from './presentation/${kebab}.controller'
91
+
92
+ // Eagerly load decorated classes so @Service()/@Repository() decorators register in the DI container
93
+ import.meta.glob(
94
+ ['./domain/services/**/*.ts', './application/use-cases/**/*.ts', '!./**/*.test.ts'],
95
+ { eager: true },
96
+ )
97
+
98
+ export class ${pascal}Module implements AppModule {
99
+ /**
100
+ * Register module dependencies in the DI container.
101
+ * Bind repository interface tokens to their implementations here.
102
+ * To swap implementations (e.g. in-memory -> Drizzle), change the factory target.
103
+ */
104
+ register(container: Container): void {
105
+ container.registerFactory(${pascal.toUpperCase()}_REPOSITORY, () =>
106
+ container.resolve(${repo === "inmemory" ? `InMemory${pascal}Repository` : `Drizzle${pascal}Repository`}),
107
+ )
108
+ }
109
+
110
+ /**
111
+ * Declare HTTP routes for this module.
112
+ * The path is prefixed with the global apiPrefix and version (e.g. /api/v1/${plural}).
113
+ * Passing 'controller' enables automatic OpenAPI spec generation via SwaggerAdapter.
114
+ */
115
+ routes(): ModuleRoutes {
116
+ return {
117
+ path: '/${plural}',
118
+ router: buildRoutes(${pascal}Controller),
119
+ controller: ${pascal}Controller,
120
+ }
121
+ }
122
+ }
123
+ `);
124
+ await write(`presentation/${kebab}.controller.ts`, `/**
125
+ * ${pascal} Controller
126
+ *
127
+ * Presentation layer \u2014 handles HTTP requests and delegates to use cases.
128
+ * Each method receives a RequestContext with typed body, params, and query.
129
+ *
130
+ * Decorators:
131
+ * @Controller(path?) \u2014 registers this class as an HTTP controller
132
+ * @Get/@Post/@Put/@Delete(path?, validation?) \u2014 defines routes with optional Zod validation
133
+ * @Autowired() \u2014 injects dependencies lazily from the DI container
134
+ * @Middleware(...handlers) \u2014 attach middleware at class or method level
135
+ *
136
+ * Add Swagger decorators (@ApiTags, @ApiOperation, @ApiResponse) from @forinda/kickjs-swagger
137
+ * for automatic OpenAPI documentation.
138
+ */
139
+ import { Controller, Get, Post, Put, Delete, Autowired } from '@forinda/kickjs-core'
140
+ import { RequestContext } from '@forinda/kickjs-http'
141
+ import { Create${pascal}UseCase } from '../application/use-cases/create-${kebab}.use-case'
142
+ import { Get${pascal}UseCase } from '../application/use-cases/get-${kebab}.use-case'
143
+ import { List${pluralPascal}UseCase } from '../application/use-cases/list-${plural}.use-case'
144
+ import { Update${pascal}UseCase } from '../application/use-cases/update-${kebab}.use-case'
145
+ import { Delete${pascal}UseCase } from '../application/use-cases/delete-${kebab}.use-case'
146
+ import { create${pascal}Schema } from '../application/dtos/create-${kebab}.dto'
147
+ import { update${pascal}Schema } from '../application/dtos/update-${kebab}.dto'
148
+
149
+ @Controller()
150
+ export class ${pascal}Controller {
151
+ @Autowired() private create${pascal}UseCase!: Create${pascal}UseCase
152
+ @Autowired() private get${pascal}UseCase!: Get${pascal}UseCase
153
+ @Autowired() private list${pluralPascal}UseCase!: List${pluralPascal}UseCase
154
+ @Autowired() private update${pascal}UseCase!: Update${pascal}UseCase
155
+ @Autowired() private delete${pascal}UseCase!: Delete${pascal}UseCase
156
+
157
+ @Post('/', { body: create${pascal}Schema })
158
+ async create(ctx: RequestContext) {
159
+ const result = await this.create${pascal}UseCase.execute(ctx.body)
160
+ ctx.created(result)
161
+ }
162
+
163
+ @Get('/')
164
+ async list(ctx: RequestContext) {
165
+ const result = await this.list${pluralPascal}UseCase.execute()
166
+ ctx.json(result)
167
+ }
168
+
169
+ @Get('/:id')
170
+ async getById(ctx: RequestContext) {
171
+ const result = await this.get${pascal}UseCase.execute(ctx.params.id)
172
+ if (!result) return ctx.notFound('${pascal} not found')
173
+ ctx.json(result)
174
+ }
175
+
176
+ @Put('/:id', { body: update${pascal}Schema })
177
+ async update(ctx: RequestContext) {
178
+ const result = await this.update${pascal}UseCase.execute(ctx.params.id, ctx.body)
179
+ ctx.json(result)
180
+ }
181
+
182
+ @Delete('/:id')
183
+ async remove(ctx: RequestContext) {
184
+ await this.delete${pascal}UseCase.execute(ctx.params.id)
185
+ ctx.noContent()
186
+ }
187
+ }
188
+ `);
189
+ await write(`application/dtos/create-${kebab}.dto.ts`, `import { z } from 'zod'
190
+
191
+ /**
192
+ * Create ${pascal} DTO \u2014 Zod schema for validating POST request bodies.
193
+ * This schema is passed to @Post('/', { body: create${pascal}Schema }) for automatic validation.
194
+ * It also generates OpenAPI request body docs when SwaggerAdapter is used.
195
+ *
196
+ * Add more fields as needed. Supported Zod types:
197
+ * z.string(), z.number(), z.boolean(), z.enum([...]),
198
+ * z.array(), z.object(), .optional(), .default(), .transform()
199
+ */
200
+ export const create${pascal}Schema = z.object({
201
+ name: z.string().min(1, 'Name is required').max(200),
202
+ })
203
+
204
+ export type Create${pascal}DTO = z.infer<typeof create${pascal}Schema>
205
+ `);
206
+ await write(`application/dtos/update-${kebab}.dto.ts`, `import { z } from 'zod'
207
+
208
+ export const update${pascal}Schema = z.object({
209
+ name: z.string().min(1).max(200).optional(),
210
+ })
211
+
212
+ export type Update${pascal}DTO = z.infer<typeof update${pascal}Schema>
213
+ `);
214
+ await write(`application/dtos/${kebab}-response.dto.ts`, `export interface ${pascal}ResponseDTO {
215
+ id: string
216
+ name: string
217
+ createdAt: string
218
+ updatedAt: string
219
+ }
220
+ `);
221
+ const useCases = [
222
+ {
223
+ file: `create-${kebab}.use-case.ts`,
224
+ content: `/**
225
+ * Create ${pascal} Use Case
226
+ *
227
+ * Application layer \u2014 orchestrates a single business operation.
228
+ * Use cases are thin: validate input (via DTO), call domain/repo, return response.
229
+ * Keep business rules in the domain service, not here.
230
+ */
231
+ import { Service, Inject } from '@forinda/kickjs-core'
232
+ import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../../domain/repositories/${kebab}.repository'
233
+ import type { Create${pascal}DTO } from '../dtos/create-${kebab}.dto'
234
+ import type { ${pascal}ResponseDTO } from '../dtos/${kebab}-response.dto'
235
+
236
+ @Service()
237
+ export class Create${pascal}UseCase {
238
+ constructor(
239
+ @Inject(${pascal.toUpperCase()}_REPOSITORY) private readonly repo: I${pascal}Repository,
240
+ ) {}
241
+
242
+ async execute(dto: Create${pascal}DTO): Promise<${pascal}ResponseDTO> {
243
+ return this.repo.create(dto)
244
+ }
245
+ }
246
+ `
247
+ },
248
+ {
249
+ file: `get-${kebab}.use-case.ts`,
250
+ content: `import { Service, Inject } from '@forinda/kickjs-core'
251
+ import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../../domain/repositories/${kebab}.repository'
252
+ import type { ${pascal}ResponseDTO } from '../dtos/${kebab}-response.dto'
253
+
254
+ @Service()
255
+ export class Get${pascal}UseCase {
256
+ constructor(
257
+ @Inject(${pascal.toUpperCase()}_REPOSITORY) private readonly repo: I${pascal}Repository,
258
+ ) {}
259
+
260
+ async execute(id: string): Promise<${pascal}ResponseDTO | null> {
261
+ return this.repo.findById(id)
262
+ }
263
+ }
264
+ `
265
+ },
266
+ {
267
+ file: `list-${plural}.use-case.ts`,
268
+ content: `import { Service, Inject } from '@forinda/kickjs-core'
269
+ import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../../domain/repositories/${kebab}.repository'
270
+ import type { ${pascal}ResponseDTO } from '../dtos/${kebab}-response.dto'
271
+
272
+ @Service()
273
+ export class List${pluralPascal}UseCase {
274
+ constructor(
275
+ @Inject(${pascal.toUpperCase()}_REPOSITORY) private readonly repo: I${pascal}Repository,
276
+ ) {}
277
+
278
+ async execute(): Promise<${pascal}ResponseDTO[]> {
279
+ return this.repo.findAll()
280
+ }
281
+ }
282
+ `
283
+ },
284
+ {
285
+ file: `update-${kebab}.use-case.ts`,
286
+ content: `import { Service, Inject } from '@forinda/kickjs-core'
287
+ import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../../domain/repositories/${kebab}.repository'
288
+ import type { Update${pascal}DTO } from '../dtos/update-${kebab}.dto'
289
+ import type { ${pascal}ResponseDTO } from '../dtos/${kebab}-response.dto'
290
+
291
+ @Service()
292
+ export class Update${pascal}UseCase {
293
+ constructor(
294
+ @Inject(${pascal.toUpperCase()}_REPOSITORY) private readonly repo: I${pascal}Repository,
295
+ ) {}
296
+
297
+ async execute(id: string, dto: Update${pascal}DTO): Promise<${pascal}ResponseDTO> {
298
+ return this.repo.update(id, dto)
299
+ }
300
+ }
301
+ `
302
+ },
303
+ {
304
+ file: `delete-${kebab}.use-case.ts`,
305
+ content: `import { Service, Inject } from '@forinda/kickjs-core'
306
+ import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../../domain/repositories/${kebab}.repository'
307
+
308
+ @Service()
309
+ export class Delete${pascal}UseCase {
310
+ constructor(
311
+ @Inject(${pascal.toUpperCase()}_REPOSITORY) private readonly repo: I${pascal}Repository,
312
+ ) {}
313
+
314
+ async execute(id: string): Promise<void> {
315
+ await this.repo.delete(id)
316
+ }
317
+ }
318
+ `
319
+ }
320
+ ];
321
+ for (const uc of useCases) {
322
+ await write(`application/use-cases/${uc.file}`, uc.content);
323
+ }
324
+ await write(`domain/repositories/${kebab}.repository.ts`, `/**
325
+ * ${pascal} Repository Interface
326
+ *
327
+ * Domain layer \u2014 defines the contract for data access.
328
+ * The interface lives in the domain layer; implementations live in infrastructure.
329
+ * This inversion of dependencies keeps the domain pure and testable.
330
+ *
331
+ * To swap implementations (e.g. in-memory -> Drizzle -> Prisma),
332
+ * change the factory in the module's register() method.
333
+ */
334
+ import type { ${pascal}ResponseDTO } from '../../application/dtos/${kebab}-response.dto'
335
+ import type { Create${pascal}DTO } from '../../application/dtos/create-${kebab}.dto'
336
+ import type { Update${pascal}DTO } from '../../application/dtos/update-${kebab}.dto'
337
+
338
+ export interface I${pascal}Repository {
339
+ findById(id: string): Promise<${pascal}ResponseDTO | null>
340
+ findAll(): Promise<${pascal}ResponseDTO[]>
341
+ create(dto: Create${pascal}DTO): Promise<${pascal}ResponseDTO>
342
+ update(id: string, dto: Update${pascal}DTO): Promise<${pascal}ResponseDTO>
343
+ delete(id: string): Promise<void>
344
+ }
345
+
346
+ export const ${pascal.toUpperCase()}_REPOSITORY = Symbol('I${pascal}Repository')
347
+ `);
348
+ await write(`domain/services/${kebab}-domain.service.ts`, `/**
349
+ * ${pascal} Domain Service
350
+ *
351
+ * Domain layer \u2014 contains business rules that don't belong to a single entity.
352
+ * Use this for cross-entity logic, validation rules, and domain invariants.
353
+ * Keep it free of HTTP/framework concerns.
354
+ */
355
+ import { Service, Inject, HttpException } from '@forinda/kickjs-core'
356
+ import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../repositories/${kebab}.repository'
357
+
358
+ @Service()
359
+ export class ${pascal}DomainService {
360
+ constructor(
361
+ @Inject(${pascal.toUpperCase()}_REPOSITORY) private readonly repo: I${pascal}Repository,
362
+ ) {}
363
+
364
+ async ensureExists(id: string): Promise<void> {
365
+ const entity = await this.repo.findById(id)
366
+ if (!entity) {
367
+ throw HttpException.notFound('${pascal} not found')
368
+ }
369
+ }
370
+ }
371
+ `);
372
+ if (repo === "inmemory") {
373
+ await write(`infrastructure/repositories/in-memory-${kebab}.repository.ts`, `/**
374
+ * In-Memory ${pascal} Repository
375
+ *
376
+ * Infrastructure layer \u2014 implements the repository interface using a Map.
377
+ * Useful for prototyping and testing. Replace with a database implementation
378
+ * (Drizzle, Prisma, etc.) for production use.
379
+ *
380
+ * @Repository() registers this class in the DI container as a singleton.
381
+ */
382
+ import { randomUUID } from 'node:crypto'
383
+ import { Repository, HttpException } from '@forinda/kickjs-core'
384
+ import type { I${pascal}Repository } from '../../domain/repositories/${kebab}.repository'
385
+ import type { ${pascal}ResponseDTO } from '../../application/dtos/${kebab}-response.dto'
386
+ import type { Create${pascal}DTO } from '../../application/dtos/create-${kebab}.dto'
387
+ import type { Update${pascal}DTO } from '../../application/dtos/update-${kebab}.dto'
388
+
389
+ @Repository()
390
+ export class InMemory${pascal}Repository implements I${pascal}Repository {
391
+ private store = new Map<string, ${pascal}ResponseDTO>()
392
+
393
+ async findById(id: string): Promise<${pascal}ResponseDTO | null> {
394
+ return this.store.get(id) ?? null
395
+ }
396
+
397
+ async findAll(): Promise<${pascal}ResponseDTO[]> {
398
+ return Array.from(this.store.values())
399
+ }
400
+
401
+ async create(dto: Create${pascal}DTO): Promise<${pascal}ResponseDTO> {
402
+ const now = new Date().toISOString()
403
+ const entity: ${pascal}ResponseDTO = {
404
+ id: randomUUID(),
405
+ name: dto.name,
406
+ createdAt: now,
407
+ updatedAt: now,
408
+ }
409
+ this.store.set(entity.id, entity)
410
+ return entity
411
+ }
412
+
413
+ async update(id: string, dto: Update${pascal}DTO): Promise<${pascal}ResponseDTO> {
414
+ const existing = this.store.get(id)
415
+ if (!existing) throw HttpException.notFound('${pascal} not found')
416
+ const updated = { ...existing, ...dto, updatedAt: new Date().toISOString() }
417
+ this.store.set(id, updated)
418
+ return updated
419
+ }
420
+
421
+ async delete(id: string): Promise<void> {
422
+ if (!this.store.has(id)) throw HttpException.notFound('${pascal} not found')
423
+ this.store.delete(id)
424
+ }
425
+ }
426
+ `);
427
+ }
428
+ if (!noEntity && !minimal) {
429
+ await write(`domain/entities/${kebab}.entity.ts`, `/**
430
+ * ${pascal} Entity
431
+ *
432
+ * Domain layer \u2014 the core business object.
433
+ * Uses a private constructor with static factory methods (create, reconstitute)
434
+ * to enforce invariants. Properties are accessed via getters to maintain encapsulation.
435
+ *
436
+ * Patterns used:
437
+ * - Private constructor: prevents direct instantiation
438
+ * - create(): factory for new entities (generates ID, sets timestamps)
439
+ * - reconstitute(): factory for rebuilding from persistence (no side effects)
440
+ * - changeName(): mutation method that enforces business rules
441
+ */
442
+ import { ${pascal}Id } from '../value-objects/${kebab}-id.vo'
443
+
444
+ interface ${pascal}Props {
445
+ id: ${pascal}Id
446
+ name: string
447
+ createdAt: Date
448
+ updatedAt: Date
449
+ }
450
+
451
+ export class ${pascal} {
452
+ private constructor(private props: ${pascal}Props) {}
453
+
454
+ static create(params: { name: string }): ${pascal} {
455
+ const now = new Date()
456
+ return new ${pascal}({
457
+ id: ${pascal}Id.create(),
458
+ name: params.name,
459
+ createdAt: now,
460
+ updatedAt: now,
461
+ })
462
+ }
463
+
464
+ static reconstitute(props: ${pascal}Props): ${pascal} {
465
+ return new ${pascal}(props)
466
+ }
467
+
468
+ get id(): ${pascal}Id {
469
+ return this.props.id
470
+ }
471
+ get name(): string {
472
+ return this.props.name
473
+ }
474
+ get createdAt(): Date {
475
+ return this.props.createdAt
476
+ }
477
+ get updatedAt(): Date {
478
+ return this.props.updatedAt
479
+ }
480
+
481
+ changeName(name: string): void {
482
+ if (!name || name.trim().length === 0) {
483
+ throw new Error('Name cannot be empty')
484
+ }
485
+ this.props.name = name.trim()
486
+ this.props.updatedAt = new Date()
487
+ }
488
+
489
+ toJSON() {
490
+ return {
491
+ id: this.props.id.toString(),
492
+ name: this.props.name,
493
+ createdAt: this.props.createdAt.toISOString(),
494
+ updatedAt: this.props.updatedAt.toISOString(),
495
+ }
496
+ }
497
+ }
498
+ `);
499
+ await write(`domain/value-objects/${kebab}-id.vo.ts`, `/**
500
+ * ${pascal} ID Value Object
501
+ *
502
+ * Domain layer \u2014 wraps a primitive ID with type safety and validation.
503
+ * Value objects are immutable and compared by value, not reference.
504
+ *
505
+ * ${pascal}Id.create() \u2014 generate a new UUID
506
+ * ${pascal}Id.from(id) \u2014 wrap an existing ID string (validates non-empty)
507
+ * id.equals(other) \u2014 compare two IDs by value
508
+ */
509
+ import { randomUUID } from 'node:crypto'
510
+
511
+ export class ${pascal}Id {
512
+ private constructor(private readonly value: string) {}
513
+
514
+ static create(): ${pascal}Id {
515
+ return new ${pascal}Id(randomUUID())
516
+ }
517
+
518
+ static from(id: string): ${pascal}Id {
519
+ if (!id || id.trim().length === 0) {
520
+ throw new Error('${pascal}Id cannot be empty')
521
+ }
522
+ return new ${pascal}Id(id)
523
+ }
524
+
525
+ toString(): string {
526
+ return this.value
527
+ }
528
+
529
+ equals(other: ${pascal}Id): boolean {
530
+ return this.value === other.value
531
+ }
532
+ }
533
+ `);
534
+ }
535
+ await autoRegisterModule(modulesDir, pascal, plural);
536
+ return files;
537
+ }
538
+ __name(generateModule, "generateModule");
539
+ async function autoRegisterModule(modulesDir, pascal, plural) {
540
+ const indexPath = join(modulesDir, "index.ts");
541
+ const exists = await fileExists(indexPath);
542
+ if (!exists) {
543
+ await writeFileSafe(indexPath, `import type { AppModuleClass } from '@forinda/kickjs-core'
544
+ import { ${pascal}Module } from './${plural}'
545
+
546
+ export const modules: AppModuleClass[] = [${pascal}Module]
547
+ `);
548
+ return;
549
+ }
550
+ let content = await readFile2(indexPath, "utf-8");
551
+ const importLine = `import { ${pascal}Module } from './${plural}'`;
552
+ if (!content.includes(`${pascal}Module`)) {
553
+ const lastImportIdx = content.lastIndexOf("import ");
554
+ if (lastImportIdx !== -1) {
555
+ const lineEnd = content.indexOf("\n", lastImportIdx);
556
+ content = content.slice(0, lineEnd + 1) + importLine + "\n" + content.slice(lineEnd + 1);
557
+ } else {
558
+ content = importLine + "\n" + content;
559
+ }
560
+ content = content.replace(/(=\s*\[)([\s\S]*?)(])/, (_match, open, existing, close) => {
561
+ const trimmed = existing.trim();
562
+ if (!trimmed) {
563
+ return `${open}${pascal}Module${close}`;
564
+ }
565
+ const needsComma = trimmed.endsWith(",") ? "" : ",";
566
+ return `${open}${existing.trimEnd()}${needsComma} ${pascal}Module${close}`;
567
+ });
568
+ }
569
+ await writeFile2(indexPath, content, "utf-8");
570
+ }
571
+ __name(autoRegisterModule, "autoRegisterModule");
572
+
573
+ // src/generators/adapter.ts
574
+ import { join as join2 } from "path";
575
+ async function generateAdapter(options) {
576
+ const { name, outDir } = options;
577
+ const kebab = toKebabCase(name);
578
+ const pascal = toPascalCase(name);
579
+ const files = [];
580
+ const filePath = join2(outDir, `${kebab}.adapter.ts`);
581
+ await writeFileSafe(filePath, `import type { Express } from 'express'
582
+ import type { AppAdapter, AdapterMiddleware, Container } from '@forinda/kickjs-core'
583
+
584
+ export interface ${pascal}AdapterOptions {
585
+ // Add your adapter configuration here
586
+ }
587
+
588
+ /**
589
+ * ${pascal} adapter.
590
+ *
591
+ * Hooks into the Application lifecycle to add middleware, routes,
592
+ * or external service connections.
593
+ *
594
+ * Usage:
595
+ * bootstrap({
596
+ * adapters: [new ${pascal}Adapter({ ... })],
597
+ * })
598
+ */
599
+ export class ${pascal}Adapter implements AppAdapter {
600
+ name = '${pascal}Adapter'
601
+
602
+ constructor(private options: ${pascal}AdapterOptions = {}) {}
603
+
604
+ /**
605
+ * Return middleware entries that the Application will mount.
606
+ * Use \`phase\` to control where in the pipeline they run:
607
+ * 'beforeGlobal' | 'afterGlobal' | 'beforeRoutes' | 'afterRoutes'
608
+ */
609
+ middleware(): AdapterMiddleware[] {
610
+ return [
611
+ // Example: add a custom header to all responses
612
+ // {
613
+ // phase: 'beforeGlobal',
614
+ // handler: (_req: any, res: any, next: any) => {
615
+ // res.setHeader('X-${pascal}', 'true')
616
+ // next()
617
+ // },
618
+ // },
619
+ // Example: scope middleware to a specific path
620
+ // {
621
+ // phase: 'beforeRoutes',
622
+ // path: '/api/v1/admin',
623
+ // handler: myAdminMiddleware(),
624
+ // },
625
+ ]
626
+ }
627
+
628
+ /**
629
+ * Called before global middleware.
630
+ * Use this to mount routes that bypass the middleware stack
631
+ * (health checks, docs UI, static assets).
632
+ */
633
+ beforeMount(app: Express, container: Container): void {
634
+ // Example: mount a status route
635
+ // app.get('/${kebab}/status', (_req, res) => {
636
+ // res.json({ status: 'ok' })
637
+ // })
638
+ }
639
+
640
+ /**
641
+ * Called after modules and routes are registered, before the server starts.
642
+ * Use this for late-stage DI registrations or config validation.
643
+ */
644
+ beforeStart(app: Express, container: Container): void {
645
+ // Example: register a service in the DI container
646
+ // container.registerInstance(MY_TOKEN, new MyService(this.options))
647
+ }
648
+
649
+ /**
650
+ * Called after the HTTP server is listening.
651
+ * Use this to attach to the raw http.Server (Socket.IO, gRPC, etc).
652
+ */
653
+ afterStart(server: any, container: Container): void {
654
+ // Example: attach Socket.IO
655
+ // const io = new Server(server)
656
+ // container.registerInstance(SOCKET_IO, io)
657
+ }
658
+
659
+ /**
660
+ * Called on graceful shutdown. Clean up connections.
661
+ */
662
+ async shutdown(): Promise<void> {
663
+ // Example: close a connection pool
664
+ // await this.pool.end()
665
+ }
666
+ }
667
+ `);
668
+ files.push(filePath);
669
+ return files;
670
+ }
671
+ __name(generateAdapter, "generateAdapter");
672
+
673
+ // src/generators/middleware.ts
674
+ import { join as join3 } from "path";
675
+ async function generateMiddleware(options) {
676
+ const { name, outDir } = options;
677
+ const kebab = toKebabCase(name);
678
+ const camel = toCamelCase(name);
679
+ const files = [];
680
+ const filePath = join3(outDir, `${kebab}.middleware.ts`);
681
+ await writeFileSafe(filePath, `import type { Request, Response, NextFunction } from 'express'
682
+
683
+ export interface ${toPascalCase(name)}Options {
684
+ // Add configuration options here
685
+ }
686
+
687
+ /**
688
+ * ${toPascalCase(name)} middleware.
689
+ *
690
+ * Usage in bootstrap:
691
+ * middleware: [${camel}()]
692
+ *
693
+ * Usage with adapter:
694
+ * middleware() { return [{ handler: ${camel}(), phase: 'afterGlobal' }] }
695
+ *
696
+ * Usage with @Middleware decorator:
697
+ * @Middleware(${camel}())
698
+ */
699
+ export function ${camel}(options: ${toPascalCase(name)}Options = {}) {
700
+ return (req: Request, res: Response, next: NextFunction) => {
701
+ // Implement your middleware logic here
702
+ next()
703
+ }
704
+ }
705
+ `);
706
+ files.push(filePath);
707
+ return files;
708
+ }
709
+ __name(generateMiddleware, "generateMiddleware");
710
+
711
+ // src/generators/guard.ts
712
+ import { join as join4 } from "path";
713
+ async function generateGuard(options) {
714
+ const { name, outDir } = options;
715
+ const kebab = toKebabCase(name);
716
+ const camel = toCamelCase(name);
717
+ const pascal = toPascalCase(name);
718
+ const files = [];
719
+ const filePath = join4(outDir, `${kebab}.guard.ts`);
720
+ await writeFileSafe(filePath, `import { Container, HttpException } from '@forinda/kickjs-core'
721
+ import type { RequestContext } from '@forinda/kickjs-http'
722
+
723
+ /**
724
+ * ${pascal} guard.
725
+ *
726
+ * Guards protect routes by checking conditions before the handler runs.
727
+ * Return early with an error response to block access.
728
+ *
729
+ * Usage:
730
+ * @Middleware(${camel}Guard)
731
+ * @Get('/protected')
732
+ * async handler(ctx: RequestContext) { ... }
733
+ */
734
+ export async function ${camel}Guard(ctx: RequestContext, next: () => void): Promise<void> {
735
+ // Example: check for an authorization header
736
+ const header = ctx.headers.authorization
737
+ if (!header?.startsWith('Bearer ')) {
738
+ ctx.res.status(401).json({ message: 'Missing or invalid authorization header' })
739
+ return
740
+ }
741
+
742
+ const token = header.slice(7)
743
+
744
+ try {
745
+ // Verify the token using a service from the DI container
746
+ // const container = Container.getInstance()
747
+ // const authService = container.resolve(AuthService)
748
+ // const payload = authService.verifyToken(token)
749
+ // ctx.set('auth', payload)
750
+
751
+ next()
752
+ } catch {
753
+ ctx.res.status(401).json({ message: 'Invalid or expired token' })
754
+ }
755
+ }
756
+ `);
757
+ files.push(filePath);
758
+ return files;
759
+ }
760
+ __name(generateGuard, "generateGuard");
761
+
762
+ // src/generators/service.ts
763
+ import { join as join5 } from "path";
764
+ async function generateService(options) {
765
+ const { name, outDir } = options;
766
+ const kebab = toKebabCase(name);
767
+ const pascal = toPascalCase(name);
768
+ const files = [];
769
+ const filePath = join5(outDir, `${kebab}.service.ts`);
770
+ await writeFileSafe(filePath, `import { Service } from '@forinda/kickjs-core'
771
+
772
+ @Service()
773
+ export class ${pascal}Service {
774
+ // Inject dependencies via constructor
775
+ // constructor(
776
+ // @Inject(MY_REPO) private readonly repo: IMyRepository,
777
+ // ) {}
778
+ }
779
+ `);
780
+ files.push(filePath);
781
+ return files;
782
+ }
783
+ __name(generateService, "generateService");
784
+
785
+ // src/generators/controller.ts
786
+ import { join as join6 } from "path";
787
+ async function generateController(options) {
788
+ const { name, outDir } = options;
789
+ const kebab = toKebabCase(name);
790
+ const pascal = toPascalCase(name);
791
+ const files = [];
792
+ const filePath = join6(outDir, `${kebab}.controller.ts`);
793
+ await writeFileSafe(filePath, `import { Controller, Get, Post, Autowired } from '@forinda/kickjs-core'
794
+ import type { RequestContext } from '@forinda/kickjs-http'
795
+
796
+ @Controller()
797
+ export class ${pascal}Controller {
798
+ // @Autowired() private myService!: MyService
799
+
800
+ @Get('/')
801
+ async list(ctx: RequestContext) {
802
+ ctx.json({ message: '${pascal} list' })
803
+ }
804
+
805
+ @Post('/')
806
+ async create(ctx: RequestContext) {
807
+ ctx.created({ message: '${pascal} created', data: ctx.body })
808
+ }
809
+ }
810
+ `);
811
+ files.push(filePath);
812
+ return files;
813
+ }
814
+ __name(generateController, "generateController");
815
+
816
+ // src/generators/dto.ts
817
+ import { join as join7 } from "path";
818
+ async function generateDto(options) {
819
+ const { name, outDir } = options;
820
+ const kebab = toKebabCase(name);
821
+ const pascal = toPascalCase(name);
822
+ const camel = toCamelCase(name);
823
+ const files = [];
824
+ const filePath = join7(outDir, `${kebab}.dto.ts`);
825
+ await writeFileSafe(filePath, `import { z } from 'zod'
826
+
827
+ export const ${camel}Schema = z.object({
828
+ // Define your schema fields here
829
+ name: z.string().min(1).max(200),
830
+ })
831
+
832
+ export type ${pascal}DTO = z.infer<typeof ${camel}Schema>
833
+ `);
834
+ files.push(filePath);
835
+ return files;
836
+ }
837
+ __name(generateDto, "generateDto");
838
+
839
+ // src/generators/project.ts
840
+ import { join as join8 } from "path";
841
+ async function initProject(options) {
842
+ const { name, directory, packageManager = "pnpm" } = options;
843
+ const dir = directory;
844
+ console.log(`
845
+ Creating KickJS project: ${name}
846
+ `);
847
+ await writeFileSafe(join8(dir, "package.json"), JSON.stringify({
848
+ name,
849
+ version: "0.1.0",
850
+ type: "module",
851
+ scripts: {
852
+ dev: "kick dev",
853
+ "dev:debug": "kick dev:debug",
854
+ build: "kick build",
855
+ start: "kick start",
856
+ test: "vitest run",
857
+ "test:watch": "vitest",
858
+ typecheck: "tsc --noEmit",
859
+ lint: "eslint src/",
860
+ format: "prettier --write src/"
861
+ },
862
+ dependencies: {
863
+ "@forinda/kickjs-core": "^0.1.0",
864
+ "@forinda/kickjs-http": "^0.1.0",
865
+ "@forinda/kickjs-config": "^0.1.0",
866
+ "@forinda/kickjs-swagger": "^0.1.0",
867
+ express: "^5.1.0",
868
+ "reflect-metadata": "^0.2.2",
869
+ zod: "^4.3.6",
870
+ pino: "^10.3.1",
871
+ "pino-pretty": "^13.1.3"
872
+ },
873
+ devDependencies: {
874
+ "@forinda/kickjs-cli": "^0.1.0",
875
+ "@swc/core": "^1.7.28",
876
+ "@types/express": "^5.0.6",
877
+ "@types/node": "^24.5.2",
878
+ "unplugin-swc": "^1.5.9",
879
+ vite: "^7.3.1",
880
+ "vite-node": "^5.3.0",
881
+ vitest: "^3.2.4",
882
+ typescript: "^5.9.2",
883
+ prettier: "^3.8.1"
884
+ }
885
+ }, null, 2));
886
+ await writeFileSafe(join8(dir, "vite.config.ts"), `import { defineConfig } from 'vite'
887
+ import { resolve } from 'path'
888
+ import swc from 'unplugin-swc'
889
+
890
+ export default defineConfig({
891
+ plugins: [swc.vite()],
892
+ resolve: {
893
+ alias: {
894
+ '@': resolve(__dirname, 'src'),
895
+ },
896
+ },
897
+ server: {
898
+ watch: { usePolling: false },
899
+ hmr: true,
900
+ },
901
+ build: {
902
+ target: 'node20',
903
+ ssr: true,
904
+ outDir: 'dist',
905
+ sourcemap: true,
906
+ rollupOptions: {
907
+ input: resolve(__dirname, 'src/index.ts'),
908
+ output: { format: 'esm' },
909
+ },
910
+ },
911
+ })
912
+ `);
913
+ await writeFileSafe(join8(dir, "tsconfig.json"), JSON.stringify({
914
+ compilerOptions: {
915
+ target: "ES2022",
916
+ module: "ESNext",
917
+ moduleResolution: "bundler",
918
+ lib: [
919
+ "ES2022"
920
+ ],
921
+ types: [
922
+ "node"
923
+ ],
924
+ strict: true,
925
+ esModuleInterop: true,
926
+ skipLibCheck: true,
927
+ sourceMap: true,
928
+ declaration: true,
929
+ experimentalDecorators: true,
930
+ emitDecoratorMetadata: true,
931
+ outDir: "dist",
932
+ rootDir: "src",
933
+ paths: {
934
+ "@/*": [
935
+ "./src/*"
936
+ ]
937
+ }
938
+ },
939
+ include: [
940
+ "src"
941
+ ]
942
+ }, null, 2));
943
+ await writeFileSafe(join8(dir, ".prettierrc"), JSON.stringify({
944
+ semi: false,
945
+ singleQuote: true,
946
+ trailingComma: "all",
947
+ printWidth: 100,
948
+ tabWidth: 2
949
+ }, null, 2));
950
+ await writeFileSafe(join8(dir, ".gitignore"), `node_modules/
951
+ dist/
952
+ .env
953
+ coverage/
954
+ .DS_Store
955
+ *.tsbuildinfo
956
+ `);
957
+ await writeFileSafe(join8(dir, ".env"), `PORT=3000
958
+ NODE_ENV=development
959
+ `);
960
+ await writeFileSafe(join8(dir, ".env.example"), `PORT=3000
961
+ NODE_ENV=development
962
+ `);
963
+ await writeFileSafe(join8(dir, "src/index.ts"), `import 'reflect-metadata'
964
+ import { bootstrap } from '@forinda/kickjs-http'
965
+ import { SwaggerAdapter } from '@forinda/kickjs-swagger'
966
+ import { modules } from './modules'
967
+
968
+ bootstrap({
969
+ modules,
970
+ adapters: [
971
+ new SwaggerAdapter({
972
+ info: { title: '${name}', version: '0.1.0' },
973
+ }),
974
+ ],
975
+ })
976
+ `);
977
+ await writeFileSafe(join8(dir, "src/modules/index.ts"), `import type { AppModuleClass } from '@forinda/kickjs-core'
978
+
979
+ export const modules: AppModuleClass[] = []
980
+ `);
981
+ await writeFileSafe(join8(dir, "vitest.config.ts"), `import { defineConfig } from 'vitest/config'
982
+ import swc from 'unplugin-swc'
983
+
984
+ export default defineConfig({
985
+ plugins: [swc.vite()],
986
+ test: {
987
+ globals: true,
988
+ environment: 'node',
989
+ include: ['src/**/*.test.ts'],
990
+ },
991
+ })
992
+ `);
993
+ console.log(" Project scaffolded successfully!");
994
+ console.log();
995
+ console.log(" Next steps:");
996
+ console.log(` cd ${name}`);
997
+ console.log(` ${packageManager} install`);
998
+ console.log(` kick g module user`);
999
+ console.log(` kick dev`);
1000
+ console.log();
1001
+ console.log(" Commands:");
1002
+ console.log(" kick dev Start dev server with Vite HMR");
1003
+ console.log(" kick build Production build via Vite");
1004
+ console.log(" kick start Run production build");
1005
+ console.log(" kick g module X Generate a DDD module");
1006
+ console.log();
1007
+ }
1008
+ __name(initProject, "initProject");
1009
+
1010
+ // src/config.ts
1011
+ import { readFile as readFile3, access as access2 } from "fs/promises";
1012
+ import { join as join9 } from "path";
1013
+ function defineConfig(config) {
1014
+ return config;
1015
+ }
1016
+ __name(defineConfig, "defineConfig");
1017
+ var CONFIG_FILES = [
1018
+ "kick.config.ts",
1019
+ "kick.config.js",
1020
+ "kick.config.mjs",
1021
+ "kick.config.json"
1022
+ ];
1023
+ async function loadKickConfig(cwd) {
1024
+ for (const filename of CONFIG_FILES) {
1025
+ const filepath = join9(cwd, filename);
1026
+ try {
1027
+ await access2(filepath);
1028
+ } catch {
1029
+ continue;
1030
+ }
1031
+ if (filename.endsWith(".json")) {
1032
+ const content = await readFile3(filepath, "utf-8");
1033
+ return JSON.parse(content);
1034
+ }
1035
+ try {
1036
+ const { pathToFileURL } = await import("url");
1037
+ const mod = await import(pathToFileURL(filepath).href);
1038
+ return mod.default ?? mod;
1039
+ } catch (err) {
1040
+ if (filename.endsWith(".ts")) {
1041
+ 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.`);
1042
+ }
1043
+ continue;
1044
+ }
1045
+ }
1046
+ return null;
1047
+ }
1048
+ __name(loadKickConfig, "loadKickConfig");
1049
+ export {
1050
+ defineConfig,
1051
+ generateAdapter,
1052
+ generateController,
1053
+ generateDto,
1054
+ generateGuard,
1055
+ generateMiddleware,
1056
+ generateModule,
1057
+ generateService,
1058
+ initProject,
1059
+ loadKickConfig,
1060
+ pluralize,
1061
+ toCamelCase,
1062
+ toKebabCase,
1063
+ toPascalCase
1064
+ };
1065
+ //# sourceMappingURL=index.js.map