@forinda/kickjs-cli 2.2.3 → 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/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs-cli v2.2.3
2
+ * @forinda/kickjs-cli v2.2.5
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -11,6 +11,7 @@
11
11
  import { dirname, join, resolve } from "node:path";
12
12
  import { createInterface } from "node:readline";
13
13
  import { access, mkdir, readFile, writeFile } from "node:fs/promises";
14
+ import pkg from "pluralize";
14
15
  import { execSync } from "node:child_process";
15
16
  import { readFileSync } from "node:fs";
16
17
  import { fileURLToPath } from "node:url";
@@ -45,26 +46,18 @@ function toKebabCase(name) {
45
46
  }
46
47
  /**
47
48
  * Pluralize a kebab-case name for directory/file names.
48
- * If already plural (ends in 's'), returns as-is.
49
+ * Uses the `pluralize` npm package for correct English pluralization
50
+ * including irregulars (person → people, status → statuses, child → children).
49
51
  */
50
52
  function pluralize(name) {
51
- if (name.endsWith("s")) return name;
52
- if (name.endsWith("x") || name.endsWith("z")) return name + "es";
53
- if (name.endsWith("sh") || name.endsWith("ch")) return name + "es";
54
- if (name.endsWith("y") && !/[aeiou]y$/.test(name)) return name.slice(0, -1) + "ies";
55
- return name + "s";
53
+ return pkg.plural(name);
56
54
  }
57
55
  /**
58
56
  * Pluralize a PascalCase name for class identifiers.
59
- * If already plural (ends in 's'), returns as-is.
60
57
  * Used for `List${pluralPascal}UseCase` to avoid `ListUserssUseCase`.
61
58
  */
62
59
  function pluralizePascal(name) {
63
- if (name.endsWith("s")) return name;
64
- if (name.endsWith("x") || name.endsWith("z")) return name + "es";
65
- if (name.endsWith("sh") || name.endsWith("ch")) return name + "es";
66
- if (name.endsWith("y") && !/[aeiou]y$/i.test(name)) return name.slice(0, -1) + "ies";
67
- return name + "s";
60
+ return pkg.plural(name);
68
61
  }
69
62
  //#endregion
70
63
  //#region src/generators/templates/module-index.ts
@@ -2401,13 +2394,14 @@ const CQRS_FOLDER_MAP = {
2401
2394
  * 3. Standalone default directory
2402
2395
  */
2403
2396
  function resolveOutDir(options) {
2404
- const { type, outDir, moduleName, modulesDir = "src/modules", defaultDir, pattern = "ddd" } = options;
2397
+ const { type, outDir, moduleName, modulesDir = "src/modules", defaultDir, pattern = "ddd", shouldPluralize = true } = options;
2405
2398
  if (outDir) return resolve(outDir);
2406
2399
  if (moduleName) {
2407
2400
  const folderMap = pattern === "ddd" ? DDD_FOLDER_MAP : pattern === "cqrs" ? CQRS_FOLDER_MAP : FLAT_FOLDER_MAP;
2408
- const plural = pluralize(toKebabCase(moduleName));
2401
+ const kebab = toKebabCase(moduleName);
2402
+ const folder = shouldPluralize ? pluralize(kebab) : kebab;
2409
2403
  const subfolder = folderMap[type] ?? "";
2410
- const base = join(modulesDir, plural);
2404
+ const base = join(modulesDir, folder);
2411
2405
  return resolve(subfolder ? join(base, subfolder) : base);
2412
2406
  }
2413
2407
  return resolve(defaultDir);
@@ -2422,7 +2416,8 @@ async function generateMiddleware(options) {
2422
2416
  moduleName,
2423
2417
  modulesDir,
2424
2418
  defaultDir: "src/middleware",
2425
- pattern
2419
+ pattern,
2420
+ shouldPluralize: options.pluralize ?? true
2426
2421
  });
2427
2422
  const kebab = toKebabCase(name);
2428
2423
  const camel = toCamelCase(name);
@@ -2466,7 +2461,8 @@ async function generateGuard(options) {
2466
2461
  moduleName,
2467
2462
  modulesDir,
2468
2463
  defaultDir: "src/guards",
2469
- pattern
2464
+ pattern,
2465
+ shouldPluralize: options.pluralize ?? true
2470
2466
  });
