gdharness 0.13.36 → 0.13.37
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 +494 -222
- package/build/index.js +341 -123
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -133,6 +133,28 @@ function godotCandidates(platform, home) {
|
|
|
133
133
|
}
|
|
134
134
|
var init_detection = () => {};
|
|
135
135
|
|
|
136
|
+
// src/errors.ts
|
|
137
|
+
function errorMessage(error, fallback = "Unknown error") {
|
|
138
|
+
if (error instanceof Error) {
|
|
139
|
+
return error.message || fallback;
|
|
140
|
+
}
|
|
141
|
+
if (typeof error === "string") {
|
|
142
|
+
return error || fallback;
|
|
143
|
+
}
|
|
144
|
+
if (typeof error === "number" || typeof error === "boolean") {
|
|
145
|
+
return String(error);
|
|
146
|
+
}
|
|
147
|
+
return fallback;
|
|
148
|
+
}
|
|
149
|
+
function toError(error) {
|
|
150
|
+
return error instanceof Error ? error : new Error(errorMessage(error), { cause: error });
|
|
151
|
+
}
|
|
152
|
+
var Refusal2;
|
|
153
|
+
var init_errors = __esm(() => {
|
|
154
|
+
Refusal2 = class Refusal2 extends Error {
|
|
155
|
+
};
|
|
156
|
+
});
|
|
157
|
+
|
|
136
158
|
// src/launch.ts
|
|
137
159
|
function envValue(name, variables = process.env) {
|
|
138
160
|
const value = variables[name];
|
|
@@ -176,7 +198,10 @@ function userDataIn(home, variables = process.env) {
|
|
|
176
198
|
}
|
|
177
199
|
return carried;
|
|
178
200
|
}
|
|
179
|
-
|
|
201
|
+
function savesStayPut(platform = process.platform) {
|
|
202
|
+
return platform === "darwin";
|
|
203
|
+
}
|
|
204
|
+
var OPENED_BY_A_SERVER = "GDHARNESS_OPENED_BY_A_SERVER", 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.";
|
|
180
205
|
|
|
181
206
|
// node_modules/yaml/dist/nodes/identity.js
|
|
182
207
|
var require_identity = __commonJS(function(exports) {
|
|
@@ -7120,28 +7145,6 @@ var require_public_api = __commonJS(function(exports) {
|
|
|
7120
7145
|
exports.stringify = stringify;
|
|
7121
7146
|
});
|
|
7122
7147
|
|
|
7123
|
-
// src/errors.ts
|
|
7124
|
-
function errorMessage(error, fallback = "Unknown error") {
|
|
7125
|
-
if (error instanceof Error) {
|
|
7126
|
-
return error.message || fallback;
|
|
7127
|
-
}
|
|
7128
|
-
if (typeof error === "string") {
|
|
7129
|
-
return error || fallback;
|
|
7130
|
-
}
|
|
7131
|
-
if (typeof error === "number" || typeof error === "boolean") {
|
|
7132
|
-
return String(error);
|
|
7133
|
-
}
|
|
7134
|
-
return fallback;
|
|
7135
|
-
}
|
|
7136
|
-
function toError(error) {
|
|
7137
|
-
return error instanceof Error ? error : new Error(errorMessage(error), { cause: error });
|
|
7138
|
-
}
|
|
7139
|
-
var Refusal2;
|
|
7140
|
-
var init_errors = __esm(() => {
|
|
7141
|
-
Refusal2 = class Refusal2 extends Error {
|
|
7142
|
-
};
|
|
7143
|
-
});
|
|
7144
|
-
|
|
7145
7148
|
// src/runner.ts
|
|
7146
7149
|
import { existsSync as existsSync3 } from "node:fs";
|
|
7147
7150
|
import { dirname, join as join2 } from "node:path";
|
|
@@ -7200,7 +7203,7 @@ class GameLog {
|
|
|
7200
7203
|
};
|
|
7201
7204
|
partial = { stdout: "", stderr: "", transcript: "" };
|
|
7202
7205
|
lastHeadline = null;
|
|
7203
|
-
|
|
7206
|
+
seen = { error: 0, warning: 0, info: 0 };
|
|
7204
7207
|
append(source, chunk) {
|
|
7205
7208
|
const decoded = typeof chunk === "string" ? chunk : this.decoders[source].write(chunk);
|
|
7206
7209
|
const lines = (this.partial[source] + decoded).split(`
|
|
@@ -7257,10 +7260,14 @@ class GameLog {
|
|
|
7257
7260
|
return this.entries.filter((entry) => entry.severity === severity).length;
|
|
7258
7261
|
}
|
|
7259
7262
|
select(options) {
|
|
7260
|
-
const floor = options.sinceLastCall ? this.
|
|
7261
|
-
|
|
7262
|
-
const wanted = new Set(options.severity === "error" ? ["error"] : options.severity === "warning" ? ["error", "warning"] : ["error", "warning", "info"]);
|
|
7263
|
+
const floor = options.sinceLastCall ? this.seen[options.severity] : 0;
|
|
7264
|
+
const wanted = ADMITTED[options.severity];
|
|
7263
7265
|
const needle = options.contains?.toLowerCase();
|
|
7266
|
+
if (needle === undefined) {
|
|
7267
|
+
for (const severity of wanted) {
|
|
7268
|
+
this.seen[severity] = this.entries.length;
|
|
7269
|
+
}
|
|
7270
|
+
}
|
|
7264
7271
|
const matching = this.entries.slice(floor).filter((entry) => {
|
|
7265
7272
|
if (!wanted.has(entry.severity)) {
|
|
7266
7273
|
return false;
|
|
@@ -7274,11 +7281,16 @@ class GameLog {
|
|
|
7274
7281
|
return { entries: matching.slice(omitted), omitted };
|
|
7275
7282
|
}
|
|
7276
7283
|
}
|
|
7277
|
-
var HEADLINE, COLOUR_CODE, STREAMS;
|
|
7284
|
+
var HEADLINE, COLOUR_CODE, STREAMS, ADMITTED;
|
|
7278
7285
|
var init_game_log = __esm(() => {
|
|
7279
7286
|
HEADLINE = /^(USER )?(SCRIPT ERROR|ERROR|WARNING):\s?(.*)$/;
|
|
7280
7287
|
COLOUR_CODE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
7281
7288
|
STREAMS = ["stdout", "stderr", "transcript"];
|
|
7289
|
+
ADMITTED = {
|
|
7290
|
+
error: new Set(["error"]),
|
|
7291
|
+
warning: new Set(["error", "warning"]),
|
|
7292
|
+
info: new Set(["error", "warning", "info"])
|
|
7293
|
+
};
|
|
7282
7294
|
});
|
|
7283
7295
|
|
|
7284
7296
|
// src/scratch.ts
|
|
@@ -7311,6 +7323,20 @@ var init_scratch = __esm(() => {
|
|
|
7311
7323
|
import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
7312
7324
|
import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
|
|
7313
7325
|
import { join as join5 } from "node:path";
|
|
7326
|
+
function packageUrl(root) {
|
|
7327
|
+
return `${root.replace(/\/+$/, "")}/gdharness/latest`;
|
|
7328
|
+
}
|
|
7329
|
+
function registryFor(environment) {
|
|
7330
|
+
const named = environment["GDHARNESS_REGISTRY"] ?? "";
|
|
7331
|
+
if (named === "") {
|
|
7332
|
+
return packageUrl(REGISTRY_ROOT);
|
|
7333
|
+
}
|
|
7334
|
+
try {
|
|
7335
|
+
return new URL(named).protocol === "https:" ? packageUrl(named) : packageUrl(REGISTRY_ROOT);
|
|
7336
|
+
} catch {
|
|
7337
|
+
return packageUrl(REGISTRY_ROOT);
|
|
7338
|
+
}
|
|
7339
|
+
}
|
|
7314
7340
|
function cacheDirectory(environment) {
|
|
7315
7341
|
const set = (name) => {
|
|
7316
7342
|
const value = environment[name];
|
|
@@ -7380,8 +7406,8 @@ function isNewer(candidate, current) {
|
|
|
7380
7406
|
}
|
|
7381
7407
|
return !left.prerelease && right.prerelease;
|
|
7382
7408
|
}
|
|
7383
|
-
async function fetchLatest() {
|
|
7384
|
-
const response = await fetch(
|
|
7409
|
+
async function fetchLatest(registry) {
|
|
7410
|
+
const response = await fetch(registry, {
|
|
7385
7411
|
headers: { accept: "application/vnd.npm.install-v1+json, application/json" },
|
|
7386
7412
|
redirect: "error",
|
|
7387
7413
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
@@ -7413,16 +7439,19 @@ async function fetchLatest() {
|
|
|
7413
7439
|
class UpdateCheck {
|
|
7414
7440
|
latest = null;
|
|
7415
7441
|
checking = false;
|
|
7442
|
+
asked = false;
|
|
7416
7443
|
checkedAt = 0;
|
|
7417
7444
|
retryAt = 0;
|
|
7418
7445
|
backoffMs = FIRST_RETRY_MS;
|
|
7419
7446
|
enabled;
|
|
7420
7447
|
current;
|
|
7421
7448
|
cachePath;
|
|
7422
|
-
|
|
7449
|
+
ask;
|
|
7450
|
+
constructor(current, environment = process.env, ask = () => fetchLatest(registryFor(environment))) {
|
|
7423
7451
|
this.current = current;
|
|
7424
7452
|
this.enabled = (environment["GDHARNESS_NO_UPDATE_CHECK"] ?? "") === "";
|
|
7425
7453
|
this.cachePath = cacheFile(environment);
|
|
7454
|
+
this.ask = ask;
|
|
7426
7455
|
const cached = this.enabled ? readCache(this.cachePath) : null;
|
|
7427
7456
|
if (cached !== null) {
|
|
7428
7457
|
this.latest = cached.latest;
|
|
@@ -7430,11 +7459,12 @@ class UpdateCheck {
|
|
|
7430
7459
|
}
|
|
7431
7460
|
}
|
|
7432
7461
|
refresh(now = Date.now()) {
|
|
7433
|
-
if (!this.enabled || this.checking || now < this.retryAt || !this.stale(now)) {
|
|
7462
|
+
if (!this.enabled || this.checking || now < this.retryAt || this.asked && !this.stale(now)) {
|
|
7434
7463
|
return;
|
|
7435
7464
|
}
|
|
7465
|
+
this.asked = true;
|
|
7436
7466
|
this.checking = true;
|
|
7437
|
-
|
|
7467
|
+
this.ask().then((version) => {
|
|
7438
7468
|
if (version === null) {
|
|
7439
7469
|
this.scheduleRetry(now);
|
|
7440
7470
|
return;
|
|
@@ -7473,10 +7503,10 @@ class UpdateCheck {
|
|
|
7473
7503
|
};
|
|
7474
7504
|
}
|
|
7475
7505
|
}
|
|
7476
|
-
var
|
|
7506
|
+
var REGISTRY_ROOT = "https://registry.npmjs.org", RELEASES = "https://github.com/Aureliolo/gdharness/releases/tag", CACHE_MS, REQUEST_TIMEOUT_MS = 1e4, MAX_BODY_BYTES, FIRST_RETRY_MS = 30000, MAX_RETRY_MS, VERSION;
|
|
7477
7507
|
var init_update_check = __esm(() => {
|
|
7478
7508
|
init_runner();
|
|
7479
|
-
CACHE_MS =
|
|
7509
|
+
CACHE_MS = 10 * 60 * 1000;
|
|
7480
7510
|
MAX_BODY_BYTES = 1 << 20;
|
|
7481
7511
|
MAX_RETRY_MS = 60 * 60 * 1000;
|
|
7482
7512
|
VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
@@ -7625,7 +7655,7 @@ function staleAnalysisNote(contradicted, dependsOn) {
|
|
|
7625
7655
|
}
|
|
7626
7656
|
const types = new Set(contradicted.map((one) => one.type));
|
|
7627
7657
|
const declaring = [...new Set(contradicted.map((one) => one.declaredIn))].sort();
|
|
7628
|
-
const lever = dependsOn.length === 0 ? ", and the named types depend on no other global class, so that lever is not available here. " + "That is the common case rather than the awkward one: an edit near a type that depends on " + "something refreshes it before the fault is noticed, which is why a type with no " + "dependencies is where this turns up" : `, so changing ${[...dependsOn].sort().join("
|
|
7658
|
+
const lever = dependsOn.length === 0 ? ", and the named types depend on no other global class, so that lever is not available here. " + "That is the common case rather than the awkward one: an edit near a type that depends on " + "something refreshes it before the fault is noticed, which is why a type with no " + "dependencies is where this turns up" : `, so changing ${[...dependsOn].sort().join(" or ")} and then rescanning is worth a try`;
|
|
7629
7659
|
return `The editor is reporting against an older copy of ${types.size === 1 ? "a type" : "some types"} ` + "named under contradictedByTheFile. Each member listed is declared in the file the class cache " + "points at, so those diagnostics are wrong however the code is written. Run editor_rescan first: " + "it costs about half a second, and it has cleared this in every reproduction measured since the " + "scan timing was fixed: both in one project, the scan returning in 243ms and 275ms, and one in " + "another where the same call also rebuilt the copy. " + "The fault behind it is that a script the editor has loaded keeps the copy it built, and nothing " + `rebuilds that copy when the file changes; ${declaring.length === 1 ? `editor_rescan with reloadScript set to ${declaring[0]} recompiles that copy` : `reloadScript takes one script, so a call each for ${declaring.join(" and ")} recompiles those copies`} from the file into the same ` + "object and answers with the members it has afterwards under reloadedMethods. Read that and the " + "next diagnostics as two readings, because they are of two things: measured in one window on " + "one editor, the analyser resolved a call to a newly added method while the built copy of that " + "same script did not have it. So the reload is worth doing and is not what clears these; the " + "scan is. The other lever measured is that a held type is refreshed when something it " + `depends on changes rather than when it changes itself${lever}. editor_launch restart has always ` + "worked and costs a window. project_import refresh_classes does not, and answers added: [] while " + "this is happening.";
|
|
7630
7660
|
}
|
|
7631
7661
|
function uncachedClassNote(types) {
|
|
@@ -15048,6 +15078,11 @@ function settingKeys(content) {
|
|
|
15048
15078
|
return named;
|
|
15049
15079
|
}
|
|
15050
15080
|
function settingsDroppedReport(before, after) {
|
|
15081
|
+
if (before === null || after === null) {
|
|
15082
|
+
return {
|
|
15083
|
+
settingsNote: `project.godot could not be read ${before === null ? "before" : "after"} this, so nothing can` + " be said about what the editor saved away. Opening a project rewrites the file and drops any" + " key sitting at its own default, and this is the only thing that would have named them." + " Read the file yourself if a setting was pinned at a default on purpose."
|
|
15084
|
+
};
|
|
15085
|
+
}
|
|
15051
15086
|
const dropped = [...before].filter(([key]) => !after.has(key));
|
|
15052
15087
|
if (dropped.length === 0) {
|
|
15053
15088
|
return {};
|
|
@@ -15665,7 +15700,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15665
15700
|
},
|
|
15666
15701
|
{
|
|
15667
15702
|
name: "project_test",
|
|
15668
|
-
description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Suites where everything passed are counted rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines. The class list is rebuilt first, so a suite written a moment ago is found. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so and names the path it looked in. A run where nothing failed but nodes were left in the tree comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. Needs gdUnit4 under addons/gdUnit4.",
|
|
15703
|
+
description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Suites where everything passed are counted rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines. The class list is rebuilt first, so a suite written a moment ago is found. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. On macOS the engine reads user:// off HOME and the run writes where the player keeps theirs, which the answer says under savesNote rather than leaving to be found in a save list. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so and names the path it looked in. A run where nothing failed but nodes were left in the tree comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. Needs gdUnit4 under addons/gdUnit4.",
|
|
15669
15704
|
parameters: {
|
|
15670
15705
|
projectPath: PROJECT_PATH,
|
|
15671
15706
|
path: {
|
|
@@ -15932,7 +15967,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15932
15967
|
},
|
|
15933
15968
|
{
|
|
15934
15969
|
name: "editor_launch",
|
|
15935
|
-
description: "Opens the Godot editor on a project, in a window on this machine, or restarts the one already connected. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect; it saves open scenes on the way out and answers with the version that came back. Only an editor with a window can be restarted, 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.",
|
|
15970
|
+
description: "Opens the Godot editor on a project, in a window on this machine, or restarts the one already connected. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect; it saves open scenes on the way out and answers with the version that came back. Only an editor with a window can be restarted, 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.",
|
|
15936
15971
|
parameters: { projectPath: { ...PROJECT_PATH, ops: ["open"] } },
|
|
15937
15972
|
requires: [],
|
|
15938
15973
|
operations: {
|
|
@@ -16003,7 +16038,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16003
16038
|
},
|
|
16004
16039
|
sinceLastCall: {
|
|
16005
16040
|
type: "boolean",
|
|
16006
|
-
description: "Only entries
|
|
16041
|
+
description: "Only entries this has not already answered with at this severity. Default false. Measured per severity floor and not across them, so polling for errors leaves the rest of the output for a later read rather than consuming it, and a read with contains is a search over the whole run and consumes nothing."
|
|
16007
16042
|
},
|
|
16008
16043
|
cpu: {
|
|
16009
16044
|
type: "boolean",
|
|
@@ -16022,7 +16057,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16022
16057
|
},
|
|
16023
16058
|
{
|
|
16024
16059
|
name: "editor_status",
|
|
16025
|
-
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. 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. 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. 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.',
|
|
16060
|
+
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. 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. 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.',
|
|
16026
16061
|
parameters: {},
|
|
16027
16062
|
requires: []
|
|
16028
16063
|
},
|
|
@@ -35225,57 +35260,77 @@ import { execFile as execFile4 } from "node:child_process";
|
|
|
35225
35260
|
import { existsSync as existsSync11 } from "node:fs";
|
|
35226
35261
|
import { normalize as normalize2 } from "node:path";
|
|
35227
35262
|
import { promisify as promisify4 } from "node:util";
|
|
35228
|
-
|
|
35229
|
-
|
|
35230
|
-
|
|
35231
|
-
|
|
35232
|
-
|
|
35233
|
-
|
|
35234
|
-
|
|
35235
|
-
|
|
35236
|
-
|
|
35237
|
-
|
|
35238
|
-
|
|
35239
|
-
|
|
35240
|
-
const
|
|
35241
|
-
if (
|
|
35242
|
-
|
|
35243
|
-
|
|
35244
|
-
this.found = path;
|
|
35245
|
-
}
|
|
35246
|
-
return this.found;
|
|
35247
|
-
}
|
|
35248
|
-
for (const candidate of godotCandidates(process.platform, resolveHomeDirectory())) {
|
|
35249
|
-
const path = normalize2(candidate);
|
|
35250
|
-
if (await this.answers(path)) {
|
|
35251
|
-
this.found = path;
|
|
35252
|
-
return path;
|
|
35253
|
-
}
|
|
35263
|
+
|
|
35264
|
+
class GodotLocator2 {
|
|
35265
|
+
answered = new Map;
|
|
35266
|
+
found = null;
|
|
35267
|
+
async find() {
|
|
35268
|
+
if (this.found !== null && await this.whyNot(this.found) === null) {
|
|
35269
|
+
return { ok: true, path: this.found };
|
|
35270
|
+
}
|
|
35271
|
+
this.found = null;
|
|
35272
|
+
const named = envValue("GODOT_PATH");
|
|
35273
|
+
if (named !== undefined) {
|
|
35274
|
+
const path = normalize2(named);
|
|
35275
|
+
const reason = await this.whyNot(path);
|
|
35276
|
+
if (reason === null) {
|
|
35277
|
+
this.found = path;
|
|
35278
|
+
return { ok: true, path };
|
|
35254
35279
|
}
|
|
35255
|
-
return
|
|
35280
|
+
return { ok: false, named: path, reason };
|
|
35256
35281
|
}
|
|
35257
|
-
|
|
35258
|
-
|
|
35259
|
-
|
|
35260
|
-
|
|
35261
|
-
|
|
35262
|
-
const known = this.answered.get(path);
|
|
35263
|
-
if (known !== undefined) {
|
|
35264
|
-
return known;
|
|
35282
|
+
for (const candidate of godotCandidates(process.platform, resolveHomeDirectory())) {
|
|
35283
|
+
const path = normalize2(candidate);
|
|
35284
|
+
if (await this.whyNot(path) === null) {
|
|
35285
|
+
this.found = path;
|
|
35286
|
+
return { ok: true, path };
|
|
35265
35287
|
}
|
|
35266
|
-
|
|
35267
|
-
|
|
35268
|
-
|
|
35269
|
-
|
|
35270
|
-
|
|
35271
|
-
|
|
35272
|
-
|
|
35273
|
-
|
|
35288
|
+
}
|
|
35289
|
+
return { ok: false, named: null };
|
|
35290
|
+
}
|
|
35291
|
+
static refusal(located) {
|
|
35292
|
+
if (located.named === null) {
|
|
35293
|
+
return {
|
|
35294
|
+
message: "No Godot executable found.",
|
|
35295
|
+
advice: [
|
|
35296
|
+
"Set GODOT_PATH to the Godot 4 executable",
|
|
35297
|
+
"Or install Godot where it is usually found, such as /usr/bin/godot or C:\\Program Files\\Godot"
|
|
35298
|
+
]
|
|
35299
|
+
};
|
|
35300
|
+
}
|
|
35301
|
+
return {
|
|
35302
|
+
message: `GODOT_PATH is set to ${located.named}, which ${located.reason ?? "does not answer"}.`,
|
|
35303
|
+
advice: [
|
|
35304
|
+
"Point GODOT_PATH at a Godot 4 executable that answers --version",
|
|
35305
|
+
"Or unset it, and the usual install locations are searched instead"
|
|
35306
|
+
]
|
|
35307
|
+
};
|
|
35308
|
+
}
|
|
35309
|
+
async whyNot(path) {
|
|
35310
|
+
const known = this.answered.get(path);
|
|
35311
|
+
if (known !== undefined) {
|
|
35312
|
+
return known;
|
|
35313
|
+
}
|
|
35314
|
+
let reason;
|
|
35315
|
+
if (path !== "godot" && !existsSync11(path)) {
|
|
35316
|
+
reason = "does not exist";
|
|
35317
|
+
} else {
|
|
35318
|
+
try {
|
|
35319
|
+
await run4(path, ["--version"]);
|
|
35320
|
+
reason = null;
|
|
35321
|
+
} catch (error) {
|
|
35322
|
+
reason = `does not answer --version: ${errorMessage(error)}`;
|
|
35274
35323
|
}
|
|
35275
|
-
this.answered.set(path, ok);
|
|
35276
|
-
return ok;
|
|
35277
35324
|
}
|
|
35278
|
-
|
|
35325
|
+
this.answered.set(path, reason);
|
|
35326
|
+
return reason;
|
|
35327
|
+
}
|
|
35328
|
+
}
|
|
35329
|
+
var run4;
|
|
35330
|
+
var init_godot_path = __esm(() => {
|
|
35331
|
+
init_detection();
|
|
35332
|
+
init_errors();
|
|
35333
|
+
run4 = promisify4(execFile4);
|
|
35279
35334
|
});
|
|
35280
35335
|
|
|
35281
35336
|
// src/issues.ts
|
|
@@ -35344,15 +35399,36 @@ function filledTemplate2(where, message, godotVersion) {
|
|
|
35344
35399
|
].join(`
|
|
35345
35400
|
`);
|
|
35346
35401
|
}
|
|
35347
|
-
|
|
35402
|
+
|
|
35403
|
+
class DefectsSeen {
|
|
35404
|
+
counts = new Map;
|
|
35405
|
+
before(where, message) {
|
|
35406
|
+
const signature = defectSignature2(where, message);
|
|
35407
|
+
const seen = this.counts.get(signature) ?? 0;
|
|
35408
|
+
this.counts.set(signature, seen + 1);
|
|
35409
|
+
return seen;
|
|
35410
|
+
}
|
|
35411
|
+
}
|
|
35412
|
+
function defectReport2(where, error, context = {}) {
|
|
35413
|
+
const { godotVersion, seenBefore = 0 } = context;
|
|
35348
35414
|
const message = errorMessage(error);
|
|
35349
35415
|
const title = `Defect ${defectSignature2(where, message)}: ${where}`;
|
|
35350
35416
|
const url = `${NEW_ISSUE2}?template=bug_report.md&title=${encodeURIComponent(title)}` + `&body=${encodeURIComponent(filledTemplate2(where, message, godotVersion))}`;
|
|
35351
35417
|
const width = Math.max(...facts2(where, message, godotVersion).map(([label]) => (label ?? "").length));
|
|
35352
35418
|
return [
|
|
35353
|
-
"gdharness failed in a way it does not model.
|
|
35354
|
-
"
|
|
35355
|
-
"
|
|
35419
|
+
"gdharness failed in a way it does not model. Whatever went wrong underneath, it reached you",
|
|
35420
|
+
"as a crash rather than as a refusal naming what would have worked, and that much is a defect",
|
|
35421
|
+
"in gdharness however the rest of it turns out.",
|
|
35422
|
+
"",
|
|
35423
|
+
...seenBefore === 0 ? [
|
|
35424
|
+
"Send the call once more before anything else. Some of what reaches this line is a file",
|
|
35425
|
+
"another process had open, or a connection that dropped, and those are gone by the next",
|
|
35426
|
+
"attempt. If the same Signature comes back, it is not one of those, and repeating it",
|
|
35427
|
+
"further only spends turns."
|
|
35428
|
+
] : [
|
|
35429
|
+
`You have had this exact failure ${seenBefore === 1 ? "once" : `${seenBefore} times`} in this session already, so it is a`,
|
|
35430
|
+
"state rather than a passing one, and sending the call again will not change it."
|
|
35431
|
+
],
|
|
35356
35432
|
"",
|
|
35357
35433
|
...facts2(where, message, godotVersion).map(([label, value]) => ` ${(label ?? "").padEnd(width)} ${value ?? ""}`),
|
|
35358
35434
|
"",
|
|
@@ -36315,11 +36391,53 @@ var init_project_scan = __esm(() => {
|
|
|
36315
36391
|
CARRIES_A_UID = new Set(["gd", "shader", "gdshader"]);
|
|
36316
36392
|
});
|
|
36317
36393
|
|
|
36394
|
+
// src/restart-note.ts
|
|
36395
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync13, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "node:fs";
|
|
36396
|
+
import { dirname as dirname9, join as join15 } from "node:path";
|
|
36397
|
+
function restartNotePath(projectPath) {
|
|
36398
|
+
return join15(projectPath, ".godot", "gdharness-restart.json");
|
|
36399
|
+
}
|
|
36400
|
+
function noteRestartBegun(note) {
|
|
36401
|
+
const path = restartNotePath(note.projectPath);
|
|
36402
|
+
try {
|
|
36403
|
+
mkdirSync7(dirname9(path), { recursive: true });
|
|
36404
|
+
writeFileSync9(path, `${JSON.stringify(note, null, 2)}
|
|
36405
|
+
`, "utf8");
|
|
36406
|
+
} catch (error) {
|
|
36407
|
+
console.error(`[SERVER] Could not record the restart at ${path}: ${errorMessage(error)}`);
|
|
36408
|
+
}
|
|
36409
|
+
}
|
|
36410
|
+
function restartOwed(projectPath) {
|
|
36411
|
+
try {
|
|
36412
|
+
const parsed = JSON.parse(readFileSync13(restartNotePath(projectPath), "utf8"));
|
|
36413
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
36414
|
+
return null;
|
|
36415
|
+
}
|
|
36416
|
+
const said = parsed;
|
|
36417
|
+
if (typeof said.quitAt !== "string" || typeof said.byPid !== "number" || typeof said.ports !== "object") {
|
|
36418
|
+
return null;
|
|
36419
|
+
}
|
|
36420
|
+
return said;
|
|
36421
|
+
} catch {
|
|
36422
|
+
return null;
|
|
36423
|
+
}
|
|
36424
|
+
}
|
|
36425
|
+
function restartSettled(projectPath) {
|
|
36426
|
+
try {
|
|
36427
|
+
rmSync7(restartNotePath(projectPath), { force: true });
|
|
36428
|
+
} catch (error) {
|
|
36429
|
+
console.error(`[SERVER] Could not clear the restart note: ${errorMessage(error)}`);
|
|
36430
|
+
}
|
|
36431
|
+
}
|
|
36432
|
+
var init_restart_note = __esm(() => {
|
|
36433
|
+
init_errors();
|
|
36434
|
+
});
|
|
36435
|
+
|
|
36318
36436
|
// src/runtime-client.ts
|
|
36319
|
-
import { existsSync as existsSync13, readdirSync as readdirSync5, readFileSync as
|
|
36437
|
+
import { existsSync as existsSync13, readdirSync as readdirSync5, readFileSync as readFileSync14, unlinkSync } from "node:fs";
|
|
36320
36438
|
import { createConnection as createConnection3 } from "node:net";
|
|
36321
36439
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
36322
|
-
import { join as
|
|
36440
|
+
import { join as join16, resolve as resolve4 } from "node:path";
|
|
36323
36441
|
import { setTimeout as delay2 } from "node:timers/promises";
|
|
36324
36442
|
function runtimeDirectory(variables = process.env) {
|
|
36325
36443
|
const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
|
|
@@ -36327,13 +36445,13 @@ function runtimeDirectory(variables = process.env) {
|
|
|
36327
36445
|
return explicit;
|
|
36328
36446
|
}
|
|
36329
36447
|
const perUser = envValue("XDG_RUNTIME_DIR", variables);
|
|
36330
|
-
return
|
|
36448
|
+
return join16(perUser ?? tmpdir4(), "gdharness");
|
|
36331
36449
|
}
|
|
36332
36450
|
function runtimeDirectories(variables = process.env) {
|
|
36333
36451
|
const candidates = [runtimeDirectory(variables)];
|
|
36334
36452
|
const fallbacks = process.platform === "win32" ? [envValue("SystemRoot", variables) ?? envValue("windir", variables) ?? "C:\\Windows"] : ["/tmp", "/var/tmp"];
|
|
36335
36453
|
for (const base of fallbacks) {
|
|
36336
|
-
candidates.push(
|
|
36454
|
+
candidates.push(join16(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
|
|
36337
36455
|
}
|
|
36338
36456
|
return [...new Set(candidates.map((path) => resolve4(path)))];
|
|
36339
36457
|
}
|
|
@@ -36348,7 +36466,7 @@ function processAlive(pid) {
|
|
|
36348
36466
|
function parseAnnouncement(file, pid) {
|
|
36349
36467
|
let fields;
|
|
36350
36468
|
try {
|
|
36351
|
-
fields = asParams(JSON.parse(
|
|
36469
|
+
fields = asParams(JSON.parse(readFileSync14(file, "utf8")));
|
|
36352
36470
|
} catch {
|
|
36353
36471
|
return { kind: "rubbish" };
|
|
36354
36472
|
}
|
|
@@ -36386,6 +36504,28 @@ function runtimesAnnounced(directories = runtimeDirectories()) {
|
|
|
36386
36504
|
function discoverRuntimes(directories = runtimeDirectories()) {
|
|
36387
36505
|
return runtimesAnnounced(directories).running;
|
|
36388
36506
|
}
|
|
36507
|
+
function wentAway(file, pid) {
|
|
36508
|
+
let project = "";
|
|
36509
|
+
try {
|
|
36510
|
+
const fields = asParams(JSON.parse(readFileSync14(file, "utf8")));
|
|
36511
|
+
project = readString(readParams(fields, "project") ?? {}, "path") ?? "";
|
|
36512
|
+
} catch {
|
|
36513
|
+
return;
|
|
36514
|
+
}
|
|
36515
|
+
if (project === "") {
|
|
36516
|
+
return;
|
|
36517
|
+
}
|
|
36518
|
+
gone.unshift({ pid, project, noticedAt: Date.now() });
|
|
36519
|
+
gone.length = Math.min(gone.length, GONE_KEPT);
|
|
36520
|
+
}
|
|
36521
|
+
function announcedAndGone(projectPath) {
|
|
36522
|
+
if (projectPath === undefined) {
|
|
36523
|
+
return [];
|
|
36524
|
+
}
|
|
36525
|
+
const since = Date.now() - GONE_WINDOW_MS;
|
|
36526
|
+
const wanted = resolve4(projectPath);
|
|
36527
|
+
return gone.filter((one) => one.noticedAt >= since && resolve4(one.project) === wanted);
|
|
36528
|
+
}
|
|
36389
36529
|
function announcedIn(directory) {
|
|
36390
36530
|
if (!existsSync13(directory)) {
|
|
36391
36531
|
return [];
|
|
@@ -36396,10 +36536,14 @@ function announcedIn(directory) {
|
|
|
36396
36536
|
if (!match) {
|
|
36397
36537
|
continue;
|
|
36398
36538
|
}
|
|
36399
|
-
const file =
|
|
36539
|
+
const file = join16(directory, entry);
|
|
36400
36540
|
const pid = Number.parseInt(match[1] ?? "", 10);
|
|
36401
|
-
const
|
|
36541
|
+
const alive = processAlive(pid);
|
|
36542
|
+
const announced = alive ? parseAnnouncement(file, pid) : { kind: "rubbish" };
|
|
36402
36543
|
if (announced.kind === "rubbish") {
|
|
36544
|
+
if (!alive) {
|
|
36545
|
+
wentAway(file, pid);
|
|
36546
|
+
}
|
|
36403
36547
|
try {
|
|
36404
36548
|
unlinkSync(file);
|
|
36405
36549
|
} catch {}
|
|
@@ -36438,6 +36582,16 @@ function chooseRuntime(endpoints, projectPath, unspoken = []) {
|
|
|
36438
36582
|
if (unspoken.length > 0) {
|
|
36439
36583
|
return { problem: tooNew(unspoken) };
|
|
36440
36584
|
}
|
|
36585
|
+
const went = announcedAndGone(projectPath);
|
|
36586
|
+
const first = went[0];
|
|
36587
|
+
if (first !== undefined) {
|
|
36588
|
+
const ago = Math.max(1, Math.round((Date.now() - first.noticedAt) / 1000));
|
|
36589
|
+
const others = went.length - 1;
|
|
36590
|
+
const earlier = others === 0 ? "" : ` ${others} earlier ${others === 1 ? "one" : "ones"} went the same way.`;
|
|
36591
|
+
return {
|
|
36592
|
+
problem: `A game with the runtime addon announced itself and its process is gone: pid ${first.pid}` + `${first.project === "" ? "" : ` for ${first.project}`}, noticed ${ago}s ago.${earlier}` + " It quit or was ended rather than never starting, so editor_output has what it printed" + " on the way, including whatever it broke on. Start another with editor_run once you have read it."
|
|
36593
|
+
};
|
|
36594
|
+
}
|
|
36441
36595
|
return {
|
|
36442
36596
|
problem: "No game with the runtime addon is running. Start one with editor_run, or play the project from the editor with the addon enabled."
|
|
36443
36597
|
};
|
|
@@ -36551,43 +36705,44 @@ function runtimeRequest(endpoint, command, params, timeoutMs) {
|
|
|
36551
36705
|
});
|
|
36552
36706
|
});
|
|
36553
36707
|
}
|
|
36554
|
-
var RUNTIME_PROTOCOL = 2, ANNOUNCEMENT_PATTERN, LOOK_EVERY_MS = 50, ANNOUNCE_BUDGET_MS = 5000, nextRequestId = 1;
|
|
36708
|
+
var RUNTIME_PROTOCOL = 2, ANNOUNCEMENT_PATTERN, GONE_WINDOW_MS = 120000, GONE_KEPT = 32, gone, LOOK_EVERY_MS = 50, ANNOUNCE_BUDGET_MS = 5000, nextRequestId = 1;
|
|
36555
36709
|
var init_runtime_client = __esm(() => {
|
|
36556
36710
|
ANNOUNCEMENT_PATTERN = /^runtime-(\d+)\.json$/;
|
|
36711
|
+
gone = [];
|
|
36557
36712
|
});
|
|
36558
36713
|
|
|
36559
36714
|
// src/run-record.ts
|
|
36560
36715
|
import { execFileSync } from "node:child_process";
|
|
36561
|
-
import { mkdirSync as
|
|
36562
|
-
import { basename as basename2, join as
|
|
36716
|
+
import { mkdirSync as mkdirSync8, openSync, readdirSync as readdirSync6, readFileSync as readFileSync15, rmSync as rmSync8, statSync as statSync3, writeFileSync as writeFileSync10 } from "node:fs";
|
|
36717
|
+
import { basename as basename2, join as join17 } from "node:path";
|
|
36563
36718
|
function runsDirectory() {
|
|
36564
|
-
return
|
|
36719
|
+
return join17(runtimeDirectory(), "runs");
|
|
36565
36720
|
}
|
|
36566
36721
|
function recordPath() {
|
|
36567
|
-
return
|
|
36722
|
+
return join17(runsDirectory(), "run.json");
|
|
36568
36723
|
}
|
|
36569
36724
|
function recordPaths() {
|
|
36570
|
-
return runtimeDirectories().map((directory) =>
|
|
36725
|
+
return runtimeDirectories().map((directory) => join17(directory, "runs", "run.json"));
|
|
36571
36726
|
}
|
|
36572
36727
|
function openTranscript(startedAt) {
|
|
36573
|
-
|
|
36574
|
-
const path =
|
|
36728
|
+
mkdirSync8(runsDirectory(), { recursive: true });
|
|
36729
|
+
const path = join17(runsDirectory(), `run-${startedAt}.log`);
|
|
36575
36730
|
return { path, fd: openSync(path, "a") };
|
|
36576
36731
|
}
|
|
36577
36732
|
function writeRunRecord(record) {
|
|
36578
|
-
|
|
36579
|
-
|
|
36733
|
+
mkdirSync8(runsDirectory(), { recursive: true });
|
|
36734
|
+
writeFileSync10(recordPath(), JSON.stringify(record, null, 2), "utf8");
|
|
36580
36735
|
}
|
|
36581
36736
|
function editorNotePath() {
|
|
36582
|
-
return
|
|
36737
|
+
return join17(runsDirectory(), "editor-run.json");
|
|
36583
36738
|
}
|
|
36584
36739
|
function writeEditorRunNote(note) {
|
|
36585
|
-
|
|
36586
|
-
|
|
36740
|
+
mkdirSync8(runsDirectory(), { recursive: true });
|
|
36741
|
+
writeFileSync10(editorNotePath(), JSON.stringify(note, null, 2), "utf8");
|
|
36587
36742
|
}
|
|
36588
36743
|
function readEditorRunNote() {
|
|
36589
36744
|
for (const directory of runtimeDirectories()) {
|
|
36590
|
-
const note = editorNoteAt(
|
|
36745
|
+
const note = editorNoteAt(join17(directory, "runs", "editor-run.json"));
|
|
36591
36746
|
if (note !== null) {
|
|
36592
36747
|
return note;
|
|
36593
36748
|
}
|
|
@@ -36597,7 +36752,7 @@ function readEditorRunNote() {
|
|
|
36597
36752
|
function editorNoteAt(path) {
|
|
36598
36753
|
let parsed;
|
|
36599
36754
|
try {
|
|
36600
|
-
parsed = JSON.parse(
|
|
36755
|
+
parsed = JSON.parse(readFileSync15(path, "utf8"));
|
|
36601
36756
|
} catch {
|
|
36602
36757
|
return null;
|
|
36603
36758
|
}
|
|
@@ -36620,7 +36775,7 @@ function recordRunEnded(pid, exitCode) {
|
|
|
36620
36775
|
return;
|
|
36621
36776
|
}
|
|
36622
36777
|
try {
|
|
36623
|
-
|
|
36778
|
+
writeFileSync10(path, JSON.stringify({ ...record, exitCode }, null, 2), "utf8");
|
|
36624
36779
|
} catch {}
|
|
36625
36780
|
}
|
|
36626
36781
|
function readRunRecord() {
|
|
@@ -36635,7 +36790,7 @@ function readRunRecord() {
|
|
|
36635
36790
|
function recordAt(path) {
|
|
36636
36791
|
let parsed;
|
|
36637
36792
|
try {
|
|
36638
|
-
parsed = JSON.parse(
|
|
36793
|
+
parsed = JSON.parse(readFileSync15(path, "utf8"));
|
|
36639
36794
|
} catch {
|
|
36640
36795
|
return null;
|
|
36641
36796
|
}
|
|
@@ -36680,7 +36835,7 @@ function runningAs(pid) {
|
|
|
36680
36835
|
const began = startedAt(pid, null);
|
|
36681
36836
|
const when = began === null ? {} : { startedAt: began };
|
|
36682
36837
|
if (process.platform === "linux") {
|
|
36683
|
-
const raw =
|
|
36838
|
+
const raw = readFileSync15(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
|
|
36684
36839
|
return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
|
|
36685
36840
|
}
|
|
36686
36841
|
const args = execFileSync("ps", ["-p", String(pid), "-o", "args="], {
|
|
@@ -36800,7 +36955,7 @@ function clearRunRecord(ours = () => true) {
|
|
|
36800
36955
|
continue;
|
|
36801
36956
|
}
|
|
36802
36957
|
try {
|
|
36803
|
-
|
|
36958
|
+
rmSync8(path);
|
|
36804
36959
|
} catch {}
|
|
36805
36960
|
}
|
|
36806
36961
|
}
|
|
@@ -36824,12 +36979,12 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
36824
36979
|
if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
|
|
36825
36980
|
continue;
|
|
36826
36981
|
}
|
|
36827
|
-
const path =
|
|
36982
|
+
const path = join17(directory, entry);
|
|
36828
36983
|
if (path === keep || ageMs(path, now) < keepMs) {
|
|
36829
36984
|
continue;
|
|
36830
36985
|
}
|
|
36831
36986
|
try {
|
|
36832
|
-
|
|
36987
|
+
rmSync8(path);
|
|
36833
36988
|
} catch {}
|
|
36834
36989
|
}
|
|
36835
36990
|
}
|
|
@@ -36847,15 +37002,15 @@ import {
|
|
|
36847
37002
|
closeSync,
|
|
36848
37003
|
existsSync as existsSync14,
|
|
36849
37004
|
fstatSync,
|
|
36850
|
-
mkdirSync as
|
|
37005
|
+
mkdirSync as mkdirSync9,
|
|
36851
37006
|
mkdtempSync as mkdtempSync3,
|
|
36852
37007
|
openSync as openSync2,
|
|
36853
37008
|
readdirSync as readdirSync7,
|
|
36854
|
-
readFileSync as
|
|
37009
|
+
readFileSync as readFileSync16,
|
|
36855
37010
|
readSync
|
|
36856
37011
|
} from "node:fs";
|
|
36857
37012
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
36858
|
-
import { basename as basename3, dirname as
|
|
37013
|
+
import { basename as basename3, dirname as dirname10, join as join18, normalize as normalize3 } from "node:path";
|
|
36859
37014
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
36860
37015
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
36861
37016
|
import { promisify as promisify6 } from "node:util";
|
|
@@ -37010,7 +37165,7 @@ function aboveTheRunner(entry) {
|
|
|
37010
37165
|
}
|
|
37011
37166
|
function elsewhereIn(projectPath, asked) {
|
|
37012
37167
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
37013
|
-
if (wanted === "" || existsSync14(
|
|
37168
|
+
if (wanted === "" || existsSync14(join18(projectPath, wanted))) {
|
|
37014
37169
|
return "";
|
|
37015
37170
|
}
|
|
37016
37171
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
@@ -37024,11 +37179,11 @@ function elsewhereIn(projectPath, asked) {
|
|
|
37024
37179
|
return near.length === 0 ? "" : ` The project does have ${there}.`;
|
|
37025
37180
|
}
|
|
37026
37181
|
function hasMainScene(projectFile) {
|
|
37027
|
-
const scene = parseProjectGodot(
|
|
37182
|
+
const scene = parseProjectGodot(readFileSync16(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
37028
37183
|
return typeof scene === "string" && scene !== "";
|
|
37029
37184
|
}
|
|
37030
37185
|
function editorPlaysHeadless(projectFile) {
|
|
37031
|
-
const runArgs = parseProjectGodot(
|
|
37186
|
+
const runArgs = parseProjectGodot(readFileSync16(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
37032
37187
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
37033
37188
|
}
|
|
37034
37189
|
function camelCased(params) {
|
|
@@ -37044,7 +37199,7 @@ function camelCased(params) {
|
|
|
37044
37199
|
class GodotServer {
|
|
37045
37200
|
mcp;
|
|
37046
37201
|
locator = new GodotLocator2;
|
|
37047
|
-
operationsScript =
|
|
37202
|
+
operationsScript = join18(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
37048
37203
|
godotBridge;
|
|
37049
37204
|
tools = buildToolDefinitions();
|
|
37050
37205
|
activeProcess = null;
|
|
@@ -37052,6 +37207,8 @@ class GodotServer {
|
|
|
37052
37207
|
noticedUpdate = false;
|
|
37053
37208
|
callsSinceNotice = 0;
|
|
37054
37209
|
callsSinceFeedback = 0;
|
|
37210
|
+
defectsSeen = new DefectsSeen;
|
|
37211
|
+
lastGodotVersion = null;
|
|
37055
37212
|
lspClient = null;
|
|
37056
37213
|
dapClient = null;
|
|
37057
37214
|
bridgeStartupError = null;
|
|
@@ -37096,11 +37253,11 @@ class GodotServer {
|
|
|
37096
37253
|
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
37097
37254
|
}
|
|
37098
37255
|
async run() {
|
|
37099
|
-
const
|
|
37100
|
-
if (
|
|
37101
|
-
console.error(
|
|
37256
|
+
const located = await this.locator.find();
|
|
37257
|
+
if (!located.ok) {
|
|
37258
|
+
console.error(`[SERVER] ${GodotLocator2.refusal(located).message} Until that is fixed every tool that runs the engine will say so.`);
|
|
37102
37259
|
} else {
|
|
37103
|
-
console.error(`[SERVER] Using Godot at: ${
|
|
37260
|
+
console.error(`[SERVER] Using Godot at: ${located.path}`);
|
|
37104
37261
|
}
|
|
37105
37262
|
const transport = new StdioServerTransport;
|
|
37106
37263
|
await this.mcp.server.connect(transport);
|
|
@@ -37300,7 +37457,19 @@ class GodotServer {
|
|
|
37300
37457
|
}
|
|
37301
37458
|
const where = op === "" ? tool : `${tool} op=${op}`;
|
|
37302
37459
|
console.error(`[SERVER] Unmodelled failure in ${where}:`, error);
|
|
37303
|
-
|
|
37460
|
+
const seenBefore = this.defectsSeen.before(where, errorMessage(error));
|
|
37461
|
+
return {
|
|
37462
|
+
content: [
|
|
37463
|
+
{
|
|
37464
|
+
type: "text",
|
|
37465
|
+
text: defectReport2(where, error, {
|
|
37466
|
+
seenBefore,
|
|
37467
|
+
godotVersion: this.lastGodotVersion ?? undefined
|
|
37468
|
+
})
|
|
37469
|
+
}
|
|
37470
|
+
],
|
|
37471
|
+
isError: true
|
|
37472
|
+
};
|
|
37304
37473
|
}
|
|
37305
37474
|
}
|
|
37306
37475
|
withUpdateNotice(answer) {
|
|
@@ -37661,21 +37830,19 @@ class GodotServer {
|
|
|
37661
37830
|
}
|
|
37662
37831
|
}
|
|
37663
37832
|
async engine() {
|
|
37664
|
-
const
|
|
37665
|
-
if (
|
|
37666
|
-
|
|
37667
|
-
|
|
37668
|
-
response: this.createErrorResponse("No Godot executable found.", GodotLocator2.ADVICE)
|
|
37669
|
-
};
|
|
37833
|
+
const located = await this.locator.find();
|
|
37834
|
+
if (!located.ok) {
|
|
37835
|
+
const refusal = GodotLocator2.refusal(located);
|
|
37836
|
+
return { ok: false, response: this.createErrorResponse(refusal.message, [...refusal.advice]) };
|
|
37670
37837
|
}
|
|
37671
|
-
return { ok: true, value:
|
|
37838
|
+
return { ok: true, value: located.path };
|
|
37672
37839
|
}
|
|
37673
37840
|
project(args) {
|
|
37674
37841
|
const path = readNonEmptyString(args, "projectPath");
|
|
37675
37842
|
if (path === undefined) {
|
|
37676
37843
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
37677
37844
|
}
|
|
37678
|
-
const file =
|
|
37845
|
+
const file = join18(path, "project.godot");
|
|
37679
37846
|
if (!existsSync14(file)) {
|
|
37680
37847
|
return {
|
|
37681
37848
|
ok: false,
|
|
@@ -37817,7 +37984,7 @@ class GodotServer {
|
|
|
37817
37984
|
}
|
|
37818
37985
|
const engine = await this.engine();
|
|
37819
37986
|
if (!engine.ok) {
|
|
37820
|
-
return
|
|
37987
|
+
return engine.response;
|
|
37821
37988
|
}
|
|
37822
37989
|
const projectPath = project.value.path;
|
|
37823
37990
|
const before = scriptsWithoutUid(projectPath);
|
|
@@ -37840,7 +38007,11 @@ class GodotServer {
|
|
|
37840
38007
|
async operation(operation, params, projectPath) {
|
|
37841
38008
|
const engine = await this.engine();
|
|
37842
38009
|
if (!engine.ok) {
|
|
37843
|
-
return {
|
|
38010
|
+
return {
|
|
38011
|
+
ok: false,
|
|
38012
|
+
message: engine.response.content.map((block) => block.text).join(" "),
|
|
38013
|
+
messages: []
|
|
38014
|
+
};
|
|
37844
38015
|
}
|
|
37845
38016
|
this.logDebug(`Running ${operation} in ${projectPath}: ${JSON.stringify(params)}`);
|
|
37846
38017
|
return await runOperation2({ godotPath: engine.value, script: this.operationsScript, debug: GODOT_DEBUG_MODE_DEFAULT }, operation, params, projectPath);
|
|
@@ -37872,7 +38043,7 @@ class GodotServer {
|
|
|
37872
38043
|
if (!engine.ok) {
|
|
37873
38044
|
return engine.response;
|
|
37874
38045
|
}
|
|
37875
|
-
const application = parseProjectGodot(
|
|
38046
|
+
const application = parseProjectGodot(readFileSync16(project.value.file, "utf8"))["application"] ?? {};
|
|
37876
38047
|
const name = application["config/name"];
|
|
37877
38048
|
const mainScene = application["run/main_scene"];
|
|
37878
38049
|
const info = {
|
|
@@ -37933,18 +38104,18 @@ class GodotServer {
|
|
|
37933
38104
|
if (!engine.ok) {
|
|
37934
38105
|
return engine.response;
|
|
37935
38106
|
}
|
|
37936
|
-
const folder =
|
|
38107
|
+
const folder = dirname10(output.absolutePath);
|
|
37937
38108
|
try {
|
|
37938
|
-
|
|
38109
|
+
mkdirSync9(folder, { recursive: true });
|
|
37939
38110
|
} catch (error) {
|
|
37940
|
-
return this.createErrorResponse(`Cannot create ${
|
|
38111
|
+
return this.createErrorResponse(`Cannot create ${dirname10(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
37941
38112
|
}
|
|
37942
38113
|
const debug = readBoolean(args, "debug") ?? false;
|
|
37943
|
-
const exportLogs = mkdtempSync3(
|
|
38114
|
+
const exportLogs = mkdtempSync3(join18(tmpdir5(), "gdharness-export-"));
|
|
37944
38115
|
const exportArgs = [
|
|
37945
38116
|
"--headless",
|
|
37946
38117
|
"--log-file",
|
|
37947
|
-
|
|
38118
|
+
join18(exportLogs, "engine.log"),
|
|
37948
38119
|
"--path",
|
|
37949
38120
|
project.value.path,
|
|
37950
38121
|
debug ? "--export-debug" : "--export-release",
|
|
@@ -38002,7 +38173,7 @@ class GodotServer {
|
|
|
38002
38173
|
return contained.response;
|
|
38003
38174
|
}
|
|
38004
38175
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
38005
|
-
if (!existsSync14(
|
|
38176
|
+
if (!existsSync14(join18(project.value.path, runner))) {
|
|
38006
38177
|
return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
|
|
38007
38178
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
38008
38179
|
]);
|
|
@@ -38037,7 +38208,7 @@ class GodotServer {
|
|
|
38037
38208
|
];
|
|
38038
38209
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
|
|
38039
38210
|
this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
38040
|
-
const userData = mkdtempSync3(
|
|
38211
|
+
const userData = mkdtempSync3(join18(tmpdir5(), "gdharness-tests-"));
|
|
38041
38212
|
const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
|
|
38042
38213
|
const hung = await new Promise((resolve) => {
|
|
38043
38214
|
const timer = setTimeout(() => {
|
|
@@ -38053,14 +38224,14 @@ class GodotServer {
|
|
|
38053
38224
|
resolve(false);
|
|
38054
38225
|
});
|
|
38055
38226
|
});
|
|
38056
|
-
const reportsDir =
|
|
38227
|
+
const reportsDir = join18(project.value.path, ".godot", ...ours.split("/"));
|
|
38057
38228
|
let report = null;
|
|
38058
38229
|
let reportProblem = null;
|
|
38059
38230
|
try {
|
|
38060
38231
|
const written = existsSync14(reportsDir) ? readdirSync7(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
38061
38232
|
const newest = written.at(-1);
|
|
38062
38233
|
if (newest !== undefined) {
|
|
38063
|
-
report = parseJUnit(
|
|
38234
|
+
report = parseJUnit(readFileSync16(join18(reportsDir, newest, "results.xml"), "utf8"));
|
|
38064
38235
|
}
|
|
38065
38236
|
} catch (error) {
|
|
38066
38237
|
reportProblem = errorMessage(error);
|
|
@@ -38081,6 +38252,7 @@ class GodotServer {
|
|
|
38081
38252
|
const said = printed.map((entry) => entry.text);
|
|
38082
38253
|
const nothingRan = hung ? null : whyNoReport(said, asked);
|
|
38083
38254
|
const verdict = hung ? `hung: killed after ${timeoutMs} ms` : nothingRan ?? verdicts[exitCode ?? -1] ?? `exit ${exitCode ?? "unknown"}`;
|
|
38255
|
+
const savesNote = savesStayPut() && nothingRan === null ? SAVES_NOT_MOVED_NOTE : undefined;
|
|
38084
38256
|
if (report === null) {
|
|
38085
38257
|
const note = nothingRan === null ? `The test run wrote no report (${verdict}${reportProblem ? `; ${reportProblem}` : ""}).` : `No tests ran: ${verdict}.${elsewhereIn(project.value.path, asked)}`;
|
|
38086
38258
|
return {
|
|
@@ -38095,7 +38267,8 @@ class GodotServer {
|
|
|
38095
38267
|
exitCode,
|
|
38096
38268
|
hung,
|
|
38097
38269
|
arguments: cmdArgs,
|
|
38098
|
-
entries: forAnswer(printed.slice(0, 60))
|
|
38270
|
+
entries: forAnswer(printed.slice(0, 60)),
|
|
38271
|
+
savesNote
|
|
38099
38272
|
}, null, 2)
|
|
38100
38273
|
}
|
|
38101
38274
|
],
|
|
@@ -38128,6 +38301,7 @@ class GodotServer {
|
|
|
38128
38301
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
38129
38302
|
orphans: orphans.total > 0 ? orphans.total : undefined,
|
|
38130
38303
|
notRun: notRun > 0 ? notRun : undefined,
|
|
38304
|
+
savesNote,
|
|
38131
38305
|
note: verdict.startsWith("warnings") && warnings.length === 0 ? "gdUnit4 exits 101 for orphan nodes when nothing failed, and this run printed no count of them: orphan reporting may be off in the project settings." : notRun > 0 ? `This run stopped at the first failure in each suite it failed in, so ${notRun} case${notRun === 1 ? "" : "s"} never ran and count as neither passed nor failed. Leave failFast out to run every case.` : undefined,
|
|
38132
38306
|
suites: unclean.map((suite) => ({
|
|
38133
38307
|
name: suite.name,
|
|
@@ -38150,7 +38324,8 @@ class GodotServer {
|
|
|
38150
38324
|
async godotVersion(godotPath) {
|
|
38151
38325
|
try {
|
|
38152
38326
|
const { stdout } = await run6(godotPath, ["--version"], { timeout: 1e4 });
|
|
38153
|
-
|
|
38327
|
+
this.lastGodotVersion = stdout.trim();
|
|
38328
|
+
return this.lastGodotVersion;
|
|
38154
38329
|
} catch (error) {
|
|
38155
38330
|
this.logDebug(`Godot did not answer --version: ${errorMessage(error)}`);
|
|
38156
38331
|
return null;
|
|
@@ -38196,7 +38371,7 @@ class GodotServer {
|
|
|
38196
38371
|
return null;
|
|
38197
38372
|
}
|
|
38198
38373
|
try {
|
|
38199
|
-
return
|
|
38374
|
+
return readFileSync16(contained.absolutePath, "utf8");
|
|
38200
38375
|
} catch {
|
|
38201
38376
|
return null;
|
|
38202
38377
|
}
|
|
@@ -38238,7 +38413,7 @@ class GodotServer {
|
|
|
38238
38413
|
return null;
|
|
38239
38414
|
}
|
|
38240
38415
|
try {
|
|
38241
|
-
return
|
|
38416
|
+
return readFileSync16(contained.absolutePath, "utf8");
|
|
38242
38417
|
} catch {
|
|
38243
38418
|
return null;
|
|
38244
38419
|
}
|
|
@@ -38308,16 +38483,37 @@ class GodotServer {
|
|
|
38308
38483
|
const status = this.godotBridge.getStatus();
|
|
38309
38484
|
return (status.connected ? status[field] : undefined) ?? fallback;
|
|
38310
38485
|
}
|
|
38486
|
+
restartLeftUnfinished(connected) {
|
|
38487
|
+
if (this.ownProject === null) {
|
|
38488
|
+
return null;
|
|
38489
|
+
}
|
|
38490
|
+
const owed = restartOwed(this.ownProject);
|
|
38491
|
+
if (owed === null) {
|
|
38492
|
+
return null;
|
|
38493
|
+
}
|
|
38494
|
+
if (connected) {
|
|
38495
|
+
restartSettled(this.ownProject);
|
|
38496
|
+
return null;
|
|
38497
|
+
}
|
|
38498
|
+
return owed;
|
|
38499
|
+
}
|
|
38311
38500
|
getEditorStatusPayload() {
|
|
38312
38501
|
const status = this.godotBridge.getStatus();
|
|
38313
38502
|
const isPortConflict = this.bridgeStartupError?.includes("EADDRINUSE") ?? false;
|
|
38314
38503
|
const stale = status.connected && status.addonVersion !== SERVER_VERSION;
|
|
38504
|
+
const unfinished = this.restartLeftUnfinished(status.connected);
|
|
38315
38505
|
return {
|
|
38316
38506
|
...status,
|
|
38317
38507
|
serverVersion: SERVER_VERSION,
|
|
38318
38508
|
addonIsStale: status.connected ? stale : undefined,
|
|
38319
38509
|
bridgeAvailable: this.bridgeStartupError === null,
|
|
38320
|
-
mayYetConnect: status.connected ? undefined : this.anEditorIsStillComing(status.listeningSince),
|
|
38510
|
+
mayYetConnect: status.connected ? undefined : unfinished !== null ? false : this.anEditorIsStillComing(status.listeningSince),
|
|
38511
|
+
restartInterrupted: unfinished === null ? undefined : {
|
|
38512
|
+
quitEditorPid: unfinished.editorPid,
|
|
38513
|
+
quitAt: unfinished.quitAt,
|
|
38514
|
+
byServerPid: unfinished.byPid,
|
|
38515
|
+
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."
|
|
38516
|
+
},
|
|
38321
38517
|
awaitingLaunchedEditor: status.connected || !this.launchedEditorIsUp() ? undefined : this.launchedEditor?.pid ?? undefined,
|
|
38322
38518
|
...status.connected ? this.whatTheLaunchedEditorDropped() : {},
|
|
38323
38519
|
startupError: this.bridgeStartupError,
|
|
@@ -38353,7 +38549,7 @@ class GodotServer {
|
|
|
38353
38549
|
}
|
|
38354
38550
|
}
|
|
38355
38551
|
async handleEditorStatus() {
|
|
38356
|
-
const
|
|
38552
|
+
const located = await this.locator.find();
|
|
38357
38553
|
const announced = runtimesAnnounced();
|
|
38358
38554
|
const games = await Promise.all(announced.running.map(async (endpoint) => {
|
|
38359
38555
|
const reply = await runtimeRequest(endpoint, "ping", {}, this.runtimeTimeoutMs());
|
|
@@ -38373,8 +38569,9 @@ class GodotServer {
|
|
|
38373
38569
|
debugPort: playing?.debugPort ?? this.godotBridge.getStatus().debugPort
|
|
38374
38570
|
},
|
|
38375
38571
|
godot: {
|
|
38376
|
-
path:
|
|
38377
|
-
version:
|
|
38572
|
+
path: located.ok ? located.path : null,
|
|
38573
|
+
version: located.ok ? await this.godotVersion(located.path) : null,
|
|
38574
|
+
problem: located.ok ? undefined : GodotLocator2.refusal(located).message
|
|
38378
38575
|
},
|
|
38379
38576
|
game: {
|
|
38380
38577
|
processActive: runIsUp(this.currentRun(), playing === null ? null : playing.playing),
|
|
@@ -38393,12 +38590,12 @@ class GodotServer {
|
|
|
38393
38590
|
}
|
|
38394
38591
|
settingKeysOf(projectPath) {
|
|
38395
38592
|
if (projectPath === undefined) {
|
|
38396
|
-
return
|
|
38593
|
+
return null;
|
|
38397
38594
|
}
|
|
38398
38595
|
try {
|
|
38399
|
-
return settingKeys(
|
|
38596
|
+
return settingKeys(readFileSync16(join18(projectPath, "project.godot"), "utf8"));
|
|
38400
38597
|
} catch {
|
|
38401
|
-
return
|
|
38598
|
+
return null;
|
|
38402
38599
|
}
|
|
38403
38600
|
}
|
|
38404
38601
|
async handleRestartEditor() {
|
|
@@ -38458,12 +38655,21 @@ class GodotServer {
|
|
|
38458
38655
|
lsp: this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT),
|
|
38459
38656
|
dap: this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT)
|
|
38460
38657
|
};
|
|
38658
|
+
noteRestartBegun({
|
|
38659
|
+
projectPath,
|
|
38660
|
+
editorPid: editorPid ?? null,
|
|
38661
|
+
ports,
|
|
38662
|
+
quitAt: new Date().toISOString(),
|
|
38663
|
+
byPid: process.pid
|
|
38664
|
+
});
|
|
38461
38665
|
const asked = await this.handleViaBridge("quit_editor", {});
|
|
38462
38666
|
if (asked.isError === true) {
|
|
38667
|
+
restartSettled(projectPath);
|
|
38463
38668
|
return asked;
|
|
38464
38669
|
}
|
|
38465
38670
|
await this.waitForBridge(() => !alive(editorPid), Date.now() + EDITOR_RESTART_TIMEOUT_MS);
|
|
38466
38671
|
const opened = await this.openAnEditor(engine.value, projectPath, ports);
|
|
38672
|
+
restartSettled(projectPath);
|
|
38467
38673
|
if (opened.error !== null) {
|
|
38468
38674
|
return this.createErrorResponse(`The editor was asked to go and could not be started again: ${opened.error}`, [`editor_launch opens one on ${projectPath}`]);
|
|
38469
38675
|
}
|
|
@@ -38499,10 +38705,12 @@ class GodotServer {
|
|
|
38499
38705
|
this.logDebug(`Launching Godot editor for project: ${project.value.path}`);
|
|
38500
38706
|
const ports = await this.portsForAnEditor();
|
|
38501
38707
|
const before = this.settingKeysOf(project.value.path);
|
|
38708
|
+
const unfinished = restartOwed(project.value.path);
|
|
38502
38709
|
const opened = await this.openAnEditor(engine.value, project.value.path, ports);
|
|
38503
38710
|
if (opened.error !== null) {
|
|
38504
38711
|
return this.createErrorResponse(`Could not start the editor: ${opened.error}`);
|
|
38505
38712
|
}
|
|
38713
|
+
restartSettled(project.value.path);
|
|
38506
38714
|
this.launchedFrom = { projectPath: project.value.path, before };
|
|
38507
38715
|
return this.jsonTextResponse({
|
|
38508
38716
|
launched: true,
|
|
@@ -38510,7 +38718,8 @@ class GodotServer {
|
|
|
38510
38718
|
projectPath: project.value.path,
|
|
38511
38719
|
lspPort: ports.lsp,
|
|
38512
38720
|
dapPort: ports.dap,
|
|
38513
|
-
|
|
38721
|
+
finishesRestart: unfinished === null ? undefined : `This is the launch half of a restart begun at ${unfinished.quitAt} by server pid ${unfinished.byPid}, which quit editor pid ${unfinished.editorPid ?? "unknown"} and was ended before it could start one.`,
|
|
38722
|
+
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."
|
|
38514
38723
|
});
|
|
38515
38724
|
}
|
|
38516
38725
|
async openAnEditor(engine, projectPath, ports) {
|
|
@@ -38627,7 +38836,7 @@ class GodotServer {
|
|
|
38627
38836
|
return runIsUp(run, editorSays);
|
|
38628
38837
|
}
|
|
38629
38838
|
async runtimeUp(projectPath, before, budgetMs, withArgs) {
|
|
38630
|
-
if (!existsSync14(
|
|
38839
|
+
if (!existsSync14(join18(projectPath, RUNTIME_AUTOLOAD2.path))) {
|
|
38631
38840
|
return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
|
|
38632
38841
|
}
|
|
38633
38842
|
const endpoint = await announcedSince(projectPath, before, {
|
|
@@ -39295,8 +39504,8 @@ class GodotServer {
|
|
|
39295
39504
|
return this.createErrorResponse(choice.problem);
|
|
39296
39505
|
}
|
|
39297
39506
|
const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
|
|
39298
|
-
const screenshotDir = expectsScreenshot ? mkdtempSync3(
|
|
39299
|
-
const screenshotPath = screenshotDir ?
|
|
39507
|
+
const screenshotDir = expectsScreenshot ? mkdtempSync3(join18(tmpdir5(), "gdharness-runtime-screenshot-")) : null;
|
|
39508
|
+
const screenshotPath = screenshotDir ? join18(screenshotDir, "capture.png") : null;
|
|
39300
39509
|
try {
|
|
39301
39510
|
const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs);
|
|
39302
39511
|
if (!reply.ok) {
|
|
@@ -39317,7 +39526,7 @@ class GodotServer {
|
|
|
39317
39526
|
return {
|
|
39318
39527
|
content: [
|
|
39319
39528
|
{ type: "text", text: `Screenshot captured: ${dimensions}` },
|
|
39320
|
-
{ type: "image", data:
|
|
39529
|
+
{ type: "image", data: readFileSync16(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
39321
39530
|
]
|
|
39322
39531
|
};
|
|
39323
39532
|
} finally {
|
|
@@ -39406,6 +39615,7 @@ var init_server3 = __esm(() => {
|
|
|
39406
39615
|
init_process_time();
|
|
39407
39616
|
init_project_scan();
|
|
39408
39617
|
init_resources();
|
|
39618
|
+
init_restart_note();
|
|
39409
39619
|
init_run_record();
|
|
39410
39620
|
init_runtime_client();
|
|
39411
39621
|
init_scratch();
|
|
@@ -39414,7 +39624,7 @@ var init_server3 = __esm(() => {
|
|
|
39414
39624
|
init_tool_definitions();
|
|
39415
39625
|
init_update_check();
|
|
39416
39626
|
run6 = promisify6(execFile6);
|
|
39417
|
-
__dirname2 =
|
|
39627
|
+
__dirname2 = dirname10(fileURLToPath4(import.meta.url));
|
|
39418
39628
|
VALUE_NAMES = {
|
|
39419
39629
|
string: "a string",
|
|
39420
39630
|
number: "a number",
|
|
@@ -39453,7 +39663,7 @@ var init_server3 = __esm(() => {
|
|
|
39453
39663
|
|
|
39454
39664
|
// src/cli.ts
|
|
39455
39665
|
import { existsSync as existsSync15 } from "fs";
|
|
39456
|
-
import { join as
|
|
39666
|
+
import { join as join19, resolve as resolve5 } from "path";
|
|
39457
39667
|
|
|
39458
39668
|
// src/errors.ts
|
|
39459
39669
|
class Refusal extends Error {
|
|
@@ -39461,6 +39671,7 @@ class Refusal extends Error {
|
|
|
39461
39671
|
|
|
39462
39672
|
// src/godot-path.ts
|
|
39463
39673
|
init_detection();
|
|
39674
|
+
init_errors();
|
|
39464
39675
|
import { execFile } from "node:child_process";
|
|
39465
39676
|
import { existsSync as existsSync2 } from "node:fs";
|
|
39466
39677
|
import { normalize } from "node:path";
|
|
@@ -39471,47 +39682,65 @@ class GodotLocator {
|
|
|
39471
39682
|
answered = new Map;
|
|
39472
39683
|
found = null;
|
|
39473
39684
|
async find() {
|
|
39474
|
-
if (this.found !== null && await this.
|
|
39475
|
-
return this.found;
|
|
39685
|
+
if (this.found !== null && await this.whyNot(this.found) === null) {
|
|
39686
|
+
return { ok: true, path: this.found };
|
|
39476
39687
|
}
|
|
39477
39688
|
this.found = null;
|
|
39478
39689
|
const named = envValue("GODOT_PATH");
|
|
39479
39690
|
if (named !== undefined) {
|
|
39480
39691
|
const path = normalize(named);
|
|
39481
|
-
|
|
39692
|
+
const reason = await this.whyNot(path);
|
|
39693
|
+
if (reason === null) {
|
|
39482
39694
|
this.found = path;
|
|
39695
|
+
return { ok: true, path };
|
|
39483
39696
|
}
|
|
39484
|
-
return
|
|
39697
|
+
return { ok: false, named: path, reason };
|
|
39485
39698
|
}
|
|
39486
39699
|
for (const candidate of godotCandidates(process.platform, resolveHomeDirectory())) {
|
|
39487
39700
|
const path = normalize(candidate);
|
|
39488
|
-
if (await this.
|
|
39701
|
+
if (await this.whyNot(path) === null) {
|
|
39489
39702
|
this.found = path;
|
|
39490
|
-
return path;
|
|
39703
|
+
return { ok: true, path };
|
|
39491
39704
|
}
|
|
39492
39705
|
}
|
|
39493
|
-
return null;
|
|
39706
|
+
return { ok: false, named: null };
|
|
39494
39707
|
}
|
|
39495
|
-
static
|
|
39496
|
-
|
|
39497
|
-
|
|
39498
|
-
|
|
39499
|
-
|
|
39708
|
+
static refusal(located) {
|
|
39709
|
+
if (located.named === null) {
|
|
39710
|
+
return {
|
|
39711
|
+
message: "No Godot executable found.",
|
|
39712
|
+
advice: [
|
|
39713
|
+
"Set GODOT_PATH to the Godot 4 executable",
|
|
39714
|
+
"Or install Godot where it is usually found, such as /usr/bin/godot or C:\\Program Files\\Godot"
|
|
39715
|
+
]
|
|
39716
|
+
};
|
|
39717
|
+
}
|
|
39718
|
+
return {
|
|
39719
|
+
message: `GODOT_PATH is set to ${located.named}, which ${located.reason ?? "does not answer"}.`,
|
|
39720
|
+
advice: [
|
|
39721
|
+
"Point GODOT_PATH at a Godot 4 executable that answers --version",
|
|
39722
|
+
"Or unset it, and the usual install locations are searched instead"
|
|
39723
|
+
]
|
|
39724
|
+
};
|
|
39725
|
+
}
|
|
39726
|
+
async whyNot(path) {
|
|
39500
39727
|
const known = this.answered.get(path);
|
|
39501
39728
|
if (known !== undefined) {
|
|
39502
39729
|
return known;
|
|
39503
39730
|
}
|
|
39504
|
-
let
|
|
39505
|
-
if (path
|
|
39731
|
+
let reason;
|
|
39732
|
+
if (path !== "godot" && !existsSync2(path)) {
|
|
39733
|
+
reason = "does not exist";
|
|
39734
|
+
} else {
|
|
39506
39735
|
try {
|
|
39507
39736
|
await run(path, ["--version"]);
|
|
39508
|
-
|
|
39509
|
-
} catch {
|
|
39510
|
-
|
|
39737
|
+
reason = null;
|
|
39738
|
+
} catch (error) {
|
|
39739
|
+
reason = `does not answer --version: ${errorMessage(error)}`;
|
|
39511
39740
|
}
|
|
39512
39741
|
}
|
|
39513
|
-
this.answered.set(path,
|
|
39514
|
-
return
|
|
39742
|
+
this.answered.set(path, reason);
|
|
39743
|
+
return reason;
|
|
39515
39744
|
}
|
|
39516
39745
|
}
|
|
39517
39746
|
|
|
@@ -40291,15 +40520,27 @@ function disconnect(harness, projectPath) {
|
|
|
40291
40520
|
`, "utf8");
|
|
40292
40521
|
return { harness, path, action: "removed" };
|
|
40293
40522
|
}
|
|
40294
|
-
function harnessNote(
|
|
40295
|
-
|
|
40523
|
+
function harnessNote(repinned, now) {
|
|
40524
|
+
const alsoByHand = repinned.byHand === 0 ? "" : `
|
|
40525
|
+
${repinned.byHand === 1 ? "One harness above was" : `${repinned.byHand} harnesses above were`} handed a command to run instead; run it too.`;
|
|
40526
|
+
if (repinned.moved.length > 0) {
|
|
40527
|
+
return ` 2. Your harness is still running the server it spawned from ${repinned.moved.join(", ")}.
|
|
40528
|
+
` + ` Its config names ${now} now.
|
|
40529
|
+
` + ` Reconnect the MCP server, or restart the harness.${alsoByHand}`;
|
|
40530
|
+
}
|
|
40531
|
+
if (repinned.written > 0) {
|
|
40296
40532
|
return ` 2. Your harness config already named this version, but the server it is running is
|
|
40297
40533
|
` + ` whichever one it spawned when the session started, which this cannot see from here.
|
|
40298
|
-
` +
|
|
40534
|
+
` + ` Reconnect the MCP server, or restart the harness.${alsoByHand}`;
|
|
40299
40535
|
}
|
|
40300
|
-
|
|
40301
|
-
`
|
|
40302
|
-
` +
|
|
40536
|
+
if (repinned.byHand > 0) {
|
|
40537
|
+
return ` 2. None of the harness configs found here can be written for you, so nothing was
|
|
40538
|
+
` + ` re-pinned and a reconnect on its own would come back on the same server.
|
|
40539
|
+
` + " Run what is printed above, then reconnect the MCP server or restart the harness.";
|
|
40540
|
+
}
|
|
40541
|
+
return ` 2. No harness config here names gdharness, so nothing was re-pinned and there is no
|
|
40542
|
+
` + ` config for a reconnect to read. Run gdharness setup to write one, or point your
|
|
40543
|
+
` + ` harness at ${now} yourself, then restart it.`;
|
|
40303
40544
|
}
|
|
40304
40545
|
function launchedBy(entry) {
|
|
40305
40546
|
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
@@ -40515,15 +40756,26 @@ function filledTemplate(where, message, godotVersion) {
|
|
|
40515
40756
|
].join(`
|
|
40516
40757
|
`);
|
|
40517
40758
|
}
|
|
40518
|
-
function defectReport(where, error,
|
|
40759
|
+
function defectReport(where, error, context = {}) {
|
|
40760
|
+
const { godotVersion, seenBefore = 0 } = context;
|
|
40519
40761
|
const message = errorMessage(error);
|
|
40520
40762
|
const title = `Defect ${defectSignature(where, message)}: ${where}`;
|
|
40521
40763
|
const url = `${NEW_ISSUE}?template=bug_report.md&title=${encodeURIComponent(title)}` + `&body=${encodeURIComponent(filledTemplate(where, message, godotVersion))}`;
|
|
40522
40764
|
const width = Math.max(...facts(where, message, godotVersion).map(([label]) => (label ?? "").length));
|
|
40523
40765
|
return [
|
|
40524
|
-
"gdharness failed in a way it does not model.
|
|
40525
|
-
"
|
|
40526
|
-
"
|
|
40766
|
+
"gdharness failed in a way it does not model. Whatever went wrong underneath, it reached you",
|
|
40767
|
+
"as a crash rather than as a refusal naming what would have worked, and that much is a defect",
|
|
40768
|
+
"in gdharness however the rest of it turns out.",
|
|
40769
|
+
"",
|
|
40770
|
+
...seenBefore === 0 ? [
|
|
40771
|
+
"Send the call once more before anything else. Some of what reaches this line is a file",
|
|
40772
|
+
"another process had open, or a connection that dropped, and those are gone by the next",
|
|
40773
|
+
"attempt. If the same Signature comes back, it is not one of those, and repeating it",
|
|
40774
|
+
"further only spends turns."
|
|
40775
|
+
] : [
|
|
40776
|
+
`You have had this exact failure ${seenBefore === 1 ? "once" : `${seenBefore} times`} in this session already, so it is a`,
|
|
40777
|
+
"state rather than a passing one, and sending the call again will not change it."
|
|
40778
|
+
],
|
|
40527
40779
|
"",
|
|
40528
40780
|
...facts(where, message, godotVersion).map(([label, value]) => ` ${(label ?? "").padEnd(width)} ${value ?? ""}`),
|
|
40529
40781
|
"",
|
|
@@ -40660,14 +40912,14 @@ function loaderAmong(projectPath, named) {
|
|
|
40660
40912
|
if (entry === RUNTIME_AUTOLOAD.name || path === RUNTIME_AUTOLOAD.path) {
|
|
40661
40913
|
continue;
|
|
40662
40914
|
}
|
|
40663
|
-
let source =
|
|
40915
|
+
let source = null;
|
|
40664
40916
|
try {
|
|
40665
40917
|
source = readFileSync7(join8(projectPath, path), "utf8");
|
|
40666
40918
|
} catch {}
|
|
40667
|
-
if (source
|
|
40668
|
-
return { name: entry, path };
|
|
40919
|
+
if (source?.includes(RUNTIME_AUTOLOAD.path)) {
|
|
40920
|
+
return { name: entry, path, exists: true };
|
|
40669
40921
|
}
|
|
40670
|
-
byName ??= path.toLowerCase().includes("gdharness") ? { name: entry, path } : null;
|
|
40922
|
+
byName ??= path.toLowerCase().includes("gdharness") ? { name: entry, path, exists: source !== null } : null;
|
|
40671
40923
|
}
|
|
40672
40924
|
return byName;
|
|
40673
40925
|
}
|
|
@@ -40730,8 +40982,15 @@ function inspectProject(projectPath) {
|
|
|
40730
40982
|
const runtimeLoaderAutoload = loaderAmong(projectPath, named);
|
|
40731
40983
|
const runtimeAutoload = runtimeAutoloadPath !== null || runtimeLoaderAutoload !== null;
|
|
40732
40984
|
const tracked = trackedByGit(projectPath, [...named.values()]);
|
|
40985
|
+
const addonMissing = (path) => addons.some((addon) => !addon.installed && path.startsWith(`addons/${addon.name}/`));
|
|
40733
40986
|
for (const [name, path] of named) {
|
|
40734
|
-
if (
|
|
40987
|
+
if (!existsSync7(join8(projectPath, path))) {
|
|
40988
|
+
if (!addonMissing(path)) {
|
|
40989
|
+
problems.push(`the ${name} autoload names res://${path}, which is not in this project, so it boots with a missing script; restore that file, or point the autoload at one that is here`);
|
|
40990
|
+
}
|
|
40991
|
+
continue;
|
|
40992
|
+
}
|
|
40993
|
+
if (tracked !== null && !tracked.has(path)) {
|
|
40735
40994
|
problems.push(`the ${name} autoload names res://${path}, which git does not carry, so a clone boots with a missing script; commit that file, or point the autoload at one the project tracks`);
|
|
40736
40995
|
}
|
|
40737
40996
|
}
|
|
@@ -40906,10 +41165,12 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
40906
41165
|
| --- | --- |
|
|
40907
41166
|
| What a screen says | \`runtime_inspect text\`, which reads what is drawn and leaves out what is hidden. Read \`omitted\`: a long list above a dialog pushes it past the limit, and what comes back then is the top of the screen rather than the screen |
|
|
40908
41167
|
| Find a control by the word on it | \`runtime_inspect find\` with \`says\`, rather than listing a screen and reading each one |
|
|
41168
|
+
| What a control is showing | \`runtime_inspect find\` with \`property\` as well: the value comes back on each match in the same answer, so the game cannot tick between finding the node and reading it. A \`find\` followed by a \`property\` read of the path it answered is two calls with a frame between them, and a panel that rebuilds itself answers the second with "Node not found" |
|
|
40909
41169
|
| Where a control is, and whether it is visible | \`runtime_inspect\` \`find\`, \`rect\` |
|
|
40910
|
-
| What a property reads right now | \`runtime_inspect\` \`property\`, \`runtime_invoke\` |
|
|
41170
|
+
| What a property reads right now, on a node whose path you already hold | \`runtime_inspect\` \`property\`, \`runtime_invoke\` |
|
|
40911
41171
|
| What a property on an object a node holds reads | the same two, with colons: \`_game:clock:speed\` |
|
|
40912
41172
|
| Press a button | \`runtime_input click\`, which says what was under the pointer |
|
|
41173
|
+
| Press the next thing after a click that changed screen | \`runtime_wait until\` with \`says\` and a word the new screen shows, 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 |
|
|
40913
41174
|
| Fill in a field | \`runtime_input click\` on it, then \`runtime_input text\` with \`replace\`; a submitted field has to be clicked again |
|
|
40914
41175
|
| Click somebody standing in a 3D room | \`runtime_input click\` on the Node3D, which aims at what it draws |
|
|
40915
41176
|
| Pick something out of a dropdown | \`runtime_input choose\` on it, by what the item says |
|
|
@@ -40965,8 +41226,9 @@ its own copy. So a long run is not a reason to stop editing, and the new code ar
|
|
|
40965
41226
|
A wall of "Could not find type" on the project's own classes means the editor is not holding
|
|
40966
41227
|
them. \`editor_rescan\` asks it to scan and names any class it still cannot resolve under
|
|
40967
41228
|
\`unseenByEditor\`: its walk skips a file a headless engine has already imported, so the
|
|
40968
|
-
declaration and the cache can both be right while the editor stays blind to it.
|
|
40969
|
-
|
|
41229
|
+
declaration and the cache can both be right while the editor stays blind to it. Rescan again on
|
|
41230
|
+
its own, which is the measured cure and needs no change to the declaring script; \`editor_launch
|
|
41231
|
+
restart\` also does it and costs a window.
|
|
40970
41232
|
|
|
40971
41233
|
The scan writes \`.godot/global_script_class_cache.cfg\` from the list the editor is holding, so
|
|
40972
41234
|
a class it cannot resolve is dropped out of that file too, and the next fresh engine, CI run or
|
|
@@ -41089,17 +41351,22 @@ class UsageError extends Refusal {
|
|
|
41089
41351
|
function projectArgument(at) {
|
|
41090
41352
|
const named = args.slice(at).find((value) => !value.startsWith("--"));
|
|
41091
41353
|
const projectPath = resolve5(named ?? ".");
|
|
41092
|
-
if (!existsSync15(
|
|
41354
|
+
if (!existsSync15(join19(projectPath, "project.godot"))) {
|
|
41093
41355
|
throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
|
|
41094
41356
|
}
|
|
41095
41357
|
return projectPath;
|
|
41096
41358
|
}
|
|
41097
41359
|
async function engine(projectPath) {
|
|
41098
|
-
const
|
|
41099
|
-
if (
|
|
41100
|
-
|
|
41360
|
+
const located = await new GodotLocator().find();
|
|
41361
|
+
if (located.ok) {
|
|
41362
|
+
return { godotPath: located.path, script: shippedOperationsScript(), debug: GODOT_DEBUG_MODE_DEFAULT2 };
|
|
41101
41363
|
}
|
|
41102
|
-
|
|
41364
|
+
const recorded = located.named === null && projectPath !== undefined ? recordedEnginePath(projectPath) : null;
|
|
41365
|
+
if (recorded === null) {
|
|
41366
|
+
const refusal = GodotLocator.refusal(located);
|
|
41367
|
+
throw new UsageError(`${refusal.message} ${refusal.advice.join(". ")}.`);
|
|
41368
|
+
}
|
|
41369
|
+
return { godotPath: recorded, script: shippedOperationsScript(), debug: GODOT_DEBUG_MODE_DEFAULT2 };
|
|
41103
41370
|
}
|
|
41104
41371
|
function said2(outcome, what) {
|
|
41105
41372
|
if (!outcome.ok) {
|
|
@@ -41123,7 +41390,7 @@ async function registerRuntime(godot, projectPath) {
|
|
|
41123
41390
|
return;
|
|
41124
41391
|
}
|
|
41125
41392
|
console.log(`${RUNTIME_AUTOLOAD.name} autoload left at ${named}, which this project registers itself`);
|
|
41126
|
-
if (!existsSync15(
|
|
41393
|
+
if (!existsSync15(join19(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
|
|
41127
41394
|
console.log(` that file is not in this project, so it boots with a missing script: restore it, or point the entry at res://${RUNTIME_AUTOLOAD.path}`);
|
|
41128
41395
|
}
|
|
41129
41396
|
}
|
|
@@ -41181,7 +41448,7 @@ function reportConnection(group, launch, projectPath) {
|
|
|
41181
41448
|
const written = connect(group.writer, projectPath, launch);
|
|
41182
41449
|
if (written.action === "command") {
|
|
41183
41450
|
console.log(`${who}: run ${(written.command ?? []).join(" ")}`);
|
|
41184
|
-
return;
|
|
41451
|
+
return { kind: "byHand" };
|
|
41185
41452
|
}
|
|
41186
41453
|
if (written.action === "snippet") {
|
|
41187
41454
|
console.log(`${who}: put this in ${written.path}`);
|
|
@@ -41189,7 +41456,7 @@ function reportConnection(group, launch, projectPath) {
|
|
|
41189
41456
|
`)) {
|
|
41190
41457
|
console.log(` ${line}`);
|
|
41191
41458
|
}
|
|
41192
|
-
return;
|
|
41459
|
+
return { kind: "byHand" };
|
|
41193
41460
|
}
|
|
41194
41461
|
console.log(`${who}: ${written.action} ${written.path}`);
|
|
41195
41462
|
if (written.wasLaunchedBy !== undefined) {
|
|
@@ -41201,7 +41468,7 @@ function reportConnection(group, launch, projectPath) {
|
|
|
41201
41468
|
console.log(` ${harness.name}: ${harness.manual}`);
|
|
41202
41469
|
}
|
|
41203
41470
|
}
|
|
41204
|
-
return written.wasLaunchedBy;
|
|
41471
|
+
return { kind: "written", from: written.wasLaunchedBy ?? null };
|
|
41205
41472
|
}
|
|
41206
41473
|
function nextSteps(projectPath, runtime) {
|
|
41207
41474
|
console.log(`
|
|
@@ -41335,14 +41602,18 @@ async function upgrade() {
|
|
|
41335
41602
|
const launch = launchFor(version, godot.godotPath, projectPath);
|
|
41336
41603
|
const already = HARNESSES.filter((harness) => registered(harness, projectPath));
|
|
41337
41604
|
const moved = [];
|
|
41605
|
+
let written = 0;
|
|
41606
|
+
let byHand = 0;
|
|
41338
41607
|
for (const group of groupByFile(already, projectPath)) {
|
|
41339
|
-
const
|
|
41340
|
-
if (
|
|
41341
|
-
|
|
41608
|
+
const took = reportConnection(group, launch, projectPath);
|
|
41609
|
+
if (took.kind === "byHand") {
|
|
41610
|
+
byHand += 1;
|
|
41611
|
+
continue;
|
|
41612
|
+
}
|
|
41613
|
+
written += 1;
|
|
41614
|
+
if (took.from !== null && !moved.includes(took.from)) {
|
|
41615
|
+
moved.push(took.from);
|
|
41342
41616
|
}
|
|
41343
|
-
}
|
|
41344
|
-
if (already.length === 0) {
|
|
41345
|
-
console.log("no harness config names gdharness, so none was re-pinned");
|
|
41346
41617
|
}
|
|
41347
41618
|
for (const written of writeSkill(skillDirectories(already, projectPath, existsSync15), version)) {
|
|
41348
41619
|
console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
|
|
@@ -41351,7 +41622,7 @@ async function upgrade() {
|
|
|
41351
41622
|
On ${version}. Two things this could not do for you:`);
|
|
41352
41623
|
console.log(` 1. The open editor is still running the addons it loaded at startup. Restart it with the
|
|
41353
41624
|
` + " editor_launch restart tool, which closes and reopens the window.");
|
|
41354
|
-
console.log(harnessNote(moved, [launch.command, ...launch.args].join(" ")));
|
|
41625
|
+
console.log(harnessNote({ moved, written, byHand }, [launch.command, ...launch.args].join(" ")));
|
|
41355
41626
|
}
|
|
41356
41627
|
function doctorReport(projectPath) {
|
|
41357
41628
|
const report = inspectProject(projectPath);
|
|
@@ -41362,7 +41633,8 @@ function doctorReport(projectPath) {
|
|
|
41362
41633
|
console.log(`addons/${addon.name}: ${addon.installed ? `installed, ${addon.version ?? "unknown version"}${addon.current ? "" : " (not this version)"}` : "missing"}`);
|
|
41363
41634
|
}
|
|
41364
41635
|
console.log(`editor plugins enabled: ${report.pluginsEnabled.join(", ") || "none"}`);
|
|
41365
|
-
|
|
41636
|
+
const loader = report.runtimeLoaderAutoload;
|
|
41637
|
+
console.log(`runtime autoload: ${loader === null ? report.runtimeAutoload ? "registered" : "not registered" : `registered through res://${loader.path}, as ${loader.name}${loader.exists ? "" : ", and that file is not in this project"}`}`);
|
|
41366
41638
|
console.log(report.classCacheExists ? `class cache: ${report.staleClasses.length === 0 ? "current" : `stale for ${report.staleClasses.join(", ")}`}` : "class cache: missing");
|
|
41367
41639
|
for (const problem of report.problems) {
|
|
41368
41640
|
console.log(`problem: ${problem}`);
|