gdharness 0.13.9 → 0.13.11
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 +63 -9
- package/build/index.js +63 -9
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -14897,10 +14897,10 @@ function isValueComplete(value) {
|
|
|
14897
14897
|
return depth <= 0 && !inString;
|
|
14898
14898
|
}
|
|
14899
14899
|
function settingKeys(content) {
|
|
14900
|
-
const named = new
|
|
14900
|
+
const named = new Map;
|
|
14901
14901
|
for (const [section, entries] of Object.entries(parseProjectGodot(content))) {
|
|
14902
|
-
for (const key of Object.
|
|
14903
|
-
named.
|
|
14902
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
14903
|
+
named.set(`${section}/${key}`, value);
|
|
14904
14904
|
}
|
|
14905
14905
|
}
|
|
14906
14906
|
return named;
|
|
@@ -15787,7 +15787,10 @@ var init_tool_definitions = __esm(() => {
|
|
|
15787
15787
|
summary: "run the project until it quits or is stopped",
|
|
15788
15788
|
requires: ["projectPath"]
|
|
15789
15789
|
},
|
|
15790
|
-
stop: {
|
|
15790
|
+
stop: {
|
|
15791
|
+
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",
|
|
15792
|
+
requires: []
|
|
15793
|
+
},
|
|
15791
15794
|
check: {
|
|
15792
15795
|
summary: "boot headless, quit after a few frames, and report the verdict",
|
|
15793
15796
|
requires: ["projectPath"]
|
|
@@ -36452,6 +36455,34 @@ function runningAs(pid) {
|
|
|
36452
36455
|
return null;
|
|
36453
36456
|
}
|
|
36454
36457
|
}
|
|
36458
|
+
function listeningPid(port) {
|
|
36459
|
+
try {
|
|
36460
|
+
if (process.platform === "win32") {
|
|
36461
|
+
const said = execFileSync("powershell", [
|
|
36462
|
+
"-NoProfile",
|
|
36463
|
+
"-NonInteractive",
|
|
36464
|
+
"-Command",
|
|
36465
|
+
`(Get-NetTCPConnection -State Listen -LocalPort ${port} -ErrorAction SilentlyContinue | Select-Object -First 1).OwningProcess`
|
|
36466
|
+
], { encoding: "utf8", timeout: 15000 }).trim();
|
|
36467
|
+
return /^\d+$/.test(said) ? Number(said) : null;
|
|
36468
|
+
}
|
|
36469
|
+
for (const [command, args, pattern] of [
|
|
36470
|
+
["lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], /^(\d+)/],
|
|
36471
|
+
["ss", ["-ltnpH", `sport = :${port}`], /pid=(\d+)/]
|
|
36472
|
+
]) {
|
|
36473
|
+
try {
|
|
36474
|
+
const said = execFileSync(command, [...args], { encoding: "utf8", timeout: 15000 }).trim();
|
|
36475
|
+
const found = pattern.exec(said);
|
|
36476
|
+
if (found?.[1] !== undefined) {
|
|
36477
|
+
return Number(found[1]);
|
|
36478
|
+
}
|
|
36479
|
+
} catch {}
|
|
36480
|
+
}
|
|
36481
|
+
return null;
|
|
36482
|
+
} catch {
|
|
36483
|
+
return null;
|
|
36484
|
+
}
|
|
36485
|
+
}
|
|
36455
36486
|
function windowsCommandLine(pid) {
|
|
36456
36487
|
try {
|
|
36457
36488
|
const answer = execFileSync("powershell", [
|
|
@@ -37886,6 +37917,19 @@ class GodotServer {
|
|
|
37886
37917
|
async handleDAP(toolName, args) {
|
|
37887
37918
|
return handleDAPTool(this.dap(), toolName, args);
|
|
37888
37919
|
}
|
|
37920
|
+
adapterBelongsElsewhere() {
|
|
37921
|
+
const status = this.godotBridge.getStatus();
|
|
37922
|
+
const ours = status.editorPid;
|
|
37923
|
+
if (ours === undefined) {
|
|
37924
|
+
return null;
|
|
37925
|
+
}
|
|
37926
|
+
const port = this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT);
|
|
37927
|
+
const holder = listeningPid(port);
|
|
37928
|
+
if (holder === null || holder === ours) {
|
|
37929
|
+
return null;
|
|
37930
|
+
}
|
|
37931
|
+
return `The debug adapter on port ${port} belongs to process ${holder}, and the editor on this server's bridge is process ${ours}. Godot gives every editor the same debug adapter port by default, so the one this editor names in its settings is not necessarily the one it holds. Playing the game now would put another editor's console and another project's debugger under this project's name.`;
|
|
37932
|
+
}
|
|
37889
37933
|
dap() {
|
|
37890
37934
|
const port = this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT);
|
|
37891
37935
|
if (this.dapClient !== null && this.dapClient.port !== port) {
|
|
@@ -37989,12 +38033,12 @@ class GodotServer {
|
|
|
37989
38033
|
}
|
|
37990
38034
|
settingKeysOf(projectPath) {
|
|
37991
38035
|
if (projectPath === undefined) {
|
|
37992
|
-
return new
|
|
38036
|
+
return new Map;
|
|
37993
38037
|
}
|
|
37994
38038
|
try {
|
|
37995
38039
|
return settingKeys(readFileSync15(join17(projectPath, "project.godot"), "utf8"));
|
|
37996
38040
|
} catch {
|
|
37997
|
-
return new
|
|
38041
|
+
return new Map;
|
|
37998
38042
|
}
|
|
37999
38043
|
}
|
|
38000
38044
|
async handleRestartEditor() {
|
|
@@ -38026,7 +38070,7 @@ class GodotServer {
|
|
|
38026
38070
|
}
|
|
38027
38071
|
const now = this.godotBridge.getStatus();
|
|
38028
38072
|
const after = this.settingKeysOf(before.projectPath);
|
|
38029
|
-
const dropped = [...settingsBefore].filter((key) => !after.has(key));
|
|
38073
|
+
const dropped = [...settingsBefore].filter(([key]) => !after.has(key));
|
|
38030
38074
|
return this.jsonTextResponse({
|
|
38031
38075
|
restarted: true,
|
|
38032
38076
|
editorPid: now.editorPid,
|
|
@@ -38034,8 +38078,8 @@ class GodotServer {
|
|
|
38034
38078
|
serverVersion: SERVER_VERSION,
|
|
38035
38079
|
addonIsStale: now.addonVersion !== SERVER_VERSION,
|
|
38036
38080
|
staleNote: addonMismatch(now.addonVersion, SERVER_VERSION),
|
|
38037
|
-
settingsDropped: dropped.length > 0 ? dropped : undefined,
|
|
38038
|
-
settingsNote: dropped.length > 0 ?
|
|
38081
|
+
settingsDropped: dropped.length > 0 ? dropped.map(([setting, was]) => ({ setting, was })) : undefined,
|
|
38082
|
+
settingsNote: dropped.length > 0 ? `The editor saved project.godot on its way out and these keys are no longer in it, with the value each one held. Godot writes only what differs from its own defaults, so a key named deliberately at its default value is redundant to the editor and is dropped on save; it will be dropped again on the next restart. If any of them were pinned on purpose, to keep "set to this on purpose" and "not set" apart, project_settings set puts one back: ${dropped.map(([setting, was]) => `setting "${setting}" value ${JSON.stringify(was)}`).join(", ")}. Nothing else will say they have gone until something depends on one.` : undefined,
|
|
38039
38083
|
tookMs: Date.now() - began
|
|
38040
38084
|
});
|
|
38041
38085
|
}
|
|
@@ -38233,6 +38277,14 @@ class GodotServer {
|
|
|
38233
38277
|
"GDHARNESS_DAP_PORT points this server at another one"
|
|
38234
38278
|
]);
|
|
38235
38279
|
}
|
|
38280
|
+
const stranger = this.adapterBelongsElsewhere();
|
|
38281
|
+
if (stranger !== null) {
|
|
38282
|
+
return this.createErrorResponse(stranger, [
|
|
38283
|
+
"editor_status names the editor on the bridge and the port it says it serves",
|
|
38284
|
+
"GDHARNESS_DAP_PORT points this server at the right one",
|
|
38285
|
+
"Closing the other editor, or moving its debug adapter port in its editor settings, frees this one"
|
|
38286
|
+
]);
|
|
38287
|
+
}
|
|
38236
38288
|
const answer = await this.handleViaBridge("play_scene", scene === null ? {} : { scenePath: `res://${scene}` });
|
|
38237
38289
|
if (answer.isError === true) {
|
|
38238
38290
|
return answer;
|
|
@@ -38652,11 +38704,13 @@ class GodotServer {
|
|
|
38652
38704
|
return this.jsonTextResponse({
|
|
38653
38705
|
stopped: true,
|
|
38654
38706
|
through: stopped.throughEditor ? "editor" : "gdharness",
|
|
38707
|
+
endedPid: stopped.pid,
|
|
38655
38708
|
exitedBeforeStop: stopped.exitCode !== null,
|
|
38656
38709
|
exitCode: stopped.exitCode,
|
|
38657
38710
|
errors: stopped.log.count("error"),
|
|
38658
38711
|
warnings: stopped.log.count("warning"),
|
|
38659
38712
|
clean: stopped.log.count("error") === 0,
|
|
38713
|
+
note: stopped.throughEditor ? "The editor was asked to stop the scene it is playing. Anything that game started for itself, with OS.create_process or otherwise, is not the editor's to stop and is still running." : "The process named under endedPid was ended. Anything that game started for itself, with OS.create_process or otherwise, is a separate process and is still running.",
|
|
38660
38714
|
entries: forAnswer(stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
|
|
38661
38715
|
});
|
|
38662
38716
|
}
|
package/build/index.js
CHANGED
|
@@ -27558,10 +27558,10 @@ function isValueComplete(value) {
|
|
|
27558
27558
|
return depth <= 0 && !inString;
|
|
27559
27559
|
}
|
|
27560
27560
|
function settingKeys(content) {
|
|
27561
|
-
const named = new
|
|
27561
|
+
const named = new Map;
|
|
27562
27562
|
for (const [section, entries] of Object.entries(parseProjectGodot(content))) {
|
|
27563
|
-
for (const key of Object.
|
|
27564
|
-
named.
|
|
27563
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
27564
|
+
named.set(`${section}/${key}`, value);
|
|
27565
27565
|
}
|
|
27566
27566
|
}
|
|
27567
27567
|
return named;
|
|
@@ -28099,6 +28099,34 @@ function runningAs(pid) {
|
|
|
28099
28099
|
return null;
|
|
28100
28100
|
}
|
|
28101
28101
|
}
|
|
28102
|
+
function listeningPid(port) {
|
|
28103
|
+
try {
|
|
28104
|
+
if (process.platform === "win32") {
|
|
28105
|
+
const said = execFileSync("powershell", [
|
|
28106
|
+
"-NoProfile",
|
|
28107
|
+
"-NonInteractive",
|
|
28108
|
+
"-Command",
|
|
28109
|
+
`(Get-NetTCPConnection -State Listen -LocalPort ${port} -ErrorAction SilentlyContinue | Select-Object -First 1).OwningProcess`
|
|
28110
|
+
], { encoding: "utf8", timeout: 15000 }).trim();
|
|
28111
|
+
return /^\d+$/.test(said) ? Number(said) : null;
|
|
28112
|
+
}
|
|
28113
|
+
for (const [command, args, pattern] of [
|
|
28114
|
+
["lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], /^(\d+)/],
|
|
28115
|
+
["ss", ["-ltnpH", `sport = :${port}`], /pid=(\d+)/]
|
|
28116
|
+
]) {
|
|
28117
|
+
try {
|
|
28118
|
+
const said = execFileSync(command, [...args], { encoding: "utf8", timeout: 15000 }).trim();
|
|
28119
|
+
const found = pattern.exec(said);
|
|
28120
|
+
if (found?.[1] !== undefined) {
|
|
28121
|
+
return Number(found[1]);
|
|
28122
|
+
}
|
|
28123
|
+
} catch {}
|
|
28124
|
+
}
|
|
28125
|
+
return null;
|
|
28126
|
+
} catch {
|
|
28127
|
+
return null;
|
|
28128
|
+
}
|
|
28129
|
+
}
|
|
28102
28130
|
function windowsCommandLine(pid) {
|
|
28103
28131
|
try {
|
|
28104
28132
|
const answer = execFileSync("powershell", [
|
|
@@ -28849,7 +28877,10 @@ var TOOL_SPECS = [
|
|
|
28849
28877
|
summary: "run the project until it quits or is stopped",
|
|
28850
28878
|
requires: ["projectPath"]
|
|
28851
28879
|
},
|
|
28852
|
-
stop: {
|
|
28880
|
+
stop: {
|
|
28881
|
+
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",
|
|
28882
|
+
requires: []
|
|
28883
|
+
},
|
|
28853
28884
|
check: {
|
|
28854
28885
|
summary: "boot headless, quit after a few frames, and report the verdict",
|
|
28855
28886
|
requires: ["projectPath"]
|
|
@@ -30640,6 +30671,19 @@ class GodotServer {
|
|
|
30640
30671
|
async handleDAP(toolName, args) {
|
|
30641
30672
|
return handleDAPTool(this.dap(), toolName, args);
|
|
30642
30673
|
}
|
|
30674
|
+
adapterBelongsElsewhere() {
|
|
30675
|
+
const status = this.godotBridge.getStatus();
|
|
30676
|
+
const ours = status.editorPid;
|
|
30677
|
+
if (ours === undefined) {
|
|
30678
|
+
return null;
|
|
30679
|
+
}
|
|
30680
|
+
const port = this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT);
|
|
30681
|
+
const holder = listeningPid(port);
|
|
30682
|
+
if (holder === null || holder === ours) {
|
|
30683
|
+
return null;
|
|
30684
|
+
}
|
|
30685
|
+
return `The debug adapter on port ${port} belongs to process ${holder}, and the editor on this server's bridge is process ${ours}. Godot gives every editor the same debug adapter port by default, so the one this editor names in its settings is not necessarily the one it holds. Playing the game now would put another editor's console and another project's debugger under this project's name.`;
|
|
30686
|
+
}
|
|
30643
30687
|
dap() {
|
|
30644
30688
|
const port = this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT);
|
|
30645
30689
|
if (this.dapClient !== null && this.dapClient.port !== port) {
|
|
@@ -30743,12 +30787,12 @@ class GodotServer {
|
|
|
30743
30787
|
}
|
|
30744
30788
|
settingKeysOf(projectPath) {
|
|
30745
30789
|
if (projectPath === undefined) {
|
|
30746
|
-
return new
|
|
30790
|
+
return new Map;
|
|
30747
30791
|
}
|
|
30748
30792
|
try {
|
|
30749
30793
|
return settingKeys(readFileSync11(join11(projectPath, "project.godot"), "utf8"));
|
|
30750
30794
|
} catch {
|
|
30751
|
-
return new
|
|
30795
|
+
return new Map;
|
|
30752
30796
|
}
|
|
30753
30797
|
}
|
|
30754
30798
|
async handleRestartEditor() {
|
|
@@ -30780,7 +30824,7 @@ class GodotServer {
|
|
|
30780
30824
|
}
|
|
30781
30825
|
const now = this.godotBridge.getStatus();
|
|
30782
30826
|
const after = this.settingKeysOf(before.projectPath);
|
|
30783
|
-
const dropped = [...settingsBefore].filter((key) => !after.has(key));
|
|
30827
|
+
const dropped = [...settingsBefore].filter(([key]) => !after.has(key));
|
|
30784
30828
|
return this.jsonTextResponse({
|
|
30785
30829
|
restarted: true,
|
|
30786
30830
|
editorPid: now.editorPid,
|
|
@@ -30788,8 +30832,8 @@ class GodotServer {
|
|
|
30788
30832
|
serverVersion: SERVER_VERSION,
|
|
30789
30833
|
addonIsStale: now.addonVersion !== SERVER_VERSION,
|
|
30790
30834
|
staleNote: addonMismatch(now.addonVersion, SERVER_VERSION),
|
|
30791
|
-
settingsDropped: dropped.length > 0 ? dropped : undefined,
|
|
30792
|
-
settingsNote: dropped.length > 0 ?
|
|
30835
|
+
settingsDropped: dropped.length > 0 ? dropped.map(([setting, was]) => ({ setting, was })) : undefined,
|
|
30836
|
+
settingsNote: dropped.length > 0 ? `The editor saved project.godot on its way out and these keys are no longer in it, with the value each one held. Godot writes only what differs from its own defaults, so a key named deliberately at its default value is redundant to the editor and is dropped on save; it will be dropped again on the next restart. If any of them were pinned on purpose, to keep "set to this on purpose" and "not set" apart, project_settings set puts one back: ${dropped.map(([setting, was]) => `setting "${setting}" value ${JSON.stringify(was)}`).join(", ")}. Nothing else will say they have gone until something depends on one.` : undefined,
|
|
30793
30837
|
tookMs: Date.now() - began
|
|
30794
30838
|
});
|
|
30795
30839
|
}
|
|
@@ -30987,6 +31031,14 @@ class GodotServer {
|
|
|
30987
31031
|
"GDHARNESS_DAP_PORT points this server at another one"
|
|
30988
31032
|
]);
|
|
30989
31033
|
}
|
|
31034
|
+
const stranger = this.adapterBelongsElsewhere();
|
|
31035
|
+
if (stranger !== null) {
|
|
31036
|
+
return this.createErrorResponse(stranger, [
|
|
31037
|
+
"editor_status names the editor on the bridge and the port it says it serves",
|
|
31038
|
+
"GDHARNESS_DAP_PORT points this server at the right one",
|
|
31039
|
+
"Closing the other editor, or moving its debug adapter port in its editor settings, frees this one"
|
|
31040
|
+
]);
|
|
31041
|
+
}
|
|
30990
31042
|
const answer = await this.handleViaBridge("play_scene", scene === null ? {} : { scenePath: `res://${scene}` });
|
|
30991
31043
|
if (answer.isError === true) {
|
|
30992
31044
|
return answer;
|
|
@@ -31406,11 +31458,13 @@ class GodotServer {
|
|
|
31406
31458
|
return this.jsonTextResponse({
|
|
31407
31459
|
stopped: true,
|
|
31408
31460
|
through: stopped.throughEditor ? "editor" : "gdharness",
|
|
31461
|
+
endedPid: stopped.pid,
|
|
31409
31462
|
exitedBeforeStop: stopped.exitCode !== null,
|
|
31410
31463
|
exitCode: stopped.exitCode,
|
|
31411
31464
|
errors: stopped.log.count("error"),
|
|
31412
31465
|
warnings: stopped.log.count("warning"),
|
|
31413
31466
|
clean: stopped.log.count("error") === 0,
|
|
31467
|
+
note: stopped.throughEditor ? "The editor was asked to stop the scene it is playing. Anything that game started for itself, with OS.create_process or otherwise, is not the editor's to stop and is still running." : "The process named under endedPid was ended. Anything that game started for itself, with OS.create_process or otherwise, is a separate process and is still running.",
|
|
31414
31468
|
entries: forAnswer(stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
|
|
31415
31469
|
});
|
|
31416
31470
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.11",
|
|
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",
|