@forinda/kickjs-cli 2.2.4 → 2.2.5

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.2.4
2
+ * @forinda/kickjs-cli v2.2.5
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -15,6 +15,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
15
15
  import { createInterface } from "node:readline";
16
16
  import { execSync, fork } from "node:child_process";
17
17
  import { access, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
18
+ import pkg from "pluralize";
18
19
  import { arch, platform, release } from "node:os";
19
20
  //#region \0rolldown/runtime.js
20
21
  var __defProp = Object.defineProperty;
@@ -742,18 +743,30 @@ export class UserService {
742
743
 
743
744
  ### Modules
744
745
 
745
- Register controllers and providers in modules:
746
+ Modules implement \`AppModule\` and wire controllers via \`buildRoutes()\`:
746
747
 
747
748
  \`\`\`ts
748
- import { Module } from '@forinda/kickjs'
749
+ import { type AppModule, type ModuleRoutes, buildRoutes } from '@forinda/kickjs'
749
750
  import { UserController } from './user.controller'
750
- import { UserService } from './user.service'
751
751
 
752
- @Module({
753
- controllers: [UserController],
754
- providers: [UserService],
755
- })
756
- export class UserModule {}
752
+ export class UserModule implements AppModule {
753
+ routes(): ModuleRoutes {
754
+ return {
755
+ path: '/users',
756
+ router: buildRoutes(UserController),
757
+ controller: UserController,
758
+ }
759
+ }
760
+ }
761
+ \`\`\`
762
+
763
+ Register all modules in \`src/modules/index.ts\`:
764
+
765
+ \`\`\`ts
766
+ import type { AppModuleClass } from '@forinda/kickjs'
767
+ import { UserModule } from './user/user.module'
768
+
769
+ export const modules: AppModuleClass[] = [UserModule]
757
770
  \`\`\`
758
771
 
759
772
  ### RequestContext
@@ -864,6 +877,86 @@ Hot-reload of \`.env\` changes during dev is wired up automatically via
864
877
  \`envWatchPlugin()\` in \`vite.config.ts\` — edit \`.env\`, the dev server
865
878
  reloads, and the next \`config.get()\` re-parses with the new values.
866
879
 
880
+ ### Standalone Env Utilities (No DI Required)
881
+
882
+ These functions work anywhere — scripts, CLI tools, plain files, outside \`@Service\`/\`@Controller\`:
883
+
884
+ \`\`\`ts
885
+ import { defineEnv, loadEnv, getEnv, reloadEnv, resetEnvCache, baseEnvSchema } from '@forinda/kickjs/config'
886
+ import { z } from 'zod'
887
+
888
+ // Define and parse schema
889
+ const schema = defineEnv((base) =>
890
+ base.extend({ DATABASE_URL: z.string().url() })
891
+ )
892
+ const env = loadEnv(schema) // Parse + validate process.env
893
+ console.log(env.PORT) // 3000 (coerced to number)
894
+ console.log(env.DATABASE_URL) // validated URL string
895
+
896
+ // Get single value
897
+ const port = getEnv('PORT') // typed after kick typegen
898
+
899
+ // Reload after .env changes (HMR calls this automatically)
900
+ reloadEnv()
901
+
902
+ // Reset cache in tests that swap schemas
903
+ resetEnvCache()
904
+ \`\`\`
905
+
906
+ | Function | Purpose |
907
+ |----------|---------|
908
+ | \`defineEnv(fn)\` | Extend base schema with custom Zod keys |
909
+ | \`loadEnv(schema?)\` | Parse \`process.env\`, validate, cache, return typed object |
910
+ | \`getEnv(key, schema?)\` | Get single validated env value |
911
+ | \`reloadEnv()\` | Re-read \`.env\` from disk, re-parse with same schema |
912
+ | \`resetEnvCache()\` | Clear parsed cache AND registered schema (for tests) |
913
+ | \`baseEnvSchema\` | Base Zod schema: \`PORT\`, \`NODE_ENV\`, \`LOG_LEVEL\` |
914
+
915
+ ## Standalone Utilities (No DI Required)
916
+
917
+ These utilities work outside decorated classes:
918
+
919
+ ### Logger
920
+
921
+ \`\`\`ts
922
+ import { Logger, createLogger } from '@forinda/kickjs'
923
+
924
+ const log = Logger.for('MyScript') // Static factory
925
+ log.info('Processing started')
926
+ log.error('Something failed')
927
+
928
+ const log2 = createLogger('Worker') // Function form
929
+ \`\`\`
930
+
931
+ ### Injection Tokens
932
+
933
+ \`\`\`ts
934
+ import { createToken } from '@forinda/kickjs'
935
+
936
+ // Type-safe DI tokens for factory/interface binding
937
+ const DB_URL = createToken<string>('config.database.url')
938
+ const FEATURE_FLAGS = createToken<FeatureFlags>('app.features')
939
+ \`\`\`
940
+
941
+ ### Reactivity
942
+
943
+ \`\`\`ts
944
+ import { ref, computed, watch, reactive } from '@forinda/kickjs'
945
+
946
+ const count = ref(0)
947
+ const doubled = computed(() => count.value * 2)
948
+ const stop = watch(() => count.value, (val) => console.log(val))
949
+ count.value++ // logs 1
950
+ \`\`\`
951
+
952
+ ### HTTP Errors
953
+
954
+ \`\`\`ts
955
+ import { HttpException, HttpStatus } from '@forinda/kickjs'
956
+
957
+ throw new HttpException(HttpStatus.NOT_FOUND, 'User not found')
958
+ \`\`\`
959
+
867
960
  ## Testing
868
961
 
869
962
  Tests live in \`src/**/*.test.ts\`:
@@ -898,7 +991,6 @@ Run tests:
898
991
  - \`@Roles('admin', 'user')\` — role-based access control
899
992
 
900
993
  ### DI Decorators
901
- - \`@Module({ controllers, providers, imports })\` — define module
902
994
  - \`@Service()\` — singleton service (DI-registered)
903
995
  - \`@Repository()\` — repository (semantic alias for @Service)
904
996
  - \`@Autowired()\` — property injection
@@ -976,7 +1068,7 @@ ${template === "ddd" ? `\`\`\`
976
1068
  ├── <name>.repository.ts # Data access (@Repository)
977
1069
  ├── <name>.dto.ts # Request/response schemas (Zod)
978
1070
  ├── <name>.entity.ts # Domain entity (optional)
979
- └── <name>.module.ts # Module definition (@Module)
1071
+ └── <name>.module.ts # Module definition (implements AppModule)
980
1072
  \`\`\`
981
1073
  ` : template === "cqrs" ? `\`\`\`
982
1074
  <name>/
@@ -990,7 +1082,7 @@ ${template === "ddd" ? `\`\`\`
990
1082
  │ └── <name>-created.event.ts
991
1083
  ├── <name>.controller.ts # HTTP routes
992
1084
  ├── <name>.repository.ts # Data access
993
- └── <name>.module.ts # Module definition
1085
+ └── <name>.module.ts # Module definition (implements AppModule)
994
1086
  \`\`\`
995
1087
  ` : template === "graphql" ? `\`\`\`
996
1088
  resolvers/
@@ -1003,7 +1095,7 @@ resolvers/
1003
1095
  ├── <name>.controller.ts # HTTP routes (@Controller)
1004
1096
  ├── <name>.service.ts # Business logic (@Service)
1005
1097
  ├── <name>.dto.ts # Request/response schemas (Zod)
1006
- └── <name>.module.ts # Module definition (@Module)
1098
+ └── <name>.module.ts # Module definition (implements AppModule)
1007
1099
  \`\`\`
1008
1100
  ` : `\`\`\`
1009
1101
  src/
@@ -1039,8 +1131,8 @@ If not using generators:
1039
1131
  - [ ] Create \`src/modules/<name>/<name>.controller.ts\`
1040
1132
  - [ ] Add \`@Controller('/path')\` decorator
1041
1133
  - [ ] Add route handlers with \`@Get()\`, \`@Post()\`, etc.
1042
- - [ ] Create module file with \`@Module({ controllers: [NameController] })\`
1043
- - [ ] Register module in \`src/modules/index.ts\`
1134
+ - [ ] Create module file implementing \`AppModule\` with \`routes()\` returning \`{ path, router: buildRoutes(Controller), controller }\`
1135
+ - [ ] Register module in \`src/modules/index.ts\` (\`AppModuleClass[]\` array)
1044
1136
  - [ ] Test with \`kick dev\`
1045
1137
 
1046
1138
  ### Manual Service
@@ -1048,7 +1140,7 @@ If not using generators:
1048
1140
  - [ ] Create \`src/modules/<name>/<name>.service.ts\`
1049
1141
  - [ ] Add \`@Service()\` decorator
1050
1142
  - [ ] Inject dependencies with \`@Autowired()\`
1051
- - [ ] Register in module \`providers\` array
1143
+ - [ ] Inject via \`@Autowired()\` where needed
1052
1144
  - [ ] Write unit tests
1053
1145
 
1054
1146
  ### New Middleware
@@ -1073,9 +1165,12 @@ Use \`kick add\` to install KickJS packages with correct peer dependencies:
1073
1165
  ### Generate CRUD Module
1074
1166
 
1075
1167
  \`\`\`bash
1076
- kick g scaffold user name:string email:string age:number
1168
+ kick g scaffold user name:string email:string:optional age:number
1077
1169
  \`\`\`
1078
1170
 
1171
+ Append \`:optional\` for optional fields (shell-safe, no quoting needed).
1172
+ Quoted \`?\` syntax also works: \`"email:string?"\` or \`"email?:string"\`.
1173
+
1079
1174
  This creates a full CRUD module with:
1080
1175
  - Controller with GET, POST, PUT, DELETE routes
1081
1176
  - Service with business logic
@@ -1191,7 +1286,17 @@ private config!: ConfigService
1191
1286
  const port = this.config.get('PORT') // typed: number
1192
1287
  \`\`\`
1193
1288
 
1194
- 3. **Direct \`process.env\`**avoid in app code; bypasses Zod
1289
+ 3. **Standalone utilities** (no DI works in scripts, CLI, plain files):
1290
+ \`\`\`ts
1291
+ import { loadEnv, getEnv, reloadEnv, resetEnvCache } from '@forinda/kickjs/config'
1292
+
1293
+ const env = loadEnv(schema) // Parse + validate all vars
1294
+ const port = getEnv('PORT') // Single value lookup
1295
+ reloadEnv() // Re-read .env from disk
1296
+ resetEnvCache() // Full reset (for tests)
1297
+ \`\`\`
1298
+
1299
+ 4. **Direct \`process.env\`** — avoid in app code; bypasses Zod
1195
1300
  coercion and the typed \`KickEnv\` registry.
1196
1301
 
1197
1302
  > **Pitfall**: never delete \`import './config'\` from \`src/index.ts\`.
@@ -1200,6 +1305,22 @@ const port = this.config.get('PORT') // typed: number
1200
1305
  > \`@Value()\` only works because of its raw \`process.env\` fallback —
1201
1306
  > Zod coercion + schema defaults are silently skipped.
1202
1307
 
1308
+ ## Standalone Utilities (No DI Required)
1309
+
1310
+ These work anywhere — scripts, plain files, outside \`@Service\`/\`@Controller\`:
1311
+
1312
+ | Utility | Import | Example |
1313
+ |---------|--------|---------|
1314
+ | \`Logger.for(name)\` | \`@forinda/kickjs\` | \`const log = Logger.for('MyScript')\` |
1315
+ | \`createLogger(name)\` | \`@forinda/kickjs\` | \`const log = createLogger('Worker')\` |
1316
+ | \`createToken<T>(name)\` | \`@forinda/kickjs\` | \`const TOKEN = createToken<string>('db.url')\` |
1317
+ | \`ref(value)\` | \`@forinda/kickjs\` | \`const count = ref(0)\` |
1318
+ | \`computed(fn)\` | \`@forinda/kickjs\` | \`const doubled = computed(() => count.value * 2)\` |
1319
+ | \`watch(source, cb)\` | \`@forinda/kickjs\` | \`watch(() => count.value, (v) => log(v))\` |
1320
+ | \`reactive(obj)\` | \`@forinda/kickjs\` | \`const state = reactive({ count: 0 })\` |
1321
+ | \`HttpException\` | \`@forinda/kickjs\` | \`throw new HttpException(404, 'Not found')\` |
1322
+ | \`HttpStatus\` | \`@forinda/kickjs\` | \`HttpStatus.NOT_FOUND // 404\` |
1323
+
1203
1324
  ## Key Decorators
1204
1325
 
1205
1326
  ### HTTP Routes
@@ -1214,7 +1335,7 @@ const port = this.config.get('PORT') // typed: number
1214
1335
  ### Dependency Injection
1215
1336
  | Decorator | Purpose |
1216
1337
  |-----------|---------|
1217
- | \`@Module({})\` | Define feature module |
1338
+ | \`AppModule\` interface | Define feature module (implements \`routes()\`) |
1218
1339
  | \`@Service()\` | Register singleton service |
1219
1340
  | \`@Repository()\` | Register repository |
1220
1341
  | \`@Autowired()\` | Property injection |
@@ -1310,6 +1431,26 @@ async function initProject(options) {
1310
1431
  await writeFileSafe(join(dir, "README.md"), generateReadme(name, template, packageManager));
1311
1432
  await writeFileSafe(join(dir, "CLAUDE.md"), generateClaude(name, template, packageManager));
1312
1433
  await writeFileSafe(join(dir, "AGENTS.md"), generateAgents(name, template, packageManager));
1434
+ if (options.installDeps) {
1435
+ console.log(`\n Installing dependencies with ${packageManager}...\n`);
1436
+ try {
1437
+ execSync(`${packageManager} install`, {
1438
+ cwd: dir,
1439
+ stdio: "inherit"
1440
+ });
1441
+ console.log("\n Dependencies installed successfully!");
1442
+ } catch {
1443
+ console.log(`\n Warning: ${packageManager} install failed. Run it manually.`);
1444
+ }
1445
+ }
1446
+ try {
1447
+ const { runTypegen } = await Promise.resolve().then(() => typegen_exports);
1448
+ await runTypegen({
1449
+ cwd: dir,
1450
+ allowDuplicates: true,
1451
+ silent: true
1452
+ });
1453
+ } catch {}
1313
1454
  if (options.initGit) try {
1314
1455
  execSync("git init", {
1315
1456
  cwd: dir,
@@ -1331,26 +1472,6 @@ async function initProject(options) {
1331
1472
  } catch {
1332
1473
  log("Warning: git init failed (git may not be installed)");
1333
1474
  }
1334
- if (options.installDeps) {
1335
- console.log(`\n Installing dependencies with ${packageManager}...\n`);
1336
- try {
1337
- execSync(`${packageManager} install`, {
1338
- cwd: dir,
1339
- stdio: "inherit"
1340
- });
1341
- console.log("\n Dependencies installed successfully!");
1342
- } catch {
1343
- console.log(`\n Warning: ${packageManager} install failed. Run it manually.`);
1344
- }
1345
- }
1346
- try {
1347
- const { runTypegen } = await Promise.resolve().then(() => typegen_exports);
1348
- await runTypegen({
1349
- cwd: dir,
1350
- allowDuplicates: true,
1351
- silent: true
1352
- });
1353
- } catch {}
1354
1475
  console.log("\n Project scaffolded successfully!");
1355
1476
  console.log();
1356
1477
  const needsCd = dir !== process.cwd();
@@ -1522,26 +1643,18 @@ function toKebabCase(name) {
1522
1643
  }
1523
1644
  /**
1524
1645
  * Pluralize a kebab-case name for directory/file names.
1525
- * If already plural (ends in 's'), returns as-is.
1646
+ * Uses the `pluralize` npm package for correct English pluralization
1647
+ * including irregulars (person → people, status → statuses, child → children).
1526
1648
  */
1527
1649
  function pluralize(name) {
1528
- if (name.endsWith("s")) return name;
1529
- if (name.endsWith("x") || name.endsWith("z")) return name + "es";
1530
- if (name.endsWith("sh") || name.endsWith("ch")) return name + "es";
1531
- if (name.endsWith("y") && !/[aeiou]y$/.test(name)) return name.slice(0, -1) + "ies";
1532
- return name + "s";
1650
+ return pkg.plural(name);
1533
1651
  }
1534
1652
  /**
1535
1653
  * Pluralize a PascalCase name for class identifiers.
1536
- * If already plural (ends in 's'), returns as-is.
1537
1654
  * Used for `List${pluralPascal}UseCase` to avoid `ListUserssUseCase`.
1538
1655
  */
1539
1656
  function pluralizePascal(name) {
1540
- if (name.endsWith("s")) return name;
1541
- if (name.endsWith("x") || name.endsWith("z")) return name + "es";
1542
- if (name.endsWith("sh") || name.endsWith("ch")) return name + "es";
1543
- if (name.endsWith("y") && !/[aeiou]y$/i.test(name)) return name.slice(0, -1) + "ies";
1544
- return name + "s";
1657
+ return pkg.plural(name);
1545
1658
  }
1546
1659
  //#endregion
1547
1660
  //#region src/generators/templates/module-index.ts
@@ -3585,13 +3698,14 @@ const CQRS_FOLDER_MAP = {
3585
3698
  * 3. Standalone default directory
3586
3699
  */
3587
3700
  function resolveOutDir(options) {
3588
- const { type, outDir, moduleName, modulesDir = "src/modules", defaultDir, pattern = "ddd" } = options;
3701
+ const { type, outDir, moduleName, modulesDir = "src/modules", defaultDir, pattern = "ddd", shouldPluralize = true } = options;
3589
3702
  if (outDir) return resolve(outDir);
3590
3703
  if (moduleName) {
3591
3704
  const folderMap = pattern === "ddd" ? DDD_FOLDER_MAP : pattern === "cqrs" ? CQRS_FOLDER_MAP : FLAT_FOLDER_MAP;
3592
- const plural = pluralize(toKebabCase(moduleName));
3705
+ const kebab = toKebabCase(moduleName);
3706
+ const folder = shouldPluralize ? pluralize(kebab) : kebab;
3593
3707
  const subfolder = folderMap[type] ?? "";
3594
- const base = join(modulesDir, plural);
3708
+ const base = join(modulesDir, folder);
3595
3709
  return resolve(subfolder ? join(base, subfolder) : base);
3596
3710
  }
3597
3711
  return resolve(defaultDir);
@@ -3606,7 +3720,8 @@ async function generateMiddleware(options) {
3606
3720
  moduleName,
3607
3721
  modulesDir,
3608
3722
  defaultDir: "src/middleware",
3609
- pattern
3723
+ pattern,
3724
+ shouldPluralize: options.pluralize ?? true
3610
3725
  });
3611
3726
  const kebab = toKebabCase(name);
3612
3727
  const camel = toCamelCase(name);
@@ -3650,7 +3765,8 @@ async function generateGuard(options) {
3650
3765
  moduleName,
3651
3766
  modulesDir,
3652
3767
  defaultDir: "src/guards",
3653
- pattern
3768
+ pattern,
3769
+ shouldPluralize: options.pluralize ?? true
3654
3770
  });
3655
3771
  const kebab = toKebabCase(name);
3656
3772
  const camel = toCamelCase(name);
@@ -3707,7 +3823,8 @@ async function generateService(options) {
3707
3823
  moduleName,
3708
3824
  modulesDir,
3709
3825
  defaultDir: "src/services",
3710
- pattern
3826
+ pattern,
3827
+ shouldPluralize: options.pluralize ?? true
3711
3828
  });
3712
3829
  const kebab = toKebabCase(name);
3713
3830
  const pascal = toPascalCase(name);
@@ -3736,7 +3853,8 @@ async function generateController(options) {
3736
3853
  moduleName,
3737
3854
  modulesDir,
3738
3855
  defaultDir: "src/controllers",
3739
- pattern
3856
+ pattern,
3857
+ shouldPluralize: options.pluralize ?? true
3740
3858
  });
3741
3859
  const kebab = toKebabCase(name);
3742
3860
  const pascal = toPascalCase(name);
@@ -3777,7 +3895,8 @@ async function generateDto(options) {
3777
3895
  moduleName,
3778
3896
  modulesDir,
3779
3897
  defaultDir: "src/dtos",
3780
- pattern
3898
+ pattern,
3899
+ shouldPluralize: options.pluralize ?? true
3781
3900
  });
3782
3901
  const kebab = toKebabCase(name);
3783
3902
  const pascal = toPascalCase(name);
@@ -3996,7 +4115,10 @@ export class ${pascal}Job {
3996
4115
  * json → z.any()
3997
4116
  * enum:a,b → z.enum(['a','b'])
3998
4117
  *
3999
- * Append ? for optional: title:string body:text? published:boolean?
4118
+ * Mark optional fields three equivalent syntaxes:
4119
+ * body:text:optional ← recommended (shell-safe, no quoting needed)
4120
+ * body?:text ← needs quoting in bash/zsh ("body?:text")
4121
+ * body:text? ← needs quoting in bash/zsh ("body:text?")
4000
4122
  */
4001
4123
  const TYPE_MAP = {
4002
4124
  string: {
@@ -4048,11 +4170,23 @@ function parseFields(raw) {
4048
4170
  return raw.map((f) => {
4049
4171
  const colonIdx = f.indexOf(":");
4050
4172
  if (colonIdx === -1) throw new Error(`Invalid field: "${f}". Use format: name:type (e.g. title:string)`);
4051
- const namePart = f.slice(0, colonIdx);
4052
- const typePart = f.slice(colonIdx + 1);
4173
+ let namePart = f.slice(0, colonIdx);
4174
+ let typePart = f.slice(colonIdx + 1);
4053
4175
  if (!namePart || !typePart) throw new Error(`Invalid field: "${f}". Use format: name:type (e.g. title:string)`);
4054
- const optional = typePart.endsWith("?");
4055
- const cleanType = optional ? typePart.slice(0, -1) : typePart;
4176
+ let optional = false;
4177
+ if (typePart.endsWith(":optional")) {
4178
+ typePart = typePart.slice(0, -9);
4179
+ optional = true;
4180
+ }
4181
+ if (namePart.endsWith("?")) {
4182
+ namePart = namePart.slice(0, -1);
4183
+ optional = true;
4184
+ }
4185
+ if (typePart.endsWith("?")) {
4186
+ typePart = typePart.slice(0, -1);
4187
+ optional = true;
4188
+ }
4189
+ const cleanType = typePart;
4056
4190
  if (cleanType.startsWith("enum:")) {
4057
4191
  const values = cleanType.slice(5).split(",");
4058
4192
  return {
@@ -4517,14 +4651,16 @@ async function autoRegisterModule(modulesDir, pascal, plural) {
4517
4651
  //#region src/generators/test.ts
4518
4652
  async function generateTest(options) {
4519
4653
  const { name, moduleName, modulesDir } = options;
4654
+ const shouldPluralize = options.pluralize ?? true;
4520
4655
  const kebab = toKebabCase(name);
4521
4656
  const pascal = toPascalCase(name);
4522
4657
  const files = [];
4523
4658
  let outDir;
4524
4659
  if (options.outDir) outDir = resolve(options.outDir);
4525
4660
  else if (moduleName) {
4526
- const modPlural = pluralize(toKebabCase(moduleName));
4527
- outDir = resolve(join(modulesDir ?? "src/modules", modPlural, "__tests__"));
4661
+ const modKebab = toKebabCase(moduleName);
4662
+ const modFolder = shouldPluralize ? pluralize(modKebab) : modKebab;
4663
+ outDir = resolve(join(modulesDir ?? "src/modules", modFolder, "__tests__"));
4528
4664
  } else outDir = resolve("src/__tests__");
4529
4665
  const filePath = join(outDir, `${kebab}.test.ts`);
4530
4666
  await writeFileSafe(filePath, `import { describe, it, expect, beforeEach } from 'vitest'
@@ -5703,52 +5839,60 @@ function registerGenerateCommand(program) {
5703
5839
  const dryRun = isDryRun(cmd);
5704
5840
  setDryRun(dryRun);
5705
5841
  const config = await loadKickConfig(process.cwd());
5706
- const modulesDir = resolveModuleConfig(config).dir ?? "src/modules";
5842
+ const mc = resolveModuleConfig(config);
5843
+ const modulesDir = mc.dir ?? "src/modules";
5707
5844
  printGenerated(await generateMiddleware({
5708
5845
  name,
5709
5846
  outDir: opts.out,
5710
5847
  moduleName: opts.module,
5711
5848
  modulesDir,
5712
- pattern: config?.pattern
5849
+ pattern: config?.pattern,
5850
+ pluralize: mc.pluralize ?? true
5713
5851
  }), dryRun);
5714
5852
  });
5715
5853
  gen.command("guard <name>").description("Generate a route guard (auth, roles, etc.)\n Use -m to scope it to a module: kick g guard admin -m users").option("-o, --out <dir>", "Output directory (overrides --module)").option("-m, --module <module>", "Place inside a module folder").action(async (name, opts, cmd) => {
5716
5854
  const dryRun = isDryRun(cmd);
5717
5855
  setDryRun(dryRun);
5718
5856
  const config = await loadKickConfig(process.cwd());
5719
- const modulesDir = resolveModuleConfig(config).dir ?? "src/modules";
5857
+ const mc = resolveModuleConfig(config);
5858
+ const modulesDir = mc.dir ?? "src/modules";
5720
5859
  printGenerated(await generateGuard({
5721
5860
  name,
5722
5861
  outDir: opts.out,
5723
5862
  moduleName: opts.module,
5724
5863
  modulesDir,
5725
- pattern: config?.pattern
5864
+ pattern: config?.pattern,
5865
+ pluralize: mc.pluralize ?? true
5726
5866
  }), dryRun);
5727
5867
  });
5728
5868
  gen.command("service <name>").description("Generate a @Service() class\n Use -m to scope it to a module: kick g service payment -m orders").option("-o, --out <dir>", "Output directory (overrides --module)").option("-m, --module <module>", "Place inside a module folder").action(async (name, opts, cmd) => {
5729
5869
  const dryRun = isDryRun(cmd);
5730
5870
  setDryRun(dryRun);
5731
5871
  const config = await loadKickConfig(process.cwd());
5732
- const modulesDir = resolveModuleConfig(config).dir ?? "src/modules";
5872
+ const mc = resolveModuleConfig(config);
5873
+ const modulesDir = mc.dir ?? "src/modules";
5733
5874
  printGenerated(await generateService({
5734
5875
  name,
5735
5876
  outDir: opts.out,
5736
5877
  moduleName: opts.module,
5737
5878
  modulesDir,
5738
- pattern: config?.pattern
5879
+ pattern: config?.pattern,
5880
+ pluralize: mc.pluralize ?? true
5739
5881
  }), dryRun);
5740
5882
  });
5741
5883
  gen.command("controller <name>").description("Generate a @Controller() class with basic routes\n Use -m to scope it to a module: kick g controller auth -m users").option("-o, --out <dir>", "Output directory (overrides --module)").option("-m, --module <module>", "Place inside a module folder").action(async (name, opts, cmd) => {
5742
5884
  const dryRun = isDryRun(cmd);
5743
5885
  setDryRun(dryRun);
5744
5886
  const config = await loadKickConfig(process.cwd());
5745
- const modulesDir = resolveModuleConfig(config).dir ?? "src/modules";
5887
+ const mc = resolveModuleConfig(config);
5888
+ const modulesDir = mc.dir ?? "src/modules";
5746
5889
  printGenerated(await generateController({
5747
5890
  name,
5748
5891
  outDir: opts.out,
5749
5892
  moduleName: opts.module,
5750
5893
  modulesDir,
5751
- pattern: config?.pattern
5894
+ pattern: config?.pattern,
5895
+ pluralize: mc.pluralize ?? true
5752
5896
  }), dryRun);
5753
5897
  await runPostTypegen(dryRun);
5754
5898
  });
@@ -5756,24 +5900,28 @@ function registerGenerateCommand(program) {
5756
5900
  const dryRun = isDryRun(cmd);
5757
5901
  setDryRun(dryRun);
5758
5902
  const config = await loadKickConfig(process.cwd());
5759
- const modulesDir = resolveModuleConfig(config).dir ?? "src/modules";
5903
+ const mc = resolveModuleConfig(config);
5904
+ const modulesDir = mc.dir ?? "src/modules";
5760
5905
  printGenerated(await generateDto({
5761
5906
  name,
5762
5907
  outDir: opts.out,
5763
5908
  moduleName: opts.module,
5764
5909
  modulesDir,
5765
- pattern: config?.pattern
5910
+ pattern: config?.pattern,
5911
+ pluralize: mc.pluralize ?? true
5766
5912
  }), dryRun);
5767
5913
  });
