@naturalcycles/nodejs-lib 15.114.0 → 15.115.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/dist/cli/parseArgs.d.ts +30 -7
- package/dist/cli/parseArgs.js +70 -5
- package/package.json +3 -3
- package/src/cli/parseArgs.ts +104 -15
package/dist/cli/parseArgs.d.ts
CHANGED
|
@@ -7,7 +7,10 @@ export type CliOptionType = 'string' | 'number' | 'boolean';
|
|
|
7
7
|
* yargs `.options()` that we actually use.
|
|
8
8
|
*/
|
|
9
9
|
export interface CliOption {
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Value type. Defaults to `'string'` when omitted.
|
|
12
|
+
*/
|
|
13
|
+
type?: CliOptionType;
|
|
11
14
|
/**
|
|
12
15
|
* Accept the flag multiple times, collecting values into an array.
|
|
13
16
|
* `--id a --id b` => `['a', 'b']`. Replaces yargs `type: 'array'`.
|
|
@@ -36,15 +39,34 @@ export interface CliOption {
|
|
|
36
39
|
* Single-character alias, e.g. `short: 'v'` enables `-v`.
|
|
37
40
|
*/
|
|
38
41
|
short?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Transform the raw string value into the final value. The output type is
|
|
44
|
+
* inferred from the function's return type, so this is the way to produce
|
|
45
|
+
* branded types (e.g. `IsoDate`) or richer values (parsed numbers, JSON, ...).
|
|
46
|
+
*
|
|
47
|
+
* Applied per-element for `array` options. Receives the raw string token, so
|
|
48
|
+
* it fully owns conversion - built-in `number` coercion is not applied on top.
|
|
49
|
+
* NOT applied to `default` values: a `default` is taken to be in final form.
|
|
50
|
+
*
|
|
51
|
+
* @example transform: s => s as IsoDate
|
|
52
|
+
*/
|
|
53
|
+
transform?: (value: string) => unknown;
|
|
39
54
|
}
|
|
40
55
|
export type CliOptions = Record<string, CliOption>;
|
|
41
56
|
/**
|
|
42
|
-
* Element value type of a single option, derived from `
|
|
43
|
-
*
|
|
57
|
+
* Element value type of a single option, derived from `transform` (if present,
|
|
58
|
+
* its return type wins), then `choices`, then `type`. Defaults to `string` when
|
|
59
|
+
* none narrow it (so `type` is optional).
|
|
44
60
|
*/
|
|
45
61
|
type ElemType<O extends CliOption> = O extends {
|
|
62
|
+
transform: (...args: any[]) => infer R;
|
|
63
|
+
} ? R : O extends {
|
|
46
64
|
choices: readonly (infer C)[];
|
|
47
|
-
} ? C : O
|
|
65
|
+
} ? C : O extends {
|
|
66
|
+
type: 'number';
|
|
67
|
+
} ? number : O extends {
|
|
68
|
+
type: 'boolean';
|
|
69
|
+
} ? boolean : string;
|
|
48
70
|
/**
|
|
49
71
|
* Full value type of a single option, applying `array` on top of the element
|
|
50
72
|
* type.
|
|
@@ -116,11 +138,12 @@ export declare class ParseArgsError extends Error {
|
|
|
116
138
|
* top of node's `util.parseArgs`.
|
|
117
139
|
*
|
|
118
140
|
* @example
|
|
119
|
-
* const { dir, limit } = _parseArgs({
|
|
120
|
-
* dir: {
|
|
141
|
+
* const { dir, limit, date } = _parseArgs({
|
|
142
|
+
* dir: { desc: 'Output directory' }, // type defaults to 'string'
|
|
121
143
|
* limit: { type: 'number', default: 100 },
|
|
144
|
+
* date: { transform: s => s as IsoDate }, // inferred + converted via transform
|
|
122
145
|
* })
|
|
123
|
-
* // dir?: string limit: number _: string[]
|
|
146
|
+
* // dir?: string limit: number date?: IsoDate _: string[]
|
|
124
147
|
*/
|
|
125
148
|
export declare function _parseArgs<const O extends CliOptions>(options: O, opt?: ParseArgsOptions): InferCliArgs<O>;
|
|
126
149
|
export {};
|
package/dist/cli/parseArgs.js
CHANGED
|
@@ -11,11 +11,12 @@ export class ParseArgsError extends Error {
|
|
|
11
11
|
* top of node's `util.parseArgs`.
|
|
12
12
|
*
|
|
13
13
|
* @example
|
|
14
|
-
* const { dir, limit } = _parseArgs({
|
|
15
|
-
* dir: {
|
|
14
|
+
* const { dir, limit, date } = _parseArgs({
|
|
15
|
+
* dir: { desc: 'Output directory' }, // type defaults to 'string'
|
|
16
16
|
* limit: { type: 'number', default: 100 },
|
|
17
|
+
* date: { transform: s => s as IsoDate }, // inferred + converted via transform
|
|
17
18
|
* })
|
|
18
|
-
* // dir?: string limit: number _: string[]
|
|
19
|
+
* // dir?: string limit: number date?: IsoDate _: string[]
|
|
19
20
|
*/
|
|
20
21
|
export function _parseArgs(options, opt = {}) {
|
|
21
22
|
const { args, minPositionals = 0, usage, strict = false } = opt;
|
|
@@ -42,6 +43,7 @@ export function _parseArgs(options, opt = {}) {
|
|
|
42
43
|
options: nodeOptions,
|
|
43
44
|
allowPositionals: true,
|
|
44
45
|
allowNegative: true, // native `--no-flag` support for booleans
|
|
46
|
+
tokens: true, // needed to detect the ambiguous `--boolFlag value` space form
|
|
45
47
|
// In non-strict mode, unknown options are collected into `values` but
|
|
46
48
|
// ignored below, since we only read declared options. See `strict` docs.
|
|
47
49
|
strict,
|
|
@@ -56,9 +58,13 @@ export function _parseArgs(options, opt = {}) {
|
|
|
56
58
|
process.stdout.write(buildHelp(options, usage));
|
|
57
59
|
process.exit(0);
|
|
58
60
|
}
|
|
61
|
+
assertNoSpaceValuedBoolean(parsed.tokens, options);
|
|
59
62
|
const result = { _: parsed.positionals };
|
|
60
63
|
for (const [name, def] of Object.entries(options)) {
|
|
61
64
|
let v = values[name];
|
|
65
|
+
// `transform` is only applied to arg-sourced values; a `default` is taken
|
|
66
|
+
// to be in final form (see CliOption.transform docs).
|
|
67
|
+
const fromArgs = v !== undefined;
|
|
62
68
|
if (v === undefined) {
|
|
63
69
|
if (def.default !== undefined) {
|
|
64
70
|
v = def.default;
|
|
@@ -74,9 +80,32 @@ export function _parseArgs(options, opt = {}) {
|
|
|
74
80
|
if (def.array) {
|
|
75
81
|
v = Array.isArray(v) ? v : [v];
|
|
76
82
|
}
|
|
77
|
-
|
|
83
|
+
// A non-boolean option passed as a bare flag (`--out` with no value) comes back
|
|
84
|
+
// from node's parseArgs (in non-strict mode) as boolean `true`. Reject it: the
|
|
85
|
+
// user almost certainly forgot the value, and silently coercing `true` (to `1`
|
|
86
|
+
// for numbers, `"true"` for strings, or crashing a `transform`) would hide the
|
|
87
|
+
// mistake. Only arg-sourced values are checked; a boolean `default` is left
|
|
88
|
+
// alone. `def.type === 'boolean'` legitimately produces booleans, so skip it.
|
|
89
|
+
if (fromArgs && def.type !== 'boolean') {
|
|
90
|
+
const bareFlag = Array.isArray(v)
|
|
91
|
+
? v.some(x => typeof x === 'boolean')
|
|
92
|
+
: typeof v === 'boolean';
|
|
93
|
+
if (bareFlag) {
|
|
94
|
+
throw new ParseArgsError(`Missing value for --${name}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// `transform` owns conversion, so built-in number coercion is skipped for it
|
|
98
|
+
if (def.type === 'number' && !def.transform) {
|
|
78
99
|
v = Array.isArray(v) ? v.map(x => toNumber(x, name)) : toNumber(v, name);
|
|
79
100
|
}
|
|
101
|
+
// node's parseArgs (in non-strict mode) captures the inline value of
|
|
102
|
+
// `--flag=value` on a boolean option as a string ("false"/"true"), rather than
|
|
103
|
+
// rejecting it as strict mode does. Coerce known tokens so `--flag=false` means
|
|
104
|
+
// boolean false, not a truthy "false" string. Real booleans produced by
|
|
105
|
+
// `--flag` / `--no-flag` (and boolean defaults) pass through untouched.
|
|
106
|
+
if (def.type === 'boolean') {
|
|
107
|
+
v = Array.isArray(v) ? v.map(x => toBoolean(x, name)) : toBoolean(v, name);
|
|
108
|
+
}
|
|
80
109
|
if (def.choices) {
|
|
81
110
|
const list = Array.isArray(v) ? v : [v];
|
|
82
111
|
for (const x of list) {
|
|
@@ -85,6 +114,10 @@ export function _parseArgs(options, opt = {}) {
|
|
|
85
114
|
}
|
|
86
115
|
}
|
|
87
116
|
}
|
|
117
|
+
if (def.transform && fromArgs) {
|
|
118
|
+
const { transform } = def;
|
|
119
|
+
v = Array.isArray(v) ? v.map(x => transform(x)) : transform(v);
|
|
120
|
+
}
|
|
88
121
|
result[name] = v;
|
|
89
122
|
}
|
|
90
123
|
if (parsed.positionals.length < minPositionals) {
|
|
@@ -92,6 +125,28 @@ export function _parseArgs(options, opt = {}) {
|
|
|
92
125
|
}
|
|
93
126
|
return result;
|
|
94
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* Reject the ambiguous `--boolFlag value` space form. node never consumes the
|
|
130
|
+
* next token as a boolean's value (getopt convention), so `--arg false` would
|
|
131
|
+
* silently yield `arg: true` and leak "false" into positionals. Unlike the
|
|
132
|
+
* `=value` form (handled by toBoolean) we can't recover the intended value here,
|
|
133
|
+
* so fail loudly. Only `true`/`false` tokens are treated as ambiguous; any other
|
|
134
|
+
* positional (e.g. a filename) is left as a genuine positional.
|
|
135
|
+
*/
|
|
136
|
+
function assertNoSpaceValuedBoolean(tokens, options) {
|
|
137
|
+
for (let i = 0; i < tokens.length - 1; i++) {
|
|
138
|
+
const tok = tokens[i];
|
|
139
|
+
// `tok.value === undefined` => bare flag (no inline `=value`); applies to
|
|
140
|
+
// declared boolean options only (unknown options are ignored, see `strict`).
|
|
141
|
+
if (tok.kind !== 'option' || tok.value !== undefined || options[tok.name]?.type !== 'boolean') {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const next = tokens[i + 1];
|
|
145
|
+
if (next.kind === 'positional' && (next.value === 'true' || next.value === 'false')) {
|
|
146
|
+
throw new ParseArgsError(`Boolean option --${tok.name} does not take a space-separated value ("${next.value}"); use --${tok.name}=${next.value} or --${next.value === 'false' ? `no-${tok.name}` : tok.name}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
95
150
|
function toNumber(raw, name) {
|
|
96
151
|
const n = Number(raw);
|
|
97
152
|
if (Number.isNaN(n)) {
|
|
@@ -99,6 +154,15 @@ function toNumber(raw, name) {
|
|
|
99
154
|
}
|
|
100
155
|
return n;
|
|
101
156
|
}
|
|
157
|
+
function toBoolean(raw, name) {
|
|
158
|
+
if (typeof raw === 'boolean')
|
|
159
|
+
return raw; // real boolean from --flag / --no-flag / default
|
|
160
|
+
if (raw === 'true')
|
|
161
|
+
return true;
|
|
162
|
+
if (raw === 'false')
|
|
163
|
+
return false;
|
|
164
|
+
throw new ParseArgsError(`Invalid boolean for --${name}: "${raw}"`);
|
|
165
|
+
}
|
|
102
166
|
function buildHelp(options, usage) {
|
|
103
167
|
const lines = [];
|
|
104
168
|
if (usage)
|
|
@@ -106,7 +170,8 @@ function buildHelp(options, usage) {
|
|
|
106
170
|
lines.push('Options:');
|
|
107
171
|
for (const [name, def] of Object.entries(options)) {
|
|
108
172
|
const flag = def.short ? `-${def.short}, --${name}` : `--${name}`;
|
|
109
|
-
const
|
|
173
|
+
const type = def.type ?? 'string';
|
|
174
|
+
const meta = [`[${def.array ? `${type}[]` : type}]`];
|
|
110
175
|
if (def.demandOption)
|
|
111
176
|
meta.push('[required]');
|
|
112
177
|
if (def.default !== undefined)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@naturalcycles/nodejs-lib",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "15.
|
|
4
|
+
"version": "15.115.1",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@naturalcycles/js-lib": "^15",
|
|
7
7
|
"@standard-schema/spec": "^1",
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
"yaml": "^2"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
|
-
"typescript": "
|
|
19
|
-
"@naturalcycles/dev-lib": "
|
|
18
|
+
"typescript": "^7",
|
|
19
|
+
"@naturalcycles/dev-lib": "0.0.0"
|
|
20
20
|
},
|
|
21
21
|
"exports": {
|
|
22
22
|
".": "./dist/index.js",
|
package/src/cli/parseArgs.ts
CHANGED
|
@@ -10,7 +10,10 @@ export type CliOptionType = 'string' | 'number' | 'boolean'
|
|
|
10
10
|
* yargs `.options()` that we actually use.
|
|
11
11
|
*/
|
|
12
12
|
export interface CliOption {
|
|
13
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Value type. Defaults to `'string'` when omitted.
|
|
15
|
+
*/
|
|
16
|
+
type?: CliOptionType
|
|
14
17
|
/**
|
|
15
18
|
* Accept the flag multiple times, collecting values into an array.
|
|
16
19
|
* `--id a --id b` => `['a', 'b']`. Replaces yargs `type: 'array'`.
|
|
@@ -39,23 +42,36 @@ export interface CliOption {
|
|
|
39
42
|
* Single-character alias, e.g. `short: 'v'` enables `-v`.
|
|
40
43
|
*/
|
|
41
44
|
short?: string
|
|
45
|
+
/**
|
|
46
|
+
* Transform the raw string value into the final value. The output type is
|
|
47
|
+
* inferred from the function's return type, so this is the way to produce
|
|
48
|
+
* branded types (e.g. `IsoDate`) or richer values (parsed numbers, JSON, ...).
|
|
49
|
+
*
|
|
50
|
+
* Applied per-element for `array` options. Receives the raw string token, so
|
|
51
|
+
* it fully owns conversion - built-in `number` coercion is not applied on top.
|
|
52
|
+
* NOT applied to `default` values: a `default` is taken to be in final form.
|
|
53
|
+
*
|
|
54
|
+
* @example transform: s => s as IsoDate
|
|
55
|
+
*/
|
|
56
|
+
transform?: (value: string) => unknown
|
|
42
57
|
}
|
|
43
58
|
|
|
44
59
|
export type CliOptions = Record<string, CliOption>
|
|
45
60
|
|
|
46
61
|
/**
|
|
47
|
-
* Element value type of a single option, derived from `
|
|
48
|
-
*
|
|
62
|
+
* Element value type of a single option, derived from `transform` (if present,
|
|
63
|
+
* its return type wins), then `choices`, then `type`. Defaults to `string` when
|
|
64
|
+
* none narrow it (so `type` is optional).
|
|
49
65
|
*/
|
|
50
|
-
type ElemType<O extends CliOption> = O extends {
|
|
51
|
-
?
|
|
52
|
-
: O[
|
|
53
|
-
?
|
|
54
|
-
: O
|
|
66
|
+
type ElemType<O extends CliOption> = O extends { transform: (...args: any[]) => infer R }
|
|
67
|
+
? R
|
|
68
|
+
: O extends { choices: readonly (infer C)[] }
|
|
69
|
+
? C
|
|
70
|
+
: O extends { type: 'number' }
|
|
55
71
|
? number
|
|
56
|
-
: O
|
|
72
|
+
: O extends { type: 'boolean' }
|
|
57
73
|
? boolean
|
|
58
|
-
:
|
|
74
|
+
: string
|
|
59
75
|
|
|
60
76
|
/**
|
|
61
77
|
* Full value type of a single option, applying `array` on top of the element
|
|
@@ -132,11 +148,12 @@ export class ParseArgsError extends Error {
|
|
|
132
148
|
* top of node's `util.parseArgs`.
|
|
133
149
|
*
|
|
134
150
|
* @example
|
|
135
|
-
* const { dir, limit } = _parseArgs({
|
|
136
|
-
* dir: {
|
|
151
|
+
* const { dir, limit, date } = _parseArgs({
|
|
152
|
+
* dir: { desc: 'Output directory' }, // type defaults to 'string'
|
|
137
153
|
* limit: { type: 'number', default: 100 },
|
|
154
|
+
* date: { transform: s => s as IsoDate }, // inferred + converted via transform
|
|
138
155
|
* })
|
|
139
|
-
* // dir?: string limit: number _: string[]
|
|
156
|
+
* // dir?: string limit: number date?: IsoDate _: string[]
|
|
140
157
|
*/
|
|
141
158
|
export function _parseArgs<const O extends CliOptions>(
|
|
142
159
|
options: O,
|
|
@@ -169,6 +186,7 @@ export function _parseArgs<const O extends CliOptions>(
|
|
|
169
186
|
options: nodeOptions,
|
|
170
187
|
allowPositionals: true,
|
|
171
188
|
allowNegative: true, // native `--no-flag` support for booleans
|
|
189
|
+
tokens: true, // needed to detect the ambiguous `--boolFlag value` space form
|
|
172
190
|
// In non-strict mode, unknown options are collected into `values` but
|
|
173
191
|
// ignored below, since we only read declared options. See `strict` docs.
|
|
174
192
|
strict,
|
|
@@ -185,10 +203,15 @@ export function _parseArgs<const O extends CliOptions>(
|
|
|
185
203
|
process.exit(0)
|
|
186
204
|
}
|
|
187
205
|
|
|
206
|
+
assertNoSpaceValuedBoolean(parsed.tokens, options)
|
|
207
|
+
|
|
188
208
|
const result: Record<string, unknown> = { _: parsed.positionals }
|
|
189
209
|
|
|
190
210
|
for (const [name, def] of Object.entries(options)) {
|
|
191
211
|
let v = values[name]
|
|
212
|
+
// `transform` is only applied to arg-sourced values; a `default` is taken
|
|
213
|
+
// to be in final form (see CliOption.transform docs).
|
|
214
|
+
const fromArgs = v !== undefined
|
|
192
215
|
|
|
193
216
|
if (v === undefined) {
|
|
194
217
|
if (def.default !== undefined) {
|
|
@@ -205,10 +228,35 @@ export function _parseArgs<const O extends CliOptions>(
|
|
|
205
228
|
v = Array.isArray(v) ? v : [v]
|
|
206
229
|
}
|
|
207
230
|
|
|
208
|
-
|
|
231
|
+
// A non-boolean option passed as a bare flag (`--out` with no value) comes back
|
|
232
|
+
// from node's parseArgs (in non-strict mode) as boolean `true`. Reject it: the
|
|
233
|
+
// user almost certainly forgot the value, and silently coercing `true` (to `1`
|
|
234
|
+
// for numbers, `"true"` for strings, or crashing a `transform`) would hide the
|
|
235
|
+
// mistake. Only arg-sourced values are checked; a boolean `default` is left
|
|
236
|
+
// alone. `def.type === 'boolean'` legitimately produces booleans, so skip it.
|
|
237
|
+
if (fromArgs && def.type !== 'boolean') {
|
|
238
|
+
const bareFlag = Array.isArray(v)
|
|
239
|
+
? v.some(x => typeof x === 'boolean')
|
|
240
|
+
: typeof v === 'boolean'
|
|
241
|
+
if (bareFlag) {
|
|
242
|
+
throw new ParseArgsError(`Missing value for --${name}`)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// `transform` owns conversion, so built-in number coercion is skipped for it
|
|
247
|
+
if (def.type === 'number' && !def.transform) {
|
|
209
248
|
v = Array.isArray(v) ? v.map(x => toNumber(x, name)) : toNumber(v, name)
|
|
210
249
|
}
|
|
211
250
|
|
|
251
|
+
// node's parseArgs (in non-strict mode) captures the inline value of
|
|
252
|
+
// `--flag=value` on a boolean option as a string ("false"/"true"), rather than
|
|
253
|
+
// rejecting it as strict mode does. Coerce known tokens so `--flag=false` means
|
|
254
|
+
// boolean false, not a truthy "false" string. Real booleans produced by
|
|
255
|
+
// `--flag` / `--no-flag` (and boolean defaults) pass through untouched.
|
|
256
|
+
if (def.type === 'boolean') {
|
|
257
|
+
v = Array.isArray(v) ? v.map(x => toBoolean(x, name)) : toBoolean(v, name)
|
|
258
|
+
}
|
|
259
|
+
|
|
212
260
|
if (def.choices) {
|
|
213
261
|
const list = Array.isArray(v) ? v : [v]
|
|
214
262
|
for (const x of list) {
|
|
@@ -220,6 +268,11 @@ export function _parseArgs<const O extends CliOptions>(
|
|
|
220
268
|
}
|
|
221
269
|
}
|
|
222
270
|
|
|
271
|
+
if (def.transform && fromArgs) {
|
|
272
|
+
const { transform } = def
|
|
273
|
+
v = Array.isArray(v) ? v.map(x => transform(x as string)) : transform(v as string)
|
|
274
|
+
}
|
|
275
|
+
|
|
223
276
|
result[name] = v
|
|
224
277
|
}
|
|
225
278
|
|
|
@@ -232,6 +285,34 @@ export function _parseArgs<const O extends CliOptions>(
|
|
|
232
285
|
return result as InferCliArgs<O>
|
|
233
286
|
}
|
|
234
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Reject the ambiguous `--boolFlag value` space form. node never consumes the
|
|
290
|
+
* next token as a boolean's value (getopt convention), so `--arg false` would
|
|
291
|
+
* silently yield `arg: true` and leak "false" into positionals. Unlike the
|
|
292
|
+
* `=value` form (handled by toBoolean) we can't recover the intended value here,
|
|
293
|
+
* so fail loudly. Only `true`/`false` tokens are treated as ambiguous; any other
|
|
294
|
+
* positional (e.g. a filename) is left as a genuine positional.
|
|
295
|
+
*/
|
|
296
|
+
function assertNoSpaceValuedBoolean(
|
|
297
|
+
tokens: NonNullable<ReturnType<typeof parseArgs>['tokens']>,
|
|
298
|
+
options: CliOptions,
|
|
299
|
+
): void {
|
|
300
|
+
for (let i = 0; i < tokens.length - 1; i++) {
|
|
301
|
+
const tok = tokens[i]!
|
|
302
|
+
// `tok.value === undefined` => bare flag (no inline `=value`); applies to
|
|
303
|
+
// declared boolean options only (unknown options are ignored, see `strict`).
|
|
304
|
+
if (tok.kind !== 'option' || tok.value !== undefined || options[tok.name]?.type !== 'boolean') {
|
|
305
|
+
continue
|
|
306
|
+
}
|
|
307
|
+
const next = tokens[i + 1]!
|
|
308
|
+
if (next.kind === 'positional' && (next.value === 'true' || next.value === 'false')) {
|
|
309
|
+
throw new ParseArgsError(
|
|
310
|
+
`Boolean option --${tok.name} does not take a space-separated value ("${next.value}"); use --${tok.name}=${next.value} or --${next.value === 'false' ? `no-${tok.name}` : tok.name}`,
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
235
316
|
function toNumber(raw: unknown, name: string): number {
|
|
236
317
|
const n = Number(raw)
|
|
237
318
|
if (Number.isNaN(n)) {
|
|
@@ -240,13 +321,21 @@ function toNumber(raw: unknown, name: string): number {
|
|
|
240
321
|
return n
|
|
241
322
|
}
|
|
242
323
|
|
|
324
|
+
function toBoolean(raw: unknown, name: string): boolean {
|
|
325
|
+
if (typeof raw === 'boolean') return raw // real boolean from --flag / --no-flag / default
|
|
326
|
+
if (raw === 'true') return true
|
|
327
|
+
if (raw === 'false') return false
|
|
328
|
+
throw new ParseArgsError(`Invalid boolean for --${name}: "${raw}"`)
|
|
329
|
+
}
|
|
330
|
+
|
|
243
331
|
function buildHelp(options: CliOptions, usage?: string): string {
|
|
244
332
|
const lines: string[] = []
|
|
245
333
|
if (usage) lines.push(usage, '')
|
|
246
334
|
lines.push('Options:')
|
|
247
335
|
for (const [name, def] of Object.entries(options)) {
|
|
248
336
|
const flag = def.short ? `-${def.short}, --${name}` : `--${name}`
|
|
249
|
-
const
|
|
337
|
+
const type = def.type ?? 'string'
|
|
338
|
+
const meta = [`[${def.array ? `${type}[]` : type}]`]
|
|
250
339
|
if (def.demandOption) meta.push('[required]')
|
|
251
340
|
if (def.default !== undefined) meta.push(`[default: ${JSON.stringify(def.default)}]`)
|
|
252
341
|
if (def.choices) meta.push(`[choices: ${def.choices.join(', ')}]`)
|