gdharness 0.4.2 → 0.5.1
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 +34867 -26312
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +9 -1
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +11 -3
- package/build/index.js +716 -122
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -9802,11 +9802,332 @@ 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 as readFileSync2 } from "node:fs";
|
|
9830
|
+
|
|
9831
|
+
// src/update-check.ts
|
|
9832
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
9833
|
+
import { homedir, tmpdir } from "node:os";
|
|
9834
|
+
import { join } from "node:path";
|
|
9835
|
+
|
|
9836
|
+
// src/runner.ts
|
|
9837
|
+
function currentRunner() {
|
|
9838
|
+
const versions = process.versions;
|
|
9839
|
+
return versions["bun"] === undefined ? "npx" : "bunx";
|
|
9840
|
+
}
|
|
9841
|
+
function runLine(runner, version, rest = "") {
|
|
9842
|
+
const flag = runner === "npx" ? "-y " : "";
|
|
9843
|
+
return `${runner} ${flag}gdharness@${version}${rest === "" ? "" : ` ${rest}`}`;
|
|
9844
|
+
}
|
|
9845
|
+
|
|
9846
|
+
// src/update-check.ts
|
|
9847
|
+
var REGISTRY = "https://registry.npmjs.org/gdharness/latest";
|
|
9848
|
+
var RELEASES = "https://github.com/Aureliolo/gdharness/releases/tag";
|
|
9849
|
+
var CACHE_MS = 4 * 60 * 60 * 1000;
|
|
9850
|
+
var REQUEST_TIMEOUT_MS = 1e4;
|
|
9851
|
+
var MAX_BODY_BYTES = 1 << 20;
|
|
9852
|
+
var FIRST_RETRY_MS = 30000;
|
|
9853
|
+
var MAX_RETRY_MS = 60 * 60 * 1000;
|
|
9854
|
+
var VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
9855
|
+
function cacheDirectory(environment) {
|
|
9856
|
+
const set = (name) => {
|
|
9857
|
+
const value = environment[name];
|
|
9858
|
+
return value !== undefined && value !== "" ? value : null;
|
|
9859
|
+
};
|
|
9860
|
+
const home = set("HOME") ?? homedir();
|
|
9861
|
+
if (process.platform === "win32") {
|
|
9862
|
+
return join(set("LOCALAPPDATA") ?? home, "gdharness");
|
|
9863
|
+
}
|
|
9864
|
+
if (process.platform === "darwin") {
|
|
9865
|
+
return join(home, "Library", "Caches", "gdharness");
|
|
9866
|
+
}
|
|
9867
|
+
return join(set("XDG_CACHE_HOME") ?? join(home, ".cache"), "gdharness");
|
|
9868
|
+
}
|
|
9869
|
+
function cacheFile(environment = process.env) {
|
|
9870
|
+
try {
|
|
9871
|
+
const directory = cacheDirectory(environment);
|
|
9872
|
+
mkdirSync(directory, { recursive: true, mode: 448 });
|
|
9873
|
+
return join(directory, "update-check.json");
|
|
9874
|
+
} catch {
|
|
9875
|
+
return join(tmpdir(), "gdharness-update-check.json");
|
|
9876
|
+
}
|
|
9877
|
+
}
|
|
9878
|
+
function readCache(path) {
|
|
9879
|
+
try {
|
|
9880
|
+
if (!existsSync(path)) {
|
|
9881
|
+
return null;
|
|
9882
|
+
}
|
|
9883
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
9884
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
9885
|
+
return null;
|
|
9886
|
+
}
|
|
9887
|
+
const record = parsed;
|
|
9888
|
+
const checkedAt = record["checkedAt"];
|
|
9889
|
+
const latest = record["latest"];
|
|
9890
|
+
if (typeof checkedAt !== "number" || typeof latest !== "string" || !VERSION.test(latest)) {
|
|
9891
|
+
return null;
|
|
9892
|
+
}
|
|
9893
|
+
return { checkedAt, latest };
|
|
9894
|
+
} catch {
|
|
9895
|
+
return null;
|
|
9896
|
+
}
|
|
9897
|
+
}
|
|
9898
|
+
function writeCache(path, entry) {
|
|
9899
|
+
try {
|
|
9900
|
+
writeFileSync(path, JSON.stringify(entry), { encoding: "utf8", mode: 384 });
|
|
9901
|
+
} catch {}
|
|
9902
|
+
}
|
|
9903
|
+
function parts(version) {
|
|
9904
|
+
const withoutBuild = version.split("+")[0] ?? version;
|
|
9905
|
+
const dash = withoutBuild.indexOf("-");
|
|
9906
|
+
const numeric = dash === -1 ? withoutBuild : withoutBuild.slice(0, dash);
|
|
9907
|
+
return {
|
|
9908
|
+
numbers: numeric.split(".").map((piece) => Number.parseInt(piece, 10) || 0),
|
|
9909
|
+
prerelease: dash !== -1
|
|
9910
|
+
};
|
|
9911
|
+
}
|
|
9912
|
+
function isNewer(candidate, current) {
|
|
9913
|
+
const left = parts(candidate);
|
|
9914
|
+
const right = parts(current);
|
|
9915
|
+
for (let index = 0;index < 3; index += 1) {
|
|
9916
|
+
const a = left.numbers[index] ?? 0;
|
|
9917
|
+
const b = right.numbers[index] ?? 0;
|
|
9918
|
+
if (a !== b) {
|
|
9919
|
+
return a > b;
|
|
9920
|
+
}
|
|
9921
|
+
}
|
|
9922
|
+
return !left.prerelease && right.prerelease;
|
|
9923
|
+
}
|
|
9924
|
+
async function fetchLatest() {
|
|
9925
|
+
const response = await fetch(REGISTRY, {
|
|
9926
|
+
headers: { accept: "application/vnd.npm.install-v1+json, application/json" },
|
|
9927
|
+
redirect: "error",
|
|
9928
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
9929
|
+
});
|
|
9930
|
+
if (!response.ok || response.body === null) {
|
|
9931
|
+
return null;
|
|
9932
|
+
}
|
|
9933
|
+
const declared = Number.parseInt(response.headers.get("content-length") ?? "", 10);
|
|
9934
|
+
if (Number.isFinite(declared) && declared > MAX_BODY_BYTES) {
|
|
9935
|
+
return null;
|
|
9936
|
+
}
|
|
9937
|
+
const chunks = [];
|
|
9938
|
+
let size = 0;
|
|
9939
|
+
for await (const chunk of response.body) {
|
|
9940
|
+
size += chunk.byteLength;
|
|
9941
|
+
if (size > MAX_BODY_BYTES) {
|
|
9942
|
+
return null;
|
|
9943
|
+
}
|
|
9944
|
+
chunks.push(chunk);
|
|
9945
|
+
}
|
|
9946
|
+
const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
9947
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
9948
|
+
return null;
|
|
9949
|
+
}
|
|
9950
|
+
const version = parsed["version"];
|
|
9951
|
+
return typeof version === "string" && VERSION.test(version) ? version : null;
|
|
9952
|
+
}
|
|
9953
|
+
|
|
9954
|
+
class UpdateCheck {
|
|
9955
|
+
latest = null;
|
|
9956
|
+
checking = false;
|
|
9957
|
+
checkedAt = 0;
|
|
9958
|
+
retryAt = 0;
|
|
9959
|
+
backoffMs = FIRST_RETRY_MS;
|
|
9960
|
+
enabled;
|
|
9961
|
+
current;
|
|
9962
|
+
cachePath;
|
|
9963
|
+
constructor(current, environment = process.env) {
|
|
9964
|
+
this.current = current;
|
|
9965
|
+
this.enabled = (environment["GDHARNESS_NO_UPDATE_CHECK"] ?? "") === "";
|
|
9966
|
+
this.cachePath = cacheFile(environment);
|
|
9967
|
+
const cached = this.enabled ? readCache(this.cachePath) : null;
|
|
9968
|
+
if (cached !== null) {
|
|
9969
|
+
this.latest = cached.latest;
|
|
9970
|
+
this.checkedAt = cached.checkedAt;
|
|
9971
|
+
}
|
|
9972
|
+
}
|
|
9973
|
+
refresh(now = Date.now()) {
|
|
9974
|
+
if (!this.enabled || this.checking || now < this.retryAt || now - this.checkedAt < CACHE_MS) {
|
|
9975
|
+
return;
|
|
9976
|
+
}
|
|
9977
|
+
this.checking = true;
|
|
9978
|
+
fetchLatest().then((version) => {
|
|
9979
|
+
if (version === null) {
|
|
9980
|
+
this.scheduleRetry(now);
|
|
9981
|
+
return;
|
|
9982
|
+
}
|
|
9983
|
+
this.latest = version;
|
|
9984
|
+
this.checkedAt = Date.now();
|
|
9985
|
+
this.backoffMs = FIRST_RETRY_MS;
|
|
9986
|
+
this.retryAt = 0;
|
|
9987
|
+
writeCache(this.cachePath, { checkedAt: this.checkedAt, latest: version });
|
|
9988
|
+
}).catch(() => {
|
|
9989
|
+
this.scheduleRetry(now);
|
|
9990
|
+
}).finally(() => {
|
|
9991
|
+
this.checking = false;
|
|
9992
|
+
});
|
|
9993
|
+
}
|
|
9994
|
+
scheduleRetry(now) {
|
|
9995
|
+
this.retryAt = now + this.backoffMs;
|
|
9996
|
+
this.backoffMs = Math.min(this.backoffMs * 2, MAX_RETRY_MS);
|
|
9997
|
+
}
|
|
9998
|
+
notice() {
|
|
9999
|
+
const latest = this.latest;
|
|
10000
|
+
if (!this.enabled || latest === null || !isNewer(latest, this.current)) {
|
|
10001
|
+
return null;
|
|
10002
|
+
}
|
|
10003
|
+
return {
|
|
10004
|
+
current: this.current,
|
|
10005
|
+
latest,
|
|
10006
|
+
releaseNotes: `${RELEASES}/v${latest}`,
|
|
10007
|
+
upgrade: runLine(currentRunner(), latest, "upgrade")
|
|
10008
|
+
};
|
|
10009
|
+
}
|
|
10010
|
+
}
|
|
10011
|
+
|
|
10012
|
+
// src/server-version.ts
|
|
10013
|
+
var DEBUG_MODE = process.env["DEBUG"] === "true";
|
|
10014
|
+
var GODOT_DEBUG_MODE_DEFAULT = process.env["GODOT_DEBUG"] === "true" || DEBUG_MODE;
|
|
10015
|
+
var SERVER_VERSION = (() => {
|
|
10016
|
+
try {
|
|
10017
|
+
const pkg = JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf8"));
|
|
10018
|
+
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
10019
|
+
} catch {
|
|
10020
|
+
return "0.0.0";
|
|
10021
|
+
}
|
|
10022
|
+
})();
|
|
10023
|
+
var UNVERSIONED = "addon from before versions were reported";
|
|
10024
|
+
function addonMismatch(addonVersion, serverVersion) {
|
|
10025
|
+
if (addonVersion === serverVersion) {
|
|
10026
|
+
return;
|
|
10027
|
+
}
|
|
10028
|
+
const reported = addonVersion ?? "";
|
|
10029
|
+
const editor = reported === "" ? UNVERSIONED : reported;
|
|
10030
|
+
const both = `The editor is running the ${editor} addon while this server ships ${serverVersion}.`;
|
|
10031
|
+
if (reported !== "" && isNewer(reported, serverVersion)) {
|
|
10032
|
+
return `${both} This server is the older half: reconnect it in your harness so it spawns ${reported}.`;
|
|
10033
|
+
}
|
|
10034
|
+
return `${both} Restart it with editor_launch restart to pick the new one up.`;
|
|
10035
|
+
}
|
|
10036
|
+
|
|
10037
|
+
// src/issues.ts
|
|
10038
|
+
var NEW_ISSUE = "https://github.com/Aureliolo/gdharness/issues/new";
|
|
10039
|
+
var ENHANCEMENT_URL = `${NEW_ISSUE}?template=feature_request.md`;
|
|
10040
|
+
function runtime() {
|
|
10041
|
+
const versions = process2.versions;
|
|
10042
|
+
const bun = versions["bun"];
|
|
10043
|
+
return bun === undefined ? `node ${process2.versions.node}` : `bun ${bun}`;
|
|
10044
|
+
}
|
|
10045
|
+
function defectSignature(where, message) {
|
|
10046
|
+
const normalised = `${where}
|
|
10047
|
+
${message}`.toLowerCase().replaceAll("\\", "/").replaceAll(/(?:[a-z]:)?(?:\/+[\w.-]+){2,}/gu, "<path>").replaceAll(/\d+/gu, "0");
|
|
10048
|
+
return createHash("sha256").update(normalised).digest("hex").slice(0, 8);
|
|
10049
|
+
}
|
|
10050
|
+
function facts(where, message, godotVersion) {
|
|
10051
|
+
return [
|
|
10052
|
+
["Where", where],
|
|
10053
|
+
["Error", message],
|
|
10054
|
+
["Version", `gdharness ${SERVER_VERSION}`],
|
|
10055
|
+
["Runtime", `${runtime()}, ${process2.platform} ${process2.arch}`],
|
|
10056
|
+
["Godot", godotVersion ?? "not known at the point this failed"],
|
|
10057
|
+
["Signature", defectSignature(where, message)]
|
|
10058
|
+
];
|
|
10059
|
+
}
|
|
10060
|
+
function filledTemplate(where, message, godotVersion) {
|
|
10061
|
+
return [
|
|
10062
|
+
`**gdharness version**: ${SERVER_VERSION}`,
|
|
10063
|
+
`**Godot version**: ${godotVersion ?? ""}`,
|
|
10064
|
+
`**Bun version**: ${runtime()}`,
|
|
10065
|
+
`**OS**: ${process2.platform} ${process2.arch}`,
|
|
10066
|
+
"**MCP client**:",
|
|
10067
|
+
"",
|
|
10068
|
+
"## What you did",
|
|
10069
|
+
"",
|
|
10070
|
+
where,
|
|
10071
|
+
"",
|
|
10072
|
+
"## What you expected",
|
|
10073
|
+
"",
|
|
10074
|
+
"The call to answer, or to refuse with a reason.",
|
|
10075
|
+
"",
|
|
10076
|
+
"## What happened",
|
|
10077
|
+
"",
|
|
10078
|
+
"gdharness failed in a way it does not model.",
|
|
10079
|
+
"",
|
|
10080
|
+
"```text",
|
|
10081
|
+
message,
|
|
10082
|
+
"```",
|
|
10083
|
+
"",
|
|
10084
|
+
`Signature: \`${defectSignature(where, message)}\``,
|
|
10085
|
+
"",
|
|
10086
|
+
"If the real cause turned out to be the project, the environment or the call, say so here:",
|
|
10087
|
+
"reaching you as a defect rather than as a refusal naming what would have worked is then the",
|
|
10088
|
+
"thing to fix.",
|
|
10089
|
+
"",
|
|
10090
|
+
"## Anything that makes it reproducible",
|
|
10091
|
+
""
|
|
10092
|
+
].join(`
|
|
10093
|
+
`);
|
|
10094
|
+
}
|
|
10095
|
+
function defectReport(where, error, godotVersion) {
|
|
10096
|
+
const message = errorMessage(error);
|
|
10097
|
+
const title = `Defect ${defectSignature(where, message)}: ${where}`;
|
|
10098
|
+
const url = `${NEW_ISSUE}?template=bug_report.md&title=${encodeURIComponent(title)}` + `&body=${encodeURIComponent(filledTemplate(where, message, godotVersion))}`;
|
|
10099
|
+
const width = Math.max(...facts(where, message, godotVersion).map(([label]) => (label ?? "").length));
|
|
10100
|
+
return [
|
|
10101
|
+
"gdharness failed in a way it does not model. This is a defect in gdharness rather than",
|
|
10102
|
+
"anything about the call: the arguments did not cause it, and sending it again will not",
|
|
10103
|
+
"change it.",
|
|
10104
|
+
"",
|
|
10105
|
+
...facts(where, message, godotVersion).map(([label, value]) => ` ${(label ?? "").padEnd(width)} ${value ?? ""}`),
|
|
10106
|
+
"",
|
|
10107
|
+
"Do not open an issue on your own initiative. Tell the person whose machine this is what",
|
|
10108
|
+
"broke, and ask whether you may report it to gdharness on their behalf. What the report",
|
|
10109
|
+
"carries is the lines above and nothing else, and whether that goes into a public tracker",
|
|
10110
|
+
"is theirs to decide.",
|
|
10111
|
+
"",
|
|
10112
|
+
"If they say yes, this link carries the report already filled in:",
|
|
10113
|
+
url,
|
|
10114
|
+
"",
|
|
10115
|
+
"If they say yes and you have no way to open an issue yourself, give them that link and",
|
|
10116
|
+
"those lines, and stay with them while they file it.",
|
|
10117
|
+
"",
|
|
10118
|
+
"Worth reporting either way. If you can see that what actually went wrong was the project,",
|
|
10119
|
+
"the environment or the call, then this message is the defect: a failure gdharness knows",
|
|
10120
|
+
"about is meant to arrive as a refusal naming what would have worked, not as this. Say that",
|
|
10121
|
+
"in the report and it is the more useful of the two."
|
|
10122
|
+
].join(`
|
|
10123
|
+
`);
|
|
10124
|
+
}
|
|
10125
|
+
|
|
9805
10126
|
// src/server.ts
|
|
9806
10127
|
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
|
|
10128
|
+
import { existsSync as existsSync6, mkdtempSync as mkdtempSync2, readdirSync as readdirSync5, readFileSync as readFileSync7, realpathSync as realpathSync2, rmSync as rmSync2 } from "node:fs";
|
|
10129
|
+
import { tmpdir as tmpdir4 } from "node:os";
|
|
10130
|
+
import { basename, dirname as dirname2, join as join7, normalize as normalize2 } from "node:path";
|
|
9810
10131
|
import { setTimeout as delay2 } from "node:timers/promises";
|
|
9811
10132
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
9812
10133
|
import { promisify as promisify3 } from "node:util";
|
|
@@ -24269,7 +24590,7 @@ var EMPTY_COMPLETION_RESULT = {
|
|
|
24269
24590
|
};
|
|
24270
24591
|
|
|
24271
24592
|
// node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
|
|
24272
|
-
import
|
|
24593
|
+
import process3 from "node:process";
|
|
24273
24594
|
|
|
24274
24595
|
// node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.js
|
|
24275
24596
|
var STDIO_DEFAULT_MAX_BUFFER_SIZE = 10 * 1024 * 1024;
|
|
@@ -24313,7 +24634,7 @@ function serializeMessage(message) {
|
|
|
24313
24634
|
|
|
24314
24635
|
// node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
|
|
24315
24636
|
class StdioServerTransport {
|
|
24316
|
-
constructor(_stdin =
|
|
24637
|
+
constructor(_stdin = process3.stdin, _stdout = process3.stdout, options) {
|
|
24317
24638
|
this._stdin = _stdin;
|
|
24318
24639
|
this._stdout = _stdout;
|
|
24319
24640
|
this._started = false;
|
|
@@ -24374,6 +24695,50 @@ class StdioServerTransport {
|
|
|
24374
24695
|
}
|
|
24375
24696
|
}
|
|
24376
24697
|
|
|
24698
|
+
// src/class-cache.ts
|
|
24699
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3 } from "node:fs";
|
|
24700
|
+
import { join as join2 } from "node:path";
|
|
24701
|
+
function declaredClasses(projectPath) {
|
|
24702
|
+
const declared = new Map;
|
|
24703
|
+
const visit = (directory, prefix) => {
|
|
24704
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
24705
|
+
if (entry.name.startsWith(".")) {
|
|
24706
|
+
continue;
|
|
24707
|
+
}
|
|
24708
|
+
const path = join2(directory, entry.name);
|
|
24709
|
+
if (entry.isDirectory()) {
|
|
24710
|
+
visit(path, `${prefix}${entry.name}/`);
|
|
24711
|
+
} else if (entry.isFile() && entry.name.endsWith(".gd")) {
|
|
24712
|
+
const found = /^class_name\s+([A-Za-z_][A-Za-z0-9_]*)/m.exec(readFileSync3(path, "utf8"));
|
|
24713
|
+
if (found?.[1]) {
|
|
24714
|
+
declared.set(found[1], `res://${prefix}${entry.name}`);
|
|
24715
|
+
}
|
|
24716
|
+
}
|
|
24717
|
+
}
|
|
24718
|
+
};
|
|
24719
|
+
visit(projectPath, "");
|
|
24720
|
+
return declared;
|
|
24721
|
+
}
|
|
24722
|
+
function cachedClasses(projectPath) {
|
|
24723
|
+
const cache = join2(projectPath, ".godot", "global_script_class_cache.cfg");
|
|
24724
|
+
if (!existsSync2(cache)) {
|
|
24725
|
+
return null;
|
|
24726
|
+
}
|
|
24727
|
+
const listed = new Map;
|
|
24728
|
+
const text = readFileSync3(cache, "utf8");
|
|
24729
|
+
for (const entry of text.matchAll(/"class":\s*&"([^"]+)"[\s\S]*?"path":\s*"([^"]+)"/g)) {
|
|
24730
|
+
listed.set(entry[1] ?? "", entry[2] ?? "");
|
|
24731
|
+
}
|
|
24732
|
+
return listed;
|
|
24733
|
+
}
|
|
24734
|
+
function staleAgainst(cached, projectPath) {
|
|
24735
|
+
return [...declaredClasses(projectPath)].filter(([name, path]) => cached.get(name) !== path).map(([name]) => name);
|
|
24736
|
+
}
|
|
24737
|
+
function staleClassNames(projectPath) {
|
|
24738
|
+
const cached = cachedClasses(projectPath);
|
|
24739
|
+
return cached === null ? [...declaredClasses(projectPath).keys()] : staleAgainst(cached, projectPath);
|
|
24740
|
+
}
|
|
24741
|
+
|
|
24377
24742
|
// src/dap_client.ts
|
|
24378
24743
|
import { createConnection } from "node:net";
|
|
24379
24744
|
import { setTimeout as delay } from "node:timers/promises";
|
|
@@ -24464,7 +24829,7 @@ function portFromEnv(variable, fallback) {
|
|
|
24464
24829
|
}
|
|
24465
24830
|
const parsed = Number.parseInt(raw, 10);
|
|
24466
24831
|
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535 || String(parsed) !== raw) {
|
|
24467
|
-
throw new
|
|
24832
|
+
throw new Refusal(`${variable} is "${raw}", not a port between 1 and 65535.`);
|
|
24468
24833
|
}
|
|
24469
24834
|
return parsed;
|
|
24470
24835
|
}
|
|
@@ -24488,6 +24853,7 @@ class GodotDAPClient {
|
|
|
24488
24853
|
initialized = false;
|
|
24489
24854
|
attached = false;
|
|
24490
24855
|
lastThreadId = 1;
|
|
24856
|
+
stopped = false;
|
|
24491
24857
|
breakpoints = new Map;
|
|
24492
24858
|
constructor(port = portFromEnv("GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT), host = "127.0.0.1") {
|
|
24493
24859
|
this.port = port;
|
|
@@ -24536,6 +24902,7 @@ class GodotDAPClient {
|
|
|
24536
24902
|
this.connected = false;
|
|
24537
24903
|
this.initialized = false;
|
|
24538
24904
|
this.attached = false;
|
|
24905
|
+
this.stopped = false;
|
|
24539
24906
|
this.socket = null;
|
|
24540
24907
|
this.failPendingRequests(new Error("DAP connection closed"));
|
|
24541
24908
|
});
|
|
@@ -24555,6 +24922,7 @@ class GodotDAPClient {
|
|
|
24555
24922
|
this.connected = false;
|
|
24556
24923
|
this.initialized = false;
|
|
24557
24924
|
this.attached = false;
|
|
24925
|
+
this.stopped = false;
|
|
24558
24926
|
return;
|
|
24559
24927
|
}
|
|
24560
24928
|
if (this.connected) {
|
|
@@ -24583,6 +24951,7 @@ class GodotDAPClient {
|
|
|
24583
24951
|
this.connected = false;
|
|
24584
24952
|
this.initialized = false;
|
|
24585
24953
|
this.attached = false;
|
|
24954
|
+
this.stopped = false;
|
|
24586
24955
|
}
|
|
24587
24956
|
async ensureConnected() {
|
|
24588
24957
|
if (!this.connected) {
|
|
@@ -24592,7 +24961,7 @@ class GodotDAPClient {
|
|
|
24592
24961
|
async sendRequest(command, args, timeoutMs = DAP_REQUEST_TIMEOUT_MS) {
|
|
24593
24962
|
await this.ensureConnected();
|
|
24594
24963
|
if (!this.socket) {
|
|
24595
|
-
throw new
|
|
24964
|
+
throw new Refusal("DAP socket is not available");
|
|
24596
24965
|
}
|
|
24597
24966
|
const requestSeq = this.seq++;
|
|
24598
24967
|
const request = {
|
|
@@ -24659,10 +25028,12 @@ class GodotDAPClient {
|
|
|
24659
25028
|
if (typeof threadId === "number") {
|
|
24660
25029
|
this.lastThreadId = threadId;
|
|
24661
25030
|
}
|
|
25031
|
+
this.stopped = true;
|
|
24662
25032
|
return;
|
|
24663
25033
|
}
|
|
24664
25034
|
if (eventName === "terminated" || eventName === "exited") {
|
|
24665
25035
|
this.attached = false;
|
|
25036
|
+
this.stopped = false;
|
|
24666
25037
|
}
|
|
24667
25038
|
}
|
|
24668
25039
|
async initialize() {
|
|
@@ -24736,6 +25107,7 @@ class GodotDAPClient {
|
|
|
24736
25107
|
await this.attach();
|
|
24737
25108
|
const resolvedThreadId = await this.resolveThreadId(threadId);
|
|
24738
25109
|
await this.sendRequest("continue", { threadId: resolvedThreadId });
|
|
25110
|
+
this.stopped = false;
|
|
24739
25111
|
}
|
|
24740
25112
|
async stepOver(threadId) {
|
|
24741
25113
|
await this.attach();
|
|
@@ -24805,6 +25177,9 @@ class GodotDAPClient {
|
|
|
24805
25177
|
isConnected() {
|
|
24806
25178
|
return this.connected;
|
|
24807
25179
|
}
|
|
25180
|
+
isStopped() {
|
|
25181
|
+
return this.stopped;
|
|
25182
|
+
}
|
|
24808
25183
|
async resolveThreadId(threadId) {
|
|
24809
25184
|
if (typeof threadId === "number" && threadId > 0) {
|
|
24810
25185
|
this.lastThreadId = threadId;
|
|
@@ -24832,6 +25207,7 @@ class GodotDAPClient {
|
|
|
24832
25207
|
this.connected = false;
|
|
24833
25208
|
this.initialized = false;
|
|
24834
25209
|
this.attached = false;
|
|
25210
|
+
this.stopped = false;
|
|
24835
25211
|
this.reader = new FrameReader;
|
|
24836
25212
|
socket?.destroy();
|
|
24837
25213
|
this.failPendingRequests(new Error(`Godot DAP ${detail}. The connection was dropped.`));
|
|
@@ -24856,7 +25232,7 @@ async function handleDAPTool(client, toolName, args) {
|
|
|
24856
25232
|
}
|
|
24857
25233
|
case "dap_set_breakpoint": {
|
|
24858
25234
|
if (typeof safeArgs.scriptPath !== "string" || typeof safeArgs.line !== "number") {
|
|
24859
|
-
throw new
|
|
25235
|
+
throw new Refusal("dap_set_breakpoint requires { scriptPath: string, line: number }");
|
|
24860
25236
|
}
|
|
24861
25237
|
const result = await client.setBreakpoint(safeArgs.scriptPath, safeArgs.line);
|
|
24862
25238
|
return {
|
|
@@ -24865,7 +25241,7 @@ async function handleDAPTool(client, toolName, args) {
|
|
|
24865
25241
|
}
|
|
24866
25242
|
case "dap_remove_breakpoint": {
|
|
24867
25243
|
if (typeof safeArgs.scriptPath !== "string" || typeof safeArgs.line !== "number") {
|
|
24868
|
-
throw new
|
|
25244
|
+
throw new Refusal("dap_remove_breakpoint requires { scriptPath: string, line: number }");
|
|
24869
25245
|
}
|
|
24870
25246
|
const result = await client.removeBreakpoint(safeArgs.scriptPath, safeArgs.line);
|
|
24871
25247
|
return {
|
|
@@ -24930,23 +25306,6 @@ function dictionary(entries) {
|
|
|
24930
25306
|
return Object.assign(emptyRecord(), entries);
|
|
24931
25307
|
}
|
|
24932
25308
|
|
|
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
25309
|
// src/game-log.ts
|
|
24951
25310
|
import { StringDecoder } from "node:string_decoder";
|
|
24952
25311
|
var HEADLINE = /^(USER )?(SCRIPT ERROR|ERROR|WARNING):\s?(.*)$/;
|
|
@@ -25350,7 +25709,7 @@ class GodotBridge extends EventEmitter {
|
|
|
25350
25709
|
}
|
|
25351
25710
|
sendMessage(message) {
|
|
25352
25711
|
if (this.socket?.readyState !== import_websocket.default.OPEN) {
|
|
25353
|
-
throw new
|
|
25712
|
+
throw new Refusal("Godot is not connected");
|
|
25354
25713
|
}
|
|
25355
25714
|
this.socket.send(JSON.stringify(message));
|
|
25356
25715
|
}
|
|
@@ -25458,28 +25817,28 @@ function getDefaultBridge() {
|
|
|
25458
25817
|
|
|
25459
25818
|
// src/godot-path.ts
|
|
25460
25819
|
import { execFile } from "node:child_process";
|
|
25461
|
-
import { existsSync as
|
|
25820
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
25462
25821
|
import { normalize } from "node:path";
|
|
25463
25822
|
import { promisify } from "node:util";
|
|
25464
25823
|
|
|
25465
25824
|
// src/detection.ts
|
|
25466
|
-
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
25467
|
-
import { homedir } from "node:os";
|
|
25468
|
-
import { join } from "node:path";
|
|
25825
|
+
import { existsSync as existsSync3, readdirSync as readdirSync2, statSync } from "node:fs";
|
|
25826
|
+
import { homedir as homedir2 } from "node:os";
|
|
25827
|
+
import { join as join3 } from "node:path";
|
|
25469
25828
|
function resolveHomeDirectory() {
|
|
25470
25829
|
try {
|
|
25471
|
-
return
|
|
25830
|
+
return homedir2();
|
|
25472
25831
|
} catch {
|
|
25473
25832
|
return "";
|
|
25474
25833
|
}
|
|
25475
25834
|
}
|
|
25476
25835
|
function scanDirectoryForGodotBinaries(directory, platform) {
|
|
25477
|
-
if (!directory || !
|
|
25836
|
+
if (!directory || !existsSync3(directory)) {
|
|
25478
25837
|
return [];
|
|
25479
25838
|
}
|
|
25480
25839
|
let entries;
|
|
25481
25840
|
try {
|
|
25482
|
-
entries =
|
|
25841
|
+
entries = readdirSync2(directory);
|
|
25483
25842
|
} catch {
|
|
25484
25843
|
return [];
|
|
25485
25844
|
}
|
|
@@ -25489,7 +25848,7 @@ function scanDirectoryForGodotBinaries(directory, platform) {
|
|
|
25489
25848
|
if (!pattern.test(name)) {
|
|
25490
25849
|
continue;
|
|
25491
25850
|
}
|
|
25492
|
-
const fullPath =
|
|
25851
|
+
const fullPath = join3(directory, name);
|
|
25493
25852
|
try {
|
|
25494
25853
|
const stat = statSync(fullPath);
|
|
25495
25854
|
if (stat.isFile()) {
|
|
@@ -25498,7 +25857,7 @@ function scanDirectoryForGodotBinaries(directory, platform) {
|
|
|
25498
25857
|
} catch {}
|
|
25499
25858
|
}
|
|
25500
25859
|
matches.sort((a, b) => b.mtime - a.mtime);
|
|
25501
|
-
return matches.map((m) =>
|
|
25860
|
+
return matches.map((m) => join3(directory, m.name));
|
|
25502
25861
|
}
|
|
25503
25862
|
function conventionalPaths(platform, home) {
|
|
25504
25863
|
const paths = ["godot"];
|
|
@@ -25613,7 +25972,7 @@ class GodotLocator {
|
|
|
25613
25972
|
return known;
|
|
25614
25973
|
}
|
|
25615
25974
|
let ok = false;
|
|
25616
|
-
if (path === "godot" ||
|
|
25975
|
+
if (path === "godot" || existsSync4(path)) {
|
|
25617
25976
|
try {
|
|
25618
25977
|
await run(path, ["--version"]);
|
|
25619
25978
|
ok = true;
|
|
@@ -25628,9 +25987,9 @@ class GodotLocator {
|
|
|
25628
25987
|
|
|
25629
25988
|
// src/headless.ts
|
|
25630
25989
|
import { execFile as execFile2 } from "node:child_process";
|
|
25631
|
-
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
25632
|
-
import { tmpdir } from "node:os";
|
|
25633
|
-
import { join as
|
|
25990
|
+
import { mkdtempSync, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
25991
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
25992
|
+
import { join as join4 } from "node:path";
|
|
25634
25993
|
import { promisify as promisify2 } from "node:util";
|
|
25635
25994
|
var run2 = promisify2(execFile2);
|
|
25636
25995
|
function snakeCased(params) {
|
|
@@ -25671,9 +26030,9 @@ function reason(stdout, stderr) {
|
|
|
25671
26030
|
return stdout.trim().split(/\r?\n/).at(-1) ?? "no output at all";
|
|
25672
26031
|
}
|
|
25673
26032
|
async function runOperation(engine, operation, params, projectPath) {
|
|
25674
|
-
const paramsDir = mkdtempSync(
|
|
25675
|
-
const paramsFile =
|
|
25676
|
-
|
|
26033
|
+
const paramsDir = mkdtempSync(join4(tmpdir2(), "gdharness-params-"));
|
|
26034
|
+
const paramsFile = join4(paramsDir, `${operation}.json`);
|
|
26035
|
+
writeFileSync2(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
|
|
25677
26036
|
const args = [
|
|
25678
26037
|
"--headless",
|
|
25679
26038
|
"--path",
|
|
@@ -25716,6 +26075,105 @@ async function runOperation(engine, operation, params, projectPath) {
|
|
|
25716
26075
|
return { ok: true, payload, messages: problems(stderr) };
|
|
25717
26076
|
}
|
|
25718
26077
|
|
|
26078
|
+
// src/issues.ts
|
|
26079
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
26080
|
+
import process4 from "node:process";
|
|
26081
|
+
var NEW_ISSUE2 = "https://github.com/Aureliolo/gdharness/issues/new";
|
|
26082
|
+
var ENHANCEMENT_URL2 = `${NEW_ISSUE2}?template=feature_request.md`;
|
|
26083
|
+
function feedbackNotice() {
|
|
26084
|
+
return JSON.stringify({
|
|
26085
|
+
gdharness_feedback: {
|
|
26086
|
+
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.",
|
|
26087
|
+
enhancement_url: ENHANCEMENT_URL2
|
|
26088
|
+
}
|
|
26089
|
+
}, null, 2);
|
|
26090
|
+
}
|
|
26091
|
+
function runtime2() {
|
|
26092
|
+
const versions = process4.versions;
|
|
26093
|
+
const bun = versions["bun"];
|
|
26094
|
+
return bun === undefined ? `node ${process4.versions.node}` : `bun ${bun}`;
|
|
26095
|
+
}
|
|
26096
|
+
function defectSignature2(where, message) {
|
|
26097
|
+
const normalised = `${where}
|
|
26098
|
+
${message}`.toLowerCase().replaceAll("\\", "/").replaceAll(/(?:[a-z]:)?(?:\/+[\w.-]+){2,}/gu, "<path>").replaceAll(/\d+/gu, "0");
|
|
26099
|
+
return createHash2("sha256").update(normalised).digest("hex").slice(0, 8);
|
|
26100
|
+
}
|
|
26101
|
+
function facts2(where, message, godotVersion) {
|
|
26102
|
+
return [
|
|
26103
|
+
["Where", where],
|
|
26104
|
+
["Error", message],
|
|
26105
|
+
["Version", `gdharness ${SERVER_VERSION}`],
|
|
26106
|
+
["Runtime", `${runtime2()}, ${process4.platform} ${process4.arch}`],
|
|
26107
|
+
["Godot", godotVersion ?? "not known at the point this failed"],
|
|
26108
|
+
["Signature", defectSignature2(where, message)]
|
|
26109
|
+
];
|
|
26110
|
+
}
|
|
26111
|
+
function filledTemplate2(where, message, godotVersion) {
|
|
26112
|
+
return [
|
|
26113
|
+
`**gdharness version**: ${SERVER_VERSION}`,
|
|
26114
|
+
`**Godot version**: ${godotVersion ?? ""}`,
|
|
26115
|
+
`**Bun version**: ${runtime2()}`,
|
|
26116
|
+
`**OS**: ${process4.platform} ${process4.arch}`,
|
|
26117
|
+
"**MCP client**:",
|
|
26118
|
+
"",
|
|
26119
|
+
"## What you did",
|
|
26120
|
+
"",
|
|
26121
|
+
where,
|
|
26122
|
+
"",
|
|
26123
|
+
"## What you expected",
|
|
26124
|
+
"",
|
|
26125
|
+
"The call to answer, or to refuse with a reason.",
|
|
26126
|
+
"",
|
|
26127
|
+
"## What happened",
|
|
26128
|
+
"",
|
|
26129
|
+
"gdharness failed in a way it does not model.",
|
|
26130
|
+
"",
|
|
26131
|
+
"```text",
|
|
26132
|
+
message,
|
|
26133
|
+
"```",
|
|
26134
|
+
"",
|
|
26135
|
+
`Signature: \`${defectSignature2(where, message)}\``,
|
|
26136
|
+
"",
|
|
26137
|
+
"If the real cause turned out to be the project, the environment or the call, say so here:",
|
|
26138
|
+
"reaching you as a defect rather than as a refusal naming what would have worked is then the",
|
|
26139
|
+
"thing to fix.",
|
|
26140
|
+
"",
|
|
26141
|
+
"## Anything that makes it reproducible",
|
|
26142
|
+
""
|
|
26143
|
+
].join(`
|
|
26144
|
+
`);
|
|
26145
|
+
}
|
|
26146
|
+
function defectReport2(where, error, godotVersion) {
|
|
26147
|
+
const message = errorMessage(error);
|
|
26148
|
+
const title = `Defect ${defectSignature2(where, message)}: ${where}`;
|
|
26149
|
+
const url = `${NEW_ISSUE2}?template=bug_report.md&title=${encodeURIComponent(title)}` + `&body=${encodeURIComponent(filledTemplate2(where, message, godotVersion))}`;
|
|
26150
|
+
const width = Math.max(...facts2(where, message, godotVersion).map(([label]) => (label ?? "").length));
|
|
26151
|
+
return [
|
|
26152
|
+
"gdharness failed in a way it does not model. This is a defect in gdharness rather than",
|
|
26153
|
+
"anything about the call: the arguments did not cause it, and sending it again will not",
|
|
26154
|
+
"change it.",
|
|
26155
|
+
"",
|
|
26156
|
+
...facts2(where, message, godotVersion).map(([label, value]) => ` ${(label ?? "").padEnd(width)} ${value ?? ""}`),
|
|
26157
|
+
"",
|
|
26158
|
+
"Do not open an issue on your own initiative. Tell the person whose machine this is what",
|
|
26159
|
+
"broke, and ask whether you may report it to gdharness on their behalf. What the report",
|
|
26160
|
+
"carries is the lines above and nothing else, and whether that goes into a public tracker",
|
|
26161
|
+
"is theirs to decide.",
|
|
26162
|
+
"",
|
|
26163
|
+
"If they say yes, this link carries the report already filled in:",
|
|
26164
|
+
url,
|
|
26165
|
+
"",
|
|
26166
|
+
"If they say yes and you have no way to open an issue yourself, give them that link and",
|
|
26167
|
+
"those lines, and stay with them while they file it.",
|
|
26168
|
+
"",
|
|
26169
|
+
"Worth reporting either way. If you can see that what actually went wrong was the project,",
|
|
26170
|
+
"the environment or the call, then this message is the defect: a failure gdharness knows",
|
|
26171
|
+
"about is meant to arrive as a refusal naming what would have worked, not as this. Say that",
|
|
26172
|
+
"in the report and it is the more useful of the two."
|
|
26173
|
+
].join(`
|
|
26174
|
+
`);
|
|
26175
|
+
}
|
|
26176
|
+
|
|
25719
26177
|
// src/junit.ts
|
|
25720
26178
|
class MalformedReportError extends Error {
|
|
25721
26179
|
constructor(message) {
|
|
@@ -26083,7 +26541,7 @@ class GodotLSPClient {
|
|
|
26083
26541
|
async sendRequest(method, params) {
|
|
26084
26542
|
await this.ensureConnected();
|
|
26085
26543
|
if (!this.socket) {
|
|
26086
|
-
throw new
|
|
26544
|
+
throw new Refusal("Not connected to Godot LSP");
|
|
26087
26545
|
}
|
|
26088
26546
|
this.requestId += 1;
|
|
26089
26547
|
const id = this.requestId;
|
|
@@ -26122,7 +26580,7 @@ class GodotLSPClient {
|
|
|
26122
26580
|
}
|
|
26123
26581
|
sendNotification(method, params) {
|
|
26124
26582
|
if (!this.connected || !this.socket) {
|
|
26125
|
-
throw new
|
|
26583
|
+
throw new Refusal("Not connected to Godot LSP");
|
|
26126
26584
|
}
|
|
26127
26585
|
const payload = {
|
|
26128
26586
|
jsonrpc: "2.0",
|
|
@@ -26372,36 +26830,36 @@ async function resolveLSPPaths(projectPathValue, scriptPathValue) {
|
|
|
26372
26830
|
try {
|
|
26373
26831
|
projectPath = await realpath(requestedProjectPath);
|
|
26374
26832
|
} catch {
|
|
26375
|
-
throw new
|
|
26833
|
+
throw new Refusal(`Project path does not exist: ${requestedProjectPath}`);
|
|
26376
26834
|
}
|
|
26377
26835
|
const contained = resolveWithinProject(projectPath, scriptPathValue);
|
|
26378
26836
|
if (!contained.ok) {
|
|
26379
|
-
throw new
|
|
26837
|
+
throw new Refusal(contained.reason);
|
|
26380
26838
|
}
|
|
26381
26839
|
let scriptPath;
|
|
26382
26840
|
try {
|
|
26383
26841
|
scriptPath = await realpath(contained.absolutePath);
|
|
26384
26842
|
} catch {
|
|
26385
|
-
throw new
|
|
26843
|
+
throw new Refusal(`Script file does not exist: ${contained.absolutePath}`);
|
|
26386
26844
|
}
|
|
26387
26845
|
if (!isWithinRoot(projectPath, scriptPath)) {
|
|
26388
|
-
throw new
|
|
26846
|
+
throw new Refusal("scriptPath resolves outside the project root boundary.");
|
|
26389
26847
|
}
|
|
26390
26848
|
return { projectPath, scriptPath };
|
|
26391
26849
|
}
|
|
26392
26850
|
async function handleLSPTool(client, toolName, args) {
|
|
26393
26851
|
try {
|
|
26394
26852
|
if (!args || typeof args !== "object") {
|
|
26395
|
-
throw new
|
|
26853
|
+
throw new Refusal("Tool arguments must be an object.");
|
|
26396
26854
|
}
|
|
26397
26855
|
const parsedArgs = args;
|
|
26398
26856
|
const projectPathValue = parsedArgs["projectPath"];
|
|
26399
26857
|
const scriptPathValue = parsedArgs["scriptPath"];
|
|
26400
26858
|
if (typeof projectPathValue !== "string" || projectPathValue.length === 0) {
|
|
26401
|
-
throw new
|
|
26859
|
+
throw new Refusal("Missing required argument: projectPath");
|
|
26402
26860
|
}
|
|
26403
26861
|
if (typeof scriptPathValue !== "string" || scriptPathValue.length === 0) {
|
|
26404
|
-
throw new
|
|
26862
|
+
throw new Refusal("Missing required argument: scriptPath");
|
|
26405
26863
|
}
|
|
26406
26864
|
const { projectPath, scriptPath } = await resolveLSPPaths(projectPathValue, scriptPathValue);
|
|
26407
26865
|
const content = await readFile(scriptPath, "utf8");
|
|
@@ -26415,7 +26873,7 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
26415
26873
|
const line = Number(parsedArgs["line"]);
|
|
26416
26874
|
const character = Number(parsedArgs["character"]);
|
|
26417
26875
|
if (!Number.isFinite(line) || !Number.isFinite(character)) {
|
|
26418
|
-
throw new
|
|
26876
|
+
throw new Refusal("Arguments line and character must be numbers.");
|
|
26419
26877
|
}
|
|
26420
26878
|
const completions = await client.getCompletions(scriptPath, content, line, character);
|
|
26421
26879
|
return asToolResponse({ completions });
|
|
@@ -26424,7 +26882,7 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
26424
26882
|
const line = Number(parsedArgs["line"]);
|
|
26425
26883
|
const character = Number(parsedArgs["character"]);
|
|
26426
26884
|
if (!Number.isFinite(line) || !Number.isFinite(character)) {
|
|
26427
|
-
throw new
|
|
26885
|
+
throw new Refusal("Arguments line and character must be numbers.");
|
|
26428
26886
|
}
|
|
26429
26887
|
const hover = await client.getHover(scriptPath, content, line, character);
|
|
26430
26888
|
return asToolResponse({ hover });
|
|
@@ -26444,19 +26902,19 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
26444
26902
|
}
|
|
26445
26903
|
|
|
26446
26904
|
// src/project-scan.ts
|
|
26447
|
-
import { readdirSync as
|
|
26448
|
-
import { join as
|
|
26905
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync4 } from "node:fs";
|
|
26906
|
+
import { join as join5 } from "node:path";
|
|
26449
26907
|
var SKIPPED = new Set([".git", ".godot", ".import", "node_modules"]);
|
|
26450
26908
|
var ASSET_EXTENSIONS = new Set(["png", "jpg", "jpeg", "webp", "svg", "ttf", "otf", "wav", "mp3", "ogg"]);
|
|
26451
26909
|
function projectStructure(projectPath) {
|
|
26452
26910
|
const structure = { scenes: 0, scripts: 0, assets: 0, other: 0 };
|
|
26453
26911
|
const visit = (directory) => {
|
|
26454
|
-
for (const entry of
|
|
26912
|
+
for (const entry of readdirSync3(directory, { withFileTypes: true })) {
|
|
26455
26913
|
if (entry.name.startsWith(".")) {
|
|
26456
26914
|
continue;
|
|
26457
26915
|
}
|
|
26458
26916
|
if (entry.isDirectory()) {
|
|
26459
|
-
visit(
|
|
26917
|
+
visit(join5(directory, entry.name));
|
|
26460
26918
|
} else if (entry.isFile()) {
|
|
26461
26919
|
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
26462
26920
|
if (extension === "tscn") {
|
|
@@ -26491,14 +26949,14 @@ function searchProject(projectPath, options) {
|
|
|
26491
26949
|
return false;
|
|
26492
26950
|
};
|
|
26493
26951
|
const visit = (directory) => {
|
|
26494
|
-
for (const entry of
|
|
26952
|
+
for (const entry of readdirSync3(directory, { withFileTypes: true })) {
|
|
26495
26953
|
if (full()) {
|
|
26496
26954
|
return;
|
|
26497
26955
|
}
|
|
26498
26956
|
if (SKIPPED.has(entry.name)) {
|
|
26499
26957
|
continue;
|
|
26500
26958
|
}
|
|
26501
|
-
const entryPath =
|
|
26959
|
+
const entryPath = join5(directory, entry.name);
|
|
26502
26960
|
if (entry.isDirectory()) {
|
|
26503
26961
|
visit(entryPath);
|
|
26504
26962
|
continue;
|
|
@@ -26509,7 +26967,7 @@ function searchProject(projectPath, options) {
|
|
|
26509
26967
|
}
|
|
26510
26968
|
result.summary.files_searched += 1;
|
|
26511
26969
|
const matches = [];
|
|
26512
|
-
for (const [index, line] of
|
|
26970
|
+
for (const [index, line] of readFileSync4(entryPath, "utf8").split(`
|
|
26513
26971
|
`).entries()) {
|
|
26514
26972
|
if (full()) {
|
|
26515
26973
|
break;
|
|
@@ -26532,7 +26990,7 @@ function searchProject(projectPath, options) {
|
|
|
26532
26990
|
}
|
|
26533
26991
|
|
|
26534
26992
|
// src/resources.ts
|
|
26535
|
-
import { readFileSync as
|
|
26993
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
26536
26994
|
import { extname, resolve as resolve3 } from "node:path";
|
|
26537
26995
|
var STATIC_RESOURCES = [
|
|
26538
26996
|
{
|
|
@@ -26562,24 +27020,25 @@ var RESOURCE_TEMPLATES = [
|
|
|
26562
27020
|
mimeType: "text/plain"
|
|
26563
27021
|
}
|
|
26564
27022
|
];
|
|
27023
|
+
var RESOURCE_COUNT = STATIC_RESOURCES.length + RESOURCE_TEMPLATES.length;
|
|
26565
27024
|
function ensureProjectPath(getProjectPath) {
|
|
26566
27025
|
const projectPath = getProjectPath();
|
|
26567
27026
|
if (!projectPath) {
|
|
26568
|
-
throw new
|
|
27027
|
+
throw new Refusal("Project path is not set. Set a Godot project path first.");
|
|
26569
27028
|
}
|
|
26570
27029
|
return resolve3(projectPath);
|
|
26571
27030
|
}
|
|
26572
27031
|
function uriPathToProjectPath(inputPath) {
|
|
26573
27032
|
const normalized = inputPath.replace(/\\/g, "/").trim();
|
|
26574
27033
|
if (normalized.replace(/\//g, "") === "") {
|
|
26575
|
-
throw new
|
|
27034
|
+
throw new Refusal("Resource path is empty.");
|
|
26576
27035
|
}
|
|
26577
27036
|
return normalized.replace(/^\/+/, "");
|
|
26578
27037
|
}
|
|
26579
27038
|
function resolveProjectFile(projectPath, resourcePath) {
|
|
26580
27039
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
26581
27040
|
if (!contained.ok) {
|
|
26582
|
-
throw new
|
|
27041
|
+
throw new Refusal(contained.reason);
|
|
26583
27042
|
}
|
|
26584
27043
|
return contained.absolutePath;
|
|
26585
27044
|
}
|
|
@@ -26591,14 +27050,14 @@ function parseGodotUri(uri) {
|
|
|
26591
27050
|
try {
|
|
26592
27051
|
parsed = new URL(uri);
|
|
26593
27052
|
} catch {
|
|
26594
|
-
throw new
|
|
27053
|
+
throw new Refusal(`Invalid URI: ${uri}`);
|
|
26595
27054
|
}
|
|
26596
27055
|
if (parsed.protocol !== "godot:") {
|
|
26597
|
-
throw new
|
|
27056
|
+
throw new Refusal(`Unsupported URI scheme: ${parsed.protocol}`);
|
|
26598
27057
|
}
|
|
26599
27058
|
const host = parsed.hostname;
|
|
26600
27059
|
if (host !== "scene" && host !== "script" && host !== "resource") {
|
|
26601
|
-
throw new
|
|
27060
|
+
throw new Refusal(`Unsupported Godot resource type: ${host}`);
|
|
26602
27061
|
}
|
|
26603
27062
|
const resourcePath = uriPathToProjectPath(decodeURIComponent(parsed.pathname));
|
|
26604
27063
|
return { kind: host, resourcePath };
|
|
@@ -26606,13 +27065,13 @@ function parseGodotUri(uri) {
|
|
|
26606
27065
|
function ensureAllowedExtension(kind, filePath) {
|
|
26607
27066
|
const extension = extname(filePath).toLowerCase();
|
|
26608
27067
|
if (kind === "scene" && extension !== ".tscn") {
|
|
26609
|
-
throw new
|
|
27068
|
+
throw new Refusal("Scene resources must use .tscn extension.");
|
|
26610
27069
|
}
|
|
26611
27070
|
if (kind === "script" && extension !== ".gd") {
|
|
26612
|
-
throw new
|
|
27071
|
+
throw new Refusal("Script resources must use .gd extension.");
|
|
26613
27072
|
}
|
|
26614
27073
|
if (kind === "resource" && ![".tres", ".tscn", ".gd"].includes(extension)) {
|
|
26615
|
-
throw new
|
|
27074
|
+
throw new Refusal("Resource URIs support only .tres, .tscn, and .gd files.");
|
|
26616
27075
|
}
|
|
26617
27076
|
}
|
|
26618
27077
|
function isValueComplete(value) {
|
|
@@ -26702,7 +27161,7 @@ function readResourceText(uri, getProjectPath) {
|
|
|
26702
27161
|
const parsedUri = parseGodotUri(uri);
|
|
26703
27162
|
if (parsedUri.kind === "project-info") {
|
|
26704
27163
|
const projectFilePath = resolveProjectFile(projectPath, "project.godot");
|
|
26705
|
-
const rawProject =
|
|
27164
|
+
const rawProject = readFileSync5(projectFilePath, "utf-8");
|
|
26706
27165
|
const parsedProject = parseProjectGodot(rawProject);
|
|
26707
27166
|
return {
|
|
26708
27167
|
mimeType: "application/json",
|
|
@@ -26711,7 +27170,7 @@ function readResourceText(uri, getProjectPath) {
|
|
|
26711
27170
|
}
|
|
26712
27171
|
const filePath = resolveProjectFile(projectPath, parsedUri.resourcePath);
|
|
26713
27172
|
ensureAllowedExtension(parsedUri.kind, filePath);
|
|
26714
|
-
const text =
|
|
27173
|
+
const text = readFileSync5(filePath, "utf-8");
|
|
26715
27174
|
return {
|
|
26716
27175
|
mimeType: parsedUri.kind === "script" ? "text/x-gdscript" : "text/plain",
|
|
26717
27176
|
text
|
|
@@ -26740,18 +27199,18 @@ function setupResourceHandlers(mcp, getProjectPath) {
|
|
|
26740
27199
|
};
|
|
26741
27200
|
} catch (error) {
|
|
26742
27201
|
if (error instanceof Error) {
|
|
26743
|
-
throw new
|
|
27202
|
+
throw new Refusal(`Failed to read resource '${uri}': ${error.message}`, { cause: error });
|
|
26744
27203
|
}
|
|
26745
|
-
throw new
|
|
27204
|
+
throw new Refusal(`Failed to read resource '${uri}'.`, { cause: error });
|
|
26746
27205
|
}
|
|
26747
27206
|
});
|
|
26748
27207
|
}
|
|
26749
27208
|
|
|
26750
27209
|
// src/runtime-client.ts
|
|
26751
|
-
import { existsSync as
|
|
27210
|
+
import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync6, unlinkSync } from "node:fs";
|
|
26752
27211
|
import { createConnection as createConnection3 } from "node:net";
|
|
26753
|
-
import { tmpdir as
|
|
26754
|
-
import { join as
|
|
27212
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
27213
|
+
import { join as join6, resolve as resolve4 } from "node:path";
|
|
26755
27214
|
|
|
26756
27215
|
// src/tool-args.ts
|
|
26757
27216
|
function asParams(value) {
|
|
@@ -26813,7 +27272,15 @@ function runtimeDirectory(variables = process.env) {
|
|
|
26813
27272
|
return explicit;
|
|
26814
27273
|
}
|
|
26815
27274
|
const perUser = envValue("XDG_RUNTIME_DIR", variables);
|
|
26816
|
-
return
|
|
27275
|
+
return join6(perUser ?? tmpdir3(), "gdharness");
|
|
27276
|
+
}
|
|
27277
|
+
function runtimeDirectories(variables = process.env) {
|
|
27278
|
+
const candidates = [runtimeDirectory(variables)];
|
|
27279
|
+
const fallbacks = process.platform === "win32" ? [envValue("SystemRoot", variables) ?? envValue("windir", variables) ?? "C:\\Windows"] : ["/tmp", "/var/tmp"];
|
|
27280
|
+
for (const base of fallbacks) {
|
|
27281
|
+
candidates.push(join6(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
|
|
27282
|
+
}
|
|
27283
|
+
return [...new Set(candidates.map((path) => resolve4(path)))];
|
|
26817
27284
|
}
|
|
26818
27285
|
function processAlive(pid) {
|
|
26819
27286
|
try {
|
|
@@ -26826,7 +27293,7 @@ function processAlive(pid) {
|
|
|
26826
27293
|
function parseAnnouncement(file, pid) {
|
|
26827
27294
|
let fields;
|
|
26828
27295
|
try {
|
|
26829
|
-
fields = asParams(JSON.parse(
|
|
27296
|
+
fields = asParams(JSON.parse(readFileSync6(file, "utf8")));
|
|
26830
27297
|
} catch {
|
|
26831
27298
|
return null;
|
|
26832
27299
|
}
|
|
@@ -26844,17 +27311,28 @@ function parseAnnouncement(file, pid) {
|
|
|
26844
27311
|
file
|
|
26845
27312
|
};
|
|
26846
27313
|
}
|
|
26847
|
-
function discoverRuntimes(
|
|
26848
|
-
|
|
27314
|
+
function discoverRuntimes(directories = runtimeDirectories()) {
|
|
27315
|
+
const found = new Map;
|
|
27316
|
+
for (const directory of directories) {
|
|
27317
|
+
for (const endpoint of announcedIn(directory)) {
|
|
27318
|
+
if (!found.has(endpoint.pid)) {
|
|
27319
|
+
found.set(endpoint.pid, endpoint);
|
|
27320
|
+
}
|
|
27321
|
+
}
|
|
27322
|
+
}
|
|
27323
|
+
return [...found.values()].sort((a, b) => b.pid - a.pid);
|
|
27324
|
+
}
|
|
27325
|
+
function announcedIn(directory) {
|
|
27326
|
+
if (!existsSync5(directory)) {
|
|
26849
27327
|
return [];
|
|
26850
27328
|
}
|
|
26851
27329
|
const found = [];
|
|
26852
|
-
for (const entry of
|
|
27330
|
+
for (const entry of readdirSync4(directory)) {
|
|
26853
27331
|
const match = ANNOUNCEMENT_PATTERN.exec(entry);
|
|
26854
27332
|
if (!match) {
|
|
26855
27333
|
continue;
|
|
26856
27334
|
}
|
|
26857
|
-
const file =
|
|
27335
|
+
const file = join6(directory, entry);
|
|
26858
27336
|
const pid = Number.parseInt(match[1] ?? "", 10);
|
|
26859
27337
|
const endpoint = processAlive(pid) ? parseAnnouncement(file, pid) : null;
|
|
26860
27338
|
if (endpoint) {
|
|
@@ -26865,7 +27343,7 @@ function discoverRuntimes(directory = runtimeDirectory()) {
|
|
|
26865
27343
|
} catch {}
|
|
26866
27344
|
}
|
|
26867
27345
|
}
|
|
26868
|
-
return found
|
|
27346
|
+
return found;
|
|
26869
27347
|
}
|
|
26870
27348
|
function describe2(endpoint) {
|
|
26871
27349
|
return `pid ${endpoint.pid} on ${endpoint.address}:${endpoint.port} (${endpoint.project.name || "unnamed"} at ${endpoint.project.path})`;
|
|
@@ -26987,19 +27465,6 @@ function runtimeRequest(endpoint, command, params, timeoutMs) {
|
|
|
26987
27465
|
});
|
|
26988
27466
|
}
|
|
26989
27467
|
|
|
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
27468
|
// src/tool-definitions.ts
|
|
27004
27469
|
var PROJECT_PATH = {
|
|
27005
27470
|
type: "string",
|
|
@@ -27904,6 +28369,8 @@ function buildToolDefinitions() {
|
|
|
27904
28369
|
}
|
|
27905
28370
|
|
|
27906
28371
|
// src/server.ts
|
|
28372
|
+
var UPDATE_NOTICE_EVERY = 500;
|
|
28373
|
+
var FEEDBACK_NOTICE_EVERY = 250;
|
|
27907
28374
|
var run3 = promisify3(execFile3);
|
|
27908
28375
|
var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
|
|
27909
28376
|
var EDITOR_RESTART_TIMEOUT_MS = 90000;
|
|
@@ -27982,11 +28449,11 @@ function realPathOr(path) {
|
|
|
27982
28449
|
}
|
|
27983
28450
|
}
|
|
27984
28451
|
function hasMainScene(projectFile) {
|
|
27985
|
-
const scene = parseProjectGodot(
|
|
28452
|
+
const scene = parseProjectGodot(readFileSync7(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
27986
28453
|
return typeof scene === "string" && scene !== "";
|
|
27987
28454
|
}
|
|
27988
28455
|
function editorPlaysHeadless(projectFile) {
|
|
27989
|
-
const runArgs = parseProjectGodot(
|
|
28456
|
+
const runArgs = parseProjectGodot(readFileSync7(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
27990
28457
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
27991
28458
|
}
|
|
27992
28459
|
function camelCased(params) {
|
|
@@ -28002,10 +28469,14 @@ function camelCased(params) {
|
|
|
28002
28469
|
class GodotServer {
|
|
28003
28470
|
mcp;
|
|
28004
28471
|
locator = new GodotLocator;
|
|
28005
|
-
operationsScript =
|
|
28472
|
+
operationsScript = join7(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
28006
28473
|
godotBridge;
|
|
28007
28474
|
tools = buildToolDefinitions();
|
|
28008
28475
|
activeProcess = null;
|
|
28476
|
+
updates = new UpdateCheck(SERVER_VERSION);
|
|
28477
|
+
noticedUpdate = false;
|
|
28478
|
+
callsSinceNotice = 0;
|
|
28479
|
+
callsSinceFeedback = 0;
|
|
28009
28480
|
lspClient = null;
|
|
28010
28481
|
dapClient = null;
|
|
28011
28482
|
bridgeStartupError = null;
|
|
@@ -28163,9 +28634,61 @@ class GodotServer {
|
|
|
28163
28634
|
if (typeof args["projectPath"] === "string") {
|
|
28164
28635
|
this.lastProjectPath = args["projectPath"];
|
|
28165
28636
|
}
|
|
28166
|
-
|
|
28637
|
+
this.updates.refresh();
|
|
28638
|
+
const answer = await this.answered(spec.name, checked.op ?? "", args);
|
|
28639
|
+
return this.withFeedbackNotice(this.withUpdateNotice(answer));
|
|
28167
28640
|
});
|
|
28168
28641
|
}
|
|
28642
|
+
async answered(tool, op, args) {
|
|
28643
|
+
try {
|
|
28644
|
+
return await this.dispatch(tool, op, args);
|
|
28645
|
+
} catch (error) {
|
|
28646
|
+
if (error instanceof McpError || error instanceof Refusal) {
|
|
28647
|
+
throw error;
|
|
28648
|
+
}
|
|
28649
|
+
const where = op === "" ? tool : `${tool} op=${op}`;
|
|
28650
|
+
console.error(`[SERVER] Unmodelled failure in ${where}:`, error);
|
|
28651
|
+
return { content: [{ type: "text", text: defectReport2(where, error) }], isError: true };
|
|
28652
|
+
}
|
|
28653
|
+
}
|
|
28654
|
+
withUpdateNotice(answer) {
|
|
28655
|
+
this.callsSinceNotice += 1;
|
|
28656
|
+
if (this.callsSinceNotice < UPDATE_NOTICE_EVERY && this.noticedUpdate) {
|
|
28657
|
+
return answer;
|
|
28658
|
+
}
|
|
28659
|
+
const notice = this.updates.notice();
|
|
28660
|
+
if (notice === null) {
|
|
28661
|
+
return answer;
|
|
28662
|
+
}
|
|
28663
|
+
this.noticedUpdate = true;
|
|
28664
|
+
this.callsSinceNotice = 0;
|
|
28665
|
+
return {
|
|
28666
|
+
...answer,
|
|
28667
|
+
content: [
|
|
28668
|
+
...answer.content,
|
|
28669
|
+
{
|
|
28670
|
+
type: "text",
|
|
28671
|
+
text: JSON.stringify({
|
|
28672
|
+
update_available: {
|
|
28673
|
+
...notice,
|
|
28674
|
+
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."
|
|
28675
|
+
}
|
|
28676
|
+
}, null, 2)
|
|
28677
|
+
}
|
|
28678
|
+
]
|
|
28679
|
+
};
|
|
28680
|
+
}
|
|
28681
|
+
withFeedbackNotice(answer) {
|
|
28682
|
+
this.callsSinceFeedback += 1;
|
|
28683
|
+
if (this.callsSinceFeedback < FEEDBACK_NOTICE_EVERY) {
|
|
28684
|
+
return answer;
|
|
28685
|
+
}
|
|
28686
|
+
this.callsSinceFeedback = 0;
|
|
28687
|
+
return {
|
|
28688
|
+
...answer,
|
|
28689
|
+
content: [...answer.content, { type: "text", text: feedbackNotice() }]
|
|
28690
|
+
};
|
|
28691
|
+
}
|
|
28169
28692
|
validateArguments(spec, args) {
|
|
28170
28693
|
const known = new Set([...Object.keys(spec.parameters), ...spec.operations ? ["op"] : []]);
|
|
28171
28694
|
const unknown = Object.keys(args).filter((key) => !known.has(key));
|
|
@@ -28371,9 +28894,16 @@ class GodotServer {
|
|
|
28371
28894
|
});
|
|
28372
28895
|
}
|
|
28373
28896
|
case "debug_control":
|
|
28374
|
-
|
|
28375
|
-
|
|
28376
|
-
|
|
28897
|
+
case "debug_state": {
|
|
28898
|
+
if (tool === "debug_state" && op === "output") {
|
|
28899
|
+
return await this.handleDAP("dap_get_output", args);
|
|
28900
|
+
}
|
|
28901
|
+
const held = await this.debuggedGame();
|
|
28902
|
+
if (!held.ok) {
|
|
28903
|
+
return held.response;
|
|
28904
|
+
}
|
|
28905
|
+
return tool === "debug_control" ? await this.handleDAP(`dap_${op}`, args) : await this.handleDAP(DEBUG_STATE_CALLS[op] ?? "dap_get_stack_trace", args);
|
|
28906
|
+
}
|
|
28377
28907
|
default:
|
|
28378
28908
|
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${tool}`);
|
|
28379
28909
|
}
|
|
@@ -28393,8 +28923,8 @@ class GodotServer {
|
|
|
28393
28923
|
if (path === undefined) {
|
|
28394
28924
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
28395
28925
|
}
|
|
28396
|
-
const file =
|
|
28397
|
-
if (!
|
|
28926
|
+
const file = join7(path, "project.godot");
|
|
28927
|
+
if (!existsSync6(file)) {
|
|
28398
28928
|
return {
|
|
28399
28929
|
ok: false,
|
|
28400
28930
|
response: this.createErrorResponse(`Not a Godot project: ${path}`, [
|
|
@@ -28404,6 +28934,56 @@ class GodotServer {
|
|
|
28404
28934
|
}
|
|
28405
28935
|
return { ok: true, value: { path, file } };
|
|
28406
28936
|
}
|
|
28937
|
+
async debuggedGame() {
|
|
28938
|
+
const game = this.activeProcess;
|
|
28939
|
+
if (game === null) {
|
|
28940
|
+
return {
|
|
28941
|
+
ok: false,
|
|
28942
|
+
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"])
|
|
28943
|
+
};
|
|
28944
|
+
}
|
|
28945
|
+
if (!game.throughEditor) {
|
|
28946
|
+
return {
|
|
28947
|
+
ok: false,
|
|
28948
|
+
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.", [
|
|
28949
|
+
"editor_run start plays it through the open editor, whose debugger the debug_* tools speak to",
|
|
28950
|
+
"editor_status says whether an editor has reached this server"
|
|
28951
|
+
])
|
|
28952
|
+
};
|
|
28953
|
+
}
|
|
28954
|
+
try {
|
|
28955
|
+
await this.dap().attach();
|
|
28956
|
+
} catch (error) {
|
|
28957
|
+
return {
|
|
28958
|
+
ok: false,
|
|
28959
|
+
response: this.createErrorResponse(`The editor is playing the game but its debug adapter did not answer: ${errorMessage(error)}`, [
|
|
28960
|
+
"Godot serves the debug adapter on 6006 unless --dap-port says otherwise",
|
|
28961
|
+
"GDHARNESS_DAP_PORT points this server at another one"
|
|
28962
|
+
])
|
|
28963
|
+
};
|
|
28964
|
+
}
|
|
28965
|
+
if (!this.dap().isStopped()) {
|
|
28966
|
+
return {
|
|
28967
|
+
ok: false,
|
|
28968
|
+
response: this.createErrorResponse("The game is running, not stopped, so it has no stack and no scope to read.", [
|
|
28969
|
+
"debug_breakpoint set puts a breakpoint on before the run, and it is waiting when the game starts",
|
|
28970
|
+
"editor_output reads what the running game is printing"
|
|
28971
|
+
])
|
|
28972
|
+
};
|
|
28973
|
+
}
|
|
28974
|
+
return { ok: true, value: game };
|
|
28975
|
+
}
|
|
28976
|
+
async refreshStaleClasses(projectPath) {
|
|
28977
|
+
const stale = staleClassNames(projectPath);
|
|
28978
|
+
if (stale.length === 0) {
|
|
28979
|
+
return { ok: true, value: [] };
|
|
28980
|
+
}
|
|
28981
|
+
const refreshed = await this.operation("refresh_class_cache", {}, projectPath);
|
|
28982
|
+
if (!refreshed.ok) {
|
|
28983
|
+
return { ok: false, response: this.answer(refreshed) };
|
|
28984
|
+
}
|
|
28985
|
+
return { ok: true, value: stale };
|
|
28986
|
+
}
|
|
28407
28987
|
containProjectFiles(args) {
|
|
28408
28988
|
const projectPath = readString(args, "projectPath");
|
|
28409
28989
|
if (!projectPath) {
|
|
@@ -28489,7 +29069,7 @@ class GodotServer {
|
|
|
28489
29069
|
if (!engine.ok) {
|
|
28490
29070
|
return engine.response;
|
|
28491
29071
|
}
|
|
28492
|
-
const application = parseProjectGodot(
|
|
29072
|
+
const application = parseProjectGodot(readFileSync7(project.value.file, "utf8"))["application"] ?? {};
|
|
28493
29073
|
const name = application["config/name"];
|
|
28494
29074
|
const mainScene = application["run/main_scene"];
|
|
28495
29075
|
const info = {
|
|
@@ -28578,7 +29158,7 @@ class GodotServer {
|
|
|
28578
29158
|
log.finish();
|
|
28579
29159
|
const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
|
|
28580
29160
|
const verdict = {
|
|
28581
|
-
exported: exitCode === 0 && log.count("error") === 0 &&
|
|
29161
|
+
exported: exitCode === 0 && log.count("error") === 0 && existsSync6(output.absolutePath),
|
|
28582
29162
|
preset,
|
|
28583
29163
|
outputPath: output.relativePath,
|
|
28584
29164
|
debug,
|
|
@@ -28608,7 +29188,7 @@ class GodotServer {
|
|
|
28608
29188
|
return contained.response;
|
|
28609
29189
|
}
|
|
28610
29190
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
28611
|
-
if (!
|
|
29191
|
+
if (!existsSync6(join7(project.value.path, runner))) {
|
|
28612
29192
|
return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
|
|
28613
29193
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
28614
29194
|
]);
|
|
@@ -28656,14 +29236,14 @@ class GodotServer {
|
|
|
28656
29236
|
resolve(false);
|
|
28657
29237
|
});
|
|
28658
29238
|
});
|
|
28659
|
-
const reportsDir =
|
|
29239
|
+
const reportsDir = join7(project.value.path, ".godot", "gdharness-reports");
|
|
28660
29240
|
let report = null;
|
|
28661
29241
|
let reportProblem = null;
|
|
28662
29242
|
try {
|
|
28663
|
-
const written =
|
|
29243
|
+
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
29244
|
const newest = written.at(-1);
|
|
28665
29245
|
if (newest !== undefined) {
|
|
28666
|
-
report = parseJUnit(
|
|
29246
|
+
report = parseJUnit(readFileSync7(join7(reportsDir, newest, "results.xml"), "utf8"));
|
|
28667
29247
|
}
|
|
28668
29248
|
} catch (error) {
|
|
28669
29249
|
reportProblem = errorMessage(error);
|
|
@@ -28779,7 +29359,7 @@ class GodotServer {
|
|
|
28779
29359
|
addonIsStale: status.connected ? stale : undefined,
|
|
28780
29360
|
bridgeAvailable: this.bridgeStartupError === null,
|
|
28781
29361
|
startupError: this.bridgeStartupError,
|
|
28782
|
-
staleNote: stale ?
|
|
29362
|
+
staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
|
|
28783
29363
|
note: isPortConflict ? "Bridge port is already in use. Another gdharness instance may own the editor bridge, so this server cannot report that editor connection." : undefined,
|
|
28784
29364
|
suggestion: isPortConflict ? "Stop duplicate gdharness/MCP server instances or re-run the command from the same server process that owns the bridge port." : undefined
|
|
28785
29365
|
};
|
|
@@ -28857,6 +29437,7 @@ class GodotServer {
|
|
|
28857
29437
|
addonVersion: now.addonVersion,
|
|
28858
29438
|
serverVersion: SERVER_VERSION,
|
|
28859
29439
|
addonIsStale: now.addonVersion !== SERVER_VERSION,
|
|
29440
|
+
staleNote: addonMismatch(now.addonVersion, SERVER_VERSION),
|
|
28860
29441
|
tookMs: Date.now() - began
|
|
28861
29442
|
});
|
|
28862
29443
|
}
|
|
@@ -28876,6 +29457,13 @@ class GodotServer {
|
|
|
28876
29457
|
if (!project.ok) {
|
|
28877
29458
|
return project.response;
|
|
28878
29459
|
}
|
|
29460
|
+
if (this.godotBridge.isConnected()) {
|
|
29461
|
+
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.", [
|
|
29462
|
+
"editor_status says which editor is answering, and for which project",
|
|
29463
|
+
"editor_launch restart replaces the connected editor rather than joining it",
|
|
29464
|
+
"Close the open editor first if the new project is the one you want"
|
|
29465
|
+
]);
|
|
29466
|
+
}
|
|
28879
29467
|
const engine = await this.engine();
|
|
28880
29468
|
if (!engine.ok) {
|
|
28881
29469
|
return engine.response;
|
|
@@ -28883,7 +29471,8 @@ class GodotServer {
|
|
|
28883
29471
|
this.logDebug(`Launching Godot editor for project: ${project.value.path}`);
|
|
28884
29472
|
const editor = spawn(engine.value, editorArguments(project.value.path), {
|
|
28885
29473
|
stdio: "ignore",
|
|
28886
|
-
detached: true
|
|
29474
|
+
detached: true,
|
|
29475
|
+
env: { ...process.env, GDHARNESS_RUNTIME_DIR: runtimeDirectory() }
|
|
28887
29476
|
});
|
|
28888
29477
|
const started = await new Promise((resolve) => {
|
|
28889
29478
|
editor.once("spawn", () => {
|
|
@@ -28923,6 +29512,10 @@ class GodotServer {
|
|
|
28923
29512
|
if (!engine.ok) {
|
|
28924
29513
|
return engine.response;
|
|
28925
29514
|
}
|
|
29515
|
+
const refreshed = await this.refreshStaleClasses(project.value.path);
|
|
29516
|
+
if (!refreshed.ok) {
|
|
29517
|
+
return refreshed.response;
|
|
29518
|
+
}
|
|
28926
29519
|
const sceneArgument = sceneToRun?.ok ? sceneToRun.relativePath : null;
|
|
28927
29520
|
if (op === "check") {
|
|
28928
29521
|
return await this.checkBoot(engine.value, project.value.path, sceneArgument, args);
|
|
@@ -28936,7 +29529,7 @@ class GodotServer {
|
|
|
28936
29529
|
variables: process.env
|
|
28937
29530
|
});
|
|
28938
29531
|
if (this.godotBridge.isConnected() && (!headless || editorPlaysHeadless(project.value.file))) {
|
|
28939
|
-
return await this.playThroughEditor(sceneArgument);
|
|
29532
|
+
return await this.playThroughEditor(sceneArgument, refreshed.value);
|
|
28940
29533
|
}
|
|
28941
29534
|
const cmdArgs = runArguments({
|
|
28942
29535
|
projectPath: project.value.path,
|
|
@@ -28956,10 +29549,11 @@ class GodotServer {
|
|
|
28956
29549
|
through: "gdharness",
|
|
28957
29550
|
pid: started.process.pid ?? null,
|
|
28958
29551
|
arguments: cmdArgs,
|
|
29552
|
+
refreshedClasses: refreshed.value,
|
|
28959
29553
|
message: "Use editor_output for what it prints and editor_run stop to end it."
|
|
28960
29554
|
});
|
|
28961
29555
|
}
|
|
28962
|
-
async playThroughEditor(scene) {
|
|
29556
|
+
async playThroughEditor(scene, refreshedClasses) {
|
|
28963
29557
|
const log = new GameLog;
|
|
28964
29558
|
try {
|
|
28965
29559
|
await this.dap().connect();
|
|
@@ -28985,6 +29579,7 @@ class GodotServer {
|
|
|
28985
29579
|
started: true,
|
|
28986
29580
|
through: "editor",
|
|
28987
29581
|
scene: scene === null ? "the main scene" : `res://${scene}`,
|
|
29582
|
+
refreshedClasses,
|
|
28988
29583
|
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
29584
|
});
|
|
28990
29585
|
}
|
|
@@ -29159,8 +29754,8 @@ class GodotServer {
|
|
|
29159
29754
|
return this.createErrorResponse(choice.problem);
|
|
29160
29755
|
}
|
|
29161
29756
|
const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
|
|
29162
|
-
const screenshotDir = expectsScreenshot ? mkdtempSync2(
|
|
29163
|
-
const screenshotPath = screenshotDir ?
|
|
29757
|
+
const screenshotDir = expectsScreenshot ? mkdtempSync2(join7(tmpdir4(), "gdharness-runtime-screenshot-")) : null;
|
|
29758
|
+
const screenshotPath = screenshotDir ? join7(screenshotDir, "capture.png") : null;
|
|
29164
29759
|
try {
|
|
29165
29760
|
const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs);
|
|
29166
29761
|
if (!reply.ok) {
|
|
@@ -29181,7 +29776,7 @@ class GodotServer {
|
|
|
29181
29776
|
return {
|
|
29182
29777
|
content: [
|
|
29183
29778
|
{ type: "text", text: `Screenshot captured: ${dimensions}` },
|
|
29184
|
-
{ type: "image", data:
|
|
29779
|
+
{ type: "image", data: readFileSync7(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
29185
29780
|
]
|
|
29186
29781
|
};
|
|
29187
29782
|
} finally {
|
|
@@ -29246,7 +29841,6 @@ async function runGodotServer() {
|
|
|
29246
29841
|
|
|
29247
29842
|
// src/server-entry.ts
|
|
29248
29843
|
await runGodotServer().catch((error) => {
|
|
29249
|
-
|
|
29250
|
-
console.error("Failed to run server:", errorMessage);
|
|
29844
|
+
console.error(defectReport("gdharness server startup", error));
|
|
29251
29845
|
process.exit(1);
|
|
29252
29846
|
});
|