5768
5914
  gen.command("test <name>").description("Generate a Vitest test scaffold\n Use -m to scope it to a module: kick g test user-service -m users").option("-o, --out <dir>", "Output directory (overrides --module)").option("-m, --module <module>", "Place inside a module's __tests__/ folder").action(async (name, opts, cmd) => {
5769
5915
  const dryRun = isDryRun(cmd);
5770
5916
  setDryRun(dryRun);
5771
- const modulesDir = resolveModuleConfig(await loadKickConfig(process.cwd())).dir ?? "src/modules";
5917
+ const mc = resolveModuleConfig(await loadKickConfig(process.cwd()));
5918
+ const modulesDir = mc.dir ?? "src/modules";
5772
5919
  printGenerated(await generateTest({
5773
5920
  name,
5774
5921
  outDir: opts.out,
5775
5922
  moduleName: opts.module,
5776
- modulesDir
5923
+ modulesDir,
5924
+ pluralize: mc.pluralize ?? true
5777
5925
  }), dryRun);
5778
5926
  });
5779
5927
  gen.command("resolver <name>").description("Generate a GraphQL @Resolver class with @Query and @Mutation methods").option("-o, --out <dir>", "Output directory", "src/resolvers").action(async (name, opts, cmd) => {
@@ -5793,11 +5941,11 @@ function registerGenerateCommand(program) {
5793
5941
  queue: opts.queue
5794
5942
  }), dryRun);
