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.
289 lines (285 loc) • 9.96 kB
JavaScript
'use strict';
var yargs = require('yargs');
/**
* @packageDocumentation A framework for building CLI applications that are robust and easy to maintain. Supports theming, configuration files, interactive prompts, and more.
* @module easy-cli
*/
/**
* @class EasyCLI
* The primary class for composing and running an EasyCLI application.
* This class is responsible for managing the commands, global flags, and themes for the CLI.
* It also handles the parsing of the arguments and executing the commands.
*
* @template TGlobalParams The global params for the CLI
*
* @example
* ```typescript
* const cli = new EasyCLI({
* executionName: 'my-cli',
* });
*
* const command = new EasyCLICommand(...);
* cli.addCommand(command);
*
* cli.execute();
* ```
*/
class EasyCLI {
/**
* Creates a new EasyCLI instance
*
* @param {EasyCLIConfig} [config={}] The configuration for the CLI
*/
constructor(config = {}) {
var _a, _b, _c, _d;
this.executionName = '';
this.defaultCommand = 'help';
this.commands = [];
this.globalFlags = {};
this.verboseFlag = null;
this.configFlag = null;
this.configFile = null;
this.executionName = (_a = config === null || config === undefined ? undefined : config.executionName) !== null && _a !== undefined ? _a : '';
this.commands = (_b = config === null || config === undefined ? undefined : config.commands) !== null && _b !== undefined ? _b : [];
this.defaultCommand = (_c = config === null || config === undefined ? undefined : config.defaultCommand) !== null && _c !== undefined ? _c : 'help';
this.globalFlags =
(_d = config === null || config === undefined ? undefined : config.globalFlags) !== null && _d !== undefined ? _d : {};
}
/**
* Set the theme for the CLI, will overwrite any existing theme, and this theme will be passed to all commands unless overridden.
*
* @param {EasyCLITheme} theme The theme to use
*
* @returns {EasyCLI} The EasyCLI instance
*
* @example
* ```typescript
* const theme = new EasyCLITheme();
*
* const cli = new EasyCLI();
* cli.setTheme(theme);
* ```
*/
setTheme(theme) {
this.theme = theme;
return this;
}
/**
* Set the configuration file for the CLI
*
* @param {EasyCLIConfigFile} config The configuration file to use
*
* @returns {EasyCLI} The EasyCLI instance
*
* @example
* ```typescript
* const configFile = new EasyCLIConfigFile({
* ...
* });
*
* const cli = new EasyCLI();
* cli.setConfigFile(configFile);
* ```
*/
setConfigFile(config) {
this.configFile = config;
return this;
}
/**
* Dangerously sets all the commands for the CLI, overwriting any existing commands.
*
* @param {EasyCLICommand[]} commands The commands to add to the CLI
*
* @returns {EasyCLI} The EasyCLI instance
*
* @example
* ```typescript
* const command = new EasyCLICommand(...);
* const cli = new EasyCLI();
* cli.setCommands([command]);
* ```
*/
setCommands(commands) {
this.commands = commands;
return this;
}
/**
* Adds a command to the CLI
*
* @template TParams The params that this command accepts.
*
* @param {EasyCLICommand} command The command to add
*
* @returns {EasyCLI} The EasyCLI instance
*
* @example
* ```typescript
* const command = new EasyCLICommand(...);
* const cli = new EasyCLI();
* cli.addCommand(command);
* ```
*/
addCommand(command) {
this.commands.push(command);
return this;
}
/**
* Manage the verbose flag for the CLI
*
* @param {number} [defaultVerbosity=0] The default verbosity level
* @param {Partial<CommandOption & { name: string }>} [overrides={}] Any overrides for the verbose flag
*
* @returns {EasyCLI} The EasyCLI instance
*
* @example
* ```typescript
* const cli = new EasyCLI();
* cli.handleVerboseFlag(0, { ... });
* ```
*/
handleVerboseFlag(defaultVerbosity = 0, overrides = {}) {
var _a, _b;
this.verboseFlag = (_a = overrides === null || overrides === undefined ? undefined : overrides.name) !== null && _a !== undefined ? _a : 'verbose';
this.globalFlags = {
...this.globalFlags,
[(_b = overrides === null || overrides === undefined ? undefined : overrides.name) !== null && _b !== undefined ? _b : 'verbose']: {
alias: 'v',
description: 'Set the verbosity level',
type: 'count',
default: defaultVerbosity,
...overrides,
},
};
return this;
}
/**
* Manage the configuration file flag for the CLI
*
* @param {Partial<CommandOption & { name: string }>} [overrides={}] Any overrides for the configuration file flag
*
* @returns {EasyCLI} The EasyCLI instance
*
* @example
* ```typescript
* const cli = new EasyCLI();
*
* // This will add a `--config` flag to the CLI
* cli.handleConfigFileFlag();
*
* // This will add a `--my-config` flag to the CLI
* cli.handleConfigFileFlag({ name: 'my-config' });
* ```
*
*/
handleConfigFileFlag(overrides = {}) {
var _a, _b;
if (!this.configFile)
throw new Error('No configuration file provided');
this.configFlag = (_a = overrides === null || overrides === undefined ? undefined : overrides.name) !== null && _a !== undefined ? _a : 'config';
this.globalFlags = {
...this.globalFlags,
[(_b = overrides === null || overrides === undefined ? undefined : overrides.name) !== null && _b !== undefined ? _b : 'config']: {
alias: 'c',
description: 'Specify a configuration file',
type: 'string',
...overrides,
},
};
return this;
}
/**
* An internal method to get the default values for the global flags
*
* @returns The default values for the global flags
*/
getDefaults() {
return Object.keys(this.globalFlags).reduce((acc, key) => {
if (this.globalFlags[key].default !== undefined) {
acc[key] = this.globalFlags[key].default;
}
return acc;
}, {});
}
/**
* @returns A middleware function to load the configuration file
*/
configMiddleware() {
return (argv) => {
var _a;
if (!this.configFile)
return argv;
// TODO: Extract the default values from the command args as well
const command = this.commands.find(command => command.getNames().includes(argv['_'][0]));
if (command === null || command === undefined ? undefined : command.skipConfigLoad())
return argv;
const configPath = (_a = argv === null || argv === undefined ? undefined : argv[this === null || this === undefined ? undefined : this.configFlag]) !== null && _a !== undefined ? _a : null;
const config = this.configFile.load(configPath, argv);
const defaults = this.getDefaults();
if (command) {
const commandDefaults = command.getDefaultArgv();
Object.assign(defaults, commandDefaults);
}
const argvRemovingDefaults = Object.keys(argv).reduce((acc, key) => {
if (defaults[key] === argv[key])
return acc;
acc[key] = argv[key];
return acc;
}, {});
return {
...defaults,
...config,
...argvRemovingDefaults, // Ensure that the arguments passed in take precedence
};
};
}
/**
* @returns A middleware function to set the verbosity level
*/
verboseMiddleware() {
return (argv) => {
var _a;
if (this.verboseFlag && argv[this.verboseFlag])
(_a = this.theme) === null || _a === undefined ? undefined : _a.setVerbosity(argv[this.verboseFlag]);
};
}
/**
* Run the CLI with the provided arguments.
*
* @param {((app: typeof yargs) => typeof yargs) | null} [callback=null] A callback to add additional configuration to the CLI via yargs
*
* @returns {Promise<void>} A promise that resolves when the CLI has finished executing
*
* @example
* ```typescript
* const cli = new EasyCLI(...);
* cli.execute();
* ```
*/
async execute(callback = null) {
const app = yargs;
app.scriptName(this.executionName);
// Add the global flags
Object.entries(this.globalFlags).forEach(([name, config]) => {
app.option(name, config);
});
// Add the commands
this.commands.forEach(command => {
command.setGlobalFlags(this.globalFlags);
app.command(command.convertToYargsCommand(command.getName() === this.defaultCommand, // If this is the default command it needs to add $0 to the command
this === null || this === undefined ? undefined : this.theme // Pass the theme to the command
));
});
app.help().wrap(72);
const middleware = [];
middleware.push(this.configMiddleware());
middleware.push(this.verboseMiddleware());
app.middleware(middleware, true);
// To add any additional configuration
if (callback) {
callback(app);
}
// Parse the arguments
app.parse();
}
}
exports.EasyCLI = EasyCLI;