@gunshi/plugin-renderer 0.26.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/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 kazuya kawaguchi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,289 @@
1
+ # @gunshi/plugin-renderer
2
+
3
+ > usage renderer plugin for gunshi.
4
+
5
+ This plugin provides customizable rendering for CLI help messages, usage information, and validation errors. It automatically formats command descriptions, arguments, options, examples, and error messages in a consistent and readable format.
6
+
7
+ ## 💿 Installation
8
+
9
+ ```sh
10
+ # npm
11
+ npm install --save @gunshi/plugin-renderer
12
+
13
+ # pnpm
14
+ pnpm add @gunshi/plugin-renderer
15
+
16
+ # yarn
17
+ yarn add @gunshi/plugin-renderer
18
+
19
+ # deno
20
+ deno add jsr:@gunshi/plugin-renderer
21
+
22
+ # bun
23
+ bun add @gunshi/plugin-renderer
24
+ ```
25
+
26
+ ## 🚀 Usage
27
+
28
+ ```ts
29
+ import renderer from '@gunshi/plugin-renderer'
30
+ import { cli, define } from 'gunshi'
31
+
32
+ const command = define({
33
+ name: 'deploy',
34
+ description: 'Deploy your application',
35
+ args: {
36
+ environment: {
37
+ type: 'string',
38
+ description: 'Target environment',
39
+ required: true
40
+ },
41
+ force: {
42
+ type: 'boolean',
43
+ short: 'f',
44
+ description: 'Force deployment without confirmation'
45
+ }
46
+ },
47
+ examples: '$ deploy production --force',
48
+ run: async ctx => {
49
+ console.log(`Deploying to ${ctx.values.environment}...`)
50
+ }
51
+ })
52
+
53
+ await cli(process.argv.slice(2), command, {
54
+ name: 'deploy-cli',
55
+ version: '1.0.0',
56
+ plugins: [
57
+ renderer() // Adds automatic help/usage rendering
58
+ ]
59
+ })
60
+ ```
61
+
62
+ <!-- eslint-disable markdown/no-missing-label-refs -->
63
+
64
+ > [!TIP]
65
+ > The renderer plugin automatically decorates the header, usage, and validation error renderers. When users run `--help` or encounter validation errors, the plugin displays the information in a clean, readable format.
66
+
67
+ <!-- eslint-enable markdown/no-missing-label-refs -->
68
+
69
+ ## ✨ Features
70
+
71
+ ### Automatic Rendering
72
+
73
+ This plugin automatically handles rendering for:
74
+
75
+ - **Command Headers**: Displays command name and description
76
+ - **Usage Information**: Shows usage syntax, arguments, options, examples, and subcommands
77
+ - **Validation Errors**: Formats validation errors in a user-friendly way
78
+
79
+ ### Internationalization Text Rendering
80
+
81
+ The plugin provides smart text rendering with automatic fallback:
82
+
83
+ - **With i18n plugin**: Uses translations from the i18n plugin
84
+ - **Without i18n plugin**: Falls back to default English messages and descriptions
85
+
86
+ ### Rendered Example
87
+
88
+ When a user runs `--help`, the output looks like:
89
+
90
+ ```sh
91
+ deploy - Deploy your application
92
+
93
+ USAGE
94
+ $ deploy [options] <environment>
95
+
96
+ ARGUMENTS
97
+ environment Target environment
98
+
99
+ OPTIONS
100
+ -f, --force Force deployment without confirmation
101
+ -h, --help Display this help message
102
+
103
+ EXAMPLES
104
+ $ deploy production --force
105
+ ```
106
+
107
+ ### Exported Functions
108
+
109
+ - **`renderHeader(ctx: CommandContext): Promise<string>`**: Renders the command header section
110
+ - **`renderUsage(ctx: CommandContext): Promise<string>`**: Renders the complete usage/help information
111
+ - **`renderValidationErrors(ctx: CommandContext, error: AggregateError): Promise<string>`**: Renders validation errors
112
+
113
+ ## 🧩 Context Extensions
114
+
115
+ When using the renderer plugin, your command context is extended via `ctx.extensions['g:renderer']`.
116
+
117
+ <!-- eslint-disable markdown/no-missing-label-refs -->
118
+
119
+ > [!IMPORTANT]
120
+ > This plugin extension is namespaced in `CommandContext.extensions` using this plugin ID `g:renderer` by the gunshi plugin system.
121
+
122
+ <!-- eslint-enable markdown/no-missing-label-refs -->
123
+
124
+ Available extensions:
125
+
126
+ - **`text<K>(key: K, values?: Record<string, unknown>): Promise<string>`**: Render text with optional i18n support. Handles built-in keys, argument descriptions, and custom keys intelligently.
127
+
128
+ - **`loadCommands<G>(): Promise<Command<G>[]>`**: Load and cache subcommands for rendering command lists. Results are cached after the first call for performance.
129
+
130
+ ### Usage Example
131
+
132
+ ```ts
133
+ import renderer from '@gunshi/plugin-renderer'
134
+ import { cli, define } from 'gunshi'
135
+
136
+ const deploy = define({
137
+ name: 'deploy',
138
+ description: 'Deploy the application',
139
+ run: async ctx => {
140
+ console.log('Deploying...')
141
+ }
142
+ })
143
+
144
+ const test = define({
145
+ name: 'test',
146
+ description: 'Run tests',
147
+ run: async ctx => {
148
+ console.log('Running tests...')
149
+ }
150
+ })
151
+
152
+ const entry = define({
153
+ name: 'tools',
154
+ run: async ctx => {
155
+ // Access renderer extensions
156
+ const { text, loadCommands } = ctx.extensions['g:renderer']
157
+
158
+ // Render built-in message
159
+ const usageHeader = await text('_:USAGE') // "USAGE" or translated
160
+ console.log(usageHeader)
161
+
162
+ // Load and display subcommands
163
+ const subCommands = await loadCommands()
164
+ console.log('\nAvailable commands:')
165
+ for (const cmd of subCommands) {
166
+ console.log(` ${cmd.name}: ${cmd.description}`)
167
+ }
168
+ }
169
+ })
170
+
171
+ // Create subCommands Map
172
+ const subCommands = new Map()
173
+ subCommands.set(deploy.name, deploy)
174
+ subCommands.set(test.name, test)
175
+
176
+ await cli(process.argv.slice(2), command, {
177
+ name: 'tools-cli',
178
+ version: '1.0.0',
179
+ subCommands,
180
+ plugins: [renderer()],
181
+
182
+ // Optional: Custom renderers
183
+ renderHeader: async ctx => {
184
+ return `=== ${ctx.env.name} v${ctx.env.version} ===`
185
+ },
186
+ renderUsage: async ctx => {
187
+ // Your custom usage renderer
188
+ }
189
+ })
190
+ ```
191
+
192
+ ### Integration with i18n Plugin
193
+
194
+ The renderer plugin has an optional dependency on the i18n plugin. When both plugins are used together, all rendered text automatically uses translations:
195
+
196
+ ```ts
197
+ import renderer from '@gunshi/plugin-renderer'
198
+ import i18n from '@gunshi/plugin-i18n'
199
+ import resources from '@gunshi/resources'
200
+ import { cli } from 'gunshi'
201
+
202
+ await cli(args, command, {
203
+ plugins: [
204
+ i18n({
205
+ locale: 'ja-JP',
206
+ resources // Uses built-in resources from @gunshi/resources
207
+ }),
208
+ renderer() // Will use Japanese translations
209
+ ]
210
+ })
211
+ ```
212
+
213
+ #### With Custom Resources
214
+
215
+ You can extend the built-in resources with your own translations:
216
+
217
+ ```ts
218
+ import renderer from '@gunshi/plugin-renderer'
219
+ import i18n from '@gunshi/plugin-i18n'
220
+ import resources from '@gunshi/resources'
221
+ import { cli } from 'gunshi'
222
+
223
+ // Extend built-in resources with custom messages
224
+ const customResources = {
225
+ 'en-US': {
226
+ ...resources['en-US'],
227
+ // Custom messages for your app
228
+ APP_WELCOME: 'Welcome to My CLI Tool!',
229
+ APP_PROCESSING: 'Processing your request...'
230
+ },
231
+ 'ja-JP': {
232
+ ...resources['ja-JP'],
233
+ // Custom messages in Japanese
234
+ APP_WELCOME: '私のCLIツールへようこそ!',
235
+ APP_PROCESSING: 'リクエストを処理しています...'
236
+ }
237
+ }
238
+
239
+ await cli(args, command, {
240
+ plugins: [
241
+ i18n({
242
+ locale: 'ja-JP',
243
+ resources: customResources
244
+ }),
245
+ renderer() // Will use Japanese translations including custom messages
246
+ ]
247
+ })
248
+ ```
249
+
250
+ ### Custom Rendering
251
+
252
+ You can create custom plugins that use the renderer functions while adding your own branding or logic:
253
+
254
+ ```ts
255
+ import { plugin } from '@gunshi/plugin'
256
+ import { renderUsage } from '@gunshi/plugin-renderer'
257
+
258
+ const customPlugin = plugin({
259
+ id: 'my:custom',
260
+ name: 'Custom Plugin',
261
+
262
+ setup: ctx => {
263
+ // Decorate with custom logic while using renderer functions
264
+ ctx.decorateUsageRenderer(async (baseRenderer, cmdCtx) => {
265
+ // Render usage via built-in usage renderer
266
+ const standardUsage = await renderUsage(cmdCtx)
267
+
268
+ // Add custom branding
269
+ return `
270
+ ╔══════════════════════════════════╗
271
+ ║ MY AWESOME CLI ║
272
+ ╚══════════════════════════════════╝
273
+
274
+ ${standardUsage}
275
+
276
+ © 2025 Your Company
277
+ `
278
+ })
279
+ }
280
+ })
281
+ ```
282
+
283
+ ## 📚 API References
284
+
285
+ See the [API References](./docs/index.md)
286
+
287
+ ## ©️ License
288
+
289
+ [MIT](http://opensource.org/licenses/MIT)T)
package/lib/index.d.ts ADDED
@@ -0,0 +1,123 @@
1
+ import { Command, CommandContext, DefaultGunshiParams, GunshiParams, PluginWithExtension } from "@gunshi/plugin";
2
+ import { Args } from "args-tokens";
3
+
4
+ //#region rolldown:runtime
5
+ declare namespace constants_d_exports {
6
+ export { ARG_NEGATABLE_PREFIX, ARG_PREFIX, ARG_PREFIX_AND_KEY_SEPARATOR, BUILD_IN_PREFIX_AND_KEY_SEPARATOR, BUILT_IN_KEY_SEPARATOR, BUILT_IN_PREFIX, COMMAND_BUILTIN_RESOURCE_KEYS, COMMON_ARGS, PLUGIN_PREFIX };
7
+ }
8
+ /**
9
+ * @author kazuya kawaguchi (a.k.a. kazupon)
10
+ * @license MIT
11
+ */
12
+ declare const BUILT_IN_PREFIX = "_";
13
+ declare const PLUGIN_PREFIX = "g";
14
+ declare const ARG_PREFIX = "arg";
15
+ declare const BUILT_IN_KEY_SEPARATOR = ":";
16
+ declare const BUILD_IN_PREFIX_AND_KEY_SEPARATOR: string;
17
+ declare const ARG_PREFIX_AND_KEY_SEPARATOR: string;
18
+ declare const ARG_NEGATABLE_PREFIX = "no-";
19
+ type CommonArgType = {
20
+ readonly help: {
21
+ readonly type: 'boolean';
22
+ readonly short: 'h';
23
+ readonly description: string;
24
+ };
25
+ readonly version: {
26
+ readonly type: 'boolean';
27
+ readonly short: 'v';
28
+ readonly description: string;
29
+ };
30
+ };
31
+ declare const COMMON_ARGS: CommonArgType;
32
+ declare const COMMAND_BUILTIN_RESOURCE_KEYS: readonly ["USAGE", "COMMAND", "SUBCOMMAND", "COMMANDS", "ARGUMENTS", "OPTIONS", "EXAMPLES", "FORMORE", "NEGATABLE", "DEFAULT", "CHOICES"];
33
+
34
+ //#endregion
35
+ //#region ../shared/src/types.d.ts
36
+ type RemoveIndexSignature<T> = { [K in keyof T as string extends K ? never : number extends K ? never : K]: T[K] };
37
+ /**
38
+ * Remove index signature from object or record type.
39
+ */
40
+ type RemovedIndex<T> = RemoveIndexSignature<{ [K in keyof T]: T[K] }>;
41
+ type KeyOfArgs<A extends Args> = keyof A | { [K in keyof A]: A[K]['type'] extends 'boolean' ? A[K]['negatable'] extends true ? `no-${Extract<K, string>}` : never : never }[keyof A];
42
+ /**
43
+ * Generate a namespaced key.
44
+ */
45
+ type GenerateNamespacedKey<Key extends string, Prefixed extends string = typeof BUILT_IN_PREFIX> = `${Prefixed}${typeof BUILT_IN_KEY_SEPARATOR}${Key}`;
46
+ /**
47
+ * Command i18n built-in arguments keys.
48
+ */
49
+ type CommandBuiltinArgsKeys = keyof (typeof constants_d_exports)['COMMON_ARGS'];
50
+ /**
51
+ * Command i18n built-in resource keys.
52
+ */
53
+ type CommandBuiltinResourceKeys = (typeof constants_d_exports)['COMMAND_BUILTIN_RESOURCE_KEYS'][number];
54
+ /**
55
+ * i18n built-in resource keys.
56
+ */
57
+ type BuiltinResourceKeys = CommandBuiltinArgsKeys | CommandBuiltinResourceKeys;
58
+ /**
59
+ * Command i18n built-in keys.
60
+ * The command i18n built-in keys are used by the i18n plugin for translation.
61
+ */
62
+ type CommandBuiltinKeys = GenerateNamespacedKey<BuiltinResourceKeys> | 'description' | 'examples';
63
+ /**
64
+ * Command i18n option keys.
65
+ * The command i18n option keys are used by the i18n plugin for translation.
66
+ */
67
+ type CommandArgKeys<A extends Args> = GenerateNamespacedKey<KeyOfArgs<RemovedIndex<A>>, typeof ARG_PREFIX>;
68
+
69
+ //#endregion
70
+ //#region src/types.d.ts
71
+ /**
72
+ * Extended command context which provides utilities via usage renderer plugin.
73
+ * These utilities are available via `CommandContext.extensions['g:renderer']`.
74
+ */
75
+ interface UsageRendererCommandContext<G extends GunshiParams<any> = DefaultGunshiParams> {
76
+ /**
77
+ * Render the text message
78
+ */
79
+ text: <T extends string = CommandBuiltinKeys, O = CommandArgKeys<G['args']>, K = CommandBuiltinKeys | O | T>(key: K, values?: Record<string, unknown>) => Promise<string>;
80
+ /**
81
+ * Load commands
82
+ * @returns A list of commands loaded from the command loader plugin.
83
+ */
84
+ loadCommands: <G extends GunshiParams = DefaultGunshiParams>() => Promise<Command<G>[]>;
85
+ }
86
+
87
+ //#endregion
88
+ //#region src/header.d.ts
89
+ /**
90
+ * Render the header.
91
+ * @param ctx A {@link CommandContext | command context}
92
+ * @returns A rendered header.
93
+ */
94
+ declare function renderHeader<G extends GunshiParams = DefaultGunshiParams>(ctx: Readonly<CommandContext<G>>): Promise<string>;
95
+
96
+ //#endregion
97
+ //#region src/usage.d.ts
98
+ /**
99
+ * Render the usage.
100
+ * @param ctx A {@link CommandContext | command context}
101
+ * @returns A rendered usage.
102
+ */
103
+ declare function renderUsage<G extends GunshiParams = DefaultGunshiParams>(ctx: Readonly<CommandContext<G>>): Promise<string>;
104
+
105
+ //#endregion
106
+ //#region src/validation.d.ts
107
+ /**
108
+ * Render the validation errors.
109
+ * @param ctx A {@link CommandContext | command context}
110
+ * @param error An {@link AggregateError} of option in `args-token` validation
111
+ * @returns A rendered validation error.
112
+ */
113
+ declare function renderValidationErrors<G extends GunshiParams = DefaultGunshiParams>(_ctx: CommandContext<G>, error: AggregateError): Promise<string>;
114
+
115
+ //#endregion
116
+ //#region src/index.d.ts
117
+ /**
118
+ * usage renderer plugin
119
+ */
120
+ declare function renderer(): PluginWithExtension<UsageRendererCommandContext>;
121
+
122
+ //#endregion
123
+ export { UsageRendererCommandContext, renderer as default, renderHeader, renderUsage, renderValidationErrors };
package/lib/index.js ADDED
@@ -0,0 +1,488 @@
1
+ import { plugin } from "@gunshi/plugin";
2
+
3
+ //#region ../../node_modules/.pnpm/args-tokens@0.20.1/node_modules/args-tokens/lib/utils-N7UlhLbz.js
4
+ /**
5
+ * Entry point of utils.
6
+ *
7
+ * Note that this entry point is used by gunshi to import utility functions.
8
+ *
9
+ * @module
10
+ */
11
+ /**
12
+ * @author kazuya kawaguchi (a.k.a. kazupon)
13
+ * @license MIT
14
+ */
15
+ function kebabnize(str) {
16
+ return str.replace(/[A-Z]/g, (match, offset) => (offset > 0 ? "-" : "") + match.toLowerCase());
17
+ }
18
+
19
+ //#endregion
20
+ //#region ../gunshi/src/utils.ts
21
+ function isLazyCommand(cmd) {
22
+ return typeof cmd === "function" && "commandName" in cmd && !!cmd.commandName;
23
+ }
24
+ async function resolveLazyCommand(cmd, name, needRunResolving = false) {
25
+ let command;
26
+ if (isLazyCommand(cmd)) {
27
+ const baseCommand = {
28
+ name: cmd.commandName,
29
+ description: cmd.description,
30
+ args: cmd.args,
31
+ examples: cmd.examples
32
+ };
33
+ if ("resource" in cmd && cmd.resource) baseCommand.resource = cmd.resource;
34
+ command = Object.assign(create(), baseCommand);
35
+ if (needRunResolving) {
36
+ const loaded = await cmd();
37
+ if (typeof loaded === "function") command.run = loaded;
38
+ else if (typeof loaded === "object") {
39
+ if (loaded.run == null) throw new TypeError(`'run' is required in command: ${cmd.name || name}`);
40
+ command.run = loaded.run;
41
+ command.name = loaded.name;
42
+ command.description = loaded.description;
43
+ command.args = loaded.args;
44
+ command.examples = loaded.examples;
45
+ if ("resource" in loaded && loaded.resource) command.resource = loaded.resource;
46
+ } else throw new TypeError(`Cannot resolve command: ${cmd.name || name}`);
47
+ }
48
+ } else command = Object.assign(create(), cmd);
49
+ if (command.name == null && name) command.name = name;
50
+ return deepFreeze(command);
51
+ }
52
+ function create(obj = null) {
53
+ return Object.create(obj);
54
+ }
55
+ function deepFreeze(obj, ignores = []) {
56
+ if (obj === null || typeof obj !== "object") return obj;
57
+ for (const key of Object.keys(obj)) {
58
+ const value = obj[key];
59
+ if (ignores.includes(key)) continue;
60
+ if (typeof value === "object" && value !== null) deepFreeze(value, ignores);
61
+ }
62
+ return Object.freeze(obj);
63
+ }
64
+
65
+ //#endregion
66
+ //#region ../shared/src/constants.ts
67
+ /**
68
+ * @author kazuya kawaguchi (a.k.a. kazupon)
69
+ * @license MIT
70
+ */
71
+ const BUILT_IN_PREFIX = "_";
72
+ const PLUGIN_PREFIX = "g";
73
+ const ARG_PREFIX = "arg";
74
+ const BUILT_IN_KEY_SEPARATOR = ":";
75
+ const BUILD_IN_PREFIX_AND_KEY_SEPARATOR = `${BUILT_IN_PREFIX}${BUILT_IN_KEY_SEPARATOR}`;
76
+ const ARG_PREFIX_AND_KEY_SEPARATOR = `${ARG_PREFIX}${BUILT_IN_KEY_SEPARATOR}`;
77
+ const ARG_NEGATABLE_PREFIX = "no-";
78
+ const COMMON_ARGS = {
79
+ help: {
80
+ type: "boolean",
81
+ short: "h",
82
+ description: "Display this help message"
83
+ },
84
+ version: {
85
+ type: "boolean",
86
+ short: "v",
87
+ description: "Display this version"
88
+ }
89
+ };
90
+
91
+ //#endregion
92
+ //#region ../resources/locales/en-US.json
93
+ var COMMAND = "COMMAND";
94
+ var COMMANDS = "COMMANDS";
95
+ var SUBCOMMAND = "SUBCOMMAND";
96
+ var USAGE = "USAGE";
97
+ var ARGUMENTS = "ARGUMENTS";
98
+ var OPTIONS = "OPTIONS";
99
+ var EXAMPLES = "EXAMPLES";
100
+ var FORMORE = "For more info, run any command with the `--help` flag";
101
+ var NEGATABLE = "Negatable of";
102
+ var DEFAULT = "default";
103
+ var CHOICES = "choices";
104
+ var help = "Display this help message";
105
+ var version = "Display this version";
106
+ var en_US_default = {
107
+ COMMAND,
108
+ COMMANDS,
109
+ SUBCOMMAND,
110
+ USAGE,
111
+ ARGUMENTS,
112
+ OPTIONS,
113
+ EXAMPLES,
114
+ FORMORE,
115
+ NEGATABLE,
116
+ DEFAULT,
117
+ CHOICES,
118
+ help,
119
+ version
120
+ };
121
+
122
+ //#endregion
123
+ //#region ../shared/src/utils.ts
124
+ function resolveBuiltInKey(key) {
125
+ return `${BUILT_IN_PREFIX}${BUILT_IN_KEY_SEPARATOR}${key}`;
126
+ }
127
+ function resolveArgKey(key) {
128
+ return `${ARG_PREFIX}${BUILT_IN_KEY_SEPARATOR}${key}`;
129
+ }
130
+ async function resolveExamples$1(ctx, examples) {
131
+ return typeof examples === "string" ? examples : typeof examples === "function" ? await examples(ctx) : "";
132
+ }
133
+ function namespacedId(id) {
134
+ return `${PLUGIN_PREFIX}${BUILT_IN_KEY_SEPARATOR}${id}`;
135
+ }
136
+
137
+ //#endregion
138
+ //#region src/header.ts
139
+ /**
140
+ * Render the header.
141
+ * @param ctx A {@link CommandContext | command context}
142
+ * @returns A rendered header.
143
+ */
144
+ function renderHeader(ctx) {
145
+ const title = ctx.env.description || ctx.env.name || "";
146
+ return Promise.resolve(title ? `${title} (${ctx.env.name || ""}${ctx.env.version ? ` v${ctx.env.version}` : ""})` : title);
147
+ }
148
+
149
+ //#endregion
150
+ //#region src/types.ts
151
+ /**
152
+ * The unique identifier for usage renderer plugin.
153
+ */
154
+ const pluginId = namespacedId("renderer");
155
+
156
+ //#endregion
157
+ //#region src/usage.ts
158
+ const COMMON_ARGS_KEYS = Object.keys(COMMON_ARGS);
159
+ /**
160
+ * Render the usage.
161
+ * @param ctx A {@link CommandContext | command context}
162
+ * @returns A rendered usage.
163
+ */
164
+ async function renderUsage(ctx) {
165
+ const messages = [];
166
+ if (!ctx.omitted) {
167
+ const description = await resolveDescription(ctx);
168
+ if (description) messages.push(description, "");
169
+ }
170
+ messages.push(...await renderUsageSection(ctx), "");
171
+ if (ctx.omitted && await hasCommands(ctx)) messages.push(...await renderCommandsSection(ctx), "");
172
+ if (hasPositionalArgs(ctx)) messages.push(...await renderPositionalArgsSection(ctx), "");
173
+ if (hasOptionalArgs(ctx)) messages.push(...await renderOptionalArgsSection(ctx), "");
174
+ const examples = await renderExamplesSection(ctx);
175
+ if (examples.length > 0) messages.push(...examples, "");
176
+ return messages.join("\n");
177
+ }
178
+ /**
179
+ * Render the positional arguments section
180
+ * @param ctx A {@link CommandContext | command context}
181
+ * @returns A rendered arguments section
182
+ */
183
+ async function renderPositionalArgsSection(ctx) {
184
+ const messages = [];
185
+ messages.push(`${await ctx.extensions[pluginId].text(resolveBuiltInKey("ARGUMENTS"))}:`);
186
+ messages.push(await generatePositionalArgsUsage(ctx));
187
+ return messages;
188
+ }
189
+ /**
190
+ * Render the optional arguments section
191
+ * @param ctx A {@link CommandContext | command context}
192
+ * @returns A rendered options section
193
+ */
194
+ async function renderOptionalArgsSection(ctx) {
195
+ const messages = [];
196
+ messages.push(`${await ctx.extensions[pluginId].text(resolveBuiltInKey("OPTIONS"))}:`);
197
+ messages.push(await generateOptionalArgsUsage(ctx, getOptionalArgsPairs(ctx)));
198
+ return messages;
199
+ }
200
+ /**
201
+ * Render the examples section
202
+ * @param ctx A {@link CommandContext | command context}
203
+ * @returns A rendered examples section
204
+ */
205
+ async function renderExamplesSection(ctx) {
206
+ const messages = [];
207
+ const resolvedExamples = await resolveExamples(ctx);
208
+ if (resolvedExamples) {
209
+ const examples = resolvedExamples.split("\n").map((example) => example.padStart(ctx.env.leftMargin + example.length));
210
+ messages.push(`${await ctx.extensions[pluginId].text(resolveBuiltInKey("EXAMPLES"))}:`, ...examples);
211
+ }
212
+ return messages;
213
+ }
214
+ /**
215
+ * Render the usage section
216
+ * @param ctx A {@link CommandContext | command context}
217
+ * @returns A rendered usage section
218
+ */
219
+ async function renderUsageSection(ctx) {
220
+ const messages = [`${await ctx.extensions[pluginId].text(resolveBuiltInKey("USAGE"))}:`];
221
+ if (ctx.omitted) {
222
+ const defaultCommand = `${await resolveEntry(ctx)}${await hasCommands(ctx) ? ` [${await resolveSubCommand(ctx)}]` : ""} ${[await generateOptionsSymbols(ctx), generatePositionalSymbols(ctx)].filter(Boolean).join(" ")}`;
223
+ messages.push(defaultCommand.padStart(ctx.env.leftMargin + defaultCommand.length));
224
+ if (await hasCommands(ctx)) {
225
+ const commandsUsage = `${await resolveEntry(ctx)} <${await ctx.extensions[pluginId].text(resolveBuiltInKey("COMMANDS"))}>`;
226
+ messages.push(commandsUsage.padStart(ctx.env.leftMargin + commandsUsage.length));
227
+ }
228
+ } else {
229
+ const usageStr = `${await resolveEntry(ctx)} ${await resolveSubCommand(ctx)} ${[await generateOptionsSymbols(ctx), generatePositionalSymbols(ctx)].filter(Boolean).join(" ")}`;
230
+ messages.push(usageStr.padStart(ctx.env.leftMargin + usageStr.length));
231
+ }
232
+ return messages;
233
+ }
234
+ /**
235
+ * Render the commands section
236
+ * @param ctx A {@link CommandContext | command context}
237
+ * @returns A rendered commands section
238
+ */
239
+ async function renderCommandsSection(ctx) {
240
+ const messages = [`${await ctx.extensions[pluginId].text(resolveBuiltInKey("COMMANDS"))}:`];
241
+ const loadedCommands = await ctx.extensions?.[pluginId].loadCommands() || [];
242
+ const commandMaxLength = Math.max(...loadedCommands.map((cmd) => (cmd.name || "").length));
243
+ const commandsStr = await Promise.all(loadedCommands.map((cmd) => {
244
+ const key = cmd.name || "";
245
+ const desc = cmd.description || "";
246
+ const command = `${key.padEnd(commandMaxLength + ctx.env.middleMargin)}${desc} `;
247
+ return `${command.padStart(ctx.env.leftMargin + command.length)} `;
248
+ }));
249
+ messages.push(...commandsStr, "", `${await ctx.extensions[pluginId].text(resolveBuiltInKey("FORMORE"))}:`);
250
+ messages.push(...loadedCommands.map((cmd) => {
251
+ const commandHelp = `${ctx.env.name} ${cmd.name} --help`;
252
+ return `${commandHelp.padStart(ctx.env.leftMargin + commandHelp.length)}`;
253
+ }));
254
+ return messages;
255
+ }
256
+ /**
257
+ * Resolve the entry command name
258
+ * @param ctx A {@link CommandContext | command context}
259
+ * @returns The entry command name
260
+ */
261
+ async function resolveEntry(ctx) {
262
+ return ctx.env.name || await ctx.extensions[pluginId].text(resolveBuiltInKey("COMMAND"));
263
+ }
264
+ /**
265
+ * Resolve the sub command name
266
+ * @param ctx A {@link CommandContext | command context}
267
+ * @returns The sub command name
268
+ */
269
+ async function resolveSubCommand(ctx) {
270
+ return ctx.name || await ctx.extensions[pluginId].text(resolveBuiltInKey("SUBCOMMAND"));
271
+ }
272
+ /**
273
+ * Resolve the command description
274
+ * @param ctx A {@link CommandContext | command context}
275
+ * @returns resolved command description
276
+ */
277
+ async function resolveDescription(ctx) {
278
+ return await ctx.extensions[pluginId].text("description") || ctx.description || "";
279
+ }
280
+ /**
281
+ * Resolve the command examples
282
+ * @param ctx A {@link CommandContext | command context}
283
+ * @returns resolved command examples, if not resolved, return empty string
284
+ */
285
+ async function resolveExamples(ctx) {
286
+ const ret = await ctx.extensions[pluginId].text("examples");
287
+ if (ret) return ret;
288
+ const command = ctx.env.subCommands?.get(ctx.name || "");
289
+ return await resolveExamples$1(ctx, command?.examples);
290
+ }
291
+ /**
292
+ * Check if the command has sub commands
293
+ * @param ctx A {@link CommandContext | command context}
294
+ * @returns True if the command has sub commands
295
+ */
296
+ async function hasCommands(ctx) {
297
+ const loadedCommands = await ctx.extensions?.[pluginId].loadCommands() || [];
298
+ return loadedCommands.length > 1;
299
+ }
300
+ /**
301
+ * Check if the command has optional arguments
302
+ * @param ctx A {@link CommandContext | command context}
303
+ * @returns True if the command has options
304
+ */
305
+ function hasOptionalArgs(ctx) {
306
+ return !!(ctx.args && Object.values(ctx.args).some((arg) => arg.type !== "positional"));
307
+ }
308
+ /**
309
+ * Check if the command has positional arguments
310
+ * @param ctx A {@link CommandContext | command context}
311
+ * @returns True if the command has options
312
+ */
313
+ function hasPositionalArgs(ctx) {
314
+ return !!(ctx.args && Object.values(ctx.args).some((arg) => arg.type === "positional"));
315
+ }
316
+ /**
317
+ * Check if all options have default values
318
+ * @param ctx A {@link CommandContext | command context}
319
+ * @returns True if all options have default values
320
+ */
321
+ function hasAllDefaultOptions(ctx) {
322
+ return !!(ctx.args && Object.values(ctx.args).every((arg) => arg.default));
323
+ }
324
+ /**
325
+ * Generate options symbols for usage
326
+ * @param ctx A {@link CommandContext | command context}
327
+ * @returns Options symbols for usage
328
+ */
329
+ async function generateOptionsSymbols(ctx) {
330
+ return hasOptionalArgs(ctx) ? hasAllDefaultOptions(ctx) ? `[${await ctx.extensions[pluginId].text(resolveBuiltInKey("OPTIONS"))}]` : `<${await ctx.extensions[pluginId].text(resolveBuiltInKey("OPTIONS"))}>` : "";
331
+ }
332
+ function makeShortLongOptionPair(schema, name, toKebab) {
333
+ const displayName = toKebab || schema.toKebab ? kebabnize(name) : name;
334
+ let key = `--${displayName}`;
335
+ if (schema.short) key = `-${schema.short}, ${key}`;
336
+ return key;
337
+ }
338
+ /**
339
+ * Get optional arguments pairs for usage
340
+ * @param ctx A {@link CommandContext | command context}
341
+ * @returns Options pairs for usage
342
+ */
343
+ function getOptionalArgsPairs(ctx) {
344
+ return Object.entries(ctx.args).reduce((acc, [name, schema]) => {
345
+ if (schema.type === "positional") return acc;
346
+ let key = makeShortLongOptionPair(schema, name, ctx.toKebab);
347
+ if (schema.type !== "boolean") {
348
+ const displayName = ctx.toKebab || schema.toKebab ? kebabnize(name) : name;
349
+ key = schema.default ? `${key} [${displayName}]` : `${key} <${displayName}>`;
350
+ }
351
+ acc[name] = key;
352
+ if (schema.type === "boolean" && schema.negatable && !COMMON_ARGS_KEYS.includes(name)) {
353
+ const displayName = ctx.toKebab || schema.toKebab ? kebabnize(name) : name;
354
+ acc[`${ARG_NEGATABLE_PREFIX}${name}`] = `--${ARG_NEGATABLE_PREFIX}${displayName}`;
355
+ }
356
+ return acc;
357
+ }, Object.create(null));
358
+ }
359
+ const resolveNegatableKey = (key) => key.split(ARG_NEGATABLE_PREFIX)[1];
360
+ function resolveNegatableType(key, ctx) {
361
+ return ctx.args[key.startsWith(ARG_NEGATABLE_PREFIX) ? resolveNegatableKey(key) : key].type;
362
+ }
363
+ async function generateDefaultDisplayValue(ctx, schema) {
364
+ return `${await ctx.extensions[pluginId].text(resolveBuiltInKey("DEFAULT"))}: ${schema.default}`;
365
+ }
366
+ async function resolveDisplayValue(ctx, key) {
367
+ if (COMMON_ARGS_KEYS.includes(key)) return "";
368
+ const schema = ctx.args[key];
369
+ if ((schema.type === "boolean" || schema.type === "number" || schema.type === "string" || schema.type === "custom") && schema.default !== void 0) return `(${await generateDefaultDisplayValue(ctx, schema)})`;
370
+ if (schema.type === "enum") {
371
+ const _default = schema.default !== void 0 ? await generateDefaultDisplayValue(ctx, schema) : "";
372
+ const choices = `${await ctx.extensions[pluginId].text(resolveBuiltInKey("CHOICES"))}: ${schema.choices.join(" | ")}`;
373
+ return `(${_default ? `${_default}, ${choices}` : choices})`;
374
+ }
375
+ return "";
376
+ }
377
+ /**
378
+ * Generate optional arguments usage
379
+ * @param ctx A {@link CommandContext | command context}
380
+ * @param optionsPairs Options pairs for usage
381
+ * @returns Generated options usage
382
+ */
383
+ async function generateOptionalArgsUsage(ctx, optionsPairs) {
384
+ const optionsMaxLength = Math.max(...Object.entries(optionsPairs).map(([_, value]) => value.length));
385
+ const optionSchemaMaxLength = ctx.env.usageOptionType ? Math.max(...Object.entries(optionsPairs).map(([key]) => resolveNegatableType(key, ctx).length)) : 0;
386
+ const usages = await Promise.all(Object.entries(optionsPairs).map(async ([key, value]) => {
387
+ let rawDesc = await ctx.extensions[pluginId].text(resolveArgKey(key));
388
+ if (!rawDesc && key.startsWith(ARG_NEGATABLE_PREFIX)) {
389
+ const name = resolveNegatableKey(key);
390
+ const schema = ctx.args[name];
391
+ const optionKey = makeShortLongOptionPair(schema, name, ctx.toKebab);
392
+ rawDesc = `${await ctx.extensions[pluginId].text(resolveBuiltInKey("NEGATABLE"))} ${optionKey}`;
393
+ }
394
+ const optionsSchema = ctx.env.usageOptionType ? `[${resolveNegatableType(key, ctx)}] ` : "";
395
+ const valueDesc = key.startsWith(ARG_NEGATABLE_PREFIX) ? "" : await resolveDisplayValue(ctx, key);
396
+ const desc = `${optionsSchema ? optionsSchema.padEnd(optionSchemaMaxLength + 3) : ""}${rawDesc}`;
397
+ const option = `${value.padEnd(optionsMaxLength + ctx.env.middleMargin)}${desc}${valueDesc ? ` ${valueDesc}` : ""}`;
398
+ return `${option.padStart(ctx.env.leftMargin + option.length)}`;
399
+ }));
400
+ return usages.join("\n");
401
+ }
402
+ function getPositionalArgs(ctx) {
403
+ return Object.entries(ctx.args).filter(([_, schema]) => schema.type === "positional");
404
+ }
405
+ async function generatePositionalArgsUsage(ctx) {
406
+ const positionals = getPositionalArgs(ctx);
407
+ const argsMaxLength = Math.max(...positionals.map(([name]) => name.length));
408
+ const usages = await Promise.all(positionals.map(async ([name]) => {
409
+ const desc = await ctx.extensions[pluginId].text(resolveArgKey(name)) || ctx.args[name].description || "";
410
+ const arg = `${name.padEnd(argsMaxLength + ctx.env.middleMargin)} ${desc}`;
411
+ return `${arg.padStart(ctx.env.leftMargin + arg.length)}`;
412
+ }));
413
+ return usages.join("\n");
414
+ }
415
+ function generatePositionalSymbols(ctx) {
416
+ return hasPositionalArgs(ctx) ? getPositionalArgs(ctx).map(([name]) => `<${name}>`).join(" ") : "";
417
+ }
418
+
419
+ //#endregion
420
+ //#region src/validation.ts
421
+ /**
422
+ * Render the validation errors.
423
+ * @param ctx A {@link CommandContext | command context}
424
+ * @param error An {@link AggregateError} of option in `args-token` validation
425
+ * @returns A rendered validation error.
426
+ */
427
+ function renderValidationErrors(_ctx, error) {
428
+ const messages = [];
429
+ for (const err of error.errors) messages.push(err.message);
430
+ return Promise.resolve(messages.join("\n"));
431
+ }
432
+
433
+ //#endregion
434
+ //#region src/index.ts
435
+ const i18nPluginId = namespacedId("i18n");
436
+ /**
437
+ * usage renderer plugin
438
+ */
439
+ function renderer() {
440
+ return plugin({
441
+ id: pluginId,
442
+ name: "usage renderer",
443
+ dependencies: [{
444
+ id: i18nPluginId,
445
+ optional: true
446
+ }],
447
+ extension: (ctx, cmd) => {
448
+ const { extensions: { [i18nPluginId]: i18n } } = ctx;
449
+ let cachedCommands;
450
+ async function loadCommands() {
451
+ if (cachedCommands) return cachedCommands;
452
+ const subCommands = [...ctx.env.subCommands || []];
453
+ cachedCommands = await Promise.all(subCommands.map(async ([name, cmd$1]) => await resolveLazyCommand(cmd$1, name)));
454
+ return cachedCommands;
455
+ }
456
+ async function text(key, values = Object.create(null)) {
457
+ if (i18n) return i18n.translate(key, values);
458
+ else if (key.startsWith(BUILD_IN_PREFIX_AND_KEY_SEPARATOR)) {
459
+ const resKey = key.slice(BUILD_IN_PREFIX_AND_KEY_SEPARATOR.length);
460
+ return en_US_default[resKey] || key;
461
+ } else if (key.startsWith(ARG_PREFIX_AND_KEY_SEPARATOR)) {
462
+ let argKey = key.slice(ARG_PREFIX_AND_KEY_SEPARATOR.length);
463
+ let negatable = false;
464
+ if (argKey.startsWith(ARG_NEGATABLE_PREFIX)) {
465
+ argKey = argKey.slice(ARG_NEGATABLE_PREFIX.length);
466
+ negatable = true;
467
+ }
468
+ const schema = ctx.args[argKey];
469
+ return negatable && schema.type === "boolean" && schema.negatable ? `${en_US_default["NEGATABLE"]} ${makeShortLongOptionPair(schema, argKey, ctx.toKebab)}` : schema.description || "";
470
+ } else if (key === "description") return "";
471
+ else if (key === "examples") return await resolveExamples$1(ctx, cmd.examples);
472
+ else return key;
473
+ }
474
+ return {
475
+ text,
476
+ loadCommands
477
+ };
478
+ },
479
+ setup: (ctx) => {
480
+ ctx.decorateHeaderRenderer(async (_baseRenderer, cmdCtx) => await renderHeader(cmdCtx));
481
+ ctx.decorateUsageRenderer(async (_baseRenderer, cmdCtx) => await renderUsage(cmdCtx));
482
+ ctx.decorateValidationErrorsRenderer(async (_baseRenderer, cmdCtx, error) => await renderValidationErrors(cmdCtx, error));
483
+ }
484
+ });
485
+ }
486
+
487
+ //#endregion
488
+ export { renderer as default, renderHeader, renderUsage, renderValidationErrors };
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "@gunshi/plugin-renderer",
3
+ "description": "usage renderer plugin for gunshi",
4
+ "version": "0.26.3",
5
+ "author": {
6
+ "name": "kazuya kawaguchi",
7
+ "email": "kawakazu80@gmail.com"
8
+ },
9
+ "license": "MIT",
10
+ "funding": "https://github.com/sponsors/kazupon",
11
+ "bugs": {
12
+ "url": "https://github.com/kazupon/gunshi/issues"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/kazupon/gunshi.git",
17
+ "directory": "packages/plugin-renderer"
18
+ },
19
+ "keywords": [
20
+ "gunshi",
21
+ "usage",
22
+ "renderer",
23
+ "plugin",
24
+ "cli"
25
+ ],
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "engines": {
30
+ "node": ">= 20"
31
+ },
32
+ "type": "module",
33
+ "files": [
34
+ "lib"
35
+ ],
36
+ "module": "lib/index.js",
37
+ "exports": {
38
+ ".": {
39
+ "types": "./lib/index.d.ts",
40
+ "import": "./lib/index.js",
41
+ "require": "./lib/index.js",
42
+ "default": "./lib/index.js"
43
+ },
44
+ "./package.json": "./package.json"
45
+ },
46
+ "types": "lib/index.d.ts",
47
+ "typesVersions": {
48
+ "*": {
49
+ "*": [
50
+ "./lib/*",
51
+ "./*"
52
+ ]
53
+ }
54
+ },
55
+ "dependencies": {
56
+ "@gunshi/plugin": "0.26.3"
57
+ },
58
+ "peerDependencies": {
59
+ "@gunshi/plugin-i18n": "0.26.3"
60
+ },
61
+ "devDependencies": {
62
+ "deno": "^2.3.3",
63
+ "jsr": "^0.13.4",
64
+ "jsr-exports-lint": "^0.4.1",
65
+ "publint": "^0.3.12",
66
+ "tsdown": "^0.12.3",
67
+ "typedoc": "^0.28.4",
68
+ "typedoc-plugin-markdown": "^4.6.3",
69
+ "@gunshi/shared": "0.26.3"
70
+ },
71
+ "scripts": {
72
+ "build": "tsdown",
73
+ "build:docs": "typedoc --excludeInternal",
74
+ "lint:jsr": "jsr publish --dry-run --allow-dirty",
75
+ "typecheck:deno": "deno check --import-map=../../importmap.json ./src"
76
+ }
77
+ }