@wooksjs/event-cli 0.6.6 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,353 +1,136 @@
1
- # Core Concepts — @wooksjs/event-cli
1
+ # Core setup & routing — @wooksjs/event-cli
2
2
 
3
- > Covers CLI app creation, running commands, how the CLI adapter integrates with the event context system, error handling, unknown command handling, and testing.
3
+ > Creating a CLI app, registering commands, running them, and architecture overview.
4
4
 
5
- For the underlying event context store API (`init`, `get`, `set`, `hook`, etc.) and how to create custom composables, see [event-core.md](event-core.md).
5
+ ## Concepts
6
6
 
7
- ## Mental Model
7
+ `@wooksjs/event-cli` wraps command-line argument parsing with Wooks' composable architecture. Each command invocation gets its own `EventContext`, and handlers are plain functions. Command routing uses the same `@prostojs/router` as the HTTP adapter — supporting parameters, optional params, wildcards, and aliases.
8
8
 
9
- `@wooksjs/event-cli` is the CLI adapter for Wooks. It turns every CLI invocation into an event with its own isolated context store. Instead of imperative argument parsing, you call composable functions (`useCliOptions()`, `useCliHelp()`, etc.) from anywhere in your command handler — flags and arguments are parsed on demand and cached per invocation.
10
-
11
- Key principles:
12
- 1. **Commands are routes** — CLI command paths use the same `@prostojs/router` as HTTP routes, with params (`:name`), optional segments (`:arg?`), and wildcards.
13
- 2. **Flags are composable** — Call `useCliOptions()` or `useCliOption('verbose')` from anywhere in the handler chain to get parsed flags.
14
- 3. **Built-in help generation** — Register descriptions, options, args, examples, and aliases alongside your commands. The framework generates formatted help text.
9
+ Arguments are parsed with `minimist`. Help text is generated automatically from command metadata via `@prostojs/cli-help`.
15
10
 
16
11
  ## Installation
17
12
 
18
13
  ```bash
19
- npm install wooks @wooksjs/event-cli
20
- ```
21
-
22
- ## Creating a CLI App
23
-
24
- ```ts
25
- import { createCliApp } from '@wooksjs/event-cli'
26
-
27
- const app = createCliApp()
28
-
29
- app.cli('hello', () => 'Hello World!')
30
-
31
- app.run()
32
- ```
33
-
34
- `createCliApp(opts?, wooks?)` returns a `WooksCli` instance. Options:
35
-
36
- ```ts
37
- interface TWooksCliOptions {
38
- onError?: (e: Error) => void // custom error handler
39
- onNotFound?: TWooksHandler // custom not-found handler
40
- onUnknownCommand?: (params: string[], raiseError: () => void) => unknown
41
- logger?: TConsoleBase // custom logger
42
- eventOptions?: TEventOptions // event-level logger config
43
- cliHelp?: TCliHelpRenderer | TCliHelpOptions // help renderer or options
44
- router?: {
45
- ignoreTrailingSlash?: boolean
46
- ignoreCase?: boolean
47
- cacheLimit?: number
48
- }
49
- }
50
- ```
51
-
52
- ## Running Commands
53
-
54
- ### `app.run(argv?, opts?)`
55
-
56
- Starts command processing. By default reads `process.argv.slice(2)`:
57
-
58
- ```ts
59
- // Default: reads from process.argv
60
- app.run()
61
-
62
- // Override argv for testing or programmatic use
63
- await app.run(['greet', 'Alice', '--verbose'])
64
-
65
- // Pass minimist options to control flag parsing
66
- await app.run(['cmd', '-cA'], { boolean: ['A'] })
67
- ```
68
-
69
- **How `run()` works:**
70
-
71
- 1. Parses `argv` with `minimist` to separate positional args from flags.
72
- 2. Builds a route path from positional args: `['greet', 'Alice']` becomes `/greet/Alice`.
73
- 3. Creates a CLI event context with the parsed data.
74
- 4. Looks up the matching command handler via the router.
75
- 5. Executes the handler and processes the return value.
76
-
77
- ### Return value handling
78
-
79
- The handler's return value is automatically output:
80
-
81
- ```ts
82
- // String → console.log as-is
83
- app.cli('text', () => 'plain text')
84
-
85
- // Object → JSON.stringify with indentation
86
- app.cli('json', () => ({ key: 'value' }))
87
-
88
- // Array → each element logged separately (strings as-is, objects as JSON)
89
- app.cli('list', () => ['line1', 'line2', { data: true }])
90
-
91
- // undefined → no output
92
- app.cli('silent', () => { /* side effects only */ })
93
-
94
- // Error → triggers onError handler
95
- app.cli('fail', () => new Error('something broke'))
96
- ```
97
-
98
- ## How CLI Context Works
99
-
100
- When `run()` is called, the adapter creates a CLI-specific event context:
101
-
102
- ```
103
- argv arrives
104
- → minimist parses flags
105
- → createCliContext({ argv, pathParams, cliHelp, command }, options)
106
- → AsyncLocalStorage.run(cliContextStore, handler)
107
- → router matches command path → handler runs
108
- → handler calls useCliOptions(), useCliHelp(), etc.
109
- → each composable calls useCliContext()
110
- → reads/writes the CLI context store via init(), get(), set()
111
- ```
112
-
113
- ### The CLI Context Store
114
-
115
- The CLI adapter extends the base event context with:
116
-
117
- ```ts
118
- interface TCliContextStore {
119
- flags?: Record<string, boolean | string> // parsed minimist flags
120
- }
121
- ```
122
-
123
- The `event` section contains CLI-specific data:
124
-
125
- ```ts
126
- interface TCliEventData {
127
- argv: string[] // raw argv array
128
- pathParams: string[] // positional args (non-flag)
129
- command: string // matched command path
130
- opts?: minimist.Opts // minimist options passed to run()
131
- type: 'CLI' // event type identifier
132
- cliHelp: TCliHelpRenderer // help renderer instance
133
- }
134
- ```
135
-
136
- ### Extending the CLI Store for Custom Composables
137
-
138
- Create custom composables for CLI by extending the store type via the generic parameter on `useCliContext`:
139
-
140
- ```ts
141
- import { useCliContext } from '@wooksjs/event-cli'
142
-
143
- interface TMyStore {
144
- config?: {
145
- loaded?: Record<string, unknown> | null
146
- configPath?: string
147
- }
148
- }
149
-
150
- export function useConfig() {
151
- const { store } = useCliContext<TMyStore>()
152
- const { init } = store('config')
153
-
154
- const loadConfig = () =>
155
- init('loaded', () => {
156
- const flags = useCliOptions()
157
- const configPath = flags.config as string || './config.json'
158
- return JSON.parse(fs.readFileSync(configPath, 'utf-8'))
159
- })
160
-
161
- return { loadConfig }
162
- }
14
+ pnpm add @wooksjs/event-cli
163
15
  ```
