@gunshi/plugin-renderer 0.37.3 → 1.0.0-beta.1
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 +4 -2
- package/lib/index.d.ts +592 -55
- package/lib/index.js +248 -56
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
> usage renderer plugin for gunshi.
|
|
8
8
|
|
|
9
|
-
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.
|
|
9
|
+
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. Description columns are aligned by the width a terminal draws, including full-width names; emoji sequences joined with ZWJ may still differ by terminal.
|
|
10
10
|
|
|
11
11
|
## 💿 Installation
|
|
12
12
|
|
|
@@ -136,7 +136,9 @@ Available extensions:
|
|
|
136
136
|
|
|
137
137
|
- **`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.
|
|
138
138
|
|
|
139
|
-
- **`loadCommands<G>(): Promise<Command<G>[]>`**: Load and cache
|
|
139
|
+
- **`loadCommands<G>(): Promise<Command<G>[]>`**: Load and cache commands for rendering command lists. At the top level, the list also includes the CLI's entry command when plugins registered the subcommands without adding the entry to the registry. Results are cached after the first call for performance.
|
|
140
|
+
|
|
141
|
+
- **`localizeCommandDescription(cmd): Promise<string>`**: Resolve the description of a command in the command list. Localized when `@gunshi/plugin-i18n` is installed and the command has a resource of its own; otherwise `Command.description`.
|
|
140
142
|
|
|
141
143
|
### Usage Example
|
|
142
144
|
|
package/lib/index.d.ts
CHANGED
|
@@ -1,18 +1,567 @@
|
|
|
1
1
|
import { Args, Command, CommandContext, DefaultGunshiParams, GunshiParams, PluginWithExtension } from "@gunshi/plugin";
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
//#region
|
|
5
|
-
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
|
|
2
|
+
import "@gunshi/plugin-i18n";
|
|
3
|
+
//#region ../../node_modules/.pnpm/args-tokens@1.0.0-beta.1/node_modules/args-tokens/lib/resolver.d.ts
|
|
4
|
+
//#region src/resolver.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* An argument schema definition for command-line argument parsing.
|
|
7
|
+
*
|
|
8
|
+
* This schema is similar to the schema of Node.js `util.parseArgs` but with extended features:
|
|
9
|
+
* - Additional `required`, `description`, and `hidden` properties
|
|
10
|
+
* - Extended `type` support: 'string', 'boolean', 'number', 'enum', 'positional', 'custom'
|
|
11
|
+
* - Simplified `default` property (single type, not union types)
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* Basic string argument:
|
|
15
|
+
* ```ts
|
|
16
|
+
* const schema: ArgSchema = {
|
|
17
|
+
* type: 'string',
|
|
18
|
+
* description: 'Server hostname',
|
|
19
|
+
* default: 'localhost'
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* Required number argument with alias:
|
|
25
|
+
* ```ts
|
|
26
|
+
* const schema: ArgSchema = {
|
|
27
|
+
* type: 'number',
|
|
28
|
+
* short: 'p',
|
|
29
|
+
* description: 'Port number to listen on',
|
|
30
|
+
* required: true
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* Enum argument with choices:
|
|
36
|
+
* ```ts
|
|
37
|
+
* const schema: ArgSchema = {
|
|
38
|
+
* type: 'enum',
|
|
39
|
+
* choices: ['info', 'warn', 'error'],
|
|
40
|
+
* description: 'Logging level',
|
|
41
|
+
* default: 'info'
|
|
42
|
+
* }
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
interface ArgSchema {
|
|
46
|
+
/**
|
|
47
|
+
* Type of the argument value.
|
|
48
|
+
*
|
|
49
|
+
* - `'string'`: Text value
|
|
50
|
+
* - `'boolean'`: `true`/`false` flag (can be negatable with `--no-` prefix). `--flag=true` and
|
|
51
|
+
* `--flag=false` set the value explicitly; any other value after `=` is a type error
|
|
52
|
+
* - `'number'`: Numeric value (parsed as integer or float)
|
|
53
|
+
* - `'enum'`: One of predefined string values (requires `choices` property, unless the argument
|
|
54
|
+
* has a `parse` function)
|
|
55
|
+
* - `'positional'`: Non-option argument by position
|
|
56
|
+
* - `'custom'`: Custom parsing with user-defined `parse` function
|
|
57
|
+
*
|
|
58
|
+
* Any other `type`, or no `type`, is a mistake that only untyped code can make: if the argument
|
|
59
|
+
* has no `parse` function, {@link resolveArgs} and `parse()` throw an `Error`, whether or not the
|
|
60
|
+
* argument is given.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* Different argument types:
|
|
64
|
+
* ```ts
|
|
65
|
+
* {
|
|
66
|
+
* name: { type: 'string' }, // --name value
|
|
67
|
+
* verbose: { type: 'boolean' }, // --verbose (--no-verbose needs negatable: true)
|
|
68
|
+
* port: { type: 'number' }, // --port 3000
|
|
69
|
+
* level: { type: 'enum', choices: ['debug', 'info'] },
|
|
70
|
+
* file: { type: 'positional' }, // first positional arg
|
|
71
|
+
* config: { type: 'custom', parse: JSON.parse }
|
|
72
|
+
* }
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
type: 'string' | 'boolean' | 'number' | 'enum' | 'positional' | 'custom';
|
|
76
|
+
/**
|
|
77
|
+
* Single character alias for the long option name.
|
|
78
|
+
*
|
|
79
|
+
* As example, allows users to use `-x` instead of `--extended-option`.
|
|
80
|
+
* Only valid for non-positional argument types.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* Short alias usage:
|
|
84
|
+
* ```ts
|
|
85
|
+
* {
|
|
86
|
+
* verbose: {
|
|
87
|
+
* type: 'boolean',
|
|
88
|
+
* short: 'v' // Enables both --verbose and -v
|
|
89
|
+
* },
|
|
90
|
+
* port: {
|
|
91
|
+
* type: 'number',
|
|
92
|
+
* short: 'p' // Enables both --port 3000 and -p 3000
|
|
93
|
+
* }
|
|
94
|
+
* }
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
short?: string;
|
|
98
|
+
/**
|
|
99
|
+
* Human-readable description of the argument's purpose.
|
|
100
|
+
*
|
|
101
|
+
* Used for help text generation and documentation.
|
|
102
|
+
* Should be concise but descriptive enough to understand the argument's role.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* Descriptive help text:
|
|
106
|
+
* ```ts
|
|
107
|
+
* {
|
|
108
|
+
* config: {
|
|
109
|
+
* type: 'string',
|
|
110
|
+
* description: 'Path to configuration file'
|
|
111
|
+
* },
|
|
112
|
+
* timeout: {
|
|
113
|
+
* type: 'number',
|
|
114
|
+
* description: 'Request timeout in milliseconds'
|
|
115
|
+
* }
|
|
116
|
+
* }
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
description?: string;
|
|
120
|
+
/**
|
|
121
|
+
* Hide the argument from generated help or usage output.
|
|
122
|
+
*
|
|
123
|
+
* This is metadata for renderers. It does not affect parsing, validation,
|
|
124
|
+
* required checks, defaults, conflicts, or resolved values.
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* Hidden compatibility option:
|
|
128
|
+
* ```ts
|
|
129
|
+
* {
|
|
130
|
+
* legacy: {
|
|
131
|
+
* type: 'string',
|
|
132
|
+
* hidden: true,
|
|
133
|
+
* description: 'Deprecated compatibility option'
|
|
134
|
+
* }
|
|
135
|
+
* }
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
hidden?: boolean;
|
|
139
|
+
/**
|
|
140
|
+
* Marks the argument as required.
|
|
141
|
+
*
|
|
142
|
+
* When `true`, the argument must be provided by the user. If it is missing, the error is an
|
|
143
|
+
* `ArgResolveError` with type 'required' and the code `err:arg:required-option`
|
|
144
|
+
* ({@link ArgsValidationErrorKeys}.requiredOption), or `err:arg:required-positional`
|
|
145
|
+
* ({@link ArgsValidationErrorKeys}.requiredPositional) for a positional argument. Its
|
|
146
|
+
* `values` has the `displayName` as in the message, such as `'--name'` for an option
|
|
147
|
+
* (`'--name' or '-n'` with the short name `n`) and `'file'` for a positional argument, and
|
|
148
|
+
* the `name`, which is the schema key.
|
|
149
|
+
* An option that is given without a value is reported as `err:arg:missing-value`
|
|
150
|
+
* ({@link ArgsValidationErrorKeys}.missingValue) instead, because the option itself was given.
|
|
151
|
+
* An explicit empty value, such as `--name=` or `-n ''`, is still reported as required, but the
|
|
152
|
+
* option counts as given: it is `true` in `explicit` and takes part in conflicts, as any other
|
|
153
|
+
* given option does.
|
|
154
|
+
*
|
|
155
|
+
* For single-value positional arguments, omitting `required` keeps the argument required for
|
|
156
|
+
* compatibility, unless it has a `default`, which makes it optional. A `multiple` positional
|
|
157
|
+
* argument is optional unless `required: true` is set. Set `required: false` to make a positional
|
|
158
|
+
* argument explicitly optional. Optional positional arguments leave enough input values for later
|
|
159
|
+
* required positional arguments before consuming a value.
|
|
160
|
+
*
|
|
161
|
+
* In the type of the values, such as {@link ArgValues}, a single-value positional argument whose
|
|
162
|
+
* `required` may be `false`, such as one of type `boolean`, is optional, as it may be missing,
|
|
163
|
+
* unless it has a `default`. An optional `required` of type `boolean`, as declared here, says
|
|
164
|
+
* nothing, and keeps it required. TypeScript widens a `required: true` to `boolean` in a schema
|
|
165
|
+
* written apart from the call, such as `{ type: 'positional' as const, required: true }`, which
|
|
166
|
+
* types the argument as optional: write such a schema `as const`.
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* Required arguments:
|
|
170
|
+
* ```ts
|
|
171
|
+
* {
|
|
172
|
+
* input: {
|
|
173
|
+
* type: 'string',
|
|
174
|
+
* required: true, // Must be provided: --input file.txt
|
|
175
|
+
* description: 'Input file path'
|
|
176
|
+
* },
|
|
177
|
+
* source: {
|
|
178
|
+
* type: 'positional',
|
|
179
|
+
* required: true // First positional argument must exist
|
|
180
|
+
* }
|
|
181
|
+
* }
|
|
182
|
+
* ```
|
|
183
|
+
*/
|
|
184
|
+
required?: boolean;
|
|
185
|
+
/**
|
|
186
|
+
* Allows the argument to accept multiple values.
|
|
187
|
+
*
|
|
188
|
+
* When `true`, the resolved value becomes an array. When the argument is not given and has no
|
|
189
|
+
* `default`, its value is `undefined`, not an empty array.
|
|
190
|
+
* For options: can be specified multiple times (--tags foo --tags bar)
|
|
191
|
+
* For positional: collects remaining positional arguments after preserving values for
|
|
192
|
+
* later required positional arguments.
|
|
193
|
+
*
|
|
194
|
+
* Note: Only `true` is allowed (not `false`) to make intent explicit.
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* Multiple values:
|
|
198
|
+
* ```ts
|
|
199
|
+
* {
|
|
200
|
+
* tags: {
|
|
201
|
+
* type: 'string',
|
|
202
|
+
* multiple: true, // --tags foo --tags bar → ['foo', 'bar']
|
|
203
|
+
* description: 'Tags to apply'
|
|
204
|
+
* },
|
|
205
|
+
* files: {
|
|
206
|
+
* type: 'positional',
|
|
207
|
+
* multiple: true // Collects all remaining positional args
|
|
208
|
+
* }
|
|
209
|
+
* }
|
|
210
|
+
* ```
|
|
211
|
+
*/
|
|
212
|
+
multiple?: true;
|
|
213
|
+
/**
|
|
214
|
+
* Enables negation for boolean arguments using `--no-` prefix.
|
|
215
|
+
*
|
|
216
|
+
* When `true`, allows users to explicitly set the boolean to `false`
|
|
217
|
+
* using `--no-option-name`. When `false` or omitted, only positive
|
|
218
|
+
* form is available.
|
|
219
|
+
*
|
|
220
|
+
* Only applicable to `type: 'boolean'` arguments.
|
|
221
|
+
*
|
|
222
|
+
* The negated name is always `no-` followed by the full option name. An option named
|
|
223
|
+
* `no-cache` is negated by `--no-no-cache`, and `--no-cache` sets it to `true`.
|
|
224
|
+
*
|
|
225
|
+
* The negated form does not take a value. `--no-flag=<value>` is reported as
|
|
226
|
+
* `err:arg:unexpected-value` ({@link ArgsValidationErrorKeys}.unexpectedValue).
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* Negatable boolean:
|
|
230
|
+
* ```ts
|
|
231
|
+
* {
|
|
232
|
+
* color: {
|
|
233
|
+
* type: 'boolean',
|
|
234
|
+
* negatable: true,
|
|
235
|
+
* default: true,
|
|
236
|
+
* description: 'Enable colorized output'
|
|
237
|
+
* }
|
|
238
|
+
* // Usage: --color (true), --no-color (false)
|
|
239
|
+
* }
|
|
240
|
+
* ```
|
|
241
|
+
*/
|
|
242
|
+
negatable?: boolean;
|
|
243
|
+
/**
|
|
244
|
+
* Array of allowed string values for enum-type arguments.
|
|
245
|
+
*
|
|
246
|
+
* Required when `type: 'enum'`, unless the argument has a `parse` function: if `choices` is
|
|
247
|
+
* missing or not an array, {@link resolveArgs} and `parse()` throw a `TypeError`, whether or not
|
|
248
|
+
* the argument is given. With a `parse` function and no `choices`, the value is passed to the
|
|
249
|
+
* function as for a `string` argument with a `parse` function.
|
|
250
|
+
*
|
|
251
|
+
* The argument value must be one of these choices, otherwise the error is an `ArgResolveError`
|
|
252
|
+
* with type 'type' and the code `err:arg:invalid-choice`
|
|
253
|
+
* ({@link ArgsValidationErrorKeys}.invalidChoice).
|
|
254
|
+
*
|
|
255
|
+
* The value is checked before `parse` is called, so a `parse` function receives only one of
|
|
256
|
+
* these choices and can change it, for example to upper case.
|
|
257
|
+
*
|
|
258
|
+
* Supports both mutable arrays and readonly arrays for type safety.
|
|
259
|
+
*
|
|
260
|
+
* @example
|
|
261
|
+
* Enum choices:
|
|
262
|
+
* ```ts
|
|
263
|
+
* {
|
|
264
|
+
* logLevel: {
|
|
265
|
+
* type: 'enum',
|
|
266
|
+
* choices: ['debug', 'info', 'warn', 'error'] as const,
|
|
267
|
+
* default: 'info',
|
|
268
|
+
* description: 'Logging verbosity level'
|
|
269
|
+
* },
|
|
270
|
+
* format: {
|
|
271
|
+
* type: 'enum',
|
|
272
|
+
* choices: ['json', 'yaml', 'toml'],
|
|
273
|
+
* description: 'Output format'
|
|
274
|
+
* }
|
|
275
|
+
* }
|
|
276
|
+
* ```
|
|
277
|
+
*/
|
|
278
|
+
choices?: string[] | readonly string[];
|
|
279
|
+
/**
|
|
280
|
+
* Default value used when the argument is not provided. An option that is given without a value,
|
|
281
|
+
* or with a value that is rejected, also gets its default, along with the error.
|
|
282
|
+
*
|
|
283
|
+
* The type must match the argument's `type` property:
|
|
284
|
+
* - `string` type: string default
|
|
285
|
+
* - `boolean` type: boolean default
|
|
286
|
+
* - `number` type: number default
|
|
287
|
+
* - `enum` type: one of the `choices` values, or a value that a `parse` of your own returns
|
|
288
|
+
* - `positional`/`custom` type: string, boolean, or number default
|
|
289
|
+
*
|
|
290
|
+
* The default is used as is and never goes through `parse`, including when an option is given
|
|
291
|
+
* without a value. An explicit empty value, such as `--name=` or `-n ''`, is a value, not a
|
|
292
|
+
* missing one: a `string` option without `parse` gets `''` instead of the default, unless it is
|
|
293
|
+
* `required`. What `parse` returns is a value too, even `undefined` or `null`, and the default
|
|
294
|
+
* does not replace it.
|
|
295
|
+
*
|
|
296
|
+
* The value of a `multiple` argument is an array, so its default becomes the only element of the
|
|
297
|
+
* array: `default: 'latest'` gives `['latest']`.
|
|
298
|
+
*
|
|
299
|
+
* The default of an `enum` option with `choices` is checked when it would be used, that is, when
|
|
300
|
+
* no value from the command line is used: one that is not one of the choices is reported as an
|
|
301
|
+
* `ArgResolveError` with type 'type' and the code `err:arg:invalid-default`
|
|
302
|
+
* ({@link ArgsValidationErrorKeys}.invalidDefault), and is not used. With a `parse` function of
|
|
303
|
+
* your own, or a `map()` transform, the default is a value that the function returns, which need
|
|
304
|
+
* not be one of the choices, and it is not checked. `choice()` returns the value as is, so its
|
|
305
|
+
* default is checked.
|
|
306
|
+
*
|
|
307
|
+
* For positional arguments, `multiple` ones included, the default is used when no value is left
|
|
308
|
+
* for the argument: when the positional values run out, or when the remaining ones are preserved
|
|
309
|
+
* for later required positional arguments. With `required: true`, the default is not used, and
|
|
310
|
+
* the argument is reported as required instead.
|
|
311
|
+
*
|
|
312
|
+
* @example
|
|
313
|
+
* Default values by type:
|
|
314
|
+
* ```ts
|
|
315
|
+
* {
|
|
316
|
+
* host: {
|
|
317
|
+
* type: 'string',
|
|
318
|
+
* default: 'localhost' // string default
|
|
319
|
+
* },
|
|
320
|
+
* verbose: {
|
|
321
|
+
* type: 'boolean',
|
|
322
|
+
* default: false // boolean default
|
|
323
|
+
* },
|
|
324
|
+
* port: {
|
|
325
|
+
* type: 'number',
|
|
326
|
+
* default: 8080 // number default
|
|
327
|
+
* },
|
|
328
|
+
* level: {
|
|
329
|
+
* type: 'enum',
|
|
330
|
+
* choices: ['low', 'high'],
|
|
331
|
+
* default: 'low' // must be in choices
|
|
332
|
+
* }
|
|
333
|
+
* }
|
|
334
|
+
* ```
|
|
335
|
+
*/
|
|
336
|
+
default?: string | boolean | number;
|
|
337
|
+
/**
|
|
338
|
+
* Converts the argument name from camelCase to kebab-case for CLI usage.
|
|
339
|
+
*
|
|
340
|
+
* When `true`, a property like `maxCount` becomes available as `--max-count`.
|
|
341
|
+
* This allows [CAC](https://github.com/cacjs/cac) user-friendly property names while maintaining CLI conventions.
|
|
342
|
+
*
|
|
343
|
+
* The `toKebab` option of {@link resolveArgs} and `parse()` applies it to all arguments, as in
|
|
344
|
+
* `resolveArgs(args, tokens, { toKebab: true })`.
|
|
345
|
+
*
|
|
346
|
+
* Note: Only `true` is allowed (not `false`) to make intent explicit.
|
|
347
|
+
*
|
|
348
|
+
* @example
|
|
349
|
+
* Kebab-case conversion:
|
|
350
|
+
* ```ts
|
|
351
|
+
* {
|
|
352
|
+
* maxRetries: {
|
|
353
|
+
* type: 'number',
|
|
354
|
+
* toKebab: true, // Accessible as --max-retries
|
|
355
|
+
* description: 'Maximum retry attempts'
|
|
356
|
+
* },
|
|
357
|
+
* enableLogging: {
|
|
358
|
+
* type: 'boolean',
|
|
359
|
+
* toKebab: true // Accessible as --enable-logging
|
|
360
|
+
* }
|
|
361
|
+
* }
|
|
362
|
+
* ```
|
|
363
|
+
*/
|
|
364
|
+
toKebab?: true;
|
|
365
|
+
/**
|
|
366
|
+
* Names of other options that conflict with this option.
|
|
367
|
+
*
|
|
368
|
+
* When this option is used together with any of the conflicting options, the error is an
|
|
369
|
+
* `ArgResolveError` with type 'conflict' and the code `err:arg:conflict`
|
|
370
|
+
* ({@link ArgsValidationErrorKeys}.conflict). Its `values` has the `displayName` and `name` of
|
|
371
|
+
* the argument whose `conflicts` names the other one, and the `conflictDisplayName` and
|
|
372
|
+
* `conflictName` of the other one. `displayName` and `conflictDisplayName` show an option as
|
|
373
|
+
* it was written, such as `'-p'`, and `name` and `conflictName` are the schema keys. When both
|
|
374
|
+
* name each other, `name` is the one that comes first in the schema.
|
|
375
|
+
*
|
|
376
|
+
* A positional argument can be on either side, such as `[file]` and `--stdin`. It conflicts when
|
|
377
|
+
* it is given, and it is shown by its name, such as `'file'` (in kebab-case with `toKebab`), as
|
|
378
|
+
* in its other errors. The message starts with the kind of the argument given as `name`:
|
|
379
|
+
* `Positional argument 'file' conflicts with '--stdin'` or
|
|
380
|
+
* `Optional argument '--stdin' conflicts with 'file'`.
|
|
381
|
+
*
|
|
382
|
+
* Conflicts only need to be defined on one side - if option A defines a conflict
|
|
383
|
+
* with option B, the conflict is automatically detected when both are used,
|
|
384
|
+
* regardless of whether B also defines a conflict with A.
|
|
385
|
+
*
|
|
386
|
+
* Supports both single option name or array of option names.
|
|
387
|
+
* Option names must match the property keys in the schema object exactly
|
|
388
|
+
* (no automatic conversion between camelCase and kebab-case).
|
|
389
|
+
*
|
|
390
|
+
* @example
|
|
391
|
+
* Single conflict (bidirectional definition):
|
|
392
|
+
* ```ts
|
|
393
|
+
* {
|
|
394
|
+
* summer: {
|
|
395
|
+
* type: 'boolean',
|
|
396
|
+
* conflicts: 'autumn' // Cannot use --summer with --autumn
|
|
397
|
+
* },
|
|
398
|
+
* autumn: {
|
|
399
|
+
* type: 'boolean',
|
|
400
|
+
* conflicts: 'summer' // Can define on both sides for clarity
|
|
401
|
+
* }
|
|
402
|
+
* }
|
|
403
|
+
* ```
|
|
404
|
+
*
|
|
405
|
+
* @example
|
|
406
|
+
* Single conflict (one-way definition):
|
|
407
|
+
* ```ts
|
|
408
|
+
* {
|
|
409
|
+
* summer: {
|
|
410
|
+
* type: 'boolean',
|
|
411
|
+
* conflicts: 'autumn' // Only defined on summer side
|
|
412
|
+
* },
|
|
413
|
+
* autumn: {
|
|
414
|
+
* type: 'boolean'
|
|
415
|
+
* // No conflicts defined, but still cannot use with --summer
|
|
416
|
+
* }
|
|
417
|
+
* }
|
|
418
|
+
* // Usage: --summer --autumn reports an error
|
|
419
|
+
* // Error: "Optional argument '--summer' conflicts with '--autumn'"
|
|
420
|
+
* ```
|
|
421
|
+
*
|
|
422
|
+
* @example
|
|
423
|
+
* Multiple conflicts:
|
|
424
|
+
* ```ts
|
|
425
|
+
* {
|
|
426
|
+
* port: {
|
|
427
|
+
* type: 'number',
|
|
428
|
+
* conflicts: ['socket', 'pipe'], // Cannot use with --socket or --pipe
|
|
429
|
+
* description: 'TCP port number'
|
|
430
|
+
* },
|
|
431
|
+
* socket: {
|
|
432
|
+
* type: 'string',
|
|
433
|
+
* conflicts: ['port', 'pipe'], // Cannot use with --port or --pipe
|
|
434
|
+
* description: 'Unix socket path'
|
|
435
|
+
* },
|
|
436
|
+
* pipe: {
|
|
437
|
+
* type: 'string',
|
|
438
|
+
* conflicts: ['port', 'socket'], // Cannot use with --port or --socket
|
|
439
|
+
* description: 'Named pipe path'
|
|
440
|
+
* }
|
|
441
|
+
* }
|
|
442
|
+
* // These three options are mutually exclusive
|
|
443
|
+
* ```
|
|
444
|
+
*
|
|
445
|
+
* @example
|
|
446
|
+
* With kebab-case conversion:
|
|
447
|
+
* ```ts
|
|
448
|
+
* {
|
|
449
|
+
* summerSeason: {
|
|
450
|
+
* type: 'boolean',
|
|
451
|
+
* toKebab: true, // Accessible as --summer-season
|
|
452
|
+
* conflicts: 'autumnSeason' // Must use property key, not CLI name
|
|
453
|
+
* },
|
|
454
|
+
* autumnSeason: {
|
|
455
|
+
* type: 'boolean',
|
|
456
|
+
* toKebab: true // Accessible as --autumn-season
|
|
457
|
+
* }
|
|
458
|
+
* }
|
|
459
|
+
* // Error: "Optional argument '--summer-season' conflicts with '--autumn-season'"
|
|
460
|
+
* ```
|
|
461
|
+
*/
|
|
462
|
+
conflicts?: string | string[];
|
|
463
|
+
/**
|
|
464
|
+
* Display name hint for help text generation.
|
|
465
|
+
*
|
|
466
|
+
* Provides a meaningful type hint for the argument value in help output.
|
|
467
|
+
* Particularly useful for `type: 'custom'` arguments where the type
|
|
468
|
+
* name would otherwise be unhelpful.
|
|
469
|
+
*
|
|
470
|
+
* For a `custom` option that is given without a value, it is also the `expected` in the `values`
|
|
471
|
+
* of the `err:arg:missing-value` error ({@link ArgsValidationErrorKeys}.missingValue), which is
|
|
472
|
+
* `'custom'` without it.
|
|
473
|
+
*
|
|
474
|
+
* @example
|
|
475
|
+
* Metavar usage:
|
|
476
|
+
* ```ts
|
|
477
|
+
* {
|
|
478
|
+
* port: {
|
|
479
|
+
* type: 'custom',
|
|
480
|
+
* parse: (v: string) => parseInt(v, 10),
|
|
481
|
+
* metavar: 'integer',
|
|
482
|
+
* description: 'Port number (1-65535)'
|
|
483
|
+
* }
|
|
484
|
+
* }
|
|
485
|
+
* // Help output: --port <integer> Port number (1-65535)
|
|
486
|
+
* ```
|
|
487
|
+
*/
|
|
488
|
+
metavar?: string;
|
|
489
|
+
/**
|
|
490
|
+
* Custom parsing function, used in place of the parsing of the argument's `type`.
|
|
491
|
+
*
|
|
492
|
+
* Required when `type: 'custom'`: if it is missing or not a function, {@link resolveArgs} and
|
|
493
|
+
* `parse()` throw a `TypeError`, whether or not the argument is given. The function receives the
|
|
494
|
+
* raw string value and must return the parsed result. It should throw an Error (or subclass) if
|
|
495
|
+
* parsing fails.
|
|
496
|
+
*
|
|
497
|
+
* The function's return type becomes the resolved argument type.
|
|
498
|
+
*
|
|
499
|
+
* `parse` is called synchronously, and what it returns becomes the value, even `undefined` or
|
|
500
|
+
* `null`, so throw to reject a value. An `async` function returns a promise, and the promise
|
|
501
|
+
* becomes the value as is: it is not awaited, and its rejection is not reported as an error.
|
|
502
|
+
* Await the value, or each of its elements with `multiple` (for example with `Promise.all()`),
|
|
503
|
+
* and handle the rejection yourself: a rejection that nothing handles is an unhandled rejection,
|
|
504
|
+
* which ends a Node.js process by default.
|
|
505
|
+
*
|
|
506
|
+
* A `boolean` option calls `parse` with `'true'`, or `'false'` for the negated form. Other
|
|
507
|
+
* options call it only with a value from the command line: when the option is given without a
|
|
508
|
+
* value, `parse` is not called and the missing value is reported as `err:arg:missing-value`
|
|
509
|
+
* ({@link ArgsValidationErrorKeys}.missingValue). When that error may suggest the argument after
|
|
510
|
+
* the option as its value, such as `--port=-5` for `--port -5`, a `parse` function of your own
|
|
511
|
+
* is not called to check it either: only the `parse` functions of `string()`, `number()`,
|
|
512
|
+
* `integer()`, `float()` and `choice()`, which have no side effects, are called for that.
|
|
513
|
+
* An explicit empty value, such as `--name=` or `-n=`, is passed as `''` unless
|
|
514
|
+
* `required: true` is set.
|
|
515
|
+
*
|
|
516
|
+
* An `enum` option with `choices` calls it only with one of them. Any other value, an explicit
|
|
517
|
+
* empty one included, is reported as `err:arg:invalid-choice`
|
|
518
|
+
* ({@link ArgsValidationErrorKeys}.invalidChoice), except that a required option reports an
|
|
519
|
+
* explicit empty value as required.
|
|
520
|
+
*
|
|
521
|
+
* @param value - Raw string value from command line
|
|
522
|
+
* @returns Parsed value of any type
|
|
523
|
+
* @throws {Error} Error or subclass when value is invalid
|
|
524
|
+
*
|
|
525
|
+
* @example
|
|
526
|
+
* Custom parsing functions:
|
|
527
|
+
* ```ts
|
|
528
|
+
* {
|
|
529
|
+
* config: {
|
|
530
|
+
* type: 'custom',
|
|
531
|
+
* parse: (value: string) => {
|
|
532
|
+
* try {
|
|
533
|
+
* return JSON.parse(value) // Parse JSON config
|
|
534
|
+
* } catch {
|
|
535
|
+
* throw new Error('Invalid JSON configuration')
|
|
536
|
+
* }
|
|
537
|
+
* },
|
|
538
|
+
* description: 'JSON configuration object'
|
|
539
|
+
* },
|
|
540
|
+
* date: {
|
|
541
|
+
* type: 'custom',
|
|
542
|
+
* parse: (value: string) => {
|
|
543
|
+
* const date = new Date(value)
|
|
544
|
+
* if (isNaN(date.getTime())) {
|
|
545
|
+
* throw new Error('Invalid date format')
|
|
546
|
+
* }
|
|
547
|
+
* return date
|
|
548
|
+
* }
|
|
549
|
+
* }
|
|
550
|
+
* }
|
|
551
|
+
* ```
|
|
552
|
+
*/
|
|
553
|
+
parse?: (value: string) => any;
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* An object that contains {@link ArgSchema | argument schema}.
|
|
557
|
+
*
|
|
558
|
+
* This type is used to define the structure and validation rules for command line arguments.
|
|
559
|
+
*/
|
|
560
|
+
interface Args$1 {
|
|
561
|
+
[option: string]: ArgSchema;
|
|
562
|
+
}
|
|
10
563
|
//#endregion
|
|
11
564
|
//#region ../shared/src/constants.d.ts
|
|
12
|
-
/**
|
|
13
|
-
* @author kazuya kawaguchi (a.k.a. kazupon)
|
|
14
|
-
* @license MIT
|
|
15
|
-
*/
|
|
16
565
|
/**
|
|
17
566
|
* @author kazuya kawaguchi (a.k.a. kazupon)
|
|
18
567
|
* @license MIT
|
|
@@ -35,36 +584,21 @@ type CommonArgType = {
|
|
|
35
584
|
};
|
|
36
585
|
declare const COMMON_ARGS: CommonArgType;
|
|
37
586
|
declare const COMMAND_BUILTIN_RESOURCE_KEYS: readonly ["USAGE", "COMMAND", "SUBCOMMAND", "COMMANDS", "ARGUMENTS", "OPTIONS", "EXAMPLES", "FORMORE", "NEGATABLE", "DEFAULT", "CHOICES"];
|
|
38
|
-
declare const ARG_ERROR_RESOURCE_KEYS: readonly ["err:arg:required-option", "err:arg:required-positional", "err:arg:invalid-type", "err:arg:invalid-choice", "err:arg:custom-parse", "err:arg:unknown-option"];
|
|
39
|
-
declare const
|
|
587
|
+
declare const ARG_ERROR_RESOURCE_KEYS: readonly ["err:arg:required-option", "err:arg:required-positional", "err:arg:invalid-type", "err:arg:invalid-choice", "err:arg:custom-parse", "err:arg:unknown-option", "err:arg:unexpected-value", "err:arg:missing-value", "err:arg:conflict", "err:arg:invalid-default"];
|
|
588
|
+
declare const ARG_ERROR_HINT_RESOURCE_KEYS: readonly ["err:arg:missing-value:hint"];
|
|
589
|
+
declare const COMMAND_ERROR_RESOURCE_KEYS: readonly ["err:cmd:not-found", "err:cmd:ambiguous", "err:cmd:inconsistent-options", "err:cmd:lazy-schema-mismatch"];
|
|
40
590
|
declare const SUGGESTION_ERROR_RESOURCE_KEYS: readonly ["err:suggestion:did-you-mean"];
|
|
41
|
-
//#endregion
|
|
42
|
-
//#region ../plugin-i18n/src/types.d.ts
|
|
43
|
-
|
|
44
|
-
//#endregion
|
|
45
|
-
//#region ../plugin-i18n/src/helpers.d.ts
|
|
46
|
-
|
|
47
|
-
//#endregion
|
|
48
|
-
//#region ../plugin-i18n/src/translation.d.ts
|
|
49
|
-
|
|
50
|
-
//#endregion
|
|
51
|
-
//#region ../gunshi/src/index.d.ts
|
|
52
|
-
|
|
53
591
|
//#endregion
|
|
54
592
|
//#region ../shared/src/types.d.ts
|
|
55
|
-
type RemoveIndexSignature<T> = { [K in keyof T as string extends K ? never : number extends K ? never : K]: T[K] };
|
|
56
|
-
/**
|
|
57
|
-
* Make all properties in T deeply writeable (not readonly)
|
|
58
|
-
*/
|
|
59
|
-
|
|
593
|
+
type RemoveIndexSignature<T> = { [K in keyof T as string extends K ? never : number extends K ? never : K]: T[K]; };
|
|
60
594
|
/**
|
|
61
595
|
* Remove index signature from object or record type.
|
|
62
596
|
*/
|
|
63
|
-
type RemovedIndex<T> = RemoveIndexSignature<{ [K in keyof T]: T[K] }>;
|
|
597
|
+
type RemovedIndex<T> = RemoveIndexSignature<{ [K in keyof T]: T[K]; }>;
|
|
64
598
|
/**
|
|
65
599
|
* Resolve a key on {@link Args}.
|
|
66
600
|
*/
|
|
67
|
-
type KeyOfArgs<A extends Args$1> = keyof A | { [K in keyof A]: A[K]['type'] extends 'boolean' ? A[K]['negatable'] extends true ? `no-${Extract<K, string>}` : never : never }[keyof A];
|
|
601
|
+
type KeyOfArgs<A extends Args$1> = keyof A | { [K in keyof A]: A[K]['type'] extends 'boolean' ? A[K]['negatable'] extends true ? `no-${Extract<K, string>}` : never : never; }[keyof A];
|
|
68
602
|
/**
|
|
69
603
|
* Generate a namespaced key.
|
|
70
604
|
*/
|
|
@@ -81,6 +615,10 @@ type CommandBuiltinResourceKeys = (typeof COMMAND_BUILTIN_RESOURCE_KEYS)[number]
|
|
|
81
615
|
* Args validation error resource keys.
|
|
82
616
|
*/
|
|
83
617
|
type ArgErrorResourceKeys = (typeof ARG_ERROR_RESOURCE_KEYS)[number];
|
|
618
|
+
/**
|
|
619
|
+
* Resource keys of the hints that follow an args validation error.
|
|
620
|
+
*/
|
|
621
|
+
type ArgErrorHintResourceKeys = (typeof ARG_ERROR_HINT_RESOURCE_KEYS)[number];
|
|
84
622
|
/**
|
|
85
623
|
* Command validation error resource keys.
|
|
86
624
|
*/
|
|
@@ -92,7 +630,7 @@ type SuggestionErrorResourceKeys = (typeof SUGGESTION_ERROR_RESOURCE_KEYS)[numbe
|
|
|
92
630
|
/**
|
|
93
631
|
* Error resource keys.
|
|
94
632
|
*/
|
|
95
|
-
type ErrorResourceKeys = ArgErrorResourceKeys | CommandErrorResourceKeys | SuggestionErrorResourceKeys;
|
|
633
|
+
type ErrorResourceKeys = ArgErrorResourceKeys | ArgErrorHintResourceKeys | CommandErrorResourceKeys | SuggestionErrorResourceKeys;
|
|
96
634
|
/**
|
|
97
635
|
* Command built-in keys.
|
|
98
636
|
*/
|
|
@@ -107,22 +645,11 @@ type CommandArgKeys<A extends Args$1, C = {}, K extends string = GenerateNamespa
|
|
|
107
645
|
/**
|
|
108
646
|
* Resolve translation keys for command context.
|
|
109
647
|
*/
|
|
110
|
-
type ResolveTranslationKeys<A extends Args$1, C = {}
|
|
111
|
-
// for
|
|
112
|
-
|
|
113
|
-
// for extended resources
|
|
114
|
-
R extends string = keyof RemovedIndex<E>, T extends string = (C extends {
|
|
648
|
+
type ResolveTranslationKeys<A extends Args$1, C = {} // for CommandContext
|
|
649
|
+
, E extends Record<string, string> = {} // for extended resources
|
|
650
|
+
, R extends string = keyof RemovedIndex<E>, T extends string = C extends {
|
|
115
651
|
name: infer N;
|
|
116
|
-
} ? N extends string ? GenerateNamespacedKey<R, N> : R : R | CommandBuiltinKeys
|
|
117
|
-
//#endregion
|
|
118
|
-
//#region ../shared/src/localization.d.ts
|
|
119
|
-
|
|
120
|
-
//#endregion
|
|
121
|
-
//#region ../shared/src/utils.d.ts
|
|
122
|
-
|
|
123
|
-
//#endregion
|
|
124
|
-
//#region ../shared/src/index.d.ts
|
|
125
|
-
|
|
652
|
+
} ? N extends string ? GenerateNamespacedKey<R, N> : R : R | CommandBuiltinKeys, O = CommandArgKeys<A, C>> = ErrorResourceKeys | CommandBuiltinKeys | O | T;
|
|
126
653
|
//#endregion
|
|
127
654
|
//#region src/types.d.ts
|
|
128
655
|
/**
|
|
@@ -151,19 +678,29 @@ interface UsageRendererExtension<G extends GunshiParams<any> = DefaultGunshiPara
|
|
|
151
678
|
* @param values - An optional record of values to interpolate into the translation string.
|
|
152
679
|
* @returns The resolved translation string with interpolated values if provided.
|
|
153
680
|
*/
|
|
154
|
-
text: <A extends Args = G['args'], C = {}
|
|
155
|
-
// for
|
|
156
|
-
E
|
|
157
|
-
// for extended resources
|
|
158
|
-
K = ResolveTranslationKeys<A, C, E>>(key: K, values?: Record<string, unknown>) => Promise<string>;
|
|
681
|
+
text: <A extends Args = G['args'], C = {} // for CommandContext
|
|
682
|
+
, E extends Record<string, string> = {} // for extended resources
|
|
683
|
+
, K = ResolveTranslationKeys<A, C, E>>(key: K, values?: Record<string, unknown>) => Promise<string>;
|
|
159
684
|
/**
|
|
160
685
|
* Load commands
|
|
161
686
|
*
|
|
162
687
|
* @typeParam G - A type extending {@link GunshiParams} to specify the shape of command parameters.
|
|
163
688
|
*
|
|
164
|
-
* @returns A list of commands loaded from the usage renderer plugin.
|
|
689
|
+
* @returns A list of commands loaded from the usage renderer plugin. At the top-level entry,
|
|
690
|
+
* the list includes the CLI entry command even when a plugin registered the other commands
|
|
691
|
+
* without adding the entry to `env.subCommands`.
|
|
165
692
|
*/
|
|
166
693
|
loadCommands: <G extends GunshiParams = DefaultGunshiParams>() => Promise<Command<G>[]>;
|
|
694
|
+
/**
|
|
695
|
+
* Resolve the description of a command in the command list.
|
|
696
|
+
*
|
|
697
|
+
* The description is localized when `@gunshi/plugin-i18n` is installed and the command has a
|
|
698
|
+
* resource of its own, and falls back to {@link Command.description} otherwise.
|
|
699
|
+
*
|
|
700
|
+
* @param cmd - A command from {@link UsageRendererExtension.loadCommands}.
|
|
701
|
+
* @returns The description to show for the command.
|
|
702
|
+
*/
|
|
703
|
+
localizeCommandDescription: (cmd: Command) => Promise<string>;
|
|
167
704
|
}
|
|
168
705
|
//#endregion
|
|
169
706
|
//#region src/header.d.ts
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ANONYMOUS_COMMAND_NAME, ArgsValidationErrorKeys, isArgsValidationError, isCommandNotFoundError, plugin } from "@gunshi/plugin";
|
|
2
|
-
//#region ../../node_modules/.pnpm/args-tokens@0.
|
|
1
|
+
import { ANONYMOUS_COMMAND_NAME, ArgsValidationErrorKeys, createCommandContext, isArgsValidationError, isCommandNotFoundError, isCommandResolutionError, plugin } from "@gunshi/plugin";
|
|
2
|
+
//#region ../../node_modules/.pnpm/args-tokens@1.0.0-beta.1/node_modules/args-tokens/lib/utils.js
|
|
3
3
|
/**
|
|
4
4
|
* Entry point of utils.
|
|
5
5
|
*
|
|
@@ -30,14 +30,20 @@ function kebabnize(str) {
|
|
|
30
30
|
* Check if the given command is a {@link LazyCommand}.
|
|
31
31
|
*
|
|
32
32
|
* @param cmd - A command to check
|
|
33
|
-
* @returns `true` if the command is a {@link LazyCommand}, otherwise `false
|
|
33
|
+
* @returns `true` if the command is a {@link LazyCommand}, otherwise `false`
|
|
34
|
+
*
|
|
35
|
+
* A lazy command is identified by its `commandName` property. The property may be
|
|
36
|
+
* `undefined` when the lazy command has no definition name.
|
|
34
37
|
*/
|
|
35
38
|
function isLazyCommand(cmd) {
|
|
36
|
-
return typeof cmd === "function" && "commandName" in cmd
|
|
39
|
+
return typeof cmd === "function" && "commandName" in cmd;
|
|
37
40
|
}
|
|
38
41
|
/**
|
|
39
42
|
* Resolve a lazy command to a {@link Command}.
|
|
40
43
|
*
|
|
44
|
+
* If the loader returns a command, the properties of the loaded command take precedence,
|
|
45
|
+
* and the properties that it does not define fall back to the definition that is given to `lazy`.
|
|
46
|
+
*
|
|
41
47
|
* @param cmd - A {@link Commandable} or {@link LazyCommand} to resolve
|
|
42
48
|
* @param name - Optional name of the command, if not provided, it will use the name from the command itself.
|
|
43
49
|
* @param needRunResolving - Whether to run the resolving function of the lazy command.
|
|
@@ -53,6 +59,8 @@ async function resolveLazyCommand(cmd, name, needRunResolving = false) {
|
|
|
53
59
|
examples: cmd.examples,
|
|
54
60
|
internal: cmd.internal,
|
|
55
61
|
entry: cmd.entry,
|
|
62
|
+
toKebab: cmd.toKebab,
|
|
63
|
+
rendering: cmd.rendering,
|
|
56
64
|
subCommands: cmd.subCommands
|
|
57
65
|
};
|
|
58
66
|
if ("resource" in cmd && cmd.resource) baseCommand.resource = cmd.resource;
|
|
@@ -62,13 +70,20 @@ async function resolveLazyCommand(cmd, name, needRunResolving = false) {
|
|
|
62
70
|
if (typeof loaded === "function") command.run = loaded;
|
|
63
71
|
else if (typeof loaded === "object") {
|
|
64
72
|
if (loaded.run == null) throw new TypeError(`'run' is required in command: ${cmd.name || name}`);
|
|
73
|
+
/**
|
|
74
|
+
* NOTE(kazupon): the loaded command wins, and the definition that is given to `lazy` is the fallback
|
|
75
|
+
* for what the loaded command does not define. A loader that returns `{ run }` keeps the whole definition,
|
|
76
|
+
* like a loader that returns the runner itself.
|
|
77
|
+
*/
|
|
65
78
|
command.run = loaded.run;
|
|
66
|
-
command.name = loaded.name;
|
|
67
|
-
command.description = loaded.description;
|
|
68
|
-
command.args = loaded.args;
|
|
69
|
-
command.examples = loaded.examples;
|
|
70
|
-
command.internal = loaded.internal;
|
|
71
|
-
command.entry = loaded.entry;
|
|
79
|
+
command.name = loaded.name ?? cmd.commandName;
|
|
80
|
+
command.description = loaded.description ?? cmd.description;
|
|
81
|
+
command.args = loaded.args ?? cmd.args;
|
|
82
|
+
command.examples = loaded.examples ?? cmd.examples;
|
|
83
|
+
command.internal = loaded.internal ?? cmd.internal;
|
|
84
|
+
command.entry = loaded.entry ?? cmd.entry;
|
|
85
|
+
command.toKebab = loaded.toKebab ?? cmd.toKebab;
|
|
86
|
+
command.rendering = loaded.rendering ?? cmd.rendering;
|
|
72
87
|
command.subCommands = loaded.subCommands || cmd.subCommands;
|
|
73
88
|
if ("resource" in loaded && loaded.resource) command.resource = loaded.resource;
|
|
74
89
|
} else throw new TypeError(`Cannot resolve command: ${cmd.name || name}`);
|
|
@@ -138,12 +153,20 @@ var en_US_default = {
|
|
|
138
153
|
help: "Display this help message",
|
|
139
154
|
version: "Display this version",
|
|
140
155
|
"err:arg:required-option": "Optional argument {$displayName} is required",
|
|
141
|
-
"err:arg:required-positional": "Positional argument {$
|
|
156
|
+
"err:arg:required-positional": "Positional argument {$displayName} is required",
|
|
142
157
|
"err:arg:invalid-type": "Invalid value for {$displayName}: expected {$expected}",
|
|
143
158
|
"err:arg:invalid-choice": "Invalid value for {$displayName}: expected one of {$choices}",
|
|
144
159
|
"err:arg:custom-parse": "Invalid value for {$displayName}: {$reason}",
|
|
145
160
|
"err:arg:unknown-option": "Unknown option: {$rawName}",
|
|
161
|
+
"err:arg:unexpected-value": "Optional argument {$displayName} does not take a value",
|
|
162
|
+
"err:arg:missing-value": "Optional argument {$displayName} requires a value",
|
|
163
|
+
"err:arg:missing-value:hint": "(to pass '{$next}' as its value, write '{$suggestion}')",
|
|
164
|
+
"err:arg:conflict": "{$displayName} conflicts with {$conflictDisplayName}",
|
|
165
|
+
"err:arg:invalid-default": "Invalid default for {$displayName}: {$actual} is not one of {$choices}",
|
|
146
166
|
"err:cmd:not-found": "Command not found: {$commandName}",
|
|
167
|
+
"err:cmd:ambiguous": "The command is ambiguous. Move options after the command name: {$candidatePaths}",
|
|
168
|
+
"err:cmd:inconsistent-options": "The options do not identify a consistent command. Move options after the command name.",
|
|
169
|
+
"err:cmd:lazy-schema-mismatch": "The lazy command definition does not match its loaded options. Declare leading options in the lazy definition or move options after the command name: {$commandPath}",
|
|
147
170
|
"err:suggestion:did-you-mean": "Did you mean {$name}?"
|
|
148
171
|
};
|
|
149
172
|
//#endregion
|
|
@@ -226,10 +249,36 @@ function namespacedId(id) {
|
|
|
226
249
|
* @returns A string representing the short and long option pair.
|
|
227
250
|
*/
|
|
228
251
|
function makeShortLongOptionPair(schema, name, toKebab) {
|
|
229
|
-
let key = `--${
|
|
252
|
+
let key = `--${resolveDisplayName(name, schema, toKebab)}`;
|
|
230
253
|
if (schema.short) key = `-${schema.short}, ${key}`;
|
|
231
254
|
return key;
|
|
232
255
|
}
|
|
256
|
+
/**
|
|
257
|
+
* Resolve the name that an argument is parsed and rendered under.
|
|
258
|
+
*
|
|
259
|
+
* @param name - The key of the argument.
|
|
260
|
+
* @param schema - The {@linkcode ArgSchema | argument schema}.
|
|
261
|
+
* @param toKebab - Whether to convert the name to kebab-case.
|
|
262
|
+
* @returns The name of the argument, kebab-cased when `toKebab` asks for it.
|
|
263
|
+
*/
|
|
264
|
+
function resolveDisplayName(name, schema, toKebab) {
|
|
265
|
+
return toKebab || schema.toKebab ? kebabnize(name) : name;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Collect the names that the arguments are parsed and rendered under.
|
|
269
|
+
*
|
|
270
|
+
* A positional argument is left out: it is never spelled as an option, so it cannot take the name of
|
|
271
|
+
* one. A `hidden` argument is counted, because it still owns the name that it parses.
|
|
272
|
+
*
|
|
273
|
+
* @param args - The {@linkcode Args | arguments} of a command, global options included.
|
|
274
|
+
* @param toKebab - Whether the command converts its names to kebab-case.
|
|
275
|
+
* @returns The set of names.
|
|
276
|
+
*/
|
|
277
|
+
function resolveOptionNames(args, toKebab) {
|
|
278
|
+
const names = /* @__PURE__ */ new Set();
|
|
279
|
+
for (const [name, schema] of Object.entries(args)) if (schema.type !== "positional") names.add(resolveDisplayName(name, schema, toKebab));
|
|
280
|
+
return names;
|
|
281
|
+
}
|
|
233
282
|
//#endregion
|
|
234
283
|
//#region ../shared/src/localization.ts
|
|
235
284
|
/**
|
|
@@ -257,15 +306,21 @@ function localizable(ctx, cmd, translate) {
|
|
|
257
306
|
if (key.startsWith(ERROR_PREFIX_AND_KEY_SEPARATOR)) return resolveAndFormatResource(key, key, values);
|
|
258
307
|
const namaspacedArgKey = resolveKey(ARG_PREFIX_AND_KEY_SEPARATOR, ctx.name);
|
|
259
308
|
if (key.startsWith(namaspacedArgKey)) {
|
|
260
|
-
|
|
261
|
-
|
|
309
|
+
const argKey = key.slice(namaspacedArgKey.length);
|
|
310
|
+
/**
|
|
311
|
+
* NOTE(kazupon): an argument describes itself, whatever it is called. `no-` is stripped only
|
|
312
|
+
* to reach the option that a negated form negates, and only when there is no argument under
|
|
313
|
+
* the name as written: `no-emoji` may be an argument of its own, and it then has nothing to do
|
|
314
|
+
* with `emoji`.
|
|
315
|
+
*/
|
|
316
|
+
const ownSchema = ctx.args[argKey];
|
|
317
|
+
if (ownSchema) return ownSchema.description || "";
|
|
262
318
|
if (argKey.startsWith("no-")) {
|
|
263
|
-
|
|
264
|
-
|
|
319
|
+
const negatedKey = argKey.slice(3);
|
|
320
|
+
const schema = ctx.args[negatedKey];
|
|
321
|
+
if (schema && schema.type === "boolean" && schema.negatable) return `${en_US_default["NEGATABLE"]} ${makeShortLongOptionPair(schema, negatedKey, ctx.toKebab)}`;
|
|
265
322
|
}
|
|
266
|
-
|
|
267
|
-
if (!schema) return argKey;
|
|
268
|
-
return negatable && schema.type === "boolean" && schema.negatable ? `${en_US_default["NEGATABLE"]} ${makeShortLongOptionPair(schema, argKey, ctx.toKebab)}` : schema.description || "";
|
|
323
|
+
return argKey;
|
|
269
324
|
}
|
|
270
325
|
if (key === resolveKey("description", ctx.name)) return "";
|
|
271
326
|
else if (key === resolveKey("examples", ctx.name)) return await resolveExamples$1(ctx, cmd.examples);
|
|
@@ -321,6 +376,63 @@ function renderHeader(ctx) {
|
|
|
321
376
|
*/
|
|
322
377
|
const pluginId = namespacedId("renderer");
|
|
323
378
|
//#endregion
|
|
379
|
+
//#region src/width.ts
|
|
380
|
+
/**
|
|
381
|
+
* @author kazuya kawaguchi (a.k.a. kazupon)
|
|
382
|
+
* @license MIT
|
|
383
|
+
*/
|
|
384
|
+
/**
|
|
385
|
+
* NOTE(kazupon): the columns of the usage are aligned by padding, and padding by `String#length`
|
|
386
|
+
* counts UTF-16 code units, which is not what a terminal draws: a full-width character takes two
|
|
387
|
+
* columns, a combining mark takes none, and a joined emoji takes two however many code points it is
|
|
388
|
+
* written with. The text is walked by grapheme, so that a mark or a joiner is part of the character
|
|
389
|
+
* it belongs to, and each grapheme is measured by the code point it starts with.
|
|
390
|
+
*
|
|
391
|
+
* JavaScript has no `\p{East_Asian_Width=Wide}` escape. Wide columns are those the engine already
|
|
392
|
+
* classifies as Han / Hiragana / Katakana / Hangul or emoji-presentation, plus the fullwidth slice
|
|
393
|
+
* of Halfwidth and Fullwidth Forms. That covers CLI names; the rest of the East Asian Width table
|
|
394
|
+
* is symbols that do not appear there. Unicode updates then come with the runtime.
|
|
395
|
+
*/
|
|
396
|
+
const WIDE = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}\p{Emoji_Presentation}]/u;
|
|
397
|
+
const ZERO_WIDTH = /[\p{Default_Ignorable_Code_Point}\p{Mn}]/u;
|
|
398
|
+
function graphemeWidth(codePoint) {
|
|
399
|
+
const ch = String.fromCodePoint(codePoint);
|
|
400
|
+
if (ZERO_WIDTH.test(ch)) return 0;
|
|
401
|
+
/**
|
|
402
|
+
* Halfwidth and Fullwidth Forms: only the fullwidth slices are two columns. The rest of the
|
|
403
|
+
* block is halfwidth katakana / hangul, which `\p{Script=Katakana}` and `\p{Script=Hangul}`
|
|
404
|
+
* would otherwise count as wide.
|
|
405
|
+
*/
|
|
406
|
+
if (codePoint >= 65280 && codePoint <= 65519) {
|
|
407
|
+
if (codePoint >= 65281 && codePoint <= 65376 || codePoint >= 65504 && codePoint <= 65510) return 2;
|
|
408
|
+
return 1;
|
|
409
|
+
}
|
|
410
|
+
return WIDE.test(ch) ? 2 : 1;
|
|
411
|
+
}
|
|
412
|
+
const segmenter = new Intl.Segmenter();
|
|
413
|
+
/**
|
|
414
|
+
* Measure the columns a terminal draws a string in.
|
|
415
|
+
*
|
|
416
|
+
* @param value - A string to measure
|
|
417
|
+
* @returns The number of columns
|
|
418
|
+
*/
|
|
419
|
+
function displayWidth(value) {
|
|
420
|
+
let width = 0;
|
|
421
|
+
for (const { segment } of segmenter.segment(value)) width += graphemeWidth(segment.codePointAt(0));
|
|
422
|
+
return width;
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Pad a string with spaces until it is drawn in the given number of columns.
|
|
426
|
+
*
|
|
427
|
+
* @param value - A string to pad
|
|
428
|
+
* @param width - The number of columns to pad it to
|
|
429
|
+
* @returns The padded string
|
|
430
|
+
*/
|
|
431
|
+
function padEndToWidth(value, width) {
|
|
432
|
+
const padding = width - displayWidth(value);
|
|
433
|
+
return padding > 0 ? value + " ".repeat(padding) : value;
|
|
434
|
+
}
|
|
435
|
+
//#endregion
|
|
324
436
|
//#region src/usage.ts
|
|
325
437
|
/**
|
|
326
438
|
* @author kazuya kawaguchi (a.k.a. kazupon)
|
|
@@ -331,10 +443,10 @@ function isHiddenArg(schema) {
|
|
|
331
443
|
return schema.hidden === true;
|
|
332
444
|
}
|
|
333
445
|
function getVisibleOptionalArgs(args) {
|
|
334
|
-
return Object.entries(args).filter(([
|
|
446
|
+
return Object.entries(args).filter(([, schema]) => schema.type !== "positional" && !isHiddenArg(schema));
|
|
335
447
|
}
|
|
336
448
|
function getVisiblePositionalArgs(args) {
|
|
337
|
-
return Object.entries(args).filter(([
|
|
449
|
+
return Object.entries(args).filter(([, schema]) => schema.type === "positional" && !isHiddenArg(schema));
|
|
338
450
|
}
|
|
339
451
|
/**
|
|
340
452
|
* Render the usage.
|
|
@@ -428,18 +540,21 @@ async function makeUsageSymbols(ctx) {
|
|
|
428
540
|
async function renderCommandsSection(ctx) {
|
|
429
541
|
const messages = [`${await ctx.extensions[pluginId].text(resolveBuiltInKey("COMMANDS"))}:`];
|
|
430
542
|
const loadedCommands = await ctx.extensions?.[pluginId].loadCommands() || [];
|
|
431
|
-
const
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
const
|
|
435
|
-
const
|
|
436
|
-
const commandStr = await makeCommandSymbol(ctx, cmd);
|
|
437
|
-
const symbolLength = desc.length > 0 ? commandMaxLength + optionSymbol.length + positionalSymbol.length : 0;
|
|
438
|
-
const command = `${commandStr.padEnd(symbolLength + ctx.env.middleMargin)}${desc}`;
|
|
543
|
+
const commandSymbols = await Promise.all(loadedCommands.map(async (cmd) => await makeCommandSymbol(ctx, cmd)));
|
|
544
|
+
const symbolMaxLength = Math.max(...commandSymbols.map((symbol) => displayWidth(symbol)));
|
|
545
|
+
const commandsStr = (await Promise.all(loadedCommands.map(async (cmd) => await ctx.extensions[pluginId].localizeCommandDescription(cmd)))).map((desc, index) => {
|
|
546
|
+
const commandStr = commandSymbols[index];
|
|
547
|
+
const command = desc ? `${padEndToWidth(commandStr, symbolMaxLength + ctx.env.middleMargin)}${desc}` : commandStr;
|
|
439
548
|
return `${command.padStart(ctx.env.leftMargin + command.length)}`;
|
|
440
|
-
})
|
|
549
|
+
});
|
|
441
550
|
messages.push(...commandsStr, "", `${await ctx.extensions[pluginId].text(resolveBuiltInKey("FORMORE"))}:`);
|
|
442
|
-
|
|
551
|
+
/**
|
|
552
|
+
* NOTE(kazupon): the CLI is named the same way the usage line and the entry row of the section
|
|
553
|
+
* above name it, because `CliOptions.name` is optional and the block would otherwise invite the
|
|
554
|
+
* reader to run `undefined --help`.
|
|
555
|
+
*/
|
|
556
|
+
const entry = await resolveEntry(ctx);
|
|
557
|
+
const basePath = ctx.commandPath && ctx.commandPath.length > 0 ? `${entry} ${ctx.commandPath.join(" ")}` : entry;
|
|
443
558
|
messages.push(...loadedCommands.map((cmd) => {
|
|
444
559
|
let commandStr = cmd.entry ? "" : cmd.name || "";
|
|
445
560
|
if (commandStr) commandStr += " ";
|
|
@@ -449,9 +564,15 @@ async function renderCommandsSection(ctx) {
|
|
|
449
564
|
return messages;
|
|
450
565
|
}
|
|
451
566
|
async function makeCommandSymbol(ctx, cmd) {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
567
|
+
/**
|
|
568
|
+
* NOTE(kazupon): the row describes `cmd`, so its symbols come from what `cmd` declares.
|
|
569
|
+
* A lazy command is described by the definition that is given to `lazy`, since the command
|
|
570
|
+
* list does not run the loader.
|
|
571
|
+
*/
|
|
572
|
+
const args = cmd.args || {};
|
|
573
|
+
const optionSymbol = await generateOptionsSymbols(ctx, args);
|
|
574
|
+
const positionalSymbol = generatePositionalSymbols(args);
|
|
575
|
+
let commandStr = cmd.entry ? cmd.name === void 0 || cmd.name === ANONYMOUS_COMMAND_NAME ? `[${await resolveEntry(ctx)}]` : `[${cmd.name}]` : cmd.name || "";
|
|
455
576
|
if (optionSymbol) {
|
|
456
577
|
if (commandStr) commandStr += " ";
|
|
457
578
|
commandStr += `${optionSymbol}`;
|
|
@@ -537,7 +658,11 @@ function hasPositionalArgs(args) {
|
|
|
537
658
|
*/
|
|
538
659
|
function hasAllDefaultOptions(args) {
|
|
539
660
|
const visibleOptionalArgs = getVisibleOptionalArgs(args);
|
|
540
|
-
|
|
661
|
+
/**
|
|
662
|
+
* NOTE(kazupon): an argument has a default when one is declared, whatever its value. `0`, `''` and
|
|
663
|
+
* `false` are defaults like any other, and an option that has one is not required (#750).
|
|
664
|
+
*/
|
|
665
|
+
return visibleOptionalArgs.length > 0 && visibleOptionalArgs.every(([, arg]) => arg.default !== void 0);
|
|
541
666
|
}
|
|
542
667
|
/**
|
|
543
668
|
* Generate options symbols for usage
|
|
@@ -556,23 +681,32 @@ async function generateOptionsSymbols(ctx, args) {
|
|
|
556
681
|
* @returns Options pairs for usage
|
|
557
682
|
*/
|
|
558
683
|
function getOptionalArgsPairs(ctx) {
|
|
684
|
+
/**
|
|
685
|
+
* NOTE(kazupon): an argument owns the name that it renders under, so the negated form of another
|
|
686
|
+
* option must not be listed under it (#732). The names are compared as they are rendered, because
|
|
687
|
+
* `toKebab` can bring two different keys under one name (#743). A `hidden` argument counts: it is
|
|
688
|
+
* not listed, and neither is a negated form that would take its name.
|
|
689
|
+
*/
|
|
690
|
+
const optionNames = resolveOptionNames(ctx.args, ctx.toKebab);
|
|
559
691
|
return getVisibleOptionalArgs(ctx.args).reduce((acc, [name, schema]) => {
|
|
692
|
+
const displayName = resolveDisplayName(name, schema, ctx.toKebab);
|
|
560
693
|
let key = makeShortLongOptionPair(schema, name, ctx.toKebab);
|
|
561
|
-
if (schema.type !== "boolean") {
|
|
562
|
-
const displayName = ctx.toKebab || schema.toKebab ? kebabnize(name) : name;
|
|
563
|
-
key = schema.default ? `${key} [${displayName}]` : `${key} <${displayName}>`;
|
|
564
|
-
}
|
|
694
|
+
if (schema.type !== "boolean") key = schema.default === void 0 ? `${key} <${displayName}>` : `${key} [${displayName}]`;
|
|
565
695
|
acc[name] = key;
|
|
566
696
|
if (schema.type === "boolean" && schema.negatable && !COMMON_ARGS_KEYS.includes(name)) {
|
|
567
|
-
const
|
|
568
|
-
acc[`no-${name}`] =
|
|
697
|
+
const negatedName = `no-${displayName}`;
|
|
698
|
+
if (!optionNames.has(negatedName)) acc[`no-${name}`] = `--${negatedName}`;
|
|
569
699
|
}
|
|
570
700
|
return acc;
|
|
571
701
|
}, Object.create(null));
|
|
572
702
|
}
|
|
573
|
-
const resolveNegatableKey = (key) => key.
|
|
703
|
+
const resolveNegatableKey = (key) => key.slice(3);
|
|
574
704
|
function resolveNegatableType(key, ctx) {
|
|
575
|
-
|
|
705
|
+
/**
|
|
706
|
+
* NOTE(kazupon): the key of an argument wins over the option that a negated form negates, and a
|
|
707
|
+
* negated form is a boolean flag whether or not the option it negates can be reached from here.
|
|
708
|
+
*/
|
|
709
|
+
return (ctx.args[key] ?? ctx.args[resolveNegatableKey(key)])?.type ?? "boolean";
|
|
576
710
|
}
|
|
577
711
|
async function generateDefaultDisplayValue(ctx, schema) {
|
|
578
712
|
return `${await ctx.extensions[pluginId].text(resolveBuiltInKey("DEFAULT"))}: ${schema.default}`;
|
|
@@ -598,21 +732,26 @@ async function resolveDisplayValue(ctx, key) {
|
|
|
598
732
|
async function generateOptionalArgsUsage(ctx, optionsPairs) {
|
|
599
733
|
const optionsPairsEntries = Object.entries(optionsPairs);
|
|
600
734
|
if (optionsPairsEntries.length === 0) return "";
|
|
601
|
-
const optionsMaxLength = Math.max(...optionsPairsEntries.map(([
|
|
602
|
-
const optionSchemaMaxLength = ctx.env.usageOptionType ? Math.max(...optionsPairsEntries.map(([key]) => resolveNegatableType(key, ctx)
|
|
735
|
+
const optionsMaxLength = Math.max(...optionsPairsEntries.map(([, value]) => displayWidth(value)));
|
|
736
|
+
const optionSchemaMaxLength = ctx.env.usageOptionType ? Math.max(...optionsPairsEntries.map(([key]) => displayWidth(resolveNegatableType(key, ctx)))) : 0;
|
|
603
737
|
return (await Promise.all(optionsPairsEntries.map(async ([key, value]) => {
|
|
604
738
|
let rawDesc = await ctx.extensions[pluginId].text(resolveArgKey(key, ctx.name));
|
|
605
|
-
if (!rawDesc && key.startsWith("no-")) {
|
|
739
|
+
if (!rawDesc && !ctx.args[key] && key.startsWith("no-")) {
|
|
740
|
+
/**
|
|
741
|
+
* NOTE(kazupon): only a key that no argument answers to may be read as the negated form of
|
|
742
|
+
* another option, and only when that option is there to be negated.
|
|
743
|
+
*/
|
|
606
744
|
const name = resolveNegatableKey(key);
|
|
607
745
|
const schema = ctx.args[name];
|
|
608
|
-
|
|
609
|
-
|
|
746
|
+
if (schema) {
|
|
747
|
+
const optionKey = makeShortLongOptionPair(schema, name, ctx.toKebab);
|
|
748
|
+
rawDesc = `${await ctx.extensions[pluginId].text(resolveBuiltInKey("NEGATABLE"))} ${optionKey}`;
|
|
749
|
+
}
|
|
610
750
|
}
|
|
611
751
|
const optionsSchema = ctx.env.usageOptionType ? `[${resolveNegatableType(key, ctx)}] ` : "";
|
|
612
752
|
const valueDesc = key.startsWith("no-") ? "" : await resolveDisplayValue(ctx, key);
|
|
613
|
-
const desc = `${optionsSchema ? optionsSchema
|
|
614
|
-
const
|
|
615
|
-
const option = `${value.padEnd((descLength > 0 ? optionsMaxLength : 0) + ctx.env.middleMargin)}${desc}${valueDesc ? ` ${valueDesc}` : ""}`;
|
|
753
|
+
const desc = `${optionsSchema ? padEndToWidth(optionsSchema, optionSchemaMaxLength + 3) : ""}${rawDesc}`;
|
|
754
|
+
const option = `${padEndToWidth(value, (desc.length + valueDesc.length > 0 ? optionsMaxLength : 0) + ctx.env.middleMargin)}${desc}${valueDesc ? ` ${valueDesc}` : ""}`;
|
|
616
755
|
return `${option.padStart(ctx.env.leftMargin + option.length)}`;
|
|
617
756
|
}))).join("\n");
|
|
618
757
|
}
|
|
@@ -622,10 +761,10 @@ function getPositionalArgs(args) {
|
|
|
622
761
|
async function generatePositionalArgsUsage(ctx) {
|
|
623
762
|
const positionals = getPositionalArgs(ctx.args);
|
|
624
763
|
if (positionals.length === 0) return "";
|
|
625
|
-
const argsMaxLength = Math.max(...positionals.map(([name]) => name
|
|
764
|
+
const argsMaxLength = Math.max(...positionals.map(([name]) => displayWidth(name)));
|
|
626
765
|
return (await Promise.all(positionals.map(async ([name]) => {
|
|
627
766
|
const desc = await ctx.extensions[pluginId].text(resolveArgKey(name, ctx.name)) || ctx.args[name].description || "";
|
|
628
|
-
const arg = `${name
|
|
767
|
+
const arg = `${padEndToWidth(name, argsMaxLength + ctx.env.middleMargin)} ${desc}`;
|
|
629
768
|
return `${arg.padStart(ctx.env.leftMargin + arg.length)}`;
|
|
630
769
|
}))).join("\n");
|
|
631
770
|
}
|
|
@@ -652,6 +791,7 @@ function isRequiredSinglePositionalArg(arg) {
|
|
|
652
791
|
* @license MIT
|
|
653
792
|
*/
|
|
654
793
|
const i18nPluginId$1 = namespacedId("i18n");
|
|
794
|
+
const missingValueHintKey = "err:arg:missing-value:hint";
|
|
655
795
|
/**
|
|
656
796
|
* Render the validation errors.
|
|
657
797
|
*
|
|
@@ -669,13 +809,38 @@ async function renderValidationError(ctx, error) {
|
|
|
669
809
|
const message = await localize(ctx, error.code, error.values);
|
|
670
810
|
if (message && message !== error.code) return message;
|
|
671
811
|
}
|
|
812
|
+
if (isCommandResolutionError(error) && error.code) {
|
|
813
|
+
const message = await localize(ctx, error.code, error.values);
|
|
814
|
+
if (message && message !== error.code) return message;
|
|
815
|
+
}
|
|
672
816
|
if (isArgsValidationError(error) && error.code) {
|
|
673
817
|
const values = await resolveValidationValues(ctx, error);
|
|
674
818
|
const message = await localize(ctx, error.code, values);
|
|
675
|
-
if (message && message !== error.code)
|
|
819
|
+
if (message && message !== error.code) {
|
|
820
|
+
const hint = await localizeHint(ctx, error, values);
|
|
821
|
+
return hint ? `${message} ${hint}` : message;
|
|
822
|
+
}
|
|
676
823
|
}
|
|
677
824
|
return error.message;
|
|
678
825
|
}
|
|
826
|
+
/**
|
|
827
|
+
* Localize the hint that args-tokens adds to the message of an error.
|
|
828
|
+
*
|
|
829
|
+
* A translation replaces the whole message, hint included, so the hint is translated by a key of
|
|
830
|
+
* its own and follows the translated error. args-tokens gives one today: for an option given without
|
|
831
|
+
* a value, when the argument after it may be the value, the long form with `=` (`--port=-5` for
|
|
832
|
+
* `--port -5`), in `values.next` and `values.suggestion`.
|
|
833
|
+
*
|
|
834
|
+
* @param ctx - A {@link CommandContext | command context}
|
|
835
|
+
* @param error - An args validation error
|
|
836
|
+
* @param values - The values of the error, as given to its translation
|
|
837
|
+
* @returns The localized hint, or `undefined` when the error has none.
|
|
838
|
+
*/
|
|
839
|
+
async function localizeHint(ctx, error, values) {
|
|
840
|
+
if (error.code !== ArgsValidationErrorKeys.missingValue || typeof values.next !== "string" || typeof values.suggestion !== "string") return;
|
|
841
|
+
const hint = await localize(ctx, missingValueHintKey, values);
|
|
842
|
+
return hint && hint !== missingValueHintKey ? hint : void 0;
|
|
843
|
+
}
|
|
679
844
|
async function resolveValidationValues(ctx, error) {
|
|
680
845
|
if (error.code !== ArgsValidationErrorKeys.customParse) return error.values;
|
|
681
846
|
const { reasonKey, reasonValues } = error.values;
|
|
@@ -748,7 +913,10 @@ function renderer() {
|
|
|
748
913
|
async function loadCommands() {
|
|
749
914
|
if (cachedCommands) return cachedCommands;
|
|
750
915
|
const subCommands = [...ctx.env.subCommands || []];
|
|
751
|
-
|
|
916
|
+
const allCommands = await Promise.all(subCommands.map(async ([name, cmd]) => await resolveLazyCommand(cmd, name)));
|
|
917
|
+
const entryCommand = ctx.env.entryCommand;
|
|
918
|
+
if (ctx.callMode === "entry" && subCommands.length > 0 && entryCommand && !subCommands.some(([, command]) => command === entryCommand)) allCommands.push(await resolveLazyCommand(entryCommand, ANONYMOUS_COMMAND_NAME));
|
|
919
|
+
cachedCommands = allCommands.filter((cmd) => !cmd.internal).filter(Boolean);
|
|
752
920
|
cachedCommands.sort((a, b) => {
|
|
753
921
|
if (a.entry && !b.entry) return -1;
|
|
754
922
|
if (!a.entry && b.entry) return 1;
|
|
@@ -759,9 +927,33 @@ function renderer() {
|
|
|
759
927
|
});
|
|
760
928
|
return cachedCommands;
|
|
761
929
|
}
|
|
930
|
+
/**
|
|
931
|
+
* Resolve the description of a command in the command list.
|
|
932
|
+
*
|
|
933
|
+
* NOTE(kazupon): the description is localized the way the command's own help localizes it,
|
|
934
|
+
* which needs that command's resource. `text()` is bound to the command that is running, so it
|
|
935
|
+
* cannot answer for another one. The i18n plugin loads a resource into the adapter under the
|
|
936
|
+
* name of the command it is given, and `translate()` reads it back from there (#748).
|
|
937
|
+
*
|
|
938
|
+
* @param target - A command from `loadCommands`
|
|
939
|
+
* @returns The description to show for the command
|
|
940
|
+
*/
|
|
941
|
+
async function localizeCommandDescription(target) {
|
|
942
|
+
const fallback = target.description || "";
|
|
943
|
+
if (!i18n || !target.name) return fallback;
|
|
944
|
+
const commandCtx = await createCommandContext({
|
|
945
|
+
args: target.args,
|
|
946
|
+
command: target,
|
|
947
|
+
callMode: target.entry ? "entry" : "subCommand",
|
|
948
|
+
extensions: {}
|
|
949
|
+
});
|
|
950
|
+
await i18n.loadResource(i18n.locale, commandCtx, target);
|
|
951
|
+
return await i18n.translate(resolveKey("description", target.name)) || fallback;
|
|
952
|
+
}
|
|
762
953
|
return {
|
|
763
954
|
text: localizable(ctx, cmd, i18n?.translate),
|
|
764
|
-
loadCommands
|
|
955
|
+
loadCommands,
|
|
956
|
+
localizeCommandDescription
|
|
765
957
|
};
|
|
766
958
|
},
|
|
767
959
|
setup: (ctx) => {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gunshi/plugin-renderer",
|
|
3
3
|
"description": "usage renderer plugin for gunshi",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "1.0.0-beta.1",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "kazuya kawaguchi",
|
|
7
7
|
"email": "kawakazu80@gmail.com"
|
|
@@ -53,18 +53,18 @@
|
|
|
53
53
|
}
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@gunshi/plugin": "0.
|
|
56
|
+
"@gunshi/plugin": "1.0.0-beta.1"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
|
-
"@gunshi/plugin-i18n": "0.
|
|
59
|
+
"@gunshi/plugin-i18n": "1.0.0-beta.1"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
|
+
"@gunshi/shared": "1.0.0-beta.1",
|
|
62
63
|
"deno": "^2.7.14",
|
|
63
64
|
"jsr": "^0.14.3",
|
|
64
65
|
"jsr-exports-lint": "^0.4.2",
|
|
65
66
|
"publint": "^0.3.20",
|
|
66
|
-
"tsdown": "0.
|
|
67
|
-
"@gunshi/shared": "0.37.3"
|
|
67
|
+
"tsdown": "0.22.14"
|
|
68
68
|
},
|
|
69
69
|
"scripts": {
|
|
70
70
|
"build": "tsdown",
|