mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 15:04:52 +09:00
The behaviour of creating the config file if it's missing while convenient causes confusion and problems when the tools are run in the wrong folder. They end up creating a shadow installation and misdirect the user with nonsensical error messages, instead pointing to the real problem which is that the tool can't find the configuration needed to actually run. Require a config file to be present at startup and refuse to run if this file is missing. To support the odd use-case of creating the config when missing a --create-config flag is added. To conveniently generate a config file to edit out from the "config create" command is added.
305 lines
8.7 KiB
TypeScript
305 lines
8.7 KiB
TypeScript
/**
|
|
* Implementation of commands shared between controller/host/ctl.
|
|
* @module lib/shared_commands
|
|
*/
|
|
import fs from "fs/promises";
|
|
import path from "path";
|
|
import { spawn } from "child_process";
|
|
|
|
import { loadPluginList } from "./load_plugin_list.js";
|
|
import * as libConfig from "./config/index.js";
|
|
import * as libFileOps from "./file_ops.js";
|
|
import { logger } from "./logging.js";
|
|
import * as libHelpers from "./helpers.js";
|
|
import { LockFile } from "./LockFile.js";
|
|
import { pathToFileURL } from "url";
|
|
|
|
|
|
function print(...content: any[]) {
|
|
// eslint-disable-next-line no-console
|
|
console.log(...content);
|
|
}
|
|
|
|
/**
|
|
* Yargs plugin command
|
|
*
|
|
* Can be passed to yargs.command to implement a plugin list management
|
|
* command. Use handlePluginCommand to do the requested action.
|
|
*
|
|
* @param yargs - yargs command builder.
|
|
*/
|
|
export function pluginCommand(yargs: any) {
|
|
yargs
|
|
.command("install <package>", "Install plugin from npm and add it")
|
|
.command("add <path>", "Add plugin by require path")
|
|
.command("remove <name>", "Remove plugin by name")
|
|
.command("list", "List all plugins and their path")
|
|
.demandCommand(1, "You need to specify a command to run")
|
|
.help()
|
|
.strict()
|
|
;
|
|
}
|
|
|
|
const corePackages = ["@clusterio/controller", "@clusterio/host", "@clusterio/ctl"];
|
|
|
|
/**
|
|
* Check if the current directory is a Clusterio installation
|
|
*
|
|
* A directory counts as an installation if it has a package.json created
|
|
* by the installer or one that depends on a Clusterio package.
|
|
*/
|
|
export async function isClusterioInstall(dir = process.cwd()) {
|
|
let packageJson: { name?: string, dependencies?: Record<string, string> };
|
|
try {
|
|
packageJson = JSON.parse(await fs.readFile(path.join(dir, "package.json"), "utf8"));
|
|
} catch (err: any) {
|
|
if (err.code === "ENOENT") {
|
|
return false;
|
|
}
|
|
throw err;
|
|
}
|
|
if (packageJson.name === "clusterio-install") {
|
|
return true;
|
|
}
|
|
return corePackages.some(name => packageJson.dependencies?.[name] !== undefined);
|
|
}
|
|
|
|
async function readDependencies(): Promise<Record<string, string>> {
|
|
const packageJson = JSON.parse(await fs.readFile("package.json", "utf8"));
|
|
return packageJson.dependencies ?? {};
|
|
}
|
|
|
|
async function packageNameFromSpec(packageSpec: string) {
|
|
try {
|
|
const packageJson = JSON.parse(await fs.readFile(path.join(packageSpec, "package.json"), "utf8"));
|
|
if (typeof packageJson.name === "string") {
|
|
return packageJson.name;
|
|
}
|
|
} catch (err: any) {
|
|
if (!["ENOENT", "ENOTDIR"].includes(err.code)) {
|
|
throw err;
|
|
}
|
|
}
|
|
return packageSpec.replace(/(?!^)@.*$/, "");
|
|
}
|
|
|
|
function npmInstall(packageSpec: string) {
|
|
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
return new Promise<void>((resolve, reject) => {
|
|
const child = spawn(npm, ["install", "--save", "--no-audit", "--no-fund", packageSpec], {
|
|
stdio: "inherit",
|
|
shell: process.platform === "win32",
|
|
});
|
|
child.on("error", reject);
|
|
child.on("exit", code => {
|
|
if (code === 0) {
|
|
resolve();
|
|
} else {
|
|
reject(new Error(`npm install exited with code ${code}`));
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handle yargs plugin command
|
|
*
|
|
* Handle the actions that are made available by pluginCommand.
|
|
*
|
|
* @param args - yargs args object.
|
|
* @param pluginList - Current list of plugins.
|
|
* @param pluginListPath - Path to plugin list config file.
|
|
*/
|
|
export async function handlePluginCommand(
|
|
args: Record<string, unknown>,
|
|
pluginList: Map<string, string>,
|
|
pluginListPath: string
|
|
) {
|
|
let command = (args._ as string[])[1];
|
|
const wrongDirHint = `${process.cwd()} is not a Clusterio installation, run this command from the directory ` +
|
|
"Clusterio was installed in";
|
|
|
|
if (command === "install") {
|
|
if (!await isClusterioInstall()) {
|
|
logger.error(wrongDirHint);
|
|
process.exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
const packageSpec = args.package as string;
|
|
const depsBefore = await readDependencies();
|
|
try {
|
|
await npmInstall(packageSpec);
|
|
} catch (err: any) {
|
|
logger.error(`Failed to install ${packageSpec}: ${err.message}`);
|
|
process.exitCode = 1;
|
|
return;
|
|
}
|
|
const depsAfter = await readDependencies();
|
|
|
|
const newPluginList = await loadPluginList(pluginListPath);
|
|
const added = [...newPluginList.keys()].filter(name => !pluginList.has(name));
|
|
for (const name of added) {
|
|
pluginList.set(name, newPluginList.get(name)!);
|
|
print(`Installed ${name}`);
|
|
}
|
|
if (added.length) {
|
|
return;
|
|
}
|
|
|
|
// Nothing new, either the plugin was already in the list or the package is not a plugin.
|
|
let packageNames = Object.keys(depsAfter).filter(name => depsAfter[name] !== depsBefore[name]);
|
|
if (!packageNames.length) {
|
|
packageNames = [await packageNameFromSpec(packageSpec)];
|
|
}
|
|
const existing = [...pluginList].filter(([, requirePath]) => packageNames.includes(requirePath));
|
|
if (existing.length) {
|
|
for (const [name] of existing) {
|
|
print(`${name} is already installed`);
|
|
}
|
|
} else {
|
|
logger.error(`${packageSpec} was installed but did not provide any plugins`);
|
|
process.exitCode = 1;
|
|
}
|
|
|
|
} else if (command === "add") {
|
|
let pluginPath = args.path as string;
|
|
if (/^\.\.?[\/\\]/.test(pluginPath)) {
|
|
pluginPath = path.resolve(pluginPath);
|
|
} else if (!path.isAbsolute(pluginPath) && !await isClusterioInstall()) {
|
|
logger.warn(wrongDirHint);
|
|
}
|
|
|
|
let pluginInfo: { name: string };
|
|
try {
|
|
const pluginPackage = (await import(
|
|
pathToFileURL(path.posix.join(pluginPath, "package.json")).href,
|
|
{ with: { type: "json" }},
|
|
)).default;
|
|
pluginInfo = (await import(
|
|
pathToFileURL(path.posix.join(pluginPath, pluginPackage.main ?? "index.js")).href
|
|
)).plugin;
|
|
} catch (err: any) {
|
|
logger.error(`Unable to import plugin info from ${args.path}:\n${err.stack}`);
|
|
process.exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
if (typeof pluginInfo !== "object") {
|
|
logger.error("Plugin does not correctly export a 'plugin' object");
|
|
process.exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
if (pluginList.has(pluginInfo.name)) {
|
|
logger.error(`Plugin with the same ${pluginInfo.name} already exists`);
|
|
process.exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
pluginList.set(pluginInfo.name, pluginPath);
|
|
await libFileOps.safeOutputFile(pluginListPath, JSON.stringify([...pluginList], null, "\t"));
|
|
print(`Added ${pluginInfo.name}`);
|
|
|
|
} else if (command === "remove") {
|
|
if (!pluginList.delete(args.name as string)) {
|
|
logger.error(`Plugin with name ${args.name} does not exist`);
|
|
process.exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
await libFileOps.safeOutputFile(pluginListPath, JSON.stringify([...pluginList], null, "\t"));
|
|
print(`Removed ${args.name}`);
|
|
|
|
} else if (command === "list") {
|
|
for (let [pluginName, pluginPath] of pluginList) {
|
|
print(`${pluginName} - ${pluginPath}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Yargs config command
|
|
*
|
|
* Can be passed to yargs.command to implement a config command. Use
|
|
* handleConfigCommand to do the requested action.
|
|
*
|
|
* @param yargs - yargs command builder.
|
|
*/
|
|
export function configCommand(yargs: any) {
|
|
yargs
|
|
.command("create", "Create a new config file if not present")
|
|
.command("set <field> [value]", "Set config field", (yargs: any) => {
|
|
yargs.options({
|
|
"stdin": { describe: "read value from stdin", nargs: 0, type: "boolean" },
|
|
});
|
|
})
|
|
.command("show <field>", "Show value of the given config field")
|
|
.command("list", "List all configuration fields and their values")
|
|
.demandCommand(1, "You need to specify a command to run")
|
|
.help()
|
|
.strict()
|
|
;
|
|
}
|
|
|
|
/**
|
|
* Handle yargs command
|
|
*
|
|
* Handle the actions that are made available by configCommand.
|
|
*
|
|
* @param args - yargs args object.
|
|
* @param instance - Config instance.
|
|
* @param lockFile - Lockfile required if attempting to write
|
|
*/
|
|
export async function handleConfigCommand(
|
|
args: Record<string, unknown>,
|
|
instance: libConfig.Config<any>,
|
|
lockFile: LockFile,
|
|
) {
|
|
let command = (args._ as string[])[1];
|
|
|
|
if (command === "create") {
|
|
// By the time we get here loadConfigFromArgs will have already created the config.
|
|
|
|
} else if (command === "list") {
|
|
for (const name of Object.keys(instance.constructor.fieldDefinitions)) {
|
|
print(`${name} ${JSON.stringify(instance.get(name))}`);
|
|
}
|
|
|
|
} else if (command === "show") {
|
|
try {
|
|
print(instance.get(args.field as string));
|
|
} catch (err) {
|
|
if (err instanceof libConfig.InvalidField) {
|
|
logger.error(err.message);
|
|
} else {
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
} else if (command === "set") {
|
|
await lockFile.acquire();
|
|
|
|
if (args.stdin) {
|
|
args.value = (await libHelpers.readStream(process.stdin)).toString().replace(/\r?\n$/, "");
|
|
|
|
} else if (args.value === undefined) {
|
|
args.value = null;
|
|
}
|
|
|
|
try {
|
|
instance.set(args.field as string, args.value);
|
|
await instance.save();
|
|
} catch (err) {
|
|
if (err instanceof libConfig.InvalidField || err instanceof libConfig.InvalidValue) {
|
|
logger.error(err.message);
|
|
} else {
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
await lockFile.release();
|
|
}
|
|
}
|