gdharness 0.5.7 → 0.5.9
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 +134 -39
- package/build/godot/addons/auto_reload/auto_reload.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/bridge_client.gd +143 -2
- package/build/godot/addons/gdharness_editor/bridge_client.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/plugin.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tool_executor.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +76 -0
- package/build/godot/addons/gdharness_runtime/runtime_input.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +9 -0
- package/build/godot/addons/gdharness_runtime/runtime_values.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd.uid +1 -0
- package/build/godot/operations/serialisation.gd +8 -5
- package/build/index.js +131 -45
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -10126,9 +10126,9 @@ function defectReport(where, error, godotVersion) {
|
|
|
10126
10126
|
|
|
10127
10127
|
// src/server.ts
|
|
10128
10128
|
import { execFile as execFile3, spawn } from "node:child_process";
|
|
10129
|
-
import { existsSync as existsSync6, mkdtempSync as mkdtempSync2, readdirSync as readdirSync5, readFileSync as readFileSync7, realpathSync as realpathSync2, rmSync as
|
|
10129
|
+
import { existsSync as existsSync6, mkdtempSync as mkdtempSync2, readdirSync as readdirSync5, readFileSync as readFileSync7, realpathSync as realpathSync2, rmSync as rmSync3 } from "node:fs";
|
|
10130
10130
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
10131
|
-
import { basename, dirname as
|
|
10131
|
+
import { basename, dirname as dirname3, join as join8, normalize as normalize2 } from "node:path";
|
|
10132
10132
|
import { setTimeout as delay2 } from "node:timers/promises";
|
|
10133
10133
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
10134
10134
|
import { promisify as promisify3 } from "node:util";
|
|
@@ -24696,9 +24696,47 @@ class StdioServerTransport {
|
|
|
24696
24696
|
}
|
|
24697
24697
|
}
|
|
24698
24698
|
|
|
24699
|
+
// src/bridge-announce.ts
|
|
24700
|
+
import { mkdirSync as mkdirSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
24701
|
+
import { dirname, join as join2 } from "node:path";
|
|
24702
|
+
function announcementPath(projectPath) {
|
|
24703
|
+
return join2(projectPath, ".godot", "gdharness-bridge.json");
|
|
24704
|
+
}
|
|
24705
|
+
var BRIDGE_ANNOUNCE_PROTOCOL = 1;
|
|
24706
|
+
function announceBridge(projectPath, bridge) {
|
|
24707
|
+
const path = announcementPath(projectPath);
|
|
24708
|
+
try {
|
|
24709
|
+
mkdirSync2(dirname(path), { recursive: true });
|
|
24710
|
+
const announcement = {
|
|
24711
|
+
protocol: BRIDGE_ANNOUNCE_PROTOCOL,
|
|
24712
|
+
host: bridge.host,
|
|
24713
|
+
port: bridge.port,
|
|
24714
|
+
pid: process.pid,
|
|
24715
|
+
version: bridge.version,
|
|
24716
|
+
startedAt: new Date().toISOString()
|
|
24717
|
+
};
|
|
24718
|
+
writeFileSync2(path, `${JSON.stringify(announcement, null, 2)}
|
|
24719
|
+
`, "utf8");
|
|
24720
|
+
return path;
|
|
24721
|
+
} catch (error) {
|
|
24722
|
+
console.error(`[SERVER] Could not announce the editor bridge at ${path}: ${errorMessage(error)}`);
|
|
24723
|
+
return null;
|
|
24724
|
+
}
|
|
24725
|
+
}
|
|
24726
|
+
function withdrawBridge(path) {
|
|
24727
|
+
if (path === null) {
|
|
24728
|
+
return;
|
|
24729
|
+
}
|
|
24730
|
+
try {
|
|
24731
|
+
rmSync(path, { force: true });
|
|
24732
|
+
} catch (error) {
|
|
24733
|
+
console.error(`[SERVER] Could not withdraw the editor bridge announcement: ${errorMessage(error)}`);
|
|
24734
|
+
}
|
|
24735
|
+
}
|
|
24736
|
+
|
|
24699
24737
|
// src/class-cache.ts
|
|
24700
24738
|
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3 } from "node:fs";
|
|
24701
|
-
import { join as
|
|
24739
|
+
import { join as join3 } from "node:path";
|
|
24702
24740
|
function declaredClasses(projectPath) {
|
|
24703
24741
|
const declared = new Map;
|
|
24704
24742
|
const visit = (directory, prefix) => {
|
|
@@ -24706,7 +24744,7 @@ function declaredClasses(projectPath) {
|
|
|
24706
24744
|
if (entry.name.startsWith(".")) {
|
|
24707
24745
|
continue;
|
|
24708
24746
|
}
|
|
24709
|
-
const path =
|
|
24747
|
+
const path = join3(directory, entry.name);
|
|
24710
24748
|
if (entry.isDirectory()) {
|
|
24711
24749
|
visit(path, `${prefix}${entry.name}/`);
|
|
24712
24750
|
} else if (entry.isFile() && entry.name.endsWith(".gd")) {
|
|
@@ -24721,7 +24759,7 @@ function declaredClasses(projectPath) {
|
|
|
24721
24759
|
return declared;
|
|
24722
24760
|
}
|
|
24723
24761
|
function cachedClasses(projectPath) {
|
|
24724
|
-
const cache =
|
|
24762
|
+
const cache = join3(projectPath, ".godot", "global_script_class_cache.cfg");
|
|
24725
24763
|
if (!existsSync2(cache)) {
|
|
24726
24764
|
return null;
|
|
24727
24765
|
}
|
|
@@ -25426,19 +25464,37 @@ class GodotBridge extends EventEmitter {
|
|
|
25426
25464
|
connectionInfo = null;
|
|
25427
25465
|
pendingRequests = new Map;
|
|
25428
25466
|
resourceQueues = new Map;
|
|
25429
|
-
|
|
25467
|
+
wantedPort;
|
|
25468
|
+
boundPort = null;
|
|
25430
25469
|
host;
|
|
25431
25470
|
timeoutMs;
|
|
25432
|
-
|
|
25471
|
+
mayMove;
|
|
25472
|
+
constructor(port = DEFAULT_PORT, host = DEFAULT_HOST, timeoutMs = DEFAULT_TIMEOUT_MS, mayMove = false) {
|
|
25433
25473
|
super();
|
|
25434
|
-
this.
|
|
25474
|
+
this.wantedPort = port;
|
|
25435
25475
|
this.host = host;
|
|
25436
25476
|
this.timeoutMs = timeoutMs;
|
|
25477
|
+
this.mayMove = mayMove;
|
|
25437
25478
|
}
|
|
25438
|
-
|
|
25479
|
+
get port() {
|
|
25480
|
+
return this.boundPort ?? this.wantedPort;
|
|
25481
|
+
}
|
|
25482
|
+
async start() {
|
|
25439
25483
|
if (this.httpServer) {
|
|
25440
|
-
return
|
|
25484
|
+
return;
|
|
25485
|
+
}
|
|
25486
|
+
try {
|
|
25487
|
+
await this.listenOn(this.wantedPort);
|
|
25488
|
+
} catch (error) {
|
|
25489
|
+
const held = error instanceof Error && "code" in error && error.code === "EADDRINUSE";
|
|
25490
|
+
if (!held || !this.mayMove) {
|
|
25491
|
+
throw error;
|
|
25492
|
+
}
|
|
25493
|
+
this.log("warn", `Editor bridge port ${this.wantedPort} is held; taking another one.`);
|
|
25494
|
+
await this.listenOn(0);
|
|
25441
25495
|
}
|
|
25496
|
+
}
|
|
25497
|
+
listenOn(port) {
|
|
25442
25498
|
return new Promise((resolve, reject) => {
|
|
25443
25499
|
const server = http.createServer((_req, res) => {
|
|
25444
25500
|
res.writeHead(404);
|
|
@@ -25462,6 +25518,8 @@ class GodotBridge extends EventEmitter {
|
|
|
25462
25518
|
settled = true;
|
|
25463
25519
|
this.httpServer = server;
|
|
25464
25520
|
this.godotWss = godotWss;
|
|
25521
|
+
const bound = server.address();
|
|
25522
|
+
this.boundPort = typeof bound === "object" && bound !== null ? bound.port : port;
|
|
25465
25523
|
this.log("info", `Editor bridge listening on ${this.host}:${this.port}`);
|
|
25466
25524
|
resolve();
|
|
25467
25525
|
});
|
|
@@ -25478,7 +25536,7 @@ class GodotBridge extends EventEmitter {
|
|
|
25478
25536
|
godotWss.on("error", (error) => {
|
|
25479
25537
|
this.log("error", `Godot WebSocket server error: ${error.message}`);
|
|
25480
25538
|
});
|
|
25481
|
-
server.listen(
|
|
25539
|
+
server.listen(port, this.host);
|
|
25482
25540
|
});
|
|
25483
25541
|
}
|
|
25484
25542
|
async stop() {
|
|
@@ -25512,6 +25570,7 @@ class GodotBridge extends EventEmitter {
|
|
|
25512
25570
|
this.httpServer = null;
|
|
25513
25571
|
}
|
|
25514
25572
|
await Promise.all(closeTasks);
|
|
25573
|
+
this.boundPort = null;
|
|
25515
25574
|
this.connectionInfo = null;
|
|
25516
25575
|
this.log("info", "WebSocket bridge stopped");
|
|
25517
25576
|
}
|
|
@@ -25813,8 +25872,8 @@ class GodotBridge extends EventEmitter {
|
|
|
25813
25872
|
}
|
|
25814
25873
|
}
|
|
25815
25874
|
var defaultBridge = null;
|
|
25816
|
-
function getDefaultBridge() {
|
|
25817
|
-
defaultBridge ??= new GodotBridge(resolveDefaultBridgePort(), resolveDefaultBridgeHost());
|
|
25875
|
+
function getDefaultBridge(mayMove = false) {
|
|
25876
|
+
defaultBridge ??= new GodotBridge(resolveDefaultBridgePort(), resolveDefaultBridgeHost(), DEFAULT_TIMEOUT_MS, mayMove);
|
|
25818
25877
|
return defaultBridge;
|
|
25819
25878
|
}
|
|
25820
25879
|
|
|
@@ -25827,7 +25886,7 @@ import { promisify } from "node:util";
|
|
|
25827
25886
|
// src/detection.ts
|
|
25828
25887
|
import { existsSync as existsSync3, readdirSync as readdirSync2, statSync } from "node:fs";
|
|
25829
25888
|
import { homedir as homedir2 } from "node:os";
|
|
25830
|
-
import { join as
|
|
25889
|
+
import { join as join4 } from "node:path";
|
|
25831
25890
|
function resolveHomeDirectory() {
|
|
25832
25891
|
try {
|
|
25833
25892
|
return homedir2();
|
|
@@ -25851,7 +25910,7 @@ function scanDirectoryForGodotBinaries(directory, platform) {
|
|
|
25851
25910
|
if (!pattern.test(name)) {
|
|
25852
25911
|
continue;
|
|
25853
25912
|
}
|
|
25854
|
-
const fullPath =
|
|
25913
|
+
const fullPath = join4(directory, name);
|
|
25855
25914
|
try {
|
|
25856
25915
|
const stat = statSync(fullPath);
|
|
25857
25916
|
if (stat.isFile()) {
|
|
@@ -25860,7 +25919,7 @@ function scanDirectoryForGodotBinaries(directory, platform) {
|
|
|
25860
25919
|
} catch {}
|
|
25861
25920
|
}
|
|
25862
25921
|
matches.sort((a, b) => b.mtime - a.mtime);
|
|
25863
|
-
return matches.map((m) =>
|
|
25922
|
+
return matches.map((m) => join4(directory, m.name));
|
|
25864
25923
|
}
|
|
25865
25924
|
function conventionalPaths(platform, home) {
|
|
25866
25925
|
const paths = ["godot"];
|
|
@@ -26003,9 +26062,9 @@ class GodotLocator {
|
|
|
26003
26062
|
|
|
26004
26063
|
// src/headless.ts
|
|
26005
26064
|
import { execFile as execFile2 } from "node:child_process";
|
|
26006
|
-
import { mkdtempSync, rmSync, writeFileSync as
|
|
26065
|
+
import { mkdtempSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
26007
26066
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
26008
|
-
import { join as
|
|
26067
|
+
import { join as join5 } from "node:path";
|
|
26009
26068
|
import { promisify as promisify2 } from "node:util";
|
|
26010
26069
|
var run2 = promisify2(execFile2);
|
|
26011
26070
|
function snakeCased(params) {
|
|
@@ -26046,9 +26105,9 @@ function reason(stdout, stderr) {
|
|
|
26046
26105
|
return stdout.trim().split(/\r?\n/).at(-1) ?? "no output at all";
|
|
26047
26106
|
}
|
|
26048
26107
|
async function runOperation(engine, operation, params, projectPath) {
|
|
26049
|
-
const paramsDir = mkdtempSync(
|
|
26050
|
-
const paramsFile =
|
|
26051
|
-
|
|
26108
|
+
const paramsDir = mkdtempSync(join5(tmpdir2(), "gdharness-params-"));
|
|
26109
|
+
const paramsFile = join5(paramsDir, `${operation}.json`);
|
|
26110
|
+
writeFileSync3(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
|
|
26052
26111
|
const args = [
|
|
26053
26112
|
"--headless",
|
|
26054
26113
|
"--path",
|
|
@@ -26078,7 +26137,7 @@ async function runOperation(engine, operation, params, projectPath) {
|
|
|
26078
26137
|
messages: []
|
|
26079
26138
|
};
|
|
26080
26139
|
} finally {
|
|
26081
|
-
|
|
26140
|
+
rmSync2(paramsDir, { recursive: true, force: true });
|
|
26082
26141
|
}
|
|
26083
26142
|
const payload = lastJsonObject(stdout);
|
|
26084
26143
|
if (payload === null) {
|
|
@@ -26373,7 +26432,7 @@ function parseJUnit(xml) {
|
|
|
26373
26432
|
import { realpathSync } from "node:fs";
|
|
26374
26433
|
import { readFile, realpath } from "node:fs/promises";
|
|
26375
26434
|
import { createConnection as createConnection2 } from "node:net";
|
|
26376
|
-
import { dirname, resolve as resolve2 } from "node:path";
|
|
26435
|
+
import { dirname as dirname2, resolve as resolve2 } from "node:path";
|
|
26377
26436
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
26378
26437
|
|
|
26379
26438
|
// src/paths.ts
|
|
@@ -26551,7 +26610,7 @@ class GodotLSPClient {
|
|
|
26551
26610
|
if (this.initialized) {
|
|
26552
26611
|
return;
|
|
26553
26612
|
}
|
|
26554
|
-
const rootPath = this.rootPath ??
|
|
26613
|
+
const rootPath = this.rootPath ?? dirname2(resolve2(filePath));
|
|
26555
26614
|
await this.initialize(rootPath);
|
|
26556
26615
|
}
|
|
26557
26616
|
async sendRequest(method, params) {
|
|
@@ -26919,7 +26978,7 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
26919
26978
|
|
|
26920
26979
|
// src/project-scan.ts
|
|
26921
26980
|
import { readdirSync as readdirSync3, readFileSync as readFileSync4 } from "node:fs";
|
|
26922
|
-
import { join as
|
|
26981
|
+
import { join as join6 } from "node:path";
|
|
26923
26982
|
var SKIPPED = new Set([".git", ".godot", ".import", "node_modules"]);
|
|
26924
26983
|
var ASSET_EXTENSIONS = new Set(["png", "jpg", "jpeg", "webp", "svg", "ttf", "otf", "wav", "mp3", "ogg"]);
|
|
26925
26984
|
function projectStructure(projectPath) {
|
|
@@ -26930,7 +26989,7 @@ function projectStructure(projectPath) {
|
|
|
26930
26989
|
continue;
|
|
26931
26990
|
}
|
|
26932
26991
|
if (entry.isDirectory()) {
|
|
26933
|
-
visit(
|
|
26992
|
+
visit(join6(directory, entry.name));
|
|
26934
26993
|
} else if (entry.isFile()) {
|
|
26935
26994
|
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
26936
26995
|
if (extension === "tscn") {
|
|
@@ -26972,7 +27031,7 @@ function searchProject(projectPath, options) {
|
|
|
26972
27031
|
if (SKIPPED.has(entry.name)) {
|
|
26973
27032
|
continue;
|
|
26974
27033
|
}
|
|
26975
|
-
const entryPath =
|
|
27034
|
+
const entryPath = join6(directory, entry.name);
|
|
26976
27035
|
if (entry.isDirectory()) {
|
|
26977
27036
|
visit(entryPath);
|
|
26978
27037
|
continue;
|
|
@@ -27226,7 +27285,7 @@ function setupResourceHandlers(mcp, getProjectPath) {
|
|
|
27226
27285
|
import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync6, unlinkSync } from "node:fs";
|
|
27227
27286
|
import { createConnection as createConnection3 } from "node:net";
|
|
27228
27287
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
27229
|
-
import { join as
|
|
27288
|
+
import { join as join7, resolve as resolve4 } from "node:path";
|
|
27230
27289
|
|
|
27231
27290
|
// src/tool-args.ts
|
|
27232
27291
|
function asParams(value) {
|
|
@@ -27288,13 +27347,13 @@ function runtimeDirectory(variables = process.env) {
|
|
|
27288
27347
|
return explicit;
|
|
27289
27348
|
}
|
|
27290
27349
|
const perUser = envValue("XDG_RUNTIME_DIR", variables);
|
|
27291
|
-
return
|
|
27350
|
+
return join7(perUser ?? tmpdir3(), "gdharness");
|
|
27292
27351
|
}
|
|
27293
27352
|
function runtimeDirectories(variables = process.env) {
|
|
27294
27353
|
const candidates = [runtimeDirectory(variables)];
|
|
27295
27354
|
const fallbacks = process.platform === "win32" ? [envValue("SystemRoot", variables) ?? envValue("windir", variables) ?? "C:\\Windows"] : ["/tmp", "/var/tmp"];
|
|
27296
27355
|
for (const base of fallbacks) {
|
|
27297
|
-
candidates.push(
|
|
27356
|
+
candidates.push(join7(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
|
|
27298
27357
|
}
|
|
27299
27358
|
return [...new Set(candidates.map((path) => resolve4(path)))];
|
|
27300
27359
|
}
|
|
@@ -27357,7 +27416,7 @@ function announcedIn(directory) {
|
|
|
27357
27416
|
if (!match) {
|
|
27358
27417
|
continue;
|
|
27359
27418
|
}
|
|
27360
|
-
const file =
|
|
27419
|
+
const file = join7(directory, entry);
|
|
27361
27420
|
const pid = Number.parseInt(match[1] ?? "", 10);
|
|
27362
27421
|
const announced = processAlive(pid) ? parseAnnouncement(file, pid) : { kind: "rubbish" };
|
|
27363
27422
|
if (announced.kind === "rubbish") {
|
|
@@ -28251,7 +28310,7 @@ var TOOL_SPECS = [
|
|
|
28251
28310
|
},
|
|
28252
28311
|
{
|
|
28253
28312
|
name: "runtime_input",
|
|
28254
|
-
description: "Input to the running game: a whole click on a Control named by path, or a raw action, key, mouse button or mouse motion. All of it works headless, where the window is 64 by 64 and the GUI only takes what is inside it.",
|
|
28313
|
+
description: "Input to the running game: a whole click on a Control named by path, typing into whatever has the focus, or a raw action, key, mouse button or mouse motion. All of it works headless, where the window is 64 by 64 and the GUI only takes what is inside it.",
|
|
28255
28314
|
parameters: {
|
|
28256
28315
|
projectPath: RUNNING_PROJECT_PATH,
|
|
28257
28316
|
nodePath: { type: "string", description: "click: the Control to click, at its centre." },
|
|
@@ -28259,6 +28318,10 @@ var TOOL_SPECS = [
|
|
|
28259
28318
|
pressed: { type: "boolean", description: "Press or release. Default true." },
|
|
28260
28319
|
strength: { type: "number", description: "action: 0 to 1. Default 1." },
|
|
28261
28320
|
keycode: { type: "string", description: 'key: the key name, such as "Space" or "A".' },
|
|
28321
|
+
text: {
|
|
28322
|
+
type: "string",
|
|
28323
|
+
description: "text: what to type. A newline is Enter and a tab is Tab."
|
|
28324
|
+
},
|
|
28262
28325
|
shift: { type: "boolean" },
|
|
28263
28326
|
ctrl: { type: "boolean" },
|
|
28264
28327
|
alt: { type: "boolean" },
|
|
@@ -28281,6 +28344,10 @@ var TOOL_SPECS = [
|
|
|
28281
28344
|
},
|
|
28282
28345
|
action: { summary: "press or release an action", requires: ["action"] },
|
|
28283
28346
|
key: { summary: "press or release a key", requires: ["keycode"] },
|
|
28347
|
+
text: {
|
|
28348
|
+
summary: "type a string wherever the focus is, a character at a time",
|
|
28349
|
+
requires: ["text"]
|
|
28350
|
+
},
|
|
28284
28351
|
mouse_click: { summary: "one mouse button event at a position", requires: ["x", "y"] },
|
|
28285
28352
|
mouse_motion: { summary: "move the mouse to a position", requires: ["x", "y"] }
|
|
28286
28353
|
}
|
|
@@ -28405,7 +28472,7 @@ function buildToolDefinitions() {
|
|
|
28405
28472
|
var UPDATE_NOTICE_EVERY = 500;
|
|
28406
28473
|
var FEEDBACK_NOTICE_EVERY = 250;
|
|
28407
28474
|
var run3 = promisify3(execFile3);
|
|
28408
|
-
var __dirname2 =
|
|
28475
|
+
var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
28409
28476
|
var EDITOR_RESTART_TIMEOUT_MS = 90000;
|
|
28410
28477
|
var BRIDGE_RETRY_MS = 2000;
|
|
28411
28478
|
var PATH_SOLUTIONS = [
|
|
@@ -28503,7 +28570,7 @@ function camelCased(params) {
|
|
|
28503
28570
|
class GodotServer {
|
|
28504
28571
|
mcp;
|
|
28505
28572
|
locator = new GodotLocator;
|
|
28506
|
-
operationsScript =
|
|
28573
|
+
operationsScript = join8(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
28507
28574
|
godotBridge;
|
|
28508
28575
|
tools = buildToolDefinitions();
|
|
28509
28576
|
activeProcess = null;
|
|
@@ -28517,8 +28584,11 @@ class GodotServer {
|
|
|
28517
28584
|
bridgeRetry = null;
|
|
28518
28585
|
lastProjectPath = null;
|
|
28519
28586
|
shutdownInitiated = false;
|
|
28587
|
+
ownProject;
|
|
28588
|
+
announcedAt = null;
|
|
28520
28589
|
constructor() {
|
|
28521
|
-
this.
|
|
28590
|
+
this.ownProject = envValue("GDHARNESS_PROJECT") ?? null;
|
|
28591
|
+
this.godotBridge = getDefaultBridge(this.ownProject !== null);
|
|
28522
28592
|
this.mcp = new McpServer({ name: "gdharness", version: SERVER_VERSION }, { capabilities: { tools: {}, resources: {} } });
|
|
28523
28593
|
this.setupToolHandlers();
|
|
28524
28594
|
setupResourceHandlers(this.mcp, () => this.lastProjectPath);
|
|
@@ -28566,6 +28636,7 @@ class GodotServer {
|
|
|
28566
28636
|
this.bridgeStartupError = null;
|
|
28567
28637
|
const bridgeStatus = this.godotBridge.getStatus();
|
|
28568
28638
|
console.error(`[SERVER] Godot Editor Bridge started on ${bridgeStatus.host}:${bridgeStatus.port}`);
|
|
28639
|
+
this.announceTheBridge();
|
|
28569
28640
|
} catch (bridgeError) {
|
|
28570
28641
|
const code = bridgeError instanceof Error && "code" in bridgeError && typeof bridgeError.code === "string" ? bridgeError.code : null;
|
|
28571
28642
|
const reason = errorMessage(bridgeError);
|
|
@@ -28598,6 +28669,18 @@ class GodotServer {
|
|
|
28598
28669
|
this.stopTryingTheBridge();
|
|
28599
28670
|
const bridgeStatus = this.godotBridge.getStatus();
|
|
28600
28671
|
console.error(`[SERVER] Godot Editor Bridge came up on ${bridgeStatus.host}:${bridgeStatus.port}; editor tools are live.`);
|
|
28672
|
+
this.announceTheBridge();
|
|
28673
|
+
}
|
|
28674
|
+
announceTheBridge() {
|
|
28675
|
+
if (this.ownProject === null) {
|
|
28676
|
+
return;
|
|
28677
|
+
}
|
|
28678
|
+
const status = this.godotBridge.getStatus();
|
|
28679
|
+
this.announcedAt = announceBridge(this.ownProject, {
|
|
28680
|
+
host: status.host,
|
|
28681
|
+
port: status.port,
|
|
28682
|
+
version: SERVER_VERSION
|
|
28683
|
+
});
|
|
28601
28684
|
}
|
|
28602
28685
|
stopTryingTheBridge() {
|
|
28603
28686
|
if (this.bridgeRetry !== null) {
|
|
@@ -28608,6 +28691,8 @@ class GodotServer {
|
|
|
28608
28691
|
async cleanup() {
|
|
28609
28692
|
this.logDebug("Cleaning up resources");
|
|
28610
28693
|
this.stopTryingTheBridge();
|
|
28694
|
+
withdrawBridge(this.announcedAt);
|
|
28695
|
+
this.announcedAt = null;
|
|
28611
28696
|
if (this.activeProcess) {
|
|
28612
28697
|
this.activeProcess.process?.kill();
|
|
28613
28698
|
this.activeProcess = null;
|
|
@@ -28990,7 +29075,7 @@ class GodotServer {
|
|
|
28990
29075
|
if (path === undefined) {
|
|
28991
29076
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
28992
29077
|
}
|
|
28993
|
-
const file =
|
|
29078
|
+
const file = join8(path, "project.godot");
|
|
28994
29079
|
if (!existsSync6(file)) {
|
|
28995
29080
|
return {
|
|
28996
29081
|
ok: false,
|
|
@@ -29255,7 +29340,7 @@ class GodotServer {
|
|
|
29255
29340
|
return contained.response;
|
|
29256
29341
|
}
|
|
29257
29342
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
29258
|
-
if (!existsSync6(
|
|
29343
|
+
if (!existsSync6(join8(project.value.path, runner))) {
|
|
29259
29344
|
return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
|
|
29260
29345
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
29261
29346
|
]);
|
|
@@ -29288,7 +29373,7 @@ class GodotServer {
|
|
|
29288
29373
|
];
|
|
29289
29374
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
|
|
29290
29375
|
this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
29291
|
-
const userData = mkdtempSync2(
|
|
29376
|
+
const userData = mkdtempSync2(join8(tmpdir4(), "gdharness-tests-"));
|
|
29292
29377
|
const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
|
|
29293
29378
|
const hung = await new Promise((resolve) => {
|
|
29294
29379
|
const timer = setTimeout(() => {
|
|
@@ -29304,20 +29389,20 @@ class GodotServer {
|
|
|
29304
29389
|
resolve(false);
|
|
29305
29390
|
});
|
|
29306
29391
|
});
|
|
29307
|
-
const reportsDir =
|
|
29392
|
+
const reportsDir = join8(project.value.path, ".godot", "gdharness-reports");
|
|
29308
29393
|
let report = null;
|
|
29309
29394
|
let reportProblem = null;
|
|
29310
29395
|
try {
|
|
29311
29396
|
const written = existsSync6(reportsDir) ? readdirSync5(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
29312
29397
|
const newest = written.at(-1);
|
|
29313
29398
|
if (newest !== undefined) {
|
|
29314
|
-
report = parseJUnit(readFileSync7(
|
|
29399
|
+
report = parseJUnit(readFileSync7(join8(reportsDir, newest, "results.xml"), "utf8"));
|
|
29315
29400
|
}
|
|
29316
29401
|
} catch (error) {
|
|
29317
29402
|
reportProblem = errorMessage(error);
|
|
29318
29403
|
} finally {
|
|
29319
|
-
|
|
29320
|
-
|
|
29404
|
+
rmSync3(reportsDir, { recursive: true, force: true });
|
|
29405
|
+
rmSync3(userData, { recursive: true, force: true });
|
|
29321
29406
|
}
|
|
29322
29407
|
const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries;
|
|
29323
29408
|
const verdicts = {
|
|
@@ -29430,6 +29515,7 @@ class GodotServer {
|
|
|
29430
29515
|
startupError: this.bridgeStartupError,
|
|
29431
29516
|
staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
|
|
29432
29517
|
retryingBridge: this.bridgeRetry === null ? undefined : true,
|
|
29518
|
+
announcedAt: this.announcedAt ?? undefined,
|
|
29433
29519
|
note: isPortConflict ? "Bridge port is already in use. Another gdharness instance owns the editor bridge, so this server cannot reach the editor. Usually the server this one replaced, still on its way out." : undefined,
|
|
29434
29520
|
suggestion: isPortConflict ? `This server is asking for the port again every ${BRIDGE_RETRY_MS / 1000}s and takes it the moment the other one lets go, so editor tools come back on their own. Ask again, or end the older gdharness process to have it now.` : undefined
|
|
29435
29521
|
};
|
|
@@ -29825,8 +29911,8 @@ class GodotServer {
|
|
|
29825
29911
|
return this.createErrorResponse(choice.problem);
|
|
29826
29912
|
}
|
|
29827
29913
|
const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
|
|
29828
|
-
const screenshotDir = expectsScreenshot ? mkdtempSync2(
|
|
29829
|
-
const screenshotPath = screenshotDir ?
|
|
29914
|
+
const screenshotDir = expectsScreenshot ? mkdtempSync2(join8(tmpdir4(), "gdharness-runtime-screenshot-")) : null;
|
|
29915
|
+
const screenshotPath = screenshotDir ? join8(screenshotDir, "capture.png") : null;
|
|
29830
29916
|
try {
|
|
29831
29917
|
const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs);
|
|
29832
29918
|
if (!reply.ok) {
|
|
@@ -29852,7 +29938,7 @@ class GodotServer {
|
|
|
29852
29938
|
};
|
|
29853
29939
|
} finally {
|
|
29854
29940
|
if (screenshotDir) {
|
|
29855
|
-
|
|
29941
|
+
rmSync3(screenshotDir, { recursive: true, force: true });
|
|
29856
29942
|
}
|
|
29857
29943
|
}
|
|
29858
29944
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.9",
|
|
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",
|