UNPKG

cli-kit

Version:

Everything you need to create awesome command line interfaces

149 lines (127 loc) 4.64 kB
import debug from '../lib/debug.js'; import path from 'path'; import { renderFile } from '../render/template.js'; import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const { log } = debug('cli-kit:help'); const { highlight } = debug.styles; /** * Renders help for a specific context, and its parent contexts, to a string. This function is * passed into the selected command's `action()` as a property called `help()` so that a command * can render its own help output. * * @param {Context} ctx - The context to render help. * @param {Object} [opts] - Various options to pass into `generateHelp()`. * @returns {String} */ export async function renderHelp(ctx, opts = {}) { const file = ctx.get('helpTemplateFile', path.resolve(__dirname, '..', '..', 'templates', 'help.tpl')); log(`Rendering help template: ${highlight(file)}`); return renderFile(file, Object.assign({ style: opts?.styles || {}, header: null, footer: null }, await ctx.generateHelp(opts))).trim(); } /** * The built-in help command parameters. * * @type {Object} */ export default { /** * Indicates this command is the built-in cli-kit help command so that if the CLI instance this * command belongs to gets added to another CLI instance, we don't copy it over. * @type {Boolean} */ clikitHelp: true, /** * While this is a command, we don't want to show it since we already show the `--help` flag. * @type {Boolean} */ hidden: true, /** * Output the help as JSON. Neato. * @type {Object} */ options: { '--json': null }, /** * Executes the help command. * * @param {Object} params - Various parameters. * @param {Object} [params.argv] - The parsed options. * @param {Array.<Context>} params.contexts - The stack of contexts found during parsing. * @param {Error} [params.err] - An error object in the event an error occurred. * @param {Function} params.exitCode - A function that sets the exit code. * @param {Array.<String>} [params.parentContextNames] - An array of parent context names. * @param {String} [params.unknownCommand] - The name of the unknown command. * @param {Array.<Error>} [params.warnings] - A list of warnings (error objects). * @returns {Promise} */ async action(params = {}) { let { _ = [], argv = {}, console, contexts, err, exitCode, parentContextNames, styles, warnings } = params; exitCode(+!!err); // 0=success, 1=error const formatError = err => { if (typeof err === 'string') { return { message: err }; } const type = err && err.constructor && err.constructor.name || null; return err ? { code: err.code, message: !argv.json && type ? `${type}: ${err.message}` : err.message, meta: err.meta, stack: err.stack, type } : null; }; // skip the built-in help command and find the first context for (const ctx of contexts) { // we don't display help for the help command if (ctx.clikitHelp) { continue; } // generate the help object const help = await ctx.generateHelp({ err, parentContextNames, warnings }); // check if we should error if passed an invalid command if (!err && _.length && (ctx.get('errorIfUnknownCommand') || argv.help)) { const unknownCommand = _[0]; err = params.err = new Error(`Unknown command "${unknownCommand}"`); const { distance } = await import('fastest-levenshtein'); help.suggestions = help.commands.entries .map(cmd => ({ name: cmd.name, desc: cmd.desc, dist: distance(unknownCommand, cmd.name) })) .filter(s => s.dist <= 2) .sort((a, b) => { const r = a.dist - b.dist; return r !== 0 ? r : a.name.localeCompare(b.name); }); } help.error = formatError(err); help.warnings = Array.isArray(warnings) ? warnings.map(formatError) : null; const { _stdout, _stderr } = console; // print the help output if (argv.json) { _stdout.write(JSON.stringify(help, null, ' ')); _stdout.write('\n'); } else { const file = ctx.get('helpTemplateFile', path.resolve(__dirname, '..', '..', 'templates', 'help.tpl')); log(`Rendering help template: ${highlight(file)}`); const buf = renderFile(file, { style: styles, header: null, footer: null, ...help }).trim(); // determine the output stream (err ? _stderr : _stdout).write(buf); (err ? _stderr : _stdout).write('\n'); } // set the exit code exitCode(err ? 1 : ctx.get('helpExitCode')); // we only loop until we hit the first valid context... generateHelp() will recurse // parent contexts for us break; } } };