@gunshi/plugin 0.27.0-beta.2 â 0.27.0-beta.3
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/README.md +1 -1017
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -16,1023 +16,7 @@ This package provides comprehensive APIs and TypeScript type definitions for cre
|
|
|
16
16
|
|
|
17
17
|
<!-- eslint-enable markdown/no-missing-label-refs -->
|
|
18
18
|
|
|
19
|
-
|
|
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
|
-
|
|
118
|
-
## đŋ Installation
|
|
119
|
-
|
|
120
|
-
```sh
|
|
121
|
-
# npm
|
|
122
|
-
npm install --save @gunshi/plugin
|
|
123
|
-
|
|
124
|
-
# pnpm
|
|
125
|
-
pnpm add @gunshi/plugin
|
|
126
|
-
|
|
127
|
-
# yarn
|
|
128
|
-
yarn add @gunshi/plugin
|
|
129
|
-
|
|
130
|
-
# deno
|
|
131
|
-
deno add jsr:@gunshi/plugin
|
|
132
|
-
|
|
133
|
-
# bun
|
|
134
|
-
bun add @gunshi/plugin
|
|
135
|
-
```
|
|
136
|
-
|
|
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}`)
|
|
797
|
-
|
|
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
|
-
}
|
|
809
|
-
```
|
|
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
|
|
19
|
+
About details, See [the plugin development guides](https://gunshi.dev/guide/plugin/introduction) of official docs.
|
|
1036
20
|
|
|
1037
21
|
## ÂŠī¸ License
|
|
1038
22
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gunshi/plugin",
|
|
3
3
|
"description": "plugin development kit for gunshi",
|
|
4
|
-
"version": "0.27.0-beta.
|
|
4
|
+
"version": "0.27.0-beta.3",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "kazuya kawaguchi",
|
|
7
7
|
"email": "kawakazu80@gmail.com"
|
|
@@ -55,8 +55,8 @@
|
|
|
55
55
|
"jsr": "^0.13.5",
|
|
56
56
|
"jsr-exports-lint": "^0.4.1",
|
|
57
57
|
"publint": "^0.3.14",
|
|
58
|
-
"tsdown": "
|
|
59
|
-
"gunshi": "0.27.0-beta.
|
|
58
|
+
"tsdown": "0.15.9",
|
|
59
|
+
"gunshi": "0.27.0-beta.3"
|
|
60
60
|
},
|
|
61
61
|
"scripts": {
|
|
62
62
|
"build": "tsdown",
|