gdharness 1.3.5 → 1.3.6

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
@@ -7953,16 +7953,16 @@ var init_paths = __esm(() => {
7953
7953
  // src/runner.ts
7954
7954
  import { existsSync as existsSync3 } from "node:fs";
7955
7955
  import { dirname as dirname3, join as join4 } from "node:path";
7956
- import process3 from "node:process";
7956
+ import process4 from "node:process";
7957
7957
  function currentRunner() {
7958
- const versions = process3.versions;
7958
+ const versions = process4.versions;
7959
7959
  return versions["bun"] === undefined ? "npx" : "bunx";
7960
7960
  }
7961
7961
  function runLine(runner, version, rest = "") {
7962
7962
  const flag = runner === "npx" ? "-y " : "";
7963
7963
  return `${runner} ${flag}gdharness@${version}${rest === "" ? "" : ` ${rest}`}`;
7964
7964
  }
7965
- function spawnFor(runner, version, execPath = process3.execPath) {
7965
+ function spawnFor(runner, version, execPath = process4.execPath) {
7966
7966
  const spec = `gdharness@${version}`;
7967
7967
  if (runner !== currentRunner()) {
7968
7968
  return { command: runner, args: runner === "npx" ? ["-y", spec] : ["--bun", spec] };
@@ -7973,7 +7973,7 @@ function spawnFor(runner, version, execPath = process3.execPath) {
7973
7973
  return { command: alongside(execPath, "npx") ?? "npx", args: ["-y", spec] };
7974
7974
  }
7975
7975
  function alongside(execPath, name) {
7976
- const runner = join4(dirname3(execPath), process3.platform === "win32" ? `${name}.cmd` : name);
7976
+ const runner = join4(dirname3(execPath), process4.platform === "win32" ? `${name}.cmd` : name);
7977
7977
  return existsSync3(runner) ? runner : undefined;
7978
7978
  }
7979
7979
  var init_runner = () => {};
@@ -8315,8 +8315,8 @@ var init_engine_run = __esm(() => {
8315
8315
  import { existsSync as existsSync5, readdirSync as readdirSync3, statSync as statSync4 } from "node:fs";
8316
8316
  import { homedir as homedir3 } from "node:os";
8317
8317
  import { basename as basename4, dirname as dirname5, isAbsolute as isAbsolute2, join as join6 } from "node:path";
8318
- import process5 from "node:process";
8319
- function editorCacheDirectory(godotPath, platform = process5.platform, env = process5.env, home = homedir3()) {
8318
+ import process6 from "node:process";
8319
+ function editorCacheDirectory(godotPath, platform = process6.platform, env = process6.env, home = homedir3()) {
8320
8320
  const beside = dirname5(godotPath);
8321
8321
  const bundle = platform === "darwin" && basename4(beside) === "MacOS" && basename4(dirname5(beside)) === "Contents" ? dirname5(dirname5(dirname5(beside))) : null;
8322
8322
  for (const folder of bundle === null ? [beside] : [beside, bundle]) {
@@ -8471,13 +8471,23 @@ function importRecord(projectPath, resourcePath) {
8471
8471
  var init_reimport = () => {};
8472
8472
 
8473
8473
  // src/alive.ts
8474
- import process6 from "node:process";
8474
+ import process7 from "node:process";
8475
+ function exitOf(child) {
8476
+ if (child.exitCode !== null || child.signalCode !== null) {
8477
+ return Promise.resolve({ code: child.exitCode, signal: child.signalCode });
8478
+ }
8479
+ return new Promise((resolve) => {
8480
+ child.once("exit", (code, signal) => {
8481
+ resolve({ code, signal });
8482
+ });
8483
+ });
8484
+ }
8475
8485
  function alive(pid) {
8476
8486
  if (pid === undefined || pid === null) {
8477
8487
  return false;
8478
8488
  }
8479
8489
  try {
8480
- process6.kill(pid, 0);
8490
+ process7.kill(pid, 0);
8481
8491
  return true;
8482
8492
  } catch (error) {
8483
8493
  return error.code === "EPERM";
@@ -8489,9 +8499,9 @@ var init_alive = () => {};
8489
8499
  import { existsSync as existsSync8, mkdtempSync, readdirSync as readdirSync5, rmSync as rmSync4 } from "node:fs";
8490
8500
  import { tmpdir as tmpdir2 } from "node:os";
8491
8501
  import { join as join9 } from "node:path";
8492
- import process7 from "node:process";
8502
+ import process8 from "node:process";
8493
8503
  function scratchDirectory(kind) {
8494
- return mkdtempSync(join9(tmpdir2(), `gdharness-${kind}-${process7.pid}-`));
8504
+ return mkdtempSync(join9(tmpdir2(), `gdharness-${kind}-${process8.pid}-`));
8495
8505
  }
8496
8506
  function sweepAbandonedScratch(directory = tmpdir2()) {
8497
8507
  let entries;
@@ -8566,7 +8576,7 @@ function lastTryOnExit() {
8566
8576
  return;
8567
8577
  }
8568
8578
  lastTryWatched = true;
8569
- process7.once("exit", () => {
8579
+ process8.once("exit", () => {
8570
8580
  for (const path of [...waiting, ...atExit]) {
8571
8581
  try {
8572
8582
  remove(path);
@@ -9743,7 +9753,7 @@ var init_server_version = __esm(() => {
9743
9753
 
9744
9754
  // src/issues.ts
9745
9755
  import { createHash as createHash2 } from "node:crypto";
9746
- import process8 from "node:process";
9756
+ import process9 from "node:process";
9747
9757
  function feedbackNotice(every) {
9748
9758
  return JSON.stringify({
9749
9759
  gdharness_feedback: {
@@ -9754,9 +9764,9 @@ function feedbackNotice(every) {
9754
9764
  }, null, 2);
9755
9765
  }
9756
9766
  function runtime() {
9757
- const versions = process8.versions;
9767
+ const versions = process9.versions;
9758
9768
  const bun = versions["bun"];
9759
- return bun === undefined ? `node ${process8.versions.node}` : `bun ${bun}`;
9769
+ return bun === undefined ? `node ${process9.versions.node}` : `bun ${bun}`;
9760
9770
  }
9761
9771
  function defectSignature(where, message) {
9762
9772
  const normalised = `${where}
@@ -9768,7 +9778,7 @@ function facts(where, message, godotVersion) {
9768
9778
  ["Where", where],
9769
9779
  ["Error", message],
9770
9780
  ["Version", `gdharness ${SERVER_VERSION}`],
9771
- ["Runtime", `${runtime()}, ${process8.platform} ${process8.arch}`],
9781
+ ["Runtime", `${runtime()}, ${process9.platform} ${process9.arch}`],
9772
9782
  ["Godot", godotVersion ?? "not known at the point this failed"],
9773
9783
  ["Signature", defectSignature(where, message)]
9774
9784
  ];
@@ -9778,7 +9788,7 @@ function filledTemplate(where, message, godotVersion) {
9778
9788
  `**gdharness version**: ${SERVER_VERSION}`,
9779
9789
  `**Godot version**: ${godotVersion ?? ""}`,
9780
9790
  `**Bun version**: ${runtime()}`,
9781
- `**OS**: ${process8.platform} ${process8.arch}`,
9791
+ `**OS**: ${process9.platform} ${process9.arch}`,
9782
9792
  "**MCP client**:",
9783
9793
  "",
9784
9794
  "## What you did",
@@ -10526,13 +10536,16 @@ function isToolScript(source) {
10526
10536
  function declaredClasses(projectPath) {
10527
10537
  const declared = new Map;
10528
10538
  eachScript(projectPath, (script, source) => {
10529
- const found = DECLARATION.exec(source);
10530
- if (found?.[1]) {
10531
- declared.set(found[1], script);
10539
+ const found = declaredClassOf(source);
10540
+ if (found !== null) {
10541
+ declared.set(found, script);
10532
10542
  }
10533
10543
  });
10534
10544
  return declared;
10535
10545
  }
10546
+ function declaredClassOf(source) {
10547
+ return DECLARATION.exec(source)?.[1] ?? null;
10548
+ }
10536
10549
  function eachScript(projectPath, each) {
10537
10550
  const visit = (directory, prefix) => {
10538
10551
  if (existsSync11(join13(directory, ".gdignore"))) {
@@ -11654,6 +11667,22 @@ function shippedAddonsDirectory() {
11654
11667
  function shippedOperationsScript() {
11655
11668
  return join15(dirname8(fileURLToPath(import.meta.url)), "godot", "operations", "godot_operations.gd");
11656
11669
  }
11670
+ function shippedListedRunner() {
11671
+ return join15(dirname8(fileURLToPath(import.meta.url)), "godot", "tests", "listed.gd");
11672
+ }
11673
+ function gdUnit4Unusable(projectPath) {
11674
+ const marker = "addons/gdUnit4/plugin.cfg";
11675
+ if (!existsSync13(join15(projectPath, ...marker.split("/")))) {
11676
+ return { missing: true, reason: `gdUnit4 is not installed in this project: no ${marker}` };
11677
+ }
11678
+ if (!existsSync13(join15(projectPath, ...GDUNIT4_CI_RUNNER.split("/")))) {
11679
+ return {
11680
+ missing: false,
11681
+ reason: `gdUnit4 in this project is older than 6.0, which the shipped runner needs: no ${GDUNIT4_CI_RUNNER}`
11682
+ };
11683
+ }
11684
+ return null;
11685
+ }
11657
11686
  function installAddons(projectPath, from = shippedAddonsDirectory()) {
11658
11687
  const installed = [];
11659
11688
  for (const name of ADDONS) {
@@ -11827,7 +11856,7 @@ function inspectProject(projectPath) {
11827
11856
  problems
11828
11857
  };
11829
11858
  }
11830
- var ADDONS, EDITOR_PLUGINS, RUNTIME_AUTOLOAD, SCRIPT_RUNS_SETTING = "gdharness/runtime/serve_script_runs", BIND_ADDRESS_SETTING = "gdharness/runtime/bind_address", PORT_SETTING = "gdharness/runtime/port", DIGEST_MARKER = ".gdharness-digest", RUNTIME_ADDON = "gdharness_runtime", shippedDigest, shippedRuntime;
11859
+ var ADDONS, EDITOR_PLUGINS, RUNTIME_AUTOLOAD, SCRIPT_RUNS_SETTING = "gdharness/runtime/serve_script_runs", BIND_ADDRESS_SETTING = "gdharness/runtime/bind_address", PORT_SETTING = "gdharness/runtime/port", DIGEST_MARKER = ".gdharness-digest", RUNTIME_ADDON = "gdharness_runtime", shippedDigest, shippedRuntime, GDUNIT4_CI_RUNNER = "addons/gdUnit4/src/core/runners/GdUnitTestCIRunner.gd";
11831
11860
  var init_setup = __esm(() => {
11832
11861
  init_class_cache();
11833
11862
  init_headless();
@@ -13011,8 +13040,9 @@ var init_tool_definitions = __esm(() => {
13011
13040
  },
13012
13041
  {
13013
13042
  name: "editor_launch",
13014
- title: "Open, restart or close the editor",
13015
- 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 names what that save took out of project.godot, and is left out when it took nothing; settingsNote says so when the file could not be read to tell. 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.",
13043
+ title: "Open, restart or close the editor, or bind its debug adapter again",
13044
+ description: "Opens the Godot editor on a project, or restarts or closes the one already connected, or binds its debug adapter again. 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; an editor this server opened that has not connected yet has nothing of this server's to save, so close stops its process instead. 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. 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. Godot tries each server port once, as the editor starts, so a restart binds a debug adapter that came back without its port again, in the editor that came back. rebind does the same for the connected editor without restarting it, whoever opened it; a debug session open on the adapter ends with it and is opened again by the next debug call. 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 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 in editor_status rather than waiting for an editor that is not coming, and open finishes it. 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.",
13045
+ answer: "close answers settingsDropped, naming what its save took out of project.godot and left out when it took nothing, and settingsNote when the file could not be read to tell; an editor stopped before it connected is answered with beforeConnecting. restart answers with the version that came back, and says under settingsDropped which keys its save dropped, 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 answers lspListening and dapListening once the editor's servers are up, and debugAdapterRebound says whether binding the debug adapter again took. serversSaid gives the lines the editor's log panel has about its servers, the one place Godot says why a port would not bind. Godot clears that panel whenever a game is played, unless its run/output/always_clear_output_on_play setting is off, so after a play the lines from the editor's own start are gone. rebind answers rebound, with lspListening, dapListening and serversSaid. A restart that a server's end interrupted shows as restartInterrupted in editor_status, and the open that finishes it says so under finishesRestart.",
13016
13046
  parameters: {
13017
13047
  projectPath: { ...PROJECT_PATH, ops: ["open"] },
13018
13048
  hidden: {
@@ -13038,6 +13068,11 @@ var init_tool_definitions = __esm(() => {
13038
13068
  effect: "destructive",
13039
13069
  summary: "save and quit the connected editor, and wait for it to go",
13040
13070
  requires: []
13071
+ },
13072
+ rebind: {
13073
+ effect: "destructive",
13074
+ summary: "stop and start the connected editor's debug adapter in place, ending its debug sessions, and answer whether it listens",
13075
+ requires: []
13041
13076
  }
13042
13077
  },
13043
13078
  defaultOperation: "open"
@@ -13248,8 +13283,8 @@ var init_tool_definitions = __esm(() => {
13248
13283
  name: "editor_status",
13249
13284
  title: "Editor status",
13250
13285
  effect: "read",
13251
- 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 editorCannotConnect, 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 a connected editor and on each running game (absent with neither), says it runs addon code other than what this server ships, and staleNote says which half moves: the project, by a gdharness upgrade it spells out to run in a shell, the editor by a restart, or this server by a reconnect. 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.",
13252
- answer: "projectIs, when present, is the gdharness version of the addons in the project, which differs from this server's (project_info says which of the two to move), and configIs, when present, the version the project's MCP config starts, which differs from this server's, so the next connection runs that one. 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. Its play says which play the editor is on: play.play counts the plays that editor has started since it opened, whoever started them, and play.editorPid is the editor, so two reads with different pairs are two plays even when both say playing. 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 the gdharness upgrade command, spelled out to run in a shell, 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. A runtime_* call to such a game is refused with the staleNote and not sent. 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 editorCannotConnect is a project without the gdharness editor addon installed or enabled, where no editor can ever dial in, however young the server or whichever editor is open; its remedies say how to install or enable it. 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 and took a key out of it, 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. A stale editor is asked nothing but to quit: every other call to it is refused with staleNote, which names editor_launch restart, the upgrade command or a reconnect. 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 from the socket opening until its greeting and that read are done, 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. serverVersion is the gdharness version of this server. bridgeAvailable says the editor bridge is listening; when it failed to start, startupError gives the error, and retryingBridge is true while this server tries again. announcedAt is the file where this server told editors of its project which port to dial. connectedAt is when the editor connected, and lastPongAt when it last answered this server's keep-alive ping. addonDigest is a hash of the editor addon's code, which addonIsStale compares with what this server ships. debugPort is the port of the editor's remote debugger, the one games it plays connect to. openedByAServer says a gdharness server started this editor, and syncsBreakpoints that it keeps its breakpoints when a debug session opens, which Godot otherwise clears. pendingRequests counts the requests sent to the editor and not yet answered, and queuedResources the files with writes waiting behind another write to the same file.",
13286
+ 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 editorCannotConnect, 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 a connected editor and on each running game (absent with neither), says it runs addon code other than what this server ships, and staleNote says which half moves: the project, by a gdharness upgrade it spells out to run in a shell, the editor by a restart, or this server by a reconnect. A false editor.lspListening or editor.dapListening comes with serversNote: editor_launch rebind binds the debug adapter again in place, and editor_launch restart the language server. 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.",
13287
+ answer: "projectIs, when present, is the gdharness version of the addons in the project, which differs from this server's (project_info says which of the two to move), and configIs, when present, the version the project's MCP config starts, which differs from this server's, so the next connection runs that one. 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. Its play says which play the editor is on: play.play counts the plays that editor has started since it opened, whoever started them, and play.editorPid is the editor, so two reads with different pairs are two plays even when both say playing. 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: editor_launch rebind binds the debug adapter again in place, and editor_launch restart the language server. 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 the gdharness upgrade command, spelled out to run in a shell, 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. A runtime_* call to such a game is refused with the staleNote and not sent. 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 editorCannotConnect is a project without the gdharness editor addon installed or enabled, where no editor can ever dial in, however young the server or whichever editor is open; its remedies say how to install or enable it. 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 and took a key out of it, 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. A stale editor is asked nothing but to quit: every other call to it is refused with staleNote, which names editor_launch restart, the upgrade command or a reconnect. 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 from the socket opening until its greeting and that read are done, 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. serverVersion is the gdharness version of this server. bridgeAvailable says the editor bridge is listening; when it failed to start, startupError gives the error, and retryingBridge is true while this server tries again. announcedAt is the file where this server told editors of its project which port to dial. connectedAt is when the editor connected, and lastPongAt when it last answered this server's keep-alive ping. addonDigest is a hash of the editor addon's code, which addonIsStale compares with what this server ships. debugPort is the port of the editor's remote debugger, the one games it plays connect to. openedByAServer says a gdharness server started this editor, and syncsBreakpoints that it keeps its breakpoints when a debug session opens, which Godot otherwise clears. pendingRequests counts the requests sent to the editor and not yet answered, and queuedResources the files with writes waiting behind another write to the same file.",
13253
13288
  parameters: {},
13254
13289
  requires: []
13255
13290
  },
@@ -14069,6 +14104,361 @@ var init_tool_reference = __esm(() => {
14069
14104
  init_tool_definitions();
14070
14105
  });
14071
14106
 
14107
+ // src/desktop.ts
14108
+ import { createHash as createHash4 } from "node:crypto";
14109
+ import {
14110
+ closeSync,
14111
+ existsSync as existsSync15,
14112
+ mkdtempSync as mkdtempSync2,
14113
+ openSync,
14114
+ readFileSync as readFileSync12,
14115
+ rmSync as rmSync7,
14116
+ statSync as statSync8,
14117
+ writeFileSync as writeFileSync8,
14118
+ writeSync
14119
+ } from "node:fs";
14120
+ import { tmpdir as tmpdir4 } from "node:os";
14121
+ import { join as join17 } from "node:path";
14122
+ import { setTimeout as delay } from "node:timers/promises";
14123
+ function helperAssembly(directory = tmpdir4()) {
14124
+ return join17(directory, `gdharness-desktop-${DIGEST}.dll`);
14125
+ }
14126
+ async function ensureHelperCompiled(directory = tmpdir4(), budgetMs = HELPER_COMPILE_MS, wait = true) {
14127
+ const assembly = helperAssembly(directory);
14128
+ const lock = `${assembly}.compiling`;
14129
+ const until = Date.now() + budgetMs;
14130
+ while (Date.now() < until) {
14131
+ if (existsSync15(assembly)) {
14132
+ return null;
14133
+ }
14134
+ let mine = false;
14135
+ try {
14136
+ const held = openSync(lock, "wx");
14137
+ writeSync(held, String(process.pid));
14138
+ closeSync(held);
14139
+ mine = true;
14140
+ } catch {}
14141
+ if (mine) {
14142
+ try {
14143
+ return await compileHelper(directory, assembly, until - Date.now());
14144
+ } finally {
14145
+ rmSync7(lock, { force: true });
14146
+ }
14147
+ }
14148
+ try {
14149
+ const holder = Number(readFileSync12(lock, "utf8").trim());
14150
+ const old = Date.now() - statSync8(lock).mtimeMs > budgetMs;
14151
+ if (old || Number.isInteger(holder) && holder > 0 && !alive(holder)) {
14152
+ rmSync7(lock, { force: true });
14153
+ continue;
14154
+ }
14155
+ } catch {}
14156
+ if (!wait) {
14157
+ return null;
14158
+ }
14159
+ await delay(200);
14160
+ }
14161
+ return `the desktop helper was not compiled within ${String(budgetMs)} ms`;
14162
+ }
14163
+ async function compileHelper(directory, assembly, budgetMs) {
14164
+ const scratch = mkdtempSync2(join17(directory, "gdharness-desktop-source-"));
14165
+ const source = join17(scratch, "helper.cs");
14166
+ writeFileSync8(source, SOURCE, "utf8");
14167
+ const literal = (path) => `'${path.replaceAll("'", "''")}'`;
14168
+ const script = [
14169
+ "$ErrorActionPreference = 'Stop'",
14170
+ "try {",
14171
+ ` $partial = ${literal(assembly)} + '.' + $PID`,
14172
+ ` Add-Type -TypeDefinition ([IO.File]::ReadAllText(${literal(source)}, [Text.Encoding]::UTF8)) -OutputAssembly $partial`,
14173
+ ` Move-Item -LiteralPath $partial -Destination ${literal(assembly)}`,
14174
+ "} catch {",
14175
+ " [Console]::Error.WriteLine($_.Exception.Message)",
14176
+ " exit 1",
14177
+ "}"
14178
+ ].join(`
14179
+ `);
14180
+ try {
14181
+ return await new Promise((resolve) => {
14182
+ const compiler = spawnHidden("powershell.exe", [
14183
+ "-NoProfile",
14184
+ "-NonInteractive",
14185
+ "-ExecutionPolicy",
14186
+ "Bypass",
14187
+ "-EncodedCommand",
14188
+ Buffer.from(script, "utf16le").toString("base64")
14189
+ ], { stdio: ["ignore", "ignore", "pipe"] });
14190
+ let complained = "";
14191
+ compiler.stderr.on("data", (chunk) => {
14192
+ complained += chunk.toString();
14193
+ });
14194
+ const timer = setTimeout(() => {
14195
+ compiler.kill();
14196
+ }, budgetMs);
14197
+ compiler.once("error", (error) => {
14198
+ clearTimeout(timer);
14199
+ resolve(`Windows PowerShell did not start: ${error.message}`);
14200
+ });
14201
+ compiler.once("close", (code) => {
14202
+ clearTimeout(timer);
14203
+ resolve(existsSync15(assembly) ? null : `Windows PowerShell could not compile it${code === null ? " in time" : ""}: ${complained.trim() || `exit code ${String(code)}`}`);
14204
+ });
14205
+ });
14206
+ } finally {
14207
+ rmSync7(scratch, { recursive: true, force: true });
14208
+ }
14209
+ }
14210
+ function quoted(argument) {
14211
+ if (argument !== "" && !/[\s"]/.test(argument)) {
14212
+ return argument;
14213
+ }
14214
+ let text = '"';
14215
+ let backslashes = 0;
14216
+ for (const character of argument) {
14217
+ if (character === "\\") {
14218
+ backslashes += 1;
14219
+ continue;
14220
+ }
14221
+ text += character === '"' ? `${"\\".repeat(backslashes * 2 + 1)}"` : `${"\\".repeat(backslashes)}${character}`;
14222
+ backslashes = 0;
14223
+ }
14224
+ return `${text}${"\\".repeat(backslashes * 2)}"`;
14225
+ }
14226
+ function windowsCommandLine(command, args) {
14227
+ return [command, ...args].map(quoted).join(" ");
14228
+ }
14229
+ function throughHelper(how, command, args, files, assembly = helperAssembly()) {
14230
+ const spec = {
14231
+ desktop: how.desktop ?? "",
14232
+ noActivate: how.noActivate === true,
14233
+ commandLine: windowsCommandLine(command, args),
14234
+ pidFile: files.pid,
14235
+ stopFile: files.stop,
14236
+ directory: how.directory,
14237
+ assembly
14238
+ };
14239
+ writeFileSync8(files.spec, JSON.stringify(spec), "utf8");
14240
+ const literal = (path) => `'${path.replaceAll("'", "''")}'`;
14241
+ const script = [
14242
+ "$ErrorActionPreference = 'Stop'",
14243
+ "try {",
14244
+ ` $spec = [IO.File]::ReadAllText(${literal(files.spec)}, [Text.Encoding]::UTF8) | ConvertFrom-Json`,
14245
+ " Add-Type -LiteralPath $spec.assembly",
14246
+ " $code = [GdharnessDesktop]::Run($spec.desktop, $spec.commandLine, $spec.pidFile, [bool]$spec.noActivate, $spec.stopFile, $spec.directory)",
14247
+ "} catch {",
14248
+ ` [IO.File]::WriteAllText(${literal(files.error)}, $_.Exception.Message)`,
14249
+ " exit 1",
14250
+ "}",
14251
+ "exit $code"
14252
+ ].join(`
14253
+ `);
14254
+ return {
14255
+ command: "powershell.exe",
14256
+ args: [
14257
+ "-NoProfile",
14258
+ "-NonInteractive",
14259
+ "-ExecutionPolicy",
14260
+ "Bypass",
14261
+ "-EncodedCommand",
14262
+ Buffer.from(script, "utf16le").toString("base64")
14263
+ ]
14264
+ };
14265
+ }
14266
+ var HIDDEN_DESKTOP = "gdharness", DESKTOP_VARIABLE = "GDHARNESS_DESKTOP", SOURCE = `using System;
14267
+ using System.ComponentModel;
14268
+ using System.IO;
14269
+ using System.Runtime.InteropServices;
14270
+ using System.Text;
14271
+
14272
+ public static class GdharnessDesktop {
14273
+ const uint GenericAll = 0x10000000;
14274
+ const int UseStdHandles = 0x100;
14275
+ const int UseShowWindow = 0x1;
14276
+ const short ShowNoActivate = 4;
14277
+ // No console at all, rather than a hidden one: a console's host would sit on the desktop too.
14278
+ const uint DetachedProcess = 0x00000008;
14279
+ const uint Infinite = 0xFFFFFFFF;
14280
+
14281
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
14282
+ struct StartupInfo {
14283
+ public int cb;
14284
+ public string reserved;
14285
+ public string desktop;
14286
+ public string title;
14287
+ public int x, y, xSize, ySize, xCount, yCount, fill, flags;
14288
+ public short show, reservedSize;
14289
+ public IntPtr reservedBytes, input, output, error;
14290
+ }
14291
+
14292
+ [StructLayout(LayoutKind.Sequential)]
14293
+ struct Started {
14294
+ public IntPtr process, thread;
14295
+ public int processId, threadId;
14296
+ }
14297
+
14298
+ [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
14299
+ static extern IntPtr CreateDesktopW(string name, IntPtr device, IntPtr mode, uint flags, uint access, IntPtr attributes);
14300
+
14301
+ [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
14302
+ static extern bool CreateProcessW(string application, StringBuilder commandLine, IntPtr processAttributes, IntPtr threadAttributes, bool inherit, uint flags, IntPtr environment, string directory, ref StartupInfo startup, out Started started);
14303
+
14304
+ [DllImport("kernel32.dll")] static extern IntPtr GetStdHandle(int which);
14305
+ [DllImport("kernel32.dll")] static extern uint WaitForSingleObject(IntPtr handle, uint milliseconds);
14306
+ [DllImport("kernel32.dll")] static extern bool GetExitCodeProcess(IntPtr handle, out uint code);
14307
+ [DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr handle);
14308
+ [DllImport("kernel32.dll")] static extern bool TerminateProcess(IntPtr handle, uint code);
14309
+ [DllImport("kernel32.dll")] static extern uint SetErrorMode(uint mode);
14310
+ const uint FailCriticalErrors = 0x1;
14311
+ const uint NoOpenFileErrorBox = 0x8000;
14312
+
14313
+ delegate bool EachWindow(IntPtr window, IntPtr unused);
14314
+ [DllImport("user32.dll")] static extern bool EnumWindows(EachWindow each, IntPtr unused);
14315
+ [DllImport("user32.dll")] static extern bool IsWindowVisible(IntPtr window);
14316
+ [DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr window, out uint processId);
14317
+ [DllImport("user32.dll")] static extern bool LockSetForegroundWindow(uint code);
14318
+ const uint LockForeground = 1;
14319
+ const uint UnlockForeground = 2;
14320
+ const uint TimedOut = 0x102;
14321
+
14322
+ static bool ShowsAWindow(int processId) {
14323
+ bool shown = false;
14324
+ EnumWindows(delegate(IntPtr window, IntPtr unused) {
14325
+ uint owner;
14326
+ GetWindowThreadProcessId(window, out owner);
14327
+ if (owner == (uint)processId && IsWindowVisible(window)) {
14328
+ shown = true;
14329
+ return false;
14330
+ }
14331
+ return true;
14332
+ }, IntPtr.Zero);
14333
+ return shown;
14334
+ }
14335
+
14336
+ // Keeps the foreground where it is while the process starts: its own call to take it fails while
14337
+ // the foreground is locked, and the user's own Alt+Tab or click still moves it. Released once its
14338
+ // window has been showing for a while, since a program takes the foreground as it shows its window
14339
+ // and not afterwards, or when it exits, or after two minutes whatever happens.
14340
+ static void HoldTheForeground(Started started) {
14341
+ DateTime until = DateTime.UtcNow.AddMinutes(2);
14342
+ DateTime? shownAt = null;
14343
+ while (DateTime.UtcNow < until) {
14344
+ if (WaitForSingleObject(started.process, 100) != TimedOut) {
14345
+ return;
14346
+ }
14347
+ if (shownAt == null && ShowsAWindow(started.processId)) {
14348
+ shownAt = DateTime.UtcNow;
14349
+ }
14350
+ if (shownAt != null && DateTime.UtcNow - shownAt.Value > TimeSpan.FromSeconds(10)) {
14351
+ return;
14352
+ }
14353
+ }
14354
+ }
14355
+
14356
+ // Starts the command line, both of its streams on this process's output, writes its pid, and
14357
+ // waits for it: the answer is its exit code. On the named desktop when there is one, whose handle
14358
+ // is held until then, since a desktop goes when the last handle to it does, and on this process's
14359
+ // own otherwise. With noActivate its first window is shown without being activated, which is
14360
+ // what a program's first ShowWindow does whatever it asks for when the start names a show state,
14361
+ // and the foreground is held while it starts, since Godot also asks for it outright. The process
14362
+ // is ended through the handle held here once stopFile appears, which is how a keeper ends a game
14363
+ // it did not start itself: a handle names one process, where a pid can name the next one too. The
14364
+ // process starts in directory rather than in this one's, which is a temporary directory: Windows
14365
+ // PowerShell saves its module cache relative to where it runs when the profile does not resolve.
14366
+ public static int Run(string desktop, string commandLine, string pidFile, bool noActivate, string stopFile, string directory) {
14367
+ StartupInfo startup = new StartupInfo();
14368
+ startup.cb = Marshal.SizeOf(typeof(StartupInfo));
14369
+ if (desktop.Length > 0) {
14370
+ IntPtr held = CreateDesktopW(desktop, IntPtr.Zero, IntPtr.Zero, 0, GenericAll, IntPtr.Zero);
14371
+ if (held == IntPtr.Zero) {
14372
+ throw new Win32Exception(Marshal.GetLastWin32Error(), "CreateDesktopW " + desktop);
14373
+ }
14374
+ startup.desktop = desktop;
14375
+ }
14376
+ startup.flags = UseStdHandles;
14377
+ if (noActivate) {
14378
+ startup.flags |= UseShowWindow;
14379
+ startup.show = ShowNoActivate;
14380
+ }
14381
+ startup.input = GetStdHandle(-10);
14382
+ startup.output = GetStdHandle(-11);
14383
+ startup.error = startup.output;
14384
+ Started started;
14385
+ // Inherited by what it starts: without SEM_NOGPFAULTERRORBOX, Windows Error Reporting runs when
14386
+ // the target crashes and writes its dump, and with the other two no hard-error box can open.
14387
+ // Set outright rather than cleared from this process's own: Windows PowerShell changes the mode
14388
+ // it inherited as it starts, and was seen reading 0x1 here, without the open-file bit.
14389
+ SetErrorMode(FailCriticalErrors | NoOpenFileErrorBox);
14390
+ if (noActivate) {
14391
+ LockSetForegroundWindow(LockForeground);
14392
+ }
14393
+ try {
14394
+ if (!CreateProcessW(null, new StringBuilder(commandLine), IntPtr.Zero, IntPtr.Zero, true, DetachedProcess, IntPtr.Zero, directory, ref startup, out started)) {
14395
+ throw new Win32Exception(Marshal.GetLastWin32Error(), "CreateProcessW " + commandLine);
14396
+ }
14397
+ File.WriteAllText(pidFile, started.processId.ToString());
14398
+ if (noActivate) {
14399
+ HoldTheForeground(started);
14400
+ }
14401
+ } finally {
14402
+ if (noActivate) {
14403
+ LockSetForegroundWindow(UnlockForeground);
14404
+ }
14405
+ }
14406
+ if (stopFile.Length == 0) {
14407
+ WaitForSingleObject(started.process, Infinite);
14408
+ } else {
14409
+ bool stopped = false;
14410
+ while (WaitForSingleObject(started.process, 100) == TimedOut) {
14411
+ if (!stopped && File.Exists(stopFile)) {
14412
+ TerminateProcess(started.process, 1);
14413
+ stopped = true;
14414
+ }
14415
+ }
14416
+ }
14417
+ uint code;
14418
+ GetExitCodeProcess(started.process, out code);
14419
+ CloseHandle(started.thread);
14420
+ CloseHandle(started.process);
14421
+ return unchecked((int)code);
14422
+ }
14423
+ }
14424
+ `, DIGEST, HELPER_COMPILE_MS = 180000;
14425
+ var init_desktop = __esm(() => {
14426
+ init_alive();
14427
+ init_spawn();
14428
+ DIGEST = createHash4("sha256").update(SOURCE).digest("hex").slice(0, 16);
14429
+ });
14430
+
14431
+ // src/whole-file.ts
14432
+ import { renameSync as renameSync2, rmSync as rmSync9, writeFileSync as writeFileSync9 } from "node:fs";
14433
+ function writeWhole(path, text) {
14434
+ const partial = `${path}.${process.pid}.partial`;
14435
+ try {
14436
+ writeFileSync9(partial, text, "utf8");
14437
+ } catch {
14438
+ return false;
14439
+ }
14440
+ const until = Date.now() + HELD_FILE_MS;
14441
+ for (;; ) {
14442
+ try {
14443
+ renameSync2(partial, path);
14444
+ return true;
14445
+ } catch (error) {
14446
+ const code = error.code ?? "";
14447
+ if (!HELD_BY_A_READER.has(code) || Date.now() >= until) {
14448
+ try {
14449
+ rmSync9(partial, { force: true });
14450
+ } catch {}
14451
+ return false;
14452
+ }
14453
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, HELD_FILE_RETRY_MS);
14454
+ }
14455
+ }
14456
+ }
14457
+ var HELD_FILE_MS = 1000, HELD_FILE_RETRY_MS = 10, HELD_BY_A_READER;
14458
+ var init_whole_file = __esm(() => {
14459
+ HELD_BY_A_READER = new Set(["EPERM", "EACCES", "EBUSY"]);
14460
+ });
14461
+
14072
14462
  // node_modules/@modelcontextprotocol/server/dist/chunk-Br0eD_fh.mjs
14073
14463
  var __create2, __defProp2, __getOwnPropDesc, __getOwnPropNames2, __getProtoOf2, __hasOwnProp2, __commonJSMin = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports), __exportAll = (all, symbols) => {
14074
14464
  let target = {};
@@ -33667,7 +34057,7 @@ var init_ajvProvider_CEoC__sr = __esm(() => {
33667
34057
  });
33668
34058
 
33669
34059
  // node_modules/@modelcontextprotocol/server/dist/shimsNode.mjs
33670
- import process10 from "node:process";
34060
+ import process13 from "node:process";
33671
34061
  var init_shimsNode = __esm(() => {
33672
34062
  init_ajvProvider_CEoC__sr();
33673
34063
  });
@@ -35481,7 +35871,7 @@ var swallowsErrorsAfterClose, StdioServerTransport = class {
35481
35871
  _readBuffer;
35482
35872
  _started = false;
35483
35873
  _closed = false;
35484
- constructor(_stdin = process10.stdin, _stdout = process10.stdout, options) {
35874
+ constructor(_stdin = process13.stdin, _stdout = process13.stdout, options) {
35485
35875
  this._stdin = _stdin;
35486
35876
  this._stdout = _stdout;
35487
35877
  this._readBuffer = new ReadBuffer({ maxBufferSize: options?.maxBufferSize });
@@ -35670,13 +36060,13 @@ var init_stdio = __esm(() => {
35670
36060
  });
35671
36061
 
35672
36062
  // src/autoload-failure.ts
35673
- import { existsSync as existsSync15, readFileSync as readFileSync13 } from "node:fs";
35674
- import { join as join18 } from "node:path";
36063
+ import { existsSync as existsSync19, readFileSync as readFileSync18 } from "node:fs";
36064
+ import { join as join22 } from "node:path";
35675
36065
  function scriptsAutoloaded(projectPath) {
35676
36066
  const scripts = new Map;
35677
36067
  let settings;
35678
36068
  try {
35679
- settings = parseProjectGodot(readFileSync13(join18(projectPath, "project.godot"), "utf8"));
36069
+ settings = parseProjectGodot(readFileSync18(join22(projectPath, "project.godot"), "utf8"));
35680
36070
  } catch {
35681
36071
  return scripts;
35682
36072
  }
@@ -35685,12 +36075,12 @@ function scriptsAutoloaded(projectPath) {
35685
36075
  scripts.set(`res://${path}`, name);
35686
36076
  continue;
35687
36077
  }
35688
- const scene = join18(projectPath, path);
35689
- if (!existsSync15(scene)) {
36078
+ const scene = join22(projectPath, path);
36079
+ if (!existsSync19(scene)) {
35690
36080
  continue;
35691
36081
  }
35692
36082
  try {
35693
- for (const named of readFileSync13(scene, "utf8").matchAll(/^\[ext_resource\b[^\]]*\btype="Script"[^\]]*\bpath="(res:\/\/[^"]+)"/gm)) {
36083
+ for (const named of readFileSync18(scene, "utf8").matchAll(/^\[ext_resource\b[^\]]*\btype="Script"[^\]]*\bpath="(res:\/\/[^"]+)"/gm)) {
35694
36084
  scripts.set(named[1] ?? "", name);
35695
36085
  }
35696
36086
  } catch {}
@@ -35709,31 +36099,31 @@ var init_autoload_failure = __esm(() => {
35709
36099
  });
35710
36100
 
35711
36101
  // src/runtime-client.ts
35712
- import { existsSync as existsSync16, readdirSync as readdirSync10, readFileSync as readFileSync14, statSync as statSync8, unlinkSync as unlinkSync2 } from "node:fs";
36102
+ import { existsSync as existsSync20, readdirSync as readdirSync12, readFileSync as readFileSync19, statSync as statSync12, unlinkSync as unlinkSync2 } from "node:fs";
35713
36103
  import { createConnection } from "node:net";
35714
- import { tmpdir as tmpdir4 } from "node:os";
35715
- import { dirname as dirname11, join as join19, resolve as resolve4 } from "node:path";
35716
- import { setTimeout as delay } from "node:timers/promises";
36104
+ import { tmpdir as tmpdir5 } from "node:os";
36105
+ import { dirname as dirname12, join as join23, resolve as resolve5 } from "node:path";
36106
+ import { setTimeout as delay4 } from "node:timers/promises";
35717
36107
  function runtimeDirectory(variables = process.env) {
35718
36108
  const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
35719
36109
  if (explicit) {
35720
36110
  return explicit;
35721
36111
  }
35722
36112
  const perUser = envValue("XDG_RUNTIME_DIR", variables);
35723
- return join19(perUser ?? tmpdir4(), "gdharness");
36113
+ return join23(perUser ?? tmpdir5(), "gdharness");
35724
36114
  }
35725
36115
  function runtimeDirectories(variables = process.env) {
35726
36116
  const candidates = [runtimeDirectory(variables)];
35727
36117
  const fallbacks = process.platform === "win32" ? [envValue("SystemRoot", variables) ?? envValue("windir", variables) ?? "C:\\Windows"] : ["/tmp", "/var/tmp"];
35728
36118
  for (const base of fallbacks) {
35729
- candidates.push(join19(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
36119
+ candidates.push(join23(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
35730
36120
  }
35731
- return [...new Set(candidates.map((path) => resolve4(path)))];
36121
+ return [...new Set(candidates.map((path) => resolve5(path)))];
35732
36122
  }
35733
36123
  function errorReportOf(pid, directories = runtimeDirectories()) {
35734
36124
  for (const directory of directories) {
35735
- const path = join19(directory, `runtime-${pid}.log`);
35736
- if (existsSync16(path)) {
36125
+ const path = join23(directory, `runtime-${pid}.log`);
36126
+ if (existsSync20(path)) {
35737
36127
  return path;
35738
36128
  }
35739
36129
  }
@@ -35764,7 +36154,7 @@ function reportAlreadyInTranscript(transcript, report) {
35764
36154
  function parseAnnouncement(file, pid) {
35765
36155
  let fields;
35766
36156
  try {
35767
- fields = asParams(JSON.parse(readFileSync14(file, "utf8")));
36157
+ fields = asParams(JSON.parse(readFileSync19(file, "utf8")));
35768
36158
  } catch {
35769
36159
  return { kind: "rubbish" };
35770
36160
  }
@@ -35781,7 +36171,7 @@ function parseAnnouncement(file, pid) {
35781
36171
  };
35782
36172
  let announcedAt;
35783
36173
  try {
35784
- announcedAt = statSync8(file).mtimeMs;
36174
+ announcedAt = statSync12(file).mtimeMs;
35785
36175
  } catch {}
35786
36176
  const when = announcedAt === undefined ? {} : { announcedAt };
35787
36177
  if (protocol !== RUNTIME_PROTOCOL) {
@@ -35834,7 +36224,7 @@ function discoverRuntimes(directories = runtimeDirectories()) {
35834
36224
  function wentAway(file, pid) {
35835
36225
  let project = "";
35836
36226
  try {
35837
- const fields = asParams(JSON.parse(readFileSync14(file, "utf8")));
36227
+ const fields = asParams(JSON.parse(readFileSync19(file, "utf8")));
35838
36228
  project = readString(readParams(fields, "project") ?? {}, "path") ?? "";
35839
36229
  } catch {
35840
36230
  return;
@@ -35877,29 +36267,29 @@ function strangersAmong(candidates, now, startTimes = startTimesOf) {
35877
36267
  }).map((one) => one.file));
35878
36268
  }
35879
36269
  function announcedIn(directory) {
35880
- if (!existsSync16(directory)) {
36270
+ if (!existsSync20(directory)) {
35881
36271
  return [];
35882
36272
  }
35883
36273
  const found = [];
35884
36274
  const candidates = [];
35885
36275
  const entries = [];
35886
- for (const entry of readdirSync10(directory)) {
36276
+ for (const entry of readdirSync12(directory)) {
35887
36277
  const report = ERROR_REPORT_PATTERN.exec(entry);
35888
36278
  if (report) {
35889
- sweepErrorReport(join19(directory, entry), Number.parseInt(report[1] ?? "", 10));
36279
+ sweepErrorReport(join23(directory, entry), Number.parseInt(report[1] ?? "", 10));
35890
36280
  continue;
35891
36281
  }
35892
36282
  const match = ANNOUNCEMENT_PATTERN.exec(entry);
35893
36283
  if (!match) {
35894
36284
  continue;
35895
36285
  }
35896
- const file = join19(directory, entry);
36286
+ const file = join23(directory, entry);
35897
36287
  const pid = Number.parseInt(match[1] ?? "", 10);
35898
36288
  const running = alive(pid);
35899
36289
  entries.push({ file, pid, running });
35900
36290
  if (running) {
35901
36291
  try {
35902
- candidates.push({ file, pid, writtenAt: statSync8(file).mtimeMs });
36292
+ candidates.push({ file, pid, writtenAt: statSync12(file).mtimeMs });
35903
36293
  } catch {}
35904
36294
  }
35905
36295
  }
@@ -35921,16 +36311,16 @@ function announcedIn(directory) {
35921
36311
  found.push(announced);
35922
36312
  }
35923
36313
  const held = new Set(entries.map((one) => one.file));
35924
- const here = join19(directory, ".");
36314
+ const here = join23(directory, ".");
35925
36315
  for (const file of verdicts.keys()) {
35926
- if (dirname11(file) === here && !held.has(file)) {
36316
+ if (dirname12(file) === here && !held.has(file)) {
35927
36317
  verdicts.delete(file);
35928
36318
  }
35929
36319
  }
35930
36320
  return found;
35931
36321
  }
35932
36322
  async function announcementEnded(pid, directories = runtimeDirectories()) {
35933
- const files = directories.map((directory) => join19(directory, `runtime-${pid}.json`)).filter((file) => existsSync16(file));
36323
+ const files = directories.map((directory) => join23(directory, `runtime-${pid}.json`)).filter((file) => existsSync20(file));
35934
36324
  const first = files[0];
35935
36325
  if (first === undefined) {
35936
36326
  return;
@@ -35941,7 +36331,7 @@ async function announcementEnded(pid, directories = runtimeDirectories()) {
35941
36331
  if (left <= 0) {
35942
36332
  return;
35943
36333
  }
35944
- await delay(Math.min(LOOK_EVERY_MS, left));
36334
+ await delay4(Math.min(LOOK_EVERY_MS, left));
35945
36335
  }
35946
36336
  wentAway(first, pid);
35947
36337
  for (const file of files) {
@@ -35956,7 +36346,7 @@ function sweepErrorReport(path, pid, now = Date.now()) {
35956
36346
  return;
35957
36347
  }
35958
36348
  try {
35959
- if (now - statSync8(path).mtimeMs >= ERROR_REPORT_KEEP_MS) {
36349
+ if (now - statSync12(path).mtimeMs >= ERROR_REPORT_KEEP_MS) {
35960
36350
  unlinkSync2(path);
35961
36351
  }
35962
36352
  } catch {}
@@ -35973,7 +36363,7 @@ async function announcedSince(projectPath, before, waiting = {}) {
35973
36363
  if (left <= 0 || waiting.giveUp?.() === true) {
35974
36364
  return [];
35975
36365
  }
35976
- await delay(Math.min(LOOK_EVERY_MS, left));
36366
+ await delay4(Math.min(LOOK_EVERY_MS, left));
35977
36367
  }
35978
36368
  }
35979
36369
  function describe2(endpoint) {
@@ -35986,7 +36376,7 @@ function theOneProject(endpoints) {
35986
36376
  if (endpoints.some((endpoint) => endpoint.project.path === "")) {
35987
36377
  return;
35988
36378
  }
35989
- const paths = new Set(endpoints.map((endpoint) => realAsFarAsItExists(resolve4(endpoint.project.path))));
36379
+ const paths = new Set(endpoints.map((endpoint) => realAsFarAsItExists(resolve5(endpoint.project.path))));
35990
36380
  const only = [...paths][0];
35991
36381
  return paths.size === 1 ? only : undefined;
35992
36382
  }
@@ -36017,7 +36407,7 @@ function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
36017
36407
  if (named !== undefined) {
36018
36408
  if (projectPath !== undefined && !isSameDirectory(named.project.path, projectPath)) {
36019
36409
  return {
36020
- problem: `The game with pid ${pid} is running ${named.project.path}, not ${resolve4(projectPath)}. Running: ${endpoints.map(describe2).join("; ")}.`
36410
+ problem: `The game with pid ${pid} is running ${named.project.path}, not ${resolve5(projectPath)}. Running: ${endpoints.map(describe2).join("; ")}.`
36021
36411
  };
36022
36412
  }
36023
36413
  return { endpoint: named };
@@ -36057,7 +36447,7 @@ function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
36057
36447
  };
36058
36448
  }
36059
36449
  if (projectPath !== undefined) {
36060
- const wanted = resolve4(projectPath);
36450
+ const wanted = resolve5(projectPath);
36061
36451
  const matching = endpoints.filter((endpoint) => isSameDirectory(endpoint.project.path, wanted));
36062
36452
  if (matching.length === 0) {
36063
36453
  return {
@@ -36349,14 +36739,14 @@ var init_runtime_client = __esm(() => {
36349
36739
  });
36350
36740
 
36351
36741
  // src/boot-note.ts
36352
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "node:fs";
36353
- import { dirname as dirname12, join as join20 } from "node:path";
36742
+ import { mkdirSync as mkdirSync9, readFileSync as readFileSync20, writeFileSync as writeFileSync11 } from "node:fs";
36743
+ import { dirname as dirname13, join as join24 } from "node:path";
36354
36744
  function bootNotePath(projectPath) {
36355
- return join20(projectPath, ".godot", "gdharness-boot.json");
36745
+ return join24(projectPath, ".godot", "gdharness-boot.json");
36356
36746
  }
36357
36747
  function readScenes(projectPath) {
36358
36748
  try {
36359
- return asParams(asParams(JSON.parse(readFileSync15(bootNotePath(projectPath), "utf8")))["scenes"]);
36749
+ return asParams(asParams(JSON.parse(readFileSync20(bootNotePath(projectPath), "utf8")))["scenes"]);
36360
36750
  } catch {
36361
36751
  return {};
36362
36752
  }
@@ -36372,7 +36762,7 @@ function readBootNote(projectPath, scene) {
36372
36762
  function writeBootNote(projectPath, scene, announcedAfterMs) {
36373
36763
  try {
36374
36764
  const path = bootNotePath(projectPath);
36375
- mkdirSync6(dirname12(path), { recursive: true });
36765
+ mkdirSync9(dirname13(path), { recursive: true });
36376
36766
  const scenes = {
36377
36767
  ...readScenes(projectPath),
36378
36768
  [scene]: {
@@ -36380,7 +36770,7 @@ function writeBootNote(projectPath, scene, announcedAfterMs) {
36380
36770
  at: new Date().toISOString()
36381
36771
  }
36382
36772
  };
36383
- writeFileSync8(path, JSON.stringify({ scenes }, null, 2), "utf8");
36773
+ writeFileSync11(path, JSON.stringify({ scenes }, null, 2), "utf8");
36384
36774
  } catch {}
36385
36775
  }
36386
36776
  function halfAsLongAgain(lastBootMs) {
@@ -36398,10 +36788,10 @@ var init_boot_note = __esm(() => {
36398
36788
  });
36399
36789
 
36400
36790
  // src/breakpoint-note.ts
36401
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync16, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "node:fs";
36402
- import { dirname as dirname13, isAbsolute as isAbsolute4, join as join21, relative as relative3 } from "node:path";
36791
+ import { mkdirSync as mkdirSync10, readFileSync as readFileSync21, rmSync as rmSync11, writeFileSync as writeFileSync12 } from "node:fs";
36792
+ import { dirname as dirname14, isAbsolute as isAbsolute5, join as join25, relative as relative4 } from "node:path";
36403
36793
  function breakpointNotePath(projectPath) {
36404
- return join21(projectPath, ".godot", "gdharness-breakpoints.json");
36794
+ return join25(projectPath, ".godot", "gdharness-breakpoints.json");
36405
36795
  }
36406
36796
  function writeBreakpointNote(projectPath, held) {
36407
36797
  const path = breakpointNotePath(projectPath);
@@ -36409,17 +36799,17 @@ function writeBreakpointNote(projectPath, held) {
36409
36799
  const inside = held.map((one) => ({ scriptPath: realAsFarAsItExists(one.scriptPath), lines: one.lines })).filter((one) => one.lines.length > 0 && withinProject(root, one.scriptPath));
36410
36800
  try {
36411
36801
  if (inside.length === 0) {
36412
- rmSync7(path, { force: true });
36802
+ rmSync11(path, { force: true });
36413
36803
  return;
36414
36804
  }
36415
36805
  const note = {
36416
36806
  breakpoints: inside.map((one) => ({
36417
- script: relative3(root, one.scriptPath).replaceAll("\\", "/"),
36807
+ script: relative4(root, one.scriptPath).replaceAll("\\", "/"),
36418
36808
  lines: [...one.lines]
36419
36809
  }))
36420
36810
  };
36421
- mkdirSync7(dirname13(path), { recursive: true });
36422
- writeFileSync9(path, `${JSON.stringify(note, null, 2)}
36811
+ mkdirSync10(dirname14(path), { recursive: true });
36812
+ writeFileSync12(path, `${JSON.stringify(note, null, 2)}
36423
36813
  `, "utf8");
36424
36814
  } catch (error) {
36425
36815
  console.error(`[SERVER] Could not record the breakpoints at ${path}: ${errorMessage(error)}`);
@@ -36427,7 +36817,7 @@ function writeBreakpointNote(projectPath, held) {
36427
36817
  }
36428
36818
  function readBreakpointNote(projectPath) {
36429
36819
  try {
36430
- const parsed = JSON.parse(readFileSync16(breakpointNotePath(projectPath), "utf8"));
36820
+ const parsed = JSON.parse(readFileSync21(breakpointNotePath(projectPath), "utf8"));
36431
36821
  if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.breakpoints)) {
36432
36822
  return [];
36433
36823
  }
@@ -36437,12 +36827,12 @@ function readBreakpointNote(projectPath) {
36437
36827
  continue;
36438
36828
  }
36439
36829
  const { script, lines: listed } = entry;
36440
- if (typeof script !== "string" || isAbsolute4(script) || script.split("/").includes("..")) {
36830
+ if (typeof script !== "string" || isAbsolute5(script) || script.split("/").includes("..")) {
36441
36831
  continue;
36442
36832
  }
36443
36833
  const lines = Array.isArray(listed) ? listed.filter((line) => Number.isInteger(line) && line > 0) : [];
36444
36834
  if (lines.length > 0) {
36445
- held.push({ scriptPath: realAsFarAsItExists(join21(projectPath, script)), lines });
36835
+ held.push({ scriptPath: realAsFarAsItExists(join25(projectPath, script)), lines });
36446
36836
  }
36447
36837
  }
36448
36838
  return held;
@@ -36451,54 +36841,23 @@ function readBreakpointNote(projectPath) {
36451
36841
  }
36452
36842
  }
36453
36843
  function withinProject(projectPath, scriptPath) {
36454
- const rel = relative3(projectPath, scriptPath);
36455
- return rel !== "" && !rel.startsWith("..") && !isAbsolute4(rel);
36844
+ const rel = relative4(projectPath, scriptPath);
36845
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute5(rel);
36456
36846
  }
36457
36847
  var init_breakpoint_note = __esm(() => {
36458
36848
  init_errors();
36459
36849
  init_paths();
36460
36850
  });
36461
36851
 
36462
- // src/whole-file.ts
36463
- import { renameSync as renameSync2, rmSync as rmSync8, writeFileSync as writeFileSync10 } from "node:fs";
36464
- function writeWhole(path, text) {
36465
- const partial = `${path}.${process.pid}.partial`;
36466
- try {
36467
- writeFileSync10(partial, text, "utf8");
36468
- } catch {
36469
- return false;
36470
- }
36471
- const until = Date.now() + HELD_FILE_MS;
36472
- for (;; ) {
36473
- try {
36474
- renameSync2(partial, path);
36475
- return true;
36476
- } catch (error) {
36477
- const code = error.code ?? "";
36478
- if (!HELD_BY_A_READER.has(code) || Date.now() >= until) {
36479
- try {
36480
- rmSync8(partial, { force: true });
36481
- } catch {}
36482
- return false;
36483
- }
36484
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, HELD_FILE_RETRY_MS);
36485
- }
36486
- }
36487
- }
36488
- var HELD_FILE_MS = 1000, HELD_FILE_RETRY_MS = 10, HELD_BY_A_READER;
36489
- var init_whole_file = __esm(() => {
36490
- HELD_BY_A_READER = new Set(["EPERM", "EACCES", "EBUSY"]);
36491
- });
36492
-
36493
36852
  // src/class-note.ts
36494
- import { mkdirSync as mkdirSync8, readFileSync as readFileSync17 } from "node:fs";
36495
- import { dirname as dirname14, join as join22 } from "node:path";
36853
+ import { mkdirSync as mkdirSync11, readFileSync as readFileSync22 } from "node:fs";
36854
+ import { dirname as dirname15, join as join26 } from "node:path";
36496
36855
  function classNotePath(projectPath) {
36497
- return join22(projectPath, ".godot", "gdharness-classes.json");
36856
+ return join26(projectPath, ".godot", "gdharness-classes.json");
36498
36857
  }
36499
36858
  function readClassNote(projectPath) {
36500
36859
  try {
36501
- const fields = asParams(JSON.parse(readFileSync17(classNotePath(projectPath), "utf8")));
36860
+ const fields = asParams(JSON.parse(readFileSync22(classNotePath(projectPath), "utf8")));
36502
36861
  const rebuiltWith = readArray(fields, "rebuiltWith");
36503
36862
  if (rebuiltWith === undefined || !rebuiltWith.every((name) => typeof name === "string")) {
36504
36863
  return null;
@@ -36528,7 +36887,7 @@ function unfinishedScanIn(value) {
36528
36887
  function writeClassNote(projectPath, note) {
36529
36888
  const path = classNotePath(projectPath);
36530
36889
  try {
36531
- mkdirSync8(dirname14(path), { recursive: true });
36890
+ mkdirSync11(dirname15(path), { recursive: true });
36532
36891
  } catch {
36533
36892
  return;
36534
36893
  }
@@ -36540,16 +36899,16 @@ var init_class_note = __esm(() => {
36540
36899
  });
36541
36900
 
36542
36901
  // src/config-pin.ts
36543
- import { existsSync as existsSync17, readFileSync as readFileSync18 } from "node:fs";
36544
- import { join as join23 } from "node:path";
36902
+ import { existsSync as existsSync21, readFileSync as readFileSync23 } from "node:fs";
36903
+ import { join as join27 } from "node:path";
36545
36904
  function versionsNamedInConfig(projectPath) {
36546
- const config = join23(projectPath, ".mcp.json");
36547
- if (!existsSync17(config)) {
36905
+ const config = join27(projectPath, ".mcp.json");
36906
+ if (!existsSync21(config)) {
36548
36907
  return [];
36549
36908
  }
36550
36909
  let text;
36551
36910
  try {
36552
- text = readFileSync18(config, "utf8");
36911
+ text = readFileSync23(config, "utf8");
36553
36912
  JSON.parse(text);
36554
36913
  } catch {
36555
36914
  return [];
@@ -36570,30 +36929,30 @@ var init_config_pin = __esm(() => {
36570
36929
  });
36571
36930
 
36572
36931
  // src/cut-test-run.ts
36573
- import { existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync19, rmSync as rmSync9, writeFileSync as writeFileSync11 } from "node:fs";
36574
- import { dirname as dirname15, join as join24 } from "node:path";
36932
+ import { existsSync as existsSync22, mkdirSync as mkdirSync12, readFileSync as readFileSync24, rmSync as rmSync12, writeFileSync as writeFileSync13 } from "node:fs";
36933
+ import { dirname as dirname16, join as join28 } from "node:path";
36575
36934
  function keptAt(projectPath) {
36576
- return join24(projectPath, ".godot", "gdharness-reports", "cut-run.json");
36935
+ return join28(projectPath, ".godot", "gdharness-reports", "cut-run.json");
36577
36936
  }
36578
36937
  function keepCutTestRun(projectPath, run) {
36579
36938
  const path = keptAt(projectPath);
36580
36939
  try {
36581
- mkdirSync9(dirname15(path), { recursive: true });
36582
- writeFileSync11(path, `${JSON.stringify(run, null, 2)}
36940
+ mkdirSync12(dirname16(path), { recursive: true });
36941
+ writeFileSync13(path, `${JSON.stringify(run, null, 2)}
36583
36942
  `, "utf8");
36584
36943
  } catch {}
36585
36944
  }
36586
36945
  function takeCutTestRun(projectPath) {
36587
36946
  const path = keptAt(projectPath);
36588
- if (!existsSync18(path)) {
36947
+ if (!existsSync22(path)) {
36589
36948
  return null;
36590
36949
  }
36591
36950
  try {
36592
- const run = JSON.parse(readFileSync19(path, "utf8"));
36593
- rmSync9(path, { force: true });
36951
+ const run = JSON.parse(readFileSync24(path, "utf8"));
36952
+ rmSync12(path, { force: true });
36594
36953
  return typeof run.cutAt === "string" && Array.isArray(run.paths) && run.paths.every((path) => typeof path === "string") ? run : null;
36595
36954
  } catch {
36596
- rmSync9(path, { force: true });
36955
+ rmSync12(path, { force: true });
36597
36956
  return null;
36598
36957
  }
36599
36958
  }
@@ -36733,7 +37092,7 @@ var init_ports = __esm(() => {
36733
37092
 
36734
37093
  // src/dap_client.ts
36735
37094
  import { createConnection as createConnection2 } from "node:net";
36736
- import { setTimeout as delay2 } from "node:timers/promises";
37095
+ import { setTimeout as delay5 } from "node:timers/promises";
36737
37096
  function fileKey(path) {
36738
37097
  return process.platform === "win32" ? path.replaceAll("\\", "/").toLowerCase() : path;
36739
37098
  }
@@ -37205,7 +37564,7 @@ class GodotDAPClient {
37205
37564
  await this.sendRequest(command, { threadId: resolvedThreadId });
37206
37565
  const deadline = Date.now() + this.stepPatienceMs;
37207
37566
  while (this.stops === before && this.connected && Date.now() < deadline) {
37208
- await delay2(50);
37567
+ await delay5(50);
37209
37568
  }
37210
37569
  return this.stops !== before;
37211
37570
  }
@@ -37231,7 +37590,7 @@ class GodotDAPClient {
37231
37590
  return answered;
37232
37591
  }
37233
37592
  } catch {}
37234
- await delay2(200);
37593
+ await delay5(200);
37235
37594
  }
37236
37595
  return null;
37237
37596
  }
@@ -37252,7 +37611,7 @@ class GodotDAPClient {
37252
37611
  break;
37253
37612
  }
37254
37613
  } catch {}
37255
- await delay2(200);
37614
+ await delay5(200);
37256
37615
  }
37257
37616
  if (scopes.length === 0) {
37258
37617
  throw new ReadRefusal(`The scopes of frame ${String(frame["id"])} did not arrive from the game within ${this.scopesPatienceMs / 1000}s. They come from the game through the editor, so a busy machine can take longer: ask again`);
@@ -37399,288 +37758,28 @@ var init_dap_client = __esm(() => {
37399
37758
  };
37400
37759
  });
37401
37760
 
37402
- // src/desktop.ts
37403
- import { createHash as createHash4 } from "node:crypto";
37404
- import {
37405
- closeSync,
37406
- existsSync as existsSync19,
37407
- mkdtempSync as mkdtempSync2,
37408
- openSync,
37409
- readFileSync as readFileSync20,
37410
- rmSync as rmSync10,
37411
- statSync as statSync9,
37412
- writeFileSync as writeFileSync12,
37413
- writeSync
37414
- } from "node:fs";
37415
- import { tmpdir as tmpdir5 } from "node:os";
37416
- import { join as join25 } from "node:path";
37417
- import { setTimeout as delay3 } from "node:timers/promises";
37418
- function helperAssembly(directory = tmpdir5()) {
37419
- return join25(directory, `gdharness-desktop-${DIGEST}.dll`);
37420
- }
37421
- async function ensureHelperCompiled(directory = tmpdir5(), budgetMs = HELPER_COMPILE_MS, wait = true) {
37422
- const assembly = helperAssembly(directory);
37423
- const lock = `${assembly}.compiling`;
37424
- const until = Date.now() + budgetMs;
37425
- while (Date.now() < until) {
37426
- if (existsSync19(assembly)) {
37427
- return null;
37428
- }
37429
- let mine = false;
37430
- try {
37431
- const held = openSync(lock, "wx");
37432
- writeSync(held, String(process.pid));
37433
- closeSync(held);
37434
- mine = true;
37435
- } catch {}
37436
- if (mine) {
37437
- try {
37438
- return await compileHelper(directory, assembly, until - Date.now());
37439
- } finally {
37440
- rmSync10(lock, { force: true });
37441
- }
37442
- }
37443
- try {
37444
- const holder = Number(readFileSync20(lock, "utf8").trim());
37445
- const old = Date.now() - statSync9(lock).mtimeMs > budgetMs;
37446
- if (old || Number.isInteger(holder) && holder > 0 && !alive(holder)) {
37447
- rmSync10(lock, { force: true });
37448
- continue;
37449
- }
37450
- } catch {}
37451
- if (!wait) {
37452
- return null;
37453
- }
37454
- await delay3(200);
37455
- }
37456
- return `the desktop helper was not compiled within ${String(budgetMs)} ms`;
37457
- }
37458
- async function compileHelper(directory, assembly, budgetMs) {
37459
- const scratch = mkdtempSync2(join25(directory, "gdharness-desktop-source-"));
37460
- const source = join25(scratch, "helper.cs");
37461
- writeFileSync12(source, SOURCE, "utf8");
37462
- const literal = (path) => `'${path.replaceAll("'", "''")}'`;
37463
- const script = [
37464
- "$ErrorActionPreference = 'Stop'",
37465
- "try {",
37466
- ` $partial = ${literal(assembly)} + '.' + $PID`,
37467
- ` Add-Type -TypeDefinition ([IO.File]::ReadAllText(${literal(source)}, [Text.Encoding]::UTF8)) -OutputAssembly $partial`,
37468
- ` Move-Item -LiteralPath $partial -Destination ${literal(assembly)}`,
37469
- "} catch {",
37470
- " [Console]::Error.WriteLine($_.Exception.Message)",
37471
- " exit 1",
37472
- "}"
37473
- ].join(`
37474
- `);
37475
- try {
37476
- return await new Promise((resolve) => {
37477
- const compiler = spawnHidden("powershell.exe", [
37478
- "-NoProfile",
37479
- "-NonInteractive",
37480
- "-ExecutionPolicy",
37481
- "Bypass",
37482
- "-EncodedCommand",
37483
- Buffer.from(script, "utf16le").toString("base64")
37484
- ], { stdio: ["ignore", "ignore", "pipe"] });
37485
- let complained = "";
37486
- compiler.stderr.on("data", (chunk) => {
37487
- complained += chunk.toString();
37488
- });
37489
- const timer = setTimeout(() => {
37490
- compiler.kill();
37491
- }, budgetMs);
37492
- compiler.once("error", (error) => {
37493
- clearTimeout(timer);
37494
- resolve(`Windows PowerShell did not start: ${error.message}`);
37495
- });
37496
- compiler.once("close", (code) => {
37497
- clearTimeout(timer);
37498
- resolve(existsSync19(assembly) ? null : `Windows PowerShell could not compile it${code === null ? " in time" : ""}: ${complained.trim() || `exit code ${String(code)}`}`);
37499
- });
37500
- });
37501
- } finally {
37502
- rmSync10(scratch, { recursive: true, force: true });
37503
- }
37504
- }
37505
- var HIDDEN_DESKTOP = "gdharness", SOURCE = `using System;
37506
- using System.ComponentModel;
37507
- using System.IO;
37508
- using System.Runtime.InteropServices;
37509
- using System.Text;
37510
-
37511
- public static class GdharnessDesktop {
37512
- const uint GenericAll = 0x10000000;
37513
- const int UseStdHandles = 0x100;
37514
- const int UseShowWindow = 0x1;
37515
- const short ShowNoActivate = 4;
37516
- // No console at all, rather than a hidden one: a console's host would sit on the desktop too.
37517
- const uint DetachedProcess = 0x00000008;
37518
- const uint Infinite = 0xFFFFFFFF;
37519
-
37520
- [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
37521
- struct StartupInfo {
37522
- public int cb;
37523
- public string reserved;
37524
- public string desktop;
37525
- public string title;
37526
- public int x, y, xSize, ySize, xCount, yCount, fill, flags;
37527
- public short show, reservedSize;
37528
- public IntPtr reservedBytes, input, output, error;
37529
- }
37530
-
37531
- [StructLayout(LayoutKind.Sequential)]
37532
- struct Started {
37533
- public IntPtr process, thread;
37534
- public int processId, threadId;
37535
- }
37536
-
37537
- [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
37538
- static extern IntPtr CreateDesktopW(string name, IntPtr device, IntPtr mode, uint flags, uint access, IntPtr attributes);
37539
-
37540
- [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
37541
- static extern bool CreateProcessW(string application, StringBuilder commandLine, IntPtr processAttributes, IntPtr threadAttributes, bool inherit, uint flags, IntPtr environment, string directory, ref StartupInfo startup, out Started started);
37542
-
37543
- [DllImport("kernel32.dll")] static extern IntPtr GetStdHandle(int which);
37544
- [DllImport("kernel32.dll")] static extern uint WaitForSingleObject(IntPtr handle, uint milliseconds);
37545
- [DllImport("kernel32.dll")] static extern bool GetExitCodeProcess(IntPtr handle, out uint code);
37546
- [DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr handle);
37547
- [DllImport("kernel32.dll")] static extern bool TerminateProcess(IntPtr handle, uint code);
37548
-
37549
- delegate bool EachWindow(IntPtr window, IntPtr unused);
37550
- [DllImport("user32.dll")] static extern bool EnumWindows(EachWindow each, IntPtr unused);
37551
- [DllImport("user32.dll")] static extern bool IsWindowVisible(IntPtr window);
37552
- [DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr window, out uint processId);
37553
- [DllImport("user32.dll")] static extern bool LockSetForegroundWindow(uint code);
37554
- const uint LockForeground = 1;
37555
- const uint UnlockForeground = 2;
37556
- const uint TimedOut = 0x102;
37557
-
37558
- static bool ShowsAWindow(int processId) {
37559
- bool shown = false;
37560
- EnumWindows(delegate(IntPtr window, IntPtr unused) {
37561
- uint owner;
37562
- GetWindowThreadProcessId(window, out owner);
37563
- if (owner == (uint)processId && IsWindowVisible(window)) {
37564
- shown = true;
37565
- return false;
37566
- }
37567
- return true;
37568
- }, IntPtr.Zero);
37569
- return shown;
37570
- }
37571
-
37572
- // Keeps the foreground where it is while the process starts: its own call to take it fails while
37573
- // the foreground is locked, and the user's own Alt+Tab or click still moves it. Released once its
37574
- // window has been showing for a while, since a program takes the foreground as it shows its window
37575
- // and not afterwards, or when it exits, or after two minutes whatever happens.
37576
- static void HoldTheForeground(Started started) {
37577
- DateTime until = DateTime.UtcNow.AddMinutes(2);
37578
- DateTime? shownAt = null;
37579
- while (DateTime.UtcNow < until) {
37580
- if (WaitForSingleObject(started.process, 100) != TimedOut) {
37581
- return;
37582
- }
37583
- if (shownAt == null && ShowsAWindow(started.processId)) {
37584
- shownAt = DateTime.UtcNow;
37585
- }
37586
- if (shownAt != null && DateTime.UtcNow - shownAt.Value > TimeSpan.FromSeconds(10)) {
37587
- return;
37588
- }
37589
- }
37590
- }
37591
-
37592
- // Starts the command line, both of its streams on this process's output, writes its pid, and
37593
- // waits for it: the answer is its exit code. On the named desktop when there is one, whose handle
37594
- // is held until then, since a desktop goes when the last handle to it does, and on this process's
37595
- // own otherwise. With noActivate its first window is shown without being activated, which is
37596
- // what a program's first ShowWindow does whatever it asks for when the start names a show state,
37597
- // and the foreground is held while it starts, since Godot also asks for it outright. The process
37598
- // is ended through the handle held here once stopFile appears, which is how a keeper ends a game
37599
- // it did not start itself: a handle names one process, where a pid can name the next one too. The
37600
- // process starts in directory rather than in this one's, which is a temporary directory: Windows
37601
- // PowerShell saves its module cache relative to where it runs when the profile does not resolve.
37602
- public static int Run(string desktop, string commandLine, string pidFile, bool noActivate, string stopFile, string directory) {
37603
- StartupInfo startup = new StartupInfo();
37604
- startup.cb = Marshal.SizeOf(typeof(StartupInfo));
37605
- if (desktop.Length > 0) {
37606
- IntPtr held = CreateDesktopW(desktop, IntPtr.Zero, IntPtr.Zero, 0, GenericAll, IntPtr.Zero);
37607
- if (held == IntPtr.Zero) {
37608
- throw new Win32Exception(Marshal.GetLastWin32Error(), "CreateDesktopW " + desktop);
37609
- }
37610
- startup.desktop = desktop;
37611
- }
37612
- startup.flags = UseStdHandles;
37613
- if (noActivate) {
37614
- startup.flags |= UseShowWindow;
37615
- startup.show = ShowNoActivate;
37616
- }
37617
- startup.input = GetStdHandle(-10);
37618
- startup.output = GetStdHandle(-11);
37619
- startup.error = startup.output;
37620
- Started started;
37621
- if (noActivate) {
37622
- LockSetForegroundWindow(LockForeground);
37623
- }
37624
- try {
37625
- if (!CreateProcessW(null, new StringBuilder(commandLine), IntPtr.Zero, IntPtr.Zero, true, DetachedProcess, IntPtr.Zero, directory, ref startup, out started)) {
37626
- throw new Win32Exception(Marshal.GetLastWin32Error(), "CreateProcessW " + commandLine);
37627
- }
37628
- File.WriteAllText(pidFile, started.processId.ToString());
37629
- if (noActivate) {
37630
- HoldTheForeground(started);
37631
- }
37632
- } finally {
37633
- if (noActivate) {
37634
- LockSetForegroundWindow(UnlockForeground);
37635
- }
37636
- }
37637
- if (stopFile.Length == 0) {
37638
- WaitForSingleObject(started.process, Infinite);
37639
- } else {
37640
- bool stopped = false;
37641
- while (WaitForSingleObject(started.process, 100) == TimedOut) {
37642
- if (!stopped && File.Exists(stopFile)) {
37643
- TerminateProcess(started.process, 1);
37644
- stopped = true;
37645
- }
37646
- }
37647
- }
37648
- uint code;
37649
- GetExitCodeProcess(started.process, out code);
37650
- CloseHandle(started.thread);
37651
- CloseHandle(started.process);
37652
- return unchecked((int)code);
37653
- }
37654
- }
37655
- `, DIGEST, HELPER_COMPILE_MS = 180000;
37656
- var init_desktop = __esm(() => {
37657
- init_alive();
37658
- init_spawn();
37659
- DIGEST = createHash4("sha256").update(SOURCE).digest("hex").slice(0, 16);
37660
- });
37661
-
37662
37761
  // src/editor-log.ts
37663
37762
  import {
37664
- closeSync as closeSync2,
37665
- existsSync as existsSync20,
37666
- mkdirSync as mkdirSync10,
37667
- openSync as openSync2,
37668
- readFileSync as readFileSync21,
37669
- readSync,
37670
- rmSync as rmSync11,
37671
- statSync as statSync10,
37672
- writeFileSync as writeFileSync13
37763
+ closeSync as closeSync4,
37764
+ existsSync as existsSync23,
37765
+ mkdirSync as mkdirSync13,
37766
+ openSync as openSync4,
37767
+ readFileSync as readFileSync25,
37768
+ readSync as readSync2,
37769
+ rmSync as rmSync13,
37770
+ statSync as statSync13,
37771
+ writeFileSync as writeFileSync14
37673
37772
  } from "node:fs";
37674
- import { dirname as dirname16, join as join26 } from "node:path";
37773
+ import { dirname as dirname17, join as join29 } from "node:path";
37675
37774
  function editorLogPath(projectPath) {
37676
- return join26(projectPath, ".godot", "gdharness-editor.log");
37775
+ return join29(projectPath, ".godot", "gdharness-editor.log");
37677
37776
  }
37678
37777
  function editorLogNotePath(projectPath) {
37679
- return join26(projectPath, ".godot", "gdharness-editor.json");
37778
+ return join29(projectPath, ".godot", "gdharness-editor.json");
37680
37779
  }
37681
37780
  function readEditorLogNote(projectPath) {
37682
37781
  try {
37683
- const fields = asParams(JSON.parse(readFileSync21(editorLogNotePath(projectPath), "utf8")));
37782
+ const fields = asParams(JSON.parse(readFileSync25(editorLogNotePath(projectPath), "utf8")));
37684
37783
  const pid = readNumber(fields, "pid");
37685
37784
  const startedAt = readString(fields, "startedAt");
37686
37785
  return pid !== undefined && Number.isInteger(pid) && pid > 0 && startedAt !== undefined ? { pid, startedAt } : null;
@@ -37691,16 +37790,16 @@ function readEditorLogNote(projectPath) {
37691
37790
  function clearEditorLog(projectPath) {
37692
37791
  try {
37693
37792
  const path = editorLogPath(projectPath);
37694
- mkdirSync10(dirname16(path), { recursive: true });
37695
- rmSync11(path, { force: true });
37696
- rmSync11(editorLogNotePath(projectPath), { force: true });
37793
+ mkdirSync13(dirname17(path), { recursive: true });
37794
+ rmSync13(path, { force: true });
37795
+ rmSync13(editorLogNotePath(projectPath), { force: true });
37697
37796
  } catch {}
37698
37797
  }
37699
37798
  function writeEditorLogNote(projectPath, pid) {
37700
37799
  try {
37701
37800
  const path = editorLogNotePath(projectPath);
37702
- mkdirSync10(dirname16(path), { recursive: true });
37703
- writeFileSync13(path, JSON.stringify({ pid, startedAt: new Date().toISOString() }, null, 2), "utf8");
37801
+ mkdirSync13(dirname17(path), { recursive: true });
37802
+ writeFileSync14(path, JSON.stringify({ pid, startedAt: new Date().toISOString() }, null, 2), "utf8");
37704
37803
  } catch {}
37705
37804
  }
37706
37805
  function theConsoleWasNotCaptured(why, projectPath) {
@@ -37721,7 +37820,7 @@ function editorConsole(projectPath, editorPid, openedByAServer) {
37721
37820
  return { kind: "another editor", notedPid: noted?.pid ?? null };
37722
37821
  }
37723
37822
  const path = editorLogPath(projectPath);
37724
- if (!existsSync20(path)) {
37823
+ if (!existsSync23(path)) {
37725
37824
  return { kind: "not written" };
37726
37825
  }
37727
37826
  const log = new GameLog;
@@ -37777,20 +37876,20 @@ function foldProgress(text) {
37777
37876
  `);
37778
37877
  }
37779
37878
  function theFirstOf(path) {
37780
- const whole = statSync10(path).size;
37879
+ const whole = statSync13(path).size;
37781
37880
  if (whole <= MOST_CONSOLE_BYTES) {
37782
- return { text: readFileSync21(path, "utf8") };
37881
+ return { text: readFileSync25(path, "utf8") };
37783
37882
  }
37784
- const handle = openSync2(path, "r");
37883
+ const handle = openSync4(path, "r");
37785
37884
  try {
37786
37885
  const buffer = Buffer.alloc(MOST_CONSOLE_BYTES);
37787
- const read = readSync(handle, buffer, 0, MOST_CONSOLE_BYTES, 0);
37886
+ const read = readSync2(handle, buffer, 0, MOST_CONSOLE_BYTES, 0);
37788
37887
  const decoded = buffer.subarray(0, read).toString("utf8");
37789
37888
  const lastLine = decoded.lastIndexOf(`
37790
37889
  `);
37791
37890
  return { text: lastLine === -1 ? decoded : decoded.slice(0, lastLine + 1), of: whole };
37792
37891
  } finally {
37793
- closeSync2(handle);
37892
+ closeSync4(handle);
37794
37893
  }
37795
37894
  }
37796
37895
  function shapeOf2(text) {
@@ -37940,8 +38039,8 @@ var init_editor_log = __esm(() => {
37940
38039
  });
37941
38040
 
37942
38041
  // src/rename.ts
37943
- import { existsSync as existsSync21, mkdirSync as mkdirSync11, readFileSync as readFileSync22, renameSync as renameSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync14 } from "node:fs";
37944
- import { dirname as dirname17, posix as posix2 } from "node:path";
38042
+ import { existsSync as existsSync24, mkdirSync as mkdirSync14, readFileSync as readFileSync26, renameSync as renameSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync15 } from "node:fs";
38043
+ import { dirname as dirname18, posix as posix4 } from "node:path";
37945
38044
 
37946
38045
  class Script {
37947
38046
  path;
@@ -38054,13 +38153,13 @@ class ClassGraph {
38054
38153
  }
38055
38154
  function resolvedPath(written, from) {
38056
38155
  if (written.startsWith("res://")) {
38057
- return `res://${posix2.normalize(written.slice("res://".length))}`;
38156
+ return `res://${posix4.normalize(written.slice("res://".length))}`;
38058
38157
  }
38059
- const directory = posix2.dirname(from.slice("res://".length));
38060
- return `res://${posix2.normalize(posix2.join(directory, written))}`;
38158
+ const directory = posix4.dirname(from.slice("res://".length));
38159
+ return `res://${posix4.normalize(posix4.join(directory, written))}`;
38061
38160
  }
38062
38161
  function relativeTo(target, from) {
38063
- return posix2.relative(posix2.dirname(from.slice("res://".length)), target.slice("res://".length));
38162
+ return posix4.relative(posix4.dirname(from.slice("res://".length)), target.slice("res://".length));
38064
38163
  }
38065
38164
  function scriptsOf(texts) {
38066
38165
  const scripts = new Map;
@@ -38645,25 +38744,25 @@ function rewritesOf(plan, texts, move) {
38645
38744
  }
38646
38745
  function applyRewrites(rewrites) {
38647
38746
  for (const rewrite of rewrites) {
38648
- const now = readFileSync22(rewrite.absolute, "utf8");
38747
+ const now = readFileSync26(rewrite.absolute, "utf8");
38649
38748
  if (now !== rewrite.before) {
38650
38749
  throw new Error(`${rewrite.path} changed on disk while the rename was being worked out; nothing was written`);
38651
38750
  }
38652
- if (rewrite.writeTo !== rewrite.absolute && existsSync21(rewrite.writeTo)) {
38751
+ if (rewrite.writeTo !== rewrite.absolute && existsSync24(rewrite.writeTo)) {
38653
38752
  throw new Error(`${rewrite.writeTo} already exists; nothing was written`);
38654
38753
  }
38655
38754
  }
38656
38755
  const done = [];
38657
38756
  try {
38658
38757
  for (const rewrite of rewrites) {
38659
- mkdirSync11(dirname17(rewrite.writeTo), { recursive: true });
38758
+ mkdirSync14(dirname18(rewrite.writeTo), { recursive: true });
38660
38759
  const staging = `${rewrite.writeTo}.gdharness-rename`;
38661
- writeFileSync14(staging, rewrite.after, "utf8");
38760
+ writeFileSync15(staging, rewrite.after, "utf8");
38662
38761
  renameSync3(staging, rewrite.writeTo);
38663
38762
  let movedUid = false;
38664
38763
  if (rewrite.writeTo !== rewrite.absolute) {
38665
38764
  unlinkSync3(rewrite.absolute);
38666
- if (existsSync21(`${rewrite.absolute}.uid`)) {
38765
+ if (existsSync24(`${rewrite.absolute}.uid`)) {
38667
38766
  renameSync3(`${rewrite.absolute}.uid`, `${rewrite.writeTo}.uid`);
38668
38767
  movedUid = true;
38669
38768
  }
@@ -38673,7 +38772,7 @@ function applyRewrites(rewrites) {
38673
38772
  } catch (error) {
38674
38773
  for (const { rewrite, movedUid } of done.reverse()) {
38675
38774
  try {
38676
- writeFileSync14(rewrite.absolute, rewrite.before, "utf8");
38775
+ writeFileSync15(rewrite.absolute, rewrite.before, "utf8");
38677
38776
  if (rewrite.writeTo !== rewrite.absolute) {
38678
38777
  unlinkSync3(rewrite.writeTo);
38679
38778
  if (movedUid) {
@@ -38709,11 +38808,11 @@ var init_rename = __esm(() => {
38709
38808
  });
38710
38809
 
38711
38810
  // src/lsp_client.ts
38712
- import { existsSync as existsSync22, realpathSync as realpathSync3 } from "node:fs";
38811
+ import { existsSync as existsSync25, realpathSync as realpathSync3 } from "node:fs";
38713
38812
  import { readFile, realpath } from "node:fs/promises";
38714
38813
  import { createConnection as createConnection3 } from "node:net";
38715
- import { dirname as dirname18, join as join27, resolve as resolve5 } from "node:path";
38716
- import { setTimeout as delay4 } from "node:timers/promises";
38814
+ import { dirname as dirname19, join as join30, resolve as resolve6 } from "node:path";
38815
+ import { setTimeout as delay6 } from "node:timers/promises";
38717
38816
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
38718
38817
  function initializeTimeoutMs() {
38719
38818
  const override = Number.parseInt(envValue("GDHARNESS_LSP_INITIALIZE_TIMEOUT_MS") ?? "", 10);
@@ -38779,7 +38878,7 @@ class GodotLSPClient {
38779
38878
  if (Date.now() >= until) {
38780
38879
  throw new LanguageServerNotListening(this.port, this.timeouts.listenMs);
38781
38880
  }
38782
- await delay4(LANGUAGE_SERVER_RETRY_MS);
38881
+ await delay6(LANGUAGE_SERVER_RETRY_MS);
38783
38882
  }
38784
38883
  }
38785
38884
  }
@@ -38874,7 +38973,7 @@ class GodotLSPClient {
38874
38973
  if (this.initialized) {
38875
38974
  return;
38876
38975
  }
38877
- const rootPath = this.rootPath ?? dirname18(resolve5(filePath));
38976
+ const rootPath = this.rootPath ?? dirname19(resolve6(filePath));
38878
38977
  await this.initialize(rootPath);
38879
38978
  }
38880
38979
  async sendRequest(method, params, timeoutMs = this.timeouts.requestMs) {
@@ -39016,7 +39115,7 @@ class GodotLSPClient {
39016
39115
  });
39017
39116
  }
39018
39117
  toFileUri(filePath) {
39019
- return pathToFileURL(resolve5(filePath)).href;
39118
+ return pathToFileURL(resolve6(filePath)).href;
39020
39119
  }
39021
39120
  syncDocument(filePath, content) {
39022
39121
  const uri = this.toFileUri(filePath);
@@ -39060,7 +39159,7 @@ class GodotLSPClient {
39060
39159
  }
39061
39160
  async initialize(rootPath) {
39062
39161
  await this.ensureConnected();
39063
- const resolvedRootPath = resolve5(rootPath);
39162
+ const resolvedRootPath = resolve6(rootPath);
39064
39163
  const rootUri = pathToFileURL(resolvedRootPath).href;
39065
39164
  this.forgetServedWorkspace();
39066
39165
  const result = await this.sendRequest("initialize", {
@@ -39283,7 +39382,7 @@ function normalizeLSPError(error, port) {
39283
39382
  function asRecord(value) {
39284
39383
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
39285
39384
  }
39286
- function named(value, names) {
39385
+ function named2(value, names) {
39287
39386
  return typeof value === "number" && Number.isInteger(value) ? names[value - 1] ?? value : value;
39288
39387
  }
39289
39388
  function shownPosition(position) {
@@ -39302,7 +39401,7 @@ function shownDiagnostic(entry) {
39302
39401
  const { range, severity, message, code } = asRecord(entry);
39303
39402
  return {
39304
39403
  ...shownRange(range),
39305
- severity: named(severity, SEVERITIES),
39404
+ severity: named2(severity, SEVERITIES),
39306
39405
  message,
39307
39406
  ...code === undefined || code === null || code === 0 || code === "" ? {} : { code }
39308
39407
  };
@@ -39331,7 +39430,7 @@ function shownSymbol(entry, lastOfDocument) {
39331
39430
  const { documentation, native_class: nativeClass, deprecated, detail } = symbol;
39332
39431
  return {
39333
39432
  name: symbol["name"],
39334
- kind: named(symbol["kind"], SYMBOL_KINDS),
39433
+ kind: named2(symbol["kind"], SYMBOL_KINDS),
39335
39434
  ...typeof detail === "string" && detail !== "" ? { detail } : {},
39336
39435
  ...at === null ? {} : { line: at.line, column: at.column },
39337
39436
  ...last === null ? {} : { lastLine: last },
@@ -39365,7 +39464,7 @@ function narrowedCompletions(completions, typed) {
39365
39464
  }
39366
39465
  function shownCompletion(entry) {
39367
39466
  const { data: _request, kind, ...rest } = asRecord(entry);
39368
- return { ...rest, kind: named(kind, COMPLETION_KINDS) };
39467
+ return { ...rest, kind: named2(kind, COMPLETION_KINDS) };
39369
39468
  }
39370
39469
  function shownHover(hover) {
39371
39470
  if (hover === null || typeof hover !== "object") {
@@ -39383,14 +39482,14 @@ function protocolPosition(args) {
39383
39482
  return { line: line - 1, character: character - 1 };
39384
39483
  }
39385
39484
  async function resolveLSPPaths(projectPathValue, scriptPathValue) {
39386
- const requestedProjectPath = resolve5(projectPathValue);
39485
+ const requestedProjectPath = resolve6(projectPathValue);
39387
39486
  let projectPath;
39388
39487
  try {
39389
39488
  projectPath = await realpath(requestedProjectPath);
39390
39489
  } catch {
39391
39490
  throw new ArgumentRefusal(`Project path does not exist: ${requestedProjectPath}`);
39392
39491
  }
39393
- if (!existsSync22(join27(projectPath, "project.godot"))) {
39492
+ if (!existsSync25(join30(projectPath, "project.godot"))) {
39394
39493
  throw new ArgumentRefusal(`Not a Godot project: ${projectPath}. Point projectPath at the directory holding project.godot.`);
39395
39494
  }
39396
39495
  const contained = resolveWithinProject(projectPath, scriptPathValue);
@@ -39550,10 +39649,10 @@ var init_lsp_client = __esm(() => {
39550
39649
  });
39551
39650
 
39552
39651
  // src/rename-symbol.ts
39553
- import { existsSync as existsSync23, mkdirSync as mkdirSync12, readdirSync as readdirSync11, realpathSync as realpathSync4, rmSync as rmSync12, writeFileSync as writeFileSync15 } from "node:fs";
39554
- import { basename as basename8, dirname as dirname19, join as join28, relative as relative4, resolve as resolve6 } from "node:path";
39652
+ import { existsSync as existsSync26, mkdirSync as mkdirSync15, readdirSync as readdirSync13, realpathSync as realpathSync4, rmSync as rmSync14, writeFileSync as writeFileSync16 } from "node:fs";
39653
+ import { basename as basename8, dirname as dirname20, join as join31, relative as relative5, resolve as resolve7 } from "node:path";
39555
39654
  function resourcePathOf(projectPath, absolute) {
39556
- const inside = relative4(onDiskSpelling(projectPath), onDiskSpelling(absolute));
39655
+ const inside = relative5(onDiskSpelling(projectPath), onDiskSpelling(absolute));
39557
39656
  if (inside === "" || inside.startsWith("..") || /^[A-Za-z]:/.test(inside)) {
39558
39657
  return null;
39559
39658
  }
@@ -39561,14 +39660,14 @@ function resourcePathOf(projectPath, absolute) {
39561
39660
  }
39562
39661
  function onDiskSpelling(path) {
39563
39662
  const missing = [];
39564
- let at = resolve6(path);
39663
+ let at = resolve7(path);
39565
39664
  for (;; ) {
39566
39665
  try {
39567
- return join28(realpathSync4.native(at), ...missing.reverse());
39666
+ return join31(realpathSync4.native(at), ...missing.reverse());
39568
39667
  } catch {
39569
- const parent = dirname19(at);
39668
+ const parent = dirname20(at);
39570
39669
  if (parent === at) {
39571
- return resolve6(path);
39670
+ return resolve7(path);
39572
39671
  }
39573
39672
  missing.push(basename8(at));
39574
39673
  at = parent;
@@ -39618,7 +39717,7 @@ async function renameSymbol(request, services) {
39618
39717
  reason: `newScriptPath must be a .gd path inside the project, and ${request.newScriptPath} is not.`
39619
39718
  };
39620
39719
  }
39621
- if (existsSync23(request.newScriptPath)) {
39720
+ if (existsSync26(request.newScriptPath)) {
39622
39721
  return { ok: false, reason: `${moveTo} already exists, so the script cannot be moved there.` };
39623
39722
  }
39624
39723
  }
@@ -39794,7 +39893,7 @@ async function renameSymbol(request, services) {
39794
39893
  scripts,
39795
39894
  moves: new Map([[declaringPath, moveTo]]),
39796
39895
  named: [declaringPath],
39797
- exists: (path) => existsSync23(`${projectPath}/${path.slice("res://".length)}`)
39896
+ exists: (path) => existsSync26(`${projectPath}/${path.slice("res://".length)}`)
39798
39897
  }, plan);
39799
39898
  }
39800
39899
  plan = mentionIgnored(plan, ignoredTexts(projectPath), symbol, moveTo === null ? [] : [declaringPath]);
@@ -39993,15 +40092,15 @@ ${mention.kind}`;
39993
40092
  };
39994
40093
  }
39995
40094
  function writeLeftAloneDetail(projectPath, symbol, detail) {
39996
- const directory = join28(projectPath, ".godot", "gdharness-renames");
39997
- mkdirSync12(directory, { recursive: true });
40095
+ const directory = join31(projectPath, ".godot", "gdharness-renames");
40096
+ mkdirSync15(directory, { recursive: true });
39998
40097
  detailWrites += 1;
39999
- const path = join28(directory, `${String(Date.now())}-${String(detailWrites).padStart(6, "0")}-${String(process.pid)}-${symbol}.json`);
40000
- writeFileSync15(path, `${JSON.stringify(detail, null, 2)}
40098
+ const path = join31(directory, `${String(Date.now())}-${String(detailWrites).padStart(6, "0")}-${String(process.pid)}-${symbol}.json`);
40099
+ writeFileSync16(path, `${JSON.stringify(detail, null, 2)}
40001
40100
  `, "utf8");
40002
- const kept = readdirSync11(directory).filter((name) => name.endsWith(".json")).sort().reverse();
40101
+ const kept = readdirSync13(directory).filter((name) => name.endsWith(".json")).sort().reverse();
40003
40102
  for (const old of kept.slice(10)) {
40004
- rmSync12(join28(directory, old), { force: true });
40103
+ rmSync14(join31(directory, old), { force: true });
40005
40104
  }
40006
40105
  return path;
40007
40106
  }
@@ -40042,25 +40141,25 @@ var init_rename_symbol = __esm(() => {
40042
40141
 
40043
40142
  // src/file-move.ts
40044
40143
  import {
40045
- existsSync as existsSync24,
40046
- mkdirSync as mkdirSync13,
40047
- readdirSync as readdirSync12,
40048
- readFileSync as readFileSync23,
40144
+ existsSync as existsSync27,
40145
+ mkdirSync as mkdirSync16,
40146
+ readdirSync as readdirSync14,
40147
+ readFileSync as readFileSync27,
40049
40148
  renameSync as renameSync4,
40050
- statSync as statSync11,
40051
- writeFileSync as writeFileSync16
40149
+ statSync as statSync14,
40150
+ writeFileSync as writeFileSync17
40052
40151
  } from "node:fs";
40053
- import { basename as basename9, dirname as dirname20, extname as extname3, join as join29, relative as relative5 } from "node:path";
40152
+ import { basename as basename9, dirname as dirname21, extname as extname3, join as join32, relative as relative6 } from "node:path";
40054
40153
  function filesUnder(directory) {
40055
- return readdirSync12(directory, { withFileTypes: true }).flatMap((entry) => {
40056
- const path = join29(directory, entry.name);
40154
+ return readdirSync14(directory, { withFileTypes: true }).flatMap((entry) => {
40155
+ const path = join32(directory, entry.name);
40057
40156
  return entry.isDirectory() ? filesUnder(path) : [path];
40058
40157
  });
40059
40158
  }
40060
40159
  function sameFile(one, other) {
40061
40160
  try {
40062
- const a = statSync11(one);
40063
- const b = statSync11(other);
40161
+ const a = statSync14(one);
40162
+ const b = statSync14(other);
40064
40163
  return a.ino === b.ino && a.dev === b.dev;
40065
40164
  } catch {
40066
40165
  return false;
@@ -40074,8 +40173,8 @@ function unseenByTheEngine(projectPath, path) {
40074
40173
  return `${path} is under ${step}, and the engine passes over every directory whose name starts with a dot`;
40075
40174
  }
40076
40175
  if (at < steps.length - 1) {
40077
- directory = join29(directory, step);
40078
- if (existsSync24(join29(directory, ".gdignore"))) {
40176
+ directory = join32(directory, step);
40177
+ if (existsSync27(join32(directory, ".gdignore"))) {
40079
40178
  return `${path} is under a directory holding a .gdignore, which the engine does not load`;
40080
40179
  }
40081
40180
  }
@@ -40087,21 +40186,21 @@ function installedAddonOf(projectPath, path) {
40087
40186
  if (steps[0] !== "addons" || steps[1] === undefined) {
40088
40187
  return null;
40089
40188
  }
40090
- return existsSync24(join29(projectPath, "addons", steps[1], VERSION_MARKER)) ? `res://addons/${steps[1]}` : null;
40189
+ return existsSync27(join32(projectPath, "addons", steps[1], VERSION_MARKER)) ? `res://addons/${steps[1]}` : null;
40091
40190
  }
40092
40191
  function binaryResources(projectPath) {
40093
40192
  const found = [];
40094
40193
  const visit = (directory, prefix) => {
40095
- if (existsSync24(join29(directory, ".gdignore"))) {
40194
+ if (existsSync27(join32(directory, ".gdignore"))) {
40096
40195
  return;
40097
40196
  }
40098
- for (const entry of readdirSync12(directory, { withFileTypes: true })) {
40197
+ for (const entry of readdirSync14(directory, { withFileTypes: true })) {
40099
40198
  if (entry.name.startsWith(".")) {
40100
40199
  continue;
40101
40200
  }
40102
40201
  const path = `${prefix}${entry.name}`;
40103
40202
  if (entry.isDirectory()) {
40104
- visit(join29(directory, entry.name), `${path}/`);
40203
+ visit(join32(directory, entry.name), `${path}/`);
40105
40204
  } else if (/\.(scn|res)$/i.test(entry.name)) {
40106
40205
  found.push(path);
40107
40206
  }
@@ -40111,7 +40210,7 @@ function binaryResources(projectPath) {
40111
40210
  return found;
40112
40211
  }
40113
40212
  function targetPathOf(projectPath, absolute) {
40114
- const inside = relative5(onDiskSpelling(projectPath), join29(onDiskSpelling(dirname20(absolute)), basename9(absolute)));
40213
+ const inside = relative6(onDiskSpelling(projectPath), join32(onDiskSpelling(dirname21(absolute)), basename9(absolute)));
40115
40214
  if (inside === "" || inside.startsWith("..") || /^[A-Za-z]:/.test(inside)) {
40116
40215
  return null;
40117
40216
  }
@@ -40129,13 +40228,13 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
40129
40228
  if (to === null) {
40130
40229
  return { ok: false, reason: `newPath must be a path inside the project, and ${absoluteTo} is not.` };
40131
40230
  }
40132
- if (!existsSync24(absoluteFrom)) {
40231
+ if (!existsSync27(absoluteFrom)) {
40133
40232
  return { ok: false, reason: `There is nothing at ${from} to move.` };
40134
40233
  }
40135
40234
  if (from === to) {
40136
40235
  return { ok: false, reason: `${from} is already there; nothing to move.` };
40137
40236
  }
40138
- const isDirectory = statSync11(absoluteFrom).isDirectory();
40237
+ const isDirectory = statSync14(absoluteFrom).isDirectory();
40139
40238
  if (from === "res://project.godot") {
40140
40239
  return {
40141
40240
  ok: false,
@@ -40158,7 +40257,7 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
40158
40257
  if (isDirectory && to.startsWith(`${from}/`)) {
40159
40258
  return { ok: false, reason: `${from} cannot be moved into itself.` };
40160
40259
  }
40161
- if (existsSync24(absoluteTo) && !sameFile(absoluteFrom, absoluteTo)) {
40260
+ if (existsSync27(absoluteTo) && !sameFile(absoluteFrom, absoluteTo)) {
40162
40261
  return { ok: false, reason: `${to} already exists, so nothing was moved there.` };
40163
40262
  }
40164
40263
  const unseen = unseenByTheEngine(projectPath, to);
@@ -40166,7 +40265,7 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
40166
40265
  return { ok: false, reason: `${unseen}, so nothing there could load what was moved.` };
40167
40266
  }
40168
40267
  if (!isDirectory) {
40169
- const owner = SIDECARS.map((sidecar) => from.endsWith(sidecar) ? from.slice(0, -sidecar.length) : null).find((path) => path !== null && existsSync24(join29(projectPath, path.slice("res://".length))));
40268
+ const owner = SIDECARS.map((sidecar) => from.endsWith(sidecar) ? from.slice(0, -sidecar.length) : null).find((path) => path !== null && existsSync27(join32(projectPath, path.slice("res://".length))));
40170
40269
  if (owner !== undefined && owner !== null) {
40171
40270
  return { ok: false, reason: `${from} belongs to ${owner} and moves with it: move ${owner} instead.` };
40172
40271
  }
@@ -40188,7 +40287,7 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
40188
40287
  } else {
40189
40288
  moves.set(from, to);
40190
40289
  for (const sidecar of SIDECARS) {
40191
- if (existsSync24(`${absoluteFrom}${sidecar}`)) {
40290
+ if (existsSync27(`${absoluteFrom}${sidecar}`)) {
40192
40291
  moves.set(`${from}${sidecar}`, `${to}${sidecar}`);
40193
40292
  }
40194
40293
  }
@@ -40200,12 +40299,12 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
40200
40299
  scripts,
40201
40300
  moves,
40202
40301
  named: [from],
40203
- exists: (path) => existsSync24(join29(projectPath, path.slice("res://".length)))
40302
+ exists: (path) => existsSync27(join32(projectPath, path.slice("res://".length)))
40204
40303
  }, { edits: [], mentions: [] });
40205
40304
  plan = mentionIgnored(plan, ignoredTexts(projectPath), null, [from]);
40206
40305
  const binaries = [];
40207
40306
  for (const path of binaryResources(projectPath)) {
40208
- const bytes = readFileSync23(join29(projectPath, path.slice("res://".length)));
40307
+ const bytes = readFileSync27(join32(projectPath, path.slice("res://".length)));
40209
40308
  const names = [...moves.keys()].find((moving) => bytes.includes(moving));
40210
40309
  if (names !== undefined) {
40211
40310
  binaries.push({
@@ -40224,7 +40323,7 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
40224
40323
  }));
40225
40324
  const left = leftAloneReport(mentions);
40226
40325
  const rewrites = rewritesOf(plan, texts, null);
40227
- const absoluteOf = (path) => join29(projectPath, path.slice("res://".length));
40326
+ const absoluteOf = (path) => join32(projectPath, path.slice("res://".length));
40228
40327
  const rewritten = new Map(rewrites.map((rewrite) => [rewrite.path, rewrite]));
40229
40328
  const moved = [...moves].map(([source, target]) => {
40230
40329
  const rewrite = rewritten.get(source);
@@ -40266,24 +40365,24 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
40266
40365
  }
40267
40366
  function applyFileMove(outcome) {
40268
40367
  for (const rewrite of outcome.inPlace) {
40269
- if (readFileSync23(rewrite.absolute, "utf8") !== rewrite.before) {
40368
+ if (readFileSync27(rewrite.absolute, "utf8") !== rewrite.before) {
40270
40369
  throw new Error(`${rewrite.path} changed on disk while the move was being worked out; nothing was written`);
40271
40370
  }
40272
40371
  }
40273
40372
  for (const file of outcome.moved) {
40274
- if (!existsSync24(file.absoluteFrom)) {
40373
+ if (!existsSync27(file.absoluteFrom)) {
40275
40374
  throw new Error(`${file.from} is gone from disk; nothing was written`);
40276
40375
  }
40277
- if (file.text !== null && readFileSync23(file.absoluteFrom, "utf8") !== file.text.before) {
40376
+ if (file.text !== null && readFileSync27(file.absoluteFrom, "utf8") !== file.text.before) {
40278
40377
  throw new Error(`${file.from} changed on disk while the move was being worked out; nothing was written`);
40279
40378
  }
40280
- if (existsSync24(file.absoluteTo) && !sameFile(file.absoluteFrom, file.absoluteTo)) {
40379
+ if (existsSync27(file.absoluteTo) && !sameFile(file.absoluteFrom, file.absoluteTo)) {
40281
40380
  throw new Error(`${file.to} already exists; nothing was written`);
40282
40381
  }
40283
40382
  }
40284
40383
  const undo = [];
40285
40384
  const rename = (source, target) => {
40286
- mkdirSync13(dirname20(target), { recursive: true });
40385
+ mkdirSync16(dirname21(target), { recursive: true });
40287
40386
  renameSync4(source, target);
40288
40387
  undo.push(() => {
40289
40388
  renameSync4(target, source);
@@ -40292,10 +40391,10 @@ function applyFileMove(outcome) {
40292
40391
  try {
40293
40392
  for (const rewrite of outcome.inPlace) {
40294
40393
  const staging = `${rewrite.absolute}.gdharness-move`;
40295
- writeFileSync16(staging, rewrite.after, "utf8");
40394
+ writeFileSync17(staging, rewrite.after, "utf8");
40296
40395
  renameSync4(staging, rewrite.absolute);
40297
40396
  undo.push(() => {
40298
- writeFileSync16(rewrite.absolute, rewrite.before, "utf8");
40397
+ writeFileSync17(rewrite.absolute, rewrite.before, "utf8");
40299
40398
  });
40300
40399
  }
40301
40400
  if (outcome.directory !== null) {
@@ -40307,9 +40406,9 @@ function applyFileMove(outcome) {
40307
40406
  }
40308
40407
  if (file.text !== null) {
40309
40408
  const { before, after } = file.text;
40310
- writeFileSync16(file.absoluteTo, after, "utf8");
40409
+ writeFileSync17(file.absoluteTo, after, "utf8");
40311
40410
  undo.push(() => {
40312
- writeFileSync16(file.absoluteTo, before, "utf8");
40411
+ writeFileSync17(file.absoluteTo, before, "utf8");
40313
40412
  });
40314
40413
  }
40315
40414
  }
@@ -43945,13 +44044,13 @@ var init_godot_bridge = __esm(() => {
43945
44044
  });
43946
44045
 
43947
44046
  // src/import-freshness.ts
43948
- import { createHash as createHash5 } from "node:crypto";
43949
- import { existsSync as existsSync25, readdirSync as readdirSync13, readFileSync as readFileSync24, statSync as statSync12 } from "node:fs";
43950
- import { join as join30 } from "node:path";
44047
+ import { createHash as createHash6 } from "node:crypto";
44048
+ import { existsSync as existsSync28, readdirSync as readdirSync15, readFileSync as readFileSync28, statSync as statSync15 } from "node:fs";
44049
+ import { join as join33 } from "node:path";
43951
44050
  function md5Of(file) {
43952
44051
  let stats;
43953
44052
  try {
43954
- stats = statSync12(file);
44053
+ stats = statSync15(file);
43955
44054
  } catch {
43956
44055
  return null;
43957
44056
  }
@@ -43961,7 +44060,7 @@ function md5Of(file) {
43961
44060
  }
43962
44061
  let md5;
43963
44062
  try {
43964
- md5 = createHash5("md5").update(readFileSync24(file)).digest("hex");
44063
+ md5 = createHash6("md5").update(readFileSync28(file)).digest("hex");
43965
44064
  } catch {
43966
44065
  return null;
43967
44066
  }
@@ -43991,7 +44090,7 @@ function listed2(value) {
43991
44090
  return [...(value ?? "").matchAll(/"((?:[^"\\]|\\.)*)"/g)].map((match) => match[1] ?? "");
43992
44091
  }
43993
44092
  function onDisk(projectPath, resPath) {
43994
- return join30(projectPath, resPath.slice("res://".length));
44093
+ return join33(projectPath, resPath.slice("res://".length));
43995
44094
  }
43996
44095
  function behind(projectPath, resPath, sidecar) {
43997
44096
  const remap = sectionOf(sidecar, "remap");
@@ -44000,14 +44099,14 @@ function behind(projectPath, resPath, sidecar) {
44000
44099
  return null;
44001
44100
  }
44002
44101
  const outputs = listed2(sectionOf(sidecar, "deps").get("dest_files"));
44003
- if (outputs.some((output) => !existsSync25(onDisk(projectPath, output)))) {
44102
+ if (outputs.some((output) => !existsSync28(onDisk(projectPath, output)))) {
44004
44103
  return "its imported output is not on disk";
44005
44104
  }
44006
44105
  const name = resPath.slice(resPath.lastIndexOf("/") + 1);
44007
- const record = join30(projectPath, ".godot", "imported", `${name}-${createHash5("md5").update(resPath, "utf8").digest("hex")}.md5`);
44106
+ const record = join33(projectPath, ".godot", "imported", `${name}-${createHash6("md5").update(resPath, "utf8").digest("hex")}.md5`);
44008
44107
  let recorded = "";
44009
44108
  try {
44010
- recorded = /^source_md5="([0-9a-f]*)"/m.exec(readFileSync24(record, "utf8"))?.[1] ?? "";
44109
+ recorded = /^source_md5="([0-9a-f]*)"/m.exec(readFileSync28(record, "utf8"))?.[1] ?? "";
44011
44110
  } catch {
44012
44111
  recorded = "";
44013
44112
  }
@@ -44020,12 +44119,12 @@ function behind(projectPath, resPath, sidecar) {
44020
44119
  function staleImports(projectPath) {
44021
44120
  const stale = [];
44022
44121
  const visit = (directory, resDirectory) => {
44023
- if (existsSync25(join30(directory, ".gdignore")) || existsSync25(join30(directory, VERSION_MARKER))) {
44122
+ if (existsSync28(join33(directory, ".gdignore")) || existsSync28(join33(directory, VERSION_MARKER))) {
44024
44123
  return;
44025
44124
  }
44026
44125
  let entries;
44027
44126
  try {
44028
- entries = readdirSync13(directory, { withFileTypes: true });
44127
+ entries = readdirSync15(directory, { withFileTypes: true });
44029
44128
  } catch {
44030
44129
  return;
44031
44130
  }
@@ -44036,7 +44135,7 @@ function staleImports(projectPath) {
44036
44135
  }
44037
44136
  const resPath = `${resDirectory}${entry.name}`;
44038
44137
  if (entry.isDirectory()) {
44039
- visit(join30(directory, entry.name), `${resPath}/`);
44138
+ visit(join33(directory, entry.name), `${resPath}/`);
44040
44139
  continue;
44041
44140
  }
44042
44141
  if (!entry.isFile()) {
@@ -44049,7 +44148,7 @@ function staleImports(projectPath) {
44049
44148
  }
44050
44149
  let sidecar;
44051
44150
  try {
44052
- sidecar = readFileSync24(join30(directory, entry.name), "utf8");
44151
+ sidecar = readFileSync28(join33(directory, entry.name), "utf8");
44053
44152
  } catch {
44054
44153
  continue;
44055
44154
  }
@@ -44169,25 +44268,25 @@ var DEBUG_PREFIX = "debug ";
44169
44268
  var init_keeper_channel = () => {};
44170
44269
 
44171
44270
  // src/lifecycle-log.ts
44172
- import { appendFileSync, mkdirSync as mkdirSync14, renameSync as renameSync5, statSync as statSync13 } from "node:fs";
44173
- import { dirname as dirname21, join as join31 } from "node:path";
44174
- import process11 from "node:process";
44271
+ import { appendFileSync, mkdirSync as mkdirSync17, renameSync as renameSync5, statSync as statSync16 } from "node:fs";
44272
+ import { dirname as dirname22, join as join34 } from "node:path";
44273
+ import process14 from "node:process";
44175
44274
  function lifecycleLogPath() {
44176
- return join31(runtimeDirectory(), "servers.log");
44275
+ return join34(runtimeDirectory(), "servers.log");
44177
44276
  }
44178
44277
  function writeLifecycle(event, fields = {}) {
44179
44278
  const path = lifecycleLogPath();
44180
44279
  try {
44181
- mkdirSync14(dirname21(path), { recursive: true });
44182
- if ((statSync13(path, { throwIfNoEntry: false })?.size ?? 0) > LIFECYCLE_LOG_LIMIT) {
44280
+ mkdirSync17(dirname22(path), { recursive: true });
44281
+ if ((statSync16(path, { throwIfNoEntry: false })?.size ?? 0) > LIFECYCLE_LOG_LIMIT) {
44183
44282
  try {
44184
44283
  renameSync5(path, `${path}.1`);
44185
44284
  } catch {}
44186
44285
  }
44187
44286
  const line = {
44188
44287
  at: new Date().toISOString(),
44189
- pid: process11.pid,
44190
- ppid: process11.ppid,
44288
+ pid: process14.pid,
44289
+ ppid: process14.ppid,
44191
44290
  version: SERVER_VERSION,
44192
44291
  event
44193
44292
  };
@@ -44353,7 +44452,11 @@ function notListeningNote(servers, greetedAgoMs) {
44353
44452
  const named = down.map((one) => `its ${one.name} on port ${one.port}`).join(" and ");
44354
44453
  const needs = down.map((one) => one.name === "debug adapter" ? "played runs and the debug tools need the debug adapter" : "diagnostics, symbols and renames need the language server").join(", and ");
44355
44454
  const starting = greetedAgoMs !== null && greetedAgoMs < STARTING_MS ? ` The editor greeted ${Math.round(greetedAgoMs / 1000)}s ago, so ${down.length === 1 ? "it" : "they"} may still be starting: ask again in a few seconds.` : "";
44356
- return `The editor names ${named}, and nothing accepts connections there. Godot binds each once, as the editor starts, and reports a failure only in the editor's own log panel; ${needs}. editor_launch restart binds ${down.length === 1 ? "it" : "them"} again.${starting}`;
44455
+ const cures = [
44456
+ ...servers.dap.listening === false ? ["editor_launch rebind binds the debug adapter again in place"] : [],
44457
+ ...servers.lsp.listening === false ? ["editor_launch restart binds the language server again"] : []
44458
+ ].join(", and ");
44459
+ return `The editor names ${named}, and nothing accepts connections there. Godot binds each once, as the editor starts, and reports a failure only in the editor's own log panel; ${needs}. ${cures}.${starting}`;
44357
44460
  }
44358
44461
  var STARTING_MS = 20000;
44359
44462
  var init_port_probe = () => {};
@@ -44533,16 +44636,16 @@ var init_prompts = __esm(() => {
44533
44636
  });
44534
44637
 
44535
44638
  // src/restart-note.ts
44536
- import { mkdirSync as mkdirSync15, readFileSync as readFileSync25, rmSync as rmSync13, writeFileSync as writeFileSync17 } from "node:fs";
44537
- import { dirname as dirname22, join as join32 } from "node:path";
44639
+ import { mkdirSync as mkdirSync18, readFileSync as readFileSync29, rmSync as rmSync15, writeFileSync as writeFileSync18 } from "node:fs";
44640
+ import { dirname as dirname23, join as join35 } from "node:path";
44538
44641
  function restartNotePath(projectPath) {
44539
- return join32(projectPath, ".godot", "gdharness-restart.json");
44642
+ return join35(projectPath, ".godot", "gdharness-restart.json");
44540
44643
  }
44541
44644
  function noteRestartBegun(note) {
44542
44645
  const path = restartNotePath(note.projectPath);
44543
44646
  try {
44544
- mkdirSync15(dirname22(path), { recursive: true });
44545
- writeFileSync17(path, `${JSON.stringify(note, null, 2)}
44647
+ mkdirSync18(dirname23(path), { recursive: true });
44648
+ writeFileSync18(path, `${JSON.stringify(note, null, 2)}
44546
44649
  `, "utf8");
44547
44650
  } catch (error) {
44548
44651
  console.error(`[SERVER] Could not record the restart at ${path}: ${errorMessage(error)}`);
@@ -44550,7 +44653,7 @@ function noteRestartBegun(note) {
44550
44653
  }
44551
44654
  function restartOwed(projectPath) {
44552
44655
  try {
44553
- const parsed = JSON.parse(readFileSync25(restartNotePath(projectPath), "utf8"));
44656
+ const parsed = JSON.parse(readFileSync29(restartNotePath(projectPath), "utf8"));
44554
44657
  if (typeof parsed !== "object" || parsed === null) {
44555
44658
  return null;
44556
44659
  }
@@ -44565,7 +44668,7 @@ function restartOwed(projectPath) {
44565
44668
  }
44566
44669
  function restartSettled(projectPath) {
44567
44670
  try {
44568
- rmSync13(restartNotePath(projectPath), { force: true });
44671
+ rmSync15(restartNotePath(projectPath), { force: true });
44569
44672
  } catch (error) {
44570
44673
  console.error(`[SERVER] Could not clear the restart note: ${errorMessage(error)}`);
44571
44674
  }
@@ -44575,32 +44678,32 @@ var init_restart_note = __esm(() => {
44575
44678
  });
44576
44679
 
44577
44680
  // src/run-record.ts
44578
- import { createHash as createHash6 } from "node:crypto";
44579
- import { mkdirSync as mkdirSync16, openSync as openSync3, readdirSync as readdirSync14, readFileSync as readFileSync26, rmSync as rmSync14, statSync as statSync14 } from "node:fs";
44580
- import { basename as basename10, join as join33, resolve as resolve7 } from "node:path";
44681
+ import { createHash as createHash7 } from "node:crypto";
44682
+ import { mkdirSync as mkdirSync19, openSync as openSync5, readdirSync as readdirSync16, readFileSync as readFileSync30, rmSync as rmSync16, statSync as statSync17 } from "node:fs";
44683
+ import { basename as basename10, join as join36, resolve as resolve8 } from "node:path";
44581
44684
  function runsDirectory() {
44582
- return join33(runtimeDirectory(), "runs");
44685
+ return join36(runtimeDirectory(), "runs");
44583
44686
  }
44584
44687
  function projectKey(projectPath) {
44585
- const real = realAsFarAsItExists(resolve7(projectPath)).replace(/[\\/]+$/, "");
44688
+ const real = realAsFarAsItExists(resolve8(projectPath)).replace(/[\\/]+$/, "");
44586
44689
  const folded = process.platform === "win32" || process.platform === "darwin" ? real.toLowerCase() : real;
44587
- return createHash6("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
44690
+ return createHash7("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
44588
44691
  }
44589
44692
  function runRecordPath(projectPath, directory = runtimeDirectory()) {
44590
- return join33(directory, "runs", `note-${projectKey(projectPath)}.json`);
44693
+ return join36(directory, "runs", `note-${projectKey(projectPath)}.json`);
44591
44694
  }
44592
44695
  function latestRunPath(servedBy, directory = runtimeDirectory()) {
44593
- return join33(directory, "runs", `latest-${projectKey(servedBy)}.json`);
44696
+ return join36(directory, "runs", `latest-${projectKey(servedBy)}.json`);
44594
44697
  }
44595
44698
  function writeLatestRun(servedBy, latest) {
44596
44699
  try {
44597
- mkdirSync16(runsDirectory(), { recursive: true });
44700
+ mkdirSync19(runsDirectory(), { recursive: true });
44598
44701
  writeWhole(latestRunPath(servedBy), JSON.stringify(latest, null, 2));
44599
44702
  } catch {}
44600
44703
  }
44601
44704
  function readLatestRun(servedBy, directory = runtimeDirectory()) {
44602
44705
  try {
44603
- const fields = JSON.parse(readFileSync26(latestRunPath(servedBy, directory), "utf8"));
44706
+ const fields = JSON.parse(readFileSync30(latestRunPath(servedBy, directory), "utf8"));
44604
44707
  if (typeof fields !== "object" || fields === null) {
44605
44708
  return null;
44606
44709
  }
@@ -44614,24 +44717,24 @@ function readLatestRun(servedBy, directory = runtimeDirectory()) {
44614
44717
  }
44615
44718
  }
44616
44719
  function openTranscript(startedAt) {
44617
- mkdirSync16(runsDirectory(), { recursive: true });
44618
- const path = join33(runsDirectory(), `run-${startedAt}.log`);
44619
- return { path, fd: openSync3(path, "a") };
44720
+ mkdirSync19(runsDirectory(), { recursive: true });
44721
+ const path = join36(runsDirectory(), `run-${startedAt}.log`);
44722
+ return { path, fd: openSync5(path, "a") };
44620
44723
  }
44621
44724
  function profilePath(startedAt) {
44622
- return join33(runsDirectory(), `run-${startedAt}.profile.json`);
44725
+ return join36(runsDirectory(), `run-${startedAt}.profile.json`);
44623
44726
  }
44624
44727
  function profileMarkPath(startedAt) {
44625
- return join33(runsDirectory(), `run-${startedAt}.profile-mark.json`);
44728
+ return join36(runsDirectory(), `run-${startedAt}.profile-mark.json`);
44626
44729
  }
44627
44730
  function noteIsAbout(note, playing) {
44628
44731
  return note.play !== undefined && playing !== null && note.play.play === playing.play && note.play.editorPid === playing.editorPid;
44629
44732
  }
44630
44733
  function editorNotePath(projectPath, directory = runtimeDirectory()) {
44631
- return join33(directory, "runs", `editor-${projectKey(projectPath)}.json`);
44734
+ return join36(directory, "runs", `editor-${projectKey(projectPath)}.json`);
44632
44735
  }
44633
44736
  function writeEditorRunNote(note) {
44634
- mkdirSync16(runsDirectory(), { recursive: true });
44737
+ mkdirSync19(runsDirectory(), { recursive: true });
44635
44738
  const path = editorNotePath(note.projectPath);
44636
44739
  if (!writeWhole(path, JSON.stringify(note, null, 2))) {
44637
44740
  throw new Error(`the editor run note could not be written at ${path}`);
@@ -44649,7 +44752,7 @@ function readEditorRunNote(projectPath) {
44649
44752
  function editorNoteAt(path) {
44650
44753
  let parsed;
44651
44754
  try {
44652
- parsed = JSON.parse(readFileSync26(path, "utf8"));
44755
+ parsed = JSON.parse(readFileSync30(path, "utf8"));
44653
44756
  } catch {
44654
44757
  return null;
44655
44758
  }
@@ -44681,13 +44784,13 @@ function everyRunRecord() {
44681
44784
  for (const directory of runtimeDirectories()) {
44682
44785
  let entries;
44683
44786
  try {
44684
- entries = readdirSync14(join33(directory, "runs"));
44787
+ entries = readdirSync16(join36(directory, "runs"));
44685
44788
  } catch {
44686
44789
  continue;
44687
44790
  }
44688
44791
  for (const entry of entries) {
44689
44792
  if (entry.startsWith("note-") && entry.endsWith(".json")) {
44690
- const record = recordAt(join33(directory, "runs", entry));
44793
+ const record = recordAt(join36(directory, "runs", entry));
44691
44794
  if (record !== null) {
44692
44795
  found.push(record);
44693
44796
  }
@@ -44699,7 +44802,7 @@ function everyRunRecord() {
44699
44802
  function recordAt(path) {
44700
44803
  let parsed;
44701
44804
  try {
44702
- parsed = JSON.parse(readFileSync26(path, "utf8"));
44805
+ parsed = JSON.parse(readFileSync30(path, "utf8"));
44703
44806
  } catch {
44704
44807
  return null;
44705
44808
  }
@@ -44758,7 +44861,7 @@ async function runningAs(pid) {
44758
44861
  const began = await startedAt(pid, null);
44759
44862
  const when = began === null ? {} : { startedAt: began };
44760
44863
  if (process.platform === "linux") {
44761
- const raw = readFileSync26(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
44864
+ const raw = readFileSync30(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
44762
44865
  return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
44763
44866
  }
44764
44867
  const args = (await runHidden("ps", ["-p", String(pid), "-o", "args="], { timeout: 15000 })).stdout.trim();
@@ -44896,13 +44999,13 @@ function judgeAnnouncedGame(running, projectPath, announcedAt) {
44896
44999
  function clearRunRecord(projectPath) {
44897
45000
  for (const directory of runtimeDirectories()) {
44898
45001
  try {
44899
- rmSync14(runRecordPath(projectPath, directory), { force: true });
45002
+ rmSync16(runRecordPath(projectPath, directory), { force: true });
44900
45003
  } catch {}
44901
45004
  }
44902
45005
  }
44903
45006
  function ageMs(path, now) {
44904
45007
  try {
44905
- return now - statSync14(path).mtimeMs;
45008
+ return now - statSync17(path).mtimeMs;
44906
45009
  } catch {
44907
45010
  return 0;
44908
45011
  }
@@ -44916,7 +45019,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
44916
45019
  ]));
44917
45020
  let entries;
44918
45021
  try {
44919
- entries = readdirSync14(directory);
45022
+ entries = readdirSync16(directory);
44920
45023
  } catch {
44921
45024
  return;
44922
45025
  }
@@ -44925,12 +45028,12 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
44925
45028
  if (!transcript && !entry.endsWith(".partial")) {
44926
45029
  continue;
44927
45030
  }
44928
- const path = join33(directory, entry);
45031
+ const path = join36(directory, entry);
44929
45032
  if (kept.has(path) || ageMs(path, now) < keepMs) {
44930
45033
  continue;
44931
45034
  }
44932
45035
  try {
44933
- rmSync14(path);
45036
+ rmSync16(path);
44934
45037
  } catch {}
44935
45038
  }
44936
45039
  }
@@ -44946,10 +45049,10 @@ var init_run_record = __esm(() => {
44946
45049
  });
44947
45050
 
44948
45051
  // src/script-profile.ts
44949
- import { readFileSync as readFileSync27 } from "node:fs";
45052
+ import { readFileSync as readFileSync31 } from "node:fs";
44950
45053
  function readProfileFile(path) {
44951
45054
  try {
44952
- const parsed = JSON.parse(readFileSync27(path, "utf8"));
45055
+ const parsed = JSON.parse(readFileSync31(path, "utf8"));
44953
45056
  return typeof parsed === "object" && typeof parsed.connected === "boolean" ? parsed : null;
44954
45057
  } catch {
44955
45058
  return null;
@@ -45006,7 +45109,7 @@ function writeProfileMark(path, mark) {
45006
45109
  }
45007
45110
  function readProfileMark(path) {
45008
45111
  try {
45009
- const parsed = JSON.parse(readFileSync27(path, "utf8"));
45112
+ const parsed = JSON.parse(readFileSync31(path, "utf8"));
45010
45113
  return typeof parsed === "object" && typeof parsed.totals === "object" ? parsed : null;
45011
45114
  } catch {
45012
45115
  return null;
@@ -45173,16 +45276,16 @@ var init_server_instructions = __esm(() => {
45173
45276
  });
45174
45277
 
45175
45278
  // src/test-selection.ts
45176
- import { existsSync as existsSync26, readFileSync as readFileSync28 } from "node:fs";
45177
- import { basename as basename11, join as join34 } from "node:path";
45279
+ import { existsSync as existsSync29, readFileSync as readFileSync32 } from "node:fs";
45280
+ import { basename as basename11, join as join37 } from "node:path";
45178
45281
  function casesIn(projectPath, suite) {
45179
- const text = readFileSync28(join34(projectPath, suite.slice("res://".length)), "utf8");
45282
+ const text = readFileSync32(join37(projectPath, suite.slice("res://".length)), "utf8");
45180
45283
  return [...text.matchAll(/^func\s+(test_\w*)\s*\(/gm)].map((match) => match[1] ?? "");
45181
45284
  }
45182
45285
  function suitesUnder(projectPath, asked) {
45183
45286
  const scripts = projectFiles(projectPath, [".gd"]).map((path) => `res://${path}`);
45184
45287
  return [
45185
- ...new Set(asked.flatMap((path) => path.endsWith(".gd") ? existsSync26(join34(projectPath, path.slice("res://".length))) ? [path] : [] : scripts.filter((script) => script.startsWith(`${path}/`))))
45288
+ ...new Set(asked.flatMap((path) => path.endsWith(".gd") ? existsSync29(join37(projectPath, path.slice("res://".length))) ? [path] : [] : scripts.filter((script) => script.startsWith(`${path}/`))))
45186
45289
  ];
45187
45290
  }
45188
45291
  function selectTests(projectPath, asked, given) {
@@ -45240,22 +45343,22 @@ var init_test_selection = __esm(() => {
45240
45343
  import { randomUUID as randomUUID2 } from "node:crypto";
45241
45344
  import {
45242
45345
  appendFileSync as appendFileSync2,
45243
- closeSync as closeSync3,
45346
+ closeSync as closeSync5,
45244
45347
  constants,
45245
45348
  copyFileSync,
45246
- existsSync as existsSync27,
45247
- fstatSync,
45248
- mkdirSync as mkdirSync17,
45249
- openSync as openSync4,
45250
- readdirSync as readdirSync15,
45251
- readFileSync as readFileSync29,
45252
- readSync as readSync2,
45349
+ existsSync as existsSync30,
45350
+ fstatSync as fstatSync2,
45351
+ mkdirSync as mkdirSync20,
45352
+ openSync as openSync6,
45353
+ readdirSync as readdirSync17,
45354
+ readFileSync as readFileSync33,
45355
+ readSync as readSync3,
45253
45356
  rmdirSync as rmdirSync2,
45254
- statSync as statSync15,
45255
- writeFileSync as writeFileSync18
45357
+ statSync as statSync18,
45358
+ writeFileSync as writeFileSync19
45256
45359
  } from "node:fs";
45257
- import { basename as basename12, dirname as dirname23, extname as extname4, isAbsolute as isAbsolute5, join as join35, normalize as normalize2, relative as relative6 } from "node:path";
45258
- import { setTimeout as delay5 } from "node:timers/promises";
45360
+ import { basename as basename12, dirname as dirname24, extname as extname4, isAbsolute as isAbsolute6, join as join38, normalize as normalize2, relative as relative7 } from "node:path";
45361
+ import { setTimeout as delay7 } from "node:timers/promises";
45259
45362
  import { fileURLToPath as fileURLToPath5 } from "node:url";
45260
45363
  function editorRestartTimeoutMs() {
45261
45364
  return waitFromEnv("GDHARNESS_EDITOR_RESTART_TIMEOUT_MS", 90000);
@@ -45284,22 +45387,22 @@ function writtenFileRefusal(path, rule) {
45284
45387
  if (/^(res|user):\/\//.test(path)) {
45285
45388
  return `${named} ${path} is a Godot path; ${what} is saved outside the project, at an absolute path on this machine.`;
45286
45389
  }
45287
- if (!isAbsolute5(path)) {
45390
+ if (!isAbsolute6(path)) {
45288
45391
  return `${named} ${path} is not absolute; name the whole path, so where it is written does not depend on this server's directory.`;
45289
45392
  }
45290
45393
  if (!rule.extensions.includes(extname4(path).toLowerCase())) {
45291
45394
  return `${named} ${path} does not end in ${rule.extensions.join(" or ")}, which is what ${what} is.`;
45292
45395
  }
45293
- const directory = realAsFarAsItExists(dirname23(path));
45294
- if (!existsSync27(directory)) {
45295
- return `${named}'s directory ${dirname23(path)} does not exist; ${what} creates no directories.`;
45396
+ const directory = realAsFarAsItExists(dirname24(path));
45397
+ if (!existsSync30(directory)) {
45398
+ return `${named}'s directory ${dirname24(path)} does not exist; ${what} creates no directories.`;
45296
45399
  }
45297
- const where = join35(directory, basename12(path));
45400
+ const where = join38(directory, basename12(path));
45298
45401
  const inside = rule.projects.find((project) => project !== "" && isWithinRoot(realAsFarAsItExists(project), where));
45299
45402
  if (inside !== undefined) {
45300
45403
  return `${named} ${path} is inside the project at ${inside}; ${what} is saved outside it, so it never becomes part of the game.`;
45301
45404
  }
45302
- const taken = rule.writes(where).find((file) => existsSync27(file));
45405
+ const taken = rule.writes(where).find((file) => existsSync30(file));
45303
45406
  if (taken !== undefined) {
45304
45407
  return taken === where ? `${named} ${path} already exists; ${what} is not written over a file. Name a new one.` : `${named} ${path} would write ${taken}, which already exists; ${what} is not written over a file. Name a new one.`;
45305
45408
  }
@@ -45385,7 +45488,7 @@ function keptGameRequest(tool, op, args) {
45385
45488
  }
45386
45489
  }
45387
45490
  function projectSpelling(projectPath, absolutePath) {
45388
- return `res://${relative6(realAsFarAsItExists(projectPath), realAsFarAsItExists(absolutePath)).replaceAll("\\", "/")}`;
45491
+ return `res://${relative7(realAsFarAsItExists(projectPath), realAsFarAsItExists(absolutePath)).replaceAll("\\", "/")}`;
45389
45492
  }
45390
45493
  function whichGame(args) {
45391
45494
  return { projectPath: args["projectPath"], pid: args["pid"] };
@@ -45473,7 +45576,7 @@ async function untilGone(pids, withinMs = stopWaitMs()) {
45473
45576
  const gone = () => pids.every((pid) => !alive(pid));
45474
45577
  const deadline = Date.now() + withinMs;
45475
45578
  while (!gone() && Date.now() < deadline) {
45476
- await delay5(50);
45579
+ await delay7(50);
45477
45580
  }
45478
45581
  return gone();
45479
45582
  }
@@ -45684,7 +45787,7 @@ function endedToStartThis(ended) {
45684
45787
  return ` ${which} was ended to start this one; its output is no longer what editor_output answers about.`;
45685
45788
  }
45686
45789
  function lineBeyondScript(absolutePath, relativePath, line) {
45687
- const text = readFileSync29(absolutePath, "utf8");
45790
+ const text = readFileSync33(absolutePath, "utf8");
45688
45791
  const lines = text === "" ? 0 : text.split(`
45689
45792
  `).length - (text.endsWith(`
45690
45793
  `) ? 1 : 0);
@@ -45884,7 +45987,7 @@ function notRunNote(notRun, failFast) {
45884
45987
  function exportPresetRefusal(projectPath, preset) {
45885
45988
  let text;
45886
45989
  try {
45887
- text = readFileSync29(join35(projectPath, "export_presets.cfg"), "utf8");
45990
+ text = readFileSync33(join38(projectPath, "export_presets.cfg"), "utf8");
45888
45991
  } catch {
45889
45992
  return `This project has no export_presets.cfg, so it has no preset '${preset}' to export with. project_export add_preset writes one, as the editor's Project > Export does.`;
45890
45993
  }
@@ -45948,9 +46051,9 @@ function writtenBeside(before, after, output, within) {
45948
46051
  function filesIn(folder) {
45949
46052
  const found = new Map;
45950
46053
  try {
45951
- for (const entry of readdirSync15(folder, { withFileTypes: true })) {
46054
+ for (const entry of readdirSync17(folder, { withFileTypes: true })) {
45952
46055
  if (entry.isFile()) {
45953
- found.set(entry.name, statSync15(join35(folder, entry.name)).mtimeMs);
46056
+ found.set(entry.name, statSync18(join38(folder, entry.name)).mtimeMs);
45954
46057
  }
45955
46058
  }
45956
46059
  } catch {}
@@ -45982,7 +46085,7 @@ function unsavedInEditorNote(scripts) {
45982
46085
  return `${named} ${one ? "is" : "are"} open in the editor's script ${one ? "tab" : "tabs"} with unsaved edits, and saving ${one ? "that tab" : "them"} there writes ${one ? "its" : "their"} text over what was written here: close ${one ? "it" : "them"} without saving, or save first and edit again.`;
45983
46086
  }
45984
46087
  function editorAddonMissing(projectPath) {
45985
- if (!existsSync27(join35(projectPath, "project.godot"))) {
46088
+ if (!existsSync30(join38(projectPath, "project.godot"))) {
45986
46089
  return null;
45987
46090
  }
45988
46091
  const report = inspectProject(projectPath);
@@ -46247,7 +46350,7 @@ function profileFrameFunctions(projectPath) {
46247
46350
  let projectGodot = null;
46248
46351
  if (projectPath !== null) {
46249
46352
  try {
46250
- projectGodot = readFileSync29(join35(projectPath, "project.godot"), "utf8");
46353
+ projectGodot = readFileSync33(join38(projectPath, "project.godot"), "utf8");
46251
46354
  } catch {}
46252
46355
  }
46253
46356
  return frameFunctionsFor(queuedMessagesOf(projectGodot));
@@ -46271,7 +46374,7 @@ function projectSourceOf(projectPath) {
46271
46374
  let source = null;
46272
46375
  if (inside.ok) {
46273
46376
  try {
46274
- source = readFileSync29(inside.absolutePath, "utf8");
46377
+ source = readFileSync33(inside.absolutePath, "utf8");
46275
46378
  } catch {}
46276
46379
  }
46277
46380
  read.set(script, source);
@@ -46426,7 +46529,7 @@ function foldedEntriesAnswer(log, floor = "warning") {
46426
46529
  }
46427
46530
  function gdUnitFailsOnScriptErrors(projectPath) {
46428
46531
  try {
46429
- return settingKeys(readFileSync29(join35(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
46532
+ return settingKeys(readFileSync33(join38(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
46430
46533
  } catch {
46431
46534
  return true;
46432
46535
  }
@@ -46441,13 +46544,13 @@ function uncaughtNote(uncaught) {
46441
46544
  }
46442
46545
  function elsewhereIn(projectPath, asked) {
46443
46546
  const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
46444
- if (wanted === "" || existsSync27(join35(projectPath, wanted))) {
46547
+ if (wanted === "" || existsSync30(join38(projectPath, wanted))) {
46445
46548
  return "";
46446
46549
  }
46447
46550
  const root = (wanted.split("/")[0] ?? "").toLowerCase();
46448
46551
  let near = [];
46449
46552
  try {
46450
- near = readdirSync15(projectPath, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.toLowerCase() !== root).map((entry) => entry.name).filter((name) => name.toLowerCase().startsWith("test") || root.startsWith(name.toLowerCase()));
46553
+ near = readdirSync17(projectPath, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.toLowerCase() !== root).map((entry) => entry.name).filter((name) => name.toLowerCase().startsWith("test") || root.startsWith(name.toLowerCase()));
46451
46554
  } catch {
46452
46555
  return "";
46453
46556
  }
@@ -46538,23 +46641,23 @@ function engineChecked(why) {
46538
46641
  }
46539
46642
  function isDirectory(path) {
46540
46643
  try {
46541
- return statSync15(path).isDirectory();
46644
+ return statSync18(path).isDirectory();
46542
46645
  } catch {
46543
46646
  return false;
46544
46647
  }
46545
46648
  }
46546
46649
  function scriptsUnder(directory, prefix) {
46547
- if (existsSync27(join35(directory, ".gdignore"))) {
46650
+ if (existsSync30(join38(directory, ".gdignore"))) {
46548
46651
  return [];
46549
46652
  }
46550
46653
  const found = [];
46551
- for (const entry of readdirSync15(directory, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : 1)) {
46654
+ for (const entry of readdirSync17(directory, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : 1)) {
46552
46655
  if (entry.name.startsWith(".")) {
46553
46656
  continue;
46554
46657
  }
46555
46658
  const path = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
46556
46659
  if (entry.isDirectory()) {
46557
- found.push(...scriptsUnder(join35(directory, entry.name), path));
46660
+ found.push(...scriptsUnder(join38(directory, entry.name), path));
46558
46661
  } else if (entry.isFile() && entry.name.endsWith(".gd")) {
46559
46662
  found.push(path);
46560
46663
  }
@@ -46562,11 +46665,11 @@ function scriptsUnder(directory, prefix) {
46562
46665
  return found;
46563
46666
  }
46564
46667
  function mainSceneOf(projectFile) {
46565
- const scene = parseProjectGodot(readFileSync29(projectFile, "utf8"))["application"]?.["run/main_scene"];
46668
+ const scene = parseProjectGodot(readFileSync33(projectFile, "utf8"))["application"]?.["run/main_scene"];
46566
46669
  return typeof scene === "string" && scene !== "" ? scene : null;
46567
46670
  }
46568
46671
  function editorPlaysHeadless(projectFile) {
46569
- const runArgs = parseProjectGodot(readFileSync29(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
46672
+ const runArgs = parseProjectGodot(readFileSync33(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
46570
46673
  return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
46571
46674
  }
46572
46675
  function ownArguments(params) {
@@ -46584,7 +46687,8 @@ function needsItsOwnProcess(asked) {
46584
46687
  class GodotServer {
46585
46688
  stdio = null;
46586
46689
  locator = new GodotLocator;
46587
- operationsScript = join35(__dirname2, "godot", "operations", "godot_operations.gd");
46690
+ operationsScript = join38(__dirname2, "godot", "operations", "godot_operations.gd");
46691
+ listedRunner = shippedListedRunner();
46588
46692
  godotBridge;
46589
46693
  tools = buildToolDefinitions();
46590
46694
  activeProcess = null;
@@ -46715,7 +46819,7 @@ class GodotServer {
46715
46819
  taken = true;
46716
46820
  this.logDebug(`Took port ${wanted} over from pid ${announced.pid}`);
46717
46821
  } catch {
46718
- await delay5(HANDOVER_POLL_MS);
46822
+ await delay7(HANDOVER_POLL_MS);
46719
46823
  }
46720
46824
  }
46721
46825
  this.notHandedOverBy = taken ? null : announced.pid;
@@ -46846,14 +46950,14 @@ class GodotServer {
46846
46950
  }
46847
46951
  noteTheStart() {
46848
46952
  const cwd = process.cwd();
46849
- const mcpJson = join35(cwd, ".mcp.json");
46953
+ const mcpJson = join38(cwd, ".mcp.json");
46850
46954
  const announced = this.ownProject === null ? null : readAnnouncement(announcementPath(this.ownProject));
46851
46955
  const predecessor = announced !== null && announced.pid !== process.pid && alive(announced.pid) ? announced.pid : null;
46852
46956
  writeLifecycle("started", {
46853
46957
  argv: process.argv.slice(1),
46854
46958
  cwd,
46855
46959
  project: this.ownProject,
46856
- mcpJson: existsSync27(mcpJson) ? mcpJson : null
46960
+ mcpJson: existsSync30(mcpJson) ? mcpJson : null
46857
46961
  });
46858
46962
  if (predecessor !== null) {
46859
46963
  writeLifecycle("replaced", { project: this.ownProject, predecessor });
@@ -46994,7 +47098,7 @@ class GodotServer {
46994
47098
  return checked.response;
46995
47099
  }
46996
47100
  const named = args["projectPath"];
46997
- if (typeof named === "string" && existsSync27(join35(named, "project.godot"))) {
47101
+ if (typeof named === "string" && existsSync30(join38(named, "project.godot"))) {
46998
47102
  this.lastProjectPath = named;
46999
47103
  }
47000
47104
  this.updates.refresh();
@@ -47030,8 +47134,8 @@ class GodotServer {
47030
47134
  let notSaved;
47031
47135
  try {
47032
47136
  savedAt = answer.content.map((item, index) => {
47033
- const file = join35(directory, item.type === "text" ? `${stem}-${index}.txt` : `${stem}-${index}.${item.mimeType.split("/")[1] ?? "bin"}`);
47034
- writeFileSync18(file, item.type === "text" ? item.text : Buffer.from(item.data, "base64"));
47137
+ const file = join38(directory, item.type === "text" ? `${stem}-${index}.txt` : `${stem}-${index}.${item.mimeType.split("/")[1] ?? "bin"}`);
47138
+ writeFileSync19(file, item.type === "text" ? item.text : Buffer.from(item.data, "base64"));
47035
47139
  return file;
47036
47140
  });
47037
47141
  } catch (error) {
@@ -47449,6 +47553,9 @@ class GodotServer {
47449
47553
  if (op === "close") {
47450
47554
  return await this.handleCloseEditor();
47451
47555
  }
47556
+ if (op === "rebind") {
47557
+ return await this.handleRebindEditorServers();
47558
+ }
47452
47559
  return op === "restart" ? await this.handleRestartEditor(args) : await this.handleLaunchEditor(args);
47453
47560
  case "editor_run":
47454
47561
  if (op === "stop") {
@@ -47594,7 +47701,7 @@ class GodotServer {
47594
47701
  return this.createErrorResponse("line is required: the one-based line to stop on.");
47595
47702
  }
47596
47703
  if (op === "set") {
47597
- if (!existsSync27(located.absolutePath)) {
47704
+ if (!existsSync30(located.absolutePath)) {
47598
47705
  return this.missingScript(project.value.path, located.relativePath);
47599
47706
  }
47600
47707
  const beyond = lineBeyondScript(located.absolutePath, located.relativePath, line);
@@ -47687,8 +47794,8 @@ class GodotServer {
47687
47794
  if (path === undefined) {
47688
47795
  return { ok: false, response: this.createErrorResponse("projectPath is required.") };
47689
47796
  }
47690
- const file = join35(path, "project.godot");
47691
- if (!existsSync27(file)) {
47797
+ const file = join38(path, "project.godot");
47798
+ if (!existsSync30(file)) {
47692
47799
  return {
47693
47800
  ok: false,
47694
47801
  response: this.createErrorResponse(`Not a Godot project: ${path}`, [
@@ -47708,7 +47815,7 @@ class GodotServer {
47708
47815
  }
47709
47816
  runEnvironment(args) {
47710
47817
  const savesIn = readNonEmptyString(args, "savesIn");
47711
- if (savesIn !== undefined && !isAbsolute5(savesIn)) {
47818
+ if (savesIn !== undefined && !isAbsolute6(savesIn)) {
47712
47819
  return {
47713
47820
  ok: false,
47714
47821
  response: this.createErrorResponse(`savesIn must be an absolute path, not "${savesIn}".`, [
@@ -48389,7 +48496,7 @@ class GodotServer {
48389
48496
  if (!engine.ok) {
48390
48497
  return engine.response;
48391
48498
  }
48392
- const application = parseProjectGodot(readFileSync29(project.value.file, "utf8"))["application"] ?? {};
48499
+ const application = parseProjectGodot(readFileSync33(project.value.file, "utf8"))["application"] ?? {};
48393
48500
  const name = application["config/name"];
48394
48501
  const mainScene = application["run/main_scene"];
48395
48502
  const info = {
@@ -48500,18 +48607,18 @@ class GodotServer {
48500
48607
  if (!engine.ok) {
48501
48608
  return engine.response;
48502
48609
  }
48503
- const folder = dirname23(output.absolutePath);
48610
+ const folder = dirname24(output.absolutePath);
48504
48611
  try {
48505
- mkdirSync17(folder, { recursive: true });
48612
+ mkdirSync20(folder, { recursive: true });
48506
48613
  } catch (error) {
48507
- return this.createErrorResponse(`Cannot create ${dirname23(output.relativePath)} to export into: ${errorMessage(error)}`);
48614
+ return this.createErrorResponse(`Cannot create ${dirname24(output.relativePath)} to export into: ${errorMessage(error)}`);
48508
48615
  }
48509
48616
  const debug = readBoolean(args, "debug") ?? false;
48510
48617
  const exportLogs = scratchDirectory("export");
48511
48618
  const exportArgs = [
48512
48619
  "--headless",
48513
48620
  "--log-file",
48514
- join35(exportLogs, "engine.log"),
48621
+ join38(exportLogs, "engine.log"),
48515
48622
  "--path",
48516
48623
  project.value.path,
48517
48624
  debug ? "--export-debug" : "--export-release",
@@ -48519,7 +48626,7 @@ class GodotServer {
48519
48626
  output.absolutePath
48520
48627
  ];
48521
48628
  this.logDebug(`Exporting: ${engine.value} ${exportArgs.join(" ")}`);
48522
- const writtenAt = () => existsSync27(output.absolutePath) ? statSync15(output.absolutePath).mtimeMs : null;
48629
+ const writtenAt = () => existsSync30(output.absolutePath) ? statSync18(output.absolutePath).mtimeMs : null;
48523
48630
  const caches = editorCacheDirectory(engine.value);
48524
48631
  const exportOnce = async () => {
48525
48632
  const before = writtenAt();
@@ -48548,7 +48655,7 @@ class GodotServer {
48548
48655
  if (ending.exitSignal === null && ending.exitCode === null) {
48549
48656
  return this.createErrorResponse(`Export could not be run: ${ending.failure ?? "the engine gave no exit status"}`);
48550
48657
  }
48551
- const beside = writtenBeside(listedBefore, filesIn(folder), basename12(output.absolutePath), dirname23(output.relativePath).replaceAll("\\", "/"));
48658
+ const beside = writtenBeside(listedBefore, filesIn(folder), basename12(output.absolutePath), dirname24(output.relativePath).replaceAll("\\", "/"));
48552
48659
  return exportAnswer(ending, { preset, outputPath: output.relativePath, debug }, before, writtenAt(), beside, docCacheNote);
48553
48660
  }
48554
48661
  reportTestProgress(log) {
@@ -48602,13 +48709,12 @@ class GodotServer {
48602
48709
  if (movie !== null) {
48603
48710
  return this.createErrorResponse(movie);
48604
48711
  }
48605
- const missing = paths.value.filter((path) => !existsSync27(join35(project.value.path, path.slice("res://".length))));
48712
+ const missing = paths.value.filter((path) => !existsSync30(join38(project.value.path, path.slice("res://".length))));
48606
48713
  const asked = paths.value.filter((path) => !missing.includes(path));
48607
- const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
48608
- const noRunner = !existsSync27(join35(project.value.path, runner));
48714
+ const unusable = gdUnit4Unusable(project.value.path);
48609
48715
  if (asked.length === 0) {
48610
48716
  const verdict = `nothing at ${missing.join(" and ")}`;
48611
- const andNoRunner = noRunner ? `, and gdUnit4 is not installed in this project: no ${runner}` : "";
48717
+ const andNoRunner = unusable === null ? "" : `, and ${unusable.reason}`;
48612
48718
  return {
48613
48719
  content: [
48614
48720
  {
@@ -48621,16 +48727,16 @@ class GodotServer {
48621
48727
  passed: false,
48622
48728
  verdict,
48623
48729
  tests: 0,
48624
- ...noRunner ? { gdUnit4Installed: false } : {}
48730
+ ...unusable?.missing === true ? { gdUnit4Installed: false } : {}
48625
48731
  })
48626
48732
  }
48627
48733
  ],
48628
48734
  isError: true
48629
48735
  };
48630
48736
  }
48631
- if (noRunner) {
48632
- return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
48633
- "Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
48737
+ if (unusable !== null) {
48738
+ return this.createErrorResponse(`${unusable.reason}.`, [
48739
+ `${unusable.missing ? "Install" : "Update"} gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4`
48634
48740
  ]);
48635
48741
  }
48636
48742
  const selection = selectTests(project.value.path, asked, args["tests"]);
@@ -48658,25 +48764,37 @@ class GodotServer {
48658
48764
  const ours = `gdharness-reports/${randomUUID2()}`;
48659
48765
  const reports = `res://.godot/${ours}`;
48660
48766
  const ignored = readStringArray(args, "ignore") ?? [];
48767
+ const userData = scratchDirectory("tests");
48768
+ const suitesFile = join38(userData, "suites.txt");
48769
+ const skippingFile = join38(userData, "skipping.txt");
48770
+ writeFileSync19(suitesFile, `${(selection.value?.suites ?? asked).join(`
48771
+ `)}
48772
+ `);
48773
+ writeFileSync19(skippingFile, `${[...ignored, ...selection.value?.ignore ?? []].join(`
48774
+ `)}
48775
+ `);
48661
48776
  const cmdArgs = [
48662
48777
  "--headless",
48663
48778
  "--path",
48664
48779
  project.value.path,
48665
48780
  ...flags.value,
48666
48781
  "-s",
48667
- `res://${runner}`,
48782
+ this.listedRunner,
48668
48783
  "--ignoreHeadlessMode",
48669
48784
  ...readBoolean(args, "failFast") === true ? [] : ["-c"],
48670
- ...(selection.value?.suites ?? asked).flatMap((path) => ["-a", path]),
48671
- ...[...ignored, ...selection.value?.ignore ?? []].flatMap((entry) => ["-i", entry]),
48672
48785
  "-rd",
48673
48786
  reports,
48674
48787
  "-rc",
48675
- "1"
48788
+ "1",
48789
+ "--",
48790
+ "--suites-in",
48791
+ suitesFile,
48792
+ "--skipping-in",
48793
+ skippingFile,
48794
+ "--html-report-off"
48676
48795
  ];
48677
48796
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
48678
48797
  this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
48679
- const userData = scratchDirectory("tests");
48680
48798
  const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
48681
48799
  const console2 = [];
48682
48800
  let lastSaid = Date.now();
@@ -48725,21 +48843,21 @@ class GodotServer {
48725
48843
  await run.settled;
48726
48844
  const tookMs = Date.now() - began;
48727
48845
  const notCompiled = stopped.for ?? run.log.all.map((entry) => autoloadNotCompiled(entry, autoloaded)).find((found) => found !== null) ?? null;
48728
- const reportsDir = join35(project.value.path, ".godot", ...ours.split("/"));
48846
+ const reportsDir = join38(project.value.path, ".godot", ...ours.split("/"));
48729
48847
  let report = null;
48730
48848
  let reportProblem = null;
48731
48849
  try {
48732
- const written = existsSync27(reportsDir) && notCompiled === null ? readdirSync15(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
48850
+ const written = existsSync30(reportsDir) && notCompiled === null ? readdirSync17(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
48733
48851
  const newest = written.at(-1);
48734
48852
  if (newest !== undefined) {
48735
- report = parseJUnit(readFileSync29(join35(reportsDir, newest, "results.xml"), "utf8"));
48853
+ report = parseJUnit(readFileSync33(join38(reportsDir, newest, "results.xml"), "utf8"));
48736
48854
  }
48737
48855
  } catch (error) {
48738
48856
  reportProblem = errorMessage(error);
48739
48857
  } finally {
48740
48858
  discard(reportsDir, userData);
48741
48859
  try {
48742
- rmdirSync2(dirname23(reportsDir));
48860
+ rmdirSync2(dirname24(reportsDir));
48743
48861
  } catch {}
48744
48862
  }
48745
48863
  const engineEntries = foldedRepeats(run.log.select({
@@ -48938,7 +49056,7 @@ class GodotServer {
48938
49056
  if (!location.ok) {
48939
49057
  return this.createErrorResponse(location.reason, PATH_SOLUTIONS);
48940
49058
  }
48941
- if (!existsSync27(location.absolutePath)) {
49059
+ if (!existsSync30(location.absolutePath)) {
48942
49060
  return this.missingScript(project.value.path, location.relativePath);
48943
49061
  }
48944
49062
  }
@@ -49120,11 +49238,11 @@ class GodotServer {
49120
49238
  const cached = cachedClasses(projectPath);
49121
49239
  const sourceOf = (resourcePath) => {
49122
49240
  const contained = resolveWithinProject(projectPath, resourcePath);
49123
- if (!contained.ok || !existsSync27(contained.absolutePath)) {
49241
+ if (!contained.ok || !existsSync30(contained.absolutePath)) {
49124
49242
  return null;
49125
49243
  }
49126
49244
  try {
49127
- return readFileSync29(contained.absolutePath, "utf8");
49245
+ return readFileSync33(contained.absolutePath, "utf8");
49128
49246
  } catch {
49129
49247
  return null;
49130
49248
  }
@@ -49214,11 +49332,11 @@ class GodotServer {
49214
49332
  }
49215
49333
  sourceOfClass(projectPath, resourcePath) {
49216
49334
  const contained = resolveWithinProject(projectPath, resourcePath);
49217
- if (!contained.ok || !existsSync27(contained.absolutePath)) {
49335
+ if (!contained.ok || !existsSync30(contained.absolutePath)) {
49218
49336
  return null;
49219
49337
  }
49220
49338
  try {
49221
- return readFileSync29(contained.absolutePath, "utf8");
49339
+ return readFileSync33(contained.absolutePath, "utf8");
49222
49340
  } catch {
49223
49341
  return null;
49224
49342
  }
@@ -49375,7 +49493,7 @@ class GodotServer {
49375
49493
  ...Array.isArray(stack) ? {
49376
49494
  stack: stack.map((frame) => ({
49377
49495
  ...frame,
49378
- ...projectPath !== "" && typeof frame["script"] === "string" && frame["script"].startsWith("res://") ? { path: join35(projectPath, frame["script"].slice("res://".length)) } : {}
49496
+ ...projectPath !== "" && typeof frame["script"] === "string" && frame["script"].startsWith("res://") ? { path: join38(projectPath, frame["script"].slice("res://".length)) } : {}
49379
49497
  }))
49380
49498
  } : {}
49381
49499
  });
@@ -49499,7 +49617,7 @@ class GodotServer {
49499
49617
  try {
49500
49618
  const askedAt = Date.now();
49501
49619
  const asked = this.godotBridge.invokeTool("playing_status", {}, { counted: false });
49502
- const answer = await Promise.race([asked, delay5(PLAYING_STATUS_MS).then(() => null)]);
49620
+ const answer = await Promise.race([asked, delay7(PLAYING_STATUS_MS).then(() => null)]);
49503
49621
  if (answer === null) {
49504
49622
  this.logDebug(`playing_status did not answer within ${PLAYING_STATUS_MS}ms`);
49505
49623
  asked.then(() => {
@@ -49524,7 +49642,7 @@ class GodotServer {
49524
49642
  try {
49525
49643
  const answer = await Promise.race([
49526
49644
  this.godotBridge.invokeTool("scan_status", {}, { counted: false }),
49527
- delay5(PLAYING_STATUS_MS).then(() => null)
49645
+ delay7(PLAYING_STATUS_MS).then(() => null)
49528
49646
  ]);
49529
49647
  if (answer === null) {
49530
49648
  return null;
@@ -49643,18 +49761,63 @@ class GodotServer {
49643
49761
  const deadline = Date.now() + EDITOR_SERVERS_START_MS;
49644
49762
  for (;; ) {
49645
49763
  const answer = await this.editorServersListening();
49646
- if (answer.serversNote === undefined || Date.now() >= deadline) {
49764
+ const adapterFailed = answer.lspListening === true && answer.dapListening === false;
49765
+ if (answer.serversNote === undefined || adapterFailed || Date.now() >= deadline) {
49647
49766
  return answer;
49648
49767
  }
49649
- await delay5(500);
49768
+ await delay7(500);
49650
49769
  }
49651
49770
  }
49771
+ async rebindTheDebugAdapter(servers) {
49772
+ if (servers.dapListening !== false && servers.lspListening !== false) {
49773
+ return { servers, report: {} };
49774
+ }
49775
+ const { rebound, said } = await this.editorServersSaid(servers.dapListening === false);
49776
+ const after = rebound ? await this.editorServersListening() : servers;
49777
+ return {
49778
+ servers: after,
49779
+ report: {
49780
+ ...said.length === 0 ? {} : { serversSaid: said },
49781
+ ...rebound ? { debugAdapterRebound: after.dapListening === true } : {}
49782
+ }
49783
+ };
49784
+ }
49785
+ async editorServersSaid(rebind) {
49786
+ let answer = {};
49787
+ try {
49788
+ answer = asParams(await this.godotBridge.invokeTool("editor_servers", { rebind }, { counted: false }));
49789
+ } catch (error) {
49790
+ this.logDebug(`editor_servers did not answer: ${errorMessage(error)}`);
49791
+ }
49792
+ return {
49793
+ rebound: readBoolean(answer, "rebound") === true,
49794
+ said: [...readStringArray(answer, "said") ?? [], ...readStringArray(answer, "saidSince") ?? []]
49795
+ };
49796
+ }
49797
+ async handleRebindEditorServers() {
49798
+ if (!this.godotBridge.getStatus().connected) {
49799
+ return this.createErrorResponse("No editor is connected, so there is no debug adapter to bind.", [
49800
+ "editor_launch opens one on a project",
49801
+ "editor_status says whether the bridge is up and what has reached it"
49802
+ ]);
49803
+ }
49804
+ const { rebound, said } = await this.editorServersSaid(true);
49805
+ if (!rebound) {
49806
+ return this.createErrorResponse("The editor has no debug adapter to bind again: no DebugAdapterServer is in its tree.", ["editor_status says what the editor serves"]);
49807
+ }
49808
+ const servers = await this.editorServersListening();
49809
+ return this.jsonTextResponse({
49810
+ rebound: true,
49811
+ ...servers,
49812
+ ...said.length === 0 ? {} : { serversSaid: said }
49813
+ });
49814
+ }
49652
49815
  settingKeysOf(projectPath) {
49653
49816
  if (projectPath === undefined) {
49654
49817
  return null;
49655
49818
  }
49656
49819
  try {
49657
- return settingKeys(readFileSync29(join35(projectPath, "project.godot"), "utf8"));
49820
+ return settingKeys(readFileSync33(join38(projectPath, "project.godot"), "utf8"));
49658
49821
  } catch {
49659
49822
  return null;
49660
49823
  }
@@ -49692,7 +49855,7 @@ class GodotServer {
49692
49855
  "editor_status says whether anything has reached the bridge since"
49693
49856
  ]);
49694
49857
  }
49695
- const servers = await this.editorServersOnceStarted();
49858
+ const { servers, report: rebinding } = await this.rebindTheDebugAdapter(await this.editorServersOnceStarted());
49696
49859
  const now = this.godotBridge.getStatus();
49697
49860
  const after = this.settingKeysOf(before.projectPath);
49698
49861
  return this.jsonTextResponse({
@@ -49704,6 +49867,7 @@ class GodotServer {
49704
49867
  staleNote: this.godotBridge.anotherRelease(),
49705
49868
  ...this.whatTheEditorDropped(settingsBefore, after),
49706
49869
  ...servers,
49870
+ ...rebinding,
49707
49871
  ...mine && this.launchedEditor !== null ? { arguments: this.launchedEditor.arguments } : {},
49708
49872
  tookMs: Date.now() - began
49709
49873
  });
@@ -49837,7 +50001,7 @@ class GodotServer {
49837
50001
  if (Date.now() >= deadline) {
49838
50002
  return false;
49839
50003
  }
49840
- await delay5(250);
50004
+ await delay7(250);
49841
50005
  }
49842
50006
  }
49843
50007
  async handleLaunchEditor(args) {
@@ -50096,7 +50260,7 @@ class GodotServer {
50096
50260
  }
50097
50261
  async runtimeUp(projectPath, before, budget, withArgs) {
50098
50262
  const budgetMs = budget.ms;
50099
- if (!existsSync27(join35(projectPath, RUNTIME_AUTOLOAD.path))) {
50263
+ if (!existsSync30(join38(projectPath, RUNTIME_AUTOLOAD.path))) {
50100
50264
  return runtimeVerdict(null, {
50101
50265
  addon: false,
50102
50266
  budgetMs,
@@ -50184,7 +50348,7 @@ class GodotServer {
50184
50348
  }
50185
50349
  let announcedAt;
50186
50350
  try {
50187
- announcedAt = statSync15(endpoint.file).mtimeMs;
50351
+ announcedAt = statSync18(endpoint.file).mtimeMs;
50188
50352
  } catch {
50189
50353
  return;
50190
50354
  }
@@ -50204,7 +50368,7 @@ class GodotServer {
50204
50368
  } catch (error) {
50205
50369
  return this.createErrorResponse(`The editor is connected but its debug adapter is not: ${errorMessage(error)}`, [
50206
50370
  `This asked on port ${this.dap().port}, which is where the connected editor says it serves`,
50207
- "Godot binds its debug adapter once, as the editor starts, and reports a failure only in the editor's own log panel: editor_launch restart binds it again",
50371
+ "Godot binds its debug adapter once, as the editor starts, and reports a failure only in the editor's own log panel: editor_launch rebind binds it again in place",
50208
50372
  "GDHARNESS_DAP_PORT points this server at another one"
50209
50373
  ]);
50210
50374
  }
@@ -50234,7 +50398,7 @@ class GodotServer {
50234
50398
  }
50235
50399
  const startedAt = Date.now();
50236
50400
  const transcript = openTranscript(startedAt);
50237
- closeSync3(transcript.fd);
50401
+ closeSync5(transcript.fd);
50238
50402
  const playAnswer = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
50239
50403
  const which = playOf(playAnswer);
50240
50404
  const played = {
@@ -50324,18 +50488,18 @@ class GodotServer {
50324
50488
  }
50325
50489
  let handle;
50326
50490
  try {
50327
- handle = openSync4(game.errorReport, "r");
50491
+ handle = openSync6(game.errorReport, "r");
50328
50492
  } catch {
50329
50493
  return;
50330
50494
  }
50331
50495
  try {
50332
50496
  const from = game.errorReportOffset ?? 0;
50333
- const size = fstatSync(handle).size;
50497
+ const size = fstatSync2(handle).size;
50334
50498
  if (size <= from) {
50335
50499
  return;
50336
50500
  }
50337
50501
  const buffer = Buffer.alloc(size - from);
50338
- const read = readSync2(handle, buffer, 0, buffer.length, from);
50502
+ const read = readSync3(handle, buffer, 0, buffer.length, from);
50339
50503
  let reported = buffer.subarray(0, read);
50340
50504
  if (game.consoleFromReport !== true) {
50341
50505
  const boundary = consoleBoundaryAt(reported);
@@ -50358,7 +50522,7 @@ class GodotServer {
50358
50522
  game.log.append("stderr", reported);
50359
50523
  }
50360
50524
  } finally {
50361
- closeSync3(handle);
50525
+ closeSync5(handle);
50362
50526
  }
50363
50527
  }
50364
50528
  announcedPidNamedBy(run, line) {
@@ -50378,7 +50542,7 @@ class GodotServer {
50378
50542
  return 0;
50379
50543
  }
50380
50544
  try {
50381
- return reportAlreadyInTranscript(readFileSync29(game.transcript), readFileSync29(report));
50545
+ return reportAlreadyInTranscript(readFileSync33(game.transcript), readFileSync33(report));
50382
50546
  } catch (error) {
50383
50547
  this.logDebug(`Could not compare a picked-up run's report with its transcript: ${errorMessage(error)}`);
50384
50548
  return 0;
@@ -50401,7 +50565,7 @@ class GodotServer {
50401
50565
  if (Date.now() >= deadline) {
50402
50566
  return false;
50403
50567
  }
50404
- await delay5(100);
50568
+ await delay7(100);
50405
50569
  }
50406
50570
  }
50407
50571
  async endActiveGame(reason, going, graceful) {
@@ -50627,7 +50791,7 @@ class GodotServer {
50627
50791
  async launchKeptGame(godotPath, cmdArgs, projectPath, env, desktop, profiled = false, debugged = true) {
50628
50792
  const startedAt = Date.now();
50629
50793
  const transcript = openTranscript(startedAt);
50630
- closeSync3(transcript.fd);
50794
+ closeSync5(transcript.fd);
50631
50795
  const gameEnv = profiled ? { ...env ?? process.env, [PROFILE_FROM_START]: "1" } : env;
50632
50796
  const profile = profiled ? profilePath(startedAt) : undefined;
50633
50797
  const launched = await launchOutsideTheTree({
@@ -50675,21 +50839,21 @@ class GodotServer {
50675
50839
  }
50676
50840
  let handle;
50677
50841
  try {
50678
- handle = openSync4(game.transcript, "r");
50842
+ handle = openSync6(game.transcript, "r");
50679
50843
  } catch {
50680
50844
  return;
50681
50845
  }
50682
50846
  try {
50683
- const size = fstatSync(handle).size;
50847
+ const size = fstatSync2(handle).size;
50684
50848
  if (size <= game.readOffset) {
50685
50849
  return;
50686
50850
  }
50687
50851
  const buffer = Buffer.alloc(size - game.readOffset);
50688
- const read = readSync2(handle, buffer, 0, buffer.length, game.readOffset);
50852
+ const read = readSync3(handle, buffer, 0, buffer.length, game.readOffset);
50689
50853
  game.readOffset += read;
50690
50854
  game.log.append("transcript", buffer.subarray(0, read));
50691
50855
  } finally {
50692
- closeSync3(handle);
50856
+ closeSync5(handle);
50693
50857
  }
50694
50858
  }
50695
50859
  async pickUpWhatTheEditorIsPlaying() {
@@ -51073,7 +51237,7 @@ class GodotServer {
51073
51237
  };
51074
51238
  const going = await this.runStillGoing(run);
51075
51239
  const projectPath = run.projectPath;
51076
- const projectGone = projectPath !== null && !existsSync27(join35(projectPath, "project.godot"));
51240
+ const projectGone = projectPath !== null && !existsSync30(join38(projectPath, "project.godot"));
51077
51241
  if (projectGone && callersName !== undefined) {
51078
51242
  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"]);
51079
51243
  }
@@ -51231,7 +51395,7 @@ class GodotServer {
51231
51395
  return true;
51232
51396
  }
51233
51397
  }
51234
- await delay5(100);
51398
+ await delay7(100);
51235
51399
  }
51236
51400
  return false;
51237
51401
  }
@@ -51251,7 +51415,7 @@ class GodotServer {
51251
51415
  while (cancelled?.aborted !== true && await this.runStillGoing(run) && Date.now() < until) {
51252
51416
  this.drainEditorOutput(run);
51253
51417
  this.drainTranscript(run);
51254
- await delay5(RUN_POLL_MS);
51418
+ await delay7(RUN_POLL_MS);
51255
51419
  }
51256
51420
  return await this.handleGetDebugOutput(args, await this.runStillGoing(run) ? budgetMs : undefined);
51257
51421
  }
@@ -51293,7 +51457,7 @@ class GodotServer {
51293
51457
  if (Date.now() >= until) {
51294
51458
  return false;
51295
51459
  }
51296
- await delay5(50);
51460
+ await delay7(50);
51297
51461
  }
51298
51462
  }
51299
51463
  async handleEditorConsole(args) {
@@ -51420,7 +51584,7 @@ class GodotServer {
51420
51584
  notes.push(endedWithoutACode({
51421
51585
  throughEditor: run.throughEditor,
51422
51586
  announced: announced !== undefined || run.announcedPid !== undefined,
51423
- couldAnnounce: run.projectPath !== null && existsSync27(join35(run.projectPath, RUNTIME_AUTOLOAD.path)),
51587
+ couldAnnounce: run.projectPath !== null && existsSync30(join38(run.projectPath, RUNTIME_AUTOLOAD.path)),
51424
51588
  endedHere: typeof run.endedHere === "string",
51425
51589
  pickedUp: run.pickedUp === true
51426
51590
  }));
@@ -52049,7 +52213,7 @@ class GodotServer {
52049
52213
  if (!scriptPath.ok) {
52050
52214
  return this.createErrorResponse(scriptPath.reason);
52051
52215
  }
52052
- if (!existsSync27(scriptPath.absolutePath)) {
52216
+ if (!existsSync30(scriptPath.absolutePath)) {
52053
52217
  return this.missingScript(projectPath, scriptPath.relativePath);
52054
52218
  }
52055
52219
  const asked = readNonEmptyString(args, "newScriptPath");
@@ -52463,7 +52627,7 @@ class GodotServer {
52463
52627
  }
52464
52628
  const escaped = symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
52465
52629
  const declared = new RegExp(`^\\s*(?:@\\w+(?:\\([^)]*\\))?\\s+)*class_name\\s+${escaped}\\b`, "m");
52466
- if (declared.test(readFileSync29(scriptPath, "utf8"))) {
52630
+ if (declared.test(readFileSync33(scriptPath, "utf8"))) {
52467
52631
  return null;
52468
52632
  }
52469
52633
  const checked = await this.classesTheEditorCannotSee(args);
@@ -52629,7 +52793,7 @@ class GodotServer {
52629
52793
  async ownRunOnItsWay(projectPath, budgetMs) {
52630
52794
  const run = await this.currentRun();
52631
52795
  const project = projectPath ?? run?.projectPath ?? null;
52632
- if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync27(join35(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
52796
+ if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync30(join38(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
52633
52797
  return null;
52634
52798
  }
52635
52799
  const already = runtimesAnnounced().running;
@@ -52638,7 +52802,7 @@ class GodotServer {
52638
52802
  return !run.announcedBefore.has(one.pid);
52639
52803
  }
52640
52804
  try {
52641
- return statSync15(one.file).mtimeMs >= run.startedAt;
52805
+ return statSync18(one.file).mtimeMs >= run.startedAt;
52642
52806
  } catch {
52643
52807
  return false;
52644
52808
  }
@@ -52723,7 +52887,7 @@ class GodotServer {
52723
52887
  return this.createErrorResponse(staleNote);
52724
52888
  }
52725
52889
  const screenshotDir = expectsScreenshot ? scratchDirectory("runtime-screenshot") : null;
52726
- const screenshotPath = screenshotDir ? join35(screenshotDir, "capture.png") : null;
52890
+ const screenshotPath = screenshotDir ? join38(screenshotDir, "capture.png") : null;
52727
52891
  const refused = (message) => this.createErrorResponse(message);
52728
52892
  const onGame = {
52729
52893
  pid: choice.endpoint.pid,
@@ -52784,7 +52948,7 @@ class GodotServer {
52784
52948
 
52785
52949
  Ran alongside ${ranWith.join(", ")}. ${ALONGSIDE_NOTE}`}`
52786
52950
  },
52787
- { type: "image", data: readFileSync29(screenshotPath).toString("base64"), mimeType: "image/png" }
52951
+ { type: "image", data: readFileSync33(screenshotPath).toString("base64"), mimeType: "image/png" }
52788
52952
  ]
52789
52953
  };
52790
52954
  } finally {
@@ -52963,7 +53127,7 @@ var init_server = __esm(() => {
52963
53127
  init_tool_definitions();
52964
53128
  init_tool_reference();
52965
53129
  init_update_check();
52966
- __dirname2 = dirname23(fileURLToPath5(import.meta.url));
53130
+ __dirname2 = dirname24(fileURLToPath5(import.meta.url));
52967
53131
  KEEPER_DEBUG_MS = PAUSE_MS + STEP_MS + 2 * READ_MS;
52968
53132
  CONSOLE_WRAPPER = /_console(\.exe)?$/i;
52969
53133
  LONGEST_BOOT_NOTED_MS = 5 * 60000;
@@ -53023,16 +53187,45 @@ var init_server = __esm(() => {
53023
53187
 
53024
53188
  // src/cli.ts
53025
53189
  init_bridge_announce();
53190
+ import { existsSync as existsSync31 } from "node:fs";
53191
+ import { join as join39, resolve as resolve9 } from "node:path";
53192
+
53193
+ // src/error-mode.ts
53194
+ import process3 from "node:process";
53195
+ var NO_FAULT_ERROR_BOX = 2;
53196
+ var cleared = process3.platform !== "win32";
53197
+ async function letWindowsReportCrashes() {
53198
+ if (cleared || !("bun" in process3.versions)) {
53199
+ return;
53200
+ }
53201
+ try {
53202
+ const { dlopen, FFIType } = await import("bun:ffi");
53203
+ const kernel32 = dlopen("kernel32.dll", {
53204
+ GetErrorMode: { args: [], returns: FFIType.u32 },
53205
+ SetErrorMode: { args: [FFIType.u32], returns: FFIType.u32 }
53206
+ });
53207
+ try {
53208
+ const mode = kernel32.symbols.GetErrorMode();
53209
+ kernel32.symbols.SetErrorMode(mode & ~NO_FAULT_ERROR_BOX);
53210
+ cleared = true;
53211
+ } finally {
53212
+ kernel32.close();
53213
+ }
53214
+ } catch {}
53215
+ }
53216
+ function crashesReported() {
53217
+ return cleared;
53218
+ }
53219
+
53220
+ // src/cli.ts
53026
53221
  init_errors();
53027
53222
  init_godot_path();
53028
- import { existsSync as existsSync28 } from "node:fs";
53029
- import { join as join36, resolve as resolve8 } from "node:path";
53030
53223
 
53031
53224
  // src/harnesses.ts
53032
53225
  import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, statSync as statSync3, writeFileSync as writeFileSync2 } from "node:fs";
53033
53226
  import { homedir as homedir2 } from "node:os";
53034
53227
  import { basename as basename3, dirname as dirname4, join as join5, relative as relative2, resolve as resolve2 } from "node:path";
53035
- import process4 from "node:process";
53228
+ import process5 from "node:process";
53036
53229
 
53037
53230
  // node_modules/yaml/dist/index.js
53038
53231
  var composer = require_composer();
@@ -53601,12 +53794,12 @@ var HARNESSES = [
53601
53794
  }
53602
53795
  ];
53603
53796
  function appDataRoot() {
53604
- return process4.env["APPDATA"] ?? join5(homedir2(), "AppData", "Roaming");
53797
+ return process5.env["APPDATA"] ?? join5(homedir2(), "AppData", "Roaming");
53605
53798
  }
53606
53799
  function homePath(path) {
53607
53800
  return typeof path === "string" ? join5(homedir2(), path) : join5(appDataRoot(), path.appData);
53608
53801
  }
53609
- function fileFor(harness, platform = process4.platform) {
53802
+ function fileFor(harness, platform = process5.platform) {
53610
53803
  if (typeof harness.file === "string") {
53611
53804
  return harness.file;
53612
53805
  }
@@ -53619,7 +53812,7 @@ function configPath(harness, projectPath) {
53619
53812
  }
53620
53813
  return homePath(file);
53621
53814
  }
53622
- function displayPath(harness, platform = process4.platform) {
53815
+ function displayPath(harness, platform = process5.platform) {
53623
53816
  const file = fileFor(harness, platform);
53624
53817
  if (typeof file !== "string") {
53625
53818
  return `%APPDATA%/${file.appData.replaceAll("\\", "/")}`;
@@ -53961,10 +54154,10 @@ init_issues();
53961
54154
  init_paths();
53962
54155
 
53963
54156
  // src/prompt.ts
53964
- import process9 from "node:process";
54157
+ import process10 from "node:process";
53965
54158
  import { createInterface } from "node:readline/promises";
53966
54159
  function interactive() {
53967
- return process9.stdin.isTTY && process9.stdout.isTTY;
54160
+ return process10.stdin.isTTY && process10.stdout.isTTY;
53968
54161
  }
53969
54162
 
53970
54163
  class Ask {
@@ -53973,7 +54166,7 @@ class Ask {
53973
54166
  if (!interactive()) {
53974
54167
  return fallback;
53975
54168
  }
53976
- this.line ??= createInterface({ input: process9.stdin, output: process9.stdout });
54169
+ this.line ??= createInterface({ input: process10.stdin, output: process10.stdout });
53977
54170
  const answer = (await this.line.question(`${question} ${fallback ? "[Y/n]" : "[y/N]"} `)).trim();
53978
54171
  return answer === "" ? fallback : answer.toLowerCase().startsWith("y");
53979
54172
  }
@@ -54287,19 +54480,832 @@ function writeSkill(directories, version) {
54287
54480
  });
54288
54481
  }
54289
54482
 
54483
+ // src/suite-run.ts
54484
+ init_alive();
54485
+ init_desktop();
54486
+ import {
54487
+ closeSync as closeSync3,
54488
+ existsSync as existsSync18,
54489
+ fstatSync,
54490
+ mkdirSync as mkdirSync8,
54491
+ openSync as openSync3,
54492
+ readdirSync as readdirSync11,
54493
+ readFileSync as readFileSync17,
54494
+ readSync,
54495
+ rmSync as rmSync10,
54496
+ statSync as statSync11,
54497
+ writeFileSync as writeFileSync10
54498
+ } from "node:fs";
54499
+ import { availableParallelism, freemem } from "node:os";
54500
+ import { isAbsolute as isAbsolute4, join as join21, relative as relative3, resolve as resolve4, sep as sep2 } from "node:path";
54501
+ import process12 from "node:process";
54502
+ import { setTimeout as delay3 } from "node:timers/promises";
54503
+
54504
+ // src/engine-slots.ts
54505
+ init_alive();
54506
+ init_process_children();
54507
+ import { closeSync as closeSync2, mkdirSync as mkdirSync6, openSync as openSync2, readFileSync as readFileSync13, rmSync as rmSync8, statSync as statSync9, writeSync as writeSync2 } from "node:fs";
54508
+ import { join as join18 } from "node:path";
54509
+ import process11 from "node:process";
54510
+ import { setTimeout as delay2 } from "node:timers/promises";
54511
+ var START_TOLERANCE_MS = 2000;
54512
+ var UNWRITTEN_MS = 1e4;
54513
+ var SLOT_WAIT_MS = 50;
54514
+ var START_READ_MS = 1e4;
54515
+ function holderOf(content) {
54516
+ try {
54517
+ const parsed = JSON.parse(content);
54518
+ if (typeof parsed !== "object" || parsed === null) {
54519
+ return null;
54520
+ }
54521
+ const { pid, started } = parsed;
54522
+ return typeof pid === "number" && Number.isInteger(pid) && pid > 0 && typeof started === "number" ? { pid, started } : null;
54523
+ } catch {
54524
+ return null;
54525
+ }
54526
+ }
54527
+ function lockIsFree(content, writtenAt, now, isAlive, startOf) {
54528
+ const holder = holderOf(content);
54529
+ if (holder === null) {
54530
+ return now - writtenAt > UNWRITTEN_MS;
54531
+ }
54532
+ if (!isAlive(holder.pid)) {
54533
+ return true;
54534
+ }
54535
+ const started = startOf(holder.pid);
54536
+ return started !== undefined && Math.abs(started - holder.started) > START_TOLERANCE_MS;
54537
+ }
54538
+ async function ownStart() {
54539
+ const said = (await startTimesOf([process11.pid])).get(process11.pid);
54540
+ return said ?? Math.round(Date.now() - process11.uptime() * 1000);
54541
+ }
54542
+
54543
+ class EngineSlots {
54544
+ starts = new Map;
54545
+ held = new Set;
54546
+ directory;
54547
+ count;
54548
+ holder;
54549
+ constructor(directory, count, holder) {
54550
+ this.directory = directory;
54551
+ this.count = count;
54552
+ this.holder = holder;
54553
+ }
54554
+ async take(signal) {
54555
+ mkdirSync6(this.directory, { recursive: true });
54556
+ for (;; ) {
54557
+ signal?.throwIfAborted();
54558
+ for (let slot = 0;slot < this.count; slot += 1) {
54559
+ const lock = join18(this.directory, `${String(slot)}.lock`);
54560
+ if (this.made(lock) || await this.takenOver(lock)) {
54561
+ this.held.add(lock);
54562
+ return lock;
54563
+ }
54564
+ }
54565
+ await delay2(SLOT_WAIT_MS, undefined, signal === undefined ? {} : { signal });
54566
+ }
54567
+ }
54568
+ releaseAll() {
54569
+ for (const lock of this.held) {
54570
+ this.release(lock);
54571
+ }
54572
+ }
54573
+ release(lock) {
54574
+ this.held.delete(lock);
54575
+ try {
54576
+ if (holderOf(readFileSync13(lock, "utf8"))?.pid === this.holder.pid) {
54577
+ rmSync8(lock, { force: true });
54578
+ }
54579
+ } catch {}
54580
+ }
54581
+ made(path) {
54582
+ let made;
54583
+ try {
54584
+ made = openSync2(path, "wx");
54585
+ } catch {
54586
+ return false;
54587
+ }
54588
+ try {
54589
+ writeSync2(made, JSON.stringify(this.holder));
54590
+ } finally {
54591
+ closeSync2(made);
54592
+ }
54593
+ return true;
54594
+ }
54595
+ async takenOver(lock) {
54596
+ if (!await this.free(lock)) {
54597
+ return false;
54598
+ }
54599
+ const steal = `${lock}.steal`;
54600
+ if (!this.made(steal)) {
54601
+ try {
54602
+ if (Date.now() - statSync9(steal).mtimeMs > UNWRITTEN_MS) {
54603
+ rmSync8(steal, { force: true });
54604
+ }
54605
+ } catch {}
54606
+ return false;
54607
+ }
54608
+ try {
54609
+ if (!await this.free(lock)) {
54610
+ return false;
54611
+ }
54612
+ try {
54613
+ rmSync8(lock, { force: true });
54614
+ } catch {
54615
+ return false;
54616
+ }
54617
+ return this.made(lock);
54618
+ } finally {
54619
+ rmSync8(steal, { force: true });
54620
+ }
54621
+ }
54622
+ async free(lock) {
54623
+ let content;
54624
+ let writtenAt;
54625
+ try {
54626
+ content = readFileSync13(lock, "utf8");
54627
+ writtenAt = statSync9(lock).mtimeMs;
54628
+ } catch {
54629
+ return false;
54630
+ }
54631
+ const pid = holderOf(content)?.pid;
54632
+ if (pid !== undefined && alive(pid)) {
54633
+ await this.readStart(pid);
54634
+ }
54635
+ return lockIsFree(content, writtenAt, Date.now(), alive, (asked) => this.starts.get(asked)?.started);
54636
+ }
54637
+ async readStart(pid) {
54638
+ const known = this.starts.get(pid);
54639
+ if (known !== undefined && Date.now() - known.readAt < START_READ_MS) {
54640
+ return;
54641
+ }
54642
+ const started = (await startTimesOf([pid])).get(pid);
54643
+ this.starts.set(pid, { started, readAt: Date.now() });
54644
+ }
54645
+ }
54646
+
54647
+ // src/suite-run.ts
54648
+ init_errors();
54649
+ init_setup();
54650
+ init_spawn();
54651
+
54652
+ // src/suite-fingerprint.ts
54653
+ init_class_cache();
54654
+ import { createHash as createHash5 } from "node:crypto";
54655
+ import { existsSync as existsSync16, readdirSync as readdirSync10, readFileSync as readFileSync14, statSync as statSync10 } from "node:fs";
54656
+ import { join as join19, posix as posix2 } from "node:path";
54657
+ var RES_LITERAL = /res:\/\/[^"'\s)\]<>]*/g;
54658
+ var RELATIVE_LOAD = /\b(?:pre)?load\(\s*["']((?!res:\/\/|uid:\/\/)[^"']+)["']/g;
54659
+ var REACHES_AT_RUN_TIME = [
54660
+ /\bDirAccess\b/,
54661
+ /\blist_directory\b/,
54662
+ /\b(?:load|load_threaded_request)\(\s*(?=[^\s),])/
54663
+ ];
54664
+ var BINARY_RESOURCE = /\.(?:scn|res)$/;
54665
+ var ADDON = /^res:\/\/addons\/[^/]+\//;
54666
+ var PLACEHOLDER = /[%{}]/;
54667
+
54668
+ class SuiteFingerprints {
54669
+ reached = new Map;
54670
+ scripts;
54671
+ everywhere;
54672
+ alwaysDigest;
54673
+ constructor(scripts, always) {
54674
+ this.scripts = scripts;
54675
+ this.everywhere = ["res://project.godot", ...extensionsOf(scripts.projectPath)];
54676
+ this.alwaysDigest = createHash5("sha256").update(always.join(`
54677
+ `)).digest("hex");
54678
+ }
54679
+ of(suite) {
54680
+ const seen = new Set;
54681
+ const pending = [suite.path, ...this.everywhere];
54682
+ for (let path = pending.pop();path !== undefined; path = pending.pop()) {
54683
+ if (seen.has(path)) {
54684
+ continue;
54685
+ }
54686
+ seen.add(path);
54687
+ const reached = this.read(path);
54688
+ if (reached.unreadable !== null) {
54689
+ return null;
54690
+ }
54691
+ pending.push(...reached.reaches);
54692
+ }
54693
+ const hash = createHash5("sha256").update(this.alwaysDigest);
54694
+ for (const path of [...seen].sort()) {
54695
+ hash.update(`${path}
54696
+ ${this.read(path).digest}
54697
+ `);
54698
+ }
54699
+ return hash.digest("hex");
54700
+ }
54701
+ read(asked) {
54702
+ const path = ADDON.exec(asked)?.[0] ?? asked;
54703
+ const known = this.reached.get(path);
54704
+ if (known !== undefined) {
54705
+ return known;
54706
+ }
54707
+ const reached = path === asked ? this.readAnew(path) : this.addon(path);
54708
+ this.reached.set(path, reached);
54709
+ return reached;
54710
+ }
54711
+ addon(root) {
54712
+ const hash = createHash5("sha256");
54713
+ const walk = (directory, under) => {
54714
+ for (const entry of readdirSync10(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
54715
+ const file = join19(directory, entry.name);
54716
+ if (entry.isDirectory()) {
54717
+ walk(file, `${under}${entry.name}/`);
54718
+ } else if (entry.isFile()) {
54719
+ hash.update(`${under}${entry.name}
54720
+ `).update(readFileSync14(file));
54721
+ }
54722
+ }
54723
+ };
54724
+ const directory = join19(this.scripts.projectPath, root.slice("res://".length));
54725
+ if (existsSync16(directory)) {
54726
+ walk(directory, "");
54727
+ }
54728
+ return { digest: hash.digest("hex"), reaches: [], unreadable: null };
54729
+ }
54730
+ readAnew(path) {
54731
+ if (PLACEHOLDER.test(path)) {
54732
+ return { digest: "", reaches: [], unreadable: `${path} is filled in at run time` };
54733
+ }
54734
+ const file = join19(this.scripts.projectPath, path.slice("res://".length));
54735
+ if (!existsSync16(file)) {
54736
+ return { digest: "missing", reaches: [], unreadable: null };
54737
+ }
54738
+ if (statSync10(file).isDirectory()) {
54739
+ const inside = readdirSync10(file, { withFileTypes: true }).filter((entry) => !entry.name.startsWith(".")).map((entry) => `${path.endsWith("/") ? path : `${path}/`}${entry.name}`).sort();
54740
+ return { digest: inside.join(`
54741
+ `), reaches: inside, unreadable: null };
54742
+ }
54743
+ if (BINARY_RESOURCE.test(path)) {
54744
+ return { digest: "", reaches: [], unreadable: `${path} is a binary resource` };
54745
+ }
54746
+ const bytes = readFileSync14(file);
54747
+ const digest = createHash5("sha256").update(bytes).digest("hex");
54748
+ const text = bytes.toString("latin1");
54749
+ const reaches = new Set;
54750
+ for (const [literal] of text.matchAll(RES_LITERAL)) {
54751
+ reaches.add(literal);
54752
+ }
54753
+ if (existsSync16(`${file}.import`)) {
54754
+ reaches.add(`${path}.import`);
54755
+ }
54756
+ if (path.endsWith(".gd")) {
54757
+ const script = this.scripts.script(path);
54758
+ const code = script?.code ?? "";
54759
+ const dynamic = REACHES_AT_RUN_TIME.find((pattern) => pattern.test(code));
54760
+ if (dynamic !== undefined) {
54761
+ return { digest, reaches: [], unreadable: `${path} reaches what it names at run time` };
54762
+ }
54763
+ for (const [, relative] of text.matchAll(RELATIVE_LOAD)) {
54764
+ reaches.add(`res://${posix2.join(posix2.dirname(path.slice("res://".length)), relative ?? "")}`);
54765
+ }
54766
+ const base = this.scripts.base(path);
54767
+ if (base !== null && "path" in base) {
54768
+ reaches.add(base.path);
54769
+ }
54770
+ for (const name of classesNamedIn(code, this.scripts.classes)) {
54771
+ reaches.add(this.scripts.classes.get(name) ?? "");
54772
+ }
54773
+ if (text.includes("uid://")) {
54774
+ return { digest, reaches: [], unreadable: `${path} loads by uid` };
54775
+ }
54776
+ }
54777
+ reaches.delete("");
54778
+ reaches.delete(path);
54779
+ return { digest, reaches: [...reaches], unreadable: null };
54780
+ }
54781
+ }
54782
+ function extensionsOf(projectPath) {
54783
+ const list = join19(projectPath, ".godot", "extension_list.cfg");
54784
+ return existsSync16(list) ? readFileSync14(list, "utf8").split(`
54785
+ `).map((line) => line.trim()).filter((line) => line.startsWith("res://")) : [];
54786
+ }
54787
+
54788
+ // src/suite-judge.ts
54789
+ init_junit();
54790
+ import { stripVTControlCharacters } from "node:util";
54791
+ var SUITE_OPENED = /^Run Test Suite:\s*(res:\/\/.+?\.gd)\s*$/;
54792
+ var TIME = String.raw`(?:\d+h )?(?:\d+min )?(?:\d+s )?\d+ms`;
54793
+ var CASE_LINE = new RegExp(String.raw`^\s*(res://.+?\.gd) > (.+?) (STARTED|PASSED|FAILED|FLAKY|SKIPPED|WARNING)(?: \((?:retry \d+|\d+ retries)\))?(?: (${TIME}))?\s*$`);
54794
+ var STATISTICS = new RegExp(String.raw`^Statistics:.*\|\s*([A-Z]+)(?:\s+\(.*?\))?\s+(${TIME})\s*$`);
54795
+ var ERROR_LINE = /^\s*(?:SCRIPT |USER )?ERROR: (.*?)\s*$/;
54796
+ var EXECUTED = /^Executed test cases\s*:\s*\((\d+)\//;
54797
+ var DRIVER_FALLBACK = /^\s*WARNING: Your video card drivers .*, switching to /;
54798
+ var LEAK = /ObjectDB instances (?:were )?leaked at exit|resources? still in use at exit|Pages in use exist at exit/;
54799
+ var STARVED = [
54800
+ /Parameter "mem(?:_new)?" is null\.?\s*\n\s*at: \w+ \((?:\.\/)?core\/(?:os\/memory\.cpp|templates\/cowdata\.h):/g,
54801
+ /ERROR: Failed decoding WebP image\.\s*\n\s*at: _webp_unpack/g
54802
+ ];
54803
+ function secondsOf(time) {
54804
+ let ms = 0;
54805
+ for (const [, amount, unit] of time.matchAll(/(\d+)(h|min|ms|s)\b/g)) {
54806
+ ms += Number(amount) * (unit === "h" ? 3600000 : unit === "min" ? 60000 : unit === "s" ? 1000 : 1);
54807
+ }
54808
+ return ms / 1000;
54809
+ }
54810
+ function funcOf(name) {
54811
+ const colon = name.indexOf(":");
54812
+ return colon < 0 ? name : name.slice(0, colon);
54813
+ }
54814
+ function readRun(printed) {
54815
+ const lines = stripVTControlCharacters(printed).replaceAll(`\r
54816
+ `, `
54817
+ `).split(`
54818
+ `);
54819
+ const suites = new Map;
54820
+ const errors = [];
54821
+ const leaks = [];
54822
+ let executed = null;
54823
+ let open = null;
54824
+ let during = null;
54825
+ let afterCase = [];
54826
+ let lastCase = null;
54827
+ const settleAfterCase = (owner) => {
54828
+ for (const held of afterCase) {
54829
+ errors.push({ ...held, suite: open?.path ?? null, during: owner });
54830
+ }
54831
+ afterCase = [];
54832
+ };
54833
+ for (const [at, line] of lines.entries()) {
54834
+ const opened = SUITE_OPENED.exec(line);
54835
+ if (opened?.[1] !== undefined) {
54836
+ settleAfterCase("after");
54837
+ open = suites.get(opened[1]) ?? { path: opened[1], cases: [], closed: null };
54838
+ suites.set(open.path, open);
54839
+ during = "before";
54840
+ lastCase = null;
54841
+ continue;
54842
+ }
54843
+ const caseLine = CASE_LINE.exec(line);
54844
+ if (caseLine !== null && open !== null) {
54845
+ const name = caseLine[2] ?? "";
54846
+ if (caseLine[3] === "STARTED") {
54847
+ settleAfterCase(lastCase ?? "before");
54848
+ during = name;
54849
+ } else {
54850
+ open.cases.push({
54851
+ name,
54852
+ func: funcOf(name),
54853
+ verdict: caseLine[3] ?? "",
54854
+ seconds: secondsOf(caseLine[4] ?? ""),
54855
+ at
54856
+ });
54857
+ lastCase = name;
54858
+ during = null;
54859
+ }
54860
+ continue;
54861
+ }
54862
+ const statistics = STATISTICS.exec(line);
54863
+ if (statistics !== null && open !== null) {
54864
+ settleAfterCase("after");
54865
+ open.closed = { verdict: statistics[1] ?? "", seconds: secondsOf(statistics[2] ?? "") };
54866
+ open = null;
54867
+ during = null;
54868
+ lastCase = null;
54869
+ continue;
54870
+ }
54871
+ const counted = EXECUTED.exec(line);
54872
+ if (counted?.[1] !== undefined) {
54873
+ executed = Number(counted[1]);
54874
+ continue;
54875
+ }
54876
+ if (LEAK.test(line)) {
54877
+ leaks.push(line.trim());
54878
+ continue;
54879
+ }
54880
+ if (suites.size === 0 && DRIVER_FALLBACK.test(line)) {
54881
+ errors.length = 0;
54882
+ continue;
54883
+ }
54884
+ const error = ERROR_LINE.exec(line);
54885
+ if (error === null) {
54886
+ continue;
54887
+ }
54888
+ const text = error[1] ?? "";
54889
+ if (open === null) {
54890
+ errors.push({ text, at, suite: null, during: null });
54891
+ } else if (during === null) {
54892
+ afterCase.push({ text, at });
54893
+ } else {
54894
+ errors.push({ text, at, suite: open.path, during });
54895
+ }
54896
+ }
54897
+ const last = open;
54898
+ const stoppedIn = last === null ? null : during === null ? `${last.path}, after ${lastCase ?? "its cases"}` : during === "before" ? `${last.path}, in before` : `${last.path} > ${during}`;
54899
+ settleAfterCase("after");
54900
+ const joined = lines.join(`
54901
+ `);
54902
+ const starved = STARVED.flatMap((pattern) => [...joined.matchAll(pattern)].map((found) => found[0]));
54903
+ const unloadable = scriptErrorsPrinted(lines).map((error) => `${error.path}${error.line === null ? "" : `:${String(error.line)}`}: ${error.message}`);
54904
+ return { lines, suites, errors, leaks, starved, executed, unloadable, stoppedIn };
54905
+ }
54906
+ var WINDOWS_EXITS = new Map([
54907
+ [3221225477, "STATUS_ACCESS_VIOLATION"],
54908
+ [3221225495, "STATUS_NO_MEMORY"],
54909
+ [3221225773, "STATUS_COMMITMENT_LIMIT"],
54910
+ [3221225794, "STATUS_DLL_INIT_FAILED"],
54911
+ [3221226505, "STATUS_STACK_BUFFER_OVERRUN"],
54912
+ [3228369022, "a module that could not be found (VcppException)"],
54913
+ [3228369023, "a procedure that could not be found (VcppException)"]
54914
+ ]);
54915
+ var MEMORY_EXITS = new Set([3221225495, 3221225773]);
54916
+ function exitNamed(code) {
54917
+ const unsigned = code >>> 0;
54918
+ const name = WINDOWS_EXITS.get(unsigned);
54919
+ return name === undefined ? String(code) : `0x${unsigned.toString(16).toUpperCase()} (${name})`;
54920
+ }
54921
+ var TAIL = 60;
54922
+ var MOST_NAMED = 8;
54923
+ function named(items) {
54924
+ const shown = items.slice(0, MOST_NAMED).join(", ");
54925
+ return items.length > MOST_NAMED ? `${shown} and ${String(items.length - MOST_NAMED)} more` : shown;
54926
+ }
54927
+ function judgeEngine(printed, handed, end) {
54928
+ const faults = [];
54929
+ const failing = new Set;
54930
+ const shownAt = new Set;
54931
+ if (end.ended !== null) {
54932
+ faults.push(end.ended);
54933
+ } else if (end.code !== 0) {
54934
+ faults.push(end.code === null ? `the engine was ended by ${end.signal ?? "a signal"}` : `the engine exited ${exitNamed(end.code)}`);
54935
+ }
54936
+ if (printed.unloadable.length > 0) {
54937
+ faults.push(`gdUnit4 could not load ${named(printed.unloadable)}, so it ran nothing`);
54938
+ }
54939
+ if (printed.executed === null || printed.executed === 0) {
54940
+ faults.push(printed.executed === null ? "the run never printed its summary" : "the run executed no test case at all, whatever it exited with");
54941
+ }
54942
+ for (const { path, cases } of handed.suites) {
54943
+ const suite = printed.suites.get(path);
54944
+ if (suite === undefined) {
54945
+ faults.push(`${path} never started`);
54946
+ failing.add(path);
54947
+ continue;
54948
+ }
54949
+ if (suite.closed === null) {
54950
+ faults.push(`${path} never finished`);
54951
+ failing.add(path);
54952
+ } else if (suite.closed.verdict !== "PASSED") {
54953
+ if (suite.cases.every((ran) => ran.verdict !== "FAILED")) {
54954
+ faults.push(`${path} reported ${suite.closed.verdict} with every case passing, so a hook failed`);
54955
+ }
54956
+ failing.add(path);
54957
+ }
54958
+ const finished = new Set(suite.cases.map((ran) => ran.func));
54959
+ const missing = cases.filter((func) => !finished.has(func));
54960
+ if (missing.length > 0 && suite.closed !== null) {
54961
+ faults.push(`${path} closed without running ${named(missing)}`);
54962
+ failing.add(path);
54963
+ }
54964
+ const wanted = new Set(cases);
54965
+ const unasked = [...finished].filter((func) => !wanted.has(func));
54966
+ if (unasked.length > 0) {
54967
+ faults.push(`${path} ran ${named(unasked)}, which gdharness did not read as cases of it to run here, so its ` + "plan of the cases was wrong");
54968
+ failing.add(path);
54969
+ }
54970
+ for (const ran of suite.cases) {
54971
+ if (ran.verdict === "FAILED" || ran.verdict === "FLAKY") {
54972
+ faults.push(ran.verdict === "FLAKY" ? `${path} > ${ran.name} failed before it passed on a retry` : `${path} > ${ran.name} failed`);
54973
+ failing.add(path);
54974
+ for (let at = ran.at;at < printed.lines.length && (at === ran.at || printed.lines[at]?.trim()); at += 1) {
54975
+ shownAt.add(at);
54976
+ }
54977
+ }
54978
+ }
54979
+ }
54980
+ const unexpected = unexpectedErrors(printed, handed);
54981
+ for (const error of unexpected.printed) {
54982
+ const where = error.suite === null ? "outside every suite" : `in ${error.suite} > ${error.during ?? ""}`;
54983
+ faults.push(`an engine error ${where}: ${error.text}`);
54984
+ if (error.suite !== null) {
54985
+ failing.add(error.suite);
54986
+ }
54987
+ shownAt.add(error.at);
54988
+ shownAt.add(error.at + 1);
54989
+ }
54990
+ for (const missing of unexpected.neverPrinted) {
54991
+ faults.push(`${missing.suite} > ${missing.during} declares the error "${missing.text}", which it never printed`);
54992
+ failing.add(missing.suite);
54993
+ }
54994
+ for (const leak of printed.leaks) {
54995
+ faults.push(`the engine exited holding what it should have freed: ${leak}`);
54996
+ }
54997
+ const memoryExit = end.code !== null && MEMORY_EXITS.has(end.code >>> 0);
54998
+ const starved = memoryExit || printed.starved.length > 0;
54999
+ if (printed.starved.length > 0) {
55000
+ faults.push("the engine ran out of memory");
55001
+ }
55002
+ printed.lines.forEach((line, at) => {
55003
+ if (/^(?:Overall Summary|Executed test|Total execution time| ?Exit code)/.test(line.trim())) {
55004
+ shownAt.add(at);
55005
+ }
55006
+ });
55007
+ const passed = faults.length === 0;
55008
+ const passedSuites = handed.suites.map((suite) => suite.path).filter((path) => !failing.has(path));
55009
+ const cutShort = end.ended !== null || printed.executed === null || handed.suites.some(({ path }) => printed.suites.get(path)?.closed == null);
55010
+ return {
55011
+ passed,
55012
+ faults,
55013
+ passedSuites,
55014
+ starved,
55015
+ shown: passed ? [] : linesShown(printed.lines, shownAt, cutShort)
55016
+ };
55017
+ }
55018
+ function linesShown(lines, picked, cutShort) {
55019
+ const summary = [...picked].filter((at) => at < lines.length).sort((a, b) => a - b);
55020
+ const telling = summary.some((at) => !/^(?:Overall Summary|Executed test|Total execution|\s*Exit code)/.test(lines[at]?.trim() ?? ""));
55021
+ if (!telling && !cutShort) {
55022
+ return [];
55023
+ }
55024
+ if (!telling) {
55025
+ return lines.slice(Math.max(0, lines.length - TAIL)).filter((line, at, all) => at < all.length - 1 || line !== "");
55026
+ }
55027
+ const shown = [];
55028
+ let previous = -1;
55029
+ for (const at of summary) {
55030
+ if (previous >= 0 && at > previous + 1) {
55031
+ shown.push("...");
55032
+ }
55033
+ shown.push(lines[at] ?? "");
55034
+ previous = at;
55035
+ }
55036
+ return shown;
55037
+ }
55038
+ function unexpectedErrors(printed, handed) {
55039
+ const owed = new Map;
55040
+ for (const { path } of handed.suites) {
55041
+ const suite = printed.suites.get(path);
55042
+ const declared = handed.expected[path];
55043
+ if (suite === undefined || declared === undefined) {
55044
+ continue;
55045
+ }
55046
+ const ranHooks = suite.closed === null ? ["before"] : ["before", "after"];
55047
+ for (const during of [...ranHooks, ...suite.cases.map((ran) => ran.name)]) {
55048
+ const texts = declared[funcOf(during)];
55049
+ if (Array.isArray(texts) && texts.length > 0) {
55050
+ owed.set(`${path}
55051
+ ${during}`, {
55052
+ suite: path,
55053
+ during,
55054
+ texts: texts.map((text) => String(text))
55055
+ });
55056
+ }
55057
+ }
55058
+ }
55059
+ const unexpected = [];
55060
+ for (const error of printed.errors) {
55061
+ const debt = error.suite === null || error.during === null ? undefined : owed.get(`${error.suite}
55062
+ ${error.during}`);
55063
+ const index = debt?.texts.indexOf(error.text) ?? -1;
55064
+ if (debt === undefined || index < 0) {
55065
+ unexpected.push(error);
55066
+ } else {
55067
+ debt.texts.splice(index, 1);
55068
+ }
55069
+ }
55070
+ const neverPrinted = [...owed.values()].flatMap((debt) => debt.texts.map((text) => ({ suite: debt.suite, during: debt.during, text })));
55071
+ return { printed: unexpected, neverPrinted };
55072
+ }
55073
+
55074
+ // src/suite-plan.ts
55075
+ init_class_cache();
55076
+ init_gdscript_source();
55077
+ init_whole_file();
55078
+ import { existsSync as existsSync17, mkdirSync as mkdirSync7, readFileSync as readFileSync15 } from "node:fs";
55079
+ import { dirname as dirname10, posix as posix3 } from "node:path";
55080
+ var SUITE_BASE = "GdUnitTestSuite";
55081
+ var CASE_PREFIX = "test_";
55082
+ var NEEDS_WINDOW = /^const\s+GDHARNESS_NEEDS_WINDOW\b[^=\n]*=\s*true\b/m;
55083
+ var SCENE_RUNNER = /\bscene_runner\s*\(/;
55084
+
55085
+ class ProjectScripts {
55086
+ sources = new Map;
55087
+ classes = new Map;
55088
+ read = new Map;
55089
+ projectPath;
55090
+ constructor(projectPath) {
55091
+ this.projectPath = projectPath;
55092
+ eachScript(projectPath, (script, source) => {
55093
+ this.sources.set(script, source);
55094
+ const declared = declaredClassOf(source);
55095
+ if (declared !== null) {
55096
+ this.classes.set(declared, script);
55097
+ }
55098
+ });
55099
+ }
55100
+ script(path) {
55101
+ const known = this.read.get(path);
55102
+ if (known !== undefined) {
55103
+ return known;
55104
+ }
55105
+ const source = this.sources.get(path);
55106
+ if (source === undefined) {
55107
+ return;
55108
+ }
55109
+ const shape = shapeOf(source);
55110
+ const read = {
55111
+ source,
55112
+ extendsAs: shape.body.extendsAs,
55113
+ funcs: shape.body.declarations.filter((d) => d.kind === "func").map((d) => d.name),
55114
+ code: codeOf(source)
55115
+ };
55116
+ this.read.set(path, read);
55117
+ return read;
55118
+ }
55119
+ base(path) {
55120
+ const extendsAs = this.script(path)?.extendsAs ?? null;
55121
+ if (extendsAs === null) {
55122
+ return null;
55123
+ }
55124
+ const quote = extendsAs[0];
55125
+ if (quote === '"' || quote === "'") {
55126
+ const target = extendsAs.slice(1, -1);
55127
+ return {
55128
+ path: target.startsWith("res://") ? target : `res://${posix3.join(posix3.dirname(path.slice(6)), target)}`
55129
+ };
55130
+ }
55131
+ const name = extendsAs.split(".")[0] ?? extendsAs;
55132
+ const declaring = this.classes.get(name);
55133
+ return declaring === undefined ? { name } : { path: declaring };
55134
+ }
55135
+ suite(path) {
55136
+ const cases = new Set;
55137
+ let windowed = false;
55138
+ const seen = new Set;
55139
+ let at = path;
55140
+ while (!seen.has(at)) {
55141
+ seen.add(at);
55142
+ const read = this.script(at);
55143
+ if (read === undefined) {
55144
+ return null;
55145
+ }
55146
+ for (const func of read.funcs.filter((name) => name.startsWith(CASE_PREFIX))) {
55147
+ cases.add(func);
55148
+ }
55149
+ windowed ||= NEEDS_WINDOW.test(read.code) || SCENE_RUNNER.test(read.code);
55150
+ const base = this.base(at);
55151
+ if (base === null) {
55152
+ return null;
55153
+ }
55154
+ if ("name" in base ? base.name === SUITE_BASE : base.path === this.classes.get(SUITE_BASE)) {
55155
+ return cases.size > 0 ? { path, cases: [...cases].sort(), windowed } : null;
55156
+ }
55157
+ if ("name" in base) {
55158
+ return null;
55159
+ }
55160
+ at = base.path;
55161
+ }
55162
+ return null;
55163
+ }
55164
+ suitesUnder(roots) {
55165
+ const prefixes = roots.map((root) => root.endsWith("/") ? root : `${root}/`);
55166
+ return [...this.sources.keys()].filter((path) => prefixes.some((prefix) => path.startsWith(prefix))).sort().flatMap((path) => this.suite(path) ?? []);
55167
+ }
55168
+ }
55169
+ function readTimes(file) {
55170
+ try {
55171
+ const parsed = JSON.parse(readFileSync15(file, "utf8"));
55172
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
55173
+ } catch {
55174
+ return {};
55175
+ }
55176
+ }
55177
+ function writeJson(file, value) {
55178
+ mkdirSync7(dirname10(file), { recursive: true });
55179
+ writeWhole(file, `${JSON.stringify(value, null, 1)}
55180
+ `);
55181
+ }
55182
+ function timesAfter(times, suites, runs) {
55183
+ const next = { ...times };
55184
+ const declared = new Map(suites.map((suite) => [suite.path, suite]));
55185
+ const measured = new Map;
55186
+ for (const run of runs) {
55187
+ for (const printed of run.suites.values()) {
55188
+ if (printed.closed === null || !declared.has(printed.path)) {
55189
+ continue;
55190
+ }
55191
+ const entry = measured.get(printed.path) ?? { cases: new Map, around: 0 };
55192
+ const own = new Map;
55193
+ for (const ran of printed.cases) {
55194
+ own.set(ran.func, (own.get(ran.func) ?? 0) + ran.seconds);
55195
+ }
55196
+ const inCases = [...own.values()].reduce((sum, seconds) => sum + seconds, 0);
55197
+ entry.around = Math.max(entry.around, printed.closed.seconds - inCases);
55198
+ for (const [func, seconds] of own) {
55199
+ entry.cases.set(func, seconds);
55200
+ }
55201
+ measured.set(printed.path, entry);
55202
+ }
55203
+ }
55204
+ for (const [path, entry] of measured) {
55205
+ const suite = declared.get(path);
55206
+ const cases = {};
55207
+ for (const func of suite?.cases ?? []) {
55208
+ const seconds = entry.cases.get(func) ?? times[path]?.cases[func];
55209
+ if (seconds !== undefined) {
55210
+ cases[func] = round(seconds);
55211
+ }
55212
+ }
55213
+ const inCases = Object.values(cases).reduce((sum, seconds) => sum + seconds, 0);
55214
+ next[path] = { seconds: round(entry.around + inCases), cases };
55215
+ }
55216
+ return next;
55217
+ }
55218
+ function round(seconds) {
55219
+ return Math.round(seconds * 1000) / 1000;
55220
+ }
55221
+ var UNTIMED_CASE = 0.02;
55222
+ var SPLIT_PAST_SECONDS = 10;
55223
+ var ENGINE_START_SECONDS = 5;
55224
+ function median(values) {
55225
+ if (values.length === 0) {
55226
+ return;
55227
+ }
55228
+ const sorted = [...values].sort((a, b) => a - b);
55229
+ const middle = Math.floor(sorted.length / 2);
55230
+ return sorted.length % 2 === 1 ? sorted[middle] : ((sorted[middle - 1] ?? 0) + (sorted[middle] ?? 0)) / 2;
55231
+ }
55232
+ function unitsOf(suites, times, splitPast = SPLIT_PAST_SECONDS) {
55233
+ const known = Object.values(times);
55234
+ const caseCost = median(known.flatMap((time) => Object.values(time.cases))) ?? UNTIMED_CASE;
55235
+ const aroundCost = median(known.map((time) => time.seconds - Object.values(time.cases).reduce((sum, s) => sum + s, 0))) ?? 0;
55236
+ return suites.flatMap((suite) => {
55237
+ const time = times[suite.path];
55238
+ const costOf = (func) => time?.cases[func] ?? caseCost;
55239
+ const around = time === undefined ? aroundCost : Math.max(0, time.seconds - Object.values(time.cases).reduce((sum, s) => sum + s, 0));
55240
+ const whole = around + suite.cases.reduce((sum, func) => sum + costOf(func), 0);
55241
+ if (suite.windowed || whole <= splitPast || suite.cases.length < 2) {
55242
+ return [{ suite: suite.path, cases: suite.cases, cost: whole, windowed: suite.windowed }];
55243
+ }
55244
+ return suite.cases.map((func) => ({
55245
+ suite: suite.path,
55246
+ cases: [func],
55247
+ cost: around + costOf(func),
55248
+ windowed: false
55249
+ }));
55250
+ });
55251
+ }
55252
+ function dealt(units, declared, most) {
55253
+ const windowed = units.filter((unit) => unit.windowed);
55254
+ const headless = units.filter((unit) => !unit.windowed).sort((a, b) => b.cost - a.cost || a.suite.localeCompare(b.suite) || (a.cases[0] ?? "").localeCompare(b.cases[0] ?? ""));
55255
+ const total = headless.reduce((sum, unit) => sum + unit.cost, 0);
55256
+ const slowest = headless[0]?.cost ?? 0;
55257
+ const room = Math.max(1, most - (windowed.length > 0 && most > 1 ? 1 : 0));
55258
+ const count = headless.length === 0 ? 0 : Math.max(1, Math.min(room, headless.length, Math.ceil(total / Math.max(slowest, ENGINE_START_SECONDS))));
55259
+ const bins = Array.from({ length: count }, () => []);
55260
+ const loads = Array.from({ length: count }, () => 0);
55261
+ for (const unit of headless) {
55262
+ const lightest = loads.indexOf(Math.min(...loads));
55263
+ bins[lightest]?.push(unit);
55264
+ loads[lightest] = (loads[lightest] ?? 0) + unit.cost;
55265
+ }
55266
+ const plans = bins.map((bin) => planOf(bin, declared, false));
55267
+ return windowed.length > 0 ? [planOf(windowed, declared, true), ...plans] : plans;
55268
+ }
55269
+ function inOneEngine(units, declared) {
55270
+ return planOf(units, declared, units.some((unit) => unit.windowed));
55271
+ }
55272
+ function planOf(bin, declared, windowed) {
55273
+ const held = new Map;
55274
+ for (const unit of bin) {
55275
+ held.set(unit.suite, [...held.get(unit.suite) ?? [], ...unit.cases]);
55276
+ }
55277
+ const skipping = [...held].flatMap(([path, cases]) => (declared.get(path) ?? []).filter((func) => !cases.includes(func)).map((func) => `${path}:${func}`));
55278
+ return {
55279
+ suites: [...held].map(([path, cases]) => ({ path, cases: [...cases].sort() })),
55280
+ skipping,
55281
+ cost: bin.reduce((sum, unit) => sum + unit.cost, 0),
55282
+ windowed
55283
+ };
55284
+ }
55285
+ function viewportOf(projectPath) {
55286
+ const file = `${projectPath}/project.godot`;
55287
+ if (!existsSync17(file)) {
55288
+ return null;
55289
+ }
55290
+ const text = readFileSync15(file, "utf8");
55291
+ const width = /^window\/size\/viewport_width\s*=\s*(\d+)/m.exec(text)?.[1];
55292
+ const height = /^window\/size\/viewport_height\s*=\s*(\d+)/m.exec(text)?.[1];
55293
+ return width === undefined || height === undefined ? null : { width: Number(width), height: Number(height) };
55294
+ }
55295
+
54290
55296
  // src/version.ts
54291
- import { readFileSync as readFileSync12 } from "node:fs";
54292
- import { dirname as dirname10, join as join17 } from "node:path";
55297
+ import { readFileSync as readFileSync16 } from "node:fs";
55298
+ import { dirname as dirname11, join as join20 } from "node:path";
54293
55299
  import { fileURLToPath as fileURLToPath2 } from "node:url";
54294
55300
  function getLocalVersion() {
54295
- const currentDirectory = dirname10(fileURLToPath2(import.meta.url));
55301
+ const currentDirectory = dirname11(fileURLToPath2(import.meta.url));
54296
55302
  const packageCandidates = [
54297
- join17(currentDirectory, "..", "package.json"),
54298
- join17(currentDirectory, "..", "..", "package.json")
55303
+ join20(currentDirectory, "..", "package.json"),
55304
+ join20(currentDirectory, "..", "..", "package.json")
54299
55305
  ];
54300
55306
  for (const packagePath of packageCandidates) {
54301
55307
  try {
54302
- const value = JSON.parse(readFileSync12(packagePath, "utf-8"));
55308
+ const value = JSON.parse(readFileSync16(packagePath, "utf-8"));
54303
55309
  if (typeof value === "object" && value !== null && "name" in value && value.name === "gdharness" && "version" in value && typeof value.version === "string") {
54304
55310
  return value.version;
54305
55311
  }
@@ -54308,6 +55314,513 @@ function getLocalVersion() {
54308
55314
  return "0.0.0";
54309
55315
  }
54310
55316
 
55317
+ // src/suite-run.ts
55318
+ class TestUsageError extends Refusal {
55319
+ }
55320
+ var MOST_ENGINES = 4;
55321
+ var MEMORY_KEPT = 4 * 1024 ** 3;
55322
+ var MEMORY_PER_ENGINE = 1.5 * 1024 ** 3;
55323
+ var SILENCE_FLOOR_SECONDS = 300;
55324
+ var SILENCE_TIMES_SLOWEST = 4;
55325
+ var LOOK_MS = 500;
55326
+ var AUTOLOAD_FAILED = "Failed to instantiate an autoload";
55327
+ var VALUED = new Set([
55328
+ "--engines",
55329
+ "--slots",
55330
+ "--slot-count",
55331
+ "--silence",
55332
+ "--tests",
55333
+ "--reach-also",
55334
+ "--order"
55335
+ ]);
55336
+ var SWITCHES = new Set(["--changed-only", "--import"]);
55337
+ function testOptionsOf(args, cwd) {
55338
+ const valued = new Map;
55339
+ const switches = new Set;
55340
+ const positional = [];
55341
+ for (let at = 0;at < args.length; at += 1) {
55342
+ const arg = args[at] ?? "";
55343
+ if (!arg.startsWith("--")) {
55344
+ positional.push(arg);
55345
+ } else if (SWITCHES.has(arg)) {
55346
+ switches.add(arg);
55347
+ } else if (VALUED.has(arg)) {
55348
+ const value = args[at + 1];
55349
+ if (value === undefined || value.startsWith("--")) {
55350
+ throw new TestUsageError(`${arg} needs a value.`);
55351
+ }
55352
+ valued.set(arg, [...valued.get(arg) ?? [], value]);
55353
+ at += 1;
55354
+ } else {
55355
+ throw new TestUsageError(`Unknown option ${arg} for gdharness test. Run gdharness help.`);
55356
+ }
55357
+ }
55358
+ const first = positional[0];
55359
+ const named = first === undefined ? null : resolve4(cwd, first);
55360
+ const projectNamed = named !== null && existsSync18(join21(named, "project.godot"));
55361
+ const projectPath = projectNamed ? named : resolve4(cwd);
55362
+ if (!existsSync18(join21(projectPath, "project.godot"))) {
55363
+ throw new TestUsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
55364
+ }
55365
+ const last = (flag) => valued.get(flag)?.at(-1);
55366
+ const order = last("--order");
55367
+ return {
55368
+ projectPath,
55369
+ selected: positional.slice(projectNamed ? 1 : 0).map((entry) => resPathOf(entry, projectPath, cwd)),
55370
+ engines: whole(last("--engines"), "--engines") ?? MOST_ENGINES,
55371
+ slotsDirectory: last("--slots") === undefined ? null : resolve4(cwd, last("--slots") ?? ""),
55372
+ slotCount: whole(last("--slot-count"), "--slot-count"),
55373
+ silenceSeconds: whole(last("--silence"), "--silence") ?? SILENCE_FLOOR_SECONDS,
55374
+ roots: (valued.get("--tests") ?? []).map((root) => resPathOf(root, projectPath, cwd).replace(/\/$/, "")),
55375
+ changedOnly: switches.has("--changed-only"),
55376
+ reachAlso: (valued.get("--reach-also") ?? []).map((path) => resolve4(projectPath, path)),
55377
+ importFirst: switches.has("--import"),
55378
+ order: order === undefined ? null : order === "reversed" ? "reversed" : seedOf(order)
55379
+ };
55380
+ }
55381
+ function whole(value, flag) {
55382
+ if (value === undefined) {
55383
+ return null;
55384
+ }
55385
+ const number = Number(value);
55386
+ if (!Number.isInteger(number) || number < 1) {
55387
+ throw new TestUsageError(`${flag} takes a whole number above 0, not ${value}.`);
55388
+ }
55389
+ return number;
55390
+ }
55391
+ function seedOf(value) {
55392
+ const seed = Number(value);
55393
+ if (!Number.isInteger(seed) || seed < 0) {
55394
+ throw new TestUsageError(`--order takes reversed or a whole number to shuffle by, not ${value}.`);
55395
+ }
55396
+ return seed;
55397
+ }
55398
+ function resPathOf(entry, projectPath, cwd) {
55399
+ if (entry.startsWith("res://")) {
55400
+ return entry;
55401
+ }
55402
+ const caseAt = /^(.*\.gd):(\w+)$/.exec(entry);
55403
+ const file = caseAt?.[1] ?? entry;
55404
+ const fromProject = resolve4(projectPath, file);
55405
+ const absolute = isAbsolute4(file) || !existsSync18(fromProject) ? resolve4(cwd, file) : fromProject;
55406
+ const inside = relative3(projectPath, absolute);
55407
+ if (inside.startsWith("..") || isAbsolute4(inside)) {
55408
+ throw new TestUsageError(`${entry} is not inside the project at ${projectPath}.`);
55409
+ }
55410
+ const path = `res://${inside.split(sep2).join("/")}`;
55411
+ return caseAt === null ? path : `${path}:${caseAt[2] ?? ""}`;
55412
+ }
55413
+ function homeOf(projectPath) {
55414
+ return mainCheckoutOf(projectPath) ?? projectPath;
55415
+ }
55416
+ function selectedSuites(scripts, options) {
55417
+ const roots = options.roots.length > 0 ? options.roots : ["res://tests", "res://test"].filter((root) => existsSync18(join21(options.projectPath, root.slice(6))));
55418
+ if (options.selected.length === 0) {
55419
+ const found = scripts.suitesUnder(roots);
55420
+ if (found.length === 0) {
55421
+ throw new TestUsageError(roots.length === 0 ? "The project has no tests or test directory to look for suites in. Name one with --tests." : `No gdUnit4 suite under ${roots.join(" or ")}: no script there extends GdUnitTestSuite and declares a test_ function.`);
55422
+ }
55423
+ return found;
55424
+ }
55425
+ const chosen = new Map;
55426
+ for (const entry of options.selected) {
55427
+ const caseAt = /^(res:\/\/.*\.gd):(\w+)$/.exec(entry);
55428
+ const path = caseAt?.[1] ?? entry;
55429
+ const directory = join21(options.projectPath, path.slice(6));
55430
+ const suites = existsSync18(directory) && statSync11(directory).isDirectory() ? scripts.suitesUnder([path.replace(/\/$/, "")]) : [scripts.suite(path)].filter((suite) => suite !== null);
55431
+ if (suites.length === 0) {
55432
+ throw new TestUsageError(existsSync18(directory) ? `${path} holds no gdUnit4 suite: nothing there extends GdUnitTestSuite and declares a test_ function.` : `Nothing at ${path}.`);
55433
+ }
55434
+ for (const suite of suites) {
55435
+ const picked = chosen.get(suite.path) ?? { suite, cases: new Set };
55436
+ const asked = caseAt?.[2];
55437
+ if (asked !== undefined && !suite.cases.includes(asked)) {
55438
+ throw new TestUsageError(`${suite.path} declares no case ${asked}. It declares ${suite.cases.join(", ")}.`);
55439
+ }
55440
+ for (const func of asked === undefined ? suite.cases : [asked]) {
55441
+ picked.cases.add(func);
55442
+ }
55443
+ chosen.set(suite.path, picked);
55444
+ }
55445
+ }
55446
+ return [...chosen.values()].map(({ suite, cases }) => ({
55447
+ ...suite,
55448
+ cases: suite.cases.filter((c) => cases.has(c))
55449
+ }));
55450
+ }
55451
+ function roomFor(most) {
55452
+ const byMemory = Math.floor((freemem() - MEMORY_KEPT) / MEMORY_PER_ENGINE);
55453
+ return Math.max(1, Math.min(most, availableParallelism(), byMemory));
55454
+ }
55455
+ function ordered(units, order) {
55456
+ const sorted = [...units].sort((a, b) => a.suite.localeCompare(b.suite));
55457
+ if (order === "reversed") {
55458
+ return sorted.reverse();
55459
+ }
55460
+ let state = order >>> 0;
55461
+ const next = () => {
55462
+ state = state + 1831565813 >>> 0;
55463
+ let mixed = Math.imul(state ^ state >>> 15, 1 | state);
55464
+ mixed = mixed + Math.imul(mixed ^ mixed >>> 7, 61 | mixed) ^ mixed;
55465
+ return ((mixed ^ mixed >>> 14) >>> 0) / 4294967296;
55466
+ };
55467
+ for (let at = sorted.length - 1;at > 0; at -= 1) {
55468
+ const other = Math.floor(next() * (at + 1));
55469
+ const here = sorted[at];
55470
+ const there = sorted[other];
55471
+ if (here !== undefined && there !== undefined) {
55472
+ sorted[at] = there;
55473
+ sorted[other] = here;
55474
+ }
55475
+ }
55476
+ return sorted;
55477
+ }
55478
+ async function runTestCommand(options, godotPath) {
55479
+ const unusable = gdUnit4Unusable(options.projectPath);
55480
+ if (unusable !== null) {
55481
+ throw new TestUsageError(`${unusable.reason}. Install gdUnit4 6.0 or later under addons/gdUnit4.`);
55482
+ }
55483
+ const startedAt = Date.now();
55484
+ const scripts = new ProjectScripts(options.projectPath);
55485
+ const home = homeOf(options.projectPath);
55486
+ const state = join21(home, ".godot", "gdharness");
55487
+ const timesFile = join21(state, "suite-times.json");
55488
+ const passedFile = join21(state, "suite-passed.json");
55489
+ let suites = selectedSuites(scripts, options);
55490
+ const wholeSuites = suites.map((suite) => scripts.suite(suite.path) ?? suite);
55491
+ const declared = new Map(wholeSuites.map((suite) => [suite.path, suite.cases]));
55492
+ const testRuns = join21(options.projectPath, ".godot", "gdharness", "test-runs");
55493
+ sweepRunDirectories(testRuns);
55494
+ const runDirectory = join21(testRuns, `${String(startedAt)}-${String(process12.pid)}`);
55495
+ mkdirSync8(runDirectory, { recursive: true });
55496
+ const slots = new EngineSlots(options.slotsDirectory ?? join21(home, ".godot", "gate", "slots"), options.slotCount ?? Math.min(MOST_ENGINES, availableParallelism()), { pid: process12.pid, started: await ownStart() });
55497
+ const running = new Set;
55498
+ const interrupted = () => {
55499
+ for (const end of running) {
55500
+ end();
55501
+ }
55502
+ slots.releaseAll();
55503
+ process12.exit(130);
55504
+ };
55505
+ process12.once("SIGINT", interrupted);
55506
+ process12.once("SIGTERM", interrupted);
55507
+ const launch = {
55508
+ godotPath,
55509
+ projectPath: options.projectPath,
55510
+ runDirectory,
55511
+ slots,
55512
+ silenceFloorSeconds: options.silenceSeconds,
55513
+ running
55514
+ };
55515
+ try {
55516
+ if (options.importFirst) {
55517
+ const imported = await importProject(launch);
55518
+ if (imported !== null) {
55519
+ console.log(`The import failed, so no suite ran: ${imported}`);
55520
+ return 1;
55521
+ }
55522
+ }
55523
+ const fingerprints = new SuiteFingerprints(scripts, [
55524
+ getLocalVersion(),
55525
+ readFileSync17(shippedListedRunner(), "utf8"),
55526
+ ...options.reachAlso.map((path) => `${path}
55527
+ ${existsSync18(path) ? readFileSync17(path, "latin1") : "missing"}`)
55528
+ ]);
55529
+ const printsOf = new Map(suites.map((suite) => [suite.path, fingerprints.of(suite)]));
55530
+ if (options.changedOnly) {
55531
+ const passedBefore = readRecord(passedFile);
55532
+ const unchanged = suites.filter((suite) => {
55533
+ const print = printsOf.get(suite.path);
55534
+ return print !== null && print !== undefined && passedBefore[suite.path] === print;
55535
+ });
55536
+ suites = suites.filter((suite) => !unchanged.includes(suite));
55537
+ if (unchanged.length > 0) {
55538
+ console.log(`${plural(unchanged.length, "suite")} passed with everything ${unchanged.length === 1 ? "it reaches" : "they reach"} as it stands now, and ${unchanged.length === 1 ? "is" : "are"} not run.`);
55539
+ }
55540
+ if (suites.length === 0) {
55541
+ console.log("Nothing has changed since every suite last passed.");
55542
+ rmSync10(runDirectory, { recursive: true, force: true });
55543
+ return 0;
55544
+ }
55545
+ }
55546
+ const times = readTimes(timesFile);
55547
+ const units = unitsOf(suites, times);
55548
+ const plans = options.order === null ? dealt(units, declared, roomFor(options.engines)) : [inOneEngine(ordered(units, options.order), declared)];
55549
+ const functions = suites.reduce((sum, suite) => sum + suite.cases.length, 0);
55550
+ console.log(`${plural(functions, "test function")} in ${plural(suites.length, "suite")} over ${plural(plans.length, "engine")}${plans.some((plan) => plan.windowed) ? ", one of them windowed" : ""}.`);
55551
+ let ran = await Promise.all(plans.map((plan, index) => runEngine2(launch, plan, index, units)));
55552
+ if (ran.some((engine) => engine.verdict.starved) && ran.some((engine) => !engine.verdict.passed)) {
55553
+ console.log("An engine ran out of memory, so every engine that failed runs again, one at a time.");
55554
+ const again = [];
55555
+ for (const engine of ran) {
55556
+ again.push(engine.verdict.passed ? engine : await runEngine2(launch, engine.plan, engine.index, units, "again"));
55557
+ }
55558
+ ran = again;
55559
+ }
55560
+ writeJson(timesFile, timesAfter(readTimes(timesFile), wholeSuites, ran.map((engine) => engine.printed)));
55561
+ recordPassed(passedFile, ran, printsOf);
55562
+ return report(ran, startedAt, runDirectory);
55563
+ } finally {
55564
+ process12.off("SIGINT", interrupted);
55565
+ process12.off("SIGTERM", interrupted);
55566
+ }
55567
+ }
55568
+ function plural(count, noun) {
55569
+ return `${String(count)} ${noun}${count === 1 ? "" : "s"}`;
55570
+ }
55571
+ function readRecord(file) {
55572
+ try {
55573
+ const parsed = JSON.parse(readFileSync17(file, "utf8"));
55574
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
55575
+ } catch {
55576
+ return {};
55577
+ }
55578
+ }
55579
+ function recordPassed(file, ran, printsOf) {
55580
+ const passed = readRecord(file);
55581
+ const failedSomewhere = new Set;
55582
+ const passedSomewhere = new Set;
55583
+ for (const engine of ran) {
55584
+ const passing = new Set(engine.verdict.passedSuites);
55585
+ for (const { path } of engine.plan.suites) {
55586
+ (passing.has(path) ? passedSomewhere : failedSomewhere).add(path);
55587
+ }
55588
+ }
55589
+ for (const path of new Set([...passedSomewhere, ...failedSomewhere])) {
55590
+ const print = printsOf.get(path);
55591
+ if (!failedSomewhere.has(path) && print !== null && print !== undefined) {
55592
+ passed[path] = print;
55593
+ } else {
55594
+ delete passed[path];
55595
+ }
55596
+ }
55597
+ writeJson(file, passed);
55598
+ }
55599
+ function sweepRunDirectories(testRuns) {
55600
+ if (!existsSync18(testRuns)) {
55601
+ return;
55602
+ }
55603
+ for (const entry of readdirSync11(testRuns)) {
55604
+ const pid = Number(/^\d+-(\d+)$/.exec(entry)?.[1]);
55605
+ if (!Number.isInteger(pid) || !alive(pid)) {
55606
+ rmSync10(join21(testRuns, entry), { recursive: true, force: true });
55607
+ }
55608
+ }
55609
+ }
55610
+ async function importProject(launch) {
55611
+ const directory = join21(launch.runDirectory, "import");
55612
+ mkdirSync8(join21(directory, "user"), { recursive: true });
55613
+ const log = join21(directory, "import.log");
55614
+ const lock = await launch.slots.take();
55615
+ try {
55616
+ const ended = await watched(launch, ["--headless", "--audio-driver", "Dummy", "--path", launch.projectPath, "--import"], directory, log, false, launch.silenceFloorSeconds);
55617
+ if (ended.ended !== null || ended.code !== 0) {
55618
+ return `${ended.ended ?? `the engine exited ${String(ended.code)}`}; everything it printed is in ${log}`;
55619
+ }
55620
+ return null;
55621
+ } finally {
55622
+ launch.slots.release(lock);
55623
+ }
55624
+ }
55625
+ async function runEngine2(launch, plan, index, units, again = "") {
55626
+ const directory = join21(launch.runDirectory, `engine-${String(index)}${again === "" ? "" : `-${again}`}`);
55627
+ mkdirSync8(join21(directory, "user"), { recursive: true });
55628
+ const suitesFile = join21(directory, "suites.txt");
55629
+ const skippingFile = join21(directory, "skipping.txt");
55630
+ const expectedFile = join21(directory, "expected.json");
55631
+ writeFileSync10(suitesFile, `${plan.suites.map((suite) => suite.path).join(`
55632
+ `)}
55633
+ `);
55634
+ writeFileSync10(skippingFile, `${plan.skipping.join(`
55635
+ `)}
55636
+ `);
55637
+ const viewport = viewportOf(launch.projectPath);
55638
+ const display = plan.windowed ? viewport === null ? [] : ["--resolution", `${String(viewport.width)}x${String(viewport.height)}`] : ["--headless"];
55639
+ const args = [
55640
+ ...display,
55641
+ "--audio-driver",
55642
+ "Dummy",
55643
+ "--path",
55644
+ launch.projectPath,
55645
+ "-s",
55646
+ shippedListedRunner(),
55647
+ ...plan.windowed ? [] : ["--ignoreHeadlessMode"],
55648
+ "-c",
55649
+ "-rd",
55650
+ `res://${relative3(launch.projectPath, join21(directory, "reports")).split(sep2).join("/")}`,
55651
+ "-rc",
55652
+ "1",
55653
+ "--",
55654
+ "--suites-in",
55655
+ suitesFile,
55656
+ "--skipping-in",
55657
+ skippingFile,
55658
+ "--html-report-off",
55659
+ "--junit-report-off",
55660
+ "--compile-together",
55661
+ "--expected-errors-out",
55662
+ expectedFile
55663
+ ];
55664
+ const held = new Set(plan.suites.map((suite) => suite.path));
55665
+ const slowest = Math.max(0, ...units.filter((unit) => held.has(unit.suite)).map((unit) => unit.cost));
55666
+ const silence = Math.max(launch.silenceFloorSeconds, SILENCE_TIMES_SLOWEST * slowest);
55667
+ const log = join21(directory, "engine.log");
55668
+ const lock = await launch.slots.take();
55669
+ const began = Date.now();
55670
+ let end;
55671
+ try {
55672
+ end = await watched(launch, args, directory, log, plan.windowed, silence);
55673
+ } finally {
55674
+ launch.slots.release(lock);
55675
+ }
55676
+ const printed = readRun(readFileSync17(log, "utf8"));
55677
+ const assignment = { suites: plan.suites, expected: readExpected(expectedFile) };
55678
+ return {
55679
+ index,
55680
+ plan,
55681
+ printed,
55682
+ verdict: judgeEngine(printed, assignment, end),
55683
+ log,
55684
+ seconds: (Date.now() - began) / 1000
55685
+ };
55686
+ }
55687
+ function readExpected(file) {
55688
+ try {
55689
+ const parsed = JSON.parse(readFileSync17(file, "utf8"));
55690
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
55691
+ } catch {
55692
+ return {};
55693
+ }
55694
+ }
55695
+ async function watched(launch, args, directory, log, windowed, silenceSeconds) {
55696
+ const user = join21(directory, "user");
55697
+ const env = { ...process12.env, APPDATA: user, XDG_DATA_HOME: user };
55698
+ const output = openSync3(log, "w");
55699
+ let child;
55700
+ let stop;
55701
+ try {
55702
+ if (process12.platform === "win32" && (windowed || !crashesReported())) {
55703
+ const compiled = await ensureHelperCompiled();
55704
+ if (compiled !== null) {
55705
+ const how = windowed ? "on a desktop of its own" : "through the desktop helper";
55706
+ return { code: null, signal: null, ended: `the engine could not be started ${how}: ${compiled}` };
55707
+ }
55708
+ const stopFile = join21(directory, "stop");
55709
+ const how = windowed ? { desktop: HIDDEN_DESKTOP, directory: launch.projectPath } : { directory: launch.projectPath };
55710
+ const wrapper = throughHelper(how, launch.godotPath, args, {
55711
+ spec: join21(directory, "spec.json"),
55712
+ pid: join21(directory, "pid"),
55713
+ error: join21(directory, "error"),
55714
+ stop: stopFile
55715
+ });
55716
+ child = spawnHidden(wrapper.command, wrapper.args, {
55717
+ stdio: ["ignore", output, output],
55718
+ env: windowed ? { ...env, [DESKTOP_VARIABLE]: HIDDEN_DESKTOP } : env,
55719
+ cwd: directory
55720
+ });
55721
+ stop = () => {
55722
+ writeFileSync10(stopFile, "");
55723
+ };
55724
+ } else {
55725
+ child = spawnHidden(launch.godotPath, args, {
55726
+ stdio: ["ignore", output, output],
55727
+ env,
55728
+ cwd: launch.projectPath,
55729
+ detached: process12.platform !== "win32"
55730
+ });
55731
+ stop = () => {
55732
+ endTree(child);
55733
+ };
55734
+ }
55735
+ } finally {
55736
+ closeSync3(output);
55737
+ }
55738
+ launch.running.add(stop);
55739
+ let ended = null;
55740
+ const exited = Promise.race([
55741
+ exitOf(child),
55742
+ new Promise((settle) => {
55743
+ child.once("error", (error) => {
55744
+ ended ??= `the engine did not start: ${error.message}`;
55745
+ settle({ code: null, signal: null });
55746
+ });
55747
+ })
55748
+ ]);
55749
+ const finished = { now: false };
55750
+ exited.then(() => {
55751
+ finished.now = true;
55752
+ });
55753
+ const reading = openSync3(log, "r");
55754
+ try {
55755
+ let size = 0;
55756
+ let lastGrowth = Date.now();
55757
+ let text = "";
55758
+ while (!finished.now) {
55759
+ await delay3(LOOK_MS);
55760
+ const now = fstatSync(reading).size;
55761
+ if (now > size) {
55762
+ const chunk = Buffer.alloc(now - size);
55763
+ readSync(reading, chunk, 0, chunk.length, size);
55764
+ text += chunk.toString("utf8");
55765
+ size = now;
55766
+ lastGrowth = Date.now();
55767
+ if (ended === null && text.includes(AUTOLOAD_FAILED)) {
55768
+ ended = "an autoload did not start, so the suites were ended rather than run without it";
55769
+ stop();
55770
+ }
55771
+ } else if (ended === null && Date.now() - lastGrowth > silenceSeconds * 1000) {
55772
+ const stoppedIn = readRun(text).stoppedIn;
55773
+ ended = `the engine printed nothing for ${String(silenceSeconds)} s${stoppedIn === null ? "" : ` while running ${stoppedIn}`}, so it was ended`;
55774
+ stop();
55775
+ }
55776
+ }
55777
+ } finally {
55778
+ closeSync3(reading);
55779
+ launch.running.delete(stop);
55780
+ }
55781
+ const { code, signal } = await exited;
55782
+ return { code, signal, ended };
55783
+ }
55784
+ function endTree(child) {
55785
+ if (child.pid === undefined) {
55786
+ return;
55787
+ }
55788
+ if (process12.platform === "win32") {
55789
+ runHidden("taskkill", ["/T", "/F", "/PID", String(child.pid)]).catch(() => {});
55790
+ return;
55791
+ }
55792
+ try {
55793
+ process12.kill(-child.pid, "SIGKILL");
55794
+ } catch {}
55795
+ }
55796
+ function report(ran, startedAt, runDirectory) {
55797
+ const failed = ran.filter((engine) => !engine.verdict.passed);
55798
+ for (const engine of failed) {
55799
+ console.log("");
55800
+ console.log(`--- engine ${String(engine.index)}: ${plural(engine.plan.suites.length, "suite")}, ${engine.seconds.toFixed(1)} s`);
55801
+ for (const fault of engine.verdict.faults) {
55802
+ console.log(` ${fault}`);
55803
+ }
55804
+ if (engine.verdict.shown.length > 0) {
55805
+ console.log("");
55806
+ for (const line of engine.verdict.shown) {
55807
+ console.log(line);
55808
+ }
55809
+ }
55810
+ console.log(`everything it printed: ${engine.log}`);
55811
+ }
55812
+ const cases = ran.reduce((sum, engine) => sum + [...engine.printed.suites.values()].reduce((n, s) => n + s.cases.length, 0), 0);
55813
+ const suites = new Set(ran.flatMap((engine) => engine.plan.suites.map((suite) => suite.path))).size;
55814
+ const longest = Math.max(0, ...ran.map((engine) => engine.seconds));
55815
+ const took = (Date.now() - startedAt) / 1000;
55816
+ if (failed.length === 0) {
55817
+ rmSync10(runDirectory, { recursive: true, force: true });
55818
+ }
55819
+ console.log("");
55820
+ console.log(`${failed.length === 0 ? "Passed" : "Failed"}: ${plural(cases, "case")} in ${plural(suites, "suite")} across ${plural(ran.length, "engine")}, ${took.toFixed(1)} s; the longest engine took ${longest.toFixed(1)} s.`);
55821
+ return failed.length === 0 ? 0 : 1;
55822
+ }
55823
+
54311
55824
  // src/cli.ts
54312
55825
  var args = process.argv.slice(2);
54313
55826
  var command = args[0];
@@ -54316,8 +55829,8 @@ class UsageError extends Refusal {
54316
55829
  }
54317
55830
  function projectArgument(at) {
54318
55831
  const named = args.slice(at).find((value) => !value.startsWith("--"));
54319
- const projectPath = resolve8(named ?? ".");
54320
- if (!existsSync28(join36(projectPath, "project.godot"))) {
55832
+ const projectPath = resolve9(named ?? ".");
55833
+ if (!existsSync31(join39(projectPath, "project.godot"))) {
54321
55834
  throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
54322
55835
  }
54323
55836
  return projectPath;
@@ -54362,7 +55875,7 @@ async function registerRuntime(godot, projectPath) {
54362
55875
  return;
54363
55876
  }
54364
55877
  console.log(`${RUNTIME_AUTOLOAD.name} autoload left at ${named}, which this project registers itself`);
54365
- if (!existsSync28(join36(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
55878
+ if (!existsSync31(join39(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
54366
55879
  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}`);
54367
55880
  }
54368
55881
  }
@@ -54494,7 +56007,7 @@ async function setup() {
54494
56007
  console.log("no harness configured; name one yourself, and gdharness harnesses lists them all");
54495
56008
  }
54496
56009
  if (!args.includes("--no-skill")) {
54497
- const directories = skillDirectories(chosen.harnesses, projectPath, existsSync28);
56010
+ const directories = skillDirectories(chosen.harnesses, projectPath, existsSync31);
54498
56011
  for (const written of writeSkill(directories, getLocalVersion())) {
54499
56012
  console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
54500
56013
  }
@@ -54608,7 +56121,7 @@ async function upgrade() {
54608
56121
  moved.push(took.from);
54609
56122
  }
54610
56123
  }
54611
- for (const written of writeSkill(skillDirectories(already, projectPath, existsSync28), version)) {
56124
+ for (const written of writeSkill(skillDirectories(already, projectPath, existsSync31), version)) {
54612
56125
  console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
54613
56126
  }
54614
56127
  console.log(`
@@ -54730,6 +56243,17 @@ Usage:
54730
56243
  gdharness runtime on|off [project] Register or remove the runtime autoload, which reaches
54731
56244
  an export if it is left on
54732
56245
  gdharness classes [project] Rebuild .godot/global_script_class_cache.cfg from disk
56246
+ gdharness test [project] [suite ...] [--engines N] [--slots DIR] [--slot-count N]
56247
+ [--silence S] [--tests DIR] [--changed-only] [--reach-also PATH] [--import]
56248
+ [--order reversed|SEED] [--runner]
56249
+ Run the project's gdUnit4 suites over several engines and
56250
+ judge every line they print: exit 1 on a failing case, an
56251
+ engine error no case declared, a case that never finished
56252
+ or a leak at exit. A suite is a script, a directory, or
56253
+ script.gd:test_case for one case. --changed-only leaves out
56254
+ suites that passed with everything they reach unchanged; it
56255
+ is for iterating, and no gate passes it. --runner prints the
56256
+ shipped runner's path. docs/test-runs.md has the rest.
54733
56257
  gdharness version Show the installed version
54734
56258
  gdharness help Show this help
54735
56259
 
@@ -54745,7 +56269,22 @@ function checkFlags() {
54745
56269
  }
54746
56270
  }
54747
56271
  }
56272
+ async function test() {
56273
+ const rest = args.slice(1);
56274
+ if (rest.includes("--runner")) {
56275
+ console.log(shippedListedRunner());
56276
+ return;
56277
+ }
56278
+ const options = testOptionsOf(rest, process.cwd());
56279
+ const { godotPath } = await engine(options.projectPath);
56280
+ process.exitCode = await runTestCommand(options, godotPath);
56281
+ }
54748
56282
  async function main() {
56283
+ await letWindowsReportCrashes();
56284
+ if (command === "test") {
56285
+ await test();
56286
+ return;
56287
+ }
54749
56288
  checkFlags();
54750
56289
  switch (command) {
54751
56290
  case undefined: {
@@ -54791,7 +56330,7 @@ async function main() {
54791
56330
  await main().catch((error) => {
54792
56331
  if (error instanceof Refusal) {
54793
56332
  console.error("gdharness:", error.message);
54794
- process.exit(error instanceof UsageError ? 2 : 1);
56333
+ process.exit(error instanceof UsageError || error instanceof TestUsageError ? 2 : 1);
54795
56334
  }
54796
56335
  console.error(defectReport(`gdharness ${command ?? "server"}`, error));
54797
56336
  process.exit(1);