@kzheart_/mc-pilot 0.2.0 → 0.3.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.
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function createPluginCommand(): Command;
@@ -0,0 +1,93 @@
1
+ import { Command } from "commander";
2
+ import { PluginCatalogManager } from "../instance/PluginCatalogManager.js";
3
+ import { MctError } from "../util/errors.js";
4
+ import { wrapCommand } from "../util/command.js";
5
+ function parseCommaSeparated(value) {
6
+ if (!value)
7
+ return undefined;
8
+ return value.split(",").map((s) => s.trim()).filter(Boolean);
9
+ }
10
+ export function createPluginCommand() {
11
+ const plugin = new Command("plugin")
12
+ .description("Manage the plugin center catalog");
13
+ plugin.addCommand(new Command("list")
14
+ .description("List plugins in the catalog")
15
+ .option("--query <query>", "Search by name, id, or description")
16
+ .action(wrapCommand(async (_context, { options }) => {
17
+ const manager = new PluginCatalogManager();
18
+ const plugins = await manager.list(options.query);
19
+ return { plugins, count: plugins.length };
20
+ })));
21
+ plugin.addCommand(new Command("info")
22
+ .description("Show plugin details")
23
+ .argument("<id>", "Plugin ID")
24
+ .action(wrapCommand(async (_context, { args }) => {
25
+ const manager = new PluginCatalogManager();
26
+ return manager.get(args[0]);
27
+ })));
28
+ plugin.addCommand(new Command("add")
29
+ .description("Add a plugin JAR to the catalog (id/name auto-derived from filename)")
30
+ .argument("<jar-path>", "Path to the plugin JAR file")
31
+ .option("--id <id>", "Override plugin ID")
32
+ .option("--name <name>", "Override display name")
33
+ .action(wrapCommand(async (_context, { args, options }) => {
34
+ const manager = new PluginCatalogManager();
35
+ return manager.add(args[0], {
36
+ id: options.id,
37
+ name: options.name
38
+ });
39
+ })));
40
+ plugin.addCommand(new Command("update")
41
+ .description("Update plugin metadata")
42
+ .argument("<id>", "Plugin ID")
43
+ .option("--name <name>", "Display name")
44
+ .option("--version <version>", "Version")
45
+ .option("--description <description>", "Description")
46
+ .option("--author <author>", "Author")
47
+ .option("--dependencies <deps>", "Dependency plugin IDs (comma-separated)")
48
+ .option("--tags <tags>", "Tags (comma-separated)")
49
+ .action(wrapCommand(async (_context, { args, options }) => {
50
+ const manager = new PluginCatalogManager();
51
+ return manager.update(args[0], {
52
+ name: options.name,
53
+ version: options.version,
54
+ description: options.description,
55
+ author: options.author,
56
+ dependencies: parseCommaSeparated(options.dependencies),
57
+ tags: parseCommaSeparated(options.tags)
58
+ });
59
+ })));
60
+ plugin.addCommand(new Command("remove")
61
+ .description("Remove a plugin from the catalog")
62
+ .argument("<id>", "Plugin ID")
63
+ .action(wrapCommand(async (_context, { args }) => {
64
+ const manager = new PluginCatalogManager();
65
+ const removed = await manager.remove(args[0]);
66
+ return { removed: removed.id, name: removed.name };
67
+ })));
68
+ plugin.addCommand(new Command("install")
69
+ .description("Install a plugin (with dependencies) to a server")
70
+ .argument("<id>", "Plugin ID")
71
+ .requiredOption("--server <name>", "Target server instance name")
72
+ .option("--project <name>", "Project name")
73
+ .action(wrapCommand(async (context, { args, options }) => {
74
+ const project = options.project ?? context.projectName;
75
+ if (!project) {
76
+ throw new MctError({ code: "NO_PROJECT", message: "No project specified. Use --project or run from a project directory." }, 4);
77
+ }
78
+ const manager = new PluginCatalogManager();
79
+ return manager.install(args[0], project, options.server);
80
+ })));
81
+ plugin.addCommand(new Command("resolve")
82
+ .description("Resolve dependency tree for plugins")
83
+ .argument("<ids...>", "Plugin IDs to resolve")
84
+ .action(wrapCommand(async (_context, { args }) => {
85
+ const manager = new PluginCatalogManager();
86
+ const resolved = await manager.resolve(args);
87
+ return {
88
+ order: resolved.map((p) => p.id),
89
+ plugins: resolved
90
+ };
91
+ })));
92
+ return plugin;
93
+ }
package/dist/index.js CHANGED
@@ -24,6 +24,7 @@ import { createServerCommand } from "./commands/server.js";
24
24
  import { createSignCommand } from "./commands/sign.js";
