@kzheart_/mc-pilot 0.6.0 → 0.8.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")
@@ -47,6 +47,66 @@ function readAllEvents(filePath) {
47
47
  }
48
48
  return out;
49
49
  }
50
+ function filterEvents(events, options) {
51
+ let filtered = events;
52
+ if (options.sinceMs !== undefined) {
53
+ filtered = filtered.filter((event) => event.t >= options.sinceMs);
54
+ }
55
+ if (options.type) {
56
+ const wanted = new Set(options.type.split(",").map((s) => s.trim()).filter(Boolean));
57
+ filtered = filtered.filter((event) => wanted.has(event.type));
58
+ }
59
+ return filtered;
60
+ }
61
+ function buildPayloadText(event) {
62
+ return JSON.stringify({
63
+ type: event.type,
64
+ payload: event.payload ?? {}
65
+ });
66
+ }
67
+ function buildMatchPattern(raw) {
68
+ if (!raw)
69
+ return null;
70
+ try {
71
+ return new RegExp(raw);
72
+ }
73
+ catch {
74
+ throw new MctError({ code: "INVALID_PARAMS", message: `Invalid --match pattern: ${raw}` }, 4);
75
+ }
76
+ }
77
+ async function waitForEvent(filePath, options) {
78
+ const deadline = Date.now() + options.timeoutSeconds * 1000;
79
+ const matchPattern = buildMatchPattern(options.match);
80
+ const startedAt = Date.now();
81
+ while (Date.now() < deadline) {
82
+ const events = filterEvents(readAllEvents(filePath), {
83
+ sinceMs: options.sinceMs,
84
+ type: options.type
85
+ });
86
+ const matched = matchPattern
87
+ ? events.find((event) => matchPattern.test(buildPayloadText(event)))
88
+ : events[0];
89
+ if (matched) {
90
+ return {
91
+ file: filePath,
92
+ matched: true,
93
+ waitedMs: Date.now() - startedAt,
94
+ event: matched
95
+ };
96
+ }
97
+ await new Promise((resolve) => setTimeout(resolve, 250));
98
+ }
99
+ throw new MctError({
100
+ code: "TIMEOUT",
101
+ message: `Timed out after ${options.timeoutSeconds}s waiting for events in ${filePath}`,
102
+ details: {
103
+ file: filePath,
104
+ type: options.type ?? null,
105
+ match: options.match ?? null,
106
+ sinceMs: options.sinceMs
107
+ }
108
+ }, 2);
109
+ }
50
110
  export function createEventsCommand() {
51
111
  const command = new Command("events").description("Inspect the client event log (written by the mod to ~/.mct/logs/<client>/events.jsonl)");
52
112
  command
@@ -61,15 +121,10 @@ export function createEventsCommand() {
61
121
  const clientName = globalOptions.client ?? context.activeProfile?.clients[0];
62
122
  const filePath = options.file ?? resolveEventsFile(clientName);
63
123
  const events = readAllEvents(filePath);
64
- let filtered = events;
65
- const sinceMs = parseSince(options.since, Date.now());
66
- if (sinceMs !== undefined) {
67
- filtered = filtered.filter((e) => e.t >= sinceMs);
68
- }
69
- if (options.type) {
70
- const wanted = new Set(options.type.split(",").map((s) => s.trim()).filter(Boolean));
71
- filtered = filtered.filter((e) => wanted.has(e.type));
72
- }
124
+ let filtered = filterEvents(events, {
125
+ sinceMs: parseSince(options.since, Date.now()),
126
+ type: options.type
127
+ });
73
128
  if (!options.all) {
74
129
  const tail = options.tail ?? 20;
75
130
  if (filtered.length > tail) {
@@ -93,15 +148,31 @@ export function createEventsCommand() {
93
148
  const tail = args[0] ? Number(args[0]) : 20;
94
149
  const clientName = globalOptions.client ?? context.activeProfile?.clients[0];
95
150
  const filePath = options.file ?? resolveEventsFile(clientName);
96
- let events = readAllEvents(filePath);
97
- if (options.type) {
98
- const wanted = new Set(options.type.split(",").map((s) => s.trim()).filter(Boolean));
99
- events = events.filter((e) => wanted.has(e.type));
100
- }
151
+ let events = filterEvents(readAllEvents(filePath), {
152
+ type: options.type
153
+ });
101
154
  if (events.length > tail)
102
155
  events = events.slice(events.length - tail);
103
156
  return { file: filePath, returned: events.length, events };
104
157
  }));
158
+ command
159
+ .command("wait")
160
+ .description("Wait for a matching event. Defaults to events emitted after this command starts.")
161
+ .option("--timeout <seconds>", "Timeout in seconds (default 10)", Number)
162
+ .option("--since <duration>", "Also consider events since duration ago (e.g. 30s, 5m, 1h) or epoch ms")
163
+ .option("--type <types>", "Comma-separated list of event types to include")
164
+ .option("--match <pattern>", "Regex matched against event type and payload JSON")
165
+ .option("--file <path>", "Override the log file path")
166
+ .action(wrapCommand(async (context, { options, globalOptions }) => {
167
+ const clientName = globalOptions.client ?? context.activeProfile?.clients[0];
168
+ const filePath = options.file ?? resolveEventsFile(clientName);
169
+ return waitForEvent(filePath, {
170
+ timeoutSeconds: options.timeout ?? context.timeout("default"),
171
+ sinceMs: parseSince(options.since, Date.now()) ?? Date.now(),
172
+ type: options.type,
173
+ match: options.match
174
+ });
175
+ }));
105
176
  command
106
177
  .command("clear")
107
178
  .description("Truncate the event log for the active client")
@@ -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) {
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function createSchemaCommand(getProgram: () => Command): Command;
@@ -0,0 +1,10 @@
1
+ import { Command } from "commander";
2
+ import { buildSchemaDocument } from "../schema.js";
3
+ import { wrapCommand } from "../util/command.js";
4
+ export function createSchemaCommand(getProgram) {
5
+ return new Command("schema")
6
+ .description("Output a machine-readable CLI and protocol schema")
7
+ .action(wrapCommand(async () => {
8
+ return buildSchemaDocument(getProgram());
9
+ }));
10
+ }
@@ -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;
package/dist/index.js CHANGED
@@ -21,6 +21,7 @@ import { createResourcepackCommand } from "./commands/resourcepack.js";
21
21
  import { createRotationCommand } from "./commands/rotation.js";
22
22
  import { createScreenCommand } from "./commands/screen.js";
23
23
  import { createScreenshotCommand } from "./commands/screenshot.js";
24
+ import { createSchemaCommand } from "./commands/schema.js";
24
25
  import { createServerCommand } from "./commands/server.js";
25
26
  import { createSignCommand } from "./commands/sign.js";
26
27
  import { createStatusCommand } from "./commands/status.js";
@@ -67,6 +68,7 @@ export function buildProgram() {
67
68
  program.addCommand(createServerCommand());
68
69
  program.addCommand(createClientCommand());
69
70
  program.addCommand(createPluginCommand());
71
+ program.addCommand(createSchemaCommand(() => program));
70
72
  // Game interaction commands
71
73
  program.addCommand(createChatCommand());
72
74
  program.addCommand(createInputCommand());
@@ -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
  }
@@ -0,0 +1,63 @@
1
+ import type { Argument, Command, Option } from "commander";
2
+ interface ProtocolEntry {
3
+ name?: string;
4
+ code?: string;
5
+ description?: string;
6
+ params?: string[];
7
+ exitCode?: number;
8
+ }
9
+ declare function serializeOption(option: Option): {
10
+ name: string;
11
+ flags: string;
12
+ description: string;
13
+ required: boolean;
14
+ mandatory: boolean;
15
+ defaultValue: any;
16
+ };
17
+ declare function serializeArgument(argument: Argument): {
18
+ name: string;
19
+ required: boolean;
20
+ variadic: boolean;
21
+ };
22
+ declare function serializeCommand(command: Command, parents?: string[]): {
23
+ name: string;
24
+ path: string;
25
+ description: string;
26
+ aliases: string[];
27
+ arguments: ReturnType<typeof serializeArgument>[];
28
+ options: ReturnType<typeof serializeOption>[];
29
+ subcommands: ReturnType<typeof serializeCommand>[];
30
+ leaf: boolean;
31
+ };
32
+ export declare function buildSchemaDocument(program: Command): {
33
+ schemaVersion: number;
34
+ cli: {
35
+ name: string;
36
+ description: string;
37
+ globalOptions: {
38
+ name: string;
39
+ flags: string;
40
+ description: string;
41
+ required: boolean;
42
+ mandatory: boolean;
43
+ defaultValue: any;
44
+ }[];
45
+ commands: {
46
+ name: string;
47
+ path: string;
48
+ description: string;
49
+ aliases: string[];
50
+ arguments: ReturnType<typeof serializeArgument>[];
51
+ options: ReturnType<typeof serializeOption>[];
52
+ subcommands: ReturnType<typeof serializeCommand>[];
53
+ leaf: boolean;
54
+ }[];
55
+ leafCommands: string[];
56
+ };
57
+ protocol: {
58
+ actions: number | ProtocolEntry[];
59
+ queries: number | ProtocolEntry[];
60
+ errors: number | ProtocolEntry[];
61
+ };
62
+ };
63
+ export {};
package/dist/schema.js ADDED
@@ -0,0 +1,77 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ function resolveRepoRoot() {
5
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
6
+ }
7
+ function loadJsonFile(relativePath) {
8
+ const filePath = path.join(resolveRepoRoot(), relativePath);
9
+ return JSON.parse(readFileSync(filePath, "utf8"));
10
+ }
11
+ function serializeOption(option) {
12
+ return {
13
+ name: option.name(),
14
+ flags: option.flags,
15
+ description: option.description,
16
+ required: option.required,
17
+ mandatory: option.mandatory,
18
+ defaultValue: option.defaultValue
19
+ };
20
+ }
21
+ function serializeArgument(argument) {
22
+ return {
23
+ name: argument.name(),
24
+ required: argument.required,
25
+ variadic: argument.variadic
26
+ };
27
+ }
28
+ function serializeCommand(command, parents = []) {
29
+ const pathSegments = [...parents, command.name()];
30
+ return {
31
+ name: command.name(),
32
+ path: pathSegments.join(" "),
33
+ description: command.description(),
34
+ aliases: command.aliases(),
35
+ arguments: command.registeredArguments.map(serializeArgument),
36
+ options: command.options.map(serializeOption),
37
+ subcommands: command.commands.map((subcommand) => serializeCommand(subcommand, pathSegments)),
38
+ leaf: command.commands.length === 0
39
+ };
40
+ }
41
+ function collectLeafCommands(commands) {
42
+ const leaves = [];
43
+ const visit = (command) => {
44
+ if (command.leaf) {
45
+ leaves.push(command.path);
46
+ return;
47
+ }
48
+ for (const subcommand of command.subcommands) {
49
+ visit(subcommand);
50
+ }
51
+ };
52
+ for (const command of commands) {
53
+ visit(command);
54
+ }
55
+ return leaves;
56
+ }
57
+ export function buildSchemaDocument(program) {
58
+ const actions = loadJsonFile("protocol/actions.json");
59
+ const queries = loadJsonFile("protocol/queries.json");
60
+ const errors = loadJsonFile("protocol/errors.json");
61
+ const commands = program.commands.map((command) => serializeCommand(command));
62
+ return {
63
+ schemaVersion: 1,
64
+ cli: {
65
+ name: program.name(),
66
+ description: program.description(),
67
+ globalOptions: program.options.map(serializeOption),
68
+ commands,
69
+ leafCommands: collectLeafCommands(commands)
70
+ },
71
+ protocol: {
72
+ actions: actions.actions ?? [],
73
+ queries: queries.queries ?? [],
74
+ errors: errors.errors ?? []
75
+ }
76
+ };
77
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kzheart_/mc-pilot",
3
- "version": "0.6.0",
3
+ "version": "0.8.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": {