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 | boolean ; } export enum ParamType { Config , CommandLine , Question , Env , Default } export interface IParam { value : any ; from : ParamType ; } export interface IParams { [ param ] : IParam ; } function isFunction ( value ) { return value instanceof Function ; } function getValue < T > ( value ? , ... args ) { return value instanceof Function ? value ( ... args ) : value ; } const CONFIG_FILE_NAME = 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 ) { if ( ! program ) { throw new Error ( 'program is null' ) ; } } private getCmdLineFlags ( alias , type ? ) { return ( ( alias . char ? `-${ alias . char }, ` : '' ) + `--${ alias . name }` + ( type !== 'confirm' ? ' <value>' : '' ) ) ; } private getCmdLineDescription ( option , spaces , alias ? ) { 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 ? ) { return getValue ( this . options , params ) || [ ] ; } protected findParamValue ( params , name ) { return params && params [ name ] ; } protected paramValue < T > ( params , name ) { return params && params [ name ] && params [ name ] . value ; } protected paramFrom ( params , name ) { return params && params [ name ] && params [ name ] . from ; } protected getDefaultConfigFileOption ( preProcessParam ? ) { 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 ) => { if ( allParams . length > 1 ) { StdOutUtil . printError ( `Positional parameter not supported: ${ allParams [ 0 ] }\n` , true ) ; } const cmdLineOptions = await this . preAction ( allParams [ 0 ] ) ; const optionAliases = 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 ) { if ( this . description ) { StdOutUtil . printMessage ( this . description + '\n' ) ; } return Promise . resolve ( cmdLineoptions ) ; } private async getParams ( cmdLineOptions , optionAliases ) { const params = { } ; optionAliases . filter ( opta => opta . env && opta . env in process . env ) . forEach ( opta => ( params [ opta . aliasTo ] = { value : process . env [ opta . env ! ] , from : ParamType . Env } ) ) ; let file = 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 ) { const filePath = isAbsolute ( file ) ? file : join ( process . cwd ( ) , file ) ; if ( ! pathExistsSync ( filePath ) ) { StdOutUtil . printError ( `File not found: ${ filePath }\n` , true ) ; } let config ; 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 ) { 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 ) { 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 ) { 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 ; } param params protected abstract async action ( params ) ; }