25
25
  import { createStatusCommand } from "./commands/status.js";
26
26
  import { createWaitCommand } from "./commands/wait.js";
27
+ import { createPluginCommand } from "./commands/plugin.js";
27
28
  import { createInitCommand, createDeployCommand, createUpCommand, createDownCommand, createUseCommand } from "./commands/project.js";
28
29
  import { attachGlobalOptions, wrapCommand } from "./util/command.js";
29
30
  export function buildProgram() {
@@ -64,6 +65,7 @@ export function buildProgram() {
64
65
  // Instance management
65
66
  program.addCommand(createServerCommand());
66
67
  program.addCommand(createClientCommand());
68
+ program.addCommand(createPluginCommand());
67
69
  // Game interaction commands
68
70
  program.addCommand(createChatCommand());
69
71
  program.addCommand(createInputCommand());
@@ -0,0 +1,18 @@
1
+ import type { PluginCatalog, PluginEntry } from "../util/plugin-types.js";
2
+ export declare class PluginCatalogManager {
3
+ private pluginsDir;
4
+ private jarsDir;
5
+ constructor();
6
+ loadCatalog(): Promise<PluginCatalog>;
7
+ private saveCatalog;
8
+ list(query?: string): Promise<PluginEntry[]>;
9
+ get(id: string): Promise<PluginEntry>;
10
+ add(jarPath: string, overrides?: Partial<Omit<PluginEntry, "jarFile" | "addedAt">>): Promise<PluginEntry>;
11
+ update(id: string, fields: Partial<Omit<PluginEntry, "id" | "jarFile" | "addedAt">>): Promise<PluginEntry>;
12
+ remove(id: string): Promise<PluginEntry>;
13
+ resolve(ids: string[]): Promise<PluginEntry[]>;
14
+ install(id: string, project: string, serverName: string): Promise<{
15
+ installed: string[];
16
+ serverPluginsDir: string;
17
+ }>;
18
+ }
@@ -0,0 +1,151 @@
1
+ import { copyFile, mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { resolvePluginsDir, resolvePluginJarsDir, resolveServerInstanceDir } from "../util/paths.js";
4
+ import { MctError } from "../util/errors.js";
5
+ const CATALOG_FILE = "catalog.json";
6
+ export class PluginCatalogManager {
7
+ pluginsDir;
8
+ jarsDir;
9
+ constructor() {
10
+ this.pluginsDir = resolvePluginsDir();
11
+ this.jarsDir = resolvePluginJarsDir();
12
+ }
13
+ async loadCatalog() {
14
+ try {
15
+ const content = await readFile(path.join(this.pluginsDir, CATALOG_FILE), "utf-8");
16
+ return JSON.parse(content);
17
+ }
18
+ catch {
19
+ return { plugins: [] };
20
+ }
21
+ }
22
+ async saveCatalog(catalog) {
23
+ await mkdir(this.pluginsDir, { recursive: true });
24
+ await writeFile(path.join(this.pluginsDir, CATALOG_FILE), JSON.stringify(catalog, null, 2), "utf-8");
25
+ }
26
+ async list(query) {
27
+ const catalog = await this.loadCatalog();
28
+ if (!query)
29
+ return catalog.plugins;
30
+ const q = query.toLowerCase();
31
+ return catalog.plugins.filter((p) => p.id.toLowerCase().includes(q) ||
32
+ p.name.toLowerCase().includes(q) ||
33
+ p.description.toLowerCase().includes(q) ||
34
+ p.tags.some((t) => t.toLowerCase().includes(q)));
35
+ }
36
+ async get(id) {
37
+ const catalog = await this.loadCatalog();
38
+ const entry = catalog.plugins.find((p) => p.id === id);
39
+ if (!entry) {
40
+ throw new MctError({ code: "PLUGIN_NOT_FOUND", message: `Plugin '${id}' not found in catalog` }, 4);
41
+ }
42
+ return entry;
43
+ }
44
+ async add(jarPath, overrides) {
45
+ const catalog = await this.loadCatalog();
46
+ const resolvedJar = path.resolve(jarPath);
47
+ const originalName = path.basename(resolvedJar, ".jar");
48
+ // Derive id from filename if not provided
49
+ const id = overrides?.id || originalName.toLowerCase().replace(/[^a-z0-9_-]/g, "-");
50
+ if (catalog.plugins.some((p) => p.id === id)) {
51
+ throw new MctError({ code: "PLUGIN_EXISTS", message: `Plugin '${id}' already exists in catalog` }, 4);
52
+ }
53
+ // Keep original JAR filename
54
+ const jarFileName = path.basename(resolvedJar);
55
+ await mkdir(this.jarsDir, { recursive: true });
56
+ await copyFile(resolvedJar, path.join(this.jarsDir, jarFileName));
57
+ const entry = {
58
+ id,
59
+ name: overrides?.name || originalName,
60
+ version: overrides?.version || "",
61
+ description: overrides?.description || "",
62
+ author: overrides?.author || "",
63
+ jarFile: jarFileName,
64
+ dependencies: overrides?.dependencies || [],
65
+ tags: overrides?.tags || [],
66
+ addedAt: new Date().toISOString()
67
+ };
68
+ catalog.plugins.push(entry);
69
+ await this.saveCatalog(catalog);
70
+ return entry;
71
+ }
72
+ async update(id, fields) {
73
+ const catalog = await this.loadCatalog();
74
+ const entry = catalog.plugins.find((p) => p.id === id);
75
+ if (!entry) {
76
+ throw new MctError({ code: "PLUGIN_NOT_FOUND", message: `Plugin '${id}' not found in catalog` }, 4);
77
+ }
78
+ if (fields.name !== undefined)
79
+ entry.name = fields.name;
80
+ if (fields.version !== undefined)
81
+ entry.version = fields.version;
82
+ if (fields.description !== undefined)
83
+ entry.description = fields.description;
84
+ if (fields.author !== undefined)
85
+ entry.author = fields.author;
86
+ if (fields.dependencies !== undefined)
87
+ entry.dependencies = fields.dependencies;
88
+ if (fields.tags !== undefined)
89
+ entry.tags = fields.tags;
90
+ await this.saveCatalog(catalog);
91
+ return entry;
92
+ }
93
+ async remove(id) {
94
+ const catalog = await this.loadCatalog();
95
+ const index = catalog.plugins.findIndex((p) => p.id === id);
96
+ if (index === -1) {
97
+ throw new MctError({ code: "PLUGIN_NOT_FOUND", message: `Plugin '${id}' not found in catalog` }, 4);
98
+ }
99
+ const [entry] = catalog.plugins.splice(index, 1);
100
+ try {
101
+ await rm(path.join(this.jarsDir, entry.jarFile));
102
+ }
103
+ catch {
104
+ // JAR file may already be deleted
105
+ }
106
+ await this.saveCatalog(catalog);
107
+ return entry;
108
+ }
109
+ async resolve(ids) {
110
+ const catalog = await this.loadCatalog();
111
+ const byId = new Map(catalog.plugins.map((p) => [p.id, p]));
112
+ const resolved = [];
113
+ const visited = new Set();
114
+ const visiting = new Set();
115
+ const visit = (id) => {
116
+ if (visited.has(id))
117
+ return;
118
+ if (visiting.has(id)) {
119
+ throw new MctError({ code: "CIRCULAR_DEPENDENCY", message: `Circular dependency detected involving '${id}'` }, 4);
120
+ }
121
+ const entry = byId.get(id);
122
+ if (!entry) {
123
+ throw new MctError({ code: "MISSING_DEPENDENCY", message: `Missing dependency: '${id}'` }, 4);
124
+ }
125
+ visiting.add(id);
126
+ for (const dep of entry.dependencies) {
127
+ visit(dep);
128
+ }
129
+ visiting.delete(id);
130
+ visited.add(id);
131
+ resolved.push(entry);
132
+ };
133
+ for (const id of ids) {
134
+ visit(id);
135
+ }
136
+ return resolved;
137
+ }
138
+ async install(id, project, serverName) {
139
+ const toInstall = await this.resolve([id]);
140
+ const serverPluginsDir = path.join(resolveServerInstanceDir(project, serverName), "plugins");
141
+ await mkdir(serverPluginsDir, { recursive: true });
142
+ const installed = [];
143
+ for (const entry of toInstall) {
144
+ const src = path.join(this.jarsDir, entry.jarFile);
145
+ const dest = path.join(serverPluginsDir, entry.jarFile);
146
+ await copyFile(src, dest);
147
+ installed.push(entry.id);
148
+ }
149
+ return { installed, serverPluginsDir };
150
+ }
151
+ }
@@ -5,3 +5,5 @@ export declare function resolveProjectsDir(): string;
5
5
  export declare function resolveProjectDir(project: string): string;
6
6
  export declare function resolveServerInstanceDir(project: string, server: string): string;
7
7
  export declare function resolveGlobalStateDir(): string;
8
+ export declare function resolvePluginsDir(): string;
9
+ export declare function resolvePluginJarsDir(): string;
@@ -21,3 +21,9 @@ export function resolveServerInstanceDir(project, server) {
21
21
  export function resolveGlobalStateDir() {
22
22
  return path.join(resolveMctHome(), "state");
23
23
  }
24
+ export function resolvePluginsDir() {
25
+ return path.join(resolveMctHome(), "plugins");
26
+ }
27
+ export function resolvePluginJarsDir() {
28
+ return path.join(resolvePluginsDir(), "jars");
29
+ }
@@ -0,0 +1,14 @@
1
+ export interface PluginEntry {
2
+ id: string;
3
+ name: string;
4
+ version: string;
5
+ description: string;
6
+ author: string;
7
+ jarFile: string;
8
+ dependencies: string[];
9
+ tags: string[];
10
+ addedAt: string;
11
+ }
12
+ export interface PluginCatalog {
13
+ plugins: PluginEntry[];
14
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kzheart_/mc-pilot",
3
- "version": "0.2.0",
3
+ "version": "0.3.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": {
@@ -1,82 +0,0 @@
1
- import type { CommandContext } from "../util/context.js";
2
- export interface ClientRuntimeState {
3
- name: string;
4
- version?: string;
5
- account?: string;
6
- server?: string;
7
- wsPort: number;
8
- headless: boolean;
9
- pid: number;
10
- startedAt: string;
11
- logPath: string;
12
- }
13
- export interface LaunchClientOptions {
14
- name: string;
15
- version?: string;
16
- account?: string;
17
- server?: string;
18
- wsPort?: number;
19
- headless?: boolean;
20
- }
21
- export declare class ClientManager {
22
- private readonly context;
23
- constructor(context: CommandContext);
24
- launch(options: LaunchClientOptions): Promise<ClientRuntimeState>;
25
- stop(name: string): Promise<{
26
- stopped: boolean;
27
- name: string;
28
- pid?: undefined;
29
- } | {
30
- stopped: boolean;
31
- name: string;
32
- pid: number;
33
- }>;
34
- list(): Promise<{
35
- defaultClient: string | undefined;
36
- clients: ({
37
- running: boolean;
38
- detached: boolean;
39
- name: string;
40
- version?: string;
41
- account?: string;
42
- server?: string;
43
- wsPort: number;
44
- headless: boolean;
45
- pid: number;
46
- startedAt: string;
47
- logPath: string;
48
- } | {
49
- running: boolean;
50
- stale: boolean;
51
- name: string;
52
- version?: string;
53
- account?: string;
54
- server?: string;
55
- wsPort: number;
56
- headless: boolean;
57
- pid: number;
58
- startedAt: string;
59
- logPath: string;
60
- } | {
61
- running: boolean;
62
- name: string;
63
- version?: string;
64
- account?: string;
65
- server?: string;
66
- wsPort: number;
67
- headless: boolean;
68
- pid: number;
69
- startedAt: string;
70
- logPath: string;
71
- })[];
72
- }>;
73
- waitReady(name: string, timeoutSeconds: number): Promise<{
74
- connected: boolean;
75
- url: string;
76
- }>;
77
- getClient(name?: string): Promise<ClientRuntimeState>;
78
- getWsUrl(wsPort: number): string;
79
- private getSnapshot;
80
- private writeSnapshot;
81
- private isWsReachable;
82
- }
@@ -1,221 +0,0 @@
1
- import { mkdirSync, openSync } from "node:fs";
2
- import { spawn } from "node:child_process";
3
- import path from "node:path";
4
- import { fileURLToPath } from "node:url";
5
- import { MctError } from "../util/errors.js";
6
- import { getListeningPids, isProcessRunning, killProcessTree } from "../util/process.js";
7
- import { WebSocketClient } from "./WebSocketClient.js";
8
- function getLaunchScriptPath() {
9
- const thisFile = fileURLToPath(import.meta.url);
10
- // dist/client/ClientManager.js -> scripts/launch-fabric-client.mjs
11
- return path.resolve(path.dirname(thisFile), "..", "..", "scripts", "launch-fabric-client.mjs");
12
- }
13
- const CLIENT_STATE_FILE = "clients.json";
14
- function getDefaultSnapshot() {
15
- return {
16
- clients: {}
17
- };
18
- }
19
- export class ClientManager {
20
- context;
21
- constructor(context) {
22
- this.context = context;
23
- }
24
- async launch(options) {
25
- const snapshot = await this.getSnapshot();
26
- const existing = snapshot.clients[options.name];
27
- if (existing && isProcessRunning(existing.pid)) {
28
- throw new MctError({
29
- code: "CLIENT_ALREADY_RUNNING",
30
- message: `Client ${options.name} is already running`,
31
- details: existing
32
- }, 3);
33
- }
34
- const configured = this.context.config.clients[options.name] ?? {};
35
- const wsPort = options.wsPort ?? configured.wsPort;
36
- if (!wsPort) {
37
- throw new MctError({
38
- code: "INVALID_PARAMS",
39
- message: `Client ${options.name} requires wsPort`
40
- }, 4);
41
- }
42
- const listeningPids = getListeningPids(wsPort);
43
- for (const pid of listeningPids) {
44
- killProcessTree(pid);
45
- }
46
- if (listeningPids.length > 0) {
47
- const deadline = Date.now() + 10_000;
48
- while (Date.now() < deadline) {
49
- if (getListeningPids(wsPort).length === 0) {
50
- break;
51
- }
52
- await new Promise((resolve) => setTimeout(resolve, 250));
53
- }
54
- }
55
- const launchCommand = configured.launchArgs
56
- ? [process.execPath, getLaunchScriptPath(), ...configured.launchArgs]
57
- : configured.launchCommand;
58
- if (!launchCommand || launchCommand.length === 0) {
59
- throw new MctError({
60
- code: "INVALID_PARAMS",
61
- message: `Client ${options.name} requires launchArgs (or launchCommand) in config`
62
- }, 4);
63
- }
64
- const cwd = configured.workingDir
65
- ? path.resolve(this.context.cwd, configured.workingDir)
66
- : this.context.cwd;
67
- mkdirSync(path.join(this.context.state.getRootDir(), "logs"), { recursive: true });
68
- const logPath = path.join(this.context.state.getRootDir(), "logs", `client-${options.name}.log`);
69
- const stdout = openSync(logPath, "a");
70
- const child = spawn(launchCommand[0], launchCommand.slice(1), {
71
- cwd,
72
- detached: true,
73
- stdio: ["ignore", stdout, stdout],
74
- env: {
75
- ...process.env,
76
- ...configured.env,
77
- MCT_CLIENT_NAME: options.name,
78
- MCT_CLIENT_VERSION: options.version ?? configured.version ?? "",
79
- MCT_CLIENT_ACCOUNT: options.account ?? configured.account ?? "",
80
- MCT_CLIENT_SERVER: options.server ?? configured.server ?? "",
81
- MCT_CLIENT_WS_PORT: String(wsPort),
82
- MCT_CLIENT_HEADLESS: String(options.headless ?? configured.headless ?? false)
83
- }
84
- });
85
- child.unref();
86
- const clientState = {
87
- name: options.name,
88
- version: options.version ?? configured.version,
89
- account: options.account ?? configured.account,
90
- server: options.server ?? configured.server,
91
- wsPort,
92
- headless: options.headless ?? configured.headless ?? false,
93
- pid: child.pid ?? 0,
94
- startedAt: new Date().toISOString(),
95
- logPath
96
- };
97
- snapshot.defaultClient ??= options.name;
98
- snapshot.clients[options.name] = clientState;
99
- await this.writeSnapshot(snapshot);
100
- return clientState;
101
- }
102
- async stop(name) {
103
- const snapshot = await this.getSnapshot();
104
- const client = snapshot.clients[name];
105
- if (!client) {
106
- return {
107
- stopped: false,
108
- name
109
- };
110
- }
111
- if (isProcessRunning(client.pid)) {
112
- killProcessTree(client.pid);
113
- }
114
- for (const pid of getListeningPids(client.wsPort)) {
115
- if (pid !== client.pid) {
116
- killProcessTree(pid);
117
- }
118
- }
119
- delete snapshot.clients[name];
120
- if (snapshot.defaultClient === name) {
121
- snapshot.defaultClient = Object.keys(snapshot.clients)[0];
122
- }
123
- await this.writeSnapshot(snapshot);
124
- return {
125
- stopped: true,
126
- name,
127
- pid: client.pid
128
- };
129
- }
130
- async list() {
131
- const snapshot = await this.getSnapshot();
132
- const clients = await Promise.all(Object.values(snapshot.clients).map(async (client) => {
133
- const running = isProcessRunning(client.pid);
134
- if (!running) {
135
- const wsReachable = await this.isWsReachable(client.wsPort, 1);
136
- if (wsReachable) {
137
- return {
138
- ...client,
139
- running: true,
140
- detached: true
141
- };
142
- }
143
- return {
144
- ...client,
145
- running: false,
146
- stale: true
147
- };
148
- }
149
- return {
150
- ...client,
151
- running: true
152
- };
153
- }));
154
- return {
155
- defaultClient: snapshot.defaultClient,
156
- clients
157
- };
158
- }
159
- async waitReady(name, timeoutSeconds) {
160
- const client = await this.getClient(name);
161
- const deadline = Date.now() + timeoutSeconds * 1000;
162
- while (Date.now() < deadline) {
163
- try {
164
- const ws = new WebSocketClient(this.getWsUrl(client.wsPort));
165
- return await ws.ping(1);
166
- }
167
- catch {
168
- await new Promise((resolve) => {
169
- setTimeout(resolve, 500);
170
- });
171
- }
172
- }
173
- throw new MctError({
174
- code: "TIMEOUT",
175
- message: `Timed out waiting for client ${name} to open WebSocket on ${this.getWsUrl(client.wsPort)}`
176
- }, 2);
177
- }
178
- async getClient(name) {
179
- const snapshot = await this.getSnapshot();
180
- const resolvedName = name ?? snapshot.defaultClient;
181
- if (!resolvedName) {
182
- throw new MctError({
183
- code: "CLIENT_NOT_FOUND",
184
- message: "No client is configured or running"
185
- }, 3);
186
- }
187
- const client = snapshot.clients[resolvedName];
188
- if (!client) {
189
- throw new MctError({
190
- code: "CLIENT_NOT_FOUND",
191
- message: `Client ${resolvedName} was not found`
192
- }, 3);
193
- }
194
- if (!isProcessRunning(client.pid) && !(await this.isWsReachable(client.wsPort, 1))) {
195
- throw new MctError({
196
- code: "CLIENT_NOT_RUNNING",
197
- message: `Client ${resolvedName} is not running`
198
- }, 3);
199
- }
200
- return client;
201
- }
202
- getWsUrl(wsPort) {
203
- return `ws://127.0.0.1:${wsPort}`;
204
- }
205
- async getSnapshot() {
206
- return this.context.state.readJson(CLIENT_STATE_FILE, getDefaultSnapshot());
207
- }
208
- async writeSnapshot(snapshot) {
209
- await this.context.state.writeJson(CLIENT_STATE_FILE, snapshot);
210
- }
211
- async isWsReachable(wsPort, timeoutSeconds) {
212
- try {
213
- const ws = new WebSocketClient(this.getWsUrl(wsPort));
214
- await ws.ping(timeoutSeconds);
215
- return true;
216
- }
217
- catch {
218
- return false;
219
- }
220
- }
221
- }
@@ -1,2 +0,0 @@
1
- import { Command } from "commander";
2
- export declare function createChannelCommand(): Command;
@@ -1,24 +0,0 @@
1
- import { Command } from "commander";
2
- import { createRequestAction, parseJson, withTransportTimeoutBuffer } from "./request-helpers.js";
3
- export function createChannelCommand() {
4
- const command = new Command("channel").description("Plugin Channel");
5
- command
6
- .command("send")
7
- .description("发送频道消息")
8
- .argument("<channel>", "频道名称")
9
- .requiredOption("--data <json>", "JSON 数据")
10
- .action(createRequestAction("channel.send", ({ args, options }) => ({
11
- channel: args[0],
12
- data: parseJson(String(options.data), "data")
13
- })));
14
- command
15
- .command("listen")
16
- .description("监听频道消息")
17
- .argument("<channel>", "频道名称")
18
- .option("--timeout <seconds>", "等待超时秒数", Number)
19
- .action(createRequestAction("channel.listen", ({ args, options }) => ({
20
- channel: args[0],
21
- timeout: options.timeout
22
- }), ({ options }, context) => withTransportTimeoutBuffer(options.timeout ? Number(options.timeout) : undefined, context.config.timeout.default)));
23
- return command;
24
- }
@@ -1,2 +0,0 @@
1
- import { Command } from "commander";
2
- export declare function createEffectsCommand(): Command;
@@ -1,16 +0,0 @@
1
- import { Command } from "commander";
2
- import { createRequestAction } from "./request-helpers.js";
3
- export function createEffectsCommand() {
4
- const command = new Command("effects").description("音效与粒子事件");
5
- command
6
- .command("sounds")
7
- .description("获取音效事件")
8
- .option("--last <count>", "最近条数", Number)
9
- .action(createRequestAction("effects.sounds", ({ options }) => ({ last: options.last ?? 10 })));
10
- command
11
- .command("particles")
12
- .description("获取粒子事件")
13
- .option("--last <count>", "最近条数", Number)
14
- .action(createRequestAction("effects.particles", ({ options }) => ({ last: options.last ?? 10 })));
15
- return command;
16
- }
@@ -1,63 +0,0 @@
1
- import type { CommandContext } from "../util/context.js";
2
- export interface ServerRuntimeState {
3
- pid: number;
4
- jar: string;
5
- dir: string;
6
- port: number;
7
- startedAt: string;
8
- logPath: string;
9
- }
10
- export interface StartServerOptions {
11
- jar?: string;
12
- dir?: string;
13
- port?: number;
14
- eula?: boolean;
15
- }
16
- export declare class ServerManager {
17
- private readonly context;
18
- constructor(context: CommandContext);
19
- start(options: StartServerOptions): Promise<{
20
- pid: number;
21
- jar: string;
22
- dir: string;
23
- port: number;
24
- startedAt: string;
25
- logPath: string;
26
- running: boolean;
27
- }>;
28
- stop(): Promise<{
29
- running: boolean;
30
- stopped: boolean;
31
- pid?: undefined;
32
- } | {
33
- running: boolean;
34
- stopped: boolean;
35
- pid: number;
36
- }>;
37
- status(): Promise<{
38
- running: boolean;
39
- } | {
40
- pid: number;
41
- jar: string;
42
- dir: string;
43
- port: number;
44
- startedAt: string;
45
- logPath: string;
46
- running: boolean;
47
- stale: boolean;
48
- } | {
49
- pid: number;
50
- jar: string;
51
- dir: string;
52
- port: number;
53
- startedAt: string;
54
- logPath: string;
55
- running: boolean;
56
- }>;
57
- waitReady(timeoutSeconds: number): Promise<{
58
- reachable: boolean;
59
- host: string;
60
- port: number;
61
- }>;
62
- getState(): Promise<ServerRuntimeState | null>;
63
- }
@@ -1,114 +0,0 @@
1
- import { mkdirSync, openSync, writeFileSync } from "node:fs";
2
- import { spawn } from "node:child_process";
3
- import path from "node:path";
4
- import { MctError } from "../util/errors.js";
5
- import { waitForTcpPort } from "../util/net.js";
6
- import { isProcessRunning, killProcessTree } from "../util/process.js";
7
- const SERVER_STATE_FILE = "server.json";
8
- export class ServerManager {
9
- context;
10
- constructor(context) {
11
- this.context = context;
12
- }
13
- async start(options) {
14
- const existing = await this.getState();
15
- if (existing && isProcessRunning(existing.pid)) {
16
- throw new MctError({
17
- code: "SERVER_ALREADY_RUNNING",
18
- message: "Server is already running",
19
- details: existing
20
- }, 5);
21
- }
22
- const jar = options.jar ?? this.context.config.server.jar;
23
- if (!jar) {
24
- throw new MctError({
25
- code: "INVALID_PARAMS",
26
- message: "Server jar is required"
27
- }, 4);
28
- }
29
- const dir = path.resolve(this.context.cwd, options.dir ?? this.context.config.server.dir);
30
- const port = options.port ?? this.context.config.server.port;
31
- mkdirSync(dir, { recursive: true });
32
- mkdirSync(path.join(this.context.state.getRootDir(), "logs"), { recursive: true });
33
- if (options.eula) {
34
- writeFileSync(path.join(dir, "eula.txt"), "eula=true\n", "utf8");
35
- }
36
- const logPath = path.join(this.context.state.getRootDir(), "logs", "paper-server.log");
37
- const stdout = openSync(logPath, "a");
38
- const child = spawn("java", [...this.context.config.server.jvmArgs, "-jar", path.resolve(this.context.cwd, jar), "nogui"], {
39
- cwd: dir,
40
- detached: true,
41
- stdio: ["ignore", stdout, stdout],
42
- env: {
43
- ...process.env,
44
- MCT_SERVER_PORT: String(port)
45
- }
46
- });
47
- child.unref();
48
- const state = {
49
- pid: child.pid ?? 0,
50
- jar: path.resolve(this.context.cwd, jar),
51
- dir,
52
- port,
53
- startedAt: new Date().toISOString(),
54
- logPath
55
- };
56
- await this.context.state.writeJson(SERVER_STATE_FILE, state);
57
- return {
58
- running: true,
59
- ...state
60
- };
61
- }
62
- async stop() {
63
- const state = await this.getState();
64
- if (!state) {
65
- return {
66
- running: false,
67
- stopped: false
68
- };
69
- }
70
- if (isProcessRunning(state.pid)) {
71
- killProcessTree(state.pid);
72
- }
73
- await this.context.state.remove(SERVER_STATE_FILE);
74
- return {
75
- running: false,
76
- stopped: true,
77
- pid: state.pid
78
- };
79
- }
80
- async status() {
81
- const state = await this.getState();
82
- if (!state) {
83
- return {
84
- running: false
85
- };
86
- }
87
- const running = isProcessRunning(state.pid);
88
- if (!running) {
89
- await this.context.state.remove(SERVER_STATE_FILE);
90
- return {
91
- running: false,
92
- stale: true,
93
- ...state
94
- };
95
- }
96
- return {
97
- running: true,
98
- ...state
99
- };
100
- }
101
- async waitReady(timeoutSeconds) {
102
- const state = await this.getState();
103
- if (!state) {
104
- throw new MctError({
105
- code: "SERVER_NOT_RUNNING",
106
- message: "Server is not running"
107
- }, 5);
108
- }
109
- return waitForTcpPort("127.0.0.1", state.port, timeoutSeconds);
110
- }
111
- async getState() {
112
- return this.context.state.readJson(SERVER_STATE_FILE, null);
113
- }
114
- }
@@ -1,31 +0,0 @@
1
- export declare const DEFAULT_WS_PORT_BASE = 25580;
2
- export interface MctConfig {
3
- server: {
4
- jar?: string;
5
- dir: string;
6
- port: number;
7
- jvmArgs: string[];
8
- };
9
- clients: Record<string, {
10
- version?: string;
11
- account?: string;
12
- wsPort?: number;
13
- server?: string;
14
- headless?: boolean;
15
- launchCommand?: string[];
16
- launchArgs?: string[];
17
- workingDir?: string;
18
- env?: Record<string, string>;
19
- }>;
20
- screenshot: {
21
- outputDir: string;
22
- };
23
- timeout: {
24
- serverReady: number;
25
- clientReady: number;
26
- default: number;
27
- };
28
- }
29
- export declare function resolveConfigPath(configPath: string | undefined, cwd: string): string;
30
- export declare function loadConfig(configPath: string | undefined, cwd: string): Promise<MctConfig>;
31
- export declare function writeConfig(configPath: string | undefined, cwd: string, config: MctConfig): Promise<void>;
@@ -1,59 +0,0 @@
1
- import { access, mkdir, readFile, writeFile } from "node:fs/promises";
2
- import path from "node:path";
3
- export const DEFAULT_WS_PORT_BASE = 25580;
4
- function createDefaultConfig() {
5
- return {
6
- server: {
7
- dir: "./server",
8
- port: 25565,
9
- jvmArgs: []
10
- },
11
- clients: {},
12
- screenshot: {
13
- outputDir: "./screenshots"
14
- },
15
- timeout: {
16
- serverReady: 120,
17
- clientReady: 60,
18
- default: 10
19
- }
20
- };
21
- }
22
- export function resolveConfigPath(configPath, cwd) {
23
- if (!configPath) {
24
- return path.join(cwd, "mct.config.json");
25
- }
26
- return path.isAbsolute(configPath) ? configPath : path.resolve(cwd, configPath);
27
- }
28
- export async function loadConfig(configPath, cwd) {
29
- const resolvedPath = resolveConfigPath(configPath, cwd);
30
- const defaultConfig = createDefaultConfig();
31
- try {
32
- await access(resolvedPath);
33
- }
34
- catch {
35
- return defaultConfig;
36
- }
37
- const raw = await readFile(resolvedPath, "utf8");
38
- const parsed = JSON.parse(raw);
39
- return {
40
- server: {
41
- ...defaultConfig.server,
42
- ...parsed.server
43
- },
44
- clients: parsed.clients ?? defaultConfig.clients,
45
- screenshot: {
46
- ...defaultConfig.screenshot,
47
- ...parsed.screenshot
48
- },
49
- timeout: {
50
- ...defaultConfig.timeout,
51
- ...parsed.timeout
52
- }
53
- };
54
- }
55
- export async function writeConfig(configPath, cwd, config) {
56
- const resolvedPath = resolveConfigPath(configPath, cwd);
57
- await mkdir(path.dirname(resolvedPath), { recursive: true });
58
- await writeFile(resolvedPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
59
- }