gdharness 0.10.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/cli.js CHANGED
@@ -15680,7 +15680,7 @@ var init_tool_definitions = __esm(() => {
15680
15680
  },
15681
15681
  {
15682
15682
  name: "editor_run",
15683
- description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. What runs is a scene: a SceneTree script is not an entry point here, so put the script on the root of a scene of its own and name that in scene. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way.",
15683
+ description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. What runs is a scene: a SceneTree script is not an entry point here, so put the script on the root of a scene of its own and name that in scene. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way. A start waits for the game to become something the runtime_* tools can talk to and says which it is under runtime: listening with the port it took, or why not, so the first call after a start does not have to be made twice.",
15684
15684
  parameters: {
15685
15685
  projectPath: PROJECT_PATH,
15686
15686
  scene: { type: "string", description: "A scene to run instead of the main scene." },
@@ -35780,6 +35780,7 @@ import { existsSync as existsSync10, readdirSync as readdirSync5, readFileSync a
35780
35780
  import { createConnection as createConnection3 } from "node:net";
35781
35781
  import { tmpdir as tmpdir4 } from "node:os";
35782
35782
  import { join as join13, resolve as resolve4 } from "node:path";
35783
+ import { setTimeout as delay2 } from "node:timers/promises";
35783
35784
  function runtimeDirectory(variables = process.env) {
35784
35785
  const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
35785
35786
  if (explicit) {
@@ -35868,6 +35869,22 @@ function announcedIn(directory) {
35868
35869
  }
35869
35870
  return found;
35870
35871
  }
35872
+ async function announcedSince(projectPath, before, waiting = {}) {
35873
+ const wanted = resolve4(projectPath);
35874
+ const directories = waiting.directories ?? runtimeDirectories();
35875
+ const until = Date.now() + Math.max(waiting.budgetMs ?? ANNOUNCE_BUDGET_MS, 0);
35876
+ for (;; ) {
35877
+ const fresh = discoverRuntimes(directories).find((endpoint) => !before.has(endpoint.pid) && resolve4(endpoint.project.path) === wanted);
35878
+ if (fresh !== undefined) {
35879
+ return fresh;
35880
+ }
35881
+ const left = until - Date.now();
35882
+ if (left <= 0 || waiting.giveUp?.() === true) {
35883
+ return null;
35884
+ }
35885
+ await delay2(Math.min(LOOK_EVERY_MS, left));
35886
+ }
35887
+ }
35871
35888
  function describe2(endpoint) {
35872
35889
  return `pid ${endpoint.pid} on ${endpoint.address}:${endpoint.port} (${endpoint.project.name || "unnamed"} at ${endpoint.project.path})`;
35873
35890
  }
@@ -35994,7 +36011,7 @@ function runtimeRequest(endpoint, command, params, timeoutMs) {
35994
36011
  });
35995
36012
  });
35996
36013
  }
35997
- var RUNTIME_PROTOCOL = 2, ANNOUNCEMENT_PATTERN, nextRequestId = 1;
36014
+ var RUNTIME_PROTOCOL = 2, ANNOUNCEMENT_PATTERN, LOOK_EVERY_MS = 50, ANNOUNCE_BUDGET_MS = 5000, nextRequestId = 1;
35998
36015
  var init_runtime_client = __esm(() => {
35999
36016
  ANNOUNCEMENT_PATTERN = /^runtime-(\d+)\.json$/;
36000
36017
  });
@@ -36010,13 +36027,17 @@ function installedAddonVersion(projectPath) {
36010
36027
  const said = readFileSync12(marker, "utf8").trim();
36011
36028
  return said === "" ? null : said;
36012
36029
  }
36013
- var ADDONS2, VERSION_MARKER2 = ".gdharness-version";
36030
+ var ADDONS2, RUNTIME_AUTOLOAD2, VERSION_MARKER2 = ".gdharness-version";
36014
36031
  var init_setup = __esm(() => {
36015
36032
  init_class_cache();
36016
36033
  init_headless();
36017
36034
  init_resources();
36018
36035
  init_server_version();
36019
36036
  ADDONS2 = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
36037
+ RUNTIME_AUTOLOAD2 = {
36038
+ name: "GdharnessRuntime",
36039
+ path: "addons/gdharness_runtime/runtime_autoload.gd"
36040
+ };
36020
36041
  });
36021
36042
 
36022
36043
  // src/server.ts
@@ -36024,7 +36045,7 @@ import { execFile as execFile5, spawn } from "node:child_process";
36024
36045
  import { existsSync as existsSync12, mkdirSync as mkdirSync7, mkdtempSync as mkdtempSync3, readdirSync as readdirSync6, readFileSync as readFileSync13, realpathSync as realpathSync2, rmSync as rmSync8 } from "node:fs";
36025
36046
  import { tmpdir as tmpdir5 } from "node:os";
36026
36047
  import { basename as basename2, dirname as dirname9, join as join15, normalize as normalize3 } from "node:path";
36027
- import { setTimeout as delay2 } from "node:timers/promises";
36048
+ import { setTimeout as delay3 } from "node:timers/promises";
36028
36049
  import { fileURLToPath as fileURLToPath4 } from "node:url";
36029
36050
  import { promisify as promisify5 } from "node:util";
36030
36051
  function patienceForFrames(frames, atLeast) {
@@ -37341,7 +37362,7 @@ class GodotServer {
37341
37362
  if (Date.now() >= deadline) {
37342
37363
  return false;
37343
37364
  }
37344
- await delay2(250);
37365
+ await delay3(250);
37345
37366
  }
37346
37367
  }
37347
37368
  async handleLaunchEditor(args) {
@@ -37437,12 +37458,13 @@ class GodotServer {
37437
37458
  this.logDebug("Ending the running game before starting another");
37438
37459
  await this.endActiveGame();
37439
37460
  }
37461
+ const alreadyPlaying = new Set(discoverRuntimes().map((endpoint) => endpoint.pid));
37440
37462
  const headless = resolveHeadless(args["headless"], {
37441
37463
  platform: process.platform,
37442
37464
  variables: process.env
37443
37465
  });
37444
37466
  if (this.godotBridge.isConnected() && (!headless || editorPlaysHeadless(project.value.file))) {
37445
- return await this.playThroughEditor(sceneArgument, refreshed.value);
37467
+ return await this.playThroughEditor(sceneArgument, refreshed.value, project.value.path, alreadyPlaying);
37446
37468
  }
37447
37469
  const cmdArgs = runArguments({
37448
37470
  projectPath: project.value.path,
@@ -37458,10 +37480,31 @@ class GodotServer {
37458
37480
  pid: started.process.pid ?? null,
37459
37481
  arguments: cmdArgs,
37460
37482
  refreshedClasses: refreshed.value,
37483
+ runtime: await this.runtimeUp(project.value.path, alreadyPlaying),
37461
37484
  message: "Use editor_output for what it prints and editor_run stop to end it."
37462
37485
  });
37463
37486
  }
37464
- async playThroughEditor(scene, refreshedClasses) {
37487
+ async runtimeUp(projectPath, before) {
37488
+ if (!existsSync12(join15(projectPath, RUNTIME_AUTOLOAD2.path))) {
37489
+ return {
37490
+ listening: false,
37491
+ note: "this project has no gdharness_runtime addon, so the runtime_* tools have nothing to talk to"
37492
+ };
37493
+ }
37494
+ const endpoint = await announcedSince(projectPath, before, {
37495
+ giveUp: () => this.dapClient?.isStopped() === true
37496
+ });
37497
+ if (endpoint === null) {
37498
+ const held = this.dapClient?.whereItStopped() ?? null;
37499
+ return {
37500
+ listening: false,
37501
+ note: held === null ? `nothing announced itself within ${ANNOUNCE_BUDGET_MS}ms: the game may be slow to boot, or its runtime is not coming up` : "the game stopped before its runtime came up, so the runtime_* tools have nothing to talk to yet: debug_control continue lets it carry on",
37502
+ heldAt: held
37503
+ };
37504
+ }
37505
+ return { listening: true, pid: endpoint.pid, port: endpoint.port };
37506
+ }
37507
+ async playThroughEditor(scene, refreshedClasses, projectPath, alreadyPlaying) {
37465
37508
  const log = new GameLog;
37466
37509
  try {
37467
37510
  await this.dap().connect();
@@ -37490,6 +37533,7 @@ class GodotServer {
37490
37533
  scene: scene === null ? "the main scene" : `res://${scene}`,
37491
37534
  refreshedClasses,
37492
37535
  debugPort: readNumber(asParams(JSON.parse(answer.content[0]?.text ?? "{}")), "debugPort"),
37536
+ runtime: await this.runtimeUp(projectPath, alreadyPlaying),
37493
37537
  message: "The editor is playing it, so its debugger holds it: the debug_* tools can reach it, " + "editor_output reads its console through the debug adapter, and editor_run stop ends it."
37494
37538
  });
37495
37539
  }
package/build/index.js CHANGED
@@ -10129,7 +10129,7 @@ import { execFile as execFile3, spawn } from "node:child_process";
10129
10129
  import { existsSync as existsSync7, mkdirSync as mkdirSync4, mkdtempSync as mkdtempSync2, readdirSync as readdirSync5, readFileSync as readFileSync9, realpathSync as realpathSync2, rmSync as rmSync4 } from "node:fs";
10130
10130
  import { tmpdir as tmpdir4 } from "node:os";
10131
10131
  import { basename, dirname as dirname4, join as join9, normalize as normalize2 } from "node:path";
10132
- import { setTimeout as delay2 } from "node:timers/promises";
10132
+ import { setTimeout as delay3 } from "node:timers/promises";
10133
10133
  import { fileURLToPath as fileURLToPath2 } from "node:url";
10134
10134
  import { promisify as promisify3 } from "node:util";
10135
10135
 
@@ -27417,6 +27417,7 @@ import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as
27417
27417
  import { createConnection as createConnection3 } from "node:net";
27418
27418
  import { tmpdir as tmpdir3 } from "node:os";
27419
27419
  import { join as join7, resolve as resolve4 } from "node:path";
27420
+ import { setTimeout as delay2 } from "node:timers/promises";
27420
27421
 
27421
27422
  // src/tool-args.ts
27422
27423
  function asParams(value) {
@@ -27560,6 +27561,24 @@ function announcedIn(directory) {
27560
27561
  }
27561
27562
  return found;
27562
27563
  }
27564
+ var LOOK_EVERY_MS = 50;
27565
+ var ANNOUNCE_BUDGET_MS = 5000;
27566
+ async function announcedSince(projectPath, before, waiting = {}) {
27567
+ const wanted = resolve4(projectPath);
27568
+ const directories = waiting.directories ?? runtimeDirectories();
27569
+ const until = Date.now() + Math.max(waiting.budgetMs ?? ANNOUNCE_BUDGET_MS, 0);
27570
+ for (;; ) {
27571
+ const fresh = discoverRuntimes(directories).find((endpoint) => !before.has(endpoint.pid) && resolve4(endpoint.project.path) === wanted);
27572
+ if (fresh !== undefined) {
27573
+ return fresh;
27574
+ }
27575
+ const left = until - Date.now();
27576
+ if (left <= 0 || waiting.giveUp?.() === true) {
27577
+ return null;
27578
+ }
27579
+ await delay2(Math.min(LOOK_EVERY_MS, left));
27580
+ }
27581
+ }
27563
27582
  function describe2(endpoint) {
27564
27583
  return `pid ${endpoint.pid} on ${endpoint.address}:${endpoint.port} (${endpoint.project.name || "unnamed"} at ${endpoint.project.path})`;
27565
27584
  }
@@ -27692,6 +27711,10 @@ function runtimeRequest(endpoint, command, params, timeoutMs) {
27692
27711
  import { cpSync, existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync8, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "node:fs";
27693
27712
  import { dirname as dirname3, join as join8 } from "node:path";
27694
27713
  var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
27714
+ var RUNTIME_AUTOLOAD = {
27715
+ name: "GdharnessRuntime",
27716
+ path: "addons/gdharness_runtime/runtime_autoload.gd"
27717
+ };
27695
27718
  var VERSION_MARKER = ".gdharness-version";
27696
27719
  function installedAddonVersion(projectPath) {
27697
27720
  const marker = join8(projectPath, "addons", ADDONS[0], VERSION_MARKER);
@@ -28290,7 +28313,7 @@ var TOOL_SPECS = [
28290
28313
  },
28291
28314
  {
28292
28315
  name: "editor_run",
28293
- description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. What runs is a scene: a SceneTree script is not an entry point here, so put the script on the root of a scene of its own and name that in scene. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way.",
28316
+ description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. What runs is a scene: a SceneTree script is not an entry point here, so put the script on the root of a scene of its own and name that in scene. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way. A start waits for the game to become something the runtime_* tools can talk to and says which it is under runtime: listening with the port it took, or why not, so the first call after a start does not have to be made twice.",
28294
28317
  parameters: {
28295
28318
  projectPath: PROJECT_PATH,
28296
28319
  scene: { type: "string", description: "A scene to run instead of the main scene." },
@@ -30121,7 +30144,7 @@ class GodotServer {
30121
30144
  if (Date.now() >= deadline) {
30122
30145
  return false;
30123
30146
  }
30124
- await delay2(250);
30147
+ await delay3(250);
30125
30148
  }
30126
30149
  }
30127
30150
  async handleLaunchEditor(args) {
@@ -30217,12 +30240,13 @@ class GodotServer {
30217
30240
  this.logDebug("Ending the running game before starting another");
30218
30241
  await this.endActiveGame();
30219
30242
  }
30243
+ const alreadyPlaying = new Set(discoverRuntimes().map((endpoint) => endpoint.pid));
30220
30244
  const headless = resolveHeadless(args["headless"], {
30221
30245
  platform: process.platform,
30222
30246
  variables: process.env
30223
30247
  });
30224
30248
  if (this.godotBridge.isConnected() && (!headless || editorPlaysHeadless(project.value.file))) {
30225
- return await this.playThroughEditor(sceneArgument, refreshed.value);
30249
+ return await this.playThroughEditor(sceneArgument, refreshed.value, project.value.path, alreadyPlaying);
30226
30250
  }
30227
30251
  const cmdArgs = runArguments({
30228
30252
  projectPath: project.value.path,
@@ -30238,10 +30262,31 @@ class GodotServer {
30238
30262
  pid: started.process.pid ?? null,
30239
30263
  arguments: cmdArgs,
30240
30264
  refreshedClasses: refreshed.value,
30265
+ runtime: await this.runtimeUp(project.value.path, alreadyPlaying),
30241
30266
  message: "Use editor_output for what it prints and editor_run stop to end it."
30242
30267
  });
30243
30268
  }
30244
- async playThroughEditor(scene, refreshedClasses) {
30269
+ async runtimeUp(projectPath, before) {
30270
+ if (!existsSync7(join9(projectPath, RUNTIME_AUTOLOAD.path))) {
30271
+ return {
30272
+ listening: false,
30273
+ note: "this project has no gdharness_runtime addon, so the runtime_* tools have nothing to talk to"
30274
+ };
30275
+ }
30276
+ const endpoint = await announcedSince(projectPath, before, {
30277
+ giveUp: () => this.dapClient?.isStopped() === true
30278
+ });
30279
+ if (endpoint === null) {
30280
+ const held = this.dapClient?.whereItStopped() ?? null;
30281
+ return {
30282
+ listening: false,
30283
+ note: held === null ? `nothing announced itself within ${ANNOUNCE_BUDGET_MS}ms: the game may be slow to boot, or its runtime is not coming up` : "the game stopped before its runtime came up, so the runtime_* tools have nothing to talk to yet: debug_control continue lets it carry on",
30284
+ heldAt: held
30285
+ };
30286
+ }
30287
+ return { listening: true, pid: endpoint.pid, port: endpoint.port };
30288
+ }
30289
+ async playThroughEditor(scene, refreshedClasses, projectPath, alreadyPlaying) {
30245
30290
  const log = new GameLog;
30246
30291
  try {
30247
30292
  await this.dap().connect();
@@ -30270,6 +30315,7 @@ class GodotServer {
30270
30315
  scene: scene === null ? "the main scene" : `res://${scene}`,
30271
30316
  refreshedClasses,
30272
30317
  debugPort: readNumber(asParams(JSON.parse(answer.content[0]?.text ?? "{}")), "debugPort"),
30318
+ runtime: await this.runtimeUp(projectPath, alreadyPlaying),
30273
30319
  message: "The editor is playing it, so its debugger holds it: the debug_* tools can reach it, " + "editor_output reads its console through the debug adapter, and editor_run stop ends it."
30274
30320
  });
30275
30321
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "mcpName": "io.github.Aureliolo/gdharness",
5
5
  "description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
6
6
  "type": "module",