@kzheart_/mc-pilot 0.8.1 → 0.9.1
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/data/variants.json +12 -12
- package/dist/commands/chat.js +3 -3
- package/dist/commands/client.d.ts +1 -1
- package/dist/commands/client.js +4 -4
- package/dist/commands/gui.js +8 -3
- package/dist/commands/plugin.js +3 -3
- package/dist/commands/project.js +22 -30
- package/dist/commands/request-helpers.d.ts +2 -0
- package/dist/commands/request-helpers.js +18 -0
- package/dist/commands/screenshot.js +10 -7
- package/dist/commands/server.js +7 -7
- package/dist/download/VersionMatrix.js +14 -14
- package/dist/download/client/ClientDownloader.js +1 -1
- package/dist/index.js +3 -0
- package/dist/instance/ClientInstanceManager.d.ts +1 -0
- package/dist/instance/ClientInstanceManager.js +105 -98
- package/dist/instance/ServerInstanceManager.d.ts +5 -0
- package/dist/instance/ServerInstanceManager.js +85 -3
- package/dist/util/command-log.d.ts +12 -0
- package/dist/util/command-log.js +13 -0
- package/dist/util/command.js +2 -2
- package/dist/util/context.d.ts +3 -0
- package/dist/util/context.js +10 -3
- package/dist/util/global-state.d.ts +4 -0
- package/dist/util/global-state.js +22 -0
- package/dist/util/net.d.ts +1 -0
- package/dist/util/net.js +14 -12
- package/dist/util/paths.d.ts +2 -0
- package/dist/util/paths.js +6 -0
- package/dist/util/project.d.ts +16 -4
- package/dist/util/project.js +63 -8
- package/dist/util/state.d.ts +5 -0
- package/dist/util/state.js +81 -2
- package/package.json +1 -1
- package/scripts/launch-fabric-client.mjs +13 -5
|
@@ -19,114 +19,107 @@ export class ClientInstanceManager {
|
|
|
19
19
|
this.globalState = globalState;
|
|
20
20
|
}
|
|
21
21
|
async create(options) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
22
|
+
return this.globalState.withClientLock(async () => {
|
|
23
|
+
const instanceDir = resolveClientInstanceDir(options.name);
|
|
24
|
+
await mkdir(instanceDir, { recursive: true });
|
|
25
|
+
const wsPort = options.wsPort ?? (await this.findAvailablePort());
|
|
26
|
+
const meta = {
|
|
27
|
+
name: options.name,
|
|
28
|
+
loader: options.loader ?? "fabric",
|
|
29
|
+
mcVersion: options.version,
|
|
30
|
+
wsPort,
|
|
31
|
+
account: options.account,
|
|
32
|
+
headless: options.headless,
|
|
33
|
+
launchArgs: options.launchArgs,
|
|
34
|
+
env: options.env,
|
|
35
|
+
createdAt: new Date().toISOString()
|
|
36
|
+
};
|
|
37
|
+
await writeFile(path.join(instanceDir, INSTANCE_FILE), `${JSON.stringify(meta, null, 2)}\n`, "utf8");
|
|
38
|
+
return meta;
|
|
39
|
+
});
|
|
38
40
|
}
|
|
39
41
|
async launch(clientName, options = {}) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
return this.globalState.withClientLock(async () => {
|
|
43
|
+
const state = await this.globalState.readClientState();
|
|
44
|
+
const existing = state.clients[clientName];
|
|
45
|
+
if (existing && isProcessRunning(existing.pid)) {
|
|
46
|
+
if (options.force) {
|
|
47
|
+
await this.stopTrackedClient(state, clientName, existing);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
throw new MctError({
|
|
51
|
+
code: "CLIENT_ALREADY_RUNNING",
|
|
52
|
+
message: `Client ${clientName} is already running. Pass --force to kill and relaunch.`,
|
|
53
|
+
details: existing
|
|
54
|
+
}, 3);
|
|
55
|
+
}
|
|
46
56
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}, 3);
|
|
57
|
+
const meta = await this.loadMeta(clientName);
|
|
58
|
+
const instanceDir = resolveClientInstanceDir(clientName);
|
|
59
|
+
const wsPort = options.wsPort ?? meta.wsPort;
|
|
60
|
+
if (!meta.launchArgs || meta.launchArgs.length === 0) {
|
|
61
|
+
throw new MctError({ code: "INVALID_PARAMS", message: `Client ${clientName} has no launchArgs configured` }, 4);
|
|
53
62
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const wsPort = options.wsPort ?? meta.wsPort;
|
|
58
|
-
if (!meta.launchArgs || meta.launchArgs.length === 0) {
|
|
59
|
-
throw new MctError({ code: "INVALID_PARAMS", message: `Client ${clientName} has no launchArgs configured` }, 4);
|
|
60
|
-
}
|
|
61
|
-
// Kill any existing processes on the port
|
|
62
|
-
const listeningPids = getListeningPids(wsPort);
|
|
63
|
-
for (const pid of listeningPids) {
|
|
64
|
-
killProcessTree(pid);
|
|
65
|
-
}
|
|
66
|
-
if (listeningPids.length > 0) {
|
|
67
|
-
const deadline = Date.now() + 10_000;
|
|
68
|
-
while (Date.now() < deadline) {
|
|
69
|
-
if (getListeningPids(wsPort).length === 0) {
|
|
70
|
-
break;
|
|
71
|
-
}
|
|
72
|
-
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
63
|
+
const listeningPids = getListeningPids(wsPort);
|
|
64
|
+
for (const pid of listeningPids) {
|
|
65
|
+
killProcessTree(pid);
|
|
73
66
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
cwd: minecraftDir,
|
|
83
|
-
detached: true,
|
|
84
|
-
stdio: ["ignore", stdout, stdout],
|
|
85
|
-
env: {
|
|
86
|
-
...process.env,
|
|
87
|
-
...meta.env,
|
|
88
|
-
MCT_CLIENT_NAME: clientName,
|
|
89
|
-
MCT_CLIENT_VERSION: meta.mcVersion,
|
|
90
|
-
MCT_CLIENT_ACCOUNT: options.account ?? meta.account ?? "",
|
|
91
|
-
MCT_CLIENT_SERVER: options.server ?? "",
|
|
92
|
-
MCT_CLIENT_WS_PORT: String(wsPort),
|
|
93
|
-
MCT_CLIENT_HEADLESS: String(options.headless ?? meta.headless ?? false)
|
|
67
|
+
if (listeningPids.length > 0) {
|
|
68
|
+
const deadline = Date.now() + 10_000;
|
|
69
|
+
while (Date.now() < deadline) {
|
|
70
|
+
if (getListeningPids(wsPort).length === 0) {
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
74
|
+
}
|
|
94
75
|
}
|
|
76
|
+
const launchCommand = [process.execPath, getLaunchScriptPath(), ...meta.launchArgs];
|
|
77
|
+
const minecraftDir = path.join(instanceDir, "minecraft");
|
|
78
|
+
const logsDir = path.join(resolveMctHome(), "logs");
|
|
79
|
+
mkdirSync(logsDir, { recursive: true });
|
|
80
|
+
const logPath = path.join(logsDir, `client-${clientName}.log`);
|
|
81
|
+
const stdout = openSync(logPath, "a");
|
|
82
|
+
const child = spawn(launchCommand[0], launchCommand.slice(1), {
|
|
83
|
+
cwd: minecraftDir,
|
|
84
|
+
detached: true,
|
|
85
|
+
stdio: ["ignore", stdout, stdout],
|
|
86
|
+
env: {
|
|
87
|
+
...process.env,
|
|
88
|
+
...meta.env,
|
|
89
|
+
MCT_CLIENT_NAME: clientName,
|
|
90
|
+
MCT_CLIENT_VERSION: meta.mcVersion,
|
|
91
|
+
MCT_CLIENT_ACCOUNT: options.account ?? meta.account ?? "",
|
|
92
|
+
MCT_CLIENT_SERVER: options.server ?? "",
|
|
93
|
+
MCT_CLIENT_WS_PORT: String(wsPort),
|
|
94
|
+
MCT_CLIENT_HEADLESS: String(options.headless ?? meta.headless ?? false)
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
child.unref();
|
|
98
|
+
const entry = {
|
|
99
|
+
pid: child.pid ?? 0,
|
|
100
|
+
name: clientName,
|
|
101
|
+
wsPort,
|
|
102
|
+
startedAt: new Date().toISOString(),
|
|
103
|
+
logPath,
|
|
104
|
+
instanceDir
|
|
105
|
+
};
|
|
106
|
+
state.defaultClient ??= clientName;
|
|
107
|
+
state.clients[clientName] = entry;
|
|
108
|
+
await this.globalState.writeClientState(state);
|
|
109
|
+
return entry;
|
|
95
110
|
});
|
|
96
|
-
child.unref();
|
|
97
|
-
const entry = {
|
|
98
|
-
pid: child.pid ?? 0,
|
|
99
|
-
name: clientName,
|
|
100
|
-
wsPort,
|
|
101
|
-
startedAt: new Date().toISOString(),
|
|
102
|
-
logPath,
|
|
103
|
-
instanceDir
|
|
104
|
-
};
|
|
105
|
-
state.defaultClient ??= clientName;
|
|
106
|
-
state.clients[clientName] = entry;
|
|
107
|
-
await this.globalState.writeClientState(state);
|
|
108
|
-
return entry;
|
|
109
111
|
}
|
|
110
112
|
async stop(clientName) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (isProcessRunning(entry.pid)) {
|
|
117
|
-
killProcessTree(entry.pid);
|
|
118
|
-
}
|
|
119
|
-
for (const pid of getListeningPids(entry.wsPort)) {
|
|
120
|
-
if (pid !== entry.pid) {
|
|
121
|
-
killProcessTree(pid);
|
|
113
|
+
return this.globalState.withClientLock(async () => {
|
|
114
|
+
const state = await this.globalState.readClientState();
|
|
115
|
+
const entry = state.clients[clientName];
|
|
116
|
+
if (!entry) {
|
|
117
|
+
return { stopped: false, alreadyStopped: true, name: clientName };
|
|
122
118
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
await this.globalState.writeClientState(state);
|
|
129
|
-
return { stopped: true, name: clientName, pid: entry.pid };
|
|
119
|
+
await this.stopTrackedClient(state, clientName, entry);
|
|
120
|
+
await this.globalState.writeClientState(state);
|
|
121
|
+
return { stopped: true, name: clientName, pid: entry.pid };
|
|
122
|
+
});
|
|
130
123
|
}
|
|
131
124
|
async isAlreadyRunning(clientName) {
|
|
132
125
|
const state = await this.globalState.readClientState();
|
|
@@ -340,4 +333,18 @@ export class ClientInstanceManager {
|
|
|
340
333
|
}
|
|
341
334
|
return port;
|
|
342
335
|
}
|
|
336
|
+
async stopTrackedClient(state, clientName, entry) {
|
|
337
|
+
if (isProcessRunning(entry.pid)) {
|
|
338
|
+
killProcessTree(entry.pid);
|
|
339
|
+
}
|
|
340
|
+
for (const pid of getListeningPids(entry.wsPort)) {
|
|
341
|
+
if (pid !== entry.pid) {
|
|
342
|
+
killProcessTree(pid);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
delete state.clients[clientName];
|
|
346
|
+
if (state.defaultClient === clientName) {
|
|
347
|
+
state.defaultClient = Object.keys(state.clients)[0];
|
|
348
|
+
}
|
|
349
|
+
}
|
|
343
350
|
}
|
|
@@ -70,6 +70,10 @@ export declare class ServerInstanceManager {
|
|
|
70
70
|
reachable: boolean;
|
|
71
71
|
host: string;
|
|
72
72
|
port: number;
|
|
73
|
+
phase: string;
|
|
74
|
+
logPath: string;
|
|
75
|
+
lastLine: string | null;
|
|
76
|
+
recentLines: string[];
|
|
73
77
|
}>;
|
|
74
78
|
exec(serverName: string, command: string): Promise<{
|
|
75
79
|
sent: boolean;
|
|
@@ -99,6 +103,7 @@ export declare class ServerInstanceManager {
|
|
|
99
103
|
timedOut: boolean;
|
|
100
104
|
}>;
|
|
101
105
|
private requireRuntimeEntry;
|
|
106
|
+
private describeStartup;
|
|
102
107
|
private requireRunning;
|
|
103
108
|
list(): Promise<ServerInstanceMeta[]>;
|
|
104
109
|
static listAll(globalState: GlobalStateStore): Promise<ServerInstanceMeta[]>;
|
|
@@ -4,11 +4,12 @@ import { spawn, execSync } from "node:child_process";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { resolveMctHome, resolveProjectDir, resolveServerInstanceDir } from "../util/paths.js";
|
|
6
6
|
import { MctError } from "../util/errors.js";
|
|
7
|
-
import {
|
|
7
|
+
import { isTcpPortReachable } 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
11
|
const ANSI_ESCAPE_PATTERN = /\u001b\[[0-?]*[ -/]*[@-~]/g;
|
|
12
|
+
const SERVER_READY_POLL_MS = 500;
|
|
12
13
|
export function stripAnsiCodes(text) {
|
|
13
14
|
return text.replace(ANSI_ESCAPE_PATTERN, "");
|
|
14
15
|
}
|
|
@@ -108,7 +109,7 @@ export class ServerInstanceManager {
|
|
|
108
109
|
// then exec java with stdin reading from the FIFO
|
|
109
110
|
const child = spawn("bash", [
|
|
110
111
|
"-c",
|
|
111
|
-
'exec 3<>"$MCT_STDIN_PIPE"; exec java "$@"
|
|
112
|
+
'exec 3<>"$MCT_STDIN_PIPE"; exec java "$@" 0<&3',
|
|
112
113
|
"mct-server",
|
|
113
114
|
...jvmArgs, "-jar", jarFile, "nogui"
|
|
114
115
|
], {
|
|
@@ -206,7 +207,51 @@ export class ServerInstanceManager {
|
|
|
206
207
|
if (!entry) {
|
|
207
208
|
throw new MctError({ code: "SERVER_NOT_RUNNING", message: `Server ${stateKey} is not running` }, 5);
|
|
208
209
|
}
|
|
209
|
-
|
|
210
|
+
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
211
|
+
let snapshot = await this.describeStartup(entry.logPath);
|
|
212
|
+
while (Date.now() < deadline) {
|
|
213
|
+
if (!isProcessRunning(entry.pid)) {
|
|
214
|
+
throw new MctError({
|
|
215
|
+
code: "SERVER_EXITED",
|
|
216
|
+
message: `Server ${stateKey} exited before becoming ready (${snapshot.phase})`,
|
|
217
|
+
details: {
|
|
218
|
+
pid: entry.pid,
|
|
219
|
+
host: "127.0.0.1",
|
|
220
|
+
port: entry.port,
|
|
221
|
+
phase: snapshot.phase,
|
|
222
|
+
logPath: snapshot.logPath,
|
|
223
|
+
lastLine: snapshot.lastLine,
|
|
224
|
+
recentLines: snapshot.recentLines
|
|
225
|
+
}
|
|
226
|
+
}, 5);
|
|
227
|
+
}
|
|
228
|
+
if (await isTcpPortReachable("127.0.0.1", entry.port)) {
|
|
229
|
+
snapshot = await this.describeStartup(entry.logPath);
|
|
230
|
+
return {
|
|
231
|
+
reachable: true,
|
|
232
|
+
host: "127.0.0.1",
|
|
233
|
+
port: entry.port,
|
|
234
|
+
phase: snapshot.phase,
|
|
235
|
+
logPath: snapshot.logPath,
|
|
236
|
+
lastLine: snapshot.lastLine,
|
|
237
|
+
recentLines: snapshot.recentLines
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
snapshot = await this.describeStartup(entry.logPath);
|
|
241
|
+
await new Promise((resolve) => setTimeout(resolve, SERVER_READY_POLL_MS));
|
|
242
|
+
}
|
|
243
|
+
throw new MctError({
|
|
244
|
+
code: "TIMEOUT",
|
|
245
|
+
message: `Timed out waiting for 127.0.0.1:${entry.port} (${snapshot.phase})`,
|
|
246
|
+
details: {
|
|
247
|
+
host: "127.0.0.1",
|
|
248
|
+
port: entry.port,
|
|
249
|
+
phase: snapshot.phase,
|
|
250
|
+
logPath: snapshot.logPath,
|
|
251
|
+
lastLine: snapshot.lastLine,
|
|
252
|
+
recentLines: snapshot.recentLines
|
|
253
|
+
}
|
|
254
|
+
}, 2);
|
|
210
255
|
}
|
|
211
256
|
async exec(serverName, command) {
|
|
212
257
|
const entry = await this.requireRunning(serverName);
|
|
@@ -338,6 +383,21 @@ export class ServerInstanceManager {
|
|
|
338
383
|
}
|
|
339
384
|
return entry;
|
|
340
385
|
}
|
|
386
|
+
async describeStartup(logPath) {
|
|
387
|
+
const raw = await readFile(logPath, "utf8").catch(() => "");
|
|
388
|
+
const recentLines = raw
|
|
389
|
+
.split(/\r?\n/)
|
|
390
|
+
.map((line) => stripAnsiCodes(line).trim())
|
|
391
|
+
.filter((line) => line.length > 0)
|
|
392
|
+
.slice(-10);
|
|
393
|
+
const lastLine = recentLines[recentLines.length - 1] ?? null;
|
|
394
|
+
return {
|
|
395
|
+
phase: detectServerStartupPhase(recentLines),
|
|
396
|
+
logPath,
|
|
397
|
+
recentLines,
|
|
398
|
+
lastLine
|
|
399
|
+
};
|
|
400
|
+
}
|
|
341
401
|
async requireRunning(serverName) {
|
|
342
402
|
const entry = await this.requireRuntimeEntry(serverName);
|
|
343
403
|
if (!isProcessRunning(entry.pid)) {
|
|
@@ -430,3 +490,25 @@ export class ServerInstanceManager {
|
|
|
430
490
|
return port;
|
|
431
491
|
}
|
|
432
492
|
}
|
|
493
|
+
function detectServerStartupPhase(lines) {
|
|
494
|
+
const joined = lines.join("\n");
|
|
495
|
+
if (/Done \(.+\)! For help, type "help"/.test(joined)) {
|
|
496
|
+
return "ready";
|
|
497
|
+
}
|
|
498
|
+
if (/Preparing start region|Preparing level/.test(joined)) {
|
|
499
|
+
return "initializing-world";
|
|
500
|
+
}
|
|
501
|
+
if (/Starting Minecraft server on/.test(joined)) {
|
|
502
|
+
return "binding-port";
|
|
503
|
+
}
|
|
504
|
+
if (/Loading libraries, please wait|Starting org\.bukkit\.craftbukkit\.Main|Starting minecraft server version/.test(joined)) {
|
|
505
|
+
return "bootstrapping";
|
|
506
|
+
}
|
|
507
|
+
if (/Downloading |Applying patches/.test(joined)) {
|
|
508
|
+
return "downloading";
|
|
509
|
+
}
|
|
510
|
+
if (lines.length > 0) {
|
|
511
|
+
return "starting";
|
|
512
|
+
}
|
|
513
|
+
return "waiting-for-log";
|
|
514
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface CommandLogEntry {
|
|
2
|
+
t: number;
|
|
3
|
+
iso: string;
|
|
4
|
+
argv: string[];
|
|
5
|
+
cwd: string;
|
|
6
|
+
projectId: string | null;
|
|
7
|
+
exitCode: number;
|
|
8
|
+
durationMs: number;
|
|
9
|
+
errorCode?: string;
|
|
10
|
+
errorMessage?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function appendCommandHistory(entry: CommandLogEntry): Promise<void>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { appendFile, mkdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { resolveCommandsLogPath } from "./paths.js";
|
|
4
|
+
export async function appendCommandHistory(entry) {
|
|
5
|
+
try {
|
|
6
|
+
const filePath = resolveCommandsLogPath();
|
|
7
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
8
|
+
await appendFile(filePath, `${JSON.stringify(entry)}\n`, "utf8");
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
// Never block the command on logging failure
|
|
12
|
+
}
|
|
13
|
+
}
|
package/dist/util/command.js
CHANGED
|
@@ -5,8 +5,8 @@ import { printError, printSuccess } from "./output.js";
|
|
|
5
5
|
export function attachGlobalOptions(command) {
|
|
6
6
|
return command
|
|
7
7
|
.option("--human", "Human-readable output (default: JSON)")
|
|
8
|
-
.option("--project <
|
|
9
|
-
.option("--profile <name>", "Profile name (default: from mct
|
|
8
|
+
.option("--project <id>", "Project ID (default: derived from cwd and loaded from ~/.mct/projects/<id>/project.json)")
|
|
9
|
+
.option("--profile <name>", "Profile name (default: from ~/.mct/projects/<id>/project.json)")
|
|
10
10
|
.option("--client <name>", "Target client name (required when multiple clients are running)");
|
|
11
11
|
}
|
|
12
12
|
export function wrapCommand(action) {
|
package/dist/util/context.d.ts
CHANGED
|
@@ -13,7 +13,10 @@ export interface CommandContext {
|
|
|
13
13
|
globalState: GlobalStateStore;
|
|
14
14
|
projectFile: MctProjectFile | null;
|
|
15
15
|
activeProfile: MctProfile | null;
|
|
16
|
+
projectId: string | null;
|
|
16
17
|
projectName: string | null;
|
|
18
|
+
projectRootDir: string | null;
|
|
19
|
+
projectConfigPath: string | null;
|
|
17
20
|
timeout(key: "serverReady" | "clientReady" | "default"): number;
|
|
18
21
|
}
|
|
19
22
|
export declare function createCommandContext(options: GlobalOptions): Promise<CommandContext>;
|
package/dist/util/context.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
2
|
import { GlobalStateStore } from "./global-state.js";
|
|
3
|
-
import {
|
|
3
|
+
import { loadProjectFileForCwd, loadProjectFileForId, resolveProfile } from "./project.js";
|
|
4
4
|
const TIMEOUT_DEFAULTS = {
|
|
5
5
|
serverReady: 120,
|
|
6
6
|
clientReady: 60,
|
|
@@ -9,8 +9,12 @@ const TIMEOUT_DEFAULTS = {
|
|
|
9
9
|
export async function createCommandContext(options) {
|
|
10
10
|
const cwd = process.cwd();
|
|
11
11
|
const globalState = new GlobalStateStore();
|
|
12
|
-
const
|
|
13
|
-
|
|
12
|
+
const resolvedProject = options.project
|
|
13
|
+
? await loadProjectFileForId(options.project)
|
|
14
|
+
: await loadProjectFileForCwd(cwd);
|
|
15
|
+
const projectFile = resolvedProject?.projectFile ?? null;
|
|
16
|
+
const projectId = options.project ?? resolvedProject?.projectId ?? null;
|
|
17
|
+
const projectName = projectFile?.project ?? null;
|
|
14
18
|
const activeProfile = projectFile
|
|
15
19
|
? resolveProfile(projectFile, options.profile)
|
|
16
20
|
: null;
|
|
@@ -20,7 +24,10 @@ export async function createCommandContext(options) {
|
|
|
20
24
|
globalState,
|
|
21
25
|
projectFile,
|
|
22
26
|
activeProfile,
|
|
27
|
+
projectId,
|
|
23
28
|
projectName,
|
|
29
|
+
projectRootDir: projectFile?.rootDir ?? null,
|
|
30
|
+
projectConfigPath: resolvedProject?.filePath ?? null,
|
|
24
31
|
timeout(key) {
|
|
25
32
|
return projectFile?.timeout?.[key] ?? TIMEOUT_DEFAULTS[key];
|
|
26
33
|
}
|
|
@@ -2,8 +2,12 @@ import { StateStore } from "./state.js";
|
|
|
2
2
|
import type { GlobalClientState, GlobalServerState } from "./instance-types.js";
|
|
3
3
|
export declare class GlobalStateStore extends StateStore {
|
|
4
4
|
constructor();
|
|
5
|
+
withClientLock<T>(task: () => Promise<T>): Promise<T>;
|
|
6
|
+
withServerLock<T>(task: () => Promise<T>): Promise<T>;
|
|
5
7
|
readServerState(): Promise<GlobalServerState>;
|
|
6
8
|
writeServerState(state: GlobalServerState): Promise<void>;
|
|
7
9
|
readClientState(): Promise<GlobalClientState>;
|
|
8
10
|
writeClientState(state: GlobalClientState): Promise<void>;
|
|
11
|
+
updateClientState<T>(mutate: (state: GlobalClientState) => Promise<T> | T): Promise<T>;
|
|
12
|
+
updateServerState<T>(mutate: (state: GlobalServerState) => Promise<T> | T): Promise<T>;
|
|
9
13
|
}
|
|
@@ -6,6 +6,12 @@ export class GlobalStateStore extends StateStore {
|
|
|
6
6
|
constructor() {
|
|
7
7
|
super(resolveGlobalStateDir());
|
|
8
8
|
}
|
|
9
|
+
async withClientLock(task) {
|
|
10
|
+
return this.withLock("clients", task);
|
|
11
|
+
}
|
|
12
|
+
async withServerLock(task) {
|
|
13
|
+
return this.withLock("servers", task);
|
|
14
|
+
}
|
|
9
15
|
async readServerState() {
|
|
10
16
|
return this.readJson(SERVERS_STATE_FILE, { servers: {} });
|
|
11
17
|
}
|
|
@@ -18,4 +24,20 @@ export class GlobalStateStore extends StateStore {
|
|
|
18
24
|
async writeClientState(state) {
|
|
19
25
|
await this.writeJson(CLIENTS_STATE_FILE, state);
|
|
20
26
|
}
|
|
27
|
+
async updateClientState(mutate) {
|
|
28
|
+
return this.withClientLock(async () => {
|
|
29
|
+
const state = await this.readClientState();
|
|
30
|
+
const result = await mutate(state);
|
|
31
|
+
await this.writeClientState(state);
|
|
32
|
+
return result;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
async updateServerState(mutate) {
|
|
36
|
+
return this.withServerLock(async () => {
|
|
37
|
+
const state = await this.readServerState();
|
|
38
|
+
const result = await mutate(state);
|
|
39
|
+
await this.writeServerState(state);
|
|
40
|
+
return result;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
21
43
|
}
|
package/dist/util/net.d.ts
CHANGED
package/dist/util/net.js
CHANGED
|
@@ -5,21 +5,23 @@ function wait(ms) {
|
|
|
5
5
|
setTimeout(resolve, ms);
|
|
6
6
|
});
|
|
7
7
|
}
|
|
8
|
+
export async function isTcpPortReachable(host, port) {
|
|
9
|
+
return await new Promise((resolve) => {
|
|
10
|
+
const socket = net.createConnection({ host, port });
|
|
11
|
+
socket.once("connect", () => {
|
|
12
|
+
socket.destroy();
|
|
13
|
+
resolve(true);
|
|
14
|
+
});
|
|
15
|
+
socket.once("error", () => {
|
|
16
|
+
socket.destroy();
|
|
17
|
+
resolve(false);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
}
|
|
8
21
|
export async function waitForTcpPort(host, port, timeoutSeconds) {
|
|
9
22
|
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
10
23
|
while (Date.now() < deadline) {
|
|
11
|
-
|
|
12
|
-
const socket = net.createConnection({ host, port });
|
|
13
|
-
socket.once("connect", () => {
|
|
14
|
-
socket.destroy();
|
|
15
|
-
resolve(true);
|
|
16
|
-
});
|
|
17
|
-
socket.once("error", () => {
|
|
18
|
-
socket.destroy();
|
|
19
|
-
resolve(false);
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
if (reachable) {
|
|
24
|
+
if (await isTcpPortReachable(host, port)) {
|
|
23
25
|
return {
|
|
24
26
|
reachable: true,
|
|
25
27
|
host,
|
package/dist/util/paths.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export declare function resolveClientsDir(): string;
|
|
|
3
3
|
export declare function resolveClientInstanceDir(name: string): string;
|
|
4
4
|
export declare function resolveProjectsDir(): string;
|
|
5
5
|
export declare function resolveProjectDir(project: string): string;
|
|
6
|
+
export declare function resolveProjectConfigPath(project: string): string;
|
|
7
|
+
export declare function resolveProjectScreenshotsDir(project: string): string;
|
|
6
8
|
export declare function resolveServerInstanceDir(project: string, server: string): string;
|
|
7
9
|
export declare function resolveGlobalStateDir(): string;
|
|
8
10
|
export declare function resolvePluginsDir(): string;
|
package/dist/util/paths.js
CHANGED
|
@@ -15,6 +15,12 @@ export function resolveProjectsDir() {
|
|
|
15
15
|
export function resolveProjectDir(project) {
|
|
16
16
|
return path.join(resolveProjectsDir(), project);
|
|
17
17
|
}
|
|
18
|
+
export function resolveProjectConfigPath(project) {
|
|
19
|
+
return path.join(resolveProjectDir(project), "project.json");
|
|
20
|
+
}
|
|
21
|
+
export function resolveProjectScreenshotsDir(project) {
|
|
22
|
+
return path.join(resolveProjectDir(project), "screenshots");
|
|
23
|
+
}
|
|
18
24
|
export function resolveServerInstanceDir(project, server) {
|
|
19
25
|
return path.join(resolveProjectDir(project), server);
|
|
20
26
|
}
|
package/dist/util/project.d.ts
CHANGED
|
@@ -4,7 +4,9 @@ export interface MctProfile {
|
|
|
4
4
|
deployPlugins?: string[];
|
|
5
5
|
}
|
|
6
6
|
export interface MctProjectFile {
|
|
7
|
+
projectId: string;
|
|
7
8
|
project: string;
|
|
9
|
+
rootDir: string;
|
|
8
10
|
profiles: Record<string, MctProfile>;
|
|
9
11
|
defaultProfile?: string;
|
|
10
12
|
screenshot?: {
|
|
@@ -16,8 +18,18 @@ export interface MctProjectFile {
|
|
|
16
18
|
default?: number;
|
|
17
19
|
};
|
|
18
20
|
}
|
|
19
|
-
export
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
export interface ResolvedProjectConfig {
|
|
22
|
+
projectId: string;
|
|
23
|
+
filePath: string;
|
|
24
|
+
projectFile: MctProjectFile;
|
|
25
|
+
}
|
|
26
|
+
export declare const PROJECT_FILE_NAME = "project.json";
|
|
27
|
+
export declare function normalizeProjectRoot(cwd: string): string;
|
|
28
|
+
export declare function slugifyProjectId(cwd: string): string;
|
|
29
|
+
export declare function resolveProjectFilePath(projectId: string): string;
|
|
30
|
+
export declare function loadProjectFileById(projectId: string): Promise<MctProjectFile | null>;
|
|
31
|
+
export declare function loadProjectFileForCwd(cwd: string): Promise<ResolvedProjectConfig | null>;
|
|
32
|
+
export declare function loadProjectFileForId(projectId: string): Promise<ResolvedProjectConfig | null>;
|
|
33
|
+
export declare function writeProjectFile(projectId: string, project: MctProjectFile): Promise<void>;
|
|
34
|
+
export declare function createDefaultProjectFile(cwd: string, projectName: string): MctProjectFile;
|
|
23
35
|
export declare function resolveProfile(projectFile: MctProjectFile, profileName?: string): MctProfile | null;
|