UNPKG

@mountainpass/hooked-cli

Version:
323 lines (322 loc) 13.6 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import fs from 'fs'; import inquirer from 'inquirer'; import YAML from 'yaml'; import defaults from './defaults.js'; import { displaySuccessfulScript, fetchHistory } from './history.js'; import { schemaValidator } from './schema/HookedSchema.js'; import { resolveScript } from './scriptExecutors/ScriptExecutor.js'; import { isScript } from './types.js'; import { Environment } from './utils/Environment.js'; import fileUtils from './utils/fileUtils.js'; import { fetchImports } from './utils/imports.js'; import logger from './utils/logger.js'; const isDefined = (o) => typeof o !== 'undefined' && o !== null; export const stripLeadingEmojiSpace = (str) => { return str.replace(/^\p{Extended_Pictographic}\s+/u, ''); }; export const startsWithEmojiSpace = (str) => /^\p{Extended_Pictographic}\s/u.test(str); export const stripEmojis = (str) => { return str.replace(/\p{Extended_Pictographic}/gu, '') .replace(/\s+/g, ' ') .trim(); }; export const normaliseString = (str) => { return str.normalize('NFD').replace(/\p{Diacritic}/gu, '').toLowerCase(); }; /** * Finds a script, given a path. */ export const findScript = (config, scriptPath, options) => __awaiter(void 0, void 0, void 0, function* () { var _a, _b; let script = (_a = config.scripts) !== null && _a !== void 0 ? _a : {}; // inject _logs_ into scripts if (isDefined(config.scripts)) { const history = fetchHistory().map((log) => { const display = displaySuccessfulScript(log, true); const $cmd = displaySuccessfulScript(log, false); return [display, { $cmd }]; }); // if (history.length > 0) { script = Object.assign({ [defaults.getDefaults().LOGS_MENU_OPTION]: Object.fromEntries(history) }, script); // } else { // logger.debug('No history found.') // } } const resolvedScriptPath = []; for (const path of scriptPath) { if (isDefined(script[path])) { // find exact match resolvedScriptPath.push(path); script = script[path]; } else if (isDefined(script[stripEmojis(path)])) { // find exact match without emojis resolvedScriptPath.push(path); script = script[path]; } else { // try to find partial match // search by prefix const entries = Object.entries(script); const found = entries.filter(([key, value]) => stripEmojis(normaliseString(key)).startsWith(normaliseString(path))); if (found.length === 1) { const foundKey = found[0][0]; resolvedScriptPath.push(foundKey); script = found[0][1]; } } } // no match... prompt user for which script to select if (!isScript(script) && options.batch === true) { throw new Error(`Script not found - '${scriptPath.join(' ')}' (interactive prompts disabled).`); } while (!isScript(script)) { if (typeof script === 'undefined' || script === null || Object.keys(script).length === 0) { const availableScripts = `\t- ${stringifyScripts(config).join('\t- ')}`; throw new Error(`No scripts found at path: ${JSON.stringify(scriptPath)}\nDid you mean?\n${availableScripts}`); } let choices = []; const modifiedScripts = {}; if (((_b = config.plugins) === null || _b === void 0 ? void 0 : _b.icons) === true) { // if environment flag is set, add emoji choices = Object.entries(script).map(([c, v]) => { if (startsWithEmojiSpace(c)) { return c; } else { // 🪝📁✅🟢 const icon = isScript(v) ? '🪝 ' : '📁'; // const icon = isScript(v) ? '⚡' : '📁' const modScript = `${icon} ${c}`; modifiedScripts[modScript] = true; return modScript; } }); } else { choices = Object.keys(script); } // ask user for the next script // const stdout = new CaptureStream(process.stdout) yield inquirer .prompt([ { type: 'list', name: 'next', message: 'Please select an option:', pageSize: defaults.getDefaults().PAGE_SIZE, default: choices[0], choices, loop: true, // output: stdout } ]) .then((answers) => { const nextScript = modifiedScripts[answers.next] === true ? stripLeadingEmojiSpace(answers.next) : answers.next; resolvedScriptPath.push(nextScript); script = script[nextScript]; }); } logger.debug(`Found script: ${resolvedScriptPath.join(' ')}`); return [script, resolvedScriptPath]; }); /** * Gets a list of executable scripts. */ export const stringifyScripts = (config) => { const scripts = []; const walk = (obj, path = []) => { for (const key in obj) { if (key.startsWith('$')) { scripts.push(path.join(' ')); } else { walk(obj[key], [...path, key]); } } }; walk(config.scripts); return scripts; }; /** * Resolves an environment configuration. * @param config * @param envName * @returns */ export const internalFindEnv = (config, envName = 'default', options) => { var _a; // look for exact match if (isDefined(config.env) && isDefined(config.env[envName])) { logger.debug(`Using environment: ${envName}`); const newLocal = config.env[envName]; return [newLocal === null ? {} : newLocal, envName]; } // if only one environment, always use that? No // search by prefix const envs = Object.entries((_a = config.env) !== null && _a !== void 0 ? _a : {}); const found = envs.filter(([key, value]) => key.startsWith(envName)); if (found.length === 1) { const foundEnv = found[0][0]; logger.debug(`Using environment: ${foundEnv}`); const newLocal = found[0][1]; return [newLocal === null ? {} : newLocal, foundEnv]; } else if (found.length === 0 && envName === 'default') { // ignore blank env:, if the environment is 'default' return [{}, 'default']; } const availableEnvs = envs.map(([key, value]) => `\t- ${key}`).join('\n'); throw new Error(`Environment not found: ${envName}\nDid you mean?\n${envs.length > 0 ? availableEnvs : '(No environments available)'}`); }; /* Aggregator function, for merging imported configs. */ const _mergeEnvAndScripts = (tmp, aggrEnvs, aggrScripts) => { // merge scripts - easy, they should all have unique top level names! Object.assign(aggrScripts, tmp.scripts); // merge envs - harder, they can have the same top level names... if (isDefined(tmp.env)) { Object.entries(tmp.env).forEach(([key, envVars]) => { // if they have the same top level name, then merge them... if (isDefined(aggrEnvs[key])) { aggrEnvs[key] = Object.assign(Object.assign({}, aggrEnvs[key]), envVars); } else { aggrEnvs[key] = envVars; } }); } }; export const _resolveAndMergeConfigurationWithImports = (config_1, ...args_1) => __awaiter(void 0, [config_1, ...args_1], void 0, function* (config, pullLatestFlag = false) { // load and apply imports const envs = {}; const scripts = {}; // resolve external imports const allLocal = yield fetchImports(config.imports, pullLatestFlag); // load imports from local for (const localpath of allLocal) { const filepath = fileUtils.resolvePath(localpath); logger.debug(`Importing: ${filepath}`); // files must be loaded in order const tmp = yield loadConfig(filepath, pullLatestFlag); _mergeEnvAndScripts(tmp, envs, scripts); } // do final top level merge _mergeEnvAndScripts(config, envs, scripts); // overwrite the existing config object! config.env = envs; config.scripts = scripts; }); /** * Finds and resolves the environment. * @param config * @param environment * @returns */ export const fetchGlobalEnvVars = (...args_1) => __awaiter(void 0, [...args_1], void 0, function* (config = {}, environmentNames = ['default'], options = {}, envVars = {}) { // look for and apply all matching environments const allEnvNames = new Set(); for (const envName of environmentNames) { const [foundEnv = {}, resolvedEnvName] = internalFindEnv(config, envName, options); // collect ALL environment variables (in no particular order!) for (const [key, script] of Object.entries(foundEnv)) { envVars[key] = script; } allEnvNames.add(resolvedEnvName); } return [envVars, [...allEnvNames]]; }); export const resolveEnvironmentVariables = (...args_1) => __awaiter(void 0, [...args_1], void 0, function* (config = {}, envVars, stdin = {}, env = new Environment(), options = {}) { // TODO resolve scripts, regardless of order (option 1 - dumb brute force resolution, option 2 - resolve in order) // OPTION 1 - brute force, up to 5 attempts... let remainingAttempts = Object.entries(envVars); let errors = []; while (remainingAttempts.length > 0) { const retry = []; errors = []; // attempt to resolve variables, sequentially... for (const [key, script] of remainingAttempts) { try { yield resolveScript(key, script, stdin, env, config, options, envVars, false, true); } catch (err) { // could not resolve, add to retry list errors.push(err); retry.push([key, script]); } } // no progress made, abort! if (retry.length === remainingAttempts.length) { logger.debug(`Retry stuck with ${retry.length} unresolved environment variable(s)...`); break; } else if (retry.length > 0) { // some progress made, log and retry logger.debug(`Retrying ${retry.length} unresolved environment variable(s)...`); } // update remaining attempts remainingAttempts = retry; } // if there are still remaining attempts... if (remainingAttempts.length > 0 && errors.length > 0) { const errorsString = `- ${errors.map((err) => err.message).join('\n- ')}`; // const envString = `environment=${toJsonString(envVars, true)}` throw new Error(`Could not resolve ${errors.length} environment variables:\n${errorsString}`); } // OLD WAY - assumes env vars can be resolved in order... // for (const [key, script] of keyScriptPairs) { // await resolveScript(key, script, stdin, env, config, options) // } }); export const populateScriptPath = (scripts, parentPaths = []) => { for (const [key, script] of Object.entries(scripts)) { if (isScript(script)) { script._scriptPath = [...parentPaths, key].join(' '); script._scriptPathArray = [...parentPaths, key]; } else { // TODO verify unique paths? const firstWord = key.trim().split(' ')[0]; populateScriptPath(script, [...parentPaths, firstWord]); } } }; export const loadConfig = (configFile_1, ...args_1) => __awaiter(void 0, [configFile_1, ...args_1], void 0, function* (configFile, pullLatestFlag = false, validate = false) { var _a; const fileExists = fs.existsSync(configFile); // file exists if (fileExists) { const yamlStr = fs.readFileSync(configFile, 'utf-8'); const config = YAML.parse(yamlStr); if (config === null) { throw new Error(`Invalid YAML in ${configFile} - ${yamlStr}`); } // validate this configuration file (called recursively for imports) if (!schemaValidator(configFile, config)) { if (validate) { throw new Error(`Invalid configuration file: ${configFile}`); } else { logger.warn("Attempting to continue with invalid configuration file..."); } } // add _scriptPath fields to all scripts populateScriptPath((_a = config.scripts) !== null && _a !== void 0 ? _a : {}); // merge imports with current configuration yield _resolveAndMergeConfigurationWithImports(config, pullLatestFlag); return config; } else { throw new Error(`No ${configFile} file found.`); } });