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