gdharness 1.2.24 → 1.2.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.js CHANGED
@@ -3359,21 +3359,21 @@ function defectReport(where, error, context = {}) {
3359
3359
  import { execFile as execFile5, spawn as spawn4 } from "node:child_process";
3360
3360
  import { randomUUID as randomUUID2 } from "node:crypto";
3361
3361
  import {
3362
- appendFileSync,
3362
+ appendFileSync as appendFileSync2,
3363
3363
  closeSync as closeSync2,
3364
3364
  constants,
3365
3365
  copyFileSync,
3366
3366
  existsSync as existsSync17,
3367
3367
  fstatSync,
3368
- mkdirSync as mkdirSync13,
3368
+ mkdirSync as mkdirSync14,
3369
3369
  openSync as openSync3,
3370
3370
  readdirSync as readdirSync9,
3371
3371
  readFileSync as readFileSync21,
3372
3372
  readSync as readSync2,
3373
3373
  rmdirSync as rmdirSync2,
3374
- statSync as statSync8
3374
+ statSync as statSync9
3375
3375
  } from "node:fs";
3376
- import { basename as basename5, dirname as dirname15, extname as extname3, isAbsolute as isAbsolute3, join as join22, normalize as normalize2, relative as relative4 } from "node:path";
3376
+ import { basename as basename5, dirname as dirname16, extname as extname3, isAbsolute as isAbsolute3, join as join23, normalize as normalize2, relative as relative4 } from "node:path";
3377
3377
  import { setTimeout as delay4 } from "node:timers/promises";
3378
3378
  import { fileURLToPath as fileURLToPath4 } from "node:url";
3379
3379
  import { promisify as promisify5 } from "node:util";
@@ -24888,7 +24888,7 @@ import { dirname as dirname2, join as join3 } from "node:path";
24888
24888
  // src/runtime-client.ts
24889
24889
  import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3, statSync, unlinkSync } from "node:fs";
24890
24890
  import { createConnection } from "node:net";
24891
- import { tmpdir as tmpdir2 } from "node:os";
24891
+ import { tmpdir as tmpdir3 } from "node:os";
24892
24892
  import { dirname, join as join2, resolve as resolve2 } from "node:path";
24893
24893
  import { setTimeout as delay } from "node:timers/promises";
24894
24894
 
@@ -25105,6 +25105,7 @@ import { promisify } from "node:util";
25105
25105
 
25106
25106
  // src/windows-ask.ts
25107
25107
  import { spawn } from "node:child_process";
25108
+ import { tmpdir as tmpdir2 } from "node:os";
25108
25109
  import process6 from "node:process";
25109
25110
  var SCRIPT = [
25110
25111
  "[Console]::OutputEncoding = [Text.Encoding]::UTF8",
@@ -25159,7 +25160,7 @@ function running() {
25159
25160
  "Bypass",
25160
25161
  "-EncodedCommand",
25161
25162
  Buffer.from(SCRIPT, "utf16le").toString("base64")
25162
- ], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
25163
+ ], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true, cwd: tmpdir2() });
25163
25164
  buffered = "";
25164
25165
  child.stdout.setEncoding("utf8");
