UNPKG

@thi.ng/args

Version:

Declarative, functional CLI argument/options parser, app framework, arg value coercions, multi/sub-commands, usage generation, error handling etc.

129 lines (128 loc) 3.62 kB
import { isArray } from "@thi.ng/checks/is-array"; import { illegalArgs } from "@thi.ng/errors/illegal-arguments"; import { LogLevel } from "@thi.ng/logger/api"; import { StreamLogger } from "@thi.ng/logger/stream"; import { PRESET_ANSI16, PRESET_NONE } from "@thi.ng/text-format/presets"; import { execFileSync } from "node:child_process"; import { parse } from "./parse.js"; import { usage } from "./usage.js"; import { __ansi, __colorTheme, __padRightAnsi, __wrapWithIndent } from "./utils.js"; const cliApp = async (config) => { const argv = config.argv || process.argv; const isColor = !process.env.NO_COLOR; const theme = __colorTheme(isColor); const usageOpts = { prefix: "", color: isColor, ...config.usage }; try { let cmdID; let cmd; let start = config.start ?? 2; if (config.single) { cmdID = Object.keys(config.commands)[0]; if (!cmdID) illegalArgs("no command provided"); cmd = config.commands[cmdID]; } else { cmdID = argv[start]; cmd = config.commands[cmdID]; if (!cmd) { usageOpts.prefix += __descriptions(config.commands, usageOpts); __usageAndExit(config, usageOpts); } else { usageOpts.prefix += __descriptions( { [cmdID]: cmd }, usageOpts, "\nCurrent command:\n" ); } start++; } let parsed; try { parsed = parse({ ...config.opts, ...cmd.opts }, argv, { usageOpts, start }); } catch (e) { process.exit(1); } if (!parsed) process.exit(0); if (cmd.inputs !== void 0) { const num = parsed.rest.length; let err; if (isArray(cmd.inputs)) { const [min, max] = cmd.inputs; if (num < min || num > max) { err = max < Infinity ? `expected ${min}-${max} inputs` : `expected at least ${min} input(s)`; } } else if (num !== cmd.inputs) { err = `expected ${cmd.inputs} input(s)`; } if (err) { __printError(err, theme); __usageAndExit(config, usageOpts); } } const ctx = await config.ctx( { logger: new StreamLogger(process.stderr, config.name, "INFO"), format: isColor ? PRESET_ANSI16 : PRESET_NONE, opts: parsed.result, inputs: parsed.rest }, cmd ); await cmd.fn(ctx); if (config.post) await config.post(ctx, cmd); } catch (e) { __printError(e.message, theme); process.exit(1); } }; const __usageAndExit = (config, usageOpts) => { process.stderr.write(usage(config.opts, usageOpts)); process.exit(1); }; const __descriptions = (commands, { color, lineWidth = 80 } = {}, prefix = "\nAvailable commands:\n") => { const names = Object.keys(commands); const maxLength = Math.max(...names.map((x) => x.length)); const theme = __colorTheme(color); return [ prefix, ...names.map( (x) => `${__padRightAnsi( __ansi(x, theme.command), maxLength )} : ${__wrapWithIndent( commands[x].desc, maxLength + 3, lineWidth )}` ), "\n" ].join("\n"); }; const __printError = (msg, theme) => process.stderr.write(__ansi(msg, theme.error) + "\n\n"); const terminalLineWidth = (fallback = 80) => { try { return +execFileSync("tput", ["cols"], { encoding: "ascii" }); } catch (e) { return fallback; } }; const configureLogLevel = (logger, verbose, quiet = false) => { if (quiet) logger.level = LogLevel.NONE; else if (verbose) logger.level = LogLevel.DEBUG; }; export { cliApp, configureLogLevel, terminalLineWidth };