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/index.js CHANGED
@@ -3356,27 +3356,25 @@ function defectReport(where, error, context = {}) {
3356
3356
  }
3357
3357
 
3358
3358
  // src/server.ts
3359
- import { execFile as execFile5, spawn as spawn4 } from "node:child_process";
3360
3359
  import { randomUUID as randomUUID2 } from "node:crypto";
3361
3360
  import {
3362
3361
  appendFileSync as appendFileSync2,
3363
3362
  closeSync as closeSync2,
3364
3363
  constants,
3365
3364
  copyFileSync,
3366
- existsSync as existsSync17,
3365
+ existsSync as existsSync18,
3367
3366
  fstatSync,
3368
3367
  mkdirSync as mkdirSync14,
3369
3368
  openSync as openSync3,
3370
- readdirSync as readdirSync9,
3371
- readFileSync as readFileSync21,
3369
+ readdirSync as readdirSync10,
3370
+ readFileSync as readFileSync22,
3372
3371
  readSync as readSync2,
3373
3372
  rmdirSync as rmdirSync2,
3374
- statSync as statSync9
3373
+ statSync as statSync10
3375
3374
  } from "node:fs";
3376
- import { basename as basename5, dirname as dirname16, extname as extname3, isAbsolute as isAbsolute3, join as join23, normalize as normalize2, relative as relative4 } from "node:path";
3375
+ import { basename as basename6, dirname as dirname17, extname as extname3, isAbsolute as isAbsolute4, join as join24, normalize as normalize2, relative as relative4 } from "node:path";
3377
3376
  import { setTimeout as delay4 } from "node:timers/promises";
3378
3377
  import { fileURLToPath as fileURLToPath4 } from "node:url";
3379
- import { promisify as promisify5 } from "node:util";
3380
3378
 
3381
3379
  // node_modules/@modelcontextprotocol/server/dist/chunk-Br0eD_fh.mjs
3382
3380
  var __create2 = Object.create;
@@ -24886,7 +24884,7 @@ import { mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync a
24886
24884
  import { dirname as dirname2, join as join3 } from "node:path";
24887
24885
 
24888
24886
  // src/runtime-client.ts
24889
- import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3, statSync, unlinkSync } from "node:fs";
24887
+ import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3, statSync as statSync2, unlinkSync } from "node:fs";
24890
24888
  import { createConnection } from "node:net";
24891
24889
  import { tmpdir as tmpdir3 } from "node:os";
24892
24890
  import { dirname, join as join2, resolve as resolve2 } from "node:path";
@@ -24991,7 +24989,10 @@ function engineFlagsAmongGameArgs(args) {
24991
24989
  return args.filter((one) => Object.hasOwn(ENGINE_FLAGS, one) || Object.hasOwn(SET_BY_ARGUMENT, one));
24992
24990
  }
