gdharness 1.0.12 → 1.0.13
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 +193 -50
- package/build/godot/addons/gdharness_editor/bridge_client.gd +18 -2
- package/build/index.js +119 -24
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -7531,11 +7531,20 @@ var init_update_check = __esm(() => {
|
|
|
7531
7531
|
|
|
7532
7532
|
// src/server-version.ts
|
|
7533
7533
|
import { readFileSync as readFileSync3 } from "node:fs";
|
|
7534
|
-
function
|
|
7535
|
-
if (
|
|
7534
|
+
function editorIsStale(addonVersion, serverVersion, addonDigest, shippedDigest) {
|
|
7535
|
+
if (addonDigest !== undefined && addonDigest !== "" && shippedDigest !== undefined) {
|
|
7536
|
+
return addonDigest !== shippedDigest;
|
|
7537
|
+
}
|
|
7538
|
+
return addonVersion !== serverVersion;
|
|
7539
|
+
}
|
|
7540
|
+
function addonMismatch(addonVersion, serverVersion, addonDigest, shippedDigest) {
|
|
7541
|
+
if (!editorIsStale(addonVersion, serverVersion, addonDigest, shippedDigest)) {
|
|
7536
7542
|
return;
|
|
7537
7543
|
}
|
|
7538
7544
|
const reported = addonVersion ?? "";
|
|
7545
|
+
if (reported === serverVersion) {
|
|
7546
|
+
return `The editor is running a different build of the ${reported} addon from the one this server ships. Restart it with editor_launch restart to pick this one up.`;
|
|
7547
|
+
}
|
|
7539
7548
|
const editor = reported === "" ? UNVERSIONED : `the ${reported} addon`;
|
|
7540
7549
|
const both = `The editor is running ${editor} while this server ships ${serverVersion}.`;
|
|
7541
7550
|
if (reported !== "" && isNewer(reported, serverVersion)) {
|
|
@@ -7543,8 +7552,8 @@ function addonMismatch(addonVersion, serverVersion) {
|
|
|
7543
7552
|
}
|
|
7544
7553
|
return `${both} Restart it with editor_launch restart to pick the new one up.`;
|
|
7545
7554
|
}
|
|
7546
|
-
function markIfStale(answer, addonVersion, serverVersion) {
|
|
7547
|
-
const staleNote = addonMismatch(addonVersion, serverVersion);
|
|
7555
|
+
function markIfStale(answer, addonVersion, serverVersion, addonDigest, shippedDigest) {
|
|
7556
|
+
const staleNote = addonMismatch(addonVersion, serverVersion, addonDigest, shippedDigest);
|
|
7548
7557
|
if (staleNote === undefined || typeof answer !== "object" || answer === null || Array.isArray(answer)) {
|
|
7549
7558
|
return answer;
|
|
7550
7559
|
}
|
|
@@ -7690,6 +7699,19 @@ function cacheWrittenAt(projectPath) {
|
|
|
7690
7699
|
return null;
|
|
7691
7700
|
}
|
|
7692
7701
|
}
|
|
7702
|
+
function declaredSince(projectPath, paths, names, since) {
|
|
7703
|
+
return names.filter((name) => {
|
|
7704
|
+
const path = paths.get(name);
|
|
7705
|
+
if (path === undefined || !path.startsWith("res://")) {
|
|
7706
|
+
return false;
|
|
7707
|
+
}
|
|
7708
|
+
try {
|
|
7709
|
+
return statSync2(join6(projectPath, path.slice("res://".length))).mtimeMs > since;
|
|
7710
|
+
} catch {
|
|
7711
|
+
return false;
|
|
7712
|
+
}
|
|
7713
|
+
});
|
|
7714
|
+
}
|
|
7693
7715
|
function cachedAtMissingPaths(cached, projectPath) {
|
|
7694
7716
|
return [...cached].filter(([, path]) => path.startsWith("res://") && !existsSync6(join6(projectPath, path.slice("res://".length)))).map(([name]) => name).sort();
|
|
7695
7717
|
}
|
|
@@ -15308,8 +15330,57 @@ function readParams(params, key) {
|
|
|
15308
15330
|
}
|
|
15309
15331
|
|
|
15310
15332
|
// src/setup.ts
|
|
15311
|
-
import {
|
|
15333
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
15334
|
+
import {
|
|
15335
|
+
cpSync as cpSync2,
|
|
15336
|
+
existsSync as existsSync8,
|
|
15337
|
+
mkdirSync as mkdirSync4,
|
|
15338
|
+
readdirSync as readdirSync4,
|
|
15339
|
+
readFileSync as readFileSync8,
|
|
15340
|
+
rmSync as rmSync4,
|
|
15341
|
+
statSync as statSync4,
|
|
15342
|
+
writeFileSync as writeFileSync6
|
|
15343
|
+
} from "node:fs";
|
|
15312
15344
|
import { dirname as dirname4, join as join9 } from "node:path";
|
|
15345
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
15346
|
+
function editorAddonDigest2(from = shippedAddonsDirectory2()) {
|
|
15347
|
+
const hash = createHash3("sha256");
|
|
15348
|
+
const walk = (directory, relative) => {
|
|
15349
|
+
for (const name of readdirSync4(directory).sort()) {
|
|
15350
|
+
if (name === VERSION_MARKER2 || name === DIGEST_MARKER2) {
|
|
15351
|
+
continue;
|
|
15352
|
+
}
|
|
15353
|
+
const path = join9(directory, name);
|
|
15354
|
+
const inside = `${relative}/${name}`;
|
|
15355
|
+
if (statSync4(path).isDirectory()) {
|
|
15356
|
+
walk(path, inside);
|
|
15357
|
+
} else {
|
|
15358
|
+
hash.update(`${inside}
|
|
15359
|
+
`);
|
|
15360
|
+
hash.update(readFileSync8(path));
|
|
15361
|
+
hash.update(`
|
|
15362
|
+
`);
|
|
15363
|
+
}
|
|
15364
|
+
}
|
|
15365
|
+
};
|
|
15366
|
+
for (const name of EDITOR_PLUGINS2) {
|
|
15367
|
+
walk(join9(from, name), name);
|
|
15368
|
+
}
|
|
15369
|
+
return hash.digest("hex");
|
|
15370
|
+
}
|
|
15371
|
+
function shippedEditorDigest() {
|
|
15372
|
+
if (shippedDigest === undefined) {
|
|
15373
|
+
try {
|
|
15374
|
+
shippedDigest = editorAddonDigest2();
|
|
15375
|
+
} catch {
|
|
15376
|
+
shippedDigest = null;
|
|
15377
|
+
}
|
|
15378
|
+
}
|
|
15379
|
+
return shippedDigest ?? undefined;
|
|
15380
|
+
}
|
|
15381
|
+
function shippedAddonsDirectory2() {
|
|
15382
|
+
return join9(dirname4(fileURLToPath2(import.meta.url)), "godot", "addons");
|
|
15383
|
+
}
|
|
15313
15384
|
function installedAddonVersion(projectPath) {
|
|
15314
15385
|
const marker = join9(projectPath, "addons", ADDONS2[0], VERSION_MARKER2);
|
|
15315
15386
|
if (!existsSync8(marker)) {
|
|
@@ -15318,13 +15389,14 @@ function installedAddonVersion(projectPath) {
|
|
|
15318
15389
|
const said = readFileSync8(marker, "utf8").trim();
|
|
15319
15390
|
return said === "" ? null : said;
|
|
15320
15391
|
}
|
|
15321
|
-
var ADDONS2, RUNTIME_AUTOLOAD2, SCRIPT_RUNS_SETTING = "gdharness/runtime/serve_script_runs", BIND_ADDRESS_SETTING = "gdharness/runtime/bind_address", PORT_SETTING = "gdharness/runtime/port", VERSION_MARKER2 = ".gdharness-version";
|
|
15392
|
+
var ADDONS2, EDITOR_PLUGINS2, RUNTIME_AUTOLOAD2, SCRIPT_RUNS_SETTING = "gdharness/runtime/serve_script_runs", BIND_ADDRESS_SETTING = "gdharness/runtime/bind_address", PORT_SETTING = "gdharness/runtime/port", VERSION_MARKER2 = ".gdharness-version", DIGEST_MARKER2 = ".gdharness-digest", shippedDigest;
|
|
15322
15393
|
var init_setup = __esm(() => {
|
|
15323
15394
|
init_class_cache();
|
|
15324
15395
|
init_headless();
|
|
15325
15396
|
init_resources();
|
|
15326
15397
|
init_server_version();
|
|
15327
15398
|
ADDONS2 = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
|
|
15399
|
+
EDITOR_PLUGINS2 = ["gdharness_editor", "auto_reload"];
|
|
15328
15400
|
RUNTIME_AUTOLOAD2 = {
|
|
15329
15401
|
name: "GdharnessRuntime",
|
|
15330
15402
|
path: "addons/gdharness_runtime/runtime_autoload.gd"
|
|
@@ -31490,7 +31562,7 @@ var init_process_children = __esm(() => {
|
|
|
31490
31562
|
});
|
|
31491
31563
|
|
|
31492
31564
|
// src/runtime-client.ts
|
|
31493
|
-
import { existsSync as existsSync10, readdirSync as
|
|
31565
|
+
import { existsSync as existsSync10, readdirSync as readdirSync6, readFileSync as readFileSync10, statSync as statSync5, unlinkSync } from "node:fs";
|
|
31494
31566
|
import { createConnection } from "node:net";
|
|
31495
31567
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
31496
31568
|
import { dirname as dirname7, join as join12, resolve as resolve3 } from "node:path";
|
|
@@ -31627,7 +31699,7 @@ function announcedIn(directory) {
|
|
|
31627
31699
|
const found = [];
|
|
31628
31700
|
const candidates = [];
|
|
31629
31701
|
const entries = [];
|
|
31630
|
-
for (const entry of
|
|
31702
|
+
for (const entry of readdirSync6(directory)) {
|
|
31631
31703
|
const report = ERROR_REPORT_PATTERN.exec(entry);
|
|
31632
31704
|
if (report) {
|
|
31633
31705
|
sweepErrorReport(join12(directory, entry), Number.parseInt(report[1] ?? "", 10));
|
|
@@ -31643,7 +31715,7 @@ function announcedIn(directory) {
|
|
|
31643
31715
|
entries.push({ file, pid, alive });
|
|
31644
31716
|
if (alive) {
|
|
31645
31717
|
try {
|
|
31646
|
-
candidates.push({ file, pid, writtenAt:
|
|
31718
|
+
candidates.push({ file, pid, writtenAt: statSync5(file).mtimeMs });
|
|
31647
31719
|
} catch {}
|
|
31648
31720
|
}
|
|
31649
31721
|
}
|
|
@@ -31700,7 +31772,7 @@ function sweepErrorReport(path, pid, now = Date.now()) {
|
|
|
31700
31772
|
return;
|
|
31701
31773
|
}
|
|
31702
31774
|
try {
|
|
31703
|
-
if (now -
|
|
31775
|
+
if (now - statSync5(path).mtimeMs >= ERROR_REPORT_KEEP_MS) {
|
|
31704
31776
|
unlinkSync(path);
|
|
31705
31777
|
}
|
|
31706
31778
|
} catch {}
|
|
@@ -32879,7 +32951,7 @@ import {
|
|
|
32879
32951
|
readFileSync as readFileSync16,
|
|
32880
32952
|
readSync,
|
|
32881
32953
|
rmSync as rmSync8,
|
|
32882
|
-
statSync as
|
|
32954
|
+
statSync as statSync6,
|
|
32883
32955
|
writeFileSync as writeFileSync12
|
|
32884
32956
|
} from "node:fs";
|
|
32885
32957
|
import { dirname as dirname12, join as join18 } from "node:path";
|
|
@@ -32942,7 +33014,7 @@ function editorConsole(projectPath, editorPid, openedByAServer) {
|
|
|
32942
33014
|
return { log, path, ...of === undefined ? {} : { readBytes: text.length, of } };
|
|
32943
33015
|
}
|
|
32944
33016
|
function theFirstOf(path) {
|
|
32945
|
-
const whole =
|
|
33017
|
+
const whole = statSync6(path).size;
|
|
32946
33018
|
if (whole <= MOST_CONSOLE_BYTES) {
|
|
32947
33019
|
return { text: readFileSync16(path, "utf8") };
|
|
32948
33020
|
}
|
|
@@ -36160,6 +36232,7 @@ var init_godot_bridge = __esm(() => {
|
|
|
36160
36232
|
connectedAt: this.connectionInfo?.connectedAt,
|
|
36161
36233
|
lastPongAt: this.connectionInfo?.lastPongAt,
|
|
36162
36234
|
addonVersion: this.connectionInfo?.addonVersion,
|
|
36235
|
+
addonDigest: this.connectionInfo?.addonDigest,
|
|
36163
36236
|
editorPid: this.connectionInfo?.editorPid,
|
|
36164
36237
|
lspPort: this.connectionInfo?.lspPort,
|
|
36165
36238
|
dapPort: this.connectionInfo?.dapPort,
|
|
@@ -36303,6 +36376,7 @@ var init_godot_bridge = __esm(() => {
|
|
|
36303
36376
|
}
|
|
36304
36377
|
this.connectionInfo.projectPath = message.project_path;
|
|
36305
36378
|
this.connectionInfo.addonVersion = message.addon_version ?? "";
|
|
36379
|
+
this.connectionInfo.addonDigest = message.addon_digest;
|
|
36306
36380
|
this.connectionInfo.editorPid = message.editor_pid;
|
|
36307
36381
|
this.connectionInfo.lspPort = servedPort(message.lsp_port);
|
|
36308
36382
|
this.connectionInfo.dapPort = servedPort(message.dap_port);
|
|
@@ -36556,7 +36630,7 @@ var init_godot_path = __esm(() => {
|
|
|
36556
36630
|
});
|
|
36557
36631
|
|
|
36558
36632
|
// src/issues.ts
|
|
36559
|
-
import { createHash as
|
|
36633
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
36560
36634
|
import process7 from "node:process";
|
|
36561
36635
|
function feedbackNotice() {
|
|
36562
36636
|
return JSON.stringify({
|
|
@@ -36574,7 +36648,7 @@ function runtime2() {
|
|
|
36574
36648
|
function defectSignature2(where, message) {
|
|
36575
36649
|
const normalised = `${where}
|
|
36576
36650
|
${message}`.toLowerCase().replaceAll("\\", "/").replaceAll(/(?:[a-z]:)?(?:\/+[\w.-]+){2,}/gu, "<path>").replaceAll(/\d+/gu, "0");
|
|
36577
|
-
return
|
|
36651
|
+
return createHash4("sha256").update(normalised).digest("hex").slice(0, 8);
|
|
36578
36652
|
}
|
|
36579
36653
|
function facts2(where, message, godotVersion) {
|
|
36580
36654
|
return [
|
|
@@ -36911,7 +36985,7 @@ import { realpathSync as realpathSync2 } from "node:fs";
|
|
|
36911
36985
|
import { readFile, realpath } from "node:fs/promises";
|
|
36912
36986
|
import { createConnection as createConnection3 } from "node:net";
|
|
36913
36987
|
import { dirname as dirname13, resolve as resolve4 } from "node:path";
|
|
36914
|
-
import { fileURLToPath as
|
|
36988
|
+
import { fileURLToPath as fileURLToPath4, pathToFileURL } from "node:url";
|
|
36915
36989
|
function diagnosticsKey(uri) {
|
|
36916
36990
|
let decoded;
|
|
36917
36991
|
try {
|
|
@@ -36923,7 +36997,7 @@ function diagnosticsKey(uri) {
|
|
|
36923
36997
|
return decoded;
|
|
36924
36998
|
}
|
|
36925
36999
|
try {
|
|
36926
|
-
return realpathSync2.native(
|
|
37000
|
+
return realpathSync2.native(fileURLToPath4(decoded)).toLowerCase();
|
|
36927
37001
|
} catch {
|
|
36928
37002
|
return decoded.toLowerCase();
|
|
36929
37003
|
}
|
|
@@ -37483,7 +37557,7 @@ var init_process_time = __esm(() => {
|
|
|
37483
37557
|
});
|
|
37484
37558
|
|
|
37485
37559
|
// src/project-scan.ts
|
|
37486
|
-
import { existsSync as existsSync14, readdirSync as
|
|
37560
|
+
import { existsSync as existsSync14, readdirSync as readdirSync7, readFileSync as readFileSync17 } from "node:fs";
|
|
37487
37561
|
import { join as join19 } from "node:path";
|
|
37488
37562
|
function skipped(name) {
|
|
37489
37563
|
return name.startsWith(".") || SKIPPED.has(name);
|
|
@@ -37497,7 +37571,7 @@ function projectStructure(projectPath) {
|
|
|
37497
37571
|
if (steppedOver(directory)) {
|
|
37498
37572
|
return;
|
|
37499
37573
|
}
|
|
37500
|
-
for (const entry of
|
|
37574
|
+
for (const entry of readdirSync7(directory, { withFileTypes: true })) {
|
|
37501
37575
|
if (skipped(entry.name)) {
|
|
37502
37576
|
continue;
|
|
37503
37577
|
}
|
|
@@ -37526,7 +37600,7 @@ function scriptsWithoutUid(projectPath) {
|
|
|
37526
37600
|
if (steppedOver(directory)) {
|
|
37527
37601
|
return;
|
|
37528
37602
|
}
|
|
37529
|
-
for (const entry of
|
|
37603
|
+
for (const entry of readdirSync7(directory, { withFileTypes: true })) {
|
|
37530
37604
|
if (skipped(entry.name)) {
|
|
37531
37605
|
continue;
|
|
37532
37606
|
}
|
|
@@ -37567,7 +37641,7 @@ function searchProject(projectPath, options) {
|
|
|
37567
37641
|
if (steppedOver(directory)) {
|
|
37568
37642
|
return;
|
|
37569
37643
|
}
|
|
37570
|
-
for (const entry of
|
|
37644
|
+
for (const entry of readdirSync7(directory, { withFileTypes: true })) {
|
|
37571
37645
|
if (full()) {
|
|
37572
37646
|
return;
|
|
37573
37647
|
}
|
|
@@ -37657,7 +37731,7 @@ var init_restart_note = __esm(() => {
|
|
|
37657
37731
|
|
|
37658
37732
|
// src/run-record.ts
|
|
37659
37733
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
37660
|
-
import { mkdirSync as mkdirSync12, openSync as openSync2, readdirSync as
|
|
37734
|
+
import { mkdirSync as mkdirSync12, openSync as openSync2, readdirSync as readdirSync8, readFileSync as readFileSync19, rmSync as rmSync10, statSync as statSync7, writeFileSync as writeFileSync14 } from "node:fs";
|
|
37661
37735
|
import { basename as basename3, join as join21 } from "node:path";
|
|
37662
37736
|
function runsDirectory() {
|
|
37663
37737
|
return join21(runtimeDirectory(), "runs");
|
|
@@ -37912,7 +37986,7 @@ function clearRunRecord(ours = () => true) {
|
|
|
37912
37986
|
}
|
|
37913
37987
|
function ageMs(path, now) {
|
|
37914
37988
|
try {
|
|
37915
|
-
return now -
|
|
37989
|
+
return now - statSync7(path).mtimeMs;
|
|
37916
37990
|
} catch {
|
|
37917
37991
|
return 0;
|
|
37918
37992
|
}
|
|
@@ -37922,7 +37996,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
37922
37996
|
const keep = readRunRecord()?.transcript;
|
|
37923
37997
|
let entries;
|
|
37924
37998
|
try {
|
|
37925
|
-
entries =
|
|
37999
|
+
entries = readdirSync8(directory);
|
|
37926
38000
|
} catch {
|
|
37927
38001
|
return;
|
|
37928
38002
|
}
|
|
@@ -37957,16 +38031,16 @@ import {
|
|
|
37957
38031
|
mkdirSync as mkdirSync13,
|
|
37958
38032
|
mkdtempSync as mkdtempSync3,
|
|
37959
38033
|
openSync as openSync3,
|
|
37960
|
-
readdirSync as
|
|
38034
|
+
readdirSync as readdirSync9,
|
|
37961
38035
|
readFileSync as readFileSync20,
|
|
37962
38036
|
readSync as readSync2,
|
|
37963
38037
|
rmdirSync,
|
|
37964
|
-
statSync as
|
|
38038
|
+
statSync as statSync8
|
|
37965
38039
|
} from "node:fs";
|
|
37966
38040
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
37967
38041
|
import { basename as basename4, dirname as dirname15, isAbsolute as isAbsolute3, join as join22, normalize as normalize3, relative as relative3 } from "node:path";
|
|
37968
38042
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
37969
|
-
import { fileURLToPath as
|
|
38043
|
+
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
37970
38044
|
import { promisify as promisify7 } from "node:util";
|
|
37971
38045
|
function patienceForFrames(frames, atLeast) {
|
|
37972
38046
|
return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
|
|
@@ -38212,7 +38286,7 @@ function elsewhereIn(projectPath, asked) {
|
|
|
38212
38286
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
38213
38287
|
let near = [];
|
|
38214
38288
|
try {
|
|
38215
|
-
near =
|
|
38289
|
+
near = readdirSync9(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()));
|
|
38216
38290
|
} catch {
|
|
38217
38291
|
return "";
|
|
38218
38292
|
}
|
|
@@ -39372,7 +39446,7 @@ class GodotServer {
|
|
|
39372
39446
|
let report = null;
|
|
39373
39447
|
let reportProblem = null;
|
|
39374
39448
|
try {
|
|
39375
|
-
const written = existsSync15(reportsDir) ?
|
|
39449
|
+
const written = existsSync15(reportsDir) ? readdirSync9(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
39376
39450
|
const newest = written.at(-1);
|
|
39377
39451
|
if (newest !== undefined) {
|
|
39378
39452
|
report = parseJUnit(readFileSync20(join22(reportsDir, newest, "results.xml"), "utf8"));
|
|
@@ -39501,7 +39575,7 @@ class GodotServer {
|
|
|
39501
39575
|
warnings: diagnostics.length - errors,
|
|
39502
39576
|
diagnostics,
|
|
39503
39577
|
...disagrees
|
|
39504
|
-
}, this.godotBridge.getStatus().addonVersion, SERVER_VERSION));
|
|
39578
|
+
}, this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest()));
|
|
39505
39579
|
}
|
|
39506
39580
|
whereTheFileDisagrees(diagnostics, args) {
|
|
39507
39581
|
const messages = diagnostics.map((entry) => asParams(entry)["message"]).filter((message) => typeof message === "string");
|
|
@@ -39688,7 +39762,7 @@ class GodotServer {
|
|
|
39688
39762
|
getEditorStatusPayload() {
|
|
39689
39763
|
const status = this.godotBridge.getStatus();
|
|
39690
39764
|
const isPortConflict = this.bridgeStartupError?.includes("EADDRINUSE") ?? false;
|
|
39691
|
-
const stale = status.connected && status.addonVersion
|
|
39765
|
+
const stale = status.connected && editorIsStale(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest());
|
|
39692
39766
|
const unfinished = this.restartLeftUnfinished(status.connected);
|
|
39693
39767
|
return {
|
|
39694
39768
|
...status,
|
|
@@ -39705,7 +39779,7 @@ class GodotServer {
|
|
|
39705
39779
|
awaitingLaunchedEditor: status.connected || !this.launchedEditorIsUp() ? undefined : this.launchedEditor?.pid ?? undefined,
|
|
39706
39780
|
...status.connected ? this.whatTheLaunchedEditorDropped() : {},
|
|
39707
39781
|
startupError: this.bridgeStartupError,
|
|
39708
|
-
staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
|
|
39782
|
+
staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest()) : undefined,
|
|
39709
39783
|
...this.breakpointsAtRisk(),
|
|
39710
39784
|
retryingBridge: this.bridgeRetry === null ? undefined : true,
|
|
39711
39785
|
announcedAt: this.announcedAt ?? undefined,
|
|
@@ -39837,8 +39911,8 @@ class GodotServer {
|
|
|
39837
39911
|
editorPid: now.editorPid,
|
|
39838
39912
|
addonVersion: now.addonVersion,
|
|
39839
39913
|
serverVersion: SERVER_VERSION,
|
|
39840
|
-
addonIsStale: now.addonVersion
|
|
39841
|
-
staleNote: addonMismatch(now.addonVersion, SERVER_VERSION),
|
|
39914
|
+
addonIsStale: editorIsStale(now.addonVersion, SERVER_VERSION, now.addonDigest, shippedEditorDigest()),
|
|
39915
|
+
staleNote: addonMismatch(now.addonVersion, SERVER_VERSION, now.addonDigest, shippedEditorDigest()),
|
|
39842
39916
|
...this.whatTheEditorDropped(settingsBefore, after),
|
|
39843
39917
|
tookMs: Date.now() - began
|
|
39844
39918
|
});
|
|
@@ -40115,7 +40189,7 @@ class GodotServer {
|
|
|
40115
40189
|
}
|
|
40116
40190
|
let announcedAt;
|
|
40117
40191
|
try {
|
|
40118
|
-
announcedAt =
|
|
40192
|
+
announcedAt = statSync8(endpoint.file).mtimeMs;
|
|
40119
40193
|
} catch {
|
|
40120
40194
|
return;
|
|
40121
40195
|
}
|
|
@@ -40725,7 +40799,15 @@ class GodotServer {
|
|
|
40725
40799
|
"editor_launch opens one, and an editor opened here has its console captured"
|
|
40726
40800
|
]);
|
|
40727
40801
|
}
|
|
40728
|
-
const
|
|
40802
|
+
const launched = status.connected ? null : this.launchedEditor?.pid ?? null;
|
|
40803
|
+
if (!status.connected && launched === null) {
|
|
40804
|
+
return this.createErrorResponse("No editor is connected, so there is no console to read.", [
|
|
40805
|
+
"editor_status says whether one is connected and whether it may yet be",
|
|
40806
|
+
"editor_launch opens one, and an editor opened here has its console captured"
|
|
40807
|
+
]);
|
|
40808
|
+
}
|
|
40809
|
+
const editorPid = status.connected ? status.editorPid ?? null : launched;
|
|
40810
|
+
const console2 = editorConsole(projectPath, editorPid, status.connected ? status.openedByAServer === true : true);
|
|
40729
40811
|
if ("kind" in console2) {
|
|
40730
40812
|
return this.createErrorResponse(theConsoleWasNotCaptured(console2, projectPath), [
|
|
40731
40813
|
"editor_launch restart replaces this editor with one whose console is captured",
|
|
@@ -40743,8 +40825,9 @@ class GodotServer {
|
|
|
40743
40825
|
const grouped = bursts(everything, readPositiveNumber(args, "before") ?? 3);
|
|
40744
40826
|
const shown = grouped.slice(0, MOST_GROUPS);
|
|
40745
40827
|
return this.jsonTextResponse({
|
|
40746
|
-
editorPid:
|
|
40828
|
+
editorPid: editorPid ?? undefined,
|
|
40747
40829
|
projectPath,
|
|
40830
|
+
...status.connected ? {} : this.launchedEditorIsUp() ? { stillConnecting: true } : { exitedBeforeConnecting: true },
|
|
40748
40831
|
capturedIn: console2.path,
|
|
40749
40832
|
...console2.of === undefined ? {} : {
|
|
40750
40833
|
readFromTheStart: console2.readBytes,
|
|
@@ -41043,6 +41126,7 @@ class GodotServer {
|
|
|
41043
41126
|
}
|
|
41044
41127
|
async rebuildClassCache(projectPath) {
|
|
41045
41128
|
const before = cachedClasses(projectPath);
|
|
41129
|
+
const writtenBefore = cacheWrittenAt(projectPath);
|
|
41046
41130
|
const noted = readClassNote(projectPath);
|
|
41047
41131
|
const rebuilt = await this.operation("refresh_class_cache", {}, projectPath);
|
|
41048
41132
|
if (!rebuilt.ok) {
|
|
@@ -41053,21 +41137,30 @@ class GodotServer {
|
|
|
41053
41137
|
return rebuilt;
|
|
41054
41138
|
}
|
|
41055
41139
|
const lost = noted === null || before === null ? [] : noted.rebuiltWith.filter((name) => !before.has(name) && after.has(name)).sort();
|
|
41056
|
-
const
|
|
41140
|
+
const cameBack = writtenBefore === null ? [] : declaredSince(projectPath, after, lost, writtenBefore);
|
|
41141
|
+
const droppedByTheEditor = lost.filter((name) => !cameBack.includes(name));
|
|
41142
|
+
const seenLosing = droppedByTheEditor.length > 0 && this.godotBridge.isConnected();
|
|
41057
41143
|
writeClassNote(projectPath, {
|
|
41058
41144
|
rebuiltWith: [...after.keys()].sort(),
|
|
41059
41145
|
...seenLosing ? { shortListEditorPid: this.godotBridge.getStatus().editorPid ?? null } : noted?.shortListEditorPid === undefined ? {} : { shortListEditorPid: noted.shortListEditorPid }
|
|
41060
41146
|
});
|
|
41061
|
-
|
|
41147
|
+
const notes = [];
|
|
41148
|
+
if (seenLosing) {
|
|
41149
|
+
const them = droppedByTheEditor.length === 1 ? "it" : "them";
|
|
41150
|
+
notes.push(`The cache held ${droppedByTheEditor.join(", ")} after the last rebuild and not when this one began, so the editor holding this project wrote it without ${them} in between: it holds a list shorter than the files and writes that list on every save and scan, and each rebuild puts ${them} back. editor_launch restart is what ends it.`);
|
|
41151
|
+
}
|
|
41152
|
+
if (cameBack.length > 0) {
|
|
41153
|
+
notes.push(`${cameBack.join(", ")} left the cache while ${cameBack.length === 1 ? "its script was" : "their scripts were"} gone: the script${cameBack.length === 1 ? " is" : "s are"} newer than the cache the editor last wrote, so it listed what was on disk then. editor_rescan picks ${cameBack.length === 1 ? "it" : "them"} up.`);
|
|
41154
|
+
}
|
|
41155
|
+
if (notes.length === 0) {
|
|
41062
41156
|
return rebuilt;
|
|
41063
41157
|
}
|
|
41064
|
-
const them = lost.length === 1 ? "it" : "them";
|
|
41065
41158
|
return {
|
|
41066
41159
|
...rebuilt,
|
|
41067
41160
|
payload: {
|
|
41068
41161
|
...rebuilt.payload,
|
|
41069
41162
|
lostSinceLastRebuild: lost,
|
|
41070
|
-
note:
|
|
41163
|
+
note: notes.join(" ")
|
|
41071
41164
|
}
|
|
41072
41165
|
};
|
|
41073
41166
|
}
|
|
@@ -41143,7 +41236,7 @@ class GodotServer {
|
|
|
41143
41236
|
]);
|
|
41144
41237
|
}
|
|
41145
41238
|
try {
|
|
41146
|
-
return this.jsonTextResponse(markIfStale(await this.godotBridge.invokeTool(toolName, args), this.godotBridge.getStatus().addonVersion, SERVER_VERSION));
|
|
41239
|
+
return this.jsonTextResponse(markIfStale(await this.godotBridge.invokeTool(toolName, args), this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest()));
|
|
41147
41240
|
} catch (error) {
|
|
41148
41241
|
return this.createErrorResponse(`The editor answered ${toolName} with an error: ${errorMessage(error)}`);
|
|
41149
41242
|
}
|
|
@@ -41346,7 +41439,7 @@ var init_server3 = __esm(() => {
|
|
|
41346
41439
|
init_tool_definitions();
|
|
41347
41440
|
init_update_check();
|
|
41348
41441
|
run7 = promisify7(execFile7);
|
|
41349
|
-
__dirname2 = dirname15(
|
|
41442
|
+
__dirname2 = dirname15(fileURLToPath5(import.meta.url));
|
|
41350
41443
|
CONSOLE_WRAPPER = /_console(\.exe)?$/i;
|
|
41351
41444
|
LONGEST_BOOT_NOTED_MS = 5 * 60000;
|
|
41352
41445
|
VALUE_NAMES = {
|
|
@@ -42565,7 +42658,17 @@ init_headless();
|
|
|
42565
42658
|
init_resources();
|
|
42566
42659
|
init_server_version();
|
|
42567
42660
|
import { spawnSync } from "node:child_process";
|
|
42568
|
-
import {
|
|
42661
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
42662
|
+
import {
|
|
42663
|
+
cpSync,
|
|
42664
|
+
existsSync as existsSync7,
|
|
42665
|
+
mkdirSync as mkdirSync3,
|
|
42666
|
+
readdirSync as readdirSync3,
|
|
42667
|
+
readFileSync as readFileSync7,
|
|
42668
|
+
rmSync as rmSync3,
|
|
42669
|
+
statSync as statSync3,
|
|
42670
|
+
writeFileSync as writeFileSync5
|
|
42671
|
+
} from "node:fs";
|
|
42569
42672
|
import { dirname as dirname3, join as join8 } from "node:path";
|
|
42570
42673
|
import { fileURLToPath } from "node:url";
|
|
42571
42674
|
var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
|
|
@@ -42575,6 +42678,40 @@ var RUNTIME_AUTOLOAD = {
|
|
|
42575
42678
|
path: "addons/gdharness_runtime/runtime_autoload.gd"
|
|
42576
42679
|
};
|
|
42577
42680
|
var VERSION_MARKER = ".gdharness-version";
|
|
42681
|
+
var DIGEST_MARKER = ".gdharness-digest";
|
|
42682
|
+
function editorAddonDigest(from = shippedAddonsDirectory()) {
|
|
42683
|
+
const hash = createHash2("sha256");
|
|
42684
|
+
const walk = (directory, relative) => {
|
|
42685
|
+
for (const name of readdirSync3(directory).sort()) {
|
|
42686
|
+
if (name === VERSION_MARKER || name === DIGEST_MARKER) {
|
|
42687
|
+
continue;
|
|
42688
|
+
}
|
|
42689
|
+
const path = join8(directory, name);
|
|
42690
|
+
const inside = `${relative}/${name}`;
|
|
42691
|
+
if (statSync3(path).isDirectory()) {
|
|
42692
|
+
walk(path, inside);
|
|
42693
|
+
} else {
|
|
42694
|
+
hash.update(`${inside}
|
|
42695
|
+
`);
|
|
42696
|
+
hash.update(readFileSync7(path));
|
|
42697
|
+
hash.update(`
|
|
42698
|
+
`);
|
|
42699
|
+
}
|
|
42700
|
+
}
|
|
42701
|
+
};
|
|
42702
|
+
for (const name of EDITOR_PLUGINS) {
|
|
42703
|
+
walk(join8(from, name), name);
|
|
42704
|
+
}
|
|
42705
|
+
return hash.digest("hex");
|
|
42706
|
+
}
|
|
42707
|
+
function installedEditorDigest(projectPath) {
|
|
42708
|
+
const marker = join8(projectPath, "addons", ADDONS[0], DIGEST_MARKER);
|
|
42709
|
+
if (!existsSync7(marker)) {
|
|
42710
|
+
return null;
|
|
42711
|
+
}
|
|
42712
|
+
const said = readFileSync7(marker, "utf8").trim();
|
|
42713
|
+
return said === "" ? null : said;
|
|
42714
|
+
}
|
|
42578
42715
|
function shippedAddonsDirectory() {
|
|
42579
42716
|
return join8(dirname3(fileURLToPath(import.meta.url)), "godot", "addons");
|
|
42580
42717
|
}
|
|
@@ -42597,6 +42734,8 @@ function installAddons(projectPath, from = shippedAddonsDirectory()) {
|
|
|
42597
42734
|
`);
|
|
42598
42735
|
installed.push({ name, path: target, replaced });
|
|
42599
42736
|
}
|
|
42737
|
+
writeFileSync5(join8(projectPath, "addons", ADDONS[0], DIGEST_MARKER), `${editorAddonDigest(from)}
|
|
42738
|
+
`);
|
|
42600
42739
|
return installed;
|
|
42601
42740
|
}
|
|
42602
42741
|
function removeAddons(projectPath) {
|
|
@@ -42741,7 +42880,7 @@ function inspectProject(projectPath) {
|
|
|
42741
42880
|
// src/skill.ts
|
|
42742
42881
|
init_setup();
|
|
42743
42882
|
init_tool_definitions();
|
|
42744
|
-
import { existsSync as existsSync9, mkdirSync as mkdirSync5, readdirSync as
|
|
42883
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync5, readdirSync as readdirSync5, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "node:fs";
|
|
42745
42884
|
import { basename, dirname as dirname5, join as join10 } from "node:path";
|
|
42746
42885
|
|
|
42747
42886
|
// src/tool-reference.ts
|
|
@@ -42863,7 +43002,8 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
42863
43002
|
this server is connected to. There is no ambient project, and an argument a tool does not declare is refused
|
|
42864
43003
|
rather than ignored.
|
|
42865
43004
|
- \`editor_status\` says whether an editor is connected and whether its addon matches the server.
|
|
42866
|
-
\`addonIsStale\` means
|
|
43005
|
+
\`addonIsStale\` means the editor is running different addon code from what the server ships
|
|
43006
|
+
(an older \`addonVersion\` on the same code is not stale), and \`staleNote\` says which half is behind: an editor that
|
|
42867
43007
|
needs restarting, or a server that needs reconnecting in your harness. Every answer that came
|
|
42868
43008
|
from the editor carries the same two when they differ, so a stale addon cannot answer quietly:
|
|
42869
43009
|
believe a restart over a diagnostic that arrived with them.
|
|
@@ -43050,7 +43190,7 @@ function pruneEmpty(directory, projectPath) {
|
|
|
43050
43190
|
const shared = join10(projectPath, ".agents");
|
|
43051
43191
|
let path = directory;
|
|
43052
43192
|
while (path !== projectPath && (basename(path) === "skills" || path === shared)) {
|
|
43053
|
-
if (
|
|
43193
|
+
if (readdirSync5(path).length > 0) {
|
|
43054
43194
|
return;
|
|
43055
43195
|
}
|
|
43056
43196
|
rmSync5(path, { recursive: true, force: true });
|
|
@@ -43074,9 +43214,9 @@ function writeSkill(directories, version) {
|
|
|
43074
43214
|
// src/version.ts
|
|
43075
43215
|
import { readFileSync as readFileSync9 } from "node:fs";
|
|
43076
43216
|
import { dirname as dirname6, join as join11 } from "node:path";
|
|
43077
|
-
import { fileURLToPath as
|
|
43217
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
43078
43218
|
function getLocalVersion() {
|
|
43079
|
-
const currentDirectory = dirname6(
|
|
43219
|
+
const currentDirectory = dirname6(fileURLToPath3(import.meta.url));
|
|
43080
43220
|
const packageCandidates = [
|
|
43081
43221
|
join11(currentDirectory, "..", "package.json"),
|
|
43082
43222
|
join11(currentDirectory, "..", "..", "package.json")
|
|
@@ -43338,6 +43478,7 @@ async function upgrade() {
|
|
|
43338
43478
|
}
|
|
43339
43479
|
const godot = await engine(projectPath);
|
|
43340
43480
|
console.log(installed === version ? `already ${version}; reinstalling` : `${installed} -> ${version}`);
|
|
43481
|
+
const digestBefore = installedEditorDigest(projectPath);
|
|
43341
43482
|
for (const addon of installAddons(projectPath)) {
|
|
43342
43483
|
console.log(`${addon.replaced ? "replaced" : "installed"} ${addon.path}`);
|
|
43343
43484
|
}
|
|
@@ -43369,8 +43510,10 @@ async function upgrade() {
|
|
|
43369
43510
|
console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
|
|
43370
43511
|
}
|
|
43371
43512
|
console.log(`
|
|
43372
|
-
On ${version}.
|
|
43373
|
-
console.log(` 1.
|
|
43513
|
+
On ${version}. What is left for you:`);
|
|
43514
|
+
console.log(digestBefore !== null && digestBefore === installedEditorDigest(projectPath) ? ` 1. Nothing for the editor: this upgrade left the editor addons the same code as before,
|
|
43515
|
+
` + ` so it gives an open editor nothing to restart for. editor_status says addonIsStale
|
|
43516
|
+
` + " if the editor is older than that." : ` 1. The open editor is still running the addons it loaded at startup. Restart it with the
|
|
43374
43517
|
` + " editor_launch restart tool, which closes and reopens the window.");
|
|
43375
43518
|
console.log(harnessNote({ moved, written, byHand }, [launch.command, ...launch.args].join(" ")));
|
|
43376
43519
|
}
|
|
@@ -13,6 +13,8 @@ const Read = preload("reading.gd")
|
|
|
13
13
|
const DEFAULT_URL: String = "ws://127.0.0.1:6505/godot"
|
|
14
14
|
## Written beside the addon by the install, so it names the version this copy came from.
|
|
15
15
|
const VERSION_MARKER: String = "res://addons/gdharness_editor/.gdharness-version"
|
|
16
|
+
## Written beside it, kept in step with `DIGEST_MARKER` in src/setup.ts.
|
|
17
|
+
const DIGEST_MARKER: String = "res://addons/gdharness_editor/.gdharness-digest"
|
|
16
18
|
## Written by the server that serves this project, saying where its bridge actually is. Kept in
|
|
17
19
|
## step with `announcementPath` in src/bridge-announce.ts.
|
|
18
20
|
const ANNOUNCEMENT: String = "res://.godot/gdharness-bridge.json"
|
|
@@ -70,6 +72,8 @@ var server_url: String = DEFAULT_URL
|
|
|
70
72
|
## What this copy was when it loaded, which is what the editor is running until it is restarted.
|
|
71
73
|
## See [method _loaded_version] for why it is held rather than read when it is wanted.
|
|
72
74
|
var version_at_load: String = ""
|
|
75
|
+
## The digest of the editor addon code this copy loaded, held for the same reason.
|
|
76
|
+
var digest_at_load: String = ""
|
|
73
77
|
|
|
74
78
|
var _is_connected: bool = false
|
|
75
79
|
var _reconnect_timer: Timer
|
|
@@ -98,6 +102,7 @@ var _connecting_for: float = 0.0
|
|
|
98
102
|
func _ready() -> void:
|
|
99
103
|
_project_path = ProjectSettings.globalize_path("res://")
|
|
100
104
|
version_at_load = _loaded_version()
|
|
105
|
+
digest_at_load = _loaded_digest()
|
|
101
106
|
_keep_breakpoints_through_sessions()
|
|
102
107
|
OS.set_environment(EDITOR_PID_VARIABLE, str(OS.get_process_id()))
|
|
103
108
|
|
|
@@ -290,6 +295,7 @@ func _handle_connect() -> void:
|
|
|
290
295
|
"type": "godot_ready",
|
|
291
296
|
"project_path": _project_path,
|
|
292
297
|
"addon_version": version_at_load,
|
|
298
|
+
"addon_digest": digest_at_load,
|
|
293
299
|
"editor_pid": OS.get_process_id(),
|
|
294
300
|
"lsp_port": _serves(LSP_ASKED, LSP_SETTING),
|
|
295
301
|
"dap_port": _serves(DAP_ASKED, DAP_SETTING),
|
|
@@ -408,9 +414,19 @@ func _serving(setting: String) -> int:
|
|
|
408
414
|
## and `addonIsStale` reporting false over an editor still running the old code, which is exactly
|
|
409
415
|
## what it exists to catch.
|
|
410
416
|
func _loaded_version() -> String:
|
|
411
|
-
|
|
417
|
+
return _read_marker(VERSION_MARKER)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
## The same for the digest of the editor addon code, which an upgrade that leaves the code alone
|
|
421
|
+
## rewrites unchanged, so the server can tell an editor that needs a restart from one that does not.
|
|
422
|
+
func _loaded_digest() -> String:
|
|
423
|
+
return _read_marker(DIGEST_MARKER)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
func _read_marker(path: String) -> String:
|
|
427
|
+
if not FileAccess.file_exists(path):
|
|
412
428
|
return ""
|
|
413
|
-
var file: FileAccess = FileAccess.open(
|
|
429
|
+
var file: FileAccess = FileAccess.open(path, FileAccess.READ)
|
|
414
430
|
if file == null:
|
|
415
431
|
return ""
|
|
416
432
|
var text: String = file.get_as_text().strip_edges()
|
package/build/index.js
CHANGED
|
@@ -10047,11 +10047,20 @@ var SERVER_VERSION = (() => {
|
|
|
10047
10047
|
}
|
|
10048
10048
|
})();
|
|
10049
10049
|
var UNVERSIONED = "an addon from before versions were reported";
|
|
10050
|
-
function
|
|
10051
|
-
if (
|
|
10050
|
+
function editorIsStale(addonVersion, serverVersion, addonDigest, shippedDigest) {
|
|
10051
|
+
if (addonDigest !== undefined && addonDigest !== "" && shippedDigest !== undefined) {
|
|
10052
|
+
return addonDigest !== shippedDigest;
|
|
10053
|
+
}
|
|
10054
|
+
return addonVersion !== serverVersion;
|
|
10055
|
+
}
|
|
10056
|
+
function addonMismatch(addonVersion, serverVersion, addonDigest, shippedDigest) {
|
|
10057
|
+
if (!editorIsStale(addonVersion, serverVersion, addonDigest, shippedDigest)) {
|
|
10052
10058
|
return;
|
|
10053
10059
|
}
|
|
10054
10060
|
const reported = addonVersion ?? "";
|
|
10061
|
+
if (reported === serverVersion) {
|
|
10062
|
+
return `The editor is running a different build of the ${reported} addon from the one this server ships. Restart it with editor_launch restart to pick this one up.`;
|
|
10063
|
+
}
|
|
10055
10064
|
const editor = reported === "" ? UNVERSIONED : `the ${reported} addon`;
|
|
10056
10065
|
const both = `The editor is running ${editor} while this server ships ${serverVersion}.`;
|
|
10057
10066
|
if (reported !== "" && isNewer(reported, serverVersion)) {
|
|
@@ -10059,8 +10068,8 @@ function addonMismatch(addonVersion, serverVersion) {
|
|
|
10059
10068
|
}
|
|
10060
10069
|
return `${both} Restart it with editor_launch restart to pick the new one up.`;
|
|
10061
10070
|
}
|
|
10062
|
-
function markIfStale(answer, addonVersion, serverVersion) {
|
|
10063
|
-
const staleNote = addonMismatch(addonVersion, serverVersion);
|
|
10071
|
+
function markIfStale(answer, addonVersion, serverVersion, addonDigest, shippedDigest) {
|
|
10072
|
+
const staleNote = addonMismatch(addonVersion, serverVersion, addonDigest, shippedDigest);
|
|
10064
10073
|
if (staleNote === undefined || typeof answer !== "object" || answer === null || Array.isArray(answer)) {
|
|
10065
10074
|
return answer;
|
|
10066
10075
|
}
|
|
@@ -10178,16 +10187,16 @@ import {
|
|
|
10178
10187
|
mkdirSync as mkdirSync10,
|
|
10179
10188
|
mkdtempSync as mkdtempSync2,
|
|
10180
10189
|
openSync as openSync3,
|
|
10181
|
-
readdirSync as
|
|
10190
|
+
readdirSync as readdirSync7,
|
|
10182
10191
|
readFileSync as readFileSync16,
|
|
10183
10192
|
readSync as readSync2,
|
|
10184
10193
|
rmdirSync,
|
|
10185
|
-
statSync as
|
|
10194
|
+
statSync as statSync7
|
|
10186
10195
|
} from "node:fs";
|
|
10187
10196
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
10188
10197
|
import { basename as basename3, dirname as dirname10, isAbsolute as isAbsolute3, join as join16, normalize as normalize2, relative as relative3 } from "node:path";
|
|
10189
10198
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
10190
|
-
import { fileURLToPath as
|
|
10199
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
10191
10200
|
import { promisify as promisify5 } from "node:util";
|
|
10192
10201
|
|
|
10193
10202
|
// node_modules/zod/v3/helpers/util.js
|
|
@@ -25849,6 +25858,19 @@ function cacheWrittenAt(projectPath) {
|
|
|
25849
25858
|
return null;
|
|
25850
25859
|
}
|
|
25851
25860
|
}
|
|
25861
|
+
function declaredSince(projectPath, paths, names, since) {
|
|
25862
|
+
return names.filter((name) => {
|
|
25863
|
+
const path = paths.get(name);
|
|
25864
|
+
if (path === undefined || !path.startsWith("res://")) {
|
|
25865
|
+
return false;
|
|
25866
|
+
}
|
|
25867
|
+
try {
|
|
25868
|
+
return statSync2(join6(projectPath, path.slice("res://".length))).mtimeMs > since;
|
|
25869
|
+
} catch {
|
|
25870
|
+
return false;
|
|
25871
|
+
}
|
|
25872
|
+
});
|
|
25873
|
+
}
|
|
25852
25874
|
function cachedAtMissingPaths(cached, projectPath) {
|
|
25853
25875
|
return [...cached].filter(([, path]) => path.startsWith("res://") && !existsSync3(join6(projectPath, path.slice("res://".length)))).map(([name]) => name).sort();
|
|
25854
25876
|
}
|
|
@@ -27179,6 +27201,7 @@ class GodotBridge extends EventEmitter {
|
|
|
27179
27201
|
connectedAt: this.connectionInfo?.connectedAt,
|
|
27180
27202
|
lastPongAt: this.connectionInfo?.lastPongAt,
|
|
27181
27203
|
addonVersion: this.connectionInfo?.addonVersion,
|
|
27204
|
+
addonDigest: this.connectionInfo?.addonDigest,
|
|
27182
27205
|
editorPid: this.connectionInfo?.editorPid,
|
|
27183
27206
|
lspPort: this.connectionInfo?.lspPort,
|
|
27184
27207
|
dapPort: this.connectionInfo?.dapPort,
|
|
@@ -27322,6 +27345,7 @@ class GodotBridge extends EventEmitter {
|
|
|
27322
27345
|
}
|
|
27323
27346
|
this.connectionInfo.projectPath = message.project_path;
|
|
27324
27347
|
this.connectionInfo.addonVersion = message.addon_version ?? "";
|
|
27348
|
+
this.connectionInfo.addonDigest = message.addon_digest;
|
|
27325
27349
|
this.connectionInfo.editorPid = message.editor_pid;
|
|
27326
27350
|
this.connectionInfo.lspPort = servedPort(message.lsp_port);
|
|
27327
27351
|
this.connectionInfo.dapPort = servedPort(message.dap_port);
|
|
@@ -29456,14 +29480,66 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
29456
29480
|
}
|
|
29457
29481
|
|
|
29458
29482
|
// src/setup.ts
|
|
29459
|
-
import {
|
|
29483
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
29484
|
+
import {
|
|
29485
|
+
cpSync,
|
|
29486
|
+
existsSync as existsSync9,
|
|
29487
|
+
mkdirSync as mkdirSync9,
|
|
29488
|
+
readdirSync as readdirSync6,
|
|
29489
|
+
readFileSync as readFileSync15,
|
|
29490
|
+
rmSync as rmSync7,
|
|
29491
|
+
statSync as statSync6,
|
|
29492
|
+
writeFileSync as writeFileSync10
|
|
29493
|
+
} from "node:fs";
|
|
29460
29494
|
import { dirname as dirname9, join as join15 } from "node:path";
|
|
29495
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
29461
29496
|
var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
|
|
29497
|
+
var EDITOR_PLUGINS = ["gdharness_editor", "auto_reload"];
|
|
29462
29498
|
var RUNTIME_AUTOLOAD = {
|
|
29463
29499
|
name: "GdharnessRuntime",
|
|
29464
29500
|
path: "addons/gdharness_runtime/runtime_autoload.gd"
|
|
29465
29501
|
};
|
|
29466
29502
|
var VERSION_MARKER = ".gdharness-version";
|
|
29503
|
+
var DIGEST_MARKER = ".gdharness-digest";
|
|
29504
|
+
function editorAddonDigest(from = shippedAddonsDirectory()) {
|
|
29505
|
+
const hash = createHash3("sha256");
|
|
29506
|
+
const walk = (directory, relative) => {
|
|
29507
|
+
for (const name of readdirSync6(directory).sort()) {
|
|
29508
|
+
if (name === VERSION_MARKER || name === DIGEST_MARKER) {
|
|
29509
|
+
continue;
|
|
29510
|
+
}
|
|
29511
|
+
const path = join15(directory, name);
|
|
29512
|
+
const inside = `${relative}/${name}`;
|
|
29513
|
+
if (statSync6(path).isDirectory()) {
|
|
29514
|
+
walk(path, inside);
|
|
29515
|
+
} else {
|
|
29516
|
+
hash.update(`${inside}
|
|
29517
|
+
`);
|
|
29518
|
+
hash.update(readFileSync15(path));
|
|
29519
|
+
hash.update(`
|
|
29520
|
+
`);
|
|
29521
|
+
}
|
|
29522
|
+
}
|
|
29523
|
+
};
|
|
29524
|
+
for (const name of EDITOR_PLUGINS) {
|
|
29525
|
+
walk(join15(from, name), name);
|
|
29526
|
+
}
|
|
29527
|
+
return hash.digest("hex");
|
|
29528
|
+
}
|
|
29529
|
+
var shippedDigest;
|
|
29530
|
+
function shippedEditorDigest() {
|
|
29531
|
+
if (shippedDigest === undefined) {
|
|
29532
|
+
try {
|
|
29533
|
+
shippedDigest = editorAddonDigest();
|
|
29534
|
+
} catch {
|
|
29535
|
+
shippedDigest = null;
|
|
29536
|
+
}
|
|
29537
|
+
}
|
|
29538
|
+
return shippedDigest ?? undefined;
|
|
29539
|
+
}
|
|
29540
|
+
function shippedAddonsDirectory() {
|
|
29541
|
+
return join15(dirname9(fileURLToPath2(import.meta.url)), "godot", "addons");
|
|
29542
|
+
}
|
|
29467
29543
|
function installedAddonVersion(projectPath) {
|
|
29468
29544
|
const marker = join15(projectPath, "addons", ADDONS[0], VERSION_MARKER);
|
|
29469
29545
|
if (!existsSync9(marker)) {
|
|
@@ -30657,7 +30733,7 @@ function buildToolDefinitions() {
|
|
|
30657
30733
|
var UPDATE_NOTICE_EVERY = 500;
|
|
30658
30734
|
var FEEDBACK_NOTICE_EVERY = 250;
|
|
30659
30735
|
var run5 = promisify5(execFile5);
|
|
30660
|
-
var __dirname2 = dirname10(
|
|
30736
|
+
var __dirname2 = dirname10(fileURLToPath3(import.meta.url));
|
|
30661
30737
|
var EDITOR_RESTART_TIMEOUT_MS = 90000;
|
|
30662
30738
|
var HOLD_PING_MS = 2000;
|
|
30663
30739
|
var SLOWEST_FRAME_RATE = 20;
|
|
@@ -30951,7 +31027,7 @@ function elsewhereIn(projectPath, asked) {
|
|
|
30951
31027
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
30952
31028
|
let near = [];
|
|
30953
31029
|
try {
|
|
30954
|
-
near =
|
|
31030
|
+
near = readdirSync7(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()));
|
|
30955
31031
|
} catch {
|
|
30956
31032
|
return "";
|
|
30957
31033
|
}
|
|
@@ -32111,7 +32187,7 @@ class GodotServer {
|
|
|
32111
32187
|
let report = null;
|
|
32112
32188
|
let reportProblem = null;
|
|
32113
32189
|
try {
|
|
32114
|
-
const written = existsSync10(reportsDir) ?
|
|
32190
|
+
const written = existsSync10(reportsDir) ? readdirSync7(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
32115
32191
|
const newest = written.at(-1);
|
|
32116
32192
|
if (newest !== undefined) {
|
|
32117
32193
|
report = parseJUnit(readFileSync16(join16(reportsDir, newest, "results.xml"), "utf8"));
|
|
@@ -32240,7 +32316,7 @@ class GodotServer {
|
|
|
32240
32316
|
warnings: diagnostics.length - errors,
|
|
32241
32317
|
diagnostics,
|
|
32242
32318
|
...disagrees
|
|
32243
|
-
}, this.godotBridge.getStatus().addonVersion, SERVER_VERSION));
|
|
32319
|
+
}, this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest()));
|
|
32244
32320
|
}
|
|
32245
32321
|
whereTheFileDisagrees(diagnostics, args) {
|
|
32246
32322
|
const messages = diagnostics.map((entry) => asParams(entry)["message"]).filter((message) => typeof message === "string");
|
|
@@ -32427,7 +32503,7 @@ class GodotServer {
|
|
|
32427
32503
|
getEditorStatusPayload() {
|
|
32428
32504
|
const status = this.godotBridge.getStatus();
|
|
32429
32505
|
const isPortConflict = this.bridgeStartupError?.includes("EADDRINUSE") ?? false;
|
|
32430
|
-
const stale = status.connected && status.addonVersion
|
|
32506
|
+
const stale = status.connected && editorIsStale(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest());
|
|
32431
32507
|
const unfinished = this.restartLeftUnfinished(status.connected);
|
|
32432
32508
|
return {
|
|
32433
32509
|
...status,
|
|
@@ -32444,7 +32520,7 @@ class GodotServer {
|
|
|
32444
32520
|
awaitingLaunchedEditor: status.connected || !this.launchedEditorIsUp() ? undefined : this.launchedEditor?.pid ?? undefined,
|
|
32445
32521
|
...status.connected ? this.whatTheLaunchedEditorDropped() : {},
|
|
32446
32522
|
startupError: this.bridgeStartupError,
|
|
32447
|
-
staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
|
|
32523
|
+
staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest()) : undefined,
|
|
32448
32524
|
...this.breakpointsAtRisk(),
|
|
32449
32525
|
retryingBridge: this.bridgeRetry === null ? undefined : true,
|
|
32450
32526
|
announcedAt: this.announcedAt ?? undefined,
|
|
@@ -32576,8 +32652,8 @@ class GodotServer {
|
|
|
32576
32652
|
editorPid: now.editorPid,
|
|
32577
32653
|
addonVersion: now.addonVersion,
|
|
32578
32654
|
serverVersion: SERVER_VERSION,
|
|
32579
|
-
addonIsStale: now.addonVersion
|
|
32580
|
-
staleNote: addonMismatch(now.addonVersion, SERVER_VERSION),
|
|
32655
|
+
addonIsStale: editorIsStale(now.addonVersion, SERVER_VERSION, now.addonDigest, shippedEditorDigest()),
|
|
32656
|
+
staleNote: addonMismatch(now.addonVersion, SERVER_VERSION, now.addonDigest, shippedEditorDigest()),
|
|
32581
32657
|
...this.whatTheEditorDropped(settingsBefore, after),
|
|
32582
32658
|
tookMs: Date.now() - began
|
|
32583
32659
|
});
|
|
@@ -32854,7 +32930,7 @@ class GodotServer {
|
|
|
32854
32930
|
}
|
|
32855
32931
|
let announcedAt;
|
|
32856
32932
|
try {
|
|
32857
|
-
announcedAt =
|
|
32933
|
+
announcedAt = statSync7(endpoint.file).mtimeMs;
|
|
32858
32934
|
} catch {
|
|
32859
32935
|
return;
|
|
32860
32936
|
}
|
|
@@ -33464,7 +33540,15 @@ class GodotServer {
|
|
|
33464
33540
|
"editor_launch opens one, and an editor opened here has its console captured"
|
|
33465
33541
|
]);
|
|
33466
33542
|
}
|
|
33467
|
-
const
|
|
33543
|
+
const launched = status.connected ? null : this.launchedEditor?.pid ?? null;
|
|
33544
|
+
if (!status.connected && launched === null) {
|
|
33545
|
+
return this.createErrorResponse("No editor is connected, so there is no console to read.", [
|
|
33546
|
+
"editor_status says whether one is connected and whether it may yet be",
|
|
33547
|
+
"editor_launch opens one, and an editor opened here has its console captured"
|
|
33548
|
+
]);
|
|
33549
|
+
}
|
|
33550
|
+
const editorPid = status.connected ? status.editorPid ?? null : launched;
|
|
33551
|
+
const console2 = editorConsole(projectPath, editorPid, status.connected ? status.openedByAServer === true : true);
|
|
33468
33552
|
if ("kind" in console2) {
|
|
33469
33553
|
return this.createErrorResponse(theConsoleWasNotCaptured(console2, projectPath), [
|
|
33470
33554
|
"editor_launch restart replaces this editor with one whose console is captured",
|
|
@@ -33482,8 +33566,9 @@ class GodotServer {
|
|
|
33482
33566
|
const grouped = bursts(everything, readPositiveNumber(args, "before") ?? 3);
|
|
33483
33567
|
const shown = grouped.slice(0, MOST_GROUPS);
|
|
33484
33568
|
return this.jsonTextResponse({
|
|
33485
|
-
editorPid:
|
|
33569
|
+
editorPid: editorPid ?? undefined,
|
|
33486
33570
|
projectPath,
|
|
33571
|
+
...status.connected ? {} : this.launchedEditorIsUp() ? { stillConnecting: true } : { exitedBeforeConnecting: true },
|
|
33487
33572
|
capturedIn: console2.path,
|
|
33488
33573
|
...console2.of === undefined ? {} : {
|
|
33489
33574
|
readFromTheStart: console2.readBytes,
|
|
@@ -33782,6 +33867,7 @@ class GodotServer {
|
|
|
33782
33867
|
}
|
|
33783
33868
|
async rebuildClassCache(projectPath) {
|
|
33784
33869
|
const before = cachedClasses(projectPath);
|
|
33870
|
+
const writtenBefore = cacheWrittenAt(projectPath);
|
|
33785
33871
|
const noted = readClassNote(projectPath);
|
|
33786
33872
|
const rebuilt = await this.operation("refresh_class_cache", {}, projectPath);
|
|
33787
33873
|
if (!rebuilt.ok) {
|
|
@@ -33792,21 +33878,30 @@ class GodotServer {
|
|
|
33792
33878
|
return rebuilt;
|
|
33793
33879
|
}
|
|
33794
33880
|
const lost = noted === null || before === null ? [] : noted.rebuiltWith.filter((name) => !before.has(name) && after.has(name)).sort();
|
|
33795
|
-
const
|
|
33881
|
+
const cameBack = writtenBefore === null ? [] : declaredSince(projectPath, after, lost, writtenBefore);
|
|
33882
|
+
const droppedByTheEditor = lost.filter((name) => !cameBack.includes(name));
|
|
33883
|
+
const seenLosing = droppedByTheEditor.length > 0 && this.godotBridge.isConnected();
|
|
33796
33884
|
writeClassNote(projectPath, {
|
|
33797
33885
|
rebuiltWith: [...after.keys()].sort(),
|
|
33798
33886
|
...seenLosing ? { shortListEditorPid: this.godotBridge.getStatus().editorPid ?? null } : noted?.shortListEditorPid === undefined ? {} : { shortListEditorPid: noted.shortListEditorPid }
|
|
33799
33887
|
});
|
|
33800
|
-
|
|
33888
|
+
const notes = [];
|
|
33889
|
+
if (seenLosing) {
|
|
33890
|
+
const them = droppedByTheEditor.length === 1 ? "it" : "them";
|
|
33891
|
+
notes.push(`The cache held ${droppedByTheEditor.join(", ")} after the last rebuild and not when this one began, so the editor holding this project wrote it without ${them} in between: it holds a list shorter than the files and writes that list on every save and scan, and each rebuild puts ${them} back. editor_launch restart is what ends it.`);
|
|
33892
|
+
}
|
|
33893
|
+
if (cameBack.length > 0) {
|
|
33894
|
+
notes.push(`${cameBack.join(", ")} left the cache while ${cameBack.length === 1 ? "its script was" : "their scripts were"} gone: the script${cameBack.length === 1 ? " is" : "s are"} newer than the cache the editor last wrote, so it listed what was on disk then. editor_rescan picks ${cameBack.length === 1 ? "it" : "them"} up.`);
|
|
33895
|
+
}
|
|
33896
|
+
if (notes.length === 0) {
|
|
33801
33897
|
return rebuilt;
|
|
33802
33898
|
}
|
|
33803
|
-
const them = lost.length === 1 ? "it" : "them";
|
|
33804
33899
|
return {
|
|
33805
33900
|
...rebuilt,
|
|
33806
33901
|
payload: {
|
|
33807
33902
|
...rebuilt.payload,
|
|
33808
33903
|
lostSinceLastRebuild: lost,
|
|
33809
|
-
note:
|
|
33904
|
+
note: notes.join(" ")
|
|
33810
33905
|
}
|
|
33811
33906
|
};
|
|
33812
33907
|
}
|
|
@@ -33882,7 +33977,7 @@ class GodotServer {
|
|
|
33882
33977
|
]);
|
|
33883
33978
|
}
|
|
33884
33979
|
try {
|
|
33885
|
-
return this.jsonTextResponse(markIfStale(await this.godotBridge.invokeTool(toolName, args), this.godotBridge.getStatus().addonVersion, SERVER_VERSION));
|
|
33980
|
+
return this.jsonTextResponse(markIfStale(await this.godotBridge.invokeTool(toolName, args), this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest()));
|
|
33886
33981
|
} catch (error) {
|
|
33887
33982
|
return this.createErrorResponse(`The editor answered ${toolName} with an error: ${errorMessage(error)}`);
|
|
33888
33983
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.13",
|
|
4
4
|
"mcpName": "io.github.Aureliolo/gdharness",
|
|
5
5
|
"description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
|
|
6
6
|
"type": "module",
|