gdharness 0.13.12 → 0.13.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/cli.js CHANGED
@@ -7557,6 +7557,41 @@ function cachedClasses(projectPath) {
7557
7557
  }
7558
7558
  return listed;
7559
7559
  }
7560
+ function missingMemberIn(message) {
7561
+ const found = /The (method|property) "([^"(]+)(?:\(\))?" is not present on the inferred type "([^"]+)"/.exec(message);
7562
+ if (found === null) {
7563
+ return null;
7564
+ }
7565
+ return {
7566
+ kind: found[1] === "property" ? "property" : "method",
7567
+ member: found[2] ?? "",
7568
+ type: found[3] ?? ""
7569
+ };
7570
+ }
7571
+ function declaresMember(source, missing) {
7572
+ const name = missing.member.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7573
+ const declaration = missing.kind === "method" ? new RegExp(String.raw`^\s*(?:static\s+)?func\s+${name}\s*\(`, "m") : new RegExp(String.raw`^\s*(?:static\s+)?(?:@export\s+)?(?:var|const)\s+${name}\b`, "m");
7574
+ return declaration.test(source);
7575
+ }
7576
+ function contradictedDiagnostics(messages, classes, sourceOf) {
7577
+ const read = new Map;
7578
+ const found = [];
7579
+ for (const message of messages) {
7580
+ const missing = missingMemberIn(message);
7581
+ const declaredIn = missing === null ? undefined : classes.get(missing.type);
7582
+ if (missing === null || declaredIn === undefined) {
7583
+ continue;
7584
+ }
7585
+ if (!read.has(declaredIn)) {
7586
+ read.set(declaredIn, sourceOf(declaredIn));
7587
+ }
7588
+ const source = read.get(declaredIn) ?? null;
7589
+ if (source !== null && declaresMember(source, missing)) {
7590
+ found.push({ ...missing, declaredIn });
7591
+ }
7592
+ }
7593
+ return found;
7594
+ }
7560
7595
  function cacheWrittenAt(projectPath) {
7561
7596
  const cache = join6(projectPath, ".godot", "global_script_class_cache.cfg");
7562
7597
  try {
@@ -7621,6 +7656,36 @@ function reason2(stdout, stderr) {
7621
7656
  }
7622
7657
  return stdout.trim().split(/\r?\n/).at(-1) ?? "no output at all";
7623
7658
  }
7659
+ async function runImport(godotPath, projectPath) {
7660
+ const logDir = mkdtempSync2(join7(tmpdir3(), "gdharness-import-"));
7661
+ try {
7662
+ const { stderr } = await run3(godotPath, [
7663
+ "--headless",
7664
+ "--log-file",
7665
+ join7(logDir, "engine.log"),
7666
+ "--path",
7667
+ projectPath,
7668
+ "--import"
7669
+ ]);
7670
+ return { ok: true, messages: problems2(stderr) };
7671
+ } catch (error) {
7672
+ if (error instanceof Error && "stdout" in error && "stderr" in error) {
7673
+ const failed = error;
7674
+ return {
7675
+ ok: false,
7676
+ message: `the import pass failed (exit ${failed.code ?? "unknown"}): ${reason2(failed.stdout, failed.stderr)}`,
7677
+ messages: problems2(failed.stderr)
7678
+ };
7679
+ }
7680
+ return {
7681
+ ok: false,
7682
+ message: `the import pass could not be run: ${error instanceof Error ? error.message : String(error)}`,
7683
+ messages: []
7684
+ };
7685
+ } finally {
7686
+ discard(logDir);
7687
+ }
7688
+ }
7624
7689
  async function runOperation2(engine, operation, params, projectPath) {
7625
7690
  const paramsDir = mkdtempSync2(join7(tmpdir3(), "gdharness-params-"));
7626
7691
  const paramsFile = join7(paramsDir, `${operation}.json`);
@@ -15475,7 +15540,10 @@ var init_tool_definitions = __esm(() => {
15475
15540
  requires: []
15476
15541
  },
15477
15542
  uid: { summary: "the UID of one file", requires: ["resourcePath"] },
15478
- refresh_uids: { summary: "resave every resource so UID references are current", requires: [] },
15543
+ refresh_uids: {
15544
+ summary: "import the project so every script and shader has its .uid sidecar, and name under uidsCreated the ones this made and under stillWithoutUid the ones the engine would not import: it writes no scene and no script, so a project whose sidecars are all present is left untouched",
15545
+ requires: []
15546
+ },
15479
15547
  refresh_classes: {
15480
15548
  summary: 'rewrite .godot/global_script_class_cache.cfg from the class_name declarations on disk, and name under unseenByEditor any class the editor open on this project still cannot resolve: rewriting the file does not reach the list a running editor loaded, so "added: []" means the file was already right rather than that nothing is wrong',
15481
15549
  requires: []
@@ -15716,7 +15784,7 @@ var init_tool_definitions = __esm(() => {
15716
15784
  },
15717
15785
  {
15718
15786
  name: "script_diagnostics",
15719
- description: "Errors and warnings for a script from the editor's language server, and whether the script is clean. Needs the editor running.",
15787
+ description: "Errors and warnings for a script from the editor's language server, and whether the script is clean. Needs the editor running. Any diagnostic saying a member is not present on an inferred type is checked against the file the class cache points at, and one the file contradicts is named under contradictedByTheFile with staleAnalysis saying what to do: the language server hands dependents the type it analysed at startup, so a method added to an existing class_name is reported missing at every caller until the editor restarts.",
15720
15788
  parameters: {
15721
15789
  projectPath: PROJECT_PATH,
15722
15790
  scriptPath: SCRIPT_PATH
@@ -16223,7 +16291,7 @@ var init_tool_definitions = __esm(() => {
16223
16291
  });
16224
16292
 
16225
16293
  // src/headless-operations.ts
16226
- var HEADLESS_OPERATIONS, EDITOR_READS;
16294
+ var HEADLESS_OPERATIONS, ENGINE_PASSES, EDITOR_READS;
16227
16295
  var init_headless_operations = __esm(() => {
16228
16296
  HEADLESS_OPERATIONS = dictionary({
16229
16297
  project_settings: dictionary({
@@ -16245,7 +16313,6 @@ var init_headless_operations = __esm(() => {
16245
16313
  set_options: "set_import_options",
16246
16314
  reimport: "reimport_resource",
16247
16315
  uid: "get_uid",
16248
- refresh_uids: "resave_resources",
16249
16316
  refresh_classes: "refresh_class_cache"
16250
16317
  }),
16251
16318
  project_export: dictionary({ list: "list_export_presets" }),
@@ -16257,6 +16324,9 @@ var init_headless_operations = __esm(() => {
16257
16324
  inheritance: "inspect_inheritance"
16258
16325
  })
16259
16326
  });
16327
+ ENGINE_PASSES = dictionary({
16328
+ project_import: dictionary({ refresh_uids: "import" })
16329
+ });
16260
16330
  EDITOR_READS = dictionary({
16261
16331
  project_settings: dictionary({ get: "get_project_setting" })
16262
16332
  });
@@ -36049,6 +36119,33 @@ function projectStructure(projectPath) {
36049
36119
  visit(projectPath);
36050
36120
  return structure;
36051
36121
  }
36122
+ function scriptsWithoutUid(projectPath) {
36123
+ const found = [];
36124
+ const visit = (directory, prefix) => {
36125
+ if (steppedOver(directory)) {
36126
+ return;
36127
+ }
36128
+ for (const entry of readdirSync4(directory, { withFileTypes: true })) {
36129
+ if (skipped(entry.name)) {
36130
+ continue;
36131
+ }
36132
+ const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
36133
+ if (entry.isDirectory()) {
36134
+ visit(join13(directory, entry.name), spelled);
36135
+ continue;
36136
+ }
36137
+ if (!entry.isFile()) {
36138
+ continue;
36139
+ }
36140
+ const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
36141
+ if (CARRIES_A_UID.has(extension) && !existsSync11(join13(directory, `${entry.name}.uid`))) {
36142
+ found.push(spelled);
36143
+ }
36144
+ }
36145
+ };
36146
+ visit(projectPath, "");
36147
+ return found.sort();
36148
+ }
36052
36149
  function searchProject(projectPath, options) {
36053
36150
  const extensions = new Set(options.fileTypes.map((ext) => ext.replace(/^\./, "").toLowerCase()).filter(Boolean));
36054
36151
  const result = {
@@ -36108,10 +36205,11 @@ function searchProject(projectPath, options) {
36108
36205
  visit(projectPath);
36109
36206
  return result;
36110
36207
  }
36111
- var SKIPPED, ASSET_EXTENSIONS;
36208
+ var SKIPPED, ASSET_EXTENSIONS, CARRIES_A_UID;
36112
36209
  var init_project_scan = __esm(() => {
36113
36210
  SKIPPED = new Set(["node_modules"]);
36114
36211
  ASSET_EXTENSIONS = new Set(["png", "jpg", "jpeg", "webp", "svg", "ttf", "otf", "wav", "mp3", "ogg"]);
36212
+ CARRIES_A_UID = new Set(["gd", "shader", "gdshader"]);
36115
36213
  });
36116
36214
 
36117
36215
  // src/runtime-client.ts
@@ -37218,6 +37316,9 @@ class GodotServer {
37218
37316
  return { ok: true, op };
37219
37317
  }
37220
37318
  async dispatch(tool, op, args) {
37319
+ if (ENGINE_PASSES[tool]?.[op] !== undefined) {
37320
+ return await this.handleRefreshUids(args);
37321
+ }
37221
37322
  const headless = HEADLESS_OPERATIONS[tool]?.[op];
37222
37323
  if (headless !== undefined) {
37223
37324
  const { from: asked, ...answerable } = args;
@@ -37588,6 +37689,33 @@ class GodotServer {
37588
37689
  const { op: _op, projectPath: _projectPath, ...params } = contained.value;
37589
37690
  return this.answer(await this.operation(operation, params, project.value.path));
37590
37691
  }
37692
+ async handleRefreshUids(args) {
37693
+ const project = this.project(args);
37694
+ if (!project.ok) {
37695
+ return project.response;
37696
+ }
37697
+ const engine = await this.engine();
37698
+ if (!engine.ok) {
37699
+ return this.createErrorResponse("No Godot executable found.");
37700
+ }
37701
+ const projectPath = project.value.path;
37702
+ const before = scriptsWithoutUid(projectPath);
37703
+ const imported = await runImport(engine.value, projectPath);
37704
+ if (!imported.ok) {
37705
+ return this.answer(imported);
37706
+ }
37707
+ const after = scriptsWithoutUid(projectPath);
37708
+ const given = before.filter((script) => !after.includes(script));
37709
+ return this.answer({
37710
+ ok: true,
37711
+ messages: imported.messages,
37712
+ payload: {
37713
+ uidsCreated: given,
37714
+ stillWithoutUid: after,
37715
+ note: after.length > 0 ? `${after.length} still have no .uid, which is the engine declining to import them rather than this op skipping them. No scene was written.` : "No scene was written: this reads the project and imports it, and does not resave anything."
37716
+ }
37717
+ });
37718
+ }
37591
37719
  async operation(operation, params, projectPath) {
37592
37720
  const engine = await this.engine();
37593
37721
  if (!engine.ok) {
@@ -37922,14 +38050,44 @@ class GodotServer {
37922
38050
  const severity = asParams(entry)["severity"];
37923
38051
  return severity === 1 || severity === "error" || severity === "ERROR";
37924
38052
  }).length;
38053
+ const contradicted = this.contradictedByTheFile(diagnostics, args);
37925
38054
  return this.jsonTextResponse(markIfStale({
37926
38055
  scriptPath: readString(args, "scriptPath"),
37927
38056
  clean: errors === 0,
37928
38057
  errors,
37929
38058
  warnings: diagnostics.length - errors,
37930
- diagnostics
38059
+ diagnostics,
38060
+ ...contradicted.length === 0 ? {} : {
38061
+ contradictedByTheFile: contradicted,
38062
+ staleAnalysis: `The editor is reporting against an older copy of ${contradicted.length === 1 ? "a type" : "some types"} named above. Each member listed is declared in the file the class cache points at, so those diagnostics are wrong however the code is written. editor_launch restart clears it; editor_rescan and project_import refresh_classes do not, because neither reaches the analysed types the language server built at startup.`
38063
+ }
37931
38064
  }, this.godotBridge.getStatus().addonVersion, SERVER_VERSION));
37932
38065
  }
38066
+ contradictedByTheFile(diagnostics, args) {
38067
+ const messages = diagnostics.map((entry) => asParams(entry)["message"]).filter((message) => typeof message === "string");
38068
+ if (!messages.some((message) => missingMemberIn(message) !== null)) {
38069
+ return [];
38070
+ }
38071
+ const project = this.project(args);
38072
+ if (!project.ok) {
38073
+ return [];
38074
+ }
38075
+ const classes = cachedClasses(project.value.path);
38076
+ if (classes === null) {
38077
+ return [];
38078
+ }
38079
+ return contradictedDiagnostics(messages, classes, (resourcePath) => {
38080
+ const contained = resolveWithinProject(project.value.path, resourcePath);
38081
+ if (!contained.ok || !existsSync14(contained.absolutePath)) {
38082
+ return null;
38083
+ }
38084
+ try {
38085
+ return readFileSync15(contained.absolutePath, "utf8");
38086
+ } catch {
38087
+ return null;
38088
+ }
38089
+ });
38090
+ }
37933
38091
  async handleLSP(toolName, args) {
37934
38092
  const port = this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT);
37935
38093
  if (this.lspClient !== null && this.lspClient.port !== port) {
@@ -40347,7 +40505,13 @@ function namedType(declared) {
40347
40505
  return "any";
40348
40506
  }
40349
40507
  function headlessSection() {
40350
- const byTool = Object.entries(HEADLESS_OPERATIONS).map(([tool, ops]) => `\`${tool}\` (${Object.keys(ops).join(", ")})`).sort();
40508
+ const everyOp = new Map;
40509
+ for (const table of [HEADLESS_OPERATIONS, ENGINE_PASSES]) {
40510
+ for (const [tool, ops] of Object.entries(table)) {
40511
+ everyOp.set(tool, [...everyOp.get(tool) ?? [], ...Object.keys(ops)]);
40512
+ }
40513
+ }
40514
+ const byTool = [...everyOp].map(([tool, ops]) => `\`${tool}\` (${ops.join(", ")})`).sort();
40351
40515
  return [
40352
40516
  "## What a gate can call",
40353
40517
  "",
@@ -40360,7 +40524,8 @@ function headlessSection() {
40360
40524
  "Needing nothing is not the same as changing nothing, and a gate wants both. Most of these",
40361
40525
  "write to the project because that is what they are for: `script_edit create` writes a file,",
40362
40526
  "`project_settings set` rewrites `project.godot`, `project_import reimport` and `refresh_classes`",
40363
- "write under `.godot`. The reading ops are the ones to gate on, and which is which is in each",
40527
+ "write under `.godot`, and `project_import refresh_uids` writes a `.uid` beside each script that",
40528
+ "had none. The reading ops are the ones to gate on, and which is which is in each",
40364
40529
  "op's own summary below rather than assumed from this list.",
40365
40530
  "",
40366
40531
  "Everything else needs the editor (`editor_*`, `debug_*`, `scene_*`, `resource_*`) or a running",
@@ -108,8 +108,6 @@ func _run(operation: String, params: Dictionary) -> Dictionary:
108
108
  # Resource files
109
109
  "get_uid":
110
110
  payload = ResourceFiles.new(_log).get_uid(params)
111
- "resave_resources":
112
- payload = ResourceFiles.new(_log).resave_resources(params)
113
111
  "refresh_class_cache":
114
112
  payload = ClassCache.new(_log).refresh_class_cache(params)
115
113
 
@@ -1,10 +1,8 @@
1
1
  extends RefCounted
2
2
 
3
- const FileWalk = preload("file_walk.gd")
4
3
  const Log = preload("logger.gd")
5
4
 
6
5
  var _log: Log
7
- var _files: FileWalk = FileWalk.new()
8
6
 
9
7
 
10
8
  func _init(p_log: Log) -> void:
@@ -41,92 +39,3 @@ func get_uid(params: Dictionary) -> Dictionary:
41
39
  return {
42
40
  "file": file_path, "absolute_path": absolute_path, "uid": uid_content.strip_edges(), "exists": true
43
41
  }
44
-
45
-
46
- # Resave every scene, script and shader so that UID references are current.
47
- func resave_resources(_params: Dictionary) -> Dictionary:
48
- _log.info("Resaving all resources to update UID references...")
49
- var project_path: String = "res://"
50
-
51
- var scenes: Array[String] = _files.find_files(project_path, ".tscn")
52
- _log.debug("Found " + str(scenes.size()) + " scenes")
53
-
54
- # Resave each scene
55
- var success_count: int = 0
56
- var error_count: int = 0
57
-
58
- for scene_path: String in scenes:
59
- _log.debug("Processing scene: " + scene_path)
60
-
61
- if not FileAccess.file_exists(scene_path):
62
- _log.error("Scene file does not exist at: " + scene_path)
63
- error_count += 1
64
- continue
65
-
66
- var scene: Resource = load(scene_path)
67
- if not scene:
68
- error_count += 1
69
- _log.error("Failed to load: " + scene_path)
70
- continue
71
-
72
- var error: Error = ResourceSaver.save(scene, scene_path)
73
- if error != OK:
74
- error_count += 1
75
- _log.error("Failed to save: " + scene_path + ", error: " + str(error))
76
- continue
77
-
78
- success_count += 1
79
- if _log.debug_mode:
80
- _log.debug("Scene saved successfully: " + scene_path)
81
- var file_check_after: bool = FileAccess.file_exists(scene_path)
82
- _log.debug("File exists check after save: " + str(file_check_after))
83
- if not file_check_after:
84
- _log.error("File reported as saved but does not exist at: " + scene_path)
85
-
86
- # A UID sits beside every script and shader as a .uid file. Outside the editor the save
87
- # below answers OK and writes no sidecar, so what comes back is what was resaved and how
88
- # many are still without one, rather than a count of UIDs this made.
89
- var scripts: Array[String] = (
90
- _files.find_files(project_path, ".gd")
91
- + _files.find_files(project_path, ".shader")
92
- + _files.find_files(project_path, ".gdshader")
93
- )
94
- _log.debug("Found " + str(scripts.size()) + " scripts/shaders")
95
-
96
- var missing_uids: int = 0
97
- var resaved_scripts: int = 0
98
-
99
- for script_path: String in scripts:
100
- _log.debug("Checking UID for: " + script_path)
101
- var uid_path: String = script_path + ".uid"
102
-
103
- var f: FileAccess = FileAccess.open(uid_path, FileAccess.READ)
104
- if f:
105
- _log.debug("UID file already exists for: " + script_path)
106
- continue
107
-
108
- missing_uids += 1
109
- _log.debug("Missing UID file for: " + script_path + ", resaving...")
110
-
111
- var res: Resource = load(script_path)
112
- if not res:
113
- _log.error("Failed to load resource: " + script_path)
114
- continue
115
-
116
- var error: Error = ResourceSaver.save(res, script_path)
117
- if error != OK:
118
- _log.error("Failed to resave: " + script_path + ", error: " + str(error))
119
- continue
120
-
121
- resaved_scripts += 1
122
- _log.debug("Resaved: " + script_path)
123
-
124
- _log.info("Resave operation complete")
125
-
126
- return {
127
- "scenes_processed": scenes.size(),
128
- "scenes_saved": success_count,
129
- "scenes_with_errors": error_count,
130
- "scripts_missing_uids": missing_uids,
131
- "scripts_resaved": resaved_scripts
132
- }
package/build/index.js CHANGED
@@ -24817,6 +24817,41 @@ function cachedClasses(projectPath) {
24817
24817
  }
24818
24818
  return listed;
24819
24819
  }
24820
+ function missingMemberIn(message) {
24821
+ const found = /The (method|property) "([^"(]+)(?:\(\))?" is not present on the inferred type "([^"]+)"/.exec(message);
24822
+ if (found === null) {
24823
+ return null;
24824
+ }
24825
+ return {
24826
+ kind: found[1] === "property" ? "property" : "method",
24827
+ member: found[2] ?? "",
24828
+ type: found[3] ?? ""
24829
+ };
24830
+ }
24831
+ function declaresMember(source, missing) {
24832
+ const name = missing.member.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
24833
+ const declaration = missing.kind === "method" ? new RegExp(String.raw`^\s*(?:static\s+)?func\s+${name}\s*\(`, "m") : new RegExp(String.raw`^\s*(?:static\s+)?(?:@export\s+)?(?:var|const)\s+${name}\b`, "m");
24834
+ return declaration.test(source);
24835
+ }
24836
+ function contradictedDiagnostics(messages, classes, sourceOf) {
24837
+ const read = new Map;
24838
+ const found = [];
24839
+ for (const message of messages) {
24840
+ const missing = missingMemberIn(message);
24841
+ const declaredIn = missing === null ? undefined : classes.get(missing.type);
24842
+ if (missing === null || declaredIn === undefined) {
24843
+ continue;
24844
+ }
24845
+ if (!read.has(declaredIn)) {
24846
+ read.set(declaredIn, sourceOf(declaredIn));
24847
+ }
24848
+ const source = read.get(declaredIn) ?? null;
24849
+ if (source !== null && declaresMember(source, missing)) {
24850
+ found.push({ ...missing, declaredIn });
24851
+ }
24852
+ }
24853
+ return found;
24854
+ }
24820
24855
  function cacheWrittenAt(projectPath) {
24821
24856
  const cache = join3(projectPath, ".godot", "global_script_class_cache.cfg");
24822
24857
  try {
@@ -26390,6 +26425,36 @@ function reason(stdout, stderr) {
26390
26425
  }
26391
26426
  return stdout.trim().split(/\r?\n/).at(-1) ?? "no output at all";
26392
26427
  }
26428
+ async function runImport(godotPath, projectPath) {
26429
+ const logDir = mkdtempSync(join6(tmpdir2(), "gdharness-import-"));
26430
+ try {
26431
+ const { stderr } = await run2(godotPath, [
26432
+ "--headless",
26433
+ "--log-file",
26434
+ join6(logDir, "engine.log"),
26435
+ "--path",
26436
+ projectPath,
26437
+ "--import"
26438
+ ]);
26439
+ return { ok: true, messages: problems(stderr) };
26440
+ } catch (error) {
26441
+ if (error instanceof Error && "stdout" in error && "stderr" in error) {
26442
+ const failed = error;
26443
+ return {
26444
+ ok: false,
26445
+ message: `the import pass failed (exit ${failed.code ?? "unknown"}): ${reason(failed.stdout, failed.stderr)}`,
26446
+ messages: problems(failed.stderr)
26447
+ };
26448
+ }
26449
+ return {
26450
+ ok: false,
26451
+ message: `the import pass could not be run: ${error instanceof Error ? error.message : String(error)}`,
26452
+ messages: []
26453
+ };
26454
+ } finally {
26455
+ discard(logDir);
26456
+ }
26457
+ }
26393
26458
  async function runOperation(engine, operation, params, projectPath) {
26394
26459
  const paramsDir = mkdtempSync(join6(tmpdir2(), "gdharness-params-"));
26395
26460
  const paramsFile = join6(paramsDir, `${operation}.json`);
@@ -26459,7 +26524,6 @@ var HEADLESS_OPERATIONS = dictionary({
26459
26524
  set_options: "set_import_options",
26460
26525
  reimport: "reimport_resource",
26461
26526
  uid: "get_uid",
26462
- refresh_uids: "resave_resources",
26463
26527
  refresh_classes: "refresh_class_cache"
26464
26528
  }),
26465
26529
  project_export: dictionary({ list: "list_export_presets" }),
@@ -26471,6 +26535,9 @@ var HEADLESS_OPERATIONS = dictionary({
26471
26535
  inheritance: "inspect_inheritance"
26472
26536
  })
26473
26537
  });
26538
+ var ENGINE_PASSES = dictionary({
26539
+ project_import: dictionary({ refresh_uids: "import" })
26540
+ });
26474
26541
  var EDITOR_READS = dictionary({
26475
26542
  project_settings: dictionary({ get: "get_project_setting" })
26476
26543
  });
@@ -27408,6 +27475,34 @@ function projectStructure(projectPath) {
27408
27475
  visit(projectPath);
27409
27476
  return structure;
27410
27477
  }
27478
+ var CARRIES_A_UID = new Set(["gd", "shader", "gdshader"]);
27479
+ function scriptsWithoutUid(projectPath) {
27480
+ const found = [];
27481
+ const visit = (directory, prefix) => {
27482
+ if (steppedOver(directory)) {
27483
+ return;
27484
+ }
27485
+ for (const entry of readdirSync3(directory, { withFileTypes: true })) {
27486
+ if (skipped(entry.name)) {
27487
+ continue;
27488
+ }
27489
+ const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
27490
+ if (entry.isDirectory()) {
27491
+ visit(join7(directory, entry.name), spelled);
27492
+ continue;
27493
+ }
27494
+ if (!entry.isFile()) {
27495
+ continue;
27496
+ }
27497
+ const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
27498
+ if (CARRIES_A_UID.has(extension) && !existsSync6(join7(directory, `${entry.name}.uid`))) {
27499
+ found.push(spelled);
27500
+ }
27501
+ }
27502
+ };
27503
+ visit(projectPath, "");
27504
+ return found.sort();
27505
+ }
27411
27506
  function searchProject(projectPath, options) {
27412
27507
  const extensions = new Set(options.fileTypes.map((ext) => ext.replace(/^\./, "").toLowerCase()).filter(Boolean));
27413
27508
  const result = {
@@ -28562,7 +28657,10 @@ var TOOL_SPECS = [
28562
28657
  requires: []
28563
28658
  },
28564
28659
  uid: { summary: "the UID of one file", requires: ["resourcePath"] },
28565
- refresh_uids: { summary: "resave every resource so UID references are current", requires: [] },
28660
+ refresh_uids: {
28661
+ summary: "import the project so every script and shader has its .uid sidecar, and name under uidsCreated the ones this made and under stillWithoutUid the ones the engine would not import: it writes no scene and no script, so a project whose sidecars are all present is left untouched",
28662
+ requires: []
28663
+ },
28566
28664
  refresh_classes: {
28567
28665
  summary: 'rewrite .godot/global_script_class_cache.cfg from the class_name declarations on disk, and name under unseenByEditor any class the editor open on this project still cannot resolve: rewriting the file does not reach the list a running editor loaded, so "added: []" means the file was already right rather than that nothing is wrong',
28568
28666
  requires: []
@@ -28803,7 +28901,7 @@ var TOOL_SPECS = [
28803
28901
  },
28804
28902
  {
28805
28903
  name: "script_diagnostics",
28806
- description: "Errors and warnings for a script from the editor's language server, and whether the script is clean. Needs the editor running.",
28904
+ description: "Errors and warnings for a script from the editor's language server, and whether the script is clean. Needs the editor running. Any diagnostic saying a member is not present on an inferred type is checked against the file the class cache points at, and one the file contradicts is named under contradictedByTheFile with staleAnalysis saying what to do: the language server hands dependents the type it analysed at startup, so a method added to an existing class_name is reported missing at every caller until the editor restarts.",
28807
28905
  parameters: {
28808
28906
  projectPath: PROJECT_PATH,
28809
28907
  scriptPath: SCRIPT_PATH
@@ -29970,6 +30068,9 @@ class GodotServer {
29970
30068
  return { ok: true, op };
29971
30069
  }
29972
30070
  async dispatch(tool, op, args) {
30071
+ if (ENGINE_PASSES[tool]?.[op] !== undefined) {
30072
+ return await this.handleRefreshUids(args);
30073
+ }
29973
30074
  const headless = HEADLESS_OPERATIONS[tool]?.[op];
29974
30075
  if (headless !== undefined) {
29975
30076
  const { from: asked, ...answerable } = args;
@@ -30340,6 +30441,33 @@ class GodotServer {
30340
30441
  const { op: _op, projectPath: _projectPath, ...params } = contained.value;
30341
30442
  return this.answer(await this.operation(operation, params, project.value.path));
30342
30443
  }
30444
+ async handleRefreshUids(args) {
30445
+ const project = this.project(args);
30446
+ if (!project.ok) {
30447
+ return project.response;
30448
+ }
30449
+ const engine = await this.engine();
30450
+ if (!engine.ok) {
30451
+ return this.createErrorResponse("No Godot executable found.");
30452
+ }
30453
+ const projectPath = project.value.path;
30454
+ const before = scriptsWithoutUid(projectPath);
30455
+ const imported = await runImport(engine.value, projectPath);
30456
+ if (!imported.ok) {
30457
+ return this.answer(imported);
30458
+ }
30459
+ const after = scriptsWithoutUid(projectPath);
30460
+ const given = before.filter((script) => !after.includes(script));
30461
+ return this.answer({
30462
+ ok: true,
30463
+ messages: imported.messages,
30464
+ payload: {
30465
+ uidsCreated: given,
30466
+ stillWithoutUid: after,
30467
+ note: after.length > 0 ? `${after.length} still have no .uid, which is the engine declining to import them rather than this op skipping them. No scene was written.` : "No scene was written: this reads the project and imports it, and does not resave anything."
30468
+ }
30469
+ });
30470
+ }
30343
30471
  async operation(operation, params, projectPath) {
30344
30472
  const engine = await this.engine();
30345
30473
  if (!engine.ok) {
@@ -30674,14 +30802,44 @@ class GodotServer {
30674
30802
  const severity = asParams(entry)["severity"];
30675
30803
  return severity === 1 || severity === "error" || severity === "ERROR";
30676
30804
  }).length;
30805
+ const contradicted = this.contradictedByTheFile(diagnostics, args);
30677
30806
  return this.jsonTextResponse(markIfStale({
30678
30807
  scriptPath: readString(args, "scriptPath"),
30679
30808
  clean: errors === 0,
30680
30809
  errors,
30681
30810
  warnings: diagnostics.length - errors,
30682
- diagnostics
30811
+ diagnostics,
30812
+ ...contradicted.length === 0 ? {} : {
30813
+ contradictedByTheFile: contradicted,
30814
+ staleAnalysis: `The editor is reporting against an older copy of ${contradicted.length === 1 ? "a type" : "some types"} named above. Each member listed is declared in the file the class cache points at, so those diagnostics are wrong however the code is written. editor_launch restart clears it; editor_rescan and project_import refresh_classes do not, because neither reaches the analysed types the language server built at startup.`
30815
+ }
30683
30816
  }, this.godotBridge.getStatus().addonVersion, SERVER_VERSION));
30684
30817
  }
30818
+ contradictedByTheFile(diagnostics, args) {
30819
+ const messages = diagnostics.map((entry) => asParams(entry)["message"]).filter((message) => typeof message === "string");
30820
+ if (!messages.some((message) => missingMemberIn(message) !== null)) {
30821
+ return [];
30822
+ }
30823
+ const project = this.project(args);
30824
+ if (!project.ok) {
30825
+ return [];
30826
+ }
30827
+ const classes = cachedClasses(project.value.path);
30828
+ if (classes === null) {
30829
+ return [];
30830
+ }
30831
+ return contradictedDiagnostics(messages, classes, (resourcePath) => {
30832
+ const contained = resolveWithinProject(project.value.path, resourcePath);
30833
+ if (!contained.ok || !existsSync9(contained.absolutePath)) {
30834
+ return null;
30835
+ }
30836
+ try {
30837
+ return readFileSync11(contained.absolutePath, "utf8");
30838
+ } catch {
30839
+ return null;
30840
+ }
30841
+ });
30842
+ }
30685
30843
  async handleLSP(toolName, args) {
30686
30844
  const port = this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT);
30687
30845
  if (this.lspClient !== null && this.lspClient.port !== port) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "0.13.12",
3
+ "version": "0.13.14",
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",