easy-cli-framework
Version:
A framework for building CLI applications that are robust and easy to maintain. Supports theming, configuration files, interactive prompts, and more.
416 lines (412 loc) • 14.4 kB
JavaScript
'use strict';
var yargsInteractive = require('yargs-interactive');
// @ts-ignore Untyped Module
/**
* A class that represents a command that can be run in the CLI.
* This class is a wrapper around yargs commands that allows for prompts and flags to be added to the command.
*
* @example
* ```typescript
* const command = new EasyCLICommand('do', (params, theme) => {
* theme?.getLogger().log(params);
* }, {
* description: 'Set a config variable',
* aliases: [],
* args: {
* key: {
* describe: 'What key(s) are you setting?',
* type: 'string',
* },
* },
* prompts: {
* value: {
* describe: 'the value to set',
* type: 'string',
* prompt: 'missing',
* demandOption: true,
* },
* },
* });
* ```
*/
class EasyCLICommand {
/**
* Creates a new EasyCLICommand instance.
*
* @param name The name of the command.
* @param handler The handler function that will be called when the command is run.
* @param options Optional arguments for setting up the command.
*/
constructor(name, handler, { description = '', aliases = [], flags = {}, prompts = {}, args = {}, promptGlobalKeys = [], skipConfig = false, validator = () => true, } = {}) {
this.globalFlags = {};
this.promptGlobalKeys = [];
this.name = name;
this.handler = handler;
this.description = description;
this.aliases = aliases;
this.flags = flags;
this.prompts = prompts;
this.args = args;
this.skipConfig = skipConfig;
this.promptGlobalKeys = promptGlobalKeys;
this.validator = validator;
}
/**
* @returns The names of the command and its aliases, this is used by EasyCLI to register the command with yargs and determine which command to run.
*
* @example
* ```typescript
* command.getNames(); // ['do', 'd']
* ```
*/
getNames() {
return [this.name, ...this.aliases];
}
/**
* @returns The name of the command, this is used by EasyCLI to register the command with yargs and determine which command to run.
*
* @example
* ```typescript
* command.getName(); // 'do'
* ```
*/
getName() {
return this.name;
}
/**
* @returns The keys for all command arguments, flags and prompts. This is used by EasyCLI to determine which keys this command uses.
*
* @example
* ```typescript
* command.getKeys(); // ['key', 'value', 'verbose']
* ```
*/
getKeys() {
return [
...Object.keys(this.flags),
...Object.keys(this.prompts),
...Object.keys(this.args),
];
}
/**
* @returns Whether the command should skip loading the configuration file. This is used by EasyCLI to determine if the command should load the configuration file.
*
* @example
* ```typescript
* command.skipConfigLoad(); // false
* ```
*/
skipConfigLoad() {
return this.skipConfig;
}
/**
* Adds a flag to the command.
*
* @param key The key of the flag to add.
* @param config Configuration for the flag.
*
* @returns This command instance for optional chaining
*
* @example
* ```typescript
* command.addFlag('verbose', {
* describe: 'The verbosity of the command',
* type: 'number',
* default: 0,
* });
* ```
*/
addFlag(key, config) {
this.flags[key] = config;
return this;
}
/**
* Adds a prompt to the command.
*
* @param key The key of the prompt to add.
* @param config Configuration for the prompt.
*s
* @returns This command instance for optional chaining
*
* @example
* ```typescript
* // Prompt the user for the environment to set
* command.addPrompt('env', {
* describe: 'The environment to set',
* type: 'string',
* prompt: 'always',
* demandOption: true,
* });
* ```
*/
addPrompt(key, config) {
this.prompts[key] = config;
return this;
}
/**
* Adds an argument (positional option) to the command.
*
* @param key The key of the argument to add.
* @param config Configuration for the argument.
*
* @returns This command instance for optional chaining
*
* @example
* ```typescript
* // Add an argument to the command
* command.addArgument('key', {
* describe: 'The key to set',
* type: 'string',
* demandOption: true,
* });
*
* // Builds a function similar to `app my-command [key]`
*
* // Add an argument to the command that is an array
* command.addArgument('keys', {
* describe: 'The keys to set',
* type: 'string',
* demandOption: true,
* array: true,
* });
*
* // Builds a function similar to `app my-command [key1] [key2] [key3] ...`
* ```
*/
addArgument(key, config) {
this.args[key] = config;
return this;
}
/**
* Sets the global flags for the command, these flags will be available to all commands. Used by EasyCLI to set the global flags for the CLI in order to prompt any that are set.
*
* @param flags The flags to set as global flags
*
* @example
* ```typescript
* command.setGlobalFlags({
* verbose: {
* describe: 'The verbosity of the command',
* type: 'number',
* default: 0,
* },
* });
*
*/
setGlobalFlags(flags) {
this.globalFlags = flags;
}
/**
* Prepare the flags for the command by setting the demandOption to false for flags that should be prompted.
*
* @returns A modified version of the flags object with the demandOption set to false for flags that should be prompted.
*/
prepareFlags() {
return Object.entries(this.flags).reduce((acc, [flag, { prompt = 'never', ...config }]) => {
var _a;
return {
...acc,
[flag]: {
...config,
demandOption:
// If the prompt is 'always' or when missing, we need to set this to false and we will prompt the user.
prompt === 'never' ? (_a = config === null || config === undefined ? undefined : config.demandOption) !== null && _a !== undefined ? _a : false : false,
},
};
}, {});
}
/**
* Prepare the args for the command by setting the demandOption to false for flags that should be prompted.
*
* @returns A modified version of the args object with the demandOption set to false for flags that should be prompted.
*/
prepareArgs() {
return Object.entries(this.args).reduce((acc, [flag, { prompt = 'never', ...config }]) => {
var _a;
return {
...acc,
[flag]: {
...config,
demandOption:
// If the prompt is 'always' or when missing, we need to set this to false and we will prompt the user.
prompt === 'never' ? (_a = config === null || config === undefined ? undefined : config.demandOption) !== null && _a !== undefined ? _a : false : false,
},
};
}, {});
}
/**
* Returns the default values for the command arguments and flags, this is used by EasyCLI to determine the default values for the command and whether the config file values should be used to override the defaults.
*
* @returns The default values for the command arguments and flags
*
* @example
* ```typescript
* command.getDefaultArgv(); // { key: undefined, value: undefined, verbose: 0 }
* ```
*/
getDefaultArgv() {
const args = Object.keys(this.args).reduce((acc, key) => {
var _a, _b;
acc[key] = (_b = (_a = this.args[key]) === null || _a === undefined ? undefined : _a.default) !== null && _b !== undefined ? _b : undefined;
return acc;
}, {});
const flags = Object.keys(this.flags).reduce((acc, key) => {
var _a, _b;
acc[key] = (_b = (_a = this.flags[key]) === null || _a === undefined ? undefined : _a.default) !== null && _b !== undefined ? _b : undefined;
return acc;
}, {});
return {
...args,
...flags,
};
}
/**
* Prepares the prompts for the command by merging the prompts, flags and arguments and filtering out the values that should not be prompted.
*
* @param argv The arguments passed to the command
*
* @returns A list of prompts for the command to run.
*/
preparePrompts(argv) {
const convertYargsTypeToInteractiveTypes = (type, choices, array) => {
if (choices && !array) {
return 'list';
}
if (choices && array) {
return 'checkbox';
}
switch (type) {
case 'string':
return 'input';
case 'boolean':
return 'confirm';
case 'number':
return 'number';
default:
return 'input';
}
};
// Merge the prompts from the prompts, flags and arguments
const prompts = {
...this.prompts,
...Object.entries({ ...this.globalFlags, ...this.args, ...this.flags })
// Filter out the values that should not be prompted
.filter(([key, { prompt = 'never' }]) => {
if (prompt === 'always' || this.promptGlobalKeys.includes(key))
return true; // Always prompt
if (prompt === 'never')
return false; // Never prompt
// If the prompt is missing, we need to check if the argument is missing
return argv[key] === undefined;
})
.reduce((acc, [key, config]) => {
acc[key] = { ...config };
return acc;
}, {}),
};
return Object.entries(prompts).reduce((acc, [key, { prompt = 'never', ...config }]) => {
var _a, _b, _c;
acc[key] = {
...config,
type: convertYargsTypeToInteractiveTypes((_a = config === null || config === undefined ? undefined : config.type) !== null && _a !== undefined ? _a : 'string', config === null || config === undefined ? undefined : config.choices, config === null || config === undefined ? undefined : config.array),
name: key,
describe: (_b = config === null || config === undefined ? undefined : config.describe) !== null && _b !== undefined ? _b : config.description,
prompt: 'always',
demandOption: false,
default: (_c = argv[key]) !== null && _c !== undefined ? _c : config.default,
};
return acc;
}, {});
}
/**
* Runs the prompts for the command, including flags and arguments that need to be prompted and displays the prompts to the user.
*
* @returns The values for the command from the user after the prompts have been run
*/
async prompt(args) {
const prompts = this.preparePrompts(args);
const { interactive, ...values } = await yargsInteractive().interactive({
interactive: { default: true },
...prompts,
});
return values;
}
/**
* Converts the command to a yargs command. This is used by EasyCLI to register the command with yargs.
* This can also be used to directly register the command with yargs.
*
* @param theme The theme to use for formatting strings.
*
* @returns The yargs command.
*
* @example
* ```typescript
*
* const command = new EasyCLICommand('do', (params, theme) => {
* theme?.getLogger().log(params); // Log the values of the command
* }, { description: 'Do something' });
*
* // Register the command with EasyCLI to leverage other options
* const easyCLI = new EasyCLI();
* easyCLI.addCommand(command);
*
* // Register the command with yargs directly if you don't need the other helpers.
* yargs.command(command.convertToYargsCommand());
* ```
*/
convertToYargsCommand(isDefault = false, theme) {
const flags = this.prepareFlags();
const args = this.prepareArgs();
// Merge the flags and arguments into the command string
const positionals = Object.entries(args)
.map(([key, opts]) => {
const arrayKey = opts.array ? `${key}...` : key;
return opts.demandOption ? `<${arrayKey}>` : `[${arrayKey}]`;
})
.join(' ');
const command = `${this.name} ${positionals}`.trim();
return {
command: isDefault ? ['$0', command] : command,
aliases: this.aliases,
describe: this.description,
builder: yargs => {
yargs.options(flags);
for (const [key, opt] of Object.entries(args)) {
yargs.positional(key, opt);
}
return yargs;
},
handler: async (argv) => {
await this.run(argv, theme);
},
};
}
/**
* Runs the command with the provided arguments.
*
* @param params The parameters to run the command with.
* @param theme The theme to use for formatting strings.
*
* @returns The result of the command handler.
*
* @example
* ```typescript
* command.run({ key: 'value' }, theme);
* ```
*/
async run(params, theme) {
if (!(await this.validator(params))) {
return;
}
const promptParams = await this.prompt(params);
const args = {
...params,
...promptParams,
};
if (!theme) {
return this.handler(args);
}
return this.handler(args, theme);
}
}
exports.EasyCLICommand = EasyCLICommand;