@gunshi/docs 0.27.0-beta.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,469 @@
1
+ # Command Hooks
2
+
3
+ Gunshi provides powerful lifecycle hooks that allow you to intercept and control command execution at various stages.
4
+
5
+ These hooks enable advanced scenarios like logging, monitoring, validation, and error handling.
6
+
7
+ ## Understanding Command Lifecycle
8
+
9
+ The command execution lifecycle in Gunshi follows these stages:
10
+
11
+ ```mermaid
12
+ graph TD
13
+ Start([Start]) --> A[User Input]
14
+ A --> B[Parse Arguments]
15
+ B --> C[Apply Plugins]
16
+ C --> D[Resolve Command]
17
+ D --> E[Resolve Arguments]
18
+ E --> F[Create Command Context]
19
+ F --> G[onBeforeCommand Hook]
20
+ G --> H{Success?}
21
+ H -->|Yes| I[Execute Command]
22
+ H -->|Error| K[onErrorCommand Hook]
23
+ I --> J{Success?}
24
+ J -->|Yes| L[onAfterCommand Hook]
25
+ J -->|Error| K
26
+ L --> M{Success?}
27
+ M -->|Yes| N[Return Result]
28
+ M -->|Error| K
29
+ K --> O[Throw Error]
30
+ N --> P([End])
31
+ O --> P
32
+
33
+ style G fill:#468c56,color:white
34
+ style L fill:#468c56,color:white
35
+ style K fill:#468c56,color:white
36
+ ```
37
+
38
+ ## Available Hooks
39
+
40
+ Gunshi provides three main lifecycle hooks:
41
+
42
+ - **`onBeforeCommand`**: Executes before the command runs
43
+ - **`onAfterCommand`**: Executes after successful command completion
44
+ - **`onErrorCommand`**: Executes when a command throws an error
45
+
46
+ ## Basic Hook Usage
47
+
48
+ ### Setting Up Hooks
49
+
50
+ The following example demonstrates how to configure lifecycle hooks when initializing your CLI application.
51
+
52
+ In this setup, we define three hooks that will execute at different stages of the command lifecycle:
53
+
54
+ ```ts [cli.ts]
55
+ import { cli } from 'gunshi'
56
+
57
+ await cli(
58
+ process.argv.slice(2),
59
+ {
60
+ name: 'server',
61
+ run: () => {
62
+ console.log('Starting server...')
63
+ }
64
+ },
65
+ {
66
+ name: 'my-app',
67
+ version: '1.0.0',
68
+
69
+ // Define lifecycle hooks
70
+ onBeforeCommand: ctx => {
71
+ console.log(`About to run: ${ctx.name}`)
72
+ },
73
+
74
+ onAfterCommand: (ctx, result) => {
75
+ console.log(`Command ${ctx.name} completed successfully`)
76
+ },
77
+
78
+ onErrorCommand: (ctx, error) => {
79
+ console.error(`Command ${ctx.name} failed:`, error)
80
+ }
81
+ }
82
+ )
83
+ ```
84
+
85
+ <!-- eslint-disable markdown/no-missing-label-refs -->
86
+
87
+ > [!TIP]
88
+ > The example fully code is [here](https://github.com/kazupon/gunshi/tree/main/playground/advanced/command-hooks).
89
+
90
+ <!-- eslint-enable markdown/no-missing-label-refs -->
91
+
92
+ ### Hook Parameters
93
+
94
+ Each lifecycle hook receives specific parameters that provide context about the command execution.
95
+
96
+ The `CommandContext` parameter is read-only and contains all command metadata, while `onAfterCommand` also receives the command result and `onErrorCommand` receives the thrown error:
97
+
98
+ <!-- eslint-skip -->
99
+
100
+ ```ts
101
+ {
102
+ // Before command execution
103
+ onBeforeCommand?: (ctx: Readonly<CommandContext>) => Awaitable<void>
104
+
105
+ // After successful execution
106
+ onAfterCommand?: (ctx: Readonly<CommandContext>, result: string | undefined) => Awaitable<void>
107
+
108
+ // On command error
109
+ onErrorCommand?: (ctx: Readonly<CommandContext>, error: Error) => Awaitable<void>
110
+ }
111
+ ```
112
+
113
+ <!-- eslint-disable markdown/no-missing-label-refs -->
114
+
115
+ > [!NOTE]
116
+ > The `CommandContext` object provides comprehensive information about command execution. For the complete CommandContext API reference including all properties and types, see the [CommandContext interface documentation](/api/default/interfaces/CommandContext.md).
117
+
118
+ <!-- eslint-enable markdown/no-missing-label-refs -->
119
+
120
+ With an understanding of how hooks work and their parameters, let's explore how they differ from and interact with plugin decorators.
121
+
122
+ ## Hooks vs Decorators
123
+
124
+ Gunshi provides two distinct mechanisms for controlling command execution:
125
+
126
+ 1. **CLI-level Hooks**: Lifecycle hooks that run **before and after** command execution
127
+ - `onBeforeCommand`: Pre-execution processing (logging, validation, initialization)
128
+ - `onAfterCommand`: Post-success processing (cleanup, metrics recording)
129
+ - `onErrorCommand`: Error handling (error logging, rollback)
130
+
131
+ 2. **Plugin Decorators**: **Wrap** the command itself to modify its behavior
132
+ - `decorateCommand`: Wraps command runner to add or modify functionality
133
+ - Multiple plugins can chain decorators (decorator pattern)
134
+
135
+ <!-- eslint-disable markdown/no-missing-label-refs -->
136
+
137
+ > [!TIP]
138
+ > The `decorateCommand` method is a powerful plugin API that allows wrapping command execution to add or modify functionality. It enables plugins to implement cross-cutting concerns like authentication, logging, and transaction management. For comprehensive information about how to use decorators in plugins, see the [Plugin Decorators documentation](/guide/plugin/decorators.md).
139
+
140
+ <!-- eslint-enable markdown/no-missing-label-refs -->
141
+
142
+ ### Execution Flow
143
+
144
+ The following diagram illustrates how hooks and decorators interact during command execution:
145
+
146
+ ```mermaid
147
+ graph TD
148
+ A[onBeforeCommand Hook] --> B{Success?}
149
+ B -->|Yes| C[Plugin Decorators Chain]
150
+ B -->|Error| G[onErrorCommand Hook]
151
+
152
+ C --> D[Decorated Command Runner]
153
+ D --> E{Success?}
154
+
155
+ E -->|Yes| F[onAfterCommand Hook]
156
+ E -->|Error| G
157
+
158
+ F --> H{Success?}
159
+ H -->|Yes| I[Return Result]
160
+ H -->|Error| G
161
+
162
+ G --> J[Throw Error]
163
+
164
+ style A fill:#468c56,color:white
165
+ style F fill:#468c56,color:white
166
+ style G fill:#468c56,color:white
167
+ style C fill:#7EA6E0,color:white
168
+ style D fill:#7EA6E0,color:white
169
+ ```
170
+
171
+ ### Detailed Execution Sequence
172
+
173
+ 1. **`onBeforeCommand` Hook** - Pre-execution setup and validation
174
+ 2. **Plugin Decorator Chain** - Command wrapping by plugins
175
+ - Applied in reverse order (LIFO - last registered, first executed)
176
+ - Each decorator wraps the next runner in the chain
177
+ 3. **Command Runner** - Actual command execution
178
+ 4. **`onAfterCommand` Hook** - Post-success processing
179
+ 5. **`onErrorCommand` Hook** - Error handling when exceptions occur
180
+
181
+ ### Plugin Decorator Example
182
+
183
+ The following example demonstrates how to use the `decorateCommand` method in a plugin to measure command execution time:
184
+
185
+ ```ts
186
+ import { plugin } from 'gunshi/plugin'
187
+
188
+ // Using decorateCommand in a plugin
189
+ export default plugin({
190
+ id: 'timing-plugin',
191
+ setup: ctx => {
192
+ // Wrap command execution to measure execution time
193
+ ctx.decorateCommand(baseRunner => {
194
+ return async commandCtx => {
195
+ const start = Date.now()
196
+ try {
197
+ const result = await baseRunner(commandCtx)
198
+ console.log(`Execution time: ${Date.now() - start}ms`)
199
+ return result
200
+ } catch (error) {
201
+ console.log(`Failed after: ${Date.now() - start}ms`)
202
+ throw error
203
+ }
204
+ }
205
+ })
206
+ }
207
+ })
208
+ ```
209
+
210
+ <!-- eslint-disable markdown/no-missing-label-refs -->
211
+
212
+ > [!NOTE]
213
+ > Plugins don't have CLI-level hooks (`onBeforeCommand`, etc.). Instead, they use the `decorateCommand` method to wrap command execution and add custom logic. This allows plugins to extend and modify command behavior through the decorator pattern.
214
+
215
+ <!-- eslint-enable markdown/no-missing-label-refs -->
216
+
217
+ ## Practical Use Cases
218
+
219
+ <!-- eslint-disable markdown/no-missing-label-refs -->
220
+
221
+ > [!TIP]
222
+ > The following examples use plugin extensions through `ctx.extensions`. Extensions are how plugins add functionality to the command context, allowing you to access plugin-provided features like logging, metrics, authentication, and database connections. For comprehensive information about working with extensions, including type-safe patterns and best practices, see the [Context Extensions documentation](./context-extensions.md).
223
+
224
+ <!-- eslint-enable markdown/no-missing-label-refs -->
225
+
226
+ ### Logging and Monitoring
227
+
228
+ Implement comprehensive logging across all commands:
229
+
230
+ ```ts
231
+ import { cli } from 'gunshi'
232
+ import logger, { pluginId as loggerId } from '@my/plugin-logger'
233
+
234
+ await cli(process.argv.slice(2), commands, {
235
+ name: 'my-app',
236
+
237
+ // Install logger plugin
238
+ plugins: [logger()],
239
+
240
+ onBeforeCommand: ctx => {
241
+ const logger = ctx.extensions[loggerId]
242
+ // Log command start with arguments
243
+ logger?.info('Command started', {
244
+ command: ctx.name,
245
+ args: ctx.values,
246
+ timestamp: new Date().toISOString()
247
+ })
248
+ },
249
+
250
+ onAfterCommand: (ctx, result) => {
251
+ const logger = ctx.extensions[loggerId]
252
+ // Log successful completion
253
+ logger?.info('Command completed', {
254
+ command: ctx.name,
255
+ duration: Date.now() - logger?.startTime,
256
+ result: typeof result
257
+ })
258
+ },
259
+
260
+ onErrorCommand: (ctx, error) => {
261
+ const logger = ctx.extensions[loggerId]
262
+ // Log errors with full context
263
+ logger?.error('Command failed', {
264
+ command: ctx.name,
265
+ error: error.message,
266
+ stack: error.stack,
267
+ args: ctx.values
268
+ })
269
+ }
270
+ })
271
+ ```
272
+
273
+ ### Performance Monitoring
274
+
275
+ Track command execution times and performance metrics:
276
+
277
+ ```ts
278
+ import { cli } from 'gunshi'
279
+ import metrics, { pluginId as metricsId } from '@my/plugin-metrics'
280
+
281
+ await cli(process.argv.slice(2), commands, {
282
+ name: 'my-app',
283
+
284
+ // Install metrics plugin
285
+ plugins: [metrics()],
286
+
287
+ onBeforeCommand: ctx => {
288
+ const metrics = ctx.extensions[metricsId]
289
+ // Start tracking command execution
290
+ metrics?.startTracking({
291
+ command: ctx.name,
292
+ args: ctx.values,
293
+ environment: process.env.NODE_ENV
294
+ })
295
+ },
296
+
297
+ onAfterCommand: async (ctx, result) => {
298
+ const metrics = ctx.extensions[metricsId]
299
+ // Record successful completion
300
+ const duration = metrics?.endTracking()
301
+
302
+ // Send metrics to monitoring service
303
+ await metrics?.send({
304
+ command: ctx.name,
305
+ status: 'success',
306
+ duration,
307
+ memoryUsage: process.memoryUsage(),
308
+ resultSize: result?.length || 0
309
+ })
310
+ },
311
+
312
+ onErrorCommand: async (ctx, error) => {
313
+ const metrics = ctx.extensions[metricsId]
314
+ // Record failure metrics
315
+ const duration = metrics?.endTracking()
316
+
317
+ // Send error metrics with additional context
318
+ await metrics?.send({
319
+ command: ctx.name,
320
+ status: 'failed',
321
+ duration,
322
+ error: error.message,
323
+ errorType: error.constructor.name,
324
+ stackTrace: error.stack
325
+ })
326
+ }
327
+ })
328
+ ```
329
+
330
+ ### Validation and Guards
331
+
332
+ Use hooks to implement global validation or access control:
333
+
334
+ ```ts
335
+ import { cli } from 'gunshi'
336
+ import auth, { pluginId as authId } from '@my/plugin-auth'
337
+
338
+ await cli(process.argv.slice(2), commands, {
339
+ name: 'my-app',
340
+
341
+ // Install auth plugin
342
+ plugins: [
343
+ auth({
344
+ publicCommands: ['help', 'version', 'login'],
345
+ tokenSource: ['env:AUTH_TOKEN', 'args:token']
346
+ })
347
+ ],
348
+
349
+ onBeforeCommand: async ctx => {
350
+ const auth = ctx.extensions[authId]
351
+
352
+ // Skip auth for public commands
353
+ if (auth?.isPublicCommand(ctx.name)) {
354
+ return
355
+ }
356
+
357
+ // Verify authentication
358
+ const token = auth?.getToken()
359
+ if (!token) {
360
+ throw new Error('Authentication required. Please run "login" first.')
361
+ }
362
+
363
+ const user = await auth?.verifyToken(token)
364
+ if (!user) {
365
+ throw new Error('Invalid or expired token. Please login again.')
366
+ }
367
+
368
+ // Store user info for command use
369
+ await auth?.setCurrentUser(user)
370
+ },
371
+
372
+ onAfterCommand: async ctx => {
373
+ const auth = ctx.extensions[authId]
374
+ // Clean up sensitive data after command execution
375
+ await auth?.clearSession()
376
+ },
377
+
378
+ onErrorCommand: async (ctx, error) => {
379
+ const auth = ctx.extensions[authId]
380
+ // Log security-related errors
381
+ if (error.message.includes('Authentication') || error.message.includes('token')) {
382
+ await auth?.logSecurityEvent({
383
+ type: 'auth_failure',
384
+ command: ctx.name,
385
+ timestamp: new Date().toISOString()
386
+ })
387
+ }
388
+ }
389
+ })
390
+ ```
391
+
392
+ ### Transaction Management
393
+
394
+ Implement database transactions or rollback mechanisms:
395
+
396
+ ```ts
397
+ import { cli } from 'gunshi'
398
+ import database, { pluginId as dbId } from '@my/plugin-database'
399
+
400
+ await cli(process.argv.slice(2), commands, {
401
+ name: 'my-app',
402
+
403
+ // Install database plugin with transaction support
404
+ plugins: [
405
+ database({
406
+ connectionString: process.env.DATABASE_URL,
407
+ transactionalCommands: ['create', 'update', 'delete', 'migrate']
408
+ })
409
+ ],
410
+
411
+ onBeforeCommand: async ctx => {
412
+ const db = ctx.extensions[dbId]
413
+
414
+ // Start transaction for data-modifying commands
415
+ if (db?.isTransactionalCommand(ctx.name)) {
416
+ const transaction = await db.beginTransaction()
417
+
418
+ // Store transaction ID for tracking
419
+ await db?.setCurrentTransaction(transaction.id)
420
+
421
+ console.log(`Transaction ${transaction.id} started for command: ${ctx.name}`)
422
+ }
423
+ },
424
+
425
+ onAfterCommand: async (ctx, result) => {
426
+ const db = ctx.extensions[dbId]
427
+ const transaction = db?.getCurrentTransaction()
428
+
429
+ if (transaction) {
430
+ // Commit on success
431
+ await db.commit(transaction.id)
432
+ console.log(`Transaction ${transaction.id} committed successfully`)
433
+
434
+ // Clean up transaction reference
435
+ await db.clearCurrentTransaction()
436
+ }
437
+ },
438
+
439
+ onErrorCommand: async (ctx, error) => {
440
+ const db = ctx.extensions[dbId]
441
+ const transaction = db?.getCurrentTransaction()
442
+
443
+ if (transaction) {
444
+ // Rollback on error
445
+ await db?.rollback(transaction.id)
446
+ console.error(`Transaction ${transaction.id} rolled back due to error:`, error.message)
447
+
448
+ // Log the failed transaction for audit
449
+ await db?.logTransactionFailure({
450
+ id: transaction.id,
451
+ command: ctx.name,
452
+ error: error.message,
453
+ timestamp: new Date().toISOString()
454
+ })
455
+
456
+ // Clean up transaction reference
457
+ await db?.clearCurrentTransaction()
458
+ }
459
+ }
460
+ })
461
+ ```
462
+
463
+ ## Hook Execution Order
464
+
465
+ When multiple hooks and decorators are present, they execute in a specific sequence as shown in the Hooks vs Decorators section above.
466
+
467
+ Understanding this order is crucial for implementing complex behaviors like transaction management or error recovery.
468
+
469
+ For detailed execution flow, refer to the execution diagram in the [Hooks vs Decorators](#hooks-vs-decorators) section.