@forinda/kickjs-cli 0.6.0 → 1.0.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
@@ -4,6 +4,9 @@ var __name = (target, value) => __defProp(target, "name", { value, configurable:
4
4
 
5
5
  // src/cli.ts
6
6
  import { Command } from "commander";
7
+ import { readFileSync as readFileSync2 } from "fs";
8
+ import { dirname as dirname3, join as join14 } from "path";
9
+ import { fileURLToPath as fileURLToPath2 } from "url";
7
10
 
8
11
  // src/commands/init.ts
9
12
  import { resolve, basename } from "path";
@@ -11,8 +14,10 @@ import { createInterface } from "readline";
11
14
  import { existsSync, readdirSync, rmSync } from "fs";
12
15
 
13
16
  // src/generators/project.ts
14
- import { join } from "path";
17
+ import { join, dirname as dirname2 } from "path";
15
18
  import { execSync } from "child_process";
19
+ import { readFileSync } from "fs";
20
+ import { fileURLToPath } from "url";
16
21
 
17
22
  // src/utils/fs.ts
18
23
  import { writeFile, mkdir, access, readFile } from "fs/promises";
@@ -35,15 +40,42 @@ async function fileExists(filePath) {
35
40
  __name(fileExists, "fileExists");
36
41
 
37
42
  // src/generators/project.ts
43
+ var __dirname = dirname2(fileURLToPath(import.meta.url));
44
+ var cliPkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
45
+ var KICKJS_VERSION = `^${cliPkg.version}`;
38
46
  async function initProject(options) {
39
- const { name, directory, packageManager = "pnpm" } = options;
47
+ const { name, directory, packageManager = "pnpm", template = "rest" } = options;
40
48
  const dir = directory;
41
49
  console.log(`
42
50
  Creating KickJS project: ${name}
43
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
+ }
44
76
  await writeFileSafe(join(dir, "package.json"), JSON.stringify({
45
77
  name,
46
- version: "0.1.0",
78
+ version: cliPkg.version,
47
79
  type: "module",
48
80
  scripts: {
49
81
  dev: "kick dev",
@@ -56,19 +88,9 @@ async function initProject(options) {
56
88
  lint: "eslint src/",
57
89
  format: "prettier --write src/"
58
90
  },
59
- dependencies: {
60
- "@forinda/kickjs-core": "^0.1.0",
61
- "@forinda/kickjs-http": "^0.1.0",
62
- "@forinda/kickjs-config": "^0.1.0",
63
- "@forinda/kickjs-swagger": "^0.1.0",
64
- express: "^5.1.0",
65
- "reflect-metadata": "^0.2.2",
66
- zod: "^4.3.6",
67
- pino: "^10.3.1",
68
- "pino-pretty": "^13.1.3"
69
- },
91
+ dependencies: baseDeps,
70
92
  devDependencies: {
71
- "@forinda/kickjs-cli": "^0.1.0",
93
+ "@forinda/kickjs-cli": KICKJS_VERSION,
72
94
  "@swc/core": "^1.7.28",
73
95
  "@types/express": "^5.0.6",
74
96
  "@types/node": "^24.5.2",
@@ -157,27 +179,18 @@ NODE_ENV=development
157
179
  await writeFileSafe(join(dir, ".env.example"), `PORT=3000
158
180
  NODE_ENV=development
159
181
  `);
160
- await writeFileSafe(join(dir, "src/index.ts"), `import 'reflect-metadata'
161
- import { bootstrap } from '@forinda/kickjs-http'
162
- import { SwaggerAdapter } from '@forinda/kickjs-swagger'
163
- import { modules } from './modules'
164
-
165
- bootstrap({
166
- modules,
167
- adapters: [
168
- new SwaggerAdapter({
169
- info: { title: '${name}', version: '0.1.0' },
170
- }),
171
- ],
172
- })
173
- `);
182
+ await writeFileSafe(join(dir, "src/index.ts"), getEntryFile(name, template));
174
183
  await writeFileSafe(join(dir, "src/modules/index.ts"), `import type { AppModuleClass } from '@forinda/kickjs-core'
175
184
 
176
185
  export const modules: AppModuleClass[] = []
177
186
  `);
187
+ if (template === "graphql") {
188
+ await writeFileSafe(join(dir, "src/resolvers/.gitkeep"), "");
189
+ }
178
190
  await writeFileSafe(join(dir, "kick.config.ts"), `import { defineConfig } from '@forinda/kickjs-cli'
179
191
 
180
192
  export default defineConfig({
193
+ pattern: '${template}',
181
194
  modulesDir: 'src/modules',
182
195
  defaultRepo: 'inmemory',
183
196
 
@@ -258,17 +271,103 @@ export default defineConfig({
258
271
  console.log(" Next steps:");
259
272
  if (needsCd) console.log(` cd ${name}`);
260
273
  if (!options.installDeps) console.log(` ${packageManager} install`);
261
- 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}`);
262
282
  console.log(" kick dev");
263
283
  console.log();
264
284
  console.log(" Commands:");
265
285
  console.log(" kick dev Start dev server with Vite HMR");
266
286
  console.log(" kick build Production build via Vite");
267
287
  console.log(" kick start Run production build");
268
- 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");
269
291
  console.log();
270
292
  }
271
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-http/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-http/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-http/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");
272
371
 
273
372
  // src/commands/init.ts
274
373
  function ask(question, defaultValue) {
@@ -304,7 +403,7 @@ async function confirm(question, defaultYes = true) {
304
403
  }
305
404
  __name(confirm, "confirm");
306
405
  function registerInitCommand(program) {
307
- 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) => {
308
407
  console.log();
309
408
  if (!name) {
310
409
  name = await ask("Project name", "my-api");
@@ -346,6 +445,24 @@ function registerInitCommand(program) {
346
445
  }
347
446
  }
348
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
+ }
349
466
  let packageManager = opts.pm;
350
467
  if (!packageManager) {
351
468
  packageManager = await choose("Package manager:", [
@@ -371,7 +488,8 @@ function registerInitCommand(program) {
371
488
  directory,
372
489
  packageManager,
373
490
  initGit,
374
- installDeps
491
+ installDeps,
492
+ template
375
493
  });
376
494
  });
377
495
  }
@@ -1410,10 +1528,10 @@ async function confirm2(message) {
1410
1528
  input: process.stdin,
1411
1529
  output: process.stdout
1412
1530
  });
1413
- return new Promise((resolve3) => {
1531
+ return new Promise((resolve5) => {
1414
1532
  rl.question(` ${message} (y/N) `, (answer) => {
1415
1533
  rl.close();
1416
- resolve3(answer.trim().toLowerCase() === "y");
1534
+ resolve5(answer.trim().toLowerCase() === "y");
1417
1535
  });
1418
1536
  });
1419
1537
  }
@@ -1466,6 +1584,136 @@ export default defineConfig({
1466
1584
  }
1467
1585
  __name(generateConfig, "generateConfig");
1468
1586
 
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
1620
+ }
1621
+
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
+ }
1626
+
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
+ }
1649
+ `);
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;
1662
+ }
1663
+ __name(generateResolver, "generateResolver");
1664
+
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'
1681
+
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
+ }
1705
+
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
+ }
1712
+ `);
1713
+ return files;
1714
+ }
1715
+ __name(generateJob, "generateJob");
1716
+
1469
1717
  // src/commands/generate.ts
1470
1718
  function printGenerated(files) {
1471
1719
  const cwd = process.cwd();
@@ -1532,6 +1780,21 @@ function registerGenerateCommand(program) {
1532
1780
  });
1533
1781
  printGenerated(files);
1534
1782
  });
1783
+ 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) => {
1784
+ const files = await generateResolver({
1785
+ name,
1786
+ outDir: resolve2(opts.out)
1787
+ });
1788
+ printGenerated(files);
1789
+ });
1790
+ 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) => {
1791
+ const files = await generateJob({
1792
+ name,
1793
+ outDir: resolve2(opts.out),
1794
+ queue: opts.queue
1795
+ });
1796
+ printGenerated(files);
1797
+ });
1535
1798
  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) => {
1536
1799
  const files = await generateConfig({
1537
1800
  outDir: resolve2("."),
@@ -1544,6 +1807,10 @@ function registerGenerateCommand(program) {
1544
1807
  }
1545
1808
  __name(registerGenerateCommand, "registerGenerateCommand");
1546
1809
 
1810
+ // src/commands/run.ts
1811
+ import { cpSync, existsSync as existsSync3, mkdirSync } from "fs";
1812
+ import { resolve as resolve3, join as join13 } from "path";
1813
+
1547
1814
  // src/utils/shell.ts
1548
1815
  import { execSync as execSync2 } from "child_process";
1549
1816
  function runShellCommand(command, cwd) {
@@ -1554,6 +1821,42 @@ function runShellCommand(command, cwd) {
1554
1821
  }
1555
1822
  __name(runShellCommand, "runShellCommand");
1556
1823
 
1824
+ // src/config.ts
1825
+ import { readFile as readFile3, access as access2 } from "fs/promises";
1826
+ import { join as join12 } from "path";
1827
+ var CONFIG_FILES = [
1828
+ "kick.config.ts",
1829
+ "kick.config.js",
1830
+ "kick.config.mjs",
1831
+ "kick.config.json"
1832
+ ];
1833
+ async function loadKickConfig(cwd) {
1834
+ for (const filename of CONFIG_FILES) {
1835
+ const filepath = join12(cwd, filename);
1836
+ try {
1837
+ await access2(filepath);
1838
+ } catch {
1839
+ continue;
1840
+ }
1841
+ if (filename.endsWith(".json")) {
1842
+ const content = await readFile3(filepath, "utf-8");
1843
+ return JSON.parse(content);
1844
+ }
1845
+ try {
1846
+ const { pathToFileURL } = await import("url");
1847
+ const mod = await import(pathToFileURL(filepath).href);
1848
+ return mod.default ?? mod;
1849
+ } catch (err) {
1850
+ if (filename.endsWith(".ts")) {
1851
+ 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.`);
1852
+ }
1853
+ continue;
1854
+ }
1855
+ }
1856
+ return null;
1857
+ }
1858
+ __name(loadKickConfig, "loadKickConfig");
1859
+
1557
1860
  // src/commands/run.ts