24993
24991
  function runArguments(options) {
24994
- const args = options.headless ? ["--headless", "--path", options.projectPath] : ["--path", options.projectPath];
24992
+ const args = [
24993
+ ...options.headless ? ["--headless"] : [],
24994
+ ...options.projectPath === null ? [] : ["--path", options.projectPath]
24995
+ ];
24995
24996
  if (options.silent === true && !options.headless) {
24996
24997
  args.push("--audio-driver", "Dummy");
24997
24998
  }
@@ -25049,7 +25050,7 @@ function environmentFor(options, variables = process.env) {
25049
25050
  var SAVES_NOT_MOVED_NOTE = "On macOS the engine reads user:// off HOME and ignores the variables that move it elsewhere, measured on Godot 4.7.2, so this run wrote its saves where the player keeps theirs. A suite that saves a game has written into the same folder as the copy being played, and one that tidies up after itself may have removed real saves.";
25050
25051
 
25051
25052
  // src/paths.ts
25052
- import { realpathSync } from "node:fs";
25053
+ import { realpathSync, statSync } from "node:fs";
25053
25054
  import { isAbsolute, relative, resolve, sep } from "node:path";
25054
25055
  var RESOURCE_SCHEME = "res://";
25055
25056
  var OTHER_SCHEME = /^[a-zA-Z][a-zA-Z\d+.-]+:\/\//;
@@ -25112,14 +25113,62 @@ function resolveWithinProject(projectPath, candidatePath) {
25112
25113
  }
25113
25114
  return { ok: true, absolutePath, relativePath };
25114
25115
  }
25116
+ function exportedBuild(projectPath, candidatePath) {
25117
+ const found = resolveWithinProject(projectPath, candidatePath);
25118
+ if (!found.ok) {
25119
+ return found;
25120
+ }
25121
+ let real;
25122
+ try {
25123
+ real = realpathSync.native(found.absolutePath);
25124
+ } catch {
25125
+ return {
25126
+ ok: false,
25127
+ reason: `There is no ${found.relativePath} in the project to start: export the build first, with project_export run.`
25128
+ };
25129
+ }
25130
+ if (!isWithinRoot(realPathOr(projectPath), real)) {
25131
+ return {
25132
+ ok: false,
25133
+ reason: `${found.relativePath} leads outside the project, to ${real}, so it is not started.`
25134
+ };
25135
+ }
25136
+ if (!statSync(real).isFile()) {
25137
+ return { ok: false, reason: `${found.relativePath} is not a file, so there is nothing to start.` };
25138
+ }
25139
+ return { ok: true, absolutePath: real, relativePath: found.relativePath };
25140
+ }
25115
25141
 
25116
25142
  // src/process-children.ts
25117
- import { execFile } from "node:child_process";
25118
25143
  import { basename } from "node:path";
25119
- import { promisify } from "node:util";
25144
+
25145
+ // src/spawn.ts
25146
+ import {
25147
+ execFile,
25148
+ execFileSync,
25149
+ spawn,
25150
+ spawnSync
25151
+ } from "node:child_process";
25152
+ function spawnHidden(command, args, options = {}) {
25153
+ return spawn(command, [...args], { ...options, windowsHide: true });
25154
+ }
25155
+ function runHidden(command, args, options = {}) {
25156
+ return new Promise((resolve, reject) => {
25157
+ execFile(command, [...args], { ...options, encoding: "utf8", windowsHide: true }, (error, stdout, stderr) => {
25158
+ if (error !== null) {
25159
+ Object.assign(error, { stdout, stderr });
25160
+ reject(error);
25161
+ return;
25162
+ }
25163
+ resolve({ stdout, stderr });
25164
+ });
25165
+ });
25166
+ }
25167
+ function spawnSyncHidden(command, args, options = {}) {
25168
+ return spawnSync(command, [...args], { ...options, windowsHide: true });
25169
+ }
25120
25170
 
25121
25171
  // src/windows-ask.ts
25122
- import { spawn } from "node:child_process";
25123
25172
  import { tmpdir as tmpdir2 } from "node:os";
25124
25173
  import process6 from "node:process";
25125
25174
  var SCRIPT = [
@@ -25168,14 +25217,14 @@ function running() {
25168
25217
  if (helper?.exitCode === null && helper.signalCode === null) {
25169
25218
  return helper;
25170
25219
  }
25171
- const child = spawn("powershell.exe", [
25220
+ const child = spawnHidden("powershell.exe", [
25172
25221
  "-NoProfile",
25173
25222
  "-NonInteractive",
25174
25223
  "-ExecutionPolicy",
25175
25224
  "Bypass",
25176
25225
  "-EncodedCommand",
25177
25226
  Buffer.from(SCRIPT, "utf16le").toString("base64")
25178
- ], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true, cwd: tmpdir2() });
25227
+ ], { cwd: tmpdir2() });
25179
25228
  buffered = "";
25180
25229
  child.stdout.setEncoding("utf8");
25181
25230
  child.stdout.on("data", (chunk) => {
@@ -25268,7 +25317,6 @@ function askWindows(script, withinMs) {
25268
25317
  }
25269
25318
 
25270
25319
  // src/process-children.ts
25271
- var run = promisify(execFile);
25272
25320
  async function startTimesOf(pids) {
25273
25321
  const began = new Map;
25274
25322
  if (pids.length === 0) {
@@ -25303,10 +25351,9 @@ async function startTimesOf(pids) {
25303
25351
  return began;
25304
25352
  }
25305
25353
  function printedBy(command, args, env) {
25306
- return new Promise((resolve) => {
25307
- execFile(command, [...args], { encoding: "utf8", timeout: ASK_TIMEOUT_MS, env }, (_error, stdout) => {
25308
- resolve(stdout);
25309
- });
25354
+ return runHidden(command, args, { timeout: ASK_TIMEOUT_MS, env }).then(({ stdout }) => stdout, (error) => {
25355
+ const printed = error.stdout;
25356
+ return typeof printed === "string" ? printed : "";
25310
25357
  });
25311
25358
  }
25312
25359
  var ASK_TIMEOUT_MS = 15000;
@@ -25374,10 +25421,18 @@ function linked(tree, child, parent) {
25374
25421
  }
25375
25422
  return from.startedAt >= to.startedAt;
25376
25423
  }
25424
+ function isConsoleHost(listed) {
25425
+ const executable = commandWords(listed.command)[0];
25426
+ if (executable === undefined) {
25427
+ return false;
25428
+ }
25429
+ const host = `${process.env["SystemRoot"] ?? "C:\\Windows"}\\System32\\conhost.exe`;
25430
+ return executable.replace(/^\\\?\?\\/, "").replaceAll("/", "\\").toLowerCase() === host.toLowerCase();
25431
+ }
25377
25432
  function childrenIn(tree, pid) {
25378
25433
  const children = [];
25379
- for (const child of tree.keys()) {
25380
- if (linked(tree, child, pid)) {
25434
+ for (const [child, listed] of tree) {
25435
+ if (linked(tree, child, pid) && !isConsoleHost(listed)) {
25381
25436
  children.push(child);
25382
25437
  }
25383
25438
  }
@@ -25500,7 +25555,7 @@ function listWindows() {
25500
25555
  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);
25501
25556
  }
25502
25557
  async function askPosix() {
25503
- const { stdout } = await run("ps", ["-e", "-o", "pid=,ppid=,args="], {
25558
+ const { stdout } = await runHidden("ps", ["-e", "-o", "pid=,ppid=,args="], {
25504
25559
  timeout: ASK_TIMEOUT_MS,
25505
25560
  maxBuffer: 16 * 1024 * 1024
25506
25561
  });
@@ -25636,7 +25691,7 @@ function parseAnnouncement(file, pid) {
25636
25691
  };
25637
25692
  let announcedAt;
25638
25693
  try {
25639
- announcedAt = statSync(file).mtimeMs;
25694
+ announcedAt = statSync2(file).mtimeMs;
25640
25695
  } catch {}
25641
25696
  const when = announcedAt === undefined ? {} : { announcedAt };
25642
25697
  if (protocol !== RUNTIME_PROTOCOL) {
@@ -25758,7 +25813,7 @@ function announcedIn(directory) {
25758
25813
  entries.push({ file, pid, running });
25759
25814
  if (running) {
25760
25815
  try {
25761
- candidates.push({ file, pid, writtenAt: statSync(file).mtimeMs });
25816
+ candidates.push({ file, pid, writtenAt: statSync2(file).mtimeMs });
25762
25817
  } catch {}
25763
25818
  }
25764
25819
  }
@@ -25816,7 +25871,7 @@ function sweepErrorReport(path, pid, now = Date.now()) {
25816
25871
  return;
25817
25872
  }
25818
25873
  try {
25819
- if (now - statSync(path).mtimeMs >= ERROR_REPORT_KEEP_MS) {
25874
+ if (now - statSync2(path).mtimeMs >= ERROR_REPORT_KEEP_MS) {
25820
25875
  unlinkSync(path);
25821
25876
  }
25822
25877
  } catch {}
@@ -26256,7 +26311,7 @@ function callProgress() {
26256
26311
  }
26257
26312
 
26258
26313
  // src/class-cache.ts
26259
- import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync7, statSync as statSync2 } from "node:fs";
26314
+ import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync7, statSync as statSync3 } from "node:fs";
26260
26315
  import { join as join6, posix } from "node:path";
26261
26316
 
26262
26317
  // src/gdscript-source.ts
@@ -27233,7 +27288,7 @@ function uncachedClassNote(types) {
27233
27288
  function cacheWrittenAt(projectPath) {
27234
27289
  const cache = join6(projectPath, ".godot", "global_script_class_cache.cfg");
27235
27290
  try {
27236
- return statSync2(cache).mtimeMs;
27291
+ return statSync3(cache).mtimeMs;
27237
27292
  } catch {
27238
27293
  return null;
27239
27294
  }
@@ -27245,7 +27300,7 @@ function declaredSince(projectPath, paths, names, since) {
27245
27300
  return false;
27246
27301
  }
27247
27302
  try {
27248
- return statSync2(join6(projectPath, path.slice("res://".length))).mtimeMs > since;
27303
+ return statSync3(join6(projectPath, path.slice("res://".length))).mtimeMs > since;
27249
27304
  } catch {
27250
27305
  return false;
27251
27306
  }
@@ -27271,8 +27326,40 @@ function unseenByEditor(projectPath, editorHolds) {
27271
27326
  }
27272
27327
 
27273
27328
  // src/class-note.ts
27274
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "node:fs";
27329
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync8 } from "node:fs";
27275
27330
  import { dirname as dirname5, join as join7 } from "node:path";
27331
+
27332
+ // src/whole-file.ts
27333
+ import { renameSync, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "node:fs";
27334
+ var HELD_FILE_MS = 1000;
27335
+ var HELD_FILE_RETRY_MS = 10;
27336
+ var HELD_BY_A_READER = new Set(["EPERM", "EACCES", "EBUSY"]);
27337
+ function writeWhole(path, text) {
27338
+ const partial = `${path}.${process.pid}.partial`;
27339
+ try {
27340
+ writeFileSync5(partial, text, "utf8");
27341
+ } catch {
27342
+ return false;
27343
+ }
27344
+ const until = Date.now() + HELD_FILE_MS;
27345
+ for (;; ) {
27346
+ try {
27347
+ renameSync(partial, path);
27348
+ return true;
27349
+ } catch (error) {
27350
+ const code = error.code ?? "";
27351
+ if (!HELD_BY_A_READER.has(code) || Date.now() >= until) {
27352
+ try {
27353
+ rmSync3(partial, { force: true });
27354
+ } catch {}
27355
+ return false;
27356
+ }
27357
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, HELD_FILE_RETRY_MS);
27358
+ }
27359
+ }
27360
+ }
27361
+
27362
+ // src/class-note.ts
27276
27363
  function classNotePath(projectPath) {
27277
27364
  return join7(projectPath, ".godot", "gdharness-classes.json");
27278
27365
  }
@@ -27284,21 +27371,36 @@ function readClassNote(projectPath) {
27284
27371
  return null;
27285
27372
  }
27286
27373
  const pid = readNumber(fields, "shortListEditorPid");
27374
+ const unfinished = unfinishedScanIn(fields["unseenBeforeAnUnfinishedScan"]);
27287
27375
  return {
27288
27376
  rebuiltWith: rebuiltWith.map(String),
27289
- ...Object.hasOwn(fields, "shortListEditorPid") ? { shortListEditorPid: pid ?? null } : {}
27377
+ ...Object.hasOwn(fields, "shortListEditorPid") ? { shortListEditorPid: pid ?? null } : {},
27378
+ ...unfinished === null ? {} : { unseenBeforeAnUnfinishedScan: unfinished }
27290
27379
  };
27291
27380
  } catch {
27292
27381
  return null;
27293
27382
  }
27294
27383
  }
27384
+ function unfinishedScanIn(value) {
27385
+ if (typeof value !== "object" || value === null) {
27386
+ return null;
27387
+ }
27388
+ const fields = asParams(value);
27389
+ const classes = readArray(fields, "classes");
27390
+ if (classes === undefined || !classes.every((name) => typeof name === "string")) {
27391
+ return null;
27392
+ }
27393
+ return { editorPid: readNumber(fields, "editorPid") ?? null, classes: classes.map(String) };
27394
+ }
27295
27395
  function writeClassNote(projectPath, note) {
27396
+ const path = classNotePath(projectPath);
27296
27397
  try {
27297
- const path = classNotePath(projectPath);
27298
27398
  mkdirSync5(dirname5(path), { recursive: true });
27299
- writeFileSync5(path, `${JSON.stringify(note, null, 2)}
27300
- `, "utf8");
27301
- } catch {}
27399
+ } catch {
27400
+ return;
27401
+ }
27402
+ writeWhole(path, `${JSON.stringify(note, null, 2)}
27403
+ `);
27302
27404
  }
27303
27405
 
27304
27406
  // src/config-pin.ts
@@ -27329,7 +27431,7 @@ function configDisagrees(projectPath, serverVersion) {
27329
27431
  }
27330
27432
 
27331
27433
  // src/cut-test-run.ts
27332
- import { existsSync as existsSync5, mkdirSync as mkdirSync6, readFileSync as readFileSync10, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "node:fs";
27434
+ import { existsSync as existsSync5, mkdirSync as mkdirSync6, readFileSync as readFileSync10, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "node:fs";
27333
27435
  import { dirname as dirname6, join as join9 } from "node:path";
27334
27436
  function keptAt(projectPath) {
27335
27437
  return join9(projectPath, ".godot", "gdharness-reports", "cut-run.json");
@@ -27349,10 +27451,10 @@ function takeCutTestRun(projectPath) {
27349
27451
  }
27350
27452
  try {
27351
27453
  const run = JSON.parse(readFileSync10(path, "utf8"));
27352
- rmSync3(path, { force: true });
27454
+ rmSync4(path, { force: true });
27353
27455
  return typeof run.cutAt === "string" && Array.isArray(run.paths) && run.paths.every((path) => typeof path === "string") ? run : null;
27354
27456
  } catch {
27355
- rmSync3(path, { force: true });
27457
+ rmSync4(path, { force: true });
27356
27458
  return null;
27357
27459
  }
27358
27460
  }
@@ -28172,8 +28274,8 @@ import {
28172
28274
  openSync,
28173
28275
  readFileSync as readFileSync11,
28174
28276
  readSync,
28175
- rmSync as rmSync4,
28176
- statSync as statSync3,
28277
+ rmSync as rmSync5,
28278
+ statSync as statSync4,
28177
28279
  writeFileSync as writeFileSync7
28178
28280
  } from "node:fs";
28179
28281
  import { dirname as dirname7, join as join10 } from "node:path";
@@ -28433,8 +28535,8 @@ function clearEditorLog(projectPath) {
28433
28535
  try {
28434
28536
  const path = editorLogPath(projectPath);
28435
28537
  mkdirSync7(dirname7(path), { recursive: true });
28436
- rmSync4(path, { force: true });
28437
- rmSync4(editorLogNotePath(projectPath), { force: true });
28538
+ rmSync5(path, { force: true });
28539
+ rmSync5(editorLogNotePath(projectPath), { force: true });
28438
28540
  } catch {}
28439
28541
  }
28440
28542
  function writeEditorLogNote(projectPath, pid) {
@@ -28521,7 +28623,7 @@ function foldProgress(text) {
28521
28623
  }
28522
28624
  var MOST_CONSOLE_BYTES = 8 * 1024 * 1024;
28523
28625
  function theFirstOf(path) {
28524
- const whole = statSync3(path).size;
28626
+ const whole = statSync4(path).size;
28525
28627
  if (whole <= MOST_CONSOLE_BYTES) {
28526
28628
  return { text: readFileSync11(path, "utf8") };
28527
28629
  }
@@ -28679,15 +28781,13 @@ function linesBefore(entries, at, collapsed, wanted) {
28679
28781
  }
28680
28782
 
28681
28783
  // src/engine-run.ts
28682
- import { spawn as spawn2 } from "node:child_process";
28683
28784
  function runEngine(executable, args, options = {}) {
28684
28785
  return new Promise((resolve) => {
28685
28786
  const log = new GameLog;
28686
28787
  let failure = null;
28687
28788
  const signal = callSignal();
28688
- const child = spawn2(executable, [...args], {
28789
+ const child = spawnHidden(executable, args, {
28689
28790
  stdio: ["ignore", "pipe", "pipe"],
28690
- windowsHide: true,
28691
28791
  ...signal === undefined ? {} : { signal }
28692
28792
  });
28693
28793
  child.stdout.on("data", (chunk) => {
@@ -28804,6 +28904,16 @@ function anEditorIsStillComing(listeningSince, launchedEditorIsAlive, now = Date
28804
28904
  return mayYetConnect(listeningSince, now) || launchedEditorIsAlive;
28805
28905
  }
28806
28906
 
28907
+ class BridgeTimeout extends Error {
28908
+ toolName;
28909
+ waitedMs;
28910
+ constructor(toolName, waitedMs) {
28911
+ super(`Tool ${toolName} timed out after ${waitedMs}ms`);
28912
+ this.toolName = toolName;
28913
+ this.waitedMs = waitedMs;
28914
+ }
28915
+ }
28916
+
28807
28917
  class GodotBridge extends EventEmitter {
28808
28918
  listeningSince = null;
28809
28919
  httpServer = null;
@@ -29141,7 +29251,7 @@ class GodotBridge extends EventEmitter {
29141
29251
  return new Promise((resolve, reject) => {
29142
29252
  const timeout = setTimeout(() => {
29143
29253
  this.pendingRequests.delete(requestId);
29144
- reject(new Error(`Tool ${toolName} timed out after ${this.timeoutMs}ms`));
29254
+ reject(new BridgeTimeout(toolName, this.timeoutMs));
29145
29255
  }, this.timeoutMs);
29146
29256
  this.pendingRequests.set(requestId, {
29147
29257
  toolName,
@@ -29357,13 +29467,11 @@ function getDefaultBridge(ownProject = null) {
29357
29467
  }
29358
29468
 
29359
29469
  // src/godot-path.ts
29360
- import { execFile as execFile2 } from "node:child_process";
29361
29470
  import { existsSync as existsSync8 } from "node:fs";
29362
29471
  import { normalize } from "node:path";
29363
- import { promisify as promisify2 } from "node:util";
29364
29472
 
29365
29473
  // src/detection.ts
29366
- import { existsSync as existsSync7, readdirSync as readdirSync3, statSync as statSync4 } from "node:fs";
29474
+ import { existsSync as existsSync7, readdirSync as readdirSync3, statSync as statSync5 } from "node:fs";
29367
29475
  import { homedir as homedir2 } from "node:os";
29368
29476
  import { join as join11 } from "node:path";
29369
29477
  function resolveHomeDirectory() {
@@ -29391,7 +29499,7 @@ function scanDirectoryForGodotBinaries(directory, platform) {
29391
29499
  }
29392
29500
  const fullPath = join11(directory, name);
29393
29501
  try {
29394
- const stat = statSync4(fullPath);
29502
+ const stat = statSync5(fullPath);
29395
29503
  if (stat.isFile()) {
29396
29504
  matches.push({ name, mtime: stat.mtimeMs });
29397
29505
  }
@@ -29448,8 +29556,6 @@ function godotCandidates(platform, home) {
29448
29556
  }
29449
29557
 
29450
29558
  // src/godot-path.ts
29451
- var run2 = promisify2(execFile2);
29452
-
29453
29559
  class GodotLocator {
29454
29560
  answered = new Map;
29455
29561
  found = null;
@@ -29505,7 +29611,7 @@ class GodotLocator {
29505
29611
  reason = "does not exist";
29506
29612
  } else {
29507
29613
  try {
29508
- await run2(path, ["--version"]);
29614
+ await runHidden(path, ["--version"]);
29509
29615
  reason = null;
29510
29616
  } catch (error) {
29511
29617
  reason = `does not answer --version: ${errorMessage(error)}`;
@@ -29517,33 +29623,114 @@ class GodotLocator {
29517
29623
  }
29518
29624
 
29519
29625
  // src/headless.ts
29520
- import { existsSync as existsSync11, readFileSync as readFileSync13, realpathSync as realpathSync2, writeFileSync as writeFileSync9 } from "node:fs";
29521
- import { dirname as dirname8, join as join14 } from "node:path";
29626
+ import { existsSync as existsSync12, readFileSync as readFileSync14, realpathSync as realpathSync2, writeFileSync as writeFileSync9 } from "node:fs";
29627
+ import { dirname as dirname9, join as join15 } from "node:path";
29628
+
29629
+ // src/library-copies.ts
29630
+ import { existsSync as existsSync9, readdirSync as readdirSync4, readFileSync as readFileSync12, renameSync as renameSync2, unlinkSync as unlinkSync2 } from "node:fs";
29631
+ import { basename as basename2, dirname as dirname8, isAbsolute as isAbsolute3, join as join12 } from "node:path";
29632
+ function freeLibraryCopies(projectPath, platform = process.platform) {
29633
+ const setAside = [];
29634
+ const swept = [];
29635
+ if (platform !== "win32") {
29636
+ return { setAside, swept };
29637
+ }
29638
+ for (const library of windowsLibraries(projectPath)) {
29639
+ const folder = dirname8(library);
29640
+ const copy = `~${basename2(library)}`;
29641
+ if (existsSync9(join12(folder, copy))) {
29642
+ const aside = join12(folder, `${copy}.aside-${process.pid}-${Date.now()}`);
29643
+ try {
29644
+ renameSync2(join12(folder, copy), aside);
29645
+ setAside.push(aside);
29646
+ } catch {}
29647
+ }
29648
+ let names;
29649
+ try {
29650
+ names = readdirSync4(folder);
29651
+ } catch {
29652
+ continue;
29653
+ }
29654
+ for (const name of names) {
29655
+ if (name.length > copy.length && name.toLowerCase().startsWith(copy.toLowerCase())) {
29656
+ try {
29657
+ unlinkSync2(join12(folder, name));
29658
+ swept.push(join12(folder, name));
29659
+ } catch {}
29660
+ }
29661
+ }
29662
+ }
29663
+ return { setAside, swept };
29664
+ }
29665
+ function windowsLibraries(projectPath) {
29666
+ const listed = readOrNull(join12(projectPath, ".godot", "extension_list.cfg"));
29667
+ if (listed === null) {
29668
+ return [];
29669
+ }
29670
+ const libraries = new Set;
29671
+ for (const line of listed.split(/\r?\n/)) {
29672
+ const manifest = resolveResource(projectPath, projectPath, line.trim());
29673
+ const text = manifest === null ? null : readOrNull(manifest);
29674
+ if (manifest === null || text === null) {
29675
+ continue;
29676
+ }
29677
+ let inLibraries = false;
29678
+ for (const row of text.split(/\r?\n/)) {
29679
+ const section = /^\s*\[([^\]]+)\]/.exec(row)?.[1];
29680
+ if (section !== undefined) {
29681
+ inLibraries = section.trim() === "libraries";
29682
+ continue;
29683
+ }
29684
+ const named = inLibraries ? /^\s*windows[\w.]*\s*=\s*"([^"]+)"/.exec(row)?.[1] : undefined;
29685
+ const library = named === undefined ? null : resolveResource(projectPath, dirname8(manifest), named);
29686
+ if (library !== null) {
29687
+ libraries.add(library);
29688
+ }
29689
+ }
29690
+ }
29691
+ return [...libraries];
29692
+ }
29693
+ function resolveResource(projectPath, relativeTo, path) {
29694
+ if (path === "") {
29695
+ return null;
29696
+ }
29697
+ if (path.startsWith("res://")) {
29698
+ return join12(projectPath, path.slice("res://".length));
29699
+ }
29700
+ return isAbsolute3(path) ? path : join12(relativeTo, path);
29701
+ }
29702
+ function readOrNull(path) {
29703
+ try {
29704
+ return readFileSync12(path, "utf8");
29705
+ } catch {
29706
+ return null;
29707
+ }
29708
+ }
29522
29709
 
29523
29710
  // src/reimport.ts
29524
29711
  import { createHash as createHash2 } from "node:crypto";
29525
- import { existsSync as existsSync9, readFileSync as readFileSync12, rmSync as rmSync5, statSync as statSync5, utimesSync, writeFileSync as writeFileSync8 } from "node:fs";
29526
- import { basename as basename2, join as join12 } from "node:path";
29712
+ import { existsSync as existsSync10, readFileSync as readFileSync13, rmSync as rmSync6, statSync as statSync6, utimesSync, writeFileSync as writeFileSync8 } from "node:fs";
29713
+ import { basename as basename3, join as join13 } from "node:path";
29527
29714
  function forceNextImport(projectPath, resourcePath) {
29528
29715
  const relative = resourcePath.replace(/^res:\/\//, "");
29529
- const sidecar = join12(projectPath, `${relative}.import`);
29530
- if (!existsSync9(sidecar)) {
29716
+ const sidecar = join13(projectPath, `${relative}.import`);
29717
+ if (!existsSync10(sidecar)) {
29531
29718
  return;
29532
29719
  }
29533
- const { atime, mtimeMs } = statSync5(sidecar);
29534
- writeFileSync8(sidecar, readFileSync12(sidecar, "utf8").replace(/^valid=false\r?\n/m, ""));
29720
+ const { atime, mtimeMs } = statSync6(sidecar);
29721
+ writeFileSync8(sidecar, readFileSync13(sidecar, "utf8").replace(/^valid=false\r?\n/m, ""));
29535
29722
  utimesSync(sidecar, atime, new Date(mtimeMs + 2000));
29536
- rmSync5(importRecord(projectPath, resourcePath), { force: true });
29723
+ rmSync6(importRecord(projectPath, resourcePath), { force: true });
29537
29724
  }
29538
29725
  function importRecord(projectPath, resourcePath) {
29539
29726
  const digest = createHash2("md5").update(resourcePath).digest("hex");
29540
- return join12(projectPath, ".godot", "imported", `${basename2(resourcePath)}-${digest}.md5`);
29727
+ return join13(projectPath, ".godot", "imported", `${basename3(resourcePath)}-${digest}.md5`);
29541
29728
  }
29542
29729
 
29543
29730
  // src/scratch.ts
29544
- import { existsSync as existsSync10, mkdtempSync, readdirSync as readdirSync4, rmSync as rmSync6 } from "node:fs";
29731
+ import { existsSync as existsSync11, mkdtempSync, readdirSync as readdirSync5, rmSync as rmSync7 } from "node:fs";
29545
29732
  import { tmpdir as tmpdir4 } from "node:os";
29546
- import { join as join13 } from "node:path";
29733
+ import { join as join14 } from "node:path";
29547
29734
  import process7 from "node:process";
29548
29735
  var SWEPT = { recursive: true, force: true };
29549
29736
  var AGAIN_AFTER_MS = [250, 500, 1000, 2000, 4000, 8000, 16000];
@@ -29558,12 +29745,12 @@ var KINDS = [
29558
29745
  "tests"
29559
29746
  ];
29560
29747
  function scratchDirectory(kind) {
29561
- return mkdtempSync(join13(tmpdir4(), `gdharness-${kind}-${process7.pid}-`));
29748
+ return mkdtempSync(join14(tmpdir4(), `gdharness-${kind}-${process7.pid}-`));
29562
29749
  }
29563
29750
  function sweepAbandonedScratch(directory = tmpdir4()) {
29564
29751
  let entries;
29565
29752
  try {
29566
- entries = readdirSync4(directory);
29753
+ entries = readdirSync5(directory);
29567
29754
  } catch {
29568
29755
  return;
29569
29756
  }
@@ -29574,7 +29761,7 @@ function sweepAbandonedScratch(directory = tmpdir4()) {
29574
29761
  continue;
29575
29762
  }
29576
29763
  try {
29577
- remove(join13(directory, entry));
29764
+ remove(join14(directory, entry));
29578
29765
  } catch {}
29579
29766
  }
29580
29767
  }
@@ -29600,7 +29787,7 @@ function discard(...paths) {
29600
29787
  return left;
29601
29788
  }
29602
29789
  function remove(path) {
29603
- rmSync6(path, SWEPT);
29790
+ rmSync7(path, SWEPT);
29604
29791
  }
29605
29792
  function tryAgain(path, attempt) {
29606
29793
  if (attempt === 0) {
@@ -29612,7 +29799,7 @@ function tryAgain(path, attempt) {
29612
29799
  lastTryOnExit();
29613
29800
  }
29614
29801
  const after = AGAIN_AFTER_MS[attempt];
29615
- if (after === undefined || !existsSync10(path)) {
29802
+ if (after === undefined || !existsSync11(path)) {
29616
29803
  waiting.delete(path);
29617
29804
  if (after === undefined) {
29618
29805
  atExit.add(path);
@@ -29703,7 +29890,7 @@ function refusalsIn(log) {
29703
29890
  }
29704
29891
  function scriptFault(problems, script) {
29705
29892
  const spelled = (path) => `(${path.replaceAll("\\", "/").toLowerCase()}/`;
29706
- const directories = [...new Set([spelled(dirname8(script)), spelled(resolvedOr(dirname8(script)))])];
29893
+ const directories = [...new Set([spelled(dirname9(script)), spelled(resolvedOr(dirname9(script)))])];
29707
29894
  return problems.find((entry) => {
29708
29895
  const at = entry.detail.find((line) => line.startsWith("at: "))?.replaceAll("\\", "/").toLowerCase();
29709
29896
  return entry.severity === "error" && at !== undefined && directories.some((directory) => at.includes(directory));
@@ -29728,7 +29915,7 @@ function librariesNotCopied(messages) {
29728
29915
  }
29729
29916
  function extensionRetryNote(libraries, stillNotLoaded = []) {
29730
29917
  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.`;
29731
- 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.";
29918
+ 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.";
29732
29919
  }
29733
29920
  async function runImport(godotPath, projectPath, options = {}) {
29734
29921
  const once = options.once ?? importOnce;
@@ -29736,6 +29923,7 @@ async function runImport(godotPath, projectPath, options = {}) {
29736
29923
  for (const target of options.forcing ?? []) {
29737
29924
  forceNextImport(projectPath, target);
29738
29925
  }
29926
+ freeLibraryCopies(projectPath);
29739
29927
  return once(godotPath, projectPath);
29740
29928
  };
29741
29929
  const first = await pass();
@@ -29759,7 +29947,7 @@ async function importOnce(godotPath, projectPath) {
29759
29947
  ran = await runEngine(godotPath, [
29760
29948
  "--headless",
29761
29949
  "--log-file",
29762
- join14(logDir, "engine.log"),
29950
+ join15(logDir, "engine.log"),
29763
29951
  "--path",
29764
29952
  projectPath,
29765
29953
  "--import"
@@ -29779,13 +29967,13 @@ async function importOnce(godotPath, projectPath) {
29779
29967
  }
29780
29968
  async function runOperation(engine, operation, params, projectPath) {
29781
29969
  const paramsDir = scratchDirectory("params");
29782
- const paramsFile = join14(paramsDir, `${operation}.json`);
29783
- const answerFile = join14(paramsDir, "answer.json");
29970
+ const paramsFile = join15(paramsDir, `${operation}.json`);
29971
+ const answerFile = join15(paramsDir, "answer.json");
29784
29972
  writeFileSync9(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
29785
29973
  const args = [
29786
29974
  "--headless",
29787
29975
  "--log-file",
29788
- join14(paramsDir, "engine.log"),
29976
+ join15(paramsDir, "engine.log"),
29789
29977
  "--path",
29790
29978
  projectPath,
29791
29979
  "--script",
@@ -29855,10 +30043,10 @@ async function runOperation(engine, operation, params, projectPath) {
29855
30043
  };
29856
30044
  }
29857
30045
  function answerIn(file) {
29858
- if (!existsSync11(file)) {
30046
+ if (!existsSync12(file)) {
29859
30047
  return { kind: "none" };
29860
30048
  }
29861
- const text = readFileSync13(file, "utf8");
30049
+ const text = readFileSync14(file, "utf8");
29862
30050
  try {
29863
30051
  const parsed = JSON.parse(text);
29864
30052
  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)}` };
@@ -30431,20 +30619,20 @@ function askTheKeeper(address, line, withinMs) {
30431
30619
  }
30432
30620
 
30433
30621
  // src/lifecycle-log.ts
30434
- import { appendFileSync, mkdirSync as mkdirSync8, renameSync, statSync as statSync6 } from "node:fs";
30435
- import { dirname as dirname9, join as join15 } from "node:path";
30622
+ import { appendFileSync, mkdirSync as mkdirSync8, renameSync as renameSync3, statSync as statSync7 } from "node:fs";
30623
+ import { dirname as dirname10, join as join16 } from "node:path";
30436
30624
  import process8 from "node:process";
30437
30625
  function lifecycleLogPath() {
30438
- return join15(runtimeDirectory(), "servers.log");
30626
+ return join16(runtimeDirectory(), "servers.log");
30439
30627
  }
30440
30628
  var LIFECYCLE_LOG_LIMIT = 1e6;
30441
30629
  function writeLifecycle(event, fields = {}) {
30442
30630
  const path = lifecycleLogPath();
30443
30631
  try {
30444
- mkdirSync8(dirname9(path), { recursive: true });
30445
- if ((statSync6(path, { throwIfNoEntry: false })?.size ?? 0) > LIFECYCLE_LOG_LIMIT) {
30632
+ mkdirSync8(dirname10(path), { recursive: true });
30633
+ if ((statSync7(path, { throwIfNoEntry: false })?.size ?? 0) > LIFECYCLE_LOG_LIMIT) {
30446
30634
  try {
30447
- renameSync(path, `${path}.1`);
30635
+ renameSync3(path, `${path}.1`);
30448
30636
  } catch {}
30449
30637
  }
30450
30638
  const line = {
@@ -30474,10 +30662,10 @@ function callsInFlight(calls, now = Date.now()) {
30474
30662
  }
30475
30663
 
30476
30664
  // src/lsp_client.ts
30477
- import { existsSync as existsSync12, realpathSync as realpathSync3 } from "node:fs";
30665
+ import { existsSync as existsSync13, realpathSync as realpathSync3 } from "node:fs";
30478
30666
  import { readFile, realpath } from "node:fs/promises";
30479
30667
  import { createConnection as createConnection3 } from "node:net";
30480
- import { dirname as dirname10, join as join16, resolve as resolve3 } from "node:path";
30668
+ import { dirname as dirname11, join as join17, resolve as resolve3 } from "node:path";
30481
30669
  import { setTimeout as delay3 } from "node:timers/promises";
30482
30670
  import { fileURLToPath, pathToFileURL } from "node:url";
30483
30671
  var DEFAULT_LSP_PORT = 6005;
@@ -30675,7 +30863,7 @@ class GodotLSPClient {
30675
30863
  if (this.initialized) {
30676
30864
  return;
30677
30865
  }
30678
- const rootPath = this.rootPath ?? dirname10(resolve3(filePath));
30866
+ const rootPath = this.rootPath ?? dirname11(resolve3(filePath));
30679
30867
  await this.initialize(rootPath);
30680
30868
  }
30681
30869
  async sendRequest(method, params, timeoutMs = this.timeouts.requestMs) {
@@ -31233,7 +31421,7 @@ async function resolveLSPPaths(projectPathValue, scriptPathValue) {
31233
31421
  } catch {
31234
31422
  throw new ArgumentRefusal(`Project path does not exist: ${requestedProjectPath}`);
31235
31423
  }
31236
- if (!existsSync12(join16(projectPath, "project.godot"))) {
31424
+ if (!existsSync13(join17(projectPath, "project.godot"))) {
31237
31425
  throw new ArgumentRefusal(`Not a Godot project: ${projectPath}. Point projectPath at the directory holding project.godot.`);
31238
31426
  }
31239
31427
  const contained = resolveWithinProject(projectPath, scriptPathValue);
@@ -31303,7 +31491,6 @@ async function handleLSPTool(client, toolName, args) {
31303
31491
  }
31304
31492
 
31305
31493
  // src/outside.ts
31306
- import { spawn as spawn3 } from "node:child_process";
31307
31494
  import { fileURLToPath as fileURLToPath2 } from "node:url";
31308
31495
  function changesFrom(env, base) {
31309
31496
  const changes = {};
@@ -31337,9 +31524,8 @@ function readLaunched(printed) {
31337
31524
  }
31338
31525
  async function launchOutsideTheTree(spec, runtime = process.execPath, keeper = KEEPER_SCRIPT) {
31339
31526
  return await new Promise((resolve) => {
31340
- const launcher = spawn3(runtime, [keeper, "launch"], {
31341
- stdio: ["pipe", "pipe", "pipe"],
31342
- windowsHide: true
31527
+ const launcher = spawnHidden(runtime, [keeper, "launch"], {
31528
+ stdio: ["pipe", "pipe", "pipe"]
31343
31529
  });
31344
31530
  sendSpec(launcher, {
31345
31531
  command: spec.command,
@@ -31403,9 +31589,6 @@ function notListeningNote(servers, greetedAgoMs) {
31403
31589
  }
31404
31590
 
31405
31591
  // src/process-time.ts
31406
- import { execFile as execFile3 } from "node:child_process";
31407
- import { promisify as promisify3 } from "node:util";
31408
- var run3 = promisify3(execFile3);
31409
31592
  var ASK_TIMEOUT_MS2 = 6000;
31410
31593
  async function cpuSecondsOf(pid) {
31411
31594
  try {
@@ -31420,7 +31603,7 @@ async function secondsOnWindows(pid) {
31420
31603
  return Number.isFinite(seconds) ? seconds : undefined;
31421
31604
  }
31422
31605
  async function askPosix2(pid) {
31423
- const { stdout } = await run3("ps", ["-o", "time=", "-p", String(pid)], { timeout: ASK_TIMEOUT_MS2 });
31606
+ const { stdout } = await runHidden("ps", ["-o", "time=", "-p", String(pid)], { timeout: ASK_TIMEOUT_MS2 });
31424
31607
  return secondsFromClock(stdout.trim());
31425
31608
  }
31426
31609
  function secondsFromClock(printed) {
@@ -31440,8 +31623,8 @@ function secondsFromClock(printed) {
31440
31623
  }
31441
31624
 
31442
31625
  // src/project-scan.ts
31443
- import { existsSync as existsSync13, readdirSync as readdirSync5, readFileSync as readFileSync14 } from "node:fs";
31444
- import { extname, join as join17 } from "node:path";
31626
+ import { existsSync as existsSync14, readdirSync as readdirSync6, readFileSync as readFileSync15 } from "node:fs";
31627
+ import { extname, join as join18 } from "node:path";
31445
31628
  var SKIPPED = new Set(["node_modules"]);
31446
31629
  function skipped(name) {
31447
31630
  return name.startsWith(".") || SKIPPED.has(name);
@@ -31451,10 +31634,10 @@ function steppedOver(directory) {
31451
31634
  return gdignored(directory) || installedByUs(directory);
31452
31635
  }
31453
31636
  function gdignored(directory) {
31454
- return existsSync13(join17(directory, ".gdignore"));
31637
+ return existsSync14(join18(directory, ".gdignore"));
31455
31638
  }
31456
31639
  function installedByUs(directory) {
31457
- return existsSync13(join17(directory, VERSION_MARKER));
31640
+ return existsSync14(join18(directory, VERSION_MARKER));
31458
31641
  }
31459
31642
  var SCENE_EXTENSIONS = new Set(["tscn", "scn"]);
31460
31643
  var ASSET_EXTENSIONS = new Set([
@@ -31493,12 +31676,12 @@ function projectStructure(projectPath) {
31493
31676
  if (steppedOver(directory)) {
31494
31677
  return;
31495
31678
  }
31496
- for (const entry of readdirSync5(directory, { withFileTypes: true })) {
31679
+ for (const entry of readdirSync6(directory, { withFileTypes: true })) {
31497
31680
  if (skipped(entry.name)) {
31498
31681
  continue;
31499
31682
  }
31500
31683
  if (entry.isDirectory()) {
31501
- visit(join17(directory, entry.name));
31684
+ visit(join18(directory, entry.name));
31502
31685
  } else if (entry.isFile()) {
31503
31686
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
31504
31687
  if (SCENE_EXTENSIONS.has(extension)) {
@@ -31522,13 +31705,13 @@ function projectFiles(projectPath, extensions) {
31522
31705
  if (steppedOver(directory)) {
31523
31706
  return;
31524
31707
  }
31525
- for (const entry of readdirSync5(directory, { withFileTypes: true })) {
31708
+ for (const entry of readdirSync6(directory, { withFileTypes: true })) {
31526
31709
  if (skipped(entry.name)) {
31527
31710
  continue;
31528
31711
  }
31529
31712
  const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
31530
31713
  if (entry.isDirectory()) {
31531
- visit(join17(directory, entry.name), spelled);
31714
+ visit(join18(directory, entry.name), spelled);
31532
31715
  } else if (entry.isFile() && extensions.includes(extname(entry.name).toLowerCase())) {
31533
31716
  found.push(spelled);
31534
31717
  }
@@ -31544,20 +31727,20 @@ function scriptsWithoutUid(projectPath) {
31544
31727
  if (steppedOver(directory)) {
31545
31728
  return;
31546
31729
  }
31547
- for (const entry of readdirSync5(directory, { withFileTypes: true })) {
31730
+ for (const entry of readdirSync6(directory, { withFileTypes: true })) {
31548
31731
  if (skipped(entry.name)) {
31549
31732
  continue;
31550
31733
  }
31551
31734
  const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
31552
31735
  if (entry.isDirectory()) {
31553
- visit(join17(directory, entry.name), spelled);
31736
+ visit(join18(directory, entry.name), spelled);
31554
31737
  continue;
31555
31738
  }
31556
31739
  if (!entry.isFile()) {
31557
31740
  continue;
31558
31741
  }
31559
31742
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
31560
- if (CARRIES_A_UID.has(extension) && !existsSync13(join17(directory, `${entry.name}.uid`))) {
31743
+ if (CARRIES_A_UID.has(extension) && !existsSync14(join18(directory, `${entry.name}.uid`))) {
31561
31744
  found.push(spelled);
31562
31745
  }
31563
31746
  }
@@ -31585,11 +31768,11 @@ function projectTexts(projectPath) {
31585
31768
  if (steppedOver(directory)) {
31586
31769
  return;
31587
31770
  }
31588
- for (const entry of readdirSync5(directory, { withFileTypes: true })) {
31771
+ for (const entry of readdirSync6(directory, { withFileTypes: true })) {
31589
31772
  if (skipped(entry.name)) {
31590
31773
  continue;
31591
31774
  }
31592
- const absolute = join17(directory, entry.name);
31775
+ const absolute = join18(directory, entry.name);
31593
31776
  const spelled = `${prefix}${entry.name}`;
31594
31777
  if (entry.isDirectory()) {
31595
31778
  visit(absolute, `${spelled}/`);
@@ -31599,7 +31782,7 @@ function projectTexts(projectPath) {
31599
31782
  if (!entry.isFile() || BINARY_EXTENSIONS.has(extension)) {
31600
31783
  continue;
31601
31784
  }
31602
- const bytes = readFileSync14(absolute);
31785
+ const bytes = readFileSync15(absolute);
31603
31786
  if (!isBinary(bytes)) {
31604
31787
  found.push({ path: `res://${spelled}`, absolute, text: bytes.toString("utf8") });
31605
31788
  }
@@ -31615,11 +31798,11 @@ function ignoredTexts(projectPath) {
31615
31798
  return;
31616
31799
  }
31617
31800
  const under = ignored || gdignored(directory);
31618
- for (const entry of readdirSync5(directory, { withFileTypes: true })) {
31801
+ for (const entry of readdirSync6(directory, { withFileTypes: true })) {
31619
31802
  if (skipped(entry.name)) {
31620
31803
  continue;
31621
31804
  }
31622
- const absolute = join17(directory, entry.name);
31805
+ const absolute = join18(directory, entry.name);
31623
31806
  const spelled = `${prefix}${entry.name}`;
31624
31807
  if (entry.isDirectory()) {
31625
31808
  visit(absolute, `${spelled}/`, under);
@@ -31629,7 +31812,7 @@ function ignoredTexts(projectPath) {
31629
31812
  if (!under || !entry.isFile() || BINARY_EXTENSIONS.has(extension) || entry.name === ".gdignore") {
31630
31813
  continue;
31631
31814
  }
31632
- const bytes = readFileSync14(absolute);
31815
+ const bytes = readFileSync15(absolute);
31633
31816
  if (!isBinary(bytes)) {
31634
31817
  found.push({ path: `res://${spelled}`, absolute, text: bytes.toString("utf8") });
31635
31818
  }
@@ -31652,14 +31835,14 @@ function searchProject(projectPath, options) {
31652
31835
  if (steppedOver(directory)) {
31653
31836
  return;
31654
31837
  }
31655
- for (const entry of readdirSync5(directory, { withFileTypes: true })) {
31838
+ for (const entry of readdirSync6(directory, { withFileTypes: true })) {
31656
31839
  if (full()) {
31657
31840
  return;
31658
31841
  }
31659
31842
  if (skipped(entry.name)) {
31660
31843
  continue;
31661
31844
  }
31662
- const entryPath = join17(directory, entry.name);
31845
+ const entryPath = join18(directory, entry.name);
31663
31846
  if (entry.isDirectory()) {
31664
31847
  visit(entryPath);
31665
31848
  continue;
@@ -31668,7 +31851,7 @@ function searchProject(projectPath, options) {
31668
31851
  if (!entry.isFile() || extensions !== null && !extensions.has(extension)) {
31669
31852
  continue;
31670
31853
  }
31671
- const bytes = readFileSync14(entryPath);
31854
+ const bytes = readFileSync15(entryPath);
31672
31855
  if (extensions === null && isBinary(bytes)) {
31673
31856
  continue;
31674
31857
  }
@@ -31830,8 +32013,8 @@ function setupPromptHandlers(server) {
31830
32013
  }
31831
32014
 
31832
32015
  // src/rename.ts
31833
- import { existsSync as existsSync14, mkdirSync as mkdirSync9, readFileSync as readFileSync15, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync10 } from "node:fs";
31834
- import { dirname as dirname11, posix as posix2 } from "node:path";
32016
+ import { existsSync as existsSync15, mkdirSync as mkdirSync9, readFileSync as readFileSync16, renameSync as renameSync4, unlinkSync as unlinkSync3, writeFileSync as writeFileSync10 } from "node:fs";
32017
+ import { dirname as dirname12, posix as posix2 } from "node:path";
31835
32018
  class Script {
31836
32019
  path;
31837
32020
  text;
@@ -32553,26 +32736,26 @@ function rewritesOf(plan, texts, move) {
32553
32736
  }
32554
32737
  function applyRewrites(rewrites) {
32555
32738
  for (const rewrite of rewrites) {
32556
- const now = readFileSync15(rewrite.absolute, "utf8");
32739
+ const now = readFileSync16(rewrite.absolute, "utf8");
32557
32740
  if (now !== rewrite.before) {
32558
32741
  throw new Error(`${rewrite.path} changed on disk while the rename was being worked out; nothing was written`);
32559
32742
  }
32560
- if (rewrite.writeTo !== rewrite.absolute && existsSync14(rewrite.writeTo)) {
32743
+ if (rewrite.writeTo !== rewrite.absolute && existsSync15(rewrite.writeTo)) {
32561
32744
  throw new Error(`${rewrite.writeTo} already exists; nothing was written`);
32562
32745
  }
32563
32746
  }
32564
32747
  const done = [];
32565
32748
  try {
32566
32749
  for (const rewrite of rewrites) {
32567
- mkdirSync9(dirname11(rewrite.writeTo), { recursive: true });
32750
+ mkdirSync9(dirname12(rewrite.writeTo), { recursive: true });
32568
32751
  const staging = `${rewrite.writeTo}.gdharness-rename`;
32569
32752
  writeFileSync10(staging, rewrite.after, "utf8");
32570
- renameSync2(staging, rewrite.writeTo);
32753
+ renameSync4(staging, rewrite.writeTo);
32571
32754
  let movedUid = false;
32572
32755
  if (rewrite.writeTo !== rewrite.absolute) {
32573
- unlinkSync2(rewrite.absolute);
32574
- if (existsSync14(`${rewrite.absolute}.uid`)) {
32575
- renameSync2(`${rewrite.absolute}.uid`, `${rewrite.writeTo}.uid`);
32756
+ unlinkSync3(rewrite.absolute);
32757
+ if (existsSync15(`${rewrite.absolute}.uid`)) {
32758
+ renameSync4(`${rewrite.absolute}.uid`, `${rewrite.writeTo}.uid`);
32576
32759
  movedUid = true;
32577
32760
  }
32578
32761
  }
@@ -32583,9 +32766,9 @@ function applyRewrites(rewrites) {
32583
32766
  try {
32584
32767
  writeFileSync10(rewrite.absolute, rewrite.before, "utf8");
32585
32768
  if (rewrite.writeTo !== rewrite.absolute) {
32586
- unlinkSync2(rewrite.writeTo);
32769
+ unlinkSync3(rewrite.writeTo);
32587
32770
  if (movedUid) {
32588
- renameSync2(`${rewrite.writeTo}.uid`, `${rewrite.absolute}.uid`);
32771
+ renameSync4(`${rewrite.writeTo}.uid`, `${rewrite.absolute}.uid`);
32589
32772
  }
32590
32773
  }
32591
32774
  } catch {}
@@ -32595,11 +32778,11 @@ function applyRewrites(rewrites) {
32595
32778
  }
32596
32779
 
32597
32780
  // src/rename-symbol.ts
32598
- import { existsSync as existsSync15, mkdirSync as mkdirSync10, readdirSync as readdirSync6, realpathSync as realpathSync5, rmSync as rmSync7, writeFileSync as writeFileSync11 } from "node:fs";
32599
- import { basename as basename3, dirname as dirname12, join as join18, relative as relative3, resolve as resolve5 } from "node:path";
32781
+ import { existsSync as existsSync16, mkdirSync as mkdirSync10, readdirSync as readdirSync7, realpathSync as realpathSync5, rmSync as rmSync8, writeFileSync as writeFileSync11 } from "node:fs";
32782
+ import { basename as basename4, dirname as dirname13, join as join19, relative as relative3, resolve as resolve5 } from "node:path";
32600
32783
 
32601
32784
  // src/resources.ts
32602
- import { readFileSync as readFileSync16, realpathSync as realpathSync4 } from "node:fs";
32785
+ import { readFileSync as readFileSync17, realpathSync as realpathSync4 } from "node:fs";
32603
32786
  import { extname as extname2, resolve as resolve4 } from "node:path";
32604
32787
  var STATIC_RESOURCES = [
32605
32788
  {
@@ -32830,7 +33013,7 @@ function readResourceText(uri, getProjectPath) {
32830
33013
  const parsedUri = parseGodotUri(uri);
32831
33014
  if (parsedUri.kind === "project-info") {
32832
33015
  const projectFilePath = resolveProjectFile(projectPath, "project.godot");
32833
- const rawProject = readFileSync16(projectFilePath, "utf-8");
33016
+ const rawProject = readFileSync17(projectFilePath, "utf-8");
32834
33017
  const parsedProject = parseProjectGodot(rawProject);
32835
33018
  return {
32836
33019
  mimeType: "application/json",
@@ -32839,7 +33022,7 @@ function readResourceText(uri, getProjectPath) {
32839
33022
  }
32840
33023
  const filePath = resolveProjectFile(projectPath, parsedUri.resourcePath);
32841
33024
  ensureAllowedExtension(parsedUri.kind, filePath);
32842
- const text = readFileSync16(filePath, "utf-8");
33025
+ const text = readFileSync17(filePath, "utf-8");
32843
33026
  return {
32844
33027
  mimeType: parsedUri.kind === "script" ? "text/x-gdscript" : "text/plain",
32845
33028
  text
@@ -32904,13 +33087,13 @@ function onDiskSpelling(path) {
32904
33087
  let at = resolve5(path);
32905
33088
  for (;; ) {
32906
33089
  try {
32907
- return join18(realpathSync5.native(at), ...missing.reverse());
33090
+ return join19(realpathSync5.native(at), ...missing.reverse());
32908
33091
  } catch {
32909
- const parent = dirname12(at);
33092
+ const parent = dirname13(at);
32910
33093
  if (parent === at) {
32911
33094
  return resolve5(path);
32912
33095
  }
32913
- missing.push(basename3(at));
33096
+ missing.push(basename4(at));
32914
33097
  at = parent;
32915
33098
  }
32916
33099
  }
@@ -32957,7 +33140,7 @@ async function renameSymbol(request, services) {
32957
33140
  reason: `newScriptPath must be a .gd path inside the project, and ${request.newScriptPath} is not.`
32958
33141
  };
32959
33142
  }
32960
- if (existsSync15(request.newScriptPath)) {
33143
+ if (existsSync16(request.newScriptPath)) {
32961
33144
  return { ok: false, reason: `${moveTo} already exists, so the script cannot be moved there.` };
32962
33145
  }
32963
33146
  }
@@ -33133,7 +33316,7 @@ async function renameSymbol(request, services) {
33133
33316
  scripts,
33134
33317
  from: declaringPath,
33135
33318
  to: moveTo,
33136
- exists: (path) => existsSync15(`${projectPath}/${path.slice("res://".length)}`)
33319
+ exists: (path) => existsSync16(`${projectPath}/${path.slice("res://".length)}`)
33137
33320
  }, plan);
33138
33321
  }
33139
33322
  plan = mentionIgnored(plan, ignoredTexts(projectPath), symbol, moveTo === null ? null : declaringPath);
@@ -33334,15 +33517,15 @@ ${mention.kind}`;
33334
33517
  var LEFT_ALONE_ANSWER_LINES = 300;
33335
33518
  var detailWrites = 0;
33336
33519
  function writeLeftAloneDetail(projectPath, symbol, detail) {
33337
- const directory = join18(projectPath, ".godot", "gdharness-renames");
33520
+ const directory = join19(projectPath, ".godot", "gdharness-renames");
33338
33521
  mkdirSync10(directory, { recursive: true });
33339
33522
  detailWrites += 1;
33340
- const path = join18(directory, `${String(Date.now())}-${String(detailWrites).padStart(6, "0")}-${String(process.pid)}-${symbol}.json`);
33523
+ const path = join19(directory, `${String(Date.now())}-${String(detailWrites).padStart(6, "0")}-${String(process.pid)}-${symbol}.json`);
33341
33524
  writeFileSync11(path, `${JSON.stringify(detail, null, 2)}
33342
33525
  `, "utf8");
33343
- const kept = readdirSync6(directory).filter((name) => name.endsWith(".json")).sort().reverse();
33526
+ const kept = readdirSync7(directory).filter((name) => name.endsWith(".json")).sort().reverse();
33344
33527
  for (const old of kept.slice(10)) {
33345
- rmSync7(join18(directory, old), { force: true });
33528
+ rmSync8(join19(directory, old), { force: true });
33346
33529
  }
33347
33530
  return path;
33348
33531
  }
@@ -33365,15 +33548,15 @@ function summaryOf(edits, mentions, rewrites, left) {
33365
33548
  }
33366
33549
 
33367
33550
  // src/restart-note.ts
33368
- import { mkdirSync as mkdirSync11, readFileSync as readFileSync17, rmSync as rmSync8, writeFileSync as writeFileSync12 } from "node:fs";
33369
- import { dirname as dirname13, join as join19 } from "node:path";
33551
+ import { mkdirSync as mkdirSync11, readFileSync as readFileSync18, rmSync as rmSync9, writeFileSync as writeFileSync12 } from "node:fs";
33552
+ import { dirname as dirname14, join as join20 } from "node:path";
33370
33553
  function restartNotePath(projectPath) {
33371
- return join19(projectPath, ".godot", "gdharness-restart.json");
33554
+ return join20(projectPath, ".godot", "gdharness-restart.json");
33372
33555
  }
33373
33556
  function noteRestartBegun(note) {
33374
33557
  const path = restartNotePath(note.projectPath);
33375
33558
  try {
33376
- mkdirSync11(dirname13(path), { recursive: true });
33559
+ mkdirSync11(dirname14(path), { recursive: true });
33377
33560
  writeFileSync12(path, `${JSON.stringify(note, null, 2)}
33378
33561
  `, "utf8");
33379
33562
  } catch (error) {
@@ -33382,7 +33565,7 @@ function noteRestartBegun(note) {
33382
33565
  }
33383
33566
  function restartOwed(projectPath) {
33384
33567
  try {
33385
- const parsed = JSON.parse(readFileSync17(restartNotePath(projectPath), "utf8"));
33568
+ const parsed = JSON.parse(readFileSync18(restartNotePath(projectPath), "utf8"));
33386
33569
  if (typeof parsed !== "object" || parsed === null) {
33387
33570
  return null;
33388
33571
  }
@@ -33397,66 +33580,31 @@ function restartOwed(projectPath) {
33397
33580
  }
33398
33581
  function restartSettled(projectPath) {
33399
33582
  try {
33400
- rmSync8(restartNotePath(projectPath), { force: true });
33583
+ rmSync9(restartNotePath(projectPath), { force: true });
33401
33584
  } catch (error) {
33402
33585
  console.error(`[SERVER] Could not clear the restart note: ${errorMessage(error)}`);
33403
33586
  }
33404
33587
  }
33405
33588
 
33406
33589
  // src/run-record.ts
33407
- import { execFile as execFile4 } from "node:child_process";
33408
33590
  import { createHash as createHash3 } from "node:crypto";
33409
- import { mkdirSync as mkdirSync12, openSync as openSync2, readdirSync as readdirSync7, readFileSync as readFileSync18, realpathSync as realpathSync6, rmSync as rmSync10, statSync as statSync7 } from "node:fs";
33410
- import { basename as basename4, dirname as dirname14, join as join20, resolve as resolve6 } from "node:path";
33411
- import { promisify as promisify4 } from "node:util";
33412
-
33413
- // src/whole-file.ts
33414
- import { renameSync as renameSync3, rmSync as rmSync9, writeFileSync as writeFileSync13 } from "node:fs";
33415
- var HELD_FILE_MS = 1000;
33416
- var HELD_FILE_RETRY_MS = 10;
33417
- var HELD_BY_A_READER = new Set(["EPERM", "EACCES", "EBUSY"]);
33418
- function writeWhole(path, text) {
33419
- const partial = `${path}.${process.pid}.partial`;
33420
- try {
33421
- writeFileSync13(partial, text, "utf8");
33422
- } catch {
33423
- return false;
33424
- }
33425
- const until = Date.now() + HELD_FILE_MS;
33426
- for (;; ) {
33427
- try {
33428
- renameSync3(partial, path);
33429
- return true;
33430
- } catch (error) {
33431
- const code = error.code ?? "";
33432
- if (!HELD_BY_A_READER.has(code) || Date.now() >= until) {
33433
- try {
33434
- rmSync9(partial, { force: true });
33435
- } catch {}
33436
- return false;
33437
- }
33438
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, HELD_FILE_RETRY_MS);
33439
- }
33440
- }
33441
- }
33442
-
33443
- // src/run-record.ts
33444
- var execFileAsync = promisify4(execFile4);
33591
+ import { mkdirSync as mkdirSync12, openSync as openSync2, readdirSync as readdirSync8, readFileSync as readFileSync19, realpathSync as realpathSync6, rmSync as rmSync10, statSync as statSync8 } from "node:fs";
33592
+ import { basename as basename5, dirname as dirname15, join as join21, resolve as resolve6 } from "node:path";
33445
33593
  function runsDirectory() {
33446
- return join20(runtimeDirectory(), "runs");
33594
+ return join21(runtimeDirectory(), "runs");
33447
33595
  }
33448
33596
  function realAsFarAsItExists(path) {
33449
33597
  const missing = [];
33450
33598
  let at = path;
33451
33599
  for (;; ) {
33452
33600
  try {
33453
- return join20(realpathSync6.native(at), ...missing);
33601
+ return join21(realpathSync6.native(at), ...missing);
33454
33602
  } catch {
33455
- const parent = dirname14(at);
33603
+ const parent = dirname15(at);
33456
33604
  if (parent === at) {
33457
33605
  return path;
33458
33606
  }
33459
- missing.unshift(basename4(at));
33607
+ missing.unshift(basename5(at));
33460
33608
  at = parent;
33461
33609
  }
33462
33610
  }
@@ -33467,10 +33615,10 @@ function projectKey(projectPath) {
33467
33615
  return createHash3("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
33468
33616
  }
33469
33617
  function runRecordPath(projectPath, directory = runtimeDirectory()) {
33470
- return join20(directory, "runs", `note-${projectKey(projectPath)}.json`);
33618
+ return join21(directory, "runs", `note-${projectKey(projectPath)}.json`);
33471
33619
  }
33472
33620
  function latestRunPath(servedBy, directory = runtimeDirectory()) {
33473
- return join20(directory, "runs", `latest-${projectKey(servedBy)}.json`);
33621
+ return join21(directory, "runs", `latest-${projectKey(servedBy)}.json`);
33474
33622
  }
33475
33623
  function writeLatestRun(servedBy, latest) {
33476
33624
  try {
@@ -33480,7 +33628,7 @@ function writeLatestRun(servedBy, latest) {
33480
33628
  }
33481
33629
  function readLatestRun(servedBy, directory = runtimeDirectory()) {
33482
33630
  try {
33483
- const fields = JSON.parse(readFileSync18(latestRunPath(servedBy, directory), "utf8"));
33631
+ const fields = JSON.parse(readFileSync19(latestRunPath(servedBy, directory), "utf8"));
33484
33632
  if (typeof fields !== "object" || fields === null) {
33485
33633
  return null;
33486
33634
  }
@@ -33496,17 +33644,17 @@ function readLatestRun(servedBy, directory = runtimeDirectory()) {
33496
33644
  var SHARED_NOTE = "run.json";
33497
33645
  function openTranscript(startedAt) {
33498
33646
  mkdirSync12(runsDirectory(), { recursive: true });
33499
- const path = join20(runsDirectory(), `run-${startedAt}.log`);
33647
+ const path = join21(runsDirectory(), `run-${startedAt}.log`);
33500
33648
  return { path, fd: openSync2(path, "a") };
33501
33649
  }
33502
33650
  function profilePath(startedAt) {
33503
- return join20(runsDirectory(), `run-${startedAt}.profile.json`);
33651
+ return join21(runsDirectory(), `run-${startedAt}.profile.json`);
33504
33652
  }
33505
33653
  function noteIsAbout(note, playing) {
33506
33654
  return note.play !== undefined && playing !== null && note.play.play === playing.play && note.play.editorPid === playing.editorPid;
33507
33655
  }
33508
33656
  function editorNotePath(projectPath, directory = runtimeDirectory()) {
33509
- return join20(directory, "runs", `editor-${projectKey(projectPath)}.json`);
33657
+ return join21(directory, "runs", `editor-${projectKey(projectPath)}.json`);
33510
33658
  }
33511
33659
  function writeEditorRunNote(note) {
33512
33660
  mkdirSync12(runsDirectory(), { recursive: true });
@@ -33517,7 +33665,7 @@ function writeEditorRunNote(note) {
33517
33665
  }
33518
33666
  function readEditorRunNote(projectPath) {
33519
33667
  for (const directory of runtimeDirectories()) {
33520
- const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join20(directory, "runs", "editor-run.json"));
33668
+ const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join21(directory, "runs", "editor-run.json"));
33521
33669
  if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
33522
33670
  return note;
33523
33671
  }
@@ -33527,7 +33675,7 @@ function readEditorRunNote(projectPath) {
33527
33675
  function editorNoteAt(path) {
33528
33676
  let parsed;
33529
33677
  try {
33530
- parsed = JSON.parse(readFileSync18(path, "utf8"));
33678
+ parsed = JSON.parse(readFileSync19(path, "utf8"));
33531
33679
  } catch {
33532
33680
  return null;
33533
33681
  }
@@ -33551,7 +33699,7 @@ function readRunRecord(projectPath) {
33551
33699
  if (own !== null) {
33552
33700
  return own;
33553
33701
  }
33554
- const shared = recordAt(join20(directory, "runs", SHARED_NOTE));
33702
+ const shared = recordAt(join21(directory, "runs", SHARED_NOTE));
33555
33703
  if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
33556
33704
  return shared;
33557
33705
  }
@@ -33563,13 +33711,13 @@ function everyRunRecord() {
33563
33711
  for (const directory of runtimeDirectories()) {
33564
33712
  let entries;
33565
33713
  try {
33566
- entries = readdirSync7(join20(directory, "runs"));
33714
+ entries = readdirSync8(join21(directory, "runs"));
33567
33715
  } catch {
33568
33716
  continue;
33569
33717
  }
33570
33718
  for (const entry of entries) {
33571
33719
  if (entry === SHARED_NOTE || entry.startsWith("note-") && entry.endsWith(".json")) {
33572
- const record = recordAt(join20(directory, "runs", entry));
33720
+ const record = recordAt(join21(directory, "runs", entry));
33573
33721
  if (record !== null) {
33574
33722
  found.push(record);
33575
33723
  }
@@ -33581,7 +33729,7 @@ function everyRunRecord() {
33581
33729
  function recordAt(path) {
33582
33730
  let parsed;
33583
33731
  try {
33584
- parsed = JSON.parse(readFileSync18(path, "utf8"));
33732
+ parsed = JSON.parse(readFileSync19(path, "utf8"));
33585
33733
  } catch {
33586
33734
  return null;
33587
33735
  }
@@ -33634,10 +33782,10 @@ async function runningAs(pid) {
33634
33782
  const began = await startedAt(pid, null);
33635
33783
  const when = began === null ? {} : { startedAt: began };
33636
33784
  if (process.platform === "linux") {
33637
- const raw = readFileSync18(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
33785
+ const raw = readFileSync19(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
33638
33786
  return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
33639
33787
  }
33640
- const args = (await execFileAsync("ps", ["-p", String(pid), "-o", "args="], { encoding: "utf8", timeout: 15000 })).stdout.trim();
33788
+ const args = (await runHidden("ps", ["-p", String(pid), "-o", "args="], { timeout: 15000 })).stdout.trim();
33641
33789
  return args === "" ? null : { kind: "commandLine", text: args, ...when };
33642
33790
  } catch {
33643
33791
  return null;
@@ -33646,7 +33794,7 @@ async function runningAs(pid) {
33646
33794
  async function listeningPid(port) {
33647
33795
  try {
33648
33796
  if (process.platform === "win32") {
33649
- const { stdout } = await execFileAsync("netstat", ["-ano"], { encoding: "utf8", timeout: 15000 });
33797
+ const { stdout } = await runHidden("netstat", ["-ano"], { timeout: 15000 });
33650
33798
  return listeningPidInNetstat(stdout, port);
33651
33799
  }
33652
33800
  for (const [command, args, pattern] of [
@@ -33654,7 +33802,7 @@ async function listeningPid(port) {
33654
33802
  ["ss", ["-ltnpH", `sport = :${port}`], /pid=(\d+)/]
33655
33803
  ]) {
33656
33804
  try {
33657
- const said = (await execFileAsync(command, [...args], { encoding: "utf8", timeout: 15000 })).stdout.trim();
33805
+ const said = (await runHidden(command, [...args], { timeout: 15000 })).stdout.trim();
33658
33806
  const found = pattern.exec(said);
33659
33807
  if (found?.[1] !== undefined) {
33660
33808
  return Number(found[1]);
@@ -33699,7 +33847,7 @@ async function startedAt(pid, windowsAnswer) {
33699
33847
  return Number.isNaN(when) ? null : when;
33700
33848
  }
33701
33849
  try {
33702
- const said = (await execFileAsync("ps", ["-p", String(pid), "-o", "lstart="], { encoding: "utf8", timeout: 15000 })).stdout.trim();
33850
+ const said = (await runHidden("ps", ["-p", String(pid), "-o", "lstart="], { timeout: 15000 })).stdout.trim();
33703
33851
  const when = Date.parse(said);
33704
33852
  return said === "" || Number.isNaN(when) ? null : when;
33705
33853
  } catch {
@@ -33708,10 +33856,8 @@ async function startedAt(pid, windowsAnswer) {
33708
33856
  }
33709
33857
  async function windowsImage(pid) {
33710
33858
  try {
33711
- const { stdout: csv } = await execFileAsync("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
33712
- encoding: "utf8",
33713
- timeout: 15000,
33714
- windowsHide: true
33859
+ const { stdout: csv } = await runHidden("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
33860
+ timeout: 15000
33715
33861
  });
33716
33862
  const image = /^"([^"]+)"/.exec(csv.trim())?.[1];
33717
33863
  return image === undefined ? { why: "named no process under that number" } : { image };
@@ -33741,7 +33887,7 @@ function whyNotTheRun(record, running, needed) {
33741
33887
  if (after > SAME_RUN_WINDOW_MS) {
33742
33888
  return `that process started ${Math.round(after / 1000)} seconds after the run had`;
33743
33889
  }
33744
- const engine = record.command === undefined ? null : basename4(record.command);
33890
+ const engine = record.command === undefined ? null : basename5(record.command);
33745
33891
  const said = process.platform === "win32" ? running.text.toLowerCase() : running.text;
33746
33892
  const wanted = engine === null || process.platform !== "win32" ? engine : engine.toLowerCase();
33747
33893
  if (wanted !== null && !said.includes(wanted)) {
@@ -33780,7 +33926,7 @@ function judgeAnnouncedGame(running, projectPath, announcedAt) {
33780
33926
  }
33781
33927
  function clearRunRecord(projectPath) {
33782
33928
  for (const directory of runtimeDirectories()) {
33783
- const shared = join20(directory, "runs", SHARED_NOTE);
33929
+ const shared = join21(directory, "runs", SHARED_NOTE);
33784
33930
  const sharedRecord = recordAt(shared);
33785
33931
  const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
33786
33932
  for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
@@ -33792,7 +33938,7 @@ function clearRunRecord(projectPath) {
33792
33938
  }
33793
33939
  function ageMs(path, now) {
33794
33940
  try {
33795
- return now - statSync7(path).mtimeMs;
33941
+ return now - statSync8(path).mtimeMs;
33796
33942
  } catch {
33797
33943
  return 0;
33798
33944
  }
@@ -33805,7 +33951,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
33805
33951
  ]));
33806
33952
  let entries;
33807
33953
  try {
33808
- entries = readdirSync7(directory);
33954
+ entries = readdirSync8(directory);
33809
33955
  } catch {
33810
33956
  return;
33811
33957
  }
@@ -33814,7 +33960,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
33814
33960
  if (!transcript && !entry.endsWith(".partial")) {
33815
33961
  continue;
33816
33962
  }
33817
- const path = join20(directory, entry);
33963
+ const path = join21(directory, entry);
33818
33964
  if (kept.has(path) || ageMs(path, now) < keepMs) {
33819
33965
  continue;
33820
33966
  }
@@ -33825,7 +33971,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
33825
33971
  }
33826
33972
 
33827
33973
  // src/script-origins.ts
33828
- import { join as join21 } from "node:path";
33974
+ import { join as join22 } from "node:path";
33829
33975
  var UNRESOLVED = /Could not resolve class "([A-Za-z_][A-Za-z0-9_]*)", because of a parser error/;
33830
33976
  function unresolvedClassIn(message) {
33831
33977
  return UNRESOLVED.exec(message)?.[1] ?? null;
@@ -33881,7 +34027,7 @@ function checkWithEngine(godotPath, projectPath) {
33881
34027
  const ran = await runEngine(godotPath, [
33882
34028
  "--headless",
33883
34029
  "--log-file",
33884
- join21(logDir, "engine.log"),
34030
+ join22(logDir, "engine.log"),
33885
34031
  "--path",
33886
34032
  projectPath,
33887
34033
  "--check-only",
@@ -33918,10 +34064,10 @@ function withoutListed(entries, errors) {
33918
34064
  }
33919
34065
 
33920
34066
  // src/script-profile.ts
33921
- import { readFileSync as readFileSync19 } from "node:fs";
34067
+ import { readFileSync as readFileSync20 } from "node:fs";
33922
34068
  function readProfileFile(path) {
33923
34069
  try {
33924
- const parsed = JSON.parse(readFileSync19(path, "utf8"));
34070
+ const parsed = JSON.parse(readFileSync20(path, "utf8"));
33925
34071
  return typeof parsed === "object" && typeof parsed.connected === "boolean" ? parsed : null;
33926
34072
  } catch {
33927
34073
  return null;
@@ -35056,6 +35202,11 @@ var TOOL_SPECS = [
35056
35202
  ops: ["start"],
35057
35203
  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."
35058
35204
  },
35205
+ executable: {
35206
+ type: "string",
35207
+ ops: ["start"],
35208
+ 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."
35209
+ },
35059
35210
  savesIn: {
35060
35211
  type: "string",
35061
35212
  ops: ["start", "check"],
@@ -35211,7 +35362,7 @@ var TOOL_SPECS = [
35211
35362
  name: "editor_rescan",
35212
35363
  title: "Rescan the project",
35213
35364
  effect: "additive",
35214
- 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.",
35365
+ 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.",
35215
35366
  parameters: {
35216
35367
  projectPath: PROJECT_PATH2,
35217
35368
  timeoutMs: {
@@ -35289,13 +35440,13 @@ var TOOL_SPECS = [
35289
35440
  pid: RUNNING_PID,
35290
35441
  nodePath: {
35291
35442
  type: "string",
35292
- ops: ["tree", "text", "find", "rect", "property"],
35293
- description: "tree, find, text: where to start, default /root. rect: the node to place. property: the node to read."
35443
+ ops: ["tree", "text", "find", "rect", "property", "accessibility"],
35444
+ 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.'
35294
35445
  },
35295
35446
  property: {
35296
35447
  type: "string",
35297
35448
  ops: ["property", "find"],
35298
- 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.'
35449
+ 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.'
35299
35450
  },
35300
35451
  depth: {
35301
35452
  type: "integer",
@@ -35311,8 +35462,8 @@ var TOOL_SPECS = [
35311
35462
  properties: {
35312
35463
  type: "array",
35313
35464
  items: { type: "string" },
35314
- ops: ["tree"],
35315
- 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."
35465
+ ops: ["tree", "accessibility"],
35466
+ 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.'
35316
35467
  },
35317
35468
  className: {
35318
35469
  type: "string",
@@ -35328,15 +35479,15 @@ var TOOL_SPECS = [
35328
35479
  group: { type: "string", ops: ["find"], description: "find: a group the node is in." },
35329
35480
  says: {
35330
35481
  type: "string",
35331
- ops: ["find"],
35332
- 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.'
35482
+ ops: ["find", "accessibility"],
35483
+ 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.'
35333
35484
  },
35334
35485
  limit: {
35335
35486
  type: "integer",
35336
35487
  minimum: 1,
35337
35488
  maximum: 5000,
35338
- ops: ["find", "text"],
35339
- 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."
35489
+ ops: ["find", "text", "accessibility"],
35490
+ 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."
35340
35491
  },
35341
35492
  includeHidden: {
35342
35493
  type: "boolean",
@@ -35380,12 +35531,12 @@ var TOOL_SPECS = [
35380
35531
  },
35381
35532
  metrics: {
35382
35533
  effect: "read",
35383
- 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",
35534
+ 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",
35384
35535
  requires: []
35385
35536
  },
35386
35537
  accessibility: {
35387
35538
  effect: "read",
35388
- 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`,
35539
+ 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",
35389
35540
  requires: []
35390
35541
  }
35391
35542
  },
@@ -35394,19 +35545,19 @@ var TOOL_SPECS = [
35394
35545
  {
35395
35546
  name: "runtime_invoke",
35396
35547
  title: "Set or call in the running game",
35397
- 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.",
35548
+ 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.",
35398
35549
  parameters: {
35399
35550
  projectPath: RUNNING_PROJECT_PATH,
35400
35551
  pid: RUNNING_PID,
35401
35552
  nodePath: {
35402
35553
  type: "string",
35403
35554
  ops: ["set", "call"],
35404
- description: 'Absolute node path, such as "/root/Main/Player".'
35555
+ 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.'
35405
35556
  },
35406
35557
  property: {
35407
35558
  type: "string",
35408
35559
  ops: ["set"],
35409
- 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.'
35560
+ 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.'
35410
35561
  },
35411
35562
  value: {
35412
35563
  ops: ["set"],
@@ -35539,6 +35690,13 @@ var TOOL_SPECS = [
35539
35690
  ops: ["action", "key", "mouse_click", "joypad_button"],
35540
35691
  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."
35541
35692
  },
35693
+ holdFrames: {
35694
+ type: "integer",
35695
+ minimum: 1,
35696
+ maximum: 600,
35697
+ ops: ["action", "key", "mouse_click", "joypad_button"],
35698
+ 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."
35699
+ },
35542
35700
  strength: {
35543
35701
  type: "number",
35544
35702
  minimum: 0,
@@ -35687,7 +35845,7 @@ var TOOL_SPECS = [
35687
35845
  property: {
35688
35846
  type: "string",
35689
35847
  ops: ["until"],
35690
- 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.'
35848
+ 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.'
35691
35849
  },
35692
35850
  value: {
35693
35851
  ops: ["until"],
@@ -35872,20 +36030,19 @@ function serverInstructions() {
35872
36030
  }
35873
36031
 
35874
36032
  // src/setup.ts
35875
- import { spawnSync } from "node:child_process";
35876
36033
  import { createHash as createHash4 } from "node:crypto";
35877
36034
  import {
35878
36035
  cpSync,
35879
- existsSync as existsSync16,
36036
+ existsSync as existsSync17,
35880
36037
  mkdirSync as mkdirSync13,
35881
- readdirSync as readdirSync8,
35882
- readFileSync as readFileSync20,
36038
+ readdirSync as readdirSync9,
36039
+ readFileSync as readFileSync21,
35883
36040
  rmdirSync,
35884
36041
  rmSync as rmSync11,
35885
- statSync as statSync8,
35886
- writeFileSync as writeFileSync14
36042
+ statSync as statSync9,
36043
+ writeFileSync as writeFileSync13
35887
36044
  } from "node:fs";
35888
- import { dirname as dirname15, join as join22 } from "node:path";
36045
+ import { dirname as dirname16, join as join23 } from "node:path";
35889
36046
  import { fileURLToPath as fileURLToPath3 } from "node:url";
35890
36047
  var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
35891
36048
  var EDITOR_PLUGINS = ["gdharness_editor", "auto_reload"];
@@ -35904,25 +36061,25 @@ function runtimeAddonDigest(from = shippedAddonsDirectory()) {
35904
36061
  function addonsDigest(names, from) {
35905
36062
  const hash = createHash4("sha256");
35906
36063
  const walk = (directory, relative) => {
35907
- for (const name of readdirSync8(directory).sort()) {
36064
+ for (const name of readdirSync9(directory).sort()) {
35908
36065
  if (name === VERSION_MARKER || name === DIGEST_MARKER) {
35909
36066
  continue;
35910
36067
  }
35911
- const path = join22(directory, name);
36068
+ const path = join23(directory, name);
35912
36069
  const inside = `${relative}/${name}`;
35913
- if (statSync8(path).isDirectory()) {
36070
+ if (statSync9(path).isDirectory()) {
35914
36071
  walk(path, inside);
35915
36072
  } else {
35916
36073
  hash.update(`${inside}
35917
36074
  `);
35918
- hash.update(readFileSync20(path));
36075
+ hash.update(readFileSync21(path));
35919
36076
  hash.update(`
35920
36077
  `);
35921
36078
  }
35922
36079
  }
35923
36080
  };
35924
36081
  for (const name of names) {
35925
- walk(join22(from, name), name);
36082
+ walk(join23(from, name), name);
35926
36083
  }
35927
36084
  return hash.digest("hex");
35928
36085
  }
@@ -35955,22 +36112,22 @@ function installedRuntimeDigest(projectPath) {
35955
36112
  return digestMarker(projectPath, RUNTIME_ADDON);
35956
36113
  }
35957
36114
  function digestMarker(projectPath, addon) {
35958
- const marker = join22(projectPath, "addons", addon, DIGEST_MARKER);
35959
- if (!existsSync16(marker)) {
36115
+ const marker = join23(projectPath, "addons", addon, DIGEST_MARKER);
36116
+ if (!existsSync17(marker)) {
35960
36117
  return null;
35961
36118
  }
35962
- const said = readFileSync20(marker, "utf8").trim();
36119
+ const said = readFileSync21(marker, "utf8").trim();
35963
36120
  return said === "" ? null : said;
35964
36121
  }
35965
36122
  function shippedAddonsDirectory() {
35966
- return join22(dirname15(fileURLToPath3(import.meta.url)), "godot", "addons");
36123
+ return join23(dirname16(fileURLToPath3(import.meta.url)), "godot", "addons");
35967
36124
  }
35968
36125
  function installedAddonVersion(projectPath) {
35969
- const marker = join22(projectPath, "addons", ADDONS[0], VERSION_MARKER);
35970
- if (!existsSync16(marker)) {
36126
+ const marker = join23(projectPath, "addons", ADDONS[0], VERSION_MARKER);
36127
+ if (!existsSync17(marker)) {
35971
36128
  return null;
35972
36129
  }
35973
- const said = readFileSync20(marker, "utf8").trim();
36130
+ const said = readFileSync21(marker, "utf8").trim();
35974
36131
  return said === "" ? null : said;
35975
36132
  }
35976
36133
  function loaderAmong(projectPath, named) {
@@ -35981,7 +36138,7 @@ function loaderAmong(projectPath, named) {
35981
36138
  }
35982
36139
  let source = null;
35983
36140
  try {
35984
- source = readFileSync20(join22(projectPath, path), "utf8");
36141
+ source = readFileSync21(join23(projectPath, path), "utf8");
35985
36142
  } catch {}
35986
36143
  if (source?.includes(RUNTIME_AUTOLOAD.path)) {
35987
36144
  return { name: entry, path, exists: true };
@@ -36004,7 +36161,7 @@ function trackedByGit(projectPath, wanted) {
36004
36161
  if (wanted.length === 0) {
36005
36162
  return new Set;
36006
36163
  }
36007
- const asked = spawnSync("git", ["ls-files", "-z", "--", ...wanted], {
36164
+ const asked = spawnSyncHidden("git", ["ls-files", "-z", "--", ...wanted], {
36008
36165
  cwd: projectPath,
36009
36166
  encoding: "utf8"
36010
36167
  });
@@ -36023,10 +36180,10 @@ function enabledPlugins(settings) {
36023
36180
  function inspectProject(projectPath) {
36024
36181
  const problems = [];
36025
36182
  const addons = ADDONS.map((name) => {
36026
- const directory = join22(projectPath, "addons", name);
36027
- const marker = join22(directory, VERSION_MARKER);
36028
- const installed = existsSync16(directory);
36029
- const version = existsSync16(marker) ? readFileSync20(marker, "utf8").trim() : null;
36183
+ const directory = join23(projectPath, "addons", name);
36184
+ const marker = join23(directory, VERSION_MARKER);
36185
+ const installed = existsSync17(directory);
36186
+ const version = existsSync17(marker) ? readFileSync21(marker, "utf8").trim() : null;
36030
36187
  const current = version === SERVER_VERSION;
36031
36188
  if (!installed) {
36032
36189
  problems.push(`addons/${name} is not installed; run gdharness setup`);
@@ -36035,7 +36192,7 @@ function inspectProject(projectPath) {
36035
36192
  }
36036
36193
  return { name, installed, version, current };
36037
36194
  });
36038
- const settings = parseProjectGodot(readFileSync20(join22(projectPath, "project.godot"), "utf8"));
36195
+ const settings = parseProjectGodot(readFileSync21(join23(projectPath, "project.godot"), "utf8"));
36039
36196
  const pluginsEnabled = enabledPlugins(settings);
36040
36197
  for (const name of EDITOR_PLUGINS) {
36041
36198
  if (!pluginsEnabled.includes(name)) {
@@ -36051,7 +36208,7 @@ function inspectProject(projectPath) {
36051
36208
  const tracked = trackedByGit(projectPath, [...named.values()]);
36052
36209
  const addonMissing = (path) => addons.some((addon) => !addon.installed && path.startsWith(`addons/${addon.name}/`));
36053
36210
  for (const [name, path] of named) {
36054
- if (!existsSync16(join22(projectPath, path))) {
36211
+ if (!existsSync17(join23(projectPath, path))) {
36055
36212
  if (!addonMissing(path)) {
36056
36213
  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`);
36057
36214
  }
@@ -36101,8 +36258,7 @@ function argumentShape(schema) {
36101
36258
  // src/server.ts
36102
36259
  var UPDATE_NOTICE_EVERY = 500;
36103
36260
  var FEEDBACK_NOTICE_EVERY = 250;
36104
- var run4 = promisify5(execFile5);
36105
- var __dirname2 = dirname16(fileURLToPath4(import.meta.url));
36261
+ var __dirname2 = dirname17(fileURLToPath4(import.meta.url));
36106
36262
  var PARENT_ASKED_AFTER_MS = 5000;
36107
36263
  function editorRestartTimeoutMs() {
36108
36264
  return waitFromEnv("GDHARNESS_EDITOR_RESTART_TIMEOUT_MS", 90000);
@@ -36134,22 +36290,22 @@ function writtenFileRefusal(path, rule) {
36134
36290
  if (/^(res|user):\/\//.test(path)) {
36135
36291
  return `${named} ${path} is a Godot path; ${what} is saved outside the project, at an absolute path on this machine.`;
36136
36292
  }
36137
- if (!isAbsolute3(path)) {
36293
+ if (!isAbsolute4(path)) {
36138
36294
  return `${named} ${path} is not absolute; name the whole path, so where it is written does not depend on this server's directory.`;
36139
36295
  }
36140
36296
  if (!rule.extensions.includes(extname3(path).toLowerCase())) {
36141
36297
  return `${named} ${path} does not end in ${rule.extensions.join(" or ")}, which is what ${what} is.`;
36142
36298
  }
36143
- const directory = realPathOr(dirname16(path));
36144
- if (!existsSync17(directory)) {
36145
- return `${named}'s directory ${dirname16(path)} does not exist; ${what} creates no directories.`;
36299
+ const directory = realPathOr(dirname17(path));
36300
+ if (!existsSync18(directory)) {
36301
+ return `${named}'s directory ${dirname17(path)} does not exist; ${what} creates no directories.`;
36146
36302
  }
36147
- const where = join23(directory, basename5(path));
36303
+ const where = join24(directory, basename6(path));
36148
36304
  const inside = rule.projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
36149
36305
  if (inside !== undefined) {
36150
36306
  return `${named} ${path} is inside the project at ${inside}; ${what} is saved outside it, so it never becomes part of the game.`;
36151
36307
  }
36152
- const taken = rule.writes(where).find((file) => existsSync17(file));
36308
+ const taken = rule.writes(where).find((file) => existsSync18(file));
36153
36309
  if (taken !== undefined) {
36154
36310
  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.`;
36155
36311
  }
@@ -36199,6 +36355,10 @@ function captureDetail(payload) {
36199
36355
  function patienceForFrames(frames, atLeast) {
36200
36356
  return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
36201
36357
  }
36358
+ function withDefined(fields) {
36359
+ return Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined));
36360
+ }
36361
+ var 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.";
36202
36362
  function longerWaitNote(tool, op, waitedMs) {
36203
36363
  const spec = TOOL_SPECS.find((one) => one.name === tool);
36204
36364
  if (spec !== undefined && argumentsOf(spec, op).includes("timeoutMs")) {
@@ -36666,9 +36826,35 @@ function notRunNote(notRun, failFast) {
36666
36826
  const cases = `${notRun} case${notRun === 1 ? "" : "s"}`;
36667
36827
  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.`;
36668
36828
  }
36829
+ function exportPresetRefusal(projectPath, preset) {
36830
+ let text;
36831
+ try {
36832
+ text = readFileSync22(join24(projectPath, "export_presets.cfg"), "utf8");
36833
+ } catch {
36834
+ 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.`;
36835
+ }
36836
+ const names = [];
36837
+ let inPreset = false;
36838
+ for (const line of text.split(/\r?\n/)) {
36839
+ const section = /^\s*\[([^\]]+)\]/.exec(line)?.[1];
36840
+ if (section !== undefined) {
36841
+ inPreset = /^preset\.\d+$/.test(section.trim());
36842
+ continue;
36843
+ }
36844
+ const named = inPreset ? /^\s*name\s*=\s*"((?:[^"\\]|\\.)*)"/.exec(line)?.[1] : undefined;
36845
+ if (named !== undefined) {
36846
+ names.push(named.replace(/\\(.)/g, "$1"));
36847
+ }
36848
+ }
36849
+ if (names.includes(preset)) {
36850
+ return null;
36851
+ }
36852
+ 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(", ")}.`;
36853
+ }
36669
36854
  function exportAnswer(ending, asked, before, after) {
36670
36855
  const { log } = ending;
36671
36856
  const written = after !== null && after !== before;
36857
+ const notCopied = librariesNotCopied(log.all);
36672
36858
  const verdict = {
36673
36859
  exported: ending.exitCode === 0 && ending.failure === null && log.count("error") === 0 && written,
36674
36860
  ...asked,
@@ -36677,6 +36863,10 @@ function exportAnswer(ending, asked, before, after) {
36677
36863
  failure: ending.failure ?? undefined,
36678
36864
  errors: log.count("error"),
36679
36865
  warnings: log.count("warning"),
36866
+ ...notCopied.length === 0 ? {} : {
36867
+ librariesNotLoaded: notCopied,
36868
+ 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.`
36869
+ },
36680
36870
  ...foldedEntriesAnswer(log)
36681
36871
  };
36682
36872
  if (verdict.exported) {
@@ -36721,7 +36911,7 @@ var EDITOR_FREE_INSTEAD = {
36721
36911
  scene_tree: "runtime_inspect tree reads the nodes of a game that is running, and needs no editor; scene_tree reads a scene file through the editor"
36722
36912
  };
36723
36913
  function editorAddonMissing(projectPath) {
36724
- if (!existsSync17(join23(projectPath, "project.godot"))) {
36914
+ if (!existsSync18(join24(projectPath, "project.godot"))) {
36725
36915
  return null;
36726
36916
  }
36727
36917
  const report = inspectProject(projectPath);
@@ -37003,7 +37193,7 @@ function profileFrameFunctions(projectPath) {
37003
37193
  let projectGodot = null;
37004
37194
  if (projectPath !== null) {
37005
37195
  try {
37006
- projectGodot = readFileSync21(join23(projectPath, "project.godot"), "utf8");
37196
+ projectGodot = readFileSync22(join24(projectPath, "project.godot"), "utf8");
37007
37197
  } catch {}
37008
37198
  }
37009
37199
  return frameFunctionsFor(queuedMessagesOf(projectGodot));
@@ -37029,7 +37219,7 @@ function projectSourceOf(projectPath) {
37029
37219
  let source = null;
37030
37220
  if (inside.ok) {
37031
37221
  try {
37032
- source = readFileSync21(inside.absolutePath, "utf8");
37222
+ source = readFileSync22(inside.absolutePath, "utf8");
37033
37223
  } catch {}
37034
37224
  }
37035
37225
  read.set(script, source);
@@ -37181,7 +37371,7 @@ function foldedEntriesAnswer(log, floor = "warning") {
37181
37371
  var ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd";
37182
37372
  function gdUnitFailsOnScriptErrors(projectPath) {
37183
37373
  try {
37184
- return settingKeys(readFileSync21(join23(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
37374
+ return settingKeys(readFileSync22(join24(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
37185
37375
  } catch {
37186
37376
  return true;
37187
37377
  }
@@ -37196,13 +37386,13 @@ function uncaughtNote(uncaught) {
37196
37386
  }
37197
37387
  function elsewhereIn(projectPath, asked) {
37198
37388
  const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
37199
- if (wanted === "" || existsSync17(join23(projectPath, wanted))) {
37389
+ if (wanted === "" || existsSync18(join24(projectPath, wanted))) {
37200
37390
  return "";
37201
37391
  }
37202
37392
  const root = (wanted.split("/")[0] ?? "").toLowerCase();
37203
37393
  let near = [];
37204
37394
  try {
37205
- near = readdirSync9(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()));
37395
+ near = readdirSync10(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()));
37206
37396
  } catch {
37207
37397
  return "";
37208
37398
  }
@@ -37287,23 +37477,23 @@ function scriptsToDiagnose(projectPath, given) {
37287
37477
  var EXTERNAL_MEMBER = 'Could not resolve external class member "';
37288
37478
  function isDirectory(path) {
37289
37479
  try {
37290
- return statSync9(path).isDirectory();
37480
+ return statSync10(path).isDirectory();
37291
37481
  } catch {
37292
37482
  return false;
37293
37483
  }
37294
37484
  }
37295
37485
  function scriptsUnder(directory, prefix) {
37296
- if (existsSync17(join23(directory, ".gdignore"))) {
37486
+ if (existsSync18(join24(directory, ".gdignore"))) {
37297
37487
  return [];
37298
37488
  }
37299
37489
  const found = [];
37300
- for (const entry of readdirSync9(directory, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : 1)) {
37490
+ for (const entry of readdirSync10(directory, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : 1)) {
37301
37491
  if (entry.name.startsWith(".")) {
37302
37492
  continue;
37303
37493
  }
37304
37494
  const path = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
37305
37495
  if (entry.isDirectory()) {
37306
- found.push(...scriptsUnder(join23(directory, entry.name), path));
37496
+ found.push(...scriptsUnder(join24(directory, entry.name), path));
37307
37497
  } else if (entry.isFile() && entry.name.endsWith(".gd")) {
37308
37498
  found.push(path);
37309
37499
  }
@@ -37311,7 +37501,7 @@ function scriptsUnder(directory, prefix) {
37311
37501
  return found;
37312
37502
  }
37313
37503
  function hasMainScene(projectFile) {
37314
- const scene = parseProjectGodot(readFileSync21(projectFile, "utf8"))["application"]?.["run/main_scene"];
37504
+ const scene = parseProjectGodot(readFileSync22(projectFile, "utf8"))["application"]?.["run/main_scene"];
37315
37505
  return typeof scene === "string" && scene !== "";
37316
37506
  }
37317
37507
  function runtimeAddonAnnouncesEditors(projectPath) {
@@ -37319,13 +37509,13 @@ function runtimeAddonAnnouncesEditors(projectPath) {
37319
37509
  return false;
37320
37510
  }
37321
37511
  try {
37322
- return readFileSync21(join23(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
37512
+ return readFileSync22(join24(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
37323
37513
  } catch {
37324
37514
  return false;
37325
37515
  }
37326
37516
  }
37327
37517
  function editorPlaysHeadless(projectFile) {
37328
- const runArgs = parseProjectGodot(readFileSync21(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
37518
+ const runArgs = parseProjectGodot(readFileSync22(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
37329
37519
  return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
37330
37520
  }
37331
37521
  function camelCased(params) {
@@ -37345,7 +37535,7 @@ var STATIC_LIST_HINT = { ttlMs: 5 * 60000, cacheScope: "private" };
37345
37535
  class GodotServer {
37346
37536
  stdio = null;
37347
37537
  locator = new GodotLocator;
37348
- operationsScript = join23(__dirname2, "godot", "operations", "godot_operations.gd");
37538
+ operationsScript = join24(__dirname2, "godot", "operations", "godot_operations.gd");
37349
37539
  godotBridge;
37350
37540
  tools = buildToolDefinitions();
37351
37541
  activeProcess = null;
@@ -37370,6 +37560,7 @@ class GodotServer {
37370
37560
  launchedDropped = null;
37371
37561
  closedHere = null;
37372
37562
  inFlight = new Map;
37563
+ callsOnGames = new Set;
37373
37564
  callsTaken = 0;
37374
37565
  clientNoted = null;
37375
37566
  parentCommand = null;
@@ -37601,14 +37792,14 @@ class GodotServer {
37601
37792
  }
37602
37793
  noteTheStart() {
37603
37794
  const cwd = process.cwd();
37604
- const mcpJson = join23(cwd, ".mcp.json");
37795
+ const mcpJson = join24(cwd, ".mcp.json");
37605
37796
  const announced = this.ownProject === null ? null : readAnnouncement(announcementPath(this.ownProject));
37606
37797
  const predecessor = announced !== null && announced.pid !== process.pid && alive(announced.pid) ? announced.pid : null;
37607
37798
  writeLifecycle("started", {
37608
37799
  argv: process.argv.slice(1),
37609
37800
  cwd,
37610
37801
  project: this.ownProject,
37611
- mcpJson: existsSync17(mcpJson) ? mcpJson : null
37802
+ mcpJson: existsSync18(mcpJson) ? mcpJson : null
37612
37803
  });
37613
37804
  if (predecessor !== null) {
37614
37805
  writeLifecycle("replaced", { project: this.ownProject, predecessor });
@@ -37749,7 +37940,7 @@ class GodotServer {
37749
37940
  return checked.response;
37750
37941
  }
37751
37942
  const named = args["projectPath"];
37752
- if (typeof named === "string" && existsSync17(join23(named, "project.godot"))) {
37943
+ if (typeof named === "string" && existsSync18(join24(named, "project.godot"))) {
37753
37944
  this.lastProjectPath = named;
37754
37945
  }
37755
37946
  this.updates.refresh();
@@ -38156,7 +38347,15 @@ class GodotServer {
38156
38347
  property: readNonEmptyString(args, "property") ?? ""
38157
38348
  }, { tool: "runtime_inspect", op, patience });
38158
38349
  case "accessibility":
38159
- return await this.handleRuntimeCommand("get_accessibility", whichGame(args), {
38350
+ return await this.handleRuntimeCommand("get_accessibility", {
38351
+ ...whichGame(args),
38352
+ ...withDefined({
38353
+ node_path: readNonEmptyString(args, "nodePath"),
38354
+ says: readNonEmptyString(args, "says"),
38355
+ properties: readStringArray(args, "properties"),
38356
+ limit: readPositiveNumber(args, "limit")
38357
+ })
38358
+ }, {
38160
38359
  tool: "runtime_inspect",
38161
38360
  op,
38162
38361
  patience: readPositiveNumber(args, "timeoutMs") ?? Math.max(30000, this.runtimeTimeoutMs())
@@ -38207,7 +38406,14 @@ class GodotServer {
38207
38406
  ...args["index"] === undefined ? {} : { index: args["index"] }
38208
38407
  }, { tool: "runtime_input", op });
38209
38408
  }
38210
- return await this.handleRuntimeCommand(`inject_${op}`, args, { tool: "runtime_input", op });
38409
+ {
38410
+ const holding = readPositiveNumber(args, "holdFrames");
38411
+ return await this.handleRuntimeCommand(`inject_${op}`, args, {
38412
+ tool: "runtime_input",
38413
+ op,
38414
+ ...holding === undefined ? {} : { patience: patienceForFrames(holding, this.runtimeTimeoutMs()) }
38415
+ });
38416
+ }
38211
38417
  case "runtime_wait":
38212
38418
  return await this.handleRuntimeWait(op, args);
38213
38419
  case "debug_breakpoint": {
@@ -38307,8 +38513,8 @@ class GodotServer {
38307
38513
  if (path === undefined) {
38308
38514
  return { ok: false, response: this.createErrorResponse("projectPath is required.") };
38309
38515
  }
38310
- const file = join23(path, "project.godot");
38311
- if (!existsSync17(file)) {
38516
+ const file = join24(path, "project.godot");
38517
+ if (!existsSync18(file)) {
38312
38518
  return {
38313
38519
  ok: false,
38314
38520
  response: this.createErrorResponse(`Not a Godot project: ${path}`, [
@@ -38354,7 +38560,7 @@ class GodotServer {
38354
38560
  }
38355
38561
  runEnvironment(args) {
38356
38562
  const savesIn = readNonEmptyString(args, "savesIn");
38357
- if (savesIn !== undefined && !isAbsolute3(savesIn)) {
38563
+ if (savesIn !== undefined && !isAbsolute4(savesIn)) {
38358
38564
  return {
38359
38565
  ok: false,
38360
38566
  response: this.createErrorResponse(`savesIn must be an absolute path, not "${savesIn}".`, [
@@ -38938,11 +39144,11 @@ class GodotServer {
38938
39144
  if (!engine.ok) {
38939
39145
  return engine.response;
38940
39146
  }
38941
- const application = parseProjectGodot(readFileSync21(project.value.file, "utf8"))["application"] ?? {};
39147
+ const application = parseProjectGodot(readFileSync22(project.value.file, "utf8"))["application"] ?? {};
38942
39148
  const name = application["config/name"];
38943
39149
  const mainScene = application["run/main_scene"];
38944
39150
  const info = {
38945
- name: typeof name === "string" && name !== "" ? name : basename5(project.value.path),
39151
+ name: typeof name === "string" && name !== "" ? name : basename6(project.value.path),
38946
39152
  path: project.value.path,
38947
39153
  mainScene: typeof mainScene === "string" && mainScene !== "" ? mainScene : null,
38948
39154
  godotVersion: await this.godotVersion(engine.value),
@@ -38990,22 +39196,26 @@ class GodotServer {
38990
39196
  if (!output.ok) {
38991
39197
  return this.createErrorResponse(output.reason, PATH_SOLUTIONS);
38992
39198
  }
39199
+ const noPreset = exportPresetRefusal(project.value.path, preset);
39200
+ if (noPreset !== null) {
39201
+ return this.createErrorResponse(noPreset, ["project_export list reads the presets the project has"]);
39202
+ }
38993
39203
  const engine = await this.engine();
38994
39204
  if (!engine.ok) {
38995
39205
  return engine.response;
38996
39206
  }
38997
- const folder = dirname16(output.absolutePath);
39207
+ const folder = dirname17(output.absolutePath);
38998
39208
  try {
38999
39209
  mkdirSync14(folder, { recursive: true });
39000
39210
  } catch (error) {
39001
- return this.createErrorResponse(`Cannot create ${dirname16(output.relativePath)} to export into: ${errorMessage(error)}`);
39211
+ return this.createErrorResponse(`Cannot create ${dirname17(output.relativePath)} to export into: ${errorMessage(error)}`);
39002
39212
  }
39003
39213
  const debug = readBoolean(args, "debug") ?? false;
39004
39214
  const exportLogs = scratchDirectory("export");
39005
39215
  const exportArgs = [
39006
39216
  "--headless",
39007
39217
  "--log-file",
39008
- join23(exportLogs, "engine.log"),
39218
+ join24(exportLogs, "engine.log"),
39009
39219
  "--path",
39010
39220
  project.value.path,
39011
39221
  debug ? "--export-debug" : "--export-release",
@@ -39013,8 +39223,12 @@ class GodotServer {
39013
39223
  output.absolutePath
39014
39224
  ];
39015
39225
  this.logDebug(`Exporting: ${engine.value} ${exportArgs.join(" ")}`);
39016
- const writtenAt = () => existsSync17(output.absolutePath) ? statSync9(output.absolutePath).mtimeMs : null;
39226
+ const writtenAt = () => existsSync18(output.absolutePath) ? statSync10(output.absolutePath).mtimeMs : null;
39017
39227
  const before = writtenAt();
39228
+ const freed = freeLibraryCopies(project.value.path);
39229
+ if (freed.setAside.length > 0 || freed.swept.length > 0) {
39230
+ this.logDebug(`Before the export: set aside ${freed.setAside.join(", ")}; swept ${freed.swept.join(", ")}`);
39231
+ }
39018
39232
  let ending;
39019
39233
  try {
39020
39234
  ending = await runEngine(engine.value, exportArgs, { timeout: { ms: 300000, said: "five minutes" } });
@@ -39077,10 +39291,10 @@ class GodotServer {
39077
39291
  if (movie !== null) {
39078
39292
  return this.createErrorResponse(movie);
39079
39293
  }
39080
- const missing = paths.value.filter((path) => !existsSync17(join23(project.value.path, path.slice("res://".length))));
39294
+ const missing = paths.value.filter((path) => !existsSync18(join24(project.value.path, path.slice("res://".length))));
39081
39295
  const asked = paths.value.filter((path) => !missing.includes(path));
39082
39296
  const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
39083
- const noRunner = !existsSync17(join23(project.value.path, runner));
39297
+ const noRunner = !existsSync18(join24(project.value.path, runner));
39084
39298
  if (asked.length === 0) {
39085
39299
  const verdict = `nothing at ${missing.join(" and ")}`;
39086
39300
  const andNoRunner = noRunner ? `, and gdUnit4 is not installed in this project: no ${runner}` : "";
@@ -39171,21 +39385,21 @@ class GodotServer {
39171
39385
  await untilGone([run.process.pid]);
39172
39386
  }
39173
39387
  await run.settled;
39174
- const reportsDir = join23(project.value.path, ".godot", ...ours.split("/"));
39388
+ const reportsDir = join24(project.value.path, ".godot", ...ours.split("/"));
39175
39389
  let report = null;
39176
39390
  let reportProblem = null;
39177
39391
  try {
39178
- const written = existsSync17(reportsDir) ? readdirSync9(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
39392
+ const written = existsSync18(reportsDir) ? readdirSync10(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
39179
39393
  const newest = written.at(-1);
39180
39394
  if (newest !== undefined) {
39181
- report = parseJUnit(readFileSync21(join23(reportsDir, newest, "results.xml"), "utf8"));
39395
+ report = parseJUnit(readFileSync22(join24(reportsDir, newest, "results.xml"), "utf8"));
39182
39396
  }
39183
39397
  } catch (error) {
39184
39398
  reportProblem = errorMessage(error);
39185
39399
  } finally {
39186
39400
  discard(reportsDir, userData);
39187
39401
  try {
39188
- rmdirSync2(dirname16(reportsDir));
39402
+ rmdirSync2(dirname17(reportsDir));
39189
39403
  } catch {}
39190
39404
  }
39191
39405
  const engineEntries = foldedRepeats(run.log.select({
@@ -39354,7 +39568,7 @@ class GodotServer {
39354
39568
  }
39355
39569
  async godotVersion(godotPath) {
39356
39570
  try {
39357
- const { stdout } = await run4(godotPath, ["--version"], { timeout: 1e4 });
39571
+ const { stdout } = await runHidden(godotPath, ["--version"], { timeout: 1e4 });
39358
39572
  this.lastGodotVersion = stdout.trim();
39359
39573
  return this.lastGodotVersion;
39360
39574
  } catch (error) {
@@ -39415,6 +39629,10 @@ class GodotServer {
39415
39629
  return markIfStale(answer, this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest(), this.editorCodeOnDisk());
39416
39630
  }
39417
39631
  async diagnosticsOf(args) {
39632
+ const waited = await this.waitForTheEditorOpenedHere(args, []);
39633
+ if (waited !== null) {
39634
+ return { refusal: waited, perScript: false };
39635
+ }
39418
39636
  const answer = await this.handleLSP("lsp_get_diagnostics", args);
39419
39637
  const payload = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
39420
39638
  const refused = this.languageServerRefusal(payload, "Diagnostics");
@@ -39445,11 +39663,11 @@ class GodotServer {
39445
39663
  const cached = cachedClasses(projectPath);
39446
39664
  const sourceOf = (resourcePath) => {
39447
39665
  const contained = resolveWithinProject(projectPath, resourcePath);
39448
- if (!contained.ok || !existsSync17(contained.absolutePath)) {
39666
+ if (!contained.ok || !existsSync18(contained.absolutePath)) {
39449
39667
  return null;
39450
39668
  }
39451
39669
  try {
39452
- return readFileSync21(contained.absolutePath, "utf8");
39670
+ return readFileSync22(contained.absolutePath, "utf8");
39453
39671
  } catch {
39454
39672
  return null;
39455
39673
  }
@@ -39499,9 +39717,34 @@ class GodotServer {
39499
39717
  const recorded = readClassNote(projectPath)?.shortListEditorPid;
39500
39718
  return recorded !== undefined && recorded === (this.godotBridge.getStatus().editorPid ?? null);
39501
39719
  }
39720
+ unseenBeforeAnUnfinishedScan(projectPath) {
39721
+ const noted = readClassNote(projectPath)?.unseenBeforeAnUnfinishedScan;
39722
+ if (noted === undefined || noted.editorPid !== (this.godotBridge.getStatus().editorPid ?? null)) {
39723
+ return [];
39724
+ }
39725
+ return [...noted.classes];
39726
+ }
39727
+ noteUnseenBeforeAnUnfinishedScan(projectPath, classes) {
39728
+ const note = readClassNote(projectPath);
39729
+ const held = note?.unseenBeforeAnUnfinishedScan;
39730
+ if (classes.length === 0 && held === undefined) {
39731
+ return;
39732
+ }
39733
+ const { unseenBeforeAnUnfinishedScan: _replaced, ...rest } = note ?? { rebuiltWith: [] };
39734
+ writeClassNote(projectPath, {
39735
+ ...rest,
39736
+ ...classes.length === 0 ? {} : {
39737
+ unseenBeforeAnUnfinishedScan: {
39738
+ editorPid: this.godotBridge.getStatus().editorPid ?? null,
39739
+ classes: [...classes].sort()
39740
+ }
39741
+ }
39742
+ });
39743
+ }
39502
39744
  noteAShortListEditor(projectPath) {
39503
39745
  const note = readClassNote(projectPath);
39504
39746
  writeClassNote(projectPath, {
39747
+ ...note,
39505
39748
  rebuiltWith: note?.rebuiltWith ?? [],
39506
39749
  shortListEditorPid: this.godotBridge.getStatus().editorPid ?? null
39507
39750
  });
@@ -39514,11 +39757,11 @@ class GodotServer {
39514
39757
  }
39515
39758
  sourceOfClass(projectPath, resourcePath) {
39516
39759
  const contained = resolveWithinProject(projectPath, resourcePath);
39517
- if (!contained.ok || !existsSync17(contained.absolutePath)) {
39760
+ if (!contained.ok || !existsSync18(contained.absolutePath)) {
39518
39761
  return null;
39519
39762
  }
39520
39763
  try {
39521
- return readFileSync21(contained.absolutePath, "utf8");
39764
+ return readFileSync22(contained.absolutePath, "utf8");
39522
39765
  } catch {
39523
39766
  return null;
39524
39767
  }
@@ -39526,7 +39769,17 @@ class GodotServer {
39526
39769
  async handleLSP(toolName, args) {
39527
39770
  return handleLSPTool(await this.lsp(), toolName, args);
39528
39771
  }
39772
+ async waitForTheEditorOpenedHere(args, instead) {
39773
+ if (this.godotBridge.isConnected() || this.launchedEditor === null || !await this.launchedEditorIsUp()) {
39774
+ return null;
39775
+ }
39776
+ return await this.noEditorYet(args, instead);
39777
+ }
39529
39778
  async askScriptInfo(toolName, asked, args) {
39779
+ const waited = await this.waitForTheEditorOpenedHere(args, []);
39780
+ if (waited !== null) {
39781
+ return waited;
39782
+ }
39530
39783
  const answer = await this.handleLSP(toolName, args);
39531
39784
  const refused = this.languageServerRefusal(asParams(JSON.parse(answer.content[0]?.text ?? "{}")), asked);
39532
39785
  return refused === null ? answer : refused.refusal;
@@ -39666,7 +39919,7 @@ class GodotServer {
39666
39919
  ...Array.isArray(stack) ? {
39667
39920
  stack: stack.map((frame) => ({
39668
39921
  ...frame,
39669
- ...projectPath !== "" && typeof frame["script"] === "string" && frame["script"].startsWith("res://") ? { path: join23(projectPath, frame["script"].slice("res://".length)) } : {}
39922
+ ...projectPath !== "" && typeof frame["script"] === "string" && frame["script"].startsWith("res://") ? { path: join24(projectPath, frame["script"].slice("res://".length)) } : {}
39670
39923
  }))
39671
39924
  } : {}
39672
39925
  });
@@ -39936,7 +40189,7 @@ class GodotServer {
39936
40189
  return null;
39937
40190
  }
39938
40191
  try {
39939
- return settingKeys(readFileSync21(join23(projectPath, "project.godot"), "utf8"));
40192
+ return settingKeys(readFileSync22(join24(projectPath, "project.godot"), "utf8"));
39940
40193
  } catch {
39941
40194
  return null;
39942
40195
  }
@@ -40201,6 +40454,7 @@ class GodotServer {
40201
40454
  }
40202
40455
  async openAnEditor(engine, projectPath, ports, hidden) {
40203
40456
  clearEditorLog(projectPath);
40457
+ freeLibraryCopies(projectPath);
40204
40458
  const launched = await launchOutsideTheTree({
40205
40459
  command: engine,
40206
40460
  args: editorArguments(projectPath, ports, editorLogPath(projectPath), hidden),
@@ -40251,6 +40505,17 @@ class GodotServer {
40251
40505
  if (sceneToRun && !sceneToRun.ok) {
40252
40506
  return this.createErrorResponse(sceneToRun.reason, PATH_SOLUTIONS);
40253
40507
  }
40508
+ const build = readNonEmptyString(args, "executable");
40509
+ const exported = build === undefined ? null : exportedBuild(project.value.path, build);
40510
+ if (exported !== null && !exported.ok) {
40511
+ return this.createErrorResponse(exported.reason, PATH_SOLUTIONS);
40512
+ }
40513
+ if (exported !== null && scene !== undefined) {
40514
+ 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.");
40515
+ }
40516
+ if (exported !== null && readBoolean(args, "profile") === true) {
40517
+ 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.");
40518
+ }
40254
40519
  const given = this.gameArguments(args);
40255
40520
  if (!given.ok) {
40256
40521
  return given.response;
@@ -40267,13 +40532,13 @@ class GodotServer {
40267
40532
  if (!asked.ok) {
40268
40533
  return asked.response;
40269
40534
  }
40270
- if (!sceneToRun && !hasMainScene(project.value.file)) {
40535
+ if (exported === null && !sceneToRun && !hasMainScene(project.value.file)) {
40271
40536
  return this.createErrorResponse("The project sets no main scene, so there is nothing to run.", [
40272
40537
  "Pass scene to run one scene",
40273
40538
  "Choose the main scene with project_settings set_main_scene"
40274
40539
  ]);
40275
40540
  }
40276
- const engine = await this.engine();
40541
+ const engine = exported?.ok === true ? { ok: true, value: exported.absolutePath } : await this.engine();
40277
40542
  if (!engine.ok) {
40278
40543
  return engine.response;
40279
40544
  }
@@ -40311,11 +40576,11 @@ class GodotServer {
40311
40576
  const editorWouldPlay = !headless || editorPlaysHeadless(project.value.file);
40312
40577
  const silent = resolveSilent(args["silent"], process.env) && !headless;
40313
40578
  const profiled = readBoolean(args, "profile") === true;
40314
- if (this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0 && flags.value.length === 0 && !needsItsOwnProcess(asked.value) && !silent) {
40579
+ if (exported === null && this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0 && flags.value.length === 0 && !needsItsOwnProcess(asked.value) && !silent) {
40315
40580
  return await this.playThroughEditor(sceneArgument, refreshed.value, scanned, project.value.path, alreadyPlaying, runtimeWaitMs, ended, profiled);
40316
40581
  }
40317
40582
  const cmdArgs = runArguments({
40318
- projectPath: project.value.path,
40583
+ projectPath: exported === null ? project.value.path : null,
40319
40584
  headless,
40320
40585
  scene: sceneArgument,
40321
40586
  userArgs: given.value,
@@ -40323,7 +40588,7 @@ class GodotServer {
40323
40588
  silent
40324
40589
  });
40325
40590
  this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
40326
- 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);
40591
+ 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);
40327
40592
  if ("error" in started) {
40328
40593
  return this.createErrorResponse(`The game could not be started: ${started.error}`, [
40329
40594
  "editor_status says which Godot this server runs and whether it answers"
@@ -40332,7 +40597,7 @@ class GodotServer {
40332
40597
  started.announcedBefore = alreadyPlaying;
40333
40598
  started.bootScene = runtimeWaitMs.scene;
40334
40599
  this.activeProcess = started;
40335
- 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}` : "";
40600
+ 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}` : "";
40336
40601
  const endedForThis = endedToStartThis(ended);
40337
40602
  const stranded = wereHereFirst.filter((one) => one.pid !== ended?.pid && alive(one.pid));
40338
40603
  const handed = this.dapHoldingBreakpointsOf(project.value.path).breakpointsHeld();
@@ -40342,6 +40607,7 @@ class GodotServer {
40342
40607
  through: "gdharness",
40343
40608
  alsoRunning: stranded.length === 0 ? undefined : stranded.map((one) => one.pid),
40344
40609
  pid: started.pid,
40610
+ executable: exported?.ok === true ? exported.relativePath : undefined,
40345
40611
  arguments: cmdArgs,
40346
40612
  argsNote: engineFlagsNote(given.value),
40347
40613
  transcript: started.transcript,
@@ -40353,13 +40619,13 @@ class GodotServer {
40353
40619
  endedPreviousRun: endedPreviousRun(ended),
40354
40620
  previousRunLeft: previousRunLeft(ended),
40355
40621
  ...previousRunHow(ended),
40356
- runtime: await this.runtimeUp(project.value.path, alreadyPlaying, runtimeWaitMs, given.value.length > 0),
40622
+ runtime: exported !== null ? undefined : await this.runtimeUp(project.value.path, alreadyPlaying, runtimeWaitMs, given.value.length > 0),
40357
40623
  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}`
40358
40624
  });
40359
40625
  }
40360
40626
  async runtimeUp(projectPath, before, budget, withArgs) {
40361
40627
  const budgetMs = budget.ms;
40362
- if (!existsSync17(join23(projectPath, RUNTIME_AUTOLOAD.path))) {
40628
+ if (!existsSync18(join24(projectPath, RUNTIME_AUTOLOAD.path))) {
40363
40629
  return runtimeVerdict(null, {
40364
40630
  addon: false,
40365
40631
  budgetMs,
@@ -40444,7 +40710,7 @@ class GodotServer {
40444
40710
  }
40445
40711
  let announcedAt;
40446
40712
  try {
40447
- announcedAt = statSync9(endpoint.file).mtimeMs;
40713
+ announcedAt = statSync10(endpoint.file).mtimeMs;
40448
40714
  } catch {
40449
40715
  return;
40450
40716
  }
@@ -40638,7 +40904,7 @@ class GodotServer {
40638
40904
  return 0;
40639
40905
  }
40640
40906
  try {
40641
- return reportAlreadyInTranscript(readFileSync21(game.transcript), readFileSync21(report));
40907
+ return reportAlreadyInTranscript(readFileSync22(game.transcript), readFileSync22(report));
40642
40908
  } catch (error) {
40643
40909
  this.logDebug(`Could not compare a picked-up run's report with its transcript: ${errorMessage(error)}`);
40644
40910
  return 0;
@@ -40793,7 +41059,10 @@ class GodotServer {
40793
41059
  return gone ? "gone" : "lingering";
40794
41060
  }
40795
41061
  spawnGame(godotPath, cmdArgs, env) {
40796
- const child = spawn4(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
41062
+ const child = spawnHidden(godotPath, cmdArgs, {
41063
+ stdio: ["ignore", "pipe", "pipe"],
41064
+ ...env ? { env } : {}
41065
+ });
40797
41066
  const cancelled = callSignal();
40798
41067
  if (cancelled !== undefined) {
40799
41068
  const end = () => {
@@ -40866,7 +41135,7 @@ class GodotServer {
40866
41135
  });
40867
41136
  return Object.assign(started, { settled });
40868
41137
  }
40869
- async launchKeptGame(godotPath, cmdArgs, projectPath, env, desktop, profiled = false) {
41138
+ async launchKeptGame(godotPath, cmdArgs, projectPath, env, desktop, profiled = false, debugged = true) {
40870
41139
  const startedAt = Date.now();
40871
41140
  const transcript = openTranscript(startedAt);
40872
41141
  closeSync2(transcript.fd);
@@ -40882,7 +41151,7 @@ class GodotServer {
40882
41151
  projectPath,
40883
41152
  ...this.servedBy(),
40884
41153
  ...profile === undefined ? {} : { profile },
40885
- breakpoints: this.breakpointsForAGame(projectPath)
41154
+ ...debugged ? { breakpoints: this.breakpointsForAGame(projectPath) } : { debugged: false }
40886
41155
  },
40887
41156
  ...desktop === undefined ? {} : { desktop }
40888
41157
  });
@@ -41318,7 +41587,7 @@ class GodotServer {
41318
41587
  }
41319
41588
  const going = await this.runStillGoing(run);
41320
41589
  const projectPath = run.projectPath;
41321
- const projectGone = projectPath !== null && !existsSync17(join23(projectPath, "project.godot"));
41590
+ const projectGone = projectPath !== null && !existsSync18(join24(projectPath, "project.godot"));
41322
41591
  if (projectGone && callersName !== undefined) {
41323
41592
  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"]);
41324
41593
  }
@@ -41606,7 +41875,7 @@ class GodotServer {
41606
41875
  notes.push(endedWithoutACode({
41607
41876
  throughEditor: run.throughEditor,
41608
41877
  announced: announced !== undefined || run.announcedPid !== undefined,
41609
- couldAnnounce: run.projectPath !== null && existsSync17(join23(run.projectPath, RUNTIME_AUTOLOAD.path)),
41878
+ couldAnnounce: run.projectPath !== null && existsSync18(join24(run.projectPath, RUNTIME_AUTOLOAD.path)),
41610
41879
  endedHere: typeof run.endedHere === "string",
41611
41880
  pickedUp: run.pickedUp === true
41612
41881
  }));
@@ -41846,8 +42115,8 @@ class GodotServer {
41846
42115
  try {
41847
42116
  const status = asParams(await this.godotBridge.invokeTool("scan_status", {}));
41848
42117
  return status["scanning"] === true || status["importing"] === true;
41849
- } catch {
41850
- return false;
42118
+ } catch (error) {
42119
+ return error instanceof BridgeTimeout;
41851
42120
  }
41852
42121
  }
41853
42122
  async handleRescanFilesystem(args) {
@@ -41858,7 +42127,7 @@ class GodotServer {
41858
42127
  let writtenBefore = null;
41859
42128
  let blind = { unseen: [] };
41860
42129
  let waitedForEditorMs = 0;
41861
- let first;
42130
+ let startUnanswered = false;
41862
42131
  let completedBefore;
41863
42132
  let uidsReread = [];
41864
42133
  let uidsDuplicatedOnDisk = [];
@@ -41875,7 +42144,11 @@ class GodotServer {
41875
42144
  before = projectPath === "" ? null : cachedClasses(projectPath);
41876
42145
  writtenBefore = projectPath === "" ? null : cacheWrittenAt(projectPath);
41877
42146
  blind = await this.classesTheEditorCannotSee(args);
41878
- first = await this.handleViaBridge("rescan_filesystem", args);
42147
+ const first = await this.askViaBridge("rescan_filesystem", args);
42148
+ if (first instanceof BridgeTimeout) {
42149
+ startUnanswered = true;
42150
+ break;
42151
+ }
41879
42152
  if (first.isError) {
41880
42153
  return first;
41881
42154
  }
@@ -41898,7 +42171,17 @@ class GodotServer {
41898
42171
  let idleSince = null;
41899
42172
  while (Date.now() - started < timeoutMs) {
41900
42173
  await new Promise((settle) => setTimeout(settle, 100));
41901
- const status = asParams(await this.godotBridge.invokeTool("rescan_filesystem", { ...args, statusOnly: true }));
42174
+ let status;
42175
+ try {
42176
+ status = asParams(await this.godotBridge.invokeTool("rescan_filesystem", { ...args, statusOnly: true }));
42177
+ } catch (error) {
42178
+ if (!(error instanceof BridgeTimeout)) {
42179
+ throw error;
42180
+ }
42181
+ busy = true;
42182
+ idleSince = null;
42183
+ continue;
42184
+ }
41902
42185
  const pending = Boolean(status["pending"]) && Date.now() - started < SCAN_START_MS;
41903
42186
  const completed = readNumber(status, "scansCompleted");
41904
42187
  const notYetOver = completedBefore !== undefined && completed !== undefined && completed <= completedBefore;
@@ -41981,12 +42264,20 @@ class GodotServer {
41981
42264
  }
41982
42265
  const checked = busy ? { unseen: [] } : await this.classesTheEditorCannotSee(args);
41983
42266
  const unseen = checked.unseen;
41984
- const broughtIn = busy ? [] : blind.unseen.map((one) => one.className).filter((name) => !unseen.some((still) => still.className === name));
42267
+ const carried = projectPath === "" ? [] : this.unseenBeforeAnUnfinishedScan(projectPath);
42268
+ const unseenBefore = [...new Set([...blind.unseen.map((one) => one.className), ...carried])];
42269
+ const broughtIn = busy ? [] : unseenBefore.filter((name) => !unseen.some((still) => still.className === name));
42270
+ if (projectPath !== "") {
42271
+ this.noteUnseenBeforeAnUnfinishedScan(projectPath, busy ? unseenBefore : []);
42272
+ }
41985
42273
  const dependents = busy || projectPath === "" || broughtIn.length === 0 ? null : await this.reloadDependents(projectPath, broughtIn);
41986
42274
  const stillGone = lost.filter((name) => !restored.includes(name));
41987
42275
  const notes = [];
41988
42276
  if (busy) {
41989
- notes.push("The editor was still scanning or importing when the wait ran out, so new files may not be visible yet.");
42277
+ 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.");
42278
+ if (unseenBefore.length > 0) {
42279
+ 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.`);
42280
+ }
41990
42281
  } else if (stillGone.length > 0) {
41991
42282
  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.");
41992
42283
  } else if (lost.length > 0) {
@@ -42104,8 +42395,9 @@ class GodotServer {
42104
42395
  const droppedByTheEditor = lost.filter((name) => !cameBack.includes(name));
42105
42396
  const seenLosing = droppedByTheEditor.length > 0 && this.godotBridge.isConnected();
42106
42397
  writeClassNote(projectPath, {
42398
+ ...noted,
42107
42399
  rebuiltWith: [...after.keys()].sort(),
42108
- ...seenLosing ? { shortListEditorPid: this.godotBridge.getStatus().editorPid ?? null } : noted?.shortListEditorPid === undefined ? {} : { shortListEditorPid: noted.shortListEditorPid }
42400
+ ...seenLosing ? { shortListEditorPid: this.godotBridge.getStatus().editorPid ?? null } : {}
42109
42401
  });
42110
42402
  const notes = [];
42111
42403
  if (seenLosing) {
@@ -42171,7 +42463,7 @@ class GodotServer {
42171
42463
  if (!scriptPath.ok) {
42172
42464
  return this.createErrorResponse(scriptPath.reason);
42173
42465
  }
42174
- if (!existsSync17(scriptPath.absolutePath)) {
42466
+ if (!existsSync18(scriptPath.absolutePath)) {
42175
42467
  return this.createErrorResponse(`Script file does not exist: ${scriptPath.absolutePath}`);
42176
42468
  }
42177
42469
  const asked = readNonEmptyString(args, "newScriptPath");
@@ -42179,6 +42471,10 @@ class GodotServer {
42179
42471
  if (moveTo !== null && !moveTo.ok) {
42180
42472
  return this.createErrorResponse(moveTo.reason);
42181
42473
  }
42474
+ const waited = await this.waitForTheEditorOpenedHere(args, []);
42475
+ if (waited !== null) {
42476
+ return waited;
42477
+ }
42182
42478
  const languageServer = {
42183
42479
  problem: null,
42184
42480
  initialized: false
@@ -42421,6 +42717,16 @@ class GodotServer {
42421
42717
  ]);
42422
42718
  }
42423
42719
  async handleViaBridge(toolName, args, askedAs) {
42720
+ const answered = await this.askViaBridge(toolName, args, askedAs);
42721
+ if (!(answered instanceof BridgeTimeout)) {
42722
+ return answered;
42723
+ }
42724
+ 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.`, [
42725
+ "editor_status says whether it is scanning or importing",
42726
+ "The editor still carries the request out once it is free, so read what it changed before asking again"
42727
+ ]);
42728
+ }
42729
+ async askViaBridge(toolName, args, askedAs) {
42424
42730
  if (!this.godotBridge.isConnected()) {
42425
42731
  const alternative = askedAs !== undefined && Object.hasOwn(EDITOR_FREE_INSTEAD, askedAs) ? EDITOR_FREE_INSTEAD[askedAs] : undefined;
42426
42732
  const refused = await this.noEditorYet(args, alternative === undefined ? [] : [alternative]);
@@ -42439,6 +42745,9 @@ class GodotServer {
42439
42745
  try {
42440
42746
  return this.jsonTextResponse(markIfStale(await this.godotBridge.invokeTool(toolName, args), this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest(), this.editorCodeOnDisk()));
42441
42747
  } catch (error) {
42748
+ if (error instanceof BridgeTimeout) {
42749
+ return error;
42750
+ }
42442
42751
  const message = errorMessage(error);
42443
42752
  if (message.includes(`Unknown tool: ${toolName}`)) {
42444
42753
  const status = this.godotBridge.getStatus();
@@ -42499,7 +42808,7 @@ class GodotServer {
42499
42808
  async ownRunOnItsWay(projectPath, budgetMs) {
42500
42809
  const run = await this.currentRun();
42501
42810
  const project = projectPath ?? run?.projectPath ?? null;
42502
- if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync17(join23(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
42811
+ if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync18(join24(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
42503
42812
  return null;
42504
42813
  }
42505
42814
  const already = runtimesAnnounced().running;
@@ -42508,7 +42817,7 @@ class GodotServer {
42508
42817
  return !run.announcedBefore.has(one.pid);
42509
42818
  }
42510
42819
  try {
42511
- return statSync9(one.file).mtimeMs >= run.startedAt;
42820
+ return statSync10(one.file).mtimeMs >= run.startedAt;
42512
42821
  } catch {
42513
42822
  return false;
42514
42823
  }
@@ -42584,12 +42893,25 @@ class GodotServer {
42584
42893
  }
42585
42894
  }
42586
42895
  const screenshotDir = expectsScreenshot ? scratchDirectory("runtime-screenshot") : null;
42587
- const screenshotPath = screenshotDir ? join23(screenshotDir, "capture.png") : null;
42896
+ const screenshotPath = screenshotDir ? join24(screenshotDir, "capture.png") : null;
42588
42897
  const staleNote = this.runtimeStaleNote(choice.endpoint);
42589
42898
  const stale = staleNote === undefined ? {} : { addonIsStale: true, staleNote };
42590
42899
  const refused = (message) => this.createErrorResponse(staleNote === undefined ? message : `${message}
42591
42900
 
42592
42901
  ${staleNote}`);
42902
+ const onGame = {
42903
+ pid: choice.endpoint.pid,
42904
+ what: `${asked.tool} ${asked.op}`,
42905
+ alongside: new Set
42906
+ };
42907
+ for (const other of this.callsOnGames) {
42908
+ if (other.pid === onGame.pid) {
42909
+ other.alongside.add(onGame.what);
42910
+ onGame.alongside.add(other.what);
42911
+ }
42912
+ }
42913
+ this.callsOnGames.add(onGame);
42914
+ const alongside = () => onGame.alongside.size === 0 ? {} : { ranAlongside: [...onGame.alongside], alongsideNote: ALONGSIDE_NOTE };
42593
42915
  try {
42594
42916
  const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs, !expectsScreenshot);
42595
42917
  if (!reply.ok && reply.requestId !== undefined) {
@@ -42600,7 +42922,8 @@ ${staleNote}`);
42600
42922
  waitedMs: timeoutMs,
42601
42923
  note: `${reply.message} ${longerWaitNote(asked.tool, asked.op, timeoutMs)}`,
42602
42924
  ...answeredBy,
42603
- ...stale
42925
+ ...stale,
42926
+ ...alongside()
42604
42927
  });
42605
42928
  }
42606
42929
  if (!reply.ok) {
@@ -42608,7 +42931,7 @@ ${staleNote}`);
42608
42931
  }
42609
42932
  const { id: _id, ...payload } = reply.payload;
42610
42933
  if (!expectsScreenshot) {
42611
- return this.jsonTextResponse({ ...payload, ...answeredBy, ...stale });
42934
+ return this.jsonTextResponse({ ...payload, ...answeredBy, ...stale, ...alongside() });
42612
42935
  }
42613
42936
  const returnedPath = readString(payload, "path");
42614
42937
  if (readString(payload, "type") !== "screenshot_file" || !returnedPath || !screenshotPath) {
@@ -42625,18 +42948,22 @@ ${staleNote}`);
42625
42948
  return this.createErrorResponse(`The capture was taken and could not be saved at ${saveTo}: ${errorMessage(error)}`);
42626
42949
  }
42627
42950
  }
42951
+ const ranWith = [...onGame.alongside];
42628
42952
  return {
42629
42953
  content: [
42630
42954
  {
42631
42955
  type: "text",
42632
- text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}${captureDetail(payload)}${saveTo === null ? "" : `. Saved to ${saveTo}`}${staleNote === undefined ? "" : `
42956
+ text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}${captureDetail(payload)}${saveTo === null ? "" : `. Saved to ${saveTo}`}${ranWith.length === 0 ? "" : `
42957
+
42958
+ Ran alongside ${ranWith.join(", ")}. ${ALONGSIDE_NOTE}`}${staleNote === undefined ? "" : `
42633
42959
 
42634
42960
  ${staleNote}`}`
42635
42961
  },
42636
- { type: "image", data: readFileSync21(screenshotPath).toString("base64"), mimeType: "image/png" }
42962
+ { type: "image", data: readFileSync22(screenshotPath).toString("base64"), mimeType: "image/png" }
42637
42963
  ]
42638
42964
  };
42639
42965
  } finally {
42966
+ this.callsOnGames.delete(onGame);
42640
42967
  discard(screenshotDir);
42641
42968
  }
42642
42969
  }