gdharness 0.5.0 → 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/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"];
@@ -25776,7 +25972,7 @@ class GodotLocator {
25776
25972
  return known;
25777
25973
  }
25778
25974
  let ok = false;
25779
- if (path === "godot" || existsSync3(path)) {
25975
+ if (path === "godot" || existsSync4(path)) {
25780
25976
  try {
25781
25977
  await run(path, ["--version"]);
25782
25978
  ok = true;
@@ -25791,9 +25987,9 @@ class GodotLocator {
25791
25987
 
25792
25988
  // src/headless.ts
25793
25989
  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";
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";
25797
25993
  import { promisify as promisify2 } from "node:util";
25798
25994
  var run2 = promisify2(execFile2);
25799
25995
  function snakeCased(params) {
@@ -25834,9 +26030,9 @@ function reason(stdout, stderr) {
25834
26030
  return stdout.trim().split(/\r?\n/).at(-1) ?? "no output at all";
25835
26031
  }
25836
26032
  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");
26033
+ const paramsDir = mkdtempSync(join4(tmpdir2(), "gdharness-params-"));
26034
+ const paramsFile = join4(paramsDir, `${operation}.json`);
26035
+ writeFileSync2(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
25840
26036
  const args = [
25841
26037
  "--headless",
25842
26038
  "--path",
@@ -26706,8 +26902,8 @@ async function handleLSPTool(client, toolName, args) {
26706
26902
  }
26707
26903
 
26708
26904
  // src/project-scan.ts
26709
- import { readdirSync as readdirSync3, readFileSync as readFileSync3 } from "node:fs";
26710
- import { join as join4 } from "node:path";
26905
+ import { readdirSync as readdirSync3, readFileSync as readFileSync4 } from "node:fs";
26906
+ import { join as join5 } from "node:path";
26711
26907
  var SKIPPED = new Set([".git", ".godot", ".import", "node_modules"]);
26712
26908
  var ASSET_EXTENSIONS = new Set(["png", "jpg", "jpeg", "webp", "svg", "ttf", "otf", "wav", "mp3", "ogg"]);
26713
26909
  function projectStructure(projectPath) {
@@ -26718,7 +26914,7 @@ function projectStructure(projectPath) {
26718
26914
  continue;
26719
26915
  }
26720
26916
  if (entry.isDirectory()) {
26721
- visit(join4(directory, entry.name));
26917
+ visit(join5(directory, entry.name));
26722
26918
  } else if (entry.isFile()) {
26723
26919
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
26724
26920
  if (extension === "tscn") {
@@ -26760,7 +26956,7 @@ function searchProject(projectPath, options) {
26760
26956
  if (SKIPPED.has(entry.name)) {
26761
26957
  continue;
26762
26958
  }
26763
- const entryPath = join4(directory, entry.name);
26959
+ const entryPath = join5(directory, entry.name);
26764
26960
  if (entry.isDirectory()) {
26765
26961
  visit(entryPath);
26766
26962
  continue;
@@ -26771,7 +26967,7 @@ function searchProject(projectPath, options) {
26771
26967
  }
26772
26968
  result.summary.files_searched += 1;
26773
26969
  const matches = [];
26774
- for (const [index, line] of readFileSync3(entryPath, "utf8").split(`
26970
+ for (const [index, line] of readFileSync4(entryPath, "utf8").split(`
26775
26971
  `).entries()) {
26776
26972
  if (full()) {
26777
26973
  break;
@@ -26794,7 +26990,7 @@ function searchProject(projectPath, options) {
26794
26990
  }
26795
26991
 
26796
26992
  // src/resources.ts
26797
- import { readFileSync as readFileSync4 } from "node:fs";
26993
+ import { readFileSync as readFileSync5 } from "node:fs";
26798
26994
  import { extname, resolve as resolve3 } from "node:path";
26799
26995
  var STATIC_RESOURCES = [
26800
26996
  {
@@ -26965,7 +27161,7 @@ function readResourceText(uri, getProjectPath) {
26965
27161
  const parsedUri = parseGodotUri(uri);
26966
27162
  if (parsedUri.kind === "project-info") {
26967
27163
  const projectFilePath = resolveProjectFile(projectPath, "project.godot");
26968
- const rawProject = readFileSync4(projectFilePath, "utf-8");
27164
+ const rawProject = readFileSync5(projectFilePath, "utf-8");
26969
27165
  const parsedProject = parseProjectGodot(rawProject);
26970
27166
  return {
26971
27167
  mimeType: "application/json",
@@ -26974,7 +27170,7 @@ function readResourceText(uri, getProjectPath) {
26974
27170
  }
26975
27171
  const filePath = resolveProjectFile(projectPath, parsedUri.resourcePath);
26976
27172
  ensureAllowedExtension(parsedUri.kind, filePath);
26977
- const text = readFileSync4(filePath, "utf-8");
27173
+ const text = readFileSync5(filePath, "utf-8");
26978
27174
  return {
26979
27175
  mimeType: parsedUri.kind === "script" ? "text/x-gdscript" : "text/plain",
26980
27176
  text
@@ -27011,10 +27207,10 @@ function setupResourceHandlers(mcp, getProjectPath) {
27011
27207
  }
27012
27208
 
27013
27209
  // src/runtime-client.ts
27014
- import { existsSync as existsSync4, readdirSync as readdirSync4, readFileSync as readFileSync5, unlinkSync } from "node:fs";
27210
+ import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync6, unlinkSync } from "node:fs";
27015
27211
  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";
27212
+ import { tmpdir as tmpdir3 } from "node:os";
27213
+ import { join as join6, resolve as resolve4 } from "node:path";
27018
27214
 
27019
27215
  // src/tool-args.ts
27020
27216
  function asParams(value) {
@@ -27076,7 +27272,15 @@ function runtimeDirectory(variables = process.env) {
27076
27272
  return explicit;
27077
27273
  }
27078
27274
  const perUser = envValue("XDG_RUNTIME_DIR", variables);
27079
- return join5(perUser ?? tmpdir2(), "gdharness");
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)))];
27080
27284
  }
27081
27285
  function processAlive(pid) {
27082
27286
  try {
@@ -27089,7 +27293,7 @@ function processAlive(pid) {
27089
27293
  function parseAnnouncement(file, pid) {
27090
27294
  let fields;
27091
27295
  try {
27092
- fields = asParams(JSON.parse(readFileSync5(file, "utf8")));
27296
+ fields = asParams(JSON.parse(readFileSync6(file, "utf8")));
27093
27297
  } catch {
27094
27298
  return null;
27095
27299
  }
@@ -27107,8 +27311,19 @@ function parseAnnouncement(file, pid) {
27107
27311
  file
27108
27312
  };
27109
27313
  }
27110
- function discoverRuntimes(directory = runtimeDirectory()) {
27111
- if (!existsSync4(directory)) {
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)) {
27112
27327
  return [];
27113
27328
  }
27114
27329
  const found = [];
@@ -27117,7 +27332,7 @@ function discoverRuntimes(directory = runtimeDirectory()) {
27117
27332
  if (!match) {
27118
27333
  continue;
27119
27334
  }
27120
- const file = join5(directory, entry);
27335
+ const file = join6(directory, entry);
27121
27336
  const pid = Number.parseInt(match[1] ?? "", 10);
27122
27337
  const endpoint = processAlive(pid) ? parseAnnouncement(file, pid) : null;
27123
27338
  if (endpoint) {
@@ -27128,7 +27343,7 @@ function discoverRuntimes(directory = runtimeDirectory()) {
27128
27343
  } catch {}
27129
27344
  }
27130
27345
  }
27131
- return found.sort((a, b) => b.pid - a.pid);
27346
+ return found;
27132
27347
  }
27133
27348
  function describe2(endpoint) {
27134
27349
  return `pid ${endpoint.pid} on ${endpoint.address}:${endpoint.port} (${endpoint.project.name || "unnamed"} at ${endpoint.project.path})`;
@@ -28153,187 +28368,6 @@ function buildToolDefinitions() {
28153
28368
  });
28154
28369
  }
28155
28370
 
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
28371
  // src/server.ts
28338
28372
  var UPDATE_NOTICE_EVERY = 500;
28339
28373
  var FEEDBACK_NOTICE_EVERY = 250;
@@ -29325,7 +29359,7 @@ class GodotServer {
29325
29359
  addonIsStale: status.connected ? stale : undefined,
29326
29360
  bridgeAvailable: this.bridgeStartupError === null,
29327
29361
  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,
29362
+ staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
29329
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,
29330
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
29331
29365
  };
@@ -29403,6 +29437,7 @@ class GodotServer {
29403
29437
  addonVersion: now.addonVersion,
29404
29438
  serverVersion: SERVER_VERSION,
29405
29439
  addonIsStale: now.addonVersion !== SERVER_VERSION,
29440
+ staleNote: addonMismatch(now.addonVersion, SERVER_VERSION),
29406
29441
  tookMs: Date.now() - began
29407
29442
  });
29408
29443
  }
@@ -29436,7 +29471,8 @@ class GodotServer {
29436
29471
  this.logDebug(`Launching Godot editor for project: ${project.value.path}`);
29437
29472
  const editor = spawn(engine.value, editorArguments(project.value.path), {
29438
29473
  stdio: "ignore",
29439
- detached: true
29474
+ detached: true,
29475
+ env: { ...process.env, GDHARNESS_RUNTIME_DIR: runtimeDirectory() }
29440
29476
  });
29441
29477
  const started = await new Promise((resolve) => {
29442
29478
  editor.once("spawn", () => {