gdharness 1.1.49 → 1.1.50

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 (3) hide show
  1. package/build/cli.js +157 -25
  2. package/build/index.js +157 -23
  3. package/package.json +1 -1
package/build/cli.js CHANGED
@@ -17843,10 +17843,14 @@ var init_tool_definitions = __esm(() => {
17843
17843
  },
17844
17844
  {
17845
17845
  name: "script_diagnostics",
17846
- description: "Errors and warnings for a script from the editor's language server, and whether the script is clean. Needs the editor running. 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, a method, property, constant, static function or enum member, comes back under contradictedByTheFile, 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.",
17846
+ description: "Errors and warnings for a script, or for several, from the editor's language server, and whether each is clean. Needs the editor running. 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, a method, property, constant, static function or enum member, comes back under contradictedByTheFile, 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.",
17847
17847
  parameters: {
17848
17848
  projectPath: PROJECT_PATH,
17849
- scriptPath: SCRIPT_PATH
17849
+ scriptPath: {
17850
+ type: ["string", "array"],
17851
+ items: { type: "string" },
17852
+ description: 'A script inside the project, a folder, or a list of either, such as "scripts/player.gd" or ["scripts/economy", "ui/shop.gd"]. A folder is its .gd files at any depth, skipping what the engine skips. One script is answered as it always was; several are answered together under scripts, each in that same shape, with clean, errors and warnings for all of them beside it, and a script that could not be read is named there with why while the rest are still asked about. At most 300 scripts a call.'
17853
+ }
17850
17854
  },
17851
17855
  requires: ["projectPath", "scriptPath"]
17852
17856
  },
@@ -39488,7 +39492,7 @@ class GodotLSPClient {
39488
39492
  const diagnosticsPromise = new Promise((resolveDiagnostics, rejectDiagnostics) => {
39489
39493
  const timer = setTimeout(() => {
39490
39494
  this.diagnosticsWaiters.delete(key);
39491
- rejectDiagnostics(new Error(`Godot published no diagnostics for ${key} within ${DIAGNOSTICS_TIMEOUT_MS}ms. ` + "The language server may not be running, or may not have this file in its workspace."));
39495
+ rejectDiagnostics(new NoDiagnosticsPublished(`Godot published no diagnostics for ${key} within ${DIAGNOSTICS_TIMEOUT_MS}ms. ` + "The language server may not be running, or may not have this file in its workspace."));
39492
39496
  }, DIAGNOSTICS_TIMEOUT_MS);
39493
39497
  this.diagnosticsWaiters.set(key, {
39494
39498
  resolve: resolveDiagnostics,
@@ -39718,11 +39722,12 @@ async function handleLSPTool(client, toolName, args) {
39718
39722
  return asToolResponse({
39719
39723
  error: normalizeLSPError(error, client.port),
39720
39724
  ...error instanceof AnotherProjectsServer ? { serves: error.serves } : {},
39721
- ...error instanceof ArgumentRefusal ? { refusedArguments: true } : {}
39725
+ ...error instanceof ArgumentRefusal ? { refusedArguments: true } : {},
39726
+ ...error instanceof NoDiagnosticsPublished ? { aboutThisFile: true } : {}
39722
39727
  });
39723
39728
  }
39724
39729
  }
39725
- var DEFAULT_LSP_PORT = 6005, DIAGNOSTICS_TIMEOUT_MS = 5000, REQUEST_TIMEOUT_MS2 = 1e4, REFERENCES_TIMEOUT_MS = 180000, LSPTimeout, ArgumentRefusal, AnotherProjectsServer;
39730
+ var DEFAULT_LSP_PORT = 6005, DIAGNOSTICS_TIMEOUT_MS = 5000, REQUEST_TIMEOUT_MS2 = 1e4, REFERENCES_TIMEOUT_MS = 180000, LSPTimeout, ArgumentRefusal, NoDiagnosticsPublished, AnotherProjectsServer;
39726
39731
  var init_lsp_client = __esm(() => {
39727
39732
  init_answer_json();
39728
39733
  init_errors();
@@ -39740,6 +39745,8 @@ var init_lsp_client = __esm(() => {
39740
39745
  };
39741
39746
  ArgumentRefusal = class ArgumentRefusal extends Refusal {
39742
39747
  };
39748
+ NoDiagnosticsPublished = class NoDiagnosticsPublished extends Error {
39749
+ };
39743
39750
  AnotherProjectsServer = class AnotherProjectsServer extends Refusal {
39744
39751
  serves;
39745
39752
  constructor(port, serves, asked) {
@@ -42568,6 +42575,82 @@ function testPathsIn(projectPath, given) {
42568
42575
  value: unique.filter((path) => !unique.some((other) => other !== path && within(path, other))).map((path) => `res://${path}`)
42569
42576
  };
42570
42577
  }
42578
+ function scriptsToDiagnose(projectPath, given) {
42579
+ const listed = Array.isArray(given);
42580
+ if (!listed) {
42581
+ const one = typeof given === "string" ? resolveWithinProject(projectPath, given) : null;
42582
+ if (one === null || !one.ok || !isDirectory(one.absolutePath)) {
42583
+ return { ok: true, value: null };
42584
+ }
42585
+ }
42586
+ const entries = listed ? given : [given];
42587
+ if (entries.length === 0) {
42588
+ return {
42589
+ ok: false,
42590
+ reason: "scriptPath is an empty list: name a script, a folder, or a list of either."
42591
+ };
42592
+ }
42593
+ const found = [];
42594
+ const empty = [];
42595
+ for (const [index, entry] of entries.entries()) {
42596
+ if (typeof entry !== "string" || entry.trim() === "") {
42597
+ return {
42598
+ ok: false,
42599
+ reason: `scriptPath${listed ? `[${index}]` : ""} is ${typeof entry === "string" ? "empty" : describeValue(entry)}, not a script or a folder in the project.`
42600
+ };
42601
+ }
42602
+ const location = resolveWithinProject(projectPath, entry);
42603
+ if (!location.ok) {
42604
+ return { ok: false, reason: location.reason };
42605
+ }
42606
+ const relative = location.relativePath.replace(/\/+$/, "");
42607
+ if (!isDirectory(location.absolutePath)) {
42608
+ found.push(relative);
42609
+ continue;
42610
+ }
42611
+ const inside = scriptsUnder(location.absolutePath, relative);
42612
+ if (inside.length === 0) {
42613
+ empty.push(entry);
42614
+ }
42615
+ found.push(...inside);
42616
+ }
42617
+ const unique = [...new Set(found)];
42618
+ if (unique.length === 0) {
42619
+ return { ok: false, reason: `No .gd script is under ${empty.join(" or ")}.` };
42620
+ }
42621
+ if (unique.length > MOST_DIAGNOSED) {
42622
+ return {
42623
+ ok: false,
42624
+ reason: `scriptPath names ${unique.length} scripts, and one call asks about at most ${MOST_DIAGNOSED}, each a round trip through the language server: name narrower folders, or call again for the rest.`
42625
+ };
42626
+ }
42627
+ return { ok: true, value: unique };
42628
+ }
42629
+ function isDirectory(path) {
42630
+ try {
42631
+ return statSync9(path).isDirectory();
42632
+ } catch {
42633
+ return false;
42634
+ }
42635
+ }
42636
+ function scriptsUnder(directory, prefix) {
42637
+ if (existsSync20(join26(directory, ".gdignore"))) {
42638
+ return [];
42639
+ }
42640
+ const found = [];
42641
+ for (const entry of readdirSync10(directory, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : 1)) {
42642
+ if (entry.name.startsWith(".")) {
42643
+ continue;
42644
+ }
42645
+ const path = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
42646
+ if (entry.isDirectory()) {
42647
+ found.push(...scriptsUnder(join26(directory, entry.name), path));
42648
+ } else if (entry.isFile() && entry.name.endsWith(".gd")) {
42649
+ found.push(path);
42650
+ }
42651
+ }
42652
+ return found;
42653
+ }
42571
42654
  function hasMainScene(projectFile) {
42572
42655
  const scene = parseProjectGodot(readFileSync22(projectFile, "utf8"))["application"]?.["run/main_scene"];
42573
42656
  return typeof scene === "string" && scene !== "";
@@ -44396,39 +44479,88 @@ class GodotServer {
44396
44479
  }
44397
44480
  }
44398
44481
  async handleScriptDiagnostics(args) {
44482
+ const project = this.project(args);
44483
+ const scripts = project.ok ? scriptsToDiagnose(project.value.path, args["scriptPath"]) : null;
44484
+ if (scripts !== null && !scripts.ok) {
44485
+ return this.createErrorResponse(scripts.reason);
44486
+ }
44487
+ if (scripts?.value == null) {
44488
+ const one = await this.diagnosticsOf(args);
44489
+ return "refusal" in one ? one.refusal : this.jsonTextResponse(this.markedIfStale(one.answer));
44490
+ }
44491
+ const progress = callProgress();
44492
+ const answers = [];
44493
+ let errors = 0;
44494
+ let warnings = 0;
44495
+ let unread = 0;
44496
+ for (const [at, scriptPath] of scripts.value.entries()) {
44497
+ progress?.(at + 1, `${at + 1} of ${scripts.value.length}: ${scriptPath}`);
44498
+ const one = await this.diagnosticsOf({ ...args, scriptPath });
44499
+ if ("refusal" in one) {
44500
+ if (!one.perScript) {
44501
+ return one.refusal;
44502
+ }
44503
+ unread += 1;
44504
+ answers.push({ scriptPath, error: one.refusal.content[0]?.text ?? "not read" });
44505
+ continue;
44506
+ }
44507
+ errors += Number(one.answer["errors"]);
44508
+ warnings += Number(one.answer["warnings"]);
44509
+ answers.push(one.answer);
44510
+ }
44511
+ return this.jsonTextResponse(this.markedIfStale({
44512
+ clean: errors === 0 && unread === 0,
44513
+ scriptsChecked: answers.length,
44514
+ errors,
44515
+ warnings,
44516
+ ...unread > 0 ? { scriptsNotRead: unread } : {},
44517
+ scripts: answers
44518
+ }));
44519
+ }
44520
+ markedIfStale(answer) {
44521
+ return markIfStale(answer, this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest(), this.editorCodeOnDisk());
44522
+ }
44523
+ async diagnosticsOf(args) {
44399
44524
  const answer = await this.handleLSP("lsp_get_diagnostics", args);
44400
44525
  const payload = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
44401
44526
  if (payload["error"] !== undefined) {
44402
44527
  const reason = typeof payload["error"] === "string" ? payload["error"] : JSON.stringify(payload["error"]);
44403
44528
  if (payload["refusedArguments"] === true) {
44404
- return this.createErrorResponse(reason);
44529
+ return { refusal: this.createErrorResponse(reason), perScript: true };
44405
44530
  }
44406
44531
  if (typeof payload["serves"] === "string") {
44407
- return this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
44408
- "editor_status names the editor on the bridge and the port it says it serves",
44409
- "editor_launch opens this project's own editor, on a language server port of its own",
44410
- "GDHARNESS_LSP_PORT points this server at the right one"
44411
- ]);
44532
+ return {
44533
+ refusal: this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
44534
+ "editor_status names the editor on the bridge and the port it says it serves",
44535
+ "editor_launch opens this project's own editor, on a language server port of its own",
44536
+ "GDHARNESS_LSP_PORT points this server at the right one"
44537
+ ]),
44538
+ perScript: false
44539
+ };
44412
44540
  }
44413
- return this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
44414
- `Ensure the Godot editor is running with its language server enabled, on port ${this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT)}`,
44415
- "GDHARNESS_LSP_PORT points this server at another one"
44416
- ]);
44541
+ return {
44542
+ refusal: this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
44543
+ `Ensure the Godot editor is running with its language server enabled, on port ${this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT)}`,
44544
+ "GDHARNESS_LSP_PORT points this server at another one"
44545
+ ]),
44546
+ perScript: payload["aboutThisFile"] === true
44547
+ };
44417
44548
  }
44418
44549
  const diagnostics = readArray(payload, "diagnostics") ?? [];
44419
44550
  const errors = diagnostics.filter((entry) => {
44420
44551
  const severity = asParams(entry)["severity"];
44421
44552
  return severity === 1 || severity === "error" || severity === "ERROR";
44422
44553
  }).length;
44423
- const disagrees = this.whereTheFileDisagrees(diagnostics, args);
44424
- return this.jsonTextResponse(markIfStale({
44425
- scriptPath: readString(args, "scriptPath"),
44426
- clean: errors === 0,
44427
- errors,
44428
- warnings: diagnostics.length - errors,
44429
- diagnostics,
44430
- ...disagrees
44431
- }, this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest(), this.editorCodeOnDisk()));
44554
+ return {
44555
+ answer: {
44556
+ scriptPath: readString(args, "scriptPath"),
44557
+ clean: errors === 0,
44558
+ errors,
44559
+ warnings: diagnostics.length - errors,
44560
+ diagnostics,
44561
+ ...this.whereTheFileDisagrees(diagnostics, args)
44562
+ }
44563
+ };
44432
44564
  }
44433
44565
  whereTheFileDisagrees(diagnostics, args) {
44434
44566
  const messages = diagnostics.map((entry) => asParams(entry)["message"]).filter((message) => typeof message === "string");
@@ -47238,7 +47370,7 @@ async function runGodotServer() {
47238
47370
  const server = new GodotServer;
47239
47371
  await server.run();
47240
47372
  }
47241
- var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, GREETING_WAIT_MS = 1e4, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, STOP_WAIT_MS = 1e4, KEEPER_ANSWER_MS = 15000, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, SCAN_WAIT_MS = 30000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/";
47373
+ var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, GREETING_WAIT_MS = 1e4, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, STOP_WAIT_MS = 1e4, KEEPER_ANSWER_MS = 15000, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, SCAN_WAIT_MS = 30000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", MOST_DIAGNOSED = 300;
47242
47374
  var init_server3 = __esm(() => {
47243
47375
  init_mcp();
47244
47376
  init_stdio2();
package/build/index.js CHANGED
@@ -29729,6 +29729,9 @@ class LSPTimeout extends Error {
29729
29729
  class ArgumentRefusal extends Refusal {
29730
29730
  }
29731
29731
 
29732
+ class NoDiagnosticsPublished extends Error {
29733
+ }
29734
+
29732
29735
  class AnotherProjectsServer extends Refusal {
29733
29736
  serves;
29734
29737
  constructor(port, serves, asked) {
@@ -30116,7 +30119,7 @@ class GodotLSPClient {
30116
30119
  const diagnosticsPromise = new Promise((resolveDiagnostics, rejectDiagnostics) => {
30117
30120
  const timer = setTimeout(() => {
30118
30121
  this.diagnosticsWaiters.delete(key);
30119
- rejectDiagnostics(new Error(`Godot published no diagnostics for ${key} within ${DIAGNOSTICS_TIMEOUT_MS}ms. ` + "The language server may not be running, or may not have this file in its workspace."));
30122
+ rejectDiagnostics(new NoDiagnosticsPublished(`Godot published no diagnostics for ${key} within ${DIAGNOSTICS_TIMEOUT_MS}ms. ` + "The language server may not be running, or may not have this file in its workspace."));
30120
30123
  }, DIAGNOSTICS_TIMEOUT_MS);
30121
30124
  this.diagnosticsWaiters.set(key, {
30122
30125
  resolve: resolveDiagnostics,
@@ -30346,7 +30349,8 @@ async function handleLSPTool(client, toolName, args) {
30346
30349
  return asToolResponse({
30347
30350
  error: normalizeLSPError(error, client.port),
30348
30351
  ...error instanceof AnotherProjectsServer ? { serves: error.serves } : {},
30349
- ...error instanceof ArgumentRefusal ? { refusedArguments: true } : {}
30352
+ ...error instanceof ArgumentRefusal ? { refusedArguments: true } : {},
30353
+ ...error instanceof NoDiagnosticsPublished ? { aboutThisFile: true } : {}
30350
30354
  });
30351
30355
  }
30352
30356
  }
@@ -33520,10 +33524,14 @@ var TOOL_SPECS = [
33520
33524
  },
33521
33525
  {
33522
33526
  name: "script_diagnostics",
33523
- description: "Errors and warnings for a script from the editor's language server, and whether the script is clean. Needs the editor running. 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, a method, property, constant, static function or enum member, comes back under contradictedByTheFile, 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.",
33527
+ description: "Errors and warnings for a script, or for several, from the editor's language server, and whether each is clean. Needs the editor running. 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, a method, property, constant, static function or enum member, comes back under contradictedByTheFile, 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.",
33524
33528
  parameters: {
33525
33529
  projectPath: PROJECT_PATH,
33526
- scriptPath: SCRIPT_PATH
33530
+ scriptPath: {
33531
+ type: ["string", "array"],
33532
+ items: { type: "string" },
33533
+ description: 'A script inside the project, a folder, or a list of either, such as "scripts/player.gd" or ["scripts/economy", "ui/shop.gd"]. A folder is its .gd files at any depth, skipping what the engine skips. One script is answered as it always was; several are answered together under scripts, each in that same shape, with clean, errors and warnings for all of them beside it, and a script that could not be read is named there with why while the rest are still asked about. At most 300 scripts a call.'
33534
+ }
33527
33535
  },
33528
33536
  requires: ["projectPath", "scriptPath"]
33529
33537
  },
@@ -35054,6 +35062,83 @@ function testPathsIn(projectPath, given) {
35054
35062
  value: unique.filter((path) => !unique.some((other) => other !== path && within(path, other))).map((path) => `res://${path}`)
35055
35063
  };
35056
35064
  }
35065
+ var MOST_DIAGNOSED = 300;
35066
+ function scriptsToDiagnose(projectPath, given) {
35067
+ const listed = Array.isArray(given);
35068
+ if (!listed) {
35069
+ const one = typeof given === "string" ? resolveWithinProject(projectPath, given) : null;
35070
+ if (one === null || !one.ok || !isDirectory(one.absolutePath)) {
35071
+ return { ok: true, value: null };
35072
+ }
35073
+ }
35074
+ const entries = listed ? given : [given];
35075
+ if (entries.length === 0) {
35076
+ return {
35077
+ ok: false,
35078
+ reason: "scriptPath is an empty list: name a script, a folder, or a list of either."
35079
+ };
35080
+ }
35081
+ const found = [];
35082
+ const empty = [];
35083
+ for (const [index, entry] of entries.entries()) {
35084
+ if (typeof entry !== "string" || entry.trim() === "") {
35085
+ return {
35086
+ ok: false,
35087
+ reason: `scriptPath${listed ? `[${index}]` : ""} is ${typeof entry === "string" ? "empty" : describeValue(entry)}, not a script or a folder in the project.`
35088
+ };
35089
+ }
35090
+ const location = resolveWithinProject(projectPath, entry);
35091
+ if (!location.ok) {
35092
+ return { ok: false, reason: location.reason };
35093
+ }
35094
+ const relative = location.relativePath.replace(/\/+$/, "");
35095
+ if (!isDirectory(location.absolutePath)) {
35096
+ found.push(relative);
35097
+ continue;
35098
+ }
35099
+ const inside = scriptsUnder(location.absolutePath, relative);
35100
+ if (inside.length === 0) {
35101
+ empty.push(entry);
35102
+ }
35103
+ found.push(...inside);
35104
+ }
35105
+ const unique = [...new Set(found)];
35106
+ if (unique.length === 0) {
35107
+ return { ok: false, reason: `No .gd script is under ${empty.join(" or ")}.` };
35108
+ }
35109
+ if (unique.length > MOST_DIAGNOSED) {
35110
+ return {
35111
+ ok: false,
35112
+ reason: `scriptPath names ${unique.length} scripts, and one call asks about at most ${MOST_DIAGNOSED}, each a round trip through the language server: name narrower folders, or call again for the rest.`
35113
+ };
35114
+ }
35115
+ return { ok: true, value: unique };
35116
+ }
35117
+ function isDirectory(path) {
35118
+ try {
35119
+ return statSync8(path).isDirectory();
35120
+ } catch {
35121
+ return false;
35122
+ }
35123
+ }
35124
+ function scriptsUnder(directory, prefix) {
35125
+ if (existsSync17(join22(directory, ".gdignore"))) {
35126
+ return [];
35127
+ }
35128
+ const found = [];
35129
+ for (const entry of readdirSync9(directory, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : 1)) {
35130
+ if (entry.name.startsWith(".")) {
35131
+ continue;
35132
+ }
35133
+ const path = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
35134
+ if (entry.isDirectory()) {
35135
+ found.push(...scriptsUnder(join22(directory, entry.name), path));
35136
+ } else if (entry.isFile() && entry.name.endsWith(".gd")) {
35137
+ found.push(path);
35138
+ }
35139
+ }
35140
+ return found;
35141
+ }
35057
35142
  function hasMainScene(projectFile) {
35058
35143
  const scene = parseProjectGodot(readFileSync20(projectFile, "utf8"))["application"]?.["run/main_scene"];
35059
35144
  return typeof scene === "string" && scene !== "";
@@ -36882,39 +36967,88 @@ class GodotServer {
36882
36967
  }
36883
36968
  }
36884
36969
  async handleScriptDiagnostics(args) {
36970
+ const project = this.project(args);
36971
+ const scripts = project.ok ? scriptsToDiagnose(project.value.path, args["scriptPath"]) : null;
36972
+ if (scripts !== null && !scripts.ok) {
36973
+ return this.createErrorResponse(scripts.reason);
36974
+ }
36975
+ if (scripts?.value == null) {
36976
+ const one = await this.diagnosticsOf(args);
36977
+ return "refusal" in one ? one.refusal : this.jsonTextResponse(this.markedIfStale(one.answer));
36978
+ }
36979
+ const progress = callProgress();
36980
+ const answers = [];
36981
+ let errors = 0;
36982
+ let warnings = 0;
36983
+ let unread = 0;
36984
+ for (const [at, scriptPath] of scripts.value.entries()) {
36985
+ progress?.(at + 1, `${at + 1} of ${scripts.value.length}: ${scriptPath}`);
36986
+ const one = await this.diagnosticsOf({ ...args, scriptPath });
36987
+ if ("refusal" in one) {
36988
+ if (!one.perScript) {
36989
+ return one.refusal;
36990
+ }
36991
+ unread += 1;
36992
+ answers.push({ scriptPath, error: one.refusal.content[0]?.text ?? "not read" });
36993
+ continue;
36994
+ }
36995
+ errors += Number(one.answer["errors"]);
36996
+ warnings += Number(one.answer["warnings"]);
36997
+ answers.push(one.answer);
36998
+ }
36999
+ return this.jsonTextResponse(this.markedIfStale({
37000
+ clean: errors === 0 && unread === 0,
37001
+ scriptsChecked: answers.length,
37002
+ errors,
37003
+ warnings,
37004
+ ...unread > 0 ? { scriptsNotRead: unread } : {},
37005
+ scripts: answers
37006
+ }));
37007
+ }
37008
+ markedIfStale(answer) {
37009
+ return markIfStale(answer, this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest(), this.editorCodeOnDisk());
37010
+ }
37011
+ async diagnosticsOf(args) {
36885
37012
  const answer = await this.handleLSP("lsp_get_diagnostics", args);
36886
37013
  const payload = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
36887
37014
  if (payload["error"] !== undefined) {
36888
37015
  const reason = typeof payload["error"] === "string" ? payload["error"] : JSON.stringify(payload["error"]);
36889
37016
  if (payload["refusedArguments"] === true) {
36890
- return this.createErrorResponse(reason);
37017
+ return { refusal: this.createErrorResponse(reason), perScript: true };
36891
37018
  }
36892
37019
  if (typeof payload["serves"] === "string") {
36893
- return this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
36894
- "editor_status names the editor on the bridge and the port it says it serves",
36895
- "editor_launch opens this project's own editor, on a language server port of its own",
36896
- "GDHARNESS_LSP_PORT points this server at the right one"
36897
- ]);
37020
+ return {
37021
+ refusal: this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
37022
+ "editor_status names the editor on the bridge and the port it says it serves",
37023
+ "editor_launch opens this project's own editor, on a language server port of its own",
37024
+ "GDHARNESS_LSP_PORT points this server at the right one"
37025
+ ]),
37026
+ perScript: false
37027
+ };
36898
37028
  }
36899
- return this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
36900
- `Ensure the Godot editor is running with its language server enabled, on port ${this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT)}`,
36901
- "GDHARNESS_LSP_PORT points this server at another one"
36902
- ]);
37029
+ return {
37030
+ refusal: this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
37031
+ `Ensure the Godot editor is running with its language server enabled, on port ${this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT)}`,
37032
+ "GDHARNESS_LSP_PORT points this server at another one"
37033
+ ]),
37034
+ perScript: payload["aboutThisFile"] === true
37035
+ };
36903
37036
  }
36904
37037
  const diagnostics = readArray(payload, "diagnostics") ?? [];
36905
37038
  const errors = diagnostics.filter((entry) => {
36906
37039
  const severity = asParams(entry)["severity"];
36907
37040
  return severity === 1 || severity === "error" || severity === "ERROR";
36908
37041
  }).length;
36909
- const disagrees = this.whereTheFileDisagrees(diagnostics, args);
36910
- return this.jsonTextResponse(markIfStale({
36911
- scriptPath: readString(args, "scriptPath"),
36912
- clean: errors === 0,
36913
- errors,
36914
- warnings: diagnostics.length - errors,
36915
- diagnostics,
36916
- ...disagrees
36917
- }, this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest(), this.editorCodeOnDisk()));
37042
+ return {
37043
+ answer: {
37044
+ scriptPath: readString(args, "scriptPath"),
37045
+ clean: errors === 0,
37046
+ errors,
37047
+ warnings: diagnostics.length - errors,
37048
+ diagnostics,
37049
+ ...this.whereTheFileDisagrees(diagnostics, args)
37050
+ }
37051
+ };
36918
37052
  }
36919
37053
  whereTheFileDisagrees(diagnostics, args) {
36920
37054
  const messages = diagnostics.map((entry) => asParams(entry)["message"]).filter((message) => typeof message === "string");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.1.49",
3
+ "version": "1.1.50",
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",