gdharness 1.2.28 → 1.2.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.js CHANGED
@@ -3356,7 +3356,6 @@ 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,
@@ -3371,12 +3370,11 @@ import {
3371
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
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)}`;
@@ -29521,7 +29627,7 @@ import { existsSync as existsSync12, readFileSync as readFileSync14, realpathSyn
29521
29627
  import { dirname as dirname9, join as join15 } from "node:path";
29522
29628
 
29523
29629
  // src/library-copies.ts
29524
- import { existsSync as existsSync9, readdirSync as readdirSync4, readFileSync as readFileSync12, renameSync, unlinkSync as unlinkSync2 } from "node:fs";
29630
+ import { existsSync as existsSync9, readdirSync as readdirSync4, readFileSync as readFileSync12, renameSync as renameSync2, unlinkSync as unlinkSync2 } from "node:fs";
29525
29631
  import { basename as basename2, dirname as dirname8, isAbsolute as isAbsolute3, join as join12 } from "node:path";
29526
29632
  function freeLibraryCopies(projectPath, platform = process.platform) {
29527
29633
  const setAside = [];
@@ -29535,7 +29641,7 @@ function freeLibraryCopies(projectPath, platform = process.platform) {
29535
29641
  if (existsSync9(join12(folder, copy))) {
29536
29642
  const aside = join12(folder, `${copy}.aside-${process.pid}-${Date.now()}`);
29537
29643
  try {
29538
- renameSync(join12(folder, copy), aside);
29644
+ renameSync2(join12(folder, copy), aside);
29539
29645
  setAside.push(aside);
29540
29646
  } catch {}
29541
29647
  }
@@ -29603,7 +29709,7 @@ function readOrNull(path) {
29603
29709
 
29604
29710
  // src/reimport.ts
29605
29711
  import { createHash as createHash2 } from "node:crypto";
29606
- import { existsSync as existsSync10, readFileSync as readFileSync13, rmSync as rmSync5, statSync as statSync5, utimesSync, writeFileSync as writeFileSync8 } from "node:fs";
29712
+ import { existsSync as existsSync10, readFileSync as readFileSync13, rmSync as rmSync6, statSync as statSync6, utimesSync, writeFileSync as writeFileSync8 } from "node:fs";
29607
29713
  import { basename as basename3, join as join13 } from "node:path";
29608
29714
  function forceNextImport(projectPath, resourcePath) {
29609
29715
  const relative = resourcePath.replace(/^res:\/\//, "");
@@ -29611,10 +29717,10 @@ function forceNextImport(projectPath, resourcePath) {
29611
29717
  if (!existsSync10(sidecar)) {
29612
29718
  return;
29613
29719
  }
29614
- const { atime, mtimeMs } = statSync5(sidecar);
29720
+ const { atime, mtimeMs } = statSync6(sidecar);
29615
29721
  writeFileSync8(sidecar, readFileSync13(sidecar, "utf8").replace(/^valid=false\r?\n/m, ""));
29616
29722
  utimesSync(sidecar, atime, new Date(mtimeMs + 2000));
29617
- rmSync5(importRecord(projectPath, resourcePath), { force: true });
29723
+ rmSync6(importRecord(projectPath, resourcePath), { force: true });
29618
29724
  }
29619
29725
  function importRecord(projectPath, resourcePath) {
29620
29726
  const digest = createHash2("md5").update(resourcePath).digest("hex");
@@ -29622,7 +29728,7 @@ function importRecord(projectPath, resourcePath) {
29622
29728
  }
29623
29729
 
29624
29730
  // src/scratch.ts
29625
- import { existsSync as existsSync11, mkdtempSync, readdirSync as readdirSync5, rmSync as rmSync6 } from "node:fs";
29731
+ import { existsSync as existsSync11, mkdtempSync, readdirSync as readdirSync5, rmSync as rmSync7 } from "node:fs";
29626
29732
  import { tmpdir as tmpdir4 } from "node:os";
29627
29733
  import { join as join14 } from "node:path";
29628
29734
  import process7 from "node:process";
@@ -29681,7 +29787,7 @@ function discard(...paths) {
29681
29787
  return left;
29682
29788
  }
29683
29789
  function remove(path) {
29684
- rmSync6(path, SWEPT);
29790
+ rmSync7(path, SWEPT);
29685
29791
  }
29686
29792
  function tryAgain(path, attempt) {
29687
29793
  if (attempt === 0) {
@@ -30513,7 +30619,7 @@ function askTheKeeper(address, line, withinMs) {
30513
30619
  }
30514
30620
 
30515
30621
  // src/lifecycle-log.ts
30516
- import { appendFileSync, mkdirSync as mkdirSync8, renameSync as renameSync2, statSync as statSync6 } from "node:fs";
30622
+ import { appendFileSync, mkdirSync as mkdirSync8, renameSync as renameSync3, statSync as statSync7 } from "node:fs";
30517
30623
  import { dirname as dirname10, join as join16 } from "node:path";
30518
30624
  import process8 from "node:process";
30519
30625
  function lifecycleLogPath() {
@@ -30524,9 +30630,9 @@ function writeLifecycle(event, fields = {}) {
30524
30630
  const path = lifecycleLogPath();
30525
30631
  try {
30526
30632
  mkdirSync8(dirname10(path), { recursive: true });
30527
- if ((statSync6(path, { throwIfNoEntry: false })?.size ?? 0) > LIFECYCLE_LOG_LIMIT) {
30633
+ if ((statSync7(path, { throwIfNoEntry: false })?.size ?? 0) > LIFECYCLE_LOG_LIMIT) {
30528
30634
  try {
30529
- renameSync2(path, `${path}.1`);
30635
+ renameSync3(path, `${path}.1`);
30530
30636
  } catch {}
30531
30637
  }
30532
30638
  const line = {
@@ -31385,7 +31491,6 @@ async function handleLSPTool(client, toolName, args) {
31385
31491
  }
31386
31492
 
31387
31493
  // src/outside.ts
31388
- import { spawn as spawn3 } from "node:child_process";
31389
31494
  import { fileURLToPath as fileURLToPath2 } from "node:url";
31390
31495
  function changesFrom(env, base) {
31391
31496
  const changes = {};
@@ -31419,9 +31524,8 @@ function readLaunched(printed) {
31419
31524
  }
31420
31525
  async function launchOutsideTheTree(spec, runtime = process.execPath, keeper = KEEPER_SCRIPT) {
31421
31526
  return await new Promise((resolve) => {
31422
- const launcher = spawn3(runtime, [keeper, "launch"], {
31423
- stdio: ["pipe", "pipe", "pipe"],
31424
- windowsHide: true
31527
+ const launcher = spawnHidden(runtime, [keeper, "launch"], {
31528
+ stdio: ["pipe", "pipe", "pipe"]
31425
31529
  });
31426
31530
  sendSpec(launcher, {
31427
31531
  command: spec.command,
@@ -31485,9 +31589,6 @@ function notListeningNote(servers, greetedAgoMs) {
31485
31589
  }
31486
31590
 
31487
31591
  // src/process-time.ts
31488
- import { execFile as execFile3 } from "node:child_process";
31489
- import { promisify as promisify3 } from "node:util";
31490
- var run3 = promisify3(execFile3);
31491
31592
  var ASK_TIMEOUT_MS2 = 6000;
31492
31593
  async function cpuSecondsOf(pid) {
31493
31594
  try {
@@ -31502,7 +31603,7 @@ async function secondsOnWindows(pid) {
31502
31603
  return Number.isFinite(seconds) ? seconds : undefined;
31503
31604
  }
31504
31605
  async function askPosix2(pid) {
31505
- 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 });
31506
31607
  return secondsFromClock(stdout.trim());
31507
31608
  }
31508
31609
  function secondsFromClock(printed) {
@@ -31912,7 +32013,7 @@ function setupPromptHandlers(server) {
31912
32013
  }
31913
32014
 
31914
32015
  // src/rename.ts
31915
- import { existsSync as existsSync15, mkdirSync as mkdirSync9, readFileSync as readFileSync16, renameSync as renameSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync10 } from "node:fs";
32016
+ import { existsSync as existsSync15, mkdirSync as mkdirSync9, readFileSync as readFileSync16, renameSync as renameSync4, unlinkSync as unlinkSync3, writeFileSync as writeFileSync10 } from "node:fs";
31916
32017
  import { dirname as dirname12, posix as posix2 } from "node:path";
31917
32018
  class Script {
31918
32019
  path;
@@ -32649,12 +32750,12 @@ function applyRewrites(rewrites) {
32649
32750
  mkdirSync9(dirname12(rewrite.writeTo), { recursive: true });
32650
32751
  const staging = `${rewrite.writeTo}.gdharness-rename`;
32651
32752
  writeFileSync10(staging, rewrite.after, "utf8");
32652
- renameSync3(staging, rewrite.writeTo);
32753
+ renameSync4(staging, rewrite.writeTo);
32653
32754
  let movedUid = false;
32654
32755
  if (rewrite.writeTo !== rewrite.absolute) {
32655
32756
  unlinkSync3(rewrite.absolute);
32656
32757
  if (existsSync15(`${rewrite.absolute}.uid`)) {
32657
- renameSync3(`${rewrite.absolute}.uid`, `${rewrite.writeTo}.uid`);
32758
+ renameSync4(`${rewrite.absolute}.uid`, `${rewrite.writeTo}.uid`);
32658
32759
  movedUid = true;
32659
32760
  }
32660
32761
  }
@@ -32667,7 +32768,7 @@ function applyRewrites(rewrites) {
32667
32768
  if (rewrite.writeTo !== rewrite.absolute) {
32668
32769
  unlinkSync3(rewrite.writeTo);
32669
32770
  if (movedUid) {
32670
- renameSync3(`${rewrite.writeTo}.uid`, `${rewrite.absolute}.uid`);
32771
+ renameSync4(`${rewrite.writeTo}.uid`, `${rewrite.absolute}.uid`);
32671
32772
  }
32672
32773
  }
32673
32774
  } catch {}
@@ -32677,7 +32778,7 @@ function applyRewrites(rewrites) {
32677
32778
  }
32678
32779
 
32679
32780
  // src/rename-symbol.ts
32680
- import { existsSync as existsSync16, mkdirSync as mkdirSync10, readdirSync as readdirSync7, realpathSync as realpathSync5, rmSync as rmSync7, writeFileSync as writeFileSync11 } from "node:fs";
32781
+ import { existsSync as existsSync16, mkdirSync as mkdirSync10, readdirSync as readdirSync7, realpathSync as realpathSync5, rmSync as rmSync8, writeFileSync as writeFileSync11 } from "node:fs";
32681
32782
  import { basename as basename4, dirname as dirname13, join as join19, relative as relative3, resolve as resolve5 } from "node:path";
32682
32783
 
32683
32784
  // src/resources.ts
@@ -33424,7 +33525,7 @@ function writeLeftAloneDetail(projectPath, symbol, detail) {
33424
33525
  `, "utf8");
