diff --git a/CHANGELOG.md b/CHANGELOG.md index 2991fa6f..d28dc168 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ - Added an overview of the installed plugins to the web interface. - Added clusterioctl command to list plugins on the master server. - Added Player Auth plugin for logging in to the web interface by proving the ability to log in to a server as a given user. +- Added Node.js based installer to simplify setting up a cluster. ### Changes diff --git a/README.md b/README.md index 2b7660a4..fc25bc87 100644 --- a/README.md +++ b/README.md @@ -32,17 +32,15 @@ Go to the page for the [1.2.x branch][1.2.x] for instructions on how to install * [Introduction](#introduction) * [Features](#features) * [Plugins](#plugins) -* [Ubuntu setup](#ubuntu-setup) -* [Windows setup](#windows-setup) -* [MacOS setup](#macos-setup) -* [Installing Plugins](#installing-plugins) +* [Installation](#installation) + * [Ubuntu setup](#ubuntu-setup) + * [Windows setup](#windows-setup) + * [MacOS setup](#macos-setup) + * [Installing Plugins](#installing-plugins) * [Configure Master Server](#configure-master-server) - * [Hosting Behind Proxy](#hosting-behind-proxy) - * [Setting up an admin account](#setting-up-an-admin-account) * [Running Clusterio](#running-clusterio) - * [Master Server](#master-server) - * [Slaves](#slaves) - * [Instances](#instances) +* [Setting up remote slaves](#setting-up-remote-slaves) +* [Setting up clusterioctl](#setting-up-clusterioctl) * [Common problems](#Common-problems) @@ -90,26 +88,43 @@ Want to make your own plugin? Check out the documentation on [Writing Plugins](/docs/writing-plugins.md) for where to start. -## Ubuntu setup +## Installation + +Clusterio runs on Node.js v12 and up, it's distributed via npm and comes with a guided install script. +If you alread have a recent Node.js installed you can set it up in a new directory with. + + npm init "@clusterio" + +Otherwise see below for OS specific instructions. + + +### Ubuntu setup **Warning**: These instructions are for the unstable master version and is not recommended for use, see [the 1.2.x branch][1.2.x] for how to install the stable version. -Clusterio runs on Node.js v12 and up. -Node.js itself is not supported on EOL Ubuntu releases so make sure you're on a recent release of Ubuntu. +1. Install Node.js v12 or higher. + For 20.04 LTS or below the version of Node.js provided by the Ubuntu repos are too old and you will have to use the nodesource PPA, otherwise you may skip the first line. -Master and all slaves: + wget -qO - https://deb.nodesource.com/setup_14.x | sudo -E bash - + sudo apt install nodejs - wget -qO - https://deb.nodesource.com/setup_12.x | sudo -E bash - - sudo apt install -y nodejs - mkdir clusterio - cd clusterio - npm init -y - npm install @clusterio/master @clusterio/slave @clusterio/ctl - wget -O factorio.tar.gz https://www.factorio.com/get-download/latest/headless/linux64 - tar -xf factorio.tar.gz +2. Create a new directory and run the Clusterio installer: + + mkdir clusterio + cd clusterio + npm init "@clusterio" + + Make sure to note down the admin authentication token it provides at the end as you will need it later. + +3. If you chose to use local factorio directory for the Factorio installation then download the headless build of Factorio and unpack it: + + wget -O factorio.tar.gz https://www.factorio.com/get-download/latest/headless/linux64 + tar -xf factorio.tar.gz + + To specify a version of Factorio to download replace "latest" in the URL with a version number like "1.0.0". + +4. Optionally copy the generated systemd service files in `systemd` folder to `/etc/systemd/system/`. -downloads and installs nodejs, git and clusterio. -To specify a version, change "latest" in the link to a version number like 0.14.21. **Ubuntu with Docker** @@ -129,70 +144,42 @@ The -v flag is used to specify the instance directory. Your instances (save files etc) will be stored there. --> -## Windows setup +### Windows setup **Warning**: These instructions are for the unstable master version and is not recommended for use, see [the 1.2.x branch][1.2.x] for how to install the stable version. -**Requirements** +1. Download and install the latest LTS release from http://nodejs.org. -download and install nodeJS 12 from http://nodejs.org. -Clusterio runs on Node.js v12 and up. +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. -**Master** + npm init "@clusterio" -1. Open PowerShell or Command prompt in the directory you want to install to and run the following commands. + Make sure to note down the admin authentication token it provides at the end as you will need it later. + +3. If you chose to use local factorio directory for the Factorio installation then download the Windows 64-bit zip package from https://www.factorio.com/download and extract it to the `factorio` folder in your clusterio installation folder. + + +### MacOS Setup + +1. Install the latest Node.js LTS release from http://nodejs.org or use brew (`brew install node`). + +2. Open Terminal or Command prompt in the directory you want to install to and run the following commands. mkdir clusterio cd clusterio - npm init -y - npm install @clusterio/master @clusterio/slave @clusterio/ctl + npm init "@clusterio" -2. Obtain Factorio by either of these two methods: + Make sure to note down the admin authentication token it provides at the end as you will need it later. - - Via the stand alone version on from their website +3. If you chose to use local factorio directory for the Factorio installation you will need to obtain and copy a mac version of Factorio and unpack it to to the `factorio` folder in your clusterio installation folder. - 1. Create a new folder named "factorio" in the the factorioClusterio folder. - 2. Download the MS Windows (64-bit zip package) from https://www.factorio.com/download . +### Installing Plugins - 3. Open the zip file and drag the folder called "Factorio_x.y.z" into the factorio folder created in step 1. - - - Via steam installation - - 1. Create a new folder named "factorio" in the the factorioClusterio folder. - - 2. Locate the game files by right clicking the game in steam, selecting properties, then Local Files, then Browse local files. - - 3. Go to the parent folder of the folder that Steam opened and copy the Factorio folder into the factorio folder created in step 1. - -## MacOS Setup - -**Requirements** - -Download and install nodeJS 12 from http://nodejs.org or brew (`brew install node`). -Clusterio runs on Node.js v12 and up. - -1. Open Terminal or Command prompt in the directory you want to install to and run the following commands. - - ```bash - mkdir clusterio - cd clusterio - npm init -y - npm install @clusterio/master @clusterio/slave @clusterio/ctl - ``` - -2. Obtain Factorio: - -Install Factorio and copy the factorio.app/Contents in your clusterio install folder as factorio. -If you installed factorio on your mac you can run -```bash - cp -r /Applications/factorio.app/Contents/ factorio -``` - -## Installing Plugins - -Installing plugins to make them work with Clusterio consists of two steps. -First install the package via npm, for example +For well known plugins you can select them during installation and no further steps are necessary to make them work. +Installing plugins that are not offered by the installer consists of two steps. +First install the package the plugin is provided by via npm, for example npm install @clusterio/plugin-subspace_storage @@ -202,7 +189,7 @@ Then tell clusterio that this plugin exists by adding it as a plugin. This adds it to the default `plugin-list.json` file which in the shared folder setup is loaded by master, slave and ctl. If you have slaves or ctl installed on separate computers (or directories) then you need to repeat the plugin install process for all of them. -The clusteriomaster, clusterioslave and clusterioctl commands has the plugin sub-command so you do not need to install clusteriomaster to add plugins. +The clusteriomaster, clusterioslave and clusterioctl commands has the plugin sub-command so you do not need to install clusteriomaster to add plugins, instead use the clusterio command you have available. For development purposes the `plugin add` command supports adding plugins by the absolute path to them, or a relative path that must start with either . or .. (which will then be resolved to an absolute path). @@ -214,6 +201,11 @@ You can change the port used with the command npx clusteriomaster config set master.http_port 1234 +When changing the port you will also need to change the address slaves connect with. +For the standalone installation mode you can use + + npx clusterioslave config set slave.master_url http://localhost:1234/ + If you plan to make your cluster available externally set the address that it will be accessible under with, for example @@ -226,67 +218,30 @@ You can list the config of the master server with the `npx clusteriomaster confi See the [readme for @clusterio/master](/packages/master/README.md) for more information. -### Setting up an admin account - -Before you can manage the cluster you need to bootstrap an admin account for it. -Replace `` with your Factorio username (do not make up a new username here). - - npx clusteriomaster bootstrap create-admin - npx clusteriomaster bootstrap create-ctl-config - -The first command creates a user account with the given name and promotes it to a cluster admin. -The second one sets up a `config-control.json` config for clusterioctl to connect to the master server under the given user account. - - ## Running Clusterio -After following the installation and master configuration instructions you can use the following commands to run Clusterio. +After completing the installation start up the master server and at least one slave separately. +The installer provides the `run-master` and `run-slave` scripts to make this simple. +Once the master process is running you can log into the web interface which is hosted by default on http://localhost:8080/ (adjust the port number if you changed it), use the admin authentication token provided from the installation to log in. + +The basics of setting up a Factorio server from the web interface is to create an instance, assign it to a slave and then click start. -### Master Server +## Setting up remote slaves -It's necessary to run the master server in order for anything to work. -Once you've completed the setup run the following command to start it up: - - npx clusteriomaster run +Run the installer as described in the installation section and choose "Slave only" as the operating mode to install, it'll ask for a master URL and an authentication token. +The URL is the same as what is needed to connect to the web interface, and the athentication token can be generated on the Slaves page in the web interface. +Once you start up the slave it should show up in the Slaves list and be available for assigning and running instances on. -### Slaves +## Setting up clusterioctl -Slaves connect to the master server and are managed remotely from the master server. -In order for slaves to connect to the master server they need a valid authentication token, you can create a slave config with a valid token with the following command. +There's a command line interface available for Clusterio which is installed separately with the same installer as for the master and slave. +Run the installer as described in the installation section and choose "Ctl only" as the operating mode to install, you can do this in the same directory as you have installed other clusterio component(s) to. +The installer will ask for a master URL and an authentication token, these are the same as you would use to connect to the web interface. +If you want to use a different user for the command line interface you can generate an authentication token for an existing user with - npx clusterioctl slave create-config --name Local --generate-token - -This will write a new `config-slave.json` file in the current directory (you can change the location with the `--output` option) with the name, token and url to connect to the master server with. -If you are making the config for a remote slave you will need to have set the `master.external_address` option to the URL the master server can be reached on. - -You can list the config of a slave on the slave itself with the `npx clusterioslave config list` command. -See the [readme for @clusterio/slave](/packages/slave/README.md) for more information. - -Once the config is set up run the slave with - - npx clusterioslave run - - -### Instances - -Instances are created, managed and started from the master server. -For now the only interface available is the `clusterioctl` command line tool included in Clusterio. -You can run this tool from any slave, or the master server without having to set up a config, if you want to manage the cluster from somewhere else you will need to set the `control.master_url` and `control.master_token` options with the `node clusterioctl control-config set` command: - -The basic operations to start a new instance is the following - - npx clusterioctl instance create "My instance" - npx clusterioctl instance assign "My instance" "Local" - npx clusterioctl instance start "My instance" - -The first line creates the instance configuration on the master server. -The second assigns the instance to a slave which creates the instance directory and files needed to run the instance on the given slave. -The third line starts the instance, which creates a new save if there are no save games present. - -There are many more commands available with clusterioctl. -See the [Managing a Cluster](/docs/managing-a-cluster.md) document or `npx clusterioctl --help` for a full list of them. + npx clusteriomaster bootstrap generate-user-token ## Common problems diff --git a/docs/contributing.md b/docs/contributing.md index a66942ce..f255543b 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -54,19 +54,41 @@ In order to get the development environment up and running you will need to run npm install npx lerna bootstrap --hoist + npx lerna run build This installs dependencies needed by tests and links the packages up so they work from the git work tree. -To start a cluster from the repo, substitute the commands in the normal readme as follows: +To start a cluster from the repo you will first need to initialize it, this can be done using the installer: + + node packages/create --dev + +To add plugins to use in the development environment use + + node packages/ctl plugin add ./path/to/plugin + +It's important that the path starts with `./`. +The `plugins/` directory contains the core plugins for the project and can be added using the above mentioned command. + +The documentation uses the binaries provided by the packages, but for the development environment you will need to call the packages directly: npx clusteriomaster -> node packages/master npx clusterioslave -> node packages/slave npx clusterioctl -> node packages/ctl -For web development on the master there are also the following flags: +Note that to use `clusterioctl` you will have to create a control config first: -`--dev` - Start the master with webpack hot reloading of core components + node packages/master bootstrap create-ctl-config [admin-user] + +Once you've set up the cluster you can use `node packages/master run` to run the master server and `node packages/slave run` to run the slave which connects to the master server. + +For web development on the master there are also the following flags. + +`--dev` - Recompile the `web_ui` package on the fly providing live reloading of it. + +`--dev-plugin [name]` - Recompile the web module for the given plugin on the fly proving live reloading of it. + +These options can be combined and multiple plugins can be actived for dev mode. +Note that webpack is configured to delete the web interface build when using these options, so you will have to run `npx lerna run build` before you start the master without these flags again. -`--dev-plugin [name]` - Start the master with hot reloading of a plugin ### Starting a Feature Branch diff --git a/packages/create/README.md b/packages/create/README.md new file mode 100644 index 00000000..0bdb45b1 --- /dev/null +++ b/packages/create/README.md @@ -0,0 +1,5 @@ +# Clusterio installer + +Installer for Clusterio. + +See [the Clusterio README.md](https://github.com/clusterio/factorioClusterio#readme) the for installation guide. diff --git a/packages/create/create.js b/packages/create/create.js new file mode 100755 index 00000000..4cb32463 --- /dev/null +++ b/packages/create/create.js @@ -0,0 +1,529 @@ +#!/usr/bin/env node + +"use strict"; +const child_process = require("child_process"); +const fs = require("fs-extra"); +const inquirer = require("inquirer"); +const os = require("os"); +const path = require("path"); +const phin = require("phin"); +const stream = require("stream"); +const util = require("util"); +const yargs = require("yargs"); + +const { levels, logger, setLogLevel } = require("./logging"); +let dev = false; +const scriptExt = process.platform === "win32" ? ".cmd" : ""; + + +const availablePlugins = [ + { name: "Global Chat", value: "@clusterio/plugin-global_chat" }, + { name: "Player Auth", value: "@clusterio/plugin-player_auth" }, + { name: "Research Sync", value: "@clusterio/plugin-research_sync" }, + { name: "Statistics Exporter", value: "@clusterio/plugin-statistics_exporter" }, + { name: "Subspace Storage", value: "@clusterio/plugin-subspace_storage" }, + + // Comunity plugins + { name: "Discord Bridge", value: "@hornwitser/discord_bridge" }, + { name: "Server Select", value: "@hornwitser/server_select" }, +]; + +const factorioLocations = { + win32: [ + "C:\\Program Files\\Factorio", + "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Factorio", + ], + darwin: [ + "/Applications/factorio.app/Contents", + ], +}; + + +class LineSplitter extends stream.Transform { + constructor(options) { + super(options); + this._partial = null; + } + + _transform(chunk, encoding, callback) { + if (this._partial) { + chunk = Buffer.concat([this._partial, chunk]); + this._partial = null; + } + + while (chunk.length) { + let end = chunk.indexOf("\n"); + if (end === -1) { + this._partial = chunk; + break; + } + + let next = end + 1; + // Eat carriage return as well if present + if (end >= 1 && chunk[end-1] === "\r".charCodeAt(0)) { + end -= 1; + } + + let line = chunk.slice(0, end); + chunk = chunk.slice(next); + this.push(line); + } + callback(); + } + + _flush(callback) { + if (this._partial) { + this.push(this._partial); + this._partial = null; + } + callback(); + } +} + +class InstallError extends Error { } + + +async function execFile(cmd, args) { + logger.verbose(`executing ${cmd} ${args.join(" ")}`); + const asyncExec = util.promisify(child_process.execFile); + return new Promise((resolve, reject) => { + let child = child_process.execFile(cmd, args, (err, stdout, stderr) => { + if (err) { + reject(err); + } else { + resolve({ stdout, stderr }); + } + }); + let stdout = new LineSplitter({ readableObjectMode: true }); + stdout.on("data", line => { logger.verbose(line.toString()); }); + child.stdout.pipe(stdout); + let stderr = new LineSplitter({ readableObjectMode: true }); + stderr.on("data", line => { logger.verbose(`err: ${line.toString()}`); }); + child.stderr.pipe(stderr); + }); +} + +async function execMaster(args) { + if (dev) { + return await execFile("node", [path.join("packages", "master"), ...args]); + } + return await execFile(path.join("node_modules", ".bin", `clusteriomaster${scriptExt}`), args); +} + +async function execSlave(args) { + if (dev) { + return await execFile("node", [path.join("packages", "slave"), ...args]); + } + return await execFile(path.join("node_modules", ".bin", `clusterioslave${scriptExt}`), args); +} + +function validateSlaveToken(token) { + let parts = token.split("."); + if (parts.length !== 3) { + throw new InstallError("Invalid token"); + } + + let parsed; + try { + parsed = JSON.parse(Buffer.from(parts[1], "base64")); + } catch (err) { + throw new InstallError("Invalid token"); + } + + if (parsed.aud !== "slave" || !Number.isInteger(parsed.slave)) { + throw new InstallError("Invalid token"); + } +} + +async function validateInstallDir() { + let entries = new Set(await fs.readdir(".")); + if (entries.size) { + if (!entries.has("package.json")) { + throw new InstallError("Refusing to install to non-empty directory"); + } + + let packageData; + try { + packageData = JSON.parse(await fs.readFile("package.json")); + } catch (err) { + throw new InstallError(`Failed to read package.json: ${err.message}`); + } + if (packageData.name !== "clusterio-install" || !packageData.private) { + throw new InstallError("Refusing to run on non-clusterio installation"); + } + } +} + +async function installClusterio(mode, plugins) { + try { + await fs.writeFile("package.json", JSON.stringify({ + name: "clusterio-install", + private: true, + }, null, 2), { flag: "wx" }); + } catch (err) { + if (err.code !== "EEXIST") { + throw new InstallError(`Failed to write package.json: ${err.message}`); + } + } + + let components = []; + if (["standalone", "master"].includes(mode)) { + components.push("@clusterio/master"); + } + if (["standalone", "slave"].includes(mode)) { + components.push("@clusterio/slave"); + } + if (mode === "ctl") { + components.push("@clusterio/ctl"); + } + + logger.info(`Please wait, installing ${mode}`); + try { + await execFile(`npm${scriptExt}`, ["install", ...components, ...plugins]); + } catch (err) { + 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)) { + // eslint-disable-next-line node/global-require + let pluginInfo = require(require.resolve(path.posix.join(plugin, "info"), { paths: [process.cwd()] })); + pluginList.set(pluginInfo.name, plugin); + } + } + try { + await fs.writeFile("plugin-list.json", JSON.stringify([...pluginList], null, 4)); + } catch (err) { + throw new InstallError(`Error writing plugin-list.json: ${err.message}`); + } + } +} + +async function writeScripts(mode) { + if (["standalone", "master"].includes(mode)) { + if (process.platform === "win32") { + await fs.writeFile( + "run-master.cmd", + "@echo off\n.\\node_modules\\.bin\\clusteriomaster.cmd run\n" + ); + } else { + await fs.writeFile( + "run-master.sh", + "#!/bin/sh\nexec ./node_modules/.bin/clusteriomaster run\n", + { mode: 0o755 }, + ); + await fs.outputFile( + "systemd/clusteriomaster.service", + `[Unit] +Description=Clusterio Master + +[Service] +User=${os.userInfo().username} +Group=nogroup +WorkingDirectory=${process.cwd()} +KillMode=mixed +KillSignal=SIGINT +ExecStart=${process.cwd()}/node_modules/.bin/clusteriomaster run --log-level=warn + +[Install] +WantedBy=multi-user.target +`); + } + } + + if (["standalone", "slave"].includes(mode)) { + if (process.platform === "win32") { + await fs.writeFile( + "run-slave.cmd", + "@echo off\n.\\node_modules\\.bin\\clusterioslave.cmd run\n" + ); + } else { + await fs.writeFile( + "run-slave.sh", + "#!/bin/sh\nexec ./node_modules/.bin/clusterioslave run\n", + { mode: 0o755 }, + ); + await fs.outputFile( + "systemd/clusterioslave.service", + `[Unit] +Description=Clusterio Slave + +[Service] +User=${os.userInfo().username} +Group=nogroup +WorkingDirectory=${process.cwd()} +KillMode=mixed +KillSignal=SIGINT +ExecStart=${process.cwd()}/node_modules/.bin/clusterioslave run --log-level=warn + +[Install] +WantedBy=multi-user.target +`); + } + } +} + +async function inquirerMissingArgs(args) { + let answers = {}; + if (args.mode) { answers.mode = args.mode; } + answers = await inquirer.prompt([ + { + type: "list", + name: "mode", + message: "Operating mode to install", + default: "standalone", + choices: [ + { name: "Standalone (install both master and slave on this computer)", value: "standalone" }, + { name: "Master only", value: "master" }, + { name: "Slave only", value: "slave" }, + { name: "Ctl only", value: "ctl" }, + { name: "Plugins only", value: "plugins" }, + ], + }, + ], answers); + + if (["standalone", "master"].includes(answers.mode)) { + if (args.admin) { answers.admin = args.admin; } + answers = await inquirer.prompt([ + { + type: "input", + name: "admin", + message: "Admin account name", + validate: input => { + if (!input) { + return "May not be empty"; + } + return true; + }, + }, + ], answers); + } + + if (answers.mode === "slave") { + if (args.slaveName) { answers.slaveName = args.slaveName; } + answers = await inquirer.prompt([ + { + type: "input", + name: "slaveName", + message: "Name of slave", + }, + ], answers); + } + + if (["slave", "ctl"].includes(answers.mode)) { + if (args.masterUrl) { answers.masterUrl = args.masterUrl; } + answers = await inquirer.prompt([ + { + type: "input", + name: "masterUrl", + message: "Master server URL", + }, + ], answers); + + if (args.masterToken) { answers.masterToken = args.masterToken; } + answers = await inquirer.prompt([ + { + type: "input", + name: "masterToken", + message: "Master authentication Token", + }, + ], answers); + } + + if (answers.mode === "slave") { + validateSlaveToken(answers.masterToken); + } + + if (["standalone", "slave"].includes(answers.mode)) { + let myIp; + if (args.publicAddress) { + answers.publicAddress = args.publicAddress; + } else { + try { + let result = await phin("https://api.ipify.org/"); + myIp = result.body.toString(); + } catch (err) { /* ignore */ } + } + answers = await inquirer.prompt([ + { + type: "input", + name: "publicAddress", + message: "Public DNS/IP address of this server", + default: myIp, + }, + ], answers); + + if (args.factorioDir) { answers.factorioDir = args.factorioDir; } + let locations = factorioLocations[process.platform] || []; + let foundLocations = []; + for (let location of locations) { + if (await fs.pathExists(path.join(location, "data", "changelog.txt"))) { + foundLocations.push(location); + } + } + + answers = await inquirer.prompt([ + { + type: "list", + name: "factorioDir", + message: "Path to Factorio installation", + choices: [ + ...foundLocations.map(location => ({ name: `${location} (auto detected)`, value: location })), + { name: "Use local factorio directory, you must copy an installation to it", value: "factorio" }, + { name: "Provide path manually", value: null }, + ], + }, + ], answers); + + if (answers.factorioDir === null) { + answers = await inquirer.prompt([ + { + type: "input", + name: "factorioDir", + message: "Path to Factorio installation", + askAnswered: true, + }, + ], answers); + } else if (answers.factorioDir === "factorio") { + await fs.ensureDir("factorio"); + } + } + + if (dev) { answers.plugins = []; } + answers = await inquirer.prompt([ + { + type: "checkbox", + name: "plugins", + message: "Plugins to install", + choices: availablePlugins, + pageSize: 20, + }, + ], answers); + + return answers; +} + + +async function main() { + const args = yargs + .option("log-level", { + nargs: 1, describe: "Log level to print to stdout", default: "info", + choices: ["none"].concat(Object.keys(levels)), type: "string", + }) + .option("dev", { + nargs: 0, describe: "Initialize development repository", hidden: true, default: false, type: "boolean", + }) + .option("mode", { + nargs: 1, describe: "Operating mode to install", + choices: ["standalone", "master", "slave", "ctl", "plugins"], + }) + .option("admin", { + nargs: 1, describe: "Admin account name [standalone/master]", type: "string", + }) + .option("slave-name", { + nargs: 1, describe: "Slave name [slave]", type: "string", + }) + .option("master-url", { + nargs: 1, describe: "Master URL [slave/ctl]", type: "string", + }) + .option("master-token", { + nargs: 1, describe: "Master authentication token [slave/ctl]", type: "string", + }) + .option("public-address", { + nargs: 1, describe: "DNS/IP Address to connect to this server [standalone/slave]", type: "string", + }) + .option("factorio-dir", { + nargs: 1, describe: "Path to Factorio installation [standalone/slave]", type: "string", + }) + .option("plugins", { + array: true, describe: "Plugins to install", type: "string", + }) + .argv + ; + + setLogLevel(args.logLevel === "none" ? -1 : levels[args.logLevel]); + dev = args.dev; + + if (!dev) { + await validateInstallDir(); + } + + let answers = await inquirerMissingArgs(args); + logger.verbose(JSON.stringify(answers)); + + if (!dev) { + await installClusterio(answers.mode, answers.plugins); + } + + let adminToken = null; + if (["standalone", "master"].includes(answers.mode)) { + logger.info("Setting up master"); + await execMaster(["bootstrap", "create-admin", answers.admin]); + let result = await execMaster(["bootstrap", "generate-user-token", answers.admin]); + adminToken = result.stdout.split("\n").slice(-2)[0]; + } + + if (answers.mode === "standalone") { + logger.info("Setting up slave"); + await execSlave(["config", "set", "slave.name", "local"]); + + let result = await execSlave(["config", "show", "slave.id"]); + let slaveId = Number.parseInt(result.stdout.split("\n").slice(-2)[0], 10); + + result = await execMaster(["bootstrap", "generate-slave-token", slaveId]); + let slaveToken = result.stdout.split("\n").slice(-2)[0]; + + await execSlave(["config", "set", "slave.master_token", slaveToken]); + await execSlave(["config", "set", "slave.factorio_directory", answers.factorioDir]); + } + + if (answers.mode === "slave") { + logger.info("Setting up slave"); + let slaveId = JSON.parse(Buffer.from(answers.masterToken.split(".")[1], "base64")).slave; + await execSlave(["config", "set", "slave.id", slaveId]); + await execSlave(["config", "set", "slave.name", answers.slaveName]); + await execSlave(["config", "set", "slave.master_url", answers.masterUrl]); + await execSlave(["config", "set", "slave.master_token", answers.masterToken]); + await execSlave(["config", "set", "slave.factorio_directory", answers.factorioDir]); + } + + if (!dev && ["standalone", "master", "slave"].includes(answers.mode)) { + logger.info("Writing run scripts"); + await writeScripts(answers.mode); + } + + /* eslint-disable no-console */ + console.log(`Successfully installed ${answers.mode}`); + if (adminToken) { + console.log(`Admin authentication token: ${adminToken}`); + } + /* eslint-enable no-console */ +} + +if (module === require.main) { + main().catch(err => { + if (err instanceof InstallError) { + logger.error(err.message); + } else { + logger.fatal(` ++--------------------------------------------------------------+ +| Unexpected error occured installing clusterio, please report | +| it to https://github.com/clusterio/factorioClusterio/issues | ++--------------------------------------------------------------+ +${err.stack}` + ); + } + }); +} diff --git a/packages/create/logging.js b/packages/create/logging.js new file mode 100644 index 00000000..265dd8e1 --- /dev/null +++ b/packages/create/logging.js @@ -0,0 +1,51 @@ +/* eslint-disable no-console */ +"use strict"; +const chalk = require("chalk"); + +const levels = Object.freeze({ + fatal: 0, + error: 1, + warn: 2, + audit: 3, + info: 4, + verbose: 6, +}); + +const colors = { + fatal: "inverse red", + error: "bold redBright", + warn: "bold yellowBright", + audit: "bold greenBright", + info: "bold blueBright", + verbose: "bold grey", +}; + +let maxLogLevel = levels.info; +const logger = {}; +for (let [name, level] of Object.entries(levels)) { + // eslint-disable-next-line no-loop-func + logger[name] = message => { + if (level > maxLogLevel) { + return; + } + let style = chalk; + for (let part of colors[name].split(" ")) { + style = style[part]; + } + if (["fatal", "error"].includes(name)) { + console.error(`${style(name)} ${message}`); + } else if (["warn"].includes(name)) { + console.warn(`${style(name)} ${message}`); + } else { + console.log(`${style(name)} ${message}`); + } + }; +} + +module.exports = { + colors, + levels, + setLogLevel: (level) => { maxLogLevel = level; }, + logger, +}; + diff --git a/packages/create/package.json b/packages/create/package.json new file mode 100644 index 00000000..6e452f11 --- /dev/null +++ b/packages/create/package.json @@ -0,0 +1,27 @@ +{ + "name": "@clusterio/create", + "description": "Installer for clusterio", + "author": "Hornwitser", + "license": "MIT", + "version": "2.0.0-alpha.0", + "main": "create.js", + "bin": { + "clusteriocreate": "./create.js" + }, + "keywords": [ + "factorio", "clusterio" + ], + "engines": { + "node": ">=12" + }, + "dependencies": { + "chalk": "^4.1.0", + "fs-extra": "^8.1.0", + "inquirer": "^8.1.0", + "phin": "^3.5.1", + "yargs": "^14.2.0" + }, + "publishConfig": { + "access": "public" + } +}