@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.
- package/README.md +24 -0
- package/dist/index.cjs +22 -6
- package/dist/index.d.ts +29 -6
- package/dist/index.mjs +22 -6
- package/package.json +46 -37
- package/scripts/setup-skills.js +77 -0
- package/skills/wooksjs-event-cli/SKILL.md +35 -0
- package/skills/wooksjs-event-cli/commands.md +541 -0
- package/skills/wooksjs-event-cli/core.md +353 -0
- package/skills/wooksjs-event-cli/event-core.md +562 -0
|
@@ -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.
|