@mountainpass/hooked-cli
Version:
A tool for runnable scripts
429 lines (427 loc) • 19.3 kB
JavaScript
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}`);
}
});