gdharness 1.0.2 → 1.0.4

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
@@ -16015,7 +16015,7 @@ var init_tool_definitions = __esm(() => {
16015
16015
  runtimeWaitMs: {
16016
16016
  type: "number",
16017
16017
  ops: ["start"],
16018
- description: "start: how long to wait for the game to announce its runtime before answering. Default 5000. What it waits for is the first frame, so everything the game does before drawing one is inside it, including work the args just asked for: a flag that simulates six years of game time before anything is drawn makes the announcement that late, on a project that announces promptly without it. Raise it for such a run rather than reading runtime listening false as a fault. 0 does not wait at all, which is the one to pass for a scene that announces nothing by construction, such as a bench that prints and quits."
16018
+ description: "start: how long to wait for the game to announce its runtime before answering. Default 5000, or half as long again as the last boot of this project took when that is more, up to 60000: the server notes how long each game took to announce, so a project that boots slowly is waited for from its second start on without being asked to. What it waits for is the first frame, so everything the game does before drawing one is inside it, including work the args just asked for: a flag that simulates six years of game time before anything is drawn makes the announcement that late, on a project that announces promptly without it. Raise it for such a run rather than reading runtime listening false as a fault. 0 does not wait at all, which is the one to pass for a scene that announces nothing by construction, such as a bench that prints and quits."
16019
16019
  },
16020
16020
  frames: {
16021
16021
  type: "number",
@@ -16040,7 +16040,7 @@ var init_tool_definitions = __esm(() => {
16040
16040
  requires: ["projectPath"]
16041
16041
  },
16042
16042
  stop: {
16043
- summary: "end the run and answer with what it printed last, naming the process ended under endedPid: a game that started processes of its own keeps them unless andChildren says to end those too",
16043
+ summary: "end the run and answer with what it printed last, naming the process ended under endedPid: for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. A game that started processes of its own keeps them unless andChildren says to end those too",
16044
16044
  requires: []
16045
16045
  },
16046
16046
  check: {
@@ -16155,7 +16155,7 @@ var init_tool_definitions = __esm(() => {
16155
16155
  property: {
16156
16156
  type: "string",
16157
16157
  ops: ["property", "find"],
16158
- description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A negative number counts from the end. A step that is not there is named, and says what was there instead.'
16158
+ description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A negative number counts from the end. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that takes no arguments and walks into what it returned, which is how a question only a method answers is read in one call: which control holds the focus in the viewport this one is in. A step that is not there is named, and says what was there instead.'
16159
16159
  },
16160
16160
  depth: { type: "number", ops: ["tree"], description: "tree: levels to descend. Default 3." },
16161
16161
  includeProperties: {
@@ -16236,7 +16236,7 @@ var init_tool_definitions = __esm(() => {
16236
16236
  property: {
16237
16237
  type: "string",
16238
16238
  ops: ["set"],
16239
- description: 'set: which one to write. Colons write through what a node holds, "_game:run:day", and a number or a key steps into a list or a map on the way, "_game:run:roster:0:name", a negative number counting from the end. The answer reads back off the same holder, so a write a typed container refused shows as an unchanged value.'
16239
+ description: 'set: which one to write. Colons write through what a node holds, "_game:run:day", and a number or a key steps into a list or a map on the way, "_game:run:roster:0:name", a negative number counting from the end, and a step written as a call, "get_viewport():gui_embed_subwindows", walks through what a method taking no arguments returned. The answer reads back off the same holder, so a write a typed container refused shows as an unchanged value.'
16240
16240
  },
16241
16241
  value: {
16242
16242
  ops: ["set"],
@@ -16246,7 +16246,7 @@ var init_tool_definitions = __esm(() => {
16246
16246
  method: {
16247
16247
  type: "string",
16248
16248
  ops: ["call"],
16249
- description: 'call: which one to call. Colons call through what a node holds, "_game:run:advance", the same way a property is written through them, and a list or a map is stepped into by index or key, "_game:run:roster:0:retire".'
16249
+ description: 'call: which one to call. Colons call through what a node holds, "_game:run:advance", the same way a property is written through them, a list or a map is stepped into by index or key, "_game:run:roster:0:retire", and a step written as a call walks through what a method taking no arguments returned, "get_viewport():gui_get_focus_owner".'
16250
16250
  },
16251
16251
  args: {
16252
16252
  type: "array",
@@ -16306,7 +16306,7 @@ var init_tool_definitions = __esm(() => {
16306
16306
  pressed: {
16307
16307
  type: "boolean",
16308
16308
  ops: ["action", "key", "mouse_click"],
16309
- description: "action, key: leave it out and the press is a whole one, down and up a frame apart. true holds it down, false lets go of one being held. mouse_click is one raw event, so it is down unless you say false."
16309
+ description: "action, key: leave it out and the press is a whole one, down and up a frame apart. true holds it down, false lets go of one being held. mouse_click is one raw event, so it is down unless you say false; a wheel button down is a whole step, since a wheel is never held."
16310
16310
  },
16311
16311
  strength: { type: "number", ops: ["action"], description: "action: 0 to 1. Default 1." },
16312
16312
  keycode: {
@@ -16384,7 +16384,7 @@ var init_tool_definitions = __esm(() => {
16384
16384
  requires: ["text"]
16385
16385
  },
16386
16386
  mouse_click: {
16387
- summary: "one mouse button event at a position, carrying the buttons held once it has happened. A drag is a press held with pressed true, motions, and a release with pressed false",
16387
+ summary: "one mouse button event at a position, carrying the buttons held once it has happened. A drag is a press held with pressed true, motions, and a release with pressed false. A wheel button is a whole step, press and release together as a mouse sends one, and the answer says released: a lone wheel press holds the viewport on the control that took it, so every click after it lands there",
16388
16388
  requires: ["x", "y"]
16389
16389
  },
16390
16390
  mouse_motion: {
@@ -16406,7 +16406,11 @@ var init_tool_definitions = __esm(() => {
16406
16406
  },
16407
16407
  nodePath: { type: "string", ops: ["signal", "until"], description: "signal, until: the node." },
16408
16408
  signal: { type: "string", ops: ["signal"], description: "signal: the signal name." },
16409
- property: { type: "string", ops: ["until"], description: "until: the property name." },
16409
+ property: {
16410
+ type: "string",
16411
+ ops: ["until"],
16412
+ description: 'until: the property name, or a colon path through what the node holds, "_game:run:day", read the way runtime_inspect property reads one, a step written as a call included. Walked again on every frame, so a holder the game replaces while the wait is on is followed.'
16413
+ },
16410
16414
  value: { ops: ["until"], description: "until: the value to wait for, fitted to the property's type." },
16411
16415
  says: {
16412
16416
  type: "string",
@@ -31236,155 +31240,782 @@ var init_stdio2 = __esm(() => {
31236
31240
  init_stdio();
31237
31241
  });
31238
31242
 
31239
- // src/breakpoint-note.ts
31240
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync10, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "node:fs";
31241
- import { dirname as dirname7, isAbsolute as isAbsolute2, join as join12, relative as relative2 } from "node:path";
31242
- function breakpointNotePath(projectPath) {
31243
- return join12(projectPath, ".godot", "gdharness-breakpoints.json");
31243
+ // src/process-children.ts
31244
+ import { execFile as execFile4, execFileSync, spawnSync as spawnSync2 } from "node:child_process";
31245
+ import { basename as basename2 } from "node:path";
31246
+ import { promisify as promisify4 } from "node:util";
31247
+ function startTimesOf(pids) {
31248
+ const began = new Map;
31249
+ if (pids.length === 0) {
31250
+ return began;
31251
+ }
31252
+ try {
31253
+ if (process.platform === "win32") {
31254
+ const filter = pids.map((pid) => `ProcessId=${pid}`).join(" OR ");
31255
+ const said = execFileSync("powershell", [
31256
+ "-NoProfile",
31257
+ "-NonInteractive",
31258
+ "-Command",
31259
+ `Get-CimInstance Win32_Process -Filter "${filter}" | ForEach-Object { "$($_.ProcessId) $(([DateTimeOffset]$_.CreationDate).ToUnixTimeMilliseconds())" }`
31260
+ ], { encoding: "utf8", timeout: ASK_TIMEOUT_MS, windowsHide: true });
31261
+ for (const line of said.split(`
31262
+ `)) {
31263
+ const found = /^\s*(\d+)\s+(\d+)\s*$/.exec(line);
31264
+ if (found?.[1] !== undefined && found[2] !== undefined) {
31265
+ began.set(Number(found[1]), Number(found[2]));
31266
+ }
31267
+ }
31268
+ return began;
31269
+ }
31270
+ const said = spawnSync2("ps", ["-p", pids.join(","), "-o", "pid=,lstart="], {
31271
+ encoding: "utf8",
31272
+ timeout: ASK_TIMEOUT_MS,
31273
+ env: { ...process.env, LC_ALL: "C" }
31274
+ }).stdout;
31275
+ for (const line of said.split(`
31276
+ `)) {
31277
+ const found = /^\s*(\d+)\s+(.+)$/.exec(line);
31278
+ const at = Date.parse(found?.[2]?.trim() ?? "");
31279
+ if (found?.[1] !== undefined && !Number.isNaN(at)) {
31280
+ began.set(Number(found[1]), at);
31281
+ }
31282
+ }
31283
+ } catch {}
31284
+ return began;
31244
31285
  }
31245
- function writeBreakpointNote(projectPath, held) {
31246
- const path = breakpointNotePath(projectPath);
31247
- const root = realPathOr(projectPath);
31248
- const inside = held.map((one) => ({ scriptPath: realPathOr(one.scriptPath), lines: one.lines })).filter((one) => one.lines.length > 0 && withinProject(root, one.scriptPath));
31286
+ async function processTree() {
31249
31287
  try {
31250
- if (inside.length === 0) {
31251
- rmSync6(path, { force: true });
31252
- return;
31288
+ if (process.platform === "win32") {
31289
+ return parseProcessTable(await askWindows(), true);
31253
31290
  }
31254
- const note = {
31255
- breakpoints: inside.map((one) => ({
31256
- script: relative2(root, one.scriptPath).replaceAll("\\", "/"),
31257
- lines: [...one.lines]
31258
- }))
31259
- };
31260
- mkdirSync6(dirname7(path), { recursive: true });
31261
- writeFileSync8(path, `${JSON.stringify(note, null, 2)}
31262
- `, "utf8");
31263
- } catch (error) {
31264
- console.error(`[SERVER] Could not record the breakpoints at ${path}: ${errorMessage(error)}`);
31291
+ return parseProcessTable(await askPosix());
31292
+ } catch {
31293
+ return;
31265
31294
  }
31266
31295
  }
31267
- function readBreakpointNote(projectPath) {
31268
- try {
31269
- const parsed = JSON.parse(readFileSync10(breakpointNotePath(projectPath), "utf8"));
31270
- if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.breakpoints)) {
31271
- return [];
31296
+ function parseProcessTable(printed, started = false) {
31297
+ const tree = new Map;
31298
+ for (const line of printed.split(/\r?\n/)) {
31299
+ const fields = /^\s*(\d+)\s+(\d+)(?:\s+(.*))?$/.exec(line);
31300
+ if (fields === null) {
31301
+ continue;
31272
31302
  }
31273
- const held = [];
31274
- for (const entry of parsed.breakpoints) {
31275
- if (typeof entry !== "object" || entry === null) {
31276
- continue;
31277
- }
31278
- const { script, lines: listed } = entry;
31279
- if (typeof script !== "string" || isAbsolute2(script) || script.split("/").includes("..")) {
31280
- continue;
31281
- }
31282
- const lines = Array.isArray(listed) ? listed.filter((line) => Number.isInteger(line) && line > 0) : [];
31283
- if (lines.length > 0) {
31284
- held.push({ scriptPath: realPathOr(join12(projectPath, script)), lines });
31285
- }
31303
+ const pid = Number(fields[1]);
31304
+ const parent = Number(fields[2]);
31305
+ const rest = (fields[3] ?? "").trim();
31306
+ if (pid <= 0) {
31307
+ continue;
31286
31308
  }
31287
- return held;
31288
- } catch {
31289
- return [];
31309
+ if (!started) {
31310
+ tree.set(pid, { parent, command: rest });
31311
+ continue;
31312
+ }
31313
+ const timed = /^(\d+)(?:\s+(.*))?$/.exec(rest);
31314
+ if (timed === null) {
31315
+ continue;
31316
+ }
31317
+ const began = Number(timed[1]);
31318
+ tree.set(pid, { parent, command: (timed[2] ?? "").trim(), ...began > 0 ? { startedAt: began } : {} });
31290
31319
  }
31320
+ return tree;
31291
31321
  }
31292
- function withinProject(projectPath, scriptPath) {
31293
- const rel = relative2(projectPath, scriptPath);
31294
- return rel !== "" && !rel.startsWith("..") && !isAbsolute2(rel);
31322
+ function linked(tree, child, parent) {
31323
+ const from = tree.get(child);
31324
+ const to = tree.get(parent);
31325
+ if (from === undefined || from.parent !== parent) {
31326
+ return false;
31327
+ }
31328
+ if (from.startedAt === undefined || to?.startedAt === undefined) {
31329
+ return true;
31330
+ }
31331
+ return from.startedAt >= to.startedAt;
31295
31332
  }
31296
- var init_breakpoint_note = __esm(() => {
31297
- init_errors();
31298
- init_paths();
31299
- });
31300
-
31301
- // src/bridge-announce.ts
31302
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync11, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "node:fs";
31303
- import { dirname as dirname8, join as join13 } from "node:path";
31304
- function announcementPath(projectPath) {
31305
- return join13(projectPath, ".godot", "gdharness-bridge.json");
31333
+ function childrenIn(tree, pid) {
31334
+ const children = [];
31335
+ for (const child of tree.keys()) {
31336
+ if (linked(tree, child, pid)) {
31337
+ children.push(child);
31338
+ }
31339
+ }
31340
+ return children;
31306
31341
  }
31307
- function readAnnouncement(path) {
31308
- try {
31309
- const parsed = JSON.parse(readFileSync11(path, "utf8"));
31310
- if (typeof parsed !== "object" || parsed === null) {
31311
- return null;
31342
+ function descendantsIn(tree, pid) {
31343
+ const found = [];
31344
+ const queue = [pid];
31345
+ const seen = new Set([pid]);
31346
+ while (queue.length > 0) {
31347
+ const next = queue.shift();
31348
+ if (next === undefined) {
31349
+ break;
31312
31350
  }
31313
- const said = parsed;
31314
- if (typeof said.port !== "number" || typeof said.pid !== "number") {
31315
- return null;
31351
+ for (const child of childrenIn(tree, next)) {
31352
+ if (!seen.has(child)) {
31353
+ seen.add(child);
31354
+ found.push(child);
31355
+ queue.push(child);
31356
+ }
31316
31357
  }
31317
- return said;
31318
- } catch {
31319
- return null;
31320
31358
  }
31359
+ return found;
31321
31360
  }
31322
- function announcementIsOurs(path) {
31323
- return readAnnouncement(path)?.pid === process.pid;
31361
+ function ancestorsIn(tree, pid, root) {
31362
+ const chain = [];
31363
+ const seen = new Set([pid]);
31364
+ let child = pid;
31365
+ let parent = tree.get(pid)?.parent;
31366
+ while (parent !== undefined && !seen.has(parent) && linked(tree, child, parent)) {
31367
+ chain.push(parent);
31368
+ if (parent === root) {
31369
+ break;
31370
+ }
31371
+ seen.add(parent);
31372
+ child = parent;
31373
+ parent = tree.get(parent)?.parent;
31374
+ }
31375
+ return chain;
31324
31376
  }
31325
- function announceBridge(projectPath, bridge) {
31326
- const path = announcementPath(projectPath);
31327
- try {
31328
- mkdirSync7(dirname8(path), { recursive: true });
31329
- const announcement = {
31330
- protocol: BRIDGE_ANNOUNCE_PROTOCOL,
31331
- host: bridge.host,
31332
- port: bridge.port,
31333
- pid: process.pid,
31334
- version: bridge.version,
31335
- startedAt: new Date().toISOString()
31336
- };
31337
- writeFileSync9(path, `${JSON.stringify(announcement, null, 2)}
31338
- `, "utf8");
31339
- return path;
31340
- } catch (error) {
31341
- console.error(`[SERVER] Could not announce the editor bridge at ${path}: ${errorMessage(error)}`);
31342
- return null;
31377
+ function readCommandLine(command) {
31378
+ const words = commandWords(command);
31379
+ const executable = words[0] === undefined ? "" : basename2(words[0].replaceAll("\\", "/"));
31380
+ const at = words.indexOf("--path");
31381
+ let projectPath = null;
31382
+ if (at !== -1 && at + 1 < words.length) {
31383
+ const taken = [];
31384
+ for (const word of words.slice(at + 1)) {
31385
+ if (taken.length > 0 && (word.startsWith("-") || /^[a-z]+:\/\//.test(word))) {
31386
+ break;
31387
+ }
31388
+ taken.push(word);
31389
+ }
31390
+ projectPath = taken.join(" ");
31343
31391
  }
31392
+ return {
31393
+ executable,
31394
+ projectPath,
31395
+ editor: words.some((word) => word === "-e" || word === "--editor")
31396
+ };
31344
31397
  }
31345
- function withdrawBridge(path) {
31346
- if (path === null || !announcementIsOurs(path)) {
31347
- return;
31398
+ function commandWords(command) {
31399
+ const words = [];
31400
+ let word = "";
31401
+ let quoted = false;
31402
+ let started = false;
31403
+ for (const char of command) {
31404
+ if (char === '"') {
31405
+ quoted = !quoted;
31406
+ started = true;
31407
+ } else if (!quoted && /\s/.test(char)) {
31408
+ if (started) {
31409
+ words.push(word);
31410
+ word = "";
31411
+ started = false;
31412
+ }
31413
+ } else {
31414
+ word += char;
31415
+ started = true;
31416
+ }
31348
31417
  }
31349
- try {
31350
- rmSync7(path, { force: true });
31351
- } catch (error) {
31352
- console.error(`[SERVER] Could not withdraw the editor bridge announcement: ${errorMessage(error)}`);
31418
+ if (started) {
31419
+ words.push(word);
31353
31420
  }
31421
+ return words;
31354
31422
  }
31355
- var BRIDGE_ANNOUNCE_PROTOCOL = 1;
31356
- var init_bridge_announce = __esm(() => {
31357
- init_errors();
31423
+ async function askWindows() {
31424
+ const { stdout } = await run4("powershell.exe", [
31425
+ "-NoProfile",
31426
+ "-NonInteractive",
31427
+ "-Command",
31428
+ 'Get-CimInstance Win32_Process | ForEach-Object { $began = 0; if ($_.CreationDate) { $began = ([DateTimeOffset]$_.CreationDate).ToUnixTimeMilliseconds() }; "$($_.ProcessId) $($_.ParentProcessId) $began $($_.CommandLine)" }'
31429
+ ], { timeout: ASK_TIMEOUT_MS, windowsHide: true, maxBuffer: 16 * 1024 * 1024 });
31430
+ return stdout;
31431
+ }
31432
+ async function askPosix() {
31433
+ const { stdout } = await run4("ps", ["-e", "-o", "pid=,ppid=,args="], {
31434
+ timeout: ASK_TIMEOUT_MS,
31435
+ maxBuffer: 16 * 1024 * 1024
31436
+ });
31437
+ return stdout;
31438
+ }
31439
+ var run4, ASK_TIMEOUT_MS = 15000;
31440
+ var init_process_children = __esm(() => {
31441
+ run4 = promisify4(execFile4);
31358
31442
  });
31359
31443
 
31360
- // src/config-pin.ts
31361
- import { existsSync as existsSync10, readFileSync as readFileSync12 } from "node:fs";
31362
- import { join as join14 } from "node:path";
31363
- function versionsNamedInConfig(projectPath) {
31364
- const config = join14(projectPath, ".mcp.json");
31365
- if (!existsSync10(config)) {
31366
- return [];
31444
+ // src/runtime-client.ts
31445
+ import { existsSync as existsSync10, readdirSync as readdirSync4, readFileSync as readFileSync10, statSync as statSync3, unlinkSync } from "node:fs";
31446
+ import { createConnection } from "node:net";
31447
+ import { tmpdir as tmpdir4 } from "node:os";
31448
+ import { dirname as dirname7, join as join12, resolve as resolve3 } from "node:path";
31449
+ import { setTimeout as delay } from "node:timers/promises";
31450
+ function runtimeDirectory(variables = process.env) {
31451
+ const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
31452
+ if (explicit) {
31453
+ return explicit;
31367
31454
  }
31368
- let text;
31369
- try {
31370
- text = readFileSync12(config, "utf8");
31371
- JSON.parse(text);
31372
- } catch {
31373
- return [];
31455
+ const perUser = envValue("XDG_RUNTIME_DIR", variables);
31456
+ return join12(perUser ?? tmpdir4(), "gdharness");
31457
+ }
31458
+ function runtimeDirectories(variables = process.env) {
31459
+ const candidates = [runtimeDirectory(variables)];
31460
+ const fallbacks = process.platform === "win32" ? [envValue("SystemRoot", variables) ?? envValue("windir", variables) ?? "C:\\Windows"] : ["/tmp", "/var/tmp"];
31461
+ for (const base of fallbacks) {
31462
+ candidates.push(join12(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
31374
31463
  }
31375
- return [...new Set([...text.matchAll(PINNED)].map((found) => found[1] ?? ""))].filter((version) => version !== "");
31464
+ return [...new Set(candidates.map((path) => resolve3(path)))];
31376
31465
  }
31377
- function configDisagrees(projectPath, serverVersion) {
31378
- const named = versionsNamedInConfig(projectPath);
31379
- const others = named.filter((version) => version !== serverVersion);
31380
- if (others.length === 0) {
31381
- return null;
31466
+ function errorReportOf(pid, directories = runtimeDirectories()) {
31467
+ for (const directory of directories) {
31468
+ const path = join12(directory, `runtime-${pid}.log`);
31469
+ if (existsSync10(path)) {
31470
+ return path;
31471
+ }
31382
31472
  }
31383
- return `.mcp.json in this project names gdharness@${others.join(" and gdharness@")}, and this server ` + `is ${serverVersion}. Whichever is meant to be right, nothing will change version until the ` + "MCP server is reconnected, and a pin kept anywhere else does not reach this file.";
31473
+ return null;
31384
31474
  }
31385
- var PINNED;
31386
- var init_config_pin = __esm(() => {
31387
- PINNED = /\bgdharness@(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)\b/g;
31475
+ function processAlive(pid) {
31476
+ try {
31477
+ process.kill(pid, 0);
31478
+ return true;
31479
+ } catch (error) {
31480
+ return error.code === "EPERM";
31481
+ }
31482
+ }
31483
+ function parseAnnouncement(file, pid) {
31484
+ let fields;
31485
+ try {
31486
+ fields = asParams(JSON.parse(readFileSync10(file, "utf8")));
31487
+ } catch {
31488
+ return { kind: "rubbish" };
31489
+ }
31490
+ const project = readParams(fields, "project");
31491
+ const port = readNumber(fields, "port");
31492
+ const address = readString(fields, "address");
31493
+ const protocol = readNumber(fields, "protocol");
31494
+ if (readNumber(fields, "pid") !== pid || port === undefined || !Number.isInteger(port) || port < 1 || port > 65535 || address === undefined || project === undefined) {
31495
+ return { kind: "rubbish" };
31496
+ }
31497
+ const named = {
31498
+ name: readString(project, "name") ?? "",
31499
+ path: readString(project, "path") ?? ""
31500
+ };
31501
+ if (protocol !== RUNTIME_PROTOCOL) {
31502
+ return { kind: "unspoken", unspoken: { pid, protocol: protocol ?? 0, project: named } };
31503
+ }
31504
+ const editorPid = readNumber(fields, "editor_pid");
31505
+ return {
31506
+ kind: "runtime",
31507
+ endpoint: {
31508
+ pid,
31509
+ port,
31510
+ address,
31511
+ project: named,
31512
+ file,
31513
+ ...editorPid !== undefined && Number.isInteger(editorPid) && editorPid > 0 ? { editorPid } : {}
31514
+ }
31515
+ };
31516
+ }
31517
+ function runtimesAnnounced(directories = runtimeDirectories()) {
31518
+ const running = new Map;
31519
+ const unspoken = new Map;
31520
+ for (const directory of directories) {
31521
+ for (const found of announcedIn(directory)) {
31522
+ if (found.kind === "runtime" && !running.has(found.endpoint.pid)) {
31523
+ running.set(found.endpoint.pid, found.endpoint);
31524
+ } else if (found.kind === "unspoken" && !unspoken.has(found.unspoken.pid)) {
31525
+ unspoken.set(found.unspoken.pid, found.unspoken);
31526
+ }
31527
+ }
31528
+ }
31529
+ const newest = (a, b) => b.pid - a.pid;
31530
+ return { running: [...running.values()].sort(newest), unspoken: [...unspoken.values()].sort(newest) };
31531
+ }
31532
+ function discoverRuntimes(directories = runtimeDirectories()) {
31533
+ return runtimesAnnounced(directories).running;
31534
+ }
31535
+ function wentAway(file, pid) {
31536
+ let project = "";
31537
+ try {
31538
+ const fields = asParams(JSON.parse(readFileSync10(file, "utf8")));
31539
+ project = readString(readParams(fields, "project") ?? {}, "path") ?? "";
31540
+ } catch {
31541
+ return;
31542
+ }
31543
+ if (project === "") {
31544
+ return;
31545
+ }
31546
+ gone.unshift({ pid, project, noticedAt: Date.now() });
31547
+ gone.length = Math.min(gone.length, GONE_KEPT);
31548
+ }
31549
+ function announcedAndGone(projectPath) {
31550
+ if (projectPath === undefined) {
31551
+ return [];
31552
+ }
31553
+ const since = Date.now() - GONE_WINDOW_MS;
31554
+ const wanted = resolve3(projectPath);
31555
+ return gone.filter((one) => one.noticedAt >= since && resolve3(one.project) === wanted);
31556
+ }
31557
+ function strangersAmong(candidates, now, startTimes = startTimesOf) {
31558
+ const toJudge = candidates.filter((one) => {
31559
+ if (now - one.writtenAt < JUDGED_AFTER_MS) {
31560
+ return false;
31561
+ }
31562
+ const known = verdicts.get(one.file);
31563
+ return known === undefined || known.writtenAt !== one.writtenAt || !known.stranger && now - known.judgedAt > CONFIRMED_FOR_MS;
31564
+ });
31565
+ if (toJudge.length > 0) {
31566
+ const began = startTimes(toJudge.map((one) => one.pid));
31567
+ for (const one of toJudge) {
31568
+ const at = began.get(one.pid);
31569
+ const stranger = at !== undefined && at - one.writtenAt > STARTED_AFTER_ANNOUNCING_MS;
31570
+ verdicts.set(one.file, { writtenAt: one.writtenAt, judgedAt: now, stranger });
31571
+ }
31572
+ }
31573
+ return new Set(candidates.filter((one) => verdicts.get(one.file)?.stranger === true).map((one) => one.file));
31574
+ }
31575
+ function announcedIn(directory) {
31576
+ if (!existsSync10(directory)) {
31577
+ return [];
31578
+ }
31579
+ const found = [];
31580
+ const candidates = [];
31581
+ const entries = [];
31582
+ for (const entry of readdirSync4(directory)) {
31583
+ const report = ERROR_REPORT_PATTERN.exec(entry);
31584
+ if (report) {
31585
+ sweepErrorReport(join12(directory, entry), Number.parseInt(report[1] ?? "", 10));
31586
+ continue;
31587
+ }
31588
+ const match = ANNOUNCEMENT_PATTERN.exec(entry);
31589
+ if (!match) {
31590
+ continue;
31591
+ }
31592
+ const file = join12(directory, entry);
31593
+ const pid = Number.parseInt(match[1] ?? "", 10);
31594
+ const alive = processAlive(pid);
31595
+ entries.push({ file, pid, alive });
31596
+ if (alive) {
31597
+ try {
31598
+ candidates.push({ file, pid, writtenAt: statSync3(file).mtimeMs });
31599
+ } catch {}
31600
+ }
31601
+ }
31602
+ const strangers = strangersAmong(candidates, Date.now());
31603
+ for (const { file, pid, alive } of entries) {
31604
+ const announced = alive && !strangers.has(file) ? parseAnnouncement(file, pid) : { kind: "rubbish" };
31605
+ if (announced.kind === "rubbish") {
31606
+ if (!alive || strangers.has(file)) {
31607
+ wentAway(file, pid);
31608
+ }
31609
+ try {
31610
+ unlinkSync(file);
31611
+ } catch {}
31612
+ verdicts.delete(file);
31613
+ continue;
31614
+ }
31615
+ found.push(announced);
31616
+ }
31617
+ const held = new Set(entries.map((one) => one.file));
31618
+ const here = join12(directory, ".");
31619
+ for (const file of verdicts.keys()) {
31620
+ if (dirname7(file) === here && !held.has(file)) {
31621
+ verdicts.delete(file);
31622
+ }
31623
+ }
31624
+ return found;
31625
+ }
31626
+ async function announcementEnded(pid, directories = runtimeDirectories()) {
31627
+ const files = directories.map((directory) => join12(directory, `runtime-${pid}.json`)).filter((file) => existsSync10(file));
31628
+ const first = files[0];
31629
+ if (first === undefined) {
31630
+ return;
31631
+ }
31632
+ const until = Date.now() + ENDED_WITHIN_MS;
31633
+ while (processAlive(pid)) {
31634
+ const left = until - Date.now();
31635
+ if (left <= 0) {
31636
+ return;
31637
+ }
31638
+ await delay(Math.min(LOOK_EVERY_MS, left));
31639
+ }
31640
+ wentAway(first, pid);
31641
+ for (const file of files) {
31642
+ try {
31643
+ unlinkSync(file);
31644
+ } catch {}
31645
+ verdicts.delete(file);
31646
+ }
31647
+ }
31648
+ function sweepErrorReport(path, pid, now = Date.now()) {
31649
+ if (processAlive(pid)) {
31650
+ return;
31651
+ }
31652
+ try {
31653
+ if (now - statSync3(path).mtimeMs >= ERROR_REPORT_KEEP_MS) {
31654
+ unlinkSync(path);
31655
+ }
31656
+ } catch {}
31657
+ }
31658
+ async function announcedSince(projectPath, before, waiting = {}) {
31659
+ const wanted = resolve3(projectPath);
31660
+ const directories = waiting.directories ?? runtimeDirectories();
31661
+ const until = Date.now() + Math.max(waiting.budgetMs ?? ANNOUNCE_BUDGET_MS, 0);
31662
+ for (;; ) {
31663
+ const fresh = discoverRuntimes(directories).find((endpoint) => !before.has(endpoint.pid) && resolve3(endpoint.project.path) === wanted && (waiting.accept?.(endpoint) ?? true));
31664
+ if (fresh !== undefined) {
31665
+ return fresh;
31666
+ }
31667
+ const left = until - Date.now();
31668
+ if (left <= 0 || waiting.giveUp?.() === true) {
31669
+ return null;
31670
+ }
31671
+ await delay(Math.min(LOOK_EVERY_MS, left));
31672
+ }
31673
+ }
31674
+ function describe2(endpoint) {
31675
+ return `pid ${endpoint.pid} on ${endpoint.address}:${endpoint.port} (${endpoint.project.name || "unnamed"} at ${endpoint.project.path})`;
31676
+ }
31677
+ function tooNew(unspoken) {
31678
+ const named = unspoken.map((game) => `pid ${game.pid} speaks ${game.protocol} (${game.project.name || "unnamed"} at ${game.project.path})`).join("; ");
31679
+ 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";
31680
+ return `A game is running, in a protocol this server does not speak ${named}. This server speaks ${RUNTIME_PROTOCOL}, so ${half}.`;
31681
+ }
31682
+ function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
31683
+ if (pid !== undefined) {
31684
+ const named = endpoints.find((endpoint) => endpoint.pid === pid);
31685
+ if (named !== undefined) {
31686
+ if (projectPath !== undefined && resolve3(named.project.path) !== resolve3(projectPath)) {
31687
+ return {
31688
+ problem: `The game with pid ${pid} is running ${named.project.path}, not ${resolve3(projectPath)}. Running: ${endpoints.map(describe2).join("; ")}.`
31689
+ };
31690
+ }
31691
+ return { endpoint: named };
31692
+ }
31693
+ const tooNewToo = unspoken.find((game) => game.pid === pid);
31694
+ if (tooNewToo !== undefined) {
31695
+ return { problem: tooNew([tooNewToo]) };
31696
+ }
31697
+ return {
31698
+ 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.`
31699
+ };
31700
+ }
31701
+ if (endpoints.length === 0) {
31702
+ if (unspoken.length > 0) {
31703
+ return { problem: tooNew(unspoken) };
31704
+ }
31705
+ const went = announcedAndGone(projectPath);
31706
+ const first = went[0];
31707
+ if (first !== undefined) {
31708
+ const ago = Math.max(1, Math.round((Date.now() - first.noticedAt) / 1000));
31709
+ const others = went.length - 1;
31710
+ const earlier = others === 0 ? "" : ` ${others} earlier ${others === 1 ? "one" : "ones"} went the same way.`;
31711
+ return {
31712
+ problem: `A game with the runtime addon announced itself and its process is gone: pid ${first.pid}` + `${first.project === "" ? "" : ` for ${first.project}`}, noticed ${ago}s ago.${earlier}` + " It quit or was ended rather than never starting, so editor_output has what it printed" + " on the way, including whatever it broke on. Start another with editor_run once you have read it."
31713
+ };
31714
+ }
31715
+ return {
31716
+ problem: "No game with the runtime addon is running. Start one with editor_run, or play the project from the editor with the addon enabled."
31717
+ };
31718
+ }
31719
+ if (projectPath !== undefined) {
31720
+ const wanted = resolve3(projectPath);
31721
+ const matching = endpoints.filter((endpoint) => resolve3(endpoint.project.path) === wanted);
31722
+ if (matching.length === 0) {
31723
+ return {
31724
+ problem: `No running game is from ${wanted}. Running: ${endpoints.map(describe2).join("; ")}.`
31725
+ };
31726
+ }
31727
+ if (matching.length === 1 && matching[0]) {
31728
+ return { endpoint: matching[0] };
31729
+ }
31730
+ return {
31731
+ problem: `Several games are running from ${wanted}: ${matching.map(describe2).join("; ")}. Pass pid to choose one.`
31732
+ };
31733
+ }
31734
+ if (endpoints.length === 1 && endpoints[0]) {
31735
+ return { endpoint: endpoints[0] };
31736
+ }
31737
+ return {
31738
+ problem: `Several games are running: ${endpoints.map(describe2).join("; ")}. Pass projectPath to choose one, or pid when they are from one project.`
31739
+ };
31740
+ }
31741
+ function runtimeRequest(endpoint, command, params, timeoutMs) {
31742
+ const id = nextRequestId++;
31743
+ return new Promise((settle) => {
31744
+ let done = false;
31745
+ let buffered = "";
31746
+ let welcomed = false;
31747
+ const socket = createConnection({ port: endpoint.port, host: endpoint.address });
31748
+ const finish = (reply) => {
31749
+ if (done) {
31750
+ return;
31751
+ }
31752
+ done = true;
31753
+ clearTimeout(timer);
31754
+ socket.destroy();
31755
+ settle(reply);
31756
+ };
31757
+ const timer = setTimeout(() => {
31758
+ finish({
31759
+ ok: false,
31760
+ reason: "busy",
31761
+ message: `The game (${describe2(endpoint)}) accepted the connection but did not answer '${command}' within ${timeoutMs}ms. It may be paused at a breakpoint or stuck in a long frame.`
31762
+ });
31763
+ }, timeoutMs);
31764
+ socket.setEncoding("utf8");
31765
+ socket.on("connect", () => {
31766
+ socket.write(`${JSON.stringify({ id, command, params })}
31767
+ `);
31768
+ });
31769
+ socket.on("data", (chunk) => {
31770
+ buffered += chunk;
31771
+ let newline = buffered.indexOf(`
31772
+ `);
31773
+ while (newline !== -1 && !done) {
31774
+ const line = buffered.slice(0, newline).trim();
31775
+ buffered = buffered.slice(newline + 1);
31776
+ newline = buffered.indexOf(`
31777
+ `);
31778
+ if (line === "") {
31779
+ continue;
31780
+ }
31781
+ let message;
31782
+ try {
31783
+ message = asParams(JSON.parse(line));
31784
+ } catch {
31785
+ continue;
31786
+ }
31787
+ if (!welcomed) {
31788
+ welcomed = true;
31789
+ if (readString(message, "type") === "welcome") {
31790
+ const spoken = readNumber(message, "protocol");
31791
+ if (spoken !== RUNTIME_PROTOCOL) {
31792
+ finish({
31793
+ ok: false,
31794
+ reason: "protocol",
31795
+ message: `The game (${describe2(endpoint)}) speaks runtime protocol ${spoken ?? "unknown"} and this server speaks ${RUNTIME_PROTOCOL}. Reinstall the runtime addon into the project.`
31796
+ });
31797
+ return;
31798
+ }
31799
+ continue;
31800
+ }
31801
+ }
31802
+ if (message["id"] !== id) {
31803
+ continue;
31804
+ }
31805
+ if (readString(message, "type") === "error") {
31806
+ finish({ ok: false, reason: "error", message: readString(message, "message") ?? "unknown error" });
31807
+ return;
31808
+ }
31809
+ finish({ ok: true, payload: message, endpoint });
31810
+ }
31811
+ });
31812
+ socket.on("error", (error) => {
31813
+ finish({
31814
+ ok: false,
31815
+ reason: "refused",
31816
+ message: `The game (${describe2(endpoint)}) is running but nothing answered on its port: ${error.code ?? error.message}. It may still be starting, or be shutting down.`
31817
+ });
31818
+ });
31819
+ socket.on("close", () => {
31820
+ finish({
31821
+ ok: false,
31822
+ reason: "refused",
31823
+ message: `The game (${describe2(endpoint)}) closed the connection before answering '${command}'.`
31824
+ });
31825
+ });
31826
+ });
31827
+ }
31828
+ var RUNTIME_PROTOCOL = 2, ANNOUNCEMENT_PATTERN, ERROR_REPORT_PATTERN, ERROR_REPORT_KEEP_MS, GONE_WINDOW_MS = 120000, GONE_KEPT = 32, gone, STARTED_AFTER_ANNOUNCING_MS = 2000, CONFIRMED_FOR_MS = 60000, JUDGED_AFTER_MS = 60000, verdicts, ENDED_WITHIN_MS = 3000, LOOK_EVERY_MS = 50, ANNOUNCE_BUDGET_MS = 5000, nextRequestId = 1;
31829
+ var init_runtime_client = __esm(() => {
31830
+ init_process_children();
31831
+ ANNOUNCEMENT_PATTERN = /^runtime-(\d+)\.json$/;
31832
+ ERROR_REPORT_PATTERN = /^runtime-(\d+)\.log$/;
31833
+ ERROR_REPORT_KEEP_MS = 60 * 60 * 1000;
31834
+ gone = [];
31835
+ verdicts = new Map;
31836
+ });
31837
+
31838
+ // src/boot-note.ts
31839
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync8 } from "node:fs";
31840
+ import { dirname as dirname8, join as join13 } from "node:path";
31841
+ function bootNotePath(projectPath) {
31842
+ return join13(projectPath, ".godot", "gdharness-boot.json");
31843
+ }
31844
+ function readBootNote(projectPath) {
31845
+ try {
31846
+ const noted = readNumber(asParams(JSON.parse(readFileSync11(bootNotePath(projectPath), "utf8"))), "announcedAfterMs");
31847
+ return noted !== undefined && Number.isFinite(noted) && noted > 0 ? noted : null;
31848
+ } catch {
31849
+ return null;
31850
+ }
31851
+ }
31852
+ function writeBootNote(projectPath, announcedAfterMs) {
31853
+ try {
31854
+ const path = bootNotePath(projectPath);
31855
+ mkdirSync6(dirname8(path), { recursive: true });
31856
+ writeFileSync8(path, JSON.stringify({ announcedAfterMs: Math.round(announcedAfterMs), at: new Date().toISOString() }, null, 2), "utf8");
31857
+ } catch {}
31858
+ }
31859
+ function waitSizedTo(lastBootMs, usual = ANNOUNCE_BUDGET_MS) {
31860
+ if (lastBootMs === null) {
31861
+ return usual;
31862
+ }
31863
+ return Math.min(LONGEST_SIZED_WAIT_MS, Math.max(usual, Math.round(lastBootMs * 1.5)));
31864
+ }
31865
+ var LONGEST_SIZED_WAIT_MS = 60000;
31866
+ var init_boot_note = __esm(() => {
31867
+ init_runtime_client();
31868
+ });
31869
+
31870
+ // src/breakpoint-note.ts
31871
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync12, rmSync as rmSync6, writeFileSync as writeFileSync9 } from "node:fs";
31872
+ import { dirname as dirname9, isAbsolute as isAbsolute2, join as join14, relative as relative2 } from "node:path";
31873
+ function breakpointNotePath(projectPath) {
31874
+ return join14(projectPath, ".godot", "gdharness-breakpoints.json");
31875
+ }
31876
+ function writeBreakpointNote(projectPath, held) {
31877
+ const path = breakpointNotePath(projectPath);
31878
+ const root = realPathOr(projectPath);
31879
+ const inside = held.map((one) => ({ scriptPath: realPathOr(one.scriptPath), lines: one.lines })).filter((one) => one.lines.length > 0 && withinProject(root, one.scriptPath));
31880
+ try {
31881
+ if (inside.length === 0) {
31882
+ rmSync6(path, { force: true });
31883
+ return;
31884
+ }
31885
+ const note = {
31886
+ breakpoints: inside.map((one) => ({
31887
+ script: relative2(root, one.scriptPath).replaceAll("\\", "/"),
31888
+ lines: [...one.lines]
31889
+ }))
31890
+ };
31891
+ mkdirSync7(dirname9(path), { recursive: true });
31892
+ writeFileSync9(path, `${JSON.stringify(note, null, 2)}
31893
+ `, "utf8");
31894
+ } catch (error) {
31895
+ console.error(`[SERVER] Could not record the breakpoints at ${path}: ${errorMessage(error)}`);
31896
+ }
31897
+ }
31898
+ function readBreakpointNote(projectPath) {
31899
+ try {
31900
+ const parsed = JSON.parse(readFileSync12(breakpointNotePath(projectPath), "utf8"));
31901
+ if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.breakpoints)) {
31902
+ return [];
31903
+ }
31904
+ const held = [];
31905
+ for (const entry of parsed.breakpoints) {
31906
+ if (typeof entry !== "object" || entry === null) {
31907
+ continue;
31908
+ }
31909
+ const { script, lines: listed } = entry;
31910
+ if (typeof script !== "string" || isAbsolute2(script) || script.split("/").includes("..")) {
31911
+ continue;
31912
+ }
31913
+ const lines = Array.isArray(listed) ? listed.filter((line) => Number.isInteger(line) && line > 0) : [];
31914
+ if (lines.length > 0) {
31915
+ held.push({ scriptPath: realPathOr(join14(projectPath, script)), lines });
31916
+ }
31917
+ }
31918
+ return held;
31919
+ } catch {
31920
+ return [];
31921
+ }
31922
+ }
31923
+ function withinProject(projectPath, scriptPath) {
31924
+ const rel = relative2(projectPath, scriptPath);
31925
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute2(rel);
31926
+ }
31927
+ var init_breakpoint_note = __esm(() => {
31928
+ init_errors();
31929
+ init_paths();
31930
+ });
31931
+
31932
+ // src/bridge-announce.ts
31933
+ import { mkdirSync as mkdirSync8, readFileSync as readFileSync13, rmSync as rmSync7, writeFileSync as writeFileSync10 } from "node:fs";
31934
+ import { dirname as dirname10, join as join15 } from "node:path";
31935
+ function announcementPath(projectPath) {
31936
+ return join15(projectPath, ".godot", "gdharness-bridge.json");
31937
+ }
31938
+ function readAnnouncement(path) {
31939
+ try {
31940
+ const parsed = JSON.parse(readFileSync13(path, "utf8"));
31941
+ if (typeof parsed !== "object" || parsed === null) {
31942
+ return null;
31943
+ }
31944
+ const said = parsed;
31945
+ if (typeof said.port !== "number" || typeof said.pid !== "number") {
31946
+ return null;
31947
+ }
31948
+ return said;
31949
+ } catch {
31950
+ return null;
31951
+ }
31952
+ }
31953
+ function announcementIsOurs(path) {
31954
+ return readAnnouncement(path)?.pid === process.pid;
31955
+ }
31956
+ function announceBridge(projectPath, bridge) {
31957
+ const path = announcementPath(projectPath);
31958
+ try {
31959
+ mkdirSync8(dirname10(path), { recursive: true });
31960
+ const announcement = {
31961
+ protocol: BRIDGE_ANNOUNCE_PROTOCOL,
31962
+ host: bridge.host,
31963
+ port: bridge.port,
31964
+ pid: process.pid,
31965
+ version: bridge.version,
31966
+ startedAt: new Date().toISOString()
31967
+ };
31968
+ writeFileSync10(path, `${JSON.stringify(announcement, null, 2)}
31969
+ `, "utf8");
31970
+ return path;
31971
+ } catch (error) {
31972
+ console.error(`[SERVER] Could not announce the editor bridge at ${path}: ${errorMessage(error)}`);
31973
+ return null;
31974
+ }
31975
+ }
31976
+ function withdrawBridge(path) {
31977
+ if (path === null || !announcementIsOurs(path)) {
31978
+ return;
31979
+ }
31980
+ try {
31981
+ rmSync7(path, { force: true });
31982
+ } catch (error) {
31983
+ console.error(`[SERVER] Could not withdraw the editor bridge announcement: ${errorMessage(error)}`);
31984
+ }
31985
+ }
31986
+ var BRIDGE_ANNOUNCE_PROTOCOL = 1;
31987
+ var init_bridge_announce = __esm(() => {
31988
+ init_errors();
31989
+ });
31990
+
31991
+ // src/config-pin.ts
31992
+ import { existsSync as existsSync11, readFileSync as readFileSync14 } from "node:fs";
31993
+ import { join as join16 } from "node:path";
31994
+ function versionsNamedInConfig(projectPath) {
31995
+ const config = join16(projectPath, ".mcp.json");
31996
+ if (!existsSync11(config)) {
31997
+ return [];
31998
+ }
31999
+ let text;
32000
+ try {
32001
+ text = readFileSync14(config, "utf8");
32002
+ JSON.parse(text);
32003
+ } catch {
32004
+ return [];
32005
+ }
32006
+ return [...new Set([...text.matchAll(PINNED)].map((found) => found[1] ?? ""))].filter((version) => version !== "");
32007
+ }
32008
+ function configDisagrees(projectPath, serverVersion) {
32009
+ const named = versionsNamedInConfig(projectPath);
32010
+ const others = named.filter((version) => version !== serverVersion);
32011
+ if (others.length === 0) {
32012
+ return null;
32013
+ }
32014
+ return `.mcp.json in this project names gdharness@${others.join(" and gdharness@")}, and this server ` + `is ${serverVersion}. Whichever is meant to be right, nothing will change version until the ` + "MCP server is reconnected, and a pin kept anywhere else does not reach this file.";
32015
+ }
32016
+ var PINNED;
32017
+ var init_config_pin = __esm(() => {
32018
+ PINNED = /\bgdharness@(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)\b/g;
31388
32019
  });
31389
32020
 
31390
32021
  // src/framing.ts
@@ -31514,8 +32145,8 @@ var init_ports = __esm(() => {
31514
32145
  });
31515
32146
 
31516
32147
  // src/dap_client.ts
31517
- import { createConnection } from "node:net";
31518
- import { setTimeout as delay } from "node:timers/promises";
32148
+ import { createConnection as createConnection2 } from "node:net";
32149
+ import { setTimeout as delay2 } from "node:timers/promises";
31519
32150
  function said(body, field) {
31520
32151
  const value = body?.[field];
31521
32152
  return typeof value === "string" ? value : "";
@@ -31550,7 +32181,7 @@ class GodotDAPClient {
31550
32181
  return;
31551
32182
  }
31552
32183
  await new Promise((resolve, reject) => {
31553
- const socket = createConnection({ host: this.host, port: this.port });
32184
+ const socket = createConnection2({ host: this.host, port: this.port });
31554
32185
  this.socket = socket;
31555
32186
  const connectTimeout = setTimeout(() => {
31556
32187
  socket.destroy();
@@ -31959,7 +32590,7 @@ class GodotDAPClient {
31959
32590
  return answered;
31960
32591
  }
31961
32592
  } catch {}
31962
- await delay(200);
32593
+ await delay2(200);
31963
32594
  }
31964
32595
  return [];
31965
32596
  }
@@ -31980,7 +32611,7 @@ class GodotDAPClient {
31980
32611
  break;
31981
32612
  }
31982
32613
  } catch {}
31983
- await delay(200);
32614
+ await delay2(200);
31984
32615
  }
31985
32616
  const named = [];
31986
32617
  for (const scope of scopes) {
@@ -35496,10 +36127,10 @@ var init_godot_bridge = __esm(() => {
35496
36127
  });
35497
36128
 
35498
36129
  // src/godot-path.ts
35499
- import { execFile as execFile4 } from "node:child_process";
35500
- import { existsSync as existsSync11 } from "node:fs";
36130
+ import { execFile as execFile5 } from "node:child_process";
36131
+ import { existsSync as existsSync12 } from "node:fs";
35501
36132
  import { normalize as normalize2 } from "node:path";
35502
- import { promisify as promisify4 } from "node:util";
36133
+ import { promisify as promisify5 } from "node:util";
35503
36134
 
35504
36135
  class GodotLocator2 {
35505
36136
  answered = new Map;
@@ -35552,11 +36183,11 @@ class GodotLocator2 {
35552
36183
  return known;
35553
36184
  }
35554
36185
  let reason;
35555
- if (path !== "godot" && !existsSync11(path)) {
36186
+ if (path !== "godot" && !existsSync12(path)) {
35556
36187
  reason = "does not exist";
35557
36188
  } else {
35558
36189
  try {
35559
- await run4(path, ["--version"]);
36190
+ await run5(path, ["--version"]);
35560
36191
  reason = null;
35561
36192
  } catch (error) {
35562
36193
  reason = `does not answer --version: ${errorMessage(error)}`;
@@ -35566,11 +36197,11 @@ class GodotLocator2 {
35566
36197
  return reason;
35567
36198
  }
35568
36199
  }
35569
- var run4;
36200
+ var run5;
35570
36201
  var init_godot_path = __esm(() => {
35571
36202
  init_detection();
35572
36203
  init_errors();
35573
- run4 = promisify4(execFile4);
36204
+ run5 = promisify5(execFile5);
35574
36205
  });
35575
36206
 
35576
36207
  // src/issues.ts
@@ -35927,8 +36558,8 @@ var init_junit = __esm(() => {
35927
36558
  // src/lsp_client.ts
35928
36559
  import { realpathSync as realpathSync2 } from "node:fs";
35929
36560
  import { readFile, realpath } from "node:fs/promises";
35930
- import { createConnection as createConnection2 } from "node:net";
35931
- import { dirname as dirname9, resolve as resolve3 } from "node:path";
36561
+ import { createConnection as createConnection3 } from "node:net";
36562
+ import { dirname as dirname11, resolve as resolve4 } from "node:path";
35932
36563
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
35933
36564
  function diagnosticsKey(uri) {
35934
36565
  let decoded;
@@ -35974,7 +36605,7 @@ class GodotLSPClient {
35974
36605
  }
35975
36606
  this.connectPromise = new Promise((resolveConnect, rejectConnect) => {
35976
36607
  let settled = false;
35977
- const socket = createConnection2({ port: this.port, host: this.host }, () => {
36608
+ const socket = createConnection3({ port: this.port, host: this.host }, () => {
35978
36609
  this.socket = socket;
35979
36610
  this.connected = true;
35980
36611
  this.reader = new FrameReader;
@@ -36056,7 +36687,7 @@ class GodotLSPClient {
36056
36687
  if (this.initialized) {
36057
36688
  return;
36058
36689
  }
36059
- const rootPath = this.rootPath ?? dirname9(resolve3(filePath));
36690
+ const rootPath = this.rootPath ?? dirname11(resolve4(filePath));
36060
36691
  await this.initialize(rootPath);
36061
36692
  }
36062
36693
  async sendRequest(method, params) {
@@ -36194,7 +36825,7 @@ class GodotLSPClient {
36194
36825
  });
36195
36826
  }
36196
36827
  toFileUri(filePath) {
36197
- return pathToFileURL(resolve3(filePath)).href;
36828
+ return pathToFileURL(resolve4(filePath)).href;
36198
36829
  }
36199
36830
  syncDocument(filePath, content) {
36200
36831
  const uri = this.toFileUri(filePath);
@@ -36235,7 +36866,7 @@ class GodotLSPClient {
36235
36866
  }
36236
36867
  async initialize(rootPath) {
36237
36868
  await this.ensureConnected();
36238
- const resolvedRootPath = resolve3(rootPath);
36869
+ const resolvedRootPath = resolve4(rootPath);
36239
36870
  const rootUri = pathToFileURL(resolvedRootPath).href;
36240
36871
  const result = await this.sendRequest("initialize", {
36241
36872
  processId: process.pid,
@@ -36378,7 +37009,7 @@ function normalizeLSPError(error, port) {
36378
37009
  return String(error);
36379
37010
  }
36380
37011
  async function resolveLSPPaths(projectPathValue, scriptPathValue) {
36381
- const requestedProjectPath = resolve3(projectPathValue);
37012
+ const requestedProjectPath = resolve4(projectPathValue);
36382
37013
  let projectPath;
36383
37014
  try {
36384
37015
  projectPath = await realpath(requestedProjectPath);
@@ -36432,169 +37063,33 @@ async function handleLSPTool(client, toolName, args) {
36432
37063
  return asToolResponse({ completions });
36433
37064
  }
36434
37065
  case "lsp_get_hover": {
36435
- const line = Number(parsedArgs["line"]);
36436
- const character = Number(parsedArgs["character"]);
36437
- if (!Number.isFinite(line) || !Number.isFinite(character)) {
36438
- throw new Refusal2("Arguments line and character must be numbers.");
36439
- }
36440
- const hover = await client.getHover(scriptPath, content, line, character);
36441
- return asToolResponse({ hover });
36442
- }
36443
- case "lsp_get_symbols": {
36444
- const symbols = await client.getDocumentSymbols(scriptPath, content);
36445
- return asToolResponse({ symbols });
36446
- }
36447
- default:
36448
- return asToolResponse({ error: `Unknown LSP tool: ${toolName}` });
36449
- }
36450
- } catch (error) {
36451
- return asToolResponse({
36452
- error: normalizeLSPError(error, client.port)
36453
- });
36454
- }
36455
- }
36456
- var DEFAULT_LSP_PORT = 6005, DIAGNOSTICS_TIMEOUT_MS = 5000;
36457
- var init_lsp_client = __esm(() => {
36458
- init_errors();
36459
- init_framing();
36460
- init_paths();
36461
- init_ports();
36462
- });
36463
-
36464
- // src/process-children.ts
36465
- import { execFile as execFile5 } from "node:child_process";
36466
- import { basename as basename2 } from "node:path";
36467
- import { promisify as promisify5 } from "node:util";
36468
- async function processTree() {
36469
- try {
36470
- const printed = process.platform === "win32" ? await askWindows() : await askPosix();
36471
- return parseProcessTable(printed);
36472
- } catch {
36473
- return;
36474
- }
36475
- }
36476
- function parseProcessTable(printed) {
36477
- const tree = new Map;
36478
- for (const line of printed.split(/\r?\n/)) {
36479
- const fields = /^\s*(\d+)\s+(\d+)(?:\s+(.*))?$/.exec(line);
36480
- if (fields === null) {
36481
- continue;
36482
- }
36483
- const pid = Number(fields[1]);
36484
- const parent = Number(fields[2]);
36485
- if (pid > 0) {
36486
- tree.set(pid, { parent, command: (fields[3] ?? "").trim() });
36487
- }
36488
- }
36489
- return tree;
36490
- }
36491
- function childrenIn(tree, pid) {
36492
- const children = [];
36493
- for (const [child, listed] of tree) {
36494
- if (listed.parent === pid) {
36495
- children.push(child);
36496
- }
36497
- }
36498
- return children;
36499
- }
36500
- function descendantsIn(tree, pid) {
36501
- const found = [];
36502
- const queue = [pid];
36503
- const seen = new Set([pid]);
36504
- while (queue.length > 0) {
36505
- const next = queue.shift();
36506
- if (next === undefined) {
36507
- break;
36508
- }
36509
- for (const child of childrenIn(tree, next)) {
36510
- if (!seen.has(child)) {
36511
- seen.add(child);
36512
- found.push(child);
36513
- queue.push(child);
36514
- }
36515
- }
36516
- }
36517
- return found;
36518
- }
36519
- function ancestorsIn(tree, pid, root) {
36520
- const chain = [];
36521
- const seen = new Set([pid]);
36522
- let parent = tree.get(pid)?.parent;
36523
- while (parent !== undefined && !seen.has(parent)) {
36524
- chain.push(parent);
36525
- if (parent === root) {
36526
- break;
36527
- }
36528
- seen.add(parent);
36529
- parent = tree.get(parent)?.parent;
36530
- }
36531
- return chain;
36532
- }
36533
- function readCommandLine(command) {
36534
- const words = commandWords(command);
36535
- const executable = words[0] === undefined ? "" : basename2(words[0].replaceAll("\\", "/"));
36536
- const at = words.indexOf("--path");
36537
- let projectPath = null;
36538
- if (at !== -1 && at + 1 < words.length) {
36539
- const taken = [];
36540
- for (const word of words.slice(at + 1)) {
36541
- if (taken.length > 0 && (word.startsWith("-") || /^[a-z]+:\/\//.test(word))) {
36542
- break;
37066
+ const line = Number(parsedArgs["line"]);
37067
+ const character = Number(parsedArgs["character"]);
37068
+ if (!Number.isFinite(line) || !Number.isFinite(character)) {
37069
+ throw new Refusal2("Arguments line and character must be numbers.");
37070
+ }
37071
+ const hover = await client.getHover(scriptPath, content, line, character);
37072
+ return asToolResponse({ hover });
36543
37073
  }
36544
- taken.push(word);
36545
- }
36546
- projectPath = taken.join(" ");
36547
- }
36548
- return {
36549
- executable,
36550
- projectPath,
36551
- editor: words.some((word) => word === "-e" || word === "--editor")
36552
- };
36553
- }
36554
- function commandWords(command) {
36555
- const words = [];
36556
- let word = "";
36557
- let quoted = false;
36558
- let started = false;
36559
- for (const char of command) {
36560
- if (char === '"') {
36561
- quoted = !quoted;
36562
- started = true;
36563
- } else if (!quoted && /\s/.test(char)) {
36564
- if (started) {
36565
- words.push(word);
36566
- word = "";
36567
- started = false;
37074
+ case "lsp_get_symbols": {
37075
+ const symbols = await client.getDocumentSymbols(scriptPath, content);
37076
+ return asToolResponse({ symbols });
36568
37077
  }
36569
- } else {
36570
- word += char;
36571
- started = true;
37078
+ default:
37079
+ return asToolResponse({ error: `Unknown LSP tool: ${toolName}` });
36572
37080
  }
37081
+ } catch (error) {
37082
+ return asToolResponse({
37083
+ error: normalizeLSPError(error, client.port)
37084
+ });
36573
37085
  }
36574
- if (started) {
36575
- words.push(word);
36576
- }
36577
- return words;
36578
- }
36579
- async function askWindows() {
36580
- const { stdout } = await run5("powershell.exe", [
36581
- "-NoProfile",
36582
- "-NonInteractive",
36583
- "-Command",
36584
- 'Get-CimInstance Win32_Process | ForEach-Object { "$($_.ProcessId) $($_.ParentProcessId) $($_.CommandLine)" }'
36585
- ], { timeout: ASK_TIMEOUT_MS, windowsHide: true, maxBuffer: 16 * 1024 * 1024 });
36586
- return stdout;
36587
- }
36588
- async function askPosix() {
36589
- const { stdout } = await run5("ps", ["-e", "-o", "pid=,ppid=,args="], {
36590
- timeout: ASK_TIMEOUT_MS,
36591
- maxBuffer: 16 * 1024 * 1024
36592
- });
36593
- return stdout;
36594
37086
  }
36595
- var run5, ASK_TIMEOUT_MS = 15000;
36596
- var init_process_children = __esm(() => {
36597
- run5 = promisify5(execFile5);
37087
+ var DEFAULT_LSP_PORT = 6005, DIAGNOSTICS_TIMEOUT_MS = 5000;
37088
+ var init_lsp_client = __esm(() => {
37089
+ init_errors();
37090
+ init_framing();
37091
+ init_paths();
37092
+ init_ports();
36598
37093
  });
36599
37094
 
36600
37095
  // src/process-time.ts
@@ -36637,13 +37132,13 @@ var init_process_time = __esm(() => {
36637
37132
  });
36638
37133
 
36639
37134
  // src/project-scan.ts
36640
- import { existsSync as existsSync12, readdirSync as readdirSync4, readFileSync as readFileSync13 } from "node:fs";
36641
- import { join as join15 } from "node:path";
37135
+ import { existsSync as existsSync13, readdirSync as readdirSync5, readFileSync as readFileSync15 } from "node:fs";
37136
+ import { join as join17 } from "node:path";
36642
37137
  function skipped(name) {
36643
37138
  return name.startsWith(".") || SKIPPED.has(name);
36644
37139
  }
36645
37140
  function steppedOver(directory) {
36646
- return existsSync12(join15(directory, ".gdignore"));
37141
+ return existsSync13(join17(directory, ".gdignore"));
36647
37142
  }
36648
37143
  function projectStructure(projectPath) {
36649
37144
  const structure = { scenes: 0, scripts: 0, assets: 0, other: 0 };
@@ -36651,12 +37146,12 @@ function projectStructure(projectPath) {
36651
37146
  if (steppedOver(directory)) {
36652
37147
  return;
36653
37148
  }
36654
- for (const entry of readdirSync4(directory, { withFileTypes: true })) {
37149
+ for (const entry of readdirSync5(directory, { withFileTypes: true })) {
36655
37150
  if (skipped(entry.name)) {
36656
37151
  continue;
36657
37152
  }
36658
37153
  if (entry.isDirectory()) {
36659
- visit(join15(directory, entry.name));
37154
+ visit(join17(directory, entry.name));
36660
37155
  } else if (entry.isFile()) {
36661
37156
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
36662
37157
  if (extension === "tscn") {
@@ -36680,20 +37175,20 @@ function scriptsWithoutUid(projectPath) {
36680
37175
  if (steppedOver(directory)) {
36681
37176
  return;
36682
37177
  }
36683
- for (const entry of readdirSync4(directory, { withFileTypes: true })) {
37178
+ for (const entry of readdirSync5(directory, { withFileTypes: true })) {
36684
37179
  if (skipped(entry.name)) {
36685
37180
  continue;
36686
37181
  }
36687
37182
  const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
36688
37183
  if (entry.isDirectory()) {
36689
- visit(join15(directory, entry.name), spelled);
37184
+ visit(join17(directory, entry.name), spelled);
36690
37185
  continue;
36691
37186
  }
36692
37187
  if (!entry.isFile()) {
36693
37188
  continue;
36694
37189
  }
36695
37190
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
36696
- if (CARRIES_A_UID.has(extension) && !existsSync12(join15(directory, `${entry.name}.uid`))) {
37191
+ if (CARRIES_A_UID.has(extension) && !existsSync13(join17(directory, `${entry.name}.uid`))) {
36697
37192
  found.push(spelled);
36698
37193
  }
36699
37194
  }
@@ -36721,459 +37216,126 @@ function searchProject(projectPath, options) {
36721
37216
  if (steppedOver(directory)) {
36722
37217
  return;
36723
37218
  }
36724
- for (const entry of readdirSync4(directory, { withFileTypes: true })) {
37219
+ for (const entry of readdirSync5(directory, { withFileTypes: true })) {
36725
37220
  if (full()) {
36726
37221
  return;
36727
37222
  }
36728
37223
  if (skipped(entry.name)) {
36729
37224
  continue;
36730
37225
  }
36731
- const entryPath = join15(directory, entry.name);
37226
+ const entryPath = join17(directory, entry.name);
36732
37227
  if (entry.isDirectory()) {
36733
37228
  visit(entryPath);
36734
37229
  continue;
36735
37230
  }
36736
- const extension = entry.name.includes(".") ? entry.name.split(".").pop()?.toLowerCase() ?? "" : "";
36737
- if (!entry.isFile() || !extensions.has(extension)) {
36738
- continue;
36739
- }
36740
- result.summary.files_searched += 1;
36741
- const matches = [];
36742
- for (const [index, line] of readFileSync13(entryPath, "utf8").split(`
36743
- `).entries()) {
36744
- if (full()) {
36745
- break;
36746
- }
36747
- const match = regex ? regex.exec(line)?.[0] : (options.caseSensitive ? line : line.toLowerCase()).includes(needle) ? options.query : "";
36748
- if (match) {
36749
- matches.push({ line: index + 1, content: line.trim(), match });
36750
- result.summary.total_matches += 1;
36751
- }
36752
- }
36753
- if (matches.length > 0) {
36754
- const relativePath = entryPath.slice(projectPath.length + 1).replace(/\\/g, "/");
36755
- result.results.push({ file: `res://${relativePath}`, matches });
36756
- result.summary.files_with_matches += 1;
36757
- }
36758
- }
36759
- };
36760
- visit(projectPath);
36761
- return result;
36762
- }
36763
- var SKIPPED, ASSET_EXTENSIONS, CARRIES_A_UID;
36764
- var init_project_scan = __esm(() => {
36765
- SKIPPED = new Set(["node_modules"]);
36766
- ASSET_EXTENSIONS = new Set(["png", "jpg", "jpeg", "webp", "svg", "ttf", "otf", "wav", "mp3", "ogg"]);
36767
- CARRIES_A_UID = new Set(["gd", "shader", "gdshader"]);
36768
- });
36769
-
36770
- // src/restart-note.ts
36771
- import { mkdirSync as mkdirSync8, readFileSync as readFileSync14, rmSync as rmSync8, writeFileSync as writeFileSync10 } from "node:fs";
36772
- import { dirname as dirname10, join as join16 } from "node:path";
36773
- function restartNotePath(projectPath) {
36774
- return join16(projectPath, ".godot", "gdharness-restart.json");
36775
- }
36776
- function noteRestartBegun(note) {
36777
- const path = restartNotePath(note.projectPath);
36778
- try {
36779
- mkdirSync8(dirname10(path), { recursive: true });
36780
- writeFileSync10(path, `${JSON.stringify(note, null, 2)}
36781
- `, "utf8");
36782
- } catch (error) {
36783
- console.error(`[SERVER] Could not record the restart at ${path}: ${errorMessage(error)}`);
36784
- }
36785
- }
36786
- function restartOwed(projectPath) {
36787
- try {
36788
- const parsed = JSON.parse(readFileSync14(restartNotePath(projectPath), "utf8"));
36789
- if (typeof parsed !== "object" || parsed === null) {
36790
- return null;
36791
- }
36792
- const said = parsed;
36793
- if (typeof said.quitAt !== "string" || typeof said.byPid !== "number" || typeof said.ports !== "object") {
36794
- return null;
36795
- }
36796
- return said;
36797
- } catch {
36798
- return null;
36799
- }
36800
- }
36801
- function restartSettled(projectPath) {
36802
- try {
36803
- rmSync8(restartNotePath(projectPath), { force: true });
36804
- } catch (error) {
36805
- console.error(`[SERVER] Could not clear the restart note: ${errorMessage(error)}`);
36806
- }
36807
- }
36808
- var init_restart_note = __esm(() => {
36809
- init_errors();
36810
- });
36811
-
36812
- // src/runtime-client.ts
36813
- import { existsSync as existsSync13, readdirSync as readdirSync5, readFileSync as readFileSync15, statSync as statSync3, unlinkSync } from "node:fs";
36814
- import { createConnection as createConnection3 } from "node:net";
36815
- import { tmpdir as tmpdir4 } from "node:os";
36816
- import { join as join17, resolve as resolve4 } from "node:path";
36817
- import { setTimeout as delay2 } from "node:timers/promises";
36818
- function runtimeDirectory(variables = process.env) {
36819
- const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
36820
- if (explicit) {
36821
- return explicit;
36822
- }
36823
- const perUser = envValue("XDG_RUNTIME_DIR", variables);
36824
- return join17(perUser ?? tmpdir4(), "gdharness");
36825
- }
36826
- function runtimeDirectories(variables = process.env) {
36827
- const candidates = [runtimeDirectory(variables)];
36828
- const fallbacks = process.platform === "win32" ? [envValue("SystemRoot", variables) ?? envValue("windir", variables) ?? "C:\\Windows"] : ["/tmp", "/var/tmp"];
36829
- for (const base of fallbacks) {
36830
- candidates.push(join17(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
36831
- }
36832
- return [...new Set(candidates.map((path) => resolve4(path)))];
36833
- }
36834
- function errorReportOf(pid, directories = runtimeDirectories()) {
36835
- for (const directory of directories) {
36836
- const path = join17(directory, `runtime-${pid}.log`);
36837
- if (existsSync13(path)) {
36838
- return path;
36839
- }
36840
- }
36841
- return null;
36842
- }
36843
- function processAlive(pid) {
36844
- try {
36845
- process.kill(pid, 0);
36846
- return true;
36847
- } catch (error) {
36848
- return error.code === "EPERM";
36849
- }
36850
- }
36851
- function parseAnnouncement(file, pid) {
36852
- let fields;
36853
- try {
36854
- fields = asParams(JSON.parse(readFileSync15(file, "utf8")));
36855
- } catch {
36856
- return { kind: "rubbish" };
36857
- }
36858
- const project = readParams(fields, "project");
36859
- const port = readNumber(fields, "port");
36860
- const address = readString(fields, "address");
36861
- const protocol = readNumber(fields, "protocol");
36862
- if (readNumber(fields, "pid") !== pid || port === undefined || !Number.isInteger(port) || port < 1 || port > 65535 || address === undefined || project === undefined) {
36863
- return { kind: "rubbish" };
36864
- }
36865
- const named = {
36866
- name: readString(project, "name") ?? "",
36867
- path: readString(project, "path") ?? ""
36868
- };
36869
- if (protocol !== RUNTIME_PROTOCOL) {
36870
- return { kind: "unspoken", unspoken: { pid, protocol: protocol ?? 0, project: named } };
36871
- }
36872
- const editorPid = readNumber(fields, "editor_pid");
36873
- return {
36874
- kind: "runtime",
36875
- endpoint: {
36876
- pid,
36877
- port,
36878
- address,
36879
- project: named,
36880
- file,
36881
- ...editorPid !== undefined && Number.isInteger(editorPid) && editorPid > 0 ? { editorPid } : {}
36882
- }
36883
- };
36884
- }
36885
- function runtimesAnnounced(directories = runtimeDirectories()) {
36886
- const running = new Map;
36887
- const unspoken = new Map;
36888
- for (const directory of directories) {
36889
- for (const found of announcedIn(directory)) {
36890
- if (found.kind === "runtime" && !running.has(found.endpoint.pid)) {
36891
- running.set(found.endpoint.pid, found.endpoint);
36892
- } else if (found.kind === "unspoken" && !unspoken.has(found.unspoken.pid)) {
36893
- unspoken.set(found.unspoken.pid, found.unspoken);
36894
- }
36895
- }
36896
- }
36897
- const newest = (a, b) => b.pid - a.pid;
36898
- return { running: [...running.values()].sort(newest), unspoken: [...unspoken.values()].sort(newest) };
36899
- }
36900
- function discoverRuntimes(directories = runtimeDirectories()) {
36901
- return runtimesAnnounced(directories).running;
36902
- }
36903
- function wentAway(file, pid) {
36904
- let project = "";
36905
- try {
36906
- const fields = asParams(JSON.parse(readFileSync15(file, "utf8")));
36907
- project = readString(readParams(fields, "project") ?? {}, "path") ?? "";
36908
- } catch {
36909
- return;
36910
- }
36911
- if (project === "") {
36912
- return;
36913
- }
36914
- gone.unshift({ pid, project, noticedAt: Date.now() });
36915
- gone.length = Math.min(gone.length, GONE_KEPT);
36916
- }
36917
- function announcedAndGone(projectPath) {
36918
- if (projectPath === undefined) {
36919
- return [];
36920
- }
36921
- const since = Date.now() - GONE_WINDOW_MS;
36922
- const wanted = resolve4(projectPath);
36923
- return gone.filter((one) => one.noticedAt >= since && resolve4(one.project) === wanted);
36924
- }
36925
- function announcedIn(directory) {
36926
- if (!existsSync13(directory)) {
36927
- return [];
36928
- }
36929
- const found = [];
36930
- for (const entry of readdirSync5(directory)) {
36931
- const report = ERROR_REPORT_PATTERN.exec(entry);
36932
- if (report) {
36933
- sweepErrorReport(join17(directory, entry), Number.parseInt(report[1] ?? "", 10));
36934
- continue;
36935
- }
36936
- const match = ANNOUNCEMENT_PATTERN.exec(entry);
36937
- if (!match) {
36938
- continue;
36939
- }
36940
- const file = join17(directory, entry);
36941
- const pid = Number.parseInt(match[1] ?? "", 10);
36942
- const alive = processAlive(pid);
36943
- const announced = alive ? parseAnnouncement(file, pid) : { kind: "rubbish" };
36944
- if (announced.kind === "rubbish") {
36945
- if (!alive) {
36946
- wentAway(file, pid);
37231
+ const extension = entry.name.includes(".") ? entry.name.split(".").pop()?.toLowerCase() ?? "" : "";
37232
+ if (!entry.isFile() || !extensions.has(extension)) {
37233
+ continue;
37234
+ }
37235
+ result.summary.files_searched += 1;
37236
+ const matches = [];
37237
+ for (const [index, line] of readFileSync15(entryPath, "utf8").split(`
37238
+ `).entries()) {
37239
+ if (full()) {
37240
+ break;
37241
+ }
37242
+ const match = regex ? regex.exec(line)?.[0] : (options.caseSensitive ? line : line.toLowerCase()).includes(needle) ? options.query : "";
37243
+ if (match) {
37244
+ matches.push({ line: index + 1, content: line.trim(), match });
37245
+ result.summary.total_matches += 1;
37246
+ }
37247
+ }
37248
+ if (matches.length > 0) {
37249
+ const relativePath = entryPath.slice(projectPath.length + 1).replace(/\\/g, "/");
37250
+ result.results.push({ file: `res://${relativePath}`, matches });
37251
+ result.summary.files_with_matches += 1;
36947
37252
  }
36948
- try {
36949
- unlinkSync(file);
36950
- } catch {}
36951
- continue;
36952
37253
  }
36953
- found.push(announced);
36954
- }
36955
- return found;
37254
+ };
37255
+ visit(projectPath);
37256
+ return result;
36956
37257
  }
36957
- function sweepErrorReport(path, pid, now = Date.now()) {
36958
- if (processAlive(pid)) {
36959
- return;
36960
- }
37258
+ var SKIPPED, ASSET_EXTENSIONS, CARRIES_A_UID;
37259
+ var init_project_scan = __esm(() => {
37260
+ SKIPPED = new Set(["node_modules"]);
37261
+ ASSET_EXTENSIONS = new Set(["png", "jpg", "jpeg", "webp", "svg", "ttf", "otf", "wav", "mp3", "ogg"]);
37262
+ CARRIES_A_UID = new Set(["gd", "shader", "gdshader"]);
37263
+ });
37264
+
37265
+ // src/restart-note.ts
37266
+ import { mkdirSync as mkdirSync9, readFileSync as readFileSync16, rmSync as rmSync8, writeFileSync as writeFileSync11 } from "node:fs";
37267
+ import { dirname as dirname12, join as join18 } from "node:path";
37268
+ function restartNotePath(projectPath) {
37269
+ return join18(projectPath, ".godot", "gdharness-restart.json");
37270
+ }
37271
+ function noteRestartBegun(note) {
37272
+ const path = restartNotePath(note.projectPath);
36961
37273
  try {
36962
- if (now - statSync3(path).mtimeMs >= ERROR_REPORT_KEEP_MS) {
36963
- unlinkSync(path);
36964
- }
36965
- } catch {}
37274
+ mkdirSync9(dirname12(path), { recursive: true });
37275
+ writeFileSync11(path, `${JSON.stringify(note, null, 2)}
37276
+ `, "utf8");
37277
+ } catch (error) {
37278
+ console.error(`[SERVER] Could not record the restart at ${path}: ${errorMessage(error)}`);
37279
+ }
36966
37280
  }
36967
- async function announcedSince(projectPath, before, waiting = {}) {
36968
- const wanted = resolve4(projectPath);
36969
- const directories = waiting.directories ?? runtimeDirectories();
36970
- const until = Date.now() + Math.max(waiting.budgetMs ?? ANNOUNCE_BUDGET_MS, 0);
36971
- for (;; ) {
36972
- const fresh = discoverRuntimes(directories).find((endpoint) => !before.has(endpoint.pid) && resolve4(endpoint.project.path) === wanted && (waiting.accept?.(endpoint) ?? true));
36973
- if (fresh !== undefined) {
36974
- return fresh;
37281
+ function restartOwed(projectPath) {
37282
+ try {
37283
+ const parsed = JSON.parse(readFileSync16(restartNotePath(projectPath), "utf8"));
37284
+ if (typeof parsed !== "object" || parsed === null) {
37285
+ return null;
36975
37286
  }
36976
- const left = until - Date.now();
36977
- if (left <= 0 || waiting.giveUp?.() === true) {
37287
+ const said = parsed;
37288
+ if (typeof said.quitAt !== "string" || typeof said.byPid !== "number" || typeof said.ports !== "object") {
36978
37289
  return null;
36979
37290
  }
36980
- await delay2(Math.min(LOOK_EVERY_MS, left));
37291
+ return said;
37292
+ } catch {
37293
+ return null;
36981
37294
  }
36982
37295
  }
36983
- function describe2(endpoint) {
36984
- return `pid ${endpoint.pid} on ${endpoint.address}:${endpoint.port} (${endpoint.project.name || "unnamed"} at ${endpoint.project.path})`;
36985
- }
36986
- function tooNew(unspoken) {
36987
- const named = unspoken.map((game) => `pid ${game.pid} speaks ${game.protocol} (${game.project.name || "unnamed"} at ${game.project.path})`).join("; ");
36988
- 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";
36989
- return `A game is running, in a protocol this server does not speak ${named}. This server speaks ${RUNTIME_PROTOCOL}, so ${half}.`;
36990
- }
36991
- function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
36992
- if (pid !== undefined) {
36993
- const named = endpoints.find((endpoint) => endpoint.pid === pid);
36994
- if (named !== undefined) {
36995
- if (projectPath !== undefined && resolve4(named.project.path) !== resolve4(projectPath)) {
36996
- return {
36997
- problem: `The game with pid ${pid} is running ${named.project.path}, not ${resolve4(projectPath)}. Running: ${endpoints.map(describe2).join("; ")}.`
36998
- };
36999
- }
37000
- return { endpoint: named };
37001
- }
37002
- const tooNewToo = unspoken.find((game) => game.pid === pid);
37003
- if (tooNewToo !== undefined) {
37004
- return { problem: tooNew([tooNewToo]) };
37005
- }
37006
- return {
37007
- 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.`
37008
- };
37009
- }
37010
- if (endpoints.length === 0) {
37011
- if (unspoken.length > 0) {
37012
- return { problem: tooNew(unspoken) };
37013
- }
37014
- const went = announcedAndGone(projectPath);
37015
- const first = went[0];
37016
- if (first !== undefined) {
37017
- const ago = Math.max(1, Math.round((Date.now() - first.noticedAt) / 1000));
37018
- const others = went.length - 1;
37019
- const earlier = others === 0 ? "" : ` ${others} earlier ${others === 1 ? "one" : "ones"} went the same way.`;
37020
- return {
37021
- problem: `A game with the runtime addon announced itself and its process is gone: pid ${first.pid}` + `${first.project === "" ? "" : ` for ${first.project}`}, noticed ${ago}s ago.${earlier}` + " It quit or was ended rather than never starting, so editor_output has what it printed" + " on the way, including whatever it broke on. Start another with editor_run once you have read it."
37022
- };
37023
- }
37024
- return {
37025
- problem: "No game with the runtime addon is running. Start one with editor_run, or play the project from the editor with the addon enabled."
37026
- };
37027
- }
37028
- if (projectPath !== undefined) {
37029
- const wanted = resolve4(projectPath);
37030
- const matching = endpoints.filter((endpoint) => resolve4(endpoint.project.path) === wanted);
37031
- if (matching.length === 0) {
37032
- return {
37033
- problem: `No running game is from ${wanted}. Running: ${endpoints.map(describe2).join("; ")}.`
37034
- };
37035
- }
37036
- if (matching.length === 1 && matching[0]) {
37037
- return { endpoint: matching[0] };
37038
- }
37039
- return {
37040
- problem: `Several games are running from ${wanted}: ${matching.map(describe2).join("; ")}. Pass pid to choose one.`
37041
- };
37042
- }
37043
- if (endpoints.length === 1 && endpoints[0]) {
37044
- return { endpoint: endpoints[0] };
37296
+ function restartSettled(projectPath) {
37297
+ try {
37298
+ rmSync8(restartNotePath(projectPath), { force: true });
37299
+ } catch (error) {
37300
+ console.error(`[SERVER] Could not clear the restart note: ${errorMessage(error)}`);
37045
37301
  }
37046
- return {
37047
- problem: `Several games are running: ${endpoints.map(describe2).join("; ")}. Pass projectPath to choose one, or pid when they are from one project.`
37048
- };
37049
- }
37050
- function runtimeRequest(endpoint, command, params, timeoutMs) {
37051
- const id = nextRequestId++;
37052
- return new Promise((settle) => {
37053
- let done = false;
37054
- let buffered = "";
37055
- let welcomed = false;
37056
- const socket = createConnection3({ port: endpoint.port, host: endpoint.address });
37057
- const finish = (reply) => {
37058
- if (done) {
37059
- return;
37060
- }
37061
- done = true;
37062
- clearTimeout(timer);
37063
- socket.destroy();
37064
- settle(reply);
37065
- };
37066
- const timer = setTimeout(() => {
37067
- finish({
37068
- ok: false,
37069
- reason: "busy",
37070
- message: `The game (${describe2(endpoint)}) accepted the connection but did not answer '${command}' within ${timeoutMs}ms. It may be paused at a breakpoint or stuck in a long frame.`
37071
- });
37072
- }, timeoutMs);
37073
- socket.setEncoding("utf8");
37074
- socket.on("connect", () => {
37075
- socket.write(`${JSON.stringify({ id, command, params })}
37076
- `);
37077
- });
37078
- socket.on("data", (chunk) => {
37079
- buffered += chunk;
37080
- let newline = buffered.indexOf(`
37081
- `);
37082
- while (newline !== -1 && !done) {
37083
- const line = buffered.slice(0, newline).trim();
37084
- buffered = buffered.slice(newline + 1);
37085
- newline = buffered.indexOf(`
37086
- `);
37087
- if (line === "") {
37088
- continue;
37089
- }
37090
- let message;
37091
- try {
37092
- message = asParams(JSON.parse(line));
37093
- } catch {
37094
- continue;
37095
- }
37096
- if (!welcomed) {
37097
- welcomed = true;
37098
- if (readString(message, "type") === "welcome") {
37099
- const spoken = readNumber(message, "protocol");
37100
- if (spoken !== RUNTIME_PROTOCOL) {
37101
- finish({
37102
- ok: false,
37103
- reason: "protocol",
37104
- message: `The game (${describe2(endpoint)}) speaks runtime protocol ${spoken ?? "unknown"} and this server speaks ${RUNTIME_PROTOCOL}. Reinstall the runtime addon into the project.`
37105
- });
37106
- return;
37107
- }
37108
- continue;
37109
- }
37110
- }
37111
- if (message["id"] !== id) {
37112
- continue;
37113
- }
37114
- if (readString(message, "type") === "error") {
37115
- finish({ ok: false, reason: "error", message: readString(message, "message") ?? "unknown error" });
37116
- return;
37117
- }
37118
- finish({ ok: true, payload: message, endpoint });
37119
- }
37120
- });
37121
- socket.on("error", (error) => {
37122
- finish({
37123
- ok: false,
37124
- reason: "refused",
37125
- message: `The game (${describe2(endpoint)}) is running but nothing answered on its port: ${error.code ?? error.message}. It may still be starting, or be shutting down.`
37126
- });
37127
- });
37128
- socket.on("close", () => {
37129
- finish({
37130
- ok: false,
37131
- reason: "refused",
37132
- message: `The game (${describe2(endpoint)}) closed the connection before answering '${command}'.`
37133
- });
37134
- });
37135
- });
37136
37302
  }
37137
- var RUNTIME_PROTOCOL = 2, ANNOUNCEMENT_PATTERN, ERROR_REPORT_PATTERN, ERROR_REPORT_KEEP_MS, GONE_WINDOW_MS = 120000, GONE_KEPT = 32, gone, LOOK_EVERY_MS = 50, ANNOUNCE_BUDGET_MS = 5000, nextRequestId = 1;
37138
- var init_runtime_client = __esm(() => {
37139
- ANNOUNCEMENT_PATTERN = /^runtime-(\d+)\.json$/;
37140
- ERROR_REPORT_PATTERN = /^runtime-(\d+)\.log$/;
37141
- ERROR_REPORT_KEEP_MS = 60 * 60 * 1000;
37142
- gone = [];
37303
+ var init_restart_note = __esm(() => {
37304
+ init_errors();
37143
37305
  });
37144
37306
 
37145
37307
  // src/run-record.ts
37146
- import { execFileSync } from "node:child_process";
37147
- import { mkdirSync as mkdirSync9, openSync, readdirSync as readdirSync6, readFileSync as readFileSync16, rmSync as rmSync9, statSync as statSync4, writeFileSync as writeFileSync11 } from "node:fs";
37148
- import { basename as basename3, join as join18 } from "node:path";
37308
+ import { execFileSync as execFileSync2 } from "node:child_process";
37309
+ import { mkdirSync as mkdirSync10, openSync, readdirSync as readdirSync6, readFileSync as readFileSync17, rmSync as rmSync9, statSync as statSync4, writeFileSync as writeFileSync12 } from "node:fs";
37310
+ import { basename as basename3, join as join19 } from "node:path";
37149
37311
  function runsDirectory() {
37150
- return join18(runtimeDirectory(), "runs");
37312
+ return join19(runtimeDirectory(), "runs");
37151
37313
  }
37152
37314
  function recordPath() {
37153
- return join18(runsDirectory(), "run.json");
37315
+ return join19(runsDirectory(), "run.json");
37154
37316
  }
37155
37317
  function recordPaths() {
37156
- return runtimeDirectories().map((directory) => join18(directory, "runs", "run.json"));
37318
+ return runtimeDirectories().map((directory) => join19(directory, "runs", "run.json"));
37157
37319
  }
37158
37320
  function openTranscript(startedAt) {
37159
- mkdirSync9(runsDirectory(), { recursive: true });
37160
- const path = join18(runsDirectory(), `run-${startedAt}.log`);
37321
+ mkdirSync10(runsDirectory(), { recursive: true });
37322
+ const path = join19(runsDirectory(), `run-${startedAt}.log`);
37161
37323
  return { path, fd: openSync(path, "a") };
37162
37324
  }
37163
37325
  function writeRunRecord(record) {
37164
- mkdirSync9(runsDirectory(), { recursive: true });
37165
- writeFileSync11(recordPath(), JSON.stringify(record, null, 2), "utf8");
37326
+ mkdirSync10(runsDirectory(), { recursive: true });
37327
+ writeFileSync12(recordPath(), JSON.stringify(record, null, 2), "utf8");
37166
37328
  }
37167
37329
  function editorNotePath() {
37168
- return join18(runsDirectory(), "editor-run.json");
37330
+ return join19(runsDirectory(), "editor-run.json");
37169
37331
  }
37170
37332
  function writeEditorRunNote(note) {
37171
- mkdirSync9(runsDirectory(), { recursive: true });
37172
- writeFileSync11(editorNotePath(), JSON.stringify(note, null, 2), "utf8");
37333
+ mkdirSync10(runsDirectory(), { recursive: true });
37334
+ writeFileSync12(editorNotePath(), JSON.stringify(note, null, 2), "utf8");
37173
37335
  }
37174
37336
  function readEditorRunNote() {
37175
37337
  for (const directory of runtimeDirectories()) {
37176
- const note = editorNoteAt(join18(directory, "runs", "editor-run.json"));
37338
+ const note = editorNoteAt(join19(directory, "runs", "editor-run.json"));
37177
37339
  if (note !== null) {
37178
37340
  return note;
37179
37341
  }
@@ -37183,7 +37345,7 @@ function readEditorRunNote() {
37183
37345
  function editorNoteAt(path) {
37184
37346
  let parsed;
37185
37347
  try {
37186
- parsed = JSON.parse(readFileSync16(path, "utf8"));
37348
+ parsed = JSON.parse(readFileSync17(path, "utf8"));
37187
37349
  } catch {
37188
37350
  return null;
37189
37351
  }
@@ -37206,7 +37368,7 @@ function recordRunEnded(pid, exitCode) {
37206
37368
  return;
37207
37369
  }
37208
37370
  try {
37209
- writeFileSync11(path, JSON.stringify({ ...record, exitCode }, null, 2), "utf8");
37371
+ writeFileSync12(path, JSON.stringify({ ...record, exitCode }, null, 2), "utf8");
37210
37372
  } catch {}
37211
37373
  }
37212
37374
  function readRunRecord() {
@@ -37221,7 +37383,7 @@ function readRunRecord() {
37221
37383
  function recordAt(path) {
37222
37384
  let parsed;
37223
37385
  try {
37224
- parsed = JSON.parse(readFileSync16(path, "utf8"));
37386
+ parsed = JSON.parse(readFileSync17(path, "utf8"));
37225
37387
  } catch {
37226
37388
  return null;
37227
37389
  }
@@ -37266,10 +37428,10 @@ function runningAs(pid) {
37266
37428
  const began = startedAt(pid, null);
37267
37429
  const when = began === null ? {} : { startedAt: began };
37268
37430
  if (process.platform === "linux") {
37269
- const raw = readFileSync16(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
37431
+ const raw = readFileSync17(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
37270
37432
  return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
37271
37433
  }
37272
- const args = execFileSync("ps", ["-p", String(pid), "-o", "args="], {
37434
+ const args = execFileSync2("ps", ["-p", String(pid), "-o", "args="], {
37273
37435
  encoding: "utf8",
37274
37436
  timeout: 15000
37275
37437
  }).trim();
@@ -37281,20 +37443,14 @@ function runningAs(pid) {
37281
37443
  function listeningPid(port) {
37282
37444
  try {
37283
37445
  if (process.platform === "win32") {
37284
- const said = execFileSync("powershell", [
37285
- "-NoProfile",
37286
- "-NonInteractive",
37287
- "-Command",
37288
- `(Get-NetTCPConnection -State Listen -LocalPort ${port} -ErrorAction SilentlyContinue | Select-Object -First 1).OwningProcess`
37289
- ], { encoding: "utf8", timeout: 15000 }).trim();
37290
- return /^\d+$/.test(said) ? Number(said) : null;
37446
+ return listeningPidInNetstat(execFileSync2("netstat", ["-ano"], { encoding: "utf8", timeout: 15000 }), port);
37291
37447
  }
37292
37448
  for (const [command, args, pattern] of [
37293
37449
  ["lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], /^(\d+)/],
37294
37450
  ["ss", ["-ltnpH", `sport = :${port}`], /pid=(\d+)/]
37295
37451
  ]) {
37296
37452
  try {
37297
- const said = execFileSync(command, [...args], { encoding: "utf8", timeout: 15000 }).trim();
37453
+ const said = execFileSync2(command, [...args], { encoding: "utf8", timeout: 15000 }).trim();
37298
37454
  const found = pattern.exec(said);
37299
37455
  if (found?.[1] !== undefined) {
37300
37456
  return Number(found[1]);
@@ -37306,9 +37462,22 @@ function listeningPid(port) {
37306
37462
  return null;
37307
37463
  }
37308
37464
  }
37465
+ function listeningPidInNetstat(printed, port) {
37466
+ for (const line of printed.split(/\r?\n/)) {
37467
+ const fields = /^\s*TCP\s+(\S+)\s+\S+\s+LISTENING\s+(\d+)\s*$/i.exec(line);
37468
+ if (fields === null) {
37469
+ continue;
37470
+ }
37471
+ const local = fields[1] ?? "";
37472
+ if (Number(local.slice(local.lastIndexOf(":") + 1)) === port) {
37473
+ return Number(fields[2]);
37474
+ }
37475
+ }
37476
+ return null;
37477
+ }
37309
37478
  function windowsCommandLine(pid) {
37310
37479
  try {
37311
- const answer = execFileSync("powershell", [
37480
+ const answer = execFileSync2("powershell", [
37312
37481
  "-NoProfile",
37313
37482
  "-NonInteractive",
37314
37483
  "-Command",
@@ -37326,7 +37495,7 @@ function startedAt(pid, windowsAnswer) {
37326
37495
  return Number.isNaN(when) ? null : when;
37327
37496
  }
37328
37497
  try {
37329
- const said = execFileSync("ps", ["-p", String(pid), "-o", "lstart="], {
37498
+ const said = execFileSync2("ps", ["-p", String(pid), "-o", "lstart="], {
37330
37499
  encoding: "utf8",
37331
37500
  timeout: 15000
37332
37501
  }).trim();
@@ -37338,7 +37507,7 @@ function startedAt(pid, windowsAnswer) {
37338
37507
  }
37339
37508
  function windowsImage(pid) {
37340
37509
  try {
37341
- const csv = execFileSync("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
37510
+ const csv = execFileSync2("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
37342
37511
  encoding: "utf8",
37343
37512
  timeout: 15000,
37344
37513
  windowsHide: true
@@ -37410,7 +37579,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
37410
37579
  if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
37411
37580
  continue;
37412
37581
  }
37413
- const path = join18(directory, entry);
37582
+ const path = join19(directory, entry);
37414
37583
  if (path === keep || ageMs(path, now) < keepMs) {
37415
37584
  continue;
37416
37585
  }
@@ -37433,15 +37602,16 @@ import {
37433
37602
  closeSync,
37434
37603
  existsSync as existsSync14,
37435
37604
  fstatSync,
37436
- mkdirSync as mkdirSync10,
37605
+ mkdirSync as mkdirSync11,
37437
37606
  mkdtempSync as mkdtempSync3,
37438
37607
  openSync as openSync2,
37439
37608
  readdirSync as readdirSync7,
37440
- readFileSync as readFileSync17,
37441
- readSync
37609
+ readFileSync as readFileSync18,
37610
+ readSync,
37611
+ statSync as statSync5
37442
37612
  } from "node:fs";
37443
37613
  import { tmpdir as tmpdir5 } from "node:os";
37444
- import { basename as basename4, dirname as dirname11, join as join19, normalize as normalize3, relative as relative3 } from "node:path";
37614
+ import { basename as basename4, dirname as dirname13, join as join20, normalize as normalize3, relative as relative3 } from "node:path";
37445
37615
  import { setTimeout as delay3 } from "node:timers/promises";
37446
37616
  import { fileURLToPath as fileURLToPath4 } from "node:url";
37447
37617
  import { promisify as promisify7 } from "node:util";
@@ -37564,10 +37734,11 @@ function runtimeVerdict(endpoint, after) {
37564
37734
  };
37565
37735
  }
37566
37736
  const state = after.playingSeen === false ? "the editor has not yet reported the game as playing, which a restarted editor does not until its scan is over" : "the game is still running";
37737
+ const sized = after.sizedToMs === undefined ? "" : `, half as long again as the ${after.sizedToMs}ms its last game took,`;
37567
37738
  return {
37568
37739
  listening: false,
37569
37740
  mayYetAnnounce: after.running,
37570
- note: after.running ? `nothing announced itself within ${after.budgetMs}ms and ${state}, so it may announce a moment from now: editor_status says whether it has, and editor_run start takes runtimeWaitMs to wait longer than this${after.withArgs ? ", which a run carrying its own arguments may well need, since whatever they ask the game to do before its first frame is inside this wait" : ""}` : `nothing announced itself within ${after.budgetMs}ms and the game is no longer running, so nothing is going to: editor_output has what it printed on the way down`,
37741
+ note: after.running ? `nothing announced itself within ${after.budgetMs}ms${sized} and ${state}, so it may announce a moment from now: editor_status says whether it has, and editor_run start takes runtimeWaitMs to wait longer than this${after.withArgs ? ", which a run carrying its own arguments may well need, since whatever they ask the game to do before its first frame is inside this wait" : ""}` : `nothing announced itself within ${after.budgetMs}ms${sized} and the game is no longer running, so nothing is going to: editor_output has what it printed on the way down`,
37571
37742
  heldAt: null
37572
37743
  };
37573
37744
  }
@@ -37672,7 +37843,7 @@ function aboveTheRunner(entry) {
37672
37843
  }
37673
37844
  function elsewhereIn(projectPath, asked) {
37674
37845
  const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
37675
- if (wanted === "" || existsSync14(join19(projectPath, wanted))) {
37846
+ if (wanted === "" || existsSync14(join20(projectPath, wanted))) {
37676
37847
  return "";
37677
37848
  }
37678
37849
  const root = (wanted.split("/")[0] ?? "").toLowerCase();
@@ -37686,7 +37857,7 @@ function elsewhereIn(projectPath, asked) {
37686
37857
  return near.length === 0 ? "" : ` The project does have ${there}.`;
37687
37858
  }
37688
37859
  function hasMainScene(projectFile) {
37689
- const scene = parseProjectGodot(readFileSync17(projectFile, "utf8"))["application"]?.["run/main_scene"];
37860
+ const scene = parseProjectGodot(readFileSync18(projectFile, "utf8"))["application"]?.["run/main_scene"];
37690
37861
  return typeof scene === "string" && scene !== "";
37691
37862
  }
37692
37863
  function runtimeAddonAnnouncesEditors(projectPath) {
@@ -37694,13 +37865,13 @@ function runtimeAddonAnnouncesEditors(projectPath) {
37694
37865
  return false;
37695
37866
  }
37696
37867
  try {
37697
- return readFileSync17(join19(projectPath, RUNTIME_AUTOLOAD2.path), "utf8").includes('"editor_pid"');
37868
+ return readFileSync18(join20(projectPath, RUNTIME_AUTOLOAD2.path), "utf8").includes('"editor_pid"');
37698
37869
  } catch {
37699
37870
  return false;
37700
37871
  }
37701
37872
  }
37702
37873
  function editorPlaysHeadless(projectFile) {
37703
- const runArgs = parseProjectGodot(readFileSync17(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
37874
+ const runArgs = parseProjectGodot(readFileSync18(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
37704
37875
  return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
37705
37876
  }
37706
37877
  function camelCased(params) {
@@ -37716,7 +37887,7 @@ function camelCased(params) {
37716
37887
  class GodotServer {
37717
37888
  mcp;
37718
37889
  locator = new GodotLocator2;
37719
- operationsScript = join19(__dirname2, "godot", "operations", "godot_operations.gd");
37890
+ operationsScript = join20(__dirname2, "godot", "operations", "godot_operations.gd");
37720
37891
  godotBridge;
37721
37892
  tools = buildToolDefinitions();
37722
37893
  activeProcess = null;
@@ -38374,7 +38545,7 @@ class GodotServer {
38374
38545
  if (path === undefined) {
38375
38546
  return { ok: false, response: this.createErrorResponse("projectPath is required.") };
38376
38547
  }
38377
- const file = join19(path, "project.godot");
38548
+ const file = join20(path, "project.godot");
38378
38549
  if (!existsSync14(file)) {
38379
38550
  return {
38380
38551
  ok: false,
@@ -38598,7 +38769,7 @@ class GodotServer {
38598
38769
  if (!engine.ok) {
38599
38770
  return engine.response;
38600
38771
  }
38601
- const application = parseProjectGodot(readFileSync17(project.value.file, "utf8"))["application"] ?? {};
38772
+ const application = parseProjectGodot(readFileSync18(project.value.file, "utf8"))["application"] ?? {};
38602
38773
  const name = application["config/name"];
38603
38774
  const mainScene = application["run/main_scene"];
38604
38775
  const info = {
@@ -38659,18 +38830,18 @@ class GodotServer {
38659
38830
  if (!engine.ok) {
38660
38831
  return engine.response;
38661
38832
  }
38662
- const folder = dirname11(output.absolutePath);
38833
+ const folder = dirname13(output.absolutePath);
38663
38834
  try {
38664
- mkdirSync10(folder, { recursive: true });
38835
+ mkdirSync11(folder, { recursive: true });
38665
38836
  } catch (error) {
38666
- return this.createErrorResponse(`Cannot create ${dirname11(output.relativePath)} to export into: ${errorMessage(error)}`);
38837
+ return this.createErrorResponse(`Cannot create ${dirname13(output.relativePath)} to export into: ${errorMessage(error)}`);
38667
38838
  }
38668
38839
  const debug = readBoolean(args, "debug") ?? false;
38669
- const exportLogs = mkdtempSync3(join19(tmpdir5(), "gdharness-export-"));
38840
+ const exportLogs = mkdtempSync3(join20(tmpdir5(), "gdharness-export-"));
38670
38841
  const exportArgs = [
38671
38842
  "--headless",
38672
38843
  "--log-file",
38673
- join19(exportLogs, "engine.log"),
38844
+ join20(exportLogs, "engine.log"),
38674
38845
  "--path",
38675
38846
  project.value.path,
38676
38847
  debug ? "--export-debug" : "--export-release",
@@ -38728,7 +38899,7 @@ class GodotServer {
38728
38899
  return contained.response;
38729
38900
  }
38730
38901
  const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
38731
- if (!existsSync14(join19(project.value.path, runner))) {
38902
+ if (!existsSync14(join20(project.value.path, runner))) {
38732
38903
  return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
38733
38904
  "Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
38734
38905
  ]);
@@ -38763,7 +38934,7 @@ class GodotServer {
38763
38934
  ];
38764
38935
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
38765
38936
  this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
38766
- const userData = mkdtempSync3(join19(tmpdir5(), "gdharness-tests-"));
38937
+ const userData = mkdtempSync3(join20(tmpdir5(), "gdharness-tests-"));
38767
38938
  const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
38768
38939
  const hung = await new Promise((resolve) => {
38769
38940
  const timer = setTimeout(() => {
@@ -38779,14 +38950,14 @@ class GodotServer {
38779
38950
  resolve(false);
38780
38951
  });
38781
38952
  });
38782
- const reportsDir = join19(project.value.path, ".godot", ...ours.split("/"));
38953
+ const reportsDir = join20(project.value.path, ".godot", ...ours.split("/"));
38783
38954
  let report = null;
38784
38955
  let reportProblem = null;
38785
38956
  try {
38786
38957
  const written = existsSync14(reportsDir) ? readdirSync7(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
38787
38958
  const newest = written.at(-1);
38788
38959
  if (newest !== undefined) {
38789
- report = parseJUnit(readFileSync17(join19(reportsDir, newest, "results.xml"), "utf8"));
38960
+ report = parseJUnit(readFileSync18(join20(reportsDir, newest, "results.xml"), "utf8"));
38790
38961
  }
38791
38962
  } catch (error) {
38792
38963
  reportProblem = errorMessage(error);
@@ -38926,7 +39097,7 @@ class GodotServer {
38926
39097
  return null;
38927
39098
  }
38928
39099
  try {
38929
- return readFileSync17(contained.absolutePath, "utf8");
39100
+ return readFileSync18(contained.absolutePath, "utf8");
38930
39101
  } catch {
38931
39102
  return null;
38932
39103
  }
@@ -38968,7 +39139,7 @@ class GodotServer {
38968
39139
  return null;
38969
39140
  }
38970
39141
  try {
38971
- return readFileSync17(contained.absolutePath, "utf8");
39142
+ return readFileSync18(contained.absolutePath, "utf8");
38972
39143
  } catch {
38973
39144
  return null;
38974
39145
  }
@@ -39117,11 +39288,14 @@ class GodotServer {
39117
39288
  return null;
39118
39289
  }
39119
39290
  try {
39120
- const answer = await Promise.race([
39121
- this.godotBridge.invokeTool("playing_status", {}),
39122
- delay3(PLAYING_STATUS_MS).then(() => null)
39123
- ]);
39291
+ const askedAt = Date.now();
39292
+ const asked = this.godotBridge.invokeTool("playing_status", {});
39293
+ const answer = await Promise.race([asked, delay3(PLAYING_STATUS_MS).then(() => null)]);
39124
39294
  if (answer === null) {
39295
+ this.logDebug(`playing_status did not answer within ${PLAYING_STATUS_MS}ms`);
39296
+ asked.then(() => {
39297
+ this.logDebug(`playing_status answered after ${Date.now() - askedAt}ms`);
39298
+ }).catch(() => {});
39125
39299
  return null;
39126
39300
  }
39127
39301
  return {
@@ -39194,7 +39368,7 @@ class GodotServer {
39194
39368
  return null;
39195
39369
  }
39196
39370
  try {
39197
- return settingKeys(readFileSync17(join19(projectPath, "project.godot"), "utf8"));
39371
+ return settingKeys(readFileSync18(join20(projectPath, "project.godot"), "utf8"));
39198
39372
  } catch {
39199
39373
  return null;
39200
39374
  }
@@ -39401,7 +39575,7 @@ class GodotServer {
39401
39575
  platform: process.platform,
39402
39576
  variables: process.env
39403
39577
  });
39404
- const runtimeWaitMs = readNonNegativeNumber(args, "runtimeWaitMs") ?? ANNOUNCE_BUDGET_MS;
39578
+ const runtimeWaitMs = this.announceBudget(args, project.value.path);
39405
39579
  const editorWouldPlay = !headless || editorPlaysHeadless(project.value.file);
39406
39580
  if (this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0) {
39407
39581
  return await this.playThroughEditor(sceneArgument, refreshed.value, project.value.path, alreadyPlaying, runtimeWaitMs, ended);
@@ -39434,13 +39608,17 @@ class GodotServer {
39434
39608
  message: `Use editor_output for what it prints and editor_run stop to end it.${spawnedInstead}${endedForThis}${alsoRunning}`
39435
39609
  });
39436
39610
  }
39437
- async runtimeUp(projectPath, before, budgetMs, withArgs) {
39438
- if (!existsSync14(join19(projectPath, RUNTIME_AUTOLOAD2.path))) {
39611
+ async runtimeUp(projectPath, before, budget, withArgs) {
39612
+ const budgetMs = budget.ms;
39613
+ if (!existsSync14(join20(projectPath, RUNTIME_AUTOLOAD2.path))) {
39439
39614
  return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
39440
39615
  }
39441
39616
  let asking = null;
39442
39617
  let editorAskedAt = 0;
39443
39618
  let editorSaysGoing = true;
39619
+ let editorAsks = 0;
39620
+ const waitBegan = Date.now();
39621
+ const lastSaid = () => editorSaysGoing ? "playing" : "not playing";
39444
39622
  const endpoint = await announcedSince(projectPath, before, {
39445
39623
  budgetMs,
39446
39624
  accept: (one) => this.isTheGameOf(this.currentRun(), one, projectPath),
@@ -39454,6 +39632,7 @@ class GodotServer {
39454
39632
  }
39455
39633
  if (asking === null && Date.now() - editorAskedAt >= RUN_POLL_MS) {
39456
39634
  editorAskedAt = Date.now();
39635
+ editorAsks += 1;
39457
39636
  asking = this.runStillGoing(run).then((going) => {
39458
39637
  editorSaysGoing = going;
39459
39638
  }).finally(() => {
@@ -39463,19 +39642,46 @@ class GodotServer {
39463
39642
  return !editorSaysGoing;
39464
39643
  }
39465
39644
  });
39645
+ this.logDebug(`announce wait ended after ${Date.now() - waitBegan}ms: ${endpoint === null ? "nothing announced" : `pid ${endpoint.pid} announced`}, editor asked ${editorAsks} times, last said ${lastSaid()}`);
39466
39646
  const going = this.currentRun();
39467
39647
  if (endpoint !== null && going !== null && going.announcedPid === undefined) {
39468
39648
  going.announcedPid = endpoint.pid;
39649
+ this.noteBootOf(going, endpoint);
39469
39650
  }
39470
39651
  return runtimeVerdict(endpoint, {
39471
39652
  addon: true,
39472
39653
  budgetMs,
39654
+ ...budget.sizedToMs === undefined ? {} : { sizedToMs: budget.sizedToMs },
39473
39655
  heldAt: this.dapClient?.whereItStopped() ?? null,
39474
39656
  running: endpoint !== null || going !== null && await this.runStillGoing(going),
39475
39657
  withArgs,
39476
39658
  ...going?.throughEditor === true ? { playingSeen: going.seenPlaying === true } : {}
39477
39659
  });
39478
39660
  }
39661
+ announceBudget(args, projectPath) {
39662
+ const asked = readNonNegativeNumber(args, "runtimeWaitMs");
39663
+ if (asked !== undefined) {
39664
+ return { ms: asked };
39665
+ }
39666
+ const lastBoot = readBootNote(projectPath);
39667
+ const ms = waitSizedTo(lastBoot);
39668
+ return lastBoot !== null && ms > ANNOUNCE_BUDGET_MS ? { ms, sizedToMs: lastBoot } : { ms };
39669
+ }
39670
+ noteBootOf(run, endpoint) {
39671
+ if (run.projectPath === null) {
39672
+ return;
39673
+ }
39674
+ let announcedAt;
39675
+ try {
39676
+ announcedAt = statSync5(endpoint.file).mtimeMs;
39677
+ } catch {
39678
+ return;
39679
+ }
39680
+ const took = announcedAt - run.startedAt;
39681
+ if (took > 0 && took <= LONGEST_BOOT_NOTED_MS) {
39682
+ writeBootNote(run.projectPath, took);
39683
+ }
39684
+ }
39479
39685
  async playThroughEditor(scene, refreshedClasses, projectPath, alreadyPlaying, runtimeWaitMs, ended) {
39480
39686
  const log = new GameLog;
39481
39687
  try {
@@ -39805,6 +40011,7 @@ class GodotServer {
39805
40011
  const theOne = candidates.length === 1 ? candidates[0] : undefined;
39806
40012
  if (theOne !== undefined) {
39807
40013
  run.announcedPid = theOne.pid;
40014
+ this.noteBootOf(run, theOne);
39808
40015
  }
39809
40016
  return run.announcedPid;
39810
40017
  }
@@ -40139,11 +40346,19 @@ class GodotServer {
40139
40346
  this.drainEditorOutput(stopped);
40140
40347
  this.drainTranscript(stopped);
40141
40348
  const wasRunning = await this.runStillGoing(stopped);
40142
- const endedPid = stopped.pid ?? this.announcedPidOf(stopped) ?? null;
40349
+ const announcedPid = this.announcedPidOf(stopped) ?? null;
40350
+ const endedPid = stopped.pid ?? announcedPid;
40143
40351
  const children = readBoolean(args, "andChildren") === true && wasRunning ? await this.whatTheRunStarted(stopped) : null;
40144
40352
  this.logDebug("Stopping the running game");
40145
40353
  await this.endActiveGame("editor_run stop");
40146
40354
  const ended = children === null ? null : endChildrenAmong(children);
40355
+ if (wasRunning) {
40356
+ for (const pid of new Set([endedPid, announcedPid])) {
40357
+ if (pid !== null) {
40358
+ await announcementEnded(pid);
40359
+ }
40360
+ }
40361
+ }
40147
40362
  return this.jsonTextResponse({
40148
40363
  stopped: true,
40149
40364
  through: stopped.throughEditor ? "editor" : "gdharness",
@@ -40465,8 +40680,8 @@ class GodotServer {
40465
40680
  ]);
40466
40681
  }
40467
40682
  const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
40468
- const screenshotDir = expectsScreenshot ? mkdtempSync3(join19(tmpdir5(), "gdharness-runtime-screenshot-")) : null;
40469
- const screenshotPath = screenshotDir ? join19(screenshotDir, "capture.png") : null;
40683
+ const screenshotDir = expectsScreenshot ? mkdtempSync3(join20(tmpdir5(), "gdharness-runtime-screenshot-")) : null;
40684
+ const screenshotPath = screenshotDir ? join20(screenshotDir, "capture.png") : null;
40470
40685
  try {
40471
40686
  const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs);
40472
40687
  if (!reply.ok) {
@@ -40490,7 +40705,7 @@ class GodotServer {
40490
40705
  type: "text",
40491
40706
  text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}`
40492
40707
  },
40493
- { type: "image", data: readFileSync17(screenshotPath).toString("base64"), mimeType: "image/png" }
40708
+ { type: "image", data: readFileSync18(screenshotPath).toString("base64"), mimeType: "image/png" }
40494
40709
  ]
40495
40710
  };
40496
40711
  } finally {
@@ -40557,11 +40772,12 @@ async function runGodotServer() {
40557
40772
  const server = new GodotServer;
40558
40773
  await server.run();
40559
40774
  }
40560
- var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run7, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, CONSOLE_WRAPPER, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CACHE_WRITE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/";
40775
+ var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run7, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CACHE_WRITE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/";
40561
40776
  var init_server3 = __esm(() => {
40562
40777
  init_mcp();
40563
40778
  init_stdio2();
40564
40779
  init_types();
40780
+ init_boot_note();
40565
40781
  init_breakpoint_note();
40566
40782
  init_bridge_announce();
40567
40783
  init_class_cache();
@@ -40591,8 +40807,9 @@ var init_server3 = __esm(() => {
40591
40807
  init_tool_definitions();
40592
40808
  init_update_check();
40593
40809
  run7 = promisify7(execFile7);
40594
- __dirname2 = dirname11(fileURLToPath4(import.meta.url));
40810
+ __dirname2 = dirname13(fileURLToPath4(import.meta.url));
40595
40811
  CONSOLE_WRAPPER = /_console(\.exe)?$/i;
40812
+ LONGEST_BOOT_NOTED_MS = 5 * 60000;
40596
40813
  VALUE_NAMES = {
40597
40814
  string: "a string",
40598
40815
  number: "a number",
@@ -40631,7 +40848,7 @@ var init_server3 = __esm(() => {
40631
40848
 
40632
40849
  // src/cli.ts
40633
40850
  import { existsSync as existsSync15 } from "fs";
40634
- import { join as join20, resolve as resolve5 } from "path";
40851
+ import { join as join21, resolve as resolve5 } from "path";
40635
40852
 
40636
40853
  // src/errors.ts
40637
40854
  class Refusal extends Error {
@@ -42142,6 +42359,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
42142
42359
  | Where a control is, and whether it is visible | \`runtime_inspect\` \`find\`, \`rect\` |
42143
42360
  | What a property reads right now, on a node whose path you already hold | \`runtime_inspect\` \`property\`, \`runtime_invoke\` |
42144
42361
  | What a property on an object a node holds reads | the same two, with colons: \`_game:clock:speed\` |
42362
+ | Which control holds the focus, or anything only a method answers | the same path with a step written as a call, \`get_viewport():gui_get_focus_owner\`, which calls a method taking no arguments and walks into what it returned |
42145
42363
  | Press a button | \`runtime_input click\`, which says what was under the pointer |
42146
42364
  | Press the next thing after a click that changed screen | \`runtime_wait until\` with \`says\` and a word the new screen shows, then the click. A click sent straight after a screen change lands on a control the new screen is about to free, answers \`landed: true\`, and does nothing; a frame count is a number nobody can pick correctly across a cold start and a warm one, and waiting for the words answered in about 130ms where twenty frames had not been enough |
42147
42365
  | Fill in a field | \`runtime_input click\` on it, then \`runtime_input text\` with \`replace\`; a submitted field has to be clicked again |
@@ -42335,7 +42553,7 @@ class UsageError extends Refusal {
42335
42553
  function projectArgument(at) {
42336
42554
  const named = args.slice(at).find((value) => !value.startsWith("--"));
42337
42555
  const projectPath = resolve5(named ?? ".");
42338
- if (!existsSync15(join20(projectPath, "project.godot"))) {
42556
+ if (!existsSync15(join21(projectPath, "project.godot"))) {
42339
42557
  throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
42340
42558
  }
42341
42559
  return projectPath;
@@ -42374,7 +42592,7 @@ async function registerRuntime(godot, projectPath) {
42374
42592
  return;
42375
42593
  }
42376
42594
  console.log(`${RUNTIME_AUTOLOAD.name} autoload left at ${named}, which this project registers itself`);
42377
- if (!existsSync15(join20(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
42595
+ if (!existsSync15(join21(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
42378
42596
  console.log(` that file is not in this project, so it boots with a missing script: restore it, or point the entry at res://${RUNTIME_AUTOLOAD.path}`);
42379
42597
  }
42380
42598
  }