yaro/yaro-create-cli/src/index.js
// SPDX-License-Identifier: MPL-2.0
/* eslint-disable no-param-reassign */
import { buildOutput } from './utils.js';
const UNNAMED_COMMAND_PREFIX = '___UNNAMED_COMMAND-';
/**
* Utility function to be used when you want a cli app
* that includes both `config.rootCommand` and and have multiple in `config.commands`
*/
export function rootWithMultipleCommands(globalOptions) {
// Note: 1)
// this root command can stay empty;
// or to merge global and command options, cuz they are not propagated by default;
// or if you want to know what command is matched and called;
// console.log('hela root command:', globalOptions);
// Note: 2)
// the reason we should do this, in case we have both `rootCommand` and `commands` defined,
// is because we do not have access to the correct parsed `argv`, it's not full,
// and we cannot do it internally either.
return async ({ argv, matchedCommand }) =>
matchedCommand({ ...globalOptions, ...argv });
}
export { yaroCreateCli, UNNAMED_COMMAND_PREFIX };
export default yaroCreateCli;
async function yaroCreateCli(argv, config) {
const cfg = Array.isArray(argv) ? { argv, ...config } : { argv: [], ...argv };
if (cfg.buildOutput && typeof cfg.buildOutput !== 'function') {
throw new TypeError('option `buildOutput` should be function when given');
}
if (cfg.exit && typeof cfg.exit !== 'function') {
throw new TypeError('option `exit` should function when given');
}
cfg.exit = cfg.exit ?? (() => {});
cfg.buildOutput = cfg.buildOutput ?? buildOutput;
const parse = cfg.yaroParse || (cfg.yaro && cfg.yaro.parse) || cfg.yaro;
if (typeof parse !== 'function') {
throw new TypeError('requires parser: `cfg.yaroParse` or `cfg.yaro`');
}
const parsedInfo = parse(cfg.argv);
const { rootCommand, entries } = getCommands(cfg);
const cliInfo = getCliInfo(rootCommand, entries, cfg);
if (parsedInfo.version) {
console.log(cliInfo.version);
cfg.exit(0);
return;
}
const meta = {
config: cfg,
cliInfo: { ...cliInfo },
argv: { ...parsedInfo },
rootCommand,
commands: Object.fromEntries(entries),
entries,
};
if (parsedInfo.help) {
await cfg.buildOutput(meta.argv, meta, { isHelp: true });
return;
}
if (rootCommand && meta.entries.length === 0) {
meta.matchedCommand = rootCommand;
meta.singleMode = true;
await tryCatch('ERR_ROOT_COMMAND_FAILED', meta, rootCommand);
return;
}
const matchedCommand = findMatchCommand(meta.entries, meta);
if (!matchedCommand) {
const noCommandSpecified = meta.argv._.length === 0;
const commandNotFound = noCommandSpecified === false;
await cfg.buildOutput(meta.argv, meta, {
noCommandSpecified,
commandNotFound,
exitCode: 1,
});
return;
}
if (rootCommand) {
const rootReturn = await tryCatch('ERR_ROOT_FAILURE', meta, rootCommand);
if (typeof rootReturn === 'function') {
const handler = () => rootReturn({ ...meta, matchedCommand });
meta.matchedCommand = matchedCommand;
await tryCatch('ERR_MATCHED_COMMAND_FAILURE', meta, handler);
return;
}
const code = 'ERR_UNKNOWN_STATE';
console.error(
'%s: expects `rootCommand` to return a function which calls `{ matchedCommand }` which is passed as argument',
code,
);
await cfg.buildOutput(meta.argv, meta, { isHelp: true, exitCode: 1 });
return;
}
await tryCatch('ERR_COMMAND_FAILED', meta, matchedCommand);
}
function getCommands(cfg) {
const cmds = { ...cfg.commands };
let commands = Object.entries(cmds);
let rootCmd = null;
if (typeof cfg.rootCommand === 'function') {
rootCmd = cfg.rootCommand;
}
if (!rootCmd) {
const allDef = Object.keys(cmds).length;
rootCmd = allDef === 1 ? (commands[0] ? commands[0][1] : null) : null;
commands = allDef === 1 ? [] : commands;
}
const rootCommand = typeof rootCmd === 'function' ? rootCmd : null;
if (rootCommand && rootCommand.cli) {
const { name: n } = rootCommand.cli;
rootCommand.cli.name = n.startsWith(UNNAMED_COMMAND_PREFIX) ? '_' : n;
}
const entries = commands
.filter(([_, cmd]) => cmd.isYaroCommand || typeof cmd === 'function')
.map(([key, _cmd]) => {
const cmd = _cmd;
let kk = key;
if (cmd.isYaroCommand) {
if (cmd.cli.name.startsWith(UNNAMED_COMMAND_PREFIX)) {
cmd.cli.name = kk;
cmd.cli.parts = [''];
} else {
kk = cmd.cli.name;
}
}
if (!cmd.isYaroCommand && typeof cmd === 'function' && cfg.yaroCommand) {
// in case we get just a bare function, treat it like command
return [kk, cfg.yaroCommand(cmd)];
}
return [kk, cmd];
});
return { rootCommand, entries };
}
function getCliInfo(rootCommand, commands, cfg) {
const version = cfg.version || '0.0.0';
if (rootCommand && rootCommand.cli) {
const { name, usage } = rootCommand.cli;
const n = name === '_' ? cfg.name ?? 'cli' : name;
return { name: n, usage, helpLine: `${n} ${usage}`.trim(), version };
}
const name = cfg.name || 'cli';
const usage = commands.length > 0 ? '<command>' : '';
return { name, usage, helpLine: `${name} ${usage}`.trim(), version };
}
async function tryCatch(code, meta, fn) {
let result = null;
try {
result = await fn(meta.argv);
} catch (err) {
const exitCode = err.code && typeof err.code === 'number' ? err.code : 1;
err.code = code;
err.meta = meta;
err.exitCode = exitCode;
if (meta.entries.length === 0) {
meta.config.buildOutput(meta.argv, meta, { error: err, exitCode, code });
return null;
}
if (fn.isYaroCommand) {
const nnn = fn.cli.name;
const uuu = fn.cli.usage;
meta.cliInfo.name = nnn;
meta.cliInfo.usage = uuu;
meta.cliInfo.helpLine = `${
meta.config.name || 'cli'
} ${nnn} ${uuu}`.trim();
meta.config.buildOutput(meta.argv, meta, { error: err, exitCode, code });
return null;
}
meta.config.buildOutput(meta.argv, meta, { error: err, exitCode, code });
return null;
}
return result;
}
function findMatchCommand(entries, meta) {
const match = entries.find(([name, cmd]) => {
if (cmd.isYaroCommand) {
let matched = false;
for (const [idx, arg] of meta.argv._.entries()) {
if (cmd.cli.aliases.includes(arg)) {
matched = arg;
break;
}
const tmp = meta.argv._.slice(0, idx + 1).join(' ');
matched = cmd.cli.aliases.find((x) => x === tmp) || '';
if (matched) {
break;
}
}
if (matched) {
meta.argv._ = meta.argv._.slice(matched.split(' ').length);
return true;
}
const cmdName = meta.argv._.slice(0, cmd.cli.parts.length).join(' ');
if (name === cmdName) {
meta.argv._ = meta.argv._.slice(cmd.cli.parts.length);
return true;
}
return false;
}
return false;
});
return match && match[1];
}