jackspeak 1.4.1 → 2.0.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/LICENSE.md +55 -0
- package/README.md +210 -300
- package/dist/cjs/index.d.ts +150 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +635 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/parse-args-cjs.d.ts.map +1 -0
- package/dist/cjs/parse-args-cjs.js.map +1 -0
- package/dist/cjs/parse-args.d.ts +4 -0
- package/dist/cjs/parse-args.js +42 -0
- package/dist/mjs/index.d.ts +150 -0
- package/dist/mjs/index.d.ts.map +1 -0
- package/dist/mjs/index.js +627 -0
- package/dist/mjs/index.js.map +1 -0
- package/dist/mjs/package.json +3 -0
- package/dist/mjs/parse-args-esm.d.ts.map +1 -0
- package/dist/mjs/parse-args-esm.js.map +1 -0
- package/dist/mjs/parse-args.d.ts +4 -0
- package/dist/mjs/parse-args.js +19 -0
- package/package.json +66 -12
- package/index.js +0 -652
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
import { inspect } from 'node:util';
|
|
2
|
+
import { parseArgs } from './parse-args.js';
|
|
3
|
+
// it's a tiny API, just cast it inline, it's fine
|
|
4
|
+
//@ts-ignore
|
|
5
|
+
import _cliui from 'cliui';
|
|
6
|
+
import { basename } from 'node:path';
|
|
7
|
+
const cliui = (o = {}) => _cliui(o);
|
|
8
|
+
const width = Math.min((process && process.stdout && process.stdout.columns) || 80, 80);
|
|
9
|
+
const toEnvKey = (pref, key) => {
|
|
10
|
+
return [pref, key.replace(/[^a-zA-Z0-9]+/g, ' ')]
|
|
11
|
+
.join(' ')
|
|
12
|
+
.trim()
|
|
13
|
+
.toUpperCase()
|
|
14
|
+
.replace(/ /g, '_');
|
|
15
|
+
};
|
|
16
|
+
const toEnvVal = (value, delim = '\n') => {
|
|
17
|
+
const str = typeof value === 'string'
|
|
18
|
+
? value
|
|
19
|
+
: typeof value === 'boolean'
|
|
20
|
+
? value
|
|
21
|
+
? '1'
|
|
22
|
+
: '0'
|
|
23
|
+
: typeof value === 'number'
|
|
24
|
+
? String(value)
|
|
25
|
+
: Array.isArray(value)
|
|
26
|
+
? value
|
|
27
|
+
.map((v) => toEnvVal(v))
|
|
28
|
+
.join(delim)
|
|
29
|
+
: /* c8 ignore start */
|
|
30
|
+
undefined;
|
|
31
|
+
if (typeof str !== 'string') {
|
|
32
|
+
throw new Error(`could not serialize value to environment: ${JSON.stringify(value)}`);
|
|
33
|
+
}
|
|
34
|
+
/* c8 ignore stop */
|
|
35
|
+
return str;
|
|
36
|
+
};
|
|
37
|
+
const fromEnvVal = (env, type, multiple, delim = '\n') => (multiple
|
|
38
|
+
? env.split(delim).map(v => fromEnvVal(v, type, false))
|
|
39
|
+
: type === 'string'
|
|
40
|
+
? env
|
|
41
|
+
: type === 'boolean'
|
|
42
|
+
? env === '1'
|
|
43
|
+
: +env.trim());
|
|
44
|
+
const isConfigType = (t) => typeof t === 'string' &&
|
|
45
|
+
(t === 'string' || t === 'number' || t === 'boolean');
|
|
46
|
+
const undefOrType = (v, t) => v === undefined || typeof v === t;
|
|
47
|
+
const isValidValue = (v, type, multi) => {
|
|
48
|
+
if (multi) {
|
|
49
|
+
if (!Array.isArray(v))
|
|
50
|
+
return false;
|
|
51
|
+
return !v.some((v) => !isValidValue(v, type, false));
|
|
52
|
+
}
|
|
53
|
+
if (Array.isArray(v))
|
|
54
|
+
return false;
|
|
55
|
+
return typeof v === type;
|
|
56
|
+
};
|
|
57
|
+
const isConfigOption = (o, type, multi) => !!o &&
|
|
58
|
+
typeof o === 'object' &&
|
|
59
|
+
isConfigType(o.type) &&
|
|
60
|
+
o.type === type &&
|
|
61
|
+
undefOrType(o.short, 'string') &&
|
|
62
|
+
undefOrType(o.description, 'string') &&
|
|
63
|
+
undefOrType(o.hint, 'string') &&
|
|
64
|
+
undefOrType(o.validate, 'function') &&
|
|
65
|
+
(o.default === undefined || isValidValue(o.default, type, multi)) &&
|
|
66
|
+
!!o.multiple === multi;
|
|
67
|
+
function num(o = {}) {
|
|
68
|
+
const { default: def, validate: val, ...rest } = o;
|
|
69
|
+
if (def !== undefined && !isValidValue(def, 'number', false)) {
|
|
70
|
+
throw new TypeError('invalid default value');
|
|
71
|
+
}
|
|
72
|
+
const validate = val
|
|
73
|
+
? val
|
|
74
|
+
: undefined;
|
|
75
|
+
return {
|
|
76
|
+
...rest,
|
|
77
|
+
default: def,
|
|
78
|
+
validate,
|
|
79
|
+
type: 'number',
|
|
80
|
+
multiple: false,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function numList(o = {}) {
|
|
84
|
+
const { default: def, validate: val, ...rest } = o;
|
|
85
|
+
if (def !== undefined && !isValidValue(def, 'number', true)) {
|
|
86
|
+
throw new TypeError('invalid default value');
|
|
87
|
+
}
|
|
88
|
+
const validate = val
|
|
89
|
+
? val
|
|
90
|
+
: undefined;
|
|
91
|
+
return {
|
|
92
|
+
...rest,
|
|
93
|
+
default: def,
|
|
94
|
+
validate,
|
|
95
|
+
type: 'number',
|
|
96
|
+
multiple: true,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function opt(o = {}) {
|
|
100
|
+
const { default: def, validate: val, ...rest } = o;
|
|
101
|
+
if (def !== undefined && !isValidValue(def, 'string', false)) {
|
|
102
|
+
throw new TypeError('invalid default value');
|
|
103
|
+
}
|
|
104
|
+
const validate = val
|
|
105
|
+
? val
|
|
106
|
+
: undefined;
|
|
107
|
+
return {
|
|
108
|
+
...rest,
|
|
109
|
+
default: def,
|
|
110
|
+
validate,
|
|
111
|
+
type: 'string',
|
|
112
|
+
multiple: false,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function optList(o = {}) {
|
|
116
|
+
const { default: def, validate: val, ...rest } = o;
|
|
117
|
+
if (def !== undefined && !isValidValue(def, 'string', true)) {
|
|
118
|
+
throw new TypeError('invalid default value');
|
|
119
|
+
}
|
|
120
|
+
const validate = val
|
|
121
|
+
? val
|
|
122
|
+
: undefined;
|
|
123
|
+
return {
|
|
124
|
+
...rest,
|
|
125
|
+
default: def,
|
|
126
|
+
validate,
|
|
127
|
+
type: 'string',
|
|
128
|
+
multiple: true,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function flag(o = {}) {
|
|
132
|
+
const { hint, default: def, validate: val, ...rest } = o;
|
|
133
|
+
if (def !== undefined && !isValidValue(def, 'boolean', false)) {
|
|
134
|
+
throw new TypeError('invalid default value');
|
|
135
|
+
}
|
|
136
|
+
const validate = val
|
|
137
|
+
? val
|
|
138
|
+
: undefined;
|
|
139
|
+
if (hint !== undefined) {
|
|
140
|
+
throw new TypeError('cannot provide hint for flag');
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
...rest,
|
|
144
|
+
default: def,
|
|
145
|
+
validate,
|
|
146
|
+
type: 'boolean',
|
|
147
|
+
multiple: false,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function flagList(o = {}) {
|
|
151
|
+
const { hint, default: def, validate: val, ...rest } = o;
|
|
152
|
+
if (def !== undefined && !isValidValue(def, 'boolean', true)) {
|
|
153
|
+
throw new TypeError('invalid default value');
|
|
154
|
+
}
|
|
155
|
+
const validate = val
|
|
156
|
+
? val
|
|
157
|
+
: undefined;
|
|
158
|
+
if (hint !== undefined) {
|
|
159
|
+
throw new TypeError('cannot provide hint for flag list');
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
...rest,
|
|
163
|
+
default: def,
|
|
164
|
+
validate,
|
|
165
|
+
type: 'boolean',
|
|
166
|
+
multiple: true,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const toParseArgsOptionsConfig = (options) => {
|
|
170
|
+
const c = {};
|
|
171
|
+
for (const longOption in options) {
|
|
172
|
+
const config = options[longOption];
|
|
173
|
+
if (isConfigOption(config, 'number', true)) {
|
|
174
|
+
c[longOption] = {
|
|
175
|
+
type: 'string',
|
|
176
|
+
multiple: true,
|
|
177
|
+
default: config.default?.map(c => String(c)),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
else if (isConfigOption(config, 'number', false)) {
|
|
181
|
+
c[longOption] = {
|
|
182
|
+
type: 'string',
|
|
183
|
+
multiple: false,
|
|
184
|
+
default: config.default === undefined
|
|
185
|
+
? undefined
|
|
186
|
+
: String(config.default),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
const conf = config;
|
|
191
|
+
c[longOption] = {
|
|
192
|
+
type: conf.type,
|
|
193
|
+
multiple: conf.multiple,
|
|
194
|
+
default: conf.default,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
if (typeof config.short === 'string') {
|
|
198
|
+
c[longOption].short = config.short;
|
|
199
|
+
}
|
|
200
|
+
if (config.type === 'boolean' &&
|
|
201
|
+
!longOption.startsWith('no-') &&
|
|
202
|
+
!options[`no-${longOption}`]) {
|
|
203
|
+
c[`no-${longOption}`] = {
|
|
204
|
+
type: 'boolean',
|
|
205
|
+
multiple: config.multiple,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return c;
|
|
210
|
+
};
|
|
211
|
+
export class Jack {
|
|
212
|
+
#configSet;
|
|
213
|
+
#shorts;
|
|
214
|
+
#options;
|
|
215
|
+
#fields = [];
|
|
216
|
+
#env;
|
|
217
|
+
#envPrefix;
|
|
218
|
+
#allowPositionals;
|
|
219
|
+
#usage;
|
|
220
|
+
constructor(options = {}) {
|
|
221
|
+
this.#options = options;
|
|
222
|
+
this.#allowPositionals = options.allowPositionals !== false;
|
|
223
|
+
this.#env =
|
|
224
|
+
this.#options.env === undefined ? process.env : this.#options.env;
|
|
225
|
+
this.#envPrefix = options.envPrefix;
|
|
226
|
+
// We need to fib a little, because it's always the same object, but it
|
|
227
|
+
// starts out as having an empty config set. Then each method that adds
|
|
228
|
+
// fields returns `new Jack<C & { ...newConfigs }>()`
|
|
229
|
+
this.#configSet = Object.create(null);
|
|
230
|
+
this.#shorts = Object.create(null);
|
|
231
|
+
}
|
|
232
|
+
parse(args = process.argv) {
|
|
233
|
+
if (args === process.argv) {
|
|
234
|
+
args = args.slice(process._eval !== undefined ? 1 : 2);
|
|
235
|
+
}
|
|
236
|
+
if (this.#envPrefix) {
|
|
237
|
+
for (const [field, my] of Object.entries(this.#configSet)) {
|
|
238
|
+
const ek = toEnvKey(this.#envPrefix, field);
|
|
239
|
+
const env = this.#env[ek];
|
|
240
|
+
if (env !== undefined) {
|
|
241
|
+
my.default = fromEnvVal(env, my.type, !!my.multiple, my.delim);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const options = toParseArgsOptionsConfig(this.#configSet);
|
|
246
|
+
const result = parseArgs({
|
|
247
|
+
args,
|
|
248
|
+
options,
|
|
249
|
+
// always strict, but using our own logic
|
|
250
|
+
strict: false,
|
|
251
|
+
allowPositionals: this.#allowPositionals,
|
|
252
|
+
tokens: true,
|
|
253
|
+
});
|
|
254
|
+
const p = {
|
|
255
|
+
values: {},
|
|
256
|
+
positionals: [],
|
|
257
|
+
};
|
|
258
|
+
for (const token of result.tokens) {
|
|
259
|
+
if (token.kind === 'positional') {
|
|
260
|
+
p.positionals.push(token.value);
|
|
261
|
+
if (this.#options.stopAtPositional) {
|
|
262
|
+
p.positionals.push(...args.slice(token.index + 1));
|
|
263
|
+
return p;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
else if (token.kind === 'option') {
|
|
267
|
+
let value = undefined;
|
|
268
|
+
if (token.name.startsWith('no-')) {
|
|
269
|
+
const my = this.#configSet[token.name];
|
|
270
|
+
const pname = token.name.substring('no-'.length);
|
|
271
|
+
const pos = this.#configSet[pname];
|
|
272
|
+
if (pos &&
|
|
273
|
+
pos.type === 'boolean' &&
|
|
274
|
+
(!my ||
|
|
275
|
+
(my.type === 'boolean' && !!my.multiple === !!pos.multiple))) {
|
|
276
|
+
value = false;
|
|
277
|
+
token.name = pname;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const my = this.#configSet[token.name];
|
|
281
|
+
if (!my) {
|
|
282
|
+
throw new Error(`Unknown option '${token.rawName}'. ` +
|
|
283
|
+
`To specify a positional argument starting with a '-', ` +
|
|
284
|
+
`place it at the end of the command after '--', as in ` +
|
|
285
|
+
`'-- ${token.rawName}'`);
|
|
286
|
+
}
|
|
287
|
+
if (value === undefined) {
|
|
288
|
+
if (token.value === undefined) {
|
|
289
|
+
if (my.type !== 'boolean') {
|
|
290
|
+
throw new Error(`No value provided for ${token.rawName}, expected ${my.type}`);
|
|
291
|
+
}
|
|
292
|
+
value = true;
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
if (my.type === 'boolean') {
|
|
296
|
+
throw new Error(`Flag ${token.rawName} does not take a value, received '${token.value}'`);
|
|
297
|
+
}
|
|
298
|
+
if (my.type === 'string') {
|
|
299
|
+
value = token.value;
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
value = +token.value;
|
|
303
|
+
if (value !== value) {
|
|
304
|
+
throw new Error(`Invalid value '${token.value}' provided for ` +
|
|
305
|
+
`'${token.rawName}' option, expected number`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (my.multiple) {
|
|
311
|
+
const pv = p.values;
|
|
312
|
+
pv[token.name] = pv[token.name] ?? [];
|
|
313
|
+
pv[token.name].push(value);
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
const pv = p.values;
|
|
317
|
+
pv[token.name] = value;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
for (const [field, c] of Object.entries(this.#configSet)) {
|
|
322
|
+
if (c.default !== undefined && !(field in p.values)) {
|
|
323
|
+
//@ts-ignore
|
|
324
|
+
p.values[field] = c.default;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
for (const [field, value] of Object.entries(p.values)) {
|
|
328
|
+
const valid = this.#configSet[field].validate;
|
|
329
|
+
if (valid && !valid(value)) {
|
|
330
|
+
throw new Error(`Invalid value provided for --${field}: ${JSON.stringify(value)}`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
this.#writeEnv(p);
|
|
334
|
+
return p;
|
|
335
|
+
}
|
|
336
|
+
validate(o) {
|
|
337
|
+
if (!o || typeof o !== 'object') {
|
|
338
|
+
throw new Error('Invalid config: not an object');
|
|
339
|
+
}
|
|
340
|
+
for (const field in o) {
|
|
341
|
+
const config = this.#configSet[field];
|
|
342
|
+
if (!config) {
|
|
343
|
+
throw new Error(`Unknown config option: ${field}`);
|
|
344
|
+
}
|
|
345
|
+
if (!isValidValue(o[field], config.type, !!config.multiple)) {
|
|
346
|
+
throw new Error(`Invalid type for ${field} option, expected` +
|
|
347
|
+
`${config.type}${config.multiple ? '[]' : ''}`);
|
|
348
|
+
}
|
|
349
|
+
if (config.validate && !config.validate(o[field])) {
|
|
350
|
+
throw new Error(`Invalid config value for ${field}: ${o[field]}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
#writeEnv(p) {
|
|
355
|
+
if (!this.#env || !this.#envPrefix)
|
|
356
|
+
return;
|
|
357
|
+
for (const [field, value] of Object.entries(p.values)) {
|
|
358
|
+
const my = this.#configSet[field];
|
|
359
|
+
this.#env[toEnvKey(this.#envPrefix, field)] = toEnvVal(value, my.delim);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
heading(text) {
|
|
363
|
+
this.#fields.push({ type: 'heading', text });
|
|
364
|
+
return this;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Add a long-form description to the usage output at this position.
|
|
368
|
+
*/
|
|
369
|
+
description(text) {
|
|
370
|
+
this.#fields.push({ type: 'description', text });
|
|
371
|
+
return this;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Add one or more number fields.
|
|
375
|
+
*/
|
|
376
|
+
num(fields) {
|
|
377
|
+
return this.#addFields(fields, num);
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Add one or more multiple number fields.
|
|
381
|
+
*/
|
|
382
|
+
numList(fields) {
|
|
383
|
+
return this.#addFields(fields, numList);
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Add one or more string option fields.
|
|
387
|
+
*/
|
|
388
|
+
opt(fields) {
|
|
389
|
+
return this.#addFields(fields, opt);
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Add one or more multiple string option fields.
|
|
393
|
+
*/
|
|
394
|
+
optList(fields) {
|
|
395
|
+
return this.#addFields(fields, optList);
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Add one or more flag fields.
|
|
399
|
+
*/
|
|
400
|
+
flag(fields) {
|
|
401
|
+
return this.#addFields(fields, flag);
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Add one or more multiple flag fields.
|
|
405
|
+
*/
|
|
406
|
+
flagList(fields) {
|
|
407
|
+
return this.#addFields(fields, flagList);
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Generic field definition method. Similar to flag/flagList/number/etc,
|
|
411
|
+
* but you must specify the `type` (and optionally `multiple` and `delim`)
|
|
412
|
+
* fields on each one, or Jack won't know how to define them.
|
|
413
|
+
*/
|
|
414
|
+
addFields(fields) {
|
|
415
|
+
const next = this;
|
|
416
|
+
for (const [name, field] of Object.entries(fields)) {
|
|
417
|
+
this.#validateName(name, field);
|
|
418
|
+
next.#fields.push({
|
|
419
|
+
type: 'config',
|
|
420
|
+
name,
|
|
421
|
+
value: field,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
Object.assign(next.#configSet, fields);
|
|
425
|
+
return next;
|
|
426
|
+
}
|
|
427
|
+
#addFields(fields, fn) {
|
|
428
|
+
const next = this;
|
|
429
|
+
Object.assign(next.#configSet, Object.fromEntries(Object.entries(fields).map(([name, field]) => {
|
|
430
|
+
this.#validateName(name, field);
|
|
431
|
+
const option = fn(field);
|
|
432
|
+
next.#fields.push({
|
|
433
|
+
type: 'config',
|
|
434
|
+
name,
|
|
435
|
+
value: option,
|
|
436
|
+
});
|
|
437
|
+
return [name, option];
|
|
438
|
+
})));
|
|
439
|
+
return next;
|
|
440
|
+
}
|
|
441
|
+
#validateName(name, field) {
|
|
442
|
+
if (!/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(name)) {
|
|
443
|
+
throw new TypeError(`Invalid option name: ${name}, ` +
|
|
444
|
+
`must be '-' delimited ASCII alphanumeric`);
|
|
445
|
+
}
|
|
446
|
+
if (this.#configSet[name]) {
|
|
447
|
+
throw new TypeError(`Cannot redefine option ${field}`);
|
|
448
|
+
}
|
|
449
|
+
if (this.#shorts[name]) {
|
|
450
|
+
throw new TypeError(`Cannot redefine option ${name}, already ` +
|
|
451
|
+
`in use for ${this.#shorts[name]}`);
|
|
452
|
+
}
|
|
453
|
+
if (field.short) {
|
|
454
|
+
if (!/^[a-zA-Z0-9]$/.test(field.short)) {
|
|
455
|
+
throw new TypeError(`Invalid ${name} short option: ${field.short}, ` +
|
|
456
|
+
'must be 1 ASCII alphanumeric character');
|
|
457
|
+
}
|
|
458
|
+
if (this.#shorts[field.short]) {
|
|
459
|
+
throw new TypeError(`Invalid ${name} short option: ${field.short}, ` +
|
|
460
|
+
`already in use for ${this.#shorts[field.short]}`);
|
|
461
|
+
}
|
|
462
|
+
this.#shorts[field.short] = name;
|
|
463
|
+
this.#shorts[name] = name;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
usage() {
|
|
467
|
+
if (this.#usage)
|
|
468
|
+
return this.#usage;
|
|
469
|
+
const ui = cliui({ width });
|
|
470
|
+
let start = this.#fields[0]?.type === 'heading' ? 1 : 0;
|
|
471
|
+
if (this.#fields[0]?.type === 'heading') {
|
|
472
|
+
ui.div({ text: normalize(this.#fields[0].text) });
|
|
473
|
+
}
|
|
474
|
+
ui.div({ text: 'Usage:' });
|
|
475
|
+
if (this.#options.usage) {
|
|
476
|
+
ui.div({
|
|
477
|
+
text: this.#options.usage,
|
|
478
|
+
padding: [0, 0, 0, 2],
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
else {
|
|
482
|
+
const cmd = basename(process.argv[1]);
|
|
483
|
+
const shortFlags = [];
|
|
484
|
+
const shorts = [];
|
|
485
|
+
const flags = [];
|
|
486
|
+
const opts = [];
|
|
487
|
+
for (const [field, config] of Object.entries(this.#configSet)) {
|
|
488
|
+
if (config.short) {
|
|
489
|
+
if (config.type === 'boolean')
|
|
490
|
+
shortFlags.push(config.short);
|
|
491
|
+
else
|
|
492
|
+
shorts.push([config.short, config.hint || field]);
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
if (config.type === 'boolean')
|
|
496
|
+
flags.push(field);
|
|
497
|
+
else
|
|
498
|
+
opts.push([field, config.hint || field]);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
const sf = shortFlags.length ? ' -' + shortFlags.join('') : '';
|
|
502
|
+
const so = shorts.map(([k, v]) => ` --${k}=<${v}>`).join('');
|
|
503
|
+
const lf = flags.map(k => ` --${k}`).join('');
|
|
504
|
+
const lo = opts.map(([k, v]) => ` --${k}=<${v}>`).join('');
|
|
505
|
+
const usage = `${cmd}${sf}${so}${lf}${lo}`.trim();
|
|
506
|
+
ui.div({
|
|
507
|
+
text: usage,
|
|
508
|
+
padding: [0, 0, 0, 2],
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
ui.div({ text: '' });
|
|
512
|
+
const maybeDesc = this.#fields[start];
|
|
513
|
+
if (maybeDesc?.type === 'description') {
|
|
514
|
+
start++;
|
|
515
|
+
ui.div({
|
|
516
|
+
text: normalize(maybeDesc.text),
|
|
517
|
+
});
|
|
518
|
+
ui.div({ text: '' });
|
|
519
|
+
}
|
|
520
|
+
// turn each config type into a row, and figure out the width of the
|
|
521
|
+
// left hand indentation for the option descriptions.
|
|
522
|
+
let maxMax = Math.max(12, Math.min(26, Math.floor(width / 3)));
|
|
523
|
+
let maxWidth = 8;
|
|
524
|
+
let prev = undefined;
|
|
525
|
+
const rows = [];
|
|
526
|
+
for (const field of this.#fields.slice(start)) {
|
|
527
|
+
if (field.type !== 'config') {
|
|
528
|
+
if (prev)
|
|
529
|
+
prev.skipLine = true;
|
|
530
|
+
prev = undefined;
|
|
531
|
+
field.text = normalize(field.text);
|
|
532
|
+
rows.push(field);
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
const { value } = field;
|
|
536
|
+
const desc = value.description || '';
|
|
537
|
+
const mult = value.multiple ? 'Can be set multiple times' : '';
|
|
538
|
+
const dmDelim = mult && (desc.includes('\n') ? '\n\n' : '\n');
|
|
539
|
+
const text = normalize(desc + dmDelim + mult);
|
|
540
|
+
const hint = value.hint ||
|
|
541
|
+
(value.type === 'number'
|
|
542
|
+
? 'n'
|
|
543
|
+
: value.type === 'string'
|
|
544
|
+
? field.name
|
|
545
|
+
: undefined);
|
|
546
|
+
const short = !value.short
|
|
547
|
+
? ''
|
|
548
|
+
: value.type === 'boolean'
|
|
549
|
+
? `-${value.short} `
|
|
550
|
+
: `-${value.short}<${hint}> `;
|
|
551
|
+
const left = value.type === 'boolean'
|
|
552
|
+
? `${short}--${field.name}`
|
|
553
|
+
: `${short}--${field.name}=<${hint}>`;
|
|
554
|
+
const row = { text, left };
|
|
555
|
+
if (text.length > width - maxMax) {
|
|
556
|
+
row.skipLine = true;
|
|
557
|
+
}
|
|
558
|
+
if (prev && left.length > maxMax)
|
|
559
|
+
prev.skipLine = true;
|
|
560
|
+
prev = row;
|
|
561
|
+
const len = left.length + 4;
|
|
562
|
+
if (len > maxWidth && len < maxMax) {
|
|
563
|
+
maxWidth = len;
|
|
564
|
+
}
|
|
565
|
+
rows.push(row);
|
|
566
|
+
}
|
|
567
|
+
for (const row of rows) {
|
|
568
|
+
if (row.left) {
|
|
569
|
+
// If the row is too long, don't wrap it
|
|
570
|
+
// Bump the right-hand side down a line to make room
|
|
571
|
+
if (row.left.length > maxWidth - 2) {
|
|
572
|
+
ui.div({ text: row.left, padding: [0, 0, 0, 2] });
|
|
573
|
+
ui.div({ text: row.text, padding: [0, 0, 0, maxWidth] });
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
ui.div({
|
|
577
|
+
text: row.left,
|
|
578
|
+
padding: [0, 1, 0, 2],
|
|
579
|
+
width: maxWidth,
|
|
580
|
+
}, { text: row.text });
|
|
581
|
+
}
|
|
582
|
+
if (row.skipLine) {
|
|
583
|
+
ui.div({ text: '' });
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
if (row.type === 'heading') {
|
|
588
|
+
ui.div(row);
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
ui.div({ ...row, padding: [1, 0, 1, 2] });
|
|
592
|
+
}
|
|
593
|
+
ui.div();
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
return (this.#usage = ui.toString());
|
|
597
|
+
}
|
|
598
|
+
toJSON() {
|
|
599
|
+
return Object.fromEntries(Object.entries(this.#configSet).map(([field, def]) => [
|
|
600
|
+
field,
|
|
601
|
+
{
|
|
602
|
+
type: def.type,
|
|
603
|
+
...(def.multiple ? { multiple: true } : {}),
|
|
604
|
+
...(def.delim ? { delim: def.delim } : {}),
|
|
605
|
+
...(def.short ? { short: def.short } : {}),
|
|
606
|
+
...(def.description ? { description: def.description } : {}),
|
|
607
|
+
...(def.validate ? { validate: def.validate } : {}),
|
|
608
|
+
...(def.default !== undefined ? { default: def.default } : {}),
|
|
609
|
+
},
|
|
610
|
+
]));
|
|
611
|
+
}
|
|
612
|
+
[inspect.custom](_, options) {
|
|
613
|
+
return `Jack ${inspect(this.toJSON(), options)}`;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
// Unwrap and un-indent, so we can wrap description
|
|
617
|
+
// strings however makes them look nice in the code.
|
|
618
|
+
const normalize = (s) => s
|
|
619
|
+
// remove single line breaks
|
|
620
|
+
.replace(/([^\n])\n[ \t]*([^\n])/g, '$1 $2')
|
|
621
|
+
// normalize mid-line whitespace
|
|
622
|
+
.replace(/([^\n])[ \t]+([^\n])/g, '$1 $2')
|
|
623
|
+
// two line breaks are enough
|
|
624
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
625
|
+
.trim();
|
|
626
|
+
export const jack = (options = {}) => new Jack(options);
|
|
627
|
+
//# sourceMappingURL=index.js.map
|