gdharness 0.5.9 → 0.5.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/cli.js +132 -60
- package/build/index.js +109 -44
- 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
|
}
|
|
@@ -30785,6 +30795,10 @@ var init_ports = __esm(() => {
|
|
|
30785
30795
|
// src/dap_client.ts
|
|
30786
30796
|
import { createConnection } from "node:net";
|
|
30787
30797
|
import { setTimeout as delay } from "node:timers/promises";
|
|
30798
|
+
function said(body, field) {
|
|
30799
|
+
const value = body?.[field];
|
|
30800
|
+
return typeof value === "string" ? value : "";
|
|
30801
|
+
}
|
|
30788
30802
|
|
|
30789
30803
|
class GodotDAPClient {
|
|
30790
30804
|
socket = null;
|
|
@@ -30799,7 +30813,7 @@ class GodotDAPClient {
|
|
|
30799
30813
|
initialized = false;
|
|
30800
30814
|
attached = false;
|
|
30801
30815
|
lastThreadId = 1;
|
|
30802
|
-
|
|
30816
|
+
halt = null;
|
|
30803
30817
|
breakpoints = new Map;
|
|
30804
30818
|
constructor(port = portFromEnv("GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT), host = "127.0.0.1") {
|
|
30805
30819
|
this.port = port;
|
|
@@ -30848,7 +30862,7 @@ class GodotDAPClient {
|
|
|
30848
30862
|
this.connected = false;
|
|
30849
30863
|
this.initialized = false;
|
|
30850
30864
|
this.attached = false;
|
|
30851
|
-
this.
|
|
30865
|
+
this.halt = null;
|
|
30852
30866
|
this.socket = null;
|
|
30853
30867
|
this.failPendingRequests(new Error("DAP connection closed"));
|
|
30854
30868
|
});
|
|
@@ -30868,7 +30882,7 @@ class GodotDAPClient {
|
|
|
30868
30882
|
this.connected = false;
|
|
30869
30883
|
this.initialized = false;
|
|
30870
30884
|
this.attached = false;
|
|
30871
|
-
this.
|
|
30885
|
+
this.halt = null;
|
|
30872
30886
|
return;
|
|
30873
30887
|
}
|
|
30874
30888
|
if (this.connected) {
|
|
@@ -30897,7 +30911,7 @@ class GodotDAPClient {
|
|
|
30897
30911
|
this.connected = false;
|
|
30898
30912
|
this.initialized = false;
|
|
30899
30913
|
this.attached = false;
|
|
30900
|
-
this.
|
|
30914
|
+
this.halt = null;
|
|
30901
30915
|
}
|
|
30902
30916
|
async ensureConnected() {
|
|
30903
30917
|
if (!this.connected) {
|
|
@@ -30974,12 +30988,16 @@ class GodotDAPClient {
|
|
|
30974
30988
|
if (typeof threadId === "number") {
|
|
30975
30989
|
this.lastThreadId = threadId;
|
|
30976
30990
|
}
|
|
30977
|
-
this.
|
|
30991
|
+
this.halt = {
|
|
30992
|
+
reason: said(body, "reason"),
|
|
30993
|
+
description: said(body, "description"),
|
|
30994
|
+
text: said(body, "text")
|
|
30995
|
+
};
|
|
30978
30996
|
return;
|
|
30979
30997
|
}
|
|
30980
30998
|
if (eventName === "terminated" || eventName === "exited") {
|
|
30981
30999
|
this.attached = false;
|
|
30982
|
-
this.
|
|
31000
|
+
this.halt = null;
|
|
30983
31001
|
}
|
|
30984
31002
|
}
|
|
30985
31003
|
async initialize() {
|
|
@@ -31053,7 +31071,7 @@ class GodotDAPClient {
|
|
|
31053
31071
|
await this.attach();
|
|
31054
31072
|
const resolvedThreadId = await this.resolveThreadId(threadId);
|
|
31055
31073
|
await this.sendRequest("continue", { threadId: resolvedThreadId });
|
|
31056
|
-
this.
|
|
31074
|
+
this.halt = null;
|
|
31057
31075
|
}
|
|
31058
31076
|
async stepOver(threadId) {
|
|
31059
31077
|
await this.attach();
|
|
@@ -31124,7 +31142,10 @@ class GodotDAPClient {
|
|
|
31124
31142
|
return this.connected;
|
|
31125
31143
|
}
|
|
31126
31144
|
isStopped() {
|
|
31127
|
-
return this.
|
|
31145
|
+
return this.halt !== null;
|
|
31146
|
+
}
|
|
31147
|
+
whereItStopped() {
|
|
31148
|
+
return this.halt;
|
|
31128
31149
|
}
|
|
31129
31150
|
async resolveThreadId(threadId) {
|
|
31130
31151
|
if (typeof threadId === "number" && threadId > 0) {
|
|
@@ -31153,7 +31174,7 @@ class GodotDAPClient {
|
|
|
31153
31174
|
this.connected = false;
|
|
31154
31175
|
this.initialized = false;
|
|
31155
31176
|
this.attached = false;
|
|
31156
|
-
this.
|
|
31177
|
+
this.halt = null;
|
|
31157
31178
|
this.reader = new FrameReader;
|
|
31158
31179
|
socket?.destroy();
|
|
31159
31180
|
this.failPendingRequests(new Error(`Godot DAP ${detail}. The connection was dropped.`));
|
|
@@ -35741,11 +35762,31 @@ var init_runtime_client = __esm(() => {
|
|
|
35741
35762
|
ANNOUNCEMENT_PATTERN = /^runtime-(\d+)\.json$/;
|
|
35742
35763
|
});
|
|
35743
35764
|
|
|
35765
|
+
// src/setup.ts
|
|
35766
|
+
import { cpSync as cpSync2, existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync11, rmSync as rmSync7, writeFileSync as writeFileSync8 } from "node:fs";
|
|
35767
|
+
import { dirname as dirname8, join as join14 } from "node:path";
|
|
35768
|
+
function installedAddonVersion(projectPath) {
|
|
35769
|
+
const marker = join14(projectPath, "addons", ADDONS2[0], VERSION_MARKER2);
|
|
35770
|
+
if (!existsSync11(marker)) {
|
|
35771
|
+
return null;
|
|
35772
|
+
}
|
|
35773
|
+
const said = readFileSync11(marker, "utf8").trim();
|
|
35774
|
+
return said === "" ? null : said;
|
|
35775
|
+
}
|
|
35776
|
+
var ADDONS2, VERSION_MARKER2 = ".gdharness-version";
|
|
35777
|
+
var init_setup = __esm(() => {
|
|
35778
|
+
init_class_cache();
|
|
35779
|
+
init_headless();
|
|
35780
|
+
init_resources();
|
|
35781
|
+
init_server_version();
|
|
35782
|
+
ADDONS2 = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
|
|
35783
|
+
});
|
|
35784
|
+
|
|
35744
35785
|
// src/server.ts
|
|
35745
35786
|
import { execFile as execFile5, spawn } from "node:child_process";
|
|
35746
|
-
import { existsSync as
|
|
35787
|
+
import { existsSync as existsSync12, mkdtempSync as mkdtempSync3, readdirSync as readdirSync6, readFileSync as readFileSync12, realpathSync as realpathSync2, rmSync as rmSync8 } from "node:fs";
|
|
35747
35788
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
35748
|
-
import { basename as basename2, dirname as
|
|
35789
|
+
import { basename as basename2, dirname as dirname9, join as join15, normalize as normalize3 } from "node:path";
|
|
35749
35790
|
import { setTimeout as delay2 } from "node:timers/promises";
|
|
35750
35791
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
35751
35792
|
import { promisify as promisify5 } from "node:util";
|
|
@@ -35771,11 +35812,11 @@ function realPathOr(path) {
|
|
|
35771
35812
|
}
|
|
35772
35813
|
}
|
|
35773
35814
|
function hasMainScene(projectFile) {
|
|
35774
|
-
const scene = parseProjectGodot(
|
|
35815
|
+
const scene = parseProjectGodot(readFileSync12(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
35775
35816
|
return typeof scene === "string" && scene !== "";
|
|
35776
35817
|
}
|
|
35777
35818
|
function editorPlaysHeadless(projectFile) {
|
|
35778
|
-
const runArgs = parseProjectGodot(
|
|
35819
|
+
const runArgs = parseProjectGodot(readFileSync12(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
35779
35820
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
35780
35821
|
}
|
|
35781
35822
|
function camelCased(params) {
|
|
@@ -35791,7 +35832,7 @@ function camelCased(params) {
|
|
|
35791
35832
|
class GodotServer {
|
|
35792
35833
|
mcp;
|
|
35793
35834
|
locator = new GodotLocator2;
|
|
35794
|
-
operationsScript =
|
|
35835
|
+
operationsScript = join15(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
35795
35836
|
godotBridge;
|
|
35796
35837
|
tools = buildToolDefinitions();
|
|
35797
35838
|
activeProcess = null;
|
|
@@ -36029,28 +36070,44 @@ class GodotServer {
|
|
|
36029
36070
|
if (this.callsSinceNotice < UPDATE_NOTICE_EVERY && this.noticedUpdate) {
|
|
36030
36071
|
return answer;
|
|
36031
36072
|
}
|
|
36073
|
+
const moved = this.projectHasMovedOn();
|
|
36074
|
+
if (moved !== null) {
|
|
36075
|
+
this.noticedUpdate = true;
|
|
36076
|
+
this.callsSinceNotice = 0;
|
|
36077
|
+
return this.saying(answer, {
|
|
36078
|
+
project_upgraded_under_this_server: {
|
|
36079
|
+
server_is: SERVER_VERSION,
|
|
36080
|
+
project_is: moved,
|
|
36081
|
+
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."
|
|
36082
|
+
}
|
|
36083
|
+
});
|
|
36084
|
+
}
|
|
36032
36085
|
const notice = this.updates.notice();
|
|
36033
36086
|
if (notice === null) {
|
|
36034
36087
|
return answer;
|
|
36035
36088
|
}
|
|
36036
36089
|
this.noticedUpdate = true;
|
|
36037
36090
|
this.callsSinceNotice = 0;
|
|
36091
|
+
return this.saying(answer, {
|
|
36092
|
+
update_available: {
|
|
36093
|
+
...notice,
|
|
36094
|
+
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."
|
|
36095
|
+
}
|
|
36096
|
+
});
|
|
36097
|
+
}
|
|
36098
|
+
saying(answer, block) {
|
|
36038
36099
|
return {
|
|
36039
36100
|
...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
|
-
]
|
|
36101
|
+
content: [...answer.content, { type: "text", text: JSON.stringify(block, null, 2) }]
|
|
36052
36102
|
};
|
|
36053
36103
|
}
|
|
36104
|
+
projectHasMovedOn() {
|
|
36105
|
+
if (this.ownProject === null) {
|
|
36106
|
+
return null;
|
|
36107
|
+
}
|
|
36108
|
+
const installed = installedAddonVersion(this.ownProject);
|
|
36109
|
+
return installed === null || installed === SERVER_VERSION ? null : installed;
|
|
36110
|
+
}
|
|
36054
36111
|
withFeedbackNotice(answer) {
|
|
36055
36112
|
this.callsSinceFeedback += 1;
|
|
36056
36113
|
if (this.callsSinceFeedback < FEEDBACK_NOTICE_EVERY) {
|
|
@@ -36296,8 +36353,8 @@ class GodotServer {
|
|
|
36296
36353
|
if (path === undefined) {
|
|
36297
36354
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
36298
36355
|
}
|
|
36299
|
-
const file =
|
|
36300
|
-
if (!
|
|
36356
|
+
const file = join15(path, "project.godot");
|
|
36357
|
+
if (!existsSync12(file)) {
|
|
36301
36358
|
return {
|
|
36302
36359
|
ok: false,
|
|
36303
36360
|
response: this.createErrorResponse(`Not a Godot project: ${path}`, [
|
|
@@ -36442,7 +36499,7 @@ class GodotServer {
|
|
|
36442
36499
|
if (!engine.ok) {
|
|
36443
36500
|
return engine.response;
|
|
36444
36501
|
}
|
|
36445
|
-
const application = parseProjectGodot(
|
|
36502
|
+
const application = parseProjectGodot(readFileSync12(project.value.file, "utf8"))["application"] ?? {};
|
|
36446
36503
|
const name = application["config/name"];
|
|
36447
36504
|
const mainScene = application["run/main_scene"];
|
|
36448
36505
|
const info = {
|
|
@@ -36531,7 +36588,7 @@ class GodotServer {
|
|
|
36531
36588
|
log.finish();
|
|
36532
36589
|
const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
|
|
36533
36590
|
const verdict = {
|
|
36534
|
-
exported: exitCode === 0 && log.count("error") === 0 &&
|
|
36591
|
+
exported: exitCode === 0 && log.count("error") === 0 && existsSync12(output.absolutePath),
|
|
36535
36592
|
preset,
|
|
36536
36593
|
outputPath: output.relativePath,
|
|
36537
36594
|
debug,
|
|
@@ -36561,7 +36618,7 @@ class GodotServer {
|
|
|
36561
36618
|
return contained.response;
|
|
36562
36619
|
}
|
|
36563
36620
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
36564
|
-
if (!
|
|
36621
|
+
if (!existsSync12(join15(project.value.path, runner))) {
|
|
36565
36622
|
return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
|
|
36566
36623
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
36567
36624
|
]);
|
|
@@ -36594,7 +36651,7 @@ class GodotServer {
|
|
|
36594
36651
|
];
|
|
36595
36652
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
|
|
36596
36653
|
this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
36597
|
-
const userData = mkdtempSync3(
|
|
36654
|
+
const userData = mkdtempSync3(join15(tmpdir5(), "gdharness-tests-"));
|
|
36598
36655
|
const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
|
|
36599
36656
|
const hung = await new Promise((resolve) => {
|
|
36600
36657
|
const timer = setTimeout(() => {
|
|
@@ -36610,20 +36667,20 @@ class GodotServer {
|
|
|
36610
36667
|
resolve(false);
|
|
36611
36668
|
});
|
|
36612
36669
|
});
|
|
36613
|
-
const reportsDir =
|
|
36670
|
+
const reportsDir = join15(project.value.path, ".godot", "gdharness-reports");
|
|
36614
36671
|
let report = null;
|
|
36615
36672
|
let reportProblem = null;
|
|
36616
36673
|
try {
|
|
36617
|
-
const written =
|
|
36674
|
+
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
36675
|
const newest = written.at(-1);
|
|
36619
36676
|
if (newest !== undefined) {
|
|
36620
|
-
report = parseJUnit(
|
|
36677
|
+
report = parseJUnit(readFileSync12(join15(reportsDir, newest, "results.xml"), "utf8"));
|
|
36621
36678
|
}
|
|
36622
36679
|
} catch (error) {
|
|
36623
36680
|
reportProblem = errorMessage(error);
|
|
36624
36681
|
} finally {
|
|
36625
|
-
|
|
36626
|
-
|
|
36682
|
+
rmSync8(reportsDir, { recursive: true, force: true });
|
|
36683
|
+
rmSync8(userData, { recursive: true, force: true });
|
|
36627
36684
|
}
|
|
36628
36685
|
const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries;
|
|
36629
36686
|
const verdicts = {
|
|
@@ -36737,6 +36794,7 @@ class GodotServer {
|
|
|
36737
36794
|
staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
|
|
36738
36795
|
retryingBridge: this.bridgeRetry === null ? undefined : true,
|
|
36739
36796
|
announcedAt: this.announcedAt ?? undefined,
|
|
36797
|
+
projectIs: this.projectHasMovedOn() ?? undefined,
|
|
36740
36798
|
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
36799
|
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
36800
|
};
|
|
@@ -36949,7 +37007,8 @@ class GodotServer {
|
|
|
36949
37007
|
log,
|
|
36950
37008
|
startedAt: Date.now(),
|
|
36951
37009
|
exitCode: null,
|
|
36952
|
-
throughEditor: true
|
|
37010
|
+
throughEditor: true,
|
|
37011
|
+
brokeOn: null
|
|
36953
37012
|
};
|
|
36954
37013
|
this.activeProcess = played;
|
|
36955
37014
|
return this.jsonTextResponse({
|
|
@@ -36969,6 +37028,15 @@ class GodotServer {
|
|
|
36969
37028
|
`) ? line : `${line}
|
|
36970
37029
|
`);
|
|
36971
37030
|
}
|
|
37031
|
+
this.recordWhatItBrokeOn(game);
|
|
37032
|
+
}
|
|
37033
|
+
recordWhatItBrokeOn(game) {
|
|
37034
|
+
const halt = this.dapClient?.whereItStopped() ?? null;
|
|
37035
|
+
if (halt?.reason !== "exception" || halt.text === "" || game.brokeOn === halt.text) {
|
|
37036
|
+
return;
|
|
37037
|
+
}
|
|
37038
|
+
game.brokeOn = halt.text;
|
|
37039
|
+
game.log.record("error", halt.text);
|
|
36972
37040
|
}
|
|
36973
37041
|
async endActiveGame() {
|
|
36974
37042
|
const running = this.activeProcess;
|
|
@@ -36990,7 +37058,8 @@ class GodotServer {
|
|
|
36990
37058
|
log,
|
|
36991
37059
|
startedAt: Date.now(),
|
|
36992
37060
|
exitCode: null,
|
|
36993
|
-
throughEditor: false
|
|
37061
|
+
throughEditor: false,
|
|
37062
|
+
brokeOn: null
|
|
36994
37063
|
};
|
|
36995
37064
|
child.stdout.on("data", (data) => {
|
|
36996
37065
|
log.append("stdout", data);
|
|
@@ -37055,6 +37124,7 @@ class GodotServer {
|
|
|
37055
37124
|
contains: readNonEmptyString(args, "contains"),
|
|
37056
37125
|
limit: readPositiveNumber(args, "limit") ?? 200
|
|
37057
37126
|
});
|
|
37127
|
+
const halt = this.activeProcess.throughEditor ? this.dapClient?.whereItStopped() ?? null : null;
|
|
37058
37128
|
return this.jsonTextResponse({
|
|
37059
37129
|
running: this.activeProcess.exitCode === null,
|
|
37060
37130
|
exitCode: this.activeProcess.exitCode,
|
|
@@ -37063,6 +37133,7 @@ class GodotServer {
|
|
|
37063
37133
|
errors: this.activeProcess.log.count("error"),
|
|
37064
37134
|
warnings: this.activeProcess.log.count("warning"),
|
|
37065
37135
|
clean: this.activeProcess.log.count("error") === 0,
|
|
37136
|
+
heldAt: halt,
|
|
37066
37137
|
omitted: selected.omitted,
|
|
37067
37138
|
entries: selected.entries
|
|
37068
37139
|
});
|
|
@@ -37132,8 +37203,8 @@ class GodotServer {
|
|
|
37132
37203
|
return this.createErrorResponse(choice.problem);
|
|
37133
37204
|
}
|
|
37134
37205
|
const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
|
|
37135
|
-
const screenshotDir = expectsScreenshot ? mkdtempSync3(
|
|
37136
|
-
const screenshotPath = screenshotDir ?
|
|
37206
|
+
const screenshotDir = expectsScreenshot ? mkdtempSync3(join15(tmpdir5(), "gdharness-runtime-screenshot-")) : null;
|
|
37207
|
+
const screenshotPath = screenshotDir ? join15(screenshotDir, "capture.png") : null;
|
|
37137
37208
|
try {
|
|
37138
37209
|
const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs);
|
|
37139
37210
|
if (!reply.ok) {
|
|
@@ -37154,12 +37225,12 @@ class GodotServer {
|
|
|
37154
37225
|
return {
|
|
37155
37226
|
content: [
|
|
37156
37227
|
{ type: "text", text: `Screenshot captured: ${dimensions}` },
|
|
37157
|
-
{ type: "image", data:
|
|
37228
|
+
{ type: "image", data: readFileSync12(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
37158
37229
|
]
|
|
37159
37230
|
};
|
|
37160
37231
|
} finally {
|
|
37161
37232
|
if (screenshotDir) {
|
|
37162
|
-
|
|
37233
|
+
rmSync8(screenshotDir, { recursive: true, force: true });
|
|
37163
37234
|
}
|
|
37164
37235
|
}
|
|
37165
37236
|
}
|
|
@@ -37237,10 +37308,11 @@ var init_server3 = __esm(() => {
|
|
|
37237
37308
|
init_resources();
|
|
37238
37309
|
init_runtime_client();
|
|
37239
37310
|
init_server_version();
|
|
37311
|
+
init_setup();
|
|
37240
37312
|
init_tool_definitions();
|
|
37241
37313
|
init_update_check();
|
|
37242
37314
|
run5 = promisify5(execFile5);
|
|
37243
|
-
__dirname2 =
|
|
37315
|
+
__dirname2 = dirname9(fileURLToPath4(import.meta.url));
|
|
37244
37316
|
PATH_SOLUTIONS = [
|
|
37245
37317
|
'Give the path relative to the project, such as "scenes/main.tscn" or "res://scenes/main.tscn"',
|
|
37246
37318
|
"Point projectPath at the project the file belongs to"
|
|
@@ -37297,8 +37369,8 @@ var init_server3 = __esm(() => {
|
|
|
37297
37369
|
});
|
|
37298
37370
|
|
|
37299
37371
|
// src/cli.ts
|
|
37300
|
-
import { existsSync as
|
|
37301
|
-
import { join as
|
|
37372
|
+
import { existsSync as existsSync13 } from "fs";
|
|
37373
|
+
import { join as join16, resolve as resolve5 } from "path";
|
|
37302
37374
|
|
|
37303
37375
|
// src/errors.ts
|
|
37304
37376
|
class Refusal extends Error {
|
|
@@ -38700,7 +38772,7 @@ class UsageError extends Refusal {
|
|
|
38700
38772
|
function projectArgument(at) {
|
|
38701
38773
|
const named = args.slice(at).find((value) => !value.startsWith("--"));
|
|
38702
38774
|
const projectPath = resolve5(named ?? ".");
|
|
38703
|
-
if (!
|
|
38775
|
+
if (!existsSync13(join16(projectPath, "project.godot"))) {
|
|
38704
38776
|
throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
|
|
38705
38777
|
}
|
|
38706
38778
|
return projectPath;
|
|
@@ -38712,7 +38784,7 @@ async function engine() {
|
|
|
38712
38784
|
}
|
|
38713
38785
|
return { godotPath, script: shippedOperationsScript(), debug: GODOT_DEBUG_MODE_DEFAULT2 };
|
|
38714
38786
|
}
|
|
38715
|
-
function
|
|
38787
|
+
function said2(outcome, what) {
|
|
38716
38788
|
if (!outcome.ok) {
|
|
38717
38789
|
throw new Refusal(`${what}: ${outcome.message}`);
|
|
38718
38790
|
}
|
|
@@ -38813,15 +38885,15 @@ async function setup() {
|
|
|
38813
38885
|
const enabled = await enablePlugins(godot, projectPath, EDITOR_PLUGINS);
|
|
38814
38886
|
for (const [index, outcome] of enabled.entries()) {
|
|
38815
38887
|
const name = EDITOR_PLUGINS[index] ?? "";
|
|
38816
|
-
|
|
38888
|
+
said2(outcome, `enabling ${name}`);
|
|
38817
38889
|
console.log(`${name}: ${outcome.ok ? String(outcome.payload["action"]) : "failed"}`);
|
|
38818
38890
|
}
|
|
38819
38891
|
if (runtime) {
|
|
38820
|
-
|
|
38892
|
+
said2(await setRuntime(godot, projectPath, true), "registering the runtime autoload");
|
|
38821
38893
|
console.log(`${RUNTIME_AUTOLOAD.name} autoload registered`);
|
|
38822
38894
|
}
|
|
38823
38895
|
const classes = await runOperation(godot, "refresh_class_cache", {}, projectPath);
|
|
38824
|
-
|
|
38896
|
+
said2(classes, "rebuilding the class list");
|
|
38825
38897
|
if (classes.ok) {
|
|
38826
38898
|
console.log(`class list rebuilt: ${String(classes.payload["classes"])} classes`);
|
|
38827
38899
|
}
|
|
@@ -38844,7 +38916,7 @@ async function setup() {
|
|
|
38844
38916
|
console.log("no harness configured; name one yourself, and gdharness harnesses lists them all");
|
|
38845
38917
|
}
|
|
38846
38918
|
if (!args.includes("--no-skill")) {
|
|
38847
|
-
const directories = skillDirectories(chosen.harnesses, projectPath,
|
|
38919
|
+
const directories = skillDirectories(chosen.harnesses, projectPath, existsSync13);
|
|
38848
38920
|
for (const written of writeSkill(directories, getLocalVersion())) {
|
|
38849
38921
|
console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
|
|
38850
38922
|
}
|
|
@@ -38883,11 +38955,11 @@ async function uninstall() {
|
|
|
38883
38955
|
const disabled = await disablePlugins(godot, projectPath, enabled);
|
|
38884
38956
|
for (const [index, outcome] of disabled.entries()) {
|
|
38885
38957
|
const name = enabled[index] ?? "";
|
|
38886
|
-
|
|
38958
|
+
said2(outcome, `disabling ${name}`);
|
|
38887
38959
|
console.log(`${name}: ${outcome.ok ? String(outcome.payload["action"]) : "failed"}`);
|
|
38888
38960
|
}
|
|
38889
38961
|
if (before.runtimeAutoload) {
|
|
38890
|
-
|
|
38962
|
+
said2(await setRuntime(godot, projectPath, false), "removing the runtime autoload");
|
|
38891
38963
|
console.log(`${RUNTIME_AUTOLOAD.name} autoload removed`);
|
|
38892
38964
|
}
|
|
38893
38965
|
const addons = removeAddons(projectPath);
|
|
@@ -38915,12 +38987,12 @@ async function upgrade() {
|
|
|
38915
38987
|
}
|
|
38916
38988
|
const enabled = await enablePlugins(godot, projectPath, EDITOR_PLUGINS);
|
|
38917
38989
|
for (const [index, outcome] of enabled.entries()) {
|
|
38918
|
-
|
|
38990
|
+
said2(outcome, `enabling ${EDITOR_PLUGINS[index] ?? ""}`);
|
|
38919
38991
|
}
|
|
38920
38992
|
if (before.runtimeAutoload) {
|
|
38921
|
-
|
|
38993
|
+
said2(await setRuntime(godot, projectPath, true), "registering the runtime autoload");
|
|
38922
38994
|
}
|
|
38923
|
-
|
|
38995
|
+
said2(await runOperation(godot, "refresh_class_cache", {}, projectPath), "rebuilding the class list");
|
|
38924
38996
|
const launch = launchFor(version, godot.godotPath, projectPath);
|
|
38925
38997
|
const already = HARNESSES.filter((harness) => registered(harness, projectPath));
|
|
38926
38998
|
for (const group of groupByFile(already, projectPath)) {
|
|
@@ -38929,7 +39001,7 @@ async function upgrade() {
|
|
|
38929
39001
|
if (already.length === 0) {
|
|
38930
39002
|
console.log("no harness config names gdharness, so none was re-pinned");
|
|
38931
39003
|
}
|
|
38932
|
-
for (const written of writeSkill(skillDirectories(already, projectPath,
|
|
39004
|
+
for (const written of writeSkill(skillDirectories(already, projectPath, existsSync13), version)) {
|
|
38933
39005
|
console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
|
|
38934
39006
|
}
|
|
38935
39007
|
console.log(`
|
|
@@ -38965,14 +39037,14 @@ async function runtime3() {
|
|
|
38965
39037
|
}
|
|
38966
39038
|
const projectPath = projectArgument(2);
|
|
38967
39039
|
const godot = await engine();
|
|
38968
|
-
|
|
39040
|
+
said2(await setRuntime(godot, projectPath, state === "on"), `turning the runtime ${state}`);
|
|
38969
39041
|
console.log(`${RUNTIME_AUTOLOAD.name} autoload ${state === "on" ? "registered" : "removed"}`);
|
|
38970
39042
|
}
|
|
38971
39043
|
async function classes() {
|
|
38972
39044
|
const projectPath = projectArgument(1);
|
|
38973
39045
|
const godot = await engine();
|
|
38974
39046
|
const outcome = await runOperation(godot, "refresh_class_cache", {}, projectPath);
|
|
38975
|
-
|
|
39047
|
+
said2(outcome, "rebuilding the class list");
|
|
38976
39048
|
if (outcome.ok) {
|
|
38977
39049
|
console.log(JSON.stringify(outcome.payload, null, 2));
|
|
38978
39050
|
}
|
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",
|
|
@@ -28472,7 +28507,7 @@ function buildToolDefinitions() {
|
|
|
28472
28507
|
var UPDATE_NOTICE_EVERY = 500;
|
|
28473
28508
|
var FEEDBACK_NOTICE_EVERY = 250;
|
|
28474
28509
|
var run3 = promisify3(execFile3);
|
|
28475
|
-
var __dirname2 =
|
|
28510
|
+
var __dirname2 = dirname4(fileURLToPath2(import.meta.url));
|
|
28476
28511
|
var EDITOR_RESTART_TIMEOUT_MS = 90000;
|
|
28477
28512
|
var BRIDGE_RETRY_MS = 2000;
|
|
28478
28513
|
var PATH_SOLUTIONS = [
|
|
@@ -28550,11 +28585,11 @@ function realPathOr(path) {
|
|
|
28550
28585
|
}
|
|
28551
28586
|
}
|
|
28552
28587
|
function hasMainScene(projectFile) {
|
|
28553
|
-
const scene = parseProjectGodot(
|
|
28588
|
+
const scene = parseProjectGodot(readFileSync8(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
28554
28589
|
return typeof scene === "string" && scene !== "";
|
|
28555
28590
|
}
|
|
28556
28591
|
function editorPlaysHeadless(projectFile) {
|
|
28557
|
-
const runArgs = parseProjectGodot(
|
|
28592
|
+
const runArgs = parseProjectGodot(readFileSync8(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
28558
28593
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
28559
28594
|
}
|
|
28560
28595
|
function camelCased(params) {
|
|
@@ -28570,7 +28605,7 @@ function camelCased(params) {
|
|
|
28570
28605
|
class GodotServer {
|
|
28571
28606
|
mcp;
|
|
28572
28607
|
locator = new GodotLocator;
|
|
28573
|
-
operationsScript =
|
|
28608
|
+
operationsScript = join9(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
28574
28609
|
godotBridge;
|
|
28575
28610
|
tools = buildToolDefinitions();
|
|
28576
28611
|
activeProcess = null;
|
|
@@ -28808,28 +28843,44 @@ class GodotServer {
|
|
|
28808
28843
|
if (this.callsSinceNotice < UPDATE_NOTICE_EVERY && this.noticedUpdate) {
|
|
28809
28844
|
return answer;
|
|
28810
28845
|
}
|
|
28846
|
+
const moved = this.projectHasMovedOn();
|
|
28847
|
+
if (moved !== null) {
|
|
28848
|
+
this.noticedUpdate = true;
|
|
28849
|
+
this.callsSinceNotice = 0;
|
|
28850
|
+
return this.saying(answer, {
|
|
28851
|
+
project_upgraded_under_this_server: {
|
|
28852
|
+
server_is: SERVER_VERSION,
|
|
28853
|
+
project_is: moved,
|
|
28854
|
+
what_to_do: "The project was upgraded while this server has been running, so it is still answering " + "as the version it was started as. Tell the user to reconnect the MCP server, which is " + "the only thing that replaces it: a server cannot restart itself."
|
|
28855
|
+
}
|
|
28856
|
+
});
|
|
28857
|
+
}
|
|
28811
28858
|
const notice = this.updates.notice();
|
|
28812
28859
|
if (notice === null) {
|
|
28813
28860
|
return answer;
|
|
28814
28861
|
}
|
|
28815
28862
|
this.noticedUpdate = true;
|
|
28816
28863
|
this.callsSinceNotice = 0;
|
|
28864
|
+
return this.saying(answer, {
|
|
28865
|
+
update_available: {
|
|
28866
|
+
...notice,
|
|
28867
|
+
what_to_do: "Tell the user a newer gdharness is out, with what changed, and offer to take it. " + "Only run the upgrade command if they say yes: it restarts their editor and the " + "MCP server has to be reconnected afterwards."
|
|
28868
|
+
}
|
|
28869
|
+
});
|
|
28870
|
+
}
|
|
28871
|
+
saying(answer, block) {
|
|
28817
28872
|
return {
|
|
28818
28873
|
...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
|
-
]
|
|
28874
|
+
content: [...answer.content, { type: "text", text: JSON.stringify(block, null, 2) }]
|
|
28831
28875
|
};
|
|
28832
28876
|
}
|
|
28877
|
+
projectHasMovedOn() {
|
|
28878
|
+
if (this.ownProject === null) {
|
|
28879
|
+
return null;
|
|
28880
|
+
}
|
|
28881
|
+
const installed = installedAddonVersion(this.ownProject);
|
|
28882
|
+
return installed === null || installed === SERVER_VERSION ? null : installed;
|
|
28883
|
+
}
|
|
28833
28884
|
withFeedbackNotice(answer) {
|
|
28834
28885
|
this.callsSinceFeedback += 1;
|
|
28835
28886
|
if (this.callsSinceFeedback < FEEDBACK_NOTICE_EVERY) {
|
|
@@ -29075,8 +29126,8 @@ class GodotServer {
|
|
|
29075
29126
|
if (path === undefined) {
|
|
29076
29127
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
29077
29128
|
}
|
|
29078
|
-
const file =
|
|
29079
|
-
if (!
|
|
29129
|
+
const file = join9(path, "project.godot");
|
|
29130
|
+
if (!existsSync7(file)) {
|
|
29080
29131
|
return {
|
|
29081
29132
|
ok: false,
|
|
29082
29133
|
response: this.createErrorResponse(`Not a Godot project: ${path}`, [
|
|
@@ -29221,7 +29272,7 @@ class GodotServer {
|
|
|
29221
29272
|
if (!engine.ok) {
|
|
29222
29273
|
return engine.response;
|
|
29223
29274
|
}
|
|
29224
|
-
const application = parseProjectGodot(
|
|
29275
|
+
const application = parseProjectGodot(readFileSync8(project.value.file, "utf8"))["application"] ?? {};
|
|
29225
29276
|
const name = application["config/name"];
|
|
29226
29277
|
const mainScene = application["run/main_scene"];
|
|
29227
29278
|
const info = {
|
|
@@ -29310,7 +29361,7 @@ class GodotServer {
|
|
|
29310
29361
|
log.finish();
|
|
29311
29362
|
const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
|
|
29312
29363
|
const verdict = {
|
|
29313
|
-
exported: exitCode === 0 && log.count("error") === 0 &&
|
|
29364
|
+
exported: exitCode === 0 && log.count("error") === 0 && existsSync7(output.absolutePath),
|
|
29314
29365
|
preset,
|
|
29315
29366
|
outputPath: output.relativePath,
|
|
29316
29367
|
debug,
|
|
@@ -29340,7 +29391,7 @@ class GodotServer {
|
|
|
29340
29391
|
return contained.response;
|
|
29341
29392
|
}
|
|
29342
29393
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
29343
|
-
if (!
|
|
29394
|
+
if (!existsSync7(join9(project.value.path, runner))) {
|
|
29344
29395
|
return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
|
|
29345
29396
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
29346
29397
|
]);
|
|
@@ -29373,7 +29424,7 @@ class GodotServer {
|
|
|
29373
29424
|
];
|
|
29374
29425
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
|
|
29375
29426
|
this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
29376
|
-
const userData = mkdtempSync2(
|
|
29427
|
+
const userData = mkdtempSync2(join9(tmpdir4(), "gdharness-tests-"));
|
|
29377
29428
|
const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
|
|
29378
29429
|
const hung = await new Promise((resolve) => {
|
|
29379
29430
|
const timer = setTimeout(() => {
|
|
@@ -29389,20 +29440,20 @@ class GodotServer {
|
|
|
29389
29440
|
resolve(false);
|
|
29390
29441
|
});
|
|
29391
29442
|
});
|
|
29392
|
-
const reportsDir =
|
|
29443
|
+
const reportsDir = join9(project.value.path, ".godot", "gdharness-reports");
|
|
29393
29444
|
let report = null;
|
|
29394
29445
|
let reportProblem = null;
|
|
29395
29446
|
try {
|
|
29396
|
-
const written =
|
|
29447
|
+
const written = existsSync7(reportsDir) ? readdirSync5(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
29397
29448
|
const newest = written.at(-1);
|
|
29398
29449
|
if (newest !== undefined) {
|
|
29399
|
-
report = parseJUnit(
|
|
29450
|
+
report = parseJUnit(readFileSync8(join9(reportsDir, newest, "results.xml"), "utf8"));
|
|
29400
29451
|
}
|
|
29401
29452
|
} catch (error) {
|
|
29402
29453
|
reportProblem = errorMessage(error);
|
|
29403
29454
|
} finally {
|
|
29404
|
-
|
|
29405
|
-
|
|
29455
|
+
rmSync4(reportsDir, { recursive: true, force: true });
|
|
29456
|
+
rmSync4(userData, { recursive: true, force: true });
|
|
29406
29457
|
}
|
|
29407
29458
|
const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries;
|
|
29408
29459
|
const verdicts = {
|
|
@@ -29516,6 +29567,7 @@ class GodotServer {
|
|
|
29516
29567
|
staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
|
|
29517
29568
|
retryingBridge: this.bridgeRetry === null ? undefined : true,
|
|
29518
29569
|
announcedAt: this.announcedAt ?? undefined,
|
|
29570
|
+
projectIs: this.projectHasMovedOn() ?? undefined,
|
|
29519
29571
|
note: isPortConflict ? "Bridge port is already in use. Another gdharness instance owns the editor bridge, so this server cannot reach the editor. Usually the server this one replaced, still on its way out." : undefined,
|
|
29520
29572
|
suggestion: isPortConflict ? `This server is asking for the port again every ${BRIDGE_RETRY_MS / 1000}s and takes it the moment the other one lets go, so editor tools come back on their own. Ask again, or end the older gdharness process to have it now.` : undefined
|
|
29521
29573
|
};
|
|
@@ -29728,7 +29780,8 @@ class GodotServer {
|
|
|
29728
29780
|
log,
|
|
29729
29781
|
startedAt: Date.now(),
|
|
29730
29782
|
exitCode: null,
|
|
29731
|
-
throughEditor: true
|
|
29783
|
+
throughEditor: true,
|
|
29784
|
+
brokeOn: null
|
|
29732
29785
|
};
|
|
29733
29786
|
this.activeProcess = played;
|
|
29734
29787
|
return this.jsonTextResponse({
|
|
@@ -29748,6 +29801,15 @@ class GodotServer {
|
|
|
29748
29801
|
`) ? line : `${line}
|
|
29749
29802
|
`);
|
|
29750
29803
|
}
|
|
29804
|
+
this.recordWhatItBrokeOn(game);
|
|
29805
|
+
}
|
|
29806
|
+
recordWhatItBrokeOn(game) {
|
|
29807
|
+
const halt = this.dapClient?.whereItStopped() ?? null;
|
|
29808
|
+
if (halt?.reason !== "exception" || halt.text === "" || game.brokeOn === halt.text) {
|
|
29809
|
+
return;
|
|
29810
|
+
}
|
|
29811
|
+
game.brokeOn = halt.text;
|
|
29812
|
+
game.log.record("error", halt.text);
|
|
29751
29813
|
}
|
|
29752
29814
|
async endActiveGame() {
|
|
29753
29815
|
const running = this.activeProcess;
|
|
@@ -29769,7 +29831,8 @@ class GodotServer {
|
|
|
29769
29831
|
log,
|
|
29770
29832
|
startedAt: Date.now(),
|
|
29771
29833
|
exitCode: null,
|
|
29772
|
-
throughEditor: false
|
|
29834
|
+
throughEditor: false,
|
|
29835
|
+
brokeOn: null
|
|
29773
29836
|
};
|
|
29774
29837
|
child.stdout.on("data", (data) => {
|
|
29775
29838
|
log.append("stdout", data);
|
|
@@ -29834,6 +29897,7 @@ class GodotServer {
|
|
|
29834
29897
|
contains: readNonEmptyString(args, "contains"),
|
|
29835
29898
|
limit: readPositiveNumber(args, "limit") ?? 200
|
|
29836
29899
|
});
|
|
29900
|
+
const halt = this.activeProcess.throughEditor ? this.dapClient?.whereItStopped() ?? null : null;
|
|
29837
29901
|
return this.jsonTextResponse({
|
|
29838
29902
|
running: this.activeProcess.exitCode === null,
|
|
29839
29903
|
exitCode: this.activeProcess.exitCode,
|
|
@@ -29842,6 +29906,7 @@ class GodotServer {
|
|
|
29842
29906
|
errors: this.activeProcess.log.count("error"),
|
|
29843
29907
|
warnings: this.activeProcess.log.count("warning"),
|
|
29844
29908
|
clean: this.activeProcess.log.count("error") === 0,
|
|
29909
|
+
heldAt: halt,
|
|
29845
29910
|
omitted: selected.omitted,
|
|
29846
29911
|
entries: selected.entries
|
|
29847
29912
|
});
|
|
@@ -29911,8 +29976,8 @@ class GodotServer {
|
|
|
29911
29976
|
return this.createErrorResponse(choice.problem);
|
|
29912
29977
|
}
|
|
29913
29978
|
const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
|
|
29914
|
-
const screenshotDir = expectsScreenshot ? mkdtempSync2(
|
|
29915
|
-
const screenshotPath = screenshotDir ?
|
|
29979
|
+
const screenshotDir = expectsScreenshot ? mkdtempSync2(join9(tmpdir4(), "gdharness-runtime-screenshot-")) : null;
|
|
29980
|
+
const screenshotPath = screenshotDir ? join9(screenshotDir, "capture.png") : null;
|
|
29916
29981
|
try {
|
|
29917
29982
|
const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs);
|
|
29918
29983
|
if (!reply.ok) {
|
|
@@ -29933,12 +29998,12 @@ class GodotServer {
|
|
|
29933
29998
|
return {
|
|
29934
29999
|
content: [
|
|
29935
30000
|
{ type: "text", text: `Screenshot captured: ${dimensions}` },
|
|
29936
|
-
{ type: "image", data:
|
|
30001
|
+
{ type: "image", data: readFileSync8(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
29937
30002
|
]
|
|
29938
30003
|
};
|
|
29939
30004
|
} finally {
|
|
29940
30005
|
if (screenshotDir) {
|
|
29941
|
-
|
|
30006
|
+
rmSync4(screenshotDir, { recursive: true, force: true });
|
|
29942
30007
|
}
|
|
29943
30008
|
}
|
|
29944
30009
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.10",
|
|
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",
|