diff --git a/docs/configuration.md b/docs/configuration.md index c09cafcd..8d8e73b7 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -239,6 +239,14 @@ Does not affect start or stop of the instance while the host is running. Defaults to false. +### instance.exclude_from_start_all + +If enabled this instance will be excluded from the "Start all" button operation in the web interface. +This allows you to prevent certain instances from being started when using the bulk start operation. + +Defaults to false. + + ### factorio.version Version of Factorio to use for this instance. diff --git a/packages/controller/src/InstanceInfo.ts b/packages/controller/src/InstanceInfo.ts index f04c1a3f..36959ee6 100644 --- a/packages/controller/src/InstanceInfo.ts +++ b/packages/controller/src/InstanceInfo.ts @@ -45,6 +45,7 @@ export default class InstanceInfo { this.status, this.factorioVersion, this.updatedAtMs, + this.config.get("instance.exclude_from_start_all"), ); } diff --git a/packages/ctl/src/commands.ts b/packages/ctl/src/commands.ts index 070e63f5..59be29b0 100644 --- a/packages/ctl/src/commands.ts +++ b/packages/ctl/src/commands.ts @@ -1065,6 +1065,61 @@ instanceCommands.add(new lib.Command({ }, })); +instanceCommands.add(new lib.Command({ + definition: ["start-all", "Start all stopped instances (respects exclude_from_start_all setting)", (yargs) => { + yargs.options({ + "force": { + describe: "Start instances even if they have exclude_from_start_all set", + nargs: 0, + type: "boolean", + default: false, + }, + }); + }], + handler: async function(args: { force: boolean }, control: Control) { + // Get all instances + const instances = await control.send(new lib.InstanceDetailsListRequest()); + + // Filter instances that should be started + const instancesToStart = instances.filter((instance: lib.InstanceDetails) => { + // Only start instances that are currently stopped + if (instance.status !== "stopped") { + return false; + } + + // Respect exclude_from_start_all setting unless --force is used + if (!args.force && instance.excludeFromStartAll) { + return false; + } + + return true; + }); + + if (instancesToStart.length === 0) { + print("No instances to start."); + return; + } + + print(`Starting ${instancesToStart.length} instance(s):`); + for (const instance of instancesToStart) { + print(` - ${instance.name} (ID: ${instance.id})`); + } + + // Start all filtered instances + const startPromises = instancesToStart.map(async (instance: lib.InstanceDetails) => { + try { + await control.sendTo({ instanceId: instance.id }, new lib.InstanceStartRequest()); + print(`✓ Started ${instance.name}`); + } catch (error) { + print(`✗ Failed to start ${instance.name}: ${error}`); + } + }); + + await Promise.all(startPromises); + print("Start all operation completed."); + }, +})); + instanceCommands.add(new lib.Command({ definition: ["load-scenario ", "Start instance by loading a scenario", (yargs) => { yargs.positional("instance", { describe: "Instance to start", type: "string" }); @@ -1110,6 +1165,41 @@ instanceCommands.add(new lib.Command({ }, })); +instanceCommands.add(new lib.Command({ + definition: ["stop-all", "Stop all running instances", () => {}], + handler: async function(_args, control: Control) { + // Get all instances + const instances = await control.send(new lib.InstanceDetailsListRequest()); + + // Filter instances that should be stopped + const instancesToStop = instances + .filter((instance: lib.InstanceDetails) => ["starting", "running"].includes(instance.status)); + + if (instancesToStop.length === 0) { + print("No instances to stop."); + return; + } + + print(`Stopping ${instancesToStop.length} instance(s):`); + for (const instance of instancesToStop) { + print(` - ${instance.name} (ID: ${instance.id})`); + } + + // Stop all filtered instances + const stopPromises = instancesToStop.map(async (instance: lib.InstanceDetails) => { + try { + await control.sendTo({ instanceId: instance.id }, new lib.InstanceStopRequest()); + print(`✓ Stopped ${instance.name}`); + } catch (error) { + print(`✗ Failed to stop ${instance.name}: ${error}`); + } + }); + + await Promise.all(stopPromises); + print("Stop all operation completed."); + }, +})); + instanceCommands.add(new lib.Command({ definition: ["kill ", "Kill instance", (yargs) => { yargs.positional("instance", { describe: "Instance to kill", type: "string" }); diff --git a/packages/lib/src/config/definitions.ts b/packages/lib/src/config/definitions.ts index f9f81dcc..138220fe 100644 --- a/packages/lib/src/config/definitions.ts +++ b/packages/lib/src/config/definitions.ts @@ -398,6 +398,7 @@ export interface InstanceConfigFields { "instance.id": number; "instance.assigned_host": number | null; "instance.auto_start": boolean; + "instance.exclude_from_start_all": boolean; "factorio.version": string; "factorio.executable_path": string | null; @@ -465,6 +466,11 @@ export class InstanceConfig extends classes.Config { type: "boolean", initialValue: false, }, + "instance.exclude_from_start_all": { + description: "Exclude this instance from the 'Start all' button operation", + type: "boolean", + initialValue: false, + }, "factorio.version": { description: "Version of the game to run, use latest to run the latest installed version", restartRequired: true, diff --git a/packages/lib/src/data/ModPack.ts b/packages/lib/src/data/ModPack.ts index f6aa2c65..74030b49 100644 --- a/packages/lib/src/data/ModPack.ts +++ b/packages/lib/src/data/ModPack.ts @@ -161,7 +161,7 @@ export default class ModPack { if (json.name) { modPack.name = json.name; } if (json.description) { modPack.description = json.description; } if (json.factorio_version) { modPack.factorioVersion = json.factorio_version; } - if (json.mods) { modPack.mods = new Map(json.mods.map(m => [m.name, m])); } + if (json.mods) { modPack.mods = new Map(json.mods.map((m: ModRecord) => [m.name, m])); } if (json.settings) { modPack.settings = { "startup": new Map(Object.entries(json.settings["startup"])), diff --git a/packages/lib/src/data/messages_instance.ts b/packages/lib/src/data/messages_instance.ts index 19a34eba..08119562 100644 --- a/packages/lib/src/data/messages_instance.ts +++ b/packages/lib/src/data/messages_instance.ts @@ -45,6 +45,7 @@ export class InstanceDetails { public factorioVersion: string | undefined, /** Millisecond Unix timestamp this entry was last updated at */ public updatedAtMs = 0, + public excludeFromStartAll = false, ) { } static jsonSchema = Type.Object({ @@ -58,6 +59,7 @@ export class InstanceDetails { ]), "factorioVersion": Type.Optional(Type.String()), "updatedAtMs": Type.Optional(Type.Number()), + "excludeFromStartAll": Type.Optional(Type.Boolean()), }); static fromJSON(json: Static) { @@ -69,6 +71,7 @@ export class InstanceDetails { json.status, json.factorioVersion, json.updatedAtMs, + json.excludeFromStartAll, ); } diff --git a/packages/web_ui/src/components/InstancesPage.tsx b/packages/web_ui/src/components/InstancesPage.tsx index 8a7968e8..cfa24922 100644 --- a/packages/web_ui/src/components/InstancesPage.tsx +++ b/packages/web_ui/src/components/InstancesPage.tsx @@ -86,7 +86,7 @@ export default function InstancesPage() { {account.hasPermission("core.instance.create") && } {account.hasPermission("core.instance.start") &&