@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.
@@ -19,114 +19,107 @@ export class ClientInstanceManager {
19
19
  this.globalState = globalState;
20
20
  }
21
21
  async create(options) {
22
- const instanceDir = resolveClientInstanceDir(options.name);
23
- await mkdir(instanceDir, { recursive: true });
24
- const wsPort = options.wsPort ?? (await this.findAvailablePort());
25
- const meta = {
26
- name: options.name,
27
- loader: options.loader ?? "fabric",
28
- mcVersion: options.version,
29
- wsPort,
30
- account: options.account,
31
- headless: options.headless,
32
- launchArgs: options.launchArgs,
33
- env: options.env,
34
- createdAt: new Date().toISOString()
35
- };
36
- await writeFile(path.join(instanceDir, INSTANCE_FILE), `${JSON.stringify(meta, null, 2)}\n`, "utf8");
37
- return meta;
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
- let state = await this.globalState.readClientState();
41
- const existing = state.clients[clientName];
42
- if (existing && isProcessRunning(existing.pid)) {
43
- if (options.force) {
44
- await this.stop(clientName);
45
- state = await this.globalState.readClientState();
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
- else {
48
- throw new MctError({
49
- code: "CLIENT_ALREADY_RUNNING",
50
- message: `Client ${clientName} is already running. Pass --force to kill and relaunch.`,
51
- details: existing
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
- const meta = await this.loadMeta(clientName);
56
- const instanceDir = resolveClientInstanceDir(clientName);
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
- const launchCommand = [process.execPath, getLaunchScriptPath(), ...meta.launchArgs];
76
- const minecraftDir = path.join(instanceDir, "minecraft");
77
- const logsDir = path.join(resolveMctHome(), "logs");
78
- mkdirSync(logsDir, { recursive: true });
79
- const logPath = path.join(logsDir, `client-${clientName}.log`);
80
- const stdout = openSync(logPath, "a");
81
- const child = spawn(launchCommand[0], launchCommand.slice(1), {
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
- const state = await this.globalState.readClientState();
112
- const entry = state.clients[clientName];
113
- if (!entry) {
114
- return { stopped: false, alreadyStopped: true, name: clientName };
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
- delete state.clients[clientName];
125
- if (state.defaultClient === clientName) {
126
- state.defaultClient = Object.keys(state.clients)[0];
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 { waitForTcpPort } from "../util/net.js";
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 "$@" <"$MCT_STDIN_PIPE"',
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
- return waitForTcpPort("127.0.0.1", entry.port, timeoutSeconds);
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
+ }
@@ -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 <name>", "Project name (default: from mct.project.json)")
9
- .option("--profile <name>", "Profile name (default: from mct.project.json)")
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) {
@@ -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>;
@@ -1,6 +1,6 @@
1
1
  import process from "node:process";
2
2
  import { GlobalStateStore } from "./global-state.js";
3
- import { loadProjectFile, resolveProfile } from "./project.js";
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 projectFile = await loadProjectFile(cwd);
13
- const projectName = options.project ?? projectFile?.project ?? null;
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
  }
@@ -1,3 +1,4 @@
1
+ export declare function isTcpPortReachable(host: string, port: number): Promise<boolean>;
1
2
  export declare function waitForTcpPort(host: string, port: number, timeoutSeconds: number): Promise<{
2
3
  reachable: boolean;
3
4
  host: string;
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
- const reachable = await new Promise((resolve) => {
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,
@@ -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;
@@ -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
  }
@@ -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 declare const PROJECT_FILE_NAME = "mct.project.json";
20
- export declare function resolveProjectFilePath(cwd: string): string;
21
- export declare function loadProjectFile(cwd: string): Promise<MctProjectFile | null>;
22
- export declare function writeProjectFile(cwd: string, project: MctProjectFile): Promise<void>;
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;