gdharness 1.0.11 → 1.0.12

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 +468 -108
  2. package/build/index.js +477 -125
  3. package/package.json +1 -1
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];
@@ -15349,7 +15362,7 @@ function projectPathSentence(without = callsWithoutProjectPath()) {
15349
15362
  return `Every call takes \`projectPath\` except ${named}, which ${without.length === 1 ? "takes" : "take"} none`;
15350
15363
  }
15351
15364
  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";
15365
+ 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
15366
  }
15354
15367
  function toolSpec(name) {
15355
15368
  return SPECS_BY_NAME[name];
@@ -16070,7 +16083,7 @@ var init_tool_definitions = __esm(() => {
16070
16083
  },
16071
16084
  {
16072
16085
  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.",
16086
+ 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
16087
  parameters: {
16075
16088
  severity: {
16076
16089
  type: "string",
@@ -16078,10 +16091,12 @@ var init_tool_definitions = __esm(() => {
16078
16091
  description: "The least severe entry to include. Default info, which is everything."
16079
16092
  },
16080
16093
  sinceLastCall: {
16094
+ ops: ["run"],
16081
16095
  type: "boolean",
16082
16096
  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
16097
  },
16084
16098
  cpu: {
16099
+ ops: ["run"],
16085
16100
  type: "boolean",
16086
16101
  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
16102
  },
@@ -16092,8 +16107,24 @@ var init_tool_definitions = __esm(() => {
16092
16107
  limit: {
16093
16108
  type: "number",
16094
16109
  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."
16110
+ },
16111
+ before: {
16112
+ ops: ["editor"],
16113
+ type: "number",
16114
+ 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."
16115
+ }
16116
+ },
16117
+ operations: {
16118
+ run: {
16119
+ summary: "What the game a run is playing has printed, which is everything described above",
16120
+ requires: []
16121
+ },
16122
+ editor: {
16123
+ 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",
16124
+ requires: []
16095
16125
  }
16096
16126
  },
16127
+ defaultOperation: "run",
16097
16128
  requires: []
16098
16129
  },
16099
16130
  {
@@ -31693,10 +31724,37 @@ async function announcedSince(projectPath, before, waiting = {}) {
31693
31724
  function describe2(endpoint) {
31694
31725
  return `pid ${endpoint.pid} on ${endpoint.address}:${endpoint.port} (${endpoint.project.name || "unnamed"} at ${endpoint.project.path})`;
31695
31726
  }
31727
+ function describeWithin(endpoint) {
31728
+ return `pid ${endpoint.pid} on ${endpoint.address}:${endpoint.port}`;
31729
+ }
31730
+ function theOneProject(endpoints) {
31731
+ if (endpoints.some((endpoint) => endpoint.project.path === "")) {
31732
+ return;
31733
+ }
31734
+ const paths = new Set(endpoints.map((endpoint) => resolve3(endpoint.project.path)));
31735
+ const only = [...paths][0];
31736
+ return paths.size === 1 ? only : undefined;
31737
+ }
31738
+ function severalFrom(project, games) {
31739
+ 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.";
31740
+ }
31741
+ function whichHalfIsBehind(unspoken) {
31742
+ const pids = (games) => games.map((game) => `pid ${game.pid}`).join(", ");
31743
+ const ahead = unspoken.filter((game) => game.protocol > RUNTIME_PROTOCOL);
31744
+ const behind = unspoken.filter((game) => game.protocol < RUNTIME_PROTOCOL);
31745
+ if (ahead.length > 0 && behind.length > 0) {
31746
+ return `they are on both sides of it: reconnect this server for ${pids(ahead)}, and reinstall the ` + `addon and restart ${pids(behind)}`;
31747
+ }
31748
+ if (ahead.length > 0) {
31749
+ return "this server is the older half: reconnect it so it spawns the installed version";
31750
+ }
31751
+ return `the addon is the older half: reinstall it and restart ${behind.length === 1 ? "the game" : "those games"}`;
31752
+ }
31696
31753
  function tooNew(unspoken) {
31697
31754
  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}.`;
31755
+ const count = unspoken.length === 1 ? "A game is running" : `${unspoken.length} games are running`;
31756
+ const spoken = new Set(unspoken.map((game) => game.protocol)).size === 1 ? "a protocol" : "protocols";
31757
+ return `${count}, in ${spoken} this server does not speak: ${named}. This server speaks ${RUNTIME_PROTOCOL}, so ${whichHalfIsBehind(unspoken)}.`;
31700
31758
  }
31701
31759
  function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
31702
31760
  if (pid !== undefined) {
@@ -31713,8 +31771,16 @@ function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
31713
31771
  if (tooNewToo !== undefined) {
31714
31772
  return { problem: tooNew([tooNewToo]) };
31715
31773
  }
31774
+ if (endpoints.length > 0) {
31775
+ return {
31776
+ problem: `No running game has pid ${pid}. Running: ${endpoints.map(describe2).join("; ")}. editor_status lists them under runtimes as they come and go.`
31777
+ };
31778
+ }
31779
+ if (unspoken.length > 0) {
31780
+ return { problem: `No game reachable from here has pid ${pid}. ${tooNew(unspoken)}` };
31781
+ }
31716
31782
  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.`
31783
+ 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
31784
  };
31719
31785
  }
31720
31786
  if (endpoints.length === 0) {
@@ -31746,15 +31812,17 @@ function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
31746
31812
  if (matching.length === 1 && matching[0]) {
31747
31813
  return { endpoint: matching[0] };
31748
31814
  }
31749
- return {
31750
- problem: `Several games are running from ${wanted}: ${matching.map(describe2).join("; ")}. Pass pid to choose one.`
31751
- };
31815
+ return { problem: severalFrom(wanted, matching) };
31752
31816
  }
31753
31817
  if (endpoints.length === 1 && endpoints[0]) {
31754
31818
  return { endpoint: endpoints[0] };
31755
31819
  }
31820
+ const shared = theOneProject(endpoints);
31821
+ if (shared !== undefined) {
31822
+ return { problem: severalFrom(shared, endpoints) };
31823
+ }
31756
31824
  return {
31757
- problem: `Several games are running: ${endpoints.map(describe2).join("; ")}. Pass projectPath to choose one, or pid when they are from one project.`
31825
+ 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
31826
  };
31759
31827
  }
31760
31828
  function runtimeRequest(endpoint, command, params, timeoutMs) {
@@ -32802,6 +32870,235 @@ var init_dap_client = __esm(() => {
32802
32870
  init_ports();
32803
32871
  });
32804
32872
 
32873
+ // src/editor-log.ts
32874
+ import {
32875
+ closeSync,
32876
+ existsSync as existsSync12,
32877
+ mkdirSync as mkdirSync10,
32878
+ openSync,
32879
+ readFileSync as readFileSync16,
32880
+ readSync,
32881
+ rmSync as rmSync8,
32882
+ statSync as statSync4,
32883
+ writeFileSync as writeFileSync12
32884
+ } from "node:fs";
32885
+ import { dirname as dirname12, join as join18 } from "node:path";
32886
+ function editorLogPath(projectPath) {
32887
+ return join18(projectPath, ".godot", "gdharness-editor.log");
32888
+ }
32889
+ function editorLogNotePath(projectPath) {
32890
+ return join18(projectPath, ".godot", "gdharness-editor.json");
32891
+ }
32892
+ function readEditorLogNote(projectPath) {
32893
+ try {
32894
+ const fields = asParams(JSON.parse(readFileSync16(editorLogNotePath(projectPath), "utf8")));
32895
+ const pid = readNumber(fields, "pid");
32896
+ const startedAt = readString(fields, "startedAt");
32897
+ return pid !== undefined && Number.isInteger(pid) && pid > 0 && startedAt !== undefined ? { pid, startedAt } : null;
32898
+ } catch {
32899
+ return null;
32900
+ }
32901
+ }
32902
+ function clearEditorLog(projectPath) {
32903
+ try {
32904
+ const path = editorLogPath(projectPath);
32905
+ mkdirSync10(dirname12(path), { recursive: true });
32906
+ rmSync8(path, { force: true });
32907
+ rmSync8(editorLogNotePath(projectPath), { force: true });
32908
+ } catch {}
32909
+ }
32910
+ function writeEditorLogNote(projectPath, pid) {
32911
+ try {
32912
+ const path = editorLogNotePath(projectPath);
32913
+ mkdirSync10(dirname12(path), { recursive: true });
32914
+ writeFileSync12(path, JSON.stringify({ pid, startedAt: new Date().toISOString() }, null, 2), "utf8");
32915
+ } catch {}
32916
+ }
32917
+ function theConsoleWasNotCaptured(why, projectPath) {
32918
+ if (why.kind === "not ours") {
32919
+ 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.";
32920
+ }
32921
+ if (why.kind === "older server") {
32922
+ 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.";
32923
+ }
32924
+ 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.";
32925
+ }
32926
+ function editorConsole(projectPath, editorPid, openedByAServer) {
32927
+ if (!openedByAServer) {
32928
+ return { kind: "not ours" };
32929
+ }
32930
+ const noted = readEditorLogNote(projectPath);
32931
+ if (noted === null || editorPid === null || noted.pid !== editorPid) {
32932
+ return { kind: "older server", notedPid: noted?.pid ?? null };
32933
+ }
32934
+ const path = editorLogPath(projectPath);
32935
+ if (!existsSync12(path)) {
32936
+ return { kind: "not written" };
32937
+ }
32938
+ const log = new GameLog;
32939
+ const { text, of } = theFirstOf(path);
32940
+ log.append("transcript", text);
32941
+ log.finish();
32942
+ return { log, path, ...of === undefined ? {} : { readBytes: text.length, of } };
32943
+ }
32944
+ function theFirstOf(path) {
32945
+ const whole = statSync4(path).size;
32946
+ if (whole <= MOST_CONSOLE_BYTES) {
32947
+ return { text: readFileSync16(path, "utf8") };
32948
+ }
32949
+ const handle = openSync(path, "r");
32950
+ try {
32951
+ const buffer = Buffer.alloc(MOST_CONSOLE_BYTES);
32952
+ const read = readSync(handle, buffer, 0, MOST_CONSOLE_BYTES, 0);
32953
+ const decoded = buffer.subarray(0, read).toString("utf8");
32954
+ const lastLine = decoded.lastIndexOf(`
32955
+ `);
32956
+ return { text: lastLine === -1 ? decoded : decoded.slice(0, lastLine + 1), of: whole };
32957
+ } finally {
32958
+ closeSync(handle);
32959
+ }
32960
+ }
32961
+ function shapeOf(text) {
32962
+ const values = [];
32963
+ let shape = "";
32964
+ let at = 0;
32965
+ while (at < text.length) {
32966
+ let earliest = null;
32967
+ for (const pattern of SLOTS) {
32968
+ pattern.lastIndex = at;
32969
+ const found = pattern.exec(text);
32970
+ if (found !== null && (earliest === null || found.index < earliest.index || found.index === earliest.index && found[0].length > earliest.value.length)) {
32971
+ earliest = { index: found.index, value: found[0] };
32972
+ }
32973
+ }
32974
+ if (earliest === null) {
32975
+ shape += text.slice(at);
32976
+ break;
32977
+ }
32978
+ if (earliest.value.length === 0) {
32979
+ shape += text.slice(at, at + 1);
32980
+ at += 1;
32981
+ continue;
32982
+ }
32983
+ shape += `${text.slice(at, earliest.index)}…`;
32984
+ values.push(earliest.value);
32985
+ at = earliest.index + earliest.value.length;
32986
+ }
32987
+ return { shape, values };
32988
+ }
32989
+ function bursts(entries, before) {
32990
+ const shapes = new Map;
32991
+ const shaped = entries.map((entry) => shapeOf(entry.text));
32992
+ for (const [at, entry] of entries.entries()) {
32993
+ const key = `${entry.severity}\x00${shaped[at]?.shape ?? entry.text}`;
32994
+ const group = shapes.get(key) ?? { indices: [], values: [] };
32995
+ group.indices.push(at);
32996
+ group.values.push(shaped[at]?.values ?? []);
32997
+ shapes.set(key, group);
32998
+ }
32999
+ const collapsed = new Set;
33000
+ for (const group of shapes.values()) {
33001
+ if (group.indices.length > 1) {
33002
+ for (const at of group.indices) {
33003
+ collapsed.add(at);
33004
+ }
33005
+ }
33006
+ }
33007
+ const repeated = [];
33008
+ for (const [key, group] of shapes) {
33009
+ const first = group.indices[0];
33010
+ if (group.indices.length < 2 || first === undefined) {
33011
+ continue;
33012
+ }
33013
+ const entry = entries[first];
33014
+ if (entry === undefined) {
33015
+ continue;
33016
+ }
33017
+ repeated.push({
33018
+ shape: key.slice(key.indexOf("\x00") + 1),
33019
+ count: group.indices.length,
33020
+ severity: entry.severity,
33021
+ first: { index: entry.index, text: entry.text },
33022
+ slots: slotsOf(group.values, group.indices.length),
33023
+ before: linesBefore(entries, first, collapsed, before)
33024
+ });
33025
+ }
33026
+ return repeated.sort((one, other) => other.count - one.count);
33027
+ }
33028
+ function slotsOf(values, count) {
33029
+ const width = Math.max(0, ...values.map((row) => row.length));
33030
+ const distinct = [];
33031
+ for (let at = 0;at < width; at += 1) {
33032
+ distinct.push([...new Set(values.map((row) => row[at]).filter((one) => one !== undefined))]);
33033
+ }
33034
+ const slots = [];
33035
+ for (let at = 0;at < width; at += 1) {
33036
+ const seen = distinct[at] ?? [];
33037
+ const partner = theSlotItMovesWith(values, distinct, at);
33038
+ const paired = partner === undefined ? {} : { movesWith: partner };
33039
+ if (seen.length === count && count > 1) {
33040
+ slots.push({ distinct: seen.length, allDifferent: true, ...paired });
33041
+ continue;
33042
+ }
33043
+ const some = seen.slice(0, NAMED_VALUES);
33044
+ slots.push({
33045
+ distinct: seen.length,
33046
+ some,
33047
+ ...seen.length > some.length ? { more: seen.length - some.length } : {},
33048
+ ...paired
33049
+ });
33050
+ }
33051
+ return slots;
33052
+ }
33053
+ function theSlotItMovesWith(values, distinct, at) {
33054
+ const mine = distinct[at] ?? [];
33055
+ if (mine.length < 2) {
33056
+ return;
33057
+ }
33058
+ for (let other = 0;other < at; other += 1) {
33059
+ if ((distinct[other] ?? []).length !== mine.length) {
33060
+ continue;
33061
+ }
33062
+ const goesWith = new Map;
33063
+ let oneForOne = true;
33064
+ for (const row of values) {
33065
+ const theirs = row[other];
33066
+ const ours = row[at];
33067
+ if (theirs === undefined || ours === undefined) {
33068
+ oneForOne = false;
33069
+ break;
33070
+ }
33071
+ const already = goesWith.get(theirs);
33072
+ if (already === undefined) {
33073
+ goesWith.set(theirs, ours);
33074
+ } else if (already !== ours) {
33075
+ oneForOne = false;
33076
+ break;
33077
+ }
33078
+ }
33079
+ if (oneForOne) {
33080
+ return other;
33081
+ }
33082
+ }
33083
+ return;
33084
+ }
33085
+ function linesBefore(entries, at, collapsed, wanted) {
33086
+ const found = [];
33087
+ for (let back = at - 1;back >= 0 && at - back <= WALK_BACK_LIMIT && found.length < wanted; back -= 1) {
33088
+ const entry = entries[back];
33089
+ if (entry !== undefined && !collapsed.has(back)) {
33090
+ found.push(entry.text);
33091
+ }
33092
+ }
33093
+ return found.reverse();
33094
+ }
33095
+ var MOST_CONSOLE_BYTES, NAMED_VALUES = 6, WALK_BACK_LIMIT = 500, SLOTS;
33096
+ var init_editor_log = __esm(() => {
33097
+ init_game_log();
33098
+ MOST_CONSOLE_BYTES = 8 * 1024 * 1024;
33099
+ SLOTS = [/"[^"]*"/g, /'[^']*'/g, /\b(?:res|user):\/\/\S+/g, /(?<=\.gd|\.tscn|\.cs):\d+(?::\d+)?/g];
33100
+ });
33101
+
32805
33102
  // node_modules/ws/lib/constants.js
32806
33103
  var require_constants = __commonJS(function(exports, module) {
32807
33104
  var BINARY_TYPES = ["nodebuffer", "arraybuffer", "fragments"];
@@ -36182,7 +36479,7 @@ var init_godot_bridge = __esm(() => {
36182
36479
 
36183
36480
  // src/godot-path.ts
36184
36481
  import { execFile as execFile5 } from "node:child_process";
36185
- import { existsSync as existsSync12 } from "node:fs";
36482
+ import { existsSync as existsSync13 } from "node:fs";
36186
36483
  import { normalize as normalize2 } from "node:path";
36187
36484
  import { promisify as promisify5 } from "node:util";
36188
36485
 
@@ -36237,7 +36534,7 @@ class GodotLocator2 {
36237
36534
  return known;
36238
36535
  }
36239
36536
  let reason;
36240
- if (path !== "godot" && !existsSync12(path)) {
36537
+ if (path !== "godot" && !existsSync13(path)) {
36241
36538
  reason = "does not exist";
36242
36539
  } else {
36243
36540
  try {
@@ -36613,7 +36910,7 @@ var init_junit = __esm(() => {
36613
36910
  import { realpathSync as realpathSync2 } from "node:fs";
36614
36911
  import { readFile, realpath } from "node:fs/promises";
36615
36912
  import { createConnection as createConnection3 } from "node:net";
36616
- import { dirname as dirname12, resolve as resolve4 } from "node:path";
36913
+ import { dirname as dirname13, resolve as resolve4 } from "node:path";
36617
36914
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
36618
36915
  function diagnosticsKey(uri) {
36619
36916
  let decoded;
@@ -36741,7 +37038,7 @@ class GodotLSPClient {
36741
37038
  if (this.initialized) {
36742
37039
  return;
36743
37040
  }
36744
- const rootPath = this.rootPath ?? dirname12(resolve4(filePath));
37041
+ const rootPath = this.rootPath ?? dirname13(resolve4(filePath));
36745
37042
  await this.initialize(rootPath);
36746
37043
  }
36747
37044
  async sendRequest(method, params) {
@@ -37186,13 +37483,13 @@ var init_process_time = __esm(() => {
37186
37483
  });
37187
37484
 
37188
37485
  // 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";
37486
+ import { existsSync as existsSync14, readdirSync as readdirSync5, readFileSync as readFileSync17 } from "node:fs";
37487
+ import { join as join19 } from "node:path";
37191
37488
  function skipped(name) {
37192
37489
  return name.startsWith(".") || SKIPPED.has(name);
37193
37490
  }
37194
37491
  function steppedOver(directory) {
37195
- return existsSync13(join18(directory, ".gdignore"));
37492
+ return existsSync14(join19(directory, ".gdignore"));
37196
37493
  }
37197
37494
  function projectStructure(projectPath) {
37198
37495
  const structure = { scenes: 0, scripts: 0, assets: 0, other: 0 };
@@ -37205,7 +37502,7 @@ function projectStructure(projectPath) {
37205
37502
  continue;
37206
37503
  }
37207
37504
  if (entry.isDirectory()) {
37208
- visit(join18(directory, entry.name));
37505
+ visit(join19(directory, entry.name));
37209
37506
  } else if (entry.isFile()) {
37210
37507
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
37211
37508
  if (extension === "tscn") {
@@ -37235,14 +37532,14 @@ function scriptsWithoutUid(projectPath) {
37235
37532
  }
37236
37533
  const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
37237
37534
  if (entry.isDirectory()) {
37238
- visit(join18(directory, entry.name), spelled);
37535
+ visit(join19(directory, entry.name), spelled);
37239
37536
  continue;
37240
37537
  }
37241
37538
  if (!entry.isFile()) {
37242
37539
  continue;
37243
37540
  }
37244
37541
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
37245
- if (CARRIES_A_UID.has(extension) && !existsSync13(join18(directory, `${entry.name}.uid`))) {
37542
+ if (CARRIES_A_UID.has(extension) && !existsSync14(join19(directory, `${entry.name}.uid`))) {
37246
37543
  found.push(spelled);
37247
37544
  }
37248
37545
  }
@@ -37277,7 +37574,7 @@ function searchProject(projectPath, options) {
37277
37574
  if (skipped(entry.name)) {
37278
37575
  continue;
37279
37576
  }
37280
- const entryPath = join18(directory, entry.name);
37577
+ const entryPath = join19(directory, entry.name);
37281
37578
  if (entry.isDirectory()) {
37282
37579
  visit(entryPath);
37283
37580
  continue;
@@ -37288,7 +37585,7 @@ function searchProject(projectPath, options) {
37288
37585
  }
37289
37586
  result.summary.files_searched += 1;
37290
37587
  const matches = [];
37291
- for (const [index, line] of readFileSync16(entryPath, "utf8").split(`
37588
+ for (const [index, line] of readFileSync17(entryPath, "utf8").split(`
37292
37589
  `).entries()) {
37293
37590
  if (full()) {
37294
37591
  break;
@@ -37317,16 +37614,16 @@ var init_project_scan = __esm(() => {
37317
37614
  });
37318
37615
 
37319
37616
  // 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";
37617
+ import { mkdirSync as mkdirSync11, readFileSync as readFileSync18, rmSync as rmSync9, writeFileSync as writeFileSync13 } from "node:fs";
37618
+ import { dirname as dirname14, join as join20 } from "node:path";
37322
37619
  function restartNotePath(projectPath) {
37323
- return join19(projectPath, ".godot", "gdharness-restart.json");
37620
+ return join20(projectPath, ".godot", "gdharness-restart.json");
37324
37621
  }
37325
37622
  function noteRestartBegun(note) {
37326
37623
  const path = restartNotePath(note.projectPath);
37327
37624
  try {
37328
- mkdirSync10(dirname13(path), { recursive: true });
37329
- writeFileSync12(path, `${JSON.stringify(note, null, 2)}
37625
+ mkdirSync11(dirname14(path), { recursive: true });
37626
+ writeFileSync13(path, `${JSON.stringify(note, null, 2)}
37330
37627
  `, "utf8");
37331
37628
  } catch (error) {
37332
37629
  console.error(`[SERVER] Could not record the restart at ${path}: ${errorMessage(error)}`);
@@ -37334,7 +37631,7 @@ function noteRestartBegun(note) {
37334
37631
  }
37335
37632
  function restartOwed(projectPath) {
37336
37633
  try {
37337
- const parsed = JSON.parse(readFileSync17(restartNotePath(projectPath), "utf8"));
37634
+ const parsed = JSON.parse(readFileSync18(restartNotePath(projectPath), "utf8"));
37338
37635
  if (typeof parsed !== "object" || parsed === null) {
37339
37636
  return null;
37340
37637
  }
@@ -37349,7 +37646,7 @@ function restartOwed(projectPath) {
37349
37646
  }
37350
37647
  function restartSettled(projectPath) {
37351
37648
  try {
37352
- rmSync8(restartNotePath(projectPath), { force: true });
37649
+ rmSync9(restartNotePath(projectPath), { force: true });
37353
37650
  } catch (error) {
37354
37651
  console.error(`[SERVER] Could not clear the restart note: ${errorMessage(error)}`);
37355
37652
  }
@@ -37360,36 +37657,36 @@ var init_restart_note = __esm(() => {
37360
37657
 
37361
37658
  // src/run-record.ts
37362
37659
  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";
37660
+ import { mkdirSync as mkdirSync12, openSync as openSync2, readdirSync as readdirSync6, readFileSync as readFileSync19, rmSync as rmSync10, statSync as statSync5, writeFileSync as writeFileSync14 } from "node:fs";
37661
+ import { basename as basename3, join as join21 } from "node:path";
37365
37662
  function runsDirectory() {
37366
- return join20(runtimeDirectory(), "runs");
37663
+ return join21(runtimeDirectory(), "runs");
37367
37664
  }
37368
37665
  function recordPath() {
37369
- return join20(runsDirectory(), "run.json");
37666
+ return join21(runsDirectory(), "run.json");
37370
37667
  }
37371
37668
  function recordPaths() {
37372
- return runtimeDirectories().map((directory) => join20(directory, "runs", "run.json"));
37669
+ return runtimeDirectories().map((directory) => join21(directory, "runs", "run.json"));
37373
37670
  }
37374
37671
  function openTranscript(startedAt) {
37375
- mkdirSync11(runsDirectory(), { recursive: true });
37376
- const path = join20(runsDirectory(), `run-${startedAt}.log`);
37377
- return { path, fd: openSync(path, "a") };
37672
+ mkdirSync12(runsDirectory(), { recursive: true });
37673
+ const path = join21(runsDirectory(), `run-${startedAt}.log`);
37674
+ return { path, fd: openSync2(path, "a") };
37378
37675
  }
37379
37676
  function writeRunRecord(record) {
37380
- mkdirSync11(runsDirectory(), { recursive: true });
37381
- writeFileSync13(recordPath(), JSON.stringify(record, null, 2), "utf8");
37677
+ mkdirSync12(runsDirectory(), { recursive: true });
37678
+ writeFileSync14(recordPath(), JSON.stringify(record, null, 2), "utf8");
37382
37679
  }
37383
37680
  function editorNotePath() {
37384
- return join20(runsDirectory(), "editor-run.json");
37681
+ return join21(runsDirectory(), "editor-run.json");
37385
37682
  }
37386
37683
  function writeEditorRunNote(note) {
37387
- mkdirSync11(runsDirectory(), { recursive: true });
37388
- writeFileSync13(editorNotePath(), JSON.stringify(note, null, 2), "utf8");
37684
+ mkdirSync12(runsDirectory(), { recursive: true });
37685
+ writeFileSync14(editorNotePath(), JSON.stringify(note, null, 2), "utf8");
37389
37686
  }
37390
37687
  function readEditorRunNote() {
37391
37688
  for (const directory of runtimeDirectories()) {
37392
- const note = editorNoteAt(join20(directory, "runs", "editor-run.json"));
37689
+ const note = editorNoteAt(join21(directory, "runs", "editor-run.json"));
37393
37690
  if (note !== null) {
37394
37691
  return note;
37395
37692
  }
@@ -37399,7 +37696,7 @@ function readEditorRunNote() {
37399
37696
  function editorNoteAt(path) {
37400
37697
  let parsed;
37401
37698
  try {
37402
- parsed = JSON.parse(readFileSync18(path, "utf8"));
37699
+ parsed = JSON.parse(readFileSync19(path, "utf8"));
37403
37700
  } catch {
37404
37701
  return null;
37405
37702
  }
@@ -37422,7 +37719,7 @@ function recordRunEnded(pid, exitCode) {
37422
37719
  return;
37423
37720
  }
37424
37721
  try {
37425
- writeFileSync13(path, JSON.stringify({ ...record, exitCode }, null, 2), "utf8");
37722
+ writeFileSync14(path, JSON.stringify({ ...record, exitCode }, null, 2), "utf8");
37426
37723
  } catch {}
37427
37724
  }
37428
37725
  function readRunRecord() {
@@ -37437,7 +37734,7 @@ function readRunRecord() {
37437
37734
  function recordAt(path) {
37438
37735
  let parsed;
37439
37736
  try {
37440
- parsed = JSON.parse(readFileSync18(path, "utf8"));
37737
+ parsed = JSON.parse(readFileSync19(path, "utf8"));
37441
37738
  } catch {
37442
37739
  return null;
37443
37740
  }
@@ -37482,7 +37779,7 @@ function runningAs(pid) {
37482
37779
  const began = startedAt(pid, null);
37483
37780
  const when = began === null ? {} : { startedAt: began };
37484
37781
  if (process.platform === "linux") {
37485
- const raw = readFileSync18(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
37782
+ const raw = readFileSync19(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
37486
37783
  return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
37487
37784
  }
37488
37785
  const args = execFileSync2("ps", ["-p", String(pid), "-o", "args="], {
@@ -37609,13 +37906,13 @@ function clearRunRecord(ours = () => true) {
37609
37906
  continue;
37610
37907
  }
37611
37908
  try {
37612
- rmSync9(path);
37909
+ rmSync10(path);
37613
37910
  } catch {}
37614
37911
  }
37615
37912
  }
37616
37913
  function ageMs(path, now) {
37617
37914
  try {
37618
- return now - statSync4(path).mtimeMs;
37915
+ return now - statSync5(path).mtimeMs;
37619
37916
  } catch {
37620
37917
  return 0;
37621
37918
  }
@@ -37633,12 +37930,12 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
37633
37930
  if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
37634
37931
  continue;
37635
37932
  }
37636
- const path = join20(directory, entry);
37933
+ const path = join21(directory, entry);
37637
37934
  if (path === keep || ageMs(path, now) < keepMs) {
37638
37935
  continue;
37639
37936
  }
37640
37937
  try {
37641
- rmSync9(path);
37938
+ rmSync10(path);
37642
37939
  } catch {}
37643
37940
  }
37644
37941
  }
@@ -37654,20 +37951,20 @@ import { execFile as execFile7, spawn } from "node:child_process";
37654
37951
  import { randomUUID as randomUUID2 } from "node:crypto";
37655
37952
  import {
37656
37953
  appendFileSync,
37657
- closeSync,
37658
- existsSync as existsSync14,
37954
+ closeSync as closeSync2,
37955
+ existsSync as existsSync15,
37659
37956
  fstatSync,
37660
- mkdirSync as mkdirSync12,
37957
+ mkdirSync as mkdirSync13,
37661
37958
  mkdtempSync as mkdtempSync3,
37662
- openSync as openSync2,
37959
+ openSync as openSync3,
37663
37960
  readdirSync as readdirSync7,
37664
- readFileSync as readFileSync19,
37665
- readSync,
37961
+ readFileSync as readFileSync20,
37962
+ readSync as readSync2,
37666
37963
  rmdirSync,
37667
- statSync as statSync5
37964
+ statSync as statSync6
37668
37965
  } from "node:fs";
37669
37966
  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";
37967
+ import { basename as basename4, dirname as dirname15, isAbsolute as isAbsolute3, join as join22, normalize as normalize3, relative as relative3 } from "node:path";
37671
37968
  import { setTimeout as delay3 } from "node:timers/promises";
37672
37969
  import { fileURLToPath as fileURLToPath4 } from "node:url";
37673
37970
  import { promisify as promisify7 } from "node:util";
@@ -37909,7 +38206,7 @@ function aboveTheRunner(entry) {
37909
38206
  }
37910
38207
  function elsewhereIn(projectPath, asked) {
37911
38208
  const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
37912
- if (wanted === "" || existsSync14(join21(projectPath, wanted))) {
38209
+ if (wanted === "" || existsSync15(join22(projectPath, wanted))) {
37913
38210
  return "";
37914
38211
  }
37915
38212
  const root = (wanted.split("/")[0] ?? "").toLowerCase();
@@ -37923,7 +38220,7 @@ function elsewhereIn(projectPath, asked) {
37923
38220
  return near.length === 0 ? "" : ` The project does have ${there}.`;
37924
38221
  }
37925
38222
  function hasMainScene(projectFile) {
37926
- const scene = parseProjectGodot(readFileSync19(projectFile, "utf8"))["application"]?.["run/main_scene"];
38223
+ const scene = parseProjectGodot(readFileSync20(projectFile, "utf8"))["application"]?.["run/main_scene"];
37927
38224
  return typeof scene === "string" && scene !== "";
37928
38225
  }
37929
38226
  function runtimeAddonAnnouncesEditors(projectPath) {
@@ -37931,13 +38228,13 @@ function runtimeAddonAnnouncesEditors(projectPath) {
37931
38228
  return false;
37932
38229
  }
37933
38230
  try {
37934
- return readFileSync19(join21(projectPath, RUNTIME_AUTOLOAD2.path), "utf8").includes('"editor_pid"');
38231
+ return readFileSync20(join22(projectPath, RUNTIME_AUTOLOAD2.path), "utf8").includes('"editor_pid"');
37935
38232
  } catch {
37936
38233
  return false;
37937
38234
  }
37938
38235
  }
37939
38236
  function editorPlaysHeadless(projectFile) {
37940
- const runArgs = parseProjectGodot(readFileSync19(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
38237
+ const runArgs = parseProjectGodot(readFileSync20(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
37941
38238
  return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
37942
38239
  }
37943
38240
  function camelCased(params) {
@@ -37956,7 +38253,7 @@ function needsItsOwnProcess(asked) {
37956
38253
  class GodotServer {
37957
38254
  mcp;
37958
38255
  locator = new GodotLocator2;
37959
- operationsScript = join21(__dirname2, "godot", "operations", "godot_operations.gd");
38256
+ operationsScript = join22(__dirname2, "godot", "operations", "godot_operations.gd");
37960
38257
  godotBridge;
37961
38258
  tools = buildToolDefinitions();
37962
38259
  activeProcess = null;
@@ -38621,8 +38918,8 @@ class GodotServer {
38621
38918
  if (path === undefined) {
38622
38919
  return { ok: false, response: this.createErrorResponse("projectPath is required.") };
38623
38920
  }
38624
- const file = join21(path, "project.godot");
38625
- if (!existsSync14(file)) {
38921
+ const file = join22(path, "project.godot");
38922
+ if (!existsSync15(file)) {
38626
38923
  return {
38627
38924
  ok: false,
38628
38925
  response: this.createErrorResponse(`Not a Godot project: ${path}`, [
@@ -38890,7 +39187,7 @@ class GodotServer {
38890
39187
  if (!engine.ok) {
38891
39188
  return engine.response;
38892
39189
  }
38893
- const application = parseProjectGodot(readFileSync19(project.value.file, "utf8"))["application"] ?? {};
39190
+ const application = parseProjectGodot(readFileSync20(project.value.file, "utf8"))["application"] ?? {};
38894
39191
  const name = application["config/name"];
38895
39192
  const mainScene = application["run/main_scene"];
38896
39193
  const info = {
@@ -38951,18 +39248,18 @@ class GodotServer {
38951
39248
  if (!engine.ok) {
38952
39249
  return engine.response;
38953
39250
  }
38954
- const folder = dirname14(output.absolutePath);
39251
+ const folder = dirname15(output.absolutePath);
38955
39252
  try {
38956
- mkdirSync12(folder, { recursive: true });
39253
+ mkdirSync13(folder, { recursive: true });
38957
39254
  } catch (error) {
38958
- return this.createErrorResponse(`Cannot create ${dirname14(output.relativePath)} to export into: ${errorMessage(error)}`);
39255
+ return this.createErrorResponse(`Cannot create ${dirname15(output.relativePath)} to export into: ${errorMessage(error)}`);
38959
39256
  }
38960
39257
  const debug = readBoolean(args, "debug") ?? false;
38961
- const exportLogs = mkdtempSync3(join21(tmpdir5(), "gdharness-export-"));
39258
+ const exportLogs = mkdtempSync3(join22(tmpdir5(), "gdharness-export-"));
38962
39259
  const exportArgs = [
38963
39260
  "--headless",
38964
39261
  "--log-file",
38965
- join21(exportLogs, "engine.log"),
39262
+ join22(exportLogs, "engine.log"),
38966
39263
  "--path",
38967
39264
  project.value.path,
38968
39265
  debug ? "--export-debug" : "--export-release",
@@ -38990,7 +39287,7 @@ class GodotServer {
38990
39287
  log.finish();
38991
39288
  const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
38992
39289
  const verdict = {
38993
- exported: exitCode === 0 && log.count("error") === 0 && existsSync14(output.absolutePath),
39290
+ exported: exitCode === 0 && log.count("error") === 0 && existsSync15(output.absolutePath),
38994
39291
  preset,
38995
39292
  outputPath: output.relativePath,
38996
39293
  debug,
@@ -39020,7 +39317,7 @@ class GodotServer {
39020
39317
  return contained.response;
39021
39318
  }
39022
39319
  const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
39023
- if (!existsSync14(join21(project.value.path, runner))) {
39320
+ if (!existsSync15(join22(project.value.path, runner))) {
39024
39321
  return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
39025
39322
  "Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
39026
39323
  ]);
@@ -39055,7 +39352,7 @@ class GodotServer {
39055
39352
  ];
39056
39353
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
39057
39354
  this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
39058
- const userData = mkdtempSync3(join21(tmpdir5(), "gdharness-tests-"));
39355
+ const userData = mkdtempSync3(join22(tmpdir5(), "gdharness-tests-"));
39059
39356
  const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
39060
39357
  const hung = await new Promise((resolve) => {
39061
39358
  const timer = setTimeout(() => {
@@ -39071,21 +39368,21 @@ class GodotServer {
39071
39368
  resolve(false);
39072
39369
  });
39073
39370
  });
39074
- const reportsDir = join21(project.value.path, ".godot", ...ours.split("/"));
39371
+ const reportsDir = join22(project.value.path, ".godot", ...ours.split("/"));
39075
39372
  let report = null;
39076
39373
  let reportProblem = null;
39077
39374
  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))) : [];
39375
+ const written = existsSync15(reportsDir) ? readdirSync7(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
39079
39376
  const newest = written.at(-1);
39080
39377
  if (newest !== undefined) {
39081
- report = parseJUnit(readFileSync19(join21(reportsDir, newest, "results.xml"), "utf8"));
39378
+ report = parseJUnit(readFileSync20(join22(reportsDir, newest, "results.xml"), "utf8"));
39082
39379
  }
39083
39380
  } catch (error) {
39084
39381
  reportProblem = errorMessage(error);
39085
39382
  } finally {
39086
39383
  discard(reportsDir, userData);
39087
39384
  try {
39088
- rmdirSync(dirname14(reportsDir));
39385
+ rmdirSync(dirname15(reportsDir));
39089
39386
  } catch {}
39090
39387
  }
39091
39388
  const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries.map(aboveTheRunner);
@@ -39217,11 +39514,11 @@ class GodotServer {
39217
39514
  const cached = cachedClasses(projectPath);
39218
39515
  const contradicted = cached === null ? [] : contradictedDiagnostics(messages, cached, (resourcePath) => {
39219
39516
  const contained = resolveWithinProject(projectPath, resourcePath);
39220
- if (!contained.ok || !existsSync14(contained.absolutePath)) {
39517
+ if (!contained.ok || !existsSync15(contained.absolutePath)) {
39221
39518
  return null;
39222
39519
  }
39223
39520
  try {
39224
- return readFileSync19(contained.absolutePath, "utf8");
39521
+ return readFileSync20(contained.absolutePath, "utf8");
39225
39522
  } catch {
39226
39523
  return null;
39227
39524
  }
@@ -39271,11 +39568,11 @@ class GodotServer {
39271
39568
  }
39272
39569
  sourceOfClass(projectPath, resourcePath) {
39273
39570
  const contained = resolveWithinProject(projectPath, resourcePath);
39274
- if (!contained.ok || !existsSync14(contained.absolutePath)) {
39571
+ if (!contained.ok || !existsSync15(contained.absolutePath)) {
39275
39572
  return null;
39276
39573
  }
39277
39574
  try {
39278
- return readFileSync19(contained.absolutePath, "utf8");
39575
+ return readFileSync20(contained.absolutePath, "utf8");
39279
39576
  } catch {
39280
39577
  return null;
39281
39578
  }
@@ -39504,7 +39801,7 @@ class GodotServer {
39504
39801
  return null;
39505
39802
  }
39506
39803
  try {
39507
- return settingKeys(readFileSync19(join21(projectPath, "project.godot"), "utf8"));
39804
+ return settingKeys(readFileSync20(join22(projectPath, "project.godot"), "utf8"));
39508
39805
  } catch {
39509
39806
  return null;
39510
39807
  }
@@ -39634,7 +39931,8 @@ class GodotServer {
39634
39931
  });
39635
39932
  }
39636
39933
  async openAnEditor(engine, projectPath, ports) {
39637
- const editor = spawn(engine, editorArguments(projectPath, ports), {
39934
+ clearEditorLog(projectPath);
39935
+ const editor = spawn(engine, editorArguments(projectPath, ports, editorLogPath(projectPath)), {
39638
39936
  stdio: "ignore",
39639
39937
  detached: true,
39640
39938
  env: {
@@ -39645,6 +39943,9 @@ class GodotServer {
39645
39943
  [OPENED_BY_A_SERVER]: "1"
39646
39944
  }
39647
39945
  });
39946
+ if (editor.pid !== undefined) {
39947
+ writeEditorLogNote(projectPath, editor.pid);
39948
+ }
39648
39949
  const started = await new Promise((resolve) => {
39649
39950
  editor.once("spawn", () => {
39650
39951
  resolve(null);
@@ -39751,7 +40052,7 @@ class GodotServer {
39751
40052
  }
39752
40053
  async runtimeUp(projectPath, before, budget, withArgs) {
39753
40054
  const budgetMs = budget.ms;
39754
- if (!existsSync14(join21(projectPath, RUNTIME_AUTOLOAD2.path))) {
40055
+ if (!existsSync15(join22(projectPath, RUNTIME_AUTOLOAD2.path))) {
39755
40056
  return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
39756
40057
  }
39757
40058
  let asking = null;
@@ -39814,7 +40115,7 @@ class GodotServer {
39814
40115
  }
39815
40116
  let announcedAt;
39816
40117
  try {
39817
- announcedAt = statSync5(endpoint.file).mtimeMs;
40118
+ announcedAt = statSync6(endpoint.file).mtimeMs;
39818
40119
  } catch {
39819
40120
  return;
39820
40121
  }
@@ -39850,7 +40151,7 @@ class GodotServer {
39850
40151
  }
39851
40152
  const startedAt = Date.now();
39852
40153
  const transcript = openTranscript(startedAt);
39853
- closeSync(transcript.fd);
40154
+ closeSync2(transcript.fd);
39854
40155
  const playAnswer = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
39855
40156
  const played = {
39856
40157
  process: null,
@@ -39917,7 +40218,7 @@ class GodotServer {
39917
40218
  }
39918
40219
  let handle;
39919
40220
  try {
39920
- handle = openSync2(game.errorReport, "r");
40221
+ handle = openSync3(game.errorReport, "r");
39921
40222
  } catch {
39922
40223
  return;
39923
40224
  }
@@ -39928,7 +40229,7 @@ class GodotServer {
39928
40229
  return;
39929
40230
  }
39930
40231
  const buffer = Buffer.alloc(size - from);
39931
- const read = readSync(handle, buffer, 0, buffer.length, from);
40232
+ const read = readSync2(handle, buffer, 0, buffer.length, from);
39932
40233
  game.errorReportOffset = from + read;
39933
40234
  const reported = buffer.subarray(0, read);
39934
40235
  if (game.transcript !== null) {
@@ -39937,7 +40238,7 @@ class GodotServer {
39937
40238
  game.log.append("stderr", reported);
39938
40239
  }
39939
40240
  } finally {
39940
- closeSync(handle);
40241
+ closeSync2(handle);
39941
40242
  }
39942
40243
  }
39943
40244
  recordWhatItBrokeOn(game) {
@@ -40031,7 +40332,7 @@ class GodotServer {
40031
40332
  ...env ? { env } : {}
40032
40333
  });
40033
40334
  } finally {
40034
- closeSync(transcript.fd);
40335
+ closeSync2(transcript.fd);
40035
40336
  }
40036
40337
  child.unref();
40037
40338
  const log = new GameLog;
@@ -40078,7 +40379,7 @@ class GodotServer {
40078
40379
  }
40079
40380
  let handle;
40080
40381
  try {
40081
- handle = openSync2(game.transcript, "r");
40382
+ handle = openSync3(game.transcript, "r");
40082
40383
  } catch {
40083
40384
  return;
40084
40385
  }
@@ -40088,11 +40389,11 @@ class GodotServer {
40088
40389
  return;
40089
40390
  }
40090
40391
  const buffer = Buffer.alloc(size - game.readOffset);
40091
- const read = readSync(handle, buffer, 0, buffer.length, game.readOffset);
40392
+ const read = readSync2(handle, buffer, 0, buffer.length, game.readOffset);
40092
40393
  game.readOffset += read;
40093
40394
  game.log.append("transcript", buffer.subarray(0, read));
40094
40395
  } finally {
40095
- closeSync(handle);
40396
+ closeSync2(handle);
40096
40397
  }
40097
40398
  }
40098
40399
  async pickUpWhatTheEditorIsPlaying() {
@@ -40290,7 +40591,7 @@ class GodotServer {
40290
40591
  }
40291
40592
  const unreadable = this.aRuntimeOfOursIsTooFarOff();
40292
40593
  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";
40594
+ const behind = whichHalfIsBehind([unreadable]);
40294
40595
  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
40596
  }
40296
40597
  const record = readRunRecord();
@@ -40415,7 +40716,57 @@ class GodotServer {
40415
40716
  }
40416
40717
  return runIsUp(run, said);
40417
40718
  }
40719
+ handleEditorConsole(args) {
40720
+ const status = this.godotBridge.getStatus();
40721
+ const projectPath = status.projectPath ?? this.ownProject ?? undefined;
40722
+ if (projectPath === undefined) {
40723
+ return this.createErrorResponse("No editor is connected, so there is no console to read.", [
40724
+ "editor_status says whether one is connected and whether it may yet be",
40725
+ "editor_launch opens one, and an editor opened here has its console captured"
40726
+ ]);
40727
+ }
40728
+ const console2 = editorConsole(projectPath, status.editorPid ?? null, status.openedByAServer === true);
40729
+ if ("kind" in console2) {
40730
+ return this.createErrorResponse(theConsoleWasNotCaptured(console2, projectPath), [
40731
+ "editor_launch restart replaces this editor with one whose console is captured",
40732
+ "editor_output with no op answers about the game a run is playing, which is a different log"
40733
+ ]);
40734
+ }
40735
+ const severity = readString(args, "severity");
40736
+ const selected = console2.log.select({
40737
+ severity: severity === "error" || severity === "warning" ? severity : "info",
40738
+ sinceLastCall: false,
40739
+ contains: readNonEmptyString(args, "contains"),
40740
+ limit: readPositiveNumber(args, "limit") ?? 200
40741
+ });
40742
+ const everything = console2.log.everything();
40743
+ const grouped = bursts(everything, readPositiveNumber(args, "before") ?? 3);
40744
+ const shown = grouped.slice(0, MOST_GROUPS);
40745
+ return this.jsonTextResponse({
40746
+ editorPid: status.editorPid,
40747
+ projectPath,
40748
+ capturedIn: console2.path,
40749
+ ...console2.of === undefined ? {} : {
40750
+ readFromTheStart: console2.readBytes,
40751
+ consoleBytes: console2.of,
40752
+ 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.`
40753
+ },
40754
+ lines: everything.length,
40755
+ counts: {
40756
+ error: console2.log.count("error"),
40757
+ warning: console2.log.count("warning"),
40758
+ info: console2.log.count("info")
40759
+ },
40760
+ repeated: shown,
40761
+ moreShapes: grouped.length > shown.length ? grouped.length - shown.length : undefined,
40762
+ entries: forAnswer(selected.entries),
40763
+ omitted: selected.omitted === 0 ? undefined : selected.omitted
40764
+ });
40765
+ }
40418
40766
  async handleGetDebugOutput(args, waitedMs) {
40767
+ if (readString(args, "op") === "editor") {
40768
+ return this.handleEditorConsole(args);
40769
+ }
40419
40770
  await this.pickUpWhatTheEditorIsPlaying();
40420
40771
  const run = this.currentRun();
40421
40772
  if (!run) {
@@ -40866,8 +41217,8 @@ class GodotServer {
40866
41217
  ]);
40867
41218
  }
40868
41219
  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;
41220
+ const screenshotDir = expectsScreenshot ? mkdtempSync3(join22(tmpdir5(), "gdharness-runtime-screenshot-")) : null;
41221
+ const screenshotPath = screenshotDir ? join22(screenshotDir, "capture.png") : null;
40871
41222
  try {
40872
41223
  const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs);
40873
41224
  if (!reply.ok) {
@@ -40891,7 +41242,7 @@ class GodotServer {
40891
41242
  type: "text",
40892
41243
  text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}`
40893
41244
  },
40894
- { type: "image", data: readFileSync19(screenshotPath).toString("base64"), mimeType: "image/png" }
41245
+ { type: "image", data: readFileSync20(screenshotPath).toString("base64"), mimeType: "image/png" }
40895
41246
  ]
40896
41247
  };
40897
41248
  } finally {
@@ -40958,7 +41309,7 @@ async function runGodotServer() {
40958
41309
  const server = new GodotServer;
40959
41310
  await server.run();
40960
41311
  }
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/";
41312
+ 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
41313
  var init_server3 = __esm(() => {
40963
41314
  init_mcp();
40964
41315
  init_stdio2();
@@ -40970,6 +41321,7 @@ var init_server3 = __esm(() => {
40970
41321
  init_class_note();
40971
41322
  init_config_pin();
40972
41323
  init_dap_client();
41324
+ init_editor_log();
40973
41325
  init_errors();
40974
41326
  init_game_log();
40975
41327
  init_godot_bridge();
@@ -40994,7 +41346,7 @@ var init_server3 = __esm(() => {
40994
41346
  init_tool_definitions();
40995
41347
  init_update_check();
40996
41348
  run7 = promisify7(execFile7);
40997
- __dirname2 = dirname14(fileURLToPath4(import.meta.url));
41349
+ __dirname2 = dirname15(fileURLToPath4(import.meta.url));
40998
41350
  CONSOLE_WRAPPER = /_console(\.exe)?$/i;
40999
41351
  LONGEST_BOOT_NOTED_MS = 5 * 60000;
41000
41352
  VALUE_NAMES = {
@@ -41034,8 +41386,8 @@ var init_server3 = __esm(() => {
41034
41386
  });
41035
41387
 
41036
41388
  // src/cli.ts
41037
- import { existsSync as existsSync15 } from "fs";
41038
- import { join as join22, resolve as resolve5 } from "path";
41389
+ import { existsSync as existsSync16 } from "fs";
41390
+ import { join as join23, resolve as resolve5 } from "path";
41039
41391
 
41040
41392
  // src/errors.ts
41041
41393
  class Refusal extends Error {
@@ -42532,6 +42884,14 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
42532
42884
  runtime addon announces nothing, so there is no process id and \`running\` is the editor's answer,
42533
42885
  which can lag by seconds after the game has gone. \`editor_status\` names the game under
42534
42886
  \`runtimes\`, which does not depend on the editor answering.
42887
+ - When the tools all say the project is fine and something is still wrong, read the editor's own
42888
+ console with \`editor_output op: "editor"\`. It is a different log from the one above and the only
42889
+ way anything here sees what the editor printed: its console reaches no plugin, so an editor
42890
+ gdharness opened is asked for a log file as it starts, and one somebody opened by hand is refused
42891
+ rather than answered empty. It carries the whole session from the editor's first line, which is
42892
+ where a wall of startup errors will be, with repeated messages grouped under \`repeated\` and the
42893
+ ordinary lines above each group beside them: a plugin announcing itself on the line before a
42894
+ burst of parse errors is usually the whole diagnosis.
42535
42895
  - One server, one editor. A second project is a second harness session with its own server, and
42536
42896
  that works: each editor is opened on its own language server and debug adapter ports, and says
42537
42897
  where it serves. Two editors on one project is the thing to refuse.
@@ -42741,7 +43101,7 @@ class UsageError extends Refusal {
42741
43101
  function projectArgument(at) {
42742
43102
  const named = args.slice(at).find((value) => !value.startsWith("--"));
42743
43103
  const projectPath = resolve5(named ?? ".");
42744
- if (!existsSync15(join22(projectPath, "project.godot"))) {
43104
+ if (!existsSync16(join23(projectPath, "project.godot"))) {
42745
43105
  throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
42746
43106
  }
42747
43107
  return projectPath;
@@ -42780,7 +43140,7 @@ async function registerRuntime(godot, projectPath) {
42780
43140
  return;
42781
43141
  }
42782
43142
  console.log(`${RUNTIME_AUTOLOAD.name} autoload left at ${named}, which this project registers itself`);
42783
- if (!existsSync15(join22(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
43143
+ if (!existsSync16(join23(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
42784
43144
  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
43145
  }
42786
43146
  }
@@ -42912,7 +43272,7 @@ async function setup() {
42912
43272
  console.log("no harness configured; name one yourself, and gdharness harnesses lists them all");
42913
43273
  }
42914
43274
  if (!args.includes("--no-skill")) {
42915
- const directories = skillDirectories(chosen.harnesses, projectPath, existsSync15);
43275
+ const directories = skillDirectories(chosen.harnesses, projectPath, existsSync16);
42916
43276
  for (const written of writeSkill(directories, getLocalVersion())) {
42917
43277
  console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
42918
43278
  }
@@ -43005,7 +43365,7 @@ async function upgrade() {
43005
43365
  moved.push(took.from);
43006
43366
  }
43007
43367
  }
43008
- for (const written of writeSkill(skillDirectories(already, projectPath, existsSync15), version)) {
43368
+ for (const written of writeSkill(skillDirectories(already, projectPath, existsSync16), version)) {
43009
43369
  console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
43010
43370
  }
43011
43371
  console.log(`