gdharness 1.1.47 → 1.1.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.js CHANGED
@@ -10249,17 +10249,17 @@ import {
10249
10249
  closeSync as closeSync2,
10250
10250
  constants,
10251
10251
  copyFileSync,
10252
- existsSync as existsSync16,
10252
+ existsSync as existsSync17,
10253
10253
  fstatSync,
10254
- mkdirSync as mkdirSync12,
10254
+ mkdirSync as mkdirSync13,
10255
10255
  openSync as openSync3,
10256
10256
  readdirSync as readdirSync9,
10257
- readFileSync as readFileSync19,
10257
+ readFileSync as readFileSync20,
10258
10258
  readSync as readSync2,
10259
10259
  rmdirSync as rmdirSync2,
10260
10260
  statSync as statSync8
10261
10261
  } from "node:fs";
10262
- import { basename as basename5, dirname as dirname14, extname as extname2, isAbsolute as isAbsolute3, join as join21, normalize as normalize2, relative as relative4 } from "node:path";
10262
+ import { basename as basename5, dirname as dirname15, extname as extname2, isAbsolute as isAbsolute3, join as join22, normalize as normalize2, relative as relative4 } from "node:path";
10263
10263
  import { setTimeout as delay3 } from "node:timers/promises";
10264
10264
  import { fileURLToPath as fileURLToPath4 } from "node:url";
10265
10265
  import { promisify as promisify5 } from "node:util";
