@kzheart_/mc-pilot 0.1.7 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/commands/chat.js +1 -1
  2. package/dist/commands/client.js +123 -34
  3. package/dist/commands/combat.js +3 -3
  4. package/dist/commands/gui.js +2 -2
  5. package/dist/commands/input.js +1 -1
  6. package/dist/commands/move.js +2 -2
  7. package/dist/commands/project.d.ts +6 -0
  8. package/dist/commands/project.js +155 -0
  9. package/dist/commands/request-helpers.js +4 -4
  10. package/dist/commands/server.js +82 -28
  11. package/dist/commands/wait.js +2 -2
  12. package/dist/download/VersionMatrix.d.ts +1 -1
  13. package/dist/download/client/ClientDownloader.d.ts +20 -6
  14. package/dist/download/client/ClientDownloader.js +14 -35
  15. package/dist/download/server/ServerDownloader.d.ts +3 -5
  16. package/dist/download/server/ServerDownloader.js +2 -17
  17. package/dist/index.js +20 -15
  18. package/dist/instance/ClientInstanceManager.d.ts +49 -0
  19. package/dist/instance/ClientInstanceManager.js +237 -0
  20. package/dist/instance/ServerInstanceManager.d.ts +70 -0
  21. package/dist/instance/ServerInstanceManager.js +223 -0
  22. package/dist/util/command.js +2 -2
  23. package/dist/util/context.d.ts +10 -8
  24. package/dist/util/context.js +21 -9
  25. package/dist/util/global-state.d.ts +9 -0
  26. package/dist/util/global-state.js +21 -0
  27. package/dist/util/instance-types.d.ts +46 -0
  28. package/dist/util/instance-types.js +1 -0
  29. package/dist/util/paths.d.ts +7 -0
  30. package/dist/util/paths.js +23 -0
  31. package/dist/util/project.d.ts +23 -0
  32. package/dist/util/project.js +28 -0
  33. package/package.json +1 -1
  34. package/dist/client/ClientManager.d.ts +0 -82
  35. package/dist/client/ClientManager.js +0 -221
  36. package/dist/commands/channel.d.ts +0 -2
  37. package/dist/commands/channel.js +0 -24
  38. package/dist/commands/effects.d.ts +0 -2
  39. package/dist/commands/effects.js +0 -16
  40. package/dist/server/ServerManager.d.ts +0 -63
  41. package/dist/server/ServerManager.js +0 -114
  42. package/dist/util/config.d.ts +0 -31
  43. package/dist/util/config.js +0 -59
@@ -22,7 +22,7 @@ export function createChatCommand() {
22
22
  .description("Wait for a chat message matching a pattern")
23
23
  .requiredOption("--match <pattern>", "Substring match by default; prefix with / for regex (e.g. /player\\d+/)")
24
24
  .option("--timeout <seconds>", "Timeout in seconds", Number)
25
- .action(createRequestAction("chat.wait", ({ options }) => ({ match: options.match, timeout: options.timeout }), ({ options }, context) => withTransportTimeoutBuffer(options.timeout ? Number(options.timeout) : undefined, context.config.timeout.default)));
25
+ .action(createRequestAction("chat.wait", ({ options }) => ({ match: options.match, timeout: options.timeout }), ({ options }, context) => withTransportTimeoutBuffer(options.timeout ? Number(options.timeout) : undefined, context.timeout("default"))));
26
26
  command
27
27
  .command("last")
28
28
  .description("Get the last chat message")
@@ -1,11 +1,19 @@
1
1
  import { Command } from "commander";
2
- import { ClientManager } from "../client/ClientManager.js";
3
- import { downloadClientMod } from "../download/client/ClientDownloader.js";
4
2
  import { buildClientSearchResults } from "../download/SearchCommand.js";
3
+ import { ClientInstanceManager } from "../instance/ClientInstanceManager.js";
4
+ import { MctError } from "../util/errors.js";
5
5
  import { createRequestAction } from "./request-helpers.js";
6
6
  import { wrapCommand } from "../util/command.js";
7
+ import { CacheManager } from "../download/CacheManager.js";
8
+ import { findVariantByVersionAndLoader, getModArtifactFileName, loadModVariantCatalog } from "../download/ModVariantCatalog.js";
9
+ import { detectJava } from "../download/JavaDetector.js";
10
+ import { prepareManagedFabricRuntime } from "../download/client/FabricRuntimeDownloader.js";
11
+ import { copyFileIfMissing, downloadFile } from "../download/DownloadUtils.js";
12
+ import { resolveClientInstanceDir } from "../util/paths.js";
13
+ import { access, mkdir } from "node:fs/promises";
14
+ import path from "node:path";
7
15
  export function createClientCommand() {
8
- const command = new Command("client").description("Manage Minecraft client");
16
+ const command = new Command("client").description("Manage Minecraft client instances");
9
17
  command
10
18
  .command("search")
11
19
  .description("Search available client version and loader combinations")
@@ -20,66 +28,147 @@ export function createClientCommand() {
20
28
  };
21
29
  }));
