gdharness 1.1.9 → 1.1.10
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 +928 -1251
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +21 -2
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +6 -0
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +113 -0
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd.uid +1 -0
- package/build/index.js +94 -157
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// @bun
|
|
3
2
|
import { createRequire } from "node:module";
|
|
4
3
|
var __create = Object.create;
|
|
5
4
|
var __getProtoOf = Object.getPrototypeOf;
|
|
@@ -272,6 +271,83 @@ var init_process_children = __esm(() => {
|
|
|
272
271
|
run = promisify(execFile);
|
|
273
272
|
});
|
|
274
273
|
|
|
274
|
+
// src/bridge-announce.ts
|
|
275
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
276
|
+
import { dirname, join } from "node:path";
|
|
277
|
+
function announcementPath(projectPath) {
|
|
278
|
+
return join(projectPath, ".godot", "gdharness-bridge.json");
|
|
279
|
+
}
|
|
280
|
+
function readAnnouncement(path) {
|
|
281
|
+
try {
|
|
282
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
283
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
const said = parsed;
|
|
287
|
+
if (typeof said.port !== "number" || typeof said.pid !== "number") {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
return said;
|
|
291
|
+
} catch {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
function serverBehind(announcement, startedAt) {
|
|
296
|
+
if (announcement === null || typeof announcement.version !== "string" || startedAt === undefined) {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
const announcedAt = Date.parse(announcement.startedAt);
|
|
300
|
+
if (Number.isNaN(announcedAt) || startedAt > announcedAt + 1000) {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
return { version: announcement.version, pid: announcement.pid };
|
|
304
|
+
}
|
|
305
|
+
function serverServing(projectPath) {
|
|
306
|
+
const announcement = readAnnouncement(announcementPath(projectPath));
|
|
307
|
+
if (announcement === null) {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
return serverBehind(announcement, startTimesOf([announcement.pid]).get(announcement.pid));
|
|
311
|
+
}
|
|
312
|
+
function announcementIsOurs(path) {
|
|
313
|
+
return readAnnouncement(path)?.pid === process.pid;
|
|
314
|
+
}
|
|
315
|
+
function announceBridge(projectPath, bridge) {
|
|
316
|
+
const path = announcementPath(projectPath);
|
|
317
|
+
try {
|
|
318
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
319
|
+
const announcement = {
|
|
320
|
+
protocol: BRIDGE_ANNOUNCE_PROTOCOL,
|
|
321
|
+
host: bridge.host,
|
|
322
|
+
port: bridge.port,
|
|
323
|
+
pid: process.pid,
|
|
324
|
+
version: bridge.version,
|
|
325
|
+
startedAt: new Date().toISOString()
|
|
326
|
+
};
|
|
327
|
+
writeFileSync(path, `${JSON.stringify(announcement, null, 2)}
|
|
328
|
+
`, "utf8");
|
|
329
|
+
return path;
|
|
330
|
+
} catch (error) {
|
|
331
|
+
console.error(`[SERVER] Could not announce the editor bridge at ${path}: ${errorMessage(error)}`);
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
function withdrawBridge(path) {
|
|
336
|
+
if (path === null || !announcementIsOurs(path)) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
try {
|
|
340
|
+
rmSync(path, { force: true });
|
|
341
|
+
} catch (error) {
|
|
342
|
+
console.error(`[SERVER] Could not withdraw the editor bridge announcement: ${errorMessage(error)}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
var BRIDGE_ANNOUNCE_PROTOCOL = 1;
|
|
346
|
+
var init_bridge_announce = __esm(() => {
|
|
347
|
+
init_errors();
|
|
348
|
+
init_process_children();
|
|
349
|
+
});
|
|
350
|
+
|
|
275
351
|
// src/detection.ts
|
|
276
352
|
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
277
353
|
import { homedir } from "node:os";
|
|
@@ -421,6 +497,84 @@ function environmentFor(options, variables = process.env) {
|
|
|
421
497
|
}
|
|
422
498
|
var OPENED_BY_A_SERVER = "GDHARNESS_OPENED_BY_A_SERVER", RESERVED_VARIABLE_PREFIX = "GDHARNESS_", SAVES_NOT_MOVED_NOTE = "On macOS the engine reads user:// off HOME and ignores the variables that move it elsewhere, measured on Godot 4.7.2, so this run wrote its saves where the player keeps theirs. A suite that saves a game has written into the same folder as the copy being played, and one that tidies up after itself may have removed real saves.";
|
|
423
499
|
|
|
500
|
+
// src/godot-path.ts
|
|
501
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
502
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
503
|
+
import { normalize } from "node:path";
|
|
504
|
+
import { promisify as promisify2 } from "node:util";
|
|
505
|
+
|
|
506
|
+
class GodotLocator {
|
|
507
|
+
answered = new Map;
|
|
508
|
+
found = null;
|
|
509
|
+
async find() {
|
|
510
|
+
if (this.found !== null && await this.whyNot(this.found) === null) {
|
|
511
|
+
return { ok: true, path: this.found };
|
|
512
|
+
}
|
|
513
|
+
this.found = null;
|
|
514
|
+
const named = envValue("GODOT_PATH");
|
|
515
|
+
if (named !== undefined) {
|
|
516
|
+
const path = normalize(named);
|
|
517
|
+
const reason = await this.whyNot(path);
|
|
518
|
+
if (reason === null) {
|
|
519
|
+
this.found = path;
|
|
520
|
+
return { ok: true, path };
|
|
521
|
+
}
|
|
522
|
+
return { ok: false, named: path, reason };
|
|
523
|
+
}
|
|
524
|
+
for (const candidate of godotCandidates(process.platform, resolveHomeDirectory())) {
|
|
525
|
+
const path = normalize(candidate);
|
|
526
|
+
if (await this.whyNot(path) === null) {
|
|
527
|
+
this.found = path;
|
|
528
|
+
return { ok: true, path };
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return { ok: false, named: null };
|
|
532
|
+
}
|
|
533
|
+
static refusal(located) {
|
|
534
|
+
if (located.named === null) {
|
|
535
|
+
return {
|
|
536
|
+
message: "No Godot executable found.",
|
|
537
|
+
advice: [
|
|
538
|
+
"Set GODOT_PATH to the Godot 4 executable",
|
|
539
|
+
"Or install Godot where it is usually found, such as /usr/bin/godot or C:\\Program Files\\Godot"
|
|
540
|
+
]
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
return {
|
|
544
|
+
message: `GODOT_PATH is set to ${located.named}, which ${located.reason ?? "does not answer"}.`,
|
|
545
|
+
advice: [
|
|
546
|
+
"Point GODOT_PATH at a Godot 4 executable that answers --version",
|
|
547
|
+
"Or unset it, and the usual install locations are searched instead"
|
|
548
|
+
]
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
async whyNot(path) {
|
|
552
|
+
const known = this.answered.get(path);
|
|
553
|
+
if (known !== undefined) {
|
|
554
|
+
return known;
|
|
555
|
+
}
|
|
556
|
+
let reason;
|
|
557
|
+
if (path !== "godot" && !existsSync2(path)) {
|
|
558
|
+
reason = "does not exist";
|
|
559
|
+
} else {
|
|
560
|
+
try {
|
|
561
|
+
await run2(path, ["--version"]);
|
|
562
|
+
reason = null;
|
|
563
|
+
} catch (error) {
|
|
564
|
+
reason = `does not answer --version: ${errorMessage(error)}`;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
this.answered.set(path, reason);
|
|
568
|
+
return reason;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
var run2;
|
|
572
|
+
var init_godot_path = __esm(() => {
|
|
573
|
+
init_detection();
|
|
574
|
+
init_errors();
|
|
575
|
+
run2 = promisify2(execFile2);
|
|
576
|
+
});
|
|
577
|
+
|
|
424
578
|
// node_modules/yaml/dist/nodes/identity.js
|
|
425
579
|
var require_identity = __commonJS(function(exports) {
|
|
426
580
|
var ALIAS = Symbol.for("yaml.alias");
|
|
@@ -7502,19 +7656,21 @@ class GameLog {
|
|
|
7502
7656
|
this.seen[severity] = this.entries.length;
|
|
7503
7657
|
}
|
|
7504
7658
|
}
|
|
7505
|
-
const matching = this.entries.slice(floor).filter((entry) =>
|
|
7506
|
-
if (!wanted.has(entry.severity)) {
|
|
7507
|
-
return false;
|
|
7508
|
-
}
|
|
7509
|
-
if (needle === undefined) {
|
|
7510
|
-
return true;
|
|
7511
|
-
}
|
|
7512
|
-
return [entry.text, ...entry.detail].some((line) => line.toLowerCase().includes(needle));
|
|
7513
|
-
});
|
|
7659
|
+
const matching = this.entries.slice(floor).filter((entry) => answersTo(entry, options.severity, options.contains));
|
|
7514
7660
|
const omitted = Math.max(0, matching.length - options.limit);
|
|
7515
7661
|
return { entries: matching.slice(omitted), omitted };
|
|
7516
7662
|
}
|
|
7517
7663
|
}
|
|
7664
|
+
function answersTo(entry, severity, contains) {
|
|
7665
|
+
if (!ADMITTED[severity].has(entry.severity)) {
|
|
7666
|
+
return false;
|
|
7667
|
+
}
|
|
7668
|
+
if (contains === undefined) {
|
|
7669
|
+
return true;
|
|
7670
|
+
}
|
|
7671
|
+
const needle = contains.toLowerCase();
|
|
7672
|
+
return [entry.text, ...entry.detail].some((line) => line.toLowerCase().includes(needle));
|
|
7673
|
+
}
|
|
7518
7674
|
var HEADLINE, COLOUR_CODE, STREAMS, ADMITTED;
|
|
7519
7675
|
var init_game_log = __esm(() => {
|
|
7520
7676
|
HEADLINE = /^(USER )?(SCRIPT ERROR|SHADER ERROR|ERROR|WARNING):\s?(.*)$/;
|
|
@@ -7553,6 +7709,127 @@ var init_scratch = __esm(() => {
|
|
|
7553
7709
|
SWEPT = { recursive: true, force: true, maxRetries: 20, retryDelay: 250 };
|
|
7554
7710
|
});
|
|
7555
7711
|
|
|
7712
|
+
// src/headless.ts
|
|
7713
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
7714
|
+
import { mkdtempSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
7715
|
+
import { tmpdir } from "node:os";
|
|
7716
|
+
import { join as join5 } from "node:path";
|
|
7717
|
+
import { promisify as promisify3 } from "node:util";
|
|
7718
|
+
function snakeCased(params) {
|
|
7719
|
+
const result = emptyRecord();
|
|
7720
|
+
for (const [key, value] of Object.entries(params)) {
|
|
7721
|
+
const name = key.startsWith("_") ? key : key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
7722
|
+
result[name] = value;
|
|
7723
|
+
}
|
|
7724
|
+
return result;
|
|
7725
|
+
}
|
|
7726
|
+
function lastJsonObject(stdout) {
|
|
7727
|
+
const lines = stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.startsWith("{"));
|
|
7728
|
+
for (const line of lines.reverse()) {
|
|
7729
|
+
try {
|
|
7730
|
+
const parsed = JSON.parse(line);
|
|
7731
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
7732
|
+
return parsed;
|
|
7733
|
+
}
|
|
7734
|
+
} catch {}
|
|
7735
|
+
}
|
|
7736
|
+
return null;
|
|
7737
|
+
}
|
|
7738
|
+
function problems(stderr) {
|
|
7739
|
+
const log = new GameLog;
|
|
7740
|
+
log.append("stderr", stderr);
|
|
7741
|
+
log.finish();
|
|
7742
|
+
return log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries;
|
|
7743
|
+
}
|
|
7744
|
+
function reason(stdout, stderr) {
|
|
7745
|
+
const own = stderr.split(/\r?\n/).filter((line) => line.startsWith("[ERROR] ")).map((line) => line.slice("[ERROR] ".length));
|
|
7746
|
+
if (own.length > 0) {
|
|
7747
|
+
return own.join("; ");
|
|
7748
|
+
}
|
|
7749
|
+
const engine = problems(stderr).filter((entry) => entry.severity === "error").map((entry) => entry.text);
|
|
7750
|
+
if (engine.length > 0) {
|
|
7751
|
+
return engine.join("; ");
|
|
7752
|
+
}
|
|
7753
|
+
return stdout.trim().split(/\r?\n/).at(-1) ?? "no output at all";
|
|
7754
|
+
}
|
|
7755
|
+
async function runImport(godotPath, projectPath) {
|
|
7756
|
+
const logDir = mkdtempSync(join5(tmpdir(), "gdharness-import-"));
|
|
7757
|
+
try {
|
|
7758
|
+
const { stderr } = await run3(godotPath, ["--headless", "--log-file", join5(logDir, "engine.log"), "--path", projectPath, "--import"], { signal: callSignal() });
|
|
7759
|
+
return { ok: true, messages: problems(stderr) };
|
|
7760
|
+
} catch (error) {
|
|
7761
|
+
if (error instanceof Error && "stdout" in error && "stderr" in error) {
|
|
7762
|
+
const failed = error;
|
|
7763
|
+
return {
|
|
7764
|
+
ok: false,
|
|
7765
|
+
message: `the import pass failed (exit ${failed.code ?? "unknown"}): ${reason(failed.stdout, failed.stderr)}`,
|
|
7766
|
+
messages: problems(failed.stderr)
|
|
7767
|
+
};
|
|
7768
|
+
}
|
|
7769
|
+
return {
|
|
7770
|
+
ok: false,
|
|
7771
|
+
message: `the import pass could not be run: ${error instanceof Error ? error.message : String(error)}`,
|
|
7772
|
+
messages: []
|
|
7773
|
+
};
|
|
7774
|
+
} finally {
|
|
7775
|
+
discard(logDir);
|
|
7776
|
+
}
|
|
7777
|
+
}
|
|
7778
|
+
async function runOperation(engine, operation, params, projectPath) {
|
|
7779
|
+
const paramsDir = mkdtempSync(join5(tmpdir(), "gdharness-params-"));
|
|
7780
|
+
const paramsFile = join5(paramsDir, `${operation}.json`);
|
|
7781
|
+
writeFileSync3(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
|
|
7782
|
+
const args = [
|
|
7783
|
+
"--headless",
|
|
7784
|
+
"--log-file",
|
|
7785
|
+
join5(paramsDir, "engine.log"),
|
|
7786
|
+
"--path",
|
|
7787
|
+
projectPath,
|
|
7788
|
+
"--script",
|
|
7789
|
+
engine.script,
|
|
7790
|
+
operation,
|
|
7791
|
+
`@file:${paramsFile}`,
|
|
7792
|
+
...engine.debug ? ["--debug-godot"] : []
|
|
7793
|
+
];
|
|
7794
|
+
let stdout;
|
|
7795
|
+
let stderr;
|
|
7796
|
+
try {
|
|
7797
|
+
({ stdout, stderr } = await run3(engine.godotPath, args, { signal: callSignal() }));
|
|
7798
|
+
} catch (error) {
|
|
7799
|
+
if (error instanceof Error && "stdout" in error && "stderr" in error) {
|
|
7800
|
+
const failed = error;
|
|
7801
|
+
return {
|
|
7802
|
+
ok: false,
|
|
7803
|
+
message: `${operation} failed (exit ${failed.code ?? "unknown"}): ${reason(failed.stdout, failed.stderr)}`,
|
|
7804
|
+
messages: problems(failed.stderr)
|
|
7805
|
+
};
|
|
7806
|
+
}
|
|
7807
|
+
return {
|
|
7808
|
+
ok: false,
|
|
7809
|
+
message: `${operation} could not be run: ${error instanceof Error ? error.message : String(error)}`,
|
|
7810
|
+
messages: []
|
|
7811
|
+
};
|
|
7812
|
+
} finally {
|
|
7813
|
+
discard(paramsDir);
|
|
7814
|
+
}
|
|
7815
|
+
const payload = lastJsonObject(stdout);
|
|
7816
|
+
if (payload === null) {
|
|
7817
|
+
return {
|
|
7818
|
+
ok: false,
|
|
7819
|
+
message: `${operation} produced no result: ${reason(stdout, stderr)}`,
|
|
7820
|
+
messages: problems(stderr)
|
|
7821
|
+
};
|
|
7822
|
+
}
|
|
7823
|
+
return { ok: true, payload, messages: problems(stderr) };
|
|
7824
|
+
}
|
|
7825
|
+
var run3;
|
|
7826
|
+
var init_headless = __esm(() => {
|
|
7827
|
+
init_call_signal();
|
|
7828
|
+
init_game_log();
|
|
7829
|
+
init_scratch();
|
|
7830
|
+
run3 = promisify3(execFile3);
|
|
7831
|
+
});
|
|
7832
|
+
|
|
7556
7833
|
// src/update-check.ts
|
|
7557
7834
|
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
7558
7835
|
import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
|
|
@@ -7797,8 +8074,132 @@ var init_server_version = __esm(() => {
|
|
|
7797
8074
|
})();
|
|
7798
8075
|
});
|
|
7799
8076
|
|
|
8077
|
+
// src/issues.ts
|
|
8078
|
+
import { createHash } from "node:crypto";
|
|
8079
|
+
import process4 from "node:process";
|
|
8080
|
+
function feedbackNotice() {
|
|
8081
|
+
return JSON.stringify({
|
|
8082
|
+
gdharness_feedback: {
|
|
8083
|
+
what_to_do: "If this session has run into something gdharness cannot do, or a tool that should " + "exist and does not, tell the user what is missing and offer to open an enhancement " + "issue for them. Only if they say yes, and only for a real gap: a call you got wrong " + "is not one. If you cannot open an issue yourself, give them the link and what to " + "write in it.",
|
|
8084
|
+
enhancement_url: ENHANCEMENT_URL
|
|
8085
|
+
}
|
|
8086
|
+
}, null, 2);
|
|
8087
|
+
}
|
|
8088
|
+
function runtime() {
|
|
8089
|
+
const versions = process4.versions;
|
|
8090
|
+
const bun = versions["bun"];
|
|
8091
|
+
return bun === undefined ? `node ${process4.versions.node}` : `bun ${bun}`;
|
|
8092
|
+
}
|
|
8093
|
+
function defectSignature(where, message) {
|
|
8094
|
+
const normalised = `${where}
|
|
8095
|
+
${message}`.toLowerCase().replaceAll("\\", "/").replaceAll(/(?:[a-z]:)?(?:\/+[\w.-]+){2,}/gu, "<path>").replaceAll(/\d+/gu, "0");
|
|
8096
|
+
return createHash("sha256").update(normalised).digest("hex").slice(0, 8);
|
|
8097
|
+
}
|
|
8098
|
+
function facts(where, message, godotVersion) {
|
|
8099
|
+
return [
|
|
8100
|
+
["Where", where],
|
|
8101
|
+
["Error", message],
|
|
8102
|
+
["Version", `gdharness ${SERVER_VERSION}`],
|
|
8103
|
+
["Runtime", `${runtime()}, ${process4.platform} ${process4.arch}`],
|
|
8104
|
+
["Godot", godotVersion ?? "not known at the point this failed"],
|
|
8105
|
+
["Signature", defectSignature(where, message)]
|
|
8106
|
+
];
|
|
8107
|
+
}
|
|
8108
|
+
function filledTemplate(where, message, godotVersion) {
|
|
8109
|
+
return [
|
|
8110
|
+
`**gdharness version**: ${SERVER_VERSION}`,
|
|
8111
|
+
`**Godot version**: ${godotVersion ?? ""}`,
|
|
8112
|
+
`**Bun version**: ${runtime()}`,
|
|
8113
|
+
`**OS**: ${process4.platform} ${process4.arch}`,
|
|
8114
|
+
"**MCP client**:",
|
|
8115
|
+
"",
|
|
8116
|
+
"## What you did",
|
|
8117
|
+
"",
|
|
8118
|
+
where,
|
|
8119
|
+
"",
|
|
8120
|
+
"## What you expected",
|
|
8121
|
+
"",
|
|
8122
|
+
"The call to answer, or to refuse with a reason.",
|
|
8123
|
+
"",
|
|
8124
|
+
"## What happened",
|
|
8125
|
+
"",
|
|
8126
|
+
"gdharness failed in a way it does not model.",
|
|
8127
|
+
"",
|
|
8128
|
+
"```text",
|
|
8129
|
+
message,
|
|
8130
|
+
"```",
|
|
8131
|
+
"",
|
|
8132
|
+
`Signature: \`${defectSignature(where, message)}\``,
|
|
8133
|
+
"",
|
|
8134
|
+
"If the real cause turned out to be the project, the environment or the call, say so here:",
|
|
8135
|
+
"reaching you as a defect rather than as a refusal naming what would have worked is then the",
|
|
8136
|
+
"thing to fix.",
|
|
8137
|
+
"",
|
|
8138
|
+
"## Anything that makes it reproducible",
|
|
8139
|
+
""
|
|
8140
|
+
].join(`
|
|
8141
|
+
`);
|
|
8142
|
+
}
|
|
8143
|
+
|
|
8144
|
+
class DefectsSeen {
|
|
8145
|
+
counts = new Map;
|
|
8146
|
+
before(where, message) {
|
|
8147
|
+
const signature = defectSignature(where, message);
|
|
8148
|
+
const seen = this.counts.get(signature) ?? 0;
|
|
8149
|
+
this.counts.set(signature, seen + 1);
|
|
8150
|
+
return seen;
|
|
8151
|
+
}
|
|
8152
|
+
}
|
|
8153
|
+
function defectReport(where, error, context = {}) {
|
|
8154
|
+
const { godotVersion, seenBefore = 0 } = context;
|
|
8155
|
+
const message = errorMessage(error);
|
|
8156
|
+
const title = `Defect ${defectSignature(where, message)}: ${where}`;
|
|
8157
|
+
const url = `${NEW_ISSUE}?template=bug_report.md&title=${encodeURIComponent(title)}` + `&body=${encodeURIComponent(filledTemplate(where, message, godotVersion))}`;
|
|
8158
|
+
const width = Math.max(...facts(where, message, godotVersion).map(([label]) => (label ?? "").length));
|
|
8159
|
+
return [
|
|
8160
|
+
"gdharness failed in a way it does not model. Whatever went wrong underneath, it reached you",
|
|
8161
|
+
"as a crash rather than as a refusal naming what would have worked, and that much is a defect",
|
|
8162
|
+
"in gdharness however the rest of it turns out.",
|
|
8163
|
+
"",
|
|
8164
|
+
...seenBefore === 0 ? [
|
|
8165
|
+
"Send the call once more before anything else. Some of what reaches this line is a file",
|
|
8166
|
+
"another process had open, or a connection that dropped, and those are gone by the next",
|
|
8167
|
+
"attempt. If the same Signature comes back, it is not one of those, and repeating it",
|
|
8168
|
+
"further only spends turns."
|
|
8169
|
+
] : [
|
|
8170
|
+
`You have had this exact failure ${seenBefore === 1 ? "once" : `${seenBefore} times`} in this session already, so it is a`,
|
|
8171
|
+
"state rather than a passing one, and sending the call again will not change it."
|
|
8172
|
+
],
|
|
8173
|
+
"",
|
|
8174
|
+
...facts(where, message, godotVersion).map(([label, value]) => ` ${(label ?? "").padEnd(width)} ${value ?? ""}`),
|
|
8175
|
+
"",
|
|
8176
|
+
"Do not open an issue on your own initiative. Tell the person whose machine this is what",
|
|
8177
|
+
"broke, and ask whether you may report it to gdharness on their behalf. What the report",
|
|
8178
|
+
"carries is the lines above and nothing else, and whether that goes into a public tracker",
|
|
8179
|
+
"is theirs to decide.",
|
|
8180
|
+
"",
|
|
8181
|
+
"If they say yes, this link carries the report already filled in:",
|
|
8182
|
+
url,
|
|
8183
|
+
"",
|
|
8184
|
+
"If they say yes and you have no way to open an issue yourself, give them that link and",
|
|
8185
|
+
"those lines, and stay with them while they file it.",
|
|
8186
|
+
"",
|
|
8187
|
+
"Worth reporting either way. If you can see that what actually went wrong was the project,",
|
|
8188
|
+
"the environment or the call, then this message is the defect: a failure gdharness knows",
|
|
8189
|
+
"about is meant to arrive as a refusal naming what would have worked, not as this. Say that",
|
|
8190
|
+
"in the report and it is the more useful of the two."
|
|
8191
|
+
].join(`
|
|
8192
|
+
`);
|
|
8193
|
+
}
|
|
8194
|
+
var NEW_ISSUE = "https://github.com/Aureliolo/gdharness/issues/new", ENHANCEMENT_URL;
|
|
8195
|
+
var init_issues = __esm(() => {
|
|
8196
|
+
init_errors();
|
|
8197
|
+
init_server_version();
|
|
8198
|
+
ENHANCEMENT_URL = `${NEW_ISSUE}?template=feature_request.md`;
|
|
8199
|
+
});
|
|
8200
|
+
|
|
7800
8201
|
// src/class-cache.ts
|
|
7801
|
-
import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as
|
|
8202
|
+
import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync5, statSync as statSync2 } from "node:fs";
|
|
7802
8203
|
import { join as join7 } from "node:path";
|
|
7803
8204
|
function declaredClasses(projectPath) {
|
|
7804
8205
|
const declared = new Map;
|
|
@@ -7814,7 +8215,7 @@ function declaredClasses(projectPath) {
|
|
|
7814
8215
|
if (entry.isDirectory()) {
|
|
7815
8216
|
visit(path, `${prefix}${entry.name}/`);
|
|
7816
8217
|
} else if (entry.isFile() && entry.name.endsWith(".gd")) {
|
|
7817
|
-
const found = DECLARATION.exec(
|
|
8218
|
+
const found = DECLARATION.exec(readFileSync5(path, "utf8"));
|
|
7818
8219
|
if (found?.[1]) {
|
|
7819
8220
|
declared.set(found[1], `res://${prefix}${entry.name}`);
|
|
7820
8221
|
}
|
|
@@ -7830,7 +8231,7 @@ function cachedClasses(projectPath) {
|
|
|
7830
8231
|
return null;
|
|
7831
8232
|
}
|
|
7832
8233
|
const listed = new Map;
|
|
7833
|
-
const text =
|
|
8234
|
+
const text = readFileSync5(cache, "utf8");
|
|
7834
8235
|
for (const entry of text.matchAll(/"class":\s*&"([^"]+)"[\s\S]*?"path":\s*"([^"]+)"/g)) {
|
|
7835
8236
|
listed.set(entry[1] ?? "", entry[2] ?? "");
|
|
7836
8237
|
}
|
|
@@ -7982,127 +8383,6 @@ var init_class_cache = __esm(() => {
|
|
|
7982
8383
|
DECLARATION = /^(?:@[a-z_]+(?:\([^)\n]*\))?\s+)*class_name\s+([A-Za-z_][A-Za-z0-9_]*)/m;
|
|
7983
8384
|
});
|
|
7984
8385
|
|
|
7985
|
-
// src/headless.ts
|
|
7986
|
-
import { execFile as execFile4 } from "node:child_process";
|
|
7987
|
-
import { mkdtempSync as mkdtempSync2, writeFileSync as writeFileSync5 } from "node:fs";
|
|
7988
|
-
import { tmpdir as tmpdir3 } from "node:os";
|
|
7989
|
-
import { join as join8 } from "node:path";
|
|
7990
|
-
import { promisify as promisify4 } from "node:util";
|
|
7991
|
-
function snakeCased2(params) {
|
|
7992
|
-
const result = emptyRecord();
|
|
7993
|
-
for (const [key, value] of Object.entries(params)) {
|
|
7994
|
-
const name = key.startsWith("_") ? key : key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
7995
|
-
result[name] = value;
|
|
7996
|
-
}
|
|
7997
|
-
return result;
|
|
7998
|
-
}
|
|
7999
|
-
function lastJsonObject2(stdout) {
|
|
8000
|
-
const lines = stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.startsWith("{"));
|
|
8001
|
-
for (const line of lines.reverse()) {
|
|
8002
|
-
try {
|
|
8003
|
-
const parsed = JSON.parse(line);
|
|
8004
|
-
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
8005
|
-
return parsed;
|
|
8006
|
-
}
|
|
8007
|
-
} catch {}
|
|
8008
|
-
}
|
|
8009
|
-
return null;
|
|
8010
|
-
}
|
|
8011
|
-
function problems2(stderr) {
|
|
8012
|
-
const log = new GameLog;
|
|
8013
|
-
log.append("stderr", stderr);
|
|
8014
|
-
log.finish();
|
|
8015
|
-
return log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries;
|
|
8016
|
-
}
|
|
8017
|
-
function reason2(stdout, stderr) {
|
|
8018
|
-
const own = stderr.split(/\r?\n/).filter((line) => line.startsWith("[ERROR] ")).map((line) => line.slice("[ERROR] ".length));
|
|
8019
|
-
if (own.length > 0) {
|
|
8020
|
-
return own.join("; ");
|
|
8021
|
-
}
|
|
8022
|
-
const engine = problems2(stderr).filter((entry) => entry.severity === "error").map((entry) => entry.text);
|
|
8023
|
-
if (engine.length > 0) {
|
|
8024
|
-
return engine.join("; ");
|
|
8025
|
-
}
|
|
8026
|
-
return stdout.trim().split(/\r?\n/).at(-1) ?? "no output at all";
|
|
8027
|
-
}
|
|
8028
|
-
async function runImport(godotPath, projectPath) {
|
|
8029
|
-
const logDir = mkdtempSync2(join8(tmpdir3(), "gdharness-import-"));
|
|
8030
|
-
try {
|
|
8031
|
-
const { stderr } = await run4(godotPath, ["--headless", "--log-file", join8(logDir, "engine.log"), "--path", projectPath, "--import"], { signal: callSignal() });
|
|
8032
|
-
return { ok: true, messages: problems2(stderr) };
|
|
8033
|
-
} catch (error) {
|
|
8034
|
-
if (error instanceof Error && "stdout" in error && "stderr" in error) {
|
|
8035
|
-
const failed = error;
|
|
8036
|
-
return {
|
|
8037
|
-
ok: false,
|
|
8038
|
-
message: `the import pass failed (exit ${failed.code ?? "unknown"}): ${reason2(failed.stdout, failed.stderr)}`,
|
|
8039
|
-
messages: problems2(failed.stderr)
|
|
8040
|
-
};
|
|
8041
|
-
}
|
|
8042
|
-
return {
|
|
8043
|
-
ok: false,
|
|
8044
|
-
message: `the import pass could not be run: ${error instanceof Error ? error.message : String(error)}`,
|
|
8045
|
-
messages: []
|
|
8046
|
-
};
|
|
8047
|
-
} finally {
|
|
8048
|
-
discard(logDir);
|
|
8049
|
-
}
|
|
8050
|
-
}
|
|
8051
|
-
async function runOperation2(engine, operation, params, projectPath) {
|
|
8052
|
-
const paramsDir = mkdtempSync2(join8(tmpdir3(), "gdharness-params-"));
|
|
8053
|
-
const paramsFile = join8(paramsDir, `${operation}.json`);
|
|
8054
|
-
writeFileSync5(paramsFile, JSON.stringify(snakeCased2(params)), "utf8");
|
|
8055
|
-
const args = [
|
|
8056
|
-
"--headless",
|
|
8057
|
-
"--log-file",
|
|
8058
|
-
join8(paramsDir, "engine.log"),
|
|
8059
|
-
"--path",
|
|
8060
|
-
projectPath,
|
|
8061
|
-
"--script",
|
|
8062
|
-
engine.script,
|
|
8063
|
-
operation,
|
|
8064
|
-
`@file:${paramsFile}`,
|
|
8065
|
-
...engine.debug ? ["--debug-godot"] : []
|
|
8066
|
-
];
|
|
8067
|
-
let stdout;
|
|
8068
|
-
let stderr;
|
|
8069
|
-
try {
|
|
8070
|
-
({ stdout, stderr } = await run4(engine.godotPath, args, { signal: callSignal() }));
|
|
8071
|
-
} catch (error) {
|
|
8072
|
-
if (error instanceof Error && "stdout" in error && "stderr" in error) {
|
|
8073
|
-
const failed = error;
|
|
8074
|
-
return {
|
|
8075
|
-
ok: false,
|
|
8076
|
-
message: `${operation} failed (exit ${failed.code ?? "unknown"}): ${reason2(failed.stdout, failed.stderr)}`,
|
|
8077
|
-
messages: problems2(failed.stderr)
|
|
8078
|
-
};
|
|
8079
|
-
}
|
|
8080
|
-
return {
|
|
8081
|
-
ok: false,
|
|
8082
|
-
message: `${operation} could not be run: ${error instanceof Error ? error.message : String(error)}`,
|
|
8083
|
-
messages: []
|
|
8084
|
-
};
|
|
8085
|
-
} finally {
|
|
8086
|
-
discard(paramsDir);
|
|
8087
|
-
}
|
|
8088
|
-
const payload = lastJsonObject2(stdout);
|
|
8089
|
-
if (payload === null) {
|
|
8090
|
-
return {
|
|
8091
|
-
ok: false,
|
|
8092
|
-
message: `${operation} produced no result: ${reason2(stdout, stderr)}`,
|
|
8093
|
-
messages: problems2(stderr)
|
|
8094
|
-
};
|
|
8095
|
-
}
|
|
8096
|
-
return { ok: true, payload, messages: problems2(stderr) };
|
|
8097
|
-
}
|
|
8098
|
-
var run4;
|
|
8099
|
-
var init_headless = __esm(() => {
|
|
8100
|
-
init_call_signal();
|
|
8101
|
-
init_game_log();
|
|
8102
|
-
init_scratch();
|
|
8103
|
-
run4 = promisify4(execFile4);
|
|
8104
|
-
});
|
|
8105
|
-
|
|
8106
8386
|
// node_modules/zod/v4/core/util.js
|
|
8107
8387
|
function getEnumValues(entries) {
|
|
8108
8388
|
const numericValues = Object.values(entries).filter((v) => typeof v === "number");
|
|
@@ -15268,7 +15548,7 @@ var init_paths = __esm(() => {
|
|
|
15268
15548
|
});
|
|
15269
15549
|
|
|
15270
15550
|
// src/resources.ts
|
|
15271
|
-
import { readFileSync as
|
|
15551
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
15272
15552
|
import { extname, resolve as resolve2 } from "node:path";
|
|
15273
15553
|
function ensureProjectPath(getProjectPath) {
|
|
15274
15554
|
const projectPath = getProjectPath();
|
|
@@ -15434,7 +15714,7 @@ function readResourceText(uri, getProjectPath) {
|
|
|
15434
15714
|
const parsedUri = parseGodotUri(uri);
|
|
15435
15715
|
if (parsedUri.kind === "project-info") {
|
|
15436
15716
|
const projectFilePath = resolveProjectFile(projectPath, "project.godot");
|
|
15437
|
-
const rawProject =
|
|
15717
|
+
const rawProject = readFileSync6(projectFilePath, "utf-8");
|
|
15438
15718
|
const parsedProject = parseProjectGodot(rawProject);
|
|
15439
15719
|
return {
|
|
15440
15720
|
mimeType: "application/json",
|
|
@@ -15443,7 +15723,7 @@ function readResourceText(uri, getProjectPath) {
|
|
|
15443
15723
|
}
|
|
15444
15724
|
const filePath = resolveProjectFile(projectPath, parsedUri.resourcePath);
|
|
15445
15725
|
ensureAllowedExtension(parsedUri.kind, filePath);
|
|
15446
|
-
const text =
|
|
15726
|
+
const text = readFileSync6(filePath, "utf-8");
|
|
15447
15727
|
return {
|
|
15448
15728
|
mimeType: parsedUri.kind === "script" ? "text/x-gdscript" : "text/plain",
|
|
15449
15729
|
text
|
|
@@ -15571,75 +15851,250 @@ function readParams(params, key) {
|
|
|
15571
15851
|
}
|
|
15572
15852
|
|
|
15573
15853
|
// src/setup.ts
|
|
15574
|
-
import {
|
|
15854
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
15855
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
15575
15856
|
import {
|
|
15576
|
-
cpSync
|
|
15577
|
-
existsSync as
|
|
15578
|
-
mkdirSync as
|
|
15579
|
-
readdirSync as
|
|
15580
|
-
readFileSync as
|
|
15581
|
-
rmdirSync
|
|
15582
|
-
rmSync as
|
|
15583
|
-
statSync as
|
|
15584
|
-
writeFileSync as
|
|
15857
|
+
cpSync,
|
|
15858
|
+
existsSync as existsSync7,
|
|
15859
|
+
mkdirSync as mkdirSync4,
|
|
15860
|
+
readdirSync as readdirSync3,
|
|
15861
|
+
readFileSync as readFileSync7,
|
|
15862
|
+
rmdirSync,
|
|
15863
|
+
rmSync as rmSync4,
|
|
15864
|
+
statSync as statSync3,
|
|
15865
|
+
writeFileSync as writeFileSync5
|
|
15585
15866
|
} from "node:fs";
|
|
15586
|
-
import { dirname as
|
|
15587
|
-
import { fileURLToPath
|
|
15588
|
-
function
|
|
15589
|
-
const hash =
|
|
15867
|
+
import { dirname as dirname4, join as join8 } from "node:path";
|
|
15868
|
+
import { fileURLToPath } from "node:url";
|
|
15869
|
+
function editorAddonDigest(from = shippedAddonsDirectory()) {
|
|
15870
|
+
const hash = createHash2("sha256");
|
|
15590
15871
|
const walk = (directory, relative) => {
|
|
15591
|
-
for (const name of
|
|
15592
|
-
if (name ===
|
|
15872
|
+
for (const name of readdirSync3(directory).sort()) {
|
|
15873
|
+
if (name === VERSION_MARKER || name === DIGEST_MARKER) {
|
|
15593
15874
|
continue;
|
|
15594
15875
|
}
|
|
15595
|
-
const path =
|
|
15876
|
+
const path = join8(directory, name);
|
|
15596
15877
|
const inside = `${relative}/${name}`;
|
|
15597
|
-
if (
|
|
15878
|
+
if (statSync3(path).isDirectory()) {
|
|
15598
15879
|
walk(path, inside);
|
|
15599
15880
|
} else {
|
|
15600
15881
|
hash.update(`${inside}
|
|
15601
15882
|
`);
|
|
15602
|
-
hash.update(
|
|
15883
|
+
hash.update(readFileSync7(path));
|
|
15603
15884
|
hash.update(`
|
|
15604
15885
|
`);
|
|
15605
15886
|
}
|
|
15606
15887
|
}
|
|
15607
15888
|
};
|
|
15608
|
-
for (const name of
|
|
15609
|
-
walk(
|
|
15889
|
+
for (const name of EDITOR_PLUGINS) {
|
|
15890
|
+
walk(join8(from, name), name);
|
|
15610
15891
|
}
|
|
15611
15892
|
return hash.digest("hex");
|
|
15612
15893
|
}
|
|
15613
15894
|
function shippedEditorDigest() {
|
|
15614
15895
|
if (shippedDigest === undefined) {
|
|
15615
15896
|
try {
|
|
15616
|
-
shippedDigest =
|
|
15897
|
+
shippedDigest = editorAddonDigest();
|
|
15617
15898
|
} catch {
|
|
15618
15899
|
shippedDigest = null;
|
|
15619
15900
|
}
|
|
15620
15901
|
}
|
|
15621
15902
|
return shippedDigest ?? undefined;
|
|
15622
15903
|
}
|
|
15623
|
-
function
|
|
15624
|
-
|
|
15904
|
+
function installedEditorDigest(projectPath) {
|
|
15905
|
+
const marker = join8(projectPath, "addons", ADDONS[0], DIGEST_MARKER);
|
|
15906
|
+
if (!existsSync7(marker)) {
|
|
15907
|
+
return null;
|
|
15908
|
+
}
|
|
15909
|
+
const said = readFileSync7(marker, "utf8").trim();
|
|
15910
|
+
return said === "" ? null : said;
|
|
15911
|
+
}
|
|
15912
|
+
function shippedAddonsDirectory() {
|
|
15913
|
+
return join8(dirname4(fileURLToPath(import.meta.url)), "godot", "addons");
|
|
15914
|
+
}
|
|
15915
|
+
function shippedOperationsScript() {
|
|
15916
|
+
return join8(dirname4(fileURLToPath(import.meta.url)), "godot", "operations", "godot_operations.gd");
|
|
15917
|
+
}
|
|
15918
|
+
function installAddons(projectPath, from = shippedAddonsDirectory()) {
|
|
15919
|
+
const installed = [];
|
|
15920
|
+
for (const name of ADDONS) {
|
|
15921
|
+
const source = join8(from, name);
|
|
15922
|
+
if (!existsSync7(join8(source, name === "gdharness_runtime" ? "runtime_autoload.gd" : "plugin.cfg"))) {
|
|
15923
|
+
throw new Error(`The package holds no ${name} addon at ${source}.`);
|
|
15924
|
+
}
|
|
15925
|
+
const target = join8(projectPath, "addons", name);
|
|
15926
|
+
const replaced = existsSync7(target);
|
|
15927
|
+
rmSync4(target, { recursive: true, force: true });
|
|
15928
|
+
mkdirSync4(dirname4(target), { recursive: true });
|
|
15929
|
+
cpSync(source, target, { recursive: true });
|
|
15930
|
+
writeFileSync5(join8(target, VERSION_MARKER), `${SERVER_VERSION}
|
|
15931
|
+
`);
|
|
15932
|
+
installed.push({ name, path: target, replaced });
|
|
15933
|
+
}
|
|
15934
|
+
writeFileSync5(join8(projectPath, "addons", ADDONS[0], DIGEST_MARKER), `${editorAddonDigest(from)}
|
|
15935
|
+
`);
|
|
15936
|
+
return installed;
|
|
15625
15937
|
}
|
|
15626
15938
|
function installedAddonVersion(projectPath) {
|
|
15627
|
-
const marker =
|
|
15628
|
-
if (!
|
|
15939
|
+
const marker = join8(projectPath, "addons", ADDONS[0], VERSION_MARKER);
|
|
15940
|
+
if (!existsSync7(marker)) {
|
|
15629
15941
|
return null;
|
|
15630
15942
|
}
|
|
15631
|
-
const said =
|
|
15943
|
+
const said = readFileSync7(marker, "utf8").trim();
|
|
15632
15944
|
return said === "" ? null : said;
|
|
15633
15945
|
}
|
|
15634
|
-
|
|
15946
|
+
function removeAddons(projectPath) {
|
|
15947
|
+
const removed = [];
|
|
15948
|
+
for (const name of ADDONS) {
|
|
15949
|
+
const target = join8(projectPath, "addons", name);
|
|
15950
|
+
if (existsSync7(target)) {
|
|
15951
|
+
rmSync4(target, { recursive: true, force: true });
|
|
15952
|
+
removed.push(target);
|
|
15953
|
+
}
|
|
15954
|
+
}
|
|
15955
|
+
const addons = join8(projectPath, "addons");
|
|
15956
|
+
if (removed.length > 0 && existsSync7(addons) && readdirSync3(addons).length === 0) {
|
|
15957
|
+
rmdirSync(addons);
|
|
15958
|
+
removed.push(addons);
|
|
15959
|
+
}
|
|
15960
|
+
return removed;
|
|
15961
|
+
}
|
|
15962
|
+
async function disablePlugins(engine, projectPath, names) {
|
|
15963
|
+
const outcomes = [];
|
|
15964
|
+
for (const name of names) {
|
|
15965
|
+
outcomes.push(await runOperation(engine, "disable_plugin", { pluginName: name }, projectPath));
|
|
15966
|
+
}
|
|
15967
|
+
return outcomes;
|
|
15968
|
+
}
|
|
15969
|
+
async function enablePlugins(engine, projectPath, names) {
|
|
15970
|
+
const outcomes = [];
|
|
15971
|
+
for (const name of names) {
|
|
15972
|
+
outcomes.push(await runOperation(engine, "enable_plugin", { pluginName: name }, projectPath));
|
|
15973
|
+
}
|
|
15974
|
+
return outcomes;
|
|
15975
|
+
}
|
|
15976
|
+
function autoloadIsOurs(named) {
|
|
15977
|
+
return named === null || named === `res://${RUNTIME_AUTOLOAD.path}`;
|
|
15978
|
+
}
|
|
15979
|
+
async function setRuntime(engine, projectPath, on) {
|
|
15980
|
+
return on ? await runOperation(engine, "add_autoload", { name: RUNTIME_AUTOLOAD.name, path: `res://${RUNTIME_AUTOLOAD.path}`, enabled: true }, projectPath) : await runOperation(engine, "remove_autoload", { name: RUNTIME_AUTOLOAD.name }, projectPath);
|
|
15981
|
+
}
|
|
15982
|
+
function loaderAmong(projectPath, named) {
|
|
15983
|
+
let byName = null;
|
|
15984
|
+
for (const [entry, path] of named) {
|
|
15985
|
+
if (entry === RUNTIME_AUTOLOAD.name || path === RUNTIME_AUTOLOAD.path) {
|
|
15986
|
+
continue;
|
|
15987
|
+
}
|
|
15988
|
+
let source = null;
|
|
15989
|
+
try {
|
|
15990
|
+
source = readFileSync7(join8(projectPath, path), "utf8");
|
|
15991
|
+
} catch {}
|
|
15992
|
+
if (source?.includes(RUNTIME_AUTOLOAD.path)) {
|
|
15993
|
+
return { name: entry, path, exists: true };
|
|
15994
|
+
}
|
|
15995
|
+
byName ??= path.toLowerCase().includes("gdharness") ? { name: entry, path, exists: source !== null } : null;
|
|
15996
|
+
}
|
|
15997
|
+
return byName;
|
|
15998
|
+
}
|
|
15999
|
+
function autoloadPaths(settings) {
|
|
16000
|
+
const named = new Map;
|
|
16001
|
+
for (const [name, value] of Object.entries(settings["autoload"] ?? {})) {
|
|
16002
|
+
const said = typeof value === "string" ? value.replace(/^\*/, "") : "";
|
|
16003
|
+
if (said.startsWith("res://")) {
|
|
16004
|
+
named.set(name, said.slice("res://".length));
|
|
16005
|
+
}
|
|
16006
|
+
}
|
|
16007
|
+
return named;
|
|
16008
|
+
}
|
|
16009
|
+
function trackedByGit(projectPath, wanted) {
|
|
16010
|
+
if (wanted.length === 0) {
|
|
16011
|
+
return new Set;
|
|
16012
|
+
}
|
|
16013
|
+
const asked = spawnSync2("git", ["ls-files", "-z", "--", ...wanted], {
|
|
16014
|
+
cwd: projectPath,
|
|
16015
|
+
encoding: "utf8"
|
|
16016
|
+
});
|
|
16017
|
+
if (asked.status !== 0) {
|
|
16018
|
+
return null;
|
|
16019
|
+
}
|
|
16020
|
+
return new Set(asked.stdout.split("\x00").filter(Boolean));
|
|
16021
|
+
}
|
|
16022
|
+
function enabledPlugins(settings) {
|
|
16023
|
+
const raw = settings["editor_plugins"]?.["enabled"];
|
|
16024
|
+
if (typeof raw !== "string") {
|
|
16025
|
+
return [];
|
|
16026
|
+
}
|
|
16027
|
+
return [...raw.matchAll(/res:\/\/addons\/([^/"]+)\/plugin\.cfg/g)].map((match) => match[1] ?? "").filter(Boolean);
|
|
16028
|
+
}
|
|
16029
|
+
function inspectProject(projectPath) {
|
|
16030
|
+
const problems = [];
|
|
16031
|
+
const addons = ADDONS.map((name) => {
|
|
16032
|
+
const directory = join8(projectPath, "addons", name);
|
|
16033
|
+
const marker = join8(directory, VERSION_MARKER);
|
|
16034
|
+
const installed = existsSync7(directory);
|
|
16035
|
+
const version = existsSync7(marker) ? readFileSync7(marker, "utf8").trim() : null;
|
|
16036
|
+
const current = version === SERVER_VERSION;
|
|
16037
|
+
if (!installed) {
|
|
16038
|
+
problems.push(`addons/${name} is not installed; run gdharness setup`);
|
|
16039
|
+
} else if (!current) {
|
|
16040
|
+
problems.push(`addons/${name} is ${version ?? "of an unknown version"}, this gdharness is ${SERVER_VERSION}; run gdharness setup`);
|
|
16041
|
+
}
|
|
16042
|
+
return { name, installed, version, current };
|
|
16043
|
+
});
|
|
16044
|
+
const settings = parseProjectGodot(readFileSync7(join8(projectPath, "project.godot"), "utf8"));
|
|
16045
|
+
const pluginsEnabled = enabledPlugins(settings);
|
|
16046
|
+
for (const name of EDITOR_PLUGINS) {
|
|
16047
|
+
if (!pluginsEnabled.includes(name)) {
|
|
16048
|
+
problems.push(`the ${name} plugin is not enabled in project.godot; run gdharness setup`);
|
|
16049
|
+
}
|
|
16050
|
+
}
|
|
16051
|
+
const autoload = settings["autoload"];
|
|
16052
|
+
const registered = autoload === undefined ? undefined : readString(autoload, RUNTIME_AUTOLOAD.name);
|
|
16053
|
+
const runtimeAutoloadPath = typeof registered === "string" ? registered.replace(/^\*/, "") : null;
|
|
16054
|
+
const named = autoloadPaths(settings);
|
|
16055
|
+
const runtimeLoaderAutoload = loaderAmong(projectPath, named);
|
|
16056
|
+
const runtimeAutoload = runtimeAutoloadPath !== null || runtimeLoaderAutoload !== null;
|
|
16057
|
+
const tracked = trackedByGit(projectPath, [...named.values()]);
|
|
16058
|
+
const addonMissing = (path) => addons.some((addon) => !addon.installed && path.startsWith(`addons/${addon.name}/`));
|
|
16059
|
+
for (const [name, path] of named) {
|
|
16060
|
+
if (!existsSync7(join8(projectPath, path))) {
|
|
16061
|
+
if (!addonMissing(path)) {
|
|
16062
|
+
problems.push(`the ${name} autoload names res://${path}, which is not in this project, so it boots with a missing script; restore that file, or point the autoload at one that is here`);
|
|
16063
|
+
}
|
|
16064
|
+
continue;
|
|
16065
|
+
}
|
|
16066
|
+
if (tracked !== null && !tracked.has(path)) {
|
|
16067
|
+
problems.push(`the ${name} autoload names res://${path}, which git does not carry, so a clone boots with a missing script; commit that file, or point the autoload at one the project tracks`);
|
|
16068
|
+
}
|
|
16069
|
+
}
|
|
16070
|
+
const cached = cachedClasses(projectPath);
|
|
16071
|
+
const staleClasses = cached === null ? [] : staleAgainst(cached, projectPath);
|
|
16072
|
+
if (cached === null) {
|
|
16073
|
+
problems.push("no .godot/global_script_class_cache.cfg: the engine knows no class_name; run project_import refresh_classes or open the editor");
|
|
16074
|
+
} else if (staleClasses.length > 0) {
|
|
16075
|
+
problems.push(`the class cache does not list ${staleClasses.join(", ")}; run project_import refresh_classes or gdharness classes`);
|
|
16076
|
+
}
|
|
16077
|
+
return {
|
|
16078
|
+
projectPath,
|
|
16079
|
+
addons,
|
|
16080
|
+
pluginsEnabled,
|
|
16081
|
+
runtimeAutoload,
|
|
16082
|
+
runtimeAutoloadPath,
|
|
16083
|
+
runtimeLoaderAutoload,
|
|
16084
|
+
staleClasses,
|
|
16085
|
+
classCacheExists: cached !== null,
|
|
16086
|
+
problems
|
|
16087
|
+
};
|
|
16088
|
+
}
|
|
16089
|
+
var ADDONS, EDITOR_PLUGINS, RUNTIME_AUTOLOAD, SCRIPT_RUNS_SETTING = "gdharness/runtime/serve_script_runs", BIND_ADDRESS_SETTING = "gdharness/runtime/bind_address", PORT_SETTING = "gdharness/runtime/port", VERSION_MARKER = ".gdharness-version", DIGEST_MARKER = ".gdharness-digest", shippedDigest;
|
|
15635
16090
|
var init_setup = __esm(() => {
|
|
15636
16091
|
init_class_cache();
|
|
15637
16092
|
init_headless();
|
|
15638
16093
|
init_resources();
|
|
15639
16094
|
init_server_version();
|
|
15640
|
-
|
|
15641
|
-
|
|
15642
|
-
|
|
16095
|
+
ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
|
|
16096
|
+
EDITOR_PLUGINS = ["gdharness_editor", "auto_reload"];
|
|
16097
|
+
RUNTIME_AUTOLOAD = {
|
|
15643
16098
|
name: "GdharnessRuntime",
|
|
15644
16099
|
path: "addons/gdharness_runtime/runtime_autoload.gd"
|
|
15645
16100
|
};
|
|
@@ -16089,6 +16544,10 @@ var init_tool_definitions = __esm(() => {
|
|
|
16089
16544
|
type: "integer",
|
|
16090
16545
|
minimum: 1,
|
|
16091
16546
|
description: "How long the run may take before it is killed. Default 600000. A run killed at the limit answers timedOut and silentForMs, how long it had printed nothing; it is called hung only when that was half the limit, or thirty seconds of a longer one, and otherwise it was still running and wants a longer timeoutMs or a narrower path."
|
|
16547
|
+
},
|
|
16548
|
+
printed: {
|
|
16549
|
+
type: "string",
|
|
16550
|
+
description: "Also answer with every line the run printed that contains this, under printed, whether it passed or not: a suite that prints a marker beside the numbers it measured carries them back without failing an assertion to do it. Matched case aside, in the text and the backtrace, at most 200 lines, the newest kept; printedOmitted counts the rest."
|
|
16092
16551
|
}
|
|
16093
16552
|
},
|
|
16094
16553
|
requires: ["projectPath"]
|
|
@@ -16738,14 +17197,19 @@ var init_tool_definitions = __esm(() => {
|
|
|
16738
17197
|
},
|
|
16739
17198
|
{
|
|
16740
17199
|
name: "runtime_input",
|
|
16741
|
-
description: "Input to the running game: a whole click on a Control or a 3D node named by path, an item chosen out of a menu, typing into whatever has the focus, or a raw action, key, mouse button or mouse motion. All of it works headless, where the window is 64 by 64 and the GUI only takes what is inside it.",
|
|
17200
|
+
description: "Input to the running game: a whole click on a Control or a 3D node named by path or on a control named by the words on it, an item chosen out of a menu, typing into whatever has the focus, or a raw action, key, mouse button or mouse motion. All of it works headless, where the window is 64 by 64 and the GUI only takes what is inside it.",
|
|
16742
17201
|
parameters: {
|
|
16743
17202
|
projectPath: RUNNING_PROJECT_PATH,
|
|
16744
17203
|
pid: RUNNING_PID,
|
|
16745
17204
|
nodePath: {
|
|
16746
17205
|
type: "string",
|
|
16747
17206
|
ops: ["click", "choose"],
|
|
16748
|
-
description: "click: the Control to click, at its centre, or the 3D node to click, where it is drawn. choose: the PopupMenu, or the OptionButton or MenuButton in front of one."
|
|
17207
|
+
description: "click: the Control to click, at its centre, or the 3D node to click, where it is drawn; with says, where to look for the control instead, default the whole tree. choose: the PopupMenu, or the OptionButton or MenuButton in front of one."
|
|
17208
|
+
},
|
|
17209
|
+
says: {
|
|
17210
|
+
type: "string",
|
|
17211
|
+
ops: ["click"],
|
|
17212
|
+
description: "click: the words on the control to click, matched the way runtime_inspect find matches says (a plain word is contained in the text, a pattern with * or ? is a glob over it), among the controls on screen, and pressed in the same frame it was found in, so a panel rebuilding between a find and a click cannot free it first. Words on a label inside a button press the button. Several matches are refused with each one listed, and index picks one."
|
|
16749
17213
|
},
|
|
16750
17214
|
action: {
|
|
16751
17215
|
type: "string",
|
|
@@ -16782,8 +17246,8 @@ var init_tool_definitions = __esm(() => {
|
|
|
16782
17246
|
},
|
|
16783
17247
|
index: {
|
|
16784
17248
|
type: "number",
|
|
16785
|
-
ops: ["choose"],
|
|
16786
|
-
description: "choose: the item to take, by where it is in the list, when text will not do."
|
|
17249
|
+
ops: ["click", "choose"],
|
|
17250
|
+
description: "click: which of several controls saying the words, counting from 0 in the order a refusal lists them. choose: the item to take, by where it is in the list, when text will not do."
|
|
16787
17251
|
},
|
|
16788
17252
|
shift: { type: "boolean", ops: ["key"], description: "key: hold Shift with it. Default false." },
|
|
16789
17253
|
ctrl: { type: "boolean", ops: ["key"], description: "key: hold Ctrl with it. Default false." },
|
|
@@ -16822,8 +17286,8 @@ var init_tool_definitions = __esm(() => {
|
|
|
16822
17286
|
requires: [],
|
|
16823
17287
|
operations: {
|
|
16824
17288
|
click: {
|
|
16825
|
-
summary: "press and release on a Control, a frame apart, and answer with what was under the pointer and what became of the control: in_tree, removed or freed. A control out of sight inside a ScrollContainer is scrolled to first, and scrolled_into_view says whether the view moved. A 3D node is clicked where it is drawn, and landed then says the interface did not swallow the press",
|
|
16826
|
-
requires: [
|
|
17289
|
+
summary: "press and release on a Control, a frame apart, and answer with what was under the pointer and what became of the control: in_tree, removed or freed. The control is named by nodePath, or by says with the words on it, found under nodePath when both are given, and the answer then says under found which match it was. A control out of sight inside a ScrollContainer is scrolled to first, and scrolled_into_view says whether the view moved. A 3D node is clicked where it is drawn, and landed then says the interface did not swallow the press",
|
|
17290
|
+
requires: []
|
|
16827
17291
|
},
|
|
16828
17292
|
choose: {
|
|
16829
17293
|
summary: "take an item out of a menu, by what it says or by where it is in the list. A menu's items are drawn rather than built, so there is nothing to click: the item takes the focus and Enter presses it, which is the engine's own path and needs no window. Answers with what was chosen and what the button in front of it shows now",
|
|
@@ -31703,10 +32167,10 @@ var init_stdio2 = __esm(() => {
|
|
|
31703
32167
|
});
|
|
31704
32168
|
|
|
31705
32169
|
// src/runtime-client.ts
|
|
31706
|
-
import { existsSync as
|
|
32170
|
+
import { existsSync as existsSync9, readdirSync as readdirSync5, readFileSync as readFileSync9, statSync as statSync4, unlinkSync } from "node:fs";
|
|
31707
32171
|
import { createConnection } from "node:net";
|
|
31708
|
-
import { tmpdir as
|
|
31709
|
-
import { dirname as
|
|
32172
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
32173
|
+
import { dirname as dirname7, join as join11, resolve as resolve3 } from "node:path";
|
|
31710
32174
|
import { setTimeout as delay } from "node:timers/promises";
|
|
31711
32175
|
function runtimeDirectory(variables = process.env) {
|
|
31712
32176
|
const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
|
|
@@ -31714,20 +32178,20 @@ function runtimeDirectory(variables = process.env) {
|
|
|
31714
32178
|
return explicit;
|
|
31715
32179
|
}
|
|
31716
32180
|
const perUser = envValue("XDG_RUNTIME_DIR", variables);
|
|
31717
|
-
return
|
|
32181
|
+
return join11(perUser ?? tmpdir3(), "gdharness");
|
|
31718
32182
|
}
|
|
31719
32183
|
function runtimeDirectories(variables = process.env) {
|
|
31720
32184
|
const candidates = [runtimeDirectory(variables)];
|
|
31721
32185
|
const fallbacks = process.platform === "win32" ? [envValue("SystemRoot", variables) ?? envValue("windir", variables) ?? "C:\\Windows"] : ["/tmp", "/var/tmp"];
|
|
31722
32186
|
for (const base of fallbacks) {
|
|
31723
|
-
candidates.push(
|
|
32187
|
+
candidates.push(join11(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
|
|
31724
32188
|
}
|
|
31725
32189
|
return [...new Set(candidates.map((path) => resolve3(path)))];
|
|
31726
32190
|
}
|
|
31727
32191
|
function errorReportOf(pid, directories = runtimeDirectories()) {
|
|
31728
32192
|
for (const directory of directories) {
|
|
31729
|
-
const path =
|
|
31730
|
-
if (
|
|
32193
|
+
const path = join11(directory, `runtime-${pid}.log`);
|
|
32194
|
+
if (existsSync9(path)) {
|
|
31731
32195
|
return path;
|
|
31732
32196
|
}
|
|
31733
32197
|
}
|
|
@@ -31744,7 +32208,7 @@ function processAlive(pid) {
|
|
|
31744
32208
|
function parseAnnouncement(file, pid) {
|
|
31745
32209
|
let fields;
|
|
31746
32210
|
try {
|
|
31747
|
-
fields = asParams(JSON.parse(
|
|
32211
|
+
fields = asParams(JSON.parse(readFileSync9(file, "utf8")));
|
|
31748
32212
|
} catch {
|
|
31749
32213
|
return { kind: "rubbish" };
|
|
31750
32214
|
}
|
|
@@ -31761,7 +32225,7 @@ function parseAnnouncement(file, pid) {
|
|
|
31761
32225
|
};
|
|
31762
32226
|
let announcedAt;
|
|
31763
32227
|
try {
|
|
31764
|
-
announcedAt =
|
|
32228
|
+
announcedAt = statSync4(file).mtimeMs;
|
|
31765
32229
|
} catch {}
|
|
31766
32230
|
const when = announcedAt === undefined ? {} : { announcedAt };
|
|
31767
32231
|
if (protocol !== RUNTIME_PROTOCOL) {
|
|
@@ -31802,7 +32266,7 @@ function discoverRuntimes(directories = runtimeDirectories()) {
|
|
|
31802
32266
|
function wentAway(file, pid) {
|
|
31803
32267
|
let project = "";
|
|
31804
32268
|
try {
|
|
31805
|
-
const fields = asParams(JSON.parse(
|
|
32269
|
+
const fields = asParams(JSON.parse(readFileSync9(file, "utf8")));
|
|
31806
32270
|
project = readString(readParams(fields, "project") ?? {}, "path") ?? "";
|
|
31807
32271
|
} catch {
|
|
31808
32272
|
return;
|
|
@@ -31839,29 +32303,29 @@ function strangersAmong(candidates, now, startTimes = startTimesOf) {
|
|
|
31839
32303
|
return new Set(candidates.filter((one) => verdicts.get(one.file)?.stranger === true).map((one) => one.file));
|
|
31840
32304
|
}
|
|
31841
32305
|
function announcedIn(directory) {
|
|
31842
|
-
if (!
|
|
32306
|
+
if (!existsSync9(directory)) {
|
|
31843
32307
|
return [];
|
|
31844
32308
|
}
|
|
31845
32309
|
const found = [];
|
|
31846
32310
|
const candidates = [];
|
|
31847
32311
|
const entries = [];
|
|
31848
|
-
for (const entry of
|
|
32312
|
+
for (const entry of readdirSync5(directory)) {
|
|
31849
32313
|
const report = ERROR_REPORT_PATTERN.exec(entry);
|
|
31850
32314
|
if (report) {
|
|
31851
|
-
sweepErrorReport(
|
|
32315
|
+
sweepErrorReport(join11(directory, entry), Number.parseInt(report[1] ?? "", 10));
|
|
31852
32316
|
continue;
|
|
31853
32317
|
}
|
|
31854
32318
|
const match = ANNOUNCEMENT_PATTERN.exec(entry);
|
|
31855
32319
|
if (!match) {
|
|
31856
32320
|
continue;
|
|
31857
32321
|
}
|
|
31858
|
-
const file =
|
|
32322
|
+
const file = join11(directory, entry);
|
|
31859
32323
|
const pid = Number.parseInt(match[1] ?? "", 10);
|
|
31860
32324
|
const alive = processAlive(pid);
|
|
31861
32325
|
entries.push({ file, pid, alive });
|
|
31862
32326
|
if (alive) {
|
|
31863
32327
|
try {
|
|
31864
|
-
candidates.push({ file, pid, writtenAt:
|
|
32328
|
+
candidates.push({ file, pid, writtenAt: statSync4(file).mtimeMs });
|
|
31865
32329
|
} catch {}
|
|
31866
32330
|
}
|
|
31867
32331
|
}
|
|
@@ -31883,16 +32347,16 @@ function announcedIn(directory) {
|
|
|
31883
32347
|
found.push(announced);
|
|
31884
32348
|
}
|
|
31885
32349
|
const held = new Set(entries.map((one) => one.file));
|
|
31886
|
-
const here =
|
|
32350
|
+
const here = join11(directory, ".");
|
|
31887
32351
|
for (const file of verdicts.keys()) {
|
|
31888
|
-
if (
|
|
32352
|
+
if (dirname7(file) === here && !held.has(file)) {
|
|
31889
32353
|
verdicts.delete(file);
|
|
31890
32354
|
}
|
|
31891
32355
|
}
|
|
31892
32356
|
return found;
|
|
31893
32357
|
}
|
|
31894
32358
|
async function announcementEnded(pid, directories = runtimeDirectories()) {
|
|
31895
|
-
const files = directories.map((directory) =>
|
|
32359
|
+
const files = directories.map((directory) => join11(directory, `runtime-${pid}.json`)).filter((file) => existsSync9(file));
|
|
31896
32360
|
const first = files[0];
|
|
31897
32361
|
if (first === undefined) {
|
|
31898
32362
|
return;
|
|
@@ -31918,7 +32382,7 @@ function sweepErrorReport(path, pid, now = Date.now()) {
|
|
|
31918
32382
|
return;
|
|
31919
32383
|
}
|
|
31920
32384
|
try {
|
|
31921
|
-
if (now -
|
|
32385
|
+
if (now - statSync4(path).mtimeMs >= ERROR_REPORT_KEEP_MS) {
|
|
31922
32386
|
unlinkSync(path);
|
|
31923
32387
|
}
|
|
31924
32388
|
} catch {}
|
|
@@ -32191,14 +32655,14 @@ var init_runtime_client = __esm(() => {
|
|
|
32191
32655
|
});
|
|
32192
32656
|
|
|
32193
32657
|
// src/boot-note.ts
|
|
32194
|
-
import { mkdirSync as
|
|
32195
|
-
import { dirname as
|
|
32658
|
+
import { mkdirSync as mkdirSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync7 } from "node:fs";
|
|
32659
|
+
import { dirname as dirname8, join as join12 } from "node:path";
|
|
32196
32660
|
function bootNotePath(projectPath) {
|
|
32197
|
-
return
|
|
32661
|
+
return join12(projectPath, ".godot", "gdharness-boot.json");
|
|
32198
32662
|
}
|
|
32199
32663
|
function readBootNote(projectPath) {
|
|
32200
32664
|
try {
|
|
32201
|
-
const noted = readNumber(asParams(JSON.parse(
|
|
32665
|
+
const noted = readNumber(asParams(JSON.parse(readFileSync10(bootNotePath(projectPath), "utf8"))), "announcedAfterMs");
|
|
32202
32666
|
return noted !== undefined && Number.isFinite(noted) && noted > 0 ? noted : null;
|
|
32203
32667
|
} catch {
|
|
32204
32668
|
return null;
|
|
@@ -32207,8 +32671,8 @@ function readBootNote(projectPath) {
|
|
|
32207
32671
|
function writeBootNote(projectPath, announcedAfterMs) {
|
|
32208
32672
|
try {
|
|
32209
32673
|
const path = bootNotePath(projectPath);
|
|
32210
|
-
|
|
32211
|
-
|
|
32674
|
+
mkdirSync6(dirname8(path), { recursive: true });
|
|
32675
|
+
writeFileSync7(path, JSON.stringify({ announcedAfterMs: Math.round(announcedAfterMs), at: new Date().toISOString() }, null, 2), "utf8");
|
|
32212
32676
|
} catch {}
|
|
32213
32677
|
}
|
|
32214
32678
|
function halfAsLongAgain(lastBootMs) {
|
|
@@ -32226,10 +32690,10 @@ var init_boot_note = __esm(() => {
|
|
|
32226
32690
|
});
|
|
32227
32691
|
|
|
32228
32692
|
// src/breakpoint-note.ts
|
|
32229
|
-
import { mkdirSync as
|
|
32230
|
-
import { dirname as
|
|
32693
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync11, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "node:fs";
|
|
32694
|
+
import { dirname as dirname9, isAbsolute as isAbsolute2, join as join13, relative as relative2 } from "node:path";
|
|
32231
32695
|
function breakpointNotePath(projectPath) {
|
|
32232
|
-
return
|
|
32696
|
+
return join13(projectPath, ".godot", "gdharness-breakpoints.json");
|
|
32233
32697
|
}
|
|
32234
32698
|
function writeBreakpointNote(projectPath, held) {
|
|
32235
32699
|
const path = breakpointNotePath(projectPath);
|
|
@@ -32237,7 +32701,7 @@ function writeBreakpointNote(projectPath, held) {
|
|
|
32237
32701
|
const inside = held.map((one) => ({ scriptPath: realPathOr(one.scriptPath), lines: one.lines })).filter((one) => one.lines.length > 0 && withinProject(root, one.scriptPath));
|
|
32238
32702
|
try {
|
|
32239
32703
|
if (inside.length === 0) {
|
|
32240
|
-
|
|
32704
|
+
rmSync6(path, { force: true });
|
|
32241
32705
|
return;
|
|
32242
32706
|
}
|
|
32243
32707
|
const note = {
|
|
@@ -32246,8 +32710,8 @@ function writeBreakpointNote(projectPath, held) {
|
|
|
32246
32710
|
lines: [...one.lines]
|
|
32247
32711
|
}))
|
|
32248
32712
|
};
|
|
32249
|
-
|
|
32250
|
-
|
|
32713
|
+
mkdirSync7(dirname9(path), { recursive: true });
|
|
32714
|
+
writeFileSync8(path, `${JSON.stringify(note, null, 2)}
|
|
32251
32715
|
`, "utf8");
|
|
32252
32716
|
} catch (error) {
|
|
32253
32717
|
console.error(`[SERVER] Could not record the breakpoints at ${path}: ${errorMessage(error)}`);
|
|
@@ -32255,7 +32719,7 @@ function writeBreakpointNote(projectPath, held) {
|
|
|
32255
32719
|
}
|
|
32256
32720
|
function readBreakpointNote(projectPath) {
|
|
32257
32721
|
try {
|
|
32258
|
-
const parsed = JSON.parse(
|
|
32722
|
+
const parsed = JSON.parse(readFileSync11(breakpointNotePath(projectPath), "utf8"));
|
|
32259
32723
|
if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.breakpoints)) {
|
|
32260
32724
|
return [];
|
|
32261
32725
|
}
|
|
@@ -32270,7 +32734,7 @@ function readBreakpointNote(projectPath) {
|
|
|
32270
32734
|
}
|
|
32271
32735
|
const lines = Array.isArray(listed) ? listed.filter((line) => Number.isInteger(line) && line > 0) : [];
|
|
32272
32736
|
if (lines.length > 0) {
|
|
32273
|
-
held.push({ scriptPath: realPathOr(
|
|
32737
|
+
held.push({ scriptPath: realPathOr(join13(projectPath, script)), lines });
|
|
32274
32738
|
}
|
|
32275
32739
|
}
|
|
32276
32740
|
return held;
|
|
@@ -32287,75 +32751,15 @@ var init_breakpoint_note = __esm(() => {
|
|
|
32287
32751
|
init_paths();
|
|
32288
32752
|
});
|
|
32289
32753
|
|
|
32290
|
-
// src/bridge-announce.ts
|
|
32291
|
-
import { mkdirSync as mkdirSync9, readFileSync as readFileSync14, rmSync as rmSync8, writeFileSync as writeFileSync11 } from "node:fs";
|
|
32292
|
-
import { dirname as dirname11, join as join16 } from "node:path";
|
|
32293
|
-
function announcementPath2(projectPath) {
|
|
32294
|
-
return join16(projectPath, ".godot", "gdharness-bridge.json");
|
|
32295
|
-
}
|
|
32296
|
-
function readAnnouncement2(path) {
|
|
32297
|
-
try {
|
|
32298
|
-
const parsed = JSON.parse(readFileSync14(path, "utf8"));
|
|
32299
|
-
if (typeof parsed !== "object" || parsed === null) {
|
|
32300
|
-
return null;
|
|
32301
|
-
}
|
|
32302
|
-
const said = parsed;
|
|
32303
|
-
if (typeof said.port !== "number" || typeof said.pid !== "number") {
|
|
32304
|
-
return null;
|
|
32305
|
-
}
|
|
32306
|
-
return said;
|
|
32307
|
-
} catch {
|
|
32308
|
-
return null;
|
|
32309
|
-
}
|
|
32310
|
-
}
|
|
32311
|
-
function announcementIsOurs(path) {
|
|
32312
|
-
return readAnnouncement2(path)?.pid === process.pid;
|
|
32313
|
-
}
|
|
32314
|
-
function announceBridge(projectPath, bridge) {
|
|
32315
|
-
const path = announcementPath2(projectPath);
|
|
32316
|
-
try {
|
|
32317
|
-
mkdirSync9(dirname11(path), { recursive: true });
|
|
32318
|
-
const announcement = {
|
|
32319
|
-
protocol: BRIDGE_ANNOUNCE_PROTOCOL,
|
|
32320
|
-
host: bridge.host,
|
|
32321
|
-
port: bridge.port,
|
|
32322
|
-
pid: process.pid,
|
|
32323
|
-
version: bridge.version,
|
|
32324
|
-
startedAt: new Date().toISOString()
|
|
32325
|
-
};
|
|
32326
|
-
writeFileSync11(path, `${JSON.stringify(announcement, null, 2)}
|
|
32327
|
-
`, "utf8");
|
|
32328
|
-
return path;
|
|
32329
|
-
} catch (error) {
|
|
32330
|
-
console.error(`[SERVER] Could not announce the editor bridge at ${path}: ${errorMessage(error)}`);
|
|
32331
|
-
return null;
|
|
32332
|
-
}
|
|
32333
|
-
}
|
|
32334
|
-
function withdrawBridge(path) {
|
|
32335
|
-
if (path === null || !announcementIsOurs(path)) {
|
|
32336
|
-
return;
|
|
32337
|
-
}
|
|
32338
|
-
try {
|
|
32339
|
-
rmSync8(path, { force: true });
|
|
32340
|
-
} catch (error) {
|
|
32341
|
-
console.error(`[SERVER] Could not withdraw the editor bridge announcement: ${errorMessage(error)}`);
|
|
32342
|
-
}
|
|
32343
|
-
}
|
|
32344
|
-
var BRIDGE_ANNOUNCE_PROTOCOL = 1;
|
|
32345
|
-
var init_bridge_announce = __esm(() => {
|
|
32346
|
-
init_errors();
|
|
32347
|
-
init_process_children();
|
|
32348
|
-
});
|
|
32349
|
-
|
|
32350
32754
|
// src/class-note.ts
|
|
32351
|
-
import { mkdirSync as
|
|
32352
|
-
import { dirname as
|
|
32755
|
+
import { mkdirSync as mkdirSync8, readFileSync as readFileSync12, writeFileSync as writeFileSync9 } from "node:fs";
|
|
32756
|
+
import { dirname as dirname10, join as join14 } from "node:path";
|
|
32353
32757
|
function classNotePath(projectPath) {
|
|
32354
|
-
return
|
|
32758
|
+
return join14(projectPath, ".godot", "gdharness-classes.json");
|
|
32355
32759
|
}
|
|
32356
32760
|
function readClassNote(projectPath) {
|
|
32357
32761
|
try {
|
|
32358
|
-
const fields = asParams(JSON.parse(
|
|
32762
|
+
const fields = asParams(JSON.parse(readFileSync12(classNotePath(projectPath), "utf8")));
|
|
32359
32763
|
const rebuiltWith = readArray(fields, "rebuiltWith");
|
|
32360
32764
|
if (rebuiltWith === undefined || !rebuiltWith.every((name) => typeof name === "string")) {
|
|
32361
32765
|
return null;
|
|
@@ -32372,24 +32776,24 @@ function readClassNote(projectPath) {
|
|
|
32372
32776
|
function writeClassNote(projectPath, note) {
|
|
32373
32777
|
try {
|
|
32374
32778
|
const path = classNotePath(projectPath);
|
|
32375
|
-
|
|
32376
|
-
|
|
32779
|
+
mkdirSync8(dirname10(path), { recursive: true });
|
|
32780
|
+
writeFileSync9(path, `${JSON.stringify(note, null, 2)}
|
|
32377
32781
|
`, "utf8");
|
|
32378
32782
|
} catch {}
|
|
32379
32783
|
}
|
|
32380
32784
|
var init_class_note = () => {};
|
|
32381
32785
|
|
|
32382
32786
|
// src/config-pin.ts
|
|
32383
|
-
import { existsSync as
|
|
32384
|
-
import { join as
|
|
32787
|
+
import { existsSync as existsSync10, readFileSync as readFileSync13 } from "node:fs";
|
|
32788
|
+
import { join as join15 } from "node:path";
|
|
32385
32789
|
function versionsNamedInConfig(projectPath) {
|
|
32386
|
-
const config =
|
|
32387
|
-
if (!
|
|
32790
|
+
const config = join15(projectPath, ".mcp.json");
|
|
32791
|
+
if (!existsSync10(config)) {
|
|
32388
32792
|
return [];
|
|
32389
32793
|
}
|
|
32390
32794
|
let text;
|
|
32391
32795
|
try {
|
|
32392
|
-
text =
|
|
32796
|
+
text = readFileSync13(config, "utf8");
|
|
32393
32797
|
JSON.parse(text);
|
|
32394
32798
|
} catch {
|
|
32395
32799
|
return [];
|
|
@@ -33146,25 +33550,25 @@ var init_desktop = () => {};
|
|
|
33146
33550
|
// src/editor-log.ts
|
|
33147
33551
|
import {
|
|
33148
33552
|
closeSync,
|
|
33149
|
-
existsSync as
|
|
33150
|
-
mkdirSync as
|
|
33553
|
+
existsSync as existsSync11,
|
|
33554
|
+
mkdirSync as mkdirSync9,
|
|
33151
33555
|
openSync,
|
|
33152
|
-
readFileSync as
|
|
33556
|
+
readFileSync as readFileSync14,
|
|
33153
33557
|
readSync,
|
|
33154
|
-
rmSync as
|
|
33155
|
-
statSync as
|
|
33156
|
-
writeFileSync as
|
|
33558
|
+
rmSync as rmSync7,
|
|
33559
|
+
statSync as statSync5,
|
|
33560
|
+
writeFileSync as writeFileSync10
|
|
33157
33561
|
} from "node:fs";
|
|
33158
|
-
import { dirname as
|
|
33562
|
+
import { dirname as dirname11, join as join16 } from "node:path";
|
|
33159
33563
|
function editorLogPath(projectPath) {
|
|
33160
|
-
return
|
|
33564
|
+
return join16(projectPath, ".godot", "gdharness-editor.log");
|
|
33161
33565
|
}
|
|
33162
33566
|
function editorLogNotePath(projectPath) {
|
|
33163
|
-
return
|
|
33567
|
+
return join16(projectPath, ".godot", "gdharness-editor.json");
|
|
33164
33568
|
}
|
|
33165
33569
|
function readEditorLogNote(projectPath) {
|
|
33166
33570
|
try {
|
|
33167
|
-
const fields = asParams(JSON.parse(
|
|
33571
|
+
const fields = asParams(JSON.parse(readFileSync14(editorLogNotePath(projectPath), "utf8")));
|
|
33168
33572
|
const pid = readNumber(fields, "pid");
|
|
33169
33573
|
const startedAt = readString(fields, "startedAt");
|
|
33170
33574
|
return pid !== undefined && Number.isInteger(pid) && pid > 0 && startedAt !== undefined ? { pid, startedAt } : null;
|
|
@@ -33175,16 +33579,16 @@ function readEditorLogNote(projectPath) {
|
|
|
33175
33579
|
function clearEditorLog(projectPath) {
|
|
33176
33580
|
try {
|
|
33177
33581
|
const path = editorLogPath(projectPath);
|
|
33178
|
-
|
|
33179
|
-
|
|
33180
|
-
|
|
33582
|
+
mkdirSync9(dirname11(path), { recursive: true });
|
|
33583
|
+
rmSync7(path, { force: true });
|
|
33584
|
+
rmSync7(editorLogNotePath(projectPath), { force: true });
|
|
33181
33585
|
} catch {}
|
|
33182
33586
|
}
|
|
33183
33587
|
function writeEditorLogNote(projectPath, pid) {
|
|
33184
33588
|
try {
|
|
33185
33589
|
const path = editorLogNotePath(projectPath);
|
|
33186
|
-
|
|
33187
|
-
|
|
33590
|
+
mkdirSync9(dirname11(path), { recursive: true });
|
|
33591
|
+
writeFileSync10(path, JSON.stringify({ pid, startedAt: new Date().toISOString() }, null, 2), "utf8");
|
|
33188
33592
|
} catch {}
|
|
33189
33593
|
}
|
|
33190
33594
|
function theConsoleWasNotCaptured(why, projectPath) {
|
|
@@ -33205,7 +33609,7 @@ function editorConsole(projectPath, editorPid, openedByAServer) {
|
|
|
33205
33609
|
return { kind: "older server", notedPid: noted?.pid ?? null };
|
|
33206
33610
|
}
|
|
33207
33611
|
const path = editorLogPath(projectPath);
|
|
33208
|
-
if (!
|
|
33612
|
+
if (!existsSync11(path)) {
|
|
33209
33613
|
return { kind: "not written" };
|
|
33210
33614
|
}
|
|
33211
33615
|
const log = new GameLog;
|
|
@@ -33215,9 +33619,9 @@ function editorConsole(projectPath, editorPid, openedByAServer) {
|
|
|
33215
33619
|
return { log, path, ...of === undefined ? {} : { readBytes: text.length, of } };
|
|
33216
33620
|
}
|
|
33217
33621
|
function theFirstOf(path) {
|
|
33218
|
-
const whole =
|
|
33622
|
+
const whole = statSync5(path).size;
|
|
33219
33623
|
if (whole <= MOST_CONSOLE_BYTES) {
|
|
33220
|
-
return { text:
|
|
33624
|
+
return { text: readFileSync14(path, "utf8") };
|
|
33221
33625
|
}
|
|
33222
33626
|
const handle = openSync(path, "r");
|
|
33223
33627
|
try {
|
|
@@ -33259,7 +33663,7 @@ function shapeOf(text) {
|
|
|
33259
33663
|
}
|
|
33260
33664
|
return { shape, values };
|
|
33261
33665
|
}
|
|
33262
|
-
function bursts(entries, before) {
|
|
33666
|
+
function bursts(entries, before, keep) {
|
|
33263
33667
|
const shapes = new Map;
|
|
33264
33668
|
const shaped = entries.map((entry) => shapeOf(entry.text));
|
|
33265
33669
|
for (const [at, entry] of entries.entries()) {
|
|
@@ -33287,6 +33691,9 @@ function bursts(entries, before) {
|
|
|
33287
33691
|
if (entry === undefined) {
|
|
33288
33692
|
continue;
|
|
33289
33693
|
}
|
|
33694
|
+
if (keep !== undefined && !group.indices.some((at) => entries[at] !== undefined && keep(entries[at]))) {
|
|
33695
|
+
continue;
|
|
33696
|
+
}
|
|
33290
33697
|
repeated.push({
|
|
33291
33698
|
shape: key.slice(key.indexOf("\x00") + 1),
|
|
33292
33699
|
count: group.indices.length,
|
|
@@ -36756,208 +37163,6 @@ var init_godot_bridge = __esm(() => {
|
|
|
36756
37163
|
};
|
|
36757
37164
|
});
|
|
36758
37165
|
|
|
36759
|
-
// src/godot-path.ts
|
|
36760
|
-
import { execFile as execFile5 } from "node:child_process";
|
|
36761
|
-
import { existsSync as existsSync13 } from "node:fs";
|
|
36762
|
-
import { normalize as normalize2 } from "node:path";
|
|
36763
|
-
import { promisify as promisify5 } from "node:util";
|
|
36764
|
-
|
|
36765
|
-
class GodotLocator2 {
|
|
36766
|
-
answered = new Map;
|
|
36767
|
-
found = null;
|
|
36768
|
-
async find() {
|
|
36769
|
-
if (this.found !== null && await this.whyNot(this.found) === null) {
|
|
36770
|
-
return { ok: true, path: this.found };
|
|
36771
|
-
}
|
|
36772
|
-
this.found = null;
|
|
36773
|
-
const named = envValue("GODOT_PATH");
|
|
36774
|
-
if (named !== undefined) {
|
|
36775
|
-
const path = normalize2(named);
|
|
36776
|
-
const reason = await this.whyNot(path);
|
|
36777
|
-
if (reason === null) {
|
|
36778
|
-
this.found = path;
|
|
36779
|
-
return { ok: true, path };
|
|
36780
|
-
}
|
|
36781
|
-
return { ok: false, named: path, reason };
|
|
36782
|
-
}
|
|
36783
|
-
for (const candidate of godotCandidates(process.platform, resolveHomeDirectory())) {
|
|
36784
|
-
const path = normalize2(candidate);
|
|
36785
|
-
if (await this.whyNot(path) === null) {
|
|
36786
|
-
this.found = path;
|
|
36787
|
-
return { ok: true, path };
|
|
36788
|
-
}
|
|
36789
|
-
}
|
|
36790
|
-
return { ok: false, named: null };
|
|
36791
|
-
}
|
|
36792
|
-
static refusal(located) {
|
|
36793
|
-
if (located.named === null) {
|
|
36794
|
-
return {
|
|
36795
|
-
message: "No Godot executable found.",
|
|
36796
|
-
advice: [
|
|
36797
|
-
"Set GODOT_PATH to the Godot 4 executable",
|
|
36798
|
-
"Or install Godot where it is usually found, such as /usr/bin/godot or C:\\Program Files\\Godot"
|
|
36799
|
-
]
|
|
36800
|
-
};
|
|
36801
|
-
}
|
|
36802
|
-
return {
|
|
36803
|
-
message: `GODOT_PATH is set to ${located.named}, which ${located.reason ?? "does not answer"}.`,
|
|
36804
|
-
advice: [
|
|
36805
|
-
"Point GODOT_PATH at a Godot 4 executable that answers --version",
|
|
36806
|
-
"Or unset it, and the usual install locations are searched instead"
|
|
36807
|
-
]
|
|
36808
|
-
};
|
|
36809
|
-
}
|
|
36810
|
-
async whyNot(path) {
|
|
36811
|
-
const known = this.answered.get(path);
|
|
36812
|
-
if (known !== undefined) {
|
|
36813
|
-
return known;
|
|
36814
|
-
}
|
|
36815
|
-
let reason;
|
|
36816
|
-
if (path !== "godot" && !existsSync13(path)) {
|
|
36817
|
-
reason = "does not exist";
|
|
36818
|
-
} else {
|
|
36819
|
-
try {
|
|
36820
|
-
await run5(path, ["--version"]);
|
|
36821
|
-
reason = null;
|
|
36822
|
-
} catch (error) {
|
|
36823
|
-
reason = `does not answer --version: ${errorMessage(error)}`;
|
|
36824
|
-
}
|
|
36825
|
-
}
|
|
36826
|
-
this.answered.set(path, reason);
|
|
36827
|
-
return reason;
|
|
36828
|
-
}
|
|
36829
|
-
}
|
|
36830
|
-
var run5;
|
|
36831
|
-
var init_godot_path = __esm(() => {
|
|
36832
|
-
init_detection();
|
|
36833
|
-
init_errors();
|
|
36834
|
-
run5 = promisify5(execFile5);
|
|
36835
|
-
});
|
|
36836
|
-
|
|
36837
|
-
// src/issues.ts
|
|
36838
|
-
import { createHash as createHash4 } from "node:crypto";
|
|
36839
|
-
import process7 from "node:process";
|
|
36840
|
-
function feedbackNotice() {
|
|
36841
|
-
return JSON.stringify({
|
|
36842
|
-
gdharness_feedback: {
|
|
36843
|
-
what_to_do: "If this session has run into something gdharness cannot do, or a tool that should " + "exist and does not, tell the user what is missing and offer to open an enhancement " + "issue for them. Only if they say yes, and only for a real gap: a call you got wrong " + "is not one. If you cannot open an issue yourself, give them the link and what to " + "write in it.",
|
|
36844
|
-
enhancement_url: ENHANCEMENT_URL2
|
|
36845
|
-
}
|
|
36846
|
-
}, null, 2);
|
|
36847
|
-
}
|
|
36848
|
-
function runtime2() {
|
|
36849
|
-
const versions = process7.versions;
|
|
36850
|
-
const bun = versions["bun"];
|
|
36851
|
-
return bun === undefined ? `node ${process7.versions.node}` : `bun ${bun}`;
|
|
36852
|
-
}
|
|
36853
|
-
function defectSignature2(where, message) {
|
|
36854
|
-
const normalised = `${where}
|
|
36855
|
-
${message}`.toLowerCase().replaceAll("\\", "/").replaceAll(/(?:[a-z]:)?(?:\/+[\w.-]+){2,}/gu, "<path>").replaceAll(/\d+/gu, "0");
|
|
36856
|
-
return createHash4("sha256").update(normalised).digest("hex").slice(0, 8);
|
|
36857
|
-
}
|
|
36858
|
-
function facts2(where, message, godotVersion) {
|
|
36859
|
-
return [
|
|
36860
|
-
["Where", where],
|
|
36861
|
-
["Error", message],
|
|
36862
|
-
["Version", `gdharness ${SERVER_VERSION}`],
|
|
36863
|
-
["Runtime", `${runtime2()}, ${process7.platform} ${process7.arch}`],
|
|
36864
|
-
["Godot", godotVersion ?? "not known at the point this failed"],
|
|
36865
|
-
["Signature", defectSignature2(where, message)]
|
|
36866
|
-
];
|
|
36867
|
-
}
|
|
36868
|
-
function filledTemplate2(where, message, godotVersion) {
|
|
36869
|
-
return [
|
|
36870
|
-
`**gdharness version**: ${SERVER_VERSION}`,
|
|
36871
|
-
`**Godot version**: ${godotVersion ?? ""}`,
|
|
36872
|
-
`**Bun version**: ${runtime2()}`,
|
|
36873
|
-
`**OS**: ${process7.platform} ${process7.arch}`,
|
|
36874
|
-
"**MCP client**:",
|
|
36875
|
-
"",
|
|
36876
|
-
"## What you did",
|
|
36877
|
-
"",
|
|
36878
|
-
where,
|
|
36879
|
-
"",
|
|
36880
|
-
"## What you expected",
|
|
36881
|
-
"",
|
|
36882
|
-
"The call to answer, or to refuse with a reason.",
|
|
36883
|
-
"",
|
|
36884
|
-
"## What happened",
|
|
36885
|
-
"",
|
|
36886
|
-
"gdharness failed in a way it does not model.",
|
|
36887
|
-
"",
|
|
36888
|
-
"```text",
|
|
36889
|
-
message,
|
|
36890
|
-
"```",
|
|
36891
|
-
"",
|
|
36892
|
-
`Signature: \`${defectSignature2(where, message)}\``,
|
|
36893
|
-
"",
|
|
36894
|
-
"If the real cause turned out to be the project, the environment or the call, say so here:",
|
|
36895
|
-
"reaching you as a defect rather than as a refusal naming what would have worked is then the",
|
|
36896
|
-
"thing to fix.",
|
|
36897
|
-
"",
|
|
36898
|
-
"## Anything that makes it reproducible",
|
|
36899
|
-
""
|
|
36900
|
-
].join(`
|
|
36901
|
-
`);
|
|
36902
|
-
}
|
|
36903
|
-
|
|
36904
|
-
class DefectsSeen {
|
|
36905
|
-
counts = new Map;
|
|
36906
|
-
before(where, message) {
|
|
36907
|
-
const signature = defectSignature2(where, message);
|
|
36908
|
-
const seen = this.counts.get(signature) ?? 0;
|
|
36909
|
-
this.counts.set(signature, seen + 1);
|
|
36910
|
-
return seen;
|
|
36911
|
-
}
|
|
36912
|
-
}
|
|
36913
|
-
function defectReport2(where, error, context = {}) {
|
|
36914
|
-
const { godotVersion, seenBefore = 0 } = context;
|
|
36915
|
-
const message = errorMessage(error);
|
|
36916
|
-
const title = `Defect ${defectSignature2(where, message)}: ${where}`;
|
|
36917
|
-
const url = `${NEW_ISSUE2}?template=bug_report.md&title=${encodeURIComponent(title)}` + `&body=${encodeURIComponent(filledTemplate2(where, message, godotVersion))}`;
|
|
36918
|
-
const width = Math.max(...facts2(where, message, godotVersion).map(([label]) => (label ?? "").length));
|
|
36919
|
-
return [
|
|
36920
|
-
"gdharness failed in a way it does not model. Whatever went wrong underneath, it reached you",
|
|
36921
|
-
"as a crash rather than as a refusal naming what would have worked, and that much is a defect",
|
|
36922
|
-
"in gdharness however the rest of it turns out.",
|
|
36923
|
-
"",
|
|
36924
|
-
...seenBefore === 0 ? [
|
|
36925
|
-
"Send the call once more before anything else. Some of what reaches this line is a file",
|
|
36926
|
-
"another process had open, or a connection that dropped, and those are gone by the next",
|
|
36927
|
-
"attempt. If the same Signature comes back, it is not one of those, and repeating it",
|
|
36928
|
-
"further only spends turns."
|
|
36929
|
-
] : [
|
|
36930
|
-
`You have had this exact failure ${seenBefore === 1 ? "once" : `${seenBefore} times`} in this session already, so it is a`,
|
|
36931
|
-
"state rather than a passing one, and sending the call again will not change it."
|
|
36932
|
-
],
|
|
36933
|
-
"",
|
|
36934
|
-
...facts2(where, message, godotVersion).map(([label, value]) => ` ${(label ?? "").padEnd(width)} ${value ?? ""}`),
|
|
36935
|
-
"",
|
|
36936
|
-
"Do not open an issue on your own initiative. Tell the person whose machine this is what",
|
|
36937
|
-
"broke, and ask whether you may report it to gdharness on their behalf. What the report",
|
|
36938
|
-
"carries is the lines above and nothing else, and whether that goes into a public tracker",
|
|
36939
|
-
"is theirs to decide.",
|
|
36940
|
-
"",
|
|
36941
|
-
"If they say yes, this link carries the report already filled in:",
|
|
36942
|
-
url,
|
|
36943
|
-
"",
|
|
36944
|
-
"If they say yes and you have no way to open an issue yourself, give them that link and",
|
|
36945
|
-
"those lines, and stay with them while they file it.",
|
|
36946
|
-
"",
|
|
36947
|
-
"Worth reporting either way. If you can see that what actually went wrong was the project,",
|
|
36948
|
-
"the environment or the call, then this message is the defect: a failure gdharness knows",
|
|
36949
|
-
"about is meant to arrive as a refusal naming what would have worked, not as this. Say that",
|
|
36950
|
-
"in the report and it is the more useful of the two."
|
|
36951
|
-
].join(`
|
|
36952
|
-
`);
|
|
36953
|
-
}
|
|
36954
|
-
var NEW_ISSUE2 = "https://github.com/Aureliolo/gdharness/issues/new", ENHANCEMENT_URL2;
|
|
36955
|
-
var init_issues = __esm(() => {
|
|
36956
|
-
init_errors();
|
|
36957
|
-
init_server_version();
|
|
36958
|
-
ENHANCEMENT_URL2 = `${NEW_ISSUE2}?template=feature_request.md`;
|
|
36959
|
-
});
|
|
36960
|
-
|
|
36961
37166
|
// src/junit.ts
|
|
36962
37167
|
function decoded(text) {
|
|
36963
37168
|
return text.replace(/&(#x[0-9a-fA-F]+|#[0-9]+|[a-zA-Z]+);/g, (whole, entity) => {
|
|
@@ -37149,7 +37354,7 @@ function scriptErrorsPrinted(printed) {
|
|
|
37149
37354
|
for (const line of printed.slice(start + 1)) {
|
|
37150
37355
|
const at = /^\s*at (res:\/\/.+):(\d+)\s*$/.exec(line);
|
|
37151
37356
|
if (at !== null && message !== null) {
|
|
37152
|
-
found.push({ path: at[1] ?? "", line: Number(at[2]), message });
|
|
37357
|
+
found.push({ path: at[1] ?? "", line: Number(at[2]) === 0 ? null : Number(at[2]), message });
|
|
37153
37358
|
message = null;
|
|
37154
37359
|
} else if (at === null && message === null && /^ {2}\S/.test(line)) {
|
|
37155
37360
|
message = line.trim();
|
|
@@ -37157,7 +37362,7 @@ function scriptErrorsPrinted(printed) {
|
|
|
37157
37362
|
break;
|
|
37158
37363
|
}
|
|
37159
37364
|
}
|
|
37160
|
-
return found.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : a.line - b.line);
|
|
37365
|
+
return found.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : (a.line ?? 0) - (b.line ?? 0));
|
|
37161
37366
|
}
|
|
37162
37367
|
function unspelled(text) {
|
|
37163
37368
|
return text.replace(/<(?:LF|CR|TAB|BS|FF|VT|BEL|ESC|DEL|0x[0-9A-F]{2})>/g, (spelling) => spelling.startsWith("<0x") ? String.fromCharCode(Number.parseInt(spelling.slice(3, 5), 16)) : SPELLED[spelling] ?? spelling);
|
|
@@ -37299,8 +37504,8 @@ var init_junit = __esm(() => {
|
|
|
37299
37504
|
import { realpathSync as realpathSync2 } from "node:fs";
|
|
37300
37505
|
import { readFile, realpath } from "node:fs/promises";
|
|
37301
37506
|
import { createConnection as createConnection3 } from "node:net";
|
|
37302
|
-
import { dirname as
|
|
37303
|
-
import { fileURLToPath as
|
|
37507
|
+
import { dirname as dirname12, resolve as resolve4 } from "node:path";
|
|
37508
|
+
import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
|
|
37304
37509
|
function diagnosticsKey(uri) {
|
|
37305
37510
|
let decoded;
|
|
37306
37511
|
try {
|
|
@@ -37312,7 +37517,7 @@ function diagnosticsKey(uri) {
|
|
|
37312
37517
|
return decoded;
|
|
37313
37518
|
}
|
|
37314
37519
|
try {
|
|
37315
|
-
return realpathSync2.native(
|
|
37520
|
+
return realpathSync2.native(fileURLToPath3(decoded)).toLowerCase();
|
|
37316
37521
|
} catch {
|
|
37317
37522
|
return decoded.toLowerCase();
|
|
37318
37523
|
}
|
|
@@ -37427,7 +37632,7 @@ class GodotLSPClient {
|
|
|
37427
37632
|
if (this.initialized) {
|
|
37428
37633
|
return;
|
|
37429
37634
|
}
|
|
37430
|
-
const rootPath = this.rootPath ??
|
|
37635
|
+
const rootPath = this.rootPath ?? dirname12(resolve4(filePath));
|
|
37431
37636
|
await this.initialize(rootPath);
|
|
37432
37637
|
}
|
|
37433
37638
|
async sendRequest(method, params) {
|
|
@@ -37834,7 +38039,7 @@ var init_lsp_client = __esm(() => {
|
|
|
37834
38039
|
|
|
37835
38040
|
// src/outside.ts
|
|
37836
38041
|
import { spawn } from "node:child_process";
|
|
37837
|
-
import { fileURLToPath as
|
|
38042
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
37838
38043
|
function changesFrom(env, base) {
|
|
37839
38044
|
const changes = {};
|
|
37840
38045
|
if (env === undefined) {
|
|
@@ -37897,12 +38102,12 @@ async function launchOutsideTheTree(spec, runtime = process.execPath, keeper = K
|
|
|
37897
38102
|
}
|
|
37898
38103
|
var KEEPER_SCRIPT;
|
|
37899
38104
|
var init_outside = __esm(() => {
|
|
37900
|
-
KEEPER_SCRIPT =
|
|
38105
|
+
KEEPER_SCRIPT = fileURLToPath4(new URL("./keeper.js", import.meta.url));
|
|
37901
38106
|
});
|
|
37902
38107
|
|
|
37903
38108
|
// src/process-time.ts
|
|
37904
|
-
import { execFile as
|
|
37905
|
-
import { promisify as
|
|
38109
|
+
import { execFile as execFile4 } from "node:child_process";
|
|
38110
|
+
import { promisify as promisify4 } from "node:util";
|
|
37906
38111
|
async function cpuSecondsOf(pid) {
|
|
37907
38112
|
try {
|
|
37908
38113
|
return process.platform === "win32" ? await askWindows2(pid) : await askPosix2(pid);
|
|
@@ -37911,12 +38116,12 @@ async function cpuSecondsOf(pid) {
|
|
|
37911
38116
|
}
|
|
37912
38117
|
}
|
|
37913
38118
|
async function askWindows2(pid) {
|
|
37914
|
-
const { stdout } = await
|
|
38119
|
+
const { stdout } = await run4("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", `(Get-Process -Id ${pid}).CPU`], { timeout: ASK_TIMEOUT_MS2, windowsHide: true });
|
|
37915
38120
|
const seconds = Number(stdout.trim().replace(",", "."));
|
|
37916
38121
|
return Number.isFinite(seconds) ? seconds : undefined;
|
|
37917
38122
|
}
|
|
37918
38123
|
async function askPosix2(pid) {
|
|
37919
|
-
const { stdout } = await
|
|
38124
|
+
const { stdout } = await run4("ps", ["-o", "time=", "-p", String(pid)], { timeout: ASK_TIMEOUT_MS2 });
|
|
37920
38125
|
return secondsFromClock(stdout.trim());
|
|
37921
38126
|
}
|
|
37922
38127
|
function secondsFromClock(printed) {
|
|
@@ -37934,19 +38139,19 @@ function secondsFromClock(printed) {
|
|
|
37934
38139
|
const extra = days === null ? 0 : Number(days) * 86400;
|
|
37935
38140
|
return Number.isFinite(extra) ? seconds + extra : undefined;
|
|
37936
38141
|
}
|
|
37937
|
-
var
|
|
38142
|
+
var run4, ASK_TIMEOUT_MS2 = 6000;
|
|
37938
38143
|
var init_process_time = __esm(() => {
|
|
37939
|
-
|
|
38144
|
+
run4 = promisify4(execFile4);
|
|
37940
38145
|
});
|
|
37941
38146
|
|
|
37942
38147
|
// src/project-scan.ts
|
|
37943
|
-
import { existsSync as
|
|
37944
|
-
import { join as
|
|
38148
|
+
import { existsSync as existsSync12, readdirSync as readdirSync6, readFileSync as readFileSync15 } from "node:fs";
|
|
38149
|
+
import { join as join17 } from "node:path";
|
|
37945
38150
|
function skipped(name) {
|
|
37946
38151
|
return name.startsWith(".") || SKIPPED.has(name);
|
|
37947
38152
|
}
|
|
37948
38153
|
function steppedOver(directory) {
|
|
37949
|
-
return
|
|
38154
|
+
return existsSync12(join17(directory, ".gdignore"));
|
|
37950
38155
|
}
|
|
37951
38156
|
function projectStructure(projectPath) {
|
|
37952
38157
|
const structure = { scenes: 0, scripts: 0, assets: 0, other: 0 };
|
|
@@ -37954,12 +38159,12 @@ function projectStructure(projectPath) {
|
|
|
37954
38159
|
if (steppedOver(directory)) {
|
|
37955
38160
|
return;
|
|
37956
38161
|
}
|
|
37957
|
-
for (const entry of
|
|
38162
|
+
for (const entry of readdirSync6(directory, { withFileTypes: true })) {
|
|
37958
38163
|
if (skipped(entry.name)) {
|
|
37959
38164
|
continue;
|
|
37960
38165
|
}
|
|
37961
38166
|
if (entry.isDirectory()) {
|
|
37962
|
-
visit(
|
|
38167
|
+
visit(join17(directory, entry.name));
|
|
37963
38168
|
} else if (entry.isFile()) {
|
|
37964
38169
|
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
37965
38170
|
if (extension === "tscn") {
|
|
@@ -37983,20 +38188,20 @@ function scriptsWithoutUid(projectPath) {
|
|
|
37983
38188
|
if (steppedOver(directory)) {
|
|
37984
38189
|
return;
|
|
37985
38190
|
}
|
|
37986
|
-
for (const entry of
|
|
38191
|
+
for (const entry of readdirSync6(directory, { withFileTypes: true })) {
|
|
37987
38192
|
if (skipped(entry.name)) {
|
|
37988
38193
|
continue;
|
|
37989
38194
|
}
|
|
37990
38195
|
const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
37991
38196
|
if (entry.isDirectory()) {
|
|
37992
|
-
visit(
|
|
38197
|
+
visit(join17(directory, entry.name), spelled);
|
|
37993
38198
|
continue;
|
|
37994
38199
|
}
|
|
37995
38200
|
if (!entry.isFile()) {
|
|
37996
38201
|
continue;
|
|
37997
38202
|
}
|
|
37998
38203
|
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
37999
|
-
if (CARRIES_A_UID.has(extension) && !
|
|
38204
|
+
if (CARRIES_A_UID.has(extension) && !existsSync12(join17(directory, `${entry.name}.uid`))) {
|
|
38000
38205
|
found.push(spelled);
|
|
38001
38206
|
}
|
|
38002
38207
|
}
|
|
@@ -38024,14 +38229,14 @@ function searchProject(projectPath, options) {
|
|
|
38024
38229
|
if (steppedOver(directory)) {
|
|
38025
38230
|
return;
|
|
38026
38231
|
}
|
|
38027
|
-
for (const entry of
|
|
38232
|
+
for (const entry of readdirSync6(directory, { withFileTypes: true })) {
|
|
38028
38233
|
if (full()) {
|
|
38029
38234
|
return;
|
|
38030
38235
|
}
|
|
38031
38236
|
if (skipped(entry.name)) {
|
|
38032
38237
|
continue;
|
|
38033
38238
|
}
|
|
38034
|
-
const entryPath =
|
|
38239
|
+
const entryPath = join17(directory, entry.name);
|
|
38035
38240
|
if (entry.isDirectory()) {
|
|
38036
38241
|
visit(entryPath);
|
|
38037
38242
|
continue;
|
|
@@ -38042,7 +38247,7 @@ function searchProject(projectPath, options) {
|
|
|
38042
38247
|
}
|
|
38043
38248
|
result.summary.files_searched += 1;
|
|
38044
38249
|
const matches = [];
|
|
38045
|
-
for (const [index, line] of
|
|
38250
|
+
for (const [index, line] of readFileSync15(entryPath, "utf8").split(`
|
|
38046
38251
|
`).entries()) {
|
|
38047
38252
|
if (full()) {
|
|
38048
38253
|
break;
|
|
@@ -38071,37 +38276,37 @@ var init_project_scan = __esm(() => {
|
|
|
38071
38276
|
});
|
|
38072
38277
|
|
|
38073
38278
|
// src/reimport.ts
|
|
38074
|
-
import { createHash as
|
|
38075
|
-
import { existsSync as
|
|
38076
|
-
import { basename as basename3, join as
|
|
38279
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
38280
|
+
import { existsSync as existsSync13, readFileSync as readFileSync16, rmSync as rmSync8, statSync as statSync6, utimesSync, writeFileSync as writeFileSync11 } from "node:fs";
|
|
38281
|
+
import { basename as basename3, join as join18 } from "node:path";
|
|
38077
38282
|
function forceNextImport(projectPath, resourcePath) {
|
|
38078
38283
|
const relative = resourcePath.replace(/^res:\/\//, "");
|
|
38079
|
-
const sidecar =
|
|
38080
|
-
if (!
|
|
38284
|
+
const sidecar = join18(projectPath, `${relative}.import`);
|
|
38285
|
+
if (!existsSync13(sidecar)) {
|
|
38081
38286
|
return;
|
|
38082
38287
|
}
|
|
38083
|
-
const { atime, mtimeMs } =
|
|
38084
|
-
|
|
38288
|
+
const { atime, mtimeMs } = statSync6(sidecar);
|
|
38289
|
+
writeFileSync11(sidecar, readFileSync16(sidecar, "utf8").replace(/^valid=false\r?\n/m, ""));
|
|
38085
38290
|
utimesSync(sidecar, atime, new Date(mtimeMs + 2000));
|
|
38086
|
-
|
|
38291
|
+
rmSync8(importRecord(projectPath, resourcePath), { force: true });
|
|
38087
38292
|
}
|
|
38088
38293
|
function importRecord(projectPath, resourcePath) {
|
|
38089
|
-
const digest =
|
|
38090
|
-
return
|
|
38294
|
+
const digest = createHash3("md5").update(resourcePath).digest("hex");
|
|
38295
|
+
return join18(projectPath, ".godot", "imported", `${basename3(resourcePath)}-${digest}.md5`);
|
|
38091
38296
|
}
|
|
38092
38297
|
var init_reimport = () => {};
|
|
38093
38298
|
|
|
38094
38299
|
// src/restart-note.ts
|
|
38095
|
-
import { mkdirSync as
|
|
38096
|
-
import { dirname as
|
|
38300
|
+
import { mkdirSync as mkdirSync10, readFileSync as readFileSync17, rmSync as rmSync9, writeFileSync as writeFileSync12 } from "node:fs";
|
|
38301
|
+
import { dirname as dirname13, join as join19 } from "node:path";
|
|
38097
38302
|
function restartNotePath(projectPath) {
|
|
38098
|
-
return
|
|
38303
|
+
return join19(projectPath, ".godot", "gdharness-restart.json");
|
|
38099
38304
|
}
|
|
38100
38305
|
function noteRestartBegun(note) {
|
|
38101
38306
|
const path = restartNotePath(note.projectPath);
|
|
38102
38307
|
try {
|
|
38103
|
-
|
|
38104
|
-
|
|
38308
|
+
mkdirSync10(dirname13(path), { recursive: true });
|
|
38309
|
+
writeFileSync12(path, `${JSON.stringify(note, null, 2)}
|
|
38105
38310
|
`, "utf8");
|
|
38106
38311
|
} catch (error) {
|
|
38107
38312
|
console.error(`[SERVER] Could not record the restart at ${path}: ${errorMessage(error)}`);
|
|
@@ -38109,7 +38314,7 @@ function noteRestartBegun(note) {
|
|
|
38109
38314
|
}
|
|
38110
38315
|
function restartOwed(projectPath) {
|
|
38111
38316
|
try {
|
|
38112
|
-
const parsed = JSON.parse(
|
|
38317
|
+
const parsed = JSON.parse(readFileSync17(restartNotePath(projectPath), "utf8"));
|
|
38113
38318
|
if (typeof parsed !== "object" || parsed === null) {
|
|
38114
38319
|
return null;
|
|
38115
38320
|
}
|
|
@@ -38124,7 +38329,7 @@ function restartOwed(projectPath) {
|
|
|
38124
38329
|
}
|
|
38125
38330
|
function restartSettled(projectPath) {
|
|
38126
38331
|
try {
|
|
38127
|
-
|
|
38332
|
+
rmSync9(restartNotePath(projectPath), { force: true });
|
|
38128
38333
|
} catch (error) {
|
|
38129
38334
|
console.error(`[SERVER] Could not clear the restart note: ${errorMessage(error)}`);
|
|
38130
38335
|
}
|
|
@@ -38135,29 +38340,29 @@ var init_restart_note = __esm(() => {
|
|
|
38135
38340
|
|
|
38136
38341
|
// src/run-record.ts
|
|
38137
38342
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
38138
|
-
import { createHash as
|
|
38343
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
38139
38344
|
import {
|
|
38140
|
-
mkdirSync as
|
|
38345
|
+
mkdirSync as mkdirSync11,
|
|
38141
38346
|
openSync as openSync2,
|
|
38142
|
-
readdirSync as
|
|
38143
|
-
readFileSync as
|
|
38347
|
+
readdirSync as readdirSync7,
|
|
38348
|
+
readFileSync as readFileSync18,
|
|
38144
38349
|
realpathSync as realpathSync3,
|
|
38145
|
-
rmSync as
|
|
38146
|
-
statSync as
|
|
38147
|
-
writeFileSync as
|
|
38350
|
+
rmSync as rmSync10,
|
|
38351
|
+
statSync as statSync7,
|
|
38352
|
+
writeFileSync as writeFileSync13
|
|
38148
38353
|
} from "node:fs";
|
|
38149
|
-
import { basename as basename4, dirname as
|
|
38354
|
+
import { basename as basename4, dirname as dirname14, join as join20, resolve as resolve5 } from "node:path";
|
|
38150
38355
|
function runsDirectory() {
|
|
38151
|
-
return
|
|
38356
|
+
return join20(runtimeDirectory(), "runs");
|
|
38152
38357
|
}
|
|
38153
38358
|
function realAsFarAsItExists(path) {
|
|
38154
38359
|
const missing = [];
|
|
38155
38360
|
let at = path;
|
|
38156
38361
|
for (;; ) {
|
|
38157
38362
|
try {
|
|
38158
|
-
return
|
|
38363
|
+
return join20(realpathSync3.native(at), ...missing);
|
|
38159
38364
|
} catch {
|
|
38160
|
-
const parent =
|
|
38365
|
+
const parent = dirname14(at);
|
|
38161
38366
|
if (parent === at) {
|
|
38162
38367
|
return path;
|
|
38163
38368
|
}
|
|
@@ -38169,26 +38374,26 @@ function realAsFarAsItExists(path) {
|
|
|
38169
38374
|
function projectKey(projectPath) {
|
|
38170
38375
|
const real = realAsFarAsItExists(resolve5(projectPath)).replace(/[\\/]+$/, "");
|
|
38171
38376
|
const folded = process.platform === "win32" || process.platform === "darwin" ? real.toLowerCase() : real;
|
|
38172
|
-
return
|
|
38377
|
+
return createHash4("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
|
|
38173
38378
|
}
|
|
38174
38379
|
function runRecordPath(projectPath, directory = runtimeDirectory()) {
|
|
38175
|
-
return
|
|
38380
|
+
return join20(directory, "runs", `note-${projectKey(projectPath)}.json`);
|
|
38176
38381
|
}
|
|
38177
38382
|
function openTranscript(startedAt) {
|
|
38178
|
-
|
|
38179
|
-
const path =
|
|
38383
|
+
mkdirSync11(runsDirectory(), { recursive: true });
|
|
38384
|
+
const path = join20(runsDirectory(), `run-${startedAt}.log`);
|
|
38180
38385
|
return { path, fd: openSync2(path, "a") };
|
|
38181
38386
|
}
|
|
38182
38387
|
function editorNotePath(projectPath, directory = runtimeDirectory()) {
|
|
38183
|
-
return
|
|
38388
|
+
return join20(directory, "runs", `editor-${projectKey(projectPath)}.json`);
|
|
38184
38389
|
}
|
|
38185
38390
|
function writeEditorRunNote(note) {
|
|
38186
|
-
|
|
38187
|
-
|
|
38391
|
+
mkdirSync11(runsDirectory(), { recursive: true });
|
|
38392
|
+
writeFileSync13(editorNotePath(note.projectPath), JSON.stringify(note, null, 2), "utf8");
|
|
38188
38393
|
}
|
|
38189
38394
|
function readEditorRunNote(projectPath) {
|
|
38190
38395
|
for (const directory of runtimeDirectories()) {
|
|
38191
|
-
const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(
|
|
38396
|
+
const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join20(directory, "runs", "editor-run.json"));
|
|
38192
38397
|
if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
|
|
38193
38398
|
return note;
|
|
38194
38399
|
}
|
|
@@ -38198,7 +38403,7 @@ function readEditorRunNote(projectPath) {
|
|
|
38198
38403
|
function editorNoteAt(path) {
|
|
38199
38404
|
let parsed;
|
|
38200
38405
|
try {
|
|
38201
|
-
parsed = JSON.parse(
|
|
38406
|
+
parsed = JSON.parse(readFileSync18(path, "utf8"));
|
|
38202
38407
|
} catch {
|
|
38203
38408
|
return null;
|
|
38204
38409
|
}
|
|
@@ -38220,7 +38425,7 @@ function readRunRecord(projectPath) {
|
|
|
38220
38425
|
if (own !== null) {
|
|
38221
38426
|
return own;
|
|
38222
38427
|
}
|
|
38223
|
-
const shared = recordAt(
|
|
38428
|
+
const shared = recordAt(join20(directory, "runs", SHARED_NOTE));
|
|
38224
38429
|
if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
|
|
38225
38430
|
return shared;
|
|
38226
38431
|
}
|
|
@@ -38232,13 +38437,13 @@ function everyRunRecord() {
|
|
|
38232
38437
|
for (const directory of runtimeDirectories()) {
|
|
38233
38438
|
let entries;
|
|
38234
38439
|
try {
|
|
38235
|
-
entries =
|
|
38440
|
+
entries = readdirSync7(join20(directory, "runs"));
|
|
38236
38441
|
} catch {
|
|
38237
38442
|
continue;
|
|
38238
38443
|
}
|
|
38239
38444
|
for (const entry of entries) {
|
|
38240
38445
|
if (entry === SHARED_NOTE || entry.startsWith("note-") && entry.endsWith(".json")) {
|
|
38241
|
-
const record = recordAt(
|
|
38446
|
+
const record = recordAt(join20(directory, "runs", entry));
|
|
38242
38447
|
if (record !== null) {
|
|
38243
38448
|
found.push(record);
|
|
38244
38449
|
}
|
|
@@ -38250,7 +38455,7 @@ function everyRunRecord() {
|
|
|
38250
38455
|
function recordAt(path) {
|
|
38251
38456
|
let parsed;
|
|
38252
38457
|
try {
|
|
38253
|
-
parsed = JSON.parse(
|
|
38458
|
+
parsed = JSON.parse(readFileSync18(path, "utf8"));
|
|
38254
38459
|
} catch {
|
|
38255
38460
|
return null;
|
|
38256
38461
|
}
|
|
@@ -38296,7 +38501,7 @@ function runningAs(pid) {
|
|
|
38296
38501
|
const began = startedAt(pid, null);
|
|
38297
38502
|
const when = began === null ? {} : { startedAt: began };
|
|
38298
38503
|
if (process.platform === "linux") {
|
|
38299
|
-
const raw =
|
|
38504
|
+
const raw = readFileSync18(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
|
|
38300
38505
|
return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
|
|
38301
38506
|
}
|
|
38302
38507
|
const args = execFileSync2("ps", ["-p", String(pid), "-o", "args="], {
|
|
@@ -38434,19 +38639,19 @@ function judgeAnnouncedGame(running, projectPath, announcedAt) {
|
|
|
38434
38639
|
}
|
|
38435
38640
|
function clearRunRecord(projectPath) {
|
|
38436
38641
|
for (const directory of runtimeDirectories()) {
|
|
38437
|
-
const shared =
|
|
38642
|
+
const shared = join20(directory, "runs", SHARED_NOTE);
|
|
38438
38643
|
const sharedRecord = recordAt(shared);
|
|
38439
38644
|
const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
|
|
38440
38645
|
for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
|
|
38441
38646
|
try {
|
|
38442
|
-
|
|
38647
|
+
rmSync10(path, { force: true });
|
|
38443
38648
|
} catch {}
|
|
38444
38649
|
}
|
|
38445
38650
|
}
|
|
38446
38651
|
}
|
|
38447
38652
|
function ageMs(path, now) {
|
|
38448
38653
|
try {
|
|
38449
|
-
return now -
|
|
38654
|
+
return now - statSync7(path).mtimeMs;
|
|
38450
38655
|
} catch {
|
|
38451
38656
|
return 0;
|
|
38452
38657
|
}
|
|
@@ -38456,7 +38661,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
38456
38661
|
const kept = new Set(everyRunRecord().map((record) => record.transcript));
|
|
38457
38662
|
let entries;
|
|
38458
38663
|
try {
|
|
38459
|
-
entries =
|
|
38664
|
+
entries = readdirSync7(directory);
|
|
38460
38665
|
} catch {
|
|
38461
38666
|
return;
|
|
38462
38667
|
}
|
|
@@ -38464,12 +38669,12 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
38464
38669
|
if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
|
|
38465
38670
|
continue;
|
|
38466
38671
|
}
|
|
38467
|
-
const path =
|
|
38672
|
+
const path = join20(directory, entry);
|
|
38468
38673
|
if (kept.has(path) || ageMs(path, now) < keepMs) {
|
|
38469
38674
|
continue;
|
|
38470
38675
|
}
|
|
38471
38676
|
try {
|
|
38472
|
-
|
|
38677
|
+
rmSync10(path);
|
|
38473
38678
|
} catch {}
|
|
38474
38679
|
}
|
|
38475
38680
|
}
|
|
@@ -38481,29 +38686,29 @@ var init_run_record = __esm(() => {
|
|
|
38481
38686
|
});
|
|
38482
38687
|
|
|
38483
38688
|
// src/server.ts
|
|
38484
|
-
import { execFile as
|
|
38689
|
+
import { execFile as execFile5, spawn as spawn2 } from "node:child_process";
|
|
38485
38690
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
38486
38691
|
import {
|
|
38487
38692
|
appendFileSync,
|
|
38488
38693
|
closeSync as closeSync2,
|
|
38489
38694
|
constants,
|
|
38490
38695
|
copyFileSync,
|
|
38491
|
-
existsSync as
|
|
38696
|
+
existsSync as existsSync14,
|
|
38492
38697
|
fstatSync,
|
|
38493
|
-
mkdirSync as
|
|
38494
|
-
mkdtempSync as
|
|
38698
|
+
mkdirSync as mkdirSync12,
|
|
38699
|
+
mkdtempSync as mkdtempSync2,
|
|
38495
38700
|
openSync as openSync3,
|
|
38496
|
-
readdirSync as
|
|
38497
|
-
readFileSync as
|
|
38701
|
+
readdirSync as readdirSync8,
|
|
38702
|
+
readFileSync as readFileSync19,
|
|
38498
38703
|
readSync as readSync2,
|
|
38499
|
-
rmdirSync as
|
|
38500
|
-
statSync as
|
|
38704
|
+
rmdirSync as rmdirSync2,
|
|
38705
|
+
statSync as statSync8
|
|
38501
38706
|
} from "node:fs";
|
|
38502
|
-
import { tmpdir as
|
|
38503
|
-
import { basename as basename5, dirname as
|
|
38707
|
+
import { tmpdir as tmpdir4 } from "node:os";
|
|
38708
|
+
import { basename as basename5, dirname as dirname15, extname as extname2, isAbsolute as isAbsolute3, join as join21, normalize as normalize2, relative as relative3 } from "node:path";
|
|
38504
38709
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
38505
|
-
import { fileURLToPath as
|
|
38506
|
-
import { promisify as
|
|
38710
|
+
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
38711
|
+
import { promisify as promisify5 } from "node:util";
|
|
38507
38712
|
function captureDestinationRefusal(outputPath, projects) {
|
|
38508
38713
|
if (/^(res|user):\/\//.test(outputPath)) {
|
|
38509
38714
|
return `outputPath ${outputPath} is a Godot path; a capture is saved outside the project, at an absolute path on this machine.`;
|
|
@@ -38514,16 +38719,16 @@ function captureDestinationRefusal(outputPath, projects) {
|
|
|
38514
38719
|
if (extname2(outputPath).toLowerCase() !== ".png") {
|
|
38515
38720
|
return `outputPath ${outputPath} does not end in .png, which is what a capture is.`;
|
|
38516
38721
|
}
|
|
38517
|
-
const directory = realPathOr(
|
|
38518
|
-
if (!
|
|
38519
|
-
return `outputPath's directory ${
|
|
38722
|
+
const directory = realPathOr(dirname15(outputPath));
|
|
38723
|
+
if (!existsSync14(directory)) {
|
|
38724
|
+
return `outputPath's directory ${dirname15(outputPath)} does not exist; a capture creates no directories.`;
|
|
38520
38725
|
}
|
|
38521
|
-
const where =
|
|
38726
|
+
const where = join21(directory, basename5(outputPath));
|
|
38522
38727
|
const inside = projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
|
|
38523
38728
|
if (inside !== undefined) {
|
|
38524
38729
|
return `outputPath ${outputPath} is inside the project at ${inside}; a capture is saved outside it, so it never becomes part of the game.`;
|
|
38525
38730
|
}
|
|
38526
|
-
if (
|
|
38731
|
+
if (existsSync14(where)) {
|
|
38527
38732
|
return `outputPath ${outputPath} already exists; a capture is not written over a file. Name a new one.`;
|
|
38528
38733
|
}
|
|
38529
38734
|
return null;
|
|
@@ -38888,15 +39093,24 @@ function changedSettings(before, after) {
|
|
|
38888
39093
|
return [...names].filter((name) => JSON.stringify(before.get(name)) !== JSON.stringify(after.get(name))).sort();
|
|
38889
39094
|
}
|
|
38890
39095
|
function scriptErrorsNote(errors) {
|
|
38891
|
-
const
|
|
39096
|
+
const causes = new Map;
|
|
38892
39097
|
for (const error of errors) {
|
|
38893
|
-
if (!
|
|
38894
|
-
|
|
39098
|
+
if (!error.message.includes(DEPENDED_FAILED) && !causes.has(error.path)) {
|
|
39099
|
+
causes.set(error.path, error);
|
|
38895
39100
|
}
|
|
38896
39101
|
}
|
|
38897
|
-
const
|
|
38898
|
-
const
|
|
38899
|
-
|
|
39102
|
+
const dependents = [...new Set(errors.map((error) => error.path))].filter((path) => !causes.has(path));
|
|
39103
|
+
const scripts = causes.size + dependents.length;
|
|
39104
|
+
const named = [...causes.values()].map((error) => `${error.line === null ? error.path : `${error.path}:${error.line}`} ${error.message}${/[.!?]\)?$/.test(error.message) ? "" : "."}`).join(" ");
|
|
39105
|
+
const following = dependents.length === 0 ? "" : causes.size === 0 ? `${dependents.join(", ")} ${dependents.length === 1 ? "depends" : "depend"} on a script that did not compile, and gdUnit4 named none with an error of its own: script_diagnostics on one of them finds it.` : `${dependents.length === 1 ? "One more" : `${dependents.length} more`} failed only because a script ${dependents.length === 1 ? "it depends" : "they depend"} on did: ${dependents.join(", ")}.`;
|
|
39106
|
+
const more = errors.filter((error) => !error.message.includes(DEPENDED_FAILED)).length - causes.size;
|
|
39107
|
+
const rest = more > 0 ? `${more === 1 ? "One more error" : `${more} more errors`} in ${causes.size === 1 ? "that script" : "those scripts"} ${more === 1 ? "is" : "are"} under scriptErrors.` : "";
|
|
39108
|
+
return [
|
|
39109
|
+
`No tests ran: gdUnit4 could not load ${scripts === 1 ? "one script" : `${scripts} scripts`} while looking for suites, and it runs no suite at all when one fails to load.`,
|
|
39110
|
+
named,
|
|
39111
|
+
following,
|
|
39112
|
+
rest
|
|
39113
|
+
].filter((part) => part !== "").join(" ");
|
|
38900
39114
|
}
|
|
38901
39115
|
function timedOutVerdict(timeoutMs, silentForMs) {
|
|
38902
39116
|
const quiet = Math.round(silentForMs);
|
|
@@ -38953,13 +39167,13 @@ function aboveTheRunner(entry) {
|
|
|
38953
39167
|
}
|
|
38954
39168
|
function elsewhereIn(projectPath, asked) {
|
|
38955
39169
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
38956
|
-
if (wanted === "" ||
|
|
39170
|
+
if (wanted === "" || existsSync14(join21(projectPath, wanted))) {
|
|
38957
39171
|
return "";
|
|
38958
39172
|
}
|
|
38959
39173
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
38960
39174
|
let near = [];
|
|
38961
39175
|
try {
|
|
38962
|
-
near =
|
|
39176
|
+
near = readdirSync8(projectPath, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.toLowerCase() !== root).map((entry) => entry.name).filter((name) => name.toLowerCase().startsWith("test") || root.startsWith(name.toLowerCase()));
|
|
38963
39177
|
} catch {
|
|
38964
39178
|
return "";
|
|
38965
39179
|
}
|
|
@@ -38967,7 +39181,7 @@ function elsewhereIn(projectPath, asked) {
|
|
|
38967
39181
|
return near.length === 0 ? "" : ` The project does have ${there}.`;
|
|
38968
39182
|
}
|
|
38969
39183
|
function hasMainScene(projectFile) {
|
|
38970
|
-
const scene = parseProjectGodot(
|
|
39184
|
+
const scene = parseProjectGodot(readFileSync19(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
38971
39185
|
return typeof scene === "string" && scene !== "";
|
|
38972
39186
|
}
|
|
38973
39187
|
function runtimeAddonAnnouncesEditors(projectPath) {
|
|
@@ -38975,13 +39189,13 @@ function runtimeAddonAnnouncesEditors(projectPath) {
|
|
|
38975
39189
|
return false;
|
|
38976
39190
|
}
|
|
38977
39191
|
try {
|
|
38978
|
-
return
|
|
39192
|
+
return readFileSync19(join21(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
|
|
38979
39193
|
} catch {
|
|
38980
39194
|
return false;
|
|
38981
39195
|
}
|
|
38982
39196
|
}
|
|
38983
39197
|
function editorPlaysHeadless(projectFile) {
|
|
38984
|
-
const runArgs = parseProjectGodot(
|
|
39198
|
+
const runArgs = parseProjectGodot(readFileSync19(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
38985
39199
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
38986
39200
|
}
|
|
38987
39201
|
function camelCased(params) {
|
|
@@ -38999,8 +39213,8 @@ function needsItsOwnProcess(asked) {
|
|
|
38999
39213
|
|
|
39000
39214
|
class GodotServer {
|
|
39001
39215
|
mcp;
|
|
39002
|
-
locator = new
|
|
39003
|
-
operationsScript =
|
|
39216
|
+
locator = new GodotLocator;
|
|
39217
|
+
operationsScript = join21(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
39004
39218
|
godotBridge;
|
|
39005
39219
|
tools = buildToolDefinitions();
|
|
39006
39220
|
activeProcess = null;
|
|
@@ -39059,7 +39273,7 @@ class GodotServer {
|
|
|
39059
39273
|
async run() {
|
|
39060
39274
|
const located = await this.locator.find();
|
|
39061
39275
|
if (!located.ok) {
|
|
39062
|
-
console.error(`[SERVER] ${
|
|
39276
|
+
console.error(`[SERVER] ${GodotLocator.refusal(located).message} Until that is fixed every tool that runs the engine will say so.`);
|
|
39063
39277
|
} else {
|
|
39064
39278
|
console.error(`[SERVER] Using Godot at: ${located.path}`);
|
|
39065
39279
|
}
|
|
@@ -39090,7 +39304,7 @@ class GodotServer {
|
|
|
39090
39304
|
return;
|
|
39091
39305
|
}
|
|
39092
39306
|
const wanted = this.godotBridge.configuredPort;
|
|
39093
|
-
const announced =
|
|
39307
|
+
const announced = readAnnouncement(announcementPath(this.ownProject));
|
|
39094
39308
|
if (announced === null || announced.pid === process.pid || announced.port !== wanted || !alive(announced.pid)) {
|
|
39095
39309
|
return;
|
|
39096
39310
|
}
|
|
@@ -39176,7 +39390,7 @@ class GodotServer {
|
|
|
39176
39390
|
if (this.ownProject === null) {
|
|
39177
39391
|
return null;
|
|
39178
39392
|
}
|
|
39179
|
-
const announced =
|
|
39393
|
+
const announced = readAnnouncement(announcementPath(this.ownProject));
|
|
39180
39394
|
if (announced === null || announced.pid === process.pid) {
|
|
39181
39395
|
return null;
|
|
39182
39396
|
}
|
|
@@ -39299,7 +39513,7 @@ class GodotServer {
|
|
|
39299
39513
|
content: [
|
|
39300
39514
|
{
|
|
39301
39515
|
type: "text",
|
|
39302
|
-
text:
|
|
39516
|
+
text: defectReport(where, error, {
|
|
39303
39517
|
seenBefore,
|
|
39304
39518
|
godotVersion: this.lastGodotVersion ?? undefined
|
|
39305
39519
|
})
|
|
@@ -39641,11 +39855,16 @@ class GodotServer {
|
|
|
39641
39855
|
return await this.handleRuntimeCommand(op === "screenshot" ? "capture_screenshot" : "capture_viewport", args);
|
|
39642
39856
|
case "runtime_input":
|
|
39643
39857
|
if (op === "click") {
|
|
39858
|
+
if (readNonEmptyString(args, "nodePath") === undefined && readNonEmptyString(args, "says") === undefined) {
|
|
39859
|
+
return this.createErrorResponse("runtime_input click needs nodePath, or says with the words on the control to click.");
|
|
39860
|
+
}
|
|
39644
39861
|
return await this.handleRuntimeCommand("click", {
|
|
39645
39862
|
...whichGame(args),
|
|
39646
39863
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
39647
39864
|
button: readString(args, "button") ?? "left",
|
|
39648
|
-
double: readBoolean(args, "doubleClick") ?? false
|
|
39865
|
+
double: readBoolean(args, "doubleClick") ?? false,
|
|
39866
|
+
...readNonEmptyString(args, "says") === undefined ? {} : { says: readNonEmptyString(args, "says") },
|
|
39867
|
+
...args["index"] === undefined ? {} : { index: args["index"] }
|
|
39649
39868
|
});
|
|
39650
39869
|
}
|
|
39651
39870
|
if (op === "choose") {
|
|
@@ -39704,7 +39923,7 @@ class GodotServer {
|
|
|
39704
39923
|
async engine() {
|
|
39705
39924
|
const located = await this.locator.find();
|
|
39706
39925
|
if (!located.ok) {
|
|
39707
|
-
const refusal =
|
|
39926
|
+
const refusal = GodotLocator.refusal(located);
|
|
39708
39927
|
return { ok: false, response: this.createErrorResponse(refusal.message, [...refusal.advice]) };
|
|
39709
39928
|
}
|
|
39710
39929
|
return { ok: true, value: located.path };
|
|
@@ -39714,8 +39933,8 @@ class GodotServer {
|
|
|
39714
39933
|
if (path === undefined) {
|
|
39715
39934
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
39716
39935
|
}
|
|
39717
|
-
const file =
|
|
39718
|
-
if (!
|
|
39936
|
+
const file = join21(path, "project.godot");
|
|
39937
|
+
if (!existsSync14(file)) {
|
|
39719
39938
|
return {
|
|
39720
39939
|
ok: false,
|
|
39721
39940
|
response: this.createErrorResponse(`Not a Godot project: ${path}`, [
|
|
@@ -40213,7 +40432,7 @@ class GodotServer {
|
|
|
40213
40432
|
}
|
|
40214
40433
|
const scanned = await this.untilTheEditorsScanIsWritten(projectPath);
|
|
40215
40434
|
this.logDebug(`Running ${operation} in ${projectPath}: ${JSON.stringify(params)}`);
|
|
40216
|
-
const outcome = await
|
|
40435
|
+
const outcome = await runOperation({ godotPath: engine.value, script: this.operationsScript, debug: GODOT_DEBUG_MODE_DEFAULT }, operation, params, projectPath);
|
|
40217
40436
|
return withScanWait(outcome, scanned);
|
|
40218
40437
|
}
|
|
40219
40438
|
answer(outcome) {
|
|
@@ -40243,7 +40462,7 @@ class GodotServer {
|
|
|
40243
40462
|
if (!engine.ok) {
|
|
40244
40463
|
return engine.response;
|
|
40245
40464
|
}
|
|
40246
|
-
const application = parseProjectGodot(
|
|
40465
|
+
const application = parseProjectGodot(readFileSync19(project.value.file, "utf8"))["application"] ?? {};
|
|
40247
40466
|
const name = application["config/name"];
|
|
40248
40467
|
const mainScene = application["run/main_scene"];
|
|
40249
40468
|
const info = {
|
|
@@ -40304,18 +40523,18 @@ class GodotServer {
|
|
|
40304
40523
|
if (!engine.ok) {
|
|
40305
40524
|
return engine.response;
|
|
40306
40525
|
}
|
|
40307
|
-
const folder =
|
|
40526
|
+
const folder = dirname15(output.absolutePath);
|
|
40308
40527
|
try {
|
|
40309
|
-
|
|
40528
|
+
mkdirSync12(folder, { recursive: true });
|
|
40310
40529
|
} catch (error) {
|
|
40311
|
-
return this.createErrorResponse(`Cannot create ${
|
|
40530
|
+
return this.createErrorResponse(`Cannot create ${dirname15(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
40312
40531
|
}
|
|
40313
40532
|
const debug = readBoolean(args, "debug") ?? false;
|
|
40314
|
-
const exportLogs =
|
|
40533
|
+
const exportLogs = mkdtempSync2(join21(tmpdir4(), "gdharness-export-"));
|
|
40315
40534
|
const exportArgs = [
|
|
40316
40535
|
"--headless",
|
|
40317
40536
|
"--log-file",
|
|
40318
|
-
|
|
40537
|
+
join21(exportLogs, "engine.log"),
|
|
40319
40538
|
"--path",
|
|
40320
40539
|
project.value.path,
|
|
40321
40540
|
debug ? "--export-debug" : "--export-release",
|
|
@@ -40326,7 +40545,7 @@ class GodotServer {
|
|
|
40326
40545
|
const log = new GameLog;
|
|
40327
40546
|
let ending = { exitCode: 0, exitSignal: null, failure: null };
|
|
40328
40547
|
try {
|
|
40329
|
-
const { stdout, stderr } = await
|
|
40548
|
+
const { stdout, stderr } = await run5(engine.value, exportArgs, {
|
|
40330
40549
|
timeout: 300000,
|
|
40331
40550
|
maxBuffer: 64 * 1024 * 1024,
|
|
40332
40551
|
signal: callSignal()
|
|
@@ -40347,7 +40566,7 @@ class GodotServer {
|
|
|
40347
40566
|
log.finish();
|
|
40348
40567
|
const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
|
|
40349
40568
|
const verdict = {
|
|
40350
|
-
exported: ending.exitCode === 0 && log.count("error") === 0 &&
|
|
40569
|
+
exported: ending.exitCode === 0 && log.count("error") === 0 && existsSync14(output.absolutePath),
|
|
40351
40570
|
preset,
|
|
40352
40571
|
outputPath: output.relativePath,
|
|
40353
40572
|
debug,
|
|
@@ -40383,7 +40602,7 @@ class GodotServer {
|
|
|
40383
40602
|
return contained.response;
|
|
40384
40603
|
}
|
|
40385
40604
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
40386
|
-
if (!
|
|
40605
|
+
if (!existsSync14(join21(project.value.path, runner))) {
|
|
40387
40606
|
return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
|
|
40388
40607
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
40389
40608
|
]);
|
|
@@ -40419,7 +40638,7 @@ class GodotServer {
|
|
|
40419
40638
|
];
|
|
40420
40639
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
|
|
40421
40640
|
this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
40422
|
-
const userData =
|
|
40641
|
+
const userData = mkdtempSync2(join21(tmpdir4(), "gdharness-tests-"));
|
|
40423
40642
|
const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
|
|
40424
40643
|
const console2 = [];
|
|
40425
40644
|
let lastSaid = Date.now();
|
|
@@ -40446,21 +40665,21 @@ class GodotServer {
|
|
|
40446
40665
|
resolve(false);
|
|
40447
40666
|
});
|
|
40448
40667
|
});
|
|
40449
|
-
const reportsDir =
|
|
40668
|
+
const reportsDir = join21(project.value.path, ".godot", ...ours.split("/"));
|
|
40450
40669
|
let report = null;
|
|
40451
40670
|
let reportProblem = null;
|
|
40452
40671
|
try {
|
|
40453
|
-
const written =
|
|
40672
|
+
const written = existsSync14(reportsDir) ? readdirSync8(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
40454
40673
|
const newest = written.at(-1);
|
|
40455
40674
|
if (newest !== undefined) {
|
|
40456
|
-
report = parseJUnit(
|
|
40675
|
+
report = parseJUnit(readFileSync19(join21(reportsDir, newest, "results.xml"), "utf8"));
|
|
40457
40676
|
}
|
|
40458
40677
|
} catch (error) {
|
|
40459
40678
|
reportProblem = errorMessage(error);
|
|
40460
40679
|
} finally {
|
|
40461
40680
|
discard(reportsDir, userData);
|
|
40462
40681
|
try {
|
|
40463
|
-
|
|
40682
|
+
rmdirSync2(dirname15(reportsDir));
|
|
40464
40683
|
} catch {}
|
|
40465
40684
|
}
|
|
40466
40685
|
const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries.map(aboveTheRunner);
|
|
@@ -40482,6 +40701,12 @@ class GodotServer {
|
|
|
40482
40701
|
const brokenScripts = new Set(scriptErrors.map((error) => error.path)).size;
|
|
40483
40702
|
const verdict = cutShort !== null ? cutShort.verdict : nothingRan ?? (brokenScripts > 0 ? `script errors in ${brokenScripts === 1 ? "one script" : `${brokenScripts} scripts`}, so no suite ran` : undefined) ?? (exitCode === null ? undefined : verdicts[exitCode]) ?? (run.exitSignal === null ? `exit ${exitCode ?? "unknown"}` : howItExited(run));
|
|
40484
40703
|
const savesNote = savesStayPut() && nothingRan === null ? SAVES_NOT_MOVED_NOTE : undefined;
|
|
40704
|
+
const marker = readNonEmptyString(args, "printed");
|
|
40705
|
+
const echoed = marker === undefined ? undefined : run.log.select({ severity: "info", sinceLastCall: false, contains: marker, limit: 200 });
|
|
40706
|
+
const printedLines = echoed === undefined ? {} : {
|
|
40707
|
+
printed: echoed.entries.map((entry) => entry.text),
|
|
40708
|
+
...echoed.omitted > 0 ? { printedOmitted: echoed.omitted } : {}
|
|
40709
|
+
};
|
|
40485
40710
|
if (report === null) {
|
|
40486
40711
|
const note = nothingRan !== null ? `No tests ran: ${verdict}.${elsewhereIn(project.value.path, asked)}` : scriptErrors.length > 0 ? scriptErrorsNote(scriptErrors) : `The test run wrote no report (${verdict}${reportProblem ? `; ${reportProblem}` : ""}).`;
|
|
40487
40712
|
return {
|
|
@@ -40500,6 +40725,7 @@ class GodotServer {
|
|
|
40500
40725
|
...scriptErrors.length > 0 ? { scriptErrors } : {},
|
|
40501
40726
|
arguments: cmdArgs,
|
|
40502
40727
|
entries: forAnswer(printed.slice(0, 60).map(aboveTheRunner)),
|
|
40728
|
+
...printedLines,
|
|
40503
40729
|
savesNote,
|
|
40504
40730
|
...scanned
|
|
40505
40731
|
}, null, 2)
|
|
@@ -40536,6 +40762,7 @@ class GodotServer {
|
|
|
40536
40762
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
40537
40763
|
orphans: orphans.total > 0 ? orphans.total : undefined,
|
|
40538
40764
|
notRun: notRun > 0 ? notRun : undefined,
|
|
40765
|
+
...printedLines,
|
|
40539
40766
|
savesNote,
|
|
40540
40767
|
...scanned,
|
|
40541
40768
|
note: verdict.startsWith("warnings") && warnings.length === 0 ? "gdUnit4 exits 101 for orphan nodes when nothing failed, and this run printed no count of them: orphan reporting may be off in the project settings." : notRun > 0 ? `This run stopped at the first failure in each suite it failed in, so ${notRun} case${notRun === 1 ? "" : "s"} never ran and count as neither passed nor failed. Leave failFast out to run every case.` : undefined,
|
|
@@ -40559,7 +40786,7 @@ class GodotServer {
|
|
|
40559
40786
|
}
|
|
40560
40787
|
async godotVersion(godotPath) {
|
|
40561
40788
|
try {
|
|
40562
|
-
const { stdout } = await
|
|
40789
|
+
const { stdout } = await run5(godotPath, ["--version"], { timeout: 1e4 });
|
|
40563
40790
|
this.lastGodotVersion = stdout.trim();
|
|
40564
40791
|
return this.lastGodotVersion;
|
|
40565
40792
|
} catch (error) {
|
|
@@ -40603,11 +40830,11 @@ class GodotServer {
|
|
|
40603
40830
|
const cached = cachedClasses(projectPath);
|
|
40604
40831
|
const contradicted = cached === null ? [] : contradictedDiagnostics(messages, cached, (resourcePath) => {
|
|
40605
40832
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
40606
|
-
if (!contained.ok || !
|
|
40833
|
+
if (!contained.ok || !existsSync14(contained.absolutePath)) {
|
|
40607
40834
|
return null;
|
|
40608
40835
|
}
|
|
40609
40836
|
try {
|
|
40610
|
-
return
|
|
40837
|
+
return readFileSync19(contained.absolutePath, "utf8");
|
|
40611
40838
|
} catch {
|
|
40612
40839
|
return null;
|
|
40613
40840
|
}
|
|
@@ -40657,11 +40884,11 @@ class GodotServer {
|
|
|
40657
40884
|
}
|
|
40658
40885
|
sourceOfClass(projectPath, resourcePath) {
|
|
40659
40886
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
40660
|
-
if (!contained.ok || !
|
|
40887
|
+
if (!contained.ok || !existsSync14(contained.absolutePath)) {
|
|
40661
40888
|
return null;
|
|
40662
40889
|
}
|
|
40663
40890
|
try {
|
|
40664
|
-
return
|
|
40891
|
+
return readFileSync19(contained.absolutePath, "utf8");
|
|
40665
40892
|
} catch {
|
|
40666
40893
|
return null;
|
|
40667
40894
|
}
|
|
@@ -40907,7 +41134,7 @@ class GodotServer {
|
|
|
40907
41134
|
godot: {
|
|
40908
41135
|
path: located.ok ? located.path : null,
|
|
40909
41136
|
version: located.ok ? await this.godotVersion(located.path) : null,
|
|
40910
|
-
problem: located.ok ? undefined :
|
|
41137
|
+
problem: located.ok ? undefined : GodotLocator.refusal(located).message
|
|
40911
41138
|
},
|
|
40912
41139
|
game: {
|
|
40913
41140
|
processActive: active,
|
|
@@ -40930,7 +41157,7 @@ class GodotServer {
|
|
|
40930
41157
|
return null;
|
|
40931
41158
|
}
|
|
40932
41159
|
try {
|
|
40933
|
-
return settingKeys(
|
|
41160
|
+
return settingKeys(readFileSync19(join21(projectPath, "project.godot"), "utf8"));
|
|
40934
41161
|
} catch {
|
|
40935
41162
|
return null;
|
|
40936
41163
|
}
|
|
@@ -41183,7 +41410,7 @@ class GodotServer {
|
|
|
41183
41410
|
}
|
|
41184
41411
|
async runtimeUp(projectPath, before, budget, withArgs) {
|
|
41185
41412
|
const budgetMs = budget.ms;
|
|
41186
|
-
if (!
|
|
41413
|
+
if (!existsSync14(join21(projectPath, RUNTIME_AUTOLOAD.path))) {
|
|
41187
41414
|
return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
|
|
41188
41415
|
}
|
|
41189
41416
|
let asking = null;
|
|
@@ -41246,7 +41473,7 @@ class GodotServer {
|
|
|
41246
41473
|
}
|
|
41247
41474
|
let announcedAt;
|
|
41248
41475
|
try {
|
|
41249
|
-
announcedAt =
|
|
41476
|
+
announcedAt = statSync8(endpoint.file).mtimeMs;
|
|
41250
41477
|
} catch {
|
|
41251
41478
|
return;
|
|
41252
41479
|
}
|
|
@@ -41921,15 +42148,18 @@ class GodotServer {
|
|
|
41921
42148
|
"editor_output with no op answers about the game a run is playing, which is a different log"
|
|
41922
42149
|
]);
|
|
41923
42150
|
}
|
|
41924
|
-
const
|
|
42151
|
+
const asked = readString(args, "severity");
|
|
42152
|
+
const severity = asked === "error" || asked === "warning" ? asked : "info";
|
|
42153
|
+
const contains = readNonEmptyString(args, "contains");
|
|
41925
42154
|
const selected = console2.log.select({
|
|
41926
|
-
severity
|
|
42155
|
+
severity,
|
|
41927
42156
|
sinceLastCall: false,
|
|
41928
|
-
contains
|
|
42157
|
+
contains,
|
|
41929
42158
|
limit: readPositiveNumber(args, "limit") ?? 200
|
|
41930
42159
|
});
|
|
41931
42160
|
const everything = console2.log.everything();
|
|
41932
|
-
const
|
|
42161
|
+
const filtered = severity !== "info" || contains !== undefined;
|
|
42162
|
+
const grouped = bursts(everything, readNonNegativeNumber(args, "before") ?? 3, filtered ? (entry) => answersTo(entry, severity, contains) : undefined);
|
|
41933
42163
|
const shown = grouped.slice(0, MOST_GROUPS);
|
|
41934
42164
|
return this.jsonTextResponse({
|
|
41935
42165
|
editorPid: editorPid ?? undefined,
|
|
@@ -41997,7 +42227,7 @@ class GodotServer {
|
|
|
41997
42227
|
notes.push(endedWithoutACode({
|
|
41998
42228
|
throughEditor: run.throughEditor,
|
|
41999
42229
|
announced: announced !== undefined || run.announcedPid !== undefined,
|
|
42000
|
-
couldAnnounce: run.projectPath !== null &&
|
|
42230
|
+
couldAnnounce: run.projectPath !== null && existsSync14(join21(run.projectPath, RUNTIME_AUTOLOAD.path)),
|
|
42001
42231
|
endedHere: typeof run.endedHere === "string",
|
|
42002
42232
|
pickedUp: run.pickedUp === true
|
|
42003
42233
|
}));
|
|
@@ -42510,7 +42740,7 @@ class GodotServer {
|
|
|
42510
42740
|
async ownRunOnItsWay(projectPath, budgetMs) {
|
|
42511
42741
|
const run = this.currentRun();
|
|
42512
42742
|
const project = projectPath ?? run?.projectPath ?? null;
|
|
42513
|
-
if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !
|
|
42743
|
+
if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync14(join21(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
|
|
42514
42744
|
return null;
|
|
42515
42745
|
}
|
|
42516
42746
|
const already = runtimesAnnounced().running;
|
|
@@ -42519,7 +42749,7 @@ class GodotServer {
|
|
|
42519
42749
|
return !run.announcedBefore.has(one.pid);
|
|
42520
42750
|
}
|
|
42521
42751
|
try {
|
|
42522
|
-
return
|
|
42752
|
+
return statSync8(one.file).mtimeMs >= run.startedAt;
|
|
42523
42753
|
} catch {
|
|
42524
42754
|
return false;
|
|
42525
42755
|
}
|
|
@@ -42589,8 +42819,8 @@ class GodotServer {
|
|
|
42589
42819
|
return this.createErrorResponse(refusal);
|
|
42590
42820
|
}
|
|
42591
42821
|
}
|
|
42592
|
-
const screenshotDir = expectsScreenshot ?
|
|
42593
|
-
const screenshotPath = screenshotDir ?
|
|
42822
|
+
const screenshotDir = expectsScreenshot ? mkdtempSync2(join21(tmpdir4(), "gdharness-runtime-screenshot-")) : null;
|
|
42823
|
+
const screenshotPath = screenshotDir ? join21(screenshotDir, "capture.png") : null;
|
|
42594
42824
|
try {
|
|
42595
42825
|
const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs, !expectsScreenshot);
|
|
42596
42826
|
if (!reply.ok && reply.requestId !== undefined) {
|
|
@@ -42614,7 +42844,7 @@ class GodotServer {
|
|
|
42614
42844
|
if (readString(payload, "type") !== "screenshot_file" || !returnedPath || !screenshotPath) {
|
|
42615
42845
|
return this.createErrorResponse(`The game answered a capture with ${JSON.stringify(payload)}`);
|
|
42616
42846
|
}
|
|
42617
|
-
if (
|
|
42847
|
+
if (normalize2(returnedPath) !== normalize2(screenshotPath)) {
|
|
42618
42848
|
return this.createErrorResponse(`Rejected screenshot file path outside the managed capture path: '${returnedPath}'`);
|
|
42619
42849
|
}
|
|
42620
42850
|
const dimensions = `${readNumber(payload, "width") ?? 0}x${readNumber(payload, "height") ?? 0} ${readString(payload, "format") ?? "unknown"}`;
|
|
@@ -42631,7 +42861,7 @@ class GodotServer {
|
|
|
42631
42861
|
type: "text",
|
|
42632
42862
|
text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}${saveTo === null ? "" : `. Saved to ${saveTo}`}`
|
|
42633
42863
|
},
|
|
42634
|
-
{ type: "image", data:
|
|
42864
|
+
{ type: "image", data: readFileSync19(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
42635
42865
|
]
|
|
42636
42866
|
};
|
|
42637
42867
|
} finally {
|
|
@@ -42731,7 +42961,7 @@ async function runGodotServer() {
|
|
|
42731
42961
|
const server = new GodotServer;
|
|
42732
42962
|
await server.run();
|
|
42733
42963
|
}
|
|
42734
|
-
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250,
|
|
42964
|
+
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run5, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, STOP_WAIT_MS = 1e4, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, SCAN_WAIT_MS = 30000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/";
|
|
42735
42965
|
var init_server3 = __esm(() => {
|
|
42736
42966
|
init_mcp();
|
|
42737
42967
|
init_stdio2();
|
|
@@ -42771,8 +43001,8 @@ var init_server3 = __esm(() => {
|
|
|
42771
43001
|
init_setup();
|
|
42772
43002
|
init_tool_definitions();
|
|
42773
43003
|
init_update_check();
|
|
42774
|
-
|
|
42775
|
-
__dirname2 =
|
|
43004
|
+
run5 = promisify5(execFile5);
|
|
43005
|
+
__dirname2 = dirname15(fileURLToPath5(import.meta.url));
|
|
42776
43006
|
CONSOLE_WRAPPER = /_console(\.exe)?$/i;
|
|
42777
43007
|
LONGEST_BOOT_NOTED_MS = 5 * 60000;
|
|
42778
43008
|
VALUE_NAMES = {
|
|
@@ -42818,128 +43048,11 @@ var init_server3 = __esm(() => {
|
|
|
42818
43048
|
});
|
|
42819
43049
|
|
|
42820
43050
|
// src/cli.ts
|
|
42821
|
-
|
|
42822
|
-
import { join as join25, resolve as resolve6 } from "path";
|
|
42823
|
-
|
|
42824
|
-
// src/bridge-announce.ts
|
|
42825
|
-
init_errors();
|
|
42826
|
-
init_process_children();
|
|
42827
|
-
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
42828
|
-
import { dirname, join } from "node:path";
|
|
42829
|
-
function announcementPath(projectPath) {
|
|
42830
|
-
return join(projectPath, ".godot", "gdharness-bridge.json");
|
|
42831
|
-
}
|
|
42832
|
-
function readAnnouncement(path) {
|
|
42833
|
-
try {
|
|
42834
|
-
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
42835
|
-
if (typeof parsed !== "object" || parsed === null) {
|
|
42836
|
-
return null;
|
|
42837
|
-
}
|
|
42838
|
-
const said = parsed;
|
|
42839
|
-
if (typeof said.port !== "number" || typeof said.pid !== "number") {
|
|
42840
|
-
return null;
|
|
42841
|
-
}
|
|
42842
|
-
return said;
|
|
42843
|
-
} catch {
|
|
42844
|
-
return null;
|
|
42845
|
-
}
|
|
42846
|
-
}
|
|
42847
|
-
function serverBehind(announcement, startedAt) {
|
|
42848
|
-
if (announcement === null || typeof announcement.version !== "string" || startedAt === undefined) {
|
|
42849
|
-
return null;
|
|
42850
|
-
}
|
|
42851
|
-
const announcedAt = Date.parse(announcement.startedAt);
|
|
42852
|
-
if (Number.isNaN(announcedAt) || startedAt > announcedAt + 1000) {
|
|
42853
|
-
return null;
|
|
42854
|
-
}
|
|
42855
|
-
return { version: announcement.version, pid: announcement.pid };
|
|
42856
|
-
}
|
|
42857
|
-
function serverServing(projectPath) {
|
|
42858
|
-
const announcement = readAnnouncement(announcementPath(projectPath));
|
|
42859
|
-
if (announcement === null) {
|
|
42860
|
-
return null;
|
|
42861
|
-
}
|
|
42862
|
-
return serverBehind(announcement, startTimesOf([announcement.pid]).get(announcement.pid));
|
|
42863
|
-
}
|
|
42864
|
-
|
|
42865
|
-
// src/errors.ts
|
|
42866
|
-
class Refusal2 extends Error {
|
|
42867
|
-
}
|
|
42868
|
-
|
|
42869
|
-
// src/godot-path.ts
|
|
42870
|
-
init_detection();
|
|
43051
|
+
init_bridge_announce();
|
|
42871
43052
|
init_errors();
|
|
42872
|
-
|
|
42873
|
-
import { existsSync as
|
|
42874
|
-
import {
|
|
42875
|
-
import { promisify as promisify2 } from "node:util";
|
|
42876
|
-
var run2 = promisify2(execFile2);
|
|
42877
|
-
|
|
42878
|
-
class GodotLocator {
|
|
42879
|
-
answered = new Map;
|
|
42880
|
-
found = null;
|
|
42881
|
-
async find() {
|
|
42882
|
-
if (this.found !== null && await this.whyNot(this.found) === null) {
|
|
42883
|
-
return { ok: true, path: this.found };
|
|
42884
|
-
}
|
|
42885
|
-
this.found = null;
|
|
42886
|
-
const named = envValue("GODOT_PATH");
|
|
42887
|
-
if (named !== undefined) {
|
|
42888
|
-
const path = normalize(named);
|
|
42889
|
-
const reason = await this.whyNot(path);
|
|
42890
|
-
if (reason === null) {
|
|
42891
|
-
this.found = path;
|
|
42892
|
-
return { ok: true, path };
|
|
42893
|
-
}
|
|
42894
|
-
return { ok: false, named: path, reason };
|
|
42895
|
-
}
|
|
42896
|
-
for (const candidate of godotCandidates(process.platform, resolveHomeDirectory())) {
|
|
42897
|
-
const path = normalize(candidate);
|
|
42898
|
-
if (await this.whyNot(path) === null) {
|
|
42899
|
-
this.found = path;
|
|
42900
|
-
return { ok: true, path };
|
|
42901
|
-
}
|
|
42902
|
-
}
|
|
42903
|
-
return { ok: false, named: null };
|
|
42904
|
-
}
|
|
42905
|
-
static refusal(located) {
|
|
42906
|
-
if (located.named === null) {
|
|
42907
|
-
return {
|
|
42908
|
-
message: "No Godot executable found.",
|
|
42909
|
-
advice: [
|
|
42910
|
-
"Set GODOT_PATH to the Godot 4 executable",
|
|
42911
|
-
"Or install Godot where it is usually found, such as /usr/bin/godot or C:\\Program Files\\Godot"
|
|
42912
|
-
]
|
|
42913
|
-
};
|
|
42914
|
-
}
|
|
42915
|
-
return {
|
|
42916
|
-
message: `GODOT_PATH is set to ${located.named}, which ${located.reason ?? "does not answer"}.`,
|
|
42917
|
-
advice: [
|
|
42918
|
-
"Point GODOT_PATH at a Godot 4 executable that answers --version",
|
|
42919
|
-
"Or unset it, and the usual install locations are searched instead"
|
|
42920
|
-
]
|
|
42921
|
-
};
|
|
42922
|
-
}
|
|
42923
|
-
async whyNot(path) {
|
|
42924
|
-
const known = this.answered.get(path);
|
|
42925
|
-
if (known !== undefined) {
|
|
42926
|
-
return known;
|
|
42927
|
-
}
|
|
42928
|
-
let reason;
|
|
42929
|
-
if (path !== "godot" && !existsSync2(path)) {
|
|
42930
|
-
reason = "does not exist";
|
|
42931
|
-
} else {
|
|
42932
|
-
try {
|
|
42933
|
-
await run2(path, ["--version"]);
|
|
42934
|
-
reason = null;
|
|
42935
|
-
} catch (error) {
|
|
42936
|
-
reason = `does not answer --version: ${errorMessage(error)}`;
|
|
42937
|
-
}
|
|
42938
|
-
}
|
|
42939
|
-
this.answered.set(path, reason);
|
|
42940
|
-
return reason;
|
|
42941
|
-
}
|
|
42942
|
-
}
|
|
43053
|
+
init_godot_path();
|
|
43054
|
+
import { existsSync as existsSync15 } from "node:fs";
|
|
43055
|
+
import { join as join22, resolve as resolve6 } from "node:path";
|
|
42943
43056
|
|
|
42944
43057
|
// src/harnesses.ts
|
|
42945
43058
|
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
@@ -43804,204 +43917,9 @@ function taken(harness, path, held, stripped) {
|
|
|
43804
43917
|
return { harness, path, action: "removed" };
|
|
43805
43918
|
}
|
|
43806
43919
|
|
|
43807
|
-
// src/
|
|
43808
|
-
|
|
43809
|
-
|
|
43810
|
-
init_scratch();
|
|
43811
|
-
import { execFile as execFile3 } from "node:child_process";
|
|
43812
|
-
import { mkdtempSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
43813
|
-
import { tmpdir } from "node:os";
|
|
43814
|
-
import { join as join5 } from "node:path";
|
|
43815
|
-
import { promisify as promisify3 } from "node:util";
|
|
43816
|
-
var run3 = promisify3(execFile3);
|
|
43817
|
-
function snakeCased(params) {
|
|
43818
|
-
const result = emptyRecord();
|
|
43819
|
-
for (const [key, value] of Object.entries(params)) {
|
|
43820
|
-
const name = key.startsWith("_") ? key : key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
43821
|
-
result[name] = value;
|
|
43822
|
-
}
|
|
43823
|
-
return result;
|
|
43824
|
-
}
|
|
43825
|
-
function lastJsonObject(stdout) {
|
|
43826
|
-
const lines = stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.startsWith("{"));
|
|
43827
|
-
for (const line of lines.reverse()) {
|
|
43828
|
-
try {
|
|
43829
|
-
const parsed = JSON.parse(line);
|
|
43830
|
-
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
43831
|
-
return parsed;
|
|
43832
|
-
}
|
|
43833
|
-
} catch {}
|
|
43834
|
-
}
|
|
43835
|
-
return null;
|
|
43836
|
-
}
|
|
43837
|
-
function problems(stderr) {
|
|
43838
|
-
const log = new GameLog;
|
|
43839
|
-
log.append("stderr", stderr);
|
|
43840
|
-
log.finish();
|
|
43841
|
-
return log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries;
|
|
43842
|
-
}
|
|
43843
|
-
function reason(stdout, stderr) {
|
|
43844
|
-
const own = stderr.split(/\r?\n/).filter((line) => line.startsWith("[ERROR] ")).map((line) => line.slice("[ERROR] ".length));
|
|
43845
|
-
if (own.length > 0) {
|
|
43846
|
-
return own.join("; ");
|
|
43847
|
-
}
|
|
43848
|
-
const engine = problems(stderr).filter((entry) => entry.severity === "error").map((entry) => entry.text);
|
|
43849
|
-
if (engine.length > 0) {
|
|
43850
|
-
return engine.join("; ");
|
|
43851
|
-
}
|
|
43852
|
-
return stdout.trim().split(/\r?\n/).at(-1) ?? "no output at all";
|
|
43853
|
-
}
|
|
43854
|
-
async function runOperation(engine, operation, params, projectPath) {
|
|
43855
|
-
const paramsDir = mkdtempSync(join5(tmpdir(), "gdharness-params-"));
|
|
43856
|
-
const paramsFile = join5(paramsDir, `${operation}.json`);
|
|
43857
|
-
writeFileSync3(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
|
|
43858
|
-
const args = [
|
|
43859
|
-
"--headless",
|
|
43860
|
-
"--log-file",
|
|
43861
|
-
join5(paramsDir, "engine.log"),
|
|
43862
|
-
"--path",
|
|
43863
|
-
projectPath,
|
|
43864
|
-
"--script",
|
|
43865
|
-
engine.script,
|
|
43866
|
-
operation,
|
|
43867
|
-
`@file:${paramsFile}`,
|
|
43868
|
-
...engine.debug ? ["--debug-godot"] : []
|
|
43869
|
-
];
|
|
43870
|
-
let stdout;
|
|
43871
|
-
let stderr;
|
|
43872
|
-
try {
|
|
43873
|
-
({ stdout, stderr } = await run3(engine.godotPath, args, { signal: callSignal() }));
|
|
43874
|
-
} catch (error) {
|
|
43875
|
-
if (error instanceof Error && "stdout" in error && "stderr" in error) {
|
|
43876
|
-
const failed = error;
|
|
43877
|
-
return {
|
|
43878
|
-
ok: false,
|
|
43879
|
-
message: `${operation} failed (exit ${failed.code ?? "unknown"}): ${reason(failed.stdout, failed.stderr)}`,
|
|
43880
|
-
messages: problems(failed.stderr)
|
|
43881
|
-
};
|
|
43882
|
-
}
|
|
43883
|
-
return {
|
|
43884
|
-
ok: false,
|
|
43885
|
-
message: `${operation} could not be run: ${error instanceof Error ? error.message : String(error)}`,
|
|
43886
|
-
messages: []
|
|
43887
|
-
};
|
|
43888
|
-
} finally {
|
|
43889
|
-
discard(paramsDir);
|
|
43890
|
-
}
|
|
43891
|
-
const payload = lastJsonObject(stdout);
|
|
43892
|
-
if (payload === null) {
|
|
43893
|
-
return {
|
|
43894
|
-
ok: false,
|
|
43895
|
-
message: `${operation} produced no result: ${reason(stdout, stderr)}`,
|
|
43896
|
-
messages: problems(stderr)
|
|
43897
|
-
};
|
|
43898
|
-
}
|
|
43899
|
-
return { ok: true, payload, messages: problems(stderr) };
|
|
43900
|
-
}
|
|
43901
|
-
|
|
43902
|
-
// src/issues.ts
|
|
43903
|
-
init_errors();
|
|
43904
|
-
init_server_version();
|
|
43905
|
-
import { createHash } from "node:crypto";
|
|
43906
|
-
import process4 from "node:process";
|
|
43907
|
-
var NEW_ISSUE = "https://github.com/Aureliolo/gdharness/issues/new";
|
|
43908
|
-
var ENHANCEMENT_URL = `${NEW_ISSUE}?template=feature_request.md`;
|
|
43909
|
-
function runtime() {
|
|
43910
|
-
const versions = process4.versions;
|
|
43911
|
-
const bun = versions["bun"];
|
|
43912
|
-
return bun === undefined ? `node ${process4.versions.node}` : `bun ${bun}`;
|
|
43913
|
-
}
|
|
43914
|
-
function defectSignature(where, message) {
|
|
43915
|
-
const normalised = `${where}
|
|
43916
|
-
${message}`.toLowerCase().replaceAll("\\", "/").replaceAll(/(?:[a-z]:)?(?:\/+[\w.-]+){2,}/gu, "<path>").replaceAll(/\d+/gu, "0");
|
|
43917
|
-
return createHash("sha256").update(normalised).digest("hex").slice(0, 8);
|
|
43918
|
-
}
|
|
43919
|
-
function facts(where, message, godotVersion) {
|
|
43920
|
-
return [
|
|
43921
|
-
["Where", where],
|
|
43922
|
-
["Error", message],
|
|
43923
|
-
["Version", `gdharness ${SERVER_VERSION}`],
|
|
43924
|
-
["Runtime", `${runtime()}, ${process4.platform} ${process4.arch}`],
|
|
43925
|
-
["Godot", godotVersion ?? "not known at the point this failed"],
|
|
43926
|
-
["Signature", defectSignature(where, message)]
|
|
43927
|
-
];
|
|
43928
|
-
}
|
|
43929
|
-
function filledTemplate(where, message, godotVersion) {
|
|
43930
|
-
return [
|
|
43931
|
-
`**gdharness version**: ${SERVER_VERSION}`,
|
|
43932
|
-
`**Godot version**: ${godotVersion ?? ""}`,
|
|
43933
|
-
`**Bun version**: ${runtime()}`,
|
|
43934
|
-
`**OS**: ${process4.platform} ${process4.arch}`,
|
|
43935
|
-
"**MCP client**:",
|
|
43936
|
-
"",
|
|
43937
|
-
"## What you did",
|
|
43938
|
-
"",
|
|
43939
|
-
where,
|
|
43940
|
-
"",
|
|
43941
|
-
"## What you expected",
|
|
43942
|
-
"",
|
|
43943
|
-
"The call to answer, or to refuse with a reason.",
|
|
43944
|
-
"",
|
|
43945
|
-
"## What happened",
|
|
43946
|
-
"",
|
|
43947
|
-
"gdharness failed in a way it does not model.",
|
|
43948
|
-
"",
|
|
43949
|
-
"```text",
|
|
43950
|
-
message,
|
|
43951
|
-
"```",
|
|
43952
|
-
"",
|
|
43953
|
-
`Signature: \`${defectSignature(where, message)}\``,
|
|
43954
|
-
"",
|
|
43955
|
-
"If the real cause turned out to be the project, the environment or the call, say so here:",
|
|
43956
|
-
"reaching you as a defect rather than as a refusal naming what would have worked is then the",
|
|
43957
|
-
"thing to fix.",
|
|
43958
|
-
"",
|
|
43959
|
-
"## Anything that makes it reproducible",
|
|
43960
|
-
""
|
|
43961
|
-
].join(`
|
|
43962
|
-
`);
|
|
43963
|
-
}
|
|
43964
|
-
function defectReport(where, error, context = {}) {
|
|
43965
|
-
const { godotVersion, seenBefore = 0 } = context;
|
|
43966
|
-
const message = errorMessage(error);
|
|
43967
|
-
const title = `Defect ${defectSignature(where, message)}: ${where}`;
|
|
43968
|
-
const url = `${NEW_ISSUE}?template=bug_report.md&title=${encodeURIComponent(title)}` + `&body=${encodeURIComponent(filledTemplate(where, message, godotVersion))}`;
|
|
43969
|
-
const width = Math.max(...facts(where, message, godotVersion).map(([label]) => (label ?? "").length));
|
|
43970
|
-
return [
|
|
43971
|
-
"gdharness failed in a way it does not model. Whatever went wrong underneath, it reached you",
|
|
43972
|
-
"as a crash rather than as a refusal naming what would have worked, and that much is a defect",
|
|
43973
|
-
"in gdharness however the rest of it turns out.",
|
|
43974
|
-
"",
|
|
43975
|
-
...seenBefore === 0 ? [
|
|
43976
|
-
"Send the call once more before anything else. Some of what reaches this line is a file",
|
|
43977
|
-
"another process had open, or a connection that dropped, and those are gone by the next",
|
|
43978
|
-
"attempt. If the same Signature comes back, it is not one of those, and repeating it",
|
|
43979
|
-
"further only spends turns."
|
|
43980
|
-
] : [
|
|
43981
|
-
`You have had this exact failure ${seenBefore === 1 ? "once" : `${seenBefore} times`} in this session already, so it is a`,
|
|
43982
|
-
"state rather than a passing one, and sending the call again will not change it."
|
|
43983
|
-
],
|
|
43984
|
-
"",
|
|
43985
|
-
...facts(where, message, godotVersion).map(([label, value]) => ` ${(label ?? "").padEnd(width)} ${value ?? ""}`),
|
|
43986
|
-
"",
|
|
43987
|
-
"Do not open an issue on your own initiative. Tell the person whose machine this is what",
|
|
43988
|
-
"broke, and ask whether you may report it to gdharness on their behalf. What the report",
|
|
43989
|
-
"carries is the lines above and nothing else, and whether that goes into a public tracker",
|
|
43990
|
-
"is theirs to decide.",
|
|
43991
|
-
"",
|
|
43992
|
-
"If they say yes, this link carries the report already filled in:",
|
|
43993
|
-
url,
|
|
43994
|
-
"",
|
|
43995
|
-
"If they say yes and you have no way to open an issue yourself, give them that link and",
|
|
43996
|
-
"those lines, and stay with them while they file it.",
|
|
43997
|
-
"",
|
|
43998
|
-
"Worth reporting either way. If you can see that what actually went wrong was the project,",
|
|
43999
|
-
"the environment or the call, then this message is the defect: a failure gdharness knows",
|
|
44000
|
-
"about is meant to arrive as a refusal naming what would have worked, not as this. Say that",
|
|
44001
|
-
"in the report and it is the more useful of the two."
|
|
44002
|
-
].join(`
|
|
44003
|
-
`);
|
|
44004
|
-
}
|
|
43920
|
+
// src/cli.ts
|
|
43921
|
+
init_headless();
|
|
43922
|
+
init_issues();
|
|
44005
43923
|
|
|
44006
43924
|
// src/prompt.ts
|
|
44007
43925
|
import process5 from "node:process";
|
|
@@ -44026,256 +43944,15 @@ class Ask {
|
|
|
44026
43944
|
}
|
|
44027
43945
|
}
|
|
44028
43946
|
|
|
44029
|
-
// src/
|
|
44030
|
-
init_update_check();
|
|
44031
|
-
import { readFileSync as readFileSync5 } from "node:fs";
|
|
44032
|
-
var DEBUG_MODE2 = process.env["DEBUG"] === "true";
|
|
44033
|
-
var GODOT_DEBUG_MODE_DEFAULT2 = process.env["GODOT_DEBUG"] === "true" || DEBUG_MODE2;
|
|
44034
|
-
var SERVER_VERSION2 = (() => {
|
|
44035
|
-
try {
|
|
44036
|
-
const pkg = JSON.parse(readFileSync5(new URL("../package.json", import.meta.url), "utf8"));
|
|
44037
|
-
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
44038
|
-
} catch {
|
|
44039
|
-
return "0.0.0";
|
|
44040
|
-
}
|
|
44041
|
-
})();
|
|
44042
|
-
|
|
44043
|
-
// src/setup.ts
|
|
44044
|
-
init_class_cache();
|
|
44045
|
-
init_headless();
|
|
44046
|
-
init_resources();
|
|
43947
|
+
// src/cli.ts
|
|
44047
43948
|
init_server_version();
|
|
44048
|
-
|
|
44049
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
44050
|
-
import {
|
|
44051
|
-
cpSync,
|
|
44052
|
-
existsSync as existsSync7,
|
|
44053
|
-
mkdirSync as mkdirSync4,
|
|
44054
|
-
readdirSync as readdirSync3,
|
|
44055
|
-
readFileSync as readFileSync8,
|
|
44056
|
-
rmdirSync,
|
|
44057
|
-
rmSync as rmSync4,
|
|
44058
|
-
statSync as statSync3,
|
|
44059
|
-
writeFileSync as writeFileSync6
|
|
44060
|
-
} from "node:fs";
|
|
44061
|
-
import { dirname as dirname4, join as join9 } from "node:path";
|
|
44062
|
-
import { fileURLToPath } from "node:url";
|
|
44063
|
-
var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
|
|
44064
|
-
var EDITOR_PLUGINS = ["gdharness_editor", "auto_reload"];
|
|
44065
|
-
var RUNTIME_AUTOLOAD = {
|
|
44066
|
-
name: "GdharnessRuntime",
|
|
44067
|
-
path: "addons/gdharness_runtime/runtime_autoload.gd"
|
|
44068
|
-
};
|
|
44069
|
-
var VERSION_MARKER = ".gdharness-version";
|
|
44070
|
-
var DIGEST_MARKER = ".gdharness-digest";
|
|
44071
|
-
function editorAddonDigest(from = shippedAddonsDirectory()) {
|
|
44072
|
-
const hash = createHash2("sha256");
|
|
44073
|
-
const walk = (directory, relative) => {
|
|
44074
|
-
for (const name of readdirSync3(directory).sort()) {
|
|
44075
|
-
if (name === VERSION_MARKER || name === DIGEST_MARKER) {
|
|
44076
|
-
continue;
|
|
44077
|
-
}
|
|
44078
|
-
const path = join9(directory, name);
|
|
44079
|
-
const inside = `${relative}/${name}`;
|
|
44080
|
-
if (statSync3(path).isDirectory()) {
|
|
44081
|
-
walk(path, inside);
|
|
44082
|
-
} else {
|
|
44083
|
-
hash.update(`${inside}
|
|
44084
|
-
`);
|
|
44085
|
-
hash.update(readFileSync8(path));
|
|
44086
|
-
hash.update(`
|
|
44087
|
-
`);
|
|
44088
|
-
}
|
|
44089
|
-
}
|
|
44090
|
-
};
|
|
44091
|
-
for (const name of EDITOR_PLUGINS) {
|
|
44092
|
-
walk(join9(from, name), name);
|
|
44093
|
-
}
|
|
44094
|
-
return hash.digest("hex");
|
|
44095
|
-
}
|
|
44096
|
-
function installedEditorDigest(projectPath) {
|
|
44097
|
-
const marker = join9(projectPath, "addons", ADDONS[0], DIGEST_MARKER);
|
|
44098
|
-
if (!existsSync7(marker)) {
|
|
44099
|
-
return null;
|
|
44100
|
-
}
|
|
44101
|
-
const said = readFileSync8(marker, "utf8").trim();
|
|
44102
|
-
return said === "" ? null : said;
|
|
44103
|
-
}
|
|
44104
|
-
function shippedAddonsDirectory() {
|
|
44105
|
-
return join9(dirname4(fileURLToPath(import.meta.url)), "godot", "addons");
|
|
44106
|
-
}
|
|
44107
|
-
function shippedOperationsScript() {
|
|
44108
|
-
return join9(dirname4(fileURLToPath(import.meta.url)), "godot", "operations", "godot_operations.gd");
|
|
44109
|
-
}
|
|
44110
|
-
function installAddons(projectPath, from = shippedAddonsDirectory()) {
|
|
44111
|
-
const installed = [];
|
|
44112
|
-
for (const name of ADDONS) {
|
|
44113
|
-
const source = join9(from, name);
|
|
44114
|
-
if (!existsSync7(join9(source, name === "gdharness_runtime" ? "runtime_autoload.gd" : "plugin.cfg"))) {
|
|
44115
|
-
throw new Error(`The package holds no ${name} addon at ${source}.`);
|
|
44116
|
-
}
|
|
44117
|
-
const target = join9(projectPath, "addons", name);
|
|
44118
|
-
const replaced = existsSync7(target);
|
|
44119
|
-
rmSync4(target, { recursive: true, force: true });
|
|
44120
|
-
mkdirSync4(dirname4(target), { recursive: true });
|
|
44121
|
-
cpSync(source, target, { recursive: true });
|
|
44122
|
-
writeFileSync6(join9(target, VERSION_MARKER), `${SERVER_VERSION}
|
|
44123
|
-
`);
|
|
44124
|
-
installed.push({ name, path: target, replaced });
|
|
44125
|
-
}
|
|
44126
|
-
writeFileSync6(join9(projectPath, "addons", ADDONS[0], DIGEST_MARKER), `${editorAddonDigest(from)}
|
|
44127
|
-
`);
|
|
44128
|
-
return installed;
|
|
44129
|
-
}
|
|
44130
|
-
function removeAddons(projectPath) {
|
|
44131
|
-
const removed = [];
|
|
44132
|
-
for (const name of ADDONS) {
|
|
44133
|
-
const target = join9(projectPath, "addons", name);
|
|
44134
|
-
if (existsSync7(target)) {
|
|
44135
|
-
rmSync4(target, { recursive: true, force: true });
|
|
44136
|
-
removed.push(target);
|
|
44137
|
-
}
|
|
44138
|
-
}
|
|
44139
|
-
const addons = join9(projectPath, "addons");
|
|
44140
|
-
if (removed.length > 0 && existsSync7(addons) && readdirSync3(addons).length === 0) {
|
|
44141
|
-
rmdirSync(addons);
|
|
44142
|
-
removed.push(addons);
|
|
44143
|
-
}
|
|
44144
|
-
return removed;
|
|
44145
|
-
}
|
|
44146
|
-
async function disablePlugins(engine, projectPath, names) {
|
|
44147
|
-
const outcomes = [];
|
|
44148
|
-
for (const name of names) {
|
|
44149
|
-
outcomes.push(await runOperation2(engine, "disable_plugin", { pluginName: name }, projectPath));
|
|
44150
|
-
}
|
|
44151
|
-
return outcomes;
|
|
44152
|
-
}
|
|
44153
|
-
async function enablePlugins(engine, projectPath, names) {
|
|
44154
|
-
const outcomes = [];
|
|
44155
|
-
for (const name of names) {
|
|
44156
|
-
outcomes.push(await runOperation2(engine, "enable_plugin", { pluginName: name }, projectPath));
|
|
44157
|
-
}
|
|
44158
|
-
return outcomes;
|
|
44159
|
-
}
|
|
44160
|
-
function autoloadIsOurs(named) {
|
|
44161
|
-
return named === null || named === `res://${RUNTIME_AUTOLOAD.path}`;
|
|
44162
|
-
}
|
|
44163
|
-
async function setRuntime(engine, projectPath, on) {
|
|
44164
|
-
return on ? await runOperation2(engine, "add_autoload", { name: RUNTIME_AUTOLOAD.name, path: `res://${RUNTIME_AUTOLOAD.path}`, enabled: true }, projectPath) : await runOperation2(engine, "remove_autoload", { name: RUNTIME_AUTOLOAD.name }, projectPath);
|
|
44165
|
-
}
|
|
44166
|
-
function loaderAmong(projectPath, named) {
|
|
44167
|
-
let byName = null;
|
|
44168
|
-
for (const [entry, path] of named) {
|
|
44169
|
-
if (entry === RUNTIME_AUTOLOAD.name || path === RUNTIME_AUTOLOAD.path) {
|
|
44170
|
-
continue;
|
|
44171
|
-
}
|
|
44172
|
-
let source = null;
|
|
44173
|
-
try {
|
|
44174
|
-
source = readFileSync8(join9(projectPath, path), "utf8");
|
|
44175
|
-
} catch {}
|
|
44176
|
-
if (source?.includes(RUNTIME_AUTOLOAD.path)) {
|
|
44177
|
-
return { name: entry, path, exists: true };
|
|
44178
|
-
}
|
|
44179
|
-
byName ??= path.toLowerCase().includes("gdharness") ? { name: entry, path, exists: source !== null } : null;
|
|
44180
|
-
}
|
|
44181
|
-
return byName;
|
|
44182
|
-
}
|
|
44183
|
-
function autoloadPaths(settings) {
|
|
44184
|
-
const named = new Map;
|
|
44185
|
-
for (const [name, value] of Object.entries(settings["autoload"] ?? {})) {
|
|
44186
|
-
const said = typeof value === "string" ? value.replace(/^\*/, "") : "";
|
|
44187
|
-
if (said.startsWith("res://")) {
|
|
44188
|
-
named.set(name, said.slice("res://".length));
|
|
44189
|
-
}
|
|
44190
|
-
}
|
|
44191
|
-
return named;
|
|
44192
|
-
}
|
|
44193
|
-
function trackedByGit(projectPath, wanted) {
|
|
44194
|
-
if (wanted.length === 0) {
|
|
44195
|
-
return new Set;
|
|
44196
|
-
}
|
|
44197
|
-
const asked = spawnSync2("git", ["ls-files", "-z", "--", ...wanted], {
|
|
44198
|
-
cwd: projectPath,
|
|
44199
|
-
encoding: "utf8"
|
|
44200
|
-
});
|
|
44201
|
-
if (asked.status !== 0) {
|
|
44202
|
-
return null;
|
|
44203
|
-
}
|
|
44204
|
-
return new Set(asked.stdout.split("\x00").filter(Boolean));
|
|
44205
|
-
}
|
|
44206
|
-
function enabledPlugins(settings) {
|
|
44207
|
-
const raw = settings["editor_plugins"]?.["enabled"];
|
|
44208
|
-
if (typeof raw !== "string") {
|
|
44209
|
-
return [];
|
|
44210
|
-
}
|
|
44211
|
-
return [...raw.matchAll(/res:\/\/addons\/([^/"]+)\/plugin\.cfg/g)].map((match) => match[1] ?? "").filter(Boolean);
|
|
44212
|
-
}
|
|
44213
|
-
function inspectProject(projectPath) {
|
|
44214
|
-
const problems = [];
|
|
44215
|
-
const addons = ADDONS.map((name) => {
|
|
44216
|
-
const directory = join9(projectPath, "addons", name);
|
|
44217
|
-
const marker = join9(directory, VERSION_MARKER);
|
|
44218
|
-
const installed = existsSync7(directory);
|
|
44219
|
-
const version = existsSync7(marker) ? readFileSync8(marker, "utf8").trim() : null;
|
|
44220
|
-
const current = version === SERVER_VERSION;
|
|
44221
|
-
if (!installed) {
|
|
44222
|
-
problems.push(`addons/${name} is not installed; run gdharness setup`);
|
|
44223
|
-
} else if (!current) {
|
|
44224
|
-
problems.push(`addons/${name} is ${version ?? "of an unknown version"}, this gdharness is ${SERVER_VERSION}; run gdharness setup`);
|
|
44225
|
-
}
|
|
44226
|
-
return { name, installed, version, current };
|
|
44227
|
-
});
|
|
44228
|
-
const settings = parseProjectGodot(readFileSync8(join9(projectPath, "project.godot"), "utf8"));
|
|
44229
|
-
const pluginsEnabled = enabledPlugins(settings);
|
|
44230
|
-
for (const name of EDITOR_PLUGINS) {
|
|
44231
|
-
if (!pluginsEnabled.includes(name)) {
|
|
44232
|
-
problems.push(`the ${name} plugin is not enabled in project.godot; run gdharness setup`);
|
|
44233
|
-
}
|
|
44234
|
-
}
|
|
44235
|
-
const autoload = settings["autoload"];
|
|
44236
|
-
const registered = autoload === undefined ? undefined : readString(autoload, RUNTIME_AUTOLOAD.name);
|
|
44237
|
-
const runtimeAutoloadPath = typeof registered === "string" ? registered.replace(/^\*/, "") : null;
|
|
44238
|
-
const named = autoloadPaths(settings);
|
|
44239
|
-
const runtimeLoaderAutoload = loaderAmong(projectPath, named);
|
|
44240
|
-
const runtimeAutoload = runtimeAutoloadPath !== null || runtimeLoaderAutoload !== null;
|
|
44241
|
-
const tracked = trackedByGit(projectPath, [...named.values()]);
|
|
44242
|
-
const addonMissing = (path) => addons.some((addon) => !addon.installed && path.startsWith(`addons/${addon.name}/`));
|
|
44243
|
-
for (const [name, path] of named) {
|
|
44244
|
-
if (!existsSync7(join9(projectPath, path))) {
|
|
44245
|
-
if (!addonMissing(path)) {
|
|
44246
|
-
problems.push(`the ${name} autoload names res://${path}, which is not in this project, so it boots with a missing script; restore that file, or point the autoload at one that is here`);
|
|
44247
|
-
}
|
|
44248
|
-
continue;
|
|
44249
|
-
}
|
|
44250
|
-
if (tracked !== null && !tracked.has(path)) {
|
|
44251
|
-
problems.push(`the ${name} autoload names res://${path}, which git does not carry, so a clone boots with a missing script; commit that file, or point the autoload at one the project tracks`);
|
|
44252
|
-
}
|
|
44253
|
-
}
|
|
44254
|
-
const cached = cachedClasses(projectPath);
|
|
44255
|
-
const staleClasses = cached === null ? [] : staleAgainst(cached, projectPath);
|
|
44256
|
-
if (cached === null) {
|
|
44257
|
-
problems.push("no .godot/global_script_class_cache.cfg: the engine knows no class_name; run project_import refresh_classes or open the editor");
|
|
44258
|
-
} else if (staleClasses.length > 0) {
|
|
44259
|
-
problems.push(`the class cache does not list ${staleClasses.join(", ")}; run project_import refresh_classes or gdharness classes`);
|
|
44260
|
-
}
|
|
44261
|
-
return {
|
|
44262
|
-
projectPath,
|
|
44263
|
-
addons,
|
|
44264
|
-
pluginsEnabled,
|
|
44265
|
-
runtimeAutoload,
|
|
44266
|
-
runtimeAutoloadPath,
|
|
44267
|
-
runtimeLoaderAutoload,
|
|
44268
|
-
staleClasses,
|
|
44269
|
-
classCacheExists: cached !== null,
|
|
44270
|
-
problems
|
|
44271
|
-
};
|
|
44272
|
-
}
|
|
43949
|
+
init_setup();
|
|
44273
43950
|
|
|
44274
43951
|
// src/skill.ts
|
|
44275
43952
|
init_setup();
|
|
44276
43953
|
init_tool_definitions();
|
|
44277
|
-
import { existsSync as
|
|
44278
|
-
import { basename as basename2, dirname as
|
|
43954
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync5, readdirSync as readdirSync4, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "node:fs";
|
|
43955
|
+
import { basename as basename2, dirname as dirname5, join as join9 } from "node:path";
|
|
44279
43956
|
|
|
44280
43957
|
// src/tool-reference.ts
|
|
44281
43958
|
init_headless_operations();
|
|
@@ -44371,7 +44048,7 @@ function renderToolsMarkdown() {
|
|
|
44371
44048
|
|
|
44372
44049
|
// src/skill.ts
|
|
44373
44050
|
var SKILL_NAME = "gdharness";
|
|
44374
|
-
var SHARED_SKILLS =
|
|
44051
|
+
var SHARED_SKILLS = join9(".agents", "skills");
|
|
44375
44052
|
function skillMarkdown(version) {
|
|
44376
44053
|
return `---
|
|
44377
44054
|
name: ${SKILL_NAME}
|
|
@@ -44441,7 +44118,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
44441
44118
|
| What a property reads right now, on a node whose path you already hold | \`runtime_inspect\` \`property\`, \`runtime_invoke\` |
|
|
44442
44119
|
| What a property on an object a node holds reads | the same two, with colons: \`_game:clock:speed\` |
|
|
44443
44120
|
| Which control holds the focus, or anything only a method answers | the same path with a step written as a call, \`get_viewport():gui_get_focus_owner\`, which calls a method taking no arguments and walks into what it returned |
|
|
44444
|
-
| Press a button | \`runtime_input click
|
|
44121
|
+
| Press a button | \`runtime_input click\` with \`says\` and the words on it, which finds it and presses it in the same frame and says what was under the pointer. A \`find\` and then a click by the path it answered is two calls over a generated path, and a panel that rebuilds between them frees the button first; \`nodePath\` narrows where \`says\` looks, and several matches are refused with an \`index\` for each |
|
|
44445
44122
|
| Press the next thing after a click that changed screen | \`runtime_wait until\` with \`says\` and a word the new screen shows (no \`nodePath\` needed: without one the whole game is searched), then the click. A click sent straight after a screen change lands on a control the new screen is about to free, answers \`landed: true\`, and does nothing; a frame count is a number nobody can pick correctly across a cold start and a warm one, and waiting for the words answered in about 130ms where twenty frames had not been enough |
|
|
44446
44123
|
| Fill in a field | \`runtime_input click\` on it, then \`runtime_input text\` with \`replace\`; a submitted field has to be clicked again |
|
|
44447
44124
|
| Click somebody standing in a 3D room | \`runtime_input click\` on the Node3D, which aims at what it draws |
|
|
@@ -44560,27 +44237,27 @@ whatever the engine said on stderr comes back under \`engine_messages\`.
|
|
|
44560
44237
|
function skillFiles(version) {
|
|
44561
44238
|
return new Map([
|
|
44562
44239
|
["SKILL.md", skillMarkdown(version)],
|
|
44563
|
-
[
|
|
44240
|
+
[join9("references", "tools.md"), renderToolsMarkdown()]
|
|
44564
44241
|
]);
|
|
44565
44242
|
}
|
|
44566
44243
|
function skillDirectories(harnesses, projectPath, exists) {
|
|
44567
|
-
const directories = [
|
|
44244
|
+
const directories = [join9(projectPath, SHARED_SKILLS, SKILL_NAME)];
|
|
44568
44245
|
for (const harness of harnesses) {
|
|
44569
44246
|
if (harness.skills === undefined) {
|
|
44570
44247
|
continue;
|
|
44571
44248
|
}
|
|
44572
|
-
const own =
|
|
44573
|
-
if ((!harness.skills.shared || exists(own)) && !directories.includes(
|
|
44574
|
-
directories.push(
|
|
44249
|
+
const own = join9(projectPath, harness.skills.dir);
|
|
44250
|
+
if ((!harness.skills.shared || exists(own)) && !directories.includes(join9(own, SKILL_NAME))) {
|
|
44251
|
+
directories.push(join9(own, SKILL_NAME));
|
|
44575
44252
|
}
|
|
44576
44253
|
}
|
|
44577
44254
|
return directories;
|
|
44578
44255
|
}
|
|
44579
44256
|
function everySkillDirectory(projectPath, harnesses) {
|
|
44580
|
-
const directories = new Set([
|
|
44257
|
+
const directories = new Set([join9(projectPath, SHARED_SKILLS, SKILL_NAME)]);
|
|
44581
44258
|
for (const harness of harnesses) {
|
|
44582
44259
|
if (harness.skills !== undefined) {
|
|
44583
|
-
directories.add(
|
|
44260
|
+
directories.add(join9(projectPath, harness.skills.dir, SKILL_NAME));
|
|
44584
44261
|
}
|
|
44585
44262
|
}
|
|
44586
44263
|
return [...directories];
|
|
@@ -44588,52 +44265,52 @@ function everySkillDirectory(projectPath, harnesses) {
|
|
|
44588
44265
|
function removeSkill(directories, projectPath) {
|
|
44589
44266
|
const removed = [];
|
|
44590
44267
|
for (const directory of directories) {
|
|
44591
|
-
if (
|
|
44592
|
-
|
|
44593
|
-
pruneEmpty(
|
|
44268
|
+
if (existsSync8(directory)) {
|
|
44269
|
+
rmSync5(directory, { recursive: true, force: true });
|
|
44270
|
+
pruneEmpty(dirname5(directory), projectPath);
|
|
44594
44271
|
removed.push(directory);
|
|
44595
44272
|
}
|
|
44596
44273
|
}
|
|
44597
44274
|
return removed;
|
|
44598
44275
|
}
|
|
44599
44276
|
function pruneEmpty(directory, projectPath) {
|
|
44600
|
-
const shared =
|
|
44277
|
+
const shared = join9(projectPath, ".agents");
|
|
44601
44278
|
let path = directory;
|
|
44602
44279
|
while (path !== projectPath && (basename2(path) === "skills" || path === shared)) {
|
|
44603
|
-
if (
|
|
44280
|
+
if (readdirSync4(path).length > 0) {
|
|
44604
44281
|
return;
|
|
44605
44282
|
}
|
|
44606
|
-
|
|
44607
|
-
path =
|
|
44283
|
+
rmSync5(path, { recursive: true, force: true });
|
|
44284
|
+
path = dirname5(path);
|
|
44608
44285
|
}
|
|
44609
44286
|
}
|
|
44610
44287
|
function writeSkill(directories, version) {
|
|
44611
44288
|
const files = skillFiles(version);
|
|
44612
44289
|
return directories.map((directory) => {
|
|
44613
|
-
const replaced =
|
|
44614
|
-
|
|
44290
|
+
const replaced = existsSync8(directory);
|
|
44291
|
+
rmSync5(directory, { recursive: true, force: true });
|
|
44615
44292
|
for (const [name, contents] of files) {
|
|
44616
|
-
const path =
|
|
44617
|
-
|
|
44618
|
-
|
|
44293
|
+
const path = join9(directory, name);
|
|
44294
|
+
mkdirSync5(dirname5(path), { recursive: true });
|
|
44295
|
+
writeFileSync6(path, contents, "utf8");
|
|
44619
44296
|
}
|
|
44620
44297
|
return { path: directory, replaced };
|
|
44621
44298
|
});
|
|
44622
44299
|
}
|
|
44623
44300
|
|
|
44624
44301
|
// src/version.ts
|
|
44625
|
-
import { readFileSync as
|
|
44626
|
-
import { dirname as
|
|
44627
|
-
import { fileURLToPath as
|
|
44302
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
44303
|
+
import { dirname as dirname6, join as join10 } from "node:path";
|
|
44304
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
44628
44305
|
function getLocalVersion() {
|
|
44629
|
-
const currentDirectory =
|
|
44306
|
+
const currentDirectory = dirname6(fileURLToPath2(import.meta.url));
|
|
44630
44307
|
const packageCandidates = [
|
|
44631
|
-
|
|
44632
|
-
|
|
44308
|
+
join10(currentDirectory, "..", "package.json"),
|
|
44309
|
+
join10(currentDirectory, "..", "..", "package.json")
|
|
44633
44310
|
];
|
|
44634
44311
|
for (const packagePath of packageCandidates) {
|
|
44635
44312
|
try {
|
|
44636
|
-
const value = JSON.parse(
|
|
44313
|
+
const value = JSON.parse(readFileSync8(packagePath, "utf-8"));
|
|
44637
44314
|
if (typeof value === "object" && value !== null && "name" in value && value.name === "gdharness" && "version" in value && typeof value.version === "string") {
|
|
44638
44315
|
return value.version;
|
|
44639
44316
|
}
|
|
@@ -44646,12 +44323,12 @@ function getLocalVersion() {
|
|
|
44646
44323
|
var args = process.argv.slice(2);
|
|
44647
44324
|
var command = args[0];
|
|
44648
44325
|
|
|
44649
|
-
class UsageError extends
|
|
44326
|
+
class UsageError extends Refusal {
|
|
44650
44327
|
}
|
|
44651
44328
|
function projectArgument(at) {
|
|
44652
44329
|
const named = args.slice(at).find((value) => !value.startsWith("--"));
|
|
44653
44330
|
const projectPath = resolve6(named ?? ".");
|
|
44654
|
-
if (!
|
|
44331
|
+
if (!existsSync15(join22(projectPath, "project.godot"))) {
|
|
44655
44332
|
throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
|
|
44656
44333
|
}
|
|
44657
44334
|
return projectPath;
|
|
@@ -44659,18 +44336,18 @@ function projectArgument(at) {
|
|
|
44659
44336
|
async function engine(projectPath) {
|
|
44660
44337
|
const located = await new GodotLocator().find();
|
|
44661
44338
|
if (located.ok) {
|
|
44662
|
-
return { godotPath: located.path, script: shippedOperationsScript(), debug:
|
|
44339
|
+
return { godotPath: located.path, script: shippedOperationsScript(), debug: GODOT_DEBUG_MODE_DEFAULT };
|
|
44663
44340
|
}
|
|
44664
44341
|
const recorded = located.named === null && projectPath !== undefined ? recordedEnginePath(projectPath) : null;
|
|
44665
44342
|
if (recorded === null) {
|
|
44666
44343
|
const refusal = GodotLocator.refusal(located);
|
|
44667
44344
|
throw new UsageError(`${refusal.message} ${refusal.advice.join(". ")}.`);
|
|
44668
44345
|
}
|
|
44669
|
-
return { godotPath: recorded, script: shippedOperationsScript(), debug:
|
|
44346
|
+
return { godotPath: recorded, script: shippedOperationsScript(), debug: GODOT_DEBUG_MODE_DEFAULT };
|
|
44670
44347
|
}
|
|
44671
44348
|
function said2(outcome, what) {
|
|
44672
44349
|
if (!outcome.ok) {
|
|
44673
|
-
throw new
|
|
44350
|
+
throw new Refusal(`${what}: ${outcome.message}`);
|
|
44674
44351
|
}
|
|
44675
44352
|
for (const entry of outcome.messages) {
|
|
44676
44353
|
console.error(` engine ${entry.severity}: ${entry.text}`);
|
|
@@ -44690,7 +44367,7 @@ async function registerRuntime(godot, projectPath) {
|
|
|
44690
44367
|
return;
|
|
44691
44368
|
}
|
|
44692
44369
|
console.log(`${RUNTIME_AUTOLOAD.name} autoload left at ${named}, which this project registers itself`);
|
|
44693
|
-
if (!
|
|
44370
|
+
if (!existsSync15(join22(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
|
|
44694
44371
|
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}`);
|
|
44695
44372
|
}
|
|
44696
44373
|
}
|
|
@@ -44822,7 +44499,7 @@ async function setup() {
|
|
|
44822
44499
|
console.log("no harness configured; name one yourself, and gdharness harnesses lists them all");
|
|
44823
44500
|
}
|
|
44824
44501
|
if (!args.includes("--no-skill")) {
|
|
44825
|
-
const directories = skillDirectories(chosen.harnesses, projectPath,
|
|
44502
|
+
const directories = skillDirectories(chosen.harnesses, projectPath, existsSync15);
|
|
44826
44503
|
for (const written of writeSkill(directories, getLocalVersion())) {
|
|
44827
44504
|
console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
|
|
44828
44505
|
}
|
|
@@ -44916,7 +44593,7 @@ async function upgrade() {
|
|
|
44916
44593
|
moved.push(took.from);
|
|
44917
44594
|
}
|
|
44918
44595
|
}
|
|
44919
|
-
for (const written of writeSkill(skillDirectories(already, projectPath,
|
|
44596
|
+
for (const written of writeSkill(skillDirectories(already, projectPath, existsSync15), version)) {
|
|
44920
44597
|
console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
|
|
44921
44598
|
}
|
|
44922
44599
|
console.log(`
|
|
@@ -44948,7 +44625,7 @@ function doctorReport(projectPath) {
|
|
|
44948
44625
|
process.exitCode = 1;
|
|
44949
44626
|
}
|
|
44950
44627
|
}
|
|
44951
|
-
async function
|
|
44628
|
+
async function runtime2() {
|
|
44952
44629
|
const state = args[1];
|
|
44953
44630
|
if (state !== "on" && state !== "off") {
|
|
44954
44631
|
throw new UsageError("gdharness runtime takes on or off, then the project directory.");
|
|
@@ -45057,7 +44734,7 @@ async function main() {
|
|
|
45057
44734
|
doctorReport(projectArgument(1));
|
|
45058
44735
|
return;
|
|
45059
44736
|
case "runtime":
|
|
45060
|
-
await
|
|
44737
|
+
await runtime2();
|
|
45061
44738
|
return;
|
|
45062
44739
|
case "classes":
|
|
45063
44740
|
await classes();
|
|
@@ -45086,7 +44763,7 @@ async function main() {
|
|
|
45086
44763
|
}
|
|
45087
44764
|
}
|
|
45088
44765
|
await main().catch((error) => {
|
|
45089
|
-
if (error instanceof
|
|
44766
|
+
if (error instanceof Refusal) {
|
|
45090
44767
|
console.error("gdharness:", error.message);
|
|
45091
44768
|
process.exit(error instanceof UsageError ? 2 : 1);
|
|
45092
44769
|
}
|