1558
1861
  function registerRunCommands(program) {
1559
1862
  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) => {
@@ -1571,9 +1874,32 @@ function registerRunCommands(program) {
1571
1874
  } catch {
1572
1875
  }
1573
1876
  });
1574
- program.command("build").description("Build for production via Vite").action(() => {
1877
+ program.command("build").description("Build for production via Vite").action(async () => {
1575
1878
  console.log("\n Building for production...\n");
1576
1879
  runShellCommand("npx vite build");
1880
+ const config = await loadKickConfig(process.cwd());
1881
+ const copyDirs = config?.copyDirs ?? [];
1882
+ if (copyDirs.length > 0) {
1883
+ console.log("\n Copying directories to dist...");
1884
+ for (const entry of copyDirs) {
1885
+ const src = typeof entry === "string" ? entry : entry.src;
1886
+ const dest = typeof entry === "string" ? join13("dist", entry) : entry.dest ?? join13("dist", src);
1887
+ const srcPath = resolve3(src);
1888
+ const destPath = resolve3(dest);
1889
+ if (!existsSync3(srcPath)) {
1890
+ console.log(` \u26A0 Skipped ${src} (not found)`);
1891
+ continue;
1892
+ }
1893
+ mkdirSync(destPath, {
1894
+ recursive: true
1895
+ });
1896
+ cpSync(srcPath, destPath, {
1897
+ recursive: true
1898
+ });
1899
+ console.log(` \u2713 ${src} \u2192 ${dest}`);
1900
+ }
1901
+ }
1902
+ console.log("\n Build complete.\n");
1577
1903
  });
1578
1904
  program.command("start").description("Start production server").option("-e, --entry <file>", "Entry file", "dist/index.js").option("-p, --port <port>", "Port number").action((opts) => {
1579
1905
  const envVars = [
@@ -1650,51 +1976,352 @@ function registerSingleCommand(program, def) {
1650
1976
  }
1651
1977
  __name(registerSingleCommand, "registerSingleCommand");
1652
1978
 
1653
- // src/config.ts
1654
- import { readFile as readFile3, access as access2 } from "fs/promises";
1655
- import { join as join10 } from "path";
1656
- var CONFIG_FILES = [
1657
- "kick.config.ts",
1658
- "kick.config.js",
1659
- "kick.config.mjs",
1660
- "kick.config.json"
1661
- ];
1662
- async function loadKickConfig(cwd) {
1663
- for (const filename of CONFIG_FILES) {
1664
- const filepath = join10(cwd, filename);
1665
- try {
1666
- await access2(filepath);
1667
- } catch {
1668
- continue;
1979
+ // src/commands/inspect.ts
1980
+ var esc = /* @__PURE__ */ __name((code) => `\x1B[${code}m`, "esc");
1981
+ var reset = esc("0");
1982
+ var bold = /* @__PURE__ */ __name((s) => `${esc("1")}${s}${reset}`, "bold");
1983
+ var dim = /* @__PURE__ */ __name((s) => `${esc("2")}${s}${reset}`, "dim");
1984
+ var green = /* @__PURE__ */ __name((s) => `${esc("32")}${s}${reset}`, "green");
1985
+ var red = /* @__PURE__ */ __name((s) => `${esc("31")}${s}${reset}`, "red");
1986
+ var yellow = /* @__PURE__ */ __name((s) => `${esc("33")}${s}${reset}`, "yellow");
1987
+ var cyan = /* @__PURE__ */ __name((s) => `${esc("36")}${s}${reset}`, "cyan");
1988
+ var magenta = /* @__PURE__ */ __name((s) => `${esc("35")}${s}${reset}`, "magenta");
1989
+ var blue = /* @__PURE__ */ __name((s) => `${esc("34")}${s}${reset}`, "blue");
1990
+ var METHOD_COLORS = {
1991
+ GET: green,
1992
+ POST: cyan,
1993
+ PUT: yellow,
1994
+ PATCH: magenta,
1995
+ DELETE: red
1996
+ };
1997
+ function colorMethod(method) {
1998
+ const fn = METHOD_COLORS[method] ?? dim;
1999
+ return fn(method.padEnd(7));
2000
+ }
2001
+ __name(colorMethod, "colorMethod");
2002
+ function formatUptime(seconds) {
2003
+ const d = Math.floor(seconds / 86400);
2004
+ const h = Math.floor(seconds % 86400 / 3600);
2005
+ const m = Math.floor(seconds % 3600 / 60);
2006
+ const s = seconds % 60;
2007
+ const parts = [];
2008
+ if (d) parts.push(`${d}d`);
2009
+ if (h) parts.push(`${h}h`);
2010
+ if (m) parts.push(`${m}m`);
2011
+ parts.push(`${s}s`);
2012
+ return parts.join(" ");
2013
+ }
2014
+ __name(formatUptime, "formatUptime");
2015
+ async function fetchJson(url) {
2016
+ const res = await fetch(url, {
2017
+ signal: AbortSignal.timeout(5e3)
2018
+ });
2019
+ if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
2020
+ return res.json();
2021
+ }
2022
+ __name(fetchJson, "fetchJson");
2023
+ async function fetchEndpoint(base, path) {
2024
+ try {
2025
+ return await fetchJson(`${base}${path}`);
2026
+ } catch {
2027
+ return null;
2028
+ }
2029
+ }
2030
+ __name(fetchEndpoint, "fetchEndpoint");
2031
+ async function fetchAll(base) {
2032
+ const [health, metrics, routes, container, ws] = await Promise.all([
2033
+ fetchEndpoint(base, "/health"),
2034
+ fetchEndpoint(base, "/metrics"),
2035
+ fetchEndpoint(base, "/routes"),
2036
+ fetchEndpoint(base, "/container"),
2037
+ fetchEndpoint(base, "/ws")
2038
+ ]);
2039
+ return {
2040
+ health,
2041
+ metrics,
2042
+ routes,
2043
+ container,
2044
+ ws
2045
+ };
2046
+ }
2047
+ __name(fetchAll, "fetchAll");
2048
+ function printSummary(base, data) {
2049
+ const { health, metrics, routes, container, ws } = data;
2050
+ const line = dim("\u2500".repeat(60));
2051
+ console.log();
2052
+ console.log(bold(` KickJS Inspector`) + dim(` \u2192 ${base}`));
2053
+ console.log(line);
2054
+ if (health) {
2055
+ const statusText = health.status === "healthy" ? green("\u25CF healthy") : red("\u25CF " + health.status);
2056
+ console.log(` ${bold("Health:")} ${statusText}`);
2057
+ } else {
2058
+ console.log(` ${bold("Health:")} ${red("\u25CF unreachable")}`);
2059
+ }
2060
+ if (metrics) {
2061
+ const rate = ((metrics.errorRate ?? 0) * 100).toFixed(1);
2062
+ const rateColor = metrics.errorRate > 0.1 ? red : metrics.errorRate > 0 ? yellow : green;
2063
+ console.log(` ${bold("Uptime:")} ${formatUptime(metrics.uptimeSeconds)}`);
2064
+ console.log(` ${bold("Requests:")} ${metrics.requests}`);
2065
+ console.log(` ${bold("Errors:")} ${metrics.serverErrors} server, ${metrics.clientErrors ?? 0} client ${dim("(")}${rateColor(rate + "%")}${dim(")")}`);
2066
+ }
2067
+ if (container) {
2068
+ console.log(` ${bold("DI:")} ${container.count} bindings`);
2069
+ }
2070
+ if (ws && ws.enabled) {
2071
+ console.log(` ${bold("WS:")} ${ws.connections ?? 0} connections, ${ws.namespaces ?? 0} namespaces`);
2072
+ }
2073
+ if (routes?.routes?.length) {
2074
+ console.log();
2075
+ console.log(bold(" Routes"));
2076
+ console.log(line);
2077
+ console.log(` ${dim("METHOD")} ${dim("PATH".padEnd(36))} ${dim("CONTROLLER")}`);
2078
+ for (const r of routes.routes) {
2079
+ const path = r.path.length > 36 ? r.path.slice(0, 33) + "..." : r.path.padEnd(36);
2080
+ console.log(` ${colorMethod(r.method)} ${path} ${blue(r.controller)}.${dim(r.handler)}`);
1669
2081
  }
1670
- if (filename.endsWith(".json")) {
1671
- const content = await readFile3(filepath, "utf-8");
1672
- return JSON.parse(content);
2082
+ }
2083
+ console.log(line);
2084
+ console.log();
2085
+ }
2086
+ __name(printSummary, "printSummary");
2087
+ function registerInspectCommand(program) {
2088
+ program.command("inspect [url]").description("Connect to a running KickJS app and display debug info").option("-p, --port <port>", "Override port").option("-w, --watch", "Poll every 5 seconds").option("-j, --json", "Output raw JSON").action(async (url, opts) => {
2089
+ let base = url ?? "http://localhost:3000";
2090
+ if (opts.port) {
2091
+ try {
2092
+ const parsed = new URL(base);
2093
+ parsed.port = opts.port;
2094
+ base = parsed.origin;
2095
+ } catch {
2096
+ base = `http://localhost:${opts.port}`;
2097
+ }
1673
2098
  }
1674
- try {
1675
- const { pathToFileURL } = await import("url");
1676
- const mod = await import(pathToFileURL(filepath).href);
1677
- return mod.default ?? mod;
1678
- } catch (err) {
1679
- if (filename.endsWith(".ts")) {
1680
- 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.`);
2099
+ const debugBase = `${base.replace(/\/$/, "")}/_debug`;
2100
+ const run = /* @__PURE__ */ __name(async () => {
2101
+ try {
2102
+ const data = await fetchAll(debugBase);
2103
+ if (opts.json) {
2104
+ console.log(JSON.stringify(data, null, 2));
2105
+ } else {
2106
+ printSummary(base, data);
2107
+ }
2108
+ } catch (err) {
2109
+ if (opts.json) {
2110
+ console.log(JSON.stringify({
2111
+ error: String(err)
2112
+ }));
2113
+ } else {
2114
+ console.error(red(` \u2716 Could not connect to ${base}`));
2115
+ console.error(dim(` ${err instanceof Error ? err.message : String(err)}`));
2116
+ }
2117
+ if (!opts.watch) process.exitCode = 1;
1681
2118
  }
1682
- continue;
2119
+ }, "run");
2120
+ if (opts.watch) {
2121
+ const poll = /* @__PURE__ */ __name(async () => {
2122
+ process.stdout.write("\x1B[2J\x1B[H");
2123
+ await run();
2124
+ }, "poll");
2125
+ await poll();
2126
+ setInterval(poll, 5e3);
2127
+ } else {
2128
+ await run();
1683
2129
  }
2130
+ });
2131
+ }
2132
+ __name(registerInspectCommand, "registerInspectCommand");
2133
+
2134
+ // src/commands/add.ts
2135
+ import { execSync as execSync3 } from "child_process";
2136
+ import { existsSync as existsSync4 } from "fs";
2137
+ import { resolve as resolve4 } from "path";
2138
+ var PACKAGE_REGISTRY = {
2139
+ // Core (already installed by kick new)
2140
+ core: {
2141
+ pkg: "@forinda/kickjs-core",
2142
+ peers: [],
2143
+ description: "DI container, decorators, reactivity"
2144
+ },
2145
+ http: {
2146
+ pkg: "@forinda/kickjs-http",
2147
+ peers: [
2148
+ "express"
2149
+ ],
2150
+ description: "Express 5, routing, middleware"
2151
+ },
2152
+ config: {
2153
+ pkg: "@forinda/kickjs-config",
2154
+ peers: [],
2155
+ description: "Zod-based env validation"
2156
+ },
2157
+ cli: {
2158
+ pkg: "@forinda/kickjs-cli",
2159
+ peers: [],
2160
+ description: "CLI tool and code generators"
2161
+ },
2162
+ // API
2163
+ swagger: {
2164
+ pkg: "@forinda/kickjs-swagger",
2165
+ peers: [],
2166
+ description: "OpenAPI spec + Swagger UI + ReDoc"
2167
+ },
2168
+ graphql: {
2169
+ pkg: "@forinda/kickjs-graphql",
2170
+ peers: [
2171
+ "graphql"
2172
+ ],
2173
+ description: "GraphQL resolvers + GraphiQL"
2174
+ },
2175
+ // Database
2176
+ drizzle: {
2177
+ pkg: "@forinda/kickjs-drizzle",
2178
+ peers: [
2179
+ "drizzle-orm"
2180
+ ],
2181
+ description: "Drizzle ORM adapter + query builder"
2182
+ },
2183
+ prisma: {
2184
+ pkg: "@forinda/kickjs-prisma",
2185
+ peers: [
2186
+ "@prisma/client"
2187
+ ],
2188
+ description: "Prisma adapter + query builder"
2189
+ },
2190
+ // Real-time
2191
+ ws: {
2192
+ pkg: "@forinda/kickjs-ws",
2193
+ peers: [
2194
+ "socket.io"
2195
+ ],
2196
+ description: "WebSocket with @WsController decorators"
2197
+ },
2198
+ // Observability
2199
+ otel: {
2200
+ pkg: "@forinda/kickjs-otel",
2201
+ peers: [
2202
+ "@opentelemetry/api"
2203
+ ],
2204
+ description: "OpenTelemetry tracing + metrics"
2205
+ },
2206
+ // Queue
2207
+ queue: {
2208
+ pkg: "@forinda/kickjs-queue",
2209
+ peers: [],
2210
+ description: "Queue adapter (BullMQ/RabbitMQ/Kafka)"
2211
+ },
2212
+ "queue:bullmq": {
2213
+ pkg: "@forinda/kickjs-queue",
2214
+ peers: [
2215
+ "bullmq",
2216
+ "ioredis"
2217
+ ],
2218
+ description: "Queue with BullMQ + Redis"
2219
+ },
2220
+ "queue:rabbitmq": {
2221
+ pkg: "@forinda/kickjs-queue",
2222
+ peers: [
2223
+ "amqplib"
2224
+ ],
2225
+ description: "Queue with RabbitMQ"
2226
+ },
2227
+ "queue:kafka": {
2228
+ pkg: "@forinda/kickjs-queue",
2229
+ peers: [
2230
+ "kafkajs"
2231
+ ],
2232
+ description: "Queue with Kafka"
2233
+ },
2234
+ // Multi-tenancy
2235
+ "multi-tenant": {
2236
+ pkg: "@forinda/kickjs-multi-tenant",
2237
+ peers: [],
2238
+ description: "Tenant resolution middleware"
2239
+ },
2240
+ // Testing
2241
+ testing: {
2242
+ pkg: "@forinda/kickjs-testing",
2243
+ peers: [],
2244
+ description: "Test utilities and TestModule builder"
1684
2245
  }
1685
- return null;
2246
+ };
2247
+ function detectPackageManager() {
2248
+ if (existsSync4(resolve4("pnpm-lock.yaml"))) return "pnpm";
2249
+ if (existsSync4(resolve4("yarn.lock"))) return "yarn";
2250
+ return "npm";
2251
+ }
2252
+ __name(detectPackageManager, "detectPackageManager");
2253
+ function registerAddCommand(program) {
2254
+ 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) => {
2255
+ if (opts.list || packages.length === 0) {
2256
+ console.log("\n Available KickJS packages:\n");
2257
+ const maxName = Math.max(...Object.keys(PACKAGE_REGISTRY).map((k) => k.length));
2258
+ for (const [name, info] of Object.entries(PACKAGE_REGISTRY)) {
2259
+ const padded = name.padEnd(maxName + 2);
2260
+ const peers = info.peers.length ? ` (+ ${info.peers.join(", ")})` : "";
2261
+ console.log(` ${padded} ${info.description}${peers}`);
2262
+ }
2263
+ console.log("\n Usage: kick add graphql drizzle otel");
2264
+ console.log(" kick add queue:bullmq");
2265
+ console.log();
2266
+ return;
2267
+ }
2268
+ const pm = opts.pm ?? detectPackageManager();
2269
+ const devFlag = opts.dev ? " -D" : "";
2270
+ const allDeps = /* @__PURE__ */ new Set();
2271
+ const unknown = [];
2272
+ for (const name of packages) {
2273
+ const entry = PACKAGE_REGISTRY[name];
2274
+ if (!entry) {
2275
+ unknown.push(name);
2276
+ continue;
2277
+ }
2278
+ allDeps.add(entry.pkg);
2279
+ for (const peer of entry.peers) {
2280
+ allDeps.add(peer);
2281
+ }
2282
+ }
2283
+ if (unknown.length > 0) {
2284
+ console.log(`
2285
+ Unknown packages: ${unknown.join(", ")}`);
2286
+ console.log(' Run "kick add --list" to see available packages.\n');
2287
+ if (allDeps.size === 0) return;
2288
+ }
2289
+ const depsArray = Array.from(allDeps);
2290
+ const installCmd = `${pm} add${devFlag} ${depsArray.join(" ")}`;
2291
+ console.log(`
2292
+ Installing ${depsArray.length} package(s):`);
2293
+ for (const dep of depsArray) {
2294
+ console.log(` + ${dep}`);
2295
+ }
2296
+ console.log();
2297
+ try {
2298
+ execSync3(installCmd, {
2299
+ stdio: "inherit"
2300
+ });
2301
+ console.log("\n Done!\n");
2302
+ } catch {
2303
+ console.log(`
2304
+ Installation failed. Run manually:
2305
+ ${installCmd}
2306
+ `);
2307
+ }
2308
+ });
1686
2309
  }
1687
- __name(loadKickConfig, "loadKickConfig");
2310
+ __name(registerAddCommand, "registerAddCommand");
1688
2311
 
1689
2312
  // src/cli.ts
2313
+ var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
2314
+ var pkg = JSON.parse(readFileSync2(join14(__dirname2, "..", "package.json"), "utf-8"));
1690
2315
  async function main() {
1691
2316
  const program = new Command();
1692
- program.name("kick").description("KickJS \u2014 A production-grade, decorator-driven Node.js framework").version("0.1.0");
2317
+ program.name("kick").description("KickJS \u2014 A production-grade, decorator-driven Node.js framework").version(pkg.version);
1693
2318
  const config = await loadKickConfig(process.cwd());
1694
2319
  registerInitCommand(program);
1695
2320
  registerGenerateCommand(program);
1696
2321
  registerRunCommands(program);
1697
2322
  registerInfoCommand(program);
2323
+ registerInspectCommand(program);
2324
+ registerAddCommand(program);
1698
2325
  registerCustomCommands(program, config);
1699
2326
  program.showHelpAfterError();
1700
2327
  await program.parseAsync(process.argv);