@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
JavaScript
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