22
30
  command
23
- .command("download")
24
- .description("Download client mod and update config")
25
- .option("--loader <loader>", "Client loader: fabric|forge|neoforge (default: fabric)")
26
- .option("--version <version>", "Minecraft version (default: 1.20.4)")
27
- .option("--dir <path>", "Mod download directory")
28
- .option("--name <name>", "Client config name (default: \"default\"); use this name with \"client launch\"")
29
- .option("--ws-port <port>", "WebSocket port for CLI-to-mod communication (default: 25560)", Number)
30
- .option("--server <address>", "Default server address (e.g. localhost:25565)")
31
- .option("--instance-dir <path>", "Client instance directory")
32
- .option("--meta-dir <path>", "Runtime metadata directory")
33
- .option("--libraries-dir <path>", "Runtime libraries directory")
34
- .option("--assets-dir <path>", "Runtime assets directory")
35
- .option("--natives-dir <path>", "Runtime natives directory")
36
- .action(wrapCommand(async (context, { options }) => {
37
- return downloadClientMod(context, options);
31
+ .command("create")
32
+ .description("Create a new client instance")
33
+ .argument("<name>", "Client instance name (e.g. fabric-1.20.4)")
34
+ .option("--version <version>", "Minecraft version (default: 1.21.4)")
35
+ .option("--loader <loader>", "Client loader: fabric (default: fabric)")
36
+ .option("--ws-port <port>", "WebSocket port (auto-assigned if omitted)", Number)
37
+ .option("--account <account>", "Offline username or account identifier")
38
+ .option("--headless", "Launch in headless mode")
39
+ .option("--java <command>", "Java command to use")
40
+ .action(wrapCommand(async (_context, { args, options }) => {
41
+ const clientName = args[0];
42
+ const loader = options.loader ?? "fabric";
43
+ const version = options.version ?? "1.21.4";
44
+ const cacheManager = new CacheManager();
45
+ const catalog = await loadModVariantCatalog();
46
+ const variant = findVariantByVersionAndLoader(catalog, version, loader);
47
+ if (!variant) {
48
+ throw new MctError({ code: "VARIANT_NOT_FOUND", message: `No mod variant found for ${version} / ${loader}` }, 4);
49
+ }
50
+ if (variant.loader !== "fabric") {
51
+ throw new MctError({ code: "UNSUPPORTED_LOADER", message: `Loader ${variant.loader} is not implemented yet` }, 4);
52
+ }
53
+ // Check Java
54
+ const java = await detectJava(options.java ?? "java");
55
+ const requiredJava = variant.javaVersion ?? 17;
56
+ if (!java.available || (java.majorVersion ?? 0) < requiredJava) {
57
+ throw new MctError({ code: "JAVA_NOT_FOUND", message: `Java ${requiredJava}+ is required for ${variant.id}` }, 4);
58
+ }
59
+ // Resolve mod artifact
60
+ const artifactFileName = getModArtifactFileName(variant);
61
+ const cacheArtifactPath = cacheManager.getModFile(artifactFileName);
62
+ const gradleModule = variant.gradleModule ?? `version-${variant.minecraftVersion}`;
63
+ const localBuildPath = path.join(process.cwd(), "client-mod", gradleModule, "build", "libs", artifactFileName);
64
+ let sourcePath;
65
+ try {
66
+ await access(localBuildPath);
67
+ sourcePath = localBuildPath;
68
+ await copyFileIfMissing(localBuildPath, cacheArtifactPath);
69
+ }
70
+ catch {
71
+ try {
72
+ await access(cacheArtifactPath);
73
+ sourcePath = cacheArtifactPath;
74
+ }
75
+ catch {
76
+ const modVersion = variant.modVersion ?? "0.1.0";
77
+ const baseUrl = process.env.MCT_MOD_DOWNLOAD_BASE_URL || "https://github.com/kzheart/mc-pilot/releases/download";
78
+ const downloadUrl = `${baseUrl}/v${modVersion}/${artifactFileName}`;
79
+ await downloadFile(downloadUrl, cacheArtifactPath, fetch);
80
+ sourcePath = cacheArtifactPath;
81
+ }
82
+ }
83
+ // Set up client instance directory
84
+ const instanceDir = resolveClientInstanceDir(clientName);
85
+ const minecraftDir = path.join(instanceDir, "minecraft");
86
+ const modsDir = path.join(minecraftDir, "mods");
87
+ await mkdir(modsDir, { recursive: true });
88
+ await copyFileIfMissing(sourcePath, path.join(modsDir, artifactFileName));
89
+ // Prepare Fabric runtime
90
+ const runtimeRootDir = path.join(cacheManager.getRootDir(), "client", "runtime", variant.minecraftVersion);
91
+ const managedRuntime = await prepareManagedFabricRuntime(variant, {
92
+ runtimeRootDir,
93
+ gameDir: minecraftDir
94
+ }, { fetchImpl: fetch });
95
+ const launchArgs = [
96
+ "--runtime-root", managedRuntime.runtimeRootDir,
97
+ "--version-id", managedRuntime.versionId,
98
+ "--game-dir", managedRuntime.gameDir
99
+ ];
100
+ const manager = new ClientInstanceManager(_context.globalState);
101
+ const meta = await manager.create({
102
+ name: clientName,
103
+ loader,
104
+ version: variant.minecraftVersion,
105
+ wsPort: options.wsPort,
106
+ account: options.account,
107
+ headless: options.headless,
108
+ launchArgs,
109
+ env: {
110
+ MCT_CLIENT_MOD_VARIANT: variant.id,
111
+ MCT_CLIENT_MOD_JAR: path.join(modsDir, artifactFileName)
112
+ }
113
+ });
114
+ return {
115
+ created: true,
116
+ ...meta,
117
+ javaCommand: java.command,
118
+ javaVersion: java.majorVersion,
119
+ modsDir,
120
+ runtimeRootDir: managedRuntime.runtimeRootDir,
121
+ runtimeVersionId: managedRuntime.versionId
122
+ };
38
123
  }));
39
124
  command
40
125
  .command("launch")
41
126
  .description("Launch a client instance")
42
- .argument("<name>", "Client name (matches a key in config \"clients\", e.g. \"default\")")
43
- .option("--version <version>", "Minecraft version")
127
+ .argument("[name]", "Client instance name (default: from active profile)")
44
128
  .option("--server <address>", "Target server address (e.g. localhost:25565)")
45
129
  .option("--account <account>", "Offline username or account identifier")
46
- .option("--ws-port <port>", "WebSocket port (default: 25560)", Number)
47
- .option("--headless", "Launch in headless mode (window hidden)")
130
+ .option("--ws-port <port>", "WebSocket port override", Number)
131
+ .option("--headless", "Launch in headless mode")
48
132
  .action(wrapCommand(async (context, { args, options }) => {
49
- const manager = new ClientManager(context);
50
- return manager.launch({
51
- name: args[0],
52
- ...options
53
- });
133
+ const clientName = args[0] ?? context.activeProfile?.clients[0];
134
+ if (!clientName) {
135
+ throw new MctError({ code: "INVALID_PARAMS", message: "Client name is required. Specify it as argument or set a profile." }, 4);
136
+ }
137
+ const manager = new ClientInstanceManager(context.globalState);
138
+ return manager.launch(clientName, options);
54
139
  }));
55
140
  command
56
141
  .command("stop")
57
142
  .description("Stop a client instance")
58
- .argument("<name>", "Client name")
143
+ .argument("<name>", "Client instance name")
59
144
  .action(wrapCommand(async (context, { args }) => {
60
- const manager = new ClientManager(context);
145
+ const manager = new ClientInstanceManager(context.globalState);
61
146
  return manager.stop(args[0]);
62
147
  }));
63
148
  command
64
149
  .command("list")
65
150
  .description("List all client instances and their status")
66
151
  .action(wrapCommand(async (context) => {
67
- const manager = new ClientManager(context);
152
+ const manager = new ClientInstanceManager(context.globalState);
68
153
  return manager.list();
69
154
  }));
70
155
  command
71
156
  .command("wait-ready")
72
157
  .description("Wait until client WebSocket is connected")
73
- .argument("<name>", "Client name")
158
+ .argument("[name]", "Client instance name (default: from active profile)")
74
159
  .option("--timeout <seconds>", "Timeout in seconds", Number)
75
160
  .action(wrapCommand(async (context, { args, options }) => {
76
- const manager = new ClientManager(context);
77
- return manager.waitReady(args[0], options.timeout ?? context.config.timeout.clientReady);
161
+ const clientName = args[0] ?? context.activeProfile?.clients[0];
162
+ if (!clientName) {
163
+ throw new MctError({ code: "INVALID_PARAMS", message: "Client name is required" }, 4);
164
+ }
165
+ const manager = new ClientInstanceManager(context.globalState);
166
+ return manager.waitReady(clientName, options.timeout ?? context.timeout("clientReady"));
78
167
  }));
79
168
  command
80
169
  .command("reconnect")
81
170
  .description("Reconnect the client to the server")
82
- .option("--address <address>", "Target server address (default: from launch config)")
171
+ .option("--address <address>", "Target server address")
83
172
  .action(createRequestAction("client.reconnect", ({ options }) => ({
84
173
  address: options.address
85
174
  })));
@@ -20,7 +20,7 @@ export function createCombatCommand() {
20
20
  .action(createRequestAction(`combat.${actionName}`, ({ options }) => ({
21
21
  filter: buildEntityFilter(options),
22
22
  timeout: options.timeout
23
- }), ({ options }, context) => withTransportTimeoutBuffer(options.timeout ? Number(options.timeout) : 30, context.config.timeout.default)));
23
+ }), ({ options }, context) => withTransportTimeoutBuffer(options.timeout ? Number(options.timeout) : 30, context.timeout("default"))));
24
24
  }
