gdharness 0.5.8 → 0.5.10

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
@@ -7229,6 +7229,16 @@ class GameLog {
7229
7229
  this.entries.push(entry);
7230
7230
  this.lastHeadline = headline ? { index: entry.index, detail } : null;
7231
7231
  }
7232
+ record(severity, text) {
7233
+ this.entries.push({
7234
+ index: this.entries.length,
7235
+ severity,
7236
+ source: "debugger",
7237
+ text,
7238
+ detail: []
7239
+ });
7240
+ this.lastHeadline = null;
7241
+ }
7232
7242
  get all() {
7233
7243
  return this.entries;
7234
7244
  }
@@ -30646,6 +30656,47 @@ var init_stdio2 = __esm(() => {
30646
30656
  init_stdio();
30647
30657
  });
30648
30658
 
30659
+ // src/bridge-announce.ts
30660
+ import { mkdirSync as mkdirSync5, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "node:fs";
30661
+ import { dirname as dirname6, join as join11 } from "node:path";
30662
+ function announcementPath(projectPath) {
30663
+ return join11(projectPath, ".godot", "gdharness-bridge.json");
30664
+ }
30665
+ function announceBridge(projectPath, bridge) {
30666
+ const path = announcementPath(projectPath);
30667
+ try {
30668
+ mkdirSync5(dirname6(path), { recursive: true });
30669
+ const announcement = {
30670
+ protocol: BRIDGE_ANNOUNCE_PROTOCOL,
30671
+ host: bridge.host,
30672
+ port: bridge.port,
30673
+ pid: process.pid,
30674
+ version: bridge.version,
30675
+ startedAt: new Date().toISOString()
30676
+ };
30677
+ writeFileSync7(path, `${JSON.stringify(announcement, null, 2)}
30678
+ `, "utf8");
30679
+ return path;
30680
+ } catch (error) {
30681
+ console.error(`[SERVER] Could not announce the editor bridge at ${path}: ${errorMessage(error)}`);
30682
+ return null;
30683
+ }
30684
+ }
30685
+ function withdrawBridge(path) {
30686
+ if (path === null) {
30687
+ return;
30688
+ }
30689
+ try {
30690
+ rmSync6(path, { force: true });
30691
+ } catch (error) {
30692
+ console.error(`[SERVER] Could not withdraw the editor bridge announcement: ${errorMessage(error)}`);
30693
+ }
30694
+ }
30695
+ var BRIDGE_ANNOUNCE_PROTOCOL = 1;
30696
+ var init_bridge_announce = __esm(() => {
30697
+ init_errors();
30698
+ });
30699
+
30649
30700
  // src/framing.ts
30650
30701
  function frame(message) {
30651
30702
  const body = Buffer.from(JSON.stringify(message), "utf8");
@@ -30744,6 +30795,10 @@ var init_ports = __esm(() => {
30744
30795
  // src/dap_client.ts
30745
30796
  import { createConnection } from "node:net";
30746
30797
  import { setTimeout as delay } from "node:timers/promises";
30798
+ function said(body, field) {
30799
+ const value = body?.[field];
30800
+ return typeof value === "string" ? value : "";
30801
+ }
30747
30802
 
30748
30803
  class GodotDAPClient {
30749
30804
  socket = null;
@@ -30758,7 +30813,7 @@ class GodotDAPClient {
30758
30813
  initialized = false;
30759
30814
  attached = false;
30760
30815
  lastThreadId = 1;
30761
- stopped = false;
30816
+ halt = null;
30762
30817
  breakpoints = new Map;
30763
30818
  constructor(port = portFromEnv("GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT), host = "127.0.0.1") {
30764
30819
  this.port = port;
@@ -30807,7 +30862,7 @@ class GodotDAPClient {
30807
30862
  this.connected = false;
30808
30863
  this.initialized = false;
30809
30864
  this.attached = false;
30810
- this.stopped = false;
30865
+ this.halt = null;
30811
30866
  this.socket = null;
30812
30867
  this.failPendingRequests(new Error("DAP connection closed"));
30813
30868
  });
@@ -30827,7 +30882,7 @@ class GodotDAPClient {
30827
30882
  this.connected = false;
30828
30883
  this.initialized = false;
30829
30884
  this.attached = false;
30830
- this.stopped = false;
30885
+ this.halt = null;
30831
30886
  return;
30832
30887
  }
30833
30888
  if (this.connected) {
@@ -30856,7 +30911,7 @@ class GodotDAPClient {
30856
30911
  this.connected = false;
30857
30912
  this.initialized = false;
30858
30913
  this.attached = false;
30859
- this.stopped = false;
30914
+ this.halt = null;
30860
30915
  }
30861
30916
  async ensureConnected() {
30862
30917
  if (!this.connected) {
@@ -30933,12 +30988,16 @@ class GodotDAPClient {
30933
30988
  if (typeof threadId === "number") {
30934
30989
  this.lastThreadId = threadId;
30935
30990
  }
30936
- this.stopped = true;
30991
+ this.halt = {
30992
+ reason: said(body, "reason"),
30993
+ description: said(body, "description"),
30994
+ text: said(body, "text")
30995
+ };
30937
30996
  return;
30938
30997
  }
30939
30998
  if (eventName === "terminated" || eventName === "exited") {
30940
30999
  this.attached = false;
30941
- this.stopped = false;
31000
+ this.halt = null;
30942
31001
  }
30943
31002
  }
30944
31003
  async initialize() {
@@ -31012,7 +31071,7 @@ class GodotDAPClient {
31012
31071
  await this.attach();
31013
31072
  const resolvedThreadId = await this.resolveThreadId(threadId);
31014
31073
  await this.sendRequest("continue", { threadId: resolvedThreadId });
31015
- this.stopped = false;
31074
+ this.halt = null;
31016
31075
  }
31017
31076
  async stepOver(threadId) {
31018
31077
  await this.attach();
@@ -31083,7 +31142,10 @@ class GodotDAPClient {
31083
31142
  return this.connected;
31084
31143
  }
31085
31144
  isStopped() {
31086
- return this.stopped;
31145
+ return this.halt !== null;
31146
+ }
31147
+ whereItStopped() {
31148
+ return this.halt;
31087
31149
  }
31088
31150
  async resolveThreadId(threadId) {
31089
31151
  if (typeof threadId === "number" && threadId > 0) {
@@ -31112,7 +31174,7 @@ class GodotDAPClient {
31112
31174
  this.connected = false;
31113
31175
  this.initialized = false;
31114
31176
  this.attached = false;
31115
- this.stopped = false;
31177
+ this.halt = null;
31116
31178
  this.reader = new FrameReader;
31117
31179
  socket?.destroy();
31118
31180
  this.failPendingRequests(new Error(`Godot DAP ${detail}. The connection was dropped.`));
@@ -34113,8 +34175,8 @@ function resolveDefaultBridgeHost() {
34113
34175
  const host = process.env["GDHARNESS_BRIDGE_HOST"]?.trim();
34114
34176
  return host === undefined || host === "" ? DEFAULT_HOST : host;
34115
34177
  }
34116
- function getDefaultBridge() {
34117
- defaultBridge ??= new GodotBridge(resolveDefaultBridgePort(), resolveDefaultBridgeHost());
34178
+ function getDefaultBridge(mayMove = false) {
34179
+ defaultBridge ??= new GodotBridge(resolveDefaultBridgePort(), resolveDefaultBridgeHost(), DEFAULT_TIMEOUT_MS, mayMove);
34118
34180
  return defaultBridge;
34119
34181
  }
34120
34182
  var DEFAULT_PORT = 6505, DEFAULT_HOST = "127.0.0.1", DEFAULT_TIMEOUT_MS = 30000, KEEPALIVE_INTERVAL_MS = 1e4, SECOND_CONNECTION_CLOSE_CODE = 4000, GodotBridge, defaultBridge = null;
@@ -34130,19 +34192,37 @@ var init_godot_bridge = __esm(() => {
34130
34192
  connectionInfo = null;
34131
34193
  pendingRequests = new Map;
34132
34194
  resourceQueues = new Map;
34133
- port;
34195
+ wantedPort;
34196
+ boundPort = null;
34134
34197
  host;
34135
34198
  timeoutMs;
34136
- constructor(port = DEFAULT_PORT, host = DEFAULT_HOST, timeoutMs = DEFAULT_TIMEOUT_MS) {
34199
+ mayMove;
34200
+ constructor(port = DEFAULT_PORT, host = DEFAULT_HOST, timeoutMs = DEFAULT_TIMEOUT_MS, mayMove = false) {
34137
34201
  super();
34138
- this.port = port;
34202
+ this.wantedPort = port;
34139
34203
  this.host = host;
34140
34204
  this.timeoutMs = timeoutMs;
34205
+ this.mayMove = mayMove;
34141
34206
  }
34142
- start() {
34207
+ get port() {
34208
+ return this.boundPort ?? this.wantedPort;
34209
+ }
34210
+ async start() {
34143
34211
  if (this.httpServer) {
34144
- return Promise.resolve();
34212
+ return;
34145
34213
  }
34214
+ try {
34215
+ await this.listenOn(this.wantedPort);
34216
+ } catch (error) {
34217
+ const held = error instanceof Error && "code" in error && error.code === "EADDRINUSE";
34218
+ if (!held || !this.mayMove) {
34219
+ throw error;
34220
+ }
34221
+ this.log("warn", `Editor bridge port ${this.wantedPort} is held; taking another one.`);
34222
+ await this.listenOn(0);
34223
+ }
34224
+ }
34225
+ listenOn(port) {
34146
34226
  return new Promise((resolve, reject) => {
34147
34227
  const server = http.createServer((_req, res) => {
34148
34228
  res.writeHead(404);
@@ -34166,6 +34246,8 @@ var init_godot_bridge = __esm(() => {
34166
34246
  settled = true;
34167
34247
  this.httpServer = server;
34168
34248
  this.godotWss = godotWss;
34249
+ const bound = server.address();
34250
+ this.boundPort = typeof bound === "object" && bound !== null ? bound.port : port;
34169
34251
  this.log("info", `Editor bridge listening on ${this.host}:${this.port}`);
34170
34252
  resolve();
34171
34253
  });
@@ -34182,7 +34264,7 @@ var init_godot_bridge = __esm(() => {
34182
34264
  godotWss.on("error", (error) => {
34183
34265
  this.log("error", `Godot WebSocket server error: ${error.message}`);
34184
34266
  });
34185
- server.listen(this.port, this.host);
34267
+ server.listen(port, this.host);
34186
34268
  });
34187
34269
  }
34188
34270
  async stop() {
@@ -34216,6 +34298,7 @@ var init_godot_bridge = __esm(() => {
34216
34298
  this.httpServer = null;
34217
34299
  }
34218
34300
  await Promise.all(closeTasks);
34301
+ this.boundPort = null;
34219
34302
  this.connectionInfo = null;
34220
34303
  this.log("info", "WebSocket bridge stopped");
34221
34304
  }
@@ -34865,7 +34948,7 @@ var init_junit = __esm(() => {
34865
34948
  import { realpathSync } from "node:fs";
34866
34949
  import { readFile, realpath } from "node:fs/promises";
34867
34950
  import { createConnection as createConnection2 } from "node:net";
34868
- import { dirname as dirname6, resolve as resolve3 } from "node:path";
34951
+ import { dirname as dirname7, resolve as resolve3 } from "node:path";
34869
34952
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
34870
34953
  function diagnosticsKey(uri) {
34871
34954
  let decoded;
@@ -34991,7 +35074,7 @@ class GodotLSPClient {
34991
35074
  if (this.initialized) {
34992
35075
  return;
34993
35076
  }
34994
- const rootPath = this.rootPath ?? dirname6(resolve3(filePath));
35077
+ const rootPath = this.rootPath ?? dirname7(resolve3(filePath));
34995
35078
  await this.initialize(rootPath);
34996
35079
  }
34997
35080
  async sendRequest(method, params) {
@@ -35366,7 +35449,7 @@ var init_lsp_client = __esm(() => {
35366
35449
 
35367
35450
  // src/project-scan.ts
35368
35451
  import { readdirSync as readdirSync4, readFileSync as readFileSync9 } from "node:fs";
35369
- import { join as join11 } from "node:path";
35452
+ import { join as join12 } from "node:path";
35370
35453
  function projectStructure(projectPath) {
35371
35454
  const structure = { scenes: 0, scripts: 0, assets: 0, other: 0 };
35372
35455
  const visit = (directory) => {
@@ -35375,7 +35458,7 @@ function projectStructure(projectPath) {
35375
35458
  continue;
35376
35459
  }
35377
35460
  if (entry.isDirectory()) {
35378
- visit(join11(directory, entry.name));
35461
+ visit(join12(directory, entry.name));
35379
35462
  } else if (entry.isFile()) {
35380
35463
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
35381
35464
  if (extension === "tscn") {
@@ -35417,7 +35500,7 @@ function searchProject(projectPath, options) {
35417
35500
  if (SKIPPED.has(entry.name)) {
35418
35501
  continue;
35419
35502
  }
35420
- const entryPath = join11(directory, entry.name);
35503
+ const entryPath = join12(directory, entry.name);
35421
35504
  if (entry.isDirectory()) {
35422
35505
  visit(entryPath);
35423
35506
  continue;
@@ -35459,20 +35542,20 @@ var init_project_scan = __esm(() => {
35459
35542
  import { existsSync as existsSync10, readdirSync as readdirSync5, readFileSync as readFileSync10, unlinkSync } from "node:fs";
35460
35543
  import { createConnection as createConnection3 } from "node:net";
35461
35544
  import { tmpdir as tmpdir4 } from "node:os";
35462
- import { join as join12, resolve as resolve4 } from "node:path";
35545
+ import { join as join13, resolve as resolve4 } from "node:path";
35463
35546
  function runtimeDirectory(variables = process.env) {
35464
35547
  const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
35465
35548
  if (explicit) {
35466
35549
  return explicit;
35467
35550
  }
35468
35551
  const perUser = envValue("XDG_RUNTIME_DIR", variables);
35469
- return join12(perUser ?? tmpdir4(), "gdharness");
35552
+ return join13(perUser ?? tmpdir4(), "gdharness");
35470
35553
  }
35471
35554
  function runtimeDirectories(variables = process.env) {
35472
35555
  const candidates = [runtimeDirectory(variables)];
35473
35556
  const fallbacks = process.platform === "win32" ? [envValue("SystemRoot", variables) ?? envValue("windir", variables) ?? "C:\\Windows"] : ["/tmp", "/var/tmp"];
35474
35557
  for (const base of fallbacks) {
35475
- candidates.push(join12(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
35558
+ candidates.push(join13(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
35476
35559
  }
35477
35560
  return [...new Set(candidates.map((path) => resolve4(path)))];
35478
35561
  }
@@ -35535,7 +35618,7 @@ function announcedIn(directory) {
35535
35618
  if (!match) {
35536
35619
  continue;
35537
35620
  }
35538
- const file = join12(directory, entry);
35621
+ const file = join13(directory, entry);
35539
35622
  const pid = Number.parseInt(match[1] ?? "", 10);
35540
35623
  const announced = processAlive(pid) ? parseAnnouncement(file, pid) : { kind: "rubbish" };
35541
35624
  if (announced.kind === "rubbish") {
@@ -35679,11 +35762,31 @@ var init_runtime_client = __esm(() => {
35679
35762
  ANNOUNCEMENT_PATTERN = /^runtime-(\d+)\.json$/;
35680
35763
  });
35681
35764
 
35765
+ // src/setup.ts
35766
+ import { cpSync as cpSync2, existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync11, rmSync as rmSync7, writeFileSync as writeFileSync8 } from "node:fs";
35767
+ import { dirname as dirname8, join as join14 } from "node:path";
35768
+ function installedAddonVersion(projectPath) {
35769
+ const marker = join14(projectPath, "addons", ADDONS2[0], VERSION_MARKER2);
35770
+ if (!existsSync11(marker)) {
35771
+ return null;
35772
+ }
35773
+ const said = readFileSync11(marker, "utf8").trim();
35774
+ return said === "" ? null : said;
35775
+ }
35776
+ var ADDONS2, VERSION_MARKER2 = ".gdharness-version";
35777
+ var init_setup = __esm(() => {
35778
+ init_class_cache();
35779
+ init_headless();
35780
+ init_resources();
35781
+ init_server_version();
35782
+ ADDONS2 = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
35783
+ });
35784
+
35682
35785
  // src/server.ts
35683
35786
  import { execFile as execFile5, spawn } from "node:child_process";
35684
- import { existsSync as existsSync11, mkdtempSync as mkdtempSync3, readdirSync as readdirSync6, readFileSync as readFileSync11, realpathSync as realpathSync2, rmSync as rmSync6 } from "node:fs";
35787
+ import { existsSync as existsSync12, mkdtempSync as mkdtempSync3, readdirSync as readdirSync6, readFileSync as readFileSync12, realpathSync as realpathSync2, rmSync as rmSync8 } from "node:fs";
35685
35788
  import { tmpdir as tmpdir5 } from "node:os";
35686
- import { basename as basename2, dirname as dirname7, join as join13, normalize as normalize3 } from "node:path";
35789
+ import { basename as basename2, dirname as dirname9, join as join15, normalize as normalize3 } from "node:path";
35687
35790
  import { setTimeout as delay2 } from "node:timers/promises";
35688
35791
  import { fileURLToPath as fileURLToPath4 } from "node:url";
35689
35792
  import { promisify as promisify5 } from "node:util";
@@ -35709,11 +35812,11 @@ function realPathOr(path) {
35709
35812
  }
35710
35813
  }
35711
35814
  function hasMainScene(projectFile) {
35712
- const scene = parseProjectGodot(readFileSync11(projectFile, "utf8"))["application"]?.["run/main_scene"];
35815
+ const scene = parseProjectGodot(readFileSync12(projectFile, "utf8"))["application"]?.["run/main_scene"];
35713
35816
  return typeof scene === "string" && scene !== "";
35714
35817
  }
35715
35818
  function editorPlaysHeadless(projectFile) {
35716
- const runArgs = parseProjectGodot(readFileSync11(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
35819
+ const runArgs = parseProjectGodot(readFileSync12(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
35717
35820
  return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
35718
35821
  }
35719
35822
  function camelCased(params) {
@@ -35729,7 +35832,7 @@ function camelCased(params) {
35729
35832
  class GodotServer {
35730
35833
  mcp;
35731
35834
  locator = new GodotLocator2;
35732
- operationsScript = join13(__dirname2, "godot", "operations", "godot_operations.gd");
35835
+ operationsScript = join15(__dirname2, "godot", "operations", "godot_operations.gd");
35733
35836
  godotBridge;
35734
35837
  tools = buildToolDefinitions();
35735
35838
  activeProcess = null;
@@ -35743,8 +35846,11 @@ class GodotServer {
35743
35846
  bridgeRetry = null;
35744
35847
  lastProjectPath = null;
35745
35848
  shutdownInitiated = false;
35849
+ ownProject;
35850
+ announcedAt = null;
35746
35851
  constructor() {
35747
- this.godotBridge = getDefaultBridge();
35852
+ this.ownProject = envValue("GDHARNESS_PROJECT") ?? null;
35853
+ this.godotBridge = getDefaultBridge(this.ownProject !== null);
35748
35854
  this.mcp = new McpServer({ name: "gdharness", version: SERVER_VERSION }, { capabilities: { tools: {}, resources: {} } });
35749
35855
  this.setupToolHandlers();
35750
35856
  setupResourceHandlers(this.mcp, () => this.lastProjectPath);
@@ -35792,6 +35898,7 @@ class GodotServer {
35792
35898
  this.bridgeStartupError = null;
35793
35899
  const bridgeStatus = this.godotBridge.getStatus();
35794
35900
  console.error(`[SERVER] Godot Editor Bridge started on ${bridgeStatus.host}:${bridgeStatus.port}`);
35901
+ this.announceTheBridge();
35795
35902
  } catch (bridgeError) {
35796
35903
  const code = bridgeError instanceof Error && "code" in bridgeError && typeof bridgeError.code === "string" ? bridgeError.code : null;
35797
35904
  const reason = errorMessage(bridgeError);
@@ -35824,6 +35931,18 @@ class GodotServer {
35824
35931
  this.stopTryingTheBridge();
35825
35932
  const bridgeStatus = this.godotBridge.getStatus();
35826
35933
  console.error(`[SERVER] Godot Editor Bridge came up on ${bridgeStatus.host}:${bridgeStatus.port}; editor tools are live.`);
35934
+ this.announceTheBridge();
35935
+ }
35936
+ announceTheBridge() {
35937
+ if (this.ownProject === null) {
35938
+ return;
35939
+ }
35940
+ const status = this.godotBridge.getStatus();
35941
+ this.announcedAt = announceBridge(this.ownProject, {
35942
+ host: status.host,
35943
+ port: status.port,
35944
+ version: SERVER_VERSION
35945
+ });
35827
35946
  }
35828
35947
  stopTryingTheBridge() {
35829
35948
  if (this.bridgeRetry !== null) {
@@ -35834,6 +35953,8 @@ class GodotServer {
35834
35953
  async cleanup() {
35835
35954
  this.logDebug("Cleaning up resources");
35836
35955
  this.stopTryingTheBridge();
35956
+ withdrawBridge(this.announcedAt);
35957
+ this.announcedAt = null;
35837
35958
  if (this.activeProcess) {
35838
35959
  this.activeProcess.process?.kill();
35839
35960
  this.activeProcess = null;
@@ -35949,28 +36070,44 @@ class GodotServer {
35949
36070
  if (this.callsSinceNotice < UPDATE_NOTICE_EVERY && this.noticedUpdate) {
35950
36071
  return answer;
35951
36072
  }
36073
+ const moved = this.projectHasMovedOn();
36074
+ if (moved !== null) {
36075
+ this.noticedUpdate = true;
36076
+ this.callsSinceNotice = 0;
36077
+ return this.saying(answer, {
36078
+ project_upgraded_under_this_server: {
36079
+ server_is: SERVER_VERSION,
36080
+ project_is: moved,
36081
+ what_to_do: "The project was upgraded while this server has been running, so it is still answering " + "as the version it was started as. Tell the user to reconnect the MCP server, which is " + "the only thing that replaces it: a server cannot restart itself."
36082
+ }
36083
+ });
36084
+ }
35952
36085
  const notice = this.updates.notice();
35953
36086
  if (notice === null) {
35954
36087
  return answer;
35955
36088
  }
35956
36089
  this.noticedUpdate = true;
35957
36090
  this.callsSinceNotice = 0;
36091
+ return this.saying(answer, {
36092
+ update_available: {
36093
+ ...notice,
36094
+ what_to_do: "Tell the user a newer gdharness is out, with what changed, and offer to take it. " + "Only run the upgrade command if they say yes: it restarts their editor and the " + "MCP server has to be reconnected afterwards."
36095
+ }
36096
+ });
36097
+ }
36098
+ saying(answer, block) {
35958
36099
  return {
35959
36100
  ...answer,
35960
- content: [
35961
- ...answer.content,
35962
- {
35963
- type: "text",
35964
- text: JSON.stringify({
35965
- update_available: {
35966
- ...notice,
35967
- what_to_do: "Tell the user a newer gdharness is out, with what changed, and offer to take it. " + "Only run the upgrade command if they say yes: it restarts their editor and the " + "MCP server has to be reconnected afterwards."
35968
- }
35969
- }, null, 2)
35970
- }
35971
- ]
36101
+ content: [...answer.content, { type: "text", text: JSON.stringify(block, null, 2) }]
35972
36102
  };
35973
36103
  }
36104
+ projectHasMovedOn() {
36105
+ if (this.ownProject === null) {
36106
+ return null;
36107
+ }
36108
+ const installed = installedAddonVersion(this.ownProject);
36109
+ return installed === null || installed === SERVER_VERSION ? null : installed;
36110
+ }
35974
36111
  withFeedbackNotice(answer) {
35975
36112
  this.callsSinceFeedback += 1;
35976
36113
  if (this.callsSinceFeedback < FEEDBACK_NOTICE_EVERY) {
@@ -36216,8 +36353,8 @@ class GodotServer {
36216
36353
  if (path === undefined) {
36217
36354
  return { ok: false, response: this.createErrorResponse("projectPath is required.") };
36218
36355
  }
36219
- const file = join13(path, "project.godot");
36220
- if (!existsSync11(file)) {
36356
+ const file = join15(path, "project.godot");
36357
+ if (!existsSync12(file)) {
36221
36358
  return {
36222
36359
  ok: false,
36223
36360
  response: this.createErrorResponse(`Not a Godot project: ${path}`, [
@@ -36362,7 +36499,7 @@ class GodotServer {
36362
36499
  if (!engine.ok) {
36363
36500
  return engine.response;
36364
36501
  }
36365
- const application = parseProjectGodot(readFileSync11(project.value.file, "utf8"))["application"] ?? {};
36502
+ const application = parseProjectGodot(readFileSync12(project.value.file, "utf8"))["application"] ?? {};
36366
36503
  const name = application["config/name"];
36367
36504
  const mainScene = application["run/main_scene"];
36368
36505
  const info = {
@@ -36451,7 +36588,7 @@ class GodotServer {
36451
36588
  log.finish();
36452
36589
  const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
36453
36590
  const verdict = {
36454
- exported: exitCode === 0 && log.count("error") === 0 && existsSync11(output.absolutePath),
36591
+ exported: exitCode === 0 && log.count("error") === 0 && existsSync12(output.absolutePath),
36455
36592
  preset,
36456
36593
  outputPath: output.relativePath,
36457
36594
  debug,
@@ -36481,7 +36618,7 @@ class GodotServer {
36481
36618
  return contained.response;
36482
36619
  }
36483
36620
  const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
36484
- if (!existsSync11(join13(project.value.path, runner))) {
36621
+ if (!existsSync12(join15(project.value.path, runner))) {
36485
36622
  return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
36486
36623
  "Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
36487
36624
  ]);
@@ -36514,7 +36651,7 @@ class GodotServer {
36514
36651
  ];
36515
36652
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
36516
36653
  this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
36517
- const userData = mkdtempSync3(join13(tmpdir5(), "gdharness-tests-"));
36654
+ const userData = mkdtempSync3(join15(tmpdir5(), "gdharness-tests-"));
36518
36655
  const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
36519
36656
  const hung = await new Promise((resolve) => {
36520
36657
  const timer = setTimeout(() => {
@@ -36530,20 +36667,20 @@ class GodotServer {
36530
36667
  resolve(false);
36531
36668
  });
36532
36669
  });
36533
- const reportsDir = join13(project.value.path, ".godot", "gdharness-reports");
36670
+ const reportsDir = join15(project.value.path, ".godot", "gdharness-reports");
36534
36671
  let report = null;
36535
36672
  let reportProblem = null;
36536
36673
  try {
36537
- const written = existsSync11(reportsDir) ? readdirSync6(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
36674
+ const written = existsSync12(reportsDir) ? readdirSync6(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
36538
36675
  const newest = written.at(-1);
36539
36676
  if (newest !== undefined) {
36540
- report = parseJUnit(readFileSync11(join13(reportsDir, newest, "results.xml"), "utf8"));
36677
+ report = parseJUnit(readFileSync12(join15(reportsDir, newest, "results.xml"), "utf8"));
36541
36678
  }
36542
36679
  } catch (error) {
36543
36680
  reportProblem = errorMessage(error);
36544
36681
  } finally {
36545
- rmSync6(reportsDir, { recursive: true, force: true });
36546
- rmSync6(userData, { recursive: true, force: true });
36682
+ rmSync8(reportsDir, { recursive: true, force: true });
36683
+ rmSync8(userData, { recursive: true, force: true });
36547
36684
  }
36548
36685
  const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries;
36549
36686
  const verdicts = {
@@ -36656,6 +36793,8 @@ class GodotServer {
36656
36793
  startupError: this.bridgeStartupError,
36657
36794
  staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
36658
36795
  retryingBridge: this.bridgeRetry === null ? undefined : true,
36796
+ announcedAt: this.announcedAt ?? undefined,
36797
+ projectIs: this.projectHasMovedOn() ?? undefined,
36659
36798
  note: isPortConflict ? "Bridge port is already in use. Another gdharness instance owns the editor bridge, so this server cannot reach the editor. Usually the server this one replaced, still on its way out." : undefined,
36660
36799
  suggestion: isPortConflict ? `This server is asking for the port again every ${BRIDGE_RETRY_MS / 1000}s and takes it the moment the other one lets go, so editor tools come back on their own. Ask again, or end the older gdharness process to have it now.` : undefined
36661
36800
  };
@@ -36868,7 +37007,8 @@ class GodotServer {
36868
37007
  log,
36869
37008
  startedAt: Date.now(),
36870
37009
  exitCode: null,
36871
- throughEditor: true
37010
+ throughEditor: true,
37011
+ brokeOn: null
36872
37012
  };
36873
37013
  this.activeProcess = played;
36874
37014
  return this.jsonTextResponse({
@@ -36888,6 +37028,15 @@ class GodotServer {
36888
37028
  `) ? line : `${line}
36889
37029
  `);
36890
37030
  }
37031
+ this.recordWhatItBrokeOn(game);
37032
+ }
37033
+ recordWhatItBrokeOn(game) {
37034
+ const halt = this.dapClient?.whereItStopped() ?? null;
37035
+ if (halt?.reason !== "exception" || halt.text === "" || game.brokeOn === halt.text) {
37036
+ return;
37037
+ }
37038
+ game.brokeOn = halt.text;
37039
+ game.log.record("error", halt.text);
36891
37040
  }
36892
37041
  async endActiveGame() {
36893
37042
  const running = this.activeProcess;
@@ -36909,7 +37058,8 @@ class GodotServer {
36909
37058
  log,
36910
37059
  startedAt: Date.now(),
36911
37060
  exitCode: null,
36912
- throughEditor: false
37061
+ throughEditor: false,
37062
+ brokeOn: null
36913
37063
  };
36914
37064
  child.stdout.on("data", (data) => {
36915
37065
  log.append("stdout", data);
@@ -36974,6 +37124,7 @@ class GodotServer {
36974
37124
  contains: readNonEmptyString(args, "contains"),
36975
37125
  limit: readPositiveNumber(args, "limit") ?? 200
36976
37126
  });
37127
+ const halt = this.activeProcess.throughEditor ? this.dapClient?.whereItStopped() ?? null : null;
36977
37128
  return this.jsonTextResponse({
36978
37129
  running: this.activeProcess.exitCode === null,
36979
37130
  exitCode: this.activeProcess.exitCode,
@@ -36982,6 +37133,7 @@ class GodotServer {
36982
37133
  errors: this.activeProcess.log.count("error"),
36983
37134
  warnings: this.activeProcess.log.count("warning"),
36984
37135
  clean: this.activeProcess.log.count("error") === 0,
37136
+ heldAt: halt,
36985
37137
  omitted: selected.omitted,
36986
37138
  entries: selected.entries
36987
37139
  });
@@ -37051,8 +37203,8 @@ class GodotServer {
37051
37203
  return this.createErrorResponse(choice.problem);
37052
37204
  }
37053
37205
  const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
37054
- const screenshotDir = expectsScreenshot ? mkdtempSync3(join13(tmpdir5(), "gdharness-runtime-screenshot-")) : null;
37055
- const screenshotPath = screenshotDir ? join13(screenshotDir, "capture.png") : null;
37206
+ const screenshotDir = expectsScreenshot ? mkdtempSync3(join15(tmpdir5(), "gdharness-runtime-screenshot-")) : null;
37207
+ const screenshotPath = screenshotDir ? join15(screenshotDir, "capture.png") : null;
37056
37208
  try {
37057
37209
  const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs);
37058
37210
  if (!reply.ok) {
@@ -37073,12 +37225,12 @@ class GodotServer {
37073
37225
  return {
37074
37226
  content: [
37075
37227
  { type: "text", text: `Screenshot captured: ${dimensions}` },
37076
- { type: "image", data: readFileSync11(screenshotPath).toString("base64"), mimeType: "image/png" }
37228
+ { type: "image", data: readFileSync12(screenshotPath).toString("base64"), mimeType: "image/png" }
37077
37229
  ]
37078
37230
  };
37079
37231
  } finally {
37080
37232
  if (screenshotDir) {
37081
- rmSync6(screenshotDir, { recursive: true, force: true });
37233
+ rmSync8(screenshotDir, { recursive: true, force: true });
37082
37234
  }
37083
37235
  }
37084
37236
  }
@@ -37140,6 +37292,7 @@ var init_server3 = __esm(() => {
37140
37292
  init_mcp();
37141
37293
  init_stdio2();
37142
37294
  init_types();
37295
+ init_bridge_announce();
37143
37296
  init_class_cache();
37144
37297
  init_dap_client();
37145
37298
  init_errors();
@@ -37155,10 +37308,11 @@ var init_server3 = __esm(() => {
37155
37308
  init_resources();
37156
37309
  init_runtime_client();
37157
37310
  init_server_version();
37311
+ init_setup();
37158
37312
  init_tool_definitions();
37159
37313
  init_update_check();
37160
37314
  run5 = promisify5(execFile5);
37161
- __dirname2 = dirname7(fileURLToPath4(import.meta.url));
37315
+ __dirname2 = dirname9(fileURLToPath4(import.meta.url));
37162
37316
  PATH_SOLUTIONS = [
37163
37317
  'Give the path relative to the project, such as "scenes/main.tscn" or "res://scenes/main.tscn"',
37164
37318
  "Point projectPath at the project the file belongs to"
@@ -37215,8 +37369,8 @@ var init_server3 = __esm(() => {
37215
37369
  });
37216
37370
 
37217
37371
  // src/cli.ts
37218
- import { existsSync as existsSync12 } from "fs";
37219
- import { join as join14, resolve as resolve5 } from "path";
37372
+ import { existsSync as existsSync13 } from "fs";
37373
+ import { join as join16, resolve as resolve5 } from "path";
37220
37374
 
37221
37375
  // src/errors.ts
37222
37376
  class Refusal extends Error {
@@ -37426,9 +37580,13 @@ function isEmptyCollection(node) {
37426
37580
  init_errors();
37427
37581
  init_runner();
37428
37582
  var SERVER_KEY = "gdharness";
37429
- function launchFor(version, godotPath, runner = currentRunner()) {
37583
+ function launchFor(version, godotPath, projectPath, runner = currentRunner()) {
37430
37584
  const spawn = spawnFor(runner, version);
37431
- return { command: spawn.command, args: spawn.args, env: { GODOT_PATH: godotPath } };
37585
+ return {
37586
+ command: spawn.command,
37587
+ args: spawn.args,
37588
+ env: { GODOT_PATH: godotPath, GDHARNESS_PROJECT: projectPath }
37589
+ };
37432
37590
  }
37433
37591
  function tomlArray(values) {
37434
37592
  return `[${values.map((value) => `"${value}"`).join(", ")}]`;
@@ -38614,7 +38772,7 @@ class UsageError extends Refusal {
38614
38772
  function projectArgument(at) {
38615
38773
  const named = args.slice(at).find((value) => !value.startsWith("--"));
38616
38774
  const projectPath = resolve5(named ?? ".");
38617
- if (!existsSync12(join14(projectPath, "project.godot"))) {
38775
+ if (!existsSync13(join16(projectPath, "project.godot"))) {
38618
38776
  throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
38619
38777
  }
38620
38778
  return projectPath;
@@ -38626,7 +38784,7 @@ async function engine() {
38626
38784
  }
38627
38785
  return { godotPath, script: shippedOperationsScript(), debug: GODOT_DEBUG_MODE_DEFAULT2 };
38628
38786
  }
38629
- function said(outcome, what) {
38787
+ function said2(outcome, what) {
38630
38788
  if (!outcome.ok) {
38631
38789
  throw new Refusal(`${what}: ${outcome.message}`);
38632
38790
  }
@@ -38727,20 +38885,20 @@ async function setup() {
38727
38885
  const enabled = await enablePlugins(godot, projectPath, EDITOR_PLUGINS);
38728
38886
  for (const [index, outcome] of enabled.entries()) {
38729
38887
  const name = EDITOR_PLUGINS[index] ?? "";
38730
- said(outcome, `enabling ${name}`);
38888
+ said2(outcome, `enabling ${name}`);
38731
38889
  console.log(`${name}: ${outcome.ok ? String(outcome.payload["action"]) : "failed"}`);
38732
38890
  }
38733
38891
  if (runtime) {
38734
- said(await setRuntime(godot, projectPath, true), "registering the runtime autoload");
38892
+ said2(await setRuntime(godot, projectPath, true), "registering the runtime autoload");
38735
38893
  console.log(`${RUNTIME_AUTOLOAD.name} autoload registered`);
38736
38894
  }
38737
38895
  const classes = await runOperation(godot, "refresh_class_cache", {}, projectPath);
38738
- said(classes, "rebuilding the class list");
38896
+ said2(classes, "rebuilding the class list");
38739
38897
  if (classes.ok) {
38740
38898
  console.log(`class list rebuilt: ${String(classes.payload["classes"])} classes`);
38741
38899
  }
38742
38900
  if (!args.includes("--no-connect")) {
38743
- const launch = launchFor(getLocalVersion(), godot.godotPath);
38901
+ const launch = launchFor(getLocalVersion(), godot.godotPath, projectPath);
38744
38902
  const ask = new Ask;
38745
38903
  let chosen;
38746
38904
  try {
@@ -38758,7 +38916,7 @@ async function setup() {
38758
38916
  console.log("no harness configured; name one yourself, and gdharness harnesses lists them all");
38759
38917
  }
38760
38918
  if (!args.includes("--no-skill")) {
38761
- const directories = skillDirectories(chosen.harnesses, projectPath, existsSync12);
38919
+ const directories = skillDirectories(chosen.harnesses, projectPath, existsSync13);
38762
38920
  for (const written of writeSkill(directories, getLocalVersion())) {
38763
38921
  console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
38764
38922
  }
@@ -38797,11 +38955,11 @@ async function uninstall() {
38797
38955
  const disabled = await disablePlugins(godot, projectPath, enabled);
38798
38956
  for (const [index, outcome] of disabled.entries()) {
38799
38957
  const name = enabled[index] ?? "";
38800
- said(outcome, `disabling ${name}`);
38958
+ said2(outcome, `disabling ${name}`);
38801
38959
  console.log(`${name}: ${outcome.ok ? String(outcome.payload["action"]) : "failed"}`);
38802
38960
  }
38803
38961
  if (before.runtimeAutoload) {
38804
- said(await setRuntime(godot, projectPath, false), "removing the runtime autoload");
38962
+ said2(await setRuntime(godot, projectPath, false), "removing the runtime autoload");
38805
38963
  console.log(`${RUNTIME_AUTOLOAD.name} autoload removed`);
38806
38964
  }
38807
38965
  const addons = removeAddons(projectPath);
@@ -38829,13 +38987,13 @@ async function upgrade() {
38829
38987
  }
38830
38988
  const enabled = await enablePlugins(godot, projectPath, EDITOR_PLUGINS);
38831
38989
  for (const [index, outcome] of enabled.entries()) {
38832
- said(outcome, `enabling ${EDITOR_PLUGINS[index] ?? ""}`);
38990
+ said2(outcome, `enabling ${EDITOR_PLUGINS[index] ?? ""}`);
38833
38991
  }
38834
38992
  if (before.runtimeAutoload) {
38835
- said(await setRuntime(godot, projectPath, true), "registering the runtime autoload");
38993
+ said2(await setRuntime(godot, projectPath, true), "registering the runtime autoload");
38836
38994
  }
38837
- said(await runOperation(godot, "refresh_class_cache", {}, projectPath), "rebuilding the class list");
38838
- const launch = launchFor(version, godot.godotPath);
38995
+ said2(await runOperation(godot, "refresh_class_cache", {}, projectPath), "rebuilding the class list");
38996
+ const launch = launchFor(version, godot.godotPath, projectPath);
38839
38997
  const already = HARNESSES.filter((harness) => registered(harness, projectPath));
38840
38998
  for (const group of groupByFile(already, projectPath)) {
38841
38999
  reportConnection(group, launch, projectPath);
@@ -38843,7 +39001,7 @@ async function upgrade() {
38843
39001
  if (already.length === 0) {
38844
39002
  console.log("no harness config names gdharness, so none was re-pinned");
38845
39003
  }
38846
- for (const written of writeSkill(skillDirectories(already, projectPath, existsSync12), version)) {
39004
+ for (const written of writeSkill(skillDirectories(already, projectPath, existsSync13), version)) {
38847
39005
  console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
38848
39006
  }
38849
39007
  console.log(`
@@ -38879,14 +39037,14 @@ async function runtime3() {
38879
39037
  }
38880
39038
  const projectPath = projectArgument(2);
38881
39039
  const godot = await engine();
38882
- said(await setRuntime(godot, projectPath, state === "on"), `turning the runtime ${state}`);
39040
+ said2(await setRuntime(godot, projectPath, state === "on"), `turning the runtime ${state}`);
38883
39041
  console.log(`${RUNTIME_AUTOLOAD.name} autoload ${state === "on" ? "registered" : "removed"}`);
38884
39042
  }
38885
39043
  async function classes() {
38886
39044
  const projectPath = projectArgument(1);
38887
39045
  const godot = await engine();
38888
39046
  const outcome = await runOperation(godot, "refresh_class_cache", {}, projectPath);
38889
- said(outcome, "rebuilding the class list");
39047
+ said2(outcome, "rebuilding the class list");
38890
39048
  if (outcome.ok) {
38891
39049
  console.log(JSON.stringify(outcome.payload, null, 2));
38892
39050
  }