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 +51 -7
- package/build/index.js +51 -5
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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",
|