gdharness 1.3.33 → 1.3.35
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
CHANGED
|
@@ -11712,7 +11712,16 @@ var init_whole_file = __esm(() => {
|
|
|
11712
11712
|
});
|
|
11713
11713
|
|
|
11714
11714
|
// src/engine-slots.ts
|
|
11715
|
-
import {
|
|
11715
|
+
import {
|
|
11716
|
+
closeSync as closeSync3,
|
|
11717
|
+
mkdirSync as mkdirSync3,
|
|
11718
|
+
openSync as openSync3,
|
|
11719
|
+
readFileSync as readFileSync7,
|
|
11720
|
+
rmSync as rmSync6,
|
|
11721
|
+
statSync as statSync6,
|
|
11722
|
+
watch,
|
|
11723
|
+
writeSync as writeSync2
|
|
11724
|
+
} from "node:fs";
|
|
11716
11725
|
import { availableParallelism as availableParallelism2, homedir as homedir2 } from "node:os";
|
|
11717
11726
|
import { join as join8, resolve as resolve4 } from "node:path";
|
|
11718
11727
|
import process7 from "node:process";
|
|
@@ -11746,6 +11755,51 @@ function machineSlots(asked = {
|
|
|
11746
11755
|
function nonEmpty(value) {
|
|
11747
11756
|
return value === undefined || value === "" ? undefined : value;
|
|
11748
11757
|
}
|
|
11758
|
+
|
|
11759
|
+
class FolderChanges {
|
|
11760
|
+
changed = false;
|
|
11761
|
+
wake = null;
|
|
11762
|
+
watcher;
|
|
11763
|
+
constructor(directory) {
|
|
11764
|
+
let watcher = null;
|
|
11765
|
+
try {
|
|
11766
|
+
watcher = watch(directory, () => {
|
|
11767
|
+
this.changed = true;
|
|
11768
|
+
this.wake?.();
|
|
11769
|
+
});
|
|
11770
|
+
watcher.on("error", () => {
|
|
11771
|
+
watcher?.close();
|
|
11772
|
+
});
|
|
11773
|
+
} catch {
|
|
11774
|
+
watcher = null;
|
|
11775
|
+
}
|
|
11776
|
+
this.watcher = watcher;
|
|
11777
|
+
}
|
|
11778
|
+
async next(lookMs, settleMs, signal) {
|
|
11779
|
+
if (!this.changed) {
|
|
11780
|
+
await new Promise((resolve) => {
|
|
11781
|
+
const done = () => {
|
|
11782
|
+
clearTimeout(timer);
|
|
11783
|
+
this.wake = null;
|
|
11784
|
+
signal?.removeEventListener("abort", done);
|
|
11785
|
+
resolve();
|
|
11786
|
+
};
|
|
11787
|
+
const timer = setTimeout(done, lookMs);
|
|
11788
|
+
this.wake = done;
|
|
11789
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
11790
|
+
});
|
|
11791
|
+
}
|
|
11792
|
+
if (this.changed && signal?.aborted !== true) {
|
|
11793
|
+
await delay3(settleMs, undefined, signal === undefined ? {} : { signal }).catch(() => {
|
|
11794
|
+
return;
|
|
11795
|
+
});
|
|
11796
|
+
}
|
|
11797
|
+
this.changed = false;
|
|
11798
|
+
}
|
|
11799
|
+
close() {
|
|
11800
|
+
this.watcher?.close();
|
|
11801
|
+
}
|
|
11802
|
+
}
|
|
11749
11803
|
function holderOf(content) {
|
|
11750
11804
|
try {
|
|
11751
11805
|
const parsed = JSON.parse(content);
|
|
@@ -11794,6 +11848,7 @@ class EngineSlots {
|
|
|
11794
11848
|
const began = Date.now();
|
|
11795
11849
|
let said = Number.NEGATIVE_INFINITY;
|
|
11796
11850
|
let mark;
|
|
11851
|
+
let changes;
|
|
11797
11852
|
try {
|
|
11798
11853
|
for (;; ) {
|
|
11799
11854
|
signal?.throwIfAborted();
|
|
@@ -11812,9 +11867,14 @@ class EngineSlots {
|
|
|
11812
11867
|
this.saidAt = now;
|
|
11813
11868
|
this.say(slotWaitSaid(now - began, this.count, this.directory));
|
|
11814
11869
|
}
|
|
11815
|
-
|
|
11870
|
+
if (changes === undefined) {
|
|
11871
|
+
changes = new FolderChanges(this.directory);
|
|
11872
|
+
continue;
|
|
11873
|
+
}
|
|
11874
|
+
await changes.next(SLOT_LOOK_MS, SLOT_SETTLE_MS, signal);
|
|
11816
11875
|
}
|
|
11817
11876
|
} finally {
|
|
11877
|
+
changes?.close();
|
|
11818
11878
|
if (mark !== undefined && this.marks.delete(mark)) {
|
|
11819
11879
|
removeWhole(mark);
|
|
11820
11880
|
}
|
|
@@ -11964,7 +12024,7 @@ async function inASlot(body) {
|
|
|
11964
12024
|
held.release();
|
|
11965
12025
|
}
|
|
11966
12026
|
}
|
|
11967
|
-
var SLOTS_VARIABLE = "GODOT_ENGINE_SLOTS", SLOT_COUNT_VARIABLE = "GODOT_ENGINE_SLOT_COUNT", PROCESSORS_A_SLOT = 5, START_TOLERANCE_MS = 2000, UNWRITTEN_MS = 1e4,
|
|
12027
|
+
var SLOTS_VARIABLE = "GODOT_ENGINE_SLOTS", SLOT_COUNT_VARIABLE = "GODOT_ENGINE_SLOT_COUNT", PROCESSORS_A_SLOT = 5, START_TOLERANCE_MS = 2000, UNWRITTEN_MS = 1e4, SLOT_LOOK_MS = 1000, SLOT_SETTLE_MS = 50, WAIT_SAID_MS = 20000, COMMAND_WAIT_SAID_MS = 2000, START_READ_MS = 1e4, WAITING_FOLDER = "waiting", waitsMarked = 0, ours, saidTo;
|
|
11968
12028
|
var init_engine_slots = __esm(() => {
|
|
11969
12029
|
init_alive();
|
|
11970
12030
|
init_call_signal();
|
|
@@ -17584,7 +17644,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17584
17644
|
title: "Editor status",
|
|
17585
17645
|
effect: "read",
|
|
17586
17646
|
description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and which games with the runtime addon are reachable, of this project and of every other on the machine. Read connected with mayYetConnect: the editor dials this server and backs off between tries, so for the first half-minute after a server starts false means not reached yet as often as no editor; mayYetConnect true is worth waiting out, false is final, and editorCannotConnect, restartInterrupted, restartUnderway, closedHere and editorsNotYetConnected say why. An editor that has connected and not yet greeted reports nothing about itself, and every other call made in that window waits up to ten seconds for it to settle. playingInEditor null means unanswered rather than nothing playing: processActive and runtimes do not depend on the editor answering. game.run names the run editor_output and editor_run stop answer for, and stays named after it ends. addonIsStale, on a connected editor and on each running game (absent with neither), says it runs addon code other than what this server ships, and staleNote says which half moves: the project, by a gdharness upgrade it spells out to run in a shell, the editor by a restart, or this server by a reconnect. A false editor.lspListening or editor.dapListening comes with serversNote: editor_launch rebind binds the debug adapter again in place, and editor_launch restart the language server. editor.scan says whether the editor is scanning, which editor_run waits out before starting a game. A game announced in a protocol this server does not speak is listed under unreadable, during an upgrade that moves the protocol, until the halves match.",
|
|
17587
|
-
answer: "projectIs, when present, is the gdharness version of the addons in the project, which differs from this server's (project_info says which of the two to move), and configIs, when present, the version the project's MCP config starts, which differs from this server's, so the next connection runs that one. server names this server: its pid, its version, and lifecycleLog, the file every gdharness server of the user appends a line to when it starts, when its client first names itself, when it replaces another server of its project, and when it ends, with why it ended (the client closing its end as stdin:end, a signal by name, superseded with the successor's pid, an uncaught exception or rejection with its stack, or the exit code) and the calls it was answering then. The parent's command line follows on its own line a few seconds after the start. A server killed without warning writes no ended line, and on Windows every kill is that, so a started line with no ended line after it is a server something killed. " + "runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath, and userDir, the directory its user:// is, where its saves and settings are read. 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. Its play says which play the editor is on: play.play counts the plays that editor has started since it opened, whoever started them, and play.editorPid is the editor, so two reads with different pairs are two plays even when both say playing. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it. It stays named after the run ends, beside processActive false, so editor_output can still read what that run printed; null when there is no run at all. startedAt says when it began, and endedAt when it ended, wherever something saw the end: a run adopted from a note another server wrote can have ended hours before this server started. editor.lspListening and editor.dapListening say whether anything accepts connections on the ports the editor names, lspPort and dapPort: Godot binds each once, as the editor starts, and reports a failure only in the log panel of the editor itself, so a false comes with serversNote: editor_launch rebind binds the debug adapter again in place, and editor_launch restart the language server. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs the gdharness upgrade command, spelled out to run in a shell, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. A runtime_* call to such a game is refused with the staleNote and not sent. 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 editorCannotConnect is a project without the gdharness editor addon installed or enabled, where no editor can ever dial in, however young the server or whichever editor is open; its remedies say how to install or enable it. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in and took a key out of it, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. A stale editor is asked nothing but to quit: every other call to it is refused with staleNote, which names editor_launch restart, the upgrade command or a reconnect. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and from the socket opening until its greeting and that read are done, identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window. serverVersion is the gdharness version of this server. bridgeAvailable says the editor bridge is listening; when it failed to start, startupError gives the error, and retryingBridge is true while this server tries again. announcedAt is the file where this server told editors of its project which port to dial. connectedAt is when the editor connected, and lastPongAt when it last answered this server's keep-alive ping. addonDigest is a hash of the editor addon's code, which addonIsStale compares with what this server ships. debugPort is the port of the editor's remote debugger, the one games it plays connect to. openedByAServer says a gdharness server started this editor,
|
|
17647
|
+
answer: "projectIs, when present, is the gdharness version of the addons in the project, which differs from this server's (project_info says which of the two to move), and configIs, when present, the version the project's MCP config starts, which differs from this server's, so the next connection runs that one. server names this server: its pid, its version, and lifecycleLog, the file every gdharness server of the user appends a line to when it starts, when its client first names itself, when it replaces another server of its project, and when it ends, with why it ended (the client closing its end as stdin:end, a signal by name, superseded with the successor's pid, an uncaught exception or rejection with its stack, or the exit code) and the calls it was answering then. The parent's command line follows on its own line a few seconds after the start. A server killed without warning writes no ended line, and on Windows every kill is that, so a started line with no ended line after it is a server something killed. " + "runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath, and userDir, the directory its user:// is, where its saves and settings are read. 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. Its play says which play the editor is on: play.play counts the plays that editor has started since it opened, whoever started them, and play.editorPid is the editor, so two reads with different pairs are two plays even when both say playing. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it. It stays named after the run ends, beside processActive false, so editor_output can still read what that run printed; null when there is no run at all. startedAt says when it began, and endedAt when it ended, wherever something saw the end: a run adopted from a note another server wrote can have ended hours before this server started. editor.lspListening and editor.dapListening say whether anything accepts connections on the ports the editor names, lspPort and dapPort: Godot binds each once, as the editor starts, and reports a failure only in the log panel of the editor itself, so a false comes with serversNote: editor_launch rebind binds the debug adapter again in place, and editor_launch restart the language server. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs the gdharness upgrade command, spelled out to run in a shell, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. A runtime_* call to such a game is refused with the staleNote and not sent. 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 editorCannotConnect is a project without the gdharness editor addon installed or enabled, where no editor can ever dial in, however young the server or whichever editor is open; its remedies say how to install or enable it. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in and took a key out of it, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. A stale editor is asked nothing but to quit: every other call to it is refused with staleNote, which names editor_launch restart, the upgrade command or a reconnect. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and from the socket opening until its greeting and that read are done, identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window. serverVersion is the gdharness version of this server. bridgeAvailable says the editor bridge is listening; when it failed to start, startupError gives the error, and retryingBridge is true while this server tries again. announcedAt is the file where this server told editors of its project which port to dial. connectedAt is when the editor connected, and lastPongAt when it last answered this server's keep-alive ping. addonDigest is a hash of the editor addon's code, which addonIsStale compares with what this server ships. debugPort is the port of the editor's remote debugger, the one games it plays connect to. openedByAServer says a gdharness server started this editor, syncsBreakpoints that it keeps its breakpoints when a debug session opens, which Godot otherwise clears, and hidden that it runs headless with no window anybody can see. pendingRequests counts the requests sent to the editor and not yet answered, and queuedResources the files with writes waiting behind another write to the same file.",
|
|
17588
17648
|
parameters: {},
|
|
17589
17649
|
requires: []
|
|
17590
17650
|
},
|
|
@@ -46914,6 +46974,7 @@ var init_godot_bridge = __esm(() => {
|
|
|
46914
46974
|
dap_port: "number",
|
|
46915
46975
|
debug_port: "number",
|
|
46916
46976
|
syncs_breakpoints: "boolean",
|
|
46977
|
+
hidden: "boolean",
|
|
46917
46978
|
opened_by_a_server: "boolean",
|
|
46918
46979
|
started_by_an_editor: "boolean"
|
|
46919
46980
|
};
|
|
@@ -47096,6 +47157,7 @@ var init_godot_bridge = __esm(() => {
|
|
|
47096
47157
|
openedByAServer: editor?.openedByAServer,
|
|
47097
47158
|
startedByAnEditor: editor?.startedByAnEditor,
|
|
47098
47159
|
syncsBreakpoints: editor?.syncsBreakpoints,
|
|
47160
|
+
hidden: editor?.hidden,
|
|
47099
47161
|
turnedAway: this.turnedAway ?? undefined,
|
|
47100
47162
|
identifying: identifying ? true : undefined,
|
|
47101
47163
|
pendingRequests: this.requestsInFlight().length,
|
|
@@ -47282,6 +47344,7 @@ var init_godot_bridge = __esm(() => {
|
|
|
47282
47344
|
this.connectionInfo.openedByAServer = message.opened_by_a_server;
|
|
47283
47345
|
this.connectionInfo.startedByAnEditor = message.started_by_an_editor;
|
|
47284
47346
|
this.connectionInfo.syncsBreakpoints = message.syncs_breakpoints;
|
|
47347
|
+
this.connectionInfo.hidden = message.hidden;
|
|
47285
47348
|
this.log("info", `Godot ready: ${message.project_path}`);
|
|
47286
47349
|
this.connectionInfo.greeted = true;
|
|
47287
47350
|
if (!message.opened_by_a_server && this.oneShotReader !== null) {
|
|
@@ -365,6 +365,8 @@ func _handle_connect() -> void:
|
|
|
365
365
|
# Whether a session opening on the adapter is told this editor's breakpoints or takes
|
|
366
366
|
# them away, so a server can say which before it sets one.
|
|
367
367
|
"syncs_breakpoints": _syncs_breakpoints(),
|
|
368
|
+
# Whether this editor runs with no window, which for one opened by hand only it can say.
|
|
369
|
+
"hidden": DisplayServer.get_name() == "headless",
|
|
368
370
|
# Whether a server started this editor, which is the only kind that can be started again
|
|
369
371
|
# as itself: the engine hands back none of the arguments it was given, so the arguments
|
|
370
372
|
# have to come from whoever wrote them. See `restart_editor` in play_tools.gd.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.35",
|
|
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",
|