aelf-command
Version:
A CLI tools for AElf
251 lines (239 loc) • 7.32 kB
JavaScript
import { interopImportCJSDefault } from 'node-cjs-interop';
import asyncValidator from 'async-validator';
const Schema = interopImportCJSDefault(asyncValidator);
import inquirer from 'inquirer';
import ora from 'ora';
import { logger } from '../utils/myLogger.js';
import { camelCase } from '../utils/utils.js';
import { commonGlobalOptionValidatorDesc, globalOptionsPrompts, strictGlobalOptionValidatorDesc } from '../utils/constants.js';
// Schema.warning = () => {}; // TypeError: Cannot add property warning, object is not extensible
const defaultOraOptions = {
text: 'AElf loading...'
};
/**
* @typedef {import('commander').Command} Command
* @typedef {import('ora').Options} OraOptions
* @typedef {import('../../types/rc/index.js').default} Registry
*/
/**
* @class
*/
class BaseSubCommand {
/**
* @param {string} commandName sub command name
* @param {{ [key: string]: any }[]} parameters sub command parameters
* @param {string} description sub command description
* @param {{ [key: string]: any }[]} options sub command options
* @param {string[]} usage make examples
* @param {Registry} rc instance of Registry
* @param {{ [key: string]: any }} validatorDesc rules of async-validator
* @param {{ [key: string]: any }} oraOptions an ora options
*/
constructor(
commandName,
parameters = [],
description,
options = [],
usage = [],
rc,
validatorDesc = strictGlobalOptionValidatorDesc,
oraOptions = defaultOraOptions
) {
this.commandName = commandName;
this.parameters = parameters;
this.description = description;
this.options = options;
this.validatorDesc = {};
this.usage = usage;
this.rc = rc;
this.oraInstance = ora(oraOptions);
this.customPrompts = false;
Object.entries(validatorDesc).forEach(([key, value]) => {
this.validatorDesc[key] = {
...strictGlobalOptionValidatorDesc[key],
...value
};
});
}
/**
* Sets custom prompts.
* @param {any} val - The value to set for custom prompts.
*/
setCustomPrompts(val) {
this.customPrompts = val;
}
/**
* Initializes the sub command with commander.
* @param {Command} commander - The commander instance.
*/
init(commander) {
let command = commander.command(`${this.commandName} ${this.getParameters()}`).description(this.description);
for (const { flag, description } of this.options) {
command = command.option(flag, description);
}
command
.action(async (...args) => {
await this.run(commander, ...args);
this.oraInstance.stop();
})
.on('--help', () => {
// todo: chalk
console.info('');
console.info('Examples:');
console.info('');
console.info(`${this.makeExamples().join('\n')}`);
});
}
/**
* Retrieves parameters as a string.
* @returns {string} Parameters string.
*/
getParameters() {
return this.parameters
.map(v => {
const { name, required = false, extraName = [] } = v;
const symbol = [name, ...extraName].join('|');
return required ? `<${symbol}>` : `[${symbol}]`;
})
.join(' ');
}
/**
* Handles errors related to universal options.
* @param {any} error - The error to handle.
*/
handleUniOptionsError(error) {
const { errors = [] } = error;
// @ts-ignore
logger.error(errors.reduce((acc, i) => `${acc}${i.message}\n`, ''));
process.exit(1);
}
/**
* Retrieves universal configuration.
* @static
* @param {Command} commander - The commander instance.
* @returns {Record<string, any>} Universal configuration.
*/
static getUniConfig(commander) {
const result = {};
Object.keys(commonGlobalOptionValidatorDesc).forEach(v => {
const options = commander.opts();
if (options[v]) {
result[v] = options[v];
}
});
return result;
}
/**
* Parses a boolean value.
* @static
* @param {any} val - The value to parse.
* @returns {any} Parsed boolean value.
*/
static parseBoolean(val) {
if (val === 'true') {
return true;
}
if (val === 'false') {
return false;
}
return val;
}
/**
* Normalizes configuration object.
* @static
* @param {any} obj - The configuration object to normalize.
* @returns {Record<string, any>} Normalized configuration object.
*/
static normalizeConfig(obj) {
// dash to camel-case
// 'true', 'false' to true, false
const result = {};
Object.entries(obj).forEach(([key, value]) => {
if (value === '' || value === null || value === undefined) {
return;
}
result[camelCase(key)] = BaseSubCommand.parseBoolean(value);
});
return result;
}
/**
* Runs the sub command.
* @param {Command} commander - The commander instance.
* @param {...any} args - Additional arguments.
* @returns {Promise<{
* localOptions: { [key: string]: any },
* options: { [key: string]: any },
* subOptions: { [key: string]: any }
* } | void>} Promise resolving to options or void.
*/
async run(commander, ...args) {
let subCommandOptions = {};
args.slice(0, this.parameters.length).forEach((v, i) => {
if (v !== undefined) {
const { name, filter = val => val } = this.parameters[i];
subCommandOptions[name] = filter(v);
}
});
// sub command options
const lastArg = args.slice(this.parameters.length)[0];
const localOptions = {};
this.options.forEach(({ name }) => {
localOptions[name] = lastArg?.[name] || undefined;
});
const uniOptions = BaseSubCommand.getUniConfig(commander);
// get options from global config and process.argv
const rc = await this.rc.getConfigs();
let options = BaseSubCommand.normalizeConfig({
...rc,
...uniOptions
});
const globalPrompts = globalOptionsPrompts.filter(
prompt => this.validatorDesc[prompt.name]?.required && !options[prompt.name]
);
const globalPromptsAns = await inquirer.prompt(globalPrompts);
options = {
...options,
...globalPromptsAns
};
this.validator = new Schema(this.validatorDesc);
try {
await this.validator.validate(options);
} catch (e) {
this.handleUniOptionsError(e);
}
subCommandOptions = BaseSubCommand.normalizeConfig(subCommandOptions);
if (this.customPrompts) {
// custom prompts process
return {
localOptions,
options,
subOptions: subCommandOptions
};
}
const subOptionsLength = Object.keys(subCommandOptions).length;
if (subOptionsLength < this.parameters.length) {
try {
const response = BaseSubCommand.normalizeConfig(await inquirer.prompt(this.parameters.slice(subOptionsLength)));
subCommandOptions = {
...subCommandOptions,
...response
};
} catch (e) {
console.error(e);
}
}
return {
localOptions,
options,
subOptions: subCommandOptions
};
}
/**
* Generates examples for usage.
* @returns {string[]} Array of example strings.
*/
makeExamples() {
return this.usage.map(cmd => `aelf-command ${this.commandName} ${cmd}`);
}
}
export default BaseSubCommand;