25
25
  command
26
26
  .command("clear")
@@ -32,7 +32,7 @@ export function createCombatCommand() {
32
32
  type: options.type,
33
33
  radius: options.radius ?? 16,
34
34
  timeout: options.timeout
35
- }), ({ options }, context) => withTransportTimeoutBuffer(options.timeout ? Number(options.timeout) : 60, context.config.timeout.default)));
35
+ }), ({ options }, context) => withTransportTimeoutBuffer(options.timeout ? Number(options.timeout) : 60, context.timeout("default"))));
36
36
  command
37
37
  .command("pickup")
38
38
  .description("Pick up nearby dropped items")
@@ -41,6 +41,6 @@ export function createCombatCommand() {
41
41
  .action(createRequestAction("combat.pickup", ({ options }) => ({
42
42
  radius: options.radius ?? 5,
43
43
  timeout: options.timeout
44
- }), ({ options }, context) => withTransportTimeoutBuffer(options.timeout ? Number(options.timeout) : 10, context.config.timeout.default)));
44
+ }), ({ options }, context) => withTransportTimeoutBuffer(options.timeout ? Number(options.timeout) : 10, context.timeout("default"))));
45
45
  return command;
46
46
  }
@@ -34,12 +34,12 @@ export function createGuiCommand() {
34
34
  .command("wait-open")
35
35
  .description("Wait for a GUI to open")
36
36
  .option("--timeout <seconds>", "Timeout in seconds", Number)
37
- .action(createRequestAction("gui.wait-open", ({ options }) => ({ timeout: options.timeout }), ({ options }, context) => withTransportTimeoutBuffer(options.timeout ? Number(options.timeout) : undefined, context.config.timeout.default)));
37
+ .action(createRequestAction("gui.wait-open", ({ options }) => ({ timeout: options.timeout }), ({ options }, context) => withTransportTimeoutBuffer(options.timeout ? Number(options.timeout) : undefined, context.timeout("default"))));
38
38
  command
39
39
  .command("wait-update")
40
40
  .description("Wait for the GUI to update")
41
41
  .option("--timeout <seconds>", "Timeout in seconds", Number)
42
- .action(createRequestAction("gui.wait-update", ({ options }) => ({ timeout: options.timeout }), ({ options }, context) => withTransportTimeoutBuffer(options.timeout ? Number(options.timeout) : undefined, context.config.timeout.default)));
42
+ .action(createRequestAction("gui.wait-update", ({ options }) => ({ timeout: options.timeout }), ({ options }, context) => withTransportTimeoutBuffer(options.timeout ? Number(options.timeout) : undefined, context.timeout("default"))));
43
43
  command
44
44
  .command("screenshot")
45
45
  .description("Take a screenshot of the current GUI")
@@ -86,7 +86,7 @@ export function createInputCommand() {
86
86
  .action(createRequestAction("input.key-hold", ({ args, options }) => ({
87
87
  key: String(args[0]),
88
88
  duration: Number(options.duration)
89
- }), ({ options }, context) => withTransportTimeoutBuffer(Math.max(Number(options.duration ?? 0) / 1000 + 2, 3), context.config.timeout.default)));
89
+ }), ({ options }, context) => withTransportTimeoutBuffer(Math.max(Number(options.duration ?? 0) / 1000 + 2, 3), context.timeout("default"))));
90
90
  keyCommand
