@kzheart_/mc-pilot 0.7.0 → 0.8.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.
@@ -47,6 +47,66 @@ function readAllEvents(filePath) {
47
47
  }
48
48
  return out;
49
49
  }
50
+ function filterEvents(events, options) {
51
+ let filtered = events;
52
+ if (options.sinceMs !== undefined) {
53
+ filtered = filtered.filter((event) => event.t >= options.sinceMs);
54
+ }
55
+ if (options.type) {
56
+ const wanted = new Set(options.type.split(",").map((s) => s.trim()).filter(Boolean));
57
+ filtered = filtered.filter((event) => wanted.has(event.type));
58
+ }
59
+ return filtered;
60
+ }
61
+ function buildPayloadText(event) {
62
+ return JSON.stringify({
63
+ type: event.type,
64
+ payload: event.payload ?? {}
65
+ });
66
+ }
67
+ function buildMatchPattern(raw) {
68
+ if (!raw)
69
+ return null;
70
+ try {
71
+ return new RegExp(raw);
72
+ }
73
+ catch {
74
+ throw new MctError({ code: "INVALID_PARAMS", message: `Invalid --match pattern: ${raw}` }, 4);
75
+ }
76
+ }
77
+ async function waitForEvent(filePath, options) {
78
+ const deadline = Date.now() + options.timeoutSeconds * 1000;
79
+ const matchPattern = buildMatchPattern(options.match);
80
+ const startedAt = Date.now();
81
+ while (Date.now() < deadline) {
82
+ const events = filterEvents(readAllEvents(filePath), {
83
+ sinceMs: options.sinceMs,
84
+ type: options.type
85
+ });
86
+ const matched = matchPattern
87
+ ? events.find((event) => matchPattern.test(buildPayloadText(event)))
88
+ : events[0];
89
+ if (matched) {
90
+ return {
91
+ file: filePath,
92
+ matched: true,
93
+ waitedMs: Date.now() - startedAt,
94
+ event: matched
95
+ };
96
+ }
97
+ await new Promise((resolve) => setTimeout(resolve, 250));
98
+ }
99
+ throw new MctError({
100
+ code: "TIMEOUT",
101
+ message: `Timed out after ${options.timeoutSeconds}s waiting for events in ${filePath}`,
102
+ details: {
103
+ file: filePath,
104
+ type: options.type ?? null,
105
+ match: options.match ?? null,
106
+ sinceMs: options.sinceMs
107
+ }
108
+ }, 2);
109
+ }
50
110
  export function createEventsCommand() {
51
111
  const command = new Command("events").description("Inspect the client event log (written by the mod to ~/.mct/logs/<client>/events.jsonl)");
52
112
  command
@@ -61,15 +121,10 @@ export function createEventsCommand() {
61
121
  const clientName = globalOptions.client ?? context.activeProfile?.clients[0];
62
122
  const filePath = options.file ?? resolveEventsFile(clientName);
63
123
  const events = readAllEvents(filePath);
64
- let filtered = events;
65
- const sinceMs = parseSince(options.since, Date.now());
66
- if (sinceMs !== undefined) {
67
- filtered = filtered.filter((e) => e.t >= sinceMs);
68
- }
69
- if (options.type) {
70
- const wanted = new Set(options.type.split(",").map((s) => s.trim()).filter(Boolean));
71
- filtered = filtered.filter((e) => wanted.has(e.type));
72
- }
124
+ let filtered = filterEvents(events, {
125
+ sinceMs: parseSince(options.since, Date.now()),
126
+ type: options.type
127
+ });
73
128
  if (!options.all) {
74
129
  const tail = options.tail ?? 20;
75
130
  if (filtered.length > tail) {
@@ -93,15 +148,31 @@ export function createEventsCommand() {
93
148
  const tail = args[0] ? Number(args[0]) : 20;
94
149
  const clientName = globalOptions.client ?? context.activeProfile?.clients[0];
95
150
  const filePath = options.file ?? resolveEventsFile(clientName);
96
- let events = readAllEvents(filePath);
97
- if (options.type) {
98
- const wanted = new Set(options.type.split(",").map((s) => s.trim()).filter(Boolean));
99
- events = events.filter((e) => wanted.has(e.type));
100
- }
151
+ let events = filterEvents(readAllEvents(filePath), {
152
+ type: options.type
153
+ });
101
154
  if (events.length > tail)
102
155
  events = events.slice(events.length - tail);
103
156
  return { file: filePath, returned: events.length, events };
104
157
  }));
158
+ command
159
+ .command("wait")
160
+ .description("Wait for a matching event. Defaults to events emitted after this command starts.")
161
+ .option("--timeout <seconds>", "Timeout in seconds (default 10)", Number)
162
+ .option("--since <duration>", "Also consider events since duration ago (e.g. 30s, 5m, 1h) or epoch ms")
163
+ .option("--type <types>", "Comma-separated list of event types to include")
164
+ .option("--match <pattern>", "Regex matched against event type and payload JSON")
165
+ .option("--file <path>", "Override the log file path")
166
+ .action(wrapCommand(async (context, { options, globalOptions }) => {
167
+ const clientName = globalOptions.client ?? context.activeProfile?.clients[0];
168
+ const filePath = options.file ?? resolveEventsFile(clientName);
169
+ return waitForEvent(filePath, {
170
+ timeoutSeconds: options.timeout ?? context.timeout("default"),
171
+ sinceMs: parseSince(options.since, Date.now()) ?? Date.now(),
172
+ type: options.type,
173
+ match: options.match
174
+ });
175
+ }));
105
176
  command
106
177
  .command("clear")
107
178
  .description("Truncate the event log for the active client")
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function createSchemaCommand(getProgram: () => Command): Command;
@@ -0,0 +1,10 @@
1
+ import { Command } from "commander";
2
+ import { buildSchemaDocument } from "../schema.js";
3
+ import { wrapCommand } from "../util/command.js";
4
+ export function createSchemaCommand(getProgram) {
5
+ return new Command("schema")
6
+ .description("Output a machine-readable CLI and protocol schema")
7
+ .action(wrapCommand(async () => {
8
+ return buildSchemaDocument(getProgram());
9
+ }));
10
+ }
package/dist/index.js CHANGED
@@ -21,6 +21,7 @@ import { createResourcepackCommand } from "./commands/resourcepack.js";
21
21
  import { createRotationCommand } from "./commands/rotation.js";
22
22
  import { createScreenCommand } from "./commands/screen.js";
23
23
  import { createScreenshotCommand } from "./commands/screenshot.js";
24
+ import { createSchemaCommand } from "./commands/schema.js";
24
25
  import { createServerCommand } from "./commands/server.js";
25
26
  import { createSignCommand } from "./commands/sign.js";
26
27
  import { createStatusCommand } from "./commands/status.js";
@@ -67,6 +68,7 @@ export function buildProgram() {
67
68
  program.addCommand(createServerCommand());
68
69
  program.addCommand(createClientCommand());
69
70
  program.addCommand(createPluginCommand());
71
+ program.addCommand(createSchemaCommand(() => program));
70
72
  // Game interaction commands
71
73
  program.addCommand(createChatCommand());
72
74
  program.addCommand(createInputCommand());
@@ -0,0 +1,63 @@
1
+ import type { Argument, Command, Option } from "commander";
2
+ interface ProtocolEntry {
3
+ name?: string;
4
+ code?: string;
5
+ description?: string;
6
+ params?: string[];
7
+ exitCode?: number;
8
+ }
9
+ declare function serializeOption(option: Option): {
10
+ name: string;
11
+ flags: string;
12
+ description: string;
13
+ required: boolean;
14
+ mandatory: boolean;
15
+ defaultValue: any;
16
+ };
17
+ declare function serializeArgument(argument: Argument): {
18
+ name: string;
19
+ required: boolean;
20
+ variadic: boolean;
21
+ };
22
+ declare function serializeCommand(command: Command, parents?: string[]): {
23
+ name: string;
24
+ path: string;
25
+ description: string;
26
+ aliases: string[];
27
+ arguments: ReturnType<typeof serializeArgument>[];
28
+ options: ReturnType<typeof serializeOption>[];
29
+ subcommands: ReturnType<typeof serializeCommand>[];
30
+ leaf: boolean;
31
+ };
32
+ export declare function buildSchemaDocument(program: Command): {
33
+ schemaVersion: number;
34
+ cli: {
35
+ name: string;
36
+ description: string;
37
+ globalOptions: {
38
+ name: string;
39
+ flags: string;
40
+ description: string;
41
+ required: boolean;
42
+ mandatory: boolean;
43
+ defaultValue: any;
44
+ }[];
45
+ commands: {
46
+ name: string;
47
+ path: string;
48
+ description: string;
49
+ aliases: string[];
50
+ arguments: ReturnType<typeof serializeArgument>[];
51
+ options: ReturnType<typeof serializeOption>[];
52
+ subcommands: ReturnType<typeof serializeCommand>[];
53
+ leaf: boolean;
54
+ }[];
55
+ leafCommands: string[];
56
+ };
57
+ protocol: {
58
+ actions: number | ProtocolEntry[];
59
+ queries: number | ProtocolEntry[];
60
+ errors: number | ProtocolEntry[];
61
+ };
62
+ };
63
+ export {};
package/dist/schema.js ADDED
@@ -0,0 +1,77 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ function resolveRepoRoot() {
5
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
6
+ }
7
+ function loadJsonFile(relativePath) {
8
+ const filePath = path.join(resolveRepoRoot(), relativePath);
9
+ return JSON.parse(readFileSync(filePath, "utf8"));
10
+ }
11
+ function serializeOption(option) {
12
+ return {
13
+ name: option.name(),
14
+ flags: option.flags,
15
+ description: option.description,
16
+ required: option.required,
17
+ mandatory: option.mandatory,
18
+ defaultValue: option.defaultValue
19
+ };
20
+ }
21
+ function serializeArgument(argument) {
22
+ return {
23
+ name: argument.name(),
24
+ required: argument.required,
25
+ variadic: argument.variadic
26
+ };
27
+ }
28
+ function serializeCommand(command, parents = []) {
29
+ const pathSegments = [...parents, command.name()];
30
+ return {
31
+ name: command.name(),
32
+ path: pathSegments.join(" "),
33
+ description: command.description(),
34
+ aliases: command.aliases(),
35
+ arguments: command.registeredArguments.map(serializeArgument),
36
+ options: command.options.map(serializeOption),
37
+ subcommands: command.commands.map((subcommand) => serializeCommand(subcommand, pathSegments)),
38
+ leaf: command.commands.length === 0
39
+ };
40
+ }
41
+ function collectLeafCommands(commands) {
42
+ const leaves = [];
43
+ const visit = (command) => {
44
+ if (command.leaf) {
45
+ leaves.push(command.path);
46
+ return;
47
+ }
48
+ for (const subcommand of command.subcommands) {
49
+ visit(subcommand);
50
+ }
51
+ };
52
+ for (const command of commands) {
53
+ visit(command);
54
+ }
55
+ return leaves;
56
+ }
57
+ export function buildSchemaDocument(program) {
58
+ const actions = loadJsonFile("protocol/actions.json");
59
+ const queries = loadJsonFile("protocol/queries.json");
60
+ const errors = loadJsonFile("protocol/errors.json");
61
+ const commands = program.commands.map((command) => serializeCommand(command));
62
+ return {
63
+ schemaVersion: 1,
64
+ cli: {
65
+ name: program.name(),
66
+ description: program.description(),
67
+ globalOptions: program.options.map(serializeOption),
68
+ commands,
69
+ leafCommands: collectLeafCommands(commands)
70
+ },
71
+ protocol: {
72
+ actions: actions.actions ?? [],
73
+ queries: queries.queries ?? [],
74
+ errors: errors.errors ?? []
75
+ }
76
+ };
77
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kzheart_/mc-pilot",
3
- "version": "0.7.0",
3
+ "version": "0.8.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": {