gdharness 1.0.12 → 1.0.13

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
@@ -7531,11 +7531,20 @@ var init_update_check = __esm(() => {
7531
7531
 
7532
7532
  // src/server-version.ts
7533
7533
  import { readFileSync as readFileSync3 } from "node:fs";
7534
- function addonMismatch(addonVersion, serverVersion) {
7535
- if (addonVersion === serverVersion) {
7534
+ function editorIsStale(addonVersion, serverVersion, addonDigest, shippedDigest) {
7535
+ if (addonDigest !== undefined && addonDigest !== "" && shippedDigest !== undefined) {
7536
+ return addonDigest !== shippedDigest;
7537
+ }
7538
+ return addonVersion !== serverVersion;
7539
+ }
7540
+ function addonMismatch(addonVersion, serverVersion, addonDigest, shippedDigest) {
7541
+ if (!editorIsStale(addonVersion, serverVersion, addonDigest, shippedDigest)) {
7536
7542
  return;
7537
7543
  }
7538
7544
  const reported = addonVersion ?? "";
7545
+ if (reported === serverVersion) {
7546
+ return `The editor is running a different build of the ${reported} addon from the one this server ships. Restart it with editor_launch restart to pick this one up.`;
7547
+ }
7539
7548
  const editor = reported === "" ? UNVERSIONED : `the ${reported} addon`;
7540
7549
  const both = `The editor is running ${editor} while this server ships ${serverVersion}.`;
7541
7550
  if (reported !== "" && isNewer(reported, serverVersion)) {
@@ -7543,8 +7552,8 @@ function addonMismatch(addonVersion, serverVersion) {
7543
7552
  }
7544
7553
  return `${both} Restart it with editor_launch restart to pick the new one up.`;
7545
7554
  }
7546
- function markIfStale(answer, addonVersion, serverVersion) {
7547
- const staleNote = addonMismatch(addonVersion, serverVersion);
7555
+ function markIfStale(answer, addonVersion, serverVersion, addonDigest, shippedDigest) {
7556
+ const staleNote = addonMismatch(addonVersion, serverVersion, addonDigest, shippedDigest);
7548
7557
  if (staleNote === undefined || typeof answer !== "object" || answer === null || Array.isArray(answer)) {
7549
7558
  return answer;
7550
7559
  }
@@ -7690,6 +7699,19 @@ function cacheWrittenAt(projectPath) {
7690
7699
  return null;
7691
7700
  }
7692
7701
  }
7702
+ function declaredSince(projectPath, paths, names, since) {
7703
+ return names.filter((name) => {
7704
+ const path = paths.get(name);
7705
+ if (path === undefined || !path.startsWith("res://")) {
7706
+ return false;
7707
+ }
7708
+ try {
7709
+ return statSync2(join6(projectPath, path.slice("res://".length))).mtimeMs > since;
7710
+ } catch {
7711
+ return false;
7712
+ }
7713
+ });
7714
+ }
7693
7715
  function cachedAtMissingPaths(cached, projectPath) {
7694
7716
  return [...cached].filter(([, path]) => path.startsWith("res://") && !existsSync6(join6(projectPath, path.slice("res://".length)))).map(([name]) => name).sort();
7695
7717
  }
@@ -15308,8 +15330,57 @@ function readParams(params, key) {
15308
15330
  }
15309
15331
 
15310
15332
  // src/setup.ts
15311
- import { cpSync as cpSync2, existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync8, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "node:fs";
15333
+ import { createHash as createHash3 } from "node:crypto";
15334
+ import {
15335
+ cpSync as cpSync2,
15336
+ existsSync as existsSync8,
15337
+ mkdirSync as mkdirSync4,
15338
+ readdirSync as readdirSync4,
15339
+ readFileSync as readFileSync8,
15340
+ rmSync as rmSync4,
15341
+ statSync as statSync4,
15342
+ writeFileSync as writeFileSync6
15343
+ } from "node:fs";
15312
15344
  import { dirname as dirname4, join as join9 } from "node:path";
15345
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
15346
+ function editorAddonDigest2(from = shippedAddonsDirectory2()) {
15347
+ const hash = createHash3("sha256");
15348
+ const walk = (directory, relative) => {
15349
+ for (const name of readdirSync4(directory).sort()) {
15350
+ if (name === VERSION_MARKER2 || name === DIGEST_MARKER2) {
15351
+ continue;
15352
+ }
15353
+ const path = join9(directory, name);
15354
+ const inside = `${relative}/${name}`;
15355
+ if (statSync4(path).isDirectory()) {
15356
+ walk(path, inside);
15357
+ } else {
15358
+ hash.update(`${inside}
15359
+ `);
15360
+ hash.update(readFileSync8(path));
15361
+ hash.update(`
15362
+ `);
15363
+ }
15364
+ }
15365
+ };
15366
+ for (const name of EDITOR_PLUGINS2) {
15367
+ walk(join9(from, name), name);
15368
+ }
15369
+ return hash.digest("hex");
15370
+ }
15371
+ function shippedEditorDigest() {
15372
+ if (shippedDigest === undefined) {
15373
+ try {
15374
+ shippedDigest = editorAddonDigest2();
15375
+ } catch {
15376
+ shippedDigest = null;
15377
+ }
15378
+ }
15379
+ return shippedDigest ?? undefined;
15380
+ }
15381
+ function shippedAddonsDirectory2() {
15382
+ return join9(dirname4(fileURLToPath2(import.meta.url)), "godot", "addons");
15383
+ }
15313
15384
  function installedAddonVersion(projectPath) {
15314
15385
  const marker = join9(projectPath, "addons", ADDONS2[0], VERSION_MARKER2);
15315
15386
  if (!existsSync8(marker)) {
@@ -15318,13 +15389,14 @@ function installedAddonVersion(projectPath) {
15318
15389
  const said = readFileSync8(marker, "utf8").trim();
15319
15390
  return said === "" ? null : said;
15320
15391
  }
15321
- var ADDONS2, RUNTIME_AUTOLOAD2, SCRIPT_RUNS_SETTING = "gdharness/runtime/serve_script_runs", BIND_ADDRESS_SETTING = "gdharness/runtime/bind_address", PORT_SETTING = "gdharness/runtime/port", VERSION_MARKER2 = ".gdharness-version";
15392
+ var ADDONS2, EDITOR_PLUGINS2, RUNTIME_AUTOLOAD2, SCRIPT_RUNS_SETTING = "gdharness/runtime/serve_script_runs", BIND_ADDRESS_SETTING = "gdharness/runtime/bind_address", PORT_SETTING = "gdharness/runtime/port", VERSION_MARKER2 = ".gdharness-version", DIGEST_MARKER2 = ".gdharness-digest", shippedDigest;
15322
15393
  var init_setup = __esm(() => {
15323
15394
  init_class_cache();
15324
15395
  init_headless();
15325
15396
  init_resources();
15326
15397
  init_server_version();
15327
15398
  ADDONS2 = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
15399
+ EDITOR_PLUGINS2 = ["gdharness_editor", "auto_reload"];
15328
15400
  RUNTIME_AUTOLOAD2 = {
15329
15401
  name: "GdharnessRuntime",
15330
15402
  path: "addons/gdharness_runtime/runtime_autoload.gd"
@@ -31490,7 +31562,7 @@ var init_process_children = __esm(() => {
31490
31562
  });
31491
31563
 
31492
31564
  // src/runtime-client.ts
31493
- import { existsSync as existsSync10, readdirSync as readdirSync4, readFileSync as readFileSync10, statSync as statSync3, unlinkSync } from "node:fs";
31565
+ import { existsSync as existsSync10, readdirSync as readdirSync6, readFileSync as readFileSync10, statSync as statSync5, unlinkSync } from "node:fs";
31494
31566
  import { createConnection } from "node:net";
31495
31567
  import { tmpdir as tmpdir4 } from "node:os";
31496
31568
  import { dirname as dirname7, join as join12, resolve as resolve3 } from "node:path";
@@ -31627,7 +31699,7 @@ function announcedIn(directory) {
31627
31699
  const found = [];
31628
31700
  const candidates = [];
31629
31701
  const entries = [];
31630
- for (const entry of readdirSync4(directory)) {
31702
+ for (const entry of readdirSync6(directory)) {
31631
31703
  const report = ERROR_REPORT_PATTERN.exec(entry);
31632
31704
  if (report) {
31633
31705
  sweepErrorReport(join12(directory, entry), Number.parseInt(report[1] ?? "", 10));
@@ -31643,7 +31715,7 @@ function announcedIn(directory) {
31643
31715
  entries.push({ file, pid, alive });
31644
31716
  if (alive) {
31645
31717
  try {
31646
- candidates.push({ file, pid, writtenAt: statSync3(file).mtimeMs });
31718
+ candidates.push({ file, pid, writtenAt: statSync5(file).mtimeMs });
31647
31719
  } catch {}
31648
31720
  }
31649
31721
  }
@@ -31700,7 +31772,7 @@ function sweepErrorReport(path, pid, now = Date.now()) {
31700
31772
  return;
31701
31773
  }
31702
31774
  try {
31703
- if (now - statSync3(path).mtimeMs >= ERROR_REPORT_KEEP_MS) {
31775
+ if (now - statSync5(path).mtimeMs >= ERROR_REPORT_KEEP_MS) {
31704
31776
  unlinkSync(path);
31705
31777
  }
31706
31778
  } catch {}
@@ -32879,7 +32951,7 @@ import {
32879
32951
  readFileSync as readFileSync16,
32880
32952
  readSync,
32881
32953
  rmSync as rmSync8,
32882
- statSync as statSync4,
32954
+ statSync as statSync6,
32883
32955
  writeFileSync as writeFileSync12
32884
32956
  } from "node:fs";
32885
32957
  import { dirname as dirname12, join as join18 } from "node:path";
@@ -32942,7 +33014,7 @@ function editorConsole(projectPath, editorPid, openedByAServer) {
32942
33014
  return { log, path, ...of === undefined ? {} : { readBytes: text.length, of } };
32943
33015
  }
32944
33016
  function theFirstOf(path) {
32945
- const whole = statSync4(path).size;
33017
+ const whole = statSync6(path).size;
32946
33018
  if (whole <= MOST_CONSOLE_BYTES) {
32947
33019
  return { text: readFileSync16(path, "utf8") };
32948
33020
  }
@@ -36160,6 +36232,7 @@ var init_godot_bridge = __esm(() => {
36160
36232
  connectedAt: this.connectionInfo?.connectedAt,
36161
36233
  lastPongAt: this.connectionInfo?.lastPongAt,
36162
36234
  addonVersion: this.connectionInfo?.addonVersion,
36235
+ addonDigest: this.connectionInfo?.addonDigest,
36163
36236
  editorPid: this.connectionInfo?.editorPid,
36164
36237
  lspPort: this.connectionInfo?.lspPort,
36165
36238
  dapPort: this.connectionInfo?.dapPort,
@@ -36303,6 +36376,7 @@ var init_godot_bridge = __esm(() => {
36303
36376
  }
36304
36377
  this.connectionInfo.projectPath = message.project_path;
36305
36378
  this.connectionInfo.addonVersion = message.addon_version ?? "";
36379
+ this.connectionInfo.addonDigest = message.addon_digest;
36306
36380
  this.connectionInfo.editorPid = message.editor_pid;
36307
36381
  this.connectionInfo.lspPort = servedPort(message.lsp_port);
36308
36382
  this.connectionInfo.dapPort = servedPort(message.dap_port);
@@ -36556,7 +36630,7 @@ var init_godot_path = __esm(() => {
36556
36630
  });
36557
36631
 
36558
36632
  // src/issues.ts
36559
- import { createHash as createHash2 } from "node:crypto";
36633
+ import { createHash as createHash4 } from "node:crypto";
36560
36634
  import process7 from "node:process";
36561
36635
  function feedbackNotice() {
36562
36636
  return JSON.stringify({
@@ -36574,7 +36648,7 @@ function runtime2() {
36574
36648
  function defectSignature2(where, message) {
36575
36649
  const normalised = `${where}
36576
36650
  ${message}`.toLowerCase().replaceAll("\\", "/").replaceAll(/(?:[a-z]:)?(?:\/+[\w.-]+){2,}/gu, "<path>").replaceAll(/\d+/gu, "0");
36577
- return createHash2("sha256").update(normalised).digest("hex").slice(0, 8);
36651
+ return createHash4("sha256").update(normalised).digest("hex").slice(0, 8);
36578
36652
  }
36579
36653
  function facts2(where, message, godotVersion) {
36580
36654
  return [
@@ -36911,7 +36985,7 @@ import { realpathSync as realpathSync2 } from "node:fs";
36911
36985
  import { readFile, realpath } from "node:fs/promises";
36912
36986
  import { createConnection as createConnection3 } from "node:net";
36913
36987
  import { dirname as dirname13, resolve as resolve4 } from "node:path";
36914
- import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
36988
+ import { fileURLToPath as fileURLToPath4, pathToFileURL } from "node:url";
36915
36989
  function diagnosticsKey(uri) {
36916
36990
  let decoded;
36917
36991
  try {
@@ -36923,7 +36997,7 @@ function diagnosticsKey(uri) {
36923
36997
  return decoded;
36924
36998
  }
36925
36999
  try {
36926
- return realpathSync2.native(fileURLToPath3(decoded)).toLowerCase();
37000
+ return realpathSync2.native(fileURLToPath4(decoded)).toLowerCase();
36927
37001
  } catch {
36928
37002
  return decoded.toLowerCase();
36929
37003
  }
@@ -37483,7 +37557,7 @@ var init_process_time = __esm(() => {
37483
37557
  });
37484
37558
 
37485
37559
  // src/project-scan.ts
37486
- import { existsSync as existsSync14, readdirSync as readdirSync5, readFileSync as readFileSync17 } from "node:fs";
37560
+ import { existsSync as existsSync14, readdirSync as readdirSync7, readFileSync as readFileSync17 } from "node:fs";
37487
37561
  import { join as join19 } from "node:path";
37488
37562
  function skipped(name) {
37489
37563
  return name.startsWith(".") || SKIPPED.has(name);
@@ -37497,7 +37571,7 @@ function projectStructure(projectPath) {
37497
37571
  if (steppedOver(directory)) {
37498
37572
  return;
37499
37573
  }
37500
- for (const entry of readdirSync5(directory, { withFileTypes: true })) {
37574
+ for (const entry of readdirSync7(directory, { withFileTypes: true })) {
37501
37575
  if (skipped(entry.name)) {
37502
37576
  continue;
37503
37577
  }
@@ -37526,7 +37600,7 @@ function scriptsWithoutUid(projectPath) {
37526
37600
  if (steppedOver(directory)) {
37527
37601
  return;
37528
37602
  }
37529
- for (const entry of readdirSync5(directory, { withFileTypes: true })) {
37603
+ for (const entry of readdirSync7(directory, { withFileTypes: true })) {
37530
37604
  if (skipped(entry.name)) {
37531
37605
  continue;
37532
37606
  }
@@ -37567,7 +37641,7 @@ function searchProject(projectPath, options) {
37567
37641
  if (steppedOver(directory)) {
37568
37642
  return;
37569
37643
  }
37570
- for (const entry of readdirSync5(directory, { withFileTypes: true })) {
37644
+ for (const entry of readdirSync7(directory, { withFileTypes: true })) {
37571
37645
  if (full()) {
37572
37646
  return;
37573
37647
  }
@@ -37657,7 +37731,7 @@ var init_restart_note = __esm(() => {
37657
37731
 
37658
37732
  // src/run-record.ts
37659
37733
  import { execFileSync as execFileSync2 } from "node:child_process";
37660
- import { mkdirSync as mkdirSync12, openSync as openSync2, readdirSync as readdirSync6, readFileSync as readFileSync19, rmSync as rmSync10, statSync as statSync5, writeFileSync as writeFileSync14 } from "node:fs";
37734
+ import { mkdirSync as mkdirSync12, openSync as openSync2, readdirSync as readdirSync8, readFileSync as readFileSync19, rmSync as rmSync10, statSync as statSync7, writeFileSync as writeFileSync14 } from "node:fs";
37661
37735
  import { basename as basename3, join as join21 } from "node:path";
37662
37736
  function runsDirectory() {
37663
37737
  return join21(runtimeDirectory(), "runs");
@@ -37912,7 +37986,7 @@ function clearRunRecord(ours = () => true) {
37912
37986
  }
37913
37987
  function ageMs(path, now) {
37914
37988
  try {
37915
- return now - statSync5(path).mtimeMs;
37989
+ return now - statSync7(path).mtimeMs;
37916
37990
  } catch {
37917
37991
  return 0;
37918
37992
  }
@@ -37922,7 +37996,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
37922
37996
  const keep = readRunRecord()?.transcript;
37923
37997
  let entries;
37924
37998
  try {
37925
- entries = readdirSync6(directory);
37999
+ entries = readdirSync8(directory);
37926
38000
  } catch {
37927
38001
  return;
37928
38002
  }
@@ -37957,16 +38031,16 @@ import {
37957
38031
  mkdirSync as mkdirSync13,
37958
38032
  mkdtempSync as mkdtempSync3,
37959
38033
  openSync as openSync3,
37960
- readdirSync as readdirSync7,
38034
+ readdirSync as readdirSync9,
37961
38035
  readFileSync as readFileSync20,
37962
38036
  readSync as readSync2,
37963
38037
  rmdirSync,
37964
- statSync as statSync6
38038
+ statSync as statSync8
37965
38039
  } from "node:fs";
37966
38040
  import { tmpdir as tmpdir5 } from "node:os";
37967
38041
  import { basename as basename4, dirname as dirname15, isAbsolute as isAbsolute3, join as join22, normalize as normalize3, relative as relative3 } from "node:path";
37968
38042
  import { setTimeout as delay3 } from "node:timers/promises";
37969
- import { fileURLToPath as fileURLToPath4 } from "node:url";
38043
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
37970
38044
  import { promisify as promisify7 } from "node:util";
37971
38045
  function patienceForFrames(frames, atLeast) {
37972
38046
  return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
@@ -38212,7 +38286,7 @@ function elsewhereIn(projectPath, asked) {
38212
38286
  const root = (wanted.split("/")[0] ?? "").toLowerCase();
38213
38287
  let near = [];
38214
38288
  try {
38215
- near = readdirSync7(projectPath, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.toLowerCase() !== root).map((entry) => entry.name).filter((name) => name.toLowerCase().startsWith("test") || root.startsWith(name.toLowerCase()));
38289
+ near = readdirSync9(projectPath, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.toLowerCase() !== root).map((entry) => entry.name).filter((name) => name.toLowerCase().startsWith("test") || root.startsWith(name.toLowerCase()));
38216
38290
  } catch {
38217
38291
  return "";
38218
38292
  }
@@ -39372,7 +39446,7 @@ class GodotServer {
39372
39446
  let report = null;
39373
39447
  let reportProblem = null;
39374
39448
  try {
39375
- const written = existsSync15(reportsDir) ? readdirSync7(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
39449
+ const written = existsSync15(reportsDir) ? readdirSync9(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
39376
39450
  const newest = written.at(-1);
39377
39451
  if (newest !== undefined) {
39378
39452
  report = parseJUnit(readFileSync20(join22(reportsDir, newest, "results.xml"), "utf8"));
@@ -39501,7 +39575,7 @@ class GodotServer {
39501
39575
  warnings: diagnostics.length - errors,
39502
39576
  diagnostics,
39503
39577
  ...disagrees
39504
- }, this.godotBridge.getStatus().addonVersion, SERVER_VERSION));
39578
+ }, this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest()));
39505
39579
  }
39506
39580
  whereTheFileDisagrees(diagnostics, args) {
39507
39581
  const messages = diagnostics.map((entry) => asParams(entry)["message"]).filter((message) => typeof message === "string");
@@ -39688,7 +39762,7 @@ class GodotServer {
39688
39762
  getEditorStatusPayload() {
39689
39763
  const status = this.godotBridge.getStatus();
39690
39764
  const isPortConflict = this.bridgeStartupError?.includes("EADDRINUSE") ?? false;
39691
- const stale = status.connected && status.addonVersion !== SERVER_VERSION;
39765
+ const stale = status.connected && editorIsStale(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest());
39692
39766
  const unfinished = this.restartLeftUnfinished(status.connected);
39693
39767
  return {
39694
39768
  ...status,
@@ -39705,7 +39779,7 @@ class GodotServer {
39705
39779
  awaitingLaunchedEditor: status.connected || !this.launchedEditorIsUp() ? undefined : this.launchedEditor?.pid ?? undefined,
39706
39780
  ...status.connected ? this.whatTheLaunchedEditorDropped() : {},
39707
39781
  startupError: this.bridgeStartupError,
39708
- staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
39782
+ staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest()) : undefined,
39709
39783
  ...this.breakpointsAtRisk(),
39710
39784
  retryingBridge: this.bridgeRetry === null ? undefined : true,
39711
39785
  announcedAt: this.announcedAt ?? undefined,
@@ -39837,8 +39911,8 @@ class GodotServer {
39837
39911
  editorPid: now.editorPid,
39838
39912
  addonVersion: now.addonVersion,
39839
39913
  serverVersion: SERVER_VERSION,
39840
- addonIsStale: now.addonVersion !== SERVER_VERSION,
39841
- staleNote: addonMismatch(now.addonVersion, SERVER_VERSION),
39914
+ addonIsStale: editorIsStale(now.addonVersion, SERVER_VERSION, now.addonDigest, shippedEditorDigest()),
39915
+ staleNote: addonMismatch(now.addonVersion, SERVER_VERSION, now.addonDigest, shippedEditorDigest()),
39842
39916
  ...this.whatTheEditorDropped(settingsBefore, after),
39843
39917
  tookMs: Date.now() - began
39844
39918
  });
@@ -40115,7 +40189,7 @@ class GodotServer {
40115
40189
  }
40116
40190
  let announcedAt;
40117
40191
  try {
40118
- announcedAt = statSync6(endpoint.file).mtimeMs;
40192
+ announcedAt = statSync8(endpoint.file).mtimeMs;
40119
40193
  } catch {
40120
40194
  return;
40121
40195
  }
@@ -40725,7 +40799,15 @@ class GodotServer {
40725
40799
  "editor_launch opens one, and an editor opened here has its console captured"
40726
40800
  ]);
40727
40801
  }
40728
- const console2 = editorConsole(projectPath, status.editorPid ?? null, status.openedByAServer === true);
40802
+ const launched = status.connected ? null : this.launchedEditor?.pid ?? null;
40803
+ if (!status.connected && launched === null) {
40804
+ return this.createErrorResponse("No editor is connected, so there is no console to read.", [
40805
+ "editor_status says whether one is connected and whether it may yet be",
40806
+ "editor_launch opens one, and an editor opened here has its console captured"
40807
+ ]);
40808
+ }
40809
+ const editorPid = status.connected ? status.editorPid ?? null : launched;
40810
+ const console2 = editorConsole(projectPath, editorPid, status.connected ? status.openedByAServer === true : true);
40729
40811
  if ("kind" in console2) {
40730
40812
  return this.createErrorResponse(theConsoleWasNotCaptured(console2, projectPath), [
40731
40813
  "editor_launch restart replaces this editor with one whose console is captured",
@@ -40743,8 +40825,9 @@ class GodotServer {
40743
40825
  const grouped = bursts(everything, readPositiveNumber(args, "before") ?? 3);
40744
40826
  const shown = grouped.slice(0, MOST_GROUPS);
40745
40827
  return this.jsonTextResponse({
40746
- editorPid: status.editorPid,
40828
+ editorPid: editorPid ?? undefined,
40747
40829
  projectPath,
40830
+ ...status.connected ? {} : this.launchedEditorIsUp() ? { stillConnecting: true } : { exitedBeforeConnecting: true },
40748
40831
  capturedIn: console2.path,
40749
40832
  ...console2.of === undefined ? {} : {
40750
40833
  readFromTheStart: console2.readBytes,
@@ -41043,6 +41126,7 @@ class GodotServer {
41043
41126
  }
41044
41127
  async rebuildClassCache(projectPath) {
41045
41128
  const before = cachedClasses(projectPath);
41129
+ const writtenBefore = cacheWrittenAt(projectPath);
41046
41130
  const noted = readClassNote(projectPath);
41047
41131
  const rebuilt = await this.operation("refresh_class_cache", {}, projectPath);
41048
41132
  if (!rebuilt.ok) {
@@ -41053,21 +41137,30 @@ class GodotServer {
41053
41137
  return rebuilt;
41054
41138
  }
41055
41139
  const lost = noted === null || before === null ? [] : noted.rebuiltWith.filter((name) => !before.has(name) && after.has(name)).sort();
41056
- const seenLosing = lost.length > 0 && this.godotBridge.isConnected();
41140
+ const cameBack = writtenBefore === null ? [] : declaredSince(projectPath, after, lost, writtenBefore);
41141
+ const droppedByTheEditor = lost.filter((name) => !cameBack.includes(name));
41142
+ const seenLosing = droppedByTheEditor.length > 0 && this.godotBridge.isConnected();
41057
41143
  writeClassNote(projectPath, {
41058
41144
  rebuiltWith: [...after.keys()].sort(),
41059
41145
  ...seenLosing ? { shortListEditorPid: this.godotBridge.getStatus().editorPid ?? null } : noted?.shortListEditorPid === undefined ? {} : { shortListEditorPid: noted.shortListEditorPid }
41060
41146
  });
41061
- if (!seenLosing) {
41147
+ const notes = [];
41148
+ if (seenLosing) {
41149
+ const them = droppedByTheEditor.length === 1 ? "it" : "them";
41150
+ notes.push(`The cache held ${droppedByTheEditor.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.`);
41151
+ }
41152
+ if (cameBack.length > 0) {
41153
+ notes.push(`${cameBack.join(", ")} left the cache while ${cameBack.length === 1 ? "its script was" : "their scripts were"} gone: the script${cameBack.length === 1 ? " is" : "s are"} newer than the cache the editor last wrote, so it listed what was on disk then. editor_rescan picks ${cameBack.length === 1 ? "it" : "them"} up.`);
41154
+ }
41155
+ if (notes.length === 0) {
41062
41156
  return rebuilt;
41063
41157
  }
41064
- const them = lost.length === 1 ? "it" : "them";
41065
41158
  return {
41066
41159
  ...rebuilt,
41067
41160
  payload: {
41068
41161
  ...rebuilt.payload,
41069
41162
  lostSinceLastRebuild: lost,
41070
- 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.`
41163
+ note: notes.join(" ")
41071
41164
  }
41072
41165
  };
41073
41166
  }
@@ -41143,7 +41236,7 @@ class GodotServer {
41143
41236
  ]);
41144
41237
  }
41145
41238
  try {
41146
- return this.jsonTextResponse(markIfStale(await this.godotBridge.invokeTool(toolName, args), this.godotBridge.getStatus().addonVersion, SERVER_VERSION));
41239
+ return this.jsonTextResponse(markIfStale(await this.godotBridge.invokeTool(toolName, args), this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest()));
41147
41240
  } catch (error) {
41148
41241
  return this.createErrorResponse(`The editor answered ${toolName} with an error: ${errorMessage(error)}`);
41149
41242
  }
@@ -41346,7 +41439,7 @@ var init_server3 = __esm(() => {
41346
41439
  init_tool_definitions();
41347
41440
  init_update_check();
41348
41441
  run7 = promisify7(execFile7);
41349
- __dirname2 = dirname15(fileURLToPath4(import.meta.url));
41442
+ __dirname2 = dirname15(fileURLToPath5(import.meta.url));
41350
41443
  CONSOLE_WRAPPER = /_console(\.exe)?$/i;
41351
41444
  LONGEST_BOOT_NOTED_MS = 5 * 60000;
41352
41445
  VALUE_NAMES = {
@@ -42565,7 +42658,17 @@ init_headless();
42565
42658
  init_resources();
42566
42659
  init_server_version();
42567
42660
  import { spawnSync } from "node:child_process";
42568
- import { cpSync, existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync7, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "node:fs";
42661
+ import { createHash as createHash2 } from "node:crypto";
42662
+ import {
42663
+ cpSync,
42664
+ existsSync as existsSync7,
42665
+ mkdirSync as mkdirSync3,
42666
+ readdirSync as readdirSync3,
42667
+ readFileSync as readFileSync7,
42668
+ rmSync as rmSync3,
42669
+ statSync as statSync3,
42670
+ writeFileSync as writeFileSync5
42671
+ } from "node:fs";
42569
42672
  import { dirname as dirname3, join as join8 } from "node:path";
42570
42673
  import { fileURLToPath } from "node:url";
42571
42674
  var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
@@ -42575,6 +42678,40 @@ var RUNTIME_AUTOLOAD = {
42575
42678
  path: "addons/gdharness_runtime/runtime_autoload.gd"
42576
42679
  };
42577
42680
  var VERSION_MARKER = ".gdharness-version";
42681
+ var DIGEST_MARKER = ".gdharness-digest";
42682
+ function editorAddonDigest(from = shippedAddonsDirectory()) {
42683
+ const hash = createHash2("sha256");
42684
+ const walk = (directory, relative) => {
42685
+ for (const name of readdirSync3(directory).sort()) {
42686
+ if (name === VERSION_MARKER || name === DIGEST_MARKER) {
42687
+ continue;
42688
+ }
42689
+ const path = join8(directory, name);
42690
+ const inside = `${relative}/${name}`;
42691
+ if (statSync3(path).isDirectory()) {
42692
+ walk(path, inside);
42693
+ } else {
42694
+ hash.update(`${inside}
42695
+ `);
42696
+ hash.update(readFileSync7(path));
42697
+ hash.update(`
42698
+ `);
42699
+ }
42700
+ }
42701
+ };
42702
+ for (const name of EDITOR_PLUGINS) {
42703
+ walk(join8(from, name), name);
42704
+ }
42705
+ return hash.digest("hex");
42706
+ }
42707
+ function installedEditorDigest(projectPath) {
42708
+ const marker = join8(projectPath, "addons", ADDONS[0], DIGEST_MARKER);
42709
+ if (!existsSync7(marker)) {
42710
+ return null;
42711
+ }
42712
+ const said = readFileSync7(marker, "utf8").trim();
42713
+ return said === "" ? null : said;
42714
+ }
42578
42715
  function shippedAddonsDirectory() {
42579
42716
  return join8(dirname3(fileURLToPath(import.meta.url)), "godot", "addons");
42580
42717
  }
@@ -42597,6 +42734,8 @@ function installAddons(projectPath, from = shippedAddonsDirectory()) {
42597
42734
  `);
42598
42735
  installed.push({ name, path: target, replaced });
42599
42736
  }
42737
+ writeFileSync5(join8(projectPath, "addons", ADDONS[0], DIGEST_MARKER), `${editorAddonDigest(from)}
42738
+ `);
42600
42739
  return installed;
42601
42740
  }
42602
42741
  function removeAddons(projectPath) {
@@ -42741,7 +42880,7 @@ function inspectProject(projectPath) {
42741
42880
  // src/skill.ts
42742
42881
  init_setup();
42743
42882
  init_tool_definitions();
42744
- import { existsSync as existsSync9, mkdirSync as mkdirSync5, readdirSync as readdirSync3, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "node:fs";
42883
+ import { existsSync as existsSync9, mkdirSync as mkdirSync5, readdirSync as readdirSync5, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "node:fs";
42745
42884
  import { basename, dirname as dirname5, join as join10 } from "node:path";
42746
42885
 
42747
42886
  // src/tool-reference.ts
@@ -42863,7 +43002,8 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
42863
43002
  this server is connected to. There is no ambient project, and an argument a tool does not declare is refused
42864
43003
  rather than ignored.
42865
43004
  - \`editor_status\` says whether an editor is connected and whether its addon matches the server.
42866
- \`addonIsStale\` means they differ, and \`staleNote\` says which half is behind: an editor that
43005
+ \`addonIsStale\` means the editor is running different addon code from what the server ships
43006
+ (an older \`addonVersion\` on the same code is not stale), and \`staleNote\` says which half is behind: an editor that
42867
43007
  needs restarting, or a server that needs reconnecting in your harness. Every answer that came
42868
43008
  from the editor carries the same two when they differ, so a stale addon cannot answer quietly:
42869
43009
  believe a restart over a diagnostic that arrived with them.
@@ -43050,7 +43190,7 @@ function pruneEmpty(directory, projectPath) {
43050
43190
  const shared = join10(projectPath, ".agents");
43051
43191
  let path = directory;
43052
43192
  while (path !== projectPath && (basename(path) === "skills" || path === shared)) {
43053
- if (readdirSync3(path).length > 0) {
43193
+ if (readdirSync5(path).length > 0) {
43054
43194
  return;
43055
43195
  }
43056
43196
  rmSync5(path, { recursive: true, force: true });
@@ -43074,9 +43214,9 @@ function writeSkill(directories, version) {
43074
43214
  // src/version.ts
43075
43215
  import { readFileSync as readFileSync9 } from "node:fs";
43076
43216
  import { dirname as dirname6, join as join11 } from "node:path";
43077
- import { fileURLToPath as fileURLToPath2 } from "node:url";
43217
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
43078
43218
  function getLocalVersion() {
43079
- const currentDirectory = dirname6(fileURLToPath2(import.meta.url));
43219
+ const currentDirectory = dirname6(fileURLToPath3(import.meta.url));
43080
43220
  const packageCandidates = [
43081
43221
  join11(currentDirectory, "..", "package.json"),
43082
43222
  join11(currentDirectory, "..", "..", "package.json")
@@ -43338,6 +43478,7 @@ async function upgrade() {
43338
43478
  }
43339
43479
  const godot = await engine(projectPath);
43340
43480
  console.log(installed === version ? `already ${version}; reinstalling` : `${installed} -> ${version}`);
43481
+ const digestBefore = installedEditorDigest(projectPath);
43341
43482
  for (const addon of installAddons(projectPath)) {
43342
43483
  console.log(`${addon.replaced ? "replaced" : "installed"} ${addon.path}`);
43343
43484
  }
@@ -43369,8 +43510,10 @@ async function upgrade() {
43369
43510
  console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
43370
43511
  }
43371
43512
  console.log(`
43372
- On ${version}. Two things this could not do for you:`);
43373
- console.log(` 1. The open editor is still running the addons it loaded at startup. Restart it with the
43513
+ On ${version}. What is left for you:`);
43514
+ console.log(digestBefore !== null && digestBefore === installedEditorDigest(projectPath) ? ` 1. Nothing for the editor: this upgrade left the editor addons the same code as before,
43515
+ ` + ` so it gives an open editor nothing to restart for. editor_status says addonIsStale
43516
+ ` + " if the editor is older than that." : ` 1. The open editor is still running the addons it loaded at startup. Restart it with the
43374
43517
  ` + " editor_launch restart tool, which closes and reopens the window.");
43375
43518
  console.log(harnessNote({ moved, written, byHand }, [launch.command, ...launch.args].join(" ")));
43376
43519
  }
@@ -13,6 +13,8 @@ const Read = preload("reading.gd")
13
13
  const DEFAULT_URL: String = "ws://127.0.0.1:6505/godot"
14
14
  ## Written beside the addon by the install, so it names the version this copy came from.
15
15
  const VERSION_MARKER: String = "res://addons/gdharness_editor/.gdharness-version"
16
+ ## Written beside it, kept in step with `DIGEST_MARKER` in src/setup.ts.
17
+ const DIGEST_MARKER: String = "res://addons/gdharness_editor/.gdharness-digest"
16
18
  ## Written by the server that serves this project, saying where its bridge actually is. Kept in
17
19
  ## step with `announcementPath` in src/bridge-announce.ts.
18
20
  const ANNOUNCEMENT: String = "res://.godot/gdharness-bridge.json"
@@ -70,6 +72,8 @@ var server_url: String = DEFAULT_URL
70
72
  ## What this copy was when it loaded, which is what the editor is running until it is restarted.
71
73
  ## See [method _loaded_version] for why it is held rather than read when it is wanted.
72
74
  var version_at_load: String = ""
75
+ ## The digest of the editor addon code this copy loaded, held for the same reason.
76
+ var digest_at_load: String = ""
73
77
 
74
78
  var _is_connected: bool = false
75
79
  var _reconnect_timer: Timer
@@ -98,6 +102,7 @@ var _connecting_for: float = 0.0
98
102
  func _ready() -> void:
99
103
  _project_path = ProjectSettings.globalize_path("res://")
100
104
  version_at_load = _loaded_version()
105
+ digest_at_load = _loaded_digest()
101
106
  _keep_breakpoints_through_sessions()
102
107
  OS.set_environment(EDITOR_PID_VARIABLE, str(OS.get_process_id()))
103
108
 
@@ -290,6 +295,7 @@ func _handle_connect() -> void:
290
295
  "type": "godot_ready",
291
296
  "project_path": _project_path,
292
297
  "addon_version": version_at_load,
298
+ "addon_digest": digest_at_load,
293
299
  "editor_pid": OS.get_process_id(),
294
300
  "lsp_port": _serves(LSP_ASKED, LSP_SETTING),
295
301
  "dap_port": _serves(DAP_ASKED, DAP_SETTING),
@@ -408,9 +414,19 @@ func _serving(setting: String) -> int:
408
414
  ## and `addonIsStale` reporting false over an editor still running the old code, which is exactly
409
415
  ## what it exists to catch.
410
416
  func _loaded_version() -> String:
411
- if not FileAccess.file_exists(VERSION_MARKER):
417
+ return _read_marker(VERSION_MARKER)
418
+
419
+
420
+ ## The same for the digest of the editor addon code, which an upgrade that leaves the code alone
421
+ ## rewrites unchanged, so the server can tell an editor that needs a restart from one that does not.
422
+ func _loaded_digest() -> String:
423
+ return _read_marker(DIGEST_MARKER)
424
+
425
+
426
+ func _read_marker(path: String) -> String:
427
+ if not FileAccess.file_exists(path):
412
428
  return ""
413
- var file: FileAccess = FileAccess.open(VERSION_MARKER, FileAccess.READ)
429
+ var file: FileAccess = FileAccess.open(path, FileAccess.READ)
414
430
  if file == null:
415
431
  return ""
416
432
  var text: String = file.get_as_text().strip_edges()
package/build/index.js CHANGED
@@ -10047,11 +10047,20 @@ var SERVER_VERSION = (() => {
10047
10047
  }
10048
10048
  })();
10049
10049
  var UNVERSIONED = "an addon from before versions were reported";
10050
- function addonMismatch(addonVersion, serverVersion) {
10051
- if (addonVersion === serverVersion) {
10050
+ function editorIsStale(addonVersion, serverVersion, addonDigest, shippedDigest) {
10051
+ if (addonDigest !== undefined && addonDigest !== "" && shippedDigest !== undefined) {
10052
+ return addonDigest !== shippedDigest;
10053
+ }
10054
+ return addonVersion !== serverVersion;
10055
+ }
10056
+ function addonMismatch(addonVersion, serverVersion, addonDigest, shippedDigest) {
10057
+ if (!editorIsStale(addonVersion, serverVersion, addonDigest, shippedDigest)) {
10052
10058
  return;
10053
10059
  }
10054
10060
  const reported = addonVersion ?? "";
10061
+ if (reported === serverVersion) {
10062
+ return `The editor is running a different build of the ${reported} addon from the one this server ships. Restart it with editor_launch restart to pick this one up.`;
10063
+ }
10055
10064
  const editor = reported === "" ? UNVERSIONED : `the ${reported} addon`;
10056
10065
  const both = `The editor is running ${editor} while this server ships ${serverVersion}.`;
10057
10066
  if (reported !== "" && isNewer(reported, serverVersion)) {
@@ -10059,8 +10068,8 @@ function addonMismatch(addonVersion, serverVersion) {
10059
10068
  }
10060
10069
  return `${both} Restart it with editor_launch restart to pick the new one up.`;
10061
10070
  }
10062
- function markIfStale(answer, addonVersion, serverVersion) {
10063
- const staleNote = addonMismatch(addonVersion, serverVersion);
10071
+ function markIfStale(answer, addonVersion, serverVersion, addonDigest, shippedDigest) {
10072
+ const staleNote = addonMismatch(addonVersion, serverVersion, addonDigest, shippedDigest);
10064
10073
  if (staleNote === undefined || typeof answer !== "object" || answer === null || Array.isArray(answer)) {
10065
10074
  return answer;
10066
10075
  }
@@ -10178,16 +10187,16 @@ import {
10178
10187
  mkdirSync as mkdirSync10,
10179
10188
  mkdtempSync as mkdtempSync2,
10180
10189
  openSync as openSync3,
10181
- readdirSync as readdirSync6,
10190
+ readdirSync as readdirSync7,
10182
10191
  readFileSync as readFileSync16,
10183
10192
  readSync as readSync2,
10184
10193
  rmdirSync,
10185
- statSync as statSync6
10194
+ statSync as statSync7
10186
10195
  } from "node:fs";
10187
10196
  import { tmpdir as tmpdir4 } from "node:os";
10188
10197
  import { basename as basename3, dirname as dirname10, isAbsolute as isAbsolute3, join as join16, normalize as normalize2, relative as relative3 } from "node:path";
10189
10198
  import { setTimeout as delay3 } from "node:timers/promises";
10190
- import { fileURLToPath as fileURLToPath2 } from "node:url";
10199
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
10191
10200
  import { promisify as promisify5 } from "node:util";
10192
10201
 
10193
10202
  // node_modules/zod/v3/helpers/util.js
@@ -25849,6 +25858,19 @@ function cacheWrittenAt(projectPath) {
25849
25858
  return null;
25850
25859
  }
25851
25860
  }
25861
+ function declaredSince(projectPath, paths, names, since) {
25862
+ return names.filter((name) => {
25863
+ const path = paths.get(name);
25864
+ if (path === undefined || !path.startsWith("res://")) {
25865
+ return false;
25866
+ }
25867
+ try {
25868
+ return statSync2(join6(projectPath, path.slice("res://".length))).mtimeMs > since;
25869
+ } catch {
25870
+ return false;
25871
+ }
25872
+ });
25873
+ }
25852
25874
  function cachedAtMissingPaths(cached, projectPath) {
25853
25875
  return [...cached].filter(([, path]) => path.startsWith("res://") && !existsSync3(join6(projectPath, path.slice("res://".length)))).map(([name]) => name).sort();
25854
25876
  }
@@ -27179,6 +27201,7 @@ class GodotBridge extends EventEmitter {
27179
27201
  connectedAt: this.connectionInfo?.connectedAt,
27180
27202
  lastPongAt: this.connectionInfo?.lastPongAt,
27181
27203
  addonVersion: this.connectionInfo?.addonVersion,
27204
+ addonDigest: this.connectionInfo?.addonDigest,
27182
27205
  editorPid: this.connectionInfo?.editorPid,
27183
27206
  lspPort: this.connectionInfo?.lspPort,
27184
27207
  dapPort: this.connectionInfo?.dapPort,
@@ -27322,6 +27345,7 @@ class GodotBridge extends EventEmitter {
27322
27345
  }
27323
27346
  this.connectionInfo.projectPath = message.project_path;
27324
27347
  this.connectionInfo.addonVersion = message.addon_version ?? "";
27348
+ this.connectionInfo.addonDigest = message.addon_digest;
27325
27349
  this.connectionInfo.editorPid = message.editor_pid;
27326
27350
  this.connectionInfo.lspPort = servedPort(message.lsp_port);
27327
27351
  this.connectionInfo.dapPort = servedPort(message.dap_port);
@@ -29456,14 +29480,66 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
29456
29480
  }
29457
29481
 
29458
29482
  // src/setup.ts
29459
- import { cpSync, existsSync as existsSync9, mkdirSync as mkdirSync9, readFileSync as readFileSync15, rmSync as rmSync7, writeFileSync as writeFileSync10 } from "node:fs";
29483
+ import { createHash as createHash3 } from "node:crypto";
29484
+ import {
29485
+ cpSync,
29486
+ existsSync as existsSync9,
29487
+ mkdirSync as mkdirSync9,
29488
+ readdirSync as readdirSync6,
29489
+ readFileSync as readFileSync15,
29490
+ rmSync as rmSync7,
29491
+ statSync as statSync6,
29492
+ writeFileSync as writeFileSync10
29493
+ } from "node:fs";
29460
29494
  import { dirname as dirname9, join as join15 } from "node:path";
29495
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
29461
29496
  var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
29497
+ var EDITOR_PLUGINS = ["gdharness_editor", "auto_reload"];
29462
29498
  var RUNTIME_AUTOLOAD = {
29463
29499
  name: "GdharnessRuntime",
29464
29500
  path: "addons/gdharness_runtime/runtime_autoload.gd"
29465
29501
  };
29466
29502
  var VERSION_MARKER = ".gdharness-version";
29503
+ var DIGEST_MARKER = ".gdharness-digest";
29504
+ function editorAddonDigest(from = shippedAddonsDirectory()) {
29505
+ const hash = createHash3("sha256");
29506
+ const walk = (directory, relative) => {
29507
+ for (const name of readdirSync6(directory).sort()) {
29508
+ if (name === VERSION_MARKER || name === DIGEST_MARKER) {
29509
+ continue;
29510
+ }
29511
+ const path = join15(directory, name);
29512
+ const inside = `${relative}/${name}`;
29513
+ if (statSync6(path).isDirectory()) {
29514
+ walk(path, inside);
29515
+ } else {
29516
+ hash.update(`${inside}
29517
+ `);
29518
+ hash.update(readFileSync15(path));
29519
+ hash.update(`
29520
+ `);
29521
+ }
29522
+ }
29523
+ };
29524
+ for (const name of EDITOR_PLUGINS) {
29525
+ walk(join15(from, name), name);
29526
+ }
29527
+ return hash.digest("hex");
29528
+ }
29529
+ var shippedDigest;
29530
+ function shippedEditorDigest() {
29531
+ if (shippedDigest === undefined) {
29532
+ try {
29533
+ shippedDigest = editorAddonDigest();
29534
+ } catch {
29535
+ shippedDigest = null;
29536
+ }
29537
+ }
29538
+ return shippedDigest ?? undefined;
29539
+ }
29540
+ function shippedAddonsDirectory() {
29541
+ return join15(dirname9(fileURLToPath2(import.meta.url)), "godot", "addons");
29542
+ }
29467
29543
  function installedAddonVersion(projectPath) {
29468
29544
  const marker = join15(projectPath, "addons", ADDONS[0], VERSION_MARKER);
29469
29545
  if (!existsSync9(marker)) {
@@ -30657,7 +30733,7 @@ function buildToolDefinitions() {
30657
30733
  var UPDATE_NOTICE_EVERY = 500;
30658
30734
  var FEEDBACK_NOTICE_EVERY = 250;
30659
30735
  var run5 = promisify5(execFile5);
30660
- var __dirname2 = dirname10(fileURLToPath2(import.meta.url));
30736
+ var __dirname2 = dirname10(fileURLToPath3(import.meta.url));
30661
30737
  var EDITOR_RESTART_TIMEOUT_MS = 90000;
30662
30738
  var HOLD_PING_MS = 2000;
30663
30739
  var SLOWEST_FRAME_RATE = 20;
@@ -30951,7 +31027,7 @@ function elsewhereIn(projectPath, asked) {
30951
31027
  const root = (wanted.split("/")[0] ?? "").toLowerCase();
30952
31028
  let near = [];
30953
31029
  try {
30954
- near = readdirSync6(projectPath, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.toLowerCase() !== root).map((entry) => entry.name).filter((name) => name.toLowerCase().startsWith("test") || root.startsWith(name.toLowerCase()));
31030
+ near = readdirSync7(projectPath, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.toLowerCase() !== root).map((entry) => entry.name).filter((name) => name.toLowerCase().startsWith("test") || root.startsWith(name.toLowerCase()));
30955
31031
  } catch {
30956
31032
  return "";
30957
31033
  }
@@ -32111,7 +32187,7 @@ class GodotServer {
32111
32187
  let report = null;
32112
32188
  let reportProblem = null;
32113
32189
  try {
32114
- const written = existsSync10(reportsDir) ? readdirSync6(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
32190
+ const written = existsSync10(reportsDir) ? readdirSync7(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
32115
32191
  const newest = written.at(-1);
32116
32192
  if (newest !== undefined) {
32117
32193
  report = parseJUnit(readFileSync16(join16(reportsDir, newest, "results.xml"), "utf8"));
@@ -32240,7 +32316,7 @@ class GodotServer {
32240
32316
  warnings: diagnostics.length - errors,
32241
32317
  diagnostics,
32242
32318
  ...disagrees
32243
- }, this.godotBridge.getStatus().addonVersion, SERVER_VERSION));
32319
+ }, this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest()));
32244
32320
  }
32245
32321
  whereTheFileDisagrees(diagnostics, args) {
32246
32322
  const messages = diagnostics.map((entry) => asParams(entry)["message"]).filter((message) => typeof message === "string");
@@ -32427,7 +32503,7 @@ class GodotServer {
32427
32503
  getEditorStatusPayload() {
32428
32504
  const status = this.godotBridge.getStatus();
32429
32505
  const isPortConflict = this.bridgeStartupError?.includes("EADDRINUSE") ?? false;
32430
- const stale = status.connected && status.addonVersion !== SERVER_VERSION;
32506
+ const stale = status.connected && editorIsStale(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest());
32431
32507
  const unfinished = this.restartLeftUnfinished(status.connected);
32432
32508
  return {
32433
32509
  ...status,
@@ -32444,7 +32520,7 @@ class GodotServer {
32444
32520
  awaitingLaunchedEditor: status.connected || !this.launchedEditorIsUp() ? undefined : this.launchedEditor?.pid ?? undefined,
32445
32521
  ...status.connected ? this.whatTheLaunchedEditorDropped() : {},
32446
32522
  startupError: this.bridgeStartupError,
32447
- staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
32523
+ staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest()) : undefined,
32448
32524
  ...this.breakpointsAtRisk(),
32449
32525
  retryingBridge: this.bridgeRetry === null ? undefined : true,
32450
32526
  announcedAt: this.announcedAt ?? undefined,
@@ -32576,8 +32652,8 @@ class GodotServer {
32576
32652
  editorPid: now.editorPid,
32577
32653
  addonVersion: now.addonVersion,
32578
32654
  serverVersion: SERVER_VERSION,
32579
- addonIsStale: now.addonVersion !== SERVER_VERSION,
32580
- staleNote: addonMismatch(now.addonVersion, SERVER_VERSION),
32655
+ addonIsStale: editorIsStale(now.addonVersion, SERVER_VERSION, now.addonDigest, shippedEditorDigest()),
32656
+ staleNote: addonMismatch(now.addonVersion, SERVER_VERSION, now.addonDigest, shippedEditorDigest()),
32581
32657
  ...this.whatTheEditorDropped(settingsBefore, after),
32582
32658
  tookMs: Date.now() - began
32583
32659
  });
@@ -32854,7 +32930,7 @@ class GodotServer {
32854
32930
  }
32855
32931
  let announcedAt;
32856
32932
  try {
32857
- announcedAt = statSync6(endpoint.file).mtimeMs;
32933
+ announcedAt = statSync7(endpoint.file).mtimeMs;
32858
32934
  } catch {
32859
32935
  return;
32860
32936
  }
@@ -33464,7 +33540,15 @@ class GodotServer {
33464
33540
  "editor_launch opens one, and an editor opened here has its console captured"
33465
33541
  ]);
33466
33542
  }
33467
- const console2 = editorConsole(projectPath, status.editorPid ?? null, status.openedByAServer === true);
33543
+ const launched = status.connected ? null : this.launchedEditor?.pid ?? null;
33544
+ if (!status.connected && launched === null) {
33545
+ return this.createErrorResponse("No editor is connected, so there is no console to read.", [
33546
+ "editor_status says whether one is connected and whether it may yet be",
33547
+ "editor_launch opens one, and an editor opened here has its console captured"
33548
+ ]);
33549
+ }
33550
+ const editorPid = status.connected ? status.editorPid ?? null : launched;
33551
+ const console2 = editorConsole(projectPath, editorPid, status.connected ? status.openedByAServer === true : true);
33468
33552
  if ("kind" in console2) {
33469
33553
  return this.createErrorResponse(theConsoleWasNotCaptured(console2, projectPath), [
33470
33554
  "editor_launch restart replaces this editor with one whose console is captured",
@@ -33482,8 +33566,9 @@ class GodotServer {
33482
33566
  const grouped = bursts(everything, readPositiveNumber(args, "before") ?? 3);
33483
33567
  const shown = grouped.slice(0, MOST_GROUPS);
33484
33568
  return this.jsonTextResponse({
33485
- editorPid: status.editorPid,
33569
+ editorPid: editorPid ?? undefined,
33486
33570
  projectPath,
33571
+ ...status.connected ? {} : this.launchedEditorIsUp() ? { stillConnecting: true } : { exitedBeforeConnecting: true },
33487
33572
  capturedIn: console2.path,
33488
33573
  ...console2.of === undefined ? {} : {
33489
33574
  readFromTheStart: console2.readBytes,
@@ -33782,6 +33867,7 @@ class GodotServer {
33782
33867
  }
33783
33868
  async rebuildClassCache(projectPath) {
33784
33869
  const before = cachedClasses(projectPath);
33870
+ const writtenBefore = cacheWrittenAt(projectPath);
33785
33871
  const noted = readClassNote(projectPath);
33786
33872
  const rebuilt = await this.operation("refresh_class_cache", {}, projectPath);
33787
33873
  if (!rebuilt.ok) {
@@ -33792,21 +33878,30 @@ class GodotServer {
33792
33878
  return rebuilt;
33793
33879
  }
33794
33880
  const lost = noted === null || before === null ? [] : noted.rebuiltWith.filter((name) => !before.has(name) && after.has(name)).sort();
33795
- const seenLosing = lost.length > 0 && this.godotBridge.isConnected();
33881
+ const cameBack = writtenBefore === null ? [] : declaredSince(projectPath, after, lost, writtenBefore);
33882
+ const droppedByTheEditor = lost.filter((name) => !cameBack.includes(name));
33883
+ const seenLosing = droppedByTheEditor.length > 0 && this.godotBridge.isConnected();
33796
33884
  writeClassNote(projectPath, {
33797
33885
  rebuiltWith: [...after.keys()].sort(),
33798
33886
  ...seenLosing ? { shortListEditorPid: this.godotBridge.getStatus().editorPid ?? null } : noted?.shortListEditorPid === undefined ? {} : { shortListEditorPid: noted.shortListEditorPid }
33799
33887
  });
33800
- if (!seenLosing) {
33888
+ const notes = [];
33889
+ if (seenLosing) {
33890
+ const them = droppedByTheEditor.length === 1 ? "it" : "them";
33891
+ notes.push(`The cache held ${droppedByTheEditor.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.`);
33892
+ }
33893
+ if (cameBack.length > 0) {
33894
+ notes.push(`${cameBack.join(", ")} left the cache while ${cameBack.length === 1 ? "its script was" : "their scripts were"} gone: the script${cameBack.length === 1 ? " is" : "s are"} newer than the cache the editor last wrote, so it listed what was on disk then. editor_rescan picks ${cameBack.length === 1 ? "it" : "them"} up.`);
33895
+ }
33896
+ if (notes.length === 0) {
33801
33897
  return rebuilt;
33802
33898
  }
33803
- const them = lost.length === 1 ? "it" : "them";
33804
33899
  return {
33805
33900
  ...rebuilt,
33806
33901
  payload: {
33807
33902
  ...rebuilt.payload,
33808
33903
  lostSinceLastRebuild: lost,
33809
- 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.`
33904
+ note: notes.join(" ")
33810
33905
  }
33811
33906
  };
33812
33907
  }
@@ -33882,7 +33977,7 @@ class GodotServer {
33882
33977
  ]);
33883
33978
  }
33884
33979
  try {
33885
- return this.jsonTextResponse(markIfStale(await this.godotBridge.invokeTool(toolName, args), this.godotBridge.getStatus().addonVersion, SERVER_VERSION));
33980
+ return this.jsonTextResponse(markIfStale(await this.godotBridge.invokeTool(toolName, args), this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest()));
33886
33981
  } catch (error) {
33887
33982
  return this.createErrorResponse(`The editor answered ${toolName} with an error: ${errorMessage(error)}`);
33888
33983
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.0.12",
3
+ "version": "1.0.13",
4
4
  "mcpName": "io.github.Aureliolo/gdharness",
5
5
  "description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
6
6
  "type": "module",