@forinda/kickjs-cli 0.7.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1400 -208
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +30 -0
- package/dist/index.js +119 -28
- 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
|
|
@@ -44,11 +44,35 @@ var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
|
44
44
|
var cliPkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
45
45
|
var KICKJS_VERSION = `^${cliPkg.version}`;
|
|
46
46
|
async function initProject(options) {
|
|
47
|
-
const { name, directory, packageManager = "pnpm" } = options;
|
|
47
|
+
const { name, directory, packageManager = "pnpm", template = "rest" } = options;
|
|
48
48
|
const dir = directory;
|
|
49
49
|
console.log(`
|
|
50
50
|
Creating KickJS project: ${name}
|
|
51
51
|
`);
|
|
52
|
+
const baseDeps = {
|
|
53
|
+
"@forinda/kickjs-core": KICKJS_VERSION,
|
|
54
|
+
"@forinda/kickjs-http": KICKJS_VERSION,
|
|
55
|
+
"@forinda/kickjs-config": KICKJS_VERSION,
|
|
56
|
+
express: "^5.1.0",
|
|
57
|
+
"reflect-metadata": "^0.2.2",
|
|
58
|
+
zod: "^4.3.6",
|
|
59
|
+
pino: "^10.3.1",
|
|
60
|
+
"pino-pretty": "^13.1.3"
|
|
61
|
+
};
|
|
62
|
+
if (template !== "minimal") {
|
|
63
|
+
baseDeps["@forinda/kickjs-swagger"] = KICKJS_VERSION;
|
|
64
|
+
}
|
|
65
|
+
if (template === "graphql") {
|
|
66
|
+
baseDeps["@forinda/kickjs-graphql"] = KICKJS_VERSION;
|
|
67
|
+
baseDeps["graphql"] = "^16.11.0";
|
|
68
|
+
}
|
|
69
|
+
if (template === "microservice") {
|
|
70
|
+
baseDeps["@forinda/kickjs-queue"] = KICKJS_VERSION;
|
|
71
|
+
baseDeps["@forinda/kickjs-otel"] = KICKJS_VERSION;
|
|
72
|
+
}
|
|
73
|
+
if (template === "ddd") {
|
|
74
|
+
baseDeps["@forinda/kickjs-swagger"] = KICKJS_VERSION;
|
|
75
|
+
}
|
|
52
76
|
await writeFileSafe(join(dir, "package.json"), JSON.stringify({
|
|
53
77
|
name,
|
|
54
78
|
version: cliPkg.version,
|
|
@@ -64,17 +88,7 @@ async function initProject(options) {
|
|
|
64
88
|
lint: "eslint src/",
|
|
65
89
|
format: "prettier --write src/"
|
|
66
90
|
},
|
|
67
|
-
dependencies:
|
|
68
|
-
"@forinda/kickjs-core": KICKJS_VERSION,
|
|
69
|
-
"@forinda/kickjs-http": KICKJS_VERSION,
|
|
70
|
-
"@forinda/kickjs-config": KICKJS_VERSION,
|
|
71
|
-
"@forinda/kickjs-swagger": KICKJS_VERSION,
|
|
72
|
-
express: "^5.1.0",
|
|
73
|
-
"reflect-metadata": "^0.2.2",
|
|
74
|
-
zod: "^4.3.6",
|
|
75
|
-
pino: "^10.3.1",
|
|
76
|
-
"pino-pretty": "^13.1.3"
|
|
77
|
-
},
|
|
91
|
+
dependencies: baseDeps,
|
|
78
92
|
devDependencies: {
|
|
79
93
|
"@forinda/kickjs-cli": KICKJS_VERSION,
|
|
80
94
|
"@swc/core": "^1.7.28",
|
|
@@ -165,27 +179,18 @@ NODE_ENV=development
|
|
|
165
179
|
await writeFileSafe(join(dir, ".env.example"), `PORT=3000
|
|
166
180
|
NODE_ENV=development
|
|
167
181
|
`);
|
|
168
|
-
await writeFileSafe(join(dir, "src/index.ts"),
|
|
169
|
-
import { bootstrap } from '@forinda/kickjs-http'
|
|
170
|
-
import { SwaggerAdapter } from '@forinda/kickjs-swagger'
|
|
171
|
-
import { modules } from './modules'
|
|
172
|
-
|
|
173
|
-
bootstrap({
|
|
174
|
-
modules,
|
|
175
|
-
adapters: [
|
|
176
|
-
new SwaggerAdapter({
|
|
177
|
-
info: { title: '${name}', version: '${cliPkg.version}' },
|
|
178
|
-
}),
|
|
179
|
-
],
|
|
180
|
-
})
|
|
181
|
-
`);
|
|
182
|
+
await writeFileSafe(join(dir, "src/index.ts"), getEntryFile(name, template));
|
|
182
183
|
await writeFileSafe(join(dir, "src/modules/index.ts"), `import type { AppModuleClass } from '@forinda/kickjs-core'
|
|
183
184
|
|
|
184
185
|
export const modules: AppModuleClass[] = []
|
|
185
186
|
`);
|
|
187
|
+
if (template === "graphql") {
|
|
188
|
+
await writeFileSafe(join(dir, "src/resolvers/.gitkeep"), "");
|
|
189
|
+
}
|
|
186
190
|
await writeFileSafe(join(dir, "kick.config.ts"), `import { defineConfig } from '@forinda/kickjs-cli'
|
|
187
191
|
|
|
188
192
|
export default defineConfig({
|
|
193
|
+
pattern: '${template}',
|
|
189
194
|
modulesDir: 'src/modules',
|
|
190
195
|
defaultRepo: 'inmemory',
|
|
191
196
|
|
|
@@ -266,17 +271,103 @@ export default defineConfig({
|
|
|
266
271
|
console.log(" Next steps:");
|
|
267
272
|
if (needsCd) console.log(` cd ${name}`);
|
|
268
273
|
if (!options.installDeps) console.log(` ${packageManager} install`);
|
|
269
|
-
|
|
274
|
+
const genHint = {
|
|
275
|
+
rest: "kick g module user",
|
|
276
|
+
graphql: "kick g resolver user",
|
|
277
|
+
ddd: "kick g module user --repo drizzle",
|
|
278
|
+
microservice: "kick g module user && kick g job email",
|
|
279
|
+
minimal: "# add your routes to src/index.ts"
|
|
280
|
+
};
|
|
281
|
+
console.log(` ${genHint[template] ?? genHint.rest}`);
|
|
270
282
|
console.log(" kick dev");
|
|
271
283
|
console.log();
|
|
272
284
|
console.log(" Commands:");
|
|
273
285
|
console.log(" kick dev Start dev server with Vite HMR");
|
|
274
286
|
console.log(" kick build Production build via Vite");
|
|
275
287
|
console.log(" kick start Run production build");
|
|
276
|
-
console.log(
|
|
288
|
+
console.log(` kick g module X Generate a DDD module`);
|
|
289
|
+
if (template === "graphql") console.log(" kick g resolver X Generate a GraphQL resolver");
|
|
290
|
+
if (template === "microservice") console.log(" kick g job X Generate a queue job processor");
|
|
277
291
|
console.log();
|
|
278
292
|
}
|
|
279
293
|
__name(initProject, "initProject");
|
|
294
|
+
function getEntryFile(name, template) {
|
|
295
|
+
switch (template) {
|
|
296
|
+
case "graphql":
|
|
297
|
+
return `import 'reflect-metadata'
|
|
298
|
+
import { bootstrap } from '@forinda/kickjs-http'
|
|
299
|
+
import { DevToolsAdapter } from '@forinda/kickjs-devtools'
|
|
300
|
+
import { GraphQLAdapter } from '@forinda/kickjs-graphql'
|
|
301
|
+
import { modules } from './modules'
|
|
302
|
+
|
|
303
|
+
// Import your resolvers here
|
|
304
|
+
// import { UserResolver } from './resolvers/user.resolver'
|
|
305
|
+
|
|
306
|
+
bootstrap({
|
|
307
|
+
modules,
|
|
308
|
+
adapters: [
|
|
309
|
+
new DevToolsAdapter(),
|
|
310
|
+
new GraphQLAdapter({
|
|
311
|
+
resolvers: [/* UserResolver */],
|
|
312
|
+
// Add custom type definitions here:
|
|
313
|
+
// typeDefs: userTypeDefs,
|
|
314
|
+
}),
|
|
315
|
+
],
|
|
316
|
+
})
|
|
317
|
+
`;
|
|
318
|
+
case "microservice":
|
|
319
|
+
return `import 'reflect-metadata'
|
|
320
|
+
import { bootstrap } from '@forinda/kickjs-http'
|
|
321
|
+
import { DevToolsAdapter } from '@forinda/kickjs-devtools'
|
|
322
|
+
import { SwaggerAdapter } from '@forinda/kickjs-swagger'
|
|
323
|
+
import { OtelAdapter } from '@forinda/kickjs-otel'
|
|
324
|
+
// import { QueueAdapter, BullMQProvider } from '@forinda/kickjs-queue'
|
|
325
|
+
import { modules } from './modules'
|
|
326
|
+
|
|
327
|
+
bootstrap({
|
|
328
|
+
modules,
|
|
329
|
+
adapters: [
|
|
330
|
+
new OtelAdapter({ serviceName: '${name}' }),
|
|
331
|
+
new DevToolsAdapter(),
|
|
332
|
+
new SwaggerAdapter({
|
|
333
|
+
info: { title: '${name}', version: '${cliPkg.version}' },
|
|
334
|
+
}),
|
|
335
|
+
// Uncomment when Redis is available:
|
|
336
|
+
// new QueueAdapter({
|
|
337
|
+
// provider: new BullMQProvider({ host: 'localhost', port: 6379 }),
|
|
338
|
+
// }),
|
|
339
|
+
],
|
|
340
|
+
})
|
|
341
|
+
`;
|
|
342
|
+
case "minimal":
|
|
343
|
+
return `import 'reflect-metadata'
|
|
344
|
+
import { bootstrap } from '@forinda/kickjs-http'
|
|
345
|
+
import { modules } from './modules'
|
|
346
|
+
|
|
347
|
+
bootstrap({ modules })
|
|
348
|
+
`;
|
|
349
|
+
case "ddd":
|
|
350
|
+
case "rest":
|
|
351
|
+
default:
|
|
352
|
+
return `import 'reflect-metadata'
|
|
353
|
+
import { bootstrap } from '@forinda/kickjs-http'
|
|
354
|
+
import { DevToolsAdapter } from '@forinda/kickjs-devtools'
|
|
355
|
+
import { SwaggerAdapter } from '@forinda/kickjs-swagger'
|
|
356
|
+
import { modules } from './modules'
|
|
357
|
+
|
|
358
|
+
bootstrap({
|
|
359
|
+
modules,
|
|
360
|
+
adapters: [
|
|
361
|
+
new DevToolsAdapter(),
|
|
362
|
+
new SwaggerAdapter({
|
|
363
|
+
info: { title: '${name}', version: '${cliPkg.version}' },
|
|
364
|
+
}),
|
|
365
|
+
],
|
|
366
|
+
})
|
|
367
|
+
`;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
__name(getEntryFile, "getEntryFile");
|
|
280
371
|
|
|
281
372
|
// src/commands/init.ts
|
|
282
373
|
function ask(question, defaultValue) {
|
|
@@ -312,7 +403,7 @@ async function confirm(question, defaultYes = true) {
|
|
|
312
403
|
}
|
|
313
404
|
__name(confirm, "confirm");
|
|
314
405
|
function registerInitCommand(program) {
|
|
315
|
-
program.command("new [name]").alias("init").description('Create a new KickJS project (use "." for current directory)').option("-d, --directory <dir>", "Target directory (defaults to project name)").option("--pm <manager>", "Package manager: pnpm | npm | yarn").option("--git", "Initialize git repository").option("--no-git", "Skip git initialization").option("--install", "Install dependencies after scaffolding").option("--no-install", "Skip dependency installation").option("-f, --force", "Remove existing files without prompting").action(async (name, opts) => {
|
|
406
|
+
program.command("new [name]").alias("init").description('Create a new KickJS project (use "." for current directory)').option("-d, --directory <dir>", "Target directory (defaults to project name)").option("--pm <manager>", "Package manager: pnpm | npm | yarn").option("--git", "Initialize git repository").option("--no-git", "Skip git initialization").option("--install", "Install dependencies after scaffolding").option("--no-install", "Skip dependency installation").option("-f, --force", "Remove existing files without prompting").option("-t, --template <type>", "Project template: rest | graphql | ddd | microservice | minimal").action(async (name, opts) => {
|
|
316
407
|
console.log();
|
|
317
408
|
if (!name) {
|
|
318
409
|
name = await ask("Project name", "my-api");
|
|
@@ -354,6 +445,24 @@ function registerInitCommand(program) {
|
|
|
354
445
|
}
|
|
355
446
|
}
|
|
356
447
|
}
|
|
448
|
+
let template = opts.template;
|
|
449
|
+
if (!template) {
|
|
450
|
+
template = await choose("Project template:", [
|
|
451
|
+
"REST API (Express + Swagger)",
|
|
452
|
+
"GraphQL API (GraphQL + GraphiQL)",
|
|
453
|
+
"DDD (Domain-Driven Design modules)",
|
|
454
|
+
"Microservice (REST + Queue worker)",
|
|
455
|
+
"Minimal (bare Express)"
|
|
456
|
+
], 0);
|
|
457
|
+
const templateMap = {
|
|
458
|
+
"REST API (Express + Swagger)": "rest",
|
|
459
|
+
"GraphQL API (GraphQL + GraphiQL)": "graphql",
|
|
460
|
+
"DDD (Domain-Driven Design modules)": "ddd",
|
|
461
|
+
"Microservice (REST + Queue worker)": "microservice",
|
|
462
|
+
"Minimal (bare Express)": "minimal"
|
|
463
|
+
};
|
|
464
|
+
template = templateMap[template] ?? "rest";
|
|
465
|
+
}
|
|
357
466
|
let packageManager = opts.pm;
|
|
358
467
|
if (!packageManager) {
|
|
359
468
|
packageManager = await choose("Package manager:", [
|
|
@@ -379,7 +488,8 @@ function registerInitCommand(program) {
|
|
|
379
488
|
directory,
|
|
380
489
|
packageManager,
|
|
381
490
|
initGit,
|
|
382
|
-
installDeps
|
|
491
|
+
installDeps,
|
|
492
|
+
template
|
|
383
493
|
});
|
|
384
494
|
});
|
|
385
495
|
}
|
|
@@ -1418,10 +1528,10 @@ async function confirm2(message) {
|
|
|
1418
1528
|
input: process.stdin,
|
|
1419
1529
|
output: process.stdout
|
|
1420
1530
|
});
|
|
1421
|
-
return new Promise((
|
|
1531
|
+
return new Promise((resolve6) => {
|
|
1422
1532
|
rl.question(` ${message} (y/N) `, (answer) => {
|
|
1423
1533
|
rl.close();
|
|
1424
|
-
|
|
1534
|
+
resolve6(answer.trim().toLowerCase() === "y");
|
|
1425
1535
|
});
|
|
1426
1536
|
});
|
|
1427
1537
|
}
|
|
@@ -1474,164 +1584,929 @@ export default defineConfig({
|
|
|
1474
1584
|
}
|
|
1475
1585
|
__name(generateConfig, "generateConfig");
|
|
1476
1586
|
|
|
1477
|
-
// src/
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1587
|
+
// src/generators/resolver.ts
|
|
1588
|
+
import { join as join10 } from "path";
|
|
1589
|
+
async function generateResolver(options) {
|
|
1590
|
+
const { name, outDir } = options;
|
|
1591
|
+
const pascal = toPascalCase(name);
|
|
1592
|
+
const kebab = toKebabCase(name);
|
|
1593
|
+
const camel = toCamelCase(name);
|
|
1594
|
+
const files = [];
|
|
1595
|
+
const write = /* @__PURE__ */ __name(async (relativePath, content) => {
|
|
1596
|
+
const fullPath = join10(outDir, relativePath);
|
|
1597
|
+
await writeFileSafe(fullPath, content);
|
|
1598
|
+
files.push(fullPath);
|
|
1599
|
+
}, "write");
|
|
1600
|
+
await write(`${kebab}.resolver.ts`, `import { Service } from '@forinda/kickjs-core'
|
|
1601
|
+
import { Resolver, Query, Mutation, Arg } from '@forinda/kickjs-graphql'
|
|
1602
|
+
|
|
1603
|
+
/**
|
|
1604
|
+
* ${pascal} GraphQL Resolver
|
|
1605
|
+
*
|
|
1606
|
+
* Decorators:
|
|
1607
|
+
* @Resolver(typeName?) \u2014 marks this class as a GraphQL resolver
|
|
1608
|
+
* @Query(name?, { returnType?, description? }) \u2014 defines a query field
|
|
1609
|
+
* @Mutation(name?, { returnType?, description? }) \u2014 defines a mutation field
|
|
1610
|
+
* @Arg(name, type?) \u2014 marks a method parameter as a GraphQL argument
|
|
1611
|
+
*/
|
|
1612
|
+
@Service()
|
|
1613
|
+
@Resolver('${pascal}')
|
|
1614
|
+
export class ${pascal}Resolver {
|
|
1615
|
+
private items: Array<{ id: string; name: string }> = []
|
|
1616
|
+
|
|
1617
|
+
@Query('${camel}s', { returnType: '[${pascal}]', description: 'List all ${camel}s' })
|
|
1618
|
+
findAll() {
|
|
1619
|
+
return this.items
|
|
1484
1620
|
}
|
|
1485
|
-
console.log();
|
|
1486
|
-
}
|
|
1487
|
-
__name(printGenerated, "printGenerated");
|
|
1488
|
-
function registerGenerateCommand(program) {
|
|
1489
|
-
const gen = program.command("generate").alias("g").description("Generate code scaffolds");
|
|
1490
|
-
gen.command("module <name>").description("Generate a full DDD module with all layers").option("--no-entity", "Skip entity and value object generation").option("--no-tests", "Skip test file generation").option("--repo <type>", "Repository implementation: inmemory | drizzle", "inmemory").option("--minimal", "Only generate index.ts and controller").option("--modules-dir <dir>", "Modules directory", "src/modules").action(async (name, opts) => {
|
|
1491
|
-
const files = await generateModule({
|
|
1492
|
-
name,
|
|
1493
|
-
modulesDir: resolve2(opts.modulesDir),
|
|
1494
|
-
noEntity: opts.entity === false,
|
|
1495
|
-
noTests: opts.tests === false,
|
|
1496
|
-
repo: opts.repo,
|
|
1497
|
-
minimal: opts.minimal
|
|
1498
|
-
});
|
|
1499
|
-
printGenerated(files);
|
|
1500
|
-
});
|
|
1501
|
-
gen.command("adapter <name>").description("Generate an AppAdapter with lifecycle hooks and middleware support").option("-o, --out <dir>", "Output directory", "src/adapters").action(async (name, opts) => {
|
|
1502
|
-
const files = await generateAdapter({
|
|
1503
|
-
name,
|
|
1504
|
-
outDir: resolve2(opts.out)
|
|
1505
|
-
});
|
|
1506
|
-
printGenerated(files);
|
|
1507
|
-
});
|
|
1508
|
-
gen.command("middleware <name>").description("Generate an Express middleware function").option("-o, --out <dir>", "Output directory", "src/middleware").action(async (name, opts) => {
|
|
1509
|
-
const files = await generateMiddleware({
|
|
1510
|
-
name,
|
|
1511
|
-
outDir: resolve2(opts.out)
|
|
1512
|
-
});
|
|
1513
|
-
printGenerated(files);
|
|
1514
|
-
});
|
|
1515
|
-
gen.command("guard <name>").description("Generate a route guard (auth, roles, etc.)").option("-o, --out <dir>", "Output directory", "src/guards").action(async (name, opts) => {
|
|
1516
|
-
const files = await generateGuard({
|
|
1517
|
-
name,
|
|
1518
|
-
outDir: resolve2(opts.out)
|
|
1519
|
-
});
|
|
1520
|
-
printGenerated(files);
|
|
1521
|
-
});
|
|
1522
|
-
gen.command("service <name>").description("Generate a @Service() class").option("-o, --out <dir>", "Output directory", "src/services").action(async (name, opts) => {
|
|
1523
|
-
const files = await generateService({
|
|
1524
|
-
name,
|
|
1525
|
-
outDir: resolve2(opts.out)
|
|
1526
|
-
});
|
|
1527
|
-
printGenerated(files);
|
|
1528
|
-
});
|
|
1529
|
-
gen.command("controller <name>").description("Generate a @Controller() class with basic routes").option("-o, --out <dir>", "Output directory", "src/controllers").action(async (name, opts) => {
|
|
1530
|
-
const files = await generateController2({
|
|
1531
|
-
name,
|
|
1532
|
-
outDir: resolve2(opts.out)
|
|
1533
|
-
});
|
|
1534
|
-
printGenerated(files);
|
|
1535
|
-
});
|
|
1536
|
-
gen.command("dto <name>").description("Generate a Zod DTO schema").option("-o, --out <dir>", "Output directory", "src/dtos").action(async (name, opts) => {
|
|
1537
|
-
const files = await generateDto({
|
|
1538
|
-
name,
|
|
1539
|
-
outDir: resolve2(opts.out)
|
|
1540
|
-
});
|
|
1541
|
-
printGenerated(files);
|
|
1542
|
-
});
|
|
1543
|
-
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) => {
|
|
1544
|
-
const files = await generateConfig({
|
|
1545
|
-
outDir: resolve2("."),
|
|
1546
|
-
modulesDir: opts.modulesDir,
|
|
1547
|
-
defaultRepo: opts.repo,
|
|
1548
|
-
force: opts.force
|
|
1549
|
-
});
|
|
1550
|
-
printGenerated(files);
|
|
1551
|
-
});
|
|
1552
|
-
}
|
|
1553
|
-
__name(registerGenerateCommand, "registerGenerateCommand");
|
|
1554
1621
|
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
cwd,
|
|
1560
|
-
stdio: "inherit"
|
|
1561
|
-
});
|
|
1562
|
-
}
|
|
1563
|
-
__name(runShellCommand, "runShellCommand");
|
|
1622
|
+
@Query('${camel}', { returnType: '${pascal}', description: 'Get a ${camel} by ID' })
|
|
1623
|
+
findById(@Arg('id', 'ID!') id: string) {
|
|
1624
|
+
return this.items.find((item) => item.id === id) ?? null
|
|
1625
|
+
}
|
|
1564
1626
|
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1627
|
+
@Mutation('create${pascal}', { returnType: '${pascal}', description: 'Create a new ${camel}' })
|
|
1628
|
+
create(@Arg('name', 'String!') name: string) {
|
|
1629
|
+
const item = { id: String(this.items.length + 1), name }
|
|
1630
|
+
this.items.push(item)
|
|
1631
|
+
return item
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
@Mutation('update${pascal}', { returnType: '${pascal}', description: 'Update a ${camel}' })
|
|
1635
|
+
update(@Arg('id', 'ID!') id: string, @Arg('name', 'String!') name: string) {
|
|
1636
|
+
const item = this.items.find((i) => i.id === id)
|
|
1637
|
+
if (item) item.name = name
|
|
1638
|
+
return item
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
@Mutation('delete${pascal}', { returnType: 'Boolean', description: 'Delete a ${camel}' })
|
|
1642
|
+
remove(@Arg('id', 'ID!') id: string) {
|
|
1643
|
+
const idx = this.items.findIndex((i) => i.id === id)
|
|
1644
|
+
if (idx === -1) return false
|
|
1645
|
+
this.items.splice(idx, 1)
|
|
1646
|
+
return true
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1576
1649
|
`);
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
];
|
|
1590
|
-
if (opts.port) envVars.push(`PORT=${opts.port}`);
|
|
1591
|
-
runShellCommand(`${envVars.join(" ")} node ${opts.entry}`);
|
|
1592
|
-
});
|
|
1593
|
-
program.command("dev:debug").description("Start dev server with Node.js inspector").option("-e, --entry <file>", "Entry file", "src/index.ts").option("-p, --port <port>", "Port number").action((opts) => {
|
|
1594
|
-
const envVars = opts.port ? `PORT=${opts.port} ` : "";
|
|
1595
|
-
try {
|
|
1596
|
-
runShellCommand(`${envVars}npx vite-node --inspect --watch ${opts.entry}`);
|
|
1597
|
-
} catch {
|
|
1598
|
-
}
|
|
1599
|
-
});
|
|
1650
|
+
await write(`${kebab}.typedefs.ts`, `/**
|
|
1651
|
+
* ${pascal} GraphQL type definitions.
|
|
1652
|
+
* Pass to GraphQLAdapter's typeDefs option to register custom types.
|
|
1653
|
+
*/
|
|
1654
|
+
export const ${camel}TypeDefs = \`
|
|
1655
|
+
type ${pascal} {
|
|
1656
|
+
id: ID!
|
|
1657
|
+
name: String!
|
|
1658
|
+
}
|
|
1659
|
+
\`
|
|
1660
|
+
`);
|
|
1661
|
+
return files;
|
|
1600
1662
|
}
|
|
1601
|
-
__name(
|
|
1663
|
+
__name(generateResolver, "generateResolver");
|
|
1602
1664
|
|
|
1603
|
-
// src/
|
|
1604
|
-
import {
|
|
1605
|
-
function
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1665
|
+
// src/generators/job.ts
|
|
1666
|
+
import { join as join11 } from "path";
|
|
1667
|
+
async function generateJob(options) {
|
|
1668
|
+
const { name, outDir } = options;
|
|
1669
|
+
const pascal = toPascalCase(name);
|
|
1670
|
+
const kebab = toKebabCase(name);
|
|
1671
|
+
const camel = toCamelCase(name);
|
|
1672
|
+
const queueName = options.queue ?? `${kebab}-queue`;
|
|
1673
|
+
const files = [];
|
|
1674
|
+
const write = /* @__PURE__ */ __name(async (relativePath, content) => {
|
|
1675
|
+
const fullPath = join11(outDir, relativePath);
|
|
1676
|
+
await writeFileSafe(fullPath, content);
|
|
1677
|
+
files.push(fullPath);
|
|
1678
|
+
}, "write");
|
|
1679
|
+
await write(`${kebab}.job.ts`, `import { Inject } from '@forinda/kickjs-core'
|
|
1680
|
+
import { Job, Process, QUEUE_MANAGER, type QueueService } from '@forinda/kickjs-queue'
|
|
1609
1681
|
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1682
|
+
/**
|
|
1683
|
+
* ${pascal} Job Processor
|
|
1684
|
+
*
|
|
1685
|
+
* Decorators:
|
|
1686
|
+
* @Job(queueName) \u2014 marks this class as a job processor for a queue
|
|
1687
|
+
* @Process(jobName?) \u2014 marks a method as the handler for a specific job type
|
|
1688
|
+
* - Without a name: handles all jobs in the queue
|
|
1689
|
+
* - With a name: handles only jobs matching that name
|
|
1690
|
+
*
|
|
1691
|
+
* To add jobs to this queue from a service or controller:
|
|
1692
|
+
* @Inject(QUEUE_MANAGER) private queue: QueueService
|
|
1693
|
+
* await this.queue.add('${queueName}', '${camel}', { ... })
|
|
1694
|
+
*/
|
|
1695
|
+
@Job('${queueName}')
|
|
1696
|
+
export class ${pascal}Job {
|
|
1697
|
+
@Process()
|
|
1698
|
+
async handle(job: { name: string; data: any; id?: string }) {
|
|
1699
|
+
console.log(\`Processing \${job.name} (id: \${job.id})\`, job.data)
|
|
1700
|
+
|
|
1701
|
+
// TODO: Implement job logic here
|
|
1702
|
+
// Example:
|
|
1703
|
+
// await this.emailService.send(job.data.to, job.data.subject, job.data.body)
|
|
1704
|
+
}
|
|
1613
1705
|
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1706
|
+
@Process('${camel}.priority')
|
|
1707
|
+
async handlePriority(job: { name: string; data: any; id?: string }) {
|
|
1708
|
+
console.log(\`Priority job: \${job.name}\`, job.data)
|
|
1709
|
+
// Handle high-priority variant of this job
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1619
1712
|
`);
|
|
1620
|
-
|
|
1713
|
+
return files;
|
|
1621
1714
|
}
|
|
1622
|
-
__name(
|
|
1715
|
+
__name(generateJob, "generateJob");
|
|
1623
1716
|
|
|
1624
|
-
// src/
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
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
|
+
|
|
2254
|
+
// src/commands/generate.ts
|
|
2255
|
+
function printGenerated(files) {
|
|
2256
|
+
const cwd = process.cwd();
|
|
2257
|
+
console.log(`
|
|
2258
|
+
Generated ${files.length} file${files.length === 1 ? "" : "s"}:`);
|
|
2259
|
+
for (const f of files) {
|
|
2260
|
+
console.log(` ${f.replace(cwd + "/", "")}`);
|
|
2261
|
+
}
|
|
2262
|
+
console.log();
|
|
2263
|
+
}
|
|
2264
|
+
__name(printGenerated, "printGenerated");
|
|
2265
|
+
function registerGenerateCommand(program) {
|
|
2266
|
+
const gen = program.command("generate").alias("g").description("Generate code scaffolds");
|
|
2267
|
+
gen.command("module <name>").description("Generate a full DDD module with all layers").option("--no-entity", "Skip entity and value object generation").option("--no-tests", "Skip test file generation").option("--repo <type>", "Repository implementation: inmemory | drizzle", "inmemory").option("--minimal", "Only generate index.ts and controller").option("--modules-dir <dir>", "Modules directory", "src/modules").action(async (name, opts) => {
|
|
2268
|
+
const files = await generateModule({
|
|
2269
|
+
name,
|
|
2270
|
+
modulesDir: resolve2(opts.modulesDir),
|
|
2271
|
+
noEntity: opts.entity === false,
|
|
2272
|
+
noTests: opts.tests === false,
|
|
2273
|
+
repo: opts.repo,
|
|
2274
|
+
minimal: opts.minimal
|
|
2275
|
+
});
|
|
2276
|
+
printGenerated(files);
|
|
2277
|
+
});
|
|
2278
|
+
gen.command("adapter <name>").description("Generate an AppAdapter with lifecycle hooks and middleware support").option("-o, --out <dir>", "Output directory", "src/adapters").action(async (name, opts) => {
|
|
2279
|
+
const files = await generateAdapter({
|
|
2280
|
+
name,
|
|
2281
|
+
outDir: resolve2(opts.out)
|
|
2282
|
+
});
|
|
2283
|
+
printGenerated(files);
|
|
2284
|
+
});
|
|
2285
|
+
gen.command("middleware <name>").description("Generate an Express middleware function").option("-o, --out <dir>", "Output directory", "src/middleware").action(async (name, opts) => {
|
|
2286
|
+
const files = await generateMiddleware({
|
|
2287
|
+
name,
|
|
2288
|
+
outDir: resolve2(opts.out)
|
|
2289
|
+
});
|
|
2290
|
+
printGenerated(files);
|
|
2291
|
+
});
|
|
2292
|
+
gen.command("guard <name>").description("Generate a route guard (auth, roles, etc.)").option("-o, --out <dir>", "Output directory", "src/guards").action(async (name, opts) => {
|
|
2293
|
+
const files = await generateGuard({
|
|
2294
|
+
name,
|
|
2295
|
+
outDir: resolve2(opts.out)
|
|
2296
|
+
});
|
|
2297
|
+
printGenerated(files);
|
|
2298
|
+
});
|
|
2299
|
+
gen.command("service <name>").description("Generate a @Service() class").option("-o, --out <dir>", "Output directory", "src/services").action(async (name, opts) => {
|
|
2300
|
+
const files = await generateService({
|
|
2301
|
+
name,
|
|
2302
|
+
outDir: resolve2(opts.out)
|
|
2303
|
+
});
|
|
2304
|
+
printGenerated(files);
|
|
2305
|
+
});
|
|
2306
|
+
gen.command("controller <name>").description("Generate a @Controller() class with basic routes").option("-o, --out <dir>", "Output directory", "src/controllers").action(async (name, opts) => {
|
|
2307
|
+
const files = await generateController2({
|
|
2308
|
+
name,
|
|
2309
|
+
outDir: resolve2(opts.out)
|
|
2310
|
+
});
|
|
2311
|
+
printGenerated(files);
|
|
2312
|
+
});
|
|
2313
|
+
gen.command("dto <name>").description("Generate a Zod DTO schema").option("-o, --out <dir>", "Output directory", "src/dtos").action(async (name, opts) => {
|
|
2314
|
+
const files = await generateDto({
|
|
2315
|
+
name,
|
|
2316
|
+
outDir: resolve2(opts.out)
|
|
2317
|
+
});
|
|
2318
|
+
printGenerated(files);
|
|
2319
|
+
});
|
|
2320
|
+
gen.command("resolver <name>").description("Generate a GraphQL @Resolver class with @Query and @Mutation methods").option("-o, --out <dir>", "Output directory", "src/resolvers").action(async (name, opts) => {
|
|
2321
|
+
const files = await generateResolver({
|
|
2322
|
+
name,
|
|
2323
|
+
outDir: resolve2(opts.out)
|
|
2324
|
+
});
|
|
2325
|
+
printGenerated(files);
|
|
2326
|
+
});
|
|
2327
|
+
gen.command("job <name>").description("Generate a @Job queue processor with @Process handlers").option("-o, --out <dir>", "Output directory", "src/jobs").option("-q, --queue <name>", "Queue name (default: <name>-queue)").action(async (name, opts) => {
|
|
2328
|
+
const files = await generateJob({
|
|
2329
|
+
name,
|
|
2330
|
+
outDir: resolve2(opts.out),
|
|
2331
|
+
queue: opts.queue
|
|
2332
|
+
});
|
|
2333
|
+
printGenerated(files);
|
|
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
|
+
});
|
|
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) => {
|
|
2356
|
+
const files = await generateConfig({
|
|
2357
|
+
outDir: resolve2("."),
|
|
2358
|
+
modulesDir: opts.modulesDir,
|
|
2359
|
+
defaultRepo: opts.repo,
|
|
2360
|
+
force: opts.force
|
|
2361
|
+
});
|
|
2362
|
+
printGenerated(files);
|
|
2363
|
+
});
|
|
2364
|
+
}
|
|
2365
|
+
__name(registerGenerateCommand, "registerGenerateCommand");
|
|
2366
|
+
|
|
2367
|
+
// src/commands/run.ts
|
|
2368
|
+
import { cpSync, existsSync as existsSync3, mkdirSync } from "fs";
|
|
2369
|
+
import { resolve as resolve3, join as join14 } from "path";
|
|
2370
|
+
|
|
2371
|
+
// src/utils/shell.ts
|
|
2372
|
+
import { execSync as execSync2 } from "child_process";
|
|
2373
|
+
function runShellCommand(command, cwd) {
|
|
2374
|
+
execSync2(command, {
|
|
2375
|
+
cwd,
|
|
2376
|
+
stdio: "inherit"
|
|
2377
|
+
});
|
|
2378
|
+
}
|
|
2379
|
+
__name(runShellCommand, "runShellCommand");
|
|
2380
|
+
|
|
2381
|
+
// src/config.ts
|
|
2382
|
+
import { readFile as readFile4, access as access2 } from "fs/promises";
|
|
2383
|
+
import { join as join13 } from "path";
|
|
2384
|
+
var CONFIG_FILES = [
|
|
2385
|
+
"kick.config.ts",
|
|
2386
|
+
"kick.config.js",
|
|
2387
|
+
"kick.config.mjs",
|
|
2388
|
+
"kick.config.json"
|
|
2389
|
+
];
|
|
2390
|
+
async function loadKickConfig(cwd) {
|
|
2391
|
+
for (const filename of CONFIG_FILES) {
|
|
2392
|
+
const filepath = join13(cwd, filename);
|
|
2393
|
+
try {
|
|
2394
|
+
await access2(filepath);
|
|
2395
|
+
} catch {
|
|
2396
|
+
continue;
|
|
2397
|
+
}
|
|
2398
|
+
if (filename.endsWith(".json")) {
|
|
2399
|
+
const content = await readFile4(filepath, "utf-8");
|
|
2400
|
+
return JSON.parse(content);
|
|
2401
|
+
}
|
|
2402
|
+
try {
|
|
2403
|
+
const { pathToFileURL: pathToFileURL2 } = await import("url");
|
|
2404
|
+
const mod = await import(pathToFileURL2(filepath).href);
|
|
2405
|
+
return mod.default ?? mod;
|
|
2406
|
+
} catch (err) {
|
|
2407
|
+
if (filename.endsWith(".ts")) {
|
|
2408
|
+
console.warn(`Warning: Failed to load ${filename}. TypeScript config files require a runtime loader (e.g. tsx, ts-node) or use kick.config.js/.mjs instead.`);
|
|
2409
|
+
}
|
|
2410
|
+
continue;
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
return null;
|
|
2414
|
+
}
|
|
2415
|
+
__name(loadKickConfig, "loadKickConfig");
|
|
2416
|
+
|
|
2417
|
+
// src/commands/run.ts
|
|
2418
|
+
function registerRunCommands(program) {
|
|
2419
|
+
program.command("dev").description("Start development server with Vite HMR (zero-downtime reload)").option("-e, --entry <file>", "Entry file", "src/index.ts").option("-p, --port <port>", "Port number").action((opts) => {
|
|
2420
|
+
const envVars = [];
|
|
2421
|
+
if (opts.port) envVars.push(`PORT=${opts.port}`);
|
|
2422
|
+
const cmd = `npx vite-node --watch ${opts.entry}`;
|
|
2423
|
+
const fullCmd = envVars.length ? `${envVars.join(" ")} ${cmd}` : cmd;
|
|
2424
|
+
console.log(`
|
|
2425
|
+
KickJS dev server starting...`);
|
|
2426
|
+
console.log(` Entry: ${opts.entry}`);
|
|
2427
|
+
console.log(` HMR: enabled (vite-node)
|
|
2428
|
+
`);
|
|
2429
|
+
try {
|
|
2430
|
+
runShellCommand(fullCmd);
|
|
2431
|
+
} catch {
|
|
2432
|
+
}
|
|
2433
|
+
});
|
|
2434
|
+
program.command("build").description("Build for production via Vite").action(async () => {
|
|
2435
|
+
console.log("\n Building for production...\n");
|
|
2436
|
+
runShellCommand("npx vite build");
|
|
2437
|
+
const config = await loadKickConfig(process.cwd());
|
|
2438
|
+
const copyDirs = config?.copyDirs ?? [];
|
|
2439
|
+
if (copyDirs.length > 0) {
|
|
2440
|
+
console.log("\n Copying directories to dist...");
|
|
2441
|
+
for (const entry of copyDirs) {
|
|
2442
|
+
const src = typeof entry === "string" ? entry : entry.src;
|
|
2443
|
+
const dest = typeof entry === "string" ? join14("dist", entry) : entry.dest ?? join14("dist", src);
|
|
2444
|
+
const srcPath = resolve3(src);
|
|
2445
|
+
const destPath = resolve3(dest);
|
|
2446
|
+
if (!existsSync3(srcPath)) {
|
|
2447
|
+
console.log(` \u26A0 Skipped ${src} (not found)`);
|
|
2448
|
+
continue;
|
|
2449
|
+
}
|
|
2450
|
+
mkdirSync(destPath, {
|
|
2451
|
+
recursive: true
|
|
2452
|
+
});
|
|
2453
|
+
cpSync(srcPath, destPath, {
|
|
2454
|
+
recursive: true
|
|
2455
|
+
});
|
|
2456
|
+
console.log(` \u2713 ${src} \u2192 ${dest}`);
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
console.log("\n Build complete.\n");
|
|
2460
|
+
});
|
|
2461
|
+
program.command("start").description("Start production server").option("-e, --entry <file>", "Entry file", "dist/index.js").option("-p, --port <port>", "Port number").action((opts) => {
|
|
2462
|
+
const envVars = [
|
|
2463
|
+
"NODE_ENV=production"
|
|
2464
|
+
];
|
|
2465
|
+
if (opts.port) envVars.push(`PORT=${opts.port}`);
|
|
2466
|
+
runShellCommand(`${envVars.join(" ")} node ${opts.entry}`);
|
|
2467
|
+
});
|
|
2468
|
+
program.command("dev:debug").description("Start dev server with Node.js inspector").option("-e, --entry <file>", "Entry file", "src/index.ts").option("-p, --port <port>", "Port number").action((opts) => {
|
|
2469
|
+
const envVars = opts.port ? `PORT=${opts.port} ` : "";
|
|
2470
|
+
try {
|
|
2471
|
+
runShellCommand(`${envVars}npx vite-node --inspect --watch ${opts.entry}`);
|
|
2472
|
+
} catch {
|
|
2473
|
+
}
|
|
2474
|
+
});
|
|
2475
|
+
}
|
|
2476
|
+
__name(registerRunCommands, "registerRunCommands");
|
|
2477
|
+
|
|
2478
|
+
// src/commands/info.ts
|
|
2479
|
+
import { platform, release, arch } from "os";
|
|
2480
|
+
function registerInfoCommand(program) {
|
|
2481
|
+
program.command("info").description("Print system and framework info").action(() => {
|
|
2482
|
+
console.log(`
|
|
2483
|
+
KickJS CLI
|
|
2484
|
+
|
|
2485
|
+
System:
|
|
2486
|
+
OS: ${platform()} ${release()} (${arch()})
|
|
2487
|
+
Node: ${process.version}
|
|
2488
|
+
|
|
2489
|
+
Packages:
|
|
2490
|
+
@forinda/kickjs-core workspace
|
|
2491
|
+
@forinda/kickjs-http workspace
|
|
2492
|
+
@forinda/kickjs-config workspace
|
|
2493
|
+
@forinda/kickjs-cli workspace
|
|
2494
|
+
`);
|
|
2495
|
+
});
|
|
2496
|
+
}
|
|
2497
|
+
__name(registerInfoCommand, "registerInfoCommand");
|
|
2498
|
+
|
|
2499
|
+
// src/commands/custom.ts
|
|
2500
|
+
function registerCustomCommands(program, config) {
|
|
2501
|
+
if (!config?.commands?.length) return;
|
|
2502
|
+
for (const cmd of config.commands) {
|
|
2503
|
+
registerSingleCommand(program, cmd);
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
__name(registerCustomCommands, "registerCustomCommands");
|
|
2507
|
+
function registerSingleCommand(program, def) {
|
|
2508
|
+
const command = program.command(def.name).description(def.description);
|
|
2509
|
+
if (def.aliases) {
|
|
1635
2510
|
for (const alias of def.aliases) {
|
|
1636
2511
|
command.alias(alias);
|
|
1637
2512
|
}
|
|
@@ -1813,45 +2688,360 @@ function registerInspectCommand(program) {
|
|
|
1813
2688
|
}
|
|
1814
2689
|
__name(registerInspectCommand, "registerInspectCommand");
|
|
1815
2690
|
|
|
1816
|
-
// src/
|
|
1817
|
-
import {
|
|
1818
|
-
import {
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
]
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
2691
|
+
// src/commands/add.ts
|
|
2692
|
+
import { execSync as execSync3 } from "child_process";
|
|
2693
|
+
import { existsSync as existsSync4 } from "fs";
|
|
2694
|
+
import { resolve as resolve4 } from "path";
|
|
2695
|
+
var PACKAGE_REGISTRY = {
|
|
2696
|
+
// Core (already installed by kick new)
|
|
2697
|
+
core: {
|
|
2698
|
+
pkg: "@forinda/kickjs-core",
|
|
2699
|
+
peers: [],
|
|
2700
|
+
description: "DI container, decorators, reactivity"
|
|
2701
|
+
},
|
|
2702
|
+
http: {
|
|
2703
|
+
pkg: "@forinda/kickjs-http",
|
|
2704
|
+
peers: [
|
|
2705
|
+
"express"
|
|
2706
|
+
],
|
|
2707
|
+
description: "Express 5, routing, middleware"
|
|
2708
|
+
},
|
|
2709
|
+
config: {
|
|
2710
|
+
pkg: "@forinda/kickjs-config",
|
|
2711
|
+
peers: [],
|
|
2712
|
+
description: "Zod-based env validation"
|
|
2713
|
+
},
|
|
2714
|
+
cli: {
|
|
2715
|
+
pkg: "@forinda/kickjs-cli",
|
|
2716
|
+
peers: [],
|
|
2717
|
+
description: "CLI tool and code generators",
|
|
2718
|
+
dev: true
|
|
2719
|
+
},
|
|
2720
|
+
// API
|
|
2721
|
+
swagger: {
|
|
2722
|
+
pkg: "@forinda/kickjs-swagger",
|
|
2723
|
+
peers: [],
|
|
2724
|
+
description: "OpenAPI spec + Swagger UI + ReDoc"
|
|
2725
|
+
},
|
|
2726
|
+
graphql: {
|
|
2727
|
+
pkg: "@forinda/kickjs-graphql",
|
|
2728
|
+
peers: [
|
|
2729
|
+
"graphql"
|
|
2730
|
+
],
|
|
2731
|
+
description: "GraphQL resolvers + GraphiQL"
|
|
2732
|
+
},
|
|
2733
|
+
// Database
|
|
2734
|
+
drizzle: {
|
|
2735
|
+
pkg: "@forinda/kickjs-drizzle",
|
|
2736
|
+
peers: [
|
|
2737
|
+
"drizzle-orm"
|
|
2738
|
+
],
|
|
2739
|
+
description: "Drizzle ORM adapter + query builder"
|
|
2740
|
+
},
|
|
2741
|
+
prisma: {
|
|
2742
|
+
pkg: "@forinda/kickjs-prisma",
|
|
2743
|
+
peers: [
|
|
2744
|
+
"@prisma/client"
|
|
2745
|
+
],
|
|
2746
|
+
description: "Prisma adapter + query builder"
|
|
2747
|
+
},
|
|
2748
|
+
// Real-time
|
|
2749
|
+
ws: {
|
|
2750
|
+
pkg: "@forinda/kickjs-ws",
|
|
2751
|
+
peers: [
|
|
2752
|
+
"socket.io"
|
|
2753
|
+
],
|
|
2754
|
+
description: "WebSocket with @WsController decorators"
|
|
2755
|
+
},
|
|
2756
|
+
// Observability
|
|
2757
|
+
otel: {
|
|
2758
|
+
pkg: "@forinda/kickjs-otel",
|
|
2759
|
+
peers: [
|
|
2760
|
+
"@opentelemetry/api"
|
|
2761
|
+
],
|
|
2762
|
+
description: "OpenTelemetry tracing + metrics"
|
|
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
|
+
},
|
|
2795
|
+
// Queue
|
|
2796
|
+
queue: {
|
|
2797
|
+
pkg: "@forinda/kickjs-queue",
|
|
2798
|
+
peers: [],
|
|
2799
|
+
description: "Queue adapter (BullMQ/RabbitMQ/Kafka)"
|
|
2800
|
+
},
|
|
2801
|
+
"queue:bullmq": {
|
|
2802
|
+
pkg: "@forinda/kickjs-queue",
|
|
2803
|
+
peers: [
|
|
2804
|
+
"bullmq",
|
|
2805
|
+
"ioredis"
|
|
2806
|
+
],
|
|
2807
|
+
description: "Queue with BullMQ + Redis"
|
|
2808
|
+
},
|
|
2809
|
+
"queue:rabbitmq": {
|
|
2810
|
+
pkg: "@forinda/kickjs-queue",
|
|
2811
|
+
peers: [
|
|
2812
|
+
"amqplib"
|
|
2813
|
+
],
|
|
2814
|
+
description: "Queue with RabbitMQ"
|
|
2815
|
+
},
|
|
2816
|
+
"queue:kafka": {
|
|
2817
|
+
pkg: "@forinda/kickjs-queue",
|
|
2818
|
+
peers: [
|
|
2819
|
+
"kafkajs"
|
|
2820
|
+
],
|
|
2821
|
+
description: "Queue with Kafka"
|
|
2822
|
+
},
|
|
2823
|
+
// Multi-tenancy
|
|
2824
|
+
"multi-tenant": {
|
|
2825
|
+
pkg: "@forinda/kickjs-multi-tenant",
|
|
2826
|
+
peers: [],
|
|
2827
|
+
description: "Tenant resolution middleware"
|
|
2828
|
+
},
|
|
2829
|
+
// Notifications
|
|
2830
|
+
notifications: {
|
|
2831
|
+
pkg: "@forinda/kickjs-notifications",
|
|
2832
|
+
peers: [],
|
|
2833
|
+
description: "Multi-channel notifications \u2014 email, Slack, Discord, webhook"
|
|
2834
|
+
},
|
|
2835
|
+
// Testing
|
|
2836
|
+
testing: {
|
|
2837
|
+
pkg: "@forinda/kickjs-testing",
|
|
2838
|
+
peers: [],
|
|
2839
|
+
description: "Test utilities and TestModule builder",
|
|
2840
|
+
dev: true
|
|
2841
|
+
}
|
|
2842
|
+
};
|
|
2843
|
+
function detectPackageManager() {
|
|
2844
|
+
if (existsSync4(resolve4("pnpm-lock.yaml"))) return "pnpm";
|
|
2845
|
+
if (existsSync4(resolve4("yarn.lock"))) return "yarn";
|
|
2846
|
+
return "npm";
|
|
2847
|
+
}
|
|
2848
|
+
__name(detectPackageManager, "detectPackageManager");
|
|
2849
|
+
function registerAddCommand(program) {
|
|
2850
|
+
program.command("add [packages...]").description("Add KickJS packages with their required dependencies").option("--pm <manager>", "Package manager override").option("-D, --dev", "Install as dev dependency").option("--list", "List all available packages").action(async (packages, opts) => {
|
|
2851
|
+
if (opts.list || packages.length === 0) {
|
|
2852
|
+
console.log("\n Available KickJS packages:\n");
|
|
2853
|
+
const maxName = Math.max(...Object.keys(PACKAGE_REGISTRY).map((k) => k.length));
|
|
2854
|
+
for (const [name, info] of Object.entries(PACKAGE_REGISTRY)) {
|
|
2855
|
+
const padded = name.padEnd(maxName + 2);
|
|
2856
|
+
const peers = info.peers.length ? ` (+ ${info.peers.join(", ")})` : "";
|
|
2857
|
+
console.log(` ${padded} ${info.description}${peers}`);
|
|
2858
|
+
}
|
|
2859
|
+
console.log("\n Usage: kick add graphql drizzle otel");
|
|
2860
|
+
console.log(" kick add queue:bullmq");
|
|
2861
|
+
console.log();
|
|
2862
|
+
return;
|
|
1832
2863
|
}
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
2864
|
+
const pm = opts.pm ?? detectPackageManager();
|
|
2865
|
+
const forceDevFlag = opts.dev;
|
|
2866
|
+
const prodDeps = /* @__PURE__ */ new Set();
|
|
2867
|
+
const devDeps = /* @__PURE__ */ new Set();
|
|
2868
|
+
const unknown = [];
|
|
2869
|
+
for (const name of packages) {
|
|
2870
|
+
const entry = PACKAGE_REGISTRY[name];
|
|
2871
|
+
if (!entry) {
|
|
2872
|
+
unknown.push(name);
|
|
2873
|
+
continue;
|
|
2874
|
+
}
|
|
2875
|
+
const target = forceDevFlag || entry.dev ? devDeps : prodDeps;
|
|
2876
|
+
target.add(entry.pkg);
|
|
2877
|
+
for (const peer of entry.peers) {
|
|
2878
|
+
target.add(peer);
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
if (unknown.length > 0) {
|
|
2882
|
+
console.log(`
|
|
2883
|
+
Unknown packages: ${unknown.join(", ")}`);
|
|
2884
|
+
console.log(' Run "kick add --list" to see available packages.\n');
|
|
2885
|
+
if (prodDeps.size === 0 && devDeps.size === 0) return;
|
|
2886
|
+
}
|
|
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
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
if (devDeps.size > 0) {
|
|
2906
|
+
const deps = Array.from(devDeps);
|
|
2907
|
+
const cmd = `${pm} add -D ${deps.join(" ")}`;
|
|
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(`
|
|
2918
|
+
Installation failed. Run manually:
|
|
2919
|
+
${cmd}
|
|
2920
|
+
`);
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
console.log(" Done!\n");
|
|
2924
|
+
});
|
|
2925
|
+
}
|
|
2926
|
+
__name(registerAddCommand, "registerAddCommand");
|
|
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);
|
|
1836
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");
|
|
1837
2952
|
try {
|
|
1838
|
-
const
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
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 {
|
|
1844
2965
|
}
|
|
1845
|
-
continue;
|
|
1846
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;
|
|
1847
3037
|
}
|
|
1848
3038
|
return null;
|
|
1849
3039
|
}
|
|
1850
|
-
__name(
|
|
3040
|
+
__name(findBin, "findBin");
|
|
1851
3041
|
|
|
1852
3042
|
// src/cli.ts
|
|
1853
3043
|
var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
1854
|
-
var pkg = JSON.parse(readFileSync2(
|
|
3044
|
+
var pkg = JSON.parse(readFileSync2(join16(__dirname2, "..", "package.json"), "utf-8"));
|
|
1855
3045
|
async function main() {
|
|
1856
3046
|
const program = new Command();
|
|
1857
3047
|
program.name("kick").description("KickJS \u2014 A production-grade, decorator-driven Node.js framework").version(pkg.version);
|
|
@@ -1861,6 +3051,8 @@ async function main() {
|
|
|
1861
3051
|
registerRunCommands(program);
|
|
1862
3052
|
registerInfoCommand(program);
|
|
1863
3053
|
registerInspectCommand(program);
|
|
3054
|
+
registerAddCommand(program);
|
|
3055
|
+
registerTinkerCommand(program);
|
|
1864
3056
|
registerCustomCommands(program, config);
|
|
1865
3057
|
program.showHelpAfterError();
|
|
1866
3058
|
await program.parseAsync(process.argv);
|