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/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(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
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 readFileSync2 } from "node:fs";
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 = join(directory, entry.name);
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(readFileSync2(path, "utf8"));
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 = join(projectPath, ".godot", "global_script_class_cache.cfg");
24528
- if (!existsSync(cache)) {
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 = readFileSync2(cache, "utf8");
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 existsSync3 } from "node:fs";
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 existsSync2, readdirSync as readdirSync2, statSync } from "node:fs";
25630
- import { homedir } from "node:os";
25631
- import { join as join2 } 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";
25632
25828
  function resolveHomeDirectory() {
25633
25829
  try {
25634
- return homedir();
25830
+ return homedir2();
25635
25831
  } catch {
25636
25832
  return "";
25637
25833
  }
25638
25834
  }
25639
25835
  function scanDirectoryForGodotBinaries(directory, platform) {
25640
- if (!directory || !existsSync2(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 = join2(directory, name);
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) => join2(directory, m.name));
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" || existsSync3(path)) {
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 join3 } from "node:path";
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(join3(tmpdir(), "gdharness-params-"));
25838
- const paramsFile = join3(paramsDir, `${operation}.json`);
25839
- writeFileSync(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
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 readFileSync3 } from "node:fs";
26710
- import { join as join4 } from "node:path";
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(join4(directory, entry.name));
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 = join4(directory, entry.name);
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 readFileSync3(entryPath, "utf8").split(`
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 readFileSync4 } from "node:fs";
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 = readFileSync4(projectFilePath, "utf-8");
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 = readFileSync4(filePath, "utf-8");
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 existsSync4, readdirSync as readdirSync4, readFileSync as readFileSync5, unlinkSync } from "node:fs";
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 tmpdir2 } from "node:os";
27017
- import { join as join5, resolve as resolve4 } from "node:path";
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 join5(perUser ?? tmpdir2(), "gdharness");
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(readFileSync5(file, "utf8")));
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(directory = runtimeDirectory()) {
27111
- if (!existsSync4(directory)) {
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 = join5(directory, entry);
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.sort((a, b) => b.pid - a.pid);
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 run = this.spawnGame(engine.value, cmdArgs);
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 ? `The editor is running the ${status.addonVersion === "" ? "addon from before versions were reported" : status.addonVersion} addon while this server ships ${SERVER_VERSION}. Restart it with editor_launch restart to pick the new one up.` : undefined,
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,