UNPKG

@mountainpass/hooked-cli

Version:
429 lines (427 loc) 19.3 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 inquirer from 'inquirer'; import path from 'path'; import YAML from 'yaml'; import { fetchGlobalEnvVars, findScript, resolveEnvironmentVariables } from '../config.js'; import defaults from '../defaults.js'; import { checkIfRecognisedAsOldScript, isBoolean, isCmdScript, isDefined, isDockerCmdScript, isEnvScript, isInternalScript, isJobsSerialScript, isNumber, isSSHCmdScript, isStdinScript, isStdinScriptFieldsMapping, isString, isUndefined, isWritePathScript } from '../types.js'; import { toJsonString } from '../utils/Environment.js'; import { mergeEnvVars } from '../utils/envVarUtils.js'; import logger from '../utils/logger.js'; import { executeCmd } from './$cmd.js'; import { StdinChoicesResolver } from './resolvers/StdinChoicesResolver.js'; import verifyLocalRequiredTools from './verifyLocalRequiredTools.js'; import { displayReRunnableScript } from '../history.js'; // Environment variable names that are exempt from being resolved const EXEMPT_ENVIRONMENT_KEYWORDS = ['DOCKER_SCRIPT', 'NPM_SCRIPT', 'MAKE_SCRIPT']; export const isScriptExectorResponse = (o) => { return typeof o === 'object' && typeof o.value === 'string'; }; /** * * @param key * @param script * @param stdin * @param env * @param config * @param options * @param envVars * @param isFinalScript - used to determine if this is the end of a process (i.e. don't capture output) * @returns */ export const resolveInternalScript = (key_1, script_1, stdin_1, env_1, config_1, options_1, envVars_1, ...args_1) => __awaiter(void 0, [key_1, script_1, stdin_1, env_1, config_1, options_1, envVars_1, ...args_1], void 0, function* (key, script, stdin, env, config, options, envVars, isFinalScript = false, storeResultAsEnv = false) { // if "step" env is defined, resolve environment variables if (isDefined(script.$env)) { mergeEnvVars(envVars, script.$env); } // actually resolve the environment variables... (internal script) yield resolveEnvironmentVariables(config, envVars, stdin, env, options); // execute the script const result = yield script.$internal({ key, stdin, env }); if (storeResultAsEnv && isString(result)) { env.putResolved(key, result); } return result; }); /** * Resolves all environment variables, and runs the $cmd script. * @param key * @param script * @param stdin * @param env * @param config * @param options * @param envVars * @param isFinalScript - used to determine if this is the end of a process (i.e. if final script, don't capture output) * @returns */ export const resolveCmdScript = (key_1, script_1, stdin_1, env_1, config_1, options_1, ...args_1) => __awaiter(void 0, [key_1, script_1, stdin_1, env_1, config_1, options_1, ...args_1], void 0, function* (key, script, stdin, env, config, options, envVars = {}, isFinalScript = false, storeResultAsEnv = false) { var _a; // if "step" $env is defined, merge environment variables if (isDefined(script.$env)) { mergeEnvVars(envVars, script.$env); } // include environments defined in $envNames if (isDefined(script.$envNames) && Array.isArray(script.$envNames) && script.$envNames.length > 0) { yield fetchGlobalEnvVars(config, script.$envNames, options, envVars); } // actually resolve the environment variables... (cmd script) yield resolveEnvironmentVariables(config, envVars, stdin, env, options); const missingKeys = env.getMissingRequiredKeys(script.$cmd); if (missingKeys.length > 0) { // eslint-disable-next-line max-len const foundString = toJsonString(env.getAll(), true); // const envVarsString = toJsonString(envVars, true) // eslint-disable-next-line max-len throw new Error(`Script '${key}' is missing required environment variables: ${JSON.stringify(missingKeys.sort())}\nFound: ${foundString}`); } // if set to true, or not defined and not a docker or ssh script, include host environment variables... const isDocker = isDockerCmdScript(script); const isSSH = isSSHCmdScript(script); if (script.$envFromHost === true) { env.putAllResolved(process.env, false); } else if (!isDefined(script.$envFromHost) && !isDocker && !isSSH) { logger.debug(`Including host environment variables for script '${key}' (isDocker=${String(isDocker)}, isSSH=${String(isSSH)})`); env.putAllResolved(process.env, false); } else { logger.debug(`Not including host environment variables for script '${key}'`); } // execute the command, capture the output try { // if running an image, verify docker is installed const runInDocker = isDockerCmdScript(script); if (runInDocker) { yield verifyLocalRequiredTools.verifyDockerExists(env); } if (isFinalScript) { logger.debug(`Rerun: ${displayReRunnableScript(options.scriptPath, (_a = options.env) === null || _a === void 0 ? void 0 : _a.split(','), stdin, options.config)}`); } // !!! run the actual command !!! let newValue = yield executeCmd(key, script, options, { env: env.resolved }, env, // N.B. we do NOT want to capture output if this is the final script, we want it to be streamed to stdout! { printStdio: true, captureStdout: !isFinalScript }); // remove trailing newlines newValue = newValue.replace(/(\r?\n)*$/, ''); // if not the final script, if (storeResultAsEnv && isString(newValue)) { env.putResolved(key, newValue); } return newValue; } catch (e) { if (isString(script.$errorMessage)) { logger.warn(script.$errorMessage); } throw e; } }); /** * Resolves all environment variables, and writes the file. * @param key * @param script * @param stdin * @param env * @param config * @param options * @param envVars * @param isFinalScript - used to determine if this is the end of a process (i.e. don't capture output) * @returns */ export const resolveWritePathScript = (key_1, script_1, stdin_1, env_1, config_1, options_1, ...args_1) => __awaiter(void 0, [key_1, script_1, stdin_1, env_1, config_1, options_1, ...args_1], void 0, function* (key, script, stdin, env, config, options, envVars = {}) { const parentDir = path.dirname(script.$path); // actually resolve the environment variables... (cmd script) yield resolveEnvironmentVariables(config, envVars, stdin, env, options); // we need the resolved path, to do the yaml/json check const resolvedPath = env.resolve(script.$path, 'path'); let content; if (isString(script.$content)) { // write string content = script.$content; } else if (/.ya?ml$/i.test(resolvedPath)) { // treat as yaml content = YAML.stringify(script.$content); } else { // treat as json content = JSON.stringify(script.$content, null, 2); } // convert to a CmdScript const cmdScript = { $cmd: ` #!/bin/sh -e ${isString(content) && isString(parentDir) && parentDir.length > 0 ? `mkdir -p ${parentDir}` : ''} ${isString(content) ? `echo Writing file: ${script.$path}` : `echo Creating dir: ${script.$path}`} ${isString(content) ? `cat > ${script.$path} << EOL\n${content}\nEOL` : `mkdir -p ${script.$path}`} ${isString(script.$permissions) ? `chmod ${script.$permissions} ${script.$path}` : ''} ${isString(script.$owner) ? `chown ${script.$owner} ${script.$path}` : ''} ` }; if (isString(script.$image)) cmdScript.$image = script.$image; if (isString(script.$ssh)) cmdScript.$ssh = script.$ssh; yield resolveCmdScript(key, cmdScript, stdin, env, config, options, envVars, true); }); export const resolveEnvScript = (key_1, script_1, stdin_1, env_1, config_1, options_1, ...args_1) => __awaiter(void 0, [key_1, script_1, stdin_1, env_1, config_1, options_1, ...args_1], void 0, function* (key, script, stdin, env, config, options, envVars = {}) { // if "step" $env is defined, merge raw environment variables if (isDefined(script.$env)) { mergeEnvVars(envVars, script.$env); } // resolve environment variables yield resolveEnvironmentVariables(config, envVars, stdin, env, options); }); /** * Converts all script paths to their Script objects. */ export const resolveScripts = (parentPath, script, config, options) => __awaiter(void 0, void 0, void 0, function* () { return yield Promise.all(script.$jobs_serial.map((refOrJob, idx) => __awaiter(void 0, void 0, void 0, function* () { // resolve job by reference if (isString(refOrJob) || isNumber(refOrJob) || isBoolean(refOrJob)) { return yield findScript(config, String(refOrJob).split(' '), options); } else { return [refOrJob, [...parentPath, `${idx}`]]; } }))); }); /** * Throws an error, if any of the provided jobs are not executable. */ export const verifyScriptsAreExecutable = (executableScriptsAndPaths) => { // check executable scripts are actually executable for (const scriptAndPaths of executableScriptsAndPaths) { const [scriptx, pathx] = scriptAndPaths; if (isCmdScript(scriptx) || isInternalScript(scriptx) || isWritePathScript(scriptx) || isEnvScript(scriptx) || isJobsSerialScript(scriptx)) { // all good } else { // uknown throw new Error(`Expected $cmd, $path or $jobs_serial, found "${typeof scriptx}" at path "${pathx.join(' ')}": ${JSON.stringify(scriptx)}`); } } }; export const executeScriptsSequentially = (executableScriptsAndPaths_1, stdin_1, env_1, config_1, options_1, ...args_1) => __awaiter(void 0, [executableScriptsAndPaths_1, stdin_1, env_1, config_1, options_1, ...args_1], void 0, function* (executableScriptsAndPaths, stdin, env, config, options, envVars = {}, isFinalScript, storeResultAsEnv) { var _a; const outputs = []; for (const scriptAndPaths of executableScriptsAndPaths) { const [scriptx, pathsx] = scriptAndPaths; if (isCmdScript(scriptx)) { // resolve $cmd $env vars (if any) if (isDefined(scriptx.$env)) { mergeEnvVars(envVars, (_a = scriptx.$env) !== null && _a !== void 0 ? _a : {}); } logger.debug(`Merged environment: ${JSON.stringify(envVars)}`); // run cmd script outputs.push(yield resolveCmdScript(pathsx.join(' '), scriptx, stdin, env, config, options, envVars, isFinalScript, storeResultAsEnv)); } else if (isInternalScript(scriptx)) { // run internal script outputs.push(yield resolveInternalScript(pathsx.join(' '), scriptx, stdin, env, config, options, envVars, isFinalScript, storeResultAsEnv)); } else if (isWritePathScript(scriptx)) { // write files yield resolveWritePathScript(pathsx.join(' '), scriptx, stdin, env, config, options, envVars); } else if (isEnvScript(scriptx)) { // write files yield resolveEnvScript(pathsx.join(' '), scriptx, stdin, env, config, options, envVars); } } return outputs; }); /** * Resolves all environment variables, and writes the file. * @param key * @param script * @param stdin * @param env * @param config * @param options * @param envVars * @param isFinalScript - used to determine if this is the end of a process (i.e. don't capture output) * @returns */ export const resolveJobsSerialScript = (key_1, script_1, stdin_1, env_1, config_1, options_1, ...args_1) => __awaiter(void 0, [key_1, script_1, stdin_1, env_1, config_1, options_1, ...args_1], void 0, function* (key, script, stdin, env, config, options, envVars = {}) { // resolve job definitions const executableScriptsAndPaths = yield resolveScripts([key], script, config, options); // check executable scripts are actually executable verifyScriptsAreExecutable(executableScriptsAndPaths); // execute scripts sequentially return yield executeScriptsSequentially(executableScriptsAndPaths, stdin, env, config, options, envVars, true, false); }); export class InvalidConfigError extends Error { constructor(m) { super(m); // Set the prototype explicitly. Object.setPrototypeOf(this, InvalidConfigError.prototype); } } /** * This resolves a single environment StdinScript. */ export const resolveStdinScript = (key, script, stdin, env, config, options, envVars) => __awaiter(void 0, void 0, void 0, function* () { const choices = yield StdinChoicesResolver(key, script, { config, env, envVars, options, stdin }); // use to lookup a choice by name or value const findMappedChoice = (candidate, choices) => { if (isUndefined(choices)) return candidate; if (isUndefined(candidate)) return undefined; return choices === null || choices === void 0 ? void 0 : choices.filter(choice => { if (isStdinScriptFieldsMapping(choice)) { return choice.name === candidate || choice.value === candidate; } else { return choice === candidate; } }).map(choice => { if (isStdinScriptFieldsMapping(choice)) { return choice.value; } else { return choice; } }).find(f => String(f)); }; const stdinMapped = findMappedChoice(stdin[key], choices); const envMapped = findMappedChoice(env.isResolvableByKey(key) ? env.resolveByKey(key) : undefined, choices); // logger.info(`Got here stdinMapped=${stdinMapped}, envMapped=${envMapped}, choices=${JSON.stringify(choices)}`) if (isDefined(stdinMapped)) { // if we already have a response, use that env.putResolved(key, stdinMapped); return; } else if (isDefined(envMapped)) { // else if we already have a value in the environment, use the mapped value env.putResolved(key, envMapped); stdin[key] = envMapped; return; } else { // // check if already resolved in environment variables... // if (env.isResolvableByKey(key)) { // // nothing more to do // logger.debug(`Key '${key}' is already resolvable - value=${env.resolveByKey(key)}`) // return // } if (options.batch === true) { throw new Error(`Could not retrieve stdin for key (interactive prompts disabled). key='${key}'.`); } // resolve env vars in name and default... const newMessage = resolveResolveScript('', { $resolve: script.$ask }, env, false); const newDefault = isString(script.$default) ? resolveResolveScript('', { $resolve: script.$default }, env, false) : script.$default; // otherwise prompt user for an answer to the $ask question // const stdout = new CaptureStream(process.stdout) yield inquirer .prompt([ { type: isDefined(choices) ? 'list' : 'text', name: key, message: newMessage, pageSize: defaults.getDefaults().PAGE_SIZE, default: newDefault, choices, loop: true, // output: stdout, } ]) .then((answers) => { const value = answers[key]; if (typeof value === 'undefined') { throw new Error(`Inquirer response is undefined, could not find key '${key}' in answers: ${JSON.stringify(answers)}`); } env.putResolved(key, value); stdin[key] = value; }); } }); /** * Used to resolve environment variables in a string. * * @param key - used for error reporting * @param script - the script to resolve (the value of $resolve) * @param env - environment variables to use for resolving */ export const resolveResolveScript = (key, script, env, insertInEnvironment = true) => { if (insertInEnvironment) { return env.resolveAndPutResolved(key, script.$resolve); } else { return env.resolve(script.$resolve, key); } }; /** * Attempt to resolve environment variables. * @param key * @param script * @param stdin * @param env * @param config * @param options * @param envVars * @returns */ export const resolveScript = (key_1, script_1, ...args_1) => __awaiter(void 0, [key_1, script_1, ...args_1], void 0, function* (key, script, stdin = {}, env, config, options, envVars, isFinalScript, storeResultAsEnv) { // ensure we're only dealing with strings... (from the yaml config) if (typeof script === 'number' || typeof script === 'boolean') { script = String(script); } // perform environment variable resolution // NOTE do not store environment variable - rather, return the string value. if (isInternalScript(script)) { return yield resolveInternalScript(key, script, stdin, env, config, options, envVars, isFinalScript, storeResultAsEnv); } else if (isWritePathScript(script)) { yield resolveWritePathScript(key, script, stdin, env, config, options, envVars); } else if (isJobsSerialScript(script)) { return (yield resolveJobsSerialScript(key, script, stdin, env, config, options, envVars)).join('\n'); } else if (isCmdScript(script)) { return yield resolveCmdScript(key, script, stdin, env, config, options, {}, isFinalScript, storeResultAsEnv); } else if (isStdinScript(script)) { yield resolveStdinScript(key, script, stdin, env, config, options, envVars); } else if (isString(script)) { // NOTE if it's a string, treat it like a "resolve" resolveResolveScript(key, { $resolve: script }, env); } // if it's resolvable, resolve it... if (env.isResolvableByKey(key)) { return env.resolveByKey(key); } else if (EXEMPT_ENVIRONMENT_KEYWORDS.includes(key) && isString(script)) { // else, check if it's exempt (i.e. internally resolved)! env.putResolved(key, script); return script; } else { // otherwise... it wasn't resolvable checkIfRecognisedAsOldScript(script); throw new Error(`Unknown or old script format detected - "${typeof script}" : ${JSON.stringify(script)} at path: ${key}`); } });