25165
25166
  child.stdout.on("data", (chunk) => {
@@ -25556,7 +25557,7 @@ function runtimeDirectory(variables = process.env) {
25556
25557
  return explicit;
25557
25558
  }
25558
25559
  const perUser = envValue("XDG_RUNTIME_DIR", variables);
25559
- return join2(perUser ?? tmpdir2(), "gdharness");
25560
+ return join2(perUser ?? tmpdir3(), "gdharness");
25560
25561
  }
25561
25562
  function runtimeDirectories(variables = process.env) {
25562
25563
  const candidates = [runtimeDirectory(variables)];
@@ -29498,7 +29499,7 @@ function importRecord(projectPath, resourcePath) {
29498
29499
 
29499
29500
  // src/scratch.ts
29500
29501
  import { existsSync as existsSync10, mkdtempSync, readdirSync as readdirSync4, rmSync as rmSync6 } from "node:fs";
29501
- import { tmpdir as tmpdir3 } from "node:os";
29502
+ import { tmpdir as tmpdir4 } from "node:os";
29502
29503
  import { join as join13 } from "node:path";
29503
29504
  import process7 from "node:process";
29504
29505
  var SWEPT = { recursive: true, force: true };
@@ -29514,9 +29515,9 @@ var KINDS = [
29514
29515
  "tests"
29515
29516
  ];
29516
29517
  function scratchDirectory(kind) {
29517
- return mkdtempSync(join13(tmpdir3(), `gdharness-${kind}-${process7.pid}-`));
29518
+ return mkdtempSync(join13(tmpdir4(), `gdharness-${kind}-${process7.pid}-`));
29518
29519
  }
29519
- function sweepAbandonedScratch(directory = tmpdir3()) {
29520
+ function sweepAbandonedScratch(directory = tmpdir4()) {
29520
29521
  let entries;
29521
29522
  try {
29522
29523
  entries = readdirSync4(directory);
@@ -29609,17 +29610,25 @@ function snakeCased(params) {
29609
29610
  return result;
29610
29611
  }
29611
29612
  function engineExtras(outcome) {
29613
+ const after = outcome.afterAnswerMessages ?? [];
29612
29614
  return {
29613
29615
  ...outcome.messages.length > 0 ? { engine_messages: outcome.messages } : {},
29614
29616
  ...outcome.messagesOmitted === undefined ? {} : { engine_messages_omitted: outcome.messagesOmitted },
29617
+ ...after.length > 0 ? { engine_messages_after_answer: after, engine_messages_after_answer_note: AFTER_ANSWER_NOTE } : {},
29618
+ ...outcome.afterAnswerOmitted === undefined ? {} : { engine_messages_after_answer_omitted: outcome.afterAnswerOmitted },
29615
29619
  ...outcome.afterAnswer === undefined ? {} : { engine_after_answer: outcome.afterAnswer }
29616
29620
  };
29617
29621
  }
29622
+ var AFTER_ANSWER_NOTE = "Printed after this operation had written its answer, while the engine ran the project's autoloads and shut down: Godot starts a project's autoloads in every script run and cannot be told not to. An error raised in a project script here is that autoload's own, and none of these changes the answer.";
29618
29623
  var OWN_ERROR = "[ERROR] ";
29624
+ var ANSWERED = "[gdharness] answered";
29619
29625
  var MESSAGES_KEPT = 200;
29620
29626
  var ERRORS_NAMED = 3;
29621
29627
  function problemsIn(log) {
29622
- return log.everything().flatMap((entry) => {
29628
+ return problemsAmong(log.everything());
29629
+ }
29630
+ function problemsAmong(entries) {
29631
+ return entries.flatMap((entry) => {
29623
29632
  if (entry.severity !== "info") {
29624
29633
  return [entry];
29625
29634
  }
@@ -29748,12 +29757,20 @@ async function runOperation(engine, operation, params, projectPath) {
29748
29757
  } finally {
29749
29758
  discard(paramsDir);
29750
29759
  }
29751
- const problems = problemsIn(ran.log);
29760
+ const everything = ran.log.everything();
29761
+ const answered = everything.findIndex((entry) => entry.source === "stderr" && entry.text === ANSWERED);
29762
+ const problems = problemsAmong(answered === -1 ? everything : everything.slice(0, answered));
29763
+ const after = answered === -1 ? [] : problemsAmong(everything.slice(answered + 1));
29764
+ const afterAnswered = {
29765
+ ...after.length === 0 ? {} : { afterAnswerMessages: after.slice(-MESSAGES_KEPT) },
29766
+ ...after.length > MESSAGES_KEPT ? { afterAnswerOmitted: after.length - MESSAGES_KEPT } : {}
29767
+ };
29752
29768
  if (answer.kind === "unreadable") {
29753
29769
  return {
29754
29770
  ok: false,
29755
29771
  message: `${operation} wrote an answer that could not be read (${answer.why})`,
29756
- ...reported2(problems)
29772
+ ...reported2(problems),
29773
+ ...afterAnswered
29757
29774
  };
29758
29775
  }
29759
29776
  if (answer.kind === "none") {
@@ -29769,14 +29786,16 @@ async function runOperation(engine, operation, params, projectPath) {
29769
29786
  return {
29770
29787
  ok: false,
29771
29788
  message: `${operation} hit an error in the operations script, so its answer is left out: part of it was ` + `never computed. ${fault.text} (${fault.detail[0]?.slice("at: ".length) ?? "no location given"})`,
29772
- ...reported2(problems)
29789
+ ...reported2(problems),
29790
+ ...afterAnswered
29773
29791
  };
29774
29792
  }
29775
- const afterAnswer = ran.exitCode === 0 && ran.failure === null ? undefined : `The operation finished and wrote this answer, and then the engine did not exit cleanly (${howItEnded(ran)}). Anything it reported on the way out is under engine_messages.`;
29793
+ const afterAnswer = ran.exitCode === 0 && ran.failure === null ? undefined : `The operation finished and wrote this answer, and then the engine did not exit cleanly (${howItEnded(ran)}). Anything it reported on the way out is under ${answered === -1 ? "engine_messages" : "engine_messages_after_answer"}.`;
29776
29794
  return {
29777
29795
  ok: true,
29778
29796
  payload: answer.payload,
29779
29797
  ...reported2(problems),
29798
+ ...afterAnswered,
29780
29799
  ...afterAnswer === undefined ? {} : { afterAnswer }
29781
29800
  };
29782
29801
  }
@@ -30333,11 +30352,54 @@ function askTheKeeperToStop(address, withinMs) {
30333
30352
  });
30334
30353
  }
30335
30354
 
30355
+ // src/lifecycle-log.ts
30356
+ import { appendFileSync, mkdirSync as mkdirSync8, renameSync, statSync as statSync6 } from "node:fs";
30357
+ import { dirname as dirname9, join as join15 } from "node:path";
30358
+ import process8 from "node:process";
30359
+ function lifecycleLogPath() {
30360
+ return join15(runtimeDirectory(), "servers.log");
30361
+ }
30362
+ var LIFECYCLE_LOG_LIMIT = 1e6;
30363
+ function writeLifecycle(event, fields = {}) {
30364
+ const path = lifecycleLogPath();
30365
+ try {
30366
+ mkdirSync8(dirname9(path), { recursive: true });
30367
+ if ((statSync6(path, { throwIfNoEntry: false })?.size ?? 0) > LIFECYCLE_LOG_LIMIT) {
30368
+ try {
30369
+ renameSync(path, `${path}.1`);
30370
+ } catch {}
30371
+ }
30372
+ const line = {
30373
+ at: new Date().toISOString(),
30374
+ pid: process8.pid,
30375
+ ppid: process8.ppid,
30376
+ version: SERVER_VERSION,
30377
+ event
30378
+ };
30379
+ for (const [name, value] of Object.entries(fields)) {
30380
+ if (!Object.hasOwn(line, name)) {
30381
+ line[name] = value;
30382
+ }
30383
+ }
30384
+ appendFileSync(path, `${JSON.stringify(line)}
30385
+ `, "utf8");
30386
+ } catch {
30387
+ console.error(`[SERVER] Could not write the lifecycle log at ${path}.`);
30388
+ }
30389
+ }
30390
+ function callsInFlight(calls, now = Date.now()) {
30391
+ return [...calls].map((call) => ({
30392
+ tool: call.tool,
30393
+ ...call.op === "" ? {} : { op: call.op },
30394
+ runningMs: now - call.startedAt
30395
+ }));
30396
+ }
30397
+
30336
30398
  // src/lsp_client.ts
30337
30399
  import { existsSync as existsSync12, realpathSync as realpathSync3 } from "node:fs";
30338
30400
  import { readFile, realpath } from "node:fs/promises";
30339
30401
  import { createConnection as createConnection3 } from "node:net";
30340
- import { dirname as dirname9, join as join15, resolve as resolve3 } from "node:path";
30402
+ import { dirname as dirname10, join as join16, resolve as resolve3 } from "node:path";
30341
30403
  import { setTimeout as delay3 } from "node:timers/promises";
30342
30404
  import { fileURLToPath, pathToFileURL } from "node:url";
30343
30405
  var DEFAULT_LSP_PORT = 6005;
@@ -30535,7 +30597,7 @@ class GodotLSPClient {
30535
30597
  if (this.initialized) {
30536
30598
  return;
30537
30599
  }
30538
- const rootPath = this.rootPath ?? dirname9(resolve3(filePath));
30600
+ const rootPath = this.rootPath ?? dirname10(resolve3(filePath));
30539
30601
  await this.initialize(rootPath);
30540
30602
  }
30541
30603
  async sendRequest(method, params, timeoutMs = this.timeouts.requestMs) {
@@ -31093,7 +31155,7 @@ async function resolveLSPPaths(projectPathValue, scriptPathValue) {
31093
31155
  } catch {
31094
31156
  throw new ArgumentRefusal(`Project path does not exist: ${requestedProjectPath}`);
31095
31157
  }
31096
- if (!existsSync12(join15(projectPath, "project.godot"))) {
31158
+ if (!existsSync12(join16(projectPath, "project.godot"))) {
31097
31159
  throw new ArgumentRefusal(`Not a Godot project: ${projectPath}. Point projectPath at the directory holding project.godot.`);
31098
31160
  }
31099
31161
  const contained = resolveWithinProject(projectPath, scriptPathValue);
@@ -31301,7 +31363,7 @@ function secondsFromClock(printed) {
31301
31363
 
31302
31364
  // src/project-scan.ts
31303
31365
  import { existsSync as existsSync13, readdirSync as readdirSync5, readFileSync as readFileSync14 } from "node:fs";
31304
- import { extname, join as join16 } from "node:path";
31366
+ import { extname, join as join17 } from "node:path";
31305
31367
  var SKIPPED = new Set(["node_modules"]);
31306
31368
  function skipped(name) {
31307
31369
  return name.startsWith(".") || SKIPPED.has(name);
@@ -31311,10 +31373,10 @@ function steppedOver(directory) {
31311
31373
  return gdignored(directory) || installedByUs(directory);
31312
31374
  }
31313
31375
  function gdignored(directory) {
31314
- return existsSync13(join16(directory, ".gdignore"));
31376
+ return existsSync13(join17(directory, ".gdignore"));
31315
31377
  }
31316
31378
  function installedByUs(directory) {
31317
- return existsSync13(join16(directory, VERSION_MARKER));
31379
+ return existsSync13(join17(directory, VERSION_MARKER));
31318
31380
  }
31319
31381
  var SCENE_EXTENSIONS = new Set(["tscn", "scn"]);
31320
31382
  var ASSET_EXTENSIONS = new Set([
@@ -31358,7 +31420,7 @@ function projectStructure(projectPath) {
31358
31420
  continue;
31359
31421
  }
31360
31422
  if (entry.isDirectory()) {
31361
- visit(join16(directory, entry.name));
31423
+ visit(join17(directory, entry.name));
31362
31424
  } else if (entry.isFile()) {
31363
31425
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
31364
31426
  if (SCENE_EXTENSIONS.has(extension)) {
@@ -31388,7 +31450,7 @@ function projectFiles(projectPath, extensions) {
31388
31450
  }
31389
31451
  const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
31390
31452
  if (entry.isDirectory()) {
31391
- visit(join16(directory, entry.name), spelled);
31453
+ visit(join17(directory, entry.name), spelled);
31392
31454
  } else if (entry.isFile() && extensions.includes(extname(entry.name).toLowerCase())) {
31393
31455
  found.push(spelled);
31394
31456
  }
@@ -31410,14 +31472,14 @@ function scriptsWithoutUid(projectPath) {
31410
31472
  }
31411
31473
  const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
31412
31474
  if (entry.isDirectory()) {
31413
- visit(join16(directory, entry.name), spelled);
31475
+ visit(join17(directory, entry.name), spelled);
31414
31476
  continue;
31415
31477
  }
31416
31478
  if (!entry.isFile()) {
31417
31479
  continue;
31418
31480
  }
31419
31481
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
31420
- if (CARRIES_A_UID.has(extension) && !existsSync13(join16(directory, `${entry.name}.uid`))) {
31482
+ if (CARRIES_A_UID.has(extension) && !existsSync13(join17(directory, `${entry.name}.uid`))) {
31421
31483
  found.push(spelled);
31422
31484
  }
31423
31485
  }
@@ -31449,7 +31511,7 @@ function projectTexts(projectPath) {
31449
31511
  if (skipped(entry.name)) {
31450
31512
  continue;
31451
31513
  }
31452
- const absolute = join16(directory, entry.name);
31514
+ const absolute = join17(directory, entry.name);
31453
31515
  const spelled = `${prefix}${entry.name}`;
31454
31516
  if (entry.isDirectory()) {
31455
31517
  visit(absolute, `${spelled}/`);
@@ -31479,7 +31541,7 @@ function ignoredTexts(projectPath) {
31479
31541
  if (skipped(entry.name)) {
31480
31542
  continue;
31481
31543
  }
31482
- const absolute = join16(directory, entry.name);
31544
+ const absolute = join17(directory, entry.name);
31483
31545
  const spelled = `${prefix}${entry.name}`;
31484
31546
  if (entry.isDirectory()) {
31485
31547
  visit(absolute, `${spelled}/`, under);
@@ -31519,7 +31581,7 @@ function searchProject(projectPath, options) {
31519
31581
  if (skipped(entry.name)) {
31520
31582
  continue;
31521
31583
  }
31522
- const entryPath = join16(directory, entry.name);
31584
+ const entryPath = join17(directory, entry.name);
31523
31585
  if (entry.isDirectory()) {
31524
31586
  visit(entryPath);
31525
31587
  continue;
@@ -31690,8 +31752,8 @@ function setupPromptHandlers(server) {
31690
31752
  }
31691
31753
 
31692
31754
  // src/rename.ts
31693
- import { existsSync as existsSync14, mkdirSync as mkdirSync8, readFileSync as readFileSync15, renameSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync10 } from "node:fs";
31694
- import { dirname as dirname10, posix as posix2 } from "node:path";
31755
+ import { existsSync as existsSync14, mkdirSync as mkdirSync9, readFileSync as readFileSync15, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync10 } from "node:fs";
31756
+ import { dirname as dirname11, posix as posix2 } from "node:path";
31695
31757
  class Script {
31696
31758
  path;
31697
31759
  text;
@@ -32424,15 +32486,15 @@ function applyRewrites(rewrites) {
32424
32486
  const done = [];
32425
32487
  try {
32426
32488
  for (const rewrite of rewrites) {
32427
- mkdirSync8(dirname10(rewrite.writeTo), { recursive: true });
32489
+ mkdirSync9(dirname11(rewrite.writeTo), { recursive: true });
32428
32490
  const staging = `${rewrite.writeTo}.gdharness-rename`;
32429
32491
  writeFileSync10(staging, rewrite.after, "utf8");
32430
- renameSync(staging, rewrite.writeTo);
32492
+ renameSync2(staging, rewrite.writeTo);
32431
32493
  let movedUid = false;
32432
32494
  if (rewrite.writeTo !== rewrite.absolute) {
32433
32495
  unlinkSync2(rewrite.absolute);
32434
32496
  if (existsSync14(`${rewrite.absolute}.uid`)) {
32435
- renameSync(`${rewrite.absolute}.uid`, `${rewrite.writeTo}.uid`);
32497
+ renameSync2(`${rewrite.absolute}.uid`, `${rewrite.writeTo}.uid`);
32436
32498
  movedUid = true;
32437
32499
  }
32438
32500
  }
@@ -32445,7 +32507,7 @@ function applyRewrites(rewrites) {
32445
32507
  if (rewrite.writeTo !== rewrite.absolute) {
32446
32508
  unlinkSync2(rewrite.writeTo);
32447
32509
  if (movedUid) {
32448
- renameSync(`${rewrite.writeTo}.uid`, `${rewrite.absolute}.uid`);
32510
+ renameSync2(`${rewrite.writeTo}.uid`, `${rewrite.absolute}.uid`);
32449
32511
  }
32450
32512
  }
32451
32513
  } catch {}
@@ -32455,8 +32517,8 @@ function applyRewrites(rewrites) {
32455
32517
  }
32456
32518
 
32457
32519
  // src/rename-symbol.ts
32458
- import { existsSync as existsSync15, mkdirSync as mkdirSync9, readdirSync as readdirSync6, realpathSync as realpathSync5, rmSync as rmSync7, writeFileSync as writeFileSync11 } from "node:fs";
32459
- import { basename as basename3, dirname as dirname11, join as join17, relative as relative3, resolve as resolve5 } from "node:path";
32520
+ import { existsSync as existsSync15, mkdirSync as mkdirSync10, readdirSync as readdirSync6, realpathSync as realpathSync5, rmSync as rmSync7, writeFileSync as writeFileSync11 } from "node:fs";
32521
+ import { basename as basename3, dirname as dirname12, join as join18, relative as relative3, resolve as resolve5 } from "node:path";
32460
32522
 
32461
32523
  // src/resources.ts
32462
32524
  import { readFileSync as readFileSync16, realpathSync as realpathSync4 } from "node:fs";
@@ -32764,9 +32826,9 @@ function onDiskSpelling(path) {
32764
32826
  let at = resolve5(path);
32765
32827
  for (;; ) {
32766
32828
  try {
32767
- return join17(realpathSync5.native(at), ...missing.reverse());
32829
+ return join18(realpathSync5.native(at), ...missing.reverse());
32768
32830
  } catch {
32769
- const parent = dirname11(at);
32831
+ const parent = dirname12(at);
32770
32832
  if (parent === at) {
32771
32833
  return resolve5(path);
32772
32834
  }
@@ -33194,15 +33256,15 @@ ${mention.kind}`;
33194
33256
  var LEFT_ALONE_ANSWER_LINES = 300;
33195
33257
  var detailWrites = 0;
33196
33258
  function writeLeftAloneDetail(projectPath, symbol, detail) {
33197
- const directory = join17(projectPath, ".godot", "gdharness-renames");
33198
- mkdirSync9(directory, { recursive: true });
33259
+ const directory = join18(projectPath, ".godot", "gdharness-renames");
33260
+ mkdirSync10(directory, { recursive: true });
33199
33261
  detailWrites += 1;
33200
- const path = join17(directory, `${String(Date.now())}-${String(detailWrites).padStart(6, "0")}-${String(process.pid)}-${symbol}.json`);
33262
+ const path = join18(directory, `${String(Date.now())}-${String(detailWrites).padStart(6, "0")}-${String(process.pid)}-${symbol}.json`);
33201
33263
  writeFileSync11(path, `${JSON.stringify(detail, null, 2)}
33202
33264
  `, "utf8");
33203
33265
  const kept = readdirSync6(directory).filter((name) => name.endsWith(".json")).sort().reverse();
33204
33266
  for (const old of kept.slice(10)) {
33205
- rmSync7(join17(directory, old), { force: true });
33267
+ rmSync7(join18(directory, old), { force: true });
33206
33268
  }
33207
33269
  return path;
33208
33270
  }
@@ -33225,15 +33287,15 @@ function summaryOf(edits, mentions, rewrites, left) {
33225
33287
  }
33226
33288
 
33227
33289
  // src/restart-note.ts
33228
- import { mkdirSync as mkdirSync10, readFileSync as readFileSync17, rmSync as rmSync8, writeFileSync as writeFileSync12 } from "node:fs";
33229
- import { dirname as dirname12, join as join18 } from "node:path";
33290
+ import { mkdirSync as mkdirSync11, readFileSync as readFileSync17, rmSync as rmSync8, writeFileSync as writeFileSync12 } from "node:fs";
33291
+ import { dirname as dirname13, join as join19 } from "node:path";
33230
33292
  function restartNotePath(projectPath) {
33231
- return join18(projectPath, ".godot", "gdharness-restart.json");
33293
+ return join19(projectPath, ".godot", "gdharness-restart.json");
33232
33294
  }
33233
33295
  function noteRestartBegun(note) {
33234
33296
  const path = restartNotePath(note.projectPath);
33235
33297
  try {
33236
- mkdirSync10(dirname12(path), { recursive: true });
33298
+ mkdirSync11(dirname13(path), { recursive: true });
33237
33299
  writeFileSync12(path, `${JSON.stringify(note, null, 2)}
33238
33300
  `, "utf8");
33239
33301
  } catch (error) {
@@ -33266,12 +33328,12 @@ function restartSettled(projectPath) {
33266
33328
  // src/run-record.ts
33267
33329
  import { execFile as execFile4 } from "node:child_process";
33268
33330
  import { createHash as createHash3 } from "node:crypto";
33269
- import { mkdirSync as mkdirSync11, openSync as openSync2, readdirSync as readdirSync7, readFileSync as readFileSync18, realpathSync as realpathSync6, rmSync as rmSync10, statSync as statSync6 } from "node:fs";
33270
- import { basename as basename4, dirname as dirname13, join as join19, resolve as resolve6 } from "node:path";
33331
+ import { mkdirSync as mkdirSync12, openSync as openSync2, readdirSync as readdirSync7, readFileSync as readFileSync18, realpathSync as realpathSync6, rmSync as rmSync10, statSync as statSync7 } from "node:fs";
33332
+ import { basename as basename4, dirname as dirname14, join as join20, resolve as resolve6 } from "node:path";
33271
33333
  import { promisify as promisify4 } from "node:util";
33272
33334
 
33273
33335
  // src/whole-file.ts
33274
- import { renameSync as renameSync2, rmSync as rmSync9, writeFileSync as writeFileSync13 } from "node:fs";
33336
+ import { renameSync as renameSync3, rmSync as rmSync9, writeFileSync as writeFileSync13 } from "node:fs";
33275
33337
  var HELD_FILE_MS = 1000;
33276
33338
  var HELD_FILE_RETRY_MS = 10;
33277
33339
  var HELD_BY_A_READER = new Set(["EPERM", "EACCES", "EBUSY"]);
@@ -33285,7 +33347,7 @@ function writeWhole(path, text) {
33285
33347
  const until = Date.now() + HELD_FILE_MS;
33286
33348
  for (;; ) {
33287
33349
  try {
33288
- renameSync2(partial, path);
33350
+ renameSync3(partial, path);
33289
33351
  return true;
33290
33352
  } catch (error) {
33291
33353
  const code = error.code ?? "";
@@ -33303,16 +33365,16 @@ function writeWhole(path, text) {
33303
33365
  // src/run-record.ts
33304
33366
  var execFileAsync = promisify4(execFile4);
33305
33367
  function runsDirectory() {
33306
- return join19(runtimeDirectory(), "runs");
33368
+ return join20(runtimeDirectory(), "runs");
33307
33369
  }
33308
33370
  function realAsFarAsItExists(path) {
33309
33371
  const missing = [];
33310
33372
  let at = path;
33311
33373
  for (;; ) {
33312
33374
  try {
33313
- return join19(realpathSync6.native(at), ...missing);
33375
+ return join20(realpathSync6.native(at), ...missing);
33314
33376
  } catch {
33315
- const parent = dirname13(at);
33377
+ const parent = dirname14(at);
33316
33378
  if (parent === at) {
33317
33379
  return path;
33318
33380
  }
@@ -33327,14 +33389,14 @@ function projectKey(projectPath) {
33327
33389
  return createHash3("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
33328
33390
  }
33329
33391
  function runRecordPath(projectPath, directory = runtimeDirectory()) {
33330
- return join19(directory, "runs", `note-${projectKey(projectPath)}.json`);
33392
+ return join20(directory, "runs", `note-${projectKey(projectPath)}.json`);
33331
33393
  }
33332
33394
  function latestRunPath(servedBy, directory = runtimeDirectory()) {
33333
- return join19(directory, "runs", `latest-${projectKey(servedBy)}.json`);
33395
+ return join20(directory, "runs", `latest-${projectKey(servedBy)}.json`);
33334
33396
  }
33335
33397
  function writeLatestRun(servedBy, latest) {
33336
33398
  try {
33337
- mkdirSync11(runsDirectory(), { recursive: true });
33399
+ mkdirSync12(runsDirectory(), { recursive: true });
33338
33400
  writeWhole(latestRunPath(servedBy), JSON.stringify(latest, null, 2));
33339
33401
  } catch {}
33340
33402
  }
@@ -33355,21 +33417,21 @@ function readLatestRun(servedBy, directory = runtimeDirectory()) {
33355
33417
  }
33356
33418
  var SHARED_NOTE = "run.json";
33357
33419
  function openTranscript(startedAt) {
33358
- mkdirSync11(runsDirectory(), { recursive: true });
33359
- const path = join19(runsDirectory(), `run-${startedAt}.log`);
33420
+ mkdirSync12(runsDirectory(), { recursive: true });
33421
+ const path = join20(runsDirectory(), `run-${startedAt}.log`);
33360
33422
  return { path, fd: openSync2(path, "a") };
33361
33423
  }
33362
33424
  function profilePath(startedAt) {
33363
- return join19(runsDirectory(), `run-${startedAt}.profile.json`);
33425
+ return join20(runsDirectory(), `run-${startedAt}.profile.json`);
33364
33426
  }
33365
33427
  function noteIsAbout(note, playing) {
33366
33428
  return note.play !== undefined && playing !== null && note.play.play === playing.play && note.play.editorPid === playing.editorPid;
33367
33429
  }
33368
33430
  function editorNotePath(projectPath, directory = runtimeDirectory()) {
33369
- return join19(directory, "runs", `editor-${projectKey(projectPath)}.json`);
33431
+ return join20(directory, "runs", `editor-${projectKey(projectPath)}.json`);
33370
33432
  }
33371
33433
  function writeEditorRunNote(note) {
33372
- mkdirSync11(runsDirectory(), { recursive: true });
33434
+ mkdirSync12(runsDirectory(), { recursive: true });
33373
33435
  const path = editorNotePath(note.projectPath);
33374
33436
  if (!writeWhole(path, JSON.stringify(note, null, 2))) {
33375
33437
  throw new Error(`the editor run note could not be written at ${path}`);
@@ -33377,7 +33439,7 @@ function writeEditorRunNote(note) {
33377
33439
  }
33378
33440
  function readEditorRunNote(projectPath) {
33379
33441
  for (const directory of runtimeDirectories()) {
33380
- const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join19(directory, "runs", "editor-run.json"));
33442
+ const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join20(directory, "runs", "editor-run.json"));
33381
33443
  if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
33382
33444
  return note;
33383
33445
  }
@@ -33411,7 +33473,7 @@ function readRunRecord(projectPath) {
33411
33473
  if (own !== null) {
33412
33474
  return own;
33413
33475
  }
33414
- const shared = recordAt(join19(directory, "runs", SHARED_NOTE));
33476
+ const shared = recordAt(join20(directory, "runs", SHARED_NOTE));
33415
33477
  if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
33416
33478
  return shared;
33417
33479
  }
@@ -33423,13 +33485,13 @@ function everyRunRecord() {
33423
33485
  for (const directory of runtimeDirectories()) {
33424
33486
  let entries;
33425
33487
  try {
33426
- entries = readdirSync7(join19(directory, "runs"));
33488
+ entries = readdirSync7(join20(directory, "runs"));
33427
33489
  } catch {
33428
33490
  continue;
33429
33491
  }
33430
33492
  for (const entry of entries) {
33431
33493
  if (entry === SHARED_NOTE || entry.startsWith("note-") && entry.endsWith(".json")) {
33432
- const record = recordAt(join19(directory, "runs", entry));
33494
+ const record = recordAt(join20(directory, "runs", entry));
33433
33495
  if (record !== null) {
33434
33496
  found.push(record);
33435
33497
  }
@@ -33640,7 +33702,7 @@ function judgeAnnouncedGame(running, projectPath, announcedAt) {
33640
33702
  }
33641
33703
  function clearRunRecord(projectPath) {
33642
33704
  for (const directory of runtimeDirectories()) {
33643
- const shared = join19(directory, "runs", SHARED_NOTE);
33705
+ const shared = join20(directory, "runs", SHARED_NOTE);
33644
33706
  const sharedRecord = recordAt(shared);
33645
33707
  const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
33646
33708
  for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
@@ -33652,7 +33714,7 @@ function clearRunRecord(projectPath) {
33652
33714
  }
33653
33715
  function ageMs(path, now) {
33654
33716
  try {
33655
- return now - statSync6(path).mtimeMs;
33717
+ return now - statSync7(path).mtimeMs;
33656
33718
  } catch {
33657
33719
  return 0;
33658
33720
  }
@@ -33674,7 +33736,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
33674
33736
  if (!transcript && !entry.endsWith(".partial")) {
33675
33737
  continue;
33676
33738
  }
33677
- const path = join19(directory, entry);
33739
+ const path = join20(directory, entry);
33678
33740
  if (kept.has(path) || ageMs(path, now) < keepMs) {
33679
33741
  continue;
33680
33742
  }
@@ -33685,7 +33747,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
33685
33747
  }
33686
33748
 
33687
33749
  // src/script-origins.ts
33688
- import { join as join20 } from "node:path";
33750
+ import { join as join21 } from "node:path";
33689
33751
  var UNRESOLVED = /Could not resolve class "([A-Za-z_][A-Za-z0-9_]*)", because of a parser error/;
33690
33752
  function unresolvedClassIn(message) {
33691
33753
  return UNRESOLVED.exec(message)?.[1] ?? null;
@@ -33741,7 +33803,7 @@ function checkWithEngine(godotPath, projectPath) {
33741
33803
  const ran = await runEngine(godotPath, [
33742
33804
  "--headless",
33743
33805
  "--log-file",
33744
- join20(logDir, "engine.log"),
33806
+ join21(logDir, "engine.log"),
33745
33807
  "--path",
33746
33808
  projectPath,
33747
33809
  "--check-only",
@@ -34859,7 +34921,7 @@ var TOOL_SPECS = [
34859
34921
  {
34860
34922
  name: "editor_launch",
34861
34923
  title: "Open, restart or close the editor",
34862
- description: "Opens the Godot editor on a project, or restarts or closes the one already connected. close saves open scenes, quits the editor and waits for its process to go, which is the only way to end a hidden one short of killing it; settingsDropped in its answer says what that save took out of project.godot. open refuses while an editor of the project is already running and has not reached this server, naming its pid, rather than putting a second on the project. The editor is an ordinary visible one on the desktop in use, and on Windows it is shown without being activated, so opening or reopening it does not take the keyboard from whatever the user is typing in. hidden: true opens it headless instead and, on Windows, on a desktop of its own, where the games it plays go too; a hidden editor serves the bridge, the language server and the debug adapter as a visible one does. A game a visible editor plays opens as a window on the desktop in use. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect, and nothing else here restarts one; it saves open scenes on the way out and answers with the version that came back. An editor a server opened is started again by the server, visible unless hidden: true is passed again, once the old one has gone: it waits 90 seconds for that, or GDHARNESS_EDITOR_RESTART_TIMEOUT_MS, and an old editor still running after it is not replaced, since the new one would find its ports held. One opened by hand restarts itself, which only one with a window can do, because the engine hands back none of the arguments it was started with. editor_status says which editor is connected and whether it is holding an old addon. Opening a project is a save and so is closing one, which is the part that is not obvious from outside: the editor imports the project on open and writes project.godot back, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped either way. A restart says which under settingsDropped in its own answer because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one. A restart of an editor this server opened is a quit and then a launch, and this server can be ended between them, usually by the reconnect the restart was being taken for: the editor is gone and nothing replaces it. That is written down before the quit, so the next server says so under restartInterrupted in editor_status rather than waiting for an editor that is not coming, and open finishes it, saying so under finishesRestart. An open is refused when the editor it would start could never reach this server: a project other than the one this server serves, whose editor dials that project's own server; a project without the gdharness editor addon installed and enabled; and a project whose editor a restart is already bringing back.",
34924
+ description: "Opens the Godot editor on a project, or restarts or closes the one already connected. close saves open scenes, quits the editor and waits for its process to go, which is the only way to end a hidden one short of killing it; settingsDropped in its answer says what that save took out of project.godot. An editor this server opened that has not connected yet has nothing of this server's to save, so close stops its process instead and answers beforeConnecting. open answers once the process has started, and the editor dials in after its import; a call needing the editor made before then waits for the one this server opened, for as long as a restart waits, and is refused naming it if it has not arrived. open refuses while an editor of the project is already running and has not reached this server, naming its pid, rather than putting a second on the project. The editor is an ordinary visible one on the desktop in use, and on Windows it is shown without being activated, so opening or reopening it does not take the keyboard from whatever the user is typing in. hidden: true opens it headless instead and, on Windows, on a desktop of its own, where the games it plays go too; a hidden editor serves the bridge, the language server and the debug adapter as a visible one does. A game a visible editor plays opens as a window on the desktop in use. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect, and nothing else here restarts one; it saves open scenes on the way out and answers with the version that came back. An editor a server opened is started again by the server, visible unless hidden: true is passed again, once the old one has gone: it waits 90 seconds for that, or GDHARNESS_EDITOR_RESTART_TIMEOUT_MS, and an old editor still running after it is not replaced, since the new one would find its ports held. One opened by hand restarts itself, which only one with a window can do, because the engine hands back none of the arguments it was started with. editor_status says which editor is connected and whether it is holding an old addon. Opening a project is a save and so is closing one, which is the part that is not obvious from outside: the editor imports the project on open and writes project.godot back, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped either way. A restart says which under settingsDropped in its own answer because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one. A restart of an editor this server opened is a quit and then a launch, and this server can be ended between them, usually by the reconnect the restart was being taken for: the editor is gone and nothing replaces it. That is written down before the quit, so the next server says so under restartInterrupted in editor_status rather than waiting for an editor that is not coming, and open finishes it, saying so under finishesRestart. An open is refused when the editor it would start could never reach this server: a project other than the one this server serves, whose editor dials that project's own server; a project without the gdharness editor addon installed and enabled; and a project whose editor a restart is already bringing back.",
34863
34925
  parameters: {
34864
34926
  projectPath: { ...PROJECT_PATH2, ops: ["open"] },
34865
34927
  hidden: {
@@ -35014,6 +35076,10 @@ var TOOL_SPECS = [
35014
35076
  title: "Game and editor output",
35015
35077
  description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. For a run the editor plays the reports come from the game itself, through the runtime addon, since the editor's debugger relays what a game prints and not what it reports; a played game without the runtime addon answers here with its prints alone, with errorsUnread true and no clean, since what it raised reaches nothing here. Answers with the counts and the verdict as well as the entries: clean is true when no error was reported, and warnings, counted beside it, do not make it false. heldAt is three answers: null for a run this server knows is not held, running or over, an object for one it knows is held at a breakpoint, whether the adapter said so or the runtime did by accepting a connection and never answering, and absent with heldUnknown beside it for a server that connected to the debugger after the stop and has no runtime to ask, since the adapter reports a stop only to the sessions connected when it happened. A run that has quit still answers here, with running false and its exit code, until the next one starts; a run ended by a signal has no exit code, and exitSignal names the signal instead. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over. running is asked of the operating system whenever there is a process to ask about, which is every run this server started and every editor-played run whose game announced its runtime. For an editor-played run that never announced, there is no process id here and running is the editor's answer, which can lag: one measured elsewhere reported a game as playing for fifteen seconds after its process had been ended from outside. editor_status names that game under runtimes, which does not depend on the editor answering. op: \"editor\" answers about the editor itself instead, which is a different log and the only way anything here can read what an editor printed: its console reaches no plugin, so the addon cannot scrape it, and an editor gdharness opened is asked for a log file as it starts. That covers the whole session from the editor's first line, which is where the output worth reading usually is, since the class cache, the plugins and the language server all say what they have to say before any harness has connected. The answer carries the entries in the order they were printed and, beside them under repeated, each message shape several lines share: how many, what varied in each slot of it and how many different values stood there, the first occurrence, and the ordinary lines above the group, found by walking back past the group rather than by counting lines. An editor somebody opened by hand has no console here and is refused saying so rather than answered empty, because an editor that printed nothing and an editor nobody captured are not the same thing. startedAt is when the run began and elapsedMs how long it has been going; for a run that is over, endedAt is when it ended and elapsedMs how long it ran, and both are left out when nobody saw the end, as for a game closed in the editor or a run picked up from a note written before ends were recorded.",
35016
35078
  parameters: {
35079
+ projectPath: {
35080
+ type: "string",
35081
+ description: "The project whose output you mean to read. It picks nothing, since this reads the one run or editor this server holds, and a call naming another project than that one's is refused rather than answered with output from a different game."
35082
+ },
35017
35083
  severity: {
35018
35084
  type: "string",
35019
35085
  enum: ["error", "warning", "info"],
@@ -35065,7 +35131,7 @@ var TOOL_SPECS = [
35065
35131
  title: "Editor status",
35066
35132
  effect: "read",
35067
35133
  description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and which games with the runtime addon are reachable, of this project and of every other on the machine. Read connected with mayYetConnect: the editor dials this server and backs off between tries, so for the first half-minute after a server starts false means not reached yet as often as no editor; mayYetConnect true is worth waiting out, false is final, and restartInterrupted, restartUnderway, closedHere and editorsNotYetConnected say why. An editor that has connected and not yet greeted reports nothing about itself, and every other call made in that window waits up to ten seconds for it to settle. playingInEditor null means unanswered rather than nothing playing: processActive and runtimes do not depend on the editor answering. game.run names the run editor_output and editor_run stop answer for, and stays named after it ends. addonIsStale, on the editor and on each game, says it runs addon code other than what this server ships, and staleNote says which half moves: gdharness upgrade in the project, a restart, or a reconnect of this server. A false editor.lspListening or editor.dapListening comes with serversNote, and editor_launch restart binds the port again. editor.scan says whether the editor is scanning, which editor_run waits out before starting a game. A game announced in a protocol this server does not speak is listed under unreadable, during an upgrade that moves the protocol, until the halves match.",
35068
- answer: "runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it. It stays named after the run ends, beside processActive false, so editor_output can still read what that run printed; null when there is no run at all. startedAt says when it began, and endedAt when it ended, wherever something saw the end: a run adopted from a note another server wrote can have ended hours before this server started. editor.lspListening and editor.dapListening say whether anything accepts connections on the ports the editor names, lspPort and dapPort: Godot binds each once, as the editor starts, and reports a failure only in the log panel of the editor itself, so a false comes with serversNote, and editor_launch restart binds it again. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and while that is read identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window.",
35134
+ answer: "server names this server: its pid, its version, and lifecycleLog, the file every gdharness server of the user appends a line to when it starts, when its client first names itself, when it replaces another server of its project, and when it ends, with why it ended (the client closing its end as stdin:end, a signal by name, superseded with the successor's pid, an uncaught exception or rejection with its stack, or the exit code) and the calls it was answering then. The parent's command line follows on its own line a few seconds after the start. A server killed without warning writes no ended line, and on Windows every kill is that, so a started line with no ended line after it is a server something killed. " + "runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it. It stays named after the run ends, beside processActive false, so editor_output can still read what that run printed; null when there is no run at all. startedAt says when it began, and endedAt when it ended, wherever something saw the end: a run adopted from a note another server wrote can have ended hours before this server started. editor.lspListening and editor.dapListening say whether anything accepts connections on the ports the editor names, lspPort and dapPort: Godot binds each once, as the editor starts, and reports a failure only in the log panel of the editor itself, so a false comes with serversNote, and editor_launch restart binds it again. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and while that is read identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window.",
35069
35135
  parameters: {},
35070
35136
  requires: []
35071
35137
  },
@@ -35268,7 +35334,7 @@ var TOOL_SPECS = [
35268
35334
  value: {
35269
35335
  ops: ["set"],
35270
35336
  blank: true,
35271
- description: `set: the value, fitted to the property's type. "" writes an empty string. A property typed as or holding an object takes a path naming one the game holds, the way call's args do, and holds that instance, or null, which the text null also means, to empty it. One declared as Variant or untyped takes anything, as the engine does: a value of the kind it holds is fitted to it, an int staying an int, and any other is stored as sent. A typed list or map, such as Array[int] or Dictionary[String, int], takes a JSON list or object with each element fitted to its element type, and an element that cannot become one is refused by its index or key, as is an object of another script than the one the list holds, whether or not that script has a class_name.`
35337
+ description: `set: the value, fitted to the property's type. A vector, colour or the like may be written as its fields, {"x": 140, "y": 300}, or tagged, {"_type": "Vector2", "x": 140, "y": 300}, which is the form a read answers with. "" writes an empty string. A property typed as or holding an object takes a path naming one the game holds, the way call's args do, and holds that instance, or null, which the text null also means, to empty it. One declared as Variant or untyped takes anything, as the engine does: a value of the kind it holds is fitted to it, an int staying an int, and any other is stored as sent. A typed list or map, such as Array[int] or Dictionary[String, int], takes a JSON list or object with each element fitted to its element type, and an element that cannot become one is refused by its index or key, as is an object of another script than the one the list holds, whether or not that script has a class_name.`
35272
35338
  },
35273
35339
  method: {
35274
35340
  type: "string",
@@ -35278,7 +35344,7 @@ var TOOL_SPECS = [
35278
35344
  args: {
35279
35345
  type: "array",
35280
35346
  ops: ["call"],
35281
- description: `call: the arguments, fitted to the method's parameter types. A parameter typed as an object takes a path naming one the game holds and is handed that instance: a colon path read from nodePath, "_game:run:wares:3", or one starting at a node, "/root/Main/Hud" or "/root/Main:_game:run". A path that reaches no object, or an object of another class than the parameter declares, is refused, and so are too few or too many arguments. The engine names a parameter typed as a script class without class_name by the class the script extends, so an object of that class with another script passes that check and the engine refuses the call: the answer says the method did not run, with the engine's reason. A parameter typed as a list or map, such as Array[int] or Array[Gear], is built from a JSON list or object element by element, objects named by their paths. The method itself takes its arguments here only: brackets on it holding arguments are refused.`
35347
+ description: `call: the arguments, fitted to the method's parameter types. A vector, colour or the like may be written as its fields, {"x": 140, "y": 300}, or tagged, {"_type": "Vector2", "x": 140, "y": 300}. A parameter typed as an object takes a path naming one the game holds and is handed that instance: a colon path read from nodePath, "_game:run:wares:3", or one starting at a node, "/root/Main/Hud" or "/root/Main:_game:run". A path that reaches no object, or an object of another class than the parameter declares, is refused, and so are too few or too many arguments. The engine names a parameter typed as a script class without class_name by the class the script extends, so an object of that class with another script passes that check and the engine refuses the call: the answer says the method did not run, with the engine's reason. A parameter typed as a list or map, such as Array[int] or Array[Gear], is built from a JSON list or object element by element, objects named by their paths. The method itself takes its arguments here only: brackets on it holding arguments are refused.`
35282
35348
  },
35283
35349
  properties: {
35284
35350
  type: ["boolean", "array"],
@@ -35372,7 +35438,7 @@ var TOOL_SPECS = [
35372
35438
  name: "runtime_input",
35373
35439
  title: "Send input to the game",
35374
35440
  description: "Input to the running game: a whole click on a Control or a 3D node named by path or on a control named by the words on it, an item chosen out of a menu, typing into whatever has the focus, or a raw action, key, mouse button or mouse motion. All of it works headless, where the window is 64 by 64 and the GUI only takes what is inside it. On gdharness's own desktop, where a windowed run goes, Windows refuses to say where the pointer is: every pointer answer there carries pointer_on_hidden_desktop, and the first of the run pointer_note saying why it matters: get_mouse_position() on the root window, and the mouse positions derived from it, read a meaningless position, while what reads the event itself (the control under it, hover, tooltips, input handlers) gets the injected position.",
35375
- answer: 'click: what was under the pointer, and what became of the control: in_tree, removed or freed. Named by says, found says which match it was, and found.pressedOn says "the words" for words in a RichTextLabel pressed where they are drawn, or "the middle of the label" with a note saying why they could not be placed; on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control scrolled into view first is scrolled each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A button in toggle mode, a CheckBox or CheckButton included, answers pressed with before and after, its button_pressed either side of the click: the same value twice is a click the game undid, and after is null when the click freed it. A refusal for a pointer turned off names where mouse_behavior_recursive was set. On a 3D node landed says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room. choose: what was chosen, read before the press, which is heard rather than assumed; what became of the control named (in_tree, removed or freed, since a game can rebuild its screen on the pick); and what the button in front of the menu shows now when it is still there. A menu is opened rather than clicked because its items are drawn rather than built, and the item is looked for as the menu stands once open, since a game can fill a menu as it opens.',
35441
+ answer: 'click: what was under the pointer, and what became of the control: in_tree, removed or freed. Named by says, found says which match it was, and found.pressedOn says "the words" for words in a RichTextLabel pressed where they are drawn, or "the middle of the label" with a note saying why they could not be placed; on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control scrolled into view first is scrolled each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A button in toggle mode, a CheckBox or CheckButton included, answers pressed with before and after, its button_pressed either side of the click: the same value twice is a click the game undid, and after is null when the click freed it. A refusal for a pointer turned off names where mouse_behavior_recursive was set. On a 3D node landed says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room. choose: what was chosen, read before the press, which is heard rather than assumed; what became of the control named (in_tree, removed or freed, since a game can rebuild its screen on the pick); and what the button in front of the menu shows now when it is still there. A menu is opened rather than clicked because its items are drawn rather than built, and the item is looked for as the menu stands once open, since a game can fill a menu as it opens. action, key and mouse_click: the event as it was sent, with whole saying whether the press was let go of again, and for a key the keycode, physical_keycode and unicode it carried. mouse_motion: the position and the relative movement sent. None of these is aimed at anything, so none says what the event reached: click on a nodePath or says answers what was under the pointer.',
35376
35442
  parameters: {
35377
35443
  projectPath: RUNNING_PROJECT_PATH,
35378
35444
  pid: RUNNING_PID,
@@ -35696,15 +35762,15 @@ import { createHash as createHash4 } from "node:crypto";
35696
35762
  import {
35697
35763
  cpSync,
35698
35764
  existsSync as existsSync16,
35699
- mkdirSync as mkdirSync12,
35765
+ mkdirSync as mkdirSync13,
35700
35766
  readdirSync as readdirSync8,
35701
35767
  readFileSync as readFileSync20,
35702
35768
  rmdirSync,
35703
35769
  rmSync as rmSync11,
35704
- statSync as statSync7,
35770
+ statSync as statSync8,
35705
35771
  writeFileSync as writeFileSync14
35706
35772
  } from "node:fs";
35707
- import { dirname as dirname14, join as join21 } from "node:path";
35773
+ import { dirname as dirname15, join as join22 } from "node:path";
35708
35774
  import { fileURLToPath as fileURLToPath3 } from "node:url";
35709
35775
  var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
35710
35776
  var EDITOR_PLUGINS = ["gdharness_editor", "auto_reload"];
@@ -35727,9 +35793,9 @@ function addonsDigest(names, from) {
35727
35793
  if (name === VERSION_MARKER || name === DIGEST_MARKER) {
35728
35794
  continue;
35729
35795
  }
35730
- const path = join21(directory, name);
35796
+ const path = join22(directory, name);
35731
35797
  const inside = `${relative}/${name}`;
35732
- if (statSync7(path).isDirectory()) {
35798
+ if (statSync8(path).isDirectory()) {
35733
35799
  walk(path, inside);
35734
35800
  } else {
35735
35801
  hash.update(`${inside}
@@ -35741,7 +35807,7 @@ function addonsDigest(names, from) {
35741
35807
  }
35742
35808
  };
35743
35809
  for (const name of names) {
35744
- walk(join21(from, name), name);
35810
+ walk(join22(from, name), name);
35745
35811
  }
35746
35812
  return hash.digest("hex");
35747
35813
  }
@@ -35774,7 +35840,7 @@ function installedRuntimeDigest(projectPath) {
35774
35840
  return digestMarker(projectPath, RUNTIME_ADDON);
35775
35841
  }
35776
35842
  function digestMarker(projectPath, addon) {
35777
- const marker = join21(projectPath, "addons", addon, DIGEST_MARKER);
35843
+ const marker = join22(projectPath, "addons", addon, DIGEST_MARKER);
35778
35844
  if (!existsSync16(marker)) {
35779
35845
  return null;
35780
35846
  }
@@ -35782,10 +35848,10 @@ function digestMarker(projectPath, addon) {
35782
35848
  return said === "" ? null : said;
35783
35849
  }
35784
35850
  function shippedAddonsDirectory() {
35785
- return join21(dirname14(fileURLToPath3(import.meta.url)), "godot", "addons");
35851
+ return join22(dirname15(fileURLToPath3(import.meta.url)), "godot", "addons");
35786
35852
  }
35787
35853
  function installedAddonVersion(projectPath) {
35788
- const marker = join21(projectPath, "addons", ADDONS[0], VERSION_MARKER);
35854
+ const marker = join22(projectPath, "addons", ADDONS[0], VERSION_MARKER);
35789
35855
  if (!existsSync16(marker)) {
35790
35856
  return null;
35791
35857
  }
@@ -35800,7 +35866,7 @@ function loaderAmong(projectPath, named) {
35800
35866
  }
35801
35867
  let source = null;
35802
35868
  try {
35803
- source = readFileSync20(join21(projectPath, path), "utf8");
35869
+ source = readFileSync20(join22(projectPath, path), "utf8");
35804
35870
  } catch {}
35805
35871
  if (source?.includes(RUNTIME_AUTOLOAD.path)) {
35806
35872
  return { name: entry, path, exists: true };
@@ -35842,8 +35908,8 @@ function enabledPlugins(settings) {
35842
35908
  function inspectProject(projectPath) {
35843
35909
  const problems = [];
35844
35910
  const addons = ADDONS.map((name) => {
35845
- const directory = join21(projectPath, "addons", name);
35846
- const marker = join21(directory, VERSION_MARKER);
35911
+ const directory = join22(projectPath, "addons", name);
35912
+ const marker = join22(directory, VERSION_MARKER);
35847
35913
  const installed = existsSync16(directory);
35848
35914
  const version = existsSync16(marker) ? readFileSync20(marker, "utf8").trim() : null;
35849
35915
  const current = version === SERVER_VERSION;
@@ -35854,7 +35920,7 @@ function inspectProject(projectPath) {
35854
35920
  }
35855
35921
  return { name, installed, version, current };
35856
35922
  });
35857
- const settings = parseProjectGodot(readFileSync20(join21(projectPath, "project.godot"), "utf8"));
35923
+ const settings = parseProjectGodot(readFileSync20(join22(projectPath, "project.godot"), "utf8"));
35858
35924
  const pluginsEnabled = enabledPlugins(settings);
35859
35925
  for (const name of EDITOR_PLUGINS) {
35860
35926
  if (!pluginsEnabled.includes(name)) {
@@ -35870,7 +35936,7 @@ function inspectProject(projectPath) {
35870
35936
  const tracked = trackedByGit(projectPath, [...named.values()]);
35871
35937
  const addonMissing = (path) => addons.some((addon) => !addon.installed && path.startsWith(`addons/${addon.name}/`));
35872
35938
  for (const [name, path] of named) {
35873
- if (!existsSync16(join21(projectPath, path))) {
35939
+ if (!existsSync16(join22(projectPath, path))) {
35874
35940
  if (!addonMissing(path)) {
35875
35941
  problems.push(`the ${name} autoload names res://${path}, which is not in this project, so it boots with a missing script; restore that file, or point the autoload at one that is here`);
35876
35942
  }
@@ -35921,7 +35987,8 @@ function argumentShape(schema) {
35921
35987
  var UPDATE_NOTICE_EVERY = 500;
35922
35988
  var FEEDBACK_NOTICE_EVERY = 250;
35923
35989
  var run4 = promisify5(execFile5);
35924
- var __dirname2 = dirname15(fileURLToPath4(import.meta.url));
35990
+ var __dirname2 = dirname16(fileURLToPath4(import.meta.url));
35991
+ var PARENT_ASKED_AFTER_MS = 5000;
35925
35992
  function editorRestartTimeoutMs() {
35926
35993
  return waitFromEnv("GDHARNESS_EDITOR_RESTART_TIMEOUT_MS", 90000);
35927
35994
  }
@@ -35958,11 +36025,11 @@ function writtenFileRefusal(path, rule) {
35958
36025
  if (!rule.extensions.includes(extname3(path).toLowerCase())) {
35959
36026
  return `${named} ${path} does not end in ${rule.extensions.join(" or ")}, which is what ${what} is.`;
35960
36027
  }
35961
- const directory = realPathOr(dirname15(path));
36028
+ const directory = realPathOr(dirname16(path));
35962
36029
  if (!existsSync17(directory)) {
35963
- return `${named}'s directory ${dirname15(path)} does not exist; ${what} creates no directories.`;
36030
+ return `${named}'s directory ${dirname16(path)} does not exist; ${what} creates no directories.`;
35964
36031
  }
35965
- const where = join22(directory, basename5(path));
36032
+ const where = join23(directory, basename5(path));
35966
36033
  const inside = rule.projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
35967
36034
  if (inside !== undefined) {
35968
36035
  return `${named} ${path} is inside the project at ${inside}; ${what} is saved outside it, so it never becomes part of the game.`;
@@ -36512,6 +36579,23 @@ function engineFlagsNote(args) {
36512
36579
  var EDITOR_FREE_INSTEAD = {
36513
36580
  scene_tree: "runtime_inspect tree reads the nodes of a game that is running, and needs no editor; scene_tree reads a scene file through the editor"
36514
36581
  };
36582
+ function editorAddonMissing(projectPath) {
36583
+ if (!existsSync17(join23(projectPath, "project.godot"))) {
36584
+ return null;
36585
+ }
36586
+ const report = inspectProject(projectPath);
36587
+ const installed = report.addons.some((addon) => addon.name === "gdharness_editor" && addon.installed);
36588
+ if (installed && report.pluginsEnabled.includes("gdharness_editor")) {
36589
+ return null;
36590
+ }
36591
+ return {
36592
+ why: `the gdharness editor addon is not ${installed ? "enabled" : "installed"} in ${projectPath}`,
36593
+ remedies: installed ? [
36594
+ "gdharness setup in the project enables it",
36595
+ "Or enable it under Project Settings, Plugins, in an editor opened by hand"
36596
+ ] : ["gdharness setup in the project installs and enables it"]
36597
+ };
36598
+ }
36515
36599
  var OPS_ELSEWHERE = {
36516
36600
  runtime_invoke: { get: "runtime_inspect property", property: "runtime_inspect property" }
36517
36601
  };
@@ -36778,7 +36862,7 @@ function profileFrameFunctions(projectPath) {
36778
36862
  let projectGodot = null;
36779
36863
  if (projectPath !== null) {
36780
36864
  try {
36781
- projectGodot = readFileSync21(join22(projectPath, "project.godot"), "utf8");
36865
+ projectGodot = readFileSync21(join23(projectPath, "project.godot"), "utf8");
36782
36866
  } catch {}
36783
36867
  }
36784
36868
  return frameFunctionsFor(queuedMessagesOf(projectGodot));
@@ -36956,7 +37040,7 @@ function foldedEntriesAnswer(log, floor = "warning") {
36956
37040
  var ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd";
36957
37041
  function gdUnitFailsOnScriptErrors(projectPath) {
36958
37042
  try {
36959
- return settingKeys(readFileSync21(join22(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
37043
+ return settingKeys(readFileSync21(join23(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
36960
37044
  } catch {
36961
37045
  return true;
36962
37046
  }
@@ -36971,7 +37055,7 @@ function uncaughtNote(uncaught) {
36971
37055
  }
36972
37056
  function elsewhereIn(projectPath, asked) {
36973
37057
  const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
36974
- if (wanted === "" || existsSync17(join22(projectPath, wanted))) {
37058
+ if (wanted === "" || existsSync17(join23(projectPath, wanted))) {
36975
37059
  return "";
36976
37060
  }
36977
37061
  const root = (wanted.split("/")[0] ?? "").toLowerCase();
@@ -37062,13 +37146,13 @@ function scriptsToDiagnose(projectPath, given) {
37062
37146
  var EXTERNAL_MEMBER = 'Could not resolve external class member "';
37063
37147
  function isDirectory(path) {
37064
37148
  try {
37065
- return statSync8(path).isDirectory();
37149
+ return statSync9(path).isDirectory();
37066
37150
  } catch {
37067
37151
  return false;
37068
37152
  }
37069
37153
  }
37070
37154
  function scriptsUnder(directory, prefix) {
37071
- if (existsSync17(join22(directory, ".gdignore"))) {
37155
+ if (existsSync17(join23(directory, ".gdignore"))) {
37072
37156
  return [];
37073
37157
  }
37074
37158
  const found = [];
@@ -37078,7 +37162,7 @@ function scriptsUnder(directory, prefix) {
37078
37162
  }
37079
37163
  const path = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
37080
37164
  if (entry.isDirectory()) {
37081
- found.push(...scriptsUnder(join22(directory, entry.name), path));
37165
+ found.push(...scriptsUnder(join23(directory, entry.name), path));
37082
37166
  } else if (entry.isFile() && entry.name.endsWith(".gd")) {
37083
37167
  found.push(path);
37084
37168
  }
@@ -37094,7 +37178,7 @@ function runtimeAddonAnnouncesEditors(projectPath) {
37094
37178
  return false;
37095
37179
  }
37096
37180
  try {
37097
- return readFileSync21(join22(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
37181
+ return readFileSync21(join23(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
37098
37182
  } catch {
37099
37183
  return false;
37100
37184
  }
@@ -37120,7 +37204,7 @@ var STATIC_LIST_HINT = { ttlMs: 5 * 60000, cacheScope: "private" };
37120
37204
  class GodotServer {
37121
37205
  stdio = null;
37122
37206
  locator = new GodotLocator;
37123
- operationsScript = join22(__dirname2, "godot", "operations", "godot_operations.gd");
37207
+ operationsScript = join23(__dirname2, "godot", "operations", "godot_operations.gd");
37124
37208
  godotBridge;
37125
37209
  tools = buildToolDefinitions();
37126
37210
  activeProcess = null;
@@ -37144,6 +37228,11 @@ class GodotServer {
37144
37228
  launchedFrom = null;
37145
37229
  launchedDropped = null;
37146
37230
  closedHere = null;
37231
+ inFlight = new Map;
37232
+ callsTaken = 0;
37233
+ clientNoted = null;
37234
+ parentCommand = null;
37235
+ endNoted = false;
37147
37236
  ownProject;
37148
37237
  announcedAt = null;
37149
37238
  handingOverFrom = null;
@@ -37180,6 +37269,7 @@ class GodotServer {
37180
37269
  return { content: [{ type: "text", text: answerJson(payload) }] };
37181
37270
  }
37182
37271
  async run() {
37272
+ this.noteTheStart();
37183
37273
  const located = await this.locator.find();
37184
37274
  if (!located.ok) {
37185
37275
  console.error(`[SERVER] ${GodotLocator.refusal(located).message} Until that is fixed every tool that runs the engine will say so.`);
@@ -37296,6 +37386,7 @@ class GodotServer {
37296
37386
  return;
37297
37387
  }
37298
37388
  console.error(`[SERVER] pid ${successor} now serves ${this.ownProject}; this server is finished and is stopping.`);
37389
+ this.noteTheEnd("superseded", { successor });
37299
37390
  this.handleShutdown(`superseded by ${successor}`, 0);
37300
37391
  }, SUCCESSOR_CHECK_MS);
37301
37392
  watch.unref();
@@ -37367,10 +37458,67 @@ class GodotServer {
37367
37458
  }
37368
37459
  await this.stdio?.close();
37369
37460
  }
37461
+ noteTheStart() {
37462
+ const cwd = process.cwd();
37463
+ const mcpJson = join23(cwd, ".mcp.json");
37464
+ const announced = this.ownProject === null ? null : readAnnouncement(announcementPath(this.ownProject));
37465
+ const predecessor = announced !== null && announced.pid !== process.pid && alive(announced.pid) ? announced.pid : null;
37466
+ writeLifecycle("started", {
37467
+ argv: process.argv.slice(1),
37468
+ cwd,
37469
+ project: this.ownProject,
37470
+ mcpJson: existsSync17(mcpJson) ? mcpJson : null
37471
+ });
37472
+ if (predecessor !== null) {
37473
+ writeLifecycle("replaced", { project: this.ownProject, predecessor });
37474
+ }
37475
+ const ask = setTimeout(() => {
37476
+ processTree().then((tree) => {
37477
+ this.parentCommand = tree?.get(process.ppid)?.command ?? null;
37478
+ writeLifecycle("parent", {
37479
+ command: this.parentCommand,
37480
+ ...tree === undefined ? { why: whyTheProcessTreeFailed() } : {}
37481
+ });
37482
+ });
37483
+ }, PARENT_ASKED_AFTER_MS);
37484
+ ask.unref();
37485
+ }
37486
+ noteTheEnd(cause, more = {}) {
37487
+ if (this.endNoted) {
37488
+ return;
37489
+ }
37490
+ this.endNoted = true;
37491
+ writeLifecycle("ended", {
37492
+ cause,
37493
+ project: this.ownProject,
37494
+ ...this.parentCommand === null ? {} : { parentCommand: this.parentCommand },
37495
+ ...this.clientNoted === null ? {} : { client: this.clientNoted },
37496
+ inFlight: callsInFlight(this.inFlight.values()),
37497
+ ...more
37498
+ });
37499
+ }
37500
+ noteTheClient(client) {
37501
+ if (client === undefined) {
37502
+ return;
37503
+ }
37504
+ const said = `${client.name} ${client.version}`;
37505
+ if (said === this.clientNoted) {
37506
+ return;
37507
+ }
37508
+ this.clientNoted = said;
37509
+ writeLifecycle("client", { client: { name: client.name, version: client.version } });
37510
+ }
37370
37511
  setupShutdownHandlers() {
37371
37512
  const requestShutdown = (source, exitCode) => {
37372
37513
  this.handleShutdown(source, exitCode);
37373
37514
  };
37515
+ process.on("uncaughtException", (error, origin) => {
37516
+ this.noteTheEnd(origin === "unhandledRejection" ? "unhandledRejection" : "uncaughtException", {
37517
+ error: error instanceof Error ? error.stack ?? error.message : String(error)
37518
+ });
37519
+ console.error(error);
37520
+ process.exit(1);
37521
+ });
37374
37522
  process.once("SIGINT", () => {
37375
37523
  requestShutdown("SIGINT", 0);
37376
37524
  });
@@ -37383,7 +37531,8 @@ class GodotServer {
37383
37531
  process.once("beforeExit", (code) => {
37384
37532
  requestShutdown(`beforeExit:${code}`);
37385
37533
  });
37386
- process.once("exit", () => {
37534
+ process.once("exit", (code) => {
37535
+ this.noteTheEnd(`exit:${code}`);
37387
37536
  this.forceCleanupOnExit();
37388
37537
  });
37389
37538
  }
@@ -37392,6 +37541,7 @@ class GodotServer {
37392
37541
  return;
37393
37542
  }
37394
37543
  this.shutdownInitiated = true;
37544
+ this.noteTheEnd(source);
37395
37545
  this.logDebug(`Shutting down server via ${source}`);
37396
37546
  try {
37397
37547
  await this.cleanup();
@@ -37431,9 +37581,23 @@ class GodotServer {
37431
37581
  return mcp;
37432
37582
  }
37433
37583
  setupToolHandlers(protocol) {
37434
- protocol.setRequestHandler("tools/list", () => ({ tools: this.tools }));
37584
+ const clientOf = (envelope) => {
37585
+ const named = envelope?.["io.modelcontextprotocol/clientInfo"];
37586
+ if (typeof named === "object" && named !== null) {
37587
+ const { name, version } = named;
37588
+ if (typeof name === "string" && typeof version === "string") {
37589
+ return { name, version };
37590
+ }
37591
+ }
37592
+ return protocol.getClientVersion();
37593
+ };
37594
+ protocol.setRequestHandler("tools/list", (_request, ctx) => {
37595
+ this.noteTheClient(clientOf(ctx.mcpReq.envelope));
37596
+ return { tools: this.tools };
37597
+ });
37435
37598
  protocol.setRequestHandler("tools/call", async (request, ctx) => {
37436
37599
  this.logDebug(`Handling tool request: ${request.params.name}`);
37600
+ this.noteTheClient(clientOf(ctx.mcpReq.envelope));
37437
37601
  const spec = toolSpec(request.params.name);
37438
37602
  if (!spec) {
37439
37603
  throw new ProtocolError(ProtocolErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}. Tools: ${TOOL_SPECS.map((tool) => tool.name).join(", ")}`);
@@ -37444,7 +37608,7 @@ class GodotServer {
37444
37608
  return checked.response;
37445
37609
  }
37446
37610
  const named = args["projectPath"];
37447
- if (typeof named === "string" && existsSync17(join22(named, "project.godot"))) {
37611
+ if (typeof named === "string" && existsSync17(join23(named, "project.godot"))) {
37448
37612
  this.lastProjectPath = named;
37449
37613
  }
37450
37614
  this.updates.refresh();
@@ -37457,8 +37621,14 @@ class GodotServer {
37457
37621
  this.logDebug(`Progress for ${spec.name} was not sent: ${errorMessage(error)}`);
37458
37622
  });
37459
37623
  };
37460
- const answer = await withCallSignal(ctx.mcpReq.signal, () => this.answered(spec.name, checked.op ?? "", args), progress);
37461
- return this.withFeedbackNotice(this.withUpdateNotice(answer));
37624
+ const call = this.callsTaken++;
37625
+ this.inFlight.set(call, { tool: spec.name, op: checked.op ?? "", startedAt: Date.now() });
37626
+ try {
37627
+ const answer = await withCallSignal(ctx.mcpReq.signal, () => this.answered(spec.name, checked.op ?? "", args), progress);
37628
+ return this.withFeedbackNotice(this.withUpdateNotice(answer));
37629
+ } finally {
37630
+ this.inFlight.delete(call);
37631
+ }
37462
37632
  });
37463
37633
  }
37464
37634
  async answered(tool, op, args) {
@@ -37677,10 +37847,12 @@ class GodotServer {
37677
37847
  ]);
37678
37848
  }
37679
37849
  if (!this.godotBridge.isConnected()) {
37680
- return this.createErrorResponse(`${tool} ${op} was asked of the editor, and no editor has reached this server.`, [
37681
- "editor_status says whether the bridge is up and whether an editor may yet connect",
37682
- 'Open the project with editor_launch, or pass from: "disk" to read the file instead'
37850
+ const refused = await this.noEditorYet(answerable, [
37851
+ `${tool} ${op} was asked of the editor; from: "disk" reads the file instead`
37683
37852
  ]);
37853
+ if (refused !== null) {
37854
+ return refused;
37855
+ }
37684
37856
  }
37685
37857
  const { op: _dropped, ...asks } = answerable;
37686
37858
  const contained = this.containProjectFiles(asks);
@@ -37765,11 +37937,11 @@ class GodotServer {
37765
37937
  case "script_info":
37766
37938
  switch (op) {
37767
37939
  case "symbols":
37768
- return await this.handleLSP("lsp_get_symbols", args);
37940
+ return await this.askScriptInfo("lsp_get_symbols", "Symbols", args);
37769
37941
  case "completion":
37770
- return await this.handleLSP("lsp_get_completions", args);
37942
+ return await this.askScriptInfo("lsp_get_completions", "Completion", args);
37771
37943
  default:
37772
- return await this.handleLSP("lsp_get_hover", args);
37944
+ return await this.askScriptInfo("lsp_get_hover", "Hover", args);
37773
37945
  }
37774
37946
  case "script_diagnostics":
37775
37947
  return await this.handleScriptDiagnostics(args);
@@ -37946,7 +38118,7 @@ class GodotServer {
37946
38118
  if (path === undefined) {
37947
38119
  return { ok: false, response: this.createErrorResponse("projectPath is required.") };
37948
38120
  }
37949
- const file = join22(path, "project.godot");
38121
+ const file = join23(path, "project.godot");
37950
38122
  if (!existsSync17(file)) {
37951
38123
  return {
37952
38124
  ok: false,
@@ -38633,18 +38805,18 @@ class GodotServer {
38633
38805
  if (!engine.ok) {
38634
38806
  return engine.response;
38635
38807
  }
38636
- const folder = dirname15(output.absolutePath);
38808
+ const folder = dirname16(output.absolutePath);
38637
38809
  try {
38638
- mkdirSync13(folder, { recursive: true });
38810
+ mkdirSync14(folder, { recursive: true });
38639
38811
  } catch (error) {
38640
- return this.createErrorResponse(`Cannot create ${dirname15(output.relativePath)} to export into: ${errorMessage(error)}`);
38812
+ return this.createErrorResponse(`Cannot create ${dirname16(output.relativePath)} to export into: ${errorMessage(error)}`);
38641
38813
  }
38642
38814
  const debug = readBoolean(args, "debug") ?? false;
38643
38815
  const exportLogs = scratchDirectory("export");
38644
38816
  const exportArgs = [
38645
38817
  "--headless",
38646
38818
  "--log-file",
38647
- join22(exportLogs, "engine.log"),
38819
+ join23(exportLogs, "engine.log"),
38648
38820
  "--path",
38649
38821
  project.value.path,
38650
38822
  debug ? "--export-debug" : "--export-release",
@@ -38652,7 +38824,7 @@ class GodotServer {
38652
38824
  output.absolutePath
38653
38825
  ];
38654
38826
  this.logDebug(`Exporting: ${engine.value} ${exportArgs.join(" ")}`);
38655
- const writtenAt = () => existsSync17(output.absolutePath) ? statSync8(output.absolutePath).mtimeMs : null;
38827
+ const writtenAt = () => existsSync17(output.absolutePath) ? statSync9(output.absolutePath).mtimeMs : null;
38656
38828
  const before = writtenAt();
38657
38829
  let ending;
38658
38830
  try {
@@ -38716,10 +38888,10 @@ class GodotServer {
38716
38888
  if (movie !== null) {
38717
38889
  return this.createErrorResponse(movie);
38718
38890
  }
38719
- const missing = paths.value.filter((path) => !existsSync17(join22(project.value.path, path.slice("res://".length))));
38891
+ const missing = paths.value.filter((path) => !existsSync17(join23(project.value.path, path.slice("res://".length))));
38720
38892
  const asked = paths.value.filter((path) => !missing.includes(path));
38721
38893
  const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
38722
- const noRunner = !existsSync17(join22(project.value.path, runner));
38894
+ const noRunner = !existsSync17(join23(project.value.path, runner));
38723
38895
  if (asked.length === 0) {
38724
38896
  const verdict = `nothing at ${missing.join(" and ")}`;
38725
38897
  const andNoRunner = noRunner ? `, and gdUnit4 is not installed in this project: no ${runner}` : "";
@@ -38810,21 +38982,21 @@ class GodotServer {
38810
38982
  await untilGone([run.process.pid]);
38811
38983
  }
38812
38984
  await run.settled;
38813
- const reportsDir = join22(project.value.path, ".godot", ...ours.split("/"));
38985
+ const reportsDir = join23(project.value.path, ".godot", ...ours.split("/"));
38814
38986
  let report = null;
38815
38987
  let reportProblem = null;
38816
38988
  try {
38817
38989
  const written = existsSync17(reportsDir) ? readdirSync9(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
38818
38990
  const newest = written.at(-1);
38819
38991
  if (newest !== undefined) {
38820
- report = parseJUnit(readFileSync21(join22(reportsDir, newest, "results.xml"), "utf8"));
38992
+ report = parseJUnit(readFileSync21(join23(reportsDir, newest, "results.xml"), "utf8"));
38821
38993
  }
38822
38994
  } catch (error) {
38823
38995
  reportProblem = errorMessage(error);
38824
38996
  } finally {
38825
38997
  discard(reportsDir, userData);
38826
38998
  try {
38827
- rmdirSync2(dirname15(reportsDir));
38999
+ rmdirSync2(dirname16(reportsDir));
38828
39000
  } catch {}
38829
39001
  }
38830
39002
  const engineEntries = foldedRepeats(run.log.select({
@@ -39056,31 +39228,9 @@ class GodotServer {
39056
39228
  async diagnosticsOf(args) {
39057
39229
  const answer = await this.handleLSP("lsp_get_diagnostics", args);
39058
39230
  const payload = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
39059
- if (payload["error"] !== undefined) {
39060
- const reason = typeof payload["error"] === "string" ? payload["error"] : JSON.stringify(payload["error"]);
39061
- if (payload["refusedArguments"] === true) {
39062
- return { refusal: this.createErrorResponse(reason), perScript: true };
39063
- }
39064
- if (payload["stillReading"] === true) {
39065
- return { refusal: this.createErrorResponse(`Diagnostics unavailable: ${reason}`), perScript: false };
39066
- }
39067
- if (typeof payload["serves"] === "string") {
39068
- return {
39069
- refusal: this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
39070
- "editor_status names the editor on the bridge and the port it says it serves",
39071
- "editor_launch opens this project's own editor, on a language server port of its own",
39072
- "GDHARNESS_LSP_PORT points this server at the right one"
39073
- ]),
39074
- perScript: false
39075
- };
39076
- }
39077
- return {
39078
- refusal: this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
39079
- `Ensure the Godot editor is running with its language server enabled, on port ${this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT)}`,
39080
- "GDHARNESS_LSP_PORT points this server at another one"
39081
- ]),
39082
- perScript: payload["aboutThisFile"] === true
39083
- };
39231
+ const refused = this.languageServerRefusal(payload, "Diagnostics");
39232
+ if (refused !== null) {
39233
+ return refused;
39084
39234
  }
39085
39235
  const diagnostics = readArray(payload, "diagnostics") ?? [];
39086
39236
  const errors = diagnostics.filter((entry) => asParams(entry)["severity"] === "error").length;
@@ -39187,6 +39337,40 @@ class GodotServer {
39187
39337
  async handleLSP(toolName, args) {
39188
39338
  return handleLSPTool(await this.lsp(), toolName, args);
39189
39339
  }
39340
+ async askScriptInfo(toolName, asked, args) {
39341
+ const answer = await this.handleLSP(toolName, args);
39342
+ const refused = this.languageServerRefusal(asParams(JSON.parse(answer.content[0]?.text ?? "{}")), asked);
39343
+ return refused === null ? answer : refused.refusal;
39344
+ }
39345
+ languageServerRefusal(payload, asked) {
39346
+ if (payload["error"] === undefined) {
39347
+ return null;
39348
+ }
39349
+ const reason = typeof payload["error"] === "string" ? payload["error"] : JSON.stringify(payload["error"]);
39350
+ if (payload["refusedArguments"] === true) {
39351
+ return { refusal: this.createErrorResponse(reason), perScript: true };
39352
+ }
39353
+ if (payload["stillReading"] === true) {
39354
+ return { refusal: this.createErrorResponse(`${asked} unavailable: ${reason}`), perScript: false };
39355
+ }
39356
+ if (typeof payload["serves"] === "string") {
39357
+ return {
39358
+ refusal: this.createErrorResponse(`${asked} unavailable: ${reason}`, [
39359
+ "editor_status names the editor on the bridge and the port it says it serves",
39360
+ "editor_launch opens this project's own editor, on a language server port of its own",
39361
+ "GDHARNESS_LSP_PORT points this server at the right one"
39362
+ ]),
39363
+ perScript: false
39364
+ };
39365
+ }
39366
+ return {
39367
+ refusal: this.createErrorResponse(`${asked} unavailable: ${reason}`, [
39368
+ `Ensure the Godot editor is running with its language server enabled, on port ${this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT)}`,
39369
+ "GDHARNESS_LSP_PORT points this server at another one"
39370
+ ]),
39371
+ perScript: payload["aboutThisFile"] === true
39372
+ };
39373
+ }
39190
39374
  async lsp() {
39191
39375
  const port = this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT);
39192
39376
  if (this.lspClient !== null && this.lspClient.port !== port) {
@@ -39287,7 +39471,7 @@ class GodotServer {
39287
39471
  return;
39288
39472
  }
39289
39473
  try {
39290
- appendFileSync(run.transcript, line.endsWith(`
39474
+ appendFileSync2(run.transcript, line.endsWith(`
39291
39475
  `) ? line : `${line}
39292
39476
  `);
39293
39477
  } catch (error) {
@@ -39498,7 +39682,8 @@ class GodotServer {
39498
39682
  serverSpeaks: RUNTIME_PROTOCOL,
39499
39683
  behind: game.protocol > RUNTIME_PROTOCOL ? "server" : "addon"
39500
39684
  }))
39501
- }
39685
+ },
39686
+ server: { pid: process.pid, version: SERVER_VERSION, lifecycleLog: lifecycleLogPath() }
39502
39687
  });
39503
39688
  }
39504
39689
  async editorServersListening() {
@@ -39530,7 +39715,7 @@ class GodotServer {
39530
39715
  return null;
39531
39716
  }
39532
39717
  try {
39533
- return settingKeys(readFileSync21(join22(projectPath, "project.godot"), "utf8"));
39718
+ return settingKeys(readFileSync21(join23(projectPath, "project.godot"), "utf8"));
39534
39719
  } catch {
39535
39720
  return null;
39536
39721
  }
@@ -39586,6 +39771,10 @@ class GodotServer {
39586
39771
  async handleCloseEditor() {
39587
39772
  const before = this.godotBridge.getStatus();
39588
39773
  if (!before.connected) {
39774
+ const launched = this.launchedEditor;
39775
+ if (launched !== null && await this.launchedEditorIsUp() && !this.godotBridge.isConnected()) {
39776
+ return await this.stopTheLaunchedEditor(launched);
39777
+ }
39589
39778
  return this.createErrorResponse("No editor is connected, so there is nothing to close.", [
39590
39779
  "editor_status says whether the bridge is up and what has reached it"
39591
39780
  ]);
@@ -39619,6 +39808,31 @@ class GodotServer {
39619
39808
  tookMs: Date.now() - began
39620
39809
  });
39621
39810
  }
39811
+ async stopTheLaunchedEditor(launched) {
39812
+ const began = Date.now();
39813
+ try {
39814
+ process.kill(launched.pid);
39815
+ } catch (error) {
39816
+ if (alive(launched.pid)) {
39817
+ return this.createErrorResponse(`The editor this server opened, pid ${launched.pid}, has not connected, and stopping it failed: ${errorMessage(error)}`, ["editor_status says whether it has since connected"]);
39818
+ }
39819
+ }
39820
+ const stopWaitMs = 1e4;
39821
+ if (!await this.waitForBridge(() => !alive(launched.pid), began + stopWaitMs)) {
39822
+ return this.createErrorResponse(`The editor this server opened, pid ${launched.pid}, had not connected and was told to stop, and is still running after ${stopWaitMs / 1000}s.`, ["editor_status says whether it has since connected"]);
39823
+ }
39824
+ this.closedHere = { editorPid: launched.pid, at: new Date().toISOString() };
39825
+ this.launchedEditor = null;
39826
+ this.launchedFrom = null;
39827
+ return this.jsonTextResponse({
39828
+ closed: true,
39829
+ editorPid: launched.pid,
39830
+ projectPath: launched.projectPath,
39831
+ beforeConnecting: true,
39832
+ note: "It had not reached the bridge, so it was stopped rather than asked to save and quit, and held nothing of this server's to save.",
39833
+ tookMs: Date.now() - began
39834
+ });
39835
+ }
39622
39836
  settleTheLaunchReading() {
39623
39837
  const watched = this.launchedFrom;
39624
39838
  const status = this.godotBridge.getStatus();
@@ -39754,13 +39968,9 @@ class GodotServer {
39754
39968
  `editor_launch with projectPath ${this.ownProject} opens the editor this server serves`
39755
39969
  ]);
39756
39970
  }
39757
- const report = inspectProject(projectPath);
39758
- const installed = report.addons.some((addon) => addon.name === "gdharness_editor" && addon.installed);
39759
- if (!installed || !report.pluginsEnabled.includes("gdharness_editor")) {
39760
- return this.createErrorResponse(`The gdharness editor addon is not ${installed ? "enabled" : "installed"} in ${projectPath}, so an editor opened on it would never reach this server.`, installed ? [
39761
- "gdharness setup in the project enables it",
39762
- "Or enable it under Project Settings, Plugins, in an editor opened by hand"
39763
- ] : ["gdharness setup in the project installs and enables it"]);
39971
+ const missing = editorAddonMissing(projectPath);
39972
+ if (missing !== null) {
39973
+ return this.createErrorResponse(`${missing.why}, so an editor opened on it would never reach this server.`, missing.remedies);
39764
39974
  }
39765
39975
  const owed = restartOwed(projectPath);
39766
39976
  if (owed !== null && restartUnderway(owed)) {
@@ -39788,7 +39998,8 @@ class GodotServer {
39788
39998
  writeEditorLogNote(projectPath, launched.pid);
39789
39999
  this.launchedEditor = {
39790
40000
  pid: launched.pid,
39791
- startedAt: (await startTimesOf([launched.pid])).get(launched.pid) ?? null
40001
+ startedAt: (await startTimesOf([launched.pid])).get(launched.pid) ?? null,
40002
+ projectPath
39792
40003
  };
39793
40004
  this.closedHere = null;
39794
40005
  return { pid: launched.pid, error: null };
@@ -39925,7 +40136,7 @@ class GodotServer {
39925
40136
  }
39926
40137
  async runtimeUp(projectPath, before, budget, withArgs) {
39927
40138
  const budgetMs = budget.ms;
39928
- if (!existsSync17(join22(projectPath, RUNTIME_AUTOLOAD.path))) {
40139
+ if (!existsSync17(join23(projectPath, RUNTIME_AUTOLOAD.path))) {
39929
40140
  return runtimeVerdict(null, {
39930
40141
  addon: false,
39931
40142
  budgetMs,
@@ -40010,7 +40221,7 @@ class GodotServer {
40010
40221
  }
40011
40222
  let announcedAt;
40012
40223
  try {
40013
- announcedAt = statSync8(endpoint.file).mtimeMs;
40224
+ announcedAt = statSync9(endpoint.file).mtimeMs;
40014
40225
  } catch {
40015
40226
  return;
40016
40227
  }
@@ -40179,7 +40390,7 @@ class GodotServer {
40179
40390
  return;
40180
40391
  }
40181
40392
  if (game.transcript !== null) {
40182
- appendFileSync(game.transcript, reported);
40393
+ appendFileSync2(game.transcript, reported);
40183
40394
  } else {
40184
40395
  game.log.append("stderr", reported);
40185
40396
  }
@@ -40883,7 +41094,7 @@ class GodotServer {
40883
41094
  }
40884
41095
  const going = await this.runStillGoing(run);
40885
41096
  const projectPath = run.projectPath;
40886
- const projectGone = projectPath !== null && !existsSync17(join22(projectPath, "project.godot"));
41097
+ const projectGone = projectPath !== null && !existsSync17(join23(projectPath, "project.godot"));
40887
41098
  if (projectGone && callersName !== undefined) {
40888
41099
  return this.createErrorResponse(`callersOf reads the project's scripts, and the run's project at ${projectPath} has no project.godot any more.`, ["editor_run profile without callersOf answers with the profile itself"]);
40889
41100
  }
@@ -41054,7 +41265,11 @@ class GodotServer {
41054
41265
  if (!greeted && launched === null && status.connected) {
41055
41266
  return this.createErrorResponse("The editor has connected and has not yet said who it is, so whose console to read cannot be told yet.", ["Ask again in a moment; editor_status shows greeting until it has"]);
41056
41267
  }
41057
- const projectPath = status.projectPath ?? this.ownProject ?? undefined;
41268
+ const projectPath = status.projectPath ?? this.launchedEditor?.projectPath ?? this.ownProject ?? undefined;
41269
+ const elsewhere = this.anotherProjectNamed(args, projectPath);
41270
+ if (elsewhere !== null) {
41271
+ return elsewhere;
41272
+ }
41058
41273
  if (projectPath === undefined || !greeted && launched === null) {
41059
41274
  return this.createErrorResponse("No editor is connected, so there is no console to read.", [
41060
41275
  "editor_status says whether one is connected and whether it may yet be",
@@ -41105,6 +41320,19 @@ class GodotServer {
41105
41320
  omitted: selected.omitted === 0 ? undefined : selected.omitted
41106
41321
  });
41107
41322
  }
41323
+ anotherProjectNamed(args, ofWhatIsRead) {
41324
+ const named = readNonEmptyString(args, "projectPath");
41325
+ if (named === undefined || ofWhatIsRead === null || ofWhatIsRead === undefined || ofWhatIsRead === "") {
41326
+ return null;
41327
+ }
41328
+ if (isSameDirectory(named, ofWhatIsRead)) {
41329
+ return null;
41330
+ }
41331
+ return this.createErrorResponse(`editor_output reads what this server holds, which is of ${ofWhatIsRead}, and this call names ${named}.`, [
41332
+ "Another project's run is read through that project's own gdharness server",
41333
+ "editor_status names every game running on the machine under runtimes"
41334
+ ]);
41335
+ }
41108
41336
  async handleGetDebugOutput(args, waitedMs) {
41109
41337
  if (readString(args, "op") === "editor") {
41110
41338
  return this.handleEditorConsole(args);
@@ -41114,6 +41342,10 @@ class GodotServer {
41114
41342
  if (!run) {
41115
41343
  return this.createErrorResponse(this.nothingOfOursIsRunning());
41116
41344
  }
41345
+ const elsewhere = this.anotherProjectNamed(args, run.projectPath ?? this.godotBridge.getStatus().projectPath);
41346
+ if (elsewhere !== null) {
41347
+ return elsewhere;
41348
+ }
41117
41349
  await this.tieThePlayedGame(run);
41118
41350
  this.drainEditorOutput(run);
41119
41351
  this.drainTranscript(run);
@@ -41150,7 +41382,7 @@ class GodotServer {
41150
41382
  notes.push(endedWithoutACode({
41151
41383
  throughEditor: run.throughEditor,
41152
41384
  announced: announced !== undefined || run.announcedPid !== undefined,
41153
- couldAnnounce: run.projectPath !== null && existsSync17(join22(run.projectPath, RUNTIME_AUTOLOAD.path)),
41385
+ couldAnnounce: run.projectPath !== null && existsSync17(join23(run.projectPath, RUNTIME_AUTOLOAD.path)),
41154
41386
  endedHere: typeof run.endedHere === "string",
41155
41387
  pickedUp: run.pickedUp === true
41156
41388
  }));
@@ -41930,15 +42162,47 @@ class GodotServer {
41930
42162
  const holds = held["classes"].map(String);
41931
42163
  return { unseen: unseenByEditor(projectPath, holds), stillHeld: heldButGone(projectPath, holds) };
41932
42164
  }
42165
+ async noEditorYet(args, instead) {
42166
+ const project = readNonEmptyString(args, "projectPath") ?? this.ownProject;
42167
+ const missing = project === null ? null : editorAddonMissing(project);
42168
+ if (missing !== null) {
42169
+ return this.createErrorResponse(`Editor not connected, and no editor of this project can connect: ${missing.why}.`, [...instead, ...missing.remedies, "editor_launch then opens an editor that connects"]);
42170
+ }
42171
+ const launched = this.launchedEditor;
42172
+ if (launched !== null && await this.launchedEditorIsUp()) {
42173
+ const began = Date.now();
42174
+ await this.waitForBridge(() => hasSaidWhoItIs(this.godotBridge.getStatus()) || !alive(launched.pid), began + editorRestartTimeoutMs());
42175
+ if (this.godotBridge.isConnected()) {
42176
+ return null;
42177
+ }
42178
+ const opened = `the editor this server opened on ${launched.projectPath}, pid ${launched.pid}`;
42179
+ if (alive(launched.pid)) {
42180
+ return this.createErrorResponse(`Editor not connected: ${opened}, is still running and has not reached the bridge in the ${String(Math.round((Date.now() - began) / 1000))}s this call waited. An editor dials in once it has imported the project and loaded its plugins, and the first open of a large project imports for minutes.`, [
42181
+ ...instead,
42182
+ "editor_status names it under awaitingLaunchedEditor until it has connected",
42183
+ "editor_output op editor shows what it is printing meanwhile"
42184
+ ]);
42185
+ }
42186
+ return this.createErrorResponse(`Editor not connected: ${opened}, exited without reaching the bridge.`, [
42187
+ ...instead,
42188
+ "editor_output op editor shows what it printed before it went",
42189
+ "editor_launch open starts another"
42190
+ ]);
42191
+ }
42192
+ return this.createErrorResponse("Editor not connected: no Godot editor with the gdharness editor addon has reached this server.", [
42193
+ ...instead,
42194
+ "Open the project in the editor with editor_launch, or by hand",
42195
+ "Enable the addon under Project > Project Settings > Plugins",
42196
+ "editor_status says whether the bridge is up and what it is listening on"
42197
+ ]);
42198
+ }
41933
42199
  async handleViaBridge(toolName, args, askedAs) {
41934
42200
  if (!this.godotBridge.isConnected()) {
41935
- const instead = askedAs !== undefined && Object.hasOwn(EDITOR_FREE_INSTEAD, askedAs) ? EDITOR_FREE_INSTEAD[askedAs] : undefined;
41936
- return this.createErrorResponse("Editor not connected: no Godot editor with the gdharness editor addon has reached this server.", [
41937
- ...instead === undefined ? [] : [instead],
41938
- "Open the project in the editor with editor_launch, or by hand",
41939
- "Enable the addon under Project > Project Settings > Plugins",
41940
- "editor_status says whether the bridge is up and what it is listening on"
41941
- ]);
42201
+ const alternative = askedAs !== undefined && Object.hasOwn(EDITOR_FREE_INSTEAD, askedAs) ? EDITOR_FREE_INSTEAD[askedAs] : undefined;
42202
+ const refused = await this.noEditorYet(args, alternative === undefined ? [] : [alternative]);
42203
+ if (refused !== null) {
42204
+ return refused;
42205
+ }
41942
42206
  }
41943
42207
  const open = this.godotBridge.getStatus().projectPath;
41944
42208
  const meant = readNonEmptyString(args, "projectPath");
@@ -42011,7 +42275,7 @@ class GodotServer {
42011
42275
  async ownRunOnItsWay(projectPath, budgetMs) {
42012
42276
  const run = await this.currentRun();
42013
42277
  const project = projectPath ?? run?.projectPath ?? null;
42014
- if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync17(join22(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
42278
+ if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync17(join23(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
42015
42279
  return null;
42016
42280
  }
42017
42281
  const already = runtimesAnnounced().running;
@@ -42020,7 +42284,7 @@ class GodotServer {
42020
42284
  return !run.announcedBefore.has(one.pid);
42021
42285
  }
42022
42286
  try {
42023
- return statSync8(one.file).mtimeMs >= run.startedAt;
42287
+ return statSync9(one.file).mtimeMs >= run.startedAt;
42024
42288
  } catch {
42025
42289
  return false;
42026
42290
  }
@@ -42096,7 +42360,7 @@ class GodotServer {
42096
42360
  }
42097
42361
  }
42098
42362
  const screenshotDir = expectsScreenshot ? scratchDirectory("runtime-screenshot") : null;
42099
- const screenshotPath = screenshotDir ? join22(screenshotDir, "capture.png") : null;
42363
+ const screenshotPath = screenshotDir ? join23(screenshotDir, "capture.png") : null;
42100
42364
  const staleNote = this.runtimeStaleNote(choice.endpoint);
42101
42365
  const stale = staleNote === undefined ? {} : { addonIsStale: true, staleNote };
42102
42366
  const refused = (message) => this.createErrorResponse(staleNote === undefined ? message : `${message}