@kzheart_/mc-pilot 0.8.0 → 0.9.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.
@@ -11,7 +11,7 @@ function normalizeChatCommand(text) {
11
11
  return command;
12
12
  }
13
13
  async function executeServerCommand(context, serverName, command) {
14
- const manager = new ServerInstanceManager(context.globalState, context.projectName);
14
+ const manager = new ServerInstanceManager(context.globalState, context.projectId);
15
15
  const result = await manager.exec(serverName, command);
16
16
  return {
17
17
  ...result,
@@ -50,7 +50,7 @@ export function createChatCommand() {
50
50
  if (preferredClient) {
51
51
  return sendClientRequest(context, preferredClient, "chat.command", { command: commandText });
52
52
  }
53
- if (!context.projectName) {
53
+ if (!context.projectId) {
54
54
  return sendClientRequest(context, undefined, "chat.command", { command: commandText });
55
55
  }
56
56
  const serverName = options.server ?? context.activeProfile?.server;
@@ -65,7 +65,7 @@ export function createChatCommand() {
65
65
  if (via !== "server") {
66
66
  throw new MctError({ code: "INVALID_PARAMS", message: `--via must be \"auto\", \"server\" or \"client\", got: ${via}` }, 4);
67
67
  }
68
- if (!context.projectName) {
68
+ if (!context.projectId) {
69
69
  throw new MctError({ code: "NO_PROJECT", message: "--via server requires a project context. Use --via client or run inside an mct project." }, 4);
70
70
  }
71
71
  const serverName = options.server ?? context.activeProfile?.server;
@@ -1,4 +1,4 @@
1
1
  import { Command } from "commander";
2
2
  import type { CommandContext } from "../util/context.js";
3
- export declare function resolveProfileServerAddress(context: Pick<CommandContext, "projectName" | "activeProfile" | "globalState">, explicitServer: string | undefined, loadPort?: (projectName: string, serverName: string) => Promise<number>): Promise<string | undefined>;
3
+ export declare function resolveProfileServerAddress(context: Pick<CommandContext, "projectId" | "activeProfile" | "globalState">, explicitServer: string | undefined, loadPort?: (projectId: string, serverName: string) => Promise<number>): Promise<string | undefined>;
4
4
  export declare function createClientCommand(): Command;
@@ -17,13 +17,13 @@ export async function resolveProfileServerAddress(context, explicitServer, loadP
17
17
  if (explicitServer) {
18
18
  return explicitServer;
19
19
  }
20
- if (!context.projectName || !context.activeProfile?.server) {
20
+ if (!context.projectId || !context.activeProfile?.server) {
21
21
  return undefined;
22
22
  }
23
23
  try {
24
24
  const port = loadPort
25
- ? await loadPort(context.projectName, context.activeProfile.server)
26
- : (await new ServerInstanceManager(context.globalState, context.projectName).loadMeta(context.activeProfile.server)).port;
25
+ ? await loadPort(context.projectId, context.activeProfile.server)
26
+ : (await new ServerInstanceManager(context.globalState, context.projectId).loadMeta(context.activeProfile.server)).port;
27
27
  return `127.0.0.1:${port}`;
28
28
  }
29
29
  catch {
@@ -1,5 +1,6 @@
1
1
  import { Command } from "commander";
2
- import { createRequestAction, parseNumberList, withTransportTimeoutBuffer } from "./request-helpers.js";
2
+ import { wrapCommand } from "../util/command.js";
3
+ import { createRequestAction, parseNumberList, resolveScreenshotOutputPath, sendClientRequest, withTransportTimeoutBuffer } from "./request-helpers.js";
3
4
  export function createGuiCommand() {
4
5
  const command = new Command("gui").description("GUI / container interaction (use \"gui snapshot\" to inspect slot indices and contents)");
5
6
  command.command("info").description("Get current GUI info (title, type, slot count)").action(createRequestAction("gui.info", () => ({})));
@@ -43,7 +44,11 @@ export function createGuiCommand() {
43
44
  command
44
45
  .command("screenshot")
45
46
  .description("Take a screenshot of the current GUI")
46
- .requiredOption("--output <path>", "Output file path")
47
- .action(createRequestAction("gui.screenshot", ({ options }) => ({ output: options.output })));
47
+ .option("--output <path>", "Output file path (default: project screenshot directory)")
48
+ .action(wrapCommand(async (context, { options, globalOptions }) => {
49
+ return sendClientRequest(context, globalOptions.client ?? context.activeProfile?.clients[0], "gui.screenshot", {
50
+ output: resolveScreenshotOutputPath(context, options.output, "gui")
51
+ });
52
+ }));
48
53
  return command;
49
54
  }
@@ -69,11 +69,11 @@ export function createPluginCommand() {
69
69
  .description("Install a plugin (with dependencies) to a server")
70
70
  .argument("<id>", "Plugin ID")
71
71
  .requiredOption("--server <name>", "Target server instance name")
72
- .option("--project <name>", "Project name")
72
+ .option("--project <id>", "Project ID")
73
73
  .action(wrapCommand(async (context, { args, options }) => {
74
- const project = options.project ?? context.projectName;
74
+ const project = options.project ?? context.projectId;
75
75
  if (!project) {
76
- throw new MctError({ code: "NO_PROJECT", message: "No project specified. Use --project or run from a project directory." }, 4);
76
+ throw new MctError({ code: "NO_PROJECT", message: "No project specified. Use --project <id> or run from a project directory." }, 4);
77
77
  }
78
78
  const manager = new PluginCatalogManager();
79
79
  return manager.install(args[0], project, options.server);
@@ -4,34 +4,26 @@ import { ServerInstanceManager } from "../instance/ServerInstanceManager.js";
4
4
  import { ClientInstanceManager } from "../instance/ClientInstanceManager.js";
5
5
  import { MctError } from "../util/errors.js";
6
6
  import { wrapCommand } from "../util/command.js";
7
- import { loadProjectFile, resolveProfile, writeProjectFile } from "../util/project.js";
7
+ import { createDefaultProjectFile, loadProjectFileForCwd, resolveProjectFilePath, resolveProfile, writeProjectFile, } from "../util/project.js";
8
8
  export function createInitCommand() {
9
9
  return new Command("init")
10
- .description("Initialize a new MC Pilot project in the current directory")
10
+ .description("Initialize a new MC Pilot project for the current directory")
11
11
  .option("--name <name>", "Project name (default: directory name)")
12
12
  .action(wrapCommand(async (context, { options }) => {
13
- const existing = await loadProjectFile(context.cwd);
13
+ const existing = await loadProjectFileForCwd(context.cwd);
14
14
  if (existing) {
15
- throw new MctError({ code: "PROJECT_EXISTS", message: "mct.project.json already exists in this directory" }, 4);
15
+ throw new MctError({ code: "PROJECT_EXISTS", message: `Project config already exists for this directory: ${existing.filePath}` }, 4);
16
16
  }
17
17
  const projectName = options.name ?? path.basename(context.cwd);
18
- const project = {
19
- project: projectName,
20
- profiles: {},
21
- screenshot: {
22
- outputDir: "./screenshots"
23
- },
24
- timeout: {
25
- serverReady: 120,
26
- clientReady: 60,
27
- default: 10
28
- }
29
- };
30
- await writeProjectFile(context.cwd, project);
18
+ const project = createDefaultProjectFile(context.cwd, projectName);
19
+ await writeProjectFile(project.projectId, project);
31
20
  return {
32
21
  created: true,
22
+ projectId: project.projectId,
33
23
  project: projectName,
34
- file: "mct.project.json"
24
+ rootDir: project.rootDir,
25
+ file: resolveProjectFilePath(project.projectId),
26
+ configPath: resolveProjectFilePath(project.projectId)
35
27
  };
36
28
  }));
37
29
  }
@@ -40,8 +32,8 @@ export function createDeployCommand() {
40
32
  .description("Deploy plugin JARs to the server instance")
41
33
  .option("--profile <name>", "Profile name")
42
34
  .action(wrapCommand(async (context, { options }) => {
43
- const { projectFile, projectName } = context;
44
- if (!projectFile || !projectName) {
35
+ const { projectFile, projectId, projectRootDir } = context;
36
+ if (!projectFile || !projectId || !projectRootDir) {
45
37
  throw new MctError({ code: "NO_PROJECT", message: "No project context. Run 'mct init' first." }, 4);
46
38
  }
47
39
  const profile = resolveProfile(projectFile, options.profile ?? projectFile.defaultProfile);
@@ -51,8 +43,8 @@ export function createDeployCommand() {
51
43
  if (!profile.deployPlugins || profile.deployPlugins.length === 0) {
52
44
  return { deployed: [], message: "No deployPlugins configured in profile" };
53
45
  }
54
- const manager = new ServerInstanceManager(context.globalState, projectName);
55
- const deployed = await manager.deploy(profile.server, profile.deployPlugins, context.cwd);
46
+ const manager = new ServerInstanceManager(context.globalState, projectId);
47
+ const deployed = await manager.deploy(profile.server, profile.deployPlugins, projectRootDir);
56
48
  return { deployed, server: profile.server };
57
49
  }));
58
50
  }
@@ -62,20 +54,20 @@ export function createUpCommand() {
62
54
  .option("--profile <name>", "Profile name")
63
55
  .option("--eula", "Auto-accept EULA")
64
56
  .action(wrapCommand(async (context, { options }) => {
65
- const { projectFile, projectName } = context;
66
- if (!projectFile || !projectName) {
57
+ const { projectFile, projectId, projectRootDir } = context;
58
+ if (!projectFile || !projectId || !projectRootDir) {
67
59
  throw new MctError({ code: "NO_PROJECT", message: "No project context. Run 'mct init' first." }, 4);
68
60
  }
69
61
  const profile = resolveProfile(projectFile, options.profile ?? projectFile.defaultProfile);
70
62
  if (!profile) {
71
63
  throw new MctError({ code: "NO_PROFILE", message: "No profile specified and no defaultProfile set" }, 4);
72
64
  }
73
- const serverManager = new ServerInstanceManager(context.globalState, projectName);
65
+ const serverManager = new ServerInstanceManager(context.globalState, projectId);
74
66
  const clientManager = new ClientInstanceManager(context.globalState);
75
67
  const results = {};
76
68
  // 1. Deploy plugins
77
69
  if (profile.deployPlugins && profile.deployPlugins.length > 0) {
78
- results.deployed = await serverManager.deploy(profile.server, profile.deployPlugins, context.cwd);
70
+ results.deployed = await serverManager.deploy(profile.server, profile.deployPlugins, projectRootDir);
79
71
  }
80
72
  // 2. Start server
81
73
  results.server = await serverManager.start(profile.server, { eula: options.eula });
@@ -109,15 +101,15 @@ export function createDownCommand() {
109
101
  .description("Stop server and clients for the active profile")
110
102
  .option("--profile <name>", "Profile name")
111
103
  .action(wrapCommand(async (context, { options }) => {
112
- const { projectFile, projectName } = context;
113
- if (!projectFile || !projectName) {
104
+ const { projectFile, projectId } = context;
105
+ if (!projectFile || !projectId) {
114
106
  throw new MctError({ code: "NO_PROJECT", message: "No project context. Run 'mct init' first." }, 4);
115
107
  }
116
108
  const profile = resolveProfile(projectFile, options.profile ?? projectFile.defaultProfile);
117
109
  if (!profile) {
118
110
  throw new MctError({ code: "NO_PROFILE", message: "No profile specified and no defaultProfile set" }, 4);
119
111
  }
120
- const serverManager = new ServerInstanceManager(context.globalState, projectName);
112
+ const serverManager = new ServerInstanceManager(context.globalState, projectId);
121
113
  const clientManager = new ClientInstanceManager(context.globalState);
122
114
  const results = {};
123
115
  // Stop clients first
@@ -155,7 +147,7 @@ export function createUseCommand() {
155
147
  }, 4);
156
148
  }
157
149
  projectFile.defaultProfile = profileName;
158
- await writeProjectFile(context.cwd, projectFile);
150
+ await writeProjectFile(projectFile.projectId, projectFile);
159
151
  return {
160
152
  defaultProfile: profileName,
161
153
  profile: projectFile.profiles[profileName]
@@ -7,6 +7,8 @@ export interface RequestPayload<TOptions> {
7
7
  }
8
8
  export declare function sendClientRequest(context: CommandContext, clientName: string | undefined, action: string, params: Record<string, unknown>, timeoutSeconds?: number): Promise<unknown>;
9
9
  export declare function resolvePreferredClientName(context: CommandContext, globalOptions: GlobalOptions): string | undefined;
10
+ export declare function resolveProjectRelativePath(context: CommandContext, targetPath: string): string;
11
+ export declare function resolveScreenshotOutputPath(context: CommandContext, output: string | undefined, prefix: "screenshot" | "gui"): string;
10
12
  export declare function createRequestAction<TOptions = Record<string, any>>(action: string, buildParams: (payload: RequestPayload<TOptions>) => Record<string, unknown>, timeoutSelector?: (payload: RequestPayload<TOptions>, context: CommandContext) => number | undefined): (this: Command, ...input: unknown[]) => Promise<void>;
11
13
  export declare function parseJson(text: string, fieldName: string): Record<string, unknown>;
12
14
  export declare function parseNumberList(text: string): number[];
@@ -1,4 +1,5 @@
1
1
  import { Command } from "commander";
2
+ import path from "node:path";
2
3
  import { ClientInstanceManager } from "../instance/ClientInstanceManager.js";
3
4
  import { WebSocketClient } from "../client/WebSocketClient.js";
4
5
  import { MctError } from "../util/errors.js";
@@ -12,6 +13,23 @@ export async function sendClientRequest(context, clientName, action, params, tim
12
13
  export function resolvePreferredClientName(context, globalOptions) {
13
14
  return globalOptions.client ?? context.activeProfile?.clients[0];
14
15
  }
16
+ export function resolveProjectRelativePath(context, targetPath) {
17
+ if (path.isAbsolute(targetPath)) {
18
+ return targetPath;
19
+ }
20
+ return path.resolve(context.projectRootDir ?? context.cwd, targetPath);
21
+ }
22
+ export function resolveScreenshotOutputPath(context, output, prefix) {
23
+ if (output) {
24
+ return resolveProjectRelativePath(context, output);
25
+ }
26
+ const outputDir = context.projectFile?.screenshot?.outputDir;
27
+ if (!outputDir) {
28
+ throw new MctError({ code: "INVALID_PARAMS", message: "--output is required outside a project context." }, 4);
29
+ }
30
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
31
+ return path.join(outputDir, `${prefix}-${timestamp}.png`);
32
+ }
15
33
  export function createRequestAction(action, buildParams, timeoutSelector) {
16
34
  return wrapCommand(async (context, payload) => {
17
35
  const timeout = timeoutSelector?.(payload, context);
@@ -1,14 +1,17 @@
1
1
  import { Command } from "commander";
2
- import { createRequestAction } from "./request-helpers.js";
2
+ import { wrapCommand } from "../util/command.js";
3
+ import { resolveScreenshotOutputPath, sendClientRequest } from "./request-helpers.js";
3
4
  export function createScreenshotCommand() {
4
5
  return new Command("screenshot")
5
6
  .description("Take a screenshot")
6
- .requiredOption("--output <path>", "Output file path (e.g. ./screenshots/test.png)")
7
+ .option("--output <path>", "Output file path (default: project screenshot directory)")
7
8
  .option("--region <region>", "Capture a sub-region, format: x,y,w,h")
8
9
  .option("--gui", "Capture the current GUI screen")
9
- .action(createRequestAction("capture.screenshot", ({ options }) => ({
10
- output: options.output,
11
- region: options.region,
12
- gui: Boolean(options.gui)
13
- })));
10
+ .action(wrapCommand(async (context, { options, globalOptions }) => {
11
+ return sendClientRequest(context, globalOptions.client ?? context.activeProfile?.clients[0], "capture.screenshot", {
12
+ output: resolveScreenshotOutputPath(context, options.output, "screenshot"),
13
+ region: options.region,
14
+ gui: Boolean(options.gui)
15
+ });
16
+ }));
14
17
  }
@@ -5,10 +5,10 @@ import { ServerInstanceManager } from "../instance/ServerInstanceManager.js";
5
5
  import { MctError } from "../util/errors.js";
6
6
  import { wrapCommand } from "../util/command.js";
7
7
  function requireProject(context) {
8
- if (!context.projectName) {
9
- throw new MctError({ code: "NO_PROJECT", message: "No project context. Run 'mct init' first or use --project <name>" }, 4);
8
+ if (!context.projectId) {
9
+ throw new MctError({ code: "NO_PROJECT", message: "No project context. Run 'mct init' first or use --project <id>" }, 4);
10
10
  }
11
- return context.projectName;
11
+ return context.projectId;
12
12
  }
13
13
  function resolveServerName(context, explicit) {
14
14
  if (explicit)
@@ -90,13 +90,13 @@ export function createServerCommand() {
90
90
  .argument("[name]", "Server instance name (omit to show all in project)")
91
91
  .option("--all", "Show running servers across all projects")
92
92
  .action(wrapCommand(async (context, { args, options }) => {
93
- if (options.all || (!context.projectName && !args[0])) {
93
+ if (options.all || (!context.projectId && !args[0])) {
94
94
  return ServerInstanceManager.statusAll(context.globalState);
95
95
  }
96
- if (!context.projectName) {
97
- throw new MctError({ code: "NO_PROJECT", message: "No project context. Omit the name to inspect all running servers, or use --project <name>." }, 4);
96
+ if (!context.projectId) {
97
+ throw new MctError({ code: "NO_PROJECT", message: "No project context. Omit the name to inspect all running servers, or use --project <id>." }, 4);
98
98
  }
99
- const manager = new ServerInstanceManager(context.globalState, context.projectName);
99
+ const manager = new ServerInstanceManager(context.globalState, context.projectId);
100
100
  return manager.status(args[0]);
101
101
  }));
102
102
  command
package/dist/index.js CHANGED
@@ -37,7 +37,7 @@ export function buildProgram() {
37
37
  "Control a real Minecraft client via CLI to simulate player actions and verify plugin behavior.\n" +
38
38
  "All commands output JSON by default. Use --human for human-readable output.\n\n" +
39
39
  "Quick start:\n" +
40
- " mct init --project my-plugin\n" +
40
+ " mct init --name my-plugin\n" +
41
41
  " mct server create paper-1.20.4 --type paper --version 1.20.4\n" +
42
42
  " mct client create fabric-1.20.4 --version 1.20.4\n" +
43
43
  " mct up --profile 1.20\n" +
@@ -53,7 +53,10 @@ export function buildProgram() {
53
53
  .action(wrapCommand(async (context) => {
54
54
  return {
55
55
  cwd: context.cwd,
56
+ projectId: context.projectId,
56
57
  project: context.projectName,
58
+ projectRootDir: context.projectRootDir,
59
+ projectConfigPath: context.projectConfigPath,
57
60
  activeProfile: context.activeProfile,
58
61
  globalStateDir: context.globalState.getRootDir()
59
62
  };
@@ -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
  }
@@ -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;
@@ -1,11 +1,24 @@
1
- import { access, readFile, writeFile } from "node:fs/promises";
1
+ import { realpathSync } from "node:fs";
2
+ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
2
3
  import path from "node:path";
3
- export const PROJECT_FILE_NAME = "mct.project.json";
4
- export function resolveProjectFilePath(cwd) {
5
- return path.join(cwd, PROJECT_FILE_NAME);
4
+ import { resolveProjectConfigPath, resolveProjectScreenshotsDir } from "./paths.js";
5
+ export const PROJECT_FILE_NAME = "project.json";
6
+ export function normalizeProjectRoot(cwd) {
7
+ try {
8
+ return realpathSync(cwd);
9
+ }
10
+ catch {
11
+ return path.resolve(cwd);
12
+ }
6
13
  }
7
- export async function loadProjectFile(cwd) {
8
- const filePath = resolveProjectFilePath(cwd);
14
+ export function slugifyProjectId(cwd) {
15
+ return cwd.replace(/[^A-Za-z0-9._-]/g, "-").replace(/-+/g, "-");
16
+ }
17
+ export function resolveProjectFilePath(projectId) {
18
+ return resolveProjectConfigPath(projectId);
19
+ }
20
+ export async function loadProjectFileById(projectId) {
21
+ const filePath = resolveProjectFilePath(projectId);
9
22
  try {
10
23
  await access(filePath);
11
24
  }
@@ -15,10 +28,52 @@ export async function loadProjectFile(cwd) {
15
28
  const raw = await readFile(filePath, "utf8");
16
29
  return JSON.parse(raw);
17
30
  }
18
- export async function writeProjectFile(cwd, project) {
19
- const filePath = resolveProjectFilePath(cwd);
31
+ export async function loadProjectFileForCwd(cwd) {
32
+ const projectId = slugifyProjectId(normalizeProjectRoot(cwd));
33
+ const projectFile = await loadProjectFileById(projectId);
34
+ if (!projectFile) {
35
+ return null;
36
+ }
37
+ return {
38
+ projectId,
39
+ filePath: resolveProjectFilePath(projectId),
40
+ projectFile
41
+ };
42
+ }
43
+ export async function loadProjectFileForId(projectId) {
44
+ const projectFile = await loadProjectFileById(projectId);
45
+ if (!projectFile) {
46
+ return null;
47
+ }
48
+ return {
49
+ projectId,
50
+ filePath: resolveProjectFilePath(projectId),
51
+ projectFile
52
+ };
53
+ }
54
+ export async function writeProjectFile(projectId, project) {
55
+ const filePath = resolveProjectFilePath(projectId);
56
+ await mkdir(path.dirname(filePath), { recursive: true });
20
57
  await writeFile(filePath, `${JSON.stringify(project, null, 2)}\n`, "utf8");
21
58
  }
59
+ export function createDefaultProjectFile(cwd, projectName) {
60
+ const rootDir = normalizeProjectRoot(cwd);
61
+ const projectId = slugifyProjectId(rootDir);
62
+ return {
63
+ projectId,
64
+ project: projectName,
65
+ rootDir,
66
+ profiles: {},
67
+ screenshot: {
68
+ outputDir: resolveProjectScreenshotsDir(projectId)
69
+ },
70
+ timeout: {
71
+ serverReady: 120,
72
+ clientReady: 60,
73
+ default: 10
74
+ }
75
+ };
76
+ }
22
77
  export function resolveProfile(projectFile, profileName) {
23
78
  const name = profileName ?? projectFile.defaultProfile;
24
79
  if (!name) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kzheart_/mc-pilot",
3
- "version": "0.8.0",
3
+ "version": "0.9.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": {