@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,940 @@
1
+ # Plugin Development Guidelines
2
+
3
+ This guide provides practical guidelines for developing reliable, maintainable, and performant Gunshi plugins.
4
+
5
+ While other sections cover implementation details and APIs, this guide focuses on recommended approaches and techniques for production-ready plugins.
6
+
7
+ <!-- eslint-disable markdown/no-missing-label-refs -->
8
+
9
+ > [!TIP]
10
+ > We recommend developing Gunshi plugins with TypeScript for enhanced type safety, better IDE support, and compile-time error detection. All examples and code snippets in this guide are written in TypeScript. While JavaScript plugins are supported, TypeScript helps prevent runtime errors and provides a superior developer experience through auto-completion and type checking.
11
+
12
+ <!-- eslint-enable markdown/no-missing-label-refs -->
13
+
14
+ <!-- eslint-disable markdown/no-missing-label-refs -->
15
+
16
+ > [!NOTE]
17
+ > Some code examples in this guide include TypeScript file extensions (`.ts`) in `import`/`export` statements. If you use this pattern in your plugin, you'll need to enable `allowImportingTsExtensions` in your `tsconfig.json`.
18
+
19
+ <!-- eslint-enable markdown/no-missing-label-refs -->
20
+
21
+ ## Design Principles
22
+
23
+ When developing Gunshi plugins, follow these core principles:
24
+
25
+ - **Single Responsibility**: Each plugin should have one clear purpose
26
+ - **Fail Fast**: Validate configuration early and provide clear error messages
27
+ - **Graceful Degradation**: Handle optional dependencies and features gracefully
28
+ - **Type Safety**: Export type definitions for all public interfaces
29
+ - **Performance Conscious**: Use lazy initialization and avoid blocking operations
30
+
31
+ These principles work together to create a robust plugin ecosystem. Single responsibility prevents conflicts and simplifies debugging.
32
+
33
+ Early validation saves time by catching errors at initialization. Graceful degradation ensures compatibility across environments.
34
+
35
+ Type safety prevents runtime errors and improves developer experience. Performance consciousness ensures responsive CLIs with instant feedback.
36
+
37
+ ## Naming Conventions
38
+
39
+ ### Plugin IDs
40
+
41
+ Use namespaced IDs to prevent conflicts and clearly identify plugin ownership.
42
+
43
+ Namespacing prevents ID collisions in large applications where multiple teams might develop plugins independently.
44
+
45
+ It enables plugin discovery and filtering by namespace, making it easy to identify official plugins versus third-party extensions.
46
+
47
+ Additionally, this convention clarifies ownership and responsibility, helping users understand a plugin's source, maintenance status, and trustworthiness at a glance.
48
+
49
+ The following examples demonstrate different namespacing conventions for plugin IDs:
50
+
51
+ ```ts
52
+ // Organization namespace
53
+ export const pluginId = 'myorg:logger' as const
54
+
55
+ // Scoped package format
56
+ export const pluginId = '@company/auth' as const
57
+
58
+ // Official Gunshi plugins
59
+ export const pluginId = 'g:i18n' as const
60
+ ```
61
+
62
+ ### Package Names
63
+
64
+ Follow consistent naming for plugin packages:
65
+
66
+ - Standalone packages: `gunshi-plugin-{feature}`
67
+ - Scoped packages: `@{org}/gunshi-plugin-{feature}` or `@{org}/gunshi-plugin`
68
+ - Example: `gunshi-plugin-logger`, `@mycompany/gunshi-plugin-auth`, `@feature/gunshi/plugin`
69
+
70
+ <!-- eslint-disable markdown/no-missing-label-refs -->
71
+
72
+ > [!NOTE]
73
+ > Packages following the pattern `@gunshi/plugin-{feature}` (e.g., `@gunshi/plugin-i18n`) are official plugins maintained by the Gunshi team. These are not third-party plugins but are part of the official Gunshi ecosystem. Third-party developers should use their own organization scope or standalone naming as described above.
74
+
75
+ <!-- eslint-enable markdown/no-missing-label-refs -->
76
+
77
+ ## Plugin Structure
78
+
79
+ ### `gunshi/plugin` vs `@gunshi/plugin`
80
+
81
+ When developing Gunshi plugins, you need to import the `plugin` function and related types.
82
+
83
+ Plugin developers can import from either `gunshi/plugin` or `@gunshi/plugin`. Both provide identical APIs and type definitions.
84
+
85
+ Use `@gunshi/plugin` when you want to minimize your plugin's dependencies and reduce `node_modules` size, as it's a smaller package that only includes plugin-related functionality.
86
+
87
+ For plugin development, we recommend using `@gunshi/plugin` to keep your plugin package lightweight.
88
+
89
+ Here are the two equivalent import options available to plugin developers:
90
+
91
+ ```ts
92
+ // Option 1: Import from main gunshi package
93
+ import { plugin } from 'gunshi/plugin'
94
+
95
+ // Option 2: Import from dedicated plugin package
96
+ import { plugin } from '@gunshi/plugin'
97
+ ```
98
+
99
+ ### Factory Function Approach
100
+
101
+ Create plugins as factory functions to allow configuration at initialization.
102
+
103
+ This approach enables configuration flexibility by accepting options at plugin creation time, allowing each instance to be configured independently without relying on global state or environment variables.
104
+
105
+ The factory function should be named after the plugin's primary functionality to make its purpose immediately clear.
106
+
107
+ **Good naming examples:**
108
+
109
+ - `logger()` for a logging plugin
110
+ - `auth()` for an authentication plugin
111
+ - `database()` for a database connection plugin
112
+
113
+ **Avoid generic names:**
114
+
115
+ - `createPlugin()` - Too generic
116
+ - `setup()` - Unclear purpose
117
+ - `init()` - Non-descriptive
118
+
119
+ Here's a complete factory function implementation:
120
+
121
+ ```ts
122
+ export interface LoggerOptions {
123
+ level?: 'debug' | 'info' | 'warn' | 'error'
124
+ format?: 'json' | 'text'
125
+ }
126
+
127
+ export default function logger(options: LoggerOptions = {}) {
128
+ const { level = 'info', format = 'text' } = options
129
+
130
+ return plugin({
131
+ id: 'logger',
132
+ extension: () => ({
133
+ log: (message: string) => {
134
+ // Use options to configure behavior
135
+ if (format === 'json') {
136
+ console.log(JSON.stringify({ level, message }))
137
+ } else {
138
+ console.log(`[${level}] ${message}`)
139
+ }
140
+ }
141
+ })
142
+ })
143
+ }
144
+ ```
145
+
146
+ ### Export Types and Constants
147
+
148
+ Exporting types enables TypeScript consumers to properly type their code, preventing runtime type mismatches in production.
149
+
150
+ This practice improves IDE autocomplete and IntelliSense, allowing developers to discover your plugin's API more easily.
151
+
152
+ It also enables compile-time verification of correct plugin usage, catching integration errors during development rather than after deployment.
153
+
154
+ The following example shows how to properly export types and constants from your plugin:
155
+
156
+ ```ts [types.ts]
157
+ export const pluginId = 'myorg:feature' as const
158
+ export type PluginId = typeof pluginId
159
+
160
+ export interface FeatureExtension {
161
+ process: (data: Data) => Promise<Result>
162
+ }
163
+ ```
164
+
165
+ ```ts [index.ts]
166
+ export * from './types.ts'
167
+ export { default } from './plugin.ts'
168
+ ```
169
+
170
+ For detailed type system usage, see [Plugin Type System](./type-system.md).
171
+
172
+ ## Error Handling
173
+
174
+ ### Validate Early, Fail Fast
175
+
176
+ Early validation helps avoid runtime issues that could occur deep in execution, potentially after performing partial operations or consuming computational resources.
177
+
178
+ By validating in the factory function, you can provide clearer error messages with full context about invalid configuration and specific steps to fix it.
179
+
180
+ This approach helps with debugging by catching errors at initialization rather than during command execution, when the source of the problem may be less clear.
181
+
182
+ The following example demonstrates early validation in the factory function:
183
+
184
+ ```ts
185
+ export default function api(endpoint: string) {
186
+ // Validate immediately
187
+ if (!endpoint || !endpoint.startsWith('http')) {
188
+ throw new Error('API plugin requires valid HTTP(S) endpoint URL')
189
+ }
190
+
191
+ return plugin({
192
+ id: 'api',
193
+ extension: () => ({
194
+ fetch: async (path: string) => {
195
+ // Endpoint is already validated
196
+ return await fetch(`${endpoint}${path}`)
197
+ }
198
+ })
199
+ })
200
+ }
201
+ ```
202
+
203
+ ### Provide Actionable Error Messages
204
+
205
+ Clear, actionable error messages reduce debugging time by pointing developers to the root cause and solution.
206
+
207
+ When errors provide specific context, possible causes, and concrete next steps, developers can more easily diagnose and fix issues independently.
208
+
209
+ This example shows how to provide helpful error messages with context and solutions:
210
+
211
+ <!-- eslint-skip -->
212
+
213
+ ```ts
214
+ extension: ctx => ({
215
+ connect: async (url: string) => {
216
+ try {
217
+ return await establishConnection(url)
218
+ } catch (error) {
219
+ // Provide context and solution
220
+ throw new Error(
221
+ `Failed to connect to ${url}.\n` +
222
+ `Possible causes:\n` +
223
+ ` - Network connectivity issues\n` +
224
+ ` - Invalid URL format\n` +
225
+ ` - Server is not responding\n` +
226
+ `Try: Verify the URL and your network connection`
227
+ )
228
+ }
229
+ }
230
+ })
231
+ ```
232
+
233
+ ### Handle Optional Dependencies Gracefully
234
+
235
+ Check for optional dependencies before using them.
236
+
237
+ Graceful dependency handling ensures your plugin works across different environments and configurations, preventing cascading failures when optional plugins are not available.
238
+
239
+ Choose the appropriate approach based on your needs.
240
+
241
+ The following example demonstrates different patterns for handling optional dependencies:
242
+
243
+ <!-- eslint-skip -->
244
+
245
+ ```ts
246
+ extension: ctx => {
247
+ // Optional dependencies from other plugins
248
+ const logger = ctx.extensions.logger
249
+ const cache = ctx.extensions.cache
250
+
251
+ return {
252
+ processData: async (data: Data) => {
253
+ // Use optional chaining (?.) for single operations
254
+ logger?.info(`Processing data: ${data.id}`)
255
+
256
+ // Use if statements for complex logic or multiple operations
257
+ if (cache) {
258
+ const cached = await cache.get(data.id)
259
+ if (cached) {
260
+ logger?.info('Cache hit') // Mix patterns when appropriate
261
+ return cached
262
+ }
263
+ }
264
+
265
+ // Process the data
266
+ try {
267
+ const result = await doProcessing(data)
268
+
269
+ // Conditional block for related operations
270
+ if (cache && result) {
271
+ await cache.set(data.id, result)
272
+ await cache.setExpiry(data.id, 3600)
273
+ }
274
+
275
+ logger?.info('Processing completed')
276
+ return result
277
+ } catch (error) {
278
+ logger?.error(`Failed: ${error.message}`)
279
+ throw error
280
+ }
281
+ }
282
+ }
283
+ }
284
+ ```
285
+
286
+ ## Resource Management
287
+
288
+ After establishing proper error handling, the next important aspect is managing resources effectively.
289
+
290
+ Proper resource management prevents memory leaks and ensures your plugin releases system resources correctly, especially important when errors occur during execution.
291
+
292
+ ### Clean Up Resources
293
+
294
+ Proper resource cleanup helps prevent memory leaks in long-running CLI tools.
295
+
296
+ System resources have practical limits - file handles (typically 1024 per process) and database connections (often 100-200 per server) can be exhausted without proper cleanup.
297
+
298
+ The following example demonstrates how to implement cleanup mechanisms for managing multiple database connections.
299
+
300
+ The plugin tracks all created connections in an array and provides a cleanup method that closes them all when the process exits:
301
+
302
+ <!-- eslint-skip -->
303
+
304
+ ```ts
305
+ extension: () => {
306
+ const connections: Connection[] = []
307
+
308
+ return {
309
+ connect: async () => {
310
+ const conn = await createConnection()
311
+ connections.push(conn)
312
+ return conn
313
+ },
314
+
315
+ cleanup: async () => {
316
+ await Promise.all(connections.map(c => c.close()))
317
+ connections.length = 0
318
+ }
319
+ }
320
+ }
321
+
322
+ // Use in `onExtension` or command hooks
323
+ onExtension: ctx => {
324
+ process.once('exit', () => ctx.extensions.myPlugin.cleanup())
325
+ }
326
+ ```
327
+
328
+ ### Handle Process Signals
329
+
330
+ Your plugin should respond to system signals for graceful shutdown. The following code shows how to register cleanup handlers that disconnect from a database when the process receives termination signals (SIGINT from Ctrl+C or SIGTERM from system shutdown):
331
+
332
+ <!-- eslint-skip -->
333
+
334
+ ```ts
335
+ onExtension: ctx => {
336
+ const cleanup = async () => {
337
+ await ctx.extensions.database.disconnect()
338
+ process.exit(0)
339
+ }
340
+
341
+ process.once('SIGINT', cleanup)
342
+ process.once('SIGTERM', cleanup)
343
+ }
344
+ ```
345
+
346
+ ## Performance Considerations
347
+
348
+ With proper resource management in place, you can focus on optimizing when and how resources are created and accessed.
349
+
350
+ The following techniques improve CLI startup time and responsiveness while maintaining the resource cleanup patterns discussed earlier.
351
+
352
+ ### Use Lazy Initialization
353
+
354
+ Lazy initialization improves CLI startup time by deferring expensive operations until actually needed.
355
+
356
+ This ensures quick response times for simple commands.
357
+
358
+ The following example demonstrates how to defer database connection initialization until the first query is executed:
359
+
360
+ <!-- eslint-skip -->
361
+
362
+ ```ts
363
+ extension: () => {
364
+ let connection: Database | null = null
365
+
366
+ const getConnection = async () => {
367
+ // Only create the connection on first access
368
+ if (!connection) {
369
+ // Expensive operation deferred until actually needed
370
+ connection = await createConnection()
371
+ }
372
+ return connection
373
+ }
374
+
375
+ return {
376
+ query: async (sql: string) => {
377
+ // Lazily initialize connection when query is first called
378
+ const conn = await getConnection()
379
+ return conn.execute(sql)
380
+ }
381
+ }
382
+ }
383
+ ```
384
+
385
+ ### Avoid Blocking Operations
386
+
387
+ Blocking operations freeze the CLI interface and degrade user experience. Use asynchronous operations to maintain interactivity, especially during initialization and command execution.
388
+
389
+ The following examples contrast blocking and non-blocking approaches to file operations and data processing:
390
+
391
+ <!-- eslint-skip -->
392
+
393
+ ```ts
394
+ extension: () => ({
395
+ // Bad: Blocks the entire CLI during initialization
396
+ loadConfig: () => {
397
+ const data = fs.readFileSync('./config.json', 'utf8') // Blocks!
398
+ return JSON.parse(data)
399
+ },
400
+
401
+ // Good: Non-blocking async operation
402
+ loadConfig: async () => {
403
+ const data = await fs.promises.readFile('./config.json', 'utf8')
404
+ return JSON.parse(data)
405
+ },
406
+
407
+ // Better: Non-blocking with progress feedback
408
+ processLargeDataset: async files => {
409
+ const results = []
410
+ for (const [index, file] of files.entries()) {
411
+ // Process file asynchronously
412
+ const data = await processFile(file)
413
+ results.push(data)
414
+ console.log(`Processing: ${index + 1}/${files.length}`)
415
+ }
416
+ console.log() // New line after progress
417
+ return results
418
+ }
419
+ })
420
+ ```
421
+
422
+ ### Optimize Module Loading
423
+
424
+ Loading heavy dependencies at startup increases CLI initialization time and memory usage. Use dynamic imports to load heavy dependencies only when needed, keeping the initial load minimal for fast command response.
425
+
426
+ These examples demonstrate how to use dynamic imports to defer loading heavy dependencies:
427
+
428
+ <!-- eslint-skip -->
429
+
430
+ ```ts
431
+ // Bad: Module-level import loads dependency at startup
432
+ import ExcelJS from 'exceljs' // 4MB loaded at startup!
433
+
434
+ extension: () => ({
435
+ generateReport: async data => {
436
+ const workbook = new ExcelJS.Workbook()
437
+ // Generate report...
438
+ }
439
+ // Other methods that don't use ExcelJS...
440
+ })
441
+
442
+ // Good: Dynamic import loads dependency only when needed
443
+ extension: () => ({
444
+ generateReport: async data => {
445
+ // Load 4MB dependency only when report is actually generated
446
+ const { Workbook } = await import('exceljs')
447
+ const workbook = new Workbook()
448
+ // Generate report...
449
+ },
450
+
451
+ // Better: Combine with user feedback for perceived performance
452
+ exportData: async (format, data) => {
453
+ if (format === 'excel') {
454
+ console.log('Loading Excel export module...')
455
+ const { exportToExcel } = await import('./exporters/excel.js')
456
+ return exportToExcel(data)
457
+ } else if (format === 'pdf') {
458
+ console.log('Loading PDF export module...')
459
+ const { exportToPDF } = await import('./exporters/pdf.js')
460
+ return exportToPDF(data)
461
+ }
462
+ // Default lightweight JSON export
463
+ return JSON.stringify(data)
464
+ }
465
+ })
466
+ ```
467
+
468
+ For more `extension` lifecycle details, see [Plugin Extensions](./extensions.md).
469
+
470
+ ## Security Considerations
471
+
472
+ ### Validate All Inputs
473
+
474
+ User input should be validated and sanitized before use.
475
+
476
+ The following example demonstrates how to validate file paths and extensions to prevent directory traversal attacks and restrict file types:
477
+
478
+ <!-- eslint-skip -->
479
+
480
+ ```ts
481
+ extension: () => ({
482
+ readFile: async (path: string) => {
483
+ // Prevent path traversal
484
+ if (path.includes('..') || path.startsWith('/')) {
485
+ throw new Error('Invalid file path')
486
+ }
487
+
488
+ // Validate file extension
489
+ const allowed = ['.json', '.yaml', '.yml']
490
+ if (!allowed.some(ext => path.endsWith(ext))) {
491
+ throw new Error('Unsupported file type')
492
+ }
493
+
494
+ return await fs.readFile(path, 'utf8')
495
+ }
496
+ })
497
+ ```
498
+
499
+ ### Protect Sensitive Data
500
+
501
+ Avoid exposing sensitive information in logs or error messages.
502
+
503
+ This example shows how to handle API keys securely in a plugin, validating them without logging sensitive data:
504
+
505
+ ```ts
506
+ export default function auth(apiKey: string) {
507
+ // Validate but don't log the key
508
+ if (!apiKey || apiKey.length < 32) {
509
+ throw new Error('Invalid API key format')
510
+ }
511
+
512
+ return plugin({
513
+ id: 'auth',
514
+ extension: () => ({
515
+ request: async (url: string) => {
516
+ try {
517
+ return await fetch(url, {
518
+ headers: { Authorization: `Bearer ${apiKey}` }
519
+ })
520
+ } catch (error) {
521
+ // Don't include the API key in errors
522
+ throw new Error(`Request failed: ${error.message}`)
523
+ }
524
+ }
525
+ })
526
+ })
527
+ }
528
+ ```
529
+
530
+ ### Prevent Prototype Pollution
531
+
532
+ Prototype pollution occurs when user-controlled data modifies `Object.prototype`, potentially injecting properties that affect all objects in your application.
533
+
534
+ This vulnerability is particularly dangerous in CLI tools that process configuration files or user-provided options, as attackers can manipulate command behavior through crafted inputs.
535
+
536
+ Use `Object.create(null)` to create objects without a prototype chain when handling user input:
537
+
538
+ <!-- eslint-skip -->
539
+
540
+ ```ts
541
+ extension: () => {
542
+ // Vulnerable: Regular object inherits from Object.prototype
543
+ const userOptions = {} // Can be polluted via __proto__ or constructor
544
+
545
+ // Safe: Object without prototype chain
546
+ const safeOptions = Object.create(null)
547
+
548
+ return {
549
+ parseConfig: config => {
550
+ // Safe storage for user-provided data
551
+ const settings = Object.create(null)
552
+
553
+ // Safely merge user config with defaults
554
+ for (const key in config) {
555
+ // Only copy own properties, not inherited ones
556
+ if (Object.prototype.hasOwnProperty.call(config, key)) {
557
+ // Prevent __proto__ and constructor pollution
558
+ if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
559
+ continue
560
+ }
561
+ settings[key] = config[key]
562
+ }
563
+ }
564
+
565
+ return settings
566
+ },
567
+
568
+ // Safe command registry without prototype chain
569
+ createCommandMap: commands => {
570
+ const commandMap = Object.create(null)
571
+
572
+ for (const cmd of commands) {
573
+ commandMap[cmd] = true
574
+ }
575
+
576
+ // Safe to check user input against this map
577
+ return commandMap
578
+ }
579
+ }
580
+ }
581
+ ```
582
+
583
+ Use `Object.create(null)` specifically when:
584
+
585
+ - Storing user-provided configuration or options
586
+ - Creating lookup maps from external input
587
+ - Building registries from dynamic data
588
+ - Merging multiple configuration sources
589
+
590
+ Regular object literals are safe for:
591
+
592
+ - Internal plugin state
593
+ - Hardcoded configurations
594
+ - Type-checked interfaces
595
+
596
+ ## Testing Strategies
597
+
598
+ Focus testing on your plugin's extension factory and how it interacts with the command context.
599
+
600
+ This ensures your plugin properly integrates with Gunshi's lifecycle and handles command metadata correctly.
601
+
602
+ The following example demonstrates how to test your plugin's extension factory and its interaction with the command context:
603
+
604
+ ```ts
605
+ import { describe, test, expect, vi } from 'vitest'
606
+ import myPlugin from './plugin.ts'
607
+
608
+ describe('Plugin Extension', () => {
609
+ test('extension factory creates correct methods', async () => {
610
+ const plugin = myPlugin({ debug: true })
611
+
612
+ // Mock a command context to simulate Gunshi's runtime environment
613
+ const mockContext = {
614
+ name: 'test-command',
615
+ values: { verbose: true },
616
+ log: vi.fn(),
617
+ extensions: {}
618
+ }
619
+
620
+ const mockCommand = { name: 'test', run: vi.fn() }
621
+
622
+ // Verify the extension factory returns all required plugin methods
623
+ const extension = await plugin.extension(mockContext, mockCommand)
624
+ expect(extension.process).toBeDefined()
625
+ expect(typeof extension.process).toBe('function')
626
+
627
+ // Verify the plugin correctly uses context.log when debug is enabled
628
+ extension.process('data')
629
+ expect(mockContext.log).toHaveBeenCalledWith('[DEBUG]', 'data')
630
+ })
631
+ })
632
+ ```
633
+
634
+ For test helpers, lifecycle testing, and integration testing strategies, see [Plugin Testing](./testing.md).
635
+
636
+ ## Documentation
637
+
638
+ Comprehensive documentation is crucial for plugin adoption and maintenance.
639
+
640
+ This section provides guidelines and real examples from official Gunshi plugins to help you create effective documentation.
641
+
642
+ ### Module-Level Documentation
643
+
644
+ Start your main plugin file with module-level JSDoc that explains the plugin's purpose and provides a complete usage example.
645
+
646
+ The following example from `@gunshi/plugin-global` demonstrates this approach:
647
+
648
+ ````ts
649
+ /**
650
+ * The entry point of global options plugin
651
+ *
652
+ * @example
653
+ * ```js
654
+ * import global from '@gunshi/plugin-global'
655
+ * import { cli } from 'gunshi'
656
+ *
657
+ * const entry = (ctx) => {
658
+ * // ...
659
+ * }
660
+ *
661
+ * await cli(process.argv.slice(2), entry, {
662
+ * // ...
663
+ *
664
+ * plugins: [
665
+ * global()
666
+ * ],
667
+ *
668
+ * // ...
669
+ * })
670
+ * ```
671
+ *
672
+ * @module
673
+ */
674
+ ````
675
+
676
+ ### Factory Function Documentation
677
+
678
+ Document your factory function comprehensively, including all parameters and return types. Here's an example from `@gunshi/plugin-i18n`:
679
+
680
+ ```ts
681
+ /**
682
+ * i18n plugin
683
+ *
684
+ * @param options - I18n plugin options
685
+ * @returns A defined plugin as i18n
686
+ */
687
+ export default function i18n(
688
+ options: I18nPluginOptions = {}
689
+ ): PluginWithExtension<I18nExtension<DefaultGunshiParams>> {
690
+ // Implementation
691
+ }
692
+ ```
693
+
694
+ For plugins with required parameters, include validation guidance:
695
+
696
+ ```ts
697
+ /**
698
+ * completion plugin
699
+ *
700
+ * @param options - Completion options
701
+ * @returns A defined plugin as completion
702
+ */
703
+ export default function completion(options: CompletionOptions = {}): PluginWithoutExtension {
704
+ const config = options.config || {}
705
+ // Validate and use options
706
+ }
707
+ ```
708
+
709
+ ### Extension Interface Documentation
710
+
711
+ Document all methods and properties exposed through your plugin's extension.
712
+
713
+ Here's a concise example:
714
+
715
+ <!-- eslint-skip -->
716
+
717
+ ```ts
718
+ /**
719
+ * Extended command context utilities available via `CommandContext.extensions['g:i18n']`.
720
+ */
721
+ export interface I18nExtension<G extends GunshiParams<any> = DefaultGunshiParams> {
722
+ /** Command locale */
723
+ locale: Intl.Locale
724
+
725
+ /** Translate a message with optional interpolation */
726
+ translate: <K>(key: K, values?: Record<string, unknown>) => string
727
+
728
+ /** Load command resources for the specified locale */
729
+ loadResource: (
730
+ locale: string | Intl.Locale,
731
+ ctx: CommandContext,
732
+ command: Command
733
+ ) => Promise<boolean>
734
+
735
+ // Additional methods follow similar documentation patterns
736
+ }
737
+ ```
738
+
739
+ For complete examples, see the official plugins' source code.
740
+
741
+ ### Configuration Options Documentation
742
+
743
+ Document all configuration options with their types, defaults, and purpose:
744
+
745
+ ```ts
746
+ /**
747
+ * i18n plugin options
748
+ */
749
+ export interface I18nPluginOptions {
750
+ /** Locale to use for translations */
751
+ locale?: string | Intl.Locale
752
+
753
+ /** Translation adapter factory */
754
+ translationAdapterFactory?: TranslationAdapterFactory
755
+
756
+ /** Built-in localizable resources */
757
+ builtinResources?: Record<string, Record<BuiltinResourceKeys, string>>
758
+ }
759
+ ```
760
+
761
+ Nested interfaces follow the same documentation pattern with JSDoc comments for each property.
762
+
763
+ ### Type Documentation Guidelines
764
+
765
+ Export all public types with clear documentation:
766
+
767
+ ```ts
768
+ /** The unique identifier for the i18n plugin */
769
+ export const pluginId = namespacedId('i18n')
770
+ export type PluginId = typeof pluginId
771
+
772
+ /** Command resource type with dynamic argument keys */
773
+ export type CommandResource<G extends GunshiParamsConstraint = DefaultGunshiParams> = {
774
+ description: string
775
+ // Dynamic properties based on command arguments
776
+ } & { [key: string]: string }
777
+
778
+ /** Async function to fetch command resources */
779
+ export type CommandResourceFetcher<G extends GunshiParamsConstraint> = (
780
+ ctx: Readonly<CommandContext<G>>
781
+ ) => Awaitable<CommandResource<G>>
782
+ ```
783
+
784
+ For more complex generic types, see official plugin implementations.
785
+
786
+ ### Plugin Dependencies Documentation
787
+
788
+ Document plugin dependencies clearly:
789
+
790
+ <!-- eslint-skip -->
791
+
792
+ ```ts
793
+ return plugin<...>({
794
+ id: pluginId,
795
+ name: 'completion',
796
+ dependencies: [{ id: namespacedId('i18n'), optional: true }] as const
797
+ // ...
798
+ })
799
+ ```
800
+
801
+ In your README, explain:
802
+
803
+ - Dependency ID and whether it's optional/required
804
+ - Purpose and effect when present
805
+ - Any automatic behaviors or integration points
806
+
807
+ ### README Template Structure
808
+
809
+ <!-- eslint-disable markdown/no-missing-label-refs -->
810
+
811
+ > [!NOTE]
812
+ > Gunshi plans to provide official tooling for plugin authors to automatically generate README templates in future releases. This tooling will help scaffold standard README files with the correct structure, sections, and formatting. Until then, the following template demonstrates the recommended structure for plugin README files.
813
+
814
+ <!-- eslint-enable markdown/no-missing-label-refs -->
815
+
816
+ A comprehensive README should follow this structure:
817
+
818
+ ```md [README.md]
819
+ # @yourorg/gunshi-plugin-{name}
820
+
821
+ > Brief description of what your plugin does.
822
+
823
+ ## Installation
824
+
825
+ \`\`\`sh
826
+
827
+ ### npm
828
+
829
+ npm install --save @yourorg/gunshi-plugin-{name}
830
+ \`\`\`
831
+
832
+ For other package managers, see [installation guide](./docs/install.md).
833
+
834
+ ## Usage
835
+
836
+ <!-- eslint-disable markdown/no-missing-label-refs, markdown/no-space-in-emphasis -->
837
+
838
+ \`\`\`ts
839
+ import { cli } from 'gunshi'
840
+ import myPlugin from '@yourorg/gunshi-plugin-{name}'
841
+
842
+ const command = {
843
+ name: 'example',
844
+ run: ctx => {
845
+ ctx.extensions['yourorg:{name}'].someMethod()
846
+ }
847
+ }
848
+
849
+ await cli(process.argv.slice(2), command, {
850
+ plugins: [myPlugin({ /* options */ })]
851
+ })
852
+ \`\`\`
853
+
854
+ <!-- eslint-enable markdown/no-missing-label-refs, markdown/no-space-in-emphasis -->
855
+
856
+ ## Plugin Options
857
+
858
+ See [API documentation](./docs/api.md) for complete options.
859
+
860
+ ## Examples
861
+
862
+ See the [examples directory](./examples) for usage examples.
863
+
864
+ ## License
865
+
866
+ [MIT](http://opensource.org/licenses/MIT)
867
+ ```
868
+
869
+ ### API Documentation Generation
870
+
871
+ Generate API documentation directly from your JSDoc comments to maintain a single source of truth.
872
+
873
+ This approach ensures your documentation stays synchronized with your code, as updates to JSDoc comments automatically reflect in generated documentation.
874
+
875
+ Various tools can generate documentation from JSDoc comments:
876
+
877
+ - **[TypeDoc](https://typedoc.org/)** - Recommended for TypeScript projects, generates documentation from TypeScript declarations and JSDoc
878
+ - **[API Extractor](https://api-extractor.com/)** - Microsoft's tool focusing on API review and documentation for TypeScript libraries
879
+
880
+ The following example demonstrates configuring `TypeDoc`, a popular choice for TypeScript plugin projects. Create a `typedoc.config.mjs` file:
881
+
882
+ ```js [typedoc.config.mjs]
883
+ // @ts-check
884
+
885
+ export default {
886
+ /**
887
+ * typedoc options
888
+ * ref: https://typedoc.org/documents/Options.html
889
+ */
890
+ entryPoints: ['./src/index.ts'],
891
+ out: 'docs',
892
+ plugin: ['typedoc-plugin-markdown'],
893
+ readme: 'none',
894
+ groupOrder: ['Variables', 'Functions', 'Classes', 'Interfaces', 'Type Aliases'],
895
+
896
+ /**
897
+ * typedoc-plugin-markdown options
898
+ * ref: https://typedoc-plugin-markdown.org/docs/options
899
+ */
900
+ entryFileName: 'index',
901
+ hidePageTitle: false,
902
+ useCodeBlocks: true,
903
+ disableSources: true,
904
+ indexFormat: 'table',
905
+ parametersFormat: 'table',
906
+ interfacePropertiesFormat: 'table',
907
+ classPropertiesFormat: 'table',
908
+ propertyMembersFormat: 'table',
909
+ typeAliasPropertiesFormat: 'table',
910
+ enumMembersFormat: 'table'
911
+ }
912
+ ```
913
+
914
+ Add documentation scripts to your `package.json`:
915
+
916
+ ```json [package.json]
917
+ {
918
+ "scripts": {
919
+ "docs": "typedoc",
920
+ "docs:watch": "typedoc --watch",
921
+ "docs:clean": "rm -rf docs"
922
+ },
923
+ "devDependencies": {
924
+ "typedoc": "^0.26.0",
925
+ "typedoc-plugin-markdown": "^4.0.0"
926
+ }
927
+ }
928
+ ```
929
+
930
+ This configuration extracts documentation from your JSDoc comments and TypeScript types, generating comprehensive API documentation without manual maintenance.
931
+
932
+ The generated documentation includes all exported functions, interfaces, types, and their associated JSDoc descriptions, ensuring consistency between code and documentation.
933
+
934
+ ## Next Steps
935
+
936
+ Following these guidelines ensures your plugins are production-ready, maintainable, and provide excellent developer experience. You've learned naming conventions, error handling patterns, performance considerations, and documentation strategies.
937
+
938
+ Now that you understand how to build high-quality plugins, explore the existing ecosystem to see these principles in action and find plugins that can enhance your CLI.
939
+
940
+ The next chapter on [Plugin List](./list.md) showcases official plugins maintained by the Gunshi team and community contributions.