gdharness 1.2.28 → 1.2.30

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
@@ -68,8 +68,34 @@ var init_errors = __esm(() => {
68
68
  };
69
69
  });
70
70
 
71
+ // src/spawn.ts
72
+ import {
73
+ execFile,
74
+ execFileSync,
75
+ spawn,
76
+ spawnSync
77
+ } from "node:child_process";
78
+ function spawnHidden(command, args, options = {}) {
79
+ return spawn(command, [...args], { ...options, windowsHide: true });
80
+ }
81
+ function runHidden(command, args, options = {}) {
82
+ return new Promise((resolve, reject) => {
83
+ execFile(command, [...args], { ...options, encoding: "utf8", windowsHide: true }, (error, stdout, stderr) => {
84
+ if (error !== null) {
85
+ Object.assign(error, { stdout, stderr });
86
+ reject(error);
87
+ return;
88
+ }
89
+ resolve({ stdout, stderr });
90
+ });
91
+ });
92
+ }
93
+ function spawnSyncHidden(command, args, options = {}) {
94
+ return spawnSync(command, [...args], { ...options, windowsHide: true });
95
+ }
96
+ var init_spawn = () => {};
97
+
71
98
  // src/windows-ask.ts
72
- import { spawn } from "node:child_process";
73
99
  import { tmpdir } from "node:os";
74
100
  import process2 from "node:process";
