gdharness 1.1.8 → 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 +929 -1245
- 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/godot/operations/project_config.gd +4 -3
- package/build/index.js +97 -159
- 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,74 +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
|
-
|
|
15582
|
-
|
|
15583
|
-
|
|
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
|
|
15584
15866
|
} from "node:fs";
|
|
15585
|
-
import { dirname as
|
|
15586
|
-
import { fileURLToPath
|
|
15587
|
-
function
|
|
15588
|
-
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");
|
|
15589
15871
|
const walk = (directory, relative) => {
|
|
15590
|
-
for (const name of
|
|
15591
|
-
if (name ===
|
|
15872
|
+
for (const name of readdirSync3(directory).sort()) {
|
|
15873
|
+
if (name === VERSION_MARKER || name === DIGEST_MARKER) {
|
|
15592
15874
|
continue;
|
|
15593
15875
|
}
|
|
15594
|
-
const path =
|
|
15876
|
+
const path = join8(directory, name);
|
|
15595
15877
|
const inside = `${relative}/${name}`;
|
|
15596
|
-
if (
|
|
15878
|
+
if (statSync3(path).isDirectory()) {
|
|
15597
15879
|
walk(path, inside);
|
|
15598
15880
|
} else {
|
|
15599
15881
|
hash.update(`${inside}
|
|
15600
15882
|
`);
|
|
15601
|
-
hash.update(
|
|
15883
|
+
hash.update(readFileSync7(path));
|
|
15602
15884
|
hash.update(`
|
|
15603
15885
|
`);
|
|
15604
15886
|
}
|
|
15605
15887
|
}
|
|
15606
15888
|
};
|
|
15607
|
-
for (const name of
|
|
15608
|
-
walk(
|
|
15889
|
+
for (const name of EDITOR_PLUGINS) {
|
|
15890
|
+
walk(join8(from, name), name);
|
|
15609
15891
|
}
|
|
15610
15892
|
return hash.digest("hex");
|
|
15611
15893
|
}
|
|
15612
15894
|
function shippedEditorDigest() {
|
|
15613
15895
|
if (shippedDigest === undefined) {
|
|
15614
15896
|
try {
|
|
15615
|
-
shippedDigest =
|
|
15897
|
+
shippedDigest = editorAddonDigest();
|
|
15616
15898
|
} catch {
|
|
15617
15899
|
shippedDigest = null;
|
|
15618
15900
|
}
|
|
15619
15901
|
}
|
|
15620
15902
|
return shippedDigest ?? undefined;
|
|
15621
15903
|
}
|
|
15622
|
-
function
|
|
15623
|
-
|
|
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;
|
|
15624
15937
|
}
|
|
15625
15938
|
function installedAddonVersion(projectPath) {
|
|
15626
|
-
const marker =
|
|
15627
|
-
if (!
|
|
15939
|
+
const marker = join8(projectPath, "addons", ADDONS[0], VERSION_MARKER);
|
|
15940
|
+
if (!existsSync7(marker)) {
|
|
15628
15941
|
return null;
|
|
15629
15942
|
}
|
|
15630
|
-
const said =
|
|
15943
|
+
const said = readFileSync7(marker, "utf8").trim();
|
|
15631
15944
|
return said === "" ? null : said;
|
|
15632
15945
|
}
|
|
15633
|
-
|
|
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;
|
|
15634
16090
|
var init_setup = __esm(() => {
|
|
15635
16091
|
init_class_cache();
|
|
15636
16092
|
init_headless();
|
|
15637
16093
|
init_resources();
|
|
15638
16094
|
init_server_version();
|
|
15639
|
-
|
|
15640
|
-
|
|
15641
|
-
|
|
16095
|
+
ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
|
|
16096
|
+
EDITOR_PLUGINS = ["gdharness_editor", "auto_reload"];
|
|
16097
|
+
RUNTIME_AUTOLOAD = {
|
|
15642
16098
|
name: "GdharnessRuntime",
|
|
15643
16099
|
path: "addons/gdharness_runtime/runtime_autoload.gd"
|
|
15644
16100
|
};
|
|
@@ -16088,6 +16544,10 @@ var init_tool_definitions = __esm(() => {
|
|
|
16088
16544
|
type: "integer",
|
|
16089
16545
|
minimum: 1,
|
|
16090
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."
|
|
16091
16551
|
}
|
|
16092
16552
|
},
|
|
16093
16553
|
requires: ["projectPath"]
|
|
@@ -16737,14 +17197,19 @@ var init_tool_definitions = __esm(() => {
|
|
|
16737
17197
|
},
|
|
16738
17198
|
{
|
|
16739
17199
|
name: "runtime_input",
|
|
16740
|
-
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.",
|
|
16741
17201
|
parameters: {
|
|
16742
17202
|
projectPath: RUNNING_PROJECT_PATH,
|
|
16743
17203
|
pid: RUNNING_PID,
|
|
16744
17204
|
nodePath: {
|
|
16745
17205
|
type: "string",
|
|
16746
17206
|
ops: ["click", "choose"],
|
|
16747
|
-
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."
|
|
16748
17213
|
},
|
|
16749
17214
|
action: {
|
|
16750
17215
|
type: "string",
|
|
@@ -16781,8 +17246,8 @@ var init_tool_definitions = __esm(() => {
|
|
|
16781
17246
|
},
|
|
16782
17247
|
index: {
|
|
16783
17248
|
type: "number",
|
|
16784
|
-
ops: ["choose"],
|
|
16785
|
-
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."
|
|
16786
17251
|
},
|
|
16787
17252
|
shift: { type: "boolean", ops: ["key"], description: "key: hold Shift with it. Default false." },
|
|
16788
17253
|
ctrl: { type: "boolean", ops: ["key"], description: "key: hold Ctrl with it. Default false." },
|
|
@@ -16821,8 +17286,8 @@ var init_tool_definitions = __esm(() => {
|
|
|
16821
17286
|
requires: [],
|
|
16822
17287
|
operations: {
|
|
16823
17288
|
click: {
|
|
16824
|
-
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",
|
|
16825
|
-
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: []
|
|
16826
17291
|
},
|
|
16827
17292
|
choose: {
|
|
16828
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",
|
|
@@ -31702,10 +32167,10 @@ var init_stdio2 = __esm(() => {
|
|
|
31702
32167
|
});
|
|
31703
32168
|
|
|
31704
32169
|
// src/runtime-client.ts
|
|
31705
|
-
import { existsSync as
|
|
32170
|
+
import { existsSync as existsSync9, readdirSync as readdirSync5, readFileSync as readFileSync9, statSync as statSync4, unlinkSync } from "node:fs";
|
|
31706
32171
|
import { createConnection } from "node:net";
|
|
31707
|
-
import { tmpdir as
|
|
31708
|
-
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";
|
|
31709
32174
|
import { setTimeout as delay } from "node:timers/promises";
|
|
31710
32175
|
function runtimeDirectory(variables = process.env) {
|
|
31711
32176
|
const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
|
|
@@ -31713,20 +32178,20 @@ function runtimeDirectory(variables = process.env) {
|
|
|
31713
32178
|
return explicit;
|
|
31714
32179
|
}
|
|
31715
32180
|
const perUser = envValue("XDG_RUNTIME_DIR", variables);
|
|
31716
|
-
return
|
|
32181
|
+
return join11(perUser ?? tmpdir3(), "gdharness");
|
|
31717
32182
|
}
|
|
31718
32183
|
function runtimeDirectories(variables = process.env) {
|
|
31719
32184
|
const candidates = [runtimeDirectory(variables)];
|
|
31720
32185
|
const fallbacks = process.platform === "win32" ? [envValue("SystemRoot", variables) ?? envValue("windir", variables) ?? "C:\\Windows"] : ["/tmp", "/var/tmp"];
|
|
31721
32186
|
for (const base of fallbacks) {
|
|
31722
|
-
candidates.push(
|
|
32187
|
+
candidates.push(join11(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
|
|
31723
32188
|
}
|
|
31724
32189
|
return [...new Set(candidates.map((path) => resolve3(path)))];
|
|
31725
32190
|
}
|
|
31726
32191
|
function errorReportOf(pid, directories = runtimeDirectories()) {
|
|
31727
32192
|
for (const directory of directories) {
|
|
31728
|
-
const path =
|
|
31729
|
-
if (
|
|
32193
|
+
const path = join11(directory, `runtime-${pid}.log`);
|
|
32194
|
+
if (existsSync9(path)) {
|
|
31730
32195
|
return path;
|
|
31731
32196
|
}
|
|
31732
32197
|
}
|
|
@@ -31743,7 +32208,7 @@ function processAlive(pid) {
|
|
|
31743
32208
|
function parseAnnouncement(file, pid) {
|
|
31744
32209
|
let fields;
|
|
31745
32210
|
try {
|
|
31746
|
-
fields = asParams(JSON.parse(
|
|
32211
|
+
fields = asParams(JSON.parse(readFileSync9(file, "utf8")));
|
|
31747
32212
|
} catch {
|
|
31748
32213
|
return { kind: "rubbish" };
|
|
31749
32214
|
}
|
|
@@ -31760,7 +32225,7 @@ function parseAnnouncement(file, pid) {
|
|
|
31760
32225
|
};
|
|
31761
32226
|
let announcedAt;
|
|
31762
32227
|
try {
|
|
31763
|
-
announcedAt =
|
|
32228
|
+
announcedAt = statSync4(file).mtimeMs;
|
|
31764
32229
|
} catch {}
|
|
31765
32230
|
const when = announcedAt === undefined ? {} : { announcedAt };
|
|
31766
32231
|
if (protocol !== RUNTIME_PROTOCOL) {
|
|
@@ -31801,7 +32266,7 @@ function discoverRuntimes(directories = runtimeDirectories()) {
|
|
|
31801
32266
|
function wentAway(file, pid) {
|
|
31802
32267
|
let project = "";
|
|
31803
32268
|
try {
|
|
31804
|
-
const fields = asParams(JSON.parse(
|
|
32269
|
+
const fields = asParams(JSON.parse(readFileSync9(file, "utf8")));
|
|
31805
32270
|
project = readString(readParams(fields, "project") ?? {}, "path") ?? "";
|
|
31806
32271
|
} catch {
|
|
31807
32272
|
return;
|
|
@@ -31838,29 +32303,29 @@ function strangersAmong(candidates, now, startTimes = startTimesOf) {
|
|
|
31838
32303
|
return new Set(candidates.filter((one) => verdicts.get(one.file)?.stranger === true).map((one) => one.file));
|
|
31839
32304
|
}
|
|
31840
32305
|
function announcedIn(directory) {
|
|
31841
|
-
if (!
|
|
32306
|
+
if (!existsSync9(directory)) {
|
|
31842
32307
|
return [];
|
|
31843
32308
|
}
|
|
31844
32309
|
const found = [];
|
|
31845
32310
|
const candidates = [];
|
|
31846
32311
|
const entries = [];
|
|
31847
|
-
for (const entry of
|
|
32312
|
+
for (const entry of readdirSync5(directory)) {
|
|
31848
32313
|
const report = ERROR_REPORT_PATTERN.exec(entry);
|
|
31849
32314
|
if (report) {
|
|
31850
|
-
sweepErrorReport(
|
|
32315
|
+
sweepErrorReport(join11(directory, entry), Number.parseInt(report[1] ?? "", 10));
|
|
31851
32316
|
continue;
|
|
31852
32317
|
}
|
|
31853
32318
|
const match = ANNOUNCEMENT_PATTERN.exec(entry);
|
|
31854
32319
|
if (!match) {
|
|
31855
32320
|
continue;
|
|
31856
32321
|
}
|
|
31857
|
-
const file =
|
|
32322
|
+
const file = join11(directory, entry);
|
|
31858
32323
|
const pid = Number.parseInt(match[1] ?? "", 10);
|
|
31859
32324
|
const alive = processAlive(pid);
|
|
31860
32325
|
entries.push({ file, pid, alive });
|
|
31861
32326
|
if (alive) {
|
|
31862
32327
|
try {
|
|
31863
|
-
candidates.push({ file, pid, writtenAt:
|
|
32328
|
+
candidates.push({ file, pid, writtenAt: statSync4(file).mtimeMs });
|
|
31864
32329
|
} catch {}
|
|
31865
32330
|
}
|
|
31866
32331
|
}
|
|
@@ -31882,16 +32347,16 @@ function announcedIn(directory) {
|
|
|
31882
32347
|
found.push(announced);
|
|
31883
32348
|
}
|
|
31884
32349
|
const held = new Set(entries.map((one) => one.file));
|
|
31885
|
-
const here =
|
|
32350
|
+
const here = join11(directory, ".");
|
|
31886
32351
|
for (const file of verdicts.keys()) {
|
|
31887
|
-
if (
|
|
32352
|
+
if (dirname7(file) === here && !held.has(file)) {
|
|
31888
32353
|
verdicts.delete(file);
|
|
31889
32354
|
}
|
|
31890
32355
|
}
|
|
31891
32356
|
return found;
|
|
31892
32357
|
}
|
|
31893
32358
|
async function announcementEnded(pid, directories = runtimeDirectories()) {
|
|
31894
|
-
const files = directories.map((directory) =>
|
|
32359
|
+
const files = directories.map((directory) => join11(directory, `runtime-${pid}.json`)).filter((file) => existsSync9(file));
|
|
31895
32360
|
const first = files[0];
|
|
31896
32361
|
if (first === undefined) {
|
|
31897
32362
|
return;
|
|
@@ -31917,7 +32382,7 @@ function sweepErrorReport(path, pid, now = Date.now()) {
|
|
|
31917
32382
|
return;
|
|
31918
32383
|
}
|
|
31919
32384
|
try {
|
|
31920
|
-
if (now -
|
|
32385
|
+
if (now - statSync4(path).mtimeMs >= ERROR_REPORT_KEEP_MS) {
|
|
31921
32386
|
unlinkSync(path);
|
|
31922
32387
|
}
|
|
31923
32388
|
} catch {}
|
|
@@ -32190,14 +32655,14 @@ var init_runtime_client = __esm(() => {
|
|
|
32190
32655
|
});
|
|
32191
32656
|
|
|
32192
32657
|
// src/boot-note.ts
|
|
32193
|
-
import { mkdirSync as
|
|
32194
|
-
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";
|
|
32195
32660
|
function bootNotePath(projectPath) {
|
|
32196
|
-
return
|
|
32661
|
+
return join12(projectPath, ".godot", "gdharness-boot.json");
|
|
32197
32662
|
}
|
|
32198
32663
|
function readBootNote(projectPath) {
|
|
32199
32664
|
try {
|
|
32200
|
-
const noted = readNumber(asParams(JSON.parse(
|
|
32665
|
+
const noted = readNumber(asParams(JSON.parse(readFileSync10(bootNotePath(projectPath), "utf8"))), "announcedAfterMs");
|
|
32201
32666
|
return noted !== undefined && Number.isFinite(noted) && noted > 0 ? noted : null;
|
|
32202
32667
|
} catch {
|
|
32203
32668
|
return null;
|
|
@@ -32206,8 +32671,8 @@ function readBootNote(projectPath) {
|
|
|
32206
32671
|
function writeBootNote(projectPath, announcedAfterMs) {
|
|
32207
32672
|
try {
|
|
32208
32673
|
const path = bootNotePath(projectPath);
|
|
32209
|
-
|
|
32210
|
-
|
|
32674
|
+
mkdirSync6(dirname8(path), { recursive: true });
|
|
32675
|
+
writeFileSync7(path, JSON.stringify({ announcedAfterMs: Math.round(announcedAfterMs), at: new Date().toISOString() }, null, 2), "utf8");
|
|
32211
32676
|
} catch {}
|
|
32212
32677
|
}
|
|
32213
32678
|
function halfAsLongAgain(lastBootMs) {
|
|
@@ -32225,10 +32690,10 @@ var init_boot_note = __esm(() => {
|
|
|
32225
32690
|
});
|
|
32226
32691
|
|
|
32227
32692
|
// src/breakpoint-note.ts
|
|
32228
|
-
import { mkdirSync as
|
|
32229
|
-
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";
|
|
32230
32695
|
function breakpointNotePath(projectPath) {
|
|
32231
|
-
return
|
|
32696
|
+
return join13(projectPath, ".godot", "gdharness-breakpoints.json");
|
|
32232
32697
|
}
|
|
32233
32698
|
function writeBreakpointNote(projectPath, held) {
|
|
32234
32699
|
const path = breakpointNotePath(projectPath);
|
|
@@ -32236,7 +32701,7 @@ function writeBreakpointNote(projectPath, held) {
|
|
|
32236
32701
|
const inside = held.map((one) => ({ scriptPath: realPathOr(one.scriptPath), lines: one.lines })).filter((one) => one.lines.length > 0 && withinProject(root, one.scriptPath));
|
|
32237
32702
|
try {
|
|
32238
32703
|
if (inside.length === 0) {
|
|
32239
|
-
|
|
32704
|
+
rmSync6(path, { force: true });
|
|
32240
32705
|
return;
|
|
32241
32706
|
}
|
|
32242
32707
|
const note = {
|
|
@@ -32245,8 +32710,8 @@ function writeBreakpointNote(projectPath, held) {
|
|
|
32245
32710
|
lines: [...one.lines]
|
|
32246
32711
|
}))
|
|
32247
32712
|
};
|
|
32248
|
-
|
|
32249
|
-
|
|
32713
|
+
mkdirSync7(dirname9(path), { recursive: true });
|
|
32714
|
+
writeFileSync8(path, `${JSON.stringify(note, null, 2)}
|
|
32250
32715
|
`, "utf8");
|
|
32251
32716
|
} catch (error) {
|
|
32252
32717
|
console.error(`[SERVER] Could not record the breakpoints at ${path}: ${errorMessage(error)}`);
|
|
@@ -32254,7 +32719,7 @@ function writeBreakpointNote(projectPath, held) {
|
|
|
32254
32719
|
}
|
|
32255
32720
|
function readBreakpointNote(projectPath) {
|
|
32256
32721
|
try {
|
|
32257
|
-
const parsed = JSON.parse(
|
|
32722
|
+
const parsed = JSON.parse(readFileSync11(breakpointNotePath(projectPath), "utf8"));
|
|
32258
32723
|
if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.breakpoints)) {
|
|
32259
32724
|
return [];
|
|
32260
32725
|
}
|
|
@@ -32269,7 +32734,7 @@ function readBreakpointNote(projectPath) {
|
|
|
32269
32734
|
}
|
|
32270
32735
|
const lines = Array.isArray(listed) ? listed.filter((line) => Number.isInteger(line) && line > 0) : [];
|
|
32271
32736
|
if (lines.length > 0) {
|
|
32272
|
-
held.push({ scriptPath: realPathOr(
|
|
32737
|
+
held.push({ scriptPath: realPathOr(join13(projectPath, script)), lines });
|
|
32273
32738
|
}
|
|
32274
32739
|
}
|
|
32275
32740
|
return held;
|
|
@@ -32286,75 +32751,15 @@ var init_breakpoint_note = __esm(() => {
|
|
|
32286
32751
|
init_paths();
|
|
32287
32752
|
});
|
|
32288
32753
|
|
|
32289
|
-
// src/bridge-announce.ts
|
|
32290
|
-
import { mkdirSync as mkdirSync9, readFileSync as readFileSync14, rmSync as rmSync8, writeFileSync as writeFileSync11 } from "node:fs";
|
|
32291
|
-
import { dirname as dirname11, join as join16 } from "node:path";
|
|
32292
|
-
function announcementPath2(projectPath) {
|
|
32293
|
-
return join16(projectPath, ".godot", "gdharness-bridge.json");
|
|
32294
|
-
}
|
|
32295
|
-
function readAnnouncement2(path) {
|
|
32296
|
-
try {
|
|
32297
|
-
const parsed = JSON.parse(readFileSync14(path, "utf8"));
|
|
32298
|
-
if (typeof parsed !== "object" || parsed === null) {
|
|
32299
|
-
return null;
|
|
32300
|
-
}
|
|
32301
|
-
const said = parsed;
|
|
32302
|
-
if (typeof said.port !== "number" || typeof said.pid !== "number") {
|
|
32303
|
-
return null;
|
|
32304
|
-
}
|
|
32305
|
-
return said;
|
|
32306
|
-
} catch {
|
|
32307
|
-
return null;
|
|
32308
|
-
}
|
|
32309
|
-
}
|
|
32310
|
-
function announcementIsOurs(path) {
|
|
32311
|
-
return readAnnouncement2(path)?.pid === process.pid;
|
|
32312
|
-
}
|
|
32313
|
-
function announceBridge(projectPath, bridge) {
|
|
32314
|
-
const path = announcementPath2(projectPath);
|
|
32315
|
-
try {
|
|
32316
|
-
mkdirSync9(dirname11(path), { recursive: true });
|
|
32317
|
-
const announcement = {
|
|
32318
|
-
protocol: BRIDGE_ANNOUNCE_PROTOCOL,
|
|
32319
|
-
host: bridge.host,
|
|
32320
|
-
port: bridge.port,
|
|
32321
|
-
pid: process.pid,
|
|
32322
|
-
version: bridge.version,
|
|
32323
|
-
startedAt: new Date().toISOString()
|
|
32324
|
-
};
|
|
32325
|
-
writeFileSync11(path, `${JSON.stringify(announcement, null, 2)}
|
|
32326
|
-
`, "utf8");
|
|
32327
|
-
return path;
|
|
32328
|
-
} catch (error) {
|
|
32329
|
-
console.error(`[SERVER] Could not announce the editor bridge at ${path}: ${errorMessage(error)}`);
|
|
32330
|
-
return null;
|
|
32331
|
-
}
|
|
32332
|
-
}
|
|
32333
|
-
function withdrawBridge(path) {
|
|
32334
|
-
if (path === null || !announcementIsOurs(path)) {
|
|
32335
|
-
return;
|
|
32336
|
-
}
|
|
32337
|
-
try {
|
|
32338
|
-
rmSync8(path, { force: true });
|
|
32339
|
-
} catch (error) {
|
|
32340
|
-
console.error(`[SERVER] Could not withdraw the editor bridge announcement: ${errorMessage(error)}`);
|
|
32341
|
-
}
|
|
32342
|
-
}
|
|
32343
|
-
var BRIDGE_ANNOUNCE_PROTOCOL = 1;
|
|
32344
|
-
var init_bridge_announce = __esm(() => {
|
|
32345
|
-
init_errors();
|
|
32346
|
-
init_process_children();
|
|
32347
|
-
});
|
|
32348
|
-
|
|
32349
32754
|
// src/class-note.ts
|
|
32350
|
-
import { mkdirSync as
|
|
32351
|
-
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";
|
|
32352
32757
|
function classNotePath(projectPath) {
|
|
32353
|
-
return
|
|
32758
|
+
return join14(projectPath, ".godot", "gdharness-classes.json");
|
|
32354
32759
|
}
|
|
32355
32760
|
function readClassNote(projectPath) {
|
|
32356
32761
|
try {
|
|
32357
|
-
const fields = asParams(JSON.parse(
|
|
32762
|
+
const fields = asParams(JSON.parse(readFileSync12(classNotePath(projectPath), "utf8")));
|
|
32358
32763
|
const rebuiltWith = readArray(fields, "rebuiltWith");
|
|
32359
32764
|
if (rebuiltWith === undefined || !rebuiltWith.every((name) => typeof name === "string")) {
|
|
32360
32765
|
return null;
|
|
@@ -32371,24 +32776,24 @@ function readClassNote(projectPath) {
|
|
|
32371
32776
|
function writeClassNote(projectPath, note) {
|
|
32372
32777
|
try {
|
|
32373
32778
|
const path = classNotePath(projectPath);
|
|
32374
|
-
|
|
32375
|
-
|
|
32779
|
+
mkdirSync8(dirname10(path), { recursive: true });
|
|
32780
|
+
writeFileSync9(path, `${JSON.stringify(note, null, 2)}
|
|
32376
32781
|
`, "utf8");
|
|
32377
32782
|
} catch {}
|
|
32378
32783
|
}
|
|
32379
32784
|
var init_class_note = () => {};
|
|
32380
32785
|
|
|
32381
32786
|
// src/config-pin.ts
|
|
32382
|
-
import { existsSync as
|
|
32383
|
-
import { join as
|
|
32787
|
+
import { existsSync as existsSync10, readFileSync as readFileSync13 } from "node:fs";
|
|
32788
|
+
import { join as join15 } from "node:path";
|
|
32384
32789
|
function versionsNamedInConfig(projectPath) {
|
|
32385
|
-
const config =
|
|
32386
|
-
if (!
|
|
32790
|
+
const config = join15(projectPath, ".mcp.json");
|
|
32791
|
+
if (!existsSync10(config)) {
|
|
32387
32792
|
return [];
|
|
32388
32793
|
}
|
|
32389
32794
|
let text;
|
|
32390
32795
|
try {
|
|
32391
|
-
text =
|
|
32796
|
+
text = readFileSync13(config, "utf8");
|
|
32392
32797
|
JSON.parse(text);
|
|
32393
32798
|
} catch {
|
|
32394
32799
|
return [];
|
|
@@ -33145,25 +33550,25 @@ var init_desktop = () => {};
|
|
|
33145
33550
|
// src/editor-log.ts
|
|
33146
33551
|
import {
|
|
33147
33552
|
closeSync,
|
|
33148
|
-
existsSync as
|
|
33149
|
-
mkdirSync as
|
|
33553
|
+
existsSync as existsSync11,
|
|
33554
|
+
mkdirSync as mkdirSync9,
|
|
33150
33555
|
openSync,
|
|
33151
|
-
readFileSync as
|
|
33556
|
+
readFileSync as readFileSync14,
|
|
33152
33557
|
readSync,
|
|
33153
|
-
rmSync as
|
|
33154
|
-
statSync as
|
|
33155
|
-
writeFileSync as
|
|
33558
|
+
rmSync as rmSync7,
|
|
33559
|
+
statSync as statSync5,
|
|
33560
|
+
writeFileSync as writeFileSync10
|
|
33156
33561
|
} from "node:fs";
|
|
33157
|
-
import { dirname as
|
|
33562
|
+
import { dirname as dirname11, join as join16 } from "node:path";
|
|
33158
33563
|
function editorLogPath(projectPath) {
|
|
33159
|
-
return
|
|
33564
|
+
return join16(projectPath, ".godot", "gdharness-editor.log");
|
|
33160
33565
|
}
|
|
33161
33566
|
function editorLogNotePath(projectPath) {
|
|
33162
|
-
return
|
|
33567
|
+
return join16(projectPath, ".godot", "gdharness-editor.json");
|
|
33163
33568
|
}
|
|
33164
33569
|
function readEditorLogNote(projectPath) {
|
|
33165
33570
|
try {
|
|
33166
|
-
const fields = asParams(JSON.parse(
|
|
33571
|
+
const fields = asParams(JSON.parse(readFileSync14(editorLogNotePath(projectPath), "utf8")));
|
|
33167
33572
|
const pid = readNumber(fields, "pid");
|
|
33168
33573
|
const startedAt = readString(fields, "startedAt");
|
|
33169
33574
|
return pid !== undefined && Number.isInteger(pid) && pid > 0 && startedAt !== undefined ? { pid, startedAt } : null;
|
|
@@ -33174,16 +33579,16 @@ function readEditorLogNote(projectPath) {
|
|
|
33174
33579
|
function clearEditorLog(projectPath) {
|
|
33175
33580
|
try {
|
|
33176
33581
|
const path = editorLogPath(projectPath);
|
|
33177
|
-
|
|
33178
|
-
|
|
33179
|
-
|
|
33582
|
+
mkdirSync9(dirname11(path), { recursive: true });
|
|
33583
|
+
rmSync7(path, { force: true });
|
|
33584
|
+
rmSync7(editorLogNotePath(projectPath), { force: true });
|
|
33180
33585
|
} catch {}
|
|
33181
33586
|
}
|
|
33182
33587
|
function writeEditorLogNote(projectPath, pid) {
|
|
33183
33588
|
try {
|
|
33184
33589
|
const path = editorLogNotePath(projectPath);
|
|
33185
|
-
|
|
33186
|
-
|
|
33590
|
+
mkdirSync9(dirname11(path), { recursive: true });
|
|
33591
|
+
writeFileSync10(path, JSON.stringify({ pid, startedAt: new Date().toISOString() }, null, 2), "utf8");
|
|
33187
33592
|
} catch {}
|
|
33188
33593
|
}
|
|
33189
33594
|
function theConsoleWasNotCaptured(why, projectPath) {
|
|
@@ -33204,7 +33609,7 @@ function editorConsole(projectPath, editorPid, openedByAServer) {
|
|
|
33204
33609
|
return { kind: "older server", notedPid: noted?.pid ?? null };
|
|
33205
33610
|
}
|
|
33206
33611
|
const path = editorLogPath(projectPath);
|
|
33207
|
-
if (!
|
|
33612
|
+
if (!existsSync11(path)) {
|
|
33208
33613
|
return { kind: "not written" };
|
|
33209
33614
|
}
|
|
33210
33615
|
const log = new GameLog;
|
|
@@ -33214,9 +33619,9 @@ function editorConsole(projectPath, editorPid, openedByAServer) {
|
|
|
33214
33619
|
return { log, path, ...of === undefined ? {} : { readBytes: text.length, of } };
|
|
33215
33620
|
}
|
|
33216
33621
|
function theFirstOf(path) {
|
|
33217
|
-
const whole =
|
|
33622
|
+
const whole = statSync5(path).size;
|
|
33218
33623
|
if (whole <= MOST_CONSOLE_BYTES) {
|
|
33219
|
-
return { text:
|
|
33624
|
+
return { text: readFileSync14(path, "utf8") };
|
|
33220
33625
|
}
|
|
33221
33626
|
const handle = openSync(path, "r");
|
|
33222
33627
|
try {
|
|
@@ -33258,7 +33663,7 @@ function shapeOf(text) {
|
|
|
33258
33663
|
}
|
|
33259
33664
|
return { shape, values };
|
|
33260
33665
|
}
|
|
33261
|
-
function bursts(entries, before) {
|
|
33666
|
+
function bursts(entries, before, keep) {
|
|
33262
33667
|
const shapes = new Map;
|
|
33263
33668
|
const shaped = entries.map((entry) => shapeOf(entry.text));
|
|
33264
33669
|
for (const [at, entry] of entries.entries()) {
|
|
@@ -33286,6 +33691,9 @@ function bursts(entries, before) {
|
|
|
33286
33691
|
if (entry === undefined) {
|
|
33287
33692
|
continue;
|
|
33288
33693
|
}
|
|
33694
|
+
if (keep !== undefined && !group.indices.some((at) => entries[at] !== undefined && keep(entries[at]))) {
|
|
33695
|
+
continue;
|
|
33696
|
+
}
|
|
33289
33697
|
repeated.push({
|
|
33290
33698
|
shape: key.slice(key.indexOf("\x00") + 1),
|
|
33291
33699
|
count: group.indices.length,
|
|
@@ -36755,208 +37163,6 @@ var init_godot_bridge = __esm(() => {
|
|
|
36755
37163
|
};
|
|
36756
37164
|
});
|
|
36757
37165
|
|
|
36758
|
-
// src/godot-path.ts
|
|
36759
|
-
import { execFile as execFile5 } from "node:child_process";
|
|
36760
|
-
import { existsSync as existsSync13 } from "node:fs";
|
|
36761
|
-
import { normalize as normalize2 } from "node:path";
|
|
36762
|
-
import { promisify as promisify5 } from "node:util";
|
|
36763
|
-
|
|
36764
|
-
class GodotLocator2 {
|
|
36765
|
-
answered = new Map;
|
|
36766
|
-
found = null;
|
|
36767
|
-
async find() {
|
|
36768
|
-
if (this.found !== null && await this.whyNot(this.found) === null) {
|
|
36769
|
-
return { ok: true, path: this.found };
|
|
36770
|
-
}
|
|
36771
|
-
this.found = null;
|
|
36772
|
-
const named = envValue("GODOT_PATH");
|
|
36773
|
-
if (named !== undefined) {
|
|
36774
|
-
const path = normalize2(named);
|
|
36775
|
-
const reason = await this.whyNot(path);
|
|
36776
|
-
if (reason === null) {
|
|
36777
|
-
this.found = path;
|
|
36778
|
-
return { ok: true, path };
|
|
36779
|
-
}
|
|
36780
|
-
return { ok: false, named: path, reason };
|
|
36781
|
-
}
|
|
36782
|
-
for (const candidate of godotCandidates(process.platform, resolveHomeDirectory())) {
|
|
36783
|
-
const path = normalize2(candidate);
|
|
36784
|
-
if (await this.whyNot(path) === null) {
|
|
36785
|
-
this.found = path;
|
|
36786
|
-
return { ok: true, path };
|
|
36787
|
-
}
|
|
36788
|
-
}
|
|
36789
|
-
return { ok: false, named: null };
|
|
36790
|
-
}
|
|
36791
|
-
static refusal(located) {
|
|
36792
|
-
if (located.named === null) {
|
|
36793
|
-
return {
|
|
36794
|
-
message: "No Godot executable found.",
|
|
36795
|
-
advice: [
|
|
36796
|
-
"Set GODOT_PATH to the Godot 4 executable",
|
|
36797
|
-
"Or install Godot where it is usually found, such as /usr/bin/godot or C:\\Program Files\\Godot"
|
|
36798
|
-
]
|
|
36799
|
-
};
|
|
36800
|
-
}
|
|
36801
|
-
return {
|
|
36802
|
-
message: `GODOT_PATH is set to ${located.named}, which ${located.reason ?? "does not answer"}.`,
|
|
36803
|
-
advice: [
|
|
36804
|
-
"Point GODOT_PATH at a Godot 4 executable that answers --version",
|
|
36805
|
-
"Or unset it, and the usual install locations are searched instead"
|
|
36806
|
-
]
|
|
36807
|
-
};
|
|
36808
|
-
}
|
|
36809
|
-
async whyNot(path) {
|
|
36810
|
-
const known = this.answered.get(path);
|
|
36811
|
-
if (known !== undefined) {
|
|
36812
|
-
return known;
|
|
36813
|
-
}
|
|
36814
|
-
let reason;
|
|
36815
|
-
if (path !== "godot" && !existsSync13(path)) {
|
|
36816
|
-
reason = "does not exist";
|
|
36817
|
-
} else {
|
|
36818
|
-
try {
|
|
36819
|
-
await run5(path, ["--version"]);
|
|
36820
|
-
reason = null;
|
|
36821
|
-
} catch (error) {
|
|
36822
|
-
reason = `does not answer --version: ${errorMessage(error)}`;
|
|
36823
|
-
}
|
|
36824
|
-
}
|
|
36825
|
-
this.answered.set(path, reason);
|
|
36826
|
-
return reason;
|
|
36827
|
-
}
|
|
36828
|
-
}
|
|
36829
|
-
var run5;
|
|
36830
|
-
var init_godot_path = __esm(() => {
|
|
36831
|
-
init_detection();
|
|
36832
|
-
init_errors();
|
|
36833
|
-
run5 = promisify5(execFile5);
|
|
36834
|
-
});
|
|
36835
|
-
|
|
36836
|
-
// src/issues.ts
|
|
36837
|
-
import { createHash as createHash4 } from "node:crypto";
|
|
36838
|
-
import process7 from "node:process";
|
|
36839
|
-
function feedbackNotice() {
|
|
36840
|
-
return JSON.stringify({
|
|
36841
|
-
gdharness_feedback: {
|
|
36842
|
-
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.",
|
|
36843
|
-
enhancement_url: ENHANCEMENT_URL2
|
|
36844
|
-
}
|
|
36845
|
-
}, null, 2);
|
|
36846
|
-
}
|
|
36847
|
-
function runtime2() {
|
|
36848
|
-
const versions = process7.versions;
|
|
36849
|
-
const bun = versions["bun"];
|
|
36850
|
-
return bun === undefined ? `node ${process7.versions.node}` : `bun ${bun}`;
|
|
36851
|
-
}
|
|
36852
|
-
function defectSignature2(where, message) {
|
|
36853
|
-
const normalised = `${where}
|
|
36854
|
-
${message}`.toLowerCase().replaceAll("\\", "/").replaceAll(/(?:[a-z]:)?(?:\/+[\w.-]+){2,}/gu, "<path>").replaceAll(/\d+/gu, "0");
|
|
36855
|
-
return createHash4("sha256").update(normalised).digest("hex").slice(0, 8);
|
|
36856
|
-
}
|
|
36857
|
-
function facts2(where, message, godotVersion) {
|
|
36858
|
-
return [
|
|
36859
|
-
["Where", where],
|
|
36860
|
-
["Error", message],
|
|
36861
|
-
["Version", `gdharness ${SERVER_VERSION}`],
|
|
36862
|
-
["Runtime", `${runtime2()}, ${process7.platform} ${process7.arch}`],
|
|
36863
|
-
["Godot", godotVersion ?? "not known at the point this failed"],
|
|
36864
|
-
["Signature", defectSignature2(where, message)]
|
|
36865
|
-
];
|
|
36866
|
-
}
|
|
36867
|
-
function filledTemplate2(where, message, godotVersion) {
|
|
36868
|
-
return [
|
|
36869
|
-
`**gdharness version**: ${SERVER_VERSION}`,
|
|
36870
|
-
`**Godot version**: ${godotVersion ?? ""}`,
|
|
36871
|
-
`**Bun version**: ${runtime2()}`,
|
|
36872
|
-
`**OS**: ${process7.platform} ${process7.arch}`,
|
|
36873
|
-
"**MCP client**:",
|
|
36874
|
-
"",
|
|
36875
|
-
"## What you did",
|
|
36876
|
-
"",
|
|
36877
|
-
where,
|
|
36878
|
-
"",
|
|
36879
|
-
"## What you expected",
|
|
36880
|
-
"",
|
|
36881
|
-
"The call to answer, or to refuse with a reason.",
|
|
36882
|
-
"",
|
|
36883
|
-
"## What happened",
|
|
36884
|
-
"",
|
|
36885
|
-
"gdharness failed in a way it does not model.",
|
|
36886
|
-
"",
|
|
36887
|
-
"```text",
|
|
36888
|
-
message,
|
|
36889
|
-
"```",
|
|
36890
|
-
"",
|
|
36891
|
-
`Signature: \`${defectSignature2(where, message)}\``,
|
|
36892
|
-
"",
|
|
36893
|
-
"If the real cause turned out to be the project, the environment or the call, say so here:",
|
|
36894
|
-
"reaching you as a defect rather than as a refusal naming what would have worked is then the",
|
|
36895
|
-
"thing to fix.",
|
|
36896
|
-
"",
|
|
36897
|
-
"## Anything that makes it reproducible",
|
|
36898
|
-
""
|
|
36899
|
-
].join(`
|
|
36900
|
-
`);
|
|
36901
|
-
}
|
|
36902
|
-
|
|
36903
|
-
class DefectsSeen {
|
|
36904
|
-
counts = new Map;
|
|
36905
|
-
before(where, message) {
|
|
36906
|
-
const signature = defectSignature2(where, message);
|
|
36907
|
-
const seen = this.counts.get(signature) ?? 0;
|
|
36908
|
-
this.counts.set(signature, seen + 1);
|
|
36909
|
-
return seen;
|
|
36910
|
-
}
|
|
36911
|
-
}
|
|
36912
|
-
function defectReport2(where, error, context = {}) {
|
|
36913
|
-
const { godotVersion, seenBefore = 0 } = context;
|
|
36914
|
-
const message = errorMessage(error);
|
|
36915
|
-
const title = `Defect ${defectSignature2(where, message)}: ${where}`;
|
|
36916
|
-
const url = `${NEW_ISSUE2}?template=bug_report.md&title=${encodeURIComponent(title)}` + `&body=${encodeURIComponent(filledTemplate2(where, message, godotVersion))}`;
|
|
36917
|
-
const width = Math.max(...facts2(where, message, godotVersion).map(([label]) => (label ?? "").length));
|
|
36918
|
-
return [
|
|
36919
|
-
"gdharness failed in a way it does not model. Whatever went wrong underneath, it reached you",
|
|
36920
|
-
"as a crash rather than as a refusal naming what would have worked, and that much is a defect",
|
|
36921
|
-
"in gdharness however the rest of it turns out.",
|
|
36922
|
-
"",
|
|
36923
|
-
...seenBefore === 0 ? [
|
|
36924
|
-
"Send the call once more before anything else. Some of what reaches this line is a file",
|
|
36925
|
-
"another process had open, or a connection that dropped, and those are gone by the next",
|
|
36926
|
-
"attempt. If the same Signature comes back, it is not one of those, and repeating it",
|
|
36927
|
-
"further only spends turns."
|
|
36928
|
-
] : [
|
|
36929
|
-
`You have had this exact failure ${seenBefore === 1 ? "once" : `${seenBefore} times`} in this session already, so it is a`,
|
|
36930
|
-
"state rather than a passing one, and sending the call again will not change it."
|
|
36931
|
-
],
|
|
36932
|
-
"",
|
|
36933
|
-
...facts2(where, message, godotVersion).map(([label, value]) => ` ${(label ?? "").padEnd(width)} ${value ?? ""}`),
|
|
36934
|
-
"",
|
|
36935
|
-
"Do not open an issue on your own initiative. Tell the person whose machine this is what",
|
|
36936
|
-
"broke, and ask whether you may report it to gdharness on their behalf. What the report",
|
|
36937
|
-
"carries is the lines above and nothing else, and whether that goes into a public tracker",
|
|
36938
|
-
"is theirs to decide.",
|
|
36939
|
-
"",
|
|
36940
|
-
"If they say yes, this link carries the report already filled in:",
|
|
36941
|
-
url,
|
|
36942
|
-
"",
|
|
36943
|
-
"If they say yes and you have no way to open an issue yourself, give them that link and",
|
|
36944
|
-
"those lines, and stay with them while they file it.",
|
|
36945
|
-
"",
|
|
36946
|
-
"Worth reporting either way. If you can see that what actually went wrong was the project,",
|
|
36947
|
-
"the environment or the call, then this message is the defect: a failure gdharness knows",
|
|
36948
|
-
"about is meant to arrive as a refusal naming what would have worked, not as this. Say that",
|
|
36949
|
-
"in the report and it is the more useful of the two."
|
|
36950
|
-
].join(`
|
|
36951
|
-
`);
|
|
36952
|
-
}
|
|
36953
|
-
var NEW_ISSUE2 = "https://github.com/Aureliolo/gdharness/issues/new", ENHANCEMENT_URL2;
|
|
36954
|
-
var init_issues = __esm(() => {
|
|
36955
|
-
init_errors();
|
|
36956
|
-
init_server_version();
|
|
36957
|
-
ENHANCEMENT_URL2 = `${NEW_ISSUE2}?template=feature_request.md`;
|
|
36958
|
-
});
|
|
36959
|
-
|
|
36960
37166
|
// src/junit.ts
|
|
36961
37167
|
function decoded(text) {
|
|
36962
37168
|
return text.replace(/&(#x[0-9a-fA-F]+|#[0-9]+|[a-zA-Z]+);/g, (whole, entity) => {
|
|
@@ -37148,7 +37354,7 @@ function scriptErrorsPrinted(printed) {
|
|
|
37148
37354
|
for (const line of printed.slice(start + 1)) {
|
|
37149
37355
|
const at = /^\s*at (res:\/\/.+):(\d+)\s*$/.exec(line);
|
|
37150
37356
|
if (at !== null && message !== null) {
|
|
37151
|
-
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 });
|
|
37152
37358
|
message = null;
|
|
37153
37359
|
} else if (at === null && message === null && /^ {2}\S/.test(line)) {
|
|
37154
37360
|
message = line.trim();
|
|
@@ -37156,7 +37362,7 @@ function scriptErrorsPrinted(printed) {
|
|
|
37156
37362
|
break;
|
|
37157
37363
|
}
|
|
37158
37364
|
}
|
|
37159
|
-
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));
|
|
37160
37366
|
}
|
|
37161
37367
|
function unspelled(text) {
|
|
37162
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);
|
|
@@ -37298,8 +37504,8 @@ var init_junit = __esm(() => {
|
|
|
37298
37504
|
import { realpathSync as realpathSync2 } from "node:fs";
|
|
37299
37505
|
import { readFile, realpath } from "node:fs/promises";
|
|
37300
37506
|
import { createConnection as createConnection3 } from "node:net";
|
|
37301
|
-
import { dirname as
|
|
37302
|
-
import { fileURLToPath as
|
|
37507
|
+
import { dirname as dirname12, resolve as resolve4 } from "node:path";
|
|
37508
|
+
import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
|
|
37303
37509
|
function diagnosticsKey(uri) {
|
|
37304
37510
|
let decoded;
|
|
37305
37511
|
try {
|
|
@@ -37311,7 +37517,7 @@ function diagnosticsKey(uri) {
|
|
|
37311
37517
|
return decoded;
|
|
37312
37518
|
}
|
|
37313
37519
|
try {
|
|
37314
|
-
return realpathSync2.native(
|
|
37520
|
+
return realpathSync2.native(fileURLToPath3(decoded)).toLowerCase();
|
|
37315
37521
|
} catch {
|
|
37316
37522
|
return decoded.toLowerCase();
|
|
37317
37523
|
}
|
|
@@ -37426,7 +37632,7 @@ class GodotLSPClient {
|
|
|
37426
37632
|
if (this.initialized) {
|
|
37427
37633
|
return;
|
|
37428
37634
|
}
|
|
37429
|
-
const rootPath = this.rootPath ??
|
|
37635
|
+
const rootPath = this.rootPath ?? dirname12(resolve4(filePath));
|
|
37430
37636
|
await this.initialize(rootPath);
|
|
37431
37637
|
}
|
|
37432
37638
|
async sendRequest(method, params) {
|
|
@@ -37833,7 +38039,7 @@ var init_lsp_client = __esm(() => {
|
|
|
37833
38039
|
|
|
37834
38040
|
// src/outside.ts
|
|
37835
38041
|
import { spawn } from "node:child_process";
|
|
37836
|
-
import { fileURLToPath as
|
|
38042
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
37837
38043
|
function changesFrom(env, base) {
|
|
37838
38044
|
const changes = {};
|
|
37839
38045
|
if (env === undefined) {
|
|
@@ -37896,12 +38102,12 @@ async function launchOutsideTheTree(spec, runtime = process.execPath, keeper = K
|
|
|
37896
38102
|
}
|
|
37897
38103
|
var KEEPER_SCRIPT;
|
|
37898
38104
|
var init_outside = __esm(() => {
|
|
37899
|
-
KEEPER_SCRIPT =
|
|
38105
|
+
KEEPER_SCRIPT = fileURLToPath4(new URL("./keeper.js", import.meta.url));
|
|
37900
38106
|
});
|
|
37901
38107
|
|
|
37902
38108
|
// src/process-time.ts
|
|
37903
|
-
import { execFile as
|
|
37904
|
-
import { promisify as
|
|
38109
|
+
import { execFile as execFile4 } from "node:child_process";
|
|
38110
|
+
import { promisify as promisify4 } from "node:util";
|
|
37905
38111
|
async function cpuSecondsOf(pid) {
|
|
37906
38112
|
try {
|
|
37907
38113
|
return process.platform === "win32" ? await askWindows2(pid) : await askPosix2(pid);
|
|
@@ -37910,12 +38116,12 @@ async function cpuSecondsOf(pid) {
|
|
|
37910
38116
|
}
|
|
37911
38117
|
}
|
|
37912
38118
|
async function askWindows2(pid) {
|
|
37913
|
-
const { stdout } = await
|
|
38119
|
+
const { stdout } = await run4("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", `(Get-Process -Id ${pid}).CPU`], { timeout: ASK_TIMEOUT_MS2, windowsHide: true });
|
|
37914
38120
|
const seconds = Number(stdout.trim().replace(",", "."));
|
|
37915
38121
|
return Number.isFinite(seconds) ? seconds : undefined;
|
|
37916
38122
|
}
|
|
37917
38123
|
async function askPosix2(pid) {
|
|
37918
|
-
const { stdout } = await
|
|
38124
|
+
const { stdout } = await run4("ps", ["-o", "time=", "-p", String(pid)], { timeout: ASK_TIMEOUT_MS2 });
|
|
37919
38125
|
return secondsFromClock(stdout.trim());
|
|
37920
38126
|
}
|
|
37921
38127
|
function secondsFromClock(printed) {
|
|
@@ -37933,19 +38139,19 @@ function secondsFromClock(printed) {
|
|
|
37933
38139
|
const extra = days === null ? 0 : Number(days) * 86400;
|
|
37934
38140
|
return Number.isFinite(extra) ? seconds + extra : undefined;
|
|
37935
38141
|
}
|
|
37936
|
-
var
|
|
38142
|
+
var run4, ASK_TIMEOUT_MS2 = 6000;
|
|
37937
38143
|
var init_process_time = __esm(() => {
|
|
37938
|
-
|
|
38144
|
+
run4 = promisify4(execFile4);
|
|
37939
38145
|
});
|
|
37940
38146
|
|
|
37941
38147
|
// src/project-scan.ts
|
|
37942
|
-
import { existsSync as
|
|
37943
|
-
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";
|
|
37944
38150
|
function skipped(name) {
|
|
37945
38151
|
return name.startsWith(".") || SKIPPED.has(name);
|
|
37946
38152
|
}
|
|
37947
38153
|
function steppedOver(directory) {
|
|
37948
|
-
return
|
|
38154
|
+
return existsSync12(join17(directory, ".gdignore"));
|
|
37949
38155
|
}
|
|
37950
38156
|
function projectStructure(projectPath) {
|
|
37951
38157
|
const structure = { scenes: 0, scripts: 0, assets: 0, other: 0 };
|
|
@@ -37953,12 +38159,12 @@ function projectStructure(projectPath) {
|
|
|
37953
38159
|
if (steppedOver(directory)) {
|
|
37954
38160
|
return;
|
|
37955
38161
|
}
|
|
37956
|
-
for (const entry of
|
|
38162
|
+
for (const entry of readdirSync6(directory, { withFileTypes: true })) {
|
|
37957
38163
|
if (skipped(entry.name)) {
|
|
37958
38164
|
continue;
|
|
37959
38165
|
}
|
|
37960
38166
|
if (entry.isDirectory()) {
|
|
37961
|
-
visit(
|
|
38167
|
+
visit(join17(directory, entry.name));
|
|
37962
38168
|
} else if (entry.isFile()) {
|
|
37963
38169
|
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
37964
38170
|
if (extension === "tscn") {
|
|
@@ -37982,20 +38188,20 @@ function scriptsWithoutUid(projectPath) {
|
|
|
37982
38188
|
if (steppedOver(directory)) {
|
|
37983
38189
|
return;
|
|
37984
38190
|
}
|
|
37985
|
-
for (const entry of
|
|
38191
|
+
for (const entry of readdirSync6(directory, { withFileTypes: true })) {
|
|
37986
38192
|
if (skipped(entry.name)) {
|
|
37987
38193
|
continue;
|
|
37988
38194
|
}
|
|
37989
38195
|
const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
37990
38196
|
if (entry.isDirectory()) {
|
|
37991
|
-
visit(
|
|
38197
|
+
visit(join17(directory, entry.name), spelled);
|
|
37992
38198
|
continue;
|
|
37993
38199
|
}
|
|
37994
38200
|
if (!entry.isFile()) {
|
|
37995
38201
|
continue;
|
|
37996
38202
|
}
|
|
37997
38203
|
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
37998
|
-
if (CARRIES_A_UID.has(extension) && !
|
|
38204
|
+
if (CARRIES_A_UID.has(extension) && !existsSync12(join17(directory, `${entry.name}.uid`))) {
|
|
37999
38205
|
found.push(spelled);
|
|
38000
38206
|
}
|
|
38001
38207
|
}
|
|
@@ -38023,14 +38229,14 @@ function searchProject(projectPath, options) {
|
|
|
38023
38229
|
if (steppedOver(directory)) {
|
|
38024
38230
|
return;
|
|
38025
38231
|
}
|
|
38026
|
-
for (const entry of
|
|
38232
|
+
for (const entry of readdirSync6(directory, { withFileTypes: true })) {
|
|
38027
38233
|
if (full()) {
|
|
38028
38234
|
return;
|
|
38029
38235
|
}
|
|
38030
38236
|
if (skipped(entry.name)) {
|
|
38031
38237
|
continue;
|
|
38032
38238
|
}
|
|
38033
|
-
const entryPath =
|
|
38239
|
+
const entryPath = join17(directory, entry.name);
|
|
38034
38240
|
if (entry.isDirectory()) {
|
|
38035
38241
|
visit(entryPath);
|
|
38036
38242
|
continue;
|
|
@@ -38041,7 +38247,7 @@ function searchProject(projectPath, options) {
|
|
|
38041
38247
|
}
|
|
38042
38248
|
result.summary.files_searched += 1;
|
|
38043
38249
|
const matches = [];
|
|
38044
|
-
for (const [index, line] of
|
|
38250
|
+
for (const [index, line] of readFileSync15(entryPath, "utf8").split(`
|
|
38045
38251
|
`).entries()) {
|
|
38046
38252
|
if (full()) {
|
|
38047
38253
|
break;
|
|
@@ -38070,37 +38276,37 @@ var init_project_scan = __esm(() => {
|
|
|
38070
38276
|
});
|
|
38071
38277
|
|
|
38072
38278
|
// src/reimport.ts
|
|
38073
|
-
import { createHash as
|
|
38074
|
-
import { existsSync as
|
|
38075
|
-
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";
|
|
38076
38282
|
function forceNextImport(projectPath, resourcePath) {
|
|
38077
38283
|
const relative = resourcePath.replace(/^res:\/\//, "");
|
|
38078
|
-
const sidecar =
|
|
38079
|
-
if (!
|
|
38284
|
+
const sidecar = join18(projectPath, `${relative}.import`);
|
|
38285
|
+
if (!existsSync13(sidecar)) {
|
|
38080
38286
|
return;
|
|
38081
38287
|
}
|
|
38082
|
-
const { atime, mtimeMs } =
|
|
38083
|
-
|
|
38288
|
+
const { atime, mtimeMs } = statSync6(sidecar);
|
|
38289
|
+
writeFileSync11(sidecar, readFileSync16(sidecar, "utf8").replace(/^valid=false\r?\n/m, ""));
|
|
38084
38290
|
utimesSync(sidecar, atime, new Date(mtimeMs + 2000));
|
|
38085
|
-
|
|
38291
|
+
rmSync8(importRecord(projectPath, resourcePath), { force: true });
|
|
38086
38292
|
}
|
|
38087
38293
|
function importRecord(projectPath, resourcePath) {
|
|
38088
|
-
const digest =
|
|
38089
|
-
return
|
|
38294
|
+
const digest = createHash3("md5").update(resourcePath).digest("hex");
|
|
38295
|
+
return join18(projectPath, ".godot", "imported", `${basename3(resourcePath)}-${digest}.md5`);
|
|
38090
38296
|
}
|
|
38091
38297
|
var init_reimport = () => {};
|
|
38092
38298
|
|
|
38093
38299
|
// src/restart-note.ts
|
|
38094
|
-
import { mkdirSync as
|
|
38095
|
-
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";
|
|
38096
38302
|
function restartNotePath(projectPath) {
|
|
38097
|
-
return
|
|
38303
|
+
return join19(projectPath, ".godot", "gdharness-restart.json");
|
|
38098
38304
|
}
|
|
38099
38305
|
function noteRestartBegun(note) {
|
|
38100
38306
|
const path = restartNotePath(note.projectPath);
|
|
38101
38307
|
try {
|
|
38102
|
-
|
|
38103
|
-
|
|
38308
|
+
mkdirSync10(dirname13(path), { recursive: true });
|
|
38309
|
+
writeFileSync12(path, `${JSON.stringify(note, null, 2)}
|
|
38104
38310
|
`, "utf8");
|
|
38105
38311
|
} catch (error) {
|
|
38106
38312
|
console.error(`[SERVER] Could not record the restart at ${path}: ${errorMessage(error)}`);
|
|
@@ -38108,7 +38314,7 @@ function noteRestartBegun(note) {
|
|
|
38108
38314
|
}
|
|
38109
38315
|
function restartOwed(projectPath) {
|
|
38110
38316
|
try {
|
|
38111
|
-
const parsed = JSON.parse(
|
|
38317
|
+
const parsed = JSON.parse(readFileSync17(restartNotePath(projectPath), "utf8"));
|
|
38112
38318
|
if (typeof parsed !== "object" || parsed === null) {
|
|
38113
38319
|
return null;
|
|
38114
38320
|
}
|
|
@@ -38123,7 +38329,7 @@ function restartOwed(projectPath) {
|
|
|
38123
38329
|
}
|
|
38124
38330
|
function restartSettled(projectPath) {
|
|
38125
38331
|
try {
|
|
38126
|
-
|
|
38332
|
+
rmSync9(restartNotePath(projectPath), { force: true });
|
|
38127
38333
|
} catch (error) {
|
|
38128
38334
|
console.error(`[SERVER] Could not clear the restart note: ${errorMessage(error)}`);
|
|
38129
38335
|
}
|
|
@@ -38134,29 +38340,29 @@ var init_restart_note = __esm(() => {
|
|
|
38134
38340
|
|
|
38135
38341
|
// src/run-record.ts
|
|
38136
38342
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
38137
|
-
import { createHash as
|
|
38343
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
38138
38344
|
import {
|
|
38139
|
-
mkdirSync as
|
|
38345
|
+
mkdirSync as mkdirSync11,
|
|
38140
38346
|
openSync as openSync2,
|
|
38141
|
-
readdirSync as
|
|
38142
|
-
readFileSync as
|
|
38347
|
+
readdirSync as readdirSync7,
|
|
38348
|
+
readFileSync as readFileSync18,
|
|
38143
38349
|
realpathSync as realpathSync3,
|
|
38144
|
-
rmSync as
|
|
38145
|
-
statSync as
|
|
38146
|
-
writeFileSync as
|
|
38350
|
+
rmSync as rmSync10,
|
|
38351
|
+
statSync as statSync7,
|
|
38352
|
+
writeFileSync as writeFileSync13
|
|
38147
38353
|
} from "node:fs";
|
|
38148
|
-
import { basename as basename4, dirname as
|
|
38354
|
+
import { basename as basename4, dirname as dirname14, join as join20, resolve as resolve5 } from "node:path";
|
|
38149
38355
|
function runsDirectory() {
|
|
38150
|
-
return
|
|
38356
|
+
return join20(runtimeDirectory(), "runs");
|
|
38151
38357
|
}
|
|
38152
38358
|
function realAsFarAsItExists(path) {
|
|
38153
38359
|
const missing = [];
|
|
38154
38360
|
let at = path;
|
|
38155
38361
|
for (;; ) {
|
|
38156
38362
|
try {
|
|
38157
|
-
return
|
|
38363
|
+
return join20(realpathSync3.native(at), ...missing);
|
|
38158
38364
|
} catch {
|
|
38159
|
-
const parent =
|
|
38365
|
+
const parent = dirname14(at);
|
|
38160
38366
|
if (parent === at) {
|
|
38161
38367
|
return path;
|
|
38162
38368
|
}
|
|
@@ -38168,26 +38374,26 @@ function realAsFarAsItExists(path) {
|
|
|
38168
38374
|
function projectKey(projectPath) {
|
|
38169
38375
|
const real = realAsFarAsItExists(resolve5(projectPath)).replace(/[\\/]+$/, "");
|
|
38170
38376
|
const folded = process.platform === "win32" || process.platform === "darwin" ? real.toLowerCase() : real;
|
|
38171
|
-
return
|
|
38377
|
+
return createHash4("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
|
|
38172
38378
|
}
|
|
38173
38379
|
function runRecordPath(projectPath, directory = runtimeDirectory()) {
|
|
38174
|
-
return
|
|
38380
|
+
return join20(directory, "runs", `note-${projectKey(projectPath)}.json`);
|
|
38175
38381
|
}
|
|
38176
38382
|
function openTranscript(startedAt) {
|
|
38177
|
-
|
|
38178
|
-
const path =
|
|
38383
|
+
mkdirSync11(runsDirectory(), { recursive: true });
|
|
38384
|
+
const path = join20(runsDirectory(), `run-${startedAt}.log`);
|
|
38179
38385
|
return { path, fd: openSync2(path, "a") };
|
|
38180
38386
|
}
|
|
38181
38387
|
function editorNotePath(projectPath, directory = runtimeDirectory()) {
|
|
38182
|
-
return
|
|
38388
|
+
return join20(directory, "runs", `editor-${projectKey(projectPath)}.json`);
|
|
38183
38389
|
}
|
|
38184
38390
|
function writeEditorRunNote(note) {
|
|
38185
|
-
|
|
38186
|
-
|
|
38391
|
+
mkdirSync11(runsDirectory(), { recursive: true });
|
|
38392
|
+
writeFileSync13(editorNotePath(note.projectPath), JSON.stringify(note, null, 2), "utf8");
|
|
38187
38393
|
}
|
|
38188
38394
|
function readEditorRunNote(projectPath) {
|
|
38189
38395
|
for (const directory of runtimeDirectories()) {
|
|
38190
|
-
const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(
|
|
38396
|
+
const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join20(directory, "runs", "editor-run.json"));
|
|
38191
38397
|
if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
|
|
38192
38398
|
return note;
|
|
38193
38399
|
}
|
|
@@ -38197,7 +38403,7 @@ function readEditorRunNote(projectPath) {
|
|
|
38197
38403
|
function editorNoteAt(path) {
|
|
38198
38404
|
let parsed;
|
|
38199
38405
|
try {
|
|
38200
|
-
parsed = JSON.parse(
|
|
38406
|
+
parsed = JSON.parse(readFileSync18(path, "utf8"));
|
|
38201
38407
|
} catch {
|
|
38202
38408
|
return null;
|
|
38203
38409
|
}
|
|
@@ -38219,7 +38425,7 @@ function readRunRecord(projectPath) {
|
|
|
38219
38425
|
if (own !== null) {
|
|
38220
38426
|
return own;
|
|
38221
38427
|
}
|
|
38222
|
-
const shared = recordAt(
|
|
38428
|
+
const shared = recordAt(join20(directory, "runs", SHARED_NOTE));
|
|
38223
38429
|
if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
|
|
38224
38430
|
return shared;
|
|
38225
38431
|
}
|
|
@@ -38231,13 +38437,13 @@ function everyRunRecord() {
|
|
|
38231
38437
|
for (const directory of runtimeDirectories()) {
|
|
38232
38438
|
let entries;
|
|
38233
38439
|
try {
|
|
38234
|
-
entries =
|
|
38440
|
+
entries = readdirSync7(join20(directory, "runs"));
|
|
38235
38441
|
} catch {
|
|
38236
38442
|
continue;
|
|
38237
38443
|
}
|
|
38238
38444
|
for (const entry of entries) {
|
|
38239
38445
|
if (entry === SHARED_NOTE || entry.startsWith("note-") && entry.endsWith(".json")) {
|
|
38240
|
-
const record = recordAt(
|
|
38446
|
+
const record = recordAt(join20(directory, "runs", entry));
|
|
38241
38447
|
if (record !== null) {
|
|
38242
38448
|
found.push(record);
|
|
38243
38449
|
}
|
|
@@ -38249,7 +38455,7 @@ function everyRunRecord() {
|
|
|
38249
38455
|
function recordAt(path) {
|
|
38250
38456
|
let parsed;
|
|
38251
38457
|
try {
|
|
38252
|
-
parsed = JSON.parse(
|
|
38458
|
+
parsed = JSON.parse(readFileSync18(path, "utf8"));
|
|
38253
38459
|
} catch {
|
|
38254
38460
|
return null;
|
|
38255
38461
|
}
|
|
@@ -38295,7 +38501,7 @@ function runningAs(pid) {
|
|
|
38295
38501
|
const began = startedAt(pid, null);
|
|
38296
38502
|
const when = began === null ? {} : { startedAt: began };
|
|
38297
38503
|
if (process.platform === "linux") {
|
|
38298
|
-
const raw =
|
|
38504
|
+
const raw = readFileSync18(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
|
|
38299
38505
|
return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
|
|
38300
38506
|
}
|
|
38301
38507
|
const args = execFileSync2("ps", ["-p", String(pid), "-o", "args="], {
|
|
@@ -38433,19 +38639,19 @@ function judgeAnnouncedGame(running, projectPath, announcedAt) {
|
|
|
38433
38639
|
}
|
|
38434
38640
|
function clearRunRecord(projectPath) {
|
|
38435
38641
|
for (const directory of runtimeDirectories()) {
|
|
38436
|
-
const shared =
|
|
38642
|
+
const shared = join20(directory, "runs", SHARED_NOTE);
|
|
38437
38643
|
const sharedRecord = recordAt(shared);
|
|
38438
38644
|
const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
|
|
38439
38645
|
for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
|
|
38440
38646
|
try {
|
|
38441
|
-
|
|
38647
|
+
rmSync10(path, { force: true });
|
|
38442
38648
|
} catch {}
|
|
38443
38649
|
}
|
|
38444
38650
|
}
|
|
38445
38651
|
}
|
|
38446
38652
|
function ageMs(path, now) {
|
|
38447
38653
|
try {
|
|
38448
|
-
return now -
|
|
38654
|
+
return now - statSync7(path).mtimeMs;
|
|
38449
38655
|
} catch {
|
|
38450
38656
|
return 0;
|
|
38451
38657
|
}
|
|
@@ -38455,7 +38661,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
38455
38661
|
const kept = new Set(everyRunRecord().map((record) => record.transcript));
|
|
38456
38662
|
let entries;
|
|
38457
38663
|
try {
|
|
38458
|
-
entries =
|
|
38664
|
+
entries = readdirSync7(directory);
|
|
38459
38665
|
} catch {
|
|
38460
38666
|
return;
|
|
38461
38667
|
}
|
|
@@ -38463,12 +38669,12 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
38463
38669
|
if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
|
|
38464
38670
|
continue;
|
|
38465
38671
|
}
|
|
38466
|
-
const path =
|
|
38672
|
+
const path = join20(directory, entry);
|
|
38467
38673
|
if (kept.has(path) || ageMs(path, now) < keepMs) {
|
|
38468
38674
|
continue;
|
|
38469
38675
|
}
|
|
38470
38676
|
try {
|
|
38471
|
-
|
|
38677
|
+
rmSync10(path);
|
|
38472
38678
|
} catch {}
|
|
38473
38679
|
}
|
|
38474
38680
|
}
|
|
@@ -38480,29 +38686,29 @@ var init_run_record = __esm(() => {
|
|
|
38480
38686
|
});
|
|
38481
38687
|
|
|
38482
38688
|
// src/server.ts
|
|
38483
|
-
import { execFile as
|
|
38689
|
+
import { execFile as execFile5, spawn as spawn2 } from "node:child_process";
|
|
38484
38690
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
38485
38691
|
import {
|
|
38486
38692
|
appendFileSync,
|
|
38487
38693
|
closeSync as closeSync2,
|
|
38488
38694
|
constants,
|
|
38489
38695
|
copyFileSync,
|
|
38490
|
-
existsSync as
|
|
38696
|
+
existsSync as existsSync14,
|
|
38491
38697
|
fstatSync,
|
|
38492
|
-
mkdirSync as
|
|
38493
|
-
mkdtempSync as
|
|
38698
|
+
mkdirSync as mkdirSync12,
|
|
38699
|
+
mkdtempSync as mkdtempSync2,
|
|
38494
38700
|
openSync as openSync3,
|
|
38495
|
-
readdirSync as
|
|
38496
|
-
readFileSync as
|
|
38701
|
+
readdirSync as readdirSync8,
|
|
38702
|
+
readFileSync as readFileSync19,
|
|
38497
38703
|
readSync as readSync2,
|
|
38498
|
-
rmdirSync,
|
|
38499
|
-
statSync as
|
|
38704
|
+
rmdirSync as rmdirSync2,
|
|
38705
|
+
statSync as statSync8
|
|
38500
38706
|
} from "node:fs";
|
|
38501
|
-
import { tmpdir as
|
|
38502
|
-
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";
|
|
38503
38709
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
38504
|
-
import { fileURLToPath as
|
|
38505
|
-
import { promisify as
|
|
38710
|
+
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
38711
|
+
import { promisify as promisify5 } from "node:util";
|
|
38506
38712
|
function captureDestinationRefusal(outputPath, projects) {
|
|
38507
38713
|
if (/^(res|user):\/\//.test(outputPath)) {
|
|
38508
38714
|
return `outputPath ${outputPath} is a Godot path; a capture is saved outside the project, at an absolute path on this machine.`;
|
|
@@ -38513,16 +38719,16 @@ function captureDestinationRefusal(outputPath, projects) {
|
|
|
38513
38719
|
if (extname2(outputPath).toLowerCase() !== ".png") {
|
|
38514
38720
|
return `outputPath ${outputPath} does not end in .png, which is what a capture is.`;
|
|
38515
38721
|
}
|
|
38516
|
-
const directory = realPathOr(
|
|
38517
|
-
if (!
|
|
38518
|
-
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.`;
|
|
38519
38725
|
}
|
|
38520
|
-
const where =
|
|
38726
|
+
const where = join21(directory, basename5(outputPath));
|
|
38521
38727
|
const inside = projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
|
|
38522
38728
|
if (inside !== undefined) {
|
|
38523
38729
|
return `outputPath ${outputPath} is inside the project at ${inside}; a capture is saved outside it, so it never becomes part of the game.`;
|
|
38524
38730
|
}
|
|
38525
|
-
if (
|
|
38731
|
+
if (existsSync14(where)) {
|
|
38526
38732
|
return `outputPath ${outputPath} already exists; a capture is not written over a file. Name a new one.`;
|
|
38527
38733
|
}
|
|
38528
38734
|
return null;
|
|
@@ -38887,15 +39093,24 @@ function changedSettings(before, after) {
|
|
|
38887
39093
|
return [...names].filter((name) => JSON.stringify(before.get(name)) !== JSON.stringify(after.get(name))).sort();
|
|
38888
39094
|
}
|
|
38889
39095
|
function scriptErrorsNote(errors) {
|
|
38890
|
-
const
|
|
39096
|
+
const causes = new Map;
|
|
38891
39097
|
for (const error of errors) {
|
|
38892
|
-
if (!
|
|
38893
|
-
|
|
39098
|
+
if (!error.message.includes(DEPENDED_FAILED) && !causes.has(error.path)) {
|
|
39099
|
+
causes.set(error.path, error);
|
|
38894
39100
|
}
|
|
38895
39101
|
}
|
|
38896
|
-
const
|
|
38897
|
-
const
|
|
38898
|
-
|
|
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(" ");
|
|
38899
39114
|
}
|
|
38900
39115
|
function timedOutVerdict(timeoutMs, silentForMs) {
|
|
38901
39116
|
const quiet = Math.round(silentForMs);
|
|
@@ -38952,13 +39167,13 @@ function aboveTheRunner(entry) {
|
|
|
38952
39167
|
}
|
|
38953
39168
|
function elsewhereIn(projectPath, asked) {
|
|
38954
39169
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
38955
|
-
if (wanted === "" ||
|
|
39170
|
+
if (wanted === "" || existsSync14(join21(projectPath, wanted))) {
|
|
38956
39171
|
return "";
|
|
38957
39172
|
}
|
|
38958
39173
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
38959
39174
|
let near = [];
|
|
38960
39175
|
try {
|
|
38961
|
-
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()));
|
|
38962
39177
|
} catch {
|
|
38963
39178
|
return "";
|
|
38964
39179
|
}
|
|
@@ -38966,7 +39181,7 @@ function elsewhereIn(projectPath, asked) {
|
|
|
38966
39181
|
return near.length === 0 ? "" : ` The project does have ${there}.`;
|
|
38967
39182
|
}
|
|
38968
39183
|
function hasMainScene(projectFile) {
|
|
38969
|
-
const scene = parseProjectGodot(
|
|
39184
|
+
const scene = parseProjectGodot(readFileSync19(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
38970
39185
|
return typeof scene === "string" && scene !== "";
|
|
38971
39186
|
}
|
|
38972
39187
|
function runtimeAddonAnnouncesEditors(projectPath) {
|
|
@@ -38974,13 +39189,13 @@ function runtimeAddonAnnouncesEditors(projectPath) {
|
|
|
38974
39189
|
return false;
|
|
38975
39190
|
}
|
|
38976
39191
|
try {
|
|
38977
|
-
return
|
|
39192
|
+
return readFileSync19(join21(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
|
|
38978
39193
|
} catch {
|
|
38979
39194
|
return false;
|
|
38980
39195
|
}
|
|
38981
39196
|
}
|
|
38982
39197
|
function editorPlaysHeadless(projectFile) {
|
|
38983
|
-
const runArgs = parseProjectGodot(
|
|
39198
|
+
const runArgs = parseProjectGodot(readFileSync19(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
38984
39199
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
38985
39200
|
}
|
|
38986
39201
|
function camelCased(params) {
|
|
@@ -38998,8 +39213,8 @@ function needsItsOwnProcess(asked) {
|
|
|
38998
39213
|
|
|
38999
39214
|
class GodotServer {
|
|
39000
39215
|
mcp;
|
|
39001
|
-
locator = new
|
|
39002
|
-
operationsScript =
|
|
39216
|
+
locator = new GodotLocator;
|
|
39217
|
+
operationsScript = join21(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
39003
39218
|
godotBridge;
|
|
39004
39219
|
tools = buildToolDefinitions();
|
|
39005
39220
|
activeProcess = null;
|
|
@@ -39058,7 +39273,7 @@ class GodotServer {
|
|
|
39058
39273
|
async run() {
|
|
39059
39274
|
const located = await this.locator.find();
|
|
39060
39275
|
if (!located.ok) {
|
|
39061
|
-
console.error(`[SERVER] ${
|
|
39276
|
+
console.error(`[SERVER] ${GodotLocator.refusal(located).message} Until that is fixed every tool that runs the engine will say so.`);
|
|
39062
39277
|
} else {
|
|
39063
39278
|
console.error(`[SERVER] Using Godot at: ${located.path}`);
|
|
39064
39279
|
}
|
|
@@ -39089,7 +39304,7 @@ class GodotServer {
|
|
|
39089
39304
|
return;
|
|
39090
39305
|
}
|
|
39091
39306
|
const wanted = this.godotBridge.configuredPort;
|
|
39092
|
-
const announced =
|
|
39307
|
+
const announced = readAnnouncement(announcementPath(this.ownProject));
|
|
39093
39308
|
if (announced === null || announced.pid === process.pid || announced.port !== wanted || !alive(announced.pid)) {
|
|
39094
39309
|
return;
|
|
39095
39310
|
}
|
|
@@ -39175,7 +39390,7 @@ class GodotServer {
|
|
|
39175
39390
|
if (this.ownProject === null) {
|
|
39176
39391
|
return null;
|
|
39177
39392
|
}
|
|
39178
|
-
const announced =
|
|
39393
|
+
const announced = readAnnouncement(announcementPath(this.ownProject));
|
|
39179
39394
|
if (announced === null || announced.pid === process.pid) {
|
|
39180
39395
|
return null;
|
|
39181
39396
|
}
|
|
@@ -39298,7 +39513,7 @@ class GodotServer {
|
|
|
39298
39513
|
content: [
|
|
39299
39514
|
{
|
|
39300
39515
|
type: "text",
|
|
39301
|
-
text:
|
|
39516
|
+
text: defectReport(where, error, {
|
|
39302
39517
|
seenBefore,
|
|
39303
39518
|
godotVersion: this.lastGodotVersion ?? undefined
|
|
39304
39519
|
})
|
|
@@ -39640,11 +39855,16 @@ class GodotServer {
|
|
|
39640
39855
|
return await this.handleRuntimeCommand(op === "screenshot" ? "capture_screenshot" : "capture_viewport", args);
|
|
39641
39856
|
case "runtime_input":
|
|
39642
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
|
+
}
|
|
39643
39861
|
return await this.handleRuntimeCommand("click", {
|
|
39644
39862
|
...whichGame(args),
|
|
39645
39863
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
39646
39864
|
button: readString(args, "button") ?? "left",
|
|
39647
|
-
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"] }
|
|
39648
39868
|
});
|
|
39649
39869
|
}
|
|
39650
39870
|
if (op === "choose") {
|
|
@@ -39703,7 +39923,7 @@ class GodotServer {
|
|
|
39703
39923
|
async engine() {
|
|
39704
39924
|
const located = await this.locator.find();
|
|
39705
39925
|
if (!located.ok) {
|
|
39706
|
-
const refusal =
|
|
39926
|
+
const refusal = GodotLocator.refusal(located);
|
|
39707
39927
|
return { ok: false, response: this.createErrorResponse(refusal.message, [...refusal.advice]) };
|
|
39708
39928
|
}
|
|
39709
39929
|
return { ok: true, value: located.path };
|
|
@@ -39713,8 +39933,8 @@ class GodotServer {
|
|
|
39713
39933
|
if (path === undefined) {
|
|
39714
39934
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
39715
39935
|
}
|
|
39716
|
-
const file =
|
|
39717
|
-
if (!
|
|
39936
|
+
const file = join21(path, "project.godot");
|
|
39937
|
+
if (!existsSync14(file)) {
|
|
39718
39938
|
return {
|
|
39719
39939
|
ok: false,
|
|
39720
39940
|
response: this.createErrorResponse(`Not a Godot project: ${path}`, [
|
|
@@ -40212,7 +40432,7 @@ class GodotServer {
|
|
|
40212
40432
|
}
|
|
40213
40433
|
const scanned = await this.untilTheEditorsScanIsWritten(projectPath);
|
|
40214
40434
|
this.logDebug(`Running ${operation} in ${projectPath}: ${JSON.stringify(params)}`);
|
|
40215
|
-
const outcome = await
|
|
40435
|
+
const outcome = await runOperation({ godotPath: engine.value, script: this.operationsScript, debug: GODOT_DEBUG_MODE_DEFAULT }, operation, params, projectPath);
|
|
40216
40436
|
return withScanWait(outcome, scanned);
|
|
40217
40437
|
}
|
|
40218
40438
|
answer(outcome) {
|
|
@@ -40242,7 +40462,7 @@ class GodotServer {
|
|
|
40242
40462
|
if (!engine.ok) {
|
|
40243
40463
|
return engine.response;
|
|
40244
40464
|
}
|
|
40245
|
-
const application = parseProjectGodot(
|
|
40465
|
+
const application = parseProjectGodot(readFileSync19(project.value.file, "utf8"))["application"] ?? {};
|
|
40246
40466
|
const name = application["config/name"];
|
|
40247
40467
|
const mainScene = application["run/main_scene"];
|
|
40248
40468
|
const info = {
|
|
@@ -40303,18 +40523,18 @@ class GodotServer {
|
|
|
40303
40523
|
if (!engine.ok) {
|
|
40304
40524
|
return engine.response;
|
|
40305
40525
|
}
|
|
40306
|
-
const folder =
|
|
40526
|
+
const folder = dirname15(output.absolutePath);
|
|
40307
40527
|
try {
|
|
40308
|
-
|
|
40528
|
+
mkdirSync12(folder, { recursive: true });
|
|
40309
40529
|
} catch (error) {
|
|
40310
|
-
return this.createErrorResponse(`Cannot create ${
|
|
40530
|
+
return this.createErrorResponse(`Cannot create ${dirname15(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
40311
40531
|
}
|
|
40312
40532
|
const debug = readBoolean(args, "debug") ?? false;
|
|
40313
|
-
const exportLogs =
|
|
40533
|
+
const exportLogs = mkdtempSync2(join21(tmpdir4(), "gdharness-export-"));
|
|
40314
40534
|
const exportArgs = [
|
|
40315
40535
|
"--headless",
|
|
40316
40536
|
"--log-file",
|
|
40317
|
-
|
|
40537
|
+
join21(exportLogs, "engine.log"),
|
|
40318
40538
|
"--path",
|
|
40319
40539
|
project.value.path,
|
|
40320
40540
|
debug ? "--export-debug" : "--export-release",
|
|
@@ -40325,7 +40545,7 @@ class GodotServer {
|
|
|
40325
40545
|
const log = new GameLog;
|
|
40326
40546
|
let ending = { exitCode: 0, exitSignal: null, failure: null };
|
|
40327
40547
|
try {
|
|
40328
|
-
const { stdout, stderr } = await
|
|
40548
|
+
const { stdout, stderr } = await run5(engine.value, exportArgs, {
|
|
40329
40549
|
timeout: 300000,
|
|
40330
40550
|
maxBuffer: 64 * 1024 * 1024,
|
|
40331
40551
|
signal: callSignal()
|
|
@@ -40346,7 +40566,7 @@ class GodotServer {
|
|
|
40346
40566
|
log.finish();
|
|
40347
40567
|
const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
|
|
40348
40568
|
const verdict = {
|
|
40349
|
-
exported: ending.exitCode === 0 && log.count("error") === 0 &&
|
|
40569
|
+
exported: ending.exitCode === 0 && log.count("error") === 0 && existsSync14(output.absolutePath),
|
|
40350
40570
|
preset,
|
|
40351
40571
|
outputPath: output.relativePath,
|
|
40352
40572
|
debug,
|
|
@@ -40382,7 +40602,7 @@ class GodotServer {
|
|
|
40382
40602
|
return contained.response;
|
|
40383
40603
|
}
|
|
40384
40604
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
40385
|
-
if (!
|
|
40605
|
+
if (!existsSync14(join21(project.value.path, runner))) {
|
|
40386
40606
|
return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
|
|
40387
40607
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
40388
40608
|
]);
|
|
@@ -40418,7 +40638,7 @@ class GodotServer {
|
|
|
40418
40638
|
];
|
|
40419
40639
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
|
|
40420
40640
|
this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
40421
|
-
const userData =
|
|
40641
|
+
const userData = mkdtempSync2(join21(tmpdir4(), "gdharness-tests-"));
|
|
40422
40642
|
const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
|
|
40423
40643
|
const console2 = [];
|
|
40424
40644
|
let lastSaid = Date.now();
|
|
@@ -40445,21 +40665,21 @@ class GodotServer {
|
|
|
40445
40665
|
resolve(false);
|
|
40446
40666
|
});
|
|
40447
40667
|
});
|
|
40448
|
-
const reportsDir =
|
|
40668
|
+
const reportsDir = join21(project.value.path, ".godot", ...ours.split("/"));
|
|
40449
40669
|
let report = null;
|
|
40450
40670
|
let reportProblem = null;
|
|
40451
40671
|
try {
|
|
40452
|
-
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))) : [];
|
|
40453
40673
|
const newest = written.at(-1);
|
|
40454
40674
|
if (newest !== undefined) {
|
|
40455
|
-
report = parseJUnit(
|
|
40675
|
+
report = parseJUnit(readFileSync19(join21(reportsDir, newest, "results.xml"), "utf8"));
|
|
40456
40676
|
}
|
|
40457
40677
|
} catch (error) {
|
|
40458
40678
|
reportProblem = errorMessage(error);
|
|
40459
40679
|
} finally {
|
|
40460
40680
|
discard(reportsDir, userData);
|
|
40461
40681
|
try {
|
|
40462
|
-
|
|
40682
|
+
rmdirSync2(dirname15(reportsDir));
|
|
40463
40683
|
} catch {}
|
|
40464
40684
|
}
|
|
40465
40685
|
const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries.map(aboveTheRunner);
|
|
@@ -40481,6 +40701,12 @@ class GodotServer {
|
|
|
40481
40701
|
const brokenScripts = new Set(scriptErrors.map((error) => error.path)).size;
|
|
40482
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));
|
|
40483
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
|
+
};
|
|
40484
40710
|
if (report === null) {
|
|
40485
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}` : ""}).`;
|
|
40486
40712
|
return {
|
|
@@ -40499,6 +40725,7 @@ class GodotServer {
|
|
|
40499
40725
|
...scriptErrors.length > 0 ? { scriptErrors } : {},
|
|
40500
40726
|
arguments: cmdArgs,
|
|
40501
40727
|
entries: forAnswer(printed.slice(0, 60).map(aboveTheRunner)),
|
|
40728
|
+
...printedLines,
|
|
40502
40729
|
savesNote,
|
|
40503
40730
|
...scanned
|
|
40504
40731
|
}, null, 2)
|
|
@@ -40535,6 +40762,7 @@ class GodotServer {
|
|
|
40535
40762
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
40536
40763
|
orphans: orphans.total > 0 ? orphans.total : undefined,
|
|
40537
40764
|
notRun: notRun > 0 ? notRun : undefined,
|
|
40765
|
+
...printedLines,
|
|
40538
40766
|
savesNote,
|
|
40539
40767
|
...scanned,
|
|
40540
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,
|
|
@@ -40558,7 +40786,7 @@ class GodotServer {
|
|
|
40558
40786
|
}
|
|
40559
40787
|
async godotVersion(godotPath) {
|
|
40560
40788
|
try {
|
|
40561
|
-
const { stdout } = await
|
|
40789
|
+
const { stdout } = await run5(godotPath, ["--version"], { timeout: 1e4 });
|
|
40562
40790
|
this.lastGodotVersion = stdout.trim();
|
|
40563
40791
|
return this.lastGodotVersion;
|
|
40564
40792
|
} catch (error) {
|
|
@@ -40602,11 +40830,11 @@ class GodotServer {
|
|
|
40602
40830
|
const cached = cachedClasses(projectPath);
|
|
40603
40831
|
const contradicted = cached === null ? [] : contradictedDiagnostics(messages, cached, (resourcePath) => {
|
|
40604
40832
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
40605
|
-
if (!contained.ok || !
|
|
40833
|
+
if (!contained.ok || !existsSync14(contained.absolutePath)) {
|
|
40606
40834
|
return null;
|
|
40607
40835
|
}
|
|
40608
40836
|
try {
|
|
40609
|
-
return
|
|
40837
|
+
return readFileSync19(contained.absolutePath, "utf8");
|
|
40610
40838
|
} catch {
|
|
40611
40839
|
return null;
|
|
40612
40840
|
}
|
|
@@ -40656,11 +40884,11 @@ class GodotServer {
|
|
|
40656
40884
|
}
|
|
40657
40885
|
sourceOfClass(projectPath, resourcePath) {
|
|
40658
40886
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
40659
|
-
if (!contained.ok || !
|
|
40887
|
+
if (!contained.ok || !existsSync14(contained.absolutePath)) {
|
|
40660
40888
|
return null;
|
|
40661
40889
|
}
|
|
40662
40890
|
try {
|
|
40663
|
-
return
|
|
40891
|
+
return readFileSync19(contained.absolutePath, "utf8");
|
|
40664
40892
|
} catch {
|
|
40665
40893
|
return null;
|
|
40666
40894
|
}
|
|
@@ -40906,7 +41134,7 @@ class GodotServer {
|
|
|
40906
41134
|
godot: {
|
|
40907
41135
|
path: located.ok ? located.path : null,
|
|
40908
41136
|
version: located.ok ? await this.godotVersion(located.path) : null,
|
|
40909
|
-
problem: located.ok ? undefined :
|
|
41137
|
+
problem: located.ok ? undefined : GodotLocator.refusal(located).message
|
|
40910
41138
|
},
|
|
40911
41139
|
game: {
|
|
40912
41140
|
processActive: active,
|
|
@@ -40929,7 +41157,7 @@ class GodotServer {
|
|
|
40929
41157
|
return null;
|
|
40930
41158
|
}
|
|
40931
41159
|
try {
|
|
40932
|
-
return settingKeys(
|
|
41160
|
+
return settingKeys(readFileSync19(join21(projectPath, "project.godot"), "utf8"));
|
|
40933
41161
|
} catch {
|
|
40934
41162
|
return null;
|
|
40935
41163
|
}
|
|
@@ -41182,7 +41410,7 @@ class GodotServer {
|
|
|
41182
41410
|
}
|
|
41183
41411
|
async runtimeUp(projectPath, before, budget, withArgs) {
|
|
41184
41412
|
const budgetMs = budget.ms;
|
|
41185
|
-
if (!
|
|
41413
|
+
if (!existsSync14(join21(projectPath, RUNTIME_AUTOLOAD.path))) {
|
|
41186
41414
|
return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
|
|
41187
41415
|
}
|
|
41188
41416
|
let asking = null;
|
|
@@ -41245,7 +41473,7 @@ class GodotServer {
|
|
|
41245
41473
|
}
|
|
41246
41474
|
let announcedAt;
|
|
41247
41475
|
try {
|
|
41248
|
-
announcedAt =
|
|
41476
|
+
announcedAt = statSync8(endpoint.file).mtimeMs;
|
|
41249
41477
|
} catch {
|
|
41250
41478
|
return;
|
|
41251
41479
|
}
|
|
@@ -41920,15 +42148,18 @@ class GodotServer {
|
|
|
41920
42148
|
"editor_output with no op answers about the game a run is playing, which is a different log"
|
|
41921
42149
|
]);
|
|
41922
42150
|
}
|
|
41923
|
-
const
|
|
42151
|
+
const asked = readString(args, "severity");
|
|
42152
|
+
const severity = asked === "error" || asked === "warning" ? asked : "info";
|
|
42153
|
+
const contains = readNonEmptyString(args, "contains");
|
|
41924
42154
|
const selected = console2.log.select({
|
|
41925
|
-
severity
|
|
42155
|
+
severity,
|
|
41926
42156
|
sinceLastCall: false,
|
|
41927
|
-
contains
|
|
42157
|
+
contains,
|
|
41928
42158
|
limit: readPositiveNumber(args, "limit") ?? 200
|
|
41929
42159
|
});
|
|
41930
42160
|
const everything = console2.log.everything();
|
|
41931
|
-
const
|
|
42161
|
+
const filtered = severity !== "info" || contains !== undefined;
|
|
42162
|
+
const grouped = bursts(everything, readNonNegativeNumber(args, "before") ?? 3, filtered ? (entry) => answersTo(entry, severity, contains) : undefined);
|
|
41932
42163
|
const shown = grouped.slice(0, MOST_GROUPS);
|
|
41933
42164
|
return this.jsonTextResponse({
|
|
41934
42165
|
editorPid: editorPid ?? undefined,
|
|
@@ -41996,7 +42227,7 @@ class GodotServer {
|
|
|
41996
42227
|
notes.push(endedWithoutACode({
|
|
41997
42228
|
throughEditor: run.throughEditor,
|
|
41998
42229
|
announced: announced !== undefined || run.announcedPid !== undefined,
|
|
41999
|
-
couldAnnounce: run.projectPath !== null &&
|
|
42230
|
+
couldAnnounce: run.projectPath !== null && existsSync14(join21(run.projectPath, RUNTIME_AUTOLOAD.path)),
|
|
42000
42231
|
endedHere: typeof run.endedHere === "string",
|
|
42001
42232
|
pickedUp: run.pickedUp === true
|
|
42002
42233
|
}));
|
|
@@ -42509,7 +42740,7 @@ class GodotServer {
|
|
|
42509
42740
|
async ownRunOnItsWay(projectPath, budgetMs) {
|
|
42510
42741
|
const run = this.currentRun();
|
|
42511
42742
|
const project = projectPath ?? run?.projectPath ?? null;
|
|
42512
|
-
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)) {
|
|
42513
42744
|
return null;
|
|
42514
42745
|
}
|
|
42515
42746
|
const already = runtimesAnnounced().running;
|
|
@@ -42518,7 +42749,7 @@ class GodotServer {
|
|
|
42518
42749
|
return !run.announcedBefore.has(one.pid);
|
|
42519
42750
|
}
|
|
42520
42751
|
try {
|
|
42521
|
-
return
|
|
42752
|
+
return statSync8(one.file).mtimeMs >= run.startedAt;
|
|
42522
42753
|
} catch {
|
|
42523
42754
|
return false;
|
|
42524
42755
|
}
|
|
@@ -42588,8 +42819,8 @@ class GodotServer {
|
|
|
42588
42819
|
return this.createErrorResponse(refusal);
|
|
42589
42820
|
}
|
|
42590
42821
|
}
|
|
42591
|
-
const screenshotDir = expectsScreenshot ?
|
|
42592
|
-
const screenshotPath = screenshotDir ?
|
|
42822
|
+
const screenshotDir = expectsScreenshot ? mkdtempSync2(join21(tmpdir4(), "gdharness-runtime-screenshot-")) : null;
|
|
42823
|
+
const screenshotPath = screenshotDir ? join21(screenshotDir, "capture.png") : null;
|
|
42593
42824
|
try {
|
|
42594
42825
|
const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs, !expectsScreenshot);
|
|
42595
42826
|
if (!reply.ok && reply.requestId !== undefined) {
|
|
@@ -42613,7 +42844,7 @@ class GodotServer {
|
|
|
42613
42844
|
if (readString(payload, "type") !== "screenshot_file" || !returnedPath || !screenshotPath) {
|
|
42614
42845
|
return this.createErrorResponse(`The game answered a capture with ${JSON.stringify(payload)}`);
|
|
42615
42846
|
}
|
|
42616
|
-
if (
|
|
42847
|
+
if (normalize2(returnedPath) !== normalize2(screenshotPath)) {
|
|
42617
42848
|
return this.createErrorResponse(`Rejected screenshot file path outside the managed capture path: '${returnedPath}'`);
|
|
42618
42849
|
}
|
|
42619
42850
|
const dimensions = `${readNumber(payload, "width") ?? 0}x${readNumber(payload, "height") ?? 0} ${readString(payload, "format") ?? "unknown"}`;
|
|
@@ -42630,7 +42861,7 @@ class GodotServer {
|
|
|
42630
42861
|
type: "text",
|
|
42631
42862
|
text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}${saveTo === null ? "" : `. Saved to ${saveTo}`}`
|
|
42632
42863
|
},
|
|
42633
|
-
{ type: "image", data:
|
|
42864
|
+
{ type: "image", data: readFileSync19(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
42634
42865
|
]
|
|
42635
42866
|
};
|
|
42636
42867
|
} finally {
|
|
@@ -42730,7 +42961,7 @@ async function runGodotServer() {
|
|
|
42730
42961
|
const server = new GodotServer;
|
|
42731
42962
|
await server.run();
|
|
42732
42963
|
}
|
|
42733
|
-
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/";
|
|
42734
42965
|
var init_server3 = __esm(() => {
|
|
42735
42966
|
init_mcp();
|
|
42736
42967
|
init_stdio2();
|
|
@@ -42770,8 +43001,8 @@ var init_server3 = __esm(() => {
|
|
|
42770
43001
|
init_setup();
|
|
42771
43002
|
init_tool_definitions();
|
|
42772
43003
|
init_update_check();
|
|
42773
|
-
|
|
42774
|
-
__dirname2 =
|
|
43004
|
+
run5 = promisify5(execFile5);
|
|
43005
|
+
__dirname2 = dirname15(fileURLToPath5(import.meta.url));
|
|
42775
43006
|
CONSOLE_WRAPPER = /_console(\.exe)?$/i;
|
|
42776
43007
|
LONGEST_BOOT_NOTED_MS = 5 * 60000;
|
|
42777
43008
|
VALUE_NAMES = {
|
|
@@ -42817,128 +43048,11 @@ var init_server3 = __esm(() => {
|
|
|
42817
43048
|
});
|
|
42818
43049
|
|
|
42819
43050
|
// src/cli.ts
|
|
42820
|
-
|
|
42821
|
-
import { join as join25, resolve as resolve6 } from "path";
|
|
42822
|
-
|
|
42823
|
-
// src/bridge-announce.ts
|
|
43051
|
+
init_bridge_announce();
|
|
42824
43052
|
init_errors();
|
|
42825
|
-
|
|
42826
|
-
import {
|
|
42827
|
-
import {
|
|
42828
|
-
function announcementPath(projectPath) {
|
|
42829
|
-
return join(projectPath, ".godot", "gdharness-bridge.json");
|
|
42830
|
-
}
|
|
42831
|
-
function readAnnouncement(path) {
|
|
42832
|
-
try {
|
|
42833
|
-
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
42834
|
-
if (typeof parsed !== "object" || parsed === null) {
|
|
42835
|
-
return null;
|
|
42836
|
-
}
|
|
42837
|
-
const said = parsed;
|
|
42838
|
-
if (typeof said.port !== "number" || typeof said.pid !== "number") {
|
|
42839
|
-
return null;
|
|
42840
|
-
}
|
|
42841
|
-
return said;
|
|
42842
|
-
} catch {
|
|
42843
|
-
return null;
|
|
42844
|
-
}
|
|
42845
|
-
}
|
|
42846
|
-
function serverBehind(announcement, startedAt) {
|
|
42847
|
-
if (announcement === null || typeof announcement.version !== "string" || startedAt === undefined) {
|
|
42848
|
-
return null;
|
|
42849
|
-
}
|
|
42850
|
-
const announcedAt = Date.parse(announcement.startedAt);
|
|
42851
|
-
if (Number.isNaN(announcedAt) || startedAt > announcedAt + 1000) {
|
|
42852
|
-
return null;
|
|
42853
|
-
}
|
|
42854
|
-
return { version: announcement.version, pid: announcement.pid };
|
|
42855
|
-
}
|
|
42856
|
-
function serverServing(projectPath) {
|
|
42857
|
-
const announcement = readAnnouncement(announcementPath(projectPath));
|
|
42858
|
-
if (announcement === null) {
|
|
42859
|
-
return null;
|
|
42860
|
-
}
|
|
42861
|
-
return serverBehind(announcement, startTimesOf([announcement.pid]).get(announcement.pid));
|
|
42862
|
-
}
|
|
42863
|
-
|
|
42864
|
-
// src/errors.ts
|
|
42865
|
-
class Refusal2 extends Error {
|
|
42866
|
-
}
|
|
42867
|
-
|
|
42868
|
-
// src/godot-path.ts
|
|
42869
|
-
init_detection();
|
|
42870
|
-
init_errors();
|
|
42871
|
-
import { execFile as execFile2 } from "node:child_process";
|
|
42872
|
-
import { existsSync as existsSync2 } from "node:fs";
|
|
42873
|
-
import { normalize } from "node:path";
|
|
42874
|
-
import { promisify as promisify2 } from "node:util";
|
|
42875
|
-
var run2 = promisify2(execFile2);
|
|
42876
|
-
|
|
42877
|
-
class GodotLocator {
|
|
42878
|
-
answered = new Map;
|
|
42879
|
-
found = null;
|
|
42880
|
-
async find() {
|
|
42881
|
-
if (this.found !== null && await this.whyNot(this.found) === null) {
|
|
42882
|
-
return { ok: true, path: this.found };
|
|
42883
|
-
}
|
|
42884
|
-
this.found = null;
|
|
42885
|
-
const named = envValue("GODOT_PATH");
|
|
42886
|
-
if (named !== undefined) {
|
|
42887
|
-
const path = normalize(named);
|
|
42888
|
-
const reason = await this.whyNot(path);
|
|
42889
|
-
if (reason === null) {
|
|
42890
|
-
this.found = path;
|
|
42891
|
-
return { ok: true, path };
|
|
42892
|
-
}
|
|
42893
|
-
return { ok: false, named: path, reason };
|
|
42894
|
-
}
|
|
42895
|
-
for (const candidate of godotCandidates(process.platform, resolveHomeDirectory())) {
|
|
42896
|
-
const path = normalize(candidate);
|
|
42897
|
-
if (await this.whyNot(path) === null) {
|
|
42898
|
-
this.found = path;
|
|
42899
|
-
return { ok: true, path };
|
|
42900
|
-
}
|
|
42901
|
-
}
|
|
42902
|
-
return { ok: false, named: null };
|
|
42903
|
-
}
|
|
42904
|
-
static refusal(located) {
|
|
42905
|
-
if (located.named === null) {
|
|
42906
|
-
return {
|
|
42907
|
-
message: "No Godot executable found.",
|
|
42908
|
-
advice: [
|
|
42909
|
-
"Set GODOT_PATH to the Godot 4 executable",
|
|
42910
|
-
"Or install Godot where it is usually found, such as /usr/bin/godot or C:\\Program Files\\Godot"
|
|
42911
|
-
]
|
|
42912
|
-
};
|
|
42913
|
-
}
|
|
42914
|
-
return {
|
|
42915
|
-
message: `GODOT_PATH is set to ${located.named}, which ${located.reason ?? "does not answer"}.`,
|
|
42916
|
-
advice: [
|
|
42917
|
-
"Point GODOT_PATH at a Godot 4 executable that answers --version",
|
|
42918
|
-
"Or unset it, and the usual install locations are searched instead"
|
|
42919
|
-
]
|
|
42920
|
-
};
|
|
42921
|
-
}
|
|
42922
|
-
async whyNot(path) {
|
|
42923
|
-
const known = this.answered.get(path);
|
|
42924
|
-
if (known !== undefined) {
|
|
42925
|
-
return known;
|
|
42926
|
-
}
|
|
42927
|
-
let reason;
|
|
42928
|
-
if (path !== "godot" && !existsSync2(path)) {
|
|
42929
|
-
reason = "does not exist";
|
|
42930
|
-
} else {
|
|
42931
|
-
try {
|
|
42932
|
-
await run2(path, ["--version"]);
|
|
42933
|
-
reason = null;
|
|
42934
|
-
} catch (error) {
|
|
42935
|
-
reason = `does not answer --version: ${errorMessage(error)}`;
|
|
42936
|
-
}
|
|
42937
|
-
}
|
|
42938
|
-
this.answered.set(path, reason);
|
|
42939
|
-
return reason;
|
|
42940
|
-
}
|
|
42941
|
-
}
|
|
43053
|
+
init_godot_path();
|
|
43054
|
+
import { existsSync as existsSync15 } from "node:fs";
|
|
43055
|
+
import { join as join22, resolve as resolve6 } from "node:path";
|
|
42942
43056
|
|
|
42943
43057
|
// src/harnesses.ts
|
|
42944
43058
|
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
@@ -43803,204 +43917,9 @@ function taken(harness, path, held, stripped) {
|
|
|
43803
43917
|
return { harness, path, action: "removed" };
|
|
43804
43918
|
}
|
|
43805
43919
|
|
|
43806
|
-
// src/
|
|
43807
|
-
|
|
43808
|
-
|
|
43809
|
-
init_scratch();
|
|
43810
|
-
import { execFile as execFile3 } from "node:child_process";
|
|
43811
|
-
import { mkdtempSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
43812
|
-
import { tmpdir } from "node:os";
|
|
43813
|
-
import { join as join5 } from "node:path";
|
|
43814
|
-
import { promisify as promisify3 } from "node:util";
|
|
43815
|
-
var run3 = promisify3(execFile3);
|
|
43816
|
-
function snakeCased(params) {
|
|
43817
|
-
const result = emptyRecord();
|
|
43818
|
-
for (const [key, value] of Object.entries(params)) {
|
|
43819
|
-
const name = key.startsWith("_") ? key : key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
43820
|
-
result[name] = value;
|
|
43821
|
-
}
|
|
43822
|
-
return result;
|
|
43823
|
-
}
|
|
43824
|
-
function lastJsonObject(stdout) {
|
|
43825
|
-
const lines = stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.startsWith("{"));
|
|
43826
|
-
for (const line of lines.reverse()) {
|
|
43827
|
-
try {
|
|
43828
|
-
const parsed = JSON.parse(line);
|
|
43829
|
-
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
43830
|
-
return parsed;
|
|
43831
|
-
}
|
|
43832
|
-
} catch {}
|
|
43833
|
-
}
|
|
43834
|
-
return null;
|
|
43835
|
-
}
|
|
43836
|
-
function problems(stderr) {
|
|
43837
|
-
const log = new GameLog;
|
|
43838
|
-
log.append("stderr", stderr);
|
|
43839
|
-
log.finish();
|
|
43840
|
-
return log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries;
|
|
43841
|
-
}
|
|
43842
|
-
function reason(stdout, stderr) {
|
|
43843
|
-
const own = stderr.split(/\r?\n/).filter((line) => line.startsWith("[ERROR] ")).map((line) => line.slice("[ERROR] ".length));
|
|
43844
|
-
if (own.length > 0) {
|
|
43845
|
-
return own.join("; ");
|
|
43846
|
-
}
|
|
43847
|
-
const engine = problems(stderr).filter((entry) => entry.severity === "error").map((entry) => entry.text);
|
|
43848
|
-
if (engine.length > 0) {
|
|
43849
|
-
return engine.join("; ");
|
|
43850
|
-
}
|
|
43851
|
-
return stdout.trim().split(/\r?\n/).at(-1) ?? "no output at all";
|
|
43852
|
-
}
|
|
43853
|
-
async function runOperation(engine, operation, params, projectPath) {
|
|
43854
|
-
const paramsDir = mkdtempSync(join5(tmpdir(), "gdharness-params-"));
|
|
43855
|
-
const paramsFile = join5(paramsDir, `${operation}.json`);
|
|
43856
|
-
writeFileSync3(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
|
|
43857
|
-
const args = [
|
|
43858
|
-
"--headless",
|
|
43859
|
-
"--log-file",
|
|
43860
|
-
join5(paramsDir, "engine.log"),
|
|
43861
|
-
"--path",
|
|
43862
|
-
projectPath,
|
|
43863
|
-
"--script",
|
|
43864
|
-
engine.script,
|
|
43865
|
-
operation,
|
|
43866
|
-
`@file:${paramsFile}`,
|
|
43867
|
-
...engine.debug ? ["--debug-godot"] : []
|
|
43868
|
-
];
|
|
43869
|
-
let stdout;
|
|
43870
|
-
let stderr;
|
|
43871
|
-
try {
|
|
43872
|
-
({ stdout, stderr } = await run3(engine.godotPath, args, { signal: callSignal() }));
|
|
43873
|
-
} catch (error) {
|
|
43874
|
-
if (error instanceof Error && "stdout" in error && "stderr" in error) {
|
|
43875
|
-
const failed = error;
|
|
43876
|
-
return {
|
|
43877
|
-
ok: false,
|
|
43878
|
-
message: `${operation} failed (exit ${failed.code ?? "unknown"}): ${reason(failed.stdout, failed.stderr)}`,
|
|
43879
|
-
messages: problems(failed.stderr)
|
|
43880
|
-
};
|
|
43881
|
-
}
|
|
43882
|
-
return {
|
|
43883
|
-
ok: false,
|
|
43884
|
-
message: `${operation} could not be run: ${error instanceof Error ? error.message : String(error)}`,
|
|
43885
|
-
messages: []
|
|
43886
|
-
};
|
|
43887
|
-
} finally {
|
|
43888
|
-
discard(paramsDir);
|
|
43889
|
-
}
|
|
43890
|
-
const payload = lastJsonObject(stdout);
|
|
43891
|
-
if (payload === null) {
|
|
43892
|
-
return {
|
|
43893
|
-
ok: false,
|
|
43894
|
-
message: `${operation} produced no result: ${reason(stdout, stderr)}`,
|
|
43895
|
-
messages: problems(stderr)
|
|
43896
|
-
};
|
|
43897
|
-
}
|
|
43898
|
-
return { ok: true, payload, messages: problems(stderr) };
|
|
43899
|
-
}
|
|
43900
|
-
|
|
43901
|
-
// src/issues.ts
|
|
43902
|
-
init_errors();
|
|
43903
|
-
init_server_version();
|
|
43904
|
-
import { createHash } from "node:crypto";
|
|
43905
|
-
import process4 from "node:process";
|
|
43906
|
-
var NEW_ISSUE = "https://github.com/Aureliolo/gdharness/issues/new";
|
|
43907
|
-
var ENHANCEMENT_URL = `${NEW_ISSUE}?template=feature_request.md`;
|
|
43908
|
-
function runtime() {
|
|
43909
|
-
const versions = process4.versions;
|
|
43910
|
-
const bun = versions["bun"];
|
|
43911
|
-
return bun === undefined ? `node ${process4.versions.node}` : `bun ${bun}`;
|
|
43912
|
-
}
|
|
43913
|
-
function defectSignature(where, message) {
|
|
43914
|
-
const normalised = `${where}
|
|
43915
|
-
${message}`.toLowerCase().replaceAll("\\", "/").replaceAll(/(?:[a-z]:)?(?:\/+[\w.-]+){2,}/gu, "<path>").replaceAll(/\d+/gu, "0");
|
|
43916
|
-
return createHash("sha256").update(normalised).digest("hex").slice(0, 8);
|
|
43917
|
-
}
|
|
43918
|
-
function facts(where, message, godotVersion) {
|
|
43919
|
-
return [
|
|
43920
|
-
["Where", where],
|
|
43921
|
-
["Error", message],
|
|
43922
|
-
["Version", `gdharness ${SERVER_VERSION}`],
|
|
43923
|
-
["Runtime", `${runtime()}, ${process4.platform} ${process4.arch}`],
|
|
43924
|
-
["Godot", godotVersion ?? "not known at the point this failed"],
|
|
43925
|
-
["Signature", defectSignature(where, message)]
|
|
43926
|
-
];
|
|
43927
|
-
}
|
|
43928
|
-
function filledTemplate(where, message, godotVersion) {
|
|
43929
|
-
return [
|
|
43930
|
-
`**gdharness version**: ${SERVER_VERSION}`,
|
|
43931
|
-
`**Godot version**: ${godotVersion ?? ""}`,
|
|
43932
|
-
`**Bun version**: ${runtime()}`,
|
|
43933
|
-
`**OS**: ${process4.platform} ${process4.arch}`,
|
|
43934
|
-
"**MCP client**:",
|
|
43935
|
-
"",
|
|
43936
|
-
"## What you did",
|
|
43937
|
-
"",
|
|
43938
|
-
where,
|
|
43939
|
-
"",
|
|
43940
|
-
"## What you expected",
|
|
43941
|
-
"",
|
|
43942
|
-
"The call to answer, or to refuse with a reason.",
|
|
43943
|
-
"",
|
|
43944
|
-
"## What happened",
|
|
43945
|
-
"",
|
|
43946
|
-
"gdharness failed in a way it does not model.",
|
|
43947
|
-
"",
|
|
43948
|
-
"```text",
|
|
43949
|
-
message,
|
|
43950
|
-
"```",
|
|
43951
|
-
"",
|
|
43952
|
-
`Signature: \`${defectSignature(where, message)}\``,
|
|
43953
|
-
"",
|
|
43954
|
-
"If the real cause turned out to be the project, the environment or the call, say so here:",
|
|
43955
|
-
"reaching you as a defect rather than as a refusal naming what would have worked is then the",
|
|
43956
|
-
"thing to fix.",
|
|
43957
|
-
"",
|
|
43958
|
-
"## Anything that makes it reproducible",
|
|
43959
|
-
""
|
|
43960
|
-
].join(`
|
|
43961
|
-
`);
|
|
43962
|
-
}
|
|
43963
|
-
function defectReport(where, error, context = {}) {
|
|
43964
|
-
const { godotVersion, seenBefore = 0 } = context;
|
|
43965
|
-
const message = errorMessage(error);
|
|
43966
|
-
const title = `Defect ${defectSignature(where, message)}: ${where}`;
|
|
43967
|
-
const url = `${NEW_ISSUE}?template=bug_report.md&title=${encodeURIComponent(title)}` + `&body=${encodeURIComponent(filledTemplate(where, message, godotVersion))}`;
|
|
43968
|
-
const width = Math.max(...facts(where, message, godotVersion).map(([label]) => (label ?? "").length));
|
|
43969
|
-
return [
|
|
43970
|
-
"gdharness failed in a way it does not model. Whatever went wrong underneath, it reached you",
|
|
43971
|
-
"as a crash rather than as a refusal naming what would have worked, and that much is a defect",
|
|
43972
|
-
"in gdharness however the rest of it turns out.",
|
|
43973
|
-
"",
|
|
43974
|
-
...seenBefore === 0 ? [
|
|
43975
|
-
"Send the call once more before anything else. Some of what reaches this line is a file",
|
|
43976
|
-
"another process had open, or a connection that dropped, and those are gone by the next",
|
|
43977
|
-
"attempt. If the same Signature comes back, it is not one of those, and repeating it",
|
|
43978
|
-
"further only spends turns."
|
|
43979
|
-
] : [
|
|
43980
|
-
`You have had this exact failure ${seenBefore === 1 ? "once" : `${seenBefore} times`} in this session already, so it is a`,
|
|
43981
|
-
"state rather than a passing one, and sending the call again will not change it."
|
|
43982
|
-
],
|
|
43983
|
-
"",
|
|
43984
|
-
...facts(where, message, godotVersion).map(([label, value]) => ` ${(label ?? "").padEnd(width)} ${value ?? ""}`),
|
|
43985
|
-
"",
|
|
43986
|
-
"Do not open an issue on your own initiative. Tell the person whose machine this is what",
|
|
43987
|
-
"broke, and ask whether you may report it to gdharness on their behalf. What the report",
|
|
43988
|
-
"carries is the lines above and nothing else, and whether that goes into a public tracker",
|
|
43989
|
-
"is theirs to decide.",
|
|
43990
|
-
"",
|
|
43991
|
-
"If they say yes, this link carries the report already filled in:",
|
|
43992
|
-
url,
|
|
43993
|
-
"",
|
|
43994
|
-
"If they say yes and you have no way to open an issue yourself, give them that link and",
|
|
43995
|
-
"those lines, and stay with them while they file it.",
|
|
43996
|
-
"",
|
|
43997
|
-
"Worth reporting either way. If you can see that what actually went wrong was the project,",
|
|
43998
|
-
"the environment or the call, then this message is the defect: a failure gdharness knows",
|
|
43999
|
-
"about is meant to arrive as a refusal naming what would have worked, not as this. Say that",
|
|
44000
|
-
"in the report and it is the more useful of the two."
|
|
44001
|
-
].join(`
|
|
44002
|
-
`);
|
|
44003
|
-
}
|
|
43920
|
+
// src/cli.ts
|
|
43921
|
+
init_headless();
|
|
43922
|
+
init_issues();
|
|
44004
43923
|
|
|
44005
43924
|
// src/prompt.ts
|
|
44006
43925
|
import process5 from "node:process";
|
|
@@ -44025,250 +43944,15 @@ class Ask {
|
|
|
44025
43944
|
}
|
|
44026
43945
|
}
|
|
44027
43946
|
|
|
44028
|
-
// src/
|
|
44029
|
-
init_update_check();
|
|
44030
|
-
import { readFileSync as readFileSync5 } from "node:fs";
|
|
44031
|
-
var DEBUG_MODE2 = process.env["DEBUG"] === "true";
|
|
44032
|
-
var GODOT_DEBUG_MODE_DEFAULT2 = process.env["GODOT_DEBUG"] === "true" || DEBUG_MODE2;
|
|
44033
|
-
var SERVER_VERSION2 = (() => {
|
|
44034
|
-
try {
|
|
44035
|
-
const pkg = JSON.parse(readFileSync5(new URL("../package.json", import.meta.url), "utf8"));
|
|
44036
|
-
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
44037
|
-
} catch {
|
|
44038
|
-
return "0.0.0";
|
|
44039
|
-
}
|
|
44040
|
-
})();
|
|
44041
|
-
|
|
44042
|
-
// src/setup.ts
|
|
44043
|
-
init_class_cache();
|
|
44044
|
-
init_headless();
|
|
44045
|
-
init_resources();
|
|
43947
|
+
// src/cli.ts
|
|
44046
43948
|
init_server_version();
|
|
44047
|
-
|
|
44048
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
44049
|
-
import {
|
|
44050
|
-
cpSync,
|
|
44051
|
-
existsSync as existsSync7,
|
|
44052
|
-
mkdirSync as mkdirSync4,
|
|
44053
|
-
readdirSync as readdirSync3,
|
|
44054
|
-
readFileSync as readFileSync8,
|
|
44055
|
-
rmSync as rmSync4,
|
|
44056
|
-
statSync as statSync3,
|
|
44057
|
-
writeFileSync as writeFileSync6
|
|
44058
|
-
} from "node:fs";
|
|
44059
|
-
import { dirname as dirname4, join as join9 } from "node:path";
|
|
44060
|
-
import { fileURLToPath } from "node:url";
|
|
44061
|
-
var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
|
|
44062
|
-
var EDITOR_PLUGINS = ["gdharness_editor", "auto_reload"];
|
|
44063
|
-
var RUNTIME_AUTOLOAD = {
|
|
44064
|
-
name: "GdharnessRuntime",
|
|
44065
|
-
path: "addons/gdharness_runtime/runtime_autoload.gd"
|
|
44066
|
-
};
|
|
44067
|
-
var VERSION_MARKER = ".gdharness-version";
|
|
44068
|
-
var DIGEST_MARKER = ".gdharness-digest";
|
|
44069
|
-
function editorAddonDigest(from = shippedAddonsDirectory()) {
|
|
44070
|
-
const hash = createHash2("sha256");
|
|
44071
|
-
const walk = (directory, relative) => {
|
|
44072
|
-
for (const name of readdirSync3(directory).sort()) {
|
|
44073
|
-
if (name === VERSION_MARKER || name === DIGEST_MARKER) {
|
|
44074
|
-
continue;
|
|
44075
|
-
}
|
|
44076
|
-
const path = join9(directory, name);
|
|
44077
|
-
const inside = `${relative}/${name}`;
|
|
44078
|
-
if (statSync3(path).isDirectory()) {
|
|
44079
|
-
walk(path, inside);
|
|
44080
|
-
} else {
|
|
44081
|
-
hash.update(`${inside}
|
|
44082
|
-
`);
|
|
44083
|
-
hash.update(readFileSync8(path));
|
|
44084
|
-
hash.update(`
|
|
44085
|
-
`);
|
|
44086
|
-
}
|
|
44087
|
-
}
|
|
44088
|
-
};
|
|
44089
|
-
for (const name of EDITOR_PLUGINS) {
|
|
44090
|
-
walk(join9(from, name), name);
|
|
44091
|
-
}
|
|
44092
|
-
return hash.digest("hex");
|
|
44093
|
-
}
|
|
44094
|
-
function installedEditorDigest(projectPath) {
|
|
44095
|
-
const marker = join9(projectPath, "addons", ADDONS[0], DIGEST_MARKER);
|
|
44096
|
-
if (!existsSync7(marker)) {
|
|
44097
|
-
return null;
|
|
44098
|
-
}
|
|
44099
|
-
const said = readFileSync8(marker, "utf8").trim();
|
|
44100
|
-
return said === "" ? null : said;
|
|
44101
|
-
}
|
|
44102
|
-
function shippedAddonsDirectory() {
|
|
44103
|
-
return join9(dirname4(fileURLToPath(import.meta.url)), "godot", "addons");
|
|
44104
|
-
}
|
|
44105
|
-
function shippedOperationsScript() {
|
|
44106
|
-
return join9(dirname4(fileURLToPath(import.meta.url)), "godot", "operations", "godot_operations.gd");
|
|
44107
|
-
}
|
|
44108
|
-
function installAddons(projectPath, from = shippedAddonsDirectory()) {
|
|
44109
|
-
const installed = [];
|
|
44110
|
-
for (const name of ADDONS) {
|
|
44111
|
-
const source = join9(from, name);
|
|
44112
|
-
if (!existsSync7(join9(source, name === "gdharness_runtime" ? "runtime_autoload.gd" : "plugin.cfg"))) {
|
|
44113
|
-
throw new Error(`The package holds no ${name} addon at ${source}.`);
|
|
44114
|
-
}
|
|
44115
|
-
const target = join9(projectPath, "addons", name);
|
|
44116
|
-
const replaced = existsSync7(target);
|
|
44117
|
-
rmSync4(target, { recursive: true, force: true });
|
|
44118
|
-
mkdirSync4(dirname4(target), { recursive: true });
|
|
44119
|
-
cpSync(source, target, { recursive: true });
|
|
44120
|
-
writeFileSync6(join9(target, VERSION_MARKER), `${SERVER_VERSION}
|
|
44121
|
-
`);
|
|
44122
|
-
installed.push({ name, path: target, replaced });
|
|
44123
|
-
}
|
|
44124
|
-
writeFileSync6(join9(projectPath, "addons", ADDONS[0], DIGEST_MARKER), `${editorAddonDigest(from)}
|
|
44125
|
-
`);
|
|
44126
|
-
return installed;
|
|
44127
|
-
}
|
|
44128
|
-
function removeAddons(projectPath) {
|
|
44129
|
-
const removed = [];
|
|
44130
|
-
for (const name of ADDONS) {
|
|
44131
|
-
const target = join9(projectPath, "addons", name);
|
|
44132
|
-
if (existsSync7(target)) {
|
|
44133
|
-
rmSync4(target, { recursive: true, force: true });
|
|
44134
|
-
removed.push(target);
|
|
44135
|
-
}
|
|
44136
|
-
}
|
|
44137
|
-
return removed;
|
|
44138
|
-
}
|
|
44139
|
-
async function disablePlugins(engine, projectPath, names) {
|
|
44140
|
-
const outcomes = [];
|
|
44141
|
-
for (const name of names) {
|
|
44142
|
-
outcomes.push(await runOperation2(engine, "disable_plugin", { pluginName: name }, projectPath));
|
|
44143
|
-
}
|
|
44144
|
-
return outcomes;
|
|
44145
|
-
}
|
|
44146
|
-
async function enablePlugins(engine, projectPath, names) {
|
|
44147
|
-
const outcomes = [];
|
|
44148
|
-
for (const name of names) {
|
|
44149
|
-
outcomes.push(await runOperation2(engine, "enable_plugin", { pluginName: name }, projectPath));
|
|
44150
|
-
}
|
|
44151
|
-
return outcomes;
|
|
44152
|
-
}
|
|
44153
|
-
function autoloadIsOurs(named) {
|
|
44154
|
-
return named === null || named === `res://${RUNTIME_AUTOLOAD.path}`;
|
|
44155
|
-
}
|
|
44156
|
-
async function setRuntime(engine, projectPath, on) {
|
|
44157
|
-
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);
|
|
44158
|
-
}
|
|
44159
|
-
function loaderAmong(projectPath, named) {
|
|
44160
|
-
let byName = null;
|
|
44161
|
-
for (const [entry, path] of named) {
|
|
44162
|
-
if (entry === RUNTIME_AUTOLOAD.name || path === RUNTIME_AUTOLOAD.path) {
|
|
44163
|
-
continue;
|
|
44164
|
-
}
|
|
44165
|
-
let source = null;
|
|
44166
|
-
try {
|
|
44167
|
-
source = readFileSync8(join9(projectPath, path), "utf8");
|
|
44168
|
-
} catch {}
|
|
44169
|
-
if (source?.includes(RUNTIME_AUTOLOAD.path)) {
|
|
44170
|
-
return { name: entry, path, exists: true };
|
|
44171
|
-
}
|
|
44172
|
-
byName ??= path.toLowerCase().includes("gdharness") ? { name: entry, path, exists: source !== null } : null;
|
|
44173
|
-
}
|
|
44174
|
-
return byName;
|
|
44175
|
-
}
|
|
44176
|
-
function autoloadPaths(settings) {
|
|
44177
|
-
const named = new Map;
|
|
44178
|
-
for (const [name, value] of Object.entries(settings["autoload"] ?? {})) {
|
|
44179
|
-
const said = typeof value === "string" ? value.replace(/^\*/, "") : "";
|
|
44180
|
-
if (said.startsWith("res://")) {
|
|
44181
|
-
named.set(name, said.slice("res://".length));
|
|
44182
|
-
}
|
|
44183
|
-
}
|
|
44184
|
-
return named;
|
|
44185
|
-
}
|
|
44186
|
-
function trackedByGit(projectPath, wanted) {
|
|
44187
|
-
if (wanted.length === 0) {
|
|
44188
|
-
return new Set;
|
|
44189
|
-
}
|
|
44190
|
-
const asked = spawnSync2("git", ["ls-files", "-z", "--", ...wanted], {
|
|
44191
|
-
cwd: projectPath,
|
|
44192
|
-
encoding: "utf8"
|
|
44193
|
-
});
|
|
44194
|
-
if (asked.status !== 0) {
|
|
44195
|
-
return null;
|
|
44196
|
-
}
|
|
44197
|
-
return new Set(asked.stdout.split("\x00").filter(Boolean));
|
|
44198
|
-
}
|
|
44199
|
-
function enabledPlugins(settings) {
|
|
44200
|
-
const raw = settings["editor_plugins"]?.["enabled"];
|
|
44201
|
-
if (typeof raw !== "string") {
|
|
44202
|
-
return [];
|
|
44203
|
-
}
|
|
44204
|
-
return [...raw.matchAll(/res:\/\/addons\/([^/"]+)\/plugin\.cfg/g)].map((match) => match[1] ?? "").filter(Boolean);
|
|
44205
|
-
}
|
|
44206
|
-
function inspectProject(projectPath) {
|
|
44207
|
-
const problems = [];
|
|
44208
|
-
const addons = ADDONS.map((name) => {
|
|
44209
|
-
const directory = join9(projectPath, "addons", name);
|
|
44210
|
-
const marker = join9(directory, VERSION_MARKER);
|
|
44211
|
-
const installed = existsSync7(directory);
|
|
44212
|
-
const version = existsSync7(marker) ? readFileSync8(marker, "utf8").trim() : null;
|
|
44213
|
-
const current = version === SERVER_VERSION;
|
|
44214
|
-
if (!installed) {
|
|
44215
|
-
problems.push(`addons/${name} is not installed; run gdharness setup`);
|
|
44216
|
-
} else if (!current) {
|
|
44217
|
-
problems.push(`addons/${name} is ${version ?? "of an unknown version"}, this gdharness is ${SERVER_VERSION}; run gdharness setup`);
|
|
44218
|
-
}
|
|
44219
|
-
return { name, installed, version, current };
|
|
44220
|
-
});
|
|
44221
|
-
const settings = parseProjectGodot(readFileSync8(join9(projectPath, "project.godot"), "utf8"));
|
|
44222
|
-
const pluginsEnabled = enabledPlugins(settings);
|
|
44223
|
-
for (const name of EDITOR_PLUGINS) {
|
|
44224
|
-
if (!pluginsEnabled.includes(name)) {
|
|
44225
|
-
problems.push(`the ${name} plugin is not enabled in project.godot; run gdharness setup`);
|
|
44226
|
-
}
|
|
44227
|
-
}
|
|
44228
|
-
const autoload = settings["autoload"];
|
|
44229
|
-
const registered = autoload === undefined ? undefined : readString(autoload, RUNTIME_AUTOLOAD.name);
|
|
44230
|
-
const runtimeAutoloadPath = typeof registered === "string" ? registered.replace(/^\*/, "") : null;
|
|
44231
|
-
const named = autoloadPaths(settings);
|
|
44232
|
-
const runtimeLoaderAutoload = loaderAmong(projectPath, named);
|
|
44233
|
-
const runtimeAutoload = runtimeAutoloadPath !== null || runtimeLoaderAutoload !== null;
|
|
44234
|
-
const tracked = trackedByGit(projectPath, [...named.values()]);
|
|
44235
|
-
const addonMissing = (path) => addons.some((addon) => !addon.installed && path.startsWith(`addons/${addon.name}/`));
|
|
44236
|
-
for (const [name, path] of named) {
|
|
44237
|
-
if (!existsSync7(join9(projectPath, path))) {
|
|
44238
|
-
if (!addonMissing(path)) {
|
|
44239
|
-
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`);
|
|
44240
|
-
}
|
|
44241
|
-
continue;
|
|
44242
|
-
}
|
|
44243
|
-
if (tracked !== null && !tracked.has(path)) {
|
|
44244
|
-
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`);
|
|
44245
|
-
}
|
|
44246
|
-
}
|
|
44247
|
-
const cached = cachedClasses(projectPath);
|
|
44248
|
-
const staleClasses = cached === null ? [] : staleAgainst(cached, projectPath);
|
|
44249
|
-
if (cached === null) {
|
|
44250
|
-
problems.push("no .godot/global_script_class_cache.cfg: the engine knows no class_name; run project_import refresh_classes or open the editor");
|
|
44251
|
-
} else if (staleClasses.length > 0) {
|
|
44252
|
-
problems.push(`the class cache does not list ${staleClasses.join(", ")}; run project_import refresh_classes or gdharness classes`);
|
|
44253
|
-
}
|
|
44254
|
-
return {
|
|
44255
|
-
projectPath,
|
|
44256
|
-
addons,
|
|
44257
|
-
pluginsEnabled,
|
|
44258
|
-
runtimeAutoload,
|
|
44259
|
-
runtimeAutoloadPath,
|
|
44260
|
-
runtimeLoaderAutoload,
|
|
44261
|
-
staleClasses,
|
|
44262
|
-
classCacheExists: cached !== null,
|
|
44263
|
-
problems
|
|
44264
|
-
};
|
|
44265
|
-
}
|
|
43949
|
+
init_setup();
|
|
44266
43950
|
|
|
44267
43951
|
// src/skill.ts
|
|
44268
43952
|
init_setup();
|
|
44269
43953
|
init_tool_definitions();
|
|
44270
|
-
import { existsSync as
|
|
44271
|
-
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";
|
|
44272
43956
|
|
|
44273
43957
|
// src/tool-reference.ts
|
|
44274
43958
|
init_headless_operations();
|
|
@@ -44364,7 +44048,7 @@ function renderToolsMarkdown() {
|
|
|
44364
44048
|
|
|
44365
44049
|
// src/skill.ts
|
|
44366
44050
|
var SKILL_NAME = "gdharness";
|
|
44367
|
-
var SHARED_SKILLS =
|
|
44051
|
+
var SHARED_SKILLS = join9(".agents", "skills");
|
|
44368
44052
|
function skillMarkdown(version) {
|
|
44369
44053
|
return `---
|
|
44370
44054
|
name: ${SKILL_NAME}
|
|
@@ -44434,7 +44118,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
44434
44118
|
| What a property reads right now, on a node whose path you already hold | \`runtime_inspect\` \`property\`, \`runtime_invoke\` |
|
|
44435
44119
|
| What a property on an object a node holds reads | the same two, with colons: \`_game:clock:speed\` |
|
|
44436
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 |
|
|
44437
|
-
| 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 |
|
|
44438
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 |
|
|
44439
44123
|
| Fill in a field | \`runtime_input click\` on it, then \`runtime_input text\` with \`replace\`; a submitted field has to be clicked again |
|
|
44440
44124
|
| Click somebody standing in a 3D room | \`runtime_input click\` on the Node3D, which aims at what it draws |
|
|
@@ -44553,27 +44237,27 @@ whatever the engine said on stderr comes back under \`engine_messages\`.
|
|
|
44553
44237
|
function skillFiles(version) {
|
|
44554
44238
|
return new Map([
|
|
44555
44239
|
["SKILL.md", skillMarkdown(version)],
|
|
44556
|
-
[
|
|
44240
|
+
[join9("references", "tools.md"), renderToolsMarkdown()]
|
|
44557
44241
|
]);
|
|
44558
44242
|
}
|
|
44559
44243
|
function skillDirectories(harnesses, projectPath, exists) {
|
|
44560
|
-
const directories = [
|
|
44244
|
+
const directories = [join9(projectPath, SHARED_SKILLS, SKILL_NAME)];
|
|
44561
44245
|
for (const harness of harnesses) {
|
|
44562
44246
|
if (harness.skills === undefined) {
|
|
44563
44247
|
continue;
|
|
44564
44248
|
}
|
|
44565
|
-
const own =
|
|
44566
|
-
if ((!harness.skills.shared || exists(own)) && !directories.includes(
|
|
44567
|
-
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));
|
|
44568
44252
|
}
|
|
44569
44253
|
}
|
|
44570
44254
|
return directories;
|
|
44571
44255
|
}
|
|
44572
44256
|
function everySkillDirectory(projectPath, harnesses) {
|
|
44573
|
-
const directories = new Set([
|
|
44257
|
+
const directories = new Set([join9(projectPath, SHARED_SKILLS, SKILL_NAME)]);
|
|
44574
44258
|
for (const harness of harnesses) {
|
|
44575
44259
|
if (harness.skills !== undefined) {
|
|
44576
|
-
directories.add(
|
|
44260
|
+
directories.add(join9(projectPath, harness.skills.dir, SKILL_NAME));
|
|
44577
44261
|
}
|
|
44578
44262
|
}
|
|
44579
44263
|
return [...directories];
|
|
@@ -44581,52 +44265,52 @@ function everySkillDirectory(projectPath, harnesses) {
|
|
|
44581
44265
|
function removeSkill(directories, projectPath) {
|
|
44582
44266
|
const removed = [];
|
|
44583
44267
|
for (const directory of directories) {
|
|
44584
|
-
if (
|
|
44585
|
-
|
|
44586
|
-
pruneEmpty(
|
|
44268
|
+
if (existsSync8(directory)) {
|
|
44269
|
+
rmSync5(directory, { recursive: true, force: true });
|
|
44270
|
+
pruneEmpty(dirname5(directory), projectPath);
|
|
44587
44271
|
removed.push(directory);
|
|
44588
44272
|
}
|
|
44589
44273
|
}
|
|
44590
44274
|
return removed;
|
|
44591
44275
|
}
|
|
44592
44276
|
function pruneEmpty(directory, projectPath) {
|
|
44593
|
-
const shared =
|
|
44277
|
+
const shared = join9(projectPath, ".agents");
|
|
44594
44278
|
let path = directory;
|
|
44595
44279
|
while (path !== projectPath && (basename2(path) === "skills" || path === shared)) {
|
|
44596
|
-
if (
|
|
44280
|
+
if (readdirSync4(path).length > 0) {
|
|
44597
44281
|
return;
|
|
44598
44282
|
}
|
|
44599
|
-
|
|
44600
|
-
path =
|
|
44283
|
+
rmSync5(path, { recursive: true, force: true });
|
|
44284
|
+
path = dirname5(path);
|
|
44601
44285
|
}
|
|
44602
44286
|
}
|
|
44603
44287
|
function writeSkill(directories, version) {
|
|
44604
44288
|
const files = skillFiles(version);
|
|
44605
44289
|
return directories.map((directory) => {
|
|
44606
|
-
const replaced =
|
|
44607
|
-
|
|
44290
|
+
const replaced = existsSync8(directory);
|
|
44291
|
+
rmSync5(directory, { recursive: true, force: true });
|
|
44608
44292
|
for (const [name, contents] of files) {
|
|
44609
|
-
const path =
|
|
44610
|
-
|
|
44611
|
-
|
|
44293
|
+
const path = join9(directory, name);
|
|
44294
|
+
mkdirSync5(dirname5(path), { recursive: true });
|
|
44295
|
+
writeFileSync6(path, contents, "utf8");
|
|
44612
44296
|
}
|
|
44613
44297
|
return { path: directory, replaced };
|
|
44614
44298
|
});
|
|
44615
44299
|
}
|
|
44616
44300
|
|
|
44617
44301
|
// src/version.ts
|
|
44618
|
-
import { readFileSync as
|
|
44619
|
-
import { dirname as
|
|
44620
|
-
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";
|
|
44621
44305
|
function getLocalVersion() {
|
|
44622
|
-
const currentDirectory =
|
|
44306
|
+
const currentDirectory = dirname6(fileURLToPath2(import.meta.url));
|
|
44623
44307
|
const packageCandidates = [
|
|
44624
|
-
|
|
44625
|
-
|
|
44308
|
+
join10(currentDirectory, "..", "package.json"),
|
|
44309
|
+
join10(currentDirectory, "..", "..", "package.json")
|
|
44626
44310
|
];
|
|
44627
44311
|
for (const packagePath of packageCandidates) {
|
|
44628
44312
|
try {
|
|
44629
|
-
const value = JSON.parse(
|
|
44313
|
+
const value = JSON.parse(readFileSync8(packagePath, "utf-8"));
|
|
44630
44314
|
if (typeof value === "object" && value !== null && "name" in value && value.name === "gdharness" && "version" in value && typeof value.version === "string") {
|
|
44631
44315
|
return value.version;
|
|
44632
44316
|
}
|
|
@@ -44639,12 +44323,12 @@ function getLocalVersion() {
|
|
|
44639
44323
|
var args = process.argv.slice(2);
|
|
44640
44324
|
var command = args[0];
|
|
44641
44325
|
|
|
44642
|
-
class UsageError extends
|
|
44326
|
+
class UsageError extends Refusal {
|
|
44643
44327
|
}
|
|
44644
44328
|
function projectArgument(at) {
|
|
44645
44329
|
const named = args.slice(at).find((value) => !value.startsWith("--"));
|
|
44646
44330
|
const projectPath = resolve6(named ?? ".");
|
|
44647
|
-
if (!
|
|
44331
|
+
if (!existsSync15(join22(projectPath, "project.godot"))) {
|
|
44648
44332
|
throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
|
|
44649
44333
|
}
|
|
44650
44334
|
return projectPath;
|
|
@@ -44652,18 +44336,18 @@ function projectArgument(at) {
|
|
|
44652
44336
|
async function engine(projectPath) {
|
|
44653
44337
|
const located = await new GodotLocator().find();
|
|
44654
44338
|
if (located.ok) {
|
|
44655
|
-
return { godotPath: located.path, script: shippedOperationsScript(), debug:
|
|
44339
|
+
return { godotPath: located.path, script: shippedOperationsScript(), debug: GODOT_DEBUG_MODE_DEFAULT };
|
|
44656
44340
|
}
|
|
44657
44341
|
const recorded = located.named === null && projectPath !== undefined ? recordedEnginePath(projectPath) : null;
|
|
44658
44342
|
if (recorded === null) {
|
|
44659
44343
|
const refusal = GodotLocator.refusal(located);
|
|
44660
44344
|
throw new UsageError(`${refusal.message} ${refusal.advice.join(". ")}.`);
|
|
44661
44345
|
}
|
|
44662
|
-
return { godotPath: recorded, script: shippedOperationsScript(), debug:
|
|
44346
|
+
return { godotPath: recorded, script: shippedOperationsScript(), debug: GODOT_DEBUG_MODE_DEFAULT };
|
|
44663
44347
|
}
|
|
44664
44348
|
function said2(outcome, what) {
|
|
44665
44349
|
if (!outcome.ok) {
|
|
44666
|
-
throw new
|
|
44350
|
+
throw new Refusal(`${what}: ${outcome.message}`);
|
|
44667
44351
|
}
|
|
44668
44352
|
for (const entry of outcome.messages) {
|
|
44669
44353
|
console.error(` engine ${entry.severity}: ${entry.text}`);
|
|
@@ -44679,11 +44363,11 @@ async function registerRuntime(godot, projectPath) {
|
|
|
44679
44363
|
}
|
|
44680
44364
|
if (autoloadIsOurs(named)) {
|
|
44681
44365
|
said2(await setRuntime(godot, projectPath, true), "registering the runtime autoload");
|
|
44682
|
-
console.log(`${RUNTIME_AUTOLOAD.name} autoload registered at res://${RUNTIME_AUTOLOAD.path}`);
|
|
44366
|
+
console.log(`${RUNTIME_AUTOLOAD.name} autoload ${named === null ? "registered" : "already registered"} at res://${RUNTIME_AUTOLOAD.path}`);
|
|
44683
44367
|
return;
|
|
44684
44368
|
}
|
|
44685
44369
|
console.log(`${RUNTIME_AUTOLOAD.name} autoload left at ${named}, which this project registers itself`);
|
|
44686
|
-
if (!
|
|
44370
|
+
if (!existsSync15(join22(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
|
|
44687
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}`);
|
|
44688
44372
|
}
|
|
44689
44373
|
}
|
|
@@ -44815,7 +44499,7 @@ async function setup() {
|
|
|
44815
44499
|
console.log("no harness configured; name one yourself, and gdharness harnesses lists them all");
|
|
44816
44500
|
}
|
|
44817
44501
|
if (!args.includes("--no-skill")) {
|
|
44818
|
-
const directories = skillDirectories(chosen.harnesses, projectPath,
|
|
44502
|
+
const directories = skillDirectories(chosen.harnesses, projectPath, existsSync15);
|
|
44819
44503
|
for (const written of writeSkill(directories, getLocalVersion())) {
|
|
44820
44504
|
console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
|
|
44821
44505
|
}
|
|
@@ -44909,7 +44593,7 @@ async function upgrade() {
|
|
|
44909
44593
|
moved.push(took.from);
|
|
44910
44594
|
}
|
|
44911
44595
|
}
|
|
44912
|
-
for (const written of writeSkill(skillDirectories(already, projectPath,
|
|
44596
|
+
for (const written of writeSkill(skillDirectories(already, projectPath, existsSync15), version)) {
|
|
44913
44597
|
console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
|
|
44914
44598
|
}
|
|
44915
44599
|
console.log(`
|
|
@@ -44941,7 +44625,7 @@ function doctorReport(projectPath) {
|
|
|
44941
44625
|
process.exitCode = 1;
|
|
44942
44626
|
}
|
|
44943
44627
|
}
|
|
44944
|
-
async function
|
|
44628
|
+
async function runtime2() {
|
|
44945
44629
|
const state = args[1];
|
|
44946
44630
|
if (state !== "on" && state !== "off") {
|
|
44947
44631
|
throw new UsageError("gdharness runtime takes on or off, then the project directory.");
|
|
@@ -45050,7 +44734,7 @@ async function main() {
|
|
|
45050
44734
|
doctorReport(projectArgument(1));
|
|
45051
44735
|
return;
|
|
45052
44736
|
case "runtime":
|
|
45053
|
-
await
|
|
44737
|
+
await runtime2();
|
|
45054
44738
|
return;
|
|
45055
44739
|
case "classes":
|
|
45056
44740
|
await classes();
|
|
@@ -45079,7 +44763,7 @@ async function main() {
|
|
|
45079
44763
|
}
|
|
45080
44764
|
}
|
|
45081
44765
|
await main().catch((error) => {
|
|
45082
|
-
if (error instanceof
|
|
44766
|
+
if (error instanceof Refusal) {
|
|
45083
44767
|
console.error("gdharness:", error.message);
|
|
45084
44768
|
process.exit(error instanceof UsageError ? 2 : 1);
|
|
45085
44769
|
}
|