cli-nano 1.1.3 → 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 +2 -2
- package/src/index.ts +68 -19
- package/src/interfaces.ts +34 -14
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": {
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { ArgsResult,
|
|
1
|
+
import type { ArgsResult, Config, FlagOption } from './interfaces.js';
|
|
2
2
|
|
|
3
3
|
export type * from './interfaces.js';
|
|
4
4
|
|
|
5
|
-
const defaultOptions: Record<string,
|
|
6
|
-
help: { alias: 'h',
|
|
7
|
-
version: { alias: 'v',
|
|
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
8
|
};
|
|
9
9
|
|
|
10
10
|
export function parseArgs<C extends Config>(config: C): ArgsResult<C> {
|
|
@@ -117,7 +117,7 @@ export function parseArgs<C extends Config>(config: C): ArgsResult<C> {
|
|
|
117
117
|
}
|
|
118
118
|
const argOrg = args[argIndex] || '';
|
|
119
119
|
let arg = argOrg;
|
|
120
|
-
let option:
|
|
120
|
+
let option: FlagOption | undefined;
|
|
121
121
|
let configKey: string | undefined;
|
|
122
122
|
|
|
123
123
|
if (argOrg.startsWith('-')) {
|
|
@@ -225,7 +225,7 @@ function formatOptionType(type: string | undefined, variadic?: boolean, required
|
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
/** Helper to find an option and its config key by argument name or alias. */
|
|
228
|
-
function findOption(options: Record<string,
|
|
228
|
+
function findOption(options: Record<string, FlagOption>, arg: string): [FlagOption | undefined, string | undefined] {
|
|
229
229
|
// Try all forms: as-is, kebab-to-camel, camel-to-kebab
|
|
230
230
|
const option = options[arg] || options[kebabToCamel(arg)] || options[camelToKebab(arg).replace(/-/g, '')];
|
|
231
231
|
if (option) {
|
|
@@ -244,28 +244,77 @@ function findOption(options: Record<string, ArgumentOptions>, arg: string): [Arg
|
|
|
244
244
|
|
|
245
245
|
/** Print CLI help documentation to the screen */
|
|
246
246
|
function printHelp(config: Config) {
|
|
247
|
-
const { command, options, version,
|
|
247
|
+
const { command, options, version, helpDescMinLength = 50, helpDescMaxLength = 100 } = config;
|
|
248
248
|
const usagePositionals = buildUsagePositionals(command.positionals);
|
|
249
249
|
|
|
250
250
|
console.log('Usage:');
|
|
251
|
-
console.log(` ${command.name} ${usagePositionals} [options]
|
|
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
|
+
|
|
252
285
|
console.log('\nArguments:');
|
|
253
286
|
command.positionals?.forEach(arg => {
|
|
254
287
|
console.log(
|
|
255
|
-
` ${formatHelpText(arg.name,
|
|
288
|
+
` ${formatHelpText(arg.name, longestOptNameLn + 6)}${formatHelpText(arg.describe, longestOptDescLn)} ${formatOptionType(arg.type, arg.variadic, arg.required)}`,
|
|
256
289
|
);
|
|
257
290
|
});
|
|
258
291
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
+
});
|
|
269
318
|
}
|
|
270
319
|
|
|
271
320
|
/** Utility to convert kebab-case to camelCase */
|
package/src/interfaces.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
export interface
|
|
1
|
+
export interface FlagOption {
|
|
2
2
|
/** option type */
|
|
3
3
|
type?: 'string' | 'boolean' | 'number' | 'array';
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
|
|
5
|
+
/** describe the flag option */
|
|
6
|
+
describe: string;
|
|
7
7
|
|
|
8
8
|
/** defaults to undefined, provide shorter alias as command options */
|
|
9
9
|
alias?: string;
|
|
@@ -11,6 +11,9 @@ export interface ArgumentOptions {
|
|
|
11
11
|
/** default value for the option if not provided */
|
|
12
12
|
default?: any;
|
|
13
13
|
|
|
14
|
+
/** optional group for grouping some commands */
|
|
15
|
+
group?: string;
|
|
16
|
+
|
|
14
17
|
/** defaults to false, is the option required? */
|
|
15
18
|
required?: boolean;
|
|
16
19
|
}
|
|
@@ -19,8 +22,8 @@ export interface PositionalArgument {
|
|
|
19
22
|
/** positional argument name (it will be displayed in the help docs) */
|
|
20
23
|
name: string;
|
|
21
24
|
|
|
22
|
-
/** positional argument
|
|
23
|
-
|
|
25
|
+
/** describe positional argument */
|
|
26
|
+
describe: string;
|
|
24
27
|
|
|
25
28
|
/** postional argument type */
|
|
26
29
|
type?: 'string' | 'boolean' | 'number' | 'array';
|
|
@@ -35,30 +38,47 @@ export interface PositionalArgument {
|
|
|
35
38
|
required?: boolean;
|
|
36
39
|
}
|
|
37
40
|
|
|
38
|
-
export interface
|
|
41
|
+
export interface CommandOption {
|
|
39
42
|
/** command name, used in the help docs */
|
|
40
43
|
name: string;
|
|
41
44
|
|
|
42
|
-
/** command
|
|
43
|
-
|
|
45
|
+
/** describe command */
|
|
46
|
+
describe: string;
|
|
47
|
+
|
|
48
|
+
/** give some example invocations of your program */
|
|
49
|
+
examples?: readonly ExampleOption[];
|
|
44
50
|
|
|
45
51
|
/** list of positional arguments */
|
|
46
52
|
positionals?: readonly PositionalArgument[];
|
|
47
53
|
}
|
|
48
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
|
+
|
|
49
63
|
/** CLI options */
|
|
50
64
|
export interface Config {
|
|
51
65
|
/** CLI definition */
|
|
52
|
-
command:
|
|
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';
|
|
53
73
|
|
|
54
|
-
/**
|
|
55
|
-
|
|
74
|
+
/** min description length shown in the help (defaults to 50) */
|
|
75
|
+
helpDescMinLength?: number;
|
|
56
76
|
|
|
57
|
-
/** description length shown in the help (defaults to
|
|
58
|
-
|
|
77
|
+
/** max description length shown in the help (defaults to 100) */
|
|
78
|
+
helpDescMaxLength?: number;
|
|
59
79
|
|
|
60
80
|
/** CLI list of flag options */
|
|
61
|
-
options: Record<string,
|
|
81
|
+
options: Record<string, FlagOption>;
|
|
62
82
|
|
|
63
83
|
/** CLI or package version */
|
|
64
84
|
version?: string;
|