@forinda/kickjs-cli 2.3.2 → 3.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.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs-cli v2.3.2
2
+ * @forinda/kickjs-cli v3.0.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -12,9 +12,10 @@ import { Command } from "commander";
12
12
  import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
13
13
  import { basename, dirname, join, relative, resolve, sep } from "node:path";
14
14
  import { fileURLToPath, pathToFileURL } from "node:url";
15
- import { createInterface } from "node:readline";
16
- import { execSync, fork, spawn } from "node:child_process";
15
+ import { execSync, fork, spawn, spawnSync } from "node:child_process";
17
16
  import { access, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
17
+ import * as clack from "@clack/prompts";
18
+ import pc from "picocolors";
18
19
  import pkg from "pluralize";
19
20
  import { arch, platform, release } from "node:os";
20
21
  //#region \0rolldown/runtime.js
@@ -52,8 +53,22 @@ async function fileExists(filePath) {
52
53
  }
53
54
  //#endregion
54
55
  //#region src/generators/templates/project-config.ts
56
+ /** Map of optional package names to their npm package identifiers */
57
+ const PACKAGE_DEPS = {
58
+ auth: "@forinda/kickjs-auth",
59
+ swagger: "@forinda/kickjs-swagger",
60
+ otel: "@forinda/kickjs-otel",
61
+ ws: "@forinda/kickjs-ws",
62
+ queue: "@forinda/kickjs-queue",
63
+ cron: "@forinda/kickjs-cron",
64
+ mailer: "@forinda/kickjs-mailer",
65
+ graphql: "@forinda/kickjs-graphql",
66
+ devtools: "@forinda/kickjs-devtools",
67
+ notifications: "@forinda/kickjs-notifications",
68
+ "multi-tenant": "@forinda/kickjs-multi-tenant"
69
+ };
55
70
  /** Generate package.json with template-aware dependencies */
56
- function generatePackageJson(name, template, kickjsVersion) {
71
+ function generatePackageJson(name, template, kickjsVersion, packages = []) {
57
72
  const baseDeps = {
58
73
  "@forinda/kickjs": kickjsVersion,
59
74
  dotenv: "^17.3.1",
@@ -63,20 +78,15 @@ function generatePackageJson(name, template, kickjsVersion) {
63
78
  pino: "^10.3.1",
64
79
  "pino-pretty": "^13.1.3"
65
80
  };
66
- if (template !== "minimal") {
67
- baseDeps["@forinda/kickjs-swagger"] = kickjsVersion;
68
- baseDeps["@forinda/kickjs-devtools"] = kickjsVersion;
69
- }
70
81
  if (template === "graphql") {
71
82
  baseDeps["@forinda/kickjs-graphql"] = kickjsVersion;
72
83
  baseDeps["graphql"] = "^16.11.0";
73
84
  }
74
- if (template === "cqrs") {
75
- baseDeps["@forinda/kickjs-queue"] = kickjsVersion;
76
- baseDeps["@forinda/kickjs-ws"] = kickjsVersion;
77
- baseDeps["@forinda/kickjs-otel"] = kickjsVersion;
85
+ for (const pkg of packages) {
86
+ const dep = PACKAGE_DEPS[pkg];
87
+ if (dep && !baseDeps[dep]) baseDeps[dep] = kickjsVersion;
78
88
  }
79
- if (template === "ddd") baseDeps["@forinda/kickjs-swagger"] = kickjsVersion;
89
+ if (packages.includes("graphql") && !baseDeps["graphql"]) baseDeps["graphql"] = "^16.11.0";
80
90
  return JSON.stringify({
81
91
  name,
82
92
  version: kickjsVersion.replace("^", ""),
@@ -279,18 +289,32 @@ export default defineConfig({
279
289
  * In production, bootstrap() auto-starts the HTTP server when
280
290
  * `globalThis.__kickjs_httpServer` is not set.
281
291
  */
282
- function generateEntryFile(name, template, version) {
292
+ function generateEntryFile(name, template, version, packages = []) {
283
293
  switch (template) {
284
- case "graphql": return `import 'reflect-metadata'
294
+ case "graphql": {
295
+ const gqlImports = [];
296
+ const gqlAdapters = [];
297
+ if (packages.includes("devtools")) {
298
+ gqlImports.push(`import { DevToolsAdapter } from '@forinda/kickjs-devtools'`);
299
+ gqlAdapters.push(` new DevToolsAdapter(),`);
300
+ }
301
+ if (packages.includes("otel")) {
302
+ gqlImports.push(`import { OtelAdapter } from '@forinda/kickjs-otel'`);
303
+ gqlAdapters.push(` new OtelAdapter({ serviceName: '${name}' }),`);
304
+ }
305
+ if (packages.includes("swagger")) {
306
+ gqlImports.push(`import { SwaggerAdapter } from '@forinda/kickjs-swagger'`);
307
+ gqlAdapters.push(` new SwaggerAdapter({ info: { title: '${name}', version: '${version}' } }),`);
308
+ }
309
+ return `import 'reflect-metadata'
285
310
  // Side-effect import — registers the extended env schema with kickjs
286
311
  // **before** any controller / service / @Value gets resolved. Without
287
312
  // this line ConfigService.get('YOUR_KEY') returns undefined because the
288
313
  // cached schema would still be the base shape. See guide/configuration.
289
314
  import './config'
290
315
  import { bootstrap } from '@forinda/kickjs'
291
- import { DevToolsAdapter } from '@forinda/kickjs-devtools'
292
316
  import { GraphQLAdapter } from '@forinda/kickjs-graphql'
293
- import { modules } from './modules'
317
+ ${gqlImports.length ? gqlImports.join("\n") + "\n" : ""}import { modules } from './modules'
294
318
 
295
319
  // Import your resolvers here
296
320
  // import { UserResolver } from './resolvers/user.resolver'
@@ -299,8 +323,7 @@ import { modules } from './modules'
299
323
  export const app = await bootstrap({
300
324
  modules,
301
325
  adapters: [
302
- new DevToolsAdapter(),
303
- new GraphQLAdapter({
326
+ ${gqlAdapters.length ? gqlAdapters.join("\n") + "\n" : ""} new GraphQLAdapter({
304
327
  resolvers: [/* UserResolver */],
305
328
  // Add custom type definitions here:
306
329
  // typeDefs: userTypeDefs,
@@ -308,51 +331,91 @@ export const app = await bootstrap({
308
331
  ],
309
332
  })
310
333
  `;
311
- case "cqrs": return `import 'reflect-metadata'
334
+ }
335
+ case "cqrs": {
336
+ const cqrsImports = [];
337
+ const cqrsAdapters = [];
338
+ if (packages.includes("otel")) {
339
+ cqrsImports.push(`import { OtelAdapter } from '@forinda/kickjs-otel'`);
340
+ cqrsAdapters.push(` new OtelAdapter({ serviceName: '${name}' }),`);
341
+ }
342
+ if (packages.includes("devtools")) {
343
+ cqrsImports.push(`import { DevToolsAdapter } from '@forinda/kickjs-devtools'`);
344
+ cqrsAdapters.push(` new DevToolsAdapter(),`);
345
+ }
346
+ if (packages.includes("swagger")) {
347
+ cqrsImports.push(`import { SwaggerAdapter } from '@forinda/kickjs-swagger'`);
348
+ cqrsAdapters.push(` new SwaggerAdapter({\n info: { title: '${name}', version: '${version}' },\n }),`);
349
+ }
350
+ if (packages.includes("graphql")) {
351
+ cqrsImports.push(`import { GraphQLAdapter } from '@forinda/kickjs-graphql'`);
352
+ cqrsAdapters.push(` new GraphQLAdapter({ resolvers: [] }),`);
353
+ }
354
+ return `import 'reflect-metadata'
312
355
  // Side-effect import — registers the extended env schema with kickjs
313
356
  // **before** any controller / service / @Value gets resolved. Without
314
357
  // this line ConfigService.get('YOUR_KEY') returns undefined because the
315
358
  // cached schema would still be the base shape. See guide/configuration.
316
359
  import './config'
317
360
  import { bootstrap } from '@forinda/kickjs'
318
- import { DevToolsAdapter } from '@forinda/kickjs-devtools'
319
- import { SwaggerAdapter } from '@forinda/kickjs-swagger'
320
- import { OtelAdapter } from '@forinda/kickjs-otel'
321
361
  // import { WsAdapter } from '@forinda/kickjs-ws'
322
362
  // import { QueueAdapter, BullMQProvider } from '@forinda/kickjs-queue'
323
- import { modules } from './modules'
363
+ ${cqrsImports.length ? cqrsImports.join("\n") + "\n" : ""}import { modules } from './modules'
324
364
 
325
365
  // Export the app for the Vite plugin (dev mode)
326
366
  export const app = await bootstrap({
327
- modules,
328
- adapters: [
329
- new OtelAdapter({ serviceName: '${name}' }),
330
- new DevToolsAdapter(),
331
- new SwaggerAdapter({
332
- info: { title: '${name}', version: '${version}' },
333
- }),
334
- // Uncomment for WebSocket support:
335
- // new WsAdapter(),
336
- // Uncomment when Redis is available:
337
- // new QueueAdapter({
338
- // provider: new BullMQProvider({ host: 'localhost', port: 6379 }),
339
- // }),
340
- ],
367
+ modules,${cqrsImports.length ? `\n adapters: [\n${cqrsAdapters.join("\n")}\n // Uncomment for WebSocket support:\n // new WsAdapter(),\n // Uncomment when Redis is available:\n // new QueueAdapter({\n // provider: new BullMQProvider({ host: 'localhost', port: 6379 }),\n // }),\n ],` : `\n adapters: [\n // Uncomment for WebSocket support:\n // new WsAdapter(),\n // Uncomment when Redis is available:\n // new QueueAdapter({\n // provider: new BullMQProvider({ host: 'localhost', port: 6379 }),\n // }),\n ],`}
341
368
  })
342
369
  `;
343
- case "minimal": return `import 'reflect-metadata'
370
+ }
371
+ case "minimal": {
372
+ const imports = [];
373
+ const adapters = [];
374
+ if (packages.includes("swagger")) {
375
+ imports.push(`import { SwaggerAdapter } from '@forinda/kickjs-swagger'`);
376
+ adapters.push(` new SwaggerAdapter({ info: { title: '${name}', version: '${version}' } }),`);
377
+ }
378
+ if (packages.includes("devtools")) {
379
+ imports.push(`import { DevToolsAdapter } from '@forinda/kickjs-devtools'`);
380
+ adapters.push(` new DevToolsAdapter(),`);
381
+ }
382
+ if (packages.includes("otel")) {
383
+ imports.push(`import { OtelAdapter } from '@forinda/kickjs-otel'`);
384
+ adapters.push(` new OtelAdapter({ serviceName: '${name}' }),`);
385
+ }
386
+ if (packages.includes("graphql")) {
387
+ imports.push(`import { GraphQLAdapter } from '@forinda/kickjs-graphql'`);
388
+ adapters.push(` new GraphQLAdapter({ resolvers: [] }),`);
389
+ }
390
+ return `import 'reflect-metadata'
344
391
  // Side-effect import — registers the extended env schema with kickjs
345
392
  // **before** any controller / service / @Value gets resolved. Without
346
393
  // this line ConfigService.get('YOUR_KEY') returns undefined because the
347
394
  // cached schema would still be the base shape. See guide/configuration.
348
395
  import './config'
349
396
  import { bootstrap } from '@forinda/kickjs'
350
- import { modules } from './modules'
397
+ ${imports.length ? imports.join("\n") + "\n" : ""}import { modules } from './modules'
351
398
 
352
399
  // Export the app for the Vite plugin (dev mode)
353
- export const app = await bootstrap({ modules })
400
+ export const app = await bootstrap({ modules${adapters.length ? `,\n adapters: [\n${adapters.join("\n")}\n ]` : ""} })
354
401
  `;
355
- default: return `import 'reflect-metadata'
402
+ }
403
+ default: {
404
+ const restImports = [];
405
+ const restAdapters = [];
406
+ if (packages.includes("devtools")) {
407
+ restImports.push(`import { DevToolsAdapter } from '@forinda/kickjs-devtools'`);
408
+ restAdapters.push(` new DevToolsAdapter(),`);
409
+ }
410
+ if (packages.includes("swagger")) {
411
+ restImports.push(`import { SwaggerAdapter } from '@forinda/kickjs-swagger'`);
412
+ restAdapters.push(` new SwaggerAdapter({\n info: { title: '${name}', version: '${version}' },\n }),`);
413
+ }
414
+ if (packages.includes("otel")) {
415
+ restImports.push(`import { OtelAdapter } from '@forinda/kickjs-otel'`);
416
+ restAdapters.push(` new OtelAdapter({ serviceName: '${name}' }),`);
417
+ }
418
+ return `import 'reflect-metadata'
356
419
  // Side-effect import — registers the extended env schema with kickjs
357
420
  // **before** any controller / service / @Value gets resolved. Without
358
421
  // this line ConfigService.get('YOUR_KEY') returns undefined because the
@@ -366,19 +429,11 @@ import {
366
429
  helmet,
367
430
  cors,
368
431
  } from '@forinda/kickjs'
369
- import { DevToolsAdapter } from '@forinda/kickjs-devtools'
370
- import { SwaggerAdapter } from '@forinda/kickjs-swagger'
371
- import { modules } from './modules'
432
+ ${restImports.length ? restImports.join("\n") + "\n" : ""}import { modules } from './modules'
372
433
 
373
434
  // Export the app for the Vite plugin (dev mode)
374
435
  export const app = await bootstrap({
375
- modules,
376
- adapters: [
377
- new DevToolsAdapter(),
378
- new SwaggerAdapter({
379
- info: { title: '${name}', version: '${version}' },
380
- }),
381
- ],
436
+ modules,${restAdapters.length ? `\n adapters: [\n${restAdapters.join("\n")}\n ],` : ""}
382
437
  middleware: [
383
438
  helmet(),
384
439
  cors({ origin: '*' }),
@@ -388,6 +443,7 @@ export const app = await bootstrap({
388
443
  ],
389
444
  })
390
445
  `;
446
+ }
391
447
  }
392
448
  }
393
449
  /** Generate src/modules/index.ts module registry */
@@ -1406,11 +1462,11 @@ const cliPkg = JSON.parse(readFileSync(join(__dirname$1, "..", "package.json"),
1406
1462
  const KICKJS_VERSION = `^${cliPkg.version}`;
1407
1463
  /** Scaffold a new KickJS project */
1408
1464
  async function initProject(options) {
1409
- const { name, directory, packageManager = "pnpm", template = "rest", defaultRepo = "inmemory" } = options;
1465
+ const { name, directory, packageManager = "pnpm", template = "rest", defaultRepo = "inmemory", packages = [] } = options;
1410
1466
  const dir = directory;
1411
1467
  const log = (msg) => console.log(` ${msg}`);
1412
1468
  console.log(`\n Creating KickJS project: ${name}\n`);
1413
- await writeFileSafe(join(dir, "package.json"), generatePackageJson(name, template, KICKJS_VERSION));
1469
+ await writeFileSafe(join(dir, "package.json"), generatePackageJson(name, template, KICKJS_VERSION, packages));
1414
1470
  await writeFileSafe(join(dir, "vite.config.ts"), generateViteConfig());
1415
1471
  await writeFileSafe(join(dir, "tsconfig.json"), generateTsConfig());
1416
1472
  await writeFileSafe(join(dir, ".prettierrc"), generatePrettierConfig());
@@ -1420,7 +1476,7 @@ async function initProject(options) {
1420
1476
  await writeFileSafe(join(dir, ".env"), generateEnv());
1421
1477
  await writeFileSafe(join(dir, ".env.example"), generateEnvExample());
1422
1478
  await writeFileSafe(join(dir, "src/config/index.ts"), generateEnvFile());
1423
- await writeFileSafe(join(dir, "src/index.ts"), generateEntryFile(name, template, cliPkg.version));
1479
+ await writeFileSafe(join(dir, "src/index.ts"), generateEntryFile(name, template, cliPkg.version, packages));
1424
1480
  await writeFileSafe(join(dir, "src/modules/index.ts"), generateModulesIndex());
1425
1481
  await writeFileSafe(join(dir, "src/modules/hello/hello.service.ts"), generateHelloService());
1426
1482
  await writeFileSafe(join(dir, "src/modules/hello/hello.controller.ts"), generateHelloController());
@@ -1515,35 +1571,142 @@ async function initProject(options) {
1515
1571
  log("");
1516
1572
  }
1517
1573
  //#endregion
1518
- //#region src/commands/init.ts
1519
- function ask(question, defaultValue) {
1520
- const rl = createInterface({
1521
- input: process.stdin,
1522
- output: process.stdout
1523
- });
1524
- const suffix = defaultValue ? ` (${defaultValue})` : "";
1525
- return new Promise((res) => {
1526
- rl.question(` ${question}${suffix}: `, (answer) => {
1527
- rl.close();
1528
- res(answer.trim() || defaultValue || "");
1529
- });
1530
- });
1574
+ //#region src/utils/colors.ts
1575
+ const METHOD_COLOR_MAP = {
1576
+ GET: pc.green,
1577
+ POST: pc.cyan,
1578
+ PUT: pc.yellow,
1579
+ PATCH: pc.magenta,
1580
+ DELETE: pc.red
1581
+ };
1582
+ /** Color an HTTP method string for terminal display */
1583
+ function httpMethodColor(method) {
1584
+ return (METHOD_COLOR_MAP[method] ?? pc.dim)(method.padEnd(7));
1585
+ }
1586
+ /** Color a severity tag for terminal display (padded to 10 chars) */
1587
+ function severityColor(severity) {
1588
+ const tag = `[${severity}]`.padEnd(10);
1589
+ switch (severity) {
1590
+ case "CRITICAL": return pc.red(tag);
1591
+ case "WARNING": return pc.yellow(tag);
1592
+ case "INFO": return pc.blue(pc.dim(tag));
1593
+ default: return tag;
1594
+ }
1595
+ }
1596
+ pc.green("✓"), pc.red("✖"), pc.yellow("⚠"), pc.blue("ℹ");
1597
+ /** Show branded intro banner */
1598
+ function intro(title) {
1599
+ clack.intro(pc.bgCyan(pc.black(` ${title} `)));
1600
+ }
1601
+ /** Show closing message */
1602
+ function outro(message) {
1603
+ clack.outro(message);
1604
+ }
1605
+ /** Handle cancellation — print message and exit */
1606
+ function handleCancel(value) {
1607
+ if (clack.isCancel(value)) {
1608
+ clack.cancel("Operation cancelled.");
1609
+ process.exit(0);
1610
+ }
1611
+ }
1612
+ /** Text input prompt */
1613
+ async function text(opts) {
1614
+ const value = await clack.text(opts);
1615
+ handleCancel(value);
1616
+ return value;
1531
1617
  }
1532
- async function choose(question, options, defaultIdx = 0) {
1533
- console.log(` ${question}`);
1534
- for (let i = 0; i < options.length; i++) console.log(` ${i === defaultIdx ? ">" : " "} ${i + 1}. ${options[i]}`);
1535
- const answer = await ask("Choose", String(defaultIdx + 1));
1536
- return options[parseInt(answer, 10) - 1] ?? options[defaultIdx];
1618
+ /** Single select prompt */
1619
+ async function select(opts) {
1620
+ const value = await clack.select(opts);
1621
+ handleCancel(value);
1622
+ return value;
1623
+ }
1624
+ /** Multi-select prompt with checkboxes */
1625
+ async function multiSelect(opts) {
1626
+ const value = await clack.multiselect(opts);
1627
+ handleCancel(value);
1628
+ return value;
1629
+ }
1630
+ /** Yes/no confirmation prompt */
1631
+ async function confirm(opts) {
1632
+ const value = await clack.confirm(opts);
1633
+ handleCancel(value);
1634
+ return value;
1537
1635
  }
1538
- async function confirm$1(question, defaultYes = true) {
1539
- const answer = await ask(`${question} (${defaultYes ? "Y/n" : "y/N"})`);
1540
- if (!answer) return defaultYes;
1541
- return answer.toLowerCase().startsWith("y");
1636
+ /** Create a spinner for progress indication */
1637
+ function spinner() {
1638
+ return clack.spinner();
1542
1639
  }
1640
+ /** Log utilities for styled messages inside clack flow */
1641
+ const log = clack.log;
1642
+ //#endregion
1643
+ //#region src/commands/init.ts
1644
+ /** All optional packages available for selection */
1645
+ const OPTIONAL_PACKAGES = [
1646
+ {
1647
+ value: "auth",
1648
+ label: "Auth",
1649
+ hint: "JWT, OAuth, API keys"
1650
+ },
1651
+ {
1652
+ value: "swagger",
1653
+ label: "Swagger",
1654
+ hint: "OpenAPI docs"
1655
+ },
1656
+ {
1657
+ value: "otel",
1658
+ label: "OpenTelemetry",
1659
+ hint: "tracing & metrics"
1660
+ },
1661
+ {
1662
+ value: "ws",
1663
+ label: "WebSocket",
1664
+ hint: "rooms, heartbeat"
1665
+ },
1666
+ {
1667
+ value: "queue",
1668
+ label: "Queue",
1669
+ hint: "BullMQ/RabbitMQ/Kafka"
1670
+ },
1671
+ {
1672
+ value: "cron",
1673
+ label: "Cron",
1674
+ hint: "scheduled jobs"
1675
+ },
1676
+ {
1677
+ value: "mailer",
1678
+ label: "Mailer",
1679
+ hint: "SMTP, Resend, SES"
1680
+ },
1681
+ {
1682
+ value: "graphql",
1683
+ label: "GraphQL",
1684
+ hint: "resolvers, GraphiQL"
1685
+ },
1686
+ {
1687
+ value: "devtools",
1688
+ label: "DevTools",
1689
+ hint: "debug dashboard"
1690
+ },
1691
+ {
1692
+ value: "notifications",
1693
+ label: "Notifications",
1694
+ hint: "email, Slack, Discord"
1695
+ },
1696
+ {
1697
+ value: "multi-tenant",
1698
+ label: "Multi-Tenant",
1699
+ hint: "tenant resolution"
1700
+ }
1701
+ ];
1543
1702
  function registerInitCommand(program) {
1544
- 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 | cqrs | minimal").option("-r, --repo <type>", "Default repository: prisma | drizzle | inmemory | custom").action(async (name, opts) => {
1545
- console.log();
1546
- if (!name) name = await ask("Project name", "my-api");
1703
+ 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 | cqrs | minimal").option("-r, --repo <type>", "Default repository: prisma | drizzle | inmemory | custom").option("--packages <packages>", "Comma-separated packages to include (e.g. auth,swagger,otel)").action(async (name, opts) => {
1704
+ intro("KickJS — Create a new project");
1705
+ if (!name) name = await text({
1706
+ message: "Project name",
1707
+ placeholder: "my-api",
1708
+ defaultValue: "my-api"
1709
+ });
1547
1710
  let directory;
1548
1711
  if (name === ".") {
1549
1712
  directory = resolve(".");
@@ -1552,15 +1715,17 @@ function registerInitCommand(program) {
1552
1715
  if (existsSync(directory)) {
1553
1716
  const entries = readdirSync(directory);
1554
1717
  if (entries.length > 0) {
1555
- if (opts.force) console.log(` Clearing existing files in ${directory}...\n`);
1718
+ if (opts.force) log.warn(`Clearing existing files in ${directory}`);
1556
1719
  else {
1557
- console.log(` Directory "${name}" is not empty:`);
1720
+ log.warn(`Directory "${name}" is not empty:`);
1558
1721
  const shown = entries.slice(0, 5);
1559
- for (const entry of shown) console.log(` - ${entry}`);
1560
- if (entries.length > 5) console.log(` ... and ${entries.length - 5} more`);
1561
- console.log();
1562
- if (!await confirm$1("Remove all existing files and proceed?", false)) {
1563
- console.log(" Aborted.\n");
1722
+ for (const entry of shown) log.message(` - ${entry}`);
1723
+ if (entries.length > 5) log.message(` ... and ${entries.length - 5} more`);
1724
+ if (!await confirm({
1725
+ message: pc.red("Remove all existing files and proceed?"),
1726
+ initialValue: false
1727
+ })) {
1728
+ outro("Aborted.");
1564
1729
  return;
1565
1730
  }
1566
1731
  }
@@ -1571,49 +1736,101 @@ function registerInitCommand(program) {
1571
1736
  }
1572
1737
  }
1573
1738
  let template = opts.template;
1574
- if (!template) {
1575
- template = await choose("Project template:", [
1576
- "REST API (Express + Swagger)",
1577
- "GraphQL API (GraphQL + GraphiQL)",
1578
- "DDD (Domain-Driven Design modules)",
1579
- "CQRS (Commands, Queries, Events + WS/Queue)",
1580
- "Minimal (bare Express)"
1581
- ], 0);
1582
- template = {
1583
- "REST API (Express + Swagger)": "rest",
1584
- "GraphQL API (GraphQL + GraphiQL)": "graphql",
1585
- "DDD (Domain-Driven Design modules)": "ddd",
1586
- "CQRS (Commands, Queries, Events + WS/Queue)": "cqrs",
1587
- "Minimal (bare Express)": "minimal"
1588
- }[template] ?? "rest";
1589
- }
1739
+ if (!template) template = await select({
1740
+ message: "Project template",
1741
+ options: [
1742
+ {
1743
+ value: "rest",
1744
+ label: "REST API",
1745
+ hint: "Express + Swagger"
1746
+ },
1747
+ {
1748
+ value: "graphql",
1749
+ label: "GraphQL API",
1750
+ hint: "GraphQL + GraphiQL"
1751
+ },
1752
+ {
1753
+ value: "ddd",
1754
+ label: "DDD",
1755
+ hint: "Domain-Driven Design modules"
1756
+ },
1757
+ {
1758
+ value: "cqrs",
1759
+ label: "CQRS",
1760
+ hint: "Commands, Queries, Events + WS/Queue"
1761
+ },
1762
+ {
1763
+ value: "minimal",
1764
+ label: "Minimal",
1765
+ hint: "bare Express"
1766
+ }
1767
+ ]
1768
+ });
1590
1769
  let packageManager = opts.pm;
1591
- if (!packageManager) packageManager = await choose("Package manager:", [
1592
- "pnpm",
1593
- "npm",
1594
- "yarn"
1595
- ], 0);
1770
+ if (!packageManager) packageManager = await select({
1771
+ message: "Package manager",
1772
+ options: [
1773
+ {
1774
+ value: "pnpm",
1775
+ label: "pnpm"
1776
+ },
1777
+ {
1778
+ value: "npm",
1779
+ label: "npm"
1780
+ },
1781
+ {
1782
+ value: "yarn",
1783
+ label: "yarn"
1784
+ }
1785
+ ]
1786
+ });
1596
1787
  let defaultRepo = opts.repo;
1597
1788
  if (!defaultRepo) {
1598
- const repoChoice = await choose("Default repository/ORM:", [
1599
- "Prisma",
1600
- "Drizzle",
1601
- "In-Memory",
1602
- "Custom (specify later)"
1603
- ], 0);
1604
- defaultRepo = {
1605
- Prisma: "prisma",
1606
- Drizzle: "drizzle",
1607
- "In-Memory": "inmemory",
1608
- "Custom (specify later)": "custom"
1609
- }[repoChoice] ?? "inmemory";
1610
- if (defaultRepo === "custom") defaultRepo = await ask("Custom repository name", "custom");
1789
+ defaultRepo = await select({
1790
+ message: "Default repository/ORM",
1791
+ options: [
1792
+ {
1793
+ value: "prisma",
1794
+ label: "Prisma"
1795
+ },
1796
+ {
1797
+ value: "drizzle",
1798
+ label: "Drizzle"
1799
+ },
1800
+ {
1801
+ value: "inmemory",
1802
+ label: "In-Memory"
1803
+ },
1804
+ {
1805
+ value: "custom",
1806
+ label: "Custom",
1807
+ hint: "specify later"
1808
+ }
1809
+ ]
1810
+ });
1811
+ if (defaultRepo === "custom") defaultRepo = await text({
1812
+ message: "Custom repository name",
1813
+ defaultValue: "custom"
1814
+ });
1611
1815
  }
1816
+ let selectedPackages;
1817
+ if (opts.packages) selectedPackages = opts.packages.split(",").map((p) => p.trim());
1818
+ else selectedPackages = await multiSelect({
1819
+ message: "Select packages to include",
1820
+ options: [...OPTIONAL_PACKAGES],
1821
+ required: false
1822
+ });
1612
1823
  let initGit;
1613
- if (opts.git === void 0) initGit = await confirm$1("Initialize git repository?", true);
1824
+ if (opts.git === void 0) initGit = await confirm({
1825
+ message: "Initialize git repository?",
1826
+ initialValue: true
1827
+ });
1614
1828
  else initGit = opts.git;
1615
1829
  let installDeps;
1616
- if (opts.install === void 0) installDeps = await confirm$1("Install dependencies?", true);
1830
+ if (opts.install === void 0) installDeps = await confirm({
1831
+ message: "Install dependencies?",
1832
+ initialValue: true
1833
+ });
1617
1834
  else installDeps = opts.install;
1618
1835
  await initProject({
1619
1836
  name,
@@ -1622,8 +1839,10 @@ function registerInitCommand(program) {
1622
1839
  initGit,
1623
1840
  installDeps,
1624
1841
  template,
1625
- defaultRepo
1842
+ defaultRepo,
1843
+ packages: selectedPackages
1626
1844
  });
1845
+ outro(`Done! Next steps: ${pc.cyan(`cd ${name} && ${packageManager} dev`)}`);
1627
1846
  });
1628
1847
  }
1629
1848
  //#endregion
@@ -3446,19 +3665,6 @@ function resolveRepoType(config) {
3446
3665
  if (typeof config === "string") return config;
3447
3666
  return config.name;
3448
3667
  }
3449
- /** Prompt the user for a single-line answer via stdin */
3450
- function promptUser(question) {
3451
- const rl = createInterface({
3452
- input: process.stdin,
3453
- output: process.stdout
3454
- });
3455
- return new Promise((resolve) => {
3456
- rl.question(question, (answer) => {
3457
- rl.close();
3458
- resolve(answer.trim().toLowerCase());
3459
- });
3460
- });
3461
- }
3462
3668
  /**
3463
3669
  * Generate a module — structure depends on the project pattern.
3464
3670
  *
@@ -3488,10 +3694,11 @@ async function generateModule(options) {
3488
3694
  return;
3489
3695
  }
3490
3696
  if (!overwriteAll && await fileExists(fullPath)) {
3491
- const answer = await promptUser(` File already exists: ${relativePath}\n Overwrite? (y/n/a = yes/no/all) `);
3492
- if (answer === "a") overwriteAll = true;
3493
- else if (answer !== "y") {
3494
- console.log(` Skipped: ${relativePath}`);
3697
+ if (!await confirm({
3698
+ message: `File exists: ${pc.dim(relativePath)}. Overwrite?`,
3699
+ initialValue: false
3700
+ })) {
3701
+ log.warn(`Skipped: ${relativePath}`);
3495
3702
  return;
3496
3703
  }
3497
3704
  }
@@ -4051,24 +4258,15 @@ export type ${pascal}DTO = z.infer<typeof ${camel}Schema>
4051
4258
  }
4052
4259
  //#endregion
4053
4260
  //#region src/generators/config.ts
4054
- async function confirm(message) {
4055
- const rl = createInterface({
4056
- input: process.stdin,
4057
- output: process.stdout
4058
- });
4059
- return new Promise((resolve) => {
4060
- rl.question(` ${message} (y/N) `, (answer) => {
4061
- rl.close();
4062
- resolve(answer.trim().toLowerCase() === "y");
4063
- });
4064
- });
4065
- }
4066
4261
  async function generateConfig(options) {
4067
4262
  const filePath = join(options.outDir, "kick.config.ts");
4068
4263
  const modulesDir = options.modulesDir ?? "src/modules";
4069
4264
  const defaultRepo = options.defaultRepo ?? "inmemory";
4070
4265
  if (existsSync(filePath) && !options.force) {
4071
- if (!await confirm("kick.config.ts already exists. Overwrite?")) {
4266
+ if (!await confirm({
4267
+ message: "kick.config.ts already exists. Overwrite?",
4268
+ initialValue: false
4269
+ })) {
4072
4270
  console.log("\n Skipped — existing kick.config.ts preserved.");
4073
4271
  return [];
4074
4272
  }
@@ -4076,8 +4274,15 @@ async function generateConfig(options) {
4076
4274
  await writeFileSafe(filePath, `import { defineConfig } from '@forinda/kickjs-cli'
4077
4275
 
4078
4276
  export default defineConfig({
4079
- modulesDir: '${modulesDir}',
4080
- defaultRepo: '${defaultRepo}',
4277
+ modules: {
4278
+ dir: '${modulesDir}',
4279
+ repo: '${defaultRepo}',
4280
+ pluralize: true,
4281
+ },
4282
+
4283
+ typegen: {
4284
+ schemaValidator: 'zod',
4285
+ },
4081
4286
 
4082
4287
  commands: [
4083
4288
  {
@@ -4107,6 +4312,251 @@ export default defineConfig({
4107
4312
  return [filePath];
4108
4313
  }
4109
4314
  //#endregion
4315
+ //#region src/generators/auth-scaffold.ts
4316
+ /**
4317
+ * Generate a complete auth module with registration, login, logout,
4318
+ * and password hashing. Uses PasswordService and the configured strategy.
4319
+ */
4320
+ async function generateAuthScaffold(options = {}) {
4321
+ const strategy = options.strategy ?? "jwt";
4322
+ const outDir = options.outDir ?? "src/modules/auth";
4323
+ const dtoDir = join(outDir, "dto");
4324
+ const files = [];
4325
+ const modulePath = join(outDir, "auth.module.ts");
4326
+ await writeFileSafe(modulePath, `import { Module } from '@forinda/kickjs'
4327
+ import { AuthController } from './auth.controller'
4328
+ import { AuthService } from './auth.service'
4329
+
4330
+ @Module({
4331
+ controllers: [AuthController],
4332
+ services: [AuthService],
4333
+ })
4334
+ export class AuthModule {}
4335
+ `);
4336
+ files.push(modulePath);
4337
+ const controllerPath = join(outDir, "auth.controller.ts");
4338
+ await writeFileSafe(controllerPath, strategy === "jwt" ? jwtControllerTemplate() : sessionControllerTemplate());
4339
+ files.push(controllerPath);
4340
+ const servicePath = join(outDir, "auth.service.ts");
4341
+ await writeFileSafe(servicePath, strategy === "jwt" ? jwtServiceTemplate() : sessionServiceTemplate());
4342
+ files.push(servicePath);
4343
+ const registerDtoPath = join(dtoDir, "register.dto.ts");
4344
+ await writeFileSafe(registerDtoPath, `import { z } from 'zod'
4345
+
4346
+ export const RegisterDto = z.object({
4347
+ email: z.string().email(),
4348
+ password: z.string().min(8),
4349
+ name: z.string().min(1).optional(),
4350
+ })
4351
+
4352
+ export type RegisterInput = z.infer<typeof RegisterDto>
4353
+ `);
4354
+ files.push(registerDtoPath);
4355
+ const loginDtoPath = join(dtoDir, "login.dto.ts");
4356
+ await writeFileSafe(loginDtoPath, `import { z } from 'zod'
4357
+
4358
+ export const LoginDto = z.object({
4359
+ email: z.string().email(),
4360
+ password: z.string().min(1),
4361
+ })
4362
+
4363
+ export type LoginInput = z.infer<typeof LoginDto>
4364
+ `);
4365
+ files.push(loginDtoPath);
4366
+ const testPath = join(outDir, "auth.test.ts");
4367
+ await writeFileSafe(testPath, `import { describe, it, expect } from 'vitest'
4368
+
4369
+ describe('Auth Module', () => {
4370
+ it.todo('POST /register — creates a new user')
4371
+ it.todo('POST /login — returns token for valid credentials')
4372
+ it.todo('POST /login — rejects invalid credentials')
4373
+ it.todo('POST /logout — invalidates session/token')
4374
+ it.todo('GET /me — returns authenticated user')
4375
+ })
4376
+ `);
4377
+ files.push(testPath);
4378
+ if (options.roleGuards !== false) {
4379
+ const guardPath = join(outDir, "auth.guard.ts");
4380
+ await writeFileSafe(guardPath, `import { Roles } from '@forinda/kickjs-auth'
4381
+
4382
+ /**
4383
+ * Role-based access guard.
4384
+ * Usage: @Roles('admin') on a controller method.
4385
+ *
4386
+ * The AuthAdapter extracts the user's roles from the JWT/session
4387
+ * and the framework checks them automatically.
4388
+ */
4389
+ export const AdminOnly = Roles('admin')
4390
+ export const ManagerOnly = Roles('manager')
4391
+ `);
4392
+ files.push(guardPath);
4393
+ }
4394
+ return files;
4395
+ }
4396
+ function jwtControllerTemplate() {
4397
+ return `import { Controller, Post, Get } from '@forinda/kickjs'
4398
+ import { Authenticated, Public } from '@forinda/kickjs-auth'
4399
+ import type { RequestContext } from '@forinda/kickjs'
4400
+ import { Autowired } from '@forinda/kickjs'
4401
+ import { AuthService } from './auth.service'
4402
+
4403
+ @Controller('/auth')
4404
+ @Authenticated()
4405
+ export class AuthController {
4406
+ @Autowired() private authService!: AuthService
4407
+
4408
+ @Post('/register')
4409
+ @Public()
4410
+ async register(ctx: RequestContext) {
4411
+ const result = await this.authService.register(ctx.body)
4412
+ return ctx.created(result)
4413
+ }
4414
+
4415
+ @Post('/login')
4416
+ @Public()
4417
+ async login(ctx: RequestContext) {
4418
+ const result = await this.authService.login(ctx.body)
4419
+ if (!result) return ctx.badRequest('Invalid credentials')
4420
+ return ctx.json(result)
4421
+ }
4422
+
4423
+ @Post('/logout')
4424
+ async logout(ctx: RequestContext) {
4425
+ return ctx.json({ message: 'Logged out' })
4426
+ }
4427
+
4428
+ @Get('/me')
4429
+ async me(ctx: RequestContext) {
4430
+ return ctx.json({ user: ctx.user })
4431
+ }
4432
+ }
4433
+ `;
4434
+ }
4435
+ function jwtServiceTemplate() {
4436
+ return `import { Service, Autowired } from '@forinda/kickjs'
4437
+ import { PasswordService } from '@forinda/kickjs-auth'
4438
+ import type { RegisterInput } from './dto/register.dto'
4439
+ import type { LoginInput } from './dto/login.dto'
4440
+
4441
+ // TODO: Replace with your User repository
4442
+ const users = new Map<string, { id: string; email: string; name?: string; passwordHash: string }>()
4443
+
4444
+ @Service()
4445
+ export class AuthService {
4446
+ @Autowired() private password!: PasswordService
4447
+
4448
+ async register(input: RegisterInput) {
4449
+ const { email, password, name } = input
4450
+
4451
+ if (users.has(email)) {
4452
+ throw new Error('User already exists')
4453
+ }
4454
+
4455
+ const passwordHash = await this.password.hash(password)
4456
+ const id = crypto.randomUUID()
4457
+ users.set(email, { id, email, name, passwordHash })
4458
+
4459
+ return { id, email, name }
4460
+ }
4461
+
4462
+ async login(input: LoginInput) {
4463
+ const { email, password } = input
4464
+ const user = users.get(email)
4465
+ if (!user) return null
4466
+
4467
+ const valid = await this.password.verify(user.passwordHash, password)
4468
+ if (!valid) return null
4469
+
4470
+ // TODO: Generate JWT token here
4471
+ // const token = jwt.sign({ sub: user.id, email: user.email }, process.env.JWT_SECRET!)
4472
+ return { user: { id: user.id, email: user.email, name: user.name } }
4473
+ }
4474
+ }
4475
+ `;
4476
+ }
4477
+ function sessionControllerTemplate() {
4478
+ return `import { Controller, Post, Get } from '@forinda/kickjs'
4479
+ import { Authenticated, Public } from '@forinda/kickjs-auth'
4480
+ import { sessionLogin, sessionLogout } from '@forinda/kickjs-auth'
4481
+ import type { RequestContext } from '@forinda/kickjs'
4482
+ import { Autowired } from '@forinda/kickjs'
4483
+ import { AuthService } from './auth.service'
4484
+
4485
+ @Controller('/auth')
4486
+ @Authenticated()
4487
+ export class AuthController {
4488
+ @Autowired() private authService!: AuthService
4489
+
4490
+ @Post('/register')
4491
+ @Public()
4492
+ async register(ctx: RequestContext) {
4493
+ const result = await this.authService.register(ctx.body)
4494
+ return ctx.created(result)
4495
+ }
4496
+
4497
+ @Post('/login')
4498
+ @Public()
4499
+ async login(ctx: RequestContext) {
4500
+ const user = await this.authService.login(ctx.body)
4501
+ if (!user) return ctx.badRequest('Invalid credentials')
4502
+ await sessionLogin(ctx.session, user)
4503
+ return ctx.json({ message: 'Logged in', user })
4504
+ }
4505
+
4506
+ @Post('/logout')
4507
+ async logout(ctx: RequestContext) {
4508
+ await sessionLogout(ctx.session)
4509
+ return ctx.json({ message: 'Logged out' })
4510
+ }
4511
+
4512
+ @Get('/me')
4513
+ async me(ctx: RequestContext) {
4514
+ return ctx.json({ user: ctx.user })
4515
+ }
4516
+ }
4517
+ `;
4518
+ }
4519
+ function sessionServiceTemplate() {
4520
+ return `import { Service, Autowired } from '@forinda/kickjs'
4521
+ import { PasswordService } from '@forinda/kickjs-auth'
4522
+ import type { RegisterInput } from './dto/register.dto'
4523
+ import type { LoginInput } from './dto/login.dto'
4524
+
4525
+ // TODO: Replace with your User repository
4526
+ const users = new Map<string, { id: string; email: string; name?: string; passwordHash: string }>()
4527
+
4528
+ @Service()
4529
+ export class AuthService {
4530
+ @Autowired() private password!: PasswordService
4531
+
4532
+ async register(input: RegisterInput) {
4533
+ const { email, password, name } = input
4534
+
4535
+ if (users.has(email)) {
4536
+ throw new Error('User already exists')
4537
+ }
4538
+
4539
+ const passwordHash = await this.password.hash(password)
4540
+ const id = crypto.randomUUID()
4541
+ users.set(email, { id, email, name, passwordHash })
4542
+
4543
+ return { id, email, name }
4544
+ }
4545
+
4546
+ async login(input: LoginInput) {
4547
+ const { email, password } = input
4548
+ const user = users.get(email)
4549
+ if (!user) return null
4550
+
4551
+ const valid = await this.password.verify(user.passwordHash, password)
4552
+ if (!valid) return null
4553
+
4554
+ return { id: user.id, email: user.email, name: user.name }
4555
+ }
4556
+ }
4557
+ `;
4558
+ }
4559
+ //#endregion
4110
4560
  //#region src/generators/resolver.ts
4111
4561
  async function generateResolver(options) {
4112
4562
  const { name, outDir } = options;
@@ -6106,6 +6556,33 @@ function registerGenerateCommand(program) {
6106
6556
  printGenerated(files, dryRun);
6107
6557
  await runPostTypegen(dryRun);
6108
6558
  });
6559
+ gen.command("auth-scaffold").description("Generate a complete auth module (register, login, logout, password hashing)\n Includes controller, service, DTOs, and test stubs.").option("-s, --strategy <type>", "Auth strategy: jwt | session").option("--role-guards", "Generate role-based guards (default: true)").option("--no-role-guards", "Skip role-based guard generation").option("-o, --out <dir>", "Output directory", "src/modules/auth").action(async (opts, cmd) => {
6560
+ const dryRun = isDryRun(cmd);
6561
+ setDryRun(dryRun);
6562
+ let strategy = opts.strategy;
6563
+ if (!strategy) strategy = await select({
6564
+ message: "Auth strategy",
6565
+ options: [{
6566
+ value: "jwt",
6567
+ label: "JWT",
6568
+ hint: "stateless token-based auth"
6569
+ }, {
6570
+ value: "session",
6571
+ label: "Session",
6572
+ hint: "server-side session with cookies"
6573
+ }]
6574
+ });
6575
+ let roleGuards = opts.roleGuards;
6576
+ if (roleGuards === void 0) roleGuards = await confirm({
6577
+ message: "Generate role-based guards?",
6578
+ initialValue: true
6579
+ });
6580
+ printGenerated(await generateAuthScaffold({
6581
+ strategy,
6582
+ outDir: opts.out,
6583
+ roleGuards
6584
+ }), dryRun);
6585
+ });
6109
6586
  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 | prisma", "inmemory").option("-f, --force", "Overwrite existing kick.config.ts without prompting").action(async (opts, cmd) => {
6110
6587
  const dryRun = isDryRun(cmd);
6111
6588
  setDryRun(dryRun);
@@ -6119,13 +6596,43 @@ function registerGenerateCommand(program) {
6119
6596
  }
6120
6597
  //#endregion
6121
6598
  //#region src/utils/shell.ts
6122
- /** Run a shell command synchronously, printing output */
6123
- function runShellCommand(command, cwd) {
6599
+ /**
6600
+ * Run a shell command synchronously, printing output.
6601
+ *
6602
+ * On Windows, `execSync` spawns via `cmd.exe` by default, which means
6603
+ * POSIX-style inline env prefixes like `FOO=bar node app.js` do NOT work.
6604
+ * Callers that need environment variables should pass them in the `env`
6605
+ * option instead of prepending them to the command string — see
6606
+ * `runNodeWithEnv` for the cross-platform helper that avoids a shell
6607
+ * entirely.
6608
+ */
6609
+ function runShellCommand(command, cwd, env) {
6124
6610
  execSync(command, {
6125
6611
  cwd,
6126
- stdio: "inherit"
6612
+ stdio: "inherit",
6613
+ env: env ? {
6614
+ ...process.env,
6615
+ ...env
6616
+ } : process.env
6127
6617
  });
6128
6618
  }
6619
+ /**
6620
+ * Cross-platform way to launch a Node.js process with a set of
6621
+ * environment variables. Uses `spawnSync` with an argument array so no
6622
+ * shell is involved — the `VAR=value node ...` POSIX prefix syntax that
6623
+ * `runShellCommand` relied on breaks on cmd.exe and PowerShell.
6624
+ */
6625
+ function runNodeWithEnv(entry, env, cwd) {
6626
+ const result = spawnSync(process.execPath, [entry], {
6627
+ cwd,
6628
+ stdio: "inherit",
6629
+ env: {
6630
+ ...process.env,
6631
+ ...env
6632
+ }
6633
+ });
6634
+ if (result.status !== 0) process.exit(result.status ?? 1);
6635
+ }
6129
6636
  //#endregion
6130
6637
  //#region src/commands/run.ts
6131
6638
  /**
@@ -6160,7 +6667,7 @@ async function startDevServer(_entry, port) {
6160
6667
  console.warn(` kick typegen: skipped (${err?.message ?? err})`);
6161
6668
  }
6162
6669
  const { createRequire } = await import("node:module");
6163
- const { createServer } = await import(createRequire(resolve("package.json")).resolve("vite"));
6670
+ const { createServer } = await import(pathToFileURL(createRequire(resolve("package.json")).resolve("vite")).href);
6164
6671
  const server = await createServer({
6165
6672
  configFile: resolve("vite.config.ts"),
6166
6673
  server: { port: port ? parseInt(port, 10) : void 0 }
@@ -6210,7 +6717,7 @@ function registerRunCommands(program) {
6210
6717
  program.command("build").description("Build for production via Vite").action(async () => {
6211
6718
  console.log("\n Building for production...\n");
6212
6719
  const { createRequire } = await import("node:module");
6213
- const { build } = await import(createRequire(resolve("package.json")).resolve("vite"));
6720
+ const { build } = await import(pathToFileURL(createRequire(resolve("package.json")).resolve("vite")).href);
6214
6721
  await build({ configFile: resolve("vite.config.ts") });
6215
6722
  const copyDirs = (await loadKickConfig(process.cwd()))?.copyDirs ?? [];
6216
6723
  if (copyDirs.length > 0) {
@@ -6232,9 +6739,9 @@ function registerRunCommands(program) {
6232
6739
  console.log("\n Build complete.\n");
6233
6740
  });
6234
6741
  program.command("start").description("Start production server").option("-e, --entry <file>", "Entry file", "dist/index.js").option("-p, --port <port>", "Port number").action((opts) => {
6235
- const envVars = ["NODE_ENV=production"];
6236
- if (opts.port) envVars.push(`PORT=${opts.port}`);
6237
- runShellCommand(`${envVars.join(" ")} node ${opts.entry}`);
6742
+ const env = { NODE_ENV: "production" };
6743
+ if (opts.port) env.PORT = String(opts.port);
6744
+ runNodeWithEnv(opts.entry, env);
6238
6745
  });
6239
6746
  program.command("dev:debug").description("Start dev server with Node.js inspector attached").option("-e, --entry <file>", "Entry file", "src/index.ts").option("-p, --port <port>", "Port number").option("--inspect-port <port>", "Inspector port", "9229").action(async (opts) => {
6240
6747
  const inspectPort = opts.inspectPort ?? "9229";
@@ -6348,26 +6855,7 @@ function registerSingleCommand(program, def) {
6348
6855
  }
6349
6856
  //#endregion
6350
6857
  //#region src/commands/inspect.ts
6351
- const esc = (code) => `\x1b[${code}m`;
6352
- const reset = esc("0");
6353
- const bold = (s) => `${esc("1")}${s}${reset}`;
6354
- const dim = (s) => `${esc("2")}${s}${reset}`;
6355
- const green = (s) => `${esc("32")}${s}${reset}`;
6356
- const red = (s) => `${esc("31")}${s}${reset}`;
6357
- const yellow = (s) => `${esc("33")}${s}${reset}`;
6358
- const cyan = (s) => `${esc("36")}${s}${reset}`;
6359
- const magenta = (s) => `${esc("35")}${s}${reset}`;
6360
- const blue = (s) => `${esc("34")}${s}${reset}`;
6361
- const METHOD_COLORS = {
6362
- GET: green,
6363
- POST: cyan,
6364
- PUT: yellow,
6365
- PATCH: magenta,
6366
- DELETE: red
6367
- };
6368
- function colorMethod(method) {
6369
- return (METHOD_COLORS[method] ?? dim)(method.padEnd(7));
6370
- }
6858
+ const { bold, dim, green, red, yellow, cyan, blue } = pc;
6371
6859
  function formatUptime(seconds) {
6372
6860
  const d = Math.floor(seconds / 86400);
6373
6861
  const h = Math.floor(seconds % 86400 / 3600);
@@ -6434,7 +6922,7 @@ function printSummary(base, data) {
6434
6922
  console.log(` ${dim("METHOD")} ${dim("PATH".padEnd(36))} ${dim("CONTROLLER")}`);
6435
6923
  for (const r of routes.routes) {
6436
6924
  const path = r.path.length > 36 ? r.path.slice(0, 33) + "..." : r.path.padEnd(36);
6437
- console.log(` ${colorMethod(r.method)} ${path} ${blue(r.controller)}.${dim(r.handler)}`);
6925
+ console.log(` ${httpMethodColor(r.method)} ${path} ${blue(r.controller)}.${dim(r.handler)}`);
6438
6926
  }
6439
6927
  }
6440
6928
  console.log(line);
@@ -7384,18 +7872,6 @@ function findBin(startDir, name) {
7384
7872
  }
7385
7873
  //#endregion
7386
7874
  //#region src/generators/remove-module.ts
7387
- function promptConfirm(message) {
7388
- const rl = createInterface({
7389
- input: process.stdin,
7390
- output: process.stdout
7391
- });
7392
- return new Promise((resolve) => {
7393
- rl.question(` ${message} (y/N) `, (answer) => {
7394
- rl.close();
7395
- resolve(answer.trim().toLowerCase() === "y");
7396
- });
7397
- });
7398
- }
7399
7875
  /**
7400
7876
  * Remove a module — deletes its directory and unregisters it from the modules index.
7401
7877
  */
@@ -7411,7 +7887,10 @@ async function removeModule(options) {
7411
7887
  return;
7412
7888
  }
7413
7889
  if (!force) {
7414
- if (!await promptConfirm(`Delete module '${plural}' at ${moduleDir}? This cannot be undone.`)) {
7890
+ if (!await confirm({
7891
+ message: pc.red(`Delete module '${plural}' at ${moduleDir}? This cannot be undone.`),
7892
+ initialValue: false
7893
+ })) {
7415
7894
  console.log("\n Cancelled.\n");
7416
7895
  return;
7417
7896
  }
@@ -7425,7 +7904,7 @@ async function removeModule(options) {
7425
7904
  if (await fileExists(indexPath)) {
7426
7905
  let content = await readFile(indexPath, "utf-8");
7427
7906
  const originalContent = content;
7428
- const importPattern = new RegExp(`^import\\s*\\{\\s*${pascal}Module\\s*\\}\\s*from\\s*['\\./${plural}']+.*\\n?`, "gm");
7907
+ const importPattern = new RegExp(`^import\\s*\\{\\s*${pascal}Module\\s*\\}\\s*from\\s*['"][^'"]*${plural}['"].*\\n?`, "gm");
7429
7908
  content = content.replace(importPattern, "");
7430
7909
  content = content.replace(new RegExp(`\\s*,?\\s*${pascal}Module\\s*,?`, "g"), (match) => {
7431
7910
  const startsWithComma = match.trimStart().startsWith(",");
@@ -7516,6 +7995,176 @@ function registerTypegenCommand(program) {
7516
7995
  });
7517
7996
  }
7518
7997
  //#endregion
7998
+ //#region src/commands/check.ts
7999
+ /** Recursively collect all .ts files under a directory */
8000
+ function collectTsFiles(dir) {
8001
+ const files = [];
8002
+ if (!existsSync(dir)) return files;
8003
+ const entries = readdirSync(dir, { withFileTypes: true });
8004
+ for (const entry of entries) {
8005
+ const fullPath = join(dir, entry.name);
8006
+ if (entry.isDirectory()) {
8007
+ if ([
8008
+ "node_modules",
8009
+ "dist",
8010
+ ".kickjs",
8011
+ ".git"
8012
+ ].includes(entry.name)) continue;
8013
+ files.push(...collectTsFiles(fullPath));
8014
+ } else if (entry.isFile() && /\.tsx?$/.test(entry.name) && !entry.name.endsWith(".d.ts")) files.push(fullPath);
8015
+ }
8016
+ return files;
8017
+ }
8018
+ /** Read a file safely, returning empty string on failure */
8019
+ function safeRead(filepath) {
8020
+ try {
8021
+ return readFileSync(filepath, "utf-8");
8022
+ } catch {
8023
+ return "";
8024
+ }
8025
+ }
8026
+ const WEAK_SECRETS = new Set([
8027
+ "secret",
8028
+ "changeme",
8029
+ "password",
8030
+ "test",
8031
+ "default",
8032
+ ""
8033
+ ]);
8034
+ function checkJwtSecret(cwd, sourceContents) {
8035
+ const envContent = safeRead(join(cwd, ".env"));
8036
+ if (envContent) {
8037
+ const match = envContent.match(/^JWT_SECRET\s*=\s*['"]?([^'"\n]*)['"]?/m);
8038
+ if (match) {
8039
+ const value = match[1].trim();
8040
+ if (WEAK_SECRETS.has(value.toLowerCase()) || value.length < 32) return {
8041
+ severity: "CRITICAL",
8042
+ message: "JWT_SECRET appears to be a default value or too short (< 32 chars) — change it"
8043
+ };
8044
+ }
8045
+ }
8046
+ for (const content of sourceContents) for (const pattern of [/JWT_SECRET['"]?\s*[:=]\s*['"]?(secret|changeme|password|test|default)['"]?/i, /secret\s*[:=]\s*['"]?(secret|changeme|password|test|default)['"]?/i]) if (pattern.test(content)) return {
8047
+ severity: "CRITICAL",
8048
+ message: "JWT_SECRET appears to be a default value in source code — use an environment variable"
8049
+ };
8050
+ return null;
8051
+ }
8052
+ function checkCorsOrigin(sourceContents) {
8053
+ for (const content of sourceContents) if (/cors\s*\(/.test(content) && /origin\s*:\s*['"]\*['"]/.test(content)) return {
8054
+ severity: "CRITICAL",
8055
+ message: "CORS origin is '*' — restrict to your domains"
8056
+ };
8057
+ return null;
8058
+ }
8059
+ function checkRateLimiting(sourceContents) {
8060
+ for (const content of sourceContents) if (/rateLimit/i.test(content) || /@RateLimit/i.test(content)) return null;
8061
+ return {
8062
+ severity: "WARNING",
8063
+ message: "No rate limiting detected — add rateLimit() middleware or @RateLimit decorator"
8064
+ };
8065
+ }
8066
+ function checkNodeEnv() {
8067
+ if (process.env.NODE_ENV !== "production") return {
8068
+ severity: "WARNING",
8069
+ message: `NODE_ENV is '${process.env.NODE_ENV ?? "undefined"}', not 'production'`
8070
+ };
8071
+ return null;
8072
+ }
8073
+ function checkTokenStore(sourceContents) {
8074
+ let hasTokenStore = false;
8075
+ let usesMemoryStore = false;
8076
+ for (const content of sourceContents) {
8077
+ if (/tokenStore/i.test(content)) hasTokenStore = true;
8078
+ if (/MemoryTokenStore/i.test(content)) usesMemoryStore = true;
8079
+ }
8080
+ if (usesMemoryStore) return {
8081
+ severity: "WARNING",
8082
+ message: "MemoryTokenStore detected — use a persistent store (Redis, DB) for production deployments"
8083
+ };
8084
+ if (!hasTokenStore) return {
8085
+ severity: "WARNING",
8086
+ message: "No token revocation store detected — consider adding one for auth token management"
8087
+ };
8088
+ return null;
8089
+ }
8090
+ function checkHelmet(sourceContents) {
8091
+ for (const content of sourceContents) if (/helmet\s*\(/.test(content)) {
8092
+ if (/security\s*\.\s*helmet\s*.*false/.test(content)) return {
8093
+ severity: "WARNING",
8094
+ message: "Helmet security headers are disabled — enable them for production"
8095
+ };
8096
+ return {
8097
+ severity: "INFO",
8098
+ message: "Helmet security headers active"
8099
+ };
8100
+ }
8101
+ return {
8102
+ severity: "WARNING",
8103
+ message: "Helmet not detected — add helmet() middleware for security headers"
8104
+ };
8105
+ }
8106
+ function checkAuthAdapter(sourceContents) {
8107
+ for (const content of sourceContents) if (/AuthAdapter/i.test(content)) return {
8108
+ severity: "INFO",
8109
+ message: "AuthAdapter configured"
8110
+ };
8111
+ return {
8112
+ severity: "INFO",
8113
+ message: "No AuthAdapter detected — add one if your app requires authentication"
8114
+ };
8115
+ }
8116
+ function runDeployChecks(cwd) {
8117
+ const sourceContents = collectTsFiles(join(cwd, "src")).map((f) => safeRead(f));
8118
+ const results = [];
8119
+ const jwtResult = checkJwtSecret(cwd, sourceContents);
8120
+ if (jwtResult) results.push(jwtResult);
8121
+ const corsResult = checkCorsOrigin(sourceContents);
8122
+ if (corsResult) results.push(corsResult);
8123
+ const rateLimitResult = checkRateLimiting(sourceContents);
8124
+ if (rateLimitResult) results.push(rateLimitResult);
8125
+ const nodeEnvResult = checkNodeEnv();
8126
+ if (nodeEnvResult) results.push(nodeEnvResult);
8127
+ const tokenStoreResult = checkTokenStore(sourceContents);
8128
+ if (tokenStoreResult) results.push(tokenStoreResult);
8129
+ results.push(checkHelmet(sourceContents));
8130
+ results.push(checkAuthAdapter(sourceContents));
8131
+ return results;
8132
+ }
8133
+ function registerCheckCommand(program) {
8134
+ program.command("check").description("Audit project for common issues").option("--deploy", "Run production readiness checks").action((opts) => {
8135
+ if (!opts.deploy) {
8136
+ console.log("\n Usage: kick check --deploy\n\n Available checks:\n --deploy Audit for production readiness (security, config, best practices)\n");
8137
+ return;
8138
+ }
8139
+ const cwd = process.cwd();
8140
+ intro("KickJS Deploy Check");
8141
+ const s = spinner();
8142
+ s.start("Scanning project...");
8143
+ const results = runDeployChecks(cwd);
8144
+ s.stop("Scan complete");
8145
+ const order = {
8146
+ CRITICAL: 0,
8147
+ WARNING: 1,
8148
+ INFO: 2
8149
+ };
8150
+ results.sort((a, b) => order[a.severity] - order[b.severity]);
8151
+ for (const r of results) log.message(`${severityColor(r.severity)} ${r.message}`);
8152
+ const critical = results.filter((r) => r.severity === "CRITICAL").length;
8153
+ const warnings = results.filter((r) => r.severity === "WARNING").length;
8154
+ const info = results.filter((r) => r.severity === "INFO").length;
8155
+ const warnLabel = warnings === 1 ? "warning" : "warnings";
8156
+ const summary = [
8157
+ critical > 0 ? pc.red(`${critical} critical`) : `${critical} critical`,
8158
+ warnings > 0 ? pc.yellow(`${warnings} ${warnLabel}`) : `${warnings} ${warnLabel}`,
8159
+ `${info} info`
8160
+ ].join(", ");
8161
+ if (critical > 0) {
8162
+ outro(pc.red(`${summary} — fix critical issues before deploying`));
8163
+ process.exit(1);
8164
+ } else outro(pc.green(`${summary} — looking good!`));
8165
+ });
8166
+ }
8167
+ //#endregion
7519
8168
  //#region src/cli.ts
7520
8169
  const __dirname = dirname(fileURLToPath(import.meta.url));
7521
8170
  const pkg$1 = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
@@ -7535,6 +8184,7 @@ async function main() {
7535
8184
  registerTinkerCommand(program);
7536
8185
  registerRemoveCommand(program);
7537
8186
  registerTypegenCommand(program);
8187
+ registerCheckCommand(program);
7538
8188
  registerCustomCommands(program, config);
7539
8189
  program.showHelpAfterError();
7540
8190
  await program.parseAsync(process.argv);