gdharness 0.9.5 → 0.9.6
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
|
@@ -15382,7 +15382,10 @@ var init_tool_definitions = __esm(() => {
|
|
|
15382
15382
|
parameters: {
|
|
15383
15383
|
projectPath: PROJECT_PATH,
|
|
15384
15384
|
preset: { type: "string", description: "Preset name from export_presets.cfg." },
|
|
15385
|
-
outputPath: {
|
|
15385
|
+
outputPath: {
|
|
15386
|
+
type: "string",
|
|
15387
|
+
description: `Where the export is written, inside the project. The directory is created if it is not there: Godot's command-line exporter refuses a missing one with "The given export path doesn't exist", which reads as a wrong path in the preset.`
|
|
15388
|
+
},
|
|
15386
15389
|
debug: { type: "boolean", description: "run: a debug export. Default false." }
|
|
15387
15390
|
},
|
|
15388
15391
|
requires: ["projectPath"],
|
|
@@ -15791,7 +15794,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15791
15794
|
says: {
|
|
15792
15795
|
type: "string",
|
|
15793
15796
|
ops: ["find"],
|
|
15794
|
-
description: "find: part of what the node has written on it, case-insensitively, which is how a button is reached by the word on it rather than by a generated path. Its own text, so a row is found by the label in it, and hidden nodes match."
|
|
15797
|
+
description: "find: part of what the node has written on it, case-insensitively, which is how a button is reached by the word on it rather than by a generated path. Its own text, so a row is found by the label in it, and hidden nodes match. A bare word is a contains; write a glob and it is one, matched against the whole of what the node says, the same as namePattern."
|
|
15795
15798
|
},
|
|
15796
15799
|
limit: {
|
|
15797
15800
|
type: "number",
|
|
@@ -36002,7 +36005,7 @@ var init_setup = __esm(() => {
|
|
|
36002
36005
|
|
|
36003
36006
|
// src/server.ts
|
|
36004
36007
|
import { execFile as execFile5, spawn } from "node:child_process";
|
|
36005
|
-
import { existsSync as existsSync12, mkdtempSync as mkdtempSync3, readdirSync as readdirSync6, readFileSync as readFileSync13, realpathSync as realpathSync2, rmSync as rmSync8 } from "node:fs";
|
|
36008
|
+
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";
|
|
36006
36009
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
36007
36010
|
import { basename as basename2, dirname as dirname9, join as join15, normalize as normalize3 } from "node:path";
|
|
36008
36011
|
import { setTimeout as delay2 } from "node:timers/promises";
|
|
@@ -36935,6 +36938,12 @@ class GodotServer {
|
|
|
36935
36938
|
if (!engine.ok) {
|
|
36936
36939
|
return engine.response;
|
|
36937
36940
|
}
|
|
36941
|
+
const folder = dirname9(output.absolutePath);
|
|
36942
|
+
try {
|
|
36943
|
+
mkdirSync7(folder, { recursive: true });
|
|
36944
|
+
} catch (error) {
|
|
36945
|
+
return this.createErrorResponse(`Cannot create ${dirname9(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
36946
|
+
}
|
|
36938
36947
|
const debug = readBoolean(args, "debug") ?? false;
|
|
36939
36948
|
const exportArgs = [
|
|
36940
36949
|
"--headless",
|
|
@@ -38930,6 +38939,7 @@ init_class_cache();
|
|
|
38930
38939
|
init_headless();
|
|
38931
38940
|
init_resources();
|
|
38932
38941
|
init_server_version();
|
|
38942
|
+
import { spawnSync } from "node:child_process";
|
|
38933
38943
|
import { cpSync, existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync7, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
38934
38944
|
import { dirname as dirname3, join as join8 } from "node:path";
|
|
38935
38945
|
import { fileURLToPath } from "node:url";
|
|
@@ -38992,6 +39002,29 @@ async function enablePlugins(engine, projectPath, names) {
|
|
|
38992
39002
|
async function setRuntime(engine, projectPath, on) {
|
|
38993
39003
|
return on ? await runOperation2(engine, "add_autoload", { name: RUNTIME_AUTOLOAD.name, path: `res://${RUNTIME_AUTOLOAD.path}`, enabled: true }, projectPath) : await runOperation2(engine, "remove_autoload", { name: RUNTIME_AUTOLOAD.name }, projectPath);
|
|
38994
39004
|
}
|
|
39005
|
+
function autoloadPaths(settings) {
|
|
39006
|
+
const named = new Map;
|
|
39007
|
+
for (const [name, value] of Object.entries(settings["autoload"] ?? {})) {
|
|
39008
|
+
const said = typeof value === "string" ? value.replace(/^\*/, "") : "";
|
|
39009
|
+
if (said.startsWith("res://")) {
|
|
39010
|
+
named.set(name, said.slice("res://".length));
|
|
39011
|
+
}
|
|
39012
|
+
}
|
|
39013
|
+
return named;
|
|
39014
|
+
}
|
|
39015
|
+
function trackedByGit(projectPath, wanted) {
|
|
39016
|
+
if (wanted.length === 0) {
|
|
39017
|
+
return new Set;
|
|
39018
|
+
}
|
|
39019
|
+
const asked = spawnSync("git", ["ls-files", "-z", "--", ...wanted], {
|
|
39020
|
+
cwd: projectPath,
|
|
39021
|
+
encoding: "utf8"
|
|
39022
|
+
});
|
|
39023
|
+
if (asked.status !== 0) {
|
|
39024
|
+
return null;
|
|
39025
|
+
}
|
|
39026
|
+
return new Set(asked.stdout.split("\x00").filter(Boolean));
|
|
39027
|
+
}
|
|
38995
39028
|
function enabledPlugins(settings) {
|
|
38996
39029
|
const raw = settings["editor_plugins"]?.["enabled"];
|
|
38997
39030
|
if (typeof raw !== "string") {
|
|
@@ -39023,6 +39056,13 @@ function inspectProject(projectPath) {
|
|
|
39023
39056
|
}
|
|
39024
39057
|
const autoload = settings["autoload"];
|
|
39025
39058
|
const runtimeAutoload = autoload !== undefined && typeof readString(autoload, RUNTIME_AUTOLOAD.name) === "string";
|
|
39059
|
+
const named = autoloadPaths(settings);
|
|
39060
|
+
const tracked = trackedByGit(projectPath, [...named.values()]);
|
|
39061
|
+
for (const [name, path] of named) {
|
|
39062
|
+
if (tracked !== null && !tracked.has(path) && existsSync7(join8(projectPath, path))) {
|
|
39063
|
+
problems.push(`the ${name} autoload names res://${path}, which git does not carry, so a clone boots with a missing script; commit that file, or point the autoload at one the project tracks`);
|
|
39064
|
+
}
|
|
39065
|
+
}
|
|
39026
39066
|
const cached = cachedClasses(projectPath);
|
|
39027
39067
|
const staleClasses = cached === null ? [] : staleAgainst(cached, projectPath);
|
|
39028
39068
|
if (cached === null) {
|
|
@@ -39157,6 +39197,9 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
39157
39197
|
These need the runtime autoload, which \`gdharness setup\` registers. \`runtime_capture\` needs a
|
|
39158
39198
|
window and refuses headless rather than handing back the last frame anything drew.
|
|
39159
39199
|
|
|
39200
|
+
A headless game answers; a \`godot -s\` script run does not. It has no game in it, so the autoload
|
|
39201
|
+
stays quiet there rather than announcing a test tier as the project.
|
|
39202
|
+
|
|
39160
39203
|
The autoload reaches an export. \`gdharness runtime off\` in the project before shipping.
|
|
39161
39204
|
|
|
39162
39205
|
## At a breakpoint
|
|
@@ -23,6 +23,12 @@ const BIND_ADDRESS_SETTING: String = "gdharness/runtime/bind_address"
|
|
|
23
23
|
## expects. A fixed port is for a client that cannot read the announcement.
|
|
24
24
|
const PORT_SETTING: String = "gdharness/runtime/port"
|
|
25
25
|
|
|
26
|
+
## How the engine is told to run a script instead of the game: `godot -s thing.gd`.
|
|
27
|
+
const SCRIPT_FLAGS: PackedStringArray = ["-s", "--script"]
|
|
28
|
+
|
|
29
|
+
## Serve a script run anyway, for somebody driving a `-s` script rather than a game.
|
|
30
|
+
const SCRIPT_RUNS_SETTING: String = "gdharness/runtime/serve_script_runs"
|
|
31
|
+
|
|
26
32
|
var values: Values = Values.new()
|
|
27
33
|
|
|
28
34
|
# The modules are members and not locals of _init, because a Callable holds its object by id
|
|
@@ -161,6 +167,15 @@ func _start_server() -> void:
|
|
|
161
167
|
_enabled = false
|
|
162
168
|
return
|
|
163
169
|
|
|
170
|
+
# And a script run has nothing to serve. Autoloads come up for `godot -s` as well, so a test
|
|
171
|
+
# tier or a batch tool binds a loopback port and writes an announcement under the project's
|
|
172
|
+
# own path: a gate that starts sixteen engines at once announces sixteen games that are not
|
|
173
|
+
# games, and a client asking the runtime anything while they run can be answered by whichever
|
|
174
|
+
# of them replies first. Measured on two projects before it was written here.
|
|
175
|
+
if _script_run() and not bool(ProjectSettings.get_setting(SCRIPT_RUNS_SETTING, false)):
|
|
176
|
+
_enabled = false
|
|
177
|
+
return
|
|
178
|
+
|
|
164
179
|
_server = TCPServer.new()
|
|
165
180
|
# listen() defaults bind_address to "*", which exposes the game to the whole network.
|
|
166
181
|
var bind_address: String = str(ProjectSettings.get_setting(BIND_ADDRESS_SETTING, DEFAULT_BIND_ADDRESS))
|
|
@@ -180,6 +195,15 @@ func _start_server() -> void:
|
|
|
180
195
|
print("[gdharness] runtime listening on %s:%d, announced at %s" % [bind_address, _port, _announcement])
|
|
181
196
|
|
|
182
197
|
|
|
198
|
+
## Whether the engine was told to run a script rather than the game.
|
|
199
|
+
func _script_run() -> bool:
|
|
200
|
+
var given: PackedStringArray = OS.get_cmdline_args()
|
|
201
|
+
for flag: String in SCRIPT_FLAGS:
|
|
202
|
+
if given.has(flag):
|
|
203
|
+
return true
|
|
204
|
+
return false
|
|
205
|
+
|
|
206
|
+
|
|
183
207
|
## Where the announcement goes. The server derives the same path with the same precedence, so
|
|
184
208
|
## the two only meet if this stays in step with `runtimeDirectory` in src/runtime-client.ts.
|
|
185
209
|
func _announcement_directory() -> String:
|
|
@@ -160,6 +160,17 @@ static func _is_literal(pattern: String) -> bool:
|
|
|
160
160
|
return not pattern.is_empty() and not pattern.contains("*") and not pattern.contains("?")
|
|
161
161
|
|
|
162
162
|
|
|
163
|
+
## Whether [param said] is what a find asked for in [param wanted].
|
|
164
|
+
##
|
|
165
|
+
## A plain word is a contains, which is what somebody looking for the row about a person means. A
|
|
166
|
+
## pattern is a glob, because [code]namePattern[/code] beside it is one and nobody writes `*Still*`
|
|
167
|
+
## in one field meaning a glob and in the other meaning those characters. Written as a contains
|
|
168
|
+
## only, a glob matched nothing at all and an empty answer reads as a control that is not on the
|
|
169
|
+
## screen: twice in one session here, over a button that was.
|
|
170
|
+
static func _says(said: String, wanted: String) -> bool:
|
|
171
|
+
return said.containsn(wanted) if _is_literal(wanted) else said.matchn(wanted)
|
|
172
|
+
|
|
173
|
+
|
|
163
174
|
## Every filter but the name, so a find that came back empty can say how many nodes the name was
|
|
164
175
|
## the only thing standing between it and.
|
|
165
176
|
func _matches_apart_from_name(node: Node, wanted: Dictionary[String, String]) -> bool:
|
|
@@ -168,7 +179,7 @@ func _matches_apart_from_name(node: Node, wanted: Dictionary[String, String]) ->
|
|
|
168
179
|
# What this node says, rather than everything said underneath it. A row is then found by the
|
|
169
180
|
# label in it, and the path answered is that label's, which is where the words a caller is
|
|
170
181
|
# looking at actually are: matching every container above it would answer with the screen.
|
|
171
|
-
if not wanted["says"].is_empty() and not said_by(node)
|
|
182
|
+
if not wanted["says"].is_empty() and not _says(said_by(node), wanted["says"]):
|
|
172
183
|
return false
|
|
173
184
|
var script: Variant = node.get_script()
|
|
174
185
|
if not wanted["script"].is_empty():
|
package/build/index.js
CHANGED
|
@@ -10126,7 +10126,7 @@ function defectReport(where, error, godotVersion) {
|
|
|
10126
10126
|
|
|
10127
10127
|
// src/server.ts
|
|
10128
10128
|
import { execFile as execFile3, spawn } from "node:child_process";
|
|
10129
|
-
import { existsSync as existsSync7, mkdtempSync as mkdtempSync2, readdirSync as readdirSync5, readFileSync as readFileSync9, realpathSync as realpathSync2, rmSync as rmSync4 } from "node:fs";
|
|
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
10132
|
import { setTimeout as delay2 } from "node:timers/promises";
|
|
@@ -27985,7 +27985,10 @@ var TOOL_SPECS = [
|
|
|
27985
27985
|
parameters: {
|
|
27986
27986
|
projectPath: PROJECT_PATH,
|
|
27987
27987
|
preset: { type: "string", description: "Preset name from export_presets.cfg." },
|
|
27988
|
-
outputPath: {
|
|
27988
|
+
outputPath: {
|
|
27989
|
+
type: "string",
|
|
27990
|
+
description: `Where the export is written, inside the project. The directory is created if it is not there: Godot's command-line exporter refuses a missing one with "The given export path doesn't exist", which reads as a wrong path in the preset.`
|
|
27991
|
+
},
|
|
27989
27992
|
debug: { type: "boolean", description: "run: a debug export. Default false." }
|
|
27990
27993
|
},
|
|
27991
27994
|
requires: ["projectPath"],
|
|
@@ -28394,7 +28397,7 @@ var TOOL_SPECS = [
|
|
|
28394
28397
|
says: {
|
|
28395
28398
|
type: "string",
|
|
28396
28399
|
ops: ["find"],
|
|
28397
|
-
description: "find: part of what the node has written on it, case-insensitively, which is how a button is reached by the word on it rather than by a generated path. Its own text, so a row is found by the label in it, and hidden nodes match."
|
|
28400
|
+
description: "find: part of what the node has written on it, case-insensitively, which is how a button is reached by the word on it rather than by a generated path. Its own text, so a row is found by the label in it, and hidden nodes match. A bare word is a contains; write a glob and it is one, matched against the whole of what the node says, the same as namePattern."
|
|
28398
28401
|
},
|
|
28399
28402
|
limit: {
|
|
28400
28403
|
type: "number",
|
|
@@ -29715,6 +29718,12 @@ class GodotServer {
|
|
|
29715
29718
|
if (!engine.ok) {
|
|
29716
29719
|
return engine.response;
|
|
29717
29720
|
}
|
|
29721
|
+
const folder = dirname4(output.absolutePath);
|
|
29722
|
+
try {
|
|
29723
|
+
mkdirSync4(folder, { recursive: true });
|
|
29724
|
+
} catch (error) {
|
|
29725
|
+
return this.createErrorResponse(`Cannot create ${dirname4(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
29726
|
+
}
|
|
29718
29727
|
const debug = readBoolean(args, "debug") ?? false;
|
|
29719
29728
|
const exportArgs = [
|
|
29720
29729
|
"--headless",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.6",
|
|
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",
|