gdharness 1.0.6 → 1.0.8

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
@@ -15694,7 +15694,7 @@ var init_tool_definitions = __esm(() => {
15694
15694
  requires: []
15695
15695
  },
15696
15696
  refresh_classes: {
15697
- summary: 'rewrite .godot/global_script_class_cache.cfg from the class_name declarations on disk, and name under unseenByEditor any class the editor open on this project still cannot resolve: rewriting the file does not reach the list a running editor loaded, so "added: []" means the file was already right rather than that nothing is wrong',
15697
+ summary: 'rewrite .godot/global_script_class_cache.cfg from the class_name declarations on disk, and name under unseenByEditor any class the editor open on this project still cannot resolve: rewriting the file does not reach the list a running editor loaded, so "added: []" means the file was already right rather than that nothing is wrong. A class the cache held after the last rebuild and had lost when this one began is named under lostSinceLastRebuild: the editor wrote the file without it in between, on a save or a scan, and will again until restarted',
15698
15698
  requires: []
15699
15699
  }
15700
15700
  }
@@ -15719,7 +15719,7 @@ var init_tool_definitions = __esm(() => {
15719
15719
  },
15720
15720
  {
15721
15721
  name: "project_test",
15722
- 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. 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 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.",
15722
+ 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: 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 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.",
15723
15723
  parameters: {
15724
15724
  projectPath: PROJECT_PATH,
15725
15725
  path: {
@@ -15936,7 +15936,7 @@ var init_tool_definitions = __esm(() => {
15936
15936
  },
15937
15937
  {
15938
15938
  name: "script_diagnostics",
15939
- description: "Errors and warnings for a script from the editor's language server, and whether the script is clean. Needs the editor running. Diagnostics are checked against the project's own files, and the two that the files disprove are named rather than passed on: a member the class cache's file declares comes back under contradictedByTheFile, and a class this project declares that the diagnostic could not resolve comes back under typesTheEditorHasNotLoaded, with inTheClassCache saying whether a launched game would resolve it. staleAnalysis then says what to do, and for both states it is editor_rescan first: it asks the editor already running, starts nothing, and has cleared each in every reproduction measured. What follows differs: for a stale analysed type, editor_rescan with reloadScript recompiles the held copy and editor_launch restart is the last resort that costs a window; for a class missing from the cache, project_import refresh_classes rewrites the cache from disk at the cost of a short headless engine, which matters beside a running bench.",
15939
+ description: "Errors and warnings for a script from the editor's language server, and whether the script is clean. Needs the editor running. Diagnostics are checked against the project's own files, and the two that the files disprove are named rather than passed on: a member the class cache's file declares comes back under contradictedByTheFile, and a class this project declares that the diagnostic could not resolve comes back under typesTheEditorHasNotLoaded, with inTheClassCache saying whether a launched game would resolve it. staleAnalysis then says what to do, and for both states it is editor_rescan first: it asks the editor already running, starts nothing, and has cleared each in every reproduction measured. The one exception is named when it applies: an editor whose last scan wrote the class cache shorter than the file holds a list behind the files and writes it on every scan, so on that editor the rescan loads nothing and staleAnalysis says editor_launch restart instead. What follows differs: for a stale analysed type, editor_rescan with reloadScript recompiles the held copy and editor_launch restart is the last resort that costs a window; for a class missing from the cache, project_import refresh_classes rewrites the cache from disk at the cost of a short headless engine, which matters beside a running bench.",
15940
15940
  parameters: {
15941
15941
  projectPath: PROJECT_PATH,
15942
15942
  scriptPath: SCRIPT_PATH
@@ -31993,17 +31993,49 @@ var init_bridge_announce = __esm(() => {
31993
31993
  init_errors();
31994
31994
  });
31995
31995
 
31996
+ // src/class-note.ts
31997
+ import { mkdirSync as mkdirSync9, readFileSync as readFileSync14, writeFileSync as writeFileSync11 } from "node:fs";
31998
+ import { dirname as dirname11, join as join16 } from "node:path";
31999
+ function classNotePath(projectPath) {
32000
+ return join16(projectPath, ".godot", "gdharness-classes.json");
32001
+ }
32002
+ function readClassNote(projectPath) {
32003
+ try {
32004
+ const fields = asParams(JSON.parse(readFileSync14(classNotePath(projectPath), "utf8")));
32005
+ const rebuiltWith = readArray(fields, "rebuiltWith");
32006
+ if (rebuiltWith === undefined || !rebuiltWith.every((name) => typeof name === "string")) {
32007
+ return null;
32008
+ }
32009
+ const pid = readNumber(fields, "shortListEditorPid");
32010
+ return {
32011
+ rebuiltWith: rebuiltWith.map(String),
32012
+ ...Object.hasOwn(fields, "shortListEditorPid") ? { shortListEditorPid: pid ?? null } : {}
32013
+ };
32014
+ } catch {
32015
+ return null;
32016
+ }
32017
+ }
32018
+ function writeClassNote(projectPath, note) {
32019
+ try {
32020
+ const path = classNotePath(projectPath);
32021
+ mkdirSync9(dirname11(path), { recursive: true });
32022
+ writeFileSync11(path, `${JSON.stringify(note, null, 2)}
32023
+ `, "utf8");
32024
+ } catch {}
32025
+ }
32026
+ var init_class_note = () => {};
32027
+
31996
32028
  // src/config-pin.ts
31997
- import { existsSync as existsSync11, readFileSync as readFileSync14 } from "node:fs";
31998
- import { join as join16 } from "node:path";
32029
+ import { existsSync as existsSync11, readFileSync as readFileSync15 } from "node:fs";
32030
+ import { join as join17 } from "node:path";
31999
32031
  function versionsNamedInConfig(projectPath) {
32000
- const config = join16(projectPath, ".mcp.json");
32032
+ const config = join17(projectPath, ".mcp.json");
32001
32033
  if (!existsSync11(config)) {
32002
32034
  return [];
32003
32035
  }
32004
32036
  let text;
32005
32037
  try {
32006
- text = readFileSync14(config, "utf8");
32038
+ text = readFileSync15(config, "utf8");
32007
32039
  JSON.parse(text);
32008
32040
  } catch {
32009
32041
  return [];
@@ -36564,7 +36596,7 @@ var init_junit = __esm(() => {
36564
36596
  import { realpathSync as realpathSync2 } from "node:fs";
36565
36597
  import { readFile, realpath } from "node:fs/promises";
36566
36598
  import { createConnection as createConnection3 } from "node:net";
36567
- import { dirname as dirname11, resolve as resolve4 } from "node:path";
36599
+ import { dirname as dirname12, resolve as resolve4 } from "node:path";
36568
36600
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
36569
36601
  function diagnosticsKey(uri) {
36570
36602
  let decoded;
@@ -36692,7 +36724,7 @@ class GodotLSPClient {
36692
36724
  if (this.initialized) {
36693
36725
  return;
36694
36726
  }
36695
- const rootPath = this.rootPath ?? dirname11(resolve4(filePath));
36727
+ const rootPath = this.rootPath ?? dirname12(resolve4(filePath));
36696
36728
  await this.initialize(rootPath);
36697
36729
  }
36698
36730
  async sendRequest(method, params) {
@@ -37137,13 +37169,13 @@ var init_process_time = __esm(() => {
37137
37169
  });
37138
37170
 
37139
37171
  // src/project-scan.ts
37140
- import { existsSync as existsSync13, readdirSync as readdirSync5, readFileSync as readFileSync15 } from "node:fs";
37141
- import { join as join17 } from "node:path";
37172
+ import { existsSync as existsSync13, readdirSync as readdirSync5, readFileSync as readFileSync16 } from "node:fs";
37173
+ import { join as join18 } from "node:path";
37142
37174
  function skipped(name) {
37143
37175
  return name.startsWith(".") || SKIPPED.has(name);
37144
37176
  }
37145
37177
  function steppedOver(directory) {
37146
- return existsSync13(join17(directory, ".gdignore"));
37178
+ return existsSync13(join18(directory, ".gdignore"));
37147
37179
  }
37148
37180
  function projectStructure(projectPath) {
37149
37181
  const structure = { scenes: 0, scripts: 0, assets: 0, other: 0 };
@@ -37156,7 +37188,7 @@ function projectStructure(projectPath) {
37156
37188
  continue;
37157
37189
  }
37158
37190
  if (entry.isDirectory()) {
37159
- visit(join17(directory, entry.name));
37191
+ visit(join18(directory, entry.name));
37160
37192
  } else if (entry.isFile()) {
37161
37193
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
37162
37194
  if (extension === "tscn") {
@@ -37186,14 +37218,14 @@ function scriptsWithoutUid(projectPath) {
37186
37218
  }
37187
37219
  const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
37188
37220
  if (entry.isDirectory()) {
37189
- visit(join17(directory, entry.name), spelled);
37221
+ visit(join18(directory, entry.name), spelled);
37190
37222
  continue;
37191
37223
  }
37192
37224
  if (!entry.isFile()) {
37193
37225
  continue;
37194
37226
  }
37195
37227
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
37196
- if (CARRIES_A_UID.has(extension) && !existsSync13(join17(directory, `${entry.name}.uid`))) {
37228
+ if (CARRIES_A_UID.has(extension) && !existsSync13(join18(directory, `${entry.name}.uid`))) {
37197
37229
  found.push(spelled);
37198
37230
  }
37199
37231
  }
@@ -37228,7 +37260,7 @@ function searchProject(projectPath, options) {
37228
37260
  if (skipped(entry.name)) {
37229
37261
  continue;
37230
37262
  }
37231
- const entryPath = join17(directory, entry.name);
37263
+ const entryPath = join18(directory, entry.name);
37232
37264
  if (entry.isDirectory()) {
37233
37265
  visit(entryPath);
37234
37266
  continue;
@@ -37239,7 +37271,7 @@ function searchProject(projectPath, options) {
37239
37271
  }
37240
37272
  result.summary.files_searched += 1;
37241
37273
  const matches = [];
37242
- for (const [index, line] of readFileSync15(entryPath, "utf8").split(`
37274
+ for (const [index, line] of readFileSync16(entryPath, "utf8").split(`
37243
37275
  `).entries()) {
37244
37276
  if (full()) {
37245
37277
  break;
@@ -37268,16 +37300,16 @@ var init_project_scan = __esm(() => {
37268
37300
  });
37269
37301
 
37270
37302
  // src/restart-note.ts
37271
- import { mkdirSync as mkdirSync9, readFileSync as readFileSync16, rmSync as rmSync8, writeFileSync as writeFileSync11 } from "node:fs";
37272
- import { dirname as dirname12, join as join18 } from "node:path";
37303
+ import { mkdirSync as mkdirSync10, readFileSync as readFileSync17, rmSync as rmSync8, writeFileSync as writeFileSync12 } from "node:fs";
37304
+ import { dirname as dirname13, join as join19 } from "node:path";
37273
37305
  function restartNotePath(projectPath) {
37274
- return join18(projectPath, ".godot", "gdharness-restart.json");
37306
+ return join19(projectPath, ".godot", "gdharness-restart.json");
37275
37307
  }
37276
37308
  function noteRestartBegun(note) {
37277
37309
  const path = restartNotePath(note.projectPath);
37278
37310
  try {
37279
- mkdirSync9(dirname12(path), { recursive: true });
37280
- writeFileSync11(path, `${JSON.stringify(note, null, 2)}
37311
+ mkdirSync10(dirname13(path), { recursive: true });
37312
+ writeFileSync12(path, `${JSON.stringify(note, null, 2)}
37281
37313
  `, "utf8");
37282
37314
  } catch (error) {
37283
37315
  console.error(`[SERVER] Could not record the restart at ${path}: ${errorMessage(error)}`);
@@ -37285,7 +37317,7 @@ function noteRestartBegun(note) {
37285
37317
  }
37286
37318
  function restartOwed(projectPath) {
37287
37319
  try {
37288
- const parsed = JSON.parse(readFileSync16(restartNotePath(projectPath), "utf8"));
37320
+ const parsed = JSON.parse(readFileSync17(restartNotePath(projectPath), "utf8"));
37289
37321
  if (typeof parsed !== "object" || parsed === null) {
37290
37322
  return null;
37291
37323
  }
@@ -37311,36 +37343,36 @@ var init_restart_note = __esm(() => {
37311
37343
 
37312
37344
  // src/run-record.ts
37313
37345
  import { execFileSync as execFileSync2 } from "node:child_process";
37314
- import { mkdirSync as mkdirSync10, openSync, readdirSync as readdirSync6, readFileSync as readFileSync17, rmSync as rmSync9, statSync as statSync4, writeFileSync as writeFileSync12 } from "node:fs";
37315
- import { basename as basename3, join as join19 } from "node:path";
37346
+ import { mkdirSync as mkdirSync11, openSync, readdirSync as readdirSync6, readFileSync as readFileSync18, rmSync as rmSync9, statSync as statSync4, writeFileSync as writeFileSync13 } from "node:fs";
37347
+ import { basename as basename3, join as join20 } from "node:path";
37316
37348
  function runsDirectory() {
37317
- return join19(runtimeDirectory(), "runs");
37349
+ return join20(runtimeDirectory(), "runs");
37318
37350
  }
37319
37351
  function recordPath() {
37320
- return join19(runsDirectory(), "run.json");
37352
+ return join20(runsDirectory(), "run.json");
37321
37353
  }
37322
37354
  function recordPaths() {
37323
- return runtimeDirectories().map((directory) => join19(directory, "runs", "run.json"));
37355
+ return runtimeDirectories().map((directory) => join20(directory, "runs", "run.json"));
37324
37356
  }
37325
37357
  function openTranscript(startedAt) {
37326
- mkdirSync10(runsDirectory(), { recursive: true });
37327
- const path = join19(runsDirectory(), `run-${startedAt}.log`);
37358
+ mkdirSync11(runsDirectory(), { recursive: true });
37359
+ const path = join20(runsDirectory(), `run-${startedAt}.log`);
37328
37360
  return { path, fd: openSync(path, "a") };
37329
37361
  }
37330
37362
  function writeRunRecord(record) {
37331
- mkdirSync10(runsDirectory(), { recursive: true });
37332
- writeFileSync12(recordPath(), JSON.stringify(record, null, 2), "utf8");
37363
+ mkdirSync11(runsDirectory(), { recursive: true });
37364
+ writeFileSync13(recordPath(), JSON.stringify(record, null, 2), "utf8");
37333
37365
  }
37334
37366
  function editorNotePath() {
37335
- return join19(runsDirectory(), "editor-run.json");
37367
+ return join20(runsDirectory(), "editor-run.json");
37336
37368
  }
37337
37369
  function writeEditorRunNote(note) {
37338
- mkdirSync10(runsDirectory(), { recursive: true });
37339
- writeFileSync12(editorNotePath(), JSON.stringify(note, null, 2), "utf8");
37370
+ mkdirSync11(runsDirectory(), { recursive: true });
37371
+ writeFileSync13(editorNotePath(), JSON.stringify(note, null, 2), "utf8");
37340
37372
  }
37341
37373
  function readEditorRunNote() {
37342
37374
  for (const directory of runtimeDirectories()) {
37343
- const note = editorNoteAt(join19(directory, "runs", "editor-run.json"));
37375
+ const note = editorNoteAt(join20(directory, "runs", "editor-run.json"));
37344
37376
  if (note !== null) {
37345
37377
  return note;
37346
37378
  }
@@ -37350,7 +37382,7 @@ function readEditorRunNote() {
37350
37382
  function editorNoteAt(path) {
37351
37383
  let parsed;
37352
37384
  try {
37353
- parsed = JSON.parse(readFileSync17(path, "utf8"));
37385
+ parsed = JSON.parse(readFileSync18(path, "utf8"));
37354
37386
  } catch {
37355
37387
  return null;
37356
37388
  }
@@ -37373,7 +37405,7 @@ function recordRunEnded(pid, exitCode) {
37373
37405
  return;
37374
37406
  }
37375
37407
  try {
37376
- writeFileSync12(path, JSON.stringify({ ...record, exitCode }, null, 2), "utf8");
37408
+ writeFileSync13(path, JSON.stringify({ ...record, exitCode }, null, 2), "utf8");
37377
37409
  } catch {}
37378
37410
  }
37379
37411
  function readRunRecord() {
@@ -37388,7 +37420,7 @@ function readRunRecord() {
37388
37420
  function recordAt(path) {
37389
37421
  let parsed;
37390
37422
  try {
37391
- parsed = JSON.parse(readFileSync17(path, "utf8"));
37423
+ parsed = JSON.parse(readFileSync18(path, "utf8"));
37392
37424
  } catch {
37393
37425
  return null;
37394
37426
  }
@@ -37433,7 +37465,7 @@ function runningAs(pid) {
37433
37465
  const began = startedAt(pid, null);
37434
37466
  const when = began === null ? {} : { startedAt: began };
37435
37467
  if (process.platform === "linux") {
37436
- const raw = readFileSync17(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
37468
+ const raw = readFileSync18(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
37437
37469
  return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
37438
37470
  }
37439
37471
  const args = execFileSync2("ps", ["-p", String(pid), "-o", "args="], {
@@ -37584,7 +37616,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
37584
37616
  if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
37585
37617
  continue;
37586
37618
  }
37587
- const path = join19(directory, entry);
37619
+ const path = join20(directory, entry);
37588
37620
  if (path === keep || ageMs(path, now) < keepMs) {
37589
37621
  continue;
37590
37622
  }
@@ -37607,16 +37639,16 @@ import {
37607
37639
  closeSync,
37608
37640
  existsSync as existsSync14,
37609
37641
  fstatSync,
37610
- mkdirSync as mkdirSync11,
37642
+ mkdirSync as mkdirSync12,
37611
37643
  mkdtempSync as mkdtempSync3,
37612
37644
  openSync as openSync2,
37613
37645
  readdirSync as readdirSync7,
37614
- readFileSync as readFileSync18,
37646
+ readFileSync as readFileSync19,
37615
37647
  readSync,
37616
37648
  statSync as statSync5
37617
37649
  } from "node:fs";
37618
37650
  import { tmpdir as tmpdir5 } from "node:os";
37619
- import { basename as basename4, dirname as dirname13, join as join20, normalize as normalize3, relative as relative3 } from "node:path";
37651
+ import { basename as basename4, dirname as dirname14, join as join21, normalize as normalize3, relative as relative3 } from "node:path";
37620
37652
  import { setTimeout as delay3 } from "node:timers/promises";
37621
37653
  import { fileURLToPath as fileURLToPath4 } from "node:url";
37622
37654
  import { promisify as promisify7 } from "node:util";
@@ -37858,7 +37890,7 @@ function aboveTheRunner(entry) {
37858
37890
  }
37859
37891
  function elsewhereIn(projectPath, asked) {
37860
37892
  const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
37861
- if (wanted === "" || existsSync14(join20(projectPath, wanted))) {
37893
+ if (wanted === "" || existsSync14(join21(projectPath, wanted))) {
37862
37894
  return "";
37863
37895
  }
37864
37896
  const root = (wanted.split("/")[0] ?? "").toLowerCase();
@@ -37872,7 +37904,7 @@ function elsewhereIn(projectPath, asked) {
37872
37904
  return near.length === 0 ? "" : ` The project does have ${there}.`;
37873
37905
  }
37874
37906
  function hasMainScene(projectFile) {
37875
- const scene = parseProjectGodot(readFileSync18(projectFile, "utf8"))["application"]?.["run/main_scene"];
37907
+ const scene = parseProjectGodot(readFileSync19(projectFile, "utf8"))["application"]?.["run/main_scene"];
37876
37908
  return typeof scene === "string" && scene !== "";
37877
37909
  }
37878
37910
  function runtimeAddonAnnouncesEditors(projectPath) {
@@ -37880,13 +37912,13 @@ function runtimeAddonAnnouncesEditors(projectPath) {
37880
37912
  return false;
37881
37913
  }
37882
37914
  try {
37883
- return readFileSync18(join20(projectPath, RUNTIME_AUTOLOAD2.path), "utf8").includes('"editor_pid"');
37915
+ return readFileSync19(join21(projectPath, RUNTIME_AUTOLOAD2.path), "utf8").includes('"editor_pid"');
37884
37916
  } catch {
37885
37917
  return false;
37886
37918
  }
37887
37919
  }
37888
37920
  function editorPlaysHeadless(projectFile) {
37889
- const runArgs = parseProjectGodot(readFileSync18(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
37921
+ const runArgs = parseProjectGodot(readFileSync19(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
37890
37922
  return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
37891
37923
  }
37892
37924
  function camelCased(params) {
@@ -37902,7 +37934,7 @@ function camelCased(params) {
37902
37934
  class GodotServer {
37903
37935
  mcp;
37904
37936
  locator = new GodotLocator2;
37905
- operationsScript = join20(__dirname2, "godot", "operations", "godot_operations.gd");
37937
+ operationsScript = join21(__dirname2, "godot", "operations", "godot_operations.gd");
37906
37938
  godotBridge;
37907
37939
  tools = buildToolDefinitions();
37908
37940
  activeProcess = null;
@@ -38333,8 +38365,15 @@ class GodotServer {
38333
38365
  const contained = this.containProjectFiles(asks);
38334
38366
  return contained.ok ? await this.handleViaBridge(editorSide, contained.value) : contained.response;
38335
38367
  }
38336
- const answered = await this.headless(headless, answerable);
38337
- return headless === "refresh_class_cache" && this.godotBridge.isConnected() ? await this.alsoSayWhatTheEditorCannotSee(answered, args) : answered;
38368
+ if (headless !== "refresh_class_cache") {
38369
+ return await this.headless(headless, answerable);
38370
+ }
38371
+ const project = this.project(answerable);
38372
+ if (!project.ok) {
38373
+ return project.response;
38374
+ }
38375
+ const answered = this.answer(await this.rebuildClassCache(project.value.path));
38376
+ return this.godotBridge.isConnected() ? await this.alsoSayWhatTheEditorCannotSee(answered, args) : answered;
38338
38377
  }
38339
38378
  const { op: _op, ...arguments_ } = args;
38340
38379
  const bridge = async (command, extra = {}) => {
@@ -38560,7 +38599,7 @@ class GodotServer {
38560
38599
  if (path === undefined) {
38561
38600
  return { ok: false, response: this.createErrorResponse("projectPath is required.") };
38562
38601
  }
38563
- const file = join20(path, "project.godot");
38602
+ const file = join21(path, "project.godot");
38564
38603
  if (!existsSync14(file)) {
38565
38604
  return {
38566
38605
  ok: false,
@@ -38784,7 +38823,7 @@ class GodotServer {
38784
38823
  if (!engine.ok) {
38785
38824
  return engine.response;
38786
38825
  }
38787
- const application = parseProjectGodot(readFileSync18(project.value.file, "utf8"))["application"] ?? {};
38826
+ const application = parseProjectGodot(readFileSync19(project.value.file, "utf8"))["application"] ?? {};
38788
38827
  const name = application["config/name"];
38789
38828
  const mainScene = application["run/main_scene"];
38790
38829
  const info = {
@@ -38845,18 +38884,18 @@ class GodotServer {
38845
38884
  if (!engine.ok) {
38846
38885
  return engine.response;
38847
38886
  }
38848
- const folder = dirname13(output.absolutePath);
38887
+ const folder = dirname14(output.absolutePath);
38849
38888
  try {
38850
- mkdirSync11(folder, { recursive: true });
38889
+ mkdirSync12(folder, { recursive: true });
38851
38890
  } catch (error) {
38852
- return this.createErrorResponse(`Cannot create ${dirname13(output.relativePath)} to export into: ${errorMessage(error)}`);
38891
+ return this.createErrorResponse(`Cannot create ${dirname14(output.relativePath)} to export into: ${errorMessage(error)}`);
38853
38892
  }
38854
38893
  const debug = readBoolean(args, "debug") ?? false;
38855
- const exportLogs = mkdtempSync3(join20(tmpdir5(), "gdharness-export-"));
38894
+ const exportLogs = mkdtempSync3(join21(tmpdir5(), "gdharness-export-"));
38856
38895
  const exportArgs = [
38857
38896
  "--headless",
38858
38897
  "--log-file",
38859
- join20(exportLogs, "engine.log"),
38898
+ join21(exportLogs, "engine.log"),
38860
38899
  "--path",
38861
38900
  project.value.path,
38862
38901
  debug ? "--export-debug" : "--export-release",
@@ -38914,7 +38953,7 @@ class GodotServer {
38914
38953
  return contained.response;
38915
38954
  }
38916
38955
  const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
38917
- if (!existsSync14(join20(project.value.path, runner))) {
38956
+ if (!existsSync14(join21(project.value.path, runner))) {
38918
38957
  return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
38919
38958
  "Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
38920
38959
  ]);
@@ -38923,7 +38962,7 @@ class GodotServer {
38923
38962
  if (!engine.ok) {
38924
38963
  return engine.response;
38925
38964
  }
38926
- const classes = await this.operation("refresh_class_cache", {}, project.value.path);
38965
+ const classes = await this.rebuildClassCache(project.value.path);
38927
38966
  if (!classes.ok) {
38928
38967
  return this.answer(classes);
38929
38968
  }
@@ -38949,7 +38988,7 @@ class GodotServer {
38949
38988
  ];
38950
38989
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
38951
38990
  this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
38952
- const userData = mkdtempSync3(join20(tmpdir5(), "gdharness-tests-"));
38991
+ const userData = mkdtempSync3(join21(tmpdir5(), "gdharness-tests-"));
38953
38992
  const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
38954
38993
  const hung = await new Promise((resolve) => {
38955
38994
  const timer = setTimeout(() => {
@@ -38965,14 +39004,14 @@ class GodotServer {
38965
39004
  resolve(false);
38966
39005
  });
38967
39006
  });
38968
- const reportsDir = join20(project.value.path, ".godot", ...ours.split("/"));
39007
+ const reportsDir = join21(project.value.path, ".godot", ...ours.split("/"));
38969
39008
  let report = null;
38970
39009
  let reportProblem = null;
38971
39010
  try {
38972
39011
  const written = existsSync14(reportsDir) ? readdirSync7(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
38973
39012
  const newest = written.at(-1);
38974
39013
  if (newest !== undefined) {
38975
- report = parseJUnit(readFileSync18(join20(reportsDir, newest, "results.xml"), "utf8"));
39014
+ report = parseJUnit(readFileSync19(join21(reportsDir, newest, "results.xml"), "utf8"));
38976
39015
  }
38977
39016
  } catch (error) {
38978
39017
  reportProblem = errorMessage(error);
@@ -39112,7 +39151,7 @@ class GodotServer {
39112
39151
  return null;
39113
39152
  }
39114
39153
  try {
39115
- return readFileSync18(contained.absolutePath, "utf8");
39154
+ return readFileSync19(contained.absolutePath, "utf8");
39116
39155
  } catch {
39117
39156
  return null;
39118
39157
  }
@@ -39140,7 +39179,8 @@ class GodotServer {
39140
39179
  }
39141
39180
  const stale = unloaded.filter((type) => type.inTheClassCache);
39142
39181
  if (stale.length > 0) {
39143
- notes.push(`${stale.map((type) => type.type).join(", ")} ${stale.length === 1 ? "is" : "are"} in the class cache and still unresolved here, which is the editor's loaded list being behind rather than anything on disk. A game launched now reads the cache and resolves ${stale.length === 1 ? "it" : "them"}, so the run and the diagnostics disagree. editor_rescan clears it, measured against a real editor with waiting the same length ruled out; editor_launch restart also does, and costs more.`);
39182
+ const remedy = this.editorWritesAShortList(projectPath) ? "editor_rescan will not clear it here: this editor wrote the class cache from a list shorter than the file on its last scan and writes the same list on every scan, so editor_launch restart is what loads it." : "editor_rescan clears it, measured against a real editor with waiting the same length ruled out; editor_launch restart also does, and costs more.";
39183
+ notes.push(`${stale.map((type) => type.type).join(", ")} ${stale.length === 1 ? "is" : "are"} in the class cache and still unresolved here, which is the editor's loaded list being behind rather than anything on disk. A game launched now reads the cache and resolves ${stale.length === 1 ? "it" : "them"}, so the run and the diagnostics disagree. ${remedy}`);
39144
39184
  }
39145
39185
  return {
39146
39186
  ...contradicted.length === 0 ? {} : { contradictedByTheFile: contradicted },
@@ -39148,13 +39188,24 @@ class GodotServer {
39148
39188
  ...notes.length === 0 ? {} : { staleAnalysis: notes.join(" ") }
39149
39189
  };
39150
39190
  }
39191
+ editorWritesAShortList(projectPath) {
39192
+ const recorded = readClassNote(projectPath)?.shortListEditorPid;
39193
+ return recorded !== undefined && recorded === (this.godotBridge.getStatus().editorPid ?? null);
39194
+ }
39195
+ noteAShortListEditor(projectPath) {
39196
+ const note = readClassNote(projectPath);
39197
+ writeClassNote(projectPath, {
39198
+ rebuiltWith: note?.rebuiltWith ?? [],
39199
+ shortListEditorPid: this.godotBridge.getStatus().editorPid ?? null
39200
+ });
39201
+ }
39151
39202
  sourceOfClass(projectPath, resourcePath) {
39152
39203
  const contained = resolveWithinProject(projectPath, resourcePath);
39153
39204
  if (!contained.ok || !existsSync14(contained.absolutePath)) {
39154
39205
  return null;
39155
39206
  }
39156
39207
  try {
39157
- return readFileSync18(contained.absolutePath, "utf8");
39208
+ return readFileSync19(contained.absolutePath, "utf8");
39158
39209
  } catch {
39159
39210
  return null;
39160
39211
  }
@@ -39383,7 +39434,7 @@ class GodotServer {
39383
39434
  return null;
39384
39435
  }
39385
39436
  try {
39386
- return settingKeys(readFileSync18(join20(projectPath, "project.godot"), "utf8"));
39437
+ return settingKeys(readFileSync19(join21(projectPath, "project.godot"), "utf8"));
39387
39438
  } catch {
39388
39439
  return null;
39389
39440
  }
@@ -39625,7 +39676,7 @@ class GodotServer {
39625
39676
  }
39626
39677
  async runtimeUp(projectPath, before, budget, withArgs) {
39627
39678
  const budgetMs = budget.ms;
39628
- if (!existsSync14(join20(projectPath, RUNTIME_AUTOLOAD2.path))) {
39679
+ if (!existsSync14(join21(projectPath, RUNTIME_AUTOLOAD2.path))) {
39629
39680
  return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
39630
39681
  }
39631
39682
  let asking = null;
@@ -40497,6 +40548,9 @@ class GodotServer {
40497
40548
  }
40498
40549
  const after = projectPath === "" || busy ? null : cachedClasses(projectPath);
40499
40550
  const lost = before === null || after === null ? [] : [...before.keys()].filter((name) => !after.has(name));
40551
+ if (lost.length > 0) {
40552
+ this.noteAShortListEditor(projectPath);
40553
+ }
40500
40554
  const atMissingPaths = after === null ? [] : cachedAtMissingPaths(after, projectPath);
40501
40555
  const restored = lost.length > 0 || atMissingPaths.length > 0 ? await this.rebuildCacheAfterLoss(projectPath, lost) : [];
40502
40556
  const rebuilt = atMissingPaths.length > 0 ? cachedClasses(projectPath) : null;
@@ -40552,7 +40606,7 @@ class GodotServer {
40552
40606
  if (projectPath === "") {
40553
40607
  return [];
40554
40608
  }
40555
- const rebuilt = await this.operation("refresh_class_cache", {}, projectPath);
40609
+ const rebuilt = await this.rebuildClassCache(projectPath);
40556
40610
  if (!rebuilt.ok) {
40557
40611
  this.logDebug(`Could not rebuild the class cache after a scan: ${rebuilt.message}`);
40558
40612
  return [];
@@ -40560,6 +40614,36 @@ class GodotServer {
40560
40614
  const now = cachedClasses(projectPath);
40561
40615
  return now === null ? [] : lost.filter((name) => now.has(name));
40562
40616
  }
40617
+ async rebuildClassCache(projectPath) {
40618
+ const before = cachedClasses(projectPath);
40619
+ const noted = readClassNote(projectPath);
40620
+ const rebuilt = await this.operation("refresh_class_cache", {}, projectPath);
40621
+ if (!rebuilt.ok) {
40622
+ return rebuilt;
40623
+ }
40624
+ const after = cachedClasses(projectPath);
40625
+ if (after === null) {
40626
+ return rebuilt;
40627
+ }
40628
+ const lost = noted === null || before === null ? [] : noted.rebuiltWith.filter((name) => !before.has(name) && after.has(name)).sort();
40629
+ const seenLosing = lost.length > 0 && this.godotBridge.isConnected();
40630
+ writeClassNote(projectPath, {
40631
+ rebuiltWith: [...after.keys()].sort(),
40632
+ ...seenLosing ? { shortListEditorPid: this.godotBridge.getStatus().editorPid ?? null } : noted?.shortListEditorPid === undefined ? {} : { shortListEditorPid: noted.shortListEditorPid }
40633
+ });
40634
+ if (!seenLosing) {
40635
+ return rebuilt;
40636
+ }
40637
+ const them = lost.length === 1 ? "it" : "them";
40638
+ return {
40639
+ ...rebuilt,
40640
+ payload: {
40641
+ ...rebuilt.payload,
40642
+ lostSinceLastRebuild: lost,
40643
+ note: `The cache held ${lost.join(", ")} after the last rebuild and not when this one began, so the editor holding this project wrote it without ${them} in between: it holds a list shorter than the files and writes that list on every save and scan, and each rebuild puts ${them} back. editor_launch restart is what ends it.`
40644
+ }
40645
+ };
40646
+ }
40563
40647
  async alsoSayWhatTheEditorCannotSee(answered, args) {
40564
40648
  const first = answered.content[0];
40565
40649
  if (answered.isError || first?.type !== "text" || typeof first.text !== "string") {
@@ -40570,15 +40654,18 @@ class GodotServer {
40570
40654
  if (checked.unseen.length === 0 && stillHeld.length === 0 && checked.unchecked === undefined) {
40571
40655
  return answered;
40572
40656
  }
40573
- const notes = [];
40657
+ const payload = asParams(JSON.parse(first.text));
40658
+ const said = readString(payload, "note");
40659
+ const notes = said === undefined ? [] : [said];
40574
40660
  if (checked.unseen.length > 0) {
40575
- notes.push("The cache on disk is right now, and the editor holding this project is still not resolving these: rewriting the file does not reach the list it already loaded. editor_rescan does, on its own and with no change to the declaring script; editor_launch restart also does, and costs more.");
40661
+ const projectPath = typeof args["projectPath"] === "string" ? args["projectPath"] : "";
40662
+ notes.push(projectPath !== "" && this.editorWritesAShortList(projectPath) ? "The cache on disk is right now, and the editor holding this project is still not resolving these: rewriting the file does not reach the list it already loaded. editor_rescan will not reach it either on this editor, whose last scan wrote the cache from a list shorter than the file and would again; editor_launch restart is what loads them." : "The cache on disk is right now, and the editor holding this project is still not resolving these: rewriting the file does not reach the list it already loaded. editor_rescan does, on its own and with no change to the declaring script; editor_launch restart also does, and costs more.");
40576
40663
  }
40577
40664
  if (stillHeld.length > 0) {
40578
40665
  notes.push(`The editor is still holding ${stillHeld.join(", ")}, which this project no longer declares, and it will write that list back over the cache this call just corrected. The entry returns pointing at a script that is gone, and the next engine to walk get_global_class_list() dies on "File not found". A rescan does not clear this one: it picks up a class that has appeared and does not drop one that has gone. editor_launch restart does.`);
40579
40666
  }
40580
40667
  return this.jsonTextResponse({
40581
- ...asParams(JSON.parse(first.text)),
40668
+ ...payload,
40582
40669
  unseenByEditor: checked.unseen.length > 0 ? checked.unseen : undefined,
40583
40670
  stillHeldByEditor: stillHeld.length > 0 ? stillHeld : undefined,
40584
40671
  classesUnchecked: checked.unchecked,
@@ -40703,8 +40790,8 @@ class GodotServer {
40703
40790
  ]);
40704
40791
  }
40705
40792
  const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
40706
- const screenshotDir = expectsScreenshot ? mkdtempSync3(join20(tmpdir5(), "gdharness-runtime-screenshot-")) : null;
40707
- const screenshotPath = screenshotDir ? join20(screenshotDir, "capture.png") : null;
40793
+ const screenshotDir = expectsScreenshot ? mkdtempSync3(join21(tmpdir5(), "gdharness-runtime-screenshot-")) : null;
40794
+ const screenshotPath = screenshotDir ? join21(screenshotDir, "capture.png") : null;
40708
40795
  try {
40709
40796
  const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs);
40710
40797
  if (!reply.ok) {
@@ -40728,7 +40815,7 @@ class GodotServer {
40728
40815
  type: "text",
40729
40816
  text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}`
40730
40817
  },
40731
- { type: "image", data: readFileSync18(screenshotPath).toString("base64"), mimeType: "image/png" }
40818
+ { type: "image", data: readFileSync19(screenshotPath).toString("base64"), mimeType: "image/png" }
40732
40819
  ]
40733
40820
  };
40734
40821
  } finally {
@@ -40804,6 +40891,7 @@ var init_server3 = __esm(() => {
40804
40891
  init_breakpoint_note();
40805
40892
  init_bridge_announce();
40806
40893
  init_class_cache();
40894
+ init_class_note();
40807
40895
  init_config_pin();
40808
40896
  init_dap_client();
40809
40897
  init_errors();
@@ -40830,7 +40918,7 @@ var init_server3 = __esm(() => {
40830
40918
  init_tool_definitions();
40831
40919
  init_update_check();
40832
40920
  run7 = promisify7(execFile7);
40833
- __dirname2 = dirname13(fileURLToPath4(import.meta.url));
40921
+ __dirname2 = dirname14(fileURLToPath4(import.meta.url));
40834
40922
  CONSOLE_WRAPPER = /_console(\.exe)?$/i;
40835
40923
  LONGEST_BOOT_NOTED_MS = 5 * 60000;
40836
40924
  VALUE_NAMES = {
@@ -40871,7 +40959,7 @@ var init_server3 = __esm(() => {
40871
40959
 
40872
40960
  // src/cli.ts
40873
40961
  import { existsSync as existsSync15 } from "fs";
40874
- import { join as join21, resolve as resolve5 } from "path";
40962
+ import { join as join22, resolve as resolve5 } from "path";
40875
40963
 
40876
40964
  // src/errors.ts
40877
40965
  class Refusal extends Error {
@@ -42576,7 +42664,7 @@ class UsageError extends Refusal {
42576
42664
  function projectArgument(at) {
42577
42665
  const named = args.slice(at).find((value) => !value.startsWith("--"));
42578
42666
  const projectPath = resolve5(named ?? ".");
42579
- if (!existsSync15(join21(projectPath, "project.godot"))) {
42667
+ if (!existsSync15(join22(projectPath, "project.godot"))) {
42580
42668
  throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
42581
42669
  }
42582
42670
  return projectPath;
@@ -42615,7 +42703,7 @@ async function registerRuntime(godot, projectPath) {
42615
42703
  return;
42616
42704
  }
42617
42705
  console.log(`${RUNTIME_AUTOLOAD.name} autoload left at ${named}, which this project registers itself`);
42618
- if (!existsSync15(join21(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
42706
+ if (!existsSync15(join22(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
42619
42707
  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}`);
42620
42708
  }
42621
42709
  }