Compare commits

..
10 Commits
Author SHA1 Message Date
Arvids Godjuks ec51adc71c Prepare Clusterio 2.0.2 release (#1089) 2026-10-15 15:31:49 +03:00
Arvids Godjuks 7467fa6b8f Refresh installation and stable release documentation (#1086)
Refresh installation and stable release documentation
2026-10-15 15:21:35 +03:00
Arvids Godjuks 5ca61414b3 Add Node.js 26 to the CI test matrix (#1088)
* Add Node.js 26 to the CI test matrix
2026-10-15 14:17:08 +03:00
Arvids Godjuks 40249d83c8 Make player auth expiry tests independent of wall clock timing (#1087) 2026-10-15 13:10:06 +03:00
BastiaanandClaude Opus 5.5 6f6795dc1f Pass the event name to web controllerConnectionEvent handlers (#1085)
The web interface registered the hook's listener directly on the
connector, so handlers got the connector's emit arguments (the hello
data for connect, nothing for the rest) instead of the event name.

Fixes #1083

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 09:56:09 +01:00
Arvids Godjuks e0755cb4ee Fix installer plugins by relying on automatic discovery (#1082)
* Rely on automatic discovery for installer plugins and document plugin package discovery requirements
2026-10-15 00:12:12 +03:00
Arvids Godjuks 71feb86ce8 Release Clusterio 2.0.1 (#1081)
* Bump npm packages to 2.0.1

* Add changelog for Clusterio 2.0.1
2026-10-14 23:04:35 +03:00
Arvids Godjuks 346e1acc97 Fix Factorio mod release uploads with native fetch (#1080)
Fix Factorio mod release uploads with native fetch
2026-10-14 22:50:55 +03:00
Arvids Godjuks 46e6445973 Remove obsolete alpha labels from Clusterio Library mod (#1079) 2026-10-14 22:35:58 +03:00
Arvids Godjuks 28eb3a07e0 Remove obsolete 2.0 development warnings (#1078) 2026-10-14 22:24:55 +03:00
35 changed files with 442 additions and 171 deletions

No files matched your search

+4 -3
View File
@@ -17,8 +17,9 @@ jobs:
strategy:
fail-fast: false
matrix:
# Supported node versions are active lts and maintenance see https://nodejs.org/en/about/releases/
node-version: [24.x]
# Test Node.js 24 and 26 across the Factorio matrix; Node.js 22 runs coverage below.
# Node.js release schedule: https://nodejs.org/en/about/releases/
node-version: [24.x, 26.x]
# Supported factorio versions are latest stable and one minor version before stable (even if from a previous major)
factorio-version: [1.1.110, 2.0.77, 2.1.17]
@@ -86,7 +87,7 @@ jobs:
node scripts/version-compat-test.js
--controller ${{ matrix.controller }}
--host ${{ matrix.host }}
--published-version $PUBLISHED_VERSION
--published-version "$PUBLISHED_VERSION"
lint:
runs-on: ubuntu-latest
+28 -1
View File
@@ -31,6 +31,33 @@ Many thanks to the following for contributing to this release:
[@username](https://github.com/username)
-->
## Version 2.0.2
### Fixes
- Fixed installation with selected plugins by relying on automatic plugin discovery, and ensured installer failures return a nonzero exit status. [#1082](<https://github.com/clusterio/clusterio/pull/1082>), [#1084](<https://github.com/clusterio/clusterio/issues/1084>)
- Fixed web plugin `controllerConnectionEvent` handlers receiving the connector's event data instead of the event name. [#1083](<https://github.com/clusterio/clusterio/issues/1083>)
- Updated installation instructions to recommend latest Node.js LTS and corrected outdated mod management, CLI, plugin, and release documentation. [#1086](<https://github.com/clusterio/clusterio/pull/1086>)
### Meta
- Made player-auth login tests independent of wall-clock timing and added verification-token expiry coverage. [#1087](<https://github.com/clusterio/clusterio/pull/1087>)
- Added Node.js 26 to the CI test matrix across all tested Factorio versions. [#1088](<https://github.com/clusterio/clusterio/pull/1088>)
Many thanks to the following for contributing to this release:
[@psihius](<https://github.com/psihius>), [@bbassie](<https://github.com/bbassie>)
## Version 2.0.1
### Fixes
- Removed obsolete development-branch warnings from controller, host and ctl startup. [#1078](<https://github.com/clusterio/clusterio/pull/1078>)
- Removed the obsolete alpha label and development warning from the Clusterio Library mod title and description. [#1079](<https://github.com/clusterio/clusterio/pull/1079>)
- Fixed Factorio mod release uploads to use valid multipart requests and files from the requested output directory, uploading variants in version order. [#1080](<https://github.com/clusterio/clusterio/pull/1080>)
Many thanks to the following for contributing to this release:
[@psihius](<https://github.com/psihius>)
## Version 2.0.0
### Features
@@ -662,7 +689,7 @@ Many thanks to the following for contributing to this release:
## Version 2.0.0-alpha.14
This release does not retain backwards compatibility with 2.0.0-alpha.13 meaning all parts of the cluster needs to updated at the same time.
See the [migration guide](/docs/migrating-from-2.0.0-alpha.13.md) for how to update.
See the [migration guide](https://github.com/clusterio/clusterio/blob/e0755cb4ee7bce0068c844ab6271da481a843e62/docs/migrating-from-2.0.0-alpha.13.md) for how to update.
### Major Features
+21 -20
View File
@@ -11,6 +11,7 @@ Discord for development/support/play: https://discord.gg/5XuDkje
## Important notice
Clusterio 2.0 is the current stable release and requires Node.js 22 or newer.
For new installations, use the latest Node.js LTS release (see [Installation](#installation)).
The previous stable series is no longer supported.
If you are upgrading from a 2.0 alpha, review the [changelog](CHANGELOG.md) before upgrading.
Plugin authors should also follow the [2.0 migration guide](docs/devs/plugin-migration-to-2.0.md).
@@ -41,7 +42,7 @@ Plugin authors should also follow the [2.0 migration guide](docs/devs/plugin-mig
* [Setting up remote hosts](#setting-up-remote-hosts)
* [Setting up clusterioctl](#setting-up-clusterioctl)
* [Known issues](#known-issues)
* [Common problems](#Common-problems)
* [Common problems](#common-problems)
## Introduction
@@ -96,8 +97,9 @@ Note that from 2.0 onwards minor versions of Clusterio may contain breaking chan
## Installation
Clusterio runs on all LTS version of NodeJS, it is distributed via npm and comes with a guided install script.
If you already have a recent Node.js installed, you can set it up in a new directory with:
Install the latest [Node.js LTS release](https://nodejs.org/en/download), which is recommended for new Clusterio installations.
Clusterio is distributed via npm and comes with a guided install script.
If you already have a supported Node.js version installed, you can set it up in a new directory with:
npm init "@clusterio"
@@ -106,12 +108,14 @@ Check out all our of decisions for [node, factorio, and operation support](/docs
### Ubuntu setup
1. Install Node.js v18 or higher.
Note the version of Node.js provided by the Ubuntu/debian repos are too old and you will have to use the nodesource PPA
1. Install the latest Node.js LTS release using the NodeSource repository for Ubuntu/Debian.
The distribution's default repository may provide an older version.
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - &&\
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - &&\
sudo apt-get install -y nodejs
Verify the installation with `node --version` and `npm --version`.
2. Log in as the user you wish to install Clusterio as (do not use root), and then create a new directory and run the Clusterio installer:
mkdir clusterio
@@ -143,7 +147,7 @@ Your instances (save files etc) will be stored there.
### Windows setup
1. Download and install the latest LTS release from http://nodejs.org.
1. Download and install the latest [Node.js LTS release](https://nodejs.org/en/download).
2. Create a new empty directory for the installation and navigate into it.
Open a PowerShell window in this new directory by Shift+Right clicking inside it and choosing "Open PowerShell window here" and then run the following command.
@@ -157,7 +161,8 @@ Your instances (save files etc) will be stored there.
### MacOS Setup
1. Install the latest Node.js LTS release from http://nodejs.org or use brew (`brew install node`).
1. Install the latest [Node.js LTS release](https://nodejs.org/en/download).
If you use [nvm](https://github.com/nvm-sh/nvm#installing-and-updating), run `nvm install --lts`.
2. Open Terminal or Command prompt in the directory you want to install to and run the following commands.
@@ -211,7 +216,11 @@ See the [readme for @clusterio/controller](/packages/controller/README.md) for m
## Managing Factorio mods
Mods in clusterio are managed as parts of modpacks. There is currently no mod portal integration, so you first need to navigate to the mods section of the web UI and upload your mods. You can then create a modpack from the uploaded mods by navigating to the mods page, hitting `create`. Once you have a modpack, you can assign it to an instance under Instance config -> Factorio -> Mod pack. You can also set a default modpack for new instances in the controller settings.
Mods in Clusterio are managed as parts of mod packs.
On the Mods page of the web interface, use **Search** to browse the Factorio Mod Portal and download mods to the controller, or upload mod zip files directly.
Mod Portal downloads require the controller's [Factorio account credentials](docs/configuration.md#controllerfactorio_username) to be configured.
Use **Create** in the Mod Packs section to create a mod pack, then add the mods you want to use.
Assign the mod pack to an instance under Instance config -> Factorio -> Mod pack, or set a default mod pack for new instances in the controller settings.
![Select mod pack for instance](docs/assets/config_select_mod_pack.png)
@@ -227,7 +236,9 @@ Once this is done, the mod settings will show up under the modpack configuration
## Setting up shared storage
The chests teleporting items to and from the shared storage on the controller has been moved into the Subspace Storage mod and plugin.
To get this to work you will need to install the Subspace Storage plugin and copy the [Subspace Storage mod](https://mods.factorio.com/mod/subspace_storage) and the [Clusterio Lib mod](https://mods.factorio.com/mod/clusterio_lib) into the `sharedMods` folder on each host installation.
To get this to work, install the Subspace Storage plugin on the controller and hosts using the [plugin installation instructions](#installing-plugins).
Download or upload the [Subspace Storage mod](https://mods.factorio.com/mod/subspace_storage) and the [Clusterio Lib mod](https://mods.factorio.com/mod/clusterio_lib) to the controller, add them to a mod pack, and assign it to each participating instance as described in [Managing Factorio mods](#managing-factorio-mods).
Hosts fetch the mods from the controller and synchronize each instance's mods folder with its assigned mod pack when the instance starts.
Do not install the [Clusterio mod](https://mods.factorio.com/mod/clusterio), this was for the old 1.2.x version of Clusterio and is not compatible with Clusterio 2.0.
@@ -279,16 +290,6 @@ If you want to use a different user for the command line interface, you can gene
This should be ignored as it does not indicate an error occured.
- After updating Factorio you may get the error `Error: Expected empty response but got "Cannot execute command. Error: [string "clusterio_private.update_instance(..."]:1: attempt to index global 'clusterio_private' (a nil value)?` on instance start.
This is due to Factorio applying a migration replacing the patched content of the save with the original scenario, attempting to start the instance again should resolve the issue.
- In the mod pack view of the web interface the "base" mod is displayed with an error complaining about the mod being missing from the controller storage.
This is due to the "base" mod currently not being connectly handled as a built-in mod to Factorio in the web interface, this error message should be ignored.
- After migrating from Alpha 13 to Alpha 14 running an instance data export fails with the error `Upload failed: 401 Unauthorized` due to the host token being issued to a slave.
To workaround this find the id of the host in the host config and generate a new host token with the same id.
Stop the host and replace the value of the `host.controller_token` field in `config-host.json` with the new host token before starting it up again.
- After migrating from Alpha 13 to Alpha 14 you get kicked out from the web interface due to the master to controller rename.
If you don't have the authentication token readily available you can manually migrate the token by opening the Inspect/Developer Tools on the page and running the following piece of JavaScript in the Console:
```
localStorage.controller_token = localStorage.master_token
```
## Common problems
+9 -6
View File
@@ -19,7 +19,7 @@ Although, we recomend checking out out our [past decisions](/docs/decisions.md)
- [Updating Your GitHub Fork](#updating-your-github-fork)
- [Publishing releases to npm](#publishing-releases-to-npm)
- [Changelog](#changelog)
- [Supported Node.js and Factorio Version](#supported-node.js-and-factorio-version)
- [Supported Node.js and Factorio Versions](#supported-nodejs-and-factorio-versions)
- [Code Style](#code-style)
- [Naming Style](#naming-style)
- [Units](#units)
@@ -200,12 +200,15 @@ The format of this log should be self explanatory, please follow it carefully. P
## Supported Node.js and Factorio Versions
Clusterio run on Node.js v18 and up, make sure to check that the Node builtin API's and npm packages you use are supported on these versions.
The GitHub Actions tests are run on whatever version of Node.js v18 and v20 is the default.
Clusterio requires Node.js 22 or newer and supports LTS releases in their active and maintenance phases.
For a new development environment, install the latest LTS release using the [installation instructions](../README.md#installation).
Make sure the Node.js built-in APIs and npm packages you use remain compatible with the supported versions.
The GitHub Actions tests currently run on Node.js 24 and 26 across the Factorio matrix, with Node.js 22 used for coverage; see the [CI workflow](../.github/workflows/node.js.yml) for the version matrix.
For Factorio Clusterio 2.0 aims to support version 0.17.69 and up, including the latest experimental release.
It's recommended that you use the lua API reference for 0.17.69, as there's no information on what version Factorio API's were introduced in.
The GitHub Actions tests are run against the latest experimental release.
For Factorio, active support covers the latest stable version and the previous major version, using version lines such as 2.1, 2.0 and 1.1 as described in the [Factorio support policy](decisions.md#factorio).
Older versions from 0.17 onwards receive passive support where it does not restrict actively supported versions.
Check the Lua API documentation for the Factorio versions your changes target and preserve compatibility with the actively supported versions.
The GitHub Actions tests use pinned Factorio releases; see the [CI workflow](../.github/workflows/node.js.yml) for the current version matrix.
Check out all our of decisions for [node, factorio, and operation support](/docs/decisions.md#supportability).
+6
View File
@@ -18,6 +18,12 @@ See [Breaking Changes](/docs/decisions.md#breaking-changes) in the decision log.
Clusterio's packages are now ES modules and plugins should be too.
Add `"type": "module"` to the plugin's `package.json` and replace `require()` and `module.exports` with `import` and `export`.
Keep `"clusterio-plugin"` in the package's `"keywords"` for automatic discovery, and keep `"main"` pointing to the plugin info module (or use the default `index.js`).
An `"exports"` map is not required for ES modules.
If you add one, expose `"./package.json": "./package.json"`; otherwise Clusterio cannot discover or load the plugin by its package name.
Clusterio currently reads the implementation path from `"main"` rather than conditional exports.
See [Plugin Structure](../writing-plugins.md#plugin-structure) for a compatible package example.
Relative imports must include the full filename with the `.js` extension.
This also applies in TypeScript, where you import `./messages.js` even though the source file is `messages.ts`.
Importing a directory no longer resolves to its `index.js`.
+5 -4
View File
@@ -30,9 +30,10 @@ Create a host config. `--id` id the host id. `--name` is the host name. `--gener
### Update Clusterio
ctl> host update [--id <host id>] [--name <host name>] [--restart]
ctl> host update <host> [--all] [--restart]
If remote updates are enabled on the host, this will attempt to update `@clusterio/host` on the machine. Changes will not be applied until the host is restarted. If the `--restart` flag is provided, the host will automatically restart once the update is complete.
Specify the host by name or ID.
If remote updates are enabled on the host, this will attempt to update `@clusterio/host` on the machine. Add `--all` to also update all npm-installed plugins. Changes will not be applied until the host is restarted. If the `--restart` flag is provided, the host will automatically restart once the update is complete.
Remote updates are enabled by default but can be disabled on a per-machine basis at the owner's discretion. For security reasons, this configuration setting cannot be changed remotely. To modify it, run the following command locally while the host is offline:
@@ -40,7 +41,7 @@ Remote updates are enabled by default but can be disabled on a per-machine basis
The controller also supports remote updates, following the same logic. The corresponding commands are:
ctl> controller update [--restart]
ctl> controller update [--all] [--restart]
controller> config set controller.allow_remote_updates true/false
### Mark restart required
@@ -116,7 +117,7 @@ Edit instance config in a text editor. Once file is saved and editor has been ex
### Assign instance to host
ctl> instace assign <instance> [host]
ctl> instance assign <instance> [host]
Assigns instance to host. `instance` is instance to assign, `host` is host to assign to. If `host` is not provided, instance will be unassigned.
-46
View File
@@ -1,46 +0,0 @@
# Migrating from 2.0.0-alpha.13 to 2.0.0-alpha.14
Alpha 14 comes with significant breaking changes for existing clusters, most notably the rename of master and slave to controller and host.
Please study the entire migration guide carefully before attempting to update an existing cluster.
## Before updating
If you're using mods you need to create mod pack string for the mod configurations you're currently running.
You can do this by running the follow RCON command on an instance that is currently running the mod configuration you want to use (you can also run this command in single player and find the string printed in Factorio's [Log file](https://wiki.factorio.com/Log_file)):
/sc local m={name="pack",description="",factorio_version=game.active_mods.base,mods={},settings={startup={},["runtime-global"]={},["runtime-per-user"]={}}} for n,v in pairs(game.active_mods) do table.insert(m.mods,{name=n,enabled=true,version=v}) end local function set(s,c) for n,v in pairs(s) do m.settings[c][n]=v end end set(settings.startup,"startup") set(settings.global,"runtime-global") if game.player then set(game.player.mod_settings,"runtime-per-user") else set(settings.player,"runtime-per-user") end local e=game.encode_string(game.table_to_json(m)) log(e) rcon.print(e)
If you for some reason want to run this function in 2.0+, use the following command:
/sc local a={name="pack",description="",factorio_version=script.active_mods.base,mods={},settings={startup={},["runtime-global"]={},["runtime-per-user"]={}}}for b,c in pairs(script.active_mods)do table.insert(a.mods,{name=b,enabled=true,version=c})end;local function d(e,f)for b,c in pairs(e)do a.settings[f][b]=c end end;d(settings.startup,"startup")d(settings.global,"runtime-global")if game.player then d(game.player.mod_settings,"runtime-per-user")else d(settings.player,"runtime-per-user")end;local g=helpers.encode_string(helpers.table_to_json(a))log(g)rcon.print(g)
This should produce a [Mod Pack String](https://forums.factorio.com/viewtopic.php?f=96&t=103578) which you need to copy and paste somewhere to keep it for later.
Mod Pack strings looks something like this:
eNqFketuwyAMhd/Fv0OUVpW25lWmqSLESdAIRNh0l4h3n9mlitZt5QdCx/7so8MKXs8ILSzaPEEFPZKJdmEbvIgiDNpwiDaczhjpU93Vu/r+Tmpz6Anah/V7RqcJRUavO4c9tBwTVnAF5upCGJeIsYx3tvsPbQTdb0lKHYlnPJHY0+ONvcdjvW8gP1ZAyGz9KL5XINaR0yLPXEFMnu2ManSh0+6j/GOFitqXG4mjNSUidVm5wlm7JMYG7Qhl3BX8FjyqZ9vztOk+NM2fvRPaceLbzbN+UejQFFOWXzfArvk6v2HWD9ZLv5JfxA1UwsubPBaMKhHGklLO76vAwfQ=
If you have multiple mod configuration create a mod pack string for each and note them down.
Double check that you are on a supported version of Node.js by running `node --version`, the version should be v18 or later, if not you will need to update Node.js before continuing.
## Migrate installations
**Note:** Check that plugins you are using have updates avaialble for alpha 14 before attempting to update, if they do not either wait for the plugin to be updated or uninstall the plugin from all places it is installed on first.
Shutdown all slaves and the master server and then migrate each installations by running the following command:
npm exec @clusterio/create -y -- --migrate-rename --log-level=verbose
This will convert the existing installation's usage of the master and slave names in packages, configs, database and logs to the new controller and host terms and update the installed packages.
If you're using systemd then copy the generated clusteriocontroller.service and/or clusteriohost.service files from the systemd folder to /etc/systemd/system/ and remove the old clusteriomaster.service and clusterioslave.service files.
Once the installations have been migrated start up the controller and hosts again.
## Post installation
Import the Mod Pack strings that was created beforehand on the controller and upload the mods needed by those mod packs to the controller.
You can assign a default mod pack in the controller config which instances use by default or set the mod pack on the per instance basis in the instance config.
Once you have assigned a mod pack to an instance run a data export on the instance in order to properly populate the mod settings in the mod and get item icons and item names resolved in the web interface.
+20 -7
View File
@@ -28,24 +28,37 @@ The steps below describe publishing npm packages manually instead.
git status
```
2. Bump the version in the `package.json` files
2. Create a release branch and bump the version in the `package.json` files. The commands below use 2.0.1 to 2.0.2 as an example; substitute the versions for the release you are preparing.
```sh
sed -i 's/2\.0\.0-alpha\.XX/2\.0\.0-alpha\.YY/' {packages,plugins}/*/package.json
````
git switch -c release/2.0.2
sed -i 's/"version": "2\.0\.1"/"version": "2.0.2"/' {packages,plugins}/*/package.json
pnpm install --no-frozen-lockfile
```
3. Tag and push the release to github
3. Review and commit the version and changelog changes, then open and merge a release pull request.
```sh
git add -p
git commit -m v2.0.0-alpha.XX
git tag v2.0.0-alpha.XX
git commit -m "Prepare 2.0.2 release"
```
4. Perform a clean install and publish the packages.
4. After the release pull request is merged, tag the merged commit and push the tag.
```sh
git switch master
git pull --ff-only upstream master
git tag v2.0.2
git push upstream v2.0.2
```
5. For manual publication, perform a clean install and publish the packages. For automated publication, publish the GitHub release as described above instead.
The repository does not track `pnpm-lock.yaml`, so allow pnpm to generate or update it during installation.
```sh
pnpm clean
pnpm install --no-frozen-lockfile
pnpm -r --filter='./packages/*' --filter='./plugins/*' --fail-if-no-match publish
```
+30 -4
View File
@@ -39,6 +39,8 @@ The basic file structure of a plugin is the following.
Clusterio plugins are Node.js packages that can be published on npm for ease of installation and distribution.
The usual guides for creating such packages apply.
At minimum the `package.json` file must contain a version entry.
For automatic discovery, include `"clusterio-plugin"` in the package's `"keywords"` array.
Installed npm plugins are discovered from the installation's dependencies when controller, host or ctl loads its configuration, and added to `plugin-list.json`.
A possible workflow for developing plugins is to place the plugin in a sub-directory of where clusterio has been installed, and rely on Node.js searching up the folder heirarchy for it to find `@clusterio/lib`.
After creating a directory for your plugin, the boilerplate can be generated automaticlly from a set of tempaltes using `npm exec @clusterio/create -y -- --plugin-template` after which you will be asked which areas your plugin will interact with and your plugin's name.
@@ -47,6 +49,28 @@ Note that it's important that the path starts with ./ or ../ (use .\ or ..\ on W
For a plugin to be recognised by Clusterio it needs to export an entry named `plugin` from its main entrypoint.
By default the main entrypoint is the `index.js` file, but this may be changed by setting the `"main"` entry to a different file in `package.json`.
Keep this `"main"` entry when using a package `"exports"` map: Clusterio currently loads the plugin implementation from `"main"` (or `index.js`), rather than selecting a conditional export.
If a plugin published on npm defines an `"exports"` map, it must also expose `"./package.json"`.
Both automatic discovery and runtime loading resolve `<package-name>/package.json` to read the plugin's metadata.
Packages that hide this file are currently skipped by discovery, and manually adding their package names to `plugin-list.json` does not make them loadable.
The generated plugin templates omit `"exports"`, which leaves `package.json` accessible.
For example, a plugin that needs an export map can use:
```json
{
"name": "example-clusterio-plugin",
"version": "1.0.0",
"type": "module",
"main": "./index.js",
"keywords": ["clusterio-plugin"],
"exports": {
".": "./index.js",
"./package.json": "./package.json"
}
}
```
Here's an example of `index.js`:
```js
@@ -65,8 +89,10 @@ export const plugin = {
The following properties are recognized:
**name**:
Internal name of the plugin.
Must be the same as the plugin's directory name
Stable internal identifier of the plugin, used as its key in the plugin list.
For local plugins automatically discovered under `plugins/` or `external_plugins/`, it must match the plugin's directory name.
It does not have to match the npm package name.
For example, the package `@clusterio/plugin-subspace_storage` declares the name `subspace_storage`.
**title**:
Name of the plugin as shown to users.
@@ -90,8 +116,8 @@ The following properties are recognized:
This is an optional parameter.
A plugin can be made that only runs on the controller.
**ControllerConfigGroup**:
Object mapping controller configuration fields to `FieldDefinitions`for this plugin.
**controllerConfigFields**:
Object mapping controller configuration fields to `FieldDefinition` objects for this plugin.
See [Plugin Configuration](#plugin-configuration)
**ctlEntrypoint**:
-8
View File
@@ -503,14 +503,6 @@ async function startup() {
}
export function bootstrap() {
// eslint-disable-next-line no-console
console.warn(`
+==========================================================+
I WARNING: This is the development branch for the 2.0 I
I version of clusterio. Expect things to break. I
+==========================================================+
`
);
startup().catch(err => {
if (err instanceof lib.StartupError || err.code === "InstallationError") {
logger.fatal(`
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@clusterio/controller",
"version": "2.0.0",
"version": "2.0.2",
"description": "Implementation of Clusterio controller",
"homepage": "https://github.com/clusterio/clusterio#readme",
"author": "Hornwitser <github@hornwitser.no>",
+2 -1
View File
@@ -2,4 +2,5 @@
Installer for Clusterio.
See [the Clusterio README.md](https://github.com/clusterio/clusterio#readme) the for installation guide.
Install the latest [Node.js LTS release](https://nodejs.org/en/download) before running the installer.
See [the Clusterio installation guide](https://github.com/clusterio/clusterio#installation) for platform-specific instructions.
+2 -25
View File
@@ -392,31 +392,7 @@ async function installClusterio(mode, plugins) {
throw new InstallError(`Failed to install: ${err.message}`);
}
if (plugins.length) {
logger.info("Setting up plugins");
let pluginList;
try {
pluginList = new Map(JSON.parse(await fs.readFile("plugin-list.json")));
} catch (err) {
if (err.code === "ENOENT") {
pluginList = new Map();
} else {
throw new InstallError(`Error loading plugin-list.json: ${err.message}`);
}
}
for (let plugin of plugins) {
if (!pluginList.has(plugin)) {
let pluginInfo = (await import(plugin)).plugin;
pluginList.set(pluginInfo.name, plugin);
}
}
try {
await safeOutputFile("plugin-list.json", JSON.stringify([...pluginList], null, "\t"));
} catch (err) {
throw new InstallError(`Error writing plugin-list.json: ${err.message}`);
}
}
// Controller, host and ctl discover installed plugins when loading their configuration.
}
async function groupIdToName(gid) {
@@ -905,6 +881,7 @@ async function main() {
if (import.meta.main) {
main().catch(err => {
process.exitCode = 1;
if (err instanceof InstallError) {
logger.error(err.message);
} else {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@clusterio/create",
"version": "2.0.0",
"version": "2.0.2",
"description": "Installer for clusterio",
"homepage": "https://github.com/clusterio/clusterio#readme",
"author": "Hornwitser <github@hornwitser.no>",
-8
View File
@@ -327,14 +327,6 @@ async function startCtl() {
}
export function bootstrap() {
// eslint-disable-next-line no-console
console.warn(`
+==========================================================+
I WARNING: This is the development branch for the 2.0 I
I version of clusterio. Expect things to break. I
+==========================================================+
`
);
startCtl().catch(err => {
if (err.errors) {
logger.fatal(JSON.stringify(err.errors, null, "\t"));
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@clusterio/ctl",
"version": "2.0.0",
"version": "2.0.2",
"description": "Clusterio command line management tool",
"homepage": "https://github.com/clusterio/clusterio#readme",
"author": "Hornwitser <github@hornwitser.no>",
-8
View File
@@ -267,14 +267,6 @@ async function startHost() {
}
export function bootstrap() {
// eslint-disable-next-line no-console
console.warn(`
+==========================================================+
I WARNING: This is the development branch for the 2.0 I
I version of clusterio. Expect things to break. I
+==========================================================+
`
);
startHost().catch(err => {
if (err instanceof lib.WebSocketError) {
// Already handled by connector.on("error") within Host
@@ -1,4 +1,29 @@
---------------------------------------------------------------------------------------------------
Version: 2.1.21
Date: 2026-10-14
Changes:
- Removed the obsolete alpha label and development warning from the mod title and description.
---------------------------------------------------------------------------------------------------
Version: 2.1.20
Date: 2026-10-14
Changes:
- Removed the obsolete alpha label and development warning from the mod title and description.
---------------------------------------------------------------------------------------------------
Version: 2.1.18
Date: 2026-10-14
Changes:
- Removed the obsolete alpha label and development warning from the mod title and description.
---------------------------------------------------------------------------------------------------
Version: 2.1.17
Date: 2026-10-14
Changes:
- Removed the obsolete alpha label and development warning from the mod title and description.
---------------------------------------------------------------------------------------------------
Version: 2.1.11
Date: 2026-10-14
Changes:
- Removed the obsolete alpha label and development warning from the mod title and description.
---------------------------------------------------------------------------------------------------
Version: 2.0.20
Date: 25. 02. 2025
Features:
+7 -7
View File
@@ -2,30 +2,30 @@
"name": "clusterio_lib",
"variants": [
{
"version": "2.0.17",
"version": "2.1.17",
"factorio_version": "0.17"
},
{
"version": "2.0.18",
"version": "2.1.18",
"factorio_version": "0.18"
},
{
"version": "2.0.11",
"version": "2.1.11",
"factorio_version": "1.1"
},
{
"version": "2.0.20",
"version": "2.1.20",
"factorio_version": "2.0"
},
{
"version": "2.0.21",
"version": "2.1.21",
"factorio_version": "2.1"
}
],
"title": "Clusterio Library (Alpha)",
"title": "Clusterio Library",
"author": "Clusterio Team",
"homepage": "https://github.com/clusterio/clusterio",
"description": "Library for interfacing with Clusterio. Warning: work in progress alpha.",
"description": "Library for interfacing with Clusterio.",
"additional_files": {
"compat.lua": ["..", "..", "modules", "clusterio", "compat.lua"],
"serialize.lua": ["..", "..", "modules", "clusterio", "serialize.lua"],
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@clusterio/host",
"version": "2.0.0",
"version": "2.0.2",
"description": "Implementation of Clusterio host server",
"homepage": "https://github.com/clusterio/clusterio#readme",
"author": "Hornwitser <github@hornwitser.no>",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@clusterio/lib",
"version": "2.0.0",
"version": "2.0.2",
"description": "Shared library for Clusterio",
"homepage": "https://github.com/clusterio/clusterio#readme",
"author": "Hornwitser <github@hornwitser.no>",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@clusterio/web_ui",
"version": "2.0.0",
"version": "2.0.2",
"description": "Clusterio web interface implementation",
"homepage": "https://github.com/clusterio/clusterio#readme",
"author": "Hornwitser",
+1 -1
View File
@@ -106,7 +106,7 @@ export class Control extends lib.Link {
});
for (let event of ["connect", "drop", "resume", "close"] as const) {
this.connector.on(event, this.hooks.controllerConnectionEvent.listener);
this.connector.on(event, () => this.hooks.controllerConnectionEvent.invoke(event));
}
this.handle(lib.AccountUpdateEvent, this.handleAccountUpdateEvent.bind(this));
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@clusterio/plugin-global_chat",
"version": "2.0.0",
"version": "2.0.2",
"description": "Clusterio plugin forwarding between Factorio servers",
"homepage": "https://github.com/clusterio/clusterio#readme",
"author": "Hornwitser <github@hornwitser.no>",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@clusterio/plugin-player_auth",
"version": "2.0.0",
"version": "2.0.2",
"description": "Clusterio plugin authenticating logged in players to the web interface",
"homepage": "https://github.com/clusterio/clusterio#readme",
"author": "Hornwitser <github@hornwitser.no>",
+30
View File
@@ -1,4 +1,5 @@
import assert from "node:assert/strict";
import { mock as nodeMock } from "node:test";
import jwt from "jsonwebtoken";
import * as mock from "../../../test/mock.js";
@@ -87,6 +88,17 @@ describe("player_auth", function() {
let endpoint;
let fetchPlayerCode;
let setVerifyCode;
let now;
let dateNow;
beforeEach(function() {
// Keep code expiry and JWT timestamps independent of HTTP request timing.
now = Date.now();
dateNow = nodeMock.method(Date, "now", () => now);
});
afterEach(function() {
dateNow.mock.restore();
});
// Adds a pending code for player through the request handlers
async function addPlayer(player, { timeout = 1, verifyCode } = {}) {
@@ -334,6 +346,8 @@ describe("player_auth", function() {
describe("integration", function() {
it("should verify a full login flow", async function() {
// A one-second timeout leaves only 1ms before the JWT's rounded expiry here.
now = Math.floor(now / 1000) * 1000 + 999;
let { playerCode } = await fetchPlayerCode(new FetchPlayerCodeRequest("test"));
const playerCodeResult = await postJSON(
@@ -353,6 +367,22 @@ describe("player_auth", function() {
assert.equal(verifyResult.status, 200);
assert.deepEqual(await verifyResult.json(), { verified: true, token: token });
});
it("should reject a verification token at its expiry", async function() {
const playerCode = await addPlayer("test");
const playerCodeResult = await postJSON(
`${controllerUrl}/api/player_auth/player_code`, { player_code: playerCode }
);
const { verify_code, verify_token } = await playerCodeResult.json();
await setVerifyCode(new SetVerifyCodeRequest("test", verify_code));
now = jwt.decode(verify_token).exp * 1000;
const verifyResult = await postJSON(
`${controllerUrl}/api/player_auth/verify`,
{ player_code: playerCode, verify_code, verify_token }
);
assert.equal(verifyResult.status, 200);
assert.deepEqual(await verifyResult.json(), { error: true, message: "jwt expired" });
});
});
});
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@clusterio/plugin-player_sync",
"version": "2.0.0",
"version": "2.0.2",
"description": "Clusterio plugin forwarding between Factorio servers",
"homepage": "https://github.com/clusterio/clusterio#readme",
"author": "Danielv <danielv@danielv.no>",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@clusterio/plugin-research_sync",
"version": "2.0.0",
"version": "2.0.2",
"description": "Clusterio plugin for synchronizing research between Factorio servers",
"homepage": "https://github.com/clusterio/clusterio#readme",
"author": "Hornwitser <github@hornwitser.no>",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@clusterio/plugin-statistics_exporter",
"version": "2.0.0",
"version": "2.0.2",
"description": "Clusterio plugin for gathering statistics from Factorio servers",
"homepage": "https://github.com/clusterio/clusterio#readme",
"author": "Hornwitser <github@hornwitser.no>",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@clusterio/plugin-subspace_storage",
"version": "2.0.0",
"version": "2.0.2",
"description": "Clusterio plugin for sharing storage between Factorio servers",
"homepage": "https://github.com/clusterio/clusterio#readme",
"author": "Hornwitser <github@hornwitser.no>",
+9 -9
View File
@@ -2,8 +2,9 @@
* Publish any mods that need to be published
*/
import FormData from "form-data";
import { openAsBlob } from "node:fs";
import fs from "node:fs/promises";
import path from "node:path";
import yargs from "yargs";
let DRY = false;
@@ -34,7 +35,7 @@ async function requestModReleases(mod) {
/**
* Upload a mod to the mod portal
*/
async function uploadModRelease(file, name) {
export async function uploadModRelease(file, name) {
// eslint-disable-next-line no-console
console.log(`Uploading new release ${file}`);
if (DRY) {
@@ -61,7 +62,7 @@ async function uploadModRelease(file, name) {
// Upload the new version of the mod
const uploadUrl = (await initResponse.json()).upload_url;
const uploadFormData = new FormData();
uploadFormData.append("file", (await fs.open(file)).createReadStream(file));
uploadFormData.append("file", await openAsBlob(file), path.basename(file));
const uploadResponse = await fetch(uploadUrl, {
method: "POST",
@@ -77,25 +78,24 @@ const regexModZip = /(\w+)_\d+.\d+.\d+\.zip/;
/**
* Publishes all mods that require publishing
*/
async function publishMods(dir) {
export async function publishMods(dir) {
const files = await fs.readdir(dir);
const mods = files
.sort((a, b) => a.localeCompare(b, "en", { numeric: true }))
.map(file => regexModZip.exec(file))
.filter(file => Boolean(file));
const uploading = [];
for (const mod of mods) {
const [file, name] = mod;
const releases = await requestModReleases(name);
if (!releases.some(release => release.file_name === file)) {
uploading.push(uploadModRelease(file, name));
// Upload in version order so the newest variant is published last.
await uploadModRelease(path.join(dir, file), name);
} else {
// eslint-disable-next-line no-console
console.log(`Skipped ${file}`);
}
}
await Promise.all(uploading);
}
/**
@@ -121,7 +121,7 @@ async function main() {
.demandOption("mods-dir", "A path to the mods dir must be specified");
}, async argv => {
DRY = argv.dry;
publishMods(argv.modsDir);
await publishMods(argv.modsDir);
})
.strict()
.parse();
+126
View File
@@ -0,0 +1,126 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { promisify } from "node:util";
const execFileAsync = promisify(execFile);
const installer = path.resolve(import.meta.dirname, "../packages/create/create.js");
const discoveryUrl = new URL("../packages/lib/dist/node/src/load_plugin_list.js", import.meta.url).href;
describe("create installer CLI", function() {
this.timeout(10000);
let directory;
beforeEach(async function() {
directory = await fs.mkdtemp(path.join(os.tmpdir(), "clusterio install #"));
await fs.writeFile(path.join(directory, "package.json"), JSON.stringify({
name: "clusterio-install", private: true,
}));
});
afterEach(async function() {
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10 });
});
async function addPlugin(name, type, source) {
const pluginDir = path.join(directory, "node_modules", name);
await fs.mkdir(pluginDir, { recursive: true });
await fs.writeFile(path.join(pluginDir, "package.json"), JSON.stringify({
name, version: "1.0.0", type, main: "index.js", keywords: ["clusterio-plugin"],
}));
await fs.writeFile(path.join(pluginDir, "index.js"), source);
const packagePath = path.join(directory, "package.json");
const pkg = JSON.parse(await fs.readFile(packagePath));
pkg.dependencies = { ...pkg.dependencies, [name]: "1.0.0" };
await fs.writeFile(packagePath, JSON.stringify(pkg));
}
function runInstaller(plugins, installFails = false) {
// Only replace npm's subprocess: fixtures stand in for downloaded packages.
// Run the actual CLI from outside the installation, like npm init does.
const mockNpm = `
import assert from "node:assert/strict";
import childProcess from "node:child_process";
import { PassThrough } from "node:stream";
childProcess.execFile = (command, options, callback) => {
assert.match(command, /^npm(?:\\.cmd)? install /);
const stdout = new PassThrough();
const stderr = new PassThrough();
queueMicrotask(() => {
stdout.end();
stderr.end();
callback(${installFails} ? new Error("npm install failed") : null, "", "");
});
return { stdout, stderr };
};
`;
return execFileAsync(process.execPath, [
"--import", `data:text/javascript,${encodeURIComponent(mockNpm)}`,
installer, "--mode", "plugins",
...(process.platform === "linux" ? ["--allow-install-as-root"] : []),
"--plugins", ...plugins,
], { cwd: directory });
}
it("installs plugins without importing them or writing a plugin list", async function() {
await addPlugin("@installer-test/deferred", "module", 'throw new Error("installer must not import plugins");');
const { stdout } = await runInstaller(["@installer-test/deferred"]);
assert.match(stdout, /Successfully installed plugins/);
await assert.rejects(fs.access(path.join(directory, "plugin-list.json")), { code: "ENOENT" });
});
[false, true].forEach(existingList => {
it(`defers registration to discovery ${existingList ? "preserving existing entries" : "with no list"}`,
async function() {
await addPlugin("@installer-test/esm", "module", 'export const plugin = { name: "esm_plugin" };');
await addPlugin("@installer-test/commonjs", "commonjs", 'exports.plugin = { name: "cjs_plugin" };');
const listPath = path.join(directory, "plugin-list.json");
const previousEntries = existingList ? [["manual", "./plugins/manual"]] : [];
const previousContents = JSON.stringify(previousEntries);
if (existingList) {
await fs.writeFile(listPath, previousContents);
}
const { stdout } = await runInstaller(["@installer-test/esm", "@installer-test/commonjs"]);
assert.match(stdout, /Successfully installed plugins/);
if (existingList) {
assert.equal(await fs.readFile(listPath, "utf8"), previousContents);
} else {
await assert.rejects(fs.access(listPath), { code: "ENOENT" });
}
// Use the real discovery function invoked by controller/host/ctl commands.
await execFileAsync(process.execPath, [
"--input-type=module", "--eval",
`import { loadPluginList } from ${JSON.stringify(discoveryUrl)};
await loadPluginList("plugin-list.json");`,
], { cwd: directory });
assert.deepEqual(JSON.parse(await fs.readFile(listPath)), [
...previousEntries,
["esm_plugin", "@installer-test/esm"],
["cjs_plugin", "@installer-test/commonjs"],
]);
}
);
});
it("exits with failure when npm installation fails", async function() {
await assert.rejects(runInstaller(["@installer-test/missing"], true), err => {
assert.equal(err.code, 1);
assert.match(err.stdout + err.stderr, /Failed to install: npm install failed/);
assert.doesNotMatch(err.stdout, /Successfully installed/);
return true;
});
});
it("exits with failure on an unexpected installer error", async function() {
await fs.writeFile(path.join(directory, "package.json"), "null");
await assert.rejects(runInstaller([]), err => {
assert.equal(err.code, 1);
assert.match(err.stdout + err.stderr, /TypeError/);
assert.doesNotMatch(err.stdout, /Successfully installed/);
return true;
});
});
});
+1
View File
@@ -142,6 +142,7 @@ describe("Integration of Clusterio", function() {
describe("copy-static", function() {
it("should copy the files from dist/web/static", async function() {
slowTest(this);
const dir = "temp/test/copy-static";
await fs.rm(dir, { force: true, recursive: true, maxRetries: 10 });
await execController("copy-static copy-static");
+1 -1
View File
@@ -382,7 +382,7 @@ before(async function() {
testPack.factorioVersion = latestFactorioVersion;
const [major, minor] = lib.normaliseMajorMinorVersion(latestFactorioVersion).split(".");
testPack.mods.set("clusterio_lib", { name: "clusterio_lib", enabled: true, version: `2.0.${major}${minor}` });
testPack.mods.set("clusterio_lib", { name: "clusterio_lib", enabled: true, version: `2.1.${major}${minor}` });
await control.sendTo("controller", new lib.ModPackCreateRequest(testPack));
await control.sendTo(
+103
View File
@@ -0,0 +1,103 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import fs from "node:fs/promises";
import path from "node:path";
import { promisify } from "node:util";
import { publishMods, uploadModRelease } from "../publish.js";
const execFileAsync = promisify(execFile);
describe("mod publishing", function() {
let originalFetch;
let directory;
beforeEach(async function() {
originalFetch = global.fetch;
await fs.mkdir("temp", { recursive: true });
directory = await fs.mkdtemp("temp/publish-test-");
});
afterEach(async function() {
global.fetch = originalFetch;
await fs.rm(directory, { recursive: true, force: true });
});
it("sends multipart mod and file fields accepted by native fetch", async function() {
const name = "multipart_test";
const filename = `${name}_1.0.0.zip`;
const contents = Buffer.from([0x50, 0x4b, 0x03, 0x04, 0x00, 0xff]);
const file = path.join(directory, filename);
await fs.writeFile(file, contents);
let calls = 0;
global.fetch = async (url, options) => {
calls += 1;
const request = new Request(url, options);
assert.match(request.headers.get("content-type"), /^multipart\/form-data; boundary=/);
const form = await request.formData();
if (calls === 1) {
assert.equal(url, "https://mods.factorio.com/api/v2/mods/releases/init_upload");
assert.equal(form.get("mod"), name);
return Response.json({ upload_url: "https://upload.example.test/release" });
}
assert.equal(url, "https://upload.example.test/release");
const uploaded = form.get("file");
assert.equal(uploaded.name, filename);
assert.deepEqual(Buffer.from(await uploaded.arrayBuffer()), contents);
return Response.json({ success: true });
};
await uploadModRelease(file, name);
assert.equal(calls, 2);
});
it("reads archives from the supplied directory and uploads unpublished versions in order", async function() {
const name = "ordered_publish_test";
for (const version of ["1.0.9", "1.0.10", "1.0.2"]) {
await fs.writeFile(path.join(directory, `${name}_${version}.zip`), version);
}
const uploaded = [];
let uploading = false;
global.fetch = async (url, options) => {
if (url === `https://mods.factorio.com/api/mods/${name}`) {
return Response.json({ releases: [{ file_name: `${name}_1.0.2.zip` }] });
}
const form = await new Request(url, options).formData();
if (url.endsWith("/init_upload")) {
assert.equal(uploading, false, "previous upload must complete first");
uploading = true;
return Response.json({ upload_url: "https://upload.example.test/release" });
}
const file = form.get("file");
uploaded.push([file.name, await file.text()]);
await new Promise(resolve => setImmediate(resolve));
uploading = false;
return Response.json({ success: true });
};
await publishMods(directory);
assert.deepEqual(uploaded, [
[`${name}_1.0.9.zip`, "1.0.9"],
[`${name}_1.0.10.zip`, "1.0.10"],
]);
});
it("runs the CLI in dry-run mode without uploading unpublished archives", async function() {
const filename = "cli_publish_test_1.0.0.zip";
await fs.writeFile(path.join(directory, filename), "dry-run archive");
const mockFetch = `
import assert from "node:assert/strict";
global.fetch = async (url, options = {}) => {
assert.equal(url, "https://mods.factorio.com/api/mods/cli_publish_test");
assert.equal(options.method ?? "GET", "GET", "dry-run must not upload");
return Response.json({ releases: [] });
};
`;
const { stdout, stderr } = await execFileAsync(process.execPath, [
"--import", `data:text/javascript,${encodeURIComponent(mockFetch)}`,
"publish.js", "factorio", directory, "--dry-run",
]);
assert.equal(stderr, "");
assert(stdout.includes("Requesting current releases of cli_publish_test"));
assert(stdout.includes(path.join(directory, filename)));
});
});