@visulima/cerebro
Version:
A delightful toolkit for building Node-powered CLIs.
1,230 lines (1,202 loc) • 47.5 kB
JavaScript
import { cwd, argv, env, execPath, execArgv, exit } from 'node:process';
import { boxen } from '@visulima/boxen';
import { bold, dim, reset, green, yellow } from '@visulima/colorize';
import { MessageFormatterProcessor, CallerProcessor } from '@visulima/pail/processor';
import { createPail } from '@visulima/pail/server';
import commandLineArgs from 'command-line-args';
import CliTable3 from 'cli-table3';
import template from '@visulima/colorize/template';
import os from 'node:os';
import { distance } from 'fastest-levenshtein';
var __defProp$m = Object.defineProperty;
var __name$m = (target, value) => __defProp$m(target, "name", { value, configurable: true });
const UPPERCASE = /[\p{Lu}]/u;
const LOWERCASE = /[\p{Ll}]/u;
const LEADING_CAPITAL = /^[\p{Lu}](?![\p{Lu}])/gu;
const IDENTIFIER = /([\p{Alpha}\p{N}_]|$)/u;
const SEPARATORS = /[_.\- ]+/;
const LEADING_SEPARATORS = new RegExp("^" + SEPARATORS.source);
const SEPARATORS_AND_IDENTIFIER = new RegExp(SEPARATORS.source + IDENTIFIER.source, "gu");
const NUMBERS_AND_IDENTIFIER = new RegExp("\\d+" + IDENTIFIER.source, "gu");
const preserveCamelCase = /* @__PURE__ */ __name$m((string, toLowerCase, toUpperCase, preserveConsecutiveUppercase2) => {
let isLastCharLower = false;
let isLastCharUpper = false;
let isLastLastCharUpper = false;
let isLastLastCharPreserved = false;
for (let index = 0; index < string.length; index++) {
const character = string[index];
isLastLastCharPreserved = index > 2 ? string[index - 3] === "-" : true;
if (isLastCharLower && UPPERCASE.test(character)) {
string = string.slice(0, index) + "-" + string.slice(index);
isLastCharLower = false;
isLastLastCharUpper = isLastCharUpper;
isLastCharUpper = true;
index++;
} else if (isLastCharUpper && isLastLastCharUpper && LOWERCASE.test(character) && (!isLastLastCharPreserved || preserveConsecutiveUppercase2)) {
string = string.slice(0, index - 1) + "-" + string.slice(index - 1);
isLastLastCharUpper = isLastCharUpper;
isLastCharUpper = false;
isLastCharLower = true;
} else {
isLastCharLower = toLowerCase(character) === character && toUpperCase(character) !== character;
isLastLastCharUpper = isLastCharUpper;
isLastCharUpper = toUpperCase(character) === character && toLowerCase(character) !== character;
}
}
return string;
}, "preserveCamelCase");
const preserveConsecutiveUppercase = /* @__PURE__ */ __name$m((input, toLowerCase) => {
LEADING_CAPITAL.lastIndex = 0;
return input.replaceAll(LEADING_CAPITAL, (match) => toLowerCase(match));
}, "preserveConsecutiveUppercase");
const postProcess = /* @__PURE__ */ __name$m((input, toUpperCase) => {
SEPARATORS_AND_IDENTIFIER.lastIndex = 0;
NUMBERS_AND_IDENTIFIER.lastIndex = 0;
return input.replaceAll(NUMBERS_AND_IDENTIFIER, (match, pattern, offset) => ["_", "-"].includes(input.charAt(offset + match.length)) ? match : toUpperCase(match)).replaceAll(SEPARATORS_AND_IDENTIFIER, (_, identifier) => toUpperCase(identifier));
}, "postProcess");
function camelCase(input, options) {
if (!(typeof input === "string" || Array.isArray(input))) {
throw new TypeError("Expected the input to be `string | string[]`");
}
options = {
pascalCase: false,
preserveConsecutiveUppercase: false,
...options
};
if (Array.isArray(input)) {
input = input.map((x) => x.trim()).filter((x) => x.length).join("-");
} else {
input = input.trim();
}
if (input.length === 0) {
return "";
}
const toLowerCase = options.locale === false ? (string) => string.toLowerCase() : (string) => string.toLocaleLowerCase(options.locale);
const toUpperCase = options.locale === false ? (string) => string.toUpperCase() : (string) => string.toLocaleUpperCase(options.locale);
if (input.length === 1) {
if (SEPARATORS.test(input)) {
return "";
}
return options.pascalCase ? toUpperCase(input) : toLowerCase(input);
}
const hasUpperCase = input !== toLowerCase(input);
if (hasUpperCase) {
input = preserveCamelCase(input, toLowerCase, toUpperCase, options.preserveConsecutiveUppercase);
}
input = input.replace(LEADING_SEPARATORS, "");
input = options.preserveConsecutiveUppercase ? preserveConsecutiveUppercase(input, toLowerCase) : toLowerCase(input);
if (options.pascalCase) {
input = toUpperCase(input.charAt(0)) + input.slice(1);
}
return postProcess(input, toUpperCase);
}
__name$m(camelCase, "camelCase");
const defaultOptions = [
{
description: "Turn on verbose output",
group: "global",
name: "verbose",
type: Boolean
},
{
description: "Turn on debugging output",
group: "global",
name: "debug",
type: Boolean
},
{
alias: "h",
description: "Print out helpful usage information",
group: "global",
name: "help",
type: Boolean
},
{
alias: "q",
description: "Silence output",
group: "global",
name: "quiet",
type: Boolean
},
{
alias: "V",
description: "Print version info",
group: "global",
name: "version",
type: Boolean
},
{
description: "Turn off colored output",
group: "global",
name: "no-color",
type: Boolean
},
{
description: "Force colored output",
group: "global",
name: "color",
type: Boolean
}
];
var __defProp$l = Object.defineProperty;
var __name$l = (target, value) => __defProp$l(target, "name", { value, configurable: true });
const templateFormat = /* @__PURE__ */ __name$l((string_) => {
if (string_) {
return template(Object.assign([], { raw: [string_.replaceAll("`", "\\`")] }));
}
return "";
}, "templateFormat");
var __defProp$k = Object.defineProperty;
var __name$k = (target, value) => __defProp$k(target, "name", { value, configurable: true });
class BaseSection {
static {
__name$k(this, "BaseSection");
}
lines;
constructor() {
this.lines = [];
}
add(line) {
this.lines.push(line);
}
toString() {
return this.lines.join(os.EOL);
}
header(text) {
this.add(bold(text));
this.lines.push("");
}
}
var __defProp$j = Object.defineProperty;
var __name$j = (target, value) => __defProp$j(target, "name", { value, configurable: true });
const defaultTableOptions = {
chars: {
bottom: "",
"bottom-left": "",
"bottom-mid": "",
"bottom-right": "",
left: " ",
"left-mid": "",
mid: "",
"mid-mid": "",
middle: " ",
right: "",
"right-mid": "",
top: "",
"top-left": "",
"top-mid": "",
"top-right": ""
},
colWidths: [40, 60],
style: { border: [], compact: true, head: [], "padding-left": 2, "padding-right": 1 },
wordWrap: true
};
class ContentSection extends BaseSection {
static {
__name$j(this, "ContentSection");
}
// eslint-disable-next-line sonarjs/cognitive-complexity
constructor(section) {
super();
if (section.header) {
this.header(templateFormat(section.header));
}
if (section.content) {
if (section.raw) {
if (Array.isArray(section.content) && section.content.every((value) => typeof value === "string")) {
section.content.forEach((row) => {
if (Array.isArray(row)) {
row.forEach((cell) => this.add(templateFormat(cell)));
} else {
this.add(templateFormat(row));
}
});
} else if (typeof section.content === "string") {
this.add(templateFormat(section.content));
} else {
throw new TypeError("Invalid raw content, must be a string or array of strings.");
}
} else {
this.add(this.getContentLines(section.content));
}
this.add("");
}
}
// eslint-disable-next-line sonarjs/cognitive-complexity,class-methods-use-this
getContentLines(content) {
if (typeof content === "string") {
const table = new CliTable3({
...defaultTableOptions,
colWidths: [80]
});
table.push([templateFormat(content)]);
return table.toString();
}
if (Array.isArray(content) && // eslint-disable-next-line @typescript-eslint/no-shadow
content.every((value) => typeof value === "string" || Array.isArray(value) && value.every((value2) => typeof value2 === "string"))) {
const table = new CliTable3({
...defaultTableOptions
});
content.forEach((row) => {
if (Array.isArray(row)) {
table.push(row.map((cell) => templateFormat(cell)));
} else {
table.push([templateFormat(row)]);
}
});
return table.toString();
}
if (typeof content === "object") {
const contentObject = content;
if (!contentObject.options || !contentObject.data) {
throw new Error(`Must have an "options" or "data" property
${JSON.stringify(content)}`);
}
const table = new CliTable3({
...defaultTableOptions,
...contentObject.options,
style: { ...defaultTableOptions.style, ...contentObject.options.style }
});
contentObject.data.forEach((row) => {
if (Array.isArray(row)) {
table.push(row.map((cell) => templateFormat(cell)));
} else {
table.push([templateFormat(row)]);
}
});
return table.toString();
}
throw new Error(`invalid input - 'content' must be a string, array of strings or a object:
${JSON.stringify(content)}`);
}
}
var __defProp$i = Object.defineProperty;
var __name$i = (target, value) => __defProp$i(target, "name", { value, configurable: true });
class OptionListSection extends BaseSection {
static {
__name$i(this, "OptionListSection");
}
// eslint-disable-next-line sonarjs/cognitive-complexity
constructor(data) {
super();
let definitions = data.optionList ?? [];
const hide = Array.isArray(data.hide) ? data.hide : [data.hide].filter(Boolean);
const groups = Array.isArray(data.group) ? data.group : [data.group].filter(Boolean);
if (hide.length > 0) {
definitions = definitions.filter((definition) => !hide.includes(definition.name));
}
if (data.header) {
this.header(templateFormat(data.header));
}
if (groups.length > 0) {
definitions = definitions.filter((definition) => {
const noGroupMatch = groups.includes("_none") && !definition.group;
const groupMatch = this.intersect(Array.isArray(definition.group) ? definition.group : [definition.group], groups);
return noGroupMatch || groupMatch ? definition : void 0;
});
}
const table = new CliTable3({
chars: {
bottom: "",
"bottom-left": "",
"bottom-mid": "",
"bottom-right": "",
left: " ",
"left-mid": "",
mid: "",
"mid-mid": "",
middle: " ",
right: "",
"right-mid": "",
top: "",
"top-left": "",
"top-mid": "",
"top-right": ""
},
colWidths: [40, 40],
style: { "padding-left": 2, "padding-right": 1 },
wordWrap: true
});
definitions.forEach(
(definition) => table.push([this.getOptionNames(definition, data.reverseNameOrder ?? false, data.isArgument ?? false), templateFormat(definition.description)])
);
this.add(table.toString());
this.lines.push("");
}
// eslint-disable-next-line class-methods-use-this,sonarjs/cognitive-complexity,@typescript-eslint/no-explicit-any
getOptionNames(definition, reverseNameOrder, isArgument) {
if (!definition.name) {
throw new TypeError("Invalid option definition, name is required.");
}
let type = definition.type ? definition.type.name.toLowerCase() : "string";
const multiple = definition.multiple || definition.lazyMultiple ? "[]" : "";
type = templateFormat(definition.typeLabel ?? `{underline ${type}${multiple}}`);
let result;
if (definition.alias) {
if (definition.name) {
const name = isArgument ? definition.name : `{yellow --${definition.name}}`;
result = reverseNameOrder ? templateFormat(`{bold ${name}}, {bold -${definition.alias}} ${type}`) : templateFormat(`{bold -${definition.alias}}, {bold ${name}} ${type}`);
} else if (reverseNameOrder) {
result = templateFormat(`{bold -${definition.alias}} ${type}`);
} else {
result = templateFormat(`{bold -${definition.alias}} ${type}`);
}
} else {
result = templateFormat(`{bold ${isArgument ? definition.name : `{yellow --${definition.name}}`}} ${type}`);
}
return result;
}
// eslint-disable-next-line class-methods-use-this
intersect(array1, array2) {
return array1.some((item1) => array2.includes(item1));
}
}
var __defProp$h = Object.defineProperty;
var __name$h = (target, value) => __defProp$h(target, "name", { value, configurable: true });
const commandLineUsage = /* @__PURE__ */ __name$h((sections) => {
const lines = Array.isArray(sections) ? sections : [sections];
if (lines.length === 0) {
return "";
}
return `
${sections.map((section) => {
if (section.optionList) {
return new OptionListSection(section).toString();
}
return new ContentSection(section).toString();
}).join("\n")}`;
}, "commandLineUsage");
var __defProp$g = Object.defineProperty;
var __name$g = (target, value) => __defProp$g(target, "name", { value, configurable: true });
const EMPTY_GROUP_KEY = "__Other";
const upperFirstChar = /* @__PURE__ */ __name$g((string_) => string_.charAt(0).toUpperCase() + string_.slice(1), "upperFirstChar");
const printGeneralHelp = /* @__PURE__ */ __name$g((logger, runtime, commands, groupOption) => {
logger.debug("no command given, printing general help...");
let filteredCommands = [...new Set(commands.values())].filter((command) => !command.hidden);
if (groupOption) {
filteredCommands = filteredCommands.filter((command) => command.group === groupOption);
}
const groupedCommands = filteredCommands.reduce((accumulator, command) => {
const group = command.group ?? EMPTY_GROUP_KEY;
if (!accumulator[group]) {
accumulator[group] = [];
}
accumulator[group].push(command);
return accumulator;
}, {});
logger.raw(
commandLineUsage(
[
{
content: `{cyan ${runtime.getCliName()}} {green <command>} [positional arguments] {yellow [options]}`,
header: "{inverse.cyan Usage }"
},
...Object.keys(groupedCommands).map((key) => {
return {
// eslint-disable-next-line security/detect-object-injection
content: groupedCommands[key].map((command) => {
let aliases = "";
if (typeof command.alias === "string") {
aliases = command.alias;
} else if (Array.isArray(command.alias)) {
aliases = command.alias.join(", ");
}
if (aliases !== "") {
aliases = ` [${aliases}]`;
}
return [`{green ${command.name}} ${aliases}`, command.description ?? ""];
}),
header: key === EMPTY_GROUP_KEY || groupOption ? `{inverse.green Available${groupOption ? ` ${upperFirstChar(groupOption)}` : ""} Commands }` : ` {inverse.green ${upperFirstChar(key)} }`
};
}),
commands.has("help") ? {
header: "{inverse.yellow Command Options }",
optionList: commands.get("help").options?.filter((option) => !option.hidden)
} : void 0,
{ header: "{inverse.yellow Global Options }", optionList: defaultOptions },
{
content: `Run "{cyan ${runtime.getCliName()}} {green help <command>}" or "{cyan ${runtime.getCliName()}} {green <command>} {yellow --help}" for help with a specific command.`,
raw: true
}
].filter(Boolean)
)
);
}, "printGeneralHelp");
const printCommandHelp = /* @__PURE__ */ __name$g((logger, runtime, commands, name) => {
const command = commands.get(name);
const usageGroups = [];
usageGroups.push({
content: `{cyan ${runtime.getCliName()}} {green ${command.name}}${command.argument ? " [positional arguments]" : ""}${command.options ? " [options]" : ""}`,
header: "{inverse.cyan Usage }"
});
if (command.description) {
usageGroups.push({ content: command.description, header: "{inverse.green Description }" });
}
if (command.argument) {
usageGroups.push({ header: "Command Positional Arguments", isArgument: true, optionList: [command.argument] });
}
if (Array.isArray(command.options) && command.options.length > 0) {
usageGroups.push({
header: "{inverse.yellow Command Options }",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
optionList: command.options.filter((option) => !option.hidden)
});
}
usageGroups.push({ header: "{inverse.yellow Global Options }", optionList: defaultOptions });
if (command.alias !== void 0 && command.alias.length > 0) {
let alias = command.alias;
if (typeof command.alias === "string") {
alias = [command.alias];
}
usageGroups.splice(1, 0, {
content: alias,
header: "Alias(es)"
});
}
if (Array.isArray(command.examples) && command.examples.length > 0) {
usageGroups.push({
content: command.examples,
header: "Examples"
});
}
logger.raw(commandLineUsage(usageGroups));
}, "printCommandHelp");
class HelpCommand {
static {
__name$g(this, "HelpCommand");
}
name = "help";
options = [
{
description: "Display only the specified group",
name: "group",
type: String
}
];
commands;
constructor(commands) {
this.commands = commands;
}
execute(toolbox) {
const { commandName, logger, options, runtime } = toolbox;
const { footer, header } = runtime.getCommandSection();
if (header) {
logger.raw(templateFormat(header));
}
if (commandName === "help") {
printGeneralHelp(logger, runtime, this.commands, options?.group);
} else {
printCommandHelp(logger, runtime, this.commands, commandName);
}
if (footer) {
logger.raw(templateFormat(footer));
}
}
}
var __defProp$f = Object.defineProperty;
var __name$f = (target, value) => __defProp$f(target, "name", { value, configurable: true });
const VersionCommand = {
alias: ["v", "V"],
description: "Output the version number",
execute: /* @__PURE__ */ __name$f(({ logger, runtime }) => {
const version = runtime.getPackageVersion();
if (version === void 0) {
logger.warn("Unknown version");
logger.debug("The version number was not provided by the cli constructor.");
} else {
logger.info(version);
}
}, "execute"),
name: "version",
options: [],
usage: []
};
const VERBOSITY_QUIET = 16;
const VERBOSITY_NORMAL = 32;
const VERBOSITY_VERBOSE = 64;
const VERBOSITY_DEBUG = 128;
const POSITIONALS_KEY = "positionals";
var __defProp$e = Object.defineProperty;
var __name$e = (target, value) => __defProp$e(target, "name", { value, configurable: true });
class EmptyToolbox {
static {
__name$e(this, "EmptyToolbox");
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
result;
argv;
options;
argument;
command;
commandName;
runtime;
logger;
constructor(commandName, command) {
this.commandName = commandName;
this.command = command;
}
}
var __defProp$d = Object.defineProperty;
var __name$d = (target, value) => __defProp$d(target, "name", { value, configurable: true });
const checkNodeVersion = /* @__PURE__ */ __name$d(() => {
const minNodeVersion = process.env.CEREBRO_MIN_NODE_VERSION ? Number(process.env.CEREBRO_MIN_NODE_VERSION) : 18;
const nodeVersion = process.version.replace("v", "");
const major = Number(/v([^.]+)/.exec(process.version)[1]);
if (major < minNodeVersion) {
console.log(
`cerebro supports a minimum Node version of ${minNodeVersion}. You have ${nodeVersion}. Read our version support policy: https://github.com/visulima/visulima#supported-nodejs-versions`
);
process.exit(1);
}
}, "checkNodeVersion");
var __defProp$c = Object.defineProperty;
var __name$c = (target, value) => __defProp$c(target, "name", { value, configurable: true });
const argumentNameRegExp = /^-{1,2}(\w+)(=(\w+))?$/;
const getParameterOption = /* @__PURE__ */ __name$c((argument, options) => {
const regExpResult = argumentNameRegExp.exec(argument);
if (regExpResult == null) {
return {};
}
const nameOrAlias = regExpResult[1];
const option = options.find((o) => o.name === nameOrAlias || o.alias === nameOrAlias);
if (option !== void 0) {
return { argName: option.name, argValue: regExpResult[3], option };
}
return {};
}, "getParameterOption");
var __defProp$b = Object.defineProperty;
var __name$b = (target, value) => __defProp$b(target, "name", { value, configurable: true });
const isBoolean = /* @__PURE__ */ __name$b((option) => option.type?.name === "Boolean", "isBoolean");
var __defProp$a = Object.defineProperty;
var __name$a = (target, value) => __defProp$a(target, "name", { value, configurable: true });
const convertType = /* @__PURE__ */ __name$a((value, option) => {
if (option.type === void 0) {
return value;
}
if (option.type.name === "Boolean") {
if (value === "true" || value === "1") {
return option.type(true);
}
if (value === "false" || value === "0") {
return option.type(false);
}
}
return option.type(value);
}, "convertType");
const booleanValue$1 = /* @__PURE__ */ new Set(["1", "0", "true", "false"]);
const getBooleanValues = /* @__PURE__ */ __name$a((arguments_, options) => {
const getBooleanValue = /* @__PURE__ */ __name$a((argumentsAndLastOption, argument) => {
const { argName, argValue, option } = getParameterOption(argument, options);
const { lastOption } = argumentsAndLastOption;
if (option && isBoolean(option) && argValue && argName) {
argumentsAndLastOption.partial[argName] = convertType(argValue, option);
} else if (argumentsAndLastOption.lastName && lastOption && isBoolean(lastOption) && booleanValue$1.has(argument)) {
argumentsAndLastOption.partial[argumentsAndLastOption.lastName] = convertType(
argument,
lastOption
// eslint-disable-next-line @typescript-eslint/no-explicit-any
);
}
return { lastName: argName, lastOption: option, partial: argumentsAndLastOption.partial };
}, "getBooleanValue");
return arguments_.reduce(getBooleanValue, { partial: {} }).partial;
}, "getBooleanValues");
var __defProp$9 = Object.defineProperty;
var __name$9 = (target, value) => __defProp$9(target, "name", { value, configurable: true });
const getTypeLabel = /* @__PURE__ */ __name$9((definition) => {
let typeLabel = definition.type ? definition.type.name.toLowerCase() : "string";
const multiple = definition.multiple ?? definition.lazyMultiple ? "[]" : "";
if (typeLabel) {
typeLabel = typeLabel === "boolean" ? "" : `{underline ${typeLabel}${multiple}}`;
}
return typeLabel;
}, "getTypeLabel");
const mapOptionTypeLabel = /* @__PURE__ */ __name$9((definition) => {
if (isBoolean(definition)) {
return definition;
}
definition.typeLabel = definition.typeLabel ?? getTypeLabel(definition);
if (definition.defaultOption) {
definition.typeLabel = `${definition.typeLabel} (D)`;
}
if (definition.required) {
definition.typeLabel = `${definition.typeLabel} (R)`;
}
return definition;
}, "mapOptionTypeLabel");
var __defProp$8 = Object.defineProperty;
var __name$8 = (target, value) => __defProp$8(target, "name", { value, configurable: true });
const booleanValue = /* @__PURE__ */ new Set(["1", "0", "true", "false"]);
const removeBooleanValues = /* @__PURE__ */ __name$8((arguments_, options) => {
const removeBooleanArguments = /* @__PURE__ */ __name$8((argumentsAndLastValue, argument) => {
const { argValue, option } = getParameterOption(argument, options);
const { lastOption } = argumentsAndLastValue;
if (lastOption && isBoolean(lastOption) && booleanValue.has(argument)) {
const copiedArguments_ = [...argumentsAndLastValue.args];
copiedArguments_.pop();
return { args: copiedArguments_ };
}
if (option && isBoolean(option) && argValue) {
return { args: argumentsAndLastValue.args };
}
return { args: [...argumentsAndLastValue.args, argument], lastOption: option };
}, "removeBooleanArguments");
return arguments_.reduce(removeBooleanArguments, { args: [] }).args;
}, "removeBooleanValues");
var __defProp$7 = Object.defineProperty;
var __name$7 = (target, value) => __defProp$7(target, "name", { value, configurable: true });
const isShort = new RegExp(/^-([^\d-])$/);
const isLong = new RegExp(/^--(\S+)/);
const isCombined = new RegExp(/^-([^\d-]{2,})$/);
const isOption = /* @__PURE__ */ __name$7((argument) => isShort.test(argument) || isLong.test(argument) || isCombined.test(argument), "isOption");
const commandLineCommands = /* @__PURE__ */ __name$7((commands, argv) => {
const command = argv[0] && isOption(argv[0]) || argv.length === 0 ? null : argv.shift() ?? null;
if (!commands.includes(command)) {
const error = new Error(`Command not recognised: ${command}`);
error.command = command;
error.name = "INVALID_COMMAND";
throw error;
}
return { argv, command };
}, "commandLineCommands");
var __defProp$6 = Object.defineProperty;
var __name$6 = (target, value) => __defProp$6(target, "name", { value, configurable: true });
const isSimilar = /* @__PURE__ */ __name$6((string1, string2) => distance(string1, string2) <= string1.length / 3 || string2.includes(string1), "isSimilar");
const findAlternatives = /* @__PURE__ */ __name$6((string, array) => {
const id = string.toLowerCase();
return array.filter((nextId) => isSimilar(nextId.toLowerCase(), id));
}, "findAlternatives");
var __defProp$5 = Object.defineProperty;
var __name$5 = (target, value) => __defProp$5(target, "name", { value, configurable: true });
const listMissingArguments = /* @__PURE__ */ __name$5((commandLineConfig, parsedArguments) => commandLineConfig.filter((config) => config.required && parsedArguments[config.name] == null).filter((config) => {
if (config.type?.name === "Boolean") {
parsedArguments[config.name] = false;
return false;
}
return true;
}), "listMissingArguments");
var __defProp$4 = Object.defineProperty;
var __name$4 = (target, value) => __defProp$4(target, "name", { value, configurable: true });
const mergeArguments = /* @__PURE__ */ __name$4((argumentLists) => {
const argumentsByName = /* @__PURE__ */ new Map();
argumentLists.forEach((argument) => {
argumentsByName.set(argument.name, { ...argumentsByName.get(argument.name), ...argument });
});
return [...argumentsByName.values()];
}, "mergeArguments");
var __defProp$3 = Object.defineProperty;
var __name$3 = (target, value) => __defProp$3(target, "name", { value, configurable: true });
const isElectronApp = /* @__PURE__ */ __name$3(() => !!process.versions.electron, "isElectronApp");
const isBundledElectronApp = /* @__PURE__ */ __name$3(() => isElectronApp() && !process.defaultApp, "isBundledElectronApp");
const getProcessArgvBinIndex = /* @__PURE__ */ __name$3(() => {
if (isBundledElectronApp()) {
return 0;
}
return 1;
}, "getProcessArgvBinIndex");
const hideBin = /* @__PURE__ */ __name$3((argv) => argv.slice(getProcessArgvBinIndex() + 1), "hideBin");
var __defProp$2 = Object.defineProperty;
var __name$2 = (target, value) => __defProp$2(target, "name", { value, configurable: true });
const COMMAND_DELIMITER = " ";
const equals = /* @__PURE__ */ __name$2((a, b) => a.length === b.length && a.every((v, index) => v === b[index]), "equals");
const parseRawCommand = /* @__PURE__ */ __name$2((commandArray) => {
if (typeof commandArray === "string") {
return commandArray.split(COMMAND_DELIMITER);
}
if (equals(commandArray, process.argv)) {
return hideBin(commandArray);
}
return commandArray;
}, "parseRawCommand");
var __defProp$1 = Object.defineProperty;
var __name$1 = (target, value) => __defProp$1(target, "name", { value, configurable: true });
const registerExceptionHandler = /* @__PURE__ */ __name$1((logger) => {
process.on("uncaughtException", (error) => {
logger.error(`Uncaught exception: ${error}`);
if (error?.stack) {
logger.error(error.stack);
}
process.exit(1);
});
process.on("unhandledRejection", (error) => {
logger.error(`Promise rejection: ${error}`);
if (error?.stack) {
logger.error(error.stack);
}
process.exit(1);
});
}, "registerExceptionHandler");
var __defProp = Object.defineProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
const isCI = "CI" in env && ("GITHUB_ACTIONS" in env || "GITLAB_CI" in env || "CIRCLECI" in env);
const lowerFirstChar = /* @__PURE__ */ __name((string_) => string_.charAt(0).toLowerCase() + string_.slice(1), "lowerFirstChar");
class Cli {
static {
__name(this, "Cli");
}
logger;
argv;
cwd;
cliName;
packageVersion;
packageName;
extensions = [];
commands;
defaultCommand;
updateNotifierOptions;
commandSection;
/**
* @param cliName The cli cliName.
* @param options The options for the CLI.
* - argv This should be in the base case process.argv
* - cwd The path of main folder.
* - logger The logger options.
* - packageName The packageJson name.
* - packageVersion The packageJson version.
*/
constructor(cliName, options = {}) {
const { argv: argv$1, cwd: cwd$1, packageName, packageVersion } = {
argv: argv,
cwd: cwd(),
...options
};
this.argv = parseRawCommand(argv$1);
if (this.argv.includes("--quiet") || this.argv.includes("-q")) {
env.CEREBRO_OUTPUT_LEVEL = String(VERBOSITY_QUIET);
} else if (this.argv.includes("--verbose") || this.argv.includes("-v")) {
env.CEREBRO_OUTPUT_LEVEL = String(VERBOSITY_VERBOSE);
} else if (this.argv.includes("--debug") || this.argv.includes("-vvv") || "DEBUG" in env) {
env.CEREBRO_OUTPUT_LEVEL = String(VERBOSITY_DEBUG);
} else {
env.CEREBRO_OUTPUT_LEVEL = String(VERBOSITY_NORMAL);
}
const cerebroLevelToPailLevel = {
"32": "informational",
"64": "trace",
"128": "debug"
};
const processors = [new MessageFormatterProcessor()];
if (env.CEREBRO_OUTPUT_LEVEL === String(VERBOSITY_DEBUG)) {
processors.push(new CallerProcessor());
}
this.logger = createPail({
logLevel: env.CEREBRO_OUTPUT_LEVEL ? cerebroLevelToPailLevel[env.CEREBRO_OUTPUT_LEVEL] ?? "informational" : "informational",
processors,
...options.logger
});
if (env.CEREBRO_OUTPUT_LEVEL === String(VERBOSITY_QUIET)) {
this.logger.disable();
}
checkNodeVersion();
registerExceptionHandler(this.logger);
this.cliName = cliName;
this.packageVersion = packageVersion;
this.packageName = packageName;
this.cwd = cwd$1;
this.defaultCommand = "help";
this.commandSection = {
header: `${this.cliName}${this.packageVersion ? ` v${this.packageVersion}` : ""}`
};
this.commands = /* @__PURE__ */ new Map();
this.addCoreExtensions();
this.addCommand(VersionCommand);
this.addCommand(new HelpCommand(this.commands));
}
setCommandSection(commandSection) {
this.commandSection = commandSection;
return this;
}
getCommandSection() {
return this.commandSection;
}
/**
* Set a default command, to display a different command if cli is call without command.
*/
setDefaultCommand(commandName) {
this.defaultCommand = commandName;
return this;
}
/**
* Add an arbitrary command to the CLI.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
addCommand(command) {
if (this.commands.has(command.name)) {
throw new Error(`Ignored command with name "${command.name}", it was found in the command list.`);
} else {
command.options?.map((option) => mapOptionTypeLabel(option));
this.validateDoubleOptions(command);
this.addNegatableOption(command);
command.options?.forEach((option) => {
option.__camelCaseName__ = camelCase(option.name);
});
this.commands.set(command.name, command);
if (command.alias !== void 0) {
let aliases = command.alias;
if (typeof command.alias === "string") {
aliases = [command.alias];
}
aliases.forEach((alias) => {
this.logger.debug("adding alias", alias);
if (this.commands.has(alias)) {
throw new Error(`Ignoring command alias "${alias}, command with the same name was found."`);
} else {
this.commands.set(alias, command);
}
});
}
}
return this;
}
/**
* Adds an extension so it is available when commands execute. They usually live
* the given name on the toolbox object passed to commands, but are able
* to manipulate the toolbox object however they want.
*/
addExtension(extension) {
this.extensions.push(extension);
return this;
}
/**
* Enable the update notifier functionality with the given options.
*
* @param options - The options for enabling the update notifier.
* options.alwaysRun - Determines whether the update check should always run. Defaults to false.
* options.distributionTag - The distribution tag to use for checking updates. Defaults to "latest".
* options.updateCheckInterval - The interval in milliseconds between each update check. Defaults to 24 hours.
*
* @example
* enableUpdateNotifier({
* alwaysRun: true,
* debug: false,
* distributionTag: "stable",
* pkg: {
* name: "my-package",
* version: "1.0.0"
* },
* updateCheckInterval: 1000 * 60 * 60
* });
*/
enableUpdateNotifier(options = {}) {
if (!this.packageName || !this.packageVersion) {
throw new Error("Cannot enable update notifier without package name and version.");
}
const configKeys = Object.keys(options);
if (configKeys.length > 0 && !configKeys.includes("alwaysRun") && !configKeys.includes("distTag") && !configKeys.includes("updateCheckInterval")) {
throw new Error("Invalid update notifier options, please check the documentation.");
}
this.updateNotifierOptions = {
alwaysRun: false,
debug: env.CEREBRO_OUTPUT_LEVEL === String(VERBOSITY_DEBUG),
distTag: "latest",
pkg: {
name: this.packageName,
version: this.packageVersion
},
updateCheckInterval: 1e3 * 60 * 60 * 24,
...options
};
return this;
}
getCliName() {
return this.cliName;
}
getPackageVersion() {
return this.packageVersion;
}
getPackageName() {
return this.packageName;
}
getCommands() {
return this.commands;
}
getCwd() {
return this.cwd;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
async run(extraOptions = {}) {
const { shouldExitProcess = true, ...otherExtraOptions } = extraOptions;
const commandNames = [...this.commands.keys()];
let parsedArguments;
this.logger.debug(`process.execPath: ${execPath}`);
this.logger.debug(`process.execArgv: ${execArgv.join(" ")}`);
this.logger.debug(`process.argv: ${argv.join(" ")}`);
try {
parsedArguments = commandLineCommands([null, ...commandNames], this.argv);
} catch (error) {
if (error.name === "INVALID_COMMAND" && error.command) {
let alternatives = "";
const foundAlternatives = findAlternatives(error.command, [...this.commands.keys()]);
if (foundAlternatives.length > 0) {
alternatives = ` Did you mean: \r
- ${foundAlternatives.join(" \r\n- ")}`;
}
this.logger.error(`"${error.command}" is not an available command.${alternatives}`);
} else {
this.logger.error(error);
}
return shouldExitProcess ? exit(1) : void 0;
}
const commandName = parsedArguments.command ?? this.defaultCommand;
const command = this.commands.get(commandName);
if (typeof command.execute !== "function") {
this.logger.error(`Command "${command.name}" has no function to execute.`);
return shouldExitProcess ? exit(1) : void 0;
}
const commandArguments = parsedArguments.argv;
this.logger.debug(`command '${commandName}' found, parsing command args: ${commandArguments.join(", ")}`);
let arguments_ = mergeArguments([...command.options ?? [], ...defaultOptions]);
arguments_.forEach((argument) => {
if (argument.multiple && argument.lazyMultiple) {
throw new Error(`Argument "${argument.name}" cannot have both multiple and lazyMultiple options, please choose one.`);
}
});
if (command.argument) {
this.logger.debug("command has positional argument, parsing them...");
arguments_ = [
{
defaultOption: true,
description: command.argument?.description,
group: "positionals",
multiple: true,
name: POSITIONALS_KEY,
type: command.argument?.type,
typeLabel: command.argument?.typeLabel
},
...arguments_
];
}
const parsedArgs = commandLineArgs(arguments_, {
argv: removeBooleanValues(commandArguments, command.options ?? []),
camelCase: true,
partial: true,
stopAtFirstUnknown: true
});
const booleanValues = getBooleanValues(commandArguments, command.options ?? []);
const commandArgs = { ...parsedArgs, _all: { ...parsedArgs._all, ...booleanValues } };
this.validateCommandOptions(arguments_, commandArgs, command);
const toolbox = new EmptyToolbox(command.name, command);
toolbox.runtime = this;
await this.registerExtensions(toolbox);
await this.updateNotifier(toolbox);
const { _all, positionals } = commandArgs;
if (_all[POSITIONALS_KEY]) {
delete _all[POSITIONALS_KEY];
}
toolbox.argument = positionals?.[POSITIONALS_KEY] ?? [];
toolbox.argv = this.argv;
toolbox.options = { ..._all, ...otherExtraOptions };
this.mapNegatableOptions(toolbox, command);
this.mapImpliesOptions(toolbox, command);
this.validateCommandArgsForConflicts(arguments_, toolbox.options, command);
this.logger.debug("command options parsed from options:");
this.logger.debug(JSON.stringify(toolbox.options, null, 2));
this.logger.debug("command argument parsed from argument:");
this.logger.debug(JSON.stringify(toolbox.argument, null, 2));
await this.prepareToolboxResult(commandArgs, toolbox, command);
return shouldExitProcess ? exit(0) : void 0;
}
// eslint-disable-next-line class-methods-use-this,@typescript-eslint/no-explicit-any
validateDoubleOptions(command) {
if (Array.isArray(command.options)) {
const groupedDuplicatedOption = command.options.reduce((accumulator, object) => {
const key = `${object.name}-${object.alias}`;
if (!accumulator[key]) {
accumulator[key] = [];
}
accumulator[key].push(object);
return accumulator;
}, {});
const duplicatedOptions = Object.values(groupedDuplicatedOption).filter((object) => object.length > 1);
let errorMessages = "";
duplicatedOptions.forEach((options) => {
const matchingOption = options[0];
const duplicate = options[1];
let flag = "alias";
if (matchingOption.name === duplicate.name) {
flag = "name";
if (matchingOption.alias === duplicate.alias) {
flag += " and alias";
}
}
errorMessages += `Cannot add option ${flag} "${JSON.stringify(duplicate)}" to command "${command.name}" due to conflicting option ${JSON.stringify(matchingOption)}
`;
});
if (errorMessages.length > 0) {
throw new Error(errorMessages);
}
}
}
/**
* Adds the core extensions. These provide the basic features
* available in cerebro.
*/
addCoreExtensions() {
this.addExtension({
execute: /* @__PURE__ */ __name((toolbox) => {
toolbox.logger = this.logger;
}, "execute"),
name: "logger"
});
}
// eslint-disable-next-line unicorn/prevent-abbreviations,@typescript-eslint/no-explicit-any
async prepareToolboxResult(commandArgs, toolbox, command) {
if (commandArgs.global?.help) {
this.logger.debug("'--help' option found, running 'help' for given command...");
const helpCommand = this.commands.get("help");
if (!helpCommand) {
throw new Error("Help command not found.");
}
await helpCommand.execute(toolbox);
return;
}
if (commandArgs.global?.version || commandArgs.global?.V) {
this.logger.debug("'--version' option found, running 'version' for given command...");
const helpCommand = this.commands.get("version");
if (!helpCommand) {
throw new Error("Version command not found.");
}
await helpCommand.execute(toolbox);
return;
}
await command.execute(toolbox);
}
async updateNotifier({ logger }) {
if (this.updateNotifierOptions?.alwaysRun || !(env.NO_UPDATE_NOTIFIER || env.NODE_ENV === "test" || this.argv.includes("--no-update-notifier") || isCI) && this.updateNotifierOptions) {
logger.raw("Checking for updates...");
const hasNewVersion = await import('../packem_chunks/has-new-version.mjs').then((m) => m.default);
const updateAvailable = await hasNewVersion(this.updateNotifierOptions);
if (updateAvailable) {
const template = "Update available " + dim(this.packageVersion + "") + reset(" → ") + green(updateAvailable);
this.logger.error(
boxen(template, {
borderColor: /* @__PURE__ */ __name((border) => yellow(border), "borderColor"),
borderStyle: "round",
margin: 1,
padding: 1,
textAlignment: "center"
})
);
}
}
}
// eslint-disable-next-line sonarjs/cognitive-complexity,class-methods-use-this,@typescript-eslint/no-explicit-any
validateCommandOptions(arguments_, commandArguments, command) {
const missingOptions = listMissingArguments(arguments_, commandArguments);
if (missingOptions.length > 0) {
throw new Error(
`You called the command "${command.name}" without the required options: ${missingOptions.map((argument) => argument.name).join(", ")}`
);
}
if (commandArguments._unknown && commandArguments._unknown.length > 0) {
const errors = [];
commandArguments._unknown.forEach((unknownOption) => {
const isOption = unknownOption.startsWith("--");
let error = `Found unknown ${isOption ? "option" : "argument"} "${unknownOption}"`;
if (isOption) {
const foundAlternatives = findAlternatives(unknownOption.replace("--", ""), [
...(command.options ?? []).map((option) => option.name),
...defaultOptions.map((option) => option.name)
]);
if (foundAlternatives.length > 0) {
const [first, ...rest] = foundAlternatives.map((alternative) => `--${alternative}`);
error += rest.length > 0 ? `, did you mean ${first} or ${rest.join(", ")}?` : `, did you mean ${first}?`;
}
}
errors.push(error);
});
if (errors.length > 0) {
throw new Error(errors.join("\n"));
}
}
}
// eslint-disable-next-line class-methods-use-this,@typescript-eslint/no-explicit-any
validateCommandArgsForConflicts(arguments_, commandArguments, command) {
const conflicts = arguments_.filter((argument) => argument.conflicts !== void 0);
if (conflicts.length > 0) {
const conflict = conflicts.find((argument) => {
if (Array.isArray(argument.conflicts)) {
return argument.conflicts.some((c) => commandArguments[c] !== void 0) && commandArguments[argument.name] !== void 0;
}
return commandArguments[argument.conflicts] !== void 0 && commandArguments[argument.name] !== void 0;
});
if (conflict) {
throw new Error(
`You called the command "${command.name}" with conflicting options: ${conflict.name} and ${typeof conflict.conflicts === "string" ? conflict.conflicts : conflict.conflicts?.join(", ")}`
);
}
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
addNegatableOption(command) {
if (Array.isArray(command.options)) {
command.options.forEach((option) => {
if (option.name.startsWith("no-") && !command.options.some((o) => o.name === option.name.replace("no-", ""))) {
if (option.type !== Boolean) {
this.logger.debug(`Cannot add negated option "${option.name}" to command "${command.name}" because it is not a boolean.`);
return;
}
const negatedOption = {
...option,
defaultValue: option.defaultValue === void 0 ? true : !option.defaultValue,
name: `${option.name.replace("no-", "")}`
};
command.options.push(negatedOption);
}
});
}
}
async registerExtensions(toolbox) {
const callback = /* @__PURE__ */ __name(async (extension) => {
if (typeof extension.execute !== "function") {
this.logger.warn(`Skipped ${extension.name} because execute is not a function.`);
return null;
}
await extension.execute(toolbox);
return null;
}, "callback");
for (const extension of this.extensions) {
await callback(extension);
}
}
// combining negatable options with their non-negated counterparts
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mapNegatableOptions(toolbox, command) {
Object.entries(toolbox.options).forEach(([key, value]) => {
if (/^no\w+/.test(key)) {
const nonNegatedKey = lowerFirstChar(key.replace("no", ""));
this.logger.debug(`mapping negated option "${key}" to "${nonNegatedKey}"`);
toolbox.options[nonNegatedKey] = !value;
command.options?.forEach((option) => {
if (option.name === nonNegatedKey) {
option.__negated__ = true;
}
});
}
});
}
// Apply any implied option values, if option is undefined or default value.
// eslint-disable-next-line class-methods-use-this,@typescript-eslint/no-explicit-any
mapImpliesOptions(toolbox, command) {
Object.keys(toolbox.options).forEach((optionKey) => {
const option = command.options?.find(
// eslint-disable-next-line no-underscore-dangle
(o) => o.__camelCaseName__ === optionKey && o.__negated__ === void 0 && o.implies !== void 0
);
if (option?.implies) {
const implies = option.implies;
Object.entries(implies).forEach(([key, value]) => {
if (toolbox.options[key] === void 0) {
toolbox.options[key] = value;
} else {
const impliedOption = command.options?.find((cOption) => cOption.name === key);
if (impliedOption?.defaultValue === void 0 || toolbox.options[key] === impliedOption.defaultValue) {
toolbox.options[key] = value;
}
}
});
}
});
}
}
export { Cli };