@minecraft/creator-tools
Version:
Minecraft Creator Tools command line and libraries.
368 lines (367 loc) • 16 kB
JavaScript
;
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.serverCommand = exports.ServerCommand = void 0;
const IToolCommand_1 = require("../IToolCommand");
/**
* DedicatedServerStatus.started value. Imported dynamically along with other
* server dependencies to avoid pulling in the full DedicatedServer module
* (which has heavy transitive dependencies) at module initialization time.
*/
// These value imports are lazy-loaded at runtime to avoid pulling in heavy
// module dependency chains (WorldLevelDat → NbtBinary → FileBase, etc.)
// at module initialization time — which causes issues in test runners.
async function getServerDependencies() {
const [{ DedicatedServerMode }, { GameType, Generator, Difficulty, PlayerPermissionsLevel }, PackageModule, { DedicatedServerStatus },] = await Promise.all([
Promise.resolve().then(() => __importStar(require("../../ICreatorToolsData"))),
Promise.resolve().then(() => __importStar(require("../../../minecraft/WorldLevelDat"))),
Promise.resolve().then(() => __importStar(require("../../Package"))),
Promise.resolve().then(() => __importStar(require("../../../local/DedicatedServer"))),
]);
return {
DedicatedServerMode,
GameType,
Generator,
Difficulty,
PlayerPermissionsLevel,
Package: PackageModule.default,
DedicatedServerStatus,
};
}
class ServerCommand extends IToolCommand_1.ToolCommandBase {
metadata = {
name: "server",
description: "Manage Bedrock Dedicated Server (start, stop, status)",
aliases: ["srv", "bds"],
category: "Server",
arguments: [
{
name: "action",
description: "Action to perform: start, stop, or status",
type: "string",
required: true,
autocompleteProvider: async (partial, _context) => {
const actions = ["start", "stop", "status"];
if (!partial)
return actions;
return actions.filter((a) => a.startsWith(partial.toLowerCase()));
},
},
],
flags: [
{
name: "slot",
shortName: "l",
description: "Server slot number (default: 0). Each slot runs on a separate port.",
type: "string",
required: false,
},
{
name: "project",
shortName: "p",
description: "Path to project to deploy (default: current project if available)",
type: "string",
required: false,
},
{
name: "session",
shortName: "s",
description: "Session name (required for MCP/API scope)",
type: "string",
required: false,
},
{
name: "fresh",
shortName: "f",
description: "Force a fresh world (discard existing world data)",
type: "boolean",
required: false,
},
{
name: "json",
shortName: "j",
description: "Output results in JSON format for scripts and CI/CD pipelines",
type: "boolean",
required: false,
},
{
name: "wait-ready",
shortName: "w",
description: "Wait for server to be fully ready before returning",
type: "boolean",
required: false,
},
{
name: "timeout",
shortName: "t",
description: "Timeout in seconds for --wait-ready (default: 60)",
type: "string",
required: false,
},
{
name: "editor",
shortName: "e",
description: "Launch BDS in Minecraft Editor mode",
type: "boolean",
required: false,
},
],
scopes: [IToolCommand_1.ToolCommandScope.ui, IToolCommand_1.ToolCommandScope.serveTerminal, IToolCommand_1.ToolCommandScope.mcp, IToolCommand_1.ToolCommandScope.serverApi],
examples: [
"/server start",
"/server start --project ./myAddon",
"/server start --slot 1 --fresh",
"/server start --wait-ready --timeout 120",
"/server start --editor",
"/server start --json",
"/server stop",
"/server status",
"/server status --json",
],
};
async execute(context, args, flags) {
const action = (args[0] || "").toLowerCase();
const slot = parseInt(flags.slot, 10) || 0;
if (!action || !["start", "stop", "status"].includes(action)) {
return this.error("INVALID_ACTION", 'Invalid action. Use "start", "stop", or "status".');
}
let result;
switch (action) {
case "start":
result = await this._startServer(context, slot, flags);
break;
case "stop":
result = await this._stopServer(context, slot, flags);
break;
case "status":
result = await this._getStatus(context, slot, flags);
break;
default:
result = this.error("INVALID_ACTION", `Unknown action: ${action}`);
break;
}
if (flags.json === true) {
const jsonOutput = result.success
? { ...result.data, message: result.message }
: { status: "error", code: result.error?.code, message: result.error?.message };
context.output.info(JSON.stringify(jsonOutput));
}
return result;
}
async _startServer(context, slot, flags) {
const serverManager = this._getServerManager(context);
if (!serverManager) {
return this.error("NO_SERVER_MANAGER", "No ServerManager available. This command requires a serve-mode or Electron context with BDS support.");
}
context.output.info("Preparing Bedrock Dedicated Server...");
try {
// Prepare ServerManager (downloads BDS if needed)
await serverManager.prepare();
// Lazy-load heavy dependencies to avoid circular import issues at module init time
const deps = await getServerDependencies();
// Build world settings with creator tools infrastructure addon
const packRefs = [];
deps.Package.ensureMinecraftCreatorToolsPackageReference(packRefs);
const worldSettings = {
gameType: deps.GameType.creative,
generator: deps.Generator.flat,
cheatsEnabled: true,
difficulty: deps.Difficulty.peaceful,
playerPermissionLevel: deps.PlayerPermissionsLevel.operator,
permissionLevel: deps.PlayerPermissionsLevel.operator,
randomSeed: "2000",
packageReferences: packRefs,
worldTemplateReferences: undefined,
isEditor: flags.editor === true,
};
const forceNewWorld = flags.fresh === true || flags.fresh === "true";
const startMessage = {
mode: deps.DedicatedServerMode.auto,
path: undefined,
forceStartNewWorld: forceNewWorld,
worldSettings,
transientWorld: true,
};
// If a project path or current project is available, add it as additional content
const projectPath = flags.project;
if (projectPath) {
startMessage.additionalContentPath = projectPath;
}
else if (context.project?.projectFolder) {
// Deploy current project's content if available
const folderPath = context.project.projectFolder.fullPath;
if (folderPath) {
startMessage.additionalContentPath = folderPath;
}
}
context.output.progress(1, 4, "Provisioning server slot...");
if (flags.editor === true) {
context.output.info("Editor mode enabled — BDS will launch with Editor=true");
}
const server = await serverManager.ensureActiveServer(slot, startMessage);
if (!server) {
return this.error("SERVER_FAILED", "Failed to create server instance");
}
context.output.progress(2, 4, "Starting Bedrock Dedicated Server...");
await server.startServer(false, startMessage);
context.output.progress(3, 4, "Waiting for server to be ready...");
await server.waitUntilStarted();
context.output.progress(4, 4, "Server started!");
const port = serverManager.getBasePortForSlot(slot);
// --wait-ready: additional verification that the server is fully ready
if (flags["wait-ready"] === true) {
const startTime = Date.now();
const timeoutSec = parseInt(flags.timeout, 10) || 60;
const deadline = startTime + timeoutSec * 1000;
while (server.status !== deps.DedicatedServerStatus.started && Date.now() < deadline) {
await new Promise((resolve) => setTimeout(resolve, 250));
}
if (server.status !== deps.DedicatedServerStatus.started) {
return {
...this.error("TIMEOUT", `Server did not reach ready state within ${timeoutSec}s`),
exitCode: IToolCommand_1.ToolCommandExitCode.Timeout,
};
}
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
context.output.info(`Server ready after ${elapsed}s`);
}
context.output.success(`Server started on slot ${slot} (port ${port})`);
return this.success(`Server started on slot ${slot} (port ${port})`, {
slot,
port,
status: "started",
});
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
return {
...this.error("START_FAILED", `Failed to start server: ${message}`),
exitCode: this._categorizeError(message),
};
}
}
async _stopServer(context, slot, _flags) {
const serverManager = this._getServerManager(context);
if (!serverManager) {
return this.error("NO_SERVER_MANAGER", "No ServerManager available.");
}
try {
const server = serverManager.getActiveServer(slot);
if (!server) {
return this.error("NO_SERVER", `No active server on slot ${slot}`);
}
context.output.info(`Stopping server on slot ${slot}...`);
await server.stopServer();
context.output.success(`Server stopped on slot ${slot}`);
return this.success(`Server stopped on slot ${slot}`, {
slot,
status: "stopped",
});
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
return {
...this.error("STOP_FAILED", `Failed to stop server: ${message}`),
exitCode: this._categorizeError(message),
};
}
}
async _getStatus(context, slot, _flags) {
const serverManager = this._getServerManager(context);
if (!serverManager) {
return this.error("NO_SERVER_MANAGER", "No ServerManager available.");
}
const server = serverManager.getActiveServer(slot);
if (!server) {
context.output.info(`No active server on slot ${slot}`);
return this.success(`No active server on slot ${slot}`, {
slot,
status: "none",
running: false,
});
}
const status = server.status;
const port = serverManager.getBasePortForSlot(slot);
const deps = await getServerDependencies();
context.output.info(`Server on slot ${slot}: status=${status}, port=${port}`);
return this.success(`Server status: ${status}`, {
slot,
port,
status: String(status),
running: status === deps.DedicatedServerStatus.started,
});
}
/**
* Categorize an error message into a standard exit code.
*/
_categorizeError(message) {
const lower = message.toLowerCase();
if ((lower.includes("port") && lower.includes("in use")) || lower.includes("eaddrinuse")) {
return IToolCommand_1.ToolCommandExitCode.PortConflict;
}
if (lower.includes("eula")) {
return IToolCommand_1.ToolCommandExitCode.EulaNotAccepted;
}
if (lower.includes("download") || lower.includes("network") || lower.includes("econnrefused")) {
return IToolCommand_1.ToolCommandExitCode.NetworkError;
}
if (lower.includes("crash") || lower.includes("unexpected exit")) {
return IToolCommand_1.ToolCommandExitCode.CrashOnStartup;
}
if (lower.includes("timeout") || lower.includes("timed out")) {
return IToolCommand_1.ToolCommandExitCode.Timeout;
}
return IToolCommand_1.ToolCommandExitCode.GenericError;
}
/**
* Get the ServerManager from whichever context is available.
* Priority: session.serverManager > minecraft (if it wraps a server) > undefined
*/
_getServerManager(context) {
// Session-based (MCP/API mode)
if (context.session?.serverManager) {
return context.session.serverManager;
}
// For UI/serve modes, we need to get or create a ServerManager.
// The ServerManager is typically created by the hosting environment
// (Electron DedicatedServerCommandHandler, MinecraftMcpServer, etc.)
// We can create one on-demand for the creatorTools instance.
return undefined;
}
}
exports.ServerCommand = ServerCommand;
exports.serverCommand = new ServerCommand();