gdharness 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/build/cli.js +856 -785
  2. package/build/index.js +3734 -3667
  3. package/package.json +1 -1
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: {
@@ -31240,11 +31240,638 @@ var init_stdio2 = __esm(() => {
31240
31240
  init_stdio();
31241
31241
  });
31242
31242
 
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;
31285
+ }
31286
+ async function processTree() {
31287
+ try {
31288
+ if (process.platform === "win32") {
31289
+ return parseProcessTable(await askWindows(), true);
31290
+ }
31291
+ return parseProcessTable(await askPosix());
31292
+ } catch {
31293
+ return;
31294
+ }
31295
+ }
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;
31302
+ }
31303
+ const pid = Number(fields[1]);
31304
+ const parent = Number(fields[2]);
31305
+ const rest = (fields[3] ?? "").trim();
31306
+ if (pid <= 0) {
31307
+ continue;
31308
+ }
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 } : {} });
31319
+ }
31320
+ return tree;
31321
+ }
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;
31332
+ }
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;
31341
+ }
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;
31350
+ }
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
+ }
31357
+ }
31358
+ }
31359
+ return found;
31360
+ }
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;
31376
+ }
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(" ");
31391
+ }
31392
+ return {
31393
+ executable,
31394
+ projectPath,
31395
+ editor: words.some((word) => word === "-e" || word === "--editor")
31396
+ };
31397
+ }
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
+ }
31417
+ }
31418
+ if (started) {
31419
+ words.push(word);
31420
+ }
31421
+ return words;
31422
+ }
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);
31442
+ });
31443
+
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;
31454
+ }
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"));
31463
+ }
31464
+ return [...new Set(candidates.map((path) => resolve3(path)))];
31465
+ }
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
+ }
31472
+ }
31473
+ return null;
31474
+ }
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
+
31243
31870
  // src/breakpoint-note.ts
31244
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync10, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "node:fs";
31245
- import { dirname as dirname7, isAbsolute as isAbsolute2, join as join12, relative as relative2 } from "node:path";
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";
31246
31873
  function breakpointNotePath(projectPath) {
31247
- return join12(projectPath, ".godot", "gdharness-breakpoints.json");
31874
+ return join14(projectPath, ".godot", "gdharness-breakpoints.json");
31248
31875
  }
31249
31876
  function writeBreakpointNote(projectPath, held) {
31250
31877
  const path = breakpointNotePath(projectPath);
@@ -31261,8 +31888,8 @@ function writeBreakpointNote(projectPath, held) {
31261
31888
  lines: [...one.lines]
31262
31889
  }))
31263
31890
  };
