@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.
- package/LICENSE +20 -0
- package/package.json +52 -0
- package/src/guide/advanced/advanced-lazy-loading.md +312 -0
- package/src/guide/advanced/command-hooks.md +469 -0
- package/src/guide/advanced/context-extensions.md +545 -0
- package/src/guide/advanced/custom-rendering.md +945 -0
- package/src/guide/advanced/docs-gen.md +594 -0
- package/src/guide/advanced/internationalization.md +677 -0
- package/src/guide/advanced/type-system.md +561 -0
- package/src/guide/essentials/auto-usage.md +281 -0
- package/src/guide/essentials/composable.md +332 -0
- package/src/guide/essentials/declarative.md +724 -0
- package/src/guide/essentials/getting-started.md +252 -0
- package/src/guide/essentials/lazy-async.md +408 -0
- package/src/guide/essentials/plugin-system.md +472 -0
- package/src/guide/essentials/type-safe.md +154 -0
- package/src/guide/introduction/setup.md +68 -0
- package/src/guide/introduction/what-is-gunshi.md +68 -0
- package/src/guide/plugin/decorators.md +545 -0
- package/src/guide/plugin/dependencies.md +519 -0
- package/src/guide/plugin/extensions.md +317 -0
- package/src/guide/plugin/getting-started.md +298 -0
- package/src/guide/plugin/guidelines.md +940 -0
- package/src/guide/plugin/introduction.md +294 -0
- package/src/guide/plugin/lifecycle.md +432 -0
- package/src/guide/plugin/list.md +37 -0
- package/src/guide/plugin/testing.md +843 -0
- package/src/guide/plugin/type-system.md +529 -0
- package/src/index.md +44 -0
- package/src/release/v0.27.md +722 -0
- package/src/showcase.md +11 -0
|
@@ -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.
|