gdharness 1.2.27 → 1.2.29

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,26 +8251,109 @@ 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
 
8257
+ // src/library-copies.ts
8258
+ import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync3, renameSync, unlinkSync } from "node:fs";
8259
+ import { basename as basename3, dirname as dirname4, isAbsolute as isAbsolute2, join as join5 } from "node:path";
8260
+ function freeLibraryCopies(projectPath, platform = process.platform) {
8261
+ const setAside = [];
8262
+ const swept = [];
8263
+ if (platform !== "win32") {
8264
+ return { setAside, swept };
8265
+ }
8266
+ for (const library of windowsLibraries(projectPath)) {
8267
+ const folder = dirname4(library);
8268
+ const copy = `~${basename3(library)}`;
8269
+ if (existsSync5(join5(folder, copy))) {
8270
+ const aside = join5(folder, `${copy}.aside-${process.pid}-${Date.now()}`);
8271
+ try {
8272
+ renameSync(join5(folder, copy), aside);
8273
+ setAside.push(aside);
8274
+ } catch {}
8275
+ }
8276
+ let names;
8277
+ try {
8278
+ names = readdirSync3(folder);
8279
+ } catch {
8280
+ continue;
8281
+ }
8282
+ for (const name of names) {
8283
+ if (name.length > copy.length && name.toLowerCase().startsWith(copy.toLowerCase())) {
8284
+ try {
8285
+ unlinkSync(join5(folder, name));
8286
+ swept.push(join5(folder, name));
8287
+ } catch {}
8288
+ }
8289
+ }
8290
+ }
8291
+ return { setAside, swept };
8292
+ }
8293
+ function windowsLibraries(projectPath) {
8294
+ const listed = readOrNull(join5(projectPath, ".godot", "extension_list.cfg"));
8295
+ if (listed === null) {
8296
+ return [];
8297
+ }
8298
+ const libraries = new Set;
8299
+ for (const line of listed.split(/\r?\n/)) {
8300
+ const manifest = resolveResource(projectPath, projectPath, line.trim());
8301
+ const text = manifest === null ? null : readOrNull(manifest);
8302
+ if (manifest === null || text === null) {
8303
+ continue;
8304
+ }
8305
+ let inLibraries = false;
8306
+ for (const row of text.split(/\r?\n/)) {
8307
+ const section = /^\s*\[([^\]]+)\]/.exec(row)?.[1];
8308
+ if (section !== undefined) {
8309
+ inLibraries = section.trim() === "libraries";
8310
+ continue;
8311
+ }
8312
+ const named = inLibraries ? /^\s*windows[\w.]*\s*=\s*"([^"]+)"/.exec(row)?.[1] : undefined;
8313
+ const library = named === undefined ? null : resolveResource(projectPath, dirname4(manifest), named);
8314
+ if (library !== null) {
8315
+ libraries.add(library);
8316
+ }
8317
+ }
8318
+ }
8319
+ return [...libraries];
8320
+ }
8321
+ function resolveResource(projectPath, relativeTo, path) {
8322
+ if (path === "") {
8323
+ return null;
8324
+ }
8325
+ if (path.startsWith("res://")) {
8326
+ return join5(projectPath, path.slice("res://".length));
8327
+ }
8328
+ return isAbsolute2(path) ? path : join5(relativeTo, path);
8329
+ }
8330
+ function readOrNull(path) {
8331
+ try {
8332
+ return readFileSync3(path, "utf8");
8333
+ } catch {
8334
+ return null;
8335
+ }
8336
+ }
8337
+ var init_library_copies = () => {};
8338
+
8201
8339
  // src/reimport.ts
8202
8340
  import { createHash } from "node:crypto";