91
91
  .command("down")
92
92
  .description("Press a key down (without releasing)")
@@ -12,7 +12,7 @@ export function createMoveCommand() {
12
12
  x: Number(args[0]),
13
13
  y: Number(args[1]),
14
14
  z: Number(args[2])
15
- }), (_payload, context) => withTransportTimeoutBuffer(30, context.config.timeout.default)));
15
+ }), (_payload, context) => withTransportTimeoutBuffer(30, context.timeout("default"))));
16
16
  const directionLabels = { forward: "Move forward", back: "Move backward", left: "Move left", right: "Move right" };
17
17
  for (const direction of ["forward", "back", "left", "right"]) {
18
18
  command
@@ -25,7 +25,7 @@ export function createMoveCommand() {
25
25
  }), ({ args }, context) => {
26
26
  const blocks = Math.abs(Number(args[0]));
27
27
  const timeout = Math.max(1.5, blocks * 2.0);
28
- return withTransportTimeoutBuffer(timeout, context.config.timeout.default);
28
+ return withTransportTimeoutBuffer(timeout, context.timeout("default"));
29
29
  }));
30
30
  }
31
31
  command
@@ -0,0 +1,6 @@
1
+ import { Command } from "commander";
2
+ export declare function createInitCommand(): Command;
3
+ export declare function createDeployCommand(): Command;
4
+ export declare function createUpCommand(): Command;
5
+ export declare function createDownCommand(): Command;
6
+ export declare function createUseCommand(): Command;
@@ -0,0 +1,155 @@
1
+ import { Command } from "commander";
2
+ import path from "node:path";
3
+ import { ServerInstanceManager } from "../instance/ServerInstanceManager.js";
4
+ import { ClientInstanceManager } from "../instance/ClientInstanceManager.js";
5
+ import { MctError } from "../util/errors.js";
6
+ import { wrapCommand } from "../util/command.js";
7
+ import { loadProjectFile, resolveProfile, writeProjectFile } from "../util/project.js";
8
+ export function createInitCommand() {
9
+ return new Command("init")
10
+ .description("Initialize a new MC Pilot project in the current directory")
11
+ .option("--name <name>", "Project name (default: directory name)")
12
+ .action(wrapCommand(async (context, { options }) => {
13
+ const existing = await loadProjectFile(context.cwd);
14
+ if (existing) {
15
+ throw new MctError({ code: "PROJECT_EXISTS", message: "mct.project.json already exists in this directory" }, 4);
16
+ }
17
+ const projectName = options.name ?? path.basename(context.cwd);
18
+ const project = {
19
+ project: projectName,
20
+ profiles: {},
21
+ screenshot: {
22
+ outputDir: "./screenshots"
23
+ },
24
+ timeout: {
25
+ serverReady: 120,
26
+ clientReady: 60,
27
+ default: 10
28
+ }
29
+ };
30
+ await writeProjectFile(context.cwd, project);
31
+ return {
32
+ created: true,
33
+ project: projectName,
34
+ file: "mct.project.json"
35
+ };
36
+ }));
37
+ }
38
+ export function createDeployCommand() {
39
+ return new Command("deploy")
40
+ .description("Deploy plugin JARs to the server instance")
41
+ .option("--profile <name>", "Profile name")
42
+ .action(wrapCommand(async (context, { options }) => {
43
+ const { projectFile, projectName } = context;
44
+ if (!projectFile || !projectName) {
45
+ throw new MctError({ code: "NO_PROJECT", message: "No project context. Run 'mct init' first." }, 4);
46
+ }
47
+ const profile = resolveProfile(projectFile, options.profile ?? projectFile.defaultProfile);
48
+ if (!profile) {
49
+ throw new MctError({ code: "NO_PROFILE", message: "No profile specified and no defaultProfile set" }, 4);
50
+ }
51
+ if (!profile.deployPlugins || profile.deployPlugins.length === 0) {
52
+ return { deployed: [], message: "No deployPlugins configured in profile" };
53
+ }
54
+ const manager = new ServerInstanceManager(context.globalState, projectName);
55
+ const deployed = await manager.deploy(profile.server, profile.deployPlugins, context.cwd);
56
+ return { deployed, server: profile.server };
57
+ }));
58
+ }
59
+ export function createUpCommand() {
60
+ return new Command("up")
61
+ .description("Deploy plugins, start server and clients, wait for ready")
62
+ .option("--profile <name>", "Profile name")
63
+ .option("--eula", "Auto-accept EULA")
64
+ .action(wrapCommand(async (context, { options }) => {
65
+ const { projectFile, projectName } = context;
66
+ if (!projectFile || !projectName) {
67
+ throw new MctError({ code: "NO_PROJECT", message: "No project context. Run 'mct init' first." }, 4);
68
+ }
69
+ const profile = resolveProfile(projectFile, options.profile ?? projectFile.defaultProfile);
70
+ if (!profile) {
71
+ throw new MctError({ code: "NO_PROFILE", message: "No profile specified and no defaultProfile set" }, 4);
72
+ }
73
+ const serverManager = new ServerInstanceManager(context.globalState, projectName);
74
+ const clientManager = new ClientInstanceManager(context.globalState);
75
+ const results = {};
76
+ // 1. Deploy plugins
77
+ if (profile.deployPlugins && profile.deployPlugins.length > 0) {
78
+ results.deployed = await serverManager.deploy(profile.server, profile.deployPlugins, context.cwd);
79
+ }
80
+ // 2. Start server
81
+ results.server = await serverManager.start(profile.server, { eula: options.eula });
82
+ // 3. Wait for server
83
+ const serverMeta = await serverManager.loadMeta(profile.server);
84
+ await serverManager.waitReady(profile.server, context.timeout("serverReady"));
85
+ // 4. Launch clients
86
+ const clientResults = [];
87
+ for (const clientName of profile.clients) {
88
+ const result = await clientManager.launch(clientName, {
89
+ server: `localhost:${serverMeta.port}`
90
+ });
91
+ clientResults.push(result);
92
+ }
93
+ results.clients = clientResults;
94
+ // 5. Wait for clients
95
+ for (const clientName of profile.clients) {
96
+ await clientManager.waitReady(clientName, context.timeout("clientReady"));
97
+ }
98
+ results.ready = true;
99
+ return results;
100
+ }));
101
+ }
102
+ export function createDownCommand() {
103
+ return new Command("down")
104
+ .description("Stop server and clients for the active profile")
105
+ .option("--profile <name>", "Profile name")
106
+ .action(wrapCommand(async (context, { options }) => {
107
+ const { projectFile, projectName } = context;
108
+ if (!projectFile || !projectName) {
109
+ throw new MctError({ code: "NO_PROJECT", message: "No project context. Run 'mct init' first." }, 4);
110
+ }
111
+ const profile = resolveProfile(projectFile, options.profile ?? projectFile.defaultProfile);
112
+ if (!profile) {
113
+ throw new MctError({ code: "NO_PROFILE", message: "No profile specified and no defaultProfile set" }, 4);
114
+ }
115
+ const serverManager = new ServerInstanceManager(context.globalState, projectName);
116
+ const clientManager = new ClientInstanceManager(context.globalState);
117
+ const results = {};
118
+ // Stop clients first
119
+ const clientResults = [];
120
+ for (const clientName of profile.clients) {
121
+ const result = await clientManager.stop(clientName);
122
+ clientResults.push(result);
123
+ }
124
+ results.clients = clientResults;
125
+ // Stop server
126
+ results.server = await serverManager.stop(profile.server);
127
+ return results;
128
+ }));
129
+ }
130
+ export function createUseCommand() {
131
+ return new Command("use")
132
+ .description("Set the default profile")
133
+ .argument("<profile>", "Profile name to set as default")
134
+ .action(wrapCommand(async (context, { args }) => {
135
+ const { projectFile } = context;
136
+ if (!projectFile) {
137
+ throw new MctError({ code: "NO_PROJECT", message: "No project context. Run 'mct init' first." }, 4);
138
+ }
139
+ const profileName = args[0];
140
+ if (!projectFile.profiles[profileName]) {
141
+ const available = Object.keys(projectFile.profiles);
142
+ throw new MctError({
143
+ code: "PROFILE_NOT_FOUND",
144
+ message: `Profile '${profileName}' not found`,
145
+ details: { available }
146
+ }, 4);
147
+ }
148
+ projectFile.defaultProfile = profileName;
149
+ await writeProjectFile(context.cwd, projectFile);
150
+ return {
151
+ defaultProfile: profileName,
152
+ profile: projectFile.profiles[profileName]
153
+ };
154
+ }));
155
+ }
@@ -1,13 +1,13 @@
1
1
  import { Command } from "commander";
