UNPKG

@visulima/cerebro

Version:

A delightful toolkit for building Node-powered CLIs.

1,230 lines (1,202 loc) 47.5 kB
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 };