gdharness 0.13.35 → 0.13.37

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.
Files changed (3) hide show
  1. package/build/cli.js +556 -234
  2. package/build/index.js +396 -132
  3. package/package.json +1 -1
package/build/index.js CHANGED
@@ -9846,9 +9846,23 @@ function runLine(runner, version, rest = "") {
9846
9846
  }
9847
9847
 
9848
9848
  // src/update-check.ts
9849
- var REGISTRY = "https://registry.npmjs.org/gdharness/latest";
9849
+ var REGISTRY_ROOT = "https://registry.npmjs.org";
9850
+ function packageUrl(root) {
9851
+ return `${root.replace(/\/+$/, "")}/gdharness/latest`;
9852
+ }
9853
+ function registryFor(environment) {
9854
+ const named = environment["GDHARNESS_REGISTRY"] ?? "";
9855
+ if (named === "") {
9856
+ return packageUrl(REGISTRY_ROOT);
9857
+ }
9858
+ try {
9859
+ return new URL(named).protocol === "https:" ? packageUrl(named) : packageUrl(REGISTRY_ROOT);
9860
+ } catch {
9861
+ return packageUrl(REGISTRY_ROOT);
9862
+ }
9863
+ }
9850
9864
  var RELEASES = "https://github.com/Aureliolo/gdharness/releases/tag";
9851
- var CACHE_MS = 4 * 60 * 60 * 1000;
9865
+ var CACHE_MS = 10 * 60 * 1000;
9852
9866
  var REQUEST_TIMEOUT_MS = 1e4;
9853
9867
  var MAX_BODY_BYTES = 1 << 20;
9854
9868
  var FIRST_RETRY_MS = 30000;
@@ -9923,8 +9937,8 @@ function isNewer(candidate, current) {
9923
9937
  }
9924
9938
  return !left.prerelease && right.prerelease;
9925
9939
  }
