gdharness 0.13.1 → 0.13.3
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 +236 -97
- 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 +233 -101
- 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"],
|
|
@@ -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) {
|
|
@@ -36950,13 +37037,17 @@ class GodotServer {
|
|
|
36950
37037
|
const known = new Set([...Object.keys(spec.parameters), ...spec.operations ? ["op"] : []]);
|
|
36951
37038
|
const unknown = Object.keys(args).filter((key) => !known.has(key));
|
|
36952
37039
|
if (unknown.length > 0) {
|
|
36953
|
-
const
|
|
37040
|
+
const asked = readString(args, "op");
|
|
37041
|
+
const named = asked !== undefined && spec.operations?.[asked] !== undefined ? asked : null;
|
|
37042
|
+
const wanted = named === null ? [...known] : [...argumentsOf(spec, named), "op"];
|
|
37043
|
+
const takes = wanted.length === 0 ? "It takes no arguments." : `It takes: ${wanted.join(", ")}.`;
|
|
37044
|
+
const where = named === null ? spec.name : `${spec.name} ${named}`;
|
|
36954
37045
|
return {
|
|
36955
37046
|
ok: false,
|
|
36956
|
-
response: this.createErrorResponse(`${
|
|
37047
|
+
response: this.createErrorResponse(`${where} does not take ${unknown.join(", ")}. ${takes} This is gdharness ${SERVER_VERSION}, which is what decides whether an argument exists.`)
|
|
36957
37048
|
};
|
|
36958
37049
|
}
|
|
36959
|
-
const wrong = wrongTypes(spec, args);
|
|
37050
|
+
const wrong = [...wrongTypes(spec, args), ...outsideTheirLists(spec, args)];
|
|
36960
37051
|
if (wrong.length > 0) {
|
|
36961
37052
|
return { ok: false, response: this.createErrorResponse(`${spec.name} takes ${wrong.join("; ")}.`) };
|
|
36962
37053
|
}
|
|
@@ -37008,7 +37099,25 @@ class GodotServer {
|
|
|
37008
37099
|
async dispatch(tool, op, args) {
|
|
37009
37100
|
const headless = HEADLESS_OPERATIONS[tool]?.[op];
|
|
37010
37101
|
if (headless !== undefined) {
|
|
37011
|
-
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);
|
|
37012
37121
|
return headless === "refresh_class_cache" && this.godotBridge.isConnected() ? await this.alsoSayWhatTheEditorCannotSee(answered, args) : answered;
|
|
37013
37122
|
}
|
|
37014
37123
|
const { op: _op, ...arguments_ } = args;
|
|
@@ -37223,8 +37332,8 @@ class GodotServer {
|
|
|
37223
37332
|
if (path === undefined) {
|
|
37224
37333
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
37225
37334
|
}
|
|
37226
|
-
const file =
|
|
37227
|
-
if (!
|
|
37335
|
+
const file = join17(path, "project.godot");
|
|
37336
|
+
if (!existsSync14(file)) {
|
|
37228
37337
|
return {
|
|
37229
37338
|
ok: false,
|
|
37230
37339
|
response: this.createErrorResponse(`Not a Godot project: ${path}`, [
|
|
@@ -37381,9 +37490,9 @@ class GodotServer {
|
|
|
37381
37490
|
}
|
|
37382
37491
|
async handleProjectInfo(args) {
|
|
37383
37492
|
const include = readStringArray(args, "include") ?? [];
|
|
37384
|
-
const unknown = include.filter((name) =>
|
|
37493
|
+
const unknown = include.filter((name) => PROJECT_INFO_SECTIONS2[name] === undefined);
|
|
37385
37494
|
if (unknown.length > 0) {
|
|
37386
|
-
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(", ")}.`);
|
|
37387
37496
|
}
|
|
37388
37497
|
const project = this.project(args);
|
|
37389
37498
|
if (!project.ok) {
|
|
@@ -37393,7 +37502,7 @@ class GodotServer {
|
|
|
37393
37502
|
if (!engine.ok) {
|
|
37394
37503
|
return engine.response;
|
|
37395
37504
|
}
|
|
37396
|
-
const application = parseProjectGodot(
|
|
37505
|
+
const application = parseProjectGodot(readFileSync15(project.value.file, "utf8"))["application"] ?? {};
|
|
37397
37506
|
const name = application["config/name"];
|
|
37398
37507
|
const mainScene = application["run/main_scene"];
|
|
37399
37508
|
const info = {
|
|
@@ -37405,7 +37514,7 @@ class GodotServer {
|
|
|
37405
37514
|
};
|
|
37406
37515
|
const detailed = args["detail"] === "full";
|
|
37407
37516
|
for (const section of include) {
|
|
37408
|
-
const spec =
|
|
37517
|
+
const spec = PROJECT_INFO_SECTIONS2[section];
|
|
37409
37518
|
if (spec === undefined) {
|
|
37410
37519
|
continue;
|
|
37411
37520
|
}
|
|
@@ -37461,11 +37570,11 @@ class GodotServer {
|
|
|
37461
37570
|
return this.createErrorResponse(`Cannot create ${dirname9(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
37462
37571
|
}
|
|
37463
37572
|
const debug = readBoolean(args, "debug") ?? false;
|
|
37464
|
-
const exportLogs = mkdtempSync3(
|
|
37573
|
+
const exportLogs = mkdtempSync3(join17(tmpdir5(), "gdharness-export-"));
|
|
37465
37574
|
const exportArgs = [
|
|
37466
37575
|
"--headless",
|
|
37467
37576
|
"--log-file",
|
|
37468
|
-
|
|
37577
|
+
join17(exportLogs, "engine.log"),
|
|
37469
37578
|
"--path",
|
|
37470
37579
|
project.value.path,
|
|
37471
37580
|
debug ? "--export-debug" : "--export-release",
|
|
@@ -37493,7 +37602,7 @@ class GodotServer {
|
|
|
37493
37602
|
log.finish();
|
|
37494
37603
|
const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
|
|
37495
37604
|
const verdict = {
|
|
37496
|
-
exported: exitCode === 0 && log.count("error") === 0 &&
|
|
37605
|
+
exported: exitCode === 0 && log.count("error") === 0 && existsSync14(output.absolutePath),
|
|
37497
37606
|
preset,
|
|
37498
37607
|
outputPath: output.relativePath,
|
|
37499
37608
|
debug,
|
|
@@ -37523,7 +37632,7 @@ class GodotServer {
|
|
|
37523
37632
|
return contained.response;
|
|
37524
37633
|
}
|
|
37525
37634
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
37526
|
-
if (!
|
|
37635
|
+
if (!existsSync14(join17(project.value.path, runner))) {
|
|
37527
37636
|
return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
|
|
37528
37637
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
37529
37638
|
]);
|
|
@@ -37558,7 +37667,7 @@ class GodotServer {
|
|
|
37558
37667
|
];
|
|
37559
37668
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
|
|
37560
37669
|
this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
37561
|
-
const userData = mkdtempSync3(
|
|
37670
|
+
const userData = mkdtempSync3(join17(tmpdir5(), "gdharness-tests-"));
|
|
37562
37671
|
const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
|
|
37563
37672
|
const hung = await new Promise((resolve) => {
|
|
37564
37673
|
const timer = setTimeout(() => {
|
|
@@ -37574,14 +37683,14 @@ class GodotServer {
|
|
|
37574
37683
|
resolve(false);
|
|
37575
37684
|
});
|
|
37576
37685
|
});
|
|
37577
|
-
const reportsDir =
|
|
37686
|
+
const reportsDir = join17(project.value.path, ".godot", ...ours.split("/"));
|
|
37578
37687
|
let report = null;
|
|
37579
37688
|
let reportProblem = null;
|
|
37580
37689
|
try {
|
|
37581
|
-
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))) : [];
|
|
37582
37691
|
const newest = written.at(-1);
|
|
37583
37692
|
if (newest !== undefined) {
|
|
37584
|
-
report = parseJUnit(
|
|
37693
|
+
report = parseJUnit(readFileSync15(join17(reportsDir, newest, "results.xml"), "utf8"));
|
|
37585
37694
|
}
|
|
37586
37695
|
} catch (error) {
|
|
37587
37696
|
reportProblem = errorMessage(error);
|
|
@@ -37693,13 +37802,13 @@ class GodotServer {
|
|
|
37693
37802
|
const severity = asParams(entry)["severity"];
|
|
37694
37803
|
return severity === 1 || severity === "error" || severity === "ERROR";
|
|
37695
37804
|
}).length;
|
|
37696
|
-
return this.jsonTextResponse({
|
|
37805
|
+
return this.jsonTextResponse(markIfStale({
|
|
37697
37806
|
scriptPath: readString(args, "scriptPath"),
|
|
37698
37807
|
clean: errors === 0,
|
|
37699
37808
|
errors,
|
|
37700
37809
|
warnings: diagnostics.length - errors,
|
|
37701
37810
|
diagnostics
|
|
37702
|
-
});
|
|
37811
|
+
}, this.godotBridge.getStatus().addonVersion, SERVER_VERSION));
|
|
37703
37812
|
}
|
|
37704
37813
|
async handleLSP(toolName, args) {
|
|
37705
37814
|
const port = this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT);
|
|
@@ -37744,6 +37853,7 @@ class GodotServer {
|
|
|
37744
37853
|
retryingBridge: this.bridgeRetry === null ? undefined : true,
|
|
37745
37854
|
announcedAt: this.announcedAt ?? undefined,
|
|
37746
37855
|
projectIs: this.projectHasMovedOn() ?? undefined,
|
|
37856
|
+
configIs: this.configDisagreement() ?? undefined,
|
|
37747
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,
|
|
37748
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
|
|
37749
37859
|
};
|
|
@@ -37999,7 +38109,7 @@ class GodotServer {
|
|
|
37999
38109
|
return runIsUp(run, editorSays);
|
|
38000
38110
|
}
|
|
38001
38111
|
async runtimeUp(projectPath, before, budgetMs) {
|
|
38002
|
-
if (!
|
|
38112
|
+
if (!existsSync14(join17(projectPath, RUNTIME_AUTOLOAD2.path))) {
|
|
38003
38113
|
return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false });
|
|
38004
38114
|
}
|
|
38005
38115
|
const endpoint = await announcedSince(projectPath, before, {
|
|
@@ -38254,7 +38364,7 @@ class GodotServer {
|
|
|
38254
38364
|
if (mine === null) {
|
|
38255
38365
|
return false;
|
|
38256
38366
|
}
|
|
38257
|
-
return project !== "" &&
|
|
38367
|
+
return project !== "" && isSameDirectory(mine, project);
|
|
38258
38368
|
}
|
|
38259
38369
|
currentRun() {
|
|
38260
38370
|
return this.activeProcess ?? this.adoptRecordedRun();
|
|
@@ -38385,6 +38495,11 @@ class GodotServer {
|
|
|
38385
38495
|
async handleRescanFilesystem(args) {
|
|
38386
38496
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 30000;
|
|
38387
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));
|
|
38388
38503
|
const first = await this.handleViaBridge("rescan_filesystem", args);
|
|
38389
38504
|
if (first.isError) {
|
|
38390
38505
|
return first;
|
|
@@ -38393,17 +38508,27 @@ class GodotServer {
|
|
|
38393
38508
|
while (busy && Date.now() - started < timeoutMs) {
|
|
38394
38509
|
await new Promise((settle) => setTimeout(settle, 100));
|
|
38395
38510
|
const status = asParams(await this.godotBridge.invokeTool("rescan_filesystem", { ...args, statusOnly: true }));
|
|
38396
|
-
|
|
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
|
+
}
|
|
38397
38519
|
}
|
|
38520
|
+
const after = projectPath === "" || busy ? null : cachedClasses(projectPath);
|
|
38521
|
+
const lost = before === null || after === null ? [] : [...before.keys()].filter((name) => !after.has(name));
|
|
38398
38522
|
const checked = busy ? { unseen: [] } : await this.classesTheEditorCannotSee(args);
|
|
38399
38523
|
const unseen = checked.unseen;
|
|
38400
38524
|
return this.jsonTextResponse({
|
|
38401
|
-
ok: !busy && unseen.length === 0 && checked.unchecked === undefined,
|
|
38525
|
+
ok: !busy && unseen.length === 0 && lost.length === 0 && checked.unchecked === undefined,
|
|
38402
38526
|
stillWorking: busy,
|
|
38403
38527
|
waitedMs: Date.now() - started,
|
|
38404
38528
|
unseenByEditor: unseen.length > 0 ? unseen : undefined,
|
|
38529
|
+
cacheLost: lost.length > 0 ? lost : undefined,
|
|
38405
38530
|
classesUnchecked: checked.unchecked,
|
|
38406
|
-
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
|
|
38407
38532
|
});
|
|
38408
38533
|
}
|
|
38409
38534
|
async alsoSayWhatTheEditorCannotSee(answered, args) {
|
|
@@ -38428,7 +38553,7 @@ class GodotServer {
|
|
|
38428
38553
|
return { unseen: [], unchecked: "no projectPath, so there was nothing to read the cache from" };
|
|
38429
38554
|
}
|
|
38430
38555
|
const open = this.godotBridge.getStatus().projectPath;
|
|
38431
|
-
if (open === undefined || !
|
|
38556
|
+
if (open === undefined || !isSameDirectory(open, projectPath)) {
|
|
38432
38557
|
return {
|
|
38433
38558
|
unseen: [],
|
|
38434
38559
|
unchecked: `the connected editor is open on ${open ?? "a project it did not name"}, not this one`
|
|
@@ -38456,6 +38581,14 @@ class GodotServer {
|
|
|
38456
38581
|
"editor_status says whether the bridge is up and what it is listening on"
|
|
38457
38582
|
]);
|
|
38458
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
|
+
}
|
|
38459
38592
|
try {
|
|
38460
38593
|
return this.jsonTextResponse(markIfStale(await this.godotBridge.invokeTool(toolName, args), this.godotBridge.getStatus().addonVersion, SERVER_VERSION));
|
|
38461
38594
|
} catch (error) {
|
|
@@ -38474,8 +38607,8 @@ class GodotServer {
|
|
|
38474
38607
|
return this.createErrorResponse(choice.problem);
|
|
38475
38608
|
}
|
|
38476
38609
|
const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
|
|
38477
|
-
const screenshotDir = expectsScreenshot ? mkdtempSync3(
|
|
38478
|
-
const screenshotPath = screenshotDir ?
|
|
38610
|
+
const screenshotDir = expectsScreenshot ? mkdtempSync3(join17(tmpdir5(), "gdharness-runtime-screenshot-")) : null;
|
|
38611
|
+
const screenshotPath = screenshotDir ? join17(screenshotDir, "capture.png") : null;
|
|
38479
38612
|
try {
|
|
38480
38613
|
const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs);
|
|
38481
38614
|
if (!reply.ok) {
|
|
@@ -38496,7 +38629,7 @@ class GodotServer {
|
|
|
38496
38629
|
return {
|
|
38497
38630
|
content: [
|
|
38498
38631
|
{ type: "text", text: `Screenshot captured: ${dimensions}` },
|
|
38499
|
-
{ type: "image", data:
|
|
38632
|
+
{ type: "image", data: readFileSync15(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
38500
38633
|
]
|
|
38501
38634
|
};
|
|
38502
38635
|
} finally {
|
|
@@ -38564,13 +38697,14 @@ async function runGodotServer() {
|
|
|
38564
38697
|
const server = new GodotServer;
|
|
38565
38698
|
await server.run();
|
|
38566
38699
|
}
|
|
38567
|
-
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/";
|
|
38568
38701
|
var init_server3 = __esm(() => {
|
|
38569
38702
|
init_mcp();
|
|
38570
38703
|
init_stdio2();
|
|
38571
38704
|
init_types();
|
|
38572
38705
|
init_bridge_announce();
|
|
38573
38706
|
init_class_cache();
|
|
38707
|
+
init_config_pin();
|
|
38574
38708
|
init_dap_client();
|
|
38575
38709
|
init_errors();
|
|
38576
38710
|
init_game_log();
|
|
@@ -38617,7 +38751,7 @@ var init_server3 = __esm(() => {
|
|
|
38617
38751
|
output: "dap_get_output",
|
|
38618
38752
|
variables: "dap_get_variables"
|
|
38619
38753
|
});
|
|
38620
|
-
|
|
38754
|
+
PROJECT_INFO_SECTIONS2 = dictionary({
|
|
38621
38755
|
autoloads: { operation: "list_autoloads", params: () => ({}) },
|
|
38622
38756
|
plugins: { operation: "list_plugins", params: () => ({}) },
|
|
38623
38757
|
export_presets: { operation: "list_export_presets", params: () => ({}) },
|
|
@@ -38631,8 +38765,8 @@ var init_server3 = __esm(() => {
|
|
|
38631
38765
|
});
|
|
38632
38766
|
|
|
38633
38767
|
// src/cli.ts
|
|
38634
|
-
import { existsSync as
|
|
38635
|
-
import { join as
|
|
38768
|
+
import { existsSync as existsSync15 } from "fs";
|
|
38769
|
+
import { join as join18, resolve as resolve5 } from "path";
|
|
38636
38770
|
|
|
38637
38771
|
// src/errors.ts
|
|
38638
38772
|
class Refusal extends Error {
|
|
@@ -39969,6 +40103,11 @@ function headlessSection() {
|
|
|
39969
40103
|
"",
|
|
39970
40104
|
"Everything else needs the editor (`editor_*`, `debug_*`, `scene_*`, `resource_*`) or a running",
|
|
39971
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.",
|
|
39972
40111
|
""
|
|
39973
40112
|
];
|
|
39974
40113
|
}
|
|
@@ -40237,7 +40376,7 @@ class UsageError extends Refusal {
|
|
|
40237
40376
|
function projectArgument(at) {
|
|
40238
40377
|
const named = args.slice(at).find((value) => !value.startsWith("--"));
|
|
40239
40378
|
const projectPath = resolve5(named ?? ".");
|
|
40240
|
-
if (!
|
|
40379
|
+
if (!existsSync15(join18(projectPath, "project.godot"))) {
|
|
40241
40380
|
throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
|
|
40242
40381
|
}
|
|
40243
40382
|
return projectPath;
|
|
@@ -40271,7 +40410,7 @@ async function registerRuntime(godot, projectPath) {
|
|
|
40271
40410
|
return;
|
|
40272
40411
|
}
|
|
40273
40412
|
console.log(`${RUNTIME_AUTOLOAD.name} autoload left at ${named}, which this project registers itself`);
|
|
40274
|
-
if (!
|
|
40413
|
+
if (!existsSync15(join18(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
|
|
40275
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}`);
|
|
40276
40415
|
}
|
|
40277
40416
|
}
|
|
@@ -40403,7 +40542,7 @@ async function setup() {
|
|
|
40403
40542
|
console.log("no harness configured; name one yourself, and gdharness harnesses lists them all");
|
|
40404
40543
|
}
|
|
40405
40544
|
if (!args.includes("--no-skill")) {
|
|
40406
|
-
const directories = skillDirectories(chosen.harnesses, projectPath,
|
|
40545
|
+
const directories = skillDirectories(chosen.harnesses, projectPath, existsSync15);
|
|
40407
40546
|
for (const written of writeSkill(directories, getLocalVersion())) {
|
|
40408
40547
|
console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
|
|
40409
40548
|
}
|
|
@@ -40492,7 +40631,7 @@ async function upgrade() {
|
|
|
40492
40631
|
if (already.length === 0) {
|
|
40493
40632
|
console.log("no harness config names gdharness, so none was re-pinned");
|
|
40494
40633
|
}
|
|
40495
|
-
for (const written of writeSkill(skillDirectories(already, projectPath,
|
|
40634
|
+
for (const written of writeSkill(skillDirectories(already, projectPath, existsSync15), version)) {
|
|
40496
40635
|
console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
|
|
40497
40636
|
}
|
|
40498
40637
|
console.log(`
|