5795
5943
  });
5796
- gen.command("scaffold <name> [fields...]").description("Generate a full CRUD module from field definitions\n Example: kick g scaffold Post title:string body:text published:boolean?\n Types: string, text, number, int, float, boolean, date, email, url, uuid, json, enum:a,b,c\n Append ? for optional fields: description:text?").option("--no-entity", "Skip entity and value object generation").option("--no-tests", "Skip test file generation").option("--no-pluralize", "Use singular names (skip auto-pluralization)").option("--modules-dir <dir>", "Modules directory").action(async (name, rawFields, opts, cmd) => {
5944
+ gen.command("scaffold <name> [fields...]").description("Generate a full CRUD module from field definitions\n Example: kick g scaffold Post title:string body:text:optional published:boolean:optional\n Types: string, text, number, int, float, boolean, date, email, url, uuid, json, enum:a,b,c\n Optional: append :optional (shell-safe): description:text:optional\n or use ? with quoting: \"description:text?\" or \"description?:text\"").option("--no-entity", "Skip entity and value object generation").option("--no-tests", "Skip test file generation").option("--no-pluralize", "Use singular names (skip auto-pluralization)").option("--modules-dir <dir>", "Modules directory").action(async (name, rawFields, opts, cmd) => {
5797
5945
  const dryRun = isDryRun(cmd);
5798
5946
  setDryRun(dryRun);
5799
5947
  if (rawFields.length === 0) {
5800
- console.error("\n Error: At least one field is required.\n Usage: kick g scaffold <name> <field:type> [field:type...]\n Example: kick g scaffold Post title:string body:text published:boolean\n");
5948
+ console.error("\n Error: At least one field is required.\n Usage: kick g scaffold <name> <field:type> [field:type...]\n Example: kick g scaffold Post title:string body:text:optional published:boolean:optional\n Optional: append :optional (shell-safe, no quoting needed)\n");
5801
5949
  process.exit(1);
5802
5950
  }
5803
5951
  const mc = resolveModuleConfig(await loadKickConfig(process.cwd()));
@@ -6613,10 +6761,10 @@ function registerTypegenCommand(program) {
6613
6761
  //#endregion
6614
6762
  //#region src/cli.ts
6615
6763
  const __dirname = dirname(fileURLToPath(import.meta.url));
6616
- const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
6764
+ const pkg$1 = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
6617
6765
  async function main() {
6618
6766
  const program = new Command();
6619
- program.name("kick").description("KickJS — A production-grade, decorator-driven Node.js framework").version(pkg.version);
6767
+ program.name("kick").description("KickJS — A production-grade, decorator-driven Node.js framework").version(pkg$1.version);
6620
6768
  const config = await loadKickConfig(process.cwd());
6621
6769
  registerInitCommand(program);
6622
6770
  registerGenerateCommand(program);
package/dist/index.d.mts CHANGED
@@ -228,6 +228,7 @@ interface GenerateMiddlewareOptions {
228
228
  moduleName?: string;
229
229
  modulesDir?: string;
230
230
  pattern?: ProjectPattern;
231
+ pluralize?: boolean;
231
232
  }
232
233
  declare function generateMiddleware(options: GenerateMiddlewareOptions): Promise<string[]>;
233
234
  //#endregion
@@ -238,6 +239,7 @@ interface GenerateGuardOptions {
238
239
  moduleName?: string;
239
240
  modulesDir?: string;
240
241
  pattern?: ProjectPattern;
242
+ pluralize?: boolean;
241
243
  }
242
244
  declare function generateGuard(options: GenerateGuardOptions): Promise<string[]>;
243
245
  //#endregion
@@ -248,6 +250,7 @@ interface GenerateServiceOptions {
248
250
  moduleName?: string;
249
251
  modulesDir?: string;
250
252
  pattern?: ProjectPattern;
253
+ pluralize?: boolean;
251
254
  }
252
255
  declare function generateService(options: GenerateServiceOptions): Promise<string[]>;
253
256
  //#endregion
@@ -258,6 +261,7 @@ interface GenerateControllerOptions {
258
261
  moduleName?: string;
259
262
  modulesDir?: string;
260
263
  pattern?: ProjectPattern;
264
+ pluralize?: boolean;
261
265
  }
262
266
  declare function generateController(options: GenerateControllerOptions): Promise<string[]>;
263
267
  //#endregion
@@ -268,6 +272,7 @@ interface GenerateDtoOptions {
268
272
  moduleName?: string;
269
273
  modulesDir?: string;
270
274
  pattern?: ProjectPattern;
275
+ pluralize?: boolean;
271
276
  }
272
277
  declare function generateDto(options: GenerateDtoOptions): Promise<string[]>;
273
278
  //#endregion
@@ -294,7 +299,8 @@ declare function toCamelCase(name: string): string;
294
299
  declare function toKebabCase(name: string): string;
295
300
  /**
296
301
  * Pluralize a kebab-case name for directory/file names.
297
- * If already plural (ends in 's'), returns as-is.
302
+ * Uses the `pluralize` npm package for correct English pluralization
303
+ * including irregulars (person → people, status → statuses, child → children).
298
304
  */
299
305
  declare function pluralize(name: string): string;
300
306
  //#endregion
@@ -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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"}