@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 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 join11 } from "path";
8
+ import { dirname as dirname3, join as join16 } from "path";
9
9
  import { fileURLToPath as fileURLToPath2 } from "url";
10
10
 
11
11
  // src/commands/init.ts
@@ -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"), `import 'reflect-metadata'
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
- console.log(" kick g module user");
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(" kick g module X Generate a DDD module");
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((resolve3) => {
1531
+ return new Promise((resolve6) => {
1422
1532
  rl.question(` ${message} (y/N) `, (answer) => {
1423
1533
  rl.close();
1424
- resolve3(answer.trim().toLowerCase() === "y");
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/commands/generate.ts
1478
- function printGenerated(files) {
1479
- const cwd = process.cwd();
1480
- console.log(`
1481
- Generated ${files.length} file${files.length === 1 ? "" : "s"}:`);
1482
- for (const f of files) {
1483
- console.log(` ${f.replace(cwd + "/", "")}`);
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
- // src/utils/shell.ts
1556
- import { execSync as execSync2 } from "child_process";
1557
- function runShellCommand(command, cwd) {
1558
- execSync2(command, {
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
- // src/commands/run.ts
1566
- function registerRunCommands(program) {
1567
- 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) => {
1568
- const envVars = [];
1569
- if (opts.port) envVars.push(`PORT=${opts.port}`);
1570
- const cmd = `npx vite-node --watch ${opts.entry}`;
1571
- const fullCmd = envVars.length ? `${envVars.join(" ")} ${cmd}` : cmd;
1572
- console.log(`
1573
- KickJS dev server starting...`);
1574
- console.log(` Entry: ${opts.entry}`);
1575
- console.log(` HMR: enabled (vite-node)
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
- try {
1578
- runShellCommand(fullCmd);
1579
- } catch {
1580
- }
1581
- });
1582
- program.command("build").description("Build for production via Vite").action(() => {
1583
- console.log("\n Building for production...\n");
1584
- runShellCommand("npx vite build");
1585
- });
1586
- program.command("start").description("Start production server").option("-e, --entry <file>", "Entry file", "dist/index.js").option("-p, --port <port>", "Port number").action((opts) => {
1587
- const envVars = [
1588
- "NODE_ENV=production"
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(registerRunCommands, "registerRunCommands");
1663
+ __name(generateResolver, "generateResolver");
1602
1664
 
1603
- // src/commands/info.ts
1604
- import { platform, release, arch } from "os";
1605
- function registerInfoCommand(program) {
1606
- program.command("info").description("Print system and framework info").action(() => {
1607
- console.log(`
1608
- KickJS CLI
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
- System:
1611
- OS: ${platform()} ${release()} (${arch()})
1612
- Node: ${process.version}
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
- Packages:
1615
- @forinda/kickjs-core workspace
1616
- @forinda/kickjs-http workspace
1617
- @forinda/kickjs-config workspace
1618
- @forinda/kickjs-cli workspace
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(registerInfoCommand, "registerInfoCommand");
1715
+ __name(generateJob, "generateJob");
1623
1716
 
1624
- // src/commands/custom.ts
1625
- function registerCustomCommands(program, config) {
1626
- if (!config?.commands?.length) return;
1627
- for (const cmd of config.commands) {
1628
- registerSingleCommand(program, cmd);
1629
- }
1630
- }
1631
- __name(registerCustomCommands, "registerCustomCommands");
1632
- function registerSingleCommand(program, def) {
1633
- const command = program.command(def.name).description(def.description);
1634
- if (def.aliases) {
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/config.ts
1817
- import { readFile as readFile3, access as access2 } from "fs/promises";
1818
- import { join as join10 } from "path";
1819
- var CONFIG_FILES = [
1820
- "kick.config.ts",
1821
- "kick.config.js",
1822
- "kick.config.mjs",
1823
- "kick.config.json"
1824
- ];
1825
- async function loadKickConfig(cwd) {
1826
- for (const filename of CONFIG_FILES) {
1827
- const filepath = join10(cwd, filename);
1828
- try {
1829
- await access2(filepath);
1830
- } catch {
1831
- continue;
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
- if (filename.endsWith(".json")) {
1834
- const content = await readFile3(filepath, "utf-8");
1835
- return JSON.parse(content);
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 { pathToFileURL } = await import("url");
1839
- const mod = await import(pathToFileURL(filepath).href);
1840
- return mod.default ?? mod;
1841
- } catch (err) {
1842
- if (filename.endsWith(".ts")) {
1843
- 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.`);
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(loadKickConfig, "loadKickConfig");
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(join11(__dirname2, "..", "package.json"), "utf-8"));
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);