@kzheart_/mc-pilot 0.6.0 → 0.7.0

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.
@@ -2,27 +2,68 @@ import { Command } from "commander";
2
2
  import { ServerInstanceManager } from "../instance/ServerInstanceManager.js";
3
3
  import { MctError } from "../util/errors.js";
4
4
  import { wrapCommand } from "../util/command.js";
5
- import { createRequestAction, sendClientRequest, withTransportTimeoutBuffer } from "./request-helpers.js";
5
+ import { createRequestAction, resolvePreferredClientName, sendClientRequest, withTransportTimeoutBuffer } from "./request-helpers.js";
6
+ function normalizeChatCommand(text) {
7
+ const command = text?.trim();
8
+ if (!command) {
9
+ throw new MctError({ code: "INVALID_PARAMS", message: "Command is required" }, 4);
10
+ }
11
+ return command;
12
+ }
13
+ async function executeServerCommand(context, serverName, command) {
14
+ const manager = new ServerInstanceManager(context.globalState, context.projectName);
15
+ const result = await manager.exec(serverName, command);
16
+ return {
17
+ ...result,
18
+ warning: "Commands that require a player sender should use --via client."
19
+ };
20
+ }
6
21
  export function createChatCommand() {
7
22
  const command = new Command("chat").description("Chat and server commands");
8
23
  command
9
24
  .command("send")
10
- .description("Send a chat message")
25
+ .description("Send a chat message. Slash-prefixed text is routed as a player command unless --literal is set.")
11
26
  .argument("<message>", "Message text")
12
- .action(createRequestAction("chat.send", ({ args }) => ({ message: args[0] })));
27
+ .option("--literal", "Send slash-prefixed text as plain chat instead of a command packet")
28
+ .action(wrapCommand(async (context, { args, options, globalOptions }) => {
29
+ const message = args[0] ?? "";
30
+ const preferredClient = resolvePreferredClientName(context, globalOptions);
31
+ if (!options.literal && message.trim().startsWith("/")) {
32
+ return sendClientRequest(context, preferredClient, "chat.command", { command: message });
33
+ }
34
+ return sendClientRequest(context, preferredClient, "chat.send", { message });
35
+ }));
13
36
  command
14
37
  .command("command")
15
- .description("Execute a server command. Defaults to server stdin FIFO (reliable); use --via client to route through the client's chat.")
38
+ .description("Execute a command. Defaults to auto-routing: prefer player context when a client is available, otherwise use server stdin.")
16
39
  .argument("<command>", "Command text, e.g. \"gamemode creative\" (leading slash optional)")
17
- .option("--via <target>", "Delivery channel: server (stdin FIFO, default) or client (client WS, may fail if chat disabled)", "server")
40
+ .option("--via <target>", "Delivery channel: auto (default), server (stdin FIFO), or client (client WS)", "auto")
18
41
  .option("--server <name>", "Server instance name when --via server (default: active profile server)")
19
42
  .action(wrapCommand(async (context, { args, options, globalOptions }) => {
20
- const via = options.via ?? "server";
43
+ const via = options.via ?? "auto";
44
+ const commandText = normalizeChatCommand(args[0]);
45
+ const preferredClient = resolvePreferredClientName(context, globalOptions);
21
46
  if (via === "client") {
22
- return sendClientRequest(context, globalOptions.client, "chat.command", { command: args[0] });
47
+ return sendClientRequest(context, preferredClient, "chat.command", { command: commandText });
48
+ }
49
+ if (via === "auto") {
50
+ if (preferredClient) {
51
+ return sendClientRequest(context, preferredClient, "chat.command", { command: commandText });
52
+ }
53
+ if (!context.projectName) {
54
+ return sendClientRequest(context, undefined, "chat.command", { command: commandText });
55
+ }
56
+ const serverName = options.server ?? context.activeProfile?.server;
57
+ if (!serverName) {
58
+ throw new MctError({
59
+ code: "INVALID_PARAMS",
60
+ message: "No client context is available, and auto-routing could not resolve a server. Use --client, --server, or run inside a project profile."
61
+ }, 4);
62
+ }
63
+ return executeServerCommand(context, serverName, commandText);
23
64
  }
24
65
  if (via !== "server") {
25
- throw new MctError({ code: "INVALID_PARAMS", message: `--via must be \"server\" or \"client\", got: ${via}` }, 4);
66
+ throw new MctError({ code: "INVALID_PARAMS", message: `--via must be \"auto\", \"server\" or \"client\", got: ${via}` }, 4);
26
67
  }
27
68
  if (!context.projectName) {
28
69
  throw new MctError({ code: "NO_PROJECT", message: "--via server requires a project context. Use --via client or run inside an mct project." }, 4);
@@ -31,8 +72,7 @@ export function createChatCommand() {
31
72
  if (!serverName) {
32
73
  throw new MctError({ code: "INVALID_PARAMS", message: "--via server requires --server <name> or an active profile with a server." }, 4);
33
74
  }
34
- const manager = new ServerInstanceManager(context.globalState, context.projectName);
35
- return manager.exec(serverName, args[0]);
75
+ return executeServerCommand(context, serverName, commandText);
36
76
  }));
37
77
  command
38
78
  .command("history")
@@ -49,5 +89,9 @@ export function createChatCommand() {
49
89
  .command("last")
50
90
  .description("Get the last chat message")
51
91
  .action(createRequestAction("chat.last", () => ({})));
92
+ command
93
+ .command("clear")
94
+ .description("Clear the cached chat history tracked by the client mod")
95
+ .action(createRequestAction("chat.clear", () => ({})));
52
96
  return command;
53
97
  }
@@ -1,2 +1,4 @@
1
1
  import { Command } from "commander";
2
+ import type { CommandContext } from "../util/context.js";
3
+ export declare function resolveProfileServerAddress(context: Pick<CommandContext, "projectName" | "activeProfile" | "globalState">, explicitServer: string | undefined, loadPort?: (projectName: string, serverName: string) => Promise<number>): Promise<string | undefined>;
2
4
  export declare function createClientCommand(): Command;
@@ -1,6 +1,7 @@
1
1
  import { Command } from "commander";
2
2
  import { buildClientSearchResults } from "../download/SearchCommand.js";
3
3
  import { ClientInstanceManager } from "../instance/ClientInstanceManager.js";
4
+ import { ServerInstanceManager } from "../instance/ServerInstanceManager.js";
4
5
  import { MctError } from "../util/errors.js";
5
6
  import { createRequestAction } from "./request-helpers.js";
6
7
  import { wrapCommand } from "../util/command.js";
@@ -12,6 +13,23 @@ import { copyFileIfMissing, downloadFile } from "../download/DownloadUtils.js";
12
13
  import { resolveClientInstanceDir } from "../util/paths.js";
13
14
  import { access, mkdir } from "node:fs/promises";
14
15
  import path from "node:path";
16
+ export async function resolveProfileServerAddress(context, explicitServer, loadPort) {
17
+ if (explicitServer) {
18
+ return explicitServer;
19
+ }
20
+ if (!context.projectName || !context.activeProfile?.server) {
21
+ return undefined;
22
+ }
23
+ try {
24
+ const port = loadPort
25
+ ? await loadPort(context.projectName, context.activeProfile.server)
26
+ : (await new ServerInstanceManager(context.globalState, context.projectName).loadMeta(context.activeProfile.server)).port;
27
+ return `127.0.0.1:${port}`;
28
+ }
29
+ catch {
30
+ return undefined;
31
+ }
32
+ }
15
33
  export function createClientCommand() {
16
34
  const command = new Command("client").description("Manage Minecraft client instances");
17
35
  command
@@ -125,7 +143,7 @@ export function createClientCommand() {
125
143
  .command("launch")
126
144
  .description("Launch a client instance")
127
145
  .argument("[name]", "Client instance name (default: from active profile)")
128
- .option("--server <address>", "Target server address (e.g. localhost:25565)")
146
+ .option("--server <address>", "Target server address (default: active profile server, e.g. localhost:25565)")
129
147
  .option("--account <account>", "Offline username or account identifier")
130
148
  .option("--ws-port <port>", "WebSocket port override", Number)
131
149
  .option("--headless", "Launch in headless mode")
@@ -136,7 +154,11 @@ export function createClientCommand() {
136
154
  throw new MctError({ code: "INVALID_PARAMS", message: "Client name is required. Specify it as argument or set a profile." }, 4);
137
155
  }
138
156
  const manager = new ClientInstanceManager(context.globalState);
139
- return manager.launch(clientName, options);
157
+ const serverAddress = await resolveProfileServerAddress(context, options.server);
158
+ return manager.launch(clientName, {
159
+ ...options,
160
+ server: serverAddress
161
+ });
140
162
  }));
141
163
  command
142
164
  .command("stop")
@@ -6,6 +6,7 @@ export interface RequestPayload<TOptions> {
6
6
  globalOptions: GlobalOptions;
7
7
  }
8
8
  export declare function sendClientRequest(context: CommandContext, clientName: string | undefined, action: string, params: Record<string, unknown>, timeoutSeconds?: number): Promise<unknown>;
9
+ export declare function resolvePreferredClientName(context: CommandContext, globalOptions: GlobalOptions): string | undefined;
9
10
  export declare function createRequestAction<TOptions = Record<string, any>>(action: string, buildParams: (payload: RequestPayload<TOptions>) => Record<string, unknown>, timeoutSelector?: (payload: RequestPayload<TOptions>, context: CommandContext) => number | undefined): (this: Command, ...input: unknown[]) => Promise<void>;
10
11
  export declare function parseJson(text: string, fieldName: string): Record<string, unknown>;
11
12
  export declare function parseNumberList(text: string): number[];
@@ -9,10 +9,13 @@ export async function sendClientRequest(context, clientName, action, params, tim
9
9
  const ws = new WebSocketClient(`ws://127.0.0.1:${client.wsPort}`);
10
10
  return ws.send(action, params, timeoutSeconds ?? context.timeout("default"));
11
11
  }
12
+ export function resolvePreferredClientName(context, globalOptions) {
13
+ return globalOptions.client ?? context.activeProfile?.clients[0];
14
+ }
12
15
  export function createRequestAction(action, buildParams, timeoutSelector) {
13
16
  return wrapCommand(async (context, payload) => {
14
17
  const timeout = timeoutSelector?.(payload, context);
15
- return sendClientRequest(context, payload.globalOptions.client, action, buildParams(payload), timeout);
18
+ return sendClientRequest(context, resolvePreferredClientName(context, payload.globalOptions), action, buildParams(payload), timeout);
16
19
  });
17
20
  }
18
21
  export function parseJson(text, fieldName) {
@@ -88,9 +88,15 @@ export function createServerCommand() {
88
88
  .command("status")
89
89
  .description("Show server status")
90
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);
91
+ .option("--all", "Show running servers across all projects")
92
+ .action(wrapCommand(async (context, { args, options }) => {
93
+ if (options.all || (!context.projectName && !args[0])) {
94
+ return ServerInstanceManager.statusAll(context.globalState);
95
+ }
96
+ if (!context.projectName) {
97
+ throw new MctError({ code: "NO_PROJECT", message: "No project context. Omit the name to inspect all running servers, or use --project <name>." }, 4);
98
+ }
99
+ const manager = new ServerInstanceManager(context.globalState, context.projectName);
94
100
  return manager.status(args[0]);
95
101
  }));
96
102
  command
@@ -137,6 +143,7 @@ export function createServerCommand() {
137
143
  .option("--follow", "Wait for new log lines (requires --timeout)")
138
144
  .option("--timeout <seconds>", "Max seconds to wait when --follow is set", Number)
139
145
  .option("--first-match", "With --follow, exit as soon as the first matching line appears")
146
+ .option("--raw-colors", "Preserve ANSI color escape sequences in returned lines")
140
147
  .action(wrapCommand(async (context, { args, options }) => {
141
148
  const project = requireProject(context);
142
149
  const serverName = resolveServerName(context, args[0]);
@@ -146,13 +153,15 @@ export function createServerCommand() {
146
153
  return manager.followLogs(serverName, {
147
154
  grep: options.grep,
148
155
  timeoutSeconds,
149
- firstMatchOnly: Boolean(options.firstMatch)
156
+ firstMatchOnly: Boolean(options.firstMatch),
157
+ rawColors: Boolean(options.rawColors)
150
158
  });
151
159
  }
152
160
  return manager.readLogs(serverName, {
153
161
  tail: options.tail,
154
162
  grep: options.grep,
155
- since: options.since
163
+ since: options.since,
164
+ rawColors: Boolean(options.rawColors)
156
165
  });
157
166
  }));
158
167
  return command;
@@ -244,7 +244,7 @@ export class ClientInstanceManager {
244
244
  });
245
245
  throw new MctError({
246
246
  code: "TIMEOUT",
247
- message: `Timed out after ${timeoutSeconds}s waiting for client ${clientName} to join a world (${wsUrl}). ${this.formatDiagnostics(diag)} Tip: try \`mct client reconnect --address <server>\` or \`mct client launch --force\`.`,
247
+ message: `Timed out after ${timeoutSeconds}s waiting for client ${clientName} to join a world (${wsUrl}). ${this.formatDiagnostics(diag)} Tip: if the client is still at the main menu, run \`mct client reconnect --address <server>\` or relaunch with \`mct client launch --server <address>\` (inside a project, plain \`mct client launch\` uses the active profile server).`,
248
248
  details: diag
249
249
  }, 2);
250
250
  }
@@ -1,5 +1,7 @@
1
1
  import type { GlobalStateStore } from "../util/global-state.js";
2
2
  import type { ServerInstanceMeta, ServerRuntimeEntry, ServerType } from "../util/instance-types.js";
3
+ export declare function stripAnsiCodes(text: string): string;
4
+ export declare function ensureServerPortProperty(instanceDir: string, port: number): Promise<void>;
3
5
  export interface CreateServerOptions {
4
6
  name: string;
5
7
  project: string;
@@ -60,6 +62,10 @@ export declare class ServerInstanceManager {
60
62
  stdinPipe?: string;
61
63
  running: boolean;
62
64
  }>;
65
+ static statusAll(globalState: GlobalStateStore): Promise<{
66
+ [key: string]: unknown;
67
+ running: boolean;
68
+ }[]>;
63
69
  waitReady(serverName: string, timeoutSeconds: number): Promise<{
64
70
  reachable: boolean;
65
71
  host: string;
@@ -74,6 +80,7 @@ export declare class ServerInstanceManager {
74
80
  tail?: number;
75
81
  grep?: string;
76
82
  since?: number;
83
+ rawColors?: boolean;
77
84
  }): Promise<{
78
85
  logPath: string;
79
86
  totalLines: number;
@@ -84,6 +91,7 @@ export declare class ServerInstanceManager {
84
91
  grep?: string;
85
92
  timeoutSeconds: number;
86
93
  firstMatchOnly?: boolean;
94
+ rawColors?: boolean;
87
95
  }): Promise<{
88
96
  logPath: string;
89
97
  matched: boolean;
@@ -8,6 +8,36 @@ import { waitForTcpPort } from "../util/net.js";
8
8
  import { isProcessRunning, killProcessTree } from "../util/process.js";
9
9
  import { copyFileIfMissing } from "../download/DownloadUtils.js";
10
10
  const INSTANCE_FILE = "instance.json";
11
+ const ANSI_ESCAPE_PATTERN = /\u001b\[[0-?]*[ -/]*[@-~]/g;
12
+ export function stripAnsiCodes(text) {
13
+ return text.replace(ANSI_ESCAPE_PATTERN, "");
14
+ }
15
+ export async function ensureServerPortProperty(instanceDir, port) {
16
+ const filePath = path.join(instanceDir, "server.properties");
17
+ let lines = [];
18
+ try {
19
+ const raw = await readFile(filePath, "utf8");
20
+ lines = raw.split(/\r?\n/);
21
+ if (lines.length > 0 && lines[lines.length - 1] === "") {
22
+ lines.pop();
23
+ }
24
+ }
25
+ catch {
26
+ // initialize from scratch
27
+ }
28
+ let updated = false;
29
+ lines = lines.map((line) => {
30
+ if (/^\s*server-port\s*=/.test(line)) {
31
+ updated = true;
32
+ return `server-port=${port}`;
33
+ }
34
+ return line;
35
+ });
36
+ if (!updated) {
37
+ lines.push(`server-port=${port}`);
38
+ }
39
+ await writeFile(filePath, `${lines.join("\n")}\n`, "utf8");
40
+ }
11
41
  export class ServerInstanceManager {
12
42
  globalState;
13
43
  project;
@@ -28,6 +58,7 @@ export class ServerInstanceManager {
28
58
  await writeFile(path.join(instanceDir, "eula.txt"), "eula=true\n", "utf8");
29
59
  }
30
60
  await mkdir(path.join(instanceDir, "plugins"), { recursive: true });
61
+ await ensureServerPortProperty(instanceDir, port);
31
62
  const meta = {
32
63
  name: options.name,
33
64
  project: options.project,
@@ -56,6 +87,7 @@ export class ServerInstanceManager {
56
87
  if (options.eula) {
57
88
  await writeFile(path.join(instanceDir, "eula.txt"), "eula=true\n", "utf8");
58
89
  }
90
+ await ensureServerPortProperty(instanceDir, meta.port);
59
91
  const mctHome = resolveMctHome();
60
92
  const logsDir = path.join(mctHome, "logs");
61
93
  const stateDir = path.join(mctHome, "state");
@@ -154,6 +186,19 @@ export class ServerInstanceManager {
154
186
  await this.globalState.writeServerState(state);
155
187
  return results;
156
188
  }
189
+ static async statusAll(globalState) {
190
+ const state = await globalState.readServerState();
191
+ const results = [];
192
+ for (const [key, entry] of Object.entries(state.servers)) {
193
+ const running = isProcessRunning(entry.pid);
194
+ if (!running) {
195
+ delete state.servers[key];
196
+ }
197
+ results.push({ running, ...entry });
198
+ }
199
+ await globalState.writeServerState(state);
200
+ return results;
201
+ }
157
202
  async waitReady(serverName, timeoutSeconds) {
158
203
  const stateKey = `${this.project}/${serverName}`;
159
204
  const state = await this.globalState.readServerState();
@@ -201,6 +246,9 @@ export class ServerInstanceManager {
201
246
  if (lines.length > 0 && lines[lines.length - 1] === "")
202
247
  lines = lines.slice(0, -1);
203
248
  const total = lines.length;
249
+ if (!options.rawColors) {
250
+ lines = lines.map((line) => stripAnsiCodes(line));
251
+ }
204
252
  if (options.since !== undefined && options.since > 0) {
205
253
  lines = lines.slice(Math.max(0, options.since));
206
254
  }
@@ -269,8 +317,9 @@ export class ServerInstanceManager {
269
317
  const parts = buffer.split("\n");
270
318
  buffer = parts.pop() ?? "";
271
319
  for (const line of parts) {
272
- if (!re || re.test(line)) {
273
- matches.push(line);
320
+ const rendered = options.rawColors ? line : stripAnsiCodes(line);
321
+ if (!re || re.test(rendered)) {
322
+ matches.push(rendered);
274
323
  if (options.firstMatchOnly)
275
324
  return finish(false);
276
325
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kzheart_/mc-pilot",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Minecraft plugin/mod automated testing CLI – control a real Minecraft client to simulate player actions",
5
5
  "type": "module",
6
6
  "bin": {