@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
JavaScript
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
};