mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 23:04:52 +09:00
Merge pull request #494 from Hornwitser/typescript-ctl
Port ctl package to Typescript
This commit is contained in:
13 files changed
+408
-283
No files matched your search
@@ -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
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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({
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
"use strict";
|
||||
const assert = require("assert").strict;
|
||||
|
||||
const clusterioctl = require("@clusterio/ctl/ctl.js");
|
||||
|
||||
|
||||
describe("clusterioctl", function() {
|
||||
});
|
||||
@@ -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";
|
||||
|
||||
Reference in new issue
Block a user