@wooksjs/event-cli 0.6.1 → 0.6.3

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.
@@ -0,0 +1,541 @@
1
+ # Commands & Help — @wooksjs/event-cli
2
+
3
+ > Covers command registration, command path syntax (arguments, aliases, options, examples), flag/option composables, help generation, and route parameters in CLI context.
4
+
5
+ ## Command Registration
6
+
7
+ ### `app.cli(path, handler)`
8
+
9
+ Register a command with a simple handler:
10
+
11
+ ```ts
12
+ import { createCliApp } from '@wooksjs/event-cli'
13
+
14
+ const app = createCliApp()
15
+
16
+ app.cli('deploy', () => {
17
+ return 'Deploying...'
18
+ })
19
+
20
+ app.run()
21
+ // $ mycli deploy → "Deploying..."
22
+ ```
23
+
24
+ ### `app.cli(path, options)`
25
+
26
+ Register a command with full metadata for help generation:
27
+
28
+ ```ts
29
+ app.cli('deploy/:env', {
30
+ description: 'Deploy to a target environment',
31
+ args: {
32
+ env: 'Target environment (staging, production)',
33
+ },
34
+ options: [
35
+ { keys: ['force', 'f'], description: 'Skip confirmation prompt' },
36
+ { keys: ['tag', 't'], description: 'Docker image tag', value: 'latest' },
37
+ ],
38
+ aliases: ['d'],
39
+ examples: [
40
+ {
41
+ description: 'Deploy to production with a specific tag',
42
+ cmd: 'production -t=v2.1.0',
43
+ },
44
+ ],
45
+ handler: () => {
46
+ const { get } = useRouteParams<{ env: string }>()
47
+ const flags = useCliOptions()
48
+ return `Deploying to ${get('env')} (force=${flags.force || false})`
49
+ },
50
+ })
51
+ ```
52
+
53
+ ### Options object shape
54
+
55
+ ```ts
56
+ interface TWooksCliEntry<T> {
57
+ handler: TWooksHandler<T> // the command handler function
58
+ description?: string // command description for help text
59
+ args?: Record<string, string> // { argName: 'description' }
60
+ options?: Array<{
61
+ keys: string[] // ['verbose', 'v'] → --verbose or -v
62
+ description?: string // option description for help
63
+ value?: string // default/example value shown in help
64
+ }>
65
+ aliases?: string[] // alternative command names
66
+ examples?: Array<{
67
+ description: string // example description
68
+ cmd: string // example command (without CLI name)
69
+ }>
70
+ onRegister?: (path: string, aliasType: number, route?: any) => void
71
+ }
72
+ ```
73
+
74
+ ## Command Path Syntax
75
+
76
+ Command paths use the same `@prostojs/router` syntax as HTTP routes. Space and `/` separators are equivalent.
77
+
78
+ ### Static commands
79
+
80
+ ```ts
81
+ app.cli('deploy', handler) // $ mycli deploy
82
+ app.cli('db migrate', handler) // $ mycli db migrate
83
+ app.cli('db/migrate', handler) // same as above
84
+ app.cli('config set', handler) // $ mycli config set
85
+ ```
86
+
87
+ ### Named arguments (`:argName`)
88
+
89
+ Captures a single positional argument:
90
+
91
+ ```ts
92
+ import { useRouteParams } from '@wooksjs/event-core'
93
+
94
+ app.cli('greet/:name', {
95
+ args: { name: 'Person to greet' },
96
+ handler: () => {
97
+ const { get } = useRouteParams<{ name: string }>()
98
+ return `Hello, ${get('name')}!`
99
+ },
100
+ })
101
+ // $ mycli greet Alice → "Hello, Alice!"
102
+ ```
103
+
104
+ ### Multiple arguments
105
+
106
+ ```ts
107
+ app.cli('copy/:source/:dest', {
108
+ args: {
109
+ source: 'Source file path',
110
+ dest: 'Destination file path',
111
+ },
112
+ handler: () => {
113
+ const { params } = useRouteParams<{ source: string; dest: string }>()
114
+ return `Copying ${params.source} to ${params.dest}`
115
+ },
116
+ })
117
+ // $ mycli copy file.txt backup/file.txt
118
+ ```
119
+
120
+ ### Optional arguments (`:arg?`)
121
+
122
+ Append `?` to make an argument optional. Optional args must be at the end:
123
+
124
+ ```ts
125
+ app.cli('logs/:service/:lines?', {
126
+ args: {
127
+ service: 'Service name',
128
+ lines: 'Number of lines (default: 50)',
129
+ },
130
+ handler: () => {
131
+ const { get } = useRouteParams<{ service: string; lines?: string }>()
132
+ const lines = get('lines') || '50'
133
+ return `Showing last ${lines} lines of ${get('service')}`
134
+ },
135
+ })
136
+ // $ mycli logs api → "Showing last 50 lines of api"
137
+ // $ mycli logs api 100 → "Showing last 100 lines of api"
138
+ ```
139
+
140
+ ### Wildcards (`*`)
141
+
142
+ Capture arbitrary remaining arguments:
143
+
144
+ ```ts
145
+ app.cli('exec/*', {
146
+ handler: () => {
147
+ const { get } = useRouteParams<{ '*': string }>()
148
+ return `Running: ${get('*')}`
149
+ },
150
+ })
151
+ // $ mycli exec npm install → "Running: npm/install"
152
+ ```
153
+
154
+ ### Regex-constrained arguments
155
+
156
+ Restrict what an argument matches:
157
+
158
+ ```ts
159
+ app.cli('migrate/:version(\\d+)', {
160
+ args: { version: 'Migration version number' },
161
+ handler: () => {
162
+ const { get } = useRouteParams<{ version: string }>()
163
+ return `Migrating to version ${get('version')}`
164
+ },
165
+ })
166
+ // $ mycli migrate 42 → matches
167
+ // $ mycli migrate abc → does NOT match (unknown command)
168
+ ```
169
+
170
+ ### Hyphen-separated arguments
171
+
172
+ ```ts
173
+ app.cli('schedule/:from-:to', {
174
+ handler: () => {
175
+ const { get } = useRouteParams<{ from: string; to: string }>()
176
+ return `Scheduled from ${get('from')} to ${get('to')}`
177
+ },
178
+ })
179
+ // $ mycli schedule 09:00-17:00
180
+ ```
181
+
182
+ ## Command Aliases
183
+
184
+ Aliases register additional paths that invoke the same handler:
185
+
186
+ ```ts
187
+ app.cli('deploy/:env', {
188
+ aliases: ['d'],
189
+ args: { env: 'Target environment' },
190
+ handler: () => {
191
+ const { get } = useRouteParams<{ env: string }>()
192
+ return `Deploying to ${get('env')}`
193
+ },
194
+ })
195
+ // $ mycli deploy staging → works
196
+ // $ mycli d staging → also works (alias)
197
+ ```
198
+
199
+ Aliases automatically inherit the command's arguments. The alias `'d'` becomes `'d/:env'`.
200
+
201
+ ## Accessing Flags and Options
202
+
203
+ ### `useCliOptions()`
204
+
205
+ Returns all parsed flags as an object (uses `minimist` under the hood):
206
+
207
+ ```ts
208
+ import { useCliOptions } from '@wooksjs/event-cli'
209
+
210
+ app.cli('build', () => {
211
+ const flags = useCliOptions()
212
+ // $ mycli build --env production --verbose -p 8080
213
+ // flags = { _: ['build'], env: 'production', verbose: true, p: 8080 }
214
+ return `Building for ${flags.env}`
215
+ })
216
+ ```
217
+
218
+ The `_` property contains all positional arguments (including command segments).
219
+
220
+ ### `useCliOption(name)`
221
+
222
+ Get a single option value. Resolves aliases — if you defined `keys: ['verbose', 'v']`, calling `useCliOption('verbose')` also checks `-v`:
223
+
224
+ ```ts
225
+ import { useCliOption } from '@wooksjs/event-cli'
226
+
227
+ app.cli('build', {
228
+ options: [
229
+ { keys: ['env', 'e'], description: 'Target environment', value: 'dev' },
230
+ { keys: ['verbose', 'v'], description: 'Verbose output' },
231
+ ],
232
+ handler: () => {
233
+ const env = useCliOption('env') // checks --env and -e
234
+ const verbose = useCliOption('verbose') // checks --verbose and -v
235
+ return `Building for ${env || 'dev'} (verbose: ${!!verbose})`
236
+ },
237
+ })
238
+ // $ mycli build -e production -v
239
+ ```
240
+
241
+ ### Minimist parsing options
242
+
243
+ Control how flags are parsed by passing options to `app.run()`:
244
+
245
+ ```ts
246
+ // Ensure -A is parsed as boolean (true/false), not string
247
+ await app.run(['build', '-cA'], { boolean: ['A'] })
248
+
249
+ // Negate boolean flags with --no- prefix
250
+ await app.run(['build', '--no-cache'], { boolean: ['cache'] })
251
+ // → { cache: false }
252
+
253
+ // Default values
254
+ await app.run(['build'], { default: { env: 'development' } })
255
+ // → { env: 'development' }
256
+ ```
257
+
258
+ ## Help System
259
+
260
+ ### Registering help metadata
261
+
262
+ Help metadata is defined alongside the command:
263
+
264
+ ```ts
265
+ app.cli('db migrate/:direction?', {
266
+ description: 'Run database migrations',
267
+ args: {
268
+ direction: 'Migration direction: up or down (default: up)',
269
+ },
270
+ options: [
271
+ { keys: ['seed', 's'], description: 'Run seeds after migration' },
272
+ { keys: ['dry-run'], description: 'Preview changes without applying' },
273
+ ],
274
+ examples: [
275
+ { description: 'Run all pending migrations', cmd: 'up' },
276
+ { description: 'Rollback last migration', cmd: 'down' },
277
+ { description: 'Preview migration with seeding', cmd: 'up -s --dry-run' },
278
+ ],
279
+ handler: () => { /* ... */ },
280
+ })
281
+ ```
282
+
283
+ ### `useCliHelp()`
284
+
285
+ Access the help system from within a handler:
286
+
287
+ ```ts
288
+ import { useCliHelp } from '@wooksjs/event-cli'
289
+
290
+ app.cli('help', () => {
291
+ const { print, render, getEntry, getCliHelp } = useCliHelp()
292
+
293
+ print(true) // print help to stdout (with colors)
294
+ print(false) // print without colors
295
+
296
+ const lines = render(80, true) // render as string array (width, colors)
297
+
298
+ const entry = getEntry() // get the matched help entry
299
+ // entry.description, entry.options, entry.args, entry.examples
300
+
301
+ const cliHelp = getCliHelp() // access the CliHelpRenderer directly
302
+ })
303
+ ```
304
+
305
+ ### `useAutoHelp(keys?, colors?)`
306
+
307
+ Automatically prints help when `--help` is passed. Returns `true` if help was printed:
308
+
309
+ ```ts
310
+ import { useAutoHelp } from '@wooksjs/event-cli'
311
+
312
+ app.cli('deploy/:env', {
313
+ description: 'Deploy to environment',
314
+ handler: () => {
315
+ if (useAutoHelp()) return // prints help and returns if --help was passed
316
+
317
+ // Normal handler logic
318
+ const { get } = useRouteParams<{ env: string }>()
319
+ return `Deploying to ${get('env')}`
320
+ },
321
+ })
322
+ // $ mycli deploy --help → prints formatted help
323
+ // $ mycli deploy staging → "Deploying to staging"
324
+ ```
325
+
326
+ Customize the trigger keys and color setting:
327
+
328
+ ```ts
329
+ // Trigger on --help or -h, without colors
330
+ if (useAutoHelp(['help', 'h'], false)) {
331
+ process.exit(0)
332
+ }
333
+ ```
334
+
335
+ ### `useCommandLookupHelp(lookupDepth?)`
336
+
337
+ Searches for similar valid commands when a wrong command is entered. Throws an error with suggestions if found. Best used in `onUnknownCommand`:
338
+
339
+ ```ts
340
+ import { createCliApp, useCommandLookupHelp } from '@wooksjs/event-cli'
341
+
342
+ const app = createCliApp({
343
+ onUnknownCommand: (path, raiseError) => {
344
+ useCommandLookupHelp() // throws with suggestions
345
+ raiseError() // fallback if no suggestions found
346
+ },
347
+ })
348
+ ```
349
+
350
+ The lookup works backwards through the command path:
351
+
352
+ ```
353
+ For command "run test:drive dir":
354
+ lookup 1: "run test:drive dir" (depth 0)
355
+ lookup 2: "run test:drive" (depth 1)
356
+ lookup 3: "run test" (depth 2)
357
+ lookup 4: "run" (depth 3)
358
+ ```
359
+
360
+ Default `lookupDepth` is 3. If a partial match with children is found, it suggests child commands. If arguments are expected, it says what arguments are missing.
361
+
362
+ ### Help Renderer Configuration
363
+
364
+ Customize the help renderer when creating the app:
365
+
366
+ ```ts
367
+ import { CliHelpRenderer } from '@prostojs/cli-help'
368
+
369
+ // Option 1: Pass options
370
+ const app = createCliApp({
371
+ cliHelp: {
372
+ name: 'mycli', // CLI name shown in help and examples
373
+ // marks, width, etc.
374
+ },
375
+ })
376
+
377
+ // Option 2: Pass a pre-configured renderer
378
+ const renderer = new CliHelpRenderer({ name: 'mycli' })
379
+ const app = createCliApp({ cliHelp: renderer })
380
+ ```
381
+
382
+ ## Route Parameters in CLI
383
+
384
+ CLI commands use the same `useRouteParams()` from `@wooksjs/event-core` as HTTP routes:
385
+
386
+ ```ts
387
+ import { useRouteParams } from '@wooksjs/event-core'
388
+
389
+ app.cli('user/:action/:id?', {
390
+ handler: () => {
391
+ const { params, get } = useRouteParams<{
392
+ action: string
393
+ id?: string
394
+ }>()
395
+
396
+ get('action') // 'create', 'delete', etc.
397
+ get('id') // '42' or undefined
398
+ params // { action: 'create', id: '42' }
399
+ },
400
+ })
401
+ ```
402
+
403
+ Parameters are always `string` (or `string[]` for repeated params). Cast numerics yourself: `Number(get('id'))`.
404
+
405
+ ## Common Patterns
406
+
407
+ ### Pattern: Multi-command CLI with help
408
+
409
+ ```ts
410
+ const app = createCliApp({
411
+ onUnknownCommand: (path, raiseError) => {
412
+ useCommandLookupHelp()
413
+ raiseError()
414
+ },
415
+ })
416
+
417
+ app.cli('init/:name?', {
418
+ description: 'Initialize a new project',
419
+ args: { name: 'Project name (default: current directory)' },
420
+ options: [
421
+ { keys: ['template', 't'], description: 'Project template', value: 'default' },
422
+ ],
423
+ handler: () => {
424
+ if (useAutoHelp()) return
425
+ const { get } = useRouteParams<{ name?: string }>()
426
+ const template = useCliOption('template') || 'default'
427
+ return `Initialized ${get('name') || '.'} with template ${template}`
428
+ },
429
+ })
430
+
431
+ app.cli('build', {
432
+ description: 'Build the project',
433
+ options: [
434
+ { keys: ['watch', 'w'], description: 'Watch mode' },
435
+ { keys: ['minify', 'm'], description: 'Minify output' },
436
+ ],
437
+ handler: () => {
438
+ if (useAutoHelp()) return
439
+ const flags = useCliOptions()
440
+ return `Building... (watch=${!!flags.watch}, minify=${!!flags.minify})`
441
+ },
442
+ })
443
+
444
+ app.cli('serve', {
445
+ description: 'Start development server',
446
+ options: [
447
+ { keys: ['port', 'p'], description: 'Port number', value: '3000' },
448
+ { keys: ['host', 'h'], description: 'Host address', value: 'localhost' },
449
+ ],
450
+ handler: () => {
451
+ if (useAutoHelp()) return
452
+ const port = useCliOption('port') || '3000'
453
+ const host = useCliOption('host') || 'localhost'
454
+ return `Serving at http://${host}:${port}`
455
+ },
456
+ })
457
+
458
+ app.run()
459
+ ```
460
+
461
+ ### Pattern: Subcommands with shared logic
462
+
463
+ ```ts
464
+ // Shared composable for database connection
465
+ function useDbConnection() {
466
+ const { store } = useCliContext<{ db?: { conn?: any } }>()
467
+ const { init } = store('db')
468
+ return {
469
+ getConnection: () => init('conn', () => {
470
+ const flags = useCliOptions()
471
+ return connectToDb(flags['db-url'] as string || 'localhost:5432')
472
+ }),
473
+ }
474
+ }
475
+
476
+ app.cli('db migrate', {
477
+ description: 'Run pending migrations',
478
+ handler: async () => {
479
+ const { getConnection } = useDbConnection()
480
+ const db = getConnection()
481
+ await db.migrate()
482
+ return 'Migrations complete'
483
+ },
484
+ })
485
+
486
+ app.cli('db seed', {
487
+ description: 'Run database seeds',
488
+ handler: async () => {
489
+ const { getConnection } = useDbConnection()
490
+ const db = getConnection()
491
+ await db.seed()
492
+ return 'Seeding complete'
493
+ },
494
+ })
495
+ ```
496
+
497
+ ### Pattern: Global flags
498
+
499
+ ```ts
500
+ // Check global flags in every handler
501
+ function useGlobalFlags() {
502
+ const flags = useCliOptions()
503
+ if (flags.verbose || flags.v) {
504
+ const logger = useEventLogger('cli')
505
+ logger.log('Verbose mode enabled')
506
+ }
507
+ return {
508
+ verbose: !!(flags.verbose || flags.v),
509
+ quiet: !!(flags.quiet || flags.q),
510
+ dryRun: !!(flags['dry-run']),
511
+ }
512
+ }
513
+
514
+ app.cli('deploy/:env', {
515
+ handler: () => {
516
+ const { verbose, dryRun } = useGlobalFlags()
517
+ const { get } = useRouteParams<{ env: string }>()
518
+ if (dryRun) return `[DRY RUN] Would deploy to ${get('env')}`
519
+ return `Deploying to ${get('env')}`
520
+ },
521
+ })
522
+ ```
523
+
524
+ ## Best Practices
525
+
526
+ - **Use `app.cli(path, options)` with metadata** — Always provide `description` and `args` so help generation works out of the box.
527
+ - **Use `useAutoHelp()` at the top of every handler** — Gives users consistent `--help` behavior across all commands.
528
+ - **Use `useCliOption(name)` over `useCliOptions()[name]`** — `useCliOption` resolves key aliases (e.g., `--verbose` / `-v`), so it respects your option definitions.
529
+ - **Use `onUnknownCommand` with `useCommandLookupHelp()`** — Provides a much better user experience than a generic "unknown command" error.
530
+ - **Return values from handlers** — Don't call `console.log` directly; the framework formats and outputs return values automatically. This also makes testing easier.
531
+ - **Type your route params** — Use `useRouteParams<{ env: string }>()` for type-safe access.
532
+ - **Use regex constraints for numeric args** — `:version(\\d+)` prevents non-numeric values from matching.
533
+
534
+ ## Gotchas
535
+
536
+ - **Spaces and slashes are equivalent** — `'cmd test'` and `'cmd/test'` register the same command path. When displayed, they appear as spaces.
537
+ - **Flags containing colons** — If a command segment contains a colon that isn't a parameter, escape it: `'config set\\:key'` → matches `$ mycli config set:key`.
538
+ - **`useCliOptions()` includes `_` array** — The `_` property from minimist contains all positional args, including command segments. Use `useRouteParams()` for named arguments instead.
539
+ - **Aliases inherit arguments** — An alias like `'d'` for `'deploy/:env'` automatically becomes `'d/:env'`. You don't need to add the `:env` yourself.
540
+ - **Boolean flag negation** — `--no-cache` with `{ boolean: ['cache'] }` sets `cache: false`. Without the boolean option, it becomes `{ 'no-cache': true }`.
541
+ - **Route precedence** — Static segments match before parametric, parametric before wildcard. `'deploy staging'` is preferred over `'deploy/:env'` when the input is `deploy staging`.