@gunshi/plugin 0.27.0-alpha.8 → 0.27.0-beta.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.
Files changed (4) hide show
  1. package/README.md +1014 -9
  2. package/lib/index.d.ts +807 -150
  3. package/lib/index.js +64 -24
  4. package/package.json +6 -6
package/README.md CHANGED
@@ -1,8 +1,12 @@
1
1
  # `@gunshi/plugin`
2
2
 
3
- > utilities for gunshi plugin
3
+ [![Version][npm-version-src]][npm-version-href]
4
+ [![InstallSize][install-size-src]][install-size-src]
5
+ [![JSR][jsr-src]][jsr-href]
4
6
 
5
- This package provides APIs and type definitions for making plugins for gunshi.
7
+ > plugin development kit for gunshi
8
+
9
+ This package provides comprehensive APIs and TypeScript type definitions for creating plugins for gunshi, a modern JavaScript command-line library.
6
10
 
7
11
  <!-- eslint-disable markdown/no-missing-label-refs -->
8
12
 
@@ -12,31 +16,1032 @@ This package provides APIs and type definitions for making plugins for gunshi.
12
16
 
13
17
  <!-- eslint-enable markdown/no-missing-label-refs -->
14
18
 
19
+ ## 📚 Table of Contents
20
+
21
+ - [Overview](#-overview)
22
+ - [Features](#-features)
23
+ - [Installation](#-installation)
24
+ - [Quick Start](#-quick-start)
25
+ - [Gunshi Lifecycle and Plugins](#-gunshi-lifecycle-and-plugins)
26
+ - [Plugin in Depth](#-plugin-in-depth)
27
+ - [Plugin Communication & Collaboration](#-plugin-communication--collaboration)
28
+ - [Plugin Best Practices](#-plugin-best-practices)
29
+ - [Official Plugins](#-official-plugins)
30
+ - [Community Plugins](#-community-plugins)
31
+ - [API Reference](#-api-reference)
32
+
33
+ ## 📜 Overview
34
+
35
+ Gunshi's plugin system is designed with a philosophy of **composability**, **type safety**, and **developer experience**. It allows you to extend CLI functionality in a modular way without modifying the core library.
36
+
37
+ ### Why Plugins?
38
+
39
+ - **Separation of Concerns**: Keep core functionality minimal while enabling rich features through plugins
40
+ - **Reusability**: Share common functionality across multiple CLI applications
41
+ - **Type Safety**: Full TypeScript support with type inference for extensions
42
+ - **Ecosystem**: Build and share plugins with the community
43
+
44
+ ### Package vs Entry Point
45
+
46
+ This package (`@gunshi/plugin`) contains the same exports as the `gunshi/plugin` entry point in the main gunshi package. Choose based on your needs:
47
+
48
+ - Use `@gunshi/plugin` when developing standalone plugin packages to minimize dependencies
49
+ - Use `gunshi/plugin` when plugins are part of your CLI application
50
+
51
+ ## 🌟 Features
52
+
53
+ Plugins can extend gunshi in multiple ways:
54
+
55
+ ### Extend CommandContext
56
+
57
+ Add custom properties to the command execution context that are available in all commands.
58
+
59
+ <!-- eslint-skip -->
60
+
61
+ ```ts
62
+ // Plugin adds authentication info to context
63
+ ctx.extensions['auth'].user // { id: '123', name: 'John' }
64
+ ```
65
+
66
+ ### Decorate Renderers
67
+
68
+ Customize how help messages, usage information, and validation errors are displayed.
69
+
70
+ ```ts
71
+ // Add colors, icons, or custom formatting
72
+ decorateHeaderRenderer((base, ctx) => `🚀 ${base(ctx)}`)
73
+ ```
74
+
75
+ ### Wrap Command Execution
76
+
77
+ Add pre/post processing logic around command execution.
78
+
79
+ ```ts
80
+ // Add logging, timing, error handling
81
+ decorateCommand(runner => async ctx => {
82
+ console.log('Before command')
83
+ const result = await runner(ctx)
84
+ console.log('After command')
85
+ return result
86
+ })
87
+ ```
88
+
89
+ ### Add Global Options
90
+
91
+ Register options available to all commands.
92
+
93
+ ```ts
94
+ // Add --verbose, --debug, etc.
95
+ addGlobalOption('verbose', { type: 'boolean', alias: 'v' })
96
+ ```
97
+
98
+ ### Add Sub Commands
99
+
100
+ Dynamically register new commands.
101
+
102
+ ```ts
103
+ // Add utility commands like 'complete', 'config', etc.
104
+ addCommand('config', configCommand)
105
+ ```
106
+
107
+ ### Manage Plugin Dependencies
108
+
109
+ Declare dependencies between plugins with automatic resolution.
110
+
111
+ <!-- eslint-skip -->
112
+
113
+ ```ts
114
+ // Ensure required plugins are loaded first
115
+ dependencies: ['logger', { id: 'cache', optional: true }]
116
+ ```
117
+
15
118
  ## đŸ’ŋ Installation
16
119
 
17
120
  ```sh
18
121
  # npm
19
122
  npm install --save @gunshi/plugin
20
123
 
21
- ## pnpm
124
+ # pnpm
22
125
  pnpm add @gunshi/plugin
23
126
 
24
- ## yarn
127
+ # yarn
25
128
  yarn add @gunshi/plugin
26
129
 
27
- ## deno
130
+ # deno
28
131
  deno add jsr:@gunshi/plugin
29
132
 
30
- ## bun
133
+ # bun
31
134
  bun add @gunshi/plugin
32
135
  ```
33
136
 
34
- ## 🚀 Usage
137
+ ## 🚀 Quick Start
138
+
139
+ ### Minimal Plugin (No Extension)
140
+
141
+ ```ts
142
+ import { plugin } from '@gunshi/plugin'
143
+
144
+ export default plugin({
145
+ id: 'my-plugin',
146
+ name: 'My First Plugin',
147
+
148
+ setup(ctx) {
149
+ console.log('Plugin loaded!')
150
+
151
+ // Add a global --quiet option
152
+ ctx.addGlobalOption('quiet', {
153
+ type: 'boolean',
154
+ alias: 'q',
155
+ description: 'Suppress output'
156
+ })
157
+ }
158
+ })
159
+ ```
160
+
161
+ ### Plugin with Extension
162
+
163
+ ```ts
164
+ import { plugin } from '@gunshi/plugin'
165
+
166
+ export interface LoggerExtension {
167
+ log: (message: string) => void
168
+ error: (message: string) => void
169
+ }
170
+
171
+ export default plugin({
172
+ id: 'logger',
173
+ name: 'Logger Plugin',
174
+
175
+ // Called for each command execution
176
+ extension: (ctx, cmd) => {
177
+ const quiet = ctx.values.quiet ?? false
178
+
179
+ return {
180
+ log: (message: string) => {
181
+ if (!quiet) {
182
+ console.log(message)
183
+ }
184
+ },
185
+ error: (message: string) => {
186
+ console.error(message)
187
+ }
188
+ }
189
+ },
190
+
191
+ // Called after extension is applied
192
+ onExtension: (ctx, cmd) => {
193
+ ctx.extensions.logger.log('Logger initialized')
194
+ }
195
+ })
196
+ ```
197
+
198
+ ## 🔄 Gunshi Lifecycle and Plugins
199
+
200
+ ### CLI Execution Lifecycle
201
+
202
+ ```mermaid
203
+ graph TD
204
+ A[A. CLI Start] --> B[B. Load Plugins]
205
+ B --> C[C. Resolve Dependencies]
206
+ C --> D[D. Execute Plugin Setup]
207
+ D --> E[E. Parse Arguments]
208
+ E --> F[F. Resolve Command]
209
+ F --> G[G. Resolve & Validate Args]
210
+ G --> H[H. Create CommandContext]
211
+ H --> I[I. Apply Extensions]
212
+ I --> J[J. Execute onExtension]
213
+ J --> K[K. Execute Command]
214
+ K --> L[L. CLI End]
215
+
216
+ style B fill:#468c56,color:white
217
+ style C fill:#468c56,color:white
218
+ style D fill:#468c56,color:white
219
+ style I fill:#468c56,color:white
220
+ style J fill:#468c56,color:white
221
+ ```
222
+
223
+ ### Plugin Interaction Points
224
+
225
+ Plugins interact with gunshi at two distinct phases during the CLI lifecycle:
226
+
227
+ **Setup Phase** (steps B-D in the lifecycle):
228
+
229
+ - Occurs during plugin initialization
230
+ - Plugins configure the CLI by adding options, commands, and decorators
231
+ - All modifications are registered but not yet executed
232
+ - This is when the `setup()` function runs
233
+
234
+ **Execution Phase** (steps I-K in the lifecycle):
235
+
236
+ - Occurs when a command is actually being executed
237
+ - Extensions are created and applied to CommandContext
238
+ - Decorators are executed to modify behavior
239
+ - This is when `extension()` and `onExtension()` run
240
+
241
+ The following diagram shows how setup configurations connect to execution behaviors:
242
+
243
+ ```mermaid
244
+ graph LR
245
+ subgraph "Setup Phase"
246
+ S1[addGlobalOption<br/>Register global options]
247
+ S2[addCommand<br/>Register commands]
248
+ S3[decorateHeaderRenderer<br/>Customize header]
249
+ S4[decorateUsageRenderer<br/>Customize usage]
250
+ S5[decorateValidationErrorsRenderer<br/>Customize errors]
251
+ S6[decorateCommand<br/>Wrap execution]
252
+ end
253
+
254
+ subgraph "Execution Phase"
255
+ E1[extension factory<br/>Create context extension]
256
+ E2[onExtension callback<br/>Post-extension hook]
257
+ E3[Decorated renderers<br/>Custom rendering]
258
+ E4[Decorated command<br/>Wrapped execution]
259
+ end
260
+
261
+ S1 --> E3
262
+ S2 --> E4
263
+ S3 --> E3
264
+ S4 --> E3
265
+ S5 --> E3
266
+ S6 --> E4
267
+ ```
268
+
269
+ ## 🔧 Plugin in Depth
270
+
271
+ ### Plugin Dependency Resolution
272
+
273
+ Gunshi uses **topological sorting** to resolve plugin dependencies, ensuring plugins are loaded in the correct order.
274
+
275
+ #### Resolution Process
276
+
277
+ 1. **Build Dependency Graph**: Create a directed graph of plugin dependencies
278
+ 2. **Detect Cycles**: Check for circular dependencies (throws error if found)
279
+ 3. **Topological Sort**: Order plugins so dependencies load before dependents
280
+ 4. **Load Plugins**: Execute setup in resolved order
281
+
282
+ Example:
283
+
284
+ ```ts
285
+ // Given these plugins:
286
+ const pluginA = plugin({
287
+ id: 'a',
288
+ dependencies: ['b', 'c']
289
+ })
290
+
291
+ const pluginB = plugin({
292
+ id: 'b',
293
+ dependencies: ['d']
294
+ })
295
+
296
+ const pluginC = plugin({
297
+ id: 'c'
298
+ })
299
+
300
+ const pluginD = plugin({
301
+ id: 'd'
302
+ })
303
+
304
+ // Resolution order: d → b → c → a
305
+ ```
306
+
307
+ #### Optional Dependencies
308
+
309
+ ```ts
310
+ plugin({
311
+ id: 'logger',
312
+ dependencies: [
313
+ 'core', // Required: throws if missing
314
+ { id: 'colors', optional: true } // Optional: continues if missing
315
+ ],
316
+
317
+ setup(ctx) {
318
+ // Check if optional dependency loaded
319
+ if (ctx.hasCommand('colors')) {
320
+ // Use colors functionality
321
+ }
322
+ }
323
+ })
324
+ ```
325
+
326
+ ### Decorators
327
+
328
+ Decorators in gunshi follow the **LIFO (Last In, First Out)** pattern.
329
+
330
+ #### Execution Order
331
+
332
+ ```ts
333
+ // Registration order
334
+ ctx.decorateCommand(decoratorA) // Registered first
335
+ ctx.decorateCommand(decoratorB) // Registered second
336
+ ctx.decorateCommand(decoratorC) // Registered third
337
+
338
+ // Execution order: C → B → A → original
339
+ ```
340
+
341
+ #### Renderer Decorators
342
+
343
+ Renderer decorators allow you to customize how gunshi displays various output elements like headers, usage information, and error messages. They wrap the base renderer functions to modify or enhance their output.
344
+
345
+ **Common Use Cases:**
346
+
347
+ - Adding colors or styling to output
348
+ - Wrapping content in boxes or borders
349
+ - Adding additional help information
350
+ - Translating or localizing messages
351
+ - Formatting errors for better readability
352
+
353
+ ```ts
354
+ plugin({
355
+ setup(ctx) {
356
+ // Header decorator - Customize command header display
357
+ ctx.decorateHeaderRenderer(async (baseRenderer, ctx) => {
358
+ const header = await baseRenderer(ctx)
359
+ // Add a decorative box around the header
360
+ return `╭─────────────────╮\n│ ${header} │\n╰─────────────────╯`
361
+ })
362
+
363
+ // Usage decorator - Enhance help message
364
+ ctx.decorateUsageRenderer(async (baseRenderer, ctx) => {
365
+ const usage = await baseRenderer(ctx)
366
+ // Append additional help resources
367
+ return usage + '\n\nFor more help: https://docs.example.com'
368
+ })
369
+
370
+ // Validation errors decorator - Format error display
371
+ ctx.decorateValidationErrorsRenderer(async (baseRenderer, ctx, error) => {
372
+ const errors = await baseRenderer(ctx, error)
373
+ // Add error icon and formatting
374
+ return `❌ Validation Failed\n\n${errors}`
375
+ })
376
+ }
377
+ })
378
+ ```
379
+
380
+ **Example Output:**
381
+
382
+ ```sh
383
+ # Before decoration (plain):
384
+ my-cli deploy - Deploy application
385
+
386
+ # After header decoration:
387
+ ╭─────────────────╮
388
+ │ my-cli deploy - Deploy application │
389
+ ╰─────────────────╯
390
+ ```
391
+
392
+ #### Command Decorators
393
+
394
+ Command decorators wrap the actual command execution, allowing you to add behavior before and after commands run. They're perfect for cross-cutting concerns that apply to all commands.
395
+
396
+ **Common Use Cases:**
397
+
398
+ - Performance monitoring and timing
399
+ - Logging and auditing
400
+ - Error handling and recovery
401
+ - Authentication checks
402
+ - Transaction management
403
+ - Resource cleanup
404
+
405
+ ```ts
406
+ plugin({
407
+ setup(ctx) {
408
+ // Timing decorator - Measure execution time
409
+ ctx.decorateCommand(runner => async ctx => {
410
+ const start = performance.now()
411
+ console.log(`🚀 Starting command: ${ctx.name || 'root'}`)
412
+
413
+ try {
414
+ const result = await runner(ctx)
415
+ const duration = performance.now() - start
416
+ console.log(`✅ Completed in ${duration.toFixed(2)}ms`)
417
+ return result
418
+ } catch (error) {
419
+ const duration = performance.now() - start
420
+ console.log(`❌ Failed after ${duration.toFixed(2)}ms`)
421
+ throw error
422
+ }
423
+ })
424
+
425
+ // Error handling decorator - Enhance error reporting
426
+ ctx.decorateCommand(runner => async ctx => {
427
+ try {
428
+ return await runner(ctx)
429
+ } catch (error) {
430
+ // Log detailed error information
431
+ console.error('Command failed:', error.message)
432
+ console.error('Context:', {
433
+ command: ctx.name,
434
+ args: ctx.values,
435
+ positionals: ctx.positionals
436
+ })
437
+
438
+ // Could also send to error tracking service
439
+ // await errorTracker.report(error, ctx)
440
+
441
+ throw error
442
+ }
443
+ })
444
+
445
+ // Authentication decorator - Ensure user is authenticated
446
+ ctx.decorateCommand(runner => async ctx => {
447
+ const auth = ctx.extensions.auth
448
+
449
+ if (auth && !auth.isAuthenticated()) {
450
+ throw new Error('Authentication required. Please login first.')
451
+ }
452
+
453
+ return await runner(ctx)
454
+ })
455
+ }
456
+ })
457
+ ```
458
+
459
+ **Execution Flow Example:**
460
+
461
+ When multiple decorators are applied, they execute in LIFO order:
462
+
463
+ ```sh
464
+ User runs command
465
+ → Authentication decorator (last registered)
466
+ → Error handling decorator
467
+ → Timing decorator (first registered)
468
+ → Actual command execution
469
+ ```
470
+
471
+ ## 🤝 Plugin Communication & Collaboration
472
+
473
+ Gunshi's plugin system provides strong type safety for plugin interactions through TypeScript's type system. This section explains how to create type-safe plugin ecosystems where plugins can communicate and share functionality.
474
+
475
+ ### Type-Safe Access to Other Plugin Extensions
476
+
477
+ The `plugin` function supports type parameters that allow you to declare and access other plugin extensions with full type safety:
478
+
479
+ ```ts
480
+ import { plugin } from '@gunshi/plugin'
481
+
482
+ // Define extension interfaces for each plugin
483
+ export interface LoggerExtension {
484
+ log: (message: string) => void
485
+ error: (message: string) => void
486
+ debug: (message: string) => void
487
+ }
488
+
489
+ export interface AuthExtension {
490
+ isAuthenticated: () => boolean
491
+ getUser: () => { id: string; name: string }
492
+ getToken: () => string | undefined
493
+ }
494
+
495
+ // Logger plugin - provides LoggerExtension
496
+ export const loggerPlugin = plugin({
497
+ id: 'logger',
498
+ name: 'Logger Plugin',
499
+
500
+ extension: (): LoggerExtension => ({
501
+ log: (message: string) => console.log(`[LOG] ${message}`),
502
+ error: (message: string) => console.error(`[ERROR] ${message}`),
503
+ debug: (message: string) => console.debug(`[DEBUG] ${message}`)
504
+ })
505
+ })
506
+
507
+ // Auth plugin - provides AuthExtension and uses LoggerExtension
508
+ export const authPlugin = plugin<
509
+ { logger: LoggerExtension }, // Declare dependency extensions
510
+ 'auth', // Plugin ID as literal type
511
+ ['logger'], // Plugin Dependencies type
512
+ AuthExtensions // Extension factory return type
513
+ >({
514
+ id: 'auth',
515
+ name: 'Authentication Plugin',
516
+ dependencies: ['logger'], // dependency declaration
517
+
518
+ setup: ctx => {
519
+ // Type-safe access to logger in setup
520
+ ctx.decorateCommand(runner => async cmdCtx => {
521
+ // cmdCtx.extensions is typed as { auth: AuthExtension, logger: LoggerExtension }
522
+ cmdCtx.extensions.logger.log('Command started')
523
+ return await runner(cmdCtx)
524
+ })
525
+ },
526
+
527
+ extension: ctx => {
528
+ // ctx.extensions.logger is fully typed as LoggerExtension
529
+ ctx.extensions.logger.log('Auth plugin initializing')
530
+
531
+ return {
532
+ isAuthenticated: () => {
533
+ const token = process.env.AUTH_TOKEN
534
+ return Boolean(token)
535
+ },
536
+ getUser: () => ({
537
+ id: '123',
538
+ name: 'John Doe'
539
+ }),
540
+ getToken: () => process.env.AUTH_TOKEN
541
+ }
542
+ },
543
+
544
+ onExtension: ctx => {
545
+ // Both extensions are available and typed
546
+ if (ctx.extensions.auth.isAuthenticated()) {
547
+ ctx.extensions.logger.log('User authenticated')
548
+ }
549
+ }
550
+ })
551
+ ```
552
+
553
+ ### Type-Safe Plugin Extensions in Commands
554
+
555
+ When defining commands, you can specify which plugin extensions your command requires, and TypeScript will ensure type safety:
556
+
557
+ ```ts
558
+ import { define, CommandContext } from 'gunshi'
559
+ import type { LoggerExtension, AuthExtension, DatabaseExtension } from 'your-plugin'
560
+
561
+ // Define the extensions your command needs
562
+ type MyCommandExtensions = {
563
+ logger: LoggerExtension
564
+ auth: AuthExtension
565
+ db?: DatabaseExtension // Optional extension
566
+ }
567
+
568
+ // Define command with typed extensions
569
+ export const deployCommand = define<MyCommandExtensions>({
570
+ name: 'deploy',
571
+ description: 'Deploy the application',
572
+ run: async ctx => {
573
+ // All extensions are fully typed
574
+ const { logger, auth, db } = ctx.extensions
575
+
576
+ // Type-safe usage
577
+ if (!auth.isAuthenticated()) {
578
+ logger.error('Authentication required')
579
+ throw new Error('Please login first')
580
+ }
581
+
582
+ const user = auth.getUser()
583
+ logger.log(`Deploying as ${user.name}`)
584
+
585
+ // Optional extension with safe access
586
+ await db?.logDeployment(user.id, ctx.values.environment)
587
+
588
+ // Command implementation...
589
+ }
590
+ })
591
+ ```
592
+
593
+ ## 💡 Plugin Best Practices
594
+
595
+ ### Plugin Package Naming Conventions
596
+
597
+ Follow these naming conventions for consistency:
598
+
599
+ - **Standalone packages**: `gunshi-plugin-{feature}`
600
+ - Example: `gunshi-plugin-logger`, `gunshi-plugin-auth`
601
+ - **Scoped packages**: `@{scope}/plugin-{feature}`
602
+ - Example: `@mycompany/plugin-logger`
603
+
604
+ ### Plugin ID Namespacing & Definition
605
+
606
+ Use namespaced IDs to avoid conflicts and export them for reusability:
607
+
608
+ ```ts
609
+ // ✅ Good: namespaced
610
+ plugin({ id: 'mycompany:auth' })
611
+ plugin({ id: 'g:renderer' }) // 'g:' for gunshi official
612
+
613
+ // ❌ Bad: might conflict
614
+ plugin({ id: 'auth' })
615
+ plugin({ id: 'logger' })
616
+ ```
617
+
618
+ **Export your plugin ID to prevent hardcoding:**
619
+
620
+ By exporting your plugin ID, you enable other plugin authors and users to reference it without hardcoding strings:
621
+
622
+ <!-- eslint-skip -->
623
+
624
+ ```ts
625
+ // Export plugin ID as a constant
626
+ export const pluginId = 'mycompany:auth' as const
627
+
628
+ // Export plugin ID type for `plugin` function type parameters
629
+ export type PluginId = typeof pluginId
630
+
631
+ // your-plugin/src/index.ts
632
+ import { pluginId } from './types.ts'
633
+
634
+ export { pluginId } from './types.ts' // Re-export for consumers
635
+
636
+ export default function auth() {
637
+ return plugin({
638
+ id: pluginId // Use the constant instead of hardcoding
639
+ // ...
640
+ })
641
+ }
642
+ ```
643
+
644
+ **Benefits for other plugin authors:**
645
+
646
+ Other plugins can now import and use your plugin ID directly:
647
+
648
+ ```ts
649
+ // another-plugin/src/index.ts
650
+ import { pluginId as authPluginId } from 'your-auth-plugin'
651
+ import type { PluginId as AuthId, AuthExtension } from 'your-auth-plugin'
652
+
653
+ export const pluginId = 'mycompany:api' as const
654
+ export type PluginId = typeof pluginId
655
+
656
+ // Define plugin extension for user ...
657
+ export interface ApiExtension {
658
+ // ...
659
+ }
660
+
661
+ // Use imported ID instead of hardcoding 'mycompany:auth'
662
+ const dependencies = [authPluginId] as const
663
+
664
+ export default function api() {
665
+ // Set Auth Plugin Extension to type parameters to use it as typed extensions
666
+ return plugin<Record<AuthId, AuthExtension>, PluginId, typeof dependencies, ApiExtension>({
667
+ id: pluginId,
668
+ dependencies,
669
+
670
+ onExtension: ctx => {
671
+ // Access extension using imported ID
672
+ const auth = ctx.extensions[authPluginId]
673
+
674
+ if (auth.isAuthenticated()) {
675
+ // Use auth features
676
+ }
677
+ }
678
+ })
679
+ }
680
+ ```
681
+
682
+ **Benefits for plugin users:**
683
+
684
+ Users can also access extensions without hardcoding plugin IDs:
685
+
686
+ ```ts
687
+ // user-code.ts
688
+ import { define } from 'gunshi'
689
+ import auth, { pluginId as authId } from 'your-auth-plugin'
690
+ import type { AuthExtension, PluginId as AuthId } from 'your-auth-plugin'
691
+
692
+ const myCommand = define<Record<AuthId, AuthExtension>>({
693
+ name: 'deploy',
694
+
695
+ run: ctx => {
696
+ // Type-safe access using imported plugin id, no hardcoded 'mycompany:auth' strings!
697
+ const auth = ctx.extensions[authId]
698
+
699
+ if (!auth.isAuthenticated()) {
700
+ throw new Error('Please login first')
701
+ }
702
+ }
703
+ })
704
+ ```
705
+
706
+ **Complete example with multiple plugins:**
707
+
708
+ ```ts
709
+ // Export all plugin IDs from a central location
710
+ // plugins/index.ts
711
+ export { pluginId as authPluginId } from '@mycompany/plugin-auth'
712
+ export { pluginId as loggerPluginId } from '@mycompany/plugin-logger'
713
+ export { pluginId as dbPluginId } from '@mycompany/plugin-database'
714
+
715
+ // Use them consistently across your application
716
+ import { cli } from 'gunshi'
717
+ import auth, { pluginId as authId } from '@mycompany/plugin-auth'
718
+ import logger, { pluginId as loggerId } from '@mycompany/plugin-logger'
719
+ import db, { pluginId as dbId } from '@mycompany/plugin-database'
720
+
721
+ // Import extension types
722
+ import type { PluginId as AuthId, AuthExtension } from '@mycompany/plugin-auth'
723
+ import type { PluginId as LoggerId, LoggerExtension } from '@mycompany/plugin-logger'
724
+ import type { PluginId as DbId, DbExtension } from '@mycompany/plugin-database'
725
+
726
+ // Define extensions to use it for command context as typed extensions
727
+ type Extensions = Record<AuthId, AuthExtension> &
728
+ Record<LoggerId, LoggerExtension> &
729
+ Record<DbId, DbExtension>
730
+
731
+ await cli<Extensions>(
732
+ process.argv.slice(2),
733
+ // Entry command
734
+ async ctx => {
735
+ // All plugin IDs are imported, no hardcoding needed
736
+ const auth = ctx.extensions[authId]
737
+ const logger = ctx.extensions[loggerId]
738
+ const db = ctx.extensions[dbId]
739
+
740
+ logger.log('Application started')
741
+
742
+ if (auth.isAuthenticated()) {
743
+ await db.connect()
744
+ }
745
+ },
746
+ // CLI options
747
+ {
748
+ plugins: [auth(), logger(), db()] // Install plugins
749
+ }
750
+ )
751
+ ```
752
+
753
+ ### Declare Plugin Dependencies Explicitly
754
+
755
+ Always declare your plugin dependencies explicitly to ensure proper initialization order and availability:
756
+
757
+ <!-- eslint-skip -->
758
+
759
+ ```ts
760
+ dependencies: ['required-plugin', { id: 'optional-plugin', optional: true }]
761
+ ```
762
+
763
+ **Why explicit dependencies are important:**
764
+
765
+ 1. **Guaranteed Load Order**: Gunshi uses topological sorting to ensure dependencies are loaded before dependent plugins
766
+ 2. **Runtime Safety**: Required dependencies throw errors if missing, preventing runtime failures
767
+ 3. **Optional Flexibility**: Optional dependencies allow graceful degradation when plugins aren't available
768
+ 4. **Clear Documentation**: Dependencies are self-documenting, making it clear what plugins work together
769
+ 5. **Type Safety**: When combined with TypeScript, dependencies enable proper type checking of extensions
770
+
771
+ **Example with reasoning:**
772
+
773
+ ```ts
774
+ export default function analytics() {
775
+ return plugin({
776
+ id: 'mycompany:analytics',
777
+
778
+ // Declare dependencies explicitly
779
+ dependencies: [
780
+ 'mycompany:logger', // Required: analytics needs logging
781
+ 'mycompany:auth', // Required: need user info for tracking
782
+ { id: 'mycompany:database', optional: true } // Optional: can work without persistence
783
+ ],
784
+
785
+ extension: ctx => {
786
+ // Required dependencies are guaranteed to exist
787
+ const logger = ctx.extensions['mycompany:logger'] // Safe to access
788
+ const auth = ctx.extensions['mycompany:auth'] // Safe to access
789
+
790
+ // Optional dependencies might be undefined
791
+ const db = ctx.extensions['mycompany:db'] // May be undefined
792
+
793
+ return {
794
+ track: async (event: string, data: unknown) => {
795
+ const user = auth.getUser()
796
+ logger.log(`Track: ${event} by ${user.id}`)
35
797
 
36
- ```js
37
- // TODO(kazupon): prepare the example
798
+ // Gracefully handle optional dependency
799
+ if (db) {
800
+ await db.store('analytics', { event, user, data })
801
+ } else {
802
+ logger.warn('Analytics not persisted (database plugin not available)')
803
+ }
804
+ }
805
+ }
806
+ }
807
+ })
808
+ }
38
809
  ```
39
810
 
811
+ **Benefits of explicit dependencies:**
812
+
813
+ - **Fail Fast**: Missing required plugins fail at startup, not runtime
814
+ - **Predictable**: Plugin loading order is deterministic
815
+ - **Maintainable**: Easy to see and update plugin relationships
816
+ - **Testable**: Can mock dependencies in tests
817
+
818
+ ### Avoid Circular Dependencies
819
+
820
+ <!-- eslint-skip -->
821
+
822
+ ```ts
823
+ // ❌ Bad: Circular dependency
824
+ pluginA: {
825
+ dependencies: ['pluginB']
826
+ }
827
+ pluginB: {
828
+ dependencies: ['pluginA']
829
+ }
830
+
831
+ // ✅ Good: Extract shared functionality
832
+ pluginShared: {
833
+ /* shared logic */
834
+ }
835
+ pluginA: {
836
+ dependencies: ['pluginShared']
837
+ }
838
+ pluginB: {
839
+ dependencies: ['pluginShared']
840
+ }
841
+ ```
842
+
843
+ ### Gracefully Handle Optional Dependencies
844
+
845
+ When declaring optional dependencies, your plugin must be designed to work correctly even when those dependencies are not installed. This ensures your plugin can adapt to different environments and configurations.
846
+
847
+ **Key principles:**
848
+
849
+ 1. **Always check for existence** before using optional dependencies
850
+ 2. **Provide fallback behavior** when optional dependencies are unavailable
851
+ 3. **Don't assume optional dependencies exist** in any part of your code
852
+ 4. **Test your plugin** with and without optional dependencies
853
+
854
+ **Example implementation pattern:**
855
+
856
+ ```ts
857
+ import { pluginId as i18nPluginId, resolveKey } from '@gunshi/plugin-i18n'
858
+ import type { I18nExtension } from '@gunshi/plugin-i18n'
859
+
860
+ export const pluginId = 'g:renderer' as const
861
+
862
+ export interface UsageRendererExtension {
863
+ // ...
864
+ }
865
+
866
+ const dependencies = [i18nPluginId] as const
867
+
868
+ export default function renderer() {
869
+ return plugin<
870
+ Record<typeof i18nPluginId, I18nExtension>,
871
+ typeof pluginId,
872
+ typeof dependencies,
873
+ UsageRendererExtension
874
+ >({
875
+ id: pluginId,
876
+ name: 'Usage Renderer',
877
+ dependencies,
878
+
879
+ extension: (ctx, cmd) => {
880
+ // Get optional extension (may be undefined)
881
+ const i18n = ctx.extensions[i18nPluginId]
882
+
883
+ return {
884
+ // Gracefully handle missing i18n plugin
885
+ text: localizable(ctx, cmd, i18n?.translate), // Pass undefined if i18n not available
886
+
887
+ renderHelp: ctx => {
888
+ // eslint-disable-next-line unicorn/prefer-ternary -- example
889
+ if (i18n) {
890
+ // Use i18n features when available
891
+ return i18n.translate(resolveKey('message', ctx))
892
+ } else {
893
+ // Fallback to default behavior
894
+ return 'Help message'
895
+ }
896
+ }
897
+ }
898
+ },
899
+
900
+ onExtension: async (ctx, cmd) => {
901
+ const i18n = ctx.extensions[i18nPluginId]
902
+
903
+ // Only use optional features if available
904
+ if (i18n) {
905
+ await i18n.loadResource(ctx.locale, ctx, cmd)
906
+ }
907
+
908
+ // Core functionality works regardless
909
+ console.log('Renderer plugin initialized')
910
+ }
911
+ })
912
+ }
913
+ ```
914
+
915
+ **Implementing adaptive functionality:**
916
+
917
+ ```ts
918
+ // Helper function that works with or without optional dependency
919
+ function localizable(ctx: CommandContext, cmd: Command, translate?: TranslateFunction) {
920
+ return (key: string, values?: Record<string, unknown>) => {
921
+ // eslint-disable-next-line unicorn/prefer-ternary -- example
922
+ if (translate) {
923
+ // Use translation when available
924
+ return translate(key, values)
925
+ } else {
926
+ // Fallback to key or default resource
927
+ return DefaultResource[key] || key
928
+ }
929
+ }
930
+ }
931
+ ```
932
+
933
+ **Benefits of this approach:**
934
+
935
+ - **Flexibility**: Users can choose which plugins to install
936
+ - **Reduced dependencies**: Core functionality doesn't require all plugins
937
+ - **Better performance**: Smaller installation size when optional plugins are omitted
938
+ - **Graceful degradation**: Features degrade smoothly rather than failing completely
939
+
940
+ ### Use extensions with optional chaining
941
+
942
+ Plugin extensions are dynamically injected, so it is safe to use optional chaining in the following cases.
943
+
944
+ #### Optional plugin dependencies
945
+
946
+ If your plugin depends on a specific plugin extension, we recommend using an option chain when using that extension:
947
+
948
+ ```ts
949
+ import { pluginId as loggerPluginId } from 'your-auth-logger'
950
+ import type { PluginId as LoggerId, LoggerExtension } from 'your-logger-plugin'
951
+
952
+ export const pluginId = 'my:api' as const
953
+ export type PluginId = typeof pluginId
954
+
955
+ export interface ApiExtension {}
956
+
957
+ // Depend on optional plugin
958
+ const dependencies = [{ id: 'my:logger', optional: true }] as const
959
+
960
+ export default function api() {
961
+ return plugin<Record<LoggerId, LoggerExtension>, PluginId, typeof dependencies, ApiExtension>({
962
+ id: pluginId,
963
+ dependencies,
964
+ onExtension: ctx => {
965
+ // May be `undefined`
966
+ const logger = ctx.extensions[LoggerId]
967
+
968
+ // Safe access
969
+ logger?.log('message')
970
+ }
971
+ })
972
+ }
973
+ ```
974
+
975
+ #### Command definition
976
+
977
+ If the `define` function has an implementation that depends on a specific plugin extension, it's recommended to use optional chaining to use the extension:
978
+
979
+ ```ts
980
+ import { define } from 'gunshi'
981
+ import auth, { pluginId as authId } from 'your-auth-plugin'
982
+ import type { AuthExtension, PluginId as AuthId } from 'your-auth-plugin'
983
+
984
+ const myCommand = define<Record<AuthId, AuthExtension>>({
985
+ // ...
986
+
987
+ run: async ctx => {
988
+ const auth = ctx.extensions[authId] // Maybe `undefined`
989
+
990
+ // Use safelly extensions with optional chaining
991
+ if (auth?.isAuthenticated()) {
992
+ // For login ...
993
+ } else {
994
+ throw new Error('Please login first')
995
+ }
996
+ }
997
+ })
998
+ ```
999
+
1000
+ ## 📚 Official Plugins
1001
+
1002
+ Learn from gunshi's official plugins:
1003
+
1004
+ ### `@gunshi/plugin-global`
1005
+
1006
+ - **Pattern**: Global options, command interception
1007
+ - **Learn**: How to add universal CLI features
1008
+
1009
+ ### `@gunshi/plugin-i18n`
1010
+
1011
+ - **Pattern**: Complex extensions, resource loading
1012
+ - **Learn**: How to manage stateful extensions
1013
+
1014
+ ### `@gunshi/plugin-renderer`
1015
+
1016
+ - **Pattern**: Renderer decorators
1017
+ - **Learn**: How to customize output formatting
1018
+
1019
+ ### `@gunshi/plugin-completion`
1020
+
1021
+ - **Pattern**: Dynamic command generation
1022
+ - **Learn**: How to add utility commands
1023
+
1024
+ ## đŸ‘Ĩ Community Plugins
1025
+
1026
+ <!-- eslint-disable markdown/no-missing-label-refs -->
1027
+
1028
+ > [!NOTE]
1029
+ > Welcome your plugins! Submit a PR to add your plugin to this list.
1030
+
1031
+ <!-- eslint-enable markdown/no-missing-label-refs -->
1032
+
1033
+ ## 📖 API Reference
1034
+
1035
+ TODO: referer the API section of gunshi docs
1036
+
40
1037
  ## ÂŠī¸ License
41
1038
 
42
1039
  [MIT](http://opensource.org/licenses/MIT)
1040
+
1041
+ <!-- Badges -->
1042
+
1043
+ [npm-version-src]: https://img.shields.io/npm/v/@gunshi/plugin?style=flat
1044
+ [npm-version-href]: https://npmjs.com/package/@gunshi/plugin@alpha
1045
+ [jsr-src]: https://jsr.io/badges/@gunshi/plugin
1046
+ [jsr-href]: https://jsr.io/@gunshi/plugin
1047
+ [install-size-src]: https://pkg-size.dev/badge/install/27672