164
16
 
165
- For the full context store API and composable patterns, see [event-core.md](event-core.md).
17
+ Peer dependencies: `@wooksjs/event-core`, `wooks`, `@prostojs/router`, `@prostojs/logger`.
166
18
 
167
- ## Error Handling
19
+ ## API Reference
168
20
 
169
- ### Default behavior
21
+ ### `createCliApp(opts?): WooksCli`
170
22
 
171
- By default, errors call `console.error` and `process.exit(1)`:
23
+ Creates a CLI application instance.
172
24
 
173
25
  ```ts
174
- app.cli('deploy', () => {
175
- throw new Error('Deployment failed')
176
- })
177
- // Output: ERROR: Deployment failed
178
- // Process exits with code 1
179
- ```
26
+ import { createCliApp } from '@wooksjs/event-cli'
180
27
 
181
- ### Custom error handler
28
+ const app = createCliApp()
182
29
 
183
- ```ts
184
- const app = createCliApp({
185
- onError: (error) => {
186
- console.error(`[FATAL] ${error.message}`)
187
- process.exit(2)
188
- },
30
+ app.cli('greet/:name', () => {
31
+ const { params } = useRouteParams<{ name: string }>()
32
+ return `Hello, ${params.name}!`
189
33
  })
190
- ```
191
34
 
192
- ### Returning errors
193
-
194
- Handlers can also return `Error` objects (not throw). The adapter passes them to `onError`:
195
-
196
- ```ts
197
- app.cli('validate', () => {
198
- if (!isValid()) return new Error('Validation failed')
199
- return 'OK'
200
- })
35
+ app.run() // processes process.argv
201
36
  ```
202
37
 
203
- ## Unknown Command Handling
204
-
205
- When no route matches the provided command:
206
-
207
- ### Default behavior
38
+ **Options (`TWooksCliOptions`):**
208
39
 
