gdharness 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/cli.js +301 -250
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +9 -1
- package/build/index.js +279 -228
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -9826,17 +9826,213 @@ function toError(error) {
|
|
|
9826
9826
|
}
|
|
9827
9827
|
|
|
9828
9828
|
// src/server-version.ts
|
|
9829
|
-
import { readFileSync } from "node:fs";
|
|
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
|
|
9830
10013
|
var DEBUG_MODE = process.env["DEBUG"] === "true";
|
|
9831
10014
|
var GODOT_DEBUG_MODE_DEFAULT = process.env["GODOT_DEBUG"] === "true" || DEBUG_MODE;
|
|
9832
10015
|
var SERVER_VERSION = (() => {
|
|
9833
10016
|
try {
|
|
9834
|
-
const pkg = JSON.parse(
|
|
10017
|
+
const pkg = JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf8"));
|
|
9835
10018
|
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
9836
10019
|
} catch {
|
|
9837
10020
|
return "0.0.0";
|
|
9838
10021
|
}
|
|
9839
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
|
+
}
|
|
9840
10036
|
|
|
9841
10037
|
// src/issues.ts
|
|
9842
10038
|
var NEW_ISSUE = "https://github.com/Aureliolo/gdharness/issues/new";
|
|
@@ -24500,8 +24696,8 @@ class StdioServerTransport {
|
|
|
24500
24696
|
}
|
|
24501
24697
|
|
|
24502
24698
|
// src/class-cache.ts
|
|
24503
|
-
import { existsSync, readdirSync, readFileSync as
|
|
24504
|
-
import { join } from "node:path";
|
|
24699
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3 } from "node:fs";
|
|
24700
|
+
import { join as join2 } from "node:path";
|
|
24505
24701
|
function declaredClasses(projectPath) {
|
|
24506
24702
|
const declared = new Map;
|
|
24507
24703
|
const visit = (directory, prefix) => {
|
|
@@ -24509,11 +24705,11 @@ function declaredClasses(projectPath) {
|
|
|
24509
24705
|
if (entry.name.startsWith(".")) {
|
|
24510
24706
|
continue;
|
|
24511
24707
|
}
|
|
24512
|
-
const path =
|
|
24708
|
+
const path = join2(directory, entry.name);
|
|
24513
24709
|
if (entry.isDirectory()) {
|
|
24514
24710
|
visit(path, `${prefix}${entry.name}/`);
|
|
24515
24711
|
} else if (entry.isFile() && entry.name.endsWith(".gd")) {
|
|
24516
|
-
const found = /^class_name\s+([A-Za-z_][A-Za-z0-9_]*)/m.exec(
|
|
24712
|
+
const found = /^class_name\s+([A-Za-z_][A-Za-z0-9_]*)/m.exec(readFileSync3(path, "utf8"));
|
|
24517
24713
|
if (found?.[1]) {
|
|
24518
24714
|
declared.set(found[1], `res://${prefix}${entry.name}`);
|
|
24519
24715
|
}
|
|
@@ -24524,12 +24720,12 @@ function declaredClasses(projectPath) {
|
|
|
24524
24720
|
return declared;
|
|
24525
24721
|
}
|
|
24526
24722
|
function cachedClasses(projectPath) {
|
|
24527
|
-
const cache =
|
|
24528
|
-
if (!
|
|
24723
|
+
const cache = join2(projectPath, ".godot", "global_script_class_cache.cfg");
|
|
24724
|
+
if (!existsSync2(cache)) {
|
|
24529
24725
|
return null;
|
|
24530
24726
|
}
|
|
24531
24727
|
const listed = new Map;
|
|
24532
|
-
const text =
|
|
24728
|
+
const text = readFileSync3(cache, "utf8");
|
|
24533
24729
|
for (const entry of text.matchAll(/"class":\s*&"([^"]+)"[\s\S]*?"path":\s*"([^"]+)"/g)) {
|
|
24534
24730
|
listed.set(entry[1] ?? "", entry[2] ?? "");
|
|
24535
24731
|
}
|
|
@@ -25621,23 +25817,23 @@ function getDefaultBridge() {
|
|
|
25621
25817
|
|
|
25622
25818
|
// src/godot-path.ts
|
|
25623
25819
|
import { execFile } from "node:child_process";
|
|
25624
|
-
import { existsSync as
|
|
25820
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
25625
25821
|
import { normalize } from "node:path";
|
|
25626
25822
|
import { promisify } from "node:util";
|
|
25627
25823
|
|
|
25628
25824
|
// src/detection.ts
|
|
25629
|
-
import { existsSync as
|
|
25630
|
-
import { homedir } from "node:os";
|
|
25631
|
-
import { join as
|
|
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";
|
|
25632
25828
|
function resolveHomeDirectory() {
|
|
25633
25829
|
try {
|
|
25634
|
-
return
|
|
25830
|
+
return homedir2();
|
|
25635
25831
|
} catch {
|
|
25636
25832
|
return "";
|
|
25637
25833
|
}
|
|
25638
25834
|
}
|
|
25639
25835
|
function scanDirectoryForGodotBinaries(directory, platform) {
|
|
25640
|
-
if (!directory || !
|
|
25836
|
+
if (!directory || !existsSync3(directory)) {
|
|
25641
25837
|
return [];
|
|
25642
25838
|
}
|
|
25643
25839
|
let entries;
|
|
@@ -25652,7 +25848,7 @@ function scanDirectoryForGodotBinaries(directory, platform) {
|
|
|
25652
25848
|
if (!pattern.test(name)) {
|
|
25653
25849
|
continue;
|
|
25654
25850
|
}
|
|
25655
|
-
const fullPath =
|
|
25851
|
+
const fullPath = join3(directory, name);
|
|
25656
25852
|
try {
|
|
25657
25853
|
const stat = statSync(fullPath);
|
|
25658
25854
|
if (stat.isFile()) {
|
|
@@ -25661,7 +25857,7 @@ function scanDirectoryForGodotBinaries(directory, platform) {
|
|
|
25661
25857
|
} catch {}
|
|
25662
25858
|
}
|
|
25663
25859
|
matches.sort((a, b) => b.mtime - a.mtime);
|
|
25664
|
-
return matches.map((m) =>
|
|
25860
|
+
return matches.map((m) => join3(directory, m.name));
|
|
25665
25861
|
}
|
|
25666
25862
|
function conventionalPaths(platform, home) {
|
|
25667
25863
|
const paths = ["godot"];
|
|
@@ -25737,6 +25933,19 @@ function runArguments(options) {
|
|
|
25737
25933
|
function editorArguments(projectPath) {
|
|
25738
25934
|
return ["-e", "--path", projectPath];
|
|
25739
25935
|
}
|
|
25936
|
+
function userDataIn(home, variables = process.env) {
|
|
25937
|
+
const moved = ["APPDATA", "XDG_DATA_HOME"];
|
|
25938
|
+
const carried = {};
|
|
25939
|
+
for (const [name, value] of Object.entries(variables)) {
|
|
25940
|
+
if (!moved.some((named) => named.toLowerCase() === name.toLowerCase())) {
|
|
25941
|
+
carried[name] = value;
|
|
25942
|
+
}
|
|
25943
|
+
}
|
|
25944
|
+
for (const named of moved) {
|
|
25945
|
+
carried[named] = home;
|
|
25946
|
+
}
|
|
25947
|
+
return carried;
|
|
25948
|
+
}
|
|
25740
25949
|
|
|
25741
25950
|
// src/godot-path.ts
|
|
25742
25951
|
var run = promisify(execFile);
|
|
@@ -25776,7 +25985,7 @@ class GodotLocator {
|
|
|
25776
25985
|
return known;
|
|
25777
25986
|
}
|
|
25778
25987
|
let ok = false;
|
|
25779
|
-
if (path === "godot" ||
|
|
25988
|
+
if (path === "godot" || existsSync4(path)) {
|
|
25780
25989
|
try {
|
|
25781
25990
|
await run(path, ["--version"]);
|
|
25782
25991
|
ok = true;
|
|
@@ -25791,9 +26000,9 @@ class GodotLocator {
|
|
|
25791
26000
|
|
|
25792
26001
|
// src/headless.ts
|
|
25793
26002
|
import { execFile as execFile2 } from "node:child_process";
|
|
25794
|
-
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
25795
|
-
import { tmpdir } from "node:os";
|
|
25796
|
-
import { join as
|
|
26003
|
+
import { mkdtempSync, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
26004
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
26005
|
+
import { join as join4 } from "node:path";
|
|
25797
26006
|
import { promisify as promisify2 } from "node:util";
|
|
25798
26007
|
var run2 = promisify2(execFile2);
|
|
25799
26008
|
function snakeCased(params) {
|
|
@@ -25834,9 +26043,9 @@ function reason(stdout, stderr) {
|
|
|
25834
26043
|
return stdout.trim().split(/\r?\n/).at(-1) ?? "no output at all";
|
|
25835
26044
|
}
|
|
25836
26045
|
async function runOperation(engine, operation, params, projectPath) {
|
|
25837
|
-
const paramsDir = mkdtempSync(
|
|
25838
|
-
const paramsFile =
|
|
25839
|
-
|
|
26046
|
+
const paramsDir = mkdtempSync(join4(tmpdir2(), "gdharness-params-"));
|
|
26047
|
+
const paramsFile = join4(paramsDir, `${operation}.json`);
|
|
26048
|
+
writeFileSync2(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
|
|
25840
26049
|
const args = [
|
|
25841
26050
|
"--headless",
|
|
25842
26051
|
"--path",
|
|
@@ -26706,8 +26915,8 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
26706
26915
|
}
|
|
26707
26916
|
|
|
26708
26917
|
// src/project-scan.ts
|
|
26709
|
-
import { readdirSync as readdirSync3, readFileSync as
|
|
26710
|
-
import { join as
|
|
26918
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync4 } from "node:fs";
|
|
26919
|
+
import { join as join5 } from "node:path";
|
|
26711
26920
|
var SKIPPED = new Set([".git", ".godot", ".import", "node_modules"]);
|
|
26712
26921
|
var ASSET_EXTENSIONS = new Set(["png", "jpg", "jpeg", "webp", "svg", "ttf", "otf", "wav", "mp3", "ogg"]);
|
|
26713
26922
|
function projectStructure(projectPath) {
|
|
@@ -26718,7 +26927,7 @@ function projectStructure(projectPath) {
|
|
|
26718
26927
|
continue;
|
|
26719
26928
|
}
|
|
26720
26929
|
if (entry.isDirectory()) {
|
|
26721
|
-
visit(
|
|
26930
|
+
visit(join5(directory, entry.name));
|
|
26722
26931
|
} else if (entry.isFile()) {
|
|
26723
26932
|
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
26724
26933
|
if (extension === "tscn") {
|
|
@@ -26760,7 +26969,7 @@ function searchProject(projectPath, options) {
|
|
|
26760
26969
|
if (SKIPPED.has(entry.name)) {
|
|
26761
26970
|
continue;
|
|
26762
26971
|
}
|
|
26763
|
-
const entryPath =
|
|
26972
|
+
const entryPath = join5(directory, entry.name);
|
|
26764
26973
|
if (entry.isDirectory()) {
|
|
26765
26974
|
visit(entryPath);
|
|
26766
26975
|
continue;
|
|
@@ -26771,7 +26980,7 @@ function searchProject(projectPath, options) {
|
|
|
26771
26980
|
}
|
|
26772
26981
|
result.summary.files_searched += 1;
|
|
26773
26982
|
const matches = [];
|
|
26774
|
-
for (const [index, line] of
|
|
26983
|
+
for (const [index, line] of readFileSync4(entryPath, "utf8").split(`
|
|
26775
26984
|
`).entries()) {
|
|
26776
26985
|
if (full()) {
|
|
26777
26986
|
break;
|
|
@@ -26794,7 +27003,7 @@ function searchProject(projectPath, options) {
|
|
|
26794
27003
|
}
|
|
26795
27004
|
|
|
26796
27005
|
// src/resources.ts
|
|
26797
|
-
import { readFileSync as
|
|
27006
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
26798
27007
|
import { extname, resolve as resolve3 } from "node:path";
|
|
26799
27008
|
var STATIC_RESOURCES = [
|
|
26800
27009
|
{
|
|
@@ -26965,7 +27174,7 @@ function readResourceText(uri, getProjectPath) {
|
|
|
26965
27174
|
const parsedUri = parseGodotUri(uri);
|
|
26966
27175
|
if (parsedUri.kind === "project-info") {
|
|
26967
27176
|
const projectFilePath = resolveProjectFile(projectPath, "project.godot");
|
|
26968
|
-
const rawProject =
|
|
27177
|
+
const rawProject = readFileSync5(projectFilePath, "utf-8");
|
|
26969
27178
|
const parsedProject = parseProjectGodot(rawProject);
|
|
26970
27179
|
return {
|
|
26971
27180
|
mimeType: "application/json",
|
|
@@ -26974,7 +27183,7 @@ function readResourceText(uri, getProjectPath) {
|
|
|
26974
27183
|
}
|
|
26975
27184
|
const filePath = resolveProjectFile(projectPath, parsedUri.resourcePath);
|
|
26976
27185
|
ensureAllowedExtension(parsedUri.kind, filePath);
|
|
26977
|
-
const text =
|
|
27186
|
+
const text = readFileSync5(filePath, "utf-8");
|
|
26978
27187
|
return {
|
|
26979
27188
|
mimeType: parsedUri.kind === "script" ? "text/x-gdscript" : "text/plain",
|
|
26980
27189
|
text
|
|
@@ -27011,10 +27220,10 @@ function setupResourceHandlers(mcp, getProjectPath) {
|
|
|
27011
27220
|
}
|
|
27012
27221
|
|
|
27013
27222
|
// src/runtime-client.ts
|
|
27014
|
-
import { existsSync as
|
|
27223
|
+
import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync6, unlinkSync } from "node:fs";
|
|
27015
27224
|
import { createConnection as createConnection3 } from "node:net";
|
|
27016
|
-
import { tmpdir as
|
|
27017
|
-
import { join as
|
|
27225
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
27226
|
+
import { join as join6, resolve as resolve4 } from "node:path";
|
|
27018
27227
|
|
|
27019
27228
|
// src/tool-args.ts
|
|
27020
27229
|
function asParams(value) {
|
|
@@ -27076,7 +27285,15 @@ function runtimeDirectory(variables = process.env) {
|
|
|
27076
27285
|
return explicit;
|
|
27077
27286
|
}
|
|
27078
27287
|
const perUser = envValue("XDG_RUNTIME_DIR", variables);
|
|
27079
|
-
return
|
|
27288
|
+
return join6(perUser ?? tmpdir3(), "gdharness");
|
|
27289
|
+
}
|
|
27290
|
+
function runtimeDirectories(variables = process.env) {
|
|
27291
|
+
const candidates = [runtimeDirectory(variables)];
|
|
27292
|
+
const fallbacks = process.platform === "win32" ? [envValue("SystemRoot", variables) ?? envValue("windir", variables) ?? "C:\\Windows"] : ["/tmp", "/var/tmp"];
|
|
27293
|
+
for (const base of fallbacks) {
|
|
27294
|
+
candidates.push(join6(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
|
|
27295
|
+
}
|
|
27296
|
+
return [...new Set(candidates.map((path) => resolve4(path)))];
|
|
27080
27297
|
}
|
|
27081
27298
|
function processAlive(pid) {
|
|
27082
27299
|
try {
|
|
@@ -27089,7 +27306,7 @@ function processAlive(pid) {
|
|
|
27089
27306
|
function parseAnnouncement(file, pid) {
|
|
27090
27307
|
let fields;
|
|
27091
27308
|
try {
|
|
27092
|
-
fields = asParams(JSON.parse(
|
|
27309
|
+
fields = asParams(JSON.parse(readFileSync6(file, "utf8")));
|
|
27093
27310
|
} catch {
|
|
27094
27311
|
return null;
|
|
27095
27312
|
}
|
|
@@ -27107,8 +27324,19 @@ function parseAnnouncement(file, pid) {
|
|
|
27107
27324
|
file
|
|
27108
27325
|
};
|
|
27109
27326
|
}
|
|
27110
|
-
function discoverRuntimes(
|
|
27111
|
-
|
|
27327
|
+
function discoverRuntimes(directories = runtimeDirectories()) {
|
|
27328
|
+
const found = new Map;
|
|
27329
|
+
for (const directory of directories) {
|
|
27330
|
+
for (const endpoint of announcedIn(directory)) {
|
|
27331
|
+
if (!found.has(endpoint.pid)) {
|
|
27332
|
+
found.set(endpoint.pid, endpoint);
|
|
27333
|
+
}
|
|
27334
|
+
}
|
|
27335
|
+
}
|
|
27336
|
+
return [...found.values()].sort((a, b) => b.pid - a.pid);
|
|
27337
|
+
}
|
|
27338
|
+
function announcedIn(directory) {
|
|
27339
|
+
if (!existsSync5(directory)) {
|
|
27112
27340
|
return [];
|
|
27113
27341
|
}
|
|
27114
27342
|
const found = [];
|
|
@@ -27117,7 +27345,7 @@ function discoverRuntimes(directory = runtimeDirectory()) {
|
|
|
27117
27345
|
if (!match) {
|
|
27118
27346
|
continue;
|
|
27119
27347
|
}
|
|
27120
|
-
const file =
|
|
27348
|
+
const file = join6(directory, entry);
|
|
27121
27349
|
const pid = Number.parseInt(match[1] ?? "", 10);
|
|
27122
27350
|
const endpoint = processAlive(pid) ? parseAnnouncement(file, pid) : null;
|
|
27123
27351
|
if (endpoint) {
|
|
@@ -27128,7 +27356,7 @@ function discoverRuntimes(directory = runtimeDirectory()) {
|
|
|
27128
27356
|
} catch {}
|
|
27129
27357
|
}
|
|
27130
27358
|
}
|
|
27131
|
-
return found
|
|
27359
|
+
return found;
|
|
27132
27360
|
}
|
|
27133
27361
|
function describe2(endpoint) {
|
|
27134
27362
|
return `pid ${endpoint.pid} on ${endpoint.address}:${endpoint.port} (${endpoint.project.name || "unnamed"} at ${endpoint.project.path})`;
|
|
@@ -27552,7 +27780,7 @@ var TOOL_SPECS = [
|
|
|
27552
27780
|
},
|
|
27553
27781
|
{
|
|
27554
27782
|
name: "project_test",
|
|
27555
|
-
description: "Runs the project's gdUnit4 tests headless and answers with every case: which failed, where, and what the assertion said. The class list is rebuilt first, so a suite written a moment ago is found. Needs gdUnit4 under addons/gdUnit4.",
|
|
27783
|
+
description: "Runs the project's gdUnit4 tests headless and answers with every case: which failed, where, and what the assertion said. The class list is rebuilt first, so a suite written a moment ago is found. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. Needs gdUnit4 under addons/gdUnit4.",
|
|
27556
27784
|
parameters: {
|
|
27557
27785
|
projectPath: PROJECT_PATH,
|
|
27558
27786
|
path: {
|
|
@@ -28153,187 +28381,6 @@ function buildToolDefinitions() {
|
|
|
28153
28381
|
});
|
|
28154
28382
|
}
|
|
28155
28383
|
|
|
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
|
-
|
|
28337
28384
|
// src/server.ts
|
|
28338
28385
|
var UPDATE_NOTICE_EVERY = 500;
|
|
28339
28386
|
var FEEDBACK_NOTICE_EVERY = 250;
|
|
@@ -29187,7 +29234,8 @@ class GodotServer {
|
|
|
29187
29234
|
];
|
|
29188
29235
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
|
|
29189
29236
|
this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
29190
|
-
const
|
|
29237
|
+
const userData = mkdtempSync2(join7(tmpdir4(), "gdharness-tests-"));
|
|
29238
|
+
const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
|
|
29191
29239
|
const hung = await new Promise((resolve) => {
|
|
29192
29240
|
const timer = setTimeout(() => {
|
|
29193
29241
|
run.process.kill();
|
|
@@ -29215,6 +29263,7 @@ class GodotServer {
|
|
|
29215
29263
|
reportProblem = errorMessage(error);
|
|
29216
29264
|
} finally {
|
|
29217
29265
|
rmSync2(reportsDir, { recursive: true, force: true });
|
|
29266
|
+
rmSync2(userData, { recursive: true, force: true });
|
|
29218
29267
|
}
|
|
29219
29268
|
const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries;
|
|
29220
29269
|
const verdicts = {
|
|
@@ -29325,7 +29374,7 @@ class GodotServer {
|
|
|
29325
29374
|
addonIsStale: status.connected ? stale : undefined,
|
|
29326
29375
|
bridgeAvailable: this.bridgeStartupError === null,
|
|
29327
29376
|
startupError: this.bridgeStartupError,
|
|
29328
|
-
staleNote: stale ?
|
|
29377
|
+
staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
|
|
29329
29378
|
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,
|
|
29330
29379
|
suggestion: isPortConflict ? "Stop duplicate gdharness/MCP server instances or re-run the command from the same server process that owns the bridge port." : undefined
|
|
29331
29380
|
};
|
|
@@ -29403,6 +29452,7 @@ class GodotServer {
|
|
|
29403
29452
|
addonVersion: now.addonVersion,
|
|
29404
29453
|
serverVersion: SERVER_VERSION,
|
|
29405
29454
|
addonIsStale: now.addonVersion !== SERVER_VERSION,
|
|
29455
|
+
staleNote: addonMismatch(now.addonVersion, SERVER_VERSION),
|
|
29406
29456
|
tookMs: Date.now() - began
|
|
29407
29457
|
});
|
|
29408
29458
|
}
|
|
@@ -29436,7 +29486,8 @@ class GodotServer {
|
|
|
29436
29486
|
this.logDebug(`Launching Godot editor for project: ${project.value.path}`);
|
|
29437
29487
|
const editor = spawn(engine.value, editorArguments(project.value.path), {
|
|
29438
29488
|
stdio: "ignore",
|
|
29439
|
-
detached: true
|
|
29489
|
+
detached: true,
|
|
29490
|
+
env: { ...process.env, GDHARNESS_RUNTIME_DIR: runtimeDirectory() }
|
|
29440
29491
|
});
|
|
29441
29492
|
const started = await new Promise((resolve) => {
|
|
29442
29493
|
editor.once("spawn", () => {
|
|
@@ -29569,8 +29620,8 @@ class GodotServer {
|
|
|
29569
29620
|
}
|
|
29570
29621
|
running.process?.kill();
|
|
29571
29622
|
}
|
|
29572
|
-
spawnGame(godotPath, cmdArgs) {
|
|
29573
|
-
const child = spawn(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"] });
|
|
29623
|
+
spawnGame(godotPath, cmdArgs, env) {
|
|
29624
|
+
const child = spawn(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
|
|
29574
29625
|
const log = new GameLog;
|
|
29575
29626
|
const started = {
|
|
29576
29627
|
process: child,
|