33425
33526
  const kept = readdirSync7(directory).filter((name) => name.endsWith(".json")).sort().reverse();
33426
33527
  for (const old of kept.slice(10)) {
33427
- rmSync7(join19(directory, old), { force: true });
33528
+ rmSync8(join19(directory, old), { force: true });
33428
33529
  }
33429
33530
  return path;
33430
33531
  }
@@ -33447,7 +33548,7 @@ function summaryOf(edits, mentions, rewrites, left) {
33447
33548
  }
33448
33549
 
33449
33550
  // src/restart-note.ts
33450
- import { mkdirSync as mkdirSync11, readFileSync as readFileSync18, rmSync as rmSync8, writeFileSync as writeFileSync12 } from "node:fs";
33551
+ import { mkdirSync as mkdirSync11, readFileSync as readFileSync18, rmSync as rmSync9, writeFileSync as writeFileSync12 } from "node:fs";
33451
33552
  import { dirname as dirname14, join as join20 } from "node:path";
33452
33553
  function restartNotePath(projectPath) {
33453
33554
  return join20(projectPath, ".godot", "gdharness-restart.json");
@@ -33479,51 +33580,16 @@ function restartOwed(projectPath) {
33479
33580
  }
33480
33581
  function restartSettled(projectPath) {
33481
33582
  try {
33482
- rmSync8(restartNotePath(projectPath), { force: true });
33583
+ rmSync9(restartNotePath(projectPath), { force: true });
33483
33584
  } catch (error) {
33484
33585
  console.error(`[SERVER] Could not clear the restart note: ${errorMessage(error)}`);
33485
33586
  }
33486
33587
  }
33487
33588
 
33488
33589
  // src/run-record.ts
33489
- import { execFile as execFile4 } from "node:child_process";
33490
33590
  import { createHash as createHash3 } from "node:crypto";
33491
- import { mkdirSync as mkdirSync12, openSync as openSync2, readdirSync as readdirSync8, readFileSync as readFileSync19, realpathSync as realpathSync6, rmSync as rmSync10, statSync as statSync7 } from "node:fs";
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";
33492
33592
  import { basename as basename5, dirname as dirname15, join as join21, resolve as resolve6 } from "node:path";
33493
- import { promisify as promisify4 } from "node:util";
33494
-
33495
- // src/whole-file.ts
33496
- import { renameSync as renameSync4, rmSync as rmSync9, writeFileSync as writeFileSync13 } from "node:fs";
33497
- var HELD_FILE_MS = 1000;
33498
- var HELD_FILE_RETRY_MS = 10;
33499
- var HELD_BY_A_READER = new Set(["EPERM", "EACCES", "EBUSY"]);
33500
- function writeWhole(path, text) {
33501
- const partial = `${path}.${process.pid}.partial`;
33502
- try {
33503
- writeFileSync13(partial, text, "utf8");
33504
- } catch {
33505
- return false;
33506
- }
33507
- const until = Date.now() + HELD_FILE_MS;
33508
- for (;; ) {
33509
- try {
33510
- renameSync4(partial, path);
33511
- return true;
33512
- } catch (error) {
33513
- const code = error.code ?? "";
33514
- if (!HELD_BY_A_READER.has(code) || Date.now() >= until) {
33515
- try {
33516
- rmSync9(partial, { force: true });
33517
- } catch {}
33518
- return false;
33519
- }
33520
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, HELD_FILE_RETRY_MS);
33521
- }
33522
- }
33523
- }
33524
-
33525
- // src/run-record.ts
33526
- var execFileAsync = promisify4(execFile4);
33527
33593
  function runsDirectory() {
33528
33594
  return join21(runtimeDirectory(), "runs");
33529
33595
  }
