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.
- package/build/cli.js +157 -25
- package/build/index.js +157 -23
- 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
|
|
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:
|
|
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
|
|
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
|
|
44408
|
-
|
|
44409
|
-
|
|
44410
|
-
|
|
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
|
|
44414
|
-
|
|
44415
|
-
|
|
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
|
-
|
|
44424
|
-
|
|
44425
|
-
|
|
44426
|
-
|
|
44427
|
-
|
|
44428
|
-
|
|
44429
|
-
|
|
44430
|
-
|
|
44431
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
36894
|
-
|
|
36895
|
-
|
|
36896
|
-
|
|
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
|
|
36900
|
-
|
|
36901
|
-
|
|
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
|
-
|
|
36910
|
-
|
|
36911
|
-
|
|
36912
|
-
|
|
36913
|
-
|
|
36914
|
-
|
|
36915
|
-
|
|
36916
|
-
|
|
36917
|
-
|
|
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.
|
|
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",
|