gdharness 0.10.2 → 0.11.1
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/README.md +23 -0
- package/build/cli.js +427 -86
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +56 -6
- package/build/index.js +417 -82
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -7190,8 +7190,12 @@ function announced(line) {
|
|
|
7190
7190
|
|
|
7191
7191
|
class GameLog {
|
|
7192
7192
|
entries = [];
|
|
7193
|
-
decoders = {
|
|
7194
|
-
|
|
7193
|
+
decoders = {
|
|
7194
|
+
stdout: new StringDecoder("utf8"),
|
|
7195
|
+
stderr: new StringDecoder("utf8"),
|
|
7196
|
+
transcript: new StringDecoder("utf8")
|
|
7197
|
+
};
|
|
7198
|
+
partial = { stdout: "", stderr: "", transcript: "" };
|
|
7195
7199
|
lastHeadline = null;
|
|
7196
7200
|
cursor = 0;
|
|
7197
7201
|
append(source, chunk) {
|
|
@@ -7204,7 +7208,7 @@ class GameLog {
|
|
|
7204
7208
|
}
|
|
7205
7209
|
}
|
|
7206
7210
|
finish() {
|
|
7207
|
-
for (const source of
|
|
7211
|
+
for (const source of STREAMS) {
|
|
7208
7212
|
const tail = this.partial[source] + this.decoders[source].end();
|
|
7209
7213
|
this.partial[source] = "";
|
|
7210
7214
|
if (tail !== "") {
|
|
@@ -7267,10 +7271,11 @@ class GameLog {
|
|
|
7267
7271
|
return { entries: matching.slice(omitted), omitted };
|
|
7268
7272
|
}
|
|
7269
7273
|
}
|
|
7270
|
-
var HEADLINE, COLOUR_CODE;
|
|
7274
|
+
var HEADLINE, COLOUR_CODE, STREAMS;
|
|
7271
7275
|
var init_game_log = __esm(() => {
|
|
7272
7276
|
HEADLINE = /^(USER )?(SCRIPT ERROR|ERROR|WARNING):\s?(.*)$/;
|
|
7273
7277
|
COLOUR_CODE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
7278
|
+
STREAMS = ["stdout", "stderr", "transcript"];
|
|
7274
7279
|
});
|
|
7275
7280
|
|
|
7276
7281
|
// src/update-check.ts
|
|
@@ -15408,7 +15413,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15408
15413
|
},
|
|
15409
15414
|
{
|
|
15410
15415
|
name: "project_test",
|
|
15411
|
-
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. Needs gdUnit4 under addons/gdUnit4.",
|
|
15416
|
+
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.",
|
|
15412
15417
|
parameters: {
|
|
15413
15418
|
projectPath: PROJECT_PATH,
|
|
15414
15419
|
path: {
|
|
@@ -15720,8 +15725,14 @@ var init_tool_definitions = __esm(() => {
|
|
|
15720
15725
|
type: "boolean",
|
|
15721
15726
|
description: "Only entries printed since the previous editor_output. Default false."
|
|
15722
15727
|
},
|
|
15723
|
-
contains: {
|
|
15724
|
-
|
|
15728
|
+
contains: {
|
|
15729
|
+
type: "string",
|
|
15730
|
+
description: "Only entries mentioning this text, matched against everything the run has printed rather than against the entries this answer would otherwise carry: a line is found however much was printed after it."
|
|
15731
|
+
},
|
|
15732
|
+
limit: {
|
|
15733
|
+
type: "number",
|
|
15734
|
+
description: "The most entries to answer with, newest kept. Default 200, and omitted says how many matching entries that left out. A run long enough to pass it needs this raised, not just filtered: filtering narrows what counts as matching, and the newest of those is still all one answer carries."
|
|
15735
|
+
}
|
|
15725
15736
|
},
|
|
15726
15737
|
requires: []
|
|
15727
15738
|
},
|
|
@@ -15820,8 +15831,8 @@ var init_tool_definitions = __esm(() => {
|
|
|
15820
15831
|
},
|
|
15821
15832
|
includeHidden: {
|
|
15822
15833
|
type: "boolean",
|
|
15823
|
-
ops: ["text"],
|
|
15824
|
-
description: "text: read hidden nodes as well, for checking that something is not showing. Default false."
|
|
15834
|
+
ops: ["text", "find"],
|
|
15835
|
+
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 anything above it is, and how many matches were left out comes back under hidden."
|
|
15825
15836
|
},
|
|
15826
15837
|
metrics: {
|
|
15827
15838
|
type: "array",
|
|
@@ -15838,7 +15849,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15838
15849
|
requires: []
|
|
15839
15850
|
},
|
|
15840
15851
|
find: {
|
|
15841
|
-
summary: "the paths of every node matching className, script, namePattern, group or says, with property read off each",
|
|
15852
|
+
summary: "the paths of every node matching className, script, namePattern, group or says, with property read off each, hidden ones included unless includeHidden says otherwise",
|
|
15842
15853
|
requires: []
|
|
15843
15854
|
},
|
|
15844
15855
|
rect: {
|
|
@@ -35168,7 +35179,37 @@ function whyNoReport(printed, asked) {
|
|
|
35168
35179
|
}
|
|
35169
35180
|
return printed.some((line) => NOTHING_RAN.test(line)) ? `no test cases found at ${asked}` : null;
|
|
35170
35181
|
}
|
|
35171
|
-
|
|
35182
|
+
function orphansPrinted(printed) {
|
|
35183
|
+
const suites = [];
|
|
35184
|
+
let total = null;
|
|
35185
|
+
let running = null;
|
|
35186
|
+
for (const line of printed) {
|
|
35187
|
+
const started = RUNNING_SUITE.exec(line);
|
|
35188
|
+
if (started?.[1] !== undefined) {
|
|
35189
|
+
running = started[1];
|
|
35190
|
+
continue;
|
|
35191
|
+
}
|
|
35192
|
+
const counted = ORPHANS.exec(line);
|
|
35193
|
+
if (counted?.[1] === undefined) {
|
|
35194
|
+
continue;
|
|
35195
|
+
}
|
|
35196
|
+
const orphans = Number(counted[1]);
|
|
35197
|
+
if (line.includes(OVERALL)) {
|
|
35198
|
+
total = orphans;
|
|
35199
|
+
running = null;
|
|
35200
|
+
continue;
|
|
35201
|
+
}
|
|
35202
|
+
if (running !== null && orphans > 0) {
|
|
35203
|
+
suites.push({ path: running, orphans });
|
|
35204
|
+
}
|
|
35205
|
+
running = null;
|
|
35206
|
+
}
|
|
35207
|
+
return {
|
|
35208
|
+
total: total ?? suites.reduce((sum, suite) => sum + suite.orphans, 0),
|
|
35209
|
+
suites
|
|
35210
|
+
};
|
|
35211
|
+
}
|
|
35212
|
+
var MalformedReportError, ENTITIES, ATTRIBUTE, NOTHING_RAN, NO_SUCH_PATH, RUNNING_SUITE, ORPHANS, OVERALL = "Overall Summary";
|
|
35172
35213
|
var init_junit = __esm(() => {
|
|
35173
35214
|
MalformedReportError = class MalformedReportError extends Error {
|
|
35174
35215
|
constructor(message) {
|
|
@@ -35180,6 +35221,8 @@ var init_junit = __esm(() => {
|
|
|
35180
35221
|
ATTRIBUTE = /\s*([^\s=/>]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/y;
|
|
35181
35222
|
NOTHING_RAN = /no test cases found/i;
|
|
35182
35223
|
NO_SUCH_PATH = /given directory or file does not exists:\s*(\S+)/i;
|
|
35224
|
+
RUNNING_SUITE = /Run Test Suite:\s*(\S+)/;
|
|
35225
|
+
ORPHANS = /(\d+)\s+orphans/;
|
|
35183
35226
|
});
|
|
35184
35227
|
|
|
35185
35228
|
// src/lsp_client.ts
|
|
@@ -35483,6 +35526,14 @@ class GodotLSPClient {
|
|
|
35483
35526
|
}
|
|
35484
35527
|
return uri;
|
|
35485
35528
|
}
|
|
35529
|
+
closeDocument(uri) {
|
|
35530
|
+
if (!this.documentVersions.delete(uri)) {
|
|
35531
|
+
return;
|
|
35532
|
+
}
|
|
35533
|
+
try {
|
|
35534
|
+
this.sendNotification("textDocument/didClose", { textDocument: { uri } });
|
|
35535
|
+
} catch {}
|
|
35536
|
+
}
|
|
35486
35537
|
async initialize(rootPath) {
|
|
35487
35538
|
await this.ensureConnected();
|
|
35488
35539
|
const resolvedRootPath = resolve3(rootPath);
|
|
@@ -35536,8 +35587,9 @@ class GodotLSPClient {
|
|
|
35536
35587
|
timer
|
|
35537
35588
|
});
|
|
35538
35589
|
});
|
|
35590
|
+
let opened = null;
|
|
35539
35591
|
try {
|
|
35540
|
-
this.syncDocument(filePath, content);
|
|
35592
|
+
opened = this.syncDocument(filePath, content);
|
|
35541
35593
|
return await diagnosticsPromise;
|
|
35542
35594
|
} catch (error) {
|
|
35543
35595
|
const waiter = this.diagnosticsWaiters.get(key);
|
|
@@ -35546,16 +35598,25 @@ class GodotLSPClient {
|
|
|
35546
35598
|
this.diagnosticsWaiters.delete(key);
|
|
35547
35599
|
}
|
|
35548
35600
|
throw error;
|
|
35601
|
+
} finally {
|
|
35602
|
+
if (opened !== null) {
|
|
35603
|
+
this.closeDocument(opened);
|
|
35604
|
+
}
|
|
35549
35605
|
}
|
|
35550
35606
|
}
|
|
35551
35607
|
async getCompletions(filePath, content, line, character) {
|
|
35552
35608
|
await this.ensureConnected();
|
|
35553
35609
|
await this.ensureInitializedForFile(filePath);
|
|
35554
35610
|
const uri = this.syncDocument(filePath, content);
|
|
35555
|
-
|
|
35556
|
-
|
|
35557
|
-
|
|
35558
|
-
|
|
35611
|
+
let result;
|
|
35612
|
+
try {
|
|
35613
|
+
result = await this.sendRequest("textDocument/completion", {
|
|
35614
|
+
textDocument: { uri },
|
|
35615
|
+
position: { line, character }
|
|
35616
|
+
});
|
|
35617
|
+
} finally {
|
|
35618
|
+
this.closeDocument(uri);
|
|
35619
|
+
}
|
|
35559
35620
|
if (Array.isArray(result)) {
|
|
35560
35621
|
return result;
|
|
35561
35622
|
}
|
|
@@ -35571,18 +35632,27 @@ class GodotLSPClient {
|
|
|
35571
35632
|
await this.ensureConnected();
|
|
35572
35633
|
await this.ensureInitializedForFile(filePath);
|
|
35573
35634
|
const uri = this.syncDocument(filePath, content);
|
|
35574
|
-
|
|
35575
|
-
textDocument
|
|
35576
|
-
|
|
35577
|
-
|
|
35635
|
+
try {
|
|
35636
|
+
return await this.sendRequest("textDocument/hover", {
|
|
35637
|
+
textDocument: { uri },
|
|
35638
|
+
position: { line, character }
|
|
35639
|
+
});
|
|
35640
|
+
} finally {
|
|
35641
|
+
this.closeDocument(uri);
|
|
35642
|
+
}
|
|
35578
35643
|
}
|
|
35579
35644
|
async getDocumentSymbols(filePath, content) {
|
|
35580
35645
|
await this.ensureConnected();
|
|
35581
35646
|
await this.ensureInitializedForFile(filePath);
|
|
35582
35647
|
const uri = this.syncDocument(filePath, content);
|
|
35583
|
-
|
|
35584
|
-
|
|
35585
|
-
|
|
35648
|
+
let result;
|
|
35649
|
+
try {
|
|
35650
|
+
result = await this.sendRequest("textDocument/documentSymbol", {
|
|
35651
|
+
textDocument: { uri }
|
|
35652
|
+
});
|
|
35653
|
+
} finally {
|
|
35654
|
+
this.closeDocument(uri);
|
|
35655
|
+
}
|
|
35586
35656
|
return Array.isArray(result) ? result : [];
|
|
35587
35657
|
}
|
|
35588
35658
|
isConnected() {
|
|
@@ -36024,15 +36094,115 @@ var init_runtime_client = __esm(() => {
|
|
|
36024
36094
|
ANNOUNCEMENT_PATTERN = /^runtime-(\d+)\.json$/;
|
|
36025
36095
|
});
|
|
36026
36096
|
|
|
36097
|
+
// src/run-record.ts
|
|
36098
|
+
import { mkdirSync as mkdirSync6, openSync, readdirSync as readdirSync6, readFileSync as readFileSync12, rmSync as rmSync7, statSync as statSync2, writeFileSync as writeFileSync8 } from "node:fs";
|
|
36099
|
+
import { join as join14 } from "node:path";
|
|
36100
|
+
function runsDirectory() {
|
|
36101
|
+
return join14(runtimeDirectory(), "runs");
|
|
36102
|
+
}
|
|
36103
|
+
function recordPath() {
|
|
36104
|
+
return join14(runsDirectory(), "run.json");
|
|
36105
|
+
}
|
|
36106
|
+
function recordPaths() {
|
|
36107
|
+
return runtimeDirectories().map((directory) => join14(directory, "runs", "run.json"));
|
|
36108
|
+
}
|
|
36109
|
+
function openTranscript(startedAt) {
|
|
36110
|
+
mkdirSync6(runsDirectory(), { recursive: true });
|
|
36111
|
+
const path = join14(runsDirectory(), `run-${startedAt}.log`);
|
|
36112
|
+
return { path, fd: openSync(path, "a") };
|
|
36113
|
+
}
|
|
36114
|
+
function writeRunRecord(record) {
|
|
36115
|
+
mkdirSync6(runsDirectory(), { recursive: true });
|
|
36116
|
+
writeFileSync8(recordPath(), JSON.stringify(record, null, 2), "utf8");
|
|
36117
|
+
}
|
|
36118
|
+
function readRunRecord() {
|
|
36119
|
+
for (const path of recordPaths()) {
|
|
36120
|
+
const record = recordAt(path);
|
|
36121
|
+
if (record !== null) {
|
|
36122
|
+
return record;
|
|
36123
|
+
}
|
|
36124
|
+
}
|
|
36125
|
+
return null;
|
|
36126
|
+
}
|
|
36127
|
+
function recordAt(path) {
|
|
36128
|
+
let parsed;
|
|
36129
|
+
try {
|
|
36130
|
+
parsed = JSON.parse(readFileSync12(path, "utf8"));
|
|
36131
|
+
} catch {
|
|
36132
|
+
return null;
|
|
36133
|
+
}
|
|
36134
|
+
if (!parsed || typeof parsed !== "object") {
|
|
36135
|
+
return null;
|
|
36136
|
+
}
|
|
36137
|
+
const fields = parsed;
|
|
36138
|
+
const pid = fields["pid"];
|
|
36139
|
+
const transcript = fields["transcript"];
|
|
36140
|
+
const startedAt = fields["startedAt"];
|
|
36141
|
+
if (typeof pid !== "number" || typeof transcript !== "string" || typeof startedAt !== "number") {
|
|
36142
|
+
return null;
|
|
36143
|
+
}
|
|
36144
|
+
const args = fields["arguments"];
|
|
36145
|
+
return {
|
|
36146
|
+
pid,
|
|
36147
|
+
transcript,
|
|
36148
|
+
startedAt,
|
|
36149
|
+
projectPath: typeof fields["projectPath"] === "string" ? fields["projectPath"] : "",
|
|
36150
|
+
arguments: Array.isArray(args) ? args.filter((value) => typeof value === "string") : []
|
|
36151
|
+
};
|
|
36152
|
+
}
|
|
36153
|
+
function clearRunRecord(ours = () => true) {
|
|
36154
|
+
for (const path of recordPaths()) {
|
|
36155
|
+
const record = recordAt(path);
|
|
36156
|
+
if (record === null || !ours(record)) {
|
|
36157
|
+
continue;
|
|
36158
|
+
}
|
|
36159
|
+
try {
|
|
36160
|
+
rmSync7(path);
|
|
36161
|
+
} catch {}
|
|
36162
|
+
}
|
|
36163
|
+
}
|
|
36164
|
+
function ageMs(path, now) {
|
|
36165
|
+
try {
|
|
36166
|
+
return now - statSync2(path).mtimeMs;
|
|
36167
|
+
} catch {
|
|
36168
|
+
return 0;
|
|
36169
|
+
}
|
|
36170
|
+
}
|
|
36171
|
+
function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
36172
|
+
const directory = runsDirectory();
|
|
36173
|
+
const keep = readRunRecord()?.transcript;
|
|
36174
|
+
let entries;
|
|
36175
|
+
try {
|
|
36176
|
+
entries = readdirSync6(directory);
|
|
36177
|
+
} catch {
|
|
36178
|
+
return;
|
|
36179
|
+
}
|
|
36180
|
+
for (const entry of entries) {
|
|
36181
|
+
if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
|
|
36182
|
+
continue;
|
|
36183
|
+
}
|
|
36184
|
+
const path = join14(directory, entry);
|
|
36185
|
+
if (path === keep || ageMs(path, now) < keepMs) {
|
|
36186
|
+
continue;
|
|
36187
|
+
}
|
|
36188
|
+
try {
|
|
36189
|
+
rmSync7(path);
|
|
36190
|
+
} catch {}
|
|
36191
|
+
}
|
|
36192
|
+
}
|
|
36193
|
+
var init_run_record = __esm(() => {
|
|
36194
|
+
init_runtime_client();
|
|
36195
|
+
});
|
|
36196
|
+
|
|
36027
36197
|
// src/setup.ts
|
|
36028
|
-
import { cpSync as cpSync2, existsSync as existsSync11, mkdirSync as
|
|
36029
|
-
import { dirname as dirname8, join as
|
|
36198
|
+
import { cpSync as cpSync2, existsSync as existsSync11, mkdirSync as mkdirSync7, readFileSync as readFileSync13, rmSync as rmSync8, writeFileSync as writeFileSync9 } from "node:fs";
|
|
36199
|
+
import { dirname as dirname8, join as join15 } from "node:path";
|
|
36030
36200
|
function installedAddonVersion(projectPath) {
|
|
36031
|
-
const marker =
|
|
36201
|
+
const marker = join15(projectPath, "addons", ADDONS2[0], VERSION_MARKER2);
|
|
36032
36202
|
if (!existsSync11(marker)) {
|
|
36033
36203
|
return null;
|
|
36034
36204
|
}
|
|
36035
|
-
const said =
|
|
36205
|
+
const said = readFileSync13(marker, "utf8").trim();
|
|
36036
36206
|
return said === "" ? null : said;
|
|
36037
36207
|
}
|
|
36038
36208
|
var ADDONS2, RUNTIME_AUTOLOAD2, VERSION_MARKER2 = ".gdharness-version";
|
|
@@ -36050,24 +36220,45 @@ var init_setup = __esm(() => {
|
|
|
36050
36220
|
|
|
36051
36221
|
// src/server.ts
|
|
36052
36222
|
import { execFile as execFile5, spawn } from "node:child_process";
|
|
36053
|
-
import {
|
|
36223
|
+
import {
|
|
36224
|
+
closeSync,
|
|
36225
|
+
existsSync as existsSync12,
|
|
36226
|
+
fstatSync,
|
|
36227
|
+
mkdirSync as mkdirSync8,
|
|
36228
|
+
mkdtempSync as mkdtempSync3,
|
|
36229
|
+
openSync as openSync2,
|
|
36230
|
+
readdirSync as readdirSync7,
|
|
36231
|
+
readFileSync as readFileSync14,
|
|
36232
|
+
readSync,
|
|
36233
|
+
realpathSync as realpathSync2,
|
|
36234
|
+
rmSync as rmSync9
|
|
36235
|
+
} from "node:fs";
|
|
36054
36236
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
36055
|
-
import { basename as basename2, dirname as dirname9, join as
|
|
36237
|
+
import { basename as basename2, dirname as dirname9, join as join16, normalize as normalize3 } from "node:path";
|
|
36056
36238
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
36057
36239
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
36058
36240
|
import { promisify as promisify5 } from "node:util";
|
|
36059
36241
|
function patienceForFrames(frames, atLeast) {
|
|
36060
36242
|
return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
|
|
36061
36243
|
}
|
|
36244
|
+
function stillRunning(run) {
|
|
36245
|
+
if (run?.exitCode !== null || run.endedUnwatched === true) {
|
|
36246
|
+
return false;
|
|
36247
|
+
}
|
|
36248
|
+
if (!run.throughEditor && run.process === null && run.pid !== null) {
|
|
36249
|
+
return alive(run.pid);
|
|
36250
|
+
}
|
|
36251
|
+
return true;
|
|
36252
|
+
}
|
|
36062
36253
|
function alive(pid) {
|
|
36063
|
-
if (pid === undefined) {
|
|
36254
|
+
if (pid === undefined || pid === null) {
|
|
36064
36255
|
return false;
|
|
36065
36256
|
}
|
|
36066
36257
|
try {
|
|
36067
36258
|
process.kill(pid, 0);
|
|
36068
36259
|
return true;
|
|
36069
|
-
} catch {
|
|
36070
|
-
return
|
|
36260
|
+
} catch (error) {
|
|
36261
|
+
return error.code === "EPERM";
|
|
36071
36262
|
}
|
|
36072
36263
|
}
|
|
36073
36264
|
function isOfType(value, kind) {
|
|
@@ -36147,13 +36338,13 @@ function aboveTheRunner(entry) {
|
|
|
36147
36338
|
}
|
|
36148
36339
|
function elsewhereIn(projectPath, asked) {
|
|
36149
36340
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
36150
|
-
if (wanted === "" || existsSync12(
|
|
36341
|
+
if (wanted === "" || existsSync12(join16(projectPath, wanted))) {
|
|
36151
36342
|
return "";
|
|
36152
36343
|
}
|
|
36153
36344
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
36154
36345
|
let near = [];
|
|
36155
36346
|
try {
|
|
36156
|
-
near =
|
|
36347
|
+
near = readdirSync7(projectPath, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.toLowerCase() !== root).map((entry) => entry.name).filter((name) => name.toLowerCase().startsWith("test") || root.startsWith(name.toLowerCase()));
|
|
36157
36348
|
} catch {
|
|
36158
36349
|
return "";
|
|
36159
36350
|
}
|
|
@@ -36161,11 +36352,11 @@ function elsewhereIn(projectPath, asked) {
|
|
|
36161
36352
|
return near.length === 0 ? "" : ` The project does have ${there}.`;
|
|
36162
36353
|
}
|
|
36163
36354
|
function hasMainScene(projectFile) {
|
|
36164
|
-
const scene = parseProjectGodot(
|
|
36355
|
+
const scene = parseProjectGodot(readFileSync14(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
36165
36356
|
return typeof scene === "string" && scene !== "";
|
|
36166
36357
|
}
|
|
36167
36358
|
function editorPlaysHeadless(projectFile) {
|
|
36168
|
-
const runArgs = parseProjectGodot(
|
|
36359
|
+
const runArgs = parseProjectGodot(readFileSync14(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
36169
36360
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
36170
36361
|
}
|
|
36171
36362
|
function camelCased(params) {
|
|
@@ -36181,7 +36372,7 @@ function camelCased(params) {
|
|
|
36181
36372
|
class GodotServer {
|
|
36182
36373
|
mcp;
|
|
36183
36374
|
locator = new GodotLocator2;
|
|
36184
|
-
operationsScript =
|
|
36375
|
+
operationsScript = join16(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
36185
36376
|
godotBridge;
|
|
36186
36377
|
tools = buildToolDefinitions();
|
|
36187
36378
|
activeProcess = null;
|
|
@@ -36338,10 +36529,7 @@ class GodotServer {
|
|
|
36338
36529
|
this.stopWatchingForASuccessor();
|
|
36339
36530
|
withdrawBridge(this.announcedAt);
|
|
36340
36531
|
this.announcedAt = null;
|
|
36341
|
-
|
|
36342
|
-
this.activeProcess.process?.kill();
|
|
36343
|
-
this.activeProcess = null;
|
|
36344
|
-
}
|
|
36532
|
+
this.activeProcess = null;
|
|
36345
36533
|
if (this.lspClient) {
|
|
36346
36534
|
try {
|
|
36347
36535
|
await this.lspClient.disconnect();
|
|
@@ -36406,14 +36594,7 @@ class GodotServer {
|
|
|
36406
36594
|
return;
|
|
36407
36595
|
}
|
|
36408
36596
|
this.shutdownInitiated = true;
|
|
36409
|
-
|
|
36410
|
-
try {
|
|
36411
|
-
this.activeProcess.process?.kill();
|
|
36412
|
-
} catch (error) {
|
|
36413
|
-
console.error("[SERVER] Failed to kill the Godot process on exit:", errorMessage(error));
|
|
36414
|
-
}
|
|
36415
|
-
this.activeProcess = null;
|
|
36416
|
-
}
|
|
36597
|
+
this.activeProcess = null;
|
|
36417
36598
|
}
|
|
36418
36599
|
setupToolHandlers() {
|
|
36419
36600
|
this.mcp.server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: this.tools }));
|
|
@@ -36773,7 +36954,7 @@ class GodotServer {
|
|
|
36773
36954
|
if (path === undefined) {
|
|
36774
36955
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
36775
36956
|
}
|
|
36776
|
-
const file =
|
|
36957
|
+
const file = join16(path, "project.godot");
|
|
36777
36958
|
if (!existsSync12(file)) {
|
|
36778
36959
|
return {
|
|
36779
36960
|
ok: false,
|
|
@@ -36943,7 +37124,7 @@ class GodotServer {
|
|
|
36943
37124
|
if (!engine.ok) {
|
|
36944
37125
|
return engine.response;
|
|
36945
37126
|
}
|
|
36946
|
-
const application = parseProjectGodot(
|
|
37127
|
+
const application = parseProjectGodot(readFileSync14(project.value.file, "utf8"))["application"] ?? {};
|
|
36947
37128
|
const name = application["config/name"];
|
|
36948
37129
|
const mainScene = application["run/main_scene"];
|
|
36949
37130
|
const info = {
|
|
@@ -37006,7 +37187,7 @@ class GodotServer {
|
|
|
37006
37187
|
}
|
|
37007
37188
|
const folder = dirname9(output.absolutePath);
|
|
37008
37189
|
try {
|
|
37009
|
-
|
|
37190
|
+
mkdirSync8(folder, { recursive: true });
|
|
37010
37191
|
} catch (error) {
|
|
37011
37192
|
return this.createErrorResponse(`Cannot create ${dirname9(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
37012
37193
|
}
|
|
@@ -37068,7 +37249,7 @@ class GodotServer {
|
|
|
37068
37249
|
return contained.response;
|
|
37069
37250
|
}
|
|
37070
37251
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
37071
|
-
if (!existsSync12(
|
|
37252
|
+
if (!existsSync12(join16(project.value.path, runner))) {
|
|
37072
37253
|
return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
|
|
37073
37254
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
37074
37255
|
]);
|
|
@@ -37102,7 +37283,7 @@ class GodotServer {
|
|
|
37102
37283
|
];
|
|
37103
37284
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
|
|
37104
37285
|
this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
37105
|
-
const userData = mkdtempSync3(
|
|
37286
|
+
const userData = mkdtempSync3(join16(tmpdir5(), "gdharness-tests-"));
|
|
37106
37287
|
const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
|
|
37107
37288
|
const hung = await new Promise((resolve) => {
|
|
37108
37289
|
const timer = setTimeout(() => {
|
|
@@ -37118,26 +37299,26 @@ class GodotServer {
|
|
|
37118
37299
|
resolve(false);
|
|
37119
37300
|
});
|
|
37120
37301
|
});
|
|
37121
|
-
const reportsDir =
|
|
37302
|
+
const reportsDir = join16(project.value.path, ".godot", "gdharness-reports");
|
|
37122
37303
|
let report = null;
|
|
37123
37304
|
let reportProblem = null;
|
|
37124
37305
|
try {
|
|
37125
|
-
const written = existsSync12(reportsDir) ?
|
|
37306
|
+
const written = existsSync12(reportsDir) ? readdirSync7(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
37126
37307
|
const newest = written.at(-1);
|
|
37127
37308
|
if (newest !== undefined) {
|
|
37128
|
-
report = parseJUnit(
|
|
37309
|
+
report = parseJUnit(readFileSync14(join16(reportsDir, newest, "results.xml"), "utf8"));
|
|
37129
37310
|
}
|
|
37130
37311
|
} catch (error) {
|
|
37131
37312
|
reportProblem = errorMessage(error);
|
|
37132
37313
|
} finally {
|
|
37133
|
-
|
|
37134
|
-
|
|
37314
|
+
rmSync9(reportsDir, { recursive: true, force: true });
|
|
37315
|
+
rmSync9(userData, { recursive: true, force: true });
|
|
37135
37316
|
}
|
|
37136
37317
|
const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries.map(aboveTheRunner);
|
|
37137
37318
|
const verdicts = {
|
|
37138
37319
|
0: "passed",
|
|
37139
37320
|
100: "failures",
|
|
37140
|
-
101: "warnings",
|
|
37321
|
+
101: "warnings: orphan nodes",
|
|
37141
37322
|
103: "headless not supported by this gdUnit4",
|
|
37142
37323
|
104: "Godot version not supported by this gdUnit4",
|
|
37143
37324
|
105: "script errors"
|
|
@@ -37169,7 +37350,17 @@ class GodotServer {
|
|
|
37169
37350
|
};
|
|
37170
37351
|
}
|
|
37171
37352
|
const failed = report.suites.flatMap((suite) => suite.cases.filter((entry) => entry.status === "failed" || entry.status === "error").map((entry) => ({ ...entry, path: suite.path })));
|
|
37172
|
-
const
|
|
37353
|
+
const orphans = orphansPrinted(said);
|
|
37354
|
+
const leftBehind = new Map(orphans.suites.map((suite) => [suite.path, suite.orphans]));
|
|
37355
|
+
const orphansIn = (path) => path === null ? 0 : leftBehind.get(path) ?? 0;
|
|
37356
|
+
const warnings = orphans.suites.map((suite) => ({
|
|
37357
|
+
kind: "orphans",
|
|
37358
|
+
path: suite.path,
|
|
37359
|
+
name: report.suites.find((entry) => entry.path === suite.path)?.name ?? suite.path,
|
|
37360
|
+
orphans: suite.orphans,
|
|
37361
|
+
message: `${suite.orphans} node${suite.orphans === 1 ? "" : "s"} were still in the tree when this suite finished. A node built in a test and neither freed nor added to the tree is the usual cause; free it, or add it with auto_free().`
|
|
37362
|
+
}));
|
|
37363
|
+
const unclean = report.suites.filter((suite) => suite.failures > 0 || suite.errors > 0 || suite.skipped > 0 || orphansIn(suite.path) > 0);
|
|
37173
37364
|
return this.jsonTextResponse({
|
|
37174
37365
|
passed: !hung && exitCode === 0 && report.failures === 0 && report.errors === 0,
|
|
37175
37366
|
verdict,
|
|
@@ -37180,6 +37371,9 @@ class GodotServer {
|
|
|
37180
37371
|
skipped: report.skipped,
|
|
37181
37372
|
time: report.time,
|
|
37182
37373
|
failed,
|
|
37374
|
+
warnings: warnings.length > 0 ? warnings : undefined,
|
|
37375
|
+
orphans: orphans.total > 0 ? orphans.total : undefined,
|
|
37376
|
+
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." : undefined,
|
|
37183
37377
|
suites: unclean.map((suite) => ({
|
|
37184
37378
|
name: suite.name,
|
|
37185
37379
|
path: suite.path,
|
|
@@ -37187,6 +37381,7 @@ class GodotServer {
|
|
|
37187
37381
|
failures: suite.failures,
|
|
37188
37382
|
errors: suite.errors,
|
|
37189
37383
|
skipped: suite.skipped,
|
|
37384
|
+
orphans: orphansIn(suite.path) > 0 ? orphansIn(suite.path) : undefined,
|
|
37190
37385
|
time: suite.time
|
|
37191
37386
|
})),
|
|
37192
37387
|
suitesPassed: report.suites.length - unclean.length,
|
|
@@ -37313,7 +37508,7 @@ class GodotServer {
|
|
|
37313
37508
|
version: godotPath === null ? null : await this.godotVersion(godotPath)
|
|
37314
37509
|
},
|
|
37315
37510
|
game: {
|
|
37316
|
-
processActive: this.
|
|
37511
|
+
processActive: stillRunning(this.currentRun()),
|
|
37317
37512
|
playingInEditor: playing,
|
|
37318
37513
|
runtimeConnected: games.some((game) => game.reachable),
|
|
37319
37514
|
runtimes: games
|
|
@@ -37481,7 +37676,7 @@ class GodotServer {
|
|
|
37481
37676
|
if (op === "check") {
|
|
37482
37677
|
return await this.checkBoot(engine.value, project.value.path, sceneArgument, args, given.value);
|
|
37483
37678
|
}
|
|
37484
|
-
if (this.
|
|
37679
|
+
if (stillRunning(this.currentRun())) {
|
|
37485
37680
|
this.logDebug("Ending the running game before starting another");
|
|
37486
37681
|
await this.endActiveGame();
|
|
37487
37682
|
}
|
|
@@ -37501,7 +37696,7 @@ class GodotServer {
|
|
|
37501
37696
|
userArgs: given.value
|
|
37502
37697
|
});
|
|
37503
37698
|
this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
37504
|
-
const started = this.
|
|
37699
|
+
const started = this.spawnKeptGame(engine.value, cmdArgs, project.value.path);
|
|
37505
37700
|
this.activeProcess = started;
|
|
37506
37701
|
const spawnedInstead = this.godotBridge.isConnected() && editorWouldPlay && given.value.length > 0 ? " The editor cannot be handed arguments for a game it plays, so this one was started " + "here: the debug_* tools answer only for a game the editor is playing." : "";
|
|
37507
37702
|
return this.jsonTextResponse({
|
|
@@ -37515,7 +37710,7 @@ class GodotServer {
|
|
|
37515
37710
|
});
|
|
37516
37711
|
}
|
|
37517
37712
|
async runtimeUp(projectPath, before) {
|
|
37518
|
-
if (!existsSync12(
|
|
37713
|
+
if (!existsSync12(join16(projectPath, RUNTIME_AUTOLOAD2.path))) {
|
|
37519
37714
|
return {
|
|
37520
37715
|
listening: false,
|
|
37521
37716
|
note: "this project has no gdharness_runtime addon, so the runtime_* tools have nothing to talk to"
|
|
@@ -37550,7 +37745,11 @@ class GodotServer {
|
|
|
37550
37745
|
}
|
|
37551
37746
|
const played = {
|
|
37552
37747
|
process: null,
|
|
37748
|
+
pid: null,
|
|
37553
37749
|
log,
|
|
37750
|
+
transcript: null,
|
|
37751
|
+
readOffset: 0,
|
|
37752
|
+
projectPath,
|
|
37554
37753
|
startedAt: Date.now(),
|
|
37555
37754
|
exitCode: null,
|
|
37556
37755
|
throughEditor: true,
|
|
@@ -37589,6 +37788,7 @@ class GodotServer {
|
|
|
37589
37788
|
async endActiveGame() {
|
|
37590
37789
|
const running = this.activeProcess;
|
|
37591
37790
|
this.activeProcess = null;
|
|
37791
|
+
clearRunRecord((record) => this.couldBeOurs(record.projectPath));
|
|
37592
37792
|
if (!running) {
|
|
37593
37793
|
return;
|
|
37594
37794
|
}
|
|
@@ -37596,14 +37796,26 @@ class GodotServer {
|
|
|
37596
37796
|
await this.handleViaBridge("stop_playing", {});
|
|
37597
37797
|
return;
|
|
37598
37798
|
}
|
|
37599
|
-
running.process
|
|
37799
|
+
if (running.process !== null) {
|
|
37800
|
+
running.process.kill();
|
|
37801
|
+
return;
|
|
37802
|
+
}
|
|
37803
|
+
if (running.pid !== null) {
|
|
37804
|
+
try {
|
|
37805
|
+
process.kill(running.pid);
|
|
37806
|
+
} catch {}
|
|
37807
|
+
}
|
|
37600
37808
|
}
|
|
37601
37809
|
spawnGame(godotPath, cmdArgs, env) {
|
|
37602
37810
|
const child = spawn(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
|
|
37603
37811
|
const log = new GameLog;
|
|
37604
37812
|
const started = {
|
|
37605
37813
|
process: child,
|
|
37814
|
+
pid: child.pid ?? null,
|
|
37606
37815
|
log,
|
|
37816
|
+
transcript: null,
|
|
37817
|
+
readOffset: 0,
|
|
37818
|
+
projectPath: null,
|
|
37607
37819
|
startedAt: Date.now(),
|
|
37608
37820
|
exitCode: null,
|
|
37609
37821
|
throughEditor: false,
|
|
@@ -37628,6 +37840,117 @@ class GodotServer {
|
|
|
37628
37840
|
});
|
|
37629
37841
|
return started;
|
|
37630
37842
|
}
|
|
37843
|
+
spawnKeptGame(godotPath, cmdArgs, projectPath, env) {
|
|
37844
|
+
const startedAt = Date.now();
|
|
37845
|
+
const transcript = openTranscript(startedAt);
|
|
37846
|
+
let child;
|
|
37847
|
+
try {
|
|
37848
|
+
child = spawn(godotPath, cmdArgs, {
|
|
37849
|
+
stdio: ["ignore", transcript.fd, transcript.fd],
|
|
37850
|
+
detached: true,
|
|
37851
|
+
...env ? { env } : {}
|
|
37852
|
+
});
|
|
37853
|
+
} finally {
|
|
37854
|
+
closeSync(transcript.fd);
|
|
37855
|
+
}
|
|
37856
|
+
child.unref();
|
|
37857
|
+
const log = new GameLog;
|
|
37858
|
+
const started = {
|
|
37859
|
+
process: child,
|
|
37860
|
+
pid: child.pid ?? null,
|
|
37861
|
+
log,
|
|
37862
|
+
transcript: transcript.path,
|
|
37863
|
+
readOffset: 0,
|
|
37864
|
+
projectPath,
|
|
37865
|
+
startedAt,
|
|
37866
|
+
exitCode: null,
|
|
37867
|
+
throughEditor: false,
|
|
37868
|
+
brokeOn: null
|
|
37869
|
+
};
|
|
37870
|
+
child.on("exit", (code) => {
|
|
37871
|
+
this.logDebug(`Godot process exited with code ${code ?? "none"}`);
|
|
37872
|
+
this.drainTranscript(started);
|
|
37873
|
+
log.finish();
|
|
37874
|
+
started.exitCode = code ?? -1;
|
|
37875
|
+
});
|
|
37876
|
+
child.on("error", (err) => {
|
|
37877
|
+
console.error("Failed to start Godot process:", err);
|
|
37878
|
+
log.append("stderr", `${err.message}
|
|
37879
|
+
`);
|
|
37880
|
+
started.exitCode = -1;
|
|
37881
|
+
});
|
|
37882
|
+
if (started.pid !== null) {
|
|
37883
|
+
writeRunRecord({
|
|
37884
|
+
pid: started.pid,
|
|
37885
|
+
transcript: transcript.path,
|
|
37886
|
+
startedAt,
|
|
37887
|
+
projectPath,
|
|
37888
|
+
arguments: cmdArgs
|
|
37889
|
+
});
|
|
37890
|
+
}
|
|
37891
|
+
sweepTranscripts();
|
|
37892
|
+
return started;
|
|
37893
|
+
}
|
|
37894
|
+
drainTranscript(game) {
|
|
37895
|
+
if (game.transcript === null) {
|
|
37896
|
+
return;
|
|
37897
|
+
}
|
|
37898
|
+
let handle;
|
|
37899
|
+
try {
|
|
37900
|
+
handle = openSync2(game.transcript, "r");
|
|
37901
|
+
} catch {
|
|
37902
|
+
return;
|
|
37903
|
+
}
|
|
37904
|
+
try {
|
|
37905
|
+
const size = fstatSync(handle).size;
|
|
37906
|
+
if (size <= game.readOffset) {
|
|
37907
|
+
return;
|
|
37908
|
+
}
|
|
37909
|
+
const buffer = Buffer.alloc(size - game.readOffset);
|
|
37910
|
+
const read = readSync(handle, buffer, 0, buffer.length, game.readOffset);
|
|
37911
|
+
game.readOffset += read;
|
|
37912
|
+
game.log.append("transcript", buffer.subarray(0, read));
|
|
37913
|
+
} finally {
|
|
37914
|
+
closeSync(handle);
|
|
37915
|
+
}
|
|
37916
|
+
}
|
|
37917
|
+
adoptRecordedRun() {
|
|
37918
|
+
const record = readRunRecord();
|
|
37919
|
+
if (record === null || !this.couldBeOurs(record.projectPath)) {
|
|
37920
|
+
return null;
|
|
37921
|
+
}
|
|
37922
|
+
const adopted = {
|
|
37923
|
+
process: null,
|
|
37924
|
+
pid: record.pid,
|
|
37925
|
+
log: new GameLog,
|
|
37926
|
+
transcript: record.transcript,
|
|
37927
|
+
readOffset: 0,
|
|
37928
|
+
projectPath: record.projectPath === "" ? null : record.projectPath,
|
|
37929
|
+
startedAt: record.startedAt,
|
|
37930
|
+
exitCode: null,
|
|
37931
|
+
throughEditor: false,
|
|
37932
|
+
brokeOn: null
|
|
37933
|
+
};
|
|
37934
|
+
this.drainTranscript(adopted);
|
|
37935
|
+
if (!alive(record.pid)) {
|
|
37936
|
+
adopted.log.finish();
|
|
37937
|
+
adopted.exitCode = null;
|
|
37938
|
+
adopted.endedUnwatched = true;
|
|
37939
|
+
}
|
|
37940
|
+
this.activeProcess = adopted;
|
|
37941
|
+
return adopted;
|
|
37942
|
+
}
|
|
37943
|
+
couldBeOurs(project) {
|
|
37944
|
+
if (project === "") {
|
|
37945
|
+
return true;
|
|
37946
|
+
}
|
|
37947
|
+
const status = this.godotBridge.getStatus();
|
|
37948
|
+
const mine = this.ownProject ?? (status.connected ? status.projectPath ?? null : null);
|
|
37949
|
+
return mine === null || samePath(mine, project);
|
|
37950
|
+
}
|
|
37951
|
+
currentRun() {
|
|
37952
|
+
return this.activeProcess ?? this.adoptRecordedRun();
|
|
37953
|
+
}
|
|
37631
37954
|
async checkBoot(godotPath, projectPath, scene, args, userArgs) {
|
|
37632
37955
|
const frames = readPositiveNumber(args, "frames") ?? 3;
|
|
37633
37956
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 60000;
|
|
@@ -37661,37 +37984,47 @@ class GodotServer {
|
|
|
37661
37984
|
});
|
|
37662
37985
|
}
|
|
37663
37986
|
handleGetDebugOutput(args) {
|
|
37664
|
-
|
|
37987
|
+
const run = this.currentRun();
|
|
37988
|
+
if (!run) {
|
|
37665
37989
|
return this.createErrorResponse("No game is running. Start one with editor_run.");
|
|
37666
37990
|
}
|
|
37667
|
-
this.drainEditorOutput(
|
|
37991
|
+
this.drainEditorOutput(run);
|
|
37992
|
+
this.drainTranscript(run);
|
|
37993
|
+
if (run.endedUnwatched !== true && !stillRunning(run)) {
|
|
37994
|
+
run.log.finish();
|
|
37995
|
+
run.endedUnwatched = run.exitCode === null;
|
|
37996
|
+
}
|
|
37668
37997
|
const severity = readString(args, "severity");
|
|
37669
|
-
const selected =
|
|
37998
|
+
const selected = run.log.select({
|
|
37670
37999
|
severity: severity === "error" || severity === "warning" ? severity : "info",
|
|
37671
38000
|
sinceLastCall: readBoolean(args, "sinceLastCall") ?? false,
|
|
37672
38001
|
contains: readNonEmptyString(args, "contains"),
|
|
37673
38002
|
limit: readPositiveNumber(args, "limit") ?? 200
|
|
37674
38003
|
});
|
|
37675
|
-
const halt =
|
|
38004
|
+
const halt = run.throughEditor ? this.dapClient?.whereItStopped() ?? null : null;
|
|
37676
38005
|
return this.jsonTextResponse({
|
|
37677
|
-
running:
|
|
37678
|
-
exitCode:
|
|
37679
|
-
through:
|
|
37680
|
-
pid:
|
|
37681
|
-
errors:
|
|
37682
|
-
warnings:
|
|
37683
|
-
clean:
|
|
38006
|
+
running: stillRunning(run),
|
|
38007
|
+
exitCode: run.exitCode,
|
|
38008
|
+
through: run.throughEditor ? "editor" : "gdharness",
|
|
38009
|
+
pid: run.pid,
|
|
38010
|
+
errors: run.log.count("error"),
|
|
38011
|
+
warnings: run.log.count("warning"),
|
|
38012
|
+
clean: run.log.count("error") === 0,
|
|
37684
38013
|
heldAt: halt,
|
|
38014
|
+
endedUnwatched: run.endedUnwatched === true ? true : undefined,
|
|
38015
|
+
project: !run.throughEditor && run.process === null ? run.projectPath ?? undefined : undefined,
|
|
38016
|
+
note: run.endedUnwatched === true ? "This run outlived the server that started it and is over now, so its exit code was never collected. Everything it printed is below, read back from its transcript." : undefined,
|
|
37685
38017
|
omitted: selected.omitted,
|
|
37686
38018
|
entries: selected.entries
|
|
37687
38019
|
});
|
|
37688
38020
|
}
|
|
37689
38021
|
async handleStopProject() {
|
|
37690
|
-
|
|
38022
|
+
const stopped = this.currentRun();
|
|
38023
|
+
if (!stopped) {
|
|
37691
38024
|
return this.createErrorResponse("No game is running. Start one with editor_run.");
|
|
37692
38025
|
}
|
|
37693
|
-
const stopped = this.activeProcess;
|
|
37694
38026
|
this.drainEditorOutput(stopped);
|
|
38027
|
+
this.drainTranscript(stopped);
|
|
37695
38028
|
this.logDebug("Stopping the running game");
|
|
37696
38029
|
await this.endActiveGame();
|
|
37697
38030
|
return this.jsonTextResponse({
|
|
@@ -37797,8 +38130,8 @@ class GodotServer {
|
|
|
37797
38130
|
return this.createErrorResponse(choice.problem);
|
|
37798
38131
|
}
|
|
37799
38132
|
const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
|
|
37800
|
-
const screenshotDir = expectsScreenshot ? mkdtempSync3(
|
|
37801
|
-
const screenshotPath = screenshotDir ?
|
|
38133
|
+
const screenshotDir = expectsScreenshot ? mkdtempSync3(join16(tmpdir5(), "gdharness-runtime-screenshot-")) : null;
|
|
38134
|
+
const screenshotPath = screenshotDir ? join16(screenshotDir, "capture.png") : null;
|
|
37802
38135
|
try {
|
|
37803
38136
|
const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs);
|
|
37804
38137
|
if (!reply.ok) {
|
|
@@ -37819,12 +38152,12 @@ class GodotServer {
|
|
|
37819
38152
|
return {
|
|
37820
38153
|
content: [
|
|
37821
38154
|
{ type: "text", text: `Screenshot captured: ${dimensions}` },
|
|
37822
|
-
{ type: "image", data:
|
|
38155
|
+
{ type: "image", data: readFileSync14(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
37823
38156
|
]
|
|
37824
38157
|
};
|
|
37825
38158
|
} finally {
|
|
37826
38159
|
if (screenshotDir) {
|
|
37827
|
-
|
|
38160
|
+
rmSync9(screenshotDir, { recursive: true, force: true });
|
|
37828
38161
|
}
|
|
37829
38162
|
}
|
|
37830
38163
|
}
|
|
@@ -37849,12 +38182,14 @@ class GodotServer {
|
|
|
37849
38182
|
return this.createErrorResponse("runtime_inspect find needs at least one of className, script, namePattern, group, says.");
|
|
37850
38183
|
}
|
|
37851
38184
|
const property = readNonEmptyString(args, "property");
|
|
38185
|
+
const includeHidden = readBoolean(args, "includeHidden");
|
|
37852
38186
|
return await this.handleRuntimeCommand("find_nodes", {
|
|
37853
38187
|
...filters,
|
|
37854
38188
|
projectPath: args["projectPath"],
|
|
37855
38189
|
root: readNonEmptyString(args, "nodePath") ?? "/root",
|
|
37856
38190
|
limit: readPositiveNumber(args, "limit") ?? 100,
|
|
37857
|
-
...property === undefined ? {} : { property }
|
|
38191
|
+
...property === undefined ? {} : { property },
|
|
38192
|
+
...includeHidden === undefined ? {} : { include_hidden: includeHidden }
|
|
37858
38193
|
});
|
|
37859
38194
|
}
|
|
37860
38195
|
async handleRuntimeWait(op, args) {
|
|
@@ -37905,6 +38240,7 @@ var init_server3 = __esm(() => {
|
|
|
37905
38240
|
init_ports();
|
|
37906
38241
|
init_project_scan();
|
|
37907
38242
|
init_resources();
|
|
38243
|
+
init_run_record();
|
|
37908
38244
|
init_runtime_client();
|
|
37909
38245
|
init_server_version();
|
|
37910
38246
|
init_setup();
|
|
@@ -37975,7 +38311,7 @@ var init_server3 = __esm(() => {
|
|
|
37975
38311
|
|
|
37976
38312
|
// src/cli.ts
|
|
37977
38313
|
import { existsSync as existsSync13 } from "fs";
|
|
37978
|
-
import { join as
|
|
38314
|
+
import { join as join17, resolve as resolve5 } from "path";
|
|
37979
38315
|
|
|
37980
38316
|
// src/errors.ts
|
|
37981
38317
|
class Refusal extends Error {
|
|
@@ -39366,6 +39702,11 @@ them. \`editor_rescan\` asks it to scan and names any class it still cannot reso
|
|
|
39366
39702
|
declaration and the cache can both be right while the editor stays blind to it. Change the
|
|
39367
39703
|
declaring script, or restart the editor.
|
|
39368
39704
|
|
|
39705
|
+
A diagnostic about a member rather than a type, \`Static function "x()" not found in base "Y"\`
|
|
39706
|
+
about something the engine compiles, is a different thing and is no longer stale here: each ask
|
|
39707
|
+
gives the document back when its answer arrives, so the next one is read off disk. If one turns
|
|
39708
|
+
up anyway, believe the run over the diagnostic and say so.
|
|
39709
|
+
|
|
39369
39710
|
## Refusals are useful
|
|
39370
39711
|
|
|
39371
39712
|
An unknown op, an argument the tool does not name, or a missing required one is refused with the
|
|
@@ -39478,7 +39819,7 @@ class UsageError extends Refusal {
|
|
|
39478
39819
|
function projectArgument(at) {
|
|
39479
39820
|
const named = args.slice(at).find((value) => !value.startsWith("--"));
|
|
39480
39821
|
const projectPath = resolve5(named ?? ".");
|
|
39481
|
-
if (!existsSync13(
|
|
39822
|
+
if (!existsSync13(join17(projectPath, "project.godot"))) {
|
|
39482
39823
|
throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
|
|
39483
39824
|
}
|
|
39484
39825
|
return projectPath;
|