2471
2467
  const kebab = toKebabCase(name);
2472
2468
  const camel = toCamelCase(name);
@@ -2523,7 +2519,8 @@ async function generateService(options) {
2523
2519
  moduleName,
2524
2520
  modulesDir,
2525
2521
  defaultDir: "src/services",
2526
- pattern
2522
+ pattern,
2523
+ shouldPluralize: options.pluralize ?? true
2527
2524
  });
2528
2525
  const kebab = toKebabCase(name);
2529
2526
  const pascal = toPascalCase(name);
@@ -2552,7 +2549,8 @@ async function generateController(options) {
2552
2549
  moduleName,
2553
2550
  modulesDir,
2554
2551
  defaultDir: "src/controllers",
2555
- pattern
2552
+ pattern,
2553
+ shouldPluralize: options.pluralize ?? true
2556
2554
  });
2557
2555
  const kebab = toKebabCase(name);
2558
2556
  const pascal = toPascalCase(name);
@@ -2593,7 +2591,8 @@ async function generateDto(options) {
2593
2591
  moduleName,
2594
2592
  modulesDir,
2595
2593
  defaultDir: "src/dtos",
2596
- pattern
2594
+ pattern,
2595
+ shouldPluralize: options.pluralize ?? true
2597
2596
  });
2598
2597
  const kebab = toKebabCase(name);
2599
2598
  const pascal = toPascalCase(name);
