/** * Command library * * Data types and utilites for commands to clusterioctl's CLI. * * @author Hornwitser * @module lib/command */ import * as libData from "./data/index.js"; import * as libErrors from "./errors.js"; import type { Link } from "./link/index.js"; import type { Argv } from "yargs"; export type CommandHandler = (args: any, ctl: any) => Promise; export type CommandDefinition = [string | string[], string?, ((yargs: Argv) => void)?]; /** * Represents a command that can be runned by clusterioctl */ export class Command { private _handler: CommandHandler; private _definition: CommandDefinition; name: string; alias: string[]; /** * Define an executable command. * * @param cmd - Command definiton. * @param cmd.definiton - * Arguments to pass to yargs .command method to define this * command. * @param cmd.handler - * Async function invoked when the command is executed. Is given * the parsed args and a reference to the {@link * module:ctl/ctl.Ctl} instance. */ constructor({ definition, handler }: { definition: CommandDefinition, handler: CommandHandler }) { if (definition[0] instanceof Array) { this.name = definition[0][0].split(" ")[0]; this.alias = definition[0].slice(1); } else { this.name = definition[0].split(" ")[0]; this.alias = []; } this._definition = definition; this._handler = handler; } register(yargs: any) { yargs.command(...this._definition); } async run(args: Record, ctl: Link) { await this._handler(args, ctl); } } /** * A node in the command tree that can hold commands and sub trees * * Container which can contain {@link module:lib.Command}s and other * CommandTrees. This is used by clusterioctl to hold the full tree of * available commands, you may extend this tree by using a ctl plugin, * see {@link module:lib.BaseControlPlugin#addCommands} */ export class CommandTree { public name: string; public alias: string[]; public description: string; public subCommands: Map; /** * Define a command containing sub commands. * * @param cmd - Command definition * @param cmd.name - Name of the command. * @param cmd.alias - Aliases for this tree. * @param cmd.description - * Descripton to provide for this command tree node. */ constructor({ name, alias, description }: { name: string, alias?: string[], description: string }) { if (typeof name !== "string") { throw new Error("name must be a string"); } this.name = name; this.alias = alias || []; this.description = description; this.subCommands = new Map(); } /** * Add a command or command tree to this tree * * @param command - * The command to add to this command tree. */ add(command: Command | CommandTree) { if (this.subCommands.has(command.name)) { throw new Error(`Command ${command.name} already exists`); } this.subCommands.set(command.name, command); for (let alias of command.alias) { if (this.subCommands.has(alias)) { throw new Error(`Alias ${alias} for command ${command.name} already exists`); } this.subCommands.set(alias, command); } } /** * Get a command or command tree from this tree * * @param name - * The name of the command or command tree to retrieve. * @return * The command to add to this command tree. */ get(name: string) { return this.subCommands.get(name); } /** * Insert the registered commands into the given yargs parser * * Traverses the the tree of sub-commands recursively and adds all of * them to the given yargs parser. * * @param yargs - yargs parser to register with. */ register(yargs: any) { yargs.command([this.name].concat(this.alias), this.description, (yargs: any) => { for (let [name, command] of this.subCommands) { // Check if the entry is not an alias. if (name === command.name) { command.register(yargs); } } }, () => { yargs.showHelp(); yargs.exit(); }); } } /** * Resolve a string with either an id or a name to the id of an entry * * An integer like string is treated as an id if an entry with that id * exists, otherwise it is looked up as a name. */ function resolveId(entries: { id: number, name: string }[], idOrName: string) { if (/^-?\d+$/.test(idOrName)) { const id = parseInt(idOrName, 10); if (entries.some(entry => entry.id === id)) { return id; } } return entries.find(entry => entry.name === idOrName)?.id; } /** * Resolve a string into a host ID * * Resolves a string with either an host name or an id into an integer with * the host ID. * * @param client - link to controller to query host on. * @param hostName - string with name or id of host. * @returns host ID. */ export async function resolveHost(client: Link, hostName: string) { const hosts = await client.sendTo("controller", new libData.HostListRequest()); const hostId = resolveId(hosts, hostName); if (hostId === undefined) { throw new libErrors.CommandError(`No host named ${hostName}`); } return hostId; } /** * Resolve a string to an instance ID * * Resolves a string with either an instance name or an id into an integer * with the instance ID. * * @param client - link to controller to query instance on. * @param instanceName - string with name or id of instance. * @returns instance ID. */ export async function resolveInstance(client: Link, instanceName: string) { const instances = await client.sendTo("controller", new libData.InstanceDetailsListRequest()); const instanceId = resolveId(instances, instanceName); if (instanceId === undefined) { throw new libErrors.CommandError(`No instance named ${instanceName}`); } return instanceId; } /** * Resolve a string to a mod pack ID * * Resolevs a string with either a mod pack name or an id into an integer * with the mod pack ID. * * @param client - * link to controller to query mod pack on. * @param modPackName - string with name or id of mod pack. * @returns mod pack ID. */ export async function resolveModPack(client: Link, modPackName: string) { const modPacks = await client.sendTo("controller", new libData.ModPackListRequest()); const modPackId = resolveId(modPacks, modPackName); if (modPackId === undefined) { throw new libErrors.CommandError(`No mod pack named ${modPackName}`); } return modPackId; } /** * Retrieve role object from string * * Resolves a string with either a role name or an id into an object * representing the role. * * @param client - link to controller to query role on. * @param roleName - string with name or id of role. * @returns Role info. */ export async function retrieveRole(client: Link, roleName: string) { const roles = await client.sendTo("controller", new libData.RoleListRequest()); const roleId = resolveId(roles, roleName); const resolvedRole = roles.find(role => role.id === roleId); if (!resolvedRole) { throw new libErrors.CommandError(`No role named ${roleName}`); } return resolvedRole; }