2
- import { ClientManager } from "../client/ClientManager.js";
2
+ import { ClientInstanceManager } from "../instance/ClientInstanceManager.js";
3
3
  import { WebSocketClient } from "../client/WebSocketClient.js";
4
4
  import { MctError } from "../util/errors.js";
5
5
  import { wrapCommand } from "../util/command.js";
6
6
  export async function sendClientRequest(context, clientName, action, params, timeoutSeconds) {
7
- const manager = new ClientManager(context);
7
+ const manager = new ClientInstanceManager(context.globalState);
8
8
  const client = await manager.getClient(clientName);
9
- const ws = new WebSocketClient(manager.getWsUrl(client.wsPort));
10
- return ws.send(action, params, timeoutSeconds ?? context.config.timeout.default);
9
+ const ws = new WebSocketClient(`ws://127.0.0.1:${client.wsPort}`);
10
+ return ws.send(action, params, timeoutSeconds ?? context.timeout("default"));
11
11
  }
12
12
  export function createRequestAction(action, buildParams, timeoutSelector) {
13
13
  return wrapCommand(async (context, payload) => {
@@ -1,10 +1,24 @@
1
1
  import { Command } from "commander";
2
2
  import { buildServerSearchResults } from "../download/SearchCommand.js";
3
- import { downloadServerJar } from "../download/server/ServerDownloader.js";
3
+ import { downloadServerJarToCache } from "../download/server/ServerDownloader.js";
4
+ import { ServerInstanceManager } from "../instance/ServerInstanceManager.js";
5
+ import { MctError } from "../util/errors.js";
4
6
  import { wrapCommand } from "../util/command.js";
5
- import { ServerManager } from "../server/ServerManager.js";
7
+ function requireProject(context) {
8
+ if (!context.projectName) {
9
+ throw new MctError({ code: "NO_PROJECT", message: "No project context. Run 'mct init' first or use --project <name>" }, 4);
10
+ }
11
+ return context.projectName;
12
+ }
13
+ function resolveServerName(context, explicit) {
14
+ if (explicit)
15
+ return explicit;
16
+ if (context.activeProfile?.server)
17
+ return context.activeProfile.server;
18
+ throw new MctError({ code: "INVALID_PARAMS", message: "Server name is required. Specify it as argument or set a profile." }, 4);
19
+ }
6
20
  export function createServerCommand() {
7
- const command = new Command("server").description("Manage Minecraft server");
21
+ const command = new Command("server").description("Manage Minecraft server instances");
8
22
  command
9
23
  .command("search")
10
24
  .description("Search available server versions")
@@ -19,48 +33,88 @@ export function createServerCommand() {
19
33
  };
20
34
  }));
21
35
  command
22
- .command("download")
23
- .description("Download server jar and update config")
24
- .option("--type <type>", "Server type: vanilla|paper|purpur|spigot")
25
- .option("--version <version>", "Minecraft version")
36
+ .command("create")
37
+ .description("Create a new server instance")
38
+ .argument("<name>", "Server instance name (e.g. paper-1.20.4)")
39
+ .option("--type <type>", "Server type: vanilla|paper|purpur|spigot (default: paper)")
40
+ .option("--version <version>", "Minecraft version (default: 1.21.4)")
26
41
  .option("--build <build>", "Specific build number")
27
- .option("--dir <path>", "Download target directory")
28
- .option("--fixtures <path>", "Fixture plugin jar, auto-copied to plugins/")
29
- .action(wrapCommand(async (context, { options }) => {
30
- return downloadServerJar(context, options);
42
+ .option("--port <number>", "Server port (auto-assigned if omitted)", Number)
43
+ .option("--jvm-args <args>", "JVM arguments (comma-separated)")
44
+ .option("--eula", "Auto-accept EULA")
45
+ .action(wrapCommand(async (context, { args, options }) => {
46
+ const project = requireProject(context);
47
+ const serverType = (options.type ?? "paper");
48
+ const version = options.version ?? "1.21.4";
49
+ const downloadResult = await downloadServerJarToCache({ type: serverType, version, build: options.build });
50
+ const manager = new ServerInstanceManager(context.globalState, project);
51
+ return manager.create({
52
+ name: args[0],
53
+ project,
54
+ type: serverType,
55
+ version,
56
+ port: options.port,
57
+ jvmArgs: options.jvmArgs?.split(",").map(a => a.trim()) ?? [],
58
+ eula: options.eula,
59
+ cachedJarPath: downloadResult.cachePath
60
+ });
31
61
  }));
32
62
  command
33
63
  .command("start")
34
- .description("Start the server")
35
- .option("--jar <path>", "Server jar path (default: from config server.jar)")
36
- .option("--dir <path>", "Server directory (default: from config server.dir)")
37
- .option("--port <number>", "Server port (default: 25565)", Number)
64
+ .description("Start a server instance")
65
+ .argument("[name]", "Server instance name (default: from active profile)")
38
66
  .option("--eula", "Auto-accept EULA")
39
- .action(wrapCommand(async (context, { options }) => {
40
- const manager = new ServerManager(context);
41
- return manager.start(options);
67
+ .option("--jvm-args <args>", "Override JVM arguments (comma-separated)")
68
+ .action(wrapCommand(async (context, { args, options }) => {
69
+ const project = requireProject(context);
70
+ const serverName = resolveServerName(context, args[0]);
71
+ const manager = new ServerInstanceManager(context.globalState, project);
72
+ return manager.start(serverName, {
73
+ eula: options.eula,
74
+ jvmArgs: options.jvmArgs?.split(",").map(a => a.trim())
75
+ });
42
76
  }));
43
77
  command
44
78
  .command("stop")
45
- .description("Stop the server")
46
- .action(wrapCommand(async (context) => {
47
- const manager = new ServerManager(context);
48
- return manager.stop();
79
+ .description("Stop a server instance")
80
+ .argument("[name]", "Server instance name (default: from active profile)")
81
+ .action(wrapCommand(async (context, { args }) => {
82
+ const project = requireProject(context);
83
+ const serverName = resolveServerName(context, args[0]);
84
+ const manager = new ServerInstanceManager(context.globalState, project);
85
+ return manager.stop(serverName);
49
86
  }));
50
87
  command
51
88
  .command("status")
52
89
  .description("Show server status")
53
- .action(wrapCommand(async (context) => {
54
- const manager = new ServerManager(context);
55
- return manager.status();
90
+ .argument("[name]", "Server instance name (omit to show all in project)")
91
+ .action(wrapCommand(async (context, { args }) => {
92
+ const project = requireProject(context);
93
+ const manager = new ServerInstanceManager(context.globalState, project);
94
+ return manager.status(args[0]);
95
+ }));
96
+ command
97
+ .command("list")
98
+ .description("List server instances")
99
+ .option("--all", "List instances across all projects")
100
+ .action(wrapCommand(async (context, { options }) => {
101
+ if (options.all) {
102
+ return { instances: await ServerInstanceManager.listAll(context.globalState) };
103
+ }
104
+ const project = requireProject(context);
105
+ const manager = new ServerInstanceManager(context.globalState, project);
106
+ return { instances: await manager.list() };
56
107
  }));
57
108
  command
58
109
  .command("wait-ready")
59
110
  .description("Wait until server port is connectable")
111
+ .argument("[name]", "Server instance name (default: from active profile)")
60
112
  .option("--timeout <seconds>", "Timeout in seconds", Number)
61
- .action(wrapCommand(async (context, { options }) => {
62
- const manager = new ServerManager(context);
63
- return manager.waitReady(options.timeout ?? context.config.timeout.serverReady);
113
+ .action(wrapCommand(async (context, { args, options }) => {
114
+ const project = requireProject(context);
115
+ const serverName = resolveServerName(context, args[0]);
116
+ const manager = new ServerInstanceManager(context.globalState, project);
117
+ return manager.waitReady(serverName, options.timeout ?? context.timeout("serverReady"));
64
118
  }));
65
119
  return command;
66
120
  }
@@ -17,7 +17,7 @@ export function createWaitCommand() {
17
17
  untilOnGround: Boolean(options.untilOnGround),
18
18
  timeout: options.timeout
19
19
  }), ({ options, args }, context) => {
20
- const requested = options.timeout ? Number(options.timeout) : args[0] ? Number(args[0]) : context.config.timeout.default;
21
- return withTransportTimeoutBuffer(requested, context.config.timeout.default);
20
+ const requested = options.timeout ? Number(options.timeout) : args[0] ? Number(args[0]) : context.timeout("default");
21
+ return withTransportTimeoutBuffer(requested, context.timeout("default"));
22
22
  }));
23
23
  }
@@ -46,8 +46,8 @@ export declare function getVersionMatrix(): {
46
46
  servers: {
47
47
  paper: ServerSupportInfo;
48
48
  purpur: ServerSupportInfo;
49
- spigot: ServerSupportInfo;
50
49
  vanilla: ServerSupportInfo;
50
+ spigot: ServerSupportInfo;
51
51
  };
52
52
  clients: {
53
53
  fabric: ClientLoaderSupportInfo;