@@ -33719,7 +33785,7 @@ async function runningAs(pid) {
33719
33785
  const raw = readFileSync19(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
33720
33786
  return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
33721
33787
  }
33722
- 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();
33723
33789
  return args === "" ? null : { kind: "commandLine", text: args, ...when };
33724
33790
  } catch {
33725
33791
  return null;
@@ -33728,7 +33794,7 @@ async function runningAs(pid) {
33728
33794
  async function listeningPid(port) {
33729
33795
  try {
33730
33796
  if (process.platform === "win32") {
33731
- const { stdout } = await execFileAsync("netstat", ["-ano"], { encoding: "utf8", timeout: 15000 });
33797
+ const { stdout } = await runHidden("netstat", ["-ano"], { timeout: 15000 });
33732
33798
  return listeningPidInNetstat(stdout, port);
33733
33799
  }
33734
33800
  for (const [command, args, pattern] of [
@@ -33736,7 +33802,7 @@ async function listeningPid(port) {
33736
33802
  ["ss", ["-ltnpH", `sport = :${port}`], /pid=(\d+)/]
33737
33803
  ]) {
33738
33804
  try {
33739
- const said = (await execFileAsync(command, [...args], { encoding: "utf8", timeout: 15000 })).stdout.trim();
33805
+ const said = (await runHidden(command, [...args], { timeout: 15000 })).stdout.trim();
33740
33806
  const found = pattern.exec(said);
33741
33807
  if (found?.[1] !== undefined) {
33742
33808
  return Number(found[1]);
@@ -33781,7 +33847,7 @@ async function startedAt(pid, windowsAnswer) {
33781
33847
  return Number.isNaN(when) ? null : when;
33782
33848
  }
33783
33849
  try {
33784
- 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();
33785
33851
  const when = Date.parse(said);
33786
33852
  return said === "" || Number.isNaN(when) ? null : when;
33787
33853
  } catch {
@@ -33790,10 +33856,8 @@ async function startedAt(pid, windowsAnswer) {
33790
33856
  }
33791
33857
  async function windowsImage(pid) {
33792
33858
  try {
33793
- const { stdout: csv } = await execFileAsync("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
33794
- encoding: "utf8",
33795
- timeout: 15000,
33796
- windowsHide: true
33859
+ const { stdout: csv } = await runHidden("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
33860
+ timeout: 15000
33797
33861
  });
33798
33862
  const image = /^"([^"]+)"/.exec(csv.trim())?.[1];
33799
33863
  return image === undefined ? { why: "named no process under that number" } : { image };
@@ -33874,7 +33938,7 @@ function clearRunRecord(projectPath) {
33874
33938
  }
33875
33939
  function ageMs(path, now) {
33876
33940
  try {
33877
- return now - statSync7(path).mtimeMs;
33941
+ return now - statSync8(path).mtimeMs;
33878
33942
  } catch {
33879
33943
  return 0;
33880
33944
  }
@@ -35138,6 +35202,11 @@ var TOOL_SPECS = [
35138
35202
  ops: ["start"],
35139
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."
35140
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
+ },
35141
35210
  savesIn: {
35142
35211
  type: "string",
35143
35212
  ops: ["start", "check"],
@@ -35293,7 +35362,7 @@ var TOOL_SPECS = [
35293
35362
  name: "editor_rescan",
35294
35363
  title: "Rescan the project",
35295
35364
  effect: "additive",
35296
- 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.",
35297
35366
  parameters: {
35298
35367
  projectPath: PROJECT_PATH2,
35299
35368
  timeoutMs: {
@@ -35371,13 +35440,13 @@ var TOOL_SPECS = [
35371
35440
  pid: RUNNING_PID,
35372
35441
  nodePath: {
35373
35442
  type: "string",
35374
- ops: ["tree", "text", "find", "rect", "property"],
35375
- 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.'
35376
35445
  },
35377
35446
  property: {
35378
35447
  type: "string",
35379
35448
  ops: ["property", "find"],
35380
- 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.'
35381
35450
  },
35382
35451
  depth: {
35383
35452
  type: "integer",
@@ -35393,8 +35462,8 @@ var TOOL_SPECS = [
35393
35462
  properties: {
35394
35463
  type: "array",
35395
35464
  items: { type: "string" },
35396
- ops: ["tree"],
35397
- 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.'
35398
35467
  },
35399
35468
  className: {
35400
35469
  type: "string",
@@ -35410,15 +35479,15 @@ var TOOL_SPECS = [
35410
35479
  group: { type: "string", ops: ["find"], description: "find: a group the node is in." },
35411
35480
  says: {
35412
35481
  type: "string",
35413
- ops: ["find"],
35414
- 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.'
35415
35484
  },
35416
35485
  limit: {
35417
35486
  type: "integer",
35418
35487
  minimum: 1,
35419
35488
  maximum: 5000,
35420
- ops: ["find", "text"],
35421
- 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."
35422
35491
  },
35423
35492
  includeHidden: {
35424
35493
  type: "boolean",
@@ -35467,7 +35536,7 @@ var TOOL_SPECS = [
35467
35536
  },
35468
35537
  accessibility: {
35469
35538
  effect: "read",
35470
- summary: "what the game tells a screen reader, read through UI Automation as a Windows screen reader reads it, under windows: one tree per window, each element with its role (text for static text, button, check_box, group for a control holding others), name, value, description, placeholder, bounds in the window's pixels, the actions a reader is offered (click, focus, toggle, set_value, select, expand_collapse, scroll_into_view), and focused on the one with the focus. node is the path of the node that made the element; an element a node made as a part of itself has part_of instead, and part: announcement marks the text a window speaks announcements through. An empty name is read as nothing: static text takes it from accessibility_update_set_value, and _note counts the text elements on screen with none. tooltip is the node's tooltip_text, which a Windows reader is not told. With no control focused, the engine reports the first node under the root, GdharnessRuntime, as focused. Needs a windowed run on Windows started with engineArgs [\"--accessibility\", \"always\"] or accessibility_support at Always Active, since the engine builds the tree only when asked; reading takes a second or two",
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",
35471
35540
  requires: []
35472
35541
  }
35473
35542
  },
@@ -35476,24 +35545,24 @@ var TOOL_SPECS = [
35476
35545
  {
35477
35546
  name: "runtime_invoke",
35478
35547
  title: "Set or call in the running game",
35479
- 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.",
35480
35549
  parameters: {
35481
35550
  projectPath: RUNNING_PROJECT_PATH,
35482
35551
  pid: RUNNING_PID,
35483
35552
  nodePath: {
35484
35553
  type: "string",
35485
35554
  ops: ["set", "call"],
35486
- 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.'
35487
35556
  },
35488
35557
  property: {
35489
35558
  type: "string",
35490
35559
  ops: ["set"],
35491
- 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.'
35492
35561
  },
35493
35562
  value: {
35494
35563
  ops: ["set"],
35495
35564
  blank: true,
35496
- description: `set: the value, fitted to the property's type. A vector, colour or the like may be written as its fields, {"x": 140, "y": 300}, or tagged, {"_type": "Vector2", "x": 140, "y": 300}, which is the form a read answers with. "" writes an empty string. A property typed as or holding an object takes a path naming one the game holds, the way call's args do, and holds that instance, or null, which the text null also means, to empty it. One declared as Variant or untyped takes anything, as the engine does: a value of the kind it holds is fitted to it, an int staying an int, and any other is stored as sent. A typed list or map, such as Array[int] or Dictionary[String, int], takes a JSON list or object with each element fitted to its element type, and an element that cannot become one is refused by its index or key, as is an object of another script than the one the list holds, whether or not that script has a class_name.`
35565
+ description: `set: the value, fitted to the property's type. A number the property would have to cut is refused rather than truncated: a fraction into an int, a number other than 0 or 1 into a bool. A vector, colour or the like may be written as its fields, {"x": 140, "y": 300}, or tagged, {"_type": "Vector2", "x": 140, "y": 300}, which is the form a read answers with. "" writes an empty string. A property typed as or holding an object takes a path naming one the game holds, the way call's args do, and holds that instance, or null, which the text null also means, to empty it. One declared as Variant or untyped takes anything, as the engine does: a value of the kind it holds is fitted to it, an int staying an int, and any other is stored as sent, a fraction into one holding an int included. A typed list or map, such as Array[int] or Dictionary[String, int], takes a JSON list or object with each element fitted to its element type, and an element that cannot become one is refused by its index or key, as is an object of another script than the one the list holds, whether or not that script has a class_name.`
35497
35566
  },
35498
35567
  method: {
35499
35568
  type: "string",
@@ -35503,7 +35572,7 @@ var TOOL_SPECS = [
35503
35572
  args: {
35504
35573
  type: "array",
35505
35574
  ops: ["call"],
35506
- description: `call: the arguments, fitted to the method's parameter types. A vector, colour or the like may be written as its fields, {"x": 140, "y": 300}, or tagged, {"_type": "Vector2", "x": 140, "y": 300}. A parameter typed as an object takes a path naming one the game holds and is handed that instance: a colon path read from nodePath, "_game:run:wares:3", or one starting at a node, "/root/Main/Hud" or "/root/Main:_game:run". A path that reaches no object, or an object of another class than the parameter declares, is refused, and so are too few or too many arguments. The engine names a parameter typed as a script class without class_name by the class the script extends, so an object of that class with another script passes that check and the engine refuses the call: the answer says the method did not run, with the engine's reason. A parameter typed as a list or map, such as Array[int] or Array[Gear], is built from a JSON list or object element by element, objects named by their paths. The method itself takes its arguments here only: brackets on it holding arguments are refused.`
35575
+ description: `call: the arguments, fitted to the method's parameter types. A number a parameter would have to cut is refused rather than truncated: a fraction for an int, a number other than 0 or 1 for a bool. A vector, colour or the like may be written as its fields, {"x": 140, "y": 300}, or tagged, {"_type": "Vector2", "x": 140, "y": 300}. A parameter typed as an object takes a path naming one the game holds and is handed that instance: a colon path read from nodePath, "_game:run:wares:3", or one starting at a node, "/root/Main/Hud" or "/root/Main:_game:run". A path that reaches no object, or an object of another class than the parameter declares, is refused, and so are too few or too many arguments. The engine names a parameter typed as a script class without class_name by the class the script extends, so an object of that class with another script passes that check and the engine refuses the call: the answer says the method did not run, with the engine's reason. A parameter typed as a list or map, such as Array[int] or Array[Gear], is built from a JSON list or object element by element, objects named by their paths. The method itself takes its arguments here only: brackets on it holding arguments are refused.`
35507
35576
  },
35508
35577
  properties: {
35509
35578
  type: ["boolean", "array"],
@@ -35621,6 +35690,13 @@ var TOOL_SPECS = [
35621
35690
  ops: ["action", "key", "mouse_click", "joypad_button"],
35622
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."
35623
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
+ },
35624
35700
  strength: {
35625
35701
  type: "number",
35626
35702
  minimum: 0,
@@ -35769,7 +35845,7 @@ var TOOL_SPECS = [
35769
35845
  property: {
35770
35846
  type: "string",
35771
35847
  ops: ["until"],
35772
- 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.'
35773
35849
  },
35774
35850
  value: {
35775
35851
  ops: ["until"],
@@ -35954,7 +36030,6 @@ function serverInstructions() {
35954
36030
  }
35955
36031
 
35956
36032
  // src/setup.ts
35957
- import { spawnSync } from "node:child_process";
35958
36033
  import { createHash as createHash4 } from "node:crypto";
35959
36034
  import {
35960
36035
  cpSync,
@@ -35964,8 +36039,8 @@ import {
35964
36039
  readFileSync as readFileSync21,
35965
36040
  rmdirSync,
35966
36041
  rmSync as rmSync11,
35967
- statSync as statSync8,
35968
- writeFileSync as writeFileSync14
36042
+ statSync as statSync9,
36043
+ writeFileSync as writeFileSync13
35969
36044
  } from "node:fs";
35970
36045
  import { dirname as dirname16, join as join23 } from "node:path";
35971
36046
  import { fileURLToPath as fileURLToPath3 } from "node:url";
@@ -35992,7 +36067,7 @@ function addonsDigest(names, from) {
35992
36067
  }
35993
36068
  const path = join23(directory, name);
35994
36069
  const inside = `${relative}/${name}`;
35995
- if (statSync8(path).isDirectory()) {
36070
+ if (statSync9(path).isDirectory()) {
35996
36071
  walk(path, inside);
35997
36072
  } else {
35998
36073
  hash.update(`${inside}
@@ -36086,7 +36161,7 @@ function trackedByGit(projectPath, wanted) {
36086
36161
  if (wanted.length === 0) {
36087
36162
  return new Set;
36088
36163
  }
36089
- const asked = spawnSync("git", ["ls-files", "-z", "--", ...wanted], {
36164
+ const asked = spawnSyncHidden("git", ["ls-files", "-z", "--", ...wanted], {
36090
36165
  cwd: projectPath,
36091
36166
  encoding: "utf8"
36092
36167
  });
@@ -36183,7 +36258,6 @@ function argumentShape(schema) {
36183
36258
  // src/server.ts
36184
36259
  var UPDATE_NOTICE_EVERY = 500;
36185
36260
  var FEEDBACK_NOTICE_EVERY = 250;
36186
- var run4 = promisify5(execFile5);
36187
36261
  var __dirname2 = dirname17(fileURLToPath4(import.meta.url));
36188
36262
  var PARENT_ASKED_AFTER_MS = 5000;
36189
36263
  function editorRestartTimeoutMs() {
@@ -36281,6 +36355,9 @@ function captureDetail(payload) {
36281
36355
  function patienceForFrames(frames, atLeast) {
36282
36356
  return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
36283
36357
  }
36358
+ function withDefined(fields) {
36359
+ return Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined));
36360
+ }
36284
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.";
36285
36362
  function longerWaitNote(tool, op, waitedMs) {
36286
36363
  const spec = TOOL_SPECS.find((one) => one.name === tool);
@@ -36749,6 +36826,31 @@ function notRunNote(notRun, failFast) {
36749
36826
  const cases = `${notRun} case${notRun === 1 ? "" : "s"}`;
36750
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.`;
36751
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
+ }
36752
36854
  function exportAnswer(ending, asked, before, after) {
36753
36855
  const { log } = ending;
36754
36856
  const written = after !== null && after !== before;
@@ -37375,7 +37477,7 @@ function scriptsToDiagnose(projectPath, given) {
37375
37477
  var EXTERNAL_MEMBER = 'Could not resolve external class member "';
37376
37478
  function isDirectory(path) {
37377
37479
  try {
37378
- return statSync9(path).isDirectory();
37480
+ return statSync10(path).isDirectory();
37379
37481
  } catch {
37380
37482
  return false;
37381
37483
  }
@@ -38245,7 +38347,15 @@ class GodotServer {
38245
38347
  property: readNonEmptyString(args, "property") ?? ""
38246
38348
  }, { tool: "runtime_inspect", op, patience });
38247
38349
  case "accessibility":
38248
- 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
+ }, {
38249
38359
  tool: "runtime_inspect",
38250
38360
  op,
38251
38361
  patience: readPositiveNumber(args, "timeoutMs") ?? Math.max(30000, this.runtimeTimeoutMs())
@@ -38296,7 +38406,14 @@ class GodotServer {
38296
38406
  ...args["index"] === undefined ? {} : { index: args["index"] }
38297
38407
  }, { tool: "runtime_input", op });
38298
38408
  }
38299
- 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
+ }
38300
38417
  case "runtime_wait":
38301
38418
  return await this.handleRuntimeWait(op, args);
38302
38419
  case "debug_breakpoint": {
@@ -39079,6 +39196,10 @@ class GodotServer {
39079
39196
  if (!output.ok) {
39080
39197
  return this.createErrorResponse(output.reason, PATH_SOLUTIONS);
39081
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
+ }
39082
39203
  const engine = await this.engine();
39083
39204
  if (!engine.ok) {
39084
39205
  return engine.response;
@@ -39102,7 +39223,7 @@ class GodotServer {
39102
39223
  output.absolutePath
39103
39224
  ];
39104
39225
  this.logDebug(`Exporting: ${engine.value} ${exportArgs.join(" ")}`);
39105
- const writtenAt = () => existsSync18(output.absolutePath) ? statSync9(output.absolutePath).mtimeMs : null;
39226
+ const writtenAt = () => existsSync18(output.absolutePath) ? statSync10(output.absolutePath).mtimeMs : null;
39106
39227
  const before = writtenAt();
39107
39228
  const freed = freeLibraryCopies(project.value.path);
39108
39229
  if (freed.setAside.length > 0 || freed.swept.length > 0) {
@@ -39447,7 +39568,7 @@ class GodotServer {
39447
39568
  }
39448
39569
  async godotVersion(godotPath) {
39449
39570
  try {
39450
- const { stdout } = await run4(godotPath, ["--version"], { timeout: 1e4 });
39571
+ const { stdout } = await runHidden(godotPath, ["--version"], { timeout: 1e4 });
39451
39572
  this.lastGodotVersion = stdout.trim();
39452
39573
  return this.lastGodotVersion;
39453
39574
  } catch (error) {
@@ -39508,6 +39629,10 @@ class GodotServer {
39508
39629
  return markIfStale(answer, this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest(), this.editorCodeOnDisk());
39509
39630
  }
39510
39631
  async diagnosticsOf(args) {
39632
+ const waited = await this.waitForTheEditorOpenedHere(args, []);
39633
+ if (waited !== null) {
39634
+ return { refusal: waited, perScript: false };
39635
+ }
39511
39636
  const answer = await this.handleLSP("lsp_get_diagnostics", args);
39512
39637
  const payload = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
39513
39638
  const refused = this.languageServerRefusal(payload, "Diagnostics");
@@ -39592,9 +39717,34 @@ class GodotServer {
39592
39717
  const recorded = readClassNote(projectPath)?.shortListEditorPid;
39593
39718
  return recorded !== undefined && recorded === (this.godotBridge.getStatus().editorPid ?? null);
39594
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
+ }
39595
39744
  noteAShortListEditor(projectPath) {
39596
39745
  const note = readClassNote(projectPath);
39597
39746
  writeClassNote(projectPath, {
39747
+ ...note,
39598
39748
  rebuiltWith: note?.rebuiltWith ?? [],
39599
39749
  shortListEditorPid: this.godotBridge.getStatus().editorPid ?? null
39600
39750
  });
@@ -39619,7 +39769,17 @@ class GodotServer {
39619
39769
  async handleLSP(toolName, args) {
39620
39770
  return handleLSPTool(await this.lsp(), toolName, args);
39621
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
+ }
39622
39778
  async askScriptInfo(toolName, asked, args) {
39779
+ const waited = await this.waitForTheEditorOpenedHere(args, []);
39780
+ if (waited !== null) {
39781
+ return waited;
39782
+ }
39623
39783
  const answer = await this.handleLSP(toolName, args);
39624
39784
  const refused = this.languageServerRefusal(asParams(JSON.parse(answer.content[0]?.text ?? "{}")), asked);
39625
39785
  return refused === null ? answer : refused.refusal;
@@ -40345,6 +40505,17 @@ class GodotServer {
40345
40505
  if (sceneToRun && !sceneToRun.ok) {
40346
40506
  return this.createErrorResponse(sceneToRun.reason, PATH_SOLUTIONS);
40347
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
+ }
40348
40519
  const given = this.gameArguments(args);
40349
40520
  if (!given.ok) {
40350
40521
  return given.response;
@@ -40361,13 +40532,13 @@ class GodotServer {
40361
40532
  if (!asked.ok) {
40362
40533
  return asked.response;
40363
40534
  }
40364
- if (!sceneToRun && !hasMainScene(project.value.file)) {
40535
+ if (exported === null && !sceneToRun && !hasMainScene(project.value.file)) {
40365
40536
  return this.createErrorResponse("The project sets no main scene, so there is nothing to run.", [
40366
40537
  "Pass scene to run one scene",
40367
40538
  "Choose the main scene with project_settings set_main_scene"
40368
40539
  ]);
40369
40540
  }
40370
- const engine = await this.engine();
40541
+ const engine = exported?.ok === true ? { ok: true, value: exported.absolutePath } : await this.engine();
40371
40542
  if (!engine.ok) {
40372
40543
  return engine.response;
40373
40544
  }
@@ -40405,11 +40576,11 @@ class GodotServer {
40405
40576
  const editorWouldPlay = !headless || editorPlaysHeadless(project.value.file);
40406
40577
  const silent = resolveSilent(args["silent"], process.env) && !headless;
40407
40578
  const profiled = readBoolean(args, "profile") === true;
40408
- 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) {
40409
40580
  return await this.playThroughEditor(sceneArgument, refreshed.value, scanned, project.value.path, alreadyPlaying, runtimeWaitMs, ended, profiled);
40410
40581
  }
40411
40582
  const cmdArgs = runArguments({
40412
- projectPath: project.value.path,
40583
+ projectPath: exported === null ? project.value.path : null,
40413
40584
  headless,
40414
40585
  scene: sceneArgument,
40415
40586
  userArgs: given.value,
@@ -40417,7 +40588,7 @@ class GodotServer {
40417
40588
  silent
40418
40589
  });
40419
40590
  this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
40420
- 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);
40421
40592
  if ("error" in started) {
40422
40593
  return this.createErrorResponse(`The game could not be started: ${started.error}`, [
40423
40594
  "editor_status says which Godot this server runs and whether it answers"
@@ -40426,7 +40597,7 @@ class GodotServer {
40426
40597
  started.announcedBefore = alreadyPlaying;
40427
40598
  started.bootScene = runtimeWaitMs.scene;
40428
40599
  this.activeProcess = started;
40429
- 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}` : "";
40430
40601
  const endedForThis = endedToStartThis(ended);
40431
40602
  const stranded = wereHereFirst.filter((one) => one.pid !== ended?.pid && alive(one.pid));
40432
40603
  const handed = this.dapHoldingBreakpointsOf(project.value.path).breakpointsHeld();
@@ -40436,6 +40607,7 @@ class GodotServer {
40436
40607
  through: "gdharness",
40437
40608
  alsoRunning: stranded.length === 0 ? undefined : stranded.map((one) => one.pid),
40438
40609
  pid: started.pid,
40610
+ executable: exported?.ok === true ? exported.relativePath : undefined,
40439
40611
  arguments: cmdArgs,
40440
40612
  argsNote: engineFlagsNote(given.value),
40441
40613
  transcript: started.transcript,
@@ -40447,7 +40619,7 @@ class GodotServer {
40447
40619
  endedPreviousRun: endedPreviousRun(ended),
40448
40620
  previousRunLeft: previousRunLeft(ended),
40449
40621
  ...previousRunHow(ended),
40450
- 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),
40451
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}`
40452
40624
  });
40453
40625
  }
@@ -40538,7 +40710,7 @@ class GodotServer {
40538
40710
  }
40539
40711
  let announcedAt;
40540
40712
  try {
40541
- announcedAt = statSync9(endpoint.file).mtimeMs;
40713
+ announcedAt = statSync10(endpoint.file).mtimeMs;
40542
40714
  } catch {
40543
40715
  return;
40544
40716
  }
@@ -40887,7 +41059,10 @@ class GodotServer {
40887
41059
  return gone ? "gone" : "lingering";
40888
41060
  }
40889
41061
  spawnGame(godotPath, cmdArgs, env) {
40890
- 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
+ });
40891
41066
  const cancelled = callSignal();
40892
41067
  if (cancelled !== undefined) {
40893
41068
  const end = () => {
@@ -40960,7 +41135,7 @@ class GodotServer {
40960
41135
  });
40961
41136
  return Object.assign(started, { settled });
40962
41137
  }
40963
- async launchKeptGame(godotPath, cmdArgs, projectPath, env, desktop, profiled = false) {
41138
+ async launchKeptGame(godotPath, cmdArgs, projectPath, env, desktop, profiled = false, debugged = true) {
40964
41139
  const startedAt = Date.now();
40965
41140
  const transcript = openTranscript(startedAt);
40966
41141
  closeSync2(transcript.fd);
@@ -40976,7 +41151,7 @@ class GodotServer {
40976
41151
  projectPath,
40977
41152
  ...this.servedBy(),
40978
41153
  ...profile === undefined ? {} : { profile },
40979
- breakpoints: this.breakpointsForAGame(projectPath)
41154
+ ...debugged ? { breakpoints: this.breakpointsForAGame(projectPath) } : { debugged: false }
40980
41155
  },
40981
41156
  ...desktop === undefined ? {} : { desktop }
40982
41157
  });
@@ -41940,8 +42115,8 @@ class GodotServer {
41940
42115
  try {
41941
42116
  const status = asParams(await this.godotBridge.invokeTool("scan_status", {}));
41942
42117
  return status["scanning"] === true || status["importing"] === true;
41943
- } catch {
41944
- return false;
42118
+ } catch (error) {
42119
+ return error instanceof BridgeTimeout;
41945
42120
  }
41946
42121
  }
41947
42122
  async handleRescanFilesystem(args) {
@@ -41952,7 +42127,7 @@ class GodotServer {
41952
42127
  let writtenBefore = null;
41953
42128
  let blind = { unseen: [] };
41954
42129
  let waitedForEditorMs = 0;
41955
- let first;
42130
+ let startUnanswered = false;
41956
42131
  let completedBefore;
41957
42132
  let uidsReread = [];
41958
42133
  let uidsDuplicatedOnDisk = [];
@@ -41969,7 +42144,11 @@ class GodotServer {
41969
42144
  before = projectPath === "" ? null : cachedClasses(projectPath);
41970
42145
  writtenBefore = projectPath === "" ? null : cacheWrittenAt(projectPath);
41971
42146
  blind = await this.classesTheEditorCannotSee(args);
41972
- 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
+ }
41973
42152
  if (first.isError) {
41974
42153
  return first;
41975
42154
  }
@@ -41992,7 +42171,17 @@ class GodotServer {
41992
42171
  let idleSince = null;
41993
42172
  while (Date.now() - started < timeoutMs) {
41994
42173
  await new Promise((settle) => setTimeout(settle, 100));
41995
- 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
+ }
41996
42185
  const pending = Boolean(status["pending"]) && Date.now() - started < SCAN_START_MS;
41997
42186
  const completed = readNumber(status, "scansCompleted");
41998
42187
  const notYetOver = completedBefore !== undefined && completed !== undefined && completed <= completedBefore;
@@ -42075,12 +42264,20 @@ class GodotServer {
42075
42264
  }
42076
42265
  const checked = busy ? { unseen: [] } : await this.classesTheEditorCannotSee(args);
42077
42266
  const unseen = checked.unseen;
42078
- 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
+ }
42079
42273
  const dependents = busy || projectPath === "" || broughtIn.length === 0 ? null : await this.reloadDependents(projectPath, broughtIn);
42080
42274
  const stillGone = lost.filter((name) => !restored.includes(name));
42081
42275
  const notes = [];
42082
42276
  if (busy) {
42083
- 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
+ }
42084
42281
  } else if (stillGone.length > 0) {
42085
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.");
42086
42283
  } else if (lost.length > 0) {
@@ -42198,8 +42395,9 @@ class GodotServer {
42198
42395
  const droppedByTheEditor = lost.filter((name) => !cameBack.includes(name));
42199
42396
  const seenLosing = droppedByTheEditor.length > 0 && this.godotBridge.isConnected();
42200
42397
  writeClassNote(projectPath, {
42398
+ ...noted,
42201
42399
  rebuiltWith: [...after.keys()].sort(),
42202
- ...seenLosing ? { shortListEditorPid: this.godotBridge.getStatus().editorPid ?? null } : noted?.shortListEditorPid === undefined ? {} : { shortListEditorPid: noted.shortListEditorPid }
42400
+ ...seenLosing ? { shortListEditorPid: this.godotBridge.getStatus().editorPid ?? null } : {}
42203
42401
  });
42204
42402
  const notes = [];
42205
42403
  if (seenLosing) {
@@ -42273,6 +42471,10 @@ class GodotServer {
42273
42471
  if (moveTo !== null && !moveTo.ok) {
42274
42472
  return this.createErrorResponse(moveTo.reason);
42275
42473
  }
42474
+ const waited = await this.waitForTheEditorOpenedHere(args, []);
42475
+ if (waited !== null) {
42476
+ return waited;
42477
+ }
42276
42478
  const languageServer = {
42277
42479
  problem: null,
42278
42480
  initialized: false
@@ -42515,6 +42717,16 @@ class GodotServer {
42515
42717
  ]);
42516
42718
  }
42517
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) {
42518
42730
  if (!this.godotBridge.isConnected()) {
42519
42731
  const alternative = askedAs !== undefined && Object.hasOwn(EDITOR_FREE_INSTEAD, askedAs) ? EDITOR_FREE_INSTEAD[askedAs] : undefined;
42520
42732
  const refused = await this.noEditorYet(args, alternative === undefined ? [] : [alternative]);
@@ -42533,6 +42745,9 @@ class GodotServer {
42533
42745
  try {
42534
42746
  return this.jsonTextResponse(markIfStale(await this.godotBridge.invokeTool(toolName, args), this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest(), this.editorCodeOnDisk()));
42535
42747
  } catch (error) {
42748
+ if (error instanceof BridgeTimeout) {
42749
+ return error;
42750
+ }
42536
42751
  const message = errorMessage(error);
42537
42752
  if (message.includes(`Unknown tool: ${toolName}`)) {
42538
42753
  const status = this.godotBridge.getStatus();
@@ -42602,7 +42817,7 @@ class GodotServer {
42602
42817
  return !run.announcedBefore.has(one.pid);
42603
42818
  }
42604
42819
  try {
42605
- return statSync9(one.file).mtimeMs >= run.startedAt;
42820
+ return statSync10(one.file).mtimeMs >= run.startedAt;
42606
42821
  } catch {
42607
42822
  return false;
42608
42823
  }