gdharness 1.1.12 → 1.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/cli.js CHANGED
@@ -68,11 +68,156 @@ var init_errors = __esm(() => {
68
68
  };
69
69
  });
70
70
 
71
+ // src/windows-ask.ts
72
+ import { spawn } from "node:child_process";
73
+ import process2 from "node:process";
74
+ function holding(child, wanted) {
75
+ if (held === wanted) {
76
+ return;
77
+ }
78
+ held = wanted;
79
+ const stdout = child.stdout;
80
+ if (wanted) {
81
+ stdout.ref?.();
82
+ } else {
83
+ stdout.unref?.();
84
+ }
85
+ }
86
+ function running() {
87
+ if (helper?.exitCode === null && helper.signalCode === null) {
88
+ return helper;
89
+ }
90
+ const child = spawn("powershell.exe", [
91
+ "-NoProfile",
92
+ "-NonInteractive",
93
+ "-ExecutionPolicy",
94
+ "Bypass",
95
+ "-EncodedCommand",
96
+ Buffer.from(SCRIPT, "utf16le").toString("base64")
97
+ ], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
98
+ buffered = "";
99
+ child.stdout.setEncoding("utf8");
100
+ child.stdout.on("data", (chunk) => {
101
+ buffered += chunk;
102
+ for (let end = buffered.indexOf(`
103
+ `);end !== -1; end = buffered.indexOf(`
104
+ `)) {
105
+ answered(buffered.slice(0, end).trim());
106
+ buffered = buffered.slice(end + 1);
107
+ }
108
+ });
109
+ child.stderr.resume();
110
+ child.stdin.on("error", () => {});
111
+ const gone = () => {
112
+ if (helper !== child) {
113
+ return;
114
+ }
115
+ helper = null;
116
+ if (out !== null) {
117
+ clearTimeout(out.timer);
118
+ out.question.reject(new WindowsAskFailure("exited before it answered"));
119
+ out = null;
120
+ }
121
+ sendNext();
122
+ };
123
+ child.once("exit", gone);
124
+ child.once("error", gone);
125
+ child.unref();
126
+ child.stdin.unref?.();
127
+ child.stderr.unref?.();
128
+ held = true;
129
+ holding(child, false);
130
+ helper = child;
131
+ return child;
132
+ }
133
+ function answered(line) {
134
+ const [id, tag, body] = line.split(" ");
135
+ if (out === null || out.id !== Number(id)) {
136
+ return;
137
+ }
138
+ const { question, timer } = out;
139
+ clearTimeout(timer);
140
+ out = null;
141
+ const said = Buffer.from(body ?? "", "base64").toString("utf8");
142
+ if (tag === "OK") {
143
+ question.resolve(said);
144
+ } else {
145
+ question.reject(new WindowsAskFailure(`refused the question: ${said.trim() || "no reason given"}`));
146
+ }
147
+ sendNext();
148
+ }
149
+ function sendNext() {
150
+ const question = queued.shift();
151
+ if (question === undefined) {
152
+ if (helper !== null) {
153
+ holding(helper, false);
154
+ }
155
+ return;
156
+ }
157
+ const child = running();
158
+ holding(child, true);
159
+ const id = next;
160
+ next += 1;
161
+ const timer = setTimeout(() => {
162
+ if (out?.id !== id) {
163
+ return;
164
+ }
165
+ out = null;
166
+ question.reject(new WindowsAskFailure(`did not answer within ${question.withinMs}ms`));
167
+ if (helper === child) {
168
+ helper = null;
169
+ }
170
+ child.kill();
171
+ sendNext();
172
+ }, question.withinMs);
173
+ out = { id, question, timer };
174
+ child.stdin.write(`${id} ${Buffer.from(question.script, "utf8").toString("base64")}
175
+ `);
176
+ }
177
+ function askWindows(script, withinMs) {
178
+ if (process2.platform !== "win32") {
179
+ return Promise.reject(new WindowsAskFailure("is asked only on Windows"));
180
+ }
181
+ return new Promise((resolve, reject) => {
182
+ queued.push({ script, withinMs, resolve, reject });
183
+ if (out === null) {
184
+ sendNext();
185
+ }
186
+ });
187
+ }
188
+ var SCRIPT, WindowsAskFailure, helper = null, buffered = "", next = 1, out = null, queued, held = false;
189
+ var init_windows_ask = __esm(() => {
190
+ SCRIPT = [
191
+ "[Console]::OutputEncoding = [Text.Encoding]::UTF8",
192
+ "$in = [Console]::In",
193
+ "while ($null -ne ($line = $in.ReadLine())) {",
194
+ ' $parts = $line.Split(" ", 2)',
195
+ " try {",
196
+ " $asked = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($parts[1]))",
197
+ " $said = (& ([ScriptBlock]::Create($asked)) | Out-String -Width 1000000)",
198
+ ' $tag = "OK"',
199
+ " } catch {",
200
+ " $said = $_.Exception.Message",
201
+ ' $tag = "ERR"',
202
+ " }",
203
+ ' [Console]::Out.Write($parts[0] + " " + $tag + " " + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($said)) + "`n")',
204
+ " [Console]::Out.Flush()",
205
+ "}"
206
+ ].join(`
207
+ `);
208
+ WindowsAskFailure = class WindowsAskFailure extends Error {
209
+ constructor(how) {
210
+ super(`PowerShell ${how}`);
211
+ }
212
+ };
213
+ queued = [];
214
+ });
215
+
71
216
  // src/process-children.ts
72
- import { execFile, execFileSync, spawnSync } from "node:child_process";
217
+ import { execFile } from "node:child_process";
73
218
  import { basename } from "node:path";
74
219
  import { promisify } from "node:util";
75
- function startTimesOf(pids) {
220
+ async function startTimesOf(pids) {
76
221
  const began = new Map;
77
222
  if (pids.length === 0) {
78
223
  return began;
@@ -80,12 +225,7 @@ function startTimesOf(pids) {
80
225
  try {
81
226
  if (process.platform === "win32") {
82
227
  const filter = pids.map((pid) => `ProcessId=${pid}`).join(" OR ");
83
- const said = execFileSync("powershell", [
84
- "-NoProfile",
85
- "-NonInteractive",
86
- "-Command",
87
- `Get-CimInstance Win32_Process -Filter "${filter}" | ForEach-Object { "$($_.ProcessId) $(([DateTimeOffset]$_.CreationDate).ToUnixTimeMilliseconds())" }`
88
- ], { encoding: "utf8", timeout: ASK_TIMEOUT_MS, windowsHide: true });
228
+ const said = await askWindows(`Get-CimInstance Win32_Process -Filter "${filter}" | ForEach-Object { "$($_.ProcessId) $(([DateTimeOffset]$_.CreationDate).ToUnixTimeMilliseconds())" }`, ASK_TIMEOUT_MS);
89
229
  for (const line of said.split(`
90
230
  `)) {
91
231
  const found = /^\s*(\d+)\s+(\d+)\s*$/.exec(line);
@@ -95,11 +235,10 @@ function startTimesOf(pids) {
95
235
  }
96
236
  return began;
97
237
  }
98
- const said = spawnSync("ps", ["-p", pids.join(","), "-o", "pid=,lstart="], {
99
- encoding: "utf8",
100
- timeout: ASK_TIMEOUT_MS,
101
- env: { ...process.env, LC_ALL: "C" }
102
- }).stdout;
238
+ const said = await printedBy("ps", ["-p", pids.join(","), "-o", "pid=,lstart="], {
239
+ ...process.env,
240
+ LC_ALL: "C"
241
+ });
103
242
  for (const line of said.split(`
104
243
  `)) {
105
244
  const found = /^\s*(\d+)\s+(.+)$/.exec(line);
@@ -111,16 +250,39 @@ function startTimesOf(pids) {
111
250
  } catch {}
112
251
  return began;
113
252
  }
253
+ function printedBy(command, args, env) {
254
+ return new Promise((resolve) => {
255
+ execFile(command, [...args], { encoding: "utf8", timeout: ASK_TIMEOUT_MS, env }, (_error, stdout) => {
256
+ resolve(stdout);
257
+ });
258
+ });
259
+ }
114
260
  async function processTree() {
115
261
  try {
116
- const tree = process.platform === "win32" ? parseProcessTable(await askWindows(), true) : parseProcessTable(await askPosix());
262
+ const tree = process.platform === "win32" ? parseProcessTable(await listWindows(), true) : parseProcessTable(await askPosix());
117
263
  lastListingFailure = undefined;
118
264
  return tree;
119
265
  } catch (error) {
120
- lastListingFailure = error instanceof Error ? error.message : String(error);
266
+ lastListingFailure = listingFailure(error);
121
267
  return;
122
268
  }
123
269
  }
270
+ function listingFailure(error) {
271
+ if (error instanceof WindowsAskFailure) {
272
+ return error.message;
273
+ }
274
+ if (!(error instanceof Error)) {
275
+ return String(error);
276
+ }
277
+ const failed = error;
278
+ 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";
279
+ const said = typeof failed.stderr === "string" ? failed.stderr.trim() : "";
280
+ const asked = basename((failed.cmd ?? "").split(" ")[0] ?? "") || "the listing";
281
+ return `${asked} ${how}${said === "" ? ", with nothing on stderr" : `; stderr: ${said.slice(0, 400)}`}`;
282
+ }
283
+ function whyTheProcessTreeFailed() {
284
+ return lastListingFailure;
285
+ }
124
286
  function parseProcessTable(printed, started = false) {
125
287
  const tree = new Map;
126
288
  for (const line of printed.split(/\r?\n/)) {
@@ -250,14 +412,8 @@ function commandWords(command) {
250
412
  }
251
413
  return words;
252
414
  }
253
- async function askWindows() {
254
- const { stdout } = await run("powershell.exe", [
255
- "-NoProfile",
256
- "-NonInteractive",
257
- "-Command",
258
- `${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)" }`
259
- ], { timeout: ASK_TIMEOUT_MS, windowsHide: true, maxBuffer: 16 * 1024 * 1024 });
260
- return stdout;
415
+ function listWindows() {
416
+ 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);
261
417
  }
262
418
  async function askPosix() {
263
419
  const { stdout } = await run("ps", ["-e", "-o", "pid=,ppid=,args="], {
@@ -266,8 +422,9 @@ async function askPosix() {
266
422
  });
267
423
  return stdout;
268
424
  }
269
- var run, POWERSHELL_UTF8 = "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; ", ASK_TIMEOUT_MS = 15000, lastListingFailure;
425
+ var run, ASK_TIMEOUT_MS = 15000, lastListingFailure;
270
426
  var init_process_children = __esm(() => {
427
+ init_windows_ask();
271
428
  run = promisify(execFile);
272
429
  });
273
430
 
@@ -302,12 +459,12 @@ function serverBehind(announcement, startedAt) {
302
459
  }
303
460
  return { version: announcement.version, pid: announcement.pid };
304
461
  }
305
- function serverServing(projectPath) {
462
+ async function serverServing(projectPath) {
306
463
  const announcement = readAnnouncement(announcementPath(projectPath));
307
464
  if (announcement === null) {
308
465
  return null;
309
466
  }
310
- return serverBehind(announcement, startTimesOf([announcement.pid]).get(announcement.pid));
467
+ return serverBehind(announcement, (await startTimesOf([announcement.pid])).get(announcement.pid));
311
468
  }
312
469
  function announcementIsOurs(path) {
313
470
  return readAnnouncement(path)?.pid === process.pid;
@@ -7520,16 +7677,16 @@ var require_public_api = __commonJS(function(exports) {
7520
7677
  // src/runner.ts
7521
7678
  import { existsSync as existsSync3 } from "node:fs";
7522
7679
  import { dirname as dirname2, join as join3 } from "node:path";
7523
- import process2 from "node:process";
7680
+ import process3 from "node:process";
7524
7681
  function currentRunner() {
7525
- const versions = process2.versions;
7682
+ const versions = process3.versions;
7526
7683
  return versions["bun"] === undefined ? "npx" : "bunx";
7527
7684
  }
7528
7685
  function runLine(runner, version, rest = "") {
7529
7686
  const flag = runner === "npx" ? "-y " : "";
7530
7687
  return `${runner} ${flag}gdharness@${version}${rest === "" ? "" : ` ${rest}`}`;
7531
7688
  }
7532
- function spawnFor(runner, version, execPath = process2.execPath) {
7689
+ function spawnFor(runner, version, execPath = process3.execPath) {
7533
7690
  const spec = `gdharness@${version}`;
7534
7691
  if (runner !== currentRunner()) {
7535
7692
  return { command: runner, args: runner === "npx" ? ["-y", spec] : ["--bun", spec] };
@@ -7540,7 +7697,7 @@ function spawnFor(runner, version, execPath = process2.execPath) {
7540
7697
  return { command: alongside(execPath, "npx") ?? "npx", args: ["-y", spec] };
7541
7698
  }
7542
7699
  function alongside(execPath, name) {
7543
- const runner = join3(dirname2(execPath), process2.platform === "win32" ? `${name}.cmd` : name);
7700
+ const runner = join3(dirname2(execPath), process3.platform === "win32" ? `${name}.cmd` : name);
7544
7701
  return existsSync3(runner) ? runner : undefined;
7545
7702
  }
7546
7703
  var init_runner = () => {};
@@ -7683,8 +7840,47 @@ var init_game_log = __esm(() => {
7683
7840
  };
7684
7841
  });
7685
7842
 
7843
+ // src/alive.ts
7844
+ import process5 from "node:process";
7845
+ function alive(pid) {
7846
+ if (pid === undefined || pid === null) {
7847
+ return false;
7848
+ }
7849
+ try {
7850
+ process5.kill(pid, 0);
7851
+ return true;
7852
+ } catch (error) {
7853
+ return error.code === "EPERM";
7854
+ }
7855
+ }
7856
+ var init_alive = () => {};
7857
+
7686
7858
  // src/scratch.ts
7687
- import { rmSync as rmSync3 } from "node:fs";
7859
+ import { existsSync as existsSync5, mkdtempSync, readdirSync as readdirSync2, rmSync as rmSync3 } from "node:fs";
7860
+ import { tmpdir } from "node:os";
7861
+ import { join as join5 } from "node:path";
7862
+ import process6 from "node:process";
7863
+ function scratchDirectory(kind) {
7864
+ return mkdtempSync(join5(tmpdir(), `gdharness-${kind}-${process6.pid}-`));
7865
+ }
7866
+ function sweepAbandonedScratch(directory = tmpdir()) {
7867
+ let entries;
7868
+ try {
7869
+ entries = readdirSync2(directory);
7870
+ } catch {
7871
+ return;
7872
+ }
7873
+ const named = new RegExp(`^gdharness-(${KINDS.join("|")})-(\\d+)-[A-Za-z0-9]{6}$`);
7874
+ for (const entry of entries) {
7875
+ const owner = named.exec(entry)?.[2];
7876
+ if (owner === undefined || alive(Number(owner))) {
7877
+ continue;
7878
+ }
7879
+ try {
7880
+ remove(join5(directory, entry));
7881
+ } catch {}
7882
+ }
7883
+ }
7688
7884
  function discardWith(remove, paths) {
7689
7885
  const left = [];
7690
7886
  for (const path of paths) {
@@ -7700,20 +7896,75 @@ function discardWith(remove, paths) {
7700
7896
  return left;
7701
7897
  }
7702
7898
  function discard(...paths) {
7703
- return discardWith((path) => {
7704
- rmSync3(path, SWEPT);
7705
- }, paths);
7899
+ const left = discardWith(remove, paths);
7900
+ for (const { path } of left) {
7901
+ tryAgain(path, 0);
7902
+ }
7903
+ return left;
7904
+ }
7905
+ function remove(path) {
7906
+ rmSync3(path, SWEPT);
7907
+ }
7908
+ function tryAgain(path, attempt) {
7909
+ if (attempt === 0) {
7910
+ if (waiting.has(path)) {
7911
+ return;
7912
+ }
7913
+ waiting.add(path);
7914
+ atExit.delete(path);
7915
+ lastTryOnExit();
7916
+ }
7917
+ const after = AGAIN_AFTER_MS[attempt];
7918
+ if (after === undefined || !existsSync5(path)) {
7919
+ waiting.delete(path);
7920
+ if (after === undefined) {
7921
+ atExit.add(path);
7922
+ }
7923
+ return;
7924
+ }
7925
+ setTimeout(() => {
7926
+ try {
7927
+ remove(path);
7928
+ waiting.delete(path);
7929
+ } catch {
7930
+ tryAgain(path, attempt + 1);
7931
+ }
7932
+ }, after).unref();
7706
7933
  }
7707
- var SWEPT;
7934
+ function lastTryOnExit() {
7935
+ if (lastTryWatched) {
7936
+ return;
7937
+ }
7938
+ lastTryWatched = true;
7939
+ process6.once("exit", () => {
7940
+ for (const path of [...waiting, ...atExit]) {
7941
+ try {
7942
+ remove(path);
7943
+ } catch {}
7944
+ }
7945
+ });
7946
+ }
7947
+ var SWEPT, AGAIN_AFTER_MS, waiting, atExit, KINDS, lastTryWatched = false;
7708
7948
  var init_scratch = __esm(() => {
7709
- SWEPT = { recursive: true, force: true, maxRetries: 20, retryDelay: 250 };
7949
+ init_alive();
7950
+ SWEPT = { recursive: true, force: true };
7951
+ AGAIN_AFTER_MS = [250, 500, 1000, 2000, 4000, 8000, 16000];
7952
+ waiting = new Set;
7953
+ atExit = new Set;
7954
+ KINDS = [
7955
+ "desktop",
7956
+ "export",
7957
+ "import",
7958
+ "params",
7959
+ "runtime-screenshot",
7960
+ "tests"
7961
+ ];
7710
7962
  });
7711
7963
 
7712
7964
  // src/headless.ts
7713
7965
  import { execFile as execFile3 } from "node:child_process";
7714
- import { mkdtempSync, writeFileSync as writeFileSync3 } from "node:fs";
7715
- import { tmpdir } from "node:os";
7716
- import { join as join5 } from "node:path";
7966
+ import { writeFileSync as writeFileSync3 } from "node:fs";
7967
+ import { join as join6 } from "node:path";
7717
7968
  import { promisify as promisify3 } from "node:util";
7718
7969
  function snakeCased(params) {
7719
7970
  const result = emptyRecord();
@@ -7753,9 +8004,9 @@ function reason(stdout, stderr) {
7753
8004
  return stdout.trim().split(/\r?\n/).at(-1) ?? "no output at all";
7754
8005
  }
7755
8006
  async function runImport(godotPath, projectPath) {
7756
- const logDir = mkdtempSync(join5(tmpdir(), "gdharness-import-"));
8007
+ const logDir = scratchDirectory("import");
7757
8008
  try {
7758
- const { stderr } = await run3(godotPath, ["--headless", "--log-file", join5(logDir, "engine.log"), "--path", projectPath, "--import"], { signal: callSignal() });
8009
+ const { stderr } = await run3(godotPath, ["--headless", "--log-file", join6(logDir, "engine.log"), "--path", projectPath, "--import"], { signal: callSignal() });
7759
8010
  return { ok: true, messages: problems(stderr) };
7760
8011
  } catch (error) {
7761
8012
  if (error instanceof Error && "stdout" in error && "stderr" in error) {
@@ -7776,13 +8027,13 @@ async function runImport(godotPath, projectPath) {
7776
8027
  }
7777
8028
  }
7778
8029
  async function runOperation(engine, operation, params, projectPath) {
7779
- const paramsDir = mkdtempSync(join5(tmpdir(), "gdharness-params-"));
7780
- const paramsFile = join5(paramsDir, `${operation}.json`);
8030
+ const paramsDir = scratchDirectory("params");
8031
+ const paramsFile = join6(paramsDir, `${operation}.json`);
7781
8032
  writeFileSync3(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
7782
8033
  const args = [
7783
8034
  "--headless",
7784
8035
  "--log-file",
7785
- join5(paramsDir, "engine.log"),
8036
+ join6(paramsDir, "engine.log"),
7786
8037
  "--path",
7787
8038
  projectPath,
7788
8039
  "--script",
@@ -7831,9 +8082,9 @@ var init_headless = __esm(() => {
7831
8082
  });
7832
8083
 
7833
8084
  // src/update-check.ts
7834
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "node:fs";
8085
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "node:fs";
7835
8086
  import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
7836
- import { join as join6 } from "node:path";
8087
+ import { join as join7 } from "node:path";
7837
8088
  function packageUrl(root) {
7838
8089
  return `${root.replace(/\/+$/, "")}/gdharness/latest`;
7839
8090
  }
@@ -7855,25 +8106,25 @@ function cacheDirectory(environment) {
7855
8106
  };
7856
8107
  const home = set("HOME") ?? homedir3();
7857
8108
  if (process.platform === "win32") {
7858
- return join6(set("LOCALAPPDATA") ?? home, "gdharness");
8109
+ return join7(set("LOCALAPPDATA") ?? home, "gdharness");
7859
8110
  }
7860
8111
  if (process.platform === "darwin") {
7861
- return join6(home, "Library", "Caches", "gdharness");
8112
+ return join7(home, "Library", "Caches", "gdharness");
7862
8113
  }
7863
- return join6(set("XDG_CACHE_HOME") ?? join6(home, ".cache"), "gdharness");
8114
+ return join7(set("XDG_CACHE_HOME") ?? join7(home, ".cache"), "gdharness");
7864
8115
  }
7865
8116
  function cacheFile(environment = process.env) {
7866
8117
  try {
7867
8118
  const directory = cacheDirectory(environment);
7868
8119
  mkdirSync3(directory, { recursive: true, mode: 448 });
7869
- return join6(directory, "update-check.json");
8120
+ return join7(directory, "update-check.json");
7870
8121
  } catch {
7871
- return join6(tmpdir2(), "gdharness-update-check.json");
8122
+ return join7(tmpdir2(), "gdharness-update-check.json");
7872
8123
  }
7873
8124
  }
7874
8125
  function readCache(path) {
7875
8126
  try {
7876
- if (!existsSync5(path)) {
8127
+ if (!existsSync6(path)) {
7877
8128
  return null;
7878
8129
  }
7879
8130
  const parsed = JSON.parse(readFileSync3(path, "utf8"));
@@ -8076,19 +8327,20 @@ var init_server_version = __esm(() => {
8076
8327
 
8077
8328
  // src/issues.ts
8078
8329
  import { createHash } from "node:crypto";
8079
- import process4 from "node:process";
8080
- function feedbackNotice() {
8330
+ import process7 from "node:process";
8331
+ function feedbackNotice(every) {
8081
8332
  return JSON.stringify({
8082
8333
  gdharness_feedback: {
8334
+ 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.`,
8083
8335
  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.",
8084
8336
  enhancement_url: ENHANCEMENT_URL
8085
8337
  }
8086
8338
  }, null, 2);
8087
8339
  }
8088
8340
  function runtime() {
8089
- const versions = process4.versions;
8341
+ const versions = process7.versions;
8090
8342
  const bun = versions["bun"];
8091
- return bun === undefined ? `node ${process4.versions.node}` : `bun ${bun}`;
8343
+ return bun === undefined ? `node ${process7.versions.node}` : `bun ${bun}`;
8092
8344
  }
8093
8345
  function defectSignature(where, message) {
8094
8346
  const normalised = `${where}
@@ -8100,7 +8352,7 @@ function facts(where, message, godotVersion) {
8100
8352
  ["Where", where],
8101
8353
  ["Error", message],
8102
8354
  ["Version", `gdharness ${SERVER_VERSION}`],
8103
- ["Runtime", `${runtime()}, ${process4.platform} ${process4.arch}`],
8355
+ ["Runtime", `${runtime()}, ${process7.platform} ${process7.arch}`],
8104
8356
  ["Godot", godotVersion ?? "not known at the point this failed"],
8105
8357
  ["Signature", defectSignature(where, message)]
8106
8358
  ];
@@ -8110,7 +8362,7 @@ function filledTemplate(where, message, godotVersion) {
8110
8362
  `**gdharness version**: ${SERVER_VERSION}`,
8111
8363
  `**Godot version**: ${godotVersion ?? ""}`,
8112
8364
  `**Bun version**: ${runtime()}`,
8113
- `**OS**: ${process4.platform} ${process4.arch}`,
8365
+ `**OS**: ${process7.platform} ${process7.arch}`,
8114
8366
  "**MCP client**:",
8115
8367
  "",
8116
8368
  "## What you did",
@@ -8199,35 +8451,82 @@ var init_issues = __esm(() => {
8199
8451
  });
8200
8452
 
8201
8453
  // src/class-cache.ts
8202
- import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync5, statSync as statSync2 } from "node:fs";
8203
- import { join as join7 } from "node:path";
8454
+ import { existsSync as existsSync7, readdirSync as readdirSync3, readFileSync as readFileSync5, statSync as statSync2 } from "node:fs";
8455
+ import { join as join8 } from "node:path";
8204
8456
  function declaredClasses(projectPath) {
8205
8457
  const declared = new Map;
8458
+ eachScript(projectPath, (script, source) => {
8459
+ const found = DECLARATION.exec(source);
8460
+ if (found?.[1]) {
8461
+ declared.set(found[1], script);
8462
+ }
8463
+ });
8464
+ return declared;
8465
+ }
8466
+ function eachScript(projectPath, each) {
8206
8467
  const visit = (directory, prefix) => {
8207
- if (existsSync6(join7(directory, ".gdignore"))) {
8468
+ if (existsSync7(join8(directory, ".gdignore"))) {
8208
8469
  return;
8209
8470
  }
8210
- for (const entry of readdirSync2(directory, { withFileTypes: true })) {
8471
+ for (const entry of readdirSync3(directory, { withFileTypes: true })) {
8211
8472
  if (entry.name.startsWith(".")) {
8212
8473
  continue;
8213
8474
  }
8214
- const path = join7(directory, entry.name);
8475
+ const path = join8(directory, entry.name);
8215
8476
  if (entry.isDirectory()) {
8216
8477
  visit(path, `${prefix}${entry.name}/`);
8217
8478
  } else if (entry.isFile() && entry.name.endsWith(".gd")) {
8218
- const found = DECLARATION.exec(readFileSync5(path, "utf8"));
8219
- if (found?.[1]) {
8220
- declared.set(found[1], `res://${prefix}${entry.name}`);
8221
- }
8479
+ each(`res://${prefix}${entry.name}`, readFileSync5(path, "utf8"));
8222
8480
  }
8223
8481
  }
8224
8482
  };
8225
8483
  visit(projectPath, "");
8226
- return declared;
8484
+ }
8485
+ function scriptsNaming(projectPath, classes) {
8486
+ const named = new Map(classes.map((name) => [name, ""]));
8487
+ const naming = [];
8488
+ eachScript(projectPath, (script, source) => {
8489
+ const declares = DECLARATION.exec(source)?.[1];
8490
+ if (declares !== undefined && named.has(declares)) {
8491
+ return;
8492
+ }
8493
+ const code = codeOf(source);
8494
+ if (classesNamedIn(code, named).length > 0) {
8495
+ naming.push({ script, tool: /^\s*@tool\b/m.test(code) });
8496
+ }
8497
+ });
8498
+ return naming.sort((a, b) => a.script.localeCompare(b.script));
8499
+ }
8500
+ function codeOf(source) {
8501
+ let code = "";
8502
+ let at = 0;
8503
+ while (at < source.length) {
8504
+ const here = source[at] ?? "";
8505
+ if (here === "#") {
8506
+ const end = source.indexOf(`
8507
+ `, at);
8508
+ at = end === -1 ? source.length : end;
8509
+ continue;
8510
+ }
8511
+ if (here === '"' || here === "'") {
8512
+ const fence = source.startsWith(here.repeat(3), at) ? here.repeat(3) : here;
8513
+ let end = at + fence.length;
8514
+ while (end < source.length && !source.startsWith(fence, end)) {
8515
+ end += source[end] === "\\" ? 2 : 1;
8516
+ }
8517
+ const literal = source.slice(at, Math.min(end + fence.length, source.length));
8518
+ code += literal.replace(/[^\n]/g, " ");
8519
+ at = end + fence.length;
8520
+ continue;
8521
+ }
8522
+ code += here;
8523
+ at += 1;
8524
+ }
8525
+ return code;
8227
8526
  }
8228
8527
  function cachedClasses(projectPath) {
8229
- const cache = join7(projectPath, ".godot", "global_script_class_cache.cfg");
8230
- if (!existsSync6(cache)) {
8528
+ const cache = join8(projectPath, ".godot", "global_script_class_cache.cfg");
8529
+ if (!existsSync7(cache)) {
8231
8530
  return null;
8232
8531
  }
8233
8532
  const listed = new Map;
@@ -8340,7 +8639,7 @@ function uncachedClassNote(types) {
8340
8639
  return `${types.join(", ")} ${one ? "is declared" : "are declared"} in this project and missing from ` + `.godot/global_script_class_cache.cfg, so a game launched now would not resolve ${one ? "it" : "them"} ` + "either. Try editor_rescan first: it asks the editor already running and starts nothing, and it " + "cleared this in 314ms in a second project with a 31-worker fan-out still importing. It is not " + "always enough, because a file another engine has already imported reads as settled and the " + "walk does not look inside it. project_import refresh_classes rewrites the cache from the " + "declarations on disk and does reach that case, at the cost of a short headless engine, which " + "is worth knowing when something else is already running against this project.";
8341
8640
  }
8342
8641
  function cacheWrittenAt(projectPath) {
8343
- const cache = join7(projectPath, ".godot", "global_script_class_cache.cfg");
8642
+ const cache = join8(projectPath, ".godot", "global_script_class_cache.cfg");
8344
8643
  try {
8345
8644
  return statSync2(cache).mtimeMs;
8346
8645
  } catch {
@@ -8354,14 +8653,14 @@ function declaredSince(projectPath, paths, names, since) {
8354
8653
  return false;
8355
8654
  }
8356
8655
  try {
8357
- return statSync2(join7(projectPath, path.slice("res://".length))).mtimeMs > since;
8656
+ return statSync2(join8(projectPath, path.slice("res://".length))).mtimeMs > since;
8358
8657
  } catch {
8359
8658
  return false;
8360
8659
  }
8361
8660
  });
8362
8661
  }
8363
8662
  function cachedAtMissingPaths(cached, projectPath) {
8364
- return [...cached].filter(([, path]) => path.startsWith("res://") && !existsSync6(join7(projectPath, path.slice("res://".length)))).map(([name]) => name).sort();
8663
+ return [...cached].filter(([, path]) => path.startsWith("res://") && !existsSync7(join8(projectPath, path.slice("res://".length)))).map(([name]) => name).sort();
8365
8664
  }
8366
8665
  function staleAgainst(cached, projectPath) {
8367
8666
  return [...declaredClasses(projectPath)].filter(([name, path]) => cached.get(name) !== path).map(([name]) => name);
@@ -15851,29 +16150,29 @@ function readParams(params, key) {
15851
16150
  }
15852
16151
 
15853
16152
  // src/setup.ts
15854
- import { spawnSync as spawnSync2 } from "node:child_process";
16153
+ import { spawnSync } from "node:child_process";
15855
16154
  import { createHash as createHash2 } from "node:crypto";
15856
16155
  import {
15857
16156
  cpSync,
15858
- existsSync as existsSync7,
16157
+ existsSync as existsSync8,
15859
16158
  mkdirSync as mkdirSync4,
15860
- readdirSync as readdirSync3,
16159
+ readdirSync as readdirSync4,
15861
16160
  readFileSync as readFileSync7,
15862
16161
  rmdirSync,
15863
16162
  rmSync as rmSync4,
15864
16163
  statSync as statSync3,
15865
16164
  writeFileSync as writeFileSync5
15866
16165
  } from "node:fs";
15867
- import { dirname as dirname4, join as join8 } from "node:path";
16166
+ import { dirname as dirname4, join as join9 } from "node:path";
15868
16167
  import { fileURLToPath } from "node:url";
15869
16168
  function editorAddonDigest(from = shippedAddonsDirectory()) {
15870
16169
  const hash = createHash2("sha256");
15871
16170
  const walk = (directory, relative) => {
15872
- for (const name of readdirSync3(directory).sort()) {
16171
+ for (const name of readdirSync4(directory).sort()) {
15873
16172
  if (name === VERSION_MARKER || name === DIGEST_MARKER) {
15874
16173
  continue;
15875
16174
  }
15876
- const path = join8(directory, name);
16175
+ const path = join9(directory, name);
15877
16176
  const inside = `${relative}/${name}`;
15878
16177
  if (statSync3(path).isDirectory()) {
15879
16178
  walk(path, inside);
@@ -15887,7 +16186,7 @@ function editorAddonDigest(from = shippedAddonsDirectory()) {
15887
16186
  }
15888
16187
  };
15889
16188
  for (const name of EDITOR_PLUGINS) {
15890
- walk(join8(from, name), name);
16189
+ walk(join9(from, name), name);
15891
16190
  }
15892
16191
  return hash.digest("hex");
15893
16192
  }
@@ -15902,42 +16201,42 @@ function shippedEditorDigest() {
15902
16201
  return shippedDigest ?? undefined;
15903
16202
  }
15904
16203
  function installedEditorDigest(projectPath) {
15905
- const marker = join8(projectPath, "addons", ADDONS[0], DIGEST_MARKER);
15906
- if (!existsSync7(marker)) {
16204
+ const marker = join9(projectPath, "addons", ADDONS[0], DIGEST_MARKER);
16205
+ if (!existsSync8(marker)) {
15907
16206
  return null;
15908
16207
  }
15909
16208
  const said = readFileSync7(marker, "utf8").trim();
15910
16209
  return said === "" ? null : said;
15911
16210
  }
15912
16211
  function shippedAddonsDirectory() {
15913
- return join8(dirname4(fileURLToPath(import.meta.url)), "godot", "addons");
16212
+ return join9(dirname4(fileURLToPath(import.meta.url)), "godot", "addons");
15914
16213
  }
15915
16214
  function shippedOperationsScript() {
15916
- return join8(dirname4(fileURLToPath(import.meta.url)), "godot", "operations", "godot_operations.gd");
16215
+ return join9(dirname4(fileURLToPath(import.meta.url)), "godot", "operations", "godot_operations.gd");
15917
16216
  }
15918
16217
  function installAddons(projectPath, from = shippedAddonsDirectory()) {
15919
16218
  const installed = [];
15920
16219
  for (const name of ADDONS) {
15921
- const source = join8(from, name);
15922
- if (!existsSync7(join8(source, name === "gdharness_runtime" ? "runtime_autoload.gd" : "plugin.cfg"))) {
16220
+ const source = join9(from, name);
16221
+ if (!existsSync8(join9(source, name === "gdharness_runtime" ? "runtime_autoload.gd" : "plugin.cfg"))) {
15923
16222
  throw new Error(`The package holds no ${name} addon at ${source}.`);
15924
16223
  }
15925
- const target = join8(projectPath, "addons", name);
15926
- const replaced = existsSync7(target);
16224
+ const target = join9(projectPath, "addons", name);
16225
+ const replaced = existsSync8(target);
15927
16226
  rmSync4(target, { recursive: true, force: true });
15928
16227
  mkdirSync4(dirname4(target), { recursive: true });
15929
16228
  cpSync(source, target, { recursive: true });
15930
- writeFileSync5(join8(target, VERSION_MARKER), `${SERVER_VERSION}
16229
+ writeFileSync5(join9(target, VERSION_MARKER), `${SERVER_VERSION}
15931
16230
  `);
15932
16231
  installed.push({ name, path: target, replaced });
15933
16232
  }
15934
- writeFileSync5(join8(projectPath, "addons", ADDONS[0], DIGEST_MARKER), `${editorAddonDigest(from)}
16233
+ writeFileSync5(join9(projectPath, "addons", ADDONS[0], DIGEST_MARKER), `${editorAddonDigest(from)}
15935
16234
  `);
15936
16235
  return installed;
15937
16236
  }
15938
16237
  function installedAddonVersion(projectPath) {
15939
- const marker = join8(projectPath, "addons", ADDONS[0], VERSION_MARKER);
15940
- if (!existsSync7(marker)) {
16238
+ const marker = join9(projectPath, "addons", ADDONS[0], VERSION_MARKER);
16239
+ if (!existsSync8(marker)) {
15941
16240
  return null;
15942
16241
  }
15943
16242
  const said = readFileSync7(marker, "utf8").trim();
@@ -15946,14 +16245,14 @@ function installedAddonVersion(projectPath) {
15946
16245
  function removeAddons(projectPath) {
15947
16246
  const removed = [];
15948
16247
  for (const name of ADDONS) {
15949
- const target = join8(projectPath, "addons", name);
15950
- if (existsSync7(target)) {
16248
+ const target = join9(projectPath, "addons", name);
16249
+ if (existsSync8(target)) {
15951
16250
  rmSync4(target, { recursive: true, force: true });
15952
16251
  removed.push(target);
15953
16252
  }
15954
16253
  }
15955
- const addons = join8(projectPath, "addons");
15956
- if (removed.length > 0 && existsSync7(addons) && readdirSync3(addons).length === 0) {
16254
+ const addons = join9(projectPath, "addons");
16255
+ if (removed.length > 0 && existsSync8(addons) && readdirSync4(addons).length === 0) {
15957
16256
  rmdirSync(addons);
15958
16257
  removed.push(addons);
15959
16258
  }
@@ -15987,7 +16286,7 @@ function loaderAmong(projectPath, named) {
15987
16286
  }
15988
16287
  let source = null;
15989
16288
  try {
15990
- source = readFileSync7(join8(projectPath, path), "utf8");
16289
+ source = readFileSync7(join9(projectPath, path), "utf8");
15991
16290
  } catch {}
15992
16291
  if (source?.includes(RUNTIME_AUTOLOAD.path)) {
15993
16292
  return { name: entry, path, exists: true };
@@ -16010,7 +16309,7 @@ function trackedByGit(projectPath, wanted) {
16010
16309
  if (wanted.length === 0) {
16011
16310
  return new Set;
16012
16311
  }
16013
- const asked = spawnSync2("git", ["ls-files", "-z", "--", ...wanted], {
16312
+ const asked = spawnSync("git", ["ls-files", "-z", "--", ...wanted], {
16014
16313
  cwd: projectPath,
16015
16314
  encoding: "utf8"
16016
16315
  });
@@ -16029,10 +16328,10 @@ function enabledPlugins(settings) {
16029
16328
  function inspectProject(projectPath) {
16030
16329
  const problems = [];
16031
16330
  const addons = ADDONS.map((name) => {
16032
- const directory = join8(projectPath, "addons", name);
16033
- const marker = join8(directory, VERSION_MARKER);
16034
- const installed = existsSync7(directory);
16035
- const version = existsSync7(marker) ? readFileSync7(marker, "utf8").trim() : null;
16331
+ const directory = join9(projectPath, "addons", name);
16332
+ const marker = join9(directory, VERSION_MARKER);
16333
+ const installed = existsSync8(directory);
16334
+ const version = existsSync8(marker) ? readFileSync7(marker, "utf8").trim() : null;
16036
16335
  const current = version === SERVER_VERSION;
16037
16336
  if (!installed) {
16038
16337
  problems.push(`addons/${name} is not installed; run gdharness setup`);
@@ -16041,7 +16340,7 @@ function inspectProject(projectPath) {
16041
16340
  }
16042
16341
  return { name, installed, version, current };
16043
16342
  });
16044
- const settings = parseProjectGodot(readFileSync7(join8(projectPath, "project.godot"), "utf8"));
16343
+ const settings = parseProjectGodot(readFileSync7(join9(projectPath, "project.godot"), "utf8"));
16045
16344
  const pluginsEnabled = enabledPlugins(settings);
16046
16345
  for (const name of EDITOR_PLUGINS) {
16047
16346
  if (!pluginsEnabled.includes(name)) {
@@ -16057,7 +16356,7 @@ function inspectProject(projectPath) {
16057
16356
  const tracked = trackedByGit(projectPath, [...named.values()]);
16058
16357
  const addonMissing = (path) => addons.some((addon) => !addon.installed && path.startsWith(`addons/${addon.name}/`));
16059
16358
  for (const [name, path] of named) {
16060
- if (!existsSync7(join8(projectPath, path))) {
16359
+ if (!existsSync8(join9(projectPath, path))) {
16061
16360
  if (!addonMissing(path)) {
16062
16361
  problems.push(`the ${name} autoload names res://${path}, which is not in this project, so it boots with a missing script; restore that file, or point the autoload at one that is here`);
16063
16362
  }
@@ -16816,7 +17115,11 @@ var init_tool_definitions = __esm(() => {
16816
17115
  name: "editor_run",
16817
17116
  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.",
16818
17117
  parameters: {
16819
- projectPath: { ...PROJECT_PATH, ops: ["start", "check"] },
17118
+ projectPath: {
17119
+ ...PROJECT_PATH,
17120
+ ops: ["start", "check", "stop", "wait"],
17121
+ 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."
17122
+ },
16820
17123
  scene: {
16821
17124
  type: "string",
16822
17125
  ops: ["start", "check"],
@@ -16948,7 +17251,7 @@ var init_tool_definitions = __esm(() => {
16948
17251
  },
16949
17252
  {
16950
17253
  name: "editor_rescan",
16951
- 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.",
17254
+ 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.",
16952
17255
  parameters: {
16953
17256
  projectPath: PROJECT_PATH,
16954
17257
  timeoutMs: {
@@ -17054,7 +17357,7 @@ var init_tool_definitions = __esm(() => {
17054
17357
  says: {
17055
17358
  type: "string",
17056
17359
  ops: ["find"],
17057
- 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."
17360
+ 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.'
17058
17361
  },
17059
17362
  limit: {
17060
17363
  type: "integer",
@@ -17091,7 +17394,7 @@ var init_tool_definitions = __esm(() => {
17091
17394
  requires: []
17092
17395
  },
17093
17396
  rect: {
17094
- summary: "one node's rectangle or position, in canvas and in window pixels. A 3D node answers with the point to aim at, which is the middle of what it draws rather than the origin it stands on, the rectangle it covers under covers, the camera that drew it, and behind_camera when it is not in front of one",
17397
+ summary: "one node's rectangle or position, in canvas and in window pixels. Canvas is the node's own viewport; window is where it is drawn in the game's window, through any embedded dialog or SubViewport it sits in, which is what runtime_input mouse_click and mouse_motion take. A 3D node answers with the point to aim at, which is the middle of what it draws rather than the origin it stands on, the rectangle it covers under covers, the camera that drew it, and behind_camera when it is not in front of one",
17095
17398
  requires: ["nodePath"]
17096
17399
  },
17097
17400
  property: {
@@ -17209,7 +17512,7 @@ var init_tool_definitions = __esm(() => {
17209
17512
  says: {
17210
17513
  type: "string",
17211
17514
  ops: ["click"],
17212
- 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."
17515
+ 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."
17213
17516
  },
17214
17517
  action: {
17215
17518
  type: "string",
@@ -17290,7 +17593,7 @@ var init_tool_definitions = __esm(() => {
17290
17593
  requires: []
17291
17594
  },
17292
17595
  choose: {
17293
- summary: "take an item out of a menu, by what it says or by where it is in the list. A menu's items are drawn rather than built, so there is nothing to click: the item takes the focus and Enter presses it, which is the engine's own path and needs no window. Answers with what was chosen and what the button in front of it shows now",
17596
+ summary: "take an item out of a menu, by what it says or by where it is in the list. A menu's items are drawn rather than built, so there is nothing to click: the item takes the focus and Enter presses it, which is the engine's own path and needs no window. Answers with what was chosen, read before the press, what became of the control named (in_tree, removed or freed, since a game can rebuild its screen on the pick), and what the button in front of it shows now when it is still there",
17294
17597
  requires: ["nodePath"]
17295
17598
  },
17296
17599
  action: { summary: "press an action, or hold it", requires: ["action"] },
@@ -17340,7 +17643,7 @@ var init_tool_definitions = __esm(() => {
17340
17643
  says: {
17341
17644
  type: "string",
17342
17645
  ops: ["until"],
17343
- 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."
17646
+ 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.'
17344
17647
  },
17345
17648
  includeHidden: {
17346
17649
  type: "boolean",
@@ -32099,10 +32402,10 @@ var init_stdio = __esm(() => {
32099
32402
  });
32100
32403
 
32101
32404
  // node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
32102
- import process6 from "node:process";
32405
+ import process9 from "node:process";
32103
32406
 
32104
32407
  class StdioServerTransport {
32105
- constructor(_stdin = process6.stdin, _stdout = process6.stdout, options) {
32408
+ constructor(_stdin = process9.stdin, _stdout = process9.stdout, options) {
32106
32409
  this._stdin = _stdin;
32107
32410
  this._stdout = _stdout;
32108
32411
  this._started = false;
@@ -32167,10 +32470,10 @@ var init_stdio2 = __esm(() => {
32167
32470
  });
32168
32471
 
32169
32472
  // src/runtime-client.ts
32170
- import { existsSync as existsSync9, readdirSync as readdirSync5, readFileSync as readFileSync9, statSync as statSync4, unlinkSync } from "node:fs";
32473
+ import { existsSync as existsSync10, readdirSync as readdirSync6, readFileSync as readFileSync9, statSync as statSync4, unlinkSync } from "node:fs";
32171
32474
  import { createConnection } from "node:net";
32172
32475
  import { tmpdir as tmpdir3 } from "node:os";
32173
- import { dirname as dirname7, join as join11, resolve as resolve3 } from "node:path";
32476
+ import { dirname as dirname7, join as join12, resolve as resolve3 } from "node:path";
32174
32477
  import { setTimeout as delay } from "node:timers/promises";
32175
32478
  function runtimeDirectory(variables = process.env) {
32176
32479
  const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
@@ -32178,20 +32481,20 @@ function runtimeDirectory(variables = process.env) {
32178
32481
  return explicit;
32179
32482
  }
32180
32483
  const perUser = envValue("XDG_RUNTIME_DIR", variables);
32181
- return join11(perUser ?? tmpdir3(), "gdharness");
32484
+ return join12(perUser ?? tmpdir3(), "gdharness");
32182
32485
  }
32183
32486
  function runtimeDirectories(variables = process.env) {
32184
32487
  const candidates = [runtimeDirectory(variables)];
32185
32488
  const fallbacks = process.platform === "win32" ? [envValue("SystemRoot", variables) ?? envValue("windir", variables) ?? "C:\\Windows"] : ["/tmp", "/var/tmp"];
32186
32489
  for (const base of fallbacks) {
32187
- candidates.push(join11(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
32490
+ candidates.push(join12(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
32188
32491
  }
32189
32492
  return [...new Set(candidates.map((path) => resolve3(path)))];
32190
32493
  }
32191
32494
  function errorReportOf(pid, directories = runtimeDirectories()) {
32192
32495
  for (const directory of directories) {
32193
- const path = join11(directory, `runtime-${pid}.log`);
32194
- if (existsSync9(path)) {
32496
+ const path = join12(directory, `runtime-${pid}.log`);
32497
+ if (existsSync10(path)) {
32195
32498
  return path;
32196
32499
  }
32197
32500
  }
@@ -32219,14 +32522,6 @@ function reportAlreadyInTranscript(transcript, report) {
32219
32522
  const end = boundary + copied.length;
32220
32523
  return end <= report.length && report.subarray(boundary, end).equals(copied) ? end : report.length;
32221
32524
  }
32222
- function processAlive(pid) {
32223
- try {
32224
- process.kill(pid, 0);
32225
- return true;
32226
- } catch (error) {
32227
- return error.code === "EPERM";
32228
- }
32229
- }
32230
32525
  function parseAnnouncement(file, pid) {
32231
32526
  let fields;
32232
32527
  try {
@@ -32314,46 +32609,52 @@ function strangersAmong(candidates, now, startTimes = startTimesOf) {
32314
32609
  const known = verdicts.get(one.file);
32315
32610
  return known === undefined || known.writtenAt !== one.writtenAt || !known.stranger && now - known.judgedAt > CONFIRMED_FOR_MS;
32316
32611
  });
32317
- if (toJudge.length > 0) {
32318
- const began = startTimes(toJudge.map((one) => one.pid));
32319
- for (const one of toJudge) {
32320
- const at = began.get(one.pid);
32321
- const stranger = at !== undefined && at - one.writtenAt > STARTED_AFTER_ANNOUNCING_MS;
32322
- verdicts.set(one.file, { writtenAt: one.writtenAt, judgedAt: now, stranger });
32323
- }
32612
+ if (toJudge.length > 0 && judging === null) {
32613
+ judging = startTimes(toJudge.map((one) => one.pid)).then((began) => {
32614
+ for (const one of toJudge) {
32615
+ const at = began.get(one.pid);
32616
+ const stranger = at !== undefined && at - one.writtenAt > STARTED_AFTER_ANNOUNCING_MS;
32617
+ verdicts.set(one.file, { writtenAt: one.writtenAt, judgedAt: now, stranger });
32618
+ }
32619
+ }).catch(() => {}).finally(() => {
32620
+ judging = null;
32621
+ });
32324
32622
  }
32325
- return new Set(candidates.filter((one) => verdicts.get(one.file)?.stranger === true).map((one) => one.file));
32623
+ return new Set(candidates.filter((one) => {
32624
+ const known = verdicts.get(one.file);
32625
+ return known?.stranger === true && known.writtenAt === one.writtenAt;
32626
+ }).map((one) => one.file));
32326
32627
  }
32327
32628
  function announcedIn(directory) {
32328
- if (!existsSync9(directory)) {
32629
+ if (!existsSync10(directory)) {
32329
32630
  return [];
32330
32631
  }
32331
32632
  const found = [];
32332
32633
  const candidates = [];
32333
32634
  const entries = [];
32334
- for (const entry of readdirSync5(directory)) {
32635
+ for (const entry of readdirSync6(directory)) {
32335
32636
  const report = ERROR_REPORT_PATTERN.exec(entry);
32336
32637
  if (report) {
32337
- sweepErrorReport(join11(directory, entry), Number.parseInt(report[1] ?? "", 10));
32638
+ sweepErrorReport(join12(directory, entry), Number.parseInt(report[1] ?? "", 10));
32338
32639
  continue;
32339
32640
  }
32340
32641
  const match = ANNOUNCEMENT_PATTERN.exec(entry);
32341
32642
  if (!match) {
32342
32643
  continue;
32343
32644
  }
32344
- const file = join11(directory, entry);
32645
+ const file = join12(directory, entry);
32345
32646
  const pid = Number.parseInt(match[1] ?? "", 10);
32346
- const alive = processAlive(pid);
32347
- entries.push({ file, pid, alive });
32348
- if (alive) {
32647
+ const running = alive(pid);
32648
+ entries.push({ file, pid, running });
32649
+ if (running) {
32349
32650
  try {
32350
32651
  candidates.push({ file, pid, writtenAt: statSync4(file).mtimeMs });
32351
32652
  } catch {}
32352
32653
  }
32353
32654
  }
32354
32655
  const strangers = strangersAmong(candidates, Date.now());
32355
- for (const { file, pid, alive } of entries) {
32356
- const gone = !alive || strangers.has(file);
32656
+ for (const { file, pid, running } of entries) {
32657
+ const gone = !running || strangers.has(file);
32357
32658
  const announced = gone ? { kind: "rubbish" } : parseAnnouncement(file, pid);
32358
32659
  if (announced.kind === "rubbish") {
32359
32660
  if (!gone) {
@@ -32369,7 +32670,7 @@ function announcedIn(directory) {
32369
32670
  found.push(announced);
32370
32671
  }
32371
32672
  const held = new Set(entries.map((one) => one.file));
32372
- const here = join11(directory, ".");
32673
+ const here = join12(directory, ".");
32373
32674
  for (const file of verdicts.keys()) {
32374
32675
  if (dirname7(file) === here && !held.has(file)) {
32375
32676
  verdicts.delete(file);
@@ -32378,13 +32679,13 @@ function announcedIn(directory) {
32378
32679
  return found;
32379
32680
  }
32380
32681
  async function announcementEnded(pid, directories = runtimeDirectories()) {
32381
- const files = directories.map((directory) => join11(directory, `runtime-${pid}.json`)).filter((file) => existsSync9(file));
32682
+ const files = directories.map((directory) => join12(directory, `runtime-${pid}.json`)).filter((file) => existsSync10(file));
32382
32683
  const first = files[0];
32383
32684
  if (first === undefined) {
32384
32685
  return;
32385
32686
  }
32386
32687
  const until = Date.now() + ENDED_WITHIN_MS;
32387
- while (processAlive(pid)) {
32688
+ while (alive(pid)) {
32388
32689
  const left = until - Date.now();
32389
32690
  if (left <= 0) {
32390
32691
  return;
@@ -32400,7 +32701,7 @@ async function announcementEnded(pid, directories = runtimeDirectories()) {
32400
32701
  }
32401
32702
  }
32402
32703
  function sweepErrorReport(path, pid, now = Date.now()) {
32403
- if (processAlive(pid)) {
32704
+ if (alive(pid)) {
32404
32705
  return;
32405
32706
  }
32406
32707
  try {
@@ -32663,8 +32964,9 @@ function runtimeRequest(endpoint, command, params, timeoutMs, keepListening = fa
32663
32964
  });
32664
32965
  });
32665
32966
  }
32666
- var RUNTIME_PROTOCOL = 2, ANNOUNCEMENT_PATTERN, ERROR_REPORT_PATTERN, CONSOLE_BOUNDARY = "[gdharness] runtime listening on ", ERROR_REPORT_KEEP_MS, GONE_WINDOW_MS = 120000, GONE_KEPT = 32, gone, STARTED_AFTER_ANNOUNCING_MS = 2000, CONFIRMED_FOR_MS = 60000, JUDGED_AFTER_MS = 60000, verdicts, ENDED_WITHIN_MS = 3000, LOOK_EVERY_MS = 50, ANNOUNCE_BUDGET_MS = 5000, nextRequestId = 1, LATE_ANSWER_MS, LATE_KEPT = 50, lateAnswers;
32967
+ var RUNTIME_PROTOCOL = 2, ANNOUNCEMENT_PATTERN, ERROR_REPORT_PATTERN, CONSOLE_BOUNDARY = "[gdharness] runtime listening on ", ERROR_REPORT_KEEP_MS, GONE_WINDOW_MS = 120000, GONE_KEPT = 32, gone, STARTED_AFTER_ANNOUNCING_MS = 2000, CONFIRMED_FOR_MS = 60000, JUDGED_AFTER_MS = 60000, verdicts, judging = null, ENDED_WITHIN_MS = 3000, LOOK_EVERY_MS = 50, ANNOUNCE_BUDGET_MS = 5000, nextRequestId = 1, LATE_ANSWER_MS, LATE_KEPT = 50, lateAnswers;
32667
32968
  var init_runtime_client = __esm(() => {
32969
+ init_alive();
32668
32970
  init_paths();
32669
32971
  init_process_children();
32670
32972
  ANNOUNCEMENT_PATTERN = /^runtime-(\d+)\.json$/;
@@ -32678,9 +32980,9 @@ var init_runtime_client = __esm(() => {
32678
32980
 
32679
32981
  // src/boot-note.ts
32680
32982
  import { mkdirSync as mkdirSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync7 } from "node:fs";
32681
- import { dirname as dirname8, join as join12 } from "node:path";
32983
+ import { dirname as dirname8, join as join13 } from "node:path";
32682
32984
  function bootNotePath(projectPath) {
32683
- return join12(projectPath, ".godot", "gdharness-boot.json");
32985
+ return join13(projectPath, ".godot", "gdharness-boot.json");
32684
32986
  }
32685
32987
  function readBootNote(projectPath) {
32686
32988
  try {
@@ -32713,9 +33015,9 @@ var init_boot_note = __esm(() => {
32713
33015
 
32714
33016
  // src/breakpoint-note.ts
32715
33017
  import { mkdirSync as mkdirSync7, readFileSync as readFileSync11, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "node:fs";
32716
- import { dirname as dirname9, isAbsolute as isAbsolute2, join as join13, relative as relative2 } from "node:path";
33018
+ import { dirname as dirname9, isAbsolute as isAbsolute2, join as join14, relative as relative2 } from "node:path";
32717
33019
  function breakpointNotePath(projectPath) {
32718
- return join13(projectPath, ".godot", "gdharness-breakpoints.json");
33020
+ return join14(projectPath, ".godot", "gdharness-breakpoints.json");
32719
33021
  }
32720
33022
  function writeBreakpointNote(projectPath, held) {
32721
33023
  const path = breakpointNotePath(projectPath);
@@ -32756,7 +33058,7 @@ function readBreakpointNote(projectPath) {
32756
33058
  }
32757
33059
  const lines = Array.isArray(listed) ? listed.filter((line) => Number.isInteger(line) && line > 0) : [];
32758
33060
  if (lines.length > 0) {
32759
- held.push({ scriptPath: realPathOr(join13(projectPath, script)), lines });
33061
+ held.push({ scriptPath: realPathOr(join14(projectPath, script)), lines });
32760
33062
  }
32761
33063
  }
32762
33064
  return held;
@@ -32775,9 +33077,9 @@ var init_breakpoint_note = __esm(() => {
32775
33077
 
32776
33078
  // src/class-note.ts
32777
33079
  import { mkdirSync as mkdirSync8, readFileSync as readFileSync12, writeFileSync as writeFileSync9 } from "node:fs";
32778
- import { dirname as dirname10, join as join14 } from "node:path";
33080
+ import { dirname as dirname10, join as join15 } from "node:path";
32779
33081
  function classNotePath(projectPath) {
32780
- return join14(projectPath, ".godot", "gdharness-classes.json");
33082
+ return join15(projectPath, ".godot", "gdharness-classes.json");
32781
33083
  }
32782
33084
  function readClassNote(projectPath) {
32783
33085
  try {
@@ -32806,11 +33108,11 @@ function writeClassNote(projectPath, note) {
32806
33108
  var init_class_note = () => {};
32807
33109
 
32808
33110
  // src/config-pin.ts
32809
- import { existsSync as existsSync10, readFileSync as readFileSync13 } from "node:fs";
32810
- import { join as join15 } from "node:path";
33111
+ import { existsSync as existsSync11, readFileSync as readFileSync13 } from "node:fs";
33112
+ import { join as join16 } from "node:path";
32811
33113
  function versionsNamedInConfig(projectPath) {
32812
- const config = join15(projectPath, ".mcp.json");
32813
- if (!existsSync10(config)) {
33114
+ const config = join16(projectPath, ".mcp.json");
33115
+ if (!existsSync11(config)) {
32814
33116
  return [];
32815
33117
  }
32816
33118
  let text;
@@ -33443,6 +33745,9 @@ class GodotDAPClient {
33443
33745
  isConnected() {
33444
33746
  return this.connected;
33445
33747
  }
33748
+ connection() {
33749
+ return this.connected ? this.socket : null;
33750
+ }
33446
33751
  isStopped() {
33447
33752
  return this.halt !== null;
33448
33753
  }
@@ -33572,7 +33877,7 @@ var init_desktop = () => {};
33572
33877
  // src/editor-log.ts
33573
33878
  import {
33574
33879
  closeSync,
33575
- existsSync as existsSync11,
33880
+ existsSync as existsSync12,
33576
33881
  mkdirSync as mkdirSync9,
33577
33882
  openSync,
33578
33883
  readFileSync as readFileSync14,
@@ -33581,12 +33886,12 @@ import {
33581
33886
  statSync as statSync5,
33582
33887
  writeFileSync as writeFileSync10
33583
33888
  } from "node:fs";
33584
- import { dirname as dirname11, join as join16 } from "node:path";
33889
+ import { dirname as dirname11, join as join17 } from "node:path";
33585
33890
  function editorLogPath(projectPath) {
33586
- return join16(projectPath, ".godot", "gdharness-editor.log");
33891
+ return join17(projectPath, ".godot", "gdharness-editor.log");
33587
33892
  }
33588
33893
  function editorLogNotePath(projectPath) {
33589
- return join16(projectPath, ".godot", "gdharness-editor.json");
33894
+ return join17(projectPath, ".godot", "gdharness-editor.json");
33590
33895
  }
33591
33896
  function readEditorLogNote(projectPath) {
33592
33897
  try {
@@ -33631,7 +33936,7 @@ function editorConsole(projectPath, editorPid, openedByAServer) {
33631
33936
  return { kind: "older server", notedPid: noted?.pid ?? null };
33632
33937
  }
33633
33938
  const path = editorLogPath(projectPath);
33634
- if (!existsSync11(path)) {
33939
+ if (!existsSync12(path)) {
33635
33940
  return { kind: "not written" };
33636
33941
  }
33637
33942
  const log = new GameLog;
@@ -38060,7 +38365,7 @@ var init_lsp_client = __esm(() => {
38060
38365
  });
38061
38366
 
38062
38367
  // src/outside.ts
38063
- import { spawn } from "node:child_process";
38368
+ import { spawn as spawn2 } from "node:child_process";
38064
38369
  import { fileURLToPath as fileURLToPath4 } from "node:url";
38065
38370
  function changesFrom(env, base) {
38066
38371
  const changes = {};
@@ -38093,7 +38398,7 @@ function readLaunched(printed) {
38093
38398
  }
38094
38399
  async function launchOutsideTheTree(spec, runtime = process.execPath, keeper = KEEPER_SCRIPT) {
38095
38400
  return await new Promise((resolve) => {
38096
- const launcher = spawn(runtime, [keeper, "launch"], {
38401
+ const launcher = spawn2(runtime, [keeper, "launch"], {
38097
38402
  stdio: ["pipe", "pipe", "pipe"],
38098
38403
  windowsHide: true
38099
38404
  });
@@ -38132,14 +38437,14 @@ import { execFile as execFile4 } from "node:child_process";
38132
38437
  import { promisify as promisify4 } from "node:util";
38133
38438
  async function cpuSecondsOf(pid) {
38134
38439
  try {
38135
- return process.platform === "win32" ? await askWindows2(pid) : await askPosix2(pid);
38440
+ return process.platform === "win32" ? await secondsOnWindows(pid) : await askPosix2(pid);
38136
38441
  } catch {
38137
38442
  return;
38138
38443
  }
38139
38444
  }
38140
- async function askWindows2(pid) {
38141
- const { stdout } = await run4("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", `(Get-Process -Id ${pid}).CPU`], { timeout: ASK_TIMEOUT_MS2, windowsHide: true });
38142
- const seconds = Number(stdout.trim().replace(",", "."));
38445
+ async function secondsOnWindows(pid) {
38446
+ const said = await askWindows(`(Get-Process -Id ${pid}).CPU`, ASK_TIMEOUT_MS2);
38447
+ const seconds = Number(said.trim().replace(",", "."));
38143
38448
  return Number.isFinite(seconds) ? seconds : undefined;
38144
38449
  }
38145
38450
  async function askPosix2(pid) {
@@ -38163,17 +38468,18 @@ function secondsFromClock(printed) {
38163
38468
  }
38164
38469
  var run4, ASK_TIMEOUT_MS2 = 6000;
38165
38470
  var init_process_time = __esm(() => {
38471
+ init_windows_ask();
38166
38472
  run4 = promisify4(execFile4);
38167
38473
  });
38168
38474
 
38169
38475
  // src/project-scan.ts
38170
- import { existsSync as existsSync12, readdirSync as readdirSync6, readFileSync as readFileSync15 } from "node:fs";
38171
- import { join as join17 } from "node:path";
38476
+ import { existsSync as existsSync13, readdirSync as readdirSync7, readFileSync as readFileSync15 } from "node:fs";
38477
+ import { join as join18 } from "node:path";
38172
38478
  function skipped(name) {
38173
38479
  return name.startsWith(".") || SKIPPED.has(name);
38174
38480
  }
38175
38481
  function steppedOver(directory) {
38176
- return existsSync12(join17(directory, ".gdignore"));
38482
+ return existsSync13(join18(directory, ".gdignore"));
38177
38483
  }
38178
38484
  function projectStructure(projectPath) {
38179
38485
  const structure = { scenes: 0, scripts: 0, assets: 0, other: 0 };
@@ -38181,12 +38487,12 @@ function projectStructure(projectPath) {
38181
38487
  if (steppedOver(directory)) {
38182
38488
  return;
38183
38489
  }
38184
- for (const entry of readdirSync6(directory, { withFileTypes: true })) {
38490
+ for (const entry of readdirSync7(directory, { withFileTypes: true })) {
38185
38491
  if (skipped(entry.name)) {
38186
38492
  continue;
38187
38493
  }
38188
38494
  if (entry.isDirectory()) {
38189
- visit(join17(directory, entry.name));
38495
+ visit(join18(directory, entry.name));
38190
38496
  } else if (entry.isFile()) {
38191
38497
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
38192
38498
  if (extension === "tscn") {
@@ -38210,20 +38516,20 @@ function scriptsWithoutUid(projectPath) {
38210
38516
  if (steppedOver(directory)) {
38211
38517
  return;
38212
38518
  }
38213
- for (const entry of readdirSync6(directory, { withFileTypes: true })) {
38519
+ for (const entry of readdirSync7(directory, { withFileTypes: true })) {
38214
38520
  if (skipped(entry.name)) {
38215
38521
  continue;
38216
38522
  }
38217
38523
  const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
38218
38524
  if (entry.isDirectory()) {
38219
- visit(join17(directory, entry.name), spelled);
38525
+ visit(join18(directory, entry.name), spelled);
38220
38526
  continue;
38221
38527
  }
38222
38528
  if (!entry.isFile()) {
38223
38529
  continue;
38224
38530
  }
38225
38531
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
38226
- if (CARRIES_A_UID.has(extension) && !existsSync12(join17(directory, `${entry.name}.uid`))) {
38532
+ if (CARRIES_A_UID.has(extension) && !existsSync13(join18(directory, `${entry.name}.uid`))) {
38227
38533
  found.push(spelled);
38228
38534
  }
38229
38535
  }
@@ -38251,14 +38557,14 @@ function searchProject(projectPath, options) {
38251
38557
  if (steppedOver(directory)) {
38252
38558
  return;
38253
38559
  }
38254
- for (const entry of readdirSync6(directory, { withFileTypes: true })) {
38560
+ for (const entry of readdirSync7(directory, { withFileTypes: true })) {
38255
38561
  if (full()) {
38256
38562
  return;
38257
38563
  }
38258
38564
  if (skipped(entry.name)) {
38259
38565
  continue;
38260
38566
  }
38261
- const entryPath = join17(directory, entry.name);
38567
+ const entryPath = join18(directory, entry.name);
38262
38568
  if (entry.isDirectory()) {
38263
38569
  visit(entryPath);
38264
38570
  continue;
@@ -38299,12 +38605,12 @@ var init_project_scan = __esm(() => {
38299
38605
 
38300
38606
  // src/reimport.ts
38301
38607
  import { createHash as createHash3 } from "node:crypto";
38302
- import { existsSync as existsSync13, readFileSync as readFileSync16, rmSync as rmSync8, statSync as statSync6, utimesSync, writeFileSync as writeFileSync11 } from "node:fs";
38303
- import { basename as basename3, join as join18 } from "node:path";
38608
+ import { existsSync as existsSync14, readFileSync as readFileSync16, rmSync as rmSync8, statSync as statSync6, utimesSync, writeFileSync as writeFileSync11 } from "node:fs";
38609
+ import { basename as basename3, join as join19 } from "node:path";
38304
38610
  function forceNextImport(projectPath, resourcePath) {
38305
38611
  const relative = resourcePath.replace(/^res:\/\//, "");
38306
- const sidecar = join18(projectPath, `${relative}.import`);
38307
- if (!existsSync13(sidecar)) {
38612
+ const sidecar = join19(projectPath, `${relative}.import`);
38613
+ if (!existsSync14(sidecar)) {
38308
38614
  return;
38309
38615
  }
38310
38616
  const { atime, mtimeMs } = statSync6(sidecar);
@@ -38314,15 +38620,15 @@ function forceNextImport(projectPath, resourcePath) {
38314
38620
  }
38315
38621
  function importRecord(projectPath, resourcePath) {
38316
38622
  const digest = createHash3("md5").update(resourcePath).digest("hex");
38317
- return join18(projectPath, ".godot", "imported", `${basename3(resourcePath)}-${digest}.md5`);
38623
+ return join19(projectPath, ".godot", "imported", `${basename3(resourcePath)}-${digest}.md5`);
38318
38624
  }
38319
38625
  var init_reimport = () => {};
38320
38626
 
38321
38627
  // src/restart-note.ts
38322
38628
  import { mkdirSync as mkdirSync10, readFileSync as readFileSync17, rmSync as rmSync9, writeFileSync as writeFileSync12 } from "node:fs";
38323
- import { dirname as dirname13, join as join19 } from "node:path";
38629
+ import { dirname as dirname13, join as join20 } from "node:path";
38324
38630
  function restartNotePath(projectPath) {
38325
- return join19(projectPath, ".godot", "gdharness-restart.json");
38631
+ return join20(projectPath, ".godot", "gdharness-restart.json");
38326
38632
  }
38327
38633
  function noteRestartBegun(note) {
38328
38634
  const path = restartNotePath(note.projectPath);
@@ -38361,28 +38667,29 @@ var init_restart_note = __esm(() => {
38361
38667
  });
38362
38668
 
38363
38669
  // src/run-record.ts
38364
- import { execFileSync as execFileSync2 } from "node:child_process";
38670
+ import { execFile as execFile5 } from "node:child_process";
38365
38671
  import { createHash as createHash4 } from "node:crypto";
38366
38672
  import {
38367
38673
  mkdirSync as mkdirSync11,
38368
38674
  openSync as openSync2,
38369
- readdirSync as readdirSync7,
38675
+ readdirSync as readdirSync8,
38370
38676
  readFileSync as readFileSync18,
38371
38677
  realpathSync as realpathSync3,
38372
38678
  rmSync as rmSync10,
38373
38679
  statSync as statSync7,
38374
38680
  writeFileSync as writeFileSync13
38375
38681
  } from "node:fs";
38376
- import { basename as basename4, dirname as dirname14, join as join20, resolve as resolve5 } from "node:path";
38682
+ import { basename as basename4, dirname as dirname14, join as join21, resolve as resolve5 } from "node:path";
38683
+ import { promisify as promisify5 } from "node:util";
38377
38684
  function runsDirectory() {
38378
- return join20(runtimeDirectory(), "runs");
38685
+ return join21(runtimeDirectory(), "runs");
38379
38686
  }
38380
38687
  function realAsFarAsItExists(path) {
38381
38688
  const missing = [];
38382
38689
  let at = path;
38383
38690
  for (;; ) {
38384
38691
  try {
38385
- return join20(realpathSync3.native(at), ...missing);
38692
+ return join21(realpathSync3.native(at), ...missing);
38386
38693
  } catch {
38387
38694
  const parent = dirname14(at);
38388
38695
  if (parent === at) {
@@ -38399,15 +38706,15 @@ function projectKey(projectPath) {
38399
38706
  return createHash4("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
38400
38707
  }
38401
38708
  function runRecordPath(projectPath, directory = runtimeDirectory()) {
38402
- return join20(directory, "runs", `note-${projectKey(projectPath)}.json`);
38709
+ return join21(directory, "runs", `note-${projectKey(projectPath)}.json`);
38403
38710
  }
38404
38711
  function openTranscript(startedAt) {
38405
38712
  mkdirSync11(runsDirectory(), { recursive: true });
38406
- const path = join20(runsDirectory(), `run-${startedAt}.log`);
38713
+ const path = join21(runsDirectory(), `run-${startedAt}.log`);
38407
38714
  return { path, fd: openSync2(path, "a") };
38408
38715
  }
38409
38716
  function editorNotePath(projectPath, directory = runtimeDirectory()) {
38410
- return join20(directory, "runs", `editor-${projectKey(projectPath)}.json`);
38717
+ return join21(directory, "runs", `editor-${projectKey(projectPath)}.json`);
38411
38718
  }
38412
38719
  function writeEditorRunNote(note) {
38413
38720
  mkdirSync11(runsDirectory(), { recursive: true });
@@ -38415,7 +38722,7 @@ function writeEditorRunNote(note) {
38415
38722
  }
38416
38723
  function readEditorRunNote(projectPath) {
38417
38724
  for (const directory of runtimeDirectories()) {
38418
- const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join20(directory, "runs", "editor-run.json"));
38725
+ const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join21(directory, "runs", "editor-run.json"));
38419
38726
  if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
38420
38727
  return note;
38421
38728
  }
@@ -38447,7 +38754,7 @@ function readRunRecord(projectPath) {
38447
38754
  if (own !== null) {
38448
38755
  return own;
38449
38756
  }
38450
- const shared = recordAt(join20(directory, "runs", SHARED_NOTE));
38757
+ const shared = recordAt(join21(directory, "runs", SHARED_NOTE));
38451
38758
  if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
38452
38759
  return shared;
38453
38760
  }
@@ -38459,13 +38766,13 @@ function everyRunRecord() {
38459
38766
  for (const directory of runtimeDirectories()) {
38460
38767
  let entries;
38461
38768
  try {
38462
- entries = readdirSync7(join20(directory, "runs"));
38769
+ entries = readdirSync8(join21(directory, "runs"));
38463
38770
  } catch {
38464
38771
  continue;
38465
38772
  }
38466
38773
  for (const entry of entries) {
38467
38774
  if (entry === SHARED_NOTE || entry.startsWith("note-") && entry.endsWith(".json")) {
38468
- const record = recordAt(join20(directory, "runs", entry));
38775
+ const record = recordAt(join21(directory, "runs", entry));
38469
38776
  if (record !== null) {
38470
38777
  found.push(record);
38471
38778
  }
@@ -38503,49 +38810,47 @@ function recordAt(path) {
38503
38810
  ...typeof fields["exitSignal"] === "string" ? { exitSignal: fields["exitSignal"] } : {}
38504
38811
  };
38505
38812
  }
38506
- function runningAs(pid) {
38813
+ async function runningAs(pid) {
38507
38814
  if (process.platform === "win32") {
38508
- const answer = windowsCommandLine(pid);
38815
+ const answer = await windowsCommandLine(pid);
38509
38816
  if (answer !== null) {
38510
38817
  const [, ...rest] = answer.split(`
38511
38818
  `);
38512
38819
  const line = rest.join(`
38513
38820
  `).trim();
38514
- const began = startedAt(pid, answer);
38821
+ const began = await startedAt(pid, answer);
38515
38822
  if (line !== "") {
38516
38823
  return { kind: "commandLine", text: line, ...began === null ? {} : { startedAt: began } };
38517
38824
  }
38518
38825
  }
38519
- const image = windowsImage(pid);
38826
+ const image = await windowsImage(pid);
38520
38827
  return image === null ? null : { kind: "image", text: image };
38521
38828
  }
38522
38829
  try {
38523
- const began = startedAt(pid, null);
38830
+ const began = await startedAt(pid, null);
38524
38831
  const when = began === null ? {} : { startedAt: began };
38525
38832
  if (process.platform === "linux") {
38526
38833
  const raw = readFileSync18(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
38527
38834
  return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
38528
38835
  }
38529
- const args = execFileSync2("ps", ["-p", String(pid), "-o", "args="], {
38530
- encoding: "utf8",
38531
- timeout: 15000
38532
- }).trim();
38836
+ const args = (await execFileAsync("ps", ["-p", String(pid), "-o", "args="], { encoding: "utf8", timeout: 15000 })).stdout.trim();
38533
38837
  return args === "" ? null : { kind: "commandLine", text: args, ...when };
38534
38838
  } catch {
38535
38839
  return null;
38536
38840
  }
38537
38841
  }
38538
- function listeningPid(port) {
38842
+ async function listeningPid(port) {
38539
38843
  try {
38540
38844
  if (process.platform === "win32") {
38541
- return listeningPidInNetstat(execFileSync2("netstat", ["-ano"], { encoding: "utf8", timeout: 15000 }), port);
38845
+ const { stdout } = await execFileAsync("netstat", ["-ano"], { encoding: "utf8", timeout: 15000 });
38846
+ return listeningPidInNetstat(stdout, port);
38542
38847
  }
38543
38848
  for (const [command, args, pattern] of [
38544
38849
  ["lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], /^(\d+)/],
38545
38850
  ["ss", ["-ltnpH", `sport = :${port}`], /pid=(\d+)/]
38546
38851
  ]) {
38547
38852
  try {
38548
- const said = execFileSync2(command, [...args], { encoding: "utf8", timeout: 15000 }).trim();
38853
+ const said = (await execFileAsync(command, [...args], { encoding: "utf8", timeout: 15000 })).stdout.trim();
38549
38854
  const found = pattern.exec(said);
38550
38855
  if (found?.[1] !== undefined) {
38551
38856
  return Number(found[1]);
@@ -38570,39 +38875,31 @@ function listeningPidInNetstat(printed, port) {
38570
38875
  }
38571
38876
  return null;
38572
38877
  }
38573
- function windowsCommandLine(pid) {
38878
+ async function windowsCommandLine(pid) {
38574
38879
  try {
38575
- const answer = execFileSync2("powershell", [
38576
- "-NoProfile",
38577
- "-NonInteractive",
38578
- "-Command",
38579
- `${POWERSHELL_UTF8}$p = Get-CimInstance Win32_Process -Filter "ProcessId=${pid}"; if ($p) { $p.CreationDate.ToUniversalTime().ToString("o"); $p.CommandLine }`
38580
- ], { encoding: "utf8", timeout: 15000, windowsHide: true }).trim();
38880
+ const answer = (await askWindows(`$p = Get-CimInstance Win32_Process -Filter "ProcessId=${pid}"; if ($p) { $p.CreationDate.ToUniversalTime().ToString("o"); $p.CommandLine }`, 15000)).trim();
38581
38881
  return answer === "" ? null : answer;
38582
38882
  } catch {
38583
38883
  return null;
38584
38884
  }
38585
38885
  }
38586
- function startedAt(pid, windowsAnswer) {
38886
+ async function startedAt(pid, windowsAnswer) {
38587
38887
  if (process.platform === "win32") {
38588
38888
  const when = Date.parse(windowsAnswer?.split(`
38589
38889
  `)[0]?.trim() ?? "");
38590
38890
  return Number.isNaN(when) ? null : when;
38591
38891
  }
38592
38892
  try {
38593
- const said = execFileSync2("ps", ["-p", String(pid), "-o", "lstart="], {
38594
- encoding: "utf8",
38595
- timeout: 15000
38596
- }).trim();
38893
+ const said = (await execFileAsync("ps", ["-p", String(pid), "-o", "lstart="], { encoding: "utf8", timeout: 15000 })).stdout.trim();
38597
38894
  const when = Date.parse(said);
38598
38895
  return said === "" || Number.isNaN(when) ? null : when;
38599
38896
  } catch {
38600
38897
  return null;
38601
38898
  }
38602
38899
  }
38603
- function windowsImage(pid) {
38900
+ async function windowsImage(pid) {
38604
38901
  try {
38605
- const csv = execFileSync2("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
38902
+ const { stdout: csv } = await execFileAsync("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
38606
38903
  encoding: "utf8",
38607
38904
  timeout: 15000,
38608
38905
  windowsHide: true
@@ -38612,11 +38909,11 @@ function windowsImage(pid) {
38612
38909
  return null;
38613
38910
  }
38614
38911
  }
38615
- function stillTheRecordedRun(record) {
38616
- return judgeRun(record, runningAs(record.pid), "confirmed");
38912
+ async function stillTheRecordedRun(record) {
38913
+ return judgeRun(record, await runningAs(record.pid), "confirmed");
38617
38914
  }
38618
- function couldStillBeTheRecordedRun(record) {
38619
- return judgeRun(record, runningAs(record.pid), "possible");
38915
+ async function couldStillBeTheRecordedRun(record) {
38916
+ return judgeRun(record, await runningAs(record.pid), "possible");
38620
38917
  }
38621
38918
  function judgeRun(record, running, needed) {
38622
38919
  if (running === null) {
@@ -38643,8 +38940,8 @@ function judgeRun(record, running, needed) {
38643
38940
  const project = process.platform === "win32" ? record.projectPath.toLowerCase() : record.projectPath;
38644
38941
  return said.includes(project);
38645
38942
  }
38646
- function stillTheAnnouncedGame(pid, projectPath, announcedAt) {
38647
- return judgeAnnouncedGame(runningAs(pid), projectPath, announcedAt);
38943
+ async function stillTheAnnouncedGame(pid, projectPath, announcedAt) {
38944
+ return judgeAnnouncedGame(await runningAs(pid), projectPath, announcedAt);
38648
38945
  }
38649
38946
  function judgeAnnouncedGame(running, projectPath, announcedAt) {
38650
38947
  if (running?.kind !== "commandLine" || running.startedAt === undefined) {
@@ -38661,7 +38958,7 @@ function judgeAnnouncedGame(running, projectPath, announcedAt) {
38661
38958
  }
38662
38959
  function clearRunRecord(projectPath) {
38663
38960
  for (const directory of runtimeDirectories()) {
38664
- const shared = join20(directory, "runs", SHARED_NOTE);
38961
+ const shared = join21(directory, "runs", SHARED_NOTE);
38665
38962
  const sharedRecord = recordAt(shared);
38666
38963
  const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
38667
38964
  for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
@@ -38683,7 +38980,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
38683
38980
  const kept = new Set(everyRunRecord().map((record) => record.transcript));
38684
38981
  let entries;
38685
38982
  try {
38686
- entries = readdirSync7(directory);
38983
+ entries = readdirSync8(directory);
38687
38984
  } catch {
38688
38985
  return;
38689
38986
  }
@@ -38691,7 +38988,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
38691
38988
  if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
38692
38989
  continue;
38693
38990
  }
38694
- const path = join20(directory, entry);
38991
+ const path = join21(directory, entry);
38695
38992
  if (kept.has(path) || ageMs(path, now) < keepMs) {
38696
38993
  continue;
38697
38994
  }
@@ -38700,37 +38997,37 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
38700
38997
  } catch {}
38701
38998
  }
38702
38999
  }
38703
- var SHARED_NOTE = "run.json", SAME_RUN_WINDOW_MS = 1e4;
39000
+ var execFileAsync, SHARED_NOTE = "run.json", SAME_RUN_WINDOW_MS = 1e4;
38704
39001
  var init_run_record = __esm(() => {
38705
39002
  init_paths();
38706
39003
  init_process_children();
38707
39004
  init_runtime_client();
39005
+ init_windows_ask();
39006
+ execFileAsync = promisify5(execFile5);
38708
39007
  });
38709
39008
 
38710
39009
  // src/server.ts
38711
- import { execFile as execFile5, spawn as spawn2 } from "node:child_process";
39010
+ import { execFile as execFile6, spawn as spawn3 } from "node:child_process";
38712
39011
  import { randomUUID as randomUUID2 } from "node:crypto";
38713
39012
  import {
38714
39013
  appendFileSync,
38715
39014
  closeSync as closeSync2,
38716
39015
  constants,
38717
39016
  copyFileSync,
38718
- existsSync as existsSync14,
39017
+ existsSync as existsSync15,
38719
39018
  fstatSync,
38720
39019
  mkdirSync as mkdirSync12,
38721
- mkdtempSync as mkdtempSync2,
38722
39020
  openSync as openSync3,
38723
- readdirSync as readdirSync8,
39021
+ readdirSync as readdirSync9,
38724
39022
  readFileSync as readFileSync19,
38725
39023
  readSync as readSync2,
38726
39024
  rmdirSync as rmdirSync2,
38727
39025
  statSync as statSync8
38728
39026
  } from "node:fs";
38729
- import { tmpdir as tmpdir4 } from "node:os";
38730
- import { basename as basename5, dirname as dirname15, extname as extname2, isAbsolute as isAbsolute3, join as join21, normalize as normalize2, relative as relative3 } from "node:path";
39027
+ import { basename as basename5, dirname as dirname15, extname as extname2, isAbsolute as isAbsolute3, join as join22, normalize as normalize2, relative as relative3 } from "node:path";
38731
39028
  import { setTimeout as delay3 } from "node:timers/promises";
38732
39029
  import { fileURLToPath as fileURLToPath5 } from "node:url";
38733
- import { promisify as promisify5 } from "node:util";
39030
+ import { promisify as promisify6 } from "node:util";
38734
39031
  function captureDestinationRefusal(outputPath, projects) {
38735
39032
  if (/^(res|user):\/\//.test(outputPath)) {
38736
39033
  return `outputPath ${outputPath} is a Godot path; a capture is saved outside the project, at an absolute path on this machine.`;
@@ -38742,15 +39039,15 @@ function captureDestinationRefusal(outputPath, projects) {
38742
39039
  return `outputPath ${outputPath} does not end in .png, which is what a capture is.`;
38743
39040
  }
38744
39041
  const directory = realPathOr(dirname15(outputPath));
38745
- if (!existsSync14(directory)) {
39042
+ if (!existsSync15(directory)) {
38746
39043
  return `outputPath's directory ${dirname15(outputPath)} does not exist; a capture creates no directories.`;
38747
39044
  }
38748
- const where = join21(directory, basename5(outputPath));
39045
+ const where = join22(directory, basename5(outputPath));
38749
39046
  const inside = projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
38750
39047
  if (inside !== undefined) {
38751
39048
  return `outputPath ${outputPath} is inside the project at ${inside}; a capture is saved outside it, so it never becomes part of the game.`;
38752
39049
  }
38753
- if (existsSync14(where)) {
39050
+ if (existsSync15(where)) {
38754
39051
  return `outputPath ${outputPath} already exists; a capture is not written over a file. Name a new one.`;
38755
39052
  }
38756
39053
  return null;
@@ -38906,7 +39203,8 @@ function aboutTheChildren(ended, throughEditor) {
38906
39203
  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}`;
38907
39204
  }
38908
39205
  if (ended.unknown === "platform") {
38909
- return "andChildren was asked for and this platform would not list what the game had started, so nothing else was ended.";
39206
+ const why = whyTheProcessTreeFailed();
39207
+ 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}.`}`;
38910
39208
  }
38911
39209
  if (ended.unknown === "no process") {
38912
39210
  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.";
@@ -38987,17 +39285,6 @@ function runtimeVerdict(endpoint, after) {
38987
39285
  heldAt: null
38988
39286
  };
38989
39287
  }
38990
- function alive(pid) {
38991
- if (pid === undefined || pid === null) {
38992
- return false;
38993
- }
38994
- try {
38995
- process.kill(pid, 0);
38996
- return true;
38997
- } catch (error) {
38998
- return error.code === "EPERM";
38999
- }
39000
- }
39001
39288
  function isOfType(value, kind) {
39002
39289
  switch (kind) {
39003
39290
  case "string":
@@ -39189,13 +39476,13 @@ function aboveTheRunner(entry) {
39189
39476
  }
39190
39477
  function elsewhereIn(projectPath, asked) {
39191
39478
  const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
39192
- if (wanted === "" || existsSync14(join21(projectPath, wanted))) {
39479
+ if (wanted === "" || existsSync15(join22(projectPath, wanted))) {
39193
39480
  return "";
39194
39481
  }
39195
39482
  const root = (wanted.split("/")[0] ?? "").toLowerCase();
39196
39483
  let near = [];
39197
39484
  try {
39198
- 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()));
39485
+ near = readdirSync9(projectPath, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.toLowerCase() !== root).map((entry) => entry.name).filter((name) => name.toLowerCase().startsWith("test") || root.startsWith(name.toLowerCase()));
39199
39486
  } catch {
39200
39487
  return "";
39201
39488
  }
@@ -39211,7 +39498,7 @@ function runtimeAddonAnnouncesEditors(projectPath) {
39211
39498
  return false;
39212
39499
  }
39213
39500
  try {
39214
- return readFileSync19(join21(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
39501
+ return readFileSync19(join22(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
39215
39502
  } catch {
39216
39503
  return false;
39217
39504
  }
@@ -39236,7 +39523,7 @@ function needsItsOwnProcess(asked) {
39236
39523
  class GodotServer {
39237
39524
  mcp;
39238
39525
  locator = new GodotLocator;
39239
- operationsScript = join21(__dirname2, "godot", "operations", "godot_operations.gd");
39526
+ operationsScript = join22(__dirname2, "godot", "operations", "godot_operations.gd");
39240
39527
  godotBridge;
39241
39528
  tools = buildToolDefinitions();
39242
39529
  activeProcess = null;
@@ -39248,6 +39535,7 @@ class GodotServer {
39248
39535
  lastGodotVersion = null;
39249
39536
  lspClient = null;
39250
39537
  dapClient = null;
39538
+ adapterPassed = null;
39251
39539
  breakpointsSeededOn = null;
39252
39540
  bridgeStartupError = null;
39253
39541
  bridgeRetry = null;
@@ -39306,6 +39594,7 @@ class GodotServer {
39306
39594
  this.handleShutdown("stdin:end", 0);
39307
39595
  });
39308
39596
  console.error("gdharness running on stdio");
39597
+ sweepAbandonedScratch();
39309
39598
  try {
39310
39599
  await this.takeOverFromAPredecessor();
39311
39600
  await this.godotBridge.start();
@@ -39632,7 +39921,7 @@ class GodotServer {
39632
39921
  this.callsSinceFeedback = 0;
39633
39922
  return {
39634
39923
  ...answer,
39635
- content: [...answer.content, { type: "text", text: feedbackNotice() }]
39924
+ content: [...answer.content, { type: "text", text: feedbackNotice(FEEDBACK_NOTICE_EVERY) }]
39636
39925
  };
39637
39926
  }
39638
39927
  validateArguments(spec, args) {
@@ -39976,8 +40265,8 @@ class GodotServer {
39976
40265
  if (path === undefined) {
39977
40266
  return { ok: false, response: this.createErrorResponse("projectPath is required.") };
39978
40267
  }
39979
- const file = join21(path, "project.godot");
39980
- if (!existsSync14(file)) {
40268
+ const file = join22(path, "project.godot");
40269
+ if (!existsSync15(file)) {
39981
40270
  return {
39982
40271
  ok: false,
39983
40272
  response: this.createErrorResponse(`Not a Godot project: ${path}`, [
@@ -40138,13 +40427,13 @@ class GodotServer {
40138
40427
  let sawAScan = false;
40139
40428
  let scanEndedAt = null;
40140
40429
  for (;; ) {
40141
- const asked = Date.now();
40142
40430
  let status;
40143
40431
  try {
40144
40432
  status = asParams(await this.godotBridge.invokeTool("scan_status", {}));
40145
40433
  } catch {
40146
40434
  return { waitedMs: Date.now() - started, stillScanning: false };
40147
40435
  }
40436
+ const answered = Date.now();
40148
40437
  if (status["scanning"] === true || status["importing"] === true || status["pending"] === true) {
40149
40438
  sawAScan = true;
40150
40439
  if (Date.now() - started >= SCAN_WAIT_MS) {
@@ -40154,7 +40443,7 @@ class GodotServer {
40154
40443
  continue;
40155
40444
  }
40156
40445
  const since = readNumber(status, "sinceScanFinishedMs");
40157
- scanEndedAt = since !== undefined && since >= 0 ? asked - since : sawAScan ? asked : null;
40446
+ scanEndedAt = since !== undefined && since >= 0 ? answered - since : sawAScan ? answered : null;
40158
40447
  break;
40159
40448
  }
40160
40449
  let waitedForTheWrite = false;
@@ -40573,11 +40862,11 @@ class GodotServer {
40573
40862
  return this.createErrorResponse(`Cannot create ${dirname15(output.relativePath)} to export into: ${errorMessage(error)}`);
40574
40863
  }
40575
40864
  const debug = readBoolean(args, "debug") ?? false;
40576
- const exportLogs = mkdtempSync2(join21(tmpdir4(), "gdharness-export-"));
40865
+ const exportLogs = scratchDirectory("export");
40577
40866
  const exportArgs = [
40578
40867
  "--headless",
40579
40868
  "--log-file",
40580
- join21(exportLogs, "engine.log"),
40869
+ join22(exportLogs, "engine.log"),
40581
40870
  "--path",
40582
40871
  project.value.path,
40583
40872
  debug ? "--export-debug" : "--export-release",
@@ -40609,7 +40898,7 @@ class GodotServer {
40609
40898
  log.finish();
40610
40899
  const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
40611
40900
  const verdict = {
40612
- exported: ending.exitCode === 0 && log.count("error") === 0 && existsSync14(output.absolutePath),
40901
+ exported: ending.exitCode === 0 && log.count("error") === 0 && existsSync15(output.absolutePath),
40613
40902
  preset,
40614
40903
  outputPath: output.relativePath,
40615
40904
  debug,
@@ -40645,7 +40934,7 @@ class GodotServer {
40645
40934
  return contained.response;
40646
40935
  }
40647
40936
  const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
40648
- if (!existsSync14(join21(project.value.path, runner))) {
40937
+ if (!existsSync15(join22(project.value.path, runner))) {
40649
40938
  return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
40650
40939
  "Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
40651
40940
  ]);
@@ -40681,7 +40970,7 @@ class GodotServer {
40681
40970
  ];
40682
40971
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
40683
40972
  this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
40684
- const userData = mkdtempSync2(join21(tmpdir4(), "gdharness-tests-"));
40973
+ const userData = scratchDirectory("tests");
40685
40974
  const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
40686
40975
  const console2 = [];
40687
40976
  let lastSaid = Date.now();
@@ -40708,14 +40997,17 @@ class GodotServer {
40708
40997
  resolve(false);
40709
40998
  });
40710
40999
  });
40711
- const reportsDir = join21(project.value.path, ".godot", ...ours.split("/"));
41000
+ if (timedOut && run.process.pid !== undefined) {
41001
+ await untilGone([run.process.pid]);
41002
+ }
41003
+ const reportsDir = join22(project.value.path, ".godot", ...ours.split("/"));
40712
41004
  let report = null;
40713
41005
  let reportProblem = null;
40714
41006
  try {
40715
- const written = existsSync14(reportsDir) ? readdirSync8(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
41007
+ const written = existsSync15(reportsDir) ? readdirSync9(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
40716
41008
  const newest = written.at(-1);
40717
41009
  if (newest !== undefined) {
40718
- report = parseJUnit(readFileSync19(join21(reportsDir, newest, "results.xml"), "utf8"));
41010
+ report = parseJUnit(readFileSync19(join22(reportsDir, newest, "results.xml"), "utf8"));
40719
41011
  }
40720
41012
  } catch (error) {
40721
41013
  reportProblem = errorMessage(error);
@@ -40873,7 +41165,7 @@ class GodotServer {
40873
41165
  const cached = cachedClasses(projectPath);
40874
41166
  const contradicted = cached === null ? [] : contradictedDiagnostics(messages, cached, (resourcePath) => {
40875
41167
  const contained = resolveWithinProject(projectPath, resourcePath);
40876
- if (!contained.ok || !existsSync14(contained.absolutePath)) {
41168
+ if (!contained.ok || !existsSync15(contained.absolutePath)) {
40877
41169
  return null;
40878
41170
  }
40879
41171
  try {
@@ -40927,7 +41219,7 @@ class GodotServer {
40927
41219
  }
40928
41220
  sourceOfClass(projectPath, resourcePath) {
40929
41221
  const contained = resolveWithinProject(projectPath, resourcePath);
40930
- if (!contained.ok || !existsSync14(contained.absolutePath)) {
41222
+ if (!contained.ok || !existsSync15(contained.absolutePath)) {
40931
41223
  return null;
40932
41224
  }
40933
41225
  try {
@@ -40948,28 +41240,32 @@ class GodotServer {
40948
41240
  async handleDAP(toolName, args) {
40949
41241
  return handleDAPTool(this.dap(), toolName, args);
40950
41242
  }
40951
- adapterBelongsElsewhere() {
41243
+ async adapterBelongsElsewhere() {
40952
41244
  const status = this.godotBridge.getStatus();
40953
41245
  const ours = status.editorPid;
40954
41246
  if (ours === undefined) {
40955
41247
  return null;
40956
41248
  }
41249
+ const connection = this.dapClient?.connection() ?? null;
41250
+ if (connection !== null && this.adapterPassed?.connection === connection && this.adapterPassed.editorPid === ours) {
41251
+ return null;
41252
+ }
40957
41253
  const port = this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT);
40958
- const holder = listeningPid(port);
41254
+ const askedAt = Date.now();
41255
+ const holder = await listeningPid(port);
41256
+ this.logDebug(`Asked who holds debug adapter port ${port} in ${Date.now() - askedAt}ms: ${holder ?? "not said"}`);
40959
41257
  if (holder === null || holder === ours) {
41258
+ this.adapterPassed = connection === null ? null : { connection, editorPid: ours };
40960
41259
  return null;
40961
41260
  }
40962
41261
  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.`;
40963
41262
  }
40964
- anEditorIsStillComing(listeningSince) {
40965
- return anEditorIsStillComing(listeningSince, this.launchedEditorIsUp());
40966
- }
40967
- launchedEditorIsUp() {
41263
+ async launchedEditorIsUp() {
40968
41264
  const launched = this.launchedEditor;
40969
41265
  if (launched === null || !alive(launched.pid)) {
40970
41266
  return false;
40971
41267
  }
40972
- const now = startTimesOf([launched.pid]).get(launched.pid);
41268
+ const now = (await startTimesOf([launched.pid])).get(launched.pid);
40973
41269
  return launched.startedAt === null || now === undefined || now === launched.startedAt;
40974
41270
  }
40975
41271
  dap() {
@@ -41058,8 +41354,9 @@ class GodotServer {
41058
41354
  }
41059
41355
  return owed;
41060
41356
  }
41061
- getEditorStatusPayload() {
41357
+ async getEditorStatusPayload() {
41062
41358
  const status = this.godotBridge.getStatus();
41359
+ const launchedIsUp = !status.connected && await this.launchedEditorIsUp();
41063
41360
  const isPortConflict = this.bridgeStartupError?.includes("EADDRINUSE") ?? false;
41064
41361
  const stale = status.connected && editorIsStale(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest());
41065
41362
  const unfinished = this.restartLeftUnfinished(status.connected);
@@ -41068,14 +41365,14 @@ class GodotServer {
41068
41365
  serverVersion: SERVER_VERSION,
41069
41366
  addonIsStale: status.connected ? stale : undefined,
41070
41367
  bridgeAvailable: this.bridgeStartupError === null,
41071
- mayYetConnect: status.connected ? undefined : unfinished !== null ? false : this.anEditorIsStillComing(status.listeningSince),
41368
+ mayYetConnect: status.connected ? undefined : unfinished !== null ? false : anEditorIsStillComing(status.listeningSince, launchedIsUp),
41072
41369
  restartInterrupted: unfinished === null ? undefined : {
41073
41370
  quitEditorPid: unfinished.editorPid,
41074
41371
  quitAt: unfinished.quitAt,
41075
41372
  byServerPid: unfinished.byPid,
41076
41373
  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."
41077
41374
  },
41078
- awaitingLaunchedEditor: status.connected || !this.launchedEditorIsUp() ? undefined : this.launchedEditor?.pid ?? undefined,
41375
+ awaitingLaunchedEditor: status.connected || !launchedIsUp ? undefined : this.launchedEditor?.pid ?? undefined,
41079
41376
  ...status.connected ? this.whatTheLaunchedEditorDropped() : {},
41080
41377
  startupError: this.bridgeStartupError,
41081
41378
  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.`,
@@ -41164,7 +41461,7 @@ class GodotServer {
41164
41461
  const asked = (async () => {
41165
41462
  const playing = await this.editorPlayingState();
41166
41463
  await this.pickUpWhatTheEditorIsPlaying();
41167
- const current = this.currentRun();
41464
+ const current = await this.currentRun();
41168
41465
  const active = current === null ? false : await this.runStillGoing(current, playing?.playing ?? null);
41169
41466
  return {
41170
41467
  playing,
@@ -41172,14 +41469,15 @@ class GodotServer {
41172
41469
  hold: current === null ? {} : await this.holdOf(current, active)
41173
41470
  };
41174
41471
  })();
41175
- const [games, { playing, active, hold }, scan] = await Promise.all([
41472
+ const [games, { playing, active, hold }, scan, editorStatus] = await Promise.all([
41176
41473
  pinged,
41177
41474
  asked,
41178
- this.editorScanState()
41475
+ this.editorScanState(),
41476
+ this.getEditorStatusPayload()
41179
41477
  ]);
41180
41478
  return this.jsonTextResponse({
41181
41479
  editor: {
41182
- ...this.getEditorStatusPayload(),
41480
+ ...editorStatus,
41183
41481
  debugPort: playing?.debugPort ?? this.godotBridge.getStatus().debugPort,
41184
41482
  scan
41185
41483
  },
@@ -41209,7 +41507,7 @@ class GodotServer {
41209
41507
  return null;
41210
41508
  }
41211
41509
  try {
41212
- return settingKeys(readFileSync19(join21(projectPath, "project.godot"), "utf8"));
41510
+ return settingKeys(readFileSync19(join22(projectPath, "project.godot"), "utf8"));
41213
41511
  } catch {
41214
41512
  return null;
41215
41513
  }
@@ -41358,7 +41656,7 @@ class GodotServer {
41358
41656
  writeEditorLogNote(projectPath, launched.pid);
41359
41657
  this.launchedEditor = {
41360
41658
  pid: launched.pid,
41361
- startedAt: startTimesOf([launched.pid]).get(launched.pid) ?? null
41659
+ startedAt: (await startTimesOf([launched.pid])).get(launched.pid) ?? null
41362
41660
  };
41363
41661
  return { pid: launched.pid, error: null };
41364
41662
  }
@@ -41406,13 +41704,15 @@ class GodotServer {
41406
41704
  }
41407
41705
  await this.pickUpWhatTheEditorIsPlaying();
41408
41706
  let ended = null;
41409
- const before = this.currentRun();
41707
+ const before = await this.currentRun();
41410
41708
  if (before !== null && await this.runStillGoing(before)) {
41411
41709
  this.logDebug("Ending the running game before starting another");
41710
+ const endingFrom = Date.now();
41412
41711
  ended = {
41413
41712
  pid: before.pid ?? this.announcedPidOf(before) ?? null,
41414
41713
  ending: await this.endActiveGame("editor_run start, which ends the run that was going before it starts another", true)
41415
41714
  };
41715
+ this.logDebug(`The running game was ${ended.ending} after ${Date.now() - endingFrom}ms`);
41416
41716
  }
41417
41717
  const alreadyPlaying = new Set(discoverRuntimes().map((endpoint) => endpoint.pid));
41418
41718
  const wereHereFirst = this.everyRuntimeOfOurs();
@@ -41462,7 +41762,7 @@ class GodotServer {
41462
41762
  }
41463
41763
  async runtimeUp(projectPath, before, budget, withArgs) {
41464
41764
  const budgetMs = budget.ms;
41465
- if (!existsSync14(join21(projectPath, RUNTIME_AUTOLOAD.path))) {
41765
+ if (!existsSync15(join22(projectPath, RUNTIME_AUTOLOAD.path))) {
41466
41766
  return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
41467
41767
  }
41468
41768
  let asking = null;
@@ -41473,12 +41773,12 @@ class GodotServer {
41473
41773
  const lastSaid = () => editorSaysGoing ? "playing" : "not playing";
41474
41774
  const endpoint = await announcedSince(projectPath, before, {
41475
41775
  budgetMs,
41476
- accept: (one) => this.isTheGameOf(this.currentRun(), one, projectPath),
41776
+ accept: (one) => this.isTheGameOf(this.activeProcess, one, projectPath),
41477
41777
  giveUp: () => {
41478
41778
  if (this.dapClient?.isStopped() === true) {
41479
41779
  return true;
41480
41780
  }
41481
- const run = this.currentRun();
41781
+ const run = this.activeProcess;
41482
41782
  if (run === null || !run.throughEditor) {
41483
41783
  return !stillRunning(run);
41484
41784
  }
@@ -41496,7 +41796,7 @@ class GodotServer {
41496
41796
  });
41497
41797
  const asked = editorAsks === 0 ? "editor not asked" : `editor asked ${editorAsks === 1 ? "once" : `${editorAsks} times`}, last said ${lastSaid()}`;
41498
41798
  this.logDebug(`announce wait ended after ${Date.now() - waitBegan}ms: ${endpoint === null ? "nothing announced" : `pid ${endpoint.pid} announced`}, ${asked}`);
41499
- const going = this.currentRun();
41799
+ const going = await this.currentRun();
41500
41800
  if (endpoint !== null && going !== null && going.announcedPid === undefined) {
41501
41801
  this.tie(going, endpoint);
41502
41802
  }
@@ -41537,8 +41837,10 @@ class GodotServer {
41537
41837
  }
41538
41838
  async playThroughEditor(scene, refreshedClasses, scanned, projectPath, alreadyPlaying, runtimeWaitMs, ended) {
41539
41839
  const log = new GameLog;
41840
+ const connectingFrom = Date.now();
41540
41841
  try {
41541
41842
  await this.dap().connect();
41843
+ this.logDebug(`The debug adapter was connected after ${Date.now() - connectingFrom}ms`);
41542
41844
  this.dapClient?.getOutput(true);
41543
41845
  } catch (error) {
41544
41846
  return this.createErrorResponse(`The editor is connected but its debug adapter is not: ${errorMessage(error)}`, [
@@ -41546,7 +41848,7 @@ class GodotServer {
41546
41848
  "GDHARNESS_DAP_PORT points this server at another one"
41547
41849
  ]);
41548
41850
  }
41549
- const stranger = this.adapterBelongsElsewhere();
41851
+ const stranger = await this.adapterBelongsElsewhere();
41550
41852
  if (stranger !== null) {
41551
41853
  return this.createErrorResponse(stranger, [
41552
41854
  "editor_status names the editor on the bridge and the port it says it serves",
@@ -41554,7 +41856,9 @@ class GodotServer {
41554
41856
  "Closing the other editor, or moving its debug adapter port in its editor settings, frees this one"
41555
41857
  ]);
41556
41858
  }
41859
+ const sendingFrom = Date.now();
41557
41860
  const breakpoints = await this.dapHoldingBreakpointsOf(projectPath).reapplyBreakpoints();
41861
+ this.logDebug(`Breakpoints were sent again after ${Date.now() - sendingFrom}ms`);
41558
41862
  const answer = await this.handleViaBridge("play_scene", scene === null ? {} : { scenePath: `res://${scene}` });
41559
41863
  if (answer.isError === true) {
41560
41864
  return answer;
@@ -41740,13 +42044,13 @@ class GodotServer {
41740
42044
  arguments: [],
41741
42045
  ...running.command === undefined ? {} : { command: running.command }
41742
42046
  };
41743
- if (!stillTheRecordedRun(startedAs)) {
42047
+ if (!await stillTheRecordedRun(startedAs)) {
41744
42048
  clearTheRecord();
41745
42049
  running.endedHere = null;
41746
42050
  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.`);
41747
42051
  return "refused";
41748
42052
  }
41749
- const signalled = runningAs(running.pid);
42053
+ const signalled = await runningAs(running.pid);
41750
42054
  const described = signalled === null ? "nothing it would name" : `${signalled.kind}: ${signalled.text}`;
41751
42055
  let landed = true;
41752
42056
  try {
@@ -41767,7 +42071,7 @@ class GodotServer {
41767
42071
  return gone ? "gone" : "lingering";
41768
42072
  }
41769
42073
  spawnGame(godotPath, cmdArgs, env) {
41770
- const child = spawn2(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
42074
+ const child = spawn3(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
41771
42075
  const cancelled = callSignal();
41772
42076
  if (cancelled !== undefined) {
41773
42077
  const end = () => {
@@ -41934,8 +42238,8 @@ class GodotServer {
41934
42238
  }
41935
42239
  return run.announcedPid;
41936
42240
  }
41937
- ownGameAmong(running, projectPath) {
41938
- const run = this.currentRun();
42241
+ async ownGameAmong(running, projectPath) {
42242
+ const run = await this.currentRun();
41939
42243
  if (run === null || !stillRunning(run)) {
41940
42244
  return;
41941
42245
  }
@@ -41946,7 +42250,7 @@ class GodotServer {
41946
42250
  return its !== undefined && running.some((one) => one.pid === its) ? its : undefined;
41947
42251
  }
41948
42252
  async ownGameThroughTheTree(running, projectPath) {
41949
- const run = this.currentRun();
42253
+ const run = await this.currentRun();
41950
42254
  if (run === null || !stillRunning(run) || run.announcedPid !== undefined) {
41951
42255
  return;
41952
42256
  }
@@ -42032,7 +42336,7 @@ class GodotServer {
42032
42336
  const editor = this.godotBridge.getStatus().editorPid;
42033
42337
  return endpoint.editorPid === undefined || endpoint.editorPid !== editor;
42034
42338
  }
42035
- adoptRecordedRun() {
42339
+ async adoptRecordedRun() {
42036
42340
  const project = this.ourProject();
42037
42341
  const record = project === null ? null : readRunRecord(project);
42038
42342
  if (record === null || !this.couldBeOurs(record.projectPath)) {
@@ -42053,7 +42357,7 @@ class GodotServer {
42053
42357
  pickedUp: true
42054
42358
  };
42055
42359
  this.drainTranscript(adopted);
42056
- if (!alive(record.pid) || !couldStillBeTheRecordedRun(record)) {
42360
+ if (!alive(record.pid) || !await couldStillBeTheRecordedRun(record)) {
42057
42361
  adopted.log.finish();
42058
42362
  adopted.exitCode = record.exitCode ?? null;
42059
42363
  adopted.exitSignal = record.exitSignal ?? null;
@@ -42134,8 +42438,8 @@ class GodotServer {
42134
42438
  const status = this.godotBridge.getStatus();
42135
42439
  return this.ownProject ?? (status.connected ? status.projectPath ?? null : null);
42136
42440
  }
42137
- currentRun() {
42138
- return this.activeProcess ?? this.adoptRecordedRun();
42441
+ async currentRun() {
42442
+ return this.activeProcess ?? await this.adoptRecordedRun();
42139
42443
  }
42140
42444
  async checkBoot(godotPath, projectPath, scene, args, userArgs, scanned, asked = {}) {
42141
42445
  const frames = readPositiveNumber(args, "frames") ?? 3;
@@ -42174,10 +42478,14 @@ class GodotServer {
42174
42478
  }
42175
42479
  async handleWaitForRun(args) {
42176
42480
  await this.pickUpWhatTheEditorIsPlaying();
42177
- const run = this.currentRun();
42481
+ const run = await this.currentRun();
42178
42482
  if (!run) {
42179
42483
  return this.createErrorResponse(this.nothingOfOursIsRunning());
42180
42484
  }
42485
+ const elsewhere = this.anotherProjectsRun(run.projectPath, args, "waited on");
42486
+ if (elsewhere !== null) {
42487
+ return elsewhere;
42488
+ }
42181
42489
  const budgetMs = readPositiveNumber(args, "timeoutMs") ?? WAIT_FOR_RUN_MS;
42182
42490
  const until = Date.now() + budgetMs;
42183
42491
  const cancelled = callSignal();
@@ -42224,7 +42532,7 @@ class GodotServer {
42224
42532
  await delay3(50);
42225
42533
  }
42226
42534
  }
42227
- handleEditorConsole(args) {
42535
+ async handleEditorConsole(args) {
42228
42536
  const status = this.godotBridge.getStatus();
42229
42537
  const projectPath = status.projectPath ?? this.ownProject ?? undefined;
42230
42538
  if (projectPath === undefined) {
@@ -42261,10 +42569,11 @@ class GodotServer {
42261
42569
  const filtered = severity !== "info" || contains !== undefined;
42262
42570
  const grouped = bursts(everything, readNonNegativeNumber(args, "before") ?? 3, filtered ? (entry) => answersTo(entry, severity, contains) : undefined);
42263
42571
  const shown = grouped.slice(0, MOST_GROUPS);
42572
+ const stillComing = !status.connected && await this.launchedEditorIsUp();
42264
42573
  return this.jsonTextResponse({
42265
42574
  editorPid: editorPid ?? undefined,
42266
42575
  projectPath,
42267
- ...status.connected ? {} : this.launchedEditorIsUp() ? { stillConnecting: true } : { exitedBeforeConnecting: true },
42576
+ ...status.connected ? {} : stillComing ? { stillConnecting: true } : { exitedBeforeConnecting: true },
42268
42577
  capturedIn: console2.path,
42269
42578
  ...console2.of === undefined ? {} : {
42270
42579
  readFromTheStart: console2.readBytes,
@@ -42288,7 +42597,7 @@ class GodotServer {
42288
42597
  return this.handleEditorConsole(args);
42289
42598
  }
42290
42599
  await this.pickUpWhatTheEditorIsPlaying();
42291
- const run = this.currentRun();
42600
+ const run = await this.currentRun();
42292
42601
  if (!run) {
42293
42602
  return this.createErrorResponse(this.nothingOfOursIsRunning());
42294
42603
  }
@@ -42327,7 +42636,7 @@ class GodotServer {
42327
42636
  notes.push(endedWithoutACode({
42328
42637
  throughEditor: run.throughEditor,
42329
42638
  announced: announced !== undefined || run.announcedPid !== undefined,
42330
- couldAnnounce: run.projectPath !== null && existsSync14(join21(run.projectPath, RUNTIME_AUTOLOAD.path)),
42639
+ couldAnnounce: run.projectPath !== null && existsSync15(join22(run.projectPath, RUNTIME_AUTOLOAD.path)),
42331
42640
  endedHere: typeof run.endedHere === "string",
42332
42641
  pickedUp: run.pickedUp === true
42333
42642
  }));
@@ -42365,16 +42674,27 @@ class GodotServer {
42365
42674
  entries: forAnswer(selected.entries)
42366
42675
  });
42367
42676
  }
42677
+ anotherProjectsRun(runsProject, args, act) {
42678
+ const named = readNonEmptyString(args, "projectPath");
42679
+ if (named === undefined || runsProject === null || isSameDirectory(runsProject, named)) {
42680
+ return null;
42681
+ }
42682
+ 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.`);
42683
+ }
42368
42684
  async handleStopProject(args) {
42369
42685
  await this.pickUpWhatTheEditorIsPlaying();
42370
- const stopped = this.currentRun();
42686
+ const stopped = await this.currentRun();
42371
42687
  const named = readNumber(args, "pid");
42372
42688
  if (named !== undefined && (stopped === null || named !== stopped.pid && named !== this.announcedPidOf(stopped))) {
42373
- return await this.stopAnnouncedGame(named, readBoolean(args, "andChildren") === true);
42689
+ return await this.stopAnnouncedGame(named, readBoolean(args, "andChildren") === true, args);
42374
42690
  }
42375
42691
  if (!stopped) {
42376
42692
  return this.createErrorResponse(this.nothingOfOursIsRunning());
42377
42693
  }
42694
+ const elsewhere = this.anotherProjectsRun(stopped.projectPath, args, "stopped");
42695
+ if (elsewhere !== null) {
42696
+ return elsewhere;
42697
+ }
42378
42698
  this.drainEditorOutput(stopped);
42379
42699
  this.drainTranscript(stopped);
42380
42700
  const wasRunning = await this.runStillGoing(stopped);
@@ -42416,16 +42736,22 @@ class GodotServer {
42416
42736
  entries: forAnswer(stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
42417
42737
  });
42418
42738
  }
42419
- async stopAnnouncedGame(pid, andChildren) {
42739
+ async stopAnnouncedGame(pid, andChildren, args) {
42420
42740
  const announced = runtimesAnnounced();
42421
42741
  const game = [
42422
42742
  ...this.allAnnouncedForOurProject(announced.running),
42423
42743
  ...this.allAnnouncedForOurProject(announced.unspoken)
42424
42744
  ].find((one) => one.pid === pid);
42745
+ if (game !== undefined) {
42746
+ const elsewhere = this.anotherProjectsRun(game.project.path, args, "signalled");
42747
+ if (elsewhere !== null) {
42748
+ return elsewhere;
42749
+ }
42750
+ }
42425
42751
  if (game === undefined) {
42426
42752
  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.`);
42427
42753
  }
42428
- const read = game.announcedAt === undefined ? null : stillTheAnnouncedGame(pid, game.project.path, game.announcedAt);
42754
+ const read = game.announcedAt === undefined ? null : await stillTheAnnouncedGame(pid, game.project.path, game.announcedAt);
42429
42755
  if (read === null) {
42430
42756
  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.`);
42431
42757
  }
@@ -42623,6 +42949,8 @@ class GodotServer {
42623
42949
  }
42624
42950
  const checked = busy ? { unseen: [] } : await this.classesTheEditorCannotSee(args);
42625
42951
  const unseen = checked.unseen;
42952
+ const broughtIn = blind.unseen.map((one) => one.className).filter((name) => !unseen.some((still) => still.className === name));
42953
+ const dependents = busy || projectPath === "" || broughtIn.length === 0 ? null : await this.reloadDependents(projectPath, broughtIn);
42626
42954
  const stillGone = lost.filter((name) => !restored.includes(name));
42627
42955
  const notes = [];
42628
42956
  if (busy) {
@@ -42634,13 +42962,16 @@ class GodotServer {
42634
42962
  } else if (unseen.length > 0) {
42635
42963
  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.");
42636
42964
  }
42965
+ if (dependents !== null && dependents.notReloaded.length > 0) {
42966
+ 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.`);
42967
+ }
42637
42968
  if (stillAtMissingPaths.length > 0) {
42638
42969
  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.`);
42639
42970
  } else if (dropped.length > 0) {
42640
42971
  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.`);
42641
42972
  }
42642
42973
  return this.jsonTextResponse({
42643
- ok: !busy && unseen.length === 0 && stillGone.length === 0 && stillAtMissingPaths.length === 0 && checked.unchecked === undefined,
42974
+ ok: !busy && unseen.length === 0 && stillGone.length === 0 && stillAtMissingPaths.length === 0 && (dependents === null || dependents.notReloaded.length === 0) && checked.unchecked === undefined,
42644
42975
  stillWorking: busy,
42645
42976
  waitedMs: Date.now() - started,
42646
42977
  waitedForEditorScanMs: waitedForEditorMs > 0 ? waitedForEditorMs : undefined,
@@ -42648,6 +42979,9 @@ class GodotServer {
42648
42979
  reloadedMethods: reloaded.methods,
42649
42980
  reloadProblem: reloaded.problem,
42650
42981
  unseenByEditor: unseen.length > 0 ? unseen : undefined,
42982
+ broughtIn: broughtIn.length > 0 ? broughtIn : undefined,
42983
+ dependentsReloaded: dependents !== null && dependents.reloaded.length > 0 ? dependents.reloaded : undefined,
42984
+ dependentsNotReloaded: dependents !== null && dependents.notReloaded.length > 0 ? dependents.notReloaded : undefined,
42651
42985
  cacheLost: lost.length > 0 ? lost : undefined,
42652
42986
  cacheRestored: restored.length > 0 ? restored : undefined,
42653
42987
  cacheDropped: dropped.length > 0 ? dropped : undefined,
@@ -42656,6 +42990,33 @@ class GodotServer {
42656
42990
  note: notes.length > 0 ? notes.join(" ") : undefined
42657
42991
  });
42658
42992
  }
42993
+ async reloadDependents(projectPath, classes) {
42994
+ const reloaded = [];
42995
+ const notReloaded = [];
42996
+ for (const { script: scriptPath, tool } of scriptsNaming(projectPath, classes)) {
42997
+ if (tool) {
42998
+ notReloaded.push({
42999
+ scriptPath,
43000
+ problem: "a @tool script, which runs inside the editor, so it is not reloaded from here; editor_launch restart recompiles it"
43001
+ });
43002
+ continue;
43003
+ }
43004
+ try {
43005
+ const answer = asParams(await this.godotBridge.invokeTool("reload_script", { scriptPath }));
43006
+ if (readArray(answer, "methods") === undefined) {
43007
+ notReloaded.push({
43008
+ scriptPath,
43009
+ problem: "the editor would not say what it has after reloading it"
43010
+ });
43011
+ } else {
43012
+ reloaded.push(scriptPath);
43013
+ }
43014
+ } catch (error) {
43015
+ notReloaded.push({ scriptPath, problem: errorMessage(error) });
43016
+ }
43017
+ }
43018
+ return { reloaded, notReloaded };
43019
+ }
42659
43020
  async rebuildCacheAfterLoss(projectPath, lost) {
42660
43021
  if (projectPath === "") {
42661
43022
  return [];
@@ -42838,9 +43199,9 @@ class GodotServer {
42838
43199
  return { heldAt: undefined, heldUnknown: true, heldNote: reply.message };
42839
43200
  }
42840
43201
  async ownRunOnItsWay(projectPath, budgetMs) {
42841
- const run = this.currentRun();
43202
+ const run = await this.currentRun();
42842
43203
  const project = projectPath ?? run?.projectPath ?? null;
42843
- if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync14(join21(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
43204
+ if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync15(join22(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
42844
43205
  return null;
42845
43206
  }
42846
43207
  const already = runtimesAnnounced().running;
@@ -42866,8 +43227,8 @@ class GodotServer {
42866
43227
  }
42867
43228
  const endpoint = await announcedSince(project, new Set(already.map((one) => one.pid)), {
42868
43229
  budgetMs: Math.min(budgetMs, remaining),
42869
- accept: (one) => this.isTheGameOf(this.currentRun(), one, project),
42870
- giveUp: () => this.currentRun() !== run || this.dapClient?.isStopped() === true || !stillRunning(run)
43230
+ accept: (one) => this.isTheGameOf(this.activeProcess, one, project),
43231
+ giveUp: () => this.activeProcess !== run || this.dapClient?.isStopped() === true || !stillRunning(run)
42871
43232
  });
42872
43233
  if (endpoint !== null) {
42873
43234
  const tiedMeanwhile = () => run.announcedPid !== undefined;
@@ -42876,7 +43237,7 @@ class GodotServer {
42876
43237
  }
42877
43238
  return null;
42878
43239
  }
42879
- if (this.currentRun() !== run || !await this.runStillGoing(run)) {
43240
+ if (await this.currentRun() !== run || !await this.runStillGoing(run)) {
42880
43241
  return null;
42881
43242
  }
42882
43243
  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.`;
@@ -42892,7 +43253,7 @@ class GodotServer {
42892
43253
  }
42893
43254
  const announced = runtimesAnnounced();
42894
43255
  const wanted = typeof projectPath === "string" ? projectPath : this.ownProject ?? undefined;
42895
- const own = typeof pid === "number" ? undefined : this.ownGameAmong(announced.running, wanted) ?? await this.ownGameThroughTheTree(announced.running, wanted);
43256
+ const own = typeof pid === "number" ? undefined : await this.ownGameAmong(announced.running, wanted) ?? await this.ownGameThroughTheTree(announced.running, wanted);
42896
43257
  const choice = chooseRuntime(announced.running, wanted, announced.unspoken, typeof pid === "number" ? pid : own);
42897
43258
  if ("problem" in choice) {
42898
43259
  return this.createErrorResponse(choice.problem);
@@ -42919,8 +43280,8 @@ class GodotServer {
42919
43280
  return this.createErrorResponse(refusal);
42920
43281
  }
42921
43282
  }
42922
- const screenshotDir = expectsScreenshot ? mkdtempSync2(join21(tmpdir4(), "gdharness-runtime-screenshot-")) : null;
42923
- const screenshotPath = screenshotDir ? join21(screenshotDir, "capture.png") : null;
43283
+ const screenshotDir = expectsScreenshot ? scratchDirectory("runtime-screenshot") : null;
43284
+ const screenshotPath = screenshotDir ? join22(screenshotDir, "capture.png") : null;
42924
43285
  try {
42925
43286
  const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs, !expectsScreenshot);
42926
43287
  if (!reply.ok && reply.requestId !== undefined) {
@@ -43066,6 +43427,7 @@ var init_server3 = __esm(() => {
43066
43427
  init_mcp();
43067
43428
  init_stdio2();
43068
43429
  init_types();
43430
+ init_alive();
43069
43431
  init_boot_note();
43070
43432
  init_breakpoint_note();
43071
43433
  init_bridge_announce();
@@ -43101,7 +43463,7 @@ var init_server3 = __esm(() => {
43101
43463
  init_setup();
43102
43464
  init_tool_definitions();
43103
43465
  init_update_check();
43104
- run5 = promisify5(execFile5);
43466
+ run5 = promisify6(execFile6);
43105
43467
  __dirname2 = dirname15(fileURLToPath5(import.meta.url));
43106
43468
  CONSOLE_WRAPPER = /_console(\.exe)?$/i;
43107
43469
  LONGEST_BOOT_NOTED_MS = 5 * 60000;
@@ -43151,14 +43513,14 @@ var init_server3 = __esm(() => {
43151
43513
  init_bridge_announce();
43152
43514
  init_errors();
43153
43515
  init_godot_path();
43154
- import { existsSync as existsSync15 } from "node:fs";
43155
- import { join as join22, resolve as resolve6 } from "node:path";
43516
+ import { existsSync as existsSync16 } from "node:fs";
43517
+ import { join as join23, resolve as resolve6 } from "node:path";
43156
43518
 
43157
43519
  // src/harnesses.ts
43158
43520
  import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
43159
43521
  import { homedir as homedir2 } from "node:os";
43160
43522
  import { dirname as dirname3, join as join4 } from "node:path";
43161
- import process3 from "node:process";
43523
+ import process4 from "node:process";
43162
43524
 
43163
43525
  // node_modules/yaml/dist/index.js
43164
43526
  var composer = require_composer();
@@ -43726,12 +44088,12 @@ var HARNESSES = [
43726
44088
  }
43727
44089
  ];
43728
44090
  function appDataRoot() {
43729
- return process3.env["APPDATA"] ?? join4(homedir2(), "AppData", "Roaming");
44091
+ return process4.env["APPDATA"] ?? join4(homedir2(), "AppData", "Roaming");
43730
44092
  }
43731
44093
  function homePath(path) {
43732
44094
  return typeof path === "string" ? join4(homedir2(), path) : join4(appDataRoot(), path.appData);
43733
44095
  }
43734
- function fileFor(harness, platform = process3.platform) {
44096
+ function fileFor(harness, platform = process4.platform) {
43735
44097
  if (typeof harness.file === "string") {
43736
44098
  return harness.file;
43737
44099
  }
@@ -43744,7 +44106,7 @@ function configPath(harness, projectPath) {
43744
44106
  }
43745
44107
  return homePath(file);
43746
44108
  }
43747
- function displayPath(harness, platform = process3.platform) {
44109
+ function displayPath(harness, platform = process4.platform) {
43748
44110
  const file = fileFor(harness, platform);
43749
44111
  if (typeof file !== "string") {
43750
44112
  return `%APPDATA%/${file.appData.replaceAll("\\", "/")}`;
@@ -44022,10 +44384,10 @@ init_headless();
44022
44384
  init_issues();
44023
44385
 
44024
44386
  // src/prompt.ts
44025
- import process5 from "node:process";
44387
+ import process8 from "node:process";
44026
44388
  import { createInterface } from "node:readline/promises";
44027
44389
  function interactive() {
44028
- return process5.stdin.isTTY && process5.stdout.isTTY;
44390
+ return process8.stdin.isTTY && process8.stdout.isTTY;
44029
44391
  }
44030
44392
 
44031
44393
  class Ask {
@@ -44034,7 +44396,7 @@ class Ask {
44034
44396
  if (!interactive()) {
44035
44397
  return fallback;
44036
44398
  }
44037
- this.line ??= createInterface({ input: process5.stdin, output: process5.stdout });
44399
+ this.line ??= createInterface({ input: process8.stdin, output: process8.stdout });
44038
44400
  const answer = (await this.line.question(`${question} ${fallback ? "[Y/n]" : "[y/N]"} `)).trim();
44039
44401
  return answer === "" ? fallback : answer.toLowerCase().startsWith("y");
44040
44402
  }
@@ -44051,8 +44413,8 @@ init_setup();
44051
44413
  // src/skill.ts
44052
44414
  init_setup();
44053
44415
  init_tool_definitions();
44054
- import { existsSync as existsSync8, mkdirSync as mkdirSync5, readdirSync as readdirSync4, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "node:fs";
44055
- import { basename as basename2, dirname as dirname5, join as join9 } from "node:path";
44416
+ import { existsSync as existsSync9, mkdirSync as mkdirSync5, readdirSync as readdirSync5, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "node:fs";
44417
+ import { basename as basename2, dirname as dirname5, join as join10 } from "node:path";
44056
44418
 
44057
44419
  // src/tool-reference.ts
44058
44420
  init_headless_operations();
@@ -44148,7 +44510,7 @@ function renderToolsMarkdown() {
44148
44510
 
44149
44511
  // src/skill.ts
44150
44512
  var SKILL_NAME = "gdharness";
44151
- var SHARED_SKILLS = join9(".agents", "skills");
44513
+ var SHARED_SKILLS = join10(".agents", "skills");
44152
44514
  function skillMarkdown(version) {
44153
44515
  return `---
44154
44516
  name: ${SKILL_NAME}
@@ -44226,6 +44588,8 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
44226
44588
  | Answer a dialog | \`runtime_input click\` on its button, or \`runtime_input key\` with \`keycode\` \`Escape\` to dismiss it: an [AcceptDialog] reads that key itself and never asks the InputMap, so \`ui_cancel\` leaves it standing |
44227
44589
  | Press a bound key | \`runtime_input action\` with the action's name, or \`runtime_input key\` with \`keycode\`, a whole press unless you hold it |
44228
44590
  | Where a 3D node is on screen | \`runtime_inspect\` \`rect\`, rather than unprojecting by hand |
44591
+ | Wait for whichever of several screens comes next | \`runtime_wait until\` with \`says\` naming each, separated by \`|\`: \`"*won*|*lost*|*turn 3*"\` is met by the first of them to appear. Read as one glob, the bars were characters no screen says and the wait ran out its whole timeout |
44592
+ | Ask the game a question its own code answers | \`runtime_invoke call\` on the method that answers it, reached through what a node holds (\`_ladder:encounter:opponent:most_answered\`), with object arguments given as paths to what the game holds and a list of paths for a list: the game's own rules answer, in the running game, before anything is committed |
44229
44593
  | Wait for something in a running game | \`runtime_wait\`, never a sleep; \`until\` with \`says\` for a panel that rebuilds its own labels. The words have to be shown: a button that exists hidden through an animation does not satisfy it until it appears, unless \`includeHidden\` says otherwise |
44230
44594
  | What every label on a screen says | \`runtime_inspect tree\` with \`properties\` naming \`text\`, which reads it off every node that has one; \`includeProperties\` is every stored property of every node and ran to seventy-six thousand characters for one row |
44231
44595
  | Whether this engine has a method or property, and what it takes | \`editor_classes info\` with \`member\`, which answers that one member and which class declares it rather than the whole class |
@@ -44337,27 +44701,27 @@ whatever the engine said on stderr comes back under \`engine_messages\`.
44337
44701
  function skillFiles(version) {
44338
44702
  return new Map([
44339
44703
  ["SKILL.md", skillMarkdown(version)],
44340
- [join9("references", "tools.md"), renderToolsMarkdown()]
44704
+ [join10("references", "tools.md"), renderToolsMarkdown()]
44341
44705
  ]);
44342
44706
  }
44343
44707
  function skillDirectories(harnesses, projectPath, exists) {
44344
- const directories = [join9(projectPath, SHARED_SKILLS, SKILL_NAME)];
44708
+ const directories = [join10(projectPath, SHARED_SKILLS, SKILL_NAME)];
44345
44709
  for (const harness of harnesses) {
44346
44710
  if (harness.skills === undefined) {
44347
44711
  continue;
44348
44712
  }
44349
- const own = join9(projectPath, harness.skills.dir);
44350
- if ((!harness.skills.shared || exists(own)) && !directories.includes(join9(own, SKILL_NAME))) {
44351
- directories.push(join9(own, SKILL_NAME));
44713
+ const own = join10(projectPath, harness.skills.dir);
44714
+ if ((!harness.skills.shared || exists(own)) && !directories.includes(join10(own, SKILL_NAME))) {
44715
+ directories.push(join10(own, SKILL_NAME));
44352
44716
  }
44353
44717
  }
44354
44718
  return directories;
44355
44719
  }
44356
44720
  function everySkillDirectory(projectPath, harnesses) {
44357
- const directories = new Set([join9(projectPath, SHARED_SKILLS, SKILL_NAME)]);
44721
+ const directories = new Set([join10(projectPath, SHARED_SKILLS, SKILL_NAME)]);
44358
44722
  for (const harness of harnesses) {
44359
44723
  if (harness.skills !== undefined) {
44360
- directories.add(join9(projectPath, harness.skills.dir, SKILL_NAME));
44724
+ directories.add(join10(projectPath, harness.skills.dir, SKILL_NAME));
44361
44725
  }
44362
44726
  }
44363
44727
  return [...directories];
@@ -44365,7 +44729,7 @@ function everySkillDirectory(projectPath, harnesses) {
44365
44729
  function removeSkill(directories, projectPath) {
44366
44730
  const removed = [];
44367
44731
  for (const directory of directories) {
44368
- if (existsSync8(directory)) {
44732
+ if (existsSync9(directory)) {
44369
44733
  rmSync5(directory, { recursive: true, force: true });
44370
44734
  pruneEmpty(dirname5(directory), projectPath);
44371
44735
  removed.push(directory);
@@ -44374,10 +44738,10 @@ function removeSkill(directories, projectPath) {
44374
44738
  return removed;
44375
44739
  }
44376
44740
  function pruneEmpty(directory, projectPath) {
44377
- const shared = join9(projectPath, ".agents");
44741
+ const shared = join10(projectPath, ".agents");
44378
44742
  let path = directory;
44379
44743
  while (path !== projectPath && (basename2(path) === "skills" || path === shared)) {
44380
- if (readdirSync4(path).length > 0) {
44744
+ if (readdirSync5(path).length > 0) {
44381
44745
  return;
44382
44746
  }
44383
44747
  rmSync5(path, { recursive: true, force: true });
@@ -44387,10 +44751,10 @@ function pruneEmpty(directory, projectPath) {
44387
44751
  function writeSkill(directories, version) {
44388
44752
  const files = skillFiles(version);
44389
44753
  return directories.map((directory) => {
44390
- const replaced = existsSync8(directory);
44754
+ const replaced = existsSync9(directory);
44391
44755
  rmSync5(directory, { recursive: true, force: true });
44392
44756
  for (const [name, contents] of files) {
44393
- const path = join9(directory, name);
44757
+ const path = join10(directory, name);
44394
44758
  mkdirSync5(dirname5(path), { recursive: true });
44395
44759
  writeFileSync6(path, contents, "utf8");
44396
44760
  }
@@ -44400,13 +44764,13 @@ function writeSkill(directories, version) {
44400
44764
 
44401
44765
  // src/version.ts
44402
44766
  import { readFileSync as readFileSync8 } from "node:fs";
44403
- import { dirname as dirname6, join as join10 } from "node:path";
44767
+ import { dirname as dirname6, join as join11 } from "node:path";
44404
44768
  import { fileURLToPath as fileURLToPath2 } from "node:url";
44405
44769
  function getLocalVersion() {
44406
44770
  const currentDirectory = dirname6(fileURLToPath2(import.meta.url));
44407
44771
  const packageCandidates = [
44408
- join10(currentDirectory, "..", "package.json"),
44409
- join10(currentDirectory, "..", "..", "package.json")
44772
+ join11(currentDirectory, "..", "package.json"),
44773
+ join11(currentDirectory, "..", "..", "package.json")
44410
44774
  ];
44411
44775
  for (const packagePath of packageCandidates) {
44412
44776
  try {
@@ -44428,7 +44792,7 @@ class UsageError extends Refusal {
44428
44792
  function projectArgument(at) {
44429
44793
  const named = args.slice(at).find((value) => !value.startsWith("--"));
44430
44794
  const projectPath = resolve6(named ?? ".");
44431
- if (!existsSync15(join22(projectPath, "project.godot"))) {
44795
+ if (!existsSync16(join23(projectPath, "project.godot"))) {
44432
44796
  throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
44433
44797
  }
44434
44798
  return projectPath;
@@ -44467,7 +44831,7 @@ async function registerRuntime(godot, projectPath) {
44467
44831
  return;
44468
44832
  }
44469
44833
  console.log(`${RUNTIME_AUTOLOAD.name} autoload left at ${named}, which this project registers itself`);
44470
- if (!existsSync15(join22(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
44834
+ if (!existsSync16(join23(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
44471
44835
  console.log(` that file is not in this project, so it boots with a missing script: restore it, or point the entry at res://${RUNTIME_AUTOLOAD.path}`);
44472
44836
  }
44473
44837
  }
@@ -44599,7 +44963,7 @@ async function setup() {
44599
44963
  console.log("no harness configured; name one yourself, and gdharness harnesses lists them all");
44600
44964
  }
44601
44965
  if (!args.includes("--no-skill")) {
44602
- const directories = skillDirectories(chosen.harnesses, projectPath, existsSync15);
44966
+ const directories = skillDirectories(chosen.harnesses, projectPath, existsSync16);
44603
44967
  for (const written of writeSkill(directories, getLocalVersion())) {
44604
44968
  console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
44605
44969
  }
@@ -44693,7 +45057,7 @@ async function upgrade() {
44693
45057
  moved.push(took.from);
44694
45058
  }
44695
45059
  }
44696
- for (const written of writeSkill(skillDirectories(already, projectPath, existsSync15), version)) {
45060
+ for (const written of writeSkill(skillDirectories(already, projectPath, existsSync16), version)) {
44697
45061
  console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
44698
45062
  }
44699
45063
  console.log(`
@@ -44702,7 +45066,7 @@ On ${version}. What is left for you:`);
44702
45066
  ` + ` so it gives an open editor nothing to restart for. editor_status says addonIsStale
44703
45067
  ` + " if the editor is older than that." : ` 1. The open editor is still running the addons it loaded at startup. Restart it with the
44704
45068
  ` + " editor_launch restart tool, which closes and reopens the window.");
44705
- const serving = serverServing(projectPath);
45069
+ const serving = await serverServing(projectPath);
44706
45070
  console.log(harnessNote({ moved, written, byHand }, [launch.command, ...launch.args].join(" "), serving === null ? null : { server: serving, isThisVersion: serving.version === version }));
44707
45071
  }
44708
45072
  function doctorReport(projectPath) {