gdharness 0.13.7 → 0.13.9
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 +151 -19
- package/build/index.js +151 -19
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -14896,6 +14896,15 @@ function isValueComplete(value) {
|
|
|
14896
14896
|
}
|
|
14897
14897
|
return depth <= 0 && !inString;
|
|
14898
14898
|
}
|
|
14899
|
+
function settingKeys(content) {
|
|
14900
|
+
const named = new Set;
|
|
14901
|
+
for (const [section, entries] of Object.entries(parseProjectGodot(content))) {
|
|
14902
|
+
for (const key of Object.keys(entries)) {
|
|
14903
|
+
named.add(`${section}/${key}`);
|
|
14904
|
+
}
|
|
14905
|
+
}
|
|
14906
|
+
return named;
|
|
14907
|
+
}
|
|
14899
14908
|
function parseProjectGodot(content) {
|
|
14900
14909
|
const result = emptyRecord();
|
|
14901
14910
|
let section = emptyRecord();
|
|
@@ -15730,8 +15739,8 @@ var init_tool_definitions = __esm(() => {
|
|
|
15730
15739
|
},
|
|
15731
15740
|
{
|
|
15732
15741
|
name: "editor_launch",
|
|
15733
|
-
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.",
|
|
15734
|
-
parameters: { projectPath: PROJECT_PATH },
|
|
15742
|
+
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. A restart saves project.godot on the way out, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped: settingsDropped names any that went, because nothing else will say so until something depends on one.",
|
|
15743
|
+
parameters: { projectPath: { ...PROJECT_PATH, ops: ["open"] } },
|
|
15735
15744
|
requires: [],
|
|
15736
15745
|
operations: {
|
|
15737
15746
|
open: { summary: "open the editor on a project", requires: ["projectPath"] },
|
|
@@ -15743,7 +15752,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15743
15752
|
name: "editor_run",
|
|
15744
15753
|
description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. What runs is a scene: a SceneTree script is not an entry point here, so put the script on the root of a scene of its own and name that in scene. args hands the game its own flags, the ones it reads with OS.get_cmdline_user_args(), and a run carrying any is started by this server rather than by the editor. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way. A start waits for the game to become something the runtime_* tools can talk to and says which it is under runtime: listening with the port it took, or why not, so the first call after a start does not have to be made twice. When it is not listening, mayYetAnnounce says whether that is final: false is a runtime that is not coming, true is a game still on its way up, which editor_status will see and runtimeWaitMs waits longer for. A start this server made also answers with transcript, the file both the run's streams are written to, so a watch on it can be armed off the start rather than off a second call.",
|
|
15745
15754
|
parameters: {
|
|
15746
|
-
projectPath: PROJECT_PATH,
|
|
15755
|
+
projectPath: { ...PROJECT_PATH, ops: ["start", "check"] },
|
|
15747
15756
|
scene: {
|
|
15748
15757
|
type: "string",
|
|
15749
15758
|
ops: ["start", "check"],
|
|
@@ -15772,11 +15781,17 @@ var init_tool_definitions = __esm(() => {
|
|
|
15772
15781
|
description: "check: how long to give the boot before it is called hung. Default 60000. wait: how long to wait for the run to end before answering anyway. Default 600000."
|
|
15773
15782
|
}
|
|
15774
15783
|
},
|
|
15775
|
-
requires: [
|
|
15784
|
+
requires: [],
|
|
15776
15785
|
operations: {
|
|
15777
|
-
start: {
|
|
15786
|
+
start: {
|
|
15787
|
+
summary: "run the project until it quits or is stopped",
|
|
15788
|
+
requires: ["projectPath"]
|
|
15789
|
+
},
|
|
15778
15790
|
stop: { summary: "end the run and answer with what it printed last", requires: [] },
|
|
15779
|
-
check: {
|
|
15791
|
+
check: {
|
|
15792
|
+
summary: "boot headless, quit after a few frames, and report the verdict",
|
|
15793
|
+
requires: ["projectPath"]
|
|
15794
|
+
},
|
|
15780
15795
|
wait: { summary: "wait for the run to end, then answer as editor_output does", requires: [] }
|
|
15781
15796
|
},
|
|
15782
15797
|
defaultOperation: "start"
|
|
@@ -31159,6 +31174,7 @@ class GodotDAPClient {
|
|
|
31159
31174
|
pendingRequests;
|
|
31160
31175
|
reader = new FrameReader;
|
|
31161
31176
|
outputBuffer = [];
|
|
31177
|
+
onOutput = null;
|
|
31162
31178
|
maxOutputLines = 1000;
|
|
31163
31179
|
initialized = false;
|
|
31164
31180
|
attached = false;
|
|
@@ -31326,6 +31342,13 @@ class GodotDAPClient {
|
|
|
31326
31342
|
if (outputText.length > 0) {
|
|
31327
31343
|
const lines = outputText.split(/\r?\n/).filter((line) => line.length > 0);
|
|
31328
31344
|
this.outputBuffer.push(...lines);
|
|
31345
|
+
if (this.onOutput !== null) {
|
|
31346
|
+
for (const line of lines) {
|
|
31347
|
+
try {
|
|
31348
|
+
this.onOutput(line);
|
|
31349
|
+
} catch {}
|
|
31350
|
+
}
|
|
31351
|
+
}
|
|
31329
31352
|
}
|
|
31330
31353
|
if (this.outputBuffer.length > this.maxOutputLines) {
|
|
31331
31354
|
this.outputBuffer = this.outputBuffer.slice(this.outputBuffer.length - this.maxOutputLines);
|
|
@@ -31382,6 +31405,9 @@ class GodotDAPClient {
|
|
|
31382
31405
|
await this.sendRequest("configurationDone", {});
|
|
31383
31406
|
this.attached = true;
|
|
31384
31407
|
}
|
|
31408
|
+
setOutputSink(sink) {
|
|
31409
|
+
this.onOutput = sink;
|
|
31410
|
+
}
|
|
31385
31411
|
getOutput(clear = false) {
|
|
31386
31412
|
const lines = [...this.outputBuffer];
|
|
31387
31413
|
if (clear) {
|
|
@@ -36321,6 +36347,41 @@ function writeRunRecord(record) {
|
|
|
36321
36347
|
mkdirSync6(runsDirectory(), { recursive: true });
|
|
36322
36348
|
writeFileSync8(recordPath(), JSON.stringify(record, null, 2), "utf8");
|
|
36323
36349
|
}
|
|
36350
|
+
function editorNotePath() {
|
|
36351
|
+
return join15(runsDirectory(), "editor-run.json");
|
|
36352
|
+
}
|
|
36353
|
+
function writeEditorRunNote(note) {
|
|
36354
|
+
mkdirSync6(runsDirectory(), { recursive: true });
|
|
36355
|
+
writeFileSync8(editorNotePath(), JSON.stringify(note, null, 2), "utf8");
|
|
36356
|
+
}
|
|
36357
|
+
function readEditorRunNote() {
|
|
36358
|
+
for (const directory of runtimeDirectories()) {
|
|
36359
|
+
const note = editorNoteAt(join15(directory, "runs", "editor-run.json"));
|
|
36360
|
+
if (note !== null) {
|
|
36361
|
+
return note;
|
|
36362
|
+
}
|
|
36363
|
+
}
|
|
36364
|
+
return null;
|
|
36365
|
+
}
|
|
36366
|
+
function editorNoteAt(path) {
|
|
36367
|
+
let parsed;
|
|
36368
|
+
try {
|
|
36369
|
+
parsed = JSON.parse(readFileSync13(path, "utf8"));
|
|
36370
|
+
} catch {
|
|
36371
|
+
return null;
|
|
36372
|
+
}
|
|
36373
|
+
if (!parsed || typeof parsed !== "object") {
|
|
36374
|
+
return null;
|
|
36375
|
+
}
|
|
36376
|
+
const fields = parsed;
|
|
36377
|
+
const projectPath = fields["projectPath"];
|
|
36378
|
+
const transcript = fields["transcript"];
|
|
36379
|
+
const startedAt = fields["startedAt"];
|
|
36380
|
+
if (typeof projectPath !== "string" || typeof transcript !== "string" || typeof startedAt !== "number") {
|
|
36381
|
+
return null;
|
|
36382
|
+
}
|
|
36383
|
+
return { projectPath, transcript, startedAt };
|
|
36384
|
+
}
|
|
36324
36385
|
function recordRunEnded(pid, exitCode) {
|
|
36325
36386
|
const path = recordPath();
|
|
36326
36387
|
const record = recordAt(path);
|
|
@@ -36513,6 +36574,7 @@ var init_setup = __esm(() => {
|
|
|
36513
36574
|
import { execFile as execFile6, spawn } from "node:child_process";
|
|
36514
36575
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
36515
36576
|
import {
|
|
36577
|
+
appendFileSync,
|
|
36516
36578
|
closeSync,
|
|
36517
36579
|
existsSync as existsSync14,
|
|
36518
36580
|
fstatSync,
|
|
@@ -37076,9 +37138,10 @@ class GodotServer {
|
|
|
37076
37138
|
if (op !== null) {
|
|
37077
37139
|
const elsewhere = Object.keys(args).filter((key) => key !== "op" && !opTakes(spec, op, key));
|
|
37078
37140
|
if (elsewhere.length > 0) {
|
|
37141
|
+
const takes = argumentsOf(spec, op);
|
|
37079
37142
|
return {
|
|
37080
37143
|
ok: false,
|
|
37081
|
-
response: this.createErrorResponse(`${spec.name} ${op} does not take ${elsewhere.join(", ")}. ${op} takes: ${
|
|
37144
|
+
response: this.createErrorResponse(`${spec.name} ${op} does not take ${elsewhere.join(", ")}. ${takes.length === 0 ? `${op} takes no arguments.` : `${op} takes: ${takes.join(", ")}.`}`)
|
|
37082
37145
|
};
|
|
37083
37146
|
}
|
|
37084
37147
|
}
|
|
@@ -37828,9 +37891,27 @@ class GodotServer {
|
|
|
37828
37891
|
if (this.dapClient !== null && this.dapClient.port !== port) {
|
|
37829
37892
|
this.dapClient = null;
|
|
37830
37893
|
}
|
|
37831
|
-
this.dapClient
|
|
37894
|
+
if (this.dapClient === null) {
|
|
37895
|
+
this.dapClient = new GodotDAPClient(port);
|
|
37896
|
+
this.dapClient.setOutputSink((line) => {
|
|
37897
|
+
this.writeToTranscript(line);
|
|
37898
|
+
});
|
|
37899
|
+
}
|
|
37832
37900
|
return this.dapClient;
|
|
37833
37901
|
}
|
|
37902
|
+
writeToTranscript(line) {
|
|
37903
|
+
const run = this.activeProcess;
|
|
37904
|
+
if (run?.throughEditor !== true || run.transcript === null) {
|
|
37905
|
+
return;
|
|
37906
|
+
}
|
|
37907
|
+
try {
|
|
37908
|
+
appendFileSync(run.transcript, line.endsWith(`
|
|
37909
|
+
`) ? line : `${line}
|
|
37910
|
+
`);
|
|
37911
|
+
} catch (error) {
|
|
37912
|
+
this.logDebug(`Could not write an editor-played run's transcript: ${errorMessage(error)}`);
|
|
37913
|
+
}
|
|
37914
|
+
}
|
|
37834
37915
|
editorServes(field, variable, fallback) {
|
|
37835
37916
|
const named = portFromEnvOrNull(variable);
|
|
37836
37917
|
if (named !== null) {
|
|
@@ -37906,6 +37987,16 @@ class GodotServer {
|
|
|
37906
37987
|
}
|
|
37907
37988
|
});
|
|
37908
37989
|
}
|
|
37990
|
+
settingKeysOf(projectPath) {
|
|
37991
|
+
if (projectPath === undefined) {
|
|
37992
|
+
return new Set;
|
|
37993
|
+
}
|
|
37994
|
+
try {
|
|
37995
|
+
return settingKeys(readFileSync15(join17(projectPath, "project.godot"), "utf8"));
|
|
37996
|
+
} catch {
|
|
37997
|
+
return new Set;
|
|
37998
|
+
}
|
|
37999
|
+
}
|
|
37909
38000
|
async handleRestartEditor() {
|
|
37910
38001
|
const before = this.godotBridge.getStatus();
|
|
37911
38002
|
if (!before.connected) {
|
|
@@ -37914,6 +38005,7 @@ class GodotServer {
|
|
|
37914
38005
|
"editor_status says whether the bridge is up and what has reached it"
|
|
37915
38006
|
]);
|
|
37916
38007
|
}
|
|
38008
|
+
const settingsBefore = this.settingKeysOf(before.projectPath);
|
|
37917
38009
|
const mine = before.openedByAServer === true && before.projectPath !== undefined;
|
|
37918
38010
|
const asked = mine ? await this.startItAgain(before.projectPath ?? "", before.editorPid) : await this.handleViaBridge("restart_editor", {});
|
|
37919
38011
|
if (asked.isError === true) {
|
|
@@ -37933,6 +38025,8 @@ class GodotServer {
|
|
|
37933
38025
|
]);
|
|
37934
38026
|
}
|
|
37935
38027
|
const now = this.godotBridge.getStatus();
|
|
38028
|
+
const after = this.settingKeysOf(before.projectPath);
|
|
38029
|
+
const dropped = [...settingsBefore].filter((key) => !after.has(key));
|
|
37936
38030
|
return this.jsonTextResponse({
|
|
37937
38031
|
restarted: true,
|
|
37938
38032
|
editorPid: now.editorPid,
|
|
@@ -37940,6 +38034,8 @@ class GodotServer {
|
|
|
37940
38034
|
serverVersion: SERVER_VERSION,
|
|
37941
38035
|
addonIsStale: now.addonVersion !== SERVER_VERSION,
|
|
37942
38036
|
staleNote: addonMismatch(now.addonVersion, SERVER_VERSION),
|
|
38037
|
+
settingsDropped: dropped.length > 0 ? dropped : undefined,
|
|
38038
|
+
settingsNote: dropped.length > 0 ? 'The editor saved project.godot on its way out and these keys are no longer in it. Godot writes only what differs from its own defaults, so a key named deliberately at its default value is redundant to the editor and is dropped on save. If any of them were pinned on purpose, to keep "set to this on purpose" and "not set" apart, put them back: nothing else will say they have gone until something depends on one.' : undefined,
|
|
37943
38039
|
tookMs: Date.now() - began
|
|
37944
38040
|
});
|
|
37945
38041
|
}
|
|
@@ -38130,6 +38226,7 @@ class GodotServer {
|
|
|
38130
38226
|
const log = new GameLog;
|
|
38131
38227
|
try {
|
|
38132
38228
|
await this.dap().connect();
|
|
38229
|
+
this.dapClient?.getOutput(true);
|
|
38133
38230
|
} catch (error) {
|
|
38134
38231
|
return this.createErrorResponse(`The editor is connected but its debug adapter is not: ${errorMessage(error)}`, [
|
|
38135
38232
|
`This asked on port ${this.dap().port}, which is where the connected editor says it serves`,
|
|
@@ -38140,19 +38237,24 @@ class GodotServer {
|
|
|
38140
38237
|
if (answer.isError === true) {
|
|
38141
38238
|
return answer;
|
|
38142
38239
|
}
|
|
38240
|
+
const startedAt = Date.now();
|
|
38241
|
+
const transcript = openTranscript(startedAt);
|
|
38242
|
+
closeSync(transcript.fd);
|
|
38143
38243
|
const played = {
|
|
38144
38244
|
process: null,
|
|
38145
38245
|
pid: null,
|
|
38146
38246
|
log,
|
|
38147
|
-
transcript:
|
|
38247
|
+
transcript: transcript.path,
|
|
38148
38248
|
readOffset: 0,
|
|
38149
38249
|
projectPath,
|
|
38150
|
-
startedAt
|
|
38250
|
+
startedAt,
|
|
38151
38251
|
exitCode: null,
|
|
38152
38252
|
throughEditor: true,
|
|
38153
38253
|
brokeOn: null
|
|
38154
38254
|
};
|
|
38155
38255
|
this.activeProcess = played;
|
|
38256
|
+
writeEditorRunNote({ projectPath, transcript: transcript.path, startedAt });
|
|
38257
|
+
sweepTranscripts();
|
|
38156
38258
|
return this.jsonTextResponse({
|
|
38157
38259
|
started: true,
|
|
38158
38260
|
through: "editor",
|
|
@@ -38168,10 +38270,15 @@ class GodotServer {
|
|
|
38168
38270
|
if (!game.throughEditor || !this.dapClient) {
|
|
38169
38271
|
return;
|
|
38170
38272
|
}
|
|
38171
|
-
|
|
38172
|
-
|
|
38273
|
+
if (game.transcript === null) {
|
|
38274
|
+
for (const line of this.dapClient.getOutput(true)) {
|
|
38275
|
+
game.log.append("stdout", line.endsWith(`
|
|
38173
38276
|
`) ? line : `${line}
|
|
38174
38277
|
`);
|
|
38278
|
+
}
|
|
38279
|
+
}
|
|
38280
|
+
if (!this.dapClient.isConnected()) {
|
|
38281
|
+
game.consoleLost = true;
|
|
38175
38282
|
}
|
|
38176
38283
|
this.recordWhatItBrokeOn(game);
|
|
38177
38284
|
}
|
|
@@ -38325,7 +38432,20 @@ class GodotServer {
|
|
|
38325
38432
|
}
|
|
38326
38433
|
}
|
|
38327
38434
|
async pickUpWhatTheEditorIsPlaying() {
|
|
38328
|
-
if (
|
|
38435
|
+
if (!this.godotBridge.isConnected()) {
|
|
38436
|
+
return;
|
|
38437
|
+
}
|
|
38438
|
+
const going = this.activeProcess;
|
|
38439
|
+
if (going !== null) {
|
|
38440
|
+
if (going.throughEditor && going.consoleLost === true && (await this.editorPlayingState())?.playing) {
|
|
38441
|
+
try {
|
|
38442
|
+
await this.dap().connect();
|
|
38443
|
+
going.log.append("stdout", `[gdharness] the debug adapter was reconnected here
|
|
38444
|
+
`);
|
|
38445
|
+
} catch (error) {
|
|
38446
|
+
this.logDebug(`Could not reconnect the adapter for a playing run: ${errorMessage(error)}`);
|
|
38447
|
+
}
|
|
38448
|
+
}
|
|
38329
38449
|
return;
|
|
38330
38450
|
}
|
|
38331
38451
|
const playing = await this.editorPlayingState();
|
|
@@ -38337,19 +38457,23 @@ class GodotServer {
|
|
|
38337
38457
|
} catch (error) {
|
|
38338
38458
|
this.logDebug(`Picked up an editor-played run without its adapter: ${errorMessage(error)}`);
|
|
38339
38459
|
}
|
|
38340
|
-
this.
|
|
38460
|
+
const project = this.godotBridge.getStatus().projectPath ?? null;
|
|
38461
|
+
const left = readEditorRunNote();
|
|
38462
|
+
const note = left !== null && project !== null && isSameDirectory(left.projectPath, project) ? left : null;
|
|
38463
|
+
const picked = {
|
|
38341
38464
|
process: null,
|
|
38342
38465
|
pid: null,
|
|
38343
38466
|
log: new GameLog,
|
|
38344
|
-
transcript: null,
|
|
38467
|
+
transcript: note?.transcript ?? null,
|
|
38345
38468
|
readOffset: 0,
|
|
38346
|
-
projectPath:
|
|
38347
|
-
startedAt: Date.now(),
|
|
38469
|
+
projectPath: project,
|
|
38470
|
+
startedAt: note?.startedAt ?? Date.now(),
|
|
38348
38471
|
exitCode: null,
|
|
38349
38472
|
throughEditor: true,
|
|
38350
38473
|
brokeOn: null,
|
|
38351
38474
|
pickedUpPlaying: true
|
|
38352
38475
|
};
|
|
38476
|
+
this.activeProcess = picked;
|
|
38353
38477
|
}
|
|
38354
38478
|
adoptRecordedRun() {
|
|
38355
38479
|
const record = readRunRecord();
|
|
@@ -38469,6 +38593,9 @@ class GodotServer {
|
|
|
38469
38593
|
const wanted = readBoolean(args, "cpu") ?? false;
|
|
38470
38594
|
const cpuSeconds = wanted && run.pid !== null && stillRunning(run) ? await cpuSecondsOf(run.pid) : undefined;
|
|
38471
38595
|
const notes = [];
|
|
38596
|
+
if (wanted && cpuSeconds === undefined && stillRunning(run)) {
|
|
38597
|
+
notes.push(run.pid === null ? "cpu was asked for and there is no process to ask: the editor is playing this run and holds it, so nothing here has its process id. editor_run start without a connected editor, or with args, starts the game here instead and that run answers cpuSeconds." : "cpu was asked for and this platform would not say what the run has used.");
|
|
38598
|
+
}
|
|
38472
38599
|
if (waitedMs !== undefined) {
|
|
38473
38600
|
notes.push(`The wait of ${waitedMs}ms ran out and the run is still going, so this is what it had printed by then rather than everything it will print. editor_run wait again to keep waiting.`);
|
|
38474
38601
|
}
|
|
@@ -38476,7 +38603,11 @@ class GodotServer {
|
|
|
38476
38603
|
notes.push("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.");
|
|
38477
38604
|
}
|
|
38478
38605
|
if (run.pickedUpPlaying === true) {
|
|
38479
|
-
notes.push("The editor was already playing this when this server reached it, so this is not the run from its start: what it printed before that
|
|
38606
|
+
notes.push(run.transcript === null ? "The editor was already playing this when this server reached it, and the server that started it left no transcript, so this is not the run from its start: what it printed before that is only in the editor. It is the run the editor is holding now, which is the one the debug_* tools answer for." : "The editor was already playing this when this server reached it, and what it printed before that has been read back from its transcript. It is the run the editor is holding now, which is the one the debug_* tools answer for.");
|
|
38607
|
+
}
|
|
38608
|
+
if (run.consoleLost === true) {
|
|
38609
|
+
const back = this.dapClient?.isConnected() === true;
|
|
38610
|
+
notes.push(`The debug adapter this run's console arrives over went away while the run was going, so what is below has a hole in it and the counts are of what was heard rather than of what was printed. ${back ? "It is connected again, and the line marking where is in the log." : "It is still gone, so nothing further will arrive here."} The project's own user://logs/godot.log has the rest, and "no entries" here means "not heard" rather than a quiet run.`);
|
|
38480
38611
|
}
|
|
38481
38612
|
if (typeof run.endedHere === "string") {
|
|
38482
38613
|
notes.push(`This run was ended here, by ${run.endedHere}.`);
|
|
@@ -38493,7 +38624,8 @@ class GodotServer {
|
|
|
38493
38624
|
pid: run.pid,
|
|
38494
38625
|
errors: run.log.count("error"),
|
|
38495
38626
|
warnings: run.log.count("warning"),
|
|
38496
|
-
clean: run.log.count("error") === 0,
|
|
38627
|
+
clean: run.consoleLost === true ? undefined : run.log.count("error") === 0,
|
|
38628
|
+
consoleLost: run.consoleLost === true ? true : undefined,
|
|
38497
38629
|
heldAt: halt,
|
|
38498
38630
|
endedUnwatched: run.endedUnwatched === true ? true : undefined,
|
|
38499
38631
|
endedBy: stillRunning(run) ? undefined : run.endedHere ?? null,
|
package/build/index.js
CHANGED
|
@@ -10142,6 +10142,7 @@ function defectReport(where, error, godotVersion) {
|
|
|
10142
10142
|
import { execFile as execFile4, spawn } from "node:child_process";
|
|
10143
10143
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
10144
10144
|
import {
|
|
10145
|
+
appendFileSync,
|
|
10145
10146
|
closeSync,
|
|
10146
10147
|
existsSync as existsSync9,
|
|
10147
10148
|
fstatSync,
|
|
@@ -25009,6 +25010,7 @@ class GodotDAPClient {
|
|
|
25009
25010
|
pendingRequests;
|
|
25010
25011
|
reader = new FrameReader;
|
|
25011
25012
|
outputBuffer = [];
|
|
25013
|
+
onOutput = null;
|
|
25012
25014
|
maxOutputLines = 1000;
|
|
25013
25015
|
initialized = false;
|
|
25014
25016
|
attached = false;
|
|
@@ -25176,6 +25178,13 @@ class GodotDAPClient {
|
|
|
25176
25178
|
if (outputText.length > 0) {
|
|
25177
25179
|
const lines = outputText.split(/\r?\n/).filter((line) => line.length > 0);
|
|
25178
25180
|
this.outputBuffer.push(...lines);
|
|
25181
|
+
if (this.onOutput !== null) {
|
|
25182
|
+
for (const line of lines) {
|
|
25183
|
+
try {
|
|
25184
|
+
this.onOutput(line);
|
|
25185
|
+
} catch {}
|
|
25186
|
+
}
|
|
25187
|
+
}
|
|
25179
25188
|
}
|
|
25180
25189
|
if (this.outputBuffer.length > this.maxOutputLines) {
|
|
25181
25190
|
this.outputBuffer = this.outputBuffer.slice(this.outputBuffer.length - this.maxOutputLines);
|
|
@@ -25232,6 +25241,9 @@ class GodotDAPClient {
|
|
|
25232
25241
|
await this.sendRequest("configurationDone", {});
|
|
25233
25242
|
this.attached = true;
|
|
25234
25243
|
}
|
|
25244
|
+
setOutputSink(sink) {
|
|
25245
|
+
this.onOutput = sink;
|
|
25246
|
+
}
|
|
25235
25247
|
getOutput(clear = false) {
|
|
25236
25248
|
const lines = [...this.outputBuffer];
|
|
25237
25249
|
if (clear) {
|
|
@@ -27545,6 +27557,15 @@ function isValueComplete(value) {
|
|
|
27545
27557
|
}
|
|
27546
27558
|
return depth <= 0 && !inString;
|
|
27547
27559
|
}
|
|
27560
|
+
function settingKeys(content) {
|
|
27561
|
+
const named = new Set;
|
|
27562
|
+
for (const [section, entries] of Object.entries(parseProjectGodot(content))) {
|
|
27563
|
+
for (const key of Object.keys(entries)) {
|
|
27564
|
+
named.add(`${section}/${key}`);
|
|
27565
|
+
}
|
|
27566
|
+
}
|
|
27567
|
+
return named;
|
|
27568
|
+
}
|
|
27548
27569
|
function parseProjectGodot(content) {
|
|
27549
27570
|
const result = emptyRecord();
|
|
27550
27571
|
let section = emptyRecord();
|
|
@@ -27973,6 +27994,41 @@ function writeRunRecord(record) {
|
|
|
27973
27994
|
mkdirSync3(runsDirectory(), { recursive: true });
|
|
27974
27995
|
writeFileSync4(recordPath(), JSON.stringify(record, null, 2), "utf8");
|
|
27975
27996
|
}
|
|
27997
|
+
function editorNotePath() {
|
|
27998
|
+
return join9(runsDirectory(), "editor-run.json");
|
|
27999
|
+
}
|
|
28000
|
+
function writeEditorRunNote(note) {
|
|
28001
|
+
mkdirSync3(runsDirectory(), { recursive: true });
|
|
28002
|
+
writeFileSync4(editorNotePath(), JSON.stringify(note, null, 2), "utf8");
|
|
28003
|
+
}
|
|
28004
|
+
function readEditorRunNote() {
|
|
28005
|
+
for (const directory of runtimeDirectories()) {
|
|
28006
|
+
const note = editorNoteAt(join9(directory, "runs", "editor-run.json"));
|
|
28007
|
+
if (note !== null) {
|
|
28008
|
+
return note;
|
|
28009
|
+
}
|
|
28010
|
+
}
|
|
28011
|
+
return null;
|
|
28012
|
+
}
|
|
28013
|
+
function editorNoteAt(path) {
|
|
28014
|
+
let parsed;
|
|
28015
|
+
try {
|
|
28016
|
+
parsed = JSON.parse(readFileSync9(path, "utf8"));
|
|
28017
|
+
} catch {
|
|
28018
|
+
return null;
|
|
28019
|
+
}
|
|
28020
|
+
if (!parsed || typeof parsed !== "object") {
|
|
28021
|
+
return null;
|
|
28022
|
+
}
|
|
28023
|
+
const fields = parsed;
|
|
28024
|
+
const projectPath = fields["projectPath"];
|
|
28025
|
+
const transcript = fields["transcript"];
|
|
28026
|
+
const startedAt = fields["startedAt"];
|
|
28027
|
+
if (typeof projectPath !== "string" || typeof transcript !== "string" || typeof startedAt !== "number") {
|
|
28028
|
+
return null;
|
|
28029
|
+
}
|
|
28030
|
+
return { projectPath, transcript, startedAt };
|
|
28031
|
+
}
|
|
27976
28032
|
function recordRunEnded(pid, exitCode) {
|
|
27977
28033
|
const path = recordPath();
|
|
27978
28034
|
const record = recordAt(path);
|
|
@@ -28745,8 +28801,8 @@ var TOOL_SPECS = [
|
|
|
28745
28801
|
},
|
|
28746
28802
|
{
|
|
28747
28803
|
name: "editor_launch",
|
|
28748
|
-
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.",
|
|
28749
|
-
parameters: { projectPath: PROJECT_PATH },
|
|
28804
|
+
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. A restart saves project.godot on the way out, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped: settingsDropped names any that went, because nothing else will say so until something depends on one.",
|
|
28805
|
+
parameters: { projectPath: { ...PROJECT_PATH, ops: ["open"] } },
|
|
28750
28806
|
requires: [],
|
|
28751
28807
|
operations: {
|
|
28752
28808
|
open: { summary: "open the editor on a project", requires: ["projectPath"] },
|
|
@@ -28758,7 +28814,7 @@ var TOOL_SPECS = [
|
|
|
28758
28814
|
name: "editor_run",
|
|
28759
28815
|
description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. What runs is a scene: a SceneTree script is not an entry point here, so put the script on the root of a scene of its own and name that in scene. args hands the game its own flags, the ones it reads with OS.get_cmdline_user_args(), and a run carrying any is started by this server rather than by the editor. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way. A start waits for the game to become something the runtime_* tools can talk to and says which it is under runtime: listening with the port it took, or why not, so the first call after a start does not have to be made twice. When it is not listening, mayYetAnnounce says whether that is final: false is a runtime that is not coming, true is a game still on its way up, which editor_status will see and runtimeWaitMs waits longer for. A start this server made also answers with transcript, the file both the run's streams are written to, so a watch on it can be armed off the start rather than off a second call.",
|
|
28760
28816
|
parameters: {
|
|
28761
|
-
projectPath: PROJECT_PATH,
|
|
28817
|
+
projectPath: { ...PROJECT_PATH, ops: ["start", "check"] },
|
|
28762
28818
|
scene: {
|
|
28763
28819
|
type: "string",
|
|
28764
28820
|
ops: ["start", "check"],
|
|
@@ -28787,11 +28843,17 @@ var TOOL_SPECS = [
|
|
|
28787
28843
|
description: "check: how long to give the boot before it is called hung. Default 60000. wait: how long to wait for the run to end before answering anyway. Default 600000."
|
|
28788
28844
|
}
|
|
28789
28845
|
},
|
|
28790
|
-
requires: [
|
|
28846
|
+
requires: [],
|
|
28791
28847
|
operations: {
|
|
28792
|
-
start: {
|
|
28848
|
+
start: {
|
|
28849
|
+
summary: "run the project until it quits or is stopped",
|
|
28850
|
+
requires: ["projectPath"]
|
|
28851
|
+
},
|
|
28793
28852
|
stop: { summary: "end the run and answer with what it printed last", requires: [] },
|
|
28794
|
-
check: {
|
|
28853
|
+
check: {
|
|
28854
|
+
summary: "boot headless, quit after a few frames, and report the verdict",
|
|
28855
|
+
requires: ["projectPath"]
|
|
28856
|
+
},
|
|
28795
28857
|
wait: { summary: "wait for the run to end, then answer as editor_output does", requires: [] }
|
|
28796
28858
|
},
|
|
28797
28859
|
defaultOperation: "start"
|
|
@@ -29830,9 +29892,10 @@ class GodotServer {
|
|
|
29830
29892
|
if (op !== null) {
|
|
29831
29893
|
const elsewhere = Object.keys(args).filter((key) => key !== "op" && !opTakes(spec, op, key));
|
|
29832
29894
|
if (elsewhere.length > 0) {
|
|
29895
|
+
const takes = argumentsOf(spec, op);
|
|
29833
29896
|
return {
|
|
29834
29897
|
ok: false,
|
|
29835
|
-
response: this.createErrorResponse(`${spec.name} ${op} does not take ${elsewhere.join(", ")}. ${op} takes: ${
|
|
29898
|
+
response: this.createErrorResponse(`${spec.name} ${op} does not take ${elsewhere.join(", ")}. ${takes.length === 0 ? `${op} takes no arguments.` : `${op} takes: ${takes.join(", ")}.`}`)
|
|
29836
29899
|
};
|
|
29837
29900
|
}
|
|
29838
29901
|
}
|
|
@@ -30582,9 +30645,27 @@ class GodotServer {
|
|
|
30582
30645
|
if (this.dapClient !== null && this.dapClient.port !== port) {
|
|
30583
30646
|
this.dapClient = null;
|
|
30584
30647
|
}
|
|
30585
|
-
this.dapClient
|
|
30648
|
+
if (this.dapClient === null) {
|
|
30649
|
+
this.dapClient = new GodotDAPClient(port);
|
|
30650
|
+
this.dapClient.setOutputSink((line) => {
|
|
30651
|
+
this.writeToTranscript(line);
|
|
30652
|
+
});
|
|
30653
|
+
}
|
|
30586
30654
|
return this.dapClient;
|
|
30587
30655
|
}
|
|
30656
|
+
writeToTranscript(line) {
|
|
30657
|
+
const run = this.activeProcess;
|
|
30658
|
+
if (run?.throughEditor !== true || run.transcript === null) {
|
|
30659
|
+
return;
|
|
30660
|
+
}
|
|
30661
|
+
try {
|
|
30662
|
+
appendFileSync(run.transcript, line.endsWith(`
|
|
30663
|
+
`) ? line : `${line}
|
|
30664
|
+
`);
|
|
30665
|
+
} catch (error) {
|
|
30666
|
+
this.logDebug(`Could not write an editor-played run's transcript: ${errorMessage(error)}`);
|
|
30667
|
+
}
|
|
30668
|
+
}
|
|
30588
30669
|
editorServes(field, variable, fallback) {
|
|
30589
30670
|
const named = portFromEnvOrNull(variable);
|
|
30590
30671
|
if (named !== null) {
|
|
@@ -30660,6 +30741,16 @@ class GodotServer {
|
|
|
30660
30741
|
}
|
|
30661
30742
|
});
|
|
30662
30743
|
}
|
|
30744
|
+
settingKeysOf(projectPath) {
|
|
30745
|
+
if (projectPath === undefined) {
|
|
30746
|
+
return new Set;
|
|
30747
|
+
}
|
|
30748
|
+
try {
|
|
30749
|
+
return settingKeys(readFileSync11(join11(projectPath, "project.godot"), "utf8"));
|
|
30750
|
+
} catch {
|
|
30751
|
+
return new Set;
|
|
30752
|
+
}
|
|
30753
|
+
}
|
|
30663
30754
|
async handleRestartEditor() {
|
|
30664
30755
|
const before = this.godotBridge.getStatus();
|
|
30665
30756
|
if (!before.connected) {
|
|
@@ -30668,6 +30759,7 @@ class GodotServer {
|
|
|
30668
30759
|
"editor_status says whether the bridge is up and what has reached it"
|
|
30669
30760
|
]);
|
|
30670
30761
|
}
|
|
30762
|
+
const settingsBefore = this.settingKeysOf(before.projectPath);
|
|
30671
30763
|
const mine = before.openedByAServer === true && before.projectPath !== undefined;
|
|
30672
30764
|
const asked = mine ? await this.startItAgain(before.projectPath ?? "", before.editorPid) : await this.handleViaBridge("restart_editor", {});
|
|
30673
30765
|
if (asked.isError === true) {
|
|
@@ -30687,6 +30779,8 @@ class GodotServer {
|
|
|
30687
30779
|
]);
|
|
30688
30780
|
}
|
|
30689
30781
|
const now = this.godotBridge.getStatus();
|
|
30782
|
+
const after = this.settingKeysOf(before.projectPath);
|
|
30783
|
+
const dropped = [...settingsBefore].filter((key) => !after.has(key));
|
|
30690
30784
|
return this.jsonTextResponse({
|
|
30691
30785
|
restarted: true,
|
|
30692
30786
|
editorPid: now.editorPid,
|
|
@@ -30694,6 +30788,8 @@ class GodotServer {
|
|
|
30694
30788
|
serverVersion: SERVER_VERSION,
|
|
30695
30789
|
addonIsStale: now.addonVersion !== SERVER_VERSION,
|
|
30696
30790
|
staleNote: addonMismatch(now.addonVersion, SERVER_VERSION),
|
|
30791
|
+
settingsDropped: dropped.length > 0 ? dropped : undefined,
|
|
30792
|
+
settingsNote: dropped.length > 0 ? 'The editor saved project.godot on its way out and these keys are no longer in it. Godot writes only what differs from its own defaults, so a key named deliberately at its default value is redundant to the editor and is dropped on save. If any of them were pinned on purpose, to keep "set to this on purpose" and "not set" apart, put them back: nothing else will say they have gone until something depends on one.' : undefined,
|
|
30697
30793
|
tookMs: Date.now() - began
|
|
30698
30794
|
});
|
|
30699
30795
|
}
|
|
@@ -30884,6 +30980,7 @@ class GodotServer {
|
|
|
30884
30980
|
const log = new GameLog;
|
|
30885
30981
|
try {
|
|
30886
30982
|
await this.dap().connect();
|
|
30983
|
+
this.dapClient?.getOutput(true);
|
|
30887
30984
|
} catch (error) {
|
|
30888
30985
|
return this.createErrorResponse(`The editor is connected but its debug adapter is not: ${errorMessage(error)}`, [
|
|
30889
30986
|
`This asked on port ${this.dap().port}, which is where the connected editor says it serves`,
|
|
@@ -30894,19 +30991,24 @@ class GodotServer {
|
|
|
30894
30991
|
if (answer.isError === true) {
|
|
30895
30992
|
return answer;
|
|
30896
30993
|
}
|
|
30994
|
+
const startedAt = Date.now();
|
|
30995
|
+
const transcript = openTranscript(startedAt);
|
|
30996
|
+
closeSync(transcript.fd);
|
|
30897
30997
|
const played = {
|
|
30898
30998
|
process: null,
|
|
30899
30999
|
pid: null,
|
|
30900
31000
|
log,
|
|
30901
|
-
transcript:
|
|
31001
|
+
transcript: transcript.path,
|
|
30902
31002
|
readOffset: 0,
|
|
30903
31003
|
projectPath,
|
|
30904
|
-
startedAt
|
|
31004
|
+
startedAt,
|
|
30905
31005
|
exitCode: null,
|
|
30906
31006
|
throughEditor: true,
|
|
30907
31007
|
brokeOn: null
|
|
30908
31008
|
};
|
|
30909
31009
|
this.activeProcess = played;
|
|
31010
|
+
writeEditorRunNote({ projectPath, transcript: transcript.path, startedAt });
|
|
31011
|
+
sweepTranscripts();
|
|
30910
31012
|
return this.jsonTextResponse({
|
|
30911
31013
|
started: true,
|
|
30912
31014
|
through: "editor",
|
|
@@ -30922,10 +31024,15 @@ class GodotServer {
|
|
|
30922
31024
|
if (!game.throughEditor || !this.dapClient) {
|
|
30923
31025
|
return;
|
|
30924
31026
|
}
|
|
30925
|
-
|
|
30926
|
-
|
|
31027
|
+
if (game.transcript === null) {
|
|
31028
|
+
for (const line of this.dapClient.getOutput(true)) {
|
|
31029
|
+
game.log.append("stdout", line.endsWith(`
|
|
30927
31030
|
`) ? line : `${line}
|
|
30928
31031
|
`);
|
|
31032
|
+
}
|
|
31033
|
+
}
|
|
31034
|
+
if (!this.dapClient.isConnected()) {
|
|
31035
|
+
game.consoleLost = true;
|
|
30929
31036
|
}
|
|
30930
31037
|
this.recordWhatItBrokeOn(game);
|
|
30931
31038
|
}
|
|
@@ -31079,7 +31186,20 @@ class GodotServer {
|
|
|
31079
31186
|
}
|
|
31080
31187
|
}
|
|
31081
31188
|
async pickUpWhatTheEditorIsPlaying() {
|
|
31082
|
-
if (
|
|
31189
|
+
if (!this.godotBridge.isConnected()) {
|
|
31190
|
+
return;
|
|
31191
|
+
}
|
|
31192
|
+
const going = this.activeProcess;
|
|
31193
|
+
if (going !== null) {
|
|
31194
|
+
if (going.throughEditor && going.consoleLost === true && (await this.editorPlayingState())?.playing) {
|
|
31195
|
+
try {
|
|
31196
|
+
await this.dap().connect();
|
|
31197
|
+
going.log.append("stdout", `[gdharness] the debug adapter was reconnected here
|
|
31198
|
+
`);
|
|
31199
|
+
} catch (error) {
|
|
31200
|
+
this.logDebug(`Could not reconnect the adapter for a playing run: ${errorMessage(error)}`);
|
|
31201
|
+
}
|
|
31202
|
+
}
|
|
31083
31203
|
return;
|
|
31084
31204
|
}
|
|
31085
31205
|
const playing = await this.editorPlayingState();
|
|
@@ -31091,19 +31211,23 @@ class GodotServer {
|
|
|
31091
31211
|
} catch (error) {
|
|
31092
31212
|
this.logDebug(`Picked up an editor-played run without its adapter: ${errorMessage(error)}`);
|
|
31093
31213
|
}
|
|
31094
|
-
this.
|
|
31214
|
+
const project = this.godotBridge.getStatus().projectPath ?? null;
|
|
31215
|
+
const left = readEditorRunNote();
|
|
31216
|
+
const note = left !== null && project !== null && isSameDirectory(left.projectPath, project) ? left : null;
|
|
31217
|
+
const picked = {
|
|
31095
31218
|
process: null,
|
|
31096
31219
|
pid: null,
|
|
31097
31220
|
log: new GameLog,
|
|
31098
|
-
transcript: null,
|
|
31221
|
+
transcript: note?.transcript ?? null,
|
|
31099
31222
|
readOffset: 0,
|
|
31100
|
-
projectPath:
|
|
31101
|
-
startedAt: Date.now(),
|
|
31223
|
+
projectPath: project,
|
|
31224
|
+
startedAt: note?.startedAt ?? Date.now(),
|
|
31102
31225
|
exitCode: null,
|
|
31103
31226
|
throughEditor: true,
|
|
31104
31227
|
brokeOn: null,
|
|
31105
31228
|
pickedUpPlaying: true
|
|
31106
31229
|
};
|
|
31230
|
+
this.activeProcess = picked;
|
|
31107
31231
|
}
|
|
31108
31232
|
adoptRecordedRun() {
|
|
31109
31233
|
const record = readRunRecord();
|
|
@@ -31223,6 +31347,9 @@ class GodotServer {
|
|
|
31223
31347
|
const wanted = readBoolean(args, "cpu") ?? false;
|
|
31224
31348
|
const cpuSeconds = wanted && run.pid !== null && stillRunning(run) ? await cpuSecondsOf(run.pid) : undefined;
|
|
31225
31349
|
const notes = [];
|
|
31350
|
+
if (wanted && cpuSeconds === undefined && stillRunning(run)) {
|
|
31351
|
+
notes.push(run.pid === null ? "cpu was asked for and there is no process to ask: the editor is playing this run and holds it, so nothing here has its process id. editor_run start without a connected editor, or with args, starts the game here instead and that run answers cpuSeconds." : "cpu was asked for and this platform would not say what the run has used.");
|
|
31352
|
+
}
|
|
31226
31353
|
if (waitedMs !== undefined) {
|
|
31227
31354
|
notes.push(`The wait of ${waitedMs}ms ran out and the run is still going, so this is what it had printed by then rather than everything it will print. editor_run wait again to keep waiting.`);
|
|
31228
31355
|
}
|
|
@@ -31230,7 +31357,11 @@ class GodotServer {
|
|
|
31230
31357
|
notes.push("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.");
|
|
31231
31358
|
}
|
|
31232
31359
|
if (run.pickedUpPlaying === true) {
|
|
31233
|
-
notes.push("The editor was already playing this when this server reached it, so this is not the run from its start: what it printed before that
|
|
31360
|
+
notes.push(run.transcript === null ? "The editor was already playing this when this server reached it, and the server that started it left no transcript, so this is not the run from its start: what it printed before that is only in the editor. It is the run the editor is holding now, which is the one the debug_* tools answer for." : "The editor was already playing this when this server reached it, and what it printed before that has been read back from its transcript. It is the run the editor is holding now, which is the one the debug_* tools answer for.");
|
|
31361
|
+
}
|
|
31362
|
+
if (run.consoleLost === true) {
|
|
31363
|
+
const back = this.dapClient?.isConnected() === true;
|
|
31364
|
+
notes.push(`The debug adapter this run's console arrives over went away while the run was going, so what is below has a hole in it and the counts are of what was heard rather than of what was printed. ${back ? "It is connected again, and the line marking where is in the log." : "It is still gone, so nothing further will arrive here."} The project's own user://logs/godot.log has the rest, and "no entries" here means "not heard" rather than a quiet run.`);
|
|
31234
31365
|
}
|
|
31235
31366
|
if (typeof run.endedHere === "string") {
|
|
31236
31367
|
notes.push(`This run was ended here, by ${run.endedHere}.`);
|
|
@@ -31247,7 +31378,8 @@ class GodotServer {
|
|
|
31247
31378
|
pid: run.pid,
|
|
31248
31379
|
errors: run.log.count("error"),
|
|
31249
31380
|
warnings: run.log.count("warning"),
|
|
31250
|
-
clean: run.log.count("error") === 0,
|
|
31381
|
+
clean: run.consoleLost === true ? undefined : run.log.count("error") === 0,
|
|
31382
|
+
consoleLost: run.consoleLost === true ? true : undefined,
|
|
31251
31383
|
heldAt: halt,
|
|
31252
31384
|
endedUnwatched: run.endedUnwatched === true ? true : undefined,
|
|
31253
31385
|
endedBy: stillRunning(run) ? undefined : run.endedHere ?? null,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.9",
|
|
4
4
|
"mcpName": "io.github.Aureliolo/gdharness",
|
|
5
5
|
"description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
|
|
6
6
|
"type": "module",
|