gdharness 1.0.10 → 1.0.12

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.
Files changed (3) hide show
  1. package/build/cli.js +475 -112
  2. package/build/index.js +480 -125
  3. package/package.json +1 -1
package/build/index.js CHANGED
@@ -10172,20 +10172,20 @@ import { execFile as execFile5, spawn } from "node:child_process";
10172
10172
  import { randomUUID as randomUUID2 } from "node:crypto";
10173
10173
  import {
10174
10174
  appendFileSync,
10175
- closeSync,
10176
- existsSync as existsSync9,
10175
+ closeSync as closeSync2,
10176
+ existsSync as existsSync10,
10177
10177
  fstatSync,
10178
- mkdirSync as mkdirSync9,
10178
+ mkdirSync as mkdirSync10,
10179
10179
  mkdtempSync as mkdtempSync2,
10180
- openSync as openSync2,
10180
+ openSync as openSync3,
10181
10181
  readdirSync as readdirSync6,
10182
- readFileSync as readFileSync15,
10183
- readSync,
10182
+ readFileSync as readFileSync16,
10183
+ readSync as readSync2,
10184
10184
  rmdirSync,
10185
- statSync as statSync5
10185
+ statSync as statSync6
10186
10186
  } from "node:fs";
10187
10187
  import { tmpdir as tmpdir4 } from "node:os";
10188
- import { basename as basename3, dirname as dirname9, isAbsolute as isAbsolute3, join as join15, normalize as normalize2, relative as relative3 } from "node:path";
10188
+ import { basename as basename3, dirname as dirname10, isAbsolute as isAbsolute3, join as join16, normalize as normalize2, relative as relative3 } from "node:path";
10189
10189
  import { setTimeout as delay3 } from "node:timers/promises";
10190
10190
  import { fileURLToPath as fileURLToPath2 } from "node:url";
10191
10191
  import { promisify as promisify5 } from "node:util";
@@ -24791,8 +24791,18 @@ function runArguments(options) {
24791
24791
  }
24792
24792
  return args;
24793
24793
  }