8203
- import { existsSync as existsSync5, readFileSync as readFileSync3, rmSync as rmSync3, statSync as statSync3, utimesSync, writeFileSync as writeFileSync3 } from "node:fs";
8204
- import { basename as basename3, join as join5 } from "node:path";
8341
+ import { existsSync as existsSync6, readFileSync as readFileSync4, rmSync as rmSync3, statSync as statSync4, utimesSync, writeFileSync as writeFileSync3 } from "node:fs";
8342
+ import { basename as basename4, join as join6 } from "node:path";
8205
8343
  function forceNextImport(projectPath, resourcePath) {
8206
8344
  const relative = resourcePath.replace(/^res:\/\//, "");
8207
- const sidecar = join5(projectPath, `${relative}.import`);
8208
- if (!existsSync5(sidecar)) {
8345
+ const sidecar = join6(projectPath, `${relative}.import`);
8346
+ if (!existsSync6(sidecar)) {
8209
8347
  return;
8210
8348
  }
8211
- const { atime, mtimeMs } = statSync3(sidecar);
8212
- writeFileSync3(sidecar, readFileSync3(sidecar, "utf8").replace(/^valid=false\r?\n/m, ""));
8349
+ const { atime, mtimeMs } = statSync4(sidecar);
8350
+ writeFileSync3(sidecar, readFileSync4(sidecar, "utf8").replace(/^valid=false\r?\n/m, ""));
8213
8351
  utimesSync(sidecar, atime, new Date(mtimeMs + 2000));
8214
8352
  rmSync3(importRecord(projectPath, resourcePath), { force: true });
8215
8353
  }
8216
8354
  function importRecord(projectPath, resourcePath) {
8217
8355
  const digest = createHash("md5").update(resourcePath).digest("hex");
8218
- return join5(projectPath, ".godot", "imported", `${basename3(resourcePath)}-${digest}.md5`);
8356
+ return join6(projectPath, ".godot", "imported", `${basename4(resourcePath)}-${digest}.md5`);
8219
8357
  }
8220
8358
  var init_reimport = () => {};
8221
8359
 
@@ -8235,17 +8373,17 @@ function alive(pid) {
8235
8373
  var init_alive = () => {};
8236
8374
 
8237
8375
  // src/scratch.ts
8238
- import { existsSync as existsSync6, mkdtempSync, readdirSync as readdirSync3, rmSync as rmSync4 } from "node:fs";
8376
+ import { existsSync as existsSync7, mkdtempSync, readdirSync as readdirSync4, rmSync as rmSync4 } from "node:fs";
8239
8377
  import { tmpdir as tmpdir2 } from "node:os";
8240
- import { join as join6 } from "node:path";
8378
+ import { join as join7 } from "node:path";
8241
8379
  import process6 from "node:process";
8242
8380
  function scratchDirectory(kind) {
8243
- return mkdtempSync(join6(tmpdir2(), `gdharness-${kind}-${process6.pid}-`));
8381
+ return mkdtempSync(join7(tmpdir2(), `gdharness-${kind}-${process6.pid}-`));
8244
8382
  }
8245
8383
  function sweepAbandonedScratch(directory = tmpdir2()) {
8246
8384
  let entries;
8247
8385
  try {
8248
- entries = readdirSync3(directory);
8386
+ entries = readdirSync4(directory);
8249
8387
  } catch {
8250
8388
  return;
8251
8389
  }
@@ -8256,7 +8394,7 @@ function sweepAbandonedScratch(directory = tmpdir2()) {
8256
8394
  continue;
8257
8395
  }
8258
8396
  try {
8259
- remove(join6(directory, entry));
8397
+ remove(join7(directory, entry));
8260
8398
  } catch {}
8261
8399
  }
8262
8400
  }
@@ -8294,7 +8432,7 @@ function tryAgain(path, attempt) {
8294
8432
  lastTryOnExit();
8295
8433
  }
8296
8434
  const after = AGAIN_AFTER_MS[attempt];
8297
- if (after === undefined || !existsSync6(path)) {
8435
+ if (after === undefined || !existsSync7(path)) {
8298
8436
  waiting.delete(path);
8299
8437
  if (after === undefined) {
8300
8438
  atExit.add(path);
@@ -8341,8 +8479,8 @@ var init_scratch = __esm(() => {
8341
8479
  });
8342
8480
 
8343
8481
  // src/headless.ts
8344
- import { existsSync as existsSync7, readFileSync as readFileSync4, realpathSync as realpathSync2, writeFileSync as writeFileSync4 } from "node:fs";
8345
- import { dirname as dirname4, join as join7 } from "node:path";
8482
+ import { existsSync as existsSync8, readFileSync as readFileSync5, realpathSync as realpathSync2, writeFileSync as writeFileSync4 } from "node:fs";
8483
+ import { dirname as dirname5, join as join8 } from "node:path";
8346
8484
  function snakeCased(params) {
8347
8485
  const result = emptyRecord();
8348
8486
  for (const [key, value] of Object.entries(params)) {
@@ -8397,7 +8535,7 @@ function refusalsIn(log) {
8397
8535
  }
8398
8536
  function scriptFault(problems, script) {
8399
8537
  const spelled = (path) => `(${path.replaceAll("\\", "/").toLowerCase()}/`;
8400
- const directories = [...new Set([spelled(dirname4(script)), spelled(resolvedOr(dirname4(script)))])];
8538
+ const directories = [...new Set([spelled(dirname5(script)), spelled(resolvedOr(dirname5(script)))])];
8401
8539
  return problems.find((entry) => {
8402
8540
  const at = entry.detail.find((line) => line.startsWith("at: "))?.replaceAll("\\", "/").toLowerCase();
8403
8541
  return entry.severity === "error" && at !== undefined && directories.some((directory) => at.includes(directory));
@@ -8422,7 +8560,7 @@ function librariesNotCopied(messages) {
8422
8560
  }
8423
8561
  function extensionRetryNote(libraries, stillNotLoaded = []) {
8424
8562
  const second = stillNotLoaded.length === 0 ? "so the import was run again and loaded the extension, and the messages are from that second run." : `so the import was run again, and it still could not load ${stillNotLoaded.join(" and ")}: this answer is from an import without ${stillNotLoaded.length === 1 ? "it" : "them"}, and its messages say what the engine hit.`;
8425
- return `The first import could not load ${libraries.join(" and ")}: on Windows an engine running as the editor ` + "loads a GDExtension from a ~ copy beside the library, and the editor open on this project holds its own " + "copy there, so the first engine started beside it cannot make one. That failed attempt moves the " + `editor's copy aside, ${second} The editor's copy is left beside the library as a ~RF….TMP file, which ` + "can be deleted once the editor has exited.";
8563
+ return `The first import could not load ${libraries.join(" and ")}: on Windows an engine running as the editor ` + "loads a GDExtension from a ~ copy beside the library, and the editor open on this project holds its own " + "copy there, so the first engine started beside it cannot make one. That failed attempt moves the " + `editor's copy aside, ${second} The editor's copy is left beside the library as a ~RF….TMP file, which ` + "the next import, export or editor this server starts on the project deletes once the editor has exited.";
8426
8564
  }
8427
8565
  async function runImport(godotPath, projectPath, options = {}) {
8428
8566
  const once = options.once ?? importOnce;
@@ -8430,6 +8568,7 @@ async function runImport(godotPath, projectPath, options = {}) {
8430
8568
  for (const target of options.forcing ?? []) {
8431
8569
  forceNextImport(projectPath, target);
8432
8570
  }
8571
+ freeLibraryCopies(projectPath);
8433
8572
  return once(godotPath, projectPath);
8434
8573
  };
8435
8574
  const first = await pass();
@@ -8453,7 +8592,7 @@ async function importOnce(godotPath, projectPath) {
8453
8592
  ran = await runEngine(godotPath, [
8454
8593
  "--headless",
8455
8594
  "--log-file",
8456
- join7(logDir, "engine.log"),
8595
+ join8(logDir, "engine.log"),
8457
8596
  "--path",
8458
8597
  projectPath,
8459
8598
  "--import"
@@ -8473,13 +8612,13 @@ async function importOnce(godotPath, projectPath) {
8473
8612
  }
8474
8613
  async function runOperation(engine, operation, params, projectPath) {
8475
8614
  const paramsDir = scratchDirectory("params");
8476
- const paramsFile = join7(paramsDir, `${operation}.json`);
8477
- const answerFile = join7(paramsDir, "answer.json");
8615
+ const paramsFile = join8(paramsDir, `${operation}.json`);
8616
+ const answerFile = join8(paramsDir, "answer.json");
8478
8617
  writeFileSync4(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
8479
8618
  const args = [
8480
8619
  "--headless",
8481
8620
  "--log-file",
8482
- join7(paramsDir, "engine.log"),
8621
+ join8(paramsDir, "engine.log"),
8483
8622
  "--path",
8484
8623
  projectPath,
8485
8624
  "--script",
@@ -8549,10 +8688,10 @@ async function runOperation(engine, operation, params, projectPath) {
8549
8688
  };
8550
8689
  }
8551
8690
  function answerIn(file) {
8552
- if (!existsSync7(file)) {
8691
+ if (!existsSync8(file)) {
8553
8692
  return { kind: "none" };
8554
8693
  }
8555
- const text = readFileSync4(file, "utf8");
8694
+ const text = readFileSync5(file, "utf8");
8556
8695
  try {
8557
8696
  const parsed = JSON.parse(text);
8558
8697
  return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? { kind: "answer", payload: parsed } : { kind: "unreadable", why: `it is JSON but not an object: ${text.slice(0, 200)}` };
@@ -8566,14 +8705,15 @@ function answerIn(file) {
8566
8705
  var AFTER_ANSWER_NOTE = "Printed after this operation had written its answer, while the engine ran the project's autoloads and shut down: Godot starts a project's autoloads in every script run and cannot be told not to. An error raised in a project script here is that autoload's own, and none of these changes the answer.", OWN_ERROR = "[ERROR] ", ANSWERED = "[gdharness] answered", MESSAGES_KEPT = 200, ERRORS_NAMED = 3;
8567
8706
  var init_headless = __esm(() => {
8568
8707
  init_engine_run();
8708
+ init_library_copies();
8569
8709
  init_reimport();
8570
8710
  init_scratch();
8571
8711
  });
8572
8712
 
8573
8713
  // src/update-check.ts
8574
- import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
8714
+ import { existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
8575
8715
  import { homedir as homedir3, tmpdir as tmpdir3 } from "node:os";
8576
- import { join as join8 } from "node:path";
8716
+ import { join as join9 } from "node:path";
8577
8717
  function packageUrl(root) {
8578
8718
  return `${root.replace(/\/+$/, "")}/gdharness/latest`;
8579
8719
  }
@@ -8595,28 +8735,28 @@ function cacheDirectory(environment) {
8595
8735
  };
8596
8736
  const home = set("HOME") ?? homedir3();
8597
8737
  if (process.platform === "win32") {
8598
- return join8(set("LOCALAPPDATA") ?? home, "gdharness");
8738
+ return join9(set("LOCALAPPDATA") ?? home, "gdharness");
8599
8739
  }
8600
8740
  if (process.platform === "darwin") {
8601
- return join8(home, "Library", "Caches", "gdharness");
8741
+ return join9(home, "Library", "Caches", "gdharness");
8602
8742
  }
8603
- return join8(set("XDG_CACHE_HOME") ?? join8(home, ".cache"), "gdharness");
8743
+ return join9(set("XDG_CACHE_HOME") ?? join9(home, ".cache"), "gdharness");
8604
8744
  }
8605
8745
  function cacheFile(environment = process.env) {
8606
8746
  try {
8607
8747
  const directory = cacheDirectory(environment);
8608
8748
  mkdirSync3(directory, { recursive: true, mode: 448 });
8609
- return join8(directory, "update-check.json");
8749
+ return join9(directory, "update-check.json");
8610
8750
  } catch {
8611
- return join8(tmpdir3(), "gdharness-update-check.json");
8751
+ return join9(tmpdir3(), "gdharness-update-check.json");
8612
8752
  }
8613
8753
  }
8614
8754
  function readCache(path) {
8615
8755
  try {
8616
- if (!existsSync8(path)) {
8756
+ if (!existsSync9(path)) {
8617
8757
  return null;
8618
8758
  }
8619
- const parsed = JSON.parse(readFileSync5(path, "utf8"));
8759
+ const parsed = JSON.parse(readFileSync6(path, "utf8"));
8620
8760
  if (typeof parsed !== "object" || parsed === null) {
8621
8761
  return null;
8622
8762
  }
@@ -8764,7 +8904,7 @@ var init_update_check = __esm(() => {
8764
8904
  });
8765
8905
 
8766
8906
  // src/server-version.ts
8767
- import { readFileSync as readFileSync6 } from "node:fs";
8907
+ import { readFileSync as readFileSync7 } from "node:fs";
8768
8908
  function editorIsStale(addonVersion, serverVersion, addonDigest, shippedDigest) {
8769
8909
  if (addonDigest !== undefined && addonDigest !== "" && shippedDigest !== undefined) {
8770
8910
  return addonDigest !== shippedDigest;
@@ -8835,7 +8975,7 @@ var init_server_version = __esm(() => {
8835
8975
  GODOT_DEBUG_MODE_DEFAULT = process.env["GODOT_DEBUG"] === "true" || DEBUG_MODE;
8836
8976
  SERVER_VERSION = (() => {
8837
8977
  try {
8838
- const pkg = JSON.parse(readFileSync6(new URL("../package.json", import.meta.url), "utf8"));
8978
+ const pkg = JSON.parse(readFileSync7(new URL("../package.json", import.meta.url), "utf8"));
8839
8979
  return typeof pkg.version === "string" ? pkg.version : "0.0.0";
8840
8980
  } catch {
8841
8981
  return "0.0.0";
@@ -9620,8 +9760,8 @@ var init_gdscript_source = __esm(() => {
9620
9760
  });
9621
9761
 
9622
9762
  // src/class-cache.ts
9623
- import { existsSync as existsSync9, readdirSync as readdirSync4, readFileSync as readFileSync7, statSync as statSync4 } from "node:fs";
9624
- import { join as join9, posix } from "node:path";
9763
+ import { existsSync as existsSync10, readdirSync as readdirSync5, readFileSync as readFileSync8, statSync as statSync5 } from "node:fs";
9764
+ import { join as join10, posix } from "node:path";
9625
9765
  function isToolScript(source) {
9626
9766
  return TOOL.test(codeOf(source));
9627
9767
  }
@@ -9637,18 +9777,18 @@ function declaredClasses(projectPath) {
9637
9777
  }
9638
9778
  function eachScript(projectPath, each) {
9639
9779
  const visit = (directory, prefix) => {
9640
- if (existsSync9(join9(directory, ".gdignore"))) {
9780
+ if (existsSync10(join10(directory, ".gdignore"))) {
9641
9781
  return;
9642
9782
  }
9643
- for (const entry of readdirSync4(directory, { withFileTypes: true })) {
9783
+ for (const entry of readdirSync5(directory, { withFileTypes: true })) {
9644
9784
  if (entry.name.startsWith(".")) {
9645
9785
  continue;
9646
9786
  }
9647
- const path = join9(directory, entry.name);
9787
+ const path = join10(directory, entry.name);
9648
9788
  if (entry.isDirectory()) {
9649
9789
  visit(path, `${prefix}${entry.name}/`);
9650
9790
  } else if (entry.isFile() && entry.name.endsWith(".gd")) {
9651
- each(`res://${prefix}${entry.name}`, readFileSync7(path, "utf8"));
9791
+ each(`res://${prefix}${entry.name}`, readFileSync8(path, "utf8"));
9652
9792
  }
9653
9793
  }
9654
9794
  };
@@ -9670,12 +9810,12 @@ function scriptsNaming(projectPath, classes) {
9670
9810
  return naming.sort((a, b) => a.script.localeCompare(b.script));
9671
9811
  }
9672
9812
  function cachedClasses(projectPath) {
9673
- const cache = join9(projectPath, ".godot", "global_script_class_cache.cfg");
9674
- if (!existsSync9(cache)) {
9813
+ const cache = join10(projectPath, ".godot", "global_script_class_cache.cfg");
9814
+ if (!existsSync10(cache)) {
9675
9815
  return null;
9676
9816
  }
9677
9817
  const listed = new Map;
9678
- const text = readFileSync7(cache, "utf8");
9818
+ const text = readFileSync8(cache, "utf8");
9679
9819
  for (const entry of text.matchAll(/"class":\s*&"([^"]+)"[\s\S]*?"path":\s*"([^"]+)"/g)) {
9680
9820
  listed.set(entry[1] ?? "", entry[2] ?? "");
9681
9821
  }
@@ -9941,9 +10081,9 @@ function uncachedClassNote(types) {
9941
10081
  return `${types.join(", ")} ${one ? "is declared" : "are declared"} in this project and missing from ` + `.godot/global_script_class_cache.cfg, so a game launched now would not resolve ${one ? "it" : "them"} ` + "either. Try editor_rescan first: it asks the editor already running and starts nothing, and it " + "cleared this in 314ms in a second project with a 31-worker fan-out still importing. It is not " + "always enough, because a file another engine has already imported reads as settled and the " + "walk does not look inside it. project_import refresh_classes rewrites the cache from the " + "declarations on disk and does reach that case, at the cost of a short headless engine, which " + "is worth knowing when something else is already running against this project.";
9942
10082
  }
9943
10083
  function cacheWrittenAt(projectPath) {
9944
- const cache = join9(projectPath, ".godot", "global_script_class_cache.cfg");
10084
+ const cache = join10(projectPath, ".godot", "global_script_class_cache.cfg");
9945
10085
  try {
9946
- return statSync4(cache).mtimeMs;
10086
+ return statSync5(cache).mtimeMs;
9947
10087
  } catch {
9948
10088
  return null;
9949
10089
  }
@@ -9955,14 +10095,14 @@ function declaredSince(projectPath, paths, names, since) {
9955
10095
  return false;
9956
10096
  }
9957
10097
  try {
9958
- return statSync4(join9(projectPath, path.slice("res://".length))).mtimeMs > since;
10098
+ return statSync5(join10(projectPath, path.slice("res://".length))).mtimeMs > since;
9959
10099
  } catch {
9960
10100
  return false;
9961
10101
  }
9962
10102
  });
9963
10103
  }
9964
10104
  function cachedAtMissingPaths(cached, projectPath) {
9965
- return [...cached].filter(([, path]) => path.startsWith("res://") && !existsSync9(join9(projectPath, path.slice("res://".length)))).map(([name]) => name).sort();
10105
+ return [...cached].filter(([, path]) => path.startsWith("res://") && !existsSync10(join10(projectPath, path.slice("res://".length)))).map(([name]) => name).sort();
9966
10106
  }
9967
10107
  function staleAgainst(cached, projectPath) {
9968
10108
  return [...declaredClasses(projectPath)].filter(([name, path]) => cached.get(name) !== path).map(([name]) => name);
@@ -9987,8 +10127,8 @@ var init_class_cache = __esm(() => {
9987
10127
  });
9988
10128
 
9989
10129
  // src/project-scan.ts
9990
- import { existsSync as existsSync10, readdirSync as readdirSync5, readFileSync as readFileSync8 } from "node:fs";
9991
- import { extname, join as join10 } from "node:path";
10130
+ import { existsSync as existsSync11, readdirSync as readdirSync6, readFileSync as readFileSync9 } from "node:fs";
10131
+ import { extname, join as join11 } from "node:path";
9992
10132
  function skipped(name) {
9993
10133
  return name.startsWith(".") || SKIPPED.has(name);
9994
10134
  }
@@ -9996,10 +10136,10 @@ function steppedOver(directory) {
9996
10136
  return gdignored(directory) || installedByUs(directory);
9997
10137
  }
9998
10138
  function gdignored(directory) {
9999
- return existsSync10(join10(directory, ".gdignore"));
10139
+ return existsSync11(join11(directory, ".gdignore"));
10000
10140
  }
10001
10141
  function installedByUs(directory) {
10002
- return existsSync10(join10(directory, VERSION_MARKER));
10142
+ return existsSync11(join11(directory, VERSION_MARKER));
10003
10143
  }
10004
10144
  function projectStructure(projectPath) {
10005
10145
  const structure = { scenes: 0, scripts: 0, assets: 0, other: 0 };
@@ -10007,12 +10147,12 @@ function projectStructure(projectPath) {
10007
10147
  if (steppedOver(directory)) {
10008
10148
  return;
10009
10149
  }
10010
- for (const entry of readdirSync5(directory, { withFileTypes: true })) {
10150
+ for (const entry of readdirSync6(directory, { withFileTypes: true })) {
10011
10151
  if (skipped(entry.name)) {
10012
10152
  continue;
10013
10153
  }
10014
10154
  if (entry.isDirectory()) {
10015
- visit(join10(directory, entry.name));
10155
+ visit(join11(directory, entry.name));
10016
10156
  } else if (entry.isFile()) {
10017
10157
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
10018
10158
  if (SCENE_EXTENSIONS.has(extension)) {
@@ -10036,13 +10176,13 @@ function projectFiles(projectPath, extensions) {
10036
10176
  if (steppedOver(directory)) {
10037
10177
  return;
10038
10178
  }
10039
- for (const entry of readdirSync5(directory, { withFileTypes: true })) {
10179
+ for (const entry of readdirSync6(directory, { withFileTypes: true })) {
10040
10180
  if (skipped(entry.name)) {
10041
10181
  continue;
10042
10182
  }
10043
10183
  const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
10044
10184
  if (entry.isDirectory()) {
10045
- visit(join10(directory, entry.name), spelled);
10185
+ visit(join11(directory, entry.name), spelled);
10046
10186
  } else if (entry.isFile() && extensions.includes(extname(entry.name).toLowerCase())) {
10047
10187
  found.push(spelled);
10048
10188
  }
@@ -10057,20 +10197,20 @@ function scriptsWithoutUid(projectPath) {
10057
10197
  if (steppedOver(directory)) {
10058
10198
  return;
10059
10199
  }
10060
- for (const entry of readdirSync5(directory, { withFileTypes: true })) {
10200
+ for (const entry of readdirSync6(directory, { withFileTypes: true })) {
10061
10201
  if (skipped(entry.name)) {
10062
10202
  continue;
10063
10203
  }
10064
10204
  const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
10065
10205
  if (entry.isDirectory()) {
10066
- visit(join10(directory, entry.name), spelled);
10206
+ visit(join11(directory, entry.name), spelled);
10067
10207
  continue;
10068
10208
  }
10069
10209
  if (!entry.isFile()) {
10070
10210
  continue;
10071
10211
  }
10072
10212
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
10073
- if (CARRIES_A_UID.has(extension) && !existsSync10(join10(directory, `${entry.name}.uid`))) {
10213
+ if (CARRIES_A_UID.has(extension) && !existsSync11(join11(directory, `${entry.name}.uid`))) {
10074
10214
  found.push(spelled);
10075
10215
  }
10076
10216
  }
@@ -10087,11 +10227,11 @@ function projectTexts(projectPath) {
10087
10227
  if (steppedOver(directory)) {
10088
10228
  return;
10089
10229
  }
10090
- for (const entry of readdirSync5(directory, { withFileTypes: true })) {
10230
+ for (const entry of readdirSync6(directory, { withFileTypes: true })) {
10091
10231
  if (skipped(entry.name)) {
10092
10232
  continue;
10093
10233
  }
10094
- const absolute = join10(directory, entry.name);
10234
+ const absolute = join11(directory, entry.name);
10095
10235
  const spelled = `${prefix}${entry.name}`;
10096
10236
  if (entry.isDirectory()) {
10097
10237
  visit(absolute, `${spelled}/`);
@@ -10101,7 +10241,7 @@ function projectTexts(projectPath) {
10101
10241
  if (!entry.isFile() || BINARY_EXTENSIONS.has(extension)) {
10102
10242
  continue;
10103
10243
  }
10104
- const bytes = readFileSync8(absolute);
10244
+ const bytes = readFileSync9(absolute);
10105
10245
  if (!isBinary(bytes)) {
10106
10246
  found.push({ path: `res://${spelled}`, absolute, text: bytes.toString("utf8") });
10107
10247
  }
@@ -10117,11 +10257,11 @@ function ignoredTexts(projectPath) {
10117
10257
  return;
10118
10258
  }
10119
10259
  const under = ignored || gdignored(directory);
10120
- for (const entry of readdirSync5(directory, { withFileTypes: true })) {
10260
+ for (const entry of readdirSync6(directory, { withFileTypes: true })) {
10121
10261
  if (skipped(entry.name)) {
10122
10262
  continue;
10123
10263
  }
10124
- const absolute = join10(directory, entry.name);
10264
+ const absolute = join11(directory, entry.name);
10125
10265
  const spelled = `${prefix}${entry.name}`;
10126
10266
  if (entry.isDirectory()) {
10127
10267
  visit(absolute, `${spelled}/`, under);
@@ -10131,7 +10271,7 @@ function ignoredTexts(projectPath) {
10131
10271
  if (!under || !entry.isFile() || BINARY_EXTENSIONS.has(extension) || entry.name === ".gdignore") {
10132
10272
  continue;
10133
10273
  }
10134
- const bytes = readFileSync8(absolute);
10274
+ const bytes = readFileSync9(absolute);
10135
10275
  if (!isBinary(bytes)) {
10136
10276
  found.push({ path: `res://${spelled}`, absolute, text: bytes.toString("utf8") });
10137
10277
  }
@@ -10154,14 +10294,14 @@ function searchProject(projectPath, options) {
10154
10294
  if (steppedOver(directory)) {
10155
10295
  return;
10156
10296
  }
10157
- for (const entry of readdirSync5(directory, { withFileTypes: true })) {
10297
+ for (const entry of readdirSync6(directory, { withFileTypes: true })) {
10158
10298
  if (full()) {
10159
10299
  return;
10160
10300
  }
10161
10301
  if (skipped(entry.name)) {
10162
10302
  continue;
10163
10303
  }
10164
- const entryPath = join10(directory, entry.name);
10304
+ const entryPath = join11(directory, entry.name);
10165
10305
  if (entry.isDirectory()) {
10166
10306
  visit(entryPath);
10167
10307
  continue;
@@ -10170,7 +10310,7 @@ function searchProject(projectPath, options) {
10170
10310
  if (!entry.isFile() || extensions !== null && !extensions.has(extension)) {
10171
10311
  continue;
10172
10312
  }
10173
- const bytes = readFileSync8(entryPath);
10313
+ const bytes = readFileSync9(entryPath);
10174
10314
  if (extensions === null && isBinary(bytes)) {
10175
10315
  continue;
10176
10316
  }
@@ -10257,7 +10397,7 @@ var init_answer_json = __esm(() => {
10257
10397
  });
10258
10398
 
10259
10399
  // src/resources.ts
10260
- import { readFileSync as readFileSync9, realpathSync as realpathSync3 } from "node:fs";
10400
+ import { readFileSync as readFileSync10, realpathSync as realpathSync3 } from "node:fs";
10261
10401
  import { extname as extname2, resolve as resolve3 } from "node:path";
10262
10402
  function ensureProjectPath(getProjectPath) {
10263
10403
  const projectPath = getProjectPath();
@@ -10453,7 +10593,7 @@ function readResourceText(uri, getProjectPath) {
10453
10593
  const parsedUri = parseGodotUri(uri);
10454
10594
  if (parsedUri.kind === "project-info") {
10455
10595
  const projectFilePath = resolveProjectFile(projectPath, "project.godot");
10456
- const rawProject = readFileSync9(projectFilePath, "utf-8");
10596
+ const rawProject = readFileSync10(projectFilePath, "utf-8");
10457
10597
  const parsedProject = parseProjectGodot(rawProject);
10458
10598
  return {
10459
10599
  mimeType: "application/json",
@@ -10462,7 +10602,7 @@ function readResourceText(uri, getProjectPath) {
10462
10602
  }
10463
10603
  const filePath = resolveProjectFile(projectPath, parsedUri.resourcePath);
10464
10604
  ensureAllowedExtension(parsedUri.kind, filePath);
10465
- const text = readFileSync9(filePath, "utf-8");
10605
+ const text = readFileSync10(filePath, "utf-8");
10466
10606
  return {
10467
10607
  mimeType: parsedUri.kind === "script" ? "text/x-gdscript" : "text/plain",
10468
10608
  text
@@ -10603,20 +10743,19 @@ function readParams(params, key) {
10603
10743
  }
10604
10744
 
10605
10745
  // src/setup.ts
10606
- import { spawnSync } from "node:child_process";
10607
10746
  import { createHash as createHash3 } from "node:crypto";
10608
10747
  import {
10609
10748
  cpSync,
10610
- existsSync as existsSync11,
10749
+ existsSync as existsSync12,
10611
10750
  mkdirSync as mkdirSync4,
10612
- readdirSync as readdirSync6,
10613
- readFileSync as readFileSync10,
10751
+ readdirSync as readdirSync7,
10752
+ readFileSync as readFileSync11,
10614
10753
  rmdirSync,
10615
10754
  rmSync as rmSync5,
10616
- statSync as statSync5,
10755
+ statSync as statSync6,
10617
10756
  writeFileSync as writeFileSync6
10618
10757
  } from "node:fs";
10619
- import { dirname as dirname5, join as join11 } from "node:path";
10758
+ import { dirname as dirname6, join as join12 } from "node:path";
10620
10759
  import { fileURLToPath } from "node:url";
10621
10760
  function editorAddonDigest(from = shippedAddonsDirectory()) {
10622
10761
  return addonsDigest(EDITOR_PLUGINS, from);
@@ -10627,25 +10766,25 @@ function runtimeAddonDigest(from = shippedAddonsDirectory()) {
10627
10766
  function addonsDigest(names, from) {
10628
10767
  const hash = createHash3("sha256");
10629
10768
  const walk = (directory, relative) => {
10630
- for (const name of readdirSync6(directory).sort()) {
10769
+ for (const name of readdirSync7(directory).sort()) {
10631
10770
  if (name === VERSION_MARKER || name === DIGEST_MARKER) {
10632
10771
  continue;
10633
10772
  }
10634
- const path = join11(directory, name);
10773
+ const path = join12(directory, name);
10635
10774
  const inside = `${relative}/${name}`;
10636
- if (statSync5(path).isDirectory()) {
10775
+ if (statSync6(path).isDirectory()) {
10637
10776
  walk(path, inside);
10638
10777
  } else {
10639
10778
  hash.update(`${inside}
10640
10779
  `);
10641
- hash.update(readFileSync10(path));
10780
+ hash.update(readFileSync11(path));
10642
10781
  hash.update(`
10643
10782
  `);
10644
10783
  }
10645
10784
  }
10646
10785
  };
10647
10786
  for (const name of names) {
10648
- walk(join11(from, name), name);
10787
+ walk(join12(from, name), name);
10649
10788
  }
10650
10789
  return hash.digest("hex");
10651
10790
  }
@@ -10676,60 +10815,60 @@ function installedRuntimeDigest(projectPath) {
10676
10815
  return digestMarker(projectPath, RUNTIME_ADDON);
10677
10816
  }
10678
10817
  function digestMarker(projectPath, addon) {
10679
- const marker = join11(projectPath, "addons", addon, DIGEST_MARKER);
10680
- if (!existsSync11(marker)) {
10818
+ const marker = join12(projectPath, "addons", addon, DIGEST_MARKER);
10819
+ if (!existsSync12(marker)) {
10681
10820
  return null;
10682
10821
  }
10683
- const said = readFileSync10(marker, "utf8").trim();
10822
+ const said = readFileSync11(marker, "utf8").trim();
10684
10823
  return said === "" ? null : said;
10685
10824
  }
10686
10825
  function shippedAddonsDirectory() {
10687
- return join11(dirname5(fileURLToPath(import.meta.url)), "godot", "addons");
10826
+ return join12(dirname6(fileURLToPath(import.meta.url)), "godot", "addons");
10688
10827
  }
10689
10828
  function shippedOperationsScript() {
10690
- return join11(dirname5(fileURLToPath(import.meta.url)), "godot", "operations", "godot_operations.gd");
10829
+ return join12(dirname6(fileURLToPath(import.meta.url)), "godot", "operations", "godot_operations.gd");
10691
10830
  }
10692
10831
  function installAddons(projectPath, from = shippedAddonsDirectory()) {
10693
10832
  const installed = [];
10694
10833
  for (const name of ADDONS) {
10695
- const source = join11(from, name);
10696
- if (!existsSync11(join11(source, name === RUNTIME_ADDON ? "runtime_autoload.gd" : "plugin.cfg"))) {
10834
+ const source = join12(from, name);
10835
+ if (!existsSync12(join12(source, name === RUNTIME_ADDON ? "runtime_autoload.gd" : "plugin.cfg"))) {
10697
10836
  throw new Error(`The package holds no ${name} addon at ${source}.`);
10698
10837
  }
10699
- const target = join11(projectPath, "addons", name);
10700
- const replaced = existsSync11(target);
10838
+ const target = join12(projectPath, "addons", name);
10839
+ const replaced = existsSync12(target);
10701
10840
  rmSync5(target, { recursive: true, force: true });
10702
- mkdirSync4(dirname5(target), { recursive: true });
10841
+ mkdirSync4(dirname6(target), { recursive: true });
10703
10842
  cpSync(source, target, { recursive: true });
10704
- writeFileSync6(join11(target, VERSION_MARKER), `${SERVER_VERSION}
10843
+ writeFileSync6(join12(target, VERSION_MARKER), `${SERVER_VERSION}
10705
10844
  `);
10706
10845
  installed.push({ name, path: target, replaced });
10707
10846
  }
10708
- writeFileSync6(join11(projectPath, "addons", ADDONS[0], DIGEST_MARKER), `${editorAddonDigest(from)}
10847
+ writeFileSync6(join12(projectPath, "addons", ADDONS[0], DIGEST_MARKER), `${editorAddonDigest(from)}
10709
10848
  `);
10710
- writeFileSync6(join11(projectPath, "addons", RUNTIME_ADDON, DIGEST_MARKER), `${runtimeAddonDigest(from)}
10849
+ writeFileSync6(join12(projectPath, "addons", RUNTIME_ADDON, DIGEST_MARKER), `${runtimeAddonDigest(from)}
10711
10850
  `);
10712
10851
  return installed;
10713
10852
  }
10714
10853
  function installedAddonVersion(projectPath) {
10715
- const marker = join11(projectPath, "addons", ADDONS[0], VERSION_MARKER);
10716
- if (!existsSync11(marker)) {
10854
+ const marker = join12(projectPath, "addons", ADDONS[0], VERSION_MARKER);
10855
+ if (!existsSync12(marker)) {
10717
10856
  return null;
10718
10857
  }
10719
- const said = readFileSync10(marker, "utf8").trim();
10858
+ const said = readFileSync11(marker, "utf8").trim();
10720
10859
  return said === "" ? null : said;
10721
10860
  }
10722
10861
  function removeAddons(projectPath) {
10723
10862
  const removed = [];
10724
10863
  for (const name of ADDONS) {
10725
- const target = join11(projectPath, "addons", name);
10726
- if (existsSync11(target)) {
10864
+ const target = join12(projectPath, "addons", name);
10865
+ if (existsSync12(target)) {
10727
10866
  rmSync5(target, { recursive: true, force: true });
10728
10867
  removed.push(target);
10729
10868
  }
10730
10869
  }
10731
- const addons = join11(projectPath, "addons");
10732
- if (removed.length > 0 && existsSync11(addons) && readdirSync6(addons).length === 0) {
10870
+ const addons = join12(projectPath, "addons");
10871
+ if (removed.length > 0 && existsSync12(addons) && readdirSync7(addons).length === 0) {
10733
10872
  rmdirSync(addons);
10734
10873
  removed.push(addons);
10735
10874
  }
@@ -10763,7 +10902,7 @@ function loaderAmong(projectPath, named) {
10763
10902
  }
10764
10903
  let source = null;
10765
10904
  try {
10766
- source = readFileSync10(join11(projectPath, path), "utf8");
10905
+ source = readFileSync11(join12(projectPath, path), "utf8");
10767
10906
  } catch {}
10768
10907
  if (source?.includes(RUNTIME_AUTOLOAD.path)) {
10769
10908
  return { name: entry, path, exists: true };
@@ -10786,7 +10925,7 @@ function trackedByGit(projectPath, wanted) {
10786
10925
  if (wanted.length === 0) {
10787
10926
  return new Set;
10788
10927
  }
10789
- const asked = spawnSync("git", ["ls-files", "-z", "--", ...wanted], {
10928
+ const asked = spawnSyncHidden("git", ["ls-files", "-z", "--", ...wanted], {
10790
10929
  cwd: projectPath,
10791
10930
  encoding: "utf8"
10792
10931
  });
@@ -10805,10 +10944,10 @@ function enabledPlugins(settings) {
10805
10944
  function inspectProject(projectPath) {
10806
10945
  const problems = [];
10807
10946
  const addons = ADDONS.map((name) => {
10808
- const directory = join11(projectPath, "addons", name);
10809
- const marker = join11(directory, VERSION_MARKER);
10810
- const installed = existsSync11(directory);
10811
- const version = existsSync11(marker) ? readFileSync10(marker, "utf8").trim() : null;
10947
+ const directory = join12(projectPath, "addons", name);
10948
+ const marker = join12(directory, VERSION_MARKER);
10949
+ const installed = existsSync12(directory);
10950
+ const version = existsSync12(marker) ? readFileSync11(marker, "utf8").trim() : null;
10812
10951
  const current = version === SERVER_VERSION;
10813
10952
  if (!installed) {
10814
10953
  problems.push(`addons/${name} is not installed; run gdharness setup`);
@@ -10817,7 +10956,7 @@ function inspectProject(projectPath) {
10817
10956
  }
10818
10957
  return { name, installed, version, current };
10819
10958
  });
10820
- const settings = parseProjectGodot(readFileSync10(join11(projectPath, "project.godot"), "utf8"));
10959
+ const settings = parseProjectGodot(readFileSync11(join12(projectPath, "project.godot"), "utf8"));
10821
10960
  const pluginsEnabled = enabledPlugins(settings);
10822
10961
  for (const name of EDITOR_PLUGINS) {
10823
10962
  if (!pluginsEnabled.includes(name)) {
@@ -10833,7 +10972,7 @@ function inspectProject(projectPath) {
10833
10972
  const tracked = trackedByGit(projectPath, [...named.values()]);
10834
10973
  const addonMissing = (path) => addons.some((addon) => !addon.installed && path.startsWith(`addons/${addon.name}/`));
10835
10974
  for (const [name, path] of named) {
10836
- if (!existsSync11(join11(projectPath, path))) {
10975
+ if (!existsSync12(join12(projectPath, path))) {
10837
10976
  if (!addonMissing(path)) {
10838
10977
  problems.push(`the ${name} autoload names res://${path}, which is not in this project, so it boots with a missing script; restore that file, or point the autoload at one that is here`);
10839
10978
  }
@@ -10869,6 +11008,7 @@ var init_setup = __esm(() => {
10869
11008
  init_project_scan();
10870
11009
  init_resources();
10871
11010
  init_server_version();
11011
+ init_spawn();
10872
11012
  ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
10873
11013
  EDITOR_PLUGINS = ["gdharness_editor", "auto_reload"];
10874
11014
  RUNTIME_AUTOLOAD = {
@@ -11917,6 +12057,11 @@ var init_tool_definitions = __esm(() => {
11917
12057
  ops: ["start"],
11918
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."
11919
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
+ },
11920
12065
  savesIn: {
11921
12066
  type: "string",
11922
12067
  ops: ["start", "check"],
@@ -12072,7 +12217,7 @@ var init_tool_definitions = __esm(() => {
12072
12217
  name: "editor_rescan",
12073
12218
  title: "Rescan the project",
12074
12219
  effect: "additive",
12075
- 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.",
12076
12221
  parameters: {
12077
12222
  projectPath: PROJECT_PATH,
12078
12223
  timeoutMs: {
@@ -12150,13 +12295,13 @@ var init_tool_definitions = __esm(() => {
12150
12295
  pid: RUNNING_PID,
12151
12296
  nodePath: {
12152
12297
  type: "string",
12153
- ops: ["tree", "text", "find", "rect", "property"],
12154
- 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.'
12155
12300
  },
12156
12301
  property: {
12157
12302
  type: "string",
12158
12303
  ops: ["property", "find"],
12159
- 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.'
12160
12305
  },
12161
12306
  depth: {
12162
12307
  type: "integer",
@@ -12172,8 +12317,8 @@ var init_tool_definitions = __esm(() => {
12172
12317
  properties: {
12173
12318
  type: "array",
12174
12319
  items: { type: "string" },
12175
- ops: ["tree"],
12176
- 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.'
12177
12322
  },
12178
12323
  className: {
12179
12324
  type: "string",
@@ -12189,15 +12334,15 @@ var init_tool_definitions = __esm(() => {
12189
12334
  group: { type: "string", ops: ["find"], description: "find: a group the node is in." },
12190
12335
  says: {
12191
12336
  type: "string",
12192
- ops: ["find"],
12193
- 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.'
12194
12339
  },
12195
12340
  limit: {
12196
12341
  type: "integer",
12197
12342
  minimum: 1,
12198
12343
  maximum: 5000,
12199
- ops: ["find", "text"],
12200
- 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."
12201
12346
  },
12202
12347
  includeHidden: {
12203
12348
  type: "boolean",
@@ -12241,12 +12386,12 @@ var init_tool_definitions = __esm(() => {
12241
12386
  },
12242
12387
  metrics: {
12243
12388
  effect: "read",
12244
- summary: "frame time, memory, draw calls and the rest. frame_time is the mean wall time of a frame over the last frames_timed frames (up to 120, timed by the game itself, physics and the wait for the display included), frame_time_max the slowest of them, both in seconds and null before a frame has passed; vsync says whether a frame waits for the display, and max_fps is the game's frame-rate cap, 0 for none. fps, process_time_max and physics_time are the engine's monitors, which it sets once a second: the frames counted in the last second, and the slowest process and physics step in it, so they trail a change by up to a second and the last two are a worst case rather than the frame. The memory, object and render counts are read as asked. memory_static is what the engine allocated on the CPU side, in bytes; render_video_mem_used, render_texture_mem_used and render_buffer_mem_used are what the renderer has on the GPU, in bytes, which memory_static does not count",
12389
+ summary: "frame time, memory, draw calls and the rest. frame_time is the mean wall time of a frame over the last frames_timed frames (up to 120, timed by the game itself, physics and the wait for the display included), frame_time_max the slowest of them, both in seconds and null before a frame has passed; vsync says whether a frame waits for the display, and max_fps is the game's frame-rate cap, 0 for none. physics_steps_per_second is the physics steps taken a second over those frames, fewer than physics_ticks_per_second when a frame needs more than max_physics_steps_per_frame or the editor's Game view slows it. fps, process_time_max and physics_time are the engine's monitors, which it sets once a second: the frames counted in the last second, and the slowest process and physics step in it, so they trail a change by up to a second and the last two are a worst case rather than the frame. The memory, object and render counts are read as asked. memory_static is what the engine allocated on the CPU side, in bytes; render_video_mem_used, render_texture_mem_used and render_buffer_mem_used are what the renderer has on the GPU, in bytes, which memory_static does not count",
12245
12390
  requires: []
12246
12391
  },
12247
12392
  accessibility: {
12248
12393
  effect: "read",
12249
- 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 as UI Automation names it (text for static text, button, check_box, group for a control holding others), its name, its value where it has one, its bounds in the window's pixels, the actions a screen reader is offered (click, focus, toggle, set_value, select, expand_collapse, scroll_into_view), and focused on the one holding the focus. The name is what is read aloud, so an empty one is read as nothing; static text takes it from DisplayServer.accessibility_update_set_value, not from a name set on it, and the answer says under _note how many text elements on screen have none. With no control focused, the engine reports the first node under the root as focused, which in a run with the runtime is GdharnessRuntime. Needs a windowed run on Windows started with engineArgs ["--accessibility", "always"] or with the project's accessibility/general/accessibility_support at Always Active, since the engine builds the tree only when asked; reading it takes a second or two, starting PowerShell inside the game so it reaches the game's own desktop`,
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",
12250
12395
  requires: []
12251
12396
  }
12252
12397
  },
@@ -12255,19 +12400,19 @@ var init_tool_definitions = __esm(() => {
12255
12400
  {
12256
12401
  name: "runtime_invoke",
12257
12402
  title: "Set or call in the running game",
12258
- 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.",
12259
12404
  parameters: {
12260
12405
  projectPath: RUNNING_PROJECT_PATH,
12261
12406
  pid: RUNNING_PID,
12262
12407
  nodePath: {
12263
12408
  type: "string",
12264
12409
  ops: ["set", "call"],
12265
- 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.'
12266
12411
  },
12267
12412
  property: {
12268
12413
  type: "string",
12269
12414
  ops: ["set"],
12270
- 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.'
12271
12416
  },
12272
12417
  value: {
12273
12418
  ops: ["set"],
@@ -12400,6 +12545,13 @@ var init_tool_definitions = __esm(() => {
12400
12545
  ops: ["action", "key", "mouse_click", "joypad_button"],
12401
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."
12402
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
+ },
12403
12555
  strength: {
12404
12556
  type: "number",
12405
12557
  minimum: 0,
@@ -12548,7 +12700,7 @@ var init_tool_definitions = __esm(() => {
12548
12700
  property: {
12549
12701
  type: "string",
12550
12702
  ops: ["until"],
12551
- 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.'
12552
12704
  },
12553
12705
  value: {
12554
12706
  ops: ["until"],
@@ -34422,10 +34574,10 @@ var init_stdio = __esm(() => {
34422
34574
  });
34423
34575
 
34424
34576
  // src/runtime-client.ts
34425
- import { existsSync as existsSync13, readdirSync as readdirSync8, readFileSync as readFileSync12, statSync as statSync6, unlinkSync } from "node:fs";
34577
+ import { existsSync as existsSync14, readdirSync as readdirSync9, readFileSync as readFileSync13, statSync as statSync7, unlinkSync as unlinkSync2 } from "node:fs";
34426
34578
  import { createConnection } from "node:net";
34427
34579
  import { tmpdir as tmpdir4 } from "node:os";
34428
- import { dirname as dirname8, join as join14, resolve as resolve4 } from "node:path";
34580
+ import { dirname as dirname9, join as join15, resolve as resolve4 } from "node:path";
34429
34581
  import { setTimeout as delay } from "node:timers/promises";
34430
34582
  function runtimeDirectory(variables = process.env) {
34431
34583
  const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
@@ -34433,20 +34585,20 @@ function runtimeDirectory(variables = process.env) {
34433
34585
  return explicit;
34434
34586
  }
34435
34587
  const perUser = envValue("XDG_RUNTIME_DIR", variables);
34436
- return join14(perUser ?? tmpdir4(), "gdharness");
34588
+ return join15(perUser ?? tmpdir4(), "gdharness");
34437
34589
  }
34438
34590
  function runtimeDirectories(variables = process.env) {
34439
34591
  const candidates = [runtimeDirectory(variables)];
34440
34592
  const fallbacks = process.platform === "win32" ? [envValue("SystemRoot", variables) ?? envValue("windir", variables) ?? "C:\\Windows"] : ["/tmp", "/var/tmp"];
34441
34593
  for (const base of fallbacks) {
34442
- candidates.push(join14(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
34594
+ candidates.push(join15(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
34443
34595
  }
34444
34596
  return [...new Set(candidates.map((path) => resolve4(path)))];
34445
34597
  }
34446
34598
  function errorReportOf(pid, directories = runtimeDirectories()) {
34447
34599
  for (const directory of directories) {
34448
- const path = join14(directory, `runtime-${pid}.log`);
34449
- if (existsSync13(path)) {
34600
+ const path = join15(directory, `runtime-${pid}.log`);
34601
+ if (existsSync14(path)) {
34450
34602
  return path;
34451
34603
  }
34452
34604
  }
@@ -34477,7 +34629,7 @@ function reportAlreadyInTranscript(transcript, report) {
34477
34629
  function parseAnnouncement(file, pid) {
34478
34630
  let fields;
34479
34631
  try {
34480
- fields = asParams(JSON.parse(readFileSync12(file, "utf8")));
34632
+ fields = asParams(JSON.parse(readFileSync13(file, "utf8")));
34481
34633
  } catch {
34482
34634
  return { kind: "rubbish" };
34483
34635
  }
@@ -34494,7 +34646,7 @@ function parseAnnouncement(file, pid) {
34494
34646
  };
34495
34647
  let announcedAt;
34496
34648
  try {
34497
- announcedAt = statSync6(file).mtimeMs;
34649
+ announcedAt = statSync7(file).mtimeMs;
34498
34650
  } catch {}
34499
34651
  const when = announcedAt === undefined ? {} : { announcedAt };
34500
34652
  if (protocol !== RUNTIME_PROTOCOL) {
@@ -34543,7 +34695,7 @@ function discoverRuntimes(directories = runtimeDirectories()) {
34543
34695
  function wentAway(file, pid) {
34544
34696
  let project = "";
34545
34697
  try {
34546
- const fields = asParams(JSON.parse(readFileSync12(file, "utf8")));
34698
+ const fields = asParams(JSON.parse(readFileSync13(file, "utf8")));
34547
34699
  project = readString(readParams(fields, "project") ?? {}, "path") ?? "";
34548
34700
  } catch {
34549
34701
  return;
@@ -34586,29 +34738,29 @@ function strangersAmong(candidates, now, startTimes = startTimesOf) {
34586
34738
  }).map((one) => one.file));
34587
34739
  }
34588
34740
  function announcedIn(directory) {
34589
- if (!existsSync13(directory)) {
34741
+ if (!existsSync14(directory)) {
34590
34742
  return [];
34591
34743
  }
34592
34744
  const found = [];
34593
34745
  const candidates = [];
34594
34746
  const entries = [];
34595
- for (const entry of readdirSync8(directory)) {
34747
+ for (const entry of readdirSync9(directory)) {
34596
34748
  const report = ERROR_REPORT_PATTERN.exec(entry);
34597
34749
  if (report) {
34598
- sweepErrorReport(join14(directory, entry), Number.parseInt(report[1] ?? "", 10));
34750
+ sweepErrorReport(join15(directory, entry), Number.parseInt(report[1] ?? "", 10));
34599
34751
  continue;
34600
34752
  }
34601
34753
  const match = ANNOUNCEMENT_PATTERN.exec(entry);
34602
34754
  if (!match) {
34603
34755
  continue;
34604
34756
  }
34605
- const file = join14(directory, entry);
34757
+ const file = join15(directory, entry);
34606
34758
  const pid = Number.parseInt(match[1] ?? "", 10);
34607
34759
  const running = alive(pid);
34608
34760
  entries.push({ file, pid, running });
34609
34761
  if (running) {
34610
34762
  try {
34611
- candidates.push({ file, pid, writtenAt: statSync6(file).mtimeMs });
34763
+ candidates.push({ file, pid, writtenAt: statSync7(file).mtimeMs });
34612
34764
  } catch {}
34613
34765
  }
34614
34766
  }
@@ -34622,7 +34774,7 @@ function announcedIn(directory) {
34622
34774
  }
34623
34775
  wentAway(file, pid);
34624
34776
  try {
34625
- unlinkSync(file);
34777
+ unlinkSync2(file);
34626
34778
  } catch {}
34627
34779
  verdicts.delete(file);
34628
34780
  continue;
@@ -34630,16 +34782,16 @@ function announcedIn(directory) {
34630
34782
  found.push(announced);
34631
34783
  }
34632
34784
  const held = new Set(entries.map((one) => one.file));
34633
- const here = join14(directory, ".");
34785
+ const here = join15(directory, ".");
34634
34786
  for (const file of verdicts.keys()) {
34635
- if (dirname8(file) === here && !held.has(file)) {
34787
+ if (dirname9(file) === here && !held.has(file)) {
34636
34788
  verdicts.delete(file);
34637
34789
  }
34638
34790
  }
34639
34791
  return found;
34640
34792
  }
34641
34793
  async function announcementEnded(pid, directories = runtimeDirectories()) {
34642
- const files = directories.map((directory) => join14(directory, `runtime-${pid}.json`)).filter((file) => existsSync13(file));
34794
+ const files = directories.map((directory) => join15(directory, `runtime-${pid}.json`)).filter((file) => existsSync14(file));
34643
34795
  const first = files[0];
34644
34796
  if (first === undefined) {
34645
34797
  return;
@@ -34655,7 +34807,7 @@ async function announcementEnded(pid, directories = runtimeDirectories()) {
34655
34807
  wentAway(first, pid);
34656
34808
  for (const file of files) {
34657
34809
  try {
34658
- unlinkSync(file);
34810
+ unlinkSync2(file);
34659
34811
  } catch {}
34660
34812
  verdicts.delete(file);
34661
34813
  }
@@ -34665,8 +34817,8 @@ function sweepErrorReport(path, pid, now = Date.now()) {
34665
34817
  return;
34666
34818
  }
34667
34819
  try {
34668
- if (now - statSync6(path).mtimeMs >= ERROR_REPORT_KEEP_MS) {
34669
- unlinkSync(path);
34820
+ if (now - statSync7(path).mtimeMs >= ERROR_REPORT_KEEP_MS) {
34821
+ unlinkSync2(path);
34670
34822
  }
34671
34823
  } catch {}
34672
34824
  }
@@ -34940,14 +35092,14 @@ var init_runtime_client = __esm(() => {
34940
35092
  });
34941
35093
 
34942
35094
  // src/boot-note.ts
34943
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "node:fs";
34944
- import { dirname as dirname9, join as join15 } from "node:path";
35095
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync14, writeFileSync as writeFileSync8 } from "node:fs";
35096
+ import { dirname as dirname10, join as join16 } from "node:path";
34945
35097
  function bootNotePath(projectPath) {
34946
- return join15(projectPath, ".godot", "gdharness-boot.json");
35098
+ return join16(projectPath, ".godot", "gdharness-boot.json");
34947
35099
  }
34948
35100
  function readScenes(projectPath) {
34949
35101
  try {
34950
- return asParams(asParams(JSON.parse(readFileSync13(bootNotePath(projectPath), "utf8")))["scenes"]);
35102
+ return asParams(asParams(JSON.parse(readFileSync14(bootNotePath(projectPath), "utf8")))["scenes"]);
34951
35103
  } catch {
34952
35104
  return {};
34953
35105
  }
@@ -34963,7 +35115,7 @@ function readBootNote(projectPath, scene) {
34963
35115
  function writeBootNote(projectPath, scene, announcedAfterMs) {
34964
35116
  try {
34965
35117
  const path = bootNotePath(projectPath);
34966
- mkdirSync6(dirname9(path), { recursive: true });
35118
+ mkdirSync6(dirname10(path), { recursive: true });
34967
35119
  const scenes = {
34968
35120
  ...readScenes(projectPath),
34969
35121
  [scene]: {
@@ -34989,10 +35141,10 @@ var init_boot_note = __esm(() => {
34989
35141
  });
34990
35142
 
34991
35143
  // src/breakpoint-note.ts
34992
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync14, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "node:fs";
34993
- import { dirname as dirname10, isAbsolute as isAbsolute2, join as join16, relative as relative3 } from "node:path";
35144
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync15, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "node:fs";
35145
+ import { dirname as dirname11, isAbsolute as isAbsolute3, join as join17, relative as relative3 } from "node:path";
34994
35146
  function breakpointNotePath(projectPath) {
34995
- return join16(projectPath, ".godot", "gdharness-breakpoints.json");
35147
+ return join17(projectPath, ".godot", "gdharness-breakpoints.json");
34996
35148
  }
34997
35149
  function writeBreakpointNote(projectPath, held) {
34998
35150
  const path = breakpointNotePath(projectPath);
@@ -35009,7 +35161,7 @@ function writeBreakpointNote(projectPath, held) {
35009
35161
  lines: [...one.lines]
35010
35162
  }))
35011
35163
  };
35012
- mkdirSync7(dirname10(path), { recursive: true });
35164
+ mkdirSync7(dirname11(path), { recursive: true });
35013
35165
  writeFileSync9(path, `${JSON.stringify(note, null, 2)}
35014
35166
  `, "utf8");
35015
35167
  } catch (error) {
@@ -35018,7 +35170,7 @@ function writeBreakpointNote(projectPath, held) {
35018
35170
  }
35019
35171
  function readBreakpointNote(projectPath) {
35020
35172
  try {
35021
- const parsed = JSON.parse(readFileSync14(breakpointNotePath(projectPath), "utf8"));
35173
+ const parsed = JSON.parse(readFileSync15(breakpointNotePath(projectPath), "utf8"));
35022
35174
  if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.breakpoints)) {
35023
35175
  return [];
35024
35176
  }
@@ -35028,12 +35180,12 @@ function readBreakpointNote(projectPath) {
35028
35180
  continue;
35029
35181
  }
35030
35182
  const { script, lines: listed } = entry;
35031
- if (typeof script !== "string" || isAbsolute2(script) || script.split("/").includes("..")) {
35183
+ if (typeof script !== "string" || isAbsolute3(script) || script.split("/").includes("..")) {
35032
35184
  continue;
35033
35185
  }
35034
35186
  const lines = Array.isArray(listed) ? listed.filter((line) => Number.isInteger(line) && line > 0) : [];
35035
35187
  if (lines.length > 0) {
35036
- held.push({ scriptPath: realPathOr(join16(projectPath, script)), lines });
35188
+ held.push({ scriptPath: realPathOr(join17(projectPath, script)), lines });
35037
35189
  }
35038
35190
  }
35039
35191
  return held;
@@ -35043,56 +35195,104 @@ function readBreakpointNote(projectPath) {
35043
35195
  }
35044
35196
  function withinProject(projectPath, scriptPath) {
35045
35197
  const rel = relative3(projectPath, scriptPath);
35046
- return rel !== "" && !rel.startsWith("..") && !isAbsolute2(rel);
35198
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute3(rel);
35047
35199
  }
35048
35200
  var init_breakpoint_note = __esm(() => {
35049
35201
  init_errors();
35050
35202
  init_paths();
35051
35203
  });
35052
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
+
35053
35236
  // src/class-note.ts
35054
- import { mkdirSync as mkdirSync8, readFileSync as readFileSync15, writeFileSync as writeFileSync10 } from "node:fs";
35055
- import { dirname as dirname11, join as join17 } from "node:path";
35237
+ import { mkdirSync as mkdirSync8, readFileSync as readFileSync16 } from "node:fs";
35238
+ import { dirname as dirname12, join as join18 } from "node:path";
35056
35239
  function classNotePath(projectPath) {
35057
- return join17(projectPath, ".godot", "gdharness-classes.json");
35240
+ return join18(projectPath, ".godot", "gdharness-classes.json");
35058
35241
  }
35059
35242
  function readClassNote(projectPath) {
35060
35243
  try {
35061
- const fields = asParams(JSON.parse(readFileSync15(classNotePath(projectPath), "utf8")));
35244
+ const fields = asParams(JSON.parse(readFileSync16(classNotePath(projectPath), "utf8")));
35062
35245
  const rebuiltWith = readArray(fields, "rebuiltWith");
35063
35246
  if (rebuiltWith === undefined || !rebuiltWith.every((name) => typeof name === "string")) {
35064
35247
  return null;
35065
35248
  }
35066
35249
  const pid = readNumber(fields, "shortListEditorPid");
35250
+ const unfinished = unfinishedScanIn(fields["unseenBeforeAnUnfinishedScan"]);
35067
35251
  return {
35068
35252
  rebuiltWith: rebuiltWith.map(String),
35069
- ...Object.hasOwn(fields, "shortListEditorPid") ? { shortListEditorPid: pid ?? null } : {}
35253
+ ...Object.hasOwn(fields, "shortListEditorPid") ? { shortListEditorPid: pid ?? null } : {},
35254
+ ...unfinished === null ? {} : { unseenBeforeAnUnfinishedScan: unfinished }
35070
35255
  };
35071
35256
  } catch {
35072
35257
  return null;
35073
35258
  }
35074
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
+ }
35075
35271
  function writeClassNote(projectPath, note) {
35272
+ const path = classNotePath(projectPath);
35076
35273
  try {
35077
- const path = classNotePath(projectPath);
35078
- mkdirSync8(dirname11(path), { recursive: true });
35079
- writeFileSync10(path, `${JSON.stringify(note, null, 2)}
35080
- `, "utf8");
35081
- } catch {}
35274
+ mkdirSync8(dirname12(path), { recursive: true });
35275
+ } catch {
35276
+ return;
35277
+ }
35278
+ writeWhole(path, `${JSON.stringify(note, null, 2)}
35279
+ `);
35082
35280
  }
35083
- var init_class_note = () => {};
35281
+ var init_class_note = __esm(() => {
35282
+ init_whole_file();
35283
+ });
35084
35284
 
35085
35285
  // src/config-pin.ts
35086
- import { existsSync as existsSync14, readFileSync as readFileSync16 } from "node:fs";
35087
- import { join as join18 } from "node:path";
35286
+ import { existsSync as existsSync15, readFileSync as readFileSync17 } from "node:fs";
35287
+ import { join as join19 } from "node:path";
35088
35288
  function versionsNamedInConfig(projectPath) {
35089
- const config = join18(projectPath, ".mcp.json");
35090
- if (!existsSync14(config)) {
35289
+ const config = join19(projectPath, ".mcp.json");
35290
+ if (!existsSync15(config)) {
35091
35291
  return [];
35092
35292
  }
35093
35293
  let text;
35094
35294
  try {
35095
- text = readFileSync16(config, "utf8");
35295
+ text = readFileSync17(config, "utf8");
35096
35296
  JSON.parse(text);
35097
35297
  } catch {
35098
35298
  return [];
@@ -35113,30 +35313,30 @@ var init_config_pin = __esm(() => {
35113
35313
  });
35114
35314
 
35115
35315
  // src/cut-test-run.ts
35116
- import { existsSync as existsSync15, mkdirSync as mkdirSync9, readFileSync as readFileSync17, rmSync as rmSync8, writeFileSync as writeFileSync11 } from "node:fs";
35117
- import { dirname as dirname12, join as join19 } from "node:path";
35316
+ import { existsSync as existsSync16, mkdirSync as mkdirSync9, readFileSync as readFileSync18, rmSync as rmSync9, writeFileSync as writeFileSync11 } from "node:fs";
35317
+ import { dirname as dirname13, join as join20 } from "node:path";
35118
35318
  function keptAt(projectPath) {
35119
- return join19(projectPath, ".godot", "gdharness-reports", "cut-run.json");
35319
+ return join20(projectPath, ".godot", "gdharness-reports", "cut-run.json");
35120
35320
  }
35121
35321
  function keepCutTestRun(projectPath, run) {
35122
35322
  const path = keptAt(projectPath);
35123
35323
  try {
35124
- mkdirSync9(dirname12(path), { recursive: true });
35324
+ mkdirSync9(dirname13(path), { recursive: true });
35125
35325
  writeFileSync11(path, `${JSON.stringify(run, null, 2)}
35126
35326
  `, "utf8");
35127
35327
  } catch {}
35128
35328
  }
35129
35329
  function takeCutTestRun(projectPath) {
35130
35330
  const path = keptAt(projectPath);
35131
- if (!existsSync15(path)) {
35331
+ if (!existsSync16(path)) {
35132
35332
  return null;
35133
35333
  }
35134
35334
  try {
35135
- const run = JSON.parse(readFileSync17(path, "utf8"));
35136
- rmSync8(path, { force: true });
35335
+ const run = JSON.parse(readFileSync18(path, "utf8"));
35336
+ rmSync9(path, { force: true });
35137
35337
  return typeof run.cutAt === "string" && Array.isArray(run.paths) && run.paths.every((path) => typeof path === "string") ? run : null;
35138
35338
  } catch {
35139
- rmSync8(path, { force: true });
35339
+ rmSync9(path, { force: true });
35140
35340
  return null;
35141
35341
  }
35142
35342
  }
@@ -35948,25 +36148,25 @@ var init_desktop = () => {};
35948
36148
  // src/editor-log.ts
35949
36149
  import {
35950
36150
  closeSync,
35951
- existsSync as existsSync16,
36151
+ existsSync as existsSync17,
35952
36152
  mkdirSync as mkdirSync10,
35953
36153
  openSync,
35954
- readFileSync as readFileSync18,
36154
+ readFileSync as readFileSync19,
35955
36155
  readSync,
35956
- rmSync as rmSync9,
35957
- statSync as statSync7,
36156
+ rmSync as rmSync10,
36157
+ statSync as statSync8,
35958
36158
  writeFileSync as writeFileSync12
35959
36159
  } from "node:fs";
35960
- import { dirname as dirname13, join as join20 } from "node:path";
36160
+ import { dirname as dirname14, join as join21 } from "node:path";
35961
36161
  function editorLogPath(projectPath) {
35962
- return join20(projectPath, ".godot", "gdharness-editor.log");
36162
+ return join21(projectPath, ".godot", "gdharness-editor.log");
35963
36163
  }
35964
36164
  function editorLogNotePath(projectPath) {
35965
- return join20(projectPath, ".godot", "gdharness-editor.json");
36165
+ return join21(projectPath, ".godot", "gdharness-editor.json");
35966
36166
  }
35967
36167
  function readEditorLogNote(projectPath) {
35968
36168
  try {
35969
- const fields = asParams(JSON.parse(readFileSync18(editorLogNotePath(projectPath), "utf8")));
36169
+ const fields = asParams(JSON.parse(readFileSync19(editorLogNotePath(projectPath), "utf8")));
35970
36170
  const pid = readNumber(fields, "pid");
35971
36171
  const startedAt = readString(fields, "startedAt");
35972
36172
  return pid !== undefined && Number.isInteger(pid) && pid > 0 && startedAt !== undefined ? { pid, startedAt } : null;
@@ -35977,15 +36177,15 @@ function readEditorLogNote(projectPath) {
35977
36177
  function clearEditorLog(projectPath) {
35978
36178
  try {
35979
36179
  const path = editorLogPath(projectPath);
35980
- mkdirSync10(dirname13(path), { recursive: true });
35981
- rmSync9(path, { force: true });
35982
- rmSync9(editorLogNotePath(projectPath), { force: true });
36180
+ mkdirSync10(dirname14(path), { recursive: true });
36181
+ rmSync10(path, { force: true });
36182
+ rmSync10(editorLogNotePath(projectPath), { force: true });
35983
36183
  } catch {}
35984
36184
  }
35985
36185
  function writeEditorLogNote(projectPath, pid) {
35986
36186
  try {
35987
36187
  const path = editorLogNotePath(projectPath);
35988
- mkdirSync10(dirname13(path), { recursive: true });
36188
+ mkdirSync10(dirname14(path), { recursive: true });
35989
36189
  writeFileSync12(path, JSON.stringify({ pid, startedAt: new Date().toISOString() }, null, 2), "utf8");
35990
36190
  } catch {}
35991
36191
  }
@@ -36007,7 +36207,7 @@ function editorConsole(projectPath, editorPid, openedByAServer) {
36007
36207
  return { kind: "older server", notedPid: noted?.pid ?? null };
36008
36208
  }
36009
36209
  const path = editorLogPath(projectPath);
36010
- if (!existsSync16(path)) {
36210
+ if (!existsSync17(path)) {
36011
36211
  return { kind: "not written" };
36012
36212
  }
36013
36213
  const log = new GameLog;
@@ -36063,9 +36263,9 @@ function foldProgress(text) {
36063
36263
  `);
36064
36264
  }
36065
36265
  function theFirstOf(path) {
36066
- const whole = statSync7(path).size;
36266
+ const whole = statSync8(path).size;
36067
36267
  if (whole <= MOST_CONSOLE_BYTES) {
36068
- return { text: readFileSync18(path, "utf8") };
36268
+ return { text: readFileSync19(path, "utf8") };
36069
36269
  }
36070
36270
  const handle = openSync(path, "r");
36071
36271
  try {
@@ -39202,12 +39402,21 @@ function getDefaultBridge(ownProject = null) {
39202
39402
  defaultBridge ??= new GodotBridge(resolveDefaultBridgePort(), resolveDefaultBridgeHost(), DEFAULT_TIMEOUT_MS, ownProject);
39203
39403
  return defaultBridge;
39204
39404
  }
39205
- 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;
39206
39406
  var init_godot_bridge = __esm(() => {
39207
39407
  init_wrapper();
39208
39408
  init_errors();
39209
39409
  init_paths();
39210
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
+ };
39211
39420
  GodotBridge = class GodotBridge extends EventEmitter {
39212
39421
  listeningSince = null;
39213
39422
  httpServer = null;
@@ -39545,7 +39754,7 @@ var init_godot_bridge = __esm(() => {
39545
39754
  return new Promise((resolve, reject) => {
39546
39755
  const timeout = setTimeout(() => {
39547
39756
  this.pendingRequests.delete(requestId);
39548
- reject(new Error(`Tool ${toolName} timed out after ${this.timeoutMs}ms`));
39757
+ reject(new BridgeTimeout(toolName, this.timeoutMs));
39549
39758
  }, this.timeoutMs);
39550
39759
  this.pendingRequests.set(requestId, {
39551
39760
  toolName,
@@ -40273,19 +40482,19 @@ var DEBUG_PREFIX = "debug ";
40273
40482
  var init_keeper_channel = () => {};
40274
40483
 
40275
40484
  // src/lifecycle-log.ts
40276
- import { appendFileSync, mkdirSync as mkdirSync11, renameSync, statSync as statSync8 } from "node:fs";
40277
- import { dirname as dirname14, join as join21 } from "node:path";
40485
+ import { appendFileSync, mkdirSync as mkdirSync11, renameSync as renameSync3, statSync as statSync9 } from "node:fs";
40486
+ import { dirname as dirname15, join as join22 } from "node:path";
40278
40487
  import process10 from "node:process";
40279
40488
  function lifecycleLogPath() {
40280
- return join21(runtimeDirectory(), "servers.log");
40489
+ return join22(runtimeDirectory(), "servers.log");
40281
40490
  }
40282
40491
  function writeLifecycle(event, fields = {}) {
40283
40492
  const path = lifecycleLogPath();
40284
40493
  try {
40285
- mkdirSync11(dirname14(path), { recursive: true });
40286
- if ((statSync8(path, { throwIfNoEntry: false })?.size ?? 0) > LIFECYCLE_LOG_LIMIT) {
40494
+ mkdirSync11(dirname15(path), { recursive: true });
40495
+ if ((statSync9(path, { throwIfNoEntry: false })?.size ?? 0) > LIFECYCLE_LOG_LIMIT) {
40287
40496
  try {
40288
- renameSync(path, `${path}.1`);
40497
+ renameSync3(path, `${path}.1`);
40289
40498
  } catch {}
40290
40499
  }
40291
40500
  const line = {
@@ -40320,10 +40529,10 @@ var init_lifecycle_log = __esm(() => {
40320
40529
  });
40321
40530
 
40322
40531
  // src/lsp_client.ts
40323
- import { existsSync as existsSync17, realpathSync as realpathSync4 } from "node:fs";
40532
+ import { existsSync as existsSync18, realpathSync as realpathSync4 } from "node:fs";
40324
40533
  import { readFile, realpath } from "node:fs/promises";
40325
40534
  import { createConnection as createConnection3 } from "node:net";
40326
- import { dirname as dirname15, join as join22, resolve as resolve5 } from "node:path";
40535
+ import { dirname as dirname16, join as join23, resolve as resolve5 } from "node:path";
40327
40536
  import { setTimeout as delay3 } from "node:timers/promises";
40328
40537
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
40329
40538
  function initializeTimeoutMs() {
@@ -40485,7 +40694,7 @@ class GodotLSPClient {
40485
40694
  if (this.initialized) {
40486
40695
  return;
40487
40696
  }
40488
- const rootPath = this.rootPath ?? dirname15(resolve5(filePath));
40697
+ const rootPath = this.rootPath ?? dirname16(resolve5(filePath));
40489
40698
  await this.initialize(rootPath);
40490
40699
  }
40491
40700
  async sendRequest(method, params, timeoutMs = this.timeouts.requestMs) {
@@ -40986,7 +41195,7 @@ async function resolveLSPPaths(projectPathValue, scriptPathValue) {
40986
41195
  } catch {
40987
41196
  throw new ArgumentRefusal(`Project path does not exist: ${requestedProjectPath}`);
40988
41197
  }
40989
- if (!existsSync17(join22(projectPath, "project.godot"))) {
41198
+ if (!existsSync18(join23(projectPath, "project.godot"))) {
40990
41199
  throw new ArgumentRefusal(`Not a Godot project: ${projectPath}. Point projectPath at the directory holding project.godot.`);
40991
41200
  }
40992
41201
  const contained = resolveWithinProject(projectPath, scriptPathValue);
@@ -41146,7 +41355,6 @@ var init_lsp_client = __esm(() => {
41146
41355
  });
41147
41356
 
41148
41357
  // src/outside.ts
41149
- import { spawn as spawn3 } from "node:child_process";
41150
41358
  import { fileURLToPath as fileURLToPath4 } from "node:url";
41151
41359
  function changesFrom(env, base) {
41152
41360
  const changes = {};
@@ -41179,9 +41387,8 @@ function readLaunched(printed) {
41179
41387
  }
41180
41388
  async function launchOutsideTheTree(spec, runtime = process.execPath, keeper = KEEPER_SCRIPT) {
41181
41389
  return await new Promise((resolve) => {
41182
- const launcher = spawn3(runtime, [keeper, "launch"], {
41183
- stdio: ["pipe", "pipe", "pipe"],
41184
- windowsHide: true
41390
+ const launcher = spawnHidden(runtime, [keeper, "launch"], {
41391
+ stdio: ["pipe", "pipe", "pipe"]
41185
41392
  });
41186
41393
  sendSpec(launcher, {
41187
41394
  command: spec.command,
@@ -41210,6 +41417,7 @@ async function launchOutsideTheTree(spec, runtime = process.execPath, keeper = K
41210
41417
  }
41211
41418
  var KEEPER_SCRIPT;
41212
41419
  var init_outside = __esm(() => {
41420
+ init_spawn();
41213
41421
  KEEPER_SCRIPT = fileURLToPath4(new URL("./keeper.js", import.meta.url));
41214
41422
  });
41215
41423
 
@@ -41250,8 +41458,6 @@ var STARTING_MS = 20000;
41250
41458
  var init_port_probe = () => {};
41251
41459
 
41252
41460
  // src/process-time.ts
41253
- import { execFile as execFile3 } from "node:child_process";
41254
- import { promisify as promisify3 } from "node:util";
41255
41461
  async function cpuSecondsOf(pid) {
41256
41462
  try {
41257
41463
  return process.platform === "win32" ? await secondsOnWindows(pid) : await askPosix2(pid);
@@ -41265,7 +41471,7 @@ async function secondsOnWindows(pid) {
41265
41471
  return Number.isFinite(seconds) ? seconds : undefined;
41266
41472
  }
41267
41473
  async function askPosix2(pid) {
41268
- 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 });
41269
41475
  return secondsFromClock(stdout.trim());
41270
41476
  }
41271
41477
  function secondsFromClock(printed) {
@@ -41283,10 +41489,10 @@ function secondsFromClock(printed) {
41283
41489
  const extra = days === null ? 0 : Number(days) * 86400;
41284
41490
  return Number.isFinite(extra) ? seconds + extra : undefined;
41285
41491
  }
41286
- var run3, ASK_TIMEOUT_MS2 = 6000;
41492
+ var ASK_TIMEOUT_MS2 = 6000;
41287
41493
  var init_process_time = __esm(() => {
41494
+ init_spawn();
41288
41495
  init_windows_ask();
41289
- run3 = promisify3(execFile3);
41290
41496
  });
41291
41497
 
41292
41498
  // src/prompts.ts
@@ -41426,8 +41632,8 @@ var init_prompts = __esm(() => {
41426
41632
  });
41427
41633
 
41428
41634
  // src/rename.ts
41429
- import { existsSync as existsSync18, mkdirSync as mkdirSync12, readFileSync as readFileSync19, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync13 } from "node:fs";
41430
- import { dirname as dirname16, posix as posix2 } from "node:path";
41635
+ import { existsSync as existsSync19, mkdirSync as mkdirSync12, readFileSync as readFileSync20, renameSync as renameSync4, unlinkSync as unlinkSync3, writeFileSync as writeFileSync13 } from "node:fs";
41636
+ import { dirname as dirname17, posix as posix2 } from "node:path";
41431
41637
 
41432
41638
  class Script {
41433
41639
  path;
@@ -42128,26 +42334,26 @@ function rewritesOf(plan, texts, move) {
42128
42334
  }
42129
42335
  function applyRewrites(rewrites) {
42130
42336
  for (const rewrite of rewrites) {
42131
- const now = readFileSync19(rewrite.absolute, "utf8");
42337
+ const now = readFileSync20(rewrite.absolute, "utf8");
42132
42338
  if (now !== rewrite.before) {
42133
42339
  throw new Error(`${rewrite.path} changed on disk while the rename was being worked out; nothing was written`);
42134
42340
  }
42135
- if (rewrite.writeTo !== rewrite.absolute && existsSync18(rewrite.writeTo)) {
42341
+ if (rewrite.writeTo !== rewrite.absolute && existsSync19(rewrite.writeTo)) {
42136
42342
  throw new Error(`${rewrite.writeTo} already exists; nothing was written`);
42137
42343
  }
42138
42344
  }
42139
42345
  const done = [];
42140
42346
  try {
42141
42347
  for (const rewrite of rewrites) {
42142
- mkdirSync12(dirname16(rewrite.writeTo), { recursive: true });
42348
+ mkdirSync12(dirname17(rewrite.writeTo), { recursive: true });
42143
42349
  const staging = `${rewrite.writeTo}.gdharness-rename`;
42144
42350
  writeFileSync13(staging, rewrite.after, "utf8");
42145
- renameSync2(staging, rewrite.writeTo);
42351
+ renameSync4(staging, rewrite.writeTo);
42146
42352
  let movedUid = false;
42147
42353
  if (rewrite.writeTo !== rewrite.absolute) {
42148
- unlinkSync2(rewrite.absolute);
42149
- if (existsSync18(`${rewrite.absolute}.uid`)) {
42150
- renameSync2(`${rewrite.absolute}.uid`, `${rewrite.writeTo}.uid`);
42354
+ unlinkSync3(rewrite.absolute);
42355
+ if (existsSync19(`${rewrite.absolute}.uid`)) {
42356
+ renameSync4(`${rewrite.absolute}.uid`, `${rewrite.writeTo}.uid`);
42151
42357
  movedUid = true;
42152
42358
  }
42153
42359
  }
@@ -42158,9 +42364,9 @@ function applyRewrites(rewrites) {
42158
42364
  try {
42159
42365
  writeFileSync13(rewrite.absolute, rewrite.before, "utf8");
42160
42366
  if (rewrite.writeTo !== rewrite.absolute) {
42161
- unlinkSync2(rewrite.writeTo);
42367
+ unlinkSync3(rewrite.writeTo);
42162
42368
  if (movedUid) {
42163
- renameSync2(`${rewrite.writeTo}.uid`, `${rewrite.absolute}.uid`);
42369
+ renameSync4(`${rewrite.writeTo}.uid`, `${rewrite.absolute}.uid`);
42164
42370
  }
42165
42371
  }
42166
42372
  } catch {}
@@ -42192,8 +42398,8 @@ var init_rename = __esm(() => {
42192
42398
  });
42193
42399
 
42194
42400
  // src/rename-symbol.ts
42195
- import { existsSync as existsSync19, mkdirSync as mkdirSync13, readdirSync as readdirSync9, realpathSync as realpathSync5, rmSync as rmSync10, writeFileSync as writeFileSync14 } from "node:fs";
42196
- import { basename as basename5, dirname as dirname17, join as join23, relative as relative4, resolve as resolve6 } from "node:path";
42401
+ import { existsSync as existsSync20, mkdirSync as mkdirSync13, readdirSync as readdirSync10, realpathSync as realpathSync5, rmSync as rmSync11, writeFileSync as writeFileSync14 } from "node:fs";
42402
+ import { basename as basename6, dirname as dirname18, join as join24, relative as relative4, resolve as resolve6 } from "node:path";
42197
42403
  function resourcePathOf(projectPath, absolute) {
42198
42404
  const inside = relative4(onDiskSpelling(projectPath), onDiskSpelling(absolute));
42199
42405
  if (inside === "" || inside.startsWith("..") || /^[A-Za-z]:/.test(inside)) {
@@ -42206,13 +42412,13 @@ function onDiskSpelling(path) {
42206
42412
  let at = resolve6(path);
42207
42413
  for (;; ) {
42208
42414
  try {
42209
- return join23(realpathSync5.native(at), ...missing.reverse());
42415
+ return join24(realpathSync5.native(at), ...missing.reverse());
42210
42416
  } catch {
42211
- const parent = dirname17(at);
42417
+ const parent = dirname18(at);
42212
42418
  if (parent === at) {
42213
42419
  return resolve6(path);
42214
42420
  }
42215
- missing.push(basename5(at));
42421
+ missing.push(basename6(at));
42216
42422
  at = parent;
42217
42423
  }
42218
42424
  }
@@ -42259,7 +42465,7 @@ async function renameSymbol(request, services) {
42259
42465
  reason: `newScriptPath must be a .gd path inside the project, and ${request.newScriptPath} is not.`
42260
42466
  };
42261
42467
  }
42262
- if (existsSync19(request.newScriptPath)) {
42468
+ if (existsSync20(request.newScriptPath)) {
42263
42469
  return { ok: false, reason: `${moveTo} already exists, so the script cannot be moved there.` };
42264
42470
  }
42265
42471
  }
@@ -42435,7 +42641,7 @@ async function renameSymbol(request, services) {
42435
42641
  scripts,
42436
42642
  from: declaringPath,
42437
42643
  to: moveTo,
42438
- exists: (path) => existsSync19(`${projectPath}/${path.slice("res://".length)}`)
42644
+ exists: (path) => existsSync20(`${projectPath}/${path.slice("res://".length)}`)
42439
42645
  }, plan);
42440
42646
  }
42441
42647
  plan = mentionIgnored(plan, ignoredTexts(projectPath), symbol, moveTo === null ? null : declaringPath);
@@ -42634,15 +42840,15 @@ ${mention.kind}`;
42634
42840
  };
42635
42841
  }
42636
42842
  function writeLeftAloneDetail(projectPath, symbol, detail) {
42637
- const directory = join23(projectPath, ".godot", "gdharness-renames");
42843
+ const directory = join24(projectPath, ".godot", "gdharness-renames");
42638
42844
  mkdirSync13(directory, { recursive: true });
42639
42845
  detailWrites += 1;
42640
- const path = join23(directory, `${String(Date.now())}-${String(detailWrites).padStart(6, "0")}-${String(process.pid)}-${symbol}.json`);
42846
+ const path = join24(directory, `${String(Date.now())}-${String(detailWrites).padStart(6, "0")}-${String(process.pid)}-${symbol}.json`);
42641
42847
  writeFileSync14(path, `${JSON.stringify(detail, null, 2)}
42642
42848
  `, "utf8");
42643
- const kept = readdirSync9(directory).filter((name) => name.endsWith(".json")).sort().reverse();
42849
+ const kept = readdirSync10(directory).filter((name) => name.endsWith(".json")).sort().reverse();
42644
42850
  for (const old of kept.slice(10)) {
42645
- rmSync10(join23(directory, old), { force: true });
42851
+ rmSync11(join24(directory, old), { force: true });
42646
42852
  }
42647
42853
  return path;
42648
42854
  }
@@ -42682,15 +42888,15 @@ var init_rename_symbol = __esm(() => {
42682
42888
  });
42683
42889
 
42684
42890
  // src/restart-note.ts
42685
- import { mkdirSync as mkdirSync14, readFileSync as readFileSync20, rmSync as rmSync11, writeFileSync as writeFileSync15 } from "node:fs";
42686
- import { dirname as dirname18, join as join24 } from "node:path";
42891
+ import { mkdirSync as mkdirSync14, readFileSync as readFileSync21, rmSync as rmSync12, writeFileSync as writeFileSync15 } from "node:fs";
42892
+ import { dirname as dirname19, join as join25 } from "node:path";
42687
42893
  function restartNotePath(projectPath) {
42688
- return join24(projectPath, ".godot", "gdharness-restart.json");
42894
+ return join25(projectPath, ".godot", "gdharness-restart.json");
42689
42895
  }
42690
42896
  function noteRestartBegun(note) {
42691
42897
  const path = restartNotePath(note.projectPath);
42692
42898
  try {
42693
- mkdirSync14(dirname18(path), { recursive: true });
42899
+ mkdirSync14(dirname19(path), { recursive: true });
42694
42900
  writeFileSync15(path, `${JSON.stringify(note, null, 2)}
42695
42901
  `, "utf8");
42696
42902
  } catch (error) {
@@ -42699,7 +42905,7 @@ function noteRestartBegun(note) {
42699
42905
  }
42700
42906
  function restartOwed(projectPath) {
42701
42907
  try {
42702
- const parsed = JSON.parse(readFileSync20(restartNotePath(projectPath), "utf8"));
42908
+ const parsed = JSON.parse(readFileSync21(restartNotePath(projectPath), "utf8"));
42703
42909
  if (typeof parsed !== "object" || parsed === null) {
42704
42910
  return null;
42705
42911
  }
@@ -42714,7 +42920,7 @@ function restartOwed(projectPath) {
42714
42920
  }
42715
42921
  function restartSettled(projectPath) {
42716
42922
  try {
42717
- rmSync11(restartNotePath(projectPath), { force: true });
42923
+ rmSync12(restartNotePath(projectPath), { force: true });
42718
42924
  } catch (error) {
42719
42925
  console.error(`[SERVER] Could not clear the restart note: ${errorMessage(error)}`);
42720
42926
  }
@@ -42723,58 +42929,25 @@ var init_restart_note = __esm(() => {
42723
42929
  init_errors();
42724
42930
  });
42725
42931
 
42726
- // src/whole-file.ts
42727
- import { renameSync as renameSync3, rmSync as rmSync12, writeFileSync as writeFileSync16 } from "node:fs";
42728
- function writeWhole(path, text) {
42729
- const partial = `${path}.${process.pid}.partial`;
42730
- try {
42731
- writeFileSync16(partial, text, "utf8");
42732
- } catch {
42733
- return false;
42734
- }
42735
- const until = Date.now() + HELD_FILE_MS;
42736
- for (;; ) {
42737
- try {
42738
- renameSync3(partial, path);
42739
- return true;
42740
- } catch (error) {
42741
- const code = error.code ?? "";
42742
- if (!HELD_BY_A_READER.has(code) || Date.now() >= until) {
42743
- try {
42744
- rmSync12(partial, { force: true });
42745
- } catch {}
42746
- return false;
42747
- }
42748
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, HELD_FILE_RETRY_MS);
42749
- }
42750
- }
42751
- }
42752
- var HELD_FILE_MS = 1000, HELD_FILE_RETRY_MS = 10, HELD_BY_A_READER;
42753
- var init_whole_file = __esm(() => {
42754
- HELD_BY_A_READER = new Set(["EPERM", "EACCES", "EBUSY"]);
42755
- });
42756
-
42757
42932
  // src/run-record.ts
42758
- import { execFile as execFile4 } from "node:child_process";
42759
42933
  import { createHash as createHash4 } from "node:crypto";
42760
- import { mkdirSync as mkdirSync15, openSync as openSync2, readdirSync as readdirSync10, readFileSync as readFileSync21, realpathSync as realpathSync6, rmSync as rmSync13, statSync as statSync9 } from "node:fs";
42761
- import { basename as basename6, dirname as dirname19, join as join25, resolve as resolve7 } from "node:path";
42762
- import { promisify as promisify4 } from "node:util";
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";
42935
+ import { basename as basename7, dirname as dirname20, join as join26, resolve as resolve7 } from "node:path";
42763
42936
  function runsDirectory() {
42764
- return join25(runtimeDirectory(), "runs");
42937
+ return join26(runtimeDirectory(), "runs");
42765
42938
  }
42766
42939
  function realAsFarAsItExists(path) {
42767
42940
  const missing = [];
42768
42941
  let at = path;
42769
42942
  for (;; ) {
42770
42943
  try {
42771
- return join25(realpathSync6.native(at), ...missing);
42944
+ return join26(realpathSync6.native(at), ...missing);
42772
42945
  } catch {
42773
- const parent = dirname19(at);
42946
+ const parent = dirname20(at);
42774
42947
  if (parent === at) {
42775
42948
  return path;
42776
42949
  }
42777
- missing.unshift(basename6(at));
42950
+ missing.unshift(basename7(at));
42778
42951
  at = parent;
42779
42952
  }
42780
42953
  }
@@ -42785,10 +42958,10 @@ function projectKey(projectPath) {
42785
42958
  return createHash4("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
42786
42959
  }
42787
42960
  function runRecordPath(projectPath, directory = runtimeDirectory()) {
42788
- return join25(directory, "runs", `note-${projectKey(projectPath)}.json`);
42961
+ return join26(directory, "runs", `note-${projectKey(projectPath)}.json`);
42789
42962
  }
42790
42963
  function latestRunPath(servedBy, directory = runtimeDirectory()) {
42791
- return join25(directory, "runs", `latest-${projectKey(servedBy)}.json`);
42964
+ return join26(directory, "runs", `latest-${projectKey(servedBy)}.json`);
42792
42965
  }
42793
42966
  function writeLatestRun(servedBy, latest) {
42794
42967
  try {
@@ -42798,7 +42971,7 @@ function writeLatestRun(servedBy, latest) {
42798
42971
  }
42799
42972
  function readLatestRun(servedBy, directory = runtimeDirectory()) {
42800
42973
  try {
42801
- const fields = JSON.parse(readFileSync21(latestRunPath(servedBy, directory), "utf8"));
42974
+ const fields = JSON.parse(readFileSync22(latestRunPath(servedBy, directory), "utf8"));
42802
42975
  if (typeof fields !== "object" || fields === null) {
42803
42976
  return null;
42804
42977
  }
@@ -42813,17 +42986,17 @@ function readLatestRun(servedBy, directory = runtimeDirectory()) {
42813
42986
  }
42814
42987
  function openTranscript(startedAt) {
42815
42988
  mkdirSync15(runsDirectory(), { recursive: true });
42816
- const path = join25(runsDirectory(), `run-${startedAt}.log`);
42989
+ const path = join26(runsDirectory(), `run-${startedAt}.log`);
42817
42990
  return { path, fd: openSync2(path, "a") };
42818
42991
  }
42819
42992
  function profilePath(startedAt) {
42820
- return join25(runsDirectory(), `run-${startedAt}.profile.json`);
42993
+ return join26(runsDirectory(), `run-${startedAt}.profile.json`);
42821
42994
  }
42822
42995
  function noteIsAbout(note, playing) {
42823
42996
  return note.play !== undefined && playing !== null && note.play.play === playing.play && note.play.editorPid === playing.editorPid;
42824
42997
  }
42825
42998
  function editorNotePath(projectPath, directory = runtimeDirectory()) {
42826
- return join25(directory, "runs", `editor-${projectKey(projectPath)}.json`);
42999
+ return join26(directory, "runs", `editor-${projectKey(projectPath)}.json`);
42827
43000
  }
42828
43001
  function writeEditorRunNote(note) {
42829
43002
  mkdirSync15(runsDirectory(), { recursive: true });
@@ -42834,7 +43007,7 @@ function writeEditorRunNote(note) {
42834
43007
  }
42835
43008
  function readEditorRunNote(projectPath) {
42836
43009
  for (const directory of runtimeDirectories()) {
42837
- const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join25(directory, "runs", "editor-run.json"));
43010
+ const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join26(directory, "runs", "editor-run.json"));
42838
43011
  if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
42839
43012
  return note;
42840
43013
  }
@@ -42844,7 +43017,7 @@ function readEditorRunNote(projectPath) {
42844
43017
  function editorNoteAt(path) {
42845
43018
  let parsed;
42846
43019
  try {
42847
- parsed = JSON.parse(readFileSync21(path, "utf8"));
43020
+ parsed = JSON.parse(readFileSync22(path, "utf8"));
42848
43021
  } catch {
42849
43022
  return null;
42850
43023
  }
@@ -42868,7 +43041,7 @@ function readRunRecord(projectPath) {
42868
43041
  if (own !== null) {
42869
43042
  return own;
42870
43043
  }
42871
- const shared = recordAt(join25(directory, "runs", SHARED_NOTE));
43044
+ const shared = recordAt(join26(directory, "runs", SHARED_NOTE));
42872
43045
  if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
42873
43046
  return shared;
42874
43047
  }
@@ -42880,13 +43053,13 @@ function everyRunRecord() {
42880
43053
  for (const directory of runtimeDirectories()) {
42881
43054
  let entries;
42882
43055
  try {
42883
- entries = readdirSync10(join25(directory, "runs"));
43056
+ entries = readdirSync11(join26(directory, "runs"));
42884
43057
  } catch {
42885
43058
  continue;
42886
43059
  }
42887
43060
  for (const entry of entries) {
42888
43061
  if (entry === SHARED_NOTE || entry.startsWith("note-") && entry.endsWith(".json")) {
42889
- const record = recordAt(join25(directory, "runs", entry));
43062
+ const record = recordAt(join26(directory, "runs", entry));
42890
43063
  if (record !== null) {
42891
43064
  found.push(record);
42892
43065
  }
@@ -42898,7 +43071,7 @@ function everyRunRecord() {
42898
43071
  function recordAt(path) {
42899
43072
  let parsed;
42900
43073
  try {
42901
- parsed = JSON.parse(readFileSync21(path, "utf8"));
43074
+ parsed = JSON.parse(readFileSync22(path, "utf8"));
42902
43075
  } catch {
42903
43076
  return null;
42904
43077
  }
@@ -42951,10 +43124,10 @@ async function runningAs(pid) {
42951
43124
  const began = await startedAt(pid, null);
42952
43125
  const when = began === null ? {} : { startedAt: began };
42953
43126
  if (process.platform === "linux") {
42954
- const raw = readFileSync21(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
43127
+ const raw = readFileSync22(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
42955
43128
  return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
42956
43129
  }
42957
- 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();
42958
43131
  return args === "" ? null : { kind: "commandLine", text: args, ...when };
42959
43132
  } catch {
42960
43133
  return null;
@@ -42963,7 +43136,7 @@ async function runningAs(pid) {
42963
43136
  async function listeningPid(port) {
42964
43137
  try {
42965
43138
  if (process.platform === "win32") {
42966
- const { stdout } = await execFileAsync("netstat", ["-ano"], { encoding: "utf8", timeout: 15000 });
43139
+ const { stdout } = await runHidden("netstat", ["-ano"], { timeout: 15000 });
42967
43140
  return listeningPidInNetstat(stdout, port);
42968
43141
  }
42969
43142
  for (const [command, args, pattern] of [
@@ -42971,7 +43144,7 @@ async function listeningPid(port) {
42971
43144
  ["ss", ["-ltnpH", `sport = :${port}`], /pid=(\d+)/]
42972
43145
  ]) {
42973
43146
  try {
42974
- const said = (await execFileAsync(command, [...args], { encoding: "utf8", timeout: 15000 })).stdout.trim();
43147
+ const said = (await runHidden(command, [...args], { timeout: 15000 })).stdout.trim();
42975
43148
  const found = pattern.exec(said);
42976
43149
  if (found?.[1] !== undefined) {
42977
43150
  return Number(found[1]);
@@ -43016,7 +43189,7 @@ async function startedAt(pid, windowsAnswer) {
43016
43189
  return Number.isNaN(when) ? null : when;
43017
43190
  }
43018
43191
  try {
43019
- 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();
43020
43193
  const when = Date.parse(said);
43021
43194
  return said === "" || Number.isNaN(when) ? null : when;
43022
43195
  } catch {
@@ -43025,10 +43198,8 @@ async function startedAt(pid, windowsAnswer) {
43025
43198
  }
43026
43199
  async function windowsImage(pid) {
43027
43200
  try {
43028
- const { stdout: csv } = await execFileAsync("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
43029
- encoding: "utf8",
43030
- timeout: 15000,
43031
- windowsHide: true
43201
+ const { stdout: csv } = await runHidden("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
43202
+ timeout: 15000
43032
43203
  });
43033
43204
  const image = /^"([^"]+)"/.exec(csv.trim())?.[1];
43034
43205
  return image === undefined ? { why: "named no process under that number" } : { image };
@@ -43057,7 +43228,7 @@ function whyNotTheRun(record, running, needed) {
43057
43228
  if (after > SAME_RUN_WINDOW_MS) {
43058
43229
  return `that process started ${Math.round(after / 1000)} seconds after the run had`;
43059
43230
  }
43060
- const engine = record.command === undefined ? null : basename6(record.command);
43231
+ const engine = record.command === undefined ? null : basename7(record.command);
43061
43232
  const said = process.platform === "win32" ? running.text.toLowerCase() : running.text;
43062
43233
  const wanted = engine === null || process.platform !== "win32" ? engine : engine.toLowerCase();
43063
43234
  if (wanted !== null && !said.includes(wanted)) {
@@ -43096,7 +43267,7 @@ function judgeAnnouncedGame(running, projectPath, announcedAt) {
43096
43267
  }
43097
43268
  function clearRunRecord(projectPath) {
43098
43269
  for (const directory of runtimeDirectories()) {
43099
- const shared = join25(directory, "runs", SHARED_NOTE);
43270
+ const shared = join26(directory, "runs", SHARED_NOTE);
43100
43271
  const sharedRecord = recordAt(shared);
43101
43272
  const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
43102
43273
  for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
@@ -43108,7 +43279,7 @@ function clearRunRecord(projectPath) {
43108
43279
  }
43109
43280
  function ageMs(path, now) {
43110
43281
  try {
43111
- return now - statSync9(path).mtimeMs;
43282
+ return now - statSync10(path).mtimeMs;
43112
43283
  } catch {
43113
43284
  return 0;
43114
43285
  }
@@ -43121,7 +43292,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
43121
43292
  ]));
43122
43293
  let entries;
43123
43294
  try {
43124
- entries = readdirSync10(directory);
43295
+ entries = readdirSync11(directory);
43125
43296
  } catch {
43126
43297
  return;
43127
43298
  }
@@ -43130,7 +43301,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
43130
43301
  if (!transcript && !entry.endsWith(".partial")) {
43131
43302
  continue;
43132
43303
  }
43133
- const path = join25(directory, entry);
43304
+ const path = join26(directory, entry);
43134
43305
  if (kept.has(path) || ageMs(path, now) < keepMs) {
43135
43306
  continue;
43136
43307
  }
@@ -43139,19 +43310,19 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
43139
43310
  } catch {}
43140
43311
  }
43141
43312
  }
43142
- var execFileAsync, SHARED_NOTE = "run.json", SAME_RUN_WINDOW_MS = 1e4;
43313
+ var SHARED_NOTE = "run.json", SAME_RUN_WINDOW_MS = 1e4;
43143
43314
  var init_run_record = __esm(() => {
43144
43315
  init_errors();
43145
43316
  init_paths();
43146
43317
  init_process_children();
43147
43318
  init_runtime_client();
43319
+ init_spawn();
43148
43320
  init_whole_file();
43149
43321
  init_windows_ask();
43150
- execFileAsync = promisify4(execFile4);
43151
43322
  });
43152
43323
 
43153
43324
  // src/script-origins.ts
43154
- import { join as join26 } from "node:path";
43325
+ import { join as join27 } from "node:path";
43155
43326
  function unresolvedClassIn(message) {
43156
43327
  return UNRESOLVED.exec(message)?.[1] ?? null;
43157
43328
  }
@@ -43204,7 +43375,7 @@ function checkWithEngine(godotPath, projectPath) {
43204
43375
  const ran = await runEngine(godotPath, [
43205
43376
  "--headless",
43206
43377
  "--log-file",
43207
- join26(logDir, "engine.log"),
43378
+ join27(logDir, "engine.log"),
43208
43379
  "--path",
43209
43380
  projectPath,
43210
43381
  "--check-only",
@@ -43249,10 +43420,10 @@ var init_script_origins = __esm(() => {
43249
43420
  });
43250
43421
 
43251
43422
  // src/script-profile.ts
43252
- import { readFileSync as readFileSync22 } from "node:fs";
43423
+ import { readFileSync as readFileSync23 } from "node:fs";
43253
43424
  function readProfileFile(path) {
43254
43425
  try {
43255
- const parsed = JSON.parse(readFileSync22(path, "utf8"));
43426
+ const parsed = JSON.parse(readFileSync23(path, "utf8"));
43256
43427
  return typeof parsed === "object" && typeof parsed.connected === "boolean" ? parsed : null;
43257
43428
  } catch {
43258
43429
  return null;
@@ -43423,27 +43594,25 @@ var init_server_instructions = __esm(() => {
43423
43594
  });
43424
43595
 
43425
43596
  // src/server.ts
43426
- import { execFile as execFile5, spawn as spawn4 } from "node:child_process";
43427
43597
  import { randomUUID as randomUUID2 } from "node:crypto";
43428
43598
  import {
43429
43599
  appendFileSync as appendFileSync2,
43430
43600
  closeSync as closeSync2,
43431
43601
  constants,
43432
43602
  copyFileSync,
43433
- existsSync as existsSync20,
43603
+ existsSync as existsSync21,
43434
43604
  fstatSync,
43435
43605
  mkdirSync as mkdirSync16,
43436
43606
  openSync as openSync3,
43437
- readdirSync as readdirSync11,
43438
- readFileSync as readFileSync23,
43607
+ readdirSync as readdirSync12,
43608
+ readFileSync as readFileSync24,
43439
43609
  readSync as readSync2,
43440
43610
  rmdirSync as rmdirSync2,
43441
- statSync as statSync10
43611
+ statSync as statSync11
43442
43612
  } from "node:fs";
43443
- import { basename as basename7, dirname as dirname20, extname as extname3, isAbsolute as isAbsolute3, join as join27, normalize as normalize2, relative as relative5 } from "node:path";
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";
43444
43614
  import { setTimeout as delay4 } from "node:timers/promises";
43445
43615
  import { fileURLToPath as fileURLToPath5 } from "node:url";
43446
- import { promisify as promisify5 } from "node:util";
43447
43616
  function editorRestartTimeoutMs() {
43448
43617
  return waitFromEnv("GDHARNESS_EDITOR_RESTART_TIMEOUT_MS", 90000);
43449
43618
  }
@@ -43471,22 +43640,22 @@ function writtenFileRefusal(path, rule) {
43471
43640
  if (/^(res|user):\/\//.test(path)) {
43472
43641
  return `${named} ${path} is a Godot path; ${what} is saved outside the project, at an absolute path on this machine.`;
43473
43642
  }
43474
- if (!isAbsolute3(path)) {
43643
+ if (!isAbsolute4(path)) {
43475
43644
  return `${named} ${path} is not absolute; name the whole path, so where it is written does not depend on this server's directory.`;
43476
43645
  }
43477
43646
  if (!rule.extensions.includes(extname3(path).toLowerCase())) {
43478
43647
  return `${named} ${path} does not end in ${rule.extensions.join(" or ")}, which is what ${what} is.`;
43479
43648
  }
43480
- const directory = realPathOr(dirname20(path));
43481
- if (!existsSync20(directory)) {
43482
- return `${named}'s directory ${dirname20(path)} does not exist; ${what} creates no directories.`;
43649
+ const directory = realPathOr(dirname21(path));
43650
+ if (!existsSync21(directory)) {
43651
+ return `${named}'s directory ${dirname21(path)} does not exist; ${what} creates no directories.`;
43483
43652
  }
43484
- const where = join27(directory, basename7(path));
43653
+ const where = join28(directory, basename8(path));
43485
43654
  const inside = rule.projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
43486
43655
  if (inside !== undefined) {
43487
43656
  return `${named} ${path} is inside the project at ${inside}; ${what} is saved outside it, so it never becomes part of the game.`;
43488
43657
  }
43489
- const taken = rule.writes(where).find((file) => existsSync20(file));
43658
+ const taken = rule.writes(where).find((file) => existsSync21(file));
43490
43659
  if (taken !== undefined) {
43491
43660
  return taken === where ? `${named} ${path} already exists; ${what} is not written over a file. Name a new one.` : `${named} ${path} would write ${taken}, which already exists; ${what} is not written over a file. Name a new one.`;
43492
43661
  }
@@ -43536,6 +43705,9 @@ function captureDetail(payload) {
43536
43705
  function patienceForFrames(frames, atLeast) {
43537
43706
  return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
43538
43707
  }
43708
+ function withDefined(fields) {
43709
+ return Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined));
43710
+ }
43539
43711
  function longerWaitNote(tool, op, waitedMs) {
43540
43712
  const spec = TOOL_SPECS.find((one) => one.name === tool);
43541
43713
  if (spec !== undefined && argumentsOf(spec, op).includes("timeoutMs")) {
@@ -43979,9 +44151,35 @@ function notRunNote(notRun, failFast) {
43979
44151
  const cases = `${notRun} case${notRun === 1 ? "" : "s"}`;
43980
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.`;
43981
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
+ }
43982
44179
  function exportAnswer(ending, asked, before, after) {
43983
44180
  const { log } = ending;
43984
44181
  const written = after !== null && after !== before;
44182
+ const notCopied = librariesNotCopied(log.all);
43985
44183
  const verdict = {
43986
44184
  exported: ending.exitCode === 0 && ending.failure === null && log.count("error") === 0 && written,
43987
44185
  ...asked,
@@ -43990,6 +44188,10 @@ function exportAnswer(ending, asked, before, after) {
43990
44188
  failure: ending.failure ?? undefined,
43991
44189
  errors: log.count("error"),
43992
44190
  warnings: log.count("warning"),
44191
+ ...notCopied.length === 0 ? {} : {
44192
+ librariesNotLoaded: notCopied,
44193
+ extensionNote: `The export could not load ${notCopied.join(" and ")}: on Windows an engine running as the editor, which an export is, loads a GDExtension from a ~ copy beside the library, and another engine on this project held that copy: it could not be moved aside before the export, or another engine made a new one in between. Export again once that engine has loaded or exited.`
44194
+ },
43993
44195
  ...foldedEntriesAnswer(log)
43994
44196
  };
43995
44197
  if (verdict.exported) {
@@ -44027,7 +44229,7 @@ function engineFlagsNote(args) {
44027
44229
  return `${named} ${among.length === 1 ? "is a flag" : "are flags"} the engine reads, and args goes to the game after --, where the engine does not read ${among.length === 1 ? "it" : "them"}. engineArgs hands the engine its own flags; ${among.length === 1 ? "leave it" : "leave them"} in args only for a game that reads ${among.length === 1 ? "that flag" : "those flags"} itself.`;
44028
44230
  }
44029
44231
  function editorAddonMissing(projectPath) {
44030
- if (!existsSync20(join27(projectPath, "project.godot"))) {
44232
+ if (!existsSync21(join28(projectPath, "project.godot"))) {
44031
44233
  return null;
44032
44234
  }
44033
44235
  const report = inspectProject(projectPath);
@@ -44263,7 +44465,7 @@ function profileFrameFunctions(projectPath) {
44263
44465
  let projectGodot = null;
44264
44466
  if (projectPath !== null) {
44265
44467
  try {
44266
- projectGodot = readFileSync23(join27(projectPath, "project.godot"), "utf8");
44468
+ projectGodot = readFileSync24(join28(projectPath, "project.godot"), "utf8");
44267
44469
  } catch {}
44268
44470
  }
44269
44471
  return frameFunctionsFor(queuedMessagesOf(projectGodot));
@@ -44287,7 +44489,7 @@ function projectSourceOf(projectPath) {
44287
44489
  let source = null;
44288
44490
  if (inside.ok) {
44289
44491
  try {
44290
- source = readFileSync23(inside.absolutePath, "utf8");
44492
+ source = readFileSync24(inside.absolutePath, "utf8");
44291
44493
  } catch {}
44292
44494
  }
44293
44495
  read.set(script, source);
@@ -44437,7 +44639,7 @@ function foldedEntriesAnswer(log, floor = "warning") {
44437
44639
  }
44438
44640
  function gdUnitFailsOnScriptErrors(projectPath) {
44439
44641
  try {
44440
- return settingKeys(readFileSync23(join27(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
44642
+ return settingKeys(readFileSync24(join28(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
44441
44643
  } catch {
44442
44644
  return true;
44443
44645
  }
@@ -44452,13 +44654,13 @@ function uncaughtNote(uncaught) {
44452
44654
  }
44453
44655
  function elsewhereIn(projectPath, asked) {
44454
44656
  const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
44455
- if (wanted === "" || existsSync20(join27(projectPath, wanted))) {
44657
+ if (wanted === "" || existsSync21(join28(projectPath, wanted))) {
44456
44658
  return "";
44457
44659
  }
44458
44660
  const root = (wanted.split("/")[0] ?? "").toLowerCase();
44459
44661
  let near = [];
44460
44662
  try {
44461
- near = readdirSync11(projectPath, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.toLowerCase() !== root).map((entry) => entry.name).filter((name) => name.toLowerCase().startsWith("test") || root.startsWith(name.toLowerCase()));
44663
+ near = readdirSync12(projectPath, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.toLowerCase() !== root).map((entry) => entry.name).filter((name) => name.toLowerCase().startsWith("test") || root.startsWith(name.toLowerCase()));
44462
44664
  } catch {
44463
44665
  return "";
44464
44666
  }
@@ -44542,23 +44744,23 @@ function scriptsToDiagnose(projectPath, given) {
44542
44744
  }
44543
44745
  function isDirectory(path) {
44544
44746
  try {
44545
- return statSync10(path).isDirectory();
44747
+ return statSync11(path).isDirectory();
44546
44748
  } catch {
44547
44749
  return false;
44548
44750
  }
44549
44751
  }
44550
44752
  function scriptsUnder(directory, prefix) {
44551
- if (existsSync20(join27(directory, ".gdignore"))) {
44753
+ if (existsSync21(join28(directory, ".gdignore"))) {
44552
44754
  return [];
44553
44755
  }
44554
44756
  const found = [];
44555
- for (const entry of readdirSync11(directory, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : 1)) {
44757
+ for (const entry of readdirSync12(directory, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : 1)) {
44556
44758
  if (entry.name.startsWith(".")) {
44557
44759
  continue;
44558
44760
  }
44559
44761
  const path = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
44560
44762
  if (entry.isDirectory()) {
44561
- found.push(...scriptsUnder(join27(directory, entry.name), path));
44763
+ found.push(...scriptsUnder(join28(directory, entry.name), path));
44562
44764
  } else if (entry.isFile() && entry.name.endsWith(".gd")) {
44563
44765
  found.push(path);
44564
44766
  }
@@ -44566,7 +44768,7 @@ function scriptsUnder(directory, prefix) {
44566
44768
  return found;
44567
44769
  }
44568
44770
  function hasMainScene(projectFile) {
44569
- const scene = parseProjectGodot(readFileSync23(projectFile, "utf8"))["application"]?.["run/main_scene"];
44771
+ const scene = parseProjectGodot(readFileSync24(projectFile, "utf8"))["application"]?.["run/main_scene"];
44570
44772
  return typeof scene === "string" && scene !== "";
44571
44773
  }
44572
44774
  function runtimeAddonAnnouncesEditors(projectPath) {
@@ -44574,13 +44776,13 @@ function runtimeAddonAnnouncesEditors(projectPath) {
44574
44776
  return false;
44575
44777
  }
44576
44778
  try {
44577
- return readFileSync23(join27(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
44779
+ return readFileSync24(join28(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
44578
44780
  } catch {
44579
44781
  return false;
44580
44782
  }
44581
44783
  }
44582
44784
  function editorPlaysHeadless(projectFile) {
44583
- const runArgs = parseProjectGodot(readFileSync23(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
44785
+ const runArgs = parseProjectGodot(readFileSync24(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
44584
44786
  return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
44585
44787
  }
44586
44788
  function camelCased(params) {
@@ -44599,7 +44801,7 @@ function needsItsOwnProcess(asked) {
44599
44801
  class GodotServer {
44600
44802
  stdio = null;
44601
44803
  locator = new GodotLocator;
44602
- operationsScript = join27(__dirname2, "godot", "operations", "godot_operations.gd");
44804
+ operationsScript = join28(__dirname2, "godot", "operations", "godot_operations.gd");
44603
44805
  godotBridge;
44604
44806
  tools = buildToolDefinitions();
44605
44807
  activeProcess = null;
@@ -44624,6 +44826,7 @@ class GodotServer {
44624
44826
  launchedDropped = null;
44625
44827
  closedHere = null;
44626
44828
  inFlight = new Map;
44829
+ callsOnGames = new Set;
44627
44830
  callsTaken = 0;
44628
44831
  clientNoted = null;
44629
44832
  parentCommand = null;
@@ -44855,14 +45058,14 @@ class GodotServer {
44855
45058
  }
44856
45059
  noteTheStart() {
44857
45060
  const cwd = process.cwd();
44858
- const mcpJson = join27(cwd, ".mcp.json");
45061
+ const mcpJson = join28(cwd, ".mcp.json");
44859
45062
  const announced = this.ownProject === null ? null : readAnnouncement(announcementPath(this.ownProject));
44860
45063
  const predecessor = announced !== null && announced.pid !== process.pid && alive(announced.pid) ? announced.pid : null;
44861
45064
  writeLifecycle("started", {
44862
45065
  argv: process.argv.slice(1),
44863
45066
  cwd,
44864
45067
  project: this.ownProject,
44865
- mcpJson: existsSync20(mcpJson) ? mcpJson : null
45068
+ mcpJson: existsSync21(mcpJson) ? mcpJson : null
44866
45069
  });
44867
45070
  if (predecessor !== null) {
44868
45071
  writeLifecycle("replaced", { project: this.ownProject, predecessor });
@@ -45003,7 +45206,7 @@ class GodotServer {
45003
45206
  return checked.response;
45004
45207
  }
45005
45208
  const named = args["projectPath"];
45006
- if (typeof named === "string" && existsSync20(join27(named, "project.godot"))) {
45209
+ if (typeof named === "string" && existsSync21(join28(named, "project.godot"))) {
45007
45210
  this.lastProjectPath = named;
45008
45211
  }
45009
45212
  this.updates.refresh();
@@ -45410,7 +45613,15 @@ class GodotServer {
45410
45613
  property: readNonEmptyString(args, "property") ?? ""
45411
45614
  }, { tool: "runtime_inspect", op, patience });
45412
45615
  case "accessibility":
45413
- 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
+ }, {
45414
45625
  tool: "runtime_inspect",
45415
45626
  op,
45416
45627
  patience: readPositiveNumber(args, "timeoutMs") ?? Math.max(30000, this.runtimeTimeoutMs())
@@ -45461,7 +45672,14 @@ class GodotServer {
45461
45672
  ...args["index"] === undefined ? {} : { index: args["index"] }
45462
45673
  }, { tool: "runtime_input", op });
45463
45674
  }
45464
- 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
+ }
45465
45683
  case "runtime_wait":
45466
45684
  return await this.handleRuntimeWait(op, args);
45467
45685
  case "debug_breakpoint": {
@@ -45561,8 +45779,8 @@ class GodotServer {
45561
45779
  if (path === undefined) {
45562
45780
  return { ok: false, response: this.createErrorResponse("projectPath is required.") };
45563
45781
  }
45564
- const file = join27(path, "project.godot");
45565
- if (!existsSync20(file)) {
45782
+ const file = join28(path, "project.godot");
45783
+ if (!existsSync21(file)) {
45566
45784
  return {
45567
45785
  ok: false,
45568
45786
  response: this.createErrorResponse(`Not a Godot project: ${path}`, [
@@ -45608,7 +45826,7 @@ class GodotServer {
45608
45826
  }
45609
45827
  runEnvironment(args) {
45610
45828
  const savesIn = readNonEmptyString(args, "savesIn");
45611
- if (savesIn !== undefined && !isAbsolute3(savesIn)) {
45829
+ if (savesIn !== undefined && !isAbsolute4(savesIn)) {
45612
45830
  return {
45613
45831
  ok: false,
45614
45832
  response: this.createErrorResponse(`savesIn must be an absolute path, not "${savesIn}".`, [
@@ -46192,11 +46410,11 @@ class GodotServer {
46192
46410
  if (!engine.ok) {
46193
46411
  return engine.response;
46194
46412
  }
46195
- const application = parseProjectGodot(readFileSync23(project.value.file, "utf8"))["application"] ?? {};
46413
+ const application = parseProjectGodot(readFileSync24(project.value.file, "utf8"))["application"] ?? {};
46196
46414
  const name = application["config/name"];
46197
46415
  const mainScene = application["run/main_scene"];
46198
46416
  const info = {
46199
- name: typeof name === "string" && name !== "" ? name : basename7(project.value.path),
46417
+ name: typeof name === "string" && name !== "" ? name : basename8(project.value.path),
46200
46418
  path: project.value.path,
46201
46419
  mainScene: typeof mainScene === "string" && mainScene !== "" ? mainScene : null,
46202
46420
  godotVersion: await this.godotVersion(engine.value),
@@ -46244,22 +46462,26 @@ class GodotServer {
46244
46462
  if (!output.ok) {
46245
46463
  return this.createErrorResponse(output.reason, PATH_SOLUTIONS);
46246
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
+ }
46247
46469
  const engine = await this.engine();
46248
46470
  if (!engine.ok) {
46249
46471
  return engine.response;
46250
46472
  }
46251
- const folder = dirname20(output.absolutePath);
46473
+ const folder = dirname21(output.absolutePath);
46252
46474
  try {
46253
46475
  mkdirSync16(folder, { recursive: true });
46254
46476
  } catch (error) {
46255
- return this.createErrorResponse(`Cannot create ${dirname20(output.relativePath)} to export into: ${errorMessage(error)}`);
46477
+ return this.createErrorResponse(`Cannot create ${dirname21(output.relativePath)} to export into: ${errorMessage(error)}`);
46256
46478
  }
46257
46479
  const debug = readBoolean(args, "debug") ?? false;
46258
46480
  const exportLogs = scratchDirectory("export");
46259
46481
  const exportArgs = [
46260
46482
  "--headless",
46261
46483
  "--log-file",
46262
- join27(exportLogs, "engine.log"),
46484
+ join28(exportLogs, "engine.log"),
46263
46485
  "--path",
46264
46486
  project.value.path,
46265
46487
  debug ? "--export-debug" : "--export-release",
@@ -46267,8 +46489,12 @@ class GodotServer {
46267
46489
  output.absolutePath
46268
46490
  ];
46269
46491
  this.logDebug(`Exporting: ${engine.value} ${exportArgs.join(" ")}`);
46270
- const writtenAt = () => existsSync20(output.absolutePath) ? statSync10(output.absolutePath).mtimeMs : null;
46492
+ const writtenAt = () => existsSync21(output.absolutePath) ? statSync11(output.absolutePath).mtimeMs : null;
46271
46493
  const before = writtenAt();
46494
+ const freed = freeLibraryCopies(project.value.path);
46495
+ if (freed.setAside.length > 0 || freed.swept.length > 0) {
46496
+ this.logDebug(`Before the export: set aside ${freed.setAside.join(", ")}; swept ${freed.swept.join(", ")}`);
46497
+ }
46272
46498
  let ending;
46273
46499
  try {
46274
46500
  ending = await runEngine(engine.value, exportArgs, { timeout: { ms: 300000, said: "five minutes" } });
@@ -46331,10 +46557,10 @@ class GodotServer {
46331
46557
  if (movie !== null) {
46332
46558
  return this.createErrorResponse(movie);
46333
46559
  }
46334
- const missing = paths.value.filter((path) => !existsSync20(join27(project.value.path, path.slice("res://".length))));
46560
+ const missing = paths.value.filter((path) => !existsSync21(join28(project.value.path, path.slice("res://".length))));
46335
46561
  const asked = paths.value.filter((path) => !missing.includes(path));
46336
46562
  const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
46337
- const noRunner = !existsSync20(join27(project.value.path, runner));
46563
+ const noRunner = !existsSync21(join28(project.value.path, runner));
46338
46564
  if (asked.length === 0) {
46339
46565
  const verdict = `nothing at ${missing.join(" and ")}`;
46340
46566
  const andNoRunner = noRunner ? `, and gdUnit4 is not installed in this project: no ${runner}` : "";
@@ -46425,21 +46651,21 @@ class GodotServer {
46425
46651
  await untilGone([run.process.pid]);
46426
46652
  }
46427
46653
  await run.settled;
46428
- const reportsDir = join27(project.value.path, ".godot", ...ours.split("/"));
46654
+ const reportsDir = join28(project.value.path, ".godot", ...ours.split("/"));
46429
46655
  let report = null;
46430
46656
  let reportProblem = null;
46431
46657
  try {
46432
- const written = existsSync20(reportsDir) ? readdirSync11(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
46658
+ const written = existsSync21(reportsDir) ? readdirSync12(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
46433
46659
  const newest = written.at(-1);
46434
46660
  if (newest !== undefined) {
46435
- report = parseJUnit(readFileSync23(join27(reportsDir, newest, "results.xml"), "utf8"));
46661
+ report = parseJUnit(readFileSync24(join28(reportsDir, newest, "results.xml"), "utf8"));
46436
46662
  }
46437
46663
  } catch (error) {
46438
46664
  reportProblem = errorMessage(error);
46439
46665
  } finally {
46440
46666
  discard(reportsDir, userData);
46441
46667
  try {
46442
- rmdirSync2(dirname20(reportsDir));
46668
+ rmdirSync2(dirname21(reportsDir));
46443
46669
  } catch {}
46444
46670
  }
46445
46671
  const engineEntries = foldedRepeats(run.log.select({
@@ -46608,7 +46834,7 @@ class GodotServer {
46608
46834
  }
46609
46835
  async godotVersion(godotPath) {
46610
46836
  try {
46611
- const { stdout } = await run4(godotPath, ["--version"], { timeout: 1e4 });
46837
+ const { stdout } = await runHidden(godotPath, ["--version"], { timeout: 1e4 });
46612
46838
  this.lastGodotVersion = stdout.trim();
46613
46839
  return this.lastGodotVersion;
46614
46840
  } catch (error) {
@@ -46669,6 +46895,10 @@ class GodotServer {
46669
46895
  return markIfStale(answer, this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest(), this.editorCodeOnDisk());
46670
46896
  }
46671
46897
  async diagnosticsOf(args) {
46898
+ const waited = await this.waitForTheEditorOpenedHere(args, []);
46899
+ if (waited !== null) {
46900
+ return { refusal: waited, perScript: false };
46901
+ }
46672
46902
  const answer = await this.handleLSP("lsp_get_diagnostics", args);
46673
46903
  const payload = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
46674
46904
  const refused = this.languageServerRefusal(payload, "Diagnostics");
@@ -46699,11 +46929,11 @@ class GodotServer {
46699
46929
  const cached = cachedClasses(projectPath);
46700
46930
  const sourceOf = (resourcePath) => {
46701
46931
  const contained = resolveWithinProject(projectPath, resourcePath);
46702
- if (!contained.ok || !existsSync20(contained.absolutePath)) {
46932
+ if (!contained.ok || !existsSync21(contained.absolutePath)) {
46703
46933
  return null;
46704
46934
  }
46705
46935
  try {
46706
- return readFileSync23(contained.absolutePath, "utf8");
46936
+ return readFileSync24(contained.absolutePath, "utf8");
46707
46937
  } catch {
46708
46938
  return null;
46709
46939
  }
@@ -46753,9 +46983,34 @@ class GodotServer {
46753
46983
  const recorded = readClassNote(projectPath)?.shortListEditorPid;
46754
46984
  return recorded !== undefined && recorded === (this.godotBridge.getStatus().editorPid ?? null);
46755
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
+ }
46756
47010
  noteAShortListEditor(projectPath) {
46757
47011
  const note = readClassNote(projectPath);
46758
47012
  writeClassNote(projectPath, {
47013
+ ...note,
46759
47014
  rebuiltWith: note?.rebuiltWith ?? [],
46760
47015
  shortListEditorPid: this.godotBridge.getStatus().editorPid ?? null
46761
47016
  });
@@ -46768,11 +47023,11 @@ class GodotServer {
46768
47023
  }
46769
47024
  sourceOfClass(projectPath, resourcePath) {
46770
47025
  const contained = resolveWithinProject(projectPath, resourcePath);
46771
- if (!contained.ok || !existsSync20(contained.absolutePath)) {
47026
+ if (!contained.ok || !existsSync21(contained.absolutePath)) {
46772
47027
  return null;
46773
47028
  }
46774
47029
  try {
46775
- return readFileSync23(contained.absolutePath, "utf8");
47030
+ return readFileSync24(contained.absolutePath, "utf8");
46776
47031
  } catch {
46777
47032
  return null;
46778
47033
  }
@@ -46780,7 +47035,17 @@ class GodotServer {
46780
47035
  async handleLSP(toolName, args) {
46781
47036
  return handleLSPTool(await this.lsp(), toolName, args);
46782
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
+ }
46783
47044
  async askScriptInfo(toolName, asked, args) {
47045
+ const waited = await this.waitForTheEditorOpenedHere(args, []);
47046
+ if (waited !== null) {
47047
+ return waited;
47048
+ }
46784
47049
  const answer = await this.handleLSP(toolName, args);
46785
47050
  const refused = this.languageServerRefusal(asParams(JSON.parse(answer.content[0]?.text ?? "{}")), asked);
46786
47051
  return refused === null ? answer : refused.refusal;
@@ -46920,7 +47185,7 @@ class GodotServer {
46920
47185
  ...Array.isArray(stack) ? {
46921
47186
  stack: stack.map((frame) => ({
46922
47187
  ...frame,
46923
- ...projectPath !== "" && typeof frame["script"] === "string" && frame["script"].startsWith("res://") ? { path: join27(projectPath, frame["script"].slice("res://".length)) } : {}
47188
+ ...projectPath !== "" && typeof frame["script"] === "string" && frame["script"].startsWith("res://") ? { path: join28(projectPath, frame["script"].slice("res://".length)) } : {}
46924
47189
  }))
46925
47190
  } : {}
46926
47191
  });
@@ -47190,7 +47455,7 @@ class GodotServer {
47190
47455
  return null;
47191
47456
  }
47192
47457
  try {
47193
- return settingKeys(readFileSync23(join27(projectPath, "project.godot"), "utf8"));
47458
+ return settingKeys(readFileSync24(join28(projectPath, "project.godot"), "utf8"));
47194
47459
  } catch {
47195
47460
  return null;
47196
47461
  }
@@ -47455,6 +47720,7 @@ class GodotServer {
47455
47720
  }
47456
47721
  async openAnEditor(engine, projectPath, ports, hidden) {
47457
47722
  clearEditorLog(projectPath);
47723
+ freeLibraryCopies(projectPath);
47458
47724
  const launched = await launchOutsideTheTree({
47459
47725
  command: engine,
47460
47726
  args: editorArguments(projectPath, ports, editorLogPath(projectPath), hidden),
@@ -47505,6 +47771,17 @@ class GodotServer {
47505
47771
  if (sceneToRun && !sceneToRun.ok) {
47506
47772
  return this.createErrorResponse(sceneToRun.reason, PATH_SOLUTIONS);
47507
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
+ }
47508
47785
  const given = this.gameArguments(args);
47509
47786
  if (!given.ok) {
47510
47787
  return given.response;
@@ -47521,13 +47798,13 @@ class GodotServer {
47521
47798
  if (!asked.ok) {
47522
47799
  return asked.response;
47523
47800
  }
47524
- if (!sceneToRun && !hasMainScene(project.value.file)) {
47801
+ if (exported === null && !sceneToRun && !hasMainScene(project.value.file)) {
47525
47802
  return this.createErrorResponse("The project sets no main scene, so there is nothing to run.", [
47526
47803
  "Pass scene to run one scene",
47527
47804
  "Choose the main scene with project_settings set_main_scene"
47528
47805
  ]);
47529
47806
  }
47530
- const engine = await this.engine();
47807
+ const engine = exported?.ok === true ? { ok: true, value: exported.absolutePath } : await this.engine();
47531
47808
  if (!engine.ok) {
47532
47809
  return engine.response;
47533
47810
  }
@@ -47565,11 +47842,11 @@ class GodotServer {
47565
47842
  const editorWouldPlay = !headless || editorPlaysHeadless(project.value.file);
47566
47843
  const silent = resolveSilent(args["silent"], process.env) && !headless;
47567
47844
  const profiled = readBoolean(args, "profile") === true;
47568
- 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) {
47569
47846
  return await this.playThroughEditor(sceneArgument, refreshed.value, scanned, project.value.path, alreadyPlaying, runtimeWaitMs, ended, profiled);
47570
47847
  }
47571
47848
  const cmdArgs = runArguments({
47572
- projectPath: project.value.path,
47849
+ projectPath: exported === null ? project.value.path : null,
47573
47850
  headless,
47574
47851
  scene: sceneArgument,
47575
47852
  userArgs: given.value,
@@ -47577,7 +47854,7 @@ class GodotServer {
47577
47854
  silent
47578
47855
  });
47579
47856
  this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
47580
- 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);
47581
47858
  if ("error" in started) {
47582
47859
  return this.createErrorResponse(`The game could not be started: ${started.error}`, [
47583
47860
  "editor_status says which Godot this server runs and whether it answers"
@@ -47586,7 +47863,7 @@ class GodotServer {
47586
47863
  started.announcedBefore = alreadyPlaying;
47587
47864
  started.bootScene = runtimeWaitMs.scene;
47588
47865
  this.activeProcess = started;
47589
- 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}` : "";
47590
47867
  const endedForThis = endedToStartThis(ended);
47591
47868
  const stranded = wereHereFirst.filter((one) => one.pid !== ended?.pid && alive(one.pid));
47592
47869
  const handed = this.dapHoldingBreakpointsOf(project.value.path).breakpointsHeld();
@@ -47596,6 +47873,7 @@ class GodotServer {
47596
47873
  through: "gdharness",
47597
47874
  alsoRunning: stranded.length === 0 ? undefined : stranded.map((one) => one.pid),
47598
47875
  pid: started.pid,
47876
+ executable: exported?.ok === true ? exported.relativePath : undefined,
47599
47877
  arguments: cmdArgs,
47600
47878
  argsNote: engineFlagsNote(given.value),
47601
47879
  transcript: started.transcript,
@@ -47607,13 +47885,13 @@ class GodotServer {
47607
47885
  endedPreviousRun: endedPreviousRun(ended),
47608
47886
  previousRunLeft: previousRunLeft(ended),
47609
47887
  ...previousRunHow(ended),
47610
- 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),
47611
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}`
47612
47890
  });
47613
47891
  }
47614
47892
  async runtimeUp(projectPath, before, budget, withArgs) {
47615
47893
  const budgetMs = budget.ms;
47616
- if (!existsSync20(join27(projectPath, RUNTIME_AUTOLOAD.path))) {
47894
+ if (!existsSync21(join28(projectPath, RUNTIME_AUTOLOAD.path))) {
47617
47895
  return runtimeVerdict(null, {
47618
47896
  addon: false,
47619
47897
  budgetMs,
@@ -47698,7 +47976,7 @@ class GodotServer {
47698
47976
  }
47699
47977
  let announcedAt;
47700
47978
  try {
47701
- announcedAt = statSync10(endpoint.file).mtimeMs;
47979
+ announcedAt = statSync11(endpoint.file).mtimeMs;
47702
47980
  } catch {
47703
47981
  return;
47704
47982
  }
@@ -47892,7 +48170,7 @@ class GodotServer {
47892
48170
  return 0;
47893
48171
  }
47894
48172
  try {
47895
- return reportAlreadyInTranscript(readFileSync23(game.transcript), readFileSync23(report));
48173
+ return reportAlreadyInTranscript(readFileSync24(game.transcript), readFileSync24(report));
47896
48174
  } catch (error) {
47897
48175
  this.logDebug(`Could not compare a picked-up run's report with its transcript: ${errorMessage(error)}`);
47898
48176
  return 0;
@@ -48047,7 +48325,10 @@ class GodotServer {
48047
48325
  return gone ? "gone" : "lingering";
48048
48326
  }
48049
48327
  spawnGame(godotPath, cmdArgs, env) {
48050
- 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
+ });
48051
48332
  const cancelled = callSignal();
48052
48333
  if (cancelled !== undefined) {
48053
48334
  const end = () => {
@@ -48120,7 +48401,7 @@ class GodotServer {
48120
48401
  });
48121
48402
  return Object.assign(started, { settled });
48122
48403
  }
48123
- async launchKeptGame(godotPath, cmdArgs, projectPath, env, desktop, profiled = false) {
48404
+ async launchKeptGame(godotPath, cmdArgs, projectPath, env, desktop, profiled = false, debugged = true) {
48124
48405
  const startedAt = Date.now();
48125
48406
  const transcript = openTranscript(startedAt);
48126
48407
  closeSync2(transcript.fd);
@@ -48136,7 +48417,7 @@ class GodotServer {
48136
48417
  projectPath,
48137
48418
  ...this.servedBy(),
48138
48419
  ...profile === undefined ? {} : { profile },
48139
- breakpoints: this.breakpointsForAGame(projectPath)
48420
+ ...debugged ? { breakpoints: this.breakpointsForAGame(projectPath) } : { debugged: false }
48140
48421
  },
48141
48422
  ...desktop === undefined ? {} : { desktop }
48142
48423
  });
@@ -48572,7 +48853,7 @@ class GodotServer {
48572
48853
  }
48573
48854
  const going = await this.runStillGoing(run);
48574
48855
  const projectPath = run.projectPath;
48575
- const projectGone = projectPath !== null && !existsSync20(join27(projectPath, "project.godot"));
48856
+ const projectGone = projectPath !== null && !existsSync21(join28(projectPath, "project.godot"));
48576
48857
  if (projectGone && callersName !== undefined) {
48577
48858
  return this.createErrorResponse(`callersOf reads the project's scripts, and the run's project at ${projectPath} has no project.godot any more.`, ["editor_run profile without callersOf answers with the profile itself"]);
48578
48859
  }
@@ -48860,7 +49141,7 @@ class GodotServer {
48860
49141
  notes.push(endedWithoutACode({
48861
49142
  throughEditor: run.throughEditor,
48862
49143
  announced: announced !== undefined || run.announcedPid !== undefined,
48863
- couldAnnounce: run.projectPath !== null && existsSync20(join27(run.projectPath, RUNTIME_AUTOLOAD.path)),
49144
+ couldAnnounce: run.projectPath !== null && existsSync21(join28(run.projectPath, RUNTIME_AUTOLOAD.path)),
48864
49145
  endedHere: typeof run.endedHere === "string",
48865
49146
  pickedUp: run.pickedUp === true
48866
49147
  }));
@@ -49100,8 +49381,8 @@ class GodotServer {
49100
49381
  try {
49101
49382
  const status = asParams(await this.godotBridge.invokeTool("scan_status", {}));
49102
49383
  return status["scanning"] === true || status["importing"] === true;
49103
- } catch {
49104
- return false;
49384
+ } catch (error) {
49385
+ return error instanceof BridgeTimeout;
49105
49386
  }
49106
49387
  }
49107
49388
  async handleRescanFilesystem(args) {
@@ -49112,7 +49393,7 @@ class GodotServer {
49112
49393
  let writtenBefore = null;
49113
49394
  let blind = { unseen: [] };
49114
49395
  let waitedForEditorMs = 0;
49115
- let first;
49396
+ let startUnanswered = false;
49116
49397
  let completedBefore;
49117
49398
  let uidsReread = [];
49118
49399
  let uidsDuplicatedOnDisk = [];
@@ -49129,7 +49410,11 @@ class GodotServer {
49129
49410
  before = projectPath === "" ? null : cachedClasses(projectPath);
49130
49411
  writtenBefore = projectPath === "" ? null : cacheWrittenAt(projectPath);
49131
49412
  blind = await this.classesTheEditorCannotSee(args);
49132
- 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
+ }
49133
49418
  if (first.isError) {
49134
49419
  return first;
49135
49420
  }
@@ -49152,7 +49437,17 @@ class GodotServer {
49152
49437
  let idleSince = null;
49153
49438
  while (Date.now() - started < timeoutMs) {
49154
49439
  await new Promise((settle) => setTimeout(settle, 100));
49155
- 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
+ }
49156
49451
  const pending = Boolean(status["pending"]) && Date.now() - started < SCAN_START_MS;
49157
49452
  const completed = readNumber(status, "scansCompleted");
49158
49453
  const notYetOver = completedBefore !== undefined && completed !== undefined && completed <= completedBefore;
@@ -49235,12 +49530,20 @@ class GodotServer {
49235
49530
  }
49236
49531
  const checked = busy ? { unseen: [] } : await this.classesTheEditorCannotSee(args);
49237
49532
  const unseen = checked.unseen;
49238
- 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
+ }
49239
49539
  const dependents = busy || projectPath === "" || broughtIn.length === 0 ? null : await this.reloadDependents(projectPath, broughtIn);
49240
49540
  const stillGone = lost.filter((name) => !restored.includes(name));
49241
49541
  const notes = [];
49242
49542
  if (busy) {
49243
- 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
+ }
49244
49547
  } else if (stillGone.length > 0) {
49245
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.");
49246
49549
  } else if (lost.length > 0) {
@@ -49358,8 +49661,9 @@ class GodotServer {
49358
49661
  const droppedByTheEditor = lost.filter((name) => !cameBack.includes(name));
49359
49662
  const seenLosing = droppedByTheEditor.length > 0 && this.godotBridge.isConnected();
49360
49663
  writeClassNote(projectPath, {
49664
+ ...noted,
49361
49665
  rebuiltWith: [...after.keys()].sort(),
49362
- ...seenLosing ? { shortListEditorPid: this.godotBridge.getStatus().editorPid ?? null } : noted?.shortListEditorPid === undefined ? {} : { shortListEditorPid: noted.shortListEditorPid }
49666
+ ...seenLosing ? { shortListEditorPid: this.godotBridge.getStatus().editorPid ?? null } : {}
49363
49667
  });
49364
49668
  const notes = [];
49365
49669
  if (seenLosing) {
@@ -49425,7 +49729,7 @@ class GodotServer {
49425
49729
  if (!scriptPath.ok) {
49426
49730
  return this.createErrorResponse(scriptPath.reason);
49427
49731
  }
49428
- if (!existsSync20(scriptPath.absolutePath)) {
49732
+ if (!existsSync21(scriptPath.absolutePath)) {
49429
49733
  return this.createErrorResponse(`Script file does not exist: ${scriptPath.absolutePath}`);
49430
49734
  }
49431
49735
  const asked = readNonEmptyString(args, "newScriptPath");
@@ -49433,6 +49737,10 @@ class GodotServer {
49433
49737
  if (moveTo !== null && !moveTo.ok) {
49434
49738
  return this.createErrorResponse(moveTo.reason);
49435
49739
  }
49740
+ const waited = await this.waitForTheEditorOpenedHere(args, []);
49741
+ if (waited !== null) {
49742
+ return waited;
49743
+ }
49436
49744
  const languageServer = {
49437
49745
  problem: null,
49438
49746
  initialized: false
@@ -49675,6 +49983,16 @@ class GodotServer {
49675
49983
  ]);
49676
49984
  }
49677
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) {
49678
49996
  if (!this.godotBridge.isConnected()) {
49679
49997
  const alternative = askedAs !== undefined && Object.hasOwn(EDITOR_FREE_INSTEAD, askedAs) ? EDITOR_FREE_INSTEAD[askedAs] : undefined;
49680
49998
  const refused = await this.noEditorYet(args, alternative === undefined ? [] : [alternative]);
@@ -49693,6 +50011,9 @@ class GodotServer {
49693
50011
  try {
49694
50012
  return this.jsonTextResponse(markIfStale(await this.godotBridge.invokeTool(toolName, args), this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest(), this.editorCodeOnDisk()));
49695
50013
  } catch (error) {
50014
+ if (error instanceof BridgeTimeout) {
50015
+ return error;
50016
+ }
49696
50017
  const message = errorMessage(error);
49697
50018
  if (message.includes(`Unknown tool: ${toolName}`)) {
49698
50019
  const status = this.godotBridge.getStatus();
@@ -49753,7 +50074,7 @@ class GodotServer {
49753
50074
  async ownRunOnItsWay(projectPath, budgetMs) {
49754
50075
  const run = await this.currentRun();
49755
50076
  const project = projectPath ?? run?.projectPath ?? null;
49756
- if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync20(join27(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
50077
+ if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync21(join28(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
49757
50078
  return null;
49758
50079
  }
49759
50080
  const already = runtimesAnnounced().running;
@@ -49762,7 +50083,7 @@ class GodotServer {
49762
50083
  return !run.announcedBefore.has(one.pid);
49763
50084
  }
49764
50085
  try {
49765
- return statSync10(one.file).mtimeMs >= run.startedAt;
50086
+ return statSync11(one.file).mtimeMs >= run.startedAt;
49766
50087
  } catch {
49767
50088
  return false;
49768
50089
  }
@@ -49838,12 +50159,25 @@ class GodotServer {
49838
50159
  }
49839
50160
  }
49840
50161
  const screenshotDir = expectsScreenshot ? scratchDirectory("runtime-screenshot") : null;
49841
- const screenshotPath = screenshotDir ? join27(screenshotDir, "capture.png") : null;
50162
+ const screenshotPath = screenshotDir ? join28(screenshotDir, "capture.png") : null;
49842
50163
  const staleNote = this.runtimeStaleNote(choice.endpoint);
49843
50164
  const stale = staleNote === undefined ? {} : { addonIsStale: true, staleNote };
49844
50165
  const refused = (message) => this.createErrorResponse(staleNote === undefined ? message : `${message}
49845
50166
 
49846
50167
  ${staleNote}`);
50168
+ const onGame = {
50169
+ pid: choice.endpoint.pid,
50170
+ what: `${asked.tool} ${asked.op}`,
50171
+ alongside: new Set
50172
+ };
50173
+ for (const other of this.callsOnGames) {
50174
+ if (other.pid === onGame.pid) {
50175
+ other.alongside.add(onGame.what);
50176
+ onGame.alongside.add(other.what);
50177
+ }
50178
+ }
50179
+ this.callsOnGames.add(onGame);
50180
+ const alongside = () => onGame.alongside.size === 0 ? {} : { ranAlongside: [...onGame.alongside], alongsideNote: ALONGSIDE_NOTE };
49847
50181
  try {
49848
50182
  const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs, !expectsScreenshot);
49849
50183
  if (!reply.ok && reply.requestId !== undefined) {
@@ -49854,7 +50188,8 @@ ${staleNote}`);
49854
50188
  waitedMs: timeoutMs,
49855
50189
  note: `${reply.message} ${longerWaitNote(asked.tool, asked.op, timeoutMs)}`,
49856
50190
  ...answeredBy,
49857
- ...stale
50191
+ ...stale,
50192
+ ...alongside()
49858
50193
  });
49859
50194
  }
49860
50195
  if (!reply.ok) {
@@ -49862,7 +50197,7 @@ ${staleNote}`);
49862
50197
  }
49863
50198
  const { id: _id, ...payload } = reply.payload;
49864
50199
  if (!expectsScreenshot) {
49865
- return this.jsonTextResponse({ ...payload, ...answeredBy, ...stale });
50200
+ return this.jsonTextResponse({ ...payload, ...answeredBy, ...stale, ...alongside() });
49866
50201
  }
49867
50202
  const returnedPath = readString(payload, "path");
49868
50203
  if (readString(payload, "type") !== "screenshot_file" || !returnedPath || !screenshotPath) {
@@ -49879,18 +50214,22 @@ ${staleNote}`);
49879
50214
  return this.createErrorResponse(`The capture was taken and could not be saved at ${saveTo}: ${errorMessage(error)}`);
49880
50215
  }
49881
50216
  }
50217
+ const ranWith = [...onGame.alongside];
49882
50218
  return {
49883
50219
  content: [
49884
50220
  {
49885
50221
  type: "text",
49886
- text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}${captureDetail(payload)}${saveTo === null ? "" : `. Saved to ${saveTo}`}${staleNote === undefined ? "" : `
50222
+ text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}${captureDetail(payload)}${saveTo === null ? "" : `. Saved to ${saveTo}`}${ranWith.length === 0 ? "" : `
50223
+
50224
+ Ran alongside ${ranWith.join(", ")}. ${ALONGSIDE_NOTE}`}${staleNote === undefined ? "" : `
49887
50225
 
49888
50226
  ${staleNote}`}`
49889
50227
  },
49890
- { type: "image", data: readFileSync23(screenshotPath).toString("base64"), mimeType: "image/png" }
50228
+ { type: "image", data: readFileSync24(screenshotPath).toString("base64"), mimeType: "image/png" }
49891
50229
  ]
49892
50230
  };
49893
50231
  } finally {
50232
+ this.callsOnGames.delete(onGame);
49894
50233
  discard(screenshotDir);
49895
50234
  }
49896
50235
  }
@@ -49989,7 +50328,7 @@ async function runGodotServer() {
49989
50328
  const server = new GodotServer;
49990
50329
  await server.run();
49991
50330
  }
49992
- 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, 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;
49993
50332
  var init_server = __esm(() => {
49994
50333
  init_dist();
49995
50334
  init_stdio();
@@ -50019,6 +50358,7 @@ var init_server = __esm(() => {
50019
50358
  init_junit();
50020
50359
  init_keeper_channel();
50021
50360
  init_launch();
50361
+ init_library_copies();
50022
50362
  init_lifecycle_log();
50023
50363
  init_lsp_client();
50024
50364
  init_outside();
@@ -50041,11 +50381,11 @@ var init_server = __esm(() => {
50041
50381
  init_server_instructions();
50042
50382
  init_server_version();
50043
50383
  init_setup();
50384
+ init_spawn();
50044
50385
  init_tool_definitions();
50045
50386
  init_tool_reference();
50046
50387
  init_update_check();
50047
- run4 = promisify5(execFile5);
50048
- __dirname2 = dirname20(fileURLToPath5(import.meta.url));
50388
+ __dirname2 = dirname21(fileURLToPath5(import.meta.url));
50049
50389
  KEEPER_DEBUG_MS = PAUSE_MS + STEP_MS + 2 * READ_MS;
50050
50390
  CONSOLE_WRAPPER = /_console(\.exe)?$/i;
50051
50391
  LONGEST_BOOT_NOTED_MS = 5 * 60000;
@@ -50104,11 +50444,11 @@ var init_server = __esm(() => {
50104
50444
  init_bridge_announce();
50105
50445
  init_errors();
50106
50446
  init_godot_path();
50107
- import { existsSync as existsSync21 } from "node:fs";
50108
- import { join as join28, resolve as resolve8 } from "node:path";
50447
+ import { existsSync as existsSync22 } from "node:fs";
50448
+ import { join as join29, resolve as resolve8 } from "node:path";
50109
50449
 
50110
50450
  // src/harnesses.ts
50111
- 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";
50112
50452
  import { homedir as homedir2 } from "node:os";
50113
50453
  import { basename as basename2, dirname as dirname3, join as join4, relative as relative2, resolve as resolve2 } from "node:path";
50114
50454
  import process4 from "node:process";
@@ -50836,7 +51176,7 @@ function mainCheckoutOf(projectPath) {
50836
51176
  top = up;
50837
51177
  }
50838
51178
  const marker = join4(top, ".git");
50839
- if (!statSync2(marker).isFile()) {
51179
+ if (!statSync3(marker).isFile()) {
50840
51180
  return null;
50841
51181
  }
50842
51182
  const pointer = /^gitdir:\s*(.+)$/m.exec(readFileSync2(marker, "utf8"))?.[1]?.trim();
@@ -50865,7 +51205,7 @@ function worktreeCheckoutsOf(projectPath) {
50865
51205
  top = up;
50866
51206
  }
50867
51207
  const listed = join4(top, ".git", "worktrees");
50868
- if (!statSync2(join4(top, ".git")).isDirectory() || !existsSync4(listed)) {
51208
+ if (!statSync3(join4(top, ".git")).isDirectory() || !existsSync4(listed)) {
50869
51209
  return [];
50870
51210
  }
50871
51211
  const below = relative2(top, resolve2(projectPath));
@@ -51070,10 +51410,10 @@ init_setup();
51070
51410
  init_setup();
51071
51411
  init_tool_definitions();
51072
51412
  init_tool_reference();
51073
- import { existsSync as existsSync12, mkdirSync as mkdirSync5, readdirSync as readdirSync7, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "node:fs";
51074
- import { basename as basename4, dirname as dirname6, join as join12 } from "node:path";
51413
+ import { existsSync as existsSync13, mkdirSync as mkdirSync5, readdirSync as readdirSync8, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "node:fs";
51414
+ import { basename as basename5, dirname as dirname7, join as join13 } from "node:path";
51075
51415
  var SKILL_NAME = "gdharness";
51076
- var SHARED_SKILLS = join12(".agents", "skills");
51416
+ var SHARED_SKILLS = join13(".agents", "skills");
51077
51417
  function skillMarkdown(version) {
51078
51418
  return `---
51079
51419
  name: ${SKILL_NAME}
@@ -51279,27 +51619,27 @@ comes back separately, under \`engine_messages_after_answer\`: none of it change
51279
51619
  function skillFiles(version) {
51280
51620
  return new Map([
51281
51621
  ["SKILL.md", skillMarkdown(version)],
51282
- [join12("references", "tools.md"), renderToolsMarkdown()]
51622
+ [join13("references", "tools.md"), renderToolsMarkdown()]
51283
51623
  ]);
51284
51624
  }
51285
51625
  function skillDirectories(harnesses, projectPath, exists) {
51286
- const directories = [join12(projectPath, SHARED_SKILLS, SKILL_NAME)];
51626
+ const directories = [join13(projectPath, SHARED_SKILLS, SKILL_NAME)];
51287
51627
  for (const harness of harnesses) {
51288
51628
  if (harness.skills === undefined) {
51289
51629
  continue;
51290
51630
  }
51291
- const own = join12(projectPath, harness.skills.dir);
51292
- if ((!harness.skills.shared || exists(own)) && !directories.includes(join12(own, SKILL_NAME))) {
51293
- directories.push(join12(own, SKILL_NAME));
51631
+ const own = join13(projectPath, harness.skills.dir);
51632
+ if ((!harness.skills.shared || exists(own)) && !directories.includes(join13(own, SKILL_NAME))) {
51633
+ directories.push(join13(own, SKILL_NAME));
51294
51634
  }
51295
51635
  }
51296
51636
  return directories;
51297
51637
  }
51298
51638
  function everySkillDirectory(projectPath, harnesses) {
51299
- const directories = new Set([join12(projectPath, SHARED_SKILLS, SKILL_NAME)]);
51639
+ const directories = new Set([join13(projectPath, SHARED_SKILLS, SKILL_NAME)]);
51300
51640
  for (const harness of harnesses) {
51301
51641
  if (harness.skills !== undefined) {
51302
- directories.add(join12(projectPath, harness.skills.dir, SKILL_NAME));
51642
+ directories.add(join13(projectPath, harness.skills.dir, SKILL_NAME));
51303
51643
  }
51304
51644
  }
51305
51645
  return [...directories];
@@ -51307,33 +51647,33 @@ function everySkillDirectory(projectPath, harnesses) {
51307
51647
  function removeSkill(directories, projectPath) {
51308
51648
  const removed = [];
51309
51649
  for (const directory of directories) {
51310
- if (existsSync12(directory)) {
51650
+ if (existsSync13(directory)) {
51311
51651
  rmSync6(directory, { recursive: true, force: true });
51312
- pruneEmpty(dirname6(directory), projectPath);
51652
+ pruneEmpty(dirname7(directory), projectPath);
51313
51653
  removed.push(directory);
51314
51654
  }
51315
51655
  }
51316
51656
  return removed;
51317
51657
  }
51318
51658
  function pruneEmpty(directory, projectPath) {
51319
- const shared = join12(projectPath, ".agents");
51659
+ const shared = join13(projectPath, ".agents");
51320
51660
  let path = directory;
51321
- while (path !== projectPath && (basename4(path) === "skills" || path === shared)) {
51322
- if (readdirSync7(path).length > 0) {
51661
+ while (path !== projectPath && (basename5(path) === "skills" || path === shared)) {
51662
+ if (readdirSync8(path).length > 0) {
51323
51663
  return;
51324
51664
  }
51325
51665
  rmSync6(path, { recursive: true, force: true });
51326
- path = dirname6(path);
51666
+ path = dirname7(path);
51327
51667
  }
51328
51668
  }
51329
51669
  function writeSkill(directories, version) {
51330
51670
  const files = skillFiles(version);
51331
51671
  return directories.map((directory) => {
51332
- const replaced = existsSync12(directory);
51672
+ const replaced = existsSync13(directory);
51333
51673
  rmSync6(directory, { recursive: true, force: true });
51334
51674
  for (const [name, contents] of files) {
51335
- const path = join12(directory, name);
51336
- mkdirSync5(dirname6(path), { recursive: true });
51675
+ const path = join13(directory, name);
51676
+ mkdirSync5(dirname7(path), { recursive: true });
51337
51677
  writeFileSync7(path, contents, "utf8");
51338
51678
  }
51339
51679
  return { path: directory, replaced };
@@ -51341,18 +51681,18 @@ function writeSkill(directories, version) {
51341
51681
  }
51342
51682
 
51343
51683
  // src/version.ts
51344
- import { readFileSync as readFileSync11 } from "node:fs";
51345
- import { dirname as dirname7, join as join13 } from "node:path";
51684
+ import { readFileSync as readFileSync12 } from "node:fs";
51685
+ import { dirname as dirname8, join as join14 } from "node:path";
51346
51686
  import { fileURLToPath as fileURLToPath2 } from "node:url";
51347
51687
  function getLocalVersion() {
51348
- const currentDirectory = dirname7(fileURLToPath2(import.meta.url));
51688
+ const currentDirectory = dirname8(fileURLToPath2(import.meta.url));
51349
51689
  const packageCandidates = [
51350
- join13(currentDirectory, "..", "package.json"),
51351
- join13(currentDirectory, "..", "..", "package.json")
51690
+ join14(currentDirectory, "..", "package.json"),
51691
+ join14(currentDirectory, "..", "..", "package.json")
51352
51692
  ];
51353
51693
  for (const packagePath of packageCandidates) {
51354
51694
  try {
51355
- const value = JSON.parse(readFileSync11(packagePath, "utf-8"));
51695
+ const value = JSON.parse(readFileSync12(packagePath, "utf-8"));
51356
51696
  if (typeof value === "object" && value !== null && "name" in value && value.name === "gdharness" && "version" in value && typeof value.version === "string") {
51357
51697
  return value.version;
51358
51698
  }
@@ -51370,7 +51710,7 @@ class UsageError extends Refusal {
51370
51710
  function projectArgument(at) {
51371
51711
  const named = args.slice(at).find((value) => !value.startsWith("--"));
51372
51712
  const projectPath = resolve8(named ?? ".");
51373
- if (!existsSync21(join28(projectPath, "project.godot"))) {
51713
+ if (!existsSync22(join29(projectPath, "project.godot"))) {
51374
51714
  throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
51375
51715
  }
51376
51716
  return projectPath;
@@ -51415,7 +51755,7 @@ async function registerRuntime(godot, projectPath) {
51415
51755
  return;
51416
51756
  }
51417
51757
  console.log(`${RUNTIME_AUTOLOAD.name} autoload left at ${named}, which this project registers itself`);
51418
- if (!existsSync21(join28(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
51758
+ if (!existsSync22(join29(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
51419
51759
  console.log(` that file is not in this project, so it boots with a missing script: restore it, or point the entry at res://${RUNTIME_AUTOLOAD.path}`);
51420
51760
  }
51421
51761
  }
@@ -51547,7 +51887,7 @@ async function setup() {
51547
51887
  console.log("no harness configured; name one yourself, and gdharness harnesses lists them all");
51548
51888
  }
51549
51889
  if (!args.includes("--no-skill")) {
51550
- const directories = skillDirectories(chosen.harnesses, projectPath, existsSync21);
51890
+ const directories = skillDirectories(chosen.harnesses, projectPath, existsSync22);
51551
51891
  for (const written of writeSkill(directories, getLocalVersion())) {
51552
51892
  console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
51553
51893
  }
@@ -51661,7 +52001,7 @@ async function upgrade() {
51661
52001
  moved.push(took.from);
51662
52002
  }
51663
52003
  }
51664
- for (const written of writeSkill(skillDirectories(already, projectPath, existsSync21), version)) {
52004
+ for (const written of writeSkill(skillDirectories(already, projectPath, existsSync22), version)) {
51665
52005
  console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
51666
52006
  }
51667
52007
  console.log(`