209
- Prints `"Unknown command: <args>"` and exits with code 1.
210
-
211
- ### Custom `onUnknownCommand`
212
-
213
- ```ts
214
- const app = createCliApp({
215
- onUnknownCommand: (pathParams, raiseError) => {
216
- // pathParams = ['unknown', 'subcommand']
217
- console.log(`Did you mean something else?`)
218
- // Call raiseError() to use the default error behavior
219
- raiseError()
220
- },
221
- })
222
- ```
40
+ | Option | Type | Description |
41
+ | ------------------ | ------------------------------------- | --------------------------------------------------------- |
42
+ | `logger` | `TConsoleBase` | Custom logger |
43
+ | `onError` | `(e: Error) => void` | Custom error handler (default: print + `process.exit(1)`) |
44
+ | `onNotFound` | `TWooksHandler` | Handler for unknown commands |
45
+ | `onUnknownCommand` | `(params, raiseError) => unknown` | Callback before "unknown command" error |
46
+ | `cliHelp` | `TCliHelpRenderer \| TCliHelpOptions` | Help renderer or options |
47
+ | `router` | router options | Custom router configuration |
223
48
 
224
- ### Smart command suggestions with `useCommandLookupHelp()`
49
+ ### `app.cli(path, handler | options)`
225
50
 
226
- The best pattern for unknown commands — suggests similar commands:
51
+ Register a CLI command. Path segments can use `/` or space as separators.
227
52
 
228
53
  ```ts
229
- import { createCliApp, useCommandLookupHelp } from '@wooksjs/event-cli'
230
-
231
- const app = createCliApp({
232
- onUnknownCommand: (path, raiseError) => {
233
- useCommandLookupHelp() // throws with suggestions if found
234
- raiseError() // fallback
235
- },
54
+ // Simple handler
55
+ app.cli('build/:target', () => {
56
+ const { params } = useRouteParams<{ target: string }>()
57
+ return `Building ${params.target}...`
236
58
  })
237
- ```
238
59
 
239
- Output example:
240
- ```
241
- ERROR: Wrong command, did you mean:
242
- $ mycli deploy staging
243
- $ mycli deploy production
60
+ // Equivalent paths:
61
+ app.cli('deploy staging', handler)
62
+ app.cli('deploy/staging', handler)
244
63
  ```
245
64
 
246
- ## Custom Not-Found Handler
65
+ ### `app.run(argv?, minimistOpts?)`
247
66
 
248
- An alternative to `onUnknownCommand` — uses the standard Wooks handler pattern:
67
+ Start command processing. Defaults to `process.argv.slice(2)`.
249
68
 
250
69
  ```ts
251
- const app = createCliApp({
252
- onNotFound: () => {
253
- console.log('Command not recognized. Run --help for usage.')
254
- process.exit(1)
255
- },
256
- })
257
- ```
258
-
259
- ## Sharing Router Between Adapters
70
+ // Use process.argv
71
+ await app.run()
260
72
 
261
- Multiple adapters can share the same Wooks router:
73
+ // Override argv
74
+ await app.run(['build', 'production', '--verbose'])
262
75
 
263
- ```ts
264
- import { Wooks } from 'wooks'
265
- import { createCliApp } from '@wooksjs/event-cli'
266
-
267
- const wooks = new Wooks()
268
- const app1 = createCliApp({}, wooks)
269
- const app2 = createCliApp({}, wooks) // shares the same routes
76
+ // With minimist options
77
+ await app.run(['cmd', '-cA'], { boolean: ['A'] })
270
78
  ```
271
79
 
272
- Or share via another adapter instance:
80
+ Returns the handler's return value, or an `Error` if the command fails.
273
81
 
274
- ```ts
275
- const app1 = createCliApp()
276
- const app2 = createCliApp({}, app1) // shares app1's router
277
- ```
278
-
279
- ## Testing
82
+ ## Command routing
280
83
 
281
- Use `app.run()` with explicit argv arrays to test commands programmatically:
84
+ Routes use the same `@prostojs/router` as HTTP. Command path segments become route segments:
282
85
 
283
86
  ```ts
284
- import { createCliApp } from '@wooksjs/event-cli'
285
- import { useRouteParams } from '@wooksjs/event-core'
87
+ // Static command
88
+ app.cli('init', handler) // $ mycli init
286
89
 
287
- const app = createCliApp({
288
- onError: (e) => { /* don't exit in tests */ },
289
- })
90
+ // With parameter
91
+ app.cli('build/:target', handler) // $ mycli build production
290
92
 
291
- app.cli('greet/:name', () => {
292
- const { get } = useRouteParams<{ name: string }>()
293
- return `Hello, ${get('name')}!`
294
- })
93
+ // Optional parameter
94
+ app.cli('test/:suite?', handler) // $ mycli test (suite = undefined)
95
+ // $ mycli test unit (suite = 'unit')
295
96
 
296
- // Test:
297
- const result = await app.run(['greet', 'Alice'])
298
- // Handler outputs: "Hello, Alice!"
97
+ // Multi-segment
98
+ app.cli('deploy staging setup', handler) // $ mycli deploy staging setup
299
99
  ```
