gdharness 1.3.61 → 1.3.62

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 CHANGED
@@ -48,8 +48,8 @@ editor_output { "projectPath": "C:/games/shop" } // errors and warni
48
48
  The open editor plays the game, so you can set a breakpoint and read the variables in scope when it
49
49
  stops.
50
50
 
51
- Every answer is read back from the engine, never echoed from the request. A tool that cannot answer
52
- says which state it is in and what would change it.
51
+ Writes answer with what the engine holds afterwards, never an echo of the request. A tool that
52
+ cannot answer says which state it is in and what would change it.
53
53
 
54
54
  ## At a glance
55
55
 
package/build/cli.js CHANGED
@@ -17264,6 +17264,52 @@ var init_lsp_client = __esm(() => {
17264
17264
  ];
17265
17265
  });
17266
17266
 
17267
+ // src/nearest.ts
17268
+ function similarity(one, other) {
17269
+ if (one === other) {
17270
+ return 1;
17271
+ }
17272
+ if (one.length < 2 || other.length < 2) {
17273
+ return 0;
17274
+ }
17275
+ const pairs = (text) => Array.from({ length: text.length - 1 }, (_, at) => text.slice(at, at + 2));
17276
+ const ones = pairs(one);
17277
+ const others = pairs(other);
17278
+ const shared = ones.filter((pair) => others.includes(pair)).length;
17279
+ return 2 * shared / (ones.length + others.length);
17280
+ }
17281
+ function withNearestFiles(said, wanted, files, beside) {
17282
+ const stem = (path) => (path.split("/").pop() ?? path).replace(/\.[^.]*$/, "");
17283
+ const named = said.split(/[\s,;]+/).map((word) => word.replace(/[.:]+$/, ""));
17284
+ const namesakes = beside.filter((file) => file !== wanted && stem(file) === stem(wanted) && !named.includes(file)).toSorted();
17285
+ if (namesakes.length > 0) {
17286
+ const alone = namesakes.length === 1;
17287
+ return `${said}. The ${alone ? "file" : "files"} of that name beside it ${alone ? "is" : "are"} ${namesakes.join(", ")}.`;
17288
+ }
17289
+ const extension = /\.([^./]+)$/.exec(wanted)?.[1];
17290
+ if (extension === undefined) {
17291
+ return said;
17292
+ }
17293
+ if (files.length === 0) {
17294
+ return `${said}. The project holds no .${extension} file.`;
17295
+ }
17296
+ const close = nearest(stem(wanted), files.map((file) => stem(file)));
17297
+ const offered = files.filter((file) => close.includes(stem(file)));
17298
+ if (offered.length > 0) {
17299
+ return `${said}. The nearest .${extension} ${offered.length === 1 ? "file is" : "files are"} ${offered.join(", ")}.`;
17300
+ }
17301
+ if (files.length <= LISTED_FILES) {
17302
+ return `${said}. The project's .${extension} files are ${files.join(", ")}.`;
17303
+ }
17304
+ return `${said}. None of the project's ${files.length} .${extension} files is named like it.`;
17305
+ }
17306
+ function nearest(word, names) {
17307
+ const wanted = word.toLowerCase();
17308
+ const score = (name) => similarity(name.toLowerCase(), wanted);
17309
+ return names.filter((name) => score(name) >= 0.5).toSorted((one, other) => score(other) - score(one)).slice(0, 3);
17310
+ }
17311
+ var LISTED_FILES = 12;
17312
+
17267
17313
  // src/resources.ts
17268
17314
  import { readFileSync as readFileSync21, realpathSync as realpathSync4 } from "node:fs";
17269
17315
  import { extname as extname4, resolve as resolve10 } from "node:path";
@@ -18093,7 +18139,7 @@ function annotationsOf(spec) {
18093
18139
  openWorldHint: false
18094
18140
  };
18095
18141
  }
