Files
factorio-clusterio/packages/lib/src/command.ts
T

243 lines
6.9 KiB
TypeScript

/**
* 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, control: any) => Promise<void>;
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.Control} 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<string, unknown>, control: Link) {
await this._handler(args, control);
}
}
/**
* 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 control plugin,
* see {@link module:lib.BaseControlPlugin#addCommands}
*/
export class CommandTree {
public name: string;
public alias: string[];
public description: string;
public subCommands: Map<string, Command | CommandTree>;
/**
* 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;
}