@@ -3006,18 +3005,30 @@ export class UserService {
3006
3005
 
3007
3006
  ### Modules
3008
3007
 
3009
- Register controllers and providers in modules:
3008
+ Modules implement \`AppModule\` and wire controllers via \`buildRoutes()\`:
3010
3009
 
3011
3010
  \`\`\`ts
3012
- import { Module } from '@forinda/kickjs'
3011
+ import { type AppModule, type ModuleRoutes, buildRoutes } from '@forinda/kickjs'
3013
3012
  import { UserController } from './user.controller'
3014
- import { UserService } from './user.service'
3015
3013
 
3016
- @Module({
3017
- controllers: [UserController],
3018
- providers: [UserService],
3019
- })
3020
- export class UserModule {}
3014
+ export class UserModule implements AppModule {
3015
+ routes(): ModuleRoutes {
3016
+ return {
3017
+ path: '/users',
3018
+ router: buildRoutes(UserController),
3019
+ controller: UserController,
3020
+ }
3021
+ }
3022
+ }
3023
+ \`\`\`
3024
+
3025
+ Register all modules in \`src/modules/index.ts\`:
3026
+
3027
+ \`\`\`ts
3028
+ import type { AppModuleClass } from '@forinda/kickjs'
3029
+ import { UserModule } from './user/user.module'
3030
+
3031
+ export const modules: AppModuleClass[] = [UserModule]
3021
3032
  \`\`\`
3022
3033
 
3023
3034
  ### RequestContext
@@ -3128,6 +3139,86 @@ Hot-reload of \`.env\` changes during dev is wired up automatically via
3128
3139
  \`envWatchPlugin()\` in \`vite.config.ts\` — edit \`.env\`, the dev server
3129
3140
  reloads, and the next \`config.get()\` re-parses with the new values.
3130
3141
 
3142
+ ### Standalone Env Utilities (No DI Required)
3143
+
3144
+ These functions work anywhere — scripts, CLI tools, plain files, outside \`@Service\`/\`@Controller\`:
3145
+
3146
+ \`\`\`ts
3147
+ import { defineEnv, loadEnv, getEnv, reloadEnv, resetEnvCache, baseEnvSchema } from '@forinda/kickjs/config'
3148
+ import { z } from 'zod'
3149
+
3150
+ // Define and parse schema
3151
+ const schema = defineEnv((base) =>
3152
+ base.extend({ DATABASE_URL: z.string().url() })
3153
+ )
3154
+ const env = loadEnv(schema) // Parse + validate process.env
3155
+ console.log(env.PORT) // 3000 (coerced to number)
3156
+ console.log(env.DATABASE_URL) // validated URL string
3157
+
3158
+ // Get single value
3159
+ const port = getEnv('PORT') // typed after kick typegen
3160
+
3161
+ // Reload after .env changes (HMR calls this automatically)
3162
+ reloadEnv()
3163
+
3164
+ // Reset cache in tests that swap schemas
3165
+ resetEnvCache()
3166
+ \`\`\`
3167
+
3168
+ | Function | Purpose |
3169
+ |----------|---------|
3170
+ | \`defineEnv(fn)\` | Extend base schema with custom Zod keys |
3171
+ | \`loadEnv(schema?)\` | Parse \`process.env\`, validate, cache, return typed object |
3172
+ | \`getEnv(key, schema?)\` | Get single validated env value |
3173
+ | \`reloadEnv()\` | Re-read \`.env\` from disk, re-parse with same schema |
3174
+ | \`resetEnvCache()\` | Clear parsed cache AND registered schema (for tests) |
3175
+ | \`baseEnvSchema\` | Base Zod schema: \`PORT\`, \`NODE_ENV\`, \`LOG_LEVEL\` |
3176
+
3177
+ ## Standalone Utilities (No DI Required)
3178
+
3179
+ These utilities work outside decorated classes:
3180
+
3181
+ ### Logger
3182
+
3183
+ \`\`\`ts
3184
+ import { Logger, createLogger } from '@forinda/kickjs'
3185
+
3186
+ const log = Logger.for('MyScript') // Static factory
3187
+ log.info('Processing started')
3188
+ log.error('Something failed')
3189
+
3190
+ const log2 = createLogger('Worker') // Function form
3191
+ \`\`\`
3192
+
3193
+ ### Injection Tokens
3194
+
3195
+ \`\`\`ts
3196
+ import { createToken } from '@forinda/kickjs'
3197
+
3198
+ // Type-safe DI tokens for factory/interface binding
3199
+ const DB_URL = createToken<string>('config.database.url')
3200
+ const FEATURE_FLAGS = createToken<FeatureFlags>('app.features')
3201
+ \`\`\`
3202
+
3203
+ ### Reactivity
3204
+
3205
+ \`\`\`ts
3206
+ import { ref, computed, watch, reactive } from '@forinda/kickjs'
3207
+
3208
+ const count = ref(0)
3209
+ const doubled = computed(() => count.value * 2)
3210
+ const stop = watch(() => count.value, (val) => console.log(val))
3211
+ count.value++ // logs 1
3212
+ \`\`\`
3213
+
3214
+ ### HTTP Errors
3215
+
3216
+ \`\`\`ts
3217
+ import { HttpException, HttpStatus } from '@forinda/kickjs'
3218
+
3219
+ throw new HttpException(HttpStatus.NOT_FOUND, 'User not found')
3220
+ \`\`\`
3221
+
3131
3222
  ## Testing
3132
3223
 
3133
3224
  Tests live in \`src/**/*.test.ts\`:
@@ -3162,7 +3253,6 @@ Run tests:
3162
3253
  - \`@Roles('admin', 'user')\` — role-based access control
3163
3254
 
3164
3255
  ### DI Decorators
3165
- - \`@Module({ controllers, providers, imports })\` — define module
3166
3256
  - \`@Service()\` — singleton service (DI-registered)
3167
3257
  - \`@Repository()\` — repository (semantic alias for @Service)
3168
3258
  - \`@Autowired()\` — property injection
@@ -3240,7 +3330,7 @@ ${template === "ddd" ? `\`\`\`
3240
3330
  ├── <name>.repository.ts # Data access (@Repository)
3241
3331
  ├── <name>.dto.ts # Request/response schemas (Zod)
3242
3332
  ├── <name>.entity.ts # Domain entity (optional)
3243
- └── <name>.module.ts # Module definition (@Module)
3333
+ └── <name>.module.ts # Module definition (implements AppModule)
3244
3334
  \`\`\`
3245
3335
  ` : template === "cqrs" ? `\`\`\`
3246
3336
  <name>/
@@ -3254,7 +3344,7 @@ ${template === "ddd" ? `\`\`\`
3254
3344
  │ └── <name>-created.event.ts
3255
3345
  ├── <name>.controller.ts # HTTP routes
3256
3346
  ├── <name>.repository.ts # Data access
3257
- └── <name>.module.ts # Module definition
3347
+ └── <name>.module.ts # Module definition (implements AppModule)
3258
3348
  \`\`\`
3259
3349
  ` : template === "graphql" ? `\`\`\`
3260
3350
  resolvers/
@@ -3267,7 +3357,7 @@ resolvers/
3267
3357
  ├── <name>.controller.ts # HTTP routes (@Controller)
3268
3358
  ├── <name>.service.ts # Business logic (@Service)
3269
3359
  ├── <name>.dto.ts # Request/response schemas (Zod)
3270
- └── <name>.module.ts # Module definition (@Module)
3360
+ └── <name>.module.ts # Module definition (implements AppModule)
3271
3361
  \`\`\`
3272
3362
  ` : `\`\`\`
3273
3363
  src/
@@ -3303,8 +3393,8 @@ If not using generators:
3303
3393
  - [ ] Create \`src/modules/<name>/<name>.controller.ts\`
3304
3394
  - [ ] Add \`@Controller('/path')\` decorator
3305
3395
  - [ ] Add route handlers with \`@Get()\`, \`@Post()\`, etc.
3306
- - [ ] Create module file with \`@Module({ controllers: [NameController] })\`
3307
- - [ ] Register module in \`src/modules/index.ts\`
3396
+ - [ ] Create module file implementing \`AppModule\` with \`routes()\` returning \`{ path, router: buildRoutes(Controller), controller }\`
3397
+ - [ ] Register module in \`src/modules/index.ts\` (\`AppModuleClass[]\` array)
3308
3398
  - [ ] Test with \`kick dev\`
3309
3399
 
3310
3400
  ### Manual Service
@@ -3312,7 +3402,7 @@ If not using generators:
3312
3402
  - [ ] Create \`src/modules/<name>/<name>.service.ts\`
3313
3403
  - [ ] Add \`@Service()\` decorator
3314
3404
  - [ ] Inject dependencies with \`@Autowired()\`
3315
- - [ ] Register in module \`providers\` array
3405
+ - [ ] Inject via \`@Autowired()\` where needed
3316
3406
  - [ ] Write unit tests
3317
3407
 
3318
3408
  ### New Middleware
@@ -3337,9 +3427,12 @@ Use \`kick add\` to install KickJS packages with correct peer dependencies:
3337
3427
  ### Generate CRUD Module
3338
3428
 
3339
3429
  \`\`\`bash
3340
- kick g scaffold user name:string email:string age:number
3430
+ kick g scaffold user name:string email:string:optional age:number
3341
3431
  \`\`\`
3342
3432
 
3433
+ Append \`:optional\` for optional fields (shell-safe, no quoting needed).
3434
+ Quoted \`?\` syntax also works: \`"email:string?"\` or \`"email?:string"\`.
3435
+
3343
3436
  This creates a full CRUD module with:
3344
3437
  - Controller with GET, POST, PUT, DELETE routes
3345
3438
  - Service with business logic
@@ -3455,7 +3548,17 @@ private config!: ConfigService
3455
3548
  const port = this.config.get('PORT') // typed: number
3456
3549
  \`\`\`
3457
3550
 
3458
- 3. **Direct \`process.env\`**avoid in app code; bypasses Zod
3551
+ 3. **Standalone utilities** (no DI works in scripts, CLI, plain files):
3552
+ \`\`\`ts
3553
+ import { loadEnv, getEnv, reloadEnv, resetEnvCache } from '@forinda/kickjs/config'
3554
+
3555
+ const env = loadEnv(schema) // Parse + validate all vars
3556
+ const port = getEnv('PORT') // Single value lookup
3557
+ reloadEnv() // Re-read .env from disk
3558
+ resetEnvCache() // Full reset (for tests)
3559
+ \`\`\`
3560
+
3561
+ 4. **Direct \`process.env\`** — avoid in app code; bypasses Zod
3459
3562
  coercion and the typed \`KickEnv\` registry.
3460
3563
 
3461
3564
  > **Pitfall**: never delete \`import './config'\` from \`src/index.ts\`.
@@ -3464,6 +3567,22 @@ const port = this.config.get('PORT') // typed: number
3464
3567
  > \`@Value()\` only works because of its raw \`process.env\` fallback —
3465
3568
  > Zod coercion + schema defaults are silently skipped.
3466
3569
 
3570
+ ## Standalone Utilities (No DI Required)
3571
+
3572
+ These work anywhere — scripts, plain files, outside \`@Service\`/\`@Controller\`:
3573
+
3574
+ | Utility | Import | Example |
3575
+ |---------|--------|---------|
3576
+ | \`Logger.for(name)\` | \`@forinda/kickjs\` | \`const log = Logger.for('MyScript')\` |
3577
+ | \`createLogger(name)\` | \`@forinda/kickjs\` | \`const log = createLogger('Worker')\` |
3578
+ | \`createToken<T>(name)\` | \`@forinda/kickjs\` | \`const TOKEN = createToken<string>('db.url')\` |
3579
+ | \`ref(value)\` | \`@forinda/kickjs\` | \`const count = ref(0)\` |
3580
+ | \`computed(fn)\` | \`@forinda/kickjs\` | \`const doubled = computed(() => count.value * 2)\` |
3581
+ | \`watch(source, cb)\` | \`@forinda/kickjs\` | \`watch(() => count.value, (v) => log(v))\` |
3582
+ | \`reactive(obj)\` | \`@forinda/kickjs\` | \`const state = reactive({ count: 0 })\` |
3583
+ | \`HttpException\` | \`@forinda/kickjs\` | \`throw new HttpException(404, 'Not found')\` |
3584
+ | \`HttpStatus\` | \`@forinda/kickjs\` | \`HttpStatus.NOT_FOUND // 404\` |
3585
+
3467
3586
  ## Key Decorators
3468
3587
 
3469
3588
  ### HTTP Routes
@@ -3478,7 +3597,7 @@ const port = this.config.get('PORT') // typed: number
3478
3597
  ### Dependency Injection
3479
3598
  | Decorator | Purpose |
3480
3599
  |-----------|---------|
3481
- | \`@Module({})\` | Define feature module |
3600
+ | \`AppModule\` interface | Define feature module (implements \`routes()\`) |
3482
3601
  | \`@Service()\` | Register singleton service |
3483
3602
  | \`@Repository()\` | Register repository |
3484
3603
  | \`@Autowired()\` | Property injection |
@@ -3574,6 +3693,26 @@ async function initProject(options) {
3574
3693
  await writeFileSafe(join(dir, "README.md"), generateReadme(name, template, packageManager));
3575
3694
  await writeFileSafe(join(dir, "CLAUDE.md"), generateClaude(name, template, packageManager));
3576
3695
  await writeFileSafe(join(dir, "AGENTS.md"), generateAgents(name, template, packageManager));
3696
+ if (options.installDeps) {
3697
+ console.log(`\n Installing dependencies with ${packageManager}...\n`);
3698
+ try {
3699
+ execSync(`${packageManager} install`, {
3700
+ cwd: dir,
3701
+ stdio: "inherit"
3702
+ });
3703
+ console.log("\n Dependencies installed successfully!");
3704
+ } catch {
3705
+ console.log(`\n Warning: ${packageManager} install failed. Run it manually.`);
3706
+ }
3707
+ }
3708
+ try {
3709
+ const { runTypegen } = await import("./typegen-CTXqSva4.mjs");
3710
+ await runTypegen({
3711
+ cwd: dir,
3712
+ allowDuplicates: true,
3713
+ silent: true
3714
+ });
3715
+ } catch {}
3577
3716
  if (options.initGit) try {
3578
3717
  execSync("git init", {
3579
3718
  cwd: dir,
@@ -3595,26 +3734,6 @@ async function initProject(options) {
3595
3734
  } catch {
3596
3735
  log("Warning: git init failed (git may not be installed)");
3597
3736
  }
3598
- if (options.installDeps) {
3599
- console.log(`\n Installing dependencies with ${packageManager}...\n`);
3600
- try {
3601
- execSync(`${packageManager} install`, {
3602
- cwd: dir,
3603
- stdio: "inherit"
3604
- });
3605
- console.log("\n Dependencies installed successfully!");
3606
- } catch {
3607
- console.log(`\n Warning: ${packageManager} install failed. Run it manually.`);
3608
- }
3609
- }
3610
- try {
3611
- const { runTypegen } = await import("./typegen-BncsvEr-.mjs");
3612
- await runTypegen({
3613
- cwd: dir,
3614
- allowDuplicates: true,
3615
- silent: true
3616
- });
3617
- } catch {}
3618
3737
  console.log("\n Project scaffolded successfully!");
3619
3738
  console.log();
3620
3739
  const needsCd = dir !== process.cwd();