@@ -26178,11 +26178,14 @@ function withdrawBridge(path) {
26178
26178
  // src/call-signal.ts
26179
26179
  import { AsyncLocalStorage } from "node:async_hooks";
26180
26180
  var current = new AsyncLocalStorage;
26181
- function withCallSignal(signal, answer) {
26182
- return signal === undefined ? answer() : current.run(signal, answer);
26181
+ function withCallSignal(signal, answer, progress) {
26182
+ return signal === undefined && progress === undefined ? answer() : current.run({ signal, progress }, answer);
26183
26183
  }
26184
26184
  function callSignal() {
26185
- return current.getStore();
26185
+ return current.getStore()?.signal;
26186
+ }
26187
+ function callProgress() {
26188
+ return current.getStore()?.progress;
26186
26189
  }
26187
26190
 
26188
26191
  // src/class-cache.ts
@@ -26945,6 +26948,41 @@ function configDisagrees(projectPath, serverVersion) {
26945
26948
  return `.mcp.json in this project names gdharness@${others.join(" and gdharness@")}, and this server ` + `is ${serverVersion}. Whichever is meant to be right, nothing will change version until the ` + "MCP server is reconnected, and a pin kept anywhere else does not reach this file.";
26946
26949
  }
26947
26950
 
26951
+ // src/cut-test-run.ts
26952
+ import { existsSync as existsSync5, mkdirSync as mkdirSync6, readFileSync as readFileSync10, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "node:fs";
26953
+ import { dirname as dirname6, join as join9 } from "node:path";
26954
+ function keptAt(projectPath) {
26955
+ return join9(projectPath, ".godot", "gdharness-reports", "cut-run.json");
26956
+ }
26957
+ function keepCutTestRun(projectPath, run) {
26958
+ const path = keptAt(projectPath);
26959
+ try {
26960
+ mkdirSync6(dirname6(path), { recursive: true });
26961
+ writeFileSync6(path, `${JSON.stringify(run, null, 2)}
26962
+ `, "utf8");
26963
+ } catch {}
26964
+ }
26965
+ function takeCutTestRun(projectPath) {
26966
+ const path = keptAt(projectPath);
26967
+ if (!existsSync5(path)) {
26968
+ return null;
26969
+ }
26970
+ try {
26971
+ const run = JSON.parse(readFileSync10(path, "utf8"));
26972
+ rmSync3(path, { force: true });
26973
+ return typeof run.cutAt === "string" && Array.isArray(run.paths) && run.paths.every((path) => typeof path === "string") ? run : null;
26974
+ } catch {
26975
+ rmSync3(path, { force: true });
26976
+ return null;
26977
+ }
26978
+ }
26979
+ function cutTestRunNote(run) {
26980
+ const finished = run.suitesFinished?.length ?? 0;
26981
+ const failing = run.suitesFailing?.length ?? 0;
26982
+ const got = finished === 0 ? "before any suite had finished" : `after ${finished === 1 ? "one suite" : `${finished} suites`} had finished${failing === 0 ? "" : finished === 1 ? ", with failures" : `, ${failing} of them with failures`}`;
26983
+ return `The test run before this one in this project, of ${run.paths.join(", ")}, was cut off by its client at ${run.cutAt} ${got}${run.suiteRunning === undefined ? "" : `, while ${run.suiteRunning} was running`}; what it finished is under previousRunCut.`;
26984
+ }
26985
+
26948
26986
  // src/dap_client.ts
26949
26987
  import { createConnection as createConnection2 } from "node:net";
26950
26988
  import { setTimeout as delay2 } from "node:timers/promises";
@@ -27737,16 +27775,16 @@ function dictionary(entries) {
27737
27775
  // src/editor-log.ts
27738
27776
  import {
27739
27777
  closeSync,
27740
- existsSync as existsSync5,
27741
- mkdirSync as mkdirSync6,
27778
+ existsSync as existsSync6,
27779
+ mkdirSync as mkdirSync7,
27742
27780
  openSync,
27743
- readFileSync as readFileSync10,
27781
+ readFileSync as readFileSync11,
27744
27782
  readSync,
27745
- rmSync as rmSync3,
27783
+ rmSync as rmSync4,
27746
27784
  statSync as statSync3,
27747
- writeFileSync as writeFileSync6
27785
+ writeFileSync as writeFileSync7
27748
27786
  } from "node:fs";
27749
- import { dirname as dirname6, join as join9 } from "node:path";
27787
+ import { dirname as dirname7, join as join10 } from "node:path";
27750
27788
 
27751
27789
  // src/game-log.ts
27752
27790
  import { StringDecoder } from "node:string_decoder";
@@ -27864,14 +27902,14 @@ function answersTo(entry, severity, contains) {
27864
27902
 
27865
27903
  // src/editor-log.ts
27866
27904
  function editorLogPath(projectPath) {
27867
- return join9(projectPath, ".godot", "gdharness-editor.log");
27905
+ return join10(projectPath, ".godot", "gdharness-editor.log");
27868
27906
  }
27869
27907
  function editorLogNotePath(projectPath) {
27870
- return join9(projectPath, ".godot", "gdharness-editor.json");
27908
+ return join10(projectPath, ".godot", "gdharness-editor.json");
27871
27909
  }
27872
27910
  function readEditorLogNote(projectPath) {
27873
27911
  try {
27874
- const fields = asParams(JSON.parse(readFileSync10(editorLogNotePath(projectPath), "utf8")));
27912
+ const fields = asParams(JSON.parse(readFileSync11(editorLogNotePath(projectPath), "utf8")));
27875
27913
  const pid = readNumber(fields, "pid");
27876
27914
  const startedAt = readString(fields, "startedAt");
27877
27915
  return pid !== undefined && Number.isInteger(pid) && pid > 0 && startedAt !== undefined ? { pid, startedAt } : null;
@@ -27882,16 +27920,16 @@ function readEditorLogNote(projectPath) {
27882
27920
  function clearEditorLog(projectPath) {
27883
27921
  try {
27884
27922
  const path = editorLogPath(projectPath);
27885
- mkdirSync6(dirname6(path), { recursive: true });
27886
- rmSync3(path, { force: true });
27887
- rmSync3(editorLogNotePath(projectPath), { force: true });
27923
+ mkdirSync7(dirname7(path), { recursive: true });
27924
+ rmSync4(path, { force: true });
27925
+ rmSync4(editorLogNotePath(projectPath), { force: true });
27888
27926
  } catch {}
27889
27927
  }
27890
27928
  function writeEditorLogNote(projectPath, pid) {
27891
27929
  try {
27892
27930
  const path = editorLogNotePath(projectPath);
27893
- mkdirSync6(dirname6(path), { recursive: true });
27894
- writeFileSync6(path, JSON.stringify({ pid, startedAt: new Date().toISOString() }, null, 2), "utf8");
27931
+ mkdirSync7(dirname7(path), { recursive: true });
27932
+ writeFileSync7(path, JSON.stringify({ pid, startedAt: new Date().toISOString() }, null, 2), "utf8");
27895
27933
  } catch {}
27896
27934
  }
27897
27935
  function theConsoleWasNotCaptured(why, projectPath) {
@@ -27912,7 +27950,7 @@ function editorConsole(projectPath, editorPid, openedByAServer) {
27912
27950
  return { kind: "older server", notedPid: noted?.pid ?? null };
27913
27951
  }
27914
27952
  const path = editorLogPath(projectPath);
27915
- if (!existsSync5(path)) {
27953
+ if (!existsSync6(path)) {
27916
27954
  return { kind: "not written" };
27917
27955
  }
27918
27956
  const log = new GameLog;
@@ -27925,7 +27963,7 @@ var MOST_CONSOLE_BYTES = 8 * 1024 * 1024;
27925
27963
  function theFirstOf(path) {
27926
27964
  const whole = statSync3(path).size;
27927
27965
  if (whole <= MOST_CONSOLE_BYTES) {
27928
- return { text: readFileSync10(path, "utf8") };
27966
+ return { text: readFileSync11(path, "utf8") };
27929
27967
  }
27930
27968
  const handle = openSync(path, "r");
27931
27969
  try {
@@ -28693,14 +28731,14 @@ function getDefaultBridge(ownProject = null) {
28693
28731
 
28694
28732
  // src/godot-path.ts
28695
28733
  import { execFile as execFile2 } from "node:child_process";
28696
- import { existsSync as existsSync7 } from "node:fs";
28734
+ import { existsSync as existsSync8 } from "node:fs";
28697
28735
  import { normalize } from "node:path";
28698
28736
  import { promisify as promisify2 } from "node:util";
28699
28737
 
28700
28738
  // src/detection.ts
28701
- import { existsSync as existsSync6, readdirSync as readdirSync3, statSync as statSync4 } from "node:fs";
28739
+ import { existsSync as existsSync7, readdirSync as readdirSync3, statSync as statSync4 } from "node:fs";
28702
28740
  import { homedir as homedir2 } from "node:os";
28703
- import { join as join10 } from "node:path";
28741
+ import { join as join11 } from "node:path";
28704
28742
  function resolveHomeDirectory() {
28705
28743
  try {
28706
28744
  return homedir2();
@@ -28709,7 +28747,7 @@ function resolveHomeDirectory() {
28709
28747
  }
28710
28748
  }
28711
28749
  function scanDirectoryForGodotBinaries(directory, platform) {
28712
- if (!directory || !existsSync6(directory)) {
28750
+ if (!directory || !existsSync7(directory)) {
28713
28751
  return [];
28714
28752
  }
28715
28753
  let entries;
@@ -28724,7 +28762,7 @@ function scanDirectoryForGodotBinaries(directory, platform) {
28724
28762
  if (!pattern.test(name)) {
28725
28763
  continue;
28726
28764
  }
28727
- const fullPath = join10(directory, name);
28765
+ const fullPath = join11(directory, name);
28728
28766
  try {
28729
28767
  const stat = statSync4(fullPath);
28730
28768
  if (stat.isFile()) {
@@ -28733,7 +28771,7 @@ function scanDirectoryForGodotBinaries(directory, platform) {
28733
28771
  } catch {}
28734
28772
  }
28735
28773
  matches.sort((a, b) => b.mtime - a.mtime);
28736
- return matches.map((m) => join10(directory, m.name));
28774
+ return matches.map((m) => join11(directory, m.name));
28737
28775
  }
28738
28776
  function conventionalPaths(platform, home) {
28739
28777
  const paths = ["godot"];
@@ -28836,7 +28874,7 @@ class GodotLocator {
28836
28874
  return known;
28837
28875
  }
28838
28876
  let reason;
28839
- if (path !== "godot" && !existsSync7(path)) {
28877
+ if (path !== "godot" && !existsSync8(path)) {
28840
28878
  reason = "does not exist";
28841
28879
  } else {
28842
28880
  try {
@@ -28852,33 +28890,33 @@ class GodotLocator {
28852
28890
  }
28853
28891
 
28854
28892
  // src/headless.ts
28855
- import { existsSync as existsSync10, readFileSync as readFileSync12, realpathSync as realpathSync2, writeFileSync as writeFileSync8 } from "node:fs";
28856
- import { dirname as dirname7, join as join13 } from "node:path";
28893
+ import { existsSync as existsSync11, readFileSync as readFileSync13, realpathSync as realpathSync2, writeFileSync as writeFileSync9 } from "node:fs";
28894
+ import { dirname as dirname8, join as join14 } from "node:path";
28857
28895
 
28858
28896
  // src/reimport.ts
28859
28897
  import { createHash as createHash2 } from "node:crypto";
28860
- import { existsSync as existsSync8, readFileSync as readFileSync11, rmSync as rmSync4, statSync as statSync5, utimesSync, writeFileSync as writeFileSync7 } from "node:fs";
28861
- import { basename as basename2, join as join11 } from "node:path";
28898
+ import { existsSync as existsSync9, readFileSync as readFileSync12, rmSync as rmSync5, statSync as statSync5, utimesSync, writeFileSync as writeFileSync8 } from "node:fs";
28899
+ import { basename as basename2, join as join12 } from "node:path";
28862
28900
  function forceNextImport(projectPath, resourcePath) {
28863
28901
  const relative = resourcePath.replace(/^res:\/\//, "");
28864
- const sidecar = join11(projectPath, `${relative}.import`);
28865
- if (!existsSync8(sidecar)) {
28902
+ const sidecar = join12(projectPath, `${relative}.import`);
28903
+ if (!existsSync9(sidecar)) {
28866
28904
  return;
28867
28905
  }
28868
28906
  const { atime, mtimeMs } = statSync5(sidecar);
28869
- writeFileSync7(sidecar, readFileSync11(sidecar, "utf8").replace(/^valid=false\r?\n/m, ""));
28907
+ writeFileSync8(sidecar, readFileSync12(sidecar, "utf8").replace(/^valid=false\r?\n/m, ""));
28870
28908
  utimesSync(sidecar, atime, new Date(mtimeMs + 2000));
28871
- rmSync4(importRecord(projectPath, resourcePath), { force: true });
28909
+ rmSync5(importRecord(projectPath, resourcePath), { force: true });
28872
28910
  }
28873
28911
  function importRecord(projectPath, resourcePath) {
28874
28912
  const digest = createHash2("md5").update(resourcePath).digest("hex");
28875
- return join11(projectPath, ".godot", "imported", `${basename2(resourcePath)}-${digest}.md5`);
28913
+ return join12(projectPath, ".godot", "imported", `${basename2(resourcePath)}-${digest}.md5`);
28876
28914
  }
28877
28915
 
28878
28916
  // src/scratch.ts
28879
- import { existsSync as existsSync9, mkdtempSync, readdirSync as readdirSync4, rmSync as rmSync5 } from "node:fs";
28917
+ import { existsSync as existsSync10, mkdtempSync, readdirSync as readdirSync4, rmSync as rmSync6 } from "node:fs";
28880
28918
  import { tmpdir as tmpdir3 } from "node:os";
28881
- import { join as join12 } from "node:path";
28919
+ import { join as join13 } from "node:path";
28882
28920
  import process7 from "node:process";
28883
28921
  var SWEPT = { recursive: true, force: true };
28884
28922
  var AGAIN_AFTER_MS = [250, 500, 1000, 2000, 4000, 8000, 16000];
@@ -28893,7 +28931,7 @@ var KINDS = [
28893
28931
  "tests"
28894
28932
  ];
28895
28933
  function scratchDirectory(kind) {
28896
- return mkdtempSync(join12(tmpdir3(), `gdharness-${kind}-${process7.pid}-`));
28934
+ return mkdtempSync(join13(tmpdir3(), `gdharness-${kind}-${process7.pid}-`));
28897
28935
  }
28898
28936
  function sweepAbandonedScratch(directory = tmpdir3()) {
28899
28937
  let entries;
@@ -28909,7 +28947,7 @@ function sweepAbandonedScratch(directory = tmpdir3()) {
28909
28947
  continue;
28910
28948
  }
28911
28949
  try {
28912
- remove(join12(directory, entry));
28950
+ remove(join13(directory, entry));
28913
28951
  } catch {}
28914
28952
  }
28915
28953
  }
@@ -28935,7 +28973,7 @@ function discard(...paths) {
28935
28973
  return left;
28936
28974
  }
28937
28975
  function remove(path) {
28938
- rmSync5(path, SWEPT);
28976
+ rmSync6(path, SWEPT);
28939
28977
  }
28940
28978
  function tryAgain(path, attempt) {
28941
28979
  if (attempt === 0) {
@@ -28947,7 +28985,7 @@ function tryAgain(path, attempt) {
28947
28985
  lastTryOnExit();
28948
28986
  }
28949
28987
  const after = AGAIN_AFTER_MS[attempt];
28950
- if (after === undefined || !existsSync9(path)) {
28988
+ if (after === undefined || !existsSync10(path)) {
28951
28989
  waiting.delete(path);
28952
28990
  if (after === undefined) {
28953
28991
  atExit.add(path);
@@ -29027,7 +29065,7 @@ function reason(log, problems) {
29027
29065
  }
29028
29066
  function scriptFault(problems, script) {
29029
29067
  const spelled = (path) => `(${path.replaceAll("\\", "/").toLowerCase()}/`;
29030
- const directories = [...new Set([spelled(dirname7(script)), spelled(resolvedOr(dirname7(script)))])];
29068
+ const directories = [...new Set([spelled(dirname8(script)), spelled(resolvedOr(dirname8(script)))])];
29031
29069
  return problems.find((entry) => {
29032
29070
  const at = entry.detail.find((line) => line.startsWith("at: "))?.replaceAll("\\", "/").toLowerCase();
29033
29071
  return entry.severity === "error" && at !== undefined && directories.some((directory) => at.includes(directory));
@@ -29083,7 +29121,7 @@ async function importOnce(godotPath, projectPath) {
29083
29121
  ran = await runEngine(godotPath, [
29084
29122
  "--headless",
29085
29123
  "--log-file",
29086
- join13(logDir, "engine.log"),
29124
+ join14(logDir, "engine.log"),
29087
29125
  "--path",
29088
29126
  projectPath,
29089
29127
  "--import"
@@ -29103,13 +29141,13 @@ async function importOnce(godotPath, projectPath) {
29103
29141
  }
29104
29142
  async function runOperation(engine, operation, params, projectPath) {
29105
29143
  const paramsDir = scratchDirectory("params");
29106
- const paramsFile = join13(paramsDir, `${operation}.json`);
29107
- const answerFile = join13(paramsDir, "answer.json");
29108
- writeFileSync8(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
29144
+ const paramsFile = join14(paramsDir, `${operation}.json`);
29145
+ const answerFile = join14(paramsDir, "answer.json");
29146
+ writeFileSync9(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
29109
29147
  const args = [
29110
29148
  "--headless",
29111
29149
  "--log-file",
29112
- join13(paramsDir, "engine.log"),
29150
+ join14(paramsDir, "engine.log"),
29113
29151
  "--path",
29114
29152
  projectPath,
29115
29153
  "--script",
@@ -29160,10 +29198,10 @@ async function runOperation(engine, operation, params, projectPath) {
29160
29198
  };
29161
29199
  }
29162
29200
  function answerIn(file) {
29163
- if (!existsSync10(file)) {
29201
+ if (!existsSync11(file)) {
29164
29202
  return { kind: "none" };
29165
29203
  }
29166
- const text = readFileSync12(file, "utf8");
29204
+ const text = readFileSync13(file, "utf8");
29167
29205
  try {
29168
29206
  const parsed = JSON.parse(text);
29169
29207
  return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? { kind: "answer", payload: parsed } : { kind: "unreadable", why: `it is JSON but not an object: ${text.slice(0, 200)}` };
@@ -29579,6 +29617,52 @@ function orphansPrinted(printed) {
29579
29617
  suites
29580
29618
  };
29581
29619
  }
29620
+ var SUITE_COUNTS = /(\d+)\s+test cases\s*\|\s*(\d+)\s+errors\s*\|\s*(\d+)\s+failures/;
29621
+ var CASE_FAILED = /^\s*res:\/\/.+?\.(?:gd|cs) > (.+?) (?:FAILED|ERROR)(?:\s|$)/;
29622
+ function finishesASuite(line) {
29623
+ return SUITE_COUNTS.test(line) && !line.includes(OVERALL);
29624
+ }
29625
+ function suitesPrinted(printed) {
29626
+ const finished = [];
29627
+ let running = null;
29628
+ let failed = [];
29629
+ for (const line of printed) {
29630
+ const started = RUNNING_SUITE.exec(line);
29631
+ if (started?.[1] !== undefined) {
29632
+ running = started[1];
29633
+ failed = [];
29634
+ continue;
29635
+ }
29636
+ if (running === null) {
29637
+ continue;
29638
+ }
29639
+ const failing = CASE_FAILED.exec(line)?.[1];
29640
+ if (failing !== undefined) {
29641
+ failed.push(failing);
29642
+ continue;
29643
+ }
29644
+ const counted = finishesASuite(line) ? SUITE_COUNTS.exec(line) : null;
29645
+ if (counted !== null) {
29646
+ finished.push({
29647
+ path: running,
29648
+ cases: Number(counted[1]),
29649
+ errors: Number(counted[2]),
29650
+ failures: Number(counted[3]),
29651
+ failed
29652
+ });
29653
+ running = null;
29654
+ }
29655
+ }
29656
+ return { finished, running };
29657
+ }
29658
+ function testProgressMessage(printed, elapsedMs) {
29659
+ const { finished, running } = suitesPrinted(printed);
29660
+ const failing = finished.filter((suite) => suite.errors + suite.failures > 0).length;
29661
+ const seconds = Math.floor(elapsedMs / 1000);
29662
+ const took = seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
29663
+ const done = finished.length === 0 ? "No suite has finished" : `${finished.length === 1 ? "One suite has" : `${finished.length} suites have`} finished${failing === 0 ? "" : `, ${failing} with failures`}`;
29664
+ return `${done}${running === null ? "" : `; running ${running}`}; ${took} in.`;
29665
+ }
29582
29666
 
29583
29667
  // src/keeper-channel.ts
29584
29668
  import { connect, createServer as createServer2 } from "node:net";
@@ -29622,10 +29706,10 @@ function askTheKeeperToStop(address, withinMs) {
29622
29706
  }
29623
29707
 
29624
29708
  // src/lsp_client.ts
29625
- import { existsSync as existsSync11, realpathSync as realpathSync3 } from "node:fs";
29709
+ import { existsSync as existsSync12, realpathSync as realpathSync3 } from "node:fs";
29626
29710
  import { readFile, realpath } from "node:fs/promises";
29627
29711
  import { createConnection as createConnection3 } from "node:net";
29628
- import { dirname as dirname8, join as join14, resolve as resolve3 } from "node:path";
29712
+ import { dirname as dirname9, join as join15, resolve as resolve3 } from "node:path";
29629
29713
  import { fileURLToPath, pathToFileURL } from "node:url";
29630
29714
  var DEFAULT_LSP_PORT = 6005;
29631
29715
  var DIAGNOSTICS_TIMEOUT_MS = 5000;
@@ -29782,7 +29866,7 @@ class GodotLSPClient {
29782
29866
  if (this.initialized) {
29783
29867
  return;
29784
29868
  }
29785
- const rootPath = this.rootPath ?? dirname8(resolve3(filePath));
29869
+ const rootPath = this.rootPath ?? dirname9(resolve3(filePath));
29786
29870
  await this.initialize(rootPath);
29787
29871
  }
29788
29872
  async sendRequest(method, params, timeoutMs = this.timeouts.requestMs) {
@@ -30193,7 +30277,7 @@ async function resolveLSPPaths(projectPathValue, scriptPathValue) {
30193
30277
  } catch {
30194
30278
  throw new ArgumentRefusal(`Project path does not exist: ${requestedProjectPath}`);
30195
30279
  }
30196
- if (!existsSync11(join14(projectPath, "project.godot"))) {
30280
+ if (!existsSync12(join15(projectPath, "project.godot"))) {
30197
30281
  throw new ArgumentRefusal(`Not a Godot project: ${projectPath}. Point projectPath at the directory holding project.godot.`);
30198
30282
  }
30199
30283
  const contained = resolveWithinProject(projectPath, scriptPathValue);
@@ -30370,14 +30454,14 @@ function secondsFromClock(printed) {
30370
30454
  }
30371
30455
 
30372
30456
  // src/project-scan.ts
30373
- import { existsSync as existsSync12, readdirSync as readdirSync5, readFileSync as readFileSync13 } from "node:fs";
30374
- import { join as join15 } from "node:path";
30457
+ import { existsSync as existsSync13, readdirSync as readdirSync5, readFileSync as readFileSync14 } from "node:fs";
30458
+ import { join as join16 } from "node:path";
30375
30459
  var SKIPPED = new Set(["node_modules"]);
30376
30460
  function skipped(name) {
30377
30461
  return name.startsWith(".") || SKIPPED.has(name);
30378
30462
  }
30379
30463
  function steppedOver(directory) {
30380
- return existsSync12(join15(directory, ".gdignore"));
30464
+ return existsSync13(join16(directory, ".gdignore"));
30381
30465
  }
30382
30466
  var SCENE_EXTENSIONS = new Set(["tscn", "scn"]);
30383
30467
  var ASSET_EXTENSIONS = new Set([
@@ -30421,7 +30505,7 @@ function projectStructure(projectPath) {
30421
30505
  continue;
30422
30506
  }
30423
30507
  if (entry.isDirectory()) {
30424
- visit(join15(directory, entry.name));
30508
+ visit(join16(directory, entry.name));
30425
30509
  } else if (entry.isFile()) {
30426
30510
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
30427
30511
  if (SCENE_EXTENSIONS.has(extension)) {
@@ -30452,14 +30536,14 @@ function scriptsWithoutUid(projectPath) {
30452
30536
  }
30453
30537
  const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
30454
30538
  if (entry.isDirectory()) {
30455
- visit(join15(directory, entry.name), spelled);
30539
+ visit(join16(directory, entry.name), spelled);
30456
30540
  continue;
30457
30541
  }
30458
30542
  if (!entry.isFile()) {
30459
30543
  continue;
30460
30544
  }
30461
30545
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
30462
- if (CARRIES_A_UID.has(extension) && !existsSync12(join15(directory, `${entry.name}.uid`))) {
30546
+ if (CARRIES_A_UID.has(extension) && !existsSync13(join16(directory, `${entry.name}.uid`))) {
30463
30547
  found.push(spelled);
30464
30548
  }
30465
30549
  }
@@ -30491,7 +30575,7 @@ function projectTexts(projectPath) {
30491
30575
  if (skipped(entry.name)) {
30492
30576
  continue;
30493
30577
  }
30494
- const absolute = join15(directory, entry.name);
30578
+ const absolute = join16(directory, entry.name);
30495
30579
  const spelled = `${prefix}${entry.name}`;
30496
30580
  if (entry.isDirectory()) {
30497
30581
  visit(absolute, `${spelled}/`);
@@ -30501,7 +30585,7 @@ function projectTexts(projectPath) {
30501
30585
  if (!entry.isFile() || BINARY_EXTENSIONS.has(extension)) {
30502
30586
  continue;
30503
30587
  }
30504
- const bytes = readFileSync13(absolute);
30588
+ const bytes = readFileSync14(absolute);
30505
30589
  if (!isBinary(bytes)) {
30506
30590
  found.push({ path: `res://${spelled}`, absolute, text: bytes.toString("utf8") });
30507
30591
  }
@@ -30518,7 +30602,7 @@ function ignoredTexts(projectPath) {
30518
30602
  if (skipped(entry.name)) {
30519
30603
  continue;
30520
30604
  }
30521
- const absolute = join15(directory, entry.name);
30605
+ const absolute = join16(directory, entry.name);
30522
30606
  const spelled = `${prefix}${entry.name}`;
30523
30607
  if (entry.isDirectory()) {
30524
30608
  visit(absolute, `${spelled}/`, under);
@@ -30528,7 +30612,7 @@ function ignoredTexts(projectPath) {
30528
30612
  if (!under || !entry.isFile() || BINARY_EXTENSIONS.has(extension) || entry.name === ".gdignore") {
30529
30613
  continue;
30530
30614
  }
30531
- const bytes = readFileSync13(absolute);
30615
+ const bytes = readFileSync14(absolute);
30532
30616
  if (!isBinary(bytes)) {
30533
30617
  found.push({ path: `res://${spelled}`, absolute, text: bytes.toString("utf8") });
30534
30618
  }
@@ -30558,7 +30642,7 @@ function searchProject(projectPath, options) {
30558
30642
  if (skipped(entry.name)) {
30559
30643
  continue;
30560
30644
  }
30561
- const entryPath = join15(directory, entry.name);
30645
+ const entryPath = join16(directory, entry.name);
30562
30646
  if (entry.isDirectory()) {
30563
30647
  visit(entryPath);
30564
30648
  continue;
@@ -30567,7 +30651,7 @@ function searchProject(projectPath, options) {
30567
30651
  if (!entry.isFile() || extensions !== null && !extensions.has(extension)) {
30568
30652
  continue;
30569
30653
  }
30570
- const bytes = readFileSync13(entryPath);
30654
+ const bytes = readFileSync14(entryPath);
30571
30655
  if (extensions === null && isBinary(bytes)) {
30572
30656
  continue;
30573
30657
  }
@@ -30597,8 +30681,8 @@ function searchProject(projectPath, options) {
30597
30681
  }
30598
30682
 
30599
30683
  // src/rename.ts
30600
- import { existsSync as existsSync13, mkdirSync as mkdirSync7, readFileSync as readFileSync14, renameSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync9 } from "node:fs";
30601
- import { dirname as dirname9, posix } from "node:path";
30684
+ import { existsSync as existsSync14, mkdirSync as mkdirSync8, readFileSync as readFileSync15, renameSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync10 } from "node:fs";
30685
+ import { dirname as dirname10, posix } from "node:path";
30602
30686
  class Script {
30603
30687
  path;
30604
30688
  text;
@@ -31320,25 +31404,25 @@ function rewritesOf(plan, texts, move) {
31320
31404
  }
31321
31405
  function applyRewrites(rewrites) {
31322
31406
  for (const rewrite of rewrites) {
31323
- const now = readFileSync14(rewrite.absolute, "utf8");
31407
+ const now = readFileSync15(rewrite.absolute, "utf8");
31324
31408
  if (now !== rewrite.before) {
31325
31409
  throw new Error(`${rewrite.path} changed on disk while the rename was being worked out; nothing was written`);
31326
31410
  }
31327
- if (rewrite.writeTo !== rewrite.absolute && existsSync13(rewrite.writeTo)) {
31411
+ if (rewrite.writeTo !== rewrite.absolute && existsSync14(rewrite.writeTo)) {
31328
31412
  throw new Error(`${rewrite.writeTo} already exists; nothing was written`);
31329
31413
  }
31330
31414
  }
31331
31415
  const done = [];
31332
31416
  try {
31333
31417
  for (const rewrite of rewrites) {
31334
- mkdirSync7(dirname9(rewrite.writeTo), { recursive: true });
31418
+ mkdirSync8(dirname10(rewrite.writeTo), { recursive: true });
31335
31419
  const staging = `${rewrite.writeTo}.gdharness-rename`;
31336
- writeFileSync9(staging, rewrite.after, "utf8");
31420
+ writeFileSync10(staging, rewrite.after, "utf8");
31337
31421
  renameSync(staging, rewrite.writeTo);
31338
31422
  let movedUid = false;
31339
31423
  if (rewrite.writeTo !== rewrite.absolute) {
31340
31424
  unlinkSync2(rewrite.absolute);
31341
- if (existsSync13(`${rewrite.absolute}.uid`)) {
31425
+ if (existsSync14(`${rewrite.absolute}.uid`)) {
31342
31426
  renameSync(`${rewrite.absolute}.uid`, `${rewrite.writeTo}.uid`);
31343
31427
  movedUid = true;
31344
31428
  }
@@ -31348,7 +31432,7 @@ function applyRewrites(rewrites) {
31348
31432
  } catch (error) {
31349
31433
  for (const { rewrite, movedUid } of done.reverse()) {
31350
31434
  try {
31351
- writeFileSync9(rewrite.absolute, rewrite.before, "utf8");
31435
+ writeFileSync10(rewrite.absolute, rewrite.before, "utf8");
31352
31436
  if (rewrite.writeTo !== rewrite.absolute) {
31353
31437
  unlinkSync2(rewrite.writeTo);
31354
31438
  if (movedUid) {
@@ -31362,11 +31446,11 @@ function applyRewrites(rewrites) {
31362
31446
  }
31363
31447
 
31364
31448
  // src/rename-symbol.ts
31365
- import { existsSync as existsSync14, mkdirSync as mkdirSync8, readdirSync as readdirSync6, realpathSync as realpathSync5, rmSync as rmSync6, writeFileSync as writeFileSync10 } from "node:fs";
31366
- import { basename as basename3, dirname as dirname10, join as join16, relative as relative3, resolve as resolve5 } from "node:path";
31449
+ import { existsSync as existsSync15, mkdirSync as mkdirSync9, readdirSync as readdirSync6, realpathSync as realpathSync5, rmSync as rmSync7, writeFileSync as writeFileSync11 } from "node:fs";
31450
+ import { basename as basename3, dirname as dirname11, join as join17, relative as relative3, resolve as resolve5 } from "node:path";
31367
31451
 
31368
31452
  // src/resources.ts
31369
- import { readFileSync as readFileSync15, realpathSync as realpathSync4 } from "node:fs";
31453
+ import { readFileSync as readFileSync16, realpathSync as realpathSync4 } from "node:fs";
31370
31454
  import { extname, resolve as resolve4 } from "node:path";
31371
31455
  var STATIC_RESOURCES = [
31372
31456
  {
@@ -31576,7 +31660,7 @@ function readResourceText(uri, getProjectPath) {
31576
31660
  const parsedUri = parseGodotUri(uri);
31577
31661
  if (parsedUri.kind === "project-info") {
31578
31662
  const projectFilePath = resolveProjectFile(projectPath, "project.godot");
31579
- const rawProject = readFileSync15(projectFilePath, "utf-8");
31663
+ const rawProject = readFileSync16(projectFilePath, "utf-8");
31580
31664
  const parsedProject = parseProjectGodot(rawProject);
31581
31665
  return {
31582
31666
  mimeType: "application/json",
@@ -31585,7 +31669,7 @@ function readResourceText(uri, getProjectPath) {
31585
31669
  }
31586
31670
  const filePath = resolveProjectFile(projectPath, parsedUri.resourcePath);
31587
31671
  ensureAllowedExtension(parsedUri.kind, filePath);
31588
- const text = readFileSync15(filePath, "utf-8");
31672
+ const text = readFileSync16(filePath, "utf-8");
31589
31673
  return {
31590
31674
  mimeType: parsedUri.kind === "script" ? "text/x-gdscript" : "text/plain",
31591
31675
  text
@@ -31643,9 +31727,9 @@ function onDiskSpelling(path) {
31643
31727
  let at = resolve5(path);
31644
31728
  for (;; ) {
31645
31729
  try {
31646
- return join16(realpathSync5.native(at), ...missing.reverse());
31730
+ return join17(realpathSync5.native(at), ...missing.reverse());
31647
31731
  } catch {
31648
- const parent = dirname10(at);
31732
+ const parent = dirname11(at);
31649
31733
  if (parent === at) {
31650
31734
  return resolve5(path);
31651
31735
  }
@@ -31696,7 +31780,7 @@ async function renameSymbol(request, services) {
31696
31780
  reason: `newScriptPath must be a .gd path inside the project, and ${request.newScriptPath} is not.`
31697
31781
  };
31698
31782
  }
31699
- if (existsSync14(request.newScriptPath)) {
31783
+ if (existsSync15(request.newScriptPath)) {
31700
31784
  return { ok: false, reason: `${moveTo} already exists, so the script cannot be moved there.` };
31701
31785
  }
31702
31786
  }
@@ -31872,7 +31956,7 @@ async function renameSymbol(request, services) {
31872
31956
  scripts,
31873
31957
  from: declaringPath,
31874
31958
  to: moveTo,
31875
- exists: (path) => existsSync14(`${projectPath}/${path.slice("res://".length)}`)
31959
+ exists: (path) => existsSync15(`${projectPath}/${path.slice("res://".length)}`)
31876
31960
  }, plan);
31877
31961
  }
31878
31962
  plan = mentionIgnored(plan, ignoredTexts(projectPath), symbol, moveTo === null ? null : declaringPath);
@@ -32073,15 +32157,15 @@ ${mention.kind}`;
32073
32157
  var LEFT_ALONE_ANSWER_LINES = 300;
32074
32158
  var detailWrites = 0;
32075
32159
  function writeLeftAloneDetail(projectPath, symbol, detail) {
32076
- const directory = join16(projectPath, ".godot", "gdharness-renames");
32077
- mkdirSync8(directory, { recursive: true });
32160
+ const directory = join17(projectPath, ".godot", "gdharness-renames");
32161
+ mkdirSync9(directory, { recursive: true });
32078
32162
  detailWrites += 1;
32079
- const path = join16(directory, `${String(Date.now())}-${String(detailWrites).padStart(6, "0")}-${String(process.pid)}-${symbol}.json`);
32080
- writeFileSync10(path, `${JSON.stringify(detail, null, 2)}
32163
+ const path = join17(directory, `${String(Date.now())}-${String(detailWrites).padStart(6, "0")}-${String(process.pid)}-${symbol}.json`);
32164
+ writeFileSync11(path, `${JSON.stringify(detail, null, 2)}
32081
32165
  `, "utf8");
32082
32166
  const kept = readdirSync6(directory).filter((name) => name.endsWith(".json")).sort().reverse();
32083
32167
  for (const old of kept.slice(10)) {
32084
- rmSync6(join16(directory, old), { force: true });
32168
+ rmSync7(join17(directory, old), { force: true });
32085
32169
  }
32086
32170
  return path;
32087
32171
  }
@@ -32104,16 +32188,16 @@ function summaryOf(edits, mentions, rewrites, left) {
32104
32188
  }
32105
32189
 
32106
32190
  // src/restart-note.ts
32107
- import { mkdirSync as mkdirSync9, readFileSync as readFileSync16, rmSync as rmSync7, writeFileSync as writeFileSync11 } from "node:fs";
32108
- import { dirname as dirname11, join as join17 } from "node:path";
32191
+ import { mkdirSync as mkdirSync10, readFileSync as readFileSync17, rmSync as rmSync8, writeFileSync as writeFileSync12 } from "node:fs";
32192
+ import { dirname as dirname12, join as join18 } from "node:path";
32109
32193
  function restartNotePath(projectPath) {
32110
- return join17(projectPath, ".godot", "gdharness-restart.json");
32194
+ return join18(projectPath, ".godot", "gdharness-restart.json");
32111
32195
  }
32112
32196
  function noteRestartBegun(note) {
32113
32197
  const path = restartNotePath(note.projectPath);
32114
32198
  try {
32115
- mkdirSync9(dirname11(path), { recursive: true });
32116
- writeFileSync11(path, `${JSON.stringify(note, null, 2)}
32199
+ mkdirSync10(dirname12(path), { recursive: true });
32200
+ writeFileSync12(path, `${JSON.stringify(note, null, 2)}
32117
32201
  `, "utf8");
32118
32202
  } catch (error) {
32119
32203
  console.error(`[SERVER] Could not record the restart at ${path}: ${errorMessage(error)}`);
@@ -32121,7 +32205,7 @@ function noteRestartBegun(note) {
32121
32205
  }
32122
32206
  function restartOwed(projectPath) {
32123
32207
  try {
32124
- const parsed = JSON.parse(readFileSync16(restartNotePath(projectPath), "utf8"));
32208
+ const parsed = JSON.parse(readFileSync17(restartNotePath(projectPath), "utf8"));
32125
32209
  if (typeof parsed !== "object" || parsed === null) {
32126
32210
  return null;
32127
32211
  }
@@ -32136,7 +32220,7 @@ function restartOwed(projectPath) {
32136
32220
  }
32137
32221
  function restartSettled(projectPath) {
32138
32222
  try {
32139
- rmSync7(restartNotePath(projectPath), { force: true });
32223
+ rmSync8(restartNotePath(projectPath), { force: true });
32140
32224
  } catch (error) {
32141
32225
  console.error(`[SERVER] Could not clear the restart note: ${errorMessage(error)}`);
32142
32226
  }
@@ -32146,29 +32230,29 @@ function restartSettled(projectPath) {
32146
32230
  import { execFile as execFile4 } from "node:child_process";
32147
32231
  import { createHash as createHash3 } from "node:crypto";
32148
32232
  import {
32149
- mkdirSync as mkdirSync10,
32233
+ mkdirSync as mkdirSync11,
32150
32234
  openSync as openSync2,
32151
32235
  readdirSync as readdirSync7,
32152
- readFileSync as readFileSync17,
32236
+ readFileSync as readFileSync18,
32153
32237
  realpathSync as realpathSync6,
32154
- rmSync as rmSync8,
32238
+ rmSync as rmSync9,
32155
32239
  statSync as statSync6,
32156
- writeFileSync as writeFileSync12
32240
+ writeFileSync as writeFileSync13
32157
32241
  } from "node:fs";
32158
- import { basename as basename4, dirname as dirname12, join as join18, resolve as resolve6 } from "node:path";
32242
+ import { basename as basename4, dirname as dirname13, join as join19, resolve as resolve6 } from "node:path";
32159
32243
  import { promisify as promisify4 } from "node:util";
32160
32244
  var execFileAsync = promisify4(execFile4);
32161
32245
  function runsDirectory() {
32162
- return join18(runtimeDirectory(), "runs");
32246
+ return join19(runtimeDirectory(), "runs");
32163
32247
  }
32164
32248
  function realAsFarAsItExists(path) {
32165
32249
  const missing = [];
32166
32250
  let at = path;
32167
32251
  for (;; ) {
32168
32252
  try {
32169
- return join18(realpathSync6.native(at), ...missing);
32253
+ return join19(realpathSync6.native(at), ...missing);
32170
32254
  } catch {
32171
- const parent = dirname12(at);
32255
+ const parent = dirname13(at);
32172
32256
  if (parent === at) {
32173
32257
  return path;
32174
32258
  }
@@ -32183,27 +32267,27 @@ function projectKey(projectPath) {
32183
32267
  return createHash3("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
32184
32268
  }
32185
32269
  function runRecordPath(projectPath, directory = runtimeDirectory()) {
32186
- return join18(directory, "runs", `note-${projectKey(projectPath)}.json`);
32270
+ return join19(directory, "runs", `note-${projectKey(projectPath)}.json`);
32187
32271
  }
32188
32272
  var SHARED_NOTE = "run.json";
32189
32273
  function openTranscript(startedAt) {
32190
- mkdirSync10(runsDirectory(), { recursive: true });
32191
- const path = join18(runsDirectory(), `run-${startedAt}.log`);
32274
+ mkdirSync11(runsDirectory(), { recursive: true });
32275
+ const path = join19(runsDirectory(), `run-${startedAt}.log`);
32192
32276
  return { path, fd: openSync2(path, "a") };
32193
32277
  }
32194
32278
  function noteIsAbout(note, playing) {
32195
32279
  return note.play !== undefined && playing !== null && note.play.play === playing.play && note.play.editorPid === playing.editorPid;
32196
32280
  }
32197
32281
  function editorNotePath(projectPath, directory = runtimeDirectory()) {
32198
- return join18(directory, "runs", `editor-${projectKey(projectPath)}.json`);
32282
+ return join19(directory, "runs", `editor-${projectKey(projectPath)}.json`);
32199
32283
  }
32200
32284
  function writeEditorRunNote(note) {
32201
- mkdirSync10(runsDirectory(), { recursive: true });
32202
- writeFileSync12(editorNotePath(note.projectPath), JSON.stringify(note, null, 2), "utf8");
32285
+ mkdirSync11(runsDirectory(), { recursive: true });
32286
+ writeFileSync13(editorNotePath(note.projectPath), JSON.stringify(note, null, 2), "utf8");
32203
32287
  }
32204
32288
  function readEditorRunNote(projectPath) {
32205
32289
  for (const directory of runtimeDirectories()) {
32206
- const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join18(directory, "runs", "editor-run.json"));
32290
+ const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join19(directory, "runs", "editor-run.json"));
32207
32291
  if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
32208
32292
  return note;
32209
32293
  }
@@ -32213,7 +32297,7 @@ function readEditorRunNote(projectPath) {
32213
32297
  function editorNoteAt(path) {
32214
32298
  let parsed;
32215
32299
  try {
32216
- parsed = JSON.parse(readFileSync17(path, "utf8"));
32300
+ parsed = JSON.parse(readFileSync18(path, "utf8"));
32217
32301
  } catch {
32218
32302
  return null;
32219
32303
  }
@@ -32237,7 +32321,7 @@ function readRunRecord(projectPath) {
32237
32321
  if (own !== null) {
32238
32322
  return own;
32239
32323
  }
32240
- const shared = recordAt(join18(directory, "runs", SHARED_NOTE));
32324
+ const shared = recordAt(join19(directory, "runs", SHARED_NOTE));
32241
32325
  if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
32242
32326
  return shared;
32243
32327
  }
@@ -32249,13 +32333,13 @@ function everyRunRecord() {
32249
32333
  for (const directory of runtimeDirectories()) {
32250
32334
  let entries;
32251
32335
  try {
32252
- entries = readdirSync7(join18(directory, "runs"));
32336
+ entries = readdirSync7(join19(directory, "runs"));
32253
32337
  } catch {
32254
32338
  continue;
32255
32339
  }
32256
32340
  for (const entry of entries) {
32257
32341
  if (entry === SHARED_NOTE || entry.startsWith("note-") && entry.endsWith(".json")) {
32258
- const record = recordAt(join18(directory, "runs", entry));
32342
+ const record = recordAt(join19(directory, "runs", entry));
32259
32343
  if (record !== null) {
32260
32344
  found.push(record);
32261
32345
  }
@@ -32267,7 +32351,7 @@ function everyRunRecord() {
32267
32351
  function recordAt(path) {
32268
32352
  let parsed;
32269
32353
  try {
32270
- parsed = JSON.parse(readFileSync17(path, "utf8"));
32354
+ parsed = JSON.parse(readFileSync18(path, "utf8"));
32271
32355
  } catch {
32272
32356
  return null;
32273
32357
  }
@@ -32318,7 +32402,7 @@ async function runningAs(pid) {
32318
32402
  const began = await startedAt(pid, null);
32319
32403
  const when = began === null ? {} : { startedAt: began };
32320
32404
  if (process.platform === "linux") {
32321
- const raw = readFileSync17(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
32405
+ const raw = readFileSync18(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
32322
32406
  return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
32323
32407
  }
32324
32408
  const args = (await execFileAsync("ps", ["-p", String(pid), "-o", "args="], { encoding: "utf8", timeout: 15000 })).stdout.trim();
@@ -32464,12 +32548,12 @@ function judgeAnnouncedGame(running, projectPath, announcedAt) {
32464
32548
  }
32465
32549
  function clearRunRecord(projectPath) {
32466
32550
  for (const directory of runtimeDirectories()) {
32467
- const shared = join18(directory, "runs", SHARED_NOTE);
32551
+ const shared = join19(directory, "runs", SHARED_NOTE);
32468
32552
  const sharedRecord = recordAt(shared);
32469
32553
  const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
32470
32554
  for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
32471
32555
  try {
32472
- rmSync8(path, { force: true });
32556
+ rmSync9(path, { force: true });
32473
32557
  } catch {}
32474
32558
  }
32475
32559
  }
@@ -32494,18 +32578,18 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
32494
32578
  if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
32495
32579
  continue;
32496
32580
  }
32497
- const path = join18(directory, entry);
32581
+ const path = join19(directory, entry);
32498
32582
  if (kept.has(path) || ageMs(path, now) < keepMs) {
32499
32583
  continue;
32500
32584
  }
32501
32585
  try {
32502
- rmSync8(path);
32586
+ rmSync9(path);
32503
32587
  } catch {}
32504
32588
  }
32505
32589
  }
32506
32590
 
32507
32591
  // src/script-origins.ts
32508
- import { join as join19 } from "node:path";
32592
+ import { join as join20 } from "node:path";
32509
32593
  var UNRESOLVED = /Could not resolve class "([A-Za-z_][A-Za-z0-9_]*)", because of a parser error/;
32510
32594
  function unresolvedClassIn(message) {
32511
32595
  return UNRESOLVED.exec(message)?.[1] ?? null;
@@ -32561,7 +32645,7 @@ function checkWithEngine(godotPath, projectPath) {
32561
32645
  const ran = await runEngine(godotPath, [
32562
32646
  "--headless",
32563
32647
  "--log-file",
32564
- join19(logDir, "engine.log"),
32648
+ join20(logDir, "engine.log"),
32565
32649
  "--path",
32566
32650
  projectPath,
32567
32651
  "--check-only",
@@ -32602,16 +32686,16 @@ import { spawnSync } from "node:child_process";
32602
32686
  import { createHash as createHash4 } from "node:crypto";
32603
32687
  import {
32604
32688
  cpSync,
32605
- existsSync as existsSync15,
32606
- mkdirSync as mkdirSync11,
32689
+ existsSync as existsSync16,
32690
+ mkdirSync as mkdirSync12,
32607
32691
  readdirSync as readdirSync8,
32608
- readFileSync as readFileSync18,
32692
+ readFileSync as readFileSync19,
32609
32693
  rmdirSync,
32610
- rmSync as rmSync9,
32694
+ rmSync as rmSync10,
32611
32695
  statSync as statSync7,
32612
- writeFileSync as writeFileSync13
32696
+ writeFileSync as writeFileSync14
32613
32697
  } from "node:fs";
32614
- import { dirname as dirname13, join as join20 } from "node:path";
32698
+ import { dirname as dirname14, join as join21 } from "node:path";
32615
32699
  import { fileURLToPath as fileURLToPath3 } from "node:url";
32616
32700
  var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
32617
32701
  var EDITOR_PLUGINS = ["gdharness_editor", "auto_reload"];
@@ -32635,21 +32719,21 @@ function addonsDigest(names, from) {
32635
32719
  if (name === VERSION_MARKER || name === DIGEST_MARKER) {
32636
32720
  continue;
32637
32721
  }
32638
- const path = join20(directory, name);
32722
+ const path = join21(directory, name);
32639
32723
  const inside = `${relative}/${name}`;
32640
32724
  if (statSync7(path).isDirectory()) {
32641
32725
  walk(path, inside);
32642
32726
  } else {
32643
32727
  hash.update(`${inside}
32644
32728
  `);
32645
- hash.update(readFileSync18(path));
32729
+ hash.update(readFileSync19(path));
32646
32730
  hash.update(`
32647
32731
  `);
32648
32732
  }
32649
32733
  }
32650
32734
  };
32651
32735
  for (const name of names) {
32652
- walk(join20(from, name), name);
32736
+ walk(join21(from, name), name);
32653
32737
  }
32654
32738
  return hash.digest("hex");
32655
32739
  }
@@ -32682,22 +32766,22 @@ function installedRuntimeDigest(projectPath) {
32682
32766
  return digestMarker(projectPath, RUNTIME_ADDON);
32683
32767
  }
32684
32768
  function digestMarker(projectPath, addon) {
32685
- const marker = join20(projectPath, "addons", addon, DIGEST_MARKER);
32686
- if (!existsSync15(marker)) {
32769
+ const marker = join21(projectPath, "addons", addon, DIGEST_MARKER);
32770
+ if (!existsSync16(marker)) {
32687
32771
  return null;
32688
32772
  }
32689
- const said = readFileSync18(marker, "utf8").trim();
32773
+ const said = readFileSync19(marker, "utf8").trim();
32690
32774
  return said === "" ? null : said;
32691
32775
  }
32692
32776
  function shippedAddonsDirectory() {
32693
- return join20(dirname13(fileURLToPath3(import.meta.url)), "godot", "addons");
32777
+ return join21(dirname14(fileURLToPath3(import.meta.url)), "godot", "addons");
32694
32778
  }
32695
32779
  function installedAddonVersion(projectPath) {
32696
- const marker = join20(projectPath, "addons", ADDONS[0], VERSION_MARKER);
32697
- if (!existsSync15(marker)) {
32780
+ const marker = join21(projectPath, "addons", ADDONS[0], VERSION_MARKER);
32781
+ if (!existsSync16(marker)) {
32698
32782
  return null;
32699
32783
  }
32700
- const said = readFileSync18(marker, "utf8").trim();
32784
+ const said = readFileSync19(marker, "utf8").trim();
32701
32785
  return said === "" ? null : said;
32702
32786
  }
32703
32787
  function loaderAmong(projectPath, named) {
@@ -32708,7 +32792,7 @@ function loaderAmong(projectPath, named) {
32708
32792
  }
32709
32793
  let source = null;
32710
32794
  try {
32711
- source = readFileSync18(join20(projectPath, path), "utf8");
32795
+ source = readFileSync19(join21(projectPath, path), "utf8");
32712
32796
  } catch {}
32713
32797
  if (source?.includes(RUNTIME_AUTOLOAD.path)) {
32714
32798
  return { name: entry, path, exists: true };
@@ -32750,10 +32834,10 @@ function enabledPlugins(settings) {
32750
32834
  function inspectProject(projectPath) {
32751
32835
  const problems = [];
32752
32836
  const addons = ADDONS.map((name) => {
32753
- const directory = join20(projectPath, "addons", name);
32754
- const marker = join20(directory, VERSION_MARKER);
32755
- const installed = existsSync15(directory);
32756
- const version = existsSync15(marker) ? readFileSync18(marker, "utf8").trim() : null;
32837
+ const directory = join21(projectPath, "addons", name);
32838
+ const marker = join21(directory, VERSION_MARKER);
32839
+ const installed = existsSync16(directory);
32840
+ const version = existsSync16(marker) ? readFileSync19(marker, "utf8").trim() : null;
32757
32841
  const current = version === SERVER_VERSION;
32758
32842
  if (!installed) {
32759
32843
  problems.push(`addons/${name} is not installed; run gdharness setup`);
@@ -32762,7 +32846,7 @@ function inspectProject(projectPath) {
32762
32846
  }
32763
32847
  return { name, installed, version, current };
32764
32848
  });
32765
- const settings = parseProjectGodot(readFileSync18(join20(projectPath, "project.godot"), "utf8"));
32849
+ const settings = parseProjectGodot(readFileSync19(join21(projectPath, "project.godot"), "utf8"));
32766
32850
  const pluginsEnabled = enabledPlugins(settings);
32767
32851
  for (const name of EDITOR_PLUGINS) {
32768
32852
  if (!pluginsEnabled.includes(name)) {
@@ -32778,7 +32862,7 @@ function inspectProject(projectPath) {
32778
32862
  const tracked = trackedByGit(projectPath, [...named.values()]);
32779
32863
  const addonMissing = (path) => addons.some((addon) => !addon.installed && path.startsWith(`addons/${addon.name}/`));
32780
32864
  for (const [name, path] of named) {
32781
- if (!existsSync15(join20(projectPath, path))) {
32865
+ if (!existsSync16(join21(projectPath, path))) {
32782
32866
  if (!addonMissing(path)) {
32783
32867
  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`);
32784
32868
  }
@@ -33184,7 +33268,7 @@ var TOOL_SPECS = [
33184
33268
  },
33185
33269
  {
33186
33270
  name: "project_test",
33187
- description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Suites where everything passed are counted rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines. The class list is rebuilt first, so a suite written a moment ago is found, and the rebuild's answer is under classes, with the classes each addon declares counted under addedInAddons and changedInAddons rather than named: a class it puts back that the cache had after the last rebuild is named under classes.lostSinceLastRebuild, which is the editor writing its shorter list over the file on every save until restarted. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. On macOS the engine reads user:// off HOME and the run writes where the player keeps theirs, which the answer says under savesNote rather than leaving to be found in a save list. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so and names the path it looked in. A script that does not load stops gdUnit4 before any suite runs: its errors come back in full under scriptErrors, and the scripts that failed only because one they depend on did are listed by path under failedThroughDependency. When they failed on a class whose own script has a parser error, which gdUnit4 never names, that script is checked by the engine and its error comes first under scriptErrors, with the class it declares under declares. A run where nothing failed but nodes were left in the tree comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. Needs gdUnit4 under addons/gdUnit4.",
33271
+ description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Suites where everything passed are counted rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines. The class list is rebuilt first, so a suite written a moment ago is found, and the rebuild's answer is under classes, with the classes each addon declares counted under addedInAddons and changedInAddons rather than named: a class it puts back that the cache had after the last rebuild is named under classes.lostSinceLastRebuild, which is the editor writing its shorter list over the file on every save until restarted. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. On macOS the engine reads user:// off HOME and the run writes where the player keeps theirs, which the answer says under savesNote rather than leaving to be found in a save list. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so and names the path it looked in. A script that does not load stops gdUnit4 before any suite runs: its errors come back in full under scriptErrors, and the scripts that failed only because one they depend on did are listed by path under failedThroughDependency. When they failed on a class whose own script has a parser error, which gdUnit4 never names, that script is checked by the engine and its error comes first under scriptErrors, with the class it declares under declares, and the errors a script has only because that class did not resolve, which the engine reads as Variant, are kept apart under followFromUnresolved. A client that sends a progress token is told as each suite finishes, and at least every twenty seconds through a long one, so an idle limit does not end a long tier. A run that ends without a report, at its timeout or otherwise, names the suites it finished under suitesFinished, the failing ones with their failed cases under suitesFailing, and the one it was in under suiteRunning. When a client cuts a run off, that is kept in the project and the next project_test there carries it under previousRunCut. A run where nothing failed but nodes were left in the tree comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. Needs gdUnit4 under addons/gdUnit4.",
33188
33272
  parameters: {
33189
33273
  projectPath: PROJECT_PATH,
33190
33274
  path: {
@@ -34009,7 +34093,7 @@ var TOOL_SPECS = [
34009
34093
  requires: [],
34010
34094
  operations: {
34011
34095
  click: {
34012
- summary: `press and release on a Control, a frame apart, and answer with what was under the pointer and what became of the control: in_tree, removed or freed. The control is named by nodePath, or by says with the words on it, found under nodePath when both are given, and the answer then says under found which match it was. Words that are part of a RichTextLabel are pressed where they are drawn rather than at the label's middle, so a link among them is the one pressed: found.pressedOn says "the words", or "the middle of the label" with a note saying why they could not be placed, and link_pressed names the meta the label reports clicked. A control out of sight inside a ScrollContainer is scrolled to first, each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A disabled button, and a control that does not process (the game paused, say), are refused: the pointer reaches them and the press does nothing. A 3D node is clicked where it is drawn, and landed then says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room`,
34096
+ summary: `press and release on a Control, a frame apart, and answer with what was under the pointer and what became of the control: in_tree, removed or freed. The control is named by nodePath, or by says with the words on it, found under nodePath when both are given, and the answer then says under found which match it was. Words that are part of a RichTextLabel are pressed where they are drawn rather than at the label's middle, so a link among them is the one pressed: found.pressedOn says "the words", or "the middle of the label" with a note saying why they could not be placed, and on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control out of sight inside a ScrollContainer is scrolled to first, each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A disabled button, and a control that does not process (the game paused, say), are refused: the pointer reaches them and the press does nothing. A 3D node is clicked where it is drawn, and landed then says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room`,
34013
34097
  requires: []
34014
34098
  },
34015
34099
  choose: {
@@ -34184,11 +34268,28 @@ function buildToolDefinitions() {
34184
34268
  });
34185
34269
  }
34186
34270
 
34271
+ // src/tool-reference.ts
34272
+ function namedType(declared) {
34273
+ if (typeof declared === "string") {
34274
+ return declared;
34275
+ }
34276
+ if (Array.isArray(declared) && declared.every((kind) => typeof kind === "string")) {
34277
+ return declared.join(" or ");
34278
+ }
34279
+ return "any";
34280
+ }
34281
+ function argumentShape(schema) {
34282
+ const named = namedType(schema["type"]);
34283
+ const items = schema["items"];
34284
+ const held = typeof items === "object" && items !== null && !Array.isArray(items) ? namedType(items["type"]) : "any";
34285
+ return named.split(" or ").map((kind) => kind === "array" && held !== "any" ? `array of ${held}s` : kind).join(" or ");
34286
+ }
34287
+
34187
34288
  // src/server.ts
34188
34289
  var UPDATE_NOTICE_EVERY = 500;
34189
34290
  var FEEDBACK_NOTICE_EVERY = 250;
34190
34291
  var run4 = promisify5(execFile5);
34191
- var __dirname2 = dirname14(fileURLToPath4(import.meta.url));
34292
+ var __dirname2 = dirname15(fileURLToPath4(import.meta.url));
34192
34293
  function editorRestartTimeoutMs() {
34193
34294
  const override = Number.parseInt(envValue("GDHARNESS_EDITOR_RESTART_TIMEOUT_MS") ?? "", 10);
34194
34295
  return Number.isInteger(override) && override > 0 ? override : 90000;
@@ -34209,16 +34310,16 @@ function captureDestinationRefusal(outputPath, projects) {
34209
34310
  if (extname2(outputPath).toLowerCase() !== ".png") {
34210
34311
  return `outputPath ${outputPath} does not end in .png, which is what a capture is.`;
34211
34312
  }
34212
- const directory = realPathOr(dirname14(outputPath));
34213
- if (!existsSync16(directory)) {
34214
- return `outputPath's directory ${dirname14(outputPath)} does not exist; a capture creates no directories.`;
34313
+ const directory = realPathOr(dirname15(outputPath));
34314
+ if (!existsSync17(directory)) {
34315
+ return `outputPath's directory ${dirname15(outputPath)} does not exist; a capture creates no directories.`;
34215
34316
  }
34216
- const where = join21(directory, basename5(outputPath));
34317
+ const where = join22(directory, basename5(outputPath));
34217
34318
  const inside = projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
34218
34319
  if (inside !== undefined) {
34219
34320
  return `outputPath ${outputPath} is inside the project at ${inside}; a capture is saved outside it, so it never becomes part of the game.`;
34220
34321
  }
34221
- if (existsSync16(where)) {
34322
+ if (existsSync17(where)) {
34222
34323
  return `outputPath ${outputPath} already exists; a capture is not written over a file. Name a new one.`;
34223
34324
  }
34224
34325
  return null;
@@ -34722,18 +34823,63 @@ function settingsNamedBy(op, args) {
34722
34823
  }
34723
34824
  }
34724
34825
  var STALLED_AFTER_MS = 30000;
34826
+ var TEST_HEARTBEAT_MS = 20000;
34725
34827
  var DEPENDED_FAILED = "Failed to compile depended scripts";
34726
34828
  function foldedScriptErrors(errors) {
34727
- const scriptErrors = errors.filter((error) => !error.message.includes(DEPENDED_FAILED));
34829
+ const unresolved = new Map;
34830
+ for (const error of errors) {
34831
+ const name = unresolvedClassIn(error.message);
34832
+ if (name !== null) {
34833
+ unresolved.set(error.path, (unresolved.get(error.path) ?? new Set).add(name));
34834
+ }
34835
+ }
34836
+ const follows = (error) => {
34837
+ const names = unresolved.get(error.path);
34838
+ if (names === undefined) {
34839
+ return false;
34840
+ }
34841
+ const reparsed = /Could not parse global class "([A-Za-z_][A-Za-z0-9_]*)"/.exec(error.message)?.[1];
34842
+ return error.message.includes('the inferred type "Variant"') || reparsed !== undefined && names.has(reparsed);
34843
+ };
34844
+ const scriptErrors = errors.filter((error) => !error.message.includes(DEPENDED_FAILED) && !follows(error));
34728
34845
  const own = new Set(scriptErrors.map((error) => error.path));
34729
34846
  return {
34730
34847
  scriptErrors,
34731
- failedThroughDependency: [...new Set(errors.map((error) => error.path))].filter((path) => !own.has(path))
34848
+ failedThroughDependency: [...new Set(errors.map((error) => error.path))].filter((path) => !own.has(path)),
34849
+ followFromUnresolved: errors.filter(follows)
34732
34850
  };
34733
34851
  }
34734
34852
  function nonEmpty(lists) {
34735
34853
  return Object.fromEntries(Object.entries(lists).filter(([, list]) => list.length > 0));
34736
34854
  }
34855
+ function withPreviousCut(answer, previous) {
34856
+ const content = answer.content.map((part) => ({ ...part }));
34857
+ const last = content.at(-1);
34858
+ let json = null;
34859
+ try {
34860
+ const parsed = JSON.parse(last?.text ?? "");
34861
+ json = typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? { ...parsed } : null;
34862
+ } catch {}
34863
+ if (last !== undefined && json !== null) {
34864
+ last.text = answerJson({ ...json, previousRunCut: previous });
34865
+ }
34866
+ const note = cutTestRunNote(previous);
34867
+ const first = content[0];
34868
+ if (first === undefined || first === last && json !== null || typeof first.text !== "string") {
34869
+ content.unshift({ type: "text", text: note });
34870
+ } else {
34871
+ first.text = `${first.text} ${note}`;
34872
+ }
34873
+ return { ...answer, content };
34874
+ }
34875
+ function finishedBeforeTheEnd(finished, failing, running) {
34876
+ if (finished === 0 && running === null) {
34877
+ return "";
34878
+ }
34879
+ const got = finished === 0 ? "no suite had finished" : `${finished === 1 ? "one suite" : `${finished} suites`} finished${failing === 0 ? "" : finished === 1 ? ", with failures" : `, ${failing} of them with failures`}`;
34880
+ const listed = finished === 0 ? "" : `; ${finished === 1 ? "it is" : "they are"} under suitesFinished${failing === 0 ? "" : ", with the failing cases under suitesFailing"}, so a run of the rest need not repeat ${finished === 1 ? "it" : "them"}`;
34881
+ return ` Before it ended, ${got}${running === null ? "" : `${finished === 0 ? " and" : ", and"} ${running} was running`}${listed}.`;
34882
+ }
34737
34883
  function scriptErrorsNote(errors, {
34738
34884
  origins,
34739
34885
  unconfirmed
@@ -34741,7 +34887,11 @@ function scriptErrorsNote(errors, {
34741
34887
  origins: [],
34742
34888
  unconfirmed: []
34743
34889
  }) {
34744
- const { scriptErrors: own, failedThroughDependency: dependents } = foldedScriptErrors(errors);
34890
+ const {
34891
+ scriptErrors: own,
34892
+ failedThroughDependency: dependents,
34893
+ followFromUnresolved: consequences
34894
+ } = foldedScriptErrors(errors);
34745
34895
  const causes = new Map;
34746
34896
  for (const error of own) {
34747
34897
  if (!causes.has(error.path)) {
@@ -34768,12 +34918,10 @@ function scriptErrorsNote(errors, {
34768
34918
  const following = dependents.length === 0 ? "" : causes.size === 0 ? `${dependents.join(", ")} ${dependents.length === 1 ? "depends" : "depend"} on a script that did not compile, and gdUnit4 named none with an error of its own: script_diagnostics on one of them finds it.` : `${dependents.length === 1 ? "One more" : `${dependents.length} more`} failed only because a script ${dependents.length === 1 ? "it depends" : "they depend"} on did: ${dependents.join(", ")}.`;
34769
34919
  const more = own.length - causes.size;
34770
34920
  const rest = more > 0 ? `${more === 1 ? "One more error" : `${more} more errors`} in ${causes.size === 1 ? "that script" : "those scripts"} ${more === 1 ? "is" : "are"} under scriptErrors.` : "";
34771
- return [
34772
- `No tests ran: gdUnit4 could not load ${scripts === 1 ? "one script" : `${scripts} scripts`} while looking for suites, and it runs no suite at all when one fails to load.`,
34773
- named,
34774
- following,
34775
- rest
34776
- ].filter((part) => part !== "").join(" ");
34921
+ const after = consequences.length > 0 ? `${consequences.length === 1 ? "One more error comes" : `${consequences.length} more errors come`} only from a class that did not resolve, which the engine reads as Variant, and ${consequences.length === 1 ? "is" : "are"} under followFromUnresolved.` : "";
34922
+ const unnamed = first.length;
34923
+ const opening = unnamed === 0 ? `No tests ran: gdUnit4 could not load ${scripts === 1 ? "one script" : `${scripts} scripts`} while looking for suites, and it runs no suite at all when one fails to load.` : `No tests ran: ${scripts + unnamed} scripts did not compile, and gdUnit4 runs no suite at all when one fails to load. It could not load ${scripts === 1 ? "one" : scripts} of them while looking for suites, and ${unnamed === 1 ? "the one it did not name comes" : `the ${unnamed} it did not name come`} first.`;
34924
+ return [opening, named, following, rest, after].filter((part) => part !== "").join(" ");
34777
34925
  }
34778
34926
  function timedOutVerdict(timeoutMs, silentForMs) {
34779
34927
  const quiet = Math.round(silentForMs);
@@ -34863,7 +35011,7 @@ function aboveTheRunner(entry) {
34863
35011
  }
34864
35012
  function elsewhereIn(projectPath, asked) {
34865
35013
  const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
34866
- if (wanted === "" || existsSync16(join21(projectPath, wanted))) {
35014
+ if (wanted === "" || existsSync17(join22(projectPath, wanted))) {
34867
35015
  return "";
34868
35016
  }
34869
35017
  const root = (wanted.split("/")[0] ?? "").toLowerCase();
@@ -34907,7 +35055,7 @@ function testPathsIn(projectPath, given) {
34907
35055
  };
34908
35056
  }
34909
35057
  function hasMainScene(projectFile) {
34910
- const scene = parseProjectGodot(readFileSync19(projectFile, "utf8"))["application"]?.["run/main_scene"];
35058
+ const scene = parseProjectGodot(readFileSync20(projectFile, "utf8"))["application"]?.["run/main_scene"];
34911
35059
  return typeof scene === "string" && scene !== "";
34912
35060
  }
34913
35061
  function runtimeAddonAnnouncesEditors(projectPath) {
@@ -34915,13 +35063,13 @@ function runtimeAddonAnnouncesEditors(projectPath) {
34915
35063
  return false;
34916
35064
  }
34917
35065
  try {
34918
- return readFileSync19(join21(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
35066
+ return readFileSync20(join22(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
34919
35067
  } catch {
34920
35068
  return false;
34921
35069
  }
34922
35070
  }
34923
35071
  function editorPlaysHeadless(projectFile) {
34924
- const runArgs = parseProjectGodot(readFileSync19(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
35072
+ const runArgs = parseProjectGodot(readFileSync20(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
34925
35073
  return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
34926
35074
  }
34927
35075
  function camelCased(params) {
@@ -34940,7 +35088,7 @@ function needsItsOwnProcess(asked) {
34940
35088
  class GodotServer {
34941
35089
  mcp;
34942
35090
  locator = new GodotLocator;
34943
- operationsScript = join21(__dirname2, "godot", "operations", "godot_operations.gd");
35091
+ operationsScript = join22(__dirname2, "godot", "operations", "godot_operations.gd");
34944
35092
  godotBridge;
34945
35093
  tools = buildToolDefinitions();
34946
35094
  activeProcess = null;
@@ -35243,11 +35391,20 @@ class GodotServer {
35243
35391
  return checked.response;
35244
35392
  }
35245
35393
  const named = args["projectPath"];
35246
- if (typeof named === "string" && existsSync16(join21(named, "project.godot"))) {
35394
+ if (typeof named === "string" && existsSync17(join22(named, "project.godot"))) {
35247
35395
  this.lastProjectPath = named;
35248
35396
  }
35249
35397
  this.updates.refresh();
35250
- const answer = await withCallSignal(extra.signal, () => this.answered(spec.name, checked.op ?? "", args));
35398
+ const token = request.params._meta?.progressToken;
35399
+ const progress = token === undefined ? undefined : (done, message) => {
35400
+ extra.sendNotification({
35401
+ method: "notifications/progress",
35402
+ params: { progressToken: token, progress: done, message }
35403
+ }).catch((error) => {
35404
+ this.logDebug(`Progress for ${spec.name} was not sent: ${errorMessage(error)}`);
35405
+ });
35406
+ };
35407
+ const answer = await withCallSignal(extra.signal, () => this.answered(spec.name, checked.op ?? "", args), progress);
35251
35408
  return this.withFeedbackNotice(this.withUpdateNotice(answer));
35252
35409
  });
35253
35410
  }
@@ -35374,7 +35531,14 @@ class GodotServer {
35374
35531
  const asked = readString(args, "op");
35375
35532
  const named = asked !== undefined && spec.operations?.[asked] !== undefined ? asked : null;
35376
35533
  const wanted = named === null ? [...known] : [...argumentsOf(spec, named), "op"];
35377
- const takes = wanted.length === 0 ? "It takes no arguments." : `It takes: ${wanted.join(", ")}.`;
35534
+ const shaped = wanted.map((name) => {
35535
+ const schema = spec.parameters[name];
35536
+ if (schema !== undefined) {
35537
+ return `${name} (${argumentShape(schema)})`;
35538
+ }
35539
+ return name === "op" && named === null && spec.operations !== undefined ? `op (${Object.keys(spec.operations).join(", ")})` : name;
35540
+ });
35541
+ const takes = wanted.length === 0 ? "It takes no arguments." : `It takes: ${shaped.join(", ")}.`;
35378
35542
  const where = named === null ? spec.name : `${spec.name} ${named}`;
35379
35543
  return {
35380
35544
  ok: false,
@@ -35717,8 +35881,8 @@ class GodotServer {
35717
35881
  if (path === undefined) {
35718
35882
  return { ok: false, response: this.createErrorResponse("projectPath is required.") };
35719
35883
  }
35720
- const file = join21(path, "project.godot");
35721
- if (!existsSync16(file)) {
35884
+ const file = join22(path, "project.godot");
35885
+ if (!existsSync17(file)) {
35722
35886
  return {
35723
35887
  ok: false,
35724
35888
  response: this.createErrorResponse(`Not a Godot project: ${path}`, [
@@ -36329,7 +36493,7 @@ class GodotServer {
36329
36493
  if (!engine.ok) {
36330
36494
  return engine.response;
36331
36495
  }
36332
- const application = parseProjectGodot(readFileSync19(project.value.file, "utf8"))["application"] ?? {};
36496
+ const application = parseProjectGodot(readFileSync20(project.value.file, "utf8"))["application"] ?? {};
36333
36497
  const name = application["config/name"];
36334
36498
  const mainScene = application["run/main_scene"];
36335
36499
  const info = {
@@ -36385,18 +36549,18 @@ class GodotServer {
36385
36549
  if (!engine.ok) {
36386
36550
  return engine.response;
36387
36551
  }
36388
- const folder = dirname14(output.absolutePath);
36552
+ const folder = dirname15(output.absolutePath);
36389
36553
  try {
36390
- mkdirSync12(folder, { recursive: true });
36554
+ mkdirSync13(folder, { recursive: true });
36391
36555
  } catch (error) {
36392
- return this.createErrorResponse(`Cannot create ${dirname14(output.relativePath)} to export into: ${errorMessage(error)}`);
36556
+ return this.createErrorResponse(`Cannot create ${dirname15(output.relativePath)} to export into: ${errorMessage(error)}`);
36393
36557
  }
36394
36558
  const debug = readBoolean(args, "debug") ?? false;
36395
36559
  const exportLogs = scratchDirectory("export");
36396
36560
  const exportArgs = [
36397
36561
  "--headless",
36398
36562
  "--log-file",
36399
- join21(exportLogs, "engine.log"),
36563
+ join22(exportLogs, "engine.log"),
36400
36564
  "--path",
36401
36565
  project.value.path,
36402
36566
  debug ? "--export-debug" : "--export-release",
@@ -36404,7 +36568,7 @@ class GodotServer {
36404
36568
  output.absolutePath
36405
36569
  ];
36406
36570
  this.logDebug(`Exporting: ${engine.value} ${exportArgs.join(" ")}`);
36407
- const writtenAt = () => existsSync16(output.absolutePath) ? statSync8(output.absolutePath).mtimeMs : null;
36571
+ const writtenAt = () => existsSync17(output.absolutePath) ? statSync8(output.absolutePath).mtimeMs : null;
36408
36572
  const before = writtenAt();
36409
36573
  let ending;
36410
36574
  try {
@@ -36417,7 +36581,38 @@ class GodotServer {
36417
36581
  }
36418
36582
  return exportAnswer(ending, { preset, outputPath: output.relativePath, debug }, before, writtenAt());
36419
36583
  }
36584
+ reportTestProgress(log) {
36585
+ const progress = callProgress();
36586
+ if (progress === undefined) {
36587
+ return;
36588
+ }
36589
+ const began = Date.now();
36590
+ let read = 0;
36591
+ let lastSent = began;
36592
+ const ticker = setInterval(() => {
36593
+ const entries = log.all;
36594
+ const finishedOne = entries.slice(read).some((entry) => finishesASuite(entry.text));
36595
+ read = entries.length;
36596
+ const now = Date.now();
36597
+ if (!finishedOne && now - lastSent < TEST_HEARTBEAT_MS) {
36598
+ return;
36599
+ }
36600
+ lastSent = now;
36601
+ progress(Math.floor((now - began) / 1000), testProgressMessage(entries.map((entry) => entry.text), now - began));
36602
+ }, 1000);
36603
+ ticker.unref();
36604
+ return ticker;
36605
+ }
36420
36606
  async handleRunTests(args) {
36607
+ const answer = await this.runTests(args);
36608
+ const project = this.project(args);
36609
+ if (!project.ok || callSignal()?.aborted === true) {
36610
+ return answer;
36611
+ }
36612
+ const previous = takeCutTestRun(project.value.path);
36613
+ return previous === null ? answer : withPreviousCut(answer, previous);
36614
+ }
36615
+ async runTests(args) {
36421
36616
  const project = this.project(args);
36422
36617
  if (!project.ok) {
36423
36618
  return project.response;
@@ -36426,7 +36621,7 @@ class GodotServer {
36426
36621
  if (!paths.ok) {
36427
36622
  return this.createErrorResponse(paths.reason, PATH_SOLUTIONS);
36428
36623
  }
36429
- const missing = paths.value.filter((path) => !existsSync16(join21(project.value.path, path.slice("res://".length))));
36624
+ const missing = paths.value.filter((path) => !existsSync17(join22(project.value.path, path.slice("res://".length))));
36430
36625
  const asked = paths.value.filter((path) => !missing.includes(path));
36431
36626
  if (asked.length === 0) {
36432
36627
  const verdict = `nothing at ${missing.join(" and ")}`;
@@ -36442,7 +36637,7 @@ class GodotServer {
36442
36637
  };
36443
36638
  }
36444
36639
  const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
36445
- if (!existsSync16(join21(project.value.path, runner))) {
36640
+ if (!existsSync17(join22(project.value.path, runner))) {
36446
36641
  return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
36447
36642
  "Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
36448
36643
  ]);
@@ -36487,6 +36682,7 @@ class GodotServer {
36487
36682
  run.process.stderr?.on("data", () => {
36488
36683
  lastSaid = Date.now();
36489
36684
  });
36685
+ const reporting = this.reportTestProgress(run.log);
36490
36686
  let silentForMs = 0;
36491
36687
  const timedOut = await new Promise((resolve) => {
36492
36688
  const timer = setTimeout(() => {
@@ -36503,25 +36699,26 @@ class GodotServer {
36503
36699
  resolve(false);
36504
36700
  });
36505
36701
  });
36702
+ clearInterval(reporting);
36506
36703
  if (timedOut && run.process.pid !== undefined) {
36507
36704
  await untilGone([run.process.pid]);
36508
36705
  }
36509
36706
  await run.settled;
36510
- const reportsDir = join21(project.value.path, ".godot", ...ours.split("/"));
36707
+ const reportsDir = join22(project.value.path, ".godot", ...ours.split("/"));
36511
36708
  let report = null;
36512
36709
  let reportProblem = null;
36513
36710
  try {
36514
- const written = existsSync16(reportsDir) ? readdirSync9(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
36711
+ const written = existsSync17(reportsDir) ? readdirSync9(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
36515
36712
  const newest = written.at(-1);
36516
36713
  if (newest !== undefined) {
36517
- report = parseJUnit(readFileSync19(join21(reportsDir, newest, "results.xml"), "utf8"));
36714
+ report = parseJUnit(readFileSync20(join22(reportsDir, newest, "results.xml"), "utf8"));
36518
36715
  }
36519
36716
  } catch (error) {
36520
36717
  reportProblem = errorMessage(error);
36521
36718
  } finally {
36522
36719
  discard(reportsDir, userData);
36523
36720
  try {
36524
- rmdirSync2(dirname14(reportsDir));
36721
+ rmdirSync2(dirname15(reportsDir));
36525
36722
  } catch {}
36526
36723
  }
36527
36724
  const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries.map(aboveTheRunner);
@@ -36558,9 +36755,28 @@ class GodotServer {
36558
36755
  printed: echoed.entries.map((entry) => entry.text),
36559
36756
  ...echoed.omitted > 0 ? { printedOmitted: echoed.omitted } : {}
36560
36757
  };
36758
+ const sofar = report === null ? suitesPrinted(said) : null;
36561
36759
  if (report === null) {
36562
- const said = nothingRan !== null ? `No tests ran: ${ranAs}.` : scriptErrors.length > 0 ? scriptErrorsNote(printedErrors, declaring) : reportProblem !== null ? `The test run's report could not be read (${reportProblem}); the engine's own verdict was ${ranAs}.` : `The test run wrote no report (${ranAs}).`;
36760
+ const finished = sofar?.finished ?? [];
36761
+ const running = sofar?.running ?? null;
36762
+ const failing = finished.filter((suite) => suite.errors + suite.failures > 0);
36763
+ const atTheEnd = {
36764
+ ...nonEmpty({
36765
+ suitesFinished: finished.map((suite) => suite.path),
36766
+ suitesFailing: failing.map(({ path, errors, failures, failed }) => ({
36767
+ path,
36768
+ errors,
36769
+ failures,
36770
+ failed
36771
+ }))
36772
+ }),
36773
+ ...running === null ? {} : { suiteRunning: running }
36774
+ };
36775
+ const said = nothingRan !== null ? `No tests ran: ${ranAs}.` : scriptErrors.length > 0 ? scriptErrorsNote(printedErrors, declaring) : reportProblem !== null ? `The test run's report could not be read (${reportProblem}); the engine's own verdict was ${ranAs}.` : `The test run wrote no report (${ranAs}).${finishedBeforeTheEnd(finished.length, failing.length, running)}`;
36563
36776
  const note = missingNote === undefined ? said : `${said} ${missingNote}`;
36777
+ if (callSignal()?.aborted === true) {
36778
+ keepCutTestRun(project.value.path, { cutAt: new Date().toISOString(), paths: asked, ...atTheEnd });
36779
+ }
36564
36780
  return {
36565
36781
  content: [
36566
36782
  { type: "text", text: note },
@@ -36576,6 +36792,7 @@ class GodotServer {
36576
36792
  ...timedOut ? { timedOut, silentForMs } : {},
36577
36793
  ...missing.length > 0 ? { missing } : {},
36578
36794
  ...nonEmpty(foldedScriptErrors(scriptErrors)),
36795
+ ...atTheEnd,
36579
36796
  arguments: cmdArgs,
36580
36797
  entries: forAnswer(withoutListed(printed, scriptErrors).slice(0, 60).map(aboveTheRunner)),
36581
36798
  ...printedLines,
@@ -36710,11 +36927,11 @@ class GodotServer {
36710
36927
  const cached = cachedClasses(projectPath);
36711
36928
  const contradicted = cached === null ? [] : contradictedDiagnostics(messages, cached, (resourcePath) => {
36712
36929
  const contained = resolveWithinProject(projectPath, resourcePath);
36713
- if (!contained.ok || !existsSync16(contained.absolutePath)) {
36930
+ if (!contained.ok || !existsSync17(contained.absolutePath)) {
36714
36931
  return null;
36715
36932
  }
36716
36933
  try {
36717
- return readFileSync19(contained.absolutePath, "utf8");
36934
+ return readFileSync20(contained.absolutePath, "utf8");
36718
36935
  } catch {
36719
36936
  return null;
36720
36937
  }
@@ -36770,11 +36987,11 @@ class GodotServer {
36770
36987
  }
36771
36988
  sourceOfClass(projectPath, resourcePath) {
36772
36989
  const contained = resolveWithinProject(projectPath, resourcePath);
36773
- if (!contained.ok || !existsSync16(contained.absolutePath)) {
36990
+ if (!contained.ok || !existsSync17(contained.absolutePath)) {
36774
36991
  return null;
36775
36992
  }
36776
36993
  try {
36777
- return readFileSync19(contained.absolutePath, "utf8");
36994
+ return readFileSync20(contained.absolutePath, "utf8");
36778
36995
  } catch {
36779
36996
  return null;
36780
36997
  }
@@ -37085,7 +37302,7 @@ class GodotServer {
37085
37302
  return null;
37086
37303
  }
37087
37304
  try {
37088
- return settingKeys(readFileSync19(join21(projectPath, "project.godot"), "utf8"));
37305
+ return settingKeys(readFileSync20(join22(projectPath, "project.godot"), "utf8"));
37089
37306
  } catch {
37090
37307
  return null;
37091
37308
  }
@@ -37407,7 +37624,7 @@ class GodotServer {
37407
37624
  }
37408
37625
  async runtimeUp(projectPath, before, budget, withArgs) {
37409
37626
  const budgetMs = budget.ms;
37410
- if (!existsSync16(join21(projectPath, RUNTIME_AUTOLOAD.path))) {
37627
+ if (!existsSync17(join22(projectPath, RUNTIME_AUTOLOAD.path))) {
37411
37628
  return runtimeVerdict(null, {
37412
37629
  addon: false,
37413
37630
  budgetMs,
@@ -37666,7 +37883,7 @@ class GodotServer {
37666
37883
  return 0;
37667
37884
  }
37668
37885
  try {
37669
- return reportAlreadyInTranscript(readFileSync19(game.transcript), readFileSync19(report));
37886
+ return reportAlreadyInTranscript(readFileSync20(game.transcript), readFileSync20(report));
37670
37887
  } catch (error) {
37671
37888
  this.logDebug(`Could not compare a picked-up run's report with its transcript: ${errorMessage(error)}`);
37672
37889
  return 0;
@@ -38424,7 +38641,7 @@ class GodotServer {
38424
38641
  notes.push(endedWithoutACode({
38425
38642
  throughEditor: run.throughEditor,
38426
38643
  announced: announced !== undefined || run.announcedPid !== undefined,
38427
- couldAnnounce: run.projectPath !== null && existsSync16(join21(run.projectPath, RUNTIME_AUTOLOAD.path)),
38644
+ couldAnnounce: run.projectPath !== null && existsSync17(join22(run.projectPath, RUNTIME_AUTOLOAD.path)),
38428
38645
  endedHere: typeof run.endedHere === "string",
38429
38646
  pickedUp: run.pickedUp === true
38430
38647
  }));
@@ -38985,7 +39202,7 @@ class GodotServer {
38985
39202
  if (!scriptPath.ok) {
38986
39203
  return this.createErrorResponse(scriptPath.reason);
38987
39204
  }
38988
- if (!existsSync16(scriptPath.absolutePath)) {
39205
+ if (!existsSync17(scriptPath.absolutePath)) {
38989
39206
  return this.createErrorResponse(`Script file does not exist: ${scriptPath.absolutePath}`);
38990
39207
  }
38991
39208
  const asked = readNonEmptyString(args, "newScriptPath");
@@ -39273,7 +39490,7 @@ class GodotServer {
39273
39490
  async ownRunOnItsWay(projectPath, budgetMs) {
39274
39491
  const run = await this.currentRun();
39275
39492
  const project = projectPath ?? run?.projectPath ?? null;
39276
- if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync16(join21(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
39493
+ if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync17(join22(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
39277
39494
  return null;
39278
39495
  }
39279
39496
  const already = runtimesAnnounced().running;
@@ -39357,7 +39574,7 @@ class GodotServer {
39357
39574
  }
39358
39575
  }
39359
39576
  const screenshotDir = expectsScreenshot ? scratchDirectory("runtime-screenshot") : null;
39360
- const screenshotPath = screenshotDir ? join21(screenshotDir, "capture.png") : null;
39577
+ const screenshotPath = screenshotDir ? join22(screenshotDir, "capture.png") : null;
39361
39578
  const staleNote = this.runtimeStaleNote(choice.endpoint);
39362
39579
  const stale = staleNote === undefined ? {} : { addonIsStale: true, staleNote };
39363
39580
  const refused = (message) => this.createErrorResponse(staleNote === undefined ? message : `${message}
@@ -39406,7 +39623,7 @@ ${staleNote}`);
39406
39623
 
39407
39624
  ${staleNote}`}`
39408
39625
  },
39409
- { type: "image", data: readFileSync19(screenshotPath).toString("base64"), mimeType: "image/png" }
39626
+ { type: "image", data: readFileSync20(screenshotPath).toString("base64"), mimeType: "image/png" }
39410
39627
  ]
39411
39628
  };
39412
39629
  } finally {