cli-kit
Version:
Everything you need to create awesome command line interfaces
181 lines (159 loc) • 5.13 kB
JavaScript
import Command from './command.js';
import E from '../lib/errors.js';
import fs from 'fs';
import path from 'path';
import { declareCLIKitClass } from '../lib/util.js';
/**
* Matches
*
* @type {RegExp}
*/
const jsRegExp = /\.js$/;
/**
* Stores a map of `Command` instances that have been registered for a context.
*
* @extends {Map}
*/
export default class CommandMap extends Map {
/**
* Declares the class name.
*
* @access public
*/
constructor() {
super();
declareCLIKitClass(this, 'CommandMap');
}
/**
* Adds a command to the map.
*
* @param {Object|String|Command|CommandMap|Array.<Object|String|Command>} cmd - An object used
* for `Command` constructor params, a path to a directory or a `.js` file, a `Command`
* instance, or an array of those types. May also be a `CommandMap` instance. If `cmd` is a
* `String` and `params` is present, then it will treat `cmd` as the command name, not a file
* path.
* @param {Object|Function} [params] - When `cmd` is the command name, then this is the options
* to pass into the `Command` constructor.
* @param {Boolean} [clone] - When `true` and `cmd` is a `Command` or `CommandMap`, it will
* clone the `Command` instead of set by reference.
* @returns {Array.<Command>}
* @access public
*/
add(cmd, params, clone) {
if (!cmd) {
throw E.INVALID_ARGUMENT('Invalid command', { name: 'cmd', scope: 'CommandMap.add', value: cmd });
}
if (params !== undefined && params !== null) {
if (typeof cmd !== 'string') {
throw E.INVALID_ARGUMENT('Command parameters are only allowed when command is a string', { name: 'cmd', scope: 'CommandMap.add', value: { cmd, params } });
} else if (typeof params === 'function') {
params = { action: params };
} else if (typeof params !== 'object') {
throw E.INVALID_ARGUMENT('Expected command parameters to be an object or function', { name: 'params', scope: 'CommandMap.add', value: { cmd, params } });
}
cmd = new Command(cmd, params);
}
const results = [];
const commands = typeof cmd === 'object' && cmd.clikit instanceof Set && (cmd.clikit.has('CommandList') || cmd.clikit.has('CommandMap')) ?
cmd.values() :
Array.isArray(cmd) ? cmd : [ cmd ];
for (let it of commands) {
cmd = null;
if (!clone && it instanceof Command) {
cmd = it;
} else if (typeof it === 'string') {
// path
it = path.resolve(it);
try {
const files = fs.statSync(it).isDirectory() ? fs.readdirSync(it).map(filename => path.join(it, filename)) : [ it ];
for (const file of files) {
if (jsRegExp.test(file)) {
cmd = new Command(file);
this.set(cmd.name, cmd);
results.push(cmd);
}
}
} catch (e) {
if (e.code === 'ENOENT') {
throw E.FILE_NOT_FOUND(`Command path does not exist: ${it}`, { name: 'command', scope: 'CommandMap.add', value: it });
}
throw e;
}
continue;
} else if (it && typeof it === 'object') {
// ctor params or Command-like
if (it.clikit instanceof Set) {
if (it.clikit.has('Extension')) {
// actions and extensions not supported here
continue;
} else if (it.clikit.has('Command')) {
cmd = new Command(it.name, it);
} else {
throw E.INVALID_COMMAND(`Invalid command: cli-kit type "${Array.from(it.clikit)[0]}" not supported`, { name: 'command', scope: 'CommandMap.add', value: it });
}
} else if (it.name && typeof it.name === 'string') {
// the object is command ctor params
cmd = new Command(it.name, it);
} else {
// an object of command names to a path, ctor params, Command object, or Command-like object
for (const [ name, value ] of Object.entries(it)) {
cmd = new Command(name, value);
this.set(cmd.name, cmd);
results.push(cmd);
}
continue;
}
}
if (cmd instanceof Command) {
this.set(cmd.name, cmd);
results.push(cmd);
} else {
throw E.INVALID_ARGUMENT(`Invalid command "${it}", expected an object`, { name: 'command', scope: 'CommandMap.add', value: it });
}
}
return results;
}
/**
* Returns the number of commands.
*
* @returns {Number}
* @access public
*/
get count() {
return this.size;
}
/**
* Generates an object containing the commands for the help screen.
*
* @returns {Promise<Object>}
* @access public
*/
async generateHelp() {
const entries = [];
for (const cmd of this.values()) {
await cmd.load();
const { aliases, clikitHelp, desc, hidden, name } = cmd;
if (!hidden && !clikitHelp) {
const labels = new Set([ name ]);
for (const [ alias, display ] of Object.entries(aliases)) {
if (display === 'visible') {
labels.add(alias);
}
}
entries.push({
name,
desc,
label: Array.from(labels).sort((a, b) => {
return a.length === b.length ? a.localeCompare(b) : a.length - b.length;
}).join(', '),
aliases: aliases ? Object.keys(aliases) : null
});
}
}
entries.sort((a, b) => a.label.localeCompare(b.label));
return {
count: entries.length,
entries
};
}
}