Merge pull request #494 from Hornwitser/typescript-ctl

Port ctl package to Typescript
This commit is contained in:
Hornwitser authored and GitHub committed 2023-08-12 20:52:14 +02:00
commit b72fa25d3f
13 files changed
+408 -283

No files matched your search

+64 -61
View File
@@ -4,18 +4,18 @@
* Command line interface for controlling a Clusterio cluster
* @module ctl/ctl
*/
"use strict";
const fs = require("fs-extra");
const yargs = require("yargs");
const version = require("./package").version;
const setBlocking = require("set-blocking");
import fs from "fs-extra";
import yargs from "yargs";
import { version } from "./package.json";
import setBlocking from "set-blocking";
import { strict as assert } from "assert";
// Reduce startup time by lazy compiling schemas.
global.lazySchemaCompilation = true;
const lib = require("@clusterio/lib");
const { ConsoleTransport, levels, logger } = lib;
(global as any).lazySchemaCompilation = true;
import * as lib from "@clusterio/lib";
import { ConsoleTransport, levels, logger } from "@clusterio/lib";
const commands = require("./src/commands");
import * as commands from "./src/commands";
/**
@@ -23,7 +23,9 @@ const commands = require("./src/commands");
* @private
*/
class ControlConnector extends lib.WebSocketClientConnector {
constructor(url, maxReconnectDelay, tlsCa, token) {
private _token: string;
constructor(url: string, maxReconnectDelay: number, tlsCa: string | undefined, token: string) {
super(url, maxReconnectDelay, tlsCa);
this._token = token;
}
@@ -48,32 +50,27 @@ class ControlConnector extends lib.WebSocketClientConnector {
* Connects to the controller over WebSocket and sends commands to it.
* @static
*/
class Control extends lib.Link {
constructor(connector, controlConfig, tlsCa, controlPlugins) {
export class Control extends lib.Link {
/** Control config used for connecting to the controller. */
config: lib.ControlConfig;
/** Certificate authority used to validate TLS connections to the controller. */
tlsCa?: string;
/** Mapping of plugin names to their instance for loaded plugins. */
plugins: Map<string, lib.BaseControlPlugin>;
/** Keep the control connection alive after the command completes. */
keepOpen = false;
constructor(
connector: ControlConnector,
controlConfig: lib.ControlConfig,
tlsCa: string | undefined,
controlPlugins: Map<string, lib.BaseControlPlugin>
) {
super(connector);
/**
* Control config used for connecting to the controller.
* @type {module:lib.ControlConfig}
*/
this.config = controlConfig;
/**
* Certificate authority used to validate TLS connections to the controller.
* @type {?string}
*/
this.tlsCa = tlsCa;
/**
* Mapping of plugin names to their instance for loaded plugins.
* @type {Map<string, module:lib.BaseControlPlugin>}
*/
this.plugins = controlPlugins;
/**
* Keep the control connection alive after the command completes.
* @type {boolean}
*/
this.keepOpen = false;
this.handle(lib.LogMessageEvent, this.handleLogMessageEvent.bind(this));
this.handle(lib.DebugWsMessageEvent, this.handleDebugWsMessageEvent.bind(this));
}
@@ -81,9 +78,9 @@ class Control extends lib.Link {
async setLogSubscriptions({
all = false,
controller = false,
hostIds = [],
instanceIds = [],
maxLevel = undefined,
hostIds = [] as number[],
instanceIds = [] as number[],
maxLevel = undefined as keyof typeof levels | undefined,
}) {
await this.send(
new lib.LogSetSubscriptionsRequest(
@@ -92,18 +89,18 @@ class Control extends lib.Link {
);
}
async handleLogMessageEvent(event) {
logger.log(event.info);
async handleLogMessageEvent(event: lib.LogMessageEvent) {
logger.log(event.info as any);
}
async handleDebugWsMessageEvent(event) {
async handleDebugWsMessageEvent(event: lib.DebugWsMessageEvent) {
// eslint-disable-next-line no-console
console.log("WS", event.direction, event.content);
}
async shutdown() {
try {
await this.connector.disconnect();
await (this.connector as ControlConnector).disconnect();
} catch (err) {
if (!(err instanceof lib.SessionLost)) {
throw err;
@@ -112,13 +109,13 @@ class Control extends lib.Link {
}
}
async function loadPlugins(pluginList) {
async function loadPlugins(pluginList: Map<string, string>) {
let pluginInfos = await lib.loadPluginInfos(pluginList);
lib.registerPluginMessages(pluginInfos);
lib.registerPluginConfigGroups(pluginInfos);
lib.finalizeConfigs();
let controlPlugins = new Map();
let controlPlugins = new Map<string, lib.BaseControlPlugin>();
for (let pluginInfo of pluginInfos) {
if (!pluginInfo.controlEntrypoint) {
continue;
@@ -132,6 +129,15 @@ async function loadPlugins(pluginList) {
return controlPlugins;
}
interface CtlArguments {
[index: string]: unknown;
$0: string,
_: (string | number)[],
logLevel: keyof typeof levels,
config: string,
pluginList: string,
}
async function startControl() {
yargs
.scriptName("clusterioctl")
@@ -163,7 +169,7 @@ async function startControl() {
;
// Parse the args first to get the configured plugin list.
let args = yargs.parse();
let args = yargs.parseSync() as CtlArguments;
// Log stream for the ctl session.
logger.add(
@@ -173,13 +179,13 @@ async function startControl() {
format: new lib.TerminalFormat(),
})
);
lib.handleUnhandledErrors(logger);
lib.handleUnhandledErrors();
logger.verbose(`Loading available plugins from ${args.pluginList}`);
let pluginList = new Map();
try {
pluginList = new Map(JSON.parse(await fs.readFile(args.pluginList)));
} catch (err) {
pluginList = new Map(JSON.parse(await fs.readFile(args.pluginList, "utf8")));
} catch (err: any) {
if (err.code !== "ENOENT") {
throw err;
}
@@ -201,15 +207,15 @@ async function startControl() {
args = yargs
.help()
.strict()
.parse()
.parse() as CtlArguments
;
logger.verbose(`Loading config from ${args.config}`);
let controlConfig = new lib.ControlConfig("control");
try {
await controlConfig.load(JSON.parse(await fs.readFile(args.config)));
await controlConfig.load(JSON.parse(await fs.readFile(args.config, "utf8")));
} catch (err) {
} catch (err: any) {
if (err.code === "ENOENT") {
logger.verbose("Config not found, initializing new config");
await controlConfig.init();
@@ -221,7 +227,7 @@ async function startControl() {
if (args._.length === 0) {
yargs.showHelp();
yargs.exit();
yargs.exit(1, undefined as unknown as Error); // Type definition file is wrong.
}
// Handle the control-config command before trying to connect.
@@ -231,11 +237,12 @@ async function startControl() {
}
// Determine which command is being executed.
let commandPath = [...args._];
let targetCommand = rootCommands;
let commandPath = [...args._] as string[];
let targetCommand: lib.CommandTree | lib.Command = rootCommands;
while (commandPath.length && targetCommand instanceof lib.CommandTree) {
targetCommand = targetCommand.get(commandPath.shift());
targetCommand = targetCommand.get(commandPath.shift()!)!;
}
assert(targetCommand instanceof lib.Command);
// The remaining commands require connecting to the controller.
if (!controlConfig.get("control.controller_url") || !controlConfig.get("control.controller_token")) {
@@ -244,17 +251,17 @@ async function startControl() {
return;
}
let tlsCa = null;
let tlsCaPath = controlConfig.get("control.tls_ca");
let tlsCa: string | undefined;
let tlsCaPath = controlConfig.get("control.tls_ca") as string | null;
if (tlsCaPath) {
tlsCa = await fs.readFile(tlsCaPath);
tlsCa = await fs.readFile(tlsCaPath, "utf8");
}
let controlConnector = new ControlConnector(
controlConfig.get("control.controller_url"),
controlConfig.get("control.max_reconnect_delay"),
controlConfig.get("control.controller_url") as string,
controlConfig.get("control.max_reconnect_delay") as number,
tlsCa,
controlConfig.get("control.controller_token")
controlConfig.get("control.controller_token") as string,
);
let control = new Control(controlConnector, controlConfig, tlsCa, controlPlugins);
try {
@@ -304,10 +311,6 @@ async function startControl() {
}
}
module.exports = {
Control,
};
if (module === require.main) {
// eslint-disable-next-line no-console
+10 -2
View File
@@ -4,12 +4,13 @@
"author": "Hornwitser",
"license": "MIT",
"version": "2.0.0-alpha.13",
"main": "ctl.js",
"main": "dist/ctl.js",
"scripts": {
"prepare": "tsc --build",
"test": "echo \"Error: no test specified\" && exit 1"
},
"bin": {
"clusterioctl": "./ctl.js"
"clusterioctl": "./dist/ctl.js"
},
"keywords": [
"factorio"
@@ -28,5 +29,12 @@
},
"publishConfig": {
"access": "public"
},
"devDependencies": {
"@types/fs-extra": "^11.0.1",
"@types/node": "^20.4.5",
"@types/set-blocking": "^2.0.0",
"@types/yargs": "^17.0.24",
"typescript": "^5.1.6"
}
}
File diff suppressed because it is too large. Load diff
+19
View File
@@ -0,0 +1,19 @@
{
"include": ["package.json", "ctl.ts", "src/**/*.ts"],
"compilerOptions": {
"lib": ["es2023"],
"module": "commonjs",
"target": "es2022",
"rootDir": ".",
"outDir": "dist",
"composite": true,
"sourceMap": true,
"strict": true,
"alwaysStrict": true,
"allowJs": true,
"checkJs": false,
"resolveJsonModule": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}
+2
View File
@@ -37,8 +37,10 @@
"devDependencies": {
"@types/fs-extra": "^11.0.1",
"@types/node": "^20.4.5",
"@types/set-blocking": "^2.0.0",
"@types/triple-beam": "^1.3.2",
"@types/ws": "^8.5.5",
"@types/yargs": "^17.0.24",
"typescript": "^5.1.6"
}
}
+4 -4
View File
@@ -9,10 +9,10 @@
import * as libData from "./data";
import * as libErrors from "./errors";
import type { Link } from "./link";
import type { Argv } from "yargs";
export type CommandHandler = (args: Record<string, unknown>, control: Link) => Promise<void>;
export type CommandDefinition = [string | string[], string?, Function?];
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
@@ -80,7 +80,7 @@ export class CommandTree {
* @param cmd.description -
* Descripton to provide for this command tree node.
*/
constructor({ name, alias, description }: { name: string, alias: string[], description: string }) {
constructor({ name, alias, description }: { name: string, alias?: string[], description: string }) {
if (typeof name !== "string") {
throw new Error("name must be a string");
}
+11 -27
View File
@@ -7,7 +7,7 @@ import type { PluginInfo } from "../plugin";
* Controller config group for {@link ControllerConfig}
* @extends classes.ConfigGroup
*/
class ControllerGroup extends classes.ConfigGroup { }
export class ControllerGroup extends classes.ConfigGroup { }
ControllerGroup.defaultAccess = ["controller", "host", "control"];
ControllerGroup.groupName = "controller";
ControllerGroup.define({
@@ -140,7 +140,7 @@ ControllerGroup.finalize();
* Controller Config class
* @extends classes.Config
*/
class ControllerConfig extends classes.Config { }
export class ControllerConfig extends classes.Config { }
ControllerConfig.registerGroup(ControllerGroup);
@@ -148,7 +148,7 @@ ControllerConfig.registerGroup(ControllerGroup);
* Host config group for {@link HostConfig}
* @extends classes.ConfigGroup
*/
class HostGroup extends classes.ConfigGroup {}
export class HostGroup extends classes.ConfigGroup {}
HostGroup.defaultAccess = ["controller", "host", "control"];
HostGroup.groupName = "host";
HostGroup.define({
@@ -230,7 +230,7 @@ HostGroup.finalize();
* Host Config class
* @extends classes.Config
*/
class HostConfig extends classes.Config { }
export class HostConfig extends classes.Config { }
HostConfig.registerGroup(HostGroup);
@@ -238,7 +238,7 @@ HostConfig.registerGroup(HostGroup);
* Instance config group for {@link classes.InstanceConfig}
* @extends classes.ConfigGroup
*/
class InstanceGroup extends classes.ConfigGroup { }
export class InstanceGroup extends classes.ConfigGroup { }
InstanceGroup.defaultAccess = ["controller", "host", "control"];
InstanceGroup.groupName = "instance";
InstanceGroup.define({
@@ -269,7 +269,7 @@ InstanceGroup.finalize();
* Factorio config group for {@link classes.InstanceConfig}
* @extends classes.ConfigGroup
*/
class FactorioGroup extends classes.ConfigGroup { }
export class FactorioGroup extends classes.ConfigGroup { }
FactorioGroup.defaultAccess = ["controller", "host", "control"];
FactorioGroup.groupName = "factorio";
FactorioGroup.define({
@@ -393,7 +393,7 @@ FactorioGroup.finalize();
* Instance config class
* @extends classes.Config
*/
class InstanceConfig extends classes.Config { }
export class InstanceConfig extends classes.Config { }
InstanceConfig.registerGroup(InstanceGroup);
InstanceConfig.registerGroup(FactorioGroup);
@@ -402,7 +402,7 @@ InstanceConfig.registerGroup(FactorioGroup);
* Control config group for {@link classes.ControlConfig}
* @extends classes.ConfigGroup
*/
class ControlGroup extends classes.ConfigGroup {}
export class ControlGroup extends classes.ConfigGroup {}
ControlGroup.defaultAccess = ["control"];
ControlGroup.groupName = "control";
ControlGroup.define({
@@ -436,7 +436,7 @@ ControlGroup.finalize();
* Control config class
* @extends classes.Config
*/
class ControlConfig extends classes.Config { }
export class ControlConfig extends classes.Config { }
ControlConfig.registerGroup(ControlGroup);
@@ -459,7 +459,7 @@ function validateGroup(pluginInfo: PluginInfo, groupName: string) {
*
* @param {Array<Object>} pluginInfos - Array of plugin info objects.
*/
function registerPluginConfigGroups(pluginInfos: PluginInfo[]) {
export function registerPluginConfigGroups(pluginInfos: PluginInfo[]) {
for (let pluginInfo of pluginInfos) {
if (pluginInfo.ControllerConfigGroup) {
validateGroup(pluginInfo, "ControllerConfigGroup");
@@ -492,25 +492,9 @@ function registerPluginConfigGroups(pluginInfos: PluginInfo[]) {
/**
* Lock configs from adding more groups and make them usable
*/
function finalizeConfigs() {
export function finalizeConfigs() {
ControllerConfig.finalize();
HostConfig.finalize();
InstanceConfig.finalize();
ControlConfig.finalize();
}
module.exports = {
ControllerGroup,
HostGroup,
InstanceGroup,
FactorioGroup,
ControlGroup,
ControllerConfig,
HostConfig,
InstanceConfig,
ControlConfig,
registerPluginConfigGroups,
finalizeConfigs,
};
+1 -1
View File
@@ -193,7 +193,7 @@ export class ModSearchRequest {
public queryIssues: string[],
public pageCount: number,
public resultCount: number,
public results: object[],
public results: { name: string, versions: ModInfo[] }[],
) { }
static jsonSchema = Type.Object({
+1 -1
View File
@@ -340,7 +340,7 @@ export abstract class WebSocketClientConnector extends WebSocketBaseConnector {
constructor(
protected _url: string,
maxReconnectDelay: number,
protected _tlsCa = null
protected _tlsCa: string | undefined,
) {
super(undefined as any, new libData.Address(libData.Address.controller, 0));
this._backoff = new ExponentialBackoff({ max: maxReconnectDelay });
+3 -3
View File
@@ -67,8 +67,8 @@ interface EventEntry {
allowedDstTypes: Set<number>;
eventFromJSON: (json: any) => Event<unknown>;
}
type RequestHandler<Req, Res> = (request: Request<Req, Res>, src: libData.Address, dst: libData.Address) => any;
type EventHandler<T> = (event: Event<T>, src: libData.Address, dst: libData.Address) => any;
type RequestHandler<Req, Res> = (request: Req, src: libData.Address, dst: libData.Address) => Promise<Res>;
type EventHandler<T> = (event: T, src: libData.Address, dst: libData.Address) => Promise<void>;
interface Router {
forwardMessage(origin: Link, message: Message, fallback: boolean): boolean,
@@ -99,7 +99,7 @@ export class Link {
constructor(
public connector: BaseConnector
) {
this.handle(libData.PingRequest, () => {});
this.handle(libData.PingRequest, async () => {});
// Process messages received by the connector
connector.on("message", payload => {
+3 -3
View File
@@ -84,7 +84,7 @@ function loadPluginClass(
*/
export async function loadControllerPluginClass(
pluginInfo: libPlugin.PluginInfo
): Promise<libPlugin.BaseControllerPlugin> {
): Promise<typeof libPlugin.BaseControllerPlugin> {
return loadPluginClass("controllerEntrypoint", "ControllerPlugin", libPlugin.BaseControllerPlugin, pluginInfo);
}
@@ -98,7 +98,7 @@ export async function loadControllerPluginClass(
*/
export async function loadInstancePluginClass(
pluginInfo: libPlugin.PluginInfo
): Promise<libPlugin.BaseInstancePlugin> {
): Promise<typeof libPlugin.BaseInstancePlugin> {
return loadPluginClass("instanceEntrypoint", "InstancePlugin", libPlugin.BaseInstancePlugin, pluginInfo);
}
@@ -112,6 +112,6 @@ export async function loadInstancePluginClass(
*/
export async function loadControlPluginClass(
pluginInfo: libPlugin.PluginInfo
): Promise<libPlugin.BaseControlPlugin> {
): Promise<typeof libPlugin.BaseControlPlugin> {
return loadPluginClass("controlEntrypoint", "ControlPlugin", libPlugin.BaseControlPlugin, pluginInfo);
}
-8
View File
@@ -1,8 +0,0 @@
"use strict";
const assert = require("assert").strict;
const clusterioctl = require("@clusterio/ctl/ctl.js");
describe("clusterioctl", function() {
});
+1 -1
View File
@@ -821,7 +821,7 @@ describe("Integration of Clusterio", function() {
let modPacks = await getControl().send(new lib.ModPackListRequest());
let modPack = modPacks.find(entry => entry.name === "full-pack");
assert(modPack, "created mod pack not found");
let reference = lib.ModPack.fromJSON({});
let reference = lib.ModPack.fromJSON({ factorio_version: "0.17.59" });
reference.id = modPack.id;
reference.name = "full-pack";
reference.description = "Description";