@forinda/kickjs-cli 0.7.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
@@ -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 join14 } 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-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");
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((resolve5) => {
1422
1532
  rl.question(` ${message} (y/N) `, (answer) => {
1423
1533
  rl.close();
1424
- resolve3(answer.trim().toLowerCase() === "y");
1534
+ resolve5(answer.trim().toLowerCase() === "y");
1425
1535
  });
1426
1536
  });
1427
1537
  }
@@ -1474,6 +1584,136 @@ export default defineConfig({
1474
1584
  }
1475
1585
  __name(generateConfig, "generateConfig");
1476
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
+
1477
1717
  // src/commands/generate.ts
1478
1718
  function printGenerated(files) {
1479
1719
  const cwd = process.cwd();
@@ -1540,6 +1780,21 @@ function registerGenerateCommand(program) {
1540
1780
  });
1541
1781
  printGenerated(files);
1542
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
+ });
1543
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) => {
1544
1799
  const files = await generateConfig({
1545
1800
  outDir: resolve2("."),
@@ -1552,6 +1807,10 @@ function registerGenerateCommand(program) {
1552
1807
  }
1553
1808
  __name(registerGenerateCommand, "registerGenerateCommand");
1554
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
+
1555
1814
  // src/utils/shell.ts
1556
1815
  import { execSync as execSync2 } from "child_process";
1557
1816
  function runShellCommand(command, cwd) {
@@ -1562,6 +1821,42 @@ function runShellCommand(command, cwd) {
1562
1821
  }
1563
1822
  __name(runShellCommand, "runShellCommand");
1564
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
+
1565
1860
  // src/commands/run.ts
1566
1861
  function registerRunCommands(program) {
1567
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) => {
@@ -1579,9 +1874,32 @@ function registerRunCommands(program) {
1579
1874
  } catch {
1580
1875
  }
1581
1876
  });
1582
- program.command("build").description("Build for production via Vite").action(() => {
1877
+ program.command("build").description("Build for production via Vite").action(async () => {
1583
1878
  console.log("\n Building for production...\n");
1584
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");
1585
1903
  });
1586
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) => {
1587
1905
  const envVars = [
@@ -1813,45 +2131,187 @@ function registerInspectCommand(program) {
1813
2131
  }
1814
2132
  __name(registerInspectCommand, "registerInspectCommand");
1815
2133
 
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;
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"
2245
+ }
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;
1832
2267
  }
1833
- if (filename.endsWith(".json")) {
1834
- const content = await readFile3(filepath, "utf-8");
1835
- return JSON.parse(content);
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}`);
1836
2295
  }
2296
+ console.log();
1837
2297
  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.`);
1844
- }
1845
- continue;
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
+ `);
1846
2307
  }
1847
- }
1848
- return null;
2308
+ });
1849
2309
  }
1850
- __name(loadKickConfig, "loadKickConfig");
2310
+ __name(registerAddCommand, "registerAddCommand");
1851
2311
 
1852
2312
  // src/cli.ts
1853
2313
  var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
1854
- var pkg = JSON.parse(readFileSync2(join11(__dirname2, "..", "package.json"), "utf-8"));
2314
+ var pkg = JSON.parse(readFileSync2(join14(__dirname2, "..", "package.json"), "utf-8"));
1855
2315
  async function main() {
1856
2316
  const program = new Command();
1857
2317
  program.name("kick").description("KickJS \u2014 A production-grade, decorator-driven Node.js framework").version(pkg.version);
@@ -1861,6 +2321,7 @@ async function main() {
1861
2321
  registerRunCommands(program);
1862
2322
  registerInfoCommand(program);
1863
2323
  registerInspectCommand(program);
2324
+ registerAddCommand(program);
1864
2325
  registerCustomCommands(program, config);
1865
2326
  program.showHelpAfterError();
1866
2327
  await program.parseAsync(process.argv);