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 || now - this.checkedAt < CACHE_MS) {
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 && now - this.checkedAt >= CACHE_MS) {
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
- const declared = declaredClasses(projectPath);
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 truncated saying whether there were more."
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.startsWith(".")) {
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 (SKIPPED.has(entry.name)) {
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([".git", ".godot", ".import", "node_modules"]);
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 existsSync10, readdirSync as readdirSync5, readFileSync as readFileSync11, unlinkSync } from "node:fs";
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 (!existsSync10(directory)) {
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 existsSync11, mkdirSync as mkdirSync7, readFileSync as readFileSync13, rmSync as rmSync8, writeFileSync as writeFileSync9 } from "node:fs";
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 (!existsSync11(marker)) {
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 existsSync12,
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 === "" || existsSync12(join16(projectPath, 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 (!existsSync12(file)) {
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 && existsSync12(output.absolutePath),
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 (!existsSync12(join16(project.value.path, runner))) {
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 = existsSync12(reportsDir) ? readdirSync7(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
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 (!existsSync12(join16(projectPath, RUNTIME_AUTOLOAD2.path))) {
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 existsSync13 } from "fs";
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 (!existsSync13(join17(projectPath, "project.godot"))) {
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 (!existsSync13(join17(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
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, existsSync13);
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, existsSync13), version)) {
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
- # One line further than asked for, so that whether anything was left behind is read off the
273
- # walk rather than guessed at from the count: a panel of exactly as many lines as the caller
274
- # asked for is one they have read all of, and saying otherwise sends them back for nothing.
275
- var read: PackedStringArray = PackedStringArray()
276
- _read_into(root, include_hidden, limit + 1, read)
277
- var more: bool = read.size() > limit
278
- var lines: PackedStringArray = read.slice(0, limit) if more else read
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": more,
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
- func _read_into(node: Node, include_hidden: bool, most: int, into: PackedStringArray) -> void:
297
- if into.size() >= most:
298
- return
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.append(said)
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 || now - this.checkedAt < CACHE_MS) {
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 && now - this.checkedAt >= CACHE_MS) {
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 existsSync7,
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
- const declared = declaredClasses(projectPath);
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([".git", ".godot", ".import", "node_modules"]);
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.startsWith(".")) {
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 (SKIPPED.has(entry.name)) {
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 existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync7, unlinkSync } from "node:fs";
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 (!existsSync5(directory)) {
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 existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync9, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "node:fs";
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 (!existsSync6(marker)) {
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 truncated saying whether there were more."
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 === "" || existsSync7(join10(projectPath, 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 (!existsSync7(file)) {
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 && existsSync7(output.absolutePath),
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 (!existsSync7(join10(project.value.path, runner))) {
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 = existsSync7(reportsDir) ? readdirSync6(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
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 (!existsSync7(join10(projectPath, RUNTIME_AUTOLOAD.path))) {
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.9",
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",