18096
- var PROJECT_INFO_SECTIONS, PROJECT_PATH, RUNNING_PROJECT_PATH, RUNNING_PID, SCENE_PATH, WITHOUT_AN_EDITOR = "Through the editor when one serves the project. With none, a headless engine runs the editor's own tool and the answer says so under writtenBy, or readBy for a read. That engine runs every script of the file, where the editor runs only tool scripts, so what a script changes as the file loads or as a property is set is put back before anything is saved, and a file where that cannot be done, such as a scene whose script does not compile, is refused and nothing is written. Each file the written one refers to is named by its uid, as the editor names it, except a file the project's uid cache has no uid for yet (one made since the project was last scanned), which is named by path and listed under namedByPath with the call that scans it. An editor of the project that is running and has not reached this server is waited for rather than written around.", ALSO_SAVED = "A scene is saved as the engine holds it once the file has loaded, and one property can set others as it loads, as anchors_preset gives a centred Control both its grow directions, so a scene not last saved by an editor can gain, change or lose lines the call did not ask for, on any node the file held, the one it names among them: alsoSaved lists each as {node, property, was, saved}, was left out where the file set nothing and saved where it now sets nothing. What a set itself moved on its node is under alsoChanged rather than here, and neither a node the call adds nor the format lines (header uid, load_steps, unique_id) are listed. The scene loads as it did. Only its text changed.", IMPORTED_FIRST = "importedFirst names the files whose import was behind them on disk, such as a picture changed since it was imported or one never imported, which were imported before the run through project_import reimport, so the run reads them as they are now. importedFirst is left out when every import was current. notImportedFirst lists, as project_import reimport does, any it could not import, and importNote says that the run reads those as they were. The import gives every script without a .uid sidecar one, as the editor's scan does, writing it beside the script in the project, and uidsWrittenFirst names those scripts. The files inside an addon are counted per addon rather than named, under importedInAddons and uidsWrittenInAddons. importTookMs is how long the import took. That time is outside the run's own time limit, so a call that imported can take that much longer than the limit it was given.", WHOLE_NUMBERS = 'A whole number of 2^53 or more, such as a 64-bit seed or a resource uid, is written as its digits, {"_type": "int", "value": "9007199254740993"}, the form a read answers with: a JSON number that large can arrive as a neighbour of the one sent, so sent as a number it is refused.', SCRIPT_PATH, RESOURCE_FILE = 'File inside the project, such as "sprites/hero.png" or "materials/steel.tres".', EXPORT_PLATFORMS, RESOURCE_PATH, NODE_PATH, PROPERTIES, XY, ANIMATION_TRACK, SCRIPT_MODIFICATIONS, INPUT_EVENTS, TILESET_SOURCES, TILEMAP_CELLS, COLOUR2, TOOL_SPECS, SPECS_BY_NAME;
18142
+ var PROJECT_INFO_SECTIONS, PROJECT_PATH, RUNNING_PROJECT_PATH, RUNNING_PID, SCENE_PATH, WITHOUT_AN_EDITOR = "The call goes through the editor when one serves the project. With none, a headless engine runs the editor's own tool and the answer says so under writtenBy, or readBy for a read. That engine runs every script of the file, where the editor runs only tool scripts, so what a script changes as the file loads or as a property is set is put back before anything is saved, and a file where that cannot be done, such as a scene whose script does not compile, is refused and nothing is written. Each file the written one refers to is named by its uid, as the editor names it, except a file the project's uid cache has no uid for yet (one made since the project was last scanned), which is named by path and listed under namedByPath with the call that scans it. A running editor of the project that has not yet reached this server is waited for.", ALSO_SAVED = "A scene is saved as the engine holds it once the file has loaded, and one property can set others as it loads, as anchors_preset gives a centred Control both its grow directions, so a scene not last saved by an editor can gain, change or lose lines the call did not ask for, on any node the file held, the one it names among them: alsoSaved lists each as {node, property, was, saved}, was left out where the file set nothing and saved where it now sets nothing. What a set itself moved on its node is under alsoChanged rather than here, and neither a node the call adds nor the format lines (header uid, load_steps, unique_id) are listed. The scene loads as it did. Only its text changed.", IMPORTED_FIRST = "importedFirst names the files whose import was behind them on disk, such as a picture changed since it was imported or one never imported, which were imported before the run through project_import reimport, so the run reads them as they are now. importedFirst is left out when every import was current. notImportedFirst lists, as project_import reimport does, any it could not import, and importNote says that the run reads those as they were. The import gives every script without a .uid sidecar one, as the editor's scan does, writing it beside the script in the project, and uidsWrittenFirst names those scripts. The files inside an addon are counted per addon rather than named, under importedInAddons and uidsWrittenInAddons. importTookMs is how long the import took. That time is outside the run's own time limit, so a call that imported can take that much longer than the limit it was given.", WHOLE_NUMBERS = 'A whole number of 2^53 or more, such as a 64-bit seed or a resource uid, is written as its digits, {"_type": "int", "value": "9007199254740993"}, the form a read answers with: a JSON number that large can arrive as a neighbour of the one sent, so sent as a number it is refused.', SCRIPT_PATH, RESOURCE_FILE = 'File inside the project, such as "sprites/hero.png" or "materials/steel.tres".', EXPORT_PLATFORMS, RESOURCE_PATH, NODE_PATH, PROPERTIES, XY, ANIMATION_TRACK, SCRIPT_MODIFICATIONS, INPUT_EVENTS, TILESET_SOURCES, TILEMAP_CELLS, COLOUR2, TOOL_SPECS, SPECS_BY_NAME;
18097
18143
  var init_tool_definitions = __esm(() => {
18098
18144
  init_launch();
18099
18145
  PROJECT_INFO_SECTIONS = [
@@ -18348,7 +18394,7 @@ var init_tool_definitions = __esm(() => {
18348
18394
  name: "project_settings",
18349
18395
  title: "Project settings",
18350
18396
  description: "Reads or writes project.godot (settings, autoloads, the main scene, input actions and plugins) and the audio bus layout, default_bus_layout.tres unless audio/buses/default_bus_layout names another. project_info include lists the autoloads, input actions and audio buses as the project holds them. A write with an editor open on the project is handed to the editor, which takes the settings the write names from the file (named under editorAdopted), even one the file already held, or reloads the bus layout, so it does not save the old values back over them. An enabled plugin's code still loads on the editor's next start, and enable_plugin notes a script it would not load. add_autoload refuses a name the editor's Autoload tab refuses: not an identifier, a keyword, a built-in type or an engine class. add_input_action on an existing action, a ui_ one included, replaces its events, and its deadzone when one is given. A set of the value the setting already holds, in the same type, writes nothing. unset takes a setting's line out of project.godot, and on one the file does not set it writes nothing. Every write changes only the lines of the settings it names and keeps every other line as the file had it, in its place. A setting res://override.cfg or the project_settings_override file sets is one a run lays over project.godot and the editor does not.",
18351
- answer: "get answers exists, true for any setting the engine knows, at_default, true when the value is the engine's own default and so has no line in project.godot, and overrides, the value each feature's override holds. Each setting under prefix carries at_default too. at_default is absent for a setting with no engine default, such as an input action the project added. An override answers override_of, its base setting, and one project.godot does not set answers exists false with holds, the base setting's value. A setting an override file sets is answered with the project's value and overridden_in, the file and the value a run reads. " + "set answers old_value and new_value, with was_new true when project.godot did not set the setting before, and was_default true when what it held was the engine's own default, which old_value then is. It adds overrides with overridesNote when a feature's override still replaces the setting, and overriddenNote when an override file does. A set that writes nothing answers unchanged true and saved false. " + "unset answers removed, old_value and default_value, the value that holds now. default_value is null for a setting the engine does not declare. For an override such as name.feature, holds is the base setting's value. On a setting the file does not set, unset answers removed false, its note naming the nearest settings when the engine does not declare it either. " + "add_input_action on an existing action answers was_new false with old_events and old_deadzone. " + "editorNote, on any write, is an editor that could not take the write, or one on another release's addon that was not asked. That editor still holds the old values until editor_launch restart.",
18397
+ answer: "get answers exists, true for any setting the engine knows, at_default, true when the value is the engine's own default and so has no line in project.godot, and overrides, the value each feature's override holds. Each setting under prefix carries at_default too. at_default is absent for a setting with no engine default, such as an input action the project added. An override answers override_of, its base setting, and one project.godot does not set answers exists false with holds, the base setting's value. A setting an override file sets is answered with the project's value and overridden_in, the file and the value a run reads. " + "set answers old_value and new_value, with was_new true when project.godot did not set the setting before, and was_default true when what it held was the engine's own default, which old_value then is. It adds overrides with overridesNote when a feature's override still replaces the setting, and overriddenNote when an override file does. A set that writes nothing answers unchanged true and saved false. " + "unset answers removed, old_value and default_value, the value that holds now. default_value is null for a setting the engine does not declare. unset of an override, such as name.feature, also answers override_of and holds, the base setting's value that builds with the feature now read. On a setting the file does not set, unset answers removed false, its note naming the nearest settings when the engine does not declare it either. " + "add_input_action on an existing action answers was_new false with old_events and old_deadzone. " + "editorNote, on any write, is an editor that could not take the write, or one on another release's addon that was not asked. That editor still holds the old values until editor_launch restart.",
18352
18398
  parameters: {
18353
18399
  projectPath: PROJECT_PATH,
18354
18400
  setting: {
@@ -18723,7 +18769,7 @@ var init_tool_definitions = __esm(() => {
18723
18769
  title: "Run tests",
18724
18770
  effect: "additive",
18725
18771
  changes: "Writes under `.godot` and adds `.uid` files. Changes no script or scene.",
18726
- description: "Runs the project's gdUnit4 suites in one engine and answers with every case that did not pass, with where it is and what its assertion said. passed is the verdict. The class list is rebuilt first, so a suite written a moment ago is found. What changed on disk since it was imported is then imported before any suite runs. On Windows and Linux the run gets a user:// of its own, away from a player's saves. On macOS the engine reads user:// off HOME, so it writes where the player keeps theirs. The run is headless unless a suite in it needs a window as gdharness test reads it, and then has one, on a desktop of its own on Windows. A run that found nothing to run is never called a pass, and names the path it looked in. A script that does not load stops gdUnit4 before any suite runs, and an autoload that does not compile stops the run at once. A client sending a progress token is told as each suite finishes, and at least every twenty seconds. A run whose every case passed is still not called passed when it printed a script error no case failed on. gdUnit4 fails a case only on the script errors its logger sees, and it cannot see a stack overflow. Needs gdUnit4 under addons/gdUnit4.",
18772
+ description: "Runs the project's gdUnit4 suites in one engine and answers with every case that did not pass, with where it is and what its assertion said. The class list is rebuilt first, so a suite written a moment ago is found. What changed on disk since it was imported is then imported before any suite runs. On Windows and Linux the run gets a user:// of its own, away from a player's saves. On macOS the engine reads user:// off HOME, so it writes where the player keeps theirs. The run is headless unless a suite in it needs a window as gdharness test reads it, and then has one, on a desktop of its own on Windows. A run that found nothing to run is never called a pass, and names the path it looked in. A script that does not load stops gdUnit4 before any suite runs, and an autoload that does not compile stops the run at once. A client sending a progress token is told as each suite finishes, and at least every twenty seconds. A run whose every case passed is still not called passed when it printed a script error no case failed on. gdUnit4 fails a case only on the script errors its logger sees, and it cannot see a stack overflow. Needs gdUnit4 under addons/gdUnit4.",
18727
18773
  answer: [
18728
18774
  "passed is the verdict to read.",
18729
18775
  "",
@@ -18775,7 +18821,7 @@ var init_tool_definitions = __esm(() => {
18775
18821
  },
18776
18822
  failFast: {
18777
18823
  type: "boolean",
18778
- description: "Stop each suite at its first failing case. Default false: every case runs. With it on, the counts are of what ran, notRun says how many cases were left. Each run after a fix then finds the next failure. That comes from failFast, not a flaky suite."
18824
+ description: "Stop each suite at its first failing case. Default false: every case runs. With it on, the counts cover only what ran. notRun counts the cases left. Each run after a fix then finds the next failure. That comes from failFast, not a flaky suite."
18779
18825
  },
18780
18826
  timeoutMs: {
18781
18827
  type: "integer",
@@ -19507,7 +19553,7 @@ var init_tool_definitions = __esm(() => {
19507
19553
  savesIn: {
19508
19554
  type: "string",
19509
19555
  ops: ["start", "check"],
19510
- description: "An absolute directory for this run's user://, so a game that saves writes nowhere near the copy somebody plays: opening a save writes the one being left, and a game that autosaves writes on its own. Godot takes no flag for it, so this is the environment, and the run is started by this server rather than played by the editor, which cannot be given one. It does not have to exist: the engine makes it, and the Godot/app_userdata/<project> tree under it, measured on 4.7.2. Whatever the run, runtime.userDir in the answer is the directory user:// is, as the game's engine resolved it. On macOS the engine reads user:// off HOME and ignores it, also measured, and the answer says so under savesNote rather than leaving the caller to find out from the player's save list."
19556
+ description: "An absolute directory for this run's user://, so a game that saves writes nowhere near the copy somebody plays: opening a save writes the one being left, and a game that autosaves writes on its own. Godot takes no flag for it, so this is the environment, and the run is started by this server rather than played by the editor, which cannot be given one. It does not have to exist: the engine makes it. Under it the engine makes Godot/app_userdata/<project>, or the custom folder's name alone when the project sets application/config/use_custom_user_dir. Whatever the run, runtime.userDir in the answer is the directory user:// is, as the game's engine resolved it. On macOS the engine reads user:// off HOME and ignores it, also measured, and the answer says so under savesNote rather than leaving the caller to find out from the player's save list."
19511
19557
  },
19512
19558
  env: {
19513
19559
  type: "object",
@@ -19610,7 +19656,7 @@ var init_tool_definitions = __esm(() => {
19610
19656
  {
19611
19657
  name: "editor_output",
19612
19658
  title: "Game and editor output",
19613
- description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. For a run the editor plays the reports come from the game itself, through the runtime addon, since the editor's debugger relays what a game prints and not what it reports. A played game without the runtime addon answers here with its prints alone, with errorsUnread true and no clean, since what it raised reaches nothing here. A run answers with counts and a verdict: clean is true when no error was reported, and warnings, counted beside it, do not make it false. heldAt is three answers: null for a run this server knows is not held, running or over, an object for one it knows is held at a breakpoint, whether the adapter said so or the runtime did by accepting a connection and never answering, and absent with heldUnknown beside it for a server that connected to the debugger after the stop and has no runtime to ask, since the adapter reports a stop only to the sessions connected when it happened. A run that has quit still answers here, with running false and its exit code, until the next one starts. A run ended by a signal has no exit code, and exitSignal names the signal instead. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over. running is asked of the operating system whenever there is a process to ask about, which is every run this server started and every editor-played run whose game announced its runtime. For an editor-played run that never announced, there is no process id here and running is the editor's answer, which can lag: one reported a game as playing fifteen seconds after its process was ended from outside. editor_status names that game under runtimes, which does not depend on the editor answering. op: \"editor\" answers about the editor itself instead, which is a different log and the only way anything here can read what an editor printed: its console reaches no plugin, so the addon cannot scrape it, and an editor gdharness opened is asked for a log file as it starts. That covers the whole session from the editor's first line, where the output worth reading usually is, since the class cache, the plugins and the language server speak before any harness connects. It answers counts with no clean, since an editor neither passes nor fails, the entries in the order they were printed and, beside them under repeated, each message shape several lines share: how many, what varied in each slot of it and how many different values stood there, the first occurrence, and the ordinary lines above the group, found by walking back past the group rather than by counting lines. An editor somebody opened by hand has no console here and is refused saying so rather than answered empty, because an editor that printed nothing and an editor nobody captured are not the same thing. startedAt is when the run began and elapsedMs how long it has been going. For a run that is over, endedAt is when it ended and elapsedMs how long it ran, and both are left out when nobody saw the end, as for a game closed in the editor or a run picked up from a note written before ends were recorded.",
19659
+ description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. For a run the editor plays the reports come from the game itself, through the runtime addon, since the editor's debugger relays what a game prints and not what it reports. A played game without the runtime addon answers here with its prints alone, with errorsUnread true and no clean, since what it raised reaches nothing here. A run answers with counts and a verdict: clean is true when no error was reported, and warnings, counted beside it, do not make it false. heldAt is three answers: null for a run this server knows is not held, running or over, an object for one it knows is held at a breakpoint, whether the adapter said so or the runtime did by accepting a connection and never answering, and absent with heldUnknown beside it for a server that connected to the debugger after the stop and has no runtime to ask, since the adapter reports a stop only to the sessions connected when it happened. A run that has quit still answers here, with running false and its exit code, until the next one starts. A run ended by a signal has no exit code, and exitSignal names the signal instead. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over. running is asked of the operating system whenever there is a process to ask about, which is every run this server started and every editor-played run whose game announced its runtime. For an editor-played run that never announced, there is no process id here and running is the editor's answer, which can lag: one reported a game as playing fifteen seconds after its process was ended from outside. editor_status does not list that game under runtimes, which holds only games that announced. op: \"editor\" answers about the editor itself instead, which is a different log and the only way anything here can read what an editor printed: its console reaches no plugin, so the addon cannot scrape it, and an editor gdharness opened is asked for a log file as it starts. That covers the whole session from the editor's first line, where the output worth reading usually is, since the class cache, the plugins and the language server speak before any harness connects. It answers counts with no clean, since an editor neither passes nor fails, the entries in the order they were printed and, beside them under repeated, each message shape several lines share: how many, what varied in each slot of it and how many different values stood there, the first occurrence, and the ordinary lines above the group, found by walking back past the group rather than by counting lines. An editor somebody opened by hand has no console here and is refused saying so rather than answered empty, because an editor that printed nothing and an editor nobody captured are not the same thing. startedAt is when the run began and elapsedMs how long it has been going. For a run that is over, endedAt is when it ended and elapsedMs how long it ran, and both are left out when nobody saw the end, as for a game closed in the editor or a run picked up from a note written before ends were recorded.",
19614
19660
  answer: "An entry with lines under its headline has them as detail, and, when one of them names a project script, script, line and function say where: the at: line when it names one, otherwise the innermost frame of the backtrace that does. A push_error or push_warning has the engine's own push_error as its at: line, so its script and line are the game's call that raised it. Each entry's source says where it was read. transcript is a line of the run's transcript file, which holds both its streams and is how every run with one is read. stdout or stderr is a line of that stream of a run with no transcript, such as an editor-played run picked up after a reconnect whose first server left no note. debugger is what the editor's debug adapter reported and the game never printed, such as the script error a game stopped on. A printerr line is info: it prints to stderr, and the engine reports no error for it. For op editor with no editor connected, connected is false, editorRunning says whether the editor whose console it is still runs, and editorNote whether this server closed it or it went without a close, as a crash does. The next editor gdharness opens on the project starts a new console. With none connected and none ever opened here on the project, there is no console, and the refusal says so.",
19615
19661
  parameters: {
19616
19662
  projectPath: {
@@ -19677,7 +19723,7 @@ var init_tool_definitions = __esm(() => {
19677
19723
  title: "Rescan the project",
19678
19724
  effect: "additive",
19679
19725
  changes: "Writes under `.godot` and adds `.uid` files. Changes no script or scene.",
19680
- description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. A scan or an import the editor is already running is waited out first rather than scanned over, which would start a second import over it. The scan is a change-detecting walk rather than an unconditional reparse, so a file another engine has already imported reads as settled and the walk does not look inside it: its class_name then stays out of the list the editor resolves against, however many times you scan. Any class in that state is named under unseenByEditor, which is what no check on disk can see, since the declaration and the cache are both correct there and only the editor disagrees. The cure is this call on its own, with no change to the declaring script, measured against a real editor on three platforms. editor_launch restart also does it and costs a window, and project_import refresh_classes does not: it rewrites the cache and does not touch what the editor is holding. The walk also passes over a file whose .uid alone changed, so the editor keeps the old UID and every engine reading its cache warns of a duplicate. Before scanning, each file whose .uid names a UID the editor does not hold for it is read again and named under uidsReread, and files on disk naming one UID between them are named under uidsDuplicatedOnDisk, which reading again cannot settle. A class the scan brings in is named under broughtIn, and every script naming it is reloaded in the editor and named under dependentsReloaded: what the editor compiled from them while it could not see the class stays compiled that way after the scan, and a script extending a new base class went on reporting \"Could not find base class\" until reloaded. One that would not reload is named under dependentsNotReloaded and the call is not ok, as it is not when the script named in reloadScript was not reloaded. A script that fails to compile is tried again once the scripts it is compiled against that the editor holds behind their files are reloaded, which are named under reloadedFirst. An editor importing answers nothing until the import is over, and a hidden one imports on the thread that answers, so a large addon install can leave the rescan itself unanswered: that is waited for as any busy editor is, within timeoutMs, and when the wait runs out first the answer is stillWorking. The classes the editor could not see before that scan are then noted in the project, and the next editor_rescan of the same editor, from any server, names them under broughtIn and reloads what names them, which it could not otherwise know to do, since by then the editor sees them. A scan writes .godot/global_script_class_cache.cfg from the list the editor is holding, so a class the editor cannot resolve goes out of that file with it. That is not a reason to refuse the scan, because on a class the editor has merely not walked yet the scan is the cure. What the answer does instead is name the loss under cacheLost and rebuild the cache from the files, naming what came back under cacheRestored, so no fresh engine, CI run or clone inherits the short file. An editor that lost classes is still holding the short list, so the note says to restart it before scanning again. The other direction is guarded the same way: a class the editor still holds for a script that was renamed or deleted comes back into the cache at a path that is not there, and the next engine to read it fails on \"Could not parse global class\" in whichever correct script shares the bare name, so the cache is rebuilt without it and the answer names it under cacheDropped. The editor goes on holding it until it is restarted, and every scan until then ends in that rebuild. A script or shader written outside the editor has no .uid sidecar until something imports it, and the scan writes one beside it, which a project that commits sidecars needs before the commit. Needs the editor connected.",
19726
+ description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. A scan or an import the editor is already running is waited out first rather than scanned over, which would start a second import over it. The scan the editor runs on its own, when its window takes the focus or files change under it, is a change-detecting walk rather than an unconditional reparse. A file another engine has already imported reads as settled there and the walk does not look inside it. Its class_name then stays out of the list the editor resolves against, however often the editor scans on its own. Any class in that state is named under unseenByEditor, which no check on disk can see. Calling it again is the cure, with no change to the declaring script. editor_launch restart also does it and costs a window, and project_import refresh_classes does not: it rewrites the cache and does not touch what the editor is holding. The walk also passes over a file whose .uid alone changed, so the editor keeps the old UID and every engine reading its cache warns of a duplicate. Before scanning, each file whose .uid names a UID the editor does not hold for it is read again and named under uidsReread, and files on disk naming one UID between them are named under uidsDuplicatedOnDisk, which reading again cannot settle. A class the scan brings in is named under broughtIn, and every script naming it is reloaded in the editor and named under dependentsReloaded: what the editor compiled from them while it could not see the class stays compiled that way after the scan, and a script extending a new base class went on reporting \"Could not find base class\" until reloaded. One that would not reload is named under dependentsNotReloaded and the call is not ok, as it is not when the script named in reloadScript was not reloaded. A script that fails to compile is tried again once the scripts it is compiled against that the editor holds behind their files are reloaded, which are named under reloadedFirst. An editor importing answers nothing until the import is over, and a hidden one imports on the thread that answers, so a large addon install can leave the rescan itself unanswered: that is waited for as any busy editor is, within timeoutMs, and when the wait runs out first the answer is stillWorking. The classes the editor could not see before that scan are then noted in the project, and the next editor_rescan of the same editor, from any server, names them under broughtIn and reloads what names them, which it could not otherwise know to do, since by then the editor sees them. A scan writes .godot/global_script_class_cache.cfg from the list the editor is holding, so a class the editor cannot resolve goes out of that file with it. That is not a reason to refuse the scan, because on a class the editor has merely not walked yet the scan is the cure. What the answer does instead is name the loss under cacheLost and rebuild the cache from the files, naming what came back under cacheRestored, so no fresh engine, CI run or clone inherits the short file. An editor that lost classes is still holding the short list, so the note says to restart it before scanning again. The other direction is guarded the same way: a class the editor still holds for a script that was renamed or deleted comes back into the cache at a path that is not there, and the next engine to read it fails on \"Could not parse global class\" in whichever correct script shares the bare name, so the cache is rebuilt without it and the answer names it under cacheDropped. The editor goes on holding it until it is restarted, and every scan until then ends in that rebuild. A script or shader written outside the editor has no .uid sidecar until something imports it, and the scan writes one beside it, which a project that commits sidecars needs before the commit. Needs the editor connected.",
19681
19727
  answer: "waitedMs is the whole call, and waitedForEditorScanMs, given only when a scan or an import was already running, the part spent waiting it out. waitedForEditorAnswerMs, given when an idle editor took a second or more to say so, is how long that took: such an editor is busy on the thread that answers with an earlier request rather than scanning, so the time is added to timeoutMs rather than taken from it, and the note says so. stillWorking is true when a scan was still running at the end of the wait. The scan the call started is looked at once even when the wait has run out, so stillWorking is never said of a scan nothing looked at.",
19682
19728
  parameters: {
19683
19729
  projectPath: PROJECT_PATH,
@@ -19787,7 +19833,7 @@ var init_tool_definitions = __esm(() => {
19787
19833
  type: "integer",
19788
19834
  minimum: 0,
19789
19835
  ops: ["tree"],
19790
- description: "tree: levels to descend, 0 for the node alone. Default 3."
19836
+ description: "tree: levels to descend, 0 for the node alone. Default 3. A node at the last level that has children answers children_not_shown with their count instead of children."
19791
19837
  },
19792
19838
  includeProperties: {
19793
19839
  type: "boolean",
@@ -20378,7 +20424,7 @@ var init_tool_definitions = __esm(() => {
20378
20424
  });
20379
20425
 
20380
20426
  // src/headless-operations.ts
20381
- var HEADLESS_OPERATIONS, ENGINE_PASSES, EDITOR_READS;
20427
+ var HEADLESS_OPERATIONS, ENGINE_PASSES, DEPENDENCY_OPERATIONS, HEADLESS_TOOLS, EDITOR_READS;
20382
20428
  var init_headless_operations = __esm(() => {
20383
20429
  HEADLESS_OPERATIONS = dictionary({
20384
20430
  project_settings: dictionary({
@@ -20418,6 +20464,13 @@ var init_headless_operations = __esm(() => {
20418
20464
  ENGINE_PASSES = dictionary({
20419
20465
  project_import: dictionary({ refresh_uids: "import", reimport: "import" })
20420
20466
  });
20467
+ DEPENDENCY_OPERATIONS = {
20468
+ forward: "get_dependencies",
20469
+ reverse: "find_resource_usages"
20470
+ };
20471
+ HEADLESS_TOOLS = dictionary({
20472
+ project_dependencies: Object.values(DEPENDENCY_OPERATIONS)
20473
+ });
20421
20474
  EDITOR_READS = dictionary({
20422
20475
  project_settings: dictionary({ get: "get_project_setting" })
20423
20476
  });
@@ -20446,13 +20499,15 @@ function headlessSection() {
20446
20499
  everyOp.set(tool, [...everyOp.get(tool) ?? [], ...Object.keys(ops)]);
20447
20500
  }
20448
20501
  }
20449
- const byTool = [...everyOp].map(([tool, ops]) => `\`${tool}\` (${ops.join(", ")})`).toSorted();
20502
+ const byTool = [
20503
+ ...[...everyOp].map(([tool, ops]) => `\`${tool}\` (${ops.join(", ")})`),
20504
+ ...Object.keys(HEADLESS_TOOLS).map((tool) => `\`${tool}\``)
20505
+ ].toSorted();
20450
20506
  return [
20451
20507
  "## Calls a CI job can make",
20452
20508
  "",
20453
20509
  "These answer from a short-lived headless engine: no editor window, no running game, and no",
20454
- "process or port outliving the answer. They are the only calls an automatic check can make at",
20455
- "all, because every other tool needs an editor somebody has open or a game somebody is running.",
20510
+ "process or port outliving the answer.",
20456
20511
  "",
20457
20512
  ...byTool.map((line) => `- ${line}`),
20458
20513
  "",
@@ -20461,11 +20516,14 @@ function headlessSection() {
20461
20516
  "write under `.godot`, and `project_import refresh_uids` writes a `.uid` beside each script that",
20462
20517
  "had none. A CI job checks with the reading ops. Each op's own summary below says which is which.",
20463
20518
  "",
20464
- "`scene_create`, `scene_node`, `scene_signal` and `resource_edit` go through the editor when one",
20465
- "serves the project and through a headless engine when none does, so they need nothing open",
20466
- "either. A CI job can call them, and checks with the reads among them. Everything else",
20467
- "needs the editor (`editor_*`, `debug_*`, `scene_animation` but its reads) or a running game",
20468
- "(`runtime_*`).",
20519
+ "`scene_create`, `scene_node`, `scene_signal`, `scene_animation`, `scene_tree` and `resource_edit`",
20520
+ "go through the editor when one serves the project and through a headless engine when none does.",
20521
+ "`project_info`, `project_search`, `project_test` and `script_diagnostics` need no editor either.",
20522
+ "A CI job can call all of them, and checks with the reads among them. `editor_run check` boots the",
20523
+ "project in a headless engine of its own, so a CI job can call it too. The other `editor_*`",
20524
+ "calls, and the `debug_*` and `runtime_*` ones, act on an editor or a running game.",
20525
+ "`editor_run start` starts a game with no editor open. `debug_breakpoint` holds breakpoints until",
20526
+ "a game runs.",
20469
20527
  "",
20470
20528
  '`project_settings get` with `from: "editor"` asks the open editor instead of the file and',
20471
20529
  "starts no engine. A CI job leaves it alone, because there is no editor on a build machine and",
@@ -20506,8 +20564,9 @@ function renderToolsMarkdown() {
20506
20564
  `${TOOL_SPECS.length} tools, named \`domain_verb\`. A tool that does several related things takes an`,
20507
20565
  "`op`. An unknown op or argument is refused with the valid set listed. Generated from the server.",
20508
20566
  "",
20509
- `${projectPathSentence()}: the \`runtime_*\` and \`debug_*\` ones pick between running games`,
20510
- "instead, and the others answer about the editor on this server.",
20567
+ `${projectPathSentence()}. The \`debug_*\` calls among them answer about the game this server`,
20568
+ "started or the editor is playing, and the `editor_*` ones about the editor on this server. A",
20569
+ "`runtime_*` call takes `projectPath` only to pick a game.",
20511
20570
  "",
20512
20571
  theGamePicked(),
20513
20572
  "",
@@ -49918,52 +49977,6 @@ var init_lifecycle_log = __esm(() => {
49918
49977
  init_server_version();
49919
49978
  });
49920
49979
 
49921
- // src/nearest.ts
49922
- function similarity(one, other) {
49923
- if (one === other) {
49924
- return 1;
49925
- }
49926
- if (one.length < 2 || other.length < 2) {
49927
- return 0;
49928
- }
49929
- const pairs = (text) => Array.from({ length: text.length - 1 }, (_, at) => text.slice(at, at + 2));
49930
- const ones = pairs(one);
49931
- const others = pairs(other);
49932
- const shared = ones.filter((pair) => others.includes(pair)).length;
49933
- return 2 * shared / (ones.length + others.length);
49934
- }
49935
- function withNearestFiles(said, wanted, files, beside) {
49936
- const stem = (path) => (path.split("/").pop() ?? path).replace(/\.[^.]*$/, "");
49937
- const named = said.split(/[\s,;]+/).map((word) => word.replace(/[.:]+$/, ""));
49938
- const namesakes = beside.filter((file) => file !== wanted && stem(file) === stem(wanted) && !named.includes(file)).toSorted();
49939
- if (namesakes.length > 0) {
49940
- const alone = namesakes.length === 1;
49941
- return `${said}. The ${alone ? "file" : "files"} of that name beside it ${alone ? "is" : "are"} ${namesakes.join(", ")}.`;
49942
- }
49943
- const extension = /\.([^./]+)$/.exec(wanted)?.[1];
49944
- if (extension === undefined) {
49945
- return said;
49946
- }
49947
- if (files.length === 0) {
49948
- return `${said}. The project holds no .${extension} file.`;
49949
- }
49950
- const close = nearest(stem(wanted), files.map((file) => stem(file)));
49951
- const offered = files.filter((file) => close.includes(stem(file)));
49952
- if (offered.length > 0) {
49953
- return `${said}. The nearest .${extension} ${offered.length === 1 ? "file is" : "files are"} ${offered.join(", ")}.`;
49954
- }
49955
- if (files.length <= LISTED_FILES) {
49956
- return `${said}. The project's .${extension} files are ${files.join(", ")}.`;
49957
- }
49958
- return `${said}. None of the project's ${files.length} .${extension} files is named like it.`;
49959
- }
49960
- function nearest(word, names) {
49961
- const wanted = word.toLowerCase();
49962
- const score = (name) => similarity(name.toLowerCase(), wanted);
49963
- return names.filter((name) => score(name) >= 0.5).toSorted((one, other) => score(other) - score(one)).slice(0, 3);
49964
- }
49965
- var LISTED_FILES = 12;
49966
-
49967
49980
  // src/outside.ts
49968
49981
  import process17 from "node:process";
49969
49982
  import { fileURLToPath as fileURLToPath5 } from "node:url";
@@ -53248,7 +53261,7 @@ class GodotServer {
53248
53261
  return this.saying(answer, {
53249
53262
  update_available: {
53250
53263
  ...notice,
53251
- what_to_do: "Tell the user a newer gdharness is out, with what changed. Run the upgrade command " + "only with their agreement: a yes now, or a standing instruction of theirs, such as one " + "in their AGENTS.md or CLAUDE.md, to upgrade on every release. Without either, offer it " + "and wait, since it replaces the addons in their project. Afterwards reconnect the MCP " + "server. Restart the editor with editor_launch restart only when upgrade says the editor " + "addons changed."
53264
+ what_to_do: "Tell the user a newer gdharness is out, with what changed. Run the upgrade command " + "only with their agreement: a yes now, or a standing instruction of theirs, such as one " + "in their AGENTS.md or CLAUDE.md, to upgrade on every release. Without either, offer it " + "and wait, since it replaces the addons in their project. Afterwards reconnect the MCP " + "server. Restart an open editor with editor_launch restart only when the upgrade output " + "asks for it."
53252
53265
  }
53253
53266
  });
53254
53267
  }
@@ -53446,7 +53459,7 @@ class GodotServer {
53446
53459
  case "project_search":
53447
53460
  return this.handleSearchProject(args);
53448
53461
  case "project_dependencies":
53449
- return await this.headless(args["direction"] === "reverse" ? "find_resource_usages" : "get_dependencies", args);
53462
+ return await this.headless(DEPENDENCY_OPERATIONS[args["direction"] === "reverse" ? "reverse" : "forward"], args);
53450
53463
  case "project_export":
53451
53464
  return await this.handleExportProject(args);
53452
53465
  case "project_test":
@@ -53772,9 +53785,9 @@ class GodotServer {
53772
53785
  ...told === null ? {} : ("unreached" in told) || typeof told["refused"] === "string" ? {
53773
53786
  runningGame: `The game this server started is running, and its debugger was not told: ${String("unreached" in told ? told.unreached : told["refused"])}. It takes the line when it is started again.`
53774
53787
  } : {
53775
- runningGame: told["taken"] === false ? `Told to the running game (pid ${kept?.pid ?? "?"}), which this server started, and not taken by it within ${READ_MS / 1000}s: a game reads its debugger between frames and every 2048 lines of script, so one inside a long frame takes the line once that frame ends.` : `Told to the running game (pid ${kept?.pid ?? "?"}), which this server started, and taken by it: anything it runs from now on stops there.`
53788
+ runningGame: told["taken"] === false ? `Told to the running game (pid ${kept?.pid ?? "?"}), which this server started, and not taken by it within ${READ_MS / 1000}s: a game reads its debugger between frames and every 2048 lines of script, so one inside a long frame takes the change once that frame ends.` : op === "set" ? `Told to the running game (pid ${kept?.pid ?? "?"}), which this server started, and taken by it: anything it runs from now on stops there.` : `Told to the running game (pid ${kept?.pid ?? "?"}), which this server started, and taken by it: it no longer stops on that line.`
53776
53789
  },
53777
- note: editor ? "The editor keeps a breakpoint set this way for one play. Every breakpoint held here is sent again before each play editor_run starts, handed to every game this server starts itself, and kept in the project for the server after a reconnect." : "No editor is connected, so the breakpoints under held are kept here: every game this server starts is handed them as it starts, an editor that connects is sent them before its first play, and they are kept in the project for the server after a reconnect.",
53790
+ note: editor ? "The editor keeps a breakpoint set this way for one play. Every breakpoint held here is sent again before each play editor_run starts, handed to every game this server starts itself, and kept in the project for the server after a reconnect." : held.length === 0 ? "No editor is connected and no breakpoint is held here, so the next game this server starts and the next editor to connect are given none." : "No editor is connected, so the breakpoints under held are kept here: every game this server starts is handed them as it starts, an editor that connects is sent them before its first play, and they are kept in the project for the server after a reconnect.",
53778
53791
  ...editor ? this.breakpointsAtRisk() : {}
53779
53792
  });
53780
53793
  }
@@ -56394,7 +56407,7 @@ ${error.text}`;
56394
56407
  ]);
56395
56408
  }
56396
56409
  if (!before.connected || before.editorPid === undefined) {
56397
- return this.createErrorResponse(`An editor has opened the bridge and has not said who it is within ${greetingWaitMs() / 1000}s, so there is no process to wait on once it is asked to go.`, ["editor_status says when it has, under greeting while it has not", "Then ask for the close again"]);
56410
+ return this.createErrorResponse(`An editor has opened the bridge and has not said who it is within ${greetingWaitMs() / 1000}s, so there is no process to wait on once it is asked to quit.`, ["editor_status says when it has, under greeting while it has not", "Then ask for the close again"]);
56398
56411
  }
56399
56412
  const pid = before.editorPid;
56400
56413
  const settingsBefore = this.settingKeysOf(before.projectPath);
@@ -56713,7 +56726,7 @@ ${error.text}`;
56713
56726
  return this.createErrorResponse("An exported build runs the main scene its pack holds, and takes no scene: leave scene out, or run the project without executable.");
56714
56727
  }
56715
56728
  if (exported !== null && readBoolean(args, "profile") === true) {
56716
- return this.createErrorResponse("An exported build cannot be profiled, because a release build quits when given --remote-debug. Leave out executable to profile the project.");
56729
+ return this.createErrorResponse("An exported build is started without the debugger, because a release build quits on --remote-debug. A profile needs the debugger, so leave out executable to profile the project.");
56717
56730
  }
56718
56731
  const own = readStringArray(args, "args") ?? [];
56719
56732
  const given = suites.value === null ? own : [...own, ...suitesSceneArgs(project.value.path, suites.value)];
@@ -58708,7 +58721,7 @@ ${error.text}`;
58708
58721
  } else if (lost.length > 0) {
58709
58722
  notes.push("The scan wrote the class cache from the list this editor is holding, which is shorter than the file was, so the cache was rebuilt from the files and these classes are back in it. Nothing was lost, but this editor is still holding the short list: do not rescan again until it has been restarted with editor_launch restart, because it will write the same list over the file each time.");
58710
58723
  } else if (unseen.length > 0) {
58711
- notes.push("The scan finished and these classes are still not in the list the editor resolves against, so every use of them reads as an unknown identifier. Its walk skips a file another engine has already imported. Rescan again on its own, which is the measured cure and needs no change to the declaring script, or restart the editor with editor_launch restart.");
58724
+ notes.push("The scan finished and these classes are still not in the list the editor resolves against, so every use of them reads as an unknown identifier. Its walk skips a file another engine has already imported. Call editor_rescan again on its own, which is the measured cure and needs no change to the declaring script, or restart the editor with editor_launch restart.");
58712
58725
  }
58713
58726
  if (dependents !== null && dependents.notReloaded.length > 0) {
58714
58727
  notes.push(notReloadedNote(broughtIn, dependents.notReloaded));
@@ -60589,10 +60602,11 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
60589
60602
 
60590
60603
  ## Start here
60591
60604
 
60592
- - ${projectPathSentence()}. Those but the \`debug_*\` ones answer about the editor this server is
60593
- connected to. There is no ambient project.
60594
- - \`runtime_*\` and \`debug_*\` pick a game by \`projectPath\`, or by \`pid\` when several run
60595
- from one project, as a bench and its workers do.
60605
+ - ${projectPathSentence()}. Those answer about the editor or game this server is connected to.
60606
+ There is no ambient project.
60607
+ - \`runtime_*\` calls pick a game by \`projectPath\`, or by \`pid\` when several run from one
60608
+ project, as a bench and its workers do. \`debug_*\` calls go to the game this server started or
60609
+ the editor is playing. \`debug_breakpoint\` also holds breakpoints when no game runs.
60596
60610
  ${theGamePicked()}
60597
60611
  - An argument a tool does not declare is refused rather than ignored.
60598
60612
  - \`editor_status\` says whether an editor is connected and whether its addon matches the server.
@@ -60614,16 +60628,16 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
60614
60628
  growing, a process list or a timeout. From outside, a bench between prints looks the same as a
60615
60629
  dead one.
60616
60630
  - \`endedBy\` says whether gdharness ended a run. \`running\` comes from the operating system when
60617
- the run has a pid: every run started through \`editor_run\`, and every editor-played run whose
60618
- game announced its runtime. A played game with no runtime addon has none, so \`running\` is the
60619
- editor's answer and can lag by seconds. \`editor_status\` lists a game under \`runtimes\`
60620
- without asking the editor.
60631
+ the run has a pid: every run this server started itself, and every editor-played run whose game
60632
+ announced its runtime. An editor-played game with no runtime addon has no pid, so \`running\` is
60633
+ the editor's answer and can lag by seconds. \`editor_status\` does not list such a game under
60634
+ \`runtimes\`.
60621
60635
  - When every tool says the project is fine and something is still wrong, read the editor's own
60622
60636
  console with \`editor_output op: "editor"\`. It runs from the editor's first line, with repeats
60623
60637
  grouped under \`repeated\`. It exists only for an editor gdharness opened, and one opened by hand
60624
60638
  is refused. The line before a burst of parse errors often names the plugin behind it.
60625
60639
  - One server, one editor. A second project needs a second harness session with its own server.
60626
- Refuse a second editor on one project.
60640
+ Never open a second editor on a project that already has one.
60627
60641
 
60628
60642
  ## Measuring a running game
60629
60643
 
@@ -60678,9 +60692,10 @@ prefixes, empty by default. A run started on a scene under one of them neither s
60678
60692
  announces, nor do the engines it starts, unless it was started to be profiled from its start.
60679
60693
 
60680
60694
  The autoload names \`res://${RUNTIME_AUTOLOAD.path}\`, the loader \`gdharness setup\` writes, which
60681
- brings the runtime up only in a debug build that has the addon. A release export leaves the entry,
60682
- the loader and the addons out. A debug export keeps them. The loader is committed with \`project.godot\`, and \`gdharness doctor\` says when it differs
60683
- from the one gdharness ships.
60695
+ brings the runtime up only in a debug build that has the addon. A release export leaves out the
60696
+ entry, the loader and the runtime, editor and auto_reload addons. A debug export keeps them. The
60697
+ loader is committed with \`project.godot\`, and \`gdharness doctor\` says when it differs from the
60698
+ one gdharness ships.
60684
60699
 
60685
60700
  ## At a breakpoint
60686
60701
 
@@ -60699,11 +60714,11 @@ so take it off when the question it was for is answered.
60699
60714
 
60700
60715
  \`scene_tree\` reads what a scene holds, rather than what the \`.tscn\` text implies, and
60701
60716
  \`resource_edit read\` reads a resource. Writes go through \`scene_create\`, \`scene_node\`,
60702
- \`scene_signal\` and the other \`resource_edit\` ops but \`move\`. All of them use the editor when
60703
- one serves the project, so the files keep their formatting, and a headless engine otherwise. The
60704
- headless engine runs the scene's scripts, so the answer puts back what they changed. It refuses a
60705
- file where it cannot, such as a scene whose script does not compile. For those, open the editor
60706
- (\`editor_launch\` with \`hidden: true\`) and ask again.
60717
+ \`scene_signal\`, \`scene_animation\` and every \`resource_edit\` op but \`move\`. All of them use
60718
+ the editor when one serves the project, so the files keep their formatting, and a headless engine
60719
+ otherwise. The headless engine runs the scene's scripts, so the answer puts back what they
60720
+ changed. It refuses a file where it cannot, such as a scene whose script does not compile. For
60721
+ those, open the editor (\`editor_launch\` with \`hidden: true\`) and ask again.
60707
60722
 
60708
60723
  \`script_info\` comes from the editor's own language server, and so does
60709
60724
  \`script_diagnostics\` when this project's editor is open. Without it a headless engine compiles the
@@ -60713,9 +60728,8 @@ scripts and reports their errors, with lines but no columns and no warnings.
60713
60728
  \`project_export\`, \`project_test\`, \`script_edit\`, \`script_info structure\` and
60714
60729
  \`editor_classes\` need nothing open: they run a short headless engine and are gone before the
60715
60730
  answer is printed. The exception is \`script_edit rename\` of a method, variable, signal or other
60716
- member, which asks the editor's language server which uses are that class's. \`project_settings
60717
- get\` also starts an engine, so a setting absent from \`project.godot\` answers with the engine's
60718
- default.
60731
+ member, which asks the editor's language server which uses are that class's. A setting absent
60732
+ from \`project.godot\` comes back from \`project_settings get\` with the engine's default.
60719
60733
 
60720
60734
  To rename a class or a member across the project, use \`script_edit rename\` rather than a text
60721
60735
  replacement. It changes the uses the engine resolves, doc links and what scenes record, and lists
@@ -60745,10 +60759,10 @@ its own copy. So a long run is not a reason to stop editing, and the new code ar
60745
60759
 
60746
60760
  A wall of "Could not find type" on the project's own classes means the editor is not holding
60747
60761
  them. \`editor_rescan\` asks it to scan and names any class it still cannot resolve under
60748
- \`unseenByEditor\`: its walk skips a file a headless engine has already imported, so the
60749
- declaration and the cache can both be right while the editor stays blind to it. Run
60750
- \`editor_rescan\` again, which needs no change to the declaring script. \`editor_launch restart\`
60751
- also fixes it, at the cost of an editor restart.
60762
+ \`unseenByEditor\`: the editor's own scan skips a file a headless engine has already imported, so
60763
+ the declaration and the cache can both be right while the editor stays blind to it. For a class
60764
+ under \`unseenByEditor\`, run \`editor_rescan\` again, which needs no change to the declaring
60765
+ script. \`editor_launch restart\` also fixes it.
60752
60766
 
60753
60767
  With no editor open, the headless engine that compiles for \`script_diagnostics\` and for
60754
60768
  \`project_info\`'s health and validation resolves class names only from that cache file. A
@@ -60761,7 +60775,9 @@ The scan writes \`.godot/global_script_class_cache.cfg\` from the list the edito
60761
60775
  a class it cannot resolve would go out of that file with it. The answer names that loss under
60762
60776
  \`cacheLost\` and rebuilds the cache from the declarations on disk, naming what came back under
60763
60777
  \`cacheRestored\`, so no fresh engine, CI run or clone inherits the short file. That is the same
60764
- class \`unseenByEditor\` names, and the loss is not a reason to refuse the scan.
60778
+ class \`unseenByEditor\` names, and the loss is not a reason to refuse the scan. An editor that lost
60779
+ classes this way still holds the short list and writes it again on every scan. Restart it with
60780
+ \`editor_launch restart\` before the next \`editor_rescan\`.
60765
60781
 
60766
60782
  The editor also scans on its own, when files change under it, and it rewrites that file just after
60767
60783
  the scan ends. Every engine this server starts on the project (a run, a boot check, a test run, a
@@ -62647,7 +62663,15 @@ async function registerRuntime(godot, projectPath) {
62647
62663
  }
62648
62664
  var SETUP_FLAGS = new Set(["--runtime", "--no-runtime", "--no-connect", "--no-skill", "--json", "--yes"]);
62649
62665
  function unknownOption(flag) {
62650
- return new UsageError(`Unknown option ${flag}. gdharness help lists each command's flags, and gdharness harnesses lists the harness flags.`);
62666
+ const known = [...SETUP_FLAGS, ...HARNESSES.map((harness) => `--${harness.id}`)];
62667
+ const [close] = nearest(flag.slice(2), known.map((name) => name.slice(2)));
62668
+ const offered = close === undefined ? "" : ` Did you mean --${close}?`;
62669
+ return new UsageError(`Unknown option ${flag}.${offered} gdharness help lists each command's flags, and gdharness harnesses lists the harness flags.`);
62670
+ }
62671
+ function asksForHelp() {
62672
+ const own = args.slice(1);
62673
+ const handedOn = own.indexOf("--");
62674
+ return (handedOn === -1 ? own : own.slice(0, handedOn)).some((arg) => arg === "--help" || arg === "-h");
62651
62675
  }
62652
62676
  function namedHarnesses() {
62653
62677
  const flags = args.filter((arg) => arg.startsWith("--") && !SETUP_FLAGS.has(arg));
@@ -62949,7 +62973,7 @@ function doctorReport(projectPath) {
62949
62973
  async function runtime2() {
62950
62974
  const state = args[1];
62951
62975
  if (state !== "on" && state !== "off") {
62952
- throw new UsageError(`gdharness runtime takes on or off, not ${state ?? "nothing"}. Example: gdharness runtime on path/to/project`);
62976
+ throw new UsageError(`gdharness runtime takes on or off${state === undefined ? "" : `, not ${state}`}. Example: gdharness runtime on path/to/project`);
62953
62977
  }
62954
62978
  const projectPath = projectArgument(2);
62955
62979
  const godot = await engine(projectPath);
@@ -63012,11 +63036,11 @@ Usage:
63012
63036
  gdharness upgrade [project] Reinstall the addons at this version and re-pin every config
63013
63037
  that already names gdharness. Asks nothing, and touches no
63014
63038
  other config. Then reconnect the MCP server, and restart an
63015
- open editor if the output says the editor addons changed.
63039
+ open editor when the upgrade output asks for it.
63016
63040
  gdharness uninstall [project] [--<harness>]
63017
- Take it all back out: the addons, the editor plugins, the
63018
- runtime autoload, the skill, and our own entry in every
63019
- config it can parse. Other servers in those files are
63041
+ Remove the addons, the editor plugins, the runtime
63042
+ autoload, the skill and gdharness's entry in every config
63043
+ it can parse. Other servers in those files are
63020
63044
  untouched. A machine-wide config may serve another project,
63021
63045
  so it is named rather than edited unless you ask by flag.
63022
63046
  gdharness harnesses Every harness, its flag and the file it reads
@@ -63100,6 +63124,10 @@ async function main() {
63100
63124
  }
63101
63125
  const served = process.env["GDHARNESS_PROJECT"];
63102
63126
  nameSlotTaker(command !== undefined ? `gdharness ${command}` : `a gdharness server${served === undefined || served === "" ? "" : ` of ${served}`}`);
63127
+ if (command !== undefined && asksForHelp()) {
63128
+ printHelp();
63129
+ return;
63130
+ }
63103
63131
  if (command === "test") {
63104
63132
  await test();
63105
63133
  return;
@@ -954,6 +954,12 @@ func _serialize_node_tree(
954
954
  _serialize_node_tree(child, depth + 1, max_depth, include_properties, named)
955
955
  )
956
956
  result["children"] = children
957
+ else:
958
+ # Below the depth asked for, said rather than left out: a container with nothing under it
959
+ # read as empty, and the buttons of a dialog six levels down were never looked for.
960
+ var below: int = node.get_child_count(true)
961
+ if below > 0:
962
+ result["children_not_shown"] = below
957
963
 
958
964
  return result
959
965
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.3.61",
3
+ "version": "1.3.62",
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",