gdharness 0.5.9 → 0.5.11
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 +140 -63
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +31 -1
- package/build/index.js +117 -47
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -7229,6 +7229,16 @@ class GameLog {
|
|
|
7229
7229
|
this.entries.push(entry);
|
|
7230
7230
|
this.lastHeadline = headline ? { index: entry.index, detail } : null;
|
|
7231
7231
|
}
|
|
7232
|
+
record(severity, text) {
|
|
7233
|
+
this.entries.push({
|
|
7234
|
+
index: this.entries.length,
|
|
7235
|
+
severity,
|
|
7236
|
+
source: "debugger",
|
|
7237
|
+
text,
|
|
7238
|
+
detail: []
|
|
7239
|
+
});
|
|
7240
|
+
this.lastHeadline = null;
|
|
7241
|
+
}
|
|
7232
7242
|
get all() {
|
|
7233
7243
|
return this.entries;
|
|
7234
7244
|
}
|
|
@@ -15741,7 +15751,10 @@ var init_tool_definitions = __esm(() => {
|
|
|
15741
15751
|
type: "string",
|
|
15742
15752
|
description: "tree, find: where to start, default /root. rect: the node to place. property: the node to read."
|
|
15743
15753
|
},
|
|
15744
|
-
property: {
|
|
15754
|
+
property: {
|
|
15755
|
+
type: "string",
|
|
15756
|
+
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."
|
|
15757
|
+
},
|
|
15745
15758
|
depth: { type: "number", description: "tree: levels to descend. Default 3." },
|
|
15746
15759
|
includeProperties: {
|
|
15747
15760
|
type: "boolean",
|
|
@@ -15768,7 +15781,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15768
15781
|
operations: {
|
|
15769
15782
|
tree: { summary: "the live scene tree", requires: [] },
|
|
15770
15783
|
find: {
|
|
15771
|
-
summary: "the paths of every node matching className, script, namePattern or group",
|
|
15784
|
+
summary: "the paths of every node matching className, script, namePattern or group, with property read off each",
|
|
15772
15785
|
requires: []
|
|
15773
15786
|
},
|
|
15774
15787
|
rect: {
|
|
@@ -30785,6 +30798,10 @@ var init_ports = __esm(() => {
|
|
|
30785
30798
|
// src/dap_client.ts
|
|
30786
30799
|
import { createConnection } from "node:net";
|
|
30787
30800
|
import { setTimeout as delay } from "node:timers/promises";
|
|
30801
|
+
function said(body, field) {
|
|
30802
|
+
const value = body?.[field];
|
|
30803
|
+
return typeof value === "string" ? value : "";
|
|
30804
|
+
}
|
|
30788
30805
|
|
|
30789
30806
|
class GodotDAPClient {
|
|
30790
30807
|
socket = null;
|
|
@@ -30799,7 +30816,7 @@ class GodotDAPClient {
|
|
|
30799
30816
|
initialized = false;
|
|
30800
30817
|
attached = false;
|
|
30801
30818
|
lastThreadId = 1;
|
|
30802
|
-
|
|
30819
|
+
halt = null;
|
|
30803
30820
|
breakpoints = new Map;
|
|
30804
30821
|
constructor(port = portFromEnv("GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT), host = "127.0.0.1") {
|
|
30805
30822
|
this.port = port;
|
|
@@ -30848,7 +30865,7 @@ class GodotDAPClient {
|
|
|
30848
30865
|
this.connected = false;
|
|
30849
30866
|
this.initialized = false;
|
|
30850
30867
|
this.attached = false;
|
|
30851
|
-
this.
|
|
30868
|
+
this.halt = null;
|
|
30852
30869
|
this.socket = null;
|
|
30853
30870
|
this.failPendingRequests(new Error("DAP connection closed"));
|
|
30854
30871
|
});
|
|
@@ -30868,7 +30885,7 @@ class GodotDAPClient {
|
|
|
30868
30885
|
this.connected = false;
|
|
30869
30886
|
this.initialized = false;
|
|
30870
30887
|
this.attached = false;
|
|
30871
|
-
this.
|
|
30888
|
+
this.halt = null;
|
|
30872
30889
|
return;
|
|
30873
30890
|
}
|
|
30874
30891
|
if (this.connected) {
|
|
@@ -30897,7 +30914,7 @@ class GodotDAPClient {
|
|
|
30897
30914
|
this.connected = false;
|
|
30898
30915
|
this.initialized = false;
|
|
30899
30916
|
this.attached = false;
|
|
30900
|
-
this.
|
|
30917
|
+
this.halt = null;
|
|
30901
30918
|
}
|
|
30902
30919
|
async ensureConnected() {
|
|
30903
30920
|
if (!this.connected) {
|
|
@@ -30974,12 +30991,16 @@ class GodotDAPClient {
|
|
|
30974
30991
|
if (typeof threadId === "number") {
|
|
30975
30992
|
this.lastThreadId = threadId;
|
|
30976
30993
|
}
|
|
30977
|
-
this.
|
|
30994
|
+
this.halt = {
|
|
30995
|
+
reason: said(body, "reason"),
|
|
30996
|
+
description: said(body, "description"),
|
|
30997
|
+
text: said(body, "text")
|
|
30998
|
+
};
|
|
30978
30999
|
return;
|
|
30979
31000
|
}
|
|
30980
31001
|
if (eventName === "terminated" || eventName === "exited") {
|
|
30981
31002
|
this.attached = false;
|
|
30982
|
-
this.
|
|
31003
|
+
this.halt = null;
|
|
30983
31004
|
}
|
|
30984
31005
|
}
|
|
30985
31006
|
async initialize() {
|
|
@@ -31053,7 +31074,7 @@ class GodotDAPClient {
|
|
|
31053
31074
|
await this.attach();
|
|
31054
31075
|
const resolvedThreadId = await this.resolveThreadId(threadId);
|
|
31055
31076
|
await this.sendRequest("continue", { threadId: resolvedThreadId });
|
|
31056
|
-
this.
|
|
31077
|
+
this.halt = null;
|
|
31057
31078
|
}
|
|
31058
31079
|
async stepOver(threadId) {
|
|
31059
31080
|
await this.attach();
|
|
@@ -31124,7 +31145,10 @@ class GodotDAPClient {
|
|
|
31124
31145
|
return this.connected;
|
|
31125
31146
|
}
|
|
31126
31147
|
isStopped() {
|
|
31127
|
-
return this.
|
|
31148
|
+
return this.halt !== null;
|
|
31149
|
+
}
|
|
31150
|
+
whereItStopped() {
|
|
31151
|
+
return this.halt;
|
|
31128
31152
|
}
|
|
31129
31153
|
async resolveThreadId(threadId) {
|
|
31130
31154
|
if (typeof threadId === "number" && threadId > 0) {
|
|
@@ -31153,7 +31177,7 @@ class GodotDAPClient {
|
|
|
31153
31177
|
this.connected = false;
|
|
31154
31178
|
this.initialized = false;
|
|
31155
31179
|
this.attached = false;
|
|
31156
|
-
this.
|
|
31180
|
+
this.halt = null;
|
|
31157
31181
|
this.reader = new FrameReader;
|
|
31158
31182
|
socket?.destroy();
|
|
31159
31183
|
this.failPendingRequests(new Error(`Godot DAP ${detail}. The connection was dropped.`));
|
|
@@ -35741,11 +35765,31 @@ var init_runtime_client = __esm(() => {
|
|
|
35741
35765
|
ANNOUNCEMENT_PATTERN = /^runtime-(\d+)\.json$/;
|
|
35742
35766
|
});
|
|
35743
35767
|
|
|
35768
|
+
// src/setup.ts
|
|
35769
|
+
import { cpSync as cpSync2, existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync11, rmSync as rmSync7, writeFileSync as writeFileSync8 } from "node:fs";
|
|
35770
|
+
import { dirname as dirname8, join as join14 } from "node:path";
|
|
35771
|
+
function installedAddonVersion(projectPath) {
|
|
35772
|
+
const marker = join14(projectPath, "addons", ADDONS2[0], VERSION_MARKER2);
|
|
35773
|
+
if (!existsSync11(marker)) {
|
|
35774
|
+
return null;
|
|
35775
|
+
}
|
|
35776
|
+
const said = readFileSync11(marker, "utf8").trim();
|
|
35777
|
+
return said === "" ? null : said;
|
|
35778
|
+
}
|
|
35779
|
+
var ADDONS2, VERSION_MARKER2 = ".gdharness-version";
|
|
35780
|
+
var init_setup = __esm(() => {
|
|
35781
|
+
init_class_cache();
|
|
35782
|
+
init_headless();
|
|
35783
|
+
init_resources();
|
|
35784
|
+
init_server_version();
|
|
35785
|
+
ADDONS2 = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
|
|
35786
|
+
});
|
|
35787
|
+
|
|
35744
35788
|
// src/server.ts
|
|
35745
35789
|
import { execFile as execFile5, spawn } from "node:child_process";
|
|
35746
|
-
import { existsSync as
|
|
35790
|
+
import { existsSync as existsSync12, mkdtempSync as mkdtempSync3, readdirSync as readdirSync6, readFileSync as readFileSync12, realpathSync as realpathSync2, rmSync as rmSync8 } from "node:fs";
|
|
35747
35791
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
35748
|
-
import { basename as basename2, dirname as
|
|
35792
|
+
import { basename as basename2, dirname as dirname9, join as join15, normalize as normalize3 } from "node:path";
|
|
35749
35793
|
import { setTimeout as delay2 } from "node:timers/promises";
|
|
35750
35794
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
35751
35795
|
import { promisify as promisify5 } from "node:util";
|
|
@@ -35771,11 +35815,11 @@ function realPathOr(path) {
|
|
|
35771
35815
|
}
|
|
35772
35816
|
}
|
|
35773
35817
|
function hasMainScene(projectFile) {
|
|
35774
|
-
const scene = parseProjectGodot(
|
|
35818
|
+
const scene = parseProjectGodot(readFileSync12(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
35775
35819
|
return typeof scene === "string" && scene !== "";
|
|
35776
35820
|
}
|
|
35777
35821
|
function editorPlaysHeadless(projectFile) {
|
|
35778
|
-
const runArgs = parseProjectGodot(
|
|
35822
|
+
const runArgs = parseProjectGodot(readFileSync12(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
35779
35823
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
35780
35824
|
}
|
|
35781
35825
|
function camelCased(params) {
|
|
@@ -35791,7 +35835,7 @@ function camelCased(params) {
|
|
|
35791
35835
|
class GodotServer {
|
|
35792
35836
|
mcp;
|
|
35793
35837
|
locator = new GodotLocator2;
|
|
35794
|
-
operationsScript =
|
|
35838
|
+
operationsScript = join15(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
35795
35839
|
godotBridge;
|
|
35796
35840
|
tools = buildToolDefinitions();
|
|
35797
35841
|
activeProcess = null;
|
|
@@ -36029,28 +36073,44 @@ class GodotServer {
|
|
|
36029
36073
|
if (this.callsSinceNotice < UPDATE_NOTICE_EVERY && this.noticedUpdate) {
|
|
36030
36074
|
return answer;
|
|
36031
36075
|
}
|
|
36076
|
+
const moved = this.projectHasMovedOn();
|
|
36077
|
+
if (moved !== null) {
|
|
36078
|
+
this.noticedUpdate = true;
|
|
36079
|
+
this.callsSinceNotice = 0;
|
|
36080
|
+
return this.saying(answer, {
|
|
36081
|
+
project_upgraded_under_this_server: {
|
|
36082
|
+
server_is: SERVER_VERSION,
|
|
36083
|
+
project_is: moved,
|
|
36084
|
+
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."
|
|
36085
|
+
}
|
|
36086
|
+
});
|
|
36087
|
+
}
|
|
36032
36088
|
const notice = this.updates.notice();
|
|
36033
36089
|
if (notice === null) {
|
|
36034
36090
|
return answer;
|
|
36035
36091
|
}
|
|
36036
36092
|
this.noticedUpdate = true;
|
|
36037
36093
|
this.callsSinceNotice = 0;
|
|
36094
|
+
return this.saying(answer, {
|
|
36095
|
+
update_available: {
|
|
36096
|
+
...notice,
|
|
36097
|
+
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."
|
|
36098
|
+
}
|
|
36099
|
+
});
|
|
36100
|
+
}
|
|
36101
|
+
saying(answer, block) {
|
|
36038
36102
|
return {
|
|
36039
36103
|
...answer,
|
|
36040
|
-
content: [
|
|
36041
|
-
...answer.content,
|
|
36042
|
-
{
|
|
36043
|
-
type: "text",
|
|
36044
|
-
text: JSON.stringify({
|
|
36045
|
-
update_available: {
|
|
36046
|
-
...notice,
|
|
36047
|
-
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."
|
|
36048
|
-
}
|
|
36049
|
-
}, null, 2)
|
|
36050
|
-
}
|
|
36051
|
-
]
|
|
36104
|
+
content: [...answer.content, { type: "text", text: JSON.stringify(block, null, 2) }]
|
|
36052
36105
|
};
|
|
36053
36106
|
}
|
|
36107
|
+
projectHasMovedOn() {
|
|
36108
|
+
if (this.ownProject === null) {
|
|
36109
|
+
return null;
|
|
36110
|
+
}
|
|
36111
|
+
const installed = installedAddonVersion(this.ownProject);
|
|
36112
|
+
return installed === null || installed === SERVER_VERSION ? null : installed;
|
|
36113
|
+
}
|
|
36054
36114
|
withFeedbackNotice(answer) {
|
|
36055
36115
|
this.callsSinceFeedback += 1;
|
|
36056
36116
|
if (this.callsSinceFeedback < FEEDBACK_NOTICE_EVERY) {
|
|
@@ -36296,8 +36356,8 @@ class GodotServer {
|
|
|
36296
36356
|
if (path === undefined) {
|
|
36297
36357
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
36298
36358
|
}
|
|
36299
|
-
const file =
|
|
36300
|
-
if (!
|
|
36359
|
+
const file = join15(path, "project.godot");
|
|
36360
|
+
if (!existsSync12(file)) {
|
|
36301
36361
|
return {
|
|
36302
36362
|
ok: false,
|
|
36303
36363
|
response: this.createErrorResponse(`Not a Godot project: ${path}`, [
|
|
@@ -36442,7 +36502,7 @@ class GodotServer {
|
|
|
36442
36502
|
if (!engine.ok) {
|
|
36443
36503
|
return engine.response;
|
|
36444
36504
|
}
|
|
36445
|
-
const application = parseProjectGodot(
|
|
36505
|
+
const application = parseProjectGodot(readFileSync12(project.value.file, "utf8"))["application"] ?? {};
|
|
36446
36506
|
const name = application["config/name"];
|
|
36447
36507
|
const mainScene = application["run/main_scene"];
|
|
36448
36508
|
const info = {
|
|
@@ -36531,7 +36591,7 @@ class GodotServer {
|
|
|
36531
36591
|
log.finish();
|
|
36532
36592
|
const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
|
|
36533
36593
|
const verdict = {
|
|
36534
|
-
exported: exitCode === 0 && log.count("error") === 0 &&
|
|
36594
|
+
exported: exitCode === 0 && log.count("error") === 0 && existsSync12(output.absolutePath),
|
|
36535
36595
|
preset,
|
|
36536
36596
|
outputPath: output.relativePath,
|
|
36537
36597
|
debug,
|
|
@@ -36561,7 +36621,7 @@ class GodotServer {
|
|
|
36561
36621
|
return contained.response;
|
|
36562
36622
|
}
|
|
36563
36623
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
36564
|
-
if (!
|
|
36624
|
+
if (!existsSync12(join15(project.value.path, runner))) {
|
|
36565
36625
|
return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
|
|
36566
36626
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
36567
36627
|
]);
|
|
@@ -36594,7 +36654,7 @@ class GodotServer {
|
|
|
36594
36654
|
];
|
|
36595
36655
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
|
|
36596
36656
|
this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
36597
|
-
const userData = mkdtempSync3(
|
|
36657
|
+
const userData = mkdtempSync3(join15(tmpdir5(), "gdharness-tests-"));
|
|
36598
36658
|
const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
|
|
36599
36659
|
const hung = await new Promise((resolve) => {
|
|
36600
36660
|
const timer = setTimeout(() => {
|
|
@@ -36610,20 +36670,20 @@ class GodotServer {
|
|
|
36610
36670
|
resolve(false);
|
|
36611
36671
|
});
|
|
36612
36672
|
});
|
|
36613
|
-
const reportsDir =
|
|
36673
|
+
const reportsDir = join15(project.value.path, ".godot", "gdharness-reports");
|
|
36614
36674
|
let report = null;
|
|
36615
36675
|
let reportProblem = null;
|
|
36616
36676
|
try {
|
|
36617
|
-
const written =
|
|
36677
|
+
const written = existsSync12(reportsDir) ? readdirSync6(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
36618
36678
|
const newest = written.at(-1);
|
|
36619
36679
|
if (newest !== undefined) {
|
|
36620
|
-
report = parseJUnit(
|
|
36680
|
+
report = parseJUnit(readFileSync12(join15(reportsDir, newest, "results.xml"), "utf8"));
|
|
36621
36681
|
}
|
|
36622
36682
|
} catch (error) {
|
|
36623
36683
|
reportProblem = errorMessage(error);
|
|
36624
36684
|
} finally {
|
|
36625
|
-
|
|
36626
|
-
|
|
36685
|
+
rmSync8(reportsDir, { recursive: true, force: true });
|
|
36686
|
+
rmSync8(userData, { recursive: true, force: true });
|
|
36627
36687
|
}
|
|
36628
36688
|
const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries;
|
|
36629
36689
|
const verdicts = {
|
|
@@ -36737,6 +36797,7 @@ class GodotServer {
|
|
|
36737
36797
|
staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
|
|
36738
36798
|
retryingBridge: this.bridgeRetry === null ? undefined : true,
|
|
36739
36799
|
announcedAt: this.announcedAt ?? undefined,
|
|
36800
|
+
projectIs: this.projectHasMovedOn() ?? undefined,
|
|
36740
36801
|
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,
|
|
36741
36802
|
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
|
|
36742
36803
|
};
|
|
@@ -36949,7 +37010,8 @@ class GodotServer {
|
|
|
36949
37010
|
log,
|
|
36950
37011
|
startedAt: Date.now(),
|
|
36951
37012
|
exitCode: null,
|
|
36952
|
-
throughEditor: true
|
|
37013
|
+
throughEditor: true,
|
|
37014
|
+
brokeOn: null
|
|
36953
37015
|
};
|
|
36954
37016
|
this.activeProcess = played;
|
|
36955
37017
|
return this.jsonTextResponse({
|
|
@@ -36969,6 +37031,15 @@ class GodotServer {
|
|
|
36969
37031
|
`) ? line : `${line}
|
|
36970
37032
|
`);
|
|
36971
37033
|
}
|
|
37034
|
+
this.recordWhatItBrokeOn(game);
|
|
37035
|
+
}
|
|
37036
|
+
recordWhatItBrokeOn(game) {
|
|
37037
|
+
const halt = this.dapClient?.whereItStopped() ?? null;
|
|
37038
|
+
if (halt?.reason !== "exception" || halt.text === "" || game.brokeOn === halt.text) {
|
|
37039
|
+
return;
|
|
37040
|
+
}
|
|
37041
|
+
game.brokeOn = halt.text;
|
|
37042
|
+
game.log.record("error", halt.text);
|
|
36972
37043
|
}
|
|
36973
37044
|
async endActiveGame() {
|
|
36974
37045
|
const running = this.activeProcess;
|
|
@@ -36990,7 +37061,8 @@ class GodotServer {
|
|
|
36990
37061
|
log,
|
|
36991
37062
|
startedAt: Date.now(),
|
|
36992
37063
|
exitCode: null,
|
|
36993
|
-
throughEditor: false
|
|
37064
|
+
throughEditor: false,
|
|
37065
|
+
brokeOn: null
|
|
36994
37066
|
};
|
|
36995
37067
|
child.stdout.on("data", (data) => {
|
|
36996
37068
|
log.append("stdout", data);
|
|
@@ -37055,6 +37127,7 @@ class GodotServer {
|
|
|
37055
37127
|
contains: readNonEmptyString(args, "contains"),
|
|
37056
37128
|
limit: readPositiveNumber(args, "limit") ?? 200
|
|
37057
37129
|
});
|
|
37130
|
+
const halt = this.activeProcess.throughEditor ? this.dapClient?.whereItStopped() ?? null : null;
|
|
37058
37131
|
return this.jsonTextResponse({
|
|
37059
37132
|
running: this.activeProcess.exitCode === null,
|
|
37060
37133
|
exitCode: this.activeProcess.exitCode,
|
|
@@ -37063,6 +37136,7 @@ class GodotServer {
|
|
|
37063
37136
|
errors: this.activeProcess.log.count("error"),
|
|
37064
37137
|
warnings: this.activeProcess.log.count("warning"),
|
|
37065
37138
|
clean: this.activeProcess.log.count("error") === 0,
|
|
37139
|
+
heldAt: halt,
|
|
37066
37140
|
omitted: selected.omitted,
|
|
37067
37141
|
entries: selected.entries
|
|
37068
37142
|
});
|
|
@@ -37132,8 +37206,8 @@ class GodotServer {
|
|
|
37132
37206
|
return this.createErrorResponse(choice.problem);
|
|
37133
37207
|
}
|
|
37134
37208
|
const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
|
|
37135
|
-
const screenshotDir = expectsScreenshot ? mkdtempSync3(
|
|
37136
|
-
const screenshotPath = screenshotDir ?
|
|
37209
|
+
const screenshotDir = expectsScreenshot ? mkdtempSync3(join15(tmpdir5(), "gdharness-runtime-screenshot-")) : null;
|
|
37210
|
+
const screenshotPath = screenshotDir ? join15(screenshotDir, "capture.png") : null;
|
|
37137
37211
|
try {
|
|
37138
37212
|
const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs);
|
|
37139
37213
|
if (!reply.ok) {
|
|
@@ -37154,12 +37228,12 @@ class GodotServer {
|
|
|
37154
37228
|
return {
|
|
37155
37229
|
content: [
|
|
37156
37230
|
{ type: "text", text: `Screenshot captured: ${dimensions}` },
|
|
37157
|
-
{ type: "image", data:
|
|
37231
|
+
{ type: "image", data: readFileSync12(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
37158
37232
|
]
|
|
37159
37233
|
};
|
|
37160
37234
|
} finally {
|
|
37161
37235
|
if (screenshotDir) {
|
|
37162
|
-
|
|
37236
|
+
rmSync8(screenshotDir, { recursive: true, force: true });
|
|
37163
37237
|
}
|
|
37164
37238
|
}
|
|
37165
37239
|
}
|
|
@@ -37180,11 +37254,13 @@ class GodotServer {
|
|
|
37180
37254
|
if (Object.keys(filters).length === 0) {
|
|
37181
37255
|
return this.createErrorResponse("runtime_inspect find needs at least one of className, script, namePattern, group.");
|
|
37182
37256
|
}
|
|
37257
|
+
const property = readNonEmptyString(args, "property");
|
|
37183
37258
|
return await this.handleRuntimeCommand("find_nodes", {
|
|
37184
37259
|
...filters,
|
|
37185
37260
|
projectPath: args["projectPath"],
|
|
37186
37261
|
root: readNonEmptyString(args, "nodePath") ?? "/root",
|
|
37187
|
-
limit: readPositiveNumber(args, "limit") ?? 100
|
|
37262
|
+
limit: readPositiveNumber(args, "limit") ?? 100,
|
|
37263
|
+
...property === undefined ? {} : { property }
|
|
37188
37264
|
});
|
|
37189
37265
|
}
|
|
37190
37266
|
async handleRuntimeWait(op, args) {
|
|
@@ -37237,10 +37313,11 @@ var init_server3 = __esm(() => {
|
|
|
37237
37313
|
init_resources();
|
|
37238
37314
|
init_runtime_client();
|
|
37239
37315
|
init_server_version();
|
|
37316
|
+
init_setup();
|
|
37240
37317
|
init_tool_definitions();
|
|
37241
37318
|
init_update_check();
|
|
37242
37319
|
run5 = promisify5(execFile5);
|
|
37243
|
-
__dirname2 =
|
|
37320
|
+
__dirname2 = dirname9(fileURLToPath4(import.meta.url));
|
|
37244
37321
|
PATH_SOLUTIONS = [
|
|
37245
37322
|
'Give the path relative to the project, such as "scenes/main.tscn" or "res://scenes/main.tscn"',
|
|
37246
37323
|
"Point projectPath at the project the file belongs to"
|
|
@@ -37297,8 +37374,8 @@ var init_server3 = __esm(() => {
|
|
|
37297
37374
|
});
|
|
37298
37375
|
|
|
37299
37376
|
// src/cli.ts
|
|
37300
|
-
import { existsSync as
|
|
37301
|
-
import { join as
|
|
37377
|
+
import { existsSync as existsSync13 } from "fs";
|
|
37378
|
+
import { join as join16, resolve as resolve5 } from "path";
|
|
37302
37379
|
|
|
37303
37380
|
// src/errors.ts
|
|
37304
37381
|
class Refusal extends Error {
|
|
@@ -38700,7 +38777,7 @@ class UsageError extends Refusal {
|
|
|
38700
38777
|
function projectArgument(at) {
|
|
38701
38778
|
const named = args.slice(at).find((value) => !value.startsWith("--"));
|
|
38702
38779
|
const projectPath = resolve5(named ?? ".");
|
|
38703
|
-
if (!
|
|
38780
|
+
if (!existsSync13(join16(projectPath, "project.godot"))) {
|
|
38704
38781
|
throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
|
|
38705
38782
|
}
|
|
38706
38783
|
return projectPath;
|
|
@@ -38712,7 +38789,7 @@ async function engine() {
|
|
|
38712
38789
|
}
|
|
38713
38790
|
return { godotPath, script: shippedOperationsScript(), debug: GODOT_DEBUG_MODE_DEFAULT2 };
|
|
38714
38791
|
}
|
|
38715
|
-
function
|
|
38792
|
+
function said2(outcome, what) {
|
|
38716
38793
|
if (!outcome.ok) {
|
|
38717
38794
|
throw new Refusal(`${what}: ${outcome.message}`);
|
|
38718
38795
|
}
|
|
@@ -38813,15 +38890,15 @@ async function setup() {
|
|
|
38813
38890
|
const enabled = await enablePlugins(godot, projectPath, EDITOR_PLUGINS);
|
|
38814
38891
|
for (const [index, outcome] of enabled.entries()) {
|
|
38815
38892
|
const name = EDITOR_PLUGINS[index] ?? "";
|
|
38816
|
-
|
|
38893
|
+
said2(outcome, `enabling ${name}`);
|
|
38817
38894
|
console.log(`${name}: ${outcome.ok ? String(outcome.payload["action"]) : "failed"}`);
|
|
38818
38895
|
}
|
|
38819
38896
|
if (runtime) {
|
|
38820
|
-
|
|
38897
|
+
said2(await setRuntime(godot, projectPath, true), "registering the runtime autoload");
|
|
38821
38898
|
console.log(`${RUNTIME_AUTOLOAD.name} autoload registered`);
|
|
38822
38899
|
}
|
|
38823
38900
|
const classes = await runOperation(godot, "refresh_class_cache", {}, projectPath);
|
|
38824
|
-
|
|
38901
|
+
said2(classes, "rebuilding the class list");
|
|
38825
38902
|
if (classes.ok) {
|
|
38826
38903
|
console.log(`class list rebuilt: ${String(classes.payload["classes"])} classes`);
|
|
38827
38904
|
}
|
|
@@ -38844,7 +38921,7 @@ async function setup() {
|
|
|
38844
38921
|
console.log("no harness configured; name one yourself, and gdharness harnesses lists them all");
|
|
38845
38922
|
}
|
|
38846
38923
|
if (!args.includes("--no-skill")) {
|
|
38847
|
-
const directories = skillDirectories(chosen.harnesses, projectPath,
|
|
38924
|
+
const directories = skillDirectories(chosen.harnesses, projectPath, existsSync13);
|
|
38848
38925
|
for (const written of writeSkill(directories, getLocalVersion())) {
|
|
38849
38926
|
console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
|
|
38850
38927
|
}
|
|
@@ -38883,11 +38960,11 @@ async function uninstall() {
|
|
|
38883
38960
|
const disabled = await disablePlugins(godot, projectPath, enabled);
|
|
38884
38961
|
for (const [index, outcome] of disabled.entries()) {
|
|
38885
38962
|
const name = enabled[index] ?? "";
|
|
38886
|
-
|
|
38963
|
+
said2(outcome, `disabling ${name}`);
|
|
38887
38964
|
console.log(`${name}: ${outcome.ok ? String(outcome.payload["action"]) : "failed"}`);
|
|
38888
38965
|
}
|
|
38889
38966
|
if (before.runtimeAutoload) {
|
|
38890
|
-
|
|
38967
|
+
said2(await setRuntime(godot, projectPath, false), "removing the runtime autoload");
|
|
38891
38968
|
console.log(`${RUNTIME_AUTOLOAD.name} autoload removed`);
|
|
38892
38969
|
}
|
|
38893
38970
|
const addons = removeAddons(projectPath);
|
|
@@ -38915,12 +38992,12 @@ async function upgrade() {
|
|
|
38915
38992
|
}
|
|
38916
38993
|
const enabled = await enablePlugins(godot, projectPath, EDITOR_PLUGINS);
|
|
38917
38994
|
for (const [index, outcome] of enabled.entries()) {
|
|
38918
|
-
|
|
38995
|
+
said2(outcome, `enabling ${EDITOR_PLUGINS[index] ?? ""}`);
|
|
38919
38996
|
}
|
|
38920
38997
|
if (before.runtimeAutoload) {
|
|
38921
|
-
|
|
38998
|
+
said2(await setRuntime(godot, projectPath, true), "registering the runtime autoload");
|
|
38922
38999
|
}
|
|
38923
|
-
|
|
39000
|
+
said2(await runOperation(godot, "refresh_class_cache", {}, projectPath), "rebuilding the class list");
|
|
38924
39001
|
const launch = launchFor(version, godot.godotPath, projectPath);
|
|
38925
39002
|
const already = HARNESSES.filter((harness) => registered(harness, projectPath));
|
|
38926
39003
|
for (const group of groupByFile(already, projectPath)) {
|
|
@@ -38929,7 +39006,7 @@ async function upgrade() {
|
|
|
38929
39006
|
if (already.length === 0) {
|
|
38930
39007
|
console.log("no harness config names gdharness, so none was re-pinned");
|
|
38931
39008
|
}
|
|
38932
|
-
for (const written of writeSkill(skillDirectories(already, projectPath,
|
|
39009
|
+
for (const written of writeSkill(skillDirectories(already, projectPath, existsSync13), version)) {
|
|
38933
39010
|
console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
|
|
38934
39011
|
}
|
|
38935
39012
|
console.log(`
|
|
@@ -38965,14 +39042,14 @@ async function runtime3() {
|
|
|
38965
39042
|
}
|
|
38966
39043
|
const projectPath = projectArgument(2);
|
|
38967
39044
|
const godot = await engine();
|
|
38968
|
-
|
|
39045
|
+
said2(await setRuntime(godot, projectPath, state === "on"), `turning the runtime ${state}`);
|
|
38969
39046
|
console.log(`${RUNTIME_AUTOLOAD.name} autoload ${state === "on" ? "registered" : "removed"}`);
|
|
38970
39047
|
}
|
|
38971
39048
|
async function classes() {
|
|
38972
39049
|
const projectPath = projectArgument(1);
|
|
38973
39050
|
const godot = await engine();
|
|
38974
39051
|
const outcome = await runOperation(godot, "refresh_class_cache", {}, projectPath);
|
|
38975
|
-
|
|
39052
|
+
said2(outcome, "rebuilding the class list");
|
|
38976
39053
|
if (outcome.ok) {
|
|
38977
39054
|
console.log(JSON.stringify(outcome.payload, null, 2));
|
|
38978
39055
|
}
|
|
@@ -36,12 +36,19 @@ func get_tree(params: Dictionary) -> Dictionary:
|
|
|
36
36
|
## Nodes matching every filter given, as paths, so a caller can name what it wants without
|
|
37
37
|
## reading the whole tree to find it. `class` matches native classes and their subclasses, and
|
|
38
38
|
## the global name of a script class; `name` is a case-insensitive glob; `script` is a path.
|
|
39
|
+
##
|
|
40
|
+
## `property` names one to read off each of them, which is the difference between one question and
|
|
41
|
+
## one call per answer. A panel of a dozen labels took thirteen calls to read, and a tree that
|
|
42
|
+
## rebuilds between them, which any HUD following a clock does, hands back paths that are gone by
|
|
43
|
+
## the time they are asked about. A node without that property says so rather than answering null,
|
|
44
|
+
## because null is what a node holding null answers.
|
|
39
45
|
func find_nodes(params: Dictionary) -> Dictionary:
|
|
40
46
|
var root_path: String = str(params.get("root", "/root"))
|
|
41
47
|
var wanted_class: String = str(params.get("class", ""))
|
|
42
48
|
var wanted_script: String = str(params.get("script", ""))
|
|
43
49
|
var wanted_name: String = str(params.get("name", ""))
|
|
44
50
|
var wanted_group: String = str(params.get("group", ""))
|
|
51
|
+
var wanted_property: String = str(params.get("property", ""))
|
|
45
52
|
var limit: int = clampi(int(params.get("limit", FIND_LIMIT)), 1, FIND_LIMIT_CEILING)
|
|
46
53
|
|
|
47
54
|
if (
|
|
@@ -67,7 +74,7 @@ func find_nodes(params: Dictionary) -> Dictionary:
|
|
|
67
74
|
if found.size() >= limit:
|
|
68
75
|
truncated = true
|
|
69
76
|
break
|
|
70
|
-
found.append(
|
|
77
|
+
found.append(_found(node, wanted_property))
|
|
71
78
|
var children: Array[Node] = node.get_children()
|
|
72
79
|
for index: int in range(children.size() - 1, -1, -1):
|
|
73
80
|
pending.push_front(children[index])
|
|
@@ -75,6 +82,29 @@ func find_nodes(params: Dictionary) -> Dictionary:
|
|
|
75
82
|
return {"type": "nodes", "count": found.size(), "truncated": truncated, "nodes": found}
|
|
76
83
|
|
|
77
84
|
|
|
85
|
+
## One match, with the named property on it when one was named.
|
|
86
|
+
##
|
|
87
|
+
## `has_property` alongside the value, because a node that has not got it and a node holding null
|
|
88
|
+
## are different answers and a bare null reads as the second. The same distinction the `property`
|
|
89
|
+
## op makes by refusing, which it cannot do here: a find over a panel matches nodes of several
|
|
90
|
+
## classes on purpose, and refusing the whole answer because one of them has no `text` would make
|
|
91
|
+
## the question unaskable.
|
|
92
|
+
func _found(node: Node, wanted_property: String) -> Dictionary:
|
|
93
|
+
var entry: Dictionary = _serialize_node(node, false)
|
|
94
|
+
if wanted_property.is_empty():
|
|
95
|
+
return entry
|
|
96
|
+
var has: bool = false
|
|
97
|
+
for prop: Dictionary in node.get_property_list():
|
|
98
|
+
if str(prop["name"]) == wanted_property:
|
|
99
|
+
has = true
|
|
100
|
+
break
|
|
101
|
+
entry["property"] = wanted_property
|
|
102
|
+
entry["has_property"] = has
|
|
103
|
+
if has:
|
|
104
|
+
entry["value"] = _values.serialize(node.get(wanted_property))
|
|
105
|
+
return entry
|
|
106
|
+
|
|
107
|
+
|
|
78
108
|
func _matches(
|
|
79
109
|
node: Node, wanted_class: String, wanted_script: String, wanted_name: String, wanted_group: String
|
|
80
110
|
) -> bool:
|
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";
|
|
@@ -24878,6 +24878,10 @@ var DEFAULT_DAP_PORT = 6006;
|
|
|
24878
24878
|
var DAP_REQUEST_TIMEOUT_MS = 1e4;
|
|
24879
24879
|
var SCOPES_TIMEOUT_MS = 20000;
|
|
24880
24880
|
var VARIABLES_POLL_TIMEOUT_MS = 2000;
|
|
24881
|
+
function said(body, field) {
|
|
24882
|
+
const value = body?.[field];
|
|
24883
|
+
return typeof value === "string" ? value : "";
|
|
24884
|
+
}
|
|
24881
24885
|
|
|
24882
24886
|
class GodotDAPClient {
|
|
24883
24887
|
socket = null;
|
|
@@ -24892,7 +24896,7 @@ class GodotDAPClient {
|
|
|
24892
24896
|
initialized = false;
|
|
24893
24897
|
attached = false;
|
|
24894
24898
|
lastThreadId = 1;
|
|
24895
|
-
|
|
24899
|
+
halt = null;
|
|
24896
24900
|
breakpoints = new Map;
|
|
24897
24901
|
constructor(port = portFromEnv("GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT), host = "127.0.0.1") {
|
|
24898
24902
|
this.port = port;
|
|
@@ -24941,7 +24945,7 @@ class GodotDAPClient {
|
|
|
24941
24945
|
this.connected = false;
|
|
24942
24946
|
this.initialized = false;
|
|
24943
24947
|
this.attached = false;
|
|
24944
|
-
this.
|
|
24948
|
+
this.halt = null;
|
|
24945
24949
|
this.socket = null;
|
|
24946
24950
|
this.failPendingRequests(new Error("DAP connection closed"));
|
|
24947
24951
|
});
|
|
@@ -24961,7 +24965,7 @@ class GodotDAPClient {
|
|
|
24961
24965
|
this.connected = false;
|
|
24962
24966
|
this.initialized = false;
|
|
24963
24967
|
this.attached = false;
|
|
24964
|
-
this.
|
|
24968
|
+
this.halt = null;
|
|
24965
24969
|
return;
|
|
24966
24970
|
}
|
|
24967
24971
|
if (this.connected) {
|
|
@@ -24990,7 +24994,7 @@ class GodotDAPClient {
|
|
|
24990
24994
|
this.connected = false;
|
|
24991
24995
|
this.initialized = false;
|
|
24992
24996
|
this.attached = false;
|
|
24993
|
-
this.
|
|
24997
|
+
this.halt = null;
|
|
24994
24998
|
}
|
|
24995
24999
|
async ensureConnected() {
|
|
24996
25000
|
if (!this.connected) {
|
|
@@ -25067,12 +25071,16 @@ class GodotDAPClient {
|
|
|
25067
25071
|
if (typeof threadId === "number") {
|
|
25068
25072
|
this.lastThreadId = threadId;
|
|
25069
25073
|
}
|
|
25070
|
-
this.
|
|
25074
|
+
this.halt = {
|
|
25075
|
+
reason: said(body, "reason"),
|
|
25076
|
+
description: said(body, "description"),
|
|
25077
|
+
text: said(body, "text")
|
|
25078
|
+
};
|
|
25071
25079
|
return;
|
|
25072
25080
|
}
|
|
25073
25081
|
if (eventName === "terminated" || eventName === "exited") {
|
|
25074
25082
|
this.attached = false;
|
|
25075
|
-
this.
|
|
25083
|
+
this.halt = null;
|
|
25076
25084
|
}
|
|
25077
25085
|
}
|
|
25078
25086
|
async initialize() {
|
|
@@ -25146,7 +25154,7 @@ class GodotDAPClient {
|
|
|
25146
25154
|
await this.attach();
|
|
25147
25155
|
const resolvedThreadId = await this.resolveThreadId(threadId);
|
|
25148
25156
|
await this.sendRequest("continue", { threadId: resolvedThreadId });
|
|
25149
|
-
this.
|
|
25157
|
+
this.halt = null;
|
|
25150
25158
|
}
|
|
25151
25159
|
async stepOver(threadId) {
|
|
25152
25160
|
await this.attach();
|
|
@@ -25217,7 +25225,10 @@ class GodotDAPClient {
|
|
|
25217
25225
|
return this.connected;
|
|
25218
25226
|
}
|
|
25219
25227
|
isStopped() {
|
|
25220
|
-
return this.
|
|
25228
|
+
return this.halt !== null;
|
|
25229
|
+
}
|
|
25230
|
+
whereItStopped() {
|
|
25231
|
+
return this.halt;
|
|
25221
25232
|
}
|
|
25222
25233
|
async resolveThreadId(threadId) {
|
|
25223
25234
|
if (typeof threadId === "number" && threadId > 0) {
|
|
@@ -25246,7 +25257,7 @@ class GodotDAPClient {
|
|
|
25246
25257
|
this.connected = false;
|
|
25247
25258
|
this.initialized = false;
|
|
25248
25259
|
this.attached = false;
|
|
25249
|
-
this.
|
|
25260
|
+
this.halt = null;
|
|
25250
25261
|
this.reader = new FrameReader;
|
|
25251
25262
|
socket?.destroy();
|
|
25252
25263
|
this.failPendingRequests(new Error(`Godot DAP ${detail}. The connection was dropped.`));
|
|
@@ -25402,6 +25413,16 @@ class GameLog {
|
|
|
25402
25413
|
this.entries.push(entry);
|
|
25403
25414
|
this.lastHeadline = headline ? { index: entry.index, detail } : null;
|
|
25404
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
|
+
}
|
|
25405
25426
|
get all() {
|
|
25406
25427
|
return this.entries;
|
|
25407
25428
|
}
|
|
@@ -27557,6 +27578,20 @@ function runtimeRequest(endpoint, command, params, timeoutMs) {
|
|
|
27557
27578
|
});
|
|
27558
27579
|
}
|
|
27559
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
|
+
|
|
27560
27595
|
// src/tool-definitions.ts
|
|
27561
27596
|
var PROJECT_PATH = {
|
|
27562
27597
|
type: "string",
|
|
@@ -28230,7 +28265,10 @@ var TOOL_SPECS = [
|
|
|
28230
28265
|
type: "string",
|
|
28231
28266
|
description: "tree, find: where to start, default /root. rect: the node to place. property: the node to read."
|
|
28232
28267
|
},
|
|
28233
|
-
property: {
|
|
28268
|
+
property: {
|
|
28269
|
+
type: "string",
|
|
28270
|
+
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."
|
|
28271
|
+
},
|
|
28234
28272
|
depth: { type: "number", description: "tree: levels to descend. Default 3." },
|
|
28235
28273
|
includeProperties: {
|
|
28236
28274
|
type: "boolean",
|
|
@@ -28257,7 +28295,7 @@ var TOOL_SPECS = [
|
|
|
28257
28295
|
operations: {
|
|
28258
28296
|
tree: { summary: "the live scene tree", requires: [] },
|
|
28259
28297
|
find: {
|
|
28260
|
-
summary: "the paths of every node matching className, script, namePattern or group",
|
|
28298
|
+
summary: "the paths of every node matching className, script, namePattern or group, with property read off each",
|
|
28261
28299
|
requires: []
|
|
28262
28300
|
},
|
|
28263
28301
|
rect: {
|
|
@@ -28472,7 +28510,7 @@ function buildToolDefinitions() {
|
|
|
28472
28510
|
var UPDATE_NOTICE_EVERY = 500;
|
|
28473
28511
|
var FEEDBACK_NOTICE_EVERY = 250;
|
|
28474
28512
|
var run3 = promisify3(execFile3);
|
|
28475
|
-
var __dirname2 =
|
|
28513
|
+
var __dirname2 = dirname4(fileURLToPath2(import.meta.url));
|
|
28476
28514
|
var EDITOR_RESTART_TIMEOUT_MS = 90000;
|
|
28477
28515
|
var BRIDGE_RETRY_MS = 2000;
|
|
28478
28516
|
var PATH_SOLUTIONS = [
|
|
@@ -28550,11 +28588,11 @@ function realPathOr(path) {
|
|
|
28550
28588
|
}
|
|
28551
28589
|
}
|
|
28552
28590
|
function hasMainScene(projectFile) {
|
|
28553
|
-
const scene = parseProjectGodot(
|
|
28591
|
+
const scene = parseProjectGodot(readFileSync8(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
28554
28592
|
return typeof scene === "string" && scene !== "";
|
|
28555
28593
|
}
|
|
28556
28594
|
function editorPlaysHeadless(projectFile) {
|
|
28557
|
-
const runArgs = parseProjectGodot(
|
|
28595
|
+
const runArgs = parseProjectGodot(readFileSync8(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
28558
28596
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
28559
28597
|
}
|
|
28560
28598
|
function camelCased(params) {
|
|
@@ -28570,7 +28608,7 @@ function camelCased(params) {
|
|
|
28570
28608
|
class GodotServer {
|
|
28571
28609
|
mcp;
|
|
28572
28610
|
locator = new GodotLocator;
|
|
28573
|
-
operationsScript =
|
|
28611
|
+
operationsScript = join9(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
28574
28612
|
godotBridge;
|
|
28575
28613
|
tools = buildToolDefinitions();
|
|
28576
28614
|
activeProcess = null;
|
|
@@ -28808,28 +28846,44 @@ class GodotServer {
|
|
|
28808
28846
|
if (this.callsSinceNotice < UPDATE_NOTICE_EVERY && this.noticedUpdate) {
|
|
28809
28847
|
return answer;
|
|
28810
28848
|
}
|
|
28849
|
+
const moved = this.projectHasMovedOn();
|
|
28850
|
+
if (moved !== null) {
|
|
28851
|
+
this.noticedUpdate = true;
|
|
28852
|
+
this.callsSinceNotice = 0;
|
|
28853
|
+
return this.saying(answer, {
|
|
28854
|
+
project_upgraded_under_this_server: {
|
|
28855
|
+
server_is: SERVER_VERSION,
|
|
28856
|
+
project_is: moved,
|
|
28857
|
+
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."
|
|
28858
|
+
}
|
|
28859
|
+
});
|
|
28860
|
+
}
|
|
28811
28861
|
const notice = this.updates.notice();
|
|
28812
28862
|
if (notice === null) {
|
|
28813
28863
|
return answer;
|
|
28814
28864
|
}
|
|
28815
28865
|
this.noticedUpdate = true;
|
|
28816
28866
|
this.callsSinceNotice = 0;
|
|
28867
|
+
return this.saying(answer, {
|
|
28868
|
+
update_available: {
|
|
28869
|
+
...notice,
|
|
28870
|
+
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."
|
|
28871
|
+
}
|
|
28872
|
+
});
|
|
28873
|
+
}
|
|
28874
|
+
saying(answer, block) {
|
|
28817
28875
|
return {
|
|
28818
28876
|
...answer,
|
|
28819
|
-
content: [
|
|
28820
|
-
...answer.content,
|
|
28821
|
-
{
|
|
28822
|
-
type: "text",
|
|
28823
|
-
text: JSON.stringify({
|
|
28824
|
-
update_available: {
|
|
28825
|
-
...notice,
|
|
28826
|
-
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."
|
|
28827
|
-
}
|
|
28828
|
-
}, null, 2)
|
|
28829
|
-
}
|
|
28830
|
-
]
|
|
28877
|
+
content: [...answer.content, { type: "text", text: JSON.stringify(block, null, 2) }]
|
|
28831
28878
|
};
|
|
28832
28879
|
}
|
|
28880
|
+
projectHasMovedOn() {
|
|
28881
|
+
if (this.ownProject === null) {
|
|
28882
|
+
return null;
|
|
28883
|
+
}
|
|
28884
|
+
const installed = installedAddonVersion(this.ownProject);
|
|
28885
|
+
return installed === null || installed === SERVER_VERSION ? null : installed;
|
|
28886
|
+
}
|
|
28833
28887
|
withFeedbackNotice(answer) {
|
|
28834
28888
|
this.callsSinceFeedback += 1;
|
|
28835
28889
|
if (this.callsSinceFeedback < FEEDBACK_NOTICE_EVERY) {
|
|
@@ -29075,8 +29129,8 @@ class GodotServer {
|
|
|
29075
29129
|
if (path === undefined) {
|
|
29076
29130
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
29077
29131
|
}
|
|
29078
|
-
const file =
|
|
29079
|
-
if (!
|
|
29132
|
+
const file = join9(path, "project.godot");
|
|
29133
|
+
if (!existsSync7(file)) {
|
|
29080
29134
|
return {
|
|
29081
29135
|
ok: false,
|
|
29082
29136
|
response: this.createErrorResponse(`Not a Godot project: ${path}`, [
|
|
@@ -29221,7 +29275,7 @@ class GodotServer {
|
|
|
29221
29275
|
if (!engine.ok) {
|
|
29222
29276
|
return engine.response;
|
|
29223
29277
|
}
|
|
29224
|
-
const application = parseProjectGodot(
|
|
29278
|
+
const application = parseProjectGodot(readFileSync8(project.value.file, "utf8"))["application"] ?? {};
|
|
29225
29279
|
const name = application["config/name"];
|
|
29226
29280
|
const mainScene = application["run/main_scene"];
|
|
29227
29281
|
const info = {
|
|
@@ -29310,7 +29364,7 @@ class GodotServer {
|
|
|
29310
29364
|
log.finish();
|
|
29311
29365
|
const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
|
|
29312
29366
|
const verdict = {
|
|
29313
|
-
exported: exitCode === 0 && log.count("error") === 0 &&
|
|
29367
|
+
exported: exitCode === 0 && log.count("error") === 0 && existsSync7(output.absolutePath),
|
|
29314
29368
|
preset,
|
|
29315
29369
|
outputPath: output.relativePath,
|
|
29316
29370
|
debug,
|
|
@@ -29340,7 +29394,7 @@ class GodotServer {
|
|
|
29340
29394
|
return contained.response;
|
|
29341
29395
|
}
|
|
29342
29396
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
29343
|
-
if (!
|
|
29397
|
+
if (!existsSync7(join9(project.value.path, runner))) {
|
|
29344
29398
|
return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
|
|
29345
29399
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
29346
29400
|
]);
|
|
@@ -29373,7 +29427,7 @@ class GodotServer {
|
|
|
29373
29427
|
];
|
|
29374
29428
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
|
|
29375
29429
|
this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
29376
|
-
const userData = mkdtempSync2(
|
|
29430
|
+
const userData = mkdtempSync2(join9(tmpdir4(), "gdharness-tests-"));
|
|
29377
29431
|
const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
|
|
29378
29432
|
const hung = await new Promise((resolve) => {
|
|
29379
29433
|
const timer = setTimeout(() => {
|
|
@@ -29389,20 +29443,20 @@ class GodotServer {
|
|
|
29389
29443
|
resolve(false);
|
|
29390
29444
|
});
|
|
29391
29445
|
});
|
|
29392
|
-
const reportsDir =
|
|
29446
|
+
const reportsDir = join9(project.value.path, ".godot", "gdharness-reports");
|
|
29393
29447
|
let report = null;
|
|
29394
29448
|
let reportProblem = null;
|
|
29395
29449
|
try {
|
|
29396
|
-
const written =
|
|
29450
|
+
const written = existsSync7(reportsDir) ? readdirSync5(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
29397
29451
|
const newest = written.at(-1);
|
|
29398
29452
|
if (newest !== undefined) {
|
|
29399
|
-
report = parseJUnit(
|
|
29453
|
+
report = parseJUnit(readFileSync8(join9(reportsDir, newest, "results.xml"), "utf8"));
|
|
29400
29454
|
}
|
|
29401
29455
|
} catch (error) {
|
|
29402
29456
|
reportProblem = errorMessage(error);
|
|
29403
29457
|
} finally {
|
|
29404
|
-
|
|
29405
|
-
|
|
29458
|
+
rmSync4(reportsDir, { recursive: true, force: true });
|
|
29459
|
+
rmSync4(userData, { recursive: true, force: true });
|
|
29406
29460
|
}
|
|
29407
29461
|
const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries;
|
|
29408
29462
|
const verdicts = {
|
|
@@ -29516,6 +29570,7 @@ class GodotServer {
|
|
|
29516
29570
|
staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
|
|
29517
29571
|
retryingBridge: this.bridgeRetry === null ? undefined : true,
|
|
29518
29572
|
announcedAt: this.announcedAt ?? undefined,
|
|
29573
|
+
projectIs: this.projectHasMovedOn() ?? undefined,
|
|
29519
29574
|
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,
|
|
29520
29575
|
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
|
|
29521
29576
|
};
|
|
@@ -29728,7 +29783,8 @@ class GodotServer {
|
|
|
29728
29783
|
log,
|
|
29729
29784
|
startedAt: Date.now(),
|
|
29730
29785
|
exitCode: null,
|
|
29731
|
-
throughEditor: true
|
|
29786
|
+
throughEditor: true,
|
|
29787
|
+
brokeOn: null
|
|
29732
29788
|
};
|
|
29733
29789
|
this.activeProcess = played;
|
|
29734
29790
|
return this.jsonTextResponse({
|
|
@@ -29748,6 +29804,15 @@ class GodotServer {
|
|
|
29748
29804
|
`) ? line : `${line}
|
|
29749
29805
|
`);
|
|
29750
29806
|
}
|
|
29807
|
+
this.recordWhatItBrokeOn(game);
|
|
29808
|
+
}
|
|
29809
|
+
recordWhatItBrokeOn(game) {
|
|
29810
|
+
const halt = this.dapClient?.whereItStopped() ?? null;
|
|
29811
|
+
if (halt?.reason !== "exception" || halt.text === "" || game.brokeOn === halt.text) {
|
|
29812
|
+
return;
|
|
29813
|
+
}
|
|
29814
|
+
game.brokeOn = halt.text;
|
|
29815
|
+
game.log.record("error", halt.text);
|
|
29751
29816
|
}
|
|
29752
29817
|
async endActiveGame() {
|
|
29753
29818
|
const running = this.activeProcess;
|
|
@@ -29769,7 +29834,8 @@ class GodotServer {
|
|
|
29769
29834
|
log,
|
|
29770
29835
|
startedAt: Date.now(),
|
|
29771
29836
|
exitCode: null,
|
|
29772
|
-
throughEditor: false
|
|
29837
|
+
throughEditor: false,
|
|
29838
|
+
brokeOn: null
|
|
29773
29839
|
};
|
|
29774
29840
|
child.stdout.on("data", (data) => {
|
|
29775
29841
|
log.append("stdout", data);
|
|
@@ -29834,6 +29900,7 @@ class GodotServer {
|
|
|
29834
29900
|
contains: readNonEmptyString(args, "contains"),
|
|
29835
29901
|
limit: readPositiveNumber(args, "limit") ?? 200
|
|
29836
29902
|
});
|
|
29903
|
+
const halt = this.activeProcess.throughEditor ? this.dapClient?.whereItStopped() ?? null : null;
|
|
29837
29904
|
return this.jsonTextResponse({
|
|
29838
29905
|
running: this.activeProcess.exitCode === null,
|
|
29839
29906
|
exitCode: this.activeProcess.exitCode,
|
|
@@ -29842,6 +29909,7 @@ class GodotServer {
|
|
|
29842
29909
|
errors: this.activeProcess.log.count("error"),
|
|
29843
29910
|
warnings: this.activeProcess.log.count("warning"),
|
|
29844
29911
|
clean: this.activeProcess.log.count("error") === 0,
|
|
29912
|
+
heldAt: halt,
|
|
29845
29913
|
omitted: selected.omitted,
|
|
29846
29914
|
entries: selected.entries
|
|
29847
29915
|
});
|
|
@@ -29911,8 +29979,8 @@ class GodotServer {
|
|
|
29911
29979
|
return this.createErrorResponse(choice.problem);
|
|
29912
29980
|
}
|
|
29913
29981
|
const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
|
|
29914
|
-
const screenshotDir = expectsScreenshot ? mkdtempSync2(
|
|
29915
|
-
const screenshotPath = screenshotDir ?
|
|
29982
|
+
const screenshotDir = expectsScreenshot ? mkdtempSync2(join9(tmpdir4(), "gdharness-runtime-screenshot-")) : null;
|
|
29983
|
+
const screenshotPath = screenshotDir ? join9(screenshotDir, "capture.png") : null;
|
|
29916
29984
|
try {
|
|
29917
29985
|
const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs);
|
|
29918
29986
|
if (!reply.ok) {
|
|
@@ -29933,12 +30001,12 @@ class GodotServer {
|
|
|
29933
30001
|
return {
|
|
29934
30002
|
content: [
|
|
29935
30003
|
{ type: "text", text: `Screenshot captured: ${dimensions}` },
|
|
29936
|
-
{ type: "image", data:
|
|
30004
|
+
{ type: "image", data: readFileSync8(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
29937
30005
|
]
|
|
29938
30006
|
};
|
|
29939
30007
|
} finally {
|
|
29940
30008
|
if (screenshotDir) {
|
|
29941
|
-
|
|
30009
|
+
rmSync4(screenshotDir, { recursive: true, force: true });
|
|
29942
30010
|
}
|
|
29943
30011
|
}
|
|
29944
30012
|
}
|
|
@@ -29959,11 +30027,13 @@ class GodotServer {
|
|
|
29959
30027
|
if (Object.keys(filters).length === 0) {
|
|
29960
30028
|
return this.createErrorResponse("runtime_inspect find needs at least one of className, script, namePattern, group.");
|
|
29961
30029
|
}
|
|
30030
|
+
const property = readNonEmptyString(args, "property");
|
|
29962
30031
|
return await this.handleRuntimeCommand("find_nodes", {
|
|
29963
30032
|
...filters,
|
|
29964
30033
|
projectPath: args["projectPath"],
|
|
29965
30034
|
root: readNonEmptyString(args, "nodePath") ?? "/root",
|
|
29966
|
-
limit: readPositiveNumber(args, "limit") ?? 100
|
|
30035
|
+
limit: readPositiveNumber(args, "limit") ?? 100,
|
|
30036
|
+
...property === undefined ? {} : { property }
|
|
29967
30037
|
});
|
|
29968
30038
|
}
|
|
29969
30039
|
async handleRuntimeWait(op, args) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.11",
|
|
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",
|