UNPKG

@devmn/cloud-cli

Version:

CLI tool for Intelligo Cloud.

383 lines (340 loc) 11.9 kB
import { isAbsolute, join } from 'path' import { pathExistsSync } from 'fs-extra' import { readFileSync } from 'fs' import * as yaml from 'js-yaml' import { CommanderStatic } from 'commander' import * as inquirer from 'inquirer' import Constants from '../utils/Constants' import StdOutUtil from '../utils/StdOutUtil' export interface IOptionAlias { name: string char?: string env?: string hide?: boolean } export interface IOption extends inquirer.ListQuestionOptions, IOptionAlias { name: string aliases?: IOptionAlias[] preProcessParam?: (param?: IParam) => void } export interface ICommandLineOptions { [option: string]: string | boolean } export enum ParamType { Config, CommandLine, Question, Env, Default } export interface IParam { value: any from: ParamType } export interface IParams { [param: string]: IParam } function isFunction(value: any): boolean { return value instanceof Function } function getValue<T>( value?: T | ((...args: any) => T), ...args: any ): T | undefined { return value instanceof Function ? value(...args) : value } const CONFIG_FILE_NAME: string = Constants.COMMON_KEYS.conf type IOptionAliasWithDetails = IOptionAlias & { aliasTo: string } export default abstract class Command { protected abstract command: string protected aliases?: string[] = undefined protected usage?: string = undefined protected description?: string = undefined protected options?: IOption[] | ((params?: IParams) => IOption[]) protected configFileProvided = false constructor(private program: CommanderStatic) { if (!program) { throw new Error('program is null') } } private getCmdLineFlags(alias: IOptionAlias, type?: string): string { return ( (alias.char ? `-${alias.char}, ` : '') + `--${alias.name}` + (type !== 'confirm' ? ' <value>' : '') ) } private getCmdLineDescription( option: IOption, spaces: string, alias?: IOptionAlias ): string { const msg = alias ? `same as --${option.name}` : getValue(option.message) || '' const env = alias ? alias.env : option.env return (msg + (env ? ` (env: ${env})` : '')) .split('\n') .reduce( (acc, l) => (!acc ? l.trim() : `${acc}\n${spaces}${l.trim()}`), '' ) } private getOptions(params?: IParams): IOption[] { return getValue(this.options, params) || [] } protected findParamValue( params: IParams | undefined, name: string ): IParam | undefined { return params && params[name] } protected paramValue<T>( params: IParams | undefined, name: string ): T | undefined { return params && params[name] && params[name].value } protected paramFrom( params: IParams | undefined, name: string ): ParamType | undefined { return params && params[name] && params[name].from } protected getDefaultConfigFileOption( preProcessParam?: (param?: IParam) => void ): IOption { return { name: CONFIG_FILE_NAME, char: 'c', env: 'CAPROVER_CONFIG_FILE', message: 'path of the file where all parameters are defined in JSON or YAML format\n' + "see others options to know config file parameters' names\n" + 'this is mainly for automation purposes, see docs', preProcessParam } } build() { if (!this.command) { throw new Error('Empty command name') } const cmd = this.program.command(this.command) if (this.aliases && this.aliases.length) { this.aliases.forEach(alias => alias && cmd.alias(alias)) } if (this.description) { cmd.description(this.description) } if (this.usage) { cmd.usage(this.usage) } const options = this.getOptions().filter( opt => opt && opt.name && !opt.hide ) const spaces = ' '.repeat( options.reduce( (max, opt) => Math.max( max, this.getCmdLineFlags(opt, opt.type).length, (opt.aliases || []) .filter(alias => alias && alias.name && !alias.hide) .reduce( (amax, a) => Math.max( amax, this.getCmdLineFlags(a, opt.type).length ), 0 ) ), 0 ) + 4 ) options.forEach(opt => { cmd.option( this.getCmdLineFlags(opt, opt.type), this.getCmdLineDescription(opt, spaces), getValue(opt.default) ) if (opt.aliases) { opt.aliases .filter(alias => alias && alias.name && !alias.hide) .forEach(alias => cmd.option( this.getCmdLineFlags(alias, opt.type), this.getCmdLineDescription(opt, spaces, alias) ) ) } }) cmd.action(async (...allParams: any[]) => { if (allParams.length > 1) { StdOutUtil.printError( `Positional parameter not supported: ${allParams[0]}\n`, true ) } const cmdLineOptions = await this.preAction(allParams[0]) const optionAliases: IOptionAliasWithDetails[] = this.getOptions() .filter(opt => opt && opt.name) .reduce( (acc, opt) => [ ...acc, { ...opt, aliasTo: opt.name }, ...(opt.aliases || []) .filter(alias => alias && alias.name) .map(alias => ({ ...alias, aliasTo: opt.name })) ], [] ) if (cmdLineOptions) { this.action(await this.getParams(cmdLineOptions, optionAliases)) } }) } protected async preAction( cmdLineoptions: ICommandLineOptions ): Promise<ICommandLineOptions | undefined> { if (this.description) { StdOutUtil.printMessage(this.description + '\n') } return Promise.resolve(cmdLineoptions) } private async getParams( cmdLineOptions: ICommandLineOptions, optionAliases: IOptionAliasWithDetails[] ): Promise<IParams> { const params: IParams = {} // Read params from env variables optionAliases .filter(opta => opta.env && opta.env in process.env) .forEach( opta => (params[opta.aliasTo] = { value: process.env[opta.env!], from: ParamType.Env }) ) // Get config file name from env variables or command line options let file: string | null = optionAliases .filter( opta => cmdLineOptions && opta.aliasTo === CONFIG_FILE_NAME && opta.name in cmdLineOptions ) .reduce((prev, opta) => cmdLineOptions[opta.name] as string, null) if (params[CONFIG_FILE_NAME]) { if (file === null) { file = params[CONFIG_FILE_NAME].value } delete params[CONFIG_FILE_NAME] } optionAliases = optionAliases.filter( opta => opta.aliasTo !== CONFIG_FILE_NAME ) if (file) { // Read params from config file const filePath = isAbsolute(file) ? file : join(process.cwd(), file) if (!pathExistsSync(filePath)) { StdOutUtil.printError(`File not found: ${filePath}\n`, true) } let config: any try { const fileContent = readFileSync(filePath, 'utf8').trim() if (fileContent && fileContent.length) { if ( fileContent.startsWith('{') || fileContent.startsWith('[') ) { config = JSON.parse(fileContent) } else { config = yaml.safeLoad(fileContent) } } if (!config) { throw new Error('Config file is empty!!') } } catch (error) { StdOutUtil.printError( `Error reading config file: ${error.message || error}\n`, true ) } this.configFileProvided = true optionAliases .filter(opta => opta.name in config) .forEach( opta => (params[opta.aliasTo] = { value: config[opta.name], from: ParamType.Config }) ) } if (cmdLineOptions) { // Overwrite params from command line options optionAliases .filter(opta => opta.name in cmdLineOptions) .forEach( opta => (params[opta.aliasTo] = { value: cmdLineOptions[opta.name], from: ParamType.CommandLine }) ) } const options = this.getOptions(params).filter(opt => opt && opt.name) let q = false for (const option of options) { const name = option.name! let param = params[name] if (param) { // Filter and validate already provided params if (option.filter) { param.value = await option.filter(param.value) } if (option.validate) { const err = await option.validate(param.value) if (err !== true) { StdOutUtil.printError( `${q ? '\n' : ''}${err || 'Error!'}\n`, true ) } } } else if (name !== CONFIG_FILE_NAME) { // Questions for missing params if (!isFunction(option.message)) { option.message += ':' } const answer = await inquirer.prompt([option]) if (name in answer) { q = true param = params[name] = { value: answer[name], from: ParamType.Question } } } if (option.preProcessParam) { await option.preProcessParam(param) } } if (q) { StdOutUtil.printMessage('') } return params } /** * This method gets called once all the required information has been collected, either manually * using the questions, or directly via the params and etc. * * @param params */ protected abstract async action(params: IParams): Promise<void> }