gdharness 1.1.13 → 1.1.15

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
@@ -10084,9 +10084,10 @@ function markIfStale(answer, addonVersion, serverVersion, addonDigest, shippedDi
10084
10084
  // src/issues.ts
10085
10085
  var NEW_ISSUE = "https://github.com/Aureliolo/gdharness/issues/new";
10086
10086
  var ENHANCEMENT_URL = `${NEW_ISSUE}?template=feature_request.md`;
10087
- function feedbackNotice() {
10087
+ function feedbackNotice(every) {
10088
10088
  return JSON.stringify({
10089
10089
  gdharness_feedback: {
10090
+ why_here: `Asked once every ${every} answers, whatever the call was. It is not about the answer it comes with, which is complete as it stands.`,
10090
10091
  what_to_do: "If this session has run into something gdharness cannot do, or a tool that should " + "exist and does not, tell the user what is missing and offer to open an enhancement " + "issue for them. Only if they say yes, and only for a real gap: a call you got wrong " + "is not one. If you cannot open an issue yourself, give them the link and what to " + "write in it.",
10091
10092
  enhancement_url: ENHANCEMENT_URL
10092
10093
  }
@@ -10200,29 +10201,27 @@ function defectReport(where, error, context = {}) {
10200
10201
  }
10201
10202
 
10202
10203
  // src/server.ts
10203
- import { execFile as execFile5, spawn as spawn2 } from "node:child_process";
10204
+ import { execFile as execFile6, spawn as spawn3 } from "node:child_process";
10204
10205
  import { randomUUID as randomUUID2 } from "node:crypto";
10205
10206
  import {
10206
10207
  appendFileSync,
10207
10208
  closeSync as closeSync2,
10208
10209
  constants,
10209
10210
  copyFileSync,
10210
- existsSync as existsSync11,
10211
+ existsSync as existsSync12,
10211
10212
  fstatSync,
10212
10213
  mkdirSync as mkdirSync10,
10213
- mkdtempSync as mkdtempSync2,
10214
10214
  openSync as openSync3,
10215
- readdirSync as readdirSync7,
10215
+ readdirSync as readdirSync8,
10216
10216
  readFileSync as readFileSync17,
10217
10217
  readSync as readSync2,
10218
10218
  rmdirSync as rmdirSync2,
10219
10219
  statSync as statSync8
10220
10220
  } from "node:fs";
10221
- import { tmpdir as tmpdir4 } from "node:os";
10222
- import { basename as basename4, dirname as dirname11, extname as extname2, isAbsolute as isAbsolute3, join as join17, normalize as normalize2, relative as relative3 } from "node:path";
10221
+ import { basename as basename4, dirname as dirname11, extname as extname2, isAbsolute as isAbsolute3, join as join18, normalize as normalize2, relative as relative3 } from "node:path";
10223
10222
  import { setTimeout as delay3 } from "node:timers/promises";
10224
10223
  import { fileURLToPath as fileURLToPath4 } from "node:url";
10225
- import { promisify as promisify5 } from "node:util";
10224
+ import { promisify as promisify6 } from "node:util";
10226
10225
 
10227
10226
  // node_modules/zod/v3/helpers/util.js
10228
10227
  var util;
@@ -24787,6 +24786,20 @@ class StdioServerTransport {
24787
24786
  }
24788
24787
  }
24789
24788
 
24789
+ // src/alive.ts
24790
+ import process5 from "node:process";
24791
+ function alive(pid) {
24792
+ if (pid === undefined || pid === null) {
24793
+ return false;
24794
+ }
24795
+ try {
24796
+ process5.kill(pid, 0);
24797
+ return true;
24798
+ } catch (error) {
24799
+ return error.code === "EPERM";
24800
+ }
24801
+ }
24802
+
24790
24803
  // src/boot-note.ts
24791
24804
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
24792
24805
  import { dirname as dirname2, join as join3 } from "node:path";
@@ -24812,8 +24825,18 @@ function resolveHeadless(requested, host) {
24812
24825
  }
24813
24826
  return envValue("DISPLAY", host.variables) === undefined && envValue("WAYLAND_DISPLAY", host.variables) === undefined;
24814
24827
  }
24828
+ function resolveSilent(requested, variables) {
24829
+ if (typeof requested === "boolean") {
24830
+ return requested;
24831
+ }
24832
+ const said = envValue("GDHARNESS_SILENT", variables)?.toLowerCase();
24833
+ return said !== undefined && said !== "0" && said !== "false";
24834
+ }
24815
24835
  function runArguments(options) {
24816
24836
  const args = options.headless ? ["--headless", "--path", options.projectPath] : ["--path", options.projectPath];
24837
+ if (options.silent === true && !options.headless) {
24838
+ args.push("--audio-driver", "Dummy");
24839
+ }
24817
24840
  if (options.quitAfter !== undefined && options.quitAfter !== null) {
24818
24841
  args.push("--quit-after", String(options.quitAfter));
24819
24842
  }
@@ -24923,12 +24946,161 @@ function resolveWithinProject(projectPath, candidatePath) {
24923
24946
  }
24924
24947
 
24925
24948
  // src/process-children.ts
24926
- import { execFile, execFileSync, spawnSync } from "node:child_process";
24949
+ import { execFile } from "node:child_process";
24927
24950
  import { basename } from "node:path";
24928
24951
  import { promisify } from "node:util";
24952
+
24953
+ // src/windows-ask.ts
24954
+ import { spawn } from "node:child_process";
24955
+ import process6 from "node:process";
24956
+ var SCRIPT = [
24957
+ "[Console]::OutputEncoding = [Text.Encoding]::UTF8",
24958
+ "$in = [Console]::In",
24959
+ "while ($null -ne ($line = $in.ReadLine())) {",
24960
+ ' $parts = $line.Split(" ", 2)',
24961
+ " try {",
24962
+ " $asked = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($parts[1]))",
24963
+ " $said = (& ([ScriptBlock]::Create($asked)) | Out-String -Width 1000000)",
24964
+ ' $tag = "OK"',
24965
+ " } catch {",
24966
+ " $said = $_.Exception.Message",
24967
+ ' $tag = "ERR"',
24968
+ " }",
24969
+ ' [Console]::Out.Write($parts[0] + " " + $tag + " " + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($said)) + "`n")',
24970
+ " [Console]::Out.Flush()",
24971
+ "}"
24972
+ ].join(`
24973
+ `);
24974
+
24975
+ class WindowsAskFailure extends Error {
24976
+ constructor(how) {
24977
+ super(`PowerShell ${how}`);
24978
+ }
24979
+ }
24980
+ var helper = null;
24981
+ var buffered = "";
24982
+ var next = 1;
24983
+ var out = null;
24984
+ var queued = [];
24985
+ var held = false;
24986
+ function holding(child, wanted) {
24987
+ if (held === wanted) {
24988
+ return;
24989
+ }
24990
+ held = wanted;
24991
+ const stdout = child.stdout;
24992
+ if (wanted) {
24993
+ stdout.ref?.();
24994
+ } else {
24995
+ stdout.unref?.();
24996
+ }
24997
+ }
24998
+ function running() {
24999
+ if (helper?.exitCode === null && helper.signalCode === null) {
25000
+ return helper;
25001
+ }
25002
+ const child = spawn("powershell.exe", [
25003
+ "-NoProfile",
25004
+ "-NonInteractive",
25005
+ "-ExecutionPolicy",
25006
+ "Bypass",
25007
+ "-EncodedCommand",
25008
+ Buffer.from(SCRIPT, "utf16le").toString("base64")
25009
+ ], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
25010
+ buffered = "";
25011
+ child.stdout.setEncoding("utf8");
25012
+ child.stdout.on("data", (chunk) => {
25013
+ buffered += chunk;
25014
+ for (let end = buffered.indexOf(`
25015
+ `);end !== -1; end = buffered.indexOf(`
25016
+ `)) {
25017
+ answered(buffered.slice(0, end).trim());
25018
+ buffered = buffered.slice(end + 1);
25019
+ }
25020
+ });
25021
+ child.stderr.resume();
25022
+ child.stdin.on("error", () => {});
25023
+ const gone = () => {
25024
+ if (helper !== child) {
25025
+ return;
25026
+ }
25027
+ helper = null;
25028
+ if (out !== null) {
25029
+ clearTimeout(out.timer);
25030
+ out.question.reject(new WindowsAskFailure("exited before it answered"));
25031
+ out = null;
25032
+ }
25033
+ sendNext();
25034
+ };
25035
+ child.once("exit", gone);
25036
+ child.once("error", gone);
25037
+ child.unref();
25038
+ child.stdin.unref?.();
25039
+ child.stderr.unref?.();
25040
+ held = true;
25041
+ holding(child, false);
25042
+ helper = child;
25043
+ return child;
25044
+ }
25045
+ function answered(line) {
25046
+ const [id, tag, body] = line.split(" ");
25047
+ if (out === null || out.id !== Number(id)) {
25048
+ return;
25049
+ }
25050
+ const { question, timer } = out;
25051
+ clearTimeout(timer);
25052
+ out = null;
25053
+ const said = Buffer.from(body ?? "", "base64").toString("utf8");
25054
+ if (tag === "OK") {
25055
+ question.resolve(said);
25056
+ } else {
25057
+ question.reject(new WindowsAskFailure(`refused the question: ${said.trim() || "no reason given"}`));
25058
+ }
25059
+ sendNext();
25060
+ }
25061
+ function sendNext() {
25062
+ const question = queued.shift();
25063
+ if (question === undefined) {
25064
+ if (helper !== null) {
25065
+ holding(helper, false);
25066
+ }
25067
+ return;
25068
+ }
25069
+ const child = running();
25070
+ holding(child, true);
25071
+ const id = next;
25072
+ next += 1;
25073
+ const timer = setTimeout(() => {
25074
+ if (out?.id !== id) {
25075
+ return;
25076
+ }
25077
+ out = null;
25078
+ question.reject(new WindowsAskFailure(`did not answer within ${question.withinMs}ms`));
25079
+ if (helper === child) {
25080
+ helper = null;
25081
+ }
25082
+ child.kill();
25083
+ sendNext();
25084
+ }, question.withinMs);
25085
+ out = { id, question, timer };
25086
+ child.stdin.write(`${id} ${Buffer.from(question.script, "utf8").toString("base64")}
25087
+ `);
25088
+ }
25089
+ function askWindows(script, withinMs) {
25090
+ if (process6.platform !== "win32") {
25091
+ return Promise.reject(new WindowsAskFailure("is asked only on Windows"));
25092
+ }
25093
+ return new Promise((resolve, reject) => {
25094
+ queued.push({ script, withinMs, resolve, reject });
25095
+ if (out === null) {
25096
+ sendNext();
25097
+ }
25098
+ });
25099
+ }
25100
+
25101
+ // src/process-children.ts
24929
25102
  var run = promisify(execFile);
24930
- var POWERSHELL_UTF8 = "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; ";
24931
- function startTimesOf(pids) {
25103
+ async function startTimesOf(pids) {
24932
25104
  const began = new Map;
24933
25105
  if (pids.length === 0) {
24934
25106
  return began;
@@ -24936,12 +25108,7 @@ function startTimesOf(pids) {
24936
25108
  try {
24937
25109
  if (process.platform === "win32") {
24938
25110
  const filter = pids.map((pid) => `ProcessId=${pid}`).join(" OR ");
24939
- const said = execFileSync("powershell", [
24940
- "-NoProfile",
24941
- "-NonInteractive",
24942
- "-Command",
24943
- `Get-CimInstance Win32_Process -Filter "${filter}" | ForEach-Object { "$($_.ProcessId) $(([DateTimeOffset]$_.CreationDate).ToUnixTimeMilliseconds())" }`
24944
- ], { encoding: "utf8", timeout: ASK_TIMEOUT_MS, windowsHide: true });
25111
+ const said = await askWindows(`Get-CimInstance Win32_Process -Filter "${filter}" | ForEach-Object { "$($_.ProcessId) $(([DateTimeOffset]$_.CreationDate).ToUnixTimeMilliseconds())" }`, ASK_TIMEOUT_MS);
24945
25112
  for (const line of said.split(`
24946
25113
  `)) {
24947
25114
  const found = /^\s*(\d+)\s+(\d+)\s*$/.exec(line);
@@ -24951,11 +25118,10 @@ function startTimesOf(pids) {
24951
25118
  }
24952
25119
  return began;
24953
25120
  }
24954
- const said = spawnSync("ps", ["-p", pids.join(","), "-o", "pid=,lstart="], {
24955
- encoding: "utf8",
24956
- timeout: ASK_TIMEOUT_MS,
24957
- env: { ...process.env, LC_ALL: "C" }
24958
- }).stdout;
25121
+ const said = await printedBy("ps", ["-p", pids.join(","), "-o", "pid=,lstart="], {
25122
+ ...process.env,
25123
+ LC_ALL: "C"
25124
+ });
24959
25125
  for (const line of said.split(`
24960
25126
  `)) {
24961
25127
  const found = /^\s*(\d+)\s+(.+)$/.exec(line);
@@ -24967,18 +25133,41 @@ function startTimesOf(pids) {
24967
25133
  } catch {}
24968
25134
  return began;
24969
25135
  }
25136
+ function printedBy(command, args, env) {
25137
+ return new Promise((resolve) => {
25138
+ execFile(command, [...args], { encoding: "utf8", timeout: ASK_TIMEOUT_MS, env }, (_error, stdout) => {
25139
+ resolve(stdout);
25140
+ });
25141
+ });
25142
+ }
24970
25143
  var ASK_TIMEOUT_MS = 15000;
24971
25144
  async function processTree() {
24972
25145
  try {
24973
- const tree = process.platform === "win32" ? parseProcessTable(await askWindows(), true) : parseProcessTable(await askPosix());
25146
+ const tree = process.platform === "win32" ? parseProcessTable(await listWindows(), true) : parseProcessTable(await askPosix());
24974
25147
  lastListingFailure = undefined;
24975
25148
  return tree;
24976
25149
  } catch (error) {
24977
- lastListingFailure = error instanceof Error ? error.message : String(error);
25150
+ lastListingFailure = listingFailure(error);
24978
25151
  return;
24979
25152
  }
24980
25153
  }
24981
25154
  var lastListingFailure;
25155
+ function listingFailure(error) {
25156
+ if (error instanceof WindowsAskFailure) {
25157
+ return error.message;
25158
+ }
25159
+ if (!(error instanceof Error)) {
25160
+ return String(error);
25161
+ }
25162
+ const failed = error;
25163
+ const how = failed.killed === true ? `was killed after the ${ASK_TIMEOUT_MS}ms budget${failed.signal ? ` (${failed.signal})` : ""}` : failed.signal ? `was ended by ${failed.signal}` : failed.code !== undefined && failed.code !== null ? `exited with ${failed.code}` : "failed";
25164
+ const said = typeof failed.stderr === "string" ? failed.stderr.trim() : "";
25165
+ const asked = basename((failed.cmd ?? "").split(" ")[0] ?? "") || "the listing";
25166
+ return `${asked} ${how}${said === "" ? ", with nothing on stderr" : `; stderr: ${said.slice(0, 400)}`}`;
25167
+ }
25168
+ function whyTheProcessTreeFailed() {
25169
+ return lastListingFailure;
25170
+ }
24982
25171
  function parseProcessTable(printed, started = false) {
24983
25172
  const tree = new Map;
24984
25173
  for (const line of printed.split(/\r?\n/)) {
@@ -25108,14 +25297,8 @@ function commandWords(command) {
25108
25297
  }
25109
25298
  return words;
25110
25299
  }
25111
- async function askWindows() {
25112
- const { stdout } = await run("powershell.exe", [
25113
- "-NoProfile",
25114
- "-NonInteractive",
25115
- "-Command",
25116
- `${POWERSHELL_UTF8}Get-CimInstance Win32_Process -Property ProcessId,ParentProcessId,CreationDate,CommandLine | ForEach-Object { $began = 0; if ($_.CreationDate) { $began = ([DateTimeOffset]$_.CreationDate).ToUnixTimeMilliseconds() }; "$($_.ProcessId) $($_.ParentProcessId) $began $($_.CommandLine)" }`
25117
- ], { timeout: ASK_TIMEOUT_MS, windowsHide: true, maxBuffer: 16 * 1024 * 1024 });
25118
- return stdout;
25300
+ function listWindows() {
25301
+ 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);
25119
25302
  }
25120
25303
  async function askPosix() {
25121
25304
  const { stdout } = await run("ps", ["-e", "-o", "pid=,ppid=,args="], {
@@ -25234,14 +25417,6 @@ function reportAlreadyInTranscript(transcript, report) {
25234
25417
  return end <= report.length && report.subarray(boundary, end).equals(copied) ? end : report.length;
25235
25418
  }
25236
25419
  var ERROR_REPORT_KEEP_MS = 60 * 60 * 1000;
25237
- function processAlive(pid) {
25238
- try {
25239
- process.kill(pid, 0);
25240
- return true;
25241
- } catch (error) {
25242
- return error.code === "EPERM";
25243
- }
25244
- }
25245
25420
  function parseAnnouncement(file, pid) {
25246
25421
  let fields;
25247
25422
  try {
@@ -25328,6 +25503,7 @@ var STARTED_AFTER_ANNOUNCING_MS = 2000;
25328
25503
  var CONFIRMED_FOR_MS = 60000;
25329
25504
  var JUDGED_AFTER_MS = 60000;
25330
25505
  var verdicts = new Map;
25506
+ var judging = null;
25331
25507
  function strangersAmong(candidates, now, startTimes = startTimesOf) {
25332
25508
  const toJudge = candidates.filter((one) => {
25333
25509
  if (now - one.writtenAt < JUDGED_AFTER_MS) {
@@ -25336,15 +25512,21 @@ function strangersAmong(candidates, now, startTimes = startTimesOf) {
25336
25512
  const known = verdicts.get(one.file);
25337
25513
  return known === undefined || known.writtenAt !== one.writtenAt || !known.stranger && now - known.judgedAt > CONFIRMED_FOR_MS;
25338
25514
  });
25339
- if (toJudge.length > 0) {
25340
- const began = startTimes(toJudge.map((one) => one.pid));
25341
- for (const one of toJudge) {
25342
- const at = began.get(one.pid);
25343
- const stranger = at !== undefined && at - one.writtenAt > STARTED_AFTER_ANNOUNCING_MS;
25344
- verdicts.set(one.file, { writtenAt: one.writtenAt, judgedAt: now, stranger });
25345
- }
25515
+ if (toJudge.length > 0 && judging === null) {
25516
+ judging = startTimes(toJudge.map((one) => one.pid)).then((began) => {
25517
+ for (const one of toJudge) {
25518
+ const at = began.get(one.pid);
25519
+ const stranger = at !== undefined && at - one.writtenAt > STARTED_AFTER_ANNOUNCING_MS;
25520
+ verdicts.set(one.file, { writtenAt: one.writtenAt, judgedAt: now, stranger });
25521
+ }
25522
+ }).catch(() => {}).finally(() => {
25523
+ judging = null;
25524
+ });
25346
25525
  }
25347
- return new Set(candidates.filter((one) => verdicts.get(one.file)?.stranger === true).map((one) => one.file));
25526
+ return new Set(candidates.filter((one) => {
25527
+ const known = verdicts.get(one.file);
25528
+ return known?.stranger === true && known.writtenAt === one.writtenAt;
25529
+ }).map((one) => one.file));
25348
25530
  }
25349
25531
  function announcedIn(directory) {
25350
25532
  if (!existsSync2(directory)) {
@@ -25365,17 +25547,17 @@ function announcedIn(directory) {
25365
25547
  }
25366
25548
  const file = join2(directory, entry);
25367
25549
  const pid = Number.parseInt(match[1] ?? "", 10);
25368
- const alive = processAlive(pid);
25369
- entries.push({ file, pid, alive });
25370
- if (alive) {
25550
+ const running = alive(pid);
25551
+ entries.push({ file, pid, running });
25552
+ if (running) {
25371
25553
  try {
25372
25554
  candidates.push({ file, pid, writtenAt: statSync(file).mtimeMs });
25373
25555
  } catch {}
25374
25556
  }
25375
25557
  }
25376
25558
  const strangers = strangersAmong(candidates, Date.now());
25377
- for (const { file, pid, alive } of entries) {
25378
- const gone = !alive || strangers.has(file);
25559
+ for (const { file, pid, running } of entries) {
25560
+ const gone = !running || strangers.has(file);
25379
25561
  const announced = gone ? { kind: "rubbish" } : parseAnnouncement(file, pid);
25380
25562
  if (announced.kind === "rubbish") {
25381
25563
  if (!gone) {
@@ -25407,7 +25589,7 @@ async function announcementEnded(pid, directories = runtimeDirectories()) {
25407
25589
  return;
25408
25590
  }
25409
25591
  const until = Date.now() + ENDED_WITHIN_MS;
25410
- while (processAlive(pid)) {
25592
+ while (alive(pid)) {
25411
25593
  const left = until - Date.now();
25412
25594
  if (left <= 0) {
25413
25595
  return;
@@ -25423,7 +25605,7 @@ async function announcementEnded(pid, directories = runtimeDirectories()) {
25423
25605
  }
25424
25606
  }
25425
25607
  function sweepErrorReport(path, pid, now = Date.now()) {
25426
- if (processAlive(pid)) {
25608
+ if (alive(pid)) {
25427
25609
  return;
25428
25610
  }
25429
25611
  try {
@@ -25853,6 +26035,15 @@ import { join as join6 } from "node:path";
25853
26035
  var DECLARATION = /^(?:@[a-z_]+(?:\([^)\n]*\))?\s+)*class_name\s+([A-Za-z_][A-Za-z0-9_]*)/m;
25854
26036
  function declaredClasses(projectPath) {
25855
26037
  const declared = new Map;
26038
+ eachScript(projectPath, (script, source) => {
26039
+ const found = DECLARATION.exec(source);
26040
+ if (found?.[1]) {
26041
+ declared.set(found[1], script);
26042
+ }
26043
+ });
26044
+ return declared;
26045
+ }
26046
+ function eachScript(projectPath, each) {
25856
26047
  const visit = (directory, prefix) => {
25857
26048
  if (existsSync3(join6(directory, ".gdignore"))) {
25858
26049
  return;
@@ -25865,15 +26056,53 @@ function declaredClasses(projectPath) {
25865
26056
  if (entry.isDirectory()) {
25866
26057
  visit(path, `${prefix}${entry.name}/`);
25867
26058
  } else if (entry.isFile() && entry.name.endsWith(".gd")) {
25868
- const found = DECLARATION.exec(readFileSync7(path, "utf8"));
25869
- if (found?.[1]) {
25870
- declared.set(found[1], `res://${prefix}${entry.name}`);
25871
- }
26059
+ each(`res://${prefix}${entry.name}`, readFileSync7(path, "utf8"));
25872
26060
  }
25873
26061
  }
25874
26062
  };
25875
26063
  visit(projectPath, "");
25876
- return declared;
26064
+ }
26065
+ function scriptsNaming(projectPath, classes) {
26066
+ const named = new Map(classes.map((name) => [name, ""]));
26067
+ const naming = [];
26068
+ eachScript(projectPath, (script, source) => {
26069
+ const declares = DECLARATION.exec(source)?.[1];
26070
+ if (declares !== undefined && named.has(declares)) {
26071
+ return;
26072
+ }
26073
+ const code = codeOf(source);
26074
+ if (classesNamedIn(code, named).length > 0) {
26075
+ naming.push({ script, tool: /^\s*@tool\b/m.test(code) });
26076
+ }
26077
+ });
26078
+ return naming.sort((a, b) => a.script.localeCompare(b.script));
26079
+ }
26080
+ function codeOf(source) {
26081
+ let code = "";
26082
+ let at = 0;
26083
+ while (at < source.length) {
26084
+ const here = source[at] ?? "";
26085
+ if (here === "#") {
26086
+ const end = source.indexOf(`
26087
+ `, at);
26088
+ at = end === -1 ? source.length : end;
26089
+ continue;
26090
+ }
26091
+ if (here === '"' || here === "'") {
26092
+ const fence = source.startsWith(here.repeat(3), at) ? here.repeat(3) : here;
26093
+ let end = at + fence.length;
26094
+ while (end < source.length && !source.startsWith(fence, end)) {
26095
+ end += source[end] === "\\" ? 2 : 1;
26096
+ }
26097
+ const literal = source.slice(at, Math.min(end + fence.length, source.length));
26098
+ code += literal.replace(/[^\n]/g, " ");
26099
+ at = end + fence.length;
26100
+ continue;
26101
+ }
26102
+ code += here;
26103
+ at += 1;
26104
+ }
26105
+ return code;
25877
26106
  }
25878
26107
  function cachedClasses(projectPath) {
25879
26108
  const cache = join6(projectPath, ".godot", "global_script_class_cache.cfg");
@@ -26697,6 +26926,9 @@ class GodotDAPClient {
26697
26926
  isConnected() {
26698
26927
  return this.connected;
26699
26928
  }
26929
+ connection() {
26930
+ return this.connected ? this.socket : null;
26931
+ }
26700
26932
  isStopped() {
26701
26933
  return this.halt !== null;
26702
26934
  }
@@ -27837,14 +28069,48 @@ class GodotLocator {
27837
28069
 
27838
28070
  // src/headless.ts
27839
28071
  import { execFile as execFile3 } from "node:child_process";
27840
- import { mkdtempSync, writeFileSync as writeFileSync7 } from "node:fs";
27841
- import { tmpdir as tmpdir3 } from "node:os";
27842
- import { join as join11 } from "node:path";
28072
+ import { writeFileSync as writeFileSync7 } from "node:fs";
28073
+ import { join as join12 } from "node:path";
27843
28074
  import { promisify as promisify3 } from "node:util";
27844
28075
 
27845
28076
  // src/scratch.ts
27846
- import { rmSync as rmSync4 } from "node:fs";
27847
- var SWEPT = { recursive: true, force: true, maxRetries: 20, retryDelay: 250 };
28077
+ import { existsSync as existsSync8, mkdtempSync, readdirSync as readdirSync4, rmSync as rmSync4 } from "node:fs";
28078
+ import { tmpdir as tmpdir3 } from "node:os";
28079
+ import { join as join11 } from "node:path";
28080
+ import process7 from "node:process";
28081
+ var SWEPT = { recursive: true, force: true };
28082
+ var AGAIN_AFTER_MS = [250, 500, 1000, 2000, 4000, 8000, 16000];
28083
+ var waiting = new Set;
28084
+ var atExit = new Set;
28085
+ var KINDS = [
28086
+ "desktop",
28087
+ "export",
28088
+ "import",
28089
+ "params",
28090
+ "runtime-screenshot",
28091
+ "tests"
28092
+ ];
28093
+ function scratchDirectory(kind) {
28094
+ return mkdtempSync(join11(tmpdir3(), `gdharness-${kind}-${process7.pid}-`));
28095
+ }
28096
+ function sweepAbandonedScratch(directory = tmpdir3()) {
28097
+ let entries;
28098
+ try {
28099
+ entries = readdirSync4(directory);
28100
+ } catch {
28101
+ return;
28102
+ }
28103
+ const named = new RegExp(`^gdharness-(${KINDS.join("|")})-(\\d+)-[A-Za-z0-9]{6}$`);
28104
+ for (const entry of entries) {
28105
+ const owner = named.exec(entry)?.[2];
28106
+ if (owner === undefined || alive(Number(owner))) {
28107
+ continue;
28108
+ }
28109
+ try {
28110
+ remove(join11(directory, entry));
28111
+ } catch {}
28112
+ }
28113
+ }
27848
28114
  function discardWith(remove, paths) {
27849
28115
  const left = [];
27850
28116
  for (const path of paths) {
@@ -27860,9 +28126,54 @@ function discardWith(remove, paths) {
27860
28126
  return left;
27861
28127
  }
27862
28128
  function discard(...paths) {
27863
- return discardWith((path) => {
27864
- rmSync4(path, SWEPT);
27865
- }, paths);
28129
+ const left = discardWith(remove, paths);
28130
+ for (const { path } of left) {
28131
+ tryAgain(path, 0);
28132
+ }
28133
+ return left;
28134
+ }
28135
+ function remove(path) {
28136
+ rmSync4(path, SWEPT);
28137
+ }
28138
+ function tryAgain(path, attempt) {
28139
+ if (attempt === 0) {
28140
+ if (waiting.has(path)) {
28141
+ return;
28142
+ }
28143
+ waiting.add(path);
28144
+ atExit.delete(path);
28145
+ lastTryOnExit();
28146
+ }
28147
+ const after = AGAIN_AFTER_MS[attempt];
28148
+ if (after === undefined || !existsSync8(path)) {
28149
+ waiting.delete(path);
28150
+ if (after === undefined) {
28151
+ atExit.add(path);
28152
+ }
28153
+ return;
28154
+ }
28155
+ setTimeout(() => {
28156
+ try {
28157
+ remove(path);
28158
+ waiting.delete(path);
28159
+ } catch {
28160
+ tryAgain(path, attempt + 1);
28161
+ }
28162
+ }, after).unref();
28163
+ }
28164
+ var lastTryWatched = false;
28165
+ function lastTryOnExit() {
28166
+ if (lastTryWatched) {
28167
+ return;
28168
+ }
28169
+ lastTryWatched = true;
28170
+ process7.once("exit", () => {
28171
+ for (const path of [...waiting, ...atExit]) {
28172
+ try {
28173
+ remove(path);
28174
+ } catch {}
28175
+ }
28176
+ });
27866
28177
  }
27867
28178
 
27868
28179
  // src/headless.ts
@@ -27905,9 +28216,9 @@ function reason(stdout, stderr) {
27905
28216
  return stdout.trim().split(/\r?\n/).at(-1) ?? "no output at all";
27906
28217
  }
27907
28218
  async function runImport(godotPath, projectPath) {
27908
- const logDir = mkdtempSync(join11(tmpdir3(), "gdharness-import-"));
28219
+ const logDir = scratchDirectory("import");
27909
28220
  try {
27910
- const { stderr } = await run3(godotPath, ["--headless", "--log-file", join11(logDir, "engine.log"), "--path", projectPath, "--import"], { signal: callSignal() });
28221
+ const { stderr } = await run3(godotPath, ["--headless", "--log-file", join12(logDir, "engine.log"), "--path", projectPath, "--import"], { signal: callSignal() });
27911
28222
  return { ok: true, messages: problems(stderr) };
27912
28223
  } catch (error) {
27913
28224
  if (error instanceof Error && "stdout" in error && "stderr" in error) {
@@ -27928,13 +28239,13 @@ async function runImport(godotPath, projectPath) {
27928
28239
  }
27929
28240
  }
27930
28241
  async function runOperation(engine, operation, params, projectPath) {
27931
- const paramsDir = mkdtempSync(join11(tmpdir3(), "gdharness-params-"));
27932
- const paramsFile = join11(paramsDir, `${operation}.json`);
28242
+ const paramsDir = scratchDirectory("params");
28243
+ const paramsFile = join12(paramsDir, `${operation}.json`);
27933
28244
  writeFileSync7(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
27934
28245
  const args = [
27935
28246
  "--headless",
27936
28247
  "--log-file",
27937
- join11(paramsDir, "engine.log"),
28248
+ join12(paramsDir, "engine.log"),
27938
28249
  "--path",
27939
28250
  projectPath,
27940
28251
  "--script",
@@ -28888,7 +29199,7 @@ async function handleLSPTool(client, toolName, args) {
28888
29199
  }
28889
29200
 
28890
29201
  // src/outside.ts
28891
- import { spawn } from "node:child_process";
29202
+ import { spawn as spawn2 } from "node:child_process";
28892
29203
  import { fileURLToPath as fileURLToPath2 } from "node:url";
28893
29204
  function changesFrom(env, base) {
28894
29205
  const changes = {};
@@ -28922,7 +29233,7 @@ function readLaunched(printed) {
28922
29233
  }
28923
29234
  async function launchOutsideTheTree(spec, runtime = process.execPath, keeper = KEEPER_SCRIPT) {
28924
29235
  return await new Promise((resolve) => {
28925
- const launcher = spawn(runtime, [keeper, "launch"], {
29236
+ const launcher = spawn2(runtime, [keeper, "launch"], {
28926
29237
  stdio: ["pipe", "pipe", "pipe"],
28927
29238
  windowsHide: true
28928
29239
  });
@@ -28959,14 +29270,14 @@ var run4 = promisify4(execFile4);
28959
29270
  var ASK_TIMEOUT_MS2 = 6000;
28960
29271
  async function cpuSecondsOf(pid) {
28961
29272
  try {
28962
- return process.platform === "win32" ? await askWindows2(pid) : await askPosix2(pid);
29273
+ return process.platform === "win32" ? await secondsOnWindows(pid) : await askPosix2(pid);
28963
29274
  } catch {
28964
29275
  return;
28965
29276
  }
28966
29277
  }
28967
- async function askWindows2(pid) {
28968
- const { stdout } = await run4("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", `(Get-Process -Id ${pid}).CPU`], { timeout: ASK_TIMEOUT_MS2, windowsHide: true });
28969
- const seconds = Number(stdout.trim().replace(",", "."));
29278
+ async function secondsOnWindows(pid) {
29279
+ const said = await askWindows(`(Get-Process -Id ${pid}).CPU`, ASK_TIMEOUT_MS2);
29280
+ const seconds = Number(said.trim().replace(",", "."));
28970
29281
  return Number.isFinite(seconds) ? seconds : undefined;
28971
29282
  }
28972
29283
  async function askPosix2(pid) {
@@ -28990,14 +29301,14 @@ function secondsFromClock(printed) {
28990
29301
  }
28991
29302
 
28992
29303
  // src/project-scan.ts
28993
- import { existsSync as existsSync8, readdirSync as readdirSync4, readFileSync as readFileSync11 } from "node:fs";
28994
- import { join as join12 } from "node:path";
29304
+ import { existsSync as existsSync9, readdirSync as readdirSync5, readFileSync as readFileSync11 } from "node:fs";
29305
+ import { join as join13 } from "node:path";
28995
29306
  var SKIPPED = new Set(["node_modules"]);
28996
29307
  function skipped(name) {
28997
29308
  return name.startsWith(".") || SKIPPED.has(name);
28998
29309
  }
28999
29310
  function steppedOver(directory) {
29000
- return existsSync8(join12(directory, ".gdignore"));
29311
+ return existsSync9(join13(directory, ".gdignore"));
29001
29312
  }
29002
29313
  var ASSET_EXTENSIONS = new Set(["png", "jpg", "jpeg", "webp", "svg", "ttf", "otf", "wav", "mp3", "ogg"]);
29003
29314
  function projectStructure(projectPath) {
@@ -29006,12 +29317,12 @@ function projectStructure(projectPath) {
29006
29317
  if (steppedOver(directory)) {
29007
29318
  return;
29008
29319
  }
29009
- for (const entry of readdirSync4(directory, { withFileTypes: true })) {
29320
+ for (const entry of readdirSync5(directory, { withFileTypes: true })) {
29010
29321
  if (skipped(entry.name)) {
29011
29322
  continue;
29012
29323
  }
29013
29324
  if (entry.isDirectory()) {
29014
- visit(join12(directory, entry.name));
29325
+ visit(join13(directory, entry.name));
29015
29326
  } else if (entry.isFile()) {
29016
29327
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
29017
29328
  if (extension === "tscn") {
@@ -29036,20 +29347,20 @@ function scriptsWithoutUid(projectPath) {
29036
29347
  if (steppedOver(directory)) {
29037
29348
  return;
29038
29349
  }
29039
- for (const entry of readdirSync4(directory, { withFileTypes: true })) {
29350
+ for (const entry of readdirSync5(directory, { withFileTypes: true })) {
29040
29351
  if (skipped(entry.name)) {
29041
29352
  continue;
29042
29353
  }
29043
29354
  const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
29044
29355
  if (entry.isDirectory()) {
29045
- visit(join12(directory, entry.name), spelled);
29356
+ visit(join13(directory, entry.name), spelled);
29046
29357
  continue;
29047
29358
  }
29048
29359
  if (!entry.isFile()) {
29049
29360
  continue;
29050
29361
  }
29051
29362
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
29052
- if (CARRIES_A_UID.has(extension) && !existsSync8(join12(directory, `${entry.name}.uid`))) {
29363
+ if (CARRIES_A_UID.has(extension) && !existsSync9(join13(directory, `${entry.name}.uid`))) {
29053
29364
  found.push(spelled);
29054
29365
  }
29055
29366
  }
@@ -29077,14 +29388,14 @@ function searchProject(projectPath, options) {
29077
29388
  if (steppedOver(directory)) {
29078
29389
  return;
29079
29390
  }
29080
- for (const entry of readdirSync4(directory, { withFileTypes: true })) {
29391
+ for (const entry of readdirSync5(directory, { withFileTypes: true })) {
29081
29392
  if (full()) {
29082
29393
  return;
29083
29394
  }
29084
29395
  if (skipped(entry.name)) {
29085
29396
  continue;
29086
29397
  }
29087
- const entryPath = join12(directory, entry.name);
29398
+ const entryPath = join13(directory, entry.name);
29088
29399
  if (entry.isDirectory()) {
29089
29400
  visit(entryPath);
29090
29401
  continue;
@@ -29119,12 +29430,12 @@ function searchProject(projectPath, options) {
29119
29430
 
29120
29431
  // src/reimport.ts
29121
29432
  import { createHash as createHash2 } from "node:crypto";
29122
- import { existsSync as existsSync9, readFileSync as readFileSync12, rmSync as rmSync5, statSync as statSync5, utimesSync, writeFileSync as writeFileSync8 } from "node:fs";
29123
- import { basename as basename2, join as join13 } from "node:path";
29433
+ import { existsSync as existsSync10, readFileSync as readFileSync12, rmSync as rmSync5, statSync as statSync5, utimesSync, writeFileSync as writeFileSync8 } from "node:fs";
29434
+ import { basename as basename2, join as join14 } from "node:path";
29124
29435
  function forceNextImport(projectPath, resourcePath) {
29125
29436
  const relative = resourcePath.replace(/^res:\/\//, "");
29126
- const sidecar = join13(projectPath, `${relative}.import`);
29127
- if (!existsSync9(sidecar)) {
29437
+ const sidecar = join14(projectPath, `${relative}.import`);
29438
+ if (!existsSync10(sidecar)) {
29128
29439
  return;
29129
29440
  }
29130
29441
  const { atime, mtimeMs } = statSync5(sidecar);
@@ -29134,7 +29445,7 @@ function forceNextImport(projectPath, resourcePath) {
29134
29445
  }
29135
29446
  function importRecord(projectPath, resourcePath) {
29136
29447
  const digest = createHash2("md5").update(resourcePath).digest("hex");
29137
- return join13(projectPath, ".godot", "imported", `${basename2(resourcePath)}-${digest}.md5`);
29448
+ return join14(projectPath, ".godot", "imported", `${basename2(resourcePath)}-${digest}.md5`);
29138
29449
  }
29139
29450
 
29140
29451
  // src/resources.ts
@@ -29380,9 +29691,9 @@ function setupResourceHandlers(mcp, getProjectPath) {
29380
29691
 
29381
29692
  // src/restart-note.ts
29382
29693
  import { mkdirSync as mkdirSync7, readFileSync as readFileSync14, rmSync as rmSync6, writeFileSync as writeFileSync9 } from "node:fs";
29383
- import { dirname as dirname8, join as join14 } from "node:path";
29694
+ import { dirname as dirname8, join as join15 } from "node:path";
29384
29695
  function restartNotePath(projectPath) {
29385
- return join14(projectPath, ".godot", "gdharness-restart.json");
29696
+ return join15(projectPath, ".godot", "gdharness-restart.json");
29386
29697
  }
29387
29698
  function noteRestartBegun(note) {
29388
29699
  const path = restartNotePath(note.projectPath);
@@ -29418,28 +29729,30 @@ function restartSettled(projectPath) {
29418
29729
  }
29419
29730
 
29420
29731
  // src/run-record.ts
29421
- import { execFileSync as execFileSync2 } from "node:child_process";
29732
+ import { execFile as execFile5 } from "node:child_process";
29422
29733
  import { createHash as createHash3 } from "node:crypto";
29423
29734
  import {
29424
29735
  mkdirSync as mkdirSync8,
29425
29736
  openSync as openSync2,
29426
- readdirSync as readdirSync5,
29737
+ readdirSync as readdirSync6,
29427
29738
  readFileSync as readFileSync15,
29428
29739
  realpathSync as realpathSync3,
29429
29740
  rmSync as rmSync7,
29430
29741
  statSync as statSync6,
29431
29742
  writeFileSync as writeFileSync10
29432
29743
  } from "node:fs";
29433
- import { basename as basename3, dirname as dirname9, join as join15, resolve as resolve5 } from "node:path";
29744
+ import { basename as basename3, dirname as dirname9, join as join16, resolve as resolve5 } from "node:path";
29745
+ import { promisify as promisify5 } from "node:util";
29746
+ var execFileAsync = promisify5(execFile5);
29434
29747
  function runsDirectory() {
29435
- return join15(runtimeDirectory(), "runs");
29748
+ return join16(runtimeDirectory(), "runs");
29436
29749
  }
29437
29750
  function realAsFarAsItExists(path) {
29438
29751
  const missing = [];
29439
29752
  let at = path;
29440
29753
  for (;; ) {
29441
29754
  try {
29442
- return join15(realpathSync3.native(at), ...missing);
29755
+ return join16(realpathSync3.native(at), ...missing);
29443
29756
  } catch {
29444
29757
  const parent = dirname9(at);
29445
29758
  if (parent === at) {
@@ -29456,16 +29769,16 @@ function projectKey(projectPath) {
29456
29769
  return createHash3("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
29457
29770
  }
29458
29771
  function runRecordPath(projectPath, directory = runtimeDirectory()) {
29459
- return join15(directory, "runs", `note-${projectKey(projectPath)}.json`);
29772
+ return join16(directory, "runs", `note-${projectKey(projectPath)}.json`);
29460
29773
  }
29461
29774
  var SHARED_NOTE = "run.json";
29462
29775
  function openTranscript(startedAt) {
29463
29776
  mkdirSync8(runsDirectory(), { recursive: true });
29464
- const path = join15(runsDirectory(), `run-${startedAt}.log`);
29777
+ const path = join16(runsDirectory(), `run-${startedAt}.log`);
29465
29778
  return { path, fd: openSync2(path, "a") };
29466
29779
  }
29467
29780
  function editorNotePath(projectPath, directory = runtimeDirectory()) {
29468
- return join15(directory, "runs", `editor-${projectKey(projectPath)}.json`);
29781
+ return join16(directory, "runs", `editor-${projectKey(projectPath)}.json`);
29469
29782
  }
29470
29783
  function writeEditorRunNote(note) {
29471
29784
  mkdirSync8(runsDirectory(), { recursive: true });
@@ -29473,7 +29786,7 @@ function writeEditorRunNote(note) {
29473
29786
  }
29474
29787
  function readEditorRunNote(projectPath) {
29475
29788
  for (const directory of runtimeDirectories()) {
29476
- const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join15(directory, "runs", "editor-run.json"));
29789
+ const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join16(directory, "runs", "editor-run.json"));
29477
29790
  if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
29478
29791
  return note;
29479
29792
  }
@@ -29505,7 +29818,7 @@ function readRunRecord(projectPath) {
29505
29818
  if (own !== null) {
29506
29819
  return own;
29507
29820
  }
29508
- const shared = recordAt(join15(directory, "runs", SHARED_NOTE));
29821
+ const shared = recordAt(join16(directory, "runs", SHARED_NOTE));
29509
29822
  if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
29510
29823
  return shared;
29511
29824
  }
@@ -29517,13 +29830,13 @@ function everyRunRecord() {
29517
29830
  for (const directory of runtimeDirectories()) {
29518
29831
  let entries;
29519
29832
  try {
29520
- entries = readdirSync5(join15(directory, "runs"));
29833
+ entries = readdirSync6(join16(directory, "runs"));
29521
29834
  } catch {
29522
29835
  continue;
29523
29836
  }
29524
29837
  for (const entry of entries) {
29525
29838
  if (entry === SHARED_NOTE || entry.startsWith("note-") && entry.endsWith(".json")) {
29526
- const record = recordAt(join15(directory, "runs", entry));
29839
+ const record = recordAt(join16(directory, "runs", entry));
29527
29840
  if (record !== null) {
29528
29841
  found.push(record);
29529
29842
  }
@@ -29561,49 +29874,47 @@ function recordAt(path) {
29561
29874
  ...typeof fields["exitSignal"] === "string" ? { exitSignal: fields["exitSignal"] } : {}
29562
29875
  };
29563
29876
  }
29564
- function runningAs(pid) {
29877
+ async function runningAs(pid) {
29565
29878
  if (process.platform === "win32") {
29566
- const answer = windowsCommandLine(pid);
29879
+ const answer = await windowsCommandLine(pid);
29567
29880
  if (answer !== null) {
29568
29881
  const [, ...rest] = answer.split(`
29569
29882
  `);
29570
29883
  const line = rest.join(`
29571
29884
  `).trim();
29572
- const began = startedAt(pid, answer);
29885
+ const began = await startedAt(pid, answer);
29573
29886
  if (line !== "") {
29574
29887
  return { kind: "commandLine", text: line, ...began === null ? {} : { startedAt: began } };
29575
29888
  }
29576
29889
  }
29577
- const image = windowsImage(pid);
29890
+ const image = await windowsImage(pid);
29578
29891
  return image === null ? null : { kind: "image", text: image };
29579
29892
  }
29580
29893
  try {
29581
- const began = startedAt(pid, null);
29894
+ const began = await startedAt(pid, null);
29582
29895
  const when = began === null ? {} : { startedAt: began };
29583
29896
  if (process.platform === "linux") {
29584
29897
  const raw = readFileSync15(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
29585
29898
  return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
29586
29899
  }
29587
- const args = execFileSync2("ps", ["-p", String(pid), "-o", "args="], {
29588
- encoding: "utf8",
29589
- timeout: 15000
29590
- }).trim();
29900
+ const args = (await execFileAsync("ps", ["-p", String(pid), "-o", "args="], { encoding: "utf8", timeout: 15000 })).stdout.trim();
29591
29901
  return args === "" ? null : { kind: "commandLine", text: args, ...when };
29592
29902
  } catch {
29593
29903
  return null;
29594
29904
  }
29595
29905
  }
29596
- function listeningPid(port) {
29906
+ async function listeningPid(port) {
29597
29907
  try {
29598
29908
  if (process.platform === "win32") {
29599
- return listeningPidInNetstat(execFileSync2("netstat", ["-ano"], { encoding: "utf8", timeout: 15000 }), port);
29909
+ const { stdout } = await execFileAsync("netstat", ["-ano"], { encoding: "utf8", timeout: 15000 });
29910
+ return listeningPidInNetstat(stdout, port);
29600
29911
  }
29601
29912
  for (const [command, args, pattern] of [
29602
29913
  ["lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], /^(\d+)/],
29603
29914
  ["ss", ["-ltnpH", `sport = :${port}`], /pid=(\d+)/]
29604
29915
  ]) {
29605
29916
  try {
29606
- const said = execFileSync2(command, [...args], { encoding: "utf8", timeout: 15000 }).trim();
29917
+ const said = (await execFileAsync(command, [...args], { encoding: "utf8", timeout: 15000 })).stdout.trim();
29607
29918
  const found = pattern.exec(said);
29608
29919
  if (found?.[1] !== undefined) {
29609
29920
  return Number(found[1]);
@@ -29628,39 +29939,31 @@ function listeningPidInNetstat(printed, port) {
29628
29939
  }
29629
29940
  return null;
29630
29941
  }
29631
- function windowsCommandLine(pid) {
29942
+ async function windowsCommandLine(pid) {
29632
29943
  try {
29633
- const answer = execFileSync2("powershell", [
29634
- "-NoProfile",
29635
- "-NonInteractive",
29636
- "-Command",
29637
- `${POWERSHELL_UTF8}$p = Get-CimInstance Win32_Process -Filter "ProcessId=${pid}"; if ($p) { $p.CreationDate.ToUniversalTime().ToString("o"); $p.CommandLine }`
29638
- ], { encoding: "utf8", timeout: 15000, windowsHide: true }).trim();
29944
+ const answer = (await askWindows(`$p = Get-CimInstance Win32_Process -Filter "ProcessId=${pid}"; if ($p) { $p.CreationDate.ToUniversalTime().ToString("o"); $p.CommandLine }`, 15000)).trim();
29639
29945
  return answer === "" ? null : answer;
29640
29946
  } catch {
29641
29947
  return null;
29642
29948
  }
29643
29949
  }
29644
- function startedAt(pid, windowsAnswer) {
29950
+ async function startedAt(pid, windowsAnswer) {
29645
29951
  if (process.platform === "win32") {
29646
29952
  const when = Date.parse(windowsAnswer?.split(`
29647
29953
  `)[0]?.trim() ?? "");
29648
29954
  return Number.isNaN(when) ? null : when;
29649
29955
  }
29650
29956
  try {
29651
- const said = execFileSync2("ps", ["-p", String(pid), "-o", "lstart="], {
29652
- encoding: "utf8",
29653
- timeout: 15000
29654
- }).trim();
29957
+ const said = (await execFileAsync("ps", ["-p", String(pid), "-o", "lstart="], { encoding: "utf8", timeout: 15000 })).stdout.trim();
29655
29958
  const when = Date.parse(said);
29656
29959
  return said === "" || Number.isNaN(when) ? null : when;
29657
29960
  } catch {
29658
29961
  return null;
29659
29962
  }
29660
29963
  }
29661
- function windowsImage(pid) {
29964
+ async function windowsImage(pid) {
29662
29965
  try {
29663
- const csv = execFileSync2("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
29966
+ const { stdout: csv } = await execFileAsync("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
29664
29967
  encoding: "utf8",
29665
29968
  timeout: 15000,
29666
29969
  windowsHide: true
@@ -29670,11 +29973,11 @@ function windowsImage(pid) {
29670
29973
  return null;
29671
29974
  }
29672
29975
  }
29673
- function stillTheRecordedRun(record) {
29674
- return judgeRun(record, runningAs(record.pid), "confirmed");
29976
+ async function stillTheRecordedRun(record) {
29977
+ return judgeRun(record, await runningAs(record.pid), "confirmed");
29675
29978
  }
29676
- function couldStillBeTheRecordedRun(record) {
29677
- return judgeRun(record, runningAs(record.pid), "possible");
29979
+ async function couldStillBeTheRecordedRun(record) {
29980
+ return judgeRun(record, await runningAs(record.pid), "possible");
29678
29981
  }
29679
29982
  var SAME_RUN_WINDOW_MS = 1e4;
29680
29983
  function judgeRun(record, running, needed) {
@@ -29702,8 +30005,8 @@ function judgeRun(record, running, needed) {
29702
30005
  const project = process.platform === "win32" ? record.projectPath.toLowerCase() : record.projectPath;
29703
30006
  return said.includes(project);
29704
30007
  }
29705
- function stillTheAnnouncedGame(pid, projectPath, announcedAt) {
29706
- return judgeAnnouncedGame(runningAs(pid), projectPath, announcedAt);
30008
+ async function stillTheAnnouncedGame(pid, projectPath, announcedAt) {
30009
+ return judgeAnnouncedGame(await runningAs(pid), projectPath, announcedAt);
29707
30010
  }
29708
30011
  function judgeAnnouncedGame(running, projectPath, announcedAt) {
29709
30012
  if (running?.kind !== "commandLine" || running.startedAt === undefined) {
@@ -29720,7 +30023,7 @@ function judgeAnnouncedGame(running, projectPath, announcedAt) {
29720
30023
  }
29721
30024
  function clearRunRecord(projectPath) {
29722
30025
  for (const directory of runtimeDirectories()) {
29723
- const shared = join15(directory, "runs", SHARED_NOTE);
30026
+ const shared = join16(directory, "runs", SHARED_NOTE);
29724
30027
  const sharedRecord = recordAt(shared);
29725
30028
  const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
29726
30029
  for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
@@ -29742,7 +30045,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
29742
30045
  const kept = new Set(everyRunRecord().map((record) => record.transcript));
29743
30046
  let entries;
29744
30047
  try {
29745
- entries = readdirSync5(directory);
30048
+ entries = readdirSync6(directory);
29746
30049
  } catch {
29747
30050
  return;
29748
30051
  }
@@ -29750,7 +30053,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
29750
30053
  if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
29751
30054
  continue;
29752
30055
  }
29753
- const path = join15(directory, entry);
30056
+ const path = join16(directory, entry);
29754
30057
  if (kept.has(path) || ageMs(path, now) < keepMs) {
29755
30058
  continue;
29756
30059
  }
@@ -29764,16 +30067,16 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
29764
30067
  import { createHash as createHash4 } from "node:crypto";
29765
30068
  import {
29766
30069
  cpSync,
29767
- existsSync as existsSync10,
30070
+ existsSync as existsSync11,
29768
30071
  mkdirSync as mkdirSync9,
29769
- readdirSync as readdirSync6,
30072
+ readdirSync as readdirSync7,
29770
30073
  readFileSync as readFileSync16,
29771
30074
  rmdirSync,
29772
30075
  rmSync as rmSync8,
29773
30076
  statSync as statSync7,
29774
30077
  writeFileSync as writeFileSync11
29775
30078
  } from "node:fs";
29776
- import { dirname as dirname10, join as join16 } from "node:path";
30079
+ import { dirname as dirname10, join as join17 } from "node:path";
29777
30080
  import { fileURLToPath as fileURLToPath3 } from "node:url";
29778
30081
  var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
29779
30082
  var EDITOR_PLUGINS = ["gdharness_editor", "auto_reload"];
@@ -29786,11 +30089,11 @@ var DIGEST_MARKER = ".gdharness-digest";
29786
30089
  function editorAddonDigest(from = shippedAddonsDirectory()) {
29787
30090
  const hash = createHash4("sha256");
29788
30091
  const walk = (directory, relative) => {
29789
- for (const name of readdirSync6(directory).sort()) {
30092
+ for (const name of readdirSync7(directory).sort()) {
29790
30093
  if (name === VERSION_MARKER || name === DIGEST_MARKER) {
29791
30094
  continue;
29792
30095
  }
29793
- const path = join16(directory, name);
30096
+ const path = join17(directory, name);
29794
30097
  const inside = `${relative}/${name}`;
29795
30098
  if (statSync7(path).isDirectory()) {
29796
30099
  walk(path, inside);
@@ -29804,7 +30107,7 @@ function editorAddonDigest(from = shippedAddonsDirectory()) {
29804
30107
  }
29805
30108
  };
29806
30109
  for (const name of EDITOR_PLUGINS) {
29807
- walk(join16(from, name), name);
30110
+ walk(join17(from, name), name);
29808
30111
  }
29809
30112
  return hash.digest("hex");
29810
30113
  }
@@ -29820,11 +30123,11 @@ function shippedEditorDigest() {
29820
30123
  return shippedDigest ?? undefined;
29821
30124
  }
29822
30125
  function shippedAddonsDirectory() {
29823
- return join16(dirname10(fileURLToPath3(import.meta.url)), "godot", "addons");
30126
+ return join17(dirname10(fileURLToPath3(import.meta.url)), "godot", "addons");
29824
30127
  }
29825
30128
  function installedAddonVersion(projectPath) {
29826
- const marker = join16(projectPath, "addons", ADDONS[0], VERSION_MARKER);
29827
- if (!existsSync10(marker)) {
30129
+ const marker = join17(projectPath, "addons", ADDONS[0], VERSION_MARKER);
30130
+ if (!existsSync11(marker)) {
29828
30131
  return null;
29829
30132
  }
29830
30133
  const said = readFileSync16(marker, "utf8").trim();
@@ -30479,7 +30782,11 @@ var TOOL_SPECS = [
30479
30782
  name: "editor_run",
30480
30783
  description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. What runs is a scene: a SceneTree script is not an entry point here, so put the script on the root of a scene of its own and name that in scene. args hands the game its own flags, the ones it reads with OS.get_cmdline_user_args(), and a run carrying any is started by this server rather than by the editor. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way. start and check both wait first for a scan the open editor is running and for the class cache it writes just after, since a game booted in between resolves no global class, as does every other engine this server starts on the project; waitedForEditorScanMs says how long, and scanNote says when a scan ran past thirty seconds and the game started during it. A start waits for the game to become something the runtime_* tools can talk to and says which it is under runtime: listening with the port it took, or why not, so the first call after a start does not have to be made twice. When it is not listening, mayYetAnnounce says whether that is final: false is a runtime that is not coming, true is a game still on its way up, which editor_status will see and runtimeWaitMs waits longer for. A start this server made also answers with transcript, the file both the run's streams are written to, so a watch on it can be armed off the start rather than off a second call.",
30481
30784
  parameters: {
30482
- projectPath: { ...PROJECT_PATH, ops: ["start", "check"] },
30785
+ projectPath: {
30786
+ ...PROJECT_PATH,
30787
+ ops: ["start", "check", "stop", "wait"],
30788
+ description: "Absolute path to the project directory, the one holding project.godot. stop, wait: optional, the project whose run is meant; a run of another project is refused rather than touched."
30789
+ },
30483
30790
  scene: {
30484
30791
  type: "string",
30485
30792
  ops: ["start", "check"],
@@ -30497,6 +30804,11 @@ var TOOL_SPECS = [
30497
30804
  ops: ["start"],
30498
30805
  description: "start: on Windows, show the window on the desktop in use. By default a windowed run this server starts goes on a desktop of its own, where it renders and runtime_capture works but the window never shows and never takes the keyboard. A game a visible editor plays opens on the desktop in use, and one a hidden editor (editor_launch hidden: true) plays goes on its desktop. Elsewhere than Windows the window shows either way. Default false."
30499
30806
  },
30807
+ silent: {
30808
+ type: "boolean",
30809
+ ops: ["start"],
30810
+ 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, and the debug_* tools do not answer for it. 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."
30811
+ },
30500
30812
  savesIn: {
30501
30813
  type: "string",
30502
30814
  ops: ["start", "check"],
@@ -30611,7 +30923,7 @@ var TOOL_SPECS = [
30611
30923
  },
30612
30924
  {
30613
30925
  name: "editor_rescan",
30614
- 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. 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.",
30926
+ 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. 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. 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.",
30615
30927
  parameters: {
30616
30928
  projectPath: PROJECT_PATH,
30617
30929
  timeoutMs: {
@@ -30717,7 +31029,7 @@ var TOOL_SPECS = [
30717
31029
  says: {
30718
31030
  type: "string",
30719
31031
  ops: ["find"],
30720
- description: "find: part of what the node has written on it as drawn, the way the text op reads it, case-insensitively, 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."
31032
+ description: 'find: part of what the node has written on it as drawn, the way the text op reads it, case-insensitively, 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.'
30721
31033
  },
30722
31034
  limit: {
30723
31035
  type: "integer",
@@ -30872,7 +31184,7 @@ var TOOL_SPECS = [
30872
31184
  says: {
30873
31185
  type: "string",
30874
31186
  ops: ["click"],
30875
- description: "click: the words on the control to click, matched the way runtime_inspect find matches says (a plain word is contained in the text, a pattern with * or ? is a glob over it), among the controls on screen, and pressed in the same frame it was found in, so a panel rebuilding between a find and a click cannot free it first. Words on a label inside a button press the button. A control with something drawn over its centre that would take the pointer, such as a screen shown over it, is not on screen. The matches are ranked the way a person picks what to press: a button before text that cannot be pressed, then a control whose whole text is the words before one saying them as part of more, and the best one is pressed; found.picked says why. Several matches at the best rank are refused with each one listed, and index picks one."
31187
+ description: "click: the words on the control to click, matched the way runtime_inspect find matches says (a plain word is contained in the text, a pattern with * or ? is a glob over it, and | separates alternatives), among the controls on screen, and pressed in the same frame it was found in, so a panel rebuilding between a find and a click cannot free it first. Words on a label inside a button press the button. A control with something drawn over its centre that would take the pointer, such as a screen shown over it, is not on screen. The matches are ranked the way a person picks what to press: a button before text that cannot be pressed, then a control whose whole text, or one whole line of it such as the title on a card, is the words before one saying them as part of more, and the best one is pressed; found.picked says why. Several matches at the best rank are refused with each one listed, and index picks one."
30876
31188
  },
30877
31189
  action: {
30878
31190
  type: "string",
@@ -31003,7 +31315,7 @@ var TOOL_SPECS = [
31003
31315
  says: {
31004
31316
  type: "string",
31005
31317
  ops: ["until"],
31006
- description: "until: wait for these words to appear anywhere under nodePath, or anywhere in the game when no nodePath is given, instead of for a property, which is how a panel that rebuilds its labels is waited on at all: the labels are named afresh each redraw and the panel is what stays put. Matched the way runtime_inspect find matches says: case-insensitive, a plain word anywhere in the text, a pattern with * or ? a glob over the whole of it; on a node the player can see unless includeHidden says otherwise; a label with a line break is matched with the break in the words, and a backslash followed by n counts as one. Instead of, not as well as: a call carrying this and a property is refused, because they ask about different things and answering one of them silently is how a caller watches a screen believing they are watching a property. It looks every frame while a look is cheap and less often on a screen large enough for a look to cost a real share of a frame, so the game keeps its speed; the answer counts the frames the wait spanned and the looks it took."
31318
+ description: 'until: wait for these words to appear anywhere under nodePath, or anywhere in the game when no nodePath is given, instead of for a property, which is how a panel that rebuilds its labels is waited on at all: the labels are named afresh each redraw and the panel is what stays put. Matched the way runtime_inspect find matches says: case-insensitive, a plain word anywhere in the text, a pattern with * or ? a glob over the whole of it, and alternatives separated by | met by whichever appears first ("*won*|*lost*"); on a node the player can see unless includeHidden says otherwise; a label with a line break is matched with the break in the words, and a backslash followed by n counts as one. Instead of, not as well as: a call carrying this and a property is refused, because they ask about different things and answering one of them silently is how a caller watches a screen believing they are watching a property. It looks every frame while a look is cheap and less often on a screen large enough for a look to cost a real share of a frame, so the game keeps its speed; the answer counts the frames the wait spanned and the looks it took.'
31007
31319
  },
31008
31320
  includeHidden: {
31009
31321
  type: "boolean",
@@ -31124,7 +31436,7 @@ function buildToolDefinitions() {
31124
31436
  // src/server.ts
31125
31437
  var UPDATE_NOTICE_EVERY = 500;
31126
31438
  var FEEDBACK_NOTICE_EVERY = 250;
31127
- var run5 = promisify5(execFile5);
31439
+ var run5 = promisify6(execFile6);
31128
31440
  var __dirname2 = dirname11(fileURLToPath4(import.meta.url));
31129
31441
  var EDITOR_RESTART_TIMEOUT_MS = 90000;
31130
31442
  var HOLD_PING_MS = 2000;
@@ -31140,15 +31452,15 @@ function captureDestinationRefusal(outputPath, projects) {
31140
31452
  return `outputPath ${outputPath} does not end in .png, which is what a capture is.`;
31141
31453
  }
31142
31454
  const directory = realPathOr(dirname11(outputPath));
31143
- if (!existsSync11(directory)) {
31455
+ if (!existsSync12(directory)) {
31144
31456
  return `outputPath's directory ${dirname11(outputPath)} does not exist; a capture creates no directories.`;
31145
31457
  }
31146
- const where = join17(directory, basename4(outputPath));
31458
+ const where = join18(directory, basename4(outputPath));
31147
31459
  const inside = projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
31148
31460
  if (inside !== undefined) {
31149
31461
  return `outputPath ${outputPath} is inside the project at ${inside}; a capture is saved outside it, so it never becomes part of the game.`;
31150
31462
  }
31151
- if (existsSync11(where)) {
31463
+ if (existsSync12(where)) {
31152
31464
  return `outputPath ${outputPath} already exists; a capture is not written over a file. Name a new one.`;
31153
31465
  }
31154
31466
  return null;
@@ -31308,7 +31620,8 @@ function aboutTheChildren(ended, throughEditor) {
31308
31620
  return throughEditor ? `Anything that game started for itself, with OS.create_process or otherwise, is not the editor's to stop and was not signalled; ${reaches}` : `Anything that game started for itself, with OS.create_process or otherwise, is a separate process this stop did not signal; ${reaches}`;
31309
31621
  }
31310
31622
  if (ended.unknown === "platform") {
31311
- return "andChildren was asked for and this platform would not list what the game had started, so nothing else was ended.";
31623
+ const why = whyTheProcessTreeFailed();
31624
+ return `andChildren was asked for and this platform would not list what the game had started, so nothing else was ended.${why === undefined ? "" : ` The listing failed: ${why}.`}`;
31312
31625
  }
31313
31626
  if (ended.unknown === "no process") {
31314
31627
  return "andChildren was asked for and this run has no process id to list under: the editor plays it and its game announced none, so nothing else was ended.";
@@ -31391,17 +31704,6 @@ function runtimeVerdict(endpoint, after) {
31391
31704
  heldAt: null
31392
31705
  };
31393
31706
  }
31394
- function alive(pid) {
31395
- if (pid === undefined || pid === null) {
31396
- return false;
31397
- }
31398
- try {
31399
- process.kill(pid, 0);
31400
- return true;
31401
- } catch (error) {
31402
- return error.code === "EPERM";
31403
- }
31404
- }
31405
31707
  function isOfType(value, kind) {
31406
31708
  switch (kind) {
31407
31709
  case "string":
@@ -31649,13 +31951,13 @@ function aboveTheRunner(entry) {
31649
31951
  }
31650
31952
  function elsewhereIn(projectPath, asked) {
31651
31953
  const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
31652
- if (wanted === "" || existsSync11(join17(projectPath, wanted))) {
31954
+ if (wanted === "" || existsSync12(join18(projectPath, wanted))) {
31653
31955
  return "";
31654
31956
  }
31655
31957
  const root = (wanted.split("/")[0] ?? "").toLowerCase();
31656
31958
  let near = [];
31657
31959
  try {
31658
- near = readdirSync7(projectPath, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.toLowerCase() !== root).map((entry) => entry.name).filter((name) => name.toLowerCase().startsWith("test") || root.startsWith(name.toLowerCase()));
31960
+ near = readdirSync8(projectPath, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.toLowerCase() !== root).map((entry) => entry.name).filter((name) => name.toLowerCase().startsWith("test") || root.startsWith(name.toLowerCase()));
31659
31961
  } catch {
31660
31962
  return "";
31661
31963
  }
@@ -31671,7 +31973,7 @@ function runtimeAddonAnnouncesEditors(projectPath) {
31671
31973
  return false;
31672
31974
  }
31673
31975
  try {
31674
- return readFileSync17(join17(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
31976
+ return readFileSync17(join18(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
31675
31977
  } catch {
31676
31978
  return false;
31677
31979
  }
@@ -31696,7 +31998,7 @@ function needsItsOwnProcess(asked) {
31696
31998
  class GodotServer {
31697
31999
  mcp;
31698
32000
  locator = new GodotLocator;
31699
- operationsScript = join17(__dirname2, "godot", "operations", "godot_operations.gd");
32001
+ operationsScript = join18(__dirname2, "godot", "operations", "godot_operations.gd");
31700
32002
  godotBridge;
31701
32003
  tools = buildToolDefinitions();
31702
32004
  activeProcess = null;
@@ -31708,6 +32010,7 @@ class GodotServer {
31708
32010
  lastGodotVersion = null;
31709
32011
  lspClient = null;
31710
32012
  dapClient = null;
32013
+ adapterPassed = null;
31711
32014
  breakpointsSeededOn = null;
31712
32015
  bridgeStartupError = null;
31713
32016
  bridgeRetry = null;
@@ -31766,6 +32069,7 @@ class GodotServer {
31766
32069
  this.handleShutdown("stdin:end", 0);
31767
32070
  });
31768
32071
  console.error("gdharness running on stdio");
32072
+ sweepAbandonedScratch();
31769
32073
  try {
31770
32074
  await this.takeOverFromAPredecessor();
31771
32075
  await this.godotBridge.start();
@@ -32092,7 +32396,7 @@ class GodotServer {
32092
32396
  this.callsSinceFeedback = 0;
32093
32397
  return {
32094
32398
  ...answer,
32095
- content: [...answer.content, { type: "text", text: feedbackNotice() }]
32399
+ content: [...answer.content, { type: "text", text: feedbackNotice(FEEDBACK_NOTICE_EVERY) }]
32096
32400
  };
32097
32401
  }
32098
32402
  validateArguments(spec, args) {
@@ -32436,8 +32740,8 @@ class GodotServer {
32436
32740
  if (path === undefined) {
32437
32741
  return { ok: false, response: this.createErrorResponse("projectPath is required.") };
32438
32742
  }
32439
- const file = join17(path, "project.godot");
32440
- if (!existsSync11(file)) {
32743
+ const file = join18(path, "project.godot");
32744
+ if (!existsSync12(file)) {
32441
32745
  return {
32442
32746
  ok: false,
32443
32747
  response: this.createErrorResponse(`Not a Godot project: ${path}`, [
@@ -32598,13 +32902,13 @@ class GodotServer {
32598
32902
  let sawAScan = false;
32599
32903
  let scanEndedAt = null;
32600
32904
  for (;; ) {
32601
- const asked = Date.now();
32602
32905
  let status;
32603
32906
  try {
32604
32907
  status = asParams(await this.godotBridge.invokeTool("scan_status", {}));
32605
32908
  } catch {
32606
32909
  return { waitedMs: Date.now() - started, stillScanning: false };
32607
32910
  }
32911
+ const answered = Date.now();
32608
32912
  if (status["scanning"] === true || status["importing"] === true || status["pending"] === true) {
32609
32913
  sawAScan = true;
32610
32914
  if (Date.now() - started >= SCAN_WAIT_MS) {
@@ -32614,7 +32918,7 @@ class GodotServer {
32614
32918
  continue;
32615
32919
  }
32616
32920
  const since = readNumber(status, "sinceScanFinishedMs");
32617
- scanEndedAt = since !== undefined && since >= 0 ? asked - since : sawAScan ? asked : null;
32921
+ scanEndedAt = since !== undefined && since >= 0 ? answered - since : sawAScan ? answered : null;
32618
32922
  break;
32619
32923
  }
32620
32924
  let waitedForTheWrite = false;
@@ -33033,11 +33337,11 @@ class GodotServer {
33033
33337
  return this.createErrorResponse(`Cannot create ${dirname11(output.relativePath)} to export into: ${errorMessage(error)}`);
33034
33338
  }
33035
33339
  const debug = readBoolean(args, "debug") ?? false;
33036
- const exportLogs = mkdtempSync2(join17(tmpdir4(), "gdharness-export-"));
33340
+ const exportLogs = scratchDirectory("export");
33037
33341
  const exportArgs = [
33038
33342
  "--headless",
33039
33343
  "--log-file",
33040
- join17(exportLogs, "engine.log"),
33344
+ join18(exportLogs, "engine.log"),
33041
33345
  "--path",
33042
33346
  project.value.path,
33043
33347
  debug ? "--export-debug" : "--export-release",
@@ -33069,7 +33373,7 @@ class GodotServer {
33069
33373
  log.finish();
33070
33374
  const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
33071
33375
  const verdict = {
33072
- exported: ending.exitCode === 0 && log.count("error") === 0 && existsSync11(output.absolutePath),
33376
+ exported: ending.exitCode === 0 && log.count("error") === 0 && existsSync12(output.absolutePath),
33073
33377
  preset,
33074
33378
  outputPath: output.relativePath,
33075
33379
  debug,
@@ -33105,7 +33409,7 @@ class GodotServer {
33105
33409
  return contained.response;
33106
33410
  }
33107
33411
  const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
33108
- if (!existsSync11(join17(project.value.path, runner))) {
33412
+ if (!existsSync12(join18(project.value.path, runner))) {
33109
33413
  return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
33110
33414
  "Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
33111
33415
  ]);
@@ -33141,7 +33445,7 @@ class GodotServer {
33141
33445
  ];
33142
33446
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
33143
33447
  this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
33144
- const userData = mkdtempSync2(join17(tmpdir4(), "gdharness-tests-"));
33448
+ const userData = scratchDirectory("tests");
33145
33449
  const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
33146
33450
  const console2 = [];
33147
33451
  let lastSaid = Date.now();
@@ -33168,14 +33472,17 @@ class GodotServer {
33168
33472
  resolve(false);
33169
33473
  });
33170
33474
  });
33171
- const reportsDir = join17(project.value.path, ".godot", ...ours.split("/"));
33475
+ if (timedOut && run.process.pid !== undefined) {
33476
+ await untilGone([run.process.pid]);
33477
+ }
33478
+ const reportsDir = join18(project.value.path, ".godot", ...ours.split("/"));
33172
33479
  let report = null;
33173
33480
  let reportProblem = null;
33174
33481
  try {
33175
- const written = existsSync11(reportsDir) ? readdirSync7(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
33482
+ const written = existsSync12(reportsDir) ? readdirSync8(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
33176
33483
  const newest = written.at(-1);
33177
33484
  if (newest !== undefined) {
33178
- report = parseJUnit(readFileSync17(join17(reportsDir, newest, "results.xml"), "utf8"));
33485
+ report = parseJUnit(readFileSync17(join18(reportsDir, newest, "results.xml"), "utf8"));
33179
33486
  }
33180
33487
  } catch (error) {
33181
33488
  reportProblem = errorMessage(error);
@@ -33333,7 +33640,7 @@ class GodotServer {
33333
33640
  const cached = cachedClasses(projectPath);
33334
33641
  const contradicted = cached === null ? [] : contradictedDiagnostics(messages, cached, (resourcePath) => {
33335
33642
  const contained = resolveWithinProject(projectPath, resourcePath);
33336
- if (!contained.ok || !existsSync11(contained.absolutePath)) {
33643
+ if (!contained.ok || !existsSync12(contained.absolutePath)) {
33337
33644
  return null;
33338
33645
  }
33339
33646
  try {
@@ -33387,7 +33694,7 @@ class GodotServer {
33387
33694
  }
33388
33695
  sourceOfClass(projectPath, resourcePath) {
33389
33696
  const contained = resolveWithinProject(projectPath, resourcePath);
33390
- if (!contained.ok || !existsSync11(contained.absolutePath)) {
33697
+ if (!contained.ok || !existsSync12(contained.absolutePath)) {
33391
33698
  return null;
33392
33699
  }
33393
33700
  try {
@@ -33408,28 +33715,32 @@ class GodotServer {
33408
33715
  async handleDAP(toolName, args) {
33409
33716
  return handleDAPTool(this.dap(), toolName, args);
33410
33717
  }
33411
- adapterBelongsElsewhere() {
33718
+ async adapterBelongsElsewhere() {
33412
33719
  const status = this.godotBridge.getStatus();
33413
33720
  const ours = status.editorPid;
33414
33721
  if (ours === undefined) {
33415
33722
  return null;
33416
33723
  }
33724
+ const connection = this.dapClient?.connection() ?? null;
33725
+ if (connection !== null && this.adapterPassed?.connection === connection && this.adapterPassed.editorPid === ours) {
33726
+ return null;
33727
+ }
33417
33728
  const port = this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT);
33418
- const holder = listeningPid(port);
33729
+ const askedAt = Date.now();
33730
+ const holder = await listeningPid(port);
33731
+ this.logDebug(`Asked who holds debug adapter port ${port} in ${Date.now() - askedAt}ms: ${holder ?? "not said"}`);
33419
33732
  if (holder === null || holder === ours) {
33733
+ this.adapterPassed = connection === null ? null : { connection, editorPid: ours };
33420
33734
  return null;
33421
33735
  }
33422
33736
  return `The debug adapter on port ${port} belongs to process ${holder}, and the editor on this server's bridge is process ${ours}. Godot gives every editor the same debug adapter port by default, so the one this editor names in its settings is not necessarily the one it holds. Playing the game now would put another editor's console and another project's debugger under this project's name.`;
33423
33737
  }
33424
- anEditorIsStillComing(listeningSince) {
33425
- return anEditorIsStillComing(listeningSince, this.launchedEditorIsUp());
33426
- }
33427
- launchedEditorIsUp() {
33738
+ async launchedEditorIsUp() {
33428
33739
  const launched = this.launchedEditor;
33429
33740
  if (launched === null || !alive(launched.pid)) {
33430
33741
  return false;
33431
33742
  }
33432
- const now = startTimesOf([launched.pid]).get(launched.pid);
33743
+ const now = (await startTimesOf([launched.pid])).get(launched.pid);
33433
33744
  return launched.startedAt === null || now === undefined || now === launched.startedAt;
33434
33745
  }
33435
33746
  dap() {
@@ -33518,8 +33829,9 @@ class GodotServer {
33518
33829
  }
33519
33830
  return owed;
33520
33831
  }
33521
- getEditorStatusPayload() {
33832
+ async getEditorStatusPayload() {
33522
33833
  const status = this.godotBridge.getStatus();
33834
+ const launchedIsUp = !status.connected && await this.launchedEditorIsUp();
33523
33835
  const isPortConflict = this.bridgeStartupError?.includes("EADDRINUSE") ?? false;
33524
33836
  const stale = status.connected && editorIsStale(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest());
33525
33837
  const unfinished = this.restartLeftUnfinished(status.connected);
@@ -33528,14 +33840,14 @@ class GodotServer {
33528
33840
  serverVersion: SERVER_VERSION,
33529
33841
  addonIsStale: status.connected ? stale : undefined,
33530
33842
  bridgeAvailable: this.bridgeStartupError === null,
33531
- mayYetConnect: status.connected ? undefined : unfinished !== null ? false : this.anEditorIsStillComing(status.listeningSince),
33843
+ mayYetConnect: status.connected ? undefined : unfinished !== null ? false : anEditorIsStillComing(status.listeningSince, launchedIsUp),
33532
33844
  restartInterrupted: unfinished === null ? undefined : {
33533
33845
  quitEditorPid: unfinished.editorPid,
33534
33846
  quitAt: unfinished.quitAt,
33535
33847
  byServerPid: unfinished.byPid,
33536
33848
  note: "A restart begun by a previous gdharness server asked this project's editor to quit, and that server was ended before it could start the editor again, so nothing is coming. editor_launch open starts one, and clears this."
33537
33849
  },
33538
- awaitingLaunchedEditor: status.connected || !this.launchedEditorIsUp() ? undefined : this.launchedEditor?.pid ?? undefined,
33850
+ awaitingLaunchedEditor: status.connected || !launchedIsUp ? undefined : this.launchedEditor?.pid ?? undefined,
33539
33851
  ...status.connected ? this.whatTheLaunchedEditorDropped() : {},
33540
33852
  startupError: this.bridgeStartupError,
33541
33853
  portNote: status.portWanted === undefined ? undefined : `Port ${status.portWanted}, the one this server was configured with, was held ${this.notHandedOverBy === null ? "by another process" : `by pid ${this.notHandedOverBy}, the gdharness server this one replaced, which had not let go of it ${HANDOVER_MS / 1000} seconds after being told it was replaced`} when the bridge started, so it took ${status.port} and announced that where the editor looks for it${this.announcedAt === null ? "" : `, ${this.announcedAt}`}. The editor finds it there; the next server started takes ${status.portWanted} again if it is free.`,
@@ -33624,7 +33936,7 @@ class GodotServer {
33624
33936
  const asked = (async () => {
33625
33937
  const playing = await this.editorPlayingState();
33626
33938
  await this.pickUpWhatTheEditorIsPlaying();
33627
- const current = this.currentRun();
33939
+ const current = await this.currentRun();
33628
33940
  const active = current === null ? false : await this.runStillGoing(current, playing?.playing ?? null);
33629
33941
  return {
33630
33942
  playing,
@@ -33632,14 +33944,15 @@ class GodotServer {
33632
33944
  hold: current === null ? {} : await this.holdOf(current, active)
33633
33945
  };
33634
33946
  })();
33635
- const [games, { playing, active, hold }, scan] = await Promise.all([
33947
+ const [games, { playing, active, hold }, scan, editorStatus] = await Promise.all([
33636
33948
  pinged,
33637
33949
  asked,
33638
- this.editorScanState()
33950
+ this.editorScanState(),
33951
+ this.getEditorStatusPayload()
33639
33952
  ]);
33640
33953
  return this.jsonTextResponse({
33641
33954
  editor: {
33642
- ...this.getEditorStatusPayload(),
33955
+ ...editorStatus,
33643
33956
  debugPort: playing?.debugPort ?? this.godotBridge.getStatus().debugPort,
33644
33957
  scan
33645
33958
  },
@@ -33669,7 +33982,7 @@ class GodotServer {
33669
33982
  return null;
33670
33983
  }
33671
33984
  try {
33672
- return settingKeys(readFileSync17(join17(projectPath, "project.godot"), "utf8"));
33985
+ return settingKeys(readFileSync17(join18(projectPath, "project.godot"), "utf8"));
33673
33986
  } catch {
33674
33987
  return null;
33675
33988
  }
@@ -33818,7 +34131,7 @@ class GodotServer {
33818
34131
  writeEditorLogNote(projectPath, launched.pid);
33819
34132
  this.launchedEditor = {
33820
34133
  pid: launched.pid,
33821
- startedAt: startTimesOf([launched.pid]).get(launched.pid) ?? null
34134
+ startedAt: (await startTimesOf([launched.pid])).get(launched.pid) ?? null
33822
34135
  };
33823
34136
  return { pid: launched.pid, error: null };
33824
34137
  }
@@ -33866,13 +34179,15 @@ class GodotServer {
33866
34179
  }
33867
34180
  await this.pickUpWhatTheEditorIsPlaying();
33868
34181
  let ended = null;
33869
- const before = this.currentRun();
34182
+ const before = await this.currentRun();
33870
34183
  if (before !== null && await this.runStillGoing(before)) {
33871
34184
  this.logDebug("Ending the running game before starting another");
34185
+ const endingFrom = Date.now();
33872
34186
  ended = {
33873
34187
  pid: before.pid ?? this.announcedPidOf(before) ?? null,
33874
34188
  ending: await this.endActiveGame("editor_run start, which ends the run that was going before it starts another", true)
33875
34189
  };
34190
+ this.logDebug(`The running game was ${ended.ending} after ${Date.now() - endingFrom}ms`);
33876
34191
  }
33877
34192
  const alreadyPlaying = new Set(discoverRuntimes().map((endpoint) => endpoint.pid));
33878
34193
  const wereHereFirst = this.everyRuntimeOfOurs();
@@ -33882,14 +34197,16 @@ class GodotServer {
33882
34197
  });
33883
34198
  const runtimeWaitMs = this.announceBudget(args, project.value.path);
33884
34199
  const editorWouldPlay = !headless || editorPlaysHeadless(project.value.file);
33885
- if (this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0 && !needsItsOwnProcess(asked.value)) {
34200
+ const silent = resolveSilent(args["silent"], process.env) && !headless;
34201
+ if (this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0 && !needsItsOwnProcess(asked.value) && !silent) {
33886
34202
  return await this.playThroughEditor(sceneArgument, refreshed.value, scanned, project.value.path, alreadyPlaying, runtimeWaitMs, ended);
33887
34203
  }
33888
34204
  const cmdArgs = runArguments({
33889
34205
  projectPath: project.value.path,
33890
34206
  headless,
33891
34207
  scene: sceneArgument,
33892
- userArgs: given.value
34208
+ userArgs: given.value,
34209
+ silent
33893
34210
  });
33894
34211
  this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
33895
34212
  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);
@@ -33900,7 +34217,7 @@ class GodotServer {
33900
34217
  }
33901
34218
  started.announcedBefore = alreadyPlaying;
33902
34219
  this.activeProcess = started;
33903
- const spawnedInstead = this.godotBridge.isConnected() && editorWouldPlay && given.value.length > 0 ? " The editor cannot be handed arguments for a game it plays, so this one was started " + "here: the debug_* tools answer only for a game the editor is playing." : this.godotBridge.isConnected() && editorWouldPlay && needsItsOwnProcess(asked.value) ? " The editor plays a game with its own environment, so a run given one was started " + "here: the debug_* tools answer only for a game the editor is playing." : "";
34220
+ const spawnedInstead = this.godotBridge.isConnected() && editorWouldPlay && given.value.length > 0 ? " The editor cannot be handed arguments for a game it plays, so this one was started " + "here: the debug_* tools answer only for a game the editor is playing." : this.godotBridge.isConnected() && editorWouldPlay && needsItsOwnProcess(asked.value) ? " The editor plays a game with its own environment, so a run given one was started " + "here: the debug_* tools answer only for a game the editor is playing." : 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"}: the debug_* tools answer only for a game the editor is playing, and silent: false plays it through the editor.` : "";
33904
34221
  const endedForThis = endedToStartThis(ended);
33905
34222
  const stranded = wereHereFirst.filter((one) => one.pid !== ended?.pid && alive(one.pid));
33906
34223
  const alsoRunning = leftRunningNote(stranded);
@@ -33922,7 +34239,7 @@ class GodotServer {
33922
34239
  }
33923
34240
  async runtimeUp(projectPath, before, budget, withArgs) {
33924
34241
  const budgetMs = budget.ms;
33925
- if (!existsSync11(join17(projectPath, RUNTIME_AUTOLOAD.path))) {
34242
+ if (!existsSync12(join18(projectPath, RUNTIME_AUTOLOAD.path))) {
33926
34243
  return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
33927
34244
  }
33928
34245
  let asking = null;
@@ -33933,12 +34250,12 @@ class GodotServer {
33933
34250
  const lastSaid = () => editorSaysGoing ? "playing" : "not playing";
33934
34251
  const endpoint = await announcedSince(projectPath, before, {
33935
34252
  budgetMs,
33936
- accept: (one) => this.isTheGameOf(this.currentRun(), one, projectPath),
34253
+ accept: (one) => this.isTheGameOf(this.activeProcess, one, projectPath),
33937
34254
  giveUp: () => {
33938
34255
  if (this.dapClient?.isStopped() === true) {
33939
34256
  return true;
33940
34257
  }
33941
- const run = this.currentRun();
34258
+ const run = this.activeProcess;
33942
34259
  if (run === null || !run.throughEditor) {
33943
34260
  return !stillRunning(run);
33944
34261
  }
@@ -33956,7 +34273,7 @@ class GodotServer {
33956
34273
  });
33957
34274
  const asked = editorAsks === 0 ? "editor not asked" : `editor asked ${editorAsks === 1 ? "once" : `${editorAsks} times`}, last said ${lastSaid()}`;
33958
34275
  this.logDebug(`announce wait ended after ${Date.now() - waitBegan}ms: ${endpoint === null ? "nothing announced" : `pid ${endpoint.pid} announced`}, ${asked}`);
33959
- const going = this.currentRun();
34276
+ const going = await this.currentRun();
33960
34277
  if (endpoint !== null && going !== null && going.announcedPid === undefined) {
33961
34278
  this.tie(going, endpoint);
33962
34279
  }
@@ -33997,8 +34314,10 @@ class GodotServer {
33997
34314
  }
33998
34315
  async playThroughEditor(scene, refreshedClasses, scanned, projectPath, alreadyPlaying, runtimeWaitMs, ended) {
33999
34316
  const log = new GameLog;
34317
+ const connectingFrom = Date.now();
34000
34318
  try {
34001
34319
  await this.dap().connect();
34320
+ this.logDebug(`The debug adapter was connected after ${Date.now() - connectingFrom}ms`);
34002
34321
  this.dapClient?.getOutput(true);
34003
34322
  } catch (error) {
34004
34323
  return this.createErrorResponse(`The editor is connected but its debug adapter is not: ${errorMessage(error)}`, [
@@ -34006,7 +34325,7 @@ class GodotServer {
34006
34325
  "GDHARNESS_DAP_PORT points this server at another one"
34007
34326
  ]);
34008
34327
  }
34009
- const stranger = this.adapterBelongsElsewhere();
34328
+ const stranger = await this.adapterBelongsElsewhere();
34010
34329
  if (stranger !== null) {
34011
34330
  return this.createErrorResponse(stranger, [
34012
34331
  "editor_status names the editor on the bridge and the port it says it serves",
@@ -34014,7 +34333,9 @@ class GodotServer {
34014
34333
  "Closing the other editor, or moving its debug adapter port in its editor settings, frees this one"
34015
34334
  ]);
34016
34335
  }
34336
+ const sendingFrom = Date.now();
34017
34337
  const breakpoints = await this.dapHoldingBreakpointsOf(projectPath).reapplyBreakpoints();
34338
+ this.logDebug(`Breakpoints were sent again after ${Date.now() - sendingFrom}ms`);
34018
34339
  const answer = await this.handleViaBridge("play_scene", scene === null ? {} : { scenePath: `res://${scene}` });
34019
34340
  if (answer.isError === true) {
34020
34341
  return answer;
@@ -34200,13 +34521,13 @@ class GodotServer {
34200
34521
  arguments: [],
34201
34522
  ...running.command === undefined ? {} : { command: running.command }
34202
34523
  };
34203
- if (!stillTheRecordedRun(startedAs)) {
34524
+ if (!await stillTheRecordedRun(startedAs)) {
34204
34525
  clearTheRecord();
34205
34526
  running.endedHere = null;
34206
34527
  running.log.record("warning", `This run was not ended here: pid ${running.pid} no longer answers as the process the run was started as, so nothing was signalled. If that process is still the game, end it yourself; if it is not, it belongs to something else.`);
34207
34528
  return "refused";
34208
34529
  }
34209
- const signalled = runningAs(running.pid);
34530
+ const signalled = await runningAs(running.pid);
34210
34531
  const described = signalled === null ? "nothing it would name" : `${signalled.kind}: ${signalled.text}`;
34211
34532
  let landed = true;
34212
34533
  try {
@@ -34227,7 +34548,7 @@ class GodotServer {
34227
34548
  return gone ? "gone" : "lingering";
34228
34549
  }
34229
34550
  spawnGame(godotPath, cmdArgs, env) {
34230
- const child = spawn2(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
34551
+ const child = spawn3(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
34231
34552
  const cancelled = callSignal();
34232
34553
  if (cancelled !== undefined) {
34233
34554
  const end = () => {
@@ -34394,8 +34715,8 @@ class GodotServer {
34394
34715
  }
34395
34716
  return run.announcedPid;
34396
34717
  }
34397
- ownGameAmong(running, projectPath) {
34398
- const run = this.currentRun();
34718
+ async ownGameAmong(running, projectPath) {
34719
+ const run = await this.currentRun();
34399
34720
  if (run === null || !stillRunning(run)) {
34400
34721
  return;
34401
34722
  }
@@ -34406,7 +34727,7 @@ class GodotServer {
34406
34727
  return its !== undefined && running.some((one) => one.pid === its) ? its : undefined;
34407
34728
  }
34408
34729
  async ownGameThroughTheTree(running, projectPath) {
34409
- const run = this.currentRun();
34730
+ const run = await this.currentRun();
34410
34731
  if (run === null || !stillRunning(run) || run.announcedPid !== undefined) {
34411
34732
  return;
34412
34733
  }
@@ -34492,7 +34813,7 @@ class GodotServer {
34492
34813
  const editor = this.godotBridge.getStatus().editorPid;
34493
34814
  return endpoint.editorPid === undefined || endpoint.editorPid !== editor;
34494
34815
  }
34495
- adoptRecordedRun() {
34816
+ async adoptRecordedRun() {
34496
34817
  const project = this.ourProject();
34497
34818
  const record = project === null ? null : readRunRecord(project);
34498
34819
  if (record === null || !this.couldBeOurs(record.projectPath)) {
@@ -34513,7 +34834,7 @@ class GodotServer {
34513
34834
  pickedUp: true
34514
34835
  };
34515
34836
  this.drainTranscript(adopted);
34516
- if (!alive(record.pid) || !couldStillBeTheRecordedRun(record)) {
34837
+ if (!alive(record.pid) || !await couldStillBeTheRecordedRun(record)) {
34517
34838
  adopted.log.finish();
34518
34839
  adopted.exitCode = record.exitCode ?? null;
34519
34840
  adopted.exitSignal = record.exitSignal ?? null;
@@ -34594,8 +34915,8 @@ class GodotServer {
34594
34915
  const status = this.godotBridge.getStatus();
34595
34916
  return this.ownProject ?? (status.connected ? status.projectPath ?? null : null);
34596
34917
  }
34597
- currentRun() {
34598
- return this.activeProcess ?? this.adoptRecordedRun();
34918
+ async currentRun() {
34919
+ return this.activeProcess ?? await this.adoptRecordedRun();
34599
34920
  }
34600
34921
  async checkBoot(godotPath, projectPath, scene, args, userArgs, scanned, asked = {}) {
34601
34922
  const frames = readPositiveNumber(args, "frames") ?? 3;
@@ -34634,10 +34955,14 @@ class GodotServer {
34634
34955
  }
34635
34956
  async handleWaitForRun(args) {
34636
34957
  await this.pickUpWhatTheEditorIsPlaying();
34637
- const run = this.currentRun();
34958
+ const run = await this.currentRun();
34638
34959
  if (!run) {
34639
34960
  return this.createErrorResponse(this.nothingOfOursIsRunning());
34640
34961
  }
34962
+ const elsewhere = this.anotherProjectsRun(run.projectPath, args, "waited on");
34963
+ if (elsewhere !== null) {
34964
+ return elsewhere;
34965
+ }
34641
34966
  const budgetMs = readPositiveNumber(args, "timeoutMs") ?? WAIT_FOR_RUN_MS;
34642
34967
  const until = Date.now() + budgetMs;
34643
34968
  const cancelled = callSignal();
@@ -34684,7 +35009,7 @@ class GodotServer {
34684
35009
  await delay3(50);
34685
35010
  }
34686
35011
  }
34687
- handleEditorConsole(args) {
35012
+ async handleEditorConsole(args) {
34688
35013
  const status = this.godotBridge.getStatus();
34689
35014
  const projectPath = status.projectPath ?? this.ownProject ?? undefined;
34690
35015
  if (projectPath === undefined) {
@@ -34721,10 +35046,11 @@ class GodotServer {
34721
35046
  const filtered = severity !== "info" || contains !== undefined;
34722
35047
  const grouped = bursts(everything, readNonNegativeNumber(args, "before") ?? 3, filtered ? (entry) => answersTo(entry, severity, contains) : undefined);
34723
35048
  const shown = grouped.slice(0, MOST_GROUPS);
35049
+ const stillComing = !status.connected && await this.launchedEditorIsUp();
34724
35050
  return this.jsonTextResponse({
34725
35051
  editorPid: editorPid ?? undefined,
34726
35052
  projectPath,
34727
- ...status.connected ? {} : this.launchedEditorIsUp() ? { stillConnecting: true } : { exitedBeforeConnecting: true },
35053
+ ...status.connected ? {} : stillComing ? { stillConnecting: true } : { exitedBeforeConnecting: true },
34728
35054
  capturedIn: console2.path,
34729
35055
  ...console2.of === undefined ? {} : {
34730
35056
  readFromTheStart: console2.readBytes,
@@ -34748,7 +35074,7 @@ class GodotServer {
34748
35074
  return this.handleEditorConsole(args);
34749
35075
  }
34750
35076
  await this.pickUpWhatTheEditorIsPlaying();
34751
- const run = this.currentRun();
35077
+ const run = await this.currentRun();
34752
35078
  if (!run) {
34753
35079
  return this.createErrorResponse(this.nothingOfOursIsRunning());
34754
35080
  }
@@ -34787,7 +35113,7 @@ class GodotServer {
34787
35113
  notes.push(endedWithoutACode({
34788
35114
  throughEditor: run.throughEditor,
34789
35115
  announced: announced !== undefined || run.announcedPid !== undefined,
34790
- couldAnnounce: run.projectPath !== null && existsSync11(join17(run.projectPath, RUNTIME_AUTOLOAD.path)),
35116
+ couldAnnounce: run.projectPath !== null && existsSync12(join18(run.projectPath, RUNTIME_AUTOLOAD.path)),
34791
35117
  endedHere: typeof run.endedHere === "string",
34792
35118
  pickedUp: run.pickedUp === true
34793
35119
  }));
@@ -34825,16 +35151,27 @@ class GodotServer {
34825
35151
  entries: forAnswer(selected.entries)
34826
35152
  });
34827
35153
  }
35154
+ anotherProjectsRun(runsProject, args, act) {
35155
+ const named = readNonEmptyString(args, "projectPath");
35156
+ if (named === undefined || runsProject === null || isSameDirectory(runsProject, named)) {
35157
+ return null;
35158
+ }
35159
+ return this.createErrorResponse(`The run this server holds is of ${runsProject}, not ${named}, so nothing was ${act}. A run of ${named} is held by the server that started it, in that project's own session.`);
35160
+ }
34828
35161
  async handleStopProject(args) {
34829
35162
  await this.pickUpWhatTheEditorIsPlaying();
34830
- const stopped = this.currentRun();
35163
+ const stopped = await this.currentRun();
34831
35164
  const named = readNumber(args, "pid");
34832
35165
  if (named !== undefined && (stopped === null || named !== stopped.pid && named !== this.announcedPidOf(stopped))) {
34833
- return await this.stopAnnouncedGame(named, readBoolean(args, "andChildren") === true);
35166
+ return await this.stopAnnouncedGame(named, readBoolean(args, "andChildren") === true, args);
34834
35167
  }
34835
35168
  if (!stopped) {
34836
35169
  return this.createErrorResponse(this.nothingOfOursIsRunning());
34837
35170
  }
35171
+ const elsewhere = this.anotherProjectsRun(stopped.projectPath, args, "stopped");
35172
+ if (elsewhere !== null) {
35173
+ return elsewhere;
35174
+ }
34838
35175
  this.drainEditorOutput(stopped);
34839
35176
  this.drainTranscript(stopped);
34840
35177
  const wasRunning = await this.runStillGoing(stopped);
@@ -34876,16 +35213,22 @@ class GodotServer {
34876
35213
  entries: forAnswer(stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
34877
35214
  });
34878
35215
  }
34879
- async stopAnnouncedGame(pid, andChildren) {
35216
+ async stopAnnouncedGame(pid, andChildren, args) {
34880
35217
  const announced = runtimesAnnounced();
34881
35218
  const game = [
34882
35219
  ...this.allAnnouncedForOurProject(announced.running),
34883
35220
  ...this.allAnnouncedForOurProject(announced.unspoken)
34884
35221
  ].find((one) => one.pid === pid);
35222
+ if (game !== undefined) {
35223
+ const elsewhere = this.anotherProjectsRun(game.project.path, args, "signalled");
35224
+ if (elsewhere !== null) {
35225
+ return elsewhere;
35226
+ }
35227
+ }
34885
35228
  if (game === undefined) {
34886
35229
  return this.createErrorResponse(`pid ${pid} is not a game announced for this project, so nothing was signalled. editor_status lists this project's games under runtimes. A game that announces no runtime is ended by the server holding its run, or with andChildren by a stop of the run that started it.`);
34887
35230
  }
34888
- const read = game.announcedAt === undefined ? null : stillTheAnnouncedGame(pid, game.project.path, game.announcedAt);
35231
+ const read = game.announcedAt === undefined ? null : await stillTheAnnouncedGame(pid, game.project.path, game.announcedAt);
34889
35232
  if (read === null) {
34890
35233
  return this.createErrorResponse(`pid ${pid} announced itself as a game of this project, but the operating system does not describe it as this project's engine run started before that announcement (the engine with --path on the project and no editor flag), so nothing was signalled. If it is still the game, runtime_invoke can ask it to quit.`);
34891
35234
  }
@@ -35083,6 +35426,8 @@ class GodotServer {
35083
35426
  }
35084
35427
  const checked = busy ? { unseen: [] } : await this.classesTheEditorCannotSee(args);
35085
35428
  const unseen = checked.unseen;
35429
+ const broughtIn = blind.unseen.map((one) => one.className).filter((name) => !unseen.some((still) => still.className === name));
35430
+ const dependents = busy || projectPath === "" || broughtIn.length === 0 ? null : await this.reloadDependents(projectPath, broughtIn);
35086
35431
  const stillGone = lost.filter((name) => !restored.includes(name));
35087
35432
  const notes = [];
35088
35433
  if (busy) {
@@ -35094,13 +35439,16 @@ class GodotServer {
35094
35439
  } else if (unseen.length > 0) {
35095
35440
  notes.push("The scan finished and these classes are still not in the list the editor resolves against, so every use of them reads as an unknown identifier. Its walk skips a file another engine has already imported. Rescan again on its own, which is the measured cure and needs no change to the declaring script, or restart the editor with editor_launch restart.");
35096
35441
  }
35442
+ if (dependents !== null && dependents.notReloaded.length > 0) {
35443
+ notes.push(`The scan brought in ${broughtIn.join(", ")}, and ${dependents.notReloaded.map((one) => one.scriptPath).join(", ")} ${dependents.notReloaded.length === 1 ? "names one of them and" : "name them and"} could not be reloaded, so the editor still holds what it compiled while it could not see the class and reports that to the language server. editor_rescan with reloadScript on each, or editor_launch restart.`);
35444
+ }
35097
35445
  if (stillAtMissingPaths.length > 0) {
35098
35446
  notes.push(`The scan wrote the class cache with ${stillAtMissingPaths.join(", ")} at a path that is not on disk, and rebuilding the cache from the files did not take it out. The next engine to read the cache fails on "Could not parse global class" in whichever correct script shares the bare name. Restart the editor with editor_launch restart, then run project_import refresh_classes.`);
35099
35447
  } else if (dropped.length > 0) {
35100
35448
  notes.push(`The scan wrote the class cache from the list this editor is holding, and that list still has ${dropped.join(", ")} for a script that is gone, so the cache was rebuilt from the files without it. The editor goes on holding it and writes it back on every scan until it is restarted with editor_launch restart; until then a scan is followed by this same rebuild.`);
35101
35449
  }
35102
35450
  return this.jsonTextResponse({
35103
- ok: !busy && unseen.length === 0 && stillGone.length === 0 && stillAtMissingPaths.length === 0 && checked.unchecked === undefined,
35451
+ ok: !busy && unseen.length === 0 && stillGone.length === 0 && stillAtMissingPaths.length === 0 && (dependents === null || dependents.notReloaded.length === 0) && checked.unchecked === undefined,
35104
35452
  stillWorking: busy,
35105
35453
  waitedMs: Date.now() - started,
35106
35454
  waitedForEditorScanMs: waitedForEditorMs > 0 ? waitedForEditorMs : undefined,
@@ -35108,6 +35456,9 @@ class GodotServer {
35108
35456
  reloadedMethods: reloaded.methods,
35109
35457
  reloadProblem: reloaded.problem,
35110
35458
  unseenByEditor: unseen.length > 0 ? unseen : undefined,
35459
+ broughtIn: broughtIn.length > 0 ? broughtIn : undefined,
35460
+ dependentsReloaded: dependents !== null && dependents.reloaded.length > 0 ? dependents.reloaded : undefined,
35461
+ dependentsNotReloaded: dependents !== null && dependents.notReloaded.length > 0 ? dependents.notReloaded : undefined,
35111
35462
  cacheLost: lost.length > 0 ? lost : undefined,
35112
35463
  cacheRestored: restored.length > 0 ? restored : undefined,
35113
35464
  cacheDropped: dropped.length > 0 ? dropped : undefined,
@@ -35116,6 +35467,33 @@ class GodotServer {
35116
35467
  note: notes.length > 0 ? notes.join(" ") : undefined
35117
35468
  });
35118
35469
  }
35470
+ async reloadDependents(projectPath, classes) {
35471
+ const reloaded = [];
35472
+ const notReloaded = [];
35473
+ for (const { script: scriptPath, tool } of scriptsNaming(projectPath, classes)) {
35474
+ if (tool) {
35475
+ notReloaded.push({
35476
+ scriptPath,
35477
+ problem: "a @tool script, which runs inside the editor, so it is not reloaded from here; editor_launch restart recompiles it"
35478
+ });
35479
+ continue;
35480
+ }
35481
+ try {
35482
+ const answer = asParams(await this.godotBridge.invokeTool("reload_script", { scriptPath }));
35483
+ if (readArray(answer, "methods") === undefined) {
35484
+ notReloaded.push({
35485
+ scriptPath,
35486
+ problem: "the editor would not say what it has after reloading it"
35487
+ });
35488
+ } else {
35489
+ reloaded.push(scriptPath);
35490
+ }
35491
+ } catch (error) {
35492
+ notReloaded.push({ scriptPath, problem: errorMessage(error) });
35493
+ }
35494
+ }
35495
+ return { reloaded, notReloaded };
35496
+ }
35119
35497
  async rebuildCacheAfterLoss(projectPath, lost) {
35120
35498
  if (projectPath === "") {
35121
35499
  return [];
@@ -35298,9 +35676,9 @@ class GodotServer {
35298
35676
  return { heldAt: undefined, heldUnknown: true, heldNote: reply.message };
35299
35677
  }
35300
35678
  async ownRunOnItsWay(projectPath, budgetMs) {
35301
- const run = this.currentRun();
35679
+ const run = await this.currentRun();
35302
35680
  const project = projectPath ?? run?.projectPath ?? null;
35303
- if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync11(join17(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
35681
+ if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync12(join18(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
35304
35682
  return null;
35305
35683
  }
35306
35684
  const already = runtimesAnnounced().running;
@@ -35326,8 +35704,8 @@ class GodotServer {
35326
35704
  }
35327
35705
  const endpoint = await announcedSince(project, new Set(already.map((one) => one.pid)), {
35328
35706
  budgetMs: Math.min(budgetMs, remaining),
35329
- accept: (one) => this.isTheGameOf(this.currentRun(), one, project),
35330
- giveUp: () => this.currentRun() !== run || this.dapClient?.isStopped() === true || !stillRunning(run)
35707
+ accept: (one) => this.isTheGameOf(this.activeProcess, one, project),
35708
+ giveUp: () => this.activeProcess !== run || this.dapClient?.isStopped() === true || !stillRunning(run)
35331
35709
  });
35332
35710
  if (endpoint !== null) {
35333
35711
  const tiedMeanwhile = () => run.announcedPid !== undefined;
@@ -35336,7 +35714,7 @@ class GodotServer {
35336
35714
  }
35337
35715
  return null;
35338
35716
  }
35339
- if (this.currentRun() !== run || !await this.runStillGoing(run)) {
35717
+ if (await this.currentRun() !== run || !await this.runStillGoing(run)) {
35340
35718
  return null;
35341
35719
  }
35342
35720
  return `The game this server started (${which}) is still coming up and has not announced its runtime after a wait of ${Math.min(budgetMs, remaining)}ms, so there is nothing to talk to yet. It is still running: ask again in a moment rather than starting another.`;
@@ -35352,7 +35730,7 @@ class GodotServer {
35352
35730
  }
35353
35731
  const announced = runtimesAnnounced();
35354
35732
  const wanted = typeof projectPath === "string" ? projectPath : this.ownProject ?? undefined;
35355
- const own = typeof pid === "number" ? undefined : this.ownGameAmong(announced.running, wanted) ?? await this.ownGameThroughTheTree(announced.running, wanted);
35733
+ const own = typeof pid === "number" ? undefined : await this.ownGameAmong(announced.running, wanted) ?? await this.ownGameThroughTheTree(announced.running, wanted);
35356
35734
  const choice = chooseRuntime(announced.running, wanted, announced.unspoken, typeof pid === "number" ? pid : own);
35357
35735
  if ("problem" in choice) {
35358
35736
  return this.createErrorResponse(choice.problem);
@@ -35379,8 +35757,8 @@ class GodotServer {
35379
35757
  return this.createErrorResponse(refusal);
35380
35758
  }
35381
35759
  }
35382
- const screenshotDir = expectsScreenshot ? mkdtempSync2(join17(tmpdir4(), "gdharness-runtime-screenshot-")) : null;
35383
- const screenshotPath = screenshotDir ? join17(screenshotDir, "capture.png") : null;
35760
+ const screenshotDir = expectsScreenshot ? scratchDirectory("runtime-screenshot") : null;
35761
+ const screenshotPath = screenshotDir ? join18(screenshotDir, "capture.png") : null;
35384
35762
  try {
35385
35763
  const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs, !expectsScreenshot);
35386
35764
  if (!reply.ok && reply.requestId !== undefined) {