gdharness 1.1.22 → 1.1.24
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 +703 -289
- package/build/godot/addons/gdharness_editor/bridge_client.gd +12 -2
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +30 -1
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +51 -5
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +2 -2
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +61 -8
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +105 -49
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +41 -19
- package/build/godot/operations/classdb_queries.gd +28 -17
- package/build/godot/operations/project_diagnostics.gd +2 -1
- package/build/index.js +597 -136
- package/build/keeper.js +153 -48
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -662,7 +662,7 @@ function environmentFor(options, variables = process.env) {
|
|
|
662
662
|
const base = options.savesIn === undefined ? { ...variables } : userDataIn(options.savesIn, variables);
|
|
663
663
|
return { ...base, ...options.env, GDHARNESS_RUNTIME_DIR: options.runtimeDirectory };
|
|
664
664
|
}
|
|
665
|
-
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.";
|
|
665
|
+
var OPENED_BY_A_SERVER = "GDHARNESS_OPENED_BY_A_SERVER", EDITOR_PID_VARIABLE = "GDHARNESS_EDITOR_PID", 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.";
|
|
666
666
|
|
|
667
667
|
// src/godot-path.ts
|
|
668
668
|
import { execFile as execFile2 } from "node:child_process";
|
|
@@ -7684,6 +7684,73 @@ var require_public_api = __commonJS(function(exports) {
|
|
|
7684
7684
|
exports.stringify = stringify;
|
|
7685
7685
|
});
|
|
7686
7686
|
|
|
7687
|
+
// src/paths.ts
|
|
7688
|
+
import { realpathSync } from "node:fs";
|
|
7689
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
7690
|
+
function realPathOr(path) {
|
|
7691
|
+
try {
|
|
7692
|
+
return realpathSync.native(path);
|
|
7693
|
+
} catch {
|
|
7694
|
+
return path;
|
|
7695
|
+
}
|
|
7696
|
+
}
|
|
7697
|
+
function isSameDirectory(onePath, otherPath) {
|
|
7698
|
+
return relative(realPathOr(resolve(onePath)), realPathOr(resolve(otherPath))) === "";
|
|
7699
|
+
}
|
|
7700
|
+
function isWithinRoot(rootPath, candidatePath) {
|
|
7701
|
+
const step = relative(resolve(rootPath), resolve(candidatePath));
|
|
7702
|
+
return step !== ".." && !step.startsWith(`..${sep}`) && !isAbsolute(step);
|
|
7703
|
+
}
|
|
7704
|
+
function resolveWithinProject(projectPath, candidatePath) {
|
|
7705
|
+
const trimmed = candidatePath.trim();
|
|
7706
|
+
if (trimmed === "") {
|
|
7707
|
+
return { ok: false, reason: "Path is empty." };
|
|
7708
|
+
}
|
|
7709
|
+
if (trimmed.includes("\x00")) {
|
|
7710
|
+
return { ok: false, reason: "Path contains a null byte." };
|
|
7711
|
+
}
|
|
7712
|
+
const withoutScheme = trimmed.startsWith(RESOURCE_SCHEME) ? trimmed.slice(RESOURCE_SCHEME.length) : trimmed;
|
|
7713
|
+
if (withoutScheme.startsWith("uid://")) {
|
|
7714
|
+
return {
|
|
7715
|
+
ok: false,
|
|
7716
|
+
reason: `Path '${trimmed}' is a UID, and a file is judged here by where it is on disk, which a UID does not say. Give the file's res:// path.`
|
|
7717
|
+
};
|
|
7718
|
+
}
|
|
7719
|
+
const scheme = OTHER_SCHEME.exec(withoutScheme);
|
|
7720
|
+
if (scheme) {
|
|
7721
|
+
return {
|
|
7722
|
+
ok: false,
|
|
7723
|
+
reason: `Path '${trimmed}' uses the '${scheme[0]}' scheme, which names somewhere other than this project.`
|
|
7724
|
+
};
|
|
7725
|
+
}
|
|
7726
|
+
if (withoutScheme.startsWith("/") || withoutScheme.startsWith("\\") || isAbsolute(withoutScheme)) {
|
|
7727
|
+
return {
|
|
7728
|
+
ok: false,
|
|
7729
|
+
reason: `Path '${trimmed}' is absolute; give a path inside the project, such as 'scenes/main.tscn'.`
|
|
7730
|
+
};
|
|
7731
|
+
}
|
|
7732
|
+
if (withoutScheme.includes(":")) {
|
|
7733
|
+
return {
|
|
7734
|
+
ok: false,
|
|
7735
|
+
reason: `Path '${trimmed}' contains ':', which no file inside a project is named with.`
|
|
7736
|
+
};
|
|
7737
|
+
}
|
|
7738
|
+
const root = resolve(projectPath);
|
|
7739
|
+
const absolutePath = resolve(root, withoutScheme);
|
|
7740
|
+
if (!isWithinRoot(root, absolutePath)) {
|
|
7741
|
+
return { ok: false, reason: `Path '${trimmed}' resolves outside the project directory.` };
|
|
7742
|
+
}
|
|
7743
|
+
const relativePath = relative(root, absolutePath).split(sep).join("/");
|
|
7744
|
+
if (relativePath === "") {
|
|
7745
|
+
return { ok: false, reason: `Path '${trimmed}' is the project directory itself, not a file in it.` };
|
|
7746
|
+
}
|
|
7747
|
+
return { ok: true, absolutePath, relativePath };
|
|
7748
|
+
}
|
|
7749
|
+
var RESOURCE_SCHEME = "res://", OTHER_SCHEME;
|
|
7750
|
+
var init_paths = __esm(() => {
|
|
7751
|
+
OTHER_SCHEME = /^[a-zA-Z][a-zA-Z\d+.-]+:\/\//;
|
|
7752
|
+
});
|
|
7753
|
+
|
|
7687
7754
|
// src/runner.ts
|
|
7688
7755
|
import { existsSync as existsSync3 } from "node:fs";
|
|
7689
7756
|
import { dirname as dirname2, join as join3 } from "node:path";
|
|
@@ -7897,22 +7964,22 @@ var init_engine_run = __esm(() => {
|
|
|
7897
7964
|
|
|
7898
7965
|
// src/reimport.ts
|
|
7899
7966
|
import { createHash } from "node:crypto";
|
|
7900
|
-
import { existsSync as existsSync5, readFileSync as readFileSync3, rmSync as rmSync3, statSync as
|
|
7901
|
-
import { basename as
|
|
7967
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3, rmSync as rmSync3, statSync as statSync3, utimesSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
7968
|
+
import { basename as basename3, join as join5 } from "node:path";
|
|
7902
7969
|
function forceNextImport(projectPath, resourcePath) {
|
|
7903
7970
|
const relative = resourcePath.replace(/^res:\/\//, "");
|
|
7904
7971
|
const sidecar = join5(projectPath, `${relative}.import`);
|
|
7905
7972
|
if (!existsSync5(sidecar)) {
|
|
7906
7973
|
return;
|
|
7907
7974
|
}
|
|
7908
|
-
const { atime, mtimeMs } =
|
|
7975
|
+
const { atime, mtimeMs } = statSync3(sidecar);
|
|
7909
7976
|
writeFileSync3(sidecar, readFileSync3(sidecar, "utf8").replace(/^valid=false\r?\n/m, ""));
|
|
7910
7977
|
utimesSync(sidecar, atime, new Date(mtimeMs + 2000));
|
|
7911
7978
|
rmSync3(importRecord(projectPath, resourcePath), { force: true });
|
|
7912
7979
|
}
|
|
7913
7980
|
function importRecord(projectPath, resourcePath) {
|
|
7914
7981
|
const digest = createHash("md5").update(resourcePath).digest("hex");
|
|
7915
|
-
return join5(projectPath, ".godot", "imported", `${
|
|
7982
|
+
return join5(projectPath, ".godot", "imported", `${basename3(resourcePath)}-${digest}.md5`);
|
|
7916
7983
|
}
|
|
7917
7984
|
var init_reimport = () => {};
|
|
7918
7985
|
|
|
@@ -8038,7 +8105,7 @@ var init_scratch = __esm(() => {
|
|
|
8038
8105
|
});
|
|
8039
8106
|
|
|
8040
8107
|
// src/headless.ts
|
|
8041
|
-
import { existsSync as existsSync7, readFileSync as readFileSync4, realpathSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
8108
|
+
import { existsSync as existsSync7, readFileSync as readFileSync4, realpathSync as realpathSync2, writeFileSync as writeFileSync4 } from "node:fs";
|
|
8042
8109
|
import { dirname as dirname4, join as join7 } from "node:path";
|
|
8043
8110
|
function snakeCased(params) {
|
|
8044
8111
|
const result = emptyRecord();
|
|
@@ -8093,7 +8160,7 @@ function scriptFault(problems, script) {
|
|
|
8093
8160
|
}
|
|
8094
8161
|
function resolvedOr(path) {
|
|
8095
8162
|
try {
|
|
8096
|
-
return
|
|
8163
|
+
return realpathSync2.native(path);
|
|
8097
8164
|
} catch {
|
|
8098
8165
|
return path;
|
|
8099
8166
|
}
|
|
@@ -8446,23 +8513,51 @@ function sameCodeNote(addonVersion, serverVersion, addonDigest, shippedDigest) {
|
|
|
8446
8513
|
}
|
|
8447
8514
|
return `The editor loaded the ${addonVersion} addons, which are the same code this ${serverVersion} server ships, so nothing needs restarting; addonVersion changes at the next editor start.`;
|
|
8448
8515
|
}
|
|
8449
|
-
function addonMismatch(addonVersion, serverVersion, addonDigest, shippedDigest) {
|
|
8516
|
+
function addonMismatch(addonVersion, serverVersion, addonDigest, shippedDigest, onDisk) {
|
|
8450
8517
|
if (!editorIsStale(addonVersion, serverVersion, addonDigest, shippedDigest)) {
|
|
8451
8518
|
return;
|
|
8452
8519
|
}
|
|
8453
8520
|
const reported = addonVersion ?? "";
|
|
8521
|
+
const restart = onDisk !== undefined && onDisk !== shippedDigest ? `The project's own addons are not this server's either, so a restart would load the same code again: gdharness upgrade in the project installs the ${serverVersion} addons, and editor_launch restart then picks them up.` : "Restart it with editor_launch restart to pick this one up.";
|
|
8454
8522
|
if (reported === serverVersion) {
|
|
8455
|
-
return `The editor is running a different build of the ${reported} addon from the one this server ships.
|
|
8523
|
+
return `The editor is running a different build of the ${reported} addon from the one this server ships. ${restart}`;
|
|
8456
8524
|
}
|
|
8457
8525
|
const editor = reported === "" ? UNVERSIONED : `the ${reported} addon`;
|
|
8458
8526
|
const both = `The editor is running ${editor} while this server ships ${serverVersion}.`;
|
|
8459
8527
|
if (reported !== "" && isNewer(reported, serverVersion)) {
|
|
8460
8528
|
return `${both} This server is the older half: reconnect it in your harness so it spawns ${reported}.`;
|
|
8461
8529
|
}
|
|
8462
|
-
return `${both}
|
|
8530
|
+
return `${both} ${restart}`;
|
|
8531
|
+
}
|
|
8532
|
+
function runtimeAddonUnknown(addon) {
|
|
8533
|
+
return addon?.version === "" && addon.digest === "";
|
|
8534
|
+
}
|
|
8535
|
+
function runtimeMismatch(pid, addon, serverVersion, shippedDigest, onDisk, projectPath) {
|
|
8536
|
+
if (runtimeAddonUnknown(addon)) {
|
|
8537
|
+
return;
|
|
8538
|
+
}
|
|
8539
|
+
if (addon !== undefined && !editorIsStale(addon.version, serverVersion, addon.digest, shippedDigest)) {
|
|
8540
|
+
return;
|
|
8541
|
+
}
|
|
8542
|
+
const game = `The game (pid ${pid})`;
|
|
8543
|
+
if (addon !== undefined && addon.version !== "" && isNewer(addon.version, serverVersion)) {
|
|
8544
|
+
return `${game} is running the ${addon.version} runtime addon while this server ships ${serverVersion}. This server is the older half: reconnect it in your harness so it spawns ${addon.version}.`;
|
|
8545
|
+
}
|
|
8546
|
+
let loaded;
|
|
8547
|
+
if (addon === undefined) {
|
|
8548
|
+
loaded = `a runtime addon from before games reported theirs, while this server ships ${serverVersion}`;
|
|
8549
|
+
} else if (addon.version === serverVersion) {
|
|
8550
|
+
loaded = `a different build of the ${serverVersion} runtime addon from the one this server ships`;
|
|
8551
|
+
} else if (addon.version === "") {
|
|
8552
|
+
loaded = `a runtime addon with no version marker, while this server ships ${serverVersion}`;
|
|
8553
|
+
} else {
|
|
8554
|
+
loaded = `the ${addon.version} runtime addon, while this server ships ${serverVersion}`;
|
|
8555
|
+
}
|
|
8556
|
+
const remedy = onDisk !== undefined && onDisk !== shippedDigest ? `The project's own runtime addon is not this server's either, so starting the game again loads the same code: gdharness upgrade in ${projectPath} installs the ${serverVersion} addons, and the game picks them up when it next starts.` : `The project holds this server's runtime addon, so the game picks it up when it next starts.`;
|
|
8557
|
+
return `${game} is running ${loaded}, and its answers come from that code. ${remedy}`;
|
|
8463
8558
|
}
|
|
8464
|
-
function markIfStale(answer, addonVersion, serverVersion, addonDigest, shippedDigest) {
|
|
8465
|
-
const staleNote = addonMismatch(addonVersion, serverVersion, addonDigest, shippedDigest);
|
|
8559
|
+
function markIfStale(answer, addonVersion, serverVersion, addonDigest, shippedDigest, onDisk) {
|
|
8560
|
+
const staleNote = addonMismatch(addonVersion, serverVersion, addonDigest, shippedDigest, onDisk);
|
|
8466
8561
|
if (staleNote === undefined || typeof answer !== "object" || answer === null || Array.isArray(answer)) {
|
|
8467
8562
|
return answer;
|
|
8468
8563
|
}
|
|
@@ -8609,7 +8704,7 @@ var init_issues = __esm(() => {
|
|
|
8609
8704
|
});
|
|
8610
8705
|
|
|
8611
8706
|
// src/class-cache.ts
|
|
8612
|
-
import { existsSync as existsSync9, readdirSync as readdirSync3, readFileSync as readFileSync7, statSync as
|
|
8707
|
+
import { existsSync as existsSync9, readdirSync as readdirSync3, readFileSync as readFileSync7, statSync as statSync4 } from "node:fs";
|
|
8613
8708
|
import { join as join9 } from "node:path";
|
|
8614
8709
|
function isToolScript(source) {
|
|
8615
8710
|
return TOOL.test(codeOf(source));
|
|
@@ -8905,7 +9000,7 @@ function uncachedClassNote(types) {
|
|
|
8905
9000
|
function cacheWrittenAt(projectPath) {
|
|
8906
9001
|
const cache = join9(projectPath, ".godot", "global_script_class_cache.cfg");
|
|
8907
9002
|
try {
|
|
8908
|
-
return
|
|
9003
|
+
return statSync4(cache).mtimeMs;
|
|
8909
9004
|
} catch {
|
|
8910
9005
|
return null;
|
|
8911
9006
|
}
|
|
@@ -8917,7 +9012,7 @@ function declaredSince(projectPath, paths, names, since) {
|
|
|
8917
9012
|
return false;
|
|
8918
9013
|
}
|
|
8919
9014
|
try {
|
|
8920
|
-
return
|
|
9015
|
+
return statSync4(join9(projectPath, path.slice("res://".length))).mtimeMs > since;
|
|
8921
9016
|
} catch {
|
|
8922
9017
|
return false;
|
|
8923
9018
|
}
|
|
@@ -16050,82 +16145,15 @@ var init_types = __esm(() => {
|
|
|
16050
16145
|
};
|
|
16051
16146
|
});
|
|
16052
16147
|
|
|
16053
|
-
// src/paths.ts
|
|
16054
|
-
import { realpathSync as realpathSync2 } from "node:fs";
|
|
16055
|
-
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
16056
|
-
function realPathOr(path) {
|
|
16057
|
-
try {
|
|
16058
|
-
return realpathSync2.native(path);
|
|
16059
|
-
} catch {
|
|
16060
|
-
return path;
|
|
16061
|
-
}
|
|
16062
|
-
}
|
|
16063
|
-
function isSameDirectory(onePath, otherPath) {
|
|
16064
|
-
return relative(realPathOr(resolve(onePath)), realPathOr(resolve(otherPath))) === "";
|
|
16065
|
-
}
|
|
16066
|
-
function isWithinRoot(rootPath, candidatePath) {
|
|
16067
|
-
const step = relative(resolve(rootPath), resolve(candidatePath));
|
|
16068
|
-
return step !== ".." && !step.startsWith(`..${sep}`) && !isAbsolute(step);
|
|
16069
|
-
}
|
|
16070
|
-
function resolveWithinProject(projectPath, candidatePath) {
|
|
16071
|
-
const trimmed = candidatePath.trim();
|
|
16072
|
-
if (trimmed === "") {
|
|
16073
|
-
return { ok: false, reason: "Path is empty." };
|
|
16074
|
-
}
|
|
16075
|
-
if (trimmed.includes("\x00")) {
|
|
16076
|
-
return { ok: false, reason: "Path contains a null byte." };
|
|
16077
|
-
}
|
|
16078
|
-
const withoutScheme = trimmed.startsWith(RESOURCE_SCHEME) ? trimmed.slice(RESOURCE_SCHEME.length) : trimmed;
|
|
16079
|
-
if (withoutScheme.startsWith("uid://")) {
|
|
16080
|
-
return {
|
|
16081
|
-
ok: false,
|
|
16082
|
-
reason: `Path '${trimmed}' is a UID, and a file is judged here by where it is on disk, which a UID does not say. Give the file's res:// path.`
|
|
16083
|
-
};
|
|
16084
|
-
}
|
|
16085
|
-
const scheme = OTHER_SCHEME.exec(withoutScheme);
|
|
16086
|
-
if (scheme) {
|
|
16087
|
-
return {
|
|
16088
|
-
ok: false,
|
|
16089
|
-
reason: `Path '${trimmed}' uses the '${scheme[0]}' scheme, which names somewhere other than this project.`
|
|
16090
|
-
};
|
|
16091
|
-
}
|
|
16092
|
-
if (withoutScheme.startsWith("/") || withoutScheme.startsWith("\\") || isAbsolute(withoutScheme)) {
|
|
16093
|
-
return {
|
|
16094
|
-
ok: false,
|
|
16095
|
-
reason: `Path '${trimmed}' is absolute; give a path inside the project, such as 'scenes/main.tscn'.`
|
|
16096
|
-
};
|
|
16097
|
-
}
|
|
16098
|
-
if (withoutScheme.includes(":")) {
|
|
16099
|
-
return {
|
|
16100
|
-
ok: false,
|
|
16101
|
-
reason: `Path '${trimmed}' contains ':', which no file inside a project is named with.`
|
|
16102
|
-
};
|
|
16103
|
-
}
|
|
16104
|
-
const root = resolve(projectPath);
|
|
16105
|
-
const absolutePath = resolve(root, withoutScheme);
|
|
16106
|
-
if (!isWithinRoot(root, absolutePath)) {
|
|
16107
|
-
return { ok: false, reason: `Path '${trimmed}' resolves outside the project directory.` };
|
|
16108
|
-
}
|
|
16109
|
-
const relativePath = relative(root, absolutePath).split(sep).join("/");
|
|
16110
|
-
if (relativePath === "") {
|
|
16111
|
-
return { ok: false, reason: `Path '${trimmed}' is the project directory itself, not a file in it.` };
|
|
16112
|
-
}
|
|
16113
|
-
return { ok: true, absolutePath, relativePath };
|
|
16114
|
-
}
|
|
16115
|
-
var RESOURCE_SCHEME = "res://", OTHER_SCHEME;
|
|
16116
|
-
var init_paths = __esm(() => {
|
|
16117
|
-
OTHER_SCHEME = /^[a-zA-Z][a-zA-Z\d+.-]+:\/\//;
|
|
16118
|
-
});
|
|
16119
|
-
|
|
16120
16148
|
// src/resources.ts
|
|
16121
|
-
import { readFileSync as readFileSync8 } from "node:fs";
|
|
16122
|
-
import { extname, resolve as
|
|
16149
|
+
import { readFileSync as readFileSync8, realpathSync as realpathSync3 } from "node:fs";
|
|
16150
|
+
import { extname, resolve as resolve3 } from "node:path";
|
|
16123
16151
|
function ensureProjectPath(getProjectPath) {
|
|
16124
16152
|
const projectPath = getProjectPath();
|
|
16125
16153
|
if (!projectPath) {
|
|
16126
|
-
throw new Refusal("
|
|
16154
|
+
throw new Refusal("This server knows no project to read from: start it with GDHARNESS_PROJECT, connect an editor, or make any tool call naming a projectPath that holds project.godot.");
|
|
16127
16155
|
}
|
|
16128
|
-
return
|
|
16156
|
+
return resolve3(projectPath);
|
|
16129
16157
|
}
|
|
16130
16158
|
function uriPathToProjectPath(inputPath) {
|
|
16131
16159
|
const normalized = inputPath.replace(/\\/g, "/").trim();
|
|
@@ -16139,7 +16167,22 @@ function resolveProjectFile(projectPath, resourcePath) {
|
|
|
16139
16167
|
if (!contained.ok) {
|
|
16140
16168
|
throw new Refusal(contained.reason);
|
|
16141
16169
|
}
|
|
16142
|
-
|
|
16170
|
+
let root;
|
|
16171
|
+
try {
|
|
16172
|
+
root = realpathSync3.native(projectPath);
|
|
16173
|
+
} catch {
|
|
16174
|
+
throw new Refusal(`The project directory ${projectPath} is not there.`);
|
|
16175
|
+
}
|
|
16176
|
+
let real;
|
|
16177
|
+
try {
|
|
16178
|
+
real = realpathSync3.native(contained.absolutePath);
|
|
16179
|
+
} catch {
|
|
16180
|
+
throw new Refusal(`No file at ${resourcePath} in the project.`);
|
|
16181
|
+
}
|
|
16182
|
+
if (!isWithinRoot(root, real)) {
|
|
16183
|
+
throw new Refusal(`${resourcePath} leads outside the project through a link, so it is not read.`);
|
|
16184
|
+
}
|
|
16185
|
+
return real;
|
|
16143
16186
|
}
|
|
16144
16187
|
function parseGodotUri(uri) {
|
|
16145
16188
|
if (uri === "godot://project/info") {
|
|
@@ -16431,12 +16474,18 @@ import {
|
|
|
16431
16474
|
readFileSync as readFileSync9,
|
|
16432
16475
|
rmdirSync,
|
|
16433
16476
|
rmSync as rmSync5,
|
|
16434
|
-
statSync as
|
|
16477
|
+
statSync as statSync5,
|
|
16435
16478
|
writeFileSync as writeFileSync6
|
|
16436
16479
|
} from "node:fs";
|
|
16437
16480
|
import { dirname as dirname5, join as join10 } from "node:path";
|
|
16438
16481
|
import { fileURLToPath } from "node:url";
|
|
16439
16482
|
function editorAddonDigest(from = shippedAddonsDirectory()) {
|
|
16483
|
+
return addonsDigest(EDITOR_PLUGINS, from);
|
|
16484
|
+
}
|
|
16485
|
+
function runtimeAddonDigest(from = shippedAddonsDirectory()) {
|
|
16486
|
+
return addonsDigest([RUNTIME_ADDON], from);
|
|
16487
|
+
}
|
|
16488
|
+
function addonsDigest(names, from) {
|
|
16440
16489
|
const hash = createHash3("sha256");
|
|
16441
16490
|
const walk = (directory, relative) => {
|
|
16442
16491
|
for (const name of readdirSync4(directory).sort()) {
|
|
@@ -16445,7 +16494,7 @@ function editorAddonDigest(from = shippedAddonsDirectory()) {
|
|
|
16445
16494
|
}
|
|
16446
16495
|
const path = join10(directory, name);
|
|
16447
16496
|
const inside = `${relative}/${name}`;
|
|
16448
|
-
if (
|
|
16497
|
+
if (statSync5(path).isDirectory()) {
|
|
16449
16498
|
walk(path, inside);
|
|
16450
16499
|
} else {
|
|
16451
16500
|
hash.update(`${inside}
|
|
@@ -16456,7 +16505,7 @@ function editorAddonDigest(from = shippedAddonsDirectory()) {
|
|
|
16456
16505
|
}
|
|
16457
16506
|
}
|
|
16458
16507
|
};
|
|
16459
|
-
for (const name of
|
|
16508
|
+
for (const name of names) {
|
|
16460
16509
|
walk(join10(from, name), name);
|
|
16461
16510
|
}
|
|
16462
16511
|
return hash.digest("hex");
|
|
@@ -16471,8 +16520,24 @@ function shippedEditorDigest() {
|
|
|
16471
16520
|
}
|
|
16472
16521
|
return shippedDigest ?? undefined;
|
|
16473
16522
|
}
|
|
16523
|
+
function shippedRuntimeDigest() {
|
|
16524
|
+
if (shippedRuntime === undefined) {
|
|
16525
|
+
try {
|
|
16526
|
+
shippedRuntime = runtimeAddonDigest();
|
|
16527
|
+
} catch {
|
|
16528
|
+
shippedRuntime = null;
|
|
16529
|
+
}
|
|
16530
|
+
}
|
|
16531
|
+
return shippedRuntime ?? undefined;
|
|
16532
|
+
}
|
|
16474
16533
|
function installedEditorDigest(projectPath) {
|
|
16475
|
-
|
|
16534
|
+
return digestMarker(projectPath, ADDONS[0]);
|
|
16535
|
+
}
|
|
16536
|
+
function installedRuntimeDigest(projectPath) {
|
|
16537
|
+
return digestMarker(projectPath, RUNTIME_ADDON);
|
|
16538
|
+
}
|
|
16539
|
+
function digestMarker(projectPath, addon) {
|
|
16540
|
+
const marker = join10(projectPath, "addons", addon, DIGEST_MARKER);
|
|
16476
16541
|
if (!existsSync10(marker)) {
|
|
16477
16542
|
return null;
|
|
16478
16543
|
}
|
|
@@ -16489,7 +16554,7 @@ function installAddons(projectPath, from = shippedAddonsDirectory()) {
|
|
|
16489
16554
|
const installed = [];
|
|
16490
16555
|
for (const name of ADDONS) {
|
|
16491
16556
|
const source = join10(from, name);
|
|
16492
|
-
if (!existsSync10(join10(source, name ===
|
|
16557
|
+
if (!existsSync10(join10(source, name === RUNTIME_ADDON ? "runtime_autoload.gd" : "plugin.cfg"))) {
|
|
16493
16558
|
throw new Error(`The package holds no ${name} addon at ${source}.`);
|
|
16494
16559
|
}
|
|
16495
16560
|
const target = join10(projectPath, "addons", name);
|
|
@@ -16502,6 +16567,8 @@ function installAddons(projectPath, from = shippedAddonsDirectory()) {
|
|
|
16502
16567
|
installed.push({ name, path: target, replaced });
|
|
16503
16568
|
}
|
|
16504
16569
|
writeFileSync6(join10(projectPath, "addons", ADDONS[0], DIGEST_MARKER), `${editorAddonDigest(from)}
|
|
16570
|
+
`);
|
|
16571
|
+
writeFileSync6(join10(projectPath, "addons", RUNTIME_ADDON, DIGEST_MARKER), `${runtimeAddonDigest(from)}
|
|
16505
16572
|
`);
|
|
16506
16573
|
return installed;
|
|
16507
16574
|
}
|
|
@@ -16656,7 +16723,7 @@ function inspectProject(projectPath) {
|
|
|
16656
16723
|
problems
|
|
16657
16724
|
};
|
|
16658
16725
|
}
|
|
16659
|
-
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;
|
|
16726
|
+
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", RUNTIME_ADDON = "gdharness_runtime", shippedDigest, shippedRuntime;
|
|
16660
16727
|
var init_setup = __esm(() => {
|
|
16661
16728
|
init_class_cache();
|
|
16662
16729
|
init_headless();
|
|
@@ -17395,13 +17462,13 @@ var init_tool_definitions = __esm(() => {
|
|
|
17395
17462
|
},
|
|
17396
17463
|
{
|
|
17397
17464
|
name: "editor_launch",
|
|
17398
|
-
description: "Opens the Godot editor on a project, or restarts the one already connected. The editor is an ordinary visible one on the desktop in use, and on Windows it is shown without being activated, so opening or reopening it does not take the keyboard from whatever the user is typing in. hidden: true opens it headless instead and, on Windows, on a desktop of its own, where the games it plays go too; a hidden editor serves the bridge, the language server and the debug adapter as a visible one does. A game a visible editor plays opens as a window on the desktop in use. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect, and nothing else here restarts one; it saves open scenes on the way out and answers with the version that came back. An editor a server opened is started again by the server, visible unless hidden: true is passed again. One opened by hand restarts itself, which only one with a window can do, because the engine hands back none of the arguments it was started with. editor_status says which editor is connected and whether it is holding an old addon. Opening a project is a save and so is closing one, which is the part that is not obvious from outside: the editor imports the project on open and writes project.godot back, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped either way. A restart says which under settingsDropped in its own answer because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one. A restart of an editor this server opened is a quit and then a launch, and this server can be ended between them, usually by the reconnect the restart was being taken for: the editor is gone and nothing replaces it. That is written down before the quit, so the next server says so under restartInterrupted in editor_status rather than waiting for an editor that is not coming, and open finishes it, saying so under finishesRestart.",
|
|
17465
|
+
description: "Opens the Godot editor on a project, or restarts the one already connected. The editor is an ordinary visible one on the desktop in use, and on Windows it is shown without being activated, so opening or reopening it does not take the keyboard from whatever the user is typing in. hidden: true opens it headless instead and, on Windows, on a desktop of its own, where the games it plays go too; a hidden editor serves the bridge, the language server and the debug adapter as a visible one does. A game a visible editor plays opens as a window on the desktop in use. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect, and nothing else here restarts one; it saves open scenes on the way out and answers with the version that came back. An editor a server opened is started again by the server, visible unless hidden: true is passed again, once the old one has gone: it waits 90 seconds for that, or GDHARNESS_EDITOR_RESTART_TIMEOUT_MS, and an old editor still running after it is not replaced, since the new one would find its ports held. One opened by hand restarts itself, which only one with a window can do, because the engine hands back none of the arguments it was started with. editor_status says which editor is connected and whether it is holding an old addon. Opening a project is a save and so is closing one, which is the part that is not obvious from outside: the editor imports the project on open and writes project.godot back, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped either way. A restart says which under settingsDropped in its own answer because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one. A restart of an editor this server opened is a quit and then a launch, and this server can be ended between them, usually by the reconnect the restart was being taken for: the editor is gone and nothing replaces it. That is written down before the quit, so the next server says so under restartInterrupted in editor_status rather than waiting for an editor that is not coming, and open finishes it, saying so under finishesRestart. An open is refused when the editor it would start could never reach this server: a project other than the one this server serves, whose editor dials that project's own server; a project without the gdharness editor addon installed and enabled; and a project whose editor a restart is already bringing back.",
|
|
17399
17466
|
parameters: {
|
|
17400
17467
|
projectPath: { ...PROJECT_PATH, ops: ["open"] },
|
|
17401
17468
|
hidden: {
|
|
17402
17469
|
type: "boolean",
|
|
17403
17470
|
ops: ["open", "restart"],
|
|
17404
|
-
description: "Open the editor headless and, on Windows, on a desktop of its own, so no window of it or of the games it plays appears. Default false: one visible editor, shown without taking the focus."
|
|
17471
|
+
description: "Open the editor headless and, on Windows, on a desktop of its own, so no window of it or of the games it plays appears. Default false: one visible editor, shown without taking the focus. On restart only for an editor a server opened; one opened by hand comes back as it was, so hidden is refused there."
|
|
17405
17472
|
}
|
|
17406
17473
|
},
|
|
17407
17474
|
requires: [],
|
|
@@ -17489,7 +17556,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17489
17556
|
requires: ["projectPath"]
|
|
17490
17557
|
},
|
|
17491
17558
|
stop: {
|
|
17492
|
-
summary: "end the run and answer with what it printed last, naming the process ended under endedPid: for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. A game that started processes of its own keeps them unless andChildren says to end those too. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled",
|
|
17559
|
+
summary: "end the run and answer with what it printed last, naming the process ended under endedPid: for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. A game that started processes of its own keeps them unless andChildren says to end those too. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled, and false with the number under stillGoing when what was told to end was still running ten seconds later",
|
|
17493
17560
|
requires: []
|
|
17494
17561
|
},
|
|
17495
17562
|
check: {
|
|
@@ -17550,7 +17617,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17550
17617
|
},
|
|
17551
17618
|
{
|
|
17552
17619
|
name: "editor_status",
|
|
17553
|
-
description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait.",
|
|
17620
|
+
description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs.",
|
|
17554
17621
|
parameters: {},
|
|
17555
17622
|
requires: []
|
|
17556
17623
|
},
|
|
@@ -17589,6 +17656,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17589
17656
|
"physics2d",
|
|
17590
17657
|
"physics3d",
|
|
17591
17658
|
"audio",
|
|
17659
|
+
"visual",
|
|
17592
17660
|
"animation",
|
|
17593
17661
|
"ui"
|
|
17594
17662
|
],
|
|
@@ -17628,7 +17696,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17628
17696
|
property: {
|
|
17629
17697
|
type: "string",
|
|
17630
17698
|
ops: ["property", "find"],
|
|
17631
|
-
description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A packed list, such as a PackedStringArray, steps the same way. A negative number counts from the end. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that takes no arguments and walks into what it returned, which is how a question only a method answers is read in one call: which control holds the focus in the viewport this one is in. A list or a map answers the calls that read it and take no arguments, "_game:run:board:size()" or "_game:inbox:keys()", and not those that change it, such as clear() or sort(). A step that is not there is named, and says what was there instead.'
|
|
17699
|
+
description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A packed list, such as a PackedStringArray, steps the same way. A negative number counts from the end. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that takes no arguments and only reads (an engine method the engine marks as reading, or a game method not declared -> void, so queue_free() is refused) and walks into what it returned, which is how a question only a method answers is read in one call: which control holds the focus in the viewport this one is in. A list or a map answers the calls that read it and take no arguments, "_game:run:board:size()" or "_game:inbox:keys()", and not those that change it, such as clear() or sort(). A step that is not there is named, and says what was there instead.'
|
|
17632
17700
|
},
|
|
17633
17701
|
depth: {
|
|
17634
17702
|
type: "integer",
|
|
@@ -17667,13 +17735,14 @@ var init_tool_definitions = __esm(() => {
|
|
|
17667
17735
|
limit: {
|
|
17668
17736
|
type: "integer",
|
|
17669
17737
|
minimum: 1,
|
|
17738
|
+
maximum: 5000,
|
|
17670
17739
|
ops: ["find", "text"],
|
|
17671
|
-
description: "find: the most nodes to answer with, default 100. text: the most lines, default 500, with omitted saying how many lines that left behind. A screen whose dialog sits under a long list is a screen read with omitted greater than zero, so raise this or point root at the dialog rather than reading the answer as what is on screen."
|
|
17740
|
+
description: "find: the most nodes to answer with, default 100. text: the most lines, default 500, with omitted saying how many lines that left behind. At most 5000 for either. A screen whose dialog sits under a long list is a screen read with omitted greater than zero, so raise this or point root at the dialog rather than reading the answer as what is on screen."
|
|
17672
17741
|
},
|
|
17673
17742
|
includeHidden: {
|
|
17674
17743
|
type: "boolean",
|
|
17675
17744
|
ops: ["text", "find"],
|
|
17676
|
-
description: "text: read hidden nodes as well, for checking that something is not showing. Default false. find: default true, since a find means the node whether or not it is drawn; false answers with what the player can actually see, which is how a panel that keeps a label for every line and hides the ones that do not is read. A node counts as hidden when
|
|
17745
|
+
description: "text: read hidden nodes as well, for checking that something is not showing. Default false. find: default true, since a find means the node whether or not it is drawn; false answers with what the player can actually see, which is how a panel that keeps a label for every line and hides the ones that do not is read. A node counts as hidden when the engine would not draw it: a hidden Control, Node2D or Node3D above it hides it, a plain Node in between breaks that chain as it does for the engine, and a hidden canvas layer or window hides everything in it. How many matches were left out comes back under hidden."
|
|
17677
17746
|
},
|
|
17678
17747
|
metrics: {
|
|
17679
17748
|
type: "array",
|
|
@@ -17772,7 +17841,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17772
17841
|
},
|
|
17773
17842
|
{
|
|
17774
17843
|
name: "runtime_capture",
|
|
17775
|
-
description: "A picture of the running game: the whole screen or one viewport, as an image. Needs the game running with a window. A capture asked for before the game's first frame is drawn waits for that frame, since the game announces itself before it and the viewport holds nothing it drew until then.",
|
|
17844
|
+
description: "A picture of the running game: the whole screen or one viewport, as an image. The whole screen includes the game's own dialogs and popups where they sit, also when a project opens them as windows of their own (subwindows not embedded). Needs the game running with a window. A capture asked for before the game's first frame is drawn waits for that frame, since the game announces itself before it and the viewport holds nothing it drew until then.",
|
|
17776
17845
|
parameters: {
|
|
17777
17846
|
projectPath: RUNNING_PROJECT_PATH,
|
|
17778
17847
|
pid: RUNNING_PID,
|
|
@@ -17946,7 +18015,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17946
18015
|
},
|
|
17947
18016
|
value: {
|
|
17948
18017
|
ops: ["until"],
|
|
17949
|
-
description: "until: the value to wait for, fitted to the property's type. On a property holding an object, whether declared to or untyped and holding one or null, the text null means null. One declared to hold an object refuses any value other than null, since it can never hold it."
|
|
18018
|
+
description: "until: the value to wait for, fitted to the property's type and compared at the precision the property keeps, so -1.1 meets a 32-bit float holding -1.10000002. A value the property would have to cut is refused: a fraction on a whole number, a number other than 0 or 1 on a bool. On a property holding an object, whether declared to or untyped and holding one or null, the text null means null. One declared to hold an object refuses any value other than null, since it can never hold it."
|
|
17950
18019
|
},
|
|
17951
18020
|
says: {
|
|
17952
18021
|
type: "string",
|
|
@@ -32778,10 +32847,10 @@ var init_stdio2 = __esm(() => {
|
|
|
32778
32847
|
});
|
|
32779
32848
|
|
|
32780
32849
|
// src/runtime-client.ts
|
|
32781
|
-
import { existsSync as existsSync12, readdirSync as readdirSync6, readFileSync as readFileSync11, statSync as
|
|
32850
|
+
import { existsSync as existsSync12, readdirSync as readdirSync6, readFileSync as readFileSync11, statSync as statSync6, unlinkSync } from "node:fs";
|
|
32782
32851
|
import { createConnection } from "node:net";
|
|
32783
32852
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
32784
|
-
import { dirname as dirname8, join as join13, resolve as
|
|
32853
|
+
import { dirname as dirname8, join as join13, resolve as resolve4 } from "node:path";
|
|
32785
32854
|
import { setTimeout as delay } from "node:timers/promises";
|
|
32786
32855
|
function runtimeDirectory(variables = process.env) {
|
|
32787
32856
|
const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
|
|
@@ -32797,7 +32866,7 @@ function runtimeDirectories(variables = process.env) {
|
|
|
32797
32866
|
for (const base of fallbacks) {
|
|
32798
32867
|
candidates.push(join13(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
|
|
32799
32868
|
}
|
|
32800
|
-
return [...new Set(candidates.map((path) =>
|
|
32869
|
+
return [...new Set(candidates.map((path) => resolve4(path)))];
|
|
32801
32870
|
}
|
|
32802
32871
|
function errorReportOf(pid, directories = runtimeDirectories()) {
|
|
32803
32872
|
for (const directory of directories) {
|
|
@@ -32850,13 +32919,14 @@ function parseAnnouncement(file, pid) {
|
|
|
32850
32919
|
};
|
|
32851
32920
|
let announcedAt;
|
|
32852
32921
|
try {
|
|
32853
|
-
announcedAt =
|
|
32922
|
+
announcedAt = statSync6(file).mtimeMs;
|
|
32854
32923
|
} catch {}
|
|
32855
32924
|
const when = announcedAt === undefined ? {} : { announcedAt };
|
|
32856
32925
|
if (protocol !== RUNTIME_PROTOCOL) {
|
|
32857
32926
|
return { kind: "unspoken", unspoken: { pid, protocol: protocol ?? 0, project: named, ...when } };
|
|
32858
32927
|
}
|
|
32859
32928
|
const editorPid = readNumber(fields, "editor_pid");
|
|
32929
|
+
const addonVersion = readString(fields, "addon_version");
|
|
32860
32930
|
return {
|
|
32861
32931
|
kind: "runtime",
|
|
32862
32932
|
endpoint: {
|
|
@@ -32866,10 +32936,17 @@ function parseAnnouncement(file, pid) {
|
|
|
32866
32936
|
project: named,
|
|
32867
32937
|
file,
|
|
32868
32938
|
...editorPid !== undefined && Number.isInteger(editorPid) && editorPid > 0 ? { editorPid } : {},
|
|
32869
|
-
...when
|
|
32939
|
+
...when,
|
|
32940
|
+
...addonVersion === undefined ? {} : { addon: { version: addonVersion, digest: readString(fields, "addon_digest") ?? "" } }
|
|
32870
32941
|
}
|
|
32871
32942
|
};
|
|
32872
32943
|
}
|
|
32944
|
+
function dialAddress(address) {
|
|
32945
|
+
if (address === "*" || address === "0.0.0.0") {
|
|
32946
|
+
return "127.0.0.1";
|
|
32947
|
+
}
|
|
32948
|
+
return address === "::" ? "::1" : address;
|
|
32949
|
+
}
|
|
32873
32950
|
function runtimesAnnounced(directories = runtimeDirectories()) {
|
|
32874
32951
|
const running = new Map;
|
|
32875
32952
|
const unspoken = new Map;
|
|
@@ -32956,7 +33033,7 @@ function announcedIn(directory) {
|
|
|
32956
33033
|
entries.push({ file, pid, running });
|
|
32957
33034
|
if (running) {
|
|
32958
33035
|
try {
|
|
32959
|
-
candidates.push({ file, pid, writtenAt:
|
|
33036
|
+
candidates.push({ file, pid, writtenAt: statSync6(file).mtimeMs });
|
|
32960
33037
|
} catch {}
|
|
32961
33038
|
}
|
|
32962
33039
|
}
|
|
@@ -33013,7 +33090,7 @@ function sweepErrorReport(path, pid, now = Date.now()) {
|
|
|
33013
33090
|
return;
|
|
33014
33091
|
}
|
|
33015
33092
|
try {
|
|
33016
|
-
if (now -
|
|
33093
|
+
if (now - statSync6(path).mtimeMs >= ERROR_REPORT_KEEP_MS) {
|
|
33017
33094
|
unlinkSync(path);
|
|
33018
33095
|
}
|
|
33019
33096
|
} catch {}
|
|
@@ -33043,7 +33120,7 @@ function theOneProject(endpoints) {
|
|
|
33043
33120
|
if (endpoints.some((endpoint) => endpoint.project.path === "")) {
|
|
33044
33121
|
return;
|
|
33045
33122
|
}
|
|
33046
|
-
const paths = new Set(endpoints.map((endpoint) => realPathOr(
|
|
33123
|
+
const paths = new Set(endpoints.map((endpoint) => realPathOr(resolve4(endpoint.project.path))));
|
|
33047
33124
|
const only = [...paths][0];
|
|
33048
33125
|
return paths.size === 1 ? only : undefined;
|
|
33049
33126
|
}
|
|
@@ -33074,7 +33151,7 @@ function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
|
|
|
33074
33151
|
if (named !== undefined) {
|
|
33075
33152
|
if (projectPath !== undefined && !isSameDirectory(named.project.path, projectPath)) {
|
|
33076
33153
|
return {
|
|
33077
|
-
problem: `The game with pid ${pid} is running ${named.project.path}, not ${
|
|
33154
|
+
problem: `The game with pid ${pid} is running ${named.project.path}, not ${resolve4(projectPath)}. Running: ${endpoints.map(describe2).join("; ")}.`
|
|
33078
33155
|
};
|
|
33079
33156
|
}
|
|
33080
33157
|
return { endpoint: named };
|
|
@@ -33114,7 +33191,7 @@ function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
|
|
|
33114
33191
|
};
|
|
33115
33192
|
}
|
|
33116
33193
|
if (projectPath !== undefined) {
|
|
33117
|
-
const wanted =
|
|
33194
|
+
const wanted = resolve4(projectPath);
|
|
33118
33195
|
const matching = endpoints.filter((endpoint) => isSameDirectory(endpoint.project.path, wanted));
|
|
33119
33196
|
if (matching.length === 0) {
|
|
33120
33197
|
return {
|
|
@@ -33159,7 +33236,7 @@ function runtimeRequest(endpoint, command, params, timeoutMs, keepListening = fa
|
|
|
33159
33236
|
let late = false;
|
|
33160
33237
|
let buffered = "";
|
|
33161
33238
|
let welcomed = false;
|
|
33162
|
-
const socket = createConnection({ port: endpoint.port, host: endpoint.address });
|
|
33239
|
+
const socket = createConnection({ port: endpoint.port, host: dialAddress(endpoint.address) });
|
|
33163
33240
|
let lateTimer = null;
|
|
33164
33241
|
const finish = (reply) => {
|
|
33165
33242
|
if (late) {
|
|
@@ -33323,7 +33400,7 @@ var init_boot_note = __esm(() => {
|
|
|
33323
33400
|
|
|
33324
33401
|
// src/breakpoint-note.ts
|
|
33325
33402
|
import { mkdirSync as mkdirSync7, readFileSync as readFileSync13, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "node:fs";
|
|
33326
|
-
import { dirname as dirname10, isAbsolute as isAbsolute2, join as join15, relative as
|
|
33403
|
+
import { dirname as dirname10, isAbsolute as isAbsolute2, join as join15, relative as relative3 } from "node:path";
|
|
33327
33404
|
function breakpointNotePath(projectPath) {
|
|
33328
33405
|
return join15(projectPath, ".godot", "gdharness-breakpoints.json");
|
|
33329
33406
|
}
|
|
@@ -33338,7 +33415,7 @@ function writeBreakpointNote(projectPath, held) {
|
|
|
33338
33415
|
}
|
|
33339
33416
|
const note = {
|
|
33340
33417
|
breakpoints: inside.map((one) => ({
|
|
33341
|
-
script:
|
|
33418
|
+
script: relative3(root, one.scriptPath).replaceAll("\\", "/"),
|
|
33342
33419
|
lines: [...one.lines]
|
|
33343
33420
|
}))
|
|
33344
33421
|
};
|
|
@@ -33375,7 +33452,7 @@ function readBreakpointNote(projectPath) {
|
|
|
33375
33452
|
}
|
|
33376
33453
|
}
|
|
33377
33454
|
function withinProject(projectPath, scriptPath) {
|
|
33378
|
-
const rel =
|
|
33455
|
+
const rel = relative3(projectPath, scriptPath);
|
|
33379
33456
|
return rel !== "" && !rel.startsWith("..") && !isAbsolute2(rel);
|
|
33380
33457
|
}
|
|
33381
33458
|
var init_breakpoint_note = __esm(() => {
|
|
@@ -34238,7 +34315,7 @@ import {
|
|
|
34238
34315
|
readFileSync as readFileSync16,
|
|
34239
34316
|
readSync,
|
|
34240
34317
|
rmSync as rmSync8,
|
|
34241
|
-
statSync as
|
|
34318
|
+
statSync as statSync7,
|
|
34242
34319
|
writeFileSync as writeFileSync11
|
|
34243
34320
|
} from "node:fs";
|
|
34244
34321
|
import { dirname as dirname12, join as join18 } from "node:path";
|
|
@@ -34301,7 +34378,7 @@ function editorConsole(projectPath, editorPid, openedByAServer) {
|
|
|
34301
34378
|
return { log, path, ...of === undefined ? {} : { readBytes: text.length, of } };
|
|
34302
34379
|
}
|
|
34303
34380
|
function theFirstOf(path) {
|
|
34304
|
-
const whole =
|
|
34381
|
+
const whole = statSync7(path).size;
|
|
34305
34382
|
if (whole <= MOST_CONSOLE_BYTES) {
|
|
34306
34383
|
return { text: readFileSync16(path, "utf8") };
|
|
34307
34384
|
}
|
|
@@ -37372,7 +37449,10 @@ function mayYetConnect(listeningSince, now = Date.now()) {
|
|
|
37372
37449
|
return listeningSince === undefined || now - listeningSince.getTime() < CONNECT_WINDOW_MS;
|
|
37373
37450
|
}
|
|
37374
37451
|
function theEditorHasComeBack(status, startedAt) {
|
|
37375
|
-
return status
|
|
37452
|
+
return hasSaidWhoItIs(status) && (status.connectedAt?.getTime() ?? 0) > startedAt;
|
|
37453
|
+
}
|
|
37454
|
+
function hasSaidWhoItIs(status) {
|
|
37455
|
+
return status.connected && status.addonVersion !== undefined;
|
|
37376
37456
|
}
|
|
37377
37457
|
function anEditorIsStillComing(listeningSince, launchedEditorIsAlive, now = Date.now()) {
|
|
37378
37458
|
return mayYetConnect(listeningSince, now) || launchedEditorIsAlive;
|
|
@@ -37531,6 +37611,7 @@ var init_godot_bridge = __esm(() => {
|
|
|
37531
37611
|
dapPort: this.connectionInfo?.dapPort,
|
|
37532
37612
|
debugPort: this.connectionInfo?.debugPort,
|
|
37533
37613
|
openedByAServer: this.connectionInfo?.openedByAServer,
|
|
37614
|
+
startedByAnEditor: this.connectionInfo?.startedByAnEditor,
|
|
37534
37615
|
syncsBreakpoints: this.connectionInfo?.syncsBreakpoints,
|
|
37535
37616
|
pendingRequests: this.pendingRequests.size,
|
|
37536
37617
|
queuedResources: this.resourceQueues.size,
|
|
@@ -37676,6 +37757,7 @@ var init_godot_bridge = __esm(() => {
|
|
|
37676
37757
|
this.connectionInfo.dapPort = servedPort(message.dap_port);
|
|
37677
37758
|
this.connectionInfo.debugPort = servedPort(message.debug_port);
|
|
37678
37759
|
this.connectionInfo.openedByAServer = message.opened_by_a_server === true;
|
|
37760
|
+
this.connectionInfo.startedByAnEditor = typeof message.started_by_an_editor === "boolean" ? message.started_by_an_editor : undefined;
|
|
37679
37761
|
this.connectionInfo.syncsBreakpoints = typeof message.syncs_breakpoints === "boolean" ? message.syncs_breakpoints : undefined;
|
|
37680
37762
|
this.log("info", `Godot ready: ${message.project_path}`);
|
|
37681
37763
|
this.emitBridgeEvent("godot_connected", { projectPath: message.project_path });
|
|
@@ -37999,6 +38081,7 @@ function parseJUnit(xml) {
|
|
|
37999
38081
|
const cases = suite.children.filter((child) => child.name === "testcase").map((element) => caseOf(suite, element));
|
|
38000
38082
|
const failures = cases.filter((entry) => entry.status === "failed").length;
|
|
38001
38083
|
const errors = cases.filter((entry) => entry.status === "error").length;
|
|
38084
|
+
const written = (kind) => suite.children.filter((child) => child.name === "testcase").reduce((sum, element) => sum + element.children.filter((child) => child.name === kind).length, 0);
|
|
38002
38085
|
return {
|
|
38003
38086
|
name: suite.attributes["name"] ?? "",
|
|
38004
38087
|
path: pathOf(suite),
|
|
@@ -38006,8 +38089,8 @@ function parseJUnit(xml) {
|
|
|
38006
38089
|
discovered: count(suite.attributes, "tests"),
|
|
38007
38090
|
failures,
|
|
38008
38091
|
errors,
|
|
38009
|
-
hookFailures: Math.max(0, count(suite.attributes, "failures") -
|
|
38010
|
-
hookErrors: Math.max(0, count(suite.attributes, "errors") -
|
|
38092
|
+
hookFailures: Math.max(0, count(suite.attributes, "failures") - written("failure")),
|
|
38093
|
+
hookErrors: Math.max(0, count(suite.attributes, "errors") - written("error")),
|
|
38011
38094
|
skipped: cases.filter((entry) => entry.status === "skipped").length,
|
|
38012
38095
|
time: count(suite.attributes, "time"),
|
|
38013
38096
|
cases
|
|
@@ -38200,11 +38283,53 @@ var init_junit = __esm(() => {
|
|
|
38200
38283
|
ORPHANS = /(\d+)\s+orphans/;
|
|
38201
38284
|
});
|
|
38202
38285
|
|
|
38286
|
+
// src/keeper-channel.ts
|
|
38287
|
+
import { connect as connect2, createServer as createServer2 } from "node:net";
|
|
38288
|
+
function askTheKeeperToStop(address, withinMs) {
|
|
38289
|
+
return new Promise((resolve) => {
|
|
38290
|
+
let said = "";
|
|
38291
|
+
let settled = false;
|
|
38292
|
+
const settle = (answer) => {
|
|
38293
|
+
if (!settled) {
|
|
38294
|
+
settled = true;
|
|
38295
|
+
clearTimeout(timer);
|
|
38296
|
+
socket.destroy();
|
|
38297
|
+
resolve(answer);
|
|
38298
|
+
}
|
|
38299
|
+
};
|
|
38300
|
+
const socket = connect2(address);
|
|
38301
|
+
const timer = setTimeout(() => {
|
|
38302
|
+
settle({ unreached: `it did not answer within ${withinMs} ms` });
|
|
38303
|
+
}, withinMs);
|
|
38304
|
+
socket.setEncoding("utf8");
|
|
38305
|
+
socket.on("connect", () => {
|
|
38306
|
+
socket.write(`stop
|
|
38307
|
+
`);
|
|
38308
|
+
});
|
|
38309
|
+
socket.on("data", (chunk) => {
|
|
38310
|
+
said += chunk;
|
|
38311
|
+
const line = said.split(`
|
|
38312
|
+
`)[0] ?? "";
|
|
38313
|
+
if (said.includes(`
|
|
38314
|
+
`)) {
|
|
38315
|
+
settle(line === "signalled" || line === "gone" ? line : { unreached: `it answered ${JSON.stringify(line)}` });
|
|
38316
|
+
}
|
|
38317
|
+
});
|
|
38318
|
+
socket.on("error", (error) => {
|
|
38319
|
+
settle({ unreached: error.code ?? error.message });
|
|
38320
|
+
});
|
|
38321
|
+
socket.on("close", () => {
|
|
38322
|
+
settle({ unreached: "it closed the connection without answering" });
|
|
38323
|
+
});
|
|
38324
|
+
});
|
|
38325
|
+
}
|
|
38326
|
+
var init_keeper_channel = () => {};
|
|
38327
|
+
|
|
38203
38328
|
// src/lsp_client.ts
|
|
38204
|
-
import { existsSync as existsSync15, realpathSync as
|
|
38329
|
+
import { existsSync as existsSync15, realpathSync as realpathSync4 } from "node:fs";
|
|
38205
38330
|
import { readFile, realpath } from "node:fs/promises";
|
|
38206
38331
|
import { createConnection as createConnection3 } from "node:net";
|
|
38207
|
-
import { dirname as dirname13, join as join19, resolve as
|
|
38332
|
+
import { dirname as dirname13, join as join19, resolve as resolve5 } from "node:path";
|
|
38208
38333
|
import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
|
|
38209
38334
|
function diagnosticsKey(uri) {
|
|
38210
38335
|
let decoded;
|
|
@@ -38217,7 +38342,7 @@ function diagnosticsKey(uri) {
|
|
|
38217
38342
|
return decoded;
|
|
38218
38343
|
}
|
|
38219
38344
|
try {
|
|
38220
|
-
return
|
|
38345
|
+
return realpathSync4.native(fileURLToPath3(decoded)).toLowerCase();
|
|
38221
38346
|
} catch {
|
|
38222
38347
|
return decoded.toLowerCase();
|
|
38223
38348
|
}
|
|
@@ -38334,7 +38459,7 @@ class GodotLSPClient {
|
|
|
38334
38459
|
if (this.initialized) {
|
|
38335
38460
|
return;
|
|
38336
38461
|
}
|
|
38337
|
-
const rootPath = this.rootPath ?? dirname13(
|
|
38462
|
+
const rootPath = this.rootPath ?? dirname13(resolve5(filePath));
|
|
38338
38463
|
await this.initialize(rootPath);
|
|
38339
38464
|
}
|
|
38340
38465
|
async sendRequest(method, params) {
|
|
@@ -38476,7 +38601,7 @@ class GodotLSPClient {
|
|
|
38476
38601
|
});
|
|
38477
38602
|
}
|
|
38478
38603
|
toFileUri(filePath) {
|
|
38479
|
-
return pathToFileURL(
|
|
38604
|
+
return pathToFileURL(resolve5(filePath)).href;
|
|
38480
38605
|
}
|
|
38481
38606
|
syncDocument(filePath, content) {
|
|
38482
38607
|
const uri = this.toFileUri(filePath);
|
|
@@ -38520,7 +38645,7 @@ class GodotLSPClient {
|
|
|
38520
38645
|
}
|
|
38521
38646
|
async initialize(rootPath) {
|
|
38522
38647
|
await this.ensureConnected();
|
|
38523
|
-
const resolvedRootPath =
|
|
38648
|
+
const resolvedRootPath = resolve5(rootPath);
|
|
38524
38649
|
const rootUri = pathToFileURL(resolvedRootPath).href;
|
|
38525
38650
|
this.forgetServedWorkspace();
|
|
38526
38651
|
const result = await this.sendRequest("initialize", {
|
|
@@ -38689,7 +38814,7 @@ function normalizeLSPError(error, port) {
|
|
|
38689
38814
|
return String(error);
|
|
38690
38815
|
}
|
|
38691
38816
|
async function resolveLSPPaths(projectPathValue, scriptPathValue) {
|
|
38692
|
-
const requestedProjectPath =
|
|
38817
|
+
const requestedProjectPath = resolve5(projectPathValue);
|
|
38693
38818
|
let projectPath;
|
|
38694
38819
|
try {
|
|
38695
38820
|
projectPath = await realpath(requestedProjectPath);
|
|
@@ -38917,7 +39042,7 @@ function projectStructure(projectPath) {
|
|
|
38917
39042
|
visit(join20(directory, entry.name));
|
|
38918
39043
|
} else if (entry.isFile()) {
|
|
38919
39044
|
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
38920
|
-
if (extension
|
|
39045
|
+
if (SCENE_EXTENSIONS.has(extension)) {
|
|
38921
39046
|
structure.scenes += 1;
|
|
38922
39047
|
} else if (extension === "gd" || extension === "cs") {
|
|
38923
39048
|
structure.scripts += 1;
|
|
@@ -38959,8 +39084,11 @@ function scriptsWithoutUid(projectPath) {
|
|
|
38959
39084
|
visit(projectPath, "");
|
|
38960
39085
|
return found.sort();
|
|
38961
39086
|
}
|
|
39087
|
+
function isBinary(bytes) {
|
|
39088
|
+
return bytes.subarray(0, 8000).includes(0);
|
|
39089
|
+
}
|
|
38962
39090
|
function searchProject(projectPath, options) {
|
|
38963
|
-
const extensions = new Set(options.fileTypes.map((ext) => ext.replace(/^\./, "").toLowerCase()).filter(Boolean));
|
|
39091
|
+
const extensions = options.fileTypes === undefined ? null : new Set(options.fileTypes.map((ext) => ext.replace(/^\./, "").toLowerCase()).filter(Boolean));
|
|
38964
39092
|
const result = {
|
|
38965
39093
|
query: options.query,
|
|
38966
39094
|
results: [],
|
|
@@ -38968,13 +39096,7 @@ function searchProject(projectPath, options) {
|
|
|
38968
39096
|
};
|
|
38969
39097
|
const regex = options.regex ? new RegExp(options.query, options.caseSensitive ? "" : "i") : null;
|
|
38970
39098
|
const needle = options.caseSensitive ? options.query : options.query.toLowerCase();
|
|
38971
|
-
const full = () =>
|
|
38972
|
-
if (result.summary.total_matches >= options.maxResults) {
|
|
38973
|
-
result.summary.truncated = true;
|
|
38974
|
-
return true;
|
|
38975
|
-
}
|
|
38976
|
-
return false;
|
|
38977
|
-
};
|
|
39099
|
+
const full = () => result.summary.truncated;
|
|
38978
39100
|
const visit = (directory) => {
|
|
38979
39101
|
if (steppedOver(directory)) {
|
|
38980
39102
|
return;
|
|
@@ -38992,21 +39114,26 @@ function searchProject(projectPath, options) {
|
|
|
38992
39114
|
continue;
|
|
38993
39115
|
}
|
|
38994
39116
|
const extension = entry.name.includes(".") ? entry.name.split(".").pop()?.toLowerCase() ?? "" : "";
|
|
38995
|
-
if (!entry.isFile() || !extensions.has(extension)) {
|
|
39117
|
+
if (!entry.isFile() || extensions !== null && !extensions.has(extension)) {
|
|
39118
|
+
continue;
|
|
39119
|
+
}
|
|
39120
|
+
const bytes = readFileSync17(entryPath);
|
|
39121
|
+
if (extensions === null && isBinary(bytes)) {
|
|
38996
39122
|
continue;
|
|
38997
39123
|
}
|
|
38998
39124
|
result.summary.files_searched += 1;
|
|
38999
39125
|
const matches = [];
|
|
39000
|
-
for (const [index, line] of
|
|
39001
|
-
|
|
39002
|
-
if (
|
|
39003
|
-
|
|
39126
|
+
for (const [index, line] of bytes.toString("utf8").split(/\r?\n/).entries()) {
|
|
39127
|
+
const found = regex ? regex.exec(line) : (options.caseSensitive ? line : line.toLowerCase()).includes(needle) ? [options.query] : null;
|
|
39128
|
+
if (found === null) {
|
|
39129
|
+
continue;
|
|
39004
39130
|
}
|
|
39005
|
-
|
|
39006
|
-
|
|
39007
|
-
|
|
39008
|
-
result.summary.total_matches += 1;
|
|
39131
|
+
if (result.summary.total_matches >= options.maxResults) {
|
|
39132
|
+
result.summary.truncated = true;
|
|
39133
|
+
break;
|
|
39009
39134
|
}
|
|
39135
|
+
matches.push({ line: index + 1, content: line.trim(), match: found[0] ?? "" });
|
|
39136
|
+
result.summary.total_matches += 1;
|
|
39010
39137
|
}
|
|
39011
39138
|
if (matches.length > 0) {
|
|
39012
39139
|
const relativePath = entryPath.slice(projectPath.length + 1).replace(/\\/g, "/");
|
|
@@ -39018,10 +39145,40 @@ function searchProject(projectPath, options) {
|
|
|
39018
39145
|
visit(projectPath);
|
|
39019
39146
|
return result;
|
|
39020
39147
|
}
|
|
39021
|
-
var SKIPPED, ASSET_EXTENSIONS, CARRIES_A_UID;
|
|
39148
|
+
var SKIPPED, SCENE_EXTENSIONS, ASSET_EXTENSIONS, CARRIES_A_UID;
|
|
39022
39149
|
var init_project_scan = __esm(() => {
|
|
39023
39150
|
SKIPPED = new Set(["node_modules"]);
|
|
39024
|
-
|
|
39151
|
+
SCENE_EXTENSIONS = new Set(["tscn", "scn"]);
|
|
39152
|
+
ASSET_EXTENSIONS = new Set([
|
|
39153
|
+
"png",
|
|
39154
|
+
"jpg",
|
|
39155
|
+
"jpeg",
|
|
39156
|
+
"webp",
|
|
39157
|
+
"svg",
|
|
39158
|
+
"bmp",
|
|
39159
|
+
"tga",
|
|
39160
|
+
"exr",
|
|
39161
|
+
"hdr",
|
|
39162
|
+
"ktx",
|
|
39163
|
+
"dds",
|
|
39164
|
+
"ttf",
|
|
39165
|
+
"otf",
|
|
39166
|
+
"woff",
|
|
39167
|
+
"woff2",
|
|
39168
|
+
"fnt",
|
|
39169
|
+
"wav",
|
|
39170
|
+
"mp3",
|
|
39171
|
+
"ogg",
|
|
39172
|
+
"glb",
|
|
39173
|
+
"gltf",
|
|
39174
|
+
"obj",
|
|
39175
|
+
"fbx",
|
|
39176
|
+
"blend",
|
|
39177
|
+
"dae",
|
|
39178
|
+
"ogv",
|
|
39179
|
+
"gdshader",
|
|
39180
|
+
"shader"
|
|
39181
|
+
]);
|
|
39025
39182
|
CARRIES_A_UID = new Set(["gd", "shader", "gdshader"]);
|
|
39026
39183
|
});
|
|
39027
39184
|
|
|
@@ -39075,12 +39232,12 @@ import {
|
|
|
39075
39232
|
openSync as openSync2,
|
|
39076
39233
|
readdirSync as readdirSync8,
|
|
39077
39234
|
readFileSync as readFileSync19,
|
|
39078
|
-
realpathSync as
|
|
39235
|
+
realpathSync as realpathSync5,
|
|
39079
39236
|
rmSync as rmSync10,
|
|
39080
|
-
statSync as
|
|
39237
|
+
statSync as statSync8,
|
|
39081
39238
|
writeFileSync as writeFileSync13
|
|
39082
39239
|
} from "node:fs";
|
|
39083
|
-
import { basename as
|
|
39240
|
+
import { basename as basename5, dirname as dirname15, join as join22, resolve as resolve6 } from "node:path";
|
|
39084
39241
|
import { promisify as promisify4 } from "node:util";
|
|
39085
39242
|
function runsDirectory() {
|
|
39086
39243
|
return join22(runtimeDirectory(), "runs");
|
|
@@ -39090,19 +39247,19 @@ function realAsFarAsItExists(path) {
|
|
|
39090
39247
|
let at = path;
|
|
39091
39248
|
for (;; ) {
|
|
39092
39249
|
try {
|
|
39093
|
-
return join22(
|
|
39250
|
+
return join22(realpathSync5.native(at), ...missing);
|
|
39094
39251
|
} catch {
|
|
39095
39252
|
const parent = dirname15(at);
|
|
39096
39253
|
if (parent === at) {
|
|
39097
39254
|
return path;
|
|
39098
39255
|
}
|
|
39099
|
-
missing.unshift(
|
|
39256
|
+
missing.unshift(basename5(at));
|
|
39100
39257
|
at = parent;
|
|
39101
39258
|
}
|
|
39102
39259
|
}
|
|
39103
39260
|
}
|
|
39104
39261
|
function projectKey(projectPath) {
|
|
39105
|
-
const real = realAsFarAsItExists(
|
|
39262
|
+
const real = realAsFarAsItExists(resolve6(projectPath)).replace(/[\\/]+$/, "");
|
|
39106
39263
|
const folded = process.platform === "win32" || process.platform === "darwin" ? real.toLowerCase() : real;
|
|
39107
39264
|
return createHash4("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
|
|
39108
39265
|
}
|
|
@@ -39209,7 +39366,10 @@ function recordAt(path) {
|
|
|
39209
39366
|
pid,
|
|
39210
39367
|
transcript,
|
|
39211
39368
|
startedAt,
|
|
39369
|
+
...typeof fields["startedBy"] === "number" ? { startedBy: fields["startedBy"] } : {},
|
|
39212
39370
|
projectPath: typeof fields["projectPath"] === "string" ? fields["projectPath"] : "",
|
|
39371
|
+
...typeof fields["servedBy"] === "string" && fields["servedBy"] !== "" ? { servedBy: fields["servedBy"] } : {},
|
|
39372
|
+
...typeof fields["keeper"] === "string" && fields["keeper"] !== "" ? { keeper: fields["keeper"] } : {},
|
|
39213
39373
|
arguments: Array.isArray(args) ? args.filter((value) => typeof value === "string") : [],
|
|
39214
39374
|
...typeof fields["command"] === "string" ? { command: fields["command"] } : {},
|
|
39215
39375
|
...typeof fields["exitCode"] === "number" ? { exitCode: fields["exitCode"] } : {},
|
|
@@ -39218,7 +39378,8 @@ function recordAt(path) {
|
|
|
39218
39378
|
}
|
|
39219
39379
|
async function runningAs(pid) {
|
|
39220
39380
|
if (process.platform === "win32") {
|
|
39221
|
-
const
|
|
39381
|
+
const read = await readWindowsCommandLine(pid);
|
|
39382
|
+
const answer = "answer" in read ? read.answer : null;
|
|
39222
39383
|
if (answer !== null) {
|
|
39223
39384
|
const [, ...rest] = answer.split(`
|
|
39224
39385
|
`);
|
|
@@ -39230,7 +39391,8 @@ async function runningAs(pid) {
|
|
|
39230
39391
|
}
|
|
39231
39392
|
}
|
|
39232
39393
|
const image = await windowsImage(pid);
|
|
39233
|
-
|
|
39394
|
+
const unread = "unread" in read ? read.unread : "Windows gave an empty command line";
|
|
39395
|
+
return "image" in image ? { kind: "image", text: image.image, unread } : { kind: "none", text: "", unread: `${unread}; tasklist ${image.why}` };
|
|
39234
39396
|
}
|
|
39235
39397
|
try {
|
|
39236
39398
|
const began = await startedAt(pid, null);
|
|
@@ -39281,13 +39443,18 @@ function listeningPidInNetstat(printed, port) {
|
|
|
39281
39443
|
}
|
|
39282
39444
|
return null;
|
|
39283
39445
|
}
|
|
39284
|
-
async function
|
|
39285
|
-
|
|
39286
|
-
|
|
39287
|
-
|
|
39288
|
-
|
|
39289
|
-
|
|
39446
|
+
async function readWindowsCommandLine(pid, ask = askWindows) {
|
|
39447
|
+
const query = `$p = Get-CimInstance Win32_Process -Filter "ProcessId=${pid}"; if ($p) { $p.CreationDate.ToUniversalTime().ToString("o"); $p.CommandLine }`;
|
|
39448
|
+
let unread = "";
|
|
39449
|
+
for (const withinMs of [15000, 45000]) {
|
|
39450
|
+
try {
|
|
39451
|
+
const answer = (await ask(query, withinMs)).trim();
|
|
39452
|
+
return answer === "" ? { unread: "Windows gave no command line for it" } : { answer };
|
|
39453
|
+
} catch (error) {
|
|
39454
|
+
unread = errorMessage(error);
|
|
39455
|
+
}
|
|
39290
39456
|
}
|
|
39457
|
+
return { unread };
|
|
39291
39458
|
}
|
|
39292
39459
|
async function startedAt(pid, windowsAnswer) {
|
|
39293
39460
|
if (process.platform === "win32") {
|
|
@@ -39310,41 +39477,53 @@ async function windowsImage(pid) {
|
|
|
39310
39477
|
timeout: 15000,
|
|
39311
39478
|
windowsHide: true
|
|
39312
39479
|
});
|
|
39313
|
-
|
|
39314
|
-
|
|
39315
|
-
|
|
39480
|
+
const image = /^"([^"]+)"/.exec(csv.trim())?.[1];
|
|
39481
|
+
return image === undefined ? { why: "named no process under that number" } : { image };
|
|
39482
|
+
} catch (error) {
|
|
39483
|
+
const failed = error;
|
|
39484
|
+
return { why: failed.killed === true ? "did not answer within 15000 ms" : errorMessage(error) };
|
|
39316
39485
|
}
|
|
39317
39486
|
}
|
|
39318
|
-
async function
|
|
39319
|
-
return
|
|
39487
|
+
async function whyNotStillTheRecordedRun(record) {
|
|
39488
|
+
return whyNotTheRun(record, await runningAs(record.pid), "confirmed");
|
|
39320
39489
|
}
|
|
39321
39490
|
async function couldStillBeTheRecordedRun(record) {
|
|
39322
39491
|
return judgeRun(record, await runningAs(record.pid), "possible");
|
|
39323
39492
|
}
|
|
39324
39493
|
function judgeRun(record, running, needed) {
|
|
39494
|
+
return whyNotTheRun(record, running, needed) === null;
|
|
39495
|
+
}
|
|
39496
|
+
function whyNotTheRun(record, running, needed) {
|
|
39325
39497
|
if (running === null) {
|
|
39326
|
-
return
|
|
39498
|
+
return "the operating system would not describe that process";
|
|
39327
39499
|
}
|
|
39328
|
-
if (running.
|
|
39329
|
-
return
|
|
39500
|
+
if (running.kind === "none") {
|
|
39501
|
+
return `the operating system would not describe that process (${running.unread ?? "no reason given"})`;
|
|
39502
|
+
}
|
|
39503
|
+
const after = running.startedAt === undefined ? 0 : running.startedAt - (record.startedBy ?? record.startedAt);
|
|
39504
|
+
if (after > SAME_RUN_WINDOW_MS) {
|
|
39505
|
+
return `that process started ${Math.round(after / 1000)} seconds after the run had`;
|
|
39330
39506
|
}
|
|
39331
|
-
const engine = record.command === undefined ? null :
|
|
39507
|
+
const engine = record.command === undefined ? null : basename5(record.command);
|
|
39332
39508
|
const said = process.platform === "win32" ? running.text.toLowerCase() : running.text;
|
|
39333
39509
|
const wanted = engine === null || process.platform !== "win32" ? engine : engine.toLowerCase();
|
|
39334
39510
|
if (wanted !== null && !said.includes(wanted)) {
|
|
39335
|
-
return
|
|
39511
|
+
return `that process is not ${engine ?? "the engine"}, which the run was started with`;
|
|
39336
39512
|
}
|
|
39337
39513
|
if (running.kind === "commandLine" && readCommandLine(running.text).editor) {
|
|
39338
|
-
return
|
|
39514
|
+
return "that process is an editor";
|
|
39339
39515
|
}
|
|
39340
39516
|
if (record.projectPath === "") {
|
|
39341
|
-
return wanted !== null;
|
|
39517
|
+
return wanted !== null ? null : "the record names neither the project nor the engine";
|
|
39342
39518
|
}
|
|
39343
39519
|
if (running.kind === "image") {
|
|
39344
|
-
|
|
39520
|
+
if (wanted === null) {
|
|
39521
|
+
return "the operating system named only its executable, and the record names no engine";
|
|
39522
|
+
}
|
|
39523
|
+
return needed === "possible" ? null : `the operating system named only its executable and not its command line (${running.unread ?? "no reason given"}), which is not enough to signal it on`;
|
|
39345
39524
|
}
|
|
39346
39525
|
const project = process.platform === "win32" ? record.projectPath.toLowerCase() : record.projectPath;
|
|
39347
|
-
return said.includes(project)
|
|
39526
|
+
return said.includes(project) ? null : `that process's command line does not name ${record.projectPath}`;
|
|
39348
39527
|
}
|
|
39349
39528
|
async function stillTheAnnouncedGame(pid, projectPath, announcedAt) {
|
|
39350
39529
|
return judgeAnnouncedGame(await runningAs(pid), projectPath, announcedAt);
|
|
@@ -39376,7 +39555,7 @@ function clearRunRecord(projectPath) {
|
|
|
39376
39555
|
}
|
|
39377
39556
|
function ageMs(path, now) {
|
|
39378
39557
|
try {
|
|
39379
|
-
return now -
|
|
39558
|
+
return now - statSync8(path).mtimeMs;
|
|
39380
39559
|
} catch {
|
|
39381
39560
|
return 0;
|
|
39382
39561
|
}
|
|
@@ -39405,6 +39584,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
39405
39584
|
}
|
|
39406
39585
|
var execFileAsync, SHARED_NOTE = "run.json", SAME_RUN_WINDOW_MS = 1e4;
|
|
39407
39586
|
var init_run_record = __esm(() => {
|
|
39587
|
+
init_errors();
|
|
39408
39588
|
init_paths();
|
|
39409
39589
|
init_process_children();
|
|
39410
39590
|
init_runtime_client();
|
|
@@ -39428,12 +39608,19 @@ import {
|
|
|
39428
39608
|
readFileSync as readFileSync20,
|
|
39429
39609
|
readSync as readSync2,
|
|
39430
39610
|
rmdirSync as rmdirSync2,
|
|
39431
|
-
statSync as
|
|
39611
|
+
statSync as statSync9
|
|
39432
39612
|
} from "node:fs";
|
|
39433
|
-
import { basename as
|
|
39613
|
+
import { basename as basename6, dirname as dirname16, extname as extname2, isAbsolute as isAbsolute3, join as join23, normalize as normalize2, relative as relative4 } from "node:path";
|
|
39434
39614
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
39435
39615
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
39436
39616
|
import { promisify as promisify5 } from "node:util";
|
|
39617
|
+
function editorRestartTimeoutMs() {
|
|
39618
|
+
const override = Number.parseInt(envValue("GDHARNESS_EDITOR_RESTART_TIMEOUT_MS") ?? "", 10);
|
|
39619
|
+
return Number.isInteger(override) && override > 0 ? override : 90000;
|
|
39620
|
+
}
|
|
39621
|
+
function restartUnderway(note, now = Date.now()) {
|
|
39622
|
+
return alive(note.byPid) && now - Date.parse(note.quitAt) < editorRestartTimeoutMs() + 30000;
|
|
39623
|
+
}
|
|
39437
39624
|
function captureDestinationRefusal(outputPath, projects) {
|
|
39438
39625
|
if (/^(res|user):\/\//.test(outputPath)) {
|
|
39439
39626
|
return `outputPath ${outputPath} is a Godot path; a capture is saved outside the project, at an absolute path on this machine.`;
|
|
@@ -39448,7 +39635,7 @@ function captureDestinationRefusal(outputPath, projects) {
|
|
|
39448
39635
|
if (!existsSync17(directory)) {
|
|
39449
39636
|
return `outputPath's directory ${dirname16(outputPath)} does not exist; a capture creates no directories.`;
|
|
39450
39637
|
}
|
|
39451
|
-
const where = join23(directory,
|
|
39638
|
+
const where = join23(directory, basename6(outputPath));
|
|
39452
39639
|
const inside = projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
|
|
39453
39640
|
if (inside !== undefined) {
|
|
39454
39641
|
return `outputPath ${outputPath} is inside the project at ${inside}; a capture is saved outside it, so it never becomes part of the game.`;
|
|
@@ -39462,7 +39649,7 @@ function patienceForFrames(frames, atLeast) {
|
|
|
39462
39649
|
return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
|
|
39463
39650
|
}
|
|
39464
39651
|
function projectSpelling(projectPath, absolutePath) {
|
|
39465
|
-
return `res://${
|
|
39652
|
+
return `res://${relative4(realPathOr(projectPath), realPathOr(absolutePath)).replaceAll("\\", "/")}`;
|
|
39466
39653
|
}
|
|
39467
39654
|
function whichGame(args) {
|
|
39468
39655
|
return { projectPath: args["projectPath"], pid: args["pid"] };
|
|
@@ -39584,22 +39771,31 @@ function stopVerdict(stop) {
|
|
|
39584
39771
|
withChildren: false
|
|
39585
39772
|
};
|
|
39586
39773
|
}
|
|
39774
|
+
if (stop.ending === "lingering") {
|
|
39775
|
+
const told = stop.throughEditor ? stop.endedPid === null ? "The editor was asked to stop the scene it is playing and was still playing it" : `The editor was asked to stop the scene it is playing, and its game, pid ${stop.endedPid}, was still running` : `pid ${stop.endedPid ?? "unknown"} was told to end and was still running`;
|
|
39776
|
+
return {
|
|
39777
|
+
stopped: false,
|
|
39778
|
+
stillGoing: stop.endedPid,
|
|
39779
|
+
note: `${told} ${STOP_WAIT_MS / 1000} seconds later, so it has not stopped. editor_status says when it has gone, and editor_run stop asks again.`,
|
|
39780
|
+
withChildren: true
|
|
39781
|
+
};
|
|
39782
|
+
}
|
|
39587
39783
|
return {
|
|
39588
39784
|
stopped: true,
|
|
39589
39785
|
endedPid: stop.endedPid,
|
|
39590
39786
|
clean: stop.clean,
|
|
39591
|
-
note: `${stop.throughEditor ? stop.endedPid === null ? "The editor was asked to stop the scene it is playing. Its game had not announced a runtime, so no process was named under endedPid, and its exit code stays with the editor." : "The editor was asked to stop the scene it is playing." : "The process named under endedPid was ended."}${stop.exitSignal === null ? "" : ` It was ended by ${stop.exitSignal} and so has no exit code.`}
|
|
39787
|
+
note: `${stop.throughEditor ? stop.endedPid === null ? "The editor was asked to stop the scene it is playing. Its game had not announced a runtime, so no process was named under endedPid, and its exit code stays with the editor." : "The editor was asked to stop the scene it is playing." : "The process named under endedPid was ended."}${stop.exitSignal === null ? "" : ` It was ended by ${stop.exitSignal} and so has no exit code.`}`,
|
|
39592
39788
|
withChildren: true
|
|
39593
39789
|
};
|
|
39594
39790
|
}
|
|
39595
39791
|
function endedPreviousRun(ended) {
|
|
39596
|
-
if (ended
|
|
39792
|
+
if (ended?.ending !== "gone") {
|
|
39597
39793
|
return;
|
|
39598
39794
|
}
|
|
39599
39795
|
return ended.pid ?? true;
|
|
39600
39796
|
}
|
|
39601
39797
|
function previousRunLeft(ended) {
|
|
39602
|
-
return ended?.ending === "refused" && ended.pid !== null ? ended.pid : undefined;
|
|
39798
|
+
return (ended?.ending === "refused" || ended?.ending === "lingering") && ended.pid !== null ? ended.pid : undefined;
|
|
39603
39799
|
}
|
|
39604
39800
|
function engineNamesOf(executable) {
|
|
39605
39801
|
return CONSOLE_WRAPPER.test(executable) ? [executable, executable.replace(CONSOLE_WRAPPER, "$1")] : [executable];
|
|
@@ -39678,8 +39874,10 @@ function endedToStartThis(ended) {
|
|
|
39678
39874
|
if (ended.ending === "unreached") {
|
|
39679
39875
|
return ` ${which} was not ended here: the editor could not be asked to stop it. The editor stops a play before starting the next, so it is not running beside this one if this start reached the editor.`;
|
|
39680
39876
|
}
|
|
39681
|
-
|
|
39682
|
-
|
|
39877
|
+
if (ended.ending === "lingering") {
|
|
39878
|
+
return ` ${which} was told to end to start this one and was still running ${STOP_WAIT_MS / 1000} seconds later, so it is running beside this one: editor_status says when it has gone.`;
|
|
39879
|
+
}
|
|
39880
|
+
return ` ${which} was ended to start this one; its output is no longer what editor_output answers about.`;
|
|
39683
39881
|
}
|
|
39684
39882
|
function runtimeVerdict(endpoint, after) {
|
|
39685
39883
|
if (endpoint !== null) {
|
|
@@ -40009,6 +40207,7 @@ class GodotServer {
|
|
|
40009
40207
|
shutdownInitiated = false;
|
|
40010
40208
|
launchedEditor = null;
|
|
40011
40209
|
launchedFrom = null;
|
|
40210
|
+
launchedDropped = null;
|
|
40012
40211
|
ownProject;
|
|
40013
40212
|
announcedAt = null;
|
|
40014
40213
|
handingOverFrom = null;
|
|
@@ -40016,9 +40215,12 @@ class GodotServer {
|
|
|
40016
40215
|
constructor() {
|
|
40017
40216
|
this.ownProject = envValue("GDHARNESS_PROJECT") ?? null;
|
|
40018
40217
|
this.godotBridge = getDefaultBridge(this.ownProject);
|
|
40218
|
+
this.godotBridge.on("godot_connected", () => {
|
|
40219
|
+
this.settleTheLaunchReading();
|
|
40220
|
+
});
|
|
40019
40221
|
this.mcp = new McpServer({ name: "gdharness", version: SERVER_VERSION }, { capabilities: { tools: {}, resources: {} } });
|
|
40020
40222
|
this.setupToolHandlers();
|
|
40021
|
-
setupResourceHandlers(this.mcp, () => this.lastProjectPath);
|
|
40223
|
+
setupResourceHandlers(this.mcp, () => this.ourProject() ?? this.lastProjectPath);
|
|
40022
40224
|
this.mcp.server.onerror = (error) => {
|
|
40023
40225
|
console.error("[MCP Error]", error);
|
|
40024
40226
|
};
|
|
@@ -40284,8 +40486,9 @@ class GodotServer {
|
|
|
40284
40486
|
if (!checked.ok) {
|
|
40285
40487
|
return checked.response;
|
|
40286
40488
|
}
|
|
40287
|
-
|
|
40288
|
-
|
|
40489
|
+
const named = args["projectPath"];
|
|
40490
|
+
if (typeof named === "string" && existsSync17(join23(named, "project.godot"))) {
|
|
40491
|
+
this.lastProjectPath = named;
|
|
40289
40492
|
}
|
|
40290
40493
|
this.updates.refresh();
|
|
40291
40494
|
const answer = await withCallSignal(extra.signal, () => this.answered(spec.name, checked.op ?? "", args));
|
|
@@ -40325,6 +40528,17 @@ class GodotServer {
|
|
|
40325
40528
|
return answer;
|
|
40326
40529
|
}
|
|
40327
40530
|
const moved = this.projectHasMovedOn();
|
|
40531
|
+
if (moved !== null && !isNewer(moved, SERVER_VERSION)) {
|
|
40532
|
+
this.noticedUpdate = true;
|
|
40533
|
+
this.callsSinceNotice = 0;
|
|
40534
|
+
return this.saying(answer, {
|
|
40535
|
+
project_addons_behind_this_server: {
|
|
40536
|
+
server_is: SERVER_VERSION,
|
|
40537
|
+
project_is: moved,
|
|
40538
|
+
what_to_do: `The project's gdharness addons are ${moved}, older than this ${SERVER_VERSION} server, so an ` + "editor restart loads the old code and reconnecting spawns this same server again. Tell the " + "user, and offer to run gdharness upgrade in the project, which replaces the addons with " + "this server's; ask first, since it changes files in their project. editor_launch restart " + "then loads them."
|
|
40539
|
+
}
|
|
40540
|
+
});
|
|
40541
|
+
}
|
|
40328
40542
|
if (moved !== null) {
|
|
40329
40543
|
this.noticedUpdate = true;
|
|
40330
40544
|
this.callsSinceNotice = 0;
|
|
@@ -40357,7 +40571,7 @@ class GodotServer {
|
|
|
40357
40571
|
return this.saying(answer, {
|
|
40358
40572
|
update_available: {
|
|
40359
40573
|
...notice,
|
|
40360
|
-
what_to_do: "Tell the user a newer gdharness is out, with what changed, and offer to take it. " + "Only run the upgrade command if they say yes: it
|
|
40574
|
+
what_to_do: "Tell the user a newer gdharness is out, with what changed, and offer to take it. " + "Only run the upgrade command if they say yes: it replaces the addons in their project, " + "and afterwards the MCP server has to be reconnected and the editor restarted with " + "editor_launch restart."
|
|
40361
40575
|
}
|
|
40362
40576
|
});
|
|
40363
40577
|
}
|
|
@@ -40374,6 +40588,15 @@ class GodotServer {
|
|
|
40374
40588
|
const installed = installedAddonVersion(this.ownProject);
|
|
40375
40589
|
return installed === null || installed === SERVER_VERSION ? null : installed;
|
|
40376
40590
|
}
|
|
40591
|
+
editorCodeOnDisk() {
|
|
40592
|
+
const status = this.godotBridge.getStatus();
|
|
40593
|
+
const project = (status.connected ? status.projectPath : undefined) ?? this.ownProject;
|
|
40594
|
+
return project === null || project === "" ? undefined : installedEditorDigest(project);
|
|
40595
|
+
}
|
|
40596
|
+
runtimeStaleNote(endpoint) {
|
|
40597
|
+
const project = endpoint.project.path;
|
|
40598
|
+
return runtimeMismatch(endpoint.pid, endpoint.addon, SERVER_VERSION, shippedRuntimeDigest(), project === "" ? undefined : installedRuntimeDigest(project), project);
|
|
40599
|
+
}
|
|
40377
40600
|
configDisagreement() {
|
|
40378
40601
|
return this.ownProject === null ? null : configDisagrees(this.ownProject, SERVER_VERSION);
|
|
40379
40602
|
}
|
|
@@ -41308,7 +41531,7 @@ class GodotServer {
|
|
|
41308
41531
|
const name = application["config/name"];
|
|
41309
41532
|
const mainScene = application["run/main_scene"];
|
|
41310
41533
|
const info = {
|
|
41311
|
-
name: typeof name === "string" && name !== "" ? name :
|
|
41534
|
+
name: typeof name === "string" && name !== "" ? name : basename6(project.value.path),
|
|
41312
41535
|
path: project.value.path,
|
|
41313
41536
|
mainScene: typeof mainScene === "string" && mainScene !== "" ? mainScene : null,
|
|
41314
41537
|
godotVersion: await this.godotVersion(engine.value),
|
|
@@ -41325,6 +41548,10 @@ class GodotServer {
|
|
|
41325
41548
|
}
|
|
41326
41549
|
return this.jsonTextResponse(info);
|
|
41327
41550
|
}
|
|
41551
|
+
fileTypes(args) {
|
|
41552
|
+
const named = readStringArray(args, "fileTypes");
|
|
41553
|
+
return named === undefined ? {} : { fileTypes: named };
|
|
41554
|
+
}
|
|
41328
41555
|
handleSearchProject(args) {
|
|
41329
41556
|
const project = this.project(args);
|
|
41330
41557
|
if (!project.ok) {
|
|
@@ -41333,16 +41560,7 @@ class GodotServer {
|
|
|
41333
41560
|
try {
|
|
41334
41561
|
return this.jsonTextResponse(searchProject(project.value.path, {
|
|
41335
41562
|
query: readString(args, "query") ?? "",
|
|
41336
|
-
fileTypes
|
|
41337
|
-
"gd",
|
|
41338
|
-
"tscn",
|
|
41339
|
-
"tres",
|
|
41340
|
-
"gdshader",
|
|
41341
|
-
"cfg",
|
|
41342
|
-
"md",
|
|
41343
|
-
"txt",
|
|
41344
|
-
"json"
|
|
41345
|
-
],
|
|
41563
|
+
...this.fileTypes(args),
|
|
41346
41564
|
regex: readBoolean(args, "regex") ?? false,
|
|
41347
41565
|
caseSensitive: readBoolean(args, "caseSensitive") ?? false,
|
|
41348
41566
|
maxResults: readPositiveNumber(args, "maxResults") ?? 100
|
|
@@ -41384,7 +41602,7 @@ class GodotServer {
|
|
|
41384
41602
|
output.absolutePath
|
|
41385
41603
|
];
|
|
41386
41604
|
this.logDebug(`Exporting: ${engine.value} ${exportArgs.join(" ")}`);
|
|
41387
|
-
const writtenAt = () => existsSync17(output.absolutePath) ?
|
|
41605
|
+
const writtenAt = () => existsSync17(output.absolutePath) ? statSync9(output.absolutePath).mtimeMs : null;
|
|
41388
41606
|
const before = writtenAt();
|
|
41389
41607
|
let ending;
|
|
41390
41608
|
try {
|
|
@@ -41653,7 +41871,7 @@ class GodotServer {
|
|
|
41653
41871
|
warnings: diagnostics.length - errors,
|
|
41654
41872
|
diagnostics,
|
|
41655
41873
|
...disagrees
|
|
41656
|
-
}, this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest()));
|
|
41874
|
+
}, this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest(), this.editorCodeOnDisk()));
|
|
41657
41875
|
}
|
|
41658
41876
|
whereTheFileDisagrees(diagnostics, args) {
|
|
41659
41877
|
const messages = diagnostics.map((entry) => asParams(entry)["message"]).filter((message) => typeof message === "string");
|
|
@@ -41804,7 +42022,7 @@ class GodotServer {
|
|
|
41804
42022
|
}
|
|
41805
42023
|
breakpointsAtRisk() {
|
|
41806
42024
|
const status = this.godotBridge.getStatus();
|
|
41807
|
-
if (!status
|
|
42025
|
+
if (!hasSaidWhoItIs(status) || status.syncsBreakpoints === true) {
|
|
41808
42026
|
return {};
|
|
41809
42027
|
}
|
|
41810
42028
|
return {
|
|
@@ -41859,26 +42077,34 @@ class GodotServer {
|
|
|
41859
42077
|
restartSettled(this.ownProject);
|
|
41860
42078
|
return null;
|
|
41861
42079
|
}
|
|
41862
|
-
return owed;
|
|
42080
|
+
return { note: owed, underway: restartUnderway(owed) };
|
|
41863
42081
|
}
|
|
41864
42082
|
async getEditorStatusPayload() {
|
|
41865
42083
|
const status = this.godotBridge.getStatus();
|
|
41866
42084
|
const launchedIsUp = !status.connected && await this.launchedEditorIsUp();
|
|
41867
42085
|
const isPortConflict = this.bridgeStartupError?.includes("EADDRINUSE") ?? false;
|
|
41868
|
-
const
|
|
42086
|
+
const greeted = hasSaidWhoItIs(status);
|
|
42087
|
+
const stale = greeted && editorIsStale(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest());
|
|
41869
42088
|
const unfinished = this.restartLeftUnfinished(status.connected);
|
|
41870
42089
|
return {
|
|
41871
42090
|
...status,
|
|
41872
42091
|
serverVersion: SERVER_VERSION,
|
|
41873
|
-
addonIsStale:
|
|
42092
|
+
addonIsStale: greeted ? stale : undefined,
|
|
42093
|
+
greeting: status.connected && !greeted ? "The editor has connected and has not yet said who it is: its project, pid, addon version and ports arrive with its greeting a moment after the socket opens, later while it imports. Ask again for them." : undefined,
|
|
41874
42094
|
bridgeAvailable: this.bridgeStartupError === null,
|
|
41875
|
-
mayYetConnect: status.connected ? undefined : unfinished !== null ?
|
|
41876
|
-
restartInterrupted: unfinished === null ? undefined : {
|
|
41877
|
-
quitEditorPid: unfinished.editorPid,
|
|
41878
|
-
quitAt: unfinished.quitAt,
|
|
41879
|
-
byServerPid: unfinished.byPid,
|
|
42095
|
+
mayYetConnect: status.connected ? undefined : unfinished !== null ? unfinished.underway : anEditorIsStillComing(status.listeningSince, launchedIsUp),
|
|
42096
|
+
restartInterrupted: unfinished === null || unfinished.underway ? undefined : {
|
|
42097
|
+
quitEditorPid: unfinished.note.editorPid,
|
|
42098
|
+
quitAt: unfinished.note.quitAt,
|
|
42099
|
+
byServerPid: unfinished.note.byPid,
|
|
41880
42100
|
note: "A restart begun by a previous gdharness server asked this project's editor to quit, and that server was ended before it could start the editor again, so nothing is coming. editor_launch open starts one, and clears this."
|
|
41881
42101
|
},
|
|
42102
|
+
restartUnderway: unfinished === null || !unfinished.underway ? undefined : {
|
|
42103
|
+
quitEditorPid: unfinished.note.editorPid,
|
|
42104
|
+
quitAt: unfinished.note.quitAt,
|
|
42105
|
+
byServerPid: unfinished.note.byPid,
|
|
42106
|
+
note: `A restart of this project's editor is under way: gdharness server pid ${unfinished.note.byPid} asked it to quit at ${unfinished.note.quitAt} and is starting it again. Wait for it rather than opening another.`
|
|
42107
|
+
},
|
|
41882
42108
|
awaitingLaunchedEditor: status.connected || !launchedIsUp ? undefined : this.launchedEditor?.pid ?? undefined,
|
|
41883
42109
|
...status.connected ? this.whatTheLaunchedEditorDropped() : {},
|
|
41884
42110
|
startupError: this.bridgeStartupError,
|
|
@@ -41888,8 +42114,9 @@ class GodotServer {
|
|
|
41888
42114
|
port: this.godotBridge.configuredPort,
|
|
41889
42115
|
note: `The gdharness server this one replaced, pid ${this.handingOverFrom}, still holds port ${this.godotBridge.configuredPort} and is standing down; this server takes the port when it lets go, and takes another after ${HANDOVER_MS / 1000} seconds if it does not.`
|
|
41890
42116
|
},
|
|
41891
|
-
staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest()) : undefined,
|
|
41892
|
-
addonNote:
|
|
42117
|
+
staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest(), this.editorCodeOnDisk()) : undefined,
|
|
42118
|
+
addonNote: greeted ? sameCodeNote(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest()) : undefined,
|
|
42119
|
+
startedByAnEditorNote: status.startedByAnEditor === true ? "Another editor started this one: Godot's own restart, or the project manager opening a project. None of the arguments a server gave the editor before it reached this one, so it serves the ports its editor settings name (and none at all while another editor holds those, when the script and debug tools reach that editor instead), its console is not captured, and it restarts itself. Close it and use editor_launch open to have an editor this server started." : undefined,
|
|
41893
42120
|
...this.breakpointsAtRisk(),
|
|
41894
42121
|
retryingBridge: this.bridgeRetry === null ? undefined : true,
|
|
41895
42122
|
announcedAt: this.announcedAt ?? undefined,
|
|
@@ -41956,12 +42183,17 @@ class GodotServer {
|
|
|
41956
42183
|
ok: false,
|
|
41957
42184
|
message: `The game is held by the editor's debugger, ${describeHalt(held)}, and answers no runtime call while it is. debug_control continue lets it go.`
|
|
41958
42185
|
};
|
|
42186
|
+
const staleNote = this.runtimeStaleNote(endpoint);
|
|
42187
|
+
const unknown = runtimeAddonUnknown(endpoint.addon);
|
|
41959
42188
|
return {
|
|
41960
42189
|
pid: endpoint.pid,
|
|
41961
42190
|
port: endpoint.port,
|
|
41962
42191
|
project: endpoint.project,
|
|
41963
42192
|
...this.ownProject === null ? {} : { ofThisProject: isSameDirectory(endpoint.project.path, this.ownProject) },
|
|
41964
42193
|
editorPid: endpoint.editorPid,
|
|
42194
|
+
addonVersion: endpoint.addon === undefined || endpoint.addon.version === "" ? null : endpoint.addon.version,
|
|
42195
|
+
...unknown ? {} : { addonIsStale: staleNote !== undefined },
|
|
42196
|
+
...staleNote === undefined ? {} : { staleNote },
|
|
41965
42197
|
reachable: reply.ok,
|
|
41966
42198
|
problem: reply.ok ? null : reply.message
|
|
41967
42199
|
};
|
|
@@ -42028,17 +42260,33 @@ class GodotServer {
|
|
|
42028
42260
|
"editor_status says whether the bridge is up and what has reached it"
|
|
42029
42261
|
]);
|
|
42030
42262
|
}
|
|
42031
|
-
|
|
42263
|
+
if (!hasSaidWhoItIs(before)) {
|
|
42264
|
+
const settled = await this.waitForBridge(() => hasSaidWhoItIs(this.godotBridge.getStatus()) || !this.godotBridge.isConnected(), Date.now() + GREETING_WAIT_MS);
|
|
42265
|
+
if (!settled) {
|
|
42266
|
+
return this.createErrorResponse(`The editor has connected and has not said who it is within ${GREETING_WAIT_MS / 1000}s, so whether this server or the editor itself restarts it cannot be told.`, [
|
|
42267
|
+
"editor_status says when it has, under greeting while it has not",
|
|
42268
|
+
"Then ask for the restart again"
|
|
42269
|
+
]);
|
|
42270
|
+
}
|
|
42271
|
+
return this.handleRestartEditor(args);
|
|
42272
|
+
}
|
|
42032
42273
|
const mine = before.openedByAServer === true && before.projectPath !== undefined;
|
|
42274
|
+
if (!mine && args["hidden"] === true) {
|
|
42275
|
+
return this.createErrorResponse("This editor was opened by hand, and restarts itself as it was, so it cannot come back hidden.", [
|
|
42276
|
+
"Close it, then editor_launch open with hidden: true starts a hidden one this server can restart",
|
|
42277
|
+
"editor_launch restart without hidden restarts it as it is"
|
|
42278
|
+
]);
|
|
42279
|
+
}
|
|
42280
|
+
const settingsBefore = this.settingKeysOf(before.projectPath);
|
|
42033
42281
|
const asked = mine ? await this.startItAgain(before.projectPath ?? "", before.editorPid, args["hidden"] === true) : await this.handleViaBridge("restart_editor", {});
|
|
42034
42282
|
if (asked.isError === true) {
|
|
42035
42283
|
return asked;
|
|
42036
42284
|
}
|
|
42037
42285
|
const startedAt = before.connectedAt?.getTime() ?? 0;
|
|
42038
42286
|
const began = Date.now();
|
|
42039
|
-
const back = await this.waitForBridge(() => theEditorHasComeBack(this.godotBridge.getStatus(), startedAt), began +
|
|
42287
|
+
const back = await this.waitForBridge(() => theEditorHasComeBack(this.godotBridge.getStatus(), startedAt), began + editorRestartTimeoutMs());
|
|
42040
42288
|
if (!back) {
|
|
42041
|
-
return this.createErrorResponse(`The editor was asked to restart and has not come back within ${
|
|
42289
|
+
return this.createErrorResponse(`The editor was asked to restart and has not come back within ${editorRestartTimeoutMs() / 1000}s.`, [
|
|
42042
42290
|
"It may be asking what to do about an unsaved scene: look at the editor window",
|
|
42043
42291
|
"An addon that no longer parses stops the editor reaching this server",
|
|
42044
42292
|
"editor_status says whether anything has reached the bridge since"
|
|
@@ -42052,18 +42300,27 @@ class GodotServer {
|
|
|
42052
42300
|
addonVersion: now.addonVersion,
|
|
42053
42301
|
serverVersion: SERVER_VERSION,
|
|
42054
42302
|
addonIsStale: editorIsStale(now.addonVersion, SERVER_VERSION, now.addonDigest, shippedEditorDigest()),
|
|
42055
|
-
staleNote: addonMismatch(now.addonVersion, SERVER_VERSION, now.addonDigest, shippedEditorDigest()),
|
|
42303
|
+
staleNote: addonMismatch(now.addonVersion, SERVER_VERSION, now.addonDigest, shippedEditorDigest(), this.editorCodeOnDisk()),
|
|
42056
42304
|
...this.whatTheEditorDropped(settingsBefore, after),
|
|
42057
42305
|
tookMs: Date.now() - began
|
|
42058
42306
|
});
|
|
42059
42307
|
}
|
|
42060
|
-
|
|
42308
|
+
settleTheLaunchReading() {
|
|
42061
42309
|
const watched = this.launchedFrom;
|
|
42062
|
-
|
|
42063
|
-
|
|
42310
|
+
const status = this.godotBridge.getStatus();
|
|
42311
|
+
if (watched === null || !hasSaidWhoItIs(status)) {
|
|
42312
|
+
return;
|
|
42064
42313
|
}
|
|
42065
42314
|
this.launchedFrom = null;
|
|
42066
|
-
|
|
42315
|
+
if (status.editorPid === undefined || status.editorPid !== this.launchedEditor?.pid) {
|
|
42316
|
+
return;
|
|
42317
|
+
}
|
|
42318
|
+
this.launchedDropped = this.whatTheEditorDropped(watched.before, this.settingKeysOf(watched.projectPath));
|
|
42319
|
+
}
|
|
42320
|
+
whatTheLaunchedEditorDropped() {
|
|
42321
|
+
const dropped = this.launchedDropped ?? {};
|
|
42322
|
+
this.launchedDropped = null;
|
|
42323
|
+
return dropped;
|
|
42067
42324
|
}
|
|
42068
42325
|
whatTheEditorDropped(before, after) {
|
|
42069
42326
|
return settingsDroppedReport(before, after);
|
|
@@ -42089,7 +42346,14 @@ class GodotServer {
|
|
|
42089
42346
|
restartSettled(projectPath);
|
|
42090
42347
|
return asked;
|
|
42091
42348
|
}
|
|
42092
|
-
await this.waitForBridge(() => !alive(editorPid), Date.now() +
|
|
42349
|
+
const gone = await this.waitForBridge(() => !alive(editorPid), Date.now() + editorRestartTimeoutMs());
|
|
42350
|
+
if (!gone) {
|
|
42351
|
+
restartSettled(projectPath);
|
|
42352
|
+
return this.createErrorResponse(`The editor was asked to go and pid ${editorPid ?? "unknown"} is still running after ${editorRestartTimeoutMs() / 1000}s, so no replacement was started: it would find the language server and debug adapter ports still held.`, [
|
|
42353
|
+
"It may be asking what to do about unsaved changes, or a script may be holding up its exit",
|
|
42354
|
+
`editor_launch open starts one on ${projectPath} once it has gone`
|
|
42355
|
+
]);
|
|
42356
|
+
}
|
|
42093
42357
|
const opened = await this.openAnEditor(engine.value, projectPath, ports, hidden);
|
|
42094
42358
|
restartSettled(projectPath);
|
|
42095
42359
|
if (opened.error !== null) {
|
|
@@ -42120,6 +42384,10 @@ class GodotServer {
|
|
|
42120
42384
|
"Another project wants its own gdharness server, which is its own harness session"
|
|
42121
42385
|
]);
|
|
42122
42386
|
}
|
|
42387
|
+
const refused = this.anEditorThatCannotConnect(project.value.path);
|
|
42388
|
+
if (refused !== null) {
|
|
42389
|
+
return refused;
|
|
42390
|
+
}
|
|
42123
42391
|
const engine = await this.engine();
|
|
42124
42392
|
if (!engine.ok) {
|
|
42125
42393
|
return engine.response;
|
|
@@ -42144,6 +42412,27 @@ class GodotServer {
|
|
|
42144
42412
|
settingsNote: before === null ? "project.godot could not be read before this editor was opened, so nothing here will be able to say what the import saved away. Opening a project rewrites the file and drops any key sitting at its own default." : before.size === 0 ? undefined : "Opening a project imports it and saves project.godot, and Godot drops any key sitting at its own default. editor_status names anything lost under settingsDropped once this editor has connected."
|
|
42145
42413
|
});
|
|
42146
42414
|
}
|
|
42415
|
+
anEditorThatCannotConnect(projectPath) {
|
|
42416
|
+
if (this.ownProject !== null && !isSameDirectory(this.ownProject, projectPath)) {
|
|
42417
|
+
return this.createErrorResponse(`This server serves ${this.ownProject}, and an editor of ${projectPath} would dial that project's own server rather than this one.`, [
|
|
42418
|
+
"Open it from that project's own gdharness session",
|
|
42419
|
+
`editor_launch with projectPath ${this.ownProject} opens the editor this server serves`
|
|
42420
|
+
]);
|
|
42421
|
+
}
|
|
42422
|
+
const report = inspectProject(projectPath);
|
|
42423
|
+
const installed = report.addons.some((addon) => addon.name === "gdharness_editor" && addon.installed);
|
|
42424
|
+
if (!installed || !report.pluginsEnabled.includes("gdharness_editor")) {
|
|
42425
|
+
return this.createErrorResponse(`The gdharness editor addon is not ${installed ? "enabled" : "installed"} in ${projectPath}, so an editor opened on it would never reach this server.`, installed ? [
|
|
42426
|
+
"gdharness setup in the project enables it",
|
|
42427
|
+
"Or enable it under Project Settings, Plugins, in an editor opened by hand"
|
|
42428
|
+
] : ["gdharness setup in the project installs and enables it"]);
|
|
42429
|
+
}
|
|
42430
|
+
const owed = restartOwed(projectPath);
|
|
42431
|
+
if (owed !== null && restartUnderway(owed)) {
|
|
42432
|
+
return this.createErrorResponse(`A restart of this project's editor is under way: gdharness server pid ${owed.byPid} asked it to quit at ${owed.quitAt} and is starting it again, so opening another would put two editors on one project.`, ["editor_status says when it has connected"]);
|
|
42433
|
+
}
|
|
42434
|
+
return null;
|
|
42435
|
+
}
|
|
42147
42436
|
async openAnEditor(engine, projectPath, ports, hidden) {
|
|
42148
42437
|
clearEditorLog(projectPath);
|
|
42149
42438
|
const launched = await launchOutsideTheTree({
|
|
@@ -42350,7 +42639,7 @@ class GodotServer {
|
|
|
42350
42639
|
}
|
|
42351
42640
|
let announcedAt;
|
|
42352
42641
|
try {
|
|
42353
|
-
announcedAt =
|
|
42642
|
+
announcedAt = statSync9(endpoint.file).mtimeMs;
|
|
42354
42643
|
} catch {
|
|
42355
42644
|
return;
|
|
42356
42645
|
}
|
|
@@ -42587,27 +42876,41 @@ class GodotServer {
|
|
|
42587
42876
|
if (running.pid === null) {
|
|
42588
42877
|
return "gone";
|
|
42589
42878
|
}
|
|
42879
|
+
if (running.keeper !== undefined) {
|
|
42880
|
+
const ended = await this.endThroughTheKeeper(running, running.pid, running.keeper);
|
|
42881
|
+
if (ended !== null) {
|
|
42882
|
+
clearTheRecord();
|
|
42883
|
+
return ended;
|
|
42884
|
+
}
|
|
42885
|
+
}
|
|
42590
42886
|
const startedAs = {
|
|
42591
42887
|
pid: running.pid,
|
|
42592
42888
|
transcript: running.transcript ?? "",
|
|
42593
42889
|
startedAt: running.startedAt,
|
|
42890
|
+
...running.startedBy === undefined ? {} : { startedBy: running.startedBy },
|
|
42594
42891
|
projectPath: running.projectPath ?? "",
|
|
42595
42892
|
arguments: [],
|
|
42596
42893
|
...running.command === undefined ? {} : { command: running.command }
|
|
42597
42894
|
};
|
|
42598
|
-
|
|
42895
|
+
const why = await whyNotStillTheRecordedRun(startedAs);
|
|
42896
|
+
if (why !== null) {
|
|
42599
42897
|
clearTheRecord();
|
|
42600
42898
|
running.endedHere = null;
|
|
42601
|
-
running.log.record("warning", `This run was not ended here: pid ${running.pid} no longer answers as the process the run was started as, so nothing was signalled. If that process is still the game, end it yourself; if it is not, it belongs to something else.`);
|
|
42899
|
+
running.log.record("warning", `This run was not ended here: pid ${running.pid} no longer answers as the process the run was started as (${why}), so nothing was signalled. If that process is still the game, end it yourself; if it is not, it belongs to something else.`);
|
|
42602
42900
|
return "refused";
|
|
42603
42901
|
}
|
|
42604
42902
|
const signalled = await runningAs(running.pid);
|
|
42605
|
-
const described = signalled === null ? "nothing it would name" : `${signalled.kind}: ${signalled.text}`;
|
|
42903
|
+
const described = signalled === null || signalled.kind === "none" ? "nothing it would name" : `${signalled.kind}: ${signalled.text}`;
|
|
42606
42904
|
let landed = true;
|
|
42905
|
+
let declined = null;
|
|
42607
42906
|
try {
|
|
42608
42907
|
process.kill(running.pid);
|
|
42609
|
-
} catch {
|
|
42908
|
+
} catch (error) {
|
|
42610
42909
|
landed = false;
|
|
42910
|
+
const code = error.code;
|
|
42911
|
+
if (code !== "ESRCH") {
|
|
42912
|
+
declined = code ?? errorMessage(error);
|
|
42913
|
+
}
|
|
42611
42914
|
}
|
|
42612
42915
|
const gone = await untilGone([running.pid]);
|
|
42613
42916
|
if (gone) {
|
|
@@ -42618,7 +42921,24 @@ class GodotServer {
|
|
|
42618
42921
|
running.exitSignal = "SIGTERM";
|
|
42619
42922
|
}
|
|
42620
42923
|
clearTheRecord();
|
|
42621
|
-
running.log.record("info", landed ? `gdharness ended pid ${running.pid}, which the operating system described as ${described}.` : `gdharness signalled pid ${running.pid} and it was already gone; the operating system had described it as ${described}.`);
|
|
42924
|
+
running.log.record(gone ? "info" : "warning", declined !== null ? `gdharness could not signal pid ${running.pid}: the operating system refused it (${declined}). It had described that process as ${described}.` : !gone ? `gdharness signalled pid ${running.pid}, which the operating system described as ${described}, and it was still running ${STOP_WAIT_MS / 1000} seconds later.` : landed ? `gdharness ended pid ${running.pid}, which the operating system described as ${described}.` : `gdharness signalled pid ${running.pid} and it was already gone; the operating system had described it as ${described}.`);
|
|
42925
|
+
return gone ? "gone" : "lingering";
|
|
42926
|
+
}
|
|
42927
|
+
async endThroughTheKeeper(run, pid, keeper) {
|
|
42928
|
+
const asked = await askTheKeeperToStop(keeper, KEEPER_ANSWER_MS);
|
|
42929
|
+
if (typeof asked !== "string") {
|
|
42930
|
+
this.logDebug(`The keeper of pid ${pid} could not be asked to end it (${asked.unreached})`);
|
|
42931
|
+
return null;
|
|
42932
|
+
}
|
|
42933
|
+
const gone = await untilGone([pid]);
|
|
42934
|
+
if (gone) {
|
|
42935
|
+
await this.untilTheExitIsRecorded(run);
|
|
42936
|
+
}
|
|
42937
|
+
if (asked === "signalled" && gone) {
|
|
42938
|
+
run.exitCode = null;
|
|
42939
|
+
run.exitSignal = "SIGTERM";
|
|
42940
|
+
}
|
|
42941
|
+
run.log.record(gone ? "info" : "warning", !gone ? `gdharness had the keeper holding pid ${pid} end it, and it was still running ${STOP_WAIT_MS / 1000} seconds later.` : asked === "signalled" ? `gdharness ended pid ${pid} through the keeper holding it.` : `gdharness asked the keeper holding pid ${pid} to end it, and it had already gone.`);
|
|
42622
42942
|
return gone ? "gone" : "lingering";
|
|
42623
42943
|
}
|
|
42624
42944
|
spawnGame(godotPath, cmdArgs, env) {
|
|
@@ -42702,13 +43022,16 @@ class GodotServer {
|
|
|
42702
43022
|
command: godotPath,
|
|
42703
43023
|
args: cmdArgs,
|
|
42704
43024
|
...env === undefined ? {} : { env },
|
|
42705
|
-
run: { transcript: transcript.path, startedAt, projectPath },
|
|
43025
|
+
run: { transcript: transcript.path, startedAt, projectPath, ...this.servedBy() },
|
|
42706
43026
|
...desktop === undefined ? {} : { desktop }
|
|
42707
43027
|
});
|
|
42708
43028
|
if ("error" in launched) {
|
|
42709
43029
|
return launched;
|
|
42710
43030
|
}
|
|
43031
|
+
const startedBy = Date.now();
|
|
42711
43032
|
sweepTranscripts();
|
|
43033
|
+
const noted = readRunRecord(projectPath);
|
|
43034
|
+
const keeper = noted?.pid === launched.pid ? noted.keeper : undefined;
|
|
42712
43035
|
return {
|
|
42713
43036
|
pid: launched.pid,
|
|
42714
43037
|
log: new GameLog,
|
|
@@ -42716,6 +43039,8 @@ class GodotServer {
|
|
|
42716
43039
|
readOffset: 0,
|
|
42717
43040
|
projectPath,
|
|
42718
43041
|
startedAt,
|
|
43042
|
+
startedBy,
|
|
43043
|
+
...keeper === undefined ? {} : { keeper },
|
|
42719
43044
|
command: godotPath,
|
|
42720
43045
|
exitCode: null,
|
|
42721
43046
|
exitSignal: null,
|
|
@@ -42935,8 +43260,9 @@ class GodotServer {
|
|
|
42935
43260
|
}
|
|
42936
43261
|
async adoptRecordedRun() {
|
|
42937
43262
|
const project = this.ourProject();
|
|
42938
|
-
const
|
|
42939
|
-
|
|
43263
|
+
const found = project === null ? [] : [readRunRecord(project), this.startedForUs(project)];
|
|
43264
|
+
const record = found.reduce((newest, one) => one !== null && (newest === null || one.startedAt > newest.startedAt) ? one : newest, null);
|
|
43265
|
+
if (record === null || !(this.couldBeOurs(record.projectPath) || this.couldBeOurs(record.servedBy ?? ""))) {
|
|
42940
43266
|
return null;
|
|
42941
43267
|
}
|
|
42942
43268
|
const adopted = {
|
|
@@ -42946,6 +43272,8 @@ class GodotServer {
|
|
|
42946
43272
|
readOffset: 0,
|
|
42947
43273
|
projectPath: record.projectPath === "" ? null : record.projectPath,
|
|
42948
43274
|
startedAt: record.startedAt,
|
|
43275
|
+
...record.startedBy === undefined ? {} : { startedBy: record.startedBy },
|
|
43276
|
+
...record.keeper === undefined ? {} : { keeper: record.keeper },
|
|
42949
43277
|
...record.command === undefined ? {} : { command: record.command },
|
|
42950
43278
|
exitCode: null,
|
|
42951
43279
|
exitSignal: null,
|
|
@@ -42966,6 +43294,13 @@ class GodotServer {
|
|
|
42966
43294
|
this.activeProcess = adopted;
|
|
42967
43295
|
return adopted;
|
|
42968
43296
|
}
|
|
43297
|
+
servedBy() {
|
|
43298
|
+
const mine = this.ourProject();
|
|
43299
|
+
return mine === null ? {} : { servedBy: mine };
|
|
43300
|
+
}
|
|
43301
|
+
startedForUs(project) {
|
|
43302
|
+
return everyRunRecord().find((one) => one.servedBy !== undefined && isSameDirectory(one.servedBy, project) && !(one.projectPath !== "" && isSameDirectory(one.projectPath, project))) ?? null;
|
|
43303
|
+
}
|
|
42969
43304
|
nothingOfOursIsRunning() {
|
|
42970
43305
|
const status = this.godotBridge.getStatus();
|
|
42971
43306
|
const noEditor = !status.connected ? " No editor is connected, so a game the editor is playing cannot be seen from here at all:" + " editor_status says whether one is on its way, and a run of yours that is still up is" + " reachable again once it is." : "";
|
|
@@ -43132,22 +43467,20 @@ class GodotServer {
|
|
|
43132
43467
|
}
|
|
43133
43468
|
async handleEditorConsole(args) {
|
|
43134
43469
|
const status = this.godotBridge.getStatus();
|
|
43135
|
-
const
|
|
43136
|
-
|
|
43137
|
-
|
|
43138
|
-
|
|
43139
|
-
"editor_launch opens one, and an editor opened here has its console captured"
|
|
43140
|
-
]);
|
|
43470
|
+
const greeted = hasSaidWhoItIs(status);
|
|
43471
|
+
const launched = greeted ? null : this.launchedEditor?.pid ?? null;
|
|
43472
|
+
if (!greeted && launched === null && status.connected) {
|
|
43473
|
+
return this.createErrorResponse("The editor has connected and has not yet said who it is, so whose console to read cannot be told yet.", ["Ask again in a moment; editor_status shows greeting until it has"]);
|
|
43141
43474
|
}
|
|
43142
|
-
const
|
|
43143
|
-
if (!
|
|
43475
|
+
const projectPath = status.projectPath ?? this.ownProject ?? undefined;
|
|
43476
|
+
if (projectPath === undefined || !greeted && launched === null) {
|
|
43144
43477
|
return this.createErrorResponse("No editor is connected, so there is no console to read.", [
|
|
43145
43478
|
"editor_status says whether one is connected and whether it may yet be",
|
|
43146
43479
|
"editor_launch opens one, and an editor opened here has its console captured"
|
|
43147
43480
|
]);
|
|
43148
43481
|
}
|
|
43149
|
-
const editorPid =
|
|
43150
|
-
const console2 = editorConsole(projectPath, editorPid,
|
|
43482
|
+
const editorPid = greeted ? status.editorPid ?? null : launched;
|
|
43483
|
+
const console2 = editorConsole(projectPath, editorPid, greeted ? status.openedByAServer === true : true);
|
|
43151
43484
|
if ("kind" in console2) {
|
|
43152
43485
|
return this.createErrorResponse(theConsoleWasNotCaptured(console2, projectPath), [
|
|
43153
43486
|
"editor_launch restart replaces this editor with one whose console is captured",
|
|
@@ -43329,6 +43662,7 @@ class GodotServer {
|
|
|
43329
43662
|
through: stopped.throughEditor ? "editor" : "gdharness",
|
|
43330
43663
|
endedPid: verdict.endedPid,
|
|
43331
43664
|
notSignalled: verdict.notSignalled,
|
|
43665
|
+
stillGoing: verdict.stillGoing,
|
|
43332
43666
|
...childrenFields(ended),
|
|
43333
43667
|
exitedBeforeStop: !wasRunning,
|
|
43334
43668
|
exitCode: stopped.exitCode,
|
|
@@ -43386,12 +43720,13 @@ class GodotServer {
|
|
|
43386
43720
|
await announcementEnded(pid);
|
|
43387
43721
|
}
|
|
43388
43722
|
return this.jsonTextResponse({
|
|
43389
|
-
stopped:
|
|
43723
|
+
stopped: gone,
|
|
43390
43724
|
through: "gdharness",
|
|
43391
|
-
endedPid: pid,
|
|
43725
|
+
endedPid: gone ? pid : undefined,
|
|
43726
|
+
stillGoing: gone ? undefined : pid,
|
|
43392
43727
|
...childrenFields(ended),
|
|
43393
43728
|
exitedBeforeStop: !landed,
|
|
43394
|
-
note: `${landed ? `pid ${pid} was ended: a game of this project that no server here was holding,` : `pid ${pid} had already gone when it was signalled: a game of this project that no server here was holding,`} identified by its announcement and by its command line, ${read.executable} run with --path ${read.projectPath ?? ""}. Nothing here read what it printed or holds its exit code, so neither is in this answer.${gone ? "" :
|
|
43729
|
+
note: `${!gone ? `pid ${pid} was told to end and was still running ${STOP_WAIT_MS / 1000} seconds later, so it has not stopped: a game of this project that no server here was holding,` : landed ? `pid ${pid} was ended: a game of this project that no server here was holding,` : `pid ${pid} had already gone when it was signalled: a game of this project that no server here was holding,`} identified by its announcement and by its command line, ${read.executable} run with --path ${read.projectPath ?? ""}. Nothing here read what it printed or holds its exit code, so neither is in this answer.${gone ? "" : " editor_status says when it has gone."} ${aboutTheChildren(ended, false)}`
|
|
43395
43730
|
});
|
|
43396
43731
|
}
|
|
43397
43732
|
async whatTheRunStarted(run) {
|
|
@@ -43538,7 +43873,8 @@ class GodotServer {
|
|
|
43538
43873
|
}
|
|
43539
43874
|
}
|
|
43540
43875
|
const after = projectPath === "" || busy ? null : cachedClasses(projectPath);
|
|
43541
|
-
const
|
|
43876
|
+
const declaredNow = before === null || after === null ? null : declaredClasses(projectPath);
|
|
43877
|
+
const lost = before === null || after === null || declaredNow === null ? [] : [...before.keys()].filter((name) => !after.has(name) && declaredNow.has(name));
|
|
43542
43878
|
if (lost.length > 0) {
|
|
43543
43879
|
this.noteAShortListEditor(projectPath);
|
|
43544
43880
|
}
|
|
@@ -43599,7 +43935,7 @@ class GodotServer {
|
|
|
43599
43935
|
}
|
|
43600
43936
|
const checked = busy ? { unseen: [] } : await this.classesTheEditorCannotSee(args);
|
|
43601
43937
|
const unseen = checked.unseen;
|
|
43602
|
-
const broughtIn = blind.unseen.map((one) => one.className).filter((name) => !unseen.some((still) => still.className === name));
|
|
43938
|
+
const broughtIn = busy ? [] : blind.unseen.map((one) => one.className).filter((name) => !unseen.some((still) => still.className === name));
|
|
43603
43939
|
const dependents = busy || projectPath === "" || broughtIn.length === 0 ? null : await this.reloadDependents(projectPath, broughtIn);
|
|
43604
43940
|
const stillGone = lost.filter((name) => !restored.includes(name));
|
|
43605
43941
|
const notes = [];
|
|
@@ -43840,7 +44176,7 @@ class GodotServer {
|
|
|
43840
44176
|
]);
|
|
43841
44177
|
}
|
|
43842
44178
|
try {
|
|
43843
|
-
return this.jsonTextResponse(markIfStale(await this.godotBridge.invokeTool(toolName, args), this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest()));
|
|
44179
|
+
return this.jsonTextResponse(markIfStale(await this.godotBridge.invokeTool(toolName, args), this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest(), this.editorCodeOnDisk()));
|
|
43844
44180
|
} catch (error) {
|
|
43845
44181
|
return this.createErrorResponse(`The editor answered ${toolName} with an error: ${errorMessage(error)}`);
|
|
43846
44182
|
}
|
|
@@ -43905,7 +44241,7 @@ class GodotServer {
|
|
|
43905
44241
|
return !run.announcedBefore.has(one.pid);
|
|
43906
44242
|
}
|
|
43907
44243
|
try {
|
|
43908
|
-
return
|
|
44244
|
+
return statSync9(one.file).mtimeMs >= run.startedAt;
|
|
43909
44245
|
} catch {
|
|
43910
44246
|
return false;
|
|
43911
44247
|
}
|
|
@@ -43981,6 +44317,11 @@ class GodotServer {
|
|
|
43981
44317
|
}
|
|
43982
44318
|
const screenshotDir = expectsScreenshot ? scratchDirectory("runtime-screenshot") : null;
|
|
43983
44319
|
const screenshotPath = screenshotDir ? join23(screenshotDir, "capture.png") : null;
|
|
44320
|
+
const staleNote = this.runtimeStaleNote(choice.endpoint);
|
|
44321
|
+
const stale = staleNote === undefined ? {} : { addonIsStale: true, staleNote };
|
|
44322
|
+
const refused = (message) => this.createErrorResponse(staleNote === undefined ? message : `${message}
|
|
44323
|
+
|
|
44324
|
+
${staleNote}`);
|
|
43984
44325
|
try {
|
|
43985
44326
|
const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs, !expectsScreenshot);
|
|
43986
44327
|
if (!reply.ok && reply.requestId !== undefined) {
|
|
@@ -43990,22 +44331,23 @@ class GodotServer {
|
|
|
43990
44331
|
command,
|
|
43991
44332
|
waitedMs: timeoutMs,
|
|
43992
44333
|
note: reply.message,
|
|
43993
|
-
...answeredBy
|
|
44334
|
+
...answeredBy,
|
|
44335
|
+
...stale
|
|
43994
44336
|
});
|
|
43995
44337
|
}
|
|
43996
44338
|
if (!reply.ok) {
|
|
43997
|
-
return
|
|
44339
|
+
return refused(reply.message);
|
|
43998
44340
|
}
|
|
43999
44341
|
const { id: _id, ...payload } = reply.payload;
|
|
44000
44342
|
if (!expectsScreenshot) {
|
|
44001
|
-
return this.jsonTextResponse({ ...payload, ...answeredBy });
|
|
44343
|
+
return this.jsonTextResponse({ ...payload, ...answeredBy, ...stale });
|
|
44002
44344
|
}
|
|
44003
44345
|
const returnedPath = readString(payload, "path");
|
|
44004
44346
|
if (readString(payload, "type") !== "screenshot_file" || !returnedPath || !screenshotPath) {
|
|
44005
|
-
return
|
|
44347
|
+
return refused(`The game answered a capture with ${JSON.stringify(payload)}`);
|
|
44006
44348
|
}
|
|
44007
44349
|
if (normalize2(returnedPath) !== normalize2(screenshotPath)) {
|
|
44008
|
-
return
|
|
44350
|
+
return refused(`Rejected screenshot file path outside the managed capture path: '${returnedPath}'`);
|
|
44009
44351
|
}
|
|
44010
44352
|
const dimensions = `${readNumber(payload, "width") ?? 0}x${readNumber(payload, "height") ?? 0} ${readString(payload, "format") ?? "unknown"}`;
|
|
44011
44353
|
if (saveTo !== null) {
|
|
@@ -44019,7 +44361,9 @@ class GodotServer {
|
|
|
44019
44361
|
content: [
|
|
44020
44362
|
{
|
|
44021
44363
|
type: "text",
|
|
44022
|
-
text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}${saveTo === null ? "" : `. Saved to ${saveTo}`}`
|
|
44364
|
+
text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}${saveTo === null ? "" : `. Saved to ${saveTo}`}${staleNote === undefined ? "" : `
|
|
44365
|
+
|
|
44366
|
+
${staleNote}`}`
|
|
44023
44367
|
},
|
|
44024
44368
|
{ type: "image", data: readFileSync20(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
44025
44369
|
]
|
|
@@ -44118,10 +44462,11 @@ class GodotServer {
|
|
|
44118
44462
|
}
|
|
44119
44463
|
}
|
|
44120
44464
|
async function runGodotServer() {
|
|
44465
|
+
delete process.env[EDITOR_PID_VARIABLE];
|
|
44121
44466
|
const server = new GodotServer;
|
|
44122
44467
|
await server.run();
|
|
44123
44468
|
}
|
|
44124
|
-
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2,
|
|
44469
|
+
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, GREETING_WAIT_MS = 1e4, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, STOP_WAIT_MS = 1e4, KEEPER_ANSWER_MS = 15000, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, SCAN_WAIT_MS = 30000, STREAMS_GRACE_MS = 2000, 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/";
|
|
44125
44470
|
var init_server3 = __esm(() => {
|
|
44126
44471
|
init_mcp();
|
|
44127
44472
|
init_stdio2();
|
|
@@ -44146,6 +44491,7 @@ var init_server3 = __esm(() => {
|
|
|
44146
44491
|
init_headless_operations();
|
|
44147
44492
|
init_issues();
|
|
44148
44493
|
init_junit();
|
|
44494
|
+
init_keeper_channel();
|
|
44149
44495
|
init_lsp_client();
|
|
44150
44496
|
init_outside();
|
|
44151
44497
|
init_paths();
|
|
@@ -44213,12 +44559,12 @@ init_bridge_announce();
|
|
|
44213
44559
|
init_errors();
|
|
44214
44560
|
init_godot_path();
|
|
44215
44561
|
import { existsSync as existsSync18 } from "node:fs";
|
|
44216
|
-
import { join as join24, resolve as
|
|
44562
|
+
import { join as join24, resolve as resolve7 } from "node:path";
|
|
44217
44563
|
|
|
44218
44564
|
// src/harnesses.ts
|
|
44219
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
44565
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
44220
44566
|
import { homedir as homedir2 } from "node:os";
|
|
44221
|
-
import { dirname as dirname3, join as join4 } from "node:path";
|
|
44567
|
+
import { basename as basename2, dirname as dirname3, join as join4, relative as relative2, resolve as resolve2 } from "node:path";
|
|
44222
44568
|
import process4 from "node:process";
|
|
44223
44569
|
|
|
44224
44570
|
// node_modules/yaml/dist/index.js
|
|
@@ -44361,6 +44707,7 @@ function isEmptyCollection(node) {
|
|
|
44361
44707
|
|
|
44362
44708
|
// src/harnesses.ts
|
|
44363
44709
|
init_errors();
|
|
44710
|
+
init_paths();
|
|
44364
44711
|
init_runner();
|
|
44365
44712
|
var SERVER_KEY = "gdharness";
|
|
44366
44713
|
function launchFor(version, godotPath, projectPath, runner = currentRunner()) {
|
|
@@ -44923,27 +45270,66 @@ function registered(harness, projectPath) {
|
|
|
44923
45270
|
}
|
|
44924
45271
|
function recordedEnginePath(projectPath) {
|
|
44925
45272
|
for (const harness of HARNESSES) {
|
|
44926
|
-
if (harness.scope !== "project"
|
|
45273
|
+
if (harness.scope !== "project") {
|
|
44927
45274
|
continue;
|
|
44928
45275
|
}
|
|
44929
|
-
const
|
|
44930
|
-
if (!existsSync4(path)) {
|
|
44931
|
-
continue;
|
|
44932
|
-
}
|
|
44933
|
-
let parsed;
|
|
44934
|
-
try {
|
|
44935
|
-
parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
44936
|
-
} catch {
|
|
44937
|
-
continue;
|
|
44938
|
-
}
|
|
44939
|
-
const entry = reach(reach(parsed, harness.container), SERVER_KEY);
|
|
44940
|
-
const named = reach(reach(entry, harness.shape === "opencode" ? "environment" : "env"), "GODOT_PATH");
|
|
45276
|
+
const named = reach(entryEnvironment(harness, projectPath), "GODOT_PATH");
|
|
44941
45277
|
if (typeof named === "string" && named !== "" && existsSync4(named)) {
|
|
44942
45278
|
return named;
|
|
44943
45279
|
}
|
|
44944
45280
|
}
|
|
44945
45281
|
return null;
|
|
44946
45282
|
}
|
|
45283
|
+
function mainCheckoutOf(projectPath) {
|
|
45284
|
+
let top = resolve2(projectPath);
|
|
45285
|
+
while (!existsSync4(join4(top, ".git"))) {
|
|
45286
|
+
const up = dirname3(top);
|
|
45287
|
+
if (up === top) {
|
|
45288
|
+
return null;
|
|
45289
|
+
}
|
|
45290
|
+
top = up;
|
|
45291
|
+
}
|
|
45292
|
+
const marker = join4(top, ".git");
|
|
45293
|
+
if (!statSync2(marker).isFile()) {
|
|
45294
|
+
return null;
|
|
45295
|
+
}
|
|
45296
|
+
const pointer = /^gitdir:\s*(.+)$/m.exec(readFileSync2(marker, "utf8"))?.[1]?.trim();
|
|
45297
|
+
if (pointer === undefined) {
|
|
45298
|
+
return null;
|
|
45299
|
+
}
|
|
45300
|
+
const entry = resolve2(top, pointer);
|
|
45301
|
+
const commonFile = join4(entry, "commondir");
|
|
45302
|
+
if (!existsSync4(commonFile)) {
|
|
45303
|
+
return null;
|
|
45304
|
+
}
|
|
45305
|
+
const common = resolve2(entry, readFileSync2(commonFile, "utf8").trim());
|
|
45306
|
+
if (basename2(common) !== ".git") {
|
|
45307
|
+
return null;
|
|
45308
|
+
}
|
|
45309
|
+
const main = join4(dirname3(common), relative2(top, resolve2(projectPath)));
|
|
45310
|
+
return isSameDirectory(main, projectPath) || !existsSync4(join4(main, "project.godot")) ? null : main;
|
|
45311
|
+
}
|
|
45312
|
+
function entryEnvironment(harness, projectPath) {
|
|
45313
|
+
if (harness.toml || harness.yaml || harness.snippet) {
|
|
45314
|
+
return;
|
|
45315
|
+
}
|
|
45316
|
+
const path = configPath(harness, projectPath);
|
|
45317
|
+
if (!existsSync4(path)) {
|
|
45318
|
+
return;
|
|
45319
|
+
}
|
|
45320
|
+
let parsed;
|
|
45321
|
+
try {
|
|
45322
|
+
parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
45323
|
+
} catch {
|
|
45324
|
+
return;
|
|
45325
|
+
}
|
|
45326
|
+
const entry = reach(reach(parsed, harness.container), SERVER_KEY);
|
|
45327
|
+
return reach(entry, harness.shape === "opencode" ? "environment" : "env");
|
|
45328
|
+
}
|
|
45329
|
+
function servedProject(harness, projectPath) {
|
|
45330
|
+
const named = reach(entryEnvironment(harness, projectPath), "GDHARNESS_PROJECT");
|
|
45331
|
+
return typeof named === "string" && named !== "" ? named : null;
|
|
45332
|
+
}
|
|
44947
45333
|
function reach(held, key) {
|
|
44948
45334
|
return typeof held === "object" && held !== null && !Array.isArray(held) ? held[key] : undefined;
|
|
44949
45335
|
}
|
|
@@ -45081,6 +45467,7 @@ function taken(harness, path, held, stripped) {
|
|
|
45081
45467
|
// src/cli.ts
|
|
45082
45468
|
init_headless();
|
|
45083
45469
|
init_issues();
|
|
45470
|
+
init_paths();
|
|
45084
45471
|
|
|
45085
45472
|
// src/prompt.ts
|
|
45086
45473
|
import process8 from "node:process";
|
|
@@ -45113,7 +45500,7 @@ init_setup();
|
|
|
45113
45500
|
init_setup();
|
|
45114
45501
|
init_tool_definitions();
|
|
45115
45502
|
import { existsSync as existsSync11, mkdirSync as mkdirSync5, readdirSync as readdirSync5, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "node:fs";
|
|
45116
|
-
import { basename as
|
|
45503
|
+
import { basename as basename4, dirname as dirname6, join as join11 } from "node:path";
|
|
45117
45504
|
|
|
45118
45505
|
// src/tool-reference.ts
|
|
45119
45506
|
init_headless_operations();
|
|
@@ -45238,7 +45625,8 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
45238
45625
|
(an older \`addonVersion\` on the same code is not stale), and \`staleNote\` says which half is behind: an editor that
|
|
45239
45626
|
needs restarting, or a server that needs reconnecting in your harness. Every answer that came
|
|
45240
45627
|
from the editor carries the same two when they differ, so a stale addon cannot answer quietly:
|
|
45241
|
-
believe a restart over a diagnostic that arrived with them.
|
|
45628
|
+
believe a restart over a diagnostic that arrived with them. A game's runtime addon is judged
|
|
45629
|
+
the same way, under each entry in \`runtimes\` and on every \`runtime_*\` answer from it.
|
|
45242
45630
|
- Start the game with \`editor_run start\`, never by spawning an engine. The editor plays it, so
|
|
45243
45631
|
its debugger holds it, which is what gives the \`debug_*\` tools something to talk to. Its answer
|
|
45244
45632
|
says under \`runtime\` whether the game can be talked to yet; when it cannot, \`mayYetAnnounce\`
|
|
@@ -45279,7 +45667,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
45279
45667
|
| Where a control is, and whether it is visible | \`runtime_inspect\` \`find\`, \`rect\` |
|
|
45280
45668
|
| What a property reads right now, on a node whose path you already hold | \`runtime_inspect\` \`property\`, \`runtime_invoke\` |
|
|
45281
45669
|
| What a property on an object a node holds reads | the same two, with colons: \`_game:clock:speed\` |
|
|
45282
|
-
| 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 |
|
|
45670
|
+
| 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 that only reads and walks into what it returned |
|
|
45283
45671
|
| 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 |
|
|
45284
45672
|
| 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 |
|
|
45285
45673
|
| Fill in a field | \`runtime_input click\` on it, then \`runtime_input text\` with \`replace\`; a submitted field has to be clicked again |
|
|
@@ -45440,7 +45828,7 @@ function removeSkill(directories, projectPath) {
|
|
|
45440
45828
|
function pruneEmpty(directory, projectPath) {
|
|
45441
45829
|
const shared = join11(projectPath, ".agents");
|
|
45442
45830
|
let path = directory;
|
|
45443
|
-
while (path !== projectPath && (
|
|
45831
|
+
while (path !== projectPath && (basename4(path) === "skills" || path === shared)) {
|
|
45444
45832
|
if (readdirSync5(path).length > 0) {
|
|
45445
45833
|
return;
|
|
45446
45834
|
}
|
|
@@ -45491,7 +45879,7 @@ class UsageError extends Refusal {
|
|
|
45491
45879
|
}
|
|
45492
45880
|
function projectArgument(at) {
|
|
45493
45881
|
const named = args.slice(at).find((value) => !value.startsWith("--"));
|
|
45494
|
-
const projectPath =
|
|
45882
|
+
const projectPath = resolve7(named ?? ".");
|
|
45495
45883
|
if (!existsSync18(join24(projectPath, "project.godot"))) {
|
|
45496
45884
|
throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
|
|
45497
45885
|
}
|
|
@@ -45503,11 +45891,17 @@ async function engine(projectPath) {
|
|
|
45503
45891
|
return { godotPath: located.path, script: shippedOperationsScript(), debug: GODOT_DEBUG_MODE_DEFAULT };
|
|
45504
45892
|
}
|
|
45505
45893
|
const recorded = located.named === null && projectPath !== undefined ? recordedEnginePath(projectPath) : null;
|
|
45506
|
-
if (recorded
|
|
45894
|
+
if (recorded !== null) {
|
|
45895
|
+
return { godotPath: recorded, script: shippedOperationsScript(), debug: GODOT_DEBUG_MODE_DEFAULT };
|
|
45896
|
+
}
|
|
45897
|
+
const main = located.named === null && projectPath !== undefined ? mainCheckoutOf(projectPath) : null;
|
|
45898
|
+
const theirs = main === null ? null : recordedEnginePath(main);
|
|
45899
|
+
if (main === null || theirs === null) {
|
|
45507
45900
|
const refusal = GodotLocator.refusal(located);
|
|
45508
45901
|
throw new UsageError(`${refusal.message} ${refusal.advice.join(". ")}.`);
|
|
45509
45902
|
}
|
|
45510
|
-
|
|
45903
|
+
console.error(`engine: ${theirs}, which the harness config of ${main} names; this project is a git worktree of it`);
|
|
45904
|
+
return { godotPath: theirs, script: shippedOperationsScript(), debug: GODOT_DEBUG_MODE_DEFAULT };
|
|
45511
45905
|
}
|
|
45512
45906
|
function said2(outcome, what) {
|
|
45513
45907
|
if (!outcome.ok) {
|
|
@@ -45680,12 +46074,14 @@ async function uninstall() {
|
|
|
45680
46074
|
const named = namedHarnesses();
|
|
45681
46075
|
const from = named.length > 0 ? named : HARNESSES;
|
|
45682
46076
|
for (const harness of from) {
|
|
45683
|
-
|
|
45684
|
-
|
|
46077
|
+
if (harness.scope === "home" && named.length === 0) {
|
|
46078
|
+
if (registered(harness, projectPath)) {
|
|
46079
|
+
console.log(`${harness.name}: left alone. Its config is machine-wide and may serve another project; pass --${harness.id} to remove it.`);
|
|
46080
|
+
}
|
|
45685
46081
|
continue;
|
|
45686
46082
|
}
|
|
45687
|
-
|
|
45688
|
-
|
|
46083
|
+
const removal = disconnect(harness, projectPath);
|
|
46084
|
+
if (removal.action === "absent") {
|
|
45689
46085
|
continue;
|
|
45690
46086
|
}
|
|
45691
46087
|
if (removal.action === "manual") {
|
|
@@ -45705,15 +46101,19 @@ async function uninstall() {
|
|
|
45705
46101
|
said2(outcome, `disabling ${name}`);
|
|
45706
46102
|
console.log(`${name}: ${outcome.ok ? String(outcome.payload["action"]) : "failed"}`);
|
|
45707
46103
|
}
|
|
45708
|
-
if (before.
|
|
46104
|
+
if (before.runtimeAutoloadPath !== null) {
|
|
45709
46105
|
said2(await setRuntime(godot, projectPath, false), "removing the runtime autoload");
|
|
45710
46106
|
console.log(`${RUNTIME_AUTOLOAD.name} autoload removed`);
|
|
45711
46107
|
}
|
|
46108
|
+
const loader = before.runtimeLoaderAutoload;
|
|
46109
|
+
if (loader !== null && loader.name !== RUNTIME_AUTOLOAD.name) {
|
|
46110
|
+
console.log(`${loader.name} names res://${loader.path}, which brings the runtime up its own way. That line is this project's, so it is left in place; the addon it brings up is gone now, so take the entry out unless that file does nothing without it.`);
|
|
46111
|
+
}
|
|
45712
46112
|
const addons = removeAddons(projectPath);
|
|
45713
46113
|
for (const path of addons) {
|
|
45714
46114
|
console.log(`removed ${path}`);
|
|
45715
46115
|
}
|
|
45716
|
-
const anything = addons.length > 0 || before.
|
|
46116
|
+
const anything = addons.length > 0 || before.runtimeAutoloadPath !== null || enabled.length > 0;
|
|
45717
46117
|
console.log(anything ? `
|
|
45718
46118
|
Reconnect your harness so it stops spawning a server that is no longer installed.` : `
|
|
45719
46119
|
Nothing of gdharness was in this project.`);
|
|
@@ -45722,13 +46122,13 @@ async function upgrade() {
|
|
|
45722
46122
|
const projectPath = projectArgument(1);
|
|
45723
46123
|
const version = getLocalVersion();
|
|
45724
46124
|
const before = inspectProject(projectPath);
|
|
45725
|
-
const
|
|
45726
|
-
if (
|
|
46125
|
+
const present = before.addons.find((addon) => addon.installed);
|
|
46126
|
+
if (present === undefined) {
|
|
45727
46127
|
const where = projectPath === process.cwd() ? "" : ` ${projectPath}`;
|
|
45728
46128
|
throw new UsageError(`gdharness is not installed in ${projectPath}. Run gdharness setup${where} instead.`);
|
|
45729
46129
|
}
|
|
45730
46130
|
const godot = await engine(projectPath);
|
|
45731
|
-
console.log(
|
|
46131
|
+
console.log(present.version === version ? `already ${version}; reinstalling` : `${present.version ?? "an unmarked copy"} -> ${version}`);
|
|
45732
46132
|
const digestBefore = installedEditorDigest(projectPath);
|
|
45733
46133
|
for (const addon of installAddons(projectPath)) {
|
|
45734
46134
|
console.log(`${addon.replaced ? "replaced" : "installed"} ${addon.path}`);
|
|
@@ -45742,7 +46142,21 @@ async function upgrade() {
|
|
|
45742
46142
|
}
|
|
45743
46143
|
said2(await runOperation(godot, "refresh_class_cache", {}, projectPath), "rebuilding the class list");
|
|
45744
46144
|
const launch = launchFor(version, godot.godotPath, projectPath);
|
|
45745
|
-
const
|
|
46145
|
+
const named = namedHarnesses();
|
|
46146
|
+
const already = HARNESSES.filter((harness) => {
|
|
46147
|
+
if (!registered(harness, projectPath)) {
|
|
46148
|
+
return false;
|
|
46149
|
+
}
|
|
46150
|
+
if (harness.scope !== "home" || named.includes(harness)) {
|
|
46151
|
+
return true;
|
|
46152
|
+
}
|
|
46153
|
+
const serves = servedProject(harness, projectPath);
|
|
46154
|
+
if (serves !== null && isSameDirectory(serves, projectPath)) {
|
|
46155
|
+
return true;
|
|
46156
|
+
}
|
|
46157
|
+
console.log(`${harness.name}: left alone. Its config is machine-wide and ${serves === null ? "names no project" : `serves ${serves}`}; pass --${harness.id} to point it at this one.`);
|
|
46158
|
+
return false;
|
|
46159
|
+
});
|
|
45746
46160
|
const moved = [];
|
|
45747
46161
|
let written = 0;
|
|
45748
46162
|
let byHand = 0;
|