gdharness 0.12.9 → 0.12.11
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
|
@@ -7401,7 +7401,7 @@ class UpdateCheck {
|
|
|
7401
7401
|
}
|
|
7402
7402
|
}
|
|
7403
7403
|
refresh(now = Date.now()) {
|
|
7404
|
-
if (!this.enabled || this.checking || now < this.retryAt ||
|
|
7404
|
+
if (!this.enabled || this.checking || now < this.retryAt || !this.stale(now)) {
|
|
7405
7405
|
return;
|
|
7406
7406
|
}
|
|
7407
7407
|
this.checking = true;
|
|
@@ -7421,6 +7421,9 @@ class UpdateCheck {
|
|
|
7421
7421
|
this.checking = false;
|
|
7422
7422
|
});
|
|
7423
7423
|
}
|
|
7424
|
+
stale(now) {
|
|
7425
|
+
return now - this.checkedAt >= CACHE_MS;
|
|
7426
|
+
}
|
|
7424
7427
|
scheduleRetry(now) {
|
|
7425
7428
|
this.retryAt = now + this.backoffMs;
|
|
7426
7429
|
this.backoffMs = Math.min(this.backoffMs * 2, MAX_RETRY_MS);
|
|
@@ -7430,7 +7433,7 @@ class UpdateCheck {
|
|
|
7430
7433
|
if (!this.enabled || latest === null || !isNewer(latest, this.current)) {
|
|
7431
7434
|
return null;
|
|
7432
7435
|
}
|
|
7433
|
-
if (this.checking &&
|
|
7436
|
+
if (this.checking && this.stale(now)) {
|
|
7434
7437
|
return null;
|
|
7435
7438
|
}
|
|
7436
7439
|
return {
|
|
@@ -7527,9 +7530,7 @@ function staleClassNames(projectPath) {
|
|
|
7527
7530
|
}
|
|
7528
7531
|
function unseenByEditor(projectPath, editorHolds) {
|
|
7529
7532
|
const held = new Set(editorHolds);
|
|
7530
|
-
|
|
7531
|
-
const source = cachedClasses(projectPath) ?? declared;
|
|
7532
|
-
return [...source].filter(([name]) => !held.has(name) && declared.has(name)).map(([className, path]) => ({ className, path: declared.get(className) ?? path }));
|
|
7533
|
+
return [...declaredClasses(projectPath)].filter(([name]) => !held.has(name)).map(([className, path]) => ({ className, path }));
|
|
7533
7534
|
}
|
|
7534
7535
|
var init_class_cache = () => {};
|
|
7535
7536
|
|
|
@@ -15323,7 +15324,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15323
15324
|
},
|
|
15324
15325
|
{
|
|
15325
15326
|
name: "project_search",
|
|
15326
|
-
description: "Searches text or a regular expression across project files and returns file paths with line numbers.",
|
|
15327
|
+
description: "Searches text or a regular expression across project files and returns file paths with line numbers. What the engine steps over is not searched: directories spelled with a dot, node_modules, and any directory holding a .gdignore. A vendored engine or an export directory is therefore absent from the results rather than matching the code you are looking for in a copy nobody runs.",
|
|
15327
15328
|
parameters: {
|
|
15328
15329
|
projectPath: PROJECT_PATH,
|
|
15329
15330
|
query: { type: "string", description: "The text or pattern to find." },
|
|
@@ -15762,7 +15763,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15762
15763
|
},
|
|
15763
15764
|
{
|
|
15764
15765
|
name: "editor_rescan",
|
|
15765
|
-
description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. The scan is a change-detecting walk rather than an unconditional reparse, so a file another engine has already imported reads as settled and the walk does not look inside it: its class_name then stays out of the list the editor resolves against, however many times you scan. Any class in that state is named under unseenByEditor, which is what no check on disk can see, since the declaration and the cache are both correct there and only the editor disagrees. The cure is a change to the declaring script, or editor_launch restart; project_import refresh_classes rewrites the cache and does not touch what the editor is holding. Needs the editor connected.",
|
|
15766
|
+
description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. The scan is a change-detecting walk rather than an unconditional reparse, so a file another engine has already imported reads as settled and the walk does not look inside it: its class_name then stays out of the list the editor resolves against, however many times you scan. Any class in that state is named under unseenByEditor, which is what no check on disk can see, since the declaration and the cache are both correct there and only the editor disagrees. The cure is a change to the declaring script, or editor_launch restart; project_import refresh_classes rewrites the cache and does not touch what the editor is holding. The scan also writes .godot/global_script_class_cache.cfg from the list the editor is holding, so a class it cannot resolve is one the scan drops out of that file: unseenByEditor names it before a fresh engine, a CI run or a clone inherits it. Needs the editor connected.",
|
|
15766
15767
|
parameters: {
|
|
15767
15768
|
projectPath: PROJECT_PATH,
|
|
15768
15769
|
timeoutMs: { type: "number", description: "How long to wait for the scan. Default 30000." }
|
|
@@ -15845,7 +15846,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15845
15846
|
limit: {
|
|
15846
15847
|
type: "number",
|
|
15847
15848
|
ops: ["find", "text"],
|
|
15848
|
-
description: "find: the most nodes to answer with, default 100. text: the most lines, default 500, with
|
|
15849
|
+
description: "find: the most nodes to answer with, default 100. text: the most lines, default 500, with omitted saying how many lines that left behind. A screen whose dialog sits under a long list is a screen read with omitted greater than zero, so raise this or point root at the dialog rather than reading the answer as what is on screen."
|
|
15849
15850
|
},
|
|
15850
15851
|
includeHidden: {
|
|
15851
15852
|
type: "boolean",
|
|
@@ -35785,13 +35786,22 @@ var init_lsp_client = __esm(() => {
|
|
|
35785
35786
|
});
|
|
35786
35787
|
|
|
35787
35788
|
// src/project-scan.ts
|
|
35788
|
-
import { readdirSync as readdirSync4, readFileSync as readFileSync10 } from "node:fs";
|
|
35789
|
+
import { existsSync as existsSync10, readdirSync as readdirSync4, readFileSync as readFileSync10 } from "node:fs";
|
|
35789
35790
|
import { join as join12 } from "node:path";
|
|
35791
|
+
function skipped(name) {
|
|
35792
|
+
return name.startsWith(".") || SKIPPED.has(name);
|
|
35793
|
+
}
|
|
35794
|
+
function steppedOver(directory) {
|
|
35795
|
+
return existsSync10(join12(directory, ".gdignore"));
|
|
35796
|
+
}
|
|
35790
35797
|
function projectStructure(projectPath) {
|
|
35791
35798
|
const structure = { scenes: 0, scripts: 0, assets: 0, other: 0 };
|
|
35792
35799
|
const visit = (directory) => {
|
|
35800
|
+
if (steppedOver(directory)) {
|
|
35801
|
+
return;
|
|
35802
|
+
}
|
|
35793
35803
|
for (const entry of readdirSync4(directory, { withFileTypes: true })) {
|
|
35794
|
-
if (entry.name
|
|
35804
|
+
if (skipped(entry.name)) {
|
|
35795
35805
|
continue;
|
|
35796
35806
|
}
|
|
35797
35807
|
if (entry.isDirectory()) {
|
|
@@ -35830,11 +35840,14 @@ function searchProject(projectPath, options) {
|
|
|
35830
35840
|
return false;
|
|
35831
35841
|
};
|
|
35832
35842
|
const visit = (directory) => {
|
|
35843
|
+
if (steppedOver(directory)) {
|
|
35844
|
+
return;
|
|
35845
|
+
}
|
|
35833
35846
|
for (const entry of readdirSync4(directory, { withFileTypes: true })) {
|
|
35834
35847
|
if (full()) {
|
|
35835
35848
|
return;
|
|
35836
35849
|
}
|
|
35837
|
-
if (
|
|
35850
|
+
if (skipped(entry.name)) {
|
|
35838
35851
|
continue;
|
|
35839
35852
|
}
|
|
35840
35853
|
const entryPath = join12(directory, entry.name);
|
|
@@ -35871,12 +35884,12 @@ function searchProject(projectPath, options) {
|
|
|
35871
35884
|
}
|
|
35872
35885
|
var SKIPPED, ASSET_EXTENSIONS;
|
|
35873
35886
|
var init_project_scan = __esm(() => {
|
|
35874
|
-
SKIPPED = new Set(["
|
|
35887
|
+
SKIPPED = new Set(["node_modules"]);
|
|
35875
35888
|
ASSET_EXTENSIONS = new Set(["png", "jpg", "jpeg", "webp", "svg", "ttf", "otf", "wav", "mp3", "ogg"]);
|
|
35876
35889
|
});
|
|
35877
35890
|
|
|
35878
35891
|
// src/runtime-client.ts
|
|
35879
|
-
import { existsSync as
|
|
35892
|
+
import { existsSync as existsSync11, readdirSync as readdirSync5, readFileSync as readFileSync11, unlinkSync } from "node:fs";
|
|
35880
35893
|
import { createConnection as createConnection3 } from "node:net";
|
|
35881
35894
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
35882
35895
|
import { join as join13, resolve as resolve4 } from "node:path";
|
|
@@ -35947,7 +35960,7 @@ function discoverRuntimes(directories = runtimeDirectories()) {
|
|
|
35947
35960
|
return runtimesAnnounced(directories).running;
|
|
35948
35961
|
}
|
|
35949
35962
|
function announcedIn(directory) {
|
|
35950
|
-
if (!
|
|
35963
|
+
if (!existsSync11(directory)) {
|
|
35951
35964
|
return [];
|
|
35952
35965
|
}
|
|
35953
35966
|
const found = [];
|
|
@@ -36284,11 +36297,11 @@ var init_run_record = __esm(() => {
|
|
|
36284
36297
|
});
|
|
36285
36298
|
|
|
36286
36299
|
// src/setup.ts
|
|
36287
|
-
import { cpSync as cpSync2, existsSync as
|
|
36300
|
+
import { cpSync as cpSync2, existsSync as existsSync12, mkdirSync as mkdirSync7, readFileSync as readFileSync13, rmSync as rmSync8, writeFileSync as writeFileSync9 } from "node:fs";
|
|
36288
36301
|
import { dirname as dirname8, join as join15 } from "node:path";
|
|
36289
36302
|
function installedAddonVersion(projectPath) {
|
|
36290
36303
|
const marker = join15(projectPath, "addons", ADDONS2[0], VERSION_MARKER2);
|
|
36291
|
-
if (!
|
|
36304
|
+
if (!existsSync12(marker)) {
|
|
36292
36305
|
return null;
|
|
36293
36306
|
}
|
|
36294
36307
|
const said = readFileSync13(marker, "utf8").trim();
|
|
@@ -36311,7 +36324,7 @@ var init_setup = __esm(() => {
|
|
|
36311
36324
|
import { execFile as execFile5, spawn } from "node:child_process";
|
|
36312
36325
|
import {
|
|
36313
36326
|
closeSync,
|
|
36314
|
-
existsSync as
|
|
36327
|
+
existsSync as existsSync13,
|
|
36315
36328
|
fstatSync,
|
|
36316
36329
|
mkdirSync as mkdirSync8,
|
|
36317
36330
|
mkdtempSync as mkdtempSync3,
|
|
@@ -36459,7 +36472,7 @@ function aboveTheRunner(entry) {
|
|
|
36459
36472
|
}
|
|
36460
36473
|
function elsewhereIn(projectPath, asked) {
|
|
36461
36474
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
36462
|
-
if (wanted === "" ||
|
|
36475
|
+
if (wanted === "" || existsSync13(join16(projectPath, wanted))) {
|
|
36463
36476
|
return "";
|
|
36464
36477
|
}
|
|
36465
36478
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
@@ -37076,7 +37089,7 @@ class GodotServer {
|
|
|
37076
37089
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
37077
37090
|
}
|
|
37078
37091
|
const file = join16(path, "project.godot");
|
|
37079
|
-
if (!
|
|
37092
|
+
if (!existsSync13(file)) {
|
|
37080
37093
|
return {
|
|
37081
37094
|
ok: false,
|
|
37082
37095
|
response: this.createErrorResponse(`Not a Godot project: ${path}`, [
|
|
@@ -37340,7 +37353,7 @@ class GodotServer {
|
|
|
37340
37353
|
log.finish();
|
|
37341
37354
|
const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
|
|
37342
37355
|
const verdict = {
|
|
37343
|
-
exported: exitCode === 0 && log.count("error") === 0 &&
|
|
37356
|
+
exported: exitCode === 0 && log.count("error") === 0 && existsSync13(output.absolutePath),
|
|
37344
37357
|
preset,
|
|
37345
37358
|
outputPath: output.relativePath,
|
|
37346
37359
|
debug,
|
|
@@ -37370,7 +37383,7 @@ class GodotServer {
|
|
|
37370
37383
|
return contained.response;
|
|
37371
37384
|
}
|
|
37372
37385
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
37373
|
-
if (!
|
|
37386
|
+
if (!existsSync13(join16(project.value.path, runner))) {
|
|
37374
37387
|
return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
|
|
37375
37388
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
37376
37389
|
]);
|
|
@@ -37424,7 +37437,7 @@ class GodotServer {
|
|
|
37424
37437
|
let report = null;
|
|
37425
37438
|
let reportProblem = null;
|
|
37426
37439
|
try {
|
|
37427
|
-
const written =
|
|
37440
|
+
const written = existsSync13(reportsDir) ? readdirSync7(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
37428
37441
|
const newest = written.at(-1);
|
|
37429
37442
|
if (newest !== undefined) {
|
|
37430
37443
|
report = parseJUnit(readFileSync14(join16(reportsDir, newest, "results.xml"), "utf8"));
|
|
@@ -37844,7 +37857,7 @@ class GodotServer {
|
|
|
37844
37857
|
return runIsUp(run, editorSays);
|
|
37845
37858
|
}
|
|
37846
37859
|
async runtimeUp(projectPath, before, budgetMs) {
|
|
37847
|
-
if (!
|
|
37860
|
+
if (!existsSync13(join16(projectPath, RUNTIME_AUTOLOAD2.path))) {
|
|
37848
37861
|
return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false });
|
|
37849
37862
|
}
|
|
37850
37863
|
const endpoint = await announcedSince(projectPath, before, {
|
|
@@ -38474,7 +38487,7 @@ var init_server3 = __esm(() => {
|
|
|
38474
38487
|
});
|
|
38475
38488
|
|
|
38476
38489
|
// src/cli.ts
|
|
38477
|
-
import { existsSync as
|
|
38490
|
+
import { existsSync as existsSync14 } from "fs";
|
|
38478
38491
|
import { join as join17, resolve as resolve5 } from "path";
|
|
38479
38492
|
|
|
38480
38493
|
// src/errors.ts
|
|
@@ -39851,7 +39864,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
39851
39864
|
|
|
39852
39865
|
| Ask | Tool |
|
|
39853
39866
|
| --- | --- |
|
|
39854
|
-
| What a screen says | \`runtime_inspect text\`, which reads what is drawn and leaves out what is hidden |
|
|
39867
|
+
| What a screen says | \`runtime_inspect text\`, which reads what is drawn and leaves out what is hidden. Read \`omitted\`: a long list above a dialog pushes it past the limit, and what comes back then is the top of the screen rather than the screen |
|
|
39855
39868
|
| Find a control by the word on it | \`runtime_inspect find\` with \`says\`, rather than listing a screen and reading each one |
|
|
39856
39869
|
| Where a control is, and whether it is visible | \`runtime_inspect\` \`find\`, \`rect\` |
|
|
39857
39870
|
| What a property reads right now | \`runtime_inspect\` \`property\`, \`runtime_invoke\` |
|
|
@@ -39890,12 +39903,21 @@ formatting. \`script_diagnostics\` and \`script_info\` come from the editor's ow
|
|
|
39890
39903
|
\`editor_classes\` need nothing open: they run a short headless engine and are gone before the
|
|
39891
39904
|
answer is printed.
|
|
39892
39905
|
|
|
39906
|
+
Editing a file while a game is running does not change the running game. \`auto_reload\` is an
|
|
39907
|
+
editor plugin and reloads into the editor's own process; the game is a separate process holding
|
|
39908
|
+
its own copy. So a long run is not a reason to stop editing, and the new code arrives on the next
|
|
39909
|
+
\`editor_run start\`.
|
|
39910
|
+
|
|
39893
39911
|
A wall of "Could not find type" on the project's own classes means the editor is not holding
|
|
39894
39912
|
them. \`editor_rescan\` asks it to scan and names any class it still cannot resolve under
|
|
39895
39913
|
\`unseenByEditor\`: its walk skips a file a headless engine has already imported, so the
|
|
39896
39914
|
declaration and the cache can both be right while the editor stays blind to it. Change the
|
|
39897
39915
|
declaring script, or restart the editor.
|
|
39898
39916
|
|
|
39917
|
+
The scan writes \`.godot/global_script_class_cache.cfg\` from the list the editor is holding, so
|
|
39918
|
+
a class it cannot resolve is dropped out of that file too, and the next fresh engine, CI run or
|
|
39919
|
+
clone starts from the narrower one. That is the same class \`unseenByEditor\` names.
|
|
39920
|
+
|
|
39899
39921
|
A diagnostic about a member rather than a type, \`Static function "x()" not found in base "Y"\`
|
|
39900
39922
|
about something the engine compiles, is a different thing and is no longer stale here: each ask
|
|
39901
39923
|
gives the document back when its answer arrives, so the next one is read off disk. If one turns
|
|
@@ -40013,7 +40035,7 @@ class UsageError extends Refusal {
|
|
|
40013
40035
|
function projectArgument(at) {
|
|
40014
40036
|
const named = args.slice(at).find((value) => !value.startsWith("--"));
|
|
40015
40037
|
const projectPath = resolve5(named ?? ".");
|
|
40016
|
-
if (!
|
|
40038
|
+
if (!existsSync14(join17(projectPath, "project.godot"))) {
|
|
40017
40039
|
throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
|
|
40018
40040
|
}
|
|
40019
40041
|
return projectPath;
|
|
@@ -40047,7 +40069,7 @@ async function registerRuntime(godot, projectPath) {
|
|
|
40047
40069
|
return;
|
|
40048
40070
|
}
|
|
40049
40071
|
console.log(`${RUNTIME_AUTOLOAD.name} autoload left at ${named}, which this project registers itself`);
|
|
40050
|
-
if (!
|
|
40072
|
+
if (!existsSync14(join17(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
|
|
40051
40073
|
console.log(` that file is not in this project, so it boots with a missing script: restore it, or point the entry at res://${RUNTIME_AUTOLOAD.path}`);
|
|
40052
40074
|
}
|
|
40053
40075
|
}
|
|
@@ -40179,7 +40201,7 @@ async function setup() {
|
|
|
40179
40201
|
console.log("no harness configured; name one yourself, and gdharness harnesses lists them all");
|
|
40180
40202
|
}
|
|
40181
40203
|
if (!args.includes("--no-skill")) {
|
|
40182
|
-
const directories = skillDirectories(chosen.harnesses, projectPath,
|
|
40204
|
+
const directories = skillDirectories(chosen.harnesses, projectPath, existsSync14);
|
|
40183
40205
|
for (const written of writeSkill(directories, getLocalVersion())) {
|
|
40184
40206
|
console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
|
|
40185
40207
|
}
|
|
@@ -40268,7 +40290,7 @@ async function upgrade() {
|
|
|
40268
40290
|
if (already.length === 0) {
|
|
40269
40291
|
console.log("no harness config names gdharness, so none was re-pinned");
|
|
40270
40292
|
}
|
|
40271
|
-
for (const written of writeSkill(skillDirectories(already, projectPath,
|
|
40293
|
+
for (const written of writeSkill(skillDirectories(already, projectPath, existsSync14), version)) {
|
|
40272
40294
|
console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
|
|
40273
40295
|
}
|
|
40274
40296
|
console.log(`
|
|
@@ -258,7 +258,8 @@ func _matches_apart_from_name(node: Node, wanted: Dictionary[String, String]) ->
|
|
|
258
258
|
##
|
|
259
259
|
## [param limit] is the first few lines rather than all of them, which is how the top of a screen
|
|
260
260
|
## is read without the hall under it: the bar along the top of a guild is eleven lines and the
|
|
261
|
-
## panel it sits on is two hundred.
|
|
261
|
+
## panel it sits on is two hundred. `omitted` says how many lines that left behind, so a screen
|
|
262
|
+
## whose interesting half is below the cut says so in a number rather than in a flag.
|
|
262
263
|
func read_text(params: Dictionary) -> Dictionary:
|
|
263
264
|
var root_path: String = str(params.get("root", "/root"))
|
|
264
265
|
var include_hidden: bool = bool(params.get("include_hidden", false))
|
|
@@ -269,19 +270,20 @@ func read_text(params: Dictionary) -> Dictionary:
|
|
|
269
270
|
return reached
|
|
270
271
|
var root: Node = reached["node"]
|
|
271
272
|
|
|
272
|
-
#
|
|
273
|
-
#
|
|
274
|
-
#
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
var
|
|
278
|
-
var
|
|
273
|
+
# The rest of the subtree is walked and counted rather than stopped at. A flag on its own gets
|
|
274
|
+
# read as a footnote: a screen answered with its first few hundred lines and the dialog the
|
|
275
|
+
# player is being asked to answer below them reads exactly like a dialog that is not there,
|
|
276
|
+
# and a bare `truncated: true` was looked straight past twice before the limit was suspected.
|
|
277
|
+
# A number is what sends somebody back.
|
|
278
|
+
var lines: PackedStringArray = PackedStringArray()
|
|
279
|
+
var left_out: int = _read_into(root, include_hidden, limit, lines)
|
|
279
280
|
return {
|
|
280
281
|
"type": "text",
|
|
281
282
|
"root": root_path,
|
|
282
283
|
"lines": lines,
|
|
283
284
|
"count": lines.size(),
|
|
284
|
-
"truncated":
|
|
285
|
+
"truncated": left_out > 0,
|
|
286
|
+
"omitted": left_out,
|
|
285
287
|
}
|
|
286
288
|
|
|
287
289
|
|
|
@@ -293,16 +295,21 @@ func read_text(params: Dictionary) -> Dictionary:
|
|
|
293
295
|
## screen full of forms answered with every label on it and none of the values in it. The hidden
|
|
294
296
|
## check covers a [Window] for the same walk: a dropdown's popup is a child that is not drawn
|
|
295
297
|
## until it is opened, and reading a closed menu would put every item on the screen.
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
298
|
+
## Fills [param into] up to [param most] lines and answers with how many further lines the rest of
|
|
299
|
+
## the subtree says, which is what the caller is told rather than left to infer.
|
|
300
|
+
func _read_into(node: Node, include_hidden: bool, most: int, into: PackedStringArray) -> int:
|
|
299
301
|
if not include_hidden and not _drawn(node):
|
|
300
|
-
return
|
|
302
|
+
return 0
|
|
303
|
+
var left_out: int = 0
|
|
301
304
|
var said: String = said_by(node)
|
|
302
305
|
if not said.is_empty():
|
|
303
|
-
into.
|
|
306
|
+
if into.size() < most:
|
|
307
|
+
into.append(said)
|
|
308
|
+
else:
|
|
309
|
+
left_out += 1
|
|
304
310
|
for child: Node in node.get_children(true):
|
|
305
|
-
_read_into(child, include_hidden, most, into)
|
|
311
|
+
left_out += _read_into(child, include_hidden, most, into)
|
|
312
|
+
return left_out
|
|
306
313
|
|
|
307
314
|
|
|
308
315
|
## Whether [param node] is on the screen at all, for the three kinds of thing that can be hidden.
|
|
@@ -4,11 +4,23 @@ extends RefCounted
|
|
|
4
4
|
# walk descends into every visible directory and matches whole names, while the extension walk
|
|
5
5
|
# skips anything beginning with a dot, including .import sidecars that would otherwise answer
|
|
6
6
|
# for the resource they belong to.
|
|
7
|
+
#
|
|
8
|
+
# Both stop at a `.gdignore`, because the engine does: nothing under one is imported, so what is
|
|
9
|
+
# in there is not a resource, not a dependency and not a global class. Walking in anyway hands
|
|
10
|
+
# every caller files the engine will never answer for.
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# A directory the engine steps over, marker file and all.
|
|
14
|
+
func is_stepped_over(path: String) -> bool:
|
|
15
|
+
return FileAccess.file_exists(path + ".gdignore")
|
|
7
16
|
|
|
8
17
|
|
|
9
18
|
# Files under `path` whose name ends with `extension`, which is passed with its dot.
|
|
10
19
|
func find_files(path: String, extension: String) -> Array[String]:
|
|
11
20
|
var files: Array[String] = []
|
|
21
|
+
if is_stepped_over(path):
|
|
22
|
+
return files
|
|
23
|
+
|
|
12
24
|
var dir: DirAccess = DirAccess.open(path)
|
|
13
25
|
|
|
14
26
|
if dir:
|
|
@@ -29,6 +41,9 @@ func find_files(path: String, extension: String) -> Array[String]:
|
|
|
29
41
|
# Files under `path` whose extension is in `extensions`, which are passed without their dot.
|
|
30
42
|
func find_files_with_extensions(path: String, extensions: Array) -> Array[String]:
|
|
31
43
|
var files: Array[String] = []
|
|
44
|
+
if is_stepped_over(path):
|
|
45
|
+
return files
|
|
46
|
+
|
|
32
47
|
var dir: DirAccess = DirAccess.open(path)
|
|
33
48
|
|
|
34
49
|
if dir:
|
package/build/index.js
CHANGED
|
@@ -9972,7 +9972,7 @@ class UpdateCheck {
|
|
|
9972
9972
|
}
|
|
9973
9973
|
}
|
|
9974
9974
|
refresh(now = Date.now()) {
|
|
9975
|
-
if (!this.enabled || this.checking || now < this.retryAt ||
|
|
9975
|
+
if (!this.enabled || this.checking || now < this.retryAt || !this.stale(now)) {
|
|
9976
9976
|
return;
|
|
9977
9977
|
}
|
|
9978
9978
|
this.checking = true;
|
|
@@ -9992,6 +9992,9 @@ class UpdateCheck {
|
|
|
9992
9992
|
this.checking = false;
|
|
9993
9993
|
});
|
|
9994
9994
|
}
|
|
9995
|
+
stale(now) {
|
|
9996
|
+
return now - this.checkedAt >= CACHE_MS;
|
|
9997
|
+
}
|
|
9995
9998
|
scheduleRetry(now) {
|
|
9996
9999
|
this.retryAt = now + this.backoffMs;
|
|
9997
10000
|
this.backoffMs = Math.min(this.backoffMs * 2, MAX_RETRY_MS);
|
|
@@ -10001,7 +10004,7 @@ class UpdateCheck {
|
|
|
10001
10004
|
if (!this.enabled || latest === null || !isNewer(latest, this.current)) {
|
|
10002
10005
|
return null;
|
|
10003
10006
|
}
|
|
10004
|
-
if (this.checking &&
|
|
10007
|
+
if (this.checking && this.stale(now)) {
|
|
10005
10008
|
return null;
|
|
10006
10009
|
}
|
|
10007
10010
|
return {
|
|
@@ -10131,7 +10134,7 @@ function defectReport(where, error, godotVersion) {
|
|
|
10131
10134
|
import { execFile as execFile3, spawn } from "node:child_process";
|
|
10132
10135
|
import {
|
|
10133
10136
|
closeSync,
|
|
10134
|
-
existsSync as
|
|
10137
|
+
existsSync as existsSync8,
|
|
10135
10138
|
fstatSync,
|
|
10136
10139
|
mkdirSync as mkdirSync5,
|
|
10137
10140
|
mkdtempSync as mkdtempSync2,
|
|
@@ -24815,9 +24818,7 @@ function staleClassNames(projectPath) {
|
|
|
24815
24818
|
}
|
|
24816
24819
|
function unseenByEditor(projectPath, editorHolds) {
|
|
24817
24820
|
const held = new Set(editorHolds);
|
|
24818
|
-
|
|
24819
|
-
const source = cachedClasses(projectPath) ?? declared;
|
|
24820
|
-
return [...source].filter(([name]) => !held.has(name) && declared.has(name)).map(([className, path]) => ({ className, path: declared.get(className) ?? path }));
|
|
24821
|
+
return [...declaredClasses(projectPath)].filter(([name]) => !held.has(name)).map(([className, path]) => ({ className, path }));
|
|
24821
24822
|
}
|
|
24822
24823
|
|
|
24823
24824
|
// src/dap_client.ts
|
|
@@ -27195,15 +27196,24 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
27195
27196
|
}
|
|
27196
27197
|
|
|
27197
27198
|
// src/project-scan.ts
|
|
27198
|
-
import { readdirSync as readdirSync3, readFileSync as readFileSync5 } from "node:fs";
|
|
27199
|
+
import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync5 } from "node:fs";
|
|
27199
27200
|
import { join as join6 } from "node:path";
|
|
27200
|
-
var SKIPPED = new Set(["
|
|
27201
|
+
var SKIPPED = new Set(["node_modules"]);
|
|
27202
|
+
function skipped(name) {
|
|
27203
|
+
return name.startsWith(".") || SKIPPED.has(name);
|
|
27204
|
+
}
|
|
27205
|
+
function steppedOver(directory) {
|
|
27206
|
+
return existsSync5(join6(directory, ".gdignore"));
|
|
27207
|
+
}
|
|
27201
27208
|
var ASSET_EXTENSIONS = new Set(["png", "jpg", "jpeg", "webp", "svg", "ttf", "otf", "wav", "mp3", "ogg"]);
|
|
27202
27209
|
function projectStructure(projectPath) {
|
|
27203
27210
|
const structure = { scenes: 0, scripts: 0, assets: 0, other: 0 };
|
|
27204
27211
|
const visit = (directory) => {
|
|
27212
|
+
if (steppedOver(directory)) {
|
|
27213
|
+
return;
|
|
27214
|
+
}
|
|
27205
27215
|
for (const entry of readdirSync3(directory, { withFileTypes: true })) {
|
|
27206
|
-
if (entry.name
|
|
27216
|
+
if (skipped(entry.name)) {
|
|
27207
27217
|
continue;
|
|
27208
27218
|
}
|
|
27209
27219
|
if (entry.isDirectory()) {
|
|
@@ -27242,11 +27252,14 @@ function searchProject(projectPath, options) {
|
|
|
27242
27252
|
return false;
|
|
27243
27253
|
};
|
|
27244
27254
|
const visit = (directory) => {
|
|
27255
|
+
if (steppedOver(directory)) {
|
|
27256
|
+
return;
|
|
27257
|
+
}
|
|
27245
27258
|
for (const entry of readdirSync3(directory, { withFileTypes: true })) {
|
|
27246
27259
|
if (full()) {
|
|
27247
27260
|
return;
|
|
27248
27261
|
}
|
|
27249
|
-
if (
|
|
27262
|
+
if (skipped(entry.name)) {
|
|
27250
27263
|
continue;
|
|
27251
27264
|
}
|
|
27252
27265
|
const entryPath = join6(directory, entry.name);
|
|
@@ -27505,7 +27518,7 @@ import { mkdirSync as mkdirSync3, openSync, readdirSync as readdirSync5, readFil
|
|
|
27505
27518
|
import { basename, join as join8 } from "node:path";
|
|
27506
27519
|
|
|
27507
27520
|
// src/runtime-client.ts
|
|
27508
|
-
import { existsSync as
|
|
27521
|
+
import { existsSync as existsSync6, readdirSync as readdirSync4, readFileSync as readFileSync7, unlinkSync } from "node:fs";
|
|
27509
27522
|
import { createConnection as createConnection3 } from "node:net";
|
|
27510
27523
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
27511
27524
|
import { join as join7, resolve as resolve4 } from "node:path";
|
|
@@ -27631,7 +27644,7 @@ function discoverRuntimes(directories = runtimeDirectories()) {
|
|
|
27631
27644
|
return runtimesAnnounced(directories).running;
|
|
27632
27645
|
}
|
|
27633
27646
|
function announcedIn(directory) {
|
|
27634
|
-
if (!
|
|
27647
|
+
if (!existsSync6(directory)) {
|
|
27635
27648
|
return [];
|
|
27636
27649
|
}
|
|
27637
27650
|
const found = [];
|
|
@@ -27961,7 +27974,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
27961
27974
|
}
|
|
27962
27975
|
|
|
27963
27976
|
// src/setup.ts
|
|
27964
|
-
import { cpSync, existsSync as
|
|
27977
|
+
import { cpSync, existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync9, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
27965
27978
|
import { dirname as dirname3, join as join9 } from "node:path";
|
|
27966
27979
|
var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
|
|
27967
27980
|
var RUNTIME_AUTOLOAD = {
|
|
@@ -27971,7 +27984,7 @@ var RUNTIME_AUTOLOAD = {
|
|
|
27971
27984
|
var VERSION_MARKER = ".gdharness-version";
|
|
27972
27985
|
function installedAddonVersion(projectPath) {
|
|
27973
27986
|
const marker = join9(projectPath, "addons", ADDONS[0], VERSION_MARKER);
|
|
27974
|
-
if (!
|
|
27987
|
+
if (!existsSync7(marker)) {
|
|
27975
27988
|
return null;
|
|
27976
27989
|
}
|
|
27977
27990
|
const said = readFileSync9(marker, "utf8").trim();
|
|
@@ -28198,7 +28211,7 @@ var TOOL_SPECS = [
|
|
|
28198
28211
|
},
|
|
28199
28212
|
{
|
|
28200
28213
|
name: "project_search",
|
|
28201
|
-
description: "Searches text or a regular expression across project files and returns file paths with line numbers.",
|
|
28214
|
+
description: "Searches text or a regular expression across project files and returns file paths with line numbers. What the engine steps over is not searched: directories spelled with a dot, node_modules, and any directory holding a .gdignore. A vendored engine or an export directory is therefore absent from the results rather than matching the code you are looking for in a copy nobody runs.",
|
|
28202
28215
|
parameters: {
|
|
28203
28216
|
projectPath: PROJECT_PATH,
|
|
28204
28217
|
query: { type: "string", description: "The text or pattern to find." },
|
|
@@ -28637,7 +28650,7 @@ var TOOL_SPECS = [
|
|
|
28637
28650
|
},
|
|
28638
28651
|
{
|
|
28639
28652
|
name: "editor_rescan",
|
|
28640
|
-
description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. The scan is a change-detecting walk rather than an unconditional reparse, so a file another engine has already imported reads as settled and the walk does not look inside it: its class_name then stays out of the list the editor resolves against, however many times you scan. Any class in that state is named under unseenByEditor, which is what no check on disk can see, since the declaration and the cache are both correct there and only the editor disagrees. The cure is a change to the declaring script, or editor_launch restart; project_import refresh_classes rewrites the cache and does not touch what the editor is holding. Needs the editor connected.",
|
|
28653
|
+
description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. The scan is a change-detecting walk rather than an unconditional reparse, so a file another engine has already imported reads as settled and the walk does not look inside it: its class_name then stays out of the list the editor resolves against, however many times you scan. Any class in that state is named under unseenByEditor, which is what no check on disk can see, since the declaration and the cache are both correct there and only the editor disagrees. The cure is a change to the declaring script, or editor_launch restart; project_import refresh_classes rewrites the cache and does not touch what the editor is holding. The scan also writes .godot/global_script_class_cache.cfg from the list the editor is holding, so a class it cannot resolve is one the scan drops out of that file: unseenByEditor names it before a fresh engine, a CI run or a clone inherits it. Needs the editor connected.",
|
|
28641
28654
|
parameters: {
|
|
28642
28655
|
projectPath: PROJECT_PATH,
|
|
28643
28656
|
timeoutMs: { type: "number", description: "How long to wait for the scan. Default 30000." }
|
|
@@ -28720,7 +28733,7 @@ var TOOL_SPECS = [
|
|
|
28720
28733
|
limit: {
|
|
28721
28734
|
type: "number",
|
|
28722
28735
|
ops: ["find", "text"],
|
|
28723
|
-
description: "find: the most nodes to answer with, default 100. text: the most lines, default 500, with
|
|
28736
|
+
description: "find: the most nodes to answer with, default 100. text: the most lines, default 500, with omitted saying how many lines that left behind. A screen whose dialog sits under a long list is a screen read with omitted greater than zero, so raise this or point root at the dialog rather than reading the answer as what is on screen."
|
|
28724
28737
|
},
|
|
28725
28738
|
includeHidden: {
|
|
28726
28739
|
type: "boolean",
|
|
@@ -29241,7 +29254,7 @@ function aboveTheRunner(entry) {
|
|
|
29241
29254
|
}
|
|
29242
29255
|
function elsewhereIn(projectPath, asked) {
|
|
29243
29256
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
29244
|
-
if (wanted === "" ||
|
|
29257
|
+
if (wanted === "" || existsSync8(join10(projectPath, wanted))) {
|
|
29245
29258
|
return "";
|
|
29246
29259
|
}
|
|
29247
29260
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
@@ -29858,7 +29871,7 @@ class GodotServer {
|
|
|
29858
29871
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
29859
29872
|
}
|
|
29860
29873
|
const file = join10(path, "project.godot");
|
|
29861
|
-
if (!
|
|
29874
|
+
if (!existsSync8(file)) {
|
|
29862
29875
|
return {
|
|
29863
29876
|
ok: false,
|
|
29864
29877
|
response: this.createErrorResponse(`Not a Godot project: ${path}`, [
|
|
@@ -30122,7 +30135,7 @@ class GodotServer {
|
|
|
30122
30135
|
log.finish();
|
|
30123
30136
|
const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
|
|
30124
30137
|
const verdict = {
|
|
30125
|
-
exported: exitCode === 0 && log.count("error") === 0 &&
|
|
30138
|
+
exported: exitCode === 0 && log.count("error") === 0 && existsSync8(output.absolutePath),
|
|
30126
30139
|
preset,
|
|
30127
30140
|
outputPath: output.relativePath,
|
|
30128
30141
|
debug,
|
|
@@ -30152,7 +30165,7 @@ class GodotServer {
|
|
|
30152
30165
|
return contained.response;
|
|
30153
30166
|
}
|
|
30154
30167
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
30155
|
-
if (!
|
|
30168
|
+
if (!existsSync8(join10(project.value.path, runner))) {
|
|
30156
30169
|
return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
|
|
30157
30170
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
30158
30171
|
]);
|
|
@@ -30206,7 +30219,7 @@ class GodotServer {
|
|
|
30206
30219
|
let report = null;
|
|
30207
30220
|
let reportProblem = null;
|
|
30208
30221
|
try {
|
|
30209
|
-
const written =
|
|
30222
|
+
const written = existsSync8(reportsDir) ? readdirSync6(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
30210
30223
|
const newest = written.at(-1);
|
|
30211
30224
|
if (newest !== undefined) {
|
|
30212
30225
|
report = parseJUnit(readFileSync10(join10(reportsDir, newest, "results.xml"), "utf8"));
|
|
@@ -30626,7 +30639,7 @@ class GodotServer {
|
|
|
30626
30639
|
return runIsUp(run, editorSays);
|
|
30627
30640
|
}
|
|
30628
30641
|
async runtimeUp(projectPath, before, budgetMs) {
|
|
30629
|
-
if (!
|
|
30642
|
+
if (!existsSync8(join10(projectPath, RUNTIME_AUTOLOAD.path))) {
|
|
30630
30643
|
return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false });
|
|
30631
30644
|
}
|
|
30632
30645
|
const endpoint = await announcedSince(projectPath, before, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.11",
|
|
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",
|