@wooksjs/event-cli 0.6.2 → 0.6.4

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,353 @@
1
+ # Core Concepts — @wooksjs/event-cli
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.
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).
6
+
7
+ ## Mental Model
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.
15
+
16
+ ## Installation
17
+
18
+ ```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
+ }
163
+ ```
164
+
165
+ For the full context store API and composable patterns, see [event-core.md](event-core.md).
166
+
167
+ ## Error Handling
168
+
169
+ ### Default behavior
170
+
171
+ By default, errors call `console.error` and `process.exit(1)`:
172
+
173
+ ```ts
174
+ app.cli('deploy', () => {
175
+ throw new Error('Deployment failed')
176
+ })
177
+ // Output: ERROR: Deployment failed
178
+ // Process exits with code 1
179
+ ```
180
+
181
+ ### Custom error handler
182
+
183
+ ```ts
184
+ const app = createCliApp({
185
+ onError: (error) => {
186
+ console.error(`[FATAL] ${error.message}`)
187
+ process.exit(2)
188
+ },
189
+ })
190
+ ```
191
+
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
+ })
201
+ ```
202
+
203
+ ## Unknown Command Handling
204
+
205
+ When no route matches the provided command:
206
+
207
+ ### Default behavior
208
+
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
+ ```
223
+
224
+ ### Smart command suggestions with `useCommandLookupHelp()`
225
+
226
+ The best pattern for unknown commands — suggests similar commands:
227
+
228
+ ```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
+ },
236
+ })
237
+ ```
238
+
239
+ Output example:
240
+ ```
241
+ ERROR: Wrong command, did you mean:
242
+ $ mycli deploy staging
243
+ $ mycli deploy production
244
+ ```
245
+
246
+ ## Custom Not-Found Handler
247
+
248
+ An alternative to `onUnknownCommand` — uses the standard Wooks handler pattern:
249
+
250
+ ```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
260
+
261
+ Multiple adapters can share the same Wooks router:
262
+
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
270
+ ```
271
+
272
+ Or share via another adapter instance:
273
+
274
+ ```ts
275
+ const app1 = createCliApp()
276
+ const app2 = createCliApp({}, app1) // shares app1's router
277
+ ```
278
+
279
+ ## Testing
280
+
281
+ Use `app.run()` with explicit argv arrays to test commands programmatically:
282
+
283
+ ```ts
284
+ import { createCliApp } from '@wooksjs/event-cli'
285
+ import { useRouteParams } from '@wooksjs/event-core'
286
+
287
+ const app = createCliApp({
288
+ onError: (e) => { /* don't exit in tests */ },
289
+ })
290
+
291
+ app.cli('greet/:name', () => {
292
+ const { get } = useRouteParams<{ name: string }>()
293
+ return `Hello, ${get('name')}!`
294
+ })
295
+
296
+ // Test:
297
+ const result = await app.run(['greet', 'Alice'])
298
+ // Handler outputs: "Hello, Alice!"
299
+ ```
300
+
301
+ ### Testing with flags
302
+
303
+ ```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:
319
+
320
+ ```ts
321
+ const app = createCliApp()
322
+ const logger = app.getLogger('[my-cli]')
323
+ logger.log('CLI started')
324
+ ```
325
+
326
+ Inside a handler, use the event-scoped logger:
327
+
328
+ ```ts
329
+ import { useEventLogger } from '@wooksjs/event-core'
330
+
331
+ app.cli('process', () => {
332
+ const logger = useEventLogger('process-handler')
333
+ logger.log('Processing command')
334
+ return 'done'
335
+ })
336
+ ```
337
+
338
+ ## Best Practices
339
+
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.
345
+
346
+ ## Gotchas
347
+
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.