1import { inspect } from 'node:util';
2import { parseArgs } from './parse-args.js';
3// it's a tiny API, just cast it inline, it's fine
4//@ts-ignore
5import cliui from '@isaacs/cliui';
6import { basename } from 'node:path';
7const width = Math.min((process && process.stdout && process.stdout.columns) || 80, 80);
8// indentation spaces from heading level
9const indent = (n) => (n - 1) * 2;
10const toEnvKey = (pref, key) => {
11    return [pref, key.replace(/[^a-zA-Z0-9]+/g, ' ')]
12        .join(' ')
13        .trim()
14        .toUpperCase()
15        .replace(/ /g, '_');
16};
17const toEnvVal = (value, delim = '\n') => {
18    const str = typeof value === 'string'
19        ? value
20        : typeof value === 'boolean'
21            ? value
22                ? '1'
23                : '0'
24            : typeof value === 'number'
25                ? String(value)
26                : Array.isArray(value)
27                    ? value
28                        .map((v) => toEnvVal(v))
29                        .join(delim)
30                    : /* c8 ignore start */
31                        undefined;
32    if (typeof str !== 'string') {
33        throw new Error(`could not serialize value to environment: ${JSON.stringify(value)}`);
34    }
35    /* c8 ignore stop */
36    return str;
37};
38const fromEnvVal = (env, type, multiple, delim = '\n') => (multiple
39    ? env
40        ? env.split(delim).map(v => fromEnvVal(v, type, false))
41        : []
42    : type === 'string'
43        ? env
44        : type === 'boolean'
45            ? env === '1'
46            : +env.trim());
47export const isConfigType = (t) => typeof t === 'string' &&
48    (t === 'string' || t === 'number' || t === 'boolean');
49const undefOrType = (v, t) => v === undefined || typeof v === t;
50// print the value type, for error message reporting
51const valueType = (v) => typeof v === 'string'
52    ? 'string'
53    : typeof v === 'boolean'
54        ? 'boolean'
55        : typeof v === 'number'
56            ? 'number'
57            : Array.isArray(v)
58                ? joinTypes([...new Set(v.map(v => valueType(v)))]) + '[]'
59                : `${v.type}${v.multiple ? '[]' : ''}`;
60const joinTypes = (types) => types.length === 1 && typeof types[0] === 'string'
61    ? types[0]
62    : `(${types.join('|')})`;
63const isValidValue = (v, type, multi) => {
64    if (multi) {
65        if (!Array.isArray(v))
66            return false;
67        return !v.some((v) => !isValidValue(v, type, false));
68    }
69    if (Array.isArray(v))
70        return false;
71    return typeof v === type;
72};
73export const isConfigOption = (o, type, multi) => !!o &&
74    typeof o === 'object' &&
75    isConfigType(o.type) &&
76    o.type === type &&
77    undefOrType(o.short, 'string') &&
78    undefOrType(o.description, 'string') &&
79    undefOrType(o.hint, 'string') &&
80    undefOrType(o.validate, 'function') &&
81    (o.default === undefined || isValidValue(o.default, type, multi)) &&
82    !!o.multiple === multi;
83function num(o = {}) {
84    const { default: def, validate: val, ...rest } = o;
85    if (def !== undefined && !isValidValue(def, 'number', false)) {
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: false,
97    };
98}
99function numList(o = {}) {
100    const { default: def, validate: val, ...rest } = o;
101    if (def !== undefined && !isValidValue(def, 'number', true)) {
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: 'number',
112        multiple: true,
113    };
114}
115function opt(o = {}) {
116    const { default: def, validate: val, ...rest } = o;
117    if (def !== undefined && !isValidValue(def, 'string', false)) {
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: false,
129    };
130}
131function optList(o = {}) {
132    const { default: def, validate: val, ...rest } = o;
133    if (def !== undefined && !isValidValue(def, 'string', true)) {
134        throw new TypeError('invalid default value');
135    }
136    const validate = val
137        ? val
138        : undefined;
139    return {
140        ...rest,
141        default: def,
142        validate,
143        type: 'string',
144        multiple: true,
145    };
146}
147function flag(o = {}) {
148    const { hint, default: def, validate: val, ...rest } = o;
149    if (def !== undefined && !isValidValue(def, 'boolean', false)) {
150        throw new TypeError('invalid default value');
151    }
152    const validate = val
153        ? val
154        : undefined;
155    if (hint !== undefined) {
156        throw new TypeError('cannot provide hint for flag');
157    }
158    return {
159        ...rest,
160        default: def,
161        validate,
162        type: 'boolean',
163        multiple: false,
164    };
165}
166function flagList(o = {}) {
167    const { hint, default: def, validate: val, ...rest } = o;
168    if (def !== undefined && !isValidValue(def, 'boolean', true)) {
169        throw new TypeError('invalid default value');
170    }
171    const validate = val
172        ? val
173        : undefined;
174    if (hint !== undefined) {
175        throw new TypeError('cannot provide hint for flag list');
176    }
177    return {
178        ...rest,
179        default: def,
180        validate,
181        type: 'boolean',
182        multiple: true,
183    };
184}
185const toParseArgsOptionsConfig = (options) => {
186    const c = {};
187    for (const longOption in options) {
188        const config = options[longOption];
189        /* c8 ignore start */
190        if (!config) {
191            throw new Error('config must be an object: ' + longOption);
192        }
193        /* c8 ignore start */
194        if (isConfigOption(config, 'number', true)) {
195            c[longOption] = {
196                type: 'string',
197                multiple: true,
198                default: config.default?.map(c => String(c)),
199            };
200        }
201        else if (isConfigOption(config, 'number', false)) {
202            c[longOption] = {
203                type: 'string',
204                multiple: false,
205                default: config.default === undefined
206                    ? undefined
207                    : String(config.default),
208            };
209        }
210        else {
211            const conf = config;
212            c[longOption] = {
213                type: conf.type,
214                multiple: conf.multiple,
215                default: conf.default,
216            };
217        }
218        const clo = c[longOption];
219        if (typeof config.short === 'string') {
220            clo.short = config.short;
221        }
222        if (config.type === 'boolean' &&
223            !longOption.startsWith('no-') &&
224            !options[`no-${longOption}`]) {
225            c[`no-${longOption}`] = {
226                type: 'boolean',
227                multiple: config.multiple,
228            };
229        }
230    }
231    return c;
232};
233const isHeading = (r) => r.type === 'heading';
234const isDescription = (r) => r.type === 'description';
235/**
236 * Class returned by the {@link jack} function and all configuration
237 * definition methods.  This is what gets chained together.
238 */
239export class Jack {
240    #configSet;
241    #shorts;
242    #options;
243    #fields = [];
244    #env;
245    #envPrefix;
246    #allowPositionals;
247    #usage;
248    #usageMarkdown;
249    constructor(options = {}) {
250        this.#options = options;
251        this.#allowPositionals = options.allowPositionals !== false;
252        this.#env =
253            this.#options.env === undefined ? process.env : this.#options.env;
254        this.#envPrefix = options.envPrefix;
255        // We need to fib a little, because it's always the same object, but it
256        // starts out as having an empty config set.  Then each method that adds
257        // fields returns `this as Jack<C & { ...newConfigs }>`
258        this.#configSet = Object.create(null);
259        this.#shorts = Object.create(null);
260    }
261    /**
262     * Set the default value (which will still be overridden by env or cli)
263     * as if from a parsed config file. The optional `source` param, if
264     * provided, will be included in error messages if a value is invalid or
265     * unknown.
266     */
267    setConfigValues(values, source = '') {
268        try {
269            this.validate(values);
270        }
271        catch (er) {
272            throw Object.assign(er, source ? { source } : {});
273        }
274        for (const [field, value] of Object.entries(values)) {
275            const my = this.#configSet[field];
276            // already validated, just for TS's benefit
277            /* c8 ignore start */
278            if (!my) {
279                throw new Error('unexpected field in config set: ' + field);
280            }
281            /* c8 ignore stop */
282            my.default = value;
283        }
284        return this;
285    }
286    /**
287     * Parse a string of arguments, and return the resulting
288     * `{ values, positionals }` object.
289     *
290     * If an {@link JackOptions#envPrefix} is set, then it will read default
291     * values from the environment, and write the resulting values back
292     * to the environment as well.
293     *
294     * Environment values always take precedence over any other value, except
295     * an explicit CLI setting.
296     */
297    parse(args = process.argv) {
298        if (args === process.argv) {
299            args = args.slice(process._eval !== undefined ? 1 : 2);
300        }
301        if (this.#envPrefix) {
302            for (const [field, my] of Object.entries(this.#configSet)) {
303                const ek = toEnvKey(this.#envPrefix, field);
304                const env = this.#env[ek];
305                if (env !== undefined) {
306                    my.default = fromEnvVal(env, my.type, !!my.multiple, my.delim);
307                }
308            }
309        }
310        const options = toParseArgsOptionsConfig(this.#configSet);
311        const result = parseArgs({
312            args,
313            options,
314            // always strict, but using our own logic
315            strict: false,
316            allowPositionals: this.#allowPositionals,
317            tokens: true,
318        });
319        const p = {
320            values: {},
321            positionals: [],
322        };
323        for (const token of result.tokens) {
324            if (token.kind === 'positional') {
325                p.positionals.push(token.value);
326                if (this.#options.stopAtPositional) {
327                    p.positionals.push(...args.slice(token.index + 1));
328                    return p;
329                }
330            }
331            else if (token.kind === 'option') {
332                let value = undefined;
333                if (token.name.startsWith('no-')) {
334                    const my = this.#configSet[token.name];
335                    const pname = token.name.substring('no-'.length);
336                    const pos = this.#configSet[pname];
337                    if (pos &&
338                        pos.type === 'boolean' &&
339                        (!my ||
340                            (my.type === 'boolean' && !!my.multiple === !!pos.multiple))) {
341                        value = false;
342                        token.name = pname;
343                    }
344                }
345                const my = this.#configSet[token.name];
346                if (!my) {
347                    throw new Error(`Unknown option '${token.rawName}'. ` +
348                        `To specify a positional argument starting with a '-', ` +
349                        `place it at the end of the command after '--', as in ` +
350                        `'-- ${token.rawName}'`);
351                }
352                if (value === undefined) {
353                    if (token.value === undefined) {
354                        if (my.type !== 'boolean') {
355                            throw new Error(`No value provided for ${token.rawName}, expected ${my.type}`);
356                        }
357                        value = true;
358                    }
359                    else {
360                        if (my.type === 'boolean') {
361                            throw new Error(`Flag ${token.rawName} does not take a value, received '${token.value}'`);
362                        }
363                        if (my.type === 'string') {
364                            value = token.value;
365                        }
366                        else {
367                            value = +token.value;
368                            if (value !== value) {
369                                throw new Error(`Invalid value '${token.value}' provided for ` +
370                                    `'${token.rawName}' option, expected number`);
371                            }
372                        }
373                    }
374                }
375                if (my.multiple) {
376                    const pv = p.values;
377                    const tn = pv[token.name] ?? [];
378                    pv[token.name] = tn;
379                    tn.push(value);
380                }
381                else {
382                    const pv = p.values;
383                    pv[token.name] = value;
384                }
385            }
386        }
387        for (const [field, c] of Object.entries(this.#configSet)) {
388            if (c.default !== undefined && !(field in p.values)) {
389                //@ts-ignore
390                p.values[field] = c.default;
391            }
392        }
393        for (const [field, value] of Object.entries(p.values)) {
394            const valid = this.#configSet[field]?.validate;
395            if (valid && !valid(value)) {
396                throw new Error(`Invalid value provided for --${field}: ${JSON.stringify(value)}`);
397            }
398        }
399        this.#writeEnv(p);
400        return p;
401    }
402    /**
403     * do not set fields as 'no-foo' if 'foo' exists and both are bools
404     * just set foo.
405     */
406    #noNoFields(f, val, s = f) {
407        if (!f.startsWith('no-') || typeof val !== 'boolean')
408            return;
409        const yes = f.substring('no-'.length);
410        // recurse so we get the core config key we care about.
411        this.#noNoFields(yes, val, s);
412        if (this.#configSet[yes]?.type === 'boolean') {
413            throw new Error(`do not set '${s}', instead set '${yes}' as desired.`);
414        }
415    }
416    /**
417     * Validate that any arbitrary object is a valid configuration `values`
418     * object.  Useful when loading config files or other sources.
419     */
420    validate(o) {
421        if (!o || typeof o !== 'object') {
422            throw new Error('Invalid config: not an object');
423        }
424        for (const field in o) {
425            this.#noNoFields(field, o[field]);
426            const config = this.#configSet[field];
427            if (!config) {
428                throw new Error(`Unknown config option: ${field}`);
429            }
430            if (!isValidValue(o[field], config.type, !!config.multiple)) {
431                throw Object.assign(new Error(`Invalid value ${valueType(o[field])} for ${field}, expected ${valueType(config)}`), {
432                    field,
433                    value: o[field],
434                });
435            }
436            if (config.validate && !config.validate(o[field])) {
437                throw new Error(`Invalid config value for ${field}: ${o[field]}`);
438            }
439        }
440    }
441    #writeEnv(p) {
442        if (!this.#env || !this.#envPrefix)
443            return;
444        for (const [field, value] of Object.entries(p.values)) {
445            const my = this.#configSet[field];
446            this.#env[toEnvKey(this.#envPrefix, field)] = toEnvVal(value, my?.delim);
447        }
448    }
449    /**
450     * Add a heading to the usage output banner
451     */
452    heading(text, level, { pre = false } = {}) {
453        if (level === undefined) {
454            level = this.#fields.some(r => isHeading(r)) ? 2 : 1;
455        }
456        this.#fields.push({ type: 'heading', text, level, pre });
457        return this;
458    }
459    /**
460     * Add a long-form description to the usage output at this position.
461     */
462    description(text, { pre } = {}) {
463        this.#fields.push({ type: 'description', text, pre });
464        return this;
465    }
466    /**
467     * Add one or more number fields.
468     */
469    num(fields) {
470        return this.#addFields(fields, num);
471    }
472    /**
473     * Add one or more multiple number fields.
474     */
475    numList(fields) {
476        return this.#addFields(fields, numList);
477    }
478    /**
479     * Add one or more string option fields.
480     */
481    opt(fields) {
482        return this.#addFields(fields, opt);
483    }
484    /**
485     * Add one or more multiple string option fields.
486     */
487    optList(fields) {
488        return this.#addFields(fields, optList);
489    }
490    /**
491     * Add one or more flag fields.
492     */
493    flag(fields) {
494        return this.#addFields(fields, flag);
495    }
496    /**
497     * Add one or more multiple flag fields.
498     */
499    flagList(fields) {
500        return this.#addFields(fields, flagList);
501    }
502    /**
503     * Generic field definition method. Similar to flag/flagList/number/etc,
504     * but you must specify the `type` (and optionally `multiple` and `delim`)
505     * fields on each one, or Jack won't know how to define them.
506     */
507    addFields(fields) {
508        const next = this;
509        for (const [name, field] of Object.entries(fields)) {
510            this.#validateName(name, field);
511            next.#fields.push({
512                type: 'config',
513                name,
514                value: field,
515            });
516        }
517        Object.assign(next.#configSet, fields);
518        return next;
519    }
520    #addFields(fields, fn) {
521        const next = this;
522        Object.assign(next.#configSet, Object.fromEntries(Object.entries(fields).map(([name, field]) => {
523            this.#validateName(name, field);
524            const option = fn(field);
525            next.#fields.push({
526                type: 'config',
527                name,
528                value: option,
529            });
530            return [name, option];
531        })));
532        return next;
533    }
534    #validateName(name, field) {
535        if (!/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(name)) {
536            throw new TypeError(`Invalid option name: ${name}, ` +
537                `must be '-' delimited ASCII alphanumeric`);
538        }
539        if (this.#configSet[name]) {
540            throw new TypeError(`Cannot redefine option ${field}`);
541        }
542        if (this.#shorts[name]) {
543            throw new TypeError(`Cannot redefine option ${name}, already ` +
544                `in use for ${this.#shorts[name]}`);
545        }
546        if (field.short) {
547            if (!/^[a-zA-Z0-9]$/.test(field.short)) {
548                throw new TypeError(`Invalid ${name} short option: ${field.short}, ` +
549                    'must be 1 ASCII alphanumeric character');
550            }
551            if (this.#shorts[field.short]) {
552                throw new TypeError(`Invalid ${name} short option: ${field.short}, ` +
553                    `already in use for ${this.#shorts[field.short]}`);
554            }
555            this.#shorts[field.short] = name;
556            this.#shorts[name] = name;
557        }
558    }
559    /**
560     * Return the usage banner for the given configuration
561     */
562    usage() {
563        if (this.#usage)
564            return this.#usage;
565        let headingLevel = 1;
566        const ui = cliui({ width });
567        const first = this.#fields[0];
568        let start = first?.type === 'heading' ? 1 : 0;
569        if (first?.type === 'heading') {
570            ui.div({
571                padding: [0, 0, 0, 0],
572                text: normalize(first.text),
573            });
574        }
575        ui.div({ padding: [0, 0, 0, 0], text: 'Usage:' });
576        if (this.#options.usage) {
577            ui.div({
578                text: this.#options.usage,
579                padding: [0, 0, 0, 2],
580            });
581        }
582        else {
583            const cmd = basename(String(process.argv[1]));
584            const shortFlags = [];
585            const shorts = [];
586            const flags = [];
587            const opts = [];
588            for (const [field, config] of Object.entries(this.#configSet)) {
589                if (config.short) {
590                    if (config.type === 'boolean')
591                        shortFlags.push(config.short);
592                    else
593                        shorts.push([config.short, config.hint || field]);
594                }
595                else {
596                    if (config.type === 'boolean')
597                        flags.push(field);
598                    else
599                        opts.push([field, config.hint || field]);
600                }
601            }
602            const sf = shortFlags.length ? ' -' + shortFlags.join('') : '';
603            const so = shorts.map(([k, v]) => ` --${k}=<${v}>`).join('');
604            const lf = flags.map(k => ` --${k}`).join('');
605            const lo = opts.map(([k, v]) => ` --${k}=<${v}>`).join('');
606            const usage = `${cmd}${sf}${so}${lf}${lo}`.trim();
607            ui.div({
608                text: usage,
609                padding: [0, 0, 0, 2],
610            });
611        }
612        ui.div({ padding: [0, 0, 0, 0], text: '' });
613        const maybeDesc = this.#fields[start];
614        if (maybeDesc && isDescription(maybeDesc)) {
615            const print = normalize(maybeDesc.text, maybeDesc.pre);
616            start++;
617            ui.div({ padding: [0, 0, 0, 0], text: print });
618            ui.div({ padding: [0, 0, 0, 0], text: '' });
619        }
620        const { rows, maxWidth } = this.#usageRows(start);
621        // every heading/description after the first gets indented by 2
622        // extra spaces.
623        for (const row of rows) {
624            if (row.left) {
625                // If the row is too long, don't wrap it
626                // Bump the right-hand side down a line to make room
627                const configIndent = indent(Math.max(headingLevel, 2));
628                if (row.left.length > maxWidth - 3) {
629                    ui.div({ text: row.left, padding: [0, 0, 0, configIndent] });
630                    ui.div({ text: row.text, padding: [0, 0, 0, maxWidth] });
631                }
632                else {
633                    ui.div({
634                        text: row.left,
635                        padding: [0, 1, 0, configIndent],
636                        width: maxWidth,
637                    }, { padding: [0, 0, 0, 0], text: row.text });
638                }
639                if (row.skipLine) {
640                    ui.div({ padding: [0, 0, 0, 0], text: '' });
641                }
642            }
643            else {
644                if (isHeading(row)) {
645                    const { level } = row;
646                    headingLevel = level;
647                    // only h1 and h2 have bottom padding
648                    // h3-h6 do not
649                    const b = level <= 2 ? 1 : 0;
650                    ui.div({ ...row, padding: [0, 0, b, indent(level)] });
651                }
652                else {
653                    ui.div({ ...row, padding: [0, 0, 1, indent(headingLevel + 1)] });
654                }
655            }
656        }
657        return (this.#usage = ui.toString());
658    }
659    /**
660     * Return the usage banner markdown for the given configuration
661     */
662    usageMarkdown() {
663        if (this.#usageMarkdown)
664            return this.#usageMarkdown;
665        const out = [];
666        let headingLevel = 1;
667        const first = this.#fields[0];
668        let start = first?.type === 'heading' ? 1 : 0;
669        if (first?.type === 'heading') {
670            out.push(`# ${normalizeOneLine(first.text)}`);
671        }
672        out.push('Usage:');
673        if (this.#options.usage) {
674            out.push(normalizeMarkdown(this.#options.usage, true));
675        }
676        else {
677            const cmd = basename(String(process.argv[1]));
678            const shortFlags = [];
679            const shorts = [];
680            const flags = [];
681            const opts = [];
682            for (const [field, config] of Object.entries(this.#configSet)) {
683                if (config.short) {
684                    if (config.type === 'boolean')
685                        shortFlags.push(config.short);
686                    else
687                        shorts.push([config.short, config.hint || field]);
688                }
689                else {
690                    if (config.type === 'boolean')
691                        flags.push(field);
692                    else
693                        opts.push([field, config.hint || field]);
694                }
695            }
696            const sf = shortFlags.length ? ' -' + shortFlags.join('') : '';
697            const so = shorts.map(([k, v]) => ` --${k}=<${v}>`).join('');
698            const lf = flags.map(k => ` --${k}`).join('');
699            const lo = opts.map(([k, v]) => ` --${k}=<${v}>`).join('');
700            const usage = `${cmd}${sf}${so}${lf}${lo}`.trim();
701            out.push(normalizeMarkdown(usage, true));
702        }
703        const maybeDesc = this.#fields[start];
704        if (maybeDesc && isDescription(maybeDesc)) {
705            out.push(normalizeMarkdown(maybeDesc.text, maybeDesc.pre));
706            start++;
707        }
708        const { rows } = this.#usageRows(start);
709        // heading level in markdown is number of # ahead of text
710        for (const row of rows) {
711            if (row.left) {
712                out.push('#'.repeat(headingLevel + 1) +
713                    ' ' +
714                    normalizeOneLine(row.left, true));
715                if (row.text)
716                    out.push(normalizeMarkdown(row.text));
717            }
718            else if (isHeading(row)) {
719                const { level } = row;
720                headingLevel = level;
721                out.push(`${'#'.repeat(headingLevel)} ${normalizeOneLine(row.text, row.pre)}`);
722            }
723            else {
724                out.push(normalizeMarkdown(row.text, !!row.pre));
725            }
726        }
727        return (this.#usageMarkdown = out.join('\n\n') + '\n');
728    }
729    #usageRows(start) {
730        // turn each config type into a row, and figure out the width of the
731        // left hand indentation for the option descriptions.
732        let maxMax = Math.max(12, Math.min(26, Math.floor(width / 3)));
733        let maxWidth = 8;
734        let prev = undefined;
735        const rows = [];
736        for (const field of this.#fields.slice(start)) {
737            if (field.type !== 'config') {
738                if (prev?.type === 'config')
739                    prev.skipLine = true;
740                prev = undefined;
741                field.text = normalize(field.text, !!field.pre);
742                rows.push(field);
743                continue;
744            }
745            const { value } = field;
746            const desc = value.description || '';
747            const mult = value.multiple ? 'Can be set multiple times' : '';
748            const dmDelim = mult && (desc.includes('\n') ? '\n\n' : '\n');
749            const text = normalize(desc + dmDelim + mult);
750            const hint = value.hint ||
751                (value.type === 'number'
752                    ? 'n'
753                    : value.type === 'string'
754                        ? field.name
755                        : undefined);
756            const short = !value.short
757                ? ''
758                : value.type === 'boolean'
759                    ? `-${value.short} `
760                    : `-${value.short}<${hint}> `;
761            const left = value.type === 'boolean'
762                ? `${short}--${field.name}`
763                : `${short}--${field.name}=<${hint}>`;
764            const row = { text, left, type: 'config' };
765            if (text.length > width - maxMax) {
766                row.skipLine = true;
767            }
768            if (prev && left.length > maxMax)
769                prev.skipLine = true;
770            prev = row;
771            const len = left.length + 4;
772            if (len > maxWidth && len < maxMax) {
773                maxWidth = len;
774            }
775            rows.push(row);
776        }
777        return { rows, maxWidth };
778    }
779    /**
780     * Return the configuration options as a plain object
781     */
782    toJSON() {
783        return Object.fromEntries(Object.entries(this.#configSet).map(([field, def]) => [
784            field,
785            {
786                type: def.type,
787                ...(def.multiple ? { multiple: true } : {}),
788                ...(def.delim ? { delim: def.delim } : {}),
789                ...(def.short ? { short: def.short } : {}),
790                ...(def.description
791                    ? { description: normalize(def.description) }
792                    : {}),
793                ...(def.validate ? { validate: def.validate } : {}),
794                ...(def.default !== undefined ? { default: def.default } : {}),
795            },
796        ]));
797    }
798    /**
799     * Custom printer for `util.inspect`
800     */
801    [inspect.custom](_, options) {
802        return `Jack ${inspect(this.toJSON(), options)}`;
803    }
804}
805// Unwrap and un-indent, so we can wrap description
806// strings however makes them look nice in the code.
807const normalize = (s, pre = false) => pre
808    ? // prepend a ZWSP to each line so cliui doesn't strip it.
809        s
810            .split('\n')
811            .map(l => `\u200b${l}`)
812            .join('\n')
813    : s
814        // remove single line breaks, except for lists
815        .replace(/([^\n])\n[ \t]*([^\n])/g, (_, $1, $2) => !/^[-*]/.test($2) ? `${$1} ${$2}` : `${$1}\n${$2}`)
816        // normalize mid-line whitespace
817        .replace(/([^\n])[ \t]+([^\n])/g, '$1 $2')
818        // two line breaks are enough
819        .replace(/\n{3,}/g, '\n\n')
820        // remove any spaces at the start of a line
821        .replace(/\n[ \t]+/g, '\n')
822        .trim();
823// normalize for markdown printing, remove leading spaces on lines
824const normalizeMarkdown = (s, pre = false) => {
825    const n = normalize(s, pre).replace(/\\/g, '\\\\');
826    return pre
827        ? `\`\`\`\n${n.replace(/\u200b/g, '')}\n\`\`\``
828        : n.replace(/\n +/g, '\n').trim();
829};
830const normalizeOneLine = (s, pre = false) => {
831    const n = normalize(s, pre)
832        .replace(/[\s\u200b]+/g, ' ')
833        .trim();
834    return pre ? `\`${n}\`` : n;
835};
836/**
837 * Main entry point. Create and return a {@link Jack} object.
838 */
839export const jack = (options = {}) => new Jack(options);
840//# sourceMappingURL=index.js.map