@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 +767 -36
- package/dist/cli.js.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
|
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-
|
|
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-
|
|
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-
|
|
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((
|
|
1531
|
+
return new Promise((resolve6) => {
|
|
1532
1532
|
rl.question(` ${message} (y/N) `, (answer) => {
|
|
1533
1533
|
rl.close();
|
|
1534
|
-
|
|
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
|
|
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
|
|
1826
|
-
import { join as
|
|
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 =
|
|
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
|
|
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(
|
|
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" ?
|
|
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
|
|
2270
|
-
const
|
|
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
|
-
|
|
2875
|
+
const target = forceDevFlag || entry.dev ? devDeps : prodDeps;
|
|
2876
|
+
target.add(entry.pkg);
|
|
2279
2877
|
for (const peer of entry.peers) {
|
|
2280
|
-
|
|
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 (
|
|
2885
|
+
if (prodDeps.size === 0 && devDeps.size === 0) return;
|
|
2288
2886
|
}
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
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
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
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
|
-
${
|
|
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(
|
|
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);
|