UNPKG

@liplum/cli

Version:

A helpful wrapper around command-line-args and command-line-usage

298 lines 10.3 kB
import commandLineArgs from 'command-line-args'; import commandLineUsage from 'command-line-usage'; import removeMarkdown from 'remove-markdown'; import didYouMean from "didyoumean"; import chalk from 'chalk'; const help = { name: 'help', alias: 'h', description: 'Display the help output', type: Boolean, group: 'global' }; /** Options available to all CLIs created with this tool */ export const globalOptions = [help]; const arrayify = (x) => (Array.isArray(x) ? x : [x]); const hasGlobal = (options) => Boolean(options.find(option => option.group === 'global')); function styleTypes(command, option) { const isRequired = command.require && command.require.includes(option.name); if (isRequired && option.type === Number) { option.typeLabel = `{rgb(173, 216, 230) {underline ${option.typeLabel || 'number'}}} [{rgb(254,91,92) required}]`; } else if (option.type === Number) { option.typeLabel = `{rgb(173, 216, 230) {underline ${option.typeLabel || 'number'}}}`; } if (isRequired && option.type === String) { option.typeLabel = `{rgb(173, 216, 230) {underline ${option.typeLabel || 'string'}}} [{rgb(254,91,92) required}]`; } else if (option.multiple && option.type === String) { option.typeLabel = `{rgb(173, 216, 230) {underline ${option.typeLabel || 'string[]'}}}`; } else if (option.type === String) { option.typeLabel = `{rgb(173, 216, 230) {underline ${option.typeLabel || 'string'}}}`; } } function addFooter(command, sections) { if (typeof command.footer === 'string') { sections.push({ content: command.footer }); } else if (command.footer) { const footers = Array.isArray(command.footer) ? command.footer : [command.footer]; footers.forEach(f => { let content = !('content' in f) ? undefined : typeof f.content === 'string' ? removeMarkdown(f.code ? f.content .split('\n') .map(s => (s.includes('```') ? undefined : `| ${s}`)) .join('\n') : f.content, { stripListLeaders: false }) : Array.isArray(f.content) ? f.content : undefined; if (typeof content === 'string') { content = content.replace(/}/g, '\\}').replace(/{/g, '\\{'); } sections.push({ ...f, header: f.header ? removeMarkdown(f.header, { stripListLeaders: false }) : undefined, content }); }); } } const printUsage = (command) => { const options = command.options || []; const sections = [ { header: command.name, content: command.description } ]; options.forEach(option => { styleTypes(command, option); }); if (hasGlobal(options)) { sections.push({ header: 'Options', optionList: options.filter(o => o.group !== 'global') }, { header: 'Global Options', optionList: [...options, ...globalOptions], group: 'global' }); } else { sections.push({ header: 'Options', optionList: [...options, ...globalOptions] }); } if (command.examples) { sections.push({ header: 'Examples', content: command.examples }); } addFooter(command, sections); console.log(commandLineUsage(sections)); return; }; const printRootUsage = (multi) => { const subCommands = multi.commands.filter((c) => !('isMulti' in c)) || []; const rootOptions = multi.options || []; const options = [...rootOptions, ...globalOptions]; const sections = []; if (multi.logo) { sections.push({ content: multi.logo, raw: true }); } sections.push({ header: multi.name, content: multi.description }); sections.push({ header: 'Synopsis', content: `$ ${multi.name} <command> <options>` }); const groups = subCommands.reduce((all, command) => { if (command.group) { all.add(command.group); } return all; }, new Set()); groups.forEach(header => { const grouped = subCommands.filter(c => c.group === header) || []; if (grouped.length > 0) { sections.push({ header, content: grouped.map(command => ({ name: command.name, description: command.description })) }); } }); if (groups.size === 0) { sections.push({ header: 'Commands', content: subCommands.map(command => ({ name: command.name, description: command.description })) }); } options.forEach(option => { styleTypes(multi, option); }); sections.push({ header: 'Global Options', optionList: options, group: ['_none', 'global'] }); addFooter(multi, sections); console.log(commandLineUsage(sections)); }; const errorReportingStyles = ['exit', 'throw', 'object']; const reportError = (error, style) => { if (style === 'exit') { console.log(error); process.exit(1); } if (style === 'throw') { throw new Error(error); } if (style === 'object') { return { error }; } return; }; const createList = (list, transform) => { const [first, ...rest] = list.map(transform); return rest.length > 0 ? `${rest.join(', ')} or ${first}` : first; }; const reportUnknownFlags = (args, [unknown], errorStyle) => { const unknownStyled = chalk.redBright(`"${unknown}"`); const type = unknown.startsWith('-') ? 'flag' : 'command'; let suggestions = didYouMean(unknown, args.map(a => (type === 'flag' ? `--${a.name}` : a.name))); let error; suggestions = Array.isArray(suggestions) ? suggestions : [suggestions]; if (suggestions.length) { const list = createList(suggestions, s => chalk.greenBright(`"${s}"`)); error = `Found unknown ${type} ${unknownStyled}, did you mean ${list}?`; } else { error = `Found unknown ${type}: ${unknownStyled}`; } return reportError(error, errorStyle); }; const initializeOptions = (options = []) => { const args = [...options]; globalOptions.forEach(o => { if (!args.find(a => a.name === o.name)) { args.push(o); } }); return args; }; const parseCommand = (command, { argv, showHelp, error = 'exit', camelCase = true }) => { const args = initializeOptions(command.options); const { global, ...rest } = commandLineArgs(args, { stopAtFirstUnknown: true, camelCase, argv }); if (rest._unknown) { printUsage(command); return reportUnknownFlags(args, rest._unknown, error); } if (global.help && showHelp) { printUsage(command); return; } const formatArrayOption = (option) => typeof option === 'string' ? `--${option}` : option.map(formatArrayOption).join(', '); if (command.require) { const missing = command.require .filter(option => (typeof option === 'string' && !(option in rest._all)) || (typeof option === 'object' && !option.find(o => (typeof o === 'string' && o in rest._all) || (typeof o === 'object' && !o.find(op => !(op in rest._all))))) || // tslint:disable-next-line strict-type-predicates (typeof option === 'string' && rest._all[option] === null)) .map(option => typeof option === 'string' ? `--${option}` : `(${option.map(formatArrayOption).join(' or ')})`); if (missing.length > 0) { const multiple = missing.length > 1; printUsage(command); return reportError(`Missing required arg${multiple ? 's' : ''}: ${missing.join(', ')}`, error); } } return { ...rest, ...rest._all, ...global }; }; /** * Create a command line application with all the bells and whistles * @param command the command to create an application for * @param options Advanced options for the application */ export function cli(command, { showHelp = true, argv, error = 'exit', camelCase = true } = {}) { const appOptions = { showHelp, argv, error, camelCase }; if (!('commands' in command)) { return parseCommand(command, appOptions); } const rootOptions = initializeOptions(command.options); const { global, _unknown, _all } = commandLineArgs(rootOptions, { stopAtFirstUnknown: true, camelCase, argv }); if (global.help && showHelp) { printRootUsage(command); return; } if (_unknown && _unknown.length > 0) { const subCommand = command.commands.find((c) => Boolean(c.name === _unknown[0])); if (subCommand) { const options = [ ...(subCommand.options || []), ...(command.options || []) ]; const parsed = cli({ ...subCommand, options }, { ...appOptions, argv: _unknown.slice(1) }); if (!parsed) { return; } return { ...parsed, _command: '_command' in parsed ? [subCommand.name, ...arrayify(parsed._command)] : subCommand.name }; } printRootUsage(command); return reportUnknownFlags([...rootOptions, ...command.commands], _unknown, error); } if (Object.keys(_all).length > 0) { return _all; } if (showHelp) { printRootUsage(command); } return reportError(`No sub-command provided to MultiCommand "${command.name}"`, error); } //# sourceMappingURL=main.js.map