From 5ec7c9f99ce91ab4264e66800d852c12c3262d31 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Sat, 12 Aug 2023 19:59:44 +0200 Subject: [PATCH 1/2] Fix bad typings in lib These were missed or broken in the in the inital porting of lib to TypeScript. --- packages/lib/package.json | 2 ++ packages/lib/src/command.ts | 8 +++--- packages/lib/src/config/definitions.ts | 38 ++++++++------------------ packages/lib/src/data/messages_mod.ts | 2 +- packages/lib/src/link/connectors.ts | 2 +- packages/lib/src/link/link.ts | 6 ++-- packages/lib/src/plugin_loader.ts | 6 ++-- 7 files changed, 25 insertions(+), 39 deletions(-) diff --git a/packages/lib/package.json b/packages/lib/package.json index 8bc6d749..bbc99d02 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -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" } } diff --git a/packages/lib/src/command.ts b/packages/lib/src/command.ts index d2173ed0..ea5eea11 100644 --- a/packages/lib/src/command.ts +++ b/packages/lib/src/command.ts @@ -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, control: Link) => Promise; -export type CommandDefinition = [string | string[], string?, Function?]; +export type CommandHandler = (args: any, control: any) => Promise; +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"); } diff --git a/packages/lib/src/config/definitions.ts b/packages/lib/src/config/definitions.ts index 2d25e542..13e18599 100644 --- a/packages/lib/src/config/definitions.ts +++ b/packages/lib/src/config/definitions.ts @@ -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} 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, -}; diff --git a/packages/lib/src/data/messages_mod.ts b/packages/lib/src/data/messages_mod.ts index 0fadcfda..0c4fe7ce 100644 --- a/packages/lib/src/data/messages_mod.ts +++ b/packages/lib/src/data/messages_mod.ts @@ -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({ diff --git a/packages/lib/src/link/connectors.ts b/packages/lib/src/link/connectors.ts index b52d2103..203f9c48 100644 --- a/packages/lib/src/link/connectors.ts +++ b/packages/lib/src/link/connectors.ts @@ -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 }); diff --git a/packages/lib/src/link/link.ts b/packages/lib/src/link/link.ts index e4352b3a..2736e79c 100644 --- a/packages/lib/src/link/link.ts +++ b/packages/lib/src/link/link.ts @@ -67,8 +67,8 @@ interface EventEntry { allowedDstTypes: Set; eventFromJSON: (json: any) => Event; } -type RequestHandler = (request: Request, src: libData.Address, dst: libData.Address) => any; -type EventHandler = (event: Event, src: libData.Address, dst: libData.Address) => any; +type RequestHandler = (request: Req, src: libData.Address, dst: libData.Address) => Promise; +type EventHandler = (event: T, src: libData.Address, dst: libData.Address) => Promise; 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 => { diff --git a/packages/lib/src/plugin_loader.ts b/packages/lib/src/plugin_loader.ts index e557bea9..6c44f897 100644 --- a/packages/lib/src/plugin_loader.ts +++ b/packages/lib/src/plugin_loader.ts @@ -84,7 +84,7 @@ function loadPluginClass( */ export async function loadControllerPluginClass( pluginInfo: libPlugin.PluginInfo -): Promise { +): Promise { return loadPluginClass("controllerEntrypoint", "ControllerPlugin", libPlugin.BaseControllerPlugin, pluginInfo); } @@ -98,7 +98,7 @@ export async function loadControllerPluginClass( */ export async function loadInstancePluginClass( pluginInfo: libPlugin.PluginInfo -): Promise { +): Promise { return loadPluginClass("instanceEntrypoint", "InstancePlugin", libPlugin.BaseInstancePlugin, pluginInfo); } @@ -112,6 +112,6 @@ export async function loadInstancePluginClass( */ export async function loadControlPluginClass( pluginInfo: libPlugin.PluginInfo -): Promise { +): Promise { return loadPluginClass("controlEntrypoint", "ControlPlugin", libPlugin.BaseControlPlugin, pluginInfo); } From ec0fded0cca96443689aeb5be7a7a773907cac0a Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Sat, 12 Aug 2023 20:02:20 +0200 Subject: [PATCH 2/2] Port ctl package to TypeScript --- packages/ctl/{ctl.js => ctl.ts} | 125 ++--- packages/ctl/package.json | 12 +- packages/ctl/src/{commands.js => commands.ts} | 461 +++++++++++------- packages/ctl/tsconfig.json | 19 + test/ctl.js | 8 - test/integration/clusterio.js | 2 +- 6 files changed, 383 insertions(+), 244 deletions(-) rename packages/ctl/{ctl.js => ctl.ts} (73%) rename packages/ctl/src/{commands.js => commands.ts} (83%) create mode 100644 packages/ctl/tsconfig.json delete mode 100644 test/ctl.js diff --git a/packages/ctl/ctl.js b/packages/ctl/ctl.ts similarity index 73% rename from packages/ctl/ctl.js rename to packages/ctl/ctl.ts index 4bdb33c5..61ecf9c2 100755 --- a/packages/ctl/ctl.js +++ b/packages/ctl/ctl.ts @@ -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; + /** Keep the control connection alive after the command completes. */ + keepOpen = false; + + constructor( + connector: ControlConnector, + controlConfig: lib.ControlConfig, + tlsCa: string | undefined, + controlPlugins: Map + ) { 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} - */ 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) { let pluginInfos = await lib.loadPluginInfos(pluginList); lib.registerPluginMessages(pluginInfos); lib.registerPluginConfigGroups(pluginInfos); lib.finalizeConfigs(); - let controlPlugins = new Map(); + let controlPlugins = new Map(); 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 diff --git a/packages/ctl/package.json b/packages/ctl/package.json index de09a5d0..e56f966d 100644 --- a/packages/ctl/package.json +++ b/packages/ctl/package.json @@ -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" } } diff --git a/packages/ctl/src/commands.js b/packages/ctl/src/commands.ts similarity index 83% rename from packages/ctl/src/commands.js rename to packages/ctl/src/commands.ts index f3c5a9d1..c4ad00f6 100644 --- a/packages/ctl/src/commands.js +++ b/packages/ctl/src/commands.ts @@ -1,47 +1,49 @@ -"use strict"; -const fs = require("fs-extra"); -const asTable = require("as-table").configure({ delimiter: " | " }); -const events = require("events"); -const path = require("path"); -const winston = require("winston"); -const phin = require("phin"); -const os = require("os"); -const child_process = require("child_process"); -const stream = require("stream"); -const util = require("util"); +import fs from "fs-extra"; +import asTableModule from "as-table"; +import events from "events"; +import path from "path"; +import winston from "winston"; +import phin from "phin"; +import os from "os"; +import child_process from "child_process"; +import stream from "stream"; +import util from "util"; +import type { Argv } from "yargs"; -const lib = require("@clusterio/lib"); -const { ConsoleTransport, levels, logger } = lib; +import * as lib from "@clusterio/lib"; +import { ConsoleTransport, levels, logger } from "@clusterio/lib"; +import type { Control } from "../ctl.js"; +const asTable = asTableModule.configure({ delimiter: " | " }); const finished = util.promisify(stream.finished); -function print(...content) { +function print(...content: any) { // eslint-disable-next-line no-console console.log(...content); } -async function getEditor(argsEditor) { +async function getEditor(argsEditor: string) { // eslint-disable-next-line - return argsEditor || process.env.EDITOR || process.env.VISUAL || -1 + return argsEditor || process.env.EDITOR || process.env.VISUAL || undefined // needed for the process.env statements to not be flagged by eslint // priority for editors is CLI argument > env.EDITOR > env.VISUAL } -async function configToKeyVal(data) { - let final = {}; +async function configToKeyVal(data: string) { + let final: Record = {}; let splitData = data.split(/\r?\n/); // split on newlines let filtered = splitData.filter((value) => value[0] !== "#").filter((a) => a); // the last filter removes empty elements left by the first. Not done on one line due to readability. for (let index in filtered) { if (index in filtered) { - filtered[index] = filtered[index].split("="); + let split = filtered[index].split("="); let finalIndex = filtered[index][0].trim(); // split on the = we added earlier, giving us both value and key let part = ""; try { - part = filtered[index][1].trim(); + part = split[1].trim(); // it's a string if we can read it } catch (err) { // if we can't read it, it's a empty field and therefor null @@ -53,7 +55,11 @@ async function configToKeyVal(data) { return final; } -async function serializedConfigToString(serializedConfig, configGroup, disallowedList) { +async function serializedConfigToString( + serializedConfig: any, + configGroup: typeof lib.Config, + disallowedList: Record, +) { let allConfigElements = ""; for (let group of serializedConfig.groups) { for (let [name, value] of Object.entries(group.fields)) { @@ -62,7 +68,7 @@ async function serializedConfigToString(serializedConfig, configGroup, disallowe } let desc = ""; try { - desc += configGroup.groups.get(group.name)._definitions.get(name).description; + desc += configGroup.groups.get(group.name)!._definitions.get(name)!.description; } catch (err) { desc += "No description found"; } @@ -80,12 +86,13 @@ const controllerCommands = new lib.CommandTree({ name: "controller", description const controllerConfigCommands = new lib.CommandTree({ name: "config", alias: ["c"], description: "controller config management", }); + controllerConfigCommands.add(new lib.Command({ definition: ["list", "List controller configuration"], - handler: async function(args, control) { + handler: async function(args: {}, control: Control) { let response = await control.send(new lib.ControllerConfigGetRequest()); - for (let group of response.serializedConfig.groups) { + for (let group of (response.serializedConfig as any).groups) { for (let [name, value] of Object.entries(group.fields)) { print(`${group.name}.${name} ${JSON.stringify(value)}`); } @@ -101,13 +108,13 @@ controllerConfigCommands.add(new lib.Command({ "stdin": { describe: "read value from stdin", nargs: 0, type: "boolean" }, }); }], - handler: async function(args, control) { + handler: async function(args: { field: string, value: string, stdin?: boolean }, control: Control) { if (args.stdin) { args.value = (await lib.readStream(process.stdin)).toString().replace(/\r?\n$/, ""); } else if (args.value === undefined) { args.value = ""; } - await control.send(new lib.ControllerConfigSetFieldRequest(args.field, args.value)); + await control.send(new lib.ControllerConfigSetFieldRequest(args.field as string, args.value as string)); }, })); @@ -120,7 +127,7 @@ controllerConfigCommands.add(new lib.Command({ "stdin": { describe: "read value from stdin", nargs: 0, type: "boolean" }, }); }], - handler: async function(args, control) { + handler: async function(args: { field: string, prop: string, value: string, stdin?: boolean }, control: Control) { if (args.stdin) { args.value = (await lib.readStream(process.stdin)).toString().replace(/\r?\n$/, ""); } @@ -129,7 +136,7 @@ controllerConfigCommands.add(new lib.Command({ if (args.value !== undefined) { value = JSON.parse(args.value); } - } catch (err) { + } catch (err: any) { // See note for the instance version of set-prop if (args.stdin || /^(\[.*]|{.*}|".*")$/.test(args.value)) { throw new lib.CommandError(`In parsing value '${args.value}': ${err.message}`); @@ -147,28 +154,24 @@ controllerConfigCommands.add(new lib.Command({ default: "", }); }], - handler: async function(args, control) { - let response = await control.send(new ControllerConfigGetRequest()); + handler: async function(args: { editor: string }, control: Control) { + let response = await control.send(new lib.ControllerConfigGetRequest()); let tmpFile = await lib.getTempFile("ctl-", "-tmp", os.tmpdir()); let editor = await getEditor(args.editor); - if (editor === -1) { + if (editor === undefined) { throw new lib.CommandError(`No editor avalible. Checked CLI input, EDITOR and VISUAL env vars Try "ctl controller config edit "`); } let allConfigElements = await serializedConfigToString( response.serializedConfig, lib.ControllerConfig, {} ); - await fs.writeFile(tmpFile, allConfigElements, (err) => { - if (err) { - throw err; - } - }); + await fs.writeFile(tmpFile, allConfigElements); let editorSpawn = child_process.spawn(editor, [tmpFile], { stdio: "inherit", - detached: false, + detached: false, }); editorSpawn.on("data", (data) => { - process.stdout.pipe(data); + process.stdout.pipe(data); }); let doneEmitter = new events.EventEmitter(); editorSpawn.on("exit", async (exit) => { @@ -177,7 +180,7 @@ controllerConfigCommands.add(new lib.Command({ for (let index in final) { if (index in final) { try { - await control.send(new ControllerConfigSetFieldRequest( + await control.send(new lib.ControllerConfigSetFieldRequest( index, final[index], )); @@ -192,12 +195,12 @@ controllerConfigCommands.add(new lib.Command({ doneEmitter.emit("dot_on_done"); }); await events.once(doneEmitter, "dot_on_done"); - await fs.unlink(tmpFile, (err) => { - if (err) { - print("err: temporary file", tmpFile, "could not be deleted."); - print("This is not fatal, but they may build up over time if the issue persists."); - } - }); + try { + await fs.unlink(tmpFile); + } catch (err) { + print("err: temporary file", tmpFile, "could not be deleted."); + print("This is not fatal, but they may build up over time if the issue persists."); + } }, })); @@ -209,13 +212,13 @@ const controllerPluginCommands = new lib.CommandTree({ }); controllerPluginCommands.add(new lib.Command({ definition: ["list", "List plugins on controller"], - handler: async function(args, control) { - let url = new URL(control.config.get("control.controller_url")); + handler: async function(args: {}, control: Control) { + let url = new URL(control.config.get("control.controller_url") as string); url.pathname += "api/plugins"; - let response = await phin({ + let response = await phin<[]>({ url, parse: "json", - core: { ca: control.tlsCa }, + core: { ca: control.tlsCa } as {}, }); print(asTable(response.body)); }, @@ -226,7 +229,7 @@ controllerCommands.add(controllerPluginCommands); const hostCommands = new lib.CommandTree({ name: "host", description: "Host management" }); hostCommands.add(new lib.Command({ definition: [["list", "l"], "List hosts connected to the controller"], - handler: async function(args, control) { + handler: async function(args: {}, control: Control) { let hosts = await control.send(new lib.HostListRequest()); print(asTable(hosts)); }, @@ -236,7 +239,7 @@ hostCommands.add(new lib.Command({ definition: ["generate-token", "Generate token for a host", (yargs) => { yargs.option("id", { type: "number", nargs: 1, describe: "Host id" }); }], - handler: async function(args, control) { + handler: async function(args: { id?: number }, control: Control) { let hostId = typeof args.id === "number" ? args.id : undefined; let response = await control.send(new lib.HostGenerateTokenRequest(hostId)); print(response); @@ -245,8 +248,8 @@ hostCommands.add(new lib.Command({ hostCommands.add(new lib.Command({ definition: ["create-config", "Create host config", (yargs) => { - yargs.option("id", { type: "number", nargs: 1, describe: "Host id", default: null }); - yargs.option("name", { type: "string", nargs: 1, describe: "Host name", default: null }); + yargs.option("id", { type: "number", nargs: 1, describe: "Host id" }); + yargs.option("name", { type: "string", nargs: 1, describe: "Host name" }); yargs.option("generate-token", { type: "boolean", nargs: 0, describe: "Generate authentication token", default: false, }); @@ -254,7 +257,10 @@ hostCommands.add(new lib.Command({ type: "string", nargs: 1, describe: "Path to output config (- for stdout)", default: "config-host.json", }); }], - handler: async function(args, control) { + handler: async function( + args: { id?: number, name?: string, generateToken: boolean, output: string }, + control: Control + ) { let rawConfig = await control.send( new lib.HostConfigCreateRequest(args.id, args.name, args.generateToken) ); @@ -266,7 +272,7 @@ hostCommands.add(new lib.Command({ logger.info(`Writing ${args.output}`); try { await fs.outputFile(args.output, content, { flag: "wx" }); - } catch (err) { + } catch (err: any) { if (err.code === "EEXIST") { throw new lib.CommandError(`File ${args.output} already exists`); } @@ -282,7 +288,7 @@ const instanceCommands = new lib.CommandTree({ }); instanceCommands.add(new lib.Command({ definition: [["list", "l"], "List instances known to the controller"], - handler: async function(args, control) { + handler: async function(args: {}, control: Control) { let list = await control.send(new lib.InstanceDetailsListRequest()); print(asTable(list)); }, @@ -296,10 +302,10 @@ instanceCommands.add(new lib.Command({ "id": { type: "number", nargs: 1, describe: "Instance id" }, }); }], - handler: async function(args, control) { + handler: async function(args: { name: string, id?: number }, control: Control) { let instanceConfig = new lib.InstanceConfig("control"); await instanceConfig.init(); - if (args.id) { + if (args.id !== undefined) { instanceConfig.set("instance.id", args.id); } instanceConfig.set("instance.name", args.name); @@ -315,11 +321,11 @@ instanceConfigCommands.add(new lib.Command({ definition: ["list ", "List configuration for an instance", (yargs) => { yargs.positional("instance", { describe: "Instance to list config for", type: "string" }); }], - handler: async function(args, control) { + handler: async function(args: { instance: string }, control: Control) { let instanceId = await lib.resolveInstance(control, args.instance); let response = await control.send(new lib.InstanceConfigGetRequest(instanceId)); - for (let group of response.config.groups) { + for (let group of (response.config as any).groups) { for (let [name, value] of Object.entries(group.fields)) { print(`${group.name}.${name} ${JSON.stringify(value)}`); } @@ -336,7 +342,10 @@ instanceConfigCommands.add(new lib.Command({ "stdin": { describe: "read value from stdin", nargs: 0, type: "boolean" }, }); }], - handler: async function(args, control) { + handler: async function( + args: { instance: string, field: string, value: string, stdin?: boolean }, + control: Control, + ) { let instanceId = await lib.resolveInstance(control, args.instance); if (args.stdin) { args.value = (await lib.readStream(process.stdin)).toString().replace(/\r?\n$/, ""); @@ -357,7 +366,10 @@ instanceConfigCommands.add(new lib.Command({ "stdin": { describe: "read value from stdin", nargs: 0, type: "boolean" }, }); }], - handler: async function(args, control) { + handler: async function( + args: { instance: string, field: string, prop: string, value: string, stdin?: boolean }, + control: Control + ) { let instanceId = await lib.resolveInstance(control, args.instance); if (args.stdin) { args.value = (await lib.readStream(process.stdin)).toString().replace(/\r?\n$/, ""); @@ -367,7 +379,7 @@ instanceConfigCommands.add(new lib.Command({ if (args.value !== undefined) { value = JSON.parse(args.value); } - } catch (err) { + } catch (err: any) { // If this is from stdin or looks like an array, object or string // literal throw the parse error, otherwise assume this is a string. // The resoning behind this is that correctly quoting the string @@ -402,32 +414,28 @@ instanceConfigCommands.add(new lib.Command({ default: "", }); }], - handler: async function(args, control) { + handler: async function(args: { instance: string, editor: string }, control: Control) { let instanceId = await lib.resolveInstance(control, args.instance); let response = await control.send(new lib.InstanceConfigGetRequest(instanceId)); let tmpFile = await lib.getTempFile("ctl-", "-tmp", os.tmpdir()); let editor = await getEditor(args.editor); - if (editor === -1) { + if (editor === undefined) { throw new lib.CommandError(`No editor avalible. Checked CLI input, EDITOR and VISUAL env vars Try "ctl controller config edit "`); } let disallowedList = {"instance.id": 0, "instance.assigned_host": 0, "factorio.settings": 0}; let allConfigElements = await serializedConfigToString( - response.serializedConfig, + response.config, lib.InstanceConfig, disallowedList ); - await fs.writeFile(tmpFile, allConfigElements, (err) => { - if (err) { - throw err; - } - }); + await fs.writeFile(tmpFile, allConfigElements); let editorSpawn = child_process.spawn(editor, [tmpFile], { stdio: "inherit", - detached: false, + detached: false, }); editorSpawn.on("data", (data) => { - process.stdout.pipe(data); + process.stdout.pipe(data); }); let doneEmitter = new events.EventEmitter(); editorSpawn.on("exit", async (exit) => { @@ -436,7 +444,7 @@ instanceConfigCommands.add(new lib.Command({ for (let index in final) { if (index in final) { try { - await control.send(new lib.InstanceConfigSetField( + await control.send(new lib.InstanceConfigSetFieldRequest( instanceId, index, final[index], @@ -457,12 +465,12 @@ instanceConfigCommands.add(new lib.Command({ doneEmitter.emit("dot_on_done"); }); await events.once(doneEmitter, "dot_on_done"); - await fs.unlink(tmpFile, (err) => { - if (err) { - print("err: temporary file", tmpFile, "could not be deleted."); - print("This is not fatal, but they may build up over time if the issue persists."); - } - }); + try { + await fs.unlink(tmpFile) + } catch (err) { + print("err: temporary file", tmpFile, "could not be deleted."); + print("This is not fatal, but they may build up over time if the issue persists."); + } }, })); instanceCommands.add(instanceConfigCommands); @@ -472,14 +480,16 @@ instanceCommands.add(new lib.Command({ yargs.positional("instance", { describe: "Instance to assign", type: "string" }); yargs.positional("host", { describe: "Host to assign to or unassign if none", type: "string" }); }], - handler: async function(args, control) { + handler: async function(args: { instance: string, host?: string }, control: Control) { let instanceId = await lib.resolveInstance(control, args.instance); - let hostId = args.host ? await lib.resolveHost(control, args.host) : null; + let hostId = args.host ? await lib.resolveHost(control, args.host) : undefined; await control.send(new lib.InstanceAssignRequest(instanceId, hostId)); }, })); -async function loadMapSettings(args) { +async function loadMapSettings( + args: { seed?: number, mapExchangeString?: string, mapGenSettings?: string, mapSettings?: string}, +) { let seed = args.seed; let mapGenSettings; let mapSettings; @@ -489,10 +499,10 @@ async function loadMapSettings(args) { mapSettings = parsed.map_settings; } if (args.mapGenSettings) { - mapGenSettings = JSON.parse(await fs.readFile(args.mapGenSettings)); + mapGenSettings = JSON.parse((await fs.readFile(args.mapGenSettings)).toString()); } if (args.mapSettings) { - mapSettings = JSON.parse(await fs.readFile(args.mapSettings)); + mapSettings = JSON.parse((await fs.readFile(args.mapSettings)).toString()); } return { @@ -509,14 +519,12 @@ instanceSaveCommands.add(new lib.Command({ definition: ["list ", "list saves on an instance", (yargs) => { yargs.positional("instance", { describe: "Instance to list saves on", type: "string" }); }], - handler: async function(args, control) { + handler: async function(args: { instance: string }, control: Control) { let instanceId = await lib.resolveInstance(control, args.instance); let saves = await control.sendTo({ instanceId }, new lib.InstanceListSavesRequest()); - for (let entry of saves) { - entry.mtime = new Date(entry.mtimeMs).toLocaleString(); - delete entry.mtimeMs; - } - print(asTable(saves)); + print(asTable(saves.map( + ({ mtimeMs, ...rest }) => ({ ...rest, mtime: new Date(mtimeMs).toLocaleString() }) + ))); }, })); @@ -531,7 +539,17 @@ instanceSaveCommands.add(new lib.Command({ "map-settings": { describe: "path to file to use for map-settings", nargs: 1, type: "string" }, }); }], - handler: async function(args, control) { + handler: async function( + args: { + instance: string, + name: string, + seed?: number, + mapExchangeString?: string, + mapGenSettings?: string, + mapSettings?: string, + }, + control: Control + ) { let instanceId = await lib.resolveInstance(control, args.instance); let { seed, mapGenSettings, mapSettings } = await loadMapSettings(args); await control.setLogSubscriptions({ instanceIds: [instanceId] }); @@ -548,7 +566,7 @@ instanceSaveCommands.add(new lib.Command({ yargs.positional("old-name", { describe: "Old name of save.", type: "string" }); yargs.positional("new-name", { describe: "New name of save.", type: "string" }); }], - handler: async function(args, control) { + handler: async function(args: { instance: string, oldName: string, newName: string }, control: Control) { let instanceId = await lib.resolveInstance(control, args.instance); await control.send(new lib.InstanceRenameSaveRequest(instanceId, args.oldName, args.newName)); }, @@ -560,7 +578,7 @@ instanceSaveCommands.add(new lib.Command({ yargs.positional("source", { describe: "Save to copy.", type: "string" }); yargs.positional("destination", { describe: "Name of copy.", type: "string" }); }], - handler: async function(args, control) { + handler: async function(args: { instance: string, source: string, destination: string }, control: Control) { let instanceId = await lib.resolveInstance(control, args.instance); await control.send(new lib.InstanceCopySaveRequest(instanceId, args.source, args.destination)); }, @@ -574,7 +592,7 @@ instanceSaveCommands.add(new lib.Command({ "name": { describe: "Name to give save on server", nargs: 1, type: "string" }, }); }], - handler: async function(args, control) { + handler: async function(args: { instance: string, filepath: string, name?: string }, control: Control) { let filename = args.name || path.basename(args.filepath); if (!filename.endsWith(".zip")) { throw new lib.CommandError("Save name must end with .zip"); @@ -583,18 +601,18 @@ instanceSaveCommands.add(new lib.Command({ let content = await fs.readFile(args.filepath); let instanceId = await lib.resolveInstance(control, args.instance); - let url = new URL(control.config.get("control.controller_url")); + let url = new URL(control.config.get("control.controller_url") as string); url.pathname += "api/upload-save"; - url.searchParams.append("instance_id", instanceId); + url.searchParams.append("instance_id", String(instanceId)); url.searchParams.append("filename", filename); - let result = await phin({ + let result = await phin<{ errors?: string[], request_errors?: string[], saves?: string[] }>({ url, method: "POST", headers: { "X-Access-Token": control.config.get("control.controller_token"), "Content-Type": "application/zip", }, - core: { ca: control.tlsCa }, + core: { ca: control.tlsCa } as {}, data: content, parse: "json", }); @@ -631,7 +649,16 @@ instanceSaveCommands.add(new lib.Command({ }); }, ], - handler: async function(args, control) { + handler: async function( + args: { + sourceInstance: string, + sourceSave: string, + targetInstance: string, + targetSave: string, + copy: boolean, + }, + control: Control + ) { let sourceInstanceId = await lib.resolveInstance(control, args.sourceInstance); let targetInstanceId = await lib.resolveInstance(control, args.targetInstance); let storedName = await control.send( @@ -652,15 +679,15 @@ instanceSaveCommands.add(new lib.Command({ yargs.positional("instance", { describe: "Instance to download save from", type: "string" }); yargs.positional("save", { describe: "Save to download", type: "string" }); }], - handler: async function(args, control) { + handler: async function(args: { instance: string, save: string }, control: Control) { let instanceId = await lib.resolveInstance(control, args.instance); let streamId = await control.send(new lib.InstanceDownloadSaveRequest(instanceId, args.save)); - let url = new URL(control.config.get("control.controller_url")); + let url = new URL(control.config.get("control.controller_url") as string); url.pathname += `api/stream/${streamId}`; let response = await phin({ url, method: "GET", - core: { ca: control.tlsCa }, + core: { ca: control.tlsCa } as {}, stream: true, }); @@ -671,7 +698,7 @@ instanceSaveCommands.add(new lib.Command({ writeStream = fs.createWriteStream(tempFilename, { flags: "wx" }); await events.once(writeStream, "open"); break; - } catch (err) { + } catch (err: any) { if (err.code === "EEXIST") { tempFilename = await lib.findUnusedName(".", tempFilename, ".tmp.zip"); } else { @@ -694,7 +721,7 @@ instanceSaveCommands.add(new lib.Command({ yargs.positional("instance", { describe: "Instance to delete save from", type: "string" }); yargs.positional("save", { describe: "Save to delete", type: "string" }); }], - handler: async function(args, control) { + handler: async function(args: { instance: string, save: string }, control: Control) { let instanceId = await lib.resolveInstance(control, args.instance); await control.send(new lib.InstanceDeleteSaveRequest(instanceId, args.save)); }, @@ -705,7 +732,7 @@ instanceCommands.add(new lib.Command({ definition: ["export-data ", "Export item icons and locale from instance", (yargs) => { yargs.positional("instance", { describe: "Instance to export from", type: "string" }); }], - handler: async function(args, control) { + handler: async function(args: { instance: string }, control: Control) { let instanceId = await lib.resolveInstance(control, args.instance); await control.setLogSubscriptions({ instanceIds: [instanceId] }); await control.sendTo({ instanceId }, new lib.InstanceExportDataRequest()); @@ -716,7 +743,7 @@ instanceCommands.add(new lib.Command({ definition: ["extract-players ", "Extract players from running save into the cluster.", (yargs) => { yargs.positional("instance", { describe: "Instance to extract players and online time from", type: "string" }); }], - handler: async function(args, control) { + handler: async function(args: { instance: string }, control: Control) { let instanceId = await lib.resolveInstance(control, args.instance); await control.setLogSubscriptions({ instanceIds: [instanceId] }); await control.sendTo({ instanceId }, new lib.InstanceExtractPlayersRequest()); @@ -731,7 +758,7 @@ instanceCommands.add(new lib.Command({ "keep-open": { describe: "Keep console open", nargs: 0, type: "boolean", default: false }, }); }], - handler: async function(args, control) { + handler: async function(args: { instance: string, save?: string, keepOpen: boolean }, control: Control) { let instanceId = await lib.resolveInstance(control, args.instance); await control.setLogSubscriptions({ instanceIds: [instanceId] }); await control.sendTo({ instanceId }, new lib.InstanceStartRequest(args.save)); @@ -751,7 +778,17 @@ instanceCommands.add(new lib.Command({ "keep-open": { describe: "Keep console open", nargs: 0, type: "boolean", default: false }, }); }], - handler: async function(args, control) { + handler: async function( + args: { + instance: string, + scenario: string, + seed?: number, + mapExchangeString?: string, + mapGenSettings?: string, + mapSettings?: string, + keepOpen: boolean, + }, + control: Control) { let instanceId = await lib.resolveInstance(control, args.instance); await control.setLogSubscriptions({ instanceIds: [instanceId] }); let { seed, mapGenSettings, mapSettings } = await loadMapSettings(args); @@ -767,10 +804,10 @@ instanceCommands.add(new lib.Command({ definition: ["stop ", "Stop instance", (yargs) => { yargs.positional("instance", { describe: "Instance to stop", type: "string" }); }], - handler: async function(args, control) { + handler: async function(args: { instance: string }, control: Control) { let instanceId = await lib.resolveInstance(control, args.instance); await control.setLogSubscriptions({ instanceIds: [instanceId] }); - await control.sendTo({ instanceId }, new lib.InstanceStopRequest(args.save)); + await control.sendTo({ instanceId }, new lib.InstanceStopRequest()); }, })); @@ -778,10 +815,10 @@ instanceCommands.add(new lib.Command({ definition: ["kill ", "Kill instance", (yargs) => { yargs.positional("instance", { describe: "Instance to kill", type: "string" }); }], - handler: async function(args, control) { + handler: async function(args: { instance: string }, control: Control) { let instanceId = await lib.resolveInstance(control, args.instance); await control.setLogSubscriptions({ instanceIds: [instanceId] }); - await control.sendTo({ instanceId }, new lib.InstanceKillRequest(args.save)); + await control.sendTo({ instanceId }, new lib.InstanceKillRequest()); }, })); @@ -789,7 +826,7 @@ instanceCommands.add(new lib.Command({ definition: ["delete ", "Delete instance", (yargs) => { yargs.positional("instance", { describe: "Instance to delete", type: "string" }); }], - handler: async function(args, control) { + handler: async function(args: { instance: string }, control: Control) { let instanceId = await lib.resolveInstance(control, args.instance); await control.send(new lib.InstanceDeleteRequest(instanceId)); }, @@ -800,7 +837,7 @@ instanceCommands.add(new lib.Command({ yargs.positional("instance", { describe: "Instance to send to", type: "string" }); yargs.positional("command", { describe: "command to send", type: "string" }); }], - handler: async function(args, control) { + handler: async function(args: { instance: string, command: string }, control: Control) { let result = await control.sendTo( { instanceId: await lib.resolveInstance(control, args.instance) }, new lib.InstanceSendRconRequest(args.command), @@ -816,7 +853,7 @@ modPackCommands.add(new lib.Command({ definition: ["show ", "Show details of mod pack", (yargs) => { yargs.positional("mod-pack", { describe: "Mod pack to show", type: "string" }); }], - handler: async function(args, control) { + handler: async function(args: { modPack: string }, control: Control) { let mods = new Map( (await control.send(new lib.ModListRequest())).map(m => [`${m.name}_${m.version}`, m]) ); @@ -839,7 +876,7 @@ modPackCommands.add(new lib.Command({ print(`${field}:`); for (let [scope, settings] of Object.entries(value)) { print(` ${scope}:`); - for (let [setting, settingValue] of settings) { + for (let [setting, settingValue] of settings as any) { print(` ${setting}: ${JSON.stringify(settingValue.value)}`); } } @@ -861,12 +898,13 @@ modPackCommands.add(new lib.Command({ modPackCommands.add(new lib.Command({ definition: [["list", "l"], "List mod packs in the cluster"], - handler: async function(args, control) { + handler: async function(args: {}, control: Control) { let modPacks = await control.send(new lib.ModPackListRequest()); let fields = ["id", "name", "factorioVersion"]; for (let entry of modPacks) { for (let field of Object.keys(entry)) { if (!fields.includes(field)) { + // @ts-ignore delete entry[field]; } } @@ -875,8 +913,11 @@ modPackCommands.add(new lib.Command({ }, })); -function setModPackSettings(modPack, args) { - function doSettings(settings, cast) { +function setModPackSettings( + modPack: lib.ModPack, + args: { boolSetting?: string[], intSetting?: string[], doubleSetting?: string[], stringSetting?: string[] }, +) { + function doSettings(settings: string[], cast: (value: string) => (string | number | boolean)) { for (let i = 0; i + 2 < settings.length; i += 3) { const [scope, name, value] = settings.slice(i, i + 3); if (!["startup", "runtime-global", "runtime-per-user"].includes(scope)) { @@ -884,7 +925,8 @@ function setModPackSettings(modPack, args) { `Setting scope must be one of startup, runtime-gloabl, or runtime-per-user, not ${scope}` ); } - modPack.settings[scope].set(name, { value: cast(value) }); + const checkedScope = scope as "startup" | "runtime-global" | "runtime-per-user"; + modPack.settings[checkedScope].set(name, { value: cast(value) }); } } @@ -912,7 +954,7 @@ function setModPackSettings(modPack, args) { doSettings(args.stringSetting || [], value => value); } -function setModPackMods(modPack, mods) { +function setModPackMods(modPack: lib.ModPack, mods: string[] | undefined) { for (let mod of mods || []) { const [name, version, sha1] = mod.split(":"); if (!version) { @@ -928,12 +970,12 @@ function setModPackMods(modPack, mods) { } } -function setModPackModsEnabled(modPack, mods, enabled) { +function setModPackModsEnabled(modPack: lib.ModPack, mods: string[] | undefined, enabled: boolean) { for (let mod of mods || []) { if (!modPack.mods.has(mod)) { throw new lib.CommandError(`Mod named ${mod} does not exist in the mod pack`); } - modPack.mods.get(mod).enabled = enabled; + modPack.mods.get(mod)!.enabled = enabled; } } @@ -951,8 +993,21 @@ modPackCommands.add(new lib.Command({ "string-setting": { describe: "Set string setting", array: true, nargs: 3, type: "string" }, }); }], - handler: async function(args, control) { - const modPack = lib.ModPack.fromJSON({}); + handler: async function( + args: { + name: string, + factorioVersion: string, + description?: string, + mods?: string[], + disabledMods?: string[], + boolSetting?: string[], + intSetting?: string[], + doubleSetting?: string[], + stringSetting?: string[], + }, + control: Control, + ) { + const modPack = new lib.ModPack(); modPack.name = args.name; if (args.description) { modPack.description = args.description; } if (args.factorioVersion) { @@ -973,7 +1028,7 @@ modPackCommands.add(new lib.Command({ definition: ["import ", "Import mod pack string", (yargs) => { yargs.positional("string", { describe: "Mod pack string to import", type: "string" }); }], - handler: async function(args, control) { + handler: async function(args: { string: string }, control: Control) { const modPack = lib.ModPack.fromModPackString(args.string); await control.send(new lib.ModPackCreateRequest(modPack)); print(`Created mod pack ${modPack.name} (${modPack.id})`); @@ -984,7 +1039,7 @@ modPackCommands.add(new lib.Command({ definition: ["export ", "Export mod pack string", (yargs) => { yargs.positional("string", { describe: "Mod pack to export", type: "string" }); }], - handler: async function(args, control) { + handler: async function(args: { modPack: string }, control: Control) { const modPack = await control.send( new lib.ModPackGetRequest(await lib.resolveModPack(control, args.modPack)) ); @@ -1010,7 +1065,24 @@ modPackCommands.add(new lib.Command({ "remove-setting": { describe: "Remove a setting", array: true, nargs: 2, type: "string" }, }); }], - handler: async function(args, control) { + handler: async function( + args: { + modPack: string, + name?: string, + description?: string, + factorioVersion?: string, + addMods?: string[], + enableMods?: string[], + disableMods?: string[], + removeMods?: string[], + boolSetting?: string[], + intSetting?: string[], + doubleSetting?: string[], + stringSetting?: string[], + removeSetting?: string[], + }, + control: Control, + ) { const modPack = await control.send( new lib.ModPackGetRequest(await lib.resolveModPack(control, args.modPack)) ); @@ -1038,14 +1110,15 @@ modPackCommands.add(new lib.Command({ setModPackSettings(modPack, args); if (args.removeSetting) { for (let i = 0; i + 1 < args.removeSetting.length; i += 2) { - const [scope, name] = settings.slice(i, i + 2); + const [scope, name] = args.removeSetting.slice(i, i + 2); if (!["startup", "runtime-global", "runtime-per-user"].includes(scope)) { throw new lib.CommandError( `Setting scope must be one of startup, runtime-gloabl, or runtime-per-user, not ${scope}` ); } - if (modPack.settings[scope].has(name)) { - modPack.settings[scope].delete(name); + const checkedScope = scope as "startup" | "runtime-global" | "runtime-per-user"; + if (modPack.settings[checkedScope].has(name)) { + modPack.settings[checkedScope].delete(name); } else { logger.warn(`Mod setting ${scope} ${name} did not exist on ${modPack.name}`); } @@ -1059,7 +1132,7 @@ modPackCommands.add(new lib.Command({ definition: ["delete ", "Delete mod pack", (yargs) => { yargs.positional("mod-pack", { describe: "Mod pack to delete", type: "string" }); }], - handler: async function(args, control) { + handler: async function(args: { modPack: string }, control: Control) { const id = await lib.resolveModPack(control, args.modPack); await control.send(new lib.ModPackDeleteRequest(id)); }, @@ -1071,7 +1144,7 @@ modCommands.add(new lib.Command({ yargs.positional("name", { describe: "Mod name to show details for", type: "string" }); yargs.positional("mod-version", { describe: "Version of the mod", type: "string" }); }], - handler: async function(args, control) { + handler: async function(args: { name: string, modVersion: string }, control: Control) { let modInfo = await control.send(new lib.ModGetRequest(args.name, args.modVersion)); for (let [field, value] of Object.entries(modInfo)) { if (value instanceof Array) { @@ -1097,12 +1170,13 @@ modCommands.add(new lib.Command({ }, }); }], - handler: async function(args, control) { + handler: async function(args: { fields: string[] }, control: Control) { let mods = await control.send(new lib.ModListRequest()); if (!args.fields.includes("all")) { for (let entry of mods) { for (let field of Object.keys(entry)) { if (!args.fields.includes(field)) { + // @ts-ignore delete entry[field]; } } @@ -1144,7 +1218,18 @@ modCommands.add(new lib.Command({ }, }); }], - handler: async function(args, control) { + handler: async function( + args: { + factorioVersion: string, + query: string, + page: number, + pageSize: number, + sort?: string, + sortOrder: "asc" | "desc", + fields: string[], + }, + control: Control + ) { let response = await control.send(new lib.ModSearchRequest( args.query, args.factorioVersion, @@ -1158,6 +1243,7 @@ modCommands.add(new lib.Command({ for (let entry of results) { for (let field of Object.keys(entry)) { if (!args.fields.includes(field)) { + // @ts-ignore delete entry[field]; } } @@ -1175,7 +1261,7 @@ modCommands.add(new lib.Command({ definition: ["upload ", "Upload mod to the cluster", (yargs) => { yargs.positional("file", { describe: "File to upload", type: "string" }); }], - handler: async function(args, control) { + handler: async function(args: { file: string }, control: Control) { let filename = path.basename(args.file); if (!filename.endsWith(".zip")) { throw new lib.CommandError("Mod filename must end with .zip"); @@ -1183,17 +1269,19 @@ modCommands.add(new lib.Command({ // phin doesn't support streaming requests :( let content = await fs.readFile(args.file); - let url = new URL(control.config.get("control.controller_url")); + let url = new URL(control.config.get("control.controller_url") as string); url.pathname += "api/upload-mod"; url.searchParams.append("filename", filename); - let result = await phin({ + let result = await phin< + { errors?: [], request_errors?: [], mods: Parameters[0][]} + >({ url, method: "POST", headers: { "X-Access-Token": control.config.get("control.controller_token"), "Content-Type": "application/zip", }, - core: { ca: control.tlsCa }, + core: { ca: control.tlsCa } as {}, data: content, parse: "json", }); @@ -1222,14 +1310,14 @@ modCommands.add(new lib.Command({ yargs.positional("name", { describe: "Internal name of mod to download", type: "string" }); yargs.positional("mod-version", { describe: "Version of mod to download", type: "string" }); }], - handler: async function(args, control) { + handler: async function(args: { name: string, modVersion: string }, control: Control) { let streamId = await control.send(new lib.ModDownloadRequest(args.name, args.modVersion)); - let url = new URL(control.config.get("control.controller_url")); + let url = new URL(control.config.get("control.controller_url") as string); url.pathname += `api/stream/${streamId}`; let response = await phin({ url, method: "GET", - core: { ca: control.tlsCa }, + core: { ca: control.tlsCa } as {}, stream: true, }); @@ -1241,7 +1329,7 @@ modCommands.add(new lib.Command({ writeStream = fs.createWriteStream(tempFilename, { flags: "wx" }); await events.once(writeStream, "open"); break; - } catch (err) { + } catch (err: any) { if (err.code === "EEXIST") { tempFilename = await lib.findUnusedName(".", tempFilename, ".tmp.zip"); } else { @@ -1262,7 +1350,7 @@ modCommands.add(new lib.Command({ yargs.positional("name", { describe: "Name of mod to delete", type: "string" }); yargs.positional("mod-version", { describe: "Version of mod to delete", type: "string" }); }], - handler: async function(args, control) { + handler: async function(args: { name: string, modVersion: string }, control: Control) { await control.send(new lib.ModDeleteRequest(args.name, args.modVersion)); }, })); @@ -1270,7 +1358,7 @@ modCommands.add(new lib.Command({ const permissionCommands = new lib.CommandTree({ name: "permission", description: "Permission inspection" }); permissionCommands.add(new lib.Command({ definition: [["list", "l"], "List permissions in the cluster"], - handler: async function(args, control) { + handler: async function(args: {}, control: Control) { let permissions = await control.send(new lib.PermissionListRequest()); print(asTable(permissions)); }, @@ -1280,7 +1368,7 @@ permissionCommands.add(new lib.Command({ const roleCommands = new lib.CommandTree({ name: "role", description: "Role management" }); roleCommands.add(new lib.Command({ definition: [["list", "l"], "List roles in the cluster"], - handler: async function(args, control) { + handler: async function(args: {}, control: Control) { let roles = await control.send(new lib.RoleListRequest()); print(asTable(roles)); }, @@ -1294,7 +1382,7 @@ roleCommands.add(new lib.Command({ "permissions": { describe: "Permissions role grants", nargs: 1, array: true, type: "string", default: [] }, }); }], - handler: async function(args, control) { + handler: async function(args: { name: string, description: string, permissions: string[] }, control: Control) { let id = await control.send(new lib.RoleCreateRequest( args.name, args.description, @@ -1318,7 +1406,18 @@ roleCommands.add(new lib.Command({ "grant-default": { describe: "Add default permissions to role", nargs: 0, type: "boolean" }, }); }], - handler: async function(args, control) { + handler: async function( + args: { + role: string, + name?: string, + description?: string, + setPerms?: string[], + addPerms?: string[], + removePerms?: string[], + grantDefault?: boolean, + }, + control: Control + ) { let role = await lib.retrieveRole(control, args.role); if (args.name !== undefined) { @@ -1352,7 +1451,7 @@ roleCommands.add(new lib.Command({ definition: ["delete ", "Delete role", (yargs) => { yargs.positional("role", { describe: "Role to delete", type: "string" }); }], - handler: async function(args, control) { + handler: async function(args: { role: string }, control: Control) { let role = await lib.retrieveRole(control, args.role); await control.send(new lib.RoleDeleteRequest(role.id)); }, @@ -1367,7 +1466,7 @@ userCommands.add(new lib.Command({ "instance-stats": { describe: "include per-instance stats", nargs: 0, type: "boolean", default: false }, }); }], - handler: async function(args, control) { + handler: async function(args: { name: string, instanceStats: boolean }, control: Control) { let user = await control.send(new lib.UserGetRequest(args.name)); Object.assign(user, user.playerStats); delete user.playerStats; @@ -1377,14 +1476,14 @@ userCommands.add(new lib.Command({ if (args.instanceStats) { let instances = await control.send(new lib.InstanceDetailsListRequest()); - function instanceName(id) { + function instanceName(id: number) { let instance = instances.find(i => i.id === id); if (instance) { return instance.name; } return ""; } - for (let [id, playerInstanceStats] of instanceStats) { + for (let [id, playerInstanceStats] of instanceStats || []) { print(); print(`Instance ${instanceName(id)} (${id}):`); print(asTable(Object.entries(playerInstanceStats).map(([property, value]) => ({ property, value })))); @@ -1400,7 +1499,7 @@ userCommands.add(new lib.Command({ "attributes": { describe: "include admin/whitelisted/banned", nargs: 0, type: "boolean", default: false }, }); }], - handler: async function(args, control) { + handler: async function(args: { stats: boolean, attributes: boolean }, control: Control) { let users = await control.send(new lib.UserListRequest()); for (let user of users) { if (args.stats) { @@ -1424,7 +1523,7 @@ userCommands.add(new lib.Command({ definition: ["create ", "Create a user", (yargs) => { yargs.positional("name", { describe: "Name of user to create", type: "string" }); }], - handler: async function(args, control) { + handler: async function(args: { name: string }, control: Control) { await control.send(new lib.UserCreateRequest(args.name)); }, })); @@ -1433,7 +1532,7 @@ userCommands.add(new lib.Command({ definition: ["revoke-token ", "Revoke token for user", (yargs) => { yargs.positional("name", { describe: "Name of user to revoke token for", type: "string" }); }], - handler: async function(args, control) { + handler: async function(args: { name: string }, control: Control) { await control.send(new lib.UserRevokeTokenRequest(args.name)); }, })); @@ -1446,7 +1545,7 @@ userCommands.add(new lib.Command({ "create": { describe: "Create user if it does not exist", nargs: 0, type: "boolean", default: false }, }); }], - handler: async function(args, control) { + handler: async function(args: { user: string, revoke: boolean, create: boolean }, control: Control) { await control.send(new lib.UserSetAdminRequest(args.user, args.create, !args.revoke)); }, })); @@ -1459,7 +1558,7 @@ userCommands.add(new lib.Command({ "create": { describe: "Create user if it does not exist", nargs: 0, type: "boolean", default: false }, }); }], - handler: async function(args, control) { + handler: async function(args: { user: string, remove: boolean, create: boolean }, control: Control) { await control.send(new lib.UserSetWhitelistedRequest(args.user, args.create, !args.remove)); }, })); @@ -1473,7 +1572,15 @@ userCommands.add(new lib.Command({ "create": { describe: "Create user if it does not exist", nargs: 0, type: "boolean", default: false }, }); }], - handler: async function(args, control) { + handler: async function( + args: { + user: string, + pardon: boolean, + reason: string, + create: boolean + }, + control: Control + ) { await control.send(new lib.UserSetBannedRequest(args.user, args.create, !args.pardon, args.reason)); }, })); @@ -1483,7 +1590,7 @@ userCommands.add(new lib.Command({ yargs.positional("user", { describe: "Name of user to change roles for", type: "string" }); yargs.positional("roles", { describe: "roles to assign", type: "string" }); }], - handler: async function(args, control) { + handler: async function(args: { user: string, roles: string[] }, control: Control) { let roles = await control.send(new lib.RoleListRequest()); let resolvedRoles = []; @@ -1516,7 +1623,7 @@ userCommands.add(new lib.Command({ definition: ["delete ", "Delete user", (yargs) => { yargs.positional("user", { describe: "Name of user to delete", type: "string" }); }], - handler: async function(args, control) { + handler: async function(args: { user: string }, control: Control) { await control.send(new lib.UserDeleteRequest(args.user)); }, })); @@ -1531,7 +1638,10 @@ logCommands.add(new lib.Command({ "instance": { describe: "Follow log of given instance", nargs: 1, type: "string", default: null }, }); }], - handler: async function(args, control) { + handler: async function( + args: { all: boolean, controller: boolean, host: string | null, instance: string | null }, + control: Control + ) { if (!args.all && !args.controller && !args.host && !args.instance) { logger.error("At least one of --all, --controller, --host and --instance must be passed"); process.exitCode = 1; @@ -1556,7 +1666,18 @@ logCommands.add(new lib.Command({ "start": { describe: "Limit from the start instead of the end", nargs: 0, type: "boolean", default: false }, }); }], - handler: async function(args, control) { + handler: async function( + args: { + all: boolean, + controller: boolean, + host: string | null, + instance: string | null, + maxLevel?: string, + limit: number, + start: boolean, + }, + control: Control + ) { if (!args.all && !args.controller && !args.host && !args.instance) { logger.error("At least one of --all, --controller, --host and --instance must be passed"); process.exitCode = 1; @@ -1569,7 +1690,7 @@ logCommands.add(new lib.Command({ args.controller, hostIds, instanceIds, - args.maxLevel, + args.maxLevel as keyof typeof levels, args.limit, args.start ? "asc" : "desc", )); @@ -1587,21 +1708,21 @@ logCommands.add(new lib.Command({ ], }); for (let info of result.log) { - stdoutLogger.log(info); + stdoutLogger.log(info as any); } }, })); const debugCommands = new lib.CommandTree({ name: "debug", description: "Debugging utilities" }); debugCommands.add(new lib.Command({ - definition: ["dump-ws", "Dump WebSocket messages sent and received by controller", (yargs) => { }], - handler: async function(args, control) { + definition: ["dump-ws", "Dump WebSocket messages sent and received by controller"], + handler: async function(args: {}, control: Control) { await control.send(new lib.DebugDumpWsRequest()); control.keepOpen = true; }, })); -async function registerCommands(controlPlugins, yargs) { +export async function registerCommands(controlPlugins: Map, yargs: Argv) { const rootCommands = new lib.CommandTree({ name: "clusterioctl", description: "Manage cluster" }); rootCommands.add(controllerCommands); rootCommands.add(hostCommands); @@ -1626,7 +1747,3 @@ async function registerCommands(controlPlugins, yargs) { return rootCommands; } - -module.exports = { - registerCommands, -}; diff --git a/packages/ctl/tsconfig.json b/packages/ctl/tsconfig.json new file mode 100644 index 00000000..b3104ba5 --- /dev/null +++ b/packages/ctl/tsconfig.json @@ -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 + } +} diff --git a/test/ctl.js b/test/ctl.js deleted file mode 100644 index 8525f83a..00000000 --- a/test/ctl.js +++ /dev/null @@ -1,8 +0,0 @@ -"use strict"; -const assert = require("assert").strict; - -const clusterioctl = require("@clusterio/ctl/ctl.js"); - - -describe("clusterioctl", function() { -}); diff --git a/test/integration/clusterio.js b/test/integration/clusterio.js index 56620cd2..864a432f 100644 --- a/test/integration/clusterio.js +++ b/test/integration/clusterio.js @@ -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";