9926
- async function fetchLatest() {
9927
- const response = await fetch(REGISTRY, {
9940
+ async function fetchLatest(registry) {
9941
+ const response = await fetch(registry, {
9928
9942
  headers: { accept: "application/vnd.npm.install-v1+json, application/json" },
9929
9943
  redirect: "error",
9930
9944
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
@@ -9956,16 +9970,19 @@ async function fetchLatest() {
9956
9970
  class UpdateCheck {
9957
9971
  latest = null;
9958
9972
  checking = false;
9973
+ asked = false;
9959
9974
  checkedAt = 0;
9960
9975
  retryAt = 0;
9961
9976
  backoffMs = FIRST_RETRY_MS;
9962
9977
  enabled;
9963
9978
  current;
9964
9979
  cachePath;
9965
- constructor(current, environment = process.env) {
9980
+ ask;
9981
+ constructor(current, environment = process.env, ask = () => fetchLatest(registryFor(environment))) {
9966
9982
  this.current = current;
9967
9983
  this.enabled = (environment["GDHARNESS_NO_UPDATE_CHECK"] ?? "") === "";
9968
9984
  this.cachePath = cacheFile(environment);
9985
+ this.ask = ask;
9969
9986
  const cached = this.enabled ? readCache(this.cachePath) : null;
9970
9987
  if (cached !== null) {
9971
9988
  this.latest = cached.latest;
@@ -9973,11 +9990,12 @@ class UpdateCheck {
9973
9990
  }
9974
9991
  }
9975
9992
  refresh(now = Date.now()) {
9976
- if (!this.enabled || this.checking || now < this.retryAt || !this.stale(now)) {
9993
+ if (!this.enabled || this.checking || now < this.retryAt || this.asked && !this.stale(now)) {
9977
9994
  return;
9978
9995
  }
9996
+ this.asked = true;
9979
9997
  this.checking = true;
9980
- fetchLatest().then((version) => {
9998
+ this.ask().then((version) => {
9981
9999
  if (version === null) {
9982
10000
  this.scheduleRetry(now);
9983
10001
  return;
@@ -10107,15 +10125,26 @@ function filledTemplate(where, message, godotVersion) {
10107
10125
  ].join(`
10108
10126
  `);
10109
10127
  }
10110
- function defectReport(where, error, godotVersion) {
10128
+ function defectReport(where, error, context = {}) {
10129
+ const { godotVersion, seenBefore = 0 } = context;
10111
10130
  const message = errorMessage(error);
10112
10131
  const title = `Defect ${defectSignature(where, message)}: ${where}`;
10113
10132
  const url = `${NEW_ISSUE}?template=bug_report.md&title=${encodeURIComponent(title)}` + `&body=${encodeURIComponent(filledTemplate(where, message, godotVersion))}`;
10114
10133
  const width = Math.max(...facts(where, message, godotVersion).map(([label]) => (label ?? "").length));
10115
10134
  return [
10116
- "gdharness failed in a way it does not model. This is a defect in gdharness rather than",
10117
- "anything about the call: the arguments did not cause it, and sending it again will not",
10118
- "change it.",
10135
+ "gdharness failed in a way it does not model. Whatever went wrong underneath, it reached you",
10136
+ "as a crash rather than as a refusal naming what would have worked, and that much is a defect",
10137
+ "in gdharness however the rest of it turns out.",
10138
+ "",
10139
+ ...seenBefore === 0 ? [
10140
+ "Send the call once more before anything else. Some of what reaches this line is a file",
10141
+ "another process had open, or a connection that dropped, and those are gone by the next",
10142
+ "attempt. If the same Signature comes back, it is not one of those, and repeating it",
10143
+ "further only spends turns."
10144
+ ] : [
10145
+ `You have had this exact failure ${seenBefore === 1 ? "once" : `${seenBefore} times`} in this session already, so it is a`,
10146
+ "state rather than a passing one, and sending the call again will not change it."
10147
+ ],
10119
10148
  "",
10120
10149
  ...facts(where, message, godotVersion).map(([label, value]) => ` ${(label ?? "").padEnd(width)} ${value ?? ""}`),
10121
10150
  "",
@@ -10146,15 +10175,15 @@ import {
10146
10175
  closeSync,
10147
10176
  existsSync as existsSync9,
10148
10177
  fstatSync,
10149
- mkdirSync as mkdirSync5,
10178
+ mkdirSync as mkdirSync6,
10150
10179
  mkdtempSync as mkdtempSync2,
10151
10180
  openSync as openSync2,
10152
10181
  readdirSync as readdirSync6,
10153
- readFileSync as readFileSync11,
10182
+ readFileSync as readFileSync12,
10154
10183
  readSync
10155
10184
  } from "node:fs";
10156
10185
  import { tmpdir as tmpdir4 } from "node:os";
10157
- import { basename as basename2, dirname as dirname4, join as join11, normalize as normalize2 } from "node:path";
10186
+ import { basename as basename2, dirname as dirname5, join as join12, normalize as normalize2 } from "node:path";
10158
10187
  import { setTimeout as delay3 } from "node:timers/promises";
10159
10188
  import { fileURLToPath as fileURLToPath2 } from "node:url";
10160
10189
  import { promisify as promisify4 } from "node:util";
@@ -24886,7 +24915,7 @@ function staleAnalysisNote(contradicted, dependsOn) {
24886
24915
  }
24887
24916
  const types = new Set(contradicted.map((one) => one.type));
24888
24917
  const declaring = [...new Set(contradicted.map((one) => one.declaredIn))].sort();
24889
- const lever = dependsOn.length === 0 ? ", and the named types depend on no other global class, so that lever is not available here. " + "That is the common case rather than the awkward one: an edit near a type that depends on " + "something refreshes it before the fault is noticed, which is why a type with no " + "dependencies is where this turns up" : `, so changing ${[...dependsOn].sort().join(", ")} and rescanning is worth a try`;
24918
+ const lever = dependsOn.length === 0 ? ", and the named types depend on no other global class, so that lever is not available here. " + "That is the common case rather than the awkward one: an edit near a type that depends on " + "something refreshes it before the fault is noticed, which is why a type with no " + "dependencies is where this turns up" : `, so changing ${[...dependsOn].sort().join(" or ")} and then rescanning is worth a try`;
24890
24919
  return `The editor is reporting against an older copy of ${types.size === 1 ? "a type" : "some types"} ` + "named under contradictedByTheFile. Each member listed is declared in the file the class cache " + "points at, so those diagnostics are wrong however the code is written. Run editor_rescan first: " + "it costs about half a second, and it has cleared this in every reproduction measured since the " + "scan timing was fixed: both in one project, the scan returning in 243ms and 275ms, and one in " + "another where the same call also rebuilt the copy. " + "The fault behind it is that a script the editor has loaded keeps the copy it built, and nothing " + `rebuilds that copy when the file changes; ${declaring.length === 1 ? `editor_rescan with reloadScript set to ${declaring[0]} recompiles that copy` : `reloadScript takes one script, so a call each for ${declaring.join(" and ")} recompiles those copies`} from the file into the same ` + "object and answers with the members it has afterwards under reloadedMethods. Read that and the " + "next diagnostics as two readings, because they are of two things: measured in one window on " + "one editor, the analyser resolved a call to a newly added method while the built copy of that " + "same script did not have it. So the reload is worth doing and is not what clears these; the " + "scan is. The other lever measured is that a held type is refreshed when something it " + `depends on changes rather than when it changes itself${lever}. editor_launch restart has always ` + "worked and costs a window. project_import refresh_classes does not, and answers added: [] while " + "this is happening.";
24891
24920
  }
24892
24921
  function uncachedClassNote(types) {
@@ -25574,6 +25603,11 @@ function announced(line) {
25574
25603
  return { severity: match[2] === "WARNING" ? "warning" : "error", text: match[3] ?? "" };
25575
25604
  }
25576
25605
  var STREAMS = ["stdout", "stderr", "transcript"];
25606
+ var ADMITTED = {
25607
+ error: new Set(["error"]),
25608
+ warning: new Set(["error", "warning"]),
25609
+ info: new Set(["error", "warning", "info"])
25610
+ };
25577
25611
  function forAnswer(entries) {
25578
25612
  return entries.map((entry) => entry.detail.length === 0 ? { index: entry.index, severity: entry.severity, source: entry.source, text: entry.text } : entry);
25579
25613
  }
@@ -25587,7 +25621,7 @@ class GameLog {
25587
25621
  };
25588
25622
  partial = { stdout: "", stderr: "", transcript: "" };
25589
25623
  lastHeadline = null;
25590
- cursor = 0;
25624
+ seen = { error: 0, warning: 0, info: 0 };
25591
25625
  append(source, chunk) {
25592
25626
  const decoded = typeof chunk === "string" ? chunk : this.decoders[source].write(chunk);
25593
25627
  const lines = (this.partial[source] + decoded).split(`
@@ -25644,10 +25678,14 @@ class GameLog {
25644
25678
  return this.entries.filter((entry) => entry.severity === severity).length;
25645
25679
  }
25646
25680
  select(options) {
25647
- const floor = options.sinceLastCall ? this.cursor : 0;
25648
- this.cursor = this.entries.length;
25649
- const wanted = new Set(options.severity === "error" ? ["error"] : options.severity === "warning" ? ["error", "warning"] : ["error", "warning", "info"]);
25681
+ const floor = options.sinceLastCall ? this.seen[options.severity] : 0;
25682
+ const wanted = ADMITTED[options.severity];
25650
25683
  const needle = options.contains?.toLowerCase();
25684
+ if (needle === undefined) {
25685
+ for (const severity of wanted) {
25686
+ this.seen[severity] = this.entries.length;
25687
+ }
25688
+ }
25651
25689
  const matching = this.entries.slice(floor).filter((entry) => {
25652
25690
  if (!wanted.has(entry.severity)) {
25653
25691
  return false;
@@ -26353,6 +26391,10 @@ function userDataIn(home, variables = process.env) {
26353
26391
  }
26354
26392
  return carried;
26355
26393
  }
26394
+ function savesStayPut(platform = process.platform) {
26395
+ return platform === "darwin";
26396
+ }
26397
+ var SAVES_NOT_MOVED_NOTE = "On macOS the engine reads user:// off HOME and ignores the variables that move it elsewhere, measured on Godot 4.7.2, so this run wrote its saves where the player keeps theirs. A suite that saves a game has written into the same folder as the copy being played, and one that tidies up after itself may have removed real saves.";
26356
26398
 
26357
26399
  // src/godot-path.ts
26358
26400
  var run = promisify(execFile);
@@ -26361,47 +26403,65 @@ class GodotLocator {
26361
26403
  answered = new Map;
26362
26404
  found = null;
26363
26405
  async find() {
26364
- if (this.found !== null && await this.answers(this.found)) {
26365
- return this.found;
26406
+ if (this.found !== null && await this.whyNot(this.found) === null) {
26407
+ return { ok: true, path: this.found };
26366
26408
  }
26367
26409
  this.found = null;
26368
26410
  const named = envValue("GODOT_PATH");
26369
26411
  if (named !== undefined) {
26370
26412
  const path = normalize(named);
26371
- if (await this.answers(path)) {
26413
+ const reason = await this.whyNot(path);
26414
+ if (reason === null) {
26372
26415
  this.found = path;
26416
+ return { ok: true, path };
26373
26417
  }
26374
- return this.found;
26418
+ return { ok: false, named: path, reason };
26375
26419
  }
26376
26420
  for (const candidate of godotCandidates(process.platform, resolveHomeDirectory())) {
26377
26421
  const path = normalize(candidate);
26378
- if (await this.answers(path)) {
26422
+ if (await this.whyNot(path) === null) {
26379
26423
  this.found = path;
26380
- return path;
26424
+ return { ok: true, path };
26381
26425
  }
26382
26426
  }
26383
- return null;
26427
+ return { ok: false, named: null };
26384
26428
  }
26385
- static ADVICE = [
26386
- "Set GODOT_PATH to the Godot 4 executable",
26387
- "Or install Godot where it is usually found, such as /usr/bin/godot or C:\\Program Files\\Godot"
26388
- ];
26389
- async answers(path) {
26429
+ static refusal(located) {
26430
+ if (located.named === null) {
26431
+ return {
26432
+ message: "No Godot executable found.",
26433
+ advice: [
26434
+ "Set GODOT_PATH to the Godot 4 executable",
26435
+ "Or install Godot where it is usually found, such as /usr/bin/godot or C:\\Program Files\\Godot"
26436
+ ]
26437
+ };
26438
+ }
26439
+ return {
26440
+ message: `GODOT_PATH is set to ${located.named}, which ${located.reason ?? "does not answer"}.`,
26441
+ advice: [
26442
+ "Point GODOT_PATH at a Godot 4 executable that answers --version",
26443
+ "Or unset it, and the usual install locations are searched instead"
26444
+ ]
26445
+ };
26446
+ }
26447
+ async whyNot(path) {
26390
26448
  const known = this.answered.get(path);
26391
26449
  if (known !== undefined) {
26392
26450
  return known;
26393
26451
  }
26394
- let ok = false;
26395
- if (path === "godot" || existsSync5(path)) {
26452
+ let reason;
26453
+ if (path !== "godot" && !existsSync5(path)) {
26454
+ reason = "does not exist";
26455
+ } else {
26396
26456
  try {
26397
26457
  await run(path, ["--version"]);
26398
- ok = true;
26399
- } catch {
26400
- ok = false;
26458
+ reason = null;
26459
+ } catch (error) {
26460
+ reason = `does not answer --version: ${errorMessage(error)}`;
26401
26461
  }
26402
26462
  }
26403
- this.answered.set(path, ok);
26404
- return ok;
26463
+ this.answered.set(path, reason);
26464
+ return reason;
26405
26465
  }
26406
26466
  }
26407
26467
 
@@ -26659,15 +26719,36 @@ function filledTemplate2(where, message, godotVersion) {
26659
26719
  ].join(`
26660
26720
  `);
26661
26721
  }
26662
- function defectReport2(where, error, godotVersion) {
26722
+
26723
+ class DefectsSeen {
26724
+ counts = new Map;
26725
+ before(where, message) {
26726
+ const signature = defectSignature2(where, message);
26727
+ const seen = this.counts.get(signature) ?? 0;
26728
+ this.counts.set(signature, seen + 1);
26729
+ return seen;
26730
+ }
26731
+ }
26732
+ function defectReport2(where, error, context = {}) {
26733
+ const { godotVersion, seenBefore = 0 } = context;
26663
26734
  const message = errorMessage(error);
26664
26735
  const title = `Defect ${defectSignature2(where, message)}: ${where}`;
26665
26736
  const url = `${NEW_ISSUE2}?template=bug_report.md&title=${encodeURIComponent(title)}` + `&body=${encodeURIComponent(filledTemplate2(where, message, godotVersion))}`;
26666
26737
  const width = Math.max(...facts2(where, message, godotVersion).map(([label]) => (label ?? "").length));
26667
26738
  return [
26668
- "gdharness failed in a way it does not model. This is a defect in gdharness rather than",
26669
- "anything about the call: the arguments did not cause it, and sending it again will not",
26670
- "change it.",
26739
+ "gdharness failed in a way it does not model. Whatever went wrong underneath, it reached you",
26740
+ "as a crash rather than as a refusal naming what would have worked, and that much is a defect",
26741
+ "in gdharness however the rest of it turns out.",
26742
+ "",
26743
+ ...seenBefore === 0 ? [
26744
+ "Send the call once more before anything else. Some of what reaches this line is a file",
26745
+ "another process had open, or a connection that dropped, and those are gone by the next",
26746
+ "attempt. If the same Signature comes back, it is not one of those, and repeating it",
26747
+ "further only spends turns."
26748
+ ] : [
26749
+ `You have had this exact failure ${seenBefore === 1 ? "once" : `${seenBefore} times`} in this session already, so it is a`,
26750
+ "state rather than a passing one, and sending the call again will not change it."
26751
+ ],
26671
26752
  "",
26672
26753
  ...facts2(where, message, godotVersion).map(([label, value]) => ` ${(label ?? "").padEnd(width)} ${value ?? ""}`),
26673
26754
  "",
@@ -27735,6 +27816,11 @@ function settingKeys(content) {
27735
27816
  return named;
27736
27817
  }
27737
27818
  function settingsDroppedReport(before, after) {
27819
+ if (before === null || after === null) {
27820
+ return {
27821
+ settingsNote: `project.godot could not be read ${before === null ? "before" : "after"} this, so nothing can` + " be said about what the editor saved away. Opening a project rewrites the file and drops any" + " key sitting at its own default, and this is the only thing that would have named them." + " Read the file yourself if a setting was pinned at a default on purpose."
27822
+ };
27823
+ }
27738
27824
  const dropped = [...before].filter(([key]) => !after.has(key));
27739
27825
  if (dropped.length === 0) {
27740
27826
  return {};
@@ -27848,16 +27934,55 @@ function setupResourceHandlers(mcp, getProjectPath) {
27848
27934
  });
27849
27935
  }
27850
27936
 
27937
+ // src/restart-note.ts
27938
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync8, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "node:fs";
27939
+ import { dirname as dirname3, join as join8 } from "node:path";
27940
+ function restartNotePath(projectPath) {
27941
+ return join8(projectPath, ".godot", "gdharness-restart.json");
27942
+ }
27943
+ function noteRestartBegun(note) {
27944
+ const path = restartNotePath(note.projectPath);
27945
+ try {
27946
+ mkdirSync3(dirname3(path), { recursive: true });
27947
+ writeFileSync4(path, `${JSON.stringify(note, null, 2)}
27948
+ `, "utf8");
27949
+ } catch (error) {
27950
+ console.error(`[SERVER] Could not record the restart at ${path}: ${errorMessage(error)}`);
27951
+ }
27952
+ }
27953
+ function restartOwed(projectPath) {
27954
+ try {
27955
+ const parsed = JSON.parse(readFileSync8(restartNotePath(projectPath), "utf8"));
27956
+ if (typeof parsed !== "object" || parsed === null) {
27957
+ return null;
27958
+ }
27959
+ const said = parsed;
27960
+ if (typeof said.quitAt !== "string" || typeof said.byPid !== "number" || typeof said.ports !== "object") {
27961
+ return null;
27962
+ }
27963
+ return said;
27964
+ } catch {
27965
+ return null;
27966
+ }
27967
+ }
27968
+ function restartSettled(projectPath) {
27969
+ try {
27970
+ rmSync3(restartNotePath(projectPath), { force: true });
27971
+ } catch (error) {
27972
+ console.error(`[SERVER] Could not clear the restart note: ${errorMessage(error)}`);
27973
+ }
27974
+ }
27975
+
27851
27976
  // src/run-record.ts
27852
27977
  import { execFileSync } from "node:child_process";
27853
- import { mkdirSync as mkdirSync3, openSync, readdirSync as readdirSync5, readFileSync as readFileSync9, rmSync as rmSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "node:fs";
27854
- import { basename, join as join9 } from "node:path";
27978
+ import { mkdirSync as mkdirSync4, openSync, readdirSync as readdirSync5, readFileSync as readFileSync10, rmSync as rmSync4, statSync as statSync3, writeFileSync as writeFileSync5 } from "node:fs";
27979
+ import { basename, join as join10 } from "node:path";
27855
27980
 
27856
27981
  // src/runtime-client.ts
27857
- import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as readFileSync8, unlinkSync } from "node:fs";
27982
+ import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as readFileSync9, unlinkSync } from "node:fs";
27858
27983
  import { createConnection as createConnection3 } from "node:net";
27859
27984
  import { tmpdir as tmpdir3 } from "node:os";
27860
- import { join as join8, resolve as resolve4 } from "node:path";
27985
+ import { join as join9, resolve as resolve4 } from "node:path";
27861
27986
  import { setTimeout as delay2 } from "node:timers/promises";
27862
27987
 
27863
27988
  // src/tool-args.ts
@@ -27925,13 +28050,13 @@ function runtimeDirectory(variables = process.env) {
27925
28050
  return explicit;
27926
28051
  }
27927
28052
  const perUser = envValue("XDG_RUNTIME_DIR", variables);
27928
- return join8(perUser ?? tmpdir3(), "gdharness");
28053
+ return join9(perUser ?? tmpdir3(), "gdharness");
27929
28054
  }
27930
28055
  function runtimeDirectories(variables = process.env) {
27931
28056
  const candidates = [runtimeDirectory(variables)];
27932
28057
  const fallbacks = process.platform === "win32" ? [envValue("SystemRoot", variables) ?? envValue("windir", variables) ?? "C:\\Windows"] : ["/tmp", "/var/tmp"];
27933
28058
  for (const base of fallbacks) {
27934
- candidates.push(join8(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
28059
+ candidates.push(join9(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
27935
28060
  }
27936
28061
  return [...new Set(candidates.map((path) => resolve4(path)))];
27937
28062
  }
@@ -27946,7 +28071,7 @@ function processAlive(pid) {
27946
28071
  function parseAnnouncement(file, pid) {
27947
28072
  let fields;
27948
28073
  try {
27949
- fields = asParams(JSON.parse(readFileSync8(file, "utf8")));
28074
+ fields = asParams(JSON.parse(readFileSync9(file, "utf8")));
27950
28075
  } catch {
27951
28076
  return { kind: "rubbish" };
27952
28077
  }
@@ -27984,6 +28109,31 @@ function runtimesAnnounced(directories = runtimeDirectories()) {
27984
28109
  function discoverRuntimes(directories = runtimeDirectories()) {
27985
28110
  return runtimesAnnounced(directories).running;
27986
28111
  }
28112
+ var GONE_WINDOW_MS = 120000;
28113
+ var GONE_KEPT = 32;
28114
+ var gone = [];
28115
+ function wentAway(file, pid) {
28116
+ let project = "";
28117
+ try {
28118
+ const fields = asParams(JSON.parse(readFileSync9(file, "utf8")));
28119
+ project = readString(readParams(fields, "project") ?? {}, "path") ?? "";
28120
+ } catch {
28121
+ return;
28122
+ }
28123
+ if (project === "") {
28124
+ return;
28125
+ }
28126
+ gone.unshift({ pid, project, noticedAt: Date.now() });
28127
+ gone.length = Math.min(gone.length, GONE_KEPT);
28128
+ }
28129
+ function announcedAndGone(projectPath) {
28130
+ if (projectPath === undefined) {
28131
+ return [];
28132
+ }
28133
+ const since = Date.now() - GONE_WINDOW_MS;
28134
+ const wanted = resolve4(projectPath);
28135
+ return gone.filter((one) => one.noticedAt >= since && resolve4(one.project) === wanted);
28136
+ }
27987
28137
  function announcedIn(directory) {
27988
28138
  if (!existsSync7(directory)) {
27989
28139
  return [];
@@ -27994,10 +28144,14 @@ function announcedIn(directory) {
27994
28144
  if (!match) {
27995
28145
  continue;
27996
28146
  }
27997
- const file = join8(directory, entry);
28147
+ const file = join9(directory, entry);
27998
28148
  const pid = Number.parseInt(match[1] ?? "", 10);
27999
- const announced = processAlive(pid) ? parseAnnouncement(file, pid) : { kind: "rubbish" };
28149
+ const alive = processAlive(pid);
28150
+ const announced = alive ? parseAnnouncement(file, pid) : { kind: "rubbish" };
28000
28151
  if (announced.kind === "rubbish") {
28152
+ if (!alive) {
28153
+ wentAway(file, pid);
28154
+ }
28001
28155
  try {
28002
28156
  unlinkSync(file);
28003
28157
  } catch {}
@@ -28038,6 +28192,16 @@ function chooseRuntime(endpoints, projectPath, unspoken = []) {
28038
28192
  if (unspoken.length > 0) {
28039
28193
  return { problem: tooNew(unspoken) };
28040
28194
  }
28195
+ const went = announcedAndGone(projectPath);
28196
+ const first = went[0];
28197
+ if (first !== undefined) {
28198
+ const ago = Math.max(1, Math.round((Date.now() - first.noticedAt) / 1000));
28199
+ const others = went.length - 1;
28200
+ const earlier = others === 0 ? "" : ` ${others} earlier ${others === 1 ? "one" : "ones"} went the same way.`;
28201
+ return {
28202
+ problem: `A game with the runtime addon announced itself and its process is gone: pid ${first.pid}` + `${first.project === "" ? "" : ` for ${first.project}`}, noticed ${ago}s ago.${earlier}` + " It quit or was ended rather than never starting, so editor_output has what it printed" + " on the way, including whatever it broke on. Start another with editor_run once you have read it."
28203
+ };
28204
+ }
28041
28205
  return {
28042
28206
  problem: "No game with the runtime addon is running. Start one with editor_run, or play the project from the editor with the addon enabled."
28043
28207
  };
@@ -28155,33 +28319,33 @@ function runtimeRequest(endpoint, command, params, timeoutMs) {
28155
28319
 
28156
28320
  // src/run-record.ts
28157
28321
  function runsDirectory() {
28158
- return join9(runtimeDirectory(), "runs");
28322
+ return join10(runtimeDirectory(), "runs");
28159
28323
  }
28160
28324
  function recordPath() {
28161
- return join9(runsDirectory(), "run.json");
28325
+ return join10(runsDirectory(), "run.json");
28162
28326
  }
28163
28327
  function recordPaths() {
28164
- return runtimeDirectories().map((directory) => join9(directory, "runs", "run.json"));
28328
+ return runtimeDirectories().map((directory) => join10(directory, "runs", "run.json"));
28165
28329
  }
28166
28330
  function openTranscript(startedAt) {
28167
- mkdirSync3(runsDirectory(), { recursive: true });
28168
- const path = join9(runsDirectory(), `run-${startedAt}.log`);
28331
+ mkdirSync4(runsDirectory(), { recursive: true });
28332
+ const path = join10(runsDirectory(), `run-${startedAt}.log`);
28169
28333
  return { path, fd: openSync(path, "a") };
28170
28334
  }
28171
28335
  function writeRunRecord(record) {
28172
- mkdirSync3(runsDirectory(), { recursive: true });
28173
- writeFileSync4(recordPath(), JSON.stringify(record, null, 2), "utf8");
28336
+ mkdirSync4(runsDirectory(), { recursive: true });
28337
+ writeFileSync5(recordPath(), JSON.stringify(record, null, 2), "utf8");
28174
28338
  }
28175
28339
  function editorNotePath() {
28176
- return join9(runsDirectory(), "editor-run.json");
28340
+ return join10(runsDirectory(), "editor-run.json");
28177
28341
  }
28178
28342
  function writeEditorRunNote(note) {
28179
- mkdirSync3(runsDirectory(), { recursive: true });
28180
- writeFileSync4(editorNotePath(), JSON.stringify(note, null, 2), "utf8");
28343
+ mkdirSync4(runsDirectory(), { recursive: true });
28344
+ writeFileSync5(editorNotePath(), JSON.stringify(note, null, 2), "utf8");
28181
28345
  }
28182
28346
  function readEditorRunNote() {
28183
28347
  for (const directory of runtimeDirectories()) {
28184
- const note = editorNoteAt(join9(directory, "runs", "editor-run.json"));
28348
+ const note = editorNoteAt(join10(directory, "runs", "editor-run.json"));
28185
28349
  if (note !== null) {
28186
28350
  return note;
28187
28351
  }
@@ -28191,7 +28355,7 @@ function readEditorRunNote() {
28191
28355
  function editorNoteAt(path) {
28192
28356
  let parsed;
28193
28357
  try {
28194
- parsed = JSON.parse(readFileSync9(path, "utf8"));
28358
+ parsed = JSON.parse(readFileSync10(path, "utf8"));
28195
28359
  } catch {
28196
28360
  return null;
28197
28361
  }
@@ -28214,7 +28378,7 @@ function recordRunEnded(pid, exitCode) {
28214
28378
  return;
28215
28379
  }
28216
28380
  try {
28217
- writeFileSync4(path, JSON.stringify({ ...record, exitCode }, null, 2), "utf8");
28381
+ writeFileSync5(path, JSON.stringify({ ...record, exitCode }, null, 2), "utf8");
28218
28382
  } catch {}
28219
28383
  }
28220
28384
  function readRunRecord() {
@@ -28229,7 +28393,7 @@ function readRunRecord() {
28229
28393
  function recordAt(path) {
28230
28394
  let parsed;
28231
28395
  try {
28232
- parsed = JSON.parse(readFileSync9(path, "utf8"));
28396
+ parsed = JSON.parse(readFileSync10(path, "utf8"));
28233
28397
  } catch {
28234
28398
  return null;
28235
28399
  }
@@ -28256,23 +28420,32 @@ function recordAt(path) {
28256
28420
  }
28257
28421
  function runningAs(pid) {
28258
28422
  if (process.platform === "win32") {
28259
- const line = windowsCommandLine(pid);
28260
- if (line !== null) {
28261
- return { kind: "commandLine", text: line };
28423
+ const answer = windowsCommandLine(pid);
28424
+ if (answer !== null) {
28425
+ const [, ...rest] = answer.split(`
28426
+ `);
28427
+ const line = rest.join(`
28428
+ `).trim();
28429
+ const began = startedAt(pid, answer);
28430
+ if (line !== "") {
28431
+ return { kind: "commandLine", text: line, ...began === null ? {} : { startedAt: began } };
28432
+ }
28262
28433
  }
28263
28434
  const image = windowsImage(pid);
28264
28435
  return image === null ? null : { kind: "image", text: image };
28265
28436
  }
28266
28437
  try {
28438
+ const began = startedAt(pid, null);
28439
+ const when = began === null ? {} : { startedAt: began };
28267
28440
  if (process.platform === "linux") {
28268
- const raw = readFileSync9(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
28269
- return raw === "" ? null : { kind: "commandLine", text: raw };
28441
+ const raw = readFileSync10(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
28442
+ return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
28270
28443
  }
28271
28444
  const args = execFileSync("ps", ["-p", String(pid), "-o", "args="], {
28272
28445
  encoding: "utf8",
28273
28446
  timeout: 15000
28274
28447
  }).trim();
28275
- return args === "" ? null : { kind: "commandLine", text: args };
28448
+ return args === "" ? null : { kind: "commandLine", text: args, ...when };
28276
28449
  } catch {
28277
28450
  return null;
28278
28451
  }
@@ -28311,13 +28484,30 @@ function windowsCommandLine(pid) {
28311
28484
  "-NoProfile",
28312
28485
  "-NonInteractive",
28313
28486
  "-Command",
28314
- `(Get-CimInstance Win32_Process -Filter "ProcessId=${pid}").CommandLine`
28487
+ `$p = Get-CimInstance Win32_Process -Filter "ProcessId=${pid}"; if ($p) { $p.CreationDate.ToUniversalTime().ToString("o"); $p.CommandLine }`
28315
28488
  ], { encoding: "utf8", timeout: 15000, windowsHide: true }).trim();
28316
28489
  return answer === "" ? null : answer;
28317
28490
  } catch {
28318
28491
  return null;
28319
28492
  }
28320
28493
  }
28494
+ function startedAt(pid, windowsAnswer) {
28495
+ if (process.platform === "win32") {
28496
+ const when = Date.parse(windowsAnswer?.split(`
28497
+ `)[0]?.trim() ?? "");
28498
+ return Number.isNaN(when) ? null : when;
28499
+ }
28500
+ try {
28501
+ const said = execFileSync("ps", ["-p", String(pid), "-o", "lstart="], {
28502
+ encoding: "utf8",
28503
+ timeout: 15000
28504
+ }).trim();
28505
+ const when = Date.parse(said);
28506
+ return said === "" || Number.isNaN(when) ? null : when;
28507
+ } catch {
28508
+ return null;
28509
+ }
28510
+ }
28321
28511
  function windowsImage(pid) {
28322
28512
  try {
28323
28513
  const csv = execFileSync("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
@@ -28337,10 +28527,14 @@ function couldStillBeTheRecordedRun(record) {
28337
28527
  return judgeRun(record, runningAs(record.pid), "possible");
28338
28528
  }
28339
28529
  var AN_EDITOR = /(?:^|\s)(?:-e|--editor)(?:\s|$)/;
28530
+ var SAME_RUN_WINDOW_MS = 1e4;
28340
28531
  function judgeRun(record, running, needed) {
28341
28532
  if (running === null) {
28342
28533
  return false;
28343
28534
  }
28535
+ if (running.startedAt !== undefined && running.startedAt - record.startedAt > SAME_RUN_WINDOW_MS) {
28536
+ return false;
28537
+ }
28344
28538
  const engine = record.command === undefined ? null : basename(record.command);
28345
28539
  const said = process.platform === "win32" ? running.text.toLowerCase() : running.text;
28346
28540
  const wanted = engine === null || process.platform !== "win32" ? engine : engine.toLowerCase();
@@ -28366,7 +28560,7 @@ function clearRunRecord(ours = () => true) {
28366
28560
  continue;
28367
28561
  }
28368
28562
  try {
28369
- rmSync3(path);
28563
+ rmSync4(path);
28370
28564
  } catch {}
28371
28565
  }
28372
28566
  }
@@ -28390,19 +28584,19 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
28390
28584
  if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
28391
28585
  continue;
28392
28586
  }
28393
- const path = join9(directory, entry);
28587
+ const path = join10(directory, entry);
28394
28588
  if (path === keep || ageMs(path, now) < keepMs) {
28395
28589
  continue;
28396
28590
  }
28397
28591
  try {
28398
- rmSync3(path);
28592
+ rmSync4(path);
28399
28593
  } catch {}
28400
28594
  }
28401
28595
  }
28402
28596
 
28403
28597
  // src/setup.ts
28404
- import { cpSync, existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync10, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "node:fs";
28405
- import { dirname as dirname3, join as join10 } from "node:path";
28598
+ import { cpSync, existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync11, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "node:fs";
28599
+ import { dirname as dirname4, join as join11 } from "node:path";
28406
28600
  var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
28407
28601
  var RUNTIME_AUTOLOAD = {
28408
28602
  name: "GdharnessRuntime",
@@ -28410,11 +28604,11 @@ var RUNTIME_AUTOLOAD = {
28410
28604
  };
28411
28605
  var VERSION_MARKER = ".gdharness-version";
28412
28606
  function installedAddonVersion(projectPath) {
28413
- const marker = join10(projectPath, "addons", ADDONS[0], VERSION_MARKER);
28607
+ const marker = join11(projectPath, "addons", ADDONS[0], VERSION_MARKER);
28414
28608
  if (!existsSync8(marker)) {
28415
28609
  return null;
28416
28610
  }
28417
- const said = readFileSync10(marker, "utf8").trim();
28611
+ const said = readFileSync11(marker, "utf8").trim();
28418
28612
  return said === "" ? null : said;
28419
28613
  }
28420
28614
 
@@ -28751,7 +28945,7 @@ var TOOL_SPECS = [
28751
28945
  },
28752
28946
  {
28753
28947
  name: "project_test",
28754
- description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Suites where everything passed are counted rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines. 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. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so and names the path it looked in. A run where nothing failed but nodes were left in the tree comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. Needs gdUnit4 under addons/gdUnit4.",
28948
+ description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Suites where everything passed are counted rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines. 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. On macOS the engine reads user:// off HOME and the run writes where the player keeps theirs, which the answer says under savesNote rather than leaving to be found in a save list. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so and names the path it looked in. A run where nothing failed but nodes were left in the tree comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. Needs gdUnit4 under addons/gdUnit4.",
28755
28949
  parameters: {
28756
28950
  projectPath: PROJECT_PATH,
28757
28951
  path: {
@@ -29018,7 +29212,7 @@ var TOOL_SPECS = [
29018
29212
  },
29019
29213
  {
29020
29214
  name: "editor_launch",
29021
- description: "Opens the Godot editor on a project, in a window on this machine, or restarts the one already connected. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect; it saves open scenes on the way out and answers with the version that came back. Only an editor with a window can be restarted, because the engine hands back none of the arguments it was started with. editor_status says which editor is connected and whether it is holding an old addon. Opening a project is a save and so is closing one, which is the part that is not obvious from outside: the editor imports the project on open and writes project.godot back, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped either way. A restart says which under settingsDropped in its own answer because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one.",
29215
+ description: "Opens the Godot editor on a project, in a window on this machine, or restarts the one already connected. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect; it saves open scenes on the way out and answers with the version that came back. Only an editor with a window can be restarted, because the engine hands back none of the arguments it was started with. editor_status says which editor is connected and whether it is holding an old addon. Opening a project is a save and so is closing one, which is the part that is not obvious from outside: the editor imports the project on open and writes project.godot back, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped either way. A restart says which under settingsDropped in its own answer because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one. A restart of an editor this server opened is a quit and then a launch, and this server can be ended between them, usually by the reconnect the restart was being taken for: the editor is gone and nothing replaces it. That is written down before the quit, so the next server says so under restartInterrupted in editor_status rather than waiting for an editor that is not coming, and open finishes it, saying so under finishesRestart.",
29022
29216
  parameters: { projectPath: { ...PROJECT_PATH, ops: ["open"] } },
29023
29217
  requires: [],
29024
29218
  operations: {
@@ -29080,7 +29274,7 @@ var TOOL_SPECS = [
29080
29274
  },
29081
29275
  {
29082
29276
  name: "editor_output",
29083
- description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. Answers with the counts and the verdict as well as the entries. A run that has quit still answers here, with running false and its exit code, until the next one starts. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: running is asked of the operating system, endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over.",
29277
+ description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. Answers with the counts and the verdict as well as the entries. A run that has quit still answers here, with running false and its exit code, until the next one starts. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over. running is asked of the operating system whenever there is a process to ask about, which is every run this server started and every editor-played run whose game announced its runtime. For an editor-played run that never announced, there is no process id here and running is the editor's answer, which can lag: one measured elsewhere reported a game as playing for fifteen seconds after its process had been ended from outside. editor_status names that game under runtimes, which does not depend on the editor answering.",
29084
29278
  parameters: {
29085
29279
  severity: {
29086
29280
  type: "string",
@@ -29089,7 +29283,7 @@ var TOOL_SPECS = [
29089
29283
  },
29090
29284
  sinceLastCall: {
29091
29285
  type: "boolean",
29092
- description: "Only entries printed since the previous editor_output. Default false."
29286
+ description: "Only entries this has not already answered with at this severity. Default false. Measured per severity floor and not across them, so polling for errors leaves the rest of the output for a later read rather than consuming it, and a read with contains is a search over the whole run and consumes nothing."
29093
29287
  },
29094
29288
  cpu: {
29095
29289
  type: "boolean",
@@ -29108,7 +29302,7 @@ var TOOL_SPECS = [
29108
29302
  },
29109
29303
  {
29110
29304
  name: "editor_status",
29111
- description: 'Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means "not reached yet" as often as it means "no editor". True is worth waiting out; false is an editor that is not there. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade: installing moves the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with. Those games are real and running, but no tool here can reach them until the two halves match, so runtimes stays empty and runtimeConnected stays false.',
29305
+ description: 'Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means "not reached yet" as often as it means "no editor". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it.',
29112
29306
  parameters: {},
29113
29307
  requires: []
29114
29308
  },
@@ -29525,7 +29719,7 @@ function buildToolDefinitions() {
29525
29719
  var UPDATE_NOTICE_EVERY = 500;
29526
29720
  var FEEDBACK_NOTICE_EVERY = 250;
29527
29721
  var run4 = promisify4(execFile4);
29528
- var __dirname2 = dirname4(fileURLToPath2(import.meta.url));
29722
+ var __dirname2 = dirname5(fileURLToPath2(import.meta.url));
29529
29723
  var EDITOR_RESTART_TIMEOUT_MS = 90000;
29530
29724
  var SLOWEST_FRAME_RATE = 20;
29531
29725
  function patienceForFrames(frames, atLeast) {
@@ -29538,12 +29732,18 @@ function stillRunning(run) {
29538
29732
  if (!run.throughEditor && run.process === null && run.pid !== null) {
29539
29733
  return alive(run.pid);
29540
29734
  }
29735
+ if (run.announcedPid !== undefined && !alive(run.announcedPid)) {
29736
+ return false;
29737
+ }
29541
29738
  return true;
29542
29739
  }
29543
29740
  function runIsUp(run, editorSays) {
29544
29741
  if (run?.throughEditor !== true) {
29545
29742
  return stillRunning(run);
29546
29743
  }
29744
+ if (run.announcedPid !== undefined && !alive(run.announcedPid)) {
29745
+ return false;
29746
+ }
29547
29747
  return editorSays ?? stillRunning(run);
29548
29748
  }
29549
29749
  function runtimeVerdict(endpoint, after) {
@@ -29715,7 +29915,7 @@ function aboveTheRunner(entry) {
29715
29915
  }
29716
29916
  function elsewhereIn(projectPath, asked) {
29717
29917
  const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
29718
- if (wanted === "" || existsSync9(join11(projectPath, wanted))) {
29918
+ if (wanted === "" || existsSync9(join12(projectPath, wanted))) {
29719
29919
  return "";
29720
29920
  }
29721
29921
  const root = (wanted.split("/")[0] ?? "").toLowerCase();
@@ -29729,11 +29929,11 @@ function elsewhereIn(projectPath, asked) {
29729
29929
  return near.length === 0 ? "" : ` The project does have ${there}.`;
29730
29930
  }
29731
29931
  function hasMainScene(projectFile) {
29732
- const scene = parseProjectGodot(readFileSync11(projectFile, "utf8"))["application"]?.["run/main_scene"];
29932
+ const scene = parseProjectGodot(readFileSync12(projectFile, "utf8"))["application"]?.["run/main_scene"];
29733
29933
  return typeof scene === "string" && scene !== "";
29734
29934
  }
29735
29935
  function editorPlaysHeadless(projectFile) {
29736
- const runArgs = parseProjectGodot(readFileSync11(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
29936
+ const runArgs = parseProjectGodot(readFileSync12(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
29737
29937
  return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
29738
29938
  }
29739
29939
  function camelCased(params) {
@@ -29749,7 +29949,7 @@ function camelCased(params) {
29749
29949
  class GodotServer {
29750
29950
  mcp;
29751
29951
  locator = new GodotLocator;
29752
- operationsScript = join11(__dirname2, "godot", "operations", "godot_operations.gd");
29952
+ operationsScript = join12(__dirname2, "godot", "operations", "godot_operations.gd");
29753
29953
  godotBridge;
29754
29954
  tools = buildToolDefinitions();
29755
29955
  activeProcess = null;
@@ -29757,6 +29957,8 @@ class GodotServer {
29757
29957
  noticedUpdate = false;
29758
29958
  callsSinceNotice = 0;
29759
29959
  callsSinceFeedback = 0;
29960
+ defectsSeen = new DefectsSeen;
29961
+ lastGodotVersion = null;
29760
29962
  lspClient = null;
29761
29963
  dapClient = null;
29762
29964
  bridgeStartupError = null;
@@ -29801,11 +30003,11 @@ class GodotServer {
29801
30003
  return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
29802
30004
  }
29803
30005
  async run() {
29804
- const godotPath = await this.locator.find();
29805
- if (godotPath === null) {
29806
- console.error("[SERVER] No Godot found. Set GODOT_PATH; until then every tool that runs the engine will say so.");
30006
+ const located = await this.locator.find();
30007
+ if (!located.ok) {
30008
+ console.error(`[SERVER] ${GodotLocator.refusal(located).message} Until that is fixed every tool that runs the engine will say so.`);
29807
30009
  } else {
29808
- console.error(`[SERVER] Using Godot at: ${godotPath}`);
30010
+ console.error(`[SERVER] Using Godot at: ${located.path}`);
29809
30011
  }
29810
30012
  const transport = new StdioServerTransport;
29811
30013
  await this.mcp.server.connect(transport);
@@ -30005,7 +30207,19 @@ class GodotServer {
30005
30207
  }
30006
30208
  const where = op === "" ? tool : `${tool} op=${op}`;
30007
30209
  console.error(`[SERVER] Unmodelled failure in ${where}:`, error);
30008
- return { content: [{ type: "text", text: defectReport2(where, error) }], isError: true };
30210
+ const seenBefore = this.defectsSeen.before(where, errorMessage(error));
30211
+ return {
30212
+ content: [
30213
+ {
30214
+ type: "text",
30215
+ text: defectReport2(where, error, {
30216
+ seenBefore,
30217
+ godotVersion: this.lastGodotVersion ?? undefined
30218
+ })
30219
+ }
30220
+ ],
30221
+ isError: true
30222
+ };
30009
30223
  }
30010
30224
  }
30011
30225
  withUpdateNotice(answer) {
@@ -30366,21 +30580,19 @@ class GodotServer {
30366
30580
  }
30367
30581
  }
30368
30582
  async engine() {
30369
- const godotPath = await this.locator.find();
30370
- if (godotPath === null) {
30371
- return {
30372
- ok: false,
30373
- response: this.createErrorResponse("No Godot executable found.", GodotLocator.ADVICE)
30374
- };
30583
+ const located = await this.locator.find();
30584
+ if (!located.ok) {
30585
+ const refusal = GodotLocator.refusal(located);
30586
+ return { ok: false, response: this.createErrorResponse(refusal.message, [...refusal.advice]) };
30375
30587
  }
30376
- return { ok: true, value: godotPath };
30588
+ return { ok: true, value: located.path };
30377
30589
  }
30378
30590
  project(args) {
30379
30591
  const path = readNonEmptyString(args, "projectPath");
30380
30592
  if (path === undefined) {
30381
30593
  return { ok: false, response: this.createErrorResponse("projectPath is required.") };
30382
30594
  }
30383
- const file = join11(path, "project.godot");
30595
+ const file = join12(path, "project.godot");
30384
30596
  if (!existsSync9(file)) {
30385
30597
  return {
30386
30598
  ok: false,
@@ -30522,7 +30734,7 @@ class GodotServer {
30522
30734
  }
30523
30735
  const engine = await this.engine();
30524
30736
  if (!engine.ok) {
30525
- return this.createErrorResponse("No Godot executable found.");
30737
+ return engine.response;
30526
30738
  }
30527
30739
  const projectPath = project.value.path;
30528
30740
  const before = scriptsWithoutUid(projectPath);
@@ -30545,7 +30757,11 @@ class GodotServer {
30545
30757
  async operation(operation, params, projectPath) {
30546
30758
  const engine = await this.engine();
30547
30759
  if (!engine.ok) {
30548
- return { ok: false, message: "No Godot executable found.", messages: [] };
30760
+ return {
30761
+ ok: false,
30762
+ message: engine.response.content.map((block) => block.text).join(" "),
30763
+ messages: []
30764
+ };
30549
30765
  }
30550
30766
  this.logDebug(`Running ${operation} in ${projectPath}: ${JSON.stringify(params)}`);
30551
30767
  return await runOperation({ godotPath: engine.value, script: this.operationsScript, debug: GODOT_DEBUG_MODE_DEFAULT }, operation, params, projectPath);
@@ -30577,7 +30793,7 @@ class GodotServer {
30577
30793
  if (!engine.ok) {
30578
30794
  return engine.response;
30579
30795
  }
30580
- const application = parseProjectGodot(readFileSync11(project.value.file, "utf8"))["application"] ?? {};
30796
+ const application = parseProjectGodot(readFileSync12(project.value.file, "utf8"))["application"] ?? {};
30581
30797
  const name = application["config/name"];
30582
30798
  const mainScene = application["run/main_scene"];
30583
30799
  const info = {
@@ -30638,18 +30854,18 @@ class GodotServer {
30638
30854
  if (!engine.ok) {
30639
30855
  return engine.response;
30640
30856
  }
30641
- const folder = dirname4(output.absolutePath);
30857
+ const folder = dirname5(output.absolutePath);
30642
30858
  try {
30643
- mkdirSync5(folder, { recursive: true });
30859
+ mkdirSync6(folder, { recursive: true });
30644
30860
  } catch (error) {
30645
- return this.createErrorResponse(`Cannot create ${dirname4(output.relativePath)} to export into: ${errorMessage(error)}`);
30861
+ return this.createErrorResponse(`Cannot create ${dirname5(output.relativePath)} to export into: ${errorMessage(error)}`);
30646
30862
  }
30647
30863
  const debug = readBoolean(args, "debug") ?? false;
30648
- const exportLogs = mkdtempSync2(join11(tmpdir4(), "gdharness-export-"));
30864
+ const exportLogs = mkdtempSync2(join12(tmpdir4(), "gdharness-export-"));
30649
30865
  const exportArgs = [
30650
30866
  "--headless",
30651
30867
  "--log-file",
30652
- join11(exportLogs, "engine.log"),
30868
+ join12(exportLogs, "engine.log"),
30653
30869
  "--path",
30654
30870
  project.value.path,
30655
30871
  debug ? "--export-debug" : "--export-release",
@@ -30707,7 +30923,7 @@ class GodotServer {
30707
30923
  return contained.response;
30708
30924
  }
30709
30925
  const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
30710
- if (!existsSync9(join11(project.value.path, runner))) {
30926
+ if (!existsSync9(join12(project.value.path, runner))) {
30711
30927
  return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
30712
30928
  "Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
30713
30929
  ]);
@@ -30742,7 +30958,7 @@ class GodotServer {
30742
30958
  ];
30743
30959
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
30744
30960
  this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
30745
- const userData = mkdtempSync2(join11(tmpdir4(), "gdharness-tests-"));
30961
+ const userData = mkdtempSync2(join12(tmpdir4(), "gdharness-tests-"));
30746
30962
  const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
30747
30963
  const hung = await new Promise((resolve) => {
30748
30964
  const timer = setTimeout(() => {
@@ -30758,14 +30974,14 @@ class GodotServer {
30758
30974
  resolve(false);
30759
30975
  });
30760
30976
  });
30761
- const reportsDir = join11(project.value.path, ".godot", ...ours.split("/"));
30977
+ const reportsDir = join12(project.value.path, ".godot", ...ours.split("/"));
30762
30978
  let report = null;
30763
30979
  let reportProblem = null;
30764
30980
  try {
30765
30981
  const written = existsSync9(reportsDir) ? readdirSync6(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
30766
30982
  const newest = written.at(-1);
30767
30983
  if (newest !== undefined) {
30768
- report = parseJUnit(readFileSync11(join11(reportsDir, newest, "results.xml"), "utf8"));
30984
+ report = parseJUnit(readFileSync12(join12(reportsDir, newest, "results.xml"), "utf8"));
30769
30985
  }
30770
30986
  } catch (error) {
30771
30987
  reportProblem = errorMessage(error);
@@ -30786,6 +31002,7 @@ class GodotServer {
30786
31002
  const said = printed.map((entry) => entry.text);
30787
31003
  const nothingRan = hung ? null : whyNoReport(said, asked);
30788
31004
  const verdict = hung ? `hung: killed after ${timeoutMs} ms` : nothingRan ?? verdicts[exitCode ?? -1] ?? `exit ${exitCode ?? "unknown"}`;
31005
+ const savesNote = savesStayPut() && nothingRan === null ? SAVES_NOT_MOVED_NOTE : undefined;
30789
31006
  if (report === null) {
30790
31007
  const note = nothingRan === null ? `The test run wrote no report (${verdict}${reportProblem ? `; ${reportProblem}` : ""}).` : `No tests ran: ${verdict}.${elsewhereIn(project.value.path, asked)}`;
30791
31008
  return {
@@ -30800,7 +31017,8 @@ class GodotServer {
30800
31017
  exitCode,
30801
31018
  hung,
30802
31019
  arguments: cmdArgs,
30803
- entries: forAnswer(printed.slice(0, 60))
31020
+ entries: forAnswer(printed.slice(0, 60)),
31021
+ savesNote
30804
31022
  }, null, 2)
30805
31023
  }
30806
31024
  ],
@@ -30833,6 +31051,7 @@ class GodotServer {
30833
31051
  warnings: warnings.length > 0 ? warnings : undefined,
30834
31052
  orphans: orphans.total > 0 ? orphans.total : undefined,
30835
31053
  notRun: notRun > 0 ? notRun : undefined,
31054
+ savesNote,
30836
31055
  note: verdict.startsWith("warnings") && warnings.length === 0 ? "gdUnit4 exits 101 for orphan nodes when nothing failed, and this run printed no count of them: orphan reporting may be off in the project settings." : notRun > 0 ? `This run stopped at the first failure in each suite it failed in, so ${notRun} case${notRun === 1 ? "" : "s"} never ran and count as neither passed nor failed. Leave failFast out to run every case.` : undefined,
30837
31056
  suites: unclean.map((suite) => ({
30838
31057
  name: suite.name,
@@ -30855,7 +31074,8 @@ class GodotServer {
30855
31074
  async godotVersion(godotPath) {
30856
31075
  try {
30857
31076
  const { stdout } = await run4(godotPath, ["--version"], { timeout: 1e4 });
30858
- return stdout.trim();
31077
+ this.lastGodotVersion = stdout.trim();
31078
+ return this.lastGodotVersion;
30859
31079
  } catch (error) {
30860
31080
  this.logDebug(`Godot did not answer --version: ${errorMessage(error)}`);
30861
31081
  return null;
@@ -30901,7 +31121,7 @@ class GodotServer {
30901
31121
  return null;
30902
31122
  }
30903
31123
  try {
30904
- return readFileSync11(contained.absolutePath, "utf8");
31124
+ return readFileSync12(contained.absolutePath, "utf8");
30905
31125
  } catch {
30906
31126
  return null;
30907
31127
  }
@@ -30943,7 +31163,7 @@ class GodotServer {
30943
31163
  return null;
30944
31164
  }
30945
31165
  try {
30946
- return readFileSync11(contained.absolutePath, "utf8");
31166
+ return readFileSync12(contained.absolutePath, "utf8");
30947
31167
  } catch {
30948
31168
  return null;
30949
31169
  }
@@ -31013,16 +31233,37 @@ class GodotServer {
31013
31233
  const status = this.godotBridge.getStatus();
31014
31234
  return (status.connected ? status[field] : undefined) ?? fallback;
31015
31235
  }
31236
+ restartLeftUnfinished(connected) {
31237
+ if (this.ownProject === null) {
31238
+ return null;
31239
+ }
31240
+ const owed = restartOwed(this.ownProject);
31241
+ if (owed === null) {
31242
+ return null;
31243
+ }
31244
+ if (connected) {
31245
+ restartSettled(this.ownProject);
31246
+ return null;
31247
+ }
31248
+ return owed;
31249
+ }
31016
31250
  getEditorStatusPayload() {
31017
31251
  const status = this.godotBridge.getStatus();
31018
31252
  const isPortConflict = this.bridgeStartupError?.includes("EADDRINUSE") ?? false;
31019
31253
  const stale = status.connected && status.addonVersion !== SERVER_VERSION;
31254
+ const unfinished = this.restartLeftUnfinished(status.connected);
31020
31255
  return {
31021
31256
  ...status,
31022
31257
  serverVersion: SERVER_VERSION,
31023
31258
  addonIsStale: status.connected ? stale : undefined,
31024
31259
  bridgeAvailable: this.bridgeStartupError === null,
31025
- mayYetConnect: status.connected ? undefined : this.anEditorIsStillComing(status.listeningSince),
31260
+ mayYetConnect: status.connected ? undefined : unfinished !== null ? false : this.anEditorIsStillComing(status.listeningSince),
31261
+ restartInterrupted: unfinished === null ? undefined : {
31262
+ quitEditorPid: unfinished.editorPid,
31263
+ quitAt: unfinished.quitAt,
31264
+ byServerPid: unfinished.byPid,
31265
+ note: "A restart begun by a previous gdharness server asked this project's editor to quit, and that server was ended before it could start the editor again, so nothing is coming. editor_launch open starts one, and clears this."
31266
+ },
31026
31267
  awaitingLaunchedEditor: status.connected || !this.launchedEditorIsUp() ? undefined : this.launchedEditor?.pid ?? undefined,
31027
31268
  ...status.connected ? this.whatTheLaunchedEditorDropped() : {},
31028
31269
  startupError: this.bridgeStartupError,
@@ -31058,7 +31299,7 @@ class GodotServer {
31058
31299
  }
31059
31300
  }
31060
31301
  async handleEditorStatus() {
31061
- const godotPath = await this.locator.find();
31302
+ const located = await this.locator.find();
31062
31303
  const announced = runtimesAnnounced();
31063
31304
  const games = await Promise.all(announced.running.map(async (endpoint) => {
31064
31305
  const reply = await runtimeRequest(endpoint, "ping", {}, this.runtimeTimeoutMs());
@@ -31078,8 +31319,9 @@ class GodotServer {
31078
31319
  debugPort: playing?.debugPort ?? this.godotBridge.getStatus().debugPort
31079
31320
  },
31080
31321
  godot: {
31081
- path: godotPath,
31082
- version: godotPath === null ? null : await this.godotVersion(godotPath)
31322
+ path: located.ok ? located.path : null,
31323
+ version: located.ok ? await this.godotVersion(located.path) : null,
31324
+ problem: located.ok ? undefined : GodotLocator.refusal(located).message
31083
31325
  },
31084
31326
  game: {
31085
31327
  processActive: runIsUp(this.currentRun(), playing === null ? null : playing.playing),
@@ -31098,12 +31340,12 @@ class GodotServer {
31098
31340
  }
31099
31341
  settingKeysOf(projectPath) {
31100
31342
  if (projectPath === undefined) {
31101
- return new Map;
31343
+ return null;
31102
31344
  }
31103
31345
  try {
31104
- return settingKeys(readFileSync11(join11(projectPath, "project.godot"), "utf8"));
31346
+ return settingKeys(readFileSync12(join12(projectPath, "project.godot"), "utf8"));
31105
31347
  } catch {
31106
- return new Map;
31348
+ return null;
31107
31349
  }
31108
31350
  }
31109
31351
  async handleRestartEditor() {
@@ -31163,12 +31405,21 @@ class GodotServer {
31163
31405
  lsp: this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT),
31164
31406
  dap: this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT)
31165
31407
  };
31408
+ noteRestartBegun({
31409
+ projectPath,
31410
+ editorPid: editorPid ?? null,
31411
+ ports,
31412
+ quitAt: new Date().toISOString(),
31413
+ byPid: process.pid
31414
+ });
31166
31415
  const asked = await this.handleViaBridge("quit_editor", {});
31167
31416
  if (asked.isError === true) {
31417
+ restartSettled(projectPath);
31168
31418
  return asked;
31169
31419
  }
31170
31420
  await this.waitForBridge(() => !alive(editorPid), Date.now() + EDITOR_RESTART_TIMEOUT_MS);
31171
31421
  const opened = await this.openAnEditor(engine.value, projectPath, ports);
31422
+ restartSettled(projectPath);
31172
31423
  if (opened.error !== null) {
31173
31424
  return this.createErrorResponse(`The editor was asked to go and could not be started again: ${opened.error}`, [`editor_launch opens one on ${projectPath}`]);
31174
31425
  }
@@ -31204,10 +31455,12 @@ class GodotServer {
31204
31455
  this.logDebug(`Launching Godot editor for project: ${project.value.path}`);
31205
31456
  const ports = await this.portsForAnEditor();
31206
31457
  const before = this.settingKeysOf(project.value.path);
31458
+ const unfinished = restartOwed(project.value.path);
31207
31459
  const opened = await this.openAnEditor(engine.value, project.value.path, ports);
31208
31460
  if (opened.error !== null) {
31209
31461
  return this.createErrorResponse(`Could not start the editor: ${opened.error}`);
31210
31462
  }
31463
+ restartSettled(project.value.path);
31211
31464
  this.launchedFrom = { projectPath: project.value.path, before };
31212
31465
  return this.jsonTextResponse({
31213
31466
  launched: true,
@@ -31215,7 +31468,8 @@ class GodotServer {
31215
31468
  projectPath: project.value.path,
31216
31469
  lspPort: ports.lsp,
31217
31470
  dapPort: ports.dap,
31218
- settingsNote: before.size === 0 ? undefined : "Opening a project imports it and saves project.godot, and Godot drops any key sitting at its own default. editor_status names anything lost under settingsDropped once this editor has connected."
31471
+ finishesRestart: unfinished === null ? undefined : `This is the launch half of a restart begun at ${unfinished.quitAt} by server pid ${unfinished.byPid}, which quit editor pid ${unfinished.editorPid ?? "unknown"} and was ended before it could start one.`,
31472
+ settingsNote: before === null ? "project.godot could not be read before this editor was opened, so nothing here will be able to say what the import saved away. Opening a project rewrites the file and drops any key sitting at its own default." : before.size === 0 ? undefined : "Opening a project imports it and saves project.godot, and Godot drops any key sitting at its own default. editor_status names anything lost under settingsDropped once this editor has connected."
31219
31473
  });
31220
31474
  }
31221
31475
  async openAnEditor(engine, projectPath, ports) {
@@ -31332,13 +31586,17 @@ class GodotServer {
31332
31586
  return runIsUp(run, editorSays);
31333
31587
  }
31334
31588
  async runtimeUp(projectPath, before, budgetMs, withArgs) {
31335
- if (!existsSync9(join11(projectPath, RUNTIME_AUTOLOAD.path))) {
31589
+ if (!existsSync9(join12(projectPath, RUNTIME_AUTOLOAD.path))) {
31336
31590
  return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
31337
31591
  }
31338
31592
  const endpoint = await announcedSince(projectPath, before, {
31339
31593
  budgetMs,
31340
31594
  giveUp: () => this.dapClient?.isStopped() === true || !stillRunning(this.currentRun())
31341
31595
  });
31596
+ const going = this.currentRun();
31597
+ if (endpoint !== null && going?.throughEditor === true && going.announcedPid === undefined) {
31598
+ going.announcedPid = endpoint.pid;
31599
+ }
31342
31600
  return runtimeVerdict(endpoint, {
31343
31601
  addon: true,
31344
31602
  budgetMs,
@@ -31448,9 +31706,15 @@ class GodotServer {
31448
31706
  running.log.record("warning", `This run was not ended here: pid ${running.pid} no longer answers as the run that was recorded, so nothing was signalled. If that process is still the game, end it yourself; if it is not, it belongs to something else.`);
31449
31707
  return;
31450
31708
  }
31709
+ const signalled = runningAs(running.pid);
31710
+ const described = signalled === null ? "nothing it would name" : `${signalled.kind}: ${signalled.text}`;
31711
+ let landed = true;
31451
31712
  try {
31452
31713
  process.kill(running.pid);
31453
- } catch {}
31714
+ } catch {
31715
+ landed = false;
31716
+ }
31717
+ running.log.record("info", landed ? `gdharness ended pid ${running.pid}, which the operating system described as ${described}.` : `gdharness signalled pid ${running.pid} and it was already gone; the operating system had described it as ${described}.`);
31454
31718
  }
31455
31719
  spawnGame(godotPath, cmdArgs, env) {
31456
31720
  const child = spawn(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
@@ -31788,7 +32052,7 @@ class GodotServer {
31788
32052
  notes.push(run.exitCode === 0 ? "This run quit on its own, cleanly: exit code 0." : "Nothing here ended this run: it stopped on its own or something outside this server stopped it.");
31789
32053
  }
31790
32054
  if (run.transcript !== null && selected.omitted > 0) {
31791
- notes.push(`Everything this run has printed is in ${run.transcript}, uncapped and still being written. Read it for output, not for whether the run is alive: running above is asked of the operating system, and a transcript that stops growing is a run between prints.`);
32055
+ notes.push(`Everything this run has printed is in ${run.transcript}, uncapped and still being written. Read it for output, not for whether the run is alive: a transcript that stops growing is a run between prints. running above is asked of the operating system while there is a process to ask about, and for an editor-played run that announced no runtime it is the editor's answer instead, which can lag after the game has gone.`);
31792
32056
  }
31793
32057
  return this.jsonTextResponse({
31794
32058
  running: stillRunning(run),
@@ -31990,8 +32254,8 @@ class GodotServer {
31990
32254
  return this.createErrorResponse(choice.problem);
31991
32255
  }
31992
32256
  const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
31993
- const screenshotDir = expectsScreenshot ? mkdtempSync2(join11(tmpdir4(), "gdharness-runtime-screenshot-")) : null;
31994
- const screenshotPath = screenshotDir ? join11(screenshotDir, "capture.png") : null;
32257
+ const screenshotDir = expectsScreenshot ? mkdtempSync2(join12(tmpdir4(), "gdharness-runtime-screenshot-")) : null;
32258
+ const screenshotPath = screenshotDir ? join12(screenshotDir, "capture.png") : null;
31995
32259
  try {
31996
32260
  const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs);
31997
32261
  if (!reply.ok) {
@@ -32012,7 +32276,7 @@ class GodotServer {
32012
32276
  return {
32013
32277
  content: [
32014
32278
  { type: "text", text: `Screenshot captured: ${dimensions}` },
32015
- { type: "image", data: readFileSync11(screenshotPath).toString("base64"), mimeType: "image/png" }
32279
+ { type: "image", data: readFileSync12(screenshotPath).toString("base64"), mimeType: "image/png" }
32016
32280
  ]
32017
32281
  };
32018
32282
  } finally {