@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,317 @@
1
+ # Plugin Extensions
2
+
3
+ Extensions are the primary mechanism for plugins to add functionality to the command context.
4
+
5
+ This guide explains how to create and use plugin extensions for type-safe inter-plugin communication.
6
+
7
+ ## What are Extensions?
8
+
9
+ Extensions allow plugins to inject custom functionality into the `CommandContext` that becomes available to all commands and other plugins.
10
+
11
+ Each extension is namespaced by the plugin ID to prevent conflicts.
12
+
13
+ Here's a simple example showing the concept:
14
+
15
+ ```js
16
+ import { plugin } from 'gunshi/plugin'
17
+ import { define } from 'gunshi'
18
+
19
+ // Plugin provides an extension
20
+ const loggerPlugin = plugin({
21
+ id: 'logger',
22
+ extension: () => ({
23
+ log: msg => console.log(msg),
24
+ error: msg => console.error(msg)
25
+ })
26
+ })
27
+
28
+ // Commands can use the extension
29
+ const command = define({
30
+ name: 'deploy',
31
+ run: ctx => {
32
+ // Access extension via plugin ID
33
+ ctx.extensions.logger.log('Starting deployment...')
34
+ }
35
+ })
36
+ ```
37
+
38
+ Extensions enable:
39
+
40
+ - **Shared Functionality**: Provide common utilities (logging, caching, database access)
41
+ - **Plugin Composition**: Build plugins that work together
42
+ - **Type Safety**: Define clear contracts between plugins
43
+ - **State Management**: Maintain state across command execution
44
+
45
+ <!-- eslint-disable markdown/no-missing-label-refs -->
46
+
47
+ > [!NOTE]
48
+ > For advanced type safety patterns and type-safe plugin communication, see the [Plugin Type System](./type-system.md) guide.
49
+
50
+ <!-- eslint-enable markdown/no-missing-label-refs -->
51
+
52
+ ## Extension Lifecycle
53
+
54
+ Understanding when and how extensions are created is crucial for effective plugin development.
55
+
56
+ ### Lifecycle Phases
57
+
58
+ <!-- eslint-disable markdown/no-missing-label-refs -->
59
+
60
+ > [!NOTE]
61
+ > The steps mentioned below (H, I) refer to the complete CLI execution lifecycle documented in the [Plugin Lifecycle](./lifecycle.md) guide. Extensions are involved in the Execution Phase, which occurs after plugins are loaded and configured during the Setup Phase.
62
+
63
+ <!-- eslint-enable markdown/no-missing-label-refs -->
64
+
65
+ During command execution, extensions go through three distinct phases:
66
+
67
+ 1. **Extension Creation (Step H)**: The `extension` factory function is called to create each plugin's extension
68
+ 2. **Post-Extension Hook (Step H)**: The `onExtension` callback is executed after all extensions are created
69
+ 3. **Command Execution (Step I)**: The actual command runs with all extensions available
70
+
71
+ The following diagram illustrates the relationship between `extension` and `onExtension`:
72
+
73
+ <h5 style="text-align: center; padding: 1em; margin: 1em">Plugin Processing (in dependency order)</h5>
74
+
75
+ ```mermaid
76
+ graph TD
77
+ subgraph " "
78
+ A1[Plugin A: extension factory] --> A2[Attach to ctx.extensions.A]
79
+ A2 --> A3[Plugin A: onExtension]
80
+ A3 --> B1[Plugin B: extension factory]
81
+ B1 --> B2[Attach to ctx.extensions.B]
82
+ B2 --> B3[Plugin B: onExtension]
83
+ B3 --> C1[Plugin C: extension factory]
84
+ C1 --> C2[Attach to ctx.extensions.C]
85
+ C2 --> C3[Plugin C: onExtension]
86
+ end
87
+
88
+ C3 --> CMD[Command Execution]
89
+
90
+ style A1 fill:#468c56,color:white
91
+ style B1 fill:#468c56,color:white
92
+ style C1 fill:#468c56,color:white
93
+ style A3 fill:#9B59B6,stroke:#633974,stroke-width:2px,color:#fff
94
+ style B3 fill:#9B59B6,stroke:#633974,stroke-width:2px,color:#fff
95
+ style C3 fill:#9B59B6,stroke:#633974,stroke-width:2px,color:#fff
96
+ style CMD fill:#3498db,color:white
97
+ ```
98
+
99
+ ### Execution Order Guarantees
100
+
101
+ Gunshi processes plugins sequentially in dependency order, ensuring dependencies are always resolved before dependents:
102
+
103
+ 1. **Plugins are sorted by dependencies** - Dependencies are processed before plugins that depend on them
104
+ 2. **For each plugin in order**:
105
+ a. The plugin's `extension` factory is called
106
+ b. The extension result is immediately attached to `ctx.extensions[pluginId]`
107
+ c. The plugin's `onExtension` callback runs (if defined) with access to its own extension and all dependency extensions
108
+ 3. **Command execution** - The command executes only after all plugins have been processed
109
+
110
+ This ensures that:
111
+
112
+ - When a plugin's `onExtension` runs, it has access to:
113
+ - Its own extension (just created)
114
+ - All dependency extensions (already processed)
115
+ - Dependencies are always initialized before dependents
116
+ - The execution order respects the dependency graph
117
+ - Commands have a fully initialized context with all extensions
118
+
119
+ <!-- eslint-disable markdown/no-missing-label-refs -->
120
+
121
+ > [!IMPORTANT]
122
+ > A plugin's `onExtension` callback does NOT have access to extensions from plugins that depend on it, as those are processed later in the sequence.
123
+
124
+ <!-- eslint-enable markdown/no-missing-label-refs -->
125
+
126
+ ## Creating Extensions
127
+
128
+ <!-- eslint-disable markdown/no-missing-label-refs -->
129
+
130
+ > [!TIP]
131
+ > **It's strongly recommended to define your extension interfaces using TypeScript.** This benefits all users: end users get IDE autocompletion and compile-time error detection when using your extension, plugin users receive type safety guarantees, and other plugin developers can build on top of your plugin with confidence.
132
+
133
+ <!-- eslint-enable markdown/no-missing-label-refs -->
134
+
135
+ ### The `extension` Factory
136
+
137
+ The `extension` factory function is called during Step H to create an extension object that becomes available through `ctx.extensions`. This is where you can:
138
+
139
+ - Create fresh instances for each command execution
140
+ - Access parsed arguments and command information
141
+ - Access extensions from dependent plugins
142
+ - Initialize resources synchronously or asynchronously
143
+ - Return methods and properties for other plugins and commands to use
144
+
145
+ ```ts [metrics.ts]
146
+ import { plugin } from 'gunshi/plugin'
147
+
148
+ export default plugin({
149
+ id: 'metrics',
150
+
151
+ extension: (ctx, cmd) => {
152
+ // Called during Step H: Create Extensions
153
+ // Fresh instance for each command execution
154
+
155
+ const startTime = Date.now()
156
+ const commandName = cmd.name || 'root'
157
+
158
+ // Access parsed command-line arguments
159
+ const verbose = ctx.values.verbose === true
160
+
161
+ // Return the extension object (can be sync or async)
162
+ return {
163
+ recordMetric: (name: string, value: number) => {
164
+ if (verbose) {
165
+ console.log(`[${commandName}] ${name}: ${value}`)
166
+ }
167
+ },
168
+ getElapsedTime: () => Date.now() - startTime
169
+ }
170
+ }
171
+ })
172
+ ```
173
+
174
+ ### The `onExtension` Hook
175
+
176
+ The `onExtension` callback runs after all extensions are created and attached to the context. This is where you can:
177
+
178
+ - Access your own extension via `ctx.extensions`
179
+ - Interact with other plugin extensions
180
+ - Perform initialization that depends on the complete context
181
+ - Set up resources needed before command execution
182
+
183
+ ```ts [database.ts]
184
+ import { plugin } from 'gunshi/plugin'
185
+
186
+ export default plugin({
187
+ id: 'database',
188
+ dependencies: [
189
+ { id: 'logger', optional: true } // Optional dependency
190
+ ],
191
+
192
+ extension: () => {
193
+ // Create the extension object
194
+ const pool = createPool()
195
+
196
+ return {
197
+ query: (sql: string) => pool.query(sql),
198
+ connect: () => pool.connect(),
199
+ disconnect: () => pool.disconnect()
200
+ }
201
+ },
202
+
203
+ onExtension: async (ctx, cmd) => {
204
+ // Called during Step H: Execute onExtension
205
+ // All extensions are now available
206
+
207
+ // Access your own extension
208
+ const db = ctx.extensions.database
209
+
210
+ // Interact with other extensions if available
211
+ // Note: ctx.extensions.logger is only available here if the logger plugin
212
+ // was processed before this plugin (as a dependency or earlier in registration)
213
+ if (ctx.extensions.logger) {
214
+ ctx.extensions.logger.log('Database plugin initialized')
215
+ }
216
+
217
+ // Perform command-specific initialization
218
+ if (cmd.name === 'migrate' || cmd.name === 'seed') {
219
+ await db.connect()
220
+
221
+ if (ctx.extensions.logger) {
222
+ ctx.extensions.logger.log('Database connected')
223
+ }
224
+ }
225
+ }
226
+ })
227
+ ```
228
+
229
+ ### Basic Extension
230
+
231
+ Start with a simple extension that provides basic functionality:
232
+
233
+ ```ts [logger.ts]
234
+ import { plugin } from 'gunshi/plugin'
235
+
236
+ // Define the extension interface
237
+ export interface LoggerExtension {
238
+ log: (message: string) => void
239
+ error: (message: string) => void
240
+ warn: (message: string) => void
241
+ debug: (message: string) => void
242
+ }
243
+
244
+ // Export for other plugins to use
245
+ export const pluginId = 'logger' as const
246
+ export type PluginId = typeof pluginId
247
+
248
+ // Implement the extension
249
+ export default plugin({
250
+ id: pluginId,
251
+ extension: (): LoggerExtension => ({
252
+ log: msg => console.log(msg),
253
+ error: msg => console.error(msg),
254
+ warn: msg => console.warn(msg),
255
+ debug: msg => console.debug(msg)
256
+ })
257
+ })
258
+ ```
259
+
260
+ ### Using Command Context and Parameters
261
+
262
+ Extensions can access the `ctx` and `cmd` parameters to adapt their behavior based on command-line arguments and command configuration:
263
+
264
+ ```ts [cache.ts]
265
+ import { plugin } from 'gunshi/plugin'
266
+
267
+ export interface CacheExtension {
268
+ get: <T>(key: string) => T | undefined
269
+ set: <T>(key: string, value: T) => void
270
+ clear: () => void
271
+ size: () => number
272
+ }
273
+
274
+ export default plugin({
275
+ id: 'cache',
276
+
277
+ extension: (ctx, cmd) => {
278
+ // Create command-specific cache
279
+ const cache = new Map<string, unknown>()
280
+ const commandName = cmd.name || 'global'
281
+ const debug = ctx.values.debug === true
282
+
283
+ return {
284
+ get: <T>(key: string): T | undefined => {
285
+ const value = cache.get(key) as T | undefined
286
+ if (debug && value !== undefined) {
287
+ console.log(`[${commandName}] Cache hit: ${key}`)
288
+ }
289
+ return value
290
+ },
291
+
292
+ set: <T>(key: string, value: T) => {
293
+ cache.set(key, value)
294
+ if (debug) {
295
+ console.log(`[${commandName}] Cache set: ${key}`)
296
+ }
297
+ },
298
+
299
+ clear: () => cache.clear(),
300
+ size: () => cache.size
301
+ }
302
+ }
303
+ })
304
+ ```
305
+
306
+ <!-- eslint-disable markdown/no-missing-label-refs -->
307
+
308
+ > [!TIP]
309
+ > The example fully code is [here](https://github.com/kazupon/gunshi/tree/main/playground/plugins/extensions).
310
+
311
+ <!-- eslint-enable markdown/no-missing-label-refs -->
312
+
313
+ ## Next Steps
314
+
315
+ You've mastered extensions—the powerful mechanism for sharing functionality between plugins and commands. With extensions, your plugins can provide APIs, services, and utilities that enhance the entire CLI ecosystem.
316
+
317
+ Now it's time to ensure your plugins are type-safe. The next chapter, [Plugin Type System](./type-system.md), will show you how to leverage TypeScript's type system to create plugins with compile-time safety and excellent IDE support.
@@ -0,0 +1,298 @@
1
+ # Getting Started with Plugin Development
2
+
3
+ This guide will walk you through creating your first Gunshi plugin, from the simplest possible plugin to more advanced patterns with extensions and decorators.
4
+
5
+ ## Your First Minimal Plugin
6
+
7
+ Let's start with the absolute minimum (no extension) - a plugin that simply logs when it's loaded:
8
+
9
+ ```js [plugin.js]
10
+ import { plugin } from 'gunshi/plugin'
11
+
12
+ // The simplest possible plugin
13
+ export default plugin({
14
+ id: 'hello',
15
+ name: 'Hello Plugin',
16
+ setup: ctx => {
17
+ console.log('Hello from plugin!')
18
+ }
19
+ })
20
+ ```
21
+
22
+ Use it in your CLI:
23
+
24
+ ```js [cli.js]
25
+ import { cli } from 'gunshi'
26
+ import hello from './plugin.js'
27
+
28
+ const entry = () => {}
29
+
30
+ await cli(process.argv.slice(2), entry, {
31
+ plugins: [hello]
32
+ })
33
+ ```
34
+
35
+ <!-- eslint-disable markdown/no-missing-label-refs -->
36
+
37
+ > [!TIP]
38
+ > The example fully code is [here](https://github.com/kazupon/gunshi/tree/main/playground/plugins/getting-started/first-minimal).
39
+
40
+ <!-- eslint-enable markdown/no-missing-label-refs -->
41
+
42
+ Run your application with plugin:
43
+
44
+ ```sh
45
+ # Run the entry with plugin
46
+ node cli.js
47
+
48
+ Hello from plugin!
49
+ ```
50
+
51
+ This plugin:
52
+
53
+ - Has a unique `id` for identification
54
+ - Has a human-readable `name`
55
+ - Runs its `setup` function during plugin initialization
56
+ - Doesn't extend the command context
57
+
58
+ ## Adding Global Options
59
+
60
+ Let's create a plugin that adds a global `--debug` option to all commands:
61
+
62
+ ```js [plugin.js]
63
+ import { plugin } from 'gunshi/plugin'
64
+
65
+ export default plugin({
66
+ id: 'debug',
67
+ name: 'Debug Plugin',
68
+
69
+ setup: ctx => {
70
+ // Add a global option available to all commands
71
+ ctx.addGlobalOption('debug', {
72
+ type: 'boolean',
73
+ short: 'd',
74
+ description: 'Enable debug output'
75
+ })
76
+ }
77
+ })
78
+ ```
79
+
80
+ Now all commands have access to `--debug`:
81
+
82
+ ```js [cli.js]
83
+ import { cli, define } from 'gunshi'
84
+ import debug from './plugin.js'
85
+
86
+ const command = define({
87
+ name: 'build',
88
+ run: ctx => {
89
+ if (ctx.values.debug) {
90
+ console.log('Debug mode enabled')
91
+ console.log('Context:', ctx)
92
+ }
93
+ console.log('Building...')
94
+ }
95
+ })
96
+
97
+ await cli(process.argv.slice(2), command, {
98
+ plugins: [debug]
99
+ })
100
+ ```
101
+
102
+ <!-- eslint-disable markdown/no-missing-label-refs -->
103
+
104
+ > [!TIP]
105
+ > The example fully code is [here](https://github.com/kazupon/gunshi/tree/main/playground/plugins/getting-started/adding-global).
106
+
107
+ <!-- eslint-enable markdown/no-missing-label-refs -->
108
+
109
+ Run your application with plugin:
110
+
111
+ ```sh
112
+ # Run command with debug option
113
+ node cli.js --debug
114
+
115
+ Debug mode enabled
116
+ Context: ...
117
+ ...
118
+ Building ...
119
+ ```
120
+
121
+ ## Adding Sub-Commands
122
+
123
+ Plugins can register sub-commands that become available to the CLI:
124
+
125
+ ```js [plugin.js]
126
+ import { plugin } from 'gunshi/plugin'
127
+
128
+ export default plugin({
129
+ id: 'tools',
130
+ name: 'Developer Tools Plugin',
131
+
132
+ setup: ctx => {
133
+ // Add a new sub-command
134
+ ctx.addCommand('clean', {
135
+ name: 'clean',
136
+ description: 'Clean build artifacts',
137
+ args: {
138
+ cache: {
139
+ type: 'boolean',
140
+ description: 'Also clear cache',
141
+ default: false
142
+ }
143
+ },
144
+ run: ctx => {
145
+ console.log('Cleaning build artifacts...')
146
+ if (ctx.values.cache) {
147
+ console.log('Clearing cache...')
148
+ }
149
+ console.log('Clean complete!')
150
+ }
151
+ })
152
+
153
+ // Add another sub-command
154
+ ctx.addCommand('lint', {
155
+ name: 'lint',
156
+ description: 'Run linter',
157
+ run: ctx => {
158
+ console.log('Running linter...')
159
+ console.log('No issues found!')
160
+ }
161
+ })
162
+ }
163
+ })
164
+ ```
165
+
166
+ Now your CLI has additional commands:
167
+
168
+ ```js [cli.js]
169
+ import { cli, define } from 'gunshi'
170
+ import tools from './plugin.js'
171
+
172
+ // Main command
173
+ const command = define({
174
+ name: 'build',
175
+ run: ctx => console.log('Building project...')
176
+ })
177
+
178
+ await cli(process.argv.slice(2), command, {
179
+ plugins: [tools]
180
+ })
181
+ ```
182
+
183
+ <!-- eslint-disable markdown/no-missing-label-refs -->
184
+
185
+ > [!TIP]
186
+ > The example fully code is [here](https://github.com/kazupon/gunshi/tree/main/playground/plugins/getting-started/adding-sub-commands).
187
+
188
+ <!-- eslint-enable markdown/no-missing-label-refs -->
189
+
190
+ Run your application with the new sub-commands:
191
+
192
+ ```sh
193
+ # Run main command
194
+ node cli.js
195
+ Building project...
196
+
197
+ # Run plugin's sub-command
198
+ node cli.js clean
199
+ Cleaning build artifacts...
200
+ Clean complete!
201
+
202
+ # With arguments
203
+ node cli.js clean --cache
204
+ Cleaning build artifacts...
205
+ Clearing cache...
206
+ Clean complete!
207
+
208
+ # Run another sub-command
209
+ node cli.js lint
210
+ Running linter...
211
+ No issues found!
212
+ ```
213
+
214
+ ## Advanced Plugin Features
215
+
216
+ Beyond basic setup and global options, plugins can provide much more powerful functionality:
217
+
218
+ ### Extensions
219
+
220
+ Plugins can extend the command context with new functionality that all commands can use.
221
+
222
+ ```js
223
+ // Simple example - adding logging functionality
224
+ export default plugin({
225
+ id: 'logger',
226
+ extension: () => ({
227
+ log: msg => console.log(msg)
228
+ })
229
+ })
230
+
231
+ // Commands can then use: ctx.extensions.logger.log('Hello')
232
+ ```
233
+
234
+ <!-- eslint-disable markdown/no-missing-label-refs -->
235
+
236
+ > [!TIP]
237
+ > Extensions are the core feature for sharing functionality between plugins and commands. Learn more in [Plugin Extensions](./extensions.md).
238
+
239
+ <!-- eslint-enable markdown/no-missing-label-refs -->
240
+
241
+ ### Decorators
242
+
243
+ Plugins can decorate (wrap) existing functionality to enhance behavior:
244
+
245
+ ```js
246
+ // Customize how help text is displayed
247
+ ctx.decorateUsageRenderer(async (baseRenderer, ctx) => {
248
+ const baseUsage = await baseRenderer(ctx)
249
+ return `${baseUsage}\n\n📚 Documentation: https://example.com/docs`
250
+ })
251
+ ```
252
+
253
+ <!-- eslint-disable markdown/no-missing-label-refs -->
254
+
255
+ > [!TIP]
256
+ > Decorators allow you to wrap commands, renderers, and more. Learn about all decorator types in [Plugin Decorators](./decorators.md).
257
+
258
+ <!-- eslint-enable markdown/no-missing-label-refs -->
259
+
260
+ ### Dependencies
261
+
262
+ Plugins can declare dependencies on other plugins:
263
+
264
+ ```js
265
+ export default plugin({
266
+ id: 'auth',
267
+ dependencies: ['logger'], // Requires logger plugin
268
+ setup: ctx => {
269
+ // Logger plugin is guaranteed to be loaded
270
+ }
271
+ })
272
+ ```
273
+
274
+ <!-- eslint-disable markdown/no-missing-label-refs -->
275
+
276
+ > [!TIP]
277
+ > Dependencies ensure plugins load in the correct order. Learn more in [Plugin Dependencies](./dependencies.md).
278
+
279
+ <!-- eslint-enable markdown/no-missing-label-refs -->
280
+
281
+ ## Summary
282
+
283
+ You've now learned the basics of Gunshi plugin development:
284
+
285
+ - Creating minimal plugins with setup functions
286
+ - Adding global options available to all commands
287
+ - Registering sub-commands through plugins
288
+ - Understanding advanced features (extensions, decorators, dependencies)
289
+
290
+ These fundamentals provide a solid foundation for building more complex plugins.
291
+
292
+ ## Next Steps
293
+
294
+ You've created your first Gunshi plugin and learned the fundamental concepts: setup functions, global options, sub-command registration, and basic plugin features.
295
+
296
+ With these foundations in place, you're ready to understand how plugins integrate with the CLI execution flow.
297
+
298
+ The next chapter, [Plugin Lifecycle](./lifecycle.md), will show you exactly when and how plugins execute during CLI runtime, giving you the knowledge to build more sophisticated plugins that interact with commands at the right moments.