UNPKG

@donmccurdy/caporal

Version:

A full-featured framework for building command line applications (cli) with node.js

1,935 lines (1,889 loc) 78.2 kB
import { EventEmitter } from 'events'; import fs from 'fs'; import path from 'path'; import kebabCase from 'lodash/kebabCase.js'; import mapKeys from 'lodash/mapKeys.js'; import reduce from 'lodash/reduce.js'; import { format, createLogger, transports } from 'winston'; import { inspect, format as format$1 } from 'util'; import c from 'chalk'; export { default as chalk } from 'chalk'; import replace from 'lodash/replace.js'; import { EOL } from 'os'; import camelCase from 'lodash/camelCase.js'; import isNumber from 'lodash/isNumber.js'; import { table, getBorderCharacters } from 'table'; import filter from 'lodash/filter.js'; import sortBy from 'lodash/sortBy.js'; import filter$1 from 'lodash/fp/filter.js'; import map from 'lodash/fp/map.js'; import flatMap from 'lodash/flatMap.js'; import wrap from 'wrap-ansi'; import map$1 from 'lodash/map.js'; import zipObject from 'lodash/zipObject.js'; import invert from 'lodash/invert.js'; import pickBy from 'lodash/pickBy.js'; import { glob } from 'glob'; import findIndex from 'lodash/findIndex.js'; function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; } /** * @packageDocumentation * @internal */ class BaseError extends Error { constructor(message, meta = {}) { super(message); this.meta = void 0; Object.setPrototypeOf(this, new.target.prototype); this.name = this.constructor.name; this.meta = meta; Error.captureStackTrace(this, this.constructor); } } /** * @packageDocumentation * @internal */ class ActionError extends BaseError { constructor(error) { const message = typeof error === "string" ? error : error.message; super(message, { error }); } } const _excluded = ["level"]; const caporalFormat = format.printf(data => { const { level } = data, meta = _objectWithoutPropertiesLoose(data, _excluded); let { message } = data; let prefix = ""; const levelStr = getLevelString(level); const metaStr = formatMeta(meta); if (metaStr !== "") { message += `${EOL}${levelStr}: ${metaStr}`; } if (level === "error") { // TODO(cleanup) const paddingLeft = meta.paddingLeft; const spaces = " ".repeat(paddingLeft || 7); prefix = EOL; message = `${replace(message, new RegExp(EOL, "g"), EOL + spaces)}${EOL}`; } return `${prefix}${levelStr}: ${message}`; }); function formatMeta(meta) { delete meta.message; delete meta[Symbol.for("level")]; delete meta[Symbol.for("message")]; delete meta[Symbol.for("splat")]; if (Object.keys(meta).length) { return inspect(meta, { showHidden: false, colors: logger.colorsEnabled }); } return ""; } function getLevelString(level) { if (!logger.colorsEnabled) { return level; } let levelStr = level; switch (level) { case "error": levelStr = c.bold.redBright(level); break; case "warn": levelStr = c.hex("#FF9900")(level); break; case "info": levelStr = c.hex("#569cd6")(level); break; case "debug": case "silly": levelStr = c.dim(level); break; } return levelStr; } let logger = createDefaultLogger(); function setLogger(loggerObj) { logger = loggerObj; } function createDefaultLogger() { const logger = createLogger({ transports: [new transports.Console({ format: format.combine(format.splat(), caporalFormat) })] }); // disableColors() disable on the logger level, // while chalk supports the --color/--no-color flag // as well as the FORCE_COLOR env var logger.disableColors = () => { logger.transports[0].format = caporalFormat; logger.colorsEnabled = false; }; logger.colorsEnabled = c.supportsColor !== false; return logger; } /** * @param err - Error object */ function fatalError(error) { if (logger.level == "debug") { logger.log(_extends({ level: "error" }, error, { message: error.message + "\n\n" + error.stack, stack: error.stack, name: error.name })); } else { logger.error(error.message); } process.exitCode = 1; } /** * @packageDocumentation * @internal */ class InvalidValidatorError extends BaseError { constructor(validator) { super("Caporal setup error: Invalid flag validator setup.", { validator }); } } /** * @packageDocumentation * @internal */ class MissingArgumentError extends BaseError { constructor(argument, command) { const msg = `Missing required argument ${c.bold(argument.name)}.`; super(msg, { argument, command }); } } /** * @packageDocumentation * @internal */ class MissingFlagError extends BaseError { constructor(flag, command) { const msg = `Missing required flag ${c.bold(flag.allNotations.join(" | "))}.`; super(msg, { flag, command }); } } /** * @packageDocumentation * @internal */ function colorize(text) { return text.replace(/<([^>]+)>/gi, match => { return c.hex("#569cd6")(match); }).replace(/<command>/gi, match => { return c.keyword("orange")(match); }).replace(/\[([^[\]]+)\]/gi, match => { return c.hex("#aaa")(match); }).replace(/ --?([^\s,]+)/gi, match => { return c.green(match); }); } /** * @packageDocumentation * @internal */ class ValidationSummaryError extends BaseError { constructor(cmd, errors) { const plural = errors.length > 1 ? "s" : ""; const msg = `The following error${plural} occured:\n` + errors.map(e => "- " + e.message.replace(/\n/g, "\n ")).join("\n") + "\n\n" + c.dim("Synopsis: ") + colorize(cmd.synopsis); super(msg, { errors }); } } /** * @packageDocumentation * @internal */ class NoActionError extends BaseError { constructor(cmd) { let message; if (cmd && !cmd.isProgramCommand()) { message = `Caporal Error: You haven't defined any action for command '${cmd.name}'.\nUse .action() to do so.`; } else { message = `Caporal Error: You haven't defined any action for program.\nUse .action() to do so.`; } super(message, { cmd }); } } /** * @packageDocumentation * @internal */ class OptionSynopsisSyntaxError extends BaseError { constructor(synopsis) { super(`Syntax error in option synopsis: ${synopsis}`, { synopsis }); } } /** * Caporal-provided validator flags. */ var CaporalValidator; (function (CaporalValidator) { /** * Number validator. Check that the value looks like a numeric one * and cast the provided value to a javascript `Number`. */ CaporalValidator[CaporalValidator["NUMBER"] = 1] = "NUMBER"; /** * Boolean validator. Check that the value looks like a boolean. * It accepts values like `true`, `false`, `yes`, `no`, `0`, and `1` * and will auto-cast those values to `true` or `false`. */ CaporalValidator[CaporalValidator["BOOLEAN"] = 2] = "BOOLEAN"; /** * String validator. Mainly used to make sure the value is a string, * and prevent Caporal auto-casting of numerics values and boolean * strings like `true` or `false`. */ CaporalValidator[CaporalValidator["STRING"] = 4] = "STRING"; /** * Array validator. Convert any provided value to an array. If a string is provided, * this validator will try to split it by commas. */ CaporalValidator[CaporalValidator["ARRAY"] = 8] = "ARRAY"; })(CaporalValidator || (CaporalValidator = {})); /** * Option possible value. * */ var OptionValueType; (function (OptionValueType) { /** * Value is optional. */ OptionValueType[OptionValueType["Optional"] = 0] = "Optional"; /** * Value is required. */ OptionValueType[OptionValueType["Required"] = 1] = "Required"; /** * Option does not have any possible value */ OptionValueType[OptionValueType["None"] = 2] = "None"; })(OptionValueType || (OptionValueType = {})); const REG_SHORT_OPT = /^-[a-z]$/i; const REG_LONG_OPT = /^--[a-z]{2,}/i; const REG_OPT = /^(-[a-zA-Z]|--\D{1}[\w-]+)/; function isShortOpt(flag) { return REG_SHORT_OPT.test(flag); } function isLongOpt(flag) { return REG_LONG_OPT.test(flag); } /** * Specific version of camelCase which does not lowercase short flags * * @param name Flag short or long name */ function camelCaseOpt(name) { return name.length === 1 ? name : camelCase(name); } function getCleanNameFromNotation(str, camelCased = true) { str = str.replace(/([[\]<>]+)/g, "").replace("...", "").replace(/^no-/, ""); return camelCased ? camelCaseOpt(str) : str; } function getDashedOpt(name) { const l = Math.min(name.length, 2); return "-".repeat(l) + kebabCase(name); } function isNumeric(n) { return !isNaN(parseFloat(n)) && isFinite(Number(n)); } function isOptionStr(str) { return str !== undefined && str !== "--" && REG_OPT.test(str); } function isConcatenatedOpt(str) { if (str.match(/^-([a-z]{2,})/i)) { return str.substr(1).split(""); } return false; } function isNegativeOpt(opt) { return opt.substr(0, 5) === "--no-"; } function isOptArray(flag) { return Array.isArray(flag); } function formatOptName(name) { return camelCaseOpt(name.replace(/^--?(no-)?/, "")); } /** * Parse a option synopsis * * @example * parseSynopsis("-f, --file <path>") * // Returns... * { * longName: 'file', * longNotation: '--file', * shortNotation: '-f', * shortName: 'f' * valueType: 0, // 0 = optional, 1 = required, 2 = no value * variadic: false * name: 'file' * notation: '--file' // either the long or short notation * } * * @param synopsis * @ignore */ function parseOptionSynopsis(synopsis) { // synopsis = synopsis.trim() const analysis = { variadic: false, valueType: OptionValueType.None, valueRequired: false, allNames: [], allNotations: [], name: "", notation: "", synopsis }; const infos = synopsis.split(/[\s\t,]+/).reduce((acc, value) => { if (isLongOpt(value)) { acc.longNotation = value; acc.longName = getCleanNameFromNotation(value.substring(2)); acc.allNames.push(acc.longName); acc.allNotations.push(value); } else if (isShortOpt(value)) { acc.shortNotation = value; acc.shortName = value.substring(1); acc.allNames.push(acc.shortName); acc.allNotations.push(value); } else if (value.substring(0, 1) === "[") { acc.valueType = OptionValueType.Optional; acc.valueRequired = false; acc.variadic = value.substr(-4, 3) === "..."; } else if (value.substring(0, 1) === "<") { acc.valueType = OptionValueType.Required; acc.valueRequired = true; acc.variadic = value.substr(-4, 3) === "..."; } return acc; }, analysis); if (infos.longName === undefined && infos.shortName === undefined) { throw new OptionSynopsisSyntaxError(synopsis); } infos.name = infos.longName || infos.shortName; infos.notation = infos.longNotation || infos.shortNotation; const fullSynopsis = _extends({}, infos); return fullSynopsis; } /** * @packageDocumentation * @internal */ function isCaporalValidator(validator) { if (typeof validator !== "number") { return false; } const mask = getCaporalValidatorsMask(); const exist = (mask & validator) === validator; return exist; } function isNumericValidator(validator) { return isCaporalValidator(validator) && Boolean(validator & CaporalValidator.NUMBER); } function isStringValidator(validator) { return isCaporalValidator(validator) && Boolean(validator & CaporalValidator.STRING); } function isBoolValidator(validator) { return isCaporalValidator(validator) && Boolean(validator & CaporalValidator.BOOLEAN); } function isArrayValidator(validator) { return isCaporalValidator(validator) && Boolean(validator & CaporalValidator.ARRAY); } function getCaporalValidatorsMask() { return Object.values(CaporalValidator).filter(isNumber).reduce((a, b) => a | b, 0); } function checkCaporalValidator(validator) { if (!isCaporalValidator(validator)) { throw new InvalidValidatorError(validator); } } function checkUserDefinedValidator(validator) { if (typeof validator !== "function" && !(validator instanceof RegExp) && !Array.isArray(validator)) { throw new InvalidValidatorError(validator); } } function checkValidator(validator) { if (validator !== undefined) { typeof validator === "number" ? checkCaporalValidator(validator) : checkUserDefinedValidator(validator); } } function getTypeHint(obj) { let hint; if (isBoolValidator(obj.validator) || "boolean" in obj && obj.boolean && obj.default !== false) { hint = "boolean"; } else if (isNumericValidator(obj.validator)) { hint = "number"; } else if (Array.isArray(obj.validator)) { const stringified = JSON.stringify(obj.validator); if (stringified.length < 300) { hint = "one of " + stringified.substr(1, stringified.length - 2); } } return hint; } function buildTable(data, options = {}) { return table(data, _extends({ border: getBorderCharacters(`void`), columnDefault: { paddingLeft: 0, paddingRight: 2 }, columns: { 0: { paddingLeft: 4, width: 35 }, 1: { width: 55, wrapWord: true, paddingRight: 0 } }, drawHorizontalLine: () => { return false; } }, options)); } function getDefaultValueHint(obj) { return obj.default !== undefined && !("boolean" in obj && obj.boolean && obj.default === false) ? "default: " + JSON.stringify(obj.default) : undefined; } function getOptionSynopsisHelp(opt, { eol: crlf, chalk: c }) { return opt.synopsis + (opt.required && opt.default === undefined ? crlf + c.dim("required") : ""); } function getOptionsTable(options, ctx, title = "OPTIONS") { options = filter(options, "visible"); if (!options.length) { return ""; } const { chalk: c, eol: crlf, table, spaces } = ctx; const help = spaces + c.bold(title) + crlf + crlf; const rows = options.map(opt => { const def = getDefaultValueHint(opt); const more = [opt.typeHint, def].filter(d => d).join(", "); const syno = getOptionSynopsisHelp(opt, ctx); const desc = opt.description + (more.length ? crlf + c.dim(more) : ""); return [syno, desc]; }); return help + table(rows); } function getArgumentsTable(args, ctx, title = "ARGUMENTS") { if (!args.length) { return ""; } const { chalk: c, eol, eol2, table, spaces } = ctx; const help = spaces + c.bold(title) + eol2; const rows = args.map(a => { const def = getDefaultValueHint(a); const more = [a.typeHint, def].filter(d => d).join(", "); const desc = a.description + (more.length ? eol + c.dim(more) : ""); return [a.synopsis, desc]; }); return help + table(rows); } function getCommandsTable(commands, ctx, title = "COMMANDS") { const { chalk, prog, eol2, table, spaces } = ctx; const cmdHint = `Type '${prog.getBin()} help <command>' to get some help about a command`; const help = spaces + chalk.bold(title) + ` ${chalk.dim("\u2014")} ` + chalk.dim(cmdHint) + eol2; const rows = commands.filter(c => c.visible).map(cmd => { return [chalk.keyword("orange")(cmd.name), cmd.description || ""]; }); return help + table(rows); } const command = async ctx => { const { cmd, globalOptions: globalFlags, eol, eol3, colorize, tpl } = ctx; const options = sortBy(cmd.options, "name"), globalOptions = Array.from(globalFlags.keys()); const help = cmd.synopsis + eol3 + (await tpl("custom", ctx)) + getArgumentsTable(cmd.args, ctx) + eol + getOptionsTable(options, ctx) + eol + getOptionsTable(globalOptions, ctx, "GLOBAL OPTIONS"); return colorize(help); }; const header = ctx => { var _process$env; const { prog, chalk: c, spaces, eol, eol2 } = ctx; const version = ((_process$env = process.env) == null ? void 0 : _process$env.NODE_ENV) === "test" ? "" : prog.getVersion(); return eol + spaces + (prog.getName() || prog.getBin()) + " " + (version || "") + (prog.getDescription() ? " \u2014 " + c.dim(prog.getDescription()) : "") + eol2; }; const program$1 = async ctx => { const { prog, globalOptions, eol, eol3, colorize, tpl } = ctx; const commands = await prog.getAllCommands(); const options = Array.from(globalOptions.keys()); const help = (await prog.getSynopsis()) + eol3 + (await tpl("custom", ctx)) + getCommandsTable(commands, ctx) + eol + getOptionsTable(options, ctx, "GLOBAL OPTIONS"); return colorize(help); }; const usage = async ctx => { var _cmd; const { tpl, prog, chalk: c, spaces, eol } = ctx; let { cmd } = ctx; // if help is asked without a `cmd` and that no command exists // within the program, override `cmd` with the program-command if (!cmd && !(await prog.hasCommands())) { ctx.cmd = cmd = prog.progCommand; } // usage const usage = `${spaces + c.bold("USAGE")} ${(_cmd = cmd) != null && _cmd.name ? "— " + c.dim(cmd.name) : ""} ${eol + spaces + spaces + c.dim("\u25B8")} `; const next = cmd ? await tpl("command", ctx) : await tpl("program", ctx); return usage + next; }; const custom = ctx => { const { prog, cmd, eol2, eol3, chalk, colorize, customHelp, indent } = ctx; const data = customHelp.get(cmd || prog); if (data) { const txt = data.map(({ text, options }) => { let str = ""; if (options.sectionName) { str += chalk.bold(options.sectionName) + eol2; } const subtxt = options.colorize ? colorize(text) : text; str += options.sectionName ? indent(subtxt) : subtxt; return str + eol3; }).join(""); return indent(txt) + eol3; } return ""; }; /** * @packageDocumentation * @internal */ var allTemplates = { __proto__: null, command: command, header: header, program: program$1, usage: usage, custom: custom }; const templates = new Map(Object.entries(allTemplates)); const customHelpMap = new Map(); /** * Customize the help * * @param obj * @param text * @param options * @internal */ function customizeHelp(obj, text, options) { const opts = _extends({ sectionName: "", colorize: true }, options); const data = customHelpMap.get(obj) || []; data.push({ text, options: opts }); customHelpMap.set(obj, data); } /** * Helper to be used to call templates from within templates * * @param name Template name * @param ctx Execution context * @internal */ async function tpl(name, ctx) { const template = templates.get(name); if (!template) { throw Error(`Caporal setup error: Unknown help template '${name}'`); } return template(ctx); } /** * @internal * @param program * @param command */ function getContext(program, command) { const spaces = " ".repeat(2); const ctx = { prog: program, cmd: command, chalk: c, colorize: colorize, customHelp: customHelpMap, tpl, globalOptions: getGlobalOptions(), table: buildTable, spaces, indent(str, sp = spaces) { return sp + replace(str.trim(), /(\r\n|\r|\n)/g, "\n" + sp); }, eol: "\n", eol2: "\n\n", eol3: "\n\n\n" }; return ctx; } /** * Return the help text * * @param program Program instance * @param command Command instance, if any * @internal */ async function getHelp(program, command) { const ctx = getContext(program, command); return [await tpl("header", ctx), await tpl("usage", ctx)].join(""); } /** * Create an Option object * * @internal * @param synopsis * @param description * @param options */ function createOption(synopsis, description, options = {}) { // eslint-disable-next-line prefer-const let { validator, required, hidden } = options; // force casting required = Boolean(required); checkValidator(validator); const syno = parseOptionSynopsis(synopsis); let boolean = syno.valueType === OptionValueType.None || isBoolValidator(validator); if (validator && !isBoolValidator(validator)) { boolean = false; } const opt = _extends({ kind: "option", default: boolean == true ? Boolean(options.default) : options.default, description, choices: Array.isArray(validator) ? validator : [] }, syno, { required, visible: !hidden, boolean, validator }); opt.typeHint = getTypeHint(opt); return opt; } /** * Display help. Return false to prevent further processing. * * @internal */ const showHelp = async ({ program, command }) => { const help = await getHelp(program, command); // eslint-disable-next-line no-console console.log(help); program.emit("help", help); return false; }, /** * Display program version. Return false to prevent further processing. * * @internal */ showVersion = ({ program }) => { // eslint-disable-next-line no-console console.log(program.getVersion()); program.emit("version", program.getVersion()); return false; }, /** * Disable colors in output * * @internal */ disableColors = ({ logger }) => { logger.disableColors(); }, /** * Set verbosity to the maximum * * @internal */ setVerbose = ({ logger }) => { logger.level = "silly"; }, /** * Makes the program quiet, eg displaying logs with level >= warning */ setQuiet = ({ logger }) => { logger.level = "warn"; }, /** * Makes the program totally silent */ setSilent = ({ logger }) => { logger.silent = true; }, /** * Install completion */ installComp = ({ program }) => { throw new Error("Completion not supported."); }, /** * Uninstall completion */ uninstallComp = ({ program }) => { throw new Error("Completion not supported."); }; /** * Global options container * * @internal */ let globalOptions; /** * Get the list of registered global flags * * @internal */ function getGlobalOptions() { if (globalOptions === undefined) { globalOptions = setupGlobalOptions(); } return globalOptions; } /** * Set up the global flags * * @internal */ function setupGlobalOptions() { const help = createOption("-h, --help", "Display global help or command-related help."), verbose = createOption("-v, --verbose", "Verbose mode: will also output debug messages."), quiet = createOption("--quiet", "Quiet mode - only displays warn and error messages."), silent = createOption("--silent", "Silent mode: does not output anything, giving no indication of success or failure other than the exit code."), version = createOption("-V, --version", "Display version."), color = createOption("--no-color", "Disable use of colors in output."), installCompOpt = createOption("--install-completion", "Install completion for your shell.", { hidden: true }), uninstallCompOpt = createOption("--uninstall-completion", "Uninstall completion for your shell.", { hidden: true }); return new Map([[help, showHelp], [version, showVersion], [color, disableColors], [verbose, setVerbose], [quiet, setQuiet], [silent, setSilent], [installCompOpt, installComp], [uninstallCompOpt, uninstallComp]]); } /** * Disable a global option * * @param name Can be the option short/long name or notation */ function disableGlobalOption(name) { const opts = getGlobalOptions(); for (const [opt] of opts) { if (opt.allNames.includes(name) || opt.allNotations.includes(name)) { return opts.delete(opt); } } return false; } /** * Add a global option to the program. * A global option is available at the program level, * and associated with one given {@link Action}. * * @param a {@link Option} instance, for example created using {@link createOption()} */ function addGlobalOption(opt, action) { return getGlobalOptions().set(opt, action); } /** * Process global options, if any * @internal */ async function processGlobalOptions(parsed, program, command) { const { options } = parsed; const actionsParams = _extends({}, parsed, { logger, program, command }); const promises = Object.entries(options).map(([opt]) => { const action = findGlobalOptAction(opt); if (action) { return action(actionsParams); } }); const results = await Promise.all(promises); return results.some(r => r === false); } /** * Find a global Option action from the option name (short or long) * * @param name Short or long name * @internal */ function findGlobalOptAction(name) { for (const [opt, action] of getGlobalOptions()) { if (opt.allNames.includes(name)) { return action; } } } /** * Find a global Option by it's name (short or long) * * @param name Short or long name * @internal */ function findGlobalOption(name) { for (const [opt] of getGlobalOptions()) { if (opt.allNames.includes(name)) { return opt; } } } /** * @packageDocumentation * @internal */ function levenshtein(a, b) { if (a === b) { return 0; } if (!a.length || !b.length) { return a.length || b.length; } let cell = 0; let lcell = 0; let dcell = 0; const row = [...Array(b.length + 1).keys()]; for (let i = 0; i < a.length; i++) { dcell = i; lcell = i + 1; for (let j = 0; j < b.length; j++) { cell = a[i] === b[j] ? dcell : Math.min(...[dcell, row[j + 1], lcell]) + 1; dcell = row[j + 1]; row[j] = lcell; lcell = cell; } row[row.length - 1] = cell; } return cell; } /** * @packageDocumentation * @internal */ const MAX_DISTANCE = 2; const sortByDistance = (a, b) => a.distance - b.distance; const keepMeaningfulSuggestions = s => s.distance <= MAX_DISTANCE; const possibilitesMapper = (input, p) => { return { suggestion: p, distance: levenshtein(input, p) }; }; /** * Get autocomplete suggestions * * @param {String} input - User input * @param {String[]} possibilities - Possibilities to retrieve suggestions from */ function getSuggestions(input, possibilities) { return possibilities.map(p => possibilitesMapper(input, p)).filter(keepMeaningfulSuggestions).sort(sortByDistance).map(p => p.suggestion); } /** * Make diff bolder in a string * * @param from original string * @param to target string */ function boldDiffString(from, to) { return [...to].map((char, index) => { if (char != from.charAt(index)) { return c.bold.greenBright(char); } return char; }).join(""); } /** * @packageDocumentation * @internal */ /** * @todo Rewrite */ class UnknownOptionError extends BaseError { constructor(flag, command) { const longFlags = filter$1(f => f.name.length > 1), getFlagNames = map(f => f.name), possibilities = getFlagNames([...longFlags(command.options), ...getGlobalOptions().keys()]), suggestions = getSuggestions(flag, possibilities); let msg = `Unknown option ${c.bold.redBright(getDashedOpt(flag))}. `; if (suggestions.length) { msg += "Did you mean " + suggestions.map(s => boldDiffString(getDashedOpt(flag), getDashedOpt(s))).join(" or maybe ") + " ?"; } super(msg, { flag, command }); } } /** * @packageDocumentation * @internal */ /** * @todo Rewrite */ class UnknownOrUnspecifiedCommandError extends BaseError { constructor(program, command) { const possibilities = filter(flatMap(program.getCommands(), c => [c.name, ...c.getAliases()])); let msg = ""; if (command) { msg = `Unknown command ${c.bold(command)}.`; const suggestions = getSuggestions(command, possibilities); if (suggestions.length) { msg += " Did you mean " + suggestions.map(s => boldDiffString(command, s)).join(" or maybe ") + " ?"; } } else { msg = "Unspecified command. Available commands are:\n" + wrap(possibilities.map(p => c.whiteBright(p)).join(", "), 60) + "." + `\n\nFor more help, type ${c.whiteBright(program.getBin() + " --help")}`; } super(msg, { command }); } } /** * @packageDocumentation * @internal */ function isOptionObject(obj) { return "allNotations" in obj; } class ValidationError extends BaseError { constructor({ value, error, validator, context }) { let message = error instanceof Error ? error.message : error; const varName = isOptionObject(context) ? "option" : "argument"; const name = isOptionObject(context) ? context.allNotations.join("|") : context.synopsis; if (isCaporalValidator(validator)) { switch (validator) { case CaporalValidator.NUMBER: message = format$1('Invalid value for %s %s.\nExpected a %s but got "%s".', varName, c.redBright(name), c.underline("number"), c.redBright(value)); break; case CaporalValidator.BOOLEAN: message = format$1('Invalid value for %s %s.\nExpected a %s (true, false, 0, 1), but got "%s".', varName, c.redBright(name), c.underline("boolean"), c.redBright(value)); break; case CaporalValidator.STRING: message = format$1('Invalid value for %s %s.\nExpected a %s, but got "%s".', varName, c.redBright(name), c.underline("string"), c.redBright(value)); break; } } else if (Array.isArray(validator)) { message = format$1('Invalid value for %s %s.\nExpected one of %s, but got "%s".', varName, c.redBright(name), "'" + validator.join("', '") + "'", c.redBright(value)); } else if (validator instanceof RegExp) { message = format$1('Invalid value for %s %s.\nExpected a value matching %s, but got "%s".', varName, c.redBright(name), c.whiteBright(validator.toString()), c.redBright(value)); } super(message + ""); } } /** * @packageDocumentation * @internal */ class TooManyArgumentsError extends BaseError { constructor(cmd, range, argsCount) { const expArgsStr = range.min === range.max ? `exactly ${range.min}.` : `between ${range.min} and ${range.max}.`; const cmdName = cmd.isProgramCommand() ? "" : `for command ${c.bold(cmd.name)}`; const message = format$1(`Too many argument(s) %s. Got %s, expected %s`, cmdName, c.bold.redBright(argsCount), c.bold.greenBright(expArgsStr)); super(message, { command: cmd }); } } /** * @packageDocumentation * @internal */ /** * Validate using a RegExp * * @param validator * @param value * @ignore */ function validateWithRegExp(validator, value, context) { if (Array.isArray(value)) { return value.map(v => { return validateWithRegExp(validator, v, context); }); } if (!validator.test(value + "")) { throw new ValidationError({ validator: validator, value, context }); } return value; } /** * @packageDocumentation * @internal */ /** * Validate using an array of valid values. * * @param validator * @param value * @ignore */ function validateWithArray(validator, value, context) { if (Array.isArray(value)) { value.forEach(v => validateWithArray(validator, v, context)); } else if (validator.includes(value) === false) { throw new ValidationError({ validator, value, context }); } return value; } /** * @packageDocumentation * @internal */ async function validateWithFunction(validator, value, context) { if (Array.isArray(value)) { return Promise.all(value.map(v => { return validateWithFunction(validator, v, context); })); } try { return await validator(value); } catch (error) { throw new ValidationError({ validator, value, error, context }); } } /** * @packageDocumentation * @internal */ function validateWithCaporal(validator, value, context, skipArrayValidation = false) { if (!skipArrayValidation && isArrayValidator(validator)) { return validateArrayFlag(validator, value, context); } else if (Array.isArray(value)) { // should not happen! throw new ValidationError({ error: "Expected a scalar value, got an array", value, validator, context }); } else if (isNumericValidator(validator)) { return validateNumericFlag(validator, value, context); } else if (isStringValidator(validator)) { return validateStringFlag(value); } else if (isBoolValidator(validator)) { return validateBoolFlag(value, context); } return value; } /** * The string validator actually just cast the value to string * * @param value * @ignore */ function validateBoolFlag(value, context) { if (typeof value === "boolean") { return value; } else if (/^(true|false|yes|no|0|1)$/i.test(String(value)) === false) { throw new ValidationError({ value, validator: CaporalValidator.BOOLEAN, context }); } return /^0|no|false$/.test(String(value)) === false; } function validateNumericFlag(validator, value, context) { const str = value + ""; if (Array.isArray(value) || !isNumeric(str)) { throw new ValidationError({ value, validator, context }); } return parseFloat(str); } function validateArrayFlag(validator, value, context) { const values = typeof value === "string" ? value.split(",") : !Array.isArray(value) ? [value] : value; return flatMap(values, el => validateWithCaporal(validator, el, context, true)); } /** * The string validator actually just cast the value to string * * @param value * @ignore */ function validateStringFlag(value) { return value + ""; } /** * @packageDocumentation * @internal */ function validate(value, validator, context) { if (typeof validator === "function") { return validateWithFunction(validator, value, context); } else if (validator instanceof RegExp) { return validateWithRegExp(validator, value, context); } else if (Array.isArray(validator)) { return validateWithArray(validator, value, context); } // Caporal flag validator else if (isCaporalValidator(validator)) { return validateWithCaporal(validator, value, context); } return value; } /** * @packageDocumentation * @internal */ function findArgument(cmd, name) { return cmd.args.find(a => a.name === name); } /** * @packageDocumentation * @internal */ /** * Get the number of required argument for a given command * * @param cmd */ function getRequiredArgsCount(cmd) { return cmd.args.filter(a => a.required).length; } function getArgsObjectFromArray(cmd, args) { const result = {}; return cmd.args.reduce((acc, arg, index) => { if (args[index] !== undefined) { acc[arg.name] = args[index]; } else if (arg.default !== undefined) { acc[arg.name] = arg.default; } return acc; }, result); } /** * Check if the given command has at leat one variadic argument * * @param cmd */ function hasVariadicArgument(cmd) { return cmd.args.some(a => a.variadic); } function getArgsRange(cmd) { const min = getRequiredArgsCount(cmd); const max = hasVariadicArgument(cmd) ? Infinity : cmd.args.length; return { min, max }; } function checkRequiredArgs(cmd, args, parsedArgv) { const errors = cmd.args.reduce((acc, arg) => { if (args[arg.name] === undefined && arg.required) { acc.push(new MissingArgumentError(arg, cmd)); } return acc; }, []); // Check if there is more args than specified if (cmd.strictArgsCount) { const numArgsError = checkNumberOfArgs(cmd, parsedArgv); if (numArgsError) { errors.push(numArgsError); } } return errors; } function checkNumberOfArgs(cmd, args) { const range = getArgsRange(cmd); const argsCount = Object.keys(args).length; if (range.max !== Infinity && range.max < Object.keys(args).length) { return new TooManyArgumentsError(cmd, range, argsCount); } } function removeCommandFromArgs(cmd, args) { const words = cmd.name.split(" ").length; return args.slice(words); } function validateArg(arg, value) { return arg.validator ? validate(value, arg.validator, arg) : value; } /** * * @param cmd * @param parsedArgv * * @todo Bugs: * * * ts-node examples/pizza/pizza.ts cancel my-order jhazd hazd * * -> result ok, should be too many arguments * */ async function validateArgs(cmd, parsedArgv) { // remove the command from the argv array const formatedArgs = cmd.isProgramCommand() ? parsedArgv : removeCommandFromArgs(cmd, parsedArgv); // transfrom args array to object, and set defaults for arguments not passed const argsObj = getArgsObjectFromArray(cmd, formatedArgs); const errors = []; const validations = reduce(argsObj, (acc, value, key) => { const arg = findArgument(cmd, key); try { /* istanbul ignore if -- should not happen */ if (!arg) { throw new BaseError(`Unknown argumment ${key}`); } acc[key] = validateArg(arg, value); } catch (e) { errors.push(e); } return acc; }, {}); const result = await reduce(validations, async (prevPromise, value, key) => { const collection = await prevPromise; collection[key] = await value; return collection; }, Promise.resolve({})); errors.push(...checkRequiredArgs(cmd, result, formatedArgs)); return { args: result, errors }; } /** * @packageDocumentation * @internal */ /** * Find an option from its name for a given command * * @param cmd Command object * @param name Option name, short or long, camel-cased */ function findOption(cmd, name) { return cmd.options.find(o => o.allNames.find(opt => opt === name)); } /** * @packageDocumentation * @internal */ function validateOption(opt, value) { return opt.validator ? validate(value, opt.validator, opt) : value; } function checkRequiredOpts(cmd, opts) { return cmd.options.reduce((acc, opt) => { if (opts[opt.name] === undefined && opt.required) { acc.push(new MissingFlagError(opt, cmd)); } return acc; }, []); } function applyDefaults(cmd, opts) { return cmd.options.reduce((acc, opt) => { if (acc[opt.name] === undefined && opt.default !== undefined) { acc[opt.name] = opt.default; } return acc; }, opts); } async function validateOptions(cmd, options) { options = applyDefaults(cmd, options); const errors = []; const validations = reduce(options, (...args) => { const [acc, value, name] = args; const opt = findGlobalOption(name) || findOption(cmd, name); try { if (opt) { acc[name] = validateOption(opt, value); } else if (cmd.strictOptions) { throw new UnknownOptionError(name, cmd); } } catch (e) { errors.push(e); } return acc; }, {}); const result = await reduce(validations, async (prevPromise, value, key) => { const collection = await prevPromise; collection[key] = await value; return collection; }, Promise.resolve({})); errors.push(...checkRequiredOpts(cmd, result)); return { options: result, errors }; } async function validateCall(cmd, result) { const { args: parsedArgs, options: parsedFlags } = result; // check if there are some global flags to handle const { options: flags, errors: flagsErrors } = await validateOptions(cmd, parsedFlags); const { args, errors: argsErrors } = await validateArgs(cmd, parsedArgs); return _extends({}, result, { args, options: flags, errors: [...argsErrors, ...flagsErrors] }); } /** * Check if the argument is explicitly required * * @ignore * @param synopsis */ function isRequired(synopsis) { return synopsis.substring(0, 1) === "<"; } /** * * @param synopsis */ function isVariadic(synopsis) { return synopsis.substr(-4, 3) === "..." || synopsis.endsWith("..."); } function parseArgumentSynopsis(synopsis) { synopsis = synopsis.trim(); const rawName = getCleanNameFromNotation(synopsis, false); const name = getCleanNameFromNotation(synopsis); const required = isRequired(synopsis); const variadic = isVariadic(synopsis); const cleanSynopsis = required ? `<${rawName}${variadic ? "..." : ""}>` : `[${rawName}${variadic ? "..." : ""}]`; return { name, synopsis: cleanSynopsis, required, variadic }; } /** * * @param synopsis - Argument synopsis * @param description - Argument description * @param [options] - Various argument options like validator and default value */ function createArgument(synopsis, description, options = {}) { const { validator, default: defaultValue } = options; checkValidator(validator); const syno = parseArgumentSynopsis(synopsis); const arg = _extends({ kind: "argument", default: defaultValue, description, choices: Array.isArray(validator) ? validator : [], validator }, syno); arg.typeHint = getTypeHint(arg); return arg; } function getOptsMapping(cmd) { const names = map$1(cmd.options, "name"); const aliases = map$1(cmd.options, o => o.shortName || o.longName); const result = zipObject(names, aliases); return pickBy(_extends({}, result, invert(result))); } function createConfigurator(defaults) { const _defaults = defaults; let config = defaults; return { reset() { config = _defaults; return config; }, get(key) { return config[key]; }, getAll() { return config; }, set(props) { // eslint-disable-next-line @typescript-eslint/no-explicit-any return Object.assign(config, props); } }; } /** * @ignore */ const PROG_CMD = "__self_cmd"; /** * @ignore */ const HELP_CMD = "help"; /** * Command class * */ class Command { /** * * @param program * @param name * @param description * @internal */ constructor(program, name, description, config = {}) { this.program = void 0; this._action = void 0; this._lastAddedArgOrOpt = void 0; this._aliases = []; this._name = void 0; this._config = void 0; /** * Command description * * @internal */ this.description = void 0; /** * Command options array * * @internal */ this.options = []; /** * Command arguments array * * @internal */ this.args = []; this.program = program; this._name = name; this.description = description; this._config = createConfigurator(_extends({ visible: true }, config)); } /** * Add one or more aliases so the command can be called by different names. * * @param aliases Command aliases */ alias(...aliases) { this._aliases.push(...aliases); return this; } /** * Name getter. Will return an empty string in the program-command context * * @internal */ get name() { return this.isProgramCommand() ? "" : this._name; } /** * Add an argument to the command. * Synopsis is a string like `<my-argument>` or `[my-argument]`. * Angled brackets (e.g. `<item>`) indicate required input. Square brackets (e.g. `[env]`) indicate optional input. * * Returns the {@link Command} object to facilitate chaining of methods. * * @param synopsis Argument synopsis. * @param description - Argument description. * @param [options] - Optional parameters including validator and default value. */ argument(synopsis, description, options = {}) { this._lastAddedArgOrOpt = createArgument(synopsis, description, options); this.args.push(this._lastAddedArgOrOpt); return this; } /** * Set the corresponding action to execute for this command * * @param action Action to execute */ action(action) { this._action = action; return this; } /** * Allow chaining command() calls. See {@link Program.command}. * */ command(name, description, config = {}) { return this.program.command(name, description, config); } /** * Makes the command the default one for the program. */ default() { this.program.defaultCommand = this; return this; } /** * Checks if the command has the given alias registered. * * @param alias * @internal */ hasAlias(alias) { return this._aliases.includes(alias); } /** * Get command aliases. * @internal */ getAliases() { return this._aliases; } /** * @internal */ isProgramCommand() { return this._name === PROG_CMD; } /** * @internal */ isHelpCommand() { return this._name === HELP_CMD; } /** * Hide the command from help. * Shortcut to calling `.configure({ visible: false })`. */ hide() { return this.configure({ visible: false }); } /** * Add an option to the current command. * * @param synopsis Option synopsis like '-f, --force', or '-f, --file \<file\>', or '--with-openssl [path]' * @param description Option description * @param options Additional parameters */ option(synopsis, description, options = {}) { const opt = this._lastAddedArgOrOpt = createOption(synopsis, description, options); this.options.push(opt); return this; } /** * @internal */ getParserConfig() { const defaults = { boolean: [], string: [], alias: getOptsMapping(this), autoCast: this.autoCast, variadic: [], ddash: false }; let parserOpts = this.options.reduce((parserOpts, opt) => { if (opt.boolean) { parserOpts.boolean.push(opt.name); } if (isStringValidator(opt.validator)) { parserOpts.string.push(opt.name); } if (opt.variadic) { parserOpts.variadic.push(opt.name); } return parserOpts; }, defaults); parserOpts = this.args.reduce((parserOpts, arg, index) => { if (!this.isProgramCommand()) { index++; } if (isBoolValidator(arg.validator)) { parserOpts.boolean.push(index); } if (isStringValidator(arg.validator)) { parserOpts.string.push(index); } if (arg.variadic) { parserOpts.variadic.push(index); } return parserOpts; }, parserOpts); return parserOpts; } /** * Return a reformated synopsis string * @internal */ get synopsis() { const opts = this.options.length ? this.options.some(f => f.required) ? "<OPTIONS...>" : "[OPTIONS...]" : ""; const name = this._name !== PROG_CMD ? " " + this._name : ""; return (this.program.getBin() + name + " " + this.args.map(a => a.synopsis).join(" ") + " " + opts).trim(); } /** * Customize command help. Can be called multiple times to add more paragraphs and/or sections. * * @param text Help contents * @param options Display options */ help(text, options = {}) { customizeHelp(this, text, options); return this; } /** * Configure some behavioral properties. * * @param props properties to set/update */ configure(props) { this._config.set(props); return this; } /** * Get a configuration property value. * * @internal * @param key Property key to get value for. See {@link CommandConfig}. */ getConfigProperty(key) { return this._config.get(key); } /** * Get the auto-casting flag. * * @internal */ get autoCast() { var _this$getConfigProper; return (_this$getConfigProper = this.getConfigProperty("autoCast")) != null ? _this$getConfigProper : this.program.getConfigProperty("autoCast"); } /** * Auto-complete */ complete(completer) { throw new Error("Completion not supported."); } /** * Toggle strict mode. * Shortcut to calling: `.configure({ strictArgsCount: strict, strictOptions: strict }). * By default, strict settings are not defined for commands, and inherit from the * program settings. Calling `.strict(value)` on a command will override the program * settings. * * @param strict boolean enabled flag */ strict(strict = true) { return this.configure({ strictArgsCount: strict, strictOptions: strict }); } /** * Computed strictOptions flag. * * @internal */ get strictOptions() { var _this$getConfigProper2; return (_this$getConfigProper2 = this.getConfigProperty("strictOptions")) != null ? _this$getConfigProper2 : this.program.getConfigProperty("strictOptions"); } /** * Computed strictArgsCount flag. * * @internal */ get strictArgsCount() { var _this$getConfigProper3; return (_this$getConfigProper3 = this.getConfigProperty("strictArgsCount")) != null ? _this$getConfigProper3 : this.program.getConfigProperty("strictArgsCount"); } /** * Enable or disable auto casting of arguments & options for the command. * This is basically a shortcut to calling `command.configure({ autoCast: enabled })`. * By default, auto-casting is inherited from the program configuration. * This method allows overriding what's been set on the program level. * * @param enabled */ cast(enabled) { return this.configure({ autoCast: enabled }); } /** * Visible flag * * @internal */ get visible() { return this.getConfigProperty("visible"); } /** * Run the action associated with the command * * @internal */ async run(parsed) { const data = _extends({ args: [], options: {}, line: "", rawOptions: {}, rawArgv: [], ddash: [] }, parsed); try { // Validate args and options, includin