gdharness 1.0.11 → 1.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/cli.js CHANGED
@@ -182,8 +182,18 @@ function runArguments(options) {
182
182
  }
183
183
  return args;
184
184
  }
185
- function editorArguments(projectPath, ports) {
186
- return ["-e", "--path", projectPath, "--lsp-port", String(ports.lsp), "--dap-port", String(ports.dap)];
185
+ function editorArguments(projectPath, ports, logFile) {
186
+ return [
187
+ "-e",
188
+ "--path",
189
+ projectPath,
190
+ "--lsp-port",
191
+ String(ports.lsp),
192
+ "--dap-port",
193
+ String(ports.dap),
194
+ "--log-file",
195
+ logFile
196
+ ];
187
197
  }
188
198
  function userDataIn(home, variables = process.env) {
189
199
  const moved = ["APPDATA", "XDG_DATA_HOME"];
@@ -7263,6 +7273,9 @@ class GameLog {
7263
7273
  count(severity) {
7264
7274
  return this.entries.filter((entry) => entry.severity === severity).length;
7265
7275
  }
7276
+ everything() {
7277
+ return this.entries;
7278
+ }
7266
7279
  select(options) {
7267
7280
  const floor = options.sinceLastCall ? this.seen[options.severity] : 0;
7268
7281
  const wanted = ADMITTED[options.severity];
@@ -7518,11 +7531,20 @@ var init_update_check = __esm(() => {
7518
7531
 
7519
7532
  // src/server-version.ts
7520
7533
  import { readFileSync as readFileSync3 } from "node:fs";
7521
- function addonMismatch(addonVersion, serverVersion) {
7522
- if (addonVersion === serverVersion) {
7534
+ function editorIsStale(addonVersion, serverVersion, addonDigest, shippedDigest) {
7535
+ if (addonDigest !== undefined && addonDigest !== "" && shippedDigest !== undefined) {
7536
+ return addonDigest !== shippedDigest;
7537
+ }
7538
+ return addonVersion !== serverVersion;
7539
+ }
7540
+ function addonMismatch(addonVersion, serverVersion, addonDigest, shippedDigest) {
7541
+ if (!editorIsStale(addonVersion, serverVersion, addonDigest, shippedDigest)) {
7523
7542
  return;
7524
7543
  }
7525
7544
  const reported = addonVersion ?? "";
7545
+ if (reported === serverVersion) {
7546
+ return `The editor is running a different build of the ${reported} addon from the one this server ships. Restart it with editor_launch restart to pick this one up.`;
7547
+ }
7526
7548
  const editor = reported === "" ? UNVERSIONED : `the ${reported} addon`;
7527
7549
  const both = `The editor is running ${editor} while this server ships ${serverVersion}.`;
7528
7550
  if (reported !== "" && isNewer(reported, serverVersion)) {
@@ -7530,8 +7552,8 @@ function addonMismatch(addonVersion, serverVersion) {
7530
7552
  }
7531
7553
  return `${both} Restart it with editor_launch restart to pick the new one up.`;
7532
7554
  }
7533
- function markIfStale(answer, addonVersion, serverVersion) {
7534
- const staleNote = addonMismatch(addonVersion, serverVersion);
7555
+ function markIfStale(answer, addonVersion, serverVersion, addonDigest, shippedDigest) {
7556
+ const staleNote = addonMismatch(addonVersion, serverVersion, addonDigest, shippedDigest);
7535
7557
  if (staleNote === undefined || typeof answer !== "object" || answer === null || Array.isArray(answer)) {
7536
7558
  return answer;
7537
7559
  }
@@ -7677,6 +7699,19 @@ function cacheWrittenAt(projectPath) {
7677
7699
  return null;
7678
7700
  }
7679
7701
  }
7702
+ function declaredSince(projectPath, paths, names, since) {
7703
+ return names.filter((name) => {
7704
+ const path = paths.get(name);
7705
+ if (path === undefined || !path.startsWith("res://")) {
7706
+ return false;
7707
+ }
7708
+ try {
7709
+ return statSync2(join6(projectPath, path.slice("res://".length))).mtimeMs > since;
7710
+ } catch {
7711
+ return false;
7712
+ }
7713
+ });
7714
+ }
7680
7715
  function cachedAtMissingPaths(cached, projectPath) {
7681
7716
  return [...cached].filter(([, path]) => path.startsWith("res://") && !existsSync6(join6(projectPath, path.slice("res://".length)))).map(([name]) => name).sort();
7682
7717
  }
@@ -15295,8 +15330,57 @@ function readParams(params, key) {
15295
15330
  }
15296
15331
 
15297
15332
  // src/setup.ts
15298
- import { cpSync as cpSync2, existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync8, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "node:fs";
15333
+ import { createHash as createHash3 } from "node:crypto";
15334
+ import {
15335
+ cpSync as cpSync2,
15336
+ existsSync as existsSync8,
15337
+ mkdirSync as mkdirSync4,
15338
+ readdirSync as readdirSync4,
15339
+ readFileSync as readFileSync8,
15340
+ rmSync as rmSync4,
15341
+ statSync as statSync4,
15342
+ writeFileSync as writeFileSync6
15343
+ } from "node:fs";
15299
15344
  import { dirname as dirname4, join as join9 } from "node:path";
15345
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
15346
+ function editorAddonDigest2(from = shippedAddonsDirectory2()) {
15347
+ const hash = createHash3("sha256");
15348
+ const walk = (directory, relative) => {
15349
+ for (const name of readdirSync4(directory).sort()) {
15350
+ if (name === VERSION_MARKER2 || name === DIGEST_MARKER2) {
15351
+ continue;
15352
+ }
15353
+ const path = join9(directory, name);
15354
+ const inside = `${relative}/${name}`;
15355
+ if (statSync4(path).isDirectory()) {
15356
+ walk(path, inside);
15357
+ } else {
15358
+ hash.update(`${inside}
15359
+ `);
15360
+ hash.update(readFileSync8(path));
15361
+ hash.update(`
15362
+ `);
15363
+ }
15364
+ }
15365
+ };
15366
+ for (const name of EDITOR_PLUGINS2) {
15367
+ walk(join9(from, name), name);
15368
+ }
15369
+ return hash.digest("hex");
15370
+ }
15371
+ function shippedEditorDigest() {
15372
+ if (shippedDigest === undefined) {
15373
+ try {
15374
+ shippedDigest = editorAddonDigest2();
15375
+ } catch {
15376
+ shippedDigest = null;
15377
+ }
15378
+ }
15379
+ return shippedDigest ?? undefined;
15380
+ }
15381
+ function shippedAddonsDirectory2() {
15382
+ return join9(dirname4(fileURLToPath2(import.meta.url)), "godot", "addons");
15383
+ }
15300
15384
  function installedAddonVersion(projectPath) {
15301
15385
  const marker = join9(projectPath, "addons", ADDONS2[0], VERSION_MARKER2);
15302
15386
  if (!existsSync8(marker)) {
@@ -15305,13 +15389,14 @@ function installedAddonVersion(projectPath) {
15305
15389
  const said = readFileSync8(marker, "utf8").trim();
15306
15390
  return said === "" ? null : said;
15307
15391
  }
15308
- var ADDONS2, RUNTIME_AUTOLOAD2, SCRIPT_RUNS_SETTING = "gdharness/runtime/serve_script_runs", BIND_ADDRESS_SETTING = "gdharness/runtime/bind_address", PORT_SETTING = "gdharness/runtime/port", VERSION_MARKER2 = ".gdharness-version";
15392
+ var ADDONS2, EDITOR_PLUGINS2, RUNTIME_AUTOLOAD2, SCRIPT_RUNS_SETTING = "gdharness/runtime/serve_script_runs", BIND_ADDRESS_SETTING = "gdharness/runtime/bind_address", PORT_SETTING = "gdharness/runtime/port", VERSION_MARKER2 = ".gdharness-version", DIGEST_MARKER2 = ".gdharness-digest", shippedDigest;
15309
15393
  var init_setup = __esm(() => {
15310
15394
  init_class_cache();
15311
15395
  init_headless();
15312
15396
  init_resources();
15313
15397
  init_server_version();
15314
15398
  ADDONS2 = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
15399
+ EDITOR_PLUGINS2 = ["gdharness_editor", "auto_reload"];
15315
15400
  RUNTIME_AUTOLOAD2 = {
15316
15401
  name: "GdharnessRuntime",
15317
15402
  path: "addons/gdharness_runtime/runtime_autoload.gd"
@@ -15349,7 +15434,7 @@ function projectPathSentence(without = callsWithoutProjectPath()) {
15349
15434
  return `Every call takes \`projectPath\` except ${named}, which ${without.length === 1 ? "takes" : "take"} none`;
15350
15435
  }
15351
15436
  function theGamePicked() {
15352
- return "the game this server started or is playing when it is among those running, named under " + "`answeredBy` where there was a choice; else the only game there is; else a refusal naming " + "every game, which is what a machine running two projects at once gets";
15437
+ return "the game this server started or is playing when it is among those running, named under " + "`answeredBy` where there was a choice; else the only game there is; else a refusal naming " + "every game and which argument tells them apart, `pid` when they are all one project and " + "`projectPath` when they are not";
15353
15438
  }
15354
15439
  function toolSpec(name) {
15355
15440
  return SPECS_BY_NAME[name];
@@ -16070,7 +16155,7 @@ var init_tool_definitions = __esm(() => {
16070
16155
  },
16071
16156
  {
16072
16157
  name: "editor_output",
16073
- 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. For a run the editor plays the reports come from the game itself, through the runtime addon, since the editor's debugger relays what a game prints and not what it reports; a played game without the runtime addon answers here with its prints alone. Answers with the counts and the verdict as well as the entries. heldAt is three answers: null for a run this server knows is not held, running or over, an object for one it knows is held at a breakpoint, whether the adapter said so or the runtime did by accepting a connection and never answering, and absent with heldUnknown beside it for a server that connected to the debugger after the stop and has no runtime to ask, since the adapter reports a stop only to the sessions connected when it happened. 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.",
16158
+ 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. For a run the editor plays the reports come from the game itself, through the runtime addon, since the editor's debugger relays what a game prints and not what it reports; a played game without the runtime addon answers here with its prints alone. Answers with the counts and the verdict as well as the entries. heldAt is three answers: null for a run this server knows is not held, running or over, an object for one it knows is held at a breakpoint, whether the adapter said so or the runtime did by accepting a connection and never answering, and absent with heldUnknown beside it for a server that connected to the debugger after the stop and has no runtime to ask, since the adapter reports a stop only to the sessions connected when it happened. 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. op: \"editor\" answers about the editor itself instead, which is a different log and the only way anything here can read what an editor printed: its console reaches no plugin, so the addon cannot scrape it, and an editor gdharness opened is asked for a log file as it starts. That covers the whole session from the editor's first line, which is where the output worth reading usually is, since the class cache, the plugins and the language server all say what they have to say before any harness has connected. The answer carries the entries in the order they were printed and, beside them under repeated, each message shape several lines share: how many, what varied in each slot of it and how many different values stood there, the first occurrence, and the ordinary lines above the group, found by walking back past the group rather than by counting lines. An editor somebody opened by hand has no console here and is refused saying so rather than answered empty, because an editor that printed nothing and an editor nobody captured are not the same thing.",
16074
16159
  parameters: {
16075
16160
  severity: {
16076
16161
  type: "string",
@@ -16078,10 +16163,12 @@ var init_tool_definitions = __esm(() => {
16078
16163
  description: "The least severe entry to include. Default info, which is everything."
16079
16164
  },
16080
16165
  sinceLastCall: {
16166
+ ops: ["run"],
16081
16167
  type: "boolean",
16082
16168
  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."
16083
16169
  },
16084
16170
  cpu: {
16171
+ ops: ["run"],
16085
16172
  type: "boolean",
16086
16173
  description: "Also answer with cpuSeconds, the processor time the run has used. Default false, because asking costs a subprocess. Worth it when elapsedMs is climbing and nothing is being printed: processor time standing still is a run that has stopped doing anything, rather than one that is slow."
16087
16174
  },
@@ -16092,8 +16179,24 @@ var init_tool_definitions = __esm(() => {
16092
16179
  limit: {
16093
16180
  type: "number",
16094
16181
  description: "The most entries to answer with, newest kept. Default 200, and omitted says how many matching entries that left out. A run long enough to pass it needs this raised, not just filtered: filtering narrows what counts as matching, and the newest of those is still all one answer carries."
16182
+ },
16183
+ before: {
16184
+ ops: ["editor"],
16185
+ type: "number",
16186
+ description: "How many lines above a repeated group to carry with it, counting only lines that are not themselves part of a group. Default 3. The line that explains a wall of identical errors is the last ordinary line above it, so this walks past the wall rather than a fixed distance back."
16095
16187
  }
16096
16188
  },
16189
+ operations: {
16190
+ run: {
16191
+ summary: "What the game a run is playing has printed, which is everything described above",
16192
+ requires: []
16193
+ },
16194
+ editor: {
16195
+ summary: "What the connected editor itself has printed, from its first line, with repeated messages grouped and the ordinary lines above each group carried with it",
16196
+ requires: []
16197
+ }
16198
+ },
16199
+ defaultOperation: "run",
16097
16200
  requires: []
16098
16201
  },
16099
16202
  {
@@ -31459,7 +31562,7 @@ var init_process_children = __esm(() => {
31459
31562
  });
31460
31563
 
31461
31564
  // src/runtime-client.ts
31462
- import { existsSync as existsSync10, readdirSync as readdirSync4, readFileSync as readFileSync10, statSync as statSync3, unlinkSync } from "node:fs";
31565
+ import { existsSync as existsSync10, readdirSync as readdirSync6, readFileSync as readFileSync10, statSync as statSync5, unlinkSync } from "node:fs";
31463
31566
  import { createConnection } from "node:net";
31464
31567
  import { tmpdir as tmpdir4 } from "node:os";
31465
31568
  import { dirname as dirname7, join as join12, resolve as resolve3 } from "node:path";
@@ -31596,7 +31699,7 @@ function announcedIn(directory) {
31596
31699
  const found = [];
31597
31700
  const candidates = [];
31598
31701
  const entries = [];
31599
- for (const entry of readdirSync4(directory)) {
31702
+ for (const entry of readdirSync6(directory)) {
31600
31703
  const report = ERROR_REPORT_PATTERN.exec(entry);
31601
31704
  if (report) {
31602
31705
  sweepErrorReport(join12(directory, entry), Number.parseInt(report[1] ?? "", 10));
@@ -31612,7 +31715,7 @@ function announcedIn(directory) {
31612
31715
  entries.push({ file, pid, alive });
31613
31716
  if (alive) {
31614
31717
  try {
31615
- candidates.push({ file, pid, writtenAt: statSync3(file).mtimeMs });
31718
+ candidates.push({ file, pid, writtenAt: statSync5(file).mtimeMs });
31616
31719
  } catch {}
31617
31720
  }
31618
31721
  }
@@ -31669,7 +31772,7 @@ function sweepErrorReport(path, pid, now = Date.now()) {
31669
31772
  return;
31670
31773
  }
31671
31774
  try {
31672
- if (now - statSync3(path).mtimeMs >= ERROR_REPORT_KEEP_MS) {
31775
+ if (now - statSync5(path).mtimeMs >= ERROR_REPORT_KEEP_MS) {
31673
31776
  unlinkSync(path);
31674
31777
  }
31675
31778
  } catch {}
@@ -31693,10 +31796,37 @@ async function announcedSince(projectPath, before, waiting = {}) {
31693
31796
  function describe2(endpoint) {
31694
31797
  return `pid ${endpoint.pid} on ${endpoint.address}:${endpoint.port} (${endpoint.project.name || "unnamed"} at ${endpoint.project.path})`;
31695
31798
  }
31799
+ function describeWithin(endpoint) {
31800
+ return `pid ${endpoint.pid} on ${endpoint.address}:${endpoint.port}`;
31801
+ }
31802
+ function theOneProject(endpoints) {
31803
+ if (endpoints.some((endpoint) => endpoint.project.path === "")) {
31804
+ return;
31805
+ }
31806
+ const paths = new Set(endpoints.map((endpoint) => resolve3(endpoint.project.path)));
31807
+ const only = [...paths][0];
31808
+ return paths.size === 1 ? only : undefined;
31809
+ }
31810
+ function severalFrom(project, games) {
31811
+ return `${games.length} games are running from ${project}: ${games.map(describeWithin).join("; ")}. ` + "Pass pid to choose one; projectPath cannot, because every one of them has this one. " + "editor_status lists them under runtimes.";
31812
+ }
31813
+ function whichHalfIsBehind(unspoken) {
31814
+ const pids = (games) => games.map((game) => `pid ${game.pid}`).join(", ");
31815
+ const ahead = unspoken.filter((game) => game.protocol > RUNTIME_PROTOCOL);
31816
+ const behind = unspoken.filter((game) => game.protocol < RUNTIME_PROTOCOL);
31817
+ if (ahead.length > 0 && behind.length > 0) {
31818
+ return `they are on both sides of it: reconnect this server for ${pids(ahead)}, and reinstall the ` + `addon and restart ${pids(behind)}`;
31819
+ }
31820
+ if (ahead.length > 0) {
31821
+ return "this server is the older half: reconnect it so it spawns the installed version";
31822
+ }
31823
+ return `the addon is the older half: reinstall it and restart ${behind.length === 1 ? "the game" : "those games"}`;
31824
+ }
31696
31825
  function tooNew(unspoken) {
31697
31826
  const named = unspoken.map((game) => `pid ${game.pid} speaks ${game.protocol} (${game.project.name || "unnamed"} at ${game.project.path})`).join("; ");
31698
- const half = unspoken.some((game) => game.protocol > RUNTIME_PROTOCOL) ? "this server is the older half: reconnect it so it spawns the installed version" : "the addon is the older half: reinstall it and restart the game";
31699
- return `A game is running, in a protocol this server does not speak ${named}. This server speaks ${RUNTIME_PROTOCOL}, so ${half}.`;
31827
+ const count = unspoken.length === 1 ? "A game is running" : `${unspoken.length} games are running`;
31828
+ const spoken = new Set(unspoken.map((game) => game.protocol)).size === 1 ? "a protocol" : "protocols";
31829
+ return `${count}, in ${spoken} this server does not speak: ${named}. This server speaks ${RUNTIME_PROTOCOL}, so ${whichHalfIsBehind(unspoken)}.`;
31700
31830
  }
31701
31831
  function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
31702
31832
  if (pid !== undefined) {
@@ -31713,8 +31843,16 @@ function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
31713
31843
  if (tooNewToo !== undefined) {
31714
31844
  return { problem: tooNew([tooNewToo]) };
31715
31845
  }
31846
+ if (endpoints.length > 0) {
31847
+ return {
31848
+ problem: `No running game has pid ${pid}. Running: ${endpoints.map(describe2).join("; ")}. editor_status lists them under runtimes as they come and go.`
31849
+ };
31850
+ }
31851
+ if (unspoken.length > 0) {
31852
+ return { problem: `No game reachable from here has pid ${pid}. ${tooNew(unspoken)}` };
31853
+ }
31716
31854
  return {
31717
- problem: endpoints.length === 0 ? `No game with the runtime addon is running, so there is none with pid ${pid}. Start one with editor_run, or play the project from the editor with the addon enabled.` : `No running game has pid ${pid}. Running: ${endpoints.map(describe2).join("; ")}. editor_status lists them under runtimes as they come and go.`
31855
+ problem: `No game with the runtime addon is running, so there is none with pid ${pid}. Start one with editor_run, or play the project from the editor with the addon enabled.`
31718
31856
  };
31719
31857
  }
31720
31858
  if (endpoints.length === 0) {
@@ -31746,15 +31884,17 @@ function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
31746
31884
  if (matching.length === 1 && matching[0]) {
31747
31885
  return { endpoint: matching[0] };
31748
31886
  }
31749
- return {
31750
- problem: `Several games are running from ${wanted}: ${matching.map(describe2).join("; ")}. Pass pid to choose one.`
31751
- };
31887
+ return { problem: severalFrom(wanted, matching) };
31752
31888
  }
31753
31889
  if (endpoints.length === 1 && endpoints[0]) {
31754
31890
  return { endpoint: endpoints[0] };
31755
31891
  }
31892
+ const shared = theOneProject(endpoints);
31893
+ if (shared !== undefined) {
31894
+ return { problem: severalFrom(shared, endpoints) };
31895
+ }
31756
31896
  return {
31757
- problem: `Several games are running: ${endpoints.map(describe2).join("; ")}. Pass projectPath to choose one, or pid when they are from one project.`
31897
+ problem: `Several games are running: ${endpoints.map(describe2).join("; ")}. Pass pid to choose one, or projectPath when the project you mean is running only one.`
31758
31898
  };
31759
31899
  }
31760
31900
  function runtimeRequest(endpoint, command, params, timeoutMs) {
@@ -32802,6 +32942,235 @@ var init_dap_client = __esm(() => {
32802
32942
  init_ports();
32803
32943
  });
32804
32944
 
32945
+ // src/editor-log.ts
32946
+ import {
32947
+ closeSync,
32948
+ existsSync as existsSync12,
32949
+ mkdirSync as mkdirSync10,
32950
+ openSync,
32951
+ readFileSync as readFileSync16,
32952
+ readSync,
32953
+ rmSync as rmSync8,
32954
+ statSync as statSync6,
32955
+ writeFileSync as writeFileSync12
32956
+ } from "node:fs";
32957
+ import { dirname as dirname12, join as join18 } from "node:path";
32958
+ function editorLogPath(projectPath) {
32959
+ return join18(projectPath, ".godot", "gdharness-editor.log");
32960
+ }
32961
+ function editorLogNotePath(projectPath) {
32962
+ return join18(projectPath, ".godot", "gdharness-editor.json");
32963
+ }
32964
+ function readEditorLogNote(projectPath) {
32965
+ try {
32966
+ const fields = asParams(JSON.parse(readFileSync16(editorLogNotePath(projectPath), "utf8")));
32967
+ const pid = readNumber(fields, "pid");
32968
+ const startedAt = readString(fields, "startedAt");
32969
+ return pid !== undefined && Number.isInteger(pid) && pid > 0 && startedAt !== undefined ? { pid, startedAt } : null;
32970
+ } catch {
32971
+ return null;
32972
+ }
32973
+ }
32974
+ function clearEditorLog(projectPath) {
32975
+ try {
32976
+ const path = editorLogPath(projectPath);
32977
+ mkdirSync10(dirname12(path), { recursive: true });
32978
+ rmSync8(path, { force: true });
32979
+ rmSync8(editorLogNotePath(projectPath), { force: true });
32980
+ } catch {}
32981
+ }
32982
+ function writeEditorLogNote(projectPath, pid) {
32983
+ try {
32984
+ const path = editorLogNotePath(projectPath);
32985
+ mkdirSync10(dirname12(path), { recursive: true });
32986
+ writeFileSync12(path, JSON.stringify({ pid, startedAt: new Date().toISOString() }, null, 2), "utf8");
32987
+ } catch {}
32988
+ }
32989
+ function theConsoleWasNotCaptured(why, projectPath) {
32990
+ if (why.kind === "not ours") {
32991
+ return "This editor was not opened by gdharness, so its console was never captured and nothing " + "here has it: an editor prints to a console no plugin can read, and the addon cannot get " + "at it from inside. This is not an editor that printed nothing.";
32992
+ }
32993
+ if (why.kind === "older server") {
32994
+ return `This editor was opened by a gdharness server, but the console beside ${projectPath} belongs ` + `to editor ${why.notedPid ?? "none recorded"} rather than to this one, so it holds an ` + "earlier session and is not offered as this one. A server from before consoles were " + "captured leaves exactly this.";
32995
+ }
32996
+ return "This editor was opened by gdharness and has written nothing to its console file yet, not " + "even the engine version it prints first, so the file is missing rather than empty. An " + "editor that could not open the file for writing leaves this.";
32997
+ }
32998
+ function editorConsole(projectPath, editorPid, openedByAServer) {
32999
+ if (!openedByAServer) {
33000
+ return { kind: "not ours" };
33001
+ }
33002
+ const noted = readEditorLogNote(projectPath);
33003
+ if (noted === null || editorPid === null || noted.pid !== editorPid) {
33004
+ return { kind: "older server", notedPid: noted?.pid ?? null };
33005
+ }
33006
+ const path = editorLogPath(projectPath);
33007
+ if (!existsSync12(path)) {
33008
+ return { kind: "not written" };
33009
+ }
33010
+ const log = new GameLog;
33011
+ const { text, of } = theFirstOf(path);
33012
+ log.append("transcript", text);
33013
+ log.finish();
33014
+ return { log, path, ...of === undefined ? {} : { readBytes: text.length, of } };
33015
+ }
33016
+ function theFirstOf(path) {
33017
+ const whole = statSync6(path).size;
33018
+ if (whole <= MOST_CONSOLE_BYTES) {
33019
+ return { text: readFileSync16(path, "utf8") };
33020
+ }
33021
+ const handle = openSync(path, "r");
33022
+ try {
33023
+ const buffer = Buffer.alloc(MOST_CONSOLE_BYTES);
33024
+ const read = readSync(handle, buffer, 0, MOST_CONSOLE_BYTES, 0);
33025
+ const decoded = buffer.subarray(0, read).toString("utf8");
33026
+ const lastLine = decoded.lastIndexOf(`
33027
+ `);
33028
+ return { text: lastLine === -1 ? decoded : decoded.slice(0, lastLine + 1), of: whole };
33029
+ } finally {
33030
+ closeSync(handle);
33031
+ }
33032
+ }
33033
+ function shapeOf(text) {
33034
+ const values = [];
33035
+ let shape = "";
33036
+ let at = 0;
33037
+ while (at < text.length) {
33038
+ let earliest = null;
33039
+ for (const pattern of SLOTS) {
33040
+ pattern.lastIndex = at;
33041
+ const found = pattern.exec(text);
33042
+ if (found !== null && (earliest === null || found.index < earliest.index || found.index === earliest.index && found[0].length > earliest.value.length)) {
33043
+ earliest = { index: found.index, value: found[0] };
33044
+ }
33045
+ }
33046
+ if (earliest === null) {
33047
+ shape += text.slice(at);
33048
+ break;
33049
+ }
33050
+ if (earliest.value.length === 0) {
33051
+ shape += text.slice(at, at + 1);
33052
+ at += 1;
33053
+ continue;
33054
+ }
33055
+ shape += `${text.slice(at, earliest.index)}…`;
33056
+ values.push(earliest.value);
33057
+ at = earliest.index + earliest.value.length;
33058
+ }
33059
+ return { shape, values };
33060
+ }
33061
+ function bursts(entries, before) {
33062
+ const shapes = new Map;
33063
+ const shaped = entries.map((entry) => shapeOf(entry.text));
33064
+ for (const [at, entry] of entries.entries()) {
33065
+ const key = `${entry.severity}\x00${shaped[at]?.shape ?? entry.text}`;
33066
+ const group = shapes.get(key) ?? { indices: [], values: [] };
33067
+ group.indices.push(at);
33068
+ group.values.push(shaped[at]?.values ?? []);
33069
+ shapes.set(key, group);
33070
+ }
33071
+ const collapsed = new Set;
33072
+ for (const group of shapes.values()) {
33073
+ if (group.indices.length > 1) {
33074
+ for (const at of group.indices) {
33075
+ collapsed.add(at);
33076
+ }
33077
+ }
33078
+ }
33079
+ const repeated = [];
33080
+ for (const [key, group] of shapes) {
33081
+ const first = group.indices[0];
33082
+ if (group.indices.length < 2 || first === undefined) {
33083
+ continue;
33084
+ }
33085
+ const entry = entries[first];
33086
+ if (entry === undefined) {
33087
+ continue;
33088
+ }
33089
+ repeated.push({
33090
+ shape: key.slice(key.indexOf("\x00") + 1),
33091
+ count: group.indices.length,
33092
+ severity: entry.severity,
33093
+ first: { index: entry.index, text: entry.text },
33094
+ slots: slotsOf(group.values, group.indices.length),
33095
+ before: linesBefore(entries, first, collapsed, before)
33096
+ });
33097
+ }
33098
+ return repeated.sort((one, other) => other.count - one.count);
33099
+ }
33100
+ function slotsOf(values, count) {
33101
+ const width = Math.max(0, ...values.map((row) => row.length));
33102
+ const distinct = [];
33103
+ for (let at = 0;at < width; at += 1) {
33104
+ distinct.push([...new Set(values.map((row) => row[at]).filter((one) => one !== undefined))]);
33105
+ }
33106
+ const slots = [];
33107
+ for (let at = 0;at < width; at += 1) {
33108
+ const seen = distinct[at] ?? [];
33109
+ const partner = theSlotItMovesWith(values, distinct, at);
33110
+ const paired = partner === undefined ? {} : { movesWith: partner };
33111
+ if (seen.length === count && count > 1) {
33112
+ slots.push({ distinct: seen.length, allDifferent: true, ...paired });
33113
+ continue;
33114
+ }
33115
+ const some = seen.slice(0, NAMED_VALUES);
33116
+ slots.push({
33117
+ distinct: seen.length,
33118
+ some,
33119
+ ...seen.length > some.length ? { more: seen.length - some.length } : {},
33120
+ ...paired
33121
+ });
33122
+ }
33123
+ return slots;
33124
+ }
33125
+ function theSlotItMovesWith(values, distinct, at) {
33126
+ const mine = distinct[at] ?? [];
33127
+ if (mine.length < 2) {
33128
+ return;
33129
+ }
33130
+ for (let other = 0;other < at; other += 1) {
33131
+ if ((distinct[other] ?? []).length !== mine.length) {
33132
+ continue;
33133
+ }
33134
+ const goesWith = new Map;
33135
+ let oneForOne = true;
33136
+ for (const row of values) {
33137
+ const theirs = row[other];
33138
+ const ours = row[at];
33139
+ if (theirs === undefined || ours === undefined) {
33140
+ oneForOne = false;
33141
+ break;
33142
+ }
33143
+ const already = goesWith.get(theirs);
33144
+ if (already === undefined) {
33145
+ goesWith.set(theirs, ours);
33146
+ } else if (already !== ours) {
33147
+ oneForOne = false;
33148
+ break;
33149
+ }
33150
+ }
33151
+ if (oneForOne) {
33152
+ return other;
33153
+ }
33154
+ }
33155
+ return;
33156
+ }
33157
+ function linesBefore(entries, at, collapsed, wanted) {
33158
+ const found = [];
33159
+ for (let back = at - 1;back >= 0 && at - back <= WALK_BACK_LIMIT && found.length < wanted; back -= 1) {
33160
+ const entry = entries[back];
33161
+ if (entry !== undefined && !collapsed.has(back)) {
33162
+ found.push(entry.text);
33163
+ }
33164
+ }
33165
+ return found.reverse();
33166
+ }
33167
+ var MOST_CONSOLE_BYTES, NAMED_VALUES = 6, WALK_BACK_LIMIT = 500, SLOTS;
33168
+ var init_editor_log = __esm(() => {
33169
+ init_game_log();
33170
+ MOST_CONSOLE_BYTES = 8 * 1024 * 1024;
33171
+ SLOTS = [/"[^"]*"/g, /'[^']*'/g, /\b(?:res|user):\/\/\S+/g, /(?<=\.gd|\.tscn|\.cs):\d+(?::\d+)?/g];
33172
+ });
33173
+
32805
33174
  // node_modules/ws/lib/constants.js
32806
33175
  var require_constants = __commonJS(function(exports, module) {
32807
33176
  var BINARY_TYPES = ["nodebuffer", "arraybuffer", "fragments"];
@@ -35863,6 +36232,7 @@ var init_godot_bridge = __esm(() => {
35863
36232
  connectedAt: this.connectionInfo?.connectedAt,
35864
36233
  lastPongAt: this.connectionInfo?.lastPongAt,
35865
36234
  addonVersion: this.connectionInfo?.addonVersion,
36235
+ addonDigest: this.connectionInfo?.addonDigest,
35866
36236
  editorPid: this.connectionInfo?.editorPid,
35867
36237
  lspPort: this.connectionInfo?.lspPort,
35868
36238
  dapPort: this.connectionInfo?.dapPort,
@@ -36006,6 +36376,7 @@ var init_godot_bridge = __esm(() => {
36006
36376
  }
36007
36377
  this.connectionInfo.projectPath = message.project_path;
36008
36378
  this.connectionInfo.addonVersion = message.addon_version ?? "";
36379
+ this.connectionInfo.addonDigest = message.addon_digest;
36009
36380
  this.connectionInfo.editorPid = message.editor_pid;
36010
36381
  this.connectionInfo.lspPort = servedPort(message.lsp_port);
36011
36382
  this.connectionInfo.dapPort = servedPort(message.dap_port);
@@ -36182,7 +36553,7 @@ var init_godot_bridge = __esm(() => {
36182
36553
 
36183
36554
  // src/godot-path.ts
36184
36555
  import { execFile as execFile5 } from "node:child_process";
36185
- import { existsSync as existsSync12 } from "node:fs";
36556
+ import { existsSync as existsSync13 } from "node:fs";
36186
36557
  import { normalize as normalize2 } from "node:path";
36187
36558
  import { promisify as promisify5 } from "node:util";
36188
36559
 
@@ -36237,7 +36608,7 @@ class GodotLocator2 {
36237
36608
  return known;
36238
36609
  }
36239
36610
  let reason;
36240
- if (path !== "godot" && !existsSync12(path)) {
36611
+ if (path !== "godot" && !existsSync13(path)) {
36241
36612
  reason = "does not exist";
36242
36613
  } else {
36243
36614
  try {
@@ -36259,7 +36630,7 @@ var init_godot_path = __esm(() => {
36259
36630
  });
36260
36631
 
36261
36632
  // src/issues.ts
36262
- import { createHash as createHash2 } from "node:crypto";
36633
+ import { createHash as createHash4 } from "node:crypto";
36263
36634
  import process7 from "node:process";
36264
36635
  function feedbackNotice() {
36265
36636
  return JSON.stringify({
@@ -36277,7 +36648,7 @@ function runtime2() {
36277
36648
  function defectSignature2(where, message) {
36278
36649
  const normalised = `${where}
36279
36650
  ${message}`.toLowerCase().replaceAll("\\", "/").replaceAll(/(?:[a-z]:)?(?:\/+[\w.-]+){2,}/gu, "<path>").replaceAll(/\d+/gu, "0");
36280
- return createHash2("sha256").update(normalised).digest("hex").slice(0, 8);
36651
+ return createHash4("sha256").update(normalised).digest("hex").slice(0, 8);
36281
36652
  }
36282
36653
  function facts2(where, message, godotVersion) {
36283
36654
  return [
@@ -36613,8 +36984,8 @@ var init_junit = __esm(() => {
36613
36984
  import { realpathSync as realpathSync2 } from "node:fs";
36614
36985
  import { readFile, realpath } from "node:fs/promises";
36615
36986
  import { createConnection as createConnection3 } from "node:net";
36616
- import { dirname as dirname12, resolve as resolve4 } from "node:path";
36617
- import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
36987
+ import { dirname as dirname13, resolve as resolve4 } from "node:path";
36988
+ import { fileURLToPath as fileURLToPath4, pathToFileURL } from "node:url";
36618
36989
  function diagnosticsKey(uri) {
36619
36990
  let decoded;
36620
36991
  try {
@@ -36626,7 +36997,7 @@ function diagnosticsKey(uri) {
36626
36997
  return decoded;
36627
36998
  }
36628
36999
  try {
36629
- return realpathSync2.native(fileURLToPath3(decoded)).toLowerCase();
37000
+ return realpathSync2.native(fileURLToPath4(decoded)).toLowerCase();
36630
37001
  } catch {
36631
37002
  return decoded.toLowerCase();
36632
37003
  }
@@ -36741,7 +37112,7 @@ class GodotLSPClient {
36741
37112
  if (this.initialized) {
36742
37113
  return;
36743
37114
  }
36744
- const rootPath = this.rootPath ?? dirname12(resolve4(filePath));
37115
+ const rootPath = this.rootPath ?? dirname13(resolve4(filePath));
36745
37116
  await this.initialize(rootPath);
36746
37117
  }
36747
37118
  async sendRequest(method, params) {
@@ -37186,13 +37557,13 @@ var init_process_time = __esm(() => {
37186
37557
  });
37187
37558
 
37188
37559
  // src/project-scan.ts
37189
- import { existsSync as existsSync13, readdirSync as readdirSync5, readFileSync as readFileSync16 } from "node:fs";
37190
- import { join as join18 } from "node:path";
37560
+ import { existsSync as existsSync14, readdirSync as readdirSync7, readFileSync as readFileSync17 } from "node:fs";
37561
+ import { join as join19 } from "node:path";
37191
37562
  function skipped(name) {
37192
37563
  return name.startsWith(".") || SKIPPED.has(name);
37193
37564
  }
37194
37565
  function steppedOver(directory) {
37195
- return existsSync13(join18(directory, ".gdignore"));
37566
+ return existsSync14(join19(directory, ".gdignore"));
37196
37567
  }
37197
37568
  function projectStructure(projectPath) {
37198
37569
  const structure = { scenes: 0, scripts: 0, assets: 0, other: 0 };
@@ -37200,12 +37571,12 @@ function projectStructure(projectPath) {
37200
37571
  if (steppedOver(directory)) {
37201
37572
  return;
37202
37573
  }
37203
- for (const entry of readdirSync5(directory, { withFileTypes: true })) {
37574
+ for (const entry of readdirSync7(directory, { withFileTypes: true })) {
37204
37575
  if (skipped(entry.name)) {
37205
37576
  continue;
37206
37577
  }
37207
37578
  if (entry.isDirectory()) {
37208
- visit(join18(directory, entry.name));
37579
+ visit(join19(directory, entry.name));
37209
37580
  } else if (entry.isFile()) {
37210
37581
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
37211
37582
  if (extension === "tscn") {
@@ -37229,20 +37600,20 @@ function scriptsWithoutUid(projectPath) {
37229
37600
  if (steppedOver(directory)) {
37230
37601
  return;
37231
37602
  }
37232
- for (const entry of readdirSync5(directory, { withFileTypes: true })) {
37603
+ for (const entry of readdirSync7(directory, { withFileTypes: true })) {
37233
37604
  if (skipped(entry.name)) {
37234
37605
  continue;
37235
37606
  }
37236
37607
  const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
37237
37608
  if (entry.isDirectory()) {
37238
- visit(join18(directory, entry.name), spelled);
37609
+ visit(join19(directory, entry.name), spelled);
37239
37610
  continue;
37240
37611
  }
37241
37612
  if (!entry.isFile()) {
37242
37613
  continue;
37243
37614
  }
37244
37615
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
37245
- if (CARRIES_A_UID.has(extension) && !existsSync13(join18(directory, `${entry.name}.uid`))) {
37616
+ if (CARRIES_A_UID.has(extension) && !existsSync14(join19(directory, `${entry.name}.uid`))) {
37246
37617
  found.push(spelled);
37247
37618
  }
37248
37619
  }
@@ -37270,14 +37641,14 @@ function searchProject(projectPath, options) {
37270
37641
  if (steppedOver(directory)) {
37271
37642
  return;
37272
37643
  }
37273
- for (const entry of readdirSync5(directory, { withFileTypes: true })) {
37644
+ for (const entry of readdirSync7(directory, { withFileTypes: true })) {
37274
37645
  if (full()) {
37275
37646
  return;
37276
37647
  }
37277
37648
  if (skipped(entry.name)) {
37278
37649
  continue;
37279
37650
  }
37280
- const entryPath = join18(directory, entry.name);
37651
+ const entryPath = join19(directory, entry.name);
37281
37652
  if (entry.isDirectory()) {
37282
37653
  visit(entryPath);
37283
37654
  continue;
@@ -37288,7 +37659,7 @@ function searchProject(projectPath, options) {
37288
37659
  }
37289
37660
  result.summary.files_searched += 1;
37290
37661
  const matches = [];
37291
- for (const [index, line] of readFileSync16(entryPath, "utf8").split(`
37662
+ for (const [index, line] of readFileSync17(entryPath, "utf8").split(`
37292
37663
  `).entries()) {
37293
37664
  if (full()) {
37294
37665
  break;
@@ -37317,16 +37688,16 @@ var init_project_scan = __esm(() => {
37317
37688
  });
37318
37689
 
37319
37690
  // src/restart-note.ts
37320
- import { mkdirSync as mkdirSync10, readFileSync as readFileSync17, rmSync as rmSync8, writeFileSync as writeFileSync12 } from "node:fs";
37321
- import { dirname as dirname13, join as join19 } from "node:path";
37691
+ import { mkdirSync as mkdirSync11, readFileSync as readFileSync18, rmSync as rmSync9, writeFileSync as writeFileSync13 } from "node:fs";
37692
+ import { dirname as dirname14, join as join20 } from "node:path";
37322
37693
  function restartNotePath(projectPath) {
37323
- return join19(projectPath, ".godot", "gdharness-restart.json");
37694
+ return join20(projectPath, ".godot", "gdharness-restart.json");
37324
37695
  }
37325
37696
  function noteRestartBegun(note) {
37326
37697
  const path = restartNotePath(note.projectPath);
37327
37698
  try {
37328
- mkdirSync10(dirname13(path), { recursive: true });
37329
- writeFileSync12(path, `${JSON.stringify(note, null, 2)}
37699
+ mkdirSync11(dirname14(path), { recursive: true });
37700
+ writeFileSync13(path, `${JSON.stringify(note, null, 2)}
37330
37701
  `, "utf8");
37331
37702
  } catch (error) {
37332
37703
  console.error(`[SERVER] Could not record the restart at ${path}: ${errorMessage(error)}`);
@@ -37334,7 +37705,7 @@ function noteRestartBegun(note) {
37334
37705
  }
37335
37706
  function restartOwed(projectPath) {
37336
37707
  try {
37337
- const parsed = JSON.parse(readFileSync17(restartNotePath(projectPath), "utf8"));
37708
+ const parsed = JSON.parse(readFileSync18(restartNotePath(projectPath), "utf8"));
37338
37709
  if (typeof parsed !== "object" || parsed === null) {
37339
37710
  return null;
37340
37711
  }
@@ -37349,7 +37720,7 @@ function restartOwed(projectPath) {
37349
37720
  }
37350
37721
  function restartSettled(projectPath) {
37351
37722
  try {
37352
- rmSync8(restartNotePath(projectPath), { force: true });
37723
+ rmSync9(restartNotePath(projectPath), { force: true });
37353
37724
  } catch (error) {
37354
37725
  console.error(`[SERVER] Could not clear the restart note: ${errorMessage(error)}`);
37355
37726
  }
@@ -37360,36 +37731,36 @@ var init_restart_note = __esm(() => {
37360
37731
 
37361
37732
  // src/run-record.ts
37362
37733
  import { execFileSync as execFileSync2 } from "node:child_process";
37363
- import { mkdirSync as mkdirSync11, openSync, readdirSync as readdirSync6, readFileSync as readFileSync18, rmSync as rmSync9, statSync as statSync4, writeFileSync as writeFileSync13 } from "node:fs";
37364
- import { basename as basename3, join as join20 } from "node:path";
37734
+ import { mkdirSync as mkdirSync12, openSync as openSync2, readdirSync as readdirSync8, readFileSync as readFileSync19, rmSync as rmSync10, statSync as statSync7, writeFileSync as writeFileSync14 } from "node:fs";
37735
+ import { basename as basename3, join as join21 } from "node:path";
37365
37736
  function runsDirectory() {
37366
- return join20(runtimeDirectory(), "runs");
37737
+ return join21(runtimeDirectory(), "runs");
37367
37738
  }
37368
37739
  function recordPath() {
37369
- return join20(runsDirectory(), "run.json");
37740
+ return join21(runsDirectory(), "run.json");
37370
37741
  }
37371
37742
  function recordPaths() {
37372
- return runtimeDirectories().map((directory) => join20(directory, "runs", "run.json"));
37743
+ return runtimeDirectories().map((directory) => join21(directory, "runs", "run.json"));
37373
37744
  }
37374
37745
  function openTranscript(startedAt) {
37375
- mkdirSync11(runsDirectory(), { recursive: true });
37376
- const path = join20(runsDirectory(), `run-${startedAt}.log`);
37377
- return { path, fd: openSync(path, "a") };
37746
+ mkdirSync12(runsDirectory(), { recursive: true });
37747
+ const path = join21(runsDirectory(), `run-${startedAt}.log`);
37748
+ return { path, fd: openSync2(path, "a") };
37378
37749
  }
37379
37750
  function writeRunRecord(record) {
37380
- mkdirSync11(runsDirectory(), { recursive: true });
37381
- writeFileSync13(recordPath(), JSON.stringify(record, null, 2), "utf8");
37751
+ mkdirSync12(runsDirectory(), { recursive: true });
37752
+ writeFileSync14(recordPath(), JSON.stringify(record, null, 2), "utf8");
37382
37753
  }
37383
37754
  function editorNotePath() {
37384
- return join20(runsDirectory(), "editor-run.json");
37755
+ return join21(runsDirectory(), "editor-run.json");
37385
37756
  }
37386
37757
  function writeEditorRunNote(note) {
37387
- mkdirSync11(runsDirectory(), { recursive: true });
37388
- writeFileSync13(editorNotePath(), JSON.stringify(note, null, 2), "utf8");
37758
+ mkdirSync12(runsDirectory(), { recursive: true });
37759
+ writeFileSync14(editorNotePath(), JSON.stringify(note, null, 2), "utf8");
37389
37760
  }
37390
37761
  function readEditorRunNote() {
37391
37762
  for (const directory of runtimeDirectories()) {
37392
- const note = editorNoteAt(join20(directory, "runs", "editor-run.json"));
37763
+ const note = editorNoteAt(join21(directory, "runs", "editor-run.json"));
37393
37764
  if (note !== null) {
37394
37765
  return note;
37395
37766
  }
@@ -37399,7 +37770,7 @@ function readEditorRunNote() {
37399
37770
  function editorNoteAt(path) {
37400
37771
  let parsed;
37401
37772
  try {
37402
- parsed = JSON.parse(readFileSync18(path, "utf8"));
37773
+ parsed = JSON.parse(readFileSync19(path, "utf8"));
37403
37774
  } catch {
37404
37775
  return null;
37405
37776
  }
@@ -37422,7 +37793,7 @@ function recordRunEnded(pid, exitCode) {
37422
37793
  return;
37423
37794
  }
37424
37795
  try {
37425
- writeFileSync13(path, JSON.stringify({ ...record, exitCode }, null, 2), "utf8");
37796
+ writeFileSync14(path, JSON.stringify({ ...record, exitCode }, null, 2), "utf8");
37426
37797
  } catch {}
37427
37798
  }
37428
37799
  function readRunRecord() {
@@ -37437,7 +37808,7 @@ function readRunRecord() {
37437
37808
  function recordAt(path) {
37438
37809
  let parsed;
37439
37810
  try {
37440
- parsed = JSON.parse(readFileSync18(path, "utf8"));
37811
+ parsed = JSON.parse(readFileSync19(path, "utf8"));
37441
37812
  } catch {
37442
37813
  return null;
37443
37814
  }
@@ -37482,7 +37853,7 @@ function runningAs(pid) {
37482
37853
  const began = startedAt(pid, null);
37483
37854
  const when = began === null ? {} : { startedAt: began };
37484
37855
  if (process.platform === "linux") {
37485
- const raw = readFileSync18(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
37856
+ const raw = readFileSync19(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
37486
37857
  return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
37487
37858
  }
37488
37859
  const args = execFileSync2("ps", ["-p", String(pid), "-o", "args="], {
@@ -37609,13 +37980,13 @@ function clearRunRecord(ours = () => true) {
37609
37980
  continue;
37610
37981
  }
37611
37982
  try {
37612
- rmSync9(path);
37983
+ rmSync10(path);
37613
37984
  } catch {}
37614
37985
  }
37615
37986
  }
37616
37987
  function ageMs(path, now) {
37617
37988
  try {
37618
- return now - statSync4(path).mtimeMs;
37989
+ return now - statSync7(path).mtimeMs;
37619
37990
  } catch {
37620
37991
  return 0;
37621
37992
  }
@@ -37625,7 +37996,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
37625
37996
  const keep = readRunRecord()?.transcript;
37626
37997
  let entries;
37627
37998
  try {
37628
- entries = readdirSync6(directory);
37999
+ entries = readdirSync8(directory);
37629
38000
  } catch {
37630
38001
  return;
37631
38002
  }
@@ -37633,12 +38004,12 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
37633
38004
  if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
37634
38005
  continue;
37635
38006
  }
37636
- const path = join20(directory, entry);
38007
+ const path = join21(directory, entry);
37637
38008
  if (path === keep || ageMs(path, now) < keepMs) {
37638
38009
  continue;
37639
38010
  }
37640
38011
  try {
37641
- rmSync9(path);
38012
+ rmSync10(path);
37642
38013
  } catch {}
37643
38014
  }
37644
38015
  }
@@ -37654,22 +38025,22 @@ import { execFile as execFile7, spawn } from "node:child_process";
37654
38025
  import { randomUUID as randomUUID2 } from "node:crypto";
37655
38026
  import {
37656
38027
  appendFileSync,
37657
- closeSync,
37658
- existsSync as existsSync14,
38028
+ closeSync as closeSync2,
38029
+ existsSync as existsSync15,
37659
38030
  fstatSync,
37660
- mkdirSync as mkdirSync12,
38031
+ mkdirSync as mkdirSync13,
37661
38032
  mkdtempSync as mkdtempSync3,
37662
- openSync as openSync2,
37663
- readdirSync as readdirSync7,
37664
- readFileSync as readFileSync19,
37665
- readSync,
38033
+ openSync as openSync3,
38034
+ readdirSync as readdirSync9,
38035
+ readFileSync as readFileSync20,
38036
+ readSync as readSync2,
37666
38037
  rmdirSync,
37667
- statSync as statSync5
38038
+ statSync as statSync8
37668
38039
  } from "node:fs";
37669
38040
  import { tmpdir as tmpdir5 } from "node:os";
37670
- import { basename as basename4, dirname as dirname14, isAbsolute as isAbsolute3, join as join21, normalize as normalize3, relative as relative3 } from "node:path";
38041
+ import { basename as basename4, dirname as dirname15, isAbsolute as isAbsolute3, join as join22, normalize as normalize3, relative as relative3 } from "node:path";
37671
38042
  import { setTimeout as delay3 } from "node:timers/promises";
37672
- import { fileURLToPath as fileURLToPath4 } from "node:url";
38043
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
37673
38044
  import { promisify as promisify7 } from "node:util";
37674
38045
  function patienceForFrames(frames, atLeast) {
37675
38046
  return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
@@ -37909,13 +38280,13 @@ function aboveTheRunner(entry) {
37909
38280
  }
37910
38281
  function elsewhereIn(projectPath, asked) {
37911
38282
  const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
37912
- if (wanted === "" || existsSync14(join21(projectPath, wanted))) {
38283
+ if (wanted === "" || existsSync15(join22(projectPath, wanted))) {
37913
38284
  return "";
37914
38285
  }
37915
38286
  const root = (wanted.split("/")[0] ?? "").toLowerCase();
37916
38287
  let near = [];
37917
38288
  try {
37918
- near = readdirSync7(projectPath, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.toLowerCase() !== root).map((entry) => entry.name).filter((name) => name.toLowerCase().startsWith("test") || root.startsWith(name.toLowerCase()));
38289
+ near = readdirSync9(projectPath, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.toLowerCase() !== root).map((entry) => entry.name).filter((name) => name.toLowerCase().startsWith("test") || root.startsWith(name.toLowerCase()));
37919
38290
  } catch {
37920
38291
  return "";
37921
38292
  }
@@ -37923,7 +38294,7 @@ function elsewhereIn(projectPath, asked) {
37923
38294
  return near.length === 0 ? "" : ` The project does have ${there}.`;
37924
38295
  }
37925
38296
  function hasMainScene(projectFile) {
37926
- const scene = parseProjectGodot(readFileSync19(projectFile, "utf8"))["application"]?.["run/main_scene"];
38297
+ const scene = parseProjectGodot(readFileSync20(projectFile, "utf8"))["application"]?.["run/main_scene"];
37927
38298
  return typeof scene === "string" && scene !== "";
37928
38299
  }
37929
38300
  function runtimeAddonAnnouncesEditors(projectPath) {
@@ -37931,13 +38302,13 @@ function runtimeAddonAnnouncesEditors(projectPath) {
37931
38302
  return false;
37932
38303
  }
37933
38304
  try {
37934
- return readFileSync19(join21(projectPath, RUNTIME_AUTOLOAD2.path), "utf8").includes('"editor_pid"');
38305
+ return readFileSync20(join22(projectPath, RUNTIME_AUTOLOAD2.path), "utf8").includes('"editor_pid"');
37935
38306
  } catch {
37936
38307
  return false;
37937
38308
  }
37938
38309
  }
37939
38310
  function editorPlaysHeadless(projectFile) {
37940
- const runArgs = parseProjectGodot(readFileSync19(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
38311
+ const runArgs = parseProjectGodot(readFileSync20(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
37941
38312
  return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
37942
38313
  }
37943
38314
  function camelCased(params) {
@@ -37956,7 +38327,7 @@ function needsItsOwnProcess(asked) {
37956
38327
  class GodotServer {
37957
38328
  mcp;
37958
38329
  locator = new GodotLocator2;
37959
- operationsScript = join21(__dirname2, "godot", "operations", "godot_operations.gd");
38330
+ operationsScript = join22(__dirname2, "godot", "operations", "godot_operations.gd");
37960
38331
  godotBridge;
37961
38332
  tools = buildToolDefinitions();
37962
38333
  activeProcess = null;
@@ -38621,8 +38992,8 @@ class GodotServer {
38621
38992
  if (path === undefined) {
38622
38993
  return { ok: false, response: this.createErrorResponse("projectPath is required.") };
38623
38994
  }
38624
- const file = join21(path, "project.godot");
38625
- if (!existsSync14(file)) {
38995
+ const file = join22(path, "project.godot");
38996
+ if (!existsSync15(file)) {
38626
38997
  return {
38627
38998
  ok: false,
38628
38999
  response: this.createErrorResponse(`Not a Godot project: ${path}`, [
@@ -38890,7 +39261,7 @@ class GodotServer {
38890
39261
  if (!engine.ok) {
38891
39262
  return engine.response;
38892
39263
  }
38893
- const application = parseProjectGodot(readFileSync19(project.value.file, "utf8"))["application"] ?? {};
39264
+ const application = parseProjectGodot(readFileSync20(project.value.file, "utf8"))["application"] ?? {};
38894
39265
  const name = application["config/name"];
38895
39266
  const mainScene = application["run/main_scene"];
38896
39267
  const info = {
@@ -38951,18 +39322,18 @@ class GodotServer {
38951
39322
  if (!engine.ok) {
38952
39323
  return engine.response;
38953
39324
  }
38954
- const folder = dirname14(output.absolutePath);
39325
+ const folder = dirname15(output.absolutePath);
38955
39326
  try {
38956
- mkdirSync12(folder, { recursive: true });
39327
+ mkdirSync13(folder, { recursive: true });
38957
39328
  } catch (error) {
38958
- return this.createErrorResponse(`Cannot create ${dirname14(output.relativePath)} to export into: ${errorMessage(error)}`);
39329
+ return this.createErrorResponse(`Cannot create ${dirname15(output.relativePath)} to export into: ${errorMessage(error)}`);
38959
39330
  }
38960
39331
  const debug = readBoolean(args, "debug") ?? false;
38961
- const exportLogs = mkdtempSync3(join21(tmpdir5(), "gdharness-export-"));
39332
+ const exportLogs = mkdtempSync3(join22(tmpdir5(), "gdharness-export-"));
38962
39333
  const exportArgs = [
38963
39334
  "--headless",
38964
39335
  "--log-file",
38965
- join21(exportLogs, "engine.log"),
39336
+ join22(exportLogs, "engine.log"),
38966
39337
  "--path",
38967
39338
  project.value.path,
38968
39339
  debug ? "--export-debug" : "--export-release",
@@ -38990,7 +39361,7 @@ class GodotServer {
38990
39361
  log.finish();
38991
39362
  const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
38992
39363
  const verdict = {
38993
- exported: exitCode === 0 && log.count("error") === 0 && existsSync14(output.absolutePath),
39364
+ exported: exitCode === 0 && log.count("error") === 0 && existsSync15(output.absolutePath),
38994
39365
  preset,
38995
39366
  outputPath: output.relativePath,
38996
39367
  debug,
@@ -39020,7 +39391,7 @@ class GodotServer {
39020
39391
  return contained.response;
39021
39392
  }
39022
39393
  const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
39023
- if (!existsSync14(join21(project.value.path, runner))) {
39394
+ if (!existsSync15(join22(project.value.path, runner))) {
39024
39395
  return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
39025
39396
  "Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
39026
39397
  ]);
@@ -39055,7 +39426,7 @@ class GodotServer {
39055
39426
  ];
39056
39427
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
39057
39428
  this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
39058
- const userData = mkdtempSync3(join21(tmpdir5(), "gdharness-tests-"));
39429
+ const userData = mkdtempSync3(join22(tmpdir5(), "gdharness-tests-"));
39059
39430
  const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
39060
39431
  const hung = await new Promise((resolve) => {
39061
39432
  const timer = setTimeout(() => {
@@ -39071,21 +39442,21 @@ class GodotServer {
39071
39442
  resolve(false);
39072
39443
  });
39073
39444
  });
39074
- const reportsDir = join21(project.value.path, ".godot", ...ours.split("/"));
39445
+ const reportsDir = join22(project.value.path, ".godot", ...ours.split("/"));
39075
39446
  let report = null;
39076
39447
  let reportProblem = null;
39077
39448
  try {
39078
- const written = existsSync14(reportsDir) ? readdirSync7(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
39449
+ const written = existsSync15(reportsDir) ? readdirSync9(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
39079
39450
  const newest = written.at(-1);
39080
39451
  if (newest !== undefined) {
39081
- report = parseJUnit(readFileSync19(join21(reportsDir, newest, "results.xml"), "utf8"));
39452
+ report = parseJUnit(readFileSync20(join22(reportsDir, newest, "results.xml"), "utf8"));
39082
39453
  }
39083
39454
  } catch (error) {
39084
39455
  reportProblem = errorMessage(error);
39085
39456
  } finally {
39086
39457
  discard(reportsDir, userData);
39087
39458
  try {
39088
- rmdirSync(dirname14(reportsDir));
39459
+ rmdirSync(dirname15(reportsDir));
39089
39460
  } catch {}
39090
39461
  }
39091
39462
  const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries.map(aboveTheRunner);
@@ -39204,7 +39575,7 @@ class GodotServer {
39204
39575
  warnings: diagnostics.length - errors,
39205
39576
  diagnostics,
39206
39577
  ...disagrees
39207
- }, this.godotBridge.getStatus().addonVersion, SERVER_VERSION));
39578
+ }, this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest()));
39208
39579
  }
39209
39580
  whereTheFileDisagrees(diagnostics, args) {
39210
39581
  const messages = diagnostics.map((entry) => asParams(entry)["message"]).filter((message) => typeof message === "string");
@@ -39217,11 +39588,11 @@ class GodotServer {
39217
39588
  const cached = cachedClasses(projectPath);
39218
39589
  const contradicted = cached === null ? [] : contradictedDiagnostics(messages, cached, (resourcePath) => {
39219
39590
  const contained = resolveWithinProject(projectPath, resourcePath);
39220
- if (!contained.ok || !existsSync14(contained.absolutePath)) {
39591
+ if (!contained.ok || !existsSync15(contained.absolutePath)) {
39221
39592
  return null;
39222
39593
  }
39223
39594
  try {
39224
- return readFileSync19(contained.absolutePath, "utf8");
39595
+ return readFileSync20(contained.absolutePath, "utf8");
39225
39596
  } catch {
39226
39597
  return null;
39227
39598
  }
@@ -39271,11 +39642,11 @@ class GodotServer {
39271
39642
  }
39272
39643
  sourceOfClass(projectPath, resourcePath) {
39273
39644
  const contained = resolveWithinProject(projectPath, resourcePath);
39274
- if (!contained.ok || !existsSync14(contained.absolutePath)) {
39645
+ if (!contained.ok || !existsSync15(contained.absolutePath)) {
39275
39646
  return null;
39276
39647
  }
39277
39648
  try {
39278
- return readFileSync19(contained.absolutePath, "utf8");
39649
+ return readFileSync20(contained.absolutePath, "utf8");
39279
39650
  } catch {
39280
39651
  return null;
39281
39652
  }
@@ -39391,7 +39762,7 @@ class GodotServer {
39391
39762
  getEditorStatusPayload() {
39392
39763
  const status = this.godotBridge.getStatus();
39393
39764
  const isPortConflict = this.bridgeStartupError?.includes("EADDRINUSE") ?? false;
39394
- const stale = status.connected && status.addonVersion !== SERVER_VERSION;
39765
+ const stale = status.connected && editorIsStale(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest());
39395
39766
  const unfinished = this.restartLeftUnfinished(status.connected);
39396
39767
  return {
39397
39768
  ...status,
@@ -39408,7 +39779,7 @@ class GodotServer {
39408
39779
  awaitingLaunchedEditor: status.connected || !this.launchedEditorIsUp() ? undefined : this.launchedEditor?.pid ?? undefined,
39409
39780
  ...status.connected ? this.whatTheLaunchedEditorDropped() : {},
39410
39781
  startupError: this.bridgeStartupError,
39411
- staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
39782
+ staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest()) : undefined,
39412
39783
  ...this.breakpointsAtRisk(),
39413
39784
  retryingBridge: this.bridgeRetry === null ? undefined : true,
39414
39785
  announcedAt: this.announcedAt ?? undefined,
@@ -39504,7 +39875,7 @@ class GodotServer {
39504
39875
  return null;
39505
39876
  }
39506
39877
  try {
39507
- return settingKeys(readFileSync19(join21(projectPath, "project.godot"), "utf8"));
39878
+ return settingKeys(readFileSync20(join22(projectPath, "project.godot"), "utf8"));
39508
39879
  } catch {
39509
39880
  return null;
39510
39881
  }
@@ -39540,8 +39911,8 @@ class GodotServer {
39540
39911
  editorPid: now.editorPid,
39541
39912
  addonVersion: now.addonVersion,
39542
39913
  serverVersion: SERVER_VERSION,
39543
- addonIsStale: now.addonVersion !== SERVER_VERSION,
39544
- staleNote: addonMismatch(now.addonVersion, SERVER_VERSION),
39914
+ addonIsStale: editorIsStale(now.addonVersion, SERVER_VERSION, now.addonDigest, shippedEditorDigest()),
39915
+ staleNote: addonMismatch(now.addonVersion, SERVER_VERSION, now.addonDigest, shippedEditorDigest()),
39545
39916
  ...this.whatTheEditorDropped(settingsBefore, after),
39546
39917
  tookMs: Date.now() - began
39547
39918
  });
@@ -39634,7 +40005,8 @@ class GodotServer {
39634
40005
  });
39635
40006
  }
39636
40007
  async openAnEditor(engine, projectPath, ports) {
39637
- const editor = spawn(engine, editorArguments(projectPath, ports), {
40008
+ clearEditorLog(projectPath);
40009
+ const editor = spawn(engine, editorArguments(projectPath, ports, editorLogPath(projectPath)), {
39638
40010
  stdio: "ignore",
39639
40011
  detached: true,
39640
40012
  env: {
@@ -39645,6 +40017,9 @@ class GodotServer {
39645
40017
  [OPENED_BY_A_SERVER]: "1"
39646
40018
  }
39647
40019
  });
40020
+ if (editor.pid !== undefined) {
40021
+ writeEditorLogNote(projectPath, editor.pid);
40022
+ }
39648
40023
  const started = await new Promise((resolve) => {
39649
40024
  editor.once("spawn", () => {
39650
40025
  resolve(null);
@@ -39751,7 +40126,7 @@ class GodotServer {
39751
40126
  }
39752
40127
  async runtimeUp(projectPath, before, budget, withArgs) {
39753
40128
  const budgetMs = budget.ms;
39754
- if (!existsSync14(join21(projectPath, RUNTIME_AUTOLOAD2.path))) {
40129
+ if (!existsSync15(join22(projectPath, RUNTIME_AUTOLOAD2.path))) {
39755
40130
  return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
39756
40131
  }
39757
40132
  let asking = null;
@@ -39814,7 +40189,7 @@ class GodotServer {
39814
40189
  }
39815
40190
  let announcedAt;
39816
40191
  try {
39817
- announcedAt = statSync5(endpoint.file).mtimeMs;
40192
+ announcedAt = statSync8(endpoint.file).mtimeMs;
39818
40193
  } catch {
39819
40194
  return;
39820
40195
  }
@@ -39850,7 +40225,7 @@ class GodotServer {
39850
40225
  }
39851
40226
  const startedAt = Date.now();
39852
40227
  const transcript = openTranscript(startedAt);
39853
- closeSync(transcript.fd);
40228
+ closeSync2(transcript.fd);
39854
40229
  const playAnswer = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
39855
40230
  const played = {
39856
40231
  process: null,
@@ -39917,7 +40292,7 @@ class GodotServer {
39917
40292
  }
39918
40293
  let handle;
39919
40294
  try {
39920
- handle = openSync2(game.errorReport, "r");
40295
+ handle = openSync3(game.errorReport, "r");
39921
40296
  } catch {
39922
40297
  return;
39923
40298
  }
@@ -39928,7 +40303,7 @@ class GodotServer {
39928
40303
  return;
39929
40304
  }
39930
40305
  const buffer = Buffer.alloc(size - from);
39931
- const read = readSync(handle, buffer, 0, buffer.length, from);
40306
+ const read = readSync2(handle, buffer, 0, buffer.length, from);
39932
40307
  game.errorReportOffset = from + read;
39933
40308
  const reported = buffer.subarray(0, read);
39934
40309
  if (game.transcript !== null) {
@@ -39937,7 +40312,7 @@ class GodotServer {
39937
40312
  game.log.append("stderr", reported);
39938
40313
  }
39939
40314
  } finally {
39940
- closeSync(handle);
40315
+ closeSync2(handle);
39941
40316
  }
39942
40317
  }
39943
40318
  recordWhatItBrokeOn(game) {
@@ -40031,7 +40406,7 @@ class GodotServer {
40031
40406
  ...env ? { env } : {}
40032
40407
  });
40033
40408
  } finally {
40034
- closeSync(transcript.fd);
40409
+ closeSync2(transcript.fd);
40035
40410
  }
40036
40411
  child.unref();
40037
40412
  const log = new GameLog;
@@ -40078,7 +40453,7 @@ class GodotServer {
40078
40453
  }
40079
40454
  let handle;
40080
40455
  try {
40081
- handle = openSync2(game.transcript, "r");
40456
+ handle = openSync3(game.transcript, "r");
40082
40457
  } catch {
40083
40458
  return;
40084
40459
  }
@@ -40088,11 +40463,11 @@ class GodotServer {
40088
40463
  return;
40089
40464
  }
40090
40465
  const buffer = Buffer.alloc(size - game.readOffset);
40091
- const read = readSync(handle, buffer, 0, buffer.length, game.readOffset);
40466
+ const read = readSync2(handle, buffer, 0, buffer.length, game.readOffset);
40092
40467
  game.readOffset += read;
40093
40468
  game.log.append("transcript", buffer.subarray(0, read));
40094
40469
  } finally {
40095
- closeSync(handle);
40470
+ closeSync2(handle);
40096
40471
  }
40097
40472
  }
40098
40473
  async pickUpWhatTheEditorIsPlaying() {
@@ -40290,7 +40665,7 @@ class GodotServer {
40290
40665
  }
40291
40666
  const unreadable = this.aRuntimeOfOursIsTooFarOff();
40292
40667
  if (unreadable !== null) {
40293
- const behind = unreadable.protocol > RUNTIME_PROTOCOL ? "this server is the older half: reconnect it so it spawns the installed version" : "the addon is the older half: reinstall it and restart the game";
40668
+ const behind = whichHalfIsBehind([unreadable]);
40294
40669
  return `A game is running for ${unreadable.project.path}, pid ${unreadable.pid}, announced in protocol` + ` ${unreadable.protocol}, which this server does not speak: it speaks ${RUNTIME_PROTOCOL}, so ${behind}.` + " Until then neither editor_run nor the runtime_* tools can reach it." + " A run started here is a separate process rather than a replacement, so starting one leaves" + " this game running as well.";
40295
40670
  }
40296
40671
  const record = readRunRecord();
@@ -40415,7 +40790,66 @@ class GodotServer {
40415
40790
  }
40416
40791
  return runIsUp(run, said);
40417
40792
  }
40793
+ handleEditorConsole(args) {
40794
+ const status = this.godotBridge.getStatus();
40795
+ const projectPath = status.projectPath ?? this.ownProject ?? undefined;
40796
+ if (projectPath === undefined) {
40797
+ return this.createErrorResponse("No editor is connected, so there is no console to read.", [
40798
+ "editor_status says whether one is connected and whether it may yet be",
40799
+ "editor_launch opens one, and an editor opened here has its console captured"
40800
+ ]);
40801
+ }
40802
+ const launched = status.connected ? null : this.launchedEditor?.pid ?? null;
40803
+ if (!status.connected && launched === null) {
40804
+ return this.createErrorResponse("No editor is connected, so there is no console to read.", [
40805
+ "editor_status says whether one is connected and whether it may yet be",
40806
+ "editor_launch opens one, and an editor opened here has its console captured"
40807
+ ]);
40808
+ }
40809
+ const editorPid = status.connected ? status.editorPid ?? null : launched;
40810
+ const console2 = editorConsole(projectPath, editorPid, status.connected ? status.openedByAServer === true : true);
40811
+ if ("kind" in console2) {
40812
+ return this.createErrorResponse(theConsoleWasNotCaptured(console2, projectPath), [
40813
+ "editor_launch restart replaces this editor with one whose console is captured",
40814
+ "editor_output with no op answers about the game a run is playing, which is a different log"
40815
+ ]);
40816
+ }
40817
+ const severity = readString(args, "severity");
40818
+ const selected = console2.log.select({
40819
+ severity: severity === "error" || severity === "warning" ? severity : "info",
40820
+ sinceLastCall: false,
40821
+ contains: readNonEmptyString(args, "contains"),
40822
+ limit: readPositiveNumber(args, "limit") ?? 200
40823
+ });
40824
+ const everything = console2.log.everything();
40825
+ const grouped = bursts(everything, readPositiveNumber(args, "before") ?? 3);
40826
+ const shown = grouped.slice(0, MOST_GROUPS);
40827
+ return this.jsonTextResponse({
40828
+ editorPid: editorPid ?? undefined,
40829
+ projectPath,
40830
+ ...status.connected ? {} : this.launchedEditorIsUp() ? { stillConnecting: true } : { exitedBeforeConnecting: true },
40831
+ capturedIn: console2.path,
40832
+ ...console2.of === undefined ? {} : {
40833
+ readFromTheStart: console2.readBytes,
40834
+ consoleBytes: console2.of,
40835
+ truncatedNote: `This editor's console is ${console2.of} bytes and the first ${console2.readBytes} of them were read, which is the start of the session rather than the end of it. Restart the editor to begin a fresh console.`
40836
+ },
40837
+ lines: everything.length,
40838
+ counts: {
40839
+ error: console2.log.count("error"),
40840
+ warning: console2.log.count("warning"),
40841
+ info: console2.log.count("info")
40842
+ },
40843
+ repeated: shown,
40844
+ moreShapes: grouped.length > shown.length ? grouped.length - shown.length : undefined,
40845
+ entries: forAnswer(selected.entries),
40846
+ omitted: selected.omitted === 0 ? undefined : selected.omitted
40847
+ });
40848
+ }
40418
40849
  async handleGetDebugOutput(args, waitedMs) {
40850
+ if (readString(args, "op") === "editor") {
40851
+ return this.handleEditorConsole(args);
40852
+ }
40419
40853
  await this.pickUpWhatTheEditorIsPlaying();
40420
40854
  const run = this.currentRun();
40421
40855
  if (!run) {
@@ -40692,6 +41126,7 @@ class GodotServer {
40692
41126
  }
40693
41127
  async rebuildClassCache(projectPath) {
40694
41128
  const before = cachedClasses(projectPath);
41129
+ const writtenBefore = cacheWrittenAt(projectPath);
40695
41130
  const noted = readClassNote(projectPath);
40696
41131
  const rebuilt = await this.operation("refresh_class_cache", {}, projectPath);
40697
41132
  if (!rebuilt.ok) {
@@ -40702,21 +41137,30 @@ class GodotServer {
40702
41137
  return rebuilt;
40703
41138
  }
40704
41139
  const lost = noted === null || before === null ? [] : noted.rebuiltWith.filter((name) => !before.has(name) && after.has(name)).sort();
40705
- const seenLosing = lost.length > 0 && this.godotBridge.isConnected();
41140
+ const cameBack = writtenBefore === null ? [] : declaredSince(projectPath, after, lost, writtenBefore);
41141
+ const droppedByTheEditor = lost.filter((name) => !cameBack.includes(name));
41142
+ const seenLosing = droppedByTheEditor.length > 0 && this.godotBridge.isConnected();
40706
41143
  writeClassNote(projectPath, {
40707
41144
  rebuiltWith: [...after.keys()].sort(),
40708
41145
  ...seenLosing ? { shortListEditorPid: this.godotBridge.getStatus().editorPid ?? null } : noted?.shortListEditorPid === undefined ? {} : { shortListEditorPid: noted.shortListEditorPid }
40709
41146
  });
40710
- if (!seenLosing) {
41147
+ const notes = [];
41148
+ if (seenLosing) {
41149
+ const them = droppedByTheEditor.length === 1 ? "it" : "them";
41150
+ notes.push(`The cache held ${droppedByTheEditor.join(", ")} after the last rebuild and not when this one began, so the editor holding this project wrote it without ${them} in between: it holds a list shorter than the files and writes that list on every save and scan, and each rebuild puts ${them} back. editor_launch restart is what ends it.`);
41151
+ }
41152
+ if (cameBack.length > 0) {
41153
+ notes.push(`${cameBack.join(", ")} left the cache while ${cameBack.length === 1 ? "its script was" : "their scripts were"} gone: the script${cameBack.length === 1 ? " is" : "s are"} newer than the cache the editor last wrote, so it listed what was on disk then. editor_rescan picks ${cameBack.length === 1 ? "it" : "them"} up.`);
41154
+ }
41155
+ if (notes.length === 0) {
40711
41156
  return rebuilt;
40712
41157
  }
40713
- const them = lost.length === 1 ? "it" : "them";
40714
41158
  return {
40715
41159
  ...rebuilt,
40716
41160
  payload: {
40717
41161
  ...rebuilt.payload,
40718
41162
  lostSinceLastRebuild: lost,
40719
- note: `The cache held ${lost.join(", ")} after the last rebuild and not when this one began, so the editor holding this project wrote it without ${them} in between: it holds a list shorter than the files and writes that list on every save and scan, and each rebuild puts ${them} back. editor_launch restart is what ends it.`
41163
+ note: notes.join(" ")
40720
41164
  }
40721
41165
  };
40722
41166
  }
@@ -40792,7 +41236,7 @@ class GodotServer {
40792
41236
  ]);
40793
41237
  }
40794
41238
  try {
40795
- return this.jsonTextResponse(markIfStale(await this.godotBridge.invokeTool(toolName, args), this.godotBridge.getStatus().addonVersion, SERVER_VERSION));
41239
+ return this.jsonTextResponse(markIfStale(await this.godotBridge.invokeTool(toolName, args), this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest()));
40796
41240
  } catch (error) {
40797
41241
  return this.createErrorResponse(`The editor answered ${toolName} with an error: ${errorMessage(error)}`);
40798
41242
  }
@@ -40866,8 +41310,8 @@ class GodotServer {
40866
41310
  ]);
40867
41311
  }
40868
41312
  const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
40869
- const screenshotDir = expectsScreenshot ? mkdtempSync3(join21(tmpdir5(), "gdharness-runtime-screenshot-")) : null;
40870
- const screenshotPath = screenshotDir ? join21(screenshotDir, "capture.png") : null;
41313
+ const screenshotDir = expectsScreenshot ? mkdtempSync3(join22(tmpdir5(), "gdharness-runtime-screenshot-")) : null;
41314
+ const screenshotPath = screenshotDir ? join22(screenshotDir, "capture.png") : null;
40871
41315
  try {
40872
41316
  const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs);
40873
41317
  if (!reply.ok) {
@@ -40891,7 +41335,7 @@ class GodotServer {
40891
41335
  type: "text",
40892
41336
  text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}`
40893
41337
  },
40894
- { type: "image", data: readFileSync19(screenshotPath).toString("base64"), mimeType: "image/png" }
41338
+ { type: "image", data: readFileSync20(screenshotPath).toString("base64"), mimeType: "image/png" }
40895
41339
  ]
40896
41340
  };
40897
41341
  } finally {
@@ -40958,7 +41402,7 @@ async function runGodotServer() {
40958
41402
  const server = new GodotServer;
40959
41403
  await server.run();
40960
41404
  }
40961
- var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run7, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CACHE_WRITE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/";
41405
+ var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run7, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CACHE_WRITE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/";
40962
41406
  var init_server3 = __esm(() => {
40963
41407
  init_mcp();
40964
41408
  init_stdio2();
@@ -40970,6 +41414,7 @@ var init_server3 = __esm(() => {
40970
41414
  init_class_note();
40971
41415
  init_config_pin();
40972
41416
  init_dap_client();
41417
+ init_editor_log();
40973
41418
  init_errors();
40974
41419
  init_game_log();
40975
41420
  init_godot_bridge();
@@ -40994,7 +41439,7 @@ var init_server3 = __esm(() => {
40994
41439
  init_tool_definitions();
40995
41440
  init_update_check();
40996
41441
  run7 = promisify7(execFile7);
40997
- __dirname2 = dirname14(fileURLToPath4(import.meta.url));
41442
+ __dirname2 = dirname15(fileURLToPath5(import.meta.url));
40998
41443
  CONSOLE_WRAPPER = /_console(\.exe)?$/i;
40999
41444
  LONGEST_BOOT_NOTED_MS = 5 * 60000;
41000
41445
  VALUE_NAMES = {
@@ -41034,8 +41479,8 @@ var init_server3 = __esm(() => {
41034
41479
  });
41035
41480
 
41036
41481
  // src/cli.ts
41037
- import { existsSync as existsSync15 } from "fs";
41038
- import { join as join22, resolve as resolve5 } from "path";
41482
+ import { existsSync as existsSync16 } from "fs";
41483
+ import { join as join23, resolve as resolve5 } from "path";
41039
41484
 
41040
41485
  // src/errors.ts
41041
41486
  class Refusal extends Error {
@@ -42213,7 +42658,17 @@ init_headless();
42213
42658
  init_resources();
42214
42659
  init_server_version();
42215
42660
  import { spawnSync } from "node:child_process";
42216
- import { cpSync, existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync7, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "node:fs";
42661
+ import { createHash as createHash2 } from "node:crypto";
42662
+ import {
42663
+ cpSync,
42664
+ existsSync as existsSync7,
42665
+ mkdirSync as mkdirSync3,
42666
+ readdirSync as readdirSync3,
42667
+ readFileSync as readFileSync7,
42668
+ rmSync as rmSync3,
42669
+ statSync as statSync3,
42670
+ writeFileSync as writeFileSync5
42671
+ } from "node:fs";
42217
42672
  import { dirname as dirname3, join as join8 } from "node:path";
42218
42673
  import { fileURLToPath } from "node:url";
42219
42674
  var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
@@ -42223,6 +42678,40 @@ var RUNTIME_AUTOLOAD = {
42223
42678
  path: "addons/gdharness_runtime/runtime_autoload.gd"
42224
42679
  };
42225
42680
  var VERSION_MARKER = ".gdharness-version";
42681
+ var DIGEST_MARKER = ".gdharness-digest";
42682
+ function editorAddonDigest(from = shippedAddonsDirectory()) {
42683
+ const hash = createHash2("sha256");
42684
+ const walk = (directory, relative) => {
42685
+ for (const name of readdirSync3(directory).sort()) {
42686
+ if (name === VERSION_MARKER || name === DIGEST_MARKER) {
42687
+ continue;
42688
+ }
42689
+ const path = join8(directory, name);
42690
+ const inside = `${relative}/${name}`;
42691
+ if (statSync3(path).isDirectory()) {
42692
+ walk(path, inside);
42693
+ } else {
42694
+ hash.update(`${inside}
42695
+ `);
42696
+ hash.update(readFileSync7(path));
42697
+ hash.update(`
42698
+ `);
42699
+ }
42700
+ }
42701
+ };
42702
+ for (const name of EDITOR_PLUGINS) {
42703
+ walk(join8(from, name), name);
42704
+ }
42705
+ return hash.digest("hex");
42706
+ }
42707
+ function installedEditorDigest(projectPath) {
42708
+ const marker = join8(projectPath, "addons", ADDONS[0], DIGEST_MARKER);
42709
+ if (!existsSync7(marker)) {
42710
+ return null;
42711
+ }
42712
+ const said = readFileSync7(marker, "utf8").trim();
42713
+ return said === "" ? null : said;
42714
+ }
42226
42715
  function shippedAddonsDirectory() {
42227
42716
  return join8(dirname3(fileURLToPath(import.meta.url)), "godot", "addons");
42228
42717
  }
@@ -42245,6 +42734,8 @@ function installAddons(projectPath, from = shippedAddonsDirectory()) {
42245
42734
  `);
42246
42735
  installed.push({ name, path: target, replaced });
42247
42736
  }
42737
+ writeFileSync5(join8(projectPath, "addons", ADDONS[0], DIGEST_MARKER), `${editorAddonDigest(from)}
42738
+ `);
42248
42739
  return installed;
42249
42740
  }
42250
42741
  function removeAddons(projectPath) {
@@ -42389,7 +42880,7 @@ function inspectProject(projectPath) {
42389
42880
  // src/skill.ts
42390
42881
  init_setup();
42391
42882
  init_tool_definitions();
42392
- import { existsSync as existsSync9, mkdirSync as mkdirSync5, readdirSync as readdirSync3, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "node:fs";
42883
+ import { existsSync as existsSync9, mkdirSync as mkdirSync5, readdirSync as readdirSync5, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "node:fs";
42393
42884
  import { basename, dirname as dirname5, join as join10 } from "node:path";
42394
42885
 
42395
42886
  // src/tool-reference.ts
@@ -42511,7 +43002,8 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
42511
43002
  this server is connected to. There is no ambient project, and an argument a tool does not declare is refused
42512
43003
  rather than ignored.
42513
43004
  - \`editor_status\` says whether an editor is connected and whether its addon matches the server.
42514
- \`addonIsStale\` means they differ, and \`staleNote\` says which half is behind: an editor that
43005
+ \`addonIsStale\` means the editor is running different addon code from what the server ships
43006
+ (an older \`addonVersion\` on the same code is not stale), and \`staleNote\` says which half is behind: an editor that
42515
43007
  needs restarting, or a server that needs reconnecting in your harness. Every answer that came
42516
43008
  from the editor carries the same two when they differ, so a stale addon cannot answer quietly:
42517
43009
  believe a restart over a diagnostic that arrived with them.
@@ -42532,6 +43024,14 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
42532
43024
  runtime addon announces nothing, so there is no process id and \`running\` is the editor's answer,
42533
43025
  which can lag by seconds after the game has gone. \`editor_status\` names the game under
42534
43026
  \`runtimes\`, which does not depend on the editor answering.
43027
+ - When the tools all say the project is fine and something is still wrong, read the editor's own
43028
+ console with \`editor_output op: "editor"\`. It is a different log from the one above and the only
43029
+ way anything here sees what the editor printed: its console reaches no plugin, so an editor
43030
+ gdharness opened is asked for a log file as it starts, and one somebody opened by hand is refused
43031
+ rather than answered empty. It carries the whole session from the editor's first line, which is
43032
+ where a wall of startup errors will be, with repeated messages grouped under \`repeated\` and the
43033
+ ordinary lines above each group beside them: a plugin announcing itself on the line before a
43034
+ burst of parse errors is usually the whole diagnosis.
42535
43035
  - One server, one editor. A second project is a second harness session with its own server, and
42536
43036
  that works: each editor is opened on its own language server and debug adapter ports, and says
42537
43037
  where it serves. Two editors on one project is the thing to refuse.
@@ -42690,7 +43190,7 @@ function pruneEmpty(directory, projectPath) {
42690
43190
  const shared = join10(projectPath, ".agents");
42691
43191
  let path = directory;
42692
43192
  while (path !== projectPath && (basename(path) === "skills" || path === shared)) {
42693
- if (readdirSync3(path).length > 0) {
43193
+ if (readdirSync5(path).length > 0) {
42694
43194
  return;
42695
43195
  }
42696
43196
  rmSync5(path, { recursive: true, force: true });
@@ -42714,9 +43214,9 @@ function writeSkill(directories, version) {
42714
43214
  // src/version.ts
42715
43215
  import { readFileSync as readFileSync9 } from "node:fs";
42716
43216
  import { dirname as dirname6, join as join11 } from "node:path";
42717
- import { fileURLToPath as fileURLToPath2 } from "node:url";
43217
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
42718
43218
  function getLocalVersion() {
42719
- const currentDirectory = dirname6(fileURLToPath2(import.meta.url));
43219
+ const currentDirectory = dirname6(fileURLToPath3(import.meta.url));
42720
43220
  const packageCandidates = [
42721
43221
  join11(currentDirectory, "..", "package.json"),
42722
43222
  join11(currentDirectory, "..", "..", "package.json")
@@ -42741,7 +43241,7 @@ class UsageError extends Refusal {
42741
43241
  function projectArgument(at) {
42742
43242
  const named = args.slice(at).find((value) => !value.startsWith("--"));
42743
43243
  const projectPath = resolve5(named ?? ".");
42744
- if (!existsSync15(join22(projectPath, "project.godot"))) {
43244
+ if (!existsSync16(join23(projectPath, "project.godot"))) {
42745
43245
  throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
42746
43246
  }
42747
43247
  return projectPath;
@@ -42780,7 +43280,7 @@ async function registerRuntime(godot, projectPath) {
42780
43280
  return;
42781
43281
  }
42782
43282
  console.log(`${RUNTIME_AUTOLOAD.name} autoload left at ${named}, which this project registers itself`);
42783
- if (!existsSync15(join22(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
43283
+ if (!existsSync16(join23(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
42784
43284
  console.log(` that file is not in this project, so it boots with a missing script: restore it, or point the entry at res://${RUNTIME_AUTOLOAD.path}`);
42785
43285
  }
42786
43286
  }
@@ -42912,7 +43412,7 @@ async function setup() {
42912
43412
  console.log("no harness configured; name one yourself, and gdharness harnesses lists them all");
42913
43413
  }
42914
43414
  if (!args.includes("--no-skill")) {
42915
- const directories = skillDirectories(chosen.harnesses, projectPath, existsSync15);
43415
+ const directories = skillDirectories(chosen.harnesses, projectPath, existsSync16);
42916
43416
  for (const written of writeSkill(directories, getLocalVersion())) {
42917
43417
  console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
42918
43418
  }
@@ -42978,6 +43478,7 @@ async function upgrade() {
42978
43478
  }
42979
43479
  const godot = await engine(projectPath);
42980
43480
  console.log(installed === version ? `already ${version}; reinstalling` : `${installed} -> ${version}`);
43481
+ const digestBefore = installedEditorDigest(projectPath);
42981
43482
  for (const addon of installAddons(projectPath)) {
42982
43483
  console.log(`${addon.replaced ? "replaced" : "installed"} ${addon.path}`);
42983
43484
  }
@@ -43005,12 +43506,14 @@ async function upgrade() {
43005
43506
  moved.push(took.from);
43006
43507
  }
43007
43508
  }
43008
- for (const written of writeSkill(skillDirectories(already, projectPath, existsSync15), version)) {
43509
+ for (const written of writeSkill(skillDirectories(already, projectPath, existsSync16), version)) {
43009
43510
  console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
43010
43511
  }
43011
43512
  console.log(`
43012
- On ${version}. Two things this could not do for you:`);
43013
- console.log(` 1. The open editor is still running the addons it loaded at startup. Restart it with the
43513
+ On ${version}. What is left for you:`);
43514
+ console.log(digestBefore !== null && digestBefore === installedEditorDigest(projectPath) ? ` 1. Nothing for the editor: this upgrade left the editor addons the same code as before,
43515
+ ` + ` so it gives an open editor nothing to restart for. editor_status says addonIsStale
43516
+ ` + " if the editor is older than that." : ` 1. The open editor is still running the addons it loaded at startup. Restart it with the
43014
43517
  ` + " editor_launch restart tool, which closes and reopens the window.");
43015
43518
  console.log(harnessNote({ moved, written, byHand }, [launch.command, ...launch.args].join(" ")));
43016
43519
  }