gdharness 0.5.8 → 0.5.10
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 +240 -82
- package/build/godot/addons/gdharness_editor/bridge_client.gd +136 -1
- package/build/index.js +217 -74
- 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
|
|
10129
|
+
import { existsSync as existsSync7, mkdtempSync as mkdtempSync2, readdirSync as readdirSync5, readFileSync as readFileSync8, realpathSync as realpathSync2, rmSync as rmSync4 } from "node:fs";
|
|
10130
10130
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
10131
|
-
import { basename, dirname as
|
|
10131
|
+
import { basename, dirname as dirname4, join as join9, 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
|
}
|
|
@@ -24840,6 +24878,10 @@ var DEFAULT_DAP_PORT = 6006;
|
|
|
24840
24878
|
var DAP_REQUEST_TIMEOUT_MS = 1e4;
|
|
24841
24879
|
var SCOPES_TIMEOUT_MS = 20000;
|
|
24842
24880
|
var VARIABLES_POLL_TIMEOUT_MS = 2000;
|
|
24881
|
+
function said(body, field) {
|
|
24882
|
+
const value = body?.[field];
|
|
24883
|
+
return typeof value === "string" ? value : "";
|
|
24884
|
+
}
|
|
24843
24885
|
|
|
24844
24886
|
class GodotDAPClient {
|
|
24845
24887
|
socket = null;
|
|
@@ -24854,7 +24896,7 @@ class GodotDAPClient {
|
|
|
24854
24896
|
initialized = false;
|
|
24855
24897
|
attached = false;
|
|
24856
24898
|
lastThreadId = 1;
|
|
24857
|
-
|
|
24899
|
+
halt = null;
|
|
24858
24900
|
breakpoints = new Map;
|
|
24859
24901
|
constructor(port = portFromEnv("GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT), host = "127.0.0.1") {
|
|
24860
24902
|
this.port = port;
|
|
@@ -24903,7 +24945,7 @@ class GodotDAPClient {
|
|
|
24903
24945
|
this.connected = false;
|
|
24904
24946
|
this.initialized = false;
|
|
24905
24947
|
this.attached = false;
|
|
24906
|
-
this.
|
|
24948
|
+
this.halt = null;
|
|
24907
24949
|
this.socket = null;
|
|
24908
24950
|
this.failPendingRequests(new Error("DAP connection closed"));
|
|
24909
24951
|
});
|
|
@@ -24923,7 +24965,7 @@ class GodotDAPClient {
|
|
|
24923
24965
|
this.connected = false;
|
|
24924
24966
|
this.initialized = false;
|
|
24925
24967
|
this.attached = false;
|
|
24926
|
-
this.
|
|
24968
|
+
this.halt = null;
|
|
24927
24969
|
return;
|
|
24928
24970
|
}
|
|
24929
24971
|
if (this.connected) {
|
|
@@ -24952,7 +24994,7 @@ class GodotDAPClient {
|
|
|
24952
24994
|
this.connected = false;
|
|
24953
24995
|
this.initialized = false;
|
|
24954
24996
|
this.attached = false;
|
|
24955
|
-
this.
|
|
24997
|
+
this.halt = null;
|
|
24956
24998
|
}
|
|
24957
24999
|
async ensureConnected() {
|
|
24958
25000
|
if (!this.connected) {
|
|
@@ -25029,12 +25071,16 @@ class GodotDAPClient {
|
|
|
25029
25071
|
if (typeof threadId === "number") {
|
|
25030
25072
|
this.lastThreadId = threadId;
|
|
25031
25073
|
}
|
|
25032
|
-
this.
|
|
25074
|
+
this.halt = {
|
|
25075
|
+
reason: said(body, "reason"),
|
|
25076
|
+
description: said(body, "description"),
|
|
25077
|
+
text: said(body, "text")
|
|
25078
|
+
};
|
|
25033
25079
|
return;
|
|
25034
25080
|
}
|
|
25035
25081
|
if (eventName === "terminated" || eventName === "exited") {
|
|
25036
25082
|
this.attached = false;
|
|
25037
|
-
this.
|
|
25083
|
+
this.halt = null;
|
|
25038
25084
|
}
|
|
25039
25085
|
}
|
|
25040
25086
|
async initialize() {
|
|
@@ -25108,7 +25154,7 @@ class GodotDAPClient {
|
|
|
25108
25154
|
await this.attach();
|
|
25109
25155
|
const resolvedThreadId = await this.resolveThreadId(threadId);
|
|
25110
25156
|
await this.sendRequest("continue", { threadId: resolvedThreadId });
|
|
25111
|
-
this.
|
|
25157
|
+
this.halt = null;
|
|
25112
25158
|
}
|
|
25113
25159
|
async stepOver(threadId) {
|
|
25114
25160
|
await this.attach();
|
|
@@ -25179,7 +25225,10 @@ class GodotDAPClient {
|
|
|
25179
25225
|
return this.connected;
|
|
25180
25226
|
}
|
|
25181
25227
|
isStopped() {
|
|
25182
|
-
return this.
|
|
25228
|
+
return this.halt !== null;
|
|
25229
|
+
}
|
|
25230
|
+
whereItStopped() {
|
|
25231
|
+
return this.halt;
|
|
25183
25232
|
}
|
|
25184
25233
|
async resolveThreadId(threadId) {
|
|
25185
25234
|
if (typeof threadId === "number" && threadId > 0) {
|
|
@@ -25208,7 +25257,7 @@ class GodotDAPClient {
|
|
|
25208
25257
|
this.connected = false;
|
|
25209
25258
|
this.initialized = false;
|
|
25210
25259
|
this.attached = false;
|
|
25211
|
-
this.
|
|
25260
|
+
this.halt = null;
|
|
25212
25261
|
this.reader = new FrameReader;
|
|
25213
25262
|
socket?.destroy();
|
|
25214
25263
|
this.failPendingRequests(new Error(`Godot DAP ${detail}. The connection was dropped.`));
|
|
@@ -25364,6 +25413,16 @@ class GameLog {
|
|
|
25364
25413
|
this.entries.push(entry);
|
|
25365
25414
|
this.lastHeadline = headline ? { index: entry.index, detail } : null;
|
|
25366
25415
|
}
|
|
25416
|
+
record(severity, text) {
|
|
25417
|
+
this.entries.push({
|
|
25418
|
+
index: this.entries.length,
|
|
25419
|
+
severity,
|
|
25420
|
+
source: "debugger",
|
|
25421
|
+
text,
|
|
25422
|
+
detail: []
|
|
25423
|
+
});
|
|
25424
|
+
this.lastHeadline = null;
|
|
25425
|
+
}
|
|
25367
25426
|
get all() {
|
|
25368
25427
|
return this.entries;
|
|
25369
25428
|
}
|
|
@@ -25426,19 +25485,37 @@ class GodotBridge extends EventEmitter {
|
|
|
25426
25485
|
connectionInfo = null;
|
|
25427
25486
|
pendingRequests = new Map;
|
|
25428
25487
|
resourceQueues = new Map;
|
|
25429
|
-
|
|
25488
|
+
wantedPort;
|
|
25489
|
+
boundPort = null;
|
|
25430
25490
|
host;
|
|
25431
25491
|
timeoutMs;
|
|
25432
|
-
|
|
25492
|
+
mayMove;
|
|
25493
|
+
constructor(port = DEFAULT_PORT, host = DEFAULT_HOST, timeoutMs = DEFAULT_TIMEOUT_MS, mayMove = false) {
|
|
25433
25494
|
super();
|
|
25434
|
-
this.
|
|
25495
|
+
this.wantedPort = port;
|
|
25435
25496
|
this.host = host;
|
|
25436
25497
|
this.timeoutMs = timeoutMs;
|
|
25498
|
+
this.mayMove = mayMove;
|
|
25499
|
+
}
|
|
25500
|
+
get port() {
|
|
25501
|
+
return this.boundPort ?? this.wantedPort;
|
|
25437
25502
|
}
|
|
25438
|
-
start() {
|
|
25503
|
+
async start() {
|
|
25439
25504
|
if (this.httpServer) {
|
|
25440
|
-
return
|
|
25505
|
+
return;
|
|
25506
|
+
}
|
|
25507
|
+
try {
|
|
25508
|
+
await this.listenOn(this.wantedPort);
|
|
25509
|
+
} catch (error) {
|
|
25510
|
+
const held = error instanceof Error && "code" in error && error.code === "EADDRINUSE";
|
|
25511
|
+
if (!held || !this.mayMove) {
|
|
25512
|
+
throw error;
|
|
25513
|
+
}
|
|
25514
|
+
this.log("warn", `Editor bridge port ${this.wantedPort} is held; taking another one.`);
|
|
25515
|
+
await this.listenOn(0);
|
|
25441
25516
|
}
|
|
25517
|
+
}
|
|
25518
|
+
listenOn(port) {
|
|
25442
25519
|
return new Promise((resolve, reject) => {
|
|
25443
25520
|
const server = http.createServer((_req, res) => {
|
|
25444
25521
|
res.writeHead(404);
|
|
@@ -25462,6 +25539,8 @@ class GodotBridge extends EventEmitter {
|
|
|
25462
25539
|
settled = true;
|
|
25463
25540
|
this.httpServer = server;
|
|
25464
25541
|
this.godotWss = godotWss;
|
|
25542
|
+
const bound = server.address();
|
|
25543
|
+
this.boundPort = typeof bound === "object" && bound !== null ? bound.port : port;
|
|
25465
25544
|
this.log("info", `Editor bridge listening on ${this.host}:${this.port}`);
|
|
25466
25545
|
resolve();
|
|
25467
25546
|
});
|
|
@@ -25478,7 +25557,7 @@ class GodotBridge extends EventEmitter {
|
|
|
25478
25557
|
godotWss.on("error", (error) => {
|
|
25479
25558
|
this.log("error", `Godot WebSocket server error: ${error.message}`);
|
|
25480
25559
|
});
|
|
25481
|
-
server.listen(
|
|
25560
|
+
server.listen(port, this.host);
|
|
25482
25561
|
});
|
|
25483
25562
|
}
|
|
25484
25563
|
async stop() {
|
|
@@ -25512,6 +25591,7 @@ class GodotBridge extends EventEmitter {
|
|
|
25512
25591
|
this.httpServer = null;
|
|
25513
25592
|
}
|
|
25514
25593
|
await Promise.all(closeTasks);
|
|
25594
|
+
this.boundPort = null;
|
|
25515
25595
|
this.connectionInfo = null;
|
|
25516
25596
|
this.log("info", "WebSocket bridge stopped");
|
|
25517
25597
|
}
|
|
@@ -25813,8 +25893,8 @@ class GodotBridge extends EventEmitter {
|
|
|
25813
25893
|
}
|
|
25814
25894
|
}
|
|
25815
25895
|
var defaultBridge = null;
|
|
25816
|
-
function getDefaultBridge() {
|
|
25817
|
-
defaultBridge ??= new GodotBridge(resolveDefaultBridgePort(), resolveDefaultBridgeHost());
|
|
25896
|
+
function getDefaultBridge(mayMove = false) {
|
|
25897
|
+
defaultBridge ??= new GodotBridge(resolveDefaultBridgePort(), resolveDefaultBridgeHost(), DEFAULT_TIMEOUT_MS, mayMove);
|
|
25818
25898
|
return defaultBridge;
|
|
25819
25899
|
}
|
|
25820
25900
|
|
|
@@ -25827,7 +25907,7 @@ import { promisify } from "node:util";
|
|
|
25827
25907
|
// src/detection.ts
|
|
25828
25908
|
import { existsSync as existsSync3, readdirSync as readdirSync2, statSync } from "node:fs";
|
|
25829
25909
|
import { homedir as homedir2 } from "node:os";
|
|
25830
|
-
import { join as
|
|
25910
|
+
import { join as join4 } from "node:path";
|
|
25831
25911
|
function resolveHomeDirectory() {
|
|
25832
25912
|
try {
|
|
25833
25913
|
return homedir2();
|
|
@@ -25851,7 +25931,7 @@ function scanDirectoryForGodotBinaries(directory, platform) {
|
|
|
25851
25931
|
if (!pattern.test(name)) {
|
|
25852
25932
|
continue;
|
|
25853
25933
|
}
|
|
25854
|
-
const fullPath =
|
|
25934
|
+
const fullPath = join4(directory, name);
|
|
25855
25935
|
try {
|
|
25856
25936
|
const stat = statSync(fullPath);
|
|
25857
25937
|
if (stat.isFile()) {
|
|
@@ -25860,7 +25940,7 @@ function scanDirectoryForGodotBinaries(directory, platform) {
|
|
|
25860
25940
|
} catch {}
|
|
25861
25941
|
}
|
|
25862
25942
|
matches.sort((a, b) => b.mtime - a.mtime);
|
|
25863
|
-
return matches.map((m) =>
|
|
25943
|
+
return matches.map((m) => join4(directory, m.name));
|
|
25864
25944
|
}
|
|
25865
25945
|
function conventionalPaths(platform, home) {
|
|
25866
25946
|
const paths = ["godot"];
|
|
@@ -26003,9 +26083,9 @@ class GodotLocator {
|
|
|
26003
26083
|
|
|
26004
26084
|
// src/headless.ts
|
|
26005
26085
|
import { execFile as execFile2 } from "node:child_process";
|
|
26006
|
-
import { mkdtempSync, rmSync, writeFileSync as
|
|
26086
|
+
import { mkdtempSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
26007
26087
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
26008
|
-
import { join as
|
|
26088
|
+
import { join as join5 } from "node:path";
|
|
26009
26089
|
import { promisify as promisify2 } from "node:util";
|
|
26010
26090
|
var run2 = promisify2(execFile2);
|
|
26011
26091
|
function snakeCased(params) {
|
|
@@ -26046,9 +26126,9 @@ function reason(stdout, stderr) {
|
|
|
26046
26126
|
return stdout.trim().split(/\r?\n/).at(-1) ?? "no output at all";
|
|
26047
26127
|
}
|
|
26048
26128
|
async function runOperation(engine, operation, params, projectPath) {
|
|
26049
|
-
const paramsDir = mkdtempSync(
|
|
26050
|
-
const paramsFile =
|
|
26051
|
-
|
|
26129
|
+
const paramsDir = mkdtempSync(join5(tmpdir2(), "gdharness-params-"));
|
|
26130
|
+
const paramsFile = join5(paramsDir, `${operation}.json`);
|
|
26131
|
+
writeFileSync3(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
|
|
26052
26132
|
const args = [
|
|
26053
26133
|
"--headless",
|
|
26054
26134
|
"--path",
|
|
@@ -26078,7 +26158,7 @@ async function runOperation(engine, operation, params, projectPath) {
|
|
|
26078
26158
|
messages: []
|
|
26079
26159
|
};
|
|
26080
26160
|
} finally {
|
|
26081
|
-
|
|
26161
|
+
rmSync2(paramsDir, { recursive: true, force: true });
|
|
26082
26162
|
}
|
|
26083
26163
|
const payload = lastJsonObject(stdout);
|
|
26084
26164
|
if (payload === null) {
|
|
@@ -26373,7 +26453,7 @@ function parseJUnit(xml) {
|
|
|
26373
26453
|
import { realpathSync } from "node:fs";
|
|
26374
26454
|
import { readFile, realpath } from "node:fs/promises";
|
|
26375
26455
|
import { createConnection as createConnection2 } from "node:net";
|
|
26376
|
-
import { dirname, resolve as resolve2 } from "node:path";
|
|
26456
|
+
import { dirname as dirname2, resolve as resolve2 } from "node:path";
|
|
26377
26457
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
26378
26458
|
|
|
26379
26459
|
// src/paths.ts
|
|
@@ -26551,7 +26631,7 @@ class GodotLSPClient {
|
|
|
26551
26631
|
if (this.initialized) {
|
|
26552
26632
|
return;
|
|
26553
26633
|
}
|
|
26554
|
-
const rootPath = this.rootPath ??
|
|
26634
|
+
const rootPath = this.rootPath ?? dirname2(resolve2(filePath));
|
|
26555
26635
|
await this.initialize(rootPath);
|
|
26556
26636
|
}
|
|
26557
26637
|
async sendRequest(method, params) {
|
|
@@ -26919,7 +26999,7 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
26919
26999
|
|
|
26920
27000
|
// src/project-scan.ts
|
|
26921
27001
|
import { readdirSync as readdirSync3, readFileSync as readFileSync4 } from "node:fs";
|
|
26922
|
-
import { join as
|
|
27002
|
+
import { join as join6 } from "node:path";
|
|
26923
27003
|
var SKIPPED = new Set([".git", ".godot", ".import", "node_modules"]);
|
|
26924
27004
|
var ASSET_EXTENSIONS = new Set(["png", "jpg", "jpeg", "webp", "svg", "ttf", "otf", "wav", "mp3", "ogg"]);
|
|
26925
27005
|
function projectStructure(projectPath) {
|
|
@@ -26930,7 +27010,7 @@ function projectStructure(projectPath) {
|
|
|
26930
27010
|
continue;
|
|
26931
27011
|
}
|
|
26932
27012
|
if (entry.isDirectory()) {
|
|
26933
|
-
visit(
|
|
27013
|
+
visit(join6(directory, entry.name));
|
|
26934
27014
|
} else if (entry.isFile()) {
|
|
26935
27015
|
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
26936
27016
|
if (extension === "tscn") {
|
|
@@ -26972,7 +27052,7 @@ function searchProject(projectPath, options) {
|
|
|
26972
27052
|
if (SKIPPED.has(entry.name)) {
|
|
26973
27053
|
continue;
|
|
26974
27054
|
}
|
|
26975
|
-
const entryPath =
|
|
27055
|
+
const entryPath = join6(directory, entry.name);
|
|
26976
27056
|
if (entry.isDirectory()) {
|
|
26977
27057
|
visit(entryPath);
|
|
26978
27058
|
continue;
|
|
@@ -27226,7 +27306,7 @@ function setupResourceHandlers(mcp, getProjectPath) {
|
|
|
27226
27306
|
import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync6, unlinkSync } from "node:fs";
|
|
27227
27307
|
import { createConnection as createConnection3 } from "node:net";
|
|
27228
27308
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
27229
|
-
import { join as
|
|
27309
|
+
import { join as join7, resolve as resolve4 } from "node:path";
|
|
27230
27310
|
|
|
27231
27311
|
// src/tool-args.ts
|
|
27232
27312
|
function asParams(value) {
|
|
@@ -27288,13 +27368,13 @@ function runtimeDirectory(variables = process.env) {
|
|
|
27288
27368
|
return explicit;
|
|
27289
27369
|
}
|
|
27290
27370
|
const perUser = envValue("XDG_RUNTIME_DIR", variables);
|
|
27291
|
-
return
|
|
27371
|
+
return join7(perUser ?? tmpdir3(), "gdharness");
|
|
27292
27372
|
}
|
|
27293
27373
|
function runtimeDirectories(variables = process.env) {
|
|
27294
27374
|
const candidates = [runtimeDirectory(variables)];
|
|
27295
27375
|
const fallbacks = process.platform === "win32" ? [envValue("SystemRoot", variables) ?? envValue("windir", variables) ?? "C:\\Windows"] : ["/tmp", "/var/tmp"];
|
|
27296
27376
|
for (const base of fallbacks) {
|
|
27297
|
-
candidates.push(
|
|
27377
|
+
candidates.push(join7(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
|
|
27298
27378
|
}
|
|
27299
27379
|
return [...new Set(candidates.map((path) => resolve4(path)))];
|
|
27300
27380
|
}
|
|
@@ -27357,7 +27437,7 @@ function announcedIn(directory) {
|
|
|
27357
27437
|
if (!match) {
|
|
27358
27438
|
continue;
|
|
27359
27439
|
}
|
|
27360
|
-
const file =
|
|
27440
|
+
const file = join7(directory, entry);
|
|
27361
27441
|
const pid = Number.parseInt(match[1] ?? "", 10);
|
|
27362
27442
|
const announced = processAlive(pid) ? parseAnnouncement(file, pid) : { kind: "rubbish" };
|
|
27363
27443
|
if (announced.kind === "rubbish") {
|
|
@@ -27498,6 +27578,20 @@ function runtimeRequest(endpoint, command, params, timeoutMs) {
|
|
|
27498
27578
|
});
|
|
27499
27579
|
}
|
|
27500
27580
|
|
|
27581
|
+
// src/setup.ts
|
|
27582
|
+
import { cpSync, existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync7, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
27583
|
+
import { dirname as dirname3, join as join8 } from "node:path";
|
|
27584
|
+
var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
|
|
27585
|
+
var VERSION_MARKER = ".gdharness-version";
|
|
27586
|
+
function installedAddonVersion(projectPath) {
|
|
27587
|
+
const marker = join8(projectPath, "addons", ADDONS[0], VERSION_MARKER);
|
|
27588
|
+
if (!existsSync6(marker)) {
|
|
27589
|
+
return null;
|
|
27590
|
+
}
|
|
27591
|
+
const said = readFileSync7(marker, "utf8").trim();
|
|
27592
|
+
return said === "" ? null : said;
|
|
27593
|
+
}
|
|
27594
|
+
|
|
27501
27595
|
// src/tool-definitions.ts
|
|
27502
27596
|
var PROJECT_PATH = {
|
|
27503
27597
|
type: "string",
|
|
@@ -28413,7 +28507,7 @@ function buildToolDefinitions() {
|
|
|
28413
28507
|
var UPDATE_NOTICE_EVERY = 500;
|
|
28414
28508
|
var FEEDBACK_NOTICE_EVERY = 250;
|
|
28415
28509
|
var run3 = promisify3(execFile3);
|
|
28416
|
-
var __dirname2 =
|
|
28510
|
+
var __dirname2 = dirname4(fileURLToPath2(import.meta.url));
|
|
28417
28511
|
var EDITOR_RESTART_TIMEOUT_MS = 90000;
|
|
28418
28512
|
var BRIDGE_RETRY_MS = 2000;
|
|
28419
28513
|
var PATH_SOLUTIONS = [
|
|
@@ -28491,11 +28585,11 @@ function realPathOr(path) {
|
|
|
28491
28585
|
}
|
|
28492
28586
|
}
|
|
28493
28587
|
function hasMainScene(projectFile) {
|
|
28494
|
-
const scene = parseProjectGodot(
|
|
28588
|
+
const scene = parseProjectGodot(readFileSync8(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
28495
28589
|
return typeof scene === "string" && scene !== "";
|
|
28496
28590
|
}
|
|
28497
28591
|
function editorPlaysHeadless(projectFile) {
|
|
28498
|
-
const runArgs = parseProjectGodot(
|
|
28592
|
+
const runArgs = parseProjectGodot(readFileSync8(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
28499
28593
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
28500
28594
|
}
|
|
28501
28595
|
function camelCased(params) {
|
|
@@ -28511,7 +28605,7 @@ function camelCased(params) {
|
|
|
28511
28605
|
class GodotServer {
|
|
28512
28606
|
mcp;
|
|
28513
28607
|
locator = new GodotLocator;
|
|
28514
|
-
operationsScript =
|
|
28608
|
+
operationsScript = join9(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
28515
28609
|
godotBridge;
|
|
28516
28610
|
tools = buildToolDefinitions();
|
|
28517
28611
|
activeProcess = null;
|
|
@@ -28525,8 +28619,11 @@ class GodotServer {
|
|
|
28525
28619
|
bridgeRetry = null;
|
|
28526
28620
|
lastProjectPath = null;
|
|
28527
28621
|
shutdownInitiated = false;
|
|
28622
|
+
ownProject;
|
|
28623
|
+
announcedAt = null;
|
|
28528
28624
|
constructor() {
|
|
28529
|
-
this.
|
|
28625
|
+
this.ownProject = envValue("GDHARNESS_PROJECT") ?? null;
|
|
28626
|
+
this.godotBridge = getDefaultBridge(this.ownProject !== null);
|
|
28530
28627
|
this.mcp = new McpServer({ name: "gdharness", version: SERVER_VERSION }, { capabilities: { tools: {}, resources: {} } });
|
|
28531
28628
|
this.setupToolHandlers();
|
|
28532
28629
|
setupResourceHandlers(this.mcp, () => this.lastProjectPath);
|
|
@@ -28574,6 +28671,7 @@ class GodotServer {
|
|
|
28574
28671
|
this.bridgeStartupError = null;
|
|
28575
28672
|
const bridgeStatus = this.godotBridge.getStatus();
|
|
28576
28673
|
console.error(`[SERVER] Godot Editor Bridge started on ${bridgeStatus.host}:${bridgeStatus.port}`);
|
|
28674
|
+
this.announceTheBridge();
|
|
28577
28675
|
} catch (bridgeError) {
|
|
28578
28676
|
const code = bridgeError instanceof Error && "code" in bridgeError && typeof bridgeError.code === "string" ? bridgeError.code : null;
|
|
28579
28677
|
const reason = errorMessage(bridgeError);
|
|
@@ -28606,6 +28704,18 @@ class GodotServer {
|
|
|
28606
28704
|
this.stopTryingTheBridge();
|
|
28607
28705
|
const bridgeStatus = this.godotBridge.getStatus();
|
|
28608
28706
|
console.error(`[SERVER] Godot Editor Bridge came up on ${bridgeStatus.host}:${bridgeStatus.port}; editor tools are live.`);
|
|
28707
|
+
this.announceTheBridge();
|
|
28708
|
+
}
|
|
28709
|
+
announceTheBridge() {
|
|
28710
|
+
if (this.ownProject === null) {
|
|
28711
|
+
return;
|
|
28712
|
+
}
|
|
28713
|
+
const status = this.godotBridge.getStatus();
|
|
28714
|
+
this.announcedAt = announceBridge(this.ownProject, {
|
|
28715
|
+
host: status.host,
|
|
28716
|
+
port: status.port,
|
|
28717
|
+
version: SERVER_VERSION
|
|
28718
|
+
});
|
|
28609
28719
|
}
|
|
28610
28720
|
stopTryingTheBridge() {
|
|
28611
28721
|
if (this.bridgeRetry !== null) {
|
|
@@ -28616,6 +28726,8 @@ class GodotServer {
|
|
|
28616
28726
|
async cleanup() {
|
|
28617
28727
|
this.logDebug("Cleaning up resources");
|
|
28618
28728
|
this.stopTryingTheBridge();
|
|
28729
|
+
withdrawBridge(this.announcedAt);
|
|
28730
|
+
this.announcedAt = null;
|
|
28619
28731
|
if (this.activeProcess) {
|
|
28620
28732
|
this.activeProcess.process?.kill();
|
|
28621
28733
|
this.activeProcess = null;
|
|
@@ -28731,28 +28843,44 @@ class GodotServer {
|
|
|
28731
28843
|
if (this.callsSinceNotice < UPDATE_NOTICE_EVERY && this.noticedUpdate) {
|
|
28732
28844
|
return answer;
|
|
28733
28845
|
}
|
|
28846
|
+
const moved = this.projectHasMovedOn();
|
|
28847
|
+
if (moved !== null) {
|
|
28848
|
+
this.noticedUpdate = true;
|
|
28849
|
+
this.callsSinceNotice = 0;
|
|
28850
|
+
return this.saying(answer, {
|
|
28851
|
+
project_upgraded_under_this_server: {
|
|
28852
|
+
server_is: SERVER_VERSION,
|
|
28853
|
+
project_is: moved,
|
|
28854
|
+
what_to_do: "The project was upgraded while this server has been running, so it is still answering " + "as the version it was started as. Tell the user to reconnect the MCP server, which is " + "the only thing that replaces it: a server cannot restart itself."
|
|
28855
|
+
}
|
|
28856
|
+
});
|
|
28857
|
+
}
|
|
28734
28858
|
const notice = this.updates.notice();
|
|
28735
28859
|
if (notice === null) {
|
|
28736
28860
|
return answer;
|
|
28737
28861
|
}
|
|
28738
28862
|
this.noticedUpdate = true;
|
|
28739
28863
|
this.callsSinceNotice = 0;
|
|
28864
|
+
return this.saying(answer, {
|
|
28865
|
+
update_available: {
|
|
28866
|
+
...notice,
|
|
28867
|
+
what_to_do: "Tell the user a newer gdharness is out, with what changed, and offer to take it. " + "Only run the upgrade command if they say yes: it restarts their editor and the " + "MCP server has to be reconnected afterwards."
|
|
28868
|
+
}
|
|
28869
|
+
});
|
|
28870
|
+
}
|
|
28871
|
+
saying(answer, block) {
|
|
28740
28872
|
return {
|
|
28741
28873
|
...answer,
|
|
28742
|
-
content: [
|
|
28743
|
-
...answer.content,
|
|
28744
|
-
{
|
|
28745
|
-
type: "text",
|
|
28746
|
-
text: JSON.stringify({
|
|
28747
|
-
update_available: {
|
|
28748
|
-
...notice,
|
|
28749
|
-
what_to_do: "Tell the user a newer gdharness is out, with what changed, and offer to take it. " + "Only run the upgrade command if they say yes: it restarts their editor and the " + "MCP server has to be reconnected afterwards."
|
|
28750
|
-
}
|
|
28751
|
-
}, null, 2)
|
|
28752
|
-
}
|
|
28753
|
-
]
|
|
28874
|
+
content: [...answer.content, { type: "text", text: JSON.stringify(block, null, 2) }]
|
|
28754
28875
|
};
|
|
28755
28876
|
}
|
|
28877
|
+
projectHasMovedOn() {
|
|
28878
|
+
if (this.ownProject === null) {
|
|
28879
|
+
return null;
|
|
28880
|
+
}
|
|
28881
|
+
const installed = installedAddonVersion(this.ownProject);
|
|
28882
|
+
return installed === null || installed === SERVER_VERSION ? null : installed;
|
|
28883
|
+
}
|
|
28756
28884
|
withFeedbackNotice(answer) {
|
|
28757
28885
|
this.callsSinceFeedback += 1;
|
|
28758
28886
|
if (this.callsSinceFeedback < FEEDBACK_NOTICE_EVERY) {
|
|
@@ -28998,8 +29126,8 @@ class GodotServer {
|
|
|
28998
29126
|
if (path === undefined) {
|
|
28999
29127
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
29000
29128
|
}
|
|
29001
|
-
const file =
|
|
29002
|
-
if (!
|
|
29129
|
+
const file = join9(path, "project.godot");
|
|
29130
|
+
if (!existsSync7(file)) {
|
|
29003
29131
|
return {
|
|
29004
29132
|
ok: false,
|
|
29005
29133
|
response: this.createErrorResponse(`Not a Godot project: ${path}`, [
|
|
@@ -29144,7 +29272,7 @@ class GodotServer {
|
|
|
29144
29272
|
if (!engine.ok) {
|
|
29145
29273
|
return engine.response;
|
|
29146
29274
|
}
|
|
29147
|
-
const application = parseProjectGodot(
|
|
29275
|
+
const application = parseProjectGodot(readFileSync8(project.value.file, "utf8"))["application"] ?? {};
|
|
29148
29276
|
const name = application["config/name"];
|
|
29149
29277
|
const mainScene = application["run/main_scene"];
|
|
29150
29278
|
const info = {
|
|
@@ -29233,7 +29361,7 @@ class GodotServer {
|
|
|
29233
29361
|
log.finish();
|
|
29234
29362
|
const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
|
|
29235
29363
|
const verdict = {
|
|
29236
|
-
exported: exitCode === 0 && log.count("error") === 0 &&
|
|
29364
|
+
exported: exitCode === 0 && log.count("error") === 0 && existsSync7(output.absolutePath),
|
|
29237
29365
|
preset,
|
|
29238
29366
|
outputPath: output.relativePath,
|
|
29239
29367
|
debug,
|
|
@@ -29263,7 +29391,7 @@ class GodotServer {
|
|
|
29263
29391
|
return contained.response;
|
|
29264
29392
|
}
|
|
29265
29393
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
29266
|
-
if (!
|
|
29394
|
+
if (!existsSync7(join9(project.value.path, runner))) {
|
|
29267
29395
|
return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
|
|
29268
29396
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
29269
29397
|
]);
|
|
@@ -29296,7 +29424,7 @@ class GodotServer {
|
|
|
29296
29424
|
];
|
|
29297
29425
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
|
|
29298
29426
|
this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
29299
|
-
const userData = mkdtempSync2(
|
|
29427
|
+
const userData = mkdtempSync2(join9(tmpdir4(), "gdharness-tests-"));
|
|
29300
29428
|
const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
|
|
29301
29429
|
const hung = await new Promise((resolve) => {
|
|
29302
29430
|
const timer = setTimeout(() => {
|
|
@@ -29312,20 +29440,20 @@ class GodotServer {
|
|
|
29312
29440
|
resolve(false);
|
|
29313
29441
|
});
|
|
29314
29442
|
});
|
|
29315
|
-
const reportsDir =
|
|
29443
|
+
const reportsDir = join9(project.value.path, ".godot", "gdharness-reports");
|
|
29316
29444
|
let report = null;
|
|
29317
29445
|
let reportProblem = null;
|
|
29318
29446
|
try {
|
|
29319
|
-
const written =
|
|
29447
|
+
const written = existsSync7(reportsDir) ? readdirSync5(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
29320
29448
|
const newest = written.at(-1);
|
|
29321
29449
|
if (newest !== undefined) {
|
|
29322
|
-
report = parseJUnit(
|
|
29450
|
+
report = parseJUnit(readFileSync8(join9(reportsDir, newest, "results.xml"), "utf8"));
|
|
29323
29451
|
}
|
|
29324
29452
|
} catch (error) {
|
|
29325
29453
|
reportProblem = errorMessage(error);
|
|
29326
29454
|
} finally {
|
|
29327
|
-
|
|
29328
|
-
|
|
29455
|
+
rmSync4(reportsDir, { recursive: true, force: true });
|
|
29456
|
+
rmSync4(userData, { recursive: true, force: true });
|
|
29329
29457
|
}
|
|
29330
29458
|
const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries;
|
|
29331
29459
|
const verdicts = {
|
|
@@ -29438,6 +29566,8 @@ class GodotServer {
|
|
|
29438
29566
|
startupError: this.bridgeStartupError,
|
|
29439
29567
|
staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
|
|
29440
29568
|
retryingBridge: this.bridgeRetry === null ? undefined : true,
|
|
29569
|
+
announcedAt: this.announcedAt ?? undefined,
|
|
29570
|
+
projectIs: this.projectHasMovedOn() ?? undefined,
|
|
29441
29571
|
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,
|
|
29442
29572
|
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
|
|
29443
29573
|
};
|
|
@@ -29650,7 +29780,8 @@ class GodotServer {
|
|
|
29650
29780
|
log,
|
|
29651
29781
|
startedAt: Date.now(),
|
|
29652
29782
|
exitCode: null,
|
|
29653
|
-
throughEditor: true
|
|
29783
|
+
throughEditor: true,
|
|
29784
|
+
brokeOn: null
|
|
29654
29785
|
};
|
|
29655
29786
|
this.activeProcess = played;
|
|
29656
29787
|
return this.jsonTextResponse({
|
|
@@ -29670,6 +29801,15 @@ class GodotServer {
|
|
|
29670
29801
|
`) ? line : `${line}
|
|
29671
29802
|
`);
|
|
29672
29803
|
}
|
|
29804
|
+
this.recordWhatItBrokeOn(game);
|
|
29805
|
+
}
|
|
29806
|
+
recordWhatItBrokeOn(game) {
|
|
29807
|
+
const halt = this.dapClient?.whereItStopped() ?? null;
|
|
29808
|
+
if (halt?.reason !== "exception" || halt.text === "" || game.brokeOn === halt.text) {
|
|
29809
|
+
return;
|
|
29810
|
+
}
|
|
29811
|
+
game.brokeOn = halt.text;
|
|
29812
|
+
game.log.record("error", halt.text);
|
|
29673
29813
|
}
|
|
29674
29814
|
async endActiveGame() {
|
|
29675
29815
|
const running = this.activeProcess;
|
|
@@ -29691,7 +29831,8 @@ class GodotServer {
|
|
|
29691
29831
|
log,
|
|
29692
29832
|
startedAt: Date.now(),
|
|
29693
29833
|
exitCode: null,
|
|
29694
|
-
throughEditor: false
|
|
29834
|
+
throughEditor: false,
|
|
29835
|
+
brokeOn: null
|
|
29695
29836
|
};
|
|
29696
29837
|
child.stdout.on("data", (data) => {
|
|
29697
29838
|
log.append("stdout", data);
|
|
@@ -29756,6 +29897,7 @@ class GodotServer {
|
|
|
29756
29897
|
contains: readNonEmptyString(args, "contains"),
|
|
29757
29898
|
limit: readPositiveNumber(args, "limit") ?? 200
|
|
29758
29899
|
});
|
|
29900
|
+
const halt = this.activeProcess.throughEditor ? this.dapClient?.whereItStopped() ?? null : null;
|
|
29759
29901
|
return this.jsonTextResponse({
|
|
29760
29902
|
running: this.activeProcess.exitCode === null,
|
|
29761
29903
|
exitCode: this.activeProcess.exitCode,
|
|
@@ -29764,6 +29906,7 @@ class GodotServer {
|
|
|
29764
29906
|
errors: this.activeProcess.log.count("error"),
|
|
29765
29907
|
warnings: this.activeProcess.log.count("warning"),
|
|
29766
29908
|
clean: this.activeProcess.log.count("error") === 0,
|
|
29909
|
+
heldAt: halt,
|
|
29767
29910
|
omitted: selected.omitted,
|
|
29768
29911
|
entries: selected.entries
|
|
29769
29912
|
});
|
|
@@ -29833,8 +29976,8 @@ class GodotServer {
|
|
|
29833
29976
|
return this.createErrorResponse(choice.problem);
|
|
29834
29977
|
}
|
|
29835
29978
|
const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
|
|
29836
|
-
const screenshotDir = expectsScreenshot ? mkdtempSync2(
|
|
29837
|
-
const screenshotPath = screenshotDir ?
|
|
29979
|
+
const screenshotDir = expectsScreenshot ? mkdtempSync2(join9(tmpdir4(), "gdharness-runtime-screenshot-")) : null;
|
|
29980
|
+
const screenshotPath = screenshotDir ? join9(screenshotDir, "capture.png") : null;
|
|
29838
29981
|
try {
|
|
29839
29982
|
const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs);
|
|
29840
29983
|
if (!reply.ok) {
|
|
@@ -29855,12 +29998,12 @@ class GodotServer {
|
|
|
29855
29998
|
return {
|
|
29856
29999
|
content: [
|
|
29857
30000
|
{ type: "text", text: `Screenshot captured: ${dimensions}` },
|
|
29858
|
-
{ type: "image", data:
|
|
30001
|
+
{ type: "image", data: readFileSync8(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
29859
30002
|
]
|
|
29860
30003
|
};
|
|
29861
30004
|
} finally {
|
|
29862
30005
|
if (screenshotDir) {
|
|
29863
|
-
|
|
30006
|
+
rmSync4(screenshotDir, { recursive: true, force: true });
|
|
29864
30007
|
}
|
|
29865
30008
|
}
|
|
29866
30009
|
}
|