31264
- mkdirSync6(dirname7(path), { recursive: true });
31265
- writeFileSync8(path, `${JSON.stringify(note, null, 2)}
31891
+ mkdirSync7(dirname9(path), { recursive: true });
31892
+ writeFileSync9(path, `${JSON.stringify(note, null, 2)}
31266
31893
  `, "utf8");
31267
31894
  } catch (error) {
31268
31895
  console.error(`[SERVER] Could not record the breakpoints at ${path}: ${errorMessage(error)}`);
@@ -31270,7 +31897,7 @@ function writeBreakpointNote(projectPath, held) {
31270
31897
  }
31271
31898
  function readBreakpointNote(projectPath) {
31272
31899
  try {
31273
- const parsed = JSON.parse(readFileSync10(breakpointNotePath(projectPath), "utf8"));
31900
+ const parsed = JSON.parse(readFileSync12(breakpointNotePath(projectPath), "utf8"));
31274
31901
  if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.breakpoints)) {
31275
31902
  return [];
31276
31903
  }
@@ -31285,7 +31912,7 @@ function readBreakpointNote(projectPath) {
31285
31912
  }
31286
31913
  const lines = Array.isArray(listed) ? listed.filter((line) => Number.isInteger(line) && line > 0) : [];
31287
31914
  if (lines.length > 0) {
31288
- held.push({ scriptPath: realPathOr(join12(projectPath, script)), lines });
31915
+ held.push({ scriptPath: realPathOr(join14(projectPath, script)), lines });
31289
31916
  }
31290
31917
  }
31291
31918
  return held;
@@ -31303,14 +31930,14 @@ var init_breakpoint_note = __esm(() => {
31303
31930
  });
31304
31931
 
31305
31932
  // src/bridge-announce.ts
31306
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync11, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "node:fs";
31307
- import { dirname as dirname8, join as join13 } from "node:path";
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";
31308
31935
  function announcementPath(projectPath) {
31309
- return join13(projectPath, ".godot", "gdharness-bridge.json");
31936
+ return join15(projectPath, ".godot", "gdharness-bridge.json");
31310
31937
  }
31311
31938
  function readAnnouncement(path) {
31312
31939
  try {
31313
- const parsed = JSON.parse(readFileSync11(path, "utf8"));
31940
+ const parsed = JSON.parse(readFileSync13(path, "utf8"));
31314
31941
  if (typeof parsed !== "object" || parsed === null) {
31315
31942
  return null;
31316
31943
  }
@@ -31329,7 +31956,7 @@ function announcementIsOurs(path) {
31329
31956
  function announceBridge(projectPath, bridge) {
31330
31957
  const path = announcementPath(projectPath);
31331
31958
  try {
31332
- mkdirSync7(dirname8(path), { recursive: true });
31959
+ mkdirSync8(dirname10(path), { recursive: true });
31333
31960
  const announcement = {
31334
31961
  protocol: BRIDGE_ANNOUNCE_PROTOCOL,
31335
31962
  host: bridge.host,
@@ -31338,7 +31965,7 @@ function announceBridge(projectPath, bridge) {
31338
31965
  version: bridge.version,
31339
31966
  startedAt: new Date().toISOString()
31340
31967
  };
31341
- writeFileSync9(path, `${JSON.stringify(announcement, null, 2)}
31968
+ writeFileSync10(path, `${JSON.stringify(announcement, null, 2)}
31342
31969
  `, "utf8");
31343
31970
  return path;
31344
31971
  } catch (error) {
@@ -31362,16 +31989,16 @@ var init_bridge_announce = __esm(() => {
31362
31989
  });
31363
31990
 
31364
31991
  // src/config-pin.ts
31365
- import { existsSync as existsSync10, readFileSync as readFileSync12 } from "node:fs";
31366
- import { join as join14 } from "node:path";
31992
+ import { existsSync as existsSync11, readFileSync as readFileSync14 } from "node:fs";
31993
+ import { join as join16 } from "node:path";
31367
31994
  function versionsNamedInConfig(projectPath) {
31368
- const config = join14(projectPath, ".mcp.json");
31369
- if (!existsSync10(config)) {
31995
+ const config = join16(projectPath, ".mcp.json");
31996
+ if (!existsSync11(config)) {
31370
31997
  return [];
31371
31998
  }
31372
31999
  let text;
31373
32000
  try {
31374
- text = readFileSync12(config, "utf8");
32001
+ text = readFileSync14(config, "utf8");
31375
32002
  JSON.parse(text);
31376
32003
  } catch {
31377
32004
  return [];
@@ -31518,8 +32145,8 @@ var init_ports = __esm(() => {
31518
32145
  });
31519
32146
 
31520
32147
  // src/dap_client.ts
31521
- import { createConnection } from "node:net";
31522
- 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";
31523
32150
  function said(body, field) {
31524
32151
  const value = body?.[field];
31525
32152
  return typeof value === "string" ? value : "";
@@ -31554,7 +32181,7 @@ class GodotDAPClient {
31554
32181
  return;
31555
32182
  }
31556
32183
  await new Promise((resolve, reject) => {
31557
- const socket = createConnection({ host: this.host, port: this.port });
32184
+ const socket = createConnection2({ host: this.host, port: this.port });
31558
32185
  this.socket = socket;
31559
32186
  const connectTimeout = setTimeout(() => {
31560
32187
  socket.destroy();
@@ -31963,7 +32590,7 @@ class GodotDAPClient {
31963
32590
  return answered;
31964
32591
  }
31965
32592
  } catch {}
31966
- await delay(200);
32593
+ await delay2(200);
31967
32594
  }
31968
32595
  return [];
31969
32596
  }
@@ -31984,7 +32611,7 @@ class GodotDAPClient {
31984
32611
  break;
31985
32612
  }
31986
32613
  } catch {}
31987
- await delay(200);
32614
+ await delay2(200);
31988
32615
  }
31989
32616
  const named = [];
31990
32617
  for (const scope of scopes) {
@@ -35500,10 +36127,10 @@ var init_godot_bridge = __esm(() => {
35500
36127
  });
35501
36128
 
35502
36129
  // src/godot-path.ts
35503
- import { execFile as execFile4 } from "node:child_process";
35504
- import { existsSync as existsSync11 } from "node:fs";
36130
+ import { execFile as execFile5 } from "node:child_process";
36131
+ import { existsSync as existsSync12 } from "node:fs";
35505
36132
  import { normalize as normalize2 } from "node:path";
35506
- import { promisify as promisify4 } from "node:util";
36133
+ import { promisify as promisify5 } from "node:util";
35507
36134
 
35508
36135
  class GodotLocator2 {
35509
36136
  answered = new Map;
@@ -35556,11 +36183,11 @@ class GodotLocator2 {
35556
36183
  return known;
35557
36184
  }
35558
36185
  let reason;
35559
- if (path !== "godot" && !existsSync11(path)) {
36186
+ if (path !== "godot" && !existsSync12(path)) {
35560
36187
  reason = "does not exist";
35561
36188
  } else {
35562
36189
  try {
35563
- await run4(path, ["--version"]);
36190
+ await run5(path, ["--version"]);
35564
36191
  reason = null;
35565
36192
  } catch (error) {
35566
36193
  reason = `does not answer --version: ${errorMessage(error)}`;
@@ -35570,11 +36197,11 @@ class GodotLocator2 {
35570
36197
  return reason;
35571
36198
  }
35572
36199
  }
35573
- var run4;
36200
+ var run5;
35574
36201
  var init_godot_path = __esm(() => {
35575
36202
  init_detection();
35576
36203
  init_errors();
35577
- run4 = promisify4(execFile4);
36204
+ run5 = promisify5(execFile5);
35578
36205
  });
35579
36206
 
35580
36207
  // src/issues.ts
@@ -35931,8 +36558,8 @@ var init_junit = __esm(() => {
35931
36558
  // src/lsp_client.ts
35932
36559
  import { realpathSync as realpathSync2 } from "node:fs";
35933
36560
  import { readFile, realpath } from "node:fs/promises";
35934
- import { createConnection as createConnection2 } from "node:net";
35935
- 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";
35936
36563
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
35937
36564
  function diagnosticsKey(uri) {
35938
36565
  let decoded;
@@ -35978,7 +36605,7 @@ class GodotLSPClient {
35978
36605
  }
35979
36606
  this.connectPromise = new Promise((resolveConnect, rejectConnect) => {
35980
36607
  let settled = false;
35981
- const socket = createConnection2({ port: this.port, host: this.host }, () => {
36608
+ const socket = createConnection3({ port: this.port, host: this.host }, () => {
35982
36609
  this.socket = socket;
35983
36610
  this.connected = true;
35984
36611
  this.reader = new FrameReader;
@@ -36060,7 +36687,7 @@ class GodotLSPClient {
36060
36687
  if (this.initialized) {
36061
36688
  return;
36062
36689
  }
36063
- const rootPath = this.rootPath ?? dirname9(resolve3(filePath));
36690
+ const rootPath = this.rootPath ?? dirname11(resolve4(filePath));
36064
36691
  await this.initialize(rootPath);
36065
36692
  }
36066
36693
  async sendRequest(method, params) {
@@ -36198,7 +36825,7 @@ class GodotLSPClient {
36198
36825
  });
36199
36826
  }
36200
36827
  toFileUri(filePath) {
36201
- return pathToFileURL(resolve3(filePath)).href;
36828
+ return pathToFileURL(resolve4(filePath)).href;
36202
36829
  }
36203
36830
  syncDocument(filePath, content) {
36204
36831
  const uri = this.toFileUri(filePath);
@@ -36239,7 +36866,7 @@ class GodotLSPClient {
36239
36866
  }
36240
36867
  async initialize(rootPath) {
36241
36868
  await this.ensureConnected();
36242
- const resolvedRootPath = resolve3(rootPath);
36869
+ const resolvedRootPath = resolve4(rootPath);
36243
36870
  const rootUri = pathToFileURL(resolvedRootPath).href;
36244
36871
  const result = await this.sendRequest("initialize", {
36245
36872
  processId: process.pid,
@@ -36382,7 +37009,7 @@ function normalizeLSPError(error, port) {
36382
37009
  return String(error);
36383
37010
  }
36384
37011
  async function resolveLSPPaths(projectPathValue, scriptPathValue) {
36385
- const requestedProjectPath = resolve3(projectPathValue);
37012
+ const requestedProjectPath = resolve4(projectPathValue);
36386
37013
  let projectPath;
36387
37014
  try {
36388
37015
  projectPath = await realpath(requestedProjectPath);
@@ -36432,238 +37059,37 @@ async function handleLSPTool(client, toolName, args) {
36432
37059
  if (!Number.isFinite(line) || !Number.isFinite(character)) {
36433
37060
  throw new Refusal2("Arguments line and character must be numbers.");
36434
37061
  }
36435
- const completions = await client.getCompletions(scriptPath, content, line, character);
36436
- return asToolResponse({ completions });
36437
- }
36438
- case "lsp_get_hover": {
36439
- const line = Number(parsedArgs["line"]);
36440
- const character = Number(parsedArgs["character"]);
36441
- if (!Number.isFinite(line) || !Number.isFinite(character)) {
36442
- throw new Refusal2("Arguments line and character must be numbers.");
36443
- }
36444
- const hover = await client.getHover(scriptPath, content, line, character);
36445
- return asToolResponse({ hover });
36446
- }
36447
- case "lsp_get_symbols": {
36448
- const symbols = await client.getDocumentSymbols(scriptPath, content);
36449
- return asToolResponse({ symbols });
36450
- }
36451
- default:
36452
- return asToolResponse({ error: `Unknown LSP tool: ${toolName}` });
36453
- }
36454
- } catch (error) {
36455
- return asToolResponse({
36456
- error: normalizeLSPError(error, client.port)
36457
- });
36458
- }
36459
- }
36460
- var DEFAULT_LSP_PORT = 6005, DIAGNOSTICS_TIMEOUT_MS = 5000;
36461
- var init_lsp_client = __esm(() => {
36462
- init_errors();
36463
- init_framing();
36464
- init_paths();
36465
- init_ports();
36466
- });
36467
-
36468
- // src/process-children.ts
36469
- import { execFile as execFile5, execFileSync, spawnSync as spawnSync2 } from "node:child_process";
36470
- import { basename as basename2 } from "node:path";
36471
- import { promisify as promisify5 } from "node:util";
36472
- function startTimesOf(pids) {
36473
- const began = new Map;
36474
- if (pids.length === 0) {
36475
- return began;
36476
- }
36477
- try {
36478
- if (process.platform === "win32") {
36479
- const filter = pids.map((pid) => `ProcessId=${pid}`).join(" OR ");
36480
- const said = execFileSync("powershell", [
36481
- "-NoProfile",
36482
- "-NonInteractive",
36483
- "-Command",
36484
- `Get-CimInstance Win32_Process -Filter "${filter}" | ForEach-Object { "$($_.ProcessId) $(([DateTimeOffset]$_.CreationDate).ToUnixTimeMilliseconds())" }`
36485
- ], { encoding: "utf8", timeout: ASK_TIMEOUT_MS, windowsHide: true });
36486
- for (const line of said.split(`
36487
- `)) {
36488
- const found = /^\s*(\d+)\s+(\d+)\s*$/.exec(line);
36489
- if (found?.[1] !== undefined && found[2] !== undefined) {
36490
- began.set(Number(found[1]), Number(found[2]));
36491
- }
36492
- }
36493
- return began;
36494
- }
36495
- const said = spawnSync2("ps", ["-p", pids.join(","), "-o", "pid=,lstart="], {
36496
- encoding: "utf8",
36497
- timeout: ASK_TIMEOUT_MS,
36498
- env: { ...process.env, LC_ALL: "C" }
36499
- }).stdout;
36500
- for (const line of said.split(`
36501
- `)) {
36502
- const found = /^\s*(\d+)\s+(.+)$/.exec(line);
36503
- const at = Date.parse(found?.[2]?.trim() ?? "");
36504
- if (found?.[1] !== undefined && !Number.isNaN(at)) {
36505
- began.set(Number(found[1]), at);
36506
- }
36507
- }
36508
- } catch {}
36509
- return began;
36510
- }
36511
- async function processTree() {
36512
- try {
36513
- if (process.platform === "win32") {
36514
- return parseProcessTable(await askWindows(), true);
36515
- }
36516
- return parseProcessTable(await askPosix());
36517
- } catch {
36518
- return;
36519
- }
36520
- }
36521
- function parseProcessTable(printed, started = false) {
36522
- const tree = new Map;
36523
- for (const line of printed.split(/\r?\n/)) {
36524
- const fields = /^\s*(\d+)\s+(\d+)(?:\s+(.*))?$/.exec(line);
36525
- if (fields === null) {
36526
- continue;
36527
- }
36528
- const pid = Number(fields[1]);
36529
- const parent = Number(fields[2]);
36530
- const rest = (fields[3] ?? "").trim();
36531
- if (pid <= 0) {
36532
- continue;
36533
- }
36534
- if (!started) {
36535
- tree.set(pid, { parent, command: rest });
36536
- continue;
36537
- }
36538
- const timed = /^(\d+)(?:\s+(.*))?$/.exec(rest);
36539
- if (timed === null) {
36540
- continue;
36541
- }
36542
- const began = Number(timed[1]);
36543
- tree.set(pid, { parent, command: (timed[2] ?? "").trim(), ...began > 0 ? { startedAt: began } : {} });
36544
- }
36545
- return tree;
36546
- }
36547
- function linked(tree, child, parent) {
36548
- const from = tree.get(child);
36549
- const to = tree.get(parent);
36550
- if (from === undefined || from.parent !== parent) {
36551
- return false;
36552
- }
36553
- if (from.startedAt === undefined || to?.startedAt === undefined) {
36554
- return true;
36555
- }
36556
- return from.startedAt >= to.startedAt;
36557
- }
36558
- function childrenIn(tree, pid) {
36559
- const children = [];
36560
- for (const child of tree.keys()) {
36561
- if (linked(tree, child, pid)) {
36562
- children.push(child);
36563
- }
36564
- }
36565
- return children;
36566
- }
36567
- function descendantsIn(tree, pid) {
36568
- const found = [];
36569
- const queue = [pid];
36570
- const seen = new Set([pid]);
36571
- while (queue.length > 0) {
36572
- const next = queue.shift();
36573
- if (next === undefined) {
36574
- break;
36575
- }
36576
- for (const child of childrenIn(tree, next)) {
36577
- if (!seen.has(child)) {
36578
- seen.add(child);
36579
- found.push(child);
36580
- queue.push(child);
36581
- }
36582
- }
36583
- }
36584
- return found;
36585
- }
36586
- function ancestorsIn(tree, pid, root) {
36587
- const chain = [];
36588
- const seen = new Set([pid]);
36589
- let child = pid;
36590
- let parent = tree.get(pid)?.parent;
36591
- while (parent !== undefined && !seen.has(parent) && linked(tree, child, parent)) {
36592
- chain.push(parent);
36593
- if (parent === root) {
36594
- break;
36595
- }
36596
- seen.add(parent);
36597
- child = parent;
36598
- parent = tree.get(parent)?.parent;
36599
- }
36600
- return chain;
36601
- }
36602
- function readCommandLine(command) {
36603
- const words = commandWords(command);
36604
- const executable = words[0] === undefined ? "" : basename2(words[0].replaceAll("\\", "/"));
36605
- const at = words.indexOf("--path");
36606
- let projectPath = null;
36607
- if (at !== -1 && at + 1 < words.length) {
36608
- const taken = [];
36609
- for (const word of words.slice(at + 1)) {
36610
- if (taken.length > 0 && (word.startsWith("-") || /^[a-z]+:\/\//.test(word))) {
36611
- break;
36612
- }
36613
- taken.push(word);
36614
- }
36615
- projectPath = taken.join(" ");
36616
- }
36617
- return {
36618
- executable,
36619
- projectPath,
36620
- editor: words.some((word) => word === "-e" || word === "--editor")
36621
- };
36622
- }
36623
- function commandWords(command) {
36624
- const words = [];
36625
- let word = "";
36626
- let quoted = false;
36627
- let started = false;
36628
- for (const char of command) {
36629
- if (char === '"') {
36630
- quoted = !quoted;
36631
- started = true;
36632
- } else if (!quoted && /\s/.test(char)) {
36633
- if (started) {
36634
- words.push(word);
36635
- word = "";
36636
- started = false;
37062
+ const completions = await client.getCompletions(scriptPath, content, line, character);
37063
+ return asToolResponse({ completions });
36637
37064
  }
36638
- } else {
36639
- word += char;
36640
- started = true;
37065
+ case "lsp_get_hover": {
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 });
37073
+ }
37074
+ case "lsp_get_symbols": {
37075
+ const symbols = await client.getDocumentSymbols(scriptPath, content);
37076
+ return asToolResponse({ symbols });
37077
+ }
37078
+ default:
37079
+ return asToolResponse({ error: `Unknown LSP tool: ${toolName}` });
36641
37080
  }
37081
+ } catch (error) {
37082
+ return asToolResponse({
37083
+ error: normalizeLSPError(error, client.port)
37084
+ });
36642
37085
  }
36643
- if (started) {
36644
- words.push(word);
36645
- }
36646
- return words;
36647
- }
36648
- async function askWindows() {
36649
- const { stdout } = await run5("powershell.exe", [
36650
- "-NoProfile",
36651
- "-NonInteractive",
36652
- "-Command",
36653
- 'Get-CimInstance Win32_Process | ForEach-Object { $began = 0; if ($_.CreationDate) { $began = ([DateTimeOffset]$_.CreationDate).ToUnixTimeMilliseconds() }; "$($_.ProcessId) $($_.ParentProcessId) $began $($_.CommandLine)" }'
36654
- ], { timeout: ASK_TIMEOUT_MS, windowsHide: true, maxBuffer: 16 * 1024 * 1024 });
36655
- return stdout;
36656
- }
36657
- async function askPosix() {
36658
- const { stdout } = await run5("ps", ["-e", "-o", "pid=,ppid=,args="], {
36659
- timeout: ASK_TIMEOUT_MS,
36660
- maxBuffer: 16 * 1024 * 1024
36661
- });
36662
- return stdout;
36663
37086
  }
36664
- var run5, ASK_TIMEOUT_MS = 15000;
36665
- var init_process_children = __esm(() => {
36666
- 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();
36667
37093
  });
36668
37094
 
36669
37095
  // src/process-time.ts
@@ -36706,13 +37132,13 @@ var init_process_time = __esm(() => {
36706
37132
  });
36707
37133
 
36708
37134
  // src/project-scan.ts
36709
- import { existsSync as existsSync12, readdirSync as readdirSync4, readFileSync as readFileSync13 } from "node:fs";
36710
- 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";
36711
37137
  function skipped(name) {
36712
37138
  return name.startsWith(".") || SKIPPED.has(name);
36713
37139
  }
36714
37140
  function steppedOver(directory) {
36715
- return existsSync12(join15(directory, ".gdignore"));
37141
+ return existsSync13(join17(directory, ".gdignore"));
36716
37142
  }
36717
37143
  function projectStructure(projectPath) {
36718
37144
  const structure = { scenes: 0, scripts: 0, assets: 0, other: 0 };
@@ -36720,12 +37146,12 @@ function projectStructure(projectPath) {
36720
37146
  if (steppedOver(directory)) {
36721
37147
  return;
36722
37148
  }
36723
- for (const entry of readdirSync4(directory, { withFileTypes: true })) {
37149
+ for (const entry of readdirSync5(directory, { withFileTypes: true })) {
36724
37150
  if (skipped(entry.name)) {
36725
37151
  continue;
36726
37152
  }
36727
37153
  if (entry.isDirectory()) {
36728
- visit(join15(directory, entry.name));
37154
+ visit(join17(directory, entry.name));
36729
37155
  } else if (entry.isFile()) {
36730
37156
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
36731
37157
  if (extension === "tscn") {
@@ -36749,20 +37175,20 @@ function scriptsWithoutUid(projectPath) {
36749
37175
  if (steppedOver(directory)) {
36750
37176
  return;
36751
37177
  }
36752
- for (const entry of readdirSync4(directory, { withFileTypes: true })) {
37178
+ for (const entry of readdirSync5(directory, { withFileTypes: true })) {
36753
37179
  if (skipped(entry.name)) {
36754
37180
  continue;
36755
37181
  }
36756
37182
  const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
36757
37183
  if (entry.isDirectory()) {
36758
- visit(join15(directory, entry.name), spelled);
37184
+ visit(join17(directory, entry.name), spelled);
36759
37185
  continue;
36760
37186
  }
36761
37187
  if (!entry.isFile()) {
36762
37188
  continue;
36763
37189
  }
36764
37190
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
36765
- 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`))) {
36766
37192
  found.push(spelled);
36767
37193
  }
36768
37194
  }
@@ -36790,14 +37216,14 @@ function searchProject(projectPath, options) {
36790
37216
  if (steppedOver(directory)) {
36791
37217
  return;
36792
37218
  }
36793
- for (const entry of readdirSync4(directory, { withFileTypes: true })) {
37219
+ for (const entry of readdirSync5(directory, { withFileTypes: true })) {
36794
37220
  if (full()) {
36795
37221
  return;
36796
37222
  }
36797
37223
  if (skipped(entry.name)) {
36798
37224
  continue;
36799
37225
  }
36800
- const entryPath = join15(directory, entry.name);
37226
+ const entryPath = join17(directory, entry.name);
36801
37227
  if (entry.isDirectory()) {
36802
37228
  visit(entryPath);
36803
37229
  continue;
@@ -36808,7 +37234,7 @@ function searchProject(projectPath, options) {
36808
37234
  }
36809
37235
  result.summary.files_searched += 1;
36810
37236
  const matches = [];
36811
- for (const [index, line] of readFileSync13(entryPath, "utf8").split(`
37237
+ for (const [index, line] of readFileSync15(entryPath, "utf8").split(`
36812
37238
  `).entries()) {
36813
37239
  if (full()) {
36814
37240
  break;
@@ -36816,494 +37242,100 @@ function searchProject(projectPath, options) {
36816
37242
  const match = regex ? regex.exec(line)?.[0] : (options.caseSensitive ? line : line.toLowerCase()).includes(needle) ? options.query : "";
36817
37243
  if (match) {
36818
37244
  matches.push({ line: index + 1, content: line.trim(), match });
36819
- result.summary.total_matches += 1;
36820
- }
36821
- }
36822
- if (matches.length > 0) {
36823
- const relativePath = entryPath.slice(projectPath.length + 1).replace(/\\/g, "/");
36824
- result.results.push({ file: `res://${relativePath}`, matches });
36825
- result.summary.files_with_matches += 1;
36826
- }
36827
- }
36828
- };
36829
- visit(projectPath);
36830
- return result;
36831
- }
36832
- var SKIPPED, ASSET_EXTENSIONS, CARRIES_A_UID;
36833
- var init_project_scan = __esm(() => {
36834
- SKIPPED = new Set(["node_modules"]);
36835
- ASSET_EXTENSIONS = new Set(["png", "jpg", "jpeg", "webp", "svg", "ttf", "otf", "wav", "mp3", "ogg"]);
36836
- CARRIES_A_UID = new Set(["gd", "shader", "gdshader"]);
36837
- });
36838
-
36839
- // src/restart-note.ts
36840
- import { mkdirSync as mkdirSync8, readFileSync as readFileSync14, rmSync as rmSync8, writeFileSync as writeFileSync10 } from "node:fs";
36841
- import { dirname as dirname10, join as join16 } from "node:path";
36842
- function restartNotePath(projectPath) {
36843
- return join16(projectPath, ".godot", "gdharness-restart.json");
36844
- }
36845
- function noteRestartBegun(note) {
36846
- const path = restartNotePath(note.projectPath);
36847
- try {
36848
- mkdirSync8(dirname10(path), { recursive: true });
36849
- writeFileSync10(path, `${JSON.stringify(note, null, 2)}
36850
- `, "utf8");
36851
- } catch (error) {
36852
- console.error(`[SERVER] Could not record the restart at ${path}: ${errorMessage(error)}`);
36853
- }
36854
- }
36855
- function restartOwed(projectPath) {
36856
- try {
36857
- const parsed = JSON.parse(readFileSync14(restartNotePath(projectPath), "utf8"));
36858
- if (typeof parsed !== "object" || parsed === null) {
36859
- return null;
36860
- }
36861
- const said = parsed;
36862
- if (typeof said.quitAt !== "string" || typeof said.byPid !== "number" || typeof said.ports !== "object") {
36863
- return null;
36864
- }
36865
- return said;
36866
- } catch {
36867
- return null;
36868
- }
36869
- }
36870
- function restartSettled(projectPath) {
36871
- try {
36872
- rmSync8(restartNotePath(projectPath), { force: true });
36873
- } catch (error) {
36874
- console.error(`[SERVER] Could not clear the restart note: ${errorMessage(error)}`);
36875
- }
36876
- }
36877
- var init_restart_note = __esm(() => {
36878
- init_errors();
36879
- });
36880
-
36881
- // src/runtime-client.ts
36882
- import { existsSync as existsSync13, readdirSync as readdirSync5, readFileSync as readFileSync15, statSync as statSync3, unlinkSync } from "node:fs";
36883
- import { createConnection as createConnection3 } from "node:net";
36884
- import { tmpdir as tmpdir4 } from "node:os";
36885
- import { dirname as dirname11, join as join17, resolve as resolve4 } from "node:path";
36886
- import { setTimeout as delay2 } from "node:timers/promises";
36887
- function runtimeDirectory(variables = process.env) {
36888
- const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
36889
- if (explicit) {
36890
- return explicit;
36891
- }
36892
- const perUser = envValue("XDG_RUNTIME_DIR", variables);
36893
- return join17(perUser ?? tmpdir4(), "gdharness");
36894
- }
36895
- function runtimeDirectories(variables = process.env) {
36896
- const candidates = [runtimeDirectory(variables)];
36897
- const fallbacks = process.platform === "win32" ? [envValue("SystemRoot", variables) ?? envValue("windir", variables) ?? "C:\\Windows"] : ["/tmp", "/var/tmp"];
36898
- for (const base of fallbacks) {
36899
- candidates.push(join17(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
36900
- }
36901
- return [...new Set(candidates.map((path) => resolve4(path)))];
36902
- }
36903
- function errorReportOf(pid, directories = runtimeDirectories()) {
36904
- for (const directory of directories) {
36905
- const path = join17(directory, `runtime-${pid}.log`);
36906
- if (existsSync13(path)) {
36907
- return path;
36908
- }
36909
- }
36910
- return null;
36911
- }
36912
- function processAlive(pid) {
36913
- try {
36914
- process.kill(pid, 0);
36915
- return true;
36916
- } catch (error) {
36917
- return error.code === "EPERM";
36918
- }
36919
- }
36920
- function parseAnnouncement(file, pid) {
36921
- let fields;
36922
- try {
36923
- fields = asParams(JSON.parse(readFileSync15(file, "utf8")));
36924
- } catch {
36925
- return { kind: "rubbish" };
36926
- }
36927
- const project = readParams(fields, "project");
36928
- const port = readNumber(fields, "port");
36929
- const address = readString(fields, "address");
36930
- const protocol = readNumber(fields, "protocol");
36931
- if (readNumber(fields, "pid") !== pid || port === undefined || !Number.isInteger(port) || port < 1 || port > 65535 || address === undefined || project === undefined) {
36932
- return { kind: "rubbish" };
36933
- }
36934
- const named = {
36935
- name: readString(project, "name") ?? "",
36936
- path: readString(project, "path") ?? ""
36937
- };
36938
- if (protocol !== RUNTIME_PROTOCOL) {
36939
- return { kind: "unspoken", unspoken: { pid, protocol: protocol ?? 0, project: named } };
36940
- }
36941
- const editorPid = readNumber(fields, "editor_pid");
36942
- return {
36943
- kind: "runtime",
36944
- endpoint: {
36945
- pid,
36946
- port,
36947
- address,
36948
- project: named,
36949
- file,
36950
- ...editorPid !== undefined && Number.isInteger(editorPid) && editorPid > 0 ? { editorPid } : {}
36951
- }
36952
- };
36953
- }
36954
- function runtimesAnnounced(directories = runtimeDirectories()) {
36955
- const running = new Map;
36956
- const unspoken = new Map;
36957
- for (const directory of directories) {
36958
- for (const found of announcedIn(directory)) {
36959
- if (found.kind === "runtime" && !running.has(found.endpoint.pid)) {
36960
- running.set(found.endpoint.pid, found.endpoint);
36961
- } else if (found.kind === "unspoken" && !unspoken.has(found.unspoken.pid)) {
36962
- unspoken.set(found.unspoken.pid, found.unspoken);
36963
- }
36964
- }
36965
- }
36966
- const newest = (a, b) => b.pid - a.pid;
36967
- return { running: [...running.values()].sort(newest), unspoken: [...unspoken.values()].sort(newest) };
36968
- }
36969
- function discoverRuntimes(directories = runtimeDirectories()) {
36970
- return runtimesAnnounced(directories).running;
36971
- }
36972
- function wentAway(file, pid) {
36973
- let project = "";
36974
- try {
36975
- const fields = asParams(JSON.parse(readFileSync15(file, "utf8")));
36976
- project = readString(readParams(fields, "project") ?? {}, "path") ?? "";
36977
- } catch {
36978
- return;
36979
- }
36980
- if (project === "") {
36981
- return;
36982
- }
36983
- gone.unshift({ pid, project, noticedAt: Date.now() });
36984
- gone.length = Math.min(gone.length, GONE_KEPT);
36985
- }
36986
- function announcedAndGone(projectPath) {
36987
- if (projectPath === undefined) {
36988
- return [];
36989
- }
36990
- const since = Date.now() - GONE_WINDOW_MS;
36991
- const wanted = resolve4(projectPath);
36992
- return gone.filter((one) => one.noticedAt >= since && resolve4(one.project) === wanted);
36993
- }
36994
- function strangersAmong(candidates, now, startTimes = startTimesOf) {
36995
- const toJudge = candidates.filter((one) => {
36996
- if (now - one.writtenAt < JUDGED_AFTER_MS) {
36997
- return false;
36998
- }
36999
- const known = verdicts.get(one.file);
37000
- return known === undefined || known.writtenAt !== one.writtenAt || !known.stranger && now - known.judgedAt > CONFIRMED_FOR_MS;
37001
- });
37002
- if (toJudge.length > 0) {
37003
- const began = startTimes(toJudge.map((one) => one.pid));
37004
- for (const one of toJudge) {
37005
- const at = began.get(one.pid);
37006
- const stranger = at !== undefined && at - one.writtenAt > STARTED_AFTER_ANNOUNCING_MS;
37007
- verdicts.set(one.file, { writtenAt: one.writtenAt, judgedAt: now, stranger });
37008
- }
37009
- }
37010
- return new Set(candidates.filter((one) => verdicts.get(one.file)?.stranger === true).map((one) => one.file));
37011
- }
37012
- function announcedIn(directory) {
37013
- if (!existsSync13(directory)) {
37014
- return [];
37015
- }
37016
- const found = [];
37017
- const candidates = [];
37018
- const entries = [];
37019
- for (const entry of readdirSync5(directory)) {
37020
- const report = ERROR_REPORT_PATTERN.exec(entry);
37021
- if (report) {
37022
- sweepErrorReport(join17(directory, entry), Number.parseInt(report[1] ?? "", 10));
37023
- continue;
37024
- }
37025
- const match = ANNOUNCEMENT_PATTERN.exec(entry);
37026
- if (!match) {
37027
- continue;
37028
- }
37029
- const file = join17(directory, entry);
37030
- const pid = Number.parseInt(match[1] ?? "", 10);
37031
- const alive = processAlive(pid);
37032
- entries.push({ file, pid, alive });
37033
- if (alive) {
37034
- try {
37035
- candidates.push({ file, pid, writtenAt: statSync3(file).mtimeMs });
37036
- } catch {}
37037
- }
37038
- }
37039
- const strangers = strangersAmong(candidates, Date.now());
37040
- for (const { file, pid, alive } of entries) {
37041
- const announced = alive && !strangers.has(file) ? parseAnnouncement(file, pid) : { kind: "rubbish" };
37042
- if (announced.kind === "rubbish") {
37043
- if (!alive || strangers.has(file)) {
37044
- wentAway(file, pid);
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;
37045
37252
  }
37046
- try {
37047
- unlinkSync(file);
37048
- } catch {}
37049
- verdicts.delete(file);
37050
- continue;
37051
- }
37052
- found.push(announced);
37053
- }
37054
- const held = new Set(entries.map((one) => one.file));
37055
- const here = join17(directory, ".");
37056
- for (const file of verdicts.keys()) {
37057
- if (dirname11(file) === here && !held.has(file)) {
37058
- verdicts.delete(file);
37059
37253
  }
37060
- }
37061
- return found;
37254
+ };
37255
+ visit(projectPath);
37256
+ return result;
37062
37257
  }
37063
- async function announcementEnded(pid, directories = runtimeDirectories()) {
37064
- const files = directories.map((directory) => join17(directory, `runtime-${pid}.json`)).filter((file) => existsSync13(file));
37065
- const first = files[0];
37066
- if (first === undefined) {
37067
- return;
37068
- }
37069
- const until = Date.now() + ENDED_WITHIN_MS;
37070
- while (processAlive(pid)) {
37071
- const left = until - Date.now();
37072
- if (left <= 0) {
37073
- return;
37074
- }
37075
- await delay2(Math.min(LOOK_EVERY_MS, left));
37076
- }
37077
- wentAway(first, pid);
37078
- for (const file of files) {
37079
- try {
37080
- unlinkSync(file);
37081
- } catch {}
37082
- verdicts.delete(file);
37083
- }
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");
37084
37270
  }
37085
- function sweepErrorReport(path, pid, now = Date.now()) {
37086
- if (processAlive(pid)) {
37087
- return;
37088
- }
37271
+ function noteRestartBegun(note) {
37272
+ const path = restartNotePath(note.projectPath);
37089
37273
  try {
37090
- if (now - statSync3(path).mtimeMs >= ERROR_REPORT_KEEP_MS) {
37091
- unlinkSync(path);
37092
- }
37093
- } 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
+ }
37094
37280
  }
37095
- async function announcedSince(projectPath, before, waiting = {}) {
37096
- const wanted = resolve4(projectPath);
37097
- const directories = waiting.directories ?? runtimeDirectories();
37098
- const until = Date.now() + Math.max(waiting.budgetMs ?? ANNOUNCE_BUDGET_MS, 0);
37099
- for (;; ) {
37100
- const fresh = discoverRuntimes(directories).find((endpoint) => !before.has(endpoint.pid) && resolve4(endpoint.project.path) === wanted && (waiting.accept?.(endpoint) ?? true));
37101
- if (fresh !== undefined) {
37102
- 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;
37103
37286
  }
37104
- const left = until - Date.now();
37105
- if (left <= 0 || waiting.giveUp?.() === true) {
37287
+ const said = parsed;
37288
+ if (typeof said.quitAt !== "string" || typeof said.byPid !== "number" || typeof said.ports !== "object") {
37106
37289
  return null;
37107
37290
  }
37108
- await delay2(Math.min(LOOK_EVERY_MS, left));
37291
+ return said;
37292
+ } catch {
37293
+ return null;
37109
37294
  }
37110
37295
  }
37111
- function describe2(endpoint) {
37112
- return `pid ${endpoint.pid} on ${endpoint.address}:${endpoint.port} (${endpoint.project.name || "unnamed"} at ${endpoint.project.path})`;
37113
- }
37114
- function tooNew(unspoken) {
37115
- const named = unspoken.map((game) => `pid ${game.pid} speaks ${game.protocol} (${game.project.name || "unnamed"} at ${game.project.path})`).join("; ");
37116
- 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";
37117
- return `A game is running, in a protocol this server does not speak ${named}. This server speaks ${RUNTIME_PROTOCOL}, so ${half}.`;
37118
- }
37119
- function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
37120
- if (pid !== undefined) {
37121
- const named = endpoints.find((endpoint) => endpoint.pid === pid);
37122
- if (named !== undefined) {
37123
- if (projectPath !== undefined && resolve4(named.project.path) !== resolve4(projectPath)) {
37124
- return {
37125
- problem: `The game with pid ${pid} is running ${named.project.path}, not ${resolve4(projectPath)}. Running: ${endpoints.map(describe2).join("; ")}.`
37126
- };
37127
- }
37128
- return { endpoint: named };
37129
- }
37130
- const tooNewToo = unspoken.find((game) => game.pid === pid);
37131
- if (tooNewToo !== undefined) {
37132
- return { problem: tooNew([tooNewToo]) };
37133
- }
37134
- return {
37135
- 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.`
37136
- };
37137
- }
37138
- if (endpoints.length === 0) {
37139
- if (unspoken.length > 0) {
37140
- return { problem: tooNew(unspoken) };
37141
- }
37142
- const went = announcedAndGone(projectPath);
37143
- const first = went[0];
37144
- if (first !== undefined) {
37145
- const ago = Math.max(1, Math.round((Date.now() - first.noticedAt) / 1000));
37146
- const others = went.length - 1;
37147
- const earlier = others === 0 ? "" : ` ${others} earlier ${others === 1 ? "one" : "ones"} went the same way.`;
37148
- return {
37149
- 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."
37150
- };
37151
- }
37152
- return {
37153
- 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."
37154
- };
37155
- }
37156
- if (projectPath !== undefined) {
37157
- const wanted = resolve4(projectPath);
37158
- const matching = endpoints.filter((endpoint) => resolve4(endpoint.project.path) === wanted);
37159
- if (matching.length === 0) {
37160
- return {
37161
- problem: `No running game is from ${wanted}. Running: ${endpoints.map(describe2).join("; ")}.`
37162
- };
37163
- }
37164
- if (matching.length === 1 && matching[0]) {
37165
- return { endpoint: matching[0] };
37166
- }
37167
- return {
37168
- problem: `Several games are running from ${wanted}: ${matching.map(describe2).join("; ")}. Pass pid to choose one.`
37169
- };
37170
- }
37171
- if (endpoints.length === 1 && endpoints[0]) {
37172
- 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)}`);
37173
37301
  }
37174
- return {
37175
- problem: `Several games are running: ${endpoints.map(describe2).join("; ")}. Pass projectPath to choose one, or pid when they are from one project.`
37176
- };
37177
- }
37178
- function runtimeRequest(endpoint, command, params, timeoutMs) {
37179
- const id = nextRequestId++;
37180
- return new Promise((settle) => {
37181
- let done = false;
37182
- let buffered = "";
37183
- let welcomed = false;
37184
- const socket = createConnection3({ port: endpoint.port, host: endpoint.address });
37185
- const finish = (reply) => {
37186
- if (done) {
37187
- return;
37188
- }
37189
- done = true;
37190
- clearTimeout(timer);
37191
- socket.destroy();
37192
- settle(reply);
37193
- };
37194
- const timer = setTimeout(() => {
37195
- finish({
37196
- ok: false,
37197
- reason: "busy",
37198
- 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.`
37199
- });
37200
- }, timeoutMs);
37201
- socket.setEncoding("utf8");
37202
- socket.on("connect", () => {
37203
- socket.write(`${JSON.stringify({ id, command, params })}
37204
- `);
37205
- });
37206
- socket.on("data", (chunk) => {
37207
- buffered += chunk;
37208
- let newline = buffered.indexOf(`
37209
- `);
37210
- while (newline !== -1 && !done) {
37211
- const line = buffered.slice(0, newline).trim();
37212
- buffered = buffered.slice(newline + 1);
37213
- newline = buffered.indexOf(`
37214
- `);
37215
- if (line === "") {
37216
- continue;
37217
- }
37218
- let message;
37219
- try {
37220
- message = asParams(JSON.parse(line));
37221
- } catch {
37222
- continue;
37223
- }
37224
- if (!welcomed) {
37225
- welcomed = true;
37226
- if (readString(message, "type") === "welcome") {
37227
- const spoken = readNumber(message, "protocol");
37228
- if (spoken !== RUNTIME_PROTOCOL) {
37229
- finish({
37230
- ok: false,
37231
- reason: "protocol",
37232
- message: `The game (${describe2(endpoint)}) speaks runtime protocol ${spoken ?? "unknown"} and this server speaks ${RUNTIME_PROTOCOL}. Reinstall the runtime addon into the project.`
37233
- });
37234
- return;
37235
- }
37236
- continue;
37237
- }
37238
- }
37239
- if (message["id"] !== id) {
37240
- continue;
37241
- }
37242
- if (readString(message, "type") === "error") {
37243
- finish({ ok: false, reason: "error", message: readString(message, "message") ?? "unknown error" });
37244
- return;
37245
- }
37246
- finish({ ok: true, payload: message, endpoint });
37247
- }
37248
- });
37249
- socket.on("error", (error) => {
37250
- finish({
37251
- ok: false,
37252
- reason: "refused",
37253
- 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.`
37254
- });
37255
- });
37256
- socket.on("close", () => {
37257
- finish({
37258
- ok: false,
37259
- reason: "refused",
37260
- message: `The game (${describe2(endpoint)}) closed the connection before answering '${command}'.`
37261
- });
37262
- });
37263
- });
37264
37302
  }
37265
- 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;
37266
- var init_runtime_client = __esm(() => {
37267
- init_process_children();
37268
- ANNOUNCEMENT_PATTERN = /^runtime-(\d+)\.json$/;
37269
- ERROR_REPORT_PATTERN = /^runtime-(\d+)\.log$/;
37270
- ERROR_REPORT_KEEP_MS = 60 * 60 * 1000;
37271
- gone = [];
37272
- verdicts = new Map;
37303
+ var init_restart_note = __esm(() => {
37304
+ init_errors();
37273
37305
  });
37274
37306
 
37275
37307
  // src/run-record.ts
37276
37308
  import { execFileSync as execFileSync2 } from "node:child_process";
37277
- import { mkdirSync as mkdirSync9, openSync, readdirSync as readdirSync6, readFileSync as readFileSync16, rmSync as rmSync9, statSync as statSync4, writeFileSync as writeFileSync11 } from "node:fs";
37278
- import { basename as basename3, join as join18 } from "node:path";
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";
37279
37311
  function runsDirectory() {
37280
- return join18(runtimeDirectory(), "runs");
37312
+ return join19(runtimeDirectory(), "runs");
37281
37313
  }
37282
37314
  function recordPath() {
37283
- return join18(runsDirectory(), "run.json");
37315
+ return join19(runsDirectory(), "run.json");
37284
37316
  }
37285
37317
  function recordPaths() {
37286
- return runtimeDirectories().map((directory) => join18(directory, "runs", "run.json"));
37318
+ return runtimeDirectories().map((directory) => join19(directory, "runs", "run.json"));
37287
37319
  }
37288
37320
  function openTranscript(startedAt) {
37289
- mkdirSync9(runsDirectory(), { recursive: true });
37290
- const path = join18(runsDirectory(), `run-${startedAt}.log`);
37321
+ mkdirSync10(runsDirectory(), { recursive: true });
37322
+ const path = join19(runsDirectory(), `run-${startedAt}.log`);
37291
37323
  return { path, fd: openSync(path, "a") };
37292
37324
  }
37293
37325
  function writeRunRecord(record) {
37294
- mkdirSync9(runsDirectory(), { recursive: true });
37295
- writeFileSync11(recordPath(), JSON.stringify(record, null, 2), "utf8");
37326
+ mkdirSync10(runsDirectory(), { recursive: true });
37327
+ writeFileSync12(recordPath(), JSON.stringify(record, null, 2), "utf8");
37296
37328
  }
37297
37329
  function editorNotePath() {
37298
- return join18(runsDirectory(), "editor-run.json");
37330
+ return join19(runsDirectory(), "editor-run.json");
37299
37331
  }
37300
37332
  function writeEditorRunNote(note) {
37301
- mkdirSync9(runsDirectory(), { recursive: true });
37302
- writeFileSync11(editorNotePath(), JSON.stringify(note, null, 2), "utf8");
37333
+ mkdirSync10(runsDirectory(), { recursive: true });
37334
+ writeFileSync12(editorNotePath(), JSON.stringify(note, null, 2), "utf8");
37303
37335
  }
37304
37336
  function readEditorRunNote() {
37305
37337
  for (const directory of runtimeDirectories()) {
37306
- const note = editorNoteAt(join18(directory, "runs", "editor-run.json"));
37338
+ const note = editorNoteAt(join19(directory, "runs", "editor-run.json"));
37307
37339
  if (note !== null) {
37308
37340
  return note;
37309
37341
  }
@@ -37313,7 +37345,7 @@ function readEditorRunNote() {
37313
37345
  function editorNoteAt(path) {
37314
37346
  let parsed;
37315
37347
  try {
37316
- parsed = JSON.parse(readFileSync16(path, "utf8"));
37348
+ parsed = JSON.parse(readFileSync17(path, "utf8"));
37317
37349
  } catch {
37318
37350
  return null;
37319
37351
  }
@@ -37336,7 +37368,7 @@ function recordRunEnded(pid, exitCode) {
37336
37368
  return;
37337
37369
  }
37338
37370
  try {
37339
- writeFileSync11(path, JSON.stringify({ ...record, exitCode }, null, 2), "utf8");
37371
+ writeFileSync12(path, JSON.stringify({ ...record, exitCode }, null, 2), "utf8");
37340
37372
  } catch {}
37341
37373
  }
37342
37374
  function readRunRecord() {
@@ -37351,7 +37383,7 @@ function readRunRecord() {
37351
37383
  function recordAt(path) {
37352
37384
  let parsed;
37353
37385
  try {
37354
- parsed = JSON.parse(readFileSync16(path, "utf8"));
37386
+ parsed = JSON.parse(readFileSync17(path, "utf8"));
37355
37387
  } catch {
37356
37388
  return null;
37357
37389
  }
@@ -37396,7 +37428,7 @@ function runningAs(pid) {
37396
37428
  const began = startedAt(pid, null);
37397
37429
  const when = began === null ? {} : { startedAt: began };
37398
37430
  if (process.platform === "linux") {
37399
- const raw = readFileSync16(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
37431
+ const raw = readFileSync17(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
37400
37432
  return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
37401
37433
  }
37402
37434
  const args = execFileSync2("ps", ["-p", String(pid), "-o", "args="], {
@@ -37547,7 +37579,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
37547
37579
  if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
37548
37580
  continue;
37549
37581
  }
37550
- const path = join18(directory, entry);
37582
+ const path = join19(directory, entry);
37551
37583
  if (path === keep || ageMs(path, now) < keepMs) {
37552
37584
  continue;
37553
37585
  }
@@ -37570,15 +37602,16 @@ import {
37570
37602
  closeSync,
37571
37603
  existsSync as existsSync14,
37572
37604
  fstatSync,
37573
- mkdirSync as mkdirSync10,
37605
+ mkdirSync as mkdirSync11,
37574
37606
  mkdtempSync as mkdtempSync3,
37575
37607
  openSync as openSync2,
37576
37608
  readdirSync as readdirSync7,
37577
- readFileSync as readFileSync17,
37578
- readSync
37609
+ readFileSync as readFileSync18,
37610
+ readSync,
37611
+ statSync as statSync5
37579
37612
  } from "node:fs";
37580
37613
  import { tmpdir as tmpdir5 } from "node:os";
37581
- import { basename as basename4, dirname as dirname12, 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";
37582
37615
  import { setTimeout as delay3 } from "node:timers/promises";
37583
37616
  import { fileURLToPath as fileURLToPath4 } from "node:url";
37584
37617
  import { promisify as promisify7 } from "node:util";
@@ -37701,10 +37734,11 @@ function runtimeVerdict(endpoint, after) {
37701
37734
  };
37702
37735
  }
37703
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,`;
37704
37738
  return {
37705
37739
  listening: false,
37706
37740
  mayYetAnnounce: after.running,
37707
- 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`,
37708
37742
  heldAt: null
37709
37743
  };
37710
37744
  }
@@ -37809,7 +37843,7 @@ function aboveTheRunner(entry) {
37809
37843
  }
37810
37844
  function elsewhereIn(projectPath, asked) {
37811
37845
  const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
37812
- if (wanted === "" || existsSync14(join19(projectPath, wanted))) {
37846
+ if (wanted === "" || existsSync14(join20(projectPath, wanted))) {
37813
37847
  return "";
37814
37848
  }
37815
37849
  const root = (wanted.split("/")[0] ?? "").toLowerCase();
@@ -37823,7 +37857,7 @@ function elsewhereIn(projectPath, asked) {
37823
37857
  return near.length === 0 ? "" : ` The project does have ${there}.`;
37824
37858
  }
37825
37859
  function hasMainScene(projectFile) {
37826
- const scene = parseProjectGodot(readFileSync17(projectFile, "utf8"))["application"]?.["run/main_scene"];
37860
+ const scene = parseProjectGodot(readFileSync18(projectFile, "utf8"))["application"]?.["run/main_scene"];
37827
37861
  return typeof scene === "string" && scene !== "";
37828
37862
  }
37829
37863
  function runtimeAddonAnnouncesEditors(projectPath) {
@@ -37831,13 +37865,13 @@ function runtimeAddonAnnouncesEditors(projectPath) {
37831
37865
  return false;
37832
37866
  }
37833
37867
  try {
37834
- return readFileSync17(join19(projectPath, RUNTIME_AUTOLOAD2.path), "utf8").includes('"editor_pid"');
37868
+ return readFileSync18(join20(projectPath, RUNTIME_AUTOLOAD2.path), "utf8").includes('"editor_pid"');
37835
37869
  } catch {
37836
37870
  return false;
37837
37871
  }
37838
37872
  }
37839
37873
  function editorPlaysHeadless(projectFile) {
37840
- const runArgs = parseProjectGodot(readFileSync17(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
37874
+ const runArgs = parseProjectGodot(readFileSync18(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
37841
37875
  return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
37842
37876
  }
37843
37877
  function camelCased(params) {
@@ -37853,7 +37887,7 @@ function camelCased(params) {
37853
37887
  class GodotServer {
37854
37888
  mcp;
37855
37889
  locator = new GodotLocator2;
37856
- operationsScript = join19(__dirname2, "godot", "operations", "godot_operations.gd");
37890
+ operationsScript = join20(__dirname2, "godot", "operations", "godot_operations.gd");
37857
37891
  godotBridge;
37858
37892
  tools = buildToolDefinitions();
37859
37893
  activeProcess = null;
@@ -38511,7 +38545,7 @@ class GodotServer {
38511
38545
  if (path === undefined) {
38512
38546
  return { ok: false, response: this.createErrorResponse("projectPath is required.") };
38513
38547
  }
38514
- const file = join19(path, "project.godot");
38548
+ const file = join20(path, "project.godot");
38515
38549
  if (!existsSync14(file)) {
38516
38550
  return {
38517
38551
  ok: false,
@@ -38735,7 +38769,7 @@ class GodotServer {
38735
38769
  if (!engine.ok) {
38736
38770
  return engine.response;
38737
38771
  }
38738
- const application = parseProjectGodot(readFileSync17(project.value.file, "utf8"))["application"] ?? {};
38772
+ const application = parseProjectGodot(readFileSync18(project.value.file, "utf8"))["application"] ?? {};
38739
38773
  const name = application["config/name"];
38740
38774
  const mainScene = application["run/main_scene"];
38741
38775
  const info = {
@@ -38796,18 +38830,18 @@ class GodotServer {
38796
38830
  if (!engine.ok) {
38797
38831
  return engine.response;
38798
38832
  }
38799
- const folder = dirname12(output.absolutePath);
38833
+ const folder = dirname13(output.absolutePath);
38800
38834
  try {
38801
- mkdirSync10(folder, { recursive: true });
38835
+ mkdirSync11(folder, { recursive: true });
38802
38836
  } catch (error) {
38803
- return this.createErrorResponse(`Cannot create ${dirname12(output.relativePath)} to export into: ${errorMessage(error)}`);
38837
+ return this.createErrorResponse(`Cannot create ${dirname13(output.relativePath)} to export into: ${errorMessage(error)}`);
38804
38838
  }
38805
38839
  const debug = readBoolean(args, "debug") ?? false;
38806
- const exportLogs = mkdtempSync3(join19(tmpdir5(), "gdharness-export-"));
38840
+ const exportLogs = mkdtempSync3(join20(tmpdir5(), "gdharness-export-"));
38807
38841
  const exportArgs = [
38808
38842
  "--headless",
38809
38843
  "--log-file",
38810
- join19(exportLogs, "engine.log"),
38844
+ join20(exportLogs, "engine.log"),
38811
38845
  "--path",
38812
38846
  project.value.path,
38813
38847
  debug ? "--export-debug" : "--export-release",
@@ -38865,7 +38899,7 @@ class GodotServer {
38865
38899
  return contained.response;
38866
38900
  }
38867
38901
  const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
38868
- if (!existsSync14(join19(project.value.path, runner))) {
38902
+ if (!existsSync14(join20(project.value.path, runner))) {
38869
38903
  return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
38870
38904
  "Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
38871
38905
  ]);
@@ -38900,7 +38934,7 @@ class GodotServer {
38900
38934
  ];
38901
38935
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
38902
38936
  this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
38903
- const userData = mkdtempSync3(join19(tmpdir5(), "gdharness-tests-"));
38937
+ const userData = mkdtempSync3(join20(tmpdir5(), "gdharness-tests-"));
38904
38938
  const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
38905
38939
  const hung = await new Promise((resolve) => {
38906
38940
  const timer = setTimeout(() => {
@@ -38916,14 +38950,14 @@ class GodotServer {
38916
38950
  resolve(false);
38917
38951
  });
38918
38952
  });
38919
- const reportsDir = join19(project.value.path, ".godot", ...ours.split("/"));
38953
+ const reportsDir = join20(project.value.path, ".godot", ...ours.split("/"));
38920
38954
  let report = null;
38921
38955
  let reportProblem = null;
38922
38956
  try {
38923
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))) : [];
38924
38958
  const newest = written.at(-1);
38925
38959
  if (newest !== undefined) {
38926
- report = parseJUnit(readFileSync17(join19(reportsDir, newest, "results.xml"), "utf8"));
38960
+ report = parseJUnit(readFileSync18(join20(reportsDir, newest, "results.xml"), "utf8"));
38927
38961
  }
38928
38962
  } catch (error) {
38929
38963
  reportProblem = errorMessage(error);
@@ -39063,7 +39097,7 @@ class GodotServer {
39063
39097
  return null;
39064
39098
  }
39065
39099
  try {
39066
- return readFileSync17(contained.absolutePath, "utf8");
39100
+ return readFileSync18(contained.absolutePath, "utf8");
39067
39101
  } catch {
39068
39102
  return null;
39069
39103
  }
@@ -39105,7 +39139,7 @@ class GodotServer {
39105
39139
  return null;
39106
39140
  }
39107
39141
  try {
39108
- return readFileSync17(contained.absolutePath, "utf8");
39142
+ return readFileSync18(contained.absolutePath, "utf8");
39109
39143
  } catch {
39110
39144
  return null;
39111
39145
  }
@@ -39334,7 +39368,7 @@ class GodotServer {
39334
39368
  return null;
39335
39369
  }
39336
39370
  try {
39337
- return settingKeys(readFileSync17(join19(projectPath, "project.godot"), "utf8"));
39371
+ return settingKeys(readFileSync18(join20(projectPath, "project.godot"), "utf8"));
39338
39372
  } catch {
39339
39373
  return null;
39340
39374
  }
@@ -39541,7 +39575,7 @@ class GodotServer {
39541
39575
  platform: process.platform,
39542
39576
  variables: process.env
39543
39577
  });
39544
- const runtimeWaitMs = readNonNegativeNumber(args, "runtimeWaitMs") ?? ANNOUNCE_BUDGET_MS;
39578
+ const runtimeWaitMs = this.announceBudget(args, project.value.path);
39545
39579
  const editorWouldPlay = !headless || editorPlaysHeadless(project.value.file);
39546
39580
  if (this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0) {
39547
39581
  return await this.playThroughEditor(sceneArgument, refreshed.value, project.value.path, alreadyPlaying, runtimeWaitMs, ended);
@@ -39574,8 +39608,9 @@ class GodotServer {
39574
39608
  message: `Use editor_output for what it prints and editor_run stop to end it.${spawnedInstead}${endedForThis}${alsoRunning}`
39575
39609
  });
39576
39610
  }
39577
- async runtimeUp(projectPath, before, budgetMs, withArgs) {
39578
- 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))) {
39579
39614
  return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
39580
39615
  }
39581
39616
  let asking = null;
@@ -39610,17 +39645,43 @@ class GodotServer {
39610
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()}`);
39611
39646
  const going = this.currentRun();
39612
39647
  if (endpoint !== null && going !== null && going.announcedPid === undefined) {
39613
- going.announcedPid = endpoint.pid;
39648
+ this.tie(going, endpoint);
39614
39649
  }
39615
39650
  return runtimeVerdict(endpoint, {
39616
39651
  addon: true,
39617
39652
  budgetMs,
39653
+ ...budget.sizedToMs === undefined ? {} : { sizedToMs: budget.sizedToMs },
39618
39654
  heldAt: this.dapClient?.whereItStopped() ?? null,
39619
39655
  running: endpoint !== null || going !== null && await this.runStillGoing(going),
39620
39656
  withArgs,
39621
39657
  ...going?.throughEditor === true ? { playingSeen: going.seenPlaying === true } : {}
39622
39658
  });
39623
39659
  }
39660
+ announceBudget(args, projectPath) {
39661
+ const asked = readNonNegativeNumber(args, "runtimeWaitMs");
39662
+ if (asked !== undefined) {
39663
+ return { ms: asked };
39664
+ }
39665
+ const lastBoot = readBootNote(projectPath);
39666
+ const ms = waitSizedTo(lastBoot);
39667
+ return lastBoot !== null && ms > ANNOUNCE_BUDGET_MS ? { ms, sizedToMs: lastBoot } : { ms };
39668
+ }
39669
+ noteBootOf(run, endpoint) {
39670
+ if (run.projectPath === null) {
39671
+ return;
39672
+ }
39673
+ let announcedAt;
39674
+ try {
39675
+ announcedAt = statSync5(endpoint.file).mtimeMs;
39676
+ } catch {
39677
+ return;
39678
+ }
39679
+ const took = announcedAt - run.startedAt;
39680
+ this.logDebug(`boot of pid ${endpoint.pid} announced ${Math.round(took)}ms after the start of ${run.projectPath}`);
39681
+ if (took > 0 && took <= LONGEST_BOOT_NOTED_MS) {
39682
+ writeBootNote(run.projectPath, took);
39683
+ }
39684
+ }
39624
39685
  async playThroughEditor(scene, refreshedClasses, projectPath, alreadyPlaying, runtimeWaitMs, ended) {
39625
39686
  const log = new GameLog;
39626
39687
  try {
@@ -39941,15 +40002,16 @@ class GodotServer {
39941
40002
  return run.announcedPid;
39942
40003
  }
39943
40004
  const ours = this.announcedOfTheRunsProject(run, announced ?? runtimesAnnounced().running);
39944
- if (run.pid !== null && ours.some((one) => one.pid === run.pid)) {
39945
- run.announcedPid = run.pid;
40005
+ const own = ours.find((one) => one.pid === run.pid);
40006
+ if (own !== undefined) {
40007
+ this.tie(run, own);
39946
40008
  return run.announcedPid;
39947
40009
  }
39948
40010
  const before = run.announcedBefore ?? new Set;
39949
40011
  const candidates = ours.filter((one) => !before.has(one.pid) && this.isTheGameOf(run, one, run.projectPath));
39950
40012
  const theOne = candidates.length === 1 ? candidates[0] : undefined;
39951
40013
  if (theOne !== undefined) {
39952
- run.announcedPid = theOne.pid;
40014
+ this.tie(run, theOne);
39953
40015
  }
39954
40016
  return run.announcedPid;
39955
40017
  }
@@ -39987,7 +40049,7 @@ class GodotServer {
39987
40049
  if (tree === undefined) {
39988
40050
  return;
39989
40051
  }
39990
- return this.tieThroughTheTree(run, tree, new Set(ours.map((one) => one.pid)));
40052
+ return this.tieThroughTheTree(run, tree, ours);
39991
40053
  }
39992
40054
  async gameProcessOf(run) {
39993
40055
  if (run.gamePid !== undefined) {
@@ -40008,21 +40070,28 @@ class GodotServer {
40008
40070
  }
40009
40071
  return found;
40010
40072
  }
40011
- tieThroughTheTree(run, tree, games) {
40073
+ tieThroughTheTree(run, tree, announced) {
40012
40074
  const root = run.pid ?? (run.throughEditor ? this.godotBridge.getStatus().editorPid : undefined);
40013
40075
  if (root === undefined) {
40014
40076
  return;
40015
40077
  }
40016
- if (games.has(root)) {
40017
- run.announcedPid = root;
40018
- return root;
40078
+ const games = new Set(announced.map((one) => one.pid));
40079
+ const own = announced.find((one) => one.pid === root);
40080
+ if (own !== undefined) {
40081
+ this.tie(run, own);
40082
+ return own.pid;
40019
40083
  }
40020
- const nearest = descendantsIn(tree, root).filter((one) => games.has(one) && !gameBetween(tree, one, root, games));
40084
+ const under = new Set(descendantsIn(tree, root));
40085
+ const nearest = announced.filter((one) => under.has(one.pid) && !gameBetween(tree, one.pid, root, games));
40021
40086
  const theOne = nearest.length === 1 ? nearest[0] : undefined;
40022
40087
  if (theOne !== undefined) {
40023
- run.announcedPid = theOne;
40088
+ this.tie(run, theOne);
40024
40089
  }
40025
- return theOne;
40090
+ return theOne?.pid;
40091
+ }
40092
+ tie(run, endpoint) {
40093
+ run.announcedPid = endpoint.pid;
40094
+ this.noteBootOf(run, endpoint);
40026
40095
  }
40027
40096
  playedByOurEditor(endpoint, projectPath) {
40028
40097
  const editor = this.godotBridge.getStatus().editorPid;
@@ -40323,7 +40392,7 @@ class GodotServer {
40323
40392
  }
40324
40393
  const announced = this.announcedOfTheRunsProject(run, runtimesAnnounced().running);
40325
40394
  const games = new Set(announced.map((one) => one.pid));
40326
- const own = this.announcedPidOf(run, announced) ?? this.tieThroughTheTree(run, tree, games) ?? run.gamePid ?? this.gameByCommandLineIn(run, tree);
40395
+ const own = this.announcedPidOf(run, announced) ?? this.tieThroughTheTree(run, tree, announced) ?? run.gamePid ?? this.gameByCommandLineIn(run, tree);
40327
40396
  if (own !== undefined && run.announcedPid === undefined) {
40328
40397
  run.gamePid = own;
40329
40398
  }
@@ -40618,8 +40687,8 @@ class GodotServer {
40618
40687
  ]);
40619
40688
  }
40620
40689
  const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
40621
- const screenshotDir = expectsScreenshot ? mkdtempSync3(join19(tmpdir5(), "gdharness-runtime-screenshot-")) : null;
40622
- const screenshotPath = screenshotDir ? join19(screenshotDir, "capture.png") : null;
40690
+ const screenshotDir = expectsScreenshot ? mkdtempSync3(join20(tmpdir5(), "gdharness-runtime-screenshot-")) : null;
40691
+ const screenshotPath = screenshotDir ? join20(screenshotDir, "capture.png") : null;
40623
40692
  try {
40624
40693
  const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs);
40625
40694
  if (!reply.ok) {
@@ -40643,7 +40712,7 @@ class GodotServer {
40643
40712
  type: "text",
40644
40713
  text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}`
40645
40714
  },
40646
- { type: "image", data: readFileSync17(screenshotPath).toString("base64"), mimeType: "image/png" }
40715
+ { type: "image", data: readFileSync18(screenshotPath).toString("base64"), mimeType: "image/png" }
40647
40716
  ]
40648
40717
  };
40649
40718
  } finally {
@@ -40710,11 +40779,12 @@ async function runGodotServer() {
40710
40779
  const server = new GodotServer;
40711
40780
  await server.run();
40712
40781
  }
40713
- 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/";
40782
+ 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/";
40714
40783
  var init_server3 = __esm(() => {
40715
40784
  init_mcp();
40716
40785
  init_stdio2();
40717
40786
  init_types();
40787
+ init_boot_note();
40718
40788
  init_breakpoint_note();
40719
40789
  init_bridge_announce();
40720
40790
  init_class_cache();
@@ -40744,8 +40814,9 @@ var init_server3 = __esm(() => {
40744
40814
  init_tool_definitions();
40745
40815
  init_update_check();
40746
40816
  run7 = promisify7(execFile7);
40747
- __dirname2 = dirname12(fileURLToPath4(import.meta.url));
40817
+ __dirname2 = dirname13(fileURLToPath4(import.meta.url));
40748
40818
  CONSOLE_WRAPPER = /_console(\.exe)?$/i;
40819
+ LONGEST_BOOT_NOTED_MS = 5 * 60000;
40749
40820
  VALUE_NAMES = {
40750
40821
  string: "a string",
40751
40822
  number: "a number",
@@ -40784,7 +40855,7 @@ var init_server3 = __esm(() => {
40784
40855
 
40785
40856
  // src/cli.ts
40786
40857
  import { existsSync as existsSync15 } from "fs";
40787
- import { join as join20, resolve as resolve5 } from "path";
40858
+ import { join as join21, resolve as resolve5 } from "path";
40788
40859
 
40789
40860
  // src/errors.ts
40790
40861
  class Refusal extends Error {
@@ -42489,7 +42560,7 @@ class UsageError extends Refusal {
42489
42560
  function projectArgument(at) {
42490
42561
  const named = args.slice(at).find((value) => !value.startsWith("--"));
42491
42562
  const projectPath = resolve5(named ?? ".");
42492
- if (!existsSync15(join20(projectPath, "project.godot"))) {
42563
+ if (!existsSync15(join21(projectPath, "project.godot"))) {
42493
42564
  throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
42494
42565
  }
42495
42566
  return projectPath;
@@ -42528,7 +42599,7 @@ async function registerRuntime(godot, projectPath) {
42528
42599
  return;
42529
42600
  }
42530
42601
  console.log(`${RUNTIME_AUTOLOAD.name} autoload left at ${named}, which this project registers itself`);
42531
- if (!existsSync15(join20(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
42602
+ if (!existsSync15(join21(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
42532
42603
  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}`);
42533
42604
  }
42534
42605
  }