24794
- function editorArguments(projectPath, ports) {
24795
- return ["-e", "--path", projectPath, "--lsp-port", String(ports.lsp), "--dap-port", String(ports.dap)];
24794
+ function editorArguments(projectPath, ports, logFile) {
24795
+ return [
24796
+ "-e",
24797
+ "--path",
24798
+ projectPath,
24799
+ "--lsp-port",
24800
+ String(ports.lsp),
24801
+ "--dap-port",
24802
+ String(ports.dap),
24803
+ "--log-file",
24804
+ logFile
24805
+ ];
24796
24806
  }
24797
24807
  var OPENED_BY_A_SERVER = "GDHARNESS_OPENED_BY_A_SERVER";
24798
24808
  function userDataIn(home, variables = process.env) {
@@ -25318,10 +25328,37 @@ async function announcedSince(projectPath, before, waiting = {}) {
25318
25328
  function describe2(endpoint) {
25319
25329
  return `pid ${endpoint.pid} on ${endpoint.address}:${endpoint.port} (${endpoint.project.name || "unnamed"} at ${endpoint.project.path})`;
25320
25330
  }
25331
+ function describeWithin(endpoint) {
25332
+ return `pid ${endpoint.pid} on ${endpoint.address}:${endpoint.port}`;
25333
+ }
25334
+ function theOneProject(endpoints) {
25335
+ if (endpoints.some((endpoint) => endpoint.project.path === "")) {
25336
+ return;
25337
+ }
25338
+ const paths = new Set(endpoints.map((endpoint) => resolve(endpoint.project.path)));
25339
+ const only = [...paths][0];
25340
+ return paths.size === 1 ? only : undefined;
25341
+ }
25342
+ function severalFrom(project, games) {
25343
+ return `${games.length} games are running from ${project}: ${games.map(describeWithin).join("; ")}. ` + "Pass pid to choose one; projectPath cannot, because every one of them has this one. " + "editor_status lists them under runtimes.";
25344
+ }
25345
+ function whichHalfIsBehind(unspoken) {
25346
+ const pids = (games) => games.map((game) => `pid ${game.pid}`).join(", ");
25347
+ const ahead = unspoken.filter((game) => game.protocol > RUNTIME_PROTOCOL);
25348
+ const behind = unspoken.filter((game) => game.protocol < RUNTIME_PROTOCOL);
25349
+ if (ahead.length > 0 && behind.length > 0) {
25350
+ return `they are on both sides of it: reconnect this server for ${pids(ahead)}, and reinstall the ` + `addon and restart ${pids(behind)}`;
25351
+ }
25352
+ if (ahead.length > 0) {
25353
+ return "this server is the older half: reconnect it so it spawns the installed version";
25354
+ }
25355
+ return `the addon is the older half: reinstall it and restart ${behind.length === 1 ? "the game" : "those games"}`;
25356
+ }
25321
25357
  function tooNew(unspoken) {
25322
25358
  const named = unspoken.map((game) => `pid ${game.pid} speaks ${game.protocol} (${game.project.name || "unnamed"} at ${game.project.path})`).join("; ");
25323
- const half = unspoken.some((game) => game.protocol > RUNTIME_PROTOCOL) ? "this server is the older half: reconnect it so it spawns the installed version" : "the addon is the older half: reinstall it and restart the game";
25324
- return `A game is running, in a protocol this server does not speak ${named}. This server speaks ${RUNTIME_PROTOCOL}, so ${half}.`;
25359
+ const count = unspoken.length === 1 ? "A game is running" : `${unspoken.length} games are running`;
25360
+ const spoken = new Set(unspoken.map((game) => game.protocol)).size === 1 ? "a protocol" : "protocols";
25361
+ return `${count}, in ${spoken} this server does not speak: ${named}. This server speaks ${RUNTIME_PROTOCOL}, so ${whichHalfIsBehind(unspoken)}.`;
25325
25362
  }
25326
25363
  function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
25327
25364
  if (pid !== undefined) {
@@ -25338,8 +25375,16 @@ function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
25338
25375
  if (tooNewToo !== undefined) {
25339
25376
  return { problem: tooNew([tooNewToo]) };
25340
25377
  }
25378
+ if (endpoints.length > 0) {
25379
+ return {
25380
+ problem: `No running game has pid ${pid}. Running: ${endpoints.map(describe2).join("; ")}. editor_status lists them under runtimes as they come and go.`
25381
+ };
25382
+ }
25383
+ if (unspoken.length > 0) {
25384
+ return { problem: `No game reachable from here has pid ${pid}. ${tooNew(unspoken)}` };
25385
+ }
25341
25386
  return {
25342
- problem: endpoints.length === 0 ? `No game with the runtime addon is running, so there is none with pid ${pid}. Start one with editor_run, or play the project from the editor with the addon enabled.` : `No running game has pid ${pid}. Running: ${endpoints.map(describe2).join("; ")}. editor_status lists them under runtimes as they come and go.`
25387
+ problem: `No game with the runtime addon is running, so there is none with pid ${pid}. Start one with editor_run, or play the project from the editor with the addon enabled.`
25343
25388
  };
25344
25389
  }
25345
25390
  if (endpoints.length === 0) {
@@ -25371,15 +25416,17 @@ function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
25371
25416
  if (matching.length === 1 && matching[0]) {
25372
25417
  return { endpoint: matching[0] };
25373
25418
  }
25374
- return {
25375
- problem: `Several games are running from ${wanted}: ${matching.map(describe2).join("; ")}. Pass pid to choose one.`
25376
- };
25419
+ return { problem: severalFrom(wanted, matching) };
25377
25420
  }
25378
25421
  if (endpoints.length === 1 && endpoints[0]) {
25379
25422
  return { endpoint: endpoints[0] };
25380
25423
  }
25424
+ const shared = theOneProject(endpoints);
25425
+ if (shared !== undefined) {
25426
+ return { problem: severalFrom(shared, endpoints) };
25427
+ }
25381
25428
  return {
25382
- problem: `Several games are running: ${endpoints.map(describe2).join("; ")}. Pass projectPath to choose one, or pid when they are from one project.`
25429
+ problem: `Several games are running: ${endpoints.map(describe2).join("; ")}. Pass pid to choose one, or projectPath when the project you mean is running only one.`
25383
25430
  };
25384
25431
  }
25385
25432
  var nextRequestId = 1;
@@ -26613,6 +26660,20 @@ function dictionary(entries) {
26613
26660
  return Object.assign(emptyRecord(), entries);
26614
26661
  }
26615
26662
 
26663
+ // src/editor-log.ts
26664
+ import {
26665
+ closeSync,
26666
+ existsSync as existsSync5,
26667
+ mkdirSync as mkdirSync6,
26668
+ openSync,
26669
+ readFileSync as readFileSync10,
26670
+ readSync,
26671
+ rmSync as rmSync3,
26672
+ statSync as statSync3,
26673
+ writeFileSync as writeFileSync6
26674
+ } from "node:fs";
26675
+ import { dirname as dirname6, join as join9 } from "node:path";
26676
+
26616
26677
  // src/game-log.ts
26617
26678
  import { StringDecoder } from "node:string_decoder";
26618
26679
  var HEADLINE = /^(USER )?(SCRIPT ERROR|SHADER ERROR|ERROR|WARNING):\s?(.*)$/;
@@ -26699,6 +26760,9 @@ class GameLog {
26699
26760
  count(severity) {
26700
26761
  return this.entries.filter((entry) => entry.severity === severity).length;
26701
26762
  }
26763
+ everything() {
26764
+ return this.entries;
26765
+ }
26702
26766
  select(options) {
26703
26767
  const floor = options.sinceLastCall ? this.seen[options.severity] : 0;
26704
26768
  const wanted = ADMITTED[options.severity];
@@ -26722,6 +26786,221 @@ class GameLog {
26722
26786
  }
26723
26787
  }
26724
26788
 
26789
+ // src/editor-log.ts
26790
+ function editorLogPath(projectPath) {
26791
+ return join9(projectPath, ".godot", "gdharness-editor.log");
26792
+ }
26793
+ function editorLogNotePath(projectPath) {
26794
+ return join9(projectPath, ".godot", "gdharness-editor.json");
26795
+ }
26796
+ function readEditorLogNote(projectPath) {
26797
+ try {
26798
+ const fields = asParams(JSON.parse(readFileSync10(editorLogNotePath(projectPath), "utf8")));
26799
+ const pid = readNumber(fields, "pid");
26800
+ const startedAt = readString(fields, "startedAt");
26801
+ return pid !== undefined && Number.isInteger(pid) && pid > 0 && startedAt !== undefined ? { pid, startedAt } : null;
26802
+ } catch {
26803
+ return null;
26804
+ }
26805
+ }
26806
+ function clearEditorLog(projectPath) {
26807
+ try {
26808
+ const path = editorLogPath(projectPath);
26809
+ mkdirSync6(dirname6(path), { recursive: true });
26810
+ rmSync3(path, { force: true });
26811
+ rmSync3(editorLogNotePath(projectPath), { force: true });
26812
+ } catch {}
26813
+ }
26814
+ function writeEditorLogNote(projectPath, pid) {
26815
+ try {
26816
+ const path = editorLogNotePath(projectPath);
26817
+ mkdirSync6(dirname6(path), { recursive: true });
26818
+ writeFileSync6(path, JSON.stringify({ pid, startedAt: new Date().toISOString() }, null, 2), "utf8");
26819
+ } catch {}
26820
+ }
26821
+ function theConsoleWasNotCaptured(why, projectPath) {
26822
+ if (why.kind === "not ours") {
26823
+ return "This editor was not opened by gdharness, so its console was never captured and nothing " + "here has it: an editor prints to a console no plugin can read, and the addon cannot get " + "at it from inside. This is not an editor that printed nothing.";
26824
+ }
26825
+ if (why.kind === "older server") {
26826
+ return `This editor was opened by a gdharness server, but the console beside ${projectPath} belongs ` + `to editor ${why.notedPid ?? "none recorded"} rather than to this one, so it holds an ` + "earlier session and is not offered as this one. A server from before consoles were " + "captured leaves exactly this.";
26827
+ }
26828
+ return "This editor was opened by gdharness and has written nothing to its console file yet, not " + "even the engine version it prints first, so the file is missing rather than empty. An " + "editor that could not open the file for writing leaves this.";
26829
+ }
26830
+ function editorConsole(projectPath, editorPid, openedByAServer) {
26831
+ if (!openedByAServer) {
26832
+ return { kind: "not ours" };
26833
+ }
26834
+ const noted = readEditorLogNote(projectPath);
26835
+ if (noted === null || editorPid === null || noted.pid !== editorPid) {
26836
+ return { kind: "older server", notedPid: noted?.pid ?? null };
26837
+ }
26838
+ const path = editorLogPath(projectPath);
26839
+ if (!existsSync5(path)) {
26840
+ return { kind: "not written" };
26841
+ }
26842
+ const log = new GameLog;
26843
+ const { text, of } = theFirstOf(path);
26844
+ log.append("transcript", text);
26845
+ log.finish();
26846
+ return { log, path, ...of === undefined ? {} : { readBytes: text.length, of } };
26847
+ }
26848
+ var MOST_CONSOLE_BYTES = 8 * 1024 * 1024;
26849
+ function theFirstOf(path) {
26850
+ const whole = statSync3(path).size;
26851
+ if (whole <= MOST_CONSOLE_BYTES) {
26852
+ return { text: readFileSync10(path, "utf8") };
26853
+ }
26854
+ const handle = openSync(path, "r");
26855
+ try {
26856
+ const buffer = Buffer.alloc(MOST_CONSOLE_BYTES);
26857
+ const read = readSync(handle, buffer, 0, MOST_CONSOLE_BYTES, 0);
26858
+ const decoded = buffer.subarray(0, read).toString("utf8");
26859
+ const lastLine = decoded.lastIndexOf(`
26860
+ `);
26861
+ return { text: lastLine === -1 ? decoded : decoded.slice(0, lastLine + 1), of: whole };
26862
+ } finally {
26863
+ closeSync(handle);
26864
+ }
26865
+ }
26866
+ var NAMED_VALUES = 6;
26867
+ var WALK_BACK_LIMIT = 500;
26868
+ var SLOTS = [/"[^"]*"/g, /'[^']*'/g, /\b(?:res|user):\/\/\S+/g, /(?<=\.gd|\.tscn|\.cs):\d+(?::\d+)?/g];
26869
+ function shapeOf(text) {
26870
+ const values = [];
26871
+ let shape = "";
26872
+ let at = 0;
26873
+ while (at < text.length) {
26874
+ let earliest = null;
26875
+ for (const pattern of SLOTS) {
26876
+ pattern.lastIndex = at;
26877
+ const found = pattern.exec(text);
26878
+ if (found !== null && (earliest === null || found.index < earliest.index || found.index === earliest.index && found[0].length > earliest.value.length)) {
26879
+ earliest = { index: found.index, value: found[0] };
26880
+ }
26881
+ }
26882
+ if (earliest === null) {
26883
+ shape += text.slice(at);
26884
+ break;
26885
+ }
26886
+ if (earliest.value.length === 0) {
26887
+ shape += text.slice(at, at + 1);
26888
+ at += 1;
26889
+ continue;
26890
+ }
26891
+ shape += `${text.slice(at, earliest.index)}…`;
26892
+ values.push(earliest.value);
26893
+ at = earliest.index + earliest.value.length;
26894
+ }
26895
+ return { shape, values };
26896
+ }
26897
+ function bursts(entries, before) {
26898
+ const shapes = new Map;
26899
+ const shaped = entries.map((entry) => shapeOf(entry.text));
26900
+ for (const [at, entry] of entries.entries()) {
26901
+ const key = `${entry.severity}\x00${shaped[at]?.shape ?? entry.text}`;
26902
+ const group = shapes.get(key) ?? { indices: [], values: [] };
26903
+ group.indices.push(at);
26904
+ group.values.push(shaped[at]?.values ?? []);
26905
+ shapes.set(key, group);
26906
+ }
26907
+ const collapsed = new Set;
26908
+ for (const group of shapes.values()) {
26909
+ if (group.indices.length > 1) {
26910
+ for (const at of group.indices) {
26911
+ collapsed.add(at);
26912
+ }
26913
+ }
26914
+ }
26915
+ const repeated = [];
26916
+ for (const [key, group] of shapes) {
26917
+ const first = group.indices[0];
26918
+ if (group.indices.length < 2 || first === undefined) {
26919
+ continue;
26920
+ }
26921
+ const entry = entries[first];
26922
+ if (entry === undefined) {
26923
+ continue;
26924
+ }
26925
+ repeated.push({
26926
+ shape: key.slice(key.indexOf("\x00") + 1),
26927
+ count: group.indices.length,
26928
+ severity: entry.severity,
26929
+ first: { index: entry.index, text: entry.text },
26930
+ slots: slotsOf(group.values, group.indices.length),
26931
+ before: linesBefore(entries, first, collapsed, before)
26932
+ });
26933
+ }
26934
+ return repeated.sort((one, other) => other.count - one.count);
26935
+ }
26936
+ function slotsOf(values, count) {
26937
+ const width = Math.max(0, ...values.map((row) => row.length));
26938
+ const distinct = [];
26939
+ for (let at = 0;at < width; at += 1) {
26940
+ distinct.push([...new Set(values.map((row) => row[at]).filter((one) => one !== undefined))]);
26941
+ }
26942
+ const slots = [];
26943
+ for (let at = 0;at < width; at += 1) {
26944
+ const seen = distinct[at] ?? [];
26945
+ const partner = theSlotItMovesWith(values, distinct, at);
26946
+ const paired = partner === undefined ? {} : { movesWith: partner };
26947
+ if (seen.length === count && count > 1) {
26948
+ slots.push({ distinct: seen.length, allDifferent: true, ...paired });
26949
+ continue;
26950
+ }
26951
+ const some = seen.slice(0, NAMED_VALUES);
26952
+ slots.push({
26953
+ distinct: seen.length,
26954
+ some,
26955
+ ...seen.length > some.length ? { more: seen.length - some.length } : {},
26956
+ ...paired
26957
+ });
26958
+ }
26959
+ return slots;
26960
+ }
26961
+ function theSlotItMovesWith(values, distinct, at) {
26962
+ const mine = distinct[at] ?? [];
26963
+ if (mine.length < 2) {
26964
+ return;
26965
+ }
26966
+ for (let other = 0;other < at; other += 1) {
26967
+ if ((distinct[other] ?? []).length !== mine.length) {
26968
+ continue;
26969
+ }
26970
+ const goesWith = new Map;
26971
+ let oneForOne = true;
26972
+ for (const row of values) {
26973
+ const theirs = row[other];
26974
+ const ours = row[at];
26975
+ if (theirs === undefined || ours === undefined) {
26976
+ oneForOne = false;
26977
+ break;
26978
+ }
26979
+ const already = goesWith.get(theirs);
26980
+ if (already === undefined) {
26981
+ goesWith.set(theirs, ours);
26982
+ } else if (already !== ours) {
26983
+ oneForOne = false;
26984
+ break;
26985
+ }
26986
+ }
26987
+ if (oneForOne) {
26988
+ return other;
26989
+ }
26990
+ }
26991
+ return;
26992
+ }
26993
+ function linesBefore(entries, at, collapsed, wanted) {
26994
+ const found = [];
26995
+ for (let back = at - 1;back >= 0 && at - back <= WALK_BACK_LIMIT && found.length < wanted; back -= 1) {
26996
+ const entry = entries[back];
26997
+ if (entry !== undefined && !collapsed.has(back)) {
26998
+ found.push(entry.text);
26999
+ }
27000
+ }
27001
+ return found.reverse();
27002
+ }
27003
+
26725
27004
  // src/godot-bridge.ts
26726
27005
  import { randomUUID } from "node:crypto";
26727
27006
  import { EventEmitter } from "node:events";
@@ -27223,14 +27502,14 @@ function getDefaultBridge(ownProject = null) {
27223
27502
 
27224
27503
  // src/godot-path.ts
27225
27504
  import { execFile as execFile2 } from "node:child_process";
27226
- import { existsSync as existsSync6 } from "node:fs";
27505
+ import { existsSync as existsSync7 } from "node:fs";
27227
27506
  import { normalize } from "node:path";
27228
27507
  import { promisify as promisify2 } from "node:util";
27229
27508
 
27230
27509
  // src/detection.ts
27231
- import { existsSync as existsSync5, readdirSync as readdirSync3, statSync as statSync3 } from "node:fs";
27510
+ import { existsSync as existsSync6, readdirSync as readdirSync3, statSync as statSync4 } from "node:fs";
27232
27511
  import { homedir as homedir2 } from "node:os";
27233
- import { join as join9 } from "node:path";
27512
+ import { join as join10 } from "node:path";
27234
27513
  function resolveHomeDirectory() {
27235
27514
  try {
27236
27515
  return homedir2();
@@ -27239,7 +27518,7 @@ function resolveHomeDirectory() {
27239
27518
  }
27240
27519
  }
27241
27520
  function scanDirectoryForGodotBinaries(directory, platform) {
27242
- if (!directory || !existsSync5(directory)) {
27521
+ if (!directory || !existsSync6(directory)) {
27243
27522
  return [];
27244
27523
  }
27245
27524
  let entries;
@@ -27254,16 +27533,16 @@ function scanDirectoryForGodotBinaries(directory, platform) {
27254
27533
  if (!pattern.test(name)) {
27255
27534
  continue;
27256
27535
  }
27257
- const fullPath = join9(directory, name);
27536
+ const fullPath = join10(directory, name);
27258
27537
  try {
27259
- const stat = statSync3(fullPath);
27538
+ const stat = statSync4(fullPath);
27260
27539
  if (stat.isFile()) {
27261
27540
  matches.push({ name, mtime: stat.mtimeMs });
27262
27541
  }
27263
27542
  } catch {}
27264
27543
  }
27265
27544
  matches.sort((a, b) => b.mtime - a.mtime);
27266
- return matches.map((m) => join9(directory, m.name));
27545
+ return matches.map((m) => join10(directory, m.name));
27267
27546
  }
27268
27547
  function conventionalPaths(platform, home) {
27269
27548
  const paths = ["godot"];
@@ -27366,7 +27645,7 @@ class GodotLocator {
27366
27645
  return known;
27367
27646
  }
27368
27647
  let reason;
27369
- if (path !== "godot" && !existsSync6(path)) {
27648
+ if (path !== "godot" && !existsSync7(path)) {
27370
27649
  reason = "does not exist";
27371
27650
  } else {
27372
27651
  try {
@@ -27383,13 +27662,13 @@ class GodotLocator {
27383
27662
 
27384
27663
  // src/headless.ts
27385
27664
  import { execFile as execFile3 } from "node:child_process";
27386
- import { mkdtempSync, writeFileSync as writeFileSync6 } from "node:fs";
27665
+ import { mkdtempSync, writeFileSync as writeFileSync7 } from "node:fs";
27387
27666
  import { tmpdir as tmpdir3 } from "node:os";
27388
- import { join as join10 } from "node:path";
27667
+ import { join as join11 } from "node:path";
27389
27668
  import { promisify as promisify3 } from "node:util";
27390
27669
 
27391
27670
  // src/scratch.ts
27392
- import { rmSync as rmSync3 } from "node:fs";
27671
+ import { rmSync as rmSync4 } from "node:fs";
27393
27672
  var SWEPT = { recursive: true, force: true, maxRetries: 20, retryDelay: 250 };
27394
27673
  function discardWith(remove, paths) {
27395
27674
  const left = [];
@@ -27407,7 +27686,7 @@ function discardWith(remove, paths) {
27407
27686
  }
27408
27687
  function discard(...paths) {
27409
27688
  return discardWith((path) => {
27410
- rmSync3(path, SWEPT);
27689
+ rmSync4(path, SWEPT);
27411
27690
  }, paths);
27412
27691
  }
27413
27692
 
@@ -27451,12 +27730,12 @@ function reason(stdout, stderr) {
27451
27730
  return stdout.trim().split(/\r?\n/).at(-1) ?? "no output at all";
27452
27731
  }
27453
27732
  async function runImport(godotPath, projectPath) {
27454
- const logDir = mkdtempSync(join10(tmpdir3(), "gdharness-import-"));
27733
+ const logDir = mkdtempSync(join11(tmpdir3(), "gdharness-import-"));
27455
27734
  try {
27456
27735
  const { stderr } = await run3(godotPath, [
27457
27736
  "--headless",
27458
27737
  "--log-file",
27459
- join10(logDir, "engine.log"),
27738
+ join11(logDir, "engine.log"),
27460
27739
  "--path",
27461
27740
  projectPath,
27462
27741
  "--import"
@@ -27481,13 +27760,13 @@ async function runImport(godotPath, projectPath) {
27481
27760
  }
27482
27761
  }
27483
27762
  async function runOperation(engine, operation, params, projectPath) {
27484
- const paramsDir = mkdtempSync(join10(tmpdir3(), "gdharness-params-"));
27485
- const paramsFile = join10(paramsDir, `${operation}.json`);
27486
- writeFileSync6(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
27763
+ const paramsDir = mkdtempSync(join11(tmpdir3(), "gdharness-params-"));
27764
+ const paramsFile = join11(paramsDir, `${operation}.json`);
27765
+ writeFileSync7(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
27487
27766
  const args = [
27488
27767
  "--headless",
27489
27768
  "--log-file",
27490
- join10(paramsDir, "engine.log"),
27769
+ join11(paramsDir, "engine.log"),
27491
27770
  "--path",
27492
27771
  projectPath,
27493
27772
  "--script",
@@ -27916,7 +28195,7 @@ function orphansPrinted(printed) {
27916
28195
  import { realpathSync as realpathSync2 } from "node:fs";
27917
28196
  import { readFile, realpath } from "node:fs/promises";
27918
28197
  import { createConnection as createConnection3 } from "node:net";
27919
- import { dirname as dirname6, resolve as resolve3 } from "node:path";
28198
+ import { dirname as dirname7, resolve as resolve3 } from "node:path";
27920
28199
  import { fileURLToPath, pathToFileURL } from "node:url";
27921
28200
  var DEFAULT_LSP_PORT = 6005;
27922
28201
  var DIAGNOSTICS_TIMEOUT_MS = 5000;
@@ -28046,7 +28325,7 @@ class GodotLSPClient {
28046
28325
  if (this.initialized) {
28047
28326
  return;
28048
28327
  }
28049
- const rootPath = this.rootPath ?? dirname6(resolve3(filePath));
28328
+ const rootPath = this.rootPath ?? dirname7(resolve3(filePath));
28050
28329
  await this.initialize(rootPath);
28051
28330
  }
28052
28331
  async sendRequest(method, params) {
@@ -28482,14 +28761,14 @@ function secondsFromClock(printed) {
28482
28761
  }
28483
28762
 
28484
28763
  // src/project-scan.ts
28485
- import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as readFileSync10 } from "node:fs";
28486
- import { join as join11 } from "node:path";
28764
+ import { existsSync as existsSync8, readdirSync as readdirSync4, readFileSync as readFileSync11 } from "node:fs";
28765
+ import { join as join12 } from "node:path";
28487
28766
  var SKIPPED = new Set(["node_modules"]);
28488
28767
  function skipped(name) {
28489
28768
  return name.startsWith(".") || SKIPPED.has(name);
28490
28769
  }
28491
28770
  function steppedOver(directory) {
28492
- return existsSync7(join11(directory, ".gdignore"));
28771
+ return existsSync8(join12(directory, ".gdignore"));
28493
28772
  }
28494
28773
  var ASSET_EXTENSIONS = new Set(["png", "jpg", "jpeg", "webp", "svg", "ttf", "otf", "wav", "mp3", "ogg"]);
28495
28774
  function projectStructure(projectPath) {
@@ -28503,7 +28782,7 @@ function projectStructure(projectPath) {
28503
28782
  continue;
28504
28783
  }
28505
28784
  if (entry.isDirectory()) {
28506
- visit(join11(directory, entry.name));
28785
+ visit(join12(directory, entry.name));
28507
28786
  } else if (entry.isFile()) {
28508
28787
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
28509
28788
  if (extension === "tscn") {
@@ -28534,14 +28813,14 @@ function scriptsWithoutUid(projectPath) {
28534
28813
  }
28535
28814
  const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
28536
28815
  if (entry.isDirectory()) {
28537
- visit(join11(directory, entry.name), spelled);
28816
+ visit(join12(directory, entry.name), spelled);
28538
28817
  continue;
28539
28818
  }
28540
28819
  if (!entry.isFile()) {
28541
28820
  continue;
28542
28821
  }
28543
28822
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
28544
- if (CARRIES_A_UID.has(extension) && !existsSync7(join11(directory, `${entry.name}.uid`))) {
28823
+ if (CARRIES_A_UID.has(extension) && !existsSync8(join12(directory, `${entry.name}.uid`))) {
28545
28824
  found.push(spelled);
28546
28825
  }
28547
28826
  }
@@ -28576,7 +28855,7 @@ function searchProject(projectPath, options) {
28576
28855
  if (skipped(entry.name)) {
28577
28856
  continue;
28578
28857
  }
28579
- const entryPath = join11(directory, entry.name);
28858
+ const entryPath = join12(directory, entry.name);
28580
28859
  if (entry.isDirectory()) {
28581
28860
  visit(entryPath);
28582
28861
  continue;
@@ -28587,7 +28866,7 @@ function searchProject(projectPath, options) {
28587
28866
  }
28588
28867
  result.summary.files_searched += 1;
28589
28868
  const matches = [];
28590
- for (const [index, line] of readFileSync10(entryPath, "utf8").split(`
28869
+ for (const [index, line] of readFileSync11(entryPath, "utf8").split(`
28591
28870
  `).entries()) {
28592
28871
  if (full()) {
28593
28872
  break;
@@ -28610,7 +28889,7 @@ function searchProject(projectPath, options) {
28610
28889
  }
28611
28890
 
28612
28891
  // src/resources.ts
28613
- import { readFileSync as readFileSync11 } from "node:fs";
28892
+ import { readFileSync as readFileSync12 } from "node:fs";
28614
28893
  import { extname, resolve as resolve4 } from "node:path";
28615
28894
  var STATIC_RESOURCES = [
28616
28895
  {
@@ -28805,7 +29084,7 @@ function readResourceText(uri, getProjectPath) {
28805
29084
  const parsedUri = parseGodotUri(uri);
28806
29085
  if (parsedUri.kind === "project-info") {
28807
29086
  const projectFilePath = resolveProjectFile(projectPath, "project.godot");
28808
- const rawProject = readFileSync11(projectFilePath, "utf-8");
29087
+ const rawProject = readFileSync12(projectFilePath, "utf-8");
28809
29088
  const parsedProject = parseProjectGodot(rawProject);
28810
29089
  return {
28811
29090
  mimeType: "application/json",
@@ -28814,7 +29093,7 @@ function readResourceText(uri, getProjectPath) {
28814
29093
  }
28815
29094
  const filePath = resolveProjectFile(projectPath, parsedUri.resourcePath);
28816
29095
  ensureAllowedExtension(parsedUri.kind, filePath);
28817
- const text = readFileSync11(filePath, "utf-8");
29096
+ const text = readFileSync12(filePath, "utf-8");
28818
29097
  return {
28819
29098
  mimeType: parsedUri.kind === "script" ? "text/x-gdscript" : "text/plain",
28820
29099
  text
@@ -28851,16 +29130,16 @@ function setupResourceHandlers(mcp, getProjectPath) {
28851
29130
  }
28852
29131
 
28853
29132
  // src/restart-note.ts
28854
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync12, rmSync as rmSync4, writeFileSync as writeFileSync7 } from "node:fs";
28855
- import { dirname as dirname7, join as join12 } from "node:path";
29133
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync13, rmSync as rmSync5, writeFileSync as writeFileSync8 } from "node:fs";
29134
+ import { dirname as dirname8, join as join13 } from "node:path";
28856
29135
  function restartNotePath(projectPath) {
28857
- return join12(projectPath, ".godot", "gdharness-restart.json");
29136
+ return join13(projectPath, ".godot", "gdharness-restart.json");
28858
29137
  }
28859
29138
  function noteRestartBegun(note) {
28860
29139
  const path = restartNotePath(note.projectPath);
28861
29140
  try {
28862
- mkdirSync6(dirname7(path), { recursive: true });
28863
- writeFileSync7(path, `${JSON.stringify(note, null, 2)}
29141
+ mkdirSync7(dirname8(path), { recursive: true });
29142
+ writeFileSync8(path, `${JSON.stringify(note, null, 2)}
28864
29143
  `, "utf8");
28865
29144
  } catch (error) {
28866
29145
  console.error(`[SERVER] Could not record the restart at ${path}: ${errorMessage(error)}`);
@@ -28868,7 +29147,7 @@ function noteRestartBegun(note) {
28868
29147
  }
28869
29148
  function restartOwed(projectPath) {
28870
29149
  try {
28871
- const parsed = JSON.parse(readFileSync12(restartNotePath(projectPath), "utf8"));
29150
+ const parsed = JSON.parse(readFileSync13(restartNotePath(projectPath), "utf8"));
28872
29151
  if (typeof parsed !== "object" || parsed === null) {
28873
29152
  return null;
28874
29153
  }
@@ -28883,7 +29162,7 @@ function restartOwed(projectPath) {
28883
29162
  }
28884
29163
  function restartSettled(projectPath) {
28885
29164
  try {
28886
- rmSync4(restartNotePath(projectPath), { force: true });
29165
+ rmSync5(restartNotePath(projectPath), { force: true });
28887
29166
  } catch (error) {
28888
29167
  console.error(`[SERVER] Could not clear the restart note: ${errorMessage(error)}`);
28889
29168
  }
@@ -28891,36 +29170,36 @@ function restartSettled(projectPath) {
28891
29170
 
28892
29171
  // src/run-record.ts
28893
29172
  import { execFileSync as execFileSync2 } from "node:child_process";
28894
- import { mkdirSync as mkdirSync7, openSync, readdirSync as readdirSync5, readFileSync as readFileSync13, rmSync as rmSync5, statSync as statSync4, writeFileSync as writeFileSync8 } from "node:fs";
28895
- import { basename as basename2, join as join13 } from "node:path";
29173
+ import { mkdirSync as mkdirSync8, openSync as openSync2, readdirSync as readdirSync5, readFileSync as readFileSync14, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync9 } from "node:fs";
29174
+ import { basename as basename2, join as join14 } from "node:path";
28896
29175
  function runsDirectory() {
28897
- return join13(runtimeDirectory(), "runs");
29176
+ return join14(runtimeDirectory(), "runs");
28898
29177
  }
28899
29178
  function recordPath() {
28900
- return join13(runsDirectory(), "run.json");
29179
+ return join14(runsDirectory(), "run.json");
28901
29180
  }
28902
29181
  function recordPaths() {
28903
- return runtimeDirectories().map((directory) => join13(directory, "runs", "run.json"));
29182
+ return runtimeDirectories().map((directory) => join14(directory, "runs", "run.json"));
28904
29183
  }
28905
29184
  function openTranscript(startedAt) {
28906
- mkdirSync7(runsDirectory(), { recursive: true });
28907
- const path = join13(runsDirectory(), `run-${startedAt}.log`);
28908
- return { path, fd: openSync(path, "a") };
29185
+ mkdirSync8(runsDirectory(), { recursive: true });
29186
+ const path = join14(runsDirectory(), `run-${startedAt}.log`);
29187
+ return { path, fd: openSync2(path, "a") };
28909
29188
  }
28910
29189
  function writeRunRecord(record) {
28911
- mkdirSync7(runsDirectory(), { recursive: true });
28912
- writeFileSync8(recordPath(), JSON.stringify(record, null, 2), "utf8");
29190
+ mkdirSync8(runsDirectory(), { recursive: true });
29191
+ writeFileSync9(recordPath(), JSON.stringify(record, null, 2), "utf8");
28913
29192
  }
28914
29193
  function editorNotePath() {
28915
- return join13(runsDirectory(), "editor-run.json");
29194
+ return join14(runsDirectory(), "editor-run.json");
28916
29195
  }
28917
29196
  function writeEditorRunNote(note) {
28918
- mkdirSync7(runsDirectory(), { recursive: true });
28919
- writeFileSync8(editorNotePath(), JSON.stringify(note, null, 2), "utf8");
29197
+ mkdirSync8(runsDirectory(), { recursive: true });
29198
+ writeFileSync9(editorNotePath(), JSON.stringify(note, null, 2), "utf8");
28920
29199
  }
28921
29200
  function readEditorRunNote() {
28922
29201
  for (const directory of runtimeDirectories()) {
28923
- const note = editorNoteAt(join13(directory, "runs", "editor-run.json"));
29202
+ const note = editorNoteAt(join14(directory, "runs", "editor-run.json"));
28924
29203
  if (note !== null) {
28925
29204
  return note;
28926
29205
  }
@@ -28930,7 +29209,7 @@ function readEditorRunNote() {
28930
29209
  function editorNoteAt(path) {
28931
29210
  let parsed;
28932
29211
  try {
28933
- parsed = JSON.parse(readFileSync13(path, "utf8"));
29212
+ parsed = JSON.parse(readFileSync14(path, "utf8"));
28934
29213
  } catch {
28935
29214
  return null;
28936
29215
  }
@@ -28953,7 +29232,7 @@ function recordRunEnded(pid, exitCode) {
28953
29232
  return;
28954
29233
  }
28955
29234
  try {
28956
- writeFileSync8(path, JSON.stringify({ ...record, exitCode }, null, 2), "utf8");
29235
+ writeFileSync9(path, JSON.stringify({ ...record, exitCode }, null, 2), "utf8");
28957
29236
  } catch {}
28958
29237
  }
28959
29238
  function readRunRecord() {
@@ -28968,7 +29247,7 @@ function readRunRecord() {
28968
29247
  function recordAt(path) {
28969
29248
  let parsed;
28970
29249
  try {
28971
- parsed = JSON.parse(readFileSync13(path, "utf8"));
29250
+ parsed = JSON.parse(readFileSync14(path, "utf8"));
28972
29251
  } catch {
28973
29252
  return null;
28974
29253
  }
@@ -29013,7 +29292,7 @@ function runningAs(pid) {
29013
29292
  const began = startedAt(pid, null);
29014
29293
  const when = began === null ? {} : { startedAt: began };
29015
29294
  if (process.platform === "linux") {
29016
- const raw = readFileSync13(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
29295
+ const raw = readFileSync14(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
29017
29296
  return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
29018
29297
  }
29019
29298
  const args = execFileSync2("ps", ["-p", String(pid), "-o", "args="], {
@@ -29142,13 +29421,13 @@ function clearRunRecord(ours = () => true) {
29142
29421
  continue;
29143
29422
  }
29144
29423
  try {
29145
- rmSync5(path);
29424
+ rmSync6(path);
29146
29425
  } catch {}
29147
29426
  }
29148
29427
  }
29149
29428
  function ageMs(path, now) {
29150
29429
  try {
29151
- return now - statSync4(path).mtimeMs;
29430
+ return now - statSync5(path).mtimeMs;
29152
29431
  } catch {
29153
29432
  return 0;
29154
29433
  }
@@ -29166,19 +29445,19 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
29166
29445
  if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
29167
29446
  continue;
29168
29447
  }
29169
- const path = join13(directory, entry);
29448
+ const path = join14(directory, entry);
29170
29449
  if (path === keep || ageMs(path, now) < keepMs) {
29171
29450
  continue;
29172
29451
  }
29173
29452
  try {
29174
- rmSync5(path);
29453
+ rmSync6(path);
29175
29454
  } catch {}
29176
29455
  }
29177
29456
  }
29178
29457
 
29179
29458
  // src/setup.ts
29180
- import { cpSync, existsSync as existsSync8, mkdirSync as mkdirSync8, readFileSync as readFileSync14, rmSync as rmSync6, writeFileSync as writeFileSync9 } from "node:fs";
29181
- import { dirname as dirname8, join as join14 } from "node:path";
29459
+ import { cpSync, existsSync as existsSync9, mkdirSync as mkdirSync9, readFileSync as readFileSync15, rmSync as rmSync7, writeFileSync as writeFileSync10 } from "node:fs";
29460
+ import { dirname as dirname9, join as join15 } from "node:path";
29182
29461
  var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
29183
29462
  var RUNTIME_AUTOLOAD = {
29184
29463
  name: "GdharnessRuntime",
@@ -29186,11 +29465,11 @@ var RUNTIME_AUTOLOAD = {
29186
29465
  };
29187
29466
  var VERSION_MARKER = ".gdharness-version";
29188
29467
  function installedAddonVersion(projectPath) {
29189
- const marker = join14(projectPath, "addons", ADDONS[0], VERSION_MARKER);
29190
- if (!existsSync8(marker)) {
29468
+ const marker = join15(projectPath, "addons", ADDONS[0], VERSION_MARKER);
29469
+ if (!existsSync9(marker)) {
29191
29470
  return null;
29192
29471
  }
29193
- const said = readFileSync14(marker, "utf8").trim();
29472
+ const said = readFileSync15(marker, "utf8").trim();
29194
29473
  return said === "" ? null : said;
29195
29474
  }
29196
29475
 
@@ -29210,13 +29489,16 @@ function opTakes(spec, op, name) {
29210
29489
  function argumentsOf(spec, op) {
29211
29490
  return Object.keys(spec.parameters).filter((name) => opTakes(spec, op, name));
29212
29491
  }
29492
+ function theGamePicked() {
29493
+ return "the game this server started or is playing when it is among those running, named under " + "`answeredBy` where there was a choice; else the only game there is; else a refusal naming " + "every game and which argument tells them apart, `pid` when they are all one project and " + "`projectPath` when they are not";
29494
+ }
29213
29495
  var PROJECT_PATH = {
29214
29496
  type: "string",
29215
29497
  description: "Absolute path to the project directory, the one holding project.godot."
29216
29498
  };
29217
29499
  var RUNNING_PROJECT_PATH = {
29218
29500
  type: "string",
29219
- description: "Which game, when more than one is running: the project directory it was started from. Not needed with one game."
29501
+ description: `Which game, when more than one is running: the project directory it was started from. The pick without it, in order: ${theGamePicked().replaceAll("`", "")}. Not needed with one game, nor for a call to this server's own.`
29220
29502
  };
29221
29503
  var RUNNING_PID = {
29222
29504
  type: "number",
@@ -29875,7 +30157,7 @@ var TOOL_SPECS = [
29875
30157
  },
29876
30158
  {
29877
30159
  name: "editor_output",
29878
- 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. Answers with the counts and the verdict as well as the entries. 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 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.",
30160
+ 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. Answers with the counts and the verdict as well as the entries. 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 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.",
29879
30161
  parameters: {
29880
30162
  severity: {
29881
30163
  type: "string",
@@ -29883,10 +30165,12 @@ var TOOL_SPECS = [
29883
30165
  description: "The least severe entry to include. Default info, which is everything."
29884
30166
  },
29885
30167
  sinceLastCall: {
30168
+ ops: ["run"],
29886
30169
  type: "boolean",
29887
30170
  description: "Only entries this has not already answered with at this severity. Default false. Measured per severity floor and not across them, so polling for errors leaves the rest of the output for a later read rather than consuming it, and a read with contains is a search over the whole run and consumes nothing."
29888
30171
  },
29889
30172
  cpu: {
30173
+ ops: ["run"],
29890
30174
  type: "boolean",
29891
30175
  description: "Also answer with cpuSeconds, the processor time the run has used. Default false, because asking costs a subprocess. Worth it when elapsedMs is climbing and nothing is being printed: processor time standing still is a run that has stopped doing anything, rather than one that is slow."
29892
30176
  },
@@ -29897,8 +30181,24 @@ var TOOL_SPECS = [
29897
30181
  limit: {
29898
30182
  type: "number",
29899
30183
  description: "The most entries to answer with, newest kept. Default 200, and omitted says how many matching entries that left out. A run long enough to pass it needs this raised, not just filtered: filtering narrows what counts as matching, and the newest of those is still all one answer carries."
30184
+ },
30185
+ before: {
30186
+ ops: ["editor"],
30187
+ type: "number",
30188
+ description: "How many lines above a repeated group to carry with it, counting only lines that are not themselves part of a group. Default 3. The line that explains a wall of identical errors is the last ordinary line above it, so this walks past the wall rather than a fixed distance back."
29900
30189
  }
29901
30190
  },
30191
+ operations: {
30192
+ run: {
30193
+ summary: "What the game a run is playing has printed, which is everything described above",
30194
+ requires: []
30195
+ },
30196
+ editor: {
30197
+ summary: "What the connected editor itself has printed, from its first line, with repeated messages grouped and the ordinary lines above each group carried with it",
30198
+ requires: []
30199
+ }
30200
+ },
30201
+ defaultOperation: "run",
29902
30202
  requires: []
29903
30203
  },
29904
30204
  {
@@ -30357,7 +30657,7 @@ function buildToolDefinitions() {
30357
30657
  var UPDATE_NOTICE_EVERY = 500;
30358
30658
  var FEEDBACK_NOTICE_EVERY = 250;
30359
30659
  var run5 = promisify5(execFile5);
30360
- var __dirname2 = dirname9(fileURLToPath2(import.meta.url));
30660
+ var __dirname2 = dirname10(fileURLToPath2(import.meta.url));
30361
30661
  var EDITOR_RESTART_TIMEOUT_MS = 90000;
30362
30662
  var HOLD_PING_MS = 2000;
30363
30663
  var SLOWEST_FRAME_RATE = 20;
@@ -30470,6 +30770,7 @@ function endedToStartThis(ended) {
30470
30770
  return ` ${which} was ended to start this one; its output is no longer what editor_output answers about.`;
30471
30771
  }
30472
30772
  var LONGEST_BOOT_NOTED_MS = 5 * 60000;
30773
+ var MOST_GROUPS = 20;
30473
30774
  function runtimeVerdict(endpoint, after) {
30474
30775
  if (endpoint !== null) {
30475
30776
  return { listening: true, pid: endpoint.pid, port: endpoint.port };
@@ -30644,7 +30945,7 @@ function aboveTheRunner(entry) {
30644
30945
  }
30645
30946
  function elsewhereIn(projectPath, asked) {
30646
30947
  const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
30647
- if (wanted === "" || existsSync9(join15(projectPath, wanted))) {
30948
+ if (wanted === "" || existsSync10(join16(projectPath, wanted))) {
30648
30949
  return "";
30649
30950
  }
30650
30951
  const root = (wanted.split("/")[0] ?? "").toLowerCase();
@@ -30658,7 +30959,7 @@ function elsewhereIn(projectPath, asked) {
30658
30959
  return near.length === 0 ? "" : ` The project does have ${there}.`;
30659
30960
  }
30660
30961
  function hasMainScene(projectFile) {
30661
- const scene = parseProjectGodot(readFileSync15(projectFile, "utf8"))["application"]?.["run/main_scene"];
30962
+ const scene = parseProjectGodot(readFileSync16(projectFile, "utf8"))["application"]?.["run/main_scene"];
30662
30963
  return typeof scene === "string" && scene !== "";
30663
30964
  }
30664
30965
  function runtimeAddonAnnouncesEditors(projectPath) {
@@ -30666,13 +30967,13 @@ function runtimeAddonAnnouncesEditors(projectPath) {
30666
30967
  return false;
30667
30968
  }
30668
30969
  try {
30669
- return readFileSync15(join15(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
30970
+ return readFileSync16(join16(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
30670
30971
  } catch {
30671
30972
  return false;
30672
30973
  }
30673
30974
  }
30674
30975
  function editorPlaysHeadless(projectFile) {
30675
- const runArgs = parseProjectGodot(readFileSync15(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
30976
+ const runArgs = parseProjectGodot(readFileSync16(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
30676
30977
  return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
30677
30978
  }
30678
30979
  function camelCased(params) {
@@ -30691,7 +30992,7 @@ function needsItsOwnProcess(asked) {
30691
30992
  class GodotServer {
30692
30993
  mcp;
30693
30994
  locator = new GodotLocator;
30694
- operationsScript = join15(__dirname2, "godot", "operations", "godot_operations.gd");
30995
+ operationsScript = join16(__dirname2, "godot", "operations", "godot_operations.gd");
30695
30996
  godotBridge;
30696
30997
  tools = buildToolDefinitions();
30697
30998
  activeProcess = null;
@@ -31356,8 +31657,8 @@ class GodotServer {
31356
31657
  if (path === undefined) {
31357
31658
  return { ok: false, response: this.createErrorResponse("projectPath is required.") };
31358
31659
  }
31359
- const file = join15(path, "project.godot");
31360
- if (!existsSync9(file)) {
31660
+ const file = join16(path, "project.godot");
31661
+ if (!existsSync10(file)) {
31361
31662
  return {
31362
31663
  ok: false,
31363
31664
  response: this.createErrorResponse(`Not a Godot project: ${path}`, [
@@ -31625,7 +31926,7 @@ class GodotServer {
31625
31926
  if (!engine.ok) {
31626
31927
  return engine.response;
31627
31928
  }
31628
- const application = parseProjectGodot(readFileSync15(project.value.file, "utf8"))["application"] ?? {};
31929
+ const application = parseProjectGodot(readFileSync16(project.value.file, "utf8"))["application"] ?? {};
31629
31930
  const name = application["config/name"];
31630
31931
  const mainScene = application["run/main_scene"];
31631
31932
  const info = {
@@ -31686,18 +31987,18 @@ class GodotServer {
31686
31987
  if (!engine.ok) {
31687
31988
  return engine.response;
31688
31989
  }
31689
- const folder = dirname9(output.absolutePath);
31990
+ const folder = dirname10(output.absolutePath);
31690
31991
  try {
31691
- mkdirSync9(folder, { recursive: true });
31992
+ mkdirSync10(folder, { recursive: true });
31692
31993
  } catch (error) {
31693
- return this.createErrorResponse(`Cannot create ${dirname9(output.relativePath)} to export into: ${errorMessage(error)}`);
31994
+ return this.createErrorResponse(`Cannot create ${dirname10(output.relativePath)} to export into: ${errorMessage(error)}`);
31694
31995
  }
31695
31996
  const debug = readBoolean(args, "debug") ?? false;
31696
- const exportLogs = mkdtempSync2(join15(tmpdir4(), "gdharness-export-"));
31997
+ const exportLogs = mkdtempSync2(join16(tmpdir4(), "gdharness-export-"));
31697
31998
  const exportArgs = [
31698
31999
  "--headless",
31699
32000
  "--log-file",
31700
- join15(exportLogs, "engine.log"),
32001
+ join16(exportLogs, "engine.log"),
31701
32002
  "--path",
31702
32003
  project.value.path,
31703
32004
  debug ? "--export-debug" : "--export-release",
@@ -31725,7 +32026,7 @@ class GodotServer {
31725
32026
  log.finish();
31726
32027
  const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
31727
32028
  const verdict = {
31728
- exported: exitCode === 0 && log.count("error") === 0 && existsSync9(output.absolutePath),
32029
+ exported: exitCode === 0 && log.count("error") === 0 && existsSync10(output.absolutePath),
31729
32030
  preset,
31730
32031
  outputPath: output.relativePath,
31731
32032
  debug,
@@ -31755,7 +32056,7 @@ class GodotServer {
31755
32056
  return contained.response;
31756
32057
  }
31757
32058
  const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
31758
- if (!existsSync9(join15(project.value.path, runner))) {
32059
+ if (!existsSync10(join16(project.value.path, runner))) {
31759
32060
  return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
31760
32061
  "Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
31761
32062
  ]);
@@ -31790,7 +32091,7 @@ class GodotServer {
31790
32091
  ];
31791
32092
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
31792
32093
  this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
31793
- const userData = mkdtempSync2(join15(tmpdir4(), "gdharness-tests-"));
32094
+ const userData = mkdtempSync2(join16(tmpdir4(), "gdharness-tests-"));
31794
32095
  const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
31795
32096
  const hung = await new Promise((resolve) => {
31796
32097
  const timer = setTimeout(() => {
@@ -31806,21 +32107,21 @@ class GodotServer {
31806
32107
  resolve(false);
31807
32108
  });
31808
32109
  });
31809
- const reportsDir = join15(project.value.path, ".godot", ...ours.split("/"));
32110
+ const reportsDir = join16(project.value.path, ".godot", ...ours.split("/"));
31810
32111
  let report = null;
31811
32112
  let reportProblem = null;
31812
32113
  try {
31813
- const written = existsSync9(reportsDir) ? readdirSync6(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
32114
+ const written = existsSync10(reportsDir) ? readdirSync6(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
31814
32115
  const newest = written.at(-1);
31815
32116
  if (newest !== undefined) {
31816
- report = parseJUnit(readFileSync15(join15(reportsDir, newest, "results.xml"), "utf8"));
32117
+ report = parseJUnit(readFileSync16(join16(reportsDir, newest, "results.xml"), "utf8"));
31817
32118
  }
31818
32119
  } catch (error) {
31819
32120
  reportProblem = errorMessage(error);
31820
32121
  } finally {
31821
32122
  discard(reportsDir, userData);
31822
32123
  try {
31823
- rmdirSync(dirname9(reportsDir));
32124
+ rmdirSync(dirname10(reportsDir));
31824
32125
  } catch {}
31825
32126
  }
31826
32127
  const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries.map(aboveTheRunner);
@@ -31952,11 +32253,11 @@ class GodotServer {
31952
32253
  const cached = cachedClasses(projectPath);
31953
32254
  const contradicted = cached === null ? [] : contradictedDiagnostics(messages, cached, (resourcePath) => {
31954
32255
  const contained = resolveWithinProject(projectPath, resourcePath);
31955
- if (!contained.ok || !existsSync9(contained.absolutePath)) {
32256
+ if (!contained.ok || !existsSync10(contained.absolutePath)) {
31956
32257
  return null;
31957
32258
  }
31958
32259
  try {
31959
- return readFileSync15(contained.absolutePath, "utf8");
32260
+ return readFileSync16(contained.absolutePath, "utf8");
31960
32261
  } catch {
31961
32262
  return null;
31962
32263
  }
@@ -32006,11 +32307,11 @@ class GodotServer {
32006
32307
  }
32007
32308
  sourceOfClass(projectPath, resourcePath) {
32008
32309
  const contained = resolveWithinProject(projectPath, resourcePath);
32009
- if (!contained.ok || !existsSync9(contained.absolutePath)) {
32310
+ if (!contained.ok || !existsSync10(contained.absolutePath)) {
32010
32311
  return null;
32011
32312
  }
32012
32313
  try {
32013
- return readFileSync15(contained.absolutePath, "utf8");
32314
+ return readFileSync16(contained.absolutePath, "utf8");
32014
32315
  } catch {
32015
32316
  return null;
32016
32317
  }
@@ -32239,7 +32540,7 @@ class GodotServer {
32239
32540
  return null;
32240
32541
  }
32241
32542
  try {
32242
- return settingKeys(readFileSync15(join15(projectPath, "project.godot"), "utf8"));
32543
+ return settingKeys(readFileSync16(join16(projectPath, "project.godot"), "utf8"));
32243
32544
  } catch {
32244
32545
  return null;
32245
32546
  }
@@ -32369,7 +32670,8 @@ class GodotServer {
32369
32670
  });
32370
32671
  }
32371
32672
  async openAnEditor(engine, projectPath, ports) {
32372
- const editor = spawn(engine, editorArguments(projectPath, ports), {
32673
+ clearEditorLog(projectPath);
32674
+ const editor = spawn(engine, editorArguments(projectPath, ports, editorLogPath(projectPath)), {
32373
32675
  stdio: "ignore",
32374
32676
  detached: true,
32375
32677
  env: {
@@ -32380,6 +32682,9 @@ class GodotServer {
32380
32682
  [OPENED_BY_A_SERVER]: "1"
32381
32683
  }
32382
32684
  });
32685
+ if (editor.pid !== undefined) {
32686
+ writeEditorLogNote(projectPath, editor.pid);
32687
+ }
32383
32688
  const started = await new Promise((resolve) => {
32384
32689
  editor.once("spawn", () => {
32385
32690
  resolve(null);
@@ -32486,7 +32791,7 @@ class GodotServer {
32486
32791
  }
32487
32792
  async runtimeUp(projectPath, before, budget, withArgs) {
32488
32793
  const budgetMs = budget.ms;
32489
- if (!existsSync9(join15(projectPath, RUNTIME_AUTOLOAD.path))) {
32794
+ if (!existsSync10(join16(projectPath, RUNTIME_AUTOLOAD.path))) {
32490
32795
  return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
32491
32796
  }
32492
32797
  let asking = null;
@@ -32549,7 +32854,7 @@ class GodotServer {
32549
32854
  }
32550
32855
  let announcedAt;
32551
32856
  try {
32552
- announcedAt = statSync5(endpoint.file).mtimeMs;
32857
+ announcedAt = statSync6(endpoint.file).mtimeMs;
32553
32858
  } catch {
32554
32859
  return;
32555
32860
  }
@@ -32585,7 +32890,7 @@ class GodotServer {
32585
32890
  }
32586
32891
  const startedAt = Date.now();
32587
32892
  const transcript = openTranscript(startedAt);
32588
- closeSync(transcript.fd);
32893
+ closeSync2(transcript.fd);
32589
32894
  const playAnswer = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
32590
32895
  const played = {
32591
32896
  process: null,
@@ -32652,7 +32957,7 @@ class GodotServer {
32652
32957
  }
32653
32958
  let handle;
32654
32959
  try {
32655
- handle = openSync2(game.errorReport, "r");
32960
+ handle = openSync3(game.errorReport, "r");
32656
32961
  } catch {
32657
32962
  return;
32658
32963
  }
@@ -32663,7 +32968,7 @@ class GodotServer {
32663
32968
  return;
32664
32969
  }
32665
32970
  const buffer = Buffer.alloc(size - from);
32666
- const read = readSync(handle, buffer, 0, buffer.length, from);
32971
+ const read = readSync2(handle, buffer, 0, buffer.length, from);
32667
32972
  game.errorReportOffset = from + read;
32668
32973
  const reported = buffer.subarray(0, read);
32669
32974
  if (game.transcript !== null) {
@@ -32672,7 +32977,7 @@ class GodotServer {
32672
32977
  game.log.append("stderr", reported);
32673
32978
  }
32674
32979
  } finally {
32675
- closeSync(handle);
32980
+ closeSync2(handle);
32676
32981
  }
32677
32982
  }
32678
32983
  recordWhatItBrokeOn(game) {
@@ -32766,7 +33071,7 @@ class GodotServer {
32766
33071
  ...env ? { env } : {}
32767
33072
  });
32768
33073
  } finally {
32769
- closeSync(transcript.fd);
33074
+ closeSync2(transcript.fd);
32770
33075
  }
32771
33076
  child.unref();
32772
33077
  const log = new GameLog;
@@ -32813,7 +33118,7 @@ class GodotServer {
32813
33118
  }
32814
33119
  let handle;
32815
33120
  try {
32816
- handle = openSync2(game.transcript, "r");
33121
+ handle = openSync3(game.transcript, "r");
32817
33122
  } catch {
32818
33123
  return;
32819
33124
  }
@@ -32823,11 +33128,11 @@ class GodotServer {
32823
33128
  return;
32824
33129
  }
32825
33130
  const buffer = Buffer.alloc(size - game.readOffset);
32826
- const read = readSync(handle, buffer, 0, buffer.length, game.readOffset);
33131
+ const read = readSync2(handle, buffer, 0, buffer.length, game.readOffset);
32827
33132
  game.readOffset += read;
32828
33133
  game.log.append("transcript", buffer.subarray(0, read));
32829
33134
  } finally {
32830
- closeSync(handle);
33135
+ closeSync2(handle);
32831
33136
  }
32832
33137
  }
32833
33138
  async pickUpWhatTheEditorIsPlaying() {
@@ -33025,7 +33330,7 @@ class GodotServer {
33025
33330
  }
33026
33331
  const unreadable = this.aRuntimeOfOursIsTooFarOff();
33027
33332
  if (unreadable !== null) {
33028
- const behind = unreadable.protocol > RUNTIME_PROTOCOL ? "this server is the older half: reconnect it so it spawns the installed version" : "the addon is the older half: reinstall it and restart the game";
33333
+ const behind = whichHalfIsBehind([unreadable]);
33029
33334
  return `A game is running for ${unreadable.project.path}, pid ${unreadable.pid}, announced in protocol` + ` ${unreadable.protocol}, which this server does not speak: it speaks ${RUNTIME_PROTOCOL}, so ${behind}.` + " Until then neither editor_run nor the runtime_* tools can reach it." + " A run started here is a separate process rather than a replacement, so starting one leaves" + " this game running as well.";
33030
33335
  }
33031
33336
  const record = readRunRecord();
@@ -33150,7 +33455,57 @@ class GodotServer {
33150
33455
  }
33151
33456
  return runIsUp(run, said);
33152
33457
  }
33458
+ handleEditorConsole(args) {
33459
+ const status = this.godotBridge.getStatus();
33460
+ const projectPath = status.projectPath ?? this.ownProject ?? undefined;
33461
+ if (projectPath === undefined) {
33462
+ return this.createErrorResponse("No editor is connected, so there is no console to read.", [
33463
+ "editor_status says whether one is connected and whether it may yet be",
33464
+ "editor_launch opens one, and an editor opened here has its console captured"
33465
+ ]);
33466
+ }
33467
+ const console2 = editorConsole(projectPath, status.editorPid ?? null, status.openedByAServer === true);
33468
+ if ("kind" in console2) {
33469
+ return this.createErrorResponse(theConsoleWasNotCaptured(console2, projectPath), [
33470
+ "editor_launch restart replaces this editor with one whose console is captured",
33471
+ "editor_output with no op answers about the game a run is playing, which is a different log"
33472
+ ]);
33473
+ }
33474
+ const severity = readString(args, "severity");
33475
+ const selected = console2.log.select({
33476
+ severity: severity === "error" || severity === "warning" ? severity : "info",
33477
+ sinceLastCall: false,
33478
+ contains: readNonEmptyString(args, "contains"),
33479
+ limit: readPositiveNumber(args, "limit") ?? 200
33480
+ });
33481
+ const everything = console2.log.everything();
33482
+ const grouped = bursts(everything, readPositiveNumber(args, "before") ?? 3);
33483
+ const shown = grouped.slice(0, MOST_GROUPS);
33484
+ return this.jsonTextResponse({
33485
+ editorPid: status.editorPid,
33486
+ projectPath,
33487
+ capturedIn: console2.path,
33488
+ ...console2.of === undefined ? {} : {
33489
+ readFromTheStart: console2.readBytes,
33490
+ consoleBytes: console2.of,
33491
+ truncatedNote: `This editor's console is ${console2.of} bytes and the first ${console2.readBytes} of them were read, which is the start of the session rather than the end of it. Restart the editor to begin a fresh console.`
33492
+ },
33493
+ lines: everything.length,
33494
+ counts: {
33495
+ error: console2.log.count("error"),
33496
+ warning: console2.log.count("warning"),
33497
+ info: console2.log.count("info")
33498
+ },
33499
+ repeated: shown,
33500
+ moreShapes: grouped.length > shown.length ? grouped.length - shown.length : undefined,
33501
+ entries: forAnswer(selected.entries),
33502
+ omitted: selected.omitted === 0 ? undefined : selected.omitted
33503
+ });
33504
+ }
33153
33505
  async handleGetDebugOutput(args, waitedMs) {
33506
+ if (readString(args, "op") === "editor") {
33507
+ return this.handleEditorConsole(args);
33508
+ }
33154
33509
  await this.pickUpWhatTheEditorIsPlaying();
33155
33510
  const run = this.currentRun();
33156
33511
  if (!run) {
@@ -33601,8 +33956,8 @@ class GodotServer {
33601
33956
  ]);
33602
33957
  }
33603
33958
  const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
33604
- const screenshotDir = expectsScreenshot ? mkdtempSync2(join15(tmpdir4(), "gdharness-runtime-screenshot-")) : null;
33605
- const screenshotPath = screenshotDir ? join15(screenshotDir, "capture.png") : null;
33959
+ const screenshotDir = expectsScreenshot ? mkdtempSync2(join16(tmpdir4(), "gdharness-runtime-screenshot-")) : null;
33960
+ const screenshotPath = screenshotDir ? join16(screenshotDir, "capture.png") : null;
33606
33961
  try {
33607
33962
  const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs);
33608
33963
  if (!reply.ok) {
@@ -33626,7 +33981,7 @@ class GodotServer {
33626
33981
  type: "text",
33627
33982
  text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}`
33628
33983
  },
33629
- { type: "image", data: readFileSync15(screenshotPath).toString("base64"), mimeType: "image/png" }
33984
+ { type: "image", data: readFileSync16(screenshotPath).toString("base64"), mimeType: "image/png" }
33630
33985
  ]
33631
33986
  };
33632
33987
  } finally {