gdharness 0.10.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/cli.js CHANGED
@@ -155,6 +155,9 @@ function runArguments(options) {
155
155
  if (options.scene !== null) {
156
156
  args.push(`res://${options.scene}`);
157
157
  }
158
+ if (options.userArgs !== undefined && options.userArgs.length > 0) {
159
+ args.push("--", ...options.userArgs);
160
+ }
158
161
  return args;
159
162
  }
160
163
  function editorArguments(projectPath, ports) {
@@ -7187,8 +7190,12 @@ function announced(line) {
7187
7190
 
7188
7191
  class GameLog {
7189
7192
  entries = [];
7190
- decoders = { stdout: new StringDecoder("utf8"), stderr: new StringDecoder("utf8") };
7191
- partial = { stdout: "", stderr: "" };
7193
+ decoders = {
7194
+ stdout: new StringDecoder("utf8"),
7195
+ stderr: new StringDecoder("utf8"),
7196
+ transcript: new StringDecoder("utf8")
7197
+ };
7198
+ partial = { stdout: "", stderr: "", transcript: "" };
7192
7199
  lastHeadline = null;
7193
7200
  cursor = 0;
7194
7201
  append(source, chunk) {
@@ -7201,7 +7208,7 @@ class GameLog {
7201
7208
  }
7202
7209
  }
7203
7210
  finish() {
7204
- for (const source of ["stdout", "stderr"]) {
7211
+ for (const source of STREAMS) {
7205
7212
  const tail = this.partial[source] + this.decoders[source].end();
7206
7213
  this.partial[source] = "";
7207
7214
  if (tail !== "") {
@@ -7264,10 +7271,11 @@ class GameLog {
7264
7271
  return { entries: matching.slice(omitted), omitted };
7265
7272
  }
7266
7273
  }
7267
- var HEADLINE, COLOUR_CODE;
7274
+ var HEADLINE, COLOUR_CODE, STREAMS;
7268
7275
  var init_game_log = __esm(() => {
7269
7276
  HEADLINE = /^(USER )?(SCRIPT ERROR|ERROR|WARNING):\s?(.*)$/;
7270
7277
  COLOUR_CODE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
7278
+ STREAMS = ["stdout", "stderr", "transcript"];
7271
7279
  });
7272
7280
 
7273
7281
  // src/update-check.ts
@@ -15405,7 +15413,7 @@ var init_tool_definitions = __esm(() => {
15405
15413
  },
15406
15414
  {
15407
15415
  name: "project_test",
15408
- description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Suites where everything passed are counted rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines. The class list is rebuilt first, so a suite written a moment ago is found. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so and names the path it looked in. Needs gdUnit4 under addons/gdUnit4.",
15416
+ description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Suites where everything passed are counted rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines. The class list is rebuilt first, so a suite written a moment ago is found. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so and names the path it looked in. A run where nothing failed but nodes were left in the tree comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. Needs gdUnit4 under addons/gdUnit4.",
15409
15417
  parameters: {
15410
15418
  projectPath: PROJECT_PATH,
15411
15419
  path: {
@@ -15680,10 +15688,15 @@ var init_tool_definitions = __esm(() => {
15680
15688
  },
15681
15689
  {
15682
15690
  name: "editor_run",
15683
- description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. What runs is a scene: a SceneTree script is not an entry point here, so put the script on the root of a scene of its own and name that in scene. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way. A start waits for the game to become something the runtime_* tools can talk to and says which it is under runtime: listening with the port it took, or why not, so the first call after a start does not have to be made twice.",
15691
+ description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. What runs is a scene: a SceneTree script is not an entry point here, so put the script on the root of a scene of its own and name that in scene. args hands the game its own flags, the ones it reads with OS.get_cmdline_user_args(), and a run carrying any is started by this server rather than by the editor. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way. A start waits for the game to become something the runtime_* tools can talk to and says which it is under runtime: listening with the port it took, or why not, so the first call after a start does not have to be made twice.",
15684
15692
  parameters: {
15685
15693
  projectPath: PROJECT_PATH,
15686
15694
  scene: { type: "string", description: "A scene to run instead of the main scene." },
15695
+ args: {
15696
+ type: "array",
15697
+ items: { type: "string" },
15698
+ description: `The game's own arguments, what OS.get_cmdline_user_args() answers, such as ["--level=2"]. The separator is added here. A run with any is started by this server rather than by the editor, which fixes the game's command line when it opens the project, so the debug_* tools do not answer for it.`
15699
+ },
15687
15700
  headless: { type: "boolean", description: "start: force a window or no window." },
15688
15701
  frames: { type: "number", description: "check: frames to run before quitting. Default 3." },
15689
15702
  timeoutMs: {
@@ -15812,8 +15825,8 @@ var init_tool_definitions = __esm(() => {
15812
15825
  },
15813
15826
  includeHidden: {
15814
15827
  type: "boolean",
15815
- ops: ["text"],
15816
- description: "text: read hidden nodes as well, for checking that something is not showing. Default false."
15828
+ ops: ["text", "find"],
15829
+ description: "text: read hidden nodes as well, for checking that something is not showing. Default false. find: default true, since a find means the node whether or not it is drawn; false answers with what the player can actually see, which is how a panel that keeps a label for every line and hides the ones that do not is read. A node counts as hidden when anything above it is, and how many matches were left out comes back under hidden."
15817
15830
  },
15818
15831
  metrics: {
15819
15832
  type: "array",
@@ -15830,7 +15843,7 @@ var init_tool_definitions = __esm(() => {
15830
15843
  requires: []
15831
15844
  },
15832
15845
  find: {
15833
- summary: "the paths of every node matching className, script, namePattern, group or says, with property read off each",
15846
+ summary: "the paths of every node matching className, script, namePattern, group or says, with property read off each, hidden ones included unless includeHidden says otherwise",
15834
15847
  requires: []
15835
15848
  },
15836
15849
  rect: {
@@ -35160,7 +35173,37 @@ function whyNoReport(printed, asked) {
35160
35173
  }
35161
35174
  return printed.some((line) => NOTHING_RAN.test(line)) ? `no test cases found at ${asked}` : null;
35162
35175
  }
35163
- var MalformedReportError, ENTITIES, ATTRIBUTE, NOTHING_RAN, NO_SUCH_PATH;
35176
+ function orphansPrinted(printed) {
35177
+ const suites = [];
35178
+ let total = null;
35179
+ let running = null;
35180
+ for (const line of printed) {
35181
+ const started = RUNNING_SUITE.exec(line);
35182
+ if (started?.[1] !== undefined) {
35183
+ running = started[1];
35184
+ continue;
35185
+ }
35186
+ const counted = ORPHANS.exec(line);
35187
+ if (counted?.[1] === undefined) {
35188
+ continue;
35189
+ }
35190
+ const orphans = Number(counted[1]);
35191
+ if (line.includes(OVERALL)) {
35192
+ total = orphans;
35193
+ running = null;
35194
+ continue;
35195
+ }
35196
+ if (running !== null && orphans > 0) {
35197
+ suites.push({ path: running, orphans });
35198
+ }
35199
+ running = null;
35200
+ }
35201
+ return {
35202
+ total: total ?? suites.reduce((sum, suite) => sum + suite.orphans, 0),
35203
+ suites
35204
+ };
35205
+ }
35206
+ var MalformedReportError, ENTITIES, ATTRIBUTE, NOTHING_RAN, NO_SUCH_PATH, RUNNING_SUITE, ORPHANS, OVERALL = "Overall Summary";
35164
35207
  var init_junit = __esm(() => {
35165
35208
  MalformedReportError = class MalformedReportError extends Error {
35166
35209
  constructor(message) {
@@ -35172,6 +35215,8 @@ var init_junit = __esm(() => {
35172
35215
  ATTRIBUTE = /\s*([^\s=/>]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/y;
35173
35216
  NOTHING_RAN = /no test cases found/i;
35174
35217
  NO_SUCH_PATH = /given directory or file does not exists:\s*(\S+)/i;
35218
+ RUNNING_SUITE = /Run Test Suite:\s*(\S+)/;
35219
+ ORPHANS = /(\d+)\s+orphans/;
35175
35220
  });
35176
35221
 
35177
35222
  // src/lsp_client.ts
@@ -35475,6 +35520,14 @@ class GodotLSPClient {
35475
35520
  }
35476
35521
  return uri;
35477
35522
  }
35523
+ closeDocument(uri) {
35524
+ if (!this.documentVersions.delete(uri)) {
35525
+ return;
35526
+ }
35527
+ try {
35528
+ this.sendNotification("textDocument/didClose", { textDocument: { uri } });
35529
+ } catch {}
35530
+ }
35478
35531
  async initialize(rootPath) {
35479
35532
  await this.ensureConnected();
35480
35533
  const resolvedRootPath = resolve3(rootPath);
@@ -35528,8 +35581,9 @@ class GodotLSPClient {
35528
35581
  timer
35529
35582
  });
35530
35583
  });
35584
+ let opened = null;
35531
35585
  try {
35532
- this.syncDocument(filePath, content);
35586
+ opened = this.syncDocument(filePath, content);
35533
35587
  return await diagnosticsPromise;
35534
35588
  } catch (error) {
35535
35589
  const waiter = this.diagnosticsWaiters.get(key);
@@ -35538,16 +35592,25 @@ class GodotLSPClient {
35538
35592
  this.diagnosticsWaiters.delete(key);
35539
35593
  }
35540
35594
  throw error;
35595
+ } finally {
35596
+ if (opened !== null) {
35597
+ this.closeDocument(opened);
35598
+ }
35541
35599
  }
35542
35600
  }
35543
35601
  async getCompletions(filePath, content, line, character) {
35544
35602
  await this.ensureConnected();
35545
35603
  await this.ensureInitializedForFile(filePath);
35546
35604
  const uri = this.syncDocument(filePath, content);
35547
- const result = await this.sendRequest("textDocument/completion", {
35548
- textDocument: { uri },
35549
- position: { line, character }
35550
- });
35605
+ let result;
35606
+ try {
35607
+ result = await this.sendRequest("textDocument/completion", {
35608
+ textDocument: { uri },
35609
+ position: { line, character }
35610
+ });
35611
+ } finally {
35612
+ this.closeDocument(uri);
35613
+ }
35551
35614
  if (Array.isArray(result)) {
35552
35615
  return result;
35553
35616
  }
@@ -35563,18 +35626,27 @@ class GodotLSPClient {
35563
35626
  await this.ensureConnected();
35564
35627
  await this.ensureInitializedForFile(filePath);
35565
35628
  const uri = this.syncDocument(filePath, content);
35566
- return this.sendRequest("textDocument/hover", {
35567
- textDocument: { uri },
35568
- position: { line, character }
35569
- });
35629
+ try {
35630
+ return await this.sendRequest("textDocument/hover", {
35631
+ textDocument: { uri },
35632
+ position: { line, character }
35633
+ });
35634
+ } finally {
35635
+ this.closeDocument(uri);
35636
+ }
35570
35637
  }
35571
35638
  async getDocumentSymbols(filePath, content) {
35572
35639
  await this.ensureConnected();
35573
35640
  await this.ensureInitializedForFile(filePath);
35574
35641
  const uri = this.syncDocument(filePath, content);
35575
- const result = await this.sendRequest("textDocument/documentSymbol", {
35576
- textDocument: { uri }
35577
- });
35642
+ let result;
35643
+ try {
35644
+ result = await this.sendRequest("textDocument/documentSymbol", {
35645
+ textDocument: { uri }
35646
+ });
35647
+ } finally {
35648
+ this.closeDocument(uri);
35649
+ }
35578
35650
  return Array.isArray(result) ? result : [];
35579
35651
  }
35580
35652
  isConnected() {
@@ -36016,15 +36088,97 @@ var init_runtime_client = __esm(() => {
36016
36088
  ANNOUNCEMENT_PATTERN = /^runtime-(\d+)\.json$/;
36017
36089
  });
36018
36090
 
36091
+ // src/run-record.ts
36092
+ import { mkdirSync as mkdirSync6, openSync, readdirSync as readdirSync6, readFileSync as readFileSync12, rmSync as rmSync7, statSync as statSync2, writeFileSync as writeFileSync8 } from "node:fs";
36093
+ import { join as join14 } from "node:path";
36094
+ function runsDirectory() {
36095
+ return join14(runtimeDirectory(), "runs");
36096
+ }
36097
+ function recordPath() {
36098
+ return join14(runsDirectory(), "run.json");
36099
+ }
36100
+ function openTranscript(startedAt) {
36101
+ mkdirSync6(runsDirectory(), { recursive: true });
36102
+ const path = join14(runsDirectory(), `run-${startedAt}.log`);
36103
+ return { path, fd: openSync(path, "a") };
36104
+ }
36105
+ function writeRunRecord(record) {
36106
+ mkdirSync6(runsDirectory(), { recursive: true });
36107
+ writeFileSync8(recordPath(), JSON.stringify(record, null, 2), "utf8");
36108
+ }
36109
+ function readRunRecord() {
36110
+ let parsed;
36111
+ try {
36112
+ parsed = JSON.parse(readFileSync12(recordPath(), "utf8"));
36113
+ } catch {
36114
+ return null;
36115
+ }
36116
+ if (!parsed || typeof parsed !== "object") {
36117
+ return null;
36118
+ }
36119
+ const fields = parsed;
36120
+ const pid = fields["pid"];
36121
+ const transcript = fields["transcript"];
36122
+ const startedAt = fields["startedAt"];
36123
+ if (typeof pid !== "number" || typeof transcript !== "string" || typeof startedAt !== "number") {
36124
+ return null;
36125
+ }
36126
+ const args = fields["arguments"];
36127
+ return {
36128
+ pid,
36129
+ transcript,
36130
+ startedAt,
36131
+ projectPath: typeof fields["projectPath"] === "string" ? fields["projectPath"] : "",
36132
+ arguments: Array.isArray(args) ? args.filter((value) => typeof value === "string") : []
36133
+ };
36134
+ }
36135
+ function clearRunRecord() {
36136
+ try {
36137
+ rmSync7(recordPath());
36138
+ } catch {}
36139
+ }
36140
+ function ageMs(path, now) {
36141
+ try {
36142
+ return now - statSync2(path).mtimeMs;
36143
+ } catch {
36144
+ return 0;
36145
+ }
36146
+ }
36147
+ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
36148
+ const directory = runsDirectory();
36149
+ const keep = readRunRecord()?.transcript;
36150
+ let entries;
36151
+ try {
36152
+ entries = readdirSync6(directory);
36153
+ } catch {
36154
+ return;
36155
+ }
36156
+ for (const entry of entries) {
36157
+ if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
36158
+ continue;
36159
+ }
36160
+ const path = join14(directory, entry);
36161
+ if (path === keep || ageMs(path, now) < keepMs) {
36162
+ continue;
36163
+ }
36164
+ try {
36165
+ rmSync7(path);
36166
+ } catch {}
36167
+ }
36168
+ }
36169
+ var init_run_record = __esm(() => {
36170
+ init_runtime_client();
36171
+ });
36172
+
36019
36173
  // src/setup.ts
36020
- import { cpSync as cpSync2, existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync12, rmSync as rmSync7, writeFileSync as writeFileSync8 } from "node:fs";
36021
- import { dirname as dirname8, join as join14 } from "node:path";
36174
+ import { cpSync as cpSync2, existsSync as existsSync11, mkdirSync as mkdirSync7, readFileSync as readFileSync13, rmSync as rmSync8, writeFileSync as writeFileSync9 } from "node:fs";
36175
+ import { dirname as dirname8, join as join15 } from "node:path";
36022
36176
  function installedAddonVersion(projectPath) {
36023
- const marker = join14(projectPath, "addons", ADDONS2[0], VERSION_MARKER2);
36177
+ const marker = join15(projectPath, "addons", ADDONS2[0], VERSION_MARKER2);
36024
36178
  if (!existsSync11(marker)) {
36025
36179
  return null;
36026
36180
  }
36027
- const said = readFileSync12(marker, "utf8").trim();
36181
+ const said = readFileSync13(marker, "utf8").trim();
36028
36182
  return said === "" ? null : said;
36029
36183
  }
36030
36184
  var ADDONS2, RUNTIME_AUTOLOAD2, VERSION_MARKER2 = ".gdharness-version";
@@ -36042,24 +36196,45 @@ var init_setup = __esm(() => {
36042
36196
 
36043
36197
  // src/server.ts
36044
36198
  import { execFile as execFile5, spawn } from "node:child_process";
36045
- import { existsSync as existsSync12, mkdirSync as mkdirSync7, mkdtempSync as mkdtempSync3, readdirSync as readdirSync6, readFileSync as readFileSync13, realpathSync as realpathSync2, rmSync as rmSync8 } from "node:fs";
36199
+ import {
36200
+ closeSync,
36201
+ existsSync as existsSync12,
36202
+ fstatSync,
36203
+ mkdirSync as mkdirSync8,
36204
+ mkdtempSync as mkdtempSync3,
36205
+ openSync as openSync2,
36206
+ readdirSync as readdirSync7,
36207
+ readFileSync as readFileSync14,
36208
+ readSync,
36209
+ realpathSync as realpathSync2,
36210
+ rmSync as rmSync9
36211
+ } from "node:fs";
36046
36212
  import { tmpdir as tmpdir5 } from "node:os";
36047
- import { basename as basename2, dirname as dirname9, join as join15, normalize as normalize3 } from "node:path";
36213
+ import { basename as basename2, dirname as dirname9, join as join16, normalize as normalize3 } from "node:path";
36048
36214
  import { setTimeout as delay3 } from "node:timers/promises";
36049
36215
  import { fileURLToPath as fileURLToPath4 } from "node:url";
36050
36216
  import { promisify as promisify5 } from "node:util";
36051
36217
  function patienceForFrames(frames, atLeast) {
36052
36218
  return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
36053
36219
  }
36220
+ function stillRunning(run) {
36221
+ if (run?.exitCode !== null || run.endedUnwatched === true) {
36222
+ return false;
36223
+ }
36224
+ if (!run.throughEditor && run.process === null && run.pid !== null) {
36225
+ return alive(run.pid);
36226
+ }
36227
+ return true;
36228
+ }
36054
36229
  function alive(pid) {
36055
- if (pid === undefined) {
36230
+ if (pid === undefined || pid === null) {
36056
36231
  return false;
36057
36232
  }
36058
36233
  try {
36059
36234
  process.kill(pid, 0);
36060
36235
  return true;
36061
- } catch {
36062
- return false;
36236
+ } catch (error) {
36237
+ return error.code === "EPERM";
36063
36238
  }
36064
36239
  }
36065
36240
  function isOfType(value, kind) {
@@ -36139,13 +36314,13 @@ function aboveTheRunner(entry) {
36139
36314
  }
36140
36315
  function elsewhereIn(projectPath, asked) {
36141
36316
  const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
36142
- if (wanted === "" || existsSync12(join15(projectPath, wanted))) {
36317
+ if (wanted === "" || existsSync12(join16(projectPath, wanted))) {
36143
36318
  return "";
36144
36319
  }
36145
36320
  const root = (wanted.split("/")[0] ?? "").toLowerCase();
36146
36321
  let near = [];
36147
36322
  try {
36148
- near = readdirSync6(projectPath, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.toLowerCase() !== root).map((entry) => entry.name).filter((name) => name.toLowerCase().startsWith("test") || root.startsWith(name.toLowerCase()));
36323
+ near = readdirSync7(projectPath, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.toLowerCase() !== root).map((entry) => entry.name).filter((name) => name.toLowerCase().startsWith("test") || root.startsWith(name.toLowerCase()));
36149
36324
  } catch {
36150
36325
  return "";
36151
36326
  }
@@ -36153,11 +36328,11 @@ function elsewhereIn(projectPath, asked) {
36153
36328
  return near.length === 0 ? "" : ` The project does have ${there}.`;
36154
36329
  }
36155
36330
  function hasMainScene(projectFile) {
36156
- const scene = parseProjectGodot(readFileSync13(projectFile, "utf8"))["application"]?.["run/main_scene"];
36331
+ const scene = parseProjectGodot(readFileSync14(projectFile, "utf8"))["application"]?.["run/main_scene"];
36157
36332
  return typeof scene === "string" && scene !== "";
36158
36333
  }
36159
36334
  function editorPlaysHeadless(projectFile) {
36160
- const runArgs = parseProjectGodot(readFileSync13(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
36335
+ const runArgs = parseProjectGodot(readFileSync14(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
36161
36336
  return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
36162
36337
  }
36163
36338
  function camelCased(params) {
@@ -36173,7 +36348,7 @@ function camelCased(params) {
36173
36348
  class GodotServer {
36174
36349
  mcp;
36175
36350
  locator = new GodotLocator2;
36176
- operationsScript = join15(__dirname2, "godot", "operations", "godot_operations.gd");
36351
+ operationsScript = join16(__dirname2, "godot", "operations", "godot_operations.gd");
36177
36352
  godotBridge;
36178
36353
  tools = buildToolDefinitions();
36179
36354
  activeProcess = null;
@@ -36330,10 +36505,7 @@ class GodotServer {
36330
36505
  this.stopWatchingForASuccessor();
36331
36506
  withdrawBridge(this.announcedAt);
36332
36507
  this.announcedAt = null;
36333
- if (this.activeProcess) {
36334
- this.activeProcess.process?.kill();
36335
- this.activeProcess = null;
36336
- }
36508
+ this.activeProcess = null;
36337
36509
  if (this.lspClient) {
36338
36510
  try {
36339
36511
  await this.lspClient.disconnect();
@@ -36398,14 +36570,7 @@ class GodotServer {
36398
36570
  return;
36399
36571
  }
36400
36572
  this.shutdownInitiated = true;
36401
- if (this.activeProcess) {
36402
- try {
36403
- this.activeProcess.process?.kill();
36404
- } catch (error) {
36405
- console.error("[SERVER] Failed to kill the Godot process on exit:", errorMessage(error));
36406
- }
36407
- this.activeProcess = null;
36408
- }
36573
+ this.activeProcess = null;
36409
36574
  }
36410
36575
  setupToolHandlers() {
36411
36576
  this.mcp.server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: this.tools }));
@@ -36765,7 +36930,7 @@ class GodotServer {
36765
36930
  if (path === undefined) {
36766
36931
  return { ok: false, response: this.createErrorResponse("projectPath is required.") };
36767
36932
  }
36768
- const file = join15(path, "project.godot");
36933
+ const file = join16(path, "project.godot");
36769
36934
  if (!existsSync12(file)) {
36770
36935
  return {
36771
36936
  ok: false,
@@ -36776,6 +36941,21 @@ class GodotServer {
36776
36941
  }
36777
36942
  return { ok: true, value: { path, file } };
36778
36943
  }
36944
+ gameArguments(args) {
36945
+ if (args["args"] === undefined) {
36946
+ return { ok: true, value: [] };
36947
+ }
36948
+ const given = readStringArray(args, "args");
36949
+ if (given === undefined) {
36950
+ return {
36951
+ ok: false,
36952
+ response: this.createErrorResponse("args must be a list of strings.", [
36953
+ `Pass the game's own flags, such as ["--level=2"]`
36954
+ ])
36955
+ };
36956
+ }
36957
+ return { ok: true, value: given };
36958
+ }
36779
36959
  async debuggedGame() {
36780
36960
  const game = this.activeProcess;
36781
36961
  if (game !== null && game.exitCode !== null) {
@@ -36920,7 +37100,7 @@ class GodotServer {
36920
37100
  if (!engine.ok) {
36921
37101
  return engine.response;
36922
37102
  }
36923
- const application = parseProjectGodot(readFileSync13(project.value.file, "utf8"))["application"] ?? {};
37103
+ const application = parseProjectGodot(readFileSync14(project.value.file, "utf8"))["application"] ?? {};
36924
37104
  const name = application["config/name"];
36925
37105
  const mainScene = application["run/main_scene"];
36926
37106
  const info = {
@@ -36983,7 +37163,7 @@ class GodotServer {
36983
37163
  }
36984
37164
  const folder = dirname9(output.absolutePath);
36985
37165
  try {
36986
- mkdirSync7(folder, { recursive: true });
37166
+ mkdirSync8(folder, { recursive: true });
36987
37167
  } catch (error) {
36988
37168
  return this.createErrorResponse(`Cannot create ${dirname9(output.relativePath)} to export into: ${errorMessage(error)}`);
36989
37169
  }
@@ -37045,7 +37225,7 @@ class GodotServer {
37045
37225
  return contained.response;
37046
37226
  }
37047
37227
  const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
37048
- if (!existsSync12(join15(project.value.path, runner))) {
37228
+ if (!existsSync12(join16(project.value.path, runner))) {
37049
37229
  return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
37050
37230
  "Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
37051
37231
  ]);
@@ -37079,7 +37259,7 @@ class GodotServer {
37079
37259
  ];
37080
37260
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
37081
37261
  this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
37082
- const userData = mkdtempSync3(join15(tmpdir5(), "gdharness-tests-"));
37262
+ const userData = mkdtempSync3(join16(tmpdir5(), "gdharness-tests-"));
37083
37263
  const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
37084
37264
  const hung = await new Promise((resolve) => {
37085
37265
  const timer = setTimeout(() => {
@@ -37095,26 +37275,26 @@ class GodotServer {
37095
37275
  resolve(false);
37096
37276
  });
37097
37277
  });
37098
- const reportsDir = join15(project.value.path, ".godot", "gdharness-reports");
37278
+ const reportsDir = join16(project.value.path, ".godot", "gdharness-reports");
37099
37279
  let report = null;
37100
37280
  let reportProblem = null;
37101
37281
  try {
37102
- const written = existsSync12(reportsDir) ? readdirSync6(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
37282
+ const written = existsSync12(reportsDir) ? readdirSync7(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
37103
37283
  const newest = written.at(-1);
37104
37284
  if (newest !== undefined) {
37105
- report = parseJUnit(readFileSync13(join15(reportsDir, newest, "results.xml"), "utf8"));
37285
+ report = parseJUnit(readFileSync14(join16(reportsDir, newest, "results.xml"), "utf8"));
37106
37286
  }
37107
37287
  } catch (error) {
37108
37288
  reportProblem = errorMessage(error);
37109
37289
  } finally {
37110
- rmSync8(reportsDir, { recursive: true, force: true });
37111
- rmSync8(userData, { recursive: true, force: true });
37290
+ rmSync9(reportsDir, { recursive: true, force: true });
37291
+ rmSync9(userData, { recursive: true, force: true });
37112
37292
  }
37113
37293
  const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries.map(aboveTheRunner);
37114
37294
  const verdicts = {
37115
37295
  0: "passed",
37116
37296
  100: "failures",
37117
- 101: "warnings",
37297
+ 101: "warnings: orphan nodes",
37118
37298
  103: "headless not supported by this gdUnit4",
37119
37299
  104: "Godot version not supported by this gdUnit4",
37120
37300
  105: "script errors"
@@ -37146,7 +37326,17 @@ class GodotServer {
37146
37326
  };
37147
37327
  }
37148
37328
  const failed = report.suites.flatMap((suite) => suite.cases.filter((entry) => entry.status === "failed" || entry.status === "error").map((entry) => ({ ...entry, path: suite.path })));
37149
- const unclean = report.suites.filter((suite) => suite.failures > 0 || suite.errors > 0 || suite.skipped > 0);
37329
+ const orphans = orphansPrinted(said);
37330
+ const leftBehind = new Map(orphans.suites.map((suite) => [suite.path, suite.orphans]));
37331
+ const orphansIn = (path) => path === null ? 0 : leftBehind.get(path) ?? 0;
37332
+ const warnings = orphans.suites.map((suite) => ({
37333
+ kind: "orphans",
37334
+ path: suite.path,
37335
+ name: report.suites.find((entry) => entry.path === suite.path)?.name ?? suite.path,
37336
+ orphans: suite.orphans,
37337
+ message: `${suite.orphans} node${suite.orphans === 1 ? "" : "s"} were still in the tree when this suite finished. A node built in a test and neither freed nor added to the tree is the usual cause; free it, or add it with auto_free().`
37338
+ }));
37339
+ const unclean = report.suites.filter((suite) => suite.failures > 0 || suite.errors > 0 || suite.skipped > 0 || orphansIn(suite.path) > 0);
37150
37340
  return this.jsonTextResponse({
37151
37341
  passed: !hung && exitCode === 0 && report.failures === 0 && report.errors === 0,
37152
37342
  verdict,
@@ -37157,6 +37347,9 @@ class GodotServer {
37157
37347
  skipped: report.skipped,
37158
37348
  time: report.time,
37159
37349
  failed,
37350
+ warnings: warnings.length > 0 ? warnings : undefined,
37351
+ orphans: orphans.total > 0 ? orphans.total : undefined,
37352
+ note: verdict.startsWith("warnings") && warnings.length === 0 ? "gdUnit4 exits 101 for orphan nodes when nothing failed, and this run printed no count of them: orphan reporting may be off in the project settings." : undefined,
37160
37353
  suites: unclean.map((suite) => ({
37161
37354
  name: suite.name,
37162
37355
  path: suite.path,
@@ -37164,6 +37357,7 @@ class GodotServer {
37164
37357
  failures: suite.failures,
37165
37358
  errors: suite.errors,
37166
37359
  skipped: suite.skipped,
37360
+ orphans: orphansIn(suite.path) > 0 ? orphansIn(suite.path) : undefined,
37167
37361
  time: suite.time
37168
37362
  })),
37169
37363
  suitesPassed: report.suites.length - unclean.length,
@@ -37290,7 +37484,7 @@ class GodotServer {
37290
37484
  version: godotPath === null ? null : await this.godotVersion(godotPath)
37291
37485
  },
37292
37486
  game: {
37293
- processActive: this.activeProcess?.exitCode === null,
37487
+ processActive: stillRunning(this.currentRun()),
37294
37488
  playingInEditor: playing,
37295
37489
  runtimeConnected: games.some((game) => game.reachable),
37296
37490
  runtimes: games
@@ -37436,6 +37630,10 @@ class GodotServer {
37436
37630
  if (sceneToRun && !sceneToRun.ok) {
37437
37631
  return this.createErrorResponse(sceneToRun.reason, PATH_SOLUTIONS);
37438
37632
  }
37633
+ const given = this.gameArguments(args);
37634
+ if (!given.ok) {
37635
+ return given.response;
37636
+ }
37439
37637
  if (!sceneToRun && !hasMainScene(project.value.file)) {
37440
37638
  return this.createErrorResponse("The project sets no main scene, so there is nothing to run.", [
37441
37639
  "Pass scene to run one scene",
@@ -37452,9 +37650,9 @@ class GodotServer {
37452
37650
  }
37453
37651
  const sceneArgument = sceneToRun?.ok ? sceneToRun.relativePath : null;
37454
37652
  if (op === "check") {
37455
- return await this.checkBoot(engine.value, project.value.path, sceneArgument, args);
37653
+ return await this.checkBoot(engine.value, project.value.path, sceneArgument, args, given.value);
37456
37654
  }
37457
- if (this.activeProcess?.exitCode === null) {
37655
+ if (stillRunning(this.currentRun())) {
37458
37656
  this.logDebug("Ending the running game before starting another");
37459
37657
  await this.endActiveGame();
37460
37658
  }
@@ -37463,17 +37661,20 @@ class GodotServer {
37463
37661
  platform: process.platform,
37464
37662
  variables: process.env
37465
37663
  });
37466
- if (this.godotBridge.isConnected() && (!headless || editorPlaysHeadless(project.value.file))) {
37664
+ const editorWouldPlay = !headless || editorPlaysHeadless(project.value.file);
37665
+ if (this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0) {
37467
37666
  return await this.playThroughEditor(sceneArgument, refreshed.value, project.value.path, alreadyPlaying);
37468
37667
  }
37469
37668
  const cmdArgs = runArguments({
37470
37669
  projectPath: project.value.path,
37471
37670
  headless,
37472
- scene: sceneArgument
37671
+ scene: sceneArgument,
37672
+ userArgs: given.value
37473
37673
  });
37474
37674
  this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
37475
- const started = this.spawnGame(engine.value, cmdArgs);
37675
+ const started = this.spawnKeptGame(engine.value, cmdArgs, project.value.path);
37476
37676
  this.activeProcess = started;
37677
+ const spawnedInstead = this.godotBridge.isConnected() && editorWouldPlay && given.value.length > 0 ? " The editor cannot be handed arguments for a game it plays, so this one was started " + "here: the debug_* tools answer only for a game the editor is playing." : "";
37477
37678
  return this.jsonTextResponse({
37478
37679
  started: true,
37479
37680
  through: "gdharness",
@@ -37481,11 +37682,11 @@ class GodotServer {
37481
37682
  arguments: cmdArgs,
37482
37683
  refreshedClasses: refreshed.value,
37483
37684
  runtime: await this.runtimeUp(project.value.path, alreadyPlaying),
37484
- message: "Use editor_output for what it prints and editor_run stop to end it."
37685
+ message: `Use editor_output for what it prints and editor_run stop to end it.${spawnedInstead}`
37485
37686
  });
37486
37687
  }
37487
37688
  async runtimeUp(projectPath, before) {
37488
- if (!existsSync12(join15(projectPath, RUNTIME_AUTOLOAD2.path))) {
37689
+ if (!existsSync12(join16(projectPath, RUNTIME_AUTOLOAD2.path))) {
37489
37690
  return {
37490
37691
  listening: false,
37491
37692
  note: "this project has no gdharness_runtime addon, so the runtime_* tools have nothing to talk to"
@@ -37520,7 +37721,11 @@ class GodotServer {
37520
37721
  }
37521
37722
  const played = {
37522
37723
  process: null,
37724
+ pid: null,
37523
37725
  log,
37726
+ transcript: null,
37727
+ readOffset: 0,
37728
+ projectPath,
37524
37729
  startedAt: Date.now(),
37525
37730
  exitCode: null,
37526
37731
  throughEditor: true,
@@ -37559,6 +37764,7 @@ class GodotServer {
37559
37764
  async endActiveGame() {
37560
37765
  const running = this.activeProcess;
37561
37766
  this.activeProcess = null;
37767
+ clearRunRecord();
37562
37768
  if (!running) {
37563
37769
  return;
37564
37770
  }
@@ -37566,14 +37772,26 @@ class GodotServer {
37566
37772
  await this.handleViaBridge("stop_playing", {});
37567
37773
  return;
37568
37774
  }
37569
- running.process?.kill();
37775
+ if (running.process !== null) {
37776
+ running.process.kill();
37777
+ return;
37778
+ }
37779
+ if (running.pid !== null) {
37780
+ try {
37781
+ process.kill(running.pid);
37782
+ } catch {}
37783
+ }
37570
37784
  }
37571
37785
  spawnGame(godotPath, cmdArgs, env) {
37572
37786
  const child = spawn(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
37573
37787
  const log = new GameLog;
37574
37788
  const started = {
37575
37789
  process: child,
37790
+ pid: child.pid ?? null,
37576
37791
  log,
37792
+ transcript: null,
37793
+ readOffset: 0,
37794
+ projectPath: null,
37577
37795
  startedAt: Date.now(),
37578
37796
  exitCode: null,
37579
37797
  throughEditor: false,
@@ -37598,10 +37816,121 @@ class GodotServer {
37598
37816
  });
37599
37817
  return started;
37600
37818
  }
37601
- async checkBoot(godotPath, projectPath, scene, args) {
37819
+ spawnKeptGame(godotPath, cmdArgs, projectPath, env) {
37820
+ const startedAt = Date.now();
37821
+ const transcript = openTranscript(startedAt);
37822
+ let child;
37823
+ try {
37824
+ child = spawn(godotPath, cmdArgs, {
37825
+ stdio: ["ignore", transcript.fd, transcript.fd],
37826
+ detached: true,
37827
+ ...env ? { env } : {}
37828
+ });
37829
+ } finally {
37830
+ closeSync(transcript.fd);
37831
+ }
37832
+ child.unref();
37833
+ const log = new GameLog;
37834
+ const started = {
37835
+ process: child,
37836
+ pid: child.pid ?? null,
37837
+ log,
37838
+ transcript: transcript.path,
37839
+ readOffset: 0,
37840
+ projectPath,
37841
+ startedAt,
37842
+ exitCode: null,
37843
+ throughEditor: false,
37844
+ brokeOn: null
37845
+ };
37846
+ child.on("exit", (code) => {
37847
+ this.logDebug(`Godot process exited with code ${code ?? "none"}`);
37848
+ this.drainTranscript(started);
37849
+ log.finish();
37850
+ started.exitCode = code ?? -1;
37851
+ });
37852
+ child.on("error", (err) => {
37853
+ console.error("Failed to start Godot process:", err);
37854
+ log.append("stderr", `${err.message}
37855
+ `);
37856
+ started.exitCode = -1;
37857
+ });
37858
+ if (started.pid !== null) {
37859
+ writeRunRecord({
37860
+ pid: started.pid,
37861
+ transcript: transcript.path,
37862
+ startedAt,
37863
+ projectPath,
37864
+ arguments: cmdArgs
37865
+ });
37866
+ }
37867
+ sweepTranscripts();
37868
+ return started;
37869
+ }
37870
+ drainTranscript(game) {
37871
+ if (game.transcript === null) {
37872
+ return;
37873
+ }
37874
+ let handle;
37875
+ try {
37876
+ handle = openSync2(game.transcript, "r");
37877
+ } catch {
37878
+ return;
37879
+ }
37880
+ try {
37881
+ const size = fstatSync(handle).size;
37882
+ if (size <= game.readOffset) {
37883
+ return;
37884
+ }
37885
+ const buffer = Buffer.alloc(size - game.readOffset);
37886
+ const read = readSync(handle, buffer, 0, buffer.length, game.readOffset);
37887
+ game.readOffset += read;
37888
+ game.log.append("transcript", buffer.subarray(0, read));
37889
+ } finally {
37890
+ closeSync(handle);
37891
+ }
37892
+ }
37893
+ adoptRecordedRun() {
37894
+ const record = readRunRecord();
37895
+ if (record === null || !this.couldBeOurs(record.projectPath)) {
37896
+ return null;
37897
+ }
37898
+ const adopted = {
37899
+ process: null,
37900
+ pid: record.pid,
37901
+ log: new GameLog,
37902
+ transcript: record.transcript,
37903
+ readOffset: 0,
37904
+ projectPath: record.projectPath === "" ? null : record.projectPath,
37905
+ startedAt: record.startedAt,
37906
+ exitCode: null,
37907
+ throughEditor: false,
37908
+ brokeOn: null
37909
+ };
37910
+ this.drainTranscript(adopted);
37911
+ if (!alive(record.pid)) {
37912
+ adopted.log.finish();
37913
+ adopted.exitCode = null;
37914
+ adopted.endedUnwatched = true;
37915
+ }
37916
+ this.activeProcess = adopted;
37917
+ return adopted;
37918
+ }
37919
+ couldBeOurs(project) {
37920
+ if (project === "") {
37921
+ return true;
37922
+ }
37923
+ const status = this.godotBridge.getStatus();
37924
+ const mine = this.ownProject ?? (status.connected ? status.projectPath ?? null : null);
37925
+ return mine === null || samePath(mine, project);
37926
+ }
37927
+ currentRun() {
37928
+ return this.activeProcess ?? this.adoptRecordedRun();
37929
+ }
37930
+ async checkBoot(godotPath, projectPath, scene, args, userArgs) {
37602
37931
  const frames = readPositiveNumber(args, "frames") ?? 3;
37603
37932
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 60000;
37604
- const cmdArgs = runArguments({ projectPath, headless: true, scene, quitAfter: frames });
37933
+ const cmdArgs = runArguments({ projectPath, headless: true, scene, quitAfter: frames, userArgs });
37605
37934
  const boot = this.spawnGame(godotPath, cmdArgs);
37606
37935
  const hung = await new Promise((resolve) => {
37607
37936
  const timer = setTimeout(() => {
@@ -37631,37 +37960,47 @@ class GodotServer {
37631
37960
  });
37632
37961
  }
37633
37962
  handleGetDebugOutput(args) {
37634
- if (!this.activeProcess) {
37963
+ const run = this.currentRun();
37964
+ if (!run) {
37635
37965
  return this.createErrorResponse("No game is running. Start one with editor_run.");
37636
37966
  }
37637
- this.drainEditorOutput(this.activeProcess);
37967
+ this.drainEditorOutput(run);
37968
+ this.drainTranscript(run);
37969
+ if (run.endedUnwatched !== true && !stillRunning(run)) {
37970
+ run.log.finish();
37971
+ run.endedUnwatched = run.exitCode === null;
37972
+ }
37638
37973
  const severity = readString(args, "severity");
37639
- const selected = this.activeProcess.log.select({
37974
+ const selected = run.log.select({
37640
37975
  severity: severity === "error" || severity === "warning" ? severity : "info",
37641
37976
  sinceLastCall: readBoolean(args, "sinceLastCall") ?? false,
37642
37977
  contains: readNonEmptyString(args, "contains"),
37643
37978
  limit: readPositiveNumber(args, "limit") ?? 200
37644
37979
  });
37645
- const halt = this.activeProcess.throughEditor ? this.dapClient?.whereItStopped() ?? null : null;
37980
+ const halt = run.throughEditor ? this.dapClient?.whereItStopped() ?? null : null;
37646
37981
  return this.jsonTextResponse({
37647
- running: this.activeProcess.exitCode === null,
37648
- exitCode: this.activeProcess.exitCode,
37649
- through: this.activeProcess.throughEditor ? "editor" : "gdharness",
37650
- pid: this.activeProcess.process?.pid ?? null,
37651
- errors: this.activeProcess.log.count("error"),
37652
- warnings: this.activeProcess.log.count("warning"),
37653
- clean: this.activeProcess.log.count("error") === 0,
37982
+ running: stillRunning(run),
37983
+ exitCode: run.exitCode,
37984
+ through: run.throughEditor ? "editor" : "gdharness",
37985
+ pid: run.pid,
37986
+ errors: run.log.count("error"),
37987
+ warnings: run.log.count("warning"),
37988
+ clean: run.log.count("error") === 0,
37654
37989
  heldAt: halt,
37990
+ endedUnwatched: run.endedUnwatched === true ? true : undefined,
37991
+ project: !run.throughEditor && run.process === null ? run.projectPath ?? undefined : undefined,
37992
+ note: run.endedUnwatched === true ? "This run outlived the server that started it and is over now, so its exit code was never collected. Everything it printed is below, read back from its transcript." : undefined,
37655
37993
  omitted: selected.omitted,
37656
37994
  entries: selected.entries
37657
37995
  });
37658
37996
  }
37659
37997
  async handleStopProject() {
37660
- if (!this.activeProcess) {
37998
+ const stopped = this.currentRun();
37999
+ if (!stopped) {
37661
38000
  return this.createErrorResponse("No game is running. Start one with editor_run.");
37662
38001
  }
37663
- const stopped = this.activeProcess;
37664
38002
  this.drainEditorOutput(stopped);
38003
+ this.drainTranscript(stopped);
37665
38004
  this.logDebug("Stopping the running game");
37666
38005
  await this.endActiveGame();
37667
38006
  return this.jsonTextResponse({
@@ -37767,8 +38106,8 @@ class GodotServer {
37767
38106
  return this.createErrorResponse(choice.problem);
37768
38107
  }
37769
38108
  const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
37770
- const screenshotDir = expectsScreenshot ? mkdtempSync3(join15(tmpdir5(), "gdharness-runtime-screenshot-")) : null;
37771
- const screenshotPath = screenshotDir ? join15(screenshotDir, "capture.png") : null;
38109
+ const screenshotDir = expectsScreenshot ? mkdtempSync3(join16(tmpdir5(), "gdharness-runtime-screenshot-")) : null;
38110
+ const screenshotPath = screenshotDir ? join16(screenshotDir, "capture.png") : null;
37772
38111
  try {
37773
38112
  const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs);
37774
38113
  if (!reply.ok) {
@@ -37789,12 +38128,12 @@ class GodotServer {
37789
38128
  return {
37790
38129
  content: [
37791
38130
  { type: "text", text: `Screenshot captured: ${dimensions}` },
37792
- { type: "image", data: readFileSync13(screenshotPath).toString("base64"), mimeType: "image/png" }
38131
+ { type: "image", data: readFileSync14(screenshotPath).toString("base64"), mimeType: "image/png" }
37793
38132
  ]
37794
38133
  };
37795
38134
  } finally {
37796
38135
  if (screenshotDir) {
37797
- rmSync8(screenshotDir, { recursive: true, force: true });
38136
+ rmSync9(screenshotDir, { recursive: true, force: true });
37798
38137
  }
37799
38138
  }
37800
38139
  }
@@ -37819,12 +38158,14 @@ class GodotServer {
37819
38158
  return this.createErrorResponse("runtime_inspect find needs at least one of className, script, namePattern, group, says.");
37820
38159
  }
37821
38160
  const property = readNonEmptyString(args, "property");
38161
+ const includeHidden = readBoolean(args, "includeHidden");
37822
38162
  return await this.handleRuntimeCommand("find_nodes", {
37823
38163
  ...filters,
37824
38164
  projectPath: args["projectPath"],
37825
38165
  root: readNonEmptyString(args, "nodePath") ?? "/root",
37826
38166
  limit: readPositiveNumber(args, "limit") ?? 100,
37827
- ...property === undefined ? {} : { property }
38167
+ ...property === undefined ? {} : { property },
38168
+ ...includeHidden === undefined ? {} : { include_hidden: includeHidden }
37828
38169
  });
37829
38170
  }
37830
38171
  async handleRuntimeWait(op, args) {
@@ -37875,6 +38216,7 @@ var init_server3 = __esm(() => {
37875
38216
  init_ports();
37876
38217
  init_project_scan();
37877
38218
  init_resources();
38219
+ init_run_record();
37878
38220
  init_runtime_client();
37879
38221
  init_server_version();
37880
38222
  init_setup();
@@ -37945,7 +38287,7 @@ var init_server3 = __esm(() => {
37945
38287
 
37946
38288
  // src/cli.ts
37947
38289
  import { existsSync as existsSync13 } from "fs";
37948
- import { join as join16, resolve as resolve5 } from "path";
38290
+ import { join as join17, resolve as resolve5 } from "path";
37949
38291
 
37950
38292
  // src/errors.ts
37951
38293
  class Refusal extends Error {
@@ -39336,6 +39678,11 @@ them. \`editor_rescan\` asks it to scan and names any class it still cannot reso
39336
39678
  declaration and the cache can both be right while the editor stays blind to it. Change the
39337
39679
  declaring script, or restart the editor.
39338
39680
 
39681
+ A diagnostic about a member rather than a type, \`Static function "x()" not found in base "Y"\`
39682
+ about something the engine compiles, is a different thing and is no longer stale here: each ask
39683
+ gives the document back when its answer arrives, so the next one is read off disk. If one turns
39684
+ up anyway, believe the run over the diagnostic and say so.
39685
+
39339
39686
  ## Refusals are useful
39340
39687
 
39341
39688
  An unknown op, an argument the tool does not name, or a missing required one is refused with the
@@ -39448,7 +39795,7 @@ class UsageError extends Refusal {
39448
39795
  function projectArgument(at) {
39449
39796
  const named = args.slice(at).find((value) => !value.startsWith("--"));
39450
39797
  const projectPath = resolve5(named ?? ".");
39451
- if (!existsSync13(join16(projectPath, "project.godot"))) {
39798
+ if (!existsSync13(join17(projectPath, "project.godot"))) {
39452
39799
  throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
39453
39800
  }
39454
39801
  return projectPath;