75
101
  function holding(child, wanted) {
@@ -88,14 +114,14 @@ function running() {
88
114
  if (helper?.exitCode === null && helper.signalCode === null) {
89
115
  return helper;
90
116
  }
91
- const child = spawn("powershell.exe", [
117
+ const child = spawnHidden("powershell.exe", [
92
118
  "-NoProfile",
93
119
  "-NonInteractive",
94
120
  "-ExecutionPolicy",
95
121
  "Bypass",
96
122
  "-EncodedCommand",
97
123
  Buffer.from(SCRIPT, "utf16le").toString("base64")
98
- ], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true, cwd: tmpdir() });
124
+ ], { cwd: tmpdir() });
99
125
  buffered = "";
100
126
  child.stdout.setEncoding("utf8");
101
127
  child.stdout.on("data", (chunk) => {
@@ -188,6 +214,7 @@ function askWindows(script, withinMs) {
188
214
  }
189
215
  var SCRIPT, WindowsAskFailure, helper = null, buffered = "", next = 1, out = null, queued, held = false;
190
216
  var init_windows_ask = __esm(() => {
217
+ init_spawn();
191
218
  SCRIPT = [
192
219
  "[Console]::OutputEncoding = [Text.Encoding]::UTF8",
193
220
  "$in = [Console]::In",
@@ -215,9 +242,7 @@ var init_windows_ask = __esm(() => {
215
242
  });
216
243
 
217
244
  // src/process-children.ts
218
- import { execFile } from "node:child_process";
219
245
  import { basename } from "node:path";
220
- import { promisify } from "node:util";
221
246
  async function startTimesOf(pids) {
222
247
  const began = new Map;
223
248
  if (pids.length === 0) {
@@ -252,10 +277,9 @@ async function startTimesOf(pids) {
252
277
  return began;
253
278
  }
254
279
  function printedBy(command, args, env) {
255
- return new Promise((resolve) => {
256
- execFile(command, [...args], { encoding: "utf8", timeout: ASK_TIMEOUT_MS, env }, (_error, stdout) => {
257
- resolve(stdout);
258
- });
280
+ return runHidden(command, args, { timeout: ASK_TIMEOUT_MS, env }).then(({ stdout }) => stdout, (error) => {
281
+ const printed = error.stdout;
282
+ return typeof printed === "string" ? printed : "";
259
283
  });
260
284
  }
261
285
  async function processTree() {
@@ -321,10 +345,18 @@ function linked(tree, child, parent) {
321
345
  }
322
346
  return from.startedAt >= to.startedAt;
323
347
  }
348
+ function isConsoleHost(listed) {
349
+ const executable = commandWords(listed.command)[0];
350
+ if (executable === undefined) {
351
+ return false;
352
+ }
353
+ const host = `${process.env["SystemRoot"] ?? "C:\\Windows"}\\System32\\conhost.exe`;
354
+ return executable.replace(/^\\\?\?\\/, "").replaceAll("/", "\\").toLowerCase() === host.toLowerCase();
355
+ }
324
356
  function childrenIn(tree, pid) {
325
357
  const children = [];
326
- for (const child of tree.keys()) {
327
- if (linked(tree, child, pid)) {
358
+ for (const [child, listed] of tree) {
359
+ if (linked(tree, child, pid) && !isConsoleHost(listed)) {
328
360
  children.push(child);
329
361
  }
330
362
  }
@@ -426,16 +458,16 @@ function listWindows() {
426
458
  return askWindows('Get-CimInstance Win32_Process -Property ProcessId,ParentProcessId,CreationDate,CommandLine | ForEach-Object { $began = 0; if ($_.CreationDate) { $began = ([DateTimeOffset]$_.CreationDate).ToUnixTimeMilliseconds() }; "$($_.ProcessId) $($_.ParentProcessId) $began $($_.CommandLine)" }', ASK_TIMEOUT_MS);
427
459
  }
428
460
  async function askPosix() {
429
- const { stdout } = await run("ps", ["-e", "-o", "pid=,ppid=,args="], {
461
+ const { stdout } = await runHidden("ps", ["-e", "-o", "pid=,ppid=,args="], {
430
462
  timeout: ASK_TIMEOUT_MS,
431
463
  maxBuffer: 16 * 1024 * 1024
432
464
  });
433
465
  return stdout;
434
466
  }
435
- var run, ASK_TIMEOUT_MS = 15000, lastListingFailure, ONE_SHOT_OPTIONS;
467
+ var ASK_TIMEOUT_MS = 15000, lastListingFailure, ONE_SHOT_OPTIONS;
436
468
  var init_process_children = __esm(() => {
469
+ init_spawn();
437
470
  init_windows_ask();
438
- run = promisify(execFile);
439
471
  ONE_SHOT_OPTIONS = [
440
472
  "--import",
441
473
  "--quit",
@@ -674,7 +706,10 @@ function engineFlagsAmongGameArgs(args) {
674
706
  return args.filter((one) => Object.hasOwn(ENGINE_FLAGS, one) || Object.hasOwn(SET_BY_ARGUMENT, one));
675
707
  }
676
708
  function runArguments(options) {
677
- const args = options.headless ? ["--headless", "--path", options.projectPath] : ["--path", options.projectPath];
709
+ const args = [
710
+ ...options.headless ? ["--headless"] : [],
711
+ ...options.projectPath === null ? [] : ["--path", options.projectPath]
712
+ ];
678
713
  if (options.silent === true && !options.headless) {
679
714
  args.push("--audio-driver", "Dummy");
680
715
  }
@@ -778,10 +813,8 @@ var init_launch = __esm(() => {
778
813
  });
779
814
 
780
815
  // src/godot-path.ts
781
- import { execFile as execFile2 } from "node:child_process";
782
816
  import { existsSync as existsSync2 } from "node:fs";
783
817
  import { normalize } from "node:path";
784
- import { promisify as promisify2 } from "node:util";
785
818
 
786
819
  class GodotLocator {
787
820
  answered = new Map;
@@ -838,7 +871,7 @@ class GodotLocator {
838
871
  reason = "does not exist";
839
872
  } else {
840
873
  try {
841
- await run2(path, ["--version"]);
874
+ await runHidden(path, ["--version"]);
842
875
  reason = null;
843
876
  } catch (error) {
844
877
  reason = `does not answer --version: ${errorMessage(error)}`;
@@ -848,12 +881,11 @@ class GodotLocator {
848
881
  return reason;
849
882
  }
850
883
  }
851
- var run2;
852
884
  var init_godot_path = __esm(() => {
853
885
  init_detection();
854
886
  init_errors();
855
887
  init_launch();
856
- run2 = promisify2(execFile2);
888
+ init_spawn();
857
889
  });
858
890
 
859
891
  // node_modules/yaml/dist/nodes/identity.js
@@ -7799,7 +7831,7 @@ var require_public_api = __commonJS(function(exports) {
7799
7831
  });
7800
7832
 
7801
7833
  // src/paths.ts
7802
- import { realpathSync } from "node:fs";
7834
+ import { realpathSync, statSync as statSync2 } from "node:fs";
7803
7835
  import { isAbsolute, relative, resolve, sep } from "node:path";
7804
7836
  function realPathOr(path) {
7805
7837
  try {
@@ -7860,6 +7892,31 @@ function resolveWithinProject(projectPath, candidatePath) {
7860
7892
  }
7861
7893
  return { ok: true, absolutePath, relativePath };
7862
7894
  }
7895
+ function exportedBuild(projectPath, candidatePath) {
7896
+ const found = resolveWithinProject(projectPath, candidatePath);
7897
+ if (!found.ok) {
7898
+ return found;
7899
+ }
7900
+ let real;
7901
+ try {
7902
+ real = realpathSync.native(found.absolutePath);
7903
+ } catch {
7904
+ return {
7905
+ ok: false,
7906
+ reason: `There is no ${found.relativePath} in the project to start: export the build first, with project_export run.`
7907
+ };
7908
+ }
7909
+ if (!isWithinRoot(realPathOr(projectPath), real)) {
7910
+ return {
7911
+ ok: false,
7912
+ reason: `${found.relativePath} leads outside the project, to ${real}, so it is not started.`
7913
+ };
7914
+ }
7915
+ if (!statSync2(real).isFile()) {
7916
+ return { ok: false, reason: `${found.relativePath} is not a file, so there is nothing to start.` };
7917
+ }
7918
+ return { ok: true, absolutePath: real, relativePath: found.relativePath };
7919
+ }
7863
7920
  var RESOURCE_SCHEME = "res://", OTHER_SCHEME;
7864
7921
  var init_paths = __esm(() => {
7865
7922
  OTHER_SCHEME = /^[a-zA-Z][a-zA-Z\d+.-]+:\/\//;
@@ -8154,15 +8211,13 @@ var init_game_log = __esm(() => {
8154
8211
  });
8155
8212
 
8156
8213
  // src/engine-run.ts
8157
- import { spawn as spawn2 } from "node:child_process";
8158
8214
  function runEngine(executable, args, options = {}) {
8159
8215
  return new Promise((resolve) => {
8160
8216
  const log = new GameLog;
8161
8217
  let failure = null;
8162
8218
  const signal = callSignal();
8163
- const child = spawn2(executable, [...args], {
8219
+ const child = spawnHidden(executable, args, {
8164
8220
  stdio: ["ignore", "pipe", "pipe"],
8165
- windowsHide: true,
8166
8221
  ...signal === undefined ? {} : { signal }
8167
8222
  });
8168
8223
  child.stdout.on("data", (chunk) => {
@@ -8196,6 +8251,7 @@ function howItEnded(run) {
8196
8251
  var init_engine_run = __esm(() => {
8197
8252
  init_call_signal();
8198
8253
  init_game_log();
8254
+ init_spawn();
8199
8255
  });
8200
8256
 
8201
8257
  // src/library-copies.ts
@@ -8282,7 +8338,7 @@ var init_library_copies = () => {};
8282
8338
 
8283
8339
  // src/reimport.ts
8284
8340
  import { createHash } from "node:crypto";
8285
- import { existsSync as existsSync6, readFileSync as readFileSync4, rmSync as rmSync3, statSync as statSync3, utimesSync, writeFileSync as writeFileSync3 } from "node:fs";
8341
+ import { existsSync as existsSync6, readFileSync as readFileSync4, rmSync as rmSync3, statSync as statSync4, utimesSync, writeFileSync as writeFileSync3 } from "node:fs";
8286
8342
  import { basename as basename4, join as join6 } from "node:path";
8287
8343
  function forceNextImport(projectPath, resourcePath) {
8288
8344
  const relative = resourcePath.replace(/^res:\/\//, "");
@@ -8290,7 +8346,7 @@ function forceNextImport(projectPath, resourcePath) {
8290
8346
  if (!existsSync6(sidecar)) {
8291
8347
  return;
8292
8348
  }
8293
- const { atime, mtimeMs } = statSync3(sidecar);
8349
+ const { atime, mtimeMs } = statSync4(sidecar);
8294
8350
  writeFileSync3(sidecar, readFileSync4(sidecar, "utf8").replace(/^valid=false\r?\n/m, ""));
8295
8351
  utimesSync(sidecar, atime, new Date(mtimeMs + 2000));
8296
8352
  rmSync3(importRecord(projectPath, resourcePath), { force: true });
@@ -9704,7 +9760,7 @@ var init_gdscript_source = __esm(() => {
9704
9760
  });
9705
9761
 
9706
9762
  // src/class-cache.ts
9707
- import { existsSync as existsSync10, readdirSync as readdirSync5, readFileSync as readFileSync8, statSync as statSync4 } from "node:fs";
9763
+ import { existsSync as existsSync10, readdirSync as readdirSync5, readFileSync as readFileSync8, statSync as statSync5 } from "node:fs";
9708
9764
  import { join as join10, posix } from "node:path";
9709
9765
  function isToolScript(source) {
9710
9766
  return TOOL.test(codeOf(source));
@@ -10027,7 +10083,7 @@ function uncachedClassNote(types) {
10027
10083
  function cacheWrittenAt(projectPath) {
10028
10084
  const cache = join10(projectPath, ".godot", "global_script_class_cache.cfg");
10029
10085
  try {
10030
- return statSync4(cache).mtimeMs;
10086
+ return statSync5(cache).mtimeMs;
10031
10087
  } catch {
10032
10088
  return null;
10033
10089
  }
@@ -10039,7 +10095,7 @@ function declaredSince(projectPath, paths, names, since) {
10039
10095
  return false;
10040
10096
  }
10041
10097
  try {
10042
- return statSync4(join10(projectPath, path.slice("res://".length))).mtimeMs > since;
10098
+ return statSync5(join10(projectPath, path.slice("res://".length))).mtimeMs > since;
10043
10099
  } catch {
10044
10100
  return false;
10045
10101
  }
@@ -10687,7 +10743,6 @@ function readParams(params, key) {
10687
10743
  }
10688
10744
 
10689
10745
  // src/setup.ts
10690
- import { spawnSync } from "node:child_process";
10691
10746
  import { createHash as createHash3 } from "node:crypto";
10692
10747
  import {
10693
10748
  cpSync,
@@ -10697,7 +10752,7 @@ import {
10697
10752
  readFileSync as readFileSync11,
10698
10753
  rmdirSync,
10699
10754
  rmSync as rmSync5,
10700
- statSync as statSync5,
10755
+ statSync as statSync6,
10701
10756
  writeFileSync as writeFileSync6
10702
10757
  } from "node:fs";
10703
10758
  import { dirname as dirname6, join as join12 } from "node:path";
@@ -10717,7 +10772,7 @@ function addonsDigest(names, from) {
10717
10772
  }
10718
10773
  const path = join12(directory, name);
10719
10774
  const inside = `${relative}/${name}`;
10720
- if (statSync5(path).isDirectory()) {
10775
+ if (statSync6(path).isDirectory()) {
10721
10776
  walk(path, inside);
10722
10777
  } else {
10723
10778
  hash.update(`${inside}
@@ -10870,7 +10925,7 @@ function trackedByGit(projectPath, wanted) {
10870
10925
  if (wanted.length === 0) {
10871
10926
  return new Set;
10872
10927
  }
10873
- const asked = spawnSync("git", ["ls-files", "-z", "--", ...wanted], {
10928
+ const asked = spawnSyncHidden("git", ["ls-files", "-z", "--", ...wanted], {
10874
10929
  cwd: projectPath,
10875
10930
  encoding: "utf8"
10876
10931
  });
@@ -10953,6 +11008,7 @@ var init_setup = __esm(() => {
10953
11008
  init_project_scan();
10954
11009
  init_resources();
10955
11010
  init_server_version();
11011
+ init_spawn();
10956
11012
  ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
10957
11013
  EDITOR_PLUGINS = ["gdharness_editor", "auto_reload"];
10958
11014
  RUNTIME_AUTOLOAD = {
@@ -12001,6 +12057,11 @@ var init_tool_definitions = __esm(() => {
12001
12057
  ops: ["start"],
12002
12058
  description: "start: run the game with no sound out of the machine, by starting the engine with --audio-driver Dummy: every stream still plays and every playback position still advances, so what is playing can still be asked. The editor takes no engine flags for a game it plays, so a silent windowed run is started by this server, whose keeper is its debugger rather than the editor. A headless run, check's included, is silent already. Default false, or true when GDHARNESS_SILENT is set in the server's environment to anything but 0 or false; silent: false then plays through the editor as before."
12003
12059
  },
12060
+ executable: {
12061
+ type: "string",
12062
+ ops: ["start"],
12063
+ description: "start: an exported build to run instead of the project, as a path inside the project such as export/windows/game.exe, so what a player runs can be measured. It takes args, engineArgs, savesIn, env, headless and visible as the project does, goes on the hidden desktop the same way, and is ended by stop, but takes no scene (its pack holds the main scene) and no profile, and has no debugger: a release build quits when given one. Its runtime is not waited for."
12064
+ },
12004
12065
  savesIn: {
12005
12066
  type: "string",
12006
12067
  ops: ["start", "check"],
@@ -12156,7 +12217,7 @@ var init_tool_definitions = __esm(() => {
12156
12217
  name: "editor_rescan",
12157
12218
  title: "Rescan the project",
12158
12219
  effect: "additive",
12159
- description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. A scan or an import the editor is already running is waited out first rather than scanned over, which would start a second import over it, and the answer says how long under waitedForEditorScanMs. The scan is a change-detecting walk rather than an unconditional reparse, so a file another engine has already imported reads as settled and the walk does not look inside it: its class_name then stays out of the list the editor resolves against, however many times you scan. Any class in that state is named under unseenByEditor, which is what no check on disk can see, since the declaration and the cache are both correct there and only the editor disagrees. The cure is this call on its own, with no change to the declaring script: measured against a real editor on three platforms, with idle waits of the same length ruled out so the scan is credited rather than the time it takes, and reproduced in a second project against its own reproduction in 203ms. editor_launch restart also does it and costs a window, and project_import refresh_classes does not: it rewrites the cache and does not touch what the editor is holding. The walk also passes over a file whose .uid alone changed, so the editor keeps the old UID and every engine reading its cache warns of a duplicate; before scanning, each file whose .uid names a UID the editor does not hold for it is read again and named under uidsReread, and files on disk naming one UID between them are named under uidsDuplicatedOnDisk, which reading again cannot settle. A class the scan brings in is named under broughtIn, and every script naming it is reloaded in the editor and named under dependentsReloaded: what the editor compiled from them while it could not see the class stays compiled that way after the scan, and a script extending a new base class went on reporting \"Could not find base class\" until reloaded. One that would not reload is named under dependentsNotReloaded and the call is not ok, as it is not when the script named in reloadScript was not reloaded. A scan writes .godot/global_script_class_cache.cfg from the list the editor is holding, so a class the editor cannot resolve goes out of that file with it. That is not a reason to refuse the scan, because on a class the editor has merely not walked yet the scan is the cure; what the answer does instead is name the loss under cacheLost and rebuild the cache from the files, naming what came back under cacheRestored, so no fresh engine, CI run or clone inherits the short file. An editor that lost classes is still holding the short list, so the note says to restart it before scanning again. The other direction is guarded the same way: a class the editor still holds for a script that was renamed or deleted comes back into the cache at a path that is not there, and the next engine to read it fails on \"Could not parse global class\" in whichever correct script shares the bare name, so the cache is rebuilt without it and the answer names it under cacheDropped; the editor goes on holding it until it is restarted, and every scan until then ends in that rebuild. A script or shader written outside the editor has no .uid sidecar until something imports it, and the scan writes one beside it, which a project that commits sidecars needs before the commit. Needs the editor connected.",
12220
+ description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. A scan or an import the editor is already running is waited out first rather than scanned over, which would start a second import over it, and the answer says how long under waitedForEditorScanMs. The scan is a change-detecting walk rather than an unconditional reparse, so a file another engine has already imported reads as settled and the walk does not look inside it: its class_name then stays out of the list the editor resolves against, however many times you scan. Any class in that state is named under unseenByEditor, which is what no check on disk can see, since the declaration and the cache are both correct there and only the editor disagrees. The cure is this call on its own, with no change to the declaring script: measured against a real editor on three platforms, with idle waits of the same length ruled out so the scan is credited rather than the time it takes, and reproduced in a second project against its own reproduction in 203ms. editor_launch restart also does it and costs a window, and project_import refresh_classes does not: it rewrites the cache and does not touch what the editor is holding. The walk also passes over a file whose .uid alone changed, so the editor keeps the old UID and every engine reading its cache warns of a duplicate; before scanning, each file whose .uid names a UID the editor does not hold for it is read again and named under uidsReread, and files on disk naming one UID between them are named under uidsDuplicatedOnDisk, which reading again cannot settle. A class the scan brings in is named under broughtIn, and every script naming it is reloaded in the editor and named under dependentsReloaded: what the editor compiled from them while it could not see the class stays compiled that way after the scan, and a script extending a new base class went on reporting \"Could not find base class\" until reloaded. One that would not reload is named under dependentsNotReloaded and the call is not ok, as it is not when the script named in reloadScript was not reloaded. An editor importing answers nothing until the import is over, and a hidden one imports on the thread that answers, so a large addon install can leave the rescan itself unanswered: that is waited for as any busy editor is, within timeoutMs, and when the wait runs out first the answer is stillWorking. The classes the editor could not see before that scan are then noted in the project, and the next editor_rescan of the same editor, from any server, names them under broughtIn and reloads what names them, which it could not otherwise know to do, since by then the editor sees them. A scan writes .godot/global_script_class_cache.cfg from the list the editor is holding, so a class the editor cannot resolve goes out of that file with it. That is not a reason to refuse the scan, because on a class the editor has merely not walked yet the scan is the cure; what the answer does instead is name the loss under cacheLost and rebuild the cache from the files, naming what came back under cacheRestored, so no fresh engine, CI run or clone inherits the short file. An editor that lost classes is still holding the short list, so the note says to restart it before scanning again. The other direction is guarded the same way: a class the editor still holds for a script that was renamed or deleted comes back into the cache at a path that is not there, and the next engine to read it fails on \"Could not parse global class\" in whichever correct script shares the bare name, so the cache is rebuilt without it and the answer names it under cacheDropped; the editor goes on holding it until it is restarted, and every scan until then ends in that rebuild. A script or shader written outside the editor has no .uid sidecar until something imports it, and the scan writes one beside it, which a project that commits sidecars needs before the commit. Needs the editor connected.",
12160
12221
  parameters: {
12161
12222
  projectPath: PROJECT_PATH,
12162
12223
  timeoutMs: {
@@ -12234,13 +12295,13 @@ var init_tool_definitions = __esm(() => {
12234
12295
  pid: RUNNING_PID,
12235
12296
  nodePath: {
12236
12297
  type: "string",
12237
- ops: ["tree", "text", "find", "rect", "property"],
12238
- description: "tree, find, text: where to start, default /root. rect: the node to place. property: the node to read."
12298
+ ops: ["tree", "text", "find", "rect", "property", "accessibility"],
12299
+ description: 'tree, find, text: where to start, default /root. rect: the node to place. property: the node to read, or an engine singleton by name as runtime_invoke takes them, "Engine" with property "get_frames_drawn()". accessibility: only the elements that node and those below it made, whether as node or part_of, each window keeping the outermost of them with what they hold, and matched counting them. What a ScrollContainer holds out of its sight is not in the tree until it is scrolled to, so an element missing under one may be scrolled away rather than hidden.'
12239
12300
  },
12240
12301
  property: {
12241
12302
  type: "string",
12242
12303
  ops: ["property", "find"],
12243
- description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A packed list, such as a PackedStringArray, steps the same way. A negative number counts from the end. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that only reads (an engine method the engine marks as reading, or a game method not declared -> void, so queue_free() is refused) and walks into what it returned, which is how a question only a method answers is read in one call: which control holds the focus in the viewport this one is in. An engine method that reads takes literal arguments in its brackets, words in double quotes, numbers, true, false or null, so a control\'s theme is read where it is drawn: \'get_theme_stylebox("panel"):content_margin_top\' or \'get_theme_constant("separation")\'. A game method is given none in a path, since nothing marks one as only reading; runtime_invoke call takes those. A list or a map answers the calls that read it and take no arguments, "_game:run:board:size()" or "_game:inbox:keys()", and not those that change it, such as clear() or sort(). A step that is not there is named, and says what was there instead.'
12304
+ description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A packed list, such as a PackedStringArray, steps the same way. A negative number counts from the end. A vector, a colour, a rectangle or a transform is stepped into by its components as GDScript names them, so one axis is one number: "size:x", "global_position:y", "modulate:a", "global_transform:origin:z"; a name the value does not have is refused, naming the ones it has. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that only reads (an engine method the engine marks as reading, or a game method not declared -> void, so queue_free() is refused) and walks into what it returned, which is how a question only a method answers is read in one call: which control holds the focus in the viewport this one is in. An engine method that reads takes literal arguments in its brackets, words in double quotes, numbers, true, false or null, so a control\'s theme is read where it is drawn: \'get_theme_stylebox("panel"):content_margin_top\' or \'get_theme_constant("separation")\'. A game method is given none in a path, since nothing marks one as only reading; runtime_invoke call takes those. A list or a map answers the calls that read it and take no arguments, "_game:run:board:size()" or "_game:inbox:keys()", and not those that change it, such as clear() or sort(). A step that is not there is named, and says what was there instead.'
12244
12305
  },
12245
12306
  depth: {
12246
12307
  type: "integer",
@@ -12256,8 +12317,8 @@ var init_tool_definitions = __esm(() => {
12256
12317
  properties: {
12257
12318
  type: "array",
12258
12319
  items: { type: "string" },
12259
- ops: ["tree"],
12260
- description: "tree: the properties to read off each node, by name, so the text of every label and button under a panel is one call: a node that has not got one leaves it out. Colons read through what a node holds, as property does for find."
12320
+ ops: ["tree", "accessibility"],
12321
+ description: 'tree: the properties to read off each node, by name, so the text of every label and button under a panel is one call: a node that has not got one leaves it out. Colons read through what a node holds, as property does for find. accessibility: the fields to keep on each element, such as ["role", "name", "node"], leaving out the rest (bounds, actions, tooltip); children are always kept.'
12261
12322
  },
12262
12323
  className: {
12263
12324
  type: "string",
@@ -12273,15 +12334,15 @@ var init_tool_definitions = __esm(() => {
12273
12334
  group: { type: "string", ops: ["find"], description: "find: a group the node is in." },
12274
12335
  says: {
12275
12336
  type: "string",
12276
- ops: ["find"],
12277
- description: 'find: part of what the node has written on it as drawn, the way the text op reads it, case-insensitively and with a typed space matching any space the game draws (a non-breaking one included), which is how a button is reached by the word on it rather than by a generated path. Its own text, so a row is found by the label in it, and hidden nodes match. A bare word is a contains; write a glob and it is one, matched against the whole of what the node says, the same as namePattern, and a glob that finds nothing says under note how many nodes it would find open at both ends. A label with a line break is matched with the break in the words, and a backslash followed by n counts as one. Alternatives separated by | match when any one of them does, each by these rules ("*won*|*lost*"); write \\| for a bar that is part of the words.'
12337
+ ops: ["find", "accessibility"],
12338
+ description: 'accessibility: only the elements whose name or value says these words, matched as find matches them, with what they hold. find: part of what the node has written on it as drawn, the way the text op reads it, case-insensitively and with a typed space matching any space the game draws (a non-breaking one included), which is how a button is reached by the word on it rather than by a generated path. Its own text, so a row is found by the label in it, and hidden nodes match. A bare word is a contains; write a glob and it is one, matched against the whole of what the node says, the same as namePattern, and a glob that finds nothing says under note how many nodes it would find open at both ends. A label with a line break is matched with the break in the words, and a backslash followed by n counts as one. Alternatives separated by | match when any one of them does, each by these rules ("*won*|*lost*"); write \\| for a bar that is part of the words.'
12278
12339
  },
12279
12340
  limit: {
12280
12341
  type: "integer",
12281
12342
  minimum: 1,
12282
12343
  maximum: 5000,
12283
- ops: ["find", "text"],
12284
- description: "find: the most nodes to answer with, default 100. text: the most lines, default 500, with omitted saying how many lines that left behind. At most 5000 for either. A screen whose dialog sits under a long list is a screen read with omitted greater than zero, so raise this or point root at the dialog rather than reading the answer as what is on screen."
12344
+ ops: ["find", "text", "accessibility"],
12345
+ description: "find: the most nodes to answer with, default 100. text: the most lines, default 500, with omitted saying how many lines that left behind. accessibility: the most elements below the windows, depth first, with omitted counting the rest; default all. At most 5000 for either. A screen whose dialog sits under a long list is a screen read with omitted greater than zero, so raise this or point root at the dialog rather than reading the answer as what is on screen."
12285
12346
  },
12286
12347
  includeHidden: {
12287
12348
  type: "boolean",
@@ -12330,7 +12391,7 @@ var init_tool_definitions = __esm(() => {
12330
12391
  },
12331
12392
  accessibility: {
12332
12393
  effect: "read",
12333
- summary: "what the game tells a screen reader, read through UI Automation as a Windows screen reader reads it, under windows: one tree per window, each element with its role (text for static text, button, check_box, group for a control holding others), name, value, description, placeholder, bounds in the window's pixels, the actions a reader is offered (click, focus, toggle, set_value, select, expand_collapse, scroll_into_view), and focused on the one with the focus. node is the path of the node that made the element; an element a node made as a part of itself has part_of instead, and part: announcement marks the text a window speaks announcements through. An empty name is read as nothing: static text takes it from accessibility_update_set_value, and _note counts the text elements on screen with none. tooltip is the node's tooltip_text, which a Windows reader is not told. With no control focused, the engine reports the first node under the root, GdharnessRuntime, as focused. Needs a windowed run on Windows started with engineArgs [\"--accessibility\", \"always\"] or accessibility_support at Always Active, since the engine builds the tree only when asked; reading takes a second or two",
12394
+ summary: "what the game tells a screen reader, read through UI Automation as a Windows screen reader reads it, under windows: one tree per window, each element with its role (text for static text, button, check_box, group for a control holding others), name, value, description, placeholder, bounds in the window's pixels, the actions a reader is offered (click, focus, toggle, set_value, select, expand_collapse, scroll_into_view), and focused on the one with the focus. node is the node that made the element; an element a node made as a part of itself has part_of, the node drawn around it that holds its words, or part_of_candidates where several are, and part: announcement marks the text a window announces through. An empty name is read as nothing: static text takes it from accessibility_update_set_value, and _note counts the text elements on screen with none. tooltip is the node's tooltip_text, which a Windows reader is not told. With nothing focused, the engine calls GdharnessRuntime focused. Needs a windowed run on Windows started with engineArgs [\"--accessibility\", \"always\"] or accessibility_support at Always Active, since the engine builds the tree only when asked; reading takes a second or two",
12334
12395
  requires: []
12335
12396
  }
12336
12397
  },
@@ -12339,24 +12400,24 @@ var init_tool_definitions = __esm(() => {
12339
12400
  {
12340
12401
  name: "runtime_invoke",
12341
12402
  title: "Set or call in the running game",
12342
- description: "Sets a property or calls a method on a node in the running game. Needs the game running with the runtime addon. The answer carries elapsed_usec, how long the call or the write took inside the game, measured around it there: the round trip to the game is a second or more, so timing from outside measures that instead. Errors and warnings the engine reported while a called method ran come back under errors, the first 20, with errors_not_shown counting the rest. A call that takes longer than timeoutMs is not cancelled: the answer is pending: true with a requestId, the call goes on and does everything it was asked, and op result with that requestId collects its reply once it comes.",
12403
+ description: "Sets a property or calls a method on a node, or on one of the engine singletons nodePath names, in the running game. Needs the game running with the runtime addon. The answer carries elapsed_usec, how long the call or the write took inside the game, measured around it there: the round trip to the game is a second or more, so timing from outside measures that instead. Errors and warnings the engine reported while a called method ran come back under errors, the first 20, with errors_not_shown counting the rest. A call that takes longer than timeoutMs is not cancelled: the answer is pending: true with a requestId, the call goes on and does everything it was asked, and op result with that requestId collects its reply once it comes.",
12343
12404
  parameters: {
12344
12405
  projectPath: RUNNING_PROJECT_PATH,
12345
12406
  pid: RUNNING_PID,
12346
12407
  nodePath: {
12347
12408
  type: "string",
12348
12409
  ops: ["set", "call"],
12349
- description: 'Absolute node path, such as "/root/Main/Player".'
12410
+ description: 'Absolute node path, such as "/root/Main/Player", or the name of an engine singleton: Engine, Performance, RenderingServer, AudioServer, PhysicsServer2D, PhysicsServer3D, NavigationServer2D, NavigationServer3D, Time or TranslationServer, so "RenderingServer" with method viewport_get_measured_render_time_cpu reads what the engine measures. The other singletons are refused: OS runs programs, DisplayServer switches the keyboard layout and Input moves the real pointer.'
12350
12411
  },
12351
12412
  property: {
12352
12413
  type: "string",
12353
12414
  ops: ["set"],
12354
- description: 'set: which one to write. Colons write through what a node holds, "_game:run:day", and a number or a key steps into a list or a map on the way, "_game:run:roster:0:name", a negative number counting from the end, and a step written as a call, "get_viewport():gui_embed_subwindows", walks through what a reading method returned, as runtime_inspect property describes. A map keyed by numbers is stepped into by the number, "_game:units:3". The answer reads back off the same holder, and a write that leaves the property as it was is refused and says what it still holds: the engine not taking it and a setter keeping the value look the same, so the refusal names neither. A write of the value the property already holds, at its own precision, as a float given in 64 bits to a property keeping 32, is not refused: it answers unchanged: true.'
12415
+ description: 'set: which one to write. Colons write through what a node holds, "_game:run:day", and a number or a key steps into a list or a map on the way, "_game:run:roster:0:name", a negative number counting from the end, and a step written as a call, "get_viewport():gui_embed_subwindows", walks through what a reading method returned, as runtime_inspect property describes. A map keyed by numbers is stepped into by the number, "_game:units:3". A component of a vector, a colour or a transform, "position:x", writes that one alone: the value holding it is a copy, so it is put back where it was read from, setter and all, and a component of what a call returned is refused, since the game keeps that copy nowhere. The answer reads back off the same holder, and a write that leaves the property as it was is refused and says what it still holds: the engine not taking it and a setter keeping the value look the same, so the refusal names neither. A write of the value the property already holds, at its own precision, as a float given in 64 bits to a property keeping 32, is not refused: it answers unchanged: true.'
12355
12416
  },
12356
12417
  value: {
12357
12418
  ops: ["set"],
12358
12419
  blank: true,
12359
- description: `set: the value, fitted to the property's type. A vector, colour or the like may be written as its fields, {"x": 140, "y": 300}, or tagged, {"_type": "Vector2", "x": 140, "y": 300}, which is the form a read answers with. "" writes an empty string. A property typed as or holding an object takes a path naming one the game holds, the way call's args do, and holds that instance, or null, which the text null also means, to empty it. One declared as Variant or untyped takes anything, as the engine does: a value of the kind it holds is fitted to it, an int staying an int, and any other is stored as sent. A typed list or map, such as Array[int] or Dictionary[String, int], takes a JSON list or object with each element fitted to its element type, and an element that cannot become one is refused by its index or key, as is an object of another script than the one the list holds, whether or not that script has a class_name.`
12420
+ description: `set: the value, fitted to the property's type. A number the property would have to cut is refused rather than truncated: a fraction into an int, a number other than 0 or 1 into a bool. A vector, colour or the like may be written as its fields, {"x": 140, "y": 300}, or tagged, {"_type": "Vector2", "x": 140, "y": 300}, which is the form a read answers with. "" writes an empty string. A property typed as or holding an object takes a path naming one the game holds, the way call's args do, and holds that instance, or null, which the text null also means, to empty it. One declared as Variant or untyped takes anything, as the engine does: a value of the kind it holds is fitted to it, an int staying an int, and any other is stored as sent, a fraction into one holding an int included. A typed list or map, such as Array[int] or Dictionary[String, int], takes a JSON list or object with each element fitted to its element type, and an element that cannot become one is refused by its index or key, as is an object of another script than the one the list holds, whether or not that script has a class_name.`
12360
12421
  },
12361
12422
  method: {
12362
12423
  type: "string",
@@ -12366,7 +12427,7 @@ var init_tool_definitions = __esm(() => {
12366
12427
  args: {
12367
12428
  type: "array",
12368
12429
  ops: ["call"],
12369
- description: `call: the arguments, fitted to the method's parameter types. A vector, colour or the like may be written as its fields, {"x": 140, "y": 300}, or tagged, {"_type": "Vector2", "x": 140, "y": 300}. A parameter typed as an object takes a path naming one the game holds and is handed that instance: a colon path read from nodePath, "_game:run:wares:3", or one starting at a node, "/root/Main/Hud" or "/root/Main:_game:run". A path that reaches no object, or an object of another class than the parameter declares, is refused, and so are too few or too many arguments. The engine names a parameter typed as a script class without class_name by the class the script extends, so an object of that class with another script passes that check and the engine refuses the call: the answer says the method did not run, with the engine's reason. A parameter typed as a list or map, such as Array[int] or Array[Gear], is built from a JSON list or object element by element, objects named by their paths. The method itself takes its arguments here only: brackets on it holding arguments are refused.`
12430
+ description: `call: the arguments, fitted to the method's parameter types. A number a parameter would have to cut is refused rather than truncated: a fraction for an int, a number other than 0 or 1 for a bool. A vector, colour or the like may be written as its fields, {"x": 140, "y": 300}, or tagged, {"_type": "Vector2", "x": 140, "y": 300}. A parameter typed as an object takes a path naming one the game holds and is handed that instance: a colon path read from nodePath, "_game:run:wares:3", or one starting at a node, "/root/Main/Hud" or "/root/Main:_game:run". A path that reaches no object, or an object of another class than the parameter declares, is refused, and so are too few or too many arguments. The engine names a parameter typed as a script class without class_name by the class the script extends, so an object of that class with another script passes that check and the engine refuses the call: the answer says the method did not run, with the engine's reason. A parameter typed as a list or map, such as Array[int] or Array[Gear], is built from a JSON list or object element by element, objects named by their paths. The method itself takes its arguments here only: brackets on it holding arguments are refused.`
12370
12431
  },
12371
12432
  properties: {
12372
12433
  type: ["boolean", "array"],
@@ -12484,6 +12545,13 @@ var init_tool_definitions = __esm(() => {
12484
12545
  ops: ["action", "key", "mouse_click", "joypad_button"],
12485
12546
  description: "action, key, mouse_click, joypad_button: leave it out and the press is a whole one, down and up a frame apart. true holds it down, false lets go of one being held. A wheel button held down is still a whole step, since a wheel is never held."
12486
12547
  },
12548
+ holdFrames: {
12549
+ type: "integer",
12550
+ minimum: 1,
12551
+ maximum: 600,
12552
+ ops: ["action", "key", "mouse_click", "joypad_button"],
12553
+ description: "action, key, mouse_click, joypad_button: hold a whole press down for this many process frames and then let go, both inside the game, so a pour, a charge or a long press runs to a set length. A press, a frames wait and a release sent as three calls hold it for the time between the calls as well. The answer says held_frames. Not with pressed, and not on a wheel step. Default 1."
12554
+ },
12487
12555
  strength: {
12488
12556
  type: "number",
12489
12557
  minimum: 0,
@@ -12632,7 +12700,7 @@ var init_tool_definitions = __esm(() => {
12632
12700
  property: {
12633
12701
  type: "string",
12634
12702
  ops: ["until"],
12635
- description: 'until: the property name, or a colon path through what the node holds, "_game:run:day", read the way runtime_inspect property reads one, a step written as a call included. Walked again on every frame, so a holder the game replaces while the wait is on is followed.'
12703
+ description: 'until: the property name, or a colon path through what the node holds, "_game:run:day", read the way runtime_inspect property reads one, a step written as a call included, and one axis of a vector as well: "global_position:x" with compare ">=" stops once a thing has travelled far enough. Walked again on every frame, so a holder the game replaces while the wait is on is followed.'
12636
12704
  },
12637
12705
  value: {
12638
12706
  ops: ["until"],
@@ -34506,7 +34574,7 @@ var init_stdio = __esm(() => {
34506
34574
  });
34507
34575
 
34508
34576
  // src/runtime-client.ts
34509
- import { existsSync as existsSync14, readdirSync as readdirSync9, readFileSync as readFileSync13, statSync as statSync6, unlinkSync as unlinkSync2 } from "node:fs";
34577
+ import { existsSync as existsSync14, readdirSync as readdirSync9, readFileSync as readFileSync13, statSync as statSync7, unlinkSync as unlinkSync2 } from "node:fs";
34510
34578
  import { createConnection } from "node:net";
34511
34579
  import { tmpdir as tmpdir4 } from "node:os";
34512
34580
  import { dirname as dirname9, join as join15, resolve as resolve4 } from "node:path";
@@ -34578,7 +34646,7 @@ function parseAnnouncement(file, pid) {
34578
34646
  };
34579
34647
  let announcedAt;
34580
34648
  try {
34581
- announcedAt = statSync6(file).mtimeMs;
34649
+ announcedAt = statSync7(file).mtimeMs;
34582
34650
  } catch {}
34583
34651
  const when = announcedAt === undefined ? {} : { announcedAt };
34584
34652
  if (protocol !== RUNTIME_PROTOCOL) {
@@ -34692,7 +34760,7 @@ function announcedIn(directory) {
34692
34760
  entries.push({ file, pid, running });
34693
34761
  if (running) {
34694
34762
  try {
34695
- candidates.push({ file, pid, writtenAt: statSync6(file).mtimeMs });
34763
+ candidates.push({ file, pid, writtenAt: statSync7(file).mtimeMs });
34696
34764
  } catch {}
34697
34765
  }
34698
34766
  }
@@ -34749,7 +34817,7 @@ function sweepErrorReport(path, pid, now = Date.now()) {
34749
34817
  return;
34750
34818
  }
34751
34819
  try {
34752
- if (now - statSync6(path).mtimeMs >= ERROR_REPORT_KEEP_MS) {
34820
+ if (now - statSync7(path).mtimeMs >= ERROR_REPORT_KEEP_MS) {
34753
34821
  unlinkSync2(path);
34754
34822
  }
34755
34823
  } catch {}
@@ -35134,8 +35202,39 @@ var init_breakpoint_note = __esm(() => {
35134
35202
  init_paths();
35135
35203
  });
35136
35204
 
35205
+ // src/whole-file.ts
35206
+ import { renameSync as renameSync2, rmSync as rmSync8, writeFileSync as writeFileSync10 } from "node:fs";
35207
+ function writeWhole(path, text) {
35208
+ const partial = `${path}.${process.pid}.partial`;
35209
+ try {
35210
+ writeFileSync10(partial, text, "utf8");
35211
+ } catch {
35212
+ return false;
35213
+ }
35214
+ const until = Date.now() + HELD_FILE_MS;
35215
+ for (;; ) {
35216
+ try {
35217
+ renameSync2(partial, path);
35218
+ return true;
35219
+ } catch (error) {
35220
+ const code = error.code ?? "";
35221
+ if (!HELD_BY_A_READER.has(code) || Date.now() >= until) {
35222
+ try {
35223
+ rmSync8(partial, { force: true });
35224
+ } catch {}
35225
+ return false;
35226
+ }
35227
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, HELD_FILE_RETRY_MS);
35228
+ }
35229
+ }
35230
+ }
35231
+ var HELD_FILE_MS = 1000, HELD_FILE_RETRY_MS = 10, HELD_BY_A_READER;
35232
+ var init_whole_file = __esm(() => {
35233
+ HELD_BY_A_READER = new Set(["EPERM", "EACCES", "EBUSY"]);
35234
+ });
35235
+
35137
35236
  // src/class-note.ts
35138
- import { mkdirSync as mkdirSync8, readFileSync as readFileSync16, writeFileSync as writeFileSync10 } from "node:fs";
35237
+ import { mkdirSync as mkdirSync8, readFileSync as readFileSync16 } from "node:fs";
35139
35238
  import { dirname as dirname12, join as join18 } from "node:path";
35140
35239
  function classNotePath(projectPath) {
35141
35240
  return join18(projectPath, ".godot", "gdharness-classes.json");
@@ -35148,23 +35247,40 @@ function readClassNote(projectPath) {
35148
35247
  return null;
35149
35248
  }
35150
35249
  const pid = readNumber(fields, "shortListEditorPid");
35250
+ const unfinished = unfinishedScanIn(fields["unseenBeforeAnUnfinishedScan"]);
35151
35251
  return {
35152
35252
  rebuiltWith: rebuiltWith.map(String),
35153
- ...Object.hasOwn(fields, "shortListEditorPid") ? { shortListEditorPid: pid ?? null } : {}
35253
+ ...Object.hasOwn(fields, "shortListEditorPid") ? { shortListEditorPid: pid ?? null } : {},
35254
+ ...unfinished === null ? {} : { unseenBeforeAnUnfinishedScan: unfinished }
35154
35255
  };
35155
35256
  } catch {
35156
35257
  return null;
35157
35258
  }
35158
35259
  }
35260
+ function unfinishedScanIn(value) {
35261
+ if (typeof value !== "object" || value === null) {
35262
+ return null;
35263
+ }
35264
+ const fields = asParams(value);
35265
+ const classes = readArray(fields, "classes");
35266
+ if (classes === undefined || !classes.every((name) => typeof name === "string")) {
35267
+ return null;
35268
+ }
35269
+ return { editorPid: readNumber(fields, "editorPid") ?? null, classes: classes.map(String) };
35270
+ }
35159
35271
  function writeClassNote(projectPath, note) {
35272
+ const path = classNotePath(projectPath);
35160
35273
  try {
35161
- const path = classNotePath(projectPath);
35162
35274
  mkdirSync8(dirname12(path), { recursive: true });
35163
- writeFileSync10(path, `${JSON.stringify(note, null, 2)}
35164
- `, "utf8");
35165
- } catch {}
35275
+ } catch {
35276
+ return;
35277
+ }
35278
+ writeWhole(path, `${JSON.stringify(note, null, 2)}
35279
+ `);
35166
35280
  }
35167
- var init_class_note = () => {};
35281
+ var init_class_note = __esm(() => {
35282
+ init_whole_file();
35283
+ });
35168
35284
 
35169
35285
  // src/config-pin.ts
35170
35286
  import { existsSync as existsSync15, readFileSync as readFileSync17 } from "node:fs";
@@ -35197,7 +35313,7 @@ var init_config_pin = __esm(() => {
35197
35313
  });
35198
35314
 
35199
35315
  // src/cut-test-run.ts
35200
- import { existsSync as existsSync16, mkdirSync as mkdirSync9, readFileSync as readFileSync18, rmSync as rmSync8, writeFileSync as writeFileSync11 } from "node:fs";
35316
+ import { existsSync as existsSync16, mkdirSync as mkdirSync9, readFileSync as readFileSync18, rmSync as rmSync9, writeFileSync as writeFileSync11 } from "node:fs";
35201
35317
  import { dirname as dirname13, join as join20 } from "node:path";
35202
35318
  function keptAt(projectPath) {
35203
35319
  return join20(projectPath, ".godot", "gdharness-reports", "cut-run.json");
@@ -35217,10 +35333,10 @@ function takeCutTestRun(projectPath) {
35217
35333
  }
35218
35334
  try {
35219
35335
  const run = JSON.parse(readFileSync18(path, "utf8"));
35220
- rmSync8(path, { force: true });
35336
+ rmSync9(path, { force: true });
35221
35337
  return typeof run.cutAt === "string" && Array.isArray(run.paths) && run.paths.every((path) => typeof path === "string") ? run : null;
35222
35338
  } catch {
35223
- rmSync8(path, { force: true });
35339
+ rmSync9(path, { force: true });
35224
35340
  return null;
35225
35341
  }
35226
35342
  }
@@ -36037,8 +36153,8 @@ import {
36037
36153
  openSync,
36038
36154
  readFileSync as readFileSync19,
36039
36155
  readSync,
36040
- rmSync as rmSync9,
36041
- statSync as statSync7,
36156
+ rmSync as rmSync10,
36157
+ statSync as statSync8,
36042
36158
  writeFileSync as writeFileSync12
36043
36159
  } from "node:fs";
36044
36160
  import { dirname as dirname14, join as join21 } from "node:path";
@@ -36062,8 +36178,8 @@ function clearEditorLog(projectPath) {
36062
36178
  try {
36063
36179
  const path = editorLogPath(projectPath);
36064
36180
  mkdirSync10(dirname14(path), { recursive: true });
36065
- rmSync9(path, { force: true });
36066
- rmSync9(editorLogNotePath(projectPath), { force: true });
36181
+ rmSync10(path, { force: true });
36182
+ rmSync10(editorLogNotePath(projectPath), { force: true });
36067
36183
  } catch {}
36068
36184
  }
36069
36185
  function writeEditorLogNote(projectPath, pid) {
@@ -36147,7 +36263,7 @@ function foldProgress(text) {
36147
36263
  `);
36148
36264
  }
36149
36265
  function theFirstOf(path) {
36150
- const whole = statSync7(path).size;
36266
+ const whole = statSync8(path).size;
36151
36267
  if (whole <= MOST_CONSOLE_BYTES) {
36152
36268
  return { text: readFileSync19(path, "utf8") };
36153
36269
  }
@@ -39286,12 +39402,21 @@ function getDefaultBridge(ownProject = null) {
39286
39402
  defaultBridge ??= new GodotBridge(resolveDefaultBridgePort(), resolveDefaultBridgeHost(), DEFAULT_TIMEOUT_MS, ownProject);
39287
39403
  return defaultBridge;
39288
39404
  }
39289
- var DEFAULT_PORT = 6505, DEFAULT_HOST = "127.0.0.1", DEFAULT_TIMEOUT_MS = 30000, KEEPALIVE_INTERVAL_MS = 1e4, SECOND_CONNECTION_CLOSE_CODE = 4000, MOST_CLOSE_REASON_BYTES = 123, OTHER_PROJECT_CLOSE_CODE = 4001, ONE_SHOT_CLOSE_CODE = 4002, CONNECT_WINDOW_MS = 35000, GodotBridge, defaultBridge = null;
39405
+ var DEFAULT_PORT = 6505, DEFAULT_HOST = "127.0.0.1", DEFAULT_TIMEOUT_MS = 30000, KEEPALIVE_INTERVAL_MS = 1e4, SECOND_CONNECTION_CLOSE_CODE = 4000, MOST_CLOSE_REASON_BYTES = 123, OTHER_PROJECT_CLOSE_CODE = 4001, ONE_SHOT_CLOSE_CODE = 4002, CONNECT_WINDOW_MS = 35000, BridgeTimeout, GodotBridge, defaultBridge = null;
39290
39406
  var init_godot_bridge = __esm(() => {
39291
39407
  init_wrapper();
39292
39408
  init_errors();
39293
39409
  init_paths();
39294
39410
  init_ports();
39411
+ BridgeTimeout = class BridgeTimeout extends Error {
39412
+ toolName;
39413
+ waitedMs;
39414
+ constructor(toolName, waitedMs) {
39415
+ super(`Tool ${toolName} timed out after ${waitedMs}ms`);
39416
+ this.toolName = toolName;
39417
+ this.waitedMs = waitedMs;
39418
+ }
39419
+ };
39295
39420
  GodotBridge = class GodotBridge extends EventEmitter {
39296
39421
  listeningSince = null;
39297
39422
  httpServer = null;
@@ -39629,7 +39754,7 @@ var init_godot_bridge = __esm(() => {
39629
39754
  return new Promise((resolve, reject) => {
39630
39755
  const timeout = setTimeout(() => {
39631
39756
  this.pendingRequests.delete(requestId);
39632
- reject(new Error(`Tool ${toolName} timed out after ${this.timeoutMs}ms`));
39757
+ reject(new BridgeTimeout(toolName, this.timeoutMs));
39633
39758
  }, this.timeoutMs);
39634
39759
  this.pendingRequests.set(requestId, {
39635
39760
  toolName,
@@ -40357,7 +40482,7 @@ var DEBUG_PREFIX = "debug ";
40357
40482
  var init_keeper_channel = () => {};
40358
40483
 
40359
40484
  // src/lifecycle-log.ts
40360
- import { appendFileSync, mkdirSync as mkdirSync11, renameSync as renameSync2, statSync as statSync8 } from "node:fs";
40485
+ import { appendFileSync, mkdirSync as mkdirSync11, renameSync as renameSync3, statSync as statSync9 } from "node:fs";
40361
40486
  import { dirname as dirname15, join as join22 } from "node:path";
40362
40487
  import process10 from "node:process";
40363
40488
  function lifecycleLogPath() {
@@ -40367,9 +40492,9 @@ function writeLifecycle(event, fields = {}) {
40367
40492
  const path = lifecycleLogPath();
40368
40493
  try {
40369
40494
  mkdirSync11(dirname15(path), { recursive: true });
40370
- if ((statSync8(path, { throwIfNoEntry: false })?.size ?? 0) > LIFECYCLE_LOG_LIMIT) {
40495
+ if ((statSync9(path, { throwIfNoEntry: false })?.size ?? 0) > LIFECYCLE_LOG_LIMIT) {
40371
40496
  try {
40372
- renameSync2(path, `${path}.1`);
40497
+ renameSync3(path, `${path}.1`);
40373
40498
  } catch {}
40374
40499
  }
40375
40500
  const line = {
@@ -41230,7 +41355,6 @@ var init_lsp_client = __esm(() => {
41230
41355
  });
41231
41356
 
41232
41357
  // src/outside.ts
41233
- import { spawn as spawn3 } from "node:child_process";
41234
41358
  import { fileURLToPath as fileURLToPath4 } from "node:url";
41235
41359
  function changesFrom(env, base) {
41236
41360
  const changes = {};
@@ -41263,9 +41387,8 @@ function readLaunched(printed) {
41263
41387
  }
41264
41388
  async function launchOutsideTheTree(spec, runtime = process.execPath, keeper = KEEPER_SCRIPT) {
41265
41389
  return await new Promise((resolve) => {
41266
- const launcher = spawn3(runtime, [keeper, "launch"], {
41267
- stdio: ["pipe", "pipe", "pipe"],
41268
- windowsHide: true
41390
+ const launcher = spawnHidden(runtime, [keeper, "launch"], {
41391
+ stdio: ["pipe", "pipe", "pipe"]
41269
41392
  });
41270
41393
  sendSpec(launcher, {
41271
41394
  command: spec.command,
@@ -41294,6 +41417,7 @@ async function launchOutsideTheTree(spec, runtime = process.execPath, keeper = K
41294
41417
  }
41295
41418
  var KEEPER_SCRIPT;
41296
41419
  var init_outside = __esm(() => {
41420
+ init_spawn();
41297
41421
  KEEPER_SCRIPT = fileURLToPath4(new URL("./keeper.js", import.meta.url));
41298
41422
  });
41299
41423
 
@@ -41334,8 +41458,6 @@ var STARTING_MS = 20000;
41334
41458
  var init_port_probe = () => {};
41335
41459
 
41336
41460
  // src/process-time.ts
41337
- import { execFile as execFile3 } from "node:child_process";
41338
- import { promisify as promisify3 } from "node:util";
41339
41461
  async function cpuSecondsOf(pid) {
41340
41462
  try {
41341
41463
  return process.platform === "win32" ? await secondsOnWindows(pid) : await askPosix2(pid);
@@ -41349,7 +41471,7 @@ async function secondsOnWindows(pid) {
41349
41471
  return Number.isFinite(seconds) ? seconds : undefined;
41350
41472
  }
41351
41473
  async function askPosix2(pid) {
41352
- const { stdout } = await run3("ps", ["-o", "time=", "-p", String(pid)], { timeout: ASK_TIMEOUT_MS2 });
41474
+ const { stdout } = await runHidden("ps", ["-o", "time=", "-p", String(pid)], { timeout: ASK_TIMEOUT_MS2 });
41353
41475
  return secondsFromClock(stdout.trim());
41354
41476
  }
41355
41477
  function secondsFromClock(printed) {
@@ -41367,10 +41489,10 @@ function secondsFromClock(printed) {
41367
41489
  const extra = days === null ? 0 : Number(days) * 86400;
41368
41490
  return Number.isFinite(extra) ? seconds + extra : undefined;
41369
41491
  }
41370
- var run3, ASK_TIMEOUT_MS2 = 6000;
41492
+ var ASK_TIMEOUT_MS2 = 6000;
41371
41493
  var init_process_time = __esm(() => {
41494
+ init_spawn();
41372
41495
  init_windows_ask();
41373
- run3 = promisify3(execFile3);
41374
41496
  });
41375
41497
 
41376
41498
  // src/prompts.ts
@@ -41510,7 +41632,7 @@ var init_prompts = __esm(() => {
41510
41632
  });
41511
41633
 
41512
41634
  // src/rename.ts
41513
- import { existsSync as existsSync19, mkdirSync as mkdirSync12, readFileSync as readFileSync20, renameSync as renameSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync13 } from "node:fs";
41635
+ import { existsSync as existsSync19, mkdirSync as mkdirSync12, readFileSync as readFileSync20, renameSync as renameSync4, unlinkSync as unlinkSync3, writeFileSync as writeFileSync13 } from "node:fs";
41514
41636
  import { dirname as dirname17, posix as posix2 } from "node:path";
41515
41637
 
41516
41638
  class Script {
@@ -42226,12 +42348,12 @@ function applyRewrites(rewrites) {
42226
42348
  mkdirSync12(dirname17(rewrite.writeTo), { recursive: true });
42227
42349
  const staging = `${rewrite.writeTo}.gdharness-rename`;
42228
42350
  writeFileSync13(staging, rewrite.after, "utf8");
42229
- renameSync3(staging, rewrite.writeTo);
42351
+ renameSync4(staging, rewrite.writeTo);
42230
42352
  let movedUid = false;
42231
42353
  if (rewrite.writeTo !== rewrite.absolute) {
42232
42354
  unlinkSync3(rewrite.absolute);
42233
42355
  if (existsSync19(`${rewrite.absolute}.uid`)) {
42234
- renameSync3(`${rewrite.absolute}.uid`, `${rewrite.writeTo}.uid`);
42356
+ renameSync4(`${rewrite.absolute}.uid`, `${rewrite.writeTo}.uid`);
42235
42357
  movedUid = true;
42236
42358
  }
42237
42359
  }
@@ -42244,7 +42366,7 @@ function applyRewrites(rewrites) {
42244
42366
  if (rewrite.writeTo !== rewrite.absolute) {
42245
42367
  unlinkSync3(rewrite.writeTo);
42246
42368
  if (movedUid) {
42247
- renameSync3(`${rewrite.writeTo}.uid`, `${rewrite.absolute}.uid`);
42369
+ renameSync4(`${rewrite.writeTo}.uid`, `${rewrite.absolute}.uid`);
42248
42370
  }
42249
42371
  }
42250
42372
  } catch {}
@@ -42276,7 +42398,7 @@ var init_rename = __esm(() => {
42276
42398
  });
42277
42399
 
42278
42400
  // src/rename-symbol.ts
42279
- import { existsSync as existsSync20, mkdirSync as mkdirSync13, readdirSync as readdirSync10, realpathSync as realpathSync5, rmSync as rmSync10, writeFileSync as writeFileSync14 } from "node:fs";
42401
+ import { existsSync as existsSync20, mkdirSync as mkdirSync13, readdirSync as readdirSync10, realpathSync as realpathSync5, rmSync as rmSync11, writeFileSync as writeFileSync14 } from "node:fs";
42280
42402
  import { basename as basename6, dirname as dirname18, join as join24, relative as relative4, resolve as resolve6 } from "node:path";
42281
42403
  function resourcePathOf(projectPath, absolute) {
42282
42404
  const inside = relative4(onDiskSpelling(projectPath), onDiskSpelling(absolute));
@@ -42726,7 +42848,7 @@ function writeLeftAloneDetail(projectPath, symbol, detail) {
42726
42848
  `, "utf8");
42727
42849
  const kept = readdirSync10(directory).filter((name) => name.endsWith(".json")).sort().reverse();
42728
42850
  for (const old of kept.slice(10)) {
42729
- rmSync10(join24(directory, old), { force: true });
42851
+ rmSync11(join24(directory, old), { force: true });
42730
42852
  }
42731
42853
  return path;
42732
42854
  }
@@ -42766,7 +42888,7 @@ var init_rename_symbol = __esm(() => {
42766
42888
  });
42767
42889
 
42768
42890
  // src/restart-note.ts
42769
- import { mkdirSync as mkdirSync14, readFileSync as readFileSync21, rmSync as rmSync11, writeFileSync as writeFileSync15 } from "node:fs";
42891
+ import { mkdirSync as mkdirSync14, readFileSync as readFileSync21, rmSync as rmSync12, writeFileSync as writeFileSync15 } from "node:fs";
42770
42892
  import { dirname as dirname19, join as join25 } from "node:path";
42771
42893
  function restartNotePath(projectPath) {
42772
42894
  return join25(projectPath, ".godot", "gdharness-restart.json");
@@ -42798,7 +42920,7 @@ function restartOwed(projectPath) {
42798
42920
  }
42799
42921
  function restartSettled(projectPath) {
42800
42922
  try {
42801
- rmSync11(restartNotePath(projectPath), { force: true });
42923
+ rmSync12(restartNotePath(projectPath), { force: true });
42802
42924
  } catch (error) {
42803
42925
  console.error(`[SERVER] Could not clear the restart note: ${errorMessage(error)}`);
42804
42926
  }
@@ -42807,43 +42929,10 @@ var init_restart_note = __esm(() => {
42807
42929
  init_errors();
42808
42930
  });
42809
42931
 
42810
- // src/whole-file.ts
42811
- import { renameSync as renameSync4, rmSync as rmSync12, writeFileSync as writeFileSync16 } from "node:fs";
42812
- function writeWhole(path, text) {
42813
- const partial = `${path}.${process.pid}.partial`;
42814
- try {
42815
- writeFileSync16(partial, text, "utf8");
42816
- } catch {
42817
- return false;
42818
- }
42819
- const until = Date.now() + HELD_FILE_MS;
42820
- for (;; ) {
42821
- try {
42822
- renameSync4(partial, path);
42823
- return true;
42824
- } catch (error) {
42825
- const code = error.code ?? "";
42826
- if (!HELD_BY_A_READER.has(code) || Date.now() >= until) {
42827
- try {
42828
- rmSync12(partial, { force: true });
42829
- } catch {}
42830
- return false;
42831
- }
42832
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, HELD_FILE_RETRY_MS);
42833
- }
42834
- }
42835
- }
42836
- var HELD_FILE_MS = 1000, HELD_FILE_RETRY_MS = 10, HELD_BY_A_READER;
42837
- var init_whole_file = __esm(() => {
42838
- HELD_BY_A_READER = new Set(["EPERM", "EACCES", "EBUSY"]);
42839
- });
42840
-
42841
42932
  // src/run-record.ts
42842
- import { execFile as execFile4 } from "node:child_process";
42843
42933
  import { createHash as createHash4 } from "node:crypto";
42844
- import { mkdirSync as mkdirSync15, openSync as openSync2, readdirSync as readdirSync11, readFileSync as readFileSync22, realpathSync as realpathSync6, rmSync as rmSync13, statSync as statSync9 } from "node:fs";
42934
+ import { mkdirSync as mkdirSync15, openSync as openSync2, readdirSync as readdirSync11, readFileSync as readFileSync22, realpathSync as realpathSync6, rmSync as rmSync13, statSync as statSync10 } from "node:fs";
42845
42935
  import { basename as basename7, dirname as dirname20, join as join26, resolve as resolve7 } from "node:path";
42846
- import { promisify as promisify4 } from "node:util";
42847
42936
  function runsDirectory() {
42848
42937
  return join26(runtimeDirectory(), "runs");
42849
42938
  }
@@ -43038,7 +43127,7 @@ async function runningAs(pid) {
43038
43127
  const raw = readFileSync22(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
43039
43128
  return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
43040
43129
  }
43041
- const args = (await execFileAsync("ps", ["-p", String(pid), "-o", "args="], { encoding: "utf8", timeout: 15000 })).stdout.trim();
43130
+ const args = (await runHidden("ps", ["-p", String(pid), "-o", "args="], { timeout: 15000 })).stdout.trim();
43042
43131
  return args === "" ? null : { kind: "commandLine", text: args, ...when };
43043
43132
  } catch {
43044
43133
  return null;
@@ -43047,7 +43136,7 @@ async function runningAs(pid) {
43047
43136
  async function listeningPid(port) {
43048
43137
  try {
43049
43138
  if (process.platform === "win32") {
43050
- const { stdout } = await execFileAsync("netstat", ["-ano"], { encoding: "utf8", timeout: 15000 });
43139
+ const { stdout } = await runHidden("netstat", ["-ano"], { timeout: 15000 });
43051
43140
  return listeningPidInNetstat(stdout, port);
43052
43141
  }
43053
43142
  for (const [command, args, pattern] of [
@@ -43055,7 +43144,7 @@ async function listeningPid(port) {
43055
43144
  ["ss", ["-ltnpH", `sport = :${port}`], /pid=(\d+)/]
43056
43145
  ]) {
43057
43146
  try {
43058
- const said = (await execFileAsync(command, [...args], { encoding: "utf8", timeout: 15000 })).stdout.trim();
43147
+ const said = (await runHidden(command, [...args], { timeout: 15000 })).stdout.trim();
43059
43148
  const found = pattern.exec(said);
43060
43149
  if (found?.[1] !== undefined) {
43061
43150
  return Number(found[1]);
@@ -43100,7 +43189,7 @@ async function startedAt(pid, windowsAnswer) {
43100
43189
  return Number.isNaN(when) ? null : when;
43101
43190
  }
43102
43191
  try {
43103
- const said = (await execFileAsync("ps", ["-p", String(pid), "-o", "lstart="], { encoding: "utf8", timeout: 15000 })).stdout.trim();
43192
+ const said = (await runHidden("ps", ["-p", String(pid), "-o", "lstart="], { timeout: 15000 })).stdout.trim();
43104
43193
  const when = Date.parse(said);
43105
43194
  return said === "" || Number.isNaN(when) ? null : when;
43106
43195
  } catch {
@@ -43109,10 +43198,8 @@ async function startedAt(pid, windowsAnswer) {
43109
43198
  }
43110
43199
  async function windowsImage(pid) {
43111
43200
  try {
43112
- const { stdout: csv } = await execFileAsync("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
43113
- encoding: "utf8",
43114
- timeout: 15000,
43115
- windowsHide: true
43201
+ const { stdout: csv } = await runHidden("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
43202
+ timeout: 15000
43116
43203
  });
43117
43204
  const image = /^"([^"]+)"/.exec(csv.trim())?.[1];
43118
43205
  return image === undefined ? { why: "named no process under that number" } : { image };
@@ -43192,7 +43279,7 @@ function clearRunRecord(projectPath) {
43192
43279
  }
43193
43280
  function ageMs(path, now) {
43194
43281
  try {
43195
- return now - statSync9(path).mtimeMs;
43282
+ return now - statSync10(path).mtimeMs;
43196
43283
  } catch {
43197
43284
  return 0;
43198
43285
  }
@@ -43223,15 +43310,15 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
43223
43310
  } catch {}
43224
43311
  }
43225
43312
  }
43226
- var execFileAsync, SHARED_NOTE = "run.json", SAME_RUN_WINDOW_MS = 1e4;
43313
+ var SHARED_NOTE = "run.json", SAME_RUN_WINDOW_MS = 1e4;
43227
43314
  var init_run_record = __esm(() => {
43228
43315
  init_errors();
43229
43316
  init_paths();
43230
43317
  init_process_children();
43231
43318
  init_runtime_client();
43319
+ init_spawn();
43232
43320
  init_whole_file();
43233
43321
  init_windows_ask();
43234
- execFileAsync = promisify4(execFile4);
43235
43322
  });
43236
43323
 
43237
43324
  // src/script-origins.ts
@@ -43507,7 +43594,6 @@ var init_server_instructions = __esm(() => {
43507
43594
  });
43508
43595
 
43509
43596
  // src/server.ts
43510
- import { execFile as execFile5, spawn as spawn4 } from "node:child_process";
43511
43597
  import { randomUUID as randomUUID2 } from "node:crypto";
43512
43598
  import {
43513
43599
  appendFileSync as appendFileSync2,
@@ -43522,12 +43608,11 @@ import {
43522
43608
  readFileSync as readFileSync24,
43523
43609
  readSync as readSync2,
43524
43610
  rmdirSync as rmdirSync2,
43525
- statSync as statSync10
43611
+ statSync as statSync11
43526
43612
  } from "node:fs";
43527
43613
  import { basename as basename8, dirname as dirname21, extname as extname3, isAbsolute as isAbsolute4, join as join28, normalize as normalize2, relative as relative5 } from "node:path";
43528
43614
  import { setTimeout as delay4 } from "node:timers/promises";
43529
43615
  import { fileURLToPath as fileURLToPath5 } from "node:url";
43530
- import { promisify as promisify5 } from "node:util";
43531
43616
  function editorRestartTimeoutMs() {
43532
43617
  return waitFromEnv("GDHARNESS_EDITOR_RESTART_TIMEOUT_MS", 90000);
43533
43618
  }
@@ -43620,6 +43705,9 @@ function captureDetail(payload) {
43620
43705
  function patienceForFrames(frames, atLeast) {
43621
43706
  return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
43622
43707
  }
43708
+ function withDefined(fields) {
43709
+ return Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined));
43710
+ }
43623
43711
  function longerWaitNote(tool, op, waitedMs) {
43624
43712
  const spec = TOOL_SPECS.find((one) => one.name === tool);
43625
43713
  if (spec !== undefined && argumentsOf(spec, op).includes("timeoutMs")) {
@@ -44063,6 +44151,31 @@ function notRunNote(notRun, failFast) {
44063
44151
  const cases = `${notRun} case${notRun === 1 ? "" : "s"}`;
44064
44152
  return failFast ? `This run stopped at the first failure in each suite it failed in, so ${cases} never ran and count as neither passed nor failed. Leave failFast out to run every case.` : `${cases} counted in ${notRun === 1 ? "its suite" : "their suites"} never ran and count as neither passed nor failed. failFast was not set, so it was not that: the suites below with notRun are the ones to read.`;
44065
44153
  }
44154
+ function exportPresetRefusal(projectPath, preset) {
44155
+ let text;
44156
+ try {
44157
+ text = readFileSync24(join28(projectPath, "export_presets.cfg"), "utf8");
44158
+ } catch {
44159
+ return `This project has no export_presets.cfg, so it has no preset '${preset}' to export with. The editor writes the file when a preset is added in Project > Export.`;
44160
+ }
44161
+ const names = [];
44162
+ let inPreset = false;
44163
+ for (const line of text.split(/\r?\n/)) {
44164
+ const section = /^\s*\[([^\]]+)\]/.exec(line)?.[1];
44165
+ if (section !== undefined) {
44166
+ inPreset = /^preset\.\d+$/.test(section.trim());
44167
+ continue;
44168
+ }
44169
+ const named = inPreset ? /^\s*name\s*=\s*"((?:[^"\\]|\\.)*)"/.exec(line)?.[1] : undefined;
44170
+ if (named !== undefined) {
44171
+ names.push(named.replace(/\\(.)/g, "$1"));
44172
+ }
44173
+ }
44174
+ if (names.includes(preset)) {
44175
+ return null;
44176
+ }
44177
+ return names.length === 0 ? `export_presets.cfg names no preset, so there is no '${preset}' to export with. The editor adds one in Project > Export.` : `This project has no export preset named '${preset}'. Its presets are ${names.map((name) => `'${name}'`).join(", ")}.`;
44178
+ }
44066
44179
  function exportAnswer(ending, asked, before, after) {
44067
44180
  const { log } = ending;
44068
44181
  const written = after !== null && after !== before;
@@ -44631,7 +44744,7 @@ function scriptsToDiagnose(projectPath, given) {
44631
44744
  }
44632
44745
  function isDirectory(path) {
44633
44746
  try {
44634
- return statSync10(path).isDirectory();
44747
+ return statSync11(path).isDirectory();
44635
44748
  } catch {
44636
44749
  return false;
44637
44750
  }
@@ -45500,7 +45613,15 @@ class GodotServer {
45500
45613
  property: readNonEmptyString(args, "property") ?? ""
45501
45614
  }, { tool: "runtime_inspect", op, patience });
45502
45615
  case "accessibility":
45503
- return await this.handleRuntimeCommand("get_accessibility", whichGame(args), {
45616
+ return await this.handleRuntimeCommand("get_accessibility", {
45617
+ ...whichGame(args),
45618
+ ...withDefined({
45619
+ node_path: readNonEmptyString(args, "nodePath"),
45620
+ says: readNonEmptyString(args, "says"),
45621
+ properties: readStringArray(args, "properties"),
45622
+ limit: readPositiveNumber(args, "limit")
45623
+ })
45624
+ }, {
45504
45625
  tool: "runtime_inspect",
45505
45626
  op,
45506
45627
  patience: readPositiveNumber(args, "timeoutMs") ?? Math.max(30000, this.runtimeTimeoutMs())
@@ -45551,7 +45672,14 @@ class GodotServer {
45551
45672
  ...args["index"] === undefined ? {} : { index: args["index"] }
45552
45673
  }, { tool: "runtime_input", op });
45553
45674
  }
45554
- return await this.handleRuntimeCommand(`inject_${op}`, args, { tool: "runtime_input", op });
45675
+ {
45676
+ const holding = readPositiveNumber(args, "holdFrames");
45677
+ return await this.handleRuntimeCommand(`inject_${op}`, args, {
45678
+ tool: "runtime_input",
45679
+ op,
45680
+ ...holding === undefined ? {} : { patience: patienceForFrames(holding, this.runtimeTimeoutMs()) }
45681
+ });
45682
+ }
45555
45683
  case "runtime_wait":
45556
45684
  return await this.handleRuntimeWait(op, args);
45557
45685
  case "debug_breakpoint": {
@@ -46334,6 +46462,10 @@ class GodotServer {
46334
46462
  if (!output.ok) {
46335
46463
  return this.createErrorResponse(output.reason, PATH_SOLUTIONS);
46336
46464
  }
46465
+ const noPreset = exportPresetRefusal(project.value.path, preset);
46466
+ if (noPreset !== null) {
46467
+ return this.createErrorResponse(noPreset, ["project_export list reads the presets the project has"]);
46468
+ }
46337
46469
  const engine = await this.engine();
46338
46470
  if (!engine.ok) {
46339
46471
  return engine.response;
@@ -46357,7 +46489,7 @@ class GodotServer {
46357
46489
  output.absolutePath
46358
46490
  ];
46359
46491
  this.logDebug(`Exporting: ${engine.value} ${exportArgs.join(" ")}`);
46360
- const writtenAt = () => existsSync21(output.absolutePath) ? statSync10(output.absolutePath).mtimeMs : null;
46492
+ const writtenAt = () => existsSync21(output.absolutePath) ? statSync11(output.absolutePath).mtimeMs : null;
46361
46493
  const before = writtenAt();
46362
46494
  const freed = freeLibraryCopies(project.value.path);
46363
46495
  if (freed.setAside.length > 0 || freed.swept.length > 0) {
@@ -46702,7 +46834,7 @@ class GodotServer {
46702
46834
  }
46703
46835
  async godotVersion(godotPath) {
46704
46836
  try {
46705
- const { stdout } = await run4(godotPath, ["--version"], { timeout: 1e4 });
46837
+ const { stdout } = await runHidden(godotPath, ["--version"], { timeout: 1e4 });
46706
46838
  this.lastGodotVersion = stdout.trim();
46707
46839
  return this.lastGodotVersion;
46708
46840
  } catch (error) {
@@ -46763,6 +46895,10 @@ class GodotServer {
46763
46895
  return markIfStale(answer, this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest(), this.editorCodeOnDisk());
46764
46896
  }
46765
46897
  async diagnosticsOf(args) {
46898
+ const waited = await this.waitForTheEditorOpenedHere(args, []);
46899
+ if (waited !== null) {
46900
+ return { refusal: waited, perScript: false };
46901
+ }
46766
46902
  const answer = await this.handleLSP("lsp_get_diagnostics", args);
46767
46903
  const payload = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
46768
46904
  const refused = this.languageServerRefusal(payload, "Diagnostics");
@@ -46847,9 +46983,34 @@ class GodotServer {
46847
46983
  const recorded = readClassNote(projectPath)?.shortListEditorPid;
46848
46984
  return recorded !== undefined && recorded === (this.godotBridge.getStatus().editorPid ?? null);
46849
46985
  }
46986
+ unseenBeforeAnUnfinishedScan(projectPath) {
46987
+ const noted = readClassNote(projectPath)?.unseenBeforeAnUnfinishedScan;
46988
+ if (noted === undefined || noted.editorPid !== (this.godotBridge.getStatus().editorPid ?? null)) {
46989
+ return [];
46990
+ }
46991
+ return [...noted.classes];
46992
+ }
46993
+ noteUnseenBeforeAnUnfinishedScan(projectPath, classes) {
46994
+ const note = readClassNote(projectPath);
46995
+ const held = note?.unseenBeforeAnUnfinishedScan;
46996
+ if (classes.length === 0 && held === undefined) {
46997
+ return;
46998
+ }
46999
+ const { unseenBeforeAnUnfinishedScan: _replaced, ...rest } = note ?? { rebuiltWith: [] };
47000
+ writeClassNote(projectPath, {
47001
+ ...rest,
47002
+ ...classes.length === 0 ? {} : {
47003
+ unseenBeforeAnUnfinishedScan: {
47004
+ editorPid: this.godotBridge.getStatus().editorPid ?? null,
47005
+ classes: [...classes].sort()
47006
+ }
47007
+ }
47008
+ });
47009
+ }
46850
47010
  noteAShortListEditor(projectPath) {
46851
47011
  const note = readClassNote(projectPath);
46852
47012
  writeClassNote(projectPath, {
47013
+ ...note,
46853
47014
  rebuiltWith: note?.rebuiltWith ?? [],
46854
47015
  shortListEditorPid: this.godotBridge.getStatus().editorPid ?? null
46855
47016
  });
@@ -46874,7 +47035,17 @@ class GodotServer {
46874
47035
  async handleLSP(toolName, args) {
46875
47036
  return handleLSPTool(await this.lsp(), toolName, args);
46876
47037
  }
47038
+ async waitForTheEditorOpenedHere(args, instead) {
47039
+ if (this.godotBridge.isConnected() || this.launchedEditor === null || !await this.launchedEditorIsUp()) {
47040
+ return null;
47041
+ }
47042
+ return await this.noEditorYet(args, instead);
47043
+ }
46877
47044
  async askScriptInfo(toolName, asked, args) {
47045
+ const waited = await this.waitForTheEditorOpenedHere(args, []);
47046
+ if (waited !== null) {
47047
+ return waited;
47048
+ }
46878
47049
  const answer = await this.handleLSP(toolName, args);
46879
47050
  const refused = this.languageServerRefusal(asParams(JSON.parse(answer.content[0]?.text ?? "{}")), asked);
46880
47051
  return refused === null ? answer : refused.refusal;
@@ -47600,6 +47771,17 @@ class GodotServer {
47600
47771
  if (sceneToRun && !sceneToRun.ok) {
47601
47772
  return this.createErrorResponse(sceneToRun.reason, PATH_SOLUTIONS);
47602
47773
  }
47774
+ const build = readNonEmptyString(args, "executable");
47775
+ const exported = build === undefined ? null : exportedBuild(project.value.path, build);
47776
+ if (exported !== null && !exported.ok) {
47777
+ return this.createErrorResponse(exported.reason, PATH_SOLUTIONS);
47778
+ }
47779
+ if (exported !== null && scene !== undefined) {
47780
+ return this.createErrorResponse("An exported build runs the main scene its pack holds, and takes no scene: leave scene out, or run the project without executable.");
47781
+ }
47782
+ if (exported !== null && readBoolean(args, "profile") === true) {
47783
+ return this.createErrorResponse("A profile is read through the debugger, and an exported build is started without one, since a release build quits when given --remote-debug. Profile the project without executable.");
47784
+ }
47603
47785
  const given = this.gameArguments(args);
47604
47786
  if (!given.ok) {
47605
47787
  return given.response;
@@ -47616,13 +47798,13 @@ class GodotServer {
47616
47798
  if (!asked.ok) {
47617
47799
  return asked.response;
47618
47800
  }
47619
- if (!sceneToRun && !hasMainScene(project.value.file)) {
47801
+ if (exported === null && !sceneToRun && !hasMainScene(project.value.file)) {
47620
47802
  return this.createErrorResponse("The project sets no main scene, so there is nothing to run.", [
47621
47803
  "Pass scene to run one scene",
47622
47804
  "Choose the main scene with project_settings set_main_scene"
47623
47805
  ]);
47624
47806
  }
47625
- const engine = await this.engine();
47807
+ const engine = exported?.ok === true ? { ok: true, value: exported.absolutePath } : await this.engine();
47626
47808
  if (!engine.ok) {
47627
47809
  return engine.response;
47628
47810
  }
@@ -47660,11 +47842,11 @@ class GodotServer {
47660
47842
  const editorWouldPlay = !headless || editorPlaysHeadless(project.value.file);
47661
47843
  const silent = resolveSilent(args["silent"], process.env) && !headless;
47662
47844
  const profiled = readBoolean(args, "profile") === true;
47663
- if (this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0 && flags.value.length === 0 && !needsItsOwnProcess(asked.value) && !silent) {
47845
+ if (exported === null && this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0 && flags.value.length === 0 && !needsItsOwnProcess(asked.value) && !silent) {
47664
47846
  return await this.playThroughEditor(sceneArgument, refreshed.value, scanned, project.value.path, alreadyPlaying, runtimeWaitMs, ended, profiled);
47665
47847
  }
47666
47848
  const cmdArgs = runArguments({
47667
- projectPath: project.value.path,
47849
+ projectPath: exported === null ? project.value.path : null,
47668
47850
  headless,
47669
47851
  scene: sceneArgument,
47670
47852
  userArgs: given.value,
@@ -47672,7 +47854,7 @@ class GodotServer {
47672
47854
  silent
47673
47855
  });
47674
47856
  this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
47675
- const started = await this.launchKeptGame(engine.value, cmdArgs, project.value.path, needsItsOwnProcess(asked.value) ? environmentFor({ ...asked.value, runtimeDirectory: runtimeDirectory() }) : undefined, headless || args["visible"] === true ? undefined : HIDDEN_DESKTOP, profiled);
47857
+ const started = await this.launchKeptGame(engine.value, cmdArgs, project.value.path, needsItsOwnProcess(asked.value) ? environmentFor({ ...asked.value, runtimeDirectory: runtimeDirectory() }) : undefined, headless || args["visible"] === true ? undefined : HIDDEN_DESKTOP, profiled, exported === null);
47676
47858
  if ("error" in started) {
47677
47859
  return this.createErrorResponse(`The game could not be started: ${started.error}`, [
47678
47860
  "editor_status says which Godot this server runs and whether it answers"
@@ -47681,7 +47863,7 @@ class GodotServer {
47681
47863
  started.announcedBefore = alreadyPlaying;
47682
47864
  started.bootScene = runtimeWaitMs.scene;
47683
47865
  this.activeProcess = started;
47684
- const spawnedInstead = this.godotBridge.isConnected() && editorWouldPlay && (given.value.length > 0 || flags.value.length > 0) ? ` The editor cannot be handed arguments for a game it plays, so this one was started here.${OWN_DEBUGGER}` : this.godotBridge.isConnected() && editorWouldPlay && needsItsOwnProcess(asked.value) ? ` The editor plays a game with its own environment, so a run given one was started here.${OWN_DEBUGGER}` : this.godotBridge.isConnected() && editorWouldPlay && silent ? " The editor cannot play a game silent, which takes an engine flag, so this one was " + `started here${args["silent"] === true ? "" : ", silent because GDHARNESS_SILENT says so"}, and silent: false plays it through the editor.${OWN_DEBUGGER}` : "";
47866
+ const spawnedInstead = exported !== null ? " An exported build is started without a debugger, since a release build quits when given one, so breakpoints and debug_state do not reach it." : this.godotBridge.isConnected() && editorWouldPlay && (given.value.length > 0 || flags.value.length > 0) ? ` The editor cannot be handed arguments for a game it plays, so this one was started here.${OWN_DEBUGGER}` : this.godotBridge.isConnected() && editorWouldPlay && needsItsOwnProcess(asked.value) ? ` The editor plays a game with its own environment, so a run given one was started here.${OWN_DEBUGGER}` : this.godotBridge.isConnected() && editorWouldPlay && silent ? " The editor cannot play a game silent, which takes an engine flag, so this one was " + `started here${args["silent"] === true ? "" : ", silent because GDHARNESS_SILENT says so"}, and silent: false plays it through the editor.${OWN_DEBUGGER}` : "";
47685
47867
  const endedForThis = endedToStartThis(ended);
47686
47868
  const stranded = wereHereFirst.filter((one) => one.pid !== ended?.pid && alive(one.pid));
47687
47869
  const handed = this.dapHoldingBreakpointsOf(project.value.path).breakpointsHeld();
@@ -47691,6 +47873,7 @@ class GodotServer {
47691
47873
  through: "gdharness",
47692
47874
  alsoRunning: stranded.length === 0 ? undefined : stranded.map((one) => one.pid),
47693
47875
  pid: started.pid,
47876
+ executable: exported?.ok === true ? exported.relativePath : undefined,
47694
47877
  arguments: cmdArgs,
47695
47878
  argsNote: engineFlagsNote(given.value),
47696
47879
  transcript: started.transcript,
@@ -47702,7 +47885,7 @@ class GodotServer {
47702
47885
  endedPreviousRun: endedPreviousRun(ended),
47703
47886
  previousRunLeft: previousRunLeft(ended),
47704
47887
  ...previousRunHow(ended),
47705
- runtime: await this.runtimeUp(project.value.path, alreadyPlaying, runtimeWaitMs, given.value.length > 0),
47888
+ runtime: exported !== null ? undefined : await this.runtimeUp(project.value.path, alreadyPlaying, runtimeWaitMs, given.value.length > 0),
47706
47889
  message: `Use editor_output for what it prints and editor_run stop to end it.${started.keeper !== undefined && handed.length > 0 ? " It stops at the lines under breakpoints, and debug_state stack reads where it is held." : ""}${spawnedInstead}${endedForThis}${alsoRunning}`
47707
47890
  });
47708
47891
  }
@@ -47793,7 +47976,7 @@ class GodotServer {
47793
47976
  }
47794
47977
  let announcedAt;
47795
47978
  try {
47796
- announcedAt = statSync10(endpoint.file).mtimeMs;
47979
+ announcedAt = statSync11(endpoint.file).mtimeMs;
47797
47980
  } catch {
47798
47981
  return;
47799
47982
  }
@@ -48142,7 +48325,10 @@ class GodotServer {
48142
48325
  return gone ? "gone" : "lingering";
48143
48326
  }
48144
48327
  spawnGame(godotPath, cmdArgs, env) {
48145
- const child = spawn4(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
48328
+ const child = spawnHidden(godotPath, cmdArgs, {
48329
+ stdio: ["ignore", "pipe", "pipe"],
48330
+ ...env ? { env } : {}
48331
+ });
48146
48332
  const cancelled = callSignal();
48147
48333
  if (cancelled !== undefined) {
48148
48334
  const end = () => {
@@ -48215,7 +48401,7 @@ class GodotServer {
48215
48401
  });
48216
48402
  return Object.assign(started, { settled });
48217
48403
  }
48218
- async launchKeptGame(godotPath, cmdArgs, projectPath, env, desktop, profiled = false) {
48404
+ async launchKeptGame(godotPath, cmdArgs, projectPath, env, desktop, profiled = false, debugged = true) {
48219
48405
  const startedAt = Date.now();
48220
48406
  const transcript = openTranscript(startedAt);
48221
48407
  closeSync2(transcript.fd);
@@ -48231,7 +48417,7 @@ class GodotServer {
48231
48417
  projectPath,
48232
48418
  ...this.servedBy(),
48233
48419
  ...profile === undefined ? {} : { profile },
48234
- breakpoints: this.breakpointsForAGame(projectPath)
48420
+ ...debugged ? { breakpoints: this.breakpointsForAGame(projectPath) } : { debugged: false }
48235
48421
  },
48236
48422
  ...desktop === undefined ? {} : { desktop }
48237
48423
  });
@@ -49195,8 +49381,8 @@ class GodotServer {
49195
49381
  try {
49196
49382
  const status = asParams(await this.godotBridge.invokeTool("scan_status", {}));
49197
49383
  return status["scanning"] === true || status["importing"] === true;
49198
- } catch {
49199
- return false;
49384
+ } catch (error) {
49385
+ return error instanceof BridgeTimeout;
49200
49386
  }
49201
49387
  }
49202
49388
  async handleRescanFilesystem(args) {
@@ -49207,7 +49393,7 @@ class GodotServer {
49207
49393
  let writtenBefore = null;
49208
49394
  let blind = { unseen: [] };
49209
49395
  let waitedForEditorMs = 0;
49210
- let first;
49396
+ let startUnanswered = false;
49211
49397
  let completedBefore;
49212
49398
  let uidsReread = [];
49213
49399
  let uidsDuplicatedOnDisk = [];
@@ -49224,7 +49410,11 @@ class GodotServer {
49224
49410
  before = projectPath === "" ? null : cachedClasses(projectPath);
49225
49411
  writtenBefore = projectPath === "" ? null : cacheWrittenAt(projectPath);
49226
49412
  blind = await this.classesTheEditorCannotSee(args);
49227
- first = await this.handleViaBridge("rescan_filesystem", args);
49413
+ const first = await this.askViaBridge("rescan_filesystem", args);
49414
+ if (first instanceof BridgeTimeout) {
49415
+ startUnanswered = true;
49416
+ break;
49417
+ }
49228
49418
  if (first.isError) {
49229
49419
  return first;
49230
49420
  }
@@ -49247,7 +49437,17 @@ class GodotServer {
49247
49437
  let idleSince = null;
49248
49438
  while (Date.now() - started < timeoutMs) {
49249
49439
  await new Promise((settle) => setTimeout(settle, 100));
49250
- const status = asParams(await this.godotBridge.invokeTool("rescan_filesystem", { ...args, statusOnly: true }));
49440
+ let status;
49441
+ try {
49442
+ status = asParams(await this.godotBridge.invokeTool("rescan_filesystem", { ...args, statusOnly: true }));
49443
+ } catch (error) {
49444
+ if (!(error instanceof BridgeTimeout)) {
49445
+ throw error;
49446
+ }
49447
+ busy = true;
49448
+ idleSince = null;
49449
+ continue;
49450
+ }
49251
49451
  const pending = Boolean(status["pending"]) && Date.now() - started < SCAN_START_MS;
49252
49452
  const completed = readNumber(status, "scansCompleted");
49253
49453
  const notYetOver = completedBefore !== undefined && completed !== undefined && completed <= completedBefore;
@@ -49330,12 +49530,20 @@ class GodotServer {
49330
49530
  }
49331
49531
  const checked = busy ? { unseen: [] } : await this.classesTheEditorCannotSee(args);
49332
49532
  const unseen = checked.unseen;
49333
- const broughtIn = busy ? [] : blind.unseen.map((one) => one.className).filter((name) => !unseen.some((still) => still.className === name));
49533
+ const carried = projectPath === "" ? [] : this.unseenBeforeAnUnfinishedScan(projectPath);
49534
+ const unseenBefore = [...new Set([...blind.unseen.map((one) => one.className), ...carried])];
49535
+ const broughtIn = busy ? [] : unseenBefore.filter((name) => !unseen.some((still) => still.className === name));
49536
+ if (projectPath !== "") {
49537
+ this.noteUnseenBeforeAnUnfinishedScan(projectPath, busy ? unseenBefore : []);
49538
+ }
49334
49539
  const dependents = busy || projectPath === "" || broughtIn.length === 0 ? null : await this.reloadDependents(projectPath, broughtIn);
49335
49540
  const stillGone = lost.filter((name) => !restored.includes(name));
49336
49541
  const notes = [];
49337
49542
  if (busy) {
49338
- notes.push("The editor was still scanning or importing when the wait ran out, so new files may not be visible yet.");
49543
+ notes.push(startUnanswered ? "The editor took the rescan in and did not answer it before the wait ran out, which is an editor importing: a hidden editor imports on the thread that answers, and answers nothing until it is done. It carries the scan out once it is free, so new files may not be visible yet." : "The editor was still scanning or importing when the wait ran out, so new files may not be visible yet.");
49544
+ if (unseenBefore.length > 0) {
49545
+ notes.push(`Scripts naming ${unseenBefore.join(", ")}, which the editor could not see before this scan, were compiled without them and stay that way until reloaded. editor_rescan again once the editor is idle reloads them, for any server on this project, until this editor is restarted.`);
49546
+ }
49339
49547
  } else if (stillGone.length > 0) {
49340
49548
  notes.push("The scan wrote the class cache from the list this editor is holding, that list is shorter than the file was, and rebuilding the cache from the files did not bring these back. Every engine that reads the cache next, including a test run, will report them as unknown identifiers. Restart the editor with editor_launch restart before scanning again.");
49341
49549
  } else if (lost.length > 0) {
@@ -49453,8 +49661,9 @@ class GodotServer {
49453
49661
  const droppedByTheEditor = lost.filter((name) => !cameBack.includes(name));
49454
49662
  const seenLosing = droppedByTheEditor.length > 0 && this.godotBridge.isConnected();
49455
49663
  writeClassNote(projectPath, {
49664
+ ...noted,
49456
49665
  rebuiltWith: [...after.keys()].sort(),
49457
- ...seenLosing ? { shortListEditorPid: this.godotBridge.getStatus().editorPid ?? null } : noted?.shortListEditorPid === undefined ? {} : { shortListEditorPid: noted.shortListEditorPid }
49666
+ ...seenLosing ? { shortListEditorPid: this.godotBridge.getStatus().editorPid ?? null } : {}
49458
49667
  });
49459
49668
  const notes = [];
49460
49669
  if (seenLosing) {
@@ -49528,6 +49737,10 @@ class GodotServer {
49528
49737
  if (moveTo !== null && !moveTo.ok) {
49529
49738
  return this.createErrorResponse(moveTo.reason);
49530
49739
  }
49740
+ const waited = await this.waitForTheEditorOpenedHere(args, []);
49741
+ if (waited !== null) {
49742
+ return waited;
49743
+ }
49531
49744
  const languageServer = {
49532
49745
  problem: null,
49533
49746
  initialized: false
@@ -49770,6 +49983,16 @@ class GodotServer {
49770
49983
  ]);
49771
49984
  }
49772
49985
  async handleViaBridge(toolName, args, askedAs) {
49986
+ const answered = await this.askViaBridge(toolName, args, askedAs);
49987
+ if (!(answered instanceof BridgeTimeout)) {
49988
+ return answered;
49989
+ }
49990
+ return this.createErrorResponse(`The editor took ${toolName} in and did not answer within ${String(answered.waitedMs / 1000)}s. An editor importing files answers nothing until the import is over, and a hidden one imports on the thread that answers.`, [
49991
+ "editor_status says whether it is scanning or importing",
49992
+ "The editor still carries the request out once it is free, so read what it changed before asking again"
49993
+ ]);
49994
+ }
49995
+ async askViaBridge(toolName, args, askedAs) {
49773
49996
  if (!this.godotBridge.isConnected()) {
49774
49997
  const alternative = askedAs !== undefined && Object.hasOwn(EDITOR_FREE_INSTEAD, askedAs) ? EDITOR_FREE_INSTEAD[askedAs] : undefined;
49775
49998
  const refused = await this.noEditorYet(args, alternative === undefined ? [] : [alternative]);
@@ -49788,6 +50011,9 @@ class GodotServer {
49788
50011
  try {
49789
50012
  return this.jsonTextResponse(markIfStale(await this.godotBridge.invokeTool(toolName, args), this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest(), this.editorCodeOnDisk()));
49790
50013
  } catch (error) {
50014
+ if (error instanceof BridgeTimeout) {
50015
+ return error;
50016
+ }
49791
50017
  const message = errorMessage(error);
49792
50018
  if (message.includes(`Unknown tool: ${toolName}`)) {
49793
50019
  const status = this.godotBridge.getStatus();
@@ -49857,7 +50083,7 @@ class GodotServer {
49857
50083
  return !run.announcedBefore.has(one.pid);
49858
50084
  }
49859
50085
  try {
49860
- return statSync10(one.file).mtimeMs >= run.startedAt;
50086
+ return statSync11(one.file).mtimeMs >= run.startedAt;
49861
50087
  } catch {
49862
50088
  return false;
49863
50089
  }
@@ -50102,7 +50328,7 @@ async function runGodotServer() {
50102
50328
  const server = new GodotServer;
50103
50329
  await server.run();
50104
50330
  }
50105
- var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, PARENT_ASKED_AFTER_MS = 5000, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, ALONGSIDE_NOTE = "Another call on this game was running while this one was: calls sent together run together and are answered as each finishes, so this answer is not from after the other. Send a call meant to come after another once that one has answered.", OWN_DEBUGGER = " Its debugger is the keeper holding it rather than the editor, so the debug_* tools answer for it all the same: breakpoints stop it, and debug_control pause breaks into it wherever it is, which the editor offers no way to do.", PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, KEEPER_DEBUG_MS, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, EDITOR_FREE_INSTEAD, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
50331
+ var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, __dirname2, PARENT_ASKED_AFTER_MS = 5000, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, ALONGSIDE_NOTE = "Another call on this game was running while this one was: calls sent together run together and are answered as each finishes, so this answer is not from after the other. Send a call meant to come after another once that one has answered.", OWN_DEBUGGER = " Its debugger is the keeper holding it rather than the editor, so the debug_* tools answer for it all the same: breakpoints stop it, and debug_control pause breaks into it wherever it is, which the editor offers no way to do.", PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, KEEPER_DEBUG_MS, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, EDITOR_FREE_INSTEAD, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
50106
50332
  var init_server = __esm(() => {
50107
50333
  init_dist();
50108
50334
  init_stdio();
@@ -50155,10 +50381,10 @@ var init_server = __esm(() => {
50155
50381
  init_server_instructions();
50156
50382
  init_server_version();
50157
50383
  init_setup();
50384
+ init_spawn();
50158
50385
  init_tool_definitions();
50159
50386
  init_tool_reference();
50160
50387
  init_update_check();
50161
- run4 = promisify5(execFile5);
50162
50388
  __dirname2 = dirname21(fileURLToPath5(import.meta.url));
50163
50389
  KEEPER_DEBUG_MS = PAUSE_MS + STEP_MS + 2 * READ_MS;
50164
50390
  CONSOLE_WRAPPER = /_console(\.exe)?$/i;
@@ -50222,7 +50448,7 @@ import { existsSync as existsSync22 } from "node:fs";
50222
50448
  import { join as join29, resolve as resolve8 } from "node:path";
50223
50449
 
50224
50450
  // src/harnesses.ts
50225
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
50451
+ 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";
50226
50452
  import { homedir as homedir2 } from "node:os";
50227
50453
  import { basename as basename2, dirname as dirname3, join as join4, relative as relative2, resolve as resolve2 } from "node:path";
50228
50454
  import process4 from "node:process";
@@ -50950,7 +51176,7 @@ function mainCheckoutOf(projectPath) {
50950
51176
  top = up;
50951
51177
  }
50952
51178
  const marker = join4(top, ".git");
50953
- if (!statSync2(marker).isFile()) {
51179
+ if (!statSync3(marker).isFile()) {
50954
51180
  return null;
50955
51181
  }
50956
51182
  const pointer = /^gitdir:\s*(.+)$/m.exec(readFileSync2(marker, "utf8"))?.[1]?.trim();
@@ -50979,7 +51205,7 @@ function worktreeCheckoutsOf(projectPath) {
50979
51205
  top = up;
50980
51206
  }
50981
51207
  const listed = join4(top, ".git", "worktrees");
50982
- if (!statSync2(join4(top, ".git")).isDirectory() || !existsSync4(listed)) {
51208
+ if (!statSync3(join4(top, ".git")).isDirectory() || !existsSync4(listed)) {
50983
51209
  return [];
50984
51210
  }
50985
51211
  const below = relative2(top, resolve2(projectPath));