@forinda/kickjs-cli 3.0.8 → 3.1.1

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 v3.0.8
2
+ * @forinda/kickjs-cli v3.1.1
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -577,11 +577,14 @@ export class HelloModule implements AppModule {
577
577
  `;
578
578
  }
579
579
  /** Generate kick.config.ts CLI configuration */
580
- function generateKickConfig(template, defaultRepo = "inmemory") {
580
+ function generateKickConfig(template, defaultRepo = "inmemory", packageManager = "pnpm") {
581
581
  return `import { defineConfig } from '@forinda/kickjs-cli'
582
582
 
583
583
  export default defineConfig({
584
584
  pattern: '${template}',
585
+ // Pinned so \`kick add\` and other dep-installing commands always use the
586
+ // project's intended package manager, regardless of which lockfile exists.
587
+ packageManager: '${packageManager}',
585
588
  modules: {
586
589
  dir: 'src/modules',
587
590
  repo: ${[
@@ -1482,7 +1485,7 @@ async function initProject(options) {
1482
1485
  await writeFileSafe(join(dir, "src/modules/hello/hello.controller.ts"), generateHelloController());
1483
1486
  await writeFileSafe(join(dir, "src/modules/hello/hello.module.ts"), generateHelloModule());
1484
1487
  if (template === "graphql") await writeFileSafe(join(dir, "src/resolvers/.gitkeep"), "");
1485
- await writeFileSafe(join(dir, "kick.config.ts"), generateKickConfig(template, defaultRepo));
1488
+ await writeFileSafe(join(dir, "kick.config.ts"), generateKickConfig(template, defaultRepo, packageManager));
1486
1489
  await writeFileSafe(join(dir, "vitest.config.ts"), generateVitestConfig());
1487
1490
  await writeFileSafe(join(dir, "README.md"), generateReadme(name, template, packageManager));
1488
1491
  await writeFileSafe(join(dir, "CLAUDE.md"), generateClaude(name, template, packageManager));
@@ -1700,7 +1703,7 @@ const OPTIONAL_PACKAGES = [
1700
1703
  }
1701
1704
  ];
1702
1705
  function registerInitCommand(program) {
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) => {
1706
+ 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 | bun").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
1707
  intro("KickJS — Create a new project");
1705
1708
  if (!name) name = await text({
1706
1709
  message: "Project name",
@@ -1781,6 +1784,10 @@ function registerInitCommand(program) {
1781
1784
  {
1782
1785
  value: "yarn",
1783
1786
  label: "yarn"
1787
+ },
1788
+ {
1789
+ value: "bun",
1790
+ label: "bun"
1784
1791
  }
1785
1792
  ]
1786
1793
  });
@@ -3329,7 +3336,7 @@ export class Prisma${pascal}Repository implements I${pascal}Repository {
3329
3336
  //#region src/generators/patterns/minimal.ts
3330
3337
  async function generateMinimalFiles(ctx) {
3331
3338
  const { pascal, kebab, plural, write } = ctx;
3332
- await write("index.ts", generateMinimalModuleIndex({
3339
+ await write(`${kebab}.module.ts`, generateMinimalModuleIndex({
3333
3340
  pascal,
3334
3341
  kebab,
3335
3342
  plural
@@ -3352,7 +3359,7 @@ export class ${pascal}Controller {
3352
3359
  //#region src/generators/patterns/rest.ts
3353
3360
  async function generateRestFiles(ctx) {
3354
3361
  const { pascal, kebab, plural, pluralPascal, repo, noTests, prismaClientPath, write } = ctx;
3355
- await write("index.ts", generateRestModuleIndex({
3362
+ await write(`${kebab}.module.ts`, generateRestModuleIndex({
3356
3363
  pascal,
3357
3364
  kebab,
3358
3365
  plural,
@@ -3448,7 +3455,7 @@ async function generateRestFiles(ctx) {
3448
3455
  //#region src/generators/patterns/cqrs.ts
3449
3456
  async function generateCqrsFiles(ctx) {
3450
3457
  const { pascal, kebab, plural, pluralPascal, repo, noTests, prismaClientPath, write } = ctx;
3451
- await write("index.ts", generateCqrsModuleIndex({
3458
+ await write(`${kebab}.module.ts`, generateCqrsModuleIndex({
3452
3459
  pascal,
3453
3460
  kebab,
3454
3461
  plural,
@@ -3557,7 +3564,7 @@ async function generateCqrsFiles(ctx) {
3557
3564
  //#region src/generators/patterns/ddd.ts
3558
3565
  async function generateDddFiles(ctx) {
3559
3566
  const { pascal, kebab, plural, pluralPascal, repo, noEntity, noTests, prismaClientPath, write } = ctx;
3560
- await write("index.ts", generateModuleIndex({
3567
+ await write(`${kebab}.module.ts`, generateModuleIndex({
3561
3568
  pascal,
3562
3569
  kebab,
3563
3570
  plural,
@@ -3732,22 +3739,24 @@ async function generateModule(options) {
3732
3739
  await generateDddFiles(ctx);
3733
3740
  break;
3734
3741
  }
3735
- if (!dryRun) await autoRegisterModule$1(modulesDir, pascal, plural);
3742
+ if (!dryRun) await autoRegisterModule$1(modulesDir, pascal, plural, kebab);
3736
3743
  return files;
3737
3744
  }
3738
3745
  /** Add the new module to src/modules/index.ts */
3739
- async function autoRegisterModule$1(modulesDir, pascal, plural) {
3746
+ async function autoRegisterModule$1(modulesDir, pascal, plural, kebab) {
3740
3747
  const indexPath = join(modulesDir, "index.ts");
3741
- if (!await fileExists(indexPath)) {
3748
+ const exists = await fileExists(indexPath);
3749
+ const importPath = `./${plural}/${kebab}.module`;
3750
+ if (!exists) {
3742
3751
  await writeFileSafe(indexPath, `import type { AppModuleClass } from '@forinda/kickjs'
3743
- import { ${pascal}Module } from './${plural}'
3752
+ import { ${pascal}Module } from '${importPath}'
3744
3753
 
3745
3754
  export const modules: AppModuleClass[] = [${pascal}Module]
3746
3755
  `);
3747
3756
  return;
3748
3757
  }
3749
3758
  let content = await readFile(indexPath, "utf-8");
3750
- const importLine = `import { ${pascal}Module } from './${plural}'`;
3759
+ const importLine = `import { ${pascal}Module } from '${importPath}'`;
3751
3760
  if (!content.includes(`${pascal}Module`)) {
3752
3761
  const lastImportIdx = content.lastIndexOf("import ");
3753
3762
  if (lastImportIdx !== -1) {
@@ -4810,7 +4819,7 @@ async function generateScaffold(options) {
4810
4819
  await writeFileSafe(fullPath, content);
4811
4820
  files.push(fullPath);
4812
4821
  };
4813
- await write("index.ts", genModuleIndex(pascal, kebab, plural));
4822
+ await write(`${kebab}.module.ts`, genModuleIndex(pascal, kebab, plural));
4814
4823
  await write("constants.ts", genConstants(pascal, fields));
4815
4824
  await write(`presentation/${kebab}.controller.ts`, genController(pascal, kebab, plural, pluralPascal));
4816
4825
  await write(`application/dtos/create-${kebab}.dto.ts`, genCreateDTO(pascal, fields));
@@ -4825,7 +4834,7 @@ async function generateScaffold(options) {
4825
4834
  await write(`domain/entities/${kebab}.entity.ts`, genEntity(pascal, kebab, fields));
4826
4835
  await write(`domain/value-objects/${kebab}-id.vo.ts`, genValueObject(pascal));
4827
4836
  }
4828
- await autoRegisterModule(modulesDir, pascal, plural);
4837
+ await autoRegisterModule(modulesDir, pascal, plural, kebab);
4829
4838
  return files;
4830
4839
  }
4831
4840
  function genCreateDTO(pascal, fields) {
@@ -5208,14 +5217,16 @@ export class Delete${pascal}UseCase {
5208
5217
  }
5209
5218
  ];
5210
5219
  }
5211
- async function autoRegisterModule(modulesDir, pascal, plural) {
5220
+ async function autoRegisterModule(modulesDir, pascal, plural, kebab) {
5212
5221
  const indexPath = join(modulesDir, "index.ts");
5213
- if (!await fileExists(indexPath)) {
5214
- await writeFileSafe(indexPath, `import type { AppModuleClass } from '@forinda/kickjs'\nimport { ${pascal}Module } from './${plural}'\n\nexport const modules: AppModuleClass[] = [${pascal}Module]\n`);
5222
+ const exists = await fileExists(indexPath);
5223
+ const importPath = `./${plural}/${kebab}.module`;
5224
+ if (!exists) {
5225
+ await writeFileSafe(indexPath, `import type { AppModuleClass } from '@forinda/kickjs'\nimport { ${pascal}Module } from '${importPath}'\n\nexport const modules: AppModuleClass[] = [${pascal}Module]\n`);
5215
5226
  return;
5216
5227
  }
5217
5228
  let content = await readFile(indexPath, "utf-8");
5218
- const importLine = `import { ${pascal}Module } from './${plural}'`;
5229
+ const importLine = `import { ${pascal}Module } from '${importPath}'`;
5219
5230
  if (!content.includes(`${pascal}Module`)) {
5220
5231
  const lastImportIdx = content.lastIndexOf("import ");
5221
5232
  if (lastImportIdx !== -1) {
@@ -5276,6 +5287,12 @@ describe('${pascal}', () => {
5276
5287
  }
5277
5288
  //#endregion
5278
5289
  //#region src/config.ts
5290
+ const PACKAGE_MANAGERS = [
5291
+ "pnpm",
5292
+ "npm",
5293
+ "yarn",
5294
+ "bun"
5295
+ ];
5279
5296
  const BUILTIN_REPO_TYPES = [
5280
5297
  "drizzle",
5281
5298
  "inmemory",
@@ -5356,6 +5373,16 @@ const DEFAULT_EXCLUDES = [
5356
5373
  */
5357
5374
  const DECORATED_CLASS_REGEX = new RegExp(String.raw`@(${DECORATOR_NAMES.join("|")})\s*\([^)]*\)` + String.raw`(?:\s*@[A-Z]\w*(?:\s*\([^)]*\))?)*` + String.raw`\s*export\s+(default\s+)?(?:abstract\s+)?class\s+(\w+)`, "g");
5358
5375
  /**
5376
+ * Match an exported class declaration that implements `AppModule`.
5377
+ * KickJS modules are not decorated — they implement the `AppModule`
5378
+ * interface — so the decorated-class scanner never picks them up. This
5379
+ * regex captures them by name so `ModuleToken` can be populated.
5380
+ *
5381
+ * Tolerates an `extends BaseClass` clause before `implements`, multiple
5382
+ * implements clauses (`implements Foo, AppModule`), and `default` exports.
5383
+ */
5384
+ const APP_MODULE_CLASS_REGEX = new RegExp(String.raw`export\s+(default\s+)?(?:abstract\s+)?class\s+(\w+)` + String.raw`(?:\s+extends\s+\w+(?:<[^>]*>)?)?` + String.raw`\s+implements\s+[^{]*\bAppModule\b`, "g");
5385
+ /**
5359
5386
  * Match a `createToken<T>('name')` call with optional `export const X =`
5360
5387
  * or `const X =` prefix. Tolerates whitespace and the type parameter
5361
5388
  * being absent (`createToken('name')`).
@@ -5369,52 +5396,89 @@ const BARE_CREATE_TOKEN_REGEX = /createToken\s*(?:<[^>]*>)?\s*\(\s*['"`]([^'"`]+
5369
5396
  /** Match `@Inject('literal')` — only literals; computed args are skipped */
5370
5397
  const INJECT_LITERAL_REGEX = /@Inject\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g;
5371
5398
  /**
5372
- * Match a route decorator immediately followed by a method declaration.
5373
- * Captures the HTTP verb, path literal (or empty), and method name.
5374
- *
5375
- * Tolerates:
5376
- * - Optional second arg to the route decorator (`@Get('/path', { ... })`)
5377
- * - Stacked decorators between the route and the method (`@Get('/') @Use(...)`)
5378
- * - Path-less decorators (`@Get()` → defaults to `/`)
5379
- * - `async` modifier on the method
5380
- *
5381
- * Run within a class body slice (see extractRoutesFromSource) so the
5382
- * captured method name is unambiguously a method on that class.
5399
+ * Locate the start of a route decorator: `@Get(`, `@Post(`, etc.
5400
+ * Used by `extractRoutesFromSource`; the rest of the route declaration
5401
+ * (balanced parens, stacked decorators, method name) is parsed by walking
5402
+ * the source forward from this match. The previous all-in-one regex
5403
+ * couldn't handle nested parens in stacked decorator args (e.g.
5404
+ * `@ApiResponse(201, { schema: z.object({ id: z.string() }) })`) — see
5405
+ * forinda/kick-js#108.
5383
5406
  */
5384
- const ROUTE_METHOD_REGEX = new RegExp(String.raw`@(${[
5407
+ const ROUTE_DECORATOR_START = new RegExp(String.raw`@(${[
5385
5408
  "Get",
5386
5409
  "Post",
5387
5410
  "Put",
5388
5411
  "Delete",
5389
5412
  "Patch"
5390
- ].join("|")})\s*\(` + String.raw`(?:\s*['"\`]([^'"\`]*)['"\`])?[^)]*\)` + String.raw`(?:\s*@[A-Z]\w*(?:\s*\([^)]*\))?)*` + String.raw`\s*(?:public\s+|private\s+|protected\s+)?(?:async\s+)?` + String.raw`([a-zA-Z_]\w*)\s*\(`, "g");
5391
- /** Extract `:placeholder` segments from an Express route path */
5392
- function extractPathParams(path) {
5393
- return (path.match(/:([a-zA-Z_]\w*)/g) ?? []).map((m) => m.slice(1));
5394
- }
5413
+ ].join("|")})\s*\(`, "g");
5395
5414
  /**
5396
- * Given the matched text of a route decorator + method declaration, return
5397
- * the substring inside the route decorator's argument list (between the
5398
- * outermost `(` and `)`). Returns `null` if no parens are found.
5399
- *
5400
- * Example input:
5401
- * `@Post('/', { body: createTaskSchema, name: 'CreateTask' }) async create(`
5402
- * Returns:
5403
- * `'/', { body: createTaskSchema, name: 'CreateTask' }`
5415
+ * Find the index of the `)` that balances the `(` at `openPos`.
5416
+ * Returns -1 if no matching `)` exists. Counts balanced parens only;
5417
+ * does not understand string literals, so a `(` or `)` inside a string
5418
+ * inside the args will skew the depth counter (matches the limitation
5419
+ * of `extractRouteOptionsArg`).
5404
5420
  */
5405
- function extractRouteOptionsArg(matchedText) {
5406
- const open = matchedText.indexOf("(");
5407
- if (open < 0) return null;
5421
+ function findBalancedClose(text, openPos) {
5408
5422
  let depth = 1;
5409
- for (let i = open + 1; i < matchedText.length; i++) {
5410
- const ch = matchedText[i];
5423
+ for (let i = openPos + 1; i < text.length; i++) {
5424
+ const ch = text[i];
5411
5425
  if (ch === "(") depth++;
5412
5426
  else if (ch === ")") {
5413
5427
  depth--;
5414
- if (depth === 0) return matchedText.slice(open + 1, i);
5428
+ if (depth === 0) return i;
5415
5429
  }
5416
5430
  }
5417
- return null;
5431
+ return -1;
5432
+ }
5433
+ /**
5434
+ * Walk forward from the end of a route decorator past any stacked
5435
+ * decorators (`@ApiOperation(...)`, `@ApiResponse(...)`, `@Middleware(fn)`,
5436
+ * etc.), then past optional `public`/`private`/`protected` and `async`,
5437
+ * and capture the method name + opening `(`.
5438
+ *
5439
+ * Returns the method name and the position immediately after the method's
5440
+ * opening `(`, or `null` if the source between the route decorator and
5441
+ * the method body doesn't fit the expected shape.
5442
+ */
5443
+ function readMethodAfterDecorators(block, startPos) {
5444
+ let pos = startPos;
5445
+ while (pos < block.length) {
5446
+ while (pos < block.length && /\s/.test(block[pos])) pos++;
5447
+ if (block[pos] !== "@") break;
5448
+ const decMatch = block.slice(pos).match(/^@([A-Z]\w*)/);
5449
+ if (!decMatch) break;
5450
+ pos += decMatch[0].length;
5451
+ while (pos < block.length && /\s/.test(block[pos])) pos++;
5452
+ if (block[pos] === "(") {
5453
+ const close = findBalancedClose(block, pos);
5454
+ if (close < 0) return null;
5455
+ pos = close + 1;
5456
+ }
5457
+ }
5458
+ while (pos < block.length && /\s/.test(block[pos])) pos++;
5459
+ for (const mod of [
5460
+ "public",
5461
+ "private",
5462
+ "protected"
5463
+ ]) if (block.slice(pos, pos + mod.length) === mod && /\s/.test(block.charAt(pos + mod.length))) {
5464
+ pos += mod.length;
5465
+ while (pos < block.length && /\s/.test(block[pos])) pos++;
5466
+ break;
5467
+ }
5468
+ if (block.slice(pos, pos + 5) === "async" && /\s/.test(block.charAt(pos + 5))) {
5469
+ pos += 5;
5470
+ while (pos < block.length && /\s/.test(block[pos])) pos++;
5471
+ }
5472
+ const methodMatch = block.slice(pos).match(/^([a-zA-Z_]\w*)\s*\(/);
5473
+ if (!methodMatch) return null;
5474
+ return {
5475
+ methodName: methodMatch[1],
5476
+ endPos: pos + methodMatch[0].length
5477
+ };
5478
+ }
5479
+ /** Extract `:placeholder` segments from an Express route path */
5480
+ function extractPathParams(path) {
5481
+ return (path.match(/:([a-zA-Z_]\w*)/g) ?? []).map((m) => m.slice(1));
5418
5482
  }
5419
5483
  /**
5420
5484
  * Extract a bare identifier value from a single field in an object literal
@@ -5545,6 +5609,19 @@ function extractClassesFromSource(source, filePath, cwd) {
5545
5609
  isDefault: Boolean(defaultMarker)
5546
5610
  });
5547
5611
  }
5612
+ APP_MODULE_CLASS_REGEX.lastIndex = 0;
5613
+ let modMatch;
5614
+ while ((modMatch = APP_MODULE_CLASS_REGEX.exec(source)) !== null) {
5615
+ const [, defaultMarker, className] = modMatch;
5616
+ if (out.some((c) => c.className === className && c.filePath === filePath)) continue;
5617
+ out.push({
5618
+ className,
5619
+ decorator: "Module",
5620
+ filePath,
5621
+ relativePath: relPath,
5622
+ isDefault: Boolean(defaultMarker)
5623
+ });
5624
+ }
5548
5625
  return out;
5549
5626
  }
5550
5627
  /** Extract `createToken('name')` definitions from a single source file */
@@ -5604,16 +5681,25 @@ function extractRoutesFromSource(source, filePath, cwd, classesInFile) {
5604
5681
  const { cls, start } = positions[i];
5605
5682
  const end = i + 1 < positions.length ? positions[i + 1].start : source.length;
5606
5683
  const block = source.slice(start, end);
5607
- ROUTE_METHOD_REGEX.lastIndex = 0;
5608
- let match;
5609
- while ((match = ROUTE_METHOD_REGEX.exec(block)) !== null) {
5610
- const [matchedText, verb, pathLiteral, methodName] = match;
5611
- const path = pathLiteral && pathLiteral.length > 0 ? pathLiteral : "/";
5612
- const apiQp = extractApiQueryParams(matchedText, source);
5613
- const routeArgs = extractRouteOptionsArg(matchedText);
5614
- const bodyId = routeArgs ? extractObjectFieldIdentifier(routeArgs, "body") : null;
5615
- const queryId = routeArgs ? extractObjectFieldIdentifier(routeArgs, "query") : null;
5616
- const paramsId = routeArgs ? extractObjectFieldIdentifier(routeArgs, "params") : null;
5684
+ ROUTE_DECORATOR_START.lastIndex = 0;
5685
+ let startMatch;
5686
+ while ((startMatch = ROUTE_DECORATOR_START.exec(block)) !== null) {
5687
+ const verb = startMatch[1];
5688
+ const decoratorStart = startMatch.index;
5689
+ const openParen = ROUTE_DECORATOR_START.lastIndex - 1;
5690
+ const closeParen = findBalancedClose(block, openParen);
5691
+ if (closeParen < 0) continue;
5692
+ const routeArgs = block.slice(openParen + 1, closeParen);
5693
+ const pathLiteralMatch = routeArgs.match(/^\s*['"`]([^'"`]*)['"`]/);
5694
+ const path = pathLiteralMatch && pathLiteralMatch[1].length > 0 ? pathLiteralMatch[1] : "/";
5695
+ const methodInfo = readMethodAfterDecorators(block, closeParen + 1);
5696
+ if (!methodInfo) continue;
5697
+ const { methodName, endPos } = methodInfo;
5698
+ ROUTE_DECORATOR_START.lastIndex = endPos;
5699
+ const apiQp = extractApiQueryParams(block.slice(decoratorStart, endPos), source);
5700
+ const bodyId = extractObjectFieldIdentifier(routeArgs, "body");
5701
+ const queryId = extractObjectFieldIdentifier(routeArgs, "query");
5702
+ const paramsId = extractObjectFieldIdentifier(routeArgs, "params");
5617
5703
  out.push({
5618
5704
  controller: cls.className,
5619
5705
  method: methodName,
@@ -6289,7 +6375,7 @@ async function safeRun(opts, silent) {
6289
6375
  //#region src/commands/generate.ts
6290
6376
  /** Check if --dry-run was passed on the parent generate command */
6291
6377
  function isDryRun(cmd) {
6292
- return cmd.parent?.opts()?.dryRun ?? false;
6378
+ return (cmd.parent?.opts())?.dryRun ?? false;
6293
6379
  }
6294
6380
  function printGenerated(files, dryRun = false) {
6295
6381
  const cwd = process.cwd();
@@ -6377,39 +6463,55 @@ function printGeneratorList() {
6377
6463
  for (const g of GENERATORS) console.log(` kick g ${g.name.padEnd(maxName + 2)} ${g.description}`);
6378
6464
  console.log();
6379
6465
  }
6466
+ /**
6467
+ * Generate one or more modules. Shared by `kick g module <names...>` and
6468
+ * the bare `kick g <names...>` shortcut.
6469
+ */
6470
+ async function runModuleGeneration(names, opts, dryRun) {
6471
+ const config = await loadKickConfig(process.cwd());
6472
+ const mc = resolveModuleConfig(config);
6473
+ const modulesDir = opts.modulesDir ?? mc.dir ?? "src/modules";
6474
+ const repo = opts.repo ?? resolveRepoType(mc.repo);
6475
+ const pattern = opts.pattern ?? config?.pattern ?? "ddd";
6476
+ const shouldPluralize = opts.pluralize === false ? false : mc.pluralize ?? true;
6477
+ const allFiles = [];
6478
+ for (const name of names) {
6479
+ const files = await generateModule({
6480
+ name,
6481
+ modulesDir: resolve(modulesDir),
6482
+ noEntity: opts.entity === false,
6483
+ noTests: opts.tests === false,
6484
+ repo,
6485
+ minimal: opts.minimal,
6486
+ force: opts.force,
6487
+ pattern,
6488
+ dryRun,
6489
+ pluralize: shouldPluralize,
6490
+ prismaClientPath: mc.prismaClientPath
6491
+ });
6492
+ allFiles.push(...files);
6493
+ }
6494
+ printGenerated(allFiles, dryRun);
6495
+ await runPostTypegen(dryRun);
6496
+ }
6380
6497
  function registerGenerateCommand(program) {
6381
- const gen = program.command("generate").alias("g").description("Generate code scaffolds").option("--list", "List all available generators").option("--dry-run", "Preview files that would be generated without writing them").action((opts) => {
6382
- if (opts.list) printGeneratorList();
6383
- else gen.help();
6498
+ const gen = program.command("generate [names...]").alias("g").description("Generate code scaffolds — bare form `kick g <name>` is shorthand for `kick g module <name>`").option("--list", "List all available generators").option("--dry-run", "Preview files that would be generated without writing them").option("--no-entity", "Skip entity and value object generation (module shortcut)").option("--no-tests", "Skip test file generation (module shortcut)").option("--repo <type>", "Repository implementation: inmemory | drizzle | prisma").option("--pattern <pattern>", "Override project pattern: rest | ddd | cqrs | minimal").option("--minimal", "Shorthand for --pattern minimal").option("--modules-dir <dir>", "Modules directory").option("--no-pluralize", "Use singular names (skip auto-pluralization)").option("-f, --force", "Overwrite existing files without prompting").action(async (names, opts, cmd) => {
6499
+ if (opts.list) {
6500
+ printGeneratorList();
6501
+ return;
6502
+ }
6503
+ if (!names || names.length === 0) {
6504
+ gen.help();
6505
+ return;
6506
+ }
6507
+ const dryRun = isDryRun(cmd);
6508
+ setDryRun(dryRun);
6509
+ await runModuleGeneration(names, opts, dryRun);
6384
6510
  });
6385
6511
  gen.command("module <names...>").description("Generate one or more modules (e.g. kick g module user task project)").option("--no-entity", "Skip entity and value object generation").option("--no-tests", "Skip test file generation").option("--repo <type>", "Repository implementation: inmemory | drizzle | prisma").option("--pattern <pattern>", "Override project pattern: rest | ddd | cqrs | minimal").option("--minimal", "Shorthand for --pattern minimal").option("--modules-dir <dir>", "Modules directory").option("--no-pluralize", "Use singular names (skip auto-pluralization)").option("-f, --force", "Overwrite existing files without prompting").action(async (names, opts, cmd) => {
6386
6512
  const dryRun = isDryRun(cmd);
6387
6513
  setDryRun(dryRun);
6388
- const config = await loadKickConfig(process.cwd());
6389
- const mc = resolveModuleConfig(config);
6390
- const modulesDir = opts.modulesDir ?? mc.dir ?? "src/modules";
6391
- const repo = opts.repo ?? resolveRepoType(mc.repo);
6392
- const pattern = opts.pattern ?? config?.pattern ?? "ddd";
6393
- const shouldPluralize = opts.pluralize === false ? false : mc.pluralize ?? true;
6394
- const allFiles = [];
6395
- for (const name of names) {
6396
- const files = await generateModule({
6397
- name,
6398
- modulesDir: resolve(modulesDir),
6399
- noEntity: opts.entity === false,
6400
- noTests: opts.tests === false,
6401
- repo,
6402
- minimal: opts.minimal,
6403
- force: opts.force,
6404
- pattern,
6405
- dryRun,
6406
- pluralize: shouldPluralize,
6407
- prismaClientPath: mc.prismaClientPath
6408
- });
6409
- allFiles.push(...files);
6410
- }
6411
- printGenerated(allFiles, dryRun);
6412
- await runPostTypegen(dryRun);
6514
+ await runModuleGeneration(names, opts, dryRun);
6413
6515
  });
6414
6516
  gen.command("adapter <name>").description("Generate an AppAdapter with lifecycle hooks and middleware support").option("-o, --out <dir>", "Output directory", "src/adapters").action(async (name, opts, cmd) => {
6415
6517
  const dryRun = isDryRun(cmd);
@@ -7092,8 +7194,36 @@ const PACKAGE_REGISTRY = {
7092
7194
  function detectPackageManager() {
7093
7195
  if (existsSync(resolve("pnpm-lock.yaml"))) return "pnpm";
7094
7196
  if (existsSync(resolve("yarn.lock"))) return "yarn";
7197
+ if (existsSync(resolve("bun.lockb")) || existsSync(resolve("bun.lock"))) return "bun";
7095
7198
  return "npm";
7096
7199
  }
7200
+ /** Read `packageManager` from package.json (corepack convention: "pnpm@10.0.0") */
7201
+ function packageManagerFromPackageJson() {
7202
+ try {
7203
+ const field = JSON.parse(readFileSync(resolve("package.json"), "utf-8")).packageManager;
7204
+ if (typeof field !== "string") return null;
7205
+ const name = field.split("@")[0];
7206
+ return PACKAGE_MANAGERS.includes(name) ? name : null;
7207
+ } catch {
7208
+ return null;
7209
+ }
7210
+ }
7211
+ /**
7212
+ * Resolve which package manager to use, in priority order:
7213
+ * 1. `--pm` CLI flag
7214
+ * 2. `packageManager` in kick.config
7215
+ * 3. `packageManager` in package.json (corepack)
7216
+ * 4. Lockfile detection
7217
+ * 5. `'npm'`
7218
+ */
7219
+ async function resolvePackageManager(flagPm) {
7220
+ if (flagPm && PACKAGE_MANAGERS.includes(flagPm)) return flagPm;
7221
+ const config = await loadKickConfig(process.cwd());
7222
+ if (config?.packageManager && PACKAGE_MANAGERS.includes(config.packageManager)) return config.packageManager;
7223
+ const fromPkg = packageManagerFromPackageJson();
7224
+ if (fromPkg) return fromPkg;
7225
+ return detectPackageManager();
7226
+ }
7097
7227
  function printPackageList() {
7098
7228
  console.log("\n Available KickJS packages:\n");
7099
7229
  const maxName = Math.max(...Object.keys(PACKAGE_REGISTRY).map((k) => k.length));
@@ -7117,7 +7247,7 @@ function registerAddCommand(program) {
7117
7247
  printPackageList();
7118
7248
  return;
7119
7249
  }
7120
- const pm = opts.pm ?? detectPackageManager();
7250
+ const pm = await resolvePackageManager(opts.pm);
7121
7251
  const forceDevFlag = opts.dev;
7122
7252
  const prodDeps = /* @__PURE__ */ new Set();
7123
7253
  const devDeps = /* @__PURE__ */ new Set();
@@ -7911,7 +8041,7 @@ async function removeModule(options) {
7911
8041
  if (await fileExists(indexPath)) {
7912
8042
  let content = await readFile(indexPath, "utf-8");
7913
8043
  const originalContent = content;
7914
- const importPattern = new RegExp(`^import\\s*\\{\\s*${pascal}Module\\s*\\}\\s*from\\s*['"][^'"]*${plural}['"].*\\n?`, "gm");
8044
+ const importPattern = new RegExp(`^import\\s*\\{\\s*${pascal}Module\\s*\\}\\s*from\\s*['"][^'"]*${plural}(?:/[^'"]*)?['"].*\\n?`, "gm");
7915
8045
  content = content.replace(importPattern, "");
7916
8046
  content = content.replace(new RegExp(`\\s*,?\\s*${pascal}Module\\s*,?`, "g"), (match) => {
7917
8047
  const startsWithComma = match.trimStart().startsWith(",");
package/dist/index.d.mts CHANGED
@@ -20,6 +20,8 @@ interface KickCommandDefinition {
20
20
  }
21
21
  /** Project pattern — controls what generators produce and which deps are installed */
22
22
  type ProjectPattern = 'rest' | 'graphql' | 'ddd' | 'cqrs' | 'minimal';
23
+ /** Package manager used for `kick add` and other dep-installing commands */
24
+ type PackageManager = 'pnpm' | 'npm' | 'yarn' | 'bun';
23
25
  /** Built-in repository types with first-class code generation support */
24
26
  type BuiltinRepoType$1 = 'drizzle' | 'inmemory' | 'prisma';
25
27
  /** Custom repository type — generates a stub with TODO markers */
@@ -132,6 +134,22 @@ interface KickConfig {
132
134
  * }
133
135
  */
134
136
  modules?: ModuleConfig;
137
+ /**
138
+ * Package manager used by `kick add` (and any future dep-installing command)
139
+ * to install dependencies. When set, overrides lockfile auto-detection so
140
+ * commands always use the project's intended package manager.
141
+ *
142
+ * Priority (highest first):
143
+ * 1. `--pm` flag on the CLI
144
+ * 2. `packageManager` in kick.config
145
+ * 3. `packageManager` field in package.json (corepack convention)
146
+ * 4. Lockfile detection (pnpm-lock.yaml → pnpm, yarn.lock → yarn)
147
+ * 5. `'npm'`
148
+ *
149
+ * @example
150
+ * packageManager: 'pnpm'
151
+ */
152
+ packageManager?: PackageManager;
135
153
  /** @deprecated Use `modules.dir` instead */
136
154
  modulesDir?: string;
137
155
  /** @deprecated Use `modules.repo` instead */
@@ -281,7 +299,7 @@ type ProjectTemplate = 'rest' | 'graphql' | 'ddd' | 'cqrs' | 'minimal';
281
299
  interface InitProjectOptions {
282
300
  name: string;
283
301
  directory: string;
284
- packageManager?: 'pnpm' | 'npm' | 'yarn';
302
+ packageManager?: 'pnpm' | 'npm' | 'yarn' | 'bun';
285
303
  initGit?: boolean;
286
304
  installDeps?: boolean;
287
305
  template?: ProjectTemplate;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/config.ts","../src/generators/module.ts","../src/generators/adapter.ts","../src/generators/middleware.ts","../src/generators/guard.ts","../src/generators/service.ts","../src/generators/controller.ts","../src/generators/dto.ts","../src/generators/project.ts","../src/utils/naming.ts"],"mappings":"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"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/config.ts","../src/generators/module.ts","../src/generators/adapter.ts","../src/generators/middleware.ts","../src/generators/guard.ts","../src/generators/service.ts","../src/generators/controller.ts","../src/generators/dto.ts","../src/generators/project.ts","../src/utils/naming.ts"],"mappings":"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"}