cli-nano 1.1.2 → 1.2.0
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 +78 -46
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +53 -12
- package/dist/interfaces.d.ts +29 -14
- package/dist/interfaces.d.ts.map +1 -1
- package/package.json +4 -3
- package/src/index.ts +328 -0
- package/src/interfaces.ts +136 -0
package/README.md
CHANGED
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
|
|
10
10
|
## cli-nano
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
Small library to create command-line tool (aka CLI) which is quite similar to [`Yargs`](https://github.com/yargs/yargs), it is as configurable as Yargs but is a fraction of its size. The library is also inspired by NodeJS `parseArgs()` but is a lot more configurable in order to get what we would expect from a more complete CLI builder tool.
|
|
13
13
|
|
|
14
14
|
### Features
|
|
15
15
|
- Parses arguments
|
|
16
|
-
- Supports defining Positional arguments
|
|
16
|
+
- Supports defining Positional (input) arguments
|
|
17
17
|
- Supports Variadic args (1 or more positional args)
|
|
18
18
|
- Automatically converts flags to camelCase to match config options
|
|
19
19
|
- accepts both `--camelCase` and `--kebab-case`
|
|
@@ -22,6 +22,7 @@ Simple library to create command-line tool (aka CLI) which is quite similar to [
|
|
|
22
22
|
- Outputs description and supplied help text by using `--help`
|
|
23
23
|
- Supports defining `required` options
|
|
24
24
|
- Supports `default` values
|
|
25
|
+
- Supports `group` for grouping command options in help
|
|
25
26
|
- No dependencies!
|
|
26
27
|
|
|
27
28
|
### Install
|
|
@@ -39,11 +40,18 @@ import { type Config, parseArgs } from 'cli-nano';
|
|
|
39
40
|
const config: Config = {
|
|
40
41
|
command: {
|
|
41
42
|
name: 'serve',
|
|
42
|
-
|
|
43
|
+
describe: 'Start a server with the given options',
|
|
44
|
+
examples: [
|
|
45
|
+
{ cmd: '$0 ./www/index.html 8080 --open', describe: 'Start web server on port 8080 and open browser' },
|
|
46
|
+
{
|
|
47
|
+
cmd: '$0 ./index.html 8081 --no-open --verbose',
|
|
48
|
+
describe: 'Start web server on port 8081 without opening browser and print more debugging logging to the console',
|
|
49
|
+
},
|
|
50
|
+
],
|
|
43
51
|
positionals: [
|
|
44
52
|
{
|
|
45
53
|
name: 'input',
|
|
46
|
-
|
|
54
|
+
describe: 'serving files or directory',
|
|
47
55
|
type: 'string',
|
|
48
56
|
variadic: true, // 1 or more
|
|
49
57
|
required: true,
|
|
@@ -51,62 +59,77 @@ const config: Config = {
|
|
|
51
59
|
{
|
|
52
60
|
name: 'port',
|
|
53
61
|
type: 'number',
|
|
54
|
-
|
|
62
|
+
describe: 'port to bind on',
|
|
55
63
|
required: false,
|
|
56
64
|
default: 5000, // optional default value
|
|
57
|
-
},
|
|
65
|
+
},
|
|
58
66
|
],
|
|
59
67
|
},
|
|
60
68
|
options: {
|
|
61
69
|
dryRun: {
|
|
62
70
|
alias: 'd',
|
|
63
71
|
type: 'boolean',
|
|
64
|
-
|
|
72
|
+
describe: 'Show what would be done, but do not actually start the server',
|
|
65
73
|
default: false, // optional default value
|
|
66
74
|
},
|
|
75
|
+
display: {
|
|
76
|
+
group: 'Advanced Options',
|
|
77
|
+
alias: 'D',
|
|
78
|
+
required: true,
|
|
79
|
+
type: 'boolean',
|
|
80
|
+
describe: 'a required display option',
|
|
81
|
+
},
|
|
67
82
|
exclude: {
|
|
68
83
|
alias: 'e',
|
|
69
84
|
type: 'array',
|
|
70
|
-
|
|
71
|
-
},
|
|
72
|
-
rainbow: {
|
|
73
|
-
type: 'boolean',
|
|
74
|
-
alias: 'r',
|
|
75
|
-
description: 'Enable rainbow mode',
|
|
76
|
-
default: true,
|
|
85
|
+
describe: 'pattern or glob to exclude (may be passed multiple times)',
|
|
77
86
|
},
|
|
78
87
|
verbose: {
|
|
79
88
|
alias: 'V',
|
|
80
89
|
type: 'boolean',
|
|
81
|
-
|
|
90
|
+
describe: 'print more information to console',
|
|
91
|
+
},
|
|
92
|
+
open: {
|
|
93
|
+
alias: 'o',
|
|
94
|
+
type: 'boolean',
|
|
95
|
+
describe: 'open browser when starting server',
|
|
96
|
+
default: true,
|
|
82
97
|
},
|
|
83
|
-
|
|
98
|
+
cache: {
|
|
84
99
|
type: 'number',
|
|
85
|
-
|
|
86
|
-
default:
|
|
100
|
+
describe: 'Set cache time (in seconds) for cache-control max-age header',
|
|
101
|
+
default: 3600,
|
|
87
102
|
},
|
|
88
|
-
|
|
89
|
-
|
|
103
|
+
address: {
|
|
104
|
+
type: 'string',
|
|
105
|
+
describe: 'Address to use',
|
|
90
106
|
required: true,
|
|
107
|
+
},
|
|
108
|
+
rainbow: {
|
|
109
|
+
group: 'Advanced Options',
|
|
91
110
|
type: 'boolean',
|
|
92
|
-
|
|
93
|
-
|
|
111
|
+
alias: 'r',
|
|
112
|
+
describe: 'Enable rainbow mode',
|
|
113
|
+
default: true,
|
|
114
|
+
},
|
|
94
115
|
},
|
|
95
116
|
version: '0.1.6',
|
|
96
|
-
|
|
97
|
-
|
|
117
|
+
helpFlagCasing: 'camel', // show help flag option in which casing (camel/kebab) (defaults to 'kebab')
|
|
118
|
+
helpDescMinLength: 40, // min description length shown in help (defaults to 50)
|
|
119
|
+
helpDescMaxLength: 120, // max description length shown in help (defaults to 100), will show ellipsis (...) when greater
|
|
98
120
|
};
|
|
99
121
|
|
|
100
122
|
const args = parseArgs(config);
|
|
101
123
|
console.log(args);
|
|
102
124
|
|
|
103
|
-
// do something with
|
|
104
|
-
//
|
|
125
|
+
// do something with parsed arguments, for example
|
|
126
|
+
// const { input, port, open } = args;
|
|
127
|
+
// startServer({ input, port, open });
|
|
105
128
|
```
|
|
106
129
|
|
|
107
130
|
### Usage with Type Inference
|
|
108
131
|
|
|
109
|
-
For full TypeScript auto-inference and
|
|
132
|
+
For full TypeScript auto-inference and intelliSense of parsed arguments, define your config as a `const` and cast it `as const`:
|
|
110
133
|
|
|
111
134
|
```ts
|
|
112
135
|
const config = {
|
|
@@ -124,10 +147,10 @@ args.display; // boolean (required)
|
|
|
124
147
|
|
|
125
148
|
> **Tip:**
|
|
126
149
|
> Using `as const` preserves literal types and tuple information, so TypeScript can infer required/optional fields and argument types automatically.
|
|
127
|
-
> If you use `const config: Config = { ... }`, you get type checking but not full
|
|
150
|
+
> If you use `const config: Config = { ... }`, you get type checking but not full intelliSense for parsed arguments.
|
|
128
151
|
|
|
129
152
|
> [!NOTE]
|
|
130
|
-
> For required+variadic positionals, the type is `[string, ...string[]]` (at least one value required). For optional variadic, it's string[]
|
|
153
|
+
> For required+variadic positionals, the type is `[string, ...string[]]` (at least one value required). For optional variadic, it's `string[]`. For non-variadic, it's `string`.
|
|
131
154
|
|
|
132
155
|
#### Example CLI Calls
|
|
133
156
|
|
|
@@ -144,13 +167,13 @@ serve dist/index.html
|
|
|
144
167
|
# With required and optional positionals
|
|
145
168
|
serve index1.html index2.html 8080 -D value
|
|
146
169
|
|
|
147
|
-
# With boolean and array options
|
|
170
|
+
# With boolean and array options entered as camelCase (kebab-case works too)
|
|
148
171
|
serve index.html 7000 --dryRun --exclude pattern1 --exclude pattern2 -D value
|
|
149
172
|
|
|
150
|
-
# With negated boolean
|
|
173
|
+
# With negated boolean entered as kebab-case
|
|
151
174
|
serve index.html 7000 --no-dryRun -D value
|
|
152
175
|
|
|
153
|
-
# With short aliases
|
|
176
|
+
# With short aliases (case sensitive)
|
|
154
177
|
serve index.html 7000 -d -e pattern1 -e pattern2 -D value
|
|
155
178
|
|
|
156
179
|
# With number option
|
|
@@ -159,7 +182,7 @@ serve index.html 7000 --up 2 -D value
|
|
|
159
182
|
|
|
160
183
|
#### Notes
|
|
161
184
|
|
|
162
|
-
- **Default values**: Use the `default` property in an option or positional argument to specify a value
|
|
185
|
+
- **Default values**: Use the `default` property in an option or positional argument to specify a value when the user does not provide one.
|
|
163
186
|
- Example for option: `{ type: 'boolean', default: false }`
|
|
164
187
|
- Example for positional: `{ name: 'port', type: 'number', default: 5000 }`
|
|
165
188
|
- **Variadic positionals**: Use `variadic: true` for arguments that accept multiple values.
|
|
@@ -167,6 +190,7 @@ serve index.html 7000 --up 2 -D value
|
|
|
167
190
|
- **Negated booleans**: Use `--no-flag` to set a boolean option to `false`.
|
|
168
191
|
- **Array options**: Repeat the flag to collect multiple values (e.g., `--exclude a --exclude b`).
|
|
169
192
|
- **Aliases**: Use `alias` for short flags (e.g., `-d` for `--dryRun`).
|
|
193
|
+
- **Groups**: Use `group` for grouping some commands in help (e.g., `{ group: 'Extra Commands' }`).
|
|
170
194
|
|
|
171
195
|
See [examples/](examples/) for more usage patterns.
|
|
172
196
|
|
|
@@ -179,7 +203,7 @@ See [examples/](examples/) for more usage patterns.
|
|
|
179
203
|
|
|
180
204
|
## Help Example
|
|
181
205
|
|
|
182
|
-
You can see below an example of a CLI help (which is the result of calling `--help` with the config shown
|
|
206
|
+
You can see below an example of a CLI help (which is the result of calling `--help` with the [config](#usage) shown above).
|
|
183
207
|
|
|
184
208
|
Please note:
|
|
185
209
|
|
|
@@ -188,19 +212,27 @@ Please note:
|
|
|
188
212
|
|
|
189
213
|
```
|
|
190
214
|
Usage:
|
|
191
|
-
serve <input..> [port] [options]
|
|
215
|
+
serve <input..> [port] [options] Start a server with the given options
|
|
216
|
+
|
|
217
|
+
Examples:
|
|
218
|
+
serve ./www/index.html 8080 --open Start web server on port 8080 and open browser
|
|
219
|
+
serve ./index.html 8081 --no-open --verbose Start web server on port 8081 without opening browser and print more debugging logging to the console
|
|
192
220
|
|
|
193
221
|
Arguments:
|
|
194
|
-
input
|
|
195
|
-
port
|
|
222
|
+
input serving files or directory <string..>
|
|
223
|
+
port port to bind on [number]
|
|
196
224
|
|
|
197
225
|
Options:
|
|
198
|
-
-d, --
|
|
199
|
-
-e, --exclude
|
|
200
|
-
-
|
|
201
|
-
-
|
|
202
|
-
--
|
|
203
|
-
|
|
204
|
-
-h, --help
|
|
205
|
-
-v, --version
|
|
206
|
-
|
|
226
|
+
-d, --dry-run Show what would be done, but do not actually start the server [boolean]
|
|
227
|
+
-e, --exclude pattern or glob to exclude (may be passed multiple times) [array]
|
|
228
|
+
-V, --verbose print more information to console [boolean]
|
|
229
|
+
-o, --open open browser when starting server [boolean]
|
|
230
|
+
--cache Set cache time (in seconds) for cache-control max-age header [number]
|
|
231
|
+
--address Address to use <string>
|
|
232
|
+
-h, --help Show help [boolean]
|
|
233
|
+
-v, --version Show version number [boolean]
|
|
234
|
+
|
|
235
|
+
Advanced Options:
|
|
236
|
+
-D, --display a required display option <boolean>
|
|
237
|
+
-r, --rainbow Enable rainbow mode [boolean]
|
|
238
|
+
```
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,EAAc,MAAM,iBAAiB,CAAC;AAEtE,mBAAmB,iBAAiB,CAAC;AAOrC,wBAAgB,SAAS,CAAC,CAAC,SAAS,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAgMpE"}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const defaultOptions = {
|
|
2
|
-
help: { alias: 'h',
|
|
3
|
-
version: { alias: 'v',
|
|
2
|
+
help: { alias: 'h', describe: 'Show help', type: 'boolean' },
|
|
3
|
+
version: { alias: 'v', describe: 'Show version number', type: 'boolean' },
|
|
4
4
|
};
|
|
5
5
|
export function parseArgs(config) {
|
|
6
6
|
const { command, options, version } = config;
|
|
@@ -227,22 +227,63 @@ function findOption(options, arg) {
|
|
|
227
227
|
}
|
|
228
228
|
/** Print CLI help documentation to the screen */
|
|
229
229
|
function printHelp(config) {
|
|
230
|
-
const { command, options, version,
|
|
230
|
+
const { command, options, version, helpDescMinLength = 50, helpDescMaxLength = 100 } = config;
|
|
231
231
|
const usagePositionals = buildUsagePositionals(command.positionals);
|
|
232
232
|
console.log('Usage:');
|
|
233
|
-
console.log(` ${command.name} ${usagePositionals} [options]
|
|
233
|
+
console.log(` ${command.name} ${usagePositionals} [options] ${command.describe}`);
|
|
234
|
+
// display any examples (when provided)
|
|
235
|
+
if (Array.isArray(command.examples) && command.examples.length) {
|
|
236
|
+
console.log('\nExamples:');
|
|
237
|
+
command.examples.forEach(ex => {
|
|
238
|
+
console.log(` ${ex.cmd.replace('$0', command.name)} ${ex.describe || ''}`);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
// calculate longest description length
|
|
242
|
+
let longestOptNameLn = 0;
|
|
243
|
+
let longestOptDescLn = 0;
|
|
244
|
+
for (const [key, option] of Object.entries({ ...options, ...defaultOptions })) {
|
|
245
|
+
const flagLn = (config.helpFlagCasing === 'camel' ? key : camelToKebab(key)).length;
|
|
246
|
+
if (flagLn > longestOptNameLn) {
|
|
247
|
+
longestOptNameLn = key.length;
|
|
248
|
+
}
|
|
249
|
+
if ((option.describe?.length ?? 0) > longestOptDescLn) {
|
|
250
|
+
longestOptDescLn = option.describe.length;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// make sure the length to use is between our defined min/max
|
|
254
|
+
if (longestOptDescLn < helpDescMinLength) {
|
|
255
|
+
longestOptDescLn = helpDescMinLength;
|
|
256
|
+
}
|
|
257
|
+
else if (longestOptDescLn > helpDescMaxLength) {
|
|
258
|
+
longestOptDescLn = helpDescMaxLength;
|
|
259
|
+
}
|
|
260
|
+
// reserve some extra spaces between option name/desc
|
|
261
|
+
longestOptDescLn += 2;
|
|
262
|
+
longestOptNameLn += 3;
|
|
234
263
|
console.log('\nArguments:');
|
|
235
264
|
command.positionals?.forEach(arg => {
|
|
236
|
-
console.log(` ${formatHelpText(arg.name,
|
|
265
|
+
console.log(` ${formatHelpText(arg.name, longestOptNameLn + 6)}${formatHelpText(arg.describe, longestOptDescLn)} ${formatOptionType(arg.type, arg.variadic, arg.required)}`);
|
|
237
266
|
});
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const
|
|
241
|
-
if (!
|
|
242
|
-
|
|
267
|
+
// Group options by their group property
|
|
268
|
+
const groupedOptions = Object.entries({ ...options, ...defaultOptions }).reduce((acc, [key, option]) => {
|
|
269
|
+
const group = option.group || 'Options';
|
|
270
|
+
if (!acc[group]) {
|
|
271
|
+
acc[group] = [];
|
|
243
272
|
}
|
|
244
|
-
|
|
245
|
-
|
|
273
|
+
acc[group].push([key, option]);
|
|
274
|
+
return acc;
|
|
275
|
+
}, {});
|
|
276
|
+
Object.keys(groupedOptions).forEach(group => {
|
|
277
|
+
console.log(`\n${group}:`);
|
|
278
|
+
groupedOptions[group].forEach(([key, option]) => {
|
|
279
|
+
const aliasStr = option.alias ? `-${option.alias}, ` : '';
|
|
280
|
+
if (!version && key === 'version') {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const flagName = config.helpFlagCasing === 'camel' ? key : camelToKebab(key);
|
|
284
|
+
console.log(` ${aliasStr.padEnd(4)}--${formatHelpText(flagName, longestOptNameLn)}${formatHelpText(option.describe || '', longestOptDescLn)} ${formatOptionType(option.type, false, option.required)}`);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
246
287
|
}
|
|
247
288
|
/** Utility to convert kebab-case to camelCase */
|
|
248
289
|
function kebabToCamel(str) {
|
package/dist/interfaces.d.ts
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
|
-
export interface
|
|
1
|
+
export interface FlagOption {
|
|
2
2
|
/** option type */
|
|
3
3
|
type?: 'string' | 'boolean' | 'number' | 'array';
|
|
4
|
-
/**
|
|
5
|
-
|
|
4
|
+
/** describe the flag option */
|
|
5
|
+
describe: string;
|
|
6
6
|
/** defaults to undefined, provide shorter alias as command options */
|
|
7
7
|
alias?: string;
|
|
8
8
|
/** default value for the option if not provided */
|
|
9
9
|
default?: any;
|
|
10
|
+
/** optional group for grouping some commands */
|
|
11
|
+
group?: string;
|
|
10
12
|
/** defaults to false, is the option required? */
|
|
11
13
|
required?: boolean;
|
|
12
14
|
}
|
|
13
15
|
export interface PositionalArgument {
|
|
14
16
|
/** positional argument name (it will be displayed in the help docs) */
|
|
15
17
|
name: string;
|
|
16
|
-
/** positional argument
|
|
17
|
-
|
|
18
|
+
/** describe positional argument */
|
|
19
|
+
describe: string;
|
|
18
20
|
/** postional argument type */
|
|
19
21
|
type?: 'string' | 'boolean' | 'number' | 'array';
|
|
20
22
|
/** defaults to false, allows multiple values for this positional argument */
|
|
@@ -24,24 +26,37 @@ export interface PositionalArgument {
|
|
|
24
26
|
/** defaults to false, is the positional argument required? */
|
|
25
27
|
required?: boolean;
|
|
26
28
|
}
|
|
27
|
-
export interface
|
|
29
|
+
export interface CommandOption {
|
|
28
30
|
/** command name, used in the help docs */
|
|
29
31
|
name: string;
|
|
30
|
-
/** command
|
|
31
|
-
|
|
32
|
+
/** describe command */
|
|
33
|
+
describe: string;
|
|
34
|
+
/** give some example invocations of your program */
|
|
35
|
+
examples?: readonly ExampleOption[];
|
|
32
36
|
/** list of positional arguments */
|
|
33
37
|
positionals?: readonly PositionalArgument[];
|
|
34
38
|
}
|
|
39
|
+
export interface ExampleOption {
|
|
40
|
+
/** script example command, the string `$0` will get interpolated to the current script name or node command */
|
|
41
|
+
cmd: string;
|
|
42
|
+
/** describe what the script command does */
|
|
43
|
+
describe: string;
|
|
44
|
+
}
|
|
35
45
|
/** CLI options */
|
|
36
46
|
export interface Config {
|
|
37
47
|
/** CLI definition */
|
|
38
|
-
command:
|
|
39
|
-
/**
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
48
|
+
command: CommandOption;
|
|
49
|
+
/**
|
|
50
|
+
* Show flag option in which casing (camelCase or kebab-case) in the help (defaults to 'kebab').
|
|
51
|
+
* Note: this is only for the help print, the parsing will always support both camel/kebab casing
|
|
52
|
+
*/
|
|
53
|
+
helpFlagCasing?: 'camel' | 'kebab';
|
|
54
|
+
/** min description length shown in the help (defaults to 50) */
|
|
55
|
+
helpDescMinLength?: number;
|
|
56
|
+
/** max description length shown in the help (defaults to 100) */
|
|
57
|
+
helpDescMaxLength?: number;
|
|
43
58
|
/** CLI list of flag options */
|
|
44
|
-
options: Record<string,
|
|
59
|
+
options: Record<string, FlagOption>;
|
|
45
60
|
/** CLI or package version */
|
|
46
61
|
version?: string;
|
|
47
62
|
}
|
package/dist/interfaces.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"interfaces.d.ts","sourceRoot":"","sources":["../src/interfaces.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,
|
|
1
|
+
{"version":3,"file":"interfaces.d.ts","sourceRoot":"","sources":["../src/interfaces.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cli-nano",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "Small command-line tool similar to `yargs` or `parseArgs` from Node.js to create a CLI accepting positional arguments, flags and options.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"access": "public"
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
|
-
"/dist"
|
|
19
|
+
"/dist",
|
|
20
|
+
"/src"
|
|
20
21
|
],
|
|
21
22
|
"license": "MIT",
|
|
22
23
|
"author": "Ghislain B.",
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import type { ArgsResult, Config, FlagOption } from './interfaces.js';
|
|
2
|
+
|
|
3
|
+
export type * from './interfaces.js';
|
|
4
|
+
|
|
5
|
+
const defaultOptions: Record<string, FlagOption> = {
|
|
6
|
+
help: { alias: 'h', describe: 'Show help', type: 'boolean' },
|
|
7
|
+
version: { alias: 'v', describe: 'Show version number', type: 'boolean' },
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function parseArgs<C extends Config>(config: C): ArgsResult<C> {
|
|
11
|
+
const { command, options, version } = config;
|
|
12
|
+
|
|
13
|
+
// Normalize args to support --option=value and -o=value
|
|
14
|
+
const args = process.argv.slice(2).flatMap(arg => {
|
|
15
|
+
if (/^--?\w[\w-]*=/.test(arg)) {
|
|
16
|
+
const [flag, ...rest] = arg.split('=');
|
|
17
|
+
return [flag, rest.join('=')];
|
|
18
|
+
}
|
|
19
|
+
return arg;
|
|
20
|
+
});
|
|
21
|
+
const result: Record<string, any> = {};
|
|
22
|
+
|
|
23
|
+
// Check for duplicate aliases
|
|
24
|
+
const aliasMap = new Map<string, string>();
|
|
25
|
+
for (const [key, opt] of Object.entries(options)) {
|
|
26
|
+
if (opt.alias) {
|
|
27
|
+
if (aliasMap.has(opt.alias)) {
|
|
28
|
+
throw new Error(`Duplicate alias detected: "${opt.alias}" used for both "${aliasMap.get(opt.alias)}" and "${key}"`);
|
|
29
|
+
}
|
|
30
|
+
aliasMap.set(opt.alias, key);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Handle --help and --version before anything else
|
|
35
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
36
|
+
printHelp(config);
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
if ((version && args.includes('--version')) || args.includes('-v')) {
|
|
40
|
+
console.log(version || 'No version specified');
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Validate: required positionals must come before optional ones
|
|
45
|
+
const positionals = command.positionals ?? [];
|
|
46
|
+
let foundOptional = false;
|
|
47
|
+
for (const pos of positionals) {
|
|
48
|
+
if (!pos.required) {
|
|
49
|
+
foundOptional = true;
|
|
50
|
+
}
|
|
51
|
+
if (foundOptional && pos.required) {
|
|
52
|
+
throw new Error(`Invalid positional argument configuration: required positional "${pos.name}" cannot follow optional positional(s).`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Handle positional arguments
|
|
57
|
+
let argIndex = 0;
|
|
58
|
+
const nonOptionArgs: string[] = [];
|
|
59
|
+
while (argIndex < args.length && !args[argIndex].startsWith('-')) {
|
|
60
|
+
nonOptionArgs.push(args[argIndex]);
|
|
61
|
+
argIndex++;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let nonOptionIndex = 0;
|
|
65
|
+
for (let i = 0; i < positionals.length; i++) {
|
|
66
|
+
const pos = positionals[i];
|
|
67
|
+
if (pos.variadic) {
|
|
68
|
+
const remaining = positionals.length - (i + 1);
|
|
69
|
+
const values = nonOptionArgs.slice(nonOptionIndex, nonOptionArgs.length - remaining);
|
|
70
|
+
if (pos.required && values.length === 0) {
|
|
71
|
+
const usagePositionals = buildUsagePositionals(positionals);
|
|
72
|
+
throw new Error(`Missing required positional argument, i.e.: "${command.name} ${usagePositionals}"`);
|
|
73
|
+
}
|
|
74
|
+
result[pos.name] = !pos.required && values.length === 0 && pos.default !== undefined ? pos.default : values;
|
|
75
|
+
nonOptionIndex += values.length;
|
|
76
|
+
} else {
|
|
77
|
+
const value = nonOptionArgs[nonOptionIndex];
|
|
78
|
+
// Check if there are enough args left for required positionals
|
|
79
|
+
const requiredLeft = positionals.slice(i).filter(p => p.required).length;
|
|
80
|
+
const argsLeft = nonOptionArgs.length - nonOptionIndex;
|
|
81
|
+
if (value !== undefined && (argsLeft > requiredLeft - (pos.required ? 1 : 0) || pos.required)) {
|
|
82
|
+
result[pos.name] = value;
|
|
83
|
+
nonOptionIndex++;
|
|
84
|
+
} else if (!pos.required && pos.default !== undefined) {
|
|
85
|
+
result[pos.name] = pos.default;
|
|
86
|
+
} else if (pos.required) {
|
|
87
|
+
const usagePositionals = buildUsagePositionals(positionals);
|
|
88
|
+
throw new Error(`Missing required positional argument, i.e.: "${command.name} ${usagePositionals}"`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Handle options
|
|
94
|
+
argIndex = 0;
|
|
95
|
+
const consumedArgs = new Set<number>();
|
|
96
|
+
// Mark all nonOptionArgs indices as consumed for positionals
|
|
97
|
+
let tempNonOptionIndex = 0;
|
|
98
|
+
for (let i = 0; i < positionals.length; i++) {
|
|
99
|
+
const pos = positionals[i];
|
|
100
|
+
if (pos.variadic) {
|
|
101
|
+
const remaining = positionals.length - (i + 1);
|
|
102
|
+
const values = nonOptionArgs.slice(tempNonOptionIndex, nonOptionArgs.length - remaining);
|
|
103
|
+
for (let j = tempNonOptionIndex; j < tempNonOptionIndex + values.length; j++) {
|
|
104
|
+
consumedArgs.add(args.findIndex((a, idx) => !a.startsWith('-') && !consumedArgs.has(idx) && a === nonOptionArgs[j]));
|
|
105
|
+
}
|
|
106
|
+
tempNonOptionIndex += values.length;
|
|
107
|
+
} else {
|
|
108
|
+
const value = nonOptionArgs[tempNonOptionIndex++];
|
|
109
|
+
consumedArgs.add(args.findIndex((a, idx) => !a.startsWith('-') && !consumedArgs.has(idx) && a === value));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
while (argIndex < args.length) {
|
|
114
|
+
if (consumedArgs.has(argIndex)) {
|
|
115
|
+
argIndex++;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const argOrg = args[argIndex] || '';
|
|
119
|
+
let arg = argOrg;
|
|
120
|
+
let option: FlagOption | undefined;
|
|
121
|
+
let configKey: string | undefined;
|
|
122
|
+
|
|
123
|
+
if (argOrg.startsWith('-')) {
|
|
124
|
+
if (argOrg.startsWith('--')) {
|
|
125
|
+
arg = argOrg.slice(2);
|
|
126
|
+
[option, configKey] = findOption(options, arg);
|
|
127
|
+
} else if (argOrg.startsWith('-')) {
|
|
128
|
+
arg = argOrg.slice(1);
|
|
129
|
+
[option, configKey] = findOption(options, arg);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Handle negated boolean in both forms
|
|
133
|
+
if (!option) {
|
|
134
|
+
const isNegated = arg.startsWith('no-');
|
|
135
|
+
const optionName = isNegated ? arg.slice(3) : arg;
|
|
136
|
+
const camelOptionName = kebabToCamel(optionName);
|
|
137
|
+
option = options[optionName] || options[camelOptionName];
|
|
138
|
+
configKey = camelOptionName in options ? camelOptionName : optionName;
|
|
139
|
+
if (option?.type === 'boolean') {
|
|
140
|
+
if (result[optionName] !== undefined || result[camelOptionName] !== undefined) {
|
|
141
|
+
throw new Error('Providing same negated and truthy argument are not allowed');
|
|
142
|
+
}
|
|
143
|
+
result[configKey] = !isNegated;
|
|
144
|
+
argIndex++;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!option || !configKey) {
|
|
150
|
+
throw new Error(`Unknown CLI option: ${arg}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
switch (option.type) {
|
|
154
|
+
case 'boolean':
|
|
155
|
+
if (result[configKey] !== undefined) {
|
|
156
|
+
throw new Error('Providing same negated and truthy argument are not allowed');
|
|
157
|
+
}
|
|
158
|
+
result[configKey] = !argOrg.startsWith('--no-') && !argOrg.startsWith('-no-');
|
|
159
|
+
break;
|
|
160
|
+
case 'number':
|
|
161
|
+
if (args[argIndex + 1] === undefined || args[argIndex + 1].startsWith('-')) {
|
|
162
|
+
throw new Error(`Missing value for option: ${configKey}`);
|
|
163
|
+
}
|
|
164
|
+
result[configKey] = Number(args[++argIndex]);
|
|
165
|
+
break;
|
|
166
|
+
case 'array': {
|
|
167
|
+
if (!result[configKey]) result[configKey] = [];
|
|
168
|
+
const arrayValue = args[++argIndex];
|
|
169
|
+
if (arrayValue === undefined || arrayValue.startsWith('-')) {
|
|
170
|
+
throw new Error(`Missing value for array option: ${configKey}`);
|
|
171
|
+
}
|
|
172
|
+
result[configKey].push(arrayValue);
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
case 'string':
|
|
176
|
+
default:
|
|
177
|
+
if (args[argIndex + 1] === undefined || args[argIndex + 1].startsWith('-')) {
|
|
178
|
+
throw new Error(`Missing value for option: ${configKey}`);
|
|
179
|
+
}
|
|
180
|
+
result[configKey] = args[++argIndex];
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
185
|
+
}
|
|
186
|
+
argIndex++;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// After all parsing, assign any `default` CLI options when undefined
|
|
190
|
+
// and check for any missing `required` CLI options
|
|
191
|
+
Object.entries(options).forEach(([key, opt]) => {
|
|
192
|
+
if (result[key] === undefined && opt.default !== undefined) {
|
|
193
|
+
result[key] = opt.default;
|
|
194
|
+
}
|
|
195
|
+
if (opt.required && result[key] === undefined) {
|
|
196
|
+
const aliasStr = opt.alias ? `-${opt.alias}, ` : '';
|
|
197
|
+
throw new Error(`Missing required option: ${aliasStr}--${key}`);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
return result as ArgsResult<C>;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Format a text to a fixed length, truncating and padding as needed. */
|
|
205
|
+
function formatHelpText(text: string, max: number) {
|
|
206
|
+
const truncated = text.length > max ? `${text.slice(0, max - 3)}...` : text;
|
|
207
|
+
return truncated.padEnd(max);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Build the usage string for positionals, e.g. "<input..> [output]" */
|
|
211
|
+
function buildUsagePositionals(positionals: readonly any[] = []) {
|
|
212
|
+
return positionals
|
|
213
|
+
.map(p => {
|
|
214
|
+
const variadic = p.variadic ? '..' : '';
|
|
215
|
+
return p.required ? `<${p.name}${variadic}>` : `[${p.name}${variadic}]`;
|
|
216
|
+
})
|
|
217
|
+
.join(' ');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Format the option/argument type for help output */
|
|
221
|
+
function formatOptionType(type: string | undefined, variadic?: boolean, required?: boolean) {
|
|
222
|
+
const t = type || 'string';
|
|
223
|
+
const variadicStr = variadic ? '..' : '';
|
|
224
|
+
return required ? `<${t}${variadicStr}>` : `[${t}${variadicStr}]`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Helper to find an option and its config key by argument name or alias. */
|
|
228
|
+
function findOption(options: Record<string, FlagOption>, arg: string): [FlagOption | undefined, string | undefined] {
|
|
229
|
+
// Try all forms: as-is, kebab-to-camel, camel-to-kebab
|
|
230
|
+
const option = options[arg] || options[kebabToCamel(arg)] || options[camelToKebab(arg).replace(/-/g, '')];
|
|
231
|
+
if (option) {
|
|
232
|
+
const configKey = Object.keys(options).find(key => options[key] === option);
|
|
233
|
+
return [option, configKey];
|
|
234
|
+
}
|
|
235
|
+
// Try matching alias in all forms
|
|
236
|
+
for (const key of Object.keys(options)) {
|
|
237
|
+
const opt = options[key];
|
|
238
|
+
if (opt.alias && (opt.alias === arg || opt.alias === kebabToCamel(arg) || opt.alias === camelToKebab(arg))) {
|
|
239
|
+
return [opt, key];
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return [undefined, undefined];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Print CLI help documentation to the screen */
|
|
246
|
+
function printHelp(config: Config) {
|
|
247
|
+
const { command, options, version, helpDescMinLength = 50, helpDescMaxLength = 100 } = config;
|
|
248
|
+
const usagePositionals = buildUsagePositionals(command.positionals);
|
|
249
|
+
|
|
250
|
+
console.log('Usage:');
|
|
251
|
+
console.log(` ${command.name} ${usagePositionals} [options] ${command.describe}`);
|
|
252
|
+
|
|
253
|
+
// display any examples (when provided)
|
|
254
|
+
if (Array.isArray(command.examples) && command.examples.length) {
|
|
255
|
+
console.log('\nExamples:');
|
|
256
|
+
command.examples.forEach(ex => {
|
|
257
|
+
console.log(` ${ex.cmd.replace('$0', command.name)} ${ex.describe || ''}`);
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// calculate longest description length
|
|
262
|
+
let longestOptNameLn = 0;
|
|
263
|
+
let longestOptDescLn = 0;
|
|
264
|
+
for (const [key, option] of Object.entries({ ...options, ...defaultOptions })) {
|
|
265
|
+
const flagLn = (config.helpFlagCasing === 'camel' ? key : camelToKebab(key)).length;
|
|
266
|
+
if (flagLn > longestOptNameLn) {
|
|
267
|
+
longestOptNameLn = key.length;
|
|
268
|
+
}
|
|
269
|
+
if ((option.describe?.length ?? 0) > longestOptDescLn) {
|
|
270
|
+
longestOptDescLn = option.describe.length;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// make sure the length to use is between our defined min/max
|
|
275
|
+
if (longestOptDescLn < helpDescMinLength) {
|
|
276
|
+
longestOptDescLn = helpDescMinLength;
|
|
277
|
+
} else if (longestOptDescLn > helpDescMaxLength) {
|
|
278
|
+
longestOptDescLn = helpDescMaxLength;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// reserve some extra spaces between option name/desc
|
|
282
|
+
longestOptDescLn += 2;
|
|
283
|
+
longestOptNameLn += 3;
|
|
284
|
+
|
|
285
|
+
console.log('\nArguments:');
|
|
286
|
+
command.positionals?.forEach(arg => {
|
|
287
|
+
console.log(
|
|
288
|
+
` ${formatHelpText(arg.name, longestOptNameLn + 6)}${formatHelpText(arg.describe, longestOptDescLn)} ${formatOptionType(arg.type, arg.variadic, arg.required)}`,
|
|
289
|
+
);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// Group options by their group property
|
|
293
|
+
const groupedOptions = Object.entries({ ...options, ...defaultOptions }).reduce(
|
|
294
|
+
(acc, [key, option]) => {
|
|
295
|
+
const group = option.group || 'Options';
|
|
296
|
+
if (!acc[group]) {
|
|
297
|
+
acc[group] = [];
|
|
298
|
+
}
|
|
299
|
+
acc[group].push([key, option]);
|
|
300
|
+
return acc;
|
|
301
|
+
},
|
|
302
|
+
{} as Record<string, [string, FlagOption][]>,
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
Object.keys(groupedOptions).forEach(group => {
|
|
306
|
+
console.log(`\n${group}:`);
|
|
307
|
+
groupedOptions[group].forEach(([key, option]) => {
|
|
308
|
+
const aliasStr = option.alias ? `-${option.alias}, ` : '';
|
|
309
|
+
if (!version && key === 'version') {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const flagName = config.helpFlagCasing === 'camel' ? key : camelToKebab(key);
|
|
313
|
+
console.log(
|
|
314
|
+
` ${aliasStr.padEnd(4)}--${formatHelpText(flagName, longestOptNameLn)}${formatHelpText(option.describe || '', longestOptDescLn)} ${formatOptionType(option.type, false, option.required)}`,
|
|
315
|
+
);
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Utility to convert kebab-case to camelCase */
|
|
321
|
+
function kebabToCamel(str: string) {
|
|
322
|
+
return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** Utility to convert camelCase to kebab-case */
|
|
326
|
+
function camelToKebab(str: string) {
|
|
327
|
+
return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
|
|
328
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
export interface FlagOption {
|
|
2
|
+
/** option type */
|
|
3
|
+
type?: 'string' | 'boolean' | 'number' | 'array';
|
|
4
|
+
|
|
5
|
+
/** describe the flag option */
|
|
6
|
+
describe: string;
|
|
7
|
+
|
|
8
|
+
/** defaults to undefined, provide shorter alias as command options */
|
|
9
|
+
alias?: string;
|
|
10
|
+
|
|
11
|
+
/** default value for the option if not provided */
|
|
12
|
+
default?: any;
|
|
13
|
+
|
|
14
|
+
/** optional group for grouping some commands */
|
|
15
|
+
group?: string;
|
|
16
|
+
|
|
17
|
+
/** defaults to false, is the option required? */
|
|
18
|
+
required?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface PositionalArgument {
|
|
22
|
+
/** positional argument name (it will be displayed in the help docs) */
|
|
23
|
+
name: string;
|
|
24
|
+
|
|
25
|
+
/** describe positional argument */
|
|
26
|
+
describe: string;
|
|
27
|
+
|
|
28
|
+
/** postional argument type */
|
|
29
|
+
type?: 'string' | 'boolean' | 'number' | 'array';
|
|
30
|
+
|
|
31
|
+
/** defaults to false, allows multiple values for this positional argument */
|
|
32
|
+
variadic?: boolean;
|
|
33
|
+
|
|
34
|
+
/** default value for the option if not provided */
|
|
35
|
+
default?: any;
|
|
36
|
+
|
|
37
|
+
/** defaults to false, is the positional argument required? */
|
|
38
|
+
required?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface CommandOption {
|
|
42
|
+
/** command name, used in the help docs */
|
|
43
|
+
name: string;
|
|
44
|
+
|
|
45
|
+
/** describe command */
|
|
46
|
+
describe: string;
|
|
47
|
+
|
|
48
|
+
/** give some example invocations of your program */
|
|
49
|
+
examples?: readonly ExampleOption[];
|
|
50
|
+
|
|
51
|
+
/** list of positional arguments */
|
|
52
|
+
positionals?: readonly PositionalArgument[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ExampleOption {
|
|
56
|
+
/** script example command, the string `$0` will get interpolated to the current script name or node command */
|
|
57
|
+
cmd: string;
|
|
58
|
+
|
|
59
|
+
/** describe what the script command does */
|
|
60
|
+
describe: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** CLI options */
|
|
64
|
+
export interface Config {
|
|
65
|
+
/** CLI definition */
|
|
66
|
+
command: CommandOption;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Show flag option in which casing (camelCase or kebab-case) in the help (defaults to 'kebab').
|
|
70
|
+
* Note: this is only for the help print, the parsing will always support both camel/kebab casing
|
|
71
|
+
*/
|
|
72
|
+
helpFlagCasing?: 'camel' | 'kebab';
|
|
73
|
+
|
|
74
|
+
/** min description length shown in the help (defaults to 50) */
|
|
75
|
+
helpDescMinLength?: number;
|
|
76
|
+
|
|
77
|
+
/** max description length shown in the help (defaults to 100) */
|
|
78
|
+
helpDescMaxLength?: number;
|
|
79
|
+
|
|
80
|
+
/** CLI list of flag options */
|
|
81
|
+
options: Record<string, FlagOption>;
|
|
82
|
+
|
|
83
|
+
/** CLI or package version */
|
|
84
|
+
version?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Utility type to map ArgumentOptions/PositionalArgument to their value type */
|
|
88
|
+
export type ArgValueType<T extends { type?: string; default?: any; variadic?: boolean; required?: boolean }> = T['type'] extends 'boolean'
|
|
89
|
+
? boolean
|
|
90
|
+
: T['type'] extends 'number'
|
|
91
|
+
? number
|
|
92
|
+
: T['type'] extends 'array'
|
|
93
|
+
? T extends { variadic: true }
|
|
94
|
+
? T extends { required: true }
|
|
95
|
+
? [string, ...string[]]
|
|
96
|
+
: string[]
|
|
97
|
+
: string | string[]
|
|
98
|
+
: T['type'] extends 'string'
|
|
99
|
+
? T extends { variadic: true }
|
|
100
|
+
? T extends { required: true }
|
|
101
|
+
? [string, ...string[]]
|
|
102
|
+
: string[]
|
|
103
|
+
: string
|
|
104
|
+
: T['type'] extends undefined
|
|
105
|
+
? T extends { variadic: true }
|
|
106
|
+
? T extends { required: true }
|
|
107
|
+
? [string, ...string[]]
|
|
108
|
+
: string[]
|
|
109
|
+
: string
|
|
110
|
+
: T['default'] extends undefined
|
|
111
|
+
? string
|
|
112
|
+
: T['default'];
|
|
113
|
+
|
|
114
|
+
/** Helper to get required keys */
|
|
115
|
+
type RequiredKeys<T> = {
|
|
116
|
+
[K in keyof T]: T[K] extends { required: true } ? K : never;
|
|
117
|
+
}[keyof T];
|
|
118
|
+
|
|
119
|
+
/** Helper to get optional keys */
|
|
120
|
+
type OptionalKeys<T> = Exclude<keyof T, RequiredKeys<T>>;
|
|
121
|
+
|
|
122
|
+
/** Map options record to an object type with required/optional properties */
|
|
123
|
+
export type OptionsToObject<T extends Record<string, any>> = { [K in RequiredKeys<T>]: ArgValueType<T[K]> } & {
|
|
124
|
+
[K in OptionalKeys<T>]?: ArgValueType<T[K]>;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/** Map positionals array to an object type with required/optional properties */
|
|
128
|
+
export type PositionalsToObject<T extends readonly PositionalArgument[] | undefined> = T extends readonly [infer P, ...infer Rest]
|
|
129
|
+
? P extends PositionalArgument
|
|
130
|
+
? (P['required'] extends true ? { [K in P['name']]: ArgValueType<P> } : { [K in P['name']]?: ArgValueType<P> }) &
|
|
131
|
+
PositionalsToObject<Rest extends readonly PositionalArgument[] ? Rest : []>
|
|
132
|
+
: PositionalsToObject<Rest extends readonly PositionalArgument[] ? Rest : []>
|
|
133
|
+
: { [key: string]: never };
|
|
134
|
+
|
|
135
|
+
/** The full result type for parseArgs */
|
|
136
|
+
export type ArgsResult<C extends Config> = PositionalsToObject<C['command']['positionals']> & OptionsToObject<C['options']>;
|