gdharness 0.13.2 → 0.13.4
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 +229 -94
- package/build/godot/addons/gdharness_editor/reading.gd +14 -7
- package/build/godot/addons/gdharness_editor/serialisation.gd +560 -0
- package/build/godot/addons/gdharness_editor/serialisation.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tool_executor.gd +8 -0
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +59 -152
- package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +58 -0
- package/build/godot/addons/gdharness_editor/tools/settings_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +14 -7
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +16 -1
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +88 -178
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +25 -2
- package/build/godot/addons/gdharness_runtime/serialisation.gd +560 -0
- package/build/godot/addons/gdharness_runtime/serialisation.gd.uid +1 -0
- package/build/godot/operations/reading.gd +6 -1
- package/build/godot/operations/serialisation.gd +448 -77
- package/build/index.js +226 -98
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -7493,7 +7493,7 @@ var init_server_version = __esm(() => {
|
|
|
7493
7493
|
});
|
|
7494
7494
|
|
|
7495
7495
|
// src/class-cache.ts
|
|
7496
|
-
import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync5 } from "node:fs";
|
|
7496
|
+
import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync5, statSync as statSync2 } from "node:fs";
|
|
7497
7497
|
import { join as join6 } from "node:path";
|
|
7498
7498
|
function declaredClasses(projectPath) {
|
|
7499
7499
|
const declared = new Map;
|
|
@@ -7531,6 +7531,14 @@ function cachedClasses(projectPath) {
|
|
|
7531
7531
|
}
|
|
7532
7532
|
return listed;
|
|
7533
7533
|
}
|
|
7534
|
+
function cacheWrittenAt(projectPath) {
|
|
7535
|
+
const cache = join6(projectPath, ".godot", "global_script_class_cache.cfg");
|
|
7536
|
+
try {
|
|
7537
|
+
return statSync2(cache).mtimeMs;
|
|
7538
|
+
} catch {
|
|
7539
|
+
return null;
|
|
7540
|
+
}
|
|
7541
|
+
}
|
|
7534
7542
|
function staleAgainst(cached, projectPath) {
|
|
7535
7543
|
return [...declaredClasses(projectPath)].filter(([name, path]) => cached.get(name) !== path).map(([name]) => name);
|
|
7536
7544
|
}
|
|
@@ -14744,9 +14752,17 @@ var init_types = __esm(() => {
|
|
|
14744
14752
|
});
|
|
14745
14753
|
|
|
14746
14754
|
// src/paths.ts
|
|
14755
|
+
import { realpathSync } from "node:fs";
|
|
14747
14756
|
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
14757
|
+
function realPathOr(path) {
|
|
14758
|
+
try {
|
|
14759
|
+
return realpathSync.native(path);
|
|
14760
|
+
} catch {
|
|
14761
|
+
return path;
|
|
14762
|
+
}
|
|
14763
|
+
}
|
|
14748
14764
|
function isSameDirectory(onePath, otherPath) {
|
|
14749
|
-
return relative(resolve(onePath), resolve(otherPath)) === "";
|
|
14765
|
+
return relative(realPathOr(resolve(onePath)), realPathOr(resolve(otherPath))) === "";
|
|
14750
14766
|
}
|
|
14751
14767
|
function isWithinRoot(rootPath, candidatePath) {
|
|
14752
14768
|
const step = relative(resolve(rootPath), resolve(candidatePath));
|
|
@@ -15127,8 +15143,16 @@ function buildToolDefinitions() {
|
|
|
15127
15143
|
};
|
|
15128
15144
|
});
|
|
15129
15145
|
}
|
|
15130
|
-
var PROJECT_PATH, RUNNING_PROJECT_PATH, SCENE_PATH, SCRIPT_PATH, RESOURCE_PATH, NODE_PATH, PROPERTIES, XY, ANIMATION_TRACK, SCRIPT_MODIFICATIONS, INPUT_EVENTS, TILESET_SOURCES, TILEMAP_CELLS, COLOUR, TOOL_SPECS, SPECS_BY_NAME;
|
|
15146
|
+
var PROJECT_INFO_SECTIONS, PROJECT_PATH, RUNNING_PROJECT_PATH, SCENE_PATH, SCRIPT_PATH, RESOURCE_PATH, NODE_PATH, PROPERTIES, XY, ANIMATION_TRACK, SCRIPT_MODIFICATIONS, INPUT_EVENTS, TILESET_SOURCES, TILEMAP_CELLS, COLOUR, TOOL_SPECS, SPECS_BY_NAME;
|
|
15131
15147
|
var init_tool_definitions = __esm(() => {
|
|
15148
|
+
PROJECT_INFO_SECTIONS = [
|
|
15149
|
+
"autoloads",
|
|
15150
|
+
"plugins",
|
|
15151
|
+
"export_presets",
|
|
15152
|
+
"audio_buses",
|
|
15153
|
+
"health",
|
|
15154
|
+
"validation"
|
|
15155
|
+
];
|
|
15132
15156
|
PROJECT_PATH = {
|
|
15133
15157
|
type: "string",
|
|
15134
15158
|
description: "Absolute path to the project directory, the one holding project.godot."
|
|
@@ -15155,7 +15179,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15155
15179
|
};
|
|
15156
15180
|
PROPERTIES = {
|
|
15157
15181
|
type: "object",
|
|
15158
|
-
description: 'Properties to set, keyed by Godot property name. Vectors, colours and the like may be written as {"x": 1, "y": 2} or tagged {"_type": "Vector2", "x": 1, "y": 2}.',
|
|
15182
|
+
description: 'Properties to set, keyed by Godot property name. Vectors, colours and the like may be written as {"x": 1, "y": 2} or tagged {"_type": "Vector2", "x": 1, "y": 2}, which is the form a read answers with, so a value read off one node can be written straight to another.',
|
|
15159
15183
|
additionalProperties: true
|
|
15160
15184
|
};
|
|
15161
15185
|
XY = {
|
|
@@ -15278,10 +15302,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15278
15302
|
projectPath: PROJECT_PATH,
|
|
15279
15303
|
include: {
|
|
15280
15304
|
type: "array",
|
|
15281
|
-
items: {
|
|
15282
|
-
type: "string",
|
|
15283
|
-
enum: ["autoloads", "plugins", "export_presets", "audio_buses", "health", "validation"]
|
|
15284
|
-
},
|
|
15305
|
+
items: { type: "string", enum: PROJECT_INFO_SECTIONS },
|
|
15285
15306
|
description: "Extra sections: registered autoloads, addons and whether each is enabled, export presets, the audio bus layout, a health report, or export validation."
|
|
15286
15307
|
},
|
|
15287
15308
|
preset: { type: "string", description: "For validation: the export preset to validate against." },
|
|
@@ -15299,6 +15320,12 @@ var init_tool_definitions = __esm(() => {
|
|
|
15299
15320
|
parameters: {
|
|
15300
15321
|
projectPath: PROJECT_PATH,
|
|
15301
15322
|
setting: { type: "string", description: 'Setting path, such as "display/window/size/viewport_width".' },
|
|
15323
|
+
from: {
|
|
15324
|
+
type: "string",
|
|
15325
|
+
enum: ["disk", "editor"],
|
|
15326
|
+
ops: ["get"],
|
|
15327
|
+
description: "get: where to read. Default disk, which starts a short-lived engine and reads project.godot, needs no editor and is what an automatic check can reproduce. editor asks the open editor instead, which costs no engine and answers what the editor holds, including changes nobody has saved. Asking for editor with none connected is refused rather than answered from disk, so an answer never means the other one quietly."
|
|
15328
|
+
},
|
|
15302
15329
|
prefix: {
|
|
15303
15330
|
type: "string",
|
|
15304
15331
|
ops: ["get"],
|
|
@@ -15790,7 +15817,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15790
15817
|
},
|
|
15791
15818
|
{
|
|
15792
15819
|
name: "editor_rescan",
|
|
15793
|
-
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 is a change to the declaring script,
|
|
15820
|
+
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 is both halves in order: a change to the declaring script, saved, and then a rescan. Measured downstream, neither works alone, and a change on its own reads as the cure having failed. 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 can go out of that file with it, which one project saw and two could not reproduce; where it happens the answer names what the file lost under cacheLost, before a fresh engine, a CI run or a clone inherits it. Needs the editor connected.",
|
|
15794
15821
|
parameters: {
|
|
15795
15822
|
projectPath: PROJECT_PATH,
|
|
15796
15823
|
timeoutMs: { type: "number", description: "How long to wait for the scan. Default 30000." }
|
|
@@ -16151,7 +16178,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16151
16178
|
});
|
|
16152
16179
|
|
|
16153
16180
|
// src/headless-operations.ts
|
|
16154
|
-
var HEADLESS_OPERATIONS;
|
|
16181
|
+
var HEADLESS_OPERATIONS, EDITOR_READS;
|
|
16155
16182
|
var init_headless_operations = __esm(() => {
|
|
16156
16183
|
HEADLESS_OPERATIONS = dictionary({
|
|
16157
16184
|
project_settings: dictionary({
|
|
@@ -16185,6 +16212,9 @@ var init_headless_operations = __esm(() => {
|
|
|
16185
16212
|
inheritance: "inspect_inheritance"
|
|
16186
16213
|
})
|
|
16187
16214
|
});
|
|
16215
|
+
EDITOR_READS = dictionary({
|
|
16216
|
+
project_settings: dictionary({ get: "get_project_setting" })
|
|
16217
|
+
});
|
|
16188
16218
|
});
|
|
16189
16219
|
|
|
16190
16220
|
// node_modules/zod/v3/helpers/util.js
|
|
@@ -30955,6 +30985,36 @@ var init_bridge_announce = __esm(() => {
|
|
|
30955
30985
|
init_errors();
|
|
30956
30986
|
});
|
|
30957
30987
|
|
|
30988
|
+
// src/config-pin.ts
|
|
30989
|
+
import { existsSync as existsSync9, readFileSync as readFileSync10 } from "node:fs";
|
|
30990
|
+
import { join as join12 } from "node:path";
|
|
30991
|
+
function versionsNamedInConfig(projectPath) {
|
|
30992
|
+
const config = join12(projectPath, ".mcp.json");
|
|
30993
|
+
if (!existsSync9(config)) {
|
|
30994
|
+
return [];
|
|
30995
|
+
}
|
|
30996
|
+
let text;
|
|
30997
|
+
try {
|
|
30998
|
+
text = readFileSync10(config, "utf8");
|
|
30999
|
+
JSON.parse(text);
|
|
31000
|
+
} catch {
|
|
31001
|
+
return [];
|
|
31002
|
+
}
|
|
31003
|
+
return [...new Set([...text.matchAll(PINNED)].map((found) => found[1] ?? ""))].filter((version) => version !== "");
|
|
31004
|
+
}
|
|
31005
|
+
function configDisagrees(projectPath, serverVersion) {
|
|
31006
|
+
const named = versionsNamedInConfig(projectPath);
|
|
31007
|
+
const others = named.filter((version) => version !== serverVersion);
|
|
31008
|
+
if (others.length === 0) {
|
|
31009
|
+
return null;
|
|
31010
|
+
}
|
|
31011
|
+
return `.mcp.json in this project names gdharness@${others.join(" and gdharness@")}, and this server ` + `is ${serverVersion}. Whichever is meant to be right, nothing will change version until the ` + "MCP server is reconnected, and a pin kept anywhere else does not reach this file.";
|
|
31012
|
+
}
|
|
31013
|
+
var PINNED;
|
|
31014
|
+
var init_config_pin = __esm(() => {
|
|
31015
|
+
PINNED = /\bgdharness@(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)\b/g;
|
|
31016
|
+
});
|
|
31017
|
+
|
|
30958
31018
|
// src/framing.ts
|
|
30959
31019
|
function frame(message) {
|
|
30960
31020
|
const body = Buffer.from(JSON.stringify(message), "utf8");
|
|
@@ -34932,7 +34992,7 @@ var init_godot_bridge = __esm(() => {
|
|
|
34932
34992
|
|
|
34933
34993
|
// src/godot-path.ts
|
|
34934
34994
|
import { execFile as execFile4 } from "node:child_process";
|
|
34935
|
-
import { existsSync as
|
|
34995
|
+
import { existsSync as existsSync10 } from "node:fs";
|
|
34936
34996
|
import { normalize as normalize2 } from "node:path";
|
|
34937
34997
|
import { promisify as promisify4 } from "node:util";
|
|
34938
34998
|
var run4, GodotLocator2;
|
|
@@ -34974,7 +35034,7 @@ var init_godot_path = __esm(() => {
|
|
|
34974
35034
|
return known;
|
|
34975
35035
|
}
|
|
34976
35036
|
let ok = false;
|
|
34977
|
-
if (path === "godot" ||
|
|
35037
|
+
if (path === "godot" || existsSync10(path)) {
|
|
34978
35038
|
try {
|
|
34979
35039
|
await run4(path, ["--version"]);
|
|
34980
35040
|
ok = true;
|
|
@@ -35319,7 +35379,7 @@ var init_junit = __esm(() => {
|
|
|
35319
35379
|
});
|
|
35320
35380
|
|
|
35321
35381
|
// src/lsp_client.ts
|
|
35322
|
-
import { realpathSync } from "node:fs";
|
|
35382
|
+
import { realpathSync as realpathSync2 } from "node:fs";
|
|
35323
35383
|
import { readFile, realpath } from "node:fs/promises";
|
|
35324
35384
|
import { createConnection as createConnection2 } from "node:net";
|
|
35325
35385
|
import { dirname as dirname7, resolve as resolve3 } from "node:path";
|
|
@@ -35335,7 +35395,7 @@ function diagnosticsKey(uri) {
|
|
|
35335
35395
|
return decoded;
|
|
35336
35396
|
}
|
|
35337
35397
|
try {
|
|
35338
|
-
return
|
|
35398
|
+
return realpathSync2.native(fileURLToPath3(decoded)).toLowerCase();
|
|
35339
35399
|
} catch {
|
|
35340
35400
|
return decoded.toLowerCase();
|
|
35341
35401
|
}
|
|
@@ -35895,13 +35955,13 @@ var init_process_time = __esm(() => {
|
|
|
35895
35955
|
});
|
|
35896
35956
|
|
|
35897
35957
|
// src/project-scan.ts
|
|
35898
|
-
import { existsSync as
|
|
35899
|
-
import { join as
|
|
35958
|
+
import { existsSync as existsSync11, readdirSync as readdirSync4, readFileSync as readFileSync11 } from "node:fs";
|
|
35959
|
+
import { join as join13 } from "node:path";
|
|
35900
35960
|
function skipped(name) {
|
|
35901
35961
|
return name.startsWith(".") || SKIPPED.has(name);
|
|
35902
35962
|
}
|
|
35903
35963
|
function steppedOver(directory) {
|
|
35904
|
-
return
|
|
35964
|
+
return existsSync11(join13(directory, ".gdignore"));
|
|
35905
35965
|
}
|
|
35906
35966
|
function projectStructure(projectPath) {
|
|
35907
35967
|
const structure = { scenes: 0, scripts: 0, assets: 0, other: 0 };
|
|
@@ -35914,7 +35974,7 @@ function projectStructure(projectPath) {
|
|
|
35914
35974
|
continue;
|
|
35915
35975
|
}
|
|
35916
35976
|
if (entry.isDirectory()) {
|
|
35917
|
-
visit(
|
|
35977
|
+
visit(join13(directory, entry.name));
|
|
35918
35978
|
} else if (entry.isFile()) {
|
|
35919
35979
|
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
35920
35980
|
if (extension === "tscn") {
|
|
@@ -35959,7 +36019,7 @@ function searchProject(projectPath, options) {
|
|
|
35959
36019
|
if (skipped(entry.name)) {
|
|
35960
36020
|
continue;
|
|
35961
36021
|
}
|
|
35962
|
-
const entryPath =
|
|
36022
|
+
const entryPath = join13(directory, entry.name);
|
|
35963
36023
|
if (entry.isDirectory()) {
|
|
35964
36024
|
visit(entryPath);
|
|
35965
36025
|
continue;
|
|
@@ -35970,7 +36030,7 @@ function searchProject(projectPath, options) {
|
|
|
35970
36030
|
}
|
|
35971
36031
|
result.summary.files_searched += 1;
|
|
35972
36032
|
const matches = [];
|
|
35973
|
-
for (const [index, line] of
|
|
36033
|
+
for (const [index, line] of readFileSync11(entryPath, "utf8").split(`
|
|
35974
36034
|
`).entries()) {
|
|
35975
36035
|
if (full()) {
|
|
35976
36036
|
break;
|
|
@@ -35998,10 +36058,10 @@ var init_project_scan = __esm(() => {
|
|
|
35998
36058
|
});
|
|
35999
36059
|
|
|
36000
36060
|
// src/runtime-client.ts
|
|
36001
|
-
import { existsSync as
|
|
36061
|
+
import { existsSync as existsSync12, readdirSync as readdirSync5, readFileSync as readFileSync12, unlinkSync } from "node:fs";
|
|
36002
36062
|
import { createConnection as createConnection3 } from "node:net";
|
|
36003
36063
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
36004
|
-
import { join as
|
|
36064
|
+
import { join as join14, resolve as resolve4 } from "node:path";
|
|
36005
36065
|
import { setTimeout as delay2 } from "node:timers/promises";
|
|
36006
36066
|
function runtimeDirectory(variables = process.env) {
|
|
36007
36067
|
const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
|
|
@@ -36009,13 +36069,13 @@ function runtimeDirectory(variables = process.env) {
|
|
|
36009
36069
|
return explicit;
|
|
36010
36070
|
}
|
|
36011
36071
|
const perUser = envValue("XDG_RUNTIME_DIR", variables);
|
|
36012
|
-
return
|
|
36072
|
+
return join14(perUser ?? tmpdir4(), "gdharness");
|
|
36013
36073
|
}
|
|
36014
36074
|
function runtimeDirectories(variables = process.env) {
|
|
36015
36075
|
const candidates = [runtimeDirectory(variables)];
|
|
36016
36076
|
const fallbacks = process.platform === "win32" ? [envValue("SystemRoot", variables) ?? envValue("windir", variables) ?? "C:\\Windows"] : ["/tmp", "/var/tmp"];
|
|
36017
36077
|
for (const base of fallbacks) {
|
|
36018
|
-
candidates.push(
|
|
36078
|
+
candidates.push(join14(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
|
|
36019
36079
|
}
|
|
36020
36080
|
return [...new Set(candidates.map((path) => resolve4(path)))];
|
|
36021
36081
|
}
|
|
@@ -36030,7 +36090,7 @@ function processAlive(pid) {
|
|
|
36030
36090
|
function parseAnnouncement(file, pid) {
|
|
36031
36091
|
let fields;
|
|
36032
36092
|
try {
|
|
36033
|
-
fields = asParams(JSON.parse(
|
|
36093
|
+
fields = asParams(JSON.parse(readFileSync12(file, "utf8")));
|
|
36034
36094
|
} catch {
|
|
36035
36095
|
return { kind: "rubbish" };
|
|
36036
36096
|
}
|
|
@@ -36069,7 +36129,7 @@ function discoverRuntimes(directories = runtimeDirectories()) {
|
|
|
36069
36129
|
return runtimesAnnounced(directories).running;
|
|
36070
36130
|
}
|
|
36071
36131
|
function announcedIn(directory) {
|
|
36072
|
-
if (!
|
|
36132
|
+
if (!existsSync12(directory)) {
|
|
36073
36133
|
return [];
|
|
36074
36134
|
}
|
|
36075
36135
|
const found = [];
|
|
@@ -36078,7 +36138,7 @@ function announcedIn(directory) {
|
|
|
36078
36138
|
if (!match) {
|
|
36079
36139
|
continue;
|
|
36080
36140
|
}
|
|
36081
|
-
const file =
|
|
36141
|
+
const file = join14(directory, entry);
|
|
36082
36142
|
const pid = Number.parseInt(match[1] ?? "", 10);
|
|
36083
36143
|
const announced = processAlive(pid) ? parseAnnouncement(file, pid) : { kind: "rubbish" };
|
|
36084
36144
|
if (announced.kind === "rubbish") {
|
|
@@ -36240,20 +36300,20 @@ var init_runtime_client = __esm(() => {
|
|
|
36240
36300
|
|
|
36241
36301
|
// src/run-record.ts
|
|
36242
36302
|
import { execFileSync } from "node:child_process";
|
|
36243
|
-
import { mkdirSync as mkdirSync6, openSync, readdirSync as readdirSync6, readFileSync as
|
|
36244
|
-
import { basename as basename2, join as
|
|
36303
|
+
import { mkdirSync as mkdirSync6, openSync, readdirSync as readdirSync6, readFileSync as readFileSync13, rmSync as rmSync7, statSync as statSync3, writeFileSync as writeFileSync8 } from "node:fs";
|
|
36304
|
+
import { basename as basename2, join as join15 } from "node:path";
|
|
36245
36305
|
function runsDirectory() {
|
|
36246
|
-
return
|
|
36306
|
+
return join15(runtimeDirectory(), "runs");
|
|
36247
36307
|
}
|
|
36248
36308
|
function recordPath() {
|
|
36249
|
-
return
|
|
36309
|
+
return join15(runsDirectory(), "run.json");
|
|
36250
36310
|
}
|
|
36251
36311
|
function recordPaths() {
|
|
36252
|
-
return runtimeDirectories().map((directory) =>
|
|
36312
|
+
return runtimeDirectories().map((directory) => join15(directory, "runs", "run.json"));
|
|
36253
36313
|
}
|
|
36254
36314
|
function openTranscript(startedAt) {
|
|
36255
36315
|
mkdirSync6(runsDirectory(), { recursive: true });
|
|
36256
|
-
const path =
|
|
36316
|
+
const path = join15(runsDirectory(), `run-${startedAt}.log`);
|
|
36257
36317
|
return { path, fd: openSync(path, "a") };
|
|
36258
36318
|
}
|
|
36259
36319
|
function writeRunRecord(record) {
|
|
@@ -36282,7 +36342,7 @@ function readRunRecord() {
|
|
|
36282
36342
|
function recordAt(path) {
|
|
36283
36343
|
let parsed;
|
|
36284
36344
|
try {
|
|
36285
|
-
parsed = JSON.parse(
|
|
36345
|
+
parsed = JSON.parse(readFileSync13(path, "utf8"));
|
|
36286
36346
|
} catch {
|
|
36287
36347
|
return null;
|
|
36288
36348
|
}
|
|
@@ -36318,7 +36378,7 @@ function runningAs(pid) {
|
|
|
36318
36378
|
}
|
|
36319
36379
|
try {
|
|
36320
36380
|
if (process.platform === "linux") {
|
|
36321
|
-
const raw =
|
|
36381
|
+
const raw = readFileSync13(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
|
|
36322
36382
|
return raw === "" ? null : { kind: "commandLine", text: raw };
|
|
36323
36383
|
}
|
|
36324
36384
|
const args = execFileSync("ps", ["-p", String(pid), "-o", "args="], {
|
|
@@ -36393,7 +36453,7 @@ function clearRunRecord(ours = () => true) {
|
|
|
36393
36453
|
}
|
|
36394
36454
|
function ageMs(path, now) {
|
|
36395
36455
|
try {
|
|
36396
|
-
return now -
|
|
36456
|
+
return now - statSync3(path).mtimeMs;
|
|
36397
36457
|
} catch {
|
|
36398
36458
|
return 0;
|
|
36399
36459
|
}
|
|
@@ -36411,7 +36471,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
36411
36471
|
if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
|
|
36412
36472
|
continue;
|
|
36413
36473
|
}
|
|
36414
|
-
const path =
|
|
36474
|
+
const path = join15(directory, entry);
|
|
36415
36475
|
if (path === keep || ageMs(path, now) < keepMs) {
|
|
36416
36476
|
continue;
|
|
36417
36477
|
}
|
|
@@ -36425,14 +36485,14 @@ var init_run_record = __esm(() => {
|
|
|
36425
36485
|
});
|
|
36426
36486
|
|
|
36427
36487
|
// src/setup.ts
|
|
36428
|
-
import { cpSync as cpSync2, existsSync as
|
|
36429
|
-
import { dirname as dirname8, join as
|
|
36488
|
+
import { cpSync as cpSync2, existsSync as existsSync13, mkdirSync as mkdirSync7, readFileSync as readFileSync14, rmSync as rmSync8, writeFileSync as writeFileSync9 } from "node:fs";
|
|
36489
|
+
import { dirname as dirname8, join as join16 } from "node:path";
|
|
36430
36490
|
function installedAddonVersion(projectPath) {
|
|
36431
|
-
const marker =
|
|
36432
|
-
if (!
|
|
36491
|
+
const marker = join16(projectPath, "addons", ADDONS2[0], VERSION_MARKER2);
|
|
36492
|
+
if (!existsSync13(marker)) {
|
|
36433
36493
|
return null;
|
|
36434
36494
|
}
|
|
36435
|
-
const said =
|
|
36495
|
+
const said = readFileSync14(marker, "utf8").trim();
|
|
36436
36496
|
return said === "" ? null : said;
|
|
36437
36497
|
}
|
|
36438
36498
|
var ADDONS2, RUNTIME_AUTOLOAD2, VERSION_MARKER2 = ".gdharness-version";
|
|
@@ -36453,19 +36513,18 @@ import { execFile as execFile6, spawn } from "node:child_process";
|
|
|
36453
36513
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
36454
36514
|
import {
|
|
36455
36515
|
closeSync,
|
|
36456
|
-
existsSync as
|
|
36516
|
+
existsSync as existsSync14,
|
|
36457
36517
|
fstatSync,
|
|
36458
36518
|
mkdirSync as mkdirSync8,
|
|
36459
36519
|
mkdtempSync as mkdtempSync3,
|
|
36460
36520
|
openSync as openSync2,
|
|
36461
36521
|
readdirSync as readdirSync7,
|
|
36462
|
-
readFileSync as
|
|
36522
|
+
readFileSync as readFileSync15,
|
|
36463
36523
|
readSync,
|
|
36464
|
-
realpathSync as realpathSync2,
|
|
36465
36524
|
rmSync as rmSync9
|
|
36466
36525
|
} from "node:fs";
|
|
36467
36526
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
36468
|
-
import { basename as basename3, dirname as dirname9, join as
|
|
36527
|
+
import { basename as basename3, dirname as dirname9, join as join17, normalize as normalize3 } from "node:path";
|
|
36469
36528
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
36470
36529
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
36471
36530
|
import { promisify as promisify6 } from "node:util";
|
|
@@ -36563,6 +36622,30 @@ function wrongTypes(spec, args) {
|
|
|
36563
36622
|
}
|
|
36564
36623
|
return complaints;
|
|
36565
36624
|
}
|
|
36625
|
+
function unlistedValues(schema, value, path) {
|
|
36626
|
+
if (typeof schema !== "object" || schema === null) {
|
|
36627
|
+
return [];
|
|
36628
|
+
}
|
|
36629
|
+
const fields = schema;
|
|
36630
|
+
const listed = fields["enum"];
|
|
36631
|
+
if (Array.isArray(listed) && typeof value === "string") {
|
|
36632
|
+
return listed.includes(value) ? [] : [`${path} as one of ${listed.join(", ")}, not ${JSON.stringify(value)}`];
|
|
36633
|
+
}
|
|
36634
|
+
if (Array.isArray(value)) {
|
|
36635
|
+
return value.flatMap((element, index) => unlistedValues(fields["items"], element, `${path}[${index}]`));
|
|
36636
|
+
}
|
|
36637
|
+
const properties = fields["properties"];
|
|
36638
|
+
if (isPlainObject3(properties) && isPlainObject3(value)) {
|
|
36639
|
+
return Object.entries(value).flatMap(([field, held]) => Object.hasOwn(properties, field) ? unlistedValues(properties[field], held, `${path}.${field}`) : []);
|
|
36640
|
+
}
|
|
36641
|
+
return [];
|
|
36642
|
+
}
|
|
36643
|
+
function isPlainObject3(value) {
|
|
36644
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
36645
|
+
}
|
|
36646
|
+
function outsideTheirLists(spec, args) {
|
|
36647
|
+
return Object.entries(args).flatMap(([name, value]) => unlistedValues(spec.parameters[name], value, name));
|
|
36648
|
+
}
|
|
36566
36649
|
function escapingPropertyValue(properties) {
|
|
36567
36650
|
if (typeof properties !== "object" || properties === null) {
|
|
36568
36651
|
return;
|
|
@@ -36577,17 +36660,6 @@ function escapingPropertyValue(properties) {
|
|
|
36577
36660
|
}
|
|
36578
36661
|
return;
|
|
36579
36662
|
}
|
|
36580
|
-
function realPathOr(path) {
|
|
36581
|
-
try {
|
|
36582
|
-
return realpathSync2.native(path);
|
|
36583
|
-
} catch {
|
|
36584
|
-
return path;
|
|
36585
|
-
}
|
|
36586
|
-
}
|
|
36587
|
-
function samePath(one, other) {
|
|
36588
|
-
const settled = (path) => realPathOr(path).replaceAll("\\", "/").replace(/\/+$/, "").toLowerCase();
|
|
36589
|
-
return settled(one) === settled(other);
|
|
36590
|
-
}
|
|
36591
36663
|
function aboveTheRunner(entry) {
|
|
36592
36664
|
const runner = entry.detail.findIndex((line) => line.includes(RUNNER_DIRECTORY));
|
|
36593
36665
|
if (runner < 0) {
|
|
@@ -36601,7 +36673,7 @@ function aboveTheRunner(entry) {
|
|
|
36601
36673
|
}
|
|
36602
36674
|
function elsewhereIn(projectPath, asked) {
|
|
36603
36675
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
36604
|
-
if (wanted === "" ||
|
|
36676
|
+
if (wanted === "" || existsSync14(join17(projectPath, wanted))) {
|
|
36605
36677
|
return "";
|
|
36606
36678
|
}
|
|
36607
36679
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
@@ -36615,11 +36687,11 @@ function elsewhereIn(projectPath, asked) {
|
|
|
36615
36687
|
return near.length === 0 ? "" : ` The project does have ${there}.`;
|
|
36616
36688
|
}
|
|
36617
36689
|
function hasMainScene(projectFile) {
|
|
36618
|
-
const scene = parseProjectGodot(
|
|
36690
|
+
const scene = parseProjectGodot(readFileSync15(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
36619
36691
|
return typeof scene === "string" && scene !== "";
|
|
36620
36692
|
}
|
|
36621
36693
|
function editorPlaysHeadless(projectFile) {
|
|
36622
|
-
const runArgs = parseProjectGodot(
|
|
36694
|
+
const runArgs = parseProjectGodot(readFileSync15(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
36623
36695
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
36624
36696
|
}
|
|
36625
36697
|
function camelCased(params) {
|
|
@@ -36635,7 +36707,7 @@ function camelCased(params) {
|
|
|
36635
36707
|
class GodotServer {
|
|
36636
36708
|
mcp;
|
|
36637
36709
|
locator = new GodotLocator2;
|
|
36638
|
-
operationsScript =
|
|
36710
|
+
operationsScript = join17(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
36639
36711
|
godotBridge;
|
|
36640
36712
|
tools = buildToolDefinitions();
|
|
36641
36713
|
activeProcess = null;
|
|
@@ -36909,6 +36981,18 @@ class GodotServer {
|
|
|
36909
36981
|
}
|
|
36910
36982
|
});
|
|
36911
36983
|
}
|
|
36984
|
+
const disagreement = this.configDisagreement();
|
|
36985
|
+
if (disagreement !== null) {
|
|
36986
|
+
this.noticedUpdate = true;
|
|
36987
|
+
this.callsSinceNotice = 0;
|
|
36988
|
+
return this.saying(answer, {
|
|
36989
|
+
config_names_another_version: {
|
|
36990
|
+
server_is: SERVER_VERSION,
|
|
36991
|
+
said: disagreement,
|
|
36992
|
+
what_to_do: "Tell the user which version their config names and which one is answering, and that " + "the two will not meet until .mcp.json is right and the MCP server is reconnected. Do " + "not edit their config without asking: which of the two they meant is theirs to say."
|
|
36993
|
+
}
|
|
36994
|
+
});
|
|
36995
|
+
}
|
|
36912
36996
|
const notice = this.updates.notice();
|
|
36913
36997
|
if (notice === null) {
|
|
36914
36998
|
return answer;
|
|
@@ -36935,6 +37019,9 @@ class GodotServer {
|
|
|
36935
37019
|
const installed = installedAddonVersion(this.ownProject);
|
|
36936
37020
|
return installed === null || installed === SERVER_VERSION ? null : installed;
|
|
36937
37021
|
}
|
|
37022
|
+
configDisagreement() {
|
|
37023
|
+
return this.ownProject === null ? null : configDisagrees(this.ownProject, SERVER_VERSION);
|
|
37024
|
+
}
|
|
36938
37025
|
withFeedbackNotice(answer) {
|
|
36939
37026
|
this.callsSinceFeedback += 1;
|
|
36940
37027
|
if (this.callsSinceFeedback < FEEDBACK_NOTICE_EVERY) {
|
|
@@ -36960,7 +37047,7 @@ class GodotServer {
|
|
|
36960
37047
|
response: this.createErrorResponse(`${where} does not take ${unknown.join(", ")}. ${takes} This is gdharness ${SERVER_VERSION}, which is what decides whether an argument exists.`)
|
|
36961
37048
|
};
|
|
36962
37049
|
}
|
|
36963
|
-
const wrong = wrongTypes(spec, args);
|
|
37050
|
+
const wrong = [...wrongTypes(spec, args), ...outsideTheirLists(spec, args)];
|
|
36964
37051
|
if (wrong.length > 0) {
|
|
36965
37052
|
return { ok: false, response: this.createErrorResponse(`${spec.name} takes ${wrong.join("; ")}.`) };
|
|
36966
37053
|
}
|
|
@@ -37012,7 +37099,25 @@ class GodotServer {
|
|
|
37012
37099
|
async dispatch(tool, op, args) {
|
|
37013
37100
|
const headless = HEADLESS_OPERATIONS[tool]?.[op];
|
|
37014
37101
|
if (headless !== undefined) {
|
|
37015
|
-
const
|
|
37102
|
+
const { from: asked, ...answerable } = args;
|
|
37103
|
+
if (asked === "editor") {
|
|
37104
|
+
const editorSide = EDITOR_READS[tool]?.[op];
|
|
37105
|
+
if (editorSide === undefined) {
|
|
37106
|
+
return this.createErrorResponse(`${tool} ${op} cannot be read from the editor, only from disk.`, [
|
|
37107
|
+
'Leave from unset, or pass from: "disk"'
|
|
37108
|
+
]);
|
|
37109
|
+
}
|
|
37110
|
+
if (!this.godotBridge.isConnected()) {
|
|
37111
|
+
return this.createErrorResponse(`${tool} ${op} was asked of the editor, and no editor has reached this server.`, [
|
|
37112
|
+
"editor_status says whether the bridge is up and whether an editor may yet connect",
|
|
37113
|
+
'Open the project with editor_launch, or pass from: "disk" to read the file instead'
|
|
37114
|
+
]);
|
|
37115
|
+
}
|
|
37116
|
+
const { op: _dropped, ...asks } = answerable;
|
|
37117
|
+
const contained = this.containProjectFiles(asks);
|
|
37118
|
+
return contained.ok ? await this.handleViaBridge(editorSide, contained.value) : contained.response;
|
|
37119
|
+
}
|
|
37120
|
+
const answered = await this.headless(headless, answerable);
|
|
37016
37121
|
return headless === "refresh_class_cache" && this.godotBridge.isConnected() ? await this.alsoSayWhatTheEditorCannotSee(answered, args) : answered;
|
|
37017
37122
|
}
|
|
37018
37123
|
const { op: _op, ...arguments_ } = args;
|
|
@@ -37227,8 +37332,8 @@ class GodotServer {
|
|
|
37227
37332
|
if (path === undefined) {
|
|
37228
37333
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
37229
37334
|
}
|
|
37230
|
-
const file =
|
|
37231
|
-
if (!
|
|
37335
|
+
const file = join17(path, "project.godot");
|
|
37336
|
+
if (!existsSync14(file)) {
|
|
37232
37337
|
return {
|
|
37233
37338
|
ok: false,
|
|
37234
37339
|
response: this.createErrorResponse(`Not a Godot project: ${path}`, [
|
|
@@ -37385,9 +37490,9 @@ class GodotServer {
|
|
|
37385
37490
|
}
|
|
37386
37491
|
async handleProjectInfo(args) {
|
|
37387
37492
|
const include = readStringArray(args, "include") ?? [];
|
|
37388
|
-
const unknown = include.filter((name) =>
|
|
37493
|
+
const unknown = include.filter((name) => PROJECT_INFO_SECTIONS2[name] === undefined);
|
|
37389
37494
|
if (unknown.length > 0) {
|
|
37390
|
-
return this.createErrorResponse(`project_info cannot include ${unknown.join(", ")}. Sections: ${Object.keys(
|
|
37495
|
+
return this.createErrorResponse(`project_info cannot include ${unknown.join(", ")}. Sections: ${Object.keys(PROJECT_INFO_SECTIONS2).join(", ")}.`);
|
|
37391
37496
|
}
|
|
37392
37497
|
const project = this.project(args);
|
|
37393
37498
|
if (!project.ok) {
|
|
@@ -37397,7 +37502,7 @@ class GodotServer {
|
|
|
37397
37502
|
if (!engine.ok) {
|
|
37398
37503
|
return engine.response;
|
|
37399
37504
|
}
|
|
37400
|
-
const application = parseProjectGodot(
|
|
37505
|
+
const application = parseProjectGodot(readFileSync15(project.value.file, "utf8"))["application"] ?? {};
|
|
37401
37506
|
const name = application["config/name"];
|
|
37402
37507
|
const mainScene = application["run/main_scene"];
|
|
37403
37508
|
const info = {
|
|
@@ -37409,7 +37514,7 @@ class GodotServer {
|
|
|
37409
37514
|
};
|
|
37410
37515
|
const detailed = args["detail"] === "full";
|
|
37411
37516
|
for (const section of include) {
|
|
37412
|
-
const spec =
|
|
37517
|
+
const spec = PROJECT_INFO_SECTIONS2[section];
|
|
37413
37518
|
if (spec === undefined) {
|
|
37414
37519
|
continue;
|
|
37415
37520
|
}
|
|
@@ -37465,11 +37570,11 @@ class GodotServer {
|
|
|
37465
37570
|
return this.createErrorResponse(`Cannot create ${dirname9(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
37466
37571
|
}
|
|
37467
37572
|
const debug = readBoolean(args, "debug") ?? false;
|
|
37468
|
-
const exportLogs = mkdtempSync3(
|
|
37573
|
+
const exportLogs = mkdtempSync3(join17(tmpdir5(), "gdharness-export-"));
|
|
37469
37574
|
const exportArgs = [
|
|
37470
37575
|
"--headless",
|
|
37471
37576
|
"--log-file",
|
|
37472
|
-
|
|
37577
|
+
join17(exportLogs, "engine.log"),
|
|
37473
37578
|
"--path",
|
|
37474
37579
|
project.value.path,
|
|
37475
37580
|
debug ? "--export-debug" : "--export-release",
|
|
@@ -37497,7 +37602,7 @@ class GodotServer {
|
|
|
37497
37602
|
log.finish();
|
|
37498
37603
|
const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
|
|
37499
37604
|
const verdict = {
|
|
37500
|
-
exported: exitCode === 0 && log.count("error") === 0 &&
|
|
37605
|
+
exported: exitCode === 0 && log.count("error") === 0 && existsSync14(output.absolutePath),
|
|
37501
37606
|
preset,
|
|
37502
37607
|
outputPath: output.relativePath,
|
|
37503
37608
|
debug,
|
|
@@ -37527,7 +37632,7 @@ class GodotServer {
|
|
|
37527
37632
|
return contained.response;
|
|
37528
37633
|
}
|
|
37529
37634
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
37530
|
-
if (!
|
|
37635
|
+
if (!existsSync14(join17(project.value.path, runner))) {
|
|
37531
37636
|
return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
|
|
37532
37637
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
37533
37638
|
]);
|
|
@@ -37562,7 +37667,7 @@ class GodotServer {
|
|
|
37562
37667
|
];
|
|
37563
37668
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
|
|
37564
37669
|
this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
37565
|
-
const userData = mkdtempSync3(
|
|
37670
|
+
const userData = mkdtempSync3(join17(tmpdir5(), "gdharness-tests-"));
|
|
37566
37671
|
const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
|
|
37567
37672
|
const hung = await new Promise((resolve) => {
|
|
37568
37673
|
const timer = setTimeout(() => {
|
|
@@ -37578,14 +37683,14 @@ class GodotServer {
|
|
|
37578
37683
|
resolve(false);
|
|
37579
37684
|
});
|
|
37580
37685
|
});
|
|
37581
|
-
const reportsDir =
|
|
37686
|
+
const reportsDir = join17(project.value.path, ".godot", ...ours.split("/"));
|
|
37582
37687
|
let report = null;
|
|
37583
37688
|
let reportProblem = null;
|
|
37584
37689
|
try {
|
|
37585
|
-
const written =
|
|
37690
|
+
const written = existsSync14(reportsDir) ? readdirSync7(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
37586
37691
|
const newest = written.at(-1);
|
|
37587
37692
|
if (newest !== undefined) {
|
|
37588
|
-
report = parseJUnit(
|
|
37693
|
+
report = parseJUnit(readFileSync15(join17(reportsDir, newest, "results.xml"), "utf8"));
|
|
37589
37694
|
}
|
|
37590
37695
|
} catch (error) {
|
|
37591
37696
|
reportProblem = errorMessage(error);
|
|
@@ -37748,6 +37853,7 @@ class GodotServer {
|
|
|
37748
37853
|
retryingBridge: this.bridgeRetry === null ? undefined : true,
|
|
37749
37854
|
announcedAt: this.announcedAt ?? undefined,
|
|
37750
37855
|
projectIs: this.projectHasMovedOn() ?? undefined,
|
|
37856
|
+
configIs: this.configDisagreement() ?? undefined,
|
|
37751
37857
|
note: isPortConflict ? "Bridge port is already in use. Another gdharness instance owns the editor bridge, so this server cannot reach the editor. Usually the server this one replaced, still on its way out." : undefined,
|
|
37752
37858
|
suggestion: isPortConflict ? `This server is asking for the port again every ${BRIDGE_RETRY_MS / 1000}s and takes it the moment the other one lets go, so editor tools come back on their own. Ask again, or end the older gdharness process to have it now.` : undefined
|
|
37753
37859
|
};
|
|
@@ -38003,7 +38109,7 @@ class GodotServer {
|
|
|
38003
38109
|
return runIsUp(run, editorSays);
|
|
38004
38110
|
}
|
|
38005
38111
|
async runtimeUp(projectPath, before, budgetMs) {
|
|
38006
|
-
if (!
|
|
38112
|
+
if (!existsSync14(join17(projectPath, RUNTIME_AUTOLOAD2.path))) {
|
|
38007
38113
|
return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false });
|
|
38008
38114
|
}
|
|
38009
38115
|
const endpoint = await announcedSince(projectPath, before, {
|
|
@@ -38258,7 +38364,7 @@ class GodotServer {
|
|
|
38258
38364
|
if (mine === null) {
|
|
38259
38365
|
return false;
|
|
38260
38366
|
}
|
|
38261
|
-
return project !== "" &&
|
|
38367
|
+
return project !== "" && isSameDirectory(mine, project);
|
|
38262
38368
|
}
|
|
38263
38369
|
currentRun() {
|
|
38264
38370
|
return this.activeProcess ?? this.adoptRecordedRun();
|
|
@@ -38389,6 +38495,11 @@ class GodotServer {
|
|
|
38389
38495
|
async handleRescanFilesystem(args) {
|
|
38390
38496
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 30000;
|
|
38391
38497
|
const started = Date.now();
|
|
38498
|
+
const projectPath = typeof args["projectPath"] === "string" ? args["projectPath"] : "";
|
|
38499
|
+
const before = projectPath === "" ? null : cachedClasses(projectPath);
|
|
38500
|
+
const writtenBefore = projectPath === "" ? null : cacheWrittenAt(projectPath);
|
|
38501
|
+
const blind = projectPath === "" ? { unseen: [] } : await this.classesTheEditorCannotSee(args);
|
|
38502
|
+
const atRisk = before === null ? [] : blind.unseen.filter((one) => before.has(one.className));
|
|
38392
38503
|
const first = await this.handleViaBridge("rescan_filesystem", args);
|
|
38393
38504
|
if (first.isError) {
|
|
38394
38505
|
return first;
|
|
@@ -38397,17 +38508,27 @@ class GodotServer {
|
|
|
38397
38508
|
while (busy && Date.now() - started < timeoutMs) {
|
|
38398
38509
|
await new Promise((settle) => setTimeout(settle, 100));
|
|
38399
38510
|
const status = asParams(await this.godotBridge.invokeTool("rescan_filesystem", { ...args, statusOnly: true }));
|
|
38400
|
-
|
|
38511
|
+
const pending = Boolean(status["pending"]) && Date.now() - started < SCAN_START_MS;
|
|
38512
|
+
busy = Boolean(status["scanning"]) || Boolean(status["importing"]) || pending;
|
|
38513
|
+
}
|
|
38514
|
+
if (!busy && atRisk.length > 0) {
|
|
38515
|
+
const until = Date.now() + CACHE_WRITE_MS;
|
|
38516
|
+
while (Date.now() < until && cacheWrittenAt(projectPath) === writtenBefore) {
|
|
38517
|
+
await new Promise((settle) => setTimeout(settle, 100));
|
|
38518
|
+
}
|
|
38401
38519
|
}
|
|
38520
|
+
const after = projectPath === "" || busy ? null : cachedClasses(projectPath);
|
|
38521
|
+
const lost = before === null || after === null ? [] : [...before.keys()].filter((name) => !after.has(name));
|
|
38402
38522
|
const checked = busy ? { unseen: [] } : await this.classesTheEditorCannotSee(args);
|
|
38403
38523
|
const unseen = checked.unseen;
|
|
38404
38524
|
return this.jsonTextResponse({
|
|
38405
|
-
ok: !busy && unseen.length === 0 && checked.unchecked === undefined,
|
|
38525
|
+
ok: !busy && unseen.length === 0 && lost.length === 0 && checked.unchecked === undefined,
|
|
38406
38526
|
stillWorking: busy,
|
|
38407
38527
|
waitedMs: Date.now() - started,
|
|
38408
38528
|
unseenByEditor: unseen.length > 0 ? unseen : undefined,
|
|
38529
|
+
cacheLost: lost.length > 0 ? lost : undefined,
|
|
38409
38530
|
classesUnchecked: checked.unchecked,
|
|
38410
|
-
note: busy ? "The editor was still scanning or importing when the wait ran out, so new files may not be visible yet." : unseen.length > 0 ? "The scan finished and these classes are still not in the list the editor resolves against, so every use of them reads as an unknown identifier. Its walk skips a file another engine has already imported. Change the declaring script and rescan, or restart the editor with editor_launch restart." : undefined
|
|
38531
|
+
note: busy ? "The editor was still scanning or importing when the wait ran out, so new files may not be visible yet." : lost.length > 0 ? "The scan wrote the class cache from the list this editor is holding, and that list is shorter than the file was: these classes were in it before the scan and are not now. Every engine that reads the cache next, including a test run, will report them as unknown identifiers in files nobody touched. Run project_import refresh_classes to put them back, and do not rescan again until the editor has been restarted with editor_launch restart, because it will write the same short list over it." : unseen.length > 0 ? "The scan finished and these classes are still not in the list the editor resolves against, so every use of them reads as an unknown identifier. Its walk skips a file another engine has already imported. Change the declaring script and rescan, or restart the editor with editor_launch restart." : undefined
|
|
38411
38532
|
});
|
|
38412
38533
|
}
|
|
38413
38534
|
async alsoSayWhatTheEditorCannotSee(answered, args) {
|
|
@@ -38432,7 +38553,7 @@ class GodotServer {
|
|
|
38432
38553
|
return { unseen: [], unchecked: "no projectPath, so there was nothing to read the cache from" };
|
|
38433
38554
|
}
|
|
38434
38555
|
const open = this.godotBridge.getStatus().projectPath;
|
|
38435
|
-
if (open === undefined || !
|
|
38556
|
+
if (open === undefined || !isSameDirectory(open, projectPath)) {
|
|
38436
38557
|
return {
|
|
38437
38558
|
unseen: [],
|
|
38438
38559
|
unchecked: `the connected editor is open on ${open ?? "a project it did not name"}, not this one`
|
|
@@ -38460,6 +38581,14 @@ class GodotServer {
|
|
|
38460
38581
|
"editor_status says whether the bridge is up and what it is listening on"
|
|
38461
38582
|
]);
|
|
38462
38583
|
}
|
|
38584
|
+
const open = this.godotBridge.getStatus().projectPath;
|
|
38585
|
+
const meant = readNonEmptyString(args, "projectPath");
|
|
38586
|
+
if (open !== undefined && open !== "" && meant !== undefined && !isSameDirectory(open, meant)) {
|
|
38587
|
+
return this.createErrorResponse(`This call names ${meant}, and the editor on this bridge has ${open} open.`, [
|
|
38588
|
+
"editor_status names the project this server's editor is showing",
|
|
38589
|
+
"One server serves one editor: start a second server for the other project"
|
|
38590
|
+
]);
|
|
38591
|
+
}
|
|
38463
38592
|
try {
|
|
38464
38593
|
return this.jsonTextResponse(markIfStale(await this.godotBridge.invokeTool(toolName, args), this.godotBridge.getStatus().addonVersion, SERVER_VERSION));
|
|
38465
38594
|
} catch (error) {
|
|
@@ -38478,8 +38607,8 @@ class GodotServer {
|
|
|
38478
38607
|
return this.createErrorResponse(choice.problem);
|
|
38479
38608
|
}
|
|
38480
38609
|
const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
|
|
38481
|
-
const screenshotDir = expectsScreenshot ? mkdtempSync3(
|
|
38482
|
-
const screenshotPath = screenshotDir ?
|
|
38610
|
+
const screenshotDir = expectsScreenshot ? mkdtempSync3(join17(tmpdir5(), "gdharness-runtime-screenshot-")) : null;
|
|
38611
|
+
const screenshotPath = screenshotDir ? join17(screenshotDir, "capture.png") : null;
|
|
38483
38612
|
try {
|
|
38484
38613
|
const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs);
|
|
38485
38614
|
if (!reply.ok) {
|
|
@@ -38500,7 +38629,7 @@ class GodotServer {
|
|
|
38500
38629
|
return {
|
|
38501
38630
|
content: [
|
|
38502
38631
|
{ type: "text", text: `Screenshot captured: ${dimensions}` },
|
|
38503
|
-
{ type: "image", data:
|
|
38632
|
+
{ type: "image", data: readFileSync15(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
38504
38633
|
]
|
|
38505
38634
|
};
|
|
38506
38635
|
} finally {
|
|
@@ -38568,13 +38697,14 @@ async function runGodotServer() {
|
|
|
38568
38697
|
const server = new GodotServer;
|
|
38569
38698
|
await server.run();
|
|
38570
38699
|
}
|
|
38571
|
-
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run6, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, SLOWEST_FRAME_RATE = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS,
|
|
38700
|
+
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run6, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, SLOWEST_FRAME_RATE = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CACHE_WRITE_MS = 2000, SCAN_START_MS = 2000, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/";
|
|
38572
38701
|
var init_server3 = __esm(() => {
|
|
38573
38702
|
init_mcp();
|
|
38574
38703
|
init_stdio2();
|
|
38575
38704
|
init_types();
|
|
38576
38705
|
init_bridge_announce();
|
|
38577
38706
|
init_class_cache();
|
|
38707
|
+
init_config_pin();
|
|
38578
38708
|
init_dap_client();
|
|
38579
38709
|
init_errors();
|
|
38580
38710
|
init_game_log();
|
|
@@ -38621,7 +38751,7 @@ var init_server3 = __esm(() => {
|
|
|
38621
38751
|
output: "dap_get_output",
|
|
38622
38752
|
variables: "dap_get_variables"
|
|
38623
38753
|
});
|
|
38624
|
-
|
|
38754
|
+
PROJECT_INFO_SECTIONS2 = dictionary({
|
|
38625
38755
|
autoloads: { operation: "list_autoloads", params: () => ({}) },
|
|
38626
38756
|
plugins: { operation: "list_plugins", params: () => ({}) },
|
|
38627
38757
|
export_presets: { operation: "list_export_presets", params: () => ({}) },
|
|
@@ -38635,8 +38765,8 @@ var init_server3 = __esm(() => {
|
|
|
38635
38765
|
});
|
|
38636
38766
|
|
|
38637
38767
|
// src/cli.ts
|
|
38638
|
-
import { existsSync as
|
|
38639
|
-
import { join as
|
|
38768
|
+
import { existsSync as existsSync15 } from "fs";
|
|
38769
|
+
import { join as join18, resolve as resolve5 } from "path";
|
|
38640
38770
|
|
|
38641
38771
|
// src/errors.ts
|
|
38642
38772
|
class Refusal extends Error {
|
|
@@ -39973,6 +40103,11 @@ function headlessSection() {
|
|
|
39973
40103
|
"",
|
|
39974
40104
|
"Everything else needs the editor (`editor_*`, `debug_*`, `scene_*`, `resource_*`) or a running",
|
|
39975
40105
|
"game (`runtime_*`).",
|
|
40106
|
+
"",
|
|
40107
|
+
'One of these can be pointed the other way: `project_settings get` takes `from: "editor"` to ask',
|
|
40108
|
+
"the open editor instead of the file, which is a different reading and costs no engine start. A",
|
|
40109
|
+
"gate leaves it alone, because there is no editor on a build machine and the call is refused",
|
|
40110
|
+
"rather than answered from the file.",
|
|
39976
40111
|
""
|
|
39977
40112
|
];
|
|
39978
40113
|
}
|
|
@@ -40241,7 +40376,7 @@ class UsageError extends Refusal {
|
|
|
40241
40376
|
function projectArgument(at) {
|
|
40242
40377
|
const named = args.slice(at).find((value) => !value.startsWith("--"));
|
|
40243
40378
|
const projectPath = resolve5(named ?? ".");
|
|
40244
|
-
if (!
|
|
40379
|
+
if (!existsSync15(join18(projectPath, "project.godot"))) {
|
|
40245
40380
|
throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
|
|
40246
40381
|
}
|
|
40247
40382
|
return projectPath;
|
|
@@ -40275,7 +40410,7 @@ async function registerRuntime(godot, projectPath) {
|
|
|
40275
40410
|
return;
|
|
40276
40411
|
}
|
|
40277
40412
|
console.log(`${RUNTIME_AUTOLOAD.name} autoload left at ${named}, which this project registers itself`);
|
|
40278
|
-
if (!
|
|
40413
|
+
if (!existsSync15(join18(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
|
|
40279
40414
|
console.log(` that file is not in this project, so it boots with a missing script: restore it, or point the entry at res://${RUNTIME_AUTOLOAD.path}`);
|
|
40280
40415
|
}
|
|
40281
40416
|
}
|
|
@@ -40407,7 +40542,7 @@ async function setup() {
|
|
|
40407
40542
|
console.log("no harness configured; name one yourself, and gdharness harnesses lists them all");
|
|
40408
40543
|
}
|
|
40409
40544
|
if (!args.includes("--no-skill")) {
|
|
40410
|
-
const directories = skillDirectories(chosen.harnesses, projectPath,
|
|
40545
|
+
const directories = skillDirectories(chosen.harnesses, projectPath, existsSync15);
|
|
40411
40546
|
for (const written of writeSkill(directories, getLocalVersion())) {
|
|
40412
40547
|
console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
|
|
40413
40548
|
}
|
|
@@ -40496,7 +40631,7 @@ async function upgrade() {
|
|
|
40496
40631
|
if (already.length === 0) {
|
|
40497
40632
|
console.log("no harness config names gdharness, so none was re-pinned");
|
|
40498
40633
|
}
|
|
40499
|
-
for (const written of writeSkill(skillDirectories(already, projectPath,
|
|
40634
|
+
for (const written of writeSkill(skillDirectories(already, projectPath, existsSync15), version)) {
|
|
40500
40635
|
console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
|
|
40501
40636
|
}
|
|
40502
40637
|
console.log(`
|