@kzheart_/mc-pilot 0.4.1 → 0.6.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 +26 -4
- package/dist/commands/client.js +4 -0
- package/dist/commands/events.d.ts +2 -0
- package/dist/commands/events.js +129 -0
- package/dist/commands/input.js +1 -4
- package/dist/commands/plugin.js +1 -1
- package/dist/commands/request-helpers.d.ts +1 -1
- package/dist/commands/server.js +39 -0
- package/dist/index.js +2 -0
- package/dist/instance/ServerInstanceManager.d.ts +27 -0
- package/dist/instance/ServerInstanceManager.js +135 -2
- package/dist/util/command.d.ts +1 -1
- package/dist/util/command.js +7 -1
- package/package.json +1 -1
package/dist/commands/chat.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import {
|
|
2
|
+
import { ServerInstanceManager } from "../instance/ServerInstanceManager.js";
|
|
3
|
+
import { MctError } from "../util/errors.js";
|
|
4
|
+
import { wrapCommand } from "../util/command.js";
|
|
5
|
+
import { createRequestAction, sendClientRequest, withTransportTimeoutBuffer } from "./request-helpers.js";
|
|
3
6
|
export function createChatCommand() {
|
|
4
7
|
const command = new Command("chat").description("Chat and server commands");
|
|
5
8
|
command
|
|
@@ -9,9 +12,28 @@ export function createChatCommand() {
|
|
|
9
12
|
.action(createRequestAction("chat.send", ({ args }) => ({ message: args[0] })));
|
|
10
13
|
command
|
|
11
14
|
.command("command")
|
|
12
|
-
.description("Execute a server command (
|
|
13
|
-
.argument("<command>", "Command text, e.g. \"gamemode creative\"")
|
|
14
|
-
.
|
|
15
|
+
.description("Execute a server command. Defaults to server stdin FIFO (reliable); use --via client to route through the client's chat.")
|
|
16
|
+
.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")
|
|
18
|
+
.option("--server <name>", "Server instance name when --via server (default: active profile server)")
|
|
19
|
+
.action(wrapCommand(async (context, { args, options, globalOptions }) => {
|
|
20
|
+
const via = options.via ?? "server";
|
|
21
|
+
if (via === "client") {
|
|
22
|
+
return sendClientRequest(context, globalOptions.client, "chat.command", { command: args[0] });
|
|
23
|
+
}
|
|
24
|
+
if (via !== "server") {
|
|
25
|
+
throw new MctError({ code: "INVALID_PARAMS", message: `--via must be \"server\" or \"client\", got: ${via}` }, 4);
|
|
26
|
+
}
|
|
27
|
+
if (!context.projectName) {
|
|
28
|
+
throw new MctError({ code: "NO_PROJECT", message: "--via server requires a project context. Use --via client or run inside an mct project." }, 4);
|
|
29
|
+
}
|
|
30
|
+
const serverName = options.server ?? context.activeProfile?.server;
|
|
31
|
+
if (!serverName) {
|
|
32
|
+
throw new MctError({ code: "INVALID_PARAMS", message: "--via server requires --server <name> or an active profile with a server." }, 4);
|
|
33
|
+
}
|
|
34
|
+
const manager = new ServerInstanceManager(context.globalState, context.projectName);
|
|
35
|
+
return manager.exec(serverName, args[0]);
|
|
36
|
+
}));
|
|
15
37
|
command
|
|
16
38
|
.command("history")
|
|
17
39
|
.description("Get chat history")
|
package/dist/commands/client.js
CHANGED
|
@@ -176,5 +176,9 @@ export function createClientCommand() {
|
|
|
176
176
|
.action(createRequestAction("client.reconnect", ({ options }) => ({
|
|
177
177
|
address: options.address
|
|
178
178
|
})));
|
|
179
|
+
command
|
|
180
|
+
.command("respawn")
|
|
181
|
+
.description("Respawn the player after death (sends C2S respawn packet, bypasses DeathScreen auto-respawn)")
|
|
182
|
+
.action(createRequestAction("client.respawn", () => ({})));
|
|
179
183
|
return command;
|
|
180
184
|
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { MctError } from "../util/errors.js";
|
|
5
|
+
import { resolveMctHome } from "../util/paths.js";
|
|
6
|
+
import { wrapCommand } from "../util/command.js";
|
|
7
|
+
function resolveEventsFile(clientName) {
|
|
8
|
+
if (!clientName) {
|
|
9
|
+
throw new MctError({
|
|
10
|
+
code: "INVALID_PARAMS",
|
|
11
|
+
message: "Client name is required. Use --client <name> or set an active profile."
|
|
12
|
+
}, 4);
|
|
13
|
+
}
|
|
14
|
+
return path.join(resolveMctHome(), "logs", clientName, "events.jsonl");
|
|
15
|
+
}
|
|
16
|
+
function parseSince(raw, nowMs) {
|
|
17
|
+
if (!raw)
|
|
18
|
+
return undefined;
|
|
19
|
+
const trimmed = raw.trim();
|
|
20
|
+
// 支持 "30s" / "5m" / "1h" / "200ms" / epoch 毫秒
|
|
21
|
+
const m = /^(\d+(?:\.\d+)?)(ms|s|m|h)?$/.exec(trimmed);
|
|
22
|
+
if (!m) {
|
|
23
|
+
throw new MctError({ code: "INVALID_PARAMS", message: `Invalid --since value: ${raw}` }, 4);
|
|
24
|
+
}
|
|
25
|
+
const value = Number(m[1]);
|
|
26
|
+
const unit = m[2];
|
|
27
|
+
if (!unit) {
|
|
28
|
+
// epoch millis
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
const multipliers = { ms: 1, s: 1000, m: 60_000, h: 3_600_000 };
|
|
32
|
+
return nowMs - value * multipliers[unit];
|
|
33
|
+
}
|
|
34
|
+
function readAllEvents(filePath) {
|
|
35
|
+
if (!existsSync(filePath))
|
|
36
|
+
return [];
|
|
37
|
+
const raw = readFileSync(filePath, "utf8");
|
|
38
|
+
const lines = raw.split("\n").filter((line) => line.length > 0);
|
|
39
|
+
const out = [];
|
|
40
|
+
for (const line of lines) {
|
|
41
|
+
try {
|
|
42
|
+
out.push(JSON.parse(line));
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// 跳过损坏行
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
export function createEventsCommand() {
|
|
51
|
+
const command = new Command("events").description("Inspect the client event log (written by the mod to ~/.mct/logs/<client>/events.jsonl)");
|
|
52
|
+
command
|
|
53
|
+
.command("list")
|
|
54
|
+
.description("Print events. Defaults to the last 20 for the active client.")
|
|
55
|
+
.option("--tail <n>", "Show the last N events (default 20)", (v) => Number(v))
|
|
56
|
+
.option("--since <duration>", "Only show events since duration ago (e.g. 30s, 5m, 1h) or epoch ms")
|
|
57
|
+
.option("--type <types>", "Comma-separated list of event types to include")
|
|
58
|
+
.option("--all", "Show all events (ignore --tail)")
|
|
59
|
+
.option("--file <path>", "Override the log file path")
|
|
60
|
+
.action(wrapCommand(async (context, { options, globalOptions }) => {
|
|
61
|
+
const clientName = globalOptions.client ?? context.activeProfile?.clients[0];
|
|
62
|
+
const filePath = options.file ?? resolveEventsFile(clientName);
|
|
63
|
+
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
|
+
}
|
|
73
|
+
if (!options.all) {
|
|
74
|
+
const tail = options.tail ?? 20;
|
|
75
|
+
if (filtered.length > tail) {
|
|
76
|
+
filtered = filtered.slice(filtered.length - tail);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
file: filePath,
|
|
81
|
+
total: events.length,
|
|
82
|
+
returned: filtered.length,
|
|
83
|
+
events: filtered
|
|
84
|
+
};
|
|
85
|
+
}));
|
|
86
|
+
command
|
|
87
|
+
.command("tail")
|
|
88
|
+
.description("Shortcut for `events list --tail N`")
|
|
89
|
+
.argument("[n]", "Number of events (default 20)")
|
|
90
|
+
.option("--type <types>", "Comma-separated event types to include")
|
|
91
|
+
.option("--file <path>", "Override the log file path")
|
|
92
|
+
.action(wrapCommand(async (context, { args, options, globalOptions }) => {
|
|
93
|
+
const tail = args[0] ? Number(args[0]) : 20;
|
|
94
|
+
const clientName = globalOptions.client ?? context.activeProfile?.clients[0];
|
|
95
|
+
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
|
+
}
|
|
101
|
+
if (events.length > tail)
|
|
102
|
+
events = events.slice(events.length - tail);
|
|
103
|
+
return { file: filePath, returned: events.length, events };
|
|
104
|
+
}));
|
|
105
|
+
command
|
|
106
|
+
.command("clear")
|
|
107
|
+
.description("Truncate the event log for the active client")
|
|
108
|
+
.option("--file <path>", "Override the log file path")
|
|
109
|
+
.action(wrapCommand(async (context, { options, globalOptions }) => {
|
|
110
|
+
const { truncateSync, existsSync: exists, mkdirSync } = await import("node:fs");
|
|
111
|
+
const clientName = globalOptions.client ?? context.activeProfile?.clients[0];
|
|
112
|
+
const filePath = options.file ?? resolveEventsFile(clientName);
|
|
113
|
+
if (exists(filePath)) {
|
|
114
|
+
truncateSync(filePath, 0);
|
|
115
|
+
return { cleared: true, file: filePath };
|
|
116
|
+
}
|
|
117
|
+
// ensure directory exists for future writes
|
|
118
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
119
|
+
return { cleared: false, reason: "file_not_found", file: filePath };
|
|
120
|
+
}));
|
|
121
|
+
command
|
|
122
|
+
.command("path")
|
|
123
|
+
.description("Print the expected path to the event log for the active client")
|
|
124
|
+
.action(wrapCommand(async (context, { globalOptions }) => {
|
|
125
|
+
const clientName = globalOptions.client ?? context.activeProfile?.clients[0];
|
|
126
|
+
return { file: resolveEventsFile(clientName), exists: existsSync(resolveEventsFile(clientName)) };
|
|
127
|
+
}));
|
|
128
|
+
return command;
|
|
129
|
+
}
|
package/dist/commands/input.js
CHANGED
|
@@ -102,10 +102,7 @@ export function createInputCommand() {
|
|
|
102
102
|
.description("Press a key combination in sequence")
|
|
103
103
|
.argument("<keys...>", "Key names")
|
|
104
104
|
.action(createRequestAction("input.key-combo", ({ args }) => ({
|
|
105
|
-
keys:
|
|
106
|
-
.split(",")
|
|
107
|
-
.map((value) => value.trim())
|
|
108
|
-
.filter(Boolean)
|
|
105
|
+
keys: args.filter((v) => v !== undefined)
|
|
109
106
|
})));
|
|
110
107
|
command
|
|
111
108
|
.command("type")
|
package/dist/commands/plugin.js
CHANGED
|
@@ -83,7 +83,7 @@ export function createPluginCommand() {
|
|
|
83
83
|
.argument("<ids...>", "Plugin IDs to resolve")
|
|
84
84
|
.action(wrapCommand(async (_context, { args }) => {
|
|
85
85
|
const manager = new PluginCatalogManager();
|
|
86
|
-
const resolved = await manager.resolve(args);
|
|
86
|
+
const resolved = await manager.resolve(args.filter((v) => v !== undefined));
|
|
87
87
|
return {
|
|
88
88
|
order: resolved.map((p) => p.id),
|
|
89
89
|
plugins: resolved
|
package/dist/commands/server.js
CHANGED
|
@@ -116,5 +116,44 @@ export function createServerCommand() {
|
|
|
116
116
|
const manager = new ServerInstanceManager(context.globalState, project);
|
|
117
117
|
return manager.waitReady(serverName, options.timeout ?? context.timeout("serverReady"));
|
|
118
118
|
}));
|
|
119
|
+
command
|
|
120
|
+
.command("exec")
|
|
121
|
+
.description("Send a console command directly to the server stdin FIFO (bypasses client chat)")
|
|
122
|
+
.argument("<command...>", "Command text (leading slash optional, e.g. \"say hi\" or \"op TEST1\")")
|
|
123
|
+
.option("--server <name>", "Server instance name (default: from active profile)")
|
|
124
|
+
.action(wrapCommand(async (context, { args, options }) => {
|
|
125
|
+
const project = requireProject(context);
|
|
126
|
+
const serverName = resolveServerName(context, options.server);
|
|
127
|
+
const manager = new ServerInstanceManager(context.globalState, project);
|
|
128
|
+
return manager.exec(serverName, args.filter((v) => v !== undefined).join(" "));
|
|
129
|
+
}));
|
|
130
|
+
command
|
|
131
|
+
.command("logs")
|
|
132
|
+
.description("Read the server log file (with optional tail/grep/follow)")
|
|
133
|
+
.argument("[name]", "Server instance name (default: from active profile)")
|
|
134
|
+
.option("--tail <n>", "Show only the last N lines", Number)
|
|
135
|
+
.option("--grep <pattern>", "Filter lines by regex")
|
|
136
|
+
.option("--since <lineNumber>", "Skip the first N lines (0-indexed)", Number)
|
|
137
|
+
.option("--follow", "Wait for new log lines (requires --timeout)")
|
|
138
|
+
.option("--timeout <seconds>", "Max seconds to wait when --follow is set", Number)
|
|
139
|
+
.option("--first-match", "With --follow, exit as soon as the first matching line appears")
|
|
140
|
+
.action(wrapCommand(async (context, { args, options }) => {
|
|
141
|
+
const project = requireProject(context);
|
|
142
|
+
const serverName = resolveServerName(context, args[0]);
|
|
143
|
+
const manager = new ServerInstanceManager(context.globalState, project);
|
|
144
|
+
if (options.follow) {
|
|
145
|
+
const timeoutSeconds = options.timeout ?? 30;
|
|
146
|
+
return manager.followLogs(serverName, {
|
|
147
|
+
grep: options.grep,
|
|
148
|
+
timeoutSeconds,
|
|
149
|
+
firstMatchOnly: Boolean(options.firstMatch)
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return manager.readLogs(serverName, {
|
|
153
|
+
tail: options.tail,
|
|
154
|
+
grep: options.grep,
|
|
155
|
+
since: options.since
|
|
156
|
+
});
|
|
157
|
+
}));
|
|
119
158
|
return command;
|
|
120
159
|
}
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { createChatCommand } from "./commands/chat.js";
|
|
|
9
9
|
import { createCombatCommand } from "./commands/combat.js";
|
|
10
10
|
import { createAnvilCommand, createCraftCommand, createEnchantCommand, createTradeCommand } from "./commands/craft.js";
|
|
11
11
|
import { createEntityCommand } from "./commands/entity.js";
|
|
12
|
+
import { createEventsCommand } from "./commands/events.js";
|
|
12
13
|
import { createGuiCommand } from "./commands/gui.js";
|
|
13
14
|
import { createHudCommand } from "./commands/hud.js";
|
|
14
15
|
import { createInputCommand } from "./commands/input.js";
|
|
@@ -90,6 +91,7 @@ export function buildProgram() {
|
|
|
90
91
|
program.addCommand(createEnchantCommand());
|
|
91
92
|
program.addCommand(createTradeCommand());
|
|
92
93
|
program.addCommand(createWaitCommand());
|
|
94
|
+
program.addCommand(createEventsCommand());
|
|
93
95
|
return program;
|
|
94
96
|
}
|
|
95
97
|
const program = buildProgram();
|
|
@@ -65,6 +65,33 @@ export declare class ServerInstanceManager {
|
|
|
65
65
|
host: string;
|
|
66
66
|
port: number;
|
|
67
67
|
}>;
|
|
68
|
+
exec(serverName: string, command: string): Promise<{
|
|
69
|
+
sent: boolean;
|
|
70
|
+
command: string;
|
|
71
|
+
stdinPipe: string;
|
|
72
|
+
}>;
|
|
73
|
+
readLogs(serverName: string, options?: {
|
|
74
|
+
tail?: number;
|
|
75
|
+
grep?: string;
|
|
76
|
+
since?: number;
|
|
77
|
+
}): Promise<{
|
|
78
|
+
logPath: string;
|
|
79
|
+
totalLines: number;
|
|
80
|
+
returnedLines: number;
|
|
81
|
+
lines: string[];
|
|
82
|
+
}>;
|
|
83
|
+
followLogs(serverName: string, options: {
|
|
84
|
+
grep?: string;
|
|
85
|
+
timeoutSeconds: number;
|
|
86
|
+
firstMatchOnly?: boolean;
|
|
87
|
+
}): Promise<{
|
|
88
|
+
logPath: string;
|
|
89
|
+
matched: boolean;
|
|
90
|
+
matches: string[];
|
|
91
|
+
timedOut: boolean;
|
|
92
|
+
}>;
|
|
93
|
+
private requireRuntimeEntry;
|
|
94
|
+
private requireRunning;
|
|
68
95
|
list(): Promise<ServerInstanceMeta[]>;
|
|
69
96
|
static listAll(globalState: GlobalStateStore): Promise<ServerInstanceMeta[]>;
|
|
70
97
|
deploy(serverName: string, jarPaths: string[], cwd: string): Promise<string[]>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { copyFile, mkdir, readdir, readFile, writeFile, unlink } from "node:fs/promises";
|
|
2
|
-
import {
|
|
1
|
+
import { copyFile, mkdir, open as fsOpen, readdir, readFile, stat, writeFile, unlink } from "node:fs/promises";
|
|
2
|
+
import { openSync, writeSync, closeSync, mkdirSync } from "node:fs";
|
|
3
3
|
import { spawn, execSync } from "node:child_process";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { resolveMctHome, resolveProjectDir, resolveServerInstanceDir } from "../util/paths.js";
|
|
@@ -163,6 +163,139 @@ export class ServerInstanceManager {
|
|
|
163
163
|
}
|
|
164
164
|
return waitForTcpPort("127.0.0.1", entry.port, timeoutSeconds);
|
|
165
165
|
}
|
|
166
|
+
async exec(serverName, command) {
|
|
167
|
+
const entry = await this.requireRunning(serverName);
|
|
168
|
+
if (!entry.stdinPipe) {
|
|
169
|
+
throw new MctError({ code: "SERVER_STDIN_UNAVAILABLE", message: `Server ${this.project}/${serverName} has no stdin FIFO (detached mode?)` }, 5);
|
|
170
|
+
}
|
|
171
|
+
const trimmed = command.trim();
|
|
172
|
+
if (!trimmed) {
|
|
173
|
+
throw new MctError({ code: "INVALID_PARAMS", message: "Command is required" }, 4);
|
|
174
|
+
}
|
|
175
|
+
const line = `${trimmed.replace(/^\//, "")}\n`;
|
|
176
|
+
// O_NONBLOCK write: bash wrapper holds FIFO fd in rw mode so this returns immediately.
|
|
177
|
+
let fd;
|
|
178
|
+
try {
|
|
179
|
+
fd = openSync(entry.stdinPipe, "w");
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
throw new MctError({ code: "SERVER_STDIN_OPEN_FAILED", message: `Failed to open stdin FIFO: ${error.message}`, details: { stdinPipe: entry.stdinPipe } }, 5);
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
writeSync(fd, line);
|
|
186
|
+
}
|
|
187
|
+
finally {
|
|
188
|
+
closeSync(fd);
|
|
189
|
+
}
|
|
190
|
+
return { sent: true, command: trimmed, stdinPipe: entry.stdinPipe };
|
|
191
|
+
}
|
|
192
|
+
async readLogs(serverName, options = {}) {
|
|
193
|
+
const entry = await this.requireRuntimeEntry(serverName);
|
|
194
|
+
const logPath = entry.logPath;
|
|
195
|
+
const raw = await readFile(logPath, "utf8").catch((error) => {
|
|
196
|
+
if (error.code === "ENOENT")
|
|
197
|
+
return "";
|
|
198
|
+
throw error;
|
|
199
|
+
});
|
|
200
|
+
let lines = raw.split("\n");
|
|
201
|
+
if (lines.length > 0 && lines[lines.length - 1] === "")
|
|
202
|
+
lines = lines.slice(0, -1);
|
|
203
|
+
const total = lines.length;
|
|
204
|
+
if (options.since !== undefined && options.since > 0) {
|
|
205
|
+
lines = lines.slice(Math.max(0, options.since));
|
|
206
|
+
}
|
|
207
|
+
if (options.grep) {
|
|
208
|
+
const re = new RegExp(options.grep);
|
|
209
|
+
lines = lines.filter((line) => re.test(line));
|
|
210
|
+
}
|
|
211
|
+
if (options.tail !== undefined && options.tail > 0 && lines.length > options.tail) {
|
|
212
|
+
lines = lines.slice(lines.length - options.tail);
|
|
213
|
+
}
|
|
214
|
+
return { logPath, totalLines: total, returnedLines: lines.length, lines };
|
|
215
|
+
}
|
|
216
|
+
async followLogs(serverName, options) {
|
|
217
|
+
const entry = await this.requireRuntimeEntry(serverName);
|
|
218
|
+
const logPath = entry.logPath;
|
|
219
|
+
const re = options.grep ? new RegExp(options.grep) : null;
|
|
220
|
+
let offset = 0;
|
|
221
|
+
try {
|
|
222
|
+
offset = (await stat(logPath)).size;
|
|
223
|
+
}
|
|
224
|
+
catch { /* file may not exist yet */ }
|
|
225
|
+
const matches = [];
|
|
226
|
+
let buffer = "";
|
|
227
|
+
let done = false;
|
|
228
|
+
return await new Promise((resolve) => {
|
|
229
|
+
let timer;
|
|
230
|
+
let poll;
|
|
231
|
+
const finish = (timedOut) => {
|
|
232
|
+
if (done)
|
|
233
|
+
return;
|
|
234
|
+
done = true;
|
|
235
|
+
if (poll)
|
|
236
|
+
clearInterval(poll);
|
|
237
|
+
if (timer)
|
|
238
|
+
clearTimeout(timer);
|
|
239
|
+
resolve({ logPath, matched: matches.length > 0, matches, timedOut });
|
|
240
|
+
};
|
|
241
|
+
const drain = async () => {
|
|
242
|
+
if (done)
|
|
243
|
+
return;
|
|
244
|
+
let currentSize;
|
|
245
|
+
try {
|
|
246
|
+
currentSize = (await stat(logPath)).size;
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (currentSize < offset) {
|
|
252
|
+
offset = 0;
|
|
253
|
+
buffer = "";
|
|
254
|
+
} // rotation / truncate
|
|
255
|
+
if (currentSize === offset)
|
|
256
|
+
return;
|
|
257
|
+
// Read raw bytes and decode — stat().size is bytes, not UTF-16 chars.
|
|
258
|
+
const fh = await fsOpen(logPath, "r");
|
|
259
|
+
try {
|
|
260
|
+
const length = currentSize - offset;
|
|
261
|
+
const buf = Buffer.allocUnsafe(length);
|
|
262
|
+
await fh.read(buf, 0, length, offset);
|
|
263
|
+
offset = currentSize;
|
|
264
|
+
buffer += buf.toString("utf8");
|
|
265
|
+
}
|
|
266
|
+
finally {
|
|
267
|
+
await fh.close();
|
|
268
|
+
}
|
|
269
|
+
const parts = buffer.split("\n");
|
|
270
|
+
buffer = parts.pop() ?? "";
|
|
271
|
+
for (const line of parts) {
|
|
272
|
+
if (!re || re.test(line)) {
|
|
273
|
+
matches.push(line);
|
|
274
|
+
if (options.firstMatchOnly)
|
|
275
|
+
return finish(false);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
timer = setTimeout(() => finish(true), options.timeoutSeconds * 1000);
|
|
280
|
+
poll = setInterval(() => { void drain(); }, 300);
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
async requireRuntimeEntry(serverName) {
|
|
284
|
+
const stateKey = `${this.project}/${serverName}`;
|
|
285
|
+
const state = await this.globalState.readServerState();
|
|
286
|
+
const entry = state.servers[stateKey];
|
|
287
|
+
if (!entry) {
|
|
288
|
+
throw new MctError({ code: "SERVER_NOT_RUNNING", message: `Server ${stateKey} is not running` }, 5);
|
|
289
|
+
}
|
|
290
|
+
return entry;
|
|
291
|
+
}
|
|
292
|
+
async requireRunning(serverName) {
|
|
293
|
+
const entry = await this.requireRuntimeEntry(serverName);
|
|
294
|
+
if (!isProcessRunning(entry.pid)) {
|
|
295
|
+
throw new MctError({ code: "SERVER_NOT_RUNNING", message: `Server ${this.project}/${serverName} PID ${entry.pid} is not alive` }, 5);
|
|
296
|
+
}
|
|
297
|
+
return entry;
|
|
298
|
+
}
|
|
166
299
|
async list() {
|
|
167
300
|
const projectDir = resolveProjectDir(this.project);
|
|
168
301
|
try {
|
package/dist/util/command.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
2
|
import { createCommandContext, type GlobalOptions } from "./context.js";
|
|
3
3
|
export type CommandAction<TOptions = Record<string, unknown>> = (context: Awaited<ReturnType<typeof createCommandContext>>, payload: {
|
|
4
|
-
args: string[];
|
|
4
|
+
args: (string | undefined)[];
|
|
5
5
|
options: TOptions;
|
|
6
6
|
command: Command;
|
|
7
7
|
globalOptions: GlobalOptions;
|
package/dist/util/command.js
CHANGED
|
@@ -13,7 +13,13 @@ export function wrapCommand(action) {
|
|
|
13
13
|
return async function wrappedCommand(...input) {
|
|
14
14
|
const command = input.at(-1);
|
|
15
15
|
const options = input.at(-2);
|
|
16
|
-
const args = input.slice(0, -2).
|
|
16
|
+
const args = input.slice(0, -2).flatMap((value) => {
|
|
17
|
+
if (Array.isArray(value))
|
|
18
|
+
return value.map((v) => String(v));
|
|
19
|
+
if (value === undefined || value === null)
|
|
20
|
+
return [undefined];
|
|
21
|
+
return [String(value)];
|
|
22
|
+
});
|
|
17
23
|
const globalOptions = command.optsWithGlobals();
|
|
18
24
|
try {
|
|
19
25
|
const context = await createCommandContext(globalOptions);
|