UNPKG

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.

356 lines (323 loc) 10.2 kB
/** * @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 */ import yargs from 'yargs'; import { CommandOption, CommandOptionObject, EasyCLICommand, } from './commands/command'; import { EasyCLITheme } from './themes'; import { EasyCLIConfigFile } from './config-files'; /** * @interface EasyCLIConfig * The configuration for the EasyCLI * * @template TGlobalParams An object representing the global params for the CLI * * @property {string} [executionName] The name to display in the help menu and error messages for the CLI * @property {string} [defaultCommand] The default command to run if no command is provided (defaults to 'help') * @property {EasyCLICommand[]} [commands] The commands to add to the CLI * @property {CommandOptionObject} [globalFlags] The global flags to add to the CLI */ export type EasyCLIConfig< TGlobalParams extends Record<string, any> = Record<string, any> > = { executionName?: string; // The name to display in the help menu and error messages for the CLI defaultCommand?: string; // The default command to run if no command is provided (defaults to 'help') commands?: EasyCLICommand<any, TGlobalParams>[]; globalFlags?: CommandOptionObject<{}, TGlobalParams>; }; /** * @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(); * ``` */ export class EasyCLI< TGlobalParams extends Record<string, any> = Record<string, any> > { private executionName: string = ''; private defaultCommand: string = 'help'; private commands: EasyCLICommand<any, TGlobalParams>[] = []; private theme?: EasyCLITheme; private globalFlags: CommandOptionObject<{}, TGlobalParams> = {} as CommandOptionObject<{}, TGlobalParams>; private verboseFlag: string | null = null; private configFlag: string | null = null; private configFile: EasyCLIConfigFile | null = null; /** * Creates a new EasyCLI instance * * @param {EasyCLIConfig} [config={}] The configuration for the CLI */ constructor(config: EasyCLIConfig<TGlobalParams> = {}) { this.executionName = config?.executionName ?? ''; this.commands = config?.commands ?? []; this.defaultCommand = config?.defaultCommand ?? 'help'; this.globalFlags = config?.globalFlags ?? ({} as CommandOptionObject<{}, TGlobalParams>); } /** * 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); * ``` */ public setTheme(theme: EasyCLITheme): EasyCLI<TGlobalParams> { 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); * ``` */ public setConfigFile(config: EasyCLIConfigFile): EasyCLI<TGlobalParams> { 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]); * ``` */ public setCommands( commands: EasyCLICommand<{}, TGlobalParams>[] ): EasyCLI<TGlobalParams> { 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); * ``` */ public addCommand<TParams extends Record<string, any> = Record<string, any>>( command: EasyCLICommand<TParams, TGlobalParams> ): EasyCLI<TGlobalParams> { 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, { ... }); * ``` */ public handleVerboseFlag( defaultVerbosity = 0, overrides = {} as Partial<CommandOption & { name: string }> ): EasyCLI<TGlobalParams> { this.verboseFlag = overrides?.name ?? 'verbose'; this.globalFlags = { ...this.globalFlags, [overrides?.name ?? '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' }); * ``` * */ public handleConfigFileFlag( overrides = {} as Partial<CommandOption & { name: string }> ): EasyCLI<TGlobalParams> { if (!this.configFile) throw new Error('No configuration file provided'); this.configFlag = overrides?.name ?? 'config'; this.globalFlags = { ...this.globalFlags, [overrides?.name ?? '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 */ private getDefaults(): Partial<TGlobalParams> { return Object.keys(this.globalFlags).reduce( (acc: TGlobalParams, key: string) => { if (this.globalFlags[key].default !== undefined) { (acc as any)[key] = this.globalFlags[key].default; } return acc; }, {} as TGlobalParams ); } /** * @returns A middleware function to load the configuration file */ private configMiddleware() { return (argv: any) => { 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?.skipConfigLoad()) return argv; const configPath = argv?.[this?.configFlag as string] ?? null; const config = this.configFile.load(configPath, argv); const defaults: any = 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; }, {} as any); return { ...defaults, ...config, ...argvRemovingDefaults, // Ensure that the arguments passed in take precedence }; }; } /** * @returns A middleware function to set the verbosity level */ private verboseMiddleware() { return (argv: any) => { if (this.verboseFlag && argv[this.verboseFlag]) this.theme?.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(); * ``` */ public async execute( callback: ((app: typeof yargs) => typeof yargs) | null = null ): Promise<void> { 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?.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(); } }