gdharness 1.0.12 → 1.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/cli.js CHANGED
@@ -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";
@@ -31599,8 +31671,7 @@ function announcedAndGone(projectPath) {
31599
31671
  return [];
31600
31672
  }
31601
31673
  const since = Date.now() - GONE_WINDOW_MS;
31602
- const wanted = resolve3(projectPath);
31603
- return gone.filter((one) => one.noticedAt >= since && resolve3(one.project) === wanted);
31674
+ return gone.filter((one) => one.noticedAt >= since && isSameDirectory(one.project, projectPath));
31604
31675
  }
31605
31676
  function strangersAmong(candidates, now, startTimes = startTimesOf) {
31606
31677
  const toJudge = candidates.filter((one) => {
@@ -31627,7 +31698,7 @@ function announcedIn(directory) {
31627
31698
  const found = [];
31628
31699
  const candidates = [];
31629
31700
  const entries = [];
31630
- for (const entry of readdirSync4(directory)) {
31701
+ for (const entry of readdirSync6(directory)) {
31631
31702
  const report = ERROR_REPORT_PATTERN.exec(entry);
31632
31703
  if (report) {
31633
31704
  sweepErrorReport(join12(directory, entry), Number.parseInt(report[1] ?? "", 10));
@@ -31643,7 +31714,7 @@ function announcedIn(directory) {
31643
31714
  entries.push({ file, pid, alive });
31644
31715
  if (alive) {
31645
31716
  try {
31646
- candidates.push({ file, pid, writtenAt: statSync3(file).mtimeMs });
31717
+ candidates.push({ file, pid, writtenAt: statSync5(file).mtimeMs });
31647
31718
  } catch {}
31648
31719
  }
31649
31720
  }
@@ -31700,17 +31771,16 @@ function sweepErrorReport(path, pid, now = Date.now()) {
31700
31771
  return;
31701
31772
  }
31702
31773
  try {
31703
- if (now - statSync3(path).mtimeMs >= ERROR_REPORT_KEEP_MS) {
31774
+ if (now - statSync5(path).mtimeMs >= ERROR_REPORT_KEEP_MS) {
31704
31775
  unlinkSync(path);
31705
31776
  }
31706
31777
  } catch {}
31707
31778
  }
31708
31779
  async function announcedSince(projectPath, before, waiting = {}) {
31709
- const wanted = resolve3(projectPath);
31710
31780
  const directories = waiting.directories ?? runtimeDirectories();
31711
31781
  const until = Date.now() + Math.max(waiting.budgetMs ?? ANNOUNCE_BUDGET_MS, 0);
31712
31782
  for (;; ) {
31713
- const fresh = discoverRuntimes(directories).find((endpoint) => !before.has(endpoint.pid) && resolve3(endpoint.project.path) === wanted && (waiting.accept?.(endpoint) ?? true));
31783
+ const fresh = discoverRuntimes(directories).find((endpoint) => !before.has(endpoint.pid) && isSameDirectory(endpoint.project.path, projectPath) && (waiting.accept?.(endpoint) ?? true));
31714
31784
  if (fresh !== undefined) {
31715
31785
  return fresh;
31716
31786
  }
@@ -31731,7 +31801,7 @@ function theOneProject(endpoints) {
31731
31801
  if (endpoints.some((endpoint) => endpoint.project.path === "")) {
31732
31802
  return;
31733
31803
  }
31734
- const paths = new Set(endpoints.map((endpoint) => resolve3(endpoint.project.path)));
31804
+ const paths = new Set(endpoints.map((endpoint) => realPathOr(resolve3(endpoint.project.path))));
31735
31805
  const only = [...paths][0];
31736
31806
  return paths.size === 1 ? only : undefined;
31737
31807
  }
@@ -31760,7 +31830,7 @@ function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
31760
31830
  if (pid !== undefined) {
31761
31831
  const named = endpoints.find((endpoint) => endpoint.pid === pid);
31762
31832
  if (named !== undefined) {
31763
- if (projectPath !== undefined && resolve3(named.project.path) !== resolve3(projectPath)) {
31833
+ if (projectPath !== undefined && !isSameDirectory(named.project.path, projectPath)) {
31764
31834
  return {
31765
31835
  problem: `The game with pid ${pid} is running ${named.project.path}, not ${resolve3(projectPath)}. Running: ${endpoints.map(describe2).join("; ")}.`
31766
31836
  };
@@ -31803,7 +31873,7 @@ function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
31803
31873
  }
31804
31874
  if (projectPath !== undefined) {
31805
31875
  const wanted = resolve3(projectPath);
31806
- const matching = endpoints.filter((endpoint) => resolve3(endpoint.project.path) === wanted);
31876
+ const matching = endpoints.filter((endpoint) => isSameDirectory(endpoint.project.path, wanted));
31807
31877
  if (matching.length === 0) {
31808
31878
  return {
31809
31879
  problem: `No running game is from ${wanted}. Running: ${endpoints.map(describe2).join("; ")}.`
@@ -31914,6 +31984,7 @@ function runtimeRequest(endpoint, command, params, timeoutMs) {
31914
31984
  }
31915
31985
  var RUNTIME_PROTOCOL = 2, ANNOUNCEMENT_PATTERN, ERROR_REPORT_PATTERN, ERROR_REPORT_KEEP_MS, GONE_WINDOW_MS = 120000, GONE_KEPT = 32, gone, STARTED_AFTER_ANNOUNCING_MS = 2000, CONFIRMED_FOR_MS = 60000, JUDGED_AFTER_MS = 60000, verdicts, ENDED_WITHIN_MS = 3000, LOOK_EVERY_MS = 50, ANNOUNCE_BUDGET_MS = 5000, nextRequestId = 1;
31916
31986
  var init_runtime_client = __esm(() => {
31987
+ init_paths();
31917
31988
  init_process_children();
31918
31989
  ANNOUNCEMENT_PATTERN = /^runtime-(\d+)\.json$/;
31919
31990
  ERROR_REPORT_PATTERN = /^runtime-(\d+)\.log$/;
@@ -32879,7 +32950,7 @@ import {
32879
32950
  readFileSync as readFileSync16,
32880
32951
  readSync,
32881
32952
  rmSync as rmSync8,
32882
- statSync as statSync4,
32953
+ statSync as statSync6,
32883
32954
  writeFileSync as writeFileSync12
32884
32955
  } from "node:fs";
32885
32956
  import { dirname as dirname12, join as join18 } from "node:path";
@@ -32942,7 +33013,7 @@ function editorConsole(projectPath, editorPid, openedByAServer) {
32942
33013
  return { log, path, ...of === undefined ? {} : { readBytes: text.length, of } };
32943
33014
  }
32944
33015
  function theFirstOf(path) {
32945
- const whole = statSync4(path).size;
33016
+ const whole = statSync6(path).size;
32946
33017
  if (whole <= MOST_CONSOLE_BYTES) {
32947
33018
  return { text: readFileSync16(path, "utf8") };
32948
33019
  }
@@ -36160,6 +36231,7 @@ var init_godot_bridge = __esm(() => {
36160
36231
  connectedAt: this.connectionInfo?.connectedAt,
36161
36232
  lastPongAt: this.connectionInfo?.lastPongAt,
36162
36233
  addonVersion: this.connectionInfo?.addonVersion,
36234
+ addonDigest: this.connectionInfo?.addonDigest,
36163
36235
  editorPid: this.connectionInfo?.editorPid,
36164
36236
  lspPort: this.connectionInfo?.lspPort,
36165
36237
  dapPort: this.connectionInfo?.dapPort,
@@ -36303,6 +36375,7 @@ var init_godot_bridge = __esm(() => {
36303
36375
  }
36304
36376
  this.connectionInfo.projectPath = message.project_path;
36305
36377
  this.connectionInfo.addonVersion = message.addon_version ?? "";
36378
+ this.connectionInfo.addonDigest = message.addon_digest;
36306
36379
  this.connectionInfo.editorPid = message.editor_pid;
36307
36380
  this.connectionInfo.lspPort = servedPort(message.lsp_port);
36308
36381
  this.connectionInfo.dapPort = servedPort(message.dap_port);
@@ -36556,7 +36629,7 @@ var init_godot_path = __esm(() => {
36556
36629
  });
36557
36630
 
36558
36631
  // src/issues.ts
36559
- import { createHash as createHash2 } from "node:crypto";
36632
+ import { createHash as createHash4 } from "node:crypto";
36560
36633
  import process7 from "node:process";
36561
36634
  function feedbackNotice() {
36562
36635
  return JSON.stringify({
@@ -36574,7 +36647,7 @@ function runtime2() {
36574
36647
  function defectSignature2(where, message) {
36575
36648
  const normalised = `${where}
36576
36649
  ${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);
36650
+ return createHash4("sha256").update(normalised).digest("hex").slice(0, 8);
36578
36651
  }
36579
36652
  function facts2(where, message, godotVersion) {
36580
36653
  return [
@@ -36911,7 +36984,7 @@ import { realpathSync as realpathSync2 } from "node:fs";
36911
36984
  import { readFile, realpath } from "node:fs/promises";
36912
36985
  import { createConnection as createConnection3 } from "node:net";
36913
36986
  import { dirname as dirname13, resolve as resolve4 } from "node:path";
36914
- import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
36987
+ import { fileURLToPath as fileURLToPath4, pathToFileURL } from "node:url";
36915
36988
  function diagnosticsKey(uri) {
36916
36989
  let decoded;
36917
36990
  try {
@@ -36923,7 +36996,7 @@ function diagnosticsKey(uri) {
36923
36996
  return decoded;
36924
36997
  }
36925
36998
  try {
36926
- return realpathSync2.native(fileURLToPath3(decoded)).toLowerCase();
36999
+ return realpathSync2.native(fileURLToPath4(decoded)).toLowerCase();
36927
37000
  } catch {
36928
37001
  return decoded.toLowerCase();
36929
37002
  }
@@ -37483,7 +37556,7 @@ var init_process_time = __esm(() => {
37483
37556
  });
37484
37557
 
37485
37558
  // src/project-scan.ts
37486
- import { existsSync as existsSync14, readdirSync as readdirSync5, readFileSync as readFileSync17 } from "node:fs";
37559
+ import { existsSync as existsSync14, readdirSync as readdirSync7, readFileSync as readFileSync17 } from "node:fs";
37487
37560
  import { join as join19 } from "node:path";
37488
37561
  function skipped(name) {
37489
37562
  return name.startsWith(".") || SKIPPED.has(name);
@@ -37497,7 +37570,7 @@ function projectStructure(projectPath) {
37497
37570
  if (steppedOver(directory)) {
37498
37571
  return;
37499
37572
  }
37500
- for (const entry of readdirSync5(directory, { withFileTypes: true })) {
37573
+ for (const entry of readdirSync7(directory, { withFileTypes: true })) {
37501
37574
  if (skipped(entry.name)) {
37502
37575
  continue;
37503
37576
  }
@@ -37526,7 +37599,7 @@ function scriptsWithoutUid(projectPath) {
37526
37599
  if (steppedOver(directory)) {
37527
37600
  return;
37528
37601
  }
37529
- for (const entry of readdirSync5(directory, { withFileTypes: true })) {
37602
+ for (const entry of readdirSync7(directory, { withFileTypes: true })) {
37530
37603
  if (skipped(entry.name)) {
37531
37604
  continue;
37532
37605
  }
@@ -37567,7 +37640,7 @@ function searchProject(projectPath, options) {
37567
37640
  if (steppedOver(directory)) {
37568
37641
  return;
37569
37642
  }
37570
- for (const entry of readdirSync5(directory, { withFileTypes: true })) {
37643
+ for (const entry of readdirSync7(directory, { withFileTypes: true })) {
37571
37644
  if (full()) {
37572
37645
  return;
37573
37646
  }
@@ -37657,7 +37730,7 @@ var init_restart_note = __esm(() => {
37657
37730
 
37658
37731
  // src/run-record.ts
37659
37732
  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";
37733
+ 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
37734
  import { basename as basename3, join as join21 } from "node:path";
37662
37735
  function runsDirectory() {
37663
37736
  return join21(runtimeDirectory(), "runs");
@@ -37912,7 +37985,7 @@ function clearRunRecord(ours = () => true) {
37912
37985
  }
37913
37986
  function ageMs(path, now) {
37914
37987
  try {
37915
- return now - statSync5(path).mtimeMs;
37988
+ return now - statSync7(path).mtimeMs;
37916
37989
  } catch {
37917
37990
  return 0;
37918
37991
  }
@@ -37922,7 +37995,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
37922
37995
  const keep = readRunRecord()?.transcript;
37923
37996
  let entries;
37924
37997
  try {
37925
- entries = readdirSync6(directory);
37998
+ entries = readdirSync8(directory);
37926
37999
  } catch {
37927
38000
  return;
37928
38001
  }
@@ -37957,16 +38030,16 @@ import {
37957
38030
  mkdirSync as mkdirSync13,
37958
38031
  mkdtempSync as mkdtempSync3,
37959
38032
  openSync as openSync3,
37960
- readdirSync as readdirSync7,
38033
+ readdirSync as readdirSync9,
37961
38034
  readFileSync as readFileSync20,
37962
38035
  readSync as readSync2,
37963
38036
  rmdirSync,
37964
- statSync as statSync6
38037
+ statSync as statSync8
37965
38038
  } from "node:fs";
37966
38039
  import { tmpdir as tmpdir5 } from "node:os";
37967
38040
  import { basename as basename4, dirname as dirname15, isAbsolute as isAbsolute3, join as join22, normalize as normalize3, relative as relative3 } from "node:path";
37968
38041
  import { setTimeout as delay3 } from "node:timers/promises";
37969
- import { fileURLToPath as fileURLToPath4 } from "node:url";
38042
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
37970
38043
  import { promisify as promisify7 } from "node:util";
37971
38044
  function patienceForFrames(frames, atLeast) {
37972
38045
  return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
@@ -38212,7 +38285,7 @@ function elsewhereIn(projectPath, asked) {
38212
38285
  const root = (wanted.split("/")[0] ?? "").toLowerCase();
38213
38286
  let near = [];
38214
38287
  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()));
38288
+ 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
38289
  } catch {
38217
38290
  return "";
38218
38291
  }
@@ -39372,7 +39445,7 @@ class GodotServer {
39372
39445
  let report = null;
39373
39446
  let reportProblem = null;
39374
39447
  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))) : [];
39448
+ 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
39449
  const newest = written.at(-1);
39377
39450
  if (newest !== undefined) {
39378
39451
  report = parseJUnit(readFileSync20(join22(reportsDir, newest, "results.xml"), "utf8"));
@@ -39501,7 +39574,7 @@ class GodotServer {
39501
39574
  warnings: diagnostics.length - errors,
39502
39575
  diagnostics,
39503
39576
  ...disagrees
39504
- }, this.godotBridge.getStatus().addonVersion, SERVER_VERSION));
39577
+ }, this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest()));
39505
39578
  }
39506
39579
  whereTheFileDisagrees(diagnostics, args) {
39507
39580
  const messages = diagnostics.map((entry) => asParams(entry)["message"]).filter((message) => typeof message === "string");
@@ -39688,7 +39761,7 @@ class GodotServer {
39688
39761
  getEditorStatusPayload() {
39689
39762
  const status = this.godotBridge.getStatus();
39690
39763
  const isPortConflict = this.bridgeStartupError?.includes("EADDRINUSE") ?? false;
39691
- const stale = status.connected && status.addonVersion !== SERVER_VERSION;
39764
+ const stale = status.connected && editorIsStale(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest());
39692
39765
  const unfinished = this.restartLeftUnfinished(status.connected);
39693
39766
  return {
39694
39767
  ...status,
@@ -39705,7 +39778,7 @@ class GodotServer {
39705
39778
  awaitingLaunchedEditor: status.connected || !this.launchedEditorIsUp() ? undefined : this.launchedEditor?.pid ?? undefined,
39706
39779
  ...status.connected ? this.whatTheLaunchedEditorDropped() : {},
39707
39780
  startupError: this.bridgeStartupError,
39708
- staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
39781
+ staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest()) : undefined,
39709
39782
  ...this.breakpointsAtRisk(),
39710
39783
  retryingBridge: this.bridgeRetry === null ? undefined : true,
39711
39784
  announcedAt: this.announcedAt ?? undefined,
@@ -39837,8 +39910,8 @@ class GodotServer {
39837
39910
  editorPid: now.editorPid,
39838
39911
  addonVersion: now.addonVersion,
39839
39912
  serverVersion: SERVER_VERSION,
39840
- addonIsStale: now.addonVersion !== SERVER_VERSION,
39841
- staleNote: addonMismatch(now.addonVersion, SERVER_VERSION),
39913
+ addonIsStale: editorIsStale(now.addonVersion, SERVER_VERSION, now.addonDigest, shippedEditorDigest()),
39914
+ staleNote: addonMismatch(now.addonVersion, SERVER_VERSION, now.addonDigest, shippedEditorDigest()),
39842
39915
  ...this.whatTheEditorDropped(settingsBefore, after),
39843
39916
  tookMs: Date.now() - began
39844
39917
  });
@@ -40115,7 +40188,7 @@ class GodotServer {
40115
40188
  }
40116
40189
  let announcedAt;
40117
40190
  try {
40118
- announcedAt = statSync6(endpoint.file).mtimeMs;
40191
+ announcedAt = statSync8(endpoint.file).mtimeMs;
40119
40192
  } catch {
40120
40193
  return;
40121
40194
  }
@@ -40725,7 +40798,15 @@ class GodotServer {
40725
40798
  "editor_launch opens one, and an editor opened here has its console captured"
40726
40799
  ]);
40727
40800
  }
40728
- const console2 = editorConsole(projectPath, status.editorPid ?? null, status.openedByAServer === true);
40801
+ const launched = status.connected ? null : this.launchedEditor?.pid ?? null;
40802
+ if (!status.connected && launched === null) {
40803
+ return this.createErrorResponse("No editor is connected, so there is no console to read.", [
40804
+ "editor_status says whether one is connected and whether it may yet be",
40805
+ "editor_launch opens one, and an editor opened here has its console captured"
40806
+ ]);
40807
+ }
40808
+ const editorPid = status.connected ? status.editorPid ?? null : launched;
40809
+ const console2 = editorConsole(projectPath, editorPid, status.connected ? status.openedByAServer === true : true);
40729
40810
  if ("kind" in console2) {
40730
40811
  return this.createErrorResponse(theConsoleWasNotCaptured(console2, projectPath), [
40731
40812
  "editor_launch restart replaces this editor with one whose console is captured",
@@ -40743,8 +40824,9 @@ class GodotServer {
40743
40824
  const grouped = bursts(everything, readPositiveNumber(args, "before") ?? 3);
40744
40825
  const shown = grouped.slice(0, MOST_GROUPS);
40745
40826
  return this.jsonTextResponse({
40746
- editorPid: status.editorPid,
40827
+ editorPid: editorPid ?? undefined,
40747
40828
  projectPath,
40829
+ ...status.connected ? {} : this.launchedEditorIsUp() ? { stillConnecting: true } : { exitedBeforeConnecting: true },
40748
40830
  capturedIn: console2.path,
40749
40831
  ...console2.of === undefined ? {} : {
40750
40832
  readFromTheStart: console2.readBytes,
@@ -41043,6 +41125,7 @@ class GodotServer {
41043
41125
  }
41044
41126
  async rebuildClassCache(projectPath) {
41045
41127
  const before = cachedClasses(projectPath);
41128
+ const writtenBefore = cacheWrittenAt(projectPath);
41046
41129
  const noted = readClassNote(projectPath);
41047
41130
  const rebuilt = await this.operation("refresh_class_cache", {}, projectPath);
41048
41131
  if (!rebuilt.ok) {
@@ -41053,21 +41136,30 @@ class GodotServer {
41053
41136
  return rebuilt;
41054
41137
  }
41055
41138
  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();
41139
+ const cameBack = writtenBefore === null ? [] : declaredSince(projectPath, after, lost, writtenBefore);
41140
+ const droppedByTheEditor = lost.filter((name) => !cameBack.includes(name));
41141
+ const seenLosing = droppedByTheEditor.length > 0 && this.godotBridge.isConnected();
41057
41142
  writeClassNote(projectPath, {
41058
41143
  rebuiltWith: [...after.keys()].sort(),
41059
41144
  ...seenLosing ? { shortListEditorPid: this.godotBridge.getStatus().editorPid ?? null } : noted?.shortListEditorPid === undefined ? {} : { shortListEditorPid: noted.shortListEditorPid }
41060
41145
  });
41061
- if (!seenLosing) {
41146
+ const notes = [];
41147
+ if (seenLosing) {
41148
+ const them = droppedByTheEditor.length === 1 ? "it" : "them";
41149
+ 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.`);
41150
+ }
41151
+ if (cameBack.length > 0) {
41152
+ 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.`);
41153
+ }
41154
+ if (notes.length === 0) {
41062
41155
  return rebuilt;
41063
41156
  }
41064
- const them = lost.length === 1 ? "it" : "them";
41065
41157
  return {
41066
41158
  ...rebuilt,
41067
41159
  payload: {
41068
41160
  ...rebuilt.payload,
41069
41161
  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.`
41162
+ note: notes.join(" ")
41071
41163
  }
41072
41164
  };
41073
41165
  }
@@ -41143,7 +41235,7 @@ class GodotServer {
41143
41235
  ]);
41144
41236
  }
41145
41237
  try {
41146
- return this.jsonTextResponse(markIfStale(await this.godotBridge.invokeTool(toolName, args), this.godotBridge.getStatus().addonVersion, SERVER_VERSION));
41238
+ return this.jsonTextResponse(markIfStale(await this.godotBridge.invokeTool(toolName, args), this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest()));
41147
41239
  } catch (error) {
41148
41240
  return this.createErrorResponse(`The editor answered ${toolName} with an error: ${errorMessage(error)}`);
41149
41241
  }
@@ -41196,8 +41288,59 @@ class GodotServer {
41196
41288
  }
41197
41289
  return { heldAt: undefined, heldUnknown: true, heldNote: reply.message };
41198
41290
  }
41291
+ async ownRunOnItsWay(projectPath, budgetMs) {
41292
+ const run = this.currentRun();
41293
+ const project = projectPath ?? run?.projectPath ?? null;
41294
+ if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync15(join22(project, RUNTIME_AUTOLOAD2.path)) || !await this.runStillGoing(run)) {
41295
+ return null;
41296
+ }
41297
+ const already = runtimesAnnounced().running;
41298
+ const announcedSinceTheStart = (one) => {
41299
+ if (run.announcedBefore !== undefined) {
41300
+ return !run.announcedBefore.has(one.pid);
41301
+ }
41302
+ try {
41303
+ return statSync8(one.file).mtimeMs >= run.startedAt;
41304
+ } catch {
41305
+ return false;
41306
+ }
41307
+ };
41308
+ if (already.some((one) => announcedSinceTheStart(one) && this.isTheGameOf(run, one, project))) {
41309
+ return null;
41310
+ }
41311
+ const which = run.throughEditor ? "the game the editor is playing for it" : `pid ${run.pid ?? "unknown"}`;
41312
+ const comingUpUntil = run.startedAt + waitSizedTo(readBootNote(project)) + budgetMs;
41313
+ const remaining = comingUpUntil - Date.now();
41314
+ if (remaining <= 0) {
41315
+ const seconds = Math.round((Date.now() - run.startedAt) / 1000);
41316
+ return `The game this server started (${which}) has been running for ${seconds}s and has not announced its runtime, so there is nothing to talk to. A game that keeps the runtime off, as a project's own loader can for some scenes, never announces; editor_output says what it has printed.`;
41317
+ }
41318
+ const endpoint = await announcedSince(project, new Set(already.map((one) => one.pid)), {
41319
+ budgetMs: Math.min(budgetMs, remaining),
41320
+ accept: (one) => this.isTheGameOf(this.currentRun(), one, project),
41321
+ giveUp: () => this.currentRun() !== run || this.dapClient?.isStopped() === true || !stillRunning(run)
41322
+ });
41323
+ if (endpoint !== null) {
41324
+ const tiedMeanwhile = () => run.announcedPid !== undefined;
41325
+ if (!tiedMeanwhile()) {
41326
+ this.tie(run, endpoint);
41327
+ }
41328
+ return null;
41329
+ }
41330
+ if (this.currentRun() !== run || !await this.runStillGoing(run)) {
41331
+ return null;
41332
+ }
41333
+ return `The game this server started (${which}) is still coming up and has not announced its runtime after a wait of ${Math.min(budgetMs, remaining)}ms, so there is nothing to talk to yet. It is still running: ask again in a moment rather than starting another.`;
41334
+ }
41199
41335
  async handleRuntimeCommand(command, args, timeoutMs = this.runtimeTimeoutMs()) {
41200
41336
  const { op: _op, projectPath, pid, ...params } = asParams(args);
41337
+ const waited = typeof pid === "number" ? null : await this.ownRunOnItsWay(typeof projectPath === "string" ? projectPath : this.ownProject, timeoutMs);
41338
+ if (waited !== null) {
41339
+ return this.createErrorResponse(waited, [
41340
+ "editor_status lists it under runtimes once it has announced",
41341
+ "editor_output says what it has printed so far"
41342
+ ]);
41343
+ }
41201
41344
  const announced = runtimesAnnounced();
41202
41345
  const wanted = typeof projectPath === "string" ? projectPath : this.ownProject ?? undefined;
41203
41346
  const own = typeof pid === "number" ? undefined : this.ownGameAmong(announced.running, wanted) ?? await this.ownGameThroughTheTree(announced.running, wanted);
@@ -41346,7 +41489,7 @@ var init_server3 = __esm(() => {
41346
41489
  init_tool_definitions();
41347
41490
  init_update_check();
41348
41491
  run7 = promisify7(execFile7);
41349
- __dirname2 = dirname15(fileURLToPath4(import.meta.url));
41492
+ __dirname2 = dirname15(fileURLToPath5(import.meta.url));
41350
41493
  CONSOLE_WRAPPER = /_console(\.exe)?$/i;
41351
41494
  LONGEST_BOOT_NOTED_MS = 5 * 60000;
41352
41495
  VALUE_NAMES = {
@@ -42565,7 +42708,17 @@ init_headless();
42565
42708
  init_resources();
42566
42709
  init_server_version();
42567
42710
  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";
42711
+ import { createHash as createHash2 } from "node:crypto";
42712
+ import {
42713
+ cpSync,
42714
+ existsSync as existsSync7,
42715
+ mkdirSync as mkdirSync3,
42716
+ readdirSync as readdirSync3,
42717
+ readFileSync as readFileSync7,
42718
+ rmSync as rmSync3,
42719
+ statSync as statSync3,
42720
+ writeFileSync as writeFileSync5
42721
+ } from "node:fs";
42569
42722
  import { dirname as dirname3, join as join8 } from "node:path";
42570
42723
  import { fileURLToPath } from "node:url";
42571
42724
  var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
@@ -42575,6 +42728,40 @@ var RUNTIME_AUTOLOAD = {
42575
42728
  path: "addons/gdharness_runtime/runtime_autoload.gd"
42576
42729
  };
42577
42730
  var VERSION_MARKER = ".gdharness-version";
42731
+ var DIGEST_MARKER = ".gdharness-digest";
42732
+ function editorAddonDigest(from = shippedAddonsDirectory()) {
42733
+ const hash = createHash2("sha256");
42734
+ const walk = (directory, relative) => {
42735
+ for (const name of readdirSync3(directory).sort()) {
42736
+ if (name === VERSION_MARKER || name === DIGEST_MARKER) {
42737
+ continue;
42738
+ }
42739
+ const path = join8(directory, name);
42740
+ const inside = `${relative}/${name}`;
42741
+ if (statSync3(path).isDirectory()) {
42742
+ walk(path, inside);
42743
+ } else {
42744
+ hash.update(`${inside}
42745
+ `);
42746
+ hash.update(readFileSync7(path));
42747
+ hash.update(`
42748
+ `);
42749
+ }
42750
+ }
42751
+ };
42752
+ for (const name of EDITOR_PLUGINS) {
42753
+ walk(join8(from, name), name);
42754
+ }
42755
+ return hash.digest("hex");
42756
+ }
42757
+ function installedEditorDigest(projectPath) {
42758
+ const marker = join8(projectPath, "addons", ADDONS[0], DIGEST_MARKER);
42759
+ if (!existsSync7(marker)) {
42760
+ return null;
42761
+ }
42762
+ const said = readFileSync7(marker, "utf8").trim();
42763
+ return said === "" ? null : said;
42764
+ }
42578
42765
  function shippedAddonsDirectory() {
42579
42766
  return join8(dirname3(fileURLToPath(import.meta.url)), "godot", "addons");
42580
42767
  }
@@ -42597,6 +42784,8 @@ function installAddons(projectPath, from = shippedAddonsDirectory()) {
42597
42784
  `);
42598
42785
  installed.push({ name, path: target, replaced });
42599
42786
  }
42787
+ writeFileSync5(join8(projectPath, "addons", ADDONS[0], DIGEST_MARKER), `${editorAddonDigest(from)}
42788
+ `);
42600
42789
  return installed;
42601
42790
  }
42602
42791
  function removeAddons(projectPath) {
@@ -42741,7 +42930,7 @@ function inspectProject(projectPath) {
42741
42930
  // src/skill.ts
42742
42931
  init_setup();
42743
42932
  init_tool_definitions();
42744
- import { existsSync as existsSync9, mkdirSync as mkdirSync5, readdirSync as readdirSync3, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "node:fs";
42933
+ import { existsSync as existsSync9, mkdirSync as mkdirSync5, readdirSync as readdirSync5, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "node:fs";
42745
42934
  import { basename, dirname as dirname5, join as join10 } from "node:path";
42746
42935
 
42747
42936
  // src/tool-reference.ts
@@ -42863,7 +43052,8 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
42863
43052
  this server is connected to. There is no ambient project, and an argument a tool does not declare is refused
42864
43053
  rather than ignored.
42865
43054
  - \`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
43055
+ \`addonIsStale\` means the editor is running different addon code from what the server ships
43056
+ (an older \`addonVersion\` on the same code is not stale), and \`staleNote\` says which half is behind: an editor that
42867
43057
  needs restarting, or a server that needs reconnecting in your harness. Every answer that came
42868
43058
  from the editor carries the same two when they differ, so a stale addon cannot answer quietly:
42869
43059
  believe a restart over a diagnostic that arrived with them.
@@ -43050,7 +43240,7 @@ function pruneEmpty(directory, projectPath) {
43050
43240
  const shared = join10(projectPath, ".agents");
43051
43241
  let path = directory;
43052
43242
  while (path !== projectPath && (basename(path) === "skills" || path === shared)) {
43053
- if (readdirSync3(path).length > 0) {
43243
+ if (readdirSync5(path).length > 0) {
43054
43244
  return;
43055
43245
  }
43056
43246
  rmSync5(path, { recursive: true, force: true });
@@ -43074,9 +43264,9 @@ function writeSkill(directories, version) {
43074
43264
  // src/version.ts
43075
43265
  import { readFileSync as readFileSync9 } from "node:fs";
43076
43266
  import { dirname as dirname6, join as join11 } from "node:path";
43077
- import { fileURLToPath as fileURLToPath2 } from "node:url";
43267
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
43078
43268
  function getLocalVersion() {
43079
- const currentDirectory = dirname6(fileURLToPath2(import.meta.url));
43269
+ const currentDirectory = dirname6(fileURLToPath3(import.meta.url));
43080
43270
  const packageCandidates = [
43081
43271
  join11(currentDirectory, "..", "package.json"),
43082
43272
  join11(currentDirectory, "..", "..", "package.json")
@@ -43338,6 +43528,7 @@ async function upgrade() {
43338
43528
  }
43339
43529
  const godot = await engine(projectPath);
43340
43530
  console.log(installed === version ? `already ${version}; reinstalling` : `${installed} -> ${version}`);
43531
+ const digestBefore = installedEditorDigest(projectPath);
43341
43532
  for (const addon of installAddons(projectPath)) {
43342
43533
  console.log(`${addon.replaced ? "replaced" : "installed"} ${addon.path}`);
43343
43534
  }
@@ -43369,8 +43560,10 @@ async function upgrade() {
43369
43560
  console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
43370
43561
  }
43371
43562
  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
43563
+ On ${version}. What is left for you:`);
43564
+ console.log(digestBefore !== null && digestBefore === installedEditorDigest(projectPath) ? ` 1. Nothing for the editor: this upgrade left the editor addons the same code as before,
43565
+ ` + ` so it gives an open editor nothing to restart for. editor_status says addonIsStale
43566
+ ` + " 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
43567
  ` + " editor_launch restart tool, which closes and reopens the window.");
43375
43568
  console.log(harnessNote({ moved, written, byHand }, [launch.command, ...launch.args].join(" ")));
43376
43569
  }