gdharness 1.3.1 → 1.3.2

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.
Files changed (34) hide show
  1. package/build/cli.js +311 -86
  2. package/build/godot/addons/gdharness_editor/declared_values.gd +116 -0
  3. package/build/godot/addons/gdharness_editor/declared_values.gd.uid +1 -0
  4. package/build/godot/addons/gdharness_editor/file_reads.gd +5 -0
  5. package/build/godot/addons/gdharness_editor/property_values.gd +106 -10
  6. package/build/godot/addons/gdharness_editor/reading.gd +37 -0
  7. package/build/godot/addons/gdharness_editor/serialisation.gd +4 -0
  8. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +133 -39
  9. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +9 -9
  10. package/build/godot/addons/gdharness_editor/value_shapes.gd +7 -0
  11. package/build/godot/addons/gdharness_runtime/reading.gd +37 -0
  12. package/build/godot/addons/gdharness_runtime/runtime_values.gd +27 -1
  13. package/build/godot/addons/gdharness_runtime/serialisation.gd +4 -0
  14. package/build/godot/addons/gdharness_runtime/value_shapes.gd +7 -0
  15. package/build/godot/operations/classdb_queries.gd +13 -3
  16. package/build/godot/operations/declared_values.gd +116 -0
  17. package/build/godot/operations/dependencies.gd +109 -5
  18. package/build/godot/operations/export_presets.gd +4 -6
  19. package/build/godot/operations/file_reads.gd +5 -0
  20. package/build/godot/operations/file_walk.gd +34 -0
  21. package/build/godot/operations/gdscript_analysis.gd +7 -2
  22. package/build/godot/operations/gdscript_authoring.gd +105 -2
  23. package/build/godot/operations/godot_operations.gd +2 -0
  24. package/build/godot/operations/import_pipeline.gd +39 -0
  25. package/build/godot/operations/input_actions.gd +73 -1
  26. package/build/godot/operations/project_config.gd +59 -3
  27. package/build/godot/operations/project_diagnostics.gd +27 -2
  28. package/build/godot/operations/property_values.gd +106 -10
  29. package/build/godot/operations/reading.gd +37 -0
  30. package/build/godot/operations/resource_files.gd +4 -1
  31. package/build/godot/operations/script_check.gd +16 -0
  32. package/build/godot/operations/serialisation.gd +4 -0
  33. package/build/godot/operations/value_shapes.gd +7 -0
  34. package/package.json +1 -1
