@forinda/kickjs-cli 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 join14 } from "path";
8
+ import { dirname as dirname3, join as join16 } from "path";
9
9
  import { fileURLToPath as fileURLToPath2 } from "url";
10
10
 
11
11
  // src/commands/init.ts
@@ -296,7 +296,7 @@ function getEntryFile(name, template) {
296
296
  case "graphql":
297
297
  return `import 'reflect-metadata'
298
298
  import { bootstrap } from '@forinda/kickjs-http'
299
- import { DevToolsAdapter } from '@forinda/kickjs-http/devtools'
299
+ import { DevToolsAdapter } from '@forinda/kickjs-devtools'
300
300
  import { GraphQLAdapter } from '@forinda/kickjs-graphql'
301
301
  import { modules } from './modules'
302
302
 
@@ -318,7 +318,7 @@ bootstrap({
318
318
  case "microservice":
319
319
  return `import 'reflect-metadata'
320
320
  import { bootstrap } from '@forinda/kickjs-http'
321
- import { DevToolsAdapter } from '@forinda/kickjs-http/devtools'
321
+ import { DevToolsAdapter } from '@forinda/kickjs-devtools'
322
322
  import { SwaggerAdapter } from '@forinda/kickjs-swagger'
323
323
  import { OtelAdapter } from '@forinda/kickjs-otel'
324
324
  // import { QueueAdapter, BullMQProvider } from '@forinda/kickjs-queue'
@@ -351,7 +351,7 @@ bootstrap({ modules })
351
351
  default:
352
352
  return `import 'reflect-metadata'
353
353
  import { bootstrap } from '@forinda/kickjs-http'
354
- import { DevToolsAdapter } from '@forinda/kickjs-http/devtools'
354
+ import { DevToolsAdapter } from '@forinda/kickjs-devtools'
355
355
  import { SwaggerAdapter } from '@forinda/kickjs-swagger'
356
356
  import { modules } from './modules'
357
357
 
@@ -1528,10 +1528,10 @@ async function confirm2(message) {
1528
1528
  input: process.stdin,
1529
1529
  output: process.stdout
1530
1530
  });
1531
- return new Promise((resolve5) => {
1531
+ return new Promise((resolve6) => {
1532
1532
  rl.question(` ${message} (y/N) `, (answer) => {
1533
1533
  rl.close();
1534
- resolve5(answer.trim().toLowerCase() === "y");
1534
+ resolve6(answer.trim().toLowerCase() === "y");
1535
1535
  });
1536
1536
  });
1537
1537
  }
@@ -1714,6 +1714,543 @@ export class ${pascal}Job {
1714
1714
  }
1715
1715
  __name(generateJob, "generateJob");
1716
1716
 
1717
+ // src/generators/scaffold.ts
1718
+ import { join as join12 } from "path";
1719
+ import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
1720
+ var TYPE_MAP = {
1721
+ string: {
1722
+ ts: "string",
1723
+ zod: "z.string()"
1724
+ },
1725
+ text: {
1726
+ ts: "string",
1727
+ zod: "z.string()"
1728
+ },
1729
+ number: {
1730
+ ts: "number",
1731
+ zod: "z.number()"
1732
+ },
1733
+ int: {
1734
+ ts: "number",
1735
+ zod: "z.number().int()"
1736
+ },
1737
+ float: {
1738
+ ts: "number",
1739
+ zod: "z.number()"
1740
+ },
1741
+ boolean: {
1742
+ ts: "boolean",
1743
+ zod: "z.boolean()"
1744
+ },
1745
+ date: {
1746
+ ts: "string",
1747
+ zod: "z.string().datetime()"
1748
+ },
1749
+ email: {
1750
+ ts: "string",
1751
+ zod: "z.string().email()"
1752
+ },
1753
+ url: {
1754
+ ts: "string",
1755
+ zod: "z.string().url()"
1756
+ },
1757
+ uuid: {
1758
+ ts: "string",
1759
+ zod: "z.string().uuid()"
1760
+ },
1761
+ json: {
1762
+ ts: "any",
1763
+ zod: "z.any()"
1764
+ }
1765
+ };
1766
+ function parseFields(raw) {
1767
+ return raw.map((f) => {
1768
+ const colonIdx = f.indexOf(":");
1769
+ if (colonIdx === -1) {
1770
+ throw new Error(`Invalid field: "${f}". Use format: name:type (e.g. title:string)`);
1771
+ }
1772
+ const namePart = f.slice(0, colonIdx);
1773
+ const typePart = f.slice(colonIdx + 1);
1774
+ if (!namePart || !typePart) {
1775
+ throw new Error(`Invalid field: "${f}". Use format: name:type (e.g. title:string)`);
1776
+ }
1777
+ const optional = typePart.endsWith("?");
1778
+ const cleanType = optional ? typePart.slice(0, -1) : typePart;
1779
+ if (cleanType.startsWith("enum:")) {
1780
+ const values = cleanType.slice(5).split(",");
1781
+ return {
1782
+ name: namePart,
1783
+ type: "enum",
1784
+ tsType: values.map((v) => `'${v}'`).join(" | "),
1785
+ zodType: `z.enum([${values.map((v) => `'${v}'`).join(", ")}])`,
1786
+ optional
1787
+ };
1788
+ }
1789
+ const mapped = TYPE_MAP[cleanType];
1790
+ if (!mapped) {
1791
+ const validTypes = [
1792
+ ...Object.keys(TYPE_MAP),
1793
+ "enum:a,b,c"
1794
+ ].join(", ");
1795
+ throw new Error(`Unknown field type: "${cleanType}". Valid types: ${validTypes}`);
1796
+ }
1797
+ return {
1798
+ name: namePart,
1799
+ type: cleanType,
1800
+ tsType: mapped.ts,
1801
+ zodType: mapped.zod,
1802
+ optional
1803
+ };
1804
+ });
1805
+ }
1806
+ __name(parseFields, "parseFields");
1807
+ async function generateScaffold(options) {
1808
+ const { name, fields, modulesDir, noEntity, noTests, repo = "inmemory" } = options;
1809
+ const kebab = toKebabCase(name);
1810
+ const pascal = toPascalCase(name);
1811
+ const camel = toCamelCase(name);
1812
+ const plural = pluralize(kebab);
1813
+ const pluralPascal = pluralizePascal(pascal);
1814
+ const moduleDir = join12(modulesDir, plural);
1815
+ const files = [];
1816
+ const write = /* @__PURE__ */ __name(async (relativePath, content) => {
1817
+ const fullPath = join12(moduleDir, relativePath);
1818
+ await writeFileSafe(fullPath, content);
1819
+ files.push(fullPath);
1820
+ }, "write");
1821
+ await write("index.ts", genModuleIndex(pascal, kebab, plural, repo));
1822
+ await write("constants.ts", genConstants(pascal, fields));
1823
+ await write(`presentation/${kebab}.controller.ts`, genController(pascal, kebab, plural, pluralPascal));
1824
+ await write(`application/dtos/create-${kebab}.dto.ts`, genCreateDTO(pascal, fields));
1825
+ await write(`application/dtos/update-${kebab}.dto.ts`, genUpdateDTO(pascal, fields));
1826
+ await write(`application/dtos/${kebab}-response.dto.ts`, genResponseDTO(pascal, fields));
1827
+ const useCases = genUseCases(pascal, kebab, plural, pluralPascal);
1828
+ for (const uc of useCases) {
1829
+ await write(`application/use-cases/${uc.file}`, uc.content);
1830
+ }
1831
+ await write(`domain/repositories/${kebab}.repository.ts`, genRepositoryInterface(pascal, kebab));
1832
+ await write(`domain/services/${kebab}-domain.service.ts`, genDomainService(pascal, kebab));
1833
+ if (repo === "inmemory") {
1834
+ await write(`infrastructure/repositories/in-memory-${kebab}.repository.ts`, genInMemoryRepository(pascal, kebab, fields));
1835
+ }
1836
+ if (!noEntity) {
1837
+ await write(`domain/entities/${kebab}.entity.ts`, genEntity(pascal, kebab, fields));
1838
+ await write(`domain/value-objects/${kebab}-id.vo.ts`, genValueObject(pascal));
1839
+ }
1840
+ await autoRegisterModule2(modulesDir, pascal, plural);
1841
+ return files;
1842
+ }
1843
+ __name(generateScaffold, "generateScaffold");
1844
+ function genCreateDTO(pascal, fields) {
1845
+ const zodFields = fields.map((f) => {
1846
+ const base = f.zodType;
1847
+ return ` ${f.name}: ${base}${f.optional ? ".optional()" : ""},`;
1848
+ }).join("\n");
1849
+ return `import { z } from 'zod'
1850
+
1851
+ export const create${pascal}Schema = z.object({
1852
+ ${zodFields}
1853
+ })
1854
+
1855
+ export type Create${pascal}DTO = z.infer<typeof create${pascal}Schema>
1856
+ `;
1857
+ }
1858
+ __name(genCreateDTO, "genCreateDTO");
1859
+ function genUpdateDTO(pascal, fields) {
1860
+ const zodFields = fields.map((f) => ` ${f.name}: ${f.zodType}.optional(),`).join("\n");
1861
+ return `import { z } from 'zod'
1862
+
1863
+ export const update${pascal}Schema = z.object({
1864
+ ${zodFields}
1865
+ })
1866
+
1867
+ export type Update${pascal}DTO = z.infer<typeof update${pascal}Schema>
1868
+ `;
1869
+ }
1870
+ __name(genUpdateDTO, "genUpdateDTO");
1871
+ function genResponseDTO(pascal, fields) {
1872
+ const tsFields = fields.map((f) => ` ${f.name}${f.optional ? "?" : ""}: ${f.tsType}`).join("\n");
1873
+ return `export interface ${pascal}ResponseDTO {
1874
+ id: string
1875
+ ${tsFields}
1876
+ createdAt: string
1877
+ updatedAt: string
1878
+ }
1879
+ `;
1880
+ }
1881
+ __name(genResponseDTO, "genResponseDTO");
1882
+ function genConstants(pascal, fields) {
1883
+ const stringFields = fields.filter((f) => f.tsType === "string").map((f) => `'${f.name}'`);
1884
+ const numberFields = fields.filter((f) => f.tsType === "number").map((f) => `'${f.name}'`);
1885
+ const allFieldNames = fields.map((f) => `'${f.name}'`);
1886
+ const filterable = [
1887
+ ...allFieldNames
1888
+ ].join(", ");
1889
+ const sortable = [
1890
+ ...allFieldNames,
1891
+ "'createdAt'",
1892
+ "'updatedAt'"
1893
+ ].join(", ");
1894
+ const searchable = stringFields.length > 0 ? stringFields.join(", ") : "'name'";
1895
+ return `import type { ApiQueryParamsConfig } from '@forinda/kickjs-core'
1896
+
1897
+ export const ${pascal.toUpperCase()}_QUERY_CONFIG: ApiQueryParamsConfig = {
1898
+ filterable: [${filterable}],
1899
+ sortable: [${sortable}],
1900
+ searchable: [${searchable}],
1901
+ }
1902
+ `;
1903
+ }
1904
+ __name(genConstants, "genConstants");
1905
+ function genInMemoryRepository(pascal, kebab, fields) {
1906
+ const fieldAssignments = fields.map((f) => ` ${f.name}: dto.${f.name},`).join("\n");
1907
+ const fieldSpread = "...dto";
1908
+ return `import { randomUUID } from 'node:crypto'
1909
+ import { Repository, HttpException } from '@forinda/kickjs-core'
1910
+ import type { ParsedQuery } from '@forinda/kickjs-http'
1911
+ import type { I${pascal}Repository } from '../../domain/repositories/${kebab}.repository'
1912
+ import type { ${pascal}ResponseDTO } from '../../application/dtos/${kebab}-response.dto'
1913
+ import type { Create${pascal}DTO } from '../../application/dtos/create-${kebab}.dto'
1914
+ import type { Update${pascal}DTO } from '../../application/dtos/update-${kebab}.dto'
1915
+
1916
+ @Repository()
1917
+ export class InMemory${pascal}Repository implements I${pascal}Repository {
1918
+ private store = new Map<string, ${pascal}ResponseDTO>()
1919
+
1920
+ async findById(id: string): Promise<${pascal}ResponseDTO | null> {
1921
+ return this.store.get(id) ?? null
1922
+ }
1923
+
1924
+ async findAll(): Promise<${pascal}ResponseDTO[]> {
1925
+ return Array.from(this.store.values())
1926
+ }
1927
+
1928
+ async findPaginated(parsed: ParsedQuery): Promise<{ data: ${pascal}ResponseDTO[]; total: number }> {
1929
+ const all = Array.from(this.store.values())
1930
+ const data = all.slice(parsed.pagination.offset, parsed.pagination.offset + parsed.pagination.limit)
1931
+ return { data, total: all.length }
1932
+ }
1933
+
1934
+ async create(dto: Create${pascal}DTO): Promise<${pascal}ResponseDTO> {
1935
+ const now = new Date().toISOString()
1936
+ const entity: ${pascal}ResponseDTO = {
1937
+ id: randomUUID(),
1938
+ ${fieldAssignments}
1939
+ createdAt: now,
1940
+ updatedAt: now,
1941
+ }
1942
+ this.store.set(entity.id, entity)
1943
+ return entity
1944
+ }
1945
+
1946
+ async update(id: string, dto: Update${pascal}DTO): Promise<${pascal}ResponseDTO> {
1947
+ const existing = this.store.get(id)
1948
+ if (!existing) throw HttpException.notFound('${pascal} not found')
1949
+ const updated = { ...existing, ${fieldSpread}, updatedAt: new Date().toISOString() }
1950
+ this.store.set(id, updated)
1951
+ return updated
1952
+ }
1953
+
1954
+ async delete(id: string): Promise<void> {
1955
+ if (!this.store.has(id)) throw HttpException.notFound('${pascal} not found')
1956
+ this.store.delete(id)
1957
+ }
1958
+ }
1959
+ `;
1960
+ }
1961
+ __name(genInMemoryRepository, "genInMemoryRepository");
1962
+ function genEntity(pascal, kebab, fields) {
1963
+ const propsInterface = fields.map((f) => ` ${f.name}${f.optional ? "?" : ""}: ${f.tsType}`).join("\n");
1964
+ const createParams = fields.filter((f) => !f.optional).map((f) => `${f.name}: ${f.tsType}`).join("; ");
1965
+ const createAssignments = fields.filter((f) => !f.optional).map((f) => ` ${f.name}: params.${f.name},`).join("\n");
1966
+ const getters = fields.map((f) => ` get ${f.name}(): ${f.tsType}${f.optional ? " | undefined" : ""} {
1967
+ return this.props.${f.name}
1968
+ }`).join("\n");
1969
+ const toJsonFields = fields.map((f) => ` ${f.name}: this.props.${f.name},`).join("\n");
1970
+ return `import { ${pascal}Id } from '../value-objects/${kebab}-id.vo'
1971
+
1972
+ interface ${pascal}Props {
1973
+ id: ${pascal}Id
1974
+ ${propsInterface}
1975
+ createdAt: Date
1976
+ updatedAt: Date
1977
+ }
1978
+
1979
+ export class ${pascal} {
1980
+ private constructor(private props: ${pascal}Props) {}
1981
+
1982
+ static create(params: { ${createParams} }): ${pascal} {
1983
+ const now = new Date()
1984
+ return new ${pascal}({
1985
+ id: ${pascal}Id.create(),
1986
+ ${createAssignments}
1987
+ createdAt: now,
1988
+ updatedAt: now,
1989
+ })
1990
+ }
1991
+
1992
+ static reconstitute(props: ${pascal}Props): ${pascal} {
1993
+ return new ${pascal}(props)
1994
+ }
1995
+
1996
+ get id(): ${pascal}Id { return this.props.id }
1997
+ ${getters}
1998
+ get createdAt(): Date { return this.props.createdAt }
1999
+ get updatedAt(): Date { return this.props.updatedAt }
2000
+
2001
+ toJSON() {
2002
+ return {
2003
+ id: this.props.id.toString(),
2004
+ ${toJsonFields}
2005
+ createdAt: this.props.createdAt.toISOString(),
2006
+ updatedAt: this.props.updatedAt.toISOString(),
2007
+ }
2008
+ }
2009
+ }
2010
+ `;
2011
+ }
2012
+ __name(genEntity, "genEntity");
2013
+ function genValueObject(pascal) {
2014
+ return `import { randomUUID } from 'node:crypto'
2015
+
2016
+ export class ${pascal}Id {
2017
+ private constructor(private readonly value: string) {}
2018
+
2019
+ static create(): ${pascal}Id { return new ${pascal}Id(randomUUID()) }
2020
+
2021
+ static from(id: string): ${pascal}Id {
2022
+ if (!id || id.trim().length === 0) throw new Error('${pascal}Id cannot be empty')
2023
+ return new ${pascal}Id(id)
2024
+ }
2025
+
2026
+ toString(): string { return this.value }
2027
+ equals(other: ${pascal}Id): boolean { return this.value === other.value }
2028
+ }
2029
+ `;
2030
+ }
2031
+ __name(genValueObject, "genValueObject");
2032
+ function genModuleIndex(pascal, kebab, plural, repo) {
2033
+ return `import type { AppModule, AppModuleClass } from '@forinda/kickjs-core'
2034
+ import { ${pascal}Controller } from './presentation/${kebab}.controller'
2035
+ import { ${pascal}DomainService } from './domain/services/${kebab}-domain.service'
2036
+ import { ${pascal.toUpperCase()}_REPOSITORY } from './domain/repositories/${kebab}.repository'
2037
+ import { InMemory${pascal}Repository } from './infrastructure/repositories/in-memory-${kebab}.repository'
2038
+
2039
+ export class ${pascal}Module implements AppModule {
2040
+ register(container: any): void {
2041
+ container.registerFactory(
2042
+ ${pascal.toUpperCase()}_REPOSITORY,
2043
+ () => container.resolve(InMemory${pascal}Repository),
2044
+ )
2045
+ }
2046
+
2047
+ routes() {
2048
+ return { prefix: '/${plural}', controllers: [${pascal}Controller] }
2049
+ }
2050
+ }
2051
+ `;
2052
+ }
2053
+ __name(genModuleIndex, "genModuleIndex");
2054
+ function genController(pascal, kebab, plural, pluralPascal) {
2055
+ return `import { Controller, Get, Post, Put, Delete, Autowired, ApiQueryParams } from '@forinda/kickjs-core'
2056
+ import type { RequestContext } from '@forinda/kickjs-http'
2057
+ import { ApiTags } from '@forinda/kickjs-swagger'
2058
+ import { Create${pascal}UseCase } from '../application/use-cases/create-${kebab}.use-case'
2059
+ import { Get${pascal}UseCase } from '../application/use-cases/get-${kebab}.use-case'
2060
+ import { List${pluralPascal}UseCase } from '../application/use-cases/list-${plural}.use-case'
2061
+ import { Update${pascal}UseCase } from '../application/use-cases/update-${kebab}.use-case'
2062
+ import { Delete${pascal}UseCase } from '../application/use-cases/delete-${kebab}.use-case'
2063
+ import { create${pascal}Schema } from '../application/dtos/create-${kebab}.dto'
2064
+ import { update${pascal}Schema } from '../application/dtos/update-${kebab}.dto'
2065
+ import { ${pascal.toUpperCase()}_QUERY_CONFIG } from '../constants'
2066
+
2067
+ @Controller()
2068
+ export class ${pascal}Controller {
2069
+ @Autowired() private create${pascal}UseCase!: Create${pascal}UseCase
2070
+ @Autowired() private get${pascal}UseCase!: Get${pascal}UseCase
2071
+ @Autowired() private list${pluralPascal}UseCase!: List${pluralPascal}UseCase
2072
+ @Autowired() private update${pascal}UseCase!: Update${pascal}UseCase
2073
+ @Autowired() private delete${pascal}UseCase!: Delete${pascal}UseCase
2074
+
2075
+ @Get('/')
2076
+ @ApiTags('${pascal}')
2077
+ @ApiQueryParams(${pascal.toUpperCase()}_QUERY_CONFIG)
2078
+ async list(ctx: RequestContext) {
2079
+ return ctx.paginate(
2080
+ (parsed) => this.list${pluralPascal}UseCase.execute(parsed),
2081
+ ${pascal.toUpperCase()}_QUERY_CONFIG,
2082
+ )
2083
+ }
2084
+
2085
+ @Get('/:id')
2086
+ @ApiTags('${pascal}')
2087
+ async getById(ctx: RequestContext) {
2088
+ const result = await this.get${pascal}UseCase.execute(ctx.params.id)
2089
+ if (!result) return ctx.notFound('${pascal} not found')
2090
+ ctx.json(result)
2091
+ }
2092
+
2093
+ @Post('/', { body: create${pascal}Schema, name: 'Create${pascal}' })
2094
+ @ApiTags('${pascal}')
2095
+ async create(ctx: RequestContext) {
2096
+ const result = await this.create${pascal}UseCase.execute(ctx.body)
2097
+ ctx.created(result)
2098
+ }
2099
+
2100
+ @Put('/:id', { body: update${pascal}Schema, name: 'Update${pascal}' })
2101
+ @ApiTags('${pascal}')
2102
+ async update(ctx: RequestContext) {
2103
+ const result = await this.update${pascal}UseCase.execute(ctx.params.id, ctx.body)
2104
+ ctx.json(result)
2105
+ }
2106
+
2107
+ @Delete('/:id')
2108
+ @ApiTags('${pascal}')
2109
+ async remove(ctx: RequestContext) {
2110
+ await this.delete${pascal}UseCase.execute(ctx.params.id)
2111
+ ctx.noContent()
2112
+ }
2113
+ }
2114
+ `;
2115
+ }
2116
+ __name(genController, "genController");
2117
+ function genRepositoryInterface(pascal, kebab) {
2118
+ return `import type { ${pascal}ResponseDTO } from '../../application/dtos/${kebab}-response.dto'
2119
+ import type { Create${pascal}DTO } from '../../application/dtos/create-${kebab}.dto'
2120
+ import type { Update${pascal}DTO } from '../../application/dtos/update-${kebab}.dto'
2121
+ import type { ParsedQuery } from '@forinda/kickjs-http'
2122
+
2123
+ export interface I${pascal}Repository {
2124
+ findById(id: string): Promise<${pascal}ResponseDTO | null>
2125
+ findAll(): Promise<${pascal}ResponseDTO[]>
2126
+ findPaginated(parsed: ParsedQuery): Promise<{ data: ${pascal}ResponseDTO[]; total: number }>
2127
+ create(dto: Create${pascal}DTO): Promise<${pascal}ResponseDTO>
2128
+ update(id: string, dto: Update${pascal}DTO): Promise<${pascal}ResponseDTO>
2129
+ delete(id: string): Promise<void>
2130
+ }
2131
+
2132
+ export const ${pascal.toUpperCase()}_REPOSITORY = Symbol('I${pascal}Repository')
2133
+ `;
2134
+ }
2135
+ __name(genRepositoryInterface, "genRepositoryInterface");
2136
+ function genDomainService(pascal, kebab) {
2137
+ return `import { Service, Inject, HttpException } from '@forinda/kickjs-core'
2138
+ import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../repositories/${kebab}.repository'
2139
+
2140
+ @Service()
2141
+ export class ${pascal}DomainService {
2142
+ constructor(
2143
+ @Inject(${pascal.toUpperCase()}_REPOSITORY) private readonly repo: I${pascal}Repository,
2144
+ ) {}
2145
+
2146
+ async ensureExists(id: string): Promise<void> {
2147
+ const entity = await this.repo.findById(id)
2148
+ if (!entity) throw HttpException.notFound('${pascal} not found')
2149
+ }
2150
+ }
2151
+ `;
2152
+ }
2153
+ __name(genDomainService, "genDomainService");
2154
+ function genUseCases(pascal, kebab, plural, pluralPascal) {
2155
+ return [
2156
+ {
2157
+ file: `create-${kebab}.use-case.ts`,
2158
+ content: `import { Service, Inject } from '@forinda/kickjs-core'
2159
+ import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../../domain/repositories/${kebab}.repository'
2160
+ import type { Create${pascal}DTO } from '../dtos/create-${kebab}.dto'
2161
+
2162
+ @Service()
2163
+ export class Create${pascal}UseCase {
2164
+ constructor(@Inject(${pascal.toUpperCase()}_REPOSITORY) private repo: I${pascal}Repository) {}
2165
+ async execute(dto: Create${pascal}DTO) { return this.repo.create(dto) }
2166
+ }
2167
+ `
2168
+ },
2169
+ {
2170
+ file: `get-${kebab}.use-case.ts`,
2171
+ content: `import { Service, Inject } from '@forinda/kickjs-core'
2172
+ import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../../domain/repositories/${kebab}.repository'
2173
+
2174
+ @Service()
2175
+ export class Get${pascal}UseCase {
2176
+ constructor(@Inject(${pascal.toUpperCase()}_REPOSITORY) private repo: I${pascal}Repository) {}
2177
+ async execute(id: string) { return this.repo.findById(id) }
2178
+ }
2179
+ `
2180
+ },
2181
+ {
2182
+ file: `list-${plural}.use-case.ts`,
2183
+ content: `import { Service, Inject } from '@forinda/kickjs-core'
2184
+ import type { ParsedQuery } from '@forinda/kickjs-http'
2185
+ import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../../domain/repositories/${kebab}.repository'
2186
+
2187
+ @Service()
2188
+ export class List${pluralPascal}UseCase {
2189
+ constructor(@Inject(${pascal.toUpperCase()}_REPOSITORY) private repo: I${pascal}Repository) {}
2190
+ async execute(parsed: ParsedQuery) { return this.repo.findPaginated(parsed) }
2191
+ }
2192
+ `
2193
+ },
2194
+ {
2195
+ file: `update-${kebab}.use-case.ts`,
2196
+ content: `import { Service, Inject } from '@forinda/kickjs-core'
2197
+ import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../../domain/repositories/${kebab}.repository'
2198
+ import type { Update${pascal}DTO } from '../dtos/update-${kebab}.dto'
2199
+
2200
+ @Service()
2201
+ export class Update${pascal}UseCase {
2202
+ constructor(@Inject(${pascal.toUpperCase()}_REPOSITORY) private repo: I${pascal}Repository) {}
2203
+ async execute(id: string, dto: Update${pascal}DTO) { return this.repo.update(id, dto) }
2204
+ }
2205
+ `
2206
+ },
2207
+ {
2208
+ file: `delete-${kebab}.use-case.ts`,
2209
+ content: `import { Service, Inject } from '@forinda/kickjs-core'
2210
+ import { ${pascal.toUpperCase()}_REPOSITORY, type I${pascal}Repository } from '../../domain/repositories/${kebab}.repository'
2211
+
2212
+ @Service()
2213
+ export class Delete${pascal}UseCase {
2214
+ constructor(@Inject(${pascal.toUpperCase()}_REPOSITORY) private repo: I${pascal}Repository) {}
2215
+ async execute(id: string) { return this.repo.delete(id) }
2216
+ }
2217
+ `
2218
+ }
2219
+ ];
2220
+ }
2221
+ __name(genUseCases, "genUseCases");
2222
+ async function autoRegisterModule2(modulesDir, pascal, plural) {
2223
+ const indexPath = join12(modulesDir, "index.ts");
2224
+ const exists = await fileExists(indexPath);
2225
+ if (!exists) {
2226
+ await writeFileSafe(indexPath, `import type { AppModuleClass } from '@forinda/kickjs-core'
2227
+ import { ${pascal}Module } from './${plural}'
2228
+
2229
+ export const modules: AppModuleClass[] = [${pascal}Module]
2230
+ `);
2231
+ return;
2232
+ }
2233
+ let content = await readFile3(indexPath, "utf-8");
2234
+ const importLine = `import { ${pascal}Module } from './${plural}'`;
2235
+ if (!content.includes(`${pascal}Module`)) {
2236
+ const lastImportIdx = content.lastIndexOf("import ");
2237
+ if (lastImportIdx !== -1) {
2238
+ const lineEnd = content.indexOf("\n", lastImportIdx);
2239
+ content = content.slice(0, lineEnd + 1) + importLine + "\n" + content.slice(lineEnd + 1);
2240
+ } else {
2241
+ content = importLine + "\n" + content;
2242
+ }
2243
+ content = content.replace(/(=\s*\[)([\s\S]*?)(])/, (_match, open, existing, close) => {
2244
+ const trimmed = existing.trim();
2245
+ if (!trimmed) return `${open}${pascal}Module${close}`;
2246
+ const needsComma = trimmed.endsWith(",") ? "" : ",";
2247
+ return `${open}${existing.trimEnd()}${needsComma} ${pascal}Module${close}`;
2248
+ });
2249
+ }
2250
+ await writeFile3(indexPath, content, "utf-8");
2251
+ }
2252
+ __name(autoRegisterModule2, "autoRegisterModule");
2253
+
1717
2254
  // src/commands/generate.ts
1718
2255
  function printGenerated(files) {
1719
2256
  const cwd = process.cwd();
@@ -1795,6 +2332,26 @@ function registerGenerateCommand(program) {
1795
2332
  });
1796
2333
  printGenerated(files);
1797
2334
  });
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) => {
2336
+ if (rawFields.length === 0) {
2337
+ 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
+ process.exit(1);
2339
+ }
2340
+ const fields = parseFields(rawFields);
2341
+ const files = await generateScaffold({
2342
+ name,
2343
+ fields,
2344
+ modulesDir: resolve2(opts.modulesDir),
2345
+ noEntity: opts.entity === false,
2346
+ noTests: opts.tests === false
2347
+ });
2348
+ console.log(`
2349
+ Scaffolded ${name} with ${fields.length} field(s):`);
2350
+ for (const f of fields) {
2351
+ console.log(` ${f.name}: ${f.type}${f.optional ? " (optional)" : ""}`);
2352
+ }
2353
+ printGenerated(files);
2354
+ });
1798
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) => {
1799
2356
  const files = await generateConfig({
1800
2357
  outDir: resolve2("."),
@@ -1809,7 +2366,7 @@ __name(registerGenerateCommand, "registerGenerateCommand");
1809
2366
 
1810
2367
  // src/commands/run.ts
1811
2368
  import { cpSync, existsSync as existsSync3, mkdirSync } from "fs";
1812
- import { resolve as resolve3, join as join13 } from "path";
2369
+ import { resolve as resolve3, join as join14 } from "path";
1813
2370
 
1814
2371
  // src/utils/shell.ts
1815
2372
  import { execSync as execSync2 } from "child_process";
@@ -1822,8 +2379,8 @@ function runShellCommand(command, cwd) {
1822
2379
  __name(runShellCommand, "runShellCommand");
1823
2380
 
1824
2381
  // src/config.ts
1825
- import { readFile as readFile3, access as access2 } from "fs/promises";
1826
- import { join as join12 } from "path";
2382
+ import { readFile as readFile4, access as access2 } from "fs/promises";
2383
+ import { join as join13 } from "path";
1827
2384
  var CONFIG_FILES = [
1828
2385
  "kick.config.ts",
1829
2386
  "kick.config.js",
@@ -1832,19 +2389,19 @@ var CONFIG_FILES = [
1832
2389
  ];
1833
2390
  async function loadKickConfig(cwd) {
1834
2391
  for (const filename of CONFIG_FILES) {
1835
- const filepath = join12(cwd, filename);
2392
+ const filepath = join13(cwd, filename);
1836
2393
  try {
1837
2394
  await access2(filepath);
1838
2395
  } catch {
1839
2396
  continue;
1840
2397
  }
1841
2398
  if (filename.endsWith(".json")) {
1842
- const content = await readFile3(filepath, "utf-8");
2399
+ const content = await readFile4(filepath, "utf-8");
1843
2400
  return JSON.parse(content);
1844
2401
  }
1845
2402
  try {
1846
- const { pathToFileURL } = await import("url");
1847
- const mod = await import(pathToFileURL(filepath).href);
2403
+ const { pathToFileURL: pathToFileURL2 } = await import("url");
2404
+ const mod = await import(pathToFileURL2(filepath).href);
1848
2405
  return mod.default ?? mod;
1849
2406
  } catch (err) {
1850
2407
  if (filename.endsWith(".ts")) {
@@ -1883,7 +2440,7 @@ function registerRunCommands(program) {
1883
2440
  console.log("\n Copying directories to dist...");
1884
2441
  for (const entry of copyDirs) {
1885
2442
  const src = typeof entry === "string" ? entry : entry.src;
1886
- const dest = typeof entry === "string" ? join13("dist", entry) : entry.dest ?? join13("dist", src);
2443
+ const dest = typeof entry === "string" ? join14("dist", entry) : entry.dest ?? join14("dist", src);
1887
2444
  const srcPath = resolve3(src);
1888
2445
  const destPath = resolve3(dest);
1889
2446
  if (!existsSync3(srcPath)) {
@@ -2157,7 +2714,8 @@ var PACKAGE_REGISTRY = {
2157
2714
  cli: {
2158
2715
  pkg: "@forinda/kickjs-cli",
2159
2716
  peers: [],
2160
- description: "CLI tool and code generators"
2717
+ description: "CLI tool and code generators",
2718
+ dev: true
2161
2719
  },
2162
2720
  // API
2163
2721
  swagger: {
@@ -2203,6 +2761,37 @@ var PACKAGE_REGISTRY = {
2203
2761
  ],
2204
2762
  description: "OpenTelemetry tracing + metrics"
2205
2763
  },
2764
+ // DevTools
2765
+ devtools: {
2766
+ pkg: "@forinda/kickjs-devtools",
2767
+ peers: [],
2768
+ description: "Development dashboard \u2014 routes, DI, metrics, health",
2769
+ dev: true
2770
+ },
2771
+ // Auth
2772
+ auth: {
2773
+ pkg: "@forinda/kickjs-auth",
2774
+ peers: [
2775
+ "jsonwebtoken"
2776
+ ],
2777
+ description: "Authentication \u2014 JWT, API key, and custom strategies"
2778
+ },
2779
+ // Mailer
2780
+ mailer: {
2781
+ pkg: "@forinda/kickjs-mailer",
2782
+ peers: [
2783
+ "nodemailer"
2784
+ ],
2785
+ description: "Email sending \u2014 SMTP, Resend, SES, or custom provider"
2786
+ },
2787
+ // Cron
2788
+ cron: {
2789
+ pkg: "@forinda/kickjs-cron",
2790
+ peers: [
2791
+ "croner"
2792
+ ],
2793
+ description: "Cron job scheduling (production-grade with croner)"
2794
+ },
2206
2795
  // Queue
2207
2796
  queue: {
2208
2797
  pkg: "@forinda/kickjs-queue",
@@ -2237,11 +2826,18 @@ var PACKAGE_REGISTRY = {
2237
2826
  peers: [],
2238
2827
  description: "Tenant resolution middleware"
2239
2828
  },
2829
+ // Notifications
2830
+ notifications: {
2831
+ pkg: "@forinda/kickjs-notifications",
2832
+ peers: [],
2833
+ description: "Multi-channel notifications \u2014 email, Slack, Discord, webhook"
2834
+ },
2240
2835
  // Testing
2241
2836
  testing: {
2242
2837
  pkg: "@forinda/kickjs-testing",
2243
2838
  peers: [],
2244
- description: "Test utilities and TestModule builder"
2839
+ description: "Test utilities and TestModule builder",
2840
+ dev: true
2245
2841
  }
2246
2842
  };
2247
2843
  function detectPackageManager() {
@@ -2266,8 +2862,9 @@ function registerAddCommand(program) {
2266
2862
  return;
2267
2863
  }
2268
2864
  const pm = opts.pm ?? detectPackageManager();
2269
- const devFlag = opts.dev ? " -D" : "";
2270
- const allDeps = /* @__PURE__ */ new Set();
2865
+ const forceDevFlag = opts.dev;
2866
+ const prodDeps = /* @__PURE__ */ new Set();
2867
+ const devDeps = /* @__PURE__ */ new Set();
2271
2868
  const unknown = [];
2272
2869
  for (const name of packages) {
2273
2870
  const entry = PACKAGE_REGISTRY[name];
@@ -2275,43 +2872,176 @@ function registerAddCommand(program) {
2275
2872
  unknown.push(name);
2276
2873
  continue;
2277
2874
  }
2278
- allDeps.add(entry.pkg);
2875
+ const target = forceDevFlag || entry.dev ? devDeps : prodDeps;
2876
+ target.add(entry.pkg);
2279
2877
  for (const peer of entry.peers) {
2280
- allDeps.add(peer);
2878
+ target.add(peer);
2281
2879
  }
2282
2880
  }
2283
2881
  if (unknown.length > 0) {
2284
2882
  console.log(`
2285
2883
  Unknown packages: ${unknown.join(", ")}`);
2286
2884
  console.log(' Run "kick add --list" to see available packages.\n');
2287
- if (allDeps.size === 0) return;
2885
+ if (prodDeps.size === 0 && devDeps.size === 0) return;
2288
2886
  }
2289
- const depsArray = Array.from(allDeps);
2290
- const installCmd = `${pm} add${devFlag} ${depsArray.join(" ")}`;
2291
- console.log(`
2292
- Installing ${depsArray.length} package(s):`);
2293
- for (const dep of depsArray) {
2294
- console.log(` + ${dep}`);
2887
+ if (prodDeps.size > 0) {
2888
+ const deps = Array.from(prodDeps);
2889
+ const cmd = `${pm} add ${deps.join(" ")}`;
2890
+ console.log(`
2891
+ Installing ${deps.length} dependency(ies):`);
2892
+ for (const dep of deps) console.log(` + ${dep}`);
2893
+ console.log();
2894
+ try {
2895
+ execSync3(cmd, {
2896
+ stdio: "inherit"
2897
+ });
2898
+ } catch {
2899
+ console.log(`
2900
+ Installation failed. Run manually:
2901
+ ${cmd}
2902
+ `);
2903
+ }
2295
2904
  }
2296
- console.log();
2297
- try {
2298
- execSync3(installCmd, {
2299
- stdio: "inherit"
2300
- });
2301
- console.log("\n Done!\n");
2302
- } catch {
2905
+ if (devDeps.size > 0) {
2906
+ const deps = Array.from(devDeps);
2907
+ const cmd = `${pm} add -D ${deps.join(" ")}`;
2303
2908
  console.log(`
2909
+ Installing ${deps.length} dev dependency(ies):`);
2910
+ for (const dep of deps) console.log(` + ${dep} (dev)`);
2911
+ console.log();
2912
+ try {
2913
+ execSync3(cmd, {
2914
+ stdio: "inherit"
2915
+ });
2916
+ } catch {
2917
+ console.log(`
2304
2918
  Installation failed. Run manually:
2305
- ${installCmd}
2919
+ ${cmd}
2306
2920
  `);
2921
+ }
2307
2922
  }
2923
+ console.log(" Done!\n");
2308
2924
  });
2309
2925
  }
2310
2926
  __name(registerAddCommand, "registerAddCommand");
2311
2927
 
2928
+ // src/commands/tinker.ts
2929
+ import { resolve as resolve5, join as join15 } from "path";
2930
+ import { existsSync as existsSync5 } from "fs";
2931
+ import { pathToFileURL } from "url";
2932
+ import { fork } from "child_process";
2933
+ function registerTinkerCommand(program) {
2934
+ 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
+ const cwd = process.cwd();
2936
+ const entryPath = resolve5(cwd, opts.entry);
2937
+ if (!existsSync5(entryPath)) {
2938
+ console.error(`
2939
+ Error: ${opts.entry} not found.
2940
+ `);
2941
+ process.exit(1);
2942
+ }
2943
+ const tsxBin = findBin(cwd, "tsx");
2944
+ if (!tsxBin) {
2945
+ console.error("\n Error: tsx not found. Install it: pnpm add -D tsx\n");
2946
+ process.exit(1);
2947
+ }
2948
+ const tinkerScript = generateTinkerScript(entryPath, opts.entry);
2949
+ const tmpFile = join15(cwd, ".kick-tinker.mjs");
2950
+ const { writeFileSync, unlinkSync } = await import("fs");
2951
+ writeFileSync(tmpFile, tinkerScript, "utf-8");
2952
+ try {
2953
+ const child = fork(tmpFile, [], {
2954
+ cwd,
2955
+ execPath: tsxBin,
2956
+ stdio: "inherit"
2957
+ });
2958
+ await new Promise((resolve6) => {
2959
+ child.on("exit", () => resolve6());
2960
+ });
2961
+ } finally {
2962
+ try {
2963
+ unlinkSync(tmpFile);
2964
+ } catch {
2965
+ }
2966
+ }
2967
+ });
2968
+ }
2969
+ __name(registerTinkerCommand, "registerTinkerCommand");
2970
+ function generateTinkerScript(entryPath, displayPath) {
2971
+ const entryUrl = pathToFileURL(entryPath).href;
2972
+ return `
2973
+ import 'reflect-metadata'
2974
+
2975
+ // Prevent bootstrap() from starting the HTTP server
2976
+ process.env.KICK_TINKER = '1'
2977
+
2978
+ console.log('\\n \u{1F527} KickJS Tinker')
2979
+ console.log(' Loading: ${displayPath}\\n')
2980
+
2981
+ // Load core
2982
+ let Container, Logger, HttpException, HttpStatus
2983
+ try {
2984
+ const core = await import('@forinda/kickjs-core')
2985
+ Container = core.Container
2986
+ Logger = core.Logger
2987
+ HttpException = core.HttpException
2988
+ HttpStatus = core.HttpStatus
2989
+ } catch {
2990
+ console.error(' Error: @forinda/kickjs-core not found.')
2991
+ console.error(' Install it: pnpm add @forinda/kickjs-core\\n')
2992
+ process.exit(1)
2993
+ }
2994
+
2995
+ // Load entry to trigger decorator registration
2996
+ try {
2997
+ await import('${entryUrl}')
2998
+ } catch (err) {
2999
+ console.warn(' Warning: ' + err.message)
3000
+ console.warn(' Container may be partially initialized.\\n')
3001
+ }
3002
+
3003
+ const container = Container.getInstance()
3004
+
3005
+ // Start REPL
3006
+ const repl = await import('node:repl')
3007
+ const server = repl.start({ prompt: 'kick> ', useGlobal: true })
3008
+
3009
+ server.context.container = container
3010
+ server.context.Container = Container
3011
+ server.context.resolve = (token) => container.resolve(token)
3012
+ server.context.Logger = Logger
3013
+ server.context.HttpException = HttpException
3014
+ server.context.HttpStatus = HttpStatus
3015
+
3016
+ console.log(' Available globals:')
3017
+ console.log(' container \u2014 DI container instance')
3018
+ console.log(' resolve(T) \u2014 shorthand for container.resolve(T)')
3019
+ console.log(' Container, Logger, HttpException, HttpStatus')
3020
+ console.log()
3021
+
3022
+ server.on('exit', () => {
3023
+ console.log('\\n Goodbye!\\n')
3024
+ process.exit(0)
3025
+ })
3026
+ `;
3027
+ }
3028
+ __name(generateTinkerScript, "generateTinkerScript");
3029
+ function findBin(startDir, name) {
3030
+ let dir = startDir;
3031
+ while (true) {
3032
+ const candidate = join15(dir, "node_modules", ".bin", name);
3033
+ if (existsSync5(candidate)) return candidate;
3034
+ const parent = resolve5(dir, "..");
3035
+ if (parent === dir) break;
3036
+ dir = parent;
3037
+ }
3038
+ return null;
3039
+ }
3040
+ __name(findBin, "findBin");
3041
+
2312
3042
  // src/cli.ts
2313
3043
  var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
2314
- var pkg = JSON.parse(readFileSync2(join14(__dirname2, "..", "package.json"), "utf-8"));
3044
+ var pkg = JSON.parse(readFileSync2(join16(__dirname2, "..", "package.json"), "utf-8"));
2315
3045
  async function main() {
2316
3046
  const program = new Command();
2317
3047
  program.name("kick").description("KickJS \u2014 A production-grade, decorator-driven Node.js framework").version(pkg.version);
@@ -2322,6 +3052,7 @@ async function main() {
2322
3052
  registerInfoCommand(program);
2323
3053
  registerInspectCommand(program);
2324
3054
  registerAddCommand(program);
3055
+ registerTinkerCommand(program);
2325
3056
  registerCustomCommands(program, config);
2326
3057
  program.showHelpAfterError();
2327
3058
  await program.parseAsync(process.argv);