gdharness 0.13.20 → 0.13.22
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
|
@@ -7591,6 +7591,15 @@ function unloadedTypes(messages, declared, cached) {
|
|
|
7591
7591
|
}
|
|
7592
7592
|
return found;
|
|
7593
7593
|
}
|
|
7594
|
+
function classesNamedIn(source, known) {
|
|
7595
|
+
const named = new Set;
|
|
7596
|
+
for (const [word] of source.matchAll(/\b[A-Z][A-Za-z0-9_]*\b/g)) {
|
|
7597
|
+
if (known.has(word)) {
|
|
7598
|
+
named.add(word);
|
|
7599
|
+
}
|
|
7600
|
+
}
|
|
7601
|
+
return [...named].sort();
|
|
7602
|
+
}
|
|
7594
7603
|
function contradictedDiagnostics(messages, classes, sourceOf) {
|
|
7595
7604
|
const read = new Map;
|
|
7596
7605
|
const found = [];
|
|
@@ -15955,7 +15964,11 @@ var init_tool_definitions = __esm(() => {
|
|
|
15955
15964
|
description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. The scan is a change-detecting walk rather than an unconditional reparse, so a file another engine has already imported reads as settled and the walk does not look inside it: its class_name then stays out of the list the editor resolves against, however many times you scan. Any class in that state is named under unseenByEditor, which is what no check on disk can see, since the declaration and the cache are both correct there and only the editor disagrees. The cure measured downstream is a change to the declaring script, saved, and then a rescan: the change on its own left every affected file reporting the same errors, and the rescan after it cleared them. Whether a rescan alone would have done it there is not known, because the rescans in that report may have answered before the scan ran, which is the timing this tool no longer has. editor_launch restart also does it, and project_import refresh_classes does not: it rewrites the cache and does not touch what the editor is holding. A scan writes .godot/global_script_class_cache.cfg from the list the editor is holding, so a class the editor cannot resolve goes out of that file with it. That is not a reason to refuse the scan, because on a class the editor has merely not walked yet the scan is the cure; what the answer does instead is name the loss under cacheLost and rebuild the cache from the files, naming what came back under cacheRestored, so no fresh engine, CI run or clone inherits the short file. An editor that lost classes is still holding the short list, so the note says to restart it before scanning again. Needs the editor connected.",
|
|
15956
15965
|
parameters: {
|
|
15957
15966
|
projectPath: PROJECT_PATH,
|
|
15958
|
-
timeoutMs: { type: "number", description: "How long to wait for the scan. Default 30000." }
|
|
15967
|
+
timeoutMs: { type: "number", description: "How long to wait for the scan. Default 30000." },
|
|
15968
|
+
reloadScript: {
|
|
15969
|
+
type: "string",
|
|
15970
|
+
description: "A script to recompile in the editor after the scan, for the fault a scan cannot reach: a script the editor has loaded keeps the copy it built, and that copy is refreshed when one of its dependencies changes rather than when it changes itself, so a method added to a loaded class reads as missing at every caller. This recompiles into the same object, so the holders that kept the stale copy alive see the new one. The methods it has afterwards come back under reloadedMethods, because a reload that compiled nothing and answered OK is the failure worth catching."
|
|
15971
|
+
}
|
|
15959
15972
|
},
|
|
15960
15973
|
requires: ["projectPath"]
|
|
15961
15974
|
},
|
|
@@ -38105,7 +38118,19 @@ class GodotServer {
|
|
|
38105
38118
|
const unloaded = messages.some((message) => unknownTypeIn(message) !== null) ? unloadedTypes(messages, declaredClasses(projectPath), cached) : [];
|
|
38106
38119
|
const notes = [];
|
|
38107
38120
|
if (contradicted.length > 0) {
|
|
38108
|
-
|
|
38121
|
+
const dependsOn = new Set;
|
|
38122
|
+
for (const one of contradicted) {
|
|
38123
|
+
const source = cached === null ? null : this.sourceOfClass(projectPath, one.declaredIn);
|
|
38124
|
+
if (source === null || cached === null) {
|
|
38125
|
+
continue;
|
|
38126
|
+
}
|
|
38127
|
+
for (const name of classesNamedIn(source, cached)) {
|
|
38128
|
+
if (name !== one.type) {
|
|
38129
|
+
dependsOn.add(name);
|
|
38130
|
+
}
|
|
38131
|
+
}
|
|
38132
|
+
}
|
|
38133
|
+
notes.push(`The editor is reporting against an older copy of ${contradicted.length === 1 ? "a type" : "some types"} named under contradictedByTheFile. Each member listed is declared in the file the class cache points at, so those diagnostics are wrong however the code is written. Try editor_rescan first: it costs about half a second and cleared this in one of five measured attempts. If it does not, the lever is that a held type is refreshed when something it depends on changes rather than when it changes itself${dependsOn.size === 0 ? ", though the named types depend on no other global class, which leaves only the restart" : `, so change ${[...dependsOn].sort().join(", ")} and rescan again`}. editor_launch restart is the only remedy that has always worked, and is the one to reach for when the count above is not good enough odds. project_import refresh_classes does not, and answers added: [] while this is happening.`);
|
|
38109
38134
|
}
|
|
38110
38135
|
const uncached = unloaded.filter((type) => !type.inTheClassCache);
|
|
38111
38136
|
if (uncached.length > 0) {
|
|
@@ -38121,6 +38146,17 @@ class GodotServer {
|
|
|
38121
38146
|
...notes.length === 0 ? {} : { staleAnalysis: notes.join(" ") }
|
|
38122
38147
|
};
|
|
38123
38148
|
}
|
|
38149
|
+
sourceOfClass(projectPath, resourcePath) {
|
|
38150
|
+
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
38151
|
+
if (!contained.ok || !existsSync14(contained.absolutePath)) {
|
|
38152
|
+
return null;
|
|
38153
|
+
}
|
|
38154
|
+
try {
|
|
38155
|
+
return readFileSync15(contained.absolutePath, "utf8");
|
|
38156
|
+
} catch {
|
|
38157
|
+
return null;
|
|
38158
|
+
}
|
|
38159
|
+
}
|
|
38124
38160
|
async handleLSP(toolName, args) {
|
|
38125
38161
|
const port = this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT);
|
|
38126
38162
|
if (this.lspClient !== null && this.lspClient.port !== port) {
|
|
@@ -38958,6 +38994,17 @@ class GodotServer {
|
|
|
38958
38994
|
const after = projectPath === "" || busy ? null : cachedClasses(projectPath);
|
|
38959
38995
|
const lost = before === null || after === null ? [] : [...before.keys()].filter((name) => !after.has(name));
|
|
38960
38996
|
const restored = lost.length > 0 ? await this.rebuildCacheAfterLoss(projectPath, lost) : [];
|
|
38997
|
+
const reloading = readNonEmptyString(args, "reloadScript");
|
|
38998
|
+
let reloaded = {};
|
|
38999
|
+
if (reloading !== undefined) {
|
|
39000
|
+
try {
|
|
39001
|
+
const answer = asParams(await this.godotBridge.invokeTool("reload_script", { ...args, scriptPath: reloading }));
|
|
39002
|
+
const methods = readArray(answer, "methods");
|
|
39003
|
+
reloaded = methods ? { methods: methods.map(String) } : { problem: `the editor would not say what ${reloading} has after reloading it` };
|
|
39004
|
+
} catch (error) {
|
|
39005
|
+
reloaded = { problem: `${reloading} could not be reloaded: ${errorMessage(error)}` };
|
|
39006
|
+
}
|
|
39007
|
+
}
|
|
38961
39008
|
const checked = busy ? { unseen: [] } : await this.classesTheEditorCannotSee(args);
|
|
38962
39009
|
const unseen = checked.unseen;
|
|
38963
39010
|
const stillGone = lost.filter((name) => !restored.includes(name));
|
|
@@ -38965,6 +39012,8 @@ class GodotServer {
|
|
|
38965
39012
|
ok: !busy && unseen.length === 0 && stillGone.length === 0 && checked.unchecked === undefined,
|
|
38966
39013
|
stillWorking: busy,
|
|
38967
39014
|
waitedMs: Date.now() - started,
|
|
39015
|
+
reloadedMethods: reloaded.methods,
|
|
39016
|
+
reloadProblem: reloaded.problem,
|
|
38968
39017
|
unseenByEditor: unseen.length > 0 ? unseen : undefined,
|
|
38969
39018
|
cacheLost: lost.length > 0 ? lost : undefined,
|
|
38970
39019
|
cacheRestored: restored.length > 0 ? restored : undefined,
|
|
@@ -80,6 +80,7 @@ func _init_tools() -> void:
|
|
|
80
80
|
"list_connections": [_scene_tools, "list_connections"],
|
|
81
81
|
"rescan_filesystem": [_scene_tools, "rescan_filesystem"],
|
|
82
82
|
"global_classes": [_class_tools, "global_classes"],
|
|
83
|
+
"reload_script": [_class_tools, "reload_script"],
|
|
83
84
|
# Settings, for a caller that asked the editor rather than the file on disk
|
|
84
85
|
"get_project_setting": [_settings_tools, "get_project_setting"],
|
|
85
86
|
# Resource tools
|
|
@@ -29,6 +29,59 @@ func set_editor_plugin(plugin: EditorPlugin) -> void:
|
|
|
29
29
|
_editor_plugin = plugin
|
|
30
30
|
|
|
31
31
|
|
|
32
|
+
## Recompiles a script the editor is already holding, in place.
|
|
33
|
+
##
|
|
34
|
+
## The fault this exists for: a script the editor has loaded keeps the copy it compiled, and that
|
|
35
|
+
## copy is refreshed when one of its own dependencies changes rather than when it changes itself.
|
|
36
|
+
## So a method added to a loaded class is reported missing at every caller until something
|
|
37
|
+
## recompiles it, and a filesystem scan does not, because the scan updates what the editor knows
|
|
38
|
+
## about files rather than what it has already built from them.
|
|
39
|
+
##
|
|
40
|
+
## `reload` is the only call that reaches the built copy. It recompiles from source into the same
|
|
41
|
+
## object, so everything holding a reference sees the new version rather than a second one, which
|
|
42
|
+
## is the whole point: the holders are why the stale copy survived a scan.
|
|
43
|
+
##
|
|
44
|
+
## `keep_state` is true so instances that already exist keep their properties. A caller asking for
|
|
45
|
+
## this is mid-edit with a scene open, and dropping the state of every node using the script would
|
|
46
|
+
## be a larger surprise than the wrong diagnostic it is here to clear.
|
|
47
|
+
##
|
|
48
|
+
## The list of methods afterwards is the evidence rather than an `ok` beside it. A reload that
|
|
49
|
+
## returned OK and compiled nothing is exactly the failure worth catching, and the members it now
|
|
50
|
+
## has are the only thing that can tell those apart.
|
|
51
|
+
func reload_script(args: Dictionary) -> Dictionary:
|
|
52
|
+
var path: String = str(args.get("scriptPath", ""))
|
|
53
|
+
if path.is_empty():
|
|
54
|
+
return {"ok": false, "error": "scriptPath is required"}
|
|
55
|
+
if not ResourceLoader.exists(path):
|
|
56
|
+
return {"ok": false, "error": "No script at " + path}
|
|
57
|
+
|
|
58
|
+
var loaded: Resource = load(path)
|
|
59
|
+
var script: GDScript = loaded as GDScript
|
|
60
|
+
if script == null:
|
|
61
|
+
return {"ok": false, "error": path + " is not a GDScript"}
|
|
62
|
+
|
|
63
|
+
# The source is read off disk and put back before reloading, which is the whole of the fix and
|
|
64
|
+
# is not obvious. `reload` recompiles from the object's own `source_code`, not from the file, so
|
|
65
|
+
# a script the editor loaded before the change recompiles the text it was holding and comes back
|
|
66
|
+
# with exactly the members it already had. Measured: reloading without this answers OK and the
|
|
67
|
+
# method added since is still absent from the method list.
|
|
68
|
+
var reader: FileAccess = FileAccess.open(path, FileAccess.READ)
|
|
69
|
+
if reader == null:
|
|
70
|
+
return {"ok": false, "error": "Could not read " + path + ": " + str(FileAccess.get_open_error())}
|
|
71
|
+
script.source_code = reader.get_as_text()
|
|
72
|
+
reader.close()
|
|
73
|
+
|
|
74
|
+
var failed: Error = script.reload(true)
|
|
75
|
+
if failed != OK:
|
|
76
|
+
return {"ok": false, "error": "Reloading " + path + " answered error " + str(failed)}
|
|
77
|
+
|
|
78
|
+
var methods: Array[String] = []
|
|
79
|
+
for entry: Dictionary in script.get_script_method_list():
|
|
80
|
+
methods.append(str(entry.get("name", "")))
|
|
81
|
+
methods.sort()
|
|
82
|
+
return {"ok": true, "script": path, "methods": methods}
|
|
83
|
+
|
|
84
|
+
|
|
32
85
|
func global_classes(_args: Dictionary) -> Dictionary:
|
|
33
86
|
if not _editor_plugin:
|
|
34
87
|
return {"ok": false, "error": "Editor plugin unavailable"}
|
package/build/index.js
CHANGED
|
@@ -24851,6 +24851,15 @@ function unloadedTypes(messages, declared, cached) {
|
|
|
24851
24851
|
}
|
|
24852
24852
|
return found;
|
|
24853
24853
|
}
|
|
24854
|
+
function classesNamedIn(source, known) {
|
|
24855
|
+
const named = new Set;
|
|
24856
|
+
for (const [word] of source.matchAll(/\b[A-Z][A-Za-z0-9_]*\b/g)) {
|
|
24857
|
+
if (known.has(word)) {
|
|
24858
|
+
named.add(word);
|
|
24859
|
+
}
|
|
24860
|
+
}
|
|
24861
|
+
return [...named].sort();
|
|
24862
|
+
}
|
|
24854
24863
|
function contradictedDiagnostics(messages, classes, sourceOf) {
|
|
24855
24864
|
const read = new Map;
|
|
24856
24865
|
const found = [];
|
|
@@ -29072,7 +29081,11 @@ var TOOL_SPECS = [
|
|
|
29072
29081
|
description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. The scan is a change-detecting walk rather than an unconditional reparse, so a file another engine has already imported reads as settled and the walk does not look inside it: its class_name then stays out of the list the editor resolves against, however many times you scan. Any class in that state is named under unseenByEditor, which is what no check on disk can see, since the declaration and the cache are both correct there and only the editor disagrees. The cure measured downstream is a change to the declaring script, saved, and then a rescan: the change on its own left every affected file reporting the same errors, and the rescan after it cleared them. Whether a rescan alone would have done it there is not known, because the rescans in that report may have answered before the scan ran, which is the timing this tool no longer has. editor_launch restart also does it, and project_import refresh_classes does not: it rewrites the cache and does not touch what the editor is holding. A scan writes .godot/global_script_class_cache.cfg from the list the editor is holding, so a class the editor cannot resolve goes out of that file with it. That is not a reason to refuse the scan, because on a class the editor has merely not walked yet the scan is the cure; what the answer does instead is name the loss under cacheLost and rebuild the cache from the files, naming what came back under cacheRestored, so no fresh engine, CI run or clone inherits the short file. An editor that lost classes is still holding the short list, so the note says to restart it before scanning again. Needs the editor connected.",
|
|
29073
29082
|
parameters: {
|
|
29074
29083
|
projectPath: PROJECT_PATH,
|
|
29075
|
-
timeoutMs: { type: "number", description: "How long to wait for the scan. Default 30000." }
|
|
29084
|
+
timeoutMs: { type: "number", description: "How long to wait for the scan. Default 30000." },
|
|
29085
|
+
reloadScript: {
|
|
29086
|
+
type: "string",
|
|
29087
|
+
description: "A script to recompile in the editor after the scan, for the fault a scan cannot reach: a script the editor has loaded keeps the copy it built, and that copy is refreshed when one of its dependencies changes rather than when it changes itself, so a method added to a loaded class reads as missing at every caller. This recompiles into the same object, so the holders that kept the stale copy alive see the new one. The methods it has afterwards come back under reloadedMethods, because a reload that compiled nothing and answered OK is the failure worth catching."
|
|
29088
|
+
}
|
|
29076
29089
|
},
|
|
29077
29090
|
requires: ["projectPath"]
|
|
29078
29091
|
},
|
|
@@ -30857,7 +30870,19 @@ class GodotServer {
|
|
|
30857
30870
|
const unloaded = messages.some((message) => unknownTypeIn(message) !== null) ? unloadedTypes(messages, declaredClasses(projectPath), cached) : [];
|
|
30858
30871
|
const notes = [];
|
|
30859
30872
|
if (contradicted.length > 0) {
|
|
30860
|
-
|
|
30873
|
+
const dependsOn = new Set;
|
|
30874
|
+
for (const one of contradicted) {
|
|
30875
|
+
const source = cached === null ? null : this.sourceOfClass(projectPath, one.declaredIn);
|
|
30876
|
+
if (source === null || cached === null) {
|
|
30877
|
+
continue;
|
|
30878
|
+
}
|
|
30879
|
+
for (const name of classesNamedIn(source, cached)) {
|
|
30880
|
+
if (name !== one.type) {
|
|
30881
|
+
dependsOn.add(name);
|
|
30882
|
+
}
|
|
30883
|
+
}
|
|
30884
|
+
}
|
|
30885
|
+
notes.push(`The editor is reporting against an older copy of ${contradicted.length === 1 ? "a type" : "some types"} named under contradictedByTheFile. Each member listed is declared in the file the class cache points at, so those diagnostics are wrong however the code is written. Try editor_rescan first: it costs about half a second and cleared this in one of five measured attempts. If it does not, the lever is that a held type is refreshed when something it depends on changes rather than when it changes itself${dependsOn.size === 0 ? ", though the named types depend on no other global class, which leaves only the restart" : `, so change ${[...dependsOn].sort().join(", ")} and rescan again`}. editor_launch restart is the only remedy that has always worked, and is the one to reach for when the count above is not good enough odds. project_import refresh_classes does not, and answers added: [] while this is happening.`);
|
|
30861
30886
|
}
|
|
30862
30887
|
const uncached = unloaded.filter((type) => !type.inTheClassCache);
|
|
30863
30888
|
if (uncached.length > 0) {
|
|
@@ -30873,6 +30898,17 @@ class GodotServer {
|
|
|
30873
30898
|
...notes.length === 0 ? {} : { staleAnalysis: notes.join(" ") }
|
|
30874
30899
|
};
|
|
30875
30900
|
}
|
|
30901
|
+
sourceOfClass(projectPath, resourcePath) {
|
|
30902
|
+
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
30903
|
+
if (!contained.ok || !existsSync9(contained.absolutePath)) {
|
|
30904
|
+
return null;
|
|
30905
|
+
}
|
|
30906
|
+
try {
|
|
30907
|
+
return readFileSync11(contained.absolutePath, "utf8");
|
|
30908
|
+
} catch {
|
|
30909
|
+
return null;
|
|
30910
|
+
}
|
|
30911
|
+
}
|
|
30876
30912
|
async handleLSP(toolName, args) {
|
|
30877
30913
|
const port = this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT);
|
|
30878
30914
|
if (this.lspClient !== null && this.lspClient.port !== port) {
|
|
@@ -31710,6 +31746,17 @@ class GodotServer {
|
|
|
31710
31746
|
const after = projectPath === "" || busy ? null : cachedClasses(projectPath);
|
|
31711
31747
|
const lost = before === null || after === null ? [] : [...before.keys()].filter((name) => !after.has(name));
|
|
31712
31748
|
const restored = lost.length > 0 ? await this.rebuildCacheAfterLoss(projectPath, lost) : [];
|
|
31749
|
+
const reloading = readNonEmptyString(args, "reloadScript");
|
|
31750
|
+
let reloaded = {};
|
|
31751
|
+
if (reloading !== undefined) {
|
|
31752
|
+
try {
|
|
31753
|
+
const answer = asParams(await this.godotBridge.invokeTool("reload_script", { ...args, scriptPath: reloading }));
|
|
31754
|
+
const methods = readArray(answer, "methods");
|
|
31755
|
+
reloaded = methods ? { methods: methods.map(String) } : { problem: `the editor would not say what ${reloading} has after reloading it` };
|
|
31756
|
+
} catch (error) {
|
|
31757
|
+
reloaded = { problem: `${reloading} could not be reloaded: ${errorMessage(error)}` };
|
|
31758
|
+
}
|
|
31759
|
+
}
|
|
31713
31760
|
const checked = busy ? { unseen: [] } : await this.classesTheEditorCannotSee(args);
|
|
31714
31761
|
const unseen = checked.unseen;
|
|
31715
31762
|
const stillGone = lost.filter((name) => !restored.includes(name));
|
|
@@ -31717,6 +31764,8 @@ class GodotServer {
|
|
|
31717
31764
|
ok: !busy && unseen.length === 0 && stillGone.length === 0 && checked.unchecked === undefined,
|
|
31718
31765
|
stillWorking: busy,
|
|
31719
31766
|
waitedMs: Date.now() - started,
|
|
31767
|
+
reloadedMethods: reloaded.methods,
|
|
31768
|
+
reloadProblem: reloaded.problem,
|
|
31720
31769
|
unseenByEditor: unseen.length > 0 ? unseen : undefined,
|
|
31721
31770
|
cacheLost: lost.length > 0 ? lost : undefined,
|
|
31722
31771
|
cacheRestored: restored.length > 0 ? restored : undefined,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.22",
|
|
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",
|