package/build/cli.js CHANGED
@@ -7864,7 +7864,8 @@ function resolveWithinProject(projectPath, candidatePath, options = {}) {
7864
7864
  if (trimmed.includes("\x00")) {
7865
7865
  return { ok: false, reason: "Path contains a null byte." };
7866
7866
  }
7867
- const withoutScheme = trimmed.startsWith(RESOURCE_SCHEME) ? trimmed.slice(RESOURCE_SCHEME.length) : trimmed;
7867
+ const root = resolve(projectPath);
7868
+ const withoutScheme = trimmed.startsWith(RESOURCE_SCHEME) ? trimmed.slice(RESOURCE_SCHEME.length) : isAbsolute(trimmed) && isWithinRoot(root, trimmed) ? relative(root, resolve(trimmed)).split(sep).join("/") : trimmed;
7868
7869
  if (withoutScheme.startsWith("uid://")) {
7869
7870
  return {
7870
7871
  ok: false,
@@ -7881,7 +7882,7 @@ function resolveWithinProject(projectPath, candidatePath, options = {}) {
7881
7882
  if (withoutScheme.startsWith("/") || withoutScheme.startsWith("\\") || isAbsolute(withoutScheme)) {
7882
7883
  return {
7883
7884
  ok: false,
7884
- reason: `Path '${trimmed}' is absolute; give a path inside the project, such as 'scenes/main.tscn'.`
7885
+ reason: `Path '${trimmed}' is absolute and outside the project; give a path inside it, such as 'scenes/main.tscn'.`
7885
7886
  };
7886
7887
  }
7887
7888
  const kept = keptInside(trimmed);
@@ -7897,7 +7898,6 @@ function resolveWithinProject(projectPath, candidatePath, options = {}) {
7897
7898
  reason: `Path '${trimmed}' contains ':', which no file inside a project is named with.`
7898
7899
  };
7899
7900
  }
7900
- const root = resolve(projectPath);
7901
7901
  const absolutePath = resolve(root, withoutScheme);
7902
7902
  if (!isWithinRoot(root, absolutePath)) {
7903
7903
  return { ok: false, reason: `Path '${trimmed}' resolves outside the project directory.` };
@@ -10678,6 +10678,14 @@ function unloadedTypes(messages, declared, cached) {
10678
10678
  }
10679
10679
  return found;
10680
10680
  }
10681
+ function uncachedFor(projectPath, messagesPer) {
10682
+ if (!messagesPer.some((messages) => messages.some((message) => unknownTypeIn(message) !== null))) {
10683
+ return messagesPer.map(() => []);
10684
+ }
10685
+ const declared = declaredClasses(projectPath);
10686
+ const cached = cachedClasses(projectPath);
10687
+ return messagesPer.map((messages) => unloadedTypes(messages, declared, cached).filter((type) => !type.inTheClassCache));
10688
+ }
10681
10689
  function classesNamedIn(source, known) {
10682
10690
  const named = new Set;
10683
10691
  for (const [word] of source.matchAll(/\b[A-Z][A-Za-z0-9_]*\b/g)) {
@@ -10818,6 +10826,16 @@ function uncachedClassNote(types) {
10818
10826
  const one = types.length === 1;
10819
10827
  return `${types.join(", ")} ${one ? "is declared" : "are declared"} in this project and missing from ` + `.godot/global_script_class_cache.cfg, so a game launched now would not resolve ${one ? "it" : "them"} ` + "either. Try editor_rescan first: it asks the editor already running and starts nothing, and it " + "cleared this in 314ms in a second project with a 31-worker fan-out still importing. It is not " + "always enough, because a file another engine has already imported reads as settled and the " + "walk does not look inside it. project_import refresh_classes rewrites the cache from the " + "declarations on disk and does reach that case, at the cost of a short headless engine, which " + "is worth knowing when something else is already running against this project.";
10820
10828
  }
10829
+ function uncachedForTheEngineNote(projectPath, types) {
10830
+ if (types.length === 0) {
10831
+ return "";
10832
+ }
10833
+ const one = types.length === 1;
10834
+ const named = types.map((type) => `${type.type} (${type.declaredIn})`).join(", ");
10835
+ const cacheExists = existsSync11(join12(projectPath, ".godot", "global_script_class_cache.cfg"));
10836
+ const state = cacheExists ? `missing from .godot/global_script_class_cache.cfg, which is where the engine that compiled these scripts resolves a class name from, so the error is the cache being behind the files rather than the script.` : `the engine that compiled these scripts resolves a class name from .godot/global_script_class_cache.cfg, which has not been written yet, as in a project no editor has opened, so the error is the missing cache rather than the script.`;
10837
+ return `${named} ${one ? "is" : "are"} declared in this project${cacheExists ? " and" : ", but"} ${state} ` + "project_import refresh_classes writes the cache from the declarations on disk; editor_run start " + "writes it before a game, and an editor opening the project writes it when it scans.";
10838
+ }
10821
10839
  function cacheWrittenAt(projectPath) {
10822
10840
  const cache = join12(projectPath, ".godot", "global_script_class_cache.cfg");
10823
10841
  try {
@@ -11046,6 +11064,17 @@ function ignoredTexts(projectPath) {
11046
11064
  visit(projectPath, "", false);
11047
11065
  return found;
11048
11066
  }
11067
+ function plainMatch(line, needle, caseSensitive) {
11068
+ if (caseSensitive) {
11069
+ return line.includes(needle) ? [needle] : null;
11070
+ }
11071
+ const lowered = line.toLowerCase();
11072
+ const at = lowered.indexOf(needle);
11073
+ if (at < 0) {
11074
+ return null;
11075
+ }
11076
+ return [lowered.length === line.length ? line.slice(at, at + needle.length) : needle];
11077
+ }
11049
11078
  function searchProject(projectPath, options) {
11050
11079
  const extensions = options.fileTypes === undefined ? null : new Set(options.fileTypes.map((ext) => ext.replace(/^\./, "").toLowerCase()).filter(Boolean));
11051
11080
  const result = {
@@ -11083,7 +11112,7 @@ function searchProject(projectPath, options) {
11083
11112
  result.summary.files_searched += 1;
11084
11113
  const matches = [];
11085
11114
  for (const [index, line] of bytes.toString("utf8").split(/\r?\n/).entries()) {
11086
- const found = regex ? regex.exec(line) : (options.caseSensitive ? line : line.toLowerCase()).includes(needle) ? [options.query] : null;
11115
+ const found = regex ? regex.exec(line) : plainMatch(line, needle, options.caseSensitive);
11087
11116
  if (found === null) {
11088
11117
  continue;
11089
11118
  }
@@ -11091,7 +11120,7 @@ function searchProject(projectPath, options) {
11091
11120
  result.summary.truncated = true;
11092
11121
  break;
11093
11122
  }
11094
- matches.push({ line: index + 1, content: line.trim(), match: found[0] ?? "" });
11123
+ matches.push({ line: index + 1, content: line.trim(), match: found[0] });
11095
11124
  result.summary.total_matches += 1;
11096
11125
  }
11097
11126
  if (matches.length > 0) {
@@ -11879,6 +11908,7 @@ var init_tool_definitions = __esm(() => {
11879
11908
  init_launch();
11880
11909
  PROJECT_INFO_SECTIONS = [
11881
11910
  "autoloads",
11911
+ "input_actions",
11882
11912
  "plugins",
11883
11913
  "export_presets",
11884
11914
  "audio_buses",
@@ -11912,7 +11942,7 @@ var init_tool_definitions = __esm(() => {
11912
11942
  };
11913
11943
  PROPERTIES = {
11914
11944
  type: "object",
11915
- description: `Properties to set, keyed by Godot property name. Vectors, colours and the like may be written as {"x": 1, "y": 2} or tagged {"_type": "Vector2", "x": 1, "y": 2}, which is the form a read answers with, so a value read off one node can be written straight to another. One carrying a field its type has not, such as a z for a Vector2, is refused naming it. A colour may also be given as its html code, "#ff8000" or "#ff800080". A key may also be a colon path into one of the components of a vector, a colour, a rectangle or a transform, as GDScript names them, so one axis is written alone: "position:x", "modulate:a", "region_rect:size:y"; a component the value does not have is refused, naming the ones it has. A property holding a node takes the path to it from the node being set, such as "../Player". Each value is read back once set, and one the engine changed or refused is refused with what it holds. ${WHOLE_NUMBERS}`,
11945
+ description: `Properties to set, keyed by Godot property name. Vectors, colours and the like may be written as {"x": 1, "y": 2} or tagged {"_type": "Vector2", "x": 1, "y": 2}, which is the form a read answers with, so a value read off one node can be written straight to another. One carrying a field its type has not, such as a z for a Vector2, is refused naming it. A colour may also be given as its html code, "#ff8000" or "#ff800080". A key may also be a colon path into one of the components of a vector, a colour, a rectangle or a transform, as GDScript names them, so one axis is written alone: "position:x", "modulate:a", "region_rect:size:y"; a component the value does not have is refused, naming the ones it has. A property holding a node takes the path to it from the node being set, such as "../Player". Each value is read back once set, and one the engine changed or refused is refused with what it holds. An enum property takes the name of a value, as the script spells it, or its number; a value past an export range, or a number no value of an enum has, is written as given and named under outsideDeclared. ${WHOLE_NUMBERS}`,
11916
11946
  additionalProperties: true
11917
11947
  };
11918
11948
  XY = {
@@ -12005,7 +12035,7 @@ var init_tool_definitions = __esm(() => {
12005
12035
  };
12006
12036
  TILESET_SOURCES = {
12007
12037
  type: "array",
12008
- description: "Atlas sources, one per texture.",
12038
+ description: "Atlas sources, one per texture. A wholly transparent cell gets no tile, and the answer lists those cells as leftBlank; a strip at the right or bottom that no whole tile reaches is described in notInAnyTile. A path that already exists is refused, as create_tileset would replace the whole TileSet.",
12009
12039
  items: {
12010
12040
  type: "object",
12011
12041
  properties: {
@@ -12025,17 +12055,17 @@ var init_tool_definitions = __esm(() => {
12025
12055
  type: "object",
12026
12056
  properties: {
12027
12057
  coords: XY,
12028
- sourceId: { type: "number", description: "TileSet source index." },
12029
- atlasCoords: XY,
12058
+ sourceId: { type: "number", description: "TileSet source id, or -1 to erase the cell." },
12059
+ atlasCoords: { ...XY, description: "The tile in the source. Needed by every cell but an erased one." },
12030
12060
  alternativeTile: { type: "number" }
12031
12061
  },
12032
- required: ["coords", "sourceId", "atlasCoords"],
12062
+ required: ["coords", "sourceId"],
12033
12063
  additionalProperties: false
12034
12064
  }
12035
12065
  };
12036
12066
  COLOUR2 = {
12037
- type: "object",
12038
- description: "set_theme_color: r, g and b from 0 to 1, with a optional and opaque by default.",
12067
+ type: ["object", "string"],
12068
+ description: 'set_theme_color: r, g and b from 0 to 1, with a optional and opaque by default, or an html code such as "#ff8000" or "#ff800080".',
12039
12069
  properties: { r: { type: "number" }, g: { type: "number" }, b: { type: "number" }, a: { type: "number" } },
12040
12070
  required: ["r", "g", "b"],
12041
12071
  additionalProperties: false
@@ -12051,7 +12081,7 @@ var init_tool_definitions = __esm(() => {
12051
12081
  include: {
12052
12082
  type: "array",
12053
12083
  items: { type: "string", enum: [...PROJECT_INFO_SECTIONS] },
12054
- description: "Extra sections: registered autoloads, addons and whether each is enabled, export presets, the audio bus layout, a health report, or export validation."
12084
+ description: "Extra sections: registered autoloads, the input actions project.godot holds with their events (an engine ui_ action only when the project rebinds it, as engineAction), addons and whether each is enabled, export presets, the audio bus layout, a health report, or export validation. health and validation compile every script of the project in headless engines and name each that fails with its first error, and each file a scene, resource or script refers to that does not exist."
12055
12085
  },
12056
12086
  preset: { type: "string", description: "For validation: the export preset to validate against." },
12057
12087
  detail: {
@@ -12087,7 +12117,7 @@ var init_tool_definitions = __esm(() => {
12087
12117
  value: {
12088
12118
  blank: true,
12089
12119
  ops: ["set"],
12090
- description: 'The value to write, fitted to the type the setting is registered as, the way scene_node set fits a property. Engine types may be written as their fields, {"x": 1, "y": 2}, or tagged, {"_type": "Vector2", "x": 1, "y": 2}, and infinity as {"_type": "float", "value": "inf"}, the form a read answers with; one carrying a field its type has not is refused naming it, and a colour may also be its html code, "#ff8000". "" writes an empty string.'
12120
+ description: 'The value to write, fitted to the type the setting is registered as, the way scene_node set fits a property. Engine types may be written as their fields, {"x": 1, "y": 2}, or tagged, {"_type": "Vector2", "x": 1, "y": 2}, and infinity as {"_type": "float", "value": "inf"}, the form a read answers with; one carrying a field its type has not is refused naming it, and a colour may also be its html code, "#ff8000". "" writes an empty string. A setting the engine does not declare is written as given, a whole number as an int, and when one in its section is named like it, note names the nearest, as it may be a misspelt one.'
12091
12121
  },
12092
12122
  name: { type: "string", ops: ["add_autoload", "remove_autoload"], description: "Autoload name." },
12093
12123
  path: {
@@ -12222,7 +12252,7 @@ var init_tool_definitions = __esm(() => {
12222
12252
  direction: {
12223
12253
  type: "string",
12224
12254
  enum: ["forward", "reverse"],
12225
- description: "forward: what this resource loads, with cycles reported, a script's dependencies including every script whose class_name it uses, marked by_class with the name. summary.total_dependencies counts each file once however many places load it, and total_references counts the entries in the tree. reverse: every file that refers to it and how, a scene instancing it, a script extending, preloading or loading it, and for a script with a class_name every use of that name. Each reference carries a kind: ext_resource, extends, preload, load or path for a reference by path; setting, plugin, export_preset and import for one in project.godot, a plugin.cfg, export_presets.cfg or an import sidecar; class_name for a use of the declared name, the declaration itself being the script asked about. Two kinds are mentions rather than uses: doc is a name inside a ## documentation comment, which renaming would break in the generated docs, and comment is one in ordinary prose, which it would only make stale. summary.in_code is the count with both of those left out, which is the number to read when the question is whether anything still uses this. Default forward."
12255
+ description: "forward: what this resource loads, with cycles reported, a script's dependencies including every script whose class_name it uses, marked by_class with the name. summary.total_dependencies counts each file once however many places load it, total_references counts the entries in the tree, and missing names every file referenced that does not exist, with the files referring to it. reverse: every file that refers to it and how, for a file that is gone as well, answered with exists: false, a scene instancing it, a script extending, preloading or loading it, and for a script with a class_name every use of that name. Each reference carries a kind: ext_resource, extends, preload, load or path for a reference by path; setting, plugin, export_preset and import for one in project.godot, a plugin.cfg, export_presets.cfg or an import sidecar; class_name for a use of the declared name, the declaration itself being the script asked about. Two kinds are mentions rather than uses: doc is a name inside a ## documentation comment, which renaming would break in the generated docs, and comment is one in ordinary prose, which it would only make stale. summary.in_code is the count with both of those left out, which is the number to read when the question is whether anything still uses this. Default forward."
12226
12256
  },
12227
12257
  depth: { type: "number", description: "forward: how many levels to follow. Default unlimited." },
12228
12258
  includeBuiltin: {
@@ -12360,6 +12390,11 @@ var init_tool_definitions = __esm(() => {
12360
12390
  items: { type: "string" },
12361
12391
  description: 'Suites or cases to leave out, as "suite_name" or "suite_name:test_name". A bare test name is read as a suite name and leaves out nothing; an entry that named a case which ran all the same comes back under ignoreNotApplied with the form that would have left it out.'
12362
12392
  },
12393
+ tests: {
12394
+ type: "array",
12395
+ items: { type: "string" },
12396
+ description: 'Run only these cases of the suites under path, each "test_name" or "suite_name:test_name", the suite by its file name without .gd or its res:// path. Only the suites holding them run, with their other cases left out. A name no suite there declares as a top-level func is refused before the engine starts, with the nearest names; a case asked for that gdUnit4 did not report comes back under testsNotRun, and the run is not called passed.'
12397
+ },
12363
12398
  engineArgs: {
12364
12399
  type: "array",
12365
12400
  items: { type: "string" },
@@ -12672,7 +12707,7 @@ var init_tool_definitions = __esm(() => {
12672
12707
  {
12673
12708
  name: "script_edit",
12674
12709
  title: "Edit scripts",
12675
- description: "Creates a GDScript file, or adds functions, variables and signals to one. Every declaration written carries a type. Both load what they wrote under the project's own warning settings and answer with parses; the engine's reasons for a refusal come back under engine_messages. create answers with the extends and class_name the engine read, and a script declaring a class_name is put in the project's class list, with registered saying whether it is there. modify makes every addition or none, and answers with the line each one is on in the file written; with an editor serving the project it reloads the script there too, so the editor's answers read what was written, and editorReloaded says so (editorNote when it could not, or when a script tab holds unsaved edits that saving would write over the file). rename renames a class or a member wherever the engine uses it and writes every file or none. A new name already taken is refused with nothing written, a member's included when a class inheriting it already uses the name in code, as a local or a parameter. changed lists each file with its changed lines as they now read, and leftAlone lists every other whole-word occurrence of the old name with what the tokenizer makes of it (code, string, comment, doc, nodePath, scene, text, or ignored for a file under a .gdignore directory, which is never changed) and, where it is not plain, why: a string that is the old name whole, such as find_children's type or a call's method name, which nothing renames or compiles and which is listed first; a node named like the class; a call on a value whose type the analyser could not tell. A member rename answers under languageServer how many requests it made and how long they took. A comment, documentation line, string or text file line with nothing more to say is given under leftAloneInProse by file, kind and line instead, and code that is certainly another symbol of the same name, a bare name in a file where nothing inherits the member or one the language server resolved to another declaration, under leftAloneOtherSymbols as each why with the files and lines it covers. summary.leftAlone counts each of the three lists beside the total. When those two grouped lists run past 300 lines, they are written with each line's text to a file under the project's .godot directory instead, of which the newest ten are kept, and leftAloneDetail names the file and counts what is in it. Read both before calling the rename done; nothing in them was changed. Afterwards an open editor scans the changed files and reloads every changed script, which is also the compile check, and its answer comes back under editor; with no editor the class cache is rebuilt and the changed scripts are compiled in a fresh engine, under classCache and compiled. written is true once the files are written, and ok is false when what came after the write failed: the files are left changed, and note says so.",
12710
+ description: "Creates a GDScript file, or adds functions, variables and signals to one. Every declaration written carries a type. Both load what they wrote under the project's own warning settings and answer with parses; the engine's reasons for a refusal come back under engine_messages. create answers with the extends and class_name the engine read, and a script declaring a class_name is put in the project's class list, with registered saying whether it is there; a class_name another script or an engine class holds, or an extends naming no class, is refused with nothing written. modify makes every addition or none, refuses with nothing changed additions that would stop a script that parses from parsing, giving the engine's reasons, and answers with the line each one is on in the file written; with an editor serving the project it reloads the script there too, so the editor's answers read what was written, and editorReloaded says so (editorNote when it could not, or when a script tab holds unsaved edits that saving would write over the file). rename renames a class or a member wherever the engine uses it and writes every file or none. A new name already taken is refused with nothing written, a member's included when a class inheriting it already uses the name in code, as a local or a parameter. changed lists each file with its changed lines as they now read, and leftAlone lists every other whole-word occurrence of the old name with what the tokenizer makes of it (code, string, comment, doc, nodePath, scene, text, or ignored for a file under a .gdignore directory, which is never changed) and, where it is not plain, why: a string that is the old name whole, such as find_children's type or a call's method name, which nothing renames or compiles and which is listed first; a node named like the class; a call on a value whose type the analyser could not tell. A member rename answers under languageServer how many requests it made and how long they took. A comment, documentation line, string or text file line with nothing more to say is given under leftAloneInProse by file, kind and line instead, and code that is certainly another symbol of the same name, a bare name in a file where nothing inherits the member or one the language server resolved to another declaration, under leftAloneOtherSymbols as each why with the files and lines it covers. summary.leftAlone counts each of the three lists beside the total. When those two grouped lists run past 300 lines, they are written with each line's text to a file under the project's .godot directory instead, of which the newest ten are kept, and leftAloneDetail names the file and counts what is in it. Read both before calling the rename done; nothing in them was changed. Afterwards an open editor scans the changed files and reloads every changed script, which is also the compile check, and its answer comes back under editor; with no editor the class cache is rebuilt and the changed scripts are compiled in a fresh engine, under classCache and compiled. written is true once the files are written, and ok is false when what came after the write failed: the files are left changed, and note says so.",
12676
12711
  parameters: {
12677
12712
  projectPath: PROJECT_PATH,
12678
12713
  scriptPath: SCRIPT_PATH,
@@ -12755,7 +12790,7 @@ var init_tool_definitions = __esm(() => {
12755
12790
  operations: {
12756
12791
  structure: {
12757
12792
  effect: "read",
12758
- summary: "functions, variables, signals, class_name and extends, read from the file, and dependencies: the paths it loads or preloads as written, then the scripts declaring each project class_name its code uses, which classes_used names with the class; autoloads_used names each autoload the code uses by its global name, with the autoload's script, which the project loads rather than this script, so it is not under dependencies",
12793
+ summary: "functions, variables, signals, constants, enums, inner_classes, class_name, extends and line_count, read from the file, and dependencies: the paths it loads or preloads as written, then the scripts declaring each project class_name its code uses, which classes_used names with the class; autoloads_used names each autoload the code uses by its global name, with the autoload's script, which the project loads rather than this script, so it is not under dependencies",
12759
12794
  requires: []
12760
12795
  },
12761
12796
  symbols: {
@@ -12776,7 +12811,7 @@ var init_tool_definitions = __esm(() => {
12776
12811
  name: "script_diagnostics",
12777
12812
  title: "Script diagnostics",
12778
12813
  effect: "read",
12779
- description: "Errors and warnings for a script, or for several, from the editor's language server, and whether each is clean. With no editor of this project open, so no language server of it to ask, a short headless engine compiles the scripts instead and the answer says checkedBy: engine: every error that stops a script compiling, with its line and no column, dependenciesWithErrors naming the other scripts that failed while it loaded, and warnings null, since the engine reports warnings only to an editor. Each diagnostic carries its severity (error or warning), its message, and where it is as the editor shows it: line and column counted from 1, and endLine and endColumn just past its end. Diagnostics are checked against the project's own files, and the two that the files disprove are named rather than passed on: a member the class cache's file declares, or a project class it extends does, a method, property, constant, static function or enum member, comes back under contradictedByTheFile with the script declaring it, and so does one a diagnostic names alone as an external class member, read against the class it is looked up on in the line it is on; and a class this project declares that the diagnostic could not resolve comes back under typesTheEditorHasNotLoaded, with inTheClassCache saying whether a launched game would resolve it. staleAnalysis then says what to do, and for both states it is editor_rescan first: it asks the editor already running, starts nothing, and has cleared each in every reproduction measured. Two exceptions are named when they apply. For an enum member, constant or static function of a global class the reload comes first: in the one project that has reproduced a stale enum member, editor_rescan with reloadScript on the declaring script cleared it and a plain rescan did not. And an editor whose last scan wrote the class cache shorter than the file holds a list behind the files and writes it on every scan, so on that editor the rescan loads nothing and staleAnalysis says editor_launch restart instead. What follows differs: for a stale analysed type, editor_rescan with reloadScript recompiles the held copy and editor_launch restart is the last resort that costs a window; for a class missing from the cache, project_import refresh_classes rewrites the cache from disk at the cost of a short headless engine, which matters beside a running bench.",
12814
+ description: "Errors and warnings for a script, or for several, from the editor's language server, and whether each is clean. With no editor of this project open, so no language server of it to ask, a short headless engine compiles the scripts instead and the answer says checkedBy: engine: every error that stops a script compiling, with its line and no column, dependenciesWithErrors naming the other scripts that failed while it loaded, typesMissingFromTheClassCache naming a class the project declares that the engine could not resolve because the class cache lacks it, with staleAnalysis saying what rewrites the cache, and warnings null, since the engine reports warnings only to an editor. Each diagnostic carries its severity (error or warning), its message, and where it is as the editor shows it: line and column counted from 1, and endLine and endColumn just past its end. Diagnostics are checked against the project's own files, and the two that the files disprove are named rather than passed on: a member the class cache's file declares, or a project class it extends does, a method, property, constant, static function or enum member, comes back under contradictedByTheFile with the script declaring it, and so does one a diagnostic names alone as an external class member, read against the class it is looked up on in the line it is on; and a class this project declares that the diagnostic could not resolve comes back under typesTheEditorHasNotLoaded, with inTheClassCache saying whether a launched game would resolve it. staleAnalysis then says what to do, and for both states it is editor_rescan first: it asks the editor already running, starts nothing, and has cleared each in every reproduction measured. Two exceptions are named when they apply. For an enum member, constant or static function of a global class the reload comes first: in the one project that has reproduced a stale enum member, editor_rescan with reloadScript on the declaring script cleared it and a plain rescan did not. And an editor whose last scan wrote the class cache shorter than the file holds a list behind the files and writes it on every scan, so on that editor the rescan loads nothing and staleAnalysis says editor_launch restart instead. What follows differs: for a stale analysed type, editor_rescan with reloadScript recompiles the held copy and editor_launch restart is the last resort that costs a window; for a class missing from the cache, project_import refresh_classes rewrites the cache from disk at the cost of a short headless engine, which matters beside a running bench.",
12780
12815
  parameters: {
12781
12816
  projectPath: PROJECT_PATH,
12782
12817
  scriptPath: {
@@ -12790,7 +12825,7 @@ var init_tool_definitions = __esm(() => {
12790
12825
  {
12791
12826
  name: "resource_edit",
12792
12827
  title: "Edit resources",
12793
- description: 'Resource files: read one, create any ClassDB resource as .tres, change one, write a shader, build a TileSet, or set a Theme colour or font size. A material is a create with resourceType StandardMaterial3D, ShaderMaterial or CanvasItemMaterial. read answers with the properties the file sets, in the form modify takes, and a Theme lists its items there as "Button/colors/font_color". A create or modify whose properties made the engine change others, as a Gradient given more colours than offsets gives each new point an offset of 0, names each of those under alsoChanged with what it held before and holds now. move takes any file of the project, a script or an image as much as a resource, or a directory, and works from the files, so it needs no editor; one serving the project is brought up to it as after a move in its own FileSystem dock, its open scenes and cached resources named for the new paths and the project settings the move rewrote taken from the file. read needs no editor either: with none serving the project, a headless engine reads the file. The ops that write a resource go through the editor when one serves the project; with none, a headless engine runs the tool the editor would and the answer says so under writtenBy. That engine runs the script of the resource, 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 the save, and a resource where that cannot be done is refused and the file left as it was.',
12828
+ description: 'Resource files: read one, create any ClassDB resource as .tres, change one, write a shader, build a TileSet, or set a Theme colour or font size. A material is a create with resourceType StandardMaterial3D, ShaderMaterial or CanvasItemMaterial. read answers with the properties the file sets, in the form modify takes, with resourceType the engine class and scriptClass the class_name of its script when it declares one, and a Theme lists its items there as "Button/colors/font_color". A create or modify whose properties made the engine change others, as a Gradient given more colours than offsets gives each new point an offset of 0, names each of those under alsoChanged with what it held before and holds now. A property holding a resource takes its res:// path; a file never imported is refused with the reimport that imports it. move takes any file of the project, a script or an image as much as a resource, or a directory, and works from the files, so it needs no editor; one serving the project is brought up to it as after a move in its own FileSystem dock, its open scenes and cached resources named for the new paths and the project settings the move rewrote taken from the file. read needs no editor either: with none serving the project, a headless engine reads the file. The ops that write a resource go through the editor when one serves the project; with none, a headless engine runs the tool the editor would and the answer says so under writtenBy. That engine runs the script of the resource, 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 the save, and a resource where that cannot be done is refused and the file left as it was.',
12794
12829
  parameters: {
12795
12830
  projectPath: PROJECT_PATH,
12796
12831
  resourcePath: {
@@ -13716,7 +13751,7 @@ var init_tool_definitions = __esm(() => {
13716
13751
  {
13717
13752
  name: "debug_breakpoint",
13718
13753
  title: "Breakpoints",
13719
- description: "Sets or removes a breakpoint. Set them first, then editor_run, and the game stops where you asked. With the editor connected it goes through the editor's debug adapter; with none, it is held here. A game this server starts itself, which editor_run start does for a run given args, savesIn, env or silent, or with no editor, is handed every breakpoint held here as it starts, and one set while that game runs reaches it at once (runningGame says so). The editor keeps a breakpoint set this way for one play, so every breakpoint this server holds is sent again before each play editor_run starts, and the start answer lists them under breakpoints. The set is also kept in the project, for the server that comes after a reconnect: a breakpoint stays until it is removed here, whichever server set it. Every answer lists the whole set under held, and the breakpoints the user set in the editor's own gutter under setInEditor; setting one here never takes those away, and removing a line here clears it whoever set it. Godot clears every breakpoint in the editor when a debug session opens unless network/debug_adapter/sync_breakpoints is on, which the addon turns on as it loads; an editor where it was turned off again is said so under breakpointsAtRisk, here and in editor_status.",
13754
+ description: "Sets or removes a breakpoint. A set on a script that is not there, or past its last line, is refused, as nothing would ever stop there; a remove takes any. Set them first, then editor_run, and the game stops where you asked. With the editor connected it goes through the editor's debug adapter; with none, it is held here. A game this server starts itself, which editor_run start does for a run given args, savesIn, env or silent, or with no editor, is handed every breakpoint held here as it starts, and one set while that game runs reaches it at once (runningGame says so). The editor keeps a breakpoint set this way for one play, so every breakpoint this server holds is sent again before each play editor_run starts, and the start answer lists them under breakpoints. The set is also kept in the project, for the server that comes after a reconnect: a breakpoint stays until it is removed here, whichever server set it. Every answer lists the whole set under held, and the breakpoints the user set in the editor's own gutter under setInEditor; setting one here never takes those away, and removing a line here clears it whoever set it. Godot clears every breakpoint in the editor when a debug session opens unless network/debug_adapter/sync_breakpoints is on, which the addon turns on as it loads; an editor where it was turned off again is said so under breakpointsAtRisk, here and in editor_status.",
13720
13755
  parameters: {
13721
13756
  projectPath: PROJECT_PATH,
13722
13757
  scriptPath: SCRIPT_PATH,
@@ -39275,7 +39310,7 @@ async function resolveLSPPaths(projectPathValue, scriptPathValue) {
39275
39310
  try {
39276
39311
  scriptPath = await realpath(contained.absolutePath);
39277
39312
  } catch {
39278
- throw new ArgumentRefusal(`Script file does not exist: ${contained.absolutePath}`);
39313
+ throw new ArgumentRefusal(`Script file does not exist: res://${contained.relativePath}`);
39279
39314
  }
39280
39315
  if (!isWithinRoot(projectPath, scriptPath)) {
39281
39316
  throw new ArgumentRefusal("scriptPath resolves outside the project root boundary.");
@@ -39361,7 +39396,7 @@ var init_lsp_client = __esm(() => {
39361
39396
  AnotherProjectsServer = class AnotherProjectsServer extends Refusal {
39362
39397
  serves;
39363
39398
  constructor(port, serves, asked) {
39364
- super(`The language server on port ${port} belongs to the editor of ${serves}, not ${asked}.`);
39399
+ super(`The language server on port ${port} belongs to the editor of ${serves}; no editor of ${asked} answers there.`);
39365
39400
  this.serves = serves;
39366
39401
  }
39367
39402
  };
@@ -44089,6 +44124,46 @@ var init_lifecycle_log = __esm(() => {
44089
44124
  init_server_version();
44090
44125
  });
44091
44126
 
44127
+ // src/nearest.ts
44128
+ function similarity(one, other) {
44129
+ if (one === other) {
44130
+ return 1;
44131
+ }
44132
+ if (one.length < 2 || other.length < 2) {
44133
+ return 0;
44134
+ }
44135
+ const pairs = (text) => Array.from({ length: text.length - 1 }, (_, at) => text.slice(at, at + 2));
44136
+ const ones = pairs(one);
44137
+ const others = pairs(other);
44138
+ const shared = ones.filter((pair) => others.includes(pair)).length;
44139
+ return 2 * shared / (ones.length + others.length);
44140
+ }
44141
+ function withNearestFiles(said, wanted, files) {
44142
+ const extension = /\.([^./]+)$/.exec(wanted)?.[1];
44143
+ if (extension === undefined) {
44144
+ return said;
44145
+ }
44146
+ if (files.length === 0) {
44147
+ return `${said}. The project holds no .${extension} file.`;
44148
+ }
44149
+ const stem = (path) => (path.split("/").pop() ?? path).replace(/\.[^.]*$/, "");
44150
+ const close = nearest(stem(wanted), files.map((file) => stem(file)));
44151
+ const offered = files.filter((file) => close.includes(stem(file)));
44152
+ if (offered.length > 0) {
44153
+ return `${said}. The nearest .${extension} ${offered.length === 1 ? "file is" : "files are"} ${offered.join(", ")}.`;
44154
+ }
44155
+ if (files.length <= LISTED_FILES) {
44156
+ return `${said}. The project's .${extension} files are ${files.join(", ")}.`;
44157
+ }
44158
+ return `${said}. None of the project's ${files.length} .${extension} files is named like it.`;
44159
+ }
44160
+ function nearest(word, names) {
44161
+ const wanted = word.toLowerCase();
44162
+ const score = (name) => similarity(name.toLowerCase(), wanted);
44163
+ return names.filter((name) => score(name) >= 0.5).sort((one, other) => score(other) - score(one)).slice(0, 3);
44164
+ }
44165
+ var LISTED_FILES = 12;
44166
+
44092
44167
  // src/outside.ts
44093
44168
  import { fileURLToPath as fileURLToPath4 } from "node:url";
44094
44169
  function changesFrom(env, base) {
@@ -45022,6 +45097,70 @@ var init_server_instructions = __esm(() => {
45022
45097
  init_tool_definitions();
45023
45098
  });
45024
45099
 
45100
+ // src/test-selection.ts
45101
+ import { existsSync as existsSync26, readFileSync as readFileSync28 } from "node:fs";
45102
+ import { basename as basename10, join as join33 } from "node:path";
45103
+ function casesIn(projectPath, suite) {
45104
+ const text = readFileSync28(join33(projectPath, suite.slice("res://".length)), "utf8");
45105
+ return [...text.matchAll(/^func\s+(test_\w*)\s*\(/gm)].map((match) => match[1] ?? "");
45106
+ }
45107
+ function suitesUnder(projectPath, asked) {
45108
+ const scripts = projectFiles(projectPath, [".gd"]).map((path) => `res://${path}`);
45109
+ return [
45110
+ ...new Set(asked.flatMap((path) => path.endsWith(".gd") ? existsSync26(join33(projectPath, path.slice("res://".length))) ? [path] : [] : scripts.filter((script) => script.startsWith(`${path}/`))))
45111
+ ];
45112
+ }
45113
+ function selectTests(projectPath, asked, given) {
45114
+ if (given === undefined || given === null) {
45115
+ return { ok: true, value: null };
45116
+ }
45117
+ const entries = Array.isArray(given) ? given : [given];
45118
+ if (entries.length === 0) {
45119
+ return { ok: false, reason: "tests is an empty list: name a case, or leave tests out to run every one." };
45120
+ }
45121
+ const declared = suitesUnder(projectPath, asked).flatMap((suite) => casesIn(projectPath, suite).map((test) => ({ suite, test })));
45122
+ const nameOf = (suite) => basename10(suite, ".gd");
45123
+ const selected = [];
45124
+ for (const [index, entry] of entries.entries()) {
45125
+ if (typeof entry !== "string" || entry.trim() === "") {
45126
+ return {
45127
+ ok: false,
45128
+ reason: `tests[${index}] is not a case name: give "test_name" or "suite:test_name".`
45129
+ };
45130
+ }
45131
+ const spelled = entry.trim();
45132
+ const at = spelled.lastIndexOf(":");
45133
+ const suiteNamed = at === -1 || spelled.slice(at).startsWith("://") ? null : spelled.slice(0, at);
45134
+ const test = suiteNamed === null ? spelled : spelled.slice(at + 1);
45135
+ const suitePath = suiteNamed?.startsWith("res://") === true ? suiteNamed : null;
45136
+ const matches = declared.filter((one) => one.test === test && (suiteNamed === null || one.suite === suitePath || nameOf(one.suite) === suiteNamed));
45137
+ if (matches.length === 0) {
45138
+ const said = `No suite under ${asked.join(", ")} has a case ${spelled}`;
45139
+ if (declared.length === 0) {
45140
+ return { ok: false, reason: `${said}: none of them declares a func test_ at the top level.` };
45141
+ }
45142
+ const written = declared.map((one) => `${nameOf(one.suite)}:${one.test}`);
45143
+ const close = nearest(suiteNamed === null ? test : `${nameOf(suiteNamed)}:${test}`, [
45144
+ ...new Set(suiteNamed === null ? declared.map((one) => one.test) : written)
45145
+ ]);
45146
+ return {
45147
+ ok: false,
45148
+ reason: close.length > 0 ? `${said}. The nearest ${close.length === 1 ? "is" : "are"} ${close.join(", ")}.` : `${said}. The cases there are ${written.slice(0, 12).join(", ")}${written.length > 12 ? `, and ${written.length - 12} more` : ""}.`
45149
+ };
45150
+ }
45151
+ for (const one of matches) {
45152
+ selected.push({ entry: spelled, ...one });
45153
+ }
45154
+ }
45155
+ const suites = [...new Set(selected.map((one) => one.suite))];
45156
+ const chosen = new Set(selected.map((one) => `${one.suite}:${one.test}`));
45157
+ const ignore = declared.filter((one) => suites.includes(one.suite) && !chosen.has(`${one.suite}:${one.test}`)).map((one) => `${one.suite}:${one.test}`);
45158
+ return { ok: true, value: { suites, ignore, selected } };
45159
+ }
45160
+ var init_test_selection = __esm(() => {
45161
+ init_project_scan();
45162
+ });
45163
+
45025
45164
  // src/server.ts
45026
45165
  import { randomUUID as randomUUID2 } from "node:crypto";
45027
45166
  import {
@@ -45029,18 +45168,18 @@ import {
45029
45168
  closeSync as closeSync3,
45030
45169
  constants,
45031
45170
  copyFileSync,
45032
- existsSync as existsSync26,
45171
+ existsSync as existsSync27,
45033
45172
  fstatSync,
45034
45173
  mkdirSync as mkdirSync17,
45035
45174
  openSync as openSync4,
45036
45175
  readdirSync as readdirSync15,
45037
- readFileSync as readFileSync28,
45176
+ readFileSync as readFileSync29,
45038
45177
  readSync as readSync2,
45039
45178
  rmdirSync as rmdirSync2,
45040
45179
  statSync as statSync15,
45041
45180
  writeFileSync as writeFileSync18
45042
45181
  } from "node:fs";
45043
- import { basename as basename10, dirname as dirname23, extname as extname4, isAbsolute as isAbsolute5, join as join33, normalize as normalize2, relative as relative6 } from "node:path";
45182
+ import { basename as basename11, dirname as dirname23, extname as extname4, isAbsolute as isAbsolute5, join as join34, normalize as normalize2, relative as relative6 } from "node:path";
45044
45183
  import { setTimeout as delay5 } from "node:timers/promises";
45045
45184
  import { fileURLToPath as fileURLToPath5 } from "node:url";
45046
45185
  function editorRestartTimeoutMs() {
@@ -45077,15 +45216,15 @@ function writtenFileRefusal(path, rule) {
45077
45216
  return `${named} ${path} does not end in ${rule.extensions.join(" or ")}, which is what ${what} is.`;
45078
45217
  }
45079
45218
  const directory = realPathOr(dirname23(path));
45080
- if (!existsSync26(directory)) {
45219
+ if (!existsSync27(directory)) {
45081
45220
  return `${named}'s directory ${dirname23(path)} does not exist; ${what} creates no directories.`;
45082
45221
  }
45083
- const where = join33(directory, basename10(path));
45222
+ const where = join34(directory, basename11(path));
45084
45223
  const inside = rule.projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
45085
45224
  if (inside !== undefined) {
45086
45225
  return `${named} ${path} is inside the project at ${inside}; ${what} is saved outside it, so it never becomes part of the game.`;
45087
45226
  }
45088
- const taken = rule.writes(where).find((file) => existsSync26(file));
45227
+ const taken = rule.writes(where).find((file) => existsSync27(file));
45089
45228
  if (taken !== undefined) {
45090
45229
  return taken === where ? `${named} ${path} already exists; ${what} is not written over a file. Name a new one.` : `${named} ${path} would write ${taken}, which already exists; ${what} is not written over a file. Name a new one.`;
45091
45230
  }
@@ -45469,6 +45608,16 @@ function endedToStartThis(ended) {
45469
45608
  }
45470
45609
  return ` ${which} was ended to start this one; its output is no longer what editor_output answers about.`;
45471
45610
  }
45611
+ function lineBeyondScript(absolutePath, relativePath, line) {
45612
+ const text = readFileSync29(absolutePath, "utf8");
45613
+ const lines = text === "" ? 0 : text.split(`
45614
+ `).length - (text.endsWith(`
45615
+ `) ? 1 : 0);
45616
+ if (line <= lines) {
45617
+ return null;
45618
+ }
45619
+ return `res://${relativePath} has ${lines} line${lines === 1 ? "" : "s"}, so there is no line ${line} to stop on.`;
45620
+ }
45472
45621
  function runtimeVerdict(endpoint, after) {
45473
45622
  if (endpoint !== null) {
45474
45623
  return { listening: true, pid: endpoint.pid, port: endpoint.port };
@@ -45638,7 +45787,7 @@ function notRunNote(notRun, failFast) {
45638
45787
  function exportPresetRefusal(projectPath, preset) {
45639
45788
  let text;
45640
45789
  try {
45641
- text = readFileSync28(join33(projectPath, "export_presets.cfg"), "utf8");
45790
+ text = readFileSync29(join34(projectPath, "export_presets.cfg"), "utf8");
45642
45791
  } catch {
45643
45792
  return `This project has no export_presets.cfg, so it has no preset '${preset}' to export with. project_export add_preset writes one, as the editor's Project > Export does.`;
45644
45793
  }
@@ -45704,7 +45853,7 @@ function filesIn(folder) {
45704
45853
  try {
45705
45854
  for (const entry of readdirSync15(folder, { withFileTypes: true })) {
45706
45855
  if (entry.isFile()) {
45707
- found.set(entry.name, statSync15(join33(folder, entry.name)).mtimeMs);
45856
+ found.set(entry.name, statSync15(join34(folder, entry.name)).mtimeMs);
45708
45857
  }
45709
45858
  }
45710
45859
  } catch {}
@@ -45736,7 +45885,7 @@ function unsavedInEditorNote(scripts) {
45736
45885
  return `${named} ${one ? "is" : "are"} open in the editor's script ${one ? "tab" : "tabs"} with unsaved edits, and saving ${one ? "that tab" : "them"} there writes ${one ? "its" : "their"} text over what was written here: close ${one ? "it" : "them"} without saving, or save first and edit again.`;
45737
45886
  }
45738
45887
  function editorAddonMissing(projectPath) {
45739
- if (!existsSync26(join33(projectPath, "project.godot"))) {
45888
+ if (!existsSync27(join34(projectPath, "project.godot"))) {
45740
45889
  return null;
45741
45890
  }
45742
45891
  const report = inspectProject(projectPath);
@@ -46001,7 +46150,7 @@ function profileFrameFunctions(projectPath) {
46001
46150
  let projectGodot = null;
46002
46151
  if (projectPath !== null) {
46003
46152
  try {
46004
- projectGodot = readFileSync28(join33(projectPath, "project.godot"), "utf8");
46153
+ projectGodot = readFileSync29(join34(projectPath, "project.godot"), "utf8");
46005
46154
  } catch {}
46006
46155
  }
46007
46156
  return frameFunctionsFor(queuedMessagesOf(projectGodot));
@@ -46025,7 +46174,7 @@ function projectSourceOf(projectPath) {
46025
46174
  let source = null;
46026
46175
  if (inside.ok) {
46027
46176
  try {
46028
- source = readFileSync28(inside.absolutePath, "utf8");
46177
+ source = readFileSync29(inside.absolutePath, "utf8");
46029
46178
  } catch {}
46030
46179
  }
46031
46180
  read.set(script, source);
@@ -46180,7 +46329,7 @@ function foldedEntriesAnswer(log, floor = "warning") {
46180
46329
  }
46181
46330
  function gdUnitFailsOnScriptErrors(projectPath) {
46182
46331
  try {
46183
- return settingKeys(readFileSync28(join33(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
46332
+ return settingKeys(readFileSync29(join34(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
46184
46333
  } catch {
46185
46334
  return true;
46186
46335
  }
@@ -46195,7 +46344,7 @@ function uncaughtNote(uncaught) {
46195
46344
  }
46196
46345
  function elsewhereIn(projectPath, asked) {
46197
46346
  const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
46198
- if (wanted === "" || existsSync26(join33(projectPath, wanted))) {
46347
+ if (wanted === "" || existsSync27(join34(projectPath, wanted))) {
46199
46348
  return "";
46200
46349
  }
46201
46350
  const root = (wanted.split("/")[0] ?? "").toLowerCase();
@@ -46298,7 +46447,7 @@ function isDirectory(path) {
46298
46447
  }
46299
46448
  }
46300
46449
  function scriptsUnder(directory, prefix) {
46301
- if (existsSync26(join33(directory, ".gdignore"))) {
46450
+ if (existsSync27(join34(directory, ".gdignore"))) {
46302
46451
  return [];
46303
46452
  }
46304
46453
  const found = [];
@@ -46308,7 +46457,7 @@ function scriptsUnder(directory, prefix) {
46308
46457
  }
46309
46458
  const path = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
46310
46459
  if (entry.isDirectory()) {
46311
- found.push(...scriptsUnder(join33(directory, entry.name), path));
46460
+ found.push(...scriptsUnder(join34(directory, entry.name), path));
46312
46461
  } else if (entry.isFile() && entry.name.endsWith(".gd")) {
46313
46462
  found.push(path);
46314
46463
  }
@@ -46316,11 +46465,11 @@ function scriptsUnder(directory, prefix) {
46316
46465
  return found;
46317
46466
  }
46318
46467
  function mainSceneOf(projectFile) {
46319
- const scene = parseProjectGodot(readFileSync28(projectFile, "utf8"))["application"]?.["run/main_scene"];
46468
+ const scene = parseProjectGodot(readFileSync29(projectFile, "utf8"))["application"]?.["run/main_scene"];
46320
46469
  return typeof scene === "string" && scene !== "" ? scene : null;
46321
46470
  }
46322
46471
  function editorPlaysHeadless(projectFile) {
46323
- const runArgs = parseProjectGodot(readFileSync28(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
46472
+ const runArgs = parseProjectGodot(readFileSync29(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
46324
46473
  return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
46325
46474
  }
46326
46475
  function ownArguments(params) {
@@ -46338,7 +46487,7 @@ function needsItsOwnProcess(asked) {
46338
46487
  class GodotServer {
46339
46488
  stdio = null;
46340
46489
  locator = new GodotLocator;
46341
- operationsScript = join33(__dirname2, "godot", "operations", "godot_operations.gd");
46490
+ operationsScript = join34(__dirname2, "godot", "operations", "godot_operations.gd");
46342
46491
  godotBridge;
46343
46492
  tools = buildToolDefinitions();
46344
46493
  activeProcess = null;
@@ -46600,14 +46749,14 @@ class GodotServer {
46600
46749
  }
46601
46750
  noteTheStart() {
46602
46751
  const cwd = process.cwd();
46603
- const mcpJson = join33(cwd, ".mcp.json");
46752
+ const mcpJson = join34(cwd, ".mcp.json");
46604
46753
  const announced = this.ownProject === null ? null : readAnnouncement(announcementPath(this.ownProject));
46605
46754
  const predecessor = announced !== null && announced.pid !== process.pid && alive(announced.pid) ? announced.pid : null;
46606
46755
  writeLifecycle("started", {
46607
46756
  argv: process.argv.slice(1),
46608
46757
  cwd,
46609
46758
  project: this.ownProject,
46610
- mcpJson: existsSync26(mcpJson) ? mcpJson : null
46759
+ mcpJson: existsSync27(mcpJson) ? mcpJson : null
46611
46760
  });
46612
46761
  if (predecessor !== null) {
46613
46762
  writeLifecycle("replaced", { project: this.ownProject, predecessor });
@@ -46748,7 +46897,7 @@ class GodotServer {
46748
46897
  return checked.response;
46749
46898
  }
46750
46899
  const named = args["projectPath"];
46751
- if (typeof named === "string" && existsSync26(join33(named, "project.godot"))) {
46900
+ if (typeof named === "string" && existsSync27(join34(named, "project.godot"))) {
46752
46901
  this.lastProjectPath = named;
46753
46902
  }
46754
46903
  this.updates.refresh();
@@ -46784,7 +46933,7 @@ class GodotServer {
46784
46933
  let notSaved;
46785
46934
  try {
46786
46935
  savedAt = answer.content.map((item, index) => {
46787
- const file = join33(directory, item.type === "text" ? `${stem}-${index}.txt` : `${stem}-${index}.${item.mimeType.split("/")[1] ?? "bin"}`);
46936
+ const file = join34(directory, item.type === "text" ? `${stem}-${index}.txt` : `${stem}-${index}.${item.mimeType.split("/")[1] ?? "bin"}`);
46788
46937
  writeFileSync18(file, item.type === "text" ? item.text : Buffer.from(item.data, "base64"));
46789
46938
  return file;
46790
46939
  });
@@ -47011,7 +47160,10 @@ class GodotServer {
47011
47160
  });
47012
47161
  if (missing.length > 0) {
47013
47162
  const where = op !== null ? `${spec.name} ${op}` : spec.name;
47014
- return { ok: false, response: this.createErrorResponse(`${where} needs ${missing.join(", ")}.`) };
47163
+ const blank = missing.filter((field) => typeof args[field] === "string");
47164
+ const named = missing.map((field) => blank.includes(field) ? `${field}, which was given empty` : field);
47165
+ const joined = named.join(blank.length > 0 ? "; " : ", ");
47166
+ return { ok: false, response: this.createErrorResponse(`${where} needs ${joined}.`) };
47015
47167
  }
47016
47168
  return { ok: true, op };
47017
47169
  }
@@ -47319,6 +47471,15 @@ class GodotServer {
47319
47471
  if (line === undefined) {
47320
47472
  return this.createErrorResponse("line is required: the one-based line to stop on.");
47321
47473
  }
47474
+ if (op === "set") {
47475
+ if (!existsSync27(located.absolutePath)) {
47476
+ return this.missingScript(project.value.path, located.relativePath);
47477
+ }
47478
+ const beyond = lineBeyondScript(located.absolutePath, located.relativePath, line);
47479
+ if (beyond !== null) {
47480
+ return this.createErrorResponse(beyond);
47481
+ }
47482
+ }
47322
47483
  const editor = this.godotBridge.isConnected();
47323
47484
  let answer;
47324
47485
  if (editor) {
@@ -47401,8 +47562,8 @@ class GodotServer {
47401
47562
  if (path === undefined) {
47402
47563
  return { ok: false, response: this.createErrorResponse("projectPath is required.") };
47403
47564
  }
47404
- const file = join33(path, "project.godot");
47405
- if (!existsSync26(file)) {
47565
+ const file = join34(path, "project.godot");
47566
+ if (!existsSync27(file)) {
47406
47567
  return {
47407
47568
  ok: false,
47408
47569
  response: this.createErrorResponse(`Not a Godot project: ${path}`, [
@@ -48102,27 +48263,51 @@ class GodotServer {
48102
48263
  if (!engine.ok) {
48103
48264
  return engine.response;
48104
48265
  }
48105
- const application = parseProjectGodot(readFileSync28(project.value.file, "utf8"))["application"] ?? {};
48266
+ const application = parseProjectGodot(readFileSync29(project.value.file, "utf8"))["application"] ?? {};
48106
48267
  const name = application["config/name"];
48107
48268
  const mainScene = application["run/main_scene"];
48108
48269
  const info = {
48109
- name: typeof name === "string" && name !== "" ? name : basename10(project.value.path),
48270
+ name: typeof name === "string" && name !== "" ? name : basename11(project.value.path),
48110
48271
  path: project.value.path,
48111
48272
  mainScene: typeof mainScene === "string" && mainScene !== "" ? mainScene : null,
48112
48273
  godotVersion: await this.godotVersion(engine.value),
48113
48274
  structure: projectStructure(project.value.path)
48114
48275
  };
48115
48276
  const detailed = args["detail"] === "full";
48277
+ const compiledScripts = include.includes("health") || include.includes("validation") ? await this.compiledForProjectInfo(project.value.path) : undefined;
48116
48278
  for (const section of include) {
48117
48279
  const spec = PROJECT_INFO_SECTIONS2[section];
48118
48280
  if (spec === undefined) {
48119
48281
  continue;
48120
48282
  }
48121
- const outcome = await this.operation(spec.operation, spec.params(args, detailed), project.value.path);
48283
+ const params = {
48284
+ ...spec.params(args, detailed),
48285
+ ...compiledScripts !== undefined && (section === "health" || section === "validation") ? { compiled: compiledScripts } : {}
48286
+ };
48287
+ const outcome = await this.operation(spec.operation, params, project.value.path);
48122
48288
  info[section] = outcome.ok ? { ...outcome.payload, ...engineExtras(outcome) } : { error: outcome.message, ...engineExtras(outcome) };
48123
48289
  }
48124
48290
  return this.jsonTextResponse(info);
48125
48291
  }
48292
+ async compiledForProjectInfo(projectPath) {
48293
+ const scripts = projectFiles(projectPath, [".gd"]).map((script) => `res://${script}`);
48294
+ if (scripts.length === 0) {
48295
+ return { checked: 0, failures: [] };
48296
+ }
48297
+ const checked = await this.compileScripts(projectPath, scripts);
48298
+ if (!checked.ok) {
48299
+ return { not_checked: `no headless engine could compile the scripts: ${checked.message}` };
48300
+ }
48301
+ const failures = checked.payload.scripts.map(asParams);
48302
+ const uncached = uncachedFor(projectPath, failures.map((failure) => (readArray(failure, "errors") ?? []).map((entry) => String(asParams(entry)["message"]))));
48303
+ return {
48304
+ checked: checked.payload.checked.length,
48305
+ failures: failures.map((failure, at) => {
48306
+ const types = uncached[at] ?? [];
48307
+ return types.length === 0 ? failure : { ...failure, note: uncachedForTheEngineNote(projectPath, types) };
48308
+ })
48309
+ };
48310
+ }
48126
48311
  fileTypes(args) {
48127
48312
  const named = readStringArray(args, "fileTypes");
48128
48313
  return named === undefined ? {} : { fileTypes: named };
@@ -48200,7 +48385,7 @@ class GodotServer {
48200
48385
  const exportArgs = [
48201
48386
  "--headless",
48202
48387
  "--log-file",
48203
- join33(exportLogs, "engine.log"),
48388
+ join34(exportLogs, "engine.log"),
48204
48389
  "--path",
48205
48390
  project.value.path,
48206
48391
  debug ? "--export-debug" : "--export-release",
@@ -48208,7 +48393,7 @@ class GodotServer {
48208
48393
  output.absolutePath
48209
48394
  ];
48210
48395
  this.logDebug(`Exporting: ${engine.value} ${exportArgs.join(" ")}`);
48211
- const writtenAt = () => existsSync26(output.absolutePath) ? statSync15(output.absolutePath).mtimeMs : null;
48396
+ const writtenAt = () => existsSync27(output.absolutePath) ? statSync15(output.absolutePath).mtimeMs : null;
48212
48397
  const caches = editorCacheDirectory(engine.value);
48213
48398
  const exportOnce = async () => {
48214
48399
  const before = writtenAt();
@@ -48237,7 +48422,7 @@ class GodotServer {
48237
48422
  if (ending.exitSignal === null && ending.exitCode === null) {
48238
48423
  return this.createErrorResponse(`Export could not be run: ${ending.failure ?? "the engine gave no exit status"}`);
48239
48424
  }
48240
- const beside = writtenBeside(listedBefore, filesIn(folder), basename10(output.absolutePath), dirname23(output.relativePath).replaceAll("\\", "/"));
48425
+ const beside = writtenBeside(listedBefore, filesIn(folder), basename11(output.absolutePath), dirname23(output.relativePath).replaceAll("\\", "/"));
48241
48426
  return exportAnswer(ending, { preset, outputPath: output.relativePath, debug }, before, writtenAt(), beside, docCacheNote);
48242
48427
  }
48243
48428
  reportTestProgress(log) {
@@ -48291,10 +48476,10 @@ class GodotServer {
48291
48476
  if (movie !== null) {
48292
48477
  return this.createErrorResponse(movie);
48293
48478
  }
48294
- const missing = paths.value.filter((path) => !existsSync26(join33(project.value.path, path.slice("res://".length))));
48479
+ const missing = paths.value.filter((path) => !existsSync27(join34(project.value.path, path.slice("res://".length))));
48295
48480
  const asked = paths.value.filter((path) => !missing.includes(path));
48296
48481
  const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
48297
- const noRunner = !existsSync26(join33(project.value.path, runner));
48482
+ const noRunner = !existsSync27(join34(project.value.path, runner));
48298
48483
  if (asked.length === 0) {
48299
48484
  const verdict = `nothing at ${missing.join(" and ")}`;
48300
48485
  const andNoRunner = noRunner ? `, and gdUnit4 is not installed in this project: no ${runner}` : "";
@@ -48322,6 +48507,10 @@ class GodotServer {
48322
48507
  "Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
48323
48508
  ]);
48324
48509
  }
48510
+ const selection = selectTests(project.value.path, asked, args["tests"]);
48511
+ if (!selection.ok) {
48512
+ return this.createErrorResponse(selection.reason);
48513
+ }
48325
48514
  const engine = await this.engine();
48326
48515
  if (!engine.ok) {
48327
48516
  return engine.response;
@@ -48352,8 +48541,8 @@ class GodotServer {
48352
48541
  `res://${runner}`,
48353
48542
  "--ignoreHeadlessMode",
48354
48543
  ...readBoolean(args, "failFast") === true ? [] : ["-c"],
48355
- ...asked.flatMap((path) => ["-a", path]),
48356
- ...ignored.flatMap((entry) => ["-i", entry]),
48544
+ ...(selection.value?.suites ?? asked).flatMap((path) => ["-a", path]),
48545
+ ...[...ignored, ...selection.value?.ignore ?? []].flatMap((entry) => ["-i", entry]),
48357
48546
  "-rd",
48358
48547
  reports,
48359
48548
  "-rc",
@@ -48410,14 +48599,14 @@ class GodotServer {
48410
48599
  await run.settled;
48411
48600
  const tookMs = Date.now() - began;
48412
48601
  const notCompiled = stopped.for ?? run.log.all.map((entry) => autoloadNotCompiled(entry, autoloaded)).find((found) => found !== null) ?? null;
48413
- const reportsDir = join33(project.value.path, ".godot", ...ours.split("/"));
48602
+ const reportsDir = join34(project.value.path, ".godot", ...ours.split("/"));
48414
48603
  let report = null;
48415
48604
  let reportProblem = null;
48416
48605
  try {
48417
- const written = existsSync26(reportsDir) && notCompiled === null ? readdirSync15(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
48606
+ const written = existsSync27(reportsDir) && notCompiled === null ? readdirSync15(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
48418
48607
  const newest = written.at(-1);
48419
48608
  if (newest !== undefined) {
48420
- report = parseJUnit(readFileSync28(join33(reportsDir, newest, "results.xml"), "utf8"));
48609
+ report = parseJUnit(readFileSync29(join34(reportsDir, newest, "results.xml"), "utf8"));
48421
48610
  }
48422
48611
  } catch (error) {
48423
48612
  reportProblem = errorMessage(error);
@@ -48544,10 +48733,14 @@ class GodotServer {
48544
48733
  const uncaughtFolded = foldedRepeats(uncaught.map(aboveTheRunner));
48545
48734
  const listedAsUncaught = new Set(uncaughtFolded.map((entry) => entry.index));
48546
48735
  const notApplied = ignoredYetRan(report, ignored);
48736
+ const testsNotRun = [
48737
+ ...new Set((selection.value?.selected ?? []).filter((one) => !report.suites.some((suite) => (suite.path === one.suite || suite.name === basename11(one.suite, ".gd")) && suite.cases.some((ran) => ran.name === one.test || new RegExp(`^${one.test}\\W`).test(ran.name)))).map((one) => `${basename11(one.suite, ".gd")}:${one.test}`))
48738
+ ];
48547
48739
  return this.jsonTextResponse({
48548
- passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0 && uncaught.length === 0 && missing.length === 0,
48549
- verdict,
48740
+ passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0 && uncaught.length === 0 && missing.length === 0 && testsNotRun.length === 0,
48741
+ verdict: testsNotRun.length === 0 ? verdict : `${verdict}; not run: ${testsNotRun.join(", ")}`,
48550
48742
  ...missing.length > 0 ? { missing } : {},
48743
+ ...testsNotRun.length > 0 ? { testsNotRun } : {},
48551
48744
  ...timedOut ? { timedOut, hung, silentForMs } : {},
48552
48745
  exitCode,
48553
48746
  exitSignal: run.exitSignal ?? undefined,
@@ -48570,6 +48763,7 @@ class GodotServer {
48570
48763
  ...scanned,
48571
48764
  note: [
48572
48765
  missingNote,
48766
+ testsNotRun.length > 0 ? `${testsNotRun.join(", ")} ${testsNotRun.length === 1 ? "was" : "were"} asked for and not run, so the run is not called passed: gdUnit4 runs a test_ function only in a script that extends GdUnitTestSuite, and leaves out a suite whose script does not load.` : undefined,
48573
48767
  briefed > 0 ? `${briefed} of the ${failed.length} failing cases, those with the largest reports, are named with their first message only, since their reports would have made the answer too large to deliver; pass one case's suite path to see its reports.` : undefined,
48574
48768
  notApplied.length > 0 ? "An ignore entry named a case that ran all the same: gdUnit4 leaves a case out only as suite_name:test_name, and reads an entry with no colon as a suite name or a piece of a suite path. ignoreNotApplied gives the form for each case that ran." : undefined,
48575
48769
  uncaughtNote(uncaught),
@@ -48612,6 +48806,16 @@ class GodotServer {
48612
48806
  return this.createErrorResponse(scripts.reason);
48613
48807
  }
48614
48808
  if (scripts?.value == null) {
48809
+ const asked = readString(args, "scriptPath");
48810
+ if (project.ok && asked !== undefined) {
48811
+ const location = resolveWithinProject(project.value.path, asked);
48812
+ if (!location.ok) {
48813
+ return this.createErrorResponse(location.reason, PATH_SOLUTIONS);
48814
+ }
48815
+ if (!existsSync27(location.absolutePath)) {
48816
+ return this.missingScript(project.value.path, location.relativePath);
48817
+ }
48818
+ }
48615
48819
  const one = await this.diagnosticsOf(args);
48616
48820
  if ("noLanguageServer" in one) {
48617
48821
  const checked = project.ok ? await this.engineDiagnostics(project.value.path, [readString(args, "scriptPath") ?? ""], one.noLanguageServer) : { refusal: project.response };
@@ -48678,6 +48882,11 @@ class GodotServer {
48678
48882
  ...byEngine === null ? {} : engineChecked(byEngine)
48679
48883
  });
48680
48884
  }
48885
+ missingScript(projectPath, relativePath) {
48886
+ const scripts = projectFiles(projectPath, [".gd"]).map((script) => `res://${script}`);
48887
+ const wanted = `res://${relativePath}`;
48888
+ return this.createErrorResponse(withNearestFiles(`Script file does not exist: ${wanted}`, wanted, scripts));
48889
+ }
48681
48890
  async compileScripts(projectPath, paths) {
48682
48891
  const payload = { checked: [], failed: [], scripts: [] };
48683
48892
  const messages = [];
@@ -48714,21 +48923,28 @@ class GodotServer {
48714
48923
  String(asParams(entry)["path"]),
48715
48924
  asParams(entry)
48716
48925
  ]));
48926
+ const errorsOf = scripts.map((_, at) => readArray(failures.get(named[at] ?? "") ?? {}, "errors") ?? []);
48927
+ const uncachedPer = uncachedFor(projectPath, errorsOf.map((errors) => errors.map((entry) => String(asParams(entry)["message"]))));
48717
48928
  return {
48718
48929
  answers: scripts.map((scriptPath, at) => {
48719
48930
  const failure = failures.get(named[at] ?? "");
48720
- const diagnostics = (readArray(failure ?? {}, "errors") ?? []).map((entry) => {
48931
+ const diagnostics = (errorsOf[at] ?? []).map((entry) => {
48721
48932
  const { line, message } = asParams(entry);
48722
48933
  return { severity: "error", message, ...typeof line === "number" && line > 0 ? { line } : {} };
48723
48934
  });
48724
48935
  const dependencies = readArray(failure ?? {}, "dependencies_with_errors");
48936
+ const uncached = uncachedPer[at] ?? [];
48725
48937
  return {
48726
48938
  scriptPath,
48727
48939
  clean: diagnostics.length === 0,
48728
48940
  errors: diagnostics.length,
48729
48941
  warnings: null,
48730
48942
  diagnostics,
48731
- ...dependencies === undefined ? {} : { dependenciesWithErrors: dependencies }
48943
+ ...dependencies === undefined ? {} : { dependenciesWithErrors: dependencies },
48944
+ ...uncached.length === 0 ? {} : {
48945
+ typesMissingFromTheClassCache: uncached.map(({ type, declaredIn }) => ({ type, declaredIn })),
48946
+ staleAnalysis: uncachedForTheEngineNote(projectPath, uncached)
48947
+ }
48732
48948
  };
48733
48949
  })
48734
48950
  };
@@ -48778,11 +48994,11 @@ class GodotServer {
48778
48994
  const cached = cachedClasses(projectPath);
48779
48995
  const sourceOf = (resourcePath) => {
48780
48996
  const contained = resolveWithinProject(projectPath, resourcePath);
48781
- if (!contained.ok || !existsSync26(contained.absolutePath)) {
48997
+ if (!contained.ok || !existsSync27(contained.absolutePath)) {
48782
48998
  return null;
48783
48999
  }
48784
49000
  try {
48785
- return readFileSync28(contained.absolutePath, "utf8");
49001
+ return readFileSync29(contained.absolutePath, "utf8");
48786
49002
  } catch {
48787
49003
  return null;
48788
49004
  }
@@ -48872,11 +49088,11 @@ class GodotServer {
48872
49088
  }
48873
49089
  sourceOfClass(projectPath, resourcePath) {
48874
49090
  const contained = resolveWithinProject(projectPath, resourcePath);
48875
- if (!contained.ok || !existsSync26(contained.absolutePath)) {
49091
+ if (!contained.ok || !existsSync27(contained.absolutePath)) {
48876
49092
  return null;
48877
49093
  }
48878
49094
  try {
48879
- return readFileSync28(contained.absolutePath, "utf8");
49095
+ return readFileSync29(contained.absolutePath, "utf8");
48880
49096
  } catch {
48881
49097
  return null;
48882
49098
  }
@@ -49033,7 +49249,7 @@ class GodotServer {
49033
49249
  ...Array.isArray(stack) ? {
49034
49250
  stack: stack.map((frame) => ({
49035
49251
  ...frame,
49036
- ...projectPath !== "" && typeof frame["script"] === "string" && frame["script"].startsWith("res://") ? { path: join33(projectPath, frame["script"].slice("res://".length)) } : {}
49252
+ ...projectPath !== "" && typeof frame["script"] === "string" && frame["script"].startsWith("res://") ? { path: join34(projectPath, frame["script"].slice("res://".length)) } : {}
49037
49253
  }))
49038
49254
  } : {}
49039
49255
  });
@@ -49312,7 +49528,7 @@ class GodotServer {
49312
49528
  return null;
49313
49529
  }
49314
49530
  try {
49315
- return settingKeys(readFileSync28(join33(projectPath, "project.godot"), "utf8"));
49531
+ return settingKeys(readFileSync29(join34(projectPath, "project.godot"), "utf8"));
49316
49532
  } catch {
49317
49533
  return null;
49318
49534
  }
@@ -49757,7 +49973,7 @@ class GodotServer {
49757
49973
  }
49758
49974
  async runtimeUp(projectPath, before, budget, withArgs) {
49759
49975
  const budgetMs = budget.ms;
49760
- if (!existsSync26(join33(projectPath, RUNTIME_AUTOLOAD.path))) {
49976
+ if (!existsSync27(join34(projectPath, RUNTIME_AUTOLOAD.path))) {
49761
49977
  return runtimeVerdict(null, {
49762
49978
  addon: false,
49763
49979
  budgetMs,
@@ -50039,7 +50255,7 @@ class GodotServer {
50039
50255
  return 0;
50040
50256
  }
50041
50257
  try {
50042
- return reportAlreadyInTranscript(readFileSync28(game.transcript), readFileSync28(report));
50258
+ return reportAlreadyInTranscript(readFileSync29(game.transcript), readFileSync29(report));
50043
50259
  } catch (error) {
50044
50260
  this.logDebug(`Could not compare a picked-up run's report with its transcript: ${errorMessage(error)}`);
50045
50261
  return 0;
@@ -50734,7 +50950,7 @@ class GodotServer {
50734
50950
  };
50735
50951
  const going = await this.runStillGoing(run);
50736
50952
  const projectPath = run.projectPath;
50737
- const projectGone = projectPath !== null && !existsSync26(join33(projectPath, "project.godot"));
50953
+ const projectGone = projectPath !== null && !existsSync27(join34(projectPath, "project.godot"));
50738
50954
  if (projectGone && callersName !== undefined) {
50739
50955
  return this.createErrorResponse(`callersOf reads the project's scripts, and the run's project at ${projectPath} has no project.godot any more.`, ["editor_run profile without callersOf answers with the profile itself"]);
50740
50956
  }
@@ -51081,7 +51297,7 @@ class GodotServer {
51081
51297
  notes.push(endedWithoutACode({
51082
51298
  throughEditor: run.throughEditor,
51083
51299
  announced: announced !== undefined || run.announcedPid !== undefined,
51084
- couldAnnounce: run.projectPath !== null && existsSync26(join33(run.projectPath, RUNTIME_AUTOLOAD.path)),
51300
+ couldAnnounce: run.projectPath !== null && existsSync27(join34(run.projectPath, RUNTIME_AUTOLOAD.path)),
51085
51301
  endedHere: typeof run.endedHere === "string",
51086
51302
  pickedUp: run.pickedUp === true
51087
51303
  }));
@@ -51710,8 +51926,8 @@ class GodotServer {
51710
51926
  if (!scriptPath.ok) {
51711
51927
  return this.createErrorResponse(scriptPath.reason);
51712
51928
  }
51713
- if (!existsSync26(scriptPath.absolutePath)) {
51714
- return this.createErrorResponse(`Script file does not exist: ${scriptPath.absolutePath}`);
51929
+ if (!existsSync27(scriptPath.absolutePath)) {
51930
+ return this.missingScript(projectPath, scriptPath.relativePath);
51715
51931
  }
51716
51932
  const asked = readNonEmptyString(args, "newScriptPath");
51717
51933
  const moveTo = asked === undefined ? null : resolveWithinProject(projectPath, asked);
@@ -52124,7 +52340,7 @@ class GodotServer {
52124
52340
  }
52125
52341
  const escaped = symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
52126
52342
  const declared = new RegExp(`^\\s*(?:@\\w+(?:\\([^)]*\\))?\\s+)*class_name\\s+${escaped}\\b`, "m");
52127
- if (declared.test(readFileSync28(scriptPath, "utf8"))) {
52343
+ if (declared.test(readFileSync29(scriptPath, "utf8"))) {
52128
52344
  return null;
52129
52345
  }
52130
52346
  const checked = await this.classesTheEditorCannotSee(args);
@@ -52290,7 +52506,7 @@ class GodotServer {
52290
52506
  async ownRunOnItsWay(projectPath, budgetMs) {
52291
52507
  const run = await this.currentRun();
52292
52508
  const project = projectPath ?? run?.projectPath ?? null;
52293
- if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync26(join33(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
52509
+ if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync27(join34(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
52294
52510
  return null;
52295
52511
  }
52296
52512
  const already = runtimesAnnounced().running;
@@ -52384,7 +52600,7 @@ class GodotServer {
52384
52600
  return this.createErrorResponse(staleNote);
52385
52601
  }
52386
52602
  const screenshotDir = expectsScreenshot ? scratchDirectory("runtime-screenshot") : null;
52387
- const screenshotPath = screenshotDir ? join33(screenshotDir, "capture.png") : null;
52603
+ const screenshotPath = screenshotDir ? join34(screenshotDir, "capture.png") : null;
52388
52604
  const refused = (message) => this.createErrorResponse(message);
52389
52605
  const onGame = {
52390
52606
  pid: choice.endpoint.pid,
@@ -52445,7 +52661,7 @@ class GodotServer {
52445
52661
 
52446
52662
  Ran alongside ${ranWith.join(", ")}. ${ALONGSIDE_NOTE}`}`
52447
52663
  },
52448
- { type: "image", data: readFileSync28(screenshotPath).toString("base64"), mimeType: "image/png" }
52664
+ { type: "image", data: readFileSync29(screenshotPath).toString("base64"), mimeType: "image/png" }
52449
52665
  ]
52450
52666
  };
52451
52667
  } finally {
@@ -52620,6 +52836,7 @@ var init_server = __esm(() => {
52620
52836
  init_server_version();
52621
52837
  init_setup();
52622
52838
  init_spawn();
52839
+ init_test_selection();
52623
52840
  init_tool_definitions();
52624
52841
  init_tool_reference();
52625
52842
  init_update_check();
@@ -52668,6 +52885,7 @@ var init_server = __esm(() => {
52668
52885
  });
52669
52886
  PROJECT_INFO_SECTIONS2 = dictionary({
52670
52887
  autoloads: { operation: "list_autoloads", params: () => ({}) },
52888
+ input_actions: { operation: "list_input_actions", params: () => ({}) },
52671
52889
  plugins: { operation: "list_plugins", params: () => ({}) },
52672
52890
  export_presets: { operation: "list_export_presets", params: () => ({}) },
52673
52891
  audio_buses: { operation: "get_audio_buses", params: () => ({}) },
@@ -52684,8 +52902,8 @@ var init_server = __esm(() => {
52684
52902
  init_bridge_announce();
52685
52903
  init_errors();
52686
52904
  init_godot_path();
52687
- import { existsSync as existsSync27 } from "node:fs";
52688
- import { join as join34, resolve as resolve8 } from "node:path";
52905
+ import { existsSync as existsSync28 } from "node:fs";
52906
+ import { join as join35, resolve as resolve8 } from "node:path";
52689
52907
 
52690
52908
  // src/harnesses.ts
52691
52909
  import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, statSync as statSync3, writeFileSync as writeFileSync2 } from "node:fs";
@@ -53829,6 +54047,13 @@ declaration and the cache can both be right while the editor stays blind to it.
53829
54047
  its own, which is the measured cure and needs no change to the declaring script; \`editor_launch
53830
54048
  restart\` also does it and costs a restart of the editor.
53831
54049
 
54050
+ With no editor open, the headless engine that compiles for \`script_diagnostics\` and for
54051
+ \`project_info\`'s health and validation resolves class names only from that cache file. A
54052
+ project never opened, as a fresh clone is, has no such file, so every script typed by a project
54053
+ class fails there. \`script_diagnostics\` names the class under
54054
+ \`typesMissingFromTheClassCache\`, health and validation say so beside the failure, and
54055
+ \`project_import refresh_classes\` writes the cache from the declarations on disk.
54056
+
53832
54057
  The scan writes \`.godot/global_script_class_cache.cfg\` from the list the editor is holding, so
53833
54058
  a class it cannot resolve would go out of that file with it. The answer names that loss under
53834
54059
  \`cacheLost\` and rebuilds the cache from the declarations on disk, naming what came back under
@@ -53969,7 +54194,7 @@ class UsageError extends Refusal {
53969
54194
  function projectArgument(at) {
53970
54195
  const named = args.slice(at).find((value) => !value.startsWith("--"));
53971
54196
  const projectPath = resolve8(named ?? ".");
53972
- if (!existsSync27(join34(projectPath, "project.godot"))) {
54197
+ if (!existsSync28(join35(projectPath, "project.godot"))) {
53973
54198
  throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
53974
54199
  }
53975
54200
  return projectPath;
@@ -54014,7 +54239,7 @@ async function registerRuntime(godot, projectPath) {
54014
54239
  return;
54015
54240
  }
54016
54241
  console.log(`${RUNTIME_AUTOLOAD.name} autoload left at ${named}, which this project registers itself`);
54017
- if (!existsSync27(join34(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
54242
+ if (!existsSync28(join35(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
54018
54243
  console.log(` that file is not in this project, so it boots with a missing script: restore it, or point the entry at res://${RUNTIME_AUTOLOAD.path}`);
54019
54244
  }
54020
54245
  }
@@ -54146,7 +54371,7 @@ async function setup() {
54146
54371
  console.log("no harness configured; name one yourself, and gdharness harnesses lists them all");
54147
54372
  }
54148
54373
  if (!args.includes("--no-skill")) {
54149
- const directories = skillDirectories(chosen.harnesses, projectPath, existsSync27);
54374
+ const directories = skillDirectories(chosen.harnesses, projectPath, existsSync28);
54150
54375
  for (const written of writeSkill(directories, getLocalVersion())) {
54151
54376
  console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
54152
54377
  }
@@ -54260,7 +54485,7 @@ async function upgrade() {
54260
54485
  moved.push(took.from);
54261
54486
  }
54262
54487
  }
54263
- for (const written of writeSkill(skillDirectories(already, projectPath, existsSync27), version)) {
54488
+ for (const written of writeSkill(skillDirectories(already, projectPath, existsSync28), version)) {
54264
54489
  console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
54265
54490
  }
54266
54491
  console.log(`