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/index.js
CHANGED
|
@@ -10142,19 +10142,18 @@ import { execFile as execFile4, spawn } from "node:child_process";
|
|
|
10142
10142
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
10143
10143
|
import {
|
|
10144
10144
|
closeSync,
|
|
10145
|
-
existsSync as
|
|
10145
|
+
existsSync as existsSync9,
|
|
10146
10146
|
fstatSync,
|
|
10147
10147
|
mkdirSync as mkdirSync5,
|
|
10148
10148
|
mkdtempSync as mkdtempSync2,
|
|
10149
10149
|
openSync as openSync2,
|
|
10150
10150
|
readdirSync as readdirSync6,
|
|
10151
|
-
readFileSync as
|
|
10151
|
+
readFileSync as readFileSync11,
|
|
10152
10152
|
readSync,
|
|
10153
|
-
realpathSync as realpathSync2,
|
|
10154
10153
|
rmSync as rmSync5
|
|
10155
10154
|
} from "node:fs";
|
|
10156
10155
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
10157
|
-
import { basename as basename2, dirname as dirname4, join as
|
|
10156
|
+
import { basename as basename2, dirname as dirname4, join as join11, normalize as normalize2 } from "node:path";
|
|
10158
10157
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
10159
10158
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
10160
10159
|
import { promisify as promisify4 } from "node:util";
|
|
@@ -24779,7 +24778,7 @@ function withdrawBridge(path) {
|
|
|
24779
24778
|
}
|
|
24780
24779
|
|
|
24781
24780
|
// src/class-cache.ts
|
|
24782
|
-
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync4 } from "node:fs";
|
|
24781
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync4, statSync } from "node:fs";
|
|
24783
24782
|
import { join as join3 } from "node:path";
|
|
24784
24783
|
function declaredClasses(projectPath) {
|
|
24785
24784
|
const declared = new Map;
|
|
@@ -24817,6 +24816,14 @@ function cachedClasses(projectPath) {
|
|
|
24817
24816
|
}
|
|
24818
24817
|
return listed;
|
|
24819
24818
|
}
|
|
24819
|
+
function cacheWrittenAt(projectPath) {
|
|
24820
|
+
const cache = join3(projectPath, ".godot", "global_script_class_cache.cfg");
|
|
24821
|
+
try {
|
|
24822
|
+
return statSync(cache).mtimeMs;
|
|
24823
|
+
} catch {
|
|
24824
|
+
return null;
|
|
24825
|
+
}
|
|
24826
|
+
}
|
|
24820
24827
|
function staleAgainst(cached, projectPath) {
|
|
24821
24828
|
return [...declaredClasses(projectPath)].filter(([name, path]) => cached.get(name) !== path).map(([name]) => name);
|
|
24822
24829
|
}
|
|
@@ -24829,6 +24836,33 @@ function unseenByEditor(projectPath, editorHolds) {
|
|
|
24829
24836
|
return [...declaredClasses(projectPath)].filter(([name]) => !held.has(name)).map(([className, path]) => ({ className, path }));
|
|
24830
24837
|
}
|
|
24831
24838
|
|
|
24839
|
+
// src/config-pin.ts
|
|
24840
|
+
import { existsSync as existsSync3, readFileSync as readFileSync5 } from "node:fs";
|
|
24841
|
+
import { join as join4 } from "node:path";
|
|
24842
|
+
var PINNED = /\bgdharness@(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)\b/g;
|
|
24843
|
+
function versionsNamedInConfig(projectPath) {
|
|
24844
|
+
const config = join4(projectPath, ".mcp.json");
|
|
24845
|
+
if (!existsSync3(config)) {
|
|
24846
|
+
return [];
|
|
24847
|
+
}
|
|
24848
|
+
let text;
|
|
24849
|
+
try {
|
|
24850
|
+
text = readFileSync5(config, "utf8");
|
|
24851
|
+
JSON.parse(text);
|
|
24852
|
+
} catch {
|
|
24853
|
+
return [];
|
|
24854
|
+
}
|
|
24855
|
+
return [...new Set([...text.matchAll(PINNED)].map((found) => found[1] ?? ""))].filter((version) => version !== "");
|
|
24856
|
+
}
|
|
24857
|
+
function configDisagrees(projectPath, serverVersion) {
|
|
24858
|
+
const named = versionsNamedInConfig(projectPath);
|
|
24859
|
+
const others = named.filter((version) => version !== serverVersion);
|
|
24860
|
+
if (others.length === 0) {
|
|
24861
|
+
return null;
|
|
24862
|
+
}
|
|
24863
|
+
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.";
|
|
24864
|
+
}
|
|
24865
|
+
|
|
24832
24866
|
// src/dap_client.ts
|
|
24833
24867
|
import { createConnection } from "node:net";
|
|
24834
24868
|
import { setTimeout as delay } from "node:timers/promises";
|
|
@@ -25554,11 +25588,19 @@ var import_websocket = __toESM(require_websocket(), 1);
|
|
|
25554
25588
|
var import_websocket_server = __toESM(require_websocket_server(), 1);
|
|
25555
25589
|
|
|
25556
25590
|
// src/paths.ts
|
|
25591
|
+
import { realpathSync } from "node:fs";
|
|
25557
25592
|
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
25558
25593
|
var RESOURCE_SCHEME = "res://";
|
|
25559
25594
|
var OTHER_SCHEME = /^[a-zA-Z][a-zA-Z\d+.-]+:\/\//;
|
|
25595
|
+
function realPathOr(path) {
|
|
25596
|
+
try {
|
|
25597
|
+
return realpathSync.native(path);
|
|
25598
|
+
} catch {
|
|
25599
|
+
return path;
|
|
25600
|
+
}
|
|
25601
|
+
}
|
|
25560
25602
|
function isSameDirectory(onePath, otherPath) {
|
|
25561
|
-
return relative(resolve(onePath), resolve(otherPath)) === "";
|
|
25603
|
+
return relative(realPathOr(resolve(onePath)), realPathOr(resolve(otherPath))) === "";
|
|
25562
25604
|
}
|
|
25563
25605
|
function isWithinRoot(rootPath, candidatePath) {
|
|
25564
25606
|
const step = relative(resolve(rootPath), resolve(candidatePath));
|
|
@@ -26082,14 +26124,14 @@ function getDefaultBridge(ownProject = null) {
|
|
|
26082
26124
|
|
|
26083
26125
|
// src/godot-path.ts
|
|
26084
26126
|
import { execFile } from "node:child_process";
|
|
26085
|
-
import { existsSync as
|
|
26127
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
26086
26128
|
import { normalize } from "node:path";
|
|
26087
26129
|
import { promisify } from "node:util";
|
|
26088
26130
|
|
|
26089
26131
|
// src/detection.ts
|
|
26090
|
-
import { existsSync as
|
|
26132
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
|
|
26091
26133
|
import { homedir as homedir2 } from "node:os";
|
|
26092
|
-
import { join as
|
|
26134
|
+
import { join as join5 } from "node:path";
|
|
26093
26135
|
function resolveHomeDirectory() {
|
|
26094
26136
|
try {
|
|
26095
26137
|
return homedir2();
|
|
@@ -26098,7 +26140,7 @@ function resolveHomeDirectory() {
|
|
|
26098
26140
|
}
|
|
26099
26141
|
}
|
|
26100
26142
|
function scanDirectoryForGodotBinaries(directory, platform) {
|
|
26101
|
-
if (!directory || !
|
|
26143
|
+
if (!directory || !existsSync4(directory)) {
|
|
26102
26144
|
return [];
|
|
26103
26145
|
}
|
|
26104
26146
|
let entries;
|
|
@@ -26113,16 +26155,16 @@ function scanDirectoryForGodotBinaries(directory, platform) {
|
|
|
26113
26155
|
if (!pattern.test(name)) {
|
|
26114
26156
|
continue;
|
|
26115
26157
|
}
|
|
26116
|
-
const fullPath =
|
|
26158
|
+
const fullPath = join5(directory, name);
|
|
26117
26159
|
try {
|
|
26118
|
-
const stat =
|
|
26160
|
+
const stat = statSync2(fullPath);
|
|
26119
26161
|
if (stat.isFile()) {
|
|
26120
26162
|
matches.push({ name, mtime: stat.mtimeMs });
|
|
26121
26163
|
}
|
|
26122
26164
|
} catch {}
|
|
26123
26165
|
}
|
|
26124
26166
|
matches.sort((a, b) => b.mtime - a.mtime);
|
|
26125
|
-
return matches.map((m) =>
|
|
26167
|
+
return matches.map((m) => join5(directory, m.name));
|
|
26126
26168
|
}
|
|
26127
26169
|
function conventionalPaths(platform, home) {
|
|
26128
26170
|
const paths = ["godot"];
|
|
@@ -26254,7 +26296,7 @@ class GodotLocator {
|
|
|
26254
26296
|
return known;
|
|
26255
26297
|
}
|
|
26256
26298
|
let ok = false;
|
|
26257
|
-
if (path === "godot" ||
|
|
26299
|
+
if (path === "godot" || existsSync5(path)) {
|
|
26258
26300
|
try {
|
|
26259
26301
|
await run(path, ["--version"]);
|
|
26260
26302
|
ok = true;
|
|
@@ -26271,7 +26313,7 @@ class GodotLocator {
|
|
|
26271
26313
|
import { execFile as execFile2 } from "node:child_process";
|
|
26272
26314
|
import { mkdtempSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
26273
26315
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
26274
|
-
import { join as
|
|
26316
|
+
import { join as join6 } from "node:path";
|
|
26275
26317
|
import { promisify as promisify2 } from "node:util";
|
|
26276
26318
|
var run2 = promisify2(execFile2);
|
|
26277
26319
|
function snakeCased(params) {
|
|
@@ -26312,13 +26354,13 @@ function reason(stdout, stderr) {
|
|
|
26312
26354
|
return stdout.trim().split(/\r?\n/).at(-1) ?? "no output at all";
|
|
26313
26355
|
}
|
|
26314
26356
|
async function runOperation(engine, operation, params, projectPath) {
|
|
26315
|
-
const paramsDir = mkdtempSync(
|
|
26316
|
-
const paramsFile =
|
|
26357
|
+
const paramsDir = mkdtempSync(join6(tmpdir2(), "gdharness-params-"));
|
|
26358
|
+
const paramsFile = join6(paramsDir, `${operation}.json`);
|
|
26317
26359
|
writeFileSync3(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
|
|
26318
26360
|
const args = [
|
|
26319
26361
|
"--headless",
|
|
26320
26362
|
"--log-file",
|
|
26321
|
-
|
|
26363
|
+
join6(paramsDir, "engine.log"),
|
|
26322
26364
|
"--path",
|
|
26323
26365
|
projectPath,
|
|
26324
26366
|
"--script",
|
|
@@ -26392,6 +26434,9 @@ var HEADLESS_OPERATIONS = dictionary({
|
|
|
26392
26434
|
inheritance: "inspect_inheritance"
|
|
26393
26435
|
})
|
|
26394
26436
|
});
|
|
26437
|
+
var EDITOR_READS = dictionary({
|
|
26438
|
+
project_settings: dictionary({ get: "get_project_setting" })
|
|
26439
|
+
});
|
|
26395
26440
|
|
|
26396
26441
|
// src/issues.ts
|
|
26397
26442
|
import { createHash as createHash2 } from "node:crypto";
|
|
@@ -26718,7 +26763,7 @@ function orphansPrinted(printed) {
|
|
|
26718
26763
|
}
|
|
26719
26764
|
|
|
26720
26765
|
// src/lsp_client.ts
|
|
26721
|
-
import { realpathSync } from "node:fs";
|
|
26766
|
+
import { realpathSync as realpathSync2 } from "node:fs";
|
|
26722
26767
|
import { readFile, realpath } from "node:fs/promises";
|
|
26723
26768
|
import { createConnection as createConnection2 } from "node:net";
|
|
26724
26769
|
import { dirname as dirname2, resolve as resolve2 } from "node:path";
|
|
@@ -26736,7 +26781,7 @@ function diagnosticsKey(uri) {
|
|
|
26736
26781
|
return decoded;
|
|
26737
26782
|
}
|
|
26738
26783
|
try {
|
|
26739
|
-
return
|
|
26784
|
+
return realpathSync2.native(fileURLToPath(decoded)).toLowerCase();
|
|
26740
26785
|
} catch {
|
|
26741
26786
|
return decoded.toLowerCase();
|
|
26742
26787
|
}
|
|
@@ -27287,14 +27332,14 @@ function secondsFromClock(printed) {
|
|
|
27287
27332
|
}
|
|
27288
27333
|
|
|
27289
27334
|
// src/project-scan.ts
|
|
27290
|
-
import { existsSync as
|
|
27291
|
-
import { join as
|
|
27335
|
+
import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "node:fs";
|
|
27336
|
+
import { join as join7 } from "node:path";
|
|
27292
27337
|
var SKIPPED = new Set(["node_modules"]);
|
|
27293
27338
|
function skipped(name) {
|
|
27294
27339
|
return name.startsWith(".") || SKIPPED.has(name);
|
|
27295
27340
|
}
|
|
27296
27341
|
function steppedOver(directory) {
|
|
27297
|
-
return
|
|
27342
|
+
return existsSync6(join7(directory, ".gdignore"));
|
|
27298
27343
|
}
|
|
27299
27344
|
var ASSET_EXTENSIONS = new Set(["png", "jpg", "jpeg", "webp", "svg", "ttf", "otf", "wav", "mp3", "ogg"]);
|
|
27300
27345
|
function projectStructure(projectPath) {
|
|
@@ -27308,7 +27353,7 @@ function projectStructure(projectPath) {
|
|
|
27308
27353
|
continue;
|
|
27309
27354
|
}
|
|
27310
27355
|
if (entry.isDirectory()) {
|
|
27311
|
-
visit(
|
|
27356
|
+
visit(join7(directory, entry.name));
|
|
27312
27357
|
} else if (entry.isFile()) {
|
|
27313
27358
|
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
27314
27359
|
if (extension === "tscn") {
|
|
@@ -27353,7 +27398,7 @@ function searchProject(projectPath, options) {
|
|
|
27353
27398
|
if (skipped(entry.name)) {
|
|
27354
27399
|
continue;
|
|
27355
27400
|
}
|
|
27356
|
-
const entryPath =
|
|
27401
|
+
const entryPath = join7(directory, entry.name);
|
|
27357
27402
|
if (entry.isDirectory()) {
|
|
27358
27403
|
visit(entryPath);
|
|
27359
27404
|
continue;
|
|
@@ -27364,7 +27409,7 @@ function searchProject(projectPath, options) {
|
|
|
27364
27409
|
}
|
|
27365
27410
|
result.summary.files_searched += 1;
|
|
27366
27411
|
const matches = [];
|
|
27367
|
-
for (const [index, line] of
|
|
27412
|
+
for (const [index, line] of readFileSync6(entryPath, "utf8").split(`
|
|
27368
27413
|
`).entries()) {
|
|
27369
27414
|
if (full()) {
|
|
27370
27415
|
break;
|
|
@@ -27387,7 +27432,7 @@ function searchProject(projectPath, options) {
|
|
|
27387
27432
|
}
|
|
27388
27433
|
|
|
27389
27434
|
// src/resources.ts
|
|
27390
|
-
import { readFileSync as
|
|
27435
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
27391
27436
|
import { extname, resolve as resolve3 } from "node:path";
|
|
27392
27437
|
var STATIC_RESOURCES = [
|
|
27393
27438
|
{
|
|
@@ -27558,7 +27603,7 @@ function readResourceText(uri, getProjectPath) {
|
|
|
27558
27603
|
const parsedUri = parseGodotUri(uri);
|
|
27559
27604
|
if (parsedUri.kind === "project-info") {
|
|
27560
27605
|
const projectFilePath = resolveProjectFile(projectPath, "project.godot");
|
|
27561
|
-
const rawProject =
|
|
27606
|
+
const rawProject = readFileSync7(projectFilePath, "utf-8");
|
|
27562
27607
|
const parsedProject = parseProjectGodot(rawProject);
|
|
27563
27608
|
return {
|
|
27564
27609
|
mimeType: "application/json",
|
|
@@ -27567,7 +27612,7 @@ function readResourceText(uri, getProjectPath) {
|
|
|
27567
27612
|
}
|
|
27568
27613
|
const filePath = resolveProjectFile(projectPath, parsedUri.resourcePath);
|
|
27569
27614
|
ensureAllowedExtension(parsedUri.kind, filePath);
|
|
27570
|
-
const text =
|
|
27615
|
+
const text = readFileSync7(filePath, "utf-8");
|
|
27571
27616
|
return {
|
|
27572
27617
|
mimeType: parsedUri.kind === "script" ? "text/x-gdscript" : "text/plain",
|
|
27573
27618
|
text
|
|
@@ -27605,14 +27650,14 @@ function setupResourceHandlers(mcp, getProjectPath) {
|
|
|
27605
27650
|
|
|
27606
27651
|
// src/run-record.ts
|
|
27607
27652
|
import { execFileSync } from "node:child_process";
|
|
27608
|
-
import { mkdirSync as mkdirSync3, openSync, readdirSync as readdirSync5, readFileSync as
|
|
27609
|
-
import { basename, join as
|
|
27653
|
+
import { mkdirSync as mkdirSync3, openSync, readdirSync as readdirSync5, readFileSync as readFileSync9, rmSync as rmSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
27654
|
+
import { basename, join as join9 } from "node:path";
|
|
27610
27655
|
|
|
27611
27656
|
// src/runtime-client.ts
|
|
27612
|
-
import { existsSync as
|
|
27657
|
+
import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as readFileSync8, unlinkSync } from "node:fs";
|
|
27613
27658
|
import { createConnection as createConnection3 } from "node:net";
|
|
27614
27659
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
27615
|
-
import { join as
|
|
27660
|
+
import { join as join8, resolve as resolve4 } from "node:path";
|
|
27616
27661
|
import { setTimeout as delay2 } from "node:timers/promises";
|
|
27617
27662
|
|
|
27618
27663
|
// src/tool-args.ts
|
|
@@ -27680,13 +27725,13 @@ function runtimeDirectory(variables = process.env) {
|
|
|
27680
27725
|
return explicit;
|
|
27681
27726
|
}
|
|
27682
27727
|
const perUser = envValue("XDG_RUNTIME_DIR", variables);
|
|
27683
|
-
return
|
|
27728
|
+
return join8(perUser ?? tmpdir3(), "gdharness");
|
|
27684
27729
|
}
|
|
27685
27730
|
function runtimeDirectories(variables = process.env) {
|
|
27686
27731
|
const candidates = [runtimeDirectory(variables)];
|
|
27687
27732
|
const fallbacks = process.platform === "win32" ? [envValue("SystemRoot", variables) ?? envValue("windir", variables) ?? "C:\\Windows"] : ["/tmp", "/var/tmp"];
|
|
27688
27733
|
for (const base of fallbacks) {
|
|
27689
|
-
candidates.push(
|
|
27734
|
+
candidates.push(join8(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
|
|
27690
27735
|
}
|
|
27691
27736
|
return [...new Set(candidates.map((path) => resolve4(path)))];
|
|
27692
27737
|
}
|
|
@@ -27701,7 +27746,7 @@ function processAlive(pid) {
|
|
|
27701
27746
|
function parseAnnouncement(file, pid) {
|
|
27702
27747
|
let fields;
|
|
27703
27748
|
try {
|
|
27704
|
-
fields = asParams(JSON.parse(
|
|
27749
|
+
fields = asParams(JSON.parse(readFileSync8(file, "utf8")));
|
|
27705
27750
|
} catch {
|
|
27706
27751
|
return { kind: "rubbish" };
|
|
27707
27752
|
}
|
|
@@ -27740,7 +27785,7 @@ function discoverRuntimes(directories = runtimeDirectories()) {
|
|
|
27740
27785
|
return runtimesAnnounced(directories).running;
|
|
27741
27786
|
}
|
|
27742
27787
|
function announcedIn(directory) {
|
|
27743
|
-
if (!
|
|
27788
|
+
if (!existsSync7(directory)) {
|
|
27744
27789
|
return [];
|
|
27745
27790
|
}
|
|
27746
27791
|
const found = [];
|
|
@@ -27749,7 +27794,7 @@ function announcedIn(directory) {
|
|
|
27749
27794
|
if (!match) {
|
|
27750
27795
|
continue;
|
|
27751
27796
|
}
|
|
27752
|
-
const file =
|
|
27797
|
+
const file = join8(directory, entry);
|
|
27753
27798
|
const pid = Number.parseInt(match[1] ?? "", 10);
|
|
27754
27799
|
const announced = processAlive(pid) ? parseAnnouncement(file, pid) : { kind: "rubbish" };
|
|
27755
27800
|
if (announced.kind === "rubbish") {
|
|
@@ -27910,17 +27955,17 @@ function runtimeRequest(endpoint, command, params, timeoutMs) {
|
|
|
27910
27955
|
|
|
27911
27956
|
// src/run-record.ts
|
|
27912
27957
|
function runsDirectory() {
|
|
27913
|
-
return
|
|
27958
|
+
return join9(runtimeDirectory(), "runs");
|
|
27914
27959
|
}
|
|
27915
27960
|
function recordPath() {
|
|
27916
|
-
return
|
|
27961
|
+
return join9(runsDirectory(), "run.json");
|
|
27917
27962
|
}
|
|
27918
27963
|
function recordPaths() {
|
|
27919
|
-
return runtimeDirectories().map((directory) =>
|
|
27964
|
+
return runtimeDirectories().map((directory) => join9(directory, "runs", "run.json"));
|
|
27920
27965
|
}
|
|
27921
27966
|
function openTranscript(startedAt) {
|
|
27922
27967
|
mkdirSync3(runsDirectory(), { recursive: true });
|
|
27923
|
-
const path =
|
|
27968
|
+
const path = join9(runsDirectory(), `run-${startedAt}.log`);
|
|
27924
27969
|
return { path, fd: openSync(path, "a") };
|
|
27925
27970
|
}
|
|
27926
27971
|
function writeRunRecord(record) {
|
|
@@ -27949,7 +27994,7 @@ function readRunRecord() {
|
|
|
27949
27994
|
function recordAt(path) {
|
|
27950
27995
|
let parsed;
|
|
27951
27996
|
try {
|
|
27952
|
-
parsed = JSON.parse(
|
|
27997
|
+
parsed = JSON.parse(readFileSync9(path, "utf8"));
|
|
27953
27998
|
} catch {
|
|
27954
27999
|
return null;
|
|
27955
28000
|
}
|
|
@@ -27985,7 +28030,7 @@ function runningAs(pid) {
|
|
|
27985
28030
|
}
|
|
27986
28031
|
try {
|
|
27987
28032
|
if (process.platform === "linux") {
|
|
27988
|
-
const raw =
|
|
28033
|
+
const raw = readFileSync9(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
|
|
27989
28034
|
return raw === "" ? null : { kind: "commandLine", text: raw };
|
|
27990
28035
|
}
|
|
27991
28036
|
const args = execFileSync("ps", ["-p", String(pid), "-o", "args="], {
|
|
@@ -28060,7 +28105,7 @@ function clearRunRecord(ours = () => true) {
|
|
|
28060
28105
|
}
|
|
28061
28106
|
function ageMs(path, now) {
|
|
28062
28107
|
try {
|
|
28063
|
-
return now -
|
|
28108
|
+
return now - statSync3(path).mtimeMs;
|
|
28064
28109
|
} catch {
|
|
28065
28110
|
return 0;
|
|
28066
28111
|
}
|
|
@@ -28078,7 +28123,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
28078
28123
|
if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
|
|
28079
28124
|
continue;
|
|
28080
28125
|
}
|
|
28081
|
-
const path =
|
|
28126
|
+
const path = join9(directory, entry);
|
|
28082
28127
|
if (path === keep || ageMs(path, now) < keepMs) {
|
|
28083
28128
|
continue;
|
|
28084
28129
|
}
|
|
@@ -28089,8 +28134,8 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
28089
28134
|
}
|
|
28090
28135
|
|
|
28091
28136
|
// src/setup.ts
|
|
28092
|
-
import { cpSync, existsSync as
|
|
28093
|
-
import { dirname as dirname3, join as
|
|
28137
|
+
import { cpSync, existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync10, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
28138
|
+
import { dirname as dirname3, join as join10 } from "node:path";
|
|
28094
28139
|
var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
|
|
28095
28140
|
var RUNTIME_AUTOLOAD = {
|
|
28096
28141
|
name: "GdharnessRuntime",
|
|
@@ -28098,15 +28143,23 @@ var RUNTIME_AUTOLOAD = {
|
|
|
28098
28143
|
};
|
|
28099
28144
|
var VERSION_MARKER = ".gdharness-version";
|
|
28100
28145
|
function installedAddonVersion(projectPath) {
|
|
28101
|
-
const marker =
|
|
28102
|
-
if (!
|
|
28146
|
+
const marker = join10(projectPath, "addons", ADDONS[0], VERSION_MARKER);
|
|
28147
|
+
if (!existsSync8(marker)) {
|
|
28103
28148
|
return null;
|
|
28104
28149
|
}
|
|
28105
|
-
const said =
|
|
28150
|
+
const said = readFileSync10(marker, "utf8").trim();
|
|
28106
28151
|
return said === "" ? null : said;
|
|
28107
28152
|
}
|
|
28108
28153
|
|
|
28109
28154
|
// src/tool-definitions.ts
|
|
28155
|
+
var PROJECT_INFO_SECTIONS = [
|
|
28156
|
+
"autoloads",
|
|
28157
|
+
"plugins",
|
|
28158
|
+
"export_presets",
|
|
28159
|
+
"audio_buses",
|
|
28160
|
+
"health",
|
|
28161
|
+
"validation"
|
|
28162
|
+
];
|
|
28110
28163
|
function opTakes(spec, op, name) {
|
|
28111
28164
|
const ops = spec.parameters[name]?.ops;
|
|
28112
28165
|
return ops === undefined || ops.includes(op);
|
|
@@ -28140,7 +28193,7 @@ var NODE_PATH = {
|
|
|
28140
28193
|
};
|
|
28141
28194
|
var PROPERTIES = {
|
|
28142
28195
|
type: "object",
|
|
28143
|
-
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}.',
|
|
28196
|
+
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.',
|
|
28144
28197
|
additionalProperties: true
|
|
28145
28198
|
};
|
|
28146
28199
|
var XY = {
|
|
@@ -28263,10 +28316,7 @@ var TOOL_SPECS = [
|
|
|
28263
28316
|
projectPath: PROJECT_PATH,
|
|
28264
28317
|
include: {
|
|
28265
28318
|
type: "array",
|
|
28266
|
-
items: {
|
|
28267
|
-
type: "string",
|
|
28268
|
-
enum: ["autoloads", "plugins", "export_presets", "audio_buses", "health", "validation"]
|
|
28269
|
-
},
|
|
28319
|
+
items: { type: "string", enum: PROJECT_INFO_SECTIONS },
|
|
28270
28320
|
description: "Extra sections: registered autoloads, addons and whether each is enabled, export presets, the audio bus layout, a health report, or export validation."
|
|
28271
28321
|
},
|
|
28272
28322
|
preset: { type: "string", description: "For validation: the export preset to validate against." },
|
|
@@ -28284,6 +28334,12 @@ var TOOL_SPECS = [
|
|
|
28284
28334
|
parameters: {
|
|
28285
28335
|
projectPath: PROJECT_PATH,
|
|
28286
28336
|
setting: { type: "string", description: 'Setting path, such as "display/window/size/viewport_width".' },
|
|
28337
|
+
from: {
|
|
28338
|
+
type: "string",
|
|
28339
|
+
enum: ["disk", "editor"],
|
|
28340
|
+
ops: ["get"],
|
|
28341
|
+
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."
|
|
28342
|
+
},
|
|
28287
28343
|
prefix: {
|
|
28288
28344
|
type: "string",
|
|
28289
28345
|
ops: ["get"],
|
|
@@ -28775,7 +28831,7 @@ var TOOL_SPECS = [
|
|
|
28775
28831
|
},
|
|
28776
28832
|
{
|
|
28777
28833
|
name: "editor_rescan",
|
|
28778
|
-
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,
|
|
28834
|
+
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.",
|
|
28779
28835
|
parameters: {
|
|
28780
28836
|
projectPath: PROJECT_PATH,
|
|
28781
28837
|
timeoutMs: { type: "number", description: "How long to wait for the scan. Default 30000." }
|
|
@@ -29285,8 +29341,34 @@ function wrongTypes(spec, args) {
|
|
|
29285
29341
|
}
|
|
29286
29342
|
return complaints;
|
|
29287
29343
|
}
|
|
29344
|
+
function unlistedValues(schema, value, path) {
|
|
29345
|
+
if (typeof schema !== "object" || schema === null) {
|
|
29346
|
+
return [];
|
|
29347
|
+
}
|
|
29348
|
+
const fields = schema;
|
|
29349
|
+
const listed = fields["enum"];
|
|
29350
|
+
if (Array.isArray(listed) && typeof value === "string") {
|
|
29351
|
+
return listed.includes(value) ? [] : [`${path} as one of ${listed.join(", ")}, not ${JSON.stringify(value)}`];
|
|
29352
|
+
}
|
|
29353
|
+
if (Array.isArray(value)) {
|
|
29354
|
+
return value.flatMap((element, index) => unlistedValues(fields["items"], element, `${path}[${index}]`));
|
|
29355
|
+
}
|
|
29356
|
+
const properties = fields["properties"];
|
|
29357
|
+
if (isPlainObject3(properties) && isPlainObject3(value)) {
|
|
29358
|
+
return Object.entries(value).flatMap(([field, held]) => Object.hasOwn(properties, field) ? unlistedValues(properties[field], held, `${path}.${field}`) : []);
|
|
29359
|
+
}
|
|
29360
|
+
return [];
|
|
29361
|
+
}
|
|
29362
|
+
function isPlainObject3(value) {
|
|
29363
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
29364
|
+
}
|
|
29365
|
+
function outsideTheirLists(spec, args) {
|
|
29366
|
+
return Object.entries(args).flatMap(([name, value]) => unlistedValues(spec.parameters[name], value, name));
|
|
29367
|
+
}
|
|
29288
29368
|
var BRIDGE_RETRY_MS = 2000;
|
|
29289
29369
|
var SUCCESSOR_CHECK_MS = 1e4;
|
|
29370
|
+
var CACHE_WRITE_MS = 2000;
|
|
29371
|
+
var SCAN_START_MS = 2000;
|
|
29290
29372
|
var WAIT_FOR_RUN_MS = 600000;
|
|
29291
29373
|
var RUN_POLL_MS = 250;
|
|
29292
29374
|
var PATH_SOLUTIONS = [
|
|
@@ -29320,7 +29402,7 @@ var DEBUG_STATE_CALLS = dictionary({
|
|
|
29320
29402
|
output: "dap_get_output",
|
|
29321
29403
|
variables: "dap_get_variables"
|
|
29322
29404
|
});
|
|
29323
|
-
var
|
|
29405
|
+
var PROJECT_INFO_SECTIONS2 = dictionary({
|
|
29324
29406
|
autoloads: { operation: "list_autoloads", params: () => ({}) },
|
|
29325
29407
|
plugins: { operation: "list_plugins", params: () => ({}) },
|
|
29326
29408
|
export_presets: { operation: "list_export_presets", params: () => ({}) },
|
|
@@ -29331,17 +29413,6 @@ var PROJECT_INFO_SECTIONS = dictionary({
|
|
|
29331
29413
|
params: (args, detailed) => ({ preset: readString(args, "preset") ?? "", includeSuggestions: detailed })
|
|
29332
29414
|
}
|
|
29333
29415
|
});
|
|
29334
|
-
function realPathOr(path) {
|
|
29335
|
-
try {
|
|
29336
|
-
return realpathSync2.native(path);
|
|
29337
|
-
} catch {
|
|
29338
|
-
return path;
|
|
29339
|
-
}
|
|
29340
|
-
}
|
|
29341
|
-
function samePath(one, other) {
|
|
29342
|
-
const settled = (path) => realPathOr(path).replaceAll("\\", "/").replace(/\/+$/, "").toLowerCase();
|
|
29343
|
-
return settled(one) === settled(other);
|
|
29344
|
-
}
|
|
29345
29416
|
var RUNNER_DIRECTORY = "addons/gdUnit4/";
|
|
29346
29417
|
function aboveTheRunner(entry) {
|
|
29347
29418
|
const runner = entry.detail.findIndex((line) => line.includes(RUNNER_DIRECTORY));
|
|
@@ -29356,7 +29427,7 @@ function aboveTheRunner(entry) {
|
|
|
29356
29427
|
}
|
|
29357
29428
|
function elsewhereIn(projectPath, asked) {
|
|
29358
29429
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
29359
|
-
if (wanted === "" ||
|
|
29430
|
+
if (wanted === "" || existsSync9(join11(projectPath, wanted))) {
|
|
29360
29431
|
return "";
|
|
29361
29432
|
}
|
|
29362
29433
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
@@ -29370,11 +29441,11 @@ function elsewhereIn(projectPath, asked) {
|
|
|
29370
29441
|
return near.length === 0 ? "" : ` The project does have ${there}.`;
|
|
29371
29442
|
}
|
|
29372
29443
|
function hasMainScene(projectFile) {
|
|
29373
|
-
const scene = parseProjectGodot(
|
|
29444
|
+
const scene = parseProjectGodot(readFileSync11(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
29374
29445
|
return typeof scene === "string" && scene !== "";
|
|
29375
29446
|
}
|
|
29376
29447
|
function editorPlaysHeadless(projectFile) {
|
|
29377
|
-
const runArgs = parseProjectGodot(
|
|
29448
|
+
const runArgs = parseProjectGodot(readFileSync11(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
29378
29449
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
29379
29450
|
}
|
|
29380
29451
|
function camelCased(params) {
|
|
@@ -29390,7 +29461,7 @@ function camelCased(params) {
|
|
|
29390
29461
|
class GodotServer {
|
|
29391
29462
|
mcp;
|
|
29392
29463
|
locator = new GodotLocator;
|
|
29393
|
-
operationsScript =
|
|
29464
|
+
operationsScript = join11(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
29394
29465
|
godotBridge;
|
|
29395
29466
|
tools = buildToolDefinitions();
|
|
29396
29467
|
activeProcess = null;
|
|
@@ -29664,6 +29735,18 @@ class GodotServer {
|
|
|
29664
29735
|
}
|
|
29665
29736
|
});
|
|
29666
29737
|
}
|
|
29738
|
+
const disagreement = this.configDisagreement();
|
|
29739
|
+
if (disagreement !== null) {
|
|
29740
|
+
this.noticedUpdate = true;
|
|
29741
|
+
this.callsSinceNotice = 0;
|
|
29742
|
+
return this.saying(answer, {
|
|
29743
|
+
config_names_another_version: {
|
|
29744
|
+
server_is: SERVER_VERSION,
|
|
29745
|
+
said: disagreement,
|
|
29746
|
+
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."
|
|
29747
|
+
}
|
|
29748
|
+
});
|
|
29749
|
+
}
|
|
29667
29750
|
const notice = this.updates.notice();
|
|
29668
29751
|
if (notice === null) {
|
|
29669
29752
|
return answer;
|
|
@@ -29690,6 +29773,9 @@ class GodotServer {
|
|
|
29690
29773
|
const installed = installedAddonVersion(this.ownProject);
|
|
29691
29774
|
return installed === null || installed === SERVER_VERSION ? null : installed;
|
|
29692
29775
|
}
|
|
29776
|
+
configDisagreement() {
|
|
29777
|
+
return this.ownProject === null ? null : configDisagrees(this.ownProject, SERVER_VERSION);
|
|
29778
|
+
}
|
|
29693
29779
|
withFeedbackNotice(answer) {
|
|
29694
29780
|
this.callsSinceFeedback += 1;
|
|
29695
29781
|
if (this.callsSinceFeedback < FEEDBACK_NOTICE_EVERY) {
|
|
@@ -29715,7 +29801,7 @@ class GodotServer {
|
|
|
29715
29801
|
response: this.createErrorResponse(`${where} does not take ${unknown.join(", ")}. ${takes} This is gdharness ${SERVER_VERSION}, which is what decides whether an argument exists.`)
|
|
29716
29802
|
};
|
|
29717
29803
|
}
|
|
29718
|
-
const wrong = wrongTypes(spec, args);
|
|
29804
|
+
const wrong = [...wrongTypes(spec, args), ...outsideTheirLists(spec, args)];
|
|
29719
29805
|
if (wrong.length > 0) {
|
|
29720
29806
|
return { ok: false, response: this.createErrorResponse(`${spec.name} takes ${wrong.join("; ")}.`) };
|
|
29721
29807
|
}
|
|
@@ -29767,7 +29853,25 @@ class GodotServer {
|
|
|
29767
29853
|
async dispatch(tool, op, args) {
|
|
29768
29854
|
const headless = HEADLESS_OPERATIONS[tool]?.[op];
|
|
29769
29855
|
if (headless !== undefined) {
|
|
29770
|
-
const
|
|
29856
|
+
const { from: asked, ...answerable } = args;
|
|
29857
|
+
if (asked === "editor") {
|
|
29858
|
+
const editorSide = EDITOR_READS[tool]?.[op];
|
|
29859
|
+
if (editorSide === undefined) {
|
|
29860
|
+
return this.createErrorResponse(`${tool} ${op} cannot be read from the editor, only from disk.`, [
|
|
29861
|
+
'Leave from unset, or pass from: "disk"'
|
|
29862
|
+
]);
|
|
29863
|
+
}
|
|
29864
|
+
if (!this.godotBridge.isConnected()) {
|
|
29865
|
+
return this.createErrorResponse(`${tool} ${op} was asked of the editor, and no editor has reached this server.`, [
|
|
29866
|
+
"editor_status says whether the bridge is up and whether an editor may yet connect",
|
|
29867
|
+
'Open the project with editor_launch, or pass from: "disk" to read the file instead'
|
|
29868
|
+
]);
|
|
29869
|
+
}
|
|
29870
|
+
const { op: _dropped, ...asks } = answerable;
|
|
29871
|
+
const contained = this.containProjectFiles(asks);
|
|
29872
|
+
return contained.ok ? await this.handleViaBridge(editorSide, contained.value) : contained.response;
|
|
29873
|
+
}
|
|
29874
|
+
const answered = await this.headless(headless, answerable);
|
|
29771
29875
|
return headless === "refresh_class_cache" && this.godotBridge.isConnected() ? await this.alsoSayWhatTheEditorCannotSee(answered, args) : answered;
|
|
29772
29876
|
}
|
|
29773
29877
|
const { op: _op, ...arguments_ } = args;
|
|
@@ -29982,8 +30086,8 @@ class GodotServer {
|
|
|
29982
30086
|
if (path === undefined) {
|
|
29983
30087
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
29984
30088
|
}
|
|
29985
|
-
const file =
|
|
29986
|
-
if (!
|
|
30089
|
+
const file = join11(path, "project.godot");
|
|
30090
|
+
if (!existsSync9(file)) {
|
|
29987
30091
|
return {
|
|
29988
30092
|
ok: false,
|
|
29989
30093
|
response: this.createErrorResponse(`Not a Godot project: ${path}`, [
|
|
@@ -30140,9 +30244,9 @@ class GodotServer {
|
|
|
30140
30244
|
}
|
|
30141
30245
|
async handleProjectInfo(args) {
|
|
30142
30246
|
const include = readStringArray(args, "include") ?? [];
|
|
30143
|
-
const unknown = include.filter((name) =>
|
|
30247
|
+
const unknown = include.filter((name) => PROJECT_INFO_SECTIONS2[name] === undefined);
|
|
30144
30248
|
if (unknown.length > 0) {
|
|
30145
|
-
return this.createErrorResponse(`project_info cannot include ${unknown.join(", ")}. Sections: ${Object.keys(
|
|
30249
|
+
return this.createErrorResponse(`project_info cannot include ${unknown.join(", ")}. Sections: ${Object.keys(PROJECT_INFO_SECTIONS2).join(", ")}.`);
|
|
30146
30250
|
}
|
|
30147
30251
|
const project = this.project(args);
|
|
30148
30252
|
if (!project.ok) {
|
|
@@ -30152,7 +30256,7 @@ class GodotServer {
|
|
|
30152
30256
|
if (!engine.ok) {
|
|
30153
30257
|
return engine.response;
|
|
30154
30258
|
}
|
|
30155
|
-
const application = parseProjectGodot(
|
|
30259
|
+
const application = parseProjectGodot(readFileSync11(project.value.file, "utf8"))["application"] ?? {};
|
|
30156
30260
|
const name = application["config/name"];
|
|
30157
30261
|
const mainScene = application["run/main_scene"];
|
|
30158
30262
|
const info = {
|
|
@@ -30164,7 +30268,7 @@ class GodotServer {
|
|
|
30164
30268
|
};
|
|
30165
30269
|
const detailed = args["detail"] === "full";
|
|
30166
30270
|
for (const section of include) {
|
|
30167
|
-
const spec =
|
|
30271
|
+
const spec = PROJECT_INFO_SECTIONS2[section];
|
|
30168
30272
|
if (spec === undefined) {
|
|
30169
30273
|
continue;
|
|
30170
30274
|
}
|
|
@@ -30220,11 +30324,11 @@ class GodotServer {
|
|
|
30220
30324
|
return this.createErrorResponse(`Cannot create ${dirname4(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
30221
30325
|
}
|
|
30222
30326
|
const debug = readBoolean(args, "debug") ?? false;
|
|
30223
|
-
const exportLogs = mkdtempSync2(
|
|
30327
|
+
const exportLogs = mkdtempSync2(join11(tmpdir4(), "gdharness-export-"));
|
|
30224
30328
|
const exportArgs = [
|
|
30225
30329
|
"--headless",
|
|
30226
30330
|
"--log-file",
|
|
30227
|
-
|
|
30331
|
+
join11(exportLogs, "engine.log"),
|
|
30228
30332
|
"--path",
|
|
30229
30333
|
project.value.path,
|
|
30230
30334
|
debug ? "--export-debug" : "--export-release",
|
|
@@ -30252,7 +30356,7 @@ class GodotServer {
|
|
|
30252
30356
|
log.finish();
|
|
30253
30357
|
const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
|
|
30254
30358
|
const verdict = {
|
|
30255
|
-
exported: exitCode === 0 && log.count("error") === 0 &&
|
|
30359
|
+
exported: exitCode === 0 && log.count("error") === 0 && existsSync9(output.absolutePath),
|
|
30256
30360
|
preset,
|
|
30257
30361
|
outputPath: output.relativePath,
|
|
30258
30362
|
debug,
|
|
@@ -30282,7 +30386,7 @@ class GodotServer {
|
|
|
30282
30386
|
return contained.response;
|
|
30283
30387
|
}
|
|
30284
30388
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
30285
|
-
if (!
|
|
30389
|
+
if (!existsSync9(join11(project.value.path, runner))) {
|
|
30286
30390
|
return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
|
|
30287
30391
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
30288
30392
|
]);
|
|
@@ -30317,7 +30421,7 @@ class GodotServer {
|
|
|
30317
30421
|
];
|
|
30318
30422
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
|
|
30319
30423
|
this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
30320
|
-
const userData = mkdtempSync2(
|
|
30424
|
+
const userData = mkdtempSync2(join11(tmpdir4(), "gdharness-tests-"));
|
|
30321
30425
|
const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
|
|
30322
30426
|
const hung = await new Promise((resolve) => {
|
|
30323
30427
|
const timer = setTimeout(() => {
|
|
@@ -30333,14 +30437,14 @@ class GodotServer {
|
|
|
30333
30437
|
resolve(false);
|
|
30334
30438
|
});
|
|
30335
30439
|
});
|
|
30336
|
-
const reportsDir =
|
|
30440
|
+
const reportsDir = join11(project.value.path, ".godot", ...ours.split("/"));
|
|
30337
30441
|
let report = null;
|
|
30338
30442
|
let reportProblem = null;
|
|
30339
30443
|
try {
|
|
30340
|
-
const written =
|
|
30444
|
+
const written = existsSync9(reportsDir) ? readdirSync6(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
30341
30445
|
const newest = written.at(-1);
|
|
30342
30446
|
if (newest !== undefined) {
|
|
30343
|
-
report = parseJUnit(
|
|
30447
|
+
report = parseJUnit(readFileSync11(join11(reportsDir, newest, "results.xml"), "utf8"));
|
|
30344
30448
|
}
|
|
30345
30449
|
} catch (error) {
|
|
30346
30450
|
reportProblem = errorMessage(error);
|
|
@@ -30503,6 +30607,7 @@ class GodotServer {
|
|
|
30503
30607
|
retryingBridge: this.bridgeRetry === null ? undefined : true,
|
|
30504
30608
|
announcedAt: this.announcedAt ?? undefined,
|
|
30505
30609
|
projectIs: this.projectHasMovedOn() ?? undefined,
|
|
30610
|
+
configIs: this.configDisagreement() ?? undefined,
|
|
30506
30611
|
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,
|
|
30507
30612
|
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
|
|
30508
30613
|
};
|
|
@@ -30758,7 +30863,7 @@ class GodotServer {
|
|
|
30758
30863
|
return runIsUp(run, editorSays);
|
|
30759
30864
|
}
|
|
30760
30865
|
async runtimeUp(projectPath, before, budgetMs) {
|
|
30761
|
-
if (!
|
|
30866
|
+
if (!existsSync9(join11(projectPath, RUNTIME_AUTOLOAD.path))) {
|
|
30762
30867
|
return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false });
|
|
30763
30868
|
}
|
|
30764
30869
|
const endpoint = await announcedSince(projectPath, before, {
|
|
@@ -31013,7 +31118,7 @@ class GodotServer {
|
|
|
31013
31118
|
if (mine === null) {
|
|
31014
31119
|
return false;
|
|
31015
31120
|
}
|
|
31016
|
-
return project !== "" &&
|
|
31121
|
+
return project !== "" && isSameDirectory(mine, project);
|
|
31017
31122
|
}
|
|
31018
31123
|
currentRun() {
|
|
31019
31124
|
return this.activeProcess ?? this.adoptRecordedRun();
|
|
@@ -31144,6 +31249,11 @@ class GodotServer {
|
|
|
31144
31249
|
async handleRescanFilesystem(args) {
|
|
31145
31250
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 30000;
|
|
31146
31251
|
const started = Date.now();
|
|
31252
|
+
const projectPath = typeof args["projectPath"] === "string" ? args["projectPath"] : "";
|
|
31253
|
+
const before = projectPath === "" ? null : cachedClasses(projectPath);
|
|
31254
|
+
const writtenBefore = projectPath === "" ? null : cacheWrittenAt(projectPath);
|
|
31255
|
+
const blind = projectPath === "" ? { unseen: [] } : await this.classesTheEditorCannotSee(args);
|
|
31256
|
+
const atRisk = before === null ? [] : blind.unseen.filter((one) => before.has(one.className));
|
|
31147
31257
|
const first = await this.handleViaBridge("rescan_filesystem", args);
|
|
31148
31258
|
if (first.isError) {
|
|
31149
31259
|
return first;
|
|
@@ -31152,17 +31262,27 @@ class GodotServer {
|
|
|
31152
31262
|
while (busy && Date.now() - started < timeoutMs) {
|
|
31153
31263
|
await new Promise((settle) => setTimeout(settle, 100));
|
|
31154
31264
|
const status = asParams(await this.godotBridge.invokeTool("rescan_filesystem", { ...args, statusOnly: true }));
|
|
31155
|
-
|
|
31265
|
+
const pending = Boolean(status["pending"]) && Date.now() - started < SCAN_START_MS;
|
|
31266
|
+
busy = Boolean(status["scanning"]) || Boolean(status["importing"]) || pending;
|
|
31267
|
+
}
|
|
31268
|
+
if (!busy && atRisk.length > 0) {
|
|
31269
|
+
const until = Date.now() + CACHE_WRITE_MS;
|
|
31270
|
+
while (Date.now() < until && cacheWrittenAt(projectPath) === writtenBefore) {
|
|
31271
|
+
await new Promise((settle) => setTimeout(settle, 100));
|
|
31272
|
+
}
|
|
31156
31273
|
}
|
|
31274
|
+
const after = projectPath === "" || busy ? null : cachedClasses(projectPath);
|
|
31275
|
+
const lost = before === null || after === null ? [] : [...before.keys()].filter((name) => !after.has(name));
|
|
31157
31276
|
const checked = busy ? { unseen: [] } : await this.classesTheEditorCannotSee(args);
|
|
31158
31277
|
const unseen = checked.unseen;
|
|
31159
31278
|
return this.jsonTextResponse({
|
|
31160
|
-
ok: !busy && unseen.length === 0 && checked.unchecked === undefined,
|
|
31279
|
+
ok: !busy && unseen.length === 0 && lost.length === 0 && checked.unchecked === undefined,
|
|
31161
31280
|
stillWorking: busy,
|
|
31162
31281
|
waitedMs: Date.now() - started,
|
|
31163
31282
|
unseenByEditor: unseen.length > 0 ? unseen : undefined,
|
|
31283
|
+
cacheLost: lost.length > 0 ? lost : undefined,
|
|
31164
31284
|
classesUnchecked: checked.unchecked,
|
|
31165
|
-
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
|
|
31285
|
+
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
|
|
31166
31286
|
});
|
|
31167
31287
|
}
|
|
31168
31288
|
async alsoSayWhatTheEditorCannotSee(answered, args) {
|
|
@@ -31187,7 +31307,7 @@ class GodotServer {
|
|
|
31187
31307
|
return { unseen: [], unchecked: "no projectPath, so there was nothing to read the cache from" };
|
|
31188
31308
|
}
|
|
31189
31309
|
const open = this.godotBridge.getStatus().projectPath;
|
|
31190
|
-
if (open === undefined || !
|
|
31310
|
+
if (open === undefined || !isSameDirectory(open, projectPath)) {
|
|
31191
31311
|
return {
|
|
31192
31312
|
unseen: [],
|
|
31193
31313
|
unchecked: `the connected editor is open on ${open ?? "a project it did not name"}, not this one`
|
|
@@ -31215,6 +31335,14 @@ class GodotServer {
|
|
|
31215
31335
|
"editor_status says whether the bridge is up and what it is listening on"
|
|
31216
31336
|
]);
|
|
31217
31337
|
}
|
|
31338
|
+
const open = this.godotBridge.getStatus().projectPath;
|
|
31339
|
+
const meant = readNonEmptyString(args, "projectPath");
|
|
31340
|
+
if (open !== undefined && open !== "" && meant !== undefined && !isSameDirectory(open, meant)) {
|
|
31341
|
+
return this.createErrorResponse(`This call names ${meant}, and the editor on this bridge has ${open} open.`, [
|
|
31342
|
+
"editor_status names the project this server's editor is showing",
|
|
31343
|
+
"One server serves one editor: start a second server for the other project"
|
|
31344
|
+
]);
|
|
31345
|
+
}
|
|
31218
31346
|
try {
|
|
31219
31347
|
return this.jsonTextResponse(markIfStale(await this.godotBridge.invokeTool(toolName, args), this.godotBridge.getStatus().addonVersion, SERVER_VERSION));
|
|
31220
31348
|
} catch (error) {
|
|
@@ -31233,8 +31361,8 @@ class GodotServer {
|
|
|
31233
31361
|
return this.createErrorResponse(choice.problem);
|
|
31234
31362
|
}
|
|
31235
31363
|
const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
|
|
31236
|
-
const screenshotDir = expectsScreenshot ? mkdtempSync2(
|
|
31237
|
-
const screenshotPath = screenshotDir ?
|
|
31364
|
+
const screenshotDir = expectsScreenshot ? mkdtempSync2(join11(tmpdir4(), "gdharness-runtime-screenshot-")) : null;
|
|
31365
|
+
const screenshotPath = screenshotDir ? join11(screenshotDir, "capture.png") : null;
|
|
31238
31366
|
try {
|
|
31239
31367
|
const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs);
|
|
31240
31368
|
if (!reply.ok) {
|
|
@@ -31255,7 +31383,7 @@ class GodotServer {
|
|
|
31255
31383
|
return {
|
|
31256
31384
|
content: [
|
|
31257
31385
|
{ type: "text", text: `Screenshot captured: ${dimensions}` },
|
|
31258
|
-
{ type: "image", data:
|
|
31386
|
+
{ type: "image", data: readFileSync11(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
31259
31387
|
]
|
|
31260
31388
|
};
|
|
31261
31389
|
} finally {
|