gdharness 1.0.10 → 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 +475 -112
  2. package/build/index.js +480 -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];
@@ -15348,6 +15361,9 @@ function projectPathSentence(without = callsWithoutProjectPath()) {
15348
15361
  const named = without.map((name) => `\`${name}\``).join(", ");
15349
15362
  return `Every call takes \`projectPath\` except ${named}, which ${without.length === 1 ? "takes" : "take"} none`;
15350
15363
  }
15364
+ function theGamePicked() {
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";
15366
+ }
15351
15367
  function toolSpec(name) {
15352
15368
  return SPECS_BY_NAME[name];
15353
15369
  }
@@ -15408,7 +15424,7 @@ var init_tool_definitions = __esm(() => {
15408
15424
  };
15409
15425
  RUNNING_PROJECT_PATH = {
15410
15426
  type: "string",
15411
- description: "Which game, when more than one is running: the project directory it was started from. Not needed with one game."
15427
+ description: `Which game, when more than one is running: the project directory it was started from. The pick without it, in order: ${theGamePicked().replaceAll("`", "")}. Not needed with one game, nor for a call to this server's own.`
15412
15428
  };
15413
15429
  RUNNING_PID = {
15414
15430
  type: "number",
@@ -16067,7 +16083,7 @@ var init_tool_definitions = __esm(() => {
16067
16083
  },
16068
16084
  {
16069
16085
  name: "editor_output",
16070
- 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.",
16071
16087
  parameters: {
16072
16088
  severity: {
16073
16089
  type: "string",
@@ -16075,10 +16091,12 @@ var init_tool_definitions = __esm(() => {
16075
16091
  description: "The least severe entry to include. Default info, which is everything."
16076
16092
  },
16077
16093
  sinceLastCall: {
16094
+ ops: ["run"],
16078
16095
  type: "boolean",
16079
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."
16080
16097
  },
16081
16098
  cpu: {
16099
+ ops: ["run"],
16082
16100
  type: "boolean",
16083
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."
16084
16102
  },
@@ -16089,8 +16107,24 @@ var init_tool_definitions = __esm(() => {
16089
16107
  limit: {
16090
16108
  type: "number",
16091
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: []
16092
16125
  }
16093
16126
  },
16127
+ defaultOperation: "run",
16094
16128
  requires: []
16095
16129
  },
16096
16130
  {
@@ -31690,10 +31724,37 @@ async function announcedSince(projectPath, before, waiting = {}) {
31690
31724
  function describe2(endpoint) {
31691
31725
  return `pid ${endpoint.pid} on ${endpoint.address}:${endpoint.port} (${endpoint.project.name || "unnamed"} at ${endpoint.project.path})`;
31692
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
+ }
31693
31753
  function tooNew(unspoken) {
31694
31754
  const named = unspoken.map((game) => `pid ${game.pid} speaks ${game.protocol} (${game.project.name || "unnamed"} at ${game.project.path})`).join("; ");
31695
- 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";
31696
- 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)}.`;
31697
31758
  }
31698
31759
  function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
31699
31760
  if (pid !== undefined) {
@@ -31710,8 +31771,16 @@ function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
31710
31771
  if (tooNewToo !== undefined) {
31711
31772
  return { problem: tooNew([tooNewToo]) };
31712
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
+ }
31713
31782
  return {
31714
- 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.`
31715
31784
  };
31716
31785
  }
31717
31786
  if (endpoints.length === 0) {
@@ -31743,15 +31812,17 @@ function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
31743
31812
  if (matching.length === 1 && matching[0]) {
31744
31813
  return { endpoint: matching[0] };
31745
31814
  }
31746
- return {
31747
- problem: `Several games are running from ${wanted}: ${matching.map(describe2).join("; ")}. Pass pid to choose one.`
31748
- };
31815
+ return { problem: severalFrom(wanted, matching) };
31749
31816
  }
31750
31817
  if (endpoints.length === 1 && endpoints[0]) {
31751
31818
  return { endpoint: endpoints[0] };
31752
31819
  }
31820
+ const shared = theOneProject(endpoints);
31821
+ if (shared !== undefined) {
31822
+ return { problem: severalFrom(shared, endpoints) };
31823
+ }
31753
31824
  return {
31754
- 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.`
31755
31826
  };
31756
31827
  }
31757
31828
  function runtimeRequest(endpoint, command, params, timeoutMs) {
@@ -32799,6 +32870,235 @@ var init_dap_client = __esm(() => {
32799
32870
  init_ports();
32800
32871
  });
32801
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
+
32802
33102
  // node_modules/ws/lib/constants.js
32803
33103
  var require_constants = __commonJS(function(exports, module) {
32804
33104
  var BINARY_TYPES = ["nodebuffer", "arraybuffer", "fragments"];
@@ -36179,7 +36479,7 @@ var init_godot_bridge = __esm(() => {
36179
36479
 
36180
36480
  // src/godot-path.ts
36181
36481
  import { execFile as execFile5 } from "node:child_process";
36182
- import { existsSync as existsSync12 } from "node:fs";
36482
+ import { existsSync as existsSync13 } from "node:fs";
36183
36483
  import { normalize as normalize2 } from "node:path";
36184
36484
  import { promisify as promisify5 } from "node:util";
36185
36485
 
@@ -36234,7 +36534,7 @@ class GodotLocator2 {
36234
36534
  return known;
36235
36535
  }
36236
36536
  let reason;
36237
- if (path !== "godot" && !existsSync12(path)) {
36537
+ if (path !== "godot" && !existsSync13(path)) {
36238
36538
  reason = "does not exist";
36239
36539
  } else {
36240
36540
  try {
@@ -36610,7 +36910,7 @@ var init_junit = __esm(() => {
36610
36910
  import { realpathSync as realpathSync2 } from "node:fs";
36611
36911
  import { readFile, realpath } from "node:fs/promises";
36612
36912
  import { createConnection as createConnection3 } from "node:net";
36613
- import { dirname as dirname12, resolve as resolve4 } from "node:path";
36913
+ import { dirname as dirname13, resolve as resolve4 } from "node:path";
36614
36914
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
36615
36915
  function diagnosticsKey(uri) {
36616
36916
  let decoded;
@@ -36738,7 +37038,7 @@ class GodotLSPClient {
36738
37038
  if (this.initialized) {
36739
37039
  return;
36740
37040
  }
36741
- const rootPath = this.rootPath ?? dirname12(resolve4(filePath));
37041
+ const rootPath = this.rootPath ?? dirname13(resolve4(filePath));
36742
37042
  await this.initialize(rootPath);
36743
37043
  }
36744
37044
  async sendRequest(method, params) {
@@ -37183,13 +37483,13 @@ var init_process_time = __esm(() => {
37183
37483
  });
37184
37484
 
37185
37485
  // src/project-scan.ts
37186
- import { existsSync as existsSync13, readdirSync as readdirSync5, readFileSync as readFileSync16 } from "node:fs";
37187
- 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";
37188
37488
  function skipped(name) {
37189
37489
  return name.startsWith(".") || SKIPPED.has(name);
37190
37490
  }
37191
37491
  function steppedOver(directory) {
37192
- return existsSync13(join18(directory, ".gdignore"));
37492
+ return existsSync14(join19(directory, ".gdignore"));
37193
37493
  }
37194
37494
  function projectStructure(projectPath) {
37195
37495
  const structure = { scenes: 0, scripts: 0, assets: 0, other: 0 };
@@ -37202,7 +37502,7 @@ function projectStructure(projectPath) {
37202
37502
  continue;
37203
37503
  }
37204
37504
  if (entry.isDirectory()) {
37205
- visit(join18(directory, entry.name));
37505
+ visit(join19(directory, entry.name));
37206
37506
  } else if (entry.isFile()) {
37207
37507
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
37208
37508
  if (extension === "tscn") {
@@ -37232,14 +37532,14 @@ function scriptsWithoutUid(projectPath) {
37232
37532
  }
37233
37533
  const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
37234
37534
  if (entry.isDirectory()) {
37235
- visit(join18(directory, entry.name), spelled);
37535
+ visit(join19(directory, entry.name), spelled);
37236
37536
  continue;
37237
37537
  }
37238
37538
  if (!entry.isFile()) {
37239
37539
  continue;
37240
37540
  }
37241
37541
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
37242
- 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`))) {
37243
37543
  found.push(spelled);
37244
37544
  }
37245
37545
  }
@@ -37274,7 +37574,7 @@ function searchProject(projectPath, options) {
37274
37574
  if (skipped(entry.name)) {
37275
37575
  continue;
37276
37576
  }
37277
- const entryPath = join18(directory, entry.name);
37577
+ const entryPath = join19(directory, entry.name);
37278
37578
  if (entry.isDirectory()) {
37279
37579
  visit(entryPath);
37280
37580
  continue;
@@ -37285,7 +37585,7 @@ function searchProject(projectPath, options) {
37285
37585
  }
37286
37586
  result.summary.files_searched += 1;
37287
37587
  const matches = [];
37288
- for (const [index, line] of readFileSync16(entryPath, "utf8").split(`
37588
+ for (const [index, line] of readFileSync17(entryPath, "utf8").split(`
37289
37589
  `).entries()) {
37290
37590
  if (full()) {
37291
37591
  break;
@@ -37314,16 +37614,16 @@ var init_project_scan = __esm(() => {
37314
37614
  });
37315
37615
 
37316
37616
  // src/restart-note.ts
37317
- import { mkdirSync as mkdirSync10, readFileSync as readFileSync17, rmSync as rmSync8, writeFileSync as writeFileSync12 } from "node:fs";
37318
- 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";
37319
37619
  function restartNotePath(projectPath) {
37320
- return join19(projectPath, ".godot", "gdharness-restart.json");
37620
+ return join20(projectPath, ".godot", "gdharness-restart.json");
37321
37621
  }
37322
37622
  function noteRestartBegun(note) {
37323
37623
  const path = restartNotePath(note.projectPath);
37324
37624
  try {
37325
- mkdirSync10(dirname13(path), { recursive: true });
37326
- writeFileSync12(path, `${JSON.stringify(note, null, 2)}
37625
+ mkdirSync11(dirname14(path), { recursive: true });
37626
+ writeFileSync13(path, `${JSON.stringify(note, null, 2)}
37327
37627
  `, "utf8");
37328
37628
  } catch (error) {
37329
37629
  console.error(`[SERVER] Could not record the restart at ${path}: ${errorMessage(error)}`);
@@ -37331,7 +37631,7 @@ function noteRestartBegun(note) {
37331
37631
  }
37332
37632
  function restartOwed(projectPath) {
37333
37633
  try {
37334
- const parsed = JSON.parse(readFileSync17(restartNotePath(projectPath), "utf8"));
37634
+ const parsed = JSON.parse(readFileSync18(restartNotePath(projectPath), "utf8"));
37335
37635
  if (typeof parsed !== "object" || parsed === null) {
37336
37636
  return null;
37337
37637
  }
@@ -37346,7 +37646,7 @@ function restartOwed(projectPath) {
37346
37646
  }
37347
37647
  function restartSettled(projectPath) {
37348
37648
  try {
37349
- rmSync8(restartNotePath(projectPath), { force: true });
37649
+ rmSync9(restartNotePath(projectPath), { force: true });
37350
37650
  } catch (error) {
37351
37651
  console.error(`[SERVER] Could not clear the restart note: ${errorMessage(error)}`);
37352
37652
  }
@@ -37357,36 +37657,36 @@ var init_restart_note = __esm(() => {
37357
37657
 
37358
37658
  // src/run-record.ts
37359
37659
  import { execFileSync as execFileSync2 } from "node:child_process";
37360
- import { mkdirSync as mkdirSync11, openSync, readdirSync as readdirSync6, readFileSync as readFileSync18, rmSync as rmSync9, statSync as statSync4, writeFileSync as writeFileSync13 } from "node:fs";
37361
- 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";
37362
37662
  function runsDirectory() {
37363
- return join20(runtimeDirectory(), "runs");
37663
+ return join21(runtimeDirectory(), "runs");
37364
37664
  }
37365
37665
  function recordPath() {
37366
- return join20(runsDirectory(), "run.json");
37666
+ return join21(runsDirectory(), "run.json");
37367
37667
  }
37368
37668
  function recordPaths() {
37369
- return runtimeDirectories().map((directory) => join20(directory, "runs", "run.json"));
37669
+ return runtimeDirectories().map((directory) => join21(directory, "runs", "run.json"));
37370
37670
  }
37371
37671
  function openTranscript(startedAt) {
37372
- mkdirSync11(runsDirectory(), { recursive: true });
37373
- const path = join20(runsDirectory(), `run-${startedAt}.log`);
37374
- 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") };
37375
37675
  }
37376
37676
  function writeRunRecord(record) {
37377
- mkdirSync11(runsDirectory(), { recursive: true });
37378
- writeFileSync13(recordPath(), JSON.stringify(record, null, 2), "utf8");
37677
+ mkdirSync12(runsDirectory(), { recursive: true });
37678
+ writeFileSync14(recordPath(), JSON.stringify(record, null, 2), "utf8");
37379
37679
  }
37380
37680
  function editorNotePath() {
37381
- return join20(runsDirectory(), "editor-run.json");
37681
+ return join21(runsDirectory(), "editor-run.json");
37382
37682
  }
37383
37683
  function writeEditorRunNote(note) {
37384
- mkdirSync11(runsDirectory(), { recursive: true });
37385
- writeFileSync13(editorNotePath(), JSON.stringify(note, null, 2), "utf8");
37684
+ mkdirSync12(runsDirectory(), { recursive: true });
37685
+ writeFileSync14(editorNotePath(), JSON.stringify(note, null, 2), "utf8");
37386
37686
  }
37387
37687
  function readEditorRunNote() {
37388
37688
  for (const directory of runtimeDirectories()) {
37389
- const note = editorNoteAt(join20(directory, "runs", "editor-run.json"));
37689
+ const note = editorNoteAt(join21(directory, "runs", "editor-run.json"));
37390
37690
  if (note !== null) {
37391
37691
  return note;
37392
37692
  }
@@ -37396,7 +37696,7 @@ function readEditorRunNote() {
37396
37696
  function editorNoteAt(path) {
37397
37697
  let parsed;
37398
37698
  try {
37399
- parsed = JSON.parse(readFileSync18(path, "utf8"));
37699
+ parsed = JSON.parse(readFileSync19(path, "utf8"));
37400
37700
  } catch {
37401
37701
  return null;
37402
37702
  }
@@ -37419,7 +37719,7 @@ function recordRunEnded(pid, exitCode) {
37419
37719
  return;
37420
37720
  }
37421
37721
  try {
37422
- writeFileSync13(path, JSON.stringify({ ...record, exitCode }, null, 2), "utf8");
37722
+ writeFileSync14(path, JSON.stringify({ ...record, exitCode }, null, 2), "utf8");
37423
37723
  } catch {}
37424
37724
  }
37425
37725
  function readRunRecord() {
@@ -37434,7 +37734,7 @@ function readRunRecord() {
37434
37734
  function recordAt(path) {
37435
37735
  let parsed;
37436
37736
  try {
37437
- parsed = JSON.parse(readFileSync18(path, "utf8"));
37737
+ parsed = JSON.parse(readFileSync19(path, "utf8"));
37438
37738
  } catch {
37439
37739
  return null;
37440
37740
  }
@@ -37479,7 +37779,7 @@ function runningAs(pid) {
37479
37779
  const began = startedAt(pid, null);
37480
37780
  const when = began === null ? {} : { startedAt: began };
37481
37781
  if (process.platform === "linux") {
37482
- const raw = readFileSync18(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
37782
+ const raw = readFileSync19(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
37483
37783
  return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
37484
37784
  }
37485
37785
  const args = execFileSync2("ps", ["-p", String(pid), "-o", "args="], {
@@ -37606,13 +37906,13 @@ function clearRunRecord(ours = () => true) {
37606
37906
  continue;
37607
37907
  }
37608
37908
  try {
37609
- rmSync9(path);
37909
+ rmSync10(path);
37610
37910
  } catch {}
37611
37911
  }
37612
37912
  }
37613
37913
  function ageMs(path, now) {
37614
37914
  try {
37615
- return now - statSync4(path).mtimeMs;
37915
+ return now - statSync5(path).mtimeMs;
37616
37916
  } catch {
37617
37917
  return 0;
37618
37918
  }
@@ -37630,12 +37930,12 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
37630
37930
  if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
37631
37931
  continue;
37632
37932
  }
37633
- const path = join20(directory, entry);
37933
+ const path = join21(directory, entry);
37634
37934
  if (path === keep || ageMs(path, now) < keepMs) {
37635
37935
  continue;
37636
37936
  }
37637
37937
  try {
37638
- rmSync9(path);
37938
+ rmSync10(path);
37639
37939
  } catch {}
37640
37940
  }
37641
37941
  }
@@ -37651,20 +37951,20 @@ import { execFile as execFile7, spawn } from "node:child_process";
37651
37951
  import { randomUUID as randomUUID2 } from "node:crypto";
37652
37952
  import {
37653
37953
  appendFileSync,
37654
- closeSync,
37655
- existsSync as existsSync14,
37954
+ closeSync as closeSync2,
37955
+ existsSync as existsSync15,
37656
37956
  fstatSync,
37657
- mkdirSync as mkdirSync12,
37957
+ mkdirSync as mkdirSync13,
37658
37958
  mkdtempSync as mkdtempSync3,
37659
- openSync as openSync2,
37959
+ openSync as openSync3,
37660
37960
  readdirSync as readdirSync7,
37661
- readFileSync as readFileSync19,
37662
- readSync,
37961
+ readFileSync as readFileSync20,
37962
+ readSync as readSync2,
37663
37963
  rmdirSync,
37664
- statSync as statSync5
37964
+ statSync as statSync6
37665
37965
  } from "node:fs";
37666
37966
  import { tmpdir as tmpdir5 } from "node:os";
37667
- 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";
37668
37968
  import { setTimeout as delay3 } from "node:timers/promises";
37669
37969
  import { fileURLToPath as fileURLToPath4 } from "node:url";
37670
37970
  import { promisify as promisify7 } from "node:util";
@@ -37906,7 +38206,7 @@ function aboveTheRunner(entry) {
37906
38206
  }
37907
38207
  function elsewhereIn(projectPath, asked) {
37908
38208
  const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
37909
- if (wanted === "" || existsSync14(join21(projectPath, wanted))) {
38209
+ if (wanted === "" || existsSync15(join22(projectPath, wanted))) {
37910
38210
  return "";
37911
38211
  }
37912
38212
  const root = (wanted.split("/")[0] ?? "").toLowerCase();
@@ -37920,7 +38220,7 @@ function elsewhereIn(projectPath, asked) {
37920
38220
  return near.length === 0 ? "" : ` The project does have ${there}.`;
37921
38221
  }
37922
38222
  function hasMainScene(projectFile) {
37923
- const scene = parseProjectGodot(readFileSync19(projectFile, "utf8"))["application"]?.["run/main_scene"];
38223
+ const scene = parseProjectGodot(readFileSync20(projectFile, "utf8"))["application"]?.["run/main_scene"];
37924
38224
  return typeof scene === "string" && scene !== "";
37925
38225
  }
37926
38226
  function runtimeAddonAnnouncesEditors(projectPath) {
@@ -37928,13 +38228,13 @@ function runtimeAddonAnnouncesEditors(projectPath) {
37928
38228
  return false;
37929
38229
  }
37930
38230
  try {
37931
- return readFileSync19(join21(projectPath, RUNTIME_AUTOLOAD2.path), "utf8").includes('"editor_pid"');
38231
+ return readFileSync20(join22(projectPath, RUNTIME_AUTOLOAD2.path), "utf8").includes('"editor_pid"');
37932
38232
  } catch {
37933
38233
  return false;
37934
38234
  }
37935
38235
  }
37936
38236
  function editorPlaysHeadless(projectFile) {
37937
- const runArgs = parseProjectGodot(readFileSync19(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
38237
+ const runArgs = parseProjectGodot(readFileSync20(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
37938
38238
  return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
37939
38239
  }
37940
38240
  function camelCased(params) {
@@ -37953,7 +38253,7 @@ function needsItsOwnProcess(asked) {
37953
38253
  class GodotServer {
37954
38254
  mcp;
37955
38255
  locator = new GodotLocator2;
37956
- operationsScript = join21(__dirname2, "godot", "operations", "godot_operations.gd");
38256
+ operationsScript = join22(__dirname2, "godot", "operations", "godot_operations.gd");
37957
38257
  godotBridge;
37958
38258
  tools = buildToolDefinitions();
37959
38259
  activeProcess = null;
@@ -38618,8 +38918,8 @@ class GodotServer {
38618
38918
  if (path === undefined) {
38619
38919
  return { ok: false, response: this.createErrorResponse("projectPath is required.") };
38620
38920
  }
38621
- const file = join21(path, "project.godot");
38622
- if (!existsSync14(file)) {
38921
+ const file = join22(path, "project.godot");
38922
+ if (!existsSync15(file)) {
38623
38923
  return {
38624
38924
  ok: false,
38625
38925
  response: this.createErrorResponse(`Not a Godot project: ${path}`, [
@@ -38887,7 +39187,7 @@ class GodotServer {
38887
39187
  if (!engine.ok) {
38888
39188
  return engine.response;
38889
39189
  }
38890
- const application = parseProjectGodot(readFileSync19(project.value.file, "utf8"))["application"] ?? {};
39190
+ const application = parseProjectGodot(readFileSync20(project.value.file, "utf8"))["application"] ?? {};
38891
39191
  const name = application["config/name"];
38892
39192
  const mainScene = application["run/main_scene"];
38893
39193
  const info = {
@@ -38948,18 +39248,18 @@ class GodotServer {
38948
39248
  if (!engine.ok) {
38949
39249
  return engine.response;
38950
39250
  }
38951
- const folder = dirname14(output.absolutePath);
39251
+ const folder = dirname15(output.absolutePath);
38952
39252
  try {
38953
- mkdirSync12(folder, { recursive: true });
39253
+ mkdirSync13(folder, { recursive: true });
38954
39254
  } catch (error) {
38955
- 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)}`);
38956
39256
  }
38957
39257
  const debug = readBoolean(args, "debug") ?? false;
38958
- const exportLogs = mkdtempSync3(join21(tmpdir5(), "gdharness-export-"));
39258
+ const exportLogs = mkdtempSync3(join22(tmpdir5(), "gdharness-export-"));
38959
39259
  const exportArgs = [
38960
39260
  "--headless",
38961
39261
  "--log-file",
38962
- join21(exportLogs, "engine.log"),
39262
+ join22(exportLogs, "engine.log"),
38963
39263
  "--path",
38964
39264
  project.value.path,
38965
39265
  debug ? "--export-debug" : "--export-release",
@@ -38987,7 +39287,7 @@ class GodotServer {
38987
39287
  log.finish();
38988
39288
  const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
38989
39289
  const verdict = {
38990
- exported: exitCode === 0 && log.count("error") === 0 && existsSync14(output.absolutePath),
39290
+ exported: exitCode === 0 && log.count("error") === 0 && existsSync15(output.absolutePath),
38991
39291
  preset,
38992
39292
  outputPath: output.relativePath,
38993
39293
  debug,
@@ -39017,7 +39317,7 @@ class GodotServer {
39017
39317
  return contained.response;
39018
39318
  }
39019
39319
  const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
39020
- if (!existsSync14(join21(project.value.path, runner))) {
39320
+ if (!existsSync15(join22(project.value.path, runner))) {
39021
39321
  return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
39022
39322
  "Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
39023
39323
  ]);
@@ -39052,7 +39352,7 @@ class GodotServer {
39052
39352
  ];
39053
39353
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
39054
39354
  this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
39055
- const userData = mkdtempSync3(join21(tmpdir5(), "gdharness-tests-"));
39355
+ const userData = mkdtempSync3(join22(tmpdir5(), "gdharness-tests-"));
39056
39356
  const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
39057
39357
  const hung = await new Promise((resolve) => {
39058
39358
  const timer = setTimeout(() => {
@@ -39068,21 +39368,21 @@ class GodotServer {
39068
39368
  resolve(false);
39069
39369
  });
39070
39370
  });
39071
- const reportsDir = join21(project.value.path, ".godot", ...ours.split("/"));
39371
+ const reportsDir = join22(project.value.path, ".godot", ...ours.split("/"));
39072
39372
  let report = null;
39073
39373
  let reportProblem = null;
39074
39374
  try {
39075
- 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))) : [];
39076
39376
  const newest = written.at(-1);
39077
39377
  if (newest !== undefined) {
39078
- report = parseJUnit(readFileSync19(join21(reportsDir, newest, "results.xml"), "utf8"));
39378
+ report = parseJUnit(readFileSync20(join22(reportsDir, newest, "results.xml"), "utf8"));
39079
39379
  }
39080
39380
  } catch (error) {
39081
39381
  reportProblem = errorMessage(error);
39082
39382
  } finally {
39083
39383
  discard(reportsDir, userData);
39084
39384
  try {
39085
- rmdirSync(dirname14(reportsDir));
39385
+ rmdirSync(dirname15(reportsDir));
39086
39386
  } catch {}
39087
39387
  }
39088
39388
  const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries.map(aboveTheRunner);
@@ -39214,11 +39514,11 @@ class GodotServer {
39214
39514
  const cached = cachedClasses(projectPath);
39215
39515
  const contradicted = cached === null ? [] : contradictedDiagnostics(messages, cached, (resourcePath) => {
39216
39516
  const contained = resolveWithinProject(projectPath, resourcePath);
39217
- if (!contained.ok || !existsSync14(contained.absolutePath)) {
39517
+ if (!contained.ok || !existsSync15(contained.absolutePath)) {
39218
39518
  return null;
39219
39519
  }
39220
39520
  try {
39221
- return readFileSync19(contained.absolutePath, "utf8");
39521
+ return readFileSync20(contained.absolutePath, "utf8");
39222
39522
  } catch {
39223
39523
  return null;
39224
39524
  }
@@ -39268,11 +39568,11 @@ class GodotServer {
39268
39568
  }
39269
39569
  sourceOfClass(projectPath, resourcePath) {
39270
39570
  const contained = resolveWithinProject(projectPath, resourcePath);
39271
- if (!contained.ok || !existsSync14(contained.absolutePath)) {
39571
+ if (!contained.ok || !existsSync15(contained.absolutePath)) {
39272
39572
  return null;
39273
39573
  }
39274
39574
  try {
39275
- return readFileSync19(contained.absolutePath, "utf8");
39575
+ return readFileSync20(contained.absolutePath, "utf8");
39276
39576
  } catch {
39277
39577
  return null;
39278
39578
  }
@@ -39501,7 +39801,7 @@ class GodotServer {
39501
39801
  return null;
39502
39802
  }
39503
39803
  try {
39504
- return settingKeys(readFileSync19(join21(projectPath, "project.godot"), "utf8"));
39804
+ return settingKeys(readFileSync20(join22(projectPath, "project.godot"), "utf8"));
39505
39805
  } catch {
39506
39806
  return null;
39507
39807
  }
@@ -39631,7 +39931,8 @@ class GodotServer {
39631
39931
  });
39632
39932
  }
39633
39933
  async openAnEditor(engine, projectPath, ports) {
39634
- const editor = spawn(engine, editorArguments(projectPath, ports), {
39934
+ clearEditorLog(projectPath);
39935
+ const editor = spawn(engine, editorArguments(projectPath, ports, editorLogPath(projectPath)), {
39635
39936
  stdio: "ignore",
39636
39937
  detached: true,
39637
39938
  env: {
@@ -39642,6 +39943,9 @@ class GodotServer {
39642
39943
  [OPENED_BY_A_SERVER]: "1"
39643
39944
  }
39644
39945
  });
39946
+ if (editor.pid !== undefined) {
39947
+ writeEditorLogNote(projectPath, editor.pid);
39948
+ }
39645
39949
  const started = await new Promise((resolve) => {
39646
39950
  editor.once("spawn", () => {
39647
39951
  resolve(null);
@@ -39748,7 +40052,7 @@ class GodotServer {
39748
40052
  }
39749
40053
  async runtimeUp(projectPath, before, budget, withArgs) {
39750
40054
  const budgetMs = budget.ms;
39751
- if (!existsSync14(join21(projectPath, RUNTIME_AUTOLOAD2.path))) {
40055
+ if (!existsSync15(join22(projectPath, RUNTIME_AUTOLOAD2.path))) {
39752
40056
  return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
39753
40057
  }
39754
40058
  let asking = null;
@@ -39811,7 +40115,7 @@ class GodotServer {
39811
40115
  }
39812
40116
  let announcedAt;
39813
40117
  try {
39814
- announcedAt = statSync5(endpoint.file).mtimeMs;
40118
+ announcedAt = statSync6(endpoint.file).mtimeMs;
39815
40119
  } catch {
39816
40120
  return;
39817
40121
  }
@@ -39847,7 +40151,7 @@ class GodotServer {
39847
40151
  }
39848
40152
  const startedAt = Date.now();
39849
40153
  const transcript = openTranscript(startedAt);
39850
- closeSync(transcript.fd);
40154
+ closeSync2(transcript.fd);
39851
40155
  const playAnswer = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
39852
40156
  const played = {
39853
40157
  process: null,
@@ -39914,7 +40218,7 @@ class GodotServer {
39914
40218
  }
39915
40219
  let handle;
39916
40220
  try {
39917
- handle = openSync2(game.errorReport, "r");
40221
+ handle = openSync3(game.errorReport, "r");
39918
40222
  } catch {
39919
40223
  return;
39920
40224
  }
@@ -39925,7 +40229,7 @@ class GodotServer {
39925
40229
  return;
39926
40230
  }
39927
40231
  const buffer = Buffer.alloc(size - from);
39928
- const read = readSync(handle, buffer, 0, buffer.length, from);
40232
+ const read = readSync2(handle, buffer, 0, buffer.length, from);
39929
40233
  game.errorReportOffset = from + read;
39930
40234
  const reported = buffer.subarray(0, read);
39931
40235
  if (game.transcript !== null) {
@@ -39934,7 +40238,7 @@ class GodotServer {
39934
40238
  game.log.append("stderr", reported);
39935
40239
  }
39936
40240
  } finally {
39937
- closeSync(handle);
40241
+ closeSync2(handle);
39938
40242
  }
39939
40243
  }
39940
40244
  recordWhatItBrokeOn(game) {
@@ -40028,7 +40332,7 @@ class GodotServer {
40028
40332
  ...env ? { env } : {}
40029
40333
  });
40030
40334
  } finally {
40031
- closeSync(transcript.fd);
40335
+ closeSync2(transcript.fd);
40032
40336
  }
40033
40337
  child.unref();
40034
40338
  const log = new GameLog;
@@ -40075,7 +40379,7 @@ class GodotServer {
40075
40379
  }
40076
40380
  let handle;
40077
40381
  try {
40078
- handle = openSync2(game.transcript, "r");
40382
+ handle = openSync3(game.transcript, "r");
40079
40383
  } catch {
40080
40384
  return;
40081
40385
  }
@@ -40085,11 +40389,11 @@ class GodotServer {
40085
40389
  return;
40086
40390
  }
40087
40391
  const buffer = Buffer.alloc(size - game.readOffset);
40088
- const read = readSync(handle, buffer, 0, buffer.length, game.readOffset);
40392
+ const read = readSync2(handle, buffer, 0, buffer.length, game.readOffset);
40089
40393
  game.readOffset += read;
40090
40394
  game.log.append("transcript", buffer.subarray(0, read));
40091
40395
  } finally {
40092
- closeSync(handle);
40396
+ closeSync2(handle);
40093
40397
  }
40094
40398
  }
40095
40399
  async pickUpWhatTheEditorIsPlaying() {
@@ -40287,7 +40591,7 @@ class GodotServer {
40287
40591
  }
40288
40592
  const unreadable = this.aRuntimeOfOursIsTooFarOff();
40289
40593
  if (unreadable !== null) {
40290
- 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]);
40291
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.";
40292
40596
  }
40293
40597
  const record = readRunRecord();
@@ -40412,7 +40716,57 @@ class GodotServer {
40412
40716
  }
40413
40717
  return runIsUp(run, said);
40414
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
+ }
40415
40766
  async handleGetDebugOutput(args, waitedMs) {
40767
+ if (readString(args, "op") === "editor") {
40768
+ return this.handleEditorConsole(args);
40769
+ }
40416
40770
  await this.pickUpWhatTheEditorIsPlaying();
40417
40771
  const run = this.currentRun();
40418
40772
  if (!run) {
@@ -40863,8 +41217,8 @@ class GodotServer {
40863
41217
  ]);
40864
41218
  }
40865
41219
  const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
40866
- const screenshotDir = expectsScreenshot ? mkdtempSync3(join21(tmpdir5(), "gdharness-runtime-screenshot-")) : null;
40867
- 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;
40868
41222
  try {
40869
41223
  const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs);
40870
41224
  if (!reply.ok) {
@@ -40888,7 +41242,7 @@ class GodotServer {
40888
41242
  type: "text",
40889
41243
  text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}`
40890
41244
  },
40891
- { type: "image", data: readFileSync19(screenshotPath).toString("base64"), mimeType: "image/png" }
41245
+ { type: "image", data: readFileSync20(screenshotPath).toString("base64"), mimeType: "image/png" }
40892
41246
  ]
40893
41247
  };
40894
41248
  } finally {
@@ -40955,7 +41309,7 @@ async function runGodotServer() {
40955
41309
  const server = new GodotServer;
40956
41310
  await server.run();
40957
41311
  }
40958
- 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/";
40959
41313
  var init_server3 = __esm(() => {
40960
41314
  init_mcp();
40961
41315
  init_stdio2();
@@ -40967,6 +41321,7 @@ var init_server3 = __esm(() => {
40967
41321
  init_class_note();
40968
41322
  init_config_pin();
40969
41323
  init_dap_client();
41324
+ init_editor_log();
40970
41325
  init_errors();
40971
41326
  init_game_log();
40972
41327
  init_godot_bridge();
@@ -40991,7 +41346,7 @@ var init_server3 = __esm(() => {
40991
41346
  init_tool_definitions();
40992
41347
  init_update_check();
40993
41348
  run7 = promisify7(execFile7);
40994
- __dirname2 = dirname14(fileURLToPath4(import.meta.url));
41349
+ __dirname2 = dirname15(fileURLToPath4(import.meta.url));
40995
41350
  CONSOLE_WRAPPER = /_console(\.exe)?$/i;
40996
41351
  LONGEST_BOOT_NOTED_MS = 5 * 60000;
40997
41352
  VALUE_NAMES = {
@@ -41031,8 +41386,8 @@ var init_server3 = __esm(() => {
41031
41386
  });
41032
41387
 
41033
41388
  // src/cli.ts
41034
- import { existsSync as existsSync15 } from "fs";
41035
- 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";
41036
41391
 
41037
41392
  // src/errors.ts
41038
41393
  class Refusal extends Error {
@@ -42443,7 +42798,8 @@ function renderToolsMarkdown() {
42443
42798
  "`op`. An unknown op or argument is refused with the valid set listed. Generated from the server.",
42444
42799
  "",
42445
42800
  `${projectPathSentence()}: the \`runtime_*\` and \`debug_*\` ones pick between running games`,
42446
- "instead, and the others answer about the editor on this server. An argument a tool does not",
42801
+ "instead, and the others answer about the editor on this server. The pick, when a call names",
42802
+ `neither \`projectPath\` nor \`pid\`: ${theGamePicked()}. An argument a tool does not`,
42447
42803
  "declare is refused rather than ignored. Answers are read from the engine after the change, not",
42448
42804
  "echoed from the request. Engine stderr comes back under `engine_messages`.",
42449
42805
  "",
@@ -42503,9 +42859,8 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
42503
42859
 
42504
42860
  - ${projectPathSentence()}. The \`runtime_*\` and \`debug_*\` ones pick between running
42505
42861
  games instead, by \`projectPath\`, or by \`pid\` when several run from one project, as a bench
42506
- and its workers do: without \`pid\` the game this server started or plays is the one asked, and
42507
- the answer says so under \`answeredBy\`; the others answer about the editor this server is
42508
- connected to. There is no ambient project, and an argument a tool does not declare is refused
42862
+ and its workers do. Naming neither picks ${theGamePicked()}; the others answer about the editor
42863
+ this server is connected to. There is no ambient project, and an argument a tool does not declare is refused
42509
42864
  rather than ignored.
42510
42865
  - \`editor_status\` says whether an editor is connected and whether its addon matches the server.
42511
42866
  \`addonIsStale\` means they differ, and \`staleNote\` says which half is behind: an editor that
@@ -42529,6 +42884,14 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
42529
42884
  runtime addon announces nothing, so there is no process id and \`running\` is the editor's answer,
42530
42885
  which can lag by seconds after the game has gone. \`editor_status\` names the game under
42531
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.
42532
42895
  - One server, one editor. A second project is a second harness session with its own server, and
42533
42896
  that works: each editor is opened on its own language server and debug adapter ports, and says
42534
42897
  where it serves. Two editors on one project is the thing to refuse.
@@ -42738,7 +43101,7 @@ class UsageError extends Refusal {
42738
43101
  function projectArgument(at) {
42739
43102
  const named = args.slice(at).find((value) => !value.startsWith("--"));
42740
43103
  const projectPath = resolve5(named ?? ".");
42741
- if (!existsSync15(join22(projectPath, "project.godot"))) {
43104
+ if (!existsSync16(join23(projectPath, "project.godot"))) {
42742
43105
  throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
42743
43106
  }
42744
43107
  return projectPath;
@@ -42777,7 +43140,7 @@ async function registerRuntime(godot, projectPath) {
42777
43140
  return;
42778
43141
  }
42779
43142
  console.log(`${RUNTIME_AUTOLOAD.name} autoload left at ${named}, which this project registers itself`);
42780
- if (!existsSync15(join22(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
43143
+ if (!existsSync16(join23(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
42781
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}`);
42782
43145
  }
42783
43146
  }
@@ -42909,7 +43272,7 @@ async function setup() {
42909
43272
  console.log("no harness configured; name one yourself, and gdharness harnesses lists them all");
42910
43273
  }
42911
43274
  if (!args.includes("--no-skill")) {
42912
- const directories = skillDirectories(chosen.harnesses, projectPath, existsSync15);
43275
+ const directories = skillDirectories(chosen.harnesses, projectPath, existsSync16);
42913
43276
  for (const written of writeSkill(directories, getLocalVersion())) {
42914
43277
  console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
42915
43278
  }
@@ -43002,7 +43365,7 @@ async function upgrade() {
43002
43365
  moved.push(took.from);
43003
43366
  }
43004
43367
  }
43005
- for (const written of writeSkill(skillDirectories(already, projectPath, existsSync15), version)) {
43368
+ for (const written of writeSkill(skillDirectories(already, projectPath, existsSync16), version)) {
43006
43369
  console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
43007
43370
  }
43008
43371
  console.log(`