UNPKG

@mountainpass/hooked-cli

Version:
233 lines (232 loc) 12.9 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 { Argument, Command, Option } from 'commander'; import fs from 'fs'; import HJSON from 'hjson'; import { cyan, yellow } from './colour.js'; import common from './common/invoke.js'; import loaders from './common/loaders.js'; import defaults from './defaults.js'; import exitHandler from './exitHandler.js'; import { init, initialiseConfig, initialiseDocker, initialiseSsl } from './initialisers.js'; import verifyLocalRequiredTools from './scriptExecutors/verifyLocalRequiredTools.js'; import server from './server/server.js'; import { isNumber, isString, isUndefined } from './types.js'; import logger from './utils/logger.js'; import { loadRootPackageJsonSync } from './utils/packageJson.js'; import bcrypt from './server/bcrypt.js'; const packageJson = loadRootPackageJsonSync(); export const newProgram = (systemProcessEnvs, serverShutdownSignal) => { const program = new Command(); program .name('hooked') .description('CLI to execute preconfigured scripts.\n\nUpdate: npm i -g --prefer-online --force @mountainpass/hooked-cli') .version(packageJson.version, '-v, --version', 'Output the current version of hooked.') .addOption(new Option('-pw, --hashPassword <hashPassword>', 'Hashes the provided password, and exits.') .env('HASH_PASSWORD')) .addOption(new Option('-i, --init', 'Runs the initialisation wizard, and exits.') .env('INIT')) .addOption(new Option('-va, --validate', 'Validates the config, and exits.') .env('INIT')) .addOption(new Option('-ic, --initConfig', 'Creates a basic configuration file (hooked.yaml) in the current directory.') .default(false).env('INIT_CONFIG')) .addOption(new Option('-is, --initSsl', 'Creates self signed SSL certificates (hooked-cert.pem and hooked-key.pem) in the current directory.') .default(false).env('INIT_SSL')) .addOption(new Option('-id, --initDocker', 'Initialises a Docker compose file, starts the service, and exits.') .default(false).env('INIT_DOCKER')) .addOption(new Option('-f, --force', 'Forces the operation - usually with regard to overwriting a file.') .default(false).env('FORCE')) .addOption(new Option('-e, --env <env>', 'Accepts a comma separated list of environment names (environment "default" is always on).') .default('default').env('ENV')) .addOption(new Option('-in, --stdin <json>', 'Allows predefining stdin responses.') .default('{}').env('STDIN')) .addOption(new Option('-ls, --listEnvs', 'Lists the available environments, and exits.') .env('LISTENVS')) .addOption(new Option('-ll, --logLevel <logLevel>', '<info|debug|warn|error> Specifies the log level.') .default('info').env('LOG_LEVEL')) .addOption(new Option('-d, --debug', 'Sets the log level to "debug".') .implies({ logLevel: 'debug' })) .addOption(new Option('-sc, --skipCleanup', "If 'true', doesn't cleanup old *.sh files. Useful for debugging.") .default(false).env('SKIP_CLEANUP')) .addOption(new Option('-svc, --skipVersionCheck', 'If present, skips the version check at startup.') .default(false).env('SKIP_VERSION_CHECK')) .addOption(new Option('-dhd, --dockerHookedDir <dockerHookedDir>', 'Used to specify the HOOKED directory in relation to the Docker host.') .env('DOCKER_HOOKED_DIR')) .addOption(new Option('-tz, --timezone <timezone>', "The timezone to use for Cron triggers. e.g. 'Australia/Sydney'.") .default(Intl.DateTimeFormat().resolvedOptions().timeZone).env('TZ')) .addOption(new Option('-p, --pull', 'Force download all imports from remote to local cache.') .env('PULL')) .addOption(new Option('-u, --update', 'Prints the command to update to the latest version of hooked, and exits.') .env('UPDATE')) .addOption(new Option('-b, --batch', 'Non-interactive "batch" mode - errors if an interactive prompt is required.') .env('CI')) .addOption(new Option('-c, --config <config>', 'Specify the hooked configuration file to use.') .env('CONFIG')) .addOption(new Option('-sp, --scriptPath', 'A space-delimited script path to execute (supercedes the argument).') .argParser((val) => val.trim().split(' ')) .env('SCRIPTPATH')) .addOption(new Option('-s, --server [port]', 'Runs hooked in server mode. Enables cron jobs, rest api and web ui.') .argParser(parseInt) .implies({ batch: true }) .env('SERVER') .preset(4000) .conflicts(['version', 'env', 'stdin', 'printenv', 'pretty', 'listEnvs', 'log', 'update'])) .addOption(new Option('--ssl', 'Enable SSL, using the default hooked-cert.pem and hooked-key.pem files.') .conflicts(['version', 'env', 'stdin', 'printenv', 'pretty', 'listEnvs', 'log', 'update']) .env('SSL') .implies({ sslKey: 'hooked-key.pem', sslCert: 'hooked-cert.pem' })) .addOption(new Option('--sslKey [sslKey]', 'The no-passphrase private key in PEM format.') .conflicts(['version', 'env', 'stdin', 'printenv', 'pretty', 'listEnvs', 'log', 'update']) .env('SSL_KEY') .preset('hooked-key.pem')) .addOption(new Option('--sslCert [sslCert]', 'The certificate chains in PEM format.') .conflicts(['version', 'env', 'stdin', 'printenv', 'pretty', 'listEnvs', 'log', 'update']) .env('SSL_CERT') .preset('hooked-cert.pem')) .addOption(new Option('--api-key <apiKey>', 'The "Authorization" Bearer token, that must be present to access API endpoints.') .env('API_KEY') .conflicts(['version', 'env', 'stdin', 'printenv', 'pretty', 'listEnvs', 'log', 'update'])) .addArgument(new Argument('[scriptPath...]', 'The space delimited, path of the script to run.') .default([])) .addHelpText('afterAll', ` Provided Environment Variables: HOOKED_FILE The root hooked.yaml file that was run. HOOKED_DIR The parent directory of the HOOKED_FILE. HOOKED_ROOT <true|false> True if the current script is the root file. `) .usage('[options]') .action((scriptPathArgs, options) => __awaiter(void 0, void 0, void 0, function* () { var _a, _b, _c, _d, _e, _f; // exit handler exitHandler.onExit(options); // set log level logger.setLogLevel(options.logLevel); // set the script path from the command line arguments if (isUndefined(options.scriptPath)) { options.scriptPath = scriptPathArgs; } // set the default configuration location (should be first step!) defaults.setDefaultConfigurationFilepath(options.config); // check for server mode const port = options.server; const isServerMode = isNumber(port); // show update command... if (options.update === true) { logger.info(`Please run the command: ${cyan('npm i -g --prefer-online --force @mountainpass/hooked-cli')}`); return; } // print help if (options.help === true) { program.help(); return; } // initialise a new project... if (options.init === true) { // ensure the default configuration file path is ${HOOKED_DIR}/hooked.yaml, and not ~/hooked.yaml! yield init(options); return; } if (options.initConfig === true) { yield initialiseConfig(options); } if (options.initSsl === true) { yield initialiseSsl(options); } if (options.initDocker === true) { yield initialiseDocker(options); return; // NOTE - exit after docker initialisation! } // no config? initialise a new project... const defaultInstance = defaults.getDefaults(); if (!fs.existsSync(defaultInstance.HOOKED_FILE)) { logger.warn(`No config file found at '${defaultInstance.HOOKED_FILE}'. Launching setup...`); yield init(options); return; } else { logger.debug(`Using config file: ${defaultInstance.HOOKED_FILE}`); } // load configuration (after init, in case the config file was just created!) let config = yield loaders.loadConfiguration(systemProcessEnvs, options); if (options.validate) { return; } // check for newer versions if (options.skipVersionCheck !== true && isUndefined((_b = (_a = config.env) === null || _a === void 0 ? void 0 : _a.default) === null || _b === void 0 ? void 0 : _b.SKIP_VERSION_CHECK)) { yield verifyLocalRequiredTools.verifyLatestVersion(); } else { logger.debug('Skipping version check...'); } // hash the provided password and exit if (isString(options.hashPassword)) { logger.info(bcrypt.hash(options.hashPassword, (_d = (_c = config.server) === null || _c === void 0 ? void 0 : _c.auth.salt) !== null && _d !== void 0 ? _d : bcrypt.generateSalt())); return; } // show environment names... if (options.listEnvs === true) { logger.info(`Available environments: ${yellow(Object.keys((_e = config.env) !== null && _e !== void 0 ? _e : {}).join(', '))}`); return; } if (isServerMode) { // server mode yield server.startServer(port, systemProcessEnvs, options, (_f = config.server) !== null && _f !== void 0 ? _f : {}, serverShutdownSignal); } else { // script mode const providedEnvNames = options.env.split(','); const tmp = options.stdin.replace(/(^['"])|(['"]$)/g, ''); const stdin = HJSON.parse(tmp); if (isString(stdin)) { throw new Error(`Invalid stdin provided. Must be a JSON object. Found="${tmp}"`); } yield common.invoke(null, systemProcessEnvs, options, config, providedEnvNames, options.scriptPath, stdin, true, false); // try { // } finally { // // store and log "Rerun" command in history (if successful and not the _logs_ option!) // // const isRoot = result.envVars.HOOKED_ROOT === 'true' // const notRequestingLogs = invocationResult!.paths.join(' ') !== defaults.getDefaults().LOGS_MENU_OPTION // // if (isRoot && notRequestingLogs) { // if (notRequestingLogs) { // logger.debug(`Rerun: ${displayInvocationResult(invocationResult!)}`) // addHistory(invocationResult!) // } // // } // } } // generate rerun command (do before running script - reason: if errors, won't know how to re-run?) // Or should we ONLY store succesful scripts? I mean... it's in there in the name. // const successfulScript: SuccessfulScript = { // ts: Date.now(), // scriptPath: result.paths, // envNames: [...new Set(['default', ...result.envNames])], // stdin: result.envVars // } // // store and log "Rerun" command in history (if successful and not the _logs_ option!) // const isRoot = result.envVars.HOOKED_ROOT === 'true' // const notRequestingLogs = result.paths.join(' ') !== defaults.getDefaults().LOGS_MENU_OPTION // if (isRoot && notRequestingLogs) { // logger.debug(`Rerun: ${displaySuccessfulScript(successfulScript)}`) // } // // NOTE - update history file AFTER script is run... (otherise `git porcelain` complains about file changes) // if (isRoot && notRequestingLogs) { // addHistory(successfulScript) // } })); return program; }; export default (...args_1) => __awaiter(void 0, [...args_1], void 0, function* (argv = process.argv) { const serverShutdownController = new AbortController(); let program = newProgram(process.env, serverShutdownController.signal); return yield program.parseAsync(argv); });