gdharness 0.9.0 → 0.9.2
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 +142 -34
- package/build/godot/addons/gdharness_editor/bridge_client.gd +31 -0
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +6 -3
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +35 -15
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +150 -48
- 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 +194 -88
- 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;
|
|
@@ -25775,6 +25848,10 @@ class GodotBridge extends EventEmitter {
|
|
|
25775
25848
|
}
|
|
25776
25849
|
case "godot_ready":
|
|
25777
25850
|
if (this.connectionInfo) {
|
|
25851
|
+
if (this.belongsElsewhere(message.project_path)) {
|
|
25852
|
+
this.sendElsewhere(message.project_path);
|
|
25853
|
+
return;
|
|
25854
|
+
}
|
|
25778
25855
|
this.connectionInfo.projectPath = message.project_path;
|
|
25779
25856
|
this.connectionInfo.addonVersion = message.addon_version ?? "";
|
|
25780
25857
|
this.connectionInfo.editorPid = message.editor_pid;
|
|
@@ -25857,6 +25934,21 @@ class GodotBridge extends EventEmitter {
|
|
|
25857
25934
|
clearInterval(this.pingInterval);
|
|
25858
25935
|
this.pingInterval = null;
|
|
25859
25936
|
}
|
|
25937
|
+
belongsElsewhere(theirProject) {
|
|
25938
|
+
if (this.ownProject === null || theirProject.trim() === "") {
|
|
25939
|
+
return false;
|
|
25940
|
+
}
|
|
25941
|
+
return !isSameDirectory(this.ownProject, theirProject);
|
|
25942
|
+
}
|
|
25943
|
+
sendElsewhere(theirProject) {
|
|
25944
|
+
const said = `this server serves ${this.ownProject ?? ""}, and that editor has ${theirProject} open`;
|
|
25945
|
+
this.log("warn", `Turning away an editor from another project: ${said}`);
|
|
25946
|
+
const stranger = this.socket;
|
|
25947
|
+
this.socket = null;
|
|
25948
|
+
this.connectionInfo = null;
|
|
25949
|
+
this.stopKeepalive();
|
|
25950
|
+
stranger?.close(OTHER_PROJECT_CLOSE_CODE, said);
|
|
25951
|
+
}
|
|
25860
25952
|
handleDisconnect(disconnectedSocket, reason) {
|
|
25861
25953
|
if (disconnectedSocket && this.socket && disconnectedSocket !== this.socket) {
|
|
25862
25954
|
this.log("debug", "Ignoring stale Godot socket disconnect event");
|
|
@@ -25933,8 +26025,8 @@ class GodotBridge extends EventEmitter {
|
|
|
25933
26025
|
}
|
|
25934
26026
|
}
|
|
25935
26027
|
var defaultBridge = null;
|
|
25936
|
-
function getDefaultBridge(
|
|
25937
|
-
defaultBridge ??= new GodotBridge(resolveDefaultBridgePort(), resolveDefaultBridgeHost(), DEFAULT_TIMEOUT_MS,
|
|
26028
|
+
function getDefaultBridge(ownProject = null) {
|
|
26029
|
+
defaultBridge ??= new GodotBridge(resolveDefaultBridgePort(), resolveDefaultBridgeHost(), DEFAULT_TIMEOUT_MS, ownProject);
|
|
25938
26030
|
return defaultBridge;
|
|
25939
26031
|
}
|
|
25940
26032
|
|
|
@@ -26495,56 +26587,6 @@ import { readFile, realpath } from "node:fs/promises";
|
|
|
26495
26587
|
import { createConnection as createConnection2 } from "node:net";
|
|
26496
26588
|
import { dirname as dirname2, resolve as resolve2 } from "node:path";
|
|
26497
26589
|
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
26590
|
var DEFAULT_LSP_PORT = 6005;
|
|
26549
26591
|
var DIAGNOSTICS_TIMEOUT_MS = 5000;
|
|
26550
26592
|
function diagnosticsKey(uri) {
|
|
@@ -27038,7 +27080,7 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
27038
27080
|
}
|
|
27039
27081
|
|
|
27040
27082
|
// src/project-scan.ts
|
|
27041
|
-
import { readdirSync as readdirSync3, readFileSync as
|
|
27083
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync5 } from "node:fs";
|
|
27042
27084
|
import { join as join6 } from "node:path";
|
|
27043
27085
|
var SKIPPED = new Set([".git", ".godot", ".import", "node_modules"]);
|
|
27044
27086
|
var ASSET_EXTENSIONS = new Set(["png", "jpg", "jpeg", "webp", "svg", "ttf", "otf", "wav", "mp3", "ogg"]);
|
|
@@ -27103,7 +27145,7 @@ function searchProject(projectPath, options) {
|
|
|
27103
27145
|
}
|
|
27104
27146
|
result.summary.files_searched += 1;
|
|
27105
27147
|
const matches = [];
|
|
27106
|
-
for (const [index, line] of
|
|
27148
|
+
for (const [index, line] of readFileSync5(entryPath, "utf8").split(`
|
|
27107
27149
|
`).entries()) {
|
|
27108
27150
|
if (full()) {
|
|
27109
27151
|
break;
|
|
@@ -27126,7 +27168,7 @@ function searchProject(projectPath, options) {
|
|
|
27126
27168
|
}
|
|
27127
27169
|
|
|
27128
27170
|
// src/resources.ts
|
|
27129
|
-
import { readFileSync as
|
|
27171
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
27130
27172
|
import { extname, resolve as resolve3 } from "node:path";
|
|
27131
27173
|
var STATIC_RESOURCES = [
|
|
27132
27174
|
{
|
|
@@ -27297,7 +27339,7 @@ function readResourceText(uri, getProjectPath) {
|
|
|
27297
27339
|
const parsedUri = parseGodotUri(uri);
|
|
27298
27340
|
if (parsedUri.kind === "project-info") {
|
|
27299
27341
|
const projectFilePath = resolveProjectFile(projectPath, "project.godot");
|
|
27300
|
-
const rawProject =
|
|
27342
|
+
const rawProject = readFileSync6(projectFilePath, "utf-8");
|
|
27301
27343
|
const parsedProject = parseProjectGodot(rawProject);
|
|
27302
27344
|
return {
|
|
27303
27345
|
mimeType: "application/json",
|
|
@@ -27306,7 +27348,7 @@ function readResourceText(uri, getProjectPath) {
|
|
|
27306
27348
|
}
|
|
27307
27349
|
const filePath = resolveProjectFile(projectPath, parsedUri.resourcePath);
|
|
27308
27350
|
ensureAllowedExtension(parsedUri.kind, filePath);
|
|
27309
|
-
const text =
|
|
27351
|
+
const text = readFileSync6(filePath, "utf-8");
|
|
27310
27352
|
return {
|
|
27311
27353
|
mimeType: parsedUri.kind === "script" ? "text/x-gdscript" : "text/plain",
|
|
27312
27354
|
text
|
|
@@ -27343,7 +27385,7 @@ function setupResourceHandlers(mcp, getProjectPath) {
|
|
|
27343
27385
|
}
|
|
27344
27386
|
|
|
27345
27387
|
// src/runtime-client.ts
|
|
27346
|
-
import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as
|
|
27388
|
+
import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync7, unlinkSync } from "node:fs";
|
|
27347
27389
|
import { createConnection as createConnection3 } from "node:net";
|
|
27348
27390
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
27349
27391
|
import { join as join7, resolve as resolve4 } from "node:path";
|
|
@@ -27429,7 +27471,7 @@ function processAlive(pid) {
|
|
|
27429
27471
|
function parseAnnouncement(file, pid) {
|
|
27430
27472
|
let fields;
|
|
27431
27473
|
try {
|
|
27432
|
-
fields = asParams(JSON.parse(
|
|
27474
|
+
fields = asParams(JSON.parse(readFileSync7(file, "utf8")));
|
|
27433
27475
|
} catch {
|
|
27434
27476
|
return { kind: "rubbish" };
|
|
27435
27477
|
}
|
|
@@ -27619,7 +27661,7 @@ function runtimeRequest(endpoint, command, params, timeoutMs) {
|
|
|
27619
27661
|
}
|
|
27620
27662
|
|
|
27621
27663
|
// src/setup.ts
|
|
27622
|
-
import { cpSync, existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as
|
|
27664
|
+
import { cpSync, existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync8, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
27623
27665
|
import { dirname as dirname3, join as join8 } from "node:path";
|
|
27624
27666
|
var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
|
|
27625
27667
|
var VERSION_MARKER = ".gdharness-version";
|
|
@@ -27628,7 +27670,7 @@ function installedAddonVersion(projectPath) {
|
|
|
27628
27670
|
if (!existsSync6(marker)) {
|
|
27629
27671
|
return null;
|
|
27630
27672
|
}
|
|
27631
|
-
const said =
|
|
27673
|
+
const said = readFileSync8(marker, "utf8").trim();
|
|
27632
27674
|
return said === "" ? null : said;
|
|
27633
27675
|
}
|
|
27634
27676
|
|
|
@@ -27811,7 +27853,8 @@ var TOOL_SPECS = [
|
|
|
27811
27853
|
projectPath: PROJECT_PATH,
|
|
27812
27854
|
setting: { type: "string", description: 'Setting path, such as "display/window/size/viewport_width".' },
|
|
27813
27855
|
value: {
|
|
27814
|
-
|
|
27856
|
+
blank: true,
|
|
27857
|
+
description: 'The value to write. Engine types may be tagged, {"_type": "Vector2", "x": 1, "y": 2}. "" writes an empty string.'
|
|
27815
27858
|
},
|
|
27816
27859
|
name: { type: "string", description: "Autoload name." },
|
|
27817
27860
|
path: { type: "string", description: "Autoload script or scene inside the project." },
|
|
@@ -28316,7 +28359,7 @@ var TOOL_SPECS = [
|
|
|
28316
28359
|
property: {
|
|
28317
28360
|
type: "string",
|
|
28318
28361
|
ops: ["property", "find"],
|
|
28319
|
-
description:
|
|
28362
|
+
description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through the objects a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about; a step that is not there is named.'
|
|
28320
28363
|
},
|
|
28321
28364
|
depth: { type: "number", ops: ["tree"], description: "tree: levels to descend. Default 3." },
|
|
28322
28365
|
includeProperties: {
|
|
@@ -28333,7 +28376,7 @@ var TOOL_SPECS = [
|
|
|
28333
28376
|
namePattern: {
|
|
28334
28377
|
type: "string",
|
|
28335
28378
|
ops: ["find"],
|
|
28336
|
-
description: 'find: a case-insensitive glob on the node name, such as "Enemy*".'
|
|
28379
|
+
description: 'find: a case-insensitive glob on the node name, such as "Enemy*". Matched against the whole name, so a bare word finds only a node called exactly that; an answer of none says how many names contain it and what glob would have found them.'
|
|
28337
28380
|
},
|
|
28338
28381
|
group: { type: "string", ops: ["find"], description: "find: a group the node is in." },
|
|
28339
28382
|
says: {
|
|
@@ -28374,7 +28417,7 @@ var TOOL_SPECS = [
|
|
|
28374
28417
|
requires: ["nodePath"]
|
|
28375
28418
|
},
|
|
28376
28419
|
property: {
|
|
28377
|
-
summary: "what one property reads on a node, refusing a
|
|
28420
|
+
summary: "what one property reads on a node, or through the objects it holds, refusing a name nothing along the way has",
|
|
28378
28421
|
requires: ["nodePath", "property"]
|
|
28379
28422
|
},
|
|
28380
28423
|
metrics: { summary: "frame time, memory, draw calls and the rest", requires: [] }
|
|
@@ -28387,9 +28430,21 @@ var TOOL_SPECS = [
|
|
|
28387
28430
|
parameters: {
|
|
28388
28431
|
projectPath: RUNNING_PROJECT_PATH,
|
|
28389
28432
|
nodePath: { type: "string", description: 'Absolute node path, such as "/root/Main/Player".' },
|
|
28390
|
-
property: {
|
|
28391
|
-
|
|
28392
|
-
|
|
28433
|
+
property: {
|
|
28434
|
+
type: "string",
|
|
28435
|
+
ops: ["set"],
|
|
28436
|
+
description: 'set: which one to write. Colons write through the objects a node holds, "_game:run:day", and the answer reads back off the same holder, so a write a typed container refused shows as an unchanged value.'
|
|
28437
|
+
},
|
|
28438
|
+
value: {
|
|
28439
|
+
ops: ["set"],
|
|
28440
|
+
blank: true,
|
|
28441
|
+
description: `set: the value, fitted to the property's type. "" writes an empty string.`
|
|
28442
|
+
},
|
|
28443
|
+
method: {
|
|
28444
|
+
type: "string",
|
|
28445
|
+
ops: ["call"],
|
|
28446
|
+
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.'
|
|
28447
|
+
},
|
|
28393
28448
|
args: {
|
|
28394
28449
|
type: "array",
|
|
28395
28450
|
ops: ["call"],
|
|
@@ -28398,8 +28453,14 @@ var TOOL_SPECS = [
|
|
|
28398
28453
|
},
|
|
28399
28454
|
requires: ["nodePath"],
|
|
28400
28455
|
operations: {
|
|
28401
|
-
set: {
|
|
28402
|
-
|
|
28456
|
+
set: {
|
|
28457
|
+
summary: "set a property, on a node or on an object it holds",
|
|
28458
|
+
requires: ["property", "value"]
|
|
28459
|
+
},
|
|
28460
|
+
call: {
|
|
28461
|
+
summary: "call a method, on a node or on an object it holds, and return its result",
|
|
28462
|
+
requires: ["method"]
|
|
28463
|
+
}
|
|
28403
28464
|
}
|
|
28404
28465
|
},
|
|
28405
28466
|
{
|
|
@@ -28451,7 +28512,8 @@ var TOOL_SPECS = [
|
|
|
28451
28512
|
text: {
|
|
28452
28513
|
type: "string",
|
|
28453
28514
|
ops: ["text", "choose"],
|
|
28454
|
-
|
|
28515
|
+
blank: true,
|
|
28516
|
+
description: 'text: what to type. A newline is Enter and a tab is Tab, a space is a space, and "" with replace empties the field. choose: the item to take, by what it says.'
|
|
28455
28517
|
},
|
|
28456
28518
|
replace: {
|
|
28457
28519
|
type: "boolean",
|
|
@@ -28625,7 +28687,7 @@ function buildToolDefinitions() {
|
|
|
28625
28687
|
};
|
|
28626
28688
|
}
|
|
28627
28689
|
for (const [name, schema] of Object.entries(spec.parameters)) {
|
|
28628
|
-
const { ops: _ops, ...carried } = schema;
|
|
28690
|
+
const { ops: _ops, blank: _blank, ...carried } = schema;
|
|
28629
28691
|
properties[name] = carried;
|
|
28630
28692
|
}
|
|
28631
28693
|
const required = [...spec.requires];
|
|
@@ -28701,6 +28763,7 @@ function wrongTypes(spec, args) {
|
|
|
28701
28763
|
return complaints;
|
|
28702
28764
|
}
|
|
28703
28765
|
var BRIDGE_RETRY_MS = 2000;
|
|
28766
|
+
var SUCCESSOR_CHECK_MS = 1e4;
|
|
28704
28767
|
var PATH_SOLUTIONS = [
|
|
28705
28768
|
'Give the path relative to the project, such as "scenes/main.tscn" or "res://scenes/main.tscn"',
|
|
28706
28769
|
"Point projectPath at the project the file belongs to"
|
|
@@ -28788,11 +28851,11 @@ function aboveTheRunner(entry) {
|
|
|
28788
28851
|
};
|
|
28789
28852
|
}
|
|
28790
28853
|
function hasMainScene(projectFile) {
|
|
28791
|
-
const scene = parseProjectGodot(
|
|
28854
|
+
const scene = parseProjectGodot(readFileSync9(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
28792
28855
|
return typeof scene === "string" && scene !== "";
|
|
28793
28856
|
}
|
|
28794
28857
|
function editorPlaysHeadless(projectFile) {
|
|
28795
|
-
const runArgs = parseProjectGodot(
|
|
28858
|
+
const runArgs = parseProjectGodot(readFileSync9(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
28796
28859
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
28797
28860
|
}
|
|
28798
28861
|
function camelCased(params) {
|
|
@@ -28820,13 +28883,14 @@ class GodotServer {
|
|
|
28820
28883
|
dapClient = null;
|
|
28821
28884
|
bridgeStartupError = null;
|
|
28822
28885
|
bridgeRetry = null;
|
|
28886
|
+
successorWatch = null;
|
|
28823
28887
|
lastProjectPath = null;
|
|
28824
28888
|
shutdownInitiated = false;
|
|
28825
28889
|
ownProject;
|
|
28826
28890
|
announcedAt = null;
|
|
28827
28891
|
constructor() {
|
|
28828
28892
|
this.ownProject = envValue("GDHARNESS_PROJECT") ?? null;
|
|
28829
|
-
this.godotBridge = getDefaultBridge(this.ownProject
|
|
28893
|
+
this.godotBridge = getDefaultBridge(this.ownProject);
|
|
28830
28894
|
this.mcp = new McpServer({ name: "gdharness", version: SERVER_VERSION }, { capabilities: { tools: {}, resources: {} } });
|
|
28831
28895
|
this.setupToolHandlers();
|
|
28832
28896
|
setupResourceHandlers(this.mcp, () => this.lastProjectPath);
|
|
@@ -28919,6 +28983,7 @@ class GodotServer {
|
|
|
28919
28983
|
port: status.port,
|
|
28920
28984
|
version: SERVER_VERSION
|
|
28921
28985
|
});
|
|
28986
|
+
this.watchForASuccessor();
|
|
28922
28987
|
}
|
|
28923
28988
|
stopTryingTheBridge() {
|
|
28924
28989
|
if (this.bridgeRetry !== null) {
|
|
@@ -28926,9 +28991,46 @@ class GodotServer {
|
|
|
28926
28991
|
this.bridgeRetry = null;
|
|
28927
28992
|
}
|
|
28928
28993
|
}
|
|
28994
|
+
watchForASuccessor() {
|
|
28995
|
+
if (this.ownProject === null || this.successorWatch !== null) {
|
|
28996
|
+
return;
|
|
28997
|
+
}
|
|
28998
|
+
const watch = setInterval(() => {
|
|
28999
|
+
const successor = this.supersededBy();
|
|
29000
|
+
if (successor === null) {
|
|
29001
|
+
return;
|
|
29002
|
+
}
|
|
29003
|
+
console.error(`[SERVER] pid ${successor} now serves ${this.ownProject}; this server is finished and is stopping.`);
|
|
29004
|
+
this.handleShutdown(`superseded by ${successor}`, 0);
|
|
29005
|
+
}, SUCCESSOR_CHECK_MS);
|
|
29006
|
+
watch.unref();
|
|
29007
|
+
this.successorWatch = watch;
|
|
29008
|
+
}
|
|
29009
|
+
supersededBy() {
|
|
29010
|
+
if (this.ownProject === null) {
|
|
29011
|
+
return null;
|
|
29012
|
+
}
|
|
29013
|
+
const announced = readAnnouncement(announcementPath(this.ownProject));
|
|
29014
|
+
if (announced === null || announced.pid === process.pid) {
|
|
29015
|
+
return null;
|
|
29016
|
+
}
|
|
29017
|
+
try {
|
|
29018
|
+
process.kill(announced.pid, 0);
|
|
29019
|
+
return announced.pid;
|
|
29020
|
+
} catch {
|
|
29021
|
+
return null;
|
|
29022
|
+
}
|
|
29023
|
+
}
|
|
29024
|
+
stopWatchingForASuccessor() {
|
|
29025
|
+
if (this.successorWatch !== null) {
|
|
29026
|
+
clearInterval(this.successorWatch);
|
|
29027
|
+
this.successorWatch = null;
|
|
29028
|
+
}
|
|
29029
|
+
}
|
|
28929
29030
|
async cleanup() {
|
|
28930
29031
|
this.logDebug("Cleaning up resources");
|
|
28931
29032
|
this.stopTryingTheBridge();
|
|
29033
|
+
this.stopWatchingForASuccessor();
|
|
28932
29034
|
withdrawBridge(this.announcedAt);
|
|
28933
29035
|
this.announcedAt = null;
|
|
28934
29036
|
if (this.activeProcess) {
|
|
@@ -29141,7 +29243,11 @@ class GodotServer {
|
|
|
29141
29243
|
const required = [...spec.requires, ...op !== null ? spec.operations?.[op]?.requires ?? [] : []];
|
|
29142
29244
|
const missing = required.filter((field) => {
|
|
29143
29245
|
const value = Object.hasOwn(args, field) ? args[field] : undefined;
|
|
29144
|
-
|
|
29246
|
+
if (value === undefined || value === null) {
|
|
29247
|
+
return true;
|
|
29248
|
+
}
|
|
29249
|
+
const blank = typeof value === "string" && value.trim() === "";
|
|
29250
|
+
return blank && spec.parameters[field]?.blank !== true;
|
|
29145
29251
|
});
|
|
29146
29252
|
if (missing.length > 0) {
|
|
29147
29253
|
const where = op !== null ? `${spec.name} ${op}` : spec.name;
|
|
@@ -29506,7 +29612,7 @@ class GodotServer {
|
|
|
29506
29612
|
if (!engine.ok) {
|
|
29507
29613
|
return engine.response;
|
|
29508
29614
|
}
|
|
29509
|
-
const application = parseProjectGodot(
|
|
29615
|
+
const application = parseProjectGodot(readFileSync9(project.value.file, "utf8"))["application"] ?? {};
|
|
29510
29616
|
const name = application["config/name"];
|
|
29511
29617
|
const mainScene = application["run/main_scene"];
|
|
29512
29618
|
const info = {
|
|
@@ -29681,7 +29787,7 @@ class GodotServer {
|
|
|
29681
29787
|
const written = existsSync7(reportsDir) ? readdirSync5(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
29682
29788
|
const newest = written.at(-1);
|
|
29683
29789
|
if (newest !== undefined) {
|
|
29684
|
-
report = parseJUnit(
|
|
29790
|
+
report = parseJUnit(readFileSync9(join9(reportsDir, newest, "results.xml"), "utf8"));
|
|
29685
29791
|
}
|
|
29686
29792
|
} catch (error) {
|
|
29687
29793
|
reportProblem = errorMessage(error);
|
|
@@ -30271,7 +30377,7 @@ class GodotServer {
|
|
|
30271
30377
|
return {
|
|
30272
30378
|
content: [
|
|
30273
30379
|
{ type: "text", text: `Screenshot captured: ${dimensions}` },
|
|
30274
|
-
{ type: "image", data:
|
|
30380
|
+
{ type: "image", data: readFileSync9(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
30275
30381
|
]
|
|
30276
30382
|
};
|
|
30277
30383
|
} finally {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
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",
|