@brianmd/citty
Version:
Elegant CLI Builder
481 lines (469 loc) • 14.9 kB
JavaScript
const consola = require('consola');
const utils = require('consola/utils');
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
const consola__default = /*#__PURE__*/_interopDefaultCompat(consola);
function toArray(val) {
if (Array.isArray(val)) {
return val;
}
return val === void 0 ? [] : [val];
}
function formatLineColumns(lines, linePrefix = "") {
const maxLengh = [];
for (const line of lines) {
for (const [i, element] of line.entries()) {
maxLengh[i] = Math.max(maxLengh[i] || 0, element.length);
}
}
return lines.map(
(l) => l.map(
(c, i) => linePrefix + c[i === 0 ? "padStart" : "padEnd"](maxLengh[i])
).join(" ")
).join("\n");
}
function resolveValue(input) {
return typeof input === "function" ? input() : input;
}
class CLIError extends Error {
constructor(message, code) {
super(message);
this.code = code;
this.name = "CLIError";
}
}
const NUMBER_CHAR_RE = /\d/;
const STR_SPLITTERS = ["-", "_", "/", "."];
function isUppercase(char = "") {
if (NUMBER_CHAR_RE.test(char)) {
return void 0;
}
return char !== char.toLowerCase();
}
function splitByCase(str, separators) {
const splitters = separators ?? STR_SPLITTERS;
const parts = [];
if (!str || typeof str !== "string") {
return parts;
}
let buff = "";
let previousUpper;
let previousSplitter;
for (const char of str) {
const isSplitter = splitters.includes(char);
if (isSplitter === true) {
parts.push(buff);
buff = "";
previousUpper = void 0;
continue;
}
const isUpper = isUppercase(char);
if (previousSplitter === false) {
if (previousUpper === false && isUpper === true) {
parts.push(buff);
buff = char;
previousUpper = isUpper;
continue;
}
if (previousUpper === true && isUpper === false && buff.length > 1) {
const lastChar = buff.at(-1);
parts.push(buff.slice(0, Math.max(0, buff.length - 1)));
buff = lastChar + char;
previousUpper = isUpper;
continue;
}
}
buff += char;
previousUpper = isUpper;
previousSplitter = isSplitter;
}
parts.push(buff);
return parts;
}
function upperFirst(str) {
return str ? str[0].toUpperCase() + str.slice(1) : "";
}
function lowerFirst(str) {
return str ? str[0].toLowerCase() + str.slice(1) : "";
}
function pascalCase(str, opts) {
return str ? (Array.isArray(str) ? str : splitByCase(str)).map((p) => upperFirst(opts?.normalize ? p.toLowerCase() : p)).join("") : "";
}
function camelCase(str, opts) {
return lowerFirst(pascalCase(str || "", opts));
}
function kebabCase(str, joiner) {
return str ? (Array.isArray(str) ? str : splitByCase(str)).map((p) => p.toLowerCase()).join(joiner ?? "-") : "";
}
function toArr(any) {
return any == void 0 ? [] : Array.isArray(any) ? any : [any];
}
function toVal(out, key, val, opts) {
let x;
const old = out[key];
const nxt = ~opts.string.indexOf(key) ? val == void 0 || val === true ? "" : String(val) : typeof val === "boolean" ? val : ~opts.boolean.indexOf(key) ? val === "false" ? false : val === "true" || (out._.push((x = +val, x * 0 === 0) ? x : val), !!val) : (x = +val, x * 0 === 0) ? x : val;
out[key] = old == void 0 ? nxt : Array.isArray(old) ? old.concat(nxt) : [old, nxt];
}
function parseRawArgs(args = [], opts = {}) {
let k;
let arr;
let arg;
let name;
let val;
const out = { _: [] };
let i = 0;
let j = 0;
let idx = 0;
const len = args.length;
const alibi = opts.alias !== void 0;
const strict = opts.unknown !== void 0;
const defaults = opts.default !== void 0;
opts.alias = opts.alias || {};
opts.string = toArr(opts.string);
opts.boolean = toArr(opts.boolean);
if (alibi) {
for (k in opts.alias) {
arr = opts.alias[k] = toArr(opts.alias[k]);
for (i = 0; i < arr.length; i++) {
(opts.alias[arr[i]] = arr.concat(k)).splice(i, 1);
}
}
}
for (i = opts.boolean.length; i-- > 0; ) {
arr = opts.alias[opts.boolean[i]] || [];
for (j = arr.length; j-- > 0; ) {
opts.boolean.push(arr[j]);
}
}
for (i = opts.string.length; i-- > 0; ) {
arr = opts.alias[opts.string[i]] || [];
for (j = arr.length; j-- > 0; ) {
opts.string.push(arr[j]);
}
}
if (defaults) {
for (k in opts.default) {
name = typeof opts.default[k];
arr = opts.alias[k] = opts.alias[k] || [];
if (opts[name] !== void 0) {
opts[name].push(k);
for (i = 0; i < arr.length; i++) {
opts[name].push(arr[i]);
}
}
}
}
const keys = strict ? Object.keys(opts.alias) : [];
for (i = 0; i < len; i++) {
arg = args[i];
if (arg === "--") {
out._ = out._.concat(args.slice(++i));
break;
}
for (j = 0; j < arg.length; j++) {
if (arg.charCodeAt(j) !== 45) {
break;
}
}
if (j === 0) {
out._.push(arg);
} else if (arg.substring(j, j + 3) === "no-") {
name = arg.slice(Math.max(0, j + 3));
if (strict && !~keys.indexOf(name)) {
return opts.unknown(arg);
}
out[name] = false;
} else {
for (idx = j + 1; idx < arg.length; idx++) {
if (arg.charCodeAt(idx) === 61) {
break;
}
}
name = arg.substring(j, idx);
val = arg.slice(Math.max(0, ++idx)) || i + 1 === len || ("" + args[i + 1]).charCodeAt(0) === 45 || args[++i];
arr = j === 2 ? [name] : name;
for (idx = 0; idx < arr.length; idx++) {
name = arr[idx];
if (strict && !~keys.indexOf(name)) {
return opts.unknown("-".repeat(j) + name);
}
toVal(out, name, idx + 1 < arr.length || val, opts);
}
}
}
if (defaults) {
for (k in opts.default) {
if (out[k] === void 0) {
out[k] = opts.default[k];
}
}
}
if (alibi) {
for (k in out) {
arr = opts.alias[k] || [];
while (arr.length > 0) {
out[arr.shift()] = out[k];
}
}
}
return out;
}
function parseArgs(rawArgs, argsDef) {
const parseOptions = {
boolean: [],
string: [],
mixed: [],
alias: {},
default: {}
};
const args = resolveArgs(argsDef);
for (const arg of args) {
if (arg.type === "positional") {
continue;
}
if (arg.type === "string") {
parseOptions.string.push(arg.name);
} else if (arg.type === "boolean") {
parseOptions.boolean.push(arg.name);
}
if (arg.default !== void 0) {
parseOptions.default[arg.name] = arg.default;
}
if (arg.alias) {
parseOptions.alias[arg.name] = arg.alias;
}
}
const parsed = parseRawArgs(rawArgs, parseOptions);
const [...positionalArguments] = parsed._;
const parsedArgsProxy = new Proxy(parsed, {
get(target, prop) {
return target[prop] ?? target[camelCase(prop)] ?? target[kebabCase(prop)];
}
});
for (const [, arg] of args.entries()) {
if (arg.type === "positional") {
const nextPositionalArgument = positionalArguments.shift();
if (nextPositionalArgument !== void 0) {
parsedArgsProxy[arg.name] = nextPositionalArgument;
} else if (arg.default === void 0 && arg.required !== false) {
throw new CLIError(
`Missing required positional argument: ${arg.name.toUpperCase()}`,
"EARG"
);
} else {
parsedArgsProxy[arg.name] = arg.default;
}
} else if (arg.required && parsedArgsProxy[arg.name] === void 0) {
throw new CLIError(`Missing required argument: --${arg.name}`, "EARG");
}
}
return parsedArgsProxy;
}
function resolveArgs(argsDef) {
const args = [];
for (const [name, argDef] of Object.entries(argsDef || {})) {
args.push({
...argDef,
name,
alias: toArray(argDef.alias)
});
}
return args;
}
function defineCommand(def) {
return def;
}
async function runCommand(cmd, opts) {
const cmdArgs = await resolveValue(cmd.args || {});
const parsedArgs = parseArgs(opts.rawArgs, cmdArgs);
const context = {
rawArgs: opts.rawArgs,
args: parsedArgs,
data: opts.data,
cmd
};
if (typeof cmd.setup === "function") {
await cmd.setup(context);
}
let result;
try {
const subCommands = await resolveValue(cmd.subCommands);
if (subCommands && Object.keys(subCommands).length > 0) {
const subCommandArgIndex = opts.rawArgs.findIndex(
(arg) => !arg.startsWith("-")
);
const subCommandName = opts.rawArgs[subCommandArgIndex];
if (subCommandName) {
if (!subCommands[subCommandName]) {
throw new CLIError(
`Unknown command \`${subCommandName}\``,
"E_UNKNOWN_COMMAND"
);
}
const subCommand = await resolveValue(subCommands[subCommandName]);
if (subCommand) {
await runCommand(subCommand, {
rawArgs: opts.rawArgs.slice(subCommandArgIndex + 1)
});
}
} else if (!cmd.run) {
throw new CLIError(`No command specified.`, "E_NO_COMMAND");
}
}
if (typeof cmd.run === "function") {
result = await cmd.run(context);
}
} catch (error_) {
const error = error_ instanceof Error ? error_ : new Error(error_?.toString() ?? "Unknown Error", { cause: error_ });
if (typeof cmd.catch === "function") {
await cmd.catch(context, error);
} else {
throw error;
}
} finally {
if (typeof cmd.cleanup === "function") {
await cmd.cleanup(context);
}
}
return { result };
}
async function resolveSubCommand(cmd, rawArgs, parent) {
const subCommands = await resolveValue(cmd.subCommands);
if (subCommands && Object.keys(subCommands).length > 0) {
const subCommandArgIndex = rawArgs.findIndex((arg) => !arg.startsWith("-"));
const subCommandName = rawArgs[subCommandArgIndex];
const subCommand = await resolveValue(subCommands[subCommandName]);
if (subCommand) {
return resolveSubCommand(
subCommand,
rawArgs.slice(subCommandArgIndex + 1),
cmd
);
}
}
return [cmd, parent];
}
async function showUsage(cmd, parent) {
try {
consola__default.log(await renderUsage(cmd, parent) + "\n");
} catch (error) {
consola__default.error(error);
}
}
async function renderUsage(cmd, parent) {
const cmdMeta = await resolveValue(cmd.meta || {});
const cmdArgs = resolveArgs(await resolveValue(cmd.args || {}));
const parentMeta = await resolveValue(parent?.meta || {});
const commandName = `${parentMeta.name ? `${parentMeta.name} ` : ""}` + (cmdMeta.name || process.argv[1]);
const argLines = [];
const posLines = [];
const commandsLines = [];
const usageLine = [];
for (const arg of cmdArgs) {
if (arg.type === "positional") {
const name = arg.name.toUpperCase();
const isRequired = arg.required !== false && arg.default === void 0;
const usageHint = arg.default ? `="${arg.default}"` : "";
posLines.push(["`" + name + usageHint + "`", arg.description || ""]);
usageLine.push(isRequired ? `<${name}>` : `[${name}]`);
} else {
const isRequired = arg.required === true && arg.default === void 0;
const argStr = (arg.type === "boolean" && arg.default === true ? [
...(arg.alias || []).map((a) => `--no-${a}`),
`--no-${arg.name}`
].join(", ") : [...(arg.alias || []).map((a) => `-${a}`), `--${arg.name}`].join(
", "
)) + (arg.type === "string" && (arg.valueHint || arg.default) ? `=${arg.valueHint ? `<${arg.valueHint}>` : `"${arg.default || ""}"`}` : "");
argLines.push([
"`" + argStr + (isRequired ? " (required)" : "") + "`",
arg.description || ""
]);
if (isRequired) {
usageLine.push(argStr);
}
}
}
if (cmd.subCommands) {
const commandNames = [];
const subCommands = await resolveValue(cmd.subCommands);
for (const [name, sub] of Object.entries(subCommands)) {
const subCmd = await resolveValue(sub);
const meta = await resolveValue(subCmd?.meta);
commandsLines.push([`\`${name}\``, meta?.description || ""]);
commandNames.push(name);
}
usageLine.push(commandNames.join("|"));
}
const usageLines = [];
const version = cmdMeta.version || parentMeta.version;
usageLines.push(
utils.colors.gray(
`${cmdMeta.description} (${commandName + (version ? ` v${version}` : "")})`
),
""
);
const hasOptions = argLines.length > 0 || posLines.length > 0;
usageLines.push(
`${utils.colors.underline(utils.colors.bold("USAGE"))} \`${commandName}${hasOptions ? " [OPTIONS]" : ""} ${usageLine.join(" ")}\``,
""
);
if (posLines.length > 0) {
usageLines.push(utils.colors.underline(utils.colors.bold("ARGUMENTS")), "");
usageLines.push(formatLineColumns(posLines, " "));
usageLines.push("");
}
if (argLines.length > 0) {
usageLines.push(utils.colors.underline(utils.colors.bold("OPTIONS")), "");
usageLines.push(formatLineColumns(argLines, " "));
usageLines.push("");
}
if (commandsLines.length > 0) {
usageLines.push(utils.colors.underline(utils.colors.bold("COMMANDS")), "");
usageLines.push(formatLineColumns(commandsLines, " "));
usageLines.push(
"",
`Use \`${commandName} <command> --help\` for more information about a command.`
);
}
return usageLines.filter((l) => typeof l === "string").join("\n");
}
async function runMain(cmd, opts = {}) {
const rawArgs = opts.rawArgs || process.argv.slice(2);
const showUsage$1 = opts.showUsage || showUsage;
try {
if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
await showUsage$1(...await resolveSubCommand(cmd, rawArgs));
process.exit(0);
} else if (rawArgs.length === 1 && rawArgs[0] === "--version") {
const meta = typeof cmd.meta === "function" ? await cmd.meta() : await cmd.meta;
if (!meta?.version) {
throw new CLIError("No version specified", "E_NO_VERSION");
}
consola__default.log(meta.version);
} else {
await runCommand(cmd, { rawArgs });
}
} catch (error) {
const isCLIError = error instanceof CLIError;
if (!isCLIError) {
consola__default.error(error, "\n");
}
if (isCLIError) {
await showUsage$1(...await resolveSubCommand(cmd, rawArgs));
}
consola__default.error(error.message);
if (process.env.NODE_ENV !== "test") {
process.exit(1);
}
}
}
function createMain(cmd) {
return (opts = {}) => runMain(cmd, opts);
}
exports.createMain = createMain;
exports.defineCommand = defineCommand;
exports.parseArgs = parseArgs;
exports.renderUsage = renderUsage;
exports.runCommand = runCommand;
exports.runMain = runMain;
exports.showUsage = showUsage;
;