mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-12 23:04:52 +09:00
serializedConfigToString now asks a control-located config instance whether each field is readable and writable instead of relying on a hard-coded list per command. Hidden and unknown fields are skipped, and field descriptions are written as comments above each value. Object fields such as factorio.settings are now included, so they are written as JSON, and configToKeyVal splits on the first = only so those values survive the round trip. Fixes #1051 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
211 lines
7.8 KiB
TypeScript
211 lines
7.8 KiB
TypeScript
import fs from "node:fs/promises";
|
|
import asTableModule from "as-table";
|
|
import events from "events";
|
|
import os from "os";
|
|
import child_process from "child_process";
|
|
|
|
import * as lib from "@clusterio/lib";
|
|
import type { Ctl } from "../ctl.js";
|
|
import { print } from "./command_ops.js";
|
|
import { serializedConfigToString, getEditor, configToKeyVal } from "./config_ops.js";
|
|
|
|
const asTable = asTableModule.configure({ delimiter: " | " });
|
|
|
|
export const controllerCommands = new lib.CommandTree({ name: "controller", description: "Controller management" });
|
|
controllerCommands.add(new lib.Command({
|
|
definition: ["stop", "Stop controller"],
|
|
handler: async function(_args: object, ctl: Ctl) {
|
|
await ctl.send(new lib.ControllerStopRequest());
|
|
},
|
|
}));
|
|
|
|
controllerCommands.add(new lib.Command({
|
|
definition: ["restart", "Restart controller"],
|
|
handler: async function(_args: object, ctl: Ctl) {
|
|
await ctl.send(new lib.ControllerRestartRequest());
|
|
},
|
|
}));
|
|
controllerCommands.add(new lib.Command({
|
|
definition: ["mark-restart-required", "Mark the controller as requiring a restart"],
|
|
handler: async function(_args: object, ctl: Ctl) {
|
|
await ctl.send(new lib.ControllerMarkRestartRequiredRequest());
|
|
},
|
|
}));
|
|
controllerCommands.add(new lib.Command({
|
|
definition: ["update", "Update the controller", (yargs) => {
|
|
yargs.option("restart", { alias: "r", type: "boolean", description: "Restart after update" });
|
|
yargs.option("all", { alias: "a", type: "boolean", description: "Also update all plugins" });
|
|
}],
|
|
handler: async function(args: { restart: boolean, all: boolean }, ctl: Ctl) {
|
|
if (args.all) {
|
|
await ctl.send(new lib.UpdateAllRequest());
|
|
} else {
|
|
await ctl.send(new lib.ControllerUpdateRequest());
|
|
}
|
|
if (args.restart) {
|
|
await ctl.send(new lib.ControllerRestartRequest());
|
|
} else {
|
|
print("Controller updated; a restart is required to apply the changes.");
|
|
}
|
|
},
|
|
}));
|
|
|
|
const controllerConfigCommands = new lib.CommandTree({
|
|
name: "config", alias: ["c"], description: "controller config management",
|
|
});
|
|
|
|
controllerConfigCommands.add(new lib.Command({
|
|
definition: ["list", "List controller configuration"],
|
|
handler: async function(args: object, ctl: Ctl) {
|
|
let config = await ctl.send(new lib.ControllerConfigGetRequest());
|
|
|
|
for (let [name, value] of Object.entries(config)) {
|
|
print(`${name} ${JSON.stringify(value)}`);
|
|
}
|
|
},
|
|
}));
|
|
|
|
controllerConfigCommands.add(new lib.Command({
|
|
definition: ["set <field> [value]", "Set field in controller config", (yargs) => {
|
|
yargs.positional("field", { describe: "Field to set", type: "string" });
|
|
yargs.positional("value", { describe: "Value to set", type: "string" });
|
|
yargs.options({
|
|
"stdin": { describe: "read value from stdin", nargs: 0, type: "boolean" },
|
|
});
|
|
}],
|
|
handler: async function(args: { field: string, value?: string, stdin?: boolean }, ctl: Ctl) {
|
|
if (args.stdin) {
|
|
args.value = (await lib.readStream(process.stdin)).toString().replace(/\r?\n$/, "");
|
|
} else if (args.value === undefined) {
|
|
args.value = "";
|
|
}
|
|
await ctl.send(new lib.ControllerConfigSetFieldRequest(args.field, args.value));
|
|
},
|
|
}));
|
|
|
|
controllerConfigCommands.add(new lib.Command({
|
|
definition: ["set-prop <field> <prop> [value]", "Set property of field in controller config", (yargs) => {
|
|
yargs.positional("field", { describe: "Field to set", type: "string" });
|
|
yargs.positional("prop", { describe: "Property to set", type: "string" });
|
|
yargs.positional("value", { describe: "JSON parsed value to set", type: "string" });
|
|
yargs.options({
|
|
"stdin": { describe: "read value from stdin", nargs: 0, type: "boolean" },
|
|
});
|
|
}],
|
|
handler: async function(args: { field: string, prop: string, value?: string, stdin?: boolean }, ctl: Ctl) {
|
|
if (args.stdin) {
|
|
args.value = (await lib.readStream(process.stdin)).toString().replace(/\r?\n$/, "");
|
|
}
|
|
let value;
|
|
try {
|
|
if (args.value !== undefined) {
|
|
value = JSON.parse(args.value);
|
|
}
|
|
} catch (err: any) {
|
|
// See note for the instance version of set-prop
|
|
if (args.stdin || /^(\[.*]|{.*}|".*")$/.test(args.value!)) {
|
|
throw new lib.CommandError(`In parsing value '${args.value}': ${err.message}`);
|
|
}
|
|
value = args.value;
|
|
}
|
|
await ctl.send(new lib.ControllerConfigSetPropRequest(args.field, args.prop, value));
|
|
},
|
|
}));
|
|
controllerConfigCommands.add(new lib.Command({
|
|
definition: ["edit [editor]", "Edit controller configuration", (yargs) => {
|
|
yargs.positional("editor", {
|
|
describe: "Editor to use",
|
|
type: "string",
|
|
default: "",
|
|
});
|
|
}],
|
|
handler: async function(args: { editor: string }, ctl: Ctl) {
|
|
let config = await ctl.send(new lib.ControllerConfigGetRequest());
|
|
let tmpFile = await lib.getTempFile("ctl-", "-tmp", os.tmpdir());
|
|
let editor = await getEditor(args.editor);
|
|
if (editor === undefined) {
|
|
throw new lib.CommandError(`No editor avalible. Checked CLI input, EDITOR and VISUAL env vars
|
|
Try "ctl controller config edit <editor of choice>"`);
|
|
}
|
|
let allConfigElements = serializedConfigToString(config, lib.ControllerConfig);
|
|
await fs.writeFile(tmpFile, allConfigElements);
|
|
let editorSpawn = child_process.spawn(editor, [tmpFile], {
|
|
stdio: "inherit",
|
|
detached: false,
|
|
});
|
|
editorSpawn.on("data", (data) => {
|
|
process.stdout.pipe(data);
|
|
});
|
|
await events.once(editorSpawn, "exit");
|
|
const data = await fs.readFile(tmpFile, "utf8");
|
|
const final = await configToKeyVal(data);
|
|
for (let index in final) {
|
|
if (index in final) {
|
|
try {
|
|
await ctl.send(new lib.ControllerConfigSetFieldRequest(
|
|
index,
|
|
final[index],
|
|
));
|
|
} catch (err) {
|
|
// eslint-disable-next-line
|
|
print(`Attempt to set ${index} to ${final[index] || String(null)} failed; set back to previous value.`);
|
|
print(err);
|
|
// If the string is empty, it's better to just print "" instead of nothing
|
|
}
|
|
}
|
|
}
|
|
try {
|
|
await fs.unlink(tmpFile);
|
|
} catch (err) {
|
|
print("err: temporary file", tmpFile, "could not be deleted.");
|
|
print("This is not fatal, but they may build up over time if the issue persists.");
|
|
}
|
|
},
|
|
}));
|
|
|
|
controllerCommands.add(controllerConfigCommands);
|
|
|
|
|
|
const controllerPluginCommands = new lib.CommandTree({
|
|
name: "plugin", alias: ["p"], description: "controller plugin inspection",
|
|
});
|
|
controllerPluginCommands.add(new lib.Command({
|
|
definition: ["list", "List plugins on controller"],
|
|
handler: async function(args: object, ctl: Ctl) {
|
|
let url = new URL(ctl.config.get("ctl.controller_url")!);
|
|
url.pathname += "api/plugins";
|
|
let response = await fetch(url);
|
|
print(asTable(await response.json() as []));
|
|
},
|
|
}));
|
|
controllerPluginCommands.add(new lib.Command({
|
|
definition: ["update <plugin>", "Update a plugin on the controller", (yargs) => {
|
|
yargs.positional("plugin", { describe: "Plugin to update", type: "string" });
|
|
yargs.option("restart", { alias: "r", type: "boolean", description: "Restart after update" });
|
|
}],
|
|
handler: async function(args: { plugin: string, restart: boolean }, ctl: Ctl) {
|
|
await ctl.sendTo("controller", new lib.PluginUpdateRequest(args.plugin));
|
|
if (args.restart) {
|
|
await ctl.send(new lib.ControllerRestartRequest());
|
|
} else {
|
|
print("Plugin updated; a restart is required to apply the changes.");
|
|
}
|
|
},
|
|
}));
|
|
controllerPluginCommands.add(new lib.Command({
|
|
definition: ["install <plugin>", "Install a plugin on the controller", (yargs) => {
|
|
yargs.positional("plugin", { describe: "Plugin to install", type: "string" });
|
|
yargs.option("restart", { alias: "r", type: "boolean", description: "Restart after update" });
|
|
}],
|
|
handler: async function(args: { plugin: string, restart: boolean }, ctl: Ctl) {
|
|
await ctl.sendTo("controller", new lib.PluginInstallRequest(args.plugin));
|
|
if (args.restart) {
|
|
await ctl.send(new lib.ControllerRestartRequest());
|
|
} else {
|
|
print("Plugin installed; a restart is required to apply the changes.");
|
|
}
|
|
},
|
|
}));
|
|
|
|
controllerCommands.add(controllerPluginCommands);
|