gdharness 0.9.1 → 0.9.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 +182 -39
- package/build/godot/addons/gdharness_editor/bridge_client.gd +63 -27
- package/build/godot/addons/gdharness_editor/tool_executor.gd +1 -0
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +27 -0
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +6 -3
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +8 -6
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +52 -33
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +31 -0
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +8 -6
- package/build/index.js +216 -89
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -10126,7 +10126,7 @@ 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 existsSync7, mkdtempSync as mkdtempSync2, readdirSync as readdirSync5, readFileSync as
|
|
10129
|
+
import { existsSync as existsSync7, mkdtempSync as mkdtempSync2, readdirSync as readdirSync5, readFileSync as readFileSync9, realpathSync as realpathSync2, rmSync as rmSync4 } from "node:fs";
|
|
10130
10130
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
10131
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";
|
|
@@ -24697,12 +24697,30 @@ class StdioServerTransport {
|
|
|
24697
24697
|
}
|
|
24698
24698
|
|
|
24699
24699
|
// src/bridge-announce.ts
|
|
24700
|
-
import { mkdirSync as mkdirSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
24700
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
24701
24701
|
import { dirname, join as join2 } from "node:path";
|
|
24702
24702
|
function announcementPath(projectPath) {
|
|
24703
24703
|
return join2(projectPath, ".godot", "gdharness-bridge.json");
|
|
24704
24704
|
}
|
|
24705
24705
|
var BRIDGE_ANNOUNCE_PROTOCOL = 1;
|
|
24706
|
+
function readAnnouncement(path) {
|
|
24707
|
+
try {
|
|
24708
|
+
const parsed = JSON.parse(readFileSync3(path, "utf8"));
|
|
24709
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
24710
|
+
return null;
|
|
24711
|
+
}
|
|
24712
|
+
const said = parsed;
|
|
24713
|
+
if (typeof said.port !== "number" || typeof said.pid !== "number") {
|
|
24714
|
+
return null;
|
|
24715
|
+
}
|
|
24716
|
+
return said;
|
|
24717
|
+
} catch {
|
|
24718
|
+
return null;
|
|
24719
|
+
}
|
|
24720
|
+
}
|
|
24721
|
+
function announcementIsOurs(path) {
|
|
24722
|
+
return readAnnouncement(path)?.pid === process.pid;
|
|
24723
|
+
}
|
|
24706
24724
|
function announceBridge(projectPath, bridge) {
|
|
24707
24725
|
const path = announcementPath(projectPath);
|
|
24708
24726
|
try {
|
|
@@ -24724,7 +24742,7 @@ function announceBridge(projectPath, bridge) {
|
|
|
24724
24742
|
}
|
|
24725
24743
|
}
|
|
24726
24744
|
function withdrawBridge(path) {
|
|
24727
|
-
if (path === null) {
|
|
24745
|
+
if (path === null || !announcementIsOurs(path)) {
|
|
24728
24746
|
return;
|
|
24729
24747
|
}
|
|
24730
24748
|
try {
|
|
@@ -24735,7 +24753,7 @@ function withdrawBridge(path) {
|
|
|
24735
24753
|
}
|
|
24736
24754
|
|
|
24737
24755
|
// src/class-cache.ts
|
|
24738
|
-
import { existsSync as existsSync2, readdirSync, readFileSync as
|
|
24756
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync4 } from "node:fs";
|
|
24739
24757
|
import { join as join3 } from "node:path";
|
|
24740
24758
|
function declaredClasses(projectPath) {
|
|
24741
24759
|
const declared = new Map;
|
|
@@ -24748,7 +24766,7 @@ function declaredClasses(projectPath) {
|
|
|
24748
24766
|
if (entry.isDirectory()) {
|
|
24749
24767
|
visit(path, `${prefix}${entry.name}/`);
|
|
24750
24768
|
} else if (entry.isFile() && entry.name.endsWith(".gd")) {
|
|
24751
|
-
const found = /^class_name\s+([A-Za-z_][A-Za-z0-9_]*)/m.exec(
|
|
24769
|
+
const found = /^class_name\s+([A-Za-z_][A-Za-z0-9_]*)/m.exec(readFileSync4(path, "utf8"));
|
|
24752
24770
|
if (found?.[1]) {
|
|
24753
24771
|
declared.set(found[1], `res://${prefix}${entry.name}`);
|
|
24754
24772
|
}
|
|
@@ -24764,7 +24782,7 @@ function cachedClasses(projectPath) {
|
|
|
24764
24782
|
return null;
|
|
24765
24783
|
}
|
|
24766
24784
|
const listed = new Map;
|
|
24767
|
-
const text =
|
|
24785
|
+
const text = readFileSync4(cache, "utf8");
|
|
24768
24786
|
for (const entry of text.matchAll(/"class":\s*&"([^"]+)"[\s\S]*?"path":\s*"([^"]+)"/g)) {
|
|
24769
24787
|
listed.set(entry[1] ?? "", entry[2] ?? "");
|
|
24770
24788
|
}
|
|
@@ -25494,12 +25512,64 @@ var import_subprotocol = __toESM(require_subprotocol(), 1);
|
|
|
25494
25512
|
var import_websocket = __toESM(require_websocket(), 1);
|
|
25495
25513
|
var import_websocket_server = __toESM(require_websocket_server(), 1);
|
|
25496
25514
|
|
|
25515
|
+
// src/paths.ts
|
|
25516
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
25517
|
+
var RESOURCE_SCHEME = "res://";
|
|
25518
|
+
var OTHER_SCHEME = /^[a-zA-Z][a-zA-Z\d+.-]+:\/\//;
|
|
25519
|
+
function isSameDirectory(onePath, otherPath) {
|
|
25520
|
+
return relative(resolve(onePath), resolve(otherPath)) === "";
|
|
25521
|
+
}
|
|
25522
|
+
function isWithinRoot(rootPath, candidatePath) {
|
|
25523
|
+
const step = relative(resolve(rootPath), resolve(candidatePath));
|
|
25524
|
+
return step !== ".." && !step.startsWith(`..${sep}`) && !isAbsolute(step);
|
|
25525
|
+
}
|
|
25526
|
+
function resolveWithinProject(projectPath, candidatePath) {
|
|
25527
|
+
const trimmed = candidatePath.trim();
|
|
25528
|
+
if (trimmed === "") {
|
|
25529
|
+
return { ok: false, reason: "Path is empty." };
|
|
25530
|
+
}
|
|
25531
|
+
if (trimmed.includes("\x00")) {
|
|
25532
|
+
return { ok: false, reason: "Path contains a null byte." };
|
|
25533
|
+
}
|
|
25534
|
+
const withoutScheme = trimmed.startsWith(RESOURCE_SCHEME) ? trimmed.slice(RESOURCE_SCHEME.length) : trimmed;
|
|
25535
|
+
const scheme = OTHER_SCHEME.exec(withoutScheme);
|
|
25536
|
+
if (scheme) {
|
|
25537
|
+
return {
|
|
25538
|
+
ok: false,
|
|
25539
|
+
reason: `Path '${trimmed}' uses the '${scheme[0]}' scheme, which names somewhere other than this project.`
|
|
25540
|
+
};
|
|
25541
|
+
}
|
|
25542
|
+
if (withoutScheme.startsWith("/") || withoutScheme.startsWith("\\") || isAbsolute(withoutScheme)) {
|
|
25543
|
+
return {
|
|
25544
|
+
ok: false,
|
|
25545
|
+
reason: `Path '${trimmed}' is absolute; give a path inside the project, such as 'scenes/main.tscn'.`
|
|
25546
|
+
};
|
|
25547
|
+
}
|
|
25548
|
+
if (withoutScheme.includes(":")) {
|
|
25549
|
+
return {
|
|
25550
|
+
ok: false,
|
|
25551
|
+
reason: `Path '${trimmed}' contains ':', which no file inside a project is named with.`
|
|
25552
|
+
};
|
|
25553
|
+
}
|
|
25554
|
+
const root = resolve(projectPath);
|
|
25555
|
+
const absolutePath = resolve(root, withoutScheme);
|
|
25556
|
+
if (!isWithinRoot(root, absolutePath)) {
|
|
25557
|
+
return { ok: false, reason: `Path '${trimmed}' resolves outside the project directory.` };
|
|
25558
|
+
}
|
|
25559
|
+
const relativePath = relative(root, absolutePath).split(sep).join("/");
|
|
25560
|
+
if (relativePath === "") {
|
|
25561
|
+
return { ok: false, reason: `Path '${trimmed}' is the project directory itself, not a file in it.` };
|
|
25562
|
+
}
|
|
25563
|
+
return { ok: true, absolutePath, relativePath };
|
|
25564
|
+
}
|
|
25565
|
+
|
|
25497
25566
|
// src/godot-bridge.ts
|
|
25498
25567
|
var DEFAULT_PORT = 6505;
|
|
25499
25568
|
var DEFAULT_HOST = "127.0.0.1";
|
|
25500
25569
|
var DEFAULT_TIMEOUT_MS = 30000;
|
|
25501
25570
|
var KEEPALIVE_INTERVAL_MS = 1e4;
|
|
25502
25571
|
var SECOND_CONNECTION_CLOSE_CODE = 4000;
|
|
25572
|
+
var OTHER_PROJECT_CLOSE_CODE = 4001;
|
|
25503
25573
|
function resolveDefaultBridgePort() {
|
|
25504
25574
|
return portFromEnv("GDHARNESS_BRIDGE_PORT", DEFAULT_PORT);
|
|
25505
25575
|
}
|
|
@@ -25523,13 +25593,16 @@ class GodotBridge extends EventEmitter {
|
|
|
25523
25593
|
boundPort = null;
|
|
25524
25594
|
host;
|
|
25525
25595
|
timeoutMs;
|
|
25526
|
-
|
|
25527
|
-
constructor(port = DEFAULT_PORT, host = DEFAULT_HOST, timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
25596
|
+
ownProject;
|
|
25597
|
+
constructor(port = DEFAULT_PORT, host = DEFAULT_HOST, timeoutMs = DEFAULT_TIMEOUT_MS, ownProject = null) {
|
|
25528
25598
|
super();
|
|
25529
25599
|
this.wantedPort = port;
|
|
25530
25600
|
this.host = host;
|
|
25531
25601
|
this.timeoutMs = timeoutMs;
|
|
25532
|
-
this.
|
|
25602
|
+
this.ownProject = ownProject;
|
|
25603
|
+
}
|
|
25604
|
+
get mayMove() {
|
|
25605
|
+
return this.ownProject !== null;
|
|
25533
25606
|
}
|
|
25534
25607
|
get port() {
|
|
25535
25608
|
return this.boundPort ?? this.wantedPort;
|
|
@@ -25645,6 +25718,7 @@ class GodotBridge extends EventEmitter {
|
|
|
25645
25718
|
lspPort: this.connectionInfo?.lspPort,
|
|
25646
25719
|
dapPort: this.connectionInfo?.dapPort,
|
|
25647
25720
|
debugPort: this.connectionInfo?.debugPort,
|
|
25721
|
+
openedByAServer: this.connectionInfo?.openedByAServer,
|
|
25648
25722
|
pendingRequests: this.pendingRequests.size,
|
|
25649
25723
|
queuedResources: this.resourceQueues.size
|
|
25650
25724
|
};
|
|
@@ -25775,12 +25849,17 @@ class GodotBridge extends EventEmitter {
|
|
|
25775
25849
|
}
|
|
25776
25850
|
case "godot_ready":
|
|
25777
25851
|
if (this.connectionInfo) {
|
|
25852
|
+
if (this.belongsElsewhere(message.project_path)) {
|
|
25853
|
+
this.sendElsewhere(message.project_path);
|
|
25854
|
+
return;
|
|
25855
|
+
}
|
|
25778
25856
|
this.connectionInfo.projectPath = message.project_path;
|
|
25779
25857
|
this.connectionInfo.addonVersion = message.addon_version ?? "";
|
|
25780
25858
|
this.connectionInfo.editorPid = message.editor_pid;
|
|
25781
25859
|
this.connectionInfo.lspPort = servedPort(message.lsp_port);
|
|
25782
25860
|
this.connectionInfo.dapPort = servedPort(message.dap_port);
|
|
25783
25861
|
this.connectionInfo.debugPort = servedPort(message.debug_port);
|
|
25862
|
+
this.connectionInfo.openedByAServer = message.opened_by_a_server === true;
|
|
25784
25863
|
this.log("info", `Godot ready: ${message.project_path}`);
|
|
25785
25864
|
this.emitBridgeEvent("godot_connected", { projectPath: message.project_path });
|
|
25786
25865
|
}
|
|
@@ -25857,6 +25936,21 @@ class GodotBridge extends EventEmitter {
|
|
|
25857
25936
|
clearInterval(this.pingInterval);
|
|
25858
25937
|
this.pingInterval = null;
|
|
25859
25938
|
}
|
|
25939
|
+
belongsElsewhere(theirProject) {
|
|
25940
|
+
if (this.ownProject === null || theirProject.trim() === "") {
|
|
25941
|
+
return false;
|
|
25942
|
+
}
|
|
25943
|
+
return !isSameDirectory(this.ownProject, theirProject);
|
|
25944
|
+
}
|
|
25945
|
+
sendElsewhere(theirProject) {
|
|
25946
|
+
const said = `this server serves ${this.ownProject ?? ""}, and that editor has ${theirProject} open`;
|
|
25947
|
+
this.log("warn", `Turning away an editor from another project: ${said}`);
|
|
25948
|
+
const stranger = this.socket;
|
|
25949
|
+
this.socket = null;
|
|
25950
|
+
this.connectionInfo = null;
|
|
25951
|
+
this.stopKeepalive();
|
|
25952
|
+
stranger?.close(OTHER_PROJECT_CLOSE_CODE, said);
|
|
25953
|
+
}
|
|
25860
25954
|
handleDisconnect(disconnectedSocket, reason) {
|
|
25861
25955
|
if (disconnectedSocket && this.socket && disconnectedSocket !== this.socket) {
|
|
25862
25956
|
this.log("debug", "Ignoring stale Godot socket disconnect event");
|
|
@@ -25933,8 +26027,8 @@ class GodotBridge extends EventEmitter {
|
|
|
25933
26027
|
}
|
|
25934
26028
|
}
|
|
25935
26029
|
var defaultBridge = null;
|
|
25936
|
-
function getDefaultBridge(
|
|
25937
|
-
defaultBridge ??= new GodotBridge(resolveDefaultBridgePort(), resolveDefaultBridgeHost(), DEFAULT_TIMEOUT_MS,
|
|
26030
|
+
function getDefaultBridge(ownProject = null) {
|
|
26031
|
+
defaultBridge ??= new GodotBridge(resolveDefaultBridgePort(), resolveDefaultBridgeHost(), DEFAULT_TIMEOUT_MS, ownProject);
|
|
25938
26032
|
return defaultBridge;
|
|
25939
26033
|
}
|
|
25940
26034
|
|
|
@@ -26056,6 +26150,7 @@ function runArguments(options) {
|
|
|
26056
26150
|
function editorArguments(projectPath, ports) {
|
|
26057
26151
|
return ["-e", "--path", projectPath, "--lsp-port", String(ports.lsp), "--dap-port", String(ports.dap)];
|
|
26058
26152
|
}
|
|
26153
|
+
var OPENED_BY_A_SERVER = "GDHARNESS_OPENED_BY_A_SERVER";
|
|
26059
26154
|
function userDataIn(home, variables = process.env) {
|
|
26060
26155
|
const moved = ["APPDATA", "XDG_DATA_HOME"];
|
|
26061
26156
|
const carried = {};
|
|
@@ -26495,56 +26590,6 @@ import { readFile, realpath } from "node:fs/promises";
|
|
|
26495
26590
|
import { createConnection as createConnection2 } from "node:net";
|
|
26496
26591
|
import { dirname as dirname2, resolve as resolve2 } from "node:path";
|
|
26497
26592
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
26498
|
-
|
|
26499
|
-
// src/paths.ts
|
|
26500
|
-
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
26501
|
-
var RESOURCE_SCHEME = "res://";
|
|
26502
|
-
var OTHER_SCHEME = /^[a-zA-Z][a-zA-Z\d+.-]+:\/\//;
|
|
26503
|
-
function isWithinRoot(rootPath, candidatePath) {
|
|
26504
|
-
const step = relative(resolve(rootPath), resolve(candidatePath));
|
|
26505
|
-
return step !== ".." && !step.startsWith(`..${sep}`) && !isAbsolute(step);
|
|
26506
|
-
}
|
|
26507
|
-
function resolveWithinProject(projectPath, candidatePath) {
|
|
26508
|
-
const trimmed = candidatePath.trim();
|
|
26509
|
-
if (trimmed === "") {
|
|
26510
|
-
return { ok: false, reason: "Path is empty." };
|
|
26511
|
-
}
|
|
26512
|
-
if (trimmed.includes("\x00")) {
|
|
26513
|
-
return { ok: false, reason: "Path contains a null byte." };
|
|
26514
|
-
}
|
|
26515
|
-
const withoutScheme = trimmed.startsWith(RESOURCE_SCHEME) ? trimmed.slice(RESOURCE_SCHEME.length) : trimmed;
|
|
26516
|
-
const scheme = OTHER_SCHEME.exec(withoutScheme);
|
|
26517
|
-
if (scheme) {
|
|
26518
|
-
return {
|
|
26519
|
-
ok: false,
|
|
26520
|
-
reason: `Path '${trimmed}' uses the '${scheme[0]}' scheme, which names somewhere other than this project.`
|
|
26521
|
-
};
|
|
26522
|
-
}
|
|
26523
|
-
if (withoutScheme.startsWith("/") || withoutScheme.startsWith("\\") || isAbsolute(withoutScheme)) {
|
|
26524
|
-
return {
|
|
26525
|
-
ok: false,
|
|
26526
|
-
reason: `Path '${trimmed}' is absolute; give a path inside the project, such as 'scenes/main.tscn'.`
|
|
26527
|
-
};
|
|
26528
|
-
}
|
|
26529
|
-
if (withoutScheme.includes(":")) {
|
|
26530
|
-
return {
|
|
26531
|
-
ok: false,
|
|
26532
|
-
reason: `Path '${trimmed}' contains ':', which no file inside a project is named with.`
|
|
26533
|
-
};
|
|
26534
|
-
}
|
|
26535
|
-
const root = resolve(projectPath);
|
|
26536
|
-
const absolutePath = resolve(root, withoutScheme);
|
|
26537
|
-
if (!isWithinRoot(root, absolutePath)) {
|
|
26538
|
-
return { ok: false, reason: `Path '${trimmed}' resolves outside the project directory.` };
|
|
26539
|
-
}
|
|
26540
|
-
const relativePath = relative(root, absolutePath).split(sep).join("/");
|
|
26541
|
-
if (relativePath === "") {
|
|
26542
|
-
return { ok: false, reason: `Path '${trimmed}' is the project directory itself, not a file in it.` };
|
|
26543
|
-
}
|
|
26544
|
-
return { ok: true, absolutePath, relativePath };
|
|
26545
|
-
}
|
|
26546
|
-
|
|
26547
|
-
// src/lsp_client.ts
|
|
26548
26593
|
var DEFAULT_LSP_PORT = 6005;
|
|
26549
26594
|
var DIAGNOSTICS_TIMEOUT_MS = 5000;
|
|
26550
26595
|
function diagnosticsKey(uri) {
|
|
@@ -27038,7 +27083,7 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
27038
27083
|
}
|
|
27039
27084
|
|
|
27040
27085
|
// src/project-scan.ts
|
|
27041
|
-
import { readdirSync as readdirSync3, readFileSync as
|
|
27086
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync5 } from "node:fs";
|
|
27042
27087
|
import { join as join6 } from "node:path";
|
|
27043
27088
|
var SKIPPED = new Set([".git", ".godot", ".import", "node_modules"]);
|
|
27044
27089
|
var ASSET_EXTENSIONS = new Set(["png", "jpg", "jpeg", "webp", "svg", "ttf", "otf", "wav", "mp3", "ogg"]);
|
|
@@ -27103,7 +27148,7 @@ function searchProject(projectPath, options) {
|
|
|
27103
27148
|
}
|
|
27104
27149
|
result.summary.files_searched += 1;
|
|
27105
27150
|
const matches = [];
|
|
27106
|
-
for (const [index, line] of
|
|
27151
|
+
for (const [index, line] of readFileSync5(entryPath, "utf8").split(`
|
|
27107
27152
|
`).entries()) {
|
|
27108
27153
|
if (full()) {
|
|
27109
27154
|
break;
|
|
@@ -27126,7 +27171,7 @@ function searchProject(projectPath, options) {
|
|
|
27126
27171
|
}
|
|
27127
27172
|
|
|
27128
27173
|
// src/resources.ts
|
|
27129
|
-
import { readFileSync as
|
|
27174
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
27130
27175
|
import { extname, resolve as resolve3 } from "node:path";
|
|
27131
27176
|
var STATIC_RESOURCES = [
|
|
27132
27177
|
{
|
|
@@ -27297,7 +27342,7 @@ function readResourceText(uri, getProjectPath) {
|
|
|
27297
27342
|
const parsedUri = parseGodotUri(uri);
|
|
27298
27343
|
if (parsedUri.kind === "project-info") {
|
|
27299
27344
|
const projectFilePath = resolveProjectFile(projectPath, "project.godot");
|
|
27300
|
-
const rawProject =
|
|
27345
|
+
const rawProject = readFileSync6(projectFilePath, "utf-8");
|
|
27301
27346
|
const parsedProject = parseProjectGodot(rawProject);
|
|
27302
27347
|
return {
|
|
27303
27348
|
mimeType: "application/json",
|
|
@@ -27306,7 +27351,7 @@ function readResourceText(uri, getProjectPath) {
|
|
|
27306
27351
|
}
|
|
27307
27352
|
const filePath = resolveProjectFile(projectPath, parsedUri.resourcePath);
|
|
27308
27353
|
ensureAllowedExtension(parsedUri.kind, filePath);
|
|
27309
|
-
const text =
|
|
27354
|
+
const text = readFileSync6(filePath, "utf-8");
|
|
27310
27355
|
return {
|
|
27311
27356
|
mimeType: parsedUri.kind === "script" ? "text/x-gdscript" : "text/plain",
|
|
27312
27357
|
text
|
|
@@ -27343,7 +27388,7 @@ function setupResourceHandlers(mcp, getProjectPath) {
|
|
|
27343
27388
|
}
|
|
27344
27389
|
|
|
27345
27390
|
// src/runtime-client.ts
|
|
27346
|
-
import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as
|
|
27391
|
+
import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync7, unlinkSync } from "node:fs";
|
|
27347
27392
|
import { createConnection as createConnection3 } from "node:net";
|
|
27348
27393
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
27349
27394
|
import { join as join7, resolve as resolve4 } from "node:path";
|
|
@@ -27429,7 +27474,7 @@ function processAlive(pid) {
|
|
|
27429
27474
|
function parseAnnouncement(file, pid) {
|
|
27430
27475
|
let fields;
|
|
27431
27476
|
try {
|
|
27432
|
-
fields = asParams(JSON.parse(
|
|
27477
|
+
fields = asParams(JSON.parse(readFileSync7(file, "utf8")));
|
|
27433
27478
|
} catch {
|
|
27434
27479
|
return { kind: "rubbish" };
|
|
27435
27480
|
}
|
|
@@ -27619,7 +27664,7 @@ function runtimeRequest(endpoint, command, params, timeoutMs) {
|
|
|
27619
27664
|
}
|
|
27620
27665
|
|
|
27621
27666
|
// src/setup.ts
|
|
27622
|
-
import { cpSync, existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as
|
|
27667
|
+
import { cpSync, existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync8, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
27623
27668
|
import { dirname as dirname3, join as join8 } from "node:path";
|
|
27624
27669
|
var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
|
|
27625
27670
|
var VERSION_MARKER = ".gdharness-version";
|
|
@@ -27628,7 +27673,7 @@ function installedAddonVersion(projectPath) {
|
|
|
27628
27673
|
if (!existsSync6(marker)) {
|
|
27629
27674
|
return null;
|
|
27630
27675
|
}
|
|
27631
|
-
const said =
|
|
27676
|
+
const said = readFileSync8(marker, "utf8").trim();
|
|
27632
27677
|
return said === "" ? null : said;
|
|
27633
27678
|
}
|
|
27634
27679
|
|
|
@@ -28398,7 +28443,11 @@ var TOOL_SPECS = [
|
|
|
28398
28443
|
blank: true,
|
|
28399
28444
|
description: `set: the value, fitted to the property's type. "" writes an empty string.`
|
|
28400
28445
|
},
|
|
28401
|
-
method: {
|
|
28446
|
+
method: {
|
|
28447
|
+
type: "string",
|
|
28448
|
+
ops: ["call"],
|
|
28449
|
+
description: 'call: which one to call. Colons call through the objects a node holds, "_game:run:advance", the same way a property is written through them.'
|
|
28450
|
+
},
|
|
28402
28451
|
args: {
|
|
28403
28452
|
type: "array",
|
|
28404
28453
|
ops: ["call"],
|
|
@@ -28411,7 +28460,10 @@ var TOOL_SPECS = [
|
|
|
28411
28460
|
summary: "set a property, on a node or on an object it holds",
|
|
28412
28461
|
requires: ["property", "value"]
|
|
28413
28462
|
},
|
|
28414
|
-
call: {
|
|
28463
|
+
call: {
|
|
28464
|
+
summary: "call a method, on a node or on an object it holds, and return its result",
|
|
28465
|
+
requires: ["method"]
|
|
28466
|
+
}
|
|
28415
28467
|
}
|
|
28416
28468
|
},
|
|
28417
28469
|
{
|
|
@@ -28668,6 +28720,17 @@ var SLOWEST_FRAME_RATE = 20;
|
|
|
28668
28720
|
function patienceForFrames(frames, atLeast) {
|
|
28669
28721
|
return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
|
|
28670
28722
|
}
|
|
28723
|
+
function alive(pid) {
|
|
28724
|
+
if (pid === undefined) {
|
|
28725
|
+
return false;
|
|
28726
|
+
}
|
|
28727
|
+
try {
|
|
28728
|
+
process.kill(pid, 0);
|
|
28729
|
+
return true;
|
|
28730
|
+
} catch {
|
|
28731
|
+
return false;
|
|
28732
|
+
}
|
|
28733
|
+
}
|
|
28671
28734
|
function isOfType(value, kind) {
|
|
28672
28735
|
switch (kind) {
|
|
28673
28736
|
case "string":
|
|
@@ -28714,6 +28777,7 @@ function wrongTypes(spec, args) {
|
|
|
28714
28777
|
return complaints;
|
|
28715
28778
|
}
|
|
28716
28779
|
var BRIDGE_RETRY_MS = 2000;
|
|
28780
|
+
var SUCCESSOR_CHECK_MS = 1e4;
|
|
28717
28781
|
var PATH_SOLUTIONS = [
|
|
28718
28782
|
'Give the path relative to the project, such as "scenes/main.tscn" or "res://scenes/main.tscn"',
|
|
28719
28783
|
"Point projectPath at the project the file belongs to"
|
|
@@ -28801,11 +28865,11 @@ function aboveTheRunner(entry) {
|
|
|
28801
28865
|
};
|
|
28802
28866
|
}
|
|
28803
28867
|
function hasMainScene(projectFile) {
|
|
28804
|
-
const scene = parseProjectGodot(
|
|
28868
|
+
const scene = parseProjectGodot(readFileSync9(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
28805
28869
|
return typeof scene === "string" && scene !== "";
|
|
28806
28870
|
}
|
|
28807
28871
|
function editorPlaysHeadless(projectFile) {
|
|
28808
|
-
const runArgs = parseProjectGodot(
|
|
28872
|
+
const runArgs = parseProjectGodot(readFileSync9(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
28809
28873
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
28810
28874
|
}
|
|
28811
28875
|
function camelCased(params) {
|
|
@@ -28833,13 +28897,14 @@ class GodotServer {
|
|
|
28833
28897
|
dapClient = null;
|
|
28834
28898
|
bridgeStartupError = null;
|
|
28835
28899
|
bridgeRetry = null;
|
|
28900
|
+
successorWatch = null;
|
|
28836
28901
|
lastProjectPath = null;
|
|
28837
28902
|
shutdownInitiated = false;
|
|
28838
28903
|
ownProject;
|
|
28839
28904
|
announcedAt = null;
|
|
28840
28905
|
constructor() {
|
|
28841
28906
|
this.ownProject = envValue("GDHARNESS_PROJECT") ?? null;
|
|
28842
|
-
this.godotBridge = getDefaultBridge(this.ownProject
|
|
28907
|
+
this.godotBridge = getDefaultBridge(this.ownProject);
|
|
28843
28908
|
this.mcp = new McpServer({ name: "gdharness", version: SERVER_VERSION }, { capabilities: { tools: {}, resources: {} } });
|
|
28844
28909
|
this.setupToolHandlers();
|
|
28845
28910
|
setupResourceHandlers(this.mcp, () => this.lastProjectPath);
|
|
@@ -28932,6 +28997,7 @@ class GodotServer {
|
|
|
28932
28997
|
port: status.port,
|
|
28933
28998
|
version: SERVER_VERSION
|
|
28934
28999
|
});
|
|
29000
|
+
this.watchForASuccessor();
|
|
28935
29001
|
}
|
|
28936
29002
|
stopTryingTheBridge() {
|
|
28937
29003
|
if (this.bridgeRetry !== null) {
|
|
@@ -28939,9 +29005,41 @@ class GodotServer {
|
|
|
28939
29005
|
this.bridgeRetry = null;
|
|
28940
29006
|
}
|
|
28941
29007
|
}
|
|
29008
|
+
watchForASuccessor() {
|
|
29009
|
+
if (this.ownProject === null || this.successorWatch !== null) {
|
|
29010
|
+
return;
|
|
29011
|
+
}
|
|
29012
|
+
const watch = setInterval(() => {
|
|
29013
|
+
const successor = this.supersededBy();
|
|
29014
|
+
if (successor === null) {
|
|
29015
|
+
return;
|
|
29016
|
+
}
|
|
29017
|
+
console.error(`[SERVER] pid ${successor} now serves ${this.ownProject}; this server is finished and is stopping.`);
|
|
29018
|
+
this.handleShutdown(`superseded by ${successor}`, 0);
|
|
29019
|
+
}, SUCCESSOR_CHECK_MS);
|
|
29020
|
+
watch.unref();
|
|
29021
|
+
this.successorWatch = watch;
|
|
29022
|
+
}
|
|
29023
|
+
supersededBy() {
|
|
29024
|
+
if (this.ownProject === null) {
|
|
29025
|
+
return null;
|
|
29026
|
+
}
|
|
29027
|
+
const announced = readAnnouncement(announcementPath(this.ownProject));
|
|
29028
|
+
if (announced === null || announced.pid === process.pid) {
|
|
29029
|
+
return null;
|
|
29030
|
+
}
|
|
29031
|
+
return alive(announced.pid) ? announced.pid : null;
|
|
29032
|
+
}
|
|
29033
|
+
stopWatchingForASuccessor() {
|
|
29034
|
+
if (this.successorWatch !== null) {
|
|
29035
|
+
clearInterval(this.successorWatch);
|
|
29036
|
+
this.successorWatch = null;
|
|
29037
|
+
}
|
|
29038
|
+
}
|
|
28942
29039
|
async cleanup() {
|
|
28943
29040
|
this.logDebug("Cleaning up resources");
|
|
28944
29041
|
this.stopTryingTheBridge();
|
|
29042
|
+
this.stopWatchingForASuccessor();
|
|
28945
29043
|
withdrawBridge(this.announcedAt);
|
|
28946
29044
|
this.announcedAt = null;
|
|
28947
29045
|
if (this.activeProcess) {
|
|
@@ -29523,7 +29621,7 @@ class GodotServer {
|
|
|
29523
29621
|
if (!engine.ok) {
|
|
29524
29622
|
return engine.response;
|
|
29525
29623
|
}
|
|
29526
|
-
const application = parseProjectGodot(
|
|
29624
|
+
const application = parseProjectGodot(readFileSync9(project.value.file, "utf8"))["application"] ?? {};
|
|
29527
29625
|
const name = application["config/name"];
|
|
29528
29626
|
const mainScene = application["run/main_scene"];
|
|
29529
29627
|
const info = {
|
|
@@ -29698,7 +29796,7 @@ class GodotServer {
|
|
|
29698
29796
|
const written = existsSync7(reportsDir) ? readdirSync5(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
29699
29797
|
const newest = written.at(-1);
|
|
29700
29798
|
if (newest !== undefined) {
|
|
29701
|
-
report = parseJUnit(
|
|
29799
|
+
report = parseJUnit(readFileSync9(join9(reportsDir, newest, "results.xml"), "utf8"));
|
|
29702
29800
|
}
|
|
29703
29801
|
} catch (error) {
|
|
29704
29802
|
reportProblem = errorMessage(error);
|
|
@@ -29897,7 +29995,8 @@ class GodotServer {
|
|
|
29897
29995
|
"editor_status says whether the bridge is up and what has reached it"
|
|
29898
29996
|
]);
|
|
29899
29997
|
}
|
|
29900
|
-
const
|
|
29998
|
+
const mine = before.openedByAServer === true && before.projectPath !== undefined;
|
|
29999
|
+
const asked = mine ? await this.startItAgain(before.projectPath ?? "", before.editorPid) : await this.handleViaBridge("restart_editor", {});
|
|
29901
30000
|
if (asked.isError === true) {
|
|
29902
30001
|
return asked;
|
|
29903
30002
|
}
|
|
@@ -29925,6 +30024,26 @@ class GodotServer {
|
|
|
29925
30024
|
tookMs: Date.now() - began
|
|
29926
30025
|
});
|
|
29927
30026
|
}
|
|
30027
|
+
async startItAgain(projectPath, editorPid) {
|
|
30028
|
+
const engine = await this.engine();
|
|
30029
|
+
if (!engine.ok) {
|
|
30030
|
+
return engine.response;
|
|
30031
|
+
}
|
|
30032
|
+
const ports = {
|
|
30033
|
+
lsp: this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT),
|
|
30034
|
+
dap: this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT)
|
|
30035
|
+
};
|
|
30036
|
+
const asked = await this.handleViaBridge("quit_editor", {});
|
|
30037
|
+
if (asked.isError === true) {
|
|
30038
|
+
return asked;
|
|
30039
|
+
}
|
|
30040
|
+
await this.waitForBridge(() => !alive(editorPid), Date.now() + EDITOR_RESTART_TIMEOUT_MS);
|
|
30041
|
+
const opened = await this.openAnEditor(engine.value, projectPath, ports);
|
|
30042
|
+
if (opened.error !== null) {
|
|
30043
|
+
return this.createErrorResponse(`The editor was asked to go and could not be started again: ${opened.error}`, [`editor_launch opens one on ${projectPath}`]);
|
|
30044
|
+
}
|
|
30045
|
+
return this.jsonTextResponse({ restarting: true, pid: opened.pid });
|
|
30046
|
+
}
|
|
29928
30047
|
async waitForBridge(reached, deadline) {
|
|
29929
30048
|
for (;; ) {
|
|
29930
30049
|
if (reached()) {
|
|
@@ -29954,14 +30073,28 @@ class GodotServer {
|
|
|
29954
30073
|
}
|
|
29955
30074
|
this.logDebug(`Launching Godot editor for project: ${project.value.path}`);
|
|
29956
30075
|
const ports = await this.portsForAnEditor();
|
|
29957
|
-
const
|
|
30076
|
+
const opened = await this.openAnEditor(engine.value, project.value.path, ports);
|
|
30077
|
+
if (opened.error !== null) {
|
|
30078
|
+
return this.createErrorResponse(`Could not start the editor: ${opened.error}`);
|
|
30079
|
+
}
|
|
30080
|
+
return this.jsonTextResponse({
|
|
30081
|
+
launched: true,
|
|
30082
|
+
pid: opened.pid,
|
|
30083
|
+
projectPath: project.value.path,
|
|
30084
|
+
lspPort: ports.lsp,
|
|
30085
|
+
dapPort: ports.dap
|
|
30086
|
+
});
|
|
30087
|
+
}
|
|
30088
|
+
async openAnEditor(engine, projectPath, ports) {
|
|
30089
|
+
const editor = spawn(engine, editorArguments(projectPath, ports), {
|
|
29958
30090
|
stdio: "ignore",
|
|
29959
30091
|
detached: true,
|
|
29960
30092
|
env: {
|
|
29961
30093
|
...process.env,
|
|
29962
30094
|
GDHARNESS_RUNTIME_DIR: runtimeDirectory(),
|
|
29963
30095
|
GDHARNESS_LSP_PORT: String(ports.lsp),
|
|
29964
|
-
GDHARNESS_DAP_PORT: String(ports.dap)
|
|
30096
|
+
GDHARNESS_DAP_PORT: String(ports.dap),
|
|
30097
|
+
[OPENED_BY_A_SERVER]: "1"
|
|
29965
30098
|
}
|
|
29966
30099
|
});
|
|
29967
30100
|
const started = await new Promise((resolve) => {
|
|
@@ -29973,16 +30106,10 @@ class GodotServer {
|
|
|
29973
30106
|
});
|
|
29974
30107
|
});
|
|
29975
30108
|
if (started !== null) {
|
|
29976
|
-
return
|
|
30109
|
+
return { pid: null, error: started };
|
|
29977
30110
|
}
|
|
29978
30111
|
editor.unref();
|
|
29979
|
-
return
|
|
29980
|
-
launched: true,
|
|
29981
|
-
pid: editor.pid ?? null,
|
|
29982
|
-
projectPath: project.value.path,
|
|
29983
|
-
lspPort: ports.lsp,
|
|
29984
|
-
dapPort: ports.dap
|
|
29985
|
-
});
|
|
30112
|
+
return { pid: editor.pid ?? null, error: null };
|
|
29986
30113
|
}
|
|
29987
30114
|
async portsForAnEditor() {
|
|
29988
30115
|
const lsp = portFromEnvOrNull("GDHARNESS_LSP_PORT") ?? await freePort(DEFAULT_LSP_PORT);
|
|
@@ -30288,7 +30415,7 @@ class GodotServer {
|
|
|
30288
30415
|
return {
|
|
30289
30416
|
content: [
|
|
30290
30417
|
{ type: "text", text: `Screenshot captured: ${dimensions}` },
|
|
30291
|
-
{ type: "image", data:
|
|
30418
|
+
{ type: "image", data: readFileSync9(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
30292
30419
|
]
|
|
30293
30420
|
};
|
|
30294
30421
|
} finally {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.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",
|