gdharness 0.4.2 → 0.5.0
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/LICENSE +1 -0
- package/README.md +31 -87
- package/build/cli.js +34888 -26369
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +11 -3
- package/build/index.js +670 -112
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -9802,11 +9802,136 @@ var require_websocket_server = __commonJS(function(exports, module) {
|
|
|
9802
9802
|
}
|
|
9803
9803
|
});
|
|
9804
9804
|
|
|
9805
|
+
// src/issues.ts
|
|
9806
|
+
import { createHash } from "node:crypto";
|
|
9807
|
+
import process2 from "node:process";
|
|
9808
|
+
|
|
9809
|
+
// src/errors.ts
|
|
9810
|
+
class Refusal extends Error {
|
|
9811
|
+
}
|
|
9812
|
+
function errorMessage(error, fallback = "Unknown error") {
|
|
9813
|
+
if (error instanceof Error) {
|
|
9814
|
+
return error.message || fallback;
|
|
9815
|
+
}
|
|
9816
|
+
if (typeof error === "string") {
|
|
9817
|
+
return error || fallback;
|
|
9818
|
+
}
|
|
9819
|
+
if (typeof error === "number" || typeof error === "boolean") {
|
|
9820
|
+
return String(error);
|
|
9821
|
+
}
|
|
9822
|
+
return fallback;
|
|
9823
|
+
}
|
|
9824
|
+
function toError(error) {
|
|
9825
|
+
return error instanceof Error ? error : new Error(errorMessage(error), { cause: error });
|
|
9826
|
+
}
|
|
9827
|
+
|
|
9828
|
+
// src/server-version.ts
|
|
9829
|
+
import { readFileSync } from "node:fs";
|
|
9830
|
+
var DEBUG_MODE = process.env["DEBUG"] === "true";
|
|
9831
|
+
var GODOT_DEBUG_MODE_DEFAULT = process.env["GODOT_DEBUG"] === "true" || DEBUG_MODE;
|
|
9832
|
+
var SERVER_VERSION = (() => {
|
|
9833
|
+
try {
|
|
9834
|
+
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
9835
|
+
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
9836
|
+
} catch {
|
|
9837
|
+
return "0.0.0";
|
|
9838
|
+
}
|
|
9839
|
+
})();
|
|
9840
|
+
|
|
9841
|
+
// src/issues.ts
|
|
9842
|
+
var NEW_ISSUE = "https://github.com/Aureliolo/gdharness/issues/new";
|
|
9843
|
+
var ENHANCEMENT_URL = `${NEW_ISSUE}?template=feature_request.md`;
|
|
9844
|
+
function runtime() {
|
|
9845
|
+
const versions = process2.versions;
|
|
9846
|
+
const bun = versions["bun"];
|
|
9847
|
+
return bun === undefined ? `node ${process2.versions.node}` : `bun ${bun}`;
|
|
9848
|
+
}
|
|
9849
|
+
function defectSignature(where, message) {
|
|
9850
|
+
const normalised = `${where}
|
|
9851
|
+
${message}`.toLowerCase().replaceAll("\\", "/").replaceAll(/(?:[a-z]:)?(?:\/+[\w.-]+){2,}/gu, "<path>").replaceAll(/\d+/gu, "0");
|
|
9852
|
+
return createHash("sha256").update(normalised).digest("hex").slice(0, 8);
|
|
9853
|
+
}
|
|
9854
|
+
function facts(where, message, godotVersion) {
|
|
9855
|
+
return [
|
|
9856
|
+
["Where", where],
|
|
9857
|
+
["Error", message],
|
|
9858
|
+
["Version", `gdharness ${SERVER_VERSION}`],
|
|
9859
|
+
["Runtime", `${runtime()}, ${process2.platform} ${process2.arch}`],
|
|
9860
|
+
["Godot", godotVersion ?? "not known at the point this failed"],
|
|
9861
|
+
["Signature", defectSignature(where, message)]
|
|
9862
|
+
];
|
|
9863
|
+
}
|
|
9864
|
+
function filledTemplate(where, message, godotVersion) {
|
|
9865
|
+
return [
|
|
9866
|
+
`**gdharness version**: ${SERVER_VERSION}`,
|
|
9867
|
+
`**Godot version**: ${godotVersion ?? ""}`,
|
|
9868
|
+
`**Bun version**: ${runtime()}`,
|
|
9869
|
+
`**OS**: ${process2.platform} ${process2.arch}`,
|
|
9870
|
+
"**MCP client**:",
|
|
9871
|
+
"",
|
|
9872
|
+
"## What you did",
|
|
9873
|
+
"",
|
|
9874
|
+
where,
|
|
9875
|
+
"",
|
|
9876
|
+
"## What you expected",
|
|
9877
|
+
"",
|
|
9878
|
+
"The call to answer, or to refuse with a reason.",
|
|
9879
|
+
"",
|
|
9880
|
+
"## What happened",
|
|
9881
|
+
"",
|
|
9882
|
+
"gdharness failed in a way it does not model.",
|
|
9883
|
+
"",
|
|
9884
|
+
"```text",
|
|
9885
|
+
message,
|
|
9886
|
+
"```",
|
|
9887
|
+
"",
|
|
9888
|
+
`Signature: \`${defectSignature(where, message)}\``,
|
|
9889
|
+
"",
|
|
9890
|
+
"If the real cause turned out to be the project, the environment or the call, say so here:",
|
|
9891
|
+
"reaching you as a defect rather than as a refusal naming what would have worked is then the",
|
|
9892
|
+
"thing to fix.",
|
|
9893
|
+
"",
|
|
9894
|
+
"## Anything that makes it reproducible",
|
|
9895
|
+
""
|
|
9896
|
+
].join(`
|
|
9897
|
+
`);
|
|
9898
|
+
}
|
|
9899
|
+
function defectReport(where, error, godotVersion) {
|
|
9900
|
+
const message = errorMessage(error);
|
|
9901
|
+
const title = `Defect ${defectSignature(where, message)}: ${where}`;
|
|
9902
|
+
const url = `${NEW_ISSUE}?template=bug_report.md&title=${encodeURIComponent(title)}` + `&body=${encodeURIComponent(filledTemplate(where, message, godotVersion))}`;
|
|
9903
|
+
const width = Math.max(...facts(where, message, godotVersion).map(([label]) => (label ?? "").length));
|
|
9904
|
+
return [
|
|
9905
|
+
"gdharness failed in a way it does not model. This is a defect in gdharness rather than",
|
|
9906
|
+
"anything about the call: the arguments did not cause it, and sending it again will not",
|
|
9907
|
+
"change it.",
|
|
9908
|
+
"",
|
|
9909
|
+
...facts(where, message, godotVersion).map(([label, value]) => ` ${(label ?? "").padEnd(width)} ${value ?? ""}`),
|
|
9910
|
+
"",
|
|
9911
|
+
"Do not open an issue on your own initiative. Tell the person whose machine this is what",
|
|
9912
|
+
"broke, and ask whether you may report it to gdharness on their behalf. What the report",
|
|
9913
|
+
"carries is the lines above and nothing else, and whether that goes into a public tracker",
|
|
9914
|
+
"is theirs to decide.",
|
|
9915
|
+
"",
|
|
9916
|
+
"If they say yes, this link carries the report already filled in:",
|
|
9917
|
+
url,
|
|
9918
|
+
"",
|
|
9919
|
+
"If they say yes and you have no way to open an issue yourself, give them that link and",
|
|
9920
|
+
"those lines, and stay with them while they file it.",
|
|
9921
|
+
"",
|
|
9922
|
+
"Worth reporting either way. If you can see that what actually went wrong was the project,",
|
|
9923
|
+
"the environment or the call, then this message is the defect: a failure gdharness knows",
|
|
9924
|
+
"about is meant to arrive as a refusal naming what would have worked, not as this. Say that",
|
|
9925
|
+
"in the report and it is the more useful of the two."
|
|
9926
|
+
].join(`
|
|
9927
|
+
`);
|
|
9928
|
+
}
|
|
9929
|
+
|
|
9805
9930
|
// src/server.ts
|
|
9806
9931
|
import { execFile as execFile3, spawn } from "node:child_process";
|
|
9807
|
-
import { existsSync as
|
|
9808
|
-
import { tmpdir as
|
|
9809
|
-
import { basename, dirname as dirname2, join as
|
|
9932
|
+
import { existsSync as existsSync6, mkdtempSync as mkdtempSync2, readdirSync as readdirSync5, readFileSync as readFileSync7, realpathSync as realpathSync2, rmSync as rmSync2 } from "node:fs";
|
|
9933
|
+
import { tmpdir as tmpdir4 } from "node:os";
|
|
9934
|
+
import { basename, dirname as dirname2, join as join7, normalize as normalize2 } from "node:path";
|
|
9810
9935
|
import { setTimeout as delay2 } from "node:timers/promises";
|
|
9811
9936
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
9812
9937
|
import { promisify as promisify3 } from "node:util";
|
|
@@ -24269,7 +24394,7 @@ var EMPTY_COMPLETION_RESULT = {
|
|
|
24269
24394
|
};
|
|
24270
24395
|
|
|
24271
24396
|
// node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
|
|
24272
|
-
import
|
|
24397
|
+
import process3 from "node:process";
|
|
24273
24398
|
|
|
24274
24399
|
// node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.js
|
|
24275
24400
|
var STDIO_DEFAULT_MAX_BUFFER_SIZE = 10 * 1024 * 1024;
|
|
@@ -24313,7 +24438,7 @@ function serializeMessage(message) {
|
|
|
24313
24438
|
|
|
24314
24439
|
// node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
|
|
24315
24440
|
class StdioServerTransport {
|
|
24316
|
-
constructor(_stdin =
|
|
24441
|
+
constructor(_stdin = process3.stdin, _stdout = process3.stdout, options) {
|
|
24317
24442
|
this._stdin = _stdin;
|
|
24318
24443
|
this._stdout = _stdout;
|
|
24319
24444
|
this._started = false;
|
|
@@ -24374,6 +24499,50 @@ class StdioServerTransport {
|
|
|
24374
24499
|
}
|
|
24375
24500
|
}
|
|
24376
24501
|
|
|
24502
|
+
// src/class-cache.ts
|
|
24503
|
+
import { existsSync, readdirSync, readFileSync as readFileSync2 } from "node:fs";
|
|
24504
|
+
import { join } from "node:path";
|
|
24505
|
+
function declaredClasses(projectPath) {
|
|
24506
|
+
const declared = new Map;
|
|
24507
|
+
const visit = (directory, prefix) => {
|
|
24508
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
24509
|
+
if (entry.name.startsWith(".")) {
|
|
24510
|
+
continue;
|
|
24511
|
+
}
|
|
24512
|
+
const path = join(directory, entry.name);
|
|
24513
|
+
if (entry.isDirectory()) {
|
|
24514
|
+
visit(path, `${prefix}${entry.name}/`);
|
|
24515
|
+
} else if (entry.isFile() && entry.name.endsWith(".gd")) {
|
|
24516
|
+
const found = /^class_name\s+([A-Za-z_][A-Za-z0-9_]*)/m.exec(readFileSync2(path, "utf8"));
|
|
24517
|
+
if (found?.[1]) {
|
|
24518
|
+
declared.set(found[1], `res://${prefix}${entry.name}`);
|
|
24519
|
+
}
|
|
24520
|
+
}
|
|
24521
|
+
}
|
|
24522
|
+
};
|
|
24523
|
+
visit(projectPath, "");
|
|
24524
|
+
return declared;
|
|
24525
|
+
}
|
|
24526
|
+
function cachedClasses(projectPath) {
|
|
24527
|
+
const cache = join(projectPath, ".godot", "global_script_class_cache.cfg");
|
|
24528
|
+
if (!existsSync(cache)) {
|
|
24529
|
+
return null;
|
|
24530
|
+
}
|
|
24531
|
+
const listed = new Map;
|
|
24532
|
+
const text = readFileSync2(cache, "utf8");
|
|
24533
|
+
for (const entry of text.matchAll(/"class":\s*&"([^"]+)"[\s\S]*?"path":\s*"([^"]+)"/g)) {
|
|
24534
|
+
listed.set(entry[1] ?? "", entry[2] ?? "");
|
|
24535
|
+
}
|
|
24536
|
+
return listed;
|
|
24537
|
+
}
|
|
24538
|
+
function staleAgainst(cached, projectPath) {
|
|
24539
|
+
return [...declaredClasses(projectPath)].filter(([name, path]) => cached.get(name) !== path).map(([name]) => name);
|
|
24540
|
+
}
|
|
24541
|
+
function staleClassNames(projectPath) {
|
|
24542
|
+
const cached = cachedClasses(projectPath);
|
|
24543
|
+
return cached === null ? [...declaredClasses(projectPath).keys()] : staleAgainst(cached, projectPath);
|
|
24544
|
+
}
|
|
24545
|
+
|
|
24377
24546
|
// src/dap_client.ts
|
|
24378
24547
|
import { createConnection } from "node:net";
|
|
24379
24548
|
import { setTimeout as delay } from "node:timers/promises";
|
|
@@ -24464,7 +24633,7 @@ function portFromEnv(variable, fallback) {
|
|
|
24464
24633
|
}
|
|
24465
24634
|
const parsed = Number.parseInt(raw, 10);
|
|
24466
24635
|
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535 || String(parsed) !== raw) {
|
|
24467
|
-
throw new
|
|
24636
|
+
throw new Refusal(`${variable} is "${raw}", not a port between 1 and 65535.`);
|
|
24468
24637
|
}
|
|
24469
24638
|
return parsed;
|
|
24470
24639
|
}
|
|
@@ -24488,6 +24657,7 @@ class GodotDAPClient {
|
|
|
24488
24657
|
initialized = false;
|
|
24489
24658
|
attached = false;
|
|
24490
24659
|
lastThreadId = 1;
|
|
24660
|
+
stopped = false;
|
|
24491
24661
|
breakpoints = new Map;
|
|
24492
24662
|
constructor(port = portFromEnv("GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT), host = "127.0.0.1") {
|
|
24493
24663
|
this.port = port;
|
|
@@ -24536,6 +24706,7 @@ class GodotDAPClient {
|
|
|
24536
24706
|
this.connected = false;
|
|
24537
24707
|
this.initialized = false;
|
|
24538
24708
|
this.attached = false;
|
|
24709
|
+
this.stopped = false;
|
|
24539
24710
|
this.socket = null;
|
|
24540
24711
|
this.failPendingRequests(new Error("DAP connection closed"));
|
|
24541
24712
|
});
|
|
@@ -24555,6 +24726,7 @@ class GodotDAPClient {
|
|
|
24555
24726
|
this.connected = false;
|
|
24556
24727
|
this.initialized = false;
|
|
24557
24728
|
this.attached = false;
|
|
24729
|
+
this.stopped = false;
|
|
24558
24730
|
return;
|
|
24559
24731
|
}
|
|
24560
24732
|
if (this.connected) {
|
|
@@ -24583,6 +24755,7 @@ class GodotDAPClient {
|
|
|
24583
24755
|
this.connected = false;
|
|
24584
24756
|
this.initialized = false;
|
|
24585
24757
|
this.attached = false;
|
|
24758
|
+
this.stopped = false;
|
|
24586
24759
|
}
|
|
24587
24760
|
async ensureConnected() {
|
|
24588
24761
|
if (!this.connected) {
|
|
@@ -24592,7 +24765,7 @@ class GodotDAPClient {
|
|
|
24592
24765
|
async sendRequest(command, args, timeoutMs = DAP_REQUEST_TIMEOUT_MS) {
|
|
24593
24766
|
await this.ensureConnected();
|
|
24594
24767
|
if (!this.socket) {
|
|
24595
|
-
throw new
|
|
24768
|
+
throw new Refusal("DAP socket is not available");
|
|
24596
24769
|
}
|
|
24597
24770
|
const requestSeq = this.seq++;
|
|
24598
24771
|
const request = {
|
|
@@ -24659,10 +24832,12 @@ class GodotDAPClient {
|
|
|
24659
24832
|
if (typeof threadId === "number") {
|
|
24660
24833
|
this.lastThreadId = threadId;
|
|
24661
24834
|
}
|
|
24835
|
+
this.stopped = true;
|
|
24662
24836
|
return;
|
|
24663
24837
|
}
|
|
24664
24838
|
if (eventName === "terminated" || eventName === "exited") {
|
|
24665
24839
|
this.attached = false;
|
|
24840
|
+
this.stopped = false;
|
|
24666
24841
|
}
|
|
24667
24842
|
}
|
|
24668
24843
|
async initialize() {
|
|
@@ -24736,6 +24911,7 @@ class GodotDAPClient {
|
|
|
24736
24911
|
await this.attach();
|
|
24737
24912
|
const resolvedThreadId = await this.resolveThreadId(threadId);
|
|
24738
24913
|
await this.sendRequest("continue", { threadId: resolvedThreadId });
|
|
24914
|
+
this.stopped = false;
|
|
24739
24915
|
}
|
|
24740
24916
|
async stepOver(threadId) {
|
|
24741
24917
|
await this.attach();
|
|
@@ -24805,6 +24981,9 @@ class GodotDAPClient {
|
|
|
24805
24981
|
isConnected() {
|
|
24806
24982
|
return this.connected;
|
|
24807
24983
|
}
|
|
24984
|
+
isStopped() {
|
|
24985
|
+
return this.stopped;
|
|
24986
|
+
}
|
|
24808
24987
|
async resolveThreadId(threadId) {
|
|
24809
24988
|
if (typeof threadId === "number" && threadId > 0) {
|
|
24810
24989
|
this.lastThreadId = threadId;
|
|
@@ -24832,6 +25011,7 @@ class GodotDAPClient {
|
|
|
24832
25011
|
this.connected = false;
|
|
24833
25012
|
this.initialized = false;
|
|
24834
25013
|
this.attached = false;
|
|
25014
|
+
this.stopped = false;
|
|
24835
25015
|
this.reader = new FrameReader;
|
|
24836
25016
|
socket?.destroy();
|
|
24837
25017
|
this.failPendingRequests(new Error(`Godot DAP ${detail}. The connection was dropped.`));
|
|
@@ -24856,7 +25036,7 @@ async function handleDAPTool(client, toolName, args) {
|
|
|
24856
25036
|
}
|
|
24857
25037
|
case "dap_set_breakpoint": {
|
|
24858
25038
|
if (typeof safeArgs.scriptPath !== "string" || typeof safeArgs.line !== "number") {
|
|
24859
|
-
throw new
|
|
25039
|
+
throw new Refusal("dap_set_breakpoint requires { scriptPath: string, line: number }");
|
|
24860
25040
|
}
|
|
24861
25041
|
const result = await client.setBreakpoint(safeArgs.scriptPath, safeArgs.line);
|
|
24862
25042
|
return {
|
|
@@ -24865,7 +25045,7 @@ async function handleDAPTool(client, toolName, args) {
|
|
|
24865
25045
|
}
|
|
24866
25046
|
case "dap_remove_breakpoint": {
|
|
24867
25047
|
if (typeof safeArgs.scriptPath !== "string" || typeof safeArgs.line !== "number") {
|
|
24868
|
-
throw new
|
|
25048
|
+
throw new Refusal("dap_remove_breakpoint requires { scriptPath: string, line: number }");
|
|
24869
25049
|
}
|
|
24870
25050
|
const result = await client.removeBreakpoint(safeArgs.scriptPath, safeArgs.line);
|
|
24871
25051
|
return {
|
|
@@ -24930,23 +25110,6 @@ function dictionary(entries) {
|
|
|
24930
25110
|
return Object.assign(emptyRecord(), entries);
|
|
24931
25111
|
}
|
|
24932
25112
|
|
|
24933
|
-
// src/errors.ts
|
|
24934
|
-
function errorMessage(error, fallback = "Unknown error") {
|
|
24935
|
-
if (error instanceof Error) {
|
|
24936
|
-
return error.message || fallback;
|
|
24937
|
-
}
|
|
24938
|
-
if (typeof error === "string") {
|
|
24939
|
-
return error || fallback;
|
|
24940
|
-
}
|
|
24941
|
-
if (typeof error === "number" || typeof error === "boolean") {
|
|
24942
|
-
return String(error);
|
|
24943
|
-
}
|
|
24944
|
-
return fallback;
|
|
24945
|
-
}
|
|
24946
|
-
function toError(error) {
|
|
24947
|
-
return error instanceof Error ? error : new Error(errorMessage(error), { cause: error });
|
|
24948
|
-
}
|
|
24949
|
-
|
|
24950
25113
|
// src/game-log.ts
|
|
24951
25114
|
import { StringDecoder } from "node:string_decoder";
|
|
24952
25115
|
var HEADLINE = /^(USER )?(SCRIPT ERROR|ERROR|WARNING):\s?(.*)$/;
|
|
@@ -25350,7 +25513,7 @@ class GodotBridge extends EventEmitter {
|
|
|
25350
25513
|
}
|
|
25351
25514
|
sendMessage(message) {
|
|
25352
25515
|
if (this.socket?.readyState !== import_websocket.default.OPEN) {
|
|
25353
|
-
throw new
|
|
25516
|
+
throw new Refusal("Godot is not connected");
|
|
25354
25517
|
}
|
|
25355
25518
|
this.socket.send(JSON.stringify(message));
|
|
25356
25519
|
}
|
|
@@ -25458,14 +25621,14 @@ function getDefaultBridge() {
|
|
|
25458
25621
|
|
|
25459
25622
|
// src/godot-path.ts
|
|
25460
25623
|
import { execFile } from "node:child_process";
|
|
25461
|
-
import { existsSync as
|
|
25624
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
25462
25625
|
import { normalize } from "node:path";
|
|
25463
25626
|
import { promisify } from "node:util";
|
|
25464
25627
|
|
|
25465
25628
|
// src/detection.ts
|
|
25466
|
-
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
25629
|
+
import { existsSync as existsSync2, readdirSync as readdirSync2, statSync } from "node:fs";
|
|
25467
25630
|
import { homedir } from "node:os";
|
|
25468
|
-
import { join } from "node:path";
|
|
25631
|
+
import { join as join2 } from "node:path";
|
|
25469
25632
|
function resolveHomeDirectory() {
|
|
25470
25633
|
try {
|
|
25471
25634
|
return homedir();
|
|
@@ -25474,12 +25637,12 @@ function resolveHomeDirectory() {
|
|
|
25474
25637
|
}
|
|
25475
25638
|
}
|
|
25476
25639
|
function scanDirectoryForGodotBinaries(directory, platform) {
|
|
25477
|
-
if (!directory || !
|
|
25640
|
+
if (!directory || !existsSync2(directory)) {
|
|
25478
25641
|
return [];
|
|
25479
25642
|
}
|
|
25480
25643
|
let entries;
|
|
25481
25644
|
try {
|
|
25482
|
-
entries =
|
|
25645
|
+
entries = readdirSync2(directory);
|
|
25483
25646
|
} catch {
|
|
25484
25647
|
return [];
|
|
25485
25648
|
}
|
|
@@ -25489,7 +25652,7 @@ function scanDirectoryForGodotBinaries(directory, platform) {
|
|
|
25489
25652
|
if (!pattern.test(name)) {
|
|
25490
25653
|
continue;
|
|
25491
25654
|
}
|
|
25492
|
-
const fullPath =
|
|
25655
|
+
const fullPath = join2(directory, name);
|
|
25493
25656
|
try {
|
|
25494
25657
|
const stat = statSync(fullPath);
|
|
25495
25658
|
if (stat.isFile()) {
|
|
@@ -25498,7 +25661,7 @@ function scanDirectoryForGodotBinaries(directory, platform) {
|
|
|
25498
25661
|
} catch {}
|
|
25499
25662
|
}
|
|
25500
25663
|
matches.sort((a, b) => b.mtime - a.mtime);
|
|
25501
|
-
return matches.map((m) =>
|
|
25664
|
+
return matches.map((m) => join2(directory, m.name));
|
|
25502
25665
|
}
|
|
25503
25666
|
function conventionalPaths(platform, home) {
|
|
25504
25667
|
const paths = ["godot"];
|
|
@@ -25613,7 +25776,7 @@ class GodotLocator {
|
|
|
25613
25776
|
return known;
|
|
25614
25777
|
}
|
|
25615
25778
|
let ok = false;
|
|
25616
|
-
if (path === "godot" ||
|
|
25779
|
+
if (path === "godot" || existsSync3(path)) {
|
|
25617
25780
|
try {
|
|
25618
25781
|
await run(path, ["--version"]);
|
|
25619
25782
|
ok = true;
|
|
@@ -25630,7 +25793,7 @@ class GodotLocator {
|
|
|
25630
25793
|
import { execFile as execFile2 } from "node:child_process";
|
|
25631
25794
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
25632
25795
|
import { tmpdir } from "node:os";
|
|
25633
|
-
import { join as
|
|
25796
|
+
import { join as join3 } from "node:path";
|
|
25634
25797
|
import { promisify as promisify2 } from "node:util";
|
|
25635
25798
|
var run2 = promisify2(execFile2);
|
|
25636
25799
|
function snakeCased(params) {
|
|
@@ -25671,8 +25834,8 @@ function reason(stdout, stderr) {
|
|
|
25671
25834
|
return stdout.trim().split(/\r?\n/).at(-1) ?? "no output at all";
|
|
25672
25835
|
}
|
|
25673
25836
|
async function runOperation(engine, operation, params, projectPath) {
|
|
25674
|
-
const paramsDir = mkdtempSync(
|
|
25675
|
-
const paramsFile =
|
|
25837
|
+
const paramsDir = mkdtempSync(join3(tmpdir(), "gdharness-params-"));
|
|
25838
|
+
const paramsFile = join3(paramsDir, `${operation}.json`);
|
|
25676
25839
|
writeFileSync(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
|
|
25677
25840
|
const args = [
|
|
25678
25841
|
"--headless",
|
|
@@ -25716,6 +25879,105 @@ async function runOperation(engine, operation, params, projectPath) {
|
|
|
25716
25879
|
return { ok: true, payload, messages: problems(stderr) };
|
|
25717
25880
|
}
|
|
25718
25881
|
|
|
25882
|
+
// src/issues.ts
|
|
25883
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
25884
|
+
import process4 from "node:process";
|
|
25885
|
+
var NEW_ISSUE2 = "https://github.com/Aureliolo/gdharness/issues/new";
|
|
25886
|
+
var ENHANCEMENT_URL2 = `${NEW_ISSUE2}?template=feature_request.md`;
|
|
25887
|
+
function feedbackNotice() {
|
|
25888
|
+
return JSON.stringify({
|
|
25889
|
+
gdharness_feedback: {
|
|
25890
|
+
what_to_do: "If this session has run into something gdharness cannot do, or a tool that should " + "exist and does not, tell the user what is missing and offer to open an enhancement " + "issue for them. Only if they say yes, and only for a real gap: a call you got wrong " + "is not one. If you cannot open an issue yourself, give them the link and what to " + "write in it.",
|
|
25891
|
+
enhancement_url: ENHANCEMENT_URL2
|
|
25892
|
+
}
|
|
25893
|
+
}, null, 2);
|
|
25894
|
+
}
|
|
25895
|
+
function runtime2() {
|
|
25896
|
+
const versions = process4.versions;
|
|
25897
|
+
const bun = versions["bun"];
|
|
25898
|
+
return bun === undefined ? `node ${process4.versions.node}` : `bun ${bun}`;
|
|
25899
|
+
}
|
|
25900
|
+
function defectSignature2(where, message) {
|
|
25901
|
+
const normalised = `${where}
|
|
25902
|
+
${message}`.toLowerCase().replaceAll("\\", "/").replaceAll(/(?:[a-z]:)?(?:\/+[\w.-]+){2,}/gu, "<path>").replaceAll(/\d+/gu, "0");
|
|
25903
|
+
return createHash2("sha256").update(normalised).digest("hex").slice(0, 8);
|
|
25904
|
+
}
|
|
25905
|
+
function facts2(where, message, godotVersion) {
|
|
25906
|
+
return [
|
|
25907
|
+
["Where", where],
|
|
25908
|
+
["Error", message],
|
|
25909
|
+
["Version", `gdharness ${SERVER_VERSION}`],
|
|
25910
|
+
["Runtime", `${runtime2()}, ${process4.platform} ${process4.arch}`],
|
|
25911
|
+
["Godot", godotVersion ?? "not known at the point this failed"],
|
|
25912
|
+
["Signature", defectSignature2(where, message)]
|
|
25913
|
+
];
|
|
25914
|
+
}
|
|
25915
|
+
function filledTemplate2(where, message, godotVersion) {
|
|
25916
|
+
return [
|
|
25917
|
+
`**gdharness version**: ${SERVER_VERSION}`,
|
|
25918
|
+
`**Godot version**: ${godotVersion ?? ""}`,
|
|
25919
|
+
`**Bun version**: ${runtime2()}`,
|
|
25920
|
+
`**OS**: ${process4.platform} ${process4.arch}`,
|
|
25921
|
+
"**MCP client**:",
|
|
25922
|
+
"",
|
|
25923
|
+
"## What you did",
|
|
25924
|
+
"",
|
|
25925
|
+
where,
|
|
25926
|
+
"",
|
|
25927
|
+
"## What you expected",
|
|
25928
|
+
"",
|
|
25929
|
+
"The call to answer, or to refuse with a reason.",
|
|
25930
|
+
"",
|
|
25931
|
+
"## What happened",
|
|
25932
|
+
"",
|
|
25933
|
+
"gdharness failed in a way it does not model.",
|
|
25934
|
+
"",
|
|
25935
|
+
"```text",
|
|
25936
|
+
message,
|
|
25937
|
+
"```",
|
|
25938
|
+
"",
|
|
25939
|
+
`Signature: \`${defectSignature2(where, message)}\``,
|
|
25940
|
+
"",
|
|
25941
|
+
"If the real cause turned out to be the project, the environment or the call, say so here:",
|
|
25942
|
+
"reaching you as a defect rather than as a refusal naming what would have worked is then the",
|
|
25943
|
+
"thing to fix.",
|
|
25944
|
+
"",
|
|
25945
|
+
"## Anything that makes it reproducible",
|
|
25946
|
+
""
|
|
25947
|
+
].join(`
|
|
25948
|
+
`);
|
|
25949
|
+
}
|
|
25950
|
+
function defectReport2(where, error, godotVersion) {
|
|
25951
|
+
const message = errorMessage(error);
|
|
25952
|
+
const title = `Defect ${defectSignature2(where, message)}: ${where}`;
|
|
25953
|
+
const url = `${NEW_ISSUE2}?template=bug_report.md&title=${encodeURIComponent(title)}` + `&body=${encodeURIComponent(filledTemplate2(where, message, godotVersion))}`;
|
|
25954
|
+
const width = Math.max(...facts2(where, message, godotVersion).map(([label]) => (label ?? "").length));
|
|
25955
|
+
return [
|
|
25956
|
+
"gdharness failed in a way it does not model. This is a defect in gdharness rather than",
|
|
25957
|
+
"anything about the call: the arguments did not cause it, and sending it again will not",
|
|
25958
|
+
"change it.",
|
|
25959
|
+
"",
|
|
25960
|
+
...facts2(where, message, godotVersion).map(([label, value]) => ` ${(label ?? "").padEnd(width)} ${value ?? ""}`),
|
|
25961
|
+
"",
|
|
25962
|
+
"Do not open an issue on your own initiative. Tell the person whose machine this is what",
|
|
25963
|
+
"broke, and ask whether you may report it to gdharness on their behalf. What the report",
|
|
25964
|
+
"carries is the lines above and nothing else, and whether that goes into a public tracker",
|
|
25965
|
+
"is theirs to decide.",
|
|
25966
|
+
"",
|
|
25967
|
+
"If they say yes, this link carries the report already filled in:",
|
|
25968
|
+
url,
|
|
25969
|
+
"",
|
|
25970
|
+
"If they say yes and you have no way to open an issue yourself, give them that link and",
|
|
25971
|
+
"those lines, and stay with them while they file it.",
|
|
25972
|
+
"",
|
|
25973
|
+
"Worth reporting either way. If you can see that what actually went wrong was the project,",
|
|
25974
|
+
"the environment or the call, then this message is the defect: a failure gdharness knows",
|
|
25975
|
+
"about is meant to arrive as a refusal naming what would have worked, not as this. Say that",
|
|
25976
|
+
"in the report and it is the more useful of the two."
|
|
25977
|
+
].join(`
|
|
25978
|
+
`);
|
|
25979
|
+
}
|
|
25980
|
+
|
|
25719
25981
|
// src/junit.ts
|
|
25720
25982
|
class MalformedReportError extends Error {
|
|
25721
25983
|
constructor(message) {
|
|
@@ -26083,7 +26345,7 @@ class GodotLSPClient {
|
|
|
26083
26345
|
async sendRequest(method, params) {
|
|
26084
26346
|
await this.ensureConnected();
|
|
26085
26347
|
if (!this.socket) {
|
|
26086
|
-
throw new
|
|
26348
|
+
throw new Refusal("Not connected to Godot LSP");
|
|
26087
26349
|
}
|
|
26088
26350
|
this.requestId += 1;
|
|
26089
26351
|
const id = this.requestId;
|
|
@@ -26122,7 +26384,7 @@ class GodotLSPClient {
|
|
|
26122
26384
|
}
|
|
26123
26385
|
sendNotification(method, params) {
|
|
26124
26386
|
if (!this.connected || !this.socket) {
|
|
26125
|
-
throw new
|
|
26387
|
+
throw new Refusal("Not connected to Godot LSP");
|
|
26126
26388
|
}
|
|
26127
26389
|
const payload = {
|
|
26128
26390
|
jsonrpc: "2.0",
|
|
@@ -26372,36 +26634,36 @@ async function resolveLSPPaths(projectPathValue, scriptPathValue) {
|
|
|
26372
26634
|
try {
|
|
26373
26635
|
projectPath = await realpath(requestedProjectPath);
|
|
26374
26636
|
} catch {
|
|
26375
|
-
throw new
|
|
26637
|
+
throw new Refusal(`Project path does not exist: ${requestedProjectPath}`);
|
|
26376
26638
|
}
|
|
26377
26639
|
const contained = resolveWithinProject(projectPath, scriptPathValue);
|
|
26378
26640
|
if (!contained.ok) {
|
|
26379
|
-
throw new
|
|
26641
|
+
throw new Refusal(contained.reason);
|
|
26380
26642
|
}
|
|
26381
26643
|
let scriptPath;
|
|
26382
26644
|
try {
|
|
26383
26645
|
scriptPath = await realpath(contained.absolutePath);
|
|
26384
26646
|
} catch {
|
|
26385
|
-
throw new
|
|
26647
|
+
throw new Refusal(`Script file does not exist: ${contained.absolutePath}`);
|
|
26386
26648
|
}
|
|
26387
26649
|
if (!isWithinRoot(projectPath, scriptPath)) {
|
|
26388
|
-
throw new
|
|
26650
|
+
throw new Refusal("scriptPath resolves outside the project root boundary.");
|
|
26389
26651
|
}
|
|
26390
26652
|
return { projectPath, scriptPath };
|
|
26391
26653
|
}
|
|
26392
26654
|
async function handleLSPTool(client, toolName, args) {
|
|
26393
26655
|
try {
|
|
26394
26656
|
if (!args || typeof args !== "object") {
|
|
26395
|
-
throw new
|
|
26657
|
+
throw new Refusal("Tool arguments must be an object.");
|
|
26396
26658
|
}
|
|
26397
26659
|
const parsedArgs = args;
|
|
26398
26660
|
const projectPathValue = parsedArgs["projectPath"];
|
|
26399
26661
|
const scriptPathValue = parsedArgs["scriptPath"];
|
|
26400
26662
|
if (typeof projectPathValue !== "string" || projectPathValue.length === 0) {
|
|
26401
|
-
throw new
|
|
26663
|
+
throw new Refusal("Missing required argument: projectPath");
|
|
26402
26664
|
}
|
|
26403
26665
|
if (typeof scriptPathValue !== "string" || scriptPathValue.length === 0) {
|
|
26404
|
-
throw new
|
|
26666
|
+
throw new Refusal("Missing required argument: scriptPath");
|
|
26405
26667
|
}
|
|
26406
26668
|
const { projectPath, scriptPath } = await resolveLSPPaths(projectPathValue, scriptPathValue);
|
|
26407
26669
|
const content = await readFile(scriptPath, "utf8");
|
|
@@ -26415,7 +26677,7 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
26415
26677
|
const line = Number(parsedArgs["line"]);
|
|
26416
26678
|
const character = Number(parsedArgs["character"]);
|
|
26417
26679
|
if (!Number.isFinite(line) || !Number.isFinite(character)) {
|
|
26418
|
-
throw new
|
|
26680
|
+
throw new Refusal("Arguments line and character must be numbers.");
|
|
26419
26681
|
}
|
|
26420
26682
|
const completions = await client.getCompletions(scriptPath, content, line, character);
|
|
26421
26683
|
return asToolResponse({ completions });
|
|
@@ -26424,7 +26686,7 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
26424
26686
|
const line = Number(parsedArgs["line"]);
|
|
26425
26687
|
const character = Number(parsedArgs["character"]);
|
|
26426
26688
|
if (!Number.isFinite(line) || !Number.isFinite(character)) {
|
|
26427
|
-
throw new
|
|
26689
|
+
throw new Refusal("Arguments line and character must be numbers.");
|
|
26428
26690
|
}
|
|
26429
26691
|
const hover = await client.getHover(scriptPath, content, line, character);
|
|
26430
26692
|
return asToolResponse({ hover });
|
|
@@ -26444,19 +26706,19 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
26444
26706
|
}
|
|
26445
26707
|
|
|
26446
26708
|
// src/project-scan.ts
|
|
26447
|
-
import { readdirSync as
|
|
26448
|
-
import { join as
|
|
26709
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
26710
|
+
import { join as join4 } from "node:path";
|
|
26449
26711
|
var SKIPPED = new Set([".git", ".godot", ".import", "node_modules"]);
|
|
26450
26712
|
var ASSET_EXTENSIONS = new Set(["png", "jpg", "jpeg", "webp", "svg", "ttf", "otf", "wav", "mp3", "ogg"]);
|
|
26451
26713
|
function projectStructure(projectPath) {
|
|
26452
26714
|
const structure = { scenes: 0, scripts: 0, assets: 0, other: 0 };
|
|
26453
26715
|
const visit = (directory) => {
|
|
26454
|
-
for (const entry of
|
|
26716
|
+
for (const entry of readdirSync3(directory, { withFileTypes: true })) {
|
|
26455
26717
|
if (entry.name.startsWith(".")) {
|
|
26456
26718
|
continue;
|
|
26457
26719
|
}
|
|
26458
26720
|
if (entry.isDirectory()) {
|
|
26459
|
-
visit(
|
|
26721
|
+
visit(join4(directory, entry.name));
|
|
26460
26722
|
} else if (entry.isFile()) {
|
|
26461
26723
|
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
26462
26724
|
if (extension === "tscn") {
|
|
@@ -26491,14 +26753,14 @@ function searchProject(projectPath, options) {
|
|
|
26491
26753
|
return false;
|
|
26492
26754
|
};
|
|
26493
26755
|
const visit = (directory) => {
|
|
26494
|
-
for (const entry of
|
|
26756
|
+
for (const entry of readdirSync3(directory, { withFileTypes: true })) {
|
|
26495
26757
|
if (full()) {
|
|
26496
26758
|
return;
|
|
26497
26759
|
}
|
|
26498
26760
|
if (SKIPPED.has(entry.name)) {
|
|
26499
26761
|
continue;
|
|
26500
26762
|
}
|
|
26501
|
-
const entryPath =
|
|
26763
|
+
const entryPath = join4(directory, entry.name);
|
|
26502
26764
|
if (entry.isDirectory()) {
|
|
26503
26765
|
visit(entryPath);
|
|
26504
26766
|
continue;
|
|
@@ -26509,7 +26771,7 @@ function searchProject(projectPath, options) {
|
|
|
26509
26771
|
}
|
|
26510
26772
|
result.summary.files_searched += 1;
|
|
26511
26773
|
const matches = [];
|
|
26512
|
-
for (const [index, line] of
|
|
26774
|
+
for (const [index, line] of readFileSync3(entryPath, "utf8").split(`
|
|
26513
26775
|
`).entries()) {
|
|
26514
26776
|
if (full()) {
|
|
26515
26777
|
break;
|
|
@@ -26532,7 +26794,7 @@ function searchProject(projectPath, options) {
|
|
|
26532
26794
|
}
|
|
26533
26795
|
|
|
26534
26796
|
// src/resources.ts
|
|
26535
|
-
import { readFileSync as
|
|
26797
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
26536
26798
|
import { extname, resolve as resolve3 } from "node:path";
|
|
26537
26799
|
var STATIC_RESOURCES = [
|
|
26538
26800
|
{
|
|
@@ -26562,24 +26824,25 @@ var RESOURCE_TEMPLATES = [
|
|
|
26562
26824
|
mimeType: "text/plain"
|
|
26563
26825
|
}
|
|
26564
26826
|
];
|
|
26827
|
+
var RESOURCE_COUNT = STATIC_RESOURCES.length + RESOURCE_TEMPLATES.length;
|
|
26565
26828
|
function ensureProjectPath(getProjectPath) {
|
|
26566
26829
|
const projectPath = getProjectPath();
|
|
26567
26830
|
if (!projectPath) {
|
|
26568
|
-
throw new
|
|
26831
|
+
throw new Refusal("Project path is not set. Set a Godot project path first.");
|
|
26569
26832
|
}
|
|
26570
26833
|
return resolve3(projectPath);
|
|
26571
26834
|
}
|
|
26572
26835
|
function uriPathToProjectPath(inputPath) {
|
|
26573
26836
|
const normalized = inputPath.replace(/\\/g, "/").trim();
|
|
26574
26837
|
if (normalized.replace(/\//g, "") === "") {
|
|
26575
|
-
throw new
|
|
26838
|
+
throw new Refusal("Resource path is empty.");
|
|
26576
26839
|
}
|
|
26577
26840
|
return normalized.replace(/^\/+/, "");
|
|
26578
26841
|
}
|
|
26579
26842
|
function resolveProjectFile(projectPath, resourcePath) {
|
|
26580
26843
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
26581
26844
|
if (!contained.ok) {
|
|
26582
|
-
throw new
|
|
26845
|
+
throw new Refusal(contained.reason);
|
|
26583
26846
|
}
|
|
26584
26847
|
return contained.absolutePath;
|
|
26585
26848
|
}
|
|
@@ -26591,14 +26854,14 @@ function parseGodotUri(uri) {
|
|
|
26591
26854
|
try {
|
|
26592
26855
|
parsed = new URL(uri);
|
|
26593
26856
|
} catch {
|
|
26594
|
-
throw new
|
|
26857
|
+
throw new Refusal(`Invalid URI: ${uri}`);
|
|
26595
26858
|
}
|
|
26596
26859
|
if (parsed.protocol !== "godot:") {
|
|
26597
|
-
throw new
|
|
26860
|
+
throw new Refusal(`Unsupported URI scheme: ${parsed.protocol}`);
|
|
26598
26861
|
}
|
|
26599
26862
|
const host = parsed.hostname;
|
|
26600
26863
|
if (host !== "scene" && host !== "script" && host !== "resource") {
|
|
26601
|
-
throw new
|
|
26864
|
+
throw new Refusal(`Unsupported Godot resource type: ${host}`);
|
|
26602
26865
|
}
|
|
26603
26866
|
const resourcePath = uriPathToProjectPath(decodeURIComponent(parsed.pathname));
|
|
26604
26867
|
return { kind: host, resourcePath };
|
|
@@ -26606,13 +26869,13 @@ function parseGodotUri(uri) {
|
|
|
26606
26869
|
function ensureAllowedExtension(kind, filePath) {
|
|
26607
26870
|
const extension = extname(filePath).toLowerCase();
|
|
26608
26871
|
if (kind === "scene" && extension !== ".tscn") {
|
|
26609
|
-
throw new
|
|
26872
|
+
throw new Refusal("Scene resources must use .tscn extension.");
|
|
26610
26873
|
}
|
|
26611
26874
|
if (kind === "script" && extension !== ".gd") {
|
|
26612
|
-
throw new
|
|
26875
|
+
throw new Refusal("Script resources must use .gd extension.");
|
|
26613
26876
|
}
|
|
26614
26877
|
if (kind === "resource" && ![".tres", ".tscn", ".gd"].includes(extension)) {
|
|
26615
|
-
throw new
|
|
26878
|
+
throw new Refusal("Resource URIs support only .tres, .tscn, and .gd files.");
|
|
26616
26879
|
}
|
|
26617
26880
|
}
|
|
26618
26881
|
function isValueComplete(value) {
|
|
@@ -26702,7 +26965,7 @@ function readResourceText(uri, getProjectPath) {
|
|
|
26702
26965
|
const parsedUri = parseGodotUri(uri);
|
|
26703
26966
|
if (parsedUri.kind === "project-info") {
|
|
26704
26967
|
const projectFilePath = resolveProjectFile(projectPath, "project.godot");
|
|
26705
|
-
const rawProject =
|
|
26968
|
+
const rawProject = readFileSync4(projectFilePath, "utf-8");
|
|
26706
26969
|
const parsedProject = parseProjectGodot(rawProject);
|
|
26707
26970
|
return {
|
|
26708
26971
|
mimeType: "application/json",
|
|
@@ -26711,7 +26974,7 @@ function readResourceText(uri, getProjectPath) {
|
|
|
26711
26974
|
}
|
|
26712
26975
|
const filePath = resolveProjectFile(projectPath, parsedUri.resourcePath);
|
|
26713
26976
|
ensureAllowedExtension(parsedUri.kind, filePath);
|
|
26714
|
-
const text =
|
|
26977
|
+
const text = readFileSync4(filePath, "utf-8");
|
|
26715
26978
|
return {
|
|
26716
26979
|
mimeType: parsedUri.kind === "script" ? "text/x-gdscript" : "text/plain",
|
|
26717
26980
|
text
|
|
@@ -26740,18 +27003,18 @@ function setupResourceHandlers(mcp, getProjectPath) {
|
|
|
26740
27003
|
};
|
|
26741
27004
|
} catch (error) {
|
|
26742
27005
|
if (error instanceof Error) {
|
|
26743
|
-
throw new
|
|
27006
|
+
throw new Refusal(`Failed to read resource '${uri}': ${error.message}`, { cause: error });
|
|
26744
27007
|
}
|
|
26745
|
-
throw new
|
|
27008
|
+
throw new Refusal(`Failed to read resource '${uri}'.`, { cause: error });
|
|
26746
27009
|
}
|
|
26747
27010
|
});
|
|
26748
27011
|
}
|
|
26749
27012
|
|
|
26750
27013
|
// src/runtime-client.ts
|
|
26751
|
-
import { existsSync as
|
|
27014
|
+
import { existsSync as existsSync4, readdirSync as readdirSync4, readFileSync as readFileSync5, unlinkSync } from "node:fs";
|
|
26752
27015
|
import { createConnection as createConnection3 } from "node:net";
|
|
26753
27016
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
26754
|
-
import { join as
|
|
27017
|
+
import { join as join5, resolve as resolve4 } from "node:path";
|
|
26755
27018
|
|
|
26756
27019
|
// src/tool-args.ts
|
|
26757
27020
|
function asParams(value) {
|
|
@@ -26813,7 +27076,7 @@ function runtimeDirectory(variables = process.env) {
|
|
|
26813
27076
|
return explicit;
|
|
26814
27077
|
}
|
|
26815
27078
|
const perUser = envValue("XDG_RUNTIME_DIR", variables);
|
|
26816
|
-
return
|
|
27079
|
+
return join5(perUser ?? tmpdir2(), "gdharness");
|
|
26817
27080
|
}
|
|
26818
27081
|
function processAlive(pid) {
|
|
26819
27082
|
try {
|
|
@@ -26826,7 +27089,7 @@ function processAlive(pid) {
|
|
|
26826
27089
|
function parseAnnouncement(file, pid) {
|
|
26827
27090
|
let fields;
|
|
26828
27091
|
try {
|
|
26829
|
-
fields = asParams(JSON.parse(
|
|
27092
|
+
fields = asParams(JSON.parse(readFileSync5(file, "utf8")));
|
|
26830
27093
|
} catch {
|
|
26831
27094
|
return null;
|
|
26832
27095
|
}
|
|
@@ -26845,16 +27108,16 @@ function parseAnnouncement(file, pid) {
|
|
|
26845
27108
|
};
|
|
26846
27109
|
}
|
|
26847
27110
|
function discoverRuntimes(directory = runtimeDirectory()) {
|
|
26848
|
-
if (!
|
|
27111
|
+
if (!existsSync4(directory)) {
|
|
26849
27112
|
return [];
|
|
26850
27113
|
}
|
|
26851
27114
|
const found = [];
|
|
26852
|
-
for (const entry of
|
|
27115
|
+
for (const entry of readdirSync4(directory)) {
|
|
26853
27116
|
const match = ANNOUNCEMENT_PATTERN.exec(entry);
|
|
26854
27117
|
if (!match) {
|
|
26855
27118
|
continue;
|
|
26856
27119
|
}
|
|
26857
|
-
const file =
|
|
27120
|
+
const file = join5(directory, entry);
|
|
26858
27121
|
const pid = Number.parseInt(match[1] ?? "", 10);
|
|
26859
27122
|
const endpoint = processAlive(pid) ? parseAnnouncement(file, pid) : null;
|
|
26860
27123
|
if (endpoint) {
|
|
@@ -26987,19 +27250,6 @@ function runtimeRequest(endpoint, command, params, timeoutMs) {
|
|
|
26987
27250
|
});
|
|
26988
27251
|
}
|
|
26989
27252
|
|
|
26990
|
-
// src/server-version.ts
|
|
26991
|
-
import { readFileSync as readFileSync4 } from "node:fs";
|
|
26992
|
-
var DEBUG_MODE = process.env["DEBUG"] === "true";
|
|
26993
|
-
var GODOT_DEBUG_MODE_DEFAULT = process.env["GODOT_DEBUG"] === "true" || DEBUG_MODE;
|
|
26994
|
-
var SERVER_VERSION = (() => {
|
|
26995
|
-
try {
|
|
26996
|
-
const pkg = JSON.parse(readFileSync4(new URL("../package.json", import.meta.url), "utf8"));
|
|
26997
|
-
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
26998
|
-
} catch {
|
|
26999
|
-
return "0.0.0";
|
|
27000
|
-
}
|
|
27001
|
-
})();
|
|
27002
|
-
|
|
27003
27253
|
// src/tool-definitions.ts
|
|
27004
27254
|
var PROJECT_PATH = {
|
|
27005
27255
|
type: "string",
|
|
@@ -27903,7 +28153,190 @@ function buildToolDefinitions() {
|
|
|
27903
28153
|
});
|
|
27904
28154
|
}
|
|
27905
28155
|
|
|
28156
|
+
// src/update-check.ts
|
|
28157
|
+
import { existsSync as existsSync5, mkdirSync, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "node:fs";
|
|
28158
|
+
import { homedir as homedir2, tmpdir as tmpdir3 } from "node:os";
|
|
28159
|
+
import { join as join6 } from "node:path";
|
|
28160
|
+
|
|
28161
|
+
// src/runner.ts
|
|
28162
|
+
function currentRunner() {
|
|
28163
|
+
const versions = process.versions;
|
|
28164
|
+
return versions["bun"] === undefined ? "npx" : "bunx";
|
|
28165
|
+
}
|
|
28166
|
+
function runLine(runner, version, rest = "") {
|
|
28167
|
+
const flag = runner === "npx" ? "-y " : "";
|
|
28168
|
+
return `${runner} ${flag}gdharness@${version}${rest === "" ? "" : ` ${rest}`}`;
|
|
28169
|
+
}
|
|
28170
|
+
|
|
28171
|
+
// src/update-check.ts
|
|
28172
|
+
var REGISTRY = "https://registry.npmjs.org/gdharness/latest";
|
|
28173
|
+
var RELEASES = "https://github.com/Aureliolo/gdharness/releases/tag";
|
|
28174
|
+
var CACHE_MS = 4 * 60 * 60 * 1000;
|
|
28175
|
+
var REQUEST_TIMEOUT_MS = 1e4;
|
|
28176
|
+
var MAX_BODY_BYTES = 1 << 20;
|
|
28177
|
+
var FIRST_RETRY_MS = 30000;
|
|
28178
|
+
var MAX_RETRY_MS = 60 * 60 * 1000;
|
|
28179
|
+
var VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
28180
|
+
function cacheDirectory(environment) {
|
|
28181
|
+
const set = (name) => {
|
|
28182
|
+
const value = environment[name];
|
|
28183
|
+
return value !== undefined && value !== "" ? value : null;
|
|
28184
|
+
};
|
|
28185
|
+
const home = set("HOME") ?? homedir2();
|
|
28186
|
+
if (process.platform === "win32") {
|
|
28187
|
+
return join6(set("LOCALAPPDATA") ?? home, "gdharness");
|
|
28188
|
+
}
|
|
28189
|
+
if (process.platform === "darwin") {
|
|
28190
|
+
return join6(home, "Library", "Caches", "gdharness");
|
|
28191
|
+
}
|
|
28192
|
+
return join6(set("XDG_CACHE_HOME") ?? join6(home, ".cache"), "gdharness");
|
|
28193
|
+
}
|
|
28194
|
+
function cacheFile(environment = process.env) {
|
|
28195
|
+
try {
|
|
28196
|
+
const directory = cacheDirectory(environment);
|
|
28197
|
+
mkdirSync(directory, { recursive: true, mode: 448 });
|
|
28198
|
+
return join6(directory, "update-check.json");
|
|
28199
|
+
} catch {
|
|
28200
|
+
return join6(tmpdir3(), "gdharness-update-check.json");
|
|
28201
|
+
}
|
|
28202
|
+
}
|
|
28203
|
+
function readCache(path) {
|
|
28204
|
+
try {
|
|
28205
|
+
if (!existsSync5(path)) {
|
|
28206
|
+
return null;
|
|
28207
|
+
}
|
|
28208
|
+
const parsed = JSON.parse(readFileSync6(path, "utf8"));
|
|
28209
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
28210
|
+
return null;
|
|
28211
|
+
}
|
|
28212
|
+
const record = parsed;
|
|
28213
|
+
const checkedAt = record["checkedAt"];
|
|
28214
|
+
const latest = record["latest"];
|
|
28215
|
+
if (typeof checkedAt !== "number" || typeof latest !== "string" || !VERSION.test(latest)) {
|
|
28216
|
+
return null;
|
|
28217
|
+
}
|
|
28218
|
+
return { checkedAt, latest };
|
|
28219
|
+
} catch {
|
|
28220
|
+
return null;
|
|
28221
|
+
}
|
|
28222
|
+
}
|
|
28223
|
+
function writeCache(path, entry) {
|
|
28224
|
+
try {
|
|
28225
|
+
writeFileSync2(path, JSON.stringify(entry), { encoding: "utf8", mode: 384 });
|
|
28226
|
+
} catch {}
|
|
28227
|
+
}
|
|
28228
|
+
function parts(version) {
|
|
28229
|
+
const withoutBuild = version.split("+")[0] ?? version;
|
|
28230
|
+
const dash = withoutBuild.indexOf("-");
|
|
28231
|
+
const numeric = dash === -1 ? withoutBuild : withoutBuild.slice(0, dash);
|
|
28232
|
+
return {
|
|
28233
|
+
numbers: numeric.split(".").map((piece) => Number.parseInt(piece, 10) || 0),
|
|
28234
|
+
prerelease: dash !== -1
|
|
28235
|
+
};
|
|
28236
|
+
}
|
|
28237
|
+
function isNewer(candidate, current) {
|
|
28238
|
+
const left = parts(candidate);
|
|
28239
|
+
const right = parts(current);
|
|
28240
|
+
for (let index = 0;index < 3; index += 1) {
|
|
28241
|
+
const a = left.numbers[index] ?? 0;
|
|
28242
|
+
const b = right.numbers[index] ?? 0;
|
|
28243
|
+
if (a !== b) {
|
|
28244
|
+
return a > b;
|
|
28245
|
+
}
|
|
28246
|
+
}
|
|
28247
|
+
return !left.prerelease && right.prerelease;
|
|
28248
|
+
}
|
|
28249
|
+
async function fetchLatest() {
|
|
28250
|
+
const response = await fetch(REGISTRY, {
|
|
28251
|
+
headers: { accept: "application/vnd.npm.install-v1+json, application/json" },
|
|
28252
|
+
redirect: "error",
|
|
28253
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
28254
|
+
});
|
|
28255
|
+
if (!response.ok || response.body === null) {
|
|
28256
|
+
return null;
|
|
28257
|
+
}
|
|
28258
|
+
const declared = Number.parseInt(response.headers.get("content-length") ?? "", 10);
|
|
28259
|
+
if (Number.isFinite(declared) && declared > MAX_BODY_BYTES) {
|
|
28260
|
+
return null;
|
|
28261
|
+
}
|
|
28262
|
+
const chunks = [];
|
|
28263
|
+
let size = 0;
|
|
28264
|
+
for await (const chunk of response.body) {
|
|
28265
|
+
size += chunk.byteLength;
|
|
28266
|
+
if (size > MAX_BODY_BYTES) {
|
|
28267
|
+
return null;
|
|
28268
|
+
}
|
|
28269
|
+
chunks.push(chunk);
|
|
28270
|
+
}
|
|
28271
|
+
const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
28272
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
28273
|
+
return null;
|
|
28274
|
+
}
|
|
28275
|
+
const version = parsed["version"];
|
|
28276
|
+
return typeof version === "string" && VERSION.test(version) ? version : null;
|
|
28277
|
+
}
|
|
28278
|
+
|
|
28279
|
+
class UpdateCheck {
|
|
28280
|
+
latest = null;
|
|
28281
|
+
checking = false;
|
|
28282
|
+
checkedAt = 0;
|
|
28283
|
+
retryAt = 0;
|
|
28284
|
+
backoffMs = FIRST_RETRY_MS;
|
|
28285
|
+
enabled;
|
|
28286
|
+
current;
|
|
28287
|
+
cachePath;
|
|
28288
|
+
constructor(current, environment = process.env) {
|
|
28289
|
+
this.current = current;
|
|
28290
|
+
this.enabled = (environment["GDHARNESS_NO_UPDATE_CHECK"] ?? "") === "";
|
|
28291
|
+
this.cachePath = cacheFile(environment);
|
|
28292
|
+
const cached = this.enabled ? readCache(this.cachePath) : null;
|
|
28293
|
+
if (cached !== null) {
|
|
28294
|
+
this.latest = cached.latest;
|
|
28295
|
+
this.checkedAt = cached.checkedAt;
|
|
28296
|
+
}
|
|
28297
|
+
}
|
|
28298
|
+
refresh(now = Date.now()) {
|
|
28299
|
+
if (!this.enabled || this.checking || now < this.retryAt || now - this.checkedAt < CACHE_MS) {
|
|
28300
|
+
return;
|
|
28301
|
+
}
|
|
28302
|
+
this.checking = true;
|
|
28303
|
+
fetchLatest().then((version) => {
|
|
28304
|
+
if (version === null) {
|
|
28305
|
+
this.scheduleRetry(now);
|
|
28306
|
+
return;
|
|
28307
|
+
}
|
|
28308
|
+
this.latest = version;
|
|
28309
|
+
this.checkedAt = Date.now();
|
|
28310
|
+
this.backoffMs = FIRST_RETRY_MS;
|
|
28311
|
+
this.retryAt = 0;
|
|
28312
|
+
writeCache(this.cachePath, { checkedAt: this.checkedAt, latest: version });
|
|
28313
|
+
}).catch(() => {
|
|
28314
|
+
this.scheduleRetry(now);
|
|
28315
|
+
}).finally(() => {
|
|
28316
|
+
this.checking = false;
|
|
28317
|
+
});
|
|
28318
|
+
}
|
|
28319
|
+
scheduleRetry(now) {
|
|
28320
|
+
this.retryAt = now + this.backoffMs;
|
|
28321
|
+
this.backoffMs = Math.min(this.backoffMs * 2, MAX_RETRY_MS);
|
|
28322
|
+
}
|
|
28323
|
+
notice() {
|
|
28324
|
+
const latest = this.latest;
|
|
28325
|
+
if (!this.enabled || latest === null || !isNewer(latest, this.current)) {
|
|
28326
|
+
return null;
|
|
28327
|
+
}
|
|
28328
|
+
return {
|
|
28329
|
+
current: this.current,
|
|
28330
|
+
latest,
|
|
28331
|
+
releaseNotes: `${RELEASES}/v${latest}`,
|
|
28332
|
+
upgrade: runLine(currentRunner(), latest, "upgrade")
|
|
28333
|
+
};
|
|
28334
|
+
}
|
|
28335
|
+
}
|
|
28336
|
+
|
|
27906
28337
|
// src/server.ts
|
|
28338
|
+
var UPDATE_NOTICE_EVERY = 500;
|
|
28339
|
+
var FEEDBACK_NOTICE_EVERY = 250;
|
|
27907
28340
|
var run3 = promisify3(execFile3);
|
|
27908
28341
|
var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
|
|
27909
28342
|
var EDITOR_RESTART_TIMEOUT_MS = 90000;
|
|
@@ -27982,11 +28415,11 @@ function realPathOr(path) {
|
|
|
27982
28415
|
}
|
|
27983
28416
|
}
|
|
27984
28417
|
function hasMainScene(projectFile) {
|
|
27985
|
-
const scene = parseProjectGodot(
|
|
28418
|
+
const scene = parseProjectGodot(readFileSync7(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
27986
28419
|
return typeof scene === "string" && scene !== "";
|
|
27987
28420
|
}
|
|
27988
28421
|
function editorPlaysHeadless(projectFile) {
|
|
27989
|
-
const runArgs = parseProjectGodot(
|
|
28422
|
+
const runArgs = parseProjectGodot(readFileSync7(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
27990
28423
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
27991
28424
|
}
|
|
27992
28425
|
function camelCased(params) {
|
|
@@ -28002,10 +28435,14 @@ function camelCased(params) {
|
|
|
28002
28435
|
class GodotServer {
|
|
28003
28436
|
mcp;
|
|
28004
28437
|
locator = new GodotLocator;
|
|
28005
|
-
operationsScript =
|
|
28438
|
+
operationsScript = join7(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
28006
28439
|
godotBridge;
|
|
28007
28440
|
tools = buildToolDefinitions();
|
|
28008
28441
|
activeProcess = null;
|
|
28442
|
+
updates = new UpdateCheck(SERVER_VERSION);
|
|
28443
|
+
noticedUpdate = false;
|
|
28444
|
+
callsSinceNotice = 0;
|
|
28445
|
+
callsSinceFeedback = 0;
|
|
28009
28446
|
lspClient = null;
|
|
28010
28447
|
dapClient = null;
|
|
28011
28448
|
bridgeStartupError = null;
|
|
@@ -28163,9 +28600,61 @@ class GodotServer {
|
|
|
28163
28600
|
if (typeof args["projectPath"] === "string") {
|
|
28164
28601
|
this.lastProjectPath = args["projectPath"];
|
|
28165
28602
|
}
|
|
28166
|
-
|
|
28603
|
+
this.updates.refresh();
|
|
28604
|
+
const answer = await this.answered(spec.name, checked.op ?? "", args);
|
|
28605
|
+
return this.withFeedbackNotice(this.withUpdateNotice(answer));
|
|
28167
28606
|
});
|
|
28168
28607
|
}
|
|
28608
|
+
async answered(tool, op, args) {
|
|
28609
|
+
try {
|
|
28610
|
+
return await this.dispatch(tool, op, args);
|
|
28611
|
+
} catch (error) {
|
|
28612
|
+
if (error instanceof McpError || error instanceof Refusal) {
|
|
28613
|
+
throw error;
|
|
28614
|
+
}
|
|
28615
|
+
const where = op === "" ? tool : `${tool} op=${op}`;
|
|
28616
|
+
console.error(`[SERVER] Unmodelled failure in ${where}:`, error);
|
|
28617
|
+
return { content: [{ type: "text", text: defectReport2(where, error) }], isError: true };
|
|
28618
|
+
}
|
|
28619
|
+
}
|
|
28620
|
+
withUpdateNotice(answer) {
|
|
28621
|
+
this.callsSinceNotice += 1;
|
|
28622
|
+
if (this.callsSinceNotice < UPDATE_NOTICE_EVERY && this.noticedUpdate) {
|
|
28623
|
+
return answer;
|
|
28624
|
+
}
|
|
28625
|
+
const notice = this.updates.notice();
|
|
28626
|
+
if (notice === null) {
|
|
28627
|
+
return answer;
|
|
28628
|
+
}
|
|
28629
|
+
this.noticedUpdate = true;
|
|
28630
|
+
this.callsSinceNotice = 0;
|
|
28631
|
+
return {
|
|
28632
|
+
...answer,
|
|
28633
|
+
content: [
|
|
28634
|
+
...answer.content,
|
|
28635
|
+
{
|
|
28636
|
+
type: "text",
|
|
28637
|
+
text: JSON.stringify({
|
|
28638
|
+
update_available: {
|
|
28639
|
+
...notice,
|
|
28640
|
+
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."
|
|
28641
|
+
}
|
|
28642
|
+
}, null, 2)
|
|
28643
|
+
}
|
|
28644
|
+
]
|
|
28645
|
+
};
|
|
28646
|
+
}
|
|
28647
|
+
withFeedbackNotice(answer) {
|
|
28648
|
+
this.callsSinceFeedback += 1;
|
|
28649
|
+
if (this.callsSinceFeedback < FEEDBACK_NOTICE_EVERY) {
|
|
28650
|
+
return answer;
|
|
28651
|
+
}
|
|
28652
|
+
this.callsSinceFeedback = 0;
|
|
28653
|
+
return {
|
|
28654
|
+
...answer,
|
|
28655
|
+
content: [...answer.content, { type: "text", text: feedbackNotice() }]
|
|
28656
|
+
};
|
|
28657
|
+
}
|
|
28169
28658
|
validateArguments(spec, args) {
|
|
28170
28659
|
const known = new Set([...Object.keys(spec.parameters), ...spec.operations ? ["op"] : []]);
|
|
28171
28660
|
const unknown = Object.keys(args).filter((key) => !known.has(key));
|
|
@@ -28371,9 +28860,16 @@ class GodotServer {
|
|
|
28371
28860
|
});
|
|
28372
28861
|
}
|
|
28373
28862
|
case "debug_control":
|
|
28374
|
-
|
|
28375
|
-
|
|
28376
|
-
|
|
28863
|
+
case "debug_state": {
|
|
28864
|
+
if (tool === "debug_state" && op === "output") {
|
|
28865
|
+
return await this.handleDAP("dap_get_output", args);
|
|
28866
|
+
}
|
|
28867
|
+
const held = await this.debuggedGame();
|
|
28868
|
+
if (!held.ok) {
|
|
28869
|
+
return held.response;
|
|
28870
|
+
}
|
|
28871
|
+
return tool === "debug_control" ? await this.handleDAP(`dap_${op}`, args) : await this.handleDAP(DEBUG_STATE_CALLS[op] ?? "dap_get_stack_trace", args);
|
|
28872
|
+
}
|
|
28377
28873
|
default:
|
|
28378
28874
|
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${tool}`);
|
|
28379
28875
|
}
|
|
@@ -28393,8 +28889,8 @@ class GodotServer {
|
|
|
28393
28889
|
if (path === undefined) {
|
|
28394
28890
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
28395
28891
|
}
|
|
28396
|
-
const file =
|
|
28397
|
-
if (!
|
|
28892
|
+
const file = join7(path, "project.godot");
|
|
28893
|
+
if (!existsSync6(file)) {
|
|
28398
28894
|
return {
|
|
28399
28895
|
ok: false,
|
|
28400
28896
|
response: this.createErrorResponse(`Not a Godot project: ${path}`, [
|
|
@@ -28404,6 +28900,56 @@ class GodotServer {
|
|
|
28404
28900
|
}
|
|
28405
28901
|
return { ok: true, value: { path, file } };
|
|
28406
28902
|
}
|
|
28903
|
+
async debuggedGame() {
|
|
28904
|
+
const game = this.activeProcess;
|
|
28905
|
+
if (game === null) {
|
|
28906
|
+
return {
|
|
28907
|
+
ok: false,
|
|
28908
|
+
response: this.createErrorResponse("No game is running, so there is no debug session to answer for.", ["Start one with editor_run start, which has the editor play it"])
|
|
28909
|
+
};
|
|
28910
|
+
}
|
|
28911
|
+
if (!game.throughEditor) {
|
|
28912
|
+
return {
|
|
28913
|
+
ok: false,
|
|
28914
|
+
response: this.createErrorResponse("The running game is its own process, so no debugger is holding it: breakpoints never hit and there is no stack to read.", [
|
|
28915
|
+
"editor_run start plays it through the open editor, whose debugger the debug_* tools speak to",
|
|
28916
|
+
"editor_status says whether an editor has reached this server"
|
|
28917
|
+
])
|
|
28918
|
+
};
|
|
28919
|
+
}
|
|
28920
|
+
try {
|
|
28921
|
+
await this.dap().attach();
|
|
28922
|
+
} catch (error) {
|
|
28923
|
+
return {
|
|
28924
|
+
ok: false,
|
|
28925
|
+
response: this.createErrorResponse(`The editor is playing the game but its debug adapter did not answer: ${errorMessage(error)}`, [
|
|
28926
|
+
"Godot serves the debug adapter on 6006 unless --dap-port says otherwise",
|
|
28927
|
+
"GDHARNESS_DAP_PORT points this server at another one"
|
|
28928
|
+
])
|
|
28929
|
+
};
|
|
28930
|
+
}
|
|
28931
|
+
if (!this.dap().isStopped()) {
|
|
28932
|
+
return {
|
|
28933
|
+
ok: false,
|
|
28934
|
+
response: this.createErrorResponse("The game is running, not stopped, so it has no stack and no scope to read.", [
|
|
28935
|
+
"debug_breakpoint set puts a breakpoint on before the run, and it is waiting when the game starts",
|
|
28936
|
+
"editor_output reads what the running game is printing"
|
|
28937
|
+
])
|
|
28938
|
+
};
|
|
28939
|
+
}
|
|
28940
|
+
return { ok: true, value: game };
|
|
28941
|
+
}
|
|
28942
|
+
async refreshStaleClasses(projectPath) {
|
|
28943
|
+
const stale = staleClassNames(projectPath);
|
|
28944
|
+
if (stale.length === 0) {
|
|
28945
|
+
return { ok: true, value: [] };
|
|
28946
|
+
}
|
|
28947
|
+
const refreshed = await this.operation("refresh_class_cache", {}, projectPath);
|
|
28948
|
+
if (!refreshed.ok) {
|
|
28949
|
+
return { ok: false, response: this.answer(refreshed) };
|
|
28950
|
+
}
|
|
28951
|
+
return { ok: true, value: stale };
|
|
28952
|
+
}
|
|
28407
28953
|
containProjectFiles(args) {
|
|
28408
28954
|
const projectPath = readString(args, "projectPath");
|
|
28409
28955
|
if (!projectPath) {
|
|
@@ -28489,7 +29035,7 @@ class GodotServer {
|
|
|
28489
29035
|
if (!engine.ok) {
|
|
28490
29036
|
return engine.response;
|
|
28491
29037
|
}
|
|
28492
|
-
const application = parseProjectGodot(
|
|
29038
|
+
const application = parseProjectGodot(readFileSync7(project.value.file, "utf8"))["application"] ?? {};
|
|
28493
29039
|
const name = application["config/name"];
|
|
28494
29040
|
const mainScene = application["run/main_scene"];
|
|
28495
29041
|
const info = {
|
|
@@ -28578,7 +29124,7 @@ class GodotServer {
|
|
|
28578
29124
|
log.finish();
|
|
28579
29125
|
const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
|
|
28580
29126
|
const verdict = {
|
|
28581
|
-
exported: exitCode === 0 && log.count("error") === 0 &&
|
|
29127
|
+
exported: exitCode === 0 && log.count("error") === 0 && existsSync6(output.absolutePath),
|
|
28582
29128
|
preset,
|
|
28583
29129
|
outputPath: output.relativePath,
|
|
28584
29130
|
debug,
|
|
@@ -28608,7 +29154,7 @@ class GodotServer {
|
|
|
28608
29154
|
return contained.response;
|
|
28609
29155
|
}
|
|
28610
29156
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
28611
|
-
if (!
|
|
29157
|
+
if (!existsSync6(join7(project.value.path, runner))) {
|
|
28612
29158
|
return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
|
|
28613
29159
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
28614
29160
|
]);
|
|
@@ -28656,14 +29202,14 @@ class GodotServer {
|
|
|
28656
29202
|
resolve(false);
|
|
28657
29203
|
});
|
|
28658
29204
|
});
|
|
28659
|
-
const reportsDir =
|
|
29205
|
+
const reportsDir = join7(project.value.path, ".godot", "gdharness-reports");
|
|
28660
29206
|
let report = null;
|
|
28661
29207
|
let reportProblem = null;
|
|
28662
29208
|
try {
|
|
28663
|
-
const written =
|
|
29209
|
+
const written = existsSync6(reportsDir) ? readdirSync5(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
28664
29210
|
const newest = written.at(-1);
|
|
28665
29211
|
if (newest !== undefined) {
|
|
28666
|
-
report = parseJUnit(
|
|
29212
|
+
report = parseJUnit(readFileSync7(join7(reportsDir, newest, "results.xml"), "utf8"));
|
|
28667
29213
|
}
|
|
28668
29214
|
} catch (error) {
|
|
28669
29215
|
reportProblem = errorMessage(error);
|
|
@@ -28876,6 +29422,13 @@ class GodotServer {
|
|
|
28876
29422
|
if (!project.ok) {
|
|
28877
29423
|
return project.response;
|
|
28878
29424
|
}
|
|
29425
|
+
if (this.godotBridge.isConnected()) {
|
|
29426
|
+
return this.createErrorResponse("An editor is already connected to this server, and a second one would take the language server and debug adapter ports from it.", [
|
|
29427
|
+
"editor_status says which editor is answering, and for which project",
|
|
29428
|
+
"editor_launch restart replaces the connected editor rather than joining it",
|
|
29429
|
+
"Close the open editor first if the new project is the one you want"
|
|
29430
|
+
]);
|
|
29431
|
+
}
|
|
28879
29432
|
const engine = await this.engine();
|
|
28880
29433
|
if (!engine.ok) {
|
|
28881
29434
|
return engine.response;
|
|
@@ -28923,6 +29476,10 @@ class GodotServer {
|
|
|
28923
29476
|
if (!engine.ok) {
|
|
28924
29477
|
return engine.response;
|
|
28925
29478
|
}
|
|
29479
|
+
const refreshed = await this.refreshStaleClasses(project.value.path);
|
|
29480
|
+
if (!refreshed.ok) {
|
|
29481
|
+
return refreshed.response;
|
|
29482
|
+
}
|
|
28926
29483
|
const sceneArgument = sceneToRun?.ok ? sceneToRun.relativePath : null;
|
|
28927
29484
|
if (op === "check") {
|
|
28928
29485
|
return await this.checkBoot(engine.value, project.value.path, sceneArgument, args);
|
|
@@ -28936,7 +29493,7 @@ class GodotServer {
|
|
|
28936
29493
|
variables: process.env
|
|
28937
29494
|
});
|
|
28938
29495
|
if (this.godotBridge.isConnected() && (!headless || editorPlaysHeadless(project.value.file))) {
|
|
28939
|
-
return await this.playThroughEditor(sceneArgument);
|
|
29496
|
+
return await this.playThroughEditor(sceneArgument, refreshed.value);
|
|
28940
29497
|
}
|
|
28941
29498
|
const cmdArgs = runArguments({
|
|
28942
29499
|
projectPath: project.value.path,
|
|
@@ -28956,10 +29513,11 @@ class GodotServer {
|
|
|
28956
29513
|
through: "gdharness",
|
|
28957
29514
|
pid: started.process.pid ?? null,
|
|
28958
29515
|
arguments: cmdArgs,
|
|
29516
|
+
refreshedClasses: refreshed.value,
|
|
28959
29517
|
message: "Use editor_output for what it prints and editor_run stop to end it."
|
|
28960
29518
|
});
|
|
28961
29519
|
}
|
|
28962
|
-
async playThroughEditor(scene) {
|
|
29520
|
+
async playThroughEditor(scene, refreshedClasses) {
|
|
28963
29521
|
const log = new GameLog;
|
|
28964
29522
|
try {
|
|
28965
29523
|
await this.dap().connect();
|
|
@@ -28985,6 +29543,7 @@ class GodotServer {
|
|
|
28985
29543
|
started: true,
|
|
28986
29544
|
through: "editor",
|
|
28987
29545
|
scene: scene === null ? "the main scene" : `res://${scene}`,
|
|
29546
|
+
refreshedClasses,
|
|
28988
29547
|
message: "The editor is playing it, so its debugger holds it: the debug_* tools can reach it, " + "editor_output reads its console through the debug adapter, and editor_run stop ends it."
|
|
28989
29548
|
});
|
|
28990
29549
|
}
|
|
@@ -29159,8 +29718,8 @@ class GodotServer {
|
|
|
29159
29718
|
return this.createErrorResponse(choice.problem);
|
|
29160
29719
|
}
|
|
29161
29720
|
const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
|
|
29162
|
-
const screenshotDir = expectsScreenshot ? mkdtempSync2(
|
|
29163
|
-
const screenshotPath = screenshotDir ?
|
|
29721
|
+
const screenshotDir = expectsScreenshot ? mkdtempSync2(join7(tmpdir4(), "gdharness-runtime-screenshot-")) : null;
|
|
29722
|
+
const screenshotPath = screenshotDir ? join7(screenshotDir, "capture.png") : null;
|
|
29164
29723
|
try {
|
|
29165
29724
|
const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs);
|
|
29166
29725
|
if (!reply.ok) {
|
|
@@ -29181,7 +29740,7 @@ class GodotServer {
|
|
|
29181
29740
|
return {
|
|
29182
29741
|
content: [
|
|
29183
29742
|
{ type: "text", text: `Screenshot captured: ${dimensions}` },
|
|
29184
|
-
{ type: "image", data:
|
|
29743
|
+
{ type: "image", data: readFileSync7(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
29185
29744
|
]
|
|
29186
29745
|
};
|
|
29187
29746
|
} finally {
|
|
@@ -29246,7 +29805,6 @@ async function runGodotServer() {
|
|
|
29246
29805
|
|
|
29247
29806
|
// src/server-entry.ts
|
|
29248
29807
|
await runGodotServer().catch((error) => {
|
|
29249
|
-
|
|
29250
|
-
console.error("Failed to run server:", errorMessage);
|
|
29808
|
+
console.error(defectReport("gdharness server startup", error));
|
|
29251
29809
|
process.exit(1);
|
|
29252
29810
|
});
|