@forinda/kickjs-cli 2.3.3 → 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/README.md +19 -0
- package/dist/cli.mjs +810 -190
- package/dist/index.d.mts +1 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +133 -72
- package/dist/index.mjs.map +1 -1
- package/dist/{typegen-DdX5N7XT.mjs → typegen-BL0O61s-.mjs} +2 -2
- package/dist/{typegen-DdX5N7XT.mjs.map → typegen-BL0O61s-.mjs.map} +1 -1
- package/package.json +4 -2
package/dist/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs-cli
|
|
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
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
|
-
|
|
75
|
-
|
|
76
|
-
baseDeps[
|
|
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 (
|
|
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":
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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/
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
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
|
+
}
|
|
1531
1611
|
}
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
return
|
|
1612
|
+
/** Text input prompt */
|
|
1613
|
+
async function text(opts) {
|
|
1614
|
+
const value = await clack.text(opts);
|
|
1615
|
+
handleCancel(value);
|
|
1616
|
+
return value;
|
|
1617
|
+
}
|
|
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
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
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
|
-
|
|
1546
|
-
if (!name) name = await
|
|
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)
|
|
1718
|
+
if (opts.force) log.warn(`Clearing existing files in ${directory}`);
|
|
1556
1719
|
else {
|
|
1557
|
-
|
|
1720
|
+
log.warn(`Directory "${name}" is not empty:`);
|
|
1558
1721
|
const shown = entries.slice(0, 5);
|
|
1559
|
-
for (const entry of shown)
|
|
1560
|
-
if (entries.length > 5)
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
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
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
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
|
|
1592
|
-
"
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
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
|
-
|
|
1599
|
-
"
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
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(
|
|
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
|
-
|
|
4080
|
-
|
|
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);
|
|
@@ -6378,26 +6855,7 @@ function registerSingleCommand(program, def) {
|
|
|
6378
6855
|
}
|
|
6379
6856
|
//#endregion
|
|
6380
6857
|
//#region src/commands/inspect.ts
|
|
6381
|
-
const
|
|
6382
|
-
const reset = esc("0");
|
|
6383
|
-
const bold = (s) => `${esc("1")}${s}${reset}`;
|
|
6384
|
-
const dim = (s) => `${esc("2")}${s}${reset}`;
|
|
6385
|
-
const green = (s) => `${esc("32")}${s}${reset}`;
|
|
6386
|
-
const red = (s) => `${esc("31")}${s}${reset}`;
|
|
6387
|
-
const yellow = (s) => `${esc("33")}${s}${reset}`;
|
|
6388
|
-
const cyan = (s) => `${esc("36")}${s}${reset}`;
|
|
6389
|
-
const magenta = (s) => `${esc("35")}${s}${reset}`;
|
|
6390
|
-
const blue = (s) => `${esc("34")}${s}${reset}`;
|
|
6391
|
-
const METHOD_COLORS = {
|
|
6392
|
-
GET: green,
|
|
6393
|
-
POST: cyan,
|
|
6394
|
-
PUT: yellow,
|
|
6395
|
-
PATCH: magenta,
|
|
6396
|
-
DELETE: red
|
|
6397
|
-
};
|
|
6398
|
-
function colorMethod(method) {
|
|
6399
|
-
return (METHOD_COLORS[method] ?? dim)(method.padEnd(7));
|
|
6400
|
-
}
|
|
6858
|
+
const { bold, dim, green, red, yellow, cyan, blue } = pc;
|
|
6401
6859
|
function formatUptime(seconds) {
|
|
6402
6860
|
const d = Math.floor(seconds / 86400);
|
|
6403
6861
|
const h = Math.floor(seconds % 86400 / 3600);
|
|
@@ -6464,7 +6922,7 @@ function printSummary(base, data) {
|
|
|
6464
6922
|
console.log(` ${dim("METHOD")} ${dim("PATH".padEnd(36))} ${dim("CONTROLLER")}`);
|
|
6465
6923
|
for (const r of routes.routes) {
|
|
6466
6924
|
const path = r.path.length > 36 ? r.path.slice(0, 33) + "..." : r.path.padEnd(36);
|
|
6467
|
-
console.log(` ${
|
|
6925
|
+
console.log(` ${httpMethodColor(r.method)} ${path} ${blue(r.controller)}.${dim(r.handler)}`);
|
|
6468
6926
|
}
|
|
6469
6927
|
}
|
|
6470
6928
|
console.log(line);
|
|
@@ -7414,18 +7872,6 @@ function findBin(startDir, name) {
|
|
|
7414
7872
|
}
|
|
7415
7873
|
//#endregion
|
|
7416
7874
|
//#region src/generators/remove-module.ts
|
|
7417
|
-
function promptConfirm(message) {
|
|
7418
|
-
const rl = createInterface({
|
|
7419
|
-
input: process.stdin,
|
|
7420
|
-
output: process.stdout
|
|
7421
|
-
});
|
|
7422
|
-
return new Promise((resolve) => {
|
|
7423
|
-
rl.question(` ${message} (y/N) `, (answer) => {
|
|
7424
|
-
rl.close();
|
|
7425
|
-
resolve(answer.trim().toLowerCase() === "y");
|
|
7426
|
-
});
|
|
7427
|
-
});
|
|
7428
|
-
}
|
|
7429
7875
|
/**
|
|
7430
7876
|
* Remove a module — deletes its directory and unregisters it from the modules index.
|
|
7431
7877
|
*/
|
|
@@ -7441,7 +7887,10 @@ async function removeModule(options) {
|
|
|
7441
7887
|
return;
|
|
7442
7888
|
}
|
|
7443
7889
|
if (!force) {
|
|
7444
|
-
if (!await
|
|
7890
|
+
if (!await confirm({
|
|
7891
|
+
message: pc.red(`Delete module '${plural}' at ${moduleDir}? This cannot be undone.`),
|
|
7892
|
+
initialValue: false
|
|
7893
|
+
})) {
|
|
7445
7894
|
console.log("\n Cancelled.\n");
|
|
7446
7895
|
return;
|
|
7447
7896
|
}
|
|
@@ -7455,7 +7904,7 @@ async function removeModule(options) {
|
|
|
7455
7904
|
if (await fileExists(indexPath)) {
|
|
7456
7905
|
let content = await readFile(indexPath, "utf-8");
|
|
7457
7906
|
const originalContent = content;
|
|
7458
|
-
const importPattern = new RegExp(`^import\\s*\\{\\s*${pascal}Module\\s*\\}\\s*from\\s*['
|
|
7907
|
+
const importPattern = new RegExp(`^import\\s*\\{\\s*${pascal}Module\\s*\\}\\s*from\\s*['"][^'"]*${plural}['"].*\\n?`, "gm");
|
|
7459
7908
|
content = content.replace(importPattern, "");
|
|
7460
7909
|
content = content.replace(new RegExp(`\\s*,?\\s*${pascal}Module\\s*,?`, "g"), (match) => {
|
|
7461
7910
|
const startsWithComma = match.trimStart().startsWith(",");
|
|
@@ -7546,6 +7995,176 @@ function registerTypegenCommand(program) {
|
|
|
7546
7995
|
});
|
|
7547
7996
|
}
|
|
7548
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
|
|
7549
8168
|
//#region src/cli.ts
|
|
7550
8169
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7551
8170
|
const pkg$1 = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
@@ -7565,6 +8184,7 @@ async function main() {
|
|
|
7565
8184
|
registerTinkerCommand(program);
|
|
7566
8185
|
registerRemoveCommand(program);
|
|
7567
8186
|
registerTypegenCommand(program);
|
|
8187
|
+
registerCheckCommand(program);
|
|
7568
8188
|
registerCustomCommands(program, config);
|
|
7569
8189
|
program.showHelpAfterError();
|
|
7570
8190
|
await program.parseAsync(process.argv);
|