gdharness 1.1.13 → 1.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/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;
@@ -448,8 +605,18 @@ function resolveHeadless(requested, host) {
448
605
  }
449
606
  return envValue("DISPLAY", host.variables) === undefined && envValue("WAYLAND_DISPLAY", host.variables) === undefined;
450
607
  }
608
+ function resolveSilent(requested, variables) {
609
+ if (typeof requested === "boolean") {
610
+ return requested;
611
+ }
612
+ const said = envValue("GDHARNESS_SILENT", variables)?.toLowerCase();
613
+ return said !== undefined && said !== "0" && said !== "false";
614
+ }
451
615
  function runArguments(options) {
452
616
  const args = options.headless ? ["--headless", "--path", options.projectPath] : ["--path", options.projectPath];
617
+ if (options.silent === true && !options.headless) {
618
+ args.push("--audio-driver", "Dummy");
619
+ }
453
620
  if (options.quitAfter !== undefined && options.quitAfter !== null) {
454
621
  args.push("--quit-after", String(options.quitAfter));
455
622
  }
@@ -7520,16 +7687,16 @@ var require_public_api = __commonJS(function(exports) {
7520
7687
  // src/runner.ts
7521
7688
  import { existsSync as existsSync3 } from "node:fs";
7522
7689
  import { dirname as dirname2, join as join3 } from "node:path";
7523
- import process2 from "node:process";
7690
+ import process3 from "node:process";
7524
7691
  function currentRunner() {
7525
- const versions = process2.versions;
7692
+ const versions = process3.versions;
7526
7693
  return versions["bun"] === undefined ? "npx" : "bunx";
7527
7694
  }
7528
7695
  function runLine(runner, version, rest = "") {
7529
7696
  const flag = runner === "npx" ? "-y " : "";
7530
7697
  return `${runner} ${flag}gdharness@${version}${rest === "" ? "" : ` ${rest}`}`;
7531
7698
  }
7532
- function spawnFor(runner, version, execPath = process2.execPath) {
7699
+ function spawnFor(runner, version, execPath = process3.execPath) {
7533
7700
  const spec = `gdharness@${version}`;
7534
7701
  if (runner !== currentRunner()) {
7535
7702
  return { command: runner, args: runner === "npx" ? ["-y", spec] : ["--bun", spec] };
@@ -7540,7 +7707,7 @@ function spawnFor(runner, version, execPath = process2.execPath) {
7540
7707
  return { command: alongside(execPath, "npx") ?? "npx", args: ["-y", spec] };
7541
7708
  }
7542
7709
  function alongside(execPath, name) {
7543
- const runner = join3(dirname2(execPath), process2.platform === "win32" ? `${name}.cmd` : name);
7710
+ const runner = join3(dirname2(execPath), process3.platform === "win32" ? `${name}.cmd` : name);
7544
7711
  return existsSync3(runner) ? runner : undefined;
7545
7712
  }
7546
7713
  var init_runner = () => {};
@@ -7683,8 +7850,47 @@ var init_game_log = __esm(() => {
7683
7850
  };
7684
7851
  });
7685
7852
 
7853
+ // src/alive.ts
7854
+ import process5 from "node:process";
7855
+ function alive(pid) {
7856
+ if (pid === undefined || pid === null) {
7857
+ return false;
7858
+ }
7859
+ try {
7860
+ process5.kill(pid, 0);
7861
+ return true;
7862
+ } catch (error) {
7863
+ return error.code === "EPERM";
7864
+ }
7865
+ }
7866
+ var init_alive = () => {};
7867
+
7686
7868
  // src/scratch.ts
7687
- import { rmSync as rmSync3 } from "node:fs";
7869
+ import { existsSync as existsSync5, mkdtempSync, readdirSync as readdirSync2, rmSync as rmSync3 } from "node:fs";
7870
+ import { tmpdir } from "node:os";
7871
+ import { join as join5 } from "node:path";
7872
+ import process6 from "node:process";
7873
+ function scratchDirectory(kind) {
7874
+ return mkdtempSync(join5(tmpdir(), `gdharness-${kind}-${process6.pid}-`));
7875
+ }
7876
+ function sweepAbandonedScratch(directory = tmpdir()) {
7877
+ let entries;
7878
+ try {
7879
+ entries = readdirSync2(directory);
7880
+ } catch {
7881
+ return;
7882
+ }
7883
+ const named = new RegExp(`^gdharness-(${KINDS.join("|")})-(\\d+)-[A-Za-z0-9]{6}$`);
7884
+ for (const entry of entries) {
7885
+ const owner = named.exec(entry)?.[2];
7886
+ if (owner === undefined || alive(Number(owner))) {
7887
+ continue;
7888
+ }
7889
+ try {
7890
+ remove(join5(directory, entry));
7891
+ } catch {}
7892
+ }
7893
+ }
7688
7894
  function discardWith(remove, paths) {
7689
7895
  const left = [];
7690
7896
  for (const path of paths) {
@@ -7700,20 +7906,75 @@ function discardWith(remove, paths) {
7700
7906
  return left;
7701
7907
  }
7702
7908
  function discard(...paths) {
7703
- return discardWith((path) => {
7704
- rmSync3(path, SWEPT);
7705
- }, paths);
7909
+ const left = discardWith(remove, paths);
7910
+ for (const { path } of left) {
7911
+ tryAgain(path, 0);
7912
+ }
7913
+ return left;
7914
+ }
7915
+ function remove(path) {
7916
+ rmSync3(path, SWEPT);
7917
+ }
7918
+ function tryAgain(path, attempt) {
7919
+ if (attempt === 0) {
7920
+ if (waiting.has(path)) {
7921
+ return;
7922
+ }
7923
+ waiting.add(path);
7924
+ atExit.delete(path);
7925
+ lastTryOnExit();
7926
+ }
7927
+ const after = AGAIN_AFTER_MS[attempt];
7928
+ if (after === undefined || !existsSync5(path)) {
7929
+ waiting.delete(path);
7930
+ if (after === undefined) {
7931
+ atExit.add(path);
7932
+ }
7933
+ return;
7934
+ }
7935
+ setTimeout(() => {
7936
+ try {
7937
+ remove(path);
7938
+ waiting.delete(path);
7939
+ } catch {
7940
+ tryAgain(path, attempt + 1);
7941
+ }
7942
+ }, after).unref();
7706
7943
  }
7707
- var SWEPT;
7944
+ function lastTryOnExit() {
7945
+ if (lastTryWatched) {
7946
+ return;
7947
+ }
7948
+ lastTryWatched = true;
7949
+ process6.once("exit", () => {
7950
+ for (const path of [...waiting, ...atExit]) {
7951
+ try {
7952
+ remove(path);
7953
+ } catch {}
7954
+ }
7955
+ });
7956
+ }
7957
+ var SWEPT, AGAIN_AFTER_MS, waiting, atExit, KINDS, lastTryWatched = false;
7708
7958
  var init_scratch = __esm(() => {
7709
- SWEPT = { recursive: true, force: true, maxRetries: 20, retryDelay: 250 };
7959
+ init_alive();
7960
+ SWEPT = { recursive: true, force: true };
7961
+ AGAIN_AFTER_MS = [250, 500, 1000, 2000, 4000, 8000, 16000];
7962
+ waiting = new Set;
7963
+ atExit = new Set;
7964
+ KINDS = [
7965
+ "desktop",
7966
+ "export",
7967
+ "import",
7968
+ "params",
7969
+ "runtime-screenshot",
7970
+ "tests"
7971
+ ];
7710
7972
  });
7711
7973
 
7712
7974
  // src/headless.ts
7713
7975
  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";
7976
+ import { writeFileSync as writeFileSync3 } from "node:fs";
7977
+ import { join as join6 } from "node:path";
7717
7978
  import { promisify as promisify3 } from "node:util";
7718
7979
  function snakeCased(params) {
7719
7980
  const result = emptyRecord();
@@ -7753,9 +8014,9 @@ function reason(stdout, stderr) {
7753
8014
  return stdout.trim().split(/\r?\n/).at(-1) ?? "no output at all";
7754
8015
  }
7755
8016
  async function runImport(godotPath, projectPath) {
7756
- const logDir = mkdtempSync(join5(tmpdir(), "gdharness-import-"));
8017
+ const logDir = scratchDirectory("import");
7757
8018
  try {
7758
- const { stderr } = await run3(godotPath, ["--headless", "--log-file", join5(logDir, "engine.log"), "--path", projectPath, "--import"], { signal: callSignal() });
8019
+ const { stderr } = await run3(godotPath, ["--headless", "--log-file", join6(logDir, "engine.log"), "--path", projectPath, "--import"], { signal: callSignal() });
7759
8020
  return { ok: true, messages: problems(stderr) };
7760
8021
  } catch (error) {
7761
8022
  if (error instanceof Error && "stdout" in error && "stderr" in error) {
@@ -7776,13 +8037,13 @@ async function runImport(godotPath, projectPath) {
7776
8037
  }
7777
8038
  }
7778
8039
  async function runOperation(engine, operation, params, projectPath) {
7779
- const paramsDir = mkdtempSync(join5(tmpdir(), "gdharness-params-"));
7780
- const paramsFile = join5(paramsDir, `${operation}.json`);
8040
+ const paramsDir = scratchDirectory("params");
8041
+ const paramsFile = join6(paramsDir, `${operation}.json`);
7781
8042
  writeFileSync3(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
7782
8043
  const args = [
7783
8044
  "--headless",
7784
8045
  "--log-file",
7785
- join5(paramsDir, "engine.log"),
8046
+ join6(paramsDir, "engine.log"),
7786
8047
  "--path",
7787
8048
  projectPath,
7788
8049
  "--script",
@@ -7831,9 +8092,9 @@ var init_headless = __esm(() => {
7831
8092
  });
7832
8093
 
7833
8094
  // src/update-check.ts
7834
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "node:fs";
8095
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "node:fs";
7835
8096
  import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
7836
- import { join as join6 } from "node:path";
8097
+ import { join as join7 } from "node:path";
7837
8098
  function packageUrl(root) {
7838
8099
  return `${root.replace(/\/+$/, "")}/gdharness/latest`;
7839
8100
  }
@@ -7855,25 +8116,25 @@ function cacheDirectory(environment) {
7855
8116
  };
7856
8117
  const home = set("HOME") ?? homedir3();
7857
8118
  if (process.platform === "win32") {
7858
- return join6(set("LOCALAPPDATA") ?? home, "gdharness");
8119
+ return join7(set("LOCALAPPDATA") ?? home, "gdharness");
7859
8120
  }
7860
8121
  if (process.platform === "darwin") {
7861
- return join6(home, "Library", "Caches", "gdharness");
8122
+ return join7(home, "Library", "Caches", "gdharness");
7862
8123
  }
7863
- return join6(set("XDG_CACHE_HOME") ?? join6(home, ".cache"), "gdharness");
8124
+ return join7(set("XDG_CACHE_HOME") ?? join7(home, ".cache"), "gdharness");
7864
8125
  }
7865
8126
  function cacheFile(environment = process.env) {
7866
8127
  try {
7867
8128
  const directory = cacheDirectory(environment);
7868
8129
  mkdirSync3(directory, { recursive: true, mode: 448 });
7869
- return join6(directory, "update-check.json");
8130
+ return join7(directory, "update-check.json");
7870
8131
  } catch {
7871
- return join6(tmpdir2(), "gdharness-update-check.json");
8132
+ return join7(tmpdir2(), "gdharness-update-check.json");
7872
8133
  }
7873
8134
  }
7874
8135
  function readCache(path) {
7875
8136
  try {
7876
- if (!existsSync5(path)) {
8137
+ if (!existsSync6(path)) {
7877
8138
  return null;
7878
8139
  }
7879
8140
  const parsed = JSON.parse(readFileSync3(path, "utf8"));
@@ -8076,19 +8337,20 @@ var init_server_version = __esm(() => {
8076
8337
 
8077
8338
  // src/issues.ts
8078
8339
  import { createHash } from "node:crypto";
8079
- import process4 from "node:process";
8080
- function feedbackNotice() {
8340
+ import process7 from "node:process";
8341
+ function feedbackNotice(every) {
8081
8342
  return JSON.stringify({
8082
8343
  gdharness_feedback: {
8344
+ 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
8345
  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
8346
  enhancement_url: ENHANCEMENT_URL
8085
8347
  }
8086
8348
  }, null, 2);
8087
8349
  }
8088
8350
  function runtime() {
8089
- const versions = process4.versions;
8351
+ const versions = process7.versions;
8090
8352
  const bun = versions["bun"];
8091
- return bun === undefined ? `node ${process4.versions.node}` : `bun ${bun}`;
8353
+ return bun === undefined ? `node ${process7.versions.node}` : `bun ${bun}`;
8092
8354
  }
8093
8355
  function defectSignature(where, message) {
8094
8356
  const normalised = `${where}
@@ -8100,7 +8362,7 @@ function facts(where, message, godotVersion) {
8100
8362
  ["Where", where],
8101
8363
  ["Error", message],
8102
8364
  ["Version", `gdharness ${SERVER_VERSION}`],
8103
- ["Runtime", `${runtime()}, ${process4.platform} ${process4.arch}`],
8365
+ ["Runtime", `${runtime()}, ${process7.platform} ${process7.arch}`],
8104
8366
  ["Godot", godotVersion ?? "not known at the point this failed"],
8105
8367
  ["Signature", defectSignature(where, message)]
8106
8368
  ];
@@ -8110,7 +8372,7 @@ function filledTemplate(where, message, godotVersion) {
8110
8372
  `**gdharness version**: ${SERVER_VERSION}`,
8111
8373
  `**Godot version**: ${godotVersion ?? ""}`,
8112
8374
  `**Bun version**: ${runtime()}`,
8113
- `**OS**: ${process4.platform} ${process4.arch}`,
8375
+ `**OS**: ${process7.platform} ${process7.arch}`,
8114
8376
  "**MCP client**:",
8115
8377
  "",
8116
8378
  "## What you did",
@@ -8199,35 +8461,82 @@ var init_issues = __esm(() => {
8199
8461
  });
8200
8462
 
8201
8463
  // 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";
8464
+ import { existsSync as existsSync7, readdirSync as readdirSync3, readFileSync as readFileSync5, statSync as statSync2 } from "node:fs";
8465
+ import { join as join8 } from "node:path";
8204
8466
  function declaredClasses(projectPath) {
8205
8467
  const declared = new Map;
8468
+ eachScript(projectPath, (script, source) => {
8469
+ const found = DECLARATION.exec(source);
8470
+ if (found?.[1]) {
8471
+ declared.set(found[1], script);
8472
+ }
8473
+ });
8474
+ return declared;
8475
+ }
8476
+ function eachScript(projectPath, each) {
8206
8477
  const visit = (directory, prefix) => {
8207
- if (existsSync6(join7(directory, ".gdignore"))) {
8478
+ if (existsSync7(join8(directory, ".gdignore"))) {
8208
8479
  return;
8209
8480
  }
8210
- for (const entry of readdirSync2(directory, { withFileTypes: true })) {
8481
+ for (const entry of readdirSync3(directory, { withFileTypes: true })) {
8211
8482
  if (entry.name.startsWith(".")) {
8212
8483
  continue;
8213
8484
  }
8214
- const path = join7(directory, entry.name);
8485
+ const path = join8(directory, entry.name);
8215
8486
  if (entry.isDirectory()) {
8216
8487
  visit(path, `${prefix}${entry.name}/`);
8217
8488
  } 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
- }
8489
+ each(`res://${prefix}${entry.name}`, readFileSync5(path, "utf8"));
8222
8490
  }
8223
8491
  }
8224
8492
  };
8225
8493
  visit(projectPath, "");
8226
- return declared;
8494
+ }
8495
+ function scriptsNaming(projectPath, classes) {
8496
+ const named = new Map(classes.map((name) => [name, ""]));
8497
+ const naming = [];
8498
+ eachScript(projectPath, (script, source) => {
8499
+ const declares = DECLARATION.exec(source)?.[1];
8500
+ if (declares !== undefined && named.has(declares)) {
8501
+ return;
8502
+ }
8503
+ const code = codeOf(source);
8504
+ if (classesNamedIn(code, named).length > 0) {
8505
+ naming.push({ script, tool: /^\s*@tool\b/m.test(code) });
8506
+ }
8507
+ });
8508
+ return naming.sort((a, b) => a.script.localeCompare(b.script));
8509
+ }
8510
+ function codeOf(source) {
8511
+ let code = "";
8512
+ let at = 0;
8513
+ while (at < source.length) {
8514
+ const here = source[at] ?? "";
8515
+ if (here === "#") {
8516
+ const end = source.indexOf(`
8517
+ `, at);
8518
+ at = end === -1 ? source.length : end;
8519
+ continue;
8520
+ }
8521
+ if (here === '"' || here === "'") {
8522
+ const fence = source.startsWith(here.repeat(3), at) ? here.repeat(3) : here;
8523
+ let end = at + fence.length;
8524
+ while (end < source.length && !source.startsWith(fence, end)) {
8525
+ end += source[end] === "\\" ? 2 : 1;
8526
+ }
8527
+ const literal = source.slice(at, Math.min(end + fence.length, source.length));
8528
+ code += literal.replace(/[^\n]/g, " ");
8529
+ at = end + fence.length;
8530
+ continue;
8531
+ }
8532
+ code += here;
8533
+ at += 1;
8534
+ }
8535
+ return code;
8227
8536
  }
8228
8537
  function cachedClasses(projectPath) {
8229
- const cache = join7(projectPath, ".godot", "global_script_class_cache.cfg");
8230
- if (!existsSync6(cache)) {
8538
+ const cache = join8(projectPath, ".godot", "global_script_class_cache.cfg");
8539
+ if (!existsSync7(cache)) {
8231
8540
  return null;
8232
8541
  }
8233
8542
  const listed = new Map;
@@ -8340,7 +8649,7 @@ function uncachedClassNote(types) {
8340
8649
  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
8650
  }
8342
8651
  function cacheWrittenAt(projectPath) {
8343
- const cache = join7(projectPath, ".godot", "global_script_class_cache.cfg");
8652
+ const cache = join8(projectPath, ".godot", "global_script_class_cache.cfg");
8344
8653
  try {
8345
8654
  return statSync2(cache).mtimeMs;
8346
8655
  } catch {
@@ -8354,14 +8663,14 @@ function declaredSince(projectPath, paths, names, since) {
8354
8663
  return false;
8355
8664
  }
8356
8665
  try {
8357
- return statSync2(join7(projectPath, path.slice("res://".length))).mtimeMs > since;
8666
+ return statSync2(join8(projectPath, path.slice("res://".length))).mtimeMs > since;
8358
8667
  } catch {
8359
8668
  return false;
8360
8669
  }
8361
8670
  });
8362
8671
  }
8363
8672
  function cachedAtMissingPaths(cached, projectPath) {
8364
- return [...cached].filter(([, path]) => path.startsWith("res://") && !existsSync6(join7(projectPath, path.slice("res://".length)))).map(([name]) => name).sort();
8673
+ return [...cached].filter(([, path]) => path.startsWith("res://") && !existsSync7(join8(projectPath, path.slice("res://".length)))).map(([name]) => name).sort();
8365
8674
  }
8366
8675
  function staleAgainst(cached, projectPath) {
8367
8676
  return [...declaredClasses(projectPath)].filter(([name, path]) => cached.get(name) !== path).map(([name]) => name);
@@ -15851,29 +16160,29 @@ function readParams(params, key) {
15851
16160
  }
15852
16161
 
15853
16162
  // src/setup.ts
15854
- import { spawnSync as spawnSync2 } from "node:child_process";
16163
+ import { spawnSync } from "node:child_process";
15855
16164
  import { createHash as createHash2 } from "node:crypto";
15856
16165
  import {
15857
16166
  cpSync,
15858
- existsSync as existsSync7,
16167
+ existsSync as existsSync8,
15859
16168
  mkdirSync as mkdirSync4,
15860
- readdirSync as readdirSync3,
16169
+ readdirSync as readdirSync4,
15861
16170
  readFileSync as readFileSync7,
15862
16171
  rmdirSync,
15863
16172
  rmSync as rmSync4,
15864
16173
  statSync as statSync3,
15865
16174
  writeFileSync as writeFileSync5
15866
16175
  } from "node:fs";
15867
- import { dirname as dirname4, join as join8 } from "node:path";
16176
+ import { dirname as dirname4, join as join9 } from "node:path";
15868
16177
  import { fileURLToPath } from "node:url";
15869
16178
  function editorAddonDigest(from = shippedAddonsDirectory()) {
15870
16179
  const hash = createHash2("sha256");
15871
16180
  const walk = (directory, relative) => {
15872
- for (const name of readdirSync3(directory).sort()) {
16181
+ for (const name of readdirSync4(directory).sort()) {
15873
16182
  if (name === VERSION_MARKER || name === DIGEST_MARKER) {
15874
16183
  continue;
15875
16184
  }
15876
- const path = join8(directory, name);
16185
+ const path = join9(directory, name);
15877
16186
  const inside = `${relative}/${name}`;
15878
16187
  if (statSync3(path).isDirectory()) {
15879
16188
  walk(path, inside);
@@ -15887,7 +16196,7 @@ function editorAddonDigest(from = shippedAddonsDirectory()) {
15887
16196
  }
15888
16197
  };
15889
16198
  for (const name of EDITOR_PLUGINS) {
15890
- walk(join8(from, name), name);
16199
+ walk(join9(from, name), name);
15891
16200
  }
15892
16201
  return hash.digest("hex");
15893
16202
  }
@@ -15902,42 +16211,42 @@ function shippedEditorDigest() {
15902
16211
  return shippedDigest ?? undefined;
15903
16212
  }
15904
16213
  function installedEditorDigest(projectPath) {
15905
- const marker = join8(projectPath, "addons", ADDONS[0], DIGEST_MARKER);
15906
- if (!existsSync7(marker)) {
16214
+ const marker = join9(projectPath, "addons", ADDONS[0], DIGEST_MARKER);
16215
+ if (!existsSync8(marker)) {
15907
16216
  return null;
15908
16217
  }
15909
16218
  const said = readFileSync7(marker, "utf8").trim();
15910
16219
  return said === "" ? null : said;
15911
16220
  }
15912
16221
  function shippedAddonsDirectory() {
15913
- return join8(dirname4(fileURLToPath(import.meta.url)), "godot", "addons");
16222
+ return join9(dirname4(fileURLToPath(import.meta.url)), "godot", "addons");
15914
16223
  }
15915
16224
  function shippedOperationsScript() {
15916
- return join8(dirname4(fileURLToPath(import.meta.url)), "godot", "operations", "godot_operations.gd");
16225
+ return join9(dirname4(fileURLToPath(import.meta.url)), "godot", "operations", "godot_operations.gd");
15917
16226
  }
15918
16227
  function installAddons(projectPath, from = shippedAddonsDirectory()) {
15919
16228
  const installed = [];
15920
16229
  for (const name of ADDONS) {
15921
- const source = join8(from, name);
15922
- if (!existsSync7(join8(source, name === "gdharness_runtime" ? "runtime_autoload.gd" : "plugin.cfg"))) {
16230
+ const source = join9(from, name);
16231
+ if (!existsSync8(join9(source, name === "gdharness_runtime" ? "runtime_autoload.gd" : "plugin.cfg"))) {
15923
16232
  throw new Error(`The package holds no ${name} addon at ${source}.`);
15924
16233
  }
15925
- const target = join8(projectPath, "addons", name);
15926
- const replaced = existsSync7(target);
16234
+ const target = join9(projectPath, "addons", name);
16235
+ const replaced = existsSync8(target);
15927
16236
  rmSync4(target, { recursive: true, force: true });
15928
16237
  mkdirSync4(dirname4(target), { recursive: true });
15929
16238
  cpSync(source, target, { recursive: true });
15930
- writeFileSync5(join8(target, VERSION_MARKER), `${SERVER_VERSION}
16239
+ writeFileSync5(join9(target, VERSION_MARKER), `${SERVER_VERSION}
15931
16240
  `);
15932
16241
  installed.push({ name, path: target, replaced });
15933
16242
  }
15934
- writeFileSync5(join8(projectPath, "addons", ADDONS[0], DIGEST_MARKER), `${editorAddonDigest(from)}
16243
+ writeFileSync5(join9(projectPath, "addons", ADDONS[0], DIGEST_MARKER), `${editorAddonDigest(from)}
15935
16244
  `);
15936
16245
  return installed;
15937
16246
  }
15938
16247
  function installedAddonVersion(projectPath) {
15939
- const marker = join8(projectPath, "addons", ADDONS[0], VERSION_MARKER);
15940
- if (!existsSync7(marker)) {
16248
+ const marker = join9(projectPath, "addons", ADDONS[0], VERSION_MARKER);
16249
+ if (!existsSync8(marker)) {
15941
16250
  return null;
15942
16251
  }
15943
16252
  const said = readFileSync7(marker, "utf8").trim();
@@ -15946,14 +16255,14 @@ function installedAddonVersion(projectPath) {
15946
16255
  function removeAddons(projectPath) {
15947
16256
  const removed = [];
15948
16257
  for (const name of ADDONS) {
15949
- const target = join8(projectPath, "addons", name);
15950
- if (existsSync7(target)) {
16258
+ const target = join9(projectPath, "addons", name);
16259
+ if (existsSync8(target)) {
15951
16260
  rmSync4(target, { recursive: true, force: true });
15952
16261
  removed.push(target);
15953
16262
  }
15954
16263
  }
15955
- const addons = join8(projectPath, "addons");
15956
- if (removed.length > 0 && existsSync7(addons) && readdirSync3(addons).length === 0) {
16264
+ const addons = join9(projectPath, "addons");
16265
+ if (removed.length > 0 && existsSync8(addons) && readdirSync4(addons).length === 0) {
15957
16266
  rmdirSync(addons);
15958
16267
  removed.push(addons);
15959
16268
  }
@@ -15987,7 +16296,7 @@ function loaderAmong(projectPath, named) {
15987
16296
  }
15988
16297
  let source = null;
15989
16298
  try {
15990
- source = readFileSync7(join8(projectPath, path), "utf8");
16299
+ source = readFileSync7(join9(projectPath, path), "utf8");
15991
16300
  } catch {}
15992
16301
  if (source?.includes(RUNTIME_AUTOLOAD.path)) {
15993
16302
  return { name: entry, path, exists: true };
@@ -16010,7 +16319,7 @@ function trackedByGit(projectPath, wanted) {
16010
16319
  if (wanted.length === 0) {
16011
16320
  return new Set;
16012
16321
  }
16013
- const asked = spawnSync2("git", ["ls-files", "-z", "--", ...wanted], {
16322
+ const asked = spawnSync("git", ["ls-files", "-z", "--", ...wanted], {
16014
16323
  cwd: projectPath,
16015
16324
  encoding: "utf8"
16016
16325
  });
@@ -16029,10 +16338,10 @@ function enabledPlugins(settings) {
16029
16338
  function inspectProject(projectPath) {
16030
16339
  const problems = [];
16031
16340
  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;
16341
+ const directory = join9(projectPath, "addons", name);
16342
+ const marker = join9(directory, VERSION_MARKER);
16343
+ const installed = existsSync8(directory);
16344
+ const version = existsSync8(marker) ? readFileSync7(marker, "utf8").trim() : null;
16036
16345
  const current = version === SERVER_VERSION;
16037
16346
  if (!installed) {
16038
16347
  problems.push(`addons/${name} is not installed; run gdharness setup`);
@@ -16041,7 +16350,7 @@ function inspectProject(projectPath) {
16041
16350
  }
16042
16351
  return { name, installed, version, current };
16043
16352
  });
16044
- const settings = parseProjectGodot(readFileSync7(join8(projectPath, "project.godot"), "utf8"));
16353
+ const settings = parseProjectGodot(readFileSync7(join9(projectPath, "project.godot"), "utf8"));
16045
16354
  const pluginsEnabled = enabledPlugins(settings);
16046
16355
  for (const name of EDITOR_PLUGINS) {
16047
16356
  if (!pluginsEnabled.includes(name)) {
@@ -16057,7 +16366,7 @@ function inspectProject(projectPath) {
16057
16366
  const tracked = trackedByGit(projectPath, [...named.values()]);
16058
16367
  const addonMissing = (path) => addons.some((addon) => !addon.installed && path.startsWith(`addons/${addon.name}/`));
16059
16368
  for (const [name, path] of named) {
16060
- if (!existsSync7(join8(projectPath, path))) {
16369
+ if (!existsSync8(join9(projectPath, path))) {
16061
16370
  if (!addonMissing(path)) {
16062
16371
  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
16372
  }
@@ -16816,7 +17125,11 @@ var init_tool_definitions = __esm(() => {
16816
17125
  name: "editor_run",
16817
17126
  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
17127
  parameters: {
16819
- projectPath: { ...PROJECT_PATH, ops: ["start", "check"] },
17128
+ projectPath: {
17129
+ ...PROJECT_PATH,
17130
+ ops: ["start", "check", "stop", "wait"],
17131
+ 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."
17132
+ },
16820
17133
  scene: {
16821
17134
  type: "string",
16822
17135
  ops: ["start", "check"],
@@ -16834,6 +17147,11 @@ var init_tool_definitions = __esm(() => {
16834
17147
  ops: ["start"],
16835
17148
  description: "start: on Windows, show the window on the desktop in use. By default a windowed run this server starts goes on a desktop of its own, where it renders and runtime_capture works but the window never shows and never takes the keyboard. A game a visible editor plays opens on the desktop in use, and one a hidden editor (editor_launch hidden: true) plays goes on its desktop. Elsewhere than Windows the window shows either way. Default false."
16836
17149
  },
17150
+ silent: {
17151
+ type: "boolean",
17152
+ ops: ["start"],
17153
+ description: "start: run the game with no sound out of the machine, by starting the engine with --audio-driver Dummy: every stream still plays and every playback position still advances, so what is playing can still be asked. The editor takes no engine flags for a game it plays, so a silent windowed run is started by this server, and the debug_* tools do not answer for it. A headless run, check's included, is silent already. Default false, or true when GDHARNESS_SILENT is set in the server's environment to anything but 0 or false; silent: false then plays through the editor as before."
17154
+ },
16837
17155
  savesIn: {
16838
17156
  type: "string",
16839
17157
  ops: ["start", "check"],
@@ -16948,7 +17266,7 @@ var init_tool_definitions = __esm(() => {
16948
17266
  },
16949
17267
  {
16950
17268
  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.",
17269
+ 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
17270
  parameters: {
16953
17271
  projectPath: PROJECT_PATH,
16954
17272
  timeoutMs: {
@@ -17054,7 +17372,7 @@ var init_tool_definitions = __esm(() => {
17054
17372
  says: {
17055
17373
  type: "string",
17056
17374
  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."
17375
+ 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
17376
  },
17059
17377
  limit: {
17060
17378
  type: "integer",
@@ -17209,7 +17527,7 @@ var init_tool_definitions = __esm(() => {
17209
17527
  says: {
17210
17528
  type: "string",
17211
17529
  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."
17530
+ 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
17531
  },
17214
17532
  action: {
17215
17533
  type: "string",
@@ -17340,7 +17658,7 @@ var init_tool_definitions = __esm(() => {
17340
17658
  says: {
17341
17659
  type: "string",
17342
17660
  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."
17661
+ 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
17662
  },
17345
17663
  includeHidden: {
17346
17664
  type: "boolean",
@@ -32099,10 +32417,10 @@ var init_stdio = __esm(() => {
32099
32417
  });
32100
32418
 
32101
32419
  // node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
32102
- import process6 from "node:process";
32420
+ import process9 from "node:process";
32103
32421
 
32104
32422
  class StdioServerTransport {
32105
- constructor(_stdin = process6.stdin, _stdout = process6.stdout, options) {
32423
+ constructor(_stdin = process9.stdin, _stdout = process9.stdout, options) {
32106
32424
  this._stdin = _stdin;
32107
32425
  this._stdout = _stdout;
32108
32426
  this._started = false;
@@ -32167,10 +32485,10 @@ var init_stdio2 = __esm(() => {
32167
32485
  });
32168
32486
 
32169
32487
  // src/runtime-client.ts
32170
- import { existsSync as existsSync9, readdirSync as readdirSync5, readFileSync as readFileSync9, statSync as statSync4, unlinkSync } from "node:fs";
32488
+ import { existsSync as existsSync10, readdirSync as readdirSync6, readFileSync as readFileSync9, statSync as statSync4, unlinkSync } from "node:fs";
32171
32489
  import { createConnection } from "node:net";
32172
32490
  import { tmpdir as tmpdir3 } from "node:os";
32173
- import { dirname as dirname7, join as join11, resolve as resolve3 } from "node:path";
32491
+ import { dirname as dirname7, join as join12, resolve as resolve3 } from "node:path";
32174
32492
  import { setTimeout as delay } from "node:timers/promises";
32175
32493
  function runtimeDirectory(variables = process.env) {
32176
32494
  const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
@@ -32178,20 +32496,20 @@ function runtimeDirectory(variables = process.env) {
32178
32496
  return explicit;
32179
32497
  }
32180
32498
  const perUser = envValue("XDG_RUNTIME_DIR", variables);
32181
- return join11(perUser ?? tmpdir3(), "gdharness");
32499
+ return join12(perUser ?? tmpdir3(), "gdharness");
32182
32500
  }
32183
32501
  function runtimeDirectories(variables = process.env) {
32184
32502
  const candidates = [runtimeDirectory(variables)];
32185
32503
  const fallbacks = process.platform === "win32" ? [envValue("SystemRoot", variables) ?? envValue("windir", variables) ?? "C:\\Windows"] : ["/tmp", "/var/tmp"];
32186
32504
  for (const base of fallbacks) {
32187
- candidates.push(join11(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
32505
+ candidates.push(join12(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
32188
32506
  }
32189
32507
  return [...new Set(candidates.map((path) => resolve3(path)))];
32190
32508
  }
32191
32509
  function errorReportOf(pid, directories = runtimeDirectories()) {
32192
32510
  for (const directory of directories) {
32193
- const path = join11(directory, `runtime-${pid}.log`);
32194
- if (existsSync9(path)) {
32511
+ const path = join12(directory, `runtime-${pid}.log`);
32512
+ if (existsSync10(path)) {
32195
32513
  return path;
32196
32514
  }
32197
32515
  }
@@ -32219,14 +32537,6 @@ function reportAlreadyInTranscript(transcript, report) {
32219
32537
  const end = boundary + copied.length;
32220
32538
  return end <= report.length && report.subarray(boundary, end).equals(copied) ? end : report.length;
32221
32539
  }
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
32540
  function parseAnnouncement(file, pid) {
32231
32541
  let fields;
32232
32542
  try {
@@ -32314,46 +32624,52 @@ function strangersAmong(candidates, now, startTimes = startTimesOf) {
32314
32624
  const known = verdicts.get(one.file);
32315
32625
  return known === undefined || known.writtenAt !== one.writtenAt || !known.stranger && now - known.judgedAt > CONFIRMED_FOR_MS;
32316
32626
  });
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
- }
32627
+ if (toJudge.length > 0 && judging === null) {
32628
+ judging = startTimes(toJudge.map((one) => one.pid)).then((began) => {
32629
+ for (const one of toJudge) {
32630
+ const at = began.get(one.pid);
32631
+ const stranger = at !== undefined && at - one.writtenAt > STARTED_AFTER_ANNOUNCING_MS;
32632
+ verdicts.set(one.file, { writtenAt: one.writtenAt, judgedAt: now, stranger });
32633
+ }
32634
+ }).catch(() => {}).finally(() => {
32635
+ judging = null;
32636
+ });
32324
32637
  }
32325
- return new Set(candidates.filter((one) => verdicts.get(one.file)?.stranger === true).map((one) => one.file));
32638
+ return new Set(candidates.filter((one) => {
32639
+ const known = verdicts.get(one.file);
32640
+ return known?.stranger === true && known.writtenAt === one.writtenAt;
32641
+ }).map((one) => one.file));
32326
32642
  }
32327
32643
  function announcedIn(directory) {
32328
- if (!existsSync9(directory)) {
32644
+ if (!existsSync10(directory)) {
32329
32645
  return [];
32330
32646
  }
32331
32647
  const found = [];
32332
32648
  const candidates = [];
32333
32649
  const entries = [];
32334
- for (const entry of readdirSync5(directory)) {
32650
+ for (const entry of readdirSync6(directory)) {
32335
32651
  const report = ERROR_REPORT_PATTERN.exec(entry);
32336
32652
  if (report) {
32337
- sweepErrorReport(join11(directory, entry), Number.parseInt(report[1] ?? "", 10));
32653
+ sweepErrorReport(join12(directory, entry), Number.parseInt(report[1] ?? "", 10));
32338
32654
  continue;
32339
32655
  }
32340
32656
  const match = ANNOUNCEMENT_PATTERN.exec(entry);
32341
32657
  if (!match) {
32342
32658
  continue;
32343
32659
  }
32344
- const file = join11(directory, entry);
32660
+ const file = join12(directory, entry);
32345
32661
  const pid = Number.parseInt(match[1] ?? "", 10);
32346
- const alive = processAlive(pid);
32347
- entries.push({ file, pid, alive });
32348
- if (alive) {
32662
+ const running = alive(pid);
32663
+ entries.push({ file, pid, running });
32664
+ if (running) {
32349
32665
  try {
32350
32666
  candidates.push({ file, pid, writtenAt: statSync4(file).mtimeMs });
32351
32667
  } catch {}
32352
32668
  }
32353
32669
  }
32354
32670
  const strangers = strangersAmong(candidates, Date.now());
32355
- for (const { file, pid, alive } of entries) {
32356
- const gone = !alive || strangers.has(file);
32671
+ for (const { file, pid, running } of entries) {
32672
+ const gone = !running || strangers.has(file);
32357
32673
  const announced = gone ? { kind: "rubbish" } : parseAnnouncement(file, pid);
32358
32674
  if (announced.kind === "rubbish") {
32359
32675
  if (!gone) {
@@ -32369,7 +32685,7 @@ function announcedIn(directory) {
32369
32685
  found.push(announced);
32370
32686
  }
32371
32687
  const held = new Set(entries.map((one) => one.file));
32372
- const here = join11(directory, ".");
32688
+ const here = join12(directory, ".");
32373
32689
  for (const file of verdicts.keys()) {
32374
32690
  if (dirname7(file) === here && !held.has(file)) {
32375
32691
  verdicts.delete(file);
@@ -32378,13 +32694,13 @@ function announcedIn(directory) {
32378
32694
  return found;
32379
32695
  }
32380
32696
  async function announcementEnded(pid, directories = runtimeDirectories()) {
32381
- const files = directories.map((directory) => join11(directory, `runtime-${pid}.json`)).filter((file) => existsSync9(file));
32697
+ const files = directories.map((directory) => join12(directory, `runtime-${pid}.json`)).filter((file) => existsSync10(file));
32382
32698
  const first = files[0];
32383
32699
  if (first === undefined) {
32384
32700
  return;
32385
32701
  }
32386
32702
  const until = Date.now() + ENDED_WITHIN_MS;
32387
- while (processAlive(pid)) {
32703
+ while (alive(pid)) {
32388
32704
  const left = until - Date.now();
32389
32705
  if (left <= 0) {
32390
32706
  return;
@@ -32400,7 +32716,7 @@ async function announcementEnded(pid, directories = runtimeDirectories()) {
32400
32716
  }
32401
32717
  }
32402
32718
  function sweepErrorReport(path, pid, now = Date.now()) {
32403
- if (processAlive(pid)) {
32719
+ if (alive(pid)) {
32404
32720
  return;
32405
32721
  }
32406
32722
  try {
@@ -32663,8 +32979,9 @@ function runtimeRequest(endpoint, command, params, timeoutMs, keepListening = fa
32663
32979
  });
32664
32980
  });
32665
32981
  }
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;
32982
+ 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
32983
  var init_runtime_client = __esm(() => {
32984
+ init_alive();
32668
32985
  init_paths();
32669
32986
  init_process_children();
32670
32987
  ANNOUNCEMENT_PATTERN = /^runtime-(\d+)\.json$/;
@@ -32678,9 +32995,9 @@ var init_runtime_client = __esm(() => {
32678
32995
 
32679
32996
  // src/boot-note.ts
32680
32997
  import { mkdirSync as mkdirSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync7 } from "node:fs";
32681
- import { dirname as dirname8, join as join12 } from "node:path";
32998
+ import { dirname as dirname8, join as join13 } from "node:path";
32682
32999
  function bootNotePath(projectPath) {
32683
- return join12(projectPath, ".godot", "gdharness-boot.json");
33000
+ return join13(projectPath, ".godot", "gdharness-boot.json");
32684
33001
  }
32685
33002
  function readBootNote(projectPath) {
32686
33003
  try {
@@ -32713,9 +33030,9 @@ var init_boot_note = __esm(() => {
32713
33030
 
32714
33031
  // src/breakpoint-note.ts
32715
33032
  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";
33033
+ import { dirname as dirname9, isAbsolute as isAbsolute2, join as join14, relative as relative2 } from "node:path";
32717
33034
  function breakpointNotePath(projectPath) {
32718
- return join13(projectPath, ".godot", "gdharness-breakpoints.json");
33035
+ return join14(projectPath, ".godot", "gdharness-breakpoints.json");
32719
33036
  }
32720
33037
  function writeBreakpointNote(projectPath, held) {
32721
33038
  const path = breakpointNotePath(projectPath);
@@ -32756,7 +33073,7 @@ function readBreakpointNote(projectPath) {
32756
33073
  }
32757
33074
  const lines = Array.isArray(listed) ? listed.filter((line) => Number.isInteger(line) && line > 0) : [];
32758
33075
  if (lines.length > 0) {
32759
- held.push({ scriptPath: realPathOr(join13(projectPath, script)), lines });
33076
+ held.push({ scriptPath: realPathOr(join14(projectPath, script)), lines });
32760
33077
  }
32761
33078
  }
32762
33079
  return held;
@@ -32775,9 +33092,9 @@ var init_breakpoint_note = __esm(() => {
32775
33092
 
32776
33093
  // src/class-note.ts
32777
33094
  import { mkdirSync as mkdirSync8, readFileSync as readFileSync12, writeFileSync as writeFileSync9 } from "node:fs";
32778
- import { dirname as dirname10, join as join14 } from "node:path";
33095
+ import { dirname as dirname10, join as join15 } from "node:path";
32779
33096
  function classNotePath(projectPath) {
32780
- return join14(projectPath, ".godot", "gdharness-classes.json");
33097
+ return join15(projectPath, ".godot", "gdharness-classes.json");
32781
33098
  }
32782
33099
  function readClassNote(projectPath) {
32783
33100
  try {
@@ -32806,11 +33123,11 @@ function writeClassNote(projectPath, note) {
32806
33123
  var init_class_note = () => {};
32807
33124
 
32808
33125
  // src/config-pin.ts
32809
- import { existsSync as existsSync10, readFileSync as readFileSync13 } from "node:fs";
32810
- import { join as join15 } from "node:path";
33126
+ import { existsSync as existsSync11, readFileSync as readFileSync13 } from "node:fs";
33127
+ import { join as join16 } from "node:path";
32811
33128
  function versionsNamedInConfig(projectPath) {
32812
- const config = join15(projectPath, ".mcp.json");
32813
- if (!existsSync10(config)) {
33129
+ const config = join16(projectPath, ".mcp.json");
33130
+ if (!existsSync11(config)) {
32814
33131
  return [];
32815
33132
  }
32816
33133
  let text;
@@ -33443,6 +33760,9 @@ class GodotDAPClient {
33443
33760
  isConnected() {
33444
33761
  return this.connected;
33445
33762
  }
33763
+ connection() {
33764
+ return this.connected ? this.socket : null;
33765
+ }
33446
33766
  isStopped() {
33447
33767
  return this.halt !== null;
33448
33768
  }
@@ -33572,7 +33892,7 @@ var init_desktop = () => {};
33572
33892
  // src/editor-log.ts
33573
33893
  import {
33574
33894
  closeSync,
33575
- existsSync as existsSync11,
33895
+ existsSync as existsSync12,
33576
33896
  mkdirSync as mkdirSync9,
33577
33897
  openSync,
33578
33898
  readFileSync as readFileSync14,
@@ -33581,12 +33901,12 @@ import {
33581
33901
  statSync as statSync5,
33582
33902
  writeFileSync as writeFileSync10
33583
33903
  } from "node:fs";
33584
- import { dirname as dirname11, join as join16 } from "node:path";
33904
+ import { dirname as dirname11, join as join17 } from "node:path";
33585
33905
  function editorLogPath(projectPath) {
33586
- return join16(projectPath, ".godot", "gdharness-editor.log");
33906
+ return join17(projectPath, ".godot", "gdharness-editor.log");
33587
33907
  }
33588
33908
  function editorLogNotePath(projectPath) {
33589
- return join16(projectPath, ".godot", "gdharness-editor.json");
33909
+ return join17(projectPath, ".godot", "gdharness-editor.json");
33590
33910
  }
33591
33911
  function readEditorLogNote(projectPath) {
33592
33912
  try {
@@ -33631,7 +33951,7 @@ function editorConsole(projectPath, editorPid, openedByAServer) {
33631
33951
  return { kind: "older server", notedPid: noted?.pid ?? null };
33632
33952
  }
33633
33953
  const path = editorLogPath(projectPath);
33634
- if (!existsSync11(path)) {
33954
+ if (!existsSync12(path)) {
33635
33955
  return { kind: "not written" };
33636
33956
  }
33637
33957
  const log = new GameLog;
@@ -38060,7 +38380,7 @@ var init_lsp_client = __esm(() => {
38060
38380
  });
38061
38381
 
38062
38382
  // src/outside.ts
38063
- import { spawn } from "node:child_process";
38383
+ import { spawn as spawn2 } from "node:child_process";
38064
38384
  import { fileURLToPath as fileURLToPath4 } from "node:url";
38065
38385
  function changesFrom(env, base) {
38066
38386
  const changes = {};
@@ -38093,7 +38413,7 @@ function readLaunched(printed) {
38093
38413
  }
38094
38414
  async function launchOutsideTheTree(spec, runtime = process.execPath, keeper = KEEPER_SCRIPT) {
38095
38415
  return await new Promise((resolve) => {
38096
- const launcher = spawn(runtime, [keeper, "launch"], {
38416
+ const launcher = spawn2(runtime, [keeper, "launch"], {
38097
38417
  stdio: ["pipe", "pipe", "pipe"],
38098
38418
  windowsHide: true
38099
38419
  });
@@ -38132,14 +38452,14 @@ import { execFile as execFile4 } from "node:child_process";
38132
38452
  import { promisify as promisify4 } from "node:util";
38133
38453
  async function cpuSecondsOf(pid) {
38134
38454
  try {
38135
- return process.platform === "win32" ? await askWindows2(pid) : await askPosix2(pid);
38455
+ return process.platform === "win32" ? await secondsOnWindows(pid) : await askPosix2(pid);
38136
38456
  } catch {
38137
38457
  return;
38138
38458
  }
38139
38459
  }
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(",", "."));
38460
+ async function secondsOnWindows(pid) {
38461
+ const said = await askWindows(`(Get-Process -Id ${pid}).CPU`, ASK_TIMEOUT_MS2);
38462
+ const seconds = Number(said.trim().replace(",", "."));
38143
38463
  return Number.isFinite(seconds) ? seconds : undefined;
38144
38464
  }
38145
38465
  async function askPosix2(pid) {
@@ -38163,17 +38483,18 @@ function secondsFromClock(printed) {
38163
38483
  }
38164
38484
  var run4, ASK_TIMEOUT_MS2 = 6000;
38165
38485
  var init_process_time = __esm(() => {
38486
+ init_windows_ask();
38166
38487
  run4 = promisify4(execFile4);
38167
38488
  });
38168
38489
 
38169
38490
  // 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";
38491
+ import { existsSync as existsSync13, readdirSync as readdirSync7, readFileSync as readFileSync15 } from "node:fs";
38492
+ import { join as join18 } from "node:path";
38172
38493
  function skipped(name) {
38173
38494
  return name.startsWith(".") || SKIPPED.has(name);
38174
38495
  }
38175
38496
  function steppedOver(directory) {
38176
- return existsSync12(join17(directory, ".gdignore"));
38497
+ return existsSync13(join18(directory, ".gdignore"));
38177
38498
  }
38178
38499
  function projectStructure(projectPath) {
38179
38500
  const structure = { scenes: 0, scripts: 0, assets: 0, other: 0 };
@@ -38181,12 +38502,12 @@ function projectStructure(projectPath) {
38181
38502
  if (steppedOver(directory)) {
38182
38503
  return;
38183
38504
  }
38184
- for (const entry of readdirSync6(directory, { withFileTypes: true })) {
38505
+ for (const entry of readdirSync7(directory, { withFileTypes: true })) {
38185
38506
  if (skipped(entry.name)) {
38186
38507
  continue;
38187
38508
  }
38188
38509
  if (entry.isDirectory()) {
38189
- visit(join17(directory, entry.name));
38510
+ visit(join18(directory, entry.name));
38190
38511
  } else if (entry.isFile()) {
38191
38512
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
38192
38513
  if (extension === "tscn") {
@@ -38210,20 +38531,20 @@ function scriptsWithoutUid(projectPath) {
38210
38531
  if (steppedOver(directory)) {
38211
38532
  return;
38212
38533
  }
38213
- for (const entry of readdirSync6(directory, { withFileTypes: true })) {
38534
+ for (const entry of readdirSync7(directory, { withFileTypes: true })) {
38214
38535
  if (skipped(entry.name)) {
38215
38536
  continue;
38216
38537
  }
38217
38538
  const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
38218
38539
  if (entry.isDirectory()) {
38219
- visit(join17(directory, entry.name), spelled);
38540
+ visit(join18(directory, entry.name), spelled);
38220
38541
  continue;
38221
38542
  }
38222
38543
  if (!entry.isFile()) {
38223
38544
  continue;
38224
38545
  }
38225
38546
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
38226
- if (CARRIES_A_UID.has(extension) && !existsSync12(join17(directory, `${entry.name}.uid`))) {
38547
+ if (CARRIES_A_UID.has(extension) && !existsSync13(join18(directory, `${entry.name}.uid`))) {
38227
38548
  found.push(spelled);
38228
38549
  }
38229
38550
  }
@@ -38251,14 +38572,14 @@ function searchProject(projectPath, options) {
38251
38572
  if (steppedOver(directory)) {
38252
38573
  return;
38253
38574
  }
38254
- for (const entry of readdirSync6(directory, { withFileTypes: true })) {
38575
+ for (const entry of readdirSync7(directory, { withFileTypes: true })) {
38255
38576
  if (full()) {
38256
38577
  return;
38257
38578
  }
38258
38579
  if (skipped(entry.name)) {
38259
38580
  continue;
38260
38581
  }
38261
- const entryPath = join17(directory, entry.name);
38582
+ const entryPath = join18(directory, entry.name);
38262
38583
  if (entry.isDirectory()) {
38263
38584
  visit(entryPath);
38264
38585
  continue;
@@ -38299,12 +38620,12 @@ var init_project_scan = __esm(() => {
38299
38620
 
38300
38621
  // src/reimport.ts
38301
38622
  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";
38623
+ import { existsSync as existsSync14, readFileSync as readFileSync16, rmSync as rmSync8, statSync as statSync6, utimesSync, writeFileSync as writeFileSync11 } from "node:fs";
38624
+ import { basename as basename3, join as join19 } from "node:path";
38304
38625
  function forceNextImport(projectPath, resourcePath) {
38305
38626
  const relative = resourcePath.replace(/^res:\/\//, "");
38306
- const sidecar = join18(projectPath, `${relative}.import`);
38307
- if (!existsSync13(sidecar)) {
38627
+ const sidecar = join19(projectPath, `${relative}.import`);
38628
+ if (!existsSync14(sidecar)) {
38308
38629
  return;
38309
38630
  }
38310
38631
  const { atime, mtimeMs } = statSync6(sidecar);
@@ -38314,15 +38635,15 @@ function forceNextImport(projectPath, resourcePath) {
38314
38635
  }
38315
38636
  function importRecord(projectPath, resourcePath) {
38316
38637
  const digest = createHash3("md5").update(resourcePath).digest("hex");
38317
- return join18(projectPath, ".godot", "imported", `${basename3(resourcePath)}-${digest}.md5`);
38638
+ return join19(projectPath, ".godot", "imported", `${basename3(resourcePath)}-${digest}.md5`);
38318
38639
  }
38319
38640
  var init_reimport = () => {};
38320
38641
 
38321
38642
  // src/restart-note.ts
38322
38643
  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";
38644
+ import { dirname as dirname13, join as join20 } from "node:path";
38324
38645
  function restartNotePath(projectPath) {
38325
- return join19(projectPath, ".godot", "gdharness-restart.json");
38646
+ return join20(projectPath, ".godot", "gdharness-restart.json");
38326
38647
  }
38327
38648
  function noteRestartBegun(note) {
38328
38649
  const path = restartNotePath(note.projectPath);
@@ -38361,28 +38682,29 @@ var init_restart_note = __esm(() => {
38361
38682
  });
38362
38683
 
38363
38684
  // src/run-record.ts
38364
- import { execFileSync as execFileSync2 } from "node:child_process";
38685
+ import { execFile as execFile5 } from "node:child_process";
38365
38686
  import { createHash as createHash4 } from "node:crypto";
38366
38687
  import {
38367
38688
  mkdirSync as mkdirSync11,
38368
38689
  openSync as openSync2,
38369
- readdirSync as readdirSync7,
38690
+ readdirSync as readdirSync8,
38370
38691
  readFileSync as readFileSync18,
38371
38692
  realpathSync as realpathSync3,
38372
38693
  rmSync as rmSync10,
38373
38694
  statSync as statSync7,
38374
38695
  writeFileSync as writeFileSync13
38375
38696
  } from "node:fs";
38376
- import { basename as basename4, dirname as dirname14, join as join20, resolve as resolve5 } from "node:path";
38697
+ import { basename as basename4, dirname as dirname14, join as join21, resolve as resolve5 } from "node:path";
38698
+ import { promisify as promisify5 } from "node:util";
38377
38699
  function runsDirectory() {
38378
- return join20(runtimeDirectory(), "runs");
38700
+ return join21(runtimeDirectory(), "runs");
38379
38701
  }
38380
38702
  function realAsFarAsItExists(path) {
38381
38703
  const missing = [];
38382
38704
  let at = path;
38383
38705
  for (;; ) {
38384
38706
  try {
38385
- return join20(realpathSync3.native(at), ...missing);
38707
+ return join21(realpathSync3.native(at), ...missing);
38386
38708
  } catch {
38387
38709
  const parent = dirname14(at);
38388
38710
  if (parent === at) {
@@ -38399,15 +38721,15 @@ function projectKey(projectPath) {
38399
38721
  return createHash4("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
38400
38722
  }
38401
38723
  function runRecordPath(projectPath, directory = runtimeDirectory()) {
38402
- return join20(directory, "runs", `note-${projectKey(projectPath)}.json`);
38724
+ return join21(directory, "runs", `note-${projectKey(projectPath)}.json`);
38403
38725
  }
38404
38726
  function openTranscript(startedAt) {
38405
38727
  mkdirSync11(runsDirectory(), { recursive: true });
38406
- const path = join20(runsDirectory(), `run-${startedAt}.log`);
38728
+ const path = join21(runsDirectory(), `run-${startedAt}.log`);
38407
38729
  return { path, fd: openSync2(path, "a") };
38408
38730
  }
38409
38731
  function editorNotePath(projectPath, directory = runtimeDirectory()) {
38410
- return join20(directory, "runs", `editor-${projectKey(projectPath)}.json`);
38732
+ return join21(directory, "runs", `editor-${projectKey(projectPath)}.json`);
38411
38733
  }
38412
38734
  function writeEditorRunNote(note) {
38413
38735
  mkdirSync11(runsDirectory(), { recursive: true });
@@ -38415,7 +38737,7 @@ function writeEditorRunNote(note) {
38415
38737
  }
38416
38738
  function readEditorRunNote(projectPath) {
38417
38739
  for (const directory of runtimeDirectories()) {
38418
- const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join20(directory, "runs", "editor-run.json"));
38740
+ const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join21(directory, "runs", "editor-run.json"));
38419
38741
  if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
38420
38742
  return note;
38421
38743
  }
@@ -38447,7 +38769,7 @@ function readRunRecord(projectPath) {
38447
38769
  if (own !== null) {
38448
38770
  return own;
38449
38771
  }
38450
- const shared = recordAt(join20(directory, "runs", SHARED_NOTE));
38772
+ const shared = recordAt(join21(directory, "runs", SHARED_NOTE));
38451
38773
  if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
38452
38774
  return shared;
38453
38775
  }
@@ -38459,13 +38781,13 @@ function everyRunRecord() {
38459
38781
  for (const directory of runtimeDirectories()) {
38460
38782
  let entries;
38461
38783
  try {
38462
- entries = readdirSync7(join20(directory, "runs"));
38784
+ entries = readdirSync8(join21(directory, "runs"));
38463
38785
  } catch {
38464
38786
  continue;
38465
38787
  }
38466
38788
  for (const entry of entries) {
38467
38789
  if (entry === SHARED_NOTE || entry.startsWith("note-") && entry.endsWith(".json")) {
38468
- const record = recordAt(join20(directory, "runs", entry));
38790
+ const record = recordAt(join21(directory, "runs", entry));
38469
38791
  if (record !== null) {
38470
38792
  found.push(record);
38471
38793
  }
@@ -38503,49 +38825,47 @@ function recordAt(path) {
38503
38825
  ...typeof fields["exitSignal"] === "string" ? { exitSignal: fields["exitSignal"] } : {}
38504
38826
  };
38505
38827
  }
38506
- function runningAs(pid) {
38828
+ async function runningAs(pid) {
38507
38829
  if (process.platform === "win32") {
38508
- const answer = windowsCommandLine(pid);
38830
+ const answer = await windowsCommandLine(pid);
38509
38831
  if (answer !== null) {
38510
38832
  const [, ...rest] = answer.split(`
38511
38833
  `);
38512
38834
  const line = rest.join(`
38513
38835
  `).trim();
38514
- const began = startedAt(pid, answer);
38836
+ const began = await startedAt(pid, answer);
38515
38837
  if (line !== "") {
38516
38838
  return { kind: "commandLine", text: line, ...began === null ? {} : { startedAt: began } };
38517
38839
  }
38518
38840
  }
38519
- const image = windowsImage(pid);
38841
+ const image = await windowsImage(pid);
38520
38842
  return image === null ? null : { kind: "image", text: image };
38521
38843
  }
38522
38844
  try {
38523
- const began = startedAt(pid, null);
38845
+ const began = await startedAt(pid, null);
38524
38846
  const when = began === null ? {} : { startedAt: began };
38525
38847
  if (process.platform === "linux") {
38526
38848
  const raw = readFileSync18(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
38527
38849
  return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
38528
38850
  }
38529
- const args = execFileSync2("ps", ["-p", String(pid), "-o", "args="], {
38530
- encoding: "utf8",
38531
- timeout: 15000
38532
- }).trim();
38851
+ const args = (await execFileAsync("ps", ["-p", String(pid), "-o", "args="], { encoding: "utf8", timeout: 15000 })).stdout.trim();
38533
38852
  return args === "" ? null : { kind: "commandLine", text: args, ...when };
38534
38853
  } catch {
38535
38854
  return null;
38536
38855
  }
38537
38856
  }
38538
- function listeningPid(port) {
38857
+ async function listeningPid(port) {
38539
38858
  try {
38540
38859
  if (process.platform === "win32") {
38541
- return listeningPidInNetstat(execFileSync2("netstat", ["-ano"], { encoding: "utf8", timeout: 15000 }), port);
38860
+ const { stdout } = await execFileAsync("netstat", ["-ano"], { encoding: "utf8", timeout: 15000 });
38861
+ return listeningPidInNetstat(stdout, port);
38542
38862
  }
38543
38863
  for (const [command, args, pattern] of [
38544
38864
  ["lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], /^(\d+)/],
38545
38865
  ["ss", ["-ltnpH", `sport = :${port}`], /pid=(\d+)/]
38546
38866
  ]) {
38547
38867
  try {
38548
- const said = execFileSync2(command, [...args], { encoding: "utf8", timeout: 15000 }).trim();
38868
+ const said = (await execFileAsync(command, [...args], { encoding: "utf8", timeout: 15000 })).stdout.trim();
38549
38869
  const found = pattern.exec(said);
38550
38870
  if (found?.[1] !== undefined) {
38551
38871
  return Number(found[1]);
@@ -38570,39 +38890,31 @@ function listeningPidInNetstat(printed, port) {
38570
38890
  }
38571
38891
  return null;
38572
38892
  }
38573
- function windowsCommandLine(pid) {
38893
+ async function windowsCommandLine(pid) {
38574
38894
  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();
38895
+ const answer = (await askWindows(`$p = Get-CimInstance Win32_Process -Filter "ProcessId=${pid}"; if ($p) { $p.CreationDate.ToUniversalTime().ToString("o"); $p.CommandLine }`, 15000)).trim();
38581
38896
  return answer === "" ? null : answer;
38582
38897
  } catch {
38583
38898
  return null;
38584
38899
  }
38585
38900
  }
38586
- function startedAt(pid, windowsAnswer) {
38901
+ async function startedAt(pid, windowsAnswer) {
38587
38902
  if (process.platform === "win32") {
38588
38903
  const when = Date.parse(windowsAnswer?.split(`
38589
38904
  `)[0]?.trim() ?? "");
38590
38905
  return Number.isNaN(when) ? null : when;
38591
38906
  }
38592
38907
  try {
38593
- const said = execFileSync2("ps", ["-p", String(pid), "-o", "lstart="], {
38594
- encoding: "utf8",
38595
- timeout: 15000
38596
- }).trim();
38908
+ const said = (await execFileAsync("ps", ["-p", String(pid), "-o", "lstart="], { encoding: "utf8", timeout: 15000 })).stdout.trim();
38597
38909
  const when = Date.parse(said);
38598
38910
  return said === "" || Number.isNaN(when) ? null : when;
38599
38911
  } catch {
38600
38912
  return null;
38601
38913
  }
38602
38914
  }
38603
- function windowsImage(pid) {
38915
+ async function windowsImage(pid) {
38604
38916
  try {
38605
- const csv = execFileSync2("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
38917
+ const { stdout: csv } = await execFileAsync("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
38606
38918
  encoding: "utf8",
38607
38919
  timeout: 15000,
38608
38920
  windowsHide: true
@@ -38612,11 +38924,11 @@ function windowsImage(pid) {
38612
38924
  return null;
38613
38925
  }
38614
38926
  }
38615
- function stillTheRecordedRun(record) {
38616
- return judgeRun(record, runningAs(record.pid), "confirmed");
38927
+ async function stillTheRecordedRun(record) {
38928
+ return judgeRun(record, await runningAs(record.pid), "confirmed");
38617
38929
  }
38618
- function couldStillBeTheRecordedRun(record) {
38619
- return judgeRun(record, runningAs(record.pid), "possible");
38930
+ async function couldStillBeTheRecordedRun(record) {
38931
+ return judgeRun(record, await runningAs(record.pid), "possible");
38620
38932
  }
38621
38933
  function judgeRun(record, running, needed) {
38622
38934
  if (running === null) {
@@ -38643,8 +38955,8 @@ function judgeRun(record, running, needed) {
38643
38955
  const project = process.platform === "win32" ? record.projectPath.toLowerCase() : record.projectPath;
38644
38956
  return said.includes(project);
38645
38957
  }
38646
- function stillTheAnnouncedGame(pid, projectPath, announcedAt) {
38647
- return judgeAnnouncedGame(runningAs(pid), projectPath, announcedAt);
38958
+ async function stillTheAnnouncedGame(pid, projectPath, announcedAt) {
38959
+ return judgeAnnouncedGame(await runningAs(pid), projectPath, announcedAt);
38648
38960
  }
38649
38961
  function judgeAnnouncedGame(running, projectPath, announcedAt) {
38650
38962
  if (running?.kind !== "commandLine" || running.startedAt === undefined) {
@@ -38661,7 +38973,7 @@ function judgeAnnouncedGame(running, projectPath, announcedAt) {
38661
38973
  }
38662
38974
  function clearRunRecord(projectPath) {
38663
38975
  for (const directory of runtimeDirectories()) {
38664
- const shared = join20(directory, "runs", SHARED_NOTE);
38976
+ const shared = join21(directory, "runs", SHARED_NOTE);
38665
38977
  const sharedRecord = recordAt(shared);
38666
38978
  const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
38667
38979
  for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
@@ -38683,7 +38995,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
38683
38995
  const kept = new Set(everyRunRecord().map((record) => record.transcript));
38684
38996
  let entries;
38685
38997
  try {
38686
- entries = readdirSync7(directory);
38998
+ entries = readdirSync8(directory);
38687
38999
  } catch {
38688
39000
  return;
38689
39001
  }
@@ -38691,7 +39003,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
38691
39003
  if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
38692
39004
  continue;
38693
39005
  }
38694
- const path = join20(directory, entry);
39006
+ const path = join21(directory, entry);
38695
39007
  if (kept.has(path) || ageMs(path, now) < keepMs) {
38696
39008
  continue;
38697
39009
  }
@@ -38700,37 +39012,37 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
38700
39012
  } catch {}
38701
39013
  }
38702
39014
  }
38703
- var SHARED_NOTE = "run.json", SAME_RUN_WINDOW_MS = 1e4;
39015
+ var execFileAsync, SHARED_NOTE = "run.json", SAME_RUN_WINDOW_MS = 1e4;
38704
39016
  var init_run_record = __esm(() => {
38705
39017
  init_paths();
38706
39018
  init_process_children();
38707
39019
  init_runtime_client();
39020
+ init_windows_ask();
39021
+ execFileAsync = promisify5(execFile5);
38708
39022
  });
38709
39023
 
38710
39024
  // src/server.ts
38711
- import { execFile as execFile5, spawn as spawn2 } from "node:child_process";
39025
+ import { execFile as execFile6, spawn as spawn3 } from "node:child_process";
38712
39026
  import { randomUUID as randomUUID2 } from "node:crypto";
38713
39027
  import {
38714
39028
  appendFileSync,
38715
39029
  closeSync as closeSync2,
38716
39030
  constants,
38717
39031
  copyFileSync,
38718
- existsSync as existsSync14,
39032
+ existsSync as existsSync15,
38719
39033
  fstatSync,
38720
39034
  mkdirSync as mkdirSync12,
38721
- mkdtempSync as mkdtempSync2,
38722
39035
  openSync as openSync3,
38723
- readdirSync as readdirSync8,
39036
+ readdirSync as readdirSync9,
38724
39037
  readFileSync as readFileSync19,
38725
39038
  readSync as readSync2,
38726
39039
  rmdirSync as rmdirSync2,
38727
39040
  statSync as statSync8
38728
39041
  } 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";
39042
+ 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
39043
  import { setTimeout as delay3 } from "node:timers/promises";
38732
39044
  import { fileURLToPath as fileURLToPath5 } from "node:url";
38733
- import { promisify as promisify5 } from "node:util";
39045
+ import { promisify as promisify6 } from "node:util";
38734
39046
  function captureDestinationRefusal(outputPath, projects) {
38735
39047
  if (/^(res|user):\/\//.test(outputPath)) {
38736
39048
  return `outputPath ${outputPath} is a Godot path; a capture is saved outside the project, at an absolute path on this machine.`;
@@ -38742,15 +39054,15 @@ function captureDestinationRefusal(outputPath, projects) {
38742
39054
  return `outputPath ${outputPath} does not end in .png, which is what a capture is.`;
38743
39055
  }
38744
39056
  const directory = realPathOr(dirname15(outputPath));
38745
- if (!existsSync14(directory)) {
39057
+ if (!existsSync15(directory)) {
38746
39058
  return `outputPath's directory ${dirname15(outputPath)} does not exist; a capture creates no directories.`;
38747
39059
  }
38748
- const where = join21(directory, basename5(outputPath));
39060
+ const where = join22(directory, basename5(outputPath));
38749
39061
  const inside = projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
38750
39062
  if (inside !== undefined) {
38751
39063
  return `outputPath ${outputPath} is inside the project at ${inside}; a capture is saved outside it, so it never becomes part of the game.`;
38752
39064
  }
38753
- if (existsSync14(where)) {
39065
+ if (existsSync15(where)) {
38754
39066
  return `outputPath ${outputPath} already exists; a capture is not written over a file. Name a new one.`;
38755
39067
  }
38756
39068
  return null;
@@ -38906,7 +39218,8 @@ function aboutTheChildren(ended, throughEditor) {
38906
39218
  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
39219
  }
38908
39220
  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.";
39221
+ const why = whyTheProcessTreeFailed();
39222
+ 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
39223
  }
38911
39224
  if (ended.unknown === "no process") {
38912
39225
  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 +39300,6 @@ function runtimeVerdict(endpoint, after) {
38987
39300
  heldAt: null
38988
39301
  };
38989
39302
  }
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
39303
  function isOfType(value, kind) {
39002
39304
  switch (kind) {
39003
39305
  case "string":
@@ -39189,13 +39491,13 @@ function aboveTheRunner(entry) {
39189
39491
  }
39190
39492
  function elsewhereIn(projectPath, asked) {
39191
39493
  const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
39192
- if (wanted === "" || existsSync14(join21(projectPath, wanted))) {
39494
+ if (wanted === "" || existsSync15(join22(projectPath, wanted))) {
39193
39495
  return "";
39194
39496
  }
39195
39497
  const root = (wanted.split("/")[0] ?? "").toLowerCase();
39196
39498
  let near = [];
39197
39499
  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()));
39500
+ 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
39501
  } catch {
39200
39502
  return "";
39201
39503
  }
@@ -39211,7 +39513,7 @@ function runtimeAddonAnnouncesEditors(projectPath) {
39211
39513
  return false;
39212
39514
  }
39213
39515
  try {
39214
- return readFileSync19(join21(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
39516
+ return readFileSync19(join22(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
39215
39517
  } catch {
39216
39518
  return false;
39217
39519
  }
@@ -39236,7 +39538,7 @@ function needsItsOwnProcess(asked) {
39236
39538
  class GodotServer {
39237
39539
  mcp;
39238
39540
  locator = new GodotLocator;
39239
- operationsScript = join21(__dirname2, "godot", "operations", "godot_operations.gd");
39541
+ operationsScript = join22(__dirname2, "godot", "operations", "godot_operations.gd");
39240
39542
  godotBridge;
39241
39543
  tools = buildToolDefinitions();
39242
39544
  activeProcess = null;
@@ -39248,6 +39550,7 @@ class GodotServer {
39248
39550
  lastGodotVersion = null;
39249
39551
  lspClient = null;
39250
39552
  dapClient = null;
39553
+ adapterPassed = null;
39251
39554
  breakpointsSeededOn = null;
39252
39555
  bridgeStartupError = null;
39253
39556
  bridgeRetry = null;
@@ -39306,6 +39609,7 @@ class GodotServer {
39306
39609
  this.handleShutdown("stdin:end", 0);
39307
39610
  });
39308
39611
  console.error("gdharness running on stdio");
39612
+ sweepAbandonedScratch();
39309
39613
  try {
39310
39614
  await this.takeOverFromAPredecessor();
39311
39615
  await this.godotBridge.start();
@@ -39632,7 +39936,7 @@ class GodotServer {
39632
39936
  this.callsSinceFeedback = 0;
39633
39937
  return {
39634
39938
  ...answer,
39635
- content: [...answer.content, { type: "text", text: feedbackNotice() }]
39939
+ content: [...answer.content, { type: "text", text: feedbackNotice(FEEDBACK_NOTICE_EVERY) }]
39636
39940
  };
39637
39941
  }
39638
39942
  validateArguments(spec, args) {
@@ -39976,8 +40280,8 @@ class GodotServer {
39976
40280
  if (path === undefined) {
39977
40281
  return { ok: false, response: this.createErrorResponse("projectPath is required.") };
39978
40282
  }
39979
- const file = join21(path, "project.godot");
39980
- if (!existsSync14(file)) {
40283
+ const file = join22(path, "project.godot");
40284
+ if (!existsSync15(file)) {
39981
40285
  return {
39982
40286
  ok: false,
39983
40287
  response: this.createErrorResponse(`Not a Godot project: ${path}`, [
@@ -40138,13 +40442,13 @@ class GodotServer {
40138
40442
  let sawAScan = false;
40139
40443
  let scanEndedAt = null;
40140
40444
  for (;; ) {
40141
- const asked = Date.now();
40142
40445
  let status;
40143
40446
  try {
40144
40447
  status = asParams(await this.godotBridge.invokeTool("scan_status", {}));
40145
40448
  } catch {
40146
40449
  return { waitedMs: Date.now() - started, stillScanning: false };
40147
40450
  }
40451
+ const answered = Date.now();
40148
40452
  if (status["scanning"] === true || status["importing"] === true || status["pending"] === true) {
40149
40453
  sawAScan = true;
40150
40454
  if (Date.now() - started >= SCAN_WAIT_MS) {
@@ -40154,7 +40458,7 @@ class GodotServer {
40154
40458
  continue;
40155
40459
  }
40156
40460
  const since = readNumber(status, "sinceScanFinishedMs");
40157
- scanEndedAt = since !== undefined && since >= 0 ? asked - since : sawAScan ? asked : null;
40461
+ scanEndedAt = since !== undefined && since >= 0 ? answered - since : sawAScan ? answered : null;
40158
40462
  break;
40159
40463
  }
40160
40464
  let waitedForTheWrite = false;
@@ -40573,11 +40877,11 @@ class GodotServer {
40573
40877
  return this.createErrorResponse(`Cannot create ${dirname15(output.relativePath)} to export into: ${errorMessage(error)}`);
40574
40878
  }
40575
40879
  const debug = readBoolean(args, "debug") ?? false;
40576
- const exportLogs = mkdtempSync2(join21(tmpdir4(), "gdharness-export-"));
40880
+ const exportLogs = scratchDirectory("export");
40577
40881
  const exportArgs = [
40578
40882
  "--headless",
40579
40883
  "--log-file",
40580
- join21(exportLogs, "engine.log"),
40884
+ join22(exportLogs, "engine.log"),
40581
40885
  "--path",
40582
40886
  project.value.path,
40583
40887
  debug ? "--export-debug" : "--export-release",
@@ -40609,7 +40913,7 @@ class GodotServer {
40609
40913
  log.finish();
40610
40914
  const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
40611
40915
  const verdict = {
40612
- exported: ending.exitCode === 0 && log.count("error") === 0 && existsSync14(output.absolutePath),
40916
+ exported: ending.exitCode === 0 && log.count("error") === 0 && existsSync15(output.absolutePath),
40613
40917
  preset,
40614
40918
  outputPath: output.relativePath,
40615
40919
  debug,
@@ -40645,7 +40949,7 @@ class GodotServer {
40645
40949
  return contained.response;
40646
40950
  }
40647
40951
  const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
40648
- if (!existsSync14(join21(project.value.path, runner))) {
40952
+ if (!existsSync15(join22(project.value.path, runner))) {
40649
40953
  return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
40650
40954
  "Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
40651
40955
  ]);
@@ -40681,7 +40985,7 @@ class GodotServer {
40681
40985
  ];
40682
40986
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
40683
40987
  this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
40684
- const userData = mkdtempSync2(join21(tmpdir4(), "gdharness-tests-"));
40988
+ const userData = scratchDirectory("tests");
40685
40989
  const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
40686
40990
  const console2 = [];
40687
40991
  let lastSaid = Date.now();
@@ -40708,14 +41012,17 @@ class GodotServer {
40708
41012
  resolve(false);
40709
41013
  });
40710
41014
  });
40711
- const reportsDir = join21(project.value.path, ".godot", ...ours.split("/"));
41015
+ if (timedOut && run.process.pid !== undefined) {
41016
+ await untilGone([run.process.pid]);
41017
+ }
41018
+ const reportsDir = join22(project.value.path, ".godot", ...ours.split("/"));
40712
41019
  let report = null;
40713
41020
  let reportProblem = null;
40714
41021
  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))) : [];
41022
+ 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
41023
  const newest = written.at(-1);
40717
41024
  if (newest !== undefined) {
40718
- report = parseJUnit(readFileSync19(join21(reportsDir, newest, "results.xml"), "utf8"));
41025
+ report = parseJUnit(readFileSync19(join22(reportsDir, newest, "results.xml"), "utf8"));
40719
41026
  }
40720
41027
  } catch (error) {
40721
41028
  reportProblem = errorMessage(error);
@@ -40873,7 +41180,7 @@ class GodotServer {
40873
41180
  const cached = cachedClasses(projectPath);
40874
41181
  const contradicted = cached === null ? [] : contradictedDiagnostics(messages, cached, (resourcePath) => {
40875
41182
  const contained = resolveWithinProject(projectPath, resourcePath);
40876
- if (!contained.ok || !existsSync14(contained.absolutePath)) {
41183
+ if (!contained.ok || !existsSync15(contained.absolutePath)) {
40877
41184
  return null;
40878
41185
  }
40879
41186
  try {
@@ -40927,7 +41234,7 @@ class GodotServer {
40927
41234
  }
40928
41235
  sourceOfClass(projectPath, resourcePath) {
40929
41236
  const contained = resolveWithinProject(projectPath, resourcePath);
40930
- if (!contained.ok || !existsSync14(contained.absolutePath)) {
41237
+ if (!contained.ok || !existsSync15(contained.absolutePath)) {
40931
41238
  return null;
40932
41239
  }
40933
41240
  try {
@@ -40948,28 +41255,32 @@ class GodotServer {
40948
41255
  async handleDAP(toolName, args) {
40949
41256
  return handleDAPTool(this.dap(), toolName, args);
40950
41257
  }
40951
- adapterBelongsElsewhere() {
41258
+ async adapterBelongsElsewhere() {
40952
41259
  const status = this.godotBridge.getStatus();
40953
41260
  const ours = status.editorPid;
40954
41261
  if (ours === undefined) {
40955
41262
  return null;
40956
41263
  }
41264
+ const connection = this.dapClient?.connection() ?? null;
41265
+ if (connection !== null && this.adapterPassed?.connection === connection && this.adapterPassed.editorPid === ours) {
41266
+ return null;
41267
+ }
40957
41268
  const port = this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT);
40958
- const holder = listeningPid(port);
41269
+ const askedAt = Date.now();
41270
+ const holder = await listeningPid(port);
41271
+ this.logDebug(`Asked who holds debug adapter port ${port} in ${Date.now() - askedAt}ms: ${holder ?? "not said"}`);
40959
41272
  if (holder === null || holder === ours) {
41273
+ this.adapterPassed = connection === null ? null : { connection, editorPid: ours };
40960
41274
  return null;
40961
41275
  }
40962
41276
  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
41277
  }
40964
- anEditorIsStillComing(listeningSince) {
40965
- return anEditorIsStillComing(listeningSince, this.launchedEditorIsUp());
40966
- }
40967
- launchedEditorIsUp() {
41278
+ async launchedEditorIsUp() {
40968
41279
  const launched = this.launchedEditor;
40969
41280
  if (launched === null || !alive(launched.pid)) {
40970
41281
  return false;
40971
41282
  }
40972
- const now = startTimesOf([launched.pid]).get(launched.pid);
41283
+ const now = (await startTimesOf([launched.pid])).get(launched.pid);
40973
41284
  return launched.startedAt === null || now === undefined || now === launched.startedAt;
40974
41285
  }
40975
41286
  dap() {
@@ -41058,8 +41369,9 @@ class GodotServer {
41058
41369
  }
41059
41370
  return owed;
41060
41371
  }
41061
- getEditorStatusPayload() {
41372
+ async getEditorStatusPayload() {
41062
41373
  const status = this.godotBridge.getStatus();
41374
+ const launchedIsUp = !status.connected && await this.launchedEditorIsUp();
41063
41375
  const isPortConflict = this.bridgeStartupError?.includes("EADDRINUSE") ?? false;
41064
41376
  const stale = status.connected && editorIsStale(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest());
41065
41377
  const unfinished = this.restartLeftUnfinished(status.connected);
@@ -41068,14 +41380,14 @@ class GodotServer {
41068
41380
  serverVersion: SERVER_VERSION,
41069
41381
  addonIsStale: status.connected ? stale : undefined,
41070
41382
  bridgeAvailable: this.bridgeStartupError === null,
41071
- mayYetConnect: status.connected ? undefined : unfinished !== null ? false : this.anEditorIsStillComing(status.listeningSince),
41383
+ mayYetConnect: status.connected ? undefined : unfinished !== null ? false : anEditorIsStillComing(status.listeningSince, launchedIsUp),
41072
41384
  restartInterrupted: unfinished === null ? undefined : {
41073
41385
  quitEditorPid: unfinished.editorPid,
41074
41386
  quitAt: unfinished.quitAt,
41075
41387
  byServerPid: unfinished.byPid,
41076
41388
  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
41389
  },
41078
- awaitingLaunchedEditor: status.connected || !this.launchedEditorIsUp() ? undefined : this.launchedEditor?.pid ?? undefined,
41390
+ awaitingLaunchedEditor: status.connected || !launchedIsUp ? undefined : this.launchedEditor?.pid ?? undefined,
41079
41391
  ...status.connected ? this.whatTheLaunchedEditorDropped() : {},
41080
41392
  startupError: this.bridgeStartupError,
41081
41393
  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 +41476,7 @@ class GodotServer {
41164
41476
  const asked = (async () => {
41165
41477
  const playing = await this.editorPlayingState();
41166
41478
  await this.pickUpWhatTheEditorIsPlaying();
41167
- const current = this.currentRun();
41479
+ const current = await this.currentRun();
41168
41480
  const active = current === null ? false : await this.runStillGoing(current, playing?.playing ?? null);
41169
41481
  return {
41170
41482
  playing,
@@ -41172,14 +41484,15 @@ class GodotServer {
41172
41484
  hold: current === null ? {} : await this.holdOf(current, active)
41173
41485
  };
41174
41486
  })();
41175
- const [games, { playing, active, hold }, scan] = await Promise.all([
41487
+ const [games, { playing, active, hold }, scan, editorStatus] = await Promise.all([
41176
41488
  pinged,
41177
41489
  asked,
41178
- this.editorScanState()
41490
+ this.editorScanState(),
41491
+ this.getEditorStatusPayload()
41179
41492
  ]);
41180
41493
  return this.jsonTextResponse({
41181
41494
  editor: {
41182
- ...this.getEditorStatusPayload(),
41495
+ ...editorStatus,
41183
41496
  debugPort: playing?.debugPort ?? this.godotBridge.getStatus().debugPort,
41184
41497
  scan
41185
41498
  },
@@ -41209,7 +41522,7 @@ class GodotServer {
41209
41522
  return null;
41210
41523
  }
41211
41524
  try {
41212
- return settingKeys(readFileSync19(join21(projectPath, "project.godot"), "utf8"));
41525
+ return settingKeys(readFileSync19(join22(projectPath, "project.godot"), "utf8"));
41213
41526
  } catch {
41214
41527
  return null;
41215
41528
  }
@@ -41358,7 +41671,7 @@ class GodotServer {
41358
41671
  writeEditorLogNote(projectPath, launched.pid);
41359
41672
  this.launchedEditor = {
41360
41673
  pid: launched.pid,
41361
- startedAt: startTimesOf([launched.pid]).get(launched.pid) ?? null
41674
+ startedAt: (await startTimesOf([launched.pid])).get(launched.pid) ?? null
41362
41675
  };
41363
41676
  return { pid: launched.pid, error: null };
41364
41677
  }
@@ -41406,13 +41719,15 @@ class GodotServer {
41406
41719
  }
41407
41720
  await this.pickUpWhatTheEditorIsPlaying();
41408
41721
  let ended = null;
41409
- const before = this.currentRun();
41722
+ const before = await this.currentRun();
41410
41723
  if (before !== null && await this.runStillGoing(before)) {
41411
41724
  this.logDebug("Ending the running game before starting another");
41725
+ const endingFrom = Date.now();
41412
41726
  ended = {
41413
41727
  pid: before.pid ?? this.announcedPidOf(before) ?? null,
41414
41728
  ending: await this.endActiveGame("editor_run start, which ends the run that was going before it starts another", true)
41415
41729
  };
41730
+ this.logDebug(`The running game was ${ended.ending} after ${Date.now() - endingFrom}ms`);
41416
41731
  }
41417
41732
  const alreadyPlaying = new Set(discoverRuntimes().map((endpoint) => endpoint.pid));
41418
41733
  const wereHereFirst = this.everyRuntimeOfOurs();
@@ -41422,14 +41737,16 @@ class GodotServer {
41422
41737
  });
41423
41738
  const runtimeWaitMs = this.announceBudget(args, project.value.path);
41424
41739
  const editorWouldPlay = !headless || editorPlaysHeadless(project.value.file);
41425
- if (this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0 && !needsItsOwnProcess(asked.value)) {
41740
+ const silent = resolveSilent(args["silent"], process.env) && !headless;
41741
+ if (this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0 && !needsItsOwnProcess(asked.value) && !silent) {
41426
41742
  return await this.playThroughEditor(sceneArgument, refreshed.value, scanned, project.value.path, alreadyPlaying, runtimeWaitMs, ended);
41427
41743
  }
41428
41744
  const cmdArgs = runArguments({
41429
41745
  projectPath: project.value.path,
41430
41746
  headless,
41431
41747
  scene: sceneArgument,
41432
- userArgs: given.value
41748
+ userArgs: given.value,
41749
+ silent
41433
41750
  });
41434
41751
  this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
41435
41752
  const started = await this.launchKeptGame(engine.value, cmdArgs, project.value.path, needsItsOwnProcess(asked.value) ? environmentFor({ ...asked.value, runtimeDirectory: runtimeDirectory() }) : undefined, headless || args["visible"] === true ? undefined : HIDDEN_DESKTOP);
@@ -41440,7 +41757,7 @@ class GodotServer {
41440
41757
  }
41441
41758
  started.announcedBefore = alreadyPlaying;
41442
41759
  this.activeProcess = started;
41443
- const spawnedInstead = this.godotBridge.isConnected() && editorWouldPlay && given.value.length > 0 ? " The editor cannot be handed arguments for a game it plays, so this one was started " + "here: the debug_* tools answer only for a game the editor is playing." : this.godotBridge.isConnected() && editorWouldPlay && needsItsOwnProcess(asked.value) ? " The editor plays a game with its own environment, so a run given one was started " + "here: the debug_* tools answer only for a game the editor is playing." : "";
41760
+ const spawnedInstead = this.godotBridge.isConnected() && editorWouldPlay && given.value.length > 0 ? " The editor cannot be handed arguments for a game it plays, so this one was started " + "here: the debug_* tools answer only for a game the editor is playing." : this.godotBridge.isConnected() && editorWouldPlay && needsItsOwnProcess(asked.value) ? " The editor plays a game with its own environment, so a run given one was started " + "here: the debug_* tools answer only for a game the editor is playing." : this.godotBridge.isConnected() && editorWouldPlay && silent ? " The editor cannot play a game silent, which takes an engine flag, so this one was " + `started here${args["silent"] === true ? "" : ", silent because GDHARNESS_SILENT says so"}: the debug_* tools answer only for a game the editor is playing, and silent: false plays it through the editor.` : "";
41444
41761
  const endedForThis = endedToStartThis(ended);
41445
41762
  const stranded = wereHereFirst.filter((one) => one.pid !== ended?.pid && alive(one.pid));
41446
41763
  const alsoRunning = leftRunningNote(stranded);
@@ -41462,7 +41779,7 @@ class GodotServer {
41462
41779
  }
41463
41780
  async runtimeUp(projectPath, before, budget, withArgs) {
41464
41781
  const budgetMs = budget.ms;
41465
- if (!existsSync14(join21(projectPath, RUNTIME_AUTOLOAD.path))) {
41782
+ if (!existsSync15(join22(projectPath, RUNTIME_AUTOLOAD.path))) {
41466
41783
  return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
41467
41784
  }
41468
41785
  let asking = null;
@@ -41473,12 +41790,12 @@ class GodotServer {
41473
41790
  const lastSaid = () => editorSaysGoing ? "playing" : "not playing";
41474
41791
  const endpoint = await announcedSince(projectPath, before, {
41475
41792
  budgetMs,
41476
- accept: (one) => this.isTheGameOf(this.currentRun(), one, projectPath),
41793
+ accept: (one) => this.isTheGameOf(this.activeProcess, one, projectPath),
41477
41794
  giveUp: () => {
41478
41795
  if (this.dapClient?.isStopped() === true) {
41479
41796
  return true;
41480
41797
  }
41481
- const run = this.currentRun();
41798
+ const run = this.activeProcess;
41482
41799
  if (run === null || !run.throughEditor) {
41483
41800
  return !stillRunning(run);
41484
41801
  }
@@ -41496,7 +41813,7 @@ class GodotServer {
41496
41813
  });
41497
41814
  const asked = editorAsks === 0 ? "editor not asked" : `editor asked ${editorAsks === 1 ? "once" : `${editorAsks} times`}, last said ${lastSaid()}`;
41498
41815
  this.logDebug(`announce wait ended after ${Date.now() - waitBegan}ms: ${endpoint === null ? "nothing announced" : `pid ${endpoint.pid} announced`}, ${asked}`);
41499
- const going = this.currentRun();
41816
+ const going = await this.currentRun();
41500
41817
  if (endpoint !== null && going !== null && going.announcedPid === undefined) {
41501
41818
  this.tie(going, endpoint);
41502
41819
  }
@@ -41537,8 +41854,10 @@ class GodotServer {
41537
41854
  }
41538
41855
  async playThroughEditor(scene, refreshedClasses, scanned, projectPath, alreadyPlaying, runtimeWaitMs, ended) {
41539
41856
  const log = new GameLog;
41857
+ const connectingFrom = Date.now();
41540
41858
  try {
41541
41859
  await this.dap().connect();
41860
+ this.logDebug(`The debug adapter was connected after ${Date.now() - connectingFrom}ms`);
41542
41861
  this.dapClient?.getOutput(true);
41543
41862
  } catch (error) {
41544
41863
  return this.createErrorResponse(`The editor is connected but its debug adapter is not: ${errorMessage(error)}`, [
@@ -41546,7 +41865,7 @@ class GodotServer {
41546
41865
  "GDHARNESS_DAP_PORT points this server at another one"
41547
41866
  ]);
41548
41867
  }
41549
- const stranger = this.adapterBelongsElsewhere();
41868
+ const stranger = await this.adapterBelongsElsewhere();
41550
41869
  if (stranger !== null) {
41551
41870
  return this.createErrorResponse(stranger, [
41552
41871
  "editor_status names the editor on the bridge and the port it says it serves",
@@ -41554,7 +41873,9 @@ class GodotServer {
41554
41873
  "Closing the other editor, or moving its debug adapter port in its editor settings, frees this one"
41555
41874
  ]);
41556
41875
  }
41876
+ const sendingFrom = Date.now();
41557
41877
  const breakpoints = await this.dapHoldingBreakpointsOf(projectPath).reapplyBreakpoints();
41878
+ this.logDebug(`Breakpoints were sent again after ${Date.now() - sendingFrom}ms`);
41558
41879
  const answer = await this.handleViaBridge("play_scene", scene === null ? {} : { scenePath: `res://${scene}` });
41559
41880
  if (answer.isError === true) {
41560
41881
  return answer;
@@ -41740,13 +42061,13 @@ class GodotServer {
41740
42061
  arguments: [],
41741
42062
  ...running.command === undefined ? {} : { command: running.command }
41742
42063
  };
41743
- if (!stillTheRecordedRun(startedAs)) {
42064
+ if (!await stillTheRecordedRun(startedAs)) {
41744
42065
  clearTheRecord();
41745
42066
  running.endedHere = null;
41746
42067
  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
42068
  return "refused";
41748
42069
  }
41749
- const signalled = runningAs(running.pid);
42070
+ const signalled = await runningAs(running.pid);
41750
42071
  const described = signalled === null ? "nothing it would name" : `${signalled.kind}: ${signalled.text}`;
41751
42072
  let landed = true;
41752
42073
  try {
@@ -41767,7 +42088,7 @@ class GodotServer {
41767
42088
  return gone ? "gone" : "lingering";
41768
42089
  }
41769
42090
  spawnGame(godotPath, cmdArgs, env) {
41770
- const child = spawn2(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
42091
+ const child = spawn3(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
41771
42092
  const cancelled = callSignal();
41772
42093
  if (cancelled !== undefined) {
41773
42094
  const end = () => {
@@ -41934,8 +42255,8 @@ class GodotServer {
41934
42255
  }
41935
42256
  return run.announcedPid;
41936
42257
  }
41937
- ownGameAmong(running, projectPath) {
41938
- const run = this.currentRun();
42258
+ async ownGameAmong(running, projectPath) {
42259
+ const run = await this.currentRun();
41939
42260
  if (run === null || !stillRunning(run)) {
41940
42261
  return;
41941
42262
  }
@@ -41946,7 +42267,7 @@ class GodotServer {
41946
42267
  return its !== undefined && running.some((one) => one.pid === its) ? its : undefined;
41947
42268
  }
41948
42269
  async ownGameThroughTheTree(running, projectPath) {
41949
- const run = this.currentRun();
42270
+ const run = await this.currentRun();
41950
42271
  if (run === null || !stillRunning(run) || run.announcedPid !== undefined) {
41951
42272
  return;
41952
42273
  }
@@ -42032,7 +42353,7 @@ class GodotServer {
42032
42353
  const editor = this.godotBridge.getStatus().editorPid;
42033
42354
  return endpoint.editorPid === undefined || endpoint.editorPid !== editor;
42034
42355
  }
42035
- adoptRecordedRun() {
42356
+ async adoptRecordedRun() {
42036
42357
  const project = this.ourProject();
42037
42358
  const record = project === null ? null : readRunRecord(project);
42038
42359
  if (record === null || !this.couldBeOurs(record.projectPath)) {
@@ -42053,7 +42374,7 @@ class GodotServer {
42053
42374
  pickedUp: true
42054
42375
  };
42055
42376
  this.drainTranscript(adopted);
42056
- if (!alive(record.pid) || !couldStillBeTheRecordedRun(record)) {
42377
+ if (!alive(record.pid) || !await couldStillBeTheRecordedRun(record)) {
42057
42378
  adopted.log.finish();
42058
42379
  adopted.exitCode = record.exitCode ?? null;
42059
42380
  adopted.exitSignal = record.exitSignal ?? null;
@@ -42134,8 +42455,8 @@ class GodotServer {
42134
42455
  const status = this.godotBridge.getStatus();
42135
42456
  return this.ownProject ?? (status.connected ? status.projectPath ?? null : null);
42136
42457
  }
42137
- currentRun() {
42138
- return this.activeProcess ?? this.adoptRecordedRun();
42458
+ async currentRun() {
42459
+ return this.activeProcess ?? await this.adoptRecordedRun();
42139
42460
  }
42140
42461
  async checkBoot(godotPath, projectPath, scene, args, userArgs, scanned, asked = {}) {
42141
42462
  const frames = readPositiveNumber(args, "frames") ?? 3;
@@ -42174,10 +42495,14 @@ class GodotServer {
42174
42495
  }
42175
42496
  async handleWaitForRun(args) {
42176
42497
  await this.pickUpWhatTheEditorIsPlaying();
42177
- const run = this.currentRun();
42498
+ const run = await this.currentRun();
42178
42499
  if (!run) {
42179
42500
  return this.createErrorResponse(this.nothingOfOursIsRunning());
42180
42501
  }
42502
+ const elsewhere = this.anotherProjectsRun(run.projectPath, args, "waited on");
42503
+ if (elsewhere !== null) {
42504
+ return elsewhere;
42505
+ }
42181
42506
  const budgetMs = readPositiveNumber(args, "timeoutMs") ?? WAIT_FOR_RUN_MS;
42182
42507
  const until = Date.now() + budgetMs;
42183
42508
  const cancelled = callSignal();
@@ -42224,7 +42549,7 @@ class GodotServer {
42224
42549
  await delay3(50);
42225
42550
  }
42226
42551
  }
42227
- handleEditorConsole(args) {
42552
+ async handleEditorConsole(args) {
42228
42553
  const status = this.godotBridge.getStatus();
42229
42554
  const projectPath = status.projectPath ?? this.ownProject ?? undefined;
42230
42555
  if (projectPath === undefined) {
@@ -42261,10 +42586,11 @@ class GodotServer {
42261
42586
  const filtered = severity !== "info" || contains !== undefined;
42262
42587
  const grouped = bursts(everything, readNonNegativeNumber(args, "before") ?? 3, filtered ? (entry) => answersTo(entry, severity, contains) : undefined);
42263
42588
  const shown = grouped.slice(0, MOST_GROUPS);
42589
+ const stillComing = !status.connected && await this.launchedEditorIsUp();
42264
42590
  return this.jsonTextResponse({
42265
42591
  editorPid: editorPid ?? undefined,
42266
42592
  projectPath,
42267
- ...status.connected ? {} : this.launchedEditorIsUp() ? { stillConnecting: true } : { exitedBeforeConnecting: true },
42593
+ ...status.connected ? {} : stillComing ? { stillConnecting: true } : { exitedBeforeConnecting: true },
42268
42594
  capturedIn: console2.path,
42269
42595
  ...console2.of === undefined ? {} : {
42270
42596
  readFromTheStart: console2.readBytes,
@@ -42288,7 +42614,7 @@ class GodotServer {
42288
42614
  return this.handleEditorConsole(args);
42289
42615
  }
42290
42616
  await this.pickUpWhatTheEditorIsPlaying();
42291
- const run = this.currentRun();
42617
+ const run = await this.currentRun();
42292
42618
  if (!run) {
42293
42619
  return this.createErrorResponse(this.nothingOfOursIsRunning());
42294
42620
  }
@@ -42327,7 +42653,7 @@ class GodotServer {
42327
42653
  notes.push(endedWithoutACode({
42328
42654
  throughEditor: run.throughEditor,
42329
42655
  announced: announced !== undefined || run.announcedPid !== undefined,
42330
- couldAnnounce: run.projectPath !== null && existsSync14(join21(run.projectPath, RUNTIME_AUTOLOAD.path)),
42656
+ couldAnnounce: run.projectPath !== null && existsSync15(join22(run.projectPath, RUNTIME_AUTOLOAD.path)),
42331
42657
  endedHere: typeof run.endedHere === "string",
42332
42658
  pickedUp: run.pickedUp === true
42333
42659
  }));
@@ -42365,16 +42691,27 @@ class GodotServer {
42365
42691
  entries: forAnswer(selected.entries)
42366
42692
  });
42367
42693
  }
42694
+ anotherProjectsRun(runsProject, args, act) {
42695
+ const named = readNonEmptyString(args, "projectPath");
42696
+ if (named === undefined || runsProject === null || isSameDirectory(runsProject, named)) {
42697
+ return null;
42698
+ }
42699
+ 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.`);
42700
+ }
42368
42701
  async handleStopProject(args) {
42369
42702
  await this.pickUpWhatTheEditorIsPlaying();
42370
- const stopped = this.currentRun();
42703
+ const stopped = await this.currentRun();
42371
42704
  const named = readNumber(args, "pid");
42372
42705
  if (named !== undefined && (stopped === null || named !== stopped.pid && named !== this.announcedPidOf(stopped))) {
42373
- return await this.stopAnnouncedGame(named, readBoolean(args, "andChildren") === true);
42706
+ return await this.stopAnnouncedGame(named, readBoolean(args, "andChildren") === true, args);
42374
42707
  }
42375
42708
  if (!stopped) {
42376
42709
  return this.createErrorResponse(this.nothingOfOursIsRunning());
42377
42710
  }
42711
+ const elsewhere = this.anotherProjectsRun(stopped.projectPath, args, "stopped");
42712
+ if (elsewhere !== null) {
42713
+ return elsewhere;
42714
+ }
42378
42715
  this.drainEditorOutput(stopped);
42379
42716
  this.drainTranscript(stopped);
42380
42717
  const wasRunning = await this.runStillGoing(stopped);
@@ -42416,16 +42753,22 @@ class GodotServer {
42416
42753
  entries: forAnswer(stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
42417
42754
  });
42418
42755
  }
42419
- async stopAnnouncedGame(pid, andChildren) {
42756
+ async stopAnnouncedGame(pid, andChildren, args) {
42420
42757
  const announced = runtimesAnnounced();
42421
42758
  const game = [
42422
42759
  ...this.allAnnouncedForOurProject(announced.running),
42423
42760
  ...this.allAnnouncedForOurProject(announced.unspoken)
42424
42761
  ].find((one) => one.pid === pid);
42762
+ if (game !== undefined) {
42763
+ const elsewhere = this.anotherProjectsRun(game.project.path, args, "signalled");
42764
+ if (elsewhere !== null) {
42765
+ return elsewhere;
42766
+ }
42767
+ }
42425
42768
  if (game === undefined) {
42426
42769
  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
42770
  }
42428
- const read = game.announcedAt === undefined ? null : stillTheAnnouncedGame(pid, game.project.path, game.announcedAt);
42771
+ const read = game.announcedAt === undefined ? null : await stillTheAnnouncedGame(pid, game.project.path, game.announcedAt);
42429
42772
  if (read === null) {
42430
42773
  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
42774
  }
@@ -42623,6 +42966,8 @@ class GodotServer {
42623
42966
  }
42624
42967
  const checked = busy ? { unseen: [] } : await this.classesTheEditorCannotSee(args);
42625
42968
  const unseen = checked.unseen;
42969
+ const broughtIn = blind.unseen.map((one) => one.className).filter((name) => !unseen.some((still) => still.className === name));
42970
+ const dependents = busy || projectPath === "" || broughtIn.length === 0 ? null : await this.reloadDependents(projectPath, broughtIn);
42626
42971
  const stillGone = lost.filter((name) => !restored.includes(name));
42627
42972
  const notes = [];
42628
42973
  if (busy) {
@@ -42634,13 +42979,16 @@ class GodotServer {
42634
42979
  } else if (unseen.length > 0) {
42635
42980
  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
42981
  }
42982
+ if (dependents !== null && dependents.notReloaded.length > 0) {
42983
+ 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.`);
42984
+ }
42637
42985
  if (stillAtMissingPaths.length > 0) {
42638
42986
  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
42987
  } else if (dropped.length > 0) {
42640
42988
  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
42989
  }
42642
42990
  return this.jsonTextResponse({
42643
- ok: !busy && unseen.length === 0 && stillGone.length === 0 && stillAtMissingPaths.length === 0 && checked.unchecked === undefined,
42991
+ ok: !busy && unseen.length === 0 && stillGone.length === 0 && stillAtMissingPaths.length === 0 && (dependents === null || dependents.notReloaded.length === 0) && checked.unchecked === undefined,
42644
42992
  stillWorking: busy,
42645
42993
  waitedMs: Date.now() - started,
42646
42994
  waitedForEditorScanMs: waitedForEditorMs > 0 ? waitedForEditorMs : undefined,
@@ -42648,6 +42996,9 @@ class GodotServer {
42648
42996
  reloadedMethods: reloaded.methods,
42649
42997
  reloadProblem: reloaded.problem,
42650
42998
  unseenByEditor: unseen.length > 0 ? unseen : undefined,
42999
+ broughtIn: broughtIn.length > 0 ? broughtIn : undefined,
43000
+ dependentsReloaded: dependents !== null && dependents.reloaded.length > 0 ? dependents.reloaded : undefined,
43001
+ dependentsNotReloaded: dependents !== null && dependents.notReloaded.length > 0 ? dependents.notReloaded : undefined,
42651
43002
  cacheLost: lost.length > 0 ? lost : undefined,
42652
43003
  cacheRestored: restored.length > 0 ? restored : undefined,
42653
43004
  cacheDropped: dropped.length > 0 ? dropped : undefined,
@@ -42656,6 +43007,33 @@ class GodotServer {
42656
43007
  note: notes.length > 0 ? notes.join(" ") : undefined
42657
43008
  });
42658
43009
  }
43010
+ async reloadDependents(projectPath, classes) {
43011
+ const reloaded = [];
43012
+ const notReloaded = [];
43013
+ for (const { script: scriptPath, tool } of scriptsNaming(projectPath, classes)) {
43014
+ if (tool) {
43015
+ notReloaded.push({
43016
+ scriptPath,
43017
+ problem: "a @tool script, which runs inside the editor, so it is not reloaded from here; editor_launch restart recompiles it"
43018
+ });
43019
+ continue;
43020
+ }
43021
+ try {
43022
+ const answer = asParams(await this.godotBridge.invokeTool("reload_script", { scriptPath }));
43023
+ if (readArray(answer, "methods") === undefined) {
43024
+ notReloaded.push({
43025
+ scriptPath,
43026
+ problem: "the editor would not say what it has after reloading it"
43027
+ });
43028
+ } else {
43029
+ reloaded.push(scriptPath);
43030
+ }
43031
+ } catch (error) {
43032
+ notReloaded.push({ scriptPath, problem: errorMessage(error) });
43033
+ }
43034
+ }
43035
+ return { reloaded, notReloaded };
43036
+ }
42659
43037
  async rebuildCacheAfterLoss(projectPath, lost) {
42660
43038
  if (projectPath === "") {
42661
43039
  return [];
@@ -42838,9 +43216,9 @@ class GodotServer {
42838
43216
  return { heldAt: undefined, heldUnknown: true, heldNote: reply.message };
42839
43217
  }
42840
43218
  async ownRunOnItsWay(projectPath, budgetMs) {
42841
- const run = this.currentRun();
43219
+ const run = await this.currentRun();
42842
43220
  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)) {
43221
+ 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
43222
  return null;
42845
43223
  }
42846
43224
  const already = runtimesAnnounced().running;
@@ -42866,8 +43244,8 @@ class GodotServer {
42866
43244
  }
42867
43245
  const endpoint = await announcedSince(project, new Set(already.map((one) => one.pid)), {
42868
43246
  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)
43247
+ accept: (one) => this.isTheGameOf(this.activeProcess, one, project),
43248
+ giveUp: () => this.activeProcess !== run || this.dapClient?.isStopped() === true || !stillRunning(run)
42871
43249
  });
42872
43250
  if (endpoint !== null) {
42873
43251
  const tiedMeanwhile = () => run.announcedPid !== undefined;
@@ -42876,7 +43254,7 @@ class GodotServer {
42876
43254
  }
42877
43255
  return null;
42878
43256
  }
42879
- if (this.currentRun() !== run || !await this.runStillGoing(run)) {
43257
+ if (await this.currentRun() !== run || !await this.runStillGoing(run)) {
42880
43258
  return null;
42881
43259
  }
42882
43260
  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 +43270,7 @@ class GodotServer {
42892
43270
  }
42893
43271
  const announced = runtimesAnnounced();
42894
43272
  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);
43273
+ const own = typeof pid === "number" ? undefined : await this.ownGameAmong(announced.running, wanted) ?? await this.ownGameThroughTheTree(announced.running, wanted);
42896
43274
  const choice = chooseRuntime(announced.running, wanted, announced.unspoken, typeof pid === "number" ? pid : own);
42897
43275
  if ("problem" in choice) {
42898
43276
  return this.createErrorResponse(choice.problem);
@@ -42919,8 +43297,8 @@ class GodotServer {
42919
43297
  return this.createErrorResponse(refusal);
42920
43298
  }
42921
43299
  }
42922
- const screenshotDir = expectsScreenshot ? mkdtempSync2(join21(tmpdir4(), "gdharness-runtime-screenshot-")) : null;
42923
- const screenshotPath = screenshotDir ? join21(screenshotDir, "capture.png") : null;
43300
+ const screenshotDir = expectsScreenshot ? scratchDirectory("runtime-screenshot") : null;
43301
+ const screenshotPath = screenshotDir ? join22(screenshotDir, "capture.png") : null;
42924
43302
  try {
42925
43303
  const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs, !expectsScreenshot);
42926
43304
  if (!reply.ok && reply.requestId !== undefined) {
@@ -43066,6 +43444,7 @@ var init_server3 = __esm(() => {
43066
43444
  init_mcp();
43067
43445
  init_stdio2();
43068
43446
  init_types();
43447
+ init_alive();
43069
43448
  init_boot_note();
43070
43449
  init_breakpoint_note();
43071
43450
  init_bridge_announce();
@@ -43101,7 +43480,7 @@ var init_server3 = __esm(() => {
43101
43480
  init_setup();
43102
43481
  init_tool_definitions();
43103
43482
  init_update_check();
43104
- run5 = promisify5(execFile5);
43483
+ run5 = promisify6(execFile6);
43105
43484
  __dirname2 = dirname15(fileURLToPath5(import.meta.url));
43106
43485
  CONSOLE_WRAPPER = /_console(\.exe)?$/i;
43107
43486
  LONGEST_BOOT_NOTED_MS = 5 * 60000;
@@ -43151,14 +43530,14 @@ var init_server3 = __esm(() => {
43151
43530
  init_bridge_announce();
43152
43531
  init_errors();
43153
43532
  init_godot_path();
43154
- import { existsSync as existsSync15 } from "node:fs";
43155
- import { join as join22, resolve as resolve6 } from "node:path";
43533
+ import { existsSync as existsSync16 } from "node:fs";
43534
+ import { join as join23, resolve as resolve6 } from "node:path";
43156
43535
 
43157
43536
  // src/harnesses.ts
43158
43537
  import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
43159
43538
  import { homedir as homedir2 } from "node:os";
43160
43539
  import { dirname as dirname3, join as join4 } from "node:path";
43161
- import process3 from "node:process";
43540
+ import process4 from "node:process";
43162
43541
 
43163
43542
  // node_modules/yaml/dist/index.js
43164
43543
  var composer = require_composer();
@@ -43726,12 +44105,12 @@ var HARNESSES = [
43726
44105
  }
43727
44106
  ];
43728
44107
  function appDataRoot() {
43729
- return process3.env["APPDATA"] ?? join4(homedir2(), "AppData", "Roaming");
44108
+ return process4.env["APPDATA"] ?? join4(homedir2(), "AppData", "Roaming");
43730
44109
  }
43731
44110
  function homePath(path) {
43732
44111
  return typeof path === "string" ? join4(homedir2(), path) : join4(appDataRoot(), path.appData);
43733
44112
  }
43734
- function fileFor(harness, platform = process3.platform) {
44113
+ function fileFor(harness, platform = process4.platform) {
43735
44114
  if (typeof harness.file === "string") {
43736
44115
  return harness.file;
43737
44116
  }
@@ -43744,7 +44123,7 @@ function configPath(harness, projectPath) {
43744
44123
  }
43745
44124
  return homePath(file);
43746
44125
  }
43747
- function displayPath(harness, platform = process3.platform) {
44126
+ function displayPath(harness, platform = process4.platform) {
43748
44127
  const file = fileFor(harness, platform);
43749
44128
  if (typeof file !== "string") {
43750
44129
  return `%APPDATA%/${file.appData.replaceAll("\\", "/")}`;
@@ -44022,10 +44401,10 @@ init_headless();
44022
44401
  init_issues();
44023
44402
 
44024
44403
  // src/prompt.ts
44025
- import process5 from "node:process";
44404
+ import process8 from "node:process";
44026
44405
  import { createInterface } from "node:readline/promises";
44027
44406
  function interactive() {
44028
- return process5.stdin.isTTY && process5.stdout.isTTY;
44407
+ return process8.stdin.isTTY && process8.stdout.isTTY;
44029
44408
  }
44030
44409
 
44031
44410
  class Ask {
@@ -44034,7 +44413,7 @@ class Ask {
44034
44413
  if (!interactive()) {
44035
44414
  return fallback;
44036
44415
  }
44037
- this.line ??= createInterface({ input: process5.stdin, output: process5.stdout });
44416
+ this.line ??= createInterface({ input: process8.stdin, output: process8.stdout });
44038
44417
  const answer = (await this.line.question(`${question} ${fallback ? "[Y/n]" : "[y/N]"} `)).trim();
44039
44418
  return answer === "" ? fallback : answer.toLowerCase().startsWith("y");
44040
44419
  }
@@ -44051,8 +44430,8 @@ init_setup();
44051
44430
  // src/skill.ts
44052
44431
  init_setup();
44053
44432
  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";
44433
+ import { existsSync as existsSync9, mkdirSync as mkdirSync5, readdirSync as readdirSync5, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "node:fs";
44434
+ import { basename as basename2, dirname as dirname5, join as join10 } from "node:path";
44056
44435
 
44057
44436
  // src/tool-reference.ts
44058
44437
  init_headless_operations();
@@ -44148,7 +44527,7 @@ function renderToolsMarkdown() {
44148
44527
 
44149
44528
  // src/skill.ts
44150
44529
  var SKILL_NAME = "gdharness";
44151
- var SHARED_SKILLS = join9(".agents", "skills");
44530
+ var SHARED_SKILLS = join10(".agents", "skills");
44152
44531
  function skillMarkdown(version) {
44153
44532
  return `---
44154
44533
  name: ${SKILL_NAME}
@@ -44226,6 +44605,8 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
44226
44605
  | 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
44606
  | 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
44607
  | Where a 3D node is on screen | \`runtime_inspect\` \`rect\`, rather than unprojecting by hand |
44608
+ | 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 |
44609
+ | 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
44610
  | 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
44611
  | 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
44612
  | 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 +44718,27 @@ whatever the engine said on stderr comes back under \`engine_messages\`.
44337
44718
  function skillFiles(version) {
44338
44719
  return new Map([
44339
44720
  ["SKILL.md", skillMarkdown(version)],
44340
- [join9("references", "tools.md"), renderToolsMarkdown()]
44721
+ [join10("references", "tools.md"), renderToolsMarkdown()]
44341
44722
  ]);
44342
44723
  }
44343
44724
  function skillDirectories(harnesses, projectPath, exists) {
44344
- const directories = [join9(projectPath, SHARED_SKILLS, SKILL_NAME)];
44725
+ const directories = [join10(projectPath, SHARED_SKILLS, SKILL_NAME)];
44345
44726
  for (const harness of harnesses) {
44346
44727
  if (harness.skills === undefined) {
44347
44728
  continue;
44348
44729
  }
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));
44730
+ const own = join10(projectPath, harness.skills.dir);
44731
+ if ((!harness.skills.shared || exists(own)) && !directories.includes(join10(own, SKILL_NAME))) {
44732
+ directories.push(join10(own, SKILL_NAME));
44352
44733
  }
44353
44734
  }
44354
44735
  return directories;
44355
44736
  }
44356
44737
  function everySkillDirectory(projectPath, harnesses) {
44357
- const directories = new Set([join9(projectPath, SHARED_SKILLS, SKILL_NAME)]);
44738
+ const directories = new Set([join10(projectPath, SHARED_SKILLS, SKILL_NAME)]);
44358
44739
  for (const harness of harnesses) {
44359
44740
  if (harness.skills !== undefined) {
44360
- directories.add(join9(projectPath, harness.skills.dir, SKILL_NAME));
44741
+ directories.add(join10(projectPath, harness.skills.dir, SKILL_NAME));
44361
44742
  }
44362
44743
  }
44363
44744
  return [...directories];
@@ -44365,7 +44746,7 @@ function everySkillDirectory(projectPath, harnesses) {
44365
44746
  function removeSkill(directories, projectPath) {
44366
44747
  const removed = [];
44367
44748
  for (const directory of directories) {
44368
- if (existsSync8(directory)) {
44749
+ if (existsSync9(directory)) {
44369
44750
  rmSync5(directory, { recursive: true, force: true });
44370
44751
  pruneEmpty(dirname5(directory), projectPath);
44371
44752
  removed.push(directory);
@@ -44374,10 +44755,10 @@ function removeSkill(directories, projectPath) {
44374
44755
  return removed;
44375
44756
  }
44376
44757
  function pruneEmpty(directory, projectPath) {
44377
- const shared = join9(projectPath, ".agents");
44758
+ const shared = join10(projectPath, ".agents");
44378
44759
  let path = directory;
44379
44760
  while (path !== projectPath && (basename2(path) === "skills" || path === shared)) {
44380
- if (readdirSync4(path).length > 0) {
44761
+ if (readdirSync5(path).length > 0) {
44381
44762
  return;
44382
44763
  }
44383
44764
  rmSync5(path, { recursive: true, force: true });
@@ -44387,10 +44768,10 @@ function pruneEmpty(directory, projectPath) {
44387
44768
  function writeSkill(directories, version) {
44388
44769
  const files = skillFiles(version);
44389
44770
  return directories.map((directory) => {
44390
- const replaced = existsSync8(directory);
44771
+ const replaced = existsSync9(directory);
44391
44772
  rmSync5(directory, { recursive: true, force: true });
44392
44773
  for (const [name, contents] of files) {
44393
- const path = join9(directory, name);
44774
+ const path = join10(directory, name);
44394
44775
  mkdirSync5(dirname5(path), { recursive: true });
44395
44776
  writeFileSync6(path, contents, "utf8");
44396
44777
  }
@@ -44400,13 +44781,13 @@ function writeSkill(directories, version) {
44400
44781
 
44401
44782
  // src/version.ts
44402
44783
  import { readFileSync as readFileSync8 } from "node:fs";
44403
- import { dirname as dirname6, join as join10 } from "node:path";
44784
+ import { dirname as dirname6, join as join11 } from "node:path";
44404
44785
  import { fileURLToPath as fileURLToPath2 } from "node:url";
44405
44786
  function getLocalVersion() {
44406
44787
  const currentDirectory = dirname6(fileURLToPath2(import.meta.url));
44407
44788
  const packageCandidates = [
44408
- join10(currentDirectory, "..", "package.json"),
44409
- join10(currentDirectory, "..", "..", "package.json")
44789
+ join11(currentDirectory, "..", "package.json"),
44790
+ join11(currentDirectory, "..", "..", "package.json")
44410
44791
  ];
44411
44792
  for (const packagePath of packageCandidates) {
44412
44793
  try {
@@ -44428,7 +44809,7 @@ class UsageError extends Refusal {
44428
44809
  function projectArgument(at) {
44429
44810
  const named = args.slice(at).find((value) => !value.startsWith("--"));
44430
44811
  const projectPath = resolve6(named ?? ".");
44431
- if (!existsSync15(join22(projectPath, "project.godot"))) {
44812
+ if (!existsSync16(join23(projectPath, "project.godot"))) {
44432
44813
  throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
44433
44814
  }
44434
44815
  return projectPath;
@@ -44467,7 +44848,7 @@ async function registerRuntime(godot, projectPath) {
44467
44848
  return;
44468
44849
  }
44469
44850
  console.log(`${RUNTIME_AUTOLOAD.name} autoload left at ${named}, which this project registers itself`);
44470
- if (!existsSync15(join22(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
44851
+ if (!existsSync16(join23(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
44471
44852
  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
44853
  }
44473
44854
  }
@@ -44599,7 +44980,7 @@ async function setup() {
44599
44980
  console.log("no harness configured; name one yourself, and gdharness harnesses lists them all");
44600
44981
  }
44601
44982
  if (!args.includes("--no-skill")) {
44602
- const directories = skillDirectories(chosen.harnesses, projectPath, existsSync15);
44983
+ const directories = skillDirectories(chosen.harnesses, projectPath, existsSync16);
44603
44984
  for (const written of writeSkill(directories, getLocalVersion())) {
44604
44985
  console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
44605
44986
  }
@@ -44693,7 +45074,7 @@ async function upgrade() {
44693
45074
  moved.push(took.from);
44694
45075
  }
44695
45076
  }
44696
- for (const written of writeSkill(skillDirectories(already, projectPath, existsSync15), version)) {
45077
+ for (const written of writeSkill(skillDirectories(already, projectPath, existsSync16), version)) {
44697
45078
  console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
44698
45079
  }
44699
45080
  console.log(`
@@ -44702,7 +45083,7 @@ On ${version}. What is left for you:`);
44702
45083
  ` + ` so it gives an open editor nothing to restart for. editor_status says addonIsStale
44703
45084
  ` + " 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
45085
  ` + " editor_launch restart tool, which closes and reopens the window.");
44705
- const serving = serverServing(projectPath);
45086
+ const serving = await serverServing(projectPath);
44706
45087
  console.log(harnessNote({ moved, written, byHand }, [launch.command, ...launch.args].join(" "), serving === null ? null : { server: serving, isThisVersion: serving.version === version }));
44707
45088
  }
44708
45089
  function doctorReport(projectPath) {