@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,545 @@
1
+ # Context Extensions
2
+
3
+ Plugins in Gunshi extend the command context with additional functionality through the extension system.
4
+
5
+ This guide explains how to leverage these extensions to build more powerful CLI applications.
6
+
7
+ <!-- eslint-disable markdown/no-missing-label-refs -->
8
+
9
+ > [!TIP]
10
+ > This guide assumes familiarity with the basic concepts explained in the [Plugin System](../essentials/plugin-system.md). Context extensions are a core feature of the plugin system, providing the mechanism through which plugins deliver functionality to commands.
11
+
12
+ <!-- eslint-enable markdown/no-missing-label-refs -->
13
+
14
+ ## Understanding Context Extensions
15
+
16
+ The command context (`ctx`) is the central object passed to every command runner.
17
+
18
+ Plugins enhance this context by adding new capabilities through the extensions property, allowing your commands to access additional functionality like logging, internationalization, or custom services.
19
+
20
+ Each plugin contributes its own extension under a unique namespace, ensuring clean separation of concerns and preventing conflicts between different plugins.
21
+
22
+ ```ts
23
+ import { define } from 'gunshi'
24
+
25
+ // Basic command context
26
+ const command = define({
27
+ run: ctx => {
28
+ // Default context properties
29
+ ctx.name // Command name
30
+ ctx.version // CLI version
31
+ ctx.values // Parsed argument values
32
+ ctx.args // Raw arguments
33
+
34
+ // Plugin extensions
35
+ ctx.extensions // Object containing all plugin extensions
36
+ }
37
+ })
38
+ ```
39
+
40
+ ## How Extensions Work
41
+
42
+ Each plugin registers its extension under a unique identifier (plugin ID) within the `ctx.extensions` object.
43
+
44
+ This namespacing approach prevents collisions between plugins and makes dependencies explicit.
45
+
46
+ When a plugin is added to your CLI configuration, its extension becomes available to all commands through this standardized interface:
47
+
48
+ <!-- eslint-disable markdown/no-missing-label-refs -->
49
+
50
+ > [!TIP]
51
+ > For details on how plugin extensions work, see the [Plugin Extensions](../plugin/extensions.md) guide.
52
+
53
+ <!-- eslint-enable markdown/no-missing-label-refs -->
54
+
55
+ ```ts
56
+ import { define } from 'gunshi'
57
+ import { pluginId as globalId } from '@gunshi/plugin-global'
58
+
59
+ const command = define({
60
+ run: ctx => {
61
+ // Access global plugin extension
62
+ const globalExtension = ctx.extensions[globalId]
63
+
64
+ // Use extension methods
65
+ globalExtension.showVersion()
66
+ globalExtension.showHeader()
67
+ }
68
+ })
69
+ ```
70
+
71
+ ## Working with Built-in Plugin Extensions
72
+
73
+ Gunshi provides several official plugins with pre-built extensions that cover common CLI needs.
74
+
75
+ Understanding how to use these extensions effectively will accelerate your CLI development.
76
+
77
+ ### Global Plugin Extension
78
+
79
+ The global plugin (`@gunshi/plugin-global`) provides methods for displaying CLI information:
80
+
81
+ ```ts
82
+ import { define } from 'gunshi'
83
+ import { pluginId as globalId } from '@gunshi/plugin-global'
84
+
85
+ const command = define({
86
+ run: ctx => {
87
+ const global = ctx.extensions[globalId]
88
+
89
+ // Show version information
90
+ global.showVersion()
91
+
92
+ // Show command header
93
+ global.showHeader()
94
+
95
+ // Show usage information
96
+ global.showUsage()
97
+
98
+ // Show validation errors
99
+ if (ctx.validationError) {
100
+ global.showValidationErrors(ctx.validationError)
101
+ }
102
+ }
103
+ })
104
+ ```
105
+
106
+ <!-- eslint-disable markdown/no-missing-label-refs -->
107
+
108
+ > [!NOTE]
109
+ > For a complete list of official plugins and their features, see the [Plugin List](../plugin/list.md) guide.
110
+
111
+ <!-- eslint-enable markdown/no-missing-label-refs -->
112
+
113
+ ### Renderer Plugin Extension
114
+
115
+ The renderer plugin (`@gunshi/plugin-renderer`) provides text rendering capabilities:
116
+
117
+ ```ts
118
+ import { define } from 'gunshi'
119
+ import { pluginId as rendererId } from '@gunshi/plugin-renderer'
120
+
121
+ const command = define({
122
+ run: async ctx => {
123
+ // Check if renderer extension is available
124
+ const renderer = ctx.extensions[rendererId]
125
+
126
+ if (renderer) {
127
+ // Get translated text
128
+ const helpText = await renderer.text('HELP')
129
+
130
+ // Load subcommands for display
131
+ const commands = await renderer.loadCommands()
132
+ }
133
+ }
134
+ })
135
+ ```
136
+
137
+ <!-- eslint-disable markdown/no-missing-label-refs -->
138
+
139
+ > [!NOTE]
140
+ > The renderer plugin is typically added by the global plugin when used together. For detailed information about the renderer plugin API, see the [Renderer Plugin documentation](https://github.com/kazupon/gunshi/tree/main/packages/plugin-renderer)
141
+
142
+ <!-- eslint-enable markdown/no-missing-label-refs -->
143
+
144
+ ## Using Optional Plugin Extensions
145
+
146
+ Beyond the core plugins, Gunshi's ecosystem includes optional plugins for specialized functionality.
147
+
148
+ These extensions follow the same patterns but may not be present in all CLI configurations.
149
+
150
+ ### I18n Plugin Extension
151
+
152
+ The i18n plugin provides translation capabilities through context extensions:
153
+
154
+ ```ts
155
+ import { define } from 'gunshi'
156
+ import { pluginId as i18nId } from '@gunshi/plugin-i18n'
157
+
158
+ const command = define({
159
+ run: ctx => {
160
+ const i18n = ctx.extensions[i18nId]
161
+
162
+ if (i18n) {
163
+ // Access current locale
164
+ console.log(`Running in ${i18n.locale} locale`)
165
+
166
+ // Use translation function
167
+ const message = i18n.translate('welcome')
168
+ console.log(message)
169
+ }
170
+ }
171
+ })
172
+ ```
173
+
174
+ <!-- eslint-disable markdown/no-missing-label-refs -->
175
+
176
+ > [!NOTE]
177
+ > For comprehensive i18n usage including `resolveKey`, `defineI18n`, resource management, and working with subcommands, see the [Internationalization guide](./internationalization.md)
178
+
179
+ <!-- eslint-enable markdown/no-missing-label-refs -->
180
+
181
+ ## Extension Techniques
182
+
183
+ The following techniques demonstrate code for working effectively with extensions in various scenarios.
184
+
185
+ These approaches ensure your commands remain flexible, maintainable, and resilient.
186
+
187
+ ### Safe Extension Access
188
+
189
+ Extensions may not always be available depending on your CLI configuration and which plugins are installed.
190
+
191
+ Commands should defensively check for extension existence to prevent runtime errors and provide graceful fallbacks.
192
+
193
+ This defensive programming approach ensures your CLI remains robust even when optional plugins are not configured:
194
+
195
+ ```ts
196
+ import { define } from 'gunshi'
197
+ import { pluginId as globalId } from '@gunshi/plugin-global'
198
+
199
+ const command = define({
200
+ run: ctx => {
201
+ // Safe access technique
202
+ const global = ctx.extensions[globalId]
203
+
204
+ if (global) {
205
+ // Extension is available
206
+ global.showUsage()
207
+ } else {
208
+ // Fallback behavior
209
+ console.log('Usage information not available')
210
+ }
211
+ }
212
+ })
213
+ ```
214
+
215
+ ### Extension Composition
216
+
217
+ Commands often benefit from combining multiple plugin extensions to create richer functionality.
218
+
219
+ Extensions are designed to work together harmoniously, allowing you to compose complex behaviors from simple building blocks.
220
+
221
+ The following example demonstrates how global display features can be enhanced with dynamic content loading from the renderer extension:
222
+
223
+ ```ts
224
+ import { define } from 'gunshi'
225
+ import { pluginId as globalId } from '@gunshi/plugin-global'
226
+ import { pluginId as rendererId } from '@gunshi/plugin-renderer'
227
+
228
+ const command = define({
229
+ run: async ctx => {
230
+ const global = ctx.extensions[globalId]
231
+ const renderer = ctx.extensions[rendererId]
232
+
233
+ // Combine extensions for rich functionality
234
+ if (global && renderer) {
235
+ // Show header using global extension
236
+ global.showHeader()
237
+
238
+ // Load and display commands if renderer is available
239
+ const commands = await renderer.loadCommands()
240
+ console.log('Available commands:', commands)
241
+ }
242
+ }
243
+ })
244
+ ```
245
+
246
+ ### Dynamic Extension Usage
247
+
248
+ Extensions can be conditionally utilized based on command arguments, environment variables, or other runtime conditions.
249
+
250
+ This dynamic approach allows your CLI to adapt its behavior to different contexts and user preferences.
251
+
252
+ The following code shows how to selectively engage extensions based on command flags:
253
+
254
+ ```ts
255
+ import { define } from 'gunshi'
256
+ import { pluginId as globalId } from '@gunshi/plugin-global'
257
+ import { pluginId as loggerId } from '@my/plugin-logger'
258
+
259
+ const command = define({
260
+ args: {
261
+ verbose: { type: 'boolean' },
262
+ debug: { type: 'boolean' },
263
+ help: { type: 'boolean' }
264
+ },
265
+ run: ctx => {
266
+ // Use extensions based on command flags
267
+ if (ctx.values.help) {
268
+ ctx.extensions[globalId]?.showUsage()
269
+ return
270
+ }
271
+
272
+ // Conditional extension usage for verbose mode
273
+ if (ctx.values.verbose) {
274
+ const global = ctx.extensions[globalId]
275
+ global?.showHeader()
276
+ console.log('Running in verbose mode...')
277
+ }
278
+
279
+ // Enable debug features if available
280
+ // Check if a hypothetical logger extension exists
281
+ if (ctx.values.debug && ctx.extensions[loggerId]) {
282
+ ctx.extensions[loggerId].setLevel('debug')
283
+ }
284
+
285
+ // Your command logic here
286
+ console.log('Command executed')
287
+ }
288
+ })
289
+ ```
290
+
291
+ ## Type-Safe Extensions
292
+
293
+ Use TypeScript for compile-time safety with extensions:
294
+
295
+ ```ts
296
+ import { define } from 'gunshi'
297
+ import { pluginId as globalId } from '@gunshi/plugin-global'
298
+ import type { GlobalExtension, PluginId as GlobalId } from '@gunshi/plugin-global'
299
+
300
+ // Define command with typed extensions
301
+ const command = define<Record<GlobalId, GlobalExtension>>({
302
+ name: 'app',
303
+ run: ctx => {
304
+ // TypeScript knows about the global extension
305
+ ctx.extensions[globalId].showVersion()
306
+ ctx.extensions[globalId].showUsage()
307
+
308
+ // Type errors for unknown extensions
309
+ // ctx.extensions['unknown'].method() // Compile-time error!
310
+ }
311
+ })
312
+ ```
313
+
314
+ <!-- eslint-disable markdown/no-missing-label-refs -->
315
+
316
+ > [!NOTE]
317
+ > For comprehensive type parameter usage including `GunshiParams`, combining multiple plugin types with the intersection (`&`), and advanced type safety techniques, see the [Type System guide](./type-system.md)
318
+
319
+ <!-- eslint-enable markdown/no-missing-label-refs -->
320
+
321
+ ## Custom Extension Techniques
322
+
323
+ Extensions can serve as a foundation for building sophisticated CLI architectures.
324
+
325
+ The following advanced patterns showcase how to leverage extensions for service layers and architectural concerns.
326
+
327
+ ### Extension as Service Layer
328
+
329
+ Extensions can act as a service abstraction layer, providing consistent interfaces to external systems like databases, caches, or APIs.
330
+
331
+ This pattern decouples your command logic from infrastructure concerns and simplifies testing through mockable extensions:
332
+
333
+ ```ts
334
+ // Note: These are hypothetical example plugins for illustration purposes
335
+ // You would need to create or install actual plugins with these capabilities
336
+ import { define } from 'gunshi'
337
+ import { pluginId as dbId } from '@my/plugin-db' // Example custom plugin
338
+ import { pluginId as cacheId } from '@my/plugin-cache' // Example custom plugin
339
+
340
+ // With hypothetical database and cache plugins
341
+ const command = define({
342
+ run: async ctx => {
343
+ const db = ctx.extensions[dbId]
344
+ const cache = ctx.extensions[cacheId]
345
+
346
+ // Check cache first
347
+ const cached = await cache?.get('users')
348
+ if (cached) {
349
+ return cached
350
+ }
351
+
352
+ // Fetch from database
353
+ const users = await db?.query('SELECT * FROM users')
354
+
355
+ // Cache the result
356
+ await cache?.set('users', users, { ttl: 3600 })
357
+
358
+ return users
359
+ }
360
+ })
361
+ ```
362
+
363
+ ### Extension for Cross-Cutting Concerns
364
+
365
+ Cross-cutting concerns are aspects of your application that affect multiple commands, such as logging, authentication, monitoring, or error tracking.
366
+
367
+ Extensions provide an ideal mechanism for implementing these concerns consistently across your entire CLI.
368
+
369
+ By centralizing these capabilities in plugin extensions, you ensure uniform behavior and simplify maintenance.
370
+
371
+ The following example demonstrates a comprehensive approach to handling logging, authentication, and metrics collection:
372
+
373
+ ```ts
374
+ // Note: These are hypothetical example plugins for illustration purposes
375
+ import { define } from 'gunshi'
376
+ import { pluginId as loggerId } from '@my/plugin-logger'
377
+ import { pluginId as authId } from '@my/plugin-auth'
378
+ import { pluginId as metricsId } from '@my/plugin-metrics'
379
+
380
+ const command = define({
381
+ run: async ctx => {
382
+ const logger = ctx.extensions[loggerId]
383
+ const auth = ctx.extensions[authId]
384
+ const metrics = ctx.extensions[metricsId]
385
+
386
+ const startTime = Date.now()
387
+ logger?.info('Command started', { command: ctx.name })
388
+
389
+ // Check authentication
390
+ if (!auth?.isAuthenticated()) {
391
+ logger?.error('Authentication required')
392
+ throw new Error('Please login first')
393
+ }
394
+
395
+ try {
396
+ // Your actual command logic
397
+ // processData is a placeholder for your data processing logic
398
+ const result = await processData(ctx.values)
399
+
400
+ // Track success metrics
401
+ metrics?.track('command.success', {
402
+ command: ctx.name,
403
+ duration: Date.now() - startTime
404
+ })
405
+
406
+ logger?.info('Command completed successfully')
407
+ return result
408
+ } catch (error) {
409
+ logger?.error('Command failed', { error: error.message })
410
+ metrics?.track('command.failure', {
411
+ command: ctx.name,
412
+ error: error.message
413
+ })
414
+ throw error
415
+ }
416
+ }
417
+ })
418
+ ```
419
+
420
+ <!-- eslint-disable markdown/no-missing-label-refs -->
421
+
422
+ > [!TIP]
423
+ > Learn how to create your own plugins with custom extensions in the [Plugin Development](../plugin/introduction.md) guide.
424
+
425
+ <!-- eslint-enable markdown/no-missing-label-refs -->
426
+
427
+ ## Guidelines for Plugin Usage
428
+
429
+ ### 1. Always Import Plugin IDs
430
+
431
+ Never hardcode plugin ID strings. Always import and use the exported constants to avoid typos and ensure type safety:
432
+
433
+ <!-- eslint-skip -->
434
+
435
+ ```js
436
+ // ✅ Good: Import and use plugin ID constants
437
+ import i18n, { pluginId as i18nId } from '@gunshi/plugin-i18n'
438
+ import global, { pluginId as globalId } from '@gunshi/plugin-global'
439
+
440
+ // Use the imported IDs
441
+ const i18nExt = ctx.extensions[i18nId]
442
+ const globalExt = ctx.extensions[globalId]
443
+
444
+ // ❌ Bad: Hardcoded strings are fragile and error-prone
445
+ const i18nExt = ctx.extensions['g:i18n'] // Don't do this!
446
+ ```
447
+
448
+ ### 2. Handle Optional Plugin Extensions Gracefully
449
+
450
+ When plugins might not be available, always check for extension existence to avoid runtime errors:
451
+
452
+ ```js
453
+ import { pluginId as globalId } from '@gunshi/plugin-global'
454
+
455
+ run: ctx => {
456
+ // Safe access with optional chaining
457
+ const version = ctx.extensions[globalId]?.showVersion() || 'Version unknown'
458
+
459
+ // Or explicit checking for complex logic
460
+ const globalExt = ctx.extensions[globalId]
461
+ if (globalExt) {
462
+ // Plugin is available - use full features
463
+ globalExt.showHeader()
464
+ globalExt.showUsage()
465
+ } else {
466
+ // Graceful fallback
467
+ console.log('Help not available')
468
+ }
469
+ }
470
+ ```
471
+
472
+ ### 3. Use TypeScript for Safety
473
+
474
+ When using TypeScript, leverage type definitions for compile-time safety:
475
+
476
+ ```ts
477
+ import { define } from 'gunshi'
478
+ import { pluginId as globalId } from '@gunshi/plugin-global'
479
+ import type { GlobalExtension, PluginId as GlobalId } from '@gunshi/plugin-global'
480
+
481
+ // Type-safe command with extension
482
+ const command = define<Record<GlobalId, GlobalExtension>>({
483
+ name: 'deploy',
484
+ run: ctx => {
485
+ // TypeScript ensures type safety
486
+ ctx.extensions[globalId].showVersion()
487
+ }
488
+ })
489
+ ```
490
+
491
+ <!-- eslint-disable markdown/no-missing-label-refs -->
492
+
493
+ > [!NOTE]
494
+ > For advanced TypeScript techniques including combining multiple plugin types and using `GunshiParams`, see the [Type System guide](./type-system.md).
495
+
496
+ <!-- eslint-enable markdown/no-missing-label-refs -->
497
+
498
+ ## Troubleshooting
499
+
500
+ ### Extension Not Found
501
+
502
+ If an extension is not available:
503
+
504
+ ```js
505
+ // Debug which extensions are available
506
+ console.log('Available extensions:', Object.keys(ctx.extensions))
507
+
508
+ // Check if plugin was added
509
+ if (!ctx.extensions[pluginId]) {
510
+ console.error(`Plugin ${pluginId} not installed`)
511
+ console.error('Add it to your CLI plugins:')
512
+ console.error('plugins: [yourPlugin()]')
513
+ }
514
+ ```
515
+
516
+ ### Type Errors with Extensions
517
+
518
+ For TypeScript users, ensure proper type definitions:
519
+
520
+ ```ts
521
+ // Import types
522
+ import type { YourExtension, PluginId } from 'your-plugin'
523
+
524
+ // Define with proper types
525
+ const command = define<Record<PluginId, YourExtension>>({
526
+ // Command definition
527
+ })
528
+ ```
529
+
530
+ ### Extension Method Not Working
531
+
532
+ Check the plugin documentation for correct usage:
533
+
534
+ ```js
535
+ import { pluginId as globalId } from '@gunshi/plugin-global'
536
+
537
+ // Wrong: Direct method call without checking
538
+ ctx.extensions[globalId].showVersion() // May fail if plugin not available
539
+
540
+ // Right: Store reference and check existence first
541
+ const global = ctx.extensions[globalId]
542
+ if (global) {
543
+ global.showVersion()
544
+ }
545
+ ```