300
100
 
301
- ### Testing with flags
101
+ Route params accessed via `useRouteParams()`:
302
102
 
303
103
  ```ts
304
- app.cli('deploy', () => {
305
- const flags = useCliOptions()
306
- return flags.env as string || 'development'
307
- })
308
-
309
- await app.run(['deploy', '--env', 'production'])
310
- // Output: "production"
311
-
312
- // With minimist options for boolean flags:
313
- await app.run(['deploy', '-v'], { boolean: ['v'] })
314
- ```
315
-
316
- ## Logging
317
-
318
- Get a scoped logger from the app:
104
+ import { useRouteParams } from '@wooksjs/event-cli'
319
105
 
320
- ```ts
321
- const app = createCliApp()
322
- const logger = app.getLogger('[my-cli]')
323
- logger.log('CLI started')
106
+ app.cli('generate/:type/:name', () => {
107
+ const { params } = useRouteParams<{ type: string; name: string }>()
108
+ return `Generating ${params.type}: ${params.name}`
109
+ })
324
110
  ```
325
111
 
326
- Inside a handler, use the event-scoped logger:
112
+ ## Response handling
327
113
 
328
- ```ts
329
- import { useEventLogger } from '@wooksjs/event-core'
114
+ Handler return values are printed to stdout:
330
115
 
331
- app.cli('process', () => {
332
- const logger = useEventLogger('process-handler')
333
- logger.log('Processing command')
334
- return 'done'
335
- })
336
- ```
116
+ | Return type | Behavior |
117
+ | ----------- | ------------------------------------------------- |
118
+ | `string` | `console.log(value)` |
119
+ | `array` | Each item logged (strings as-is, objects as JSON) |
120
+ | `object` | `console.log(JSON.stringify(value, null, ' '))` |
121
+ | `Error` | Passed to `onError` handler |
122
+ | `undefined` | Nothing printed |
337
123
 
338
124
  ## Best Practices
339
125
 
340
- - **Use `createCliApp()` factory** — Don't instantiate `WooksCli` directly unless extending the class.
341
- - **Use `app.run(argv)` for testing** — Pass explicit argv arrays to test commands without launching a process.
342
- - **Use `onUnknownCommand` with `useCommandLookupHelp()`** — Gives users helpful suggestions when they mistype commands.
343
- - **Use minimist opts for boolean flags** — Pass `{ boolean: ['verbose', 'v'] }` to `app.run()` to ensure flags like `-v` are parsed as booleans, not strings.
344
- - **Return values instead of `console.log`** — The framework handles output formatting. Returning lets you test handlers more easily.
126
+ - Use `useRouteParams()` for positional arguments and `useCliOptions()` for flags
127
+ - Prefer the options object form of `app.cli()` to get auto-generated help
128
+ - Use `onUnknownCommand` with `useCommandLookupHelp()` for "did you mean?" suggestions
129
+ - Command paths with spaces and `/` are equivalent — use whichever is more readable
345
130
 
346
131
  ## Gotchas
347
132
 
348
- - **Composables must be called within a command handler** (inside the async context). Calling them at module load time throws.
349
- - **`run()` returns a promise** — Always `await` it, especially in tests.
350
- - **Command paths use `/` or space separators** — `'cmd test'` and `'cmd/test'` are equivalent.
351
- - **Flags with `--no-` prefix** — `--no-verbose` sets `verbose: false` when using `{ boolean: ['verbose'] }` in minimist opts.
352
- - **Positional args after flags** — minimist's `_` array contains all positional arguments, including the command segments themselves. Use `useRouteParams()` to access command arguments, not raw `_`.
353
- - **URI encoding** — Command segments containing `/` are automatically URI-encoded so they don't create extra path segments.
133
+ - `app.run()` is async — always `await` it
134
+ - Default error handling calls `process.exit(1)` — override with `onError` for testing
135
+ - Colons in command names need escaping: `app.cli('use\\:dev', handler)` for `$ mycli use:dev`
136
+ - `minimist` treats `--no-flag` as `flag: false` when the flag is in `boolean` array