gdharness 0.5.1 → 0.5.3
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/build/cli.js +58 -25
- package/build/index.js +58 -25
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -160,6 +160,19 @@ function runArguments(options) {
|
|
|
160
160
|
function editorArguments(projectPath) {
|
|
161
161
|
return ["-e", "--path", projectPath];
|
|
162
162
|
}
|
|
163
|
+
function userDataIn(home, variables = process.env) {
|
|
164
|
+
const moved = ["APPDATA", "XDG_DATA_HOME"];
|
|
165
|
+
const carried = {};
|
|
166
|
+
for (const [name, value] of Object.entries(variables)) {
|
|
167
|
+
if (!moved.some((named) => named.toLowerCase() === name.toLowerCase())) {
|
|
168
|
+
carried[name] = value;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
for (const named of moved) {
|
|
172
|
+
carried[named] = home;
|
|
173
|
+
}
|
|
174
|
+
return carried;
|
|
175
|
+
}
|
|
163
176
|
|
|
164
177
|
// node_modules/yaml/dist/nodes/identity.js
|
|
165
178
|
var require_identity = __commonJS(function(exports) {
|
|
@@ -15338,7 +15351,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15338
15351
|
},
|
|
15339
15352
|
{
|
|
15340
15353
|
name: "project_test",
|
|
15341
|
-
description: "Runs the project's gdUnit4 tests headless and answers with every case: which failed, where, and what the assertion said. The class list is rebuilt first, so a suite written a moment ago is found. Needs gdUnit4 under addons/gdUnit4.",
|
|
15354
|
+
description: "Runs the project's gdUnit4 tests headless and answers with every case: which failed, where, and what the assertion said. The class list is rebuilt first, so a suite written a moment ago is found. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. Needs gdUnit4 under addons/gdUnit4.",
|
|
15342
15355
|
parameters: {
|
|
15343
15356
|
projectPath: PROJECT_PATH,
|
|
15344
15357
|
path: {
|
|
@@ -35447,32 +35460,41 @@ function parseAnnouncement(file, pid) {
|
|
|
35447
35460
|
try {
|
|
35448
35461
|
fields = asParams(JSON.parse(readFileSync10(file, "utf8")));
|
|
35449
35462
|
} catch {
|
|
35450
|
-
return
|
|
35463
|
+
return { kind: "rubbish" };
|
|
35451
35464
|
}
|
|
35452
35465
|
const project = readParams(fields, "project");
|
|
35453
35466
|
const port = readNumber(fields, "port");
|
|
35454
35467
|
const address = readString(fields, "address");
|
|
35455
|
-
|
|
35456
|
-
|
|
35468
|
+
const protocol = readNumber(fields, "protocol");
|
|
35469
|
+
if (readNumber(fields, "pid") !== pid || port === undefined || !Number.isInteger(port) || port < 1 || port > 65535 || address === undefined || project === undefined) {
|
|
35470
|
+
return { kind: "rubbish" };
|
|
35457
35471
|
}
|
|
35458
|
-
|
|
35459
|
-
|
|
35460
|
-
|
|
35461
|
-
address,
|
|
35462
|
-
project: { name: readString(project, "name") ?? "", path: readString(project, "path") ?? "" },
|
|
35463
|
-
file
|
|
35472
|
+
const named = {
|
|
35473
|
+
name: readString(project, "name") ?? "",
|
|
35474
|
+
path: readString(project, "path") ?? ""
|
|
35464
35475
|
};
|
|
35476
|
+
if (protocol !== RUNTIME_PROTOCOL) {
|
|
35477
|
+
return { kind: "unspoken", unspoken: { pid, protocol: protocol ?? 0, project: named } };
|
|
35478
|
+
}
|
|
35479
|
+
return { kind: "runtime", endpoint: { pid, port, address, project: named, file } };
|
|
35465
35480
|
}
|
|
35466
|
-
function
|
|
35467
|
-
const
|
|
35481
|
+
function runtimesAnnounced(directories = runtimeDirectories()) {
|
|
35482
|
+
const running = new Map;
|
|
35483
|
+
const unspoken = new Map;
|
|
35468
35484
|
for (const directory of directories) {
|
|
35469
|
-
for (const
|
|
35470
|
-
if (
|
|
35471
|
-
|
|
35485
|
+
for (const found of announcedIn(directory)) {
|
|
35486
|
+
if (found.kind === "runtime" && !running.has(found.endpoint.pid)) {
|
|
35487
|
+
running.set(found.endpoint.pid, found.endpoint);
|
|
35488
|
+
} else if (found.kind === "unspoken" && !unspoken.has(found.unspoken.pid)) {
|
|
35489
|
+
unspoken.set(found.unspoken.pid, found.unspoken);
|
|
35472
35490
|
}
|
|
35473
35491
|
}
|
|
35474
35492
|
}
|
|
35475
|
-
|
|
35493
|
+
const newest = (a, b) => b.pid - a.pid;
|
|
35494
|
+
return { running: [...running.values()].sort(newest), unspoken: [...unspoken.values()].sort(newest) };
|
|
35495
|
+
}
|
|
35496
|
+
function discoverRuntimes(directories = runtimeDirectories()) {
|
|
35497
|
+
return runtimesAnnounced(directories).running;
|
|
35476
35498
|
}
|
|
35477
35499
|
function announcedIn(directory) {
|
|
35478
35500
|
if (!existsSync9(directory)) {
|
|
@@ -35486,22 +35508,30 @@ function announcedIn(directory) {
|
|
|
35486
35508
|
}
|
|
35487
35509
|
const file = join11(directory, entry);
|
|
35488
35510
|
const pid = Number.parseInt(match[1] ?? "", 10);
|
|
35489
|
-
const
|
|
35490
|
-
if (
|
|
35491
|
-
found.push(endpoint);
|
|
35492
|
-
} else {
|
|
35511
|
+
const announced = processAlive(pid) ? parseAnnouncement(file, pid) : { kind: "rubbish" };
|
|
35512
|
+
if (announced.kind === "rubbish") {
|
|
35493
35513
|
try {
|
|
35494
35514
|
unlinkSync(file);
|
|
35495
35515
|
} catch {}
|
|
35516
|
+
continue;
|
|
35496
35517
|
}
|
|
35518
|
+
found.push(announced);
|
|
35497
35519
|
}
|
|
35498
35520
|
return found;
|
|
35499
35521
|
}
|
|
35500
35522
|
function describe2(endpoint) {
|
|
35501
35523
|
return `pid ${endpoint.pid} on ${endpoint.address}:${endpoint.port} (${endpoint.project.name || "unnamed"} at ${endpoint.project.path})`;
|
|
35502
35524
|
}
|
|
35503
|
-
function
|
|
35525
|
+
function tooNew(unspoken) {
|
|
35526
|
+
const named = unspoken.map((game) => `pid ${game.pid} speaks ${game.protocol} (${game.project.name || "unnamed"} at ${game.project.path})`).join("; ");
|
|
35527
|
+
const half = unspoken.some((game) => game.protocol > RUNTIME_PROTOCOL) ? "this server is the older half: reconnect it so it spawns the installed version" : "the addon is the older half: reinstall it and restart the game";
|
|
35528
|
+
return `A game is running, in a protocol this server does not speak ${named}. This server speaks ${RUNTIME_PROTOCOL}, so ${half}.`;
|
|
35529
|
+
}
|
|
35530
|
+
function chooseRuntime(endpoints, projectPath, unspoken = []) {
|
|
35504
35531
|
if (endpoints.length === 0) {
|
|
35532
|
+
if (unspoken.length > 0) {
|
|
35533
|
+
return { problem: tooNew(unspoken) };
|
|
35534
|
+
}
|
|
35505
35535
|
return {
|
|
35506
35536
|
problem: "No game with the runtime addon is running. Start one with editor_run, or play the project from the editor with the addon enabled."
|
|
35507
35537
|
};
|
|
@@ -36422,7 +36452,8 @@ class GodotServer {
|
|
|
36422
36452
|
];
|
|
36423
36453
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
|
|
36424
36454
|
this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
36425
|
-
const
|
|
36455
|
+
const userData = mkdtempSync3(join12(tmpdir5(), "gdharness-tests-"));
|
|
36456
|
+
const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
|
|
36426
36457
|
const hung = await new Promise((resolve) => {
|
|
36427
36458
|
const timer = setTimeout(() => {
|
|
36428
36459
|
run.process.kill();
|
|
@@ -36450,6 +36481,7 @@ class GodotServer {
|
|
|
36450
36481
|
reportProblem = errorMessage(error);
|
|
36451
36482
|
} finally {
|
|
36452
36483
|
rmSync6(reportsDir, { recursive: true, force: true });
|
|
36484
|
+
rmSync6(userData, { recursive: true, force: true });
|
|
36453
36485
|
}
|
|
36454
36486
|
const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries;
|
|
36455
36487
|
const verdicts = {
|
|
@@ -36806,8 +36838,8 @@ class GodotServer {
|
|
|
36806
36838
|
}
|
|
36807
36839
|
running.process?.kill();
|
|
36808
36840
|
}
|
|
36809
|
-
spawnGame(godotPath, cmdArgs) {
|
|
36810
|
-
const child = spawn(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"] });
|
|
36841
|
+
spawnGame(godotPath, cmdArgs, env) {
|
|
36842
|
+
const child = spawn(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
|
|
36811
36843
|
const log = new GameLog;
|
|
36812
36844
|
const started = {
|
|
36813
36845
|
process: child,
|
|
@@ -36950,7 +36982,8 @@ class GodotServer {
|
|
|
36950
36982
|
}
|
|
36951
36983
|
async handleRuntimeCommand(command, args, timeoutMs = this.runtimeTimeoutMs()) {
|
|
36952
36984
|
const { op: _op, projectPath, ...params } = asParams(args);
|
|
36953
|
-
const
|
|
36985
|
+
const announced = runtimesAnnounced();
|
|
36986
|
+
const choice = chooseRuntime(announced.running, typeof projectPath === "string" ? projectPath : undefined, announced.unspoken);
|
|
36954
36987
|
if ("problem" in choice) {
|
|
36955
36988
|
return this.createErrorResponse(choice.problem);
|
|
36956
36989
|
}
|
package/build/index.js
CHANGED
|
@@ -25933,6 +25933,19 @@ function runArguments(options) {
|
|
|
25933
25933
|
function editorArguments(projectPath) {
|
|
25934
25934
|
return ["-e", "--path", projectPath];
|
|
25935
25935
|
}
|
|
25936
|
+
function userDataIn(home, variables = process.env) {
|
|
25937
|
+
const moved = ["APPDATA", "XDG_DATA_HOME"];
|
|
25938
|
+
const carried = {};
|
|
25939
|
+
for (const [name, value] of Object.entries(variables)) {
|
|
25940
|
+
if (!moved.some((named) => named.toLowerCase() === name.toLowerCase())) {
|
|
25941
|
+
carried[name] = value;
|
|
25942
|
+
}
|
|
25943
|
+
}
|
|
25944
|
+
for (const named of moved) {
|
|
25945
|
+
carried[named] = home;
|
|
25946
|
+
}
|
|
25947
|
+
return carried;
|
|
25948
|
+
}
|
|
25936
25949
|
|
|
25937
25950
|
// src/godot-path.ts
|
|
25938
25951
|
var run = promisify(execFile);
|
|
@@ -27295,32 +27308,41 @@ function parseAnnouncement(file, pid) {
|
|
|
27295
27308
|
try {
|
|
27296
27309
|
fields = asParams(JSON.parse(readFileSync6(file, "utf8")));
|
|
27297
27310
|
} catch {
|
|
27298
|
-
return
|
|
27311
|
+
return { kind: "rubbish" };
|
|
27299
27312
|
}
|
|
27300
27313
|
const project = readParams(fields, "project");
|
|
27301
27314
|
const port = readNumber(fields, "port");
|
|
27302
27315
|
const address = readString(fields, "address");
|
|
27303
|
-
|
|
27304
|
-
|
|
27316
|
+
const protocol = readNumber(fields, "protocol");
|
|
27317
|
+
if (readNumber(fields, "pid") !== pid || port === undefined || !Number.isInteger(port) || port < 1 || port > 65535 || address === undefined || project === undefined) {
|
|
27318
|
+
return { kind: "rubbish" };
|
|
27305
27319
|
}
|
|
27306
|
-
|
|
27307
|
-
|
|
27308
|
-
|
|
27309
|
-
address,
|
|
27310
|
-
project: { name: readString(project, "name") ?? "", path: readString(project, "path") ?? "" },
|
|
27311
|
-
file
|
|
27320
|
+
const named = {
|
|
27321
|
+
name: readString(project, "name") ?? "",
|
|
27322
|
+
path: readString(project, "path") ?? ""
|
|
27312
27323
|
};
|
|
27324
|
+
if (protocol !== RUNTIME_PROTOCOL) {
|
|
27325
|
+
return { kind: "unspoken", unspoken: { pid, protocol: protocol ?? 0, project: named } };
|
|
27326
|
+
}
|
|
27327
|
+
return { kind: "runtime", endpoint: { pid, port, address, project: named, file } };
|
|
27313
27328
|
}
|
|
27314
|
-
function
|
|
27315
|
-
const
|
|
27329
|
+
function runtimesAnnounced(directories = runtimeDirectories()) {
|
|
27330
|
+
const running = new Map;
|
|
27331
|
+
const unspoken = new Map;
|
|
27316
27332
|
for (const directory of directories) {
|
|
27317
|
-
for (const
|
|
27318
|
-
if (
|
|
27319
|
-
|
|
27333
|
+
for (const found of announcedIn(directory)) {
|
|
27334
|
+
if (found.kind === "runtime" && !running.has(found.endpoint.pid)) {
|
|
27335
|
+
running.set(found.endpoint.pid, found.endpoint);
|
|
27336
|
+
} else if (found.kind === "unspoken" && !unspoken.has(found.unspoken.pid)) {
|
|
27337
|
+
unspoken.set(found.unspoken.pid, found.unspoken);
|
|
27320
27338
|
}
|
|
27321
27339
|
}
|
|
27322
27340
|
}
|
|
27323
|
-
|
|
27341
|
+
const newest = (a, b) => b.pid - a.pid;
|
|
27342
|
+
return { running: [...running.values()].sort(newest), unspoken: [...unspoken.values()].sort(newest) };
|
|
27343
|
+
}
|
|
27344
|
+
function discoverRuntimes(directories = runtimeDirectories()) {
|
|
27345
|
+
return runtimesAnnounced(directories).running;
|
|
27324
27346
|
}
|
|
27325
27347
|
function announcedIn(directory) {
|
|
27326
27348
|
if (!existsSync5(directory)) {
|
|
@@ -27334,22 +27356,30 @@ function announcedIn(directory) {
|
|
|
27334
27356
|
}
|
|
27335
27357
|
const file = join6(directory, entry);
|
|
27336
27358
|
const pid = Number.parseInt(match[1] ?? "", 10);
|
|
27337
|
-
const
|
|
27338
|
-
if (
|
|
27339
|
-
found.push(endpoint);
|
|
27340
|
-
} else {
|
|
27359
|
+
const announced = processAlive(pid) ? parseAnnouncement(file, pid) : { kind: "rubbish" };
|
|
27360
|
+
if (announced.kind === "rubbish") {
|
|
27341
27361
|
try {
|
|
27342
27362
|
unlinkSync(file);
|
|
27343
27363
|
} catch {}
|
|
27364
|
+
continue;
|
|
27344
27365
|
}
|
|
27366
|
+
found.push(announced);
|
|
27345
27367
|
}
|
|
27346
27368
|
return found;
|
|
27347
27369
|
}
|
|
27348
27370
|
function describe2(endpoint) {
|
|
27349
27371
|
return `pid ${endpoint.pid} on ${endpoint.address}:${endpoint.port} (${endpoint.project.name || "unnamed"} at ${endpoint.project.path})`;
|
|
27350
27372
|
}
|
|
27351
|
-
function
|
|
27373
|
+
function tooNew(unspoken) {
|
|
27374
|
+
const named = unspoken.map((game) => `pid ${game.pid} speaks ${game.protocol} (${game.project.name || "unnamed"} at ${game.project.path})`).join("; ");
|
|
27375
|
+
const half = unspoken.some((game) => game.protocol > RUNTIME_PROTOCOL) ? "this server is the older half: reconnect it so it spawns the installed version" : "the addon is the older half: reinstall it and restart the game";
|
|
27376
|
+
return `A game is running, in a protocol this server does not speak ${named}. This server speaks ${RUNTIME_PROTOCOL}, so ${half}.`;
|
|
27377
|
+
}
|
|
27378
|
+
function chooseRuntime(endpoints, projectPath, unspoken = []) {
|
|
27352
27379
|
if (endpoints.length === 0) {
|
|
27380
|
+
if (unspoken.length > 0) {
|
|
27381
|
+
return { problem: tooNew(unspoken) };
|
|
27382
|
+
}
|
|
27353
27383
|
return {
|
|
27354
27384
|
problem: "No game with the runtime addon is running. Start one with editor_run, or play the project from the editor with the addon enabled."
|
|
27355
27385
|
};
|
|
@@ -27767,7 +27797,7 @@ var TOOL_SPECS = [
|
|
|
27767
27797
|
},
|
|
27768
27798
|
{
|
|
27769
27799
|
name: "project_test",
|
|
27770
|
-
description: "Runs the project's gdUnit4 tests headless and answers with every case: which failed, where, and what the assertion said. The class list is rebuilt first, so a suite written a moment ago is found. Needs gdUnit4 under addons/gdUnit4.",
|
|
27800
|
+
description: "Runs the project's gdUnit4 tests headless and answers with every case: which failed, where, and what the assertion said. The class list is rebuilt first, so a suite written a moment ago is found. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. Needs gdUnit4 under addons/gdUnit4.",
|
|
27771
27801
|
parameters: {
|
|
27772
27802
|
projectPath: PROJECT_PATH,
|
|
27773
27803
|
path: {
|
|
@@ -29221,7 +29251,8 @@ class GodotServer {
|
|
|
29221
29251
|
];
|
|
29222
29252
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
|
|
29223
29253
|
this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
29224
|
-
const
|
|
29254
|
+
const userData = mkdtempSync2(join7(tmpdir4(), "gdharness-tests-"));
|
|
29255
|
+
const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
|
|
29225
29256
|
const hung = await new Promise((resolve) => {
|
|
29226
29257
|
const timer = setTimeout(() => {
|
|
29227
29258
|
run.process.kill();
|
|
@@ -29249,6 +29280,7 @@ class GodotServer {
|
|
|
29249
29280
|
reportProblem = errorMessage(error);
|
|
29250
29281
|
} finally {
|
|
29251
29282
|
rmSync2(reportsDir, { recursive: true, force: true });
|
|
29283
|
+
rmSync2(userData, { recursive: true, force: true });
|
|
29252
29284
|
}
|
|
29253
29285
|
const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries;
|
|
29254
29286
|
const verdicts = {
|
|
@@ -29605,8 +29637,8 @@ class GodotServer {
|
|
|
29605
29637
|
}
|
|
29606
29638
|
running.process?.kill();
|
|
29607
29639
|
}
|
|
29608
|
-
spawnGame(godotPath, cmdArgs) {
|
|
29609
|
-
const child = spawn(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"] });
|
|
29640
|
+
spawnGame(godotPath, cmdArgs, env) {
|
|
29641
|
+
const child = spawn(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
|
|
29610
29642
|
const log = new GameLog;
|
|
29611
29643
|
const started = {
|
|
29612
29644
|
process: child,
|
|
@@ -29749,7 +29781,8 @@ class GodotServer {
|
|
|
29749
29781
|
}
|
|
29750
29782
|
async handleRuntimeCommand(command, args, timeoutMs = this.runtimeTimeoutMs()) {
|
|
29751
29783
|
const { op: _op, projectPath, ...params } = asParams(args);
|
|
29752
|
-
const
|
|
29784
|
+
const announced = runtimesAnnounced();
|
|
29785
|
+
const choice = chooseRuntime(announced.running, typeof projectPath === "string" ? projectPath : undefined, announced.unspoken);
|
|
29753
29786
|
if ("problem" in choice) {
|
|
29754
29787
|
return this.createErrorResponse(choice.problem);
|
|
29755
29788
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"mcpName": "io.github.Aureliolo/gdharness",
|
|
5
5
|
"description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
|
|
6
6
|
"type": "module",
|