@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.
- package/dist/commands/chat.js +54 -10
- package/dist/commands/client.d.ts +2 -0
- package/dist/commands/client.js +24 -2
- package/dist/commands/request-helpers.d.ts +1 -0
- package/dist/commands/request-helpers.js +4 -1
- package/dist/commands/server.js +14 -5
- package/dist/instance/ClientInstanceManager.js +1 -1
- package/dist/instance/ServerInstanceManager.d.ts +8 -0
- package/dist/instance/ServerInstanceManager.js +51 -2
- package/package.json +1 -1
package/dist/commands/chat.js
CHANGED
|
@@ -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
|
-
.
|
|
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
|
|
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,
|
|
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 ?? "
|
|
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,
|
|
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
|
-
|
|
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;
|
package/dist/commands/client.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
18
|
+
return sendClientRequest(context, resolvePreferredClientName(context, payload.globalOptions), action, buildParams(payload), timeout);
|
|
16
19
|
});
|
|
17
20
|
}
|
|
18
21
|
export function parseJson(text, fieldName) {
|
package/dist/commands/server.js
CHANGED
|
@@ -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
|
-
.
|
|
92
|
-
|
|
93
|
-
|
|
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:
|
|
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
|
-
|
|
273
|
-
|
|
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
|
}
|