gdharness 1.2.24 → 1.2.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/cli.js CHANGED
@@ -70,6 +70,7 @@ var init_errors = __esm(() => {
70
70
 
71
71
  // src/windows-ask.ts
72
72
  import { spawn } from "node:child_process";
73
+ import { tmpdir } from "node:os";
73
74
  import process2 from "node:process";
74
75
  function holding(child, wanted) {
75
76
  if (held === wanted) {
@@ -94,7 +95,7 @@ function running() {
94
95
  "Bypass",
95
96
  "-EncodedCommand",
96
97
  Buffer.from(SCRIPT, "utf16le").toString("base64")
97
- ], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
98
+ ], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true, cwd: tmpdir() });
98
99
  buffered = "";
99
100
  child.stdout.setEncoding("utf8");
100
101
  child.stdout.on("data", (chunk) => {
@@ -8220,13 +8221,13 @@ var init_alive = () => {};
8220
8221
 
8221
8222
  // src/scratch.ts
8222
8223
  import { existsSync as existsSync6, mkdtempSync, readdirSync as readdirSync3, rmSync as rmSync4 } from "node:fs";
8223
- import { tmpdir } from "node:os";
8224
+ import { tmpdir as tmpdir2 } from "node:os";
8224
8225
  import { join as join6 } from "node:path";
8225
8226
  import process6 from "node:process";
8226
8227
  function scratchDirectory(kind) {
8227
- return mkdtempSync(join6(tmpdir(), `gdharness-${kind}-${process6.pid}-`));
8228
+ return mkdtempSync(join6(tmpdir2(), `gdharness-${kind}-${process6.pid}-`));
8228
8229
  }
8229
- function sweepAbandonedScratch(directory = tmpdir()) {
8230
+ function sweepAbandonedScratch(directory = tmpdir2()) {
8230
8231
  let entries;
8231
8232
  try {
8232
8233
  entries = readdirSync3(directory);
@@ -8336,14 +8337,20 @@ function snakeCased(params) {
8336
8337
  return result;
8337
8338
  }
8338
8339
  function engineExtras(outcome) {
8340
+ const after = outcome.afterAnswerMessages ?? [];
8339
8341
  return {
8340
8342
  ...outcome.messages.length > 0 ? { engine_messages: outcome.messages } : {},
8341
8343
  ...outcome.messagesOmitted === undefined ? {} : { engine_messages_omitted: outcome.messagesOmitted },
8344
+ ...after.length > 0 ? { engine_messages_after_answer: after, engine_messages_after_answer_note: AFTER_ANSWER_NOTE } : {},
8345
+ ...outcome.afterAnswerOmitted === undefined ? {} : { engine_messages_after_answer_omitted: outcome.afterAnswerOmitted },
8342
8346
  ...outcome.afterAnswer === undefined ? {} : { engine_after_answer: outcome.afterAnswer }
8343
8347
  };
8344
8348
  }
8345
8349
  function problemsIn(log) {
8346
- return log.everything().flatMap((entry) => {
8350
+ return problemsAmong(log.everything());
8351
+ }
8352
+ function problemsAmong(entries) {
8353
+ return entries.flatMap((entry) => {
8347
8354
  if (entry.severity !== "info") {
8348
8355
  return [entry];
8349
8356
  }
@@ -8472,12 +8479,20 @@ async function runOperation(engine, operation, params, projectPath) {
8472
8479
  } finally {
8473
8480
  discard(paramsDir);
8474
8481
  }
8475
- const problems = problemsIn(ran.log);
8482
+ const everything = ran.log.everything();
8483
+ const answered = everything.findIndex((entry) => entry.source === "stderr" && entry.text === ANSWERED);
8484
+ const problems = problemsAmong(answered === -1 ? everything : everything.slice(0, answered));
8485
+ const after = answered === -1 ? [] : problemsAmong(everything.slice(answered + 1));
8486
+ const afterAnswered = {
8487
+ ...after.length === 0 ? {} : { afterAnswerMessages: after.slice(-MESSAGES_KEPT) },
8488
+ ...after.length > MESSAGES_KEPT ? { afterAnswerOmitted: after.length - MESSAGES_KEPT } : {}
8489
+ };
8476
8490
  if (answer.kind === "unreadable") {
8477
8491
  return {
8478
8492
  ok: false,
8479
8493
  message: `${operation} wrote an answer that could not be read (${answer.why})`,
8480
- ...reported2(problems)
8494
+ ...reported2(problems),
8495
+ ...afterAnswered
8481
8496
  };
8482
8497
  }
8483
8498
  if (answer.kind === "none") {
@@ -8493,14 +8508,16 @@ async function runOperation(engine, operation, params, projectPath) {
8493
8508
  return {
8494
8509
  ok: false,
8495
8510
  message: `${operation} hit an error in the operations script, so its answer is left out: part of it was ` + `never computed. ${fault.text} (${fault.detail[0]?.slice("at: ".length) ?? "no location given"})`,
8496
- ...reported2(problems)
8511
+ ...reported2(problems),
8512
+ ...afterAnswered
8497
8513
  };
8498
8514
  }
8499
- const afterAnswer = ran.exitCode === 0 && ran.failure === null ? undefined : `The operation finished and wrote this answer, and then the engine did not exit cleanly (${howItEnded(ran)}). Anything it reported on the way out is under engine_messages.`;
8515
+ const afterAnswer = ran.exitCode === 0 && ran.failure === null ? undefined : `The operation finished and wrote this answer, and then the engine did not exit cleanly (${howItEnded(ran)}). Anything it reported on the way out is under ${answered === -1 ? "engine_messages" : "engine_messages_after_answer"}.`;
8500
8516
  return {
8501
8517
  ok: true,
8502
8518
  payload: answer.payload,
8503
8519
  ...reported2(problems),
8520
+ ...afterAnswered,
8504
8521
  ...afterAnswer === undefined ? {} : { afterAnswer }
8505
8522
  };
8506
8523
  }
@@ -8519,7 +8536,7 @@ function answerIn(file) {
8519
8536
  };
8520
8537
  }
8521
8538
  }
8522
- var OWN_ERROR = "[ERROR] ", MESSAGES_KEPT = 200, ERRORS_NAMED = 3;
8539
+ var AFTER_ANSWER_NOTE = "Printed after this operation had written its answer, while the engine ran the project's autoloads and shut down: Godot starts a project's autoloads in every script run and cannot be told not to. An error raised in a project script here is that autoload's own, and none of these changes the answer.", OWN_ERROR = "[ERROR] ", ANSWERED = "[gdharness] answered", MESSAGES_KEPT = 200, ERRORS_NAMED = 3;
8523
8540
  var init_headless = __esm(() => {
8524
8541
  init_engine_run();
8525
8542
  init_reimport();
@@ -8528,7 +8545,7 @@ var init_headless = __esm(() => {
8528
8545
 
8529
8546
  // src/update-check.ts
8530
8547
  import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
8531
- import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
8548
+ import { homedir as homedir3, tmpdir as tmpdir3 } from "node:os";
8532
8549
  import { join as join8 } from "node:path";
8533
8550
  function packageUrl(root) {
8534
8551
  return `${root.replace(/\/+$/, "")}/gdharness/latest`;
@@ -8564,7 +8581,7 @@ function cacheFile(environment = process.env) {
8564
8581
  mkdirSync3(directory, { recursive: true, mode: 448 });
8565
8582
  return join8(directory, "update-check.json");
8566
8583
  } catch {
8567
- return join8(tmpdir2(), "gdharness-update-check.json");
8584
+ return join8(tmpdir3(), "gdharness-update-check.json");
8568
8585
  }
8569
8586
  }
8570
8587
  function readCache(path) {
@@ -11810,7 +11827,7 @@ var init_tool_definitions = __esm(() => {
11810
11827
  {
11811
11828
  name: "editor_launch",
11812
11829
  title: "Open, restart or close the editor",
11813
- description: "Opens the Godot editor on a project, or restarts or closes the one already connected. close saves open scenes, quits the editor and waits for its process to go, which is the only way to end a hidden one short of killing it; settingsDropped in its answer says what that save took out of project.godot. open refuses while an editor of the project is already running and has not reached this server, naming its pid, rather than putting a second on the project. The editor is an ordinary visible one on the desktop in use, and on Windows it is shown without being activated, so opening or reopening it does not take the keyboard from whatever the user is typing in. hidden: true opens it headless instead and, on Windows, on a desktop of its own, where the games it plays go too; a hidden editor serves the bridge, the language server and the debug adapter as a visible one does. A game a visible editor plays opens as a window on the desktop in use. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect, and nothing else here restarts one; it saves open scenes on the way out and answers with the version that came back. An editor a server opened is started again by the server, visible unless hidden: true is passed again, once the old one has gone: it waits 90 seconds for that, or GDHARNESS_EDITOR_RESTART_TIMEOUT_MS, and an old editor still running after it is not replaced, since the new one would find its ports held. One opened by hand restarts itself, which only one with a window can do, because the engine hands back none of the arguments it was started with. editor_status says which editor is connected and whether it is holding an old addon. Opening a project is a save and so is closing one, which is the part that is not obvious from outside: the editor imports the project on open and writes project.godot back, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped either way. A restart says which under settingsDropped in its own answer because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one. A restart of an editor this server opened is a quit and then a launch, and this server can be ended between them, usually by the reconnect the restart was being taken for: the editor is gone and nothing replaces it. That is written down before the quit, so the next server says so under restartInterrupted in editor_status rather than waiting for an editor that is not coming, and open finishes it, saying so under finishesRestart. An open is refused when the editor it would start could never reach this server: a project other than the one this server serves, whose editor dials that project's own server; a project without the gdharness editor addon installed and enabled; and a project whose editor a restart is already bringing back.",
11830
+ description: "Opens the Godot editor on a project, or restarts or closes the one already connected. close saves open scenes, quits the editor and waits for its process to go, which is the only way to end a hidden one short of killing it; settingsDropped in its answer says what that save took out of project.godot. An editor this server opened that has not connected yet has nothing of this server's to save, so close stops its process instead and answers beforeConnecting. open answers once the process has started, and the editor dials in after its import; a call needing the editor made before then waits for the one this server opened, for as long as a restart waits, and is refused naming it if it has not arrived. open refuses while an editor of the project is already running and has not reached this server, naming its pid, rather than putting a second on the project. The editor is an ordinary visible one on the desktop in use, and on Windows it is shown without being activated, so opening or reopening it does not take the keyboard from whatever the user is typing in. hidden: true opens it headless instead and, on Windows, on a desktop of its own, where the games it plays go too; a hidden editor serves the bridge, the language server and the debug adapter as a visible one does. A game a visible editor plays opens as a window on the desktop in use. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect, and nothing else here restarts one; it saves open scenes on the way out and answers with the version that came back. An editor a server opened is started again by the server, visible unless hidden: true is passed again, once the old one has gone: it waits 90 seconds for that, or GDHARNESS_EDITOR_RESTART_TIMEOUT_MS, and an old editor still running after it is not replaced, since the new one would find its ports held. One opened by hand restarts itself, which only one with a window can do, because the engine hands back none of the arguments it was started with. editor_status says which editor is connected and whether it is holding an old addon. Opening a project is a save and so is closing one, which is the part that is not obvious from outside: the editor imports the project on open and writes project.godot back, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped either way. A restart says which under settingsDropped in its own answer because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one. A restart of an editor this server opened is a quit and then a launch, and this server can be ended between them, usually by the reconnect the restart was being taken for: the editor is gone and nothing replaces it. That is written down before the quit, so the next server says so under restartInterrupted in editor_status rather than waiting for an editor that is not coming, and open finishes it, saying so under finishesRestart. An open is refused when the editor it would start could never reach this server: a project other than the one this server serves, whose editor dials that project's own server; a project without the gdharness editor addon installed and enabled; and a project whose editor a restart is already bringing back.",
11814
11831
  parameters: {
11815
11832
  projectPath: { ...PROJECT_PATH, ops: ["open"] },
11816
11833
  hidden: {
@@ -11965,6 +11982,10 @@ var init_tool_definitions = __esm(() => {
11965
11982
  title: "Game and editor output",
11966
11983
  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, with errorsUnread true and no clean, since what it raised reaches nothing here. Answers with the counts and the verdict as well as the entries: clean is true when no error was reported, and warnings, counted beside it, do not make it false. 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 ended by a signal has no exit code, and exitSignal names the signal instead. 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. startedAt is when the run began and elapsedMs how long it has been going; for a run that is over, endedAt is when it ended and elapsedMs how long it ran, and both are left out when nobody saw the end, as for a game closed in the editor or a run picked up from a note written before ends were recorded.",
11967
11984
  parameters: {
11985
+ projectPath: {
11986
+ type: "string",
11987
+ description: "The project whose output you mean to read. It picks nothing, since this reads the one run or editor this server holds, and a call naming another project than that one's is refused rather than answered with output from a different game."
11988
+ },
11968
11989
  severity: {
11969
11990
  type: "string",
11970
11991
  enum: ["error", "warning", "info"],
@@ -12016,7 +12037,7 @@ var init_tool_definitions = __esm(() => {
12016
12037
  title: "Editor status",
12017
12038
  effect: "read",
12018
12039
  description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and which games with the runtime addon are reachable, of this project and of every other on the machine. Read connected with mayYetConnect: the editor dials this server and backs off between tries, so for the first half-minute after a server starts false means not reached yet as often as no editor; mayYetConnect true is worth waiting out, false is final, and restartInterrupted, restartUnderway, closedHere and editorsNotYetConnected say why. An editor that has connected and not yet greeted reports nothing about itself, and every other call made in that window waits up to ten seconds for it to settle. playingInEditor null means unanswered rather than nothing playing: processActive and runtimes do not depend on the editor answering. game.run names the run editor_output and editor_run stop answer for, and stays named after it ends. addonIsStale, on the editor and on each game, says it runs addon code other than what this server ships, and staleNote says which half moves: gdharness upgrade in the project, a restart, or a reconnect of this server. A false editor.lspListening or editor.dapListening comes with serversNote, and editor_launch restart binds the port again. editor.scan says whether the editor is scanning, which editor_run waits out before starting a game. A game announced in a protocol this server does not speak is listed under unreadable, during an upgrade that moves the protocol, until the halves match.",
12019
- answer: "runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it. It stays named after the run ends, beside processActive false, so editor_output can still read what that run printed; null when there is no run at all. startedAt says when it began, and endedAt when it ended, wherever something saw the end: a run adopted from a note another server wrote can have ended hours before this server started. editor.lspListening and editor.dapListening say whether anything accepts connections on the ports the editor names, lspPort and dapPort: Godot binds each once, as the editor starts, and reports a failure only in the log panel of the editor itself, so a false comes with serversNote, and editor_launch restart binds it again. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and while that is read identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window.",
12040
+ answer: "server names this server: its pid, its version, and lifecycleLog, the file every gdharness server of the user appends a line to when it starts, when its client first names itself, when it replaces another server of its project, and when it ends, with why it ended (the client closing its end as stdin:end, a signal by name, superseded with the successor's pid, an uncaught exception or rejection with its stack, or the exit code) and the calls it was answering then. The parent's command line follows on its own line a few seconds after the start. A server killed without warning writes no ended line, and on Windows every kill is that, so a started line with no ended line after it is a server something killed. " + "runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it. It stays named after the run ends, beside processActive false, so editor_output can still read what that run printed; null when there is no run at all. startedAt says when it began, and endedAt when it ended, wherever something saw the end: a run adopted from a note another server wrote can have ended hours before this server started. editor.lspListening and editor.dapListening say whether anything accepts connections on the ports the editor names, lspPort and dapPort: Godot binds each once, as the editor starts, and reports a failure only in the log panel of the editor itself, so a false comes with serversNote, and editor_launch restart binds it again. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and while that is read identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window.",
12020
12041
  parameters: {},
12021
12042
  requires: []
12022
12043
  },
@@ -12219,7 +12240,7 @@ var init_tool_definitions = __esm(() => {
12219
12240
  value: {
12220
12241
  ops: ["set"],
12221
12242
  blank: true,
12222
- description: `set: the value, fitted to the property's type. "" writes an empty string. A property typed as or holding an object takes a path naming one the game holds, the way call's args do, and holds that instance, or null, which the text null also means, to empty it. One declared as Variant or untyped takes anything, as the engine does: a value of the kind it holds is fitted to it, an int staying an int, and any other is stored as sent. A typed list or map, such as Array[int] or Dictionary[String, int], takes a JSON list or object with each element fitted to its element type, and an element that cannot become one is refused by its index or key, as is an object of another script than the one the list holds, whether or not that script has a class_name.`
12243
+ description: `set: the value, fitted to the property's type. A vector, colour or the like may be written as its fields, {"x": 140, "y": 300}, or tagged, {"_type": "Vector2", "x": 140, "y": 300}, which is the form a read answers with. "" writes an empty string. A property typed as or holding an object takes a path naming one the game holds, the way call's args do, and holds that instance, or null, which the text null also means, to empty it. One declared as Variant or untyped takes anything, as the engine does: a value of the kind it holds is fitted to it, an int staying an int, and any other is stored as sent. A typed list or map, such as Array[int] or Dictionary[String, int], takes a JSON list or object with each element fitted to its element type, and an element that cannot become one is refused by its index or key, as is an object of another script than the one the list holds, whether or not that script has a class_name.`
12223
12244
  },
12224
12245
  method: {
12225
12246
  type: "string",
@@ -12229,7 +12250,7 @@ var init_tool_definitions = __esm(() => {
12229
12250
  args: {
12230
12251
  type: "array",
12231
12252
  ops: ["call"],
12232
- description: `call: the arguments, fitted to the method's parameter types. A parameter typed as an object takes a path naming one the game holds and is handed that instance: a colon path read from nodePath, "_game:run:wares:3", or one starting at a node, "/root/Main/Hud" or "/root/Main:_game:run". A path that reaches no object, or an object of another class than the parameter declares, is refused, and so are too few or too many arguments. The engine names a parameter typed as a script class without class_name by the class the script extends, so an object of that class with another script passes that check and the engine refuses the call: the answer says the method did not run, with the engine's reason. A parameter typed as a list or map, such as Array[int] or Array[Gear], is built from a JSON list or object element by element, objects named by their paths. The method itself takes its arguments here only: brackets on it holding arguments are refused.`
12253
+ description: `call: the arguments, fitted to the method's parameter types. A vector, colour or the like may be written as its fields, {"x": 140, "y": 300}, or tagged, {"_type": "Vector2", "x": 140, "y": 300}. A parameter typed as an object takes a path naming one the game holds and is handed that instance: a colon path read from nodePath, "_game:run:wares:3", or one starting at a node, "/root/Main/Hud" or "/root/Main:_game:run". A path that reaches no object, or an object of another class than the parameter declares, is refused, and so are too few or too many arguments. The engine names a parameter typed as a script class without class_name by the class the script extends, so an object of that class with another script passes that check and the engine refuses the call: the answer says the method did not run, with the engine's reason. A parameter typed as a list or map, such as Array[int] or Array[Gear], is built from a JSON list or object element by element, objects named by their paths. The method itself takes its arguments here only: brackets on it holding arguments are refused.`
12233
12254
  },
12234
12255
  properties: {
12235
12256
  type: ["boolean", "array"],
@@ -12323,7 +12344,7 @@ var init_tool_definitions = __esm(() => {
12323
12344
  name: "runtime_input",
12324
12345
  title: "Send input to the game",
12325
12346
  description: "Input to the running game: a whole click on a Control or a 3D node named by path or on a control named by the words on it, an item chosen out of a menu, typing into whatever has the focus, or a raw action, key, mouse button or mouse motion. All of it works headless, where the window is 64 by 64 and the GUI only takes what is inside it. On gdharness's own desktop, where a windowed run goes, Windows refuses to say where the pointer is: every pointer answer there carries pointer_on_hidden_desktop, and the first of the run pointer_note saying why it matters: get_mouse_position() on the root window, and the mouse positions derived from it, read a meaningless position, while what reads the event itself (the control under it, hover, tooltips, input handlers) gets the injected position.",
12326
- answer: 'click: what was under the pointer, and what became of the control: in_tree, removed or freed. Named by says, found says which match it was, and found.pressedOn says "the words" for words in a RichTextLabel pressed where they are drawn, or "the middle of the label" with a note saying why they could not be placed; on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control scrolled into view first is scrolled each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A button in toggle mode, a CheckBox or CheckButton included, answers pressed with before and after, its button_pressed either side of the click: the same value twice is a click the game undid, and after is null when the click freed it. A refusal for a pointer turned off names where mouse_behavior_recursive was set. On a 3D node landed says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room. choose: what was chosen, read before the press, which is heard rather than assumed; what became of the control named (in_tree, removed or freed, since a game can rebuild its screen on the pick); and what the button in front of the menu shows now when it is still there. A menu is opened rather than clicked because its items are drawn rather than built, and the item is looked for as the menu stands once open, since a game can fill a menu as it opens.',
12347
+ answer: 'click: what was under the pointer, and what became of the control: in_tree, removed or freed. Named by says, found says which match it was, and found.pressedOn says "the words" for words in a RichTextLabel pressed where they are drawn, or "the middle of the label" with a note saying why they could not be placed; on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control scrolled into view first is scrolled each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A button in toggle mode, a CheckBox or CheckButton included, answers pressed with before and after, its button_pressed either side of the click: the same value twice is a click the game undid, and after is null when the click freed it. A refusal for a pointer turned off names where mouse_behavior_recursive was set. On a 3D node landed says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room. choose: what was chosen, read before the press, which is heard rather than assumed; what became of the control named (in_tree, removed or freed, since a game can rebuild its screen on the pick); and what the button in front of the menu shows now when it is still there. A menu is opened rather than clicked because its items are drawn rather than built, and the item is looked for as the menu stands once open, since a game can fill a menu as it opens. action, key and mouse_click: the event as it was sent, with whole saying whether the press was let go of again, and for a key the keycode, physical_keycode and unicode it carried. mouse_motion: the position and the relative movement sent. None of these is aimed at anything, so none says what the event reached: click on a nodePath or says answers what was under the pointer.',
12327
12348
  parameters: {
12328
12349
  projectPath: RUNNING_PROJECT_PATH,
12329
12350
  pid: RUNNING_PID,
@@ -34333,7 +34354,7 @@ var init_stdio = __esm(() => {
34333
34354
  // src/runtime-client.ts
34334
34355
  import { existsSync as existsSync13, readdirSync as readdirSync8, readFileSync as readFileSync12, statSync as statSync6, unlinkSync } from "node:fs";
34335
34356
  import { createConnection } from "node:net";
34336
- import { tmpdir as tmpdir3 } from "node:os";
34357
+ import { tmpdir as tmpdir4 } from "node:os";
34337
34358
  import { dirname as dirname8, join as join14, resolve as resolve4 } from "node:path";
34338
34359
  import { setTimeout as delay } from "node:timers/promises";
34339
34360
  function runtimeDirectory(variables = process.env) {
@@ -34342,7 +34363,7 @@ function runtimeDirectory(variables = process.env) {
34342
34363
  return explicit;
34343
34364
  }
34344
34365
  const perUser = envValue("XDG_RUNTIME_DIR", variables);
34345
- return join14(perUser ?? tmpdir3(), "gdharness");
34366
+ return join14(perUser ?? tmpdir4(), "gdharness");
34346
34367
  }
34347
34368
  function runtimeDirectories(variables = process.env) {
34348
34369
  const candidates = [runtimeDirectory(variables)];
@@ -40126,11 +40147,58 @@ function askTheKeeperToStop(address, withinMs) {
40126
40147
  }
40127
40148
  var init_keeper_channel = () => {};
40128
40149
 
40150
+ // src/lifecycle-log.ts
40151
+ import { appendFileSync, mkdirSync as mkdirSync11, renameSync, statSync as statSync8 } from "node:fs";
40152
+ import { dirname as dirname14, join as join21 } from "node:path";
40153
+ import process10 from "node:process";
40154
+ function lifecycleLogPath() {
40155
+ return join21(runtimeDirectory(), "servers.log");
40156
+ }
40157
+ function writeLifecycle(event, fields = {}) {
40158
+ const path = lifecycleLogPath();
40159
+ try {
40160
+ mkdirSync11(dirname14(path), { recursive: true });
40161
+ if ((statSync8(path, { throwIfNoEntry: false })?.size ?? 0) > LIFECYCLE_LOG_LIMIT) {
40162
+ try {
40163
+ renameSync(path, `${path}.1`);
40164
+ } catch {}
40165
+ }
40166
+ const line = {
40167
+ at: new Date().toISOString(),
40168
+ pid: process10.pid,
40169
+ ppid: process10.ppid,
40170
+ version: SERVER_VERSION,
40171
+ event
40172
+ };
40173
+ for (const [name, value] of Object.entries(fields)) {
40174
+ if (!Object.hasOwn(line, name)) {
40175
+ line[name] = value;
40176
+ }
40177
+ }
40178
+ appendFileSync(path, `${JSON.stringify(line)}
40179
+ `, "utf8");
40180
+ } catch {
40181
+ console.error(`[SERVER] Could not write the lifecycle log at ${path}.`);
40182
+ }
40183
+ }
40184
+ function callsInFlight(calls, now = Date.now()) {
40185
+ return [...calls].map((call) => ({
40186
+ tool: call.tool,
40187
+ ...call.op === "" ? {} : { op: call.op },
40188
+ runningMs: now - call.startedAt
40189
+ }));
40190
+ }
40191
+ var LIFECYCLE_LOG_LIMIT = 1e6;
40192
+ var init_lifecycle_log = __esm(() => {
40193
+ init_runtime_client();
40194
+ init_server_version();
40195
+ });
40196
+
40129
40197
  // src/lsp_client.ts
40130
40198
  import { existsSync as existsSync17, realpathSync as realpathSync4 } from "node:fs";
40131
40199
  import { readFile, realpath } from "node:fs/promises";
40132
40200
  import { createConnection as createConnection3 } from "node:net";
40133
- import { dirname as dirname14, join as join21, resolve as resolve5 } from "node:path";
40201
+ import { dirname as dirname15, join as join22, resolve as resolve5 } from "node:path";
40134
40202
  import { setTimeout as delay3 } from "node:timers/promises";
40135
40203
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
40136
40204
  function initializeTimeoutMs() {
@@ -40292,7 +40360,7 @@ class GodotLSPClient {
40292
40360
  if (this.initialized) {
40293
40361
  return;
40294
40362
  }
40295
- const rootPath = this.rootPath ?? dirname14(resolve5(filePath));
40363
+ const rootPath = this.rootPath ?? dirname15(resolve5(filePath));
40296
40364
  await this.initialize(rootPath);
40297
40365
  }
40298
40366
  async sendRequest(method, params, timeoutMs = this.timeouts.requestMs) {
@@ -40793,7 +40861,7 @@ async function resolveLSPPaths(projectPathValue, scriptPathValue) {
40793
40861
  } catch {
40794
40862
  throw new ArgumentRefusal(`Project path does not exist: ${requestedProjectPath}`);
40795
40863
  }
40796
- if (!existsSync17(join21(projectPath, "project.godot"))) {
40864
+ if (!existsSync17(join22(projectPath, "project.godot"))) {
40797
40865
  throw new ArgumentRefusal(`Not a Godot project: ${projectPath}. Point projectPath at the directory holding project.godot.`);
40798
40866
  }
40799
40867
  const contained = resolveWithinProject(projectPath, scriptPathValue);
@@ -41233,8 +41301,8 @@ var init_prompts = __esm(() => {
41233
41301
  });
41234
41302
 
41235
41303
  // src/rename.ts
41236
- import { existsSync as existsSync18, mkdirSync as mkdirSync11, readFileSync as readFileSync19, renameSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync13 } from "node:fs";
41237
- import { dirname as dirname15, posix as posix2 } from "node:path";
41304
+ import { existsSync as existsSync18, mkdirSync as mkdirSync12, readFileSync as readFileSync19, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync13 } from "node:fs";
41305
+ import { dirname as dirname16, posix as posix2 } from "node:path";
41238
41306
 
41239
41307
  class Script {
41240
41308
  path;
@@ -41946,15 +42014,15 @@ function applyRewrites(rewrites) {
41946
42014
  const done = [];
41947
42015
  try {
41948
42016
  for (const rewrite of rewrites) {
41949
- mkdirSync11(dirname15(rewrite.writeTo), { recursive: true });
42017
+ mkdirSync12(dirname16(rewrite.writeTo), { recursive: true });
41950
42018
  const staging = `${rewrite.writeTo}.gdharness-rename`;
41951
42019
  writeFileSync13(staging, rewrite.after, "utf8");
41952
- renameSync(staging, rewrite.writeTo);
42020
+ renameSync2(staging, rewrite.writeTo);
41953
42021
  let movedUid = false;
41954
42022
  if (rewrite.writeTo !== rewrite.absolute) {
41955
42023
  unlinkSync2(rewrite.absolute);
41956
42024
  if (existsSync18(`${rewrite.absolute}.uid`)) {
41957
- renameSync(`${rewrite.absolute}.uid`, `${rewrite.writeTo}.uid`);
42025
+ renameSync2(`${rewrite.absolute}.uid`, `${rewrite.writeTo}.uid`);
41958
42026
  movedUid = true;
41959
42027
  }
41960
42028
  }
@@ -41967,7 +42035,7 @@ function applyRewrites(rewrites) {
41967
42035
  if (rewrite.writeTo !== rewrite.absolute) {
41968
42036
  unlinkSync2(rewrite.writeTo);
41969
42037
  if (movedUid) {
41970
- renameSync(`${rewrite.writeTo}.uid`, `${rewrite.absolute}.uid`);
42038
+ renameSync2(`${rewrite.writeTo}.uid`, `${rewrite.absolute}.uid`);
41971
42039
  }
41972
42040
  }
41973
42041
  } catch {}
@@ -41999,8 +42067,8 @@ var init_rename = __esm(() => {
41999
42067
  });
42000
42068
 
42001
42069
  // src/rename-symbol.ts
42002
- import { existsSync as existsSync19, mkdirSync as mkdirSync12, readdirSync as readdirSync9, realpathSync as realpathSync5, rmSync as rmSync10, writeFileSync as writeFileSync14 } from "node:fs";
42003
- import { basename as basename5, dirname as dirname16, join as join22, relative as relative4, resolve as resolve6 } from "node:path";
42070
+ import { existsSync as existsSync19, mkdirSync as mkdirSync13, readdirSync as readdirSync9, realpathSync as realpathSync5, rmSync as rmSync10, writeFileSync as writeFileSync14 } from "node:fs";
42071
+ import { basename as basename5, dirname as dirname17, join as join23, relative as relative4, resolve as resolve6 } from "node:path";
42004
42072
  function resourcePathOf(projectPath, absolute) {
42005
42073
  const inside = relative4(onDiskSpelling(projectPath), onDiskSpelling(absolute));
42006
42074
  if (inside === "" || inside.startsWith("..") || /^[A-Za-z]:/.test(inside)) {
@@ -42013,9 +42081,9 @@ function onDiskSpelling(path) {
42013
42081
  let at = resolve6(path);
42014
42082
  for (;; ) {
42015
42083
  try {
42016
- return join22(realpathSync5.native(at), ...missing.reverse());
42084
+ return join23(realpathSync5.native(at), ...missing.reverse());
42017
42085
  } catch {
42018
- const parent = dirname16(at);
42086
+ const parent = dirname17(at);
42019
42087
  if (parent === at) {
42020
42088
  return resolve6(path);
42021
42089
  }
@@ -42441,15 +42509,15 @@ ${mention.kind}`;
42441
42509
  };
42442
42510
  }
42443
42511
  function writeLeftAloneDetail(projectPath, symbol, detail) {
42444
- const directory = join22(projectPath, ".godot", "gdharness-renames");
42445
- mkdirSync12(directory, { recursive: true });
42512
+ const directory = join23(projectPath, ".godot", "gdharness-renames");
42513
+ mkdirSync13(directory, { recursive: true });
42446
42514
  detailWrites += 1;
42447
- const path = join22(directory, `${String(Date.now())}-${String(detailWrites).padStart(6, "0")}-${String(process.pid)}-${symbol}.json`);
42515
+ const path = join23(directory, `${String(Date.now())}-${String(detailWrites).padStart(6, "0")}-${String(process.pid)}-${symbol}.json`);
42448
42516
  writeFileSync14(path, `${JSON.stringify(detail, null, 2)}
42449
42517
  `, "utf8");
42450
42518
  const kept = readdirSync9(directory).filter((name) => name.endsWith(".json")).sort().reverse();
42451
42519
  for (const old of kept.slice(10)) {
42452
- rmSync10(join22(directory, old), { force: true });
42520
+ rmSync10(join23(directory, old), { force: true });
42453
42521
  }
42454
42522
  return path;
42455
42523
  }
@@ -42489,15 +42557,15 @@ var init_rename_symbol = __esm(() => {
42489
42557
  });
42490
42558
 
42491
42559
  // src/restart-note.ts
42492
- import { mkdirSync as mkdirSync13, readFileSync as readFileSync20, rmSync as rmSync11, writeFileSync as writeFileSync15 } from "node:fs";
42493
- import { dirname as dirname17, join as join23 } from "node:path";
42560
+ import { mkdirSync as mkdirSync14, readFileSync as readFileSync20, rmSync as rmSync11, writeFileSync as writeFileSync15 } from "node:fs";
42561
+ import { dirname as dirname18, join as join24 } from "node:path";
42494
42562
  function restartNotePath(projectPath) {
42495
- return join23(projectPath, ".godot", "gdharness-restart.json");
42563
+ return join24(projectPath, ".godot", "gdharness-restart.json");
42496
42564
  }
42497
42565
  function noteRestartBegun(note) {
42498
42566
  const path = restartNotePath(note.projectPath);
42499
42567
  try {
42500
- mkdirSync13(dirname17(path), { recursive: true });
42568
+ mkdirSync14(dirname18(path), { recursive: true });
42501
42569
  writeFileSync15(path, `${JSON.stringify(note, null, 2)}
42502
42570
  `, "utf8");
42503
42571
  } catch (error) {
@@ -42531,7 +42599,7 @@ var init_restart_note = __esm(() => {
42531
42599
  });
42532
42600
 
42533
42601
  // src/whole-file.ts
42534
- import { renameSync as renameSync2, rmSync as rmSync12, writeFileSync as writeFileSync16 } from "node:fs";
42602
+ import { renameSync as renameSync3, rmSync as rmSync12, writeFileSync as writeFileSync16 } from "node:fs";
42535
42603
  function writeWhole(path, text) {
42536
42604
  const partial = `${path}.${process.pid}.partial`;
42537
42605
  try {
@@ -42542,7 +42610,7 @@ function writeWhole(path, text) {
42542
42610
  const until = Date.now() + HELD_FILE_MS;
42543
42611
  for (;; ) {
42544
42612
  try {
42545
- renameSync2(partial, path);
42613
+ renameSync3(partial, path);
42546
42614
  return true;
42547
42615
  } catch (error) {
42548
42616
  const code = error.code ?? "";
@@ -42564,20 +42632,20 @@ var init_whole_file = __esm(() => {
42564
42632
  // src/run-record.ts
42565
42633
  import { execFile as execFile4 } from "node:child_process";
42566
42634
  import { createHash as createHash4 } from "node:crypto";
42567
- import { mkdirSync as mkdirSync14, openSync as openSync2, readdirSync as readdirSync10, readFileSync as readFileSync21, realpathSync as realpathSync6, rmSync as rmSync13, statSync as statSync8 } from "node:fs";
42568
- import { basename as basename6, dirname as dirname18, join as join24, resolve as resolve7 } from "node:path";
42635
+ import { mkdirSync as mkdirSync15, openSync as openSync2, readdirSync as readdirSync10, readFileSync as readFileSync21, realpathSync as realpathSync6, rmSync as rmSync13, statSync as statSync9 } from "node:fs";
42636
+ import { basename as basename6, dirname as dirname19, join as join25, resolve as resolve7 } from "node:path";
42569
42637
  import { promisify as promisify4 } from "node:util";
42570
42638
  function runsDirectory() {
42571
- return join24(runtimeDirectory(), "runs");
42639
+ return join25(runtimeDirectory(), "runs");
42572
42640
  }
42573
42641
  function realAsFarAsItExists(path) {
42574
42642
  const missing = [];
42575
42643
  let at = path;
42576
42644
  for (;; ) {
42577
42645
  try {
42578
- return join24(realpathSync6.native(at), ...missing);
42646
+ return join25(realpathSync6.native(at), ...missing);
42579
42647
  } catch {
42580
- const parent = dirname18(at);
42648
+ const parent = dirname19(at);
42581
42649
  if (parent === at) {
42582
42650
  return path;
42583
42651
  }
@@ -42592,14 +42660,14 @@ function projectKey(projectPath) {
42592
42660
  return createHash4("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
42593
42661
  }
42594
42662
  function runRecordPath(projectPath, directory = runtimeDirectory()) {
42595
- return join24(directory, "runs", `note-${projectKey(projectPath)}.json`);
42663
+ return join25(directory, "runs", `note-${projectKey(projectPath)}.json`);
42596
42664
  }
42597
42665
  function latestRunPath(servedBy, directory = runtimeDirectory()) {
42598
- return join24(directory, "runs", `latest-${projectKey(servedBy)}.json`);
42666
+ return join25(directory, "runs", `latest-${projectKey(servedBy)}.json`);
42599
42667
  }
42600
42668
  function writeLatestRun(servedBy, latest) {
42601
42669
  try {
42602
- mkdirSync14(runsDirectory(), { recursive: true });
42670
+ mkdirSync15(runsDirectory(), { recursive: true });
42603
42671
  writeWhole(latestRunPath(servedBy), JSON.stringify(latest, null, 2));
42604
42672
  } catch {}
42605
42673
  }
@@ -42619,21 +42687,21 @@ function readLatestRun(servedBy, directory = runtimeDirectory()) {
42619
42687
  }
42620
42688
  }
42621
42689
  function openTranscript(startedAt) {
42622
- mkdirSync14(runsDirectory(), { recursive: true });
42623
- const path = join24(runsDirectory(), `run-${startedAt}.log`);
42690
+ mkdirSync15(runsDirectory(), { recursive: true });
42691
+ const path = join25(runsDirectory(), `run-${startedAt}.log`);
42624
42692
  return { path, fd: openSync2(path, "a") };
42625
42693
  }
42626
42694
  function profilePath(startedAt) {
42627
- return join24(runsDirectory(), `run-${startedAt}.profile.json`);
42695
+ return join25(runsDirectory(), `run-${startedAt}.profile.json`);
42628
42696
  }
42629
42697
  function noteIsAbout(note, playing) {
42630
42698
  return note.play !== undefined && playing !== null && note.play.play === playing.play && note.play.editorPid === playing.editorPid;
42631
42699
  }
42632
42700
  function editorNotePath(projectPath, directory = runtimeDirectory()) {
42633
- return join24(directory, "runs", `editor-${projectKey(projectPath)}.json`);
42701
+ return join25(directory, "runs", `editor-${projectKey(projectPath)}.json`);
42634
42702
  }
42635
42703
  function writeEditorRunNote(note) {
42636
- mkdirSync14(runsDirectory(), { recursive: true });
42704
+ mkdirSync15(runsDirectory(), { recursive: true });
42637
42705
  const path = editorNotePath(note.projectPath);
42638
42706
  if (!writeWhole(path, JSON.stringify(note, null, 2))) {
42639
42707
  throw new Error(`the editor run note could not be written at ${path}`);
@@ -42641,7 +42709,7 @@ function writeEditorRunNote(note) {
42641
42709
  }
42642
42710
  function readEditorRunNote(projectPath) {
42643
42711
  for (const directory of runtimeDirectories()) {
42644
- const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join24(directory, "runs", "editor-run.json"));
42712
+ const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join25(directory, "runs", "editor-run.json"));
42645
42713
  if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
42646
42714
  return note;
42647
42715
  }
@@ -42675,7 +42743,7 @@ function readRunRecord(projectPath) {
42675
42743
  if (own !== null) {
42676
42744
  return own;
42677
42745
  }
42678
- const shared = recordAt(join24(directory, "runs", SHARED_NOTE));
42746
+ const shared = recordAt(join25(directory, "runs", SHARED_NOTE));
42679
42747
  if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
42680
42748
  return shared;
42681
42749
  }
@@ -42687,13 +42755,13 @@ function everyRunRecord() {
42687
42755
  for (const directory of runtimeDirectories()) {
42688
42756
  let entries;
42689
42757
  try {
42690
- entries = readdirSync10(join24(directory, "runs"));
42758
+ entries = readdirSync10(join25(directory, "runs"));
42691
42759
  } catch {
42692
42760
  continue;
42693
42761
  }
42694
42762
  for (const entry of entries) {
42695
42763
  if (entry === SHARED_NOTE || entry.startsWith("note-") && entry.endsWith(".json")) {
42696
- const record = recordAt(join24(directory, "runs", entry));
42764
+ const record = recordAt(join25(directory, "runs", entry));
42697
42765
  if (record !== null) {
42698
42766
  found.push(record);
42699
42767
  }
@@ -42903,7 +42971,7 @@ function judgeAnnouncedGame(running, projectPath, announcedAt) {
42903
42971
  }
42904
42972
  function clearRunRecord(projectPath) {
42905
42973
  for (const directory of runtimeDirectories()) {
42906
- const shared = join24(directory, "runs", SHARED_NOTE);
42974
+ const shared = join25(directory, "runs", SHARED_NOTE);
42907
42975
  const sharedRecord = recordAt(shared);
42908
42976
  const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
42909
42977
  for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
@@ -42915,7 +42983,7 @@ function clearRunRecord(projectPath) {
42915
42983
  }
42916
42984
  function ageMs(path, now) {
42917
42985
  try {
42918
- return now - statSync8(path).mtimeMs;
42986
+ return now - statSync9(path).mtimeMs;
42919
42987
  } catch {
42920
42988
  return 0;
42921
42989
  }
@@ -42937,7 +43005,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
42937
43005
  if (!transcript && !entry.endsWith(".partial")) {
42938
43006
  continue;
42939
43007
  }
42940
- const path = join24(directory, entry);
43008
+ const path = join25(directory, entry);
42941
43009
  if (kept.has(path) || ageMs(path, now) < keepMs) {
42942
43010
  continue;
42943
43011
  }
@@ -42958,7 +43026,7 @@ var init_run_record = __esm(() => {
42958
43026
  });
42959
43027
 
42960
43028
  // src/script-origins.ts
42961
- import { join as join25 } from "node:path";
43029
+ import { join as join26 } from "node:path";
42962
43030
  function unresolvedClassIn(message) {
42963
43031
  return UNRESOLVED.exec(message)?.[1] ?? null;
42964
43032
  }
@@ -43011,7 +43079,7 @@ function checkWithEngine(godotPath, projectPath) {
43011
43079
  const ran = await runEngine(godotPath, [
43012
43080
  "--headless",
43013
43081
  "--log-file",
43014
- join25(logDir, "engine.log"),
43082
+ join26(logDir, "engine.log"),
43015
43083
  "--path",
43016
43084
  projectPath,
43017
43085
  "--check-only",
@@ -43240,21 +43308,21 @@ var init_server_instructions = __esm(() => {
43240
43308
  import { execFile as execFile5, spawn as spawn4 } from "node:child_process";
43241
43309
  import { randomUUID as randomUUID2 } from "node:crypto";
43242
43310
  import {
43243
- appendFileSync,
43311
+ appendFileSync as appendFileSync2,
43244
43312
  closeSync as closeSync2,
43245
43313
  constants,
43246
43314
  copyFileSync,
43247
43315
  existsSync as existsSync20,
43248
43316
  fstatSync,
43249
- mkdirSync as mkdirSync15,
43317
+ mkdirSync as mkdirSync16,
43250
43318
  openSync as openSync3,
43251
43319
  readdirSync as readdirSync11,
43252
43320
  readFileSync as readFileSync23,
43253
43321
  readSync as readSync2,
43254
43322
  rmdirSync as rmdirSync2,
43255
- statSync as statSync9
43323
+ statSync as statSync10
43256
43324
  } from "node:fs";
43257
- import { basename as basename7, dirname as dirname19, extname as extname3, isAbsolute as isAbsolute3, join as join26, normalize as normalize2, relative as relative5 } from "node:path";
43325
+ import { basename as basename7, dirname as dirname20, extname as extname3, isAbsolute as isAbsolute3, join as join27, normalize as normalize2, relative as relative5 } from "node:path";
43258
43326
  import { setTimeout as delay4 } from "node:timers/promises";
43259
43327
  import { fileURLToPath as fileURLToPath5 } from "node:url";
43260
43328
  import { promisify as promisify5 } from "node:util";
@@ -43291,11 +43359,11 @@ function writtenFileRefusal(path, rule) {
43291
43359
  if (!rule.extensions.includes(extname3(path).toLowerCase())) {
43292
43360
  return `${named} ${path} does not end in ${rule.extensions.join(" or ")}, which is what ${what} is.`;
43293
43361
  }
43294
- const directory = realPathOr(dirname19(path));
43362
+ const directory = realPathOr(dirname20(path));
43295
43363
  if (!existsSync20(directory)) {
43296
- return `${named}'s directory ${dirname19(path)} does not exist; ${what} creates no directories.`;
43364
+ return `${named}'s directory ${dirname20(path)} does not exist; ${what} creates no directories.`;
43297
43365
  }
43298
- const where = join26(directory, basename7(path));
43366
+ const where = join27(directory, basename7(path));
43299
43367
  const inside = rule.projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
43300
43368
  if (inside !== undefined) {
43301
43369
  return `${named} ${path} is inside the project at ${inside}; ${what} is saved outside it, so it never becomes part of the game.`;
@@ -43816,6 +43884,23 @@ function engineFlagsNote(args) {
43816
43884
  const named = among.join(", ");
43817
43885
  return `${named} ${among.length === 1 ? "is a flag" : "are flags"} the engine reads, and args goes to the game after --, where the engine does not read ${among.length === 1 ? "it" : "them"}. engineArgs hands the engine its own flags; ${among.length === 1 ? "leave it" : "leave them"} in args only for a game that reads ${among.length === 1 ? "that flag" : "those flags"} itself.`;
43818
43886
  }
43887
+ function editorAddonMissing(projectPath) {
43888
+ if (!existsSync20(join27(projectPath, "project.godot"))) {
43889
+ return null;
43890
+ }
43891
+ const report = inspectProject(projectPath);
43892
+ const installed = report.addons.some((addon) => addon.name === "gdharness_editor" && addon.installed);
43893
+ if (installed && report.pluginsEnabled.includes("gdharness_editor")) {
43894
+ return null;
43895
+ }
43896
+ return {
43897
+ why: `the gdharness editor addon is not ${installed ? "enabled" : "installed"} in ${projectPath}`,
43898
+ remedies: installed ? [
43899
+ "gdharness setup in the project enables it",
43900
+ "Or enable it under Project Settings, Plugins, in an editor opened by hand"
43901
+ ] : ["gdharness setup in the project installs and enables it"]
43902
+ };
43903
+ }
43819
43904
  function opElsewhere(tool, requested) {
43820
43905
  const ops = Object.hasOwn(OPS_ELSEWHERE, tool) ? OPS_ELSEWHERE[tool] : undefined;
43821
43906
  const where = ops !== undefined && typeof requested === "string" && Object.hasOwn(ops, requested) ? ops[requested] : undefined;
@@ -44036,7 +44121,7 @@ function profileFrameFunctions(projectPath) {
44036
44121
  let projectGodot = null;
44037
44122
  if (projectPath !== null) {
44038
44123
  try {
44039
- projectGodot = readFileSync23(join26(projectPath, "project.godot"), "utf8");
44124
+ projectGodot = readFileSync23(join27(projectPath, "project.godot"), "utf8");
44040
44125
  } catch {}
44041
44126
  }
44042
44127
  return frameFunctionsFor(queuedMessagesOf(projectGodot));
@@ -44210,7 +44295,7 @@ function foldedEntriesAnswer(log, floor = "warning") {
44210
44295
  }
44211
44296
  function gdUnitFailsOnScriptErrors(projectPath) {
44212
44297
  try {
44213
- return settingKeys(readFileSync23(join26(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
44298
+ return settingKeys(readFileSync23(join27(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
44214
44299
  } catch {
44215
44300
  return true;
44216
44301
  }
@@ -44225,7 +44310,7 @@ function uncaughtNote(uncaught) {
44225
44310
  }
44226
44311
  function elsewhereIn(projectPath, asked) {
44227
44312
  const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
44228
- if (wanted === "" || existsSync20(join26(projectPath, wanted))) {
44313
+ if (wanted === "" || existsSync20(join27(projectPath, wanted))) {
44229
44314
  return "";
44230
44315
  }
44231
44316
  const root = (wanted.split("/")[0] ?? "").toLowerCase();
@@ -44315,13 +44400,13 @@ function scriptsToDiagnose(projectPath, given) {
44315
44400
  }
44316
44401
  function isDirectory(path) {
44317
44402
  try {
44318
- return statSync9(path).isDirectory();
44403
+ return statSync10(path).isDirectory();
44319
44404
  } catch {
44320
44405
  return false;
44321
44406
  }
44322
44407
  }
44323
44408
  function scriptsUnder(directory, prefix) {
44324
- if (existsSync20(join26(directory, ".gdignore"))) {
44409
+ if (existsSync20(join27(directory, ".gdignore"))) {
44325
44410
  return [];
44326
44411
  }
44327
44412
  const found = [];
@@ -44331,7 +44416,7 @@ function scriptsUnder(directory, prefix) {
44331
44416
  }
44332
44417
  const path = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
44333
44418
  if (entry.isDirectory()) {
44334
- found.push(...scriptsUnder(join26(directory, entry.name), path));
44419
+ found.push(...scriptsUnder(join27(directory, entry.name), path));
44335
44420
  } else if (entry.isFile() && entry.name.endsWith(".gd")) {
44336
44421
  found.push(path);
44337
44422
  }
@@ -44347,7 +44432,7 @@ function runtimeAddonAnnouncesEditors(projectPath) {
44347
44432
  return false;
44348
44433
  }
44349
44434
  try {
44350
- return readFileSync23(join26(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
44435
+ return readFileSync23(join27(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
44351
44436
  } catch {
44352
44437
  return false;
44353
44438
  }
@@ -44372,7 +44457,7 @@ function needsItsOwnProcess(asked) {
44372
44457
  class GodotServer {
44373
44458
  stdio = null;
44374
44459
  locator = new GodotLocator;
44375
- operationsScript = join26(__dirname2, "godot", "operations", "godot_operations.gd");
44460
+ operationsScript = join27(__dirname2, "godot", "operations", "godot_operations.gd");
44376
44461
  godotBridge;
44377
44462
  tools = buildToolDefinitions();
44378
44463
  activeProcess = null;
@@ -44396,6 +44481,11 @@ class GodotServer {
44396
44481
  launchedFrom = null;
44397
44482
  launchedDropped = null;
44398
44483
  closedHere = null;
44484
+ inFlight = new Map;
44485
+ callsTaken = 0;
44486
+ clientNoted = null;
44487
+ parentCommand = null;
44488
+ endNoted = false;
44399
44489
  ownProject;
44400
44490
  announcedAt = null;
44401
44491
  handingOverFrom = null;
@@ -44432,6 +44522,7 @@ class GodotServer {
44432
44522
  return { content: [{ type: "text", text: answerJson(payload) }] };
44433
44523
  }
44434
44524
  async run() {
44525
+ this.noteTheStart();
44435
44526
  const located = await this.locator.find();
44436
44527
  if (!located.ok) {
44437
44528
  console.error(`[SERVER] ${GodotLocator.refusal(located).message} Until that is fixed every tool that runs the engine will say so.`);
@@ -44548,6 +44639,7 @@ class GodotServer {
44548
44639
  return;
44549
44640
  }
44550
44641
  console.error(`[SERVER] pid ${successor} now serves ${this.ownProject}; this server is finished and is stopping.`);
44642
+ this.noteTheEnd("superseded", { successor });
44551
44643
  this.handleShutdown(`superseded by ${successor}`, 0);
44552
44644
  }, SUCCESSOR_CHECK_MS);
44553
44645
  watch.unref();
@@ -44619,10 +44711,67 @@ class GodotServer {
44619
44711
  }
44620
44712
  await this.stdio?.close();
44621
44713
  }
44714
+ noteTheStart() {
44715
+ const cwd = process.cwd();
44716
+ const mcpJson = join27(cwd, ".mcp.json");
44717
+ const announced = this.ownProject === null ? null : readAnnouncement(announcementPath(this.ownProject));
44718
+ const predecessor = announced !== null && announced.pid !== process.pid && alive(announced.pid) ? announced.pid : null;
44719
+ writeLifecycle("started", {
44720
+ argv: process.argv.slice(1),
44721
+ cwd,
44722
+ project: this.ownProject,
44723
+ mcpJson: existsSync20(mcpJson) ? mcpJson : null
44724
+ });
44725
+ if (predecessor !== null) {
44726
+ writeLifecycle("replaced", { project: this.ownProject, predecessor });
44727
+ }
44728
+ const ask = setTimeout(() => {
44729
+ processTree().then((tree) => {
44730
+ this.parentCommand = tree?.get(process.ppid)?.command ?? null;
44731
+ writeLifecycle("parent", {
44732
+ command: this.parentCommand,
44733
+ ...tree === undefined ? { why: whyTheProcessTreeFailed() } : {}
44734
+ });
44735
+ });
44736
+ }, PARENT_ASKED_AFTER_MS);
44737
+ ask.unref();
44738
+ }
44739
+ noteTheEnd(cause, more = {}) {
44740
+ if (this.endNoted) {
44741
+ return;
44742
+ }
44743
+ this.endNoted = true;
44744
+ writeLifecycle("ended", {
44745
+ cause,
44746
+ project: this.ownProject,
44747
+ ...this.parentCommand === null ? {} : { parentCommand: this.parentCommand },
44748
+ ...this.clientNoted === null ? {} : { client: this.clientNoted },
44749
+ inFlight: callsInFlight(this.inFlight.values()),
44750
+ ...more
44751
+ });
44752
+ }
44753
+ noteTheClient(client) {
44754
+ if (client === undefined) {
44755
+ return;
44756
+ }
44757
+ const said = `${client.name} ${client.version}`;
44758
+ if (said === this.clientNoted) {
44759
+ return;
44760
+ }
44761
+ this.clientNoted = said;
44762
+ writeLifecycle("client", { client: { name: client.name, version: client.version } });
44763
+ }
44622
44764
  setupShutdownHandlers() {
44623
44765
  const requestShutdown = (source, exitCode) => {
44624
44766
  this.handleShutdown(source, exitCode);
44625
44767
  };
44768
+ process.on("uncaughtException", (error, origin) => {
44769
+ this.noteTheEnd(origin === "unhandledRejection" ? "unhandledRejection" : "uncaughtException", {
44770
+ error: error instanceof Error ? error.stack ?? error.message : String(error)
44771
+ });
44772
+ console.error(error);
44773
+ process.exit(1);
44774
+ });
44626
44775
  process.once("SIGINT", () => {
44627
44776
  requestShutdown("SIGINT", 0);
44628
44777
  });
@@ -44635,7 +44784,8 @@ class GodotServer {
44635
44784
  process.once("beforeExit", (code) => {
44636
44785
  requestShutdown(`beforeExit:${code}`);
44637
44786
  });
44638
- process.once("exit", () => {
44787
+ process.once("exit", (code) => {
44788
+ this.noteTheEnd(`exit:${code}`);
44639
44789
  this.forceCleanupOnExit();
44640
44790
  });
44641
44791
  }
@@ -44644,6 +44794,7 @@ class GodotServer {
44644
44794
  return;
44645
44795
  }
44646
44796
  this.shutdownInitiated = true;
44797
+ this.noteTheEnd(source);
44647
44798
  this.logDebug(`Shutting down server via ${source}`);
44648
44799
  try {
44649
44800
  await this.cleanup();
@@ -44683,9 +44834,23 @@ class GodotServer {
44683
44834
  return mcp;
44684
44835
  }
44685
44836
  setupToolHandlers(protocol) {
44686
- protocol.setRequestHandler("tools/list", () => ({ tools: this.tools }));
44837
+ const clientOf = (envelope) => {
44838
+ const named = envelope?.["io.modelcontextprotocol/clientInfo"];
44839
+ if (typeof named === "object" && named !== null) {
44840
+ const { name, version } = named;
44841
+ if (typeof name === "string" && typeof version === "string") {
44842
+ return { name, version };
44843
+ }
44844
+ }
44845
+ return protocol.getClientVersion();
44846
+ };
44847
+ protocol.setRequestHandler("tools/list", (_request, ctx) => {
44848
+ this.noteTheClient(clientOf(ctx.mcpReq.envelope));
44849
+ return { tools: this.tools };
44850
+ });
44687
44851
  protocol.setRequestHandler("tools/call", async (request, ctx) => {
44688
44852
  this.logDebug(`Handling tool request: ${request.params.name}`);
44853
+ this.noteTheClient(clientOf(ctx.mcpReq.envelope));
44689
44854
  const spec = toolSpec(request.params.name);
44690
44855
  if (!spec) {
44691
44856
  throw new ProtocolError(ProtocolErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}. Tools: ${TOOL_SPECS.map((tool) => tool.name).join(", ")}`);
@@ -44696,7 +44861,7 @@ class GodotServer {
44696
44861
  return checked.response;
44697
44862
  }
44698
44863
  const named = args["projectPath"];
44699
- if (typeof named === "string" && existsSync20(join26(named, "project.godot"))) {
44864
+ if (typeof named === "string" && existsSync20(join27(named, "project.godot"))) {
44700
44865
  this.lastProjectPath = named;
44701
44866
  }
44702
44867
  this.updates.refresh();
@@ -44709,8 +44874,14 @@ class GodotServer {
44709
44874
  this.logDebug(`Progress for ${spec.name} was not sent: ${errorMessage(error)}`);
44710
44875
  });
44711
44876
  };
44712
- const answer = await withCallSignal(ctx.mcpReq.signal, () => this.answered(spec.name, checked.op ?? "", args), progress);
44713
- return this.withFeedbackNotice(this.withUpdateNotice(answer));
44877
+ const call = this.callsTaken++;
44878
+ this.inFlight.set(call, { tool: spec.name, op: checked.op ?? "", startedAt: Date.now() });
44879
+ try {
44880
+ const answer = await withCallSignal(ctx.mcpReq.signal, () => this.answered(spec.name, checked.op ?? "", args), progress);
44881
+ return this.withFeedbackNotice(this.withUpdateNotice(answer));
44882
+ } finally {
44883
+ this.inFlight.delete(call);
44884
+ }
44714
44885
  });
44715
44886
  }
44716
44887
  async answered(tool, op, args) {
@@ -44929,10 +45100,12 @@ class GodotServer {
44929
45100
  ]);
44930
45101
  }
44931
45102
  if (!this.godotBridge.isConnected()) {
44932
- return this.createErrorResponse(`${tool} ${op} was asked of the editor, and no editor has reached this server.`, [
44933
- "editor_status says whether the bridge is up and whether an editor may yet connect",
44934
- 'Open the project with editor_launch, or pass from: "disk" to read the file instead'
45103
+ const refused = await this.noEditorYet(answerable, [
45104
+ `${tool} ${op} was asked of the editor; from: "disk" reads the file instead`
44935
45105
  ]);
45106
+ if (refused !== null) {
45107
+ return refused;
45108
+ }
44936
45109
  }
44937
45110
  const { op: _dropped, ...asks } = answerable;
44938
45111
  const contained = this.containProjectFiles(asks);
@@ -45017,11 +45190,11 @@ class GodotServer {
45017
45190
  case "script_info":
45018
45191
  switch (op) {
45019
45192
  case "symbols":
45020
- return await this.handleLSP("lsp_get_symbols", args);
45193
+ return await this.askScriptInfo("lsp_get_symbols", "Symbols", args);
45021
45194
  case "completion":
45022
- return await this.handleLSP("lsp_get_completions", args);
45195
+ return await this.askScriptInfo("lsp_get_completions", "Completion", args);
45023
45196
  default:
45024
- return await this.handleLSP("lsp_get_hover", args);
45197
+ return await this.askScriptInfo("lsp_get_hover", "Hover", args);
45025
45198
  }
45026
45199
  case "script_diagnostics":
45027
45200
  return await this.handleScriptDiagnostics(args);
@@ -45198,7 +45371,7 @@ class GodotServer {
45198
45371
  if (path === undefined) {
45199
45372
  return { ok: false, response: this.createErrorResponse("projectPath is required.") };
45200
45373
  }
45201
- const file = join26(path, "project.godot");
45374
+ const file = join27(path, "project.godot");
45202
45375
  if (!existsSync20(file)) {
45203
45376
  return {
45204
45377
  ok: false,
@@ -45885,18 +46058,18 @@ class GodotServer {
45885
46058
  if (!engine.ok) {
45886
46059
  return engine.response;
45887
46060
  }
45888
- const folder = dirname19(output.absolutePath);
46061
+ const folder = dirname20(output.absolutePath);
45889
46062
  try {
45890
- mkdirSync15(folder, { recursive: true });
46063
+ mkdirSync16(folder, { recursive: true });
45891
46064
  } catch (error) {
45892
- return this.createErrorResponse(`Cannot create ${dirname19(output.relativePath)} to export into: ${errorMessage(error)}`);
46065
+ return this.createErrorResponse(`Cannot create ${dirname20(output.relativePath)} to export into: ${errorMessage(error)}`);
45893
46066
  }
45894
46067
  const debug = readBoolean(args, "debug") ?? false;
45895
46068
  const exportLogs = scratchDirectory("export");
45896
46069
  const exportArgs = [
45897
46070
  "--headless",
45898
46071
  "--log-file",
45899
- join26(exportLogs, "engine.log"),
46072
+ join27(exportLogs, "engine.log"),
45900
46073
  "--path",
45901
46074
  project.value.path,
45902
46075
  debug ? "--export-debug" : "--export-release",
@@ -45904,7 +46077,7 @@ class GodotServer {
45904
46077
  output.absolutePath
45905
46078
  ];
45906
46079
  this.logDebug(`Exporting: ${engine.value} ${exportArgs.join(" ")}`);
45907
- const writtenAt = () => existsSync20(output.absolutePath) ? statSync9(output.absolutePath).mtimeMs : null;
46080
+ const writtenAt = () => existsSync20(output.absolutePath) ? statSync10(output.absolutePath).mtimeMs : null;
45908
46081
  const before = writtenAt();
45909
46082
  let ending;
45910
46083
  try {
@@ -45968,10 +46141,10 @@ class GodotServer {
45968
46141
  if (movie !== null) {
45969
46142
  return this.createErrorResponse(movie);
45970
46143
  }
45971
- const missing = paths.value.filter((path) => !existsSync20(join26(project.value.path, path.slice("res://".length))));
46144
+ const missing = paths.value.filter((path) => !existsSync20(join27(project.value.path, path.slice("res://".length))));
45972
46145
  const asked = paths.value.filter((path) => !missing.includes(path));
45973
46146
  const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
45974
- const noRunner = !existsSync20(join26(project.value.path, runner));
46147
+ const noRunner = !existsSync20(join27(project.value.path, runner));
45975
46148
  if (asked.length === 0) {
45976
46149
  const verdict = `nothing at ${missing.join(" and ")}`;
45977
46150
  const andNoRunner = noRunner ? `, and gdUnit4 is not installed in this project: no ${runner}` : "";
@@ -46062,21 +46235,21 @@ class GodotServer {
46062
46235
  await untilGone([run.process.pid]);
46063
46236
  }
46064
46237
  await run.settled;
46065
- const reportsDir = join26(project.value.path, ".godot", ...ours.split("/"));
46238
+ const reportsDir = join27(project.value.path, ".godot", ...ours.split("/"));
46066
46239
  let report = null;
46067
46240
  let reportProblem = null;
46068
46241
  try {
46069
46242
  const written = existsSync20(reportsDir) ? readdirSync11(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
46070
46243
  const newest = written.at(-1);
46071
46244
  if (newest !== undefined) {
46072
- report = parseJUnit(readFileSync23(join26(reportsDir, newest, "results.xml"), "utf8"));
46245
+ report = parseJUnit(readFileSync23(join27(reportsDir, newest, "results.xml"), "utf8"));
46073
46246
  }
46074
46247
  } catch (error) {
46075
46248
  reportProblem = errorMessage(error);
46076
46249
  } finally {
46077
46250
  discard(reportsDir, userData);
46078
46251
  try {
46079
- rmdirSync2(dirname19(reportsDir));
46252
+ rmdirSync2(dirname20(reportsDir));
46080
46253
  } catch {}
46081
46254
  }
46082
46255
  const engineEntries = foldedRepeats(run.log.select({
@@ -46308,31 +46481,9 @@ class GodotServer {
46308
46481
  async diagnosticsOf(args) {
46309
46482
  const answer = await this.handleLSP("lsp_get_diagnostics", args);
46310
46483
  const payload = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
46311
- if (payload["error"] !== undefined) {
46312
- const reason = typeof payload["error"] === "string" ? payload["error"] : JSON.stringify(payload["error"]);
46313
- if (payload["refusedArguments"] === true) {
46314
- return { refusal: this.createErrorResponse(reason), perScript: true };
46315
- }
46316
- if (payload["stillReading"] === true) {
46317
- return { refusal: this.createErrorResponse(`Diagnostics unavailable: ${reason}`), perScript: false };
46318
- }
46319
- if (typeof payload["serves"] === "string") {
46320
- return {
46321
- refusal: this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
46322
- "editor_status names the editor on the bridge and the port it says it serves",
46323
- "editor_launch opens this project's own editor, on a language server port of its own",
46324
- "GDHARNESS_LSP_PORT points this server at the right one"
46325
- ]),
46326
- perScript: false
46327
- };
46328
- }
46329
- return {
46330
- refusal: this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
46331
- `Ensure the Godot editor is running with its language server enabled, on port ${this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT)}`,
46332
- "GDHARNESS_LSP_PORT points this server at another one"
46333
- ]),
46334
- perScript: payload["aboutThisFile"] === true
46335
- };
46484
+ const refused = this.languageServerRefusal(payload, "Diagnostics");
46485
+ if (refused !== null) {
46486
+ return refused;
46336
46487
  }
46337
46488
  const diagnostics = readArray(payload, "diagnostics") ?? [];
46338
46489
  const errors = diagnostics.filter((entry) => asParams(entry)["severity"] === "error").length;
@@ -46439,6 +46590,40 @@ class GodotServer {
46439
46590
  async handleLSP(toolName, args) {
46440
46591
  return handleLSPTool(await this.lsp(), toolName, args);
46441
46592
  }
46593
+ async askScriptInfo(toolName, asked, args) {
46594
+ const answer = await this.handleLSP(toolName, args);
46595
+ const refused = this.languageServerRefusal(asParams(JSON.parse(answer.content[0]?.text ?? "{}")), asked);
46596
+ return refused === null ? answer : refused.refusal;
46597
+ }
46598
+ languageServerRefusal(payload, asked) {
46599
+ if (payload["error"] === undefined) {
46600
+ return null;
46601
+ }
46602
+ const reason = typeof payload["error"] === "string" ? payload["error"] : JSON.stringify(payload["error"]);
46603
+ if (payload["refusedArguments"] === true) {
46604
+ return { refusal: this.createErrorResponse(reason), perScript: true };
46605
+ }
46606
+ if (payload["stillReading"] === true) {
46607
+ return { refusal: this.createErrorResponse(`${asked} unavailable: ${reason}`), perScript: false };
46608
+ }
46609
+ if (typeof payload["serves"] === "string") {
46610
+ return {
46611
+ refusal: this.createErrorResponse(`${asked} unavailable: ${reason}`, [
46612
+ "editor_status names the editor on the bridge and the port it says it serves",
46613
+ "editor_launch opens this project's own editor, on a language server port of its own",
46614
+ "GDHARNESS_LSP_PORT points this server at the right one"
46615
+ ]),
46616
+ perScript: false
46617
+ };
46618
+ }
46619
+ return {
46620
+ refusal: this.createErrorResponse(`${asked} unavailable: ${reason}`, [
46621
+ `Ensure the Godot editor is running with its language server enabled, on port ${this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT)}`,
46622
+ "GDHARNESS_LSP_PORT points this server at another one"
46623
+ ]),
46624
+ perScript: payload["aboutThisFile"] === true
46625
+ };
46626
+ }
46442
46627
  async lsp() {
46443
46628
  const port = this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT);
46444
46629
  if (this.lspClient !== null && this.lspClient.port !== port) {
@@ -46539,7 +46724,7 @@ class GodotServer {
46539
46724
  return;
46540
46725
  }
46541
46726
  try {
46542
- appendFileSync(run.transcript, line.endsWith(`
46727
+ appendFileSync2(run.transcript, line.endsWith(`
46543
46728
  `) ? line : `${line}
46544
46729
  `);
46545
46730
  } catch (error) {
@@ -46750,7 +46935,8 @@ class GodotServer {
46750
46935
  serverSpeaks: RUNTIME_PROTOCOL,
46751
46936
  behind: game.protocol > RUNTIME_PROTOCOL ? "server" : "addon"
46752
46937
  }))
46753
- }
46938
+ },
46939
+ server: { pid: process.pid, version: SERVER_VERSION, lifecycleLog: lifecycleLogPath() }
46754
46940
  });
46755
46941
  }
46756
46942
  async editorServersListening() {
@@ -46782,7 +46968,7 @@ class GodotServer {
46782
46968
  return null;
46783
46969
  }
46784
46970
  try {
46785
- return settingKeys(readFileSync23(join26(projectPath, "project.godot"), "utf8"));
46971
+ return settingKeys(readFileSync23(join27(projectPath, "project.godot"), "utf8"));
46786
46972
  } catch {
46787
46973
  return null;
46788
46974
  }
@@ -46838,6 +47024,10 @@ class GodotServer {
46838
47024
  async handleCloseEditor() {
46839
47025
  const before = this.godotBridge.getStatus();
46840
47026
  if (!before.connected) {
47027
+ const launched = this.launchedEditor;
47028
+ if (launched !== null && await this.launchedEditorIsUp() && !this.godotBridge.isConnected()) {
47029
+ return await this.stopTheLaunchedEditor(launched);
47030
+ }
46841
47031
  return this.createErrorResponse("No editor is connected, so there is nothing to close.", [
46842
47032
  "editor_status says whether the bridge is up and what has reached it"
46843
47033
  ]);
@@ -46871,6 +47061,31 @@ class GodotServer {
46871
47061
  tookMs: Date.now() - began
46872
47062
  });
46873
47063
  }
47064
+ async stopTheLaunchedEditor(launched) {
47065
+ const began = Date.now();
47066
+ try {
47067
+ process.kill(launched.pid);
47068
+ } catch (error) {
47069
+ if (alive(launched.pid)) {
47070
+ return this.createErrorResponse(`The editor this server opened, pid ${launched.pid}, has not connected, and stopping it failed: ${errorMessage(error)}`, ["editor_status says whether it has since connected"]);
47071
+ }
47072
+ }
47073
+ const stopWaitMs = 1e4;
47074
+ if (!await this.waitForBridge(() => !alive(launched.pid), began + stopWaitMs)) {
47075
+ return this.createErrorResponse(`The editor this server opened, pid ${launched.pid}, had not connected and was told to stop, and is still running after ${stopWaitMs / 1000}s.`, ["editor_status says whether it has since connected"]);
47076
+ }
47077
+ this.closedHere = { editorPid: launched.pid, at: new Date().toISOString() };
47078
+ this.launchedEditor = null;
47079
+ this.launchedFrom = null;
47080
+ return this.jsonTextResponse({
47081
+ closed: true,
47082
+ editorPid: launched.pid,
47083
+ projectPath: launched.projectPath,
47084
+ beforeConnecting: true,
47085
+ note: "It had not reached the bridge, so it was stopped rather than asked to save and quit, and held nothing of this server's to save.",
47086
+ tookMs: Date.now() - began
47087
+ });
47088
+ }
46874
47089
  settleTheLaunchReading() {
46875
47090
  const watched = this.launchedFrom;
46876
47091
  const status = this.godotBridge.getStatus();
@@ -47006,13 +47221,9 @@ class GodotServer {
47006
47221
  `editor_launch with projectPath ${this.ownProject} opens the editor this server serves`
47007
47222
  ]);
47008
47223
  }
47009
- const report = inspectProject(projectPath);
47010
- const installed = report.addons.some((addon) => addon.name === "gdharness_editor" && addon.installed);
47011
- if (!installed || !report.pluginsEnabled.includes("gdharness_editor")) {
47012
- return this.createErrorResponse(`The gdharness editor addon is not ${installed ? "enabled" : "installed"} in ${projectPath}, so an editor opened on it would never reach this server.`, installed ? [
47013
- "gdharness setup in the project enables it",
47014
- "Or enable it under Project Settings, Plugins, in an editor opened by hand"
47015
- ] : ["gdharness setup in the project installs and enables it"]);
47224
+ const missing = editorAddonMissing(projectPath);
47225
+ if (missing !== null) {
47226
+ return this.createErrorResponse(`${missing.why}, so an editor opened on it would never reach this server.`, missing.remedies);
47016
47227
  }
47017
47228
  const owed = restartOwed(projectPath);
47018
47229
  if (owed !== null && restartUnderway(owed)) {
@@ -47040,7 +47251,8 @@ class GodotServer {
47040
47251
  writeEditorLogNote(projectPath, launched.pid);
47041
47252
  this.launchedEditor = {
47042
47253
  pid: launched.pid,
47043
- startedAt: (await startTimesOf([launched.pid])).get(launched.pid) ?? null
47254
+ startedAt: (await startTimesOf([launched.pid])).get(launched.pid) ?? null,
47255
+ projectPath
47044
47256
  };
47045
47257
  this.closedHere = null;
47046
47258
  return { pid: launched.pid, error: null };
@@ -47177,7 +47389,7 @@ class GodotServer {
47177
47389
  }
47178
47390
  async runtimeUp(projectPath, before, budget, withArgs) {
47179
47391
  const budgetMs = budget.ms;
47180
- if (!existsSync20(join26(projectPath, RUNTIME_AUTOLOAD.path))) {
47392
+ if (!existsSync20(join27(projectPath, RUNTIME_AUTOLOAD.path))) {
47181
47393
  return runtimeVerdict(null, {
47182
47394
  addon: false,
47183
47395
  budgetMs,
@@ -47262,7 +47474,7 @@ class GodotServer {
47262
47474
  }
47263
47475
  let announcedAt;
47264
47476
  try {
47265
- announcedAt = statSync9(endpoint.file).mtimeMs;
47477
+ announcedAt = statSync10(endpoint.file).mtimeMs;
47266
47478
  } catch {
47267
47479
  return;
47268
47480
  }
@@ -47431,7 +47643,7 @@ class GodotServer {
47431
47643
  return;
47432
47644
  }
47433
47645
  if (game.transcript !== null) {
47434
- appendFileSync(game.transcript, reported);
47646
+ appendFileSync2(game.transcript, reported);
47435
47647
  } else {
47436
47648
  game.log.append("stderr", reported);
47437
47649
  }
@@ -48135,7 +48347,7 @@ class GodotServer {
48135
48347
  }
48136
48348
  const going = await this.runStillGoing(run);
48137
48349
  const projectPath = run.projectPath;
48138
- const projectGone = projectPath !== null && !existsSync20(join26(projectPath, "project.godot"));
48350
+ const projectGone = projectPath !== null && !existsSync20(join27(projectPath, "project.godot"));
48139
48351
  if (projectGone && callersName !== undefined) {
48140
48352
  return this.createErrorResponse(`callersOf reads the project's scripts, and the run's project at ${projectPath} has no project.godot any more.`, ["editor_run profile without callersOf answers with the profile itself"]);
48141
48353
  }
@@ -48306,7 +48518,11 @@ class GodotServer {
48306
48518
  if (!greeted && launched === null && status.connected) {
48307
48519
  return this.createErrorResponse("The editor has connected and has not yet said who it is, so whose console to read cannot be told yet.", ["Ask again in a moment; editor_status shows greeting until it has"]);
48308
48520
  }
48309
- const projectPath = status.projectPath ?? this.ownProject ?? undefined;
48521
+ const projectPath = status.projectPath ?? this.launchedEditor?.projectPath ?? this.ownProject ?? undefined;
48522
+ const elsewhere = this.anotherProjectNamed(args, projectPath);
48523
+ if (elsewhere !== null) {
48524
+ return elsewhere;
48525
+ }
48310
48526
  if (projectPath === undefined || !greeted && launched === null) {
48311
48527
  return this.createErrorResponse("No editor is connected, so there is no console to read.", [
48312
48528
  "editor_status says whether one is connected and whether it may yet be",
@@ -48357,6 +48573,19 @@ class GodotServer {
48357
48573
  omitted: selected.omitted === 0 ? undefined : selected.omitted
48358
48574
  });
48359
48575
  }
48576
+ anotherProjectNamed(args, ofWhatIsRead) {
48577
+ const named = readNonEmptyString(args, "projectPath");
48578
+ if (named === undefined || ofWhatIsRead === null || ofWhatIsRead === undefined || ofWhatIsRead === "") {
48579
+ return null;
48580
+ }
48581
+ if (isSameDirectory(named, ofWhatIsRead)) {
48582
+ return null;
48583
+ }
48584
+ return this.createErrorResponse(`editor_output reads what this server holds, which is of ${ofWhatIsRead}, and this call names ${named}.`, [
48585
+ "Another project's run is read through that project's own gdharness server",
48586
+ "editor_status names every game running on the machine under runtimes"
48587
+ ]);
48588
+ }
48360
48589
  async handleGetDebugOutput(args, waitedMs) {
48361
48590
  if (readString(args, "op") === "editor") {
48362
48591
  return this.handleEditorConsole(args);
@@ -48366,6 +48595,10 @@ class GodotServer {
48366
48595
  if (!run) {
48367
48596
  return this.createErrorResponse(this.nothingOfOursIsRunning());
48368
48597
  }
48598
+ const elsewhere = this.anotherProjectNamed(args, run.projectPath ?? this.godotBridge.getStatus().projectPath);
48599
+ if (elsewhere !== null) {
48600
+ return elsewhere;
48601
+ }
48369
48602
  await this.tieThePlayedGame(run);
48370
48603
  this.drainEditorOutput(run);
48371
48604
  this.drainTranscript(run);
@@ -48402,7 +48635,7 @@ class GodotServer {
48402
48635
  notes.push(endedWithoutACode({
48403
48636
  throughEditor: run.throughEditor,
48404
48637
  announced: announced !== undefined || run.announcedPid !== undefined,
48405
- couldAnnounce: run.projectPath !== null && existsSync20(join26(run.projectPath, RUNTIME_AUTOLOAD.path)),
48638
+ couldAnnounce: run.projectPath !== null && existsSync20(join27(run.projectPath, RUNTIME_AUTOLOAD.path)),
48406
48639
  endedHere: typeof run.endedHere === "string",
48407
48640
  pickedUp: run.pickedUp === true
48408
48641
  }));
@@ -49182,15 +49415,47 @@ class GodotServer {
49182
49415
  const holds = held["classes"].map(String);
49183
49416
  return { unseen: unseenByEditor(projectPath, holds), stillHeld: heldButGone(projectPath, holds) };
49184
49417
  }
49418
+ async noEditorYet(args, instead) {
49419
+ const project = readNonEmptyString(args, "projectPath") ?? this.ownProject;
49420
+ const missing = project === null ? null : editorAddonMissing(project);
49421
+ if (missing !== null) {
49422
+ return this.createErrorResponse(`Editor not connected, and no editor of this project can connect: ${missing.why}.`, [...instead, ...missing.remedies, "editor_launch then opens an editor that connects"]);
49423
+ }
49424
+ const launched = this.launchedEditor;
49425
+ if (launched !== null && await this.launchedEditorIsUp()) {
49426
+ const began = Date.now();
49427
+ await this.waitForBridge(() => hasSaidWhoItIs(this.godotBridge.getStatus()) || !alive(launched.pid), began + editorRestartTimeoutMs());
49428
+ if (this.godotBridge.isConnected()) {
49429
+ return null;
49430
+ }
49431
+ const opened = `the editor this server opened on ${launched.projectPath}, pid ${launched.pid}`;
49432
+ if (alive(launched.pid)) {
49433
+ return this.createErrorResponse(`Editor not connected: ${opened}, is still running and has not reached the bridge in the ${String(Math.round((Date.now() - began) / 1000))}s this call waited. An editor dials in once it has imported the project and loaded its plugins, and the first open of a large project imports for minutes.`, [
49434
+ ...instead,
49435
+ "editor_status names it under awaitingLaunchedEditor until it has connected",
49436
+ "editor_output op editor shows what it is printing meanwhile"
49437
+ ]);
49438
+ }
49439
+ return this.createErrorResponse(`Editor not connected: ${opened}, exited without reaching the bridge.`, [
49440
+ ...instead,
49441
+ "editor_output op editor shows what it printed before it went",
49442
+ "editor_launch open starts another"
49443
+ ]);
49444
+ }
49445
+ return this.createErrorResponse("Editor not connected: no Godot editor with the gdharness editor addon has reached this server.", [
49446
+ ...instead,
49447
+ "Open the project in the editor with editor_launch, or by hand",
49448
+ "Enable the addon under Project > Project Settings > Plugins",
49449
+ "editor_status says whether the bridge is up and what it is listening on"
49450
+ ]);
49451
+ }
49185
49452
  async handleViaBridge(toolName, args, askedAs) {
49186
49453
  if (!this.godotBridge.isConnected()) {
49187
- const instead = askedAs !== undefined && Object.hasOwn(EDITOR_FREE_INSTEAD, askedAs) ? EDITOR_FREE_INSTEAD[askedAs] : undefined;
49188
- return this.createErrorResponse("Editor not connected: no Godot editor with the gdharness editor addon has reached this server.", [
49189
- ...instead === undefined ? [] : [instead],
49190
- "Open the project in the editor with editor_launch, or by hand",
49191
- "Enable the addon under Project > Project Settings > Plugins",
49192
- "editor_status says whether the bridge is up and what it is listening on"
49193
- ]);
49454
+ const alternative = askedAs !== undefined && Object.hasOwn(EDITOR_FREE_INSTEAD, askedAs) ? EDITOR_FREE_INSTEAD[askedAs] : undefined;
49455
+ const refused = await this.noEditorYet(args, alternative === undefined ? [] : [alternative]);
49456
+ if (refused !== null) {
49457
+ return refused;
49458
+ }
49194
49459
  }
49195
49460
  const open = this.godotBridge.getStatus().projectPath;
49196
49461
  const meant = readNonEmptyString(args, "projectPath");
@@ -49263,7 +49528,7 @@ class GodotServer {
49263
49528
  async ownRunOnItsWay(projectPath, budgetMs) {
49264
49529
  const run = await this.currentRun();
49265
49530
  const project = projectPath ?? run?.projectPath ?? null;
49266
- if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync20(join26(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
49531
+ if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync20(join27(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
49267
49532
  return null;
49268
49533
  }
49269
49534
  const already = runtimesAnnounced().running;
@@ -49272,7 +49537,7 @@ class GodotServer {
49272
49537
  return !run.announcedBefore.has(one.pid);
49273
49538
  }
49274
49539
  try {
49275
- return statSync9(one.file).mtimeMs >= run.startedAt;
49540
+ return statSync10(one.file).mtimeMs >= run.startedAt;
49276
49541
  } catch {
49277
49542
  return false;
49278
49543
  }
@@ -49348,7 +49613,7 @@ class GodotServer {
49348
49613
  }
49349
49614
  }
49350
49615
  const screenshotDir = expectsScreenshot ? scratchDirectory("runtime-screenshot") : null;
49351
- const screenshotPath = screenshotDir ? join26(screenshotDir, "capture.png") : null;
49616
+ const screenshotPath = screenshotDir ? join27(screenshotDir, "capture.png") : null;
49352
49617
  const staleNote = this.runtimeStaleNote(choice.endpoint);
49353
49618
  const stale = staleNote === undefined ? {} : { addonIsStale: true, staleNote };
49354
49619
  const refused = (message) => this.createErrorResponse(staleNote === undefined ? message : `${message}
@@ -49499,7 +49764,7 @@ async function runGodotServer() {
49499
49764
  const server = new GodotServer;
49500
49765
  await server.run();
49501
49766
  }
49502
- var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, EDITOR_FREE_INSTEAD, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
49767
+ var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, PARENT_ASKED_AFTER_MS = 5000, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, EDITOR_FREE_INSTEAD, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
49503
49768
  var init_server = __esm(() => {
49504
49769
  init_dist();
49505
49770
  init_stdio();
@@ -49528,6 +49793,7 @@ var init_server = __esm(() => {
49528
49793
  init_junit();
49529
49794
  init_keeper_channel();
49530
49795
  init_launch();
49796
+ init_lifecycle_log();
49531
49797
  init_lsp_client();
49532
49798
  init_outside();
49533
49799
  init_paths();
@@ -49553,7 +49819,7 @@ var init_server = __esm(() => {
49553
49819
  init_tool_reference();
49554
49820
  init_update_check();
49555
49821
  run4 = promisify5(execFile5);
49556
- __dirname2 = dirname19(fileURLToPath5(import.meta.url));
49822
+ __dirname2 = dirname20(fileURLToPath5(import.meta.url));
49557
49823
  CONSOLE_WRAPPER = /_console(\.exe)?$/i;
49558
49824
  LONGEST_BOOT_NOTED_MS = 5 * 60000;
49559
49825
  VALUE_NAMES = {
@@ -49612,7 +49878,7 @@ init_bridge_announce();
49612
49878
  init_errors();
49613
49879
  init_godot_path();
49614
49880
  import { existsSync as existsSync21 } from "node:fs";
49615
- import { join as join27, resolve as resolve8 } from "node:path";
49881
+ import { join as join28, resolve as resolve8 } from "node:path";
49616
49882
 
49617
49883
  // src/harnesses.ts
49618
49884
  import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
@@ -50773,7 +51039,9 @@ emptily: no game running, a game with no debugger behind it, a game running rath
50773
51039
  control outside a headless viewport. Read the refusal instead of retrying the call.
50774
51040
 
50775
51041
  Every answer is read back from the engine after the change rather than echoed from the request, and
50776
- whatever the engine said on stderr comes back under \`engine_messages\`.
51042
+ whatever the engine said on stderr comes back under \`engine_messages\`. A headless engine runs the
51043
+ project's autoloads after the operation has answered, and what it reports from then until it exits
51044
+ comes back separately, under \`engine_messages_after_answer\`: none of it changes the answer.
50777
51045
 
50778
51046
  ## More
50779
51047
 
@@ -50875,7 +51143,7 @@ class UsageError extends Refusal {
50875
51143
  function projectArgument(at) {
50876
51144
  const named = args.slice(at).find((value) => !value.startsWith("--"));
50877
51145
  const projectPath = resolve8(named ?? ".");
50878
- if (!existsSync21(join27(projectPath, "project.godot"))) {
51146
+ if (!existsSync21(join28(projectPath, "project.godot"))) {
50879
51147
  throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
50880
51148
  }
50881
51149
  return projectPath;
@@ -50920,7 +51188,7 @@ async function registerRuntime(godot, projectPath) {
50920
51188
  return;
50921
51189
  }
50922
51190
  console.log(`${RUNTIME_AUTOLOAD.name} autoload left at ${named}, which this project registers itself`);
50923
- if (!existsSync21(join27(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
51191
+ if (!existsSync21(join28(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
50924
51192
  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}`);
50925
51193
  }
50926
51194
  }