gdharness 1.3.7 → 1.3.8

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
@@ -8367,9 +8367,487 @@ var init_doc_cache = __esm(() => {
8367
8367
  CACHE_FILE = /^editor_doc_cache-\d+\.\d+\.res$/;
8368
8368
  });
8369
8369
 
8370
+ // src/project-scan.ts
8371
+ import { existsSync as existsSync6, readdirSync as readdirSync4, readFileSync as readFileSync3 } from "node:fs";
8372
+ import { extname, join as join7 } from "node:path";
8373
+ function skipped(name) {
8374
+ return name.startsWith(".") || SKIPPED.has(name);
8375
+ }
8376
+ function steppedOver(directory) {
8377
+ return gdignored(directory) || installedByUs(directory);
8378
+ }
8379
+ function gdignored(directory) {
8380
+ return existsSync6(join7(directory, ".gdignore"));
8381
+ }
8382
+ function installedByUs(directory) {
8383
+ return existsSync6(join7(directory, VERSION_MARKER));
8384
+ }
8385
+ function projectStructure(projectPath) {
8386
+ const structure = { scenes: 0, scripts: 0, assets: 0, other: 0 };
8387
+ const visit = (directory) => {
8388
+ if (steppedOver(directory)) {
8389
+ return;
8390
+ }
8391
+ for (const entry of readdirSync4(directory, { withFileTypes: true })) {
8392
+ if (skipped(entry.name)) {
8393
+ continue;
8394
+ }
8395
+ if (entry.isDirectory()) {
8396
+ visit(join7(directory, entry.name));
8397
+ } else if (entry.isFile()) {
8398
+ const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
8399
+ if (SIDECAR_EXTENSIONS.has(extension)) {
8400
+ continue;
8401
+ }
8402
+ if (SCENE_EXTENSIONS.has(extension)) {
8403
+ structure.scenes += 1;
8404
+ } else if (extension === "gd" || extension === "cs") {
8405
+ structure.scripts += 1;
8406
+ } else if (ASSET_EXTENSIONS.has(extension)) {
8407
+ structure.assets += 1;
8408
+ } else {
8409
+ structure.other += 1;
8410
+ }
8411
+ }
8412
+ }
8413
+ };
8414
+ visit(projectPath);
8415
+ return structure;
8416
+ }
8417
+ function projectFiles(projectPath, extensions) {
8418
+ const found = [];
8419
+ const visit = (directory, prefix) => {
8420
+ if (steppedOver(directory)) {
8421
+ return;
8422
+ }
8423
+ for (const entry of readdirSync4(directory, { withFileTypes: true })) {
8424
+ if (skipped(entry.name)) {
8425
+ continue;
8426
+ }
8427
+ const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
8428
+ if (entry.isDirectory()) {
8429
+ visit(join7(directory, entry.name), spelled);
8430
+ } else if (entry.isFile() && extensions.includes(extname(entry.name).toLowerCase())) {
8431
+ found.push(spelled);
8432
+ }
8433
+ }
8434
+ };
8435
+ visit(projectPath, "");
8436
+ return found.sort();
8437
+ }
8438
+ function scriptsWithoutUid(projectPath) {
8439
+ const found = [];
8440
+ const visit = (directory, prefix) => {
8441
+ if (steppedOver(directory)) {
8442
+ return;
8443
+ }
8444
+ for (const entry of readdirSync4(directory, { withFileTypes: true })) {
8445
+ if (skipped(entry.name)) {
8446
+ continue;
8447
+ }
8448
+ const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
8449
+ if (entry.isDirectory()) {
8450
+ visit(join7(directory, entry.name), spelled);
8451
+ continue;
8452
+ }
8453
+ if (!entry.isFile()) {
8454
+ continue;
8455
+ }
8456
+ const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
8457
+ if (CARRIES_A_UID.has(extension) && !existsSync6(join7(directory, `${entry.name}.uid`))) {
8458
+ found.push(spelled);
8459
+ }
8460
+ }
8461
+ };
8462
+ visit(projectPath, "");
8463
+ return found.sort();
8464
+ }
8465
+ function pathsInUidCache(projectPath) {
8466
+ let data;
8467
+ try {
8468
+ data = readFileSync3(join7(projectPath, ".godot", "uid_cache.bin"));
8469
+ } catch {
8470
+ return null;
8471
+ }
8472
+ if (data.length < 4) {
8473
+ return null;
8474
+ }
8475
+ const count = data.readUInt32LE(0);
8476
+ const paths = new Map;
8477
+ let at = 4;
8478
+ for (let entry = 0;entry < count; entry++) {
8479
+ if (at + 12 > data.length) {
8480
+ return null;
8481
+ }
8482
+ const id = data.readBigUInt64LE(at);
8483
+ const length = data.readUInt32LE(at + 8);
8484
+ at += 12;
8485
+ if (at + length > data.length) {
8486
+ return null;
8487
+ }
8488
+ paths.set(id, data.toString("utf8", at, at + length));
8489
+ at += length;
8490
+ }
8491
+ return new Set(paths.values());
8492
+ }
8493
+ function isBinary(bytes) {
8494
+ return bytes.subarray(0, 8000).includes(0);
8495
+ }
8496
+ function projectTexts(projectPath) {
8497
+ const found = [];
8498
+ const visit = (directory, prefix) => {
8499
+ if (steppedOver(directory)) {
8500
+ return;
8501
+ }
8502
+ for (const entry of readdirSync4(directory, { withFileTypes: true })) {
8503
+ if (skipped(entry.name)) {
8504
+ continue;
8505
+ }
8506
+ const absolute = join7(directory, entry.name);
8507
+ const spelled = `${prefix}${entry.name}`;
8508
+ if (entry.isDirectory()) {
8509
+ visit(absolute, `${spelled}/`);
8510
+ continue;
8511
+ }
8512
+ const extension = entry.name.includes(".") ? entry.name.split(".").pop()?.toLowerCase() ?? "" : "";
8513
+ if (!entry.isFile() || BINARY_EXTENSIONS.has(extension)) {
8514
+ continue;
8515
+ }
8516
+ const bytes = readFileSync3(absolute);
8517
+ if (!isBinary(bytes)) {
8518
+ found.push({ path: `res://${spelled}`, absolute, text: bytes.toString("utf8") });
8519
+ }
8520
+ }
8521
+ };
8522
+ visit(projectPath, "");
8523
+ return found;
8524
+ }
8525
+ function ignoredTexts(projectPath) {
8526
+ const found = [];
8527
+ const visit = (directory, prefix, ignored) => {
8528
+ if (installedByUs(directory)) {
8529
+ return;
8530
+ }
8531
+ const under = ignored || gdignored(directory);
8532
+ for (const entry of readdirSync4(directory, { withFileTypes: true })) {
8533
+ if (skipped(entry.name)) {
8534
+ continue;
8535
+ }
8536
+ const absolute = join7(directory, entry.name);
8537
+ const spelled = `${prefix}${entry.name}`;
8538
+ if (entry.isDirectory()) {
8539
+ visit(absolute, `${spelled}/`, under);
8540
+ continue;
8541
+ }
8542
+ const extension = entry.name.includes(".") ? entry.name.split(".").pop()?.toLowerCase() ?? "" : "";
8543
+ if (!under || !entry.isFile() || BINARY_EXTENSIONS.has(extension) || entry.name === ".gdignore") {
8544
+ continue;
8545
+ }
8546
+ const bytes = readFileSync3(absolute);
8547
+ if (!isBinary(bytes)) {
8548
+ found.push({ path: `res://${spelled}`, absolute, text: bytes.toString("utf8") });
8549
+ }
8550
+ }
8551
+ };
8552
+ visit(projectPath, "", false);
8553
+ return found;
8554
+ }
8555
+ function plainMatch(line, needle, caseSensitive) {
8556
+ if (caseSensitive) {
8557
+ return line.includes(needle) ? [needle] : null;
8558
+ }
8559
+ const lowered = line.toLowerCase();
8560
+ const at = lowered.indexOf(needle);
8561
+ if (at < 0) {
8562
+ return null;
8563
+ }
8564
+ return [lowered.length === line.length ? line.slice(at, at + needle.length) : needle];
8565
+ }
8566
+ function compiledQuery(query, caseSensitive) {
8567
+ try {
8568
+ return new RegExp(query, caseSensitive ? "" : "i");
8569
+ } catch (error) {
8570
+ const why = errorMessage(error).split(": ").at(-1) ?? "";
8571
+ throw new Error(`'${query}' is not a regular expression: ${why.charAt(0).toLowerCase()}${why.slice(1)}. With regex true, a character such as ( ) [ ] { } . * + ? | ^ $ or \\ stands for itself only after a backslash, as in \\(; or leave regex out to search for the text as written.`);
8572
+ }
8573
+ }
8574
+ function searchProject(projectPath, options) {
8575
+ const extensions = options.fileTypes === undefined ? null : new Set(options.fileTypes.map((ext) => ext.replace(/^\./, "").toLowerCase()).filter(Boolean));
8576
+ const result = {
8577
+ query: options.query,
8578
+ results: [],
8579
+ summary: { files_searched: 0, files_with_matches: 0, total_matches: 0, truncated: false }
8580
+ };
8581
+ const regex = options.regex ? compiledQuery(options.query, options.caseSensitive) : null;
8582
+ const needle = options.caseSensitive ? options.query : options.query.toLowerCase();
8583
+ const full = () => result.summary.truncated;
8584
+ const notSearched = [];
8585
+ const visit = (directory) => {
8586
+ if (steppedOver(directory)) {
8587
+ if (gdignored(directory)) {
8588
+ notSearched.push(`res://${directory.slice(projectPath.length + 1).replace(/\\/g, "/")}/`);
8589
+ }
8590
+ return;
8591
+ }
8592
+ for (const entry of readdirSync4(directory, { withFileTypes: true })) {
8593
+ if (full()) {
8594
+ return;
8595
+ }
8596
+ if (skipped(entry.name)) {
8597
+ continue;
8598
+ }
8599
+ const entryPath = join7(directory, entry.name);
8600
+ if (entry.isDirectory()) {
8601
+ visit(entryPath);
8602
+ continue;
8603
+ }
8604
+ const extension = entry.name.includes(".") ? entry.name.split(".").pop()?.toLowerCase() ?? "" : "";
8605
+ if (!entry.isFile() || extensions !== null && !extensions.has(extension)) {
8606
+ continue;
8607
+ }
8608
+ const bytes = readFileSync3(entryPath);
8609
+ if (extensions === null && isBinary(bytes)) {
8610
+ continue;
8611
+ }
8612
+ result.summary.files_searched += 1;
8613
+ const matches = [];
8614
+ for (const [index, line] of bytes.toString("utf8").split(/\r?\n/).entries()) {
8615
+ const found = regex ? regex.exec(line) : plainMatch(line, needle, options.caseSensitive);
8616
+ if (found === null) {
8617
+ continue;
8618
+ }
8619
+ if (result.summary.total_matches >= options.maxResults) {
8620
+ result.summary.truncated = true;
8621
+ break;
8622
+ }
8623
+ matches.push({ line: index + 1, content: line.trim(), match: found[0] });
8624
+ result.summary.total_matches += 1;
8625
+ }
8626
+ if (matches.length > 0) {
8627
+ const relativePath = entryPath.slice(projectPath.length + 1).replace(/\\/g, "/");
8628
+ result.results.push({ file: `res://${relativePath}`, matches });
8629
+ result.summary.files_with_matches += 1;
8630
+ }
8631
+ }
8632
+ };
8633
+ visit(projectPath);
8634
+ return notSearched.length === 0 ? result : { ...result, not_searched: notSearched };
8635
+ }
8636
+ var SKIPPED, VERSION_MARKER = ".gdharness-version", SCENE_EXTENSIONS, SIDECAR_EXTENSIONS, ASSET_EXTENSIONS, CARRIES_A_UID, BINARY_EXTENSIONS;
8637
+ var init_project_scan = __esm(() => {
8638
+ init_errors();
8639
+ SKIPPED = new Set(["node_modules"]);
8640
+ SCENE_EXTENSIONS = new Set(["tscn", "scn"]);
8641
+ SIDECAR_EXTENSIONS = new Set(["import", "uid"]);
8642
+ ASSET_EXTENSIONS = new Set([
8643
+ "png",
8644
+ "jpg",
8645
+ "jpeg",
8646
+ "webp",
8647
+ "svg",
8648
+ "bmp",
8649
+ "tga",
8650
+ "exr",
8651
+ "hdr",
8652
+ "ktx",
8653
+ "dds",
8654
+ "ttf",
8655
+ "otf",
8656
+ "woff",
8657
+ "woff2",
8658
+ "fnt",
8659
+ "wav",
8660
+ "mp3",
8661
+ "ogg",
8662
+ "glb",
8663
+ "gltf",
8664
+ "obj",
8665
+ "fbx",
8666
+ "blend",
8667
+ "dae",
8668
+ "ogv",
8669
+ "gdshader",
8670
+ "shader"
8671
+ ]);
8672
+ CARRIES_A_UID = new Set(["gd", "shader", "gdshader"]);
8673
+ BINARY_EXTENSIONS = new Set([
8674
+ ...[...ASSET_EXTENSIONS].filter((extension) => !["svg", "gltf", "gdshader", "shader", "fnt"].includes(extension)),
8675
+ "scn",
8676
+ "res",
8677
+ "zip",
8678
+ "pck",
8679
+ "exe",
8680
+ "dll",
8681
+ "so",
8682
+ "dylib"
8683
+ ]);
8684
+ });
8685
+
8686
+ // src/import-freshness.ts
8687
+ import { createHash } from "node:crypto";
8688
+ import { existsSync as existsSync7, readdirSync as readdirSync5, readFileSync as readFileSync4, statSync as statSync5 } from "node:fs";
8689
+ import { join as join8 } from "node:path";
8690
+ function md5Of(file) {
8691
+ let stats;
8692
+ try {
8693
+ stats = statSync5(file);
8694
+ } catch {
8695
+ return null;
8696
+ }
8697
+ const known = hashes.get(file);
8698
+ if (known !== undefined && known.size === stats.size && known.mtimeMs === stats.mtimeMs) {
8699
+ return known.md5;
8700
+ }
8701
+ let md5;
8702
+ try {
8703
+ md5 = createHash("md5").update(readFileSync4(file)).digest("hex");
8704
+ } catch {
8705
+ return null;
8706
+ }
8707
+ hashes.set(file, { size: stats.size, mtimeMs: stats.mtimeMs, md5 });
8708
+ return md5;
8709
+ }
8710
+ function sectionOf(text, section) {
8711
+ const keys = new Map;
8712
+ let inside = false;
8713
+ for (const line of text.split(/\r?\n/)) {
8714
+ const header = /^\[([^\]]+)\]\s*$/.exec(line);
8715
+ if (header !== null) {
8716
+ inside = header[1] === section;
8717
+ continue;
8718
+ }
8719
+ const pair = inside ? /^([A-Za-z0-9_./]+)=(.*)$/.exec(line) : null;
8720
+ if (pair?.[1] !== undefined && pair[2] !== undefined) {
8721
+ keys.set(pair[1], pair[2]);
8722
+ }
8723
+ }
8724
+ return keys;
8725
+ }
8726
+ function unquoted(value) {
8727
+ return value === undefined ? "" : value.replace(/^"(.*)"$/, "$1");
8728
+ }
8729
+ function listed(value) {
8730
+ return [...(value ?? "").matchAll(/"((?:[^"\\]|\\.)*)"/g)].map((match) => match[1] ?? "");
8731
+ }
8732
+ function onDisk(projectPath, resPath) {
8733
+ return join8(projectPath, resPath.slice("res://".length));
8734
+ }
8735
+ function behind(projectPath, resPath, sidecar) {
8736
+ const remap = sectionOf(sidecar, "remap");
8737
+ const importer = unquoted(remap.get("importer"));
8738
+ if (importer === "keep" || importer === "skip") {
8739
+ return null;
8740
+ }
8741
+ const outputs = listed(sectionOf(sidecar, "deps").get("dest_files"));
8742
+ if (outputs.some((output) => !existsSync7(onDisk(projectPath, output)))) {
8743
+ return "its imported output is not on disk";
8744
+ }
8745
+ const name = resPath.slice(resPath.lastIndexOf("/") + 1);
8746
+ const record = join8(projectPath, ".godot", "imported", `${name}-${createHash("md5").update(resPath, "utf8").digest("hex")}.md5`);
8747
+ let recorded = "";
8748
+ try {
8749
+ recorded = /^source_md5="([0-9a-f]*)"/m.exec(readFileSync4(record, "utf8"))?.[1] ?? "";
8750
+ } catch {
8751
+ recorded = "";
8752
+ }
8753
+ if (recorded === "") {
8754
+ return "its import recorded no hash of its source";
8755
+ }
8756
+ const current = md5Of(onDisk(projectPath, resPath));
8757
+ return current === null || current === recorded ? null : "the source changed after it was imported";
8758
+ }
8759
+ function staleImports(projectPath) {
8760
+ const stale = [];
8761
+ const visit = (directory, resDirectory) => {
8762
+ if (existsSync7(join8(directory, ".gdignore")) || existsSync7(join8(directory, VERSION_MARKER))) {
8763
+ return;
8764
+ }
8765
+ let entries;
8766
+ try {
8767
+ entries = readdirSync5(directory, { withFileTypes: true });
8768
+ } catch {
8769
+ return;
8770
+ }
8771
+ const names = new Set(entries.map((entry) => entry.name));
8772
+ for (const entry of entries) {
8773
+ if (entry.name.startsWith(".")) {
8774
+ continue;
8775
+ }
8776
+ const resPath = `${resDirectory}${entry.name}`;
8777
+ if (entry.isDirectory()) {
8778
+ visit(join8(directory, entry.name), `${resPath}/`);
8779
+ continue;
8780
+ }
8781
+ if (!entry.isFile()) {
8782
+ continue;
8783
+ }
8784
+ if (entry.name.endsWith(".import")) {
8785
+ const source = resPath.slice(0, -".import".length);
8786
+ if (!names.has(entry.name.slice(0, -".import".length))) {
8787
+ continue;
8788
+ }
8789
+ let sidecar;
8790
+ try {
8791
+ sidecar = readFileSync4(join8(directory, entry.name), "utf8");
8792
+ } catch {
8793
+ continue;
8794
+ }
8795
+ const reason = behind(projectPath, source, sidecar);
8796
+ if (reason !== null) {
8797
+ stale.push({ path: source, reason });
8798
+ }
8799
+ continue;
8800
+ }
8801
+ const extension = entry.name.slice(entry.name.lastIndexOf(".") + 1).toLowerCase();
8802
+ if (IMPORTABLE.has(extension) && !names.has(`${entry.name}.import`)) {
8803
+ stale.push({ path: resPath, reason: "it has not been imported" });
8804
+ }
8805
+ }
8806
+ };
8807
+ visit(projectPath, "res://");
8808
+ return stale.sort((one, other) => one.path.localeCompare(other.path));
8809
+ }
8810
+ var IMPORTABLE_EXTENSIONS, IMPORTABLE, hashes;
8811
+ var init_import_freshness = __esm(() => {
8812
+ init_project_scan();
8813
+ IMPORTABLE_EXTENSIONS = [
8814
+ "png",
8815
+ "jpg",
8816
+ "jpeg",
8817
+ "webp",
8818
+ "svg",
8819
+ "bmp",
8820
+ "tga",
8821
+ "exr",
8822
+ "hdr",
8823
+ "dds",
8824
+ "ktx",
8825
+ "wav",
8826
+ "mp3",
8827
+ "ogg",
8828
+ "ttf",
8829
+ "otf",
8830
+ "ttc",
8831
+ "otc",
8832
+ "woff",
8833
+ "woff2",
8834
+ "pfb",
8835
+ "pfm",
8836
+ "fnt",
8837
+ "font",
8838
+ "glb",
8839
+ "gltf",
8840
+ "fbx",
8841
+ "obj",
8842
+ "dae"
8843
+ ];
8844
+ IMPORTABLE = new Set(IMPORTABLE_EXTENSIONS);
8845
+ hashes = new Map;
8846
+ });
8847
+
8370
8848
  // src/library-copies.ts
8371
- import { existsSync as existsSync6, readdirSync as readdirSync4, readFileSync as readFileSync3, renameSync, unlinkSync } from "node:fs";
8372
- import { basename as basename5, dirname as dirname6, isAbsolute as isAbsolute3, join as join7 } from "node:path";
8849
+ import { existsSync as existsSync8, readdirSync as readdirSync6, readFileSync as readFileSync5, renameSync, unlinkSync } from "node:fs";
8850
+ import { basename as basename5, dirname as dirname6, isAbsolute as isAbsolute3, join as join9 } from "node:path";
8373
8851
  function freeLibraryCopies(projectPath, platform = process.platform) {
8374
8852
  const setAside = [];
8375
8853
  const swept = [];
@@ -8379,24 +8857,24 @@ function freeLibraryCopies(projectPath, platform = process.platform) {
8379
8857
  for (const library of windowsLibraries(projectPath)) {
8380
8858
  const folder = dirname6(library);
8381
8859
  const copy = `~${basename5(library)}`;
8382
- if (existsSync6(join7(folder, copy))) {
8383
- const aside = join7(folder, `${copy}.aside-${process.pid}-${Date.now()}`);
8860
+ if (existsSync8(join9(folder, copy))) {
8861
+ const aside = join9(folder, `${copy}.aside-${process.pid}-${Date.now()}`);
8384
8862
  try {
8385
- renameSync(join7(folder, copy), aside);
8863
+ renameSync(join9(folder, copy), aside);
8386
8864
  setAside.push(aside);
8387
8865
  } catch {}
8388
8866
  }
8389
8867
  let names;
8390
8868
  try {
8391
- names = readdirSync4(folder);
8869
+ names = readdirSync6(folder);
8392
8870
  } catch {
8393
8871
  continue;
8394
8872
  }
8395
8873
  for (const name of names) {
8396
8874
  if (name.length > copy.length && name.toLowerCase().startsWith(copy.toLowerCase())) {
8397
8875
  try {
8398
- unlinkSync(join7(folder, name));
8399
- swept.push(join7(folder, name));
8876
+ unlinkSync(join9(folder, name));
8877
+ swept.push(join9(folder, name));
8400
8878
  } catch {}
8401
8879
  }
8402
8880
  }
@@ -8404,7 +8882,7 @@ function freeLibraryCopies(projectPath, platform = process.platform) {
8404
8882
  return { setAside, swept };
8405
8883
  }
8406
8884
  function windowsLibraries(projectPath) {
8407
- const listed = readOrNull(join7(projectPath, ".godot", "extension_list.cfg"));
8885
+ const listed = readOrNull(join9(projectPath, ".godot", "extension_list.cfg"));
8408
8886
  if (listed === null) {
8409
8887
  return [];
8410
8888
  }
@@ -8436,13 +8914,13 @@ function resolveResource(projectPath, relativeTo, path) {
8436
8914
  return null;
8437
8915
  }
8438
8916
  if (path.startsWith("res://")) {
8439
- return join7(projectPath, path.slice("res://".length));
8917
+ return join9(projectPath, path.slice("res://".length));
8440
8918
  }
8441
- return isAbsolute3(path) ? path : join7(relativeTo, path);
8919
+ return isAbsolute3(path) ? path : join9(relativeTo, path);
8442
8920
  }
8443
8921
  function readOrNull(path) {
8444
8922
  try {
8445
- return readFileSync3(path, "utf8");
8923
+ return readFileSync5(path, "utf8");
8446
8924
  } catch {
8447
8925
  return null;
8448
8926
  }
@@ -8450,23 +8928,23 @@ function readOrNull(path) {
8450
8928
  var init_library_copies = () => {};
8451
8929
 
8452
8930
  // src/reimport.ts
8453
- import { createHash } from "node:crypto";
8454
- import { existsSync as existsSync7, readFileSync as readFileSync4, rmSync as rmSync3, statSync as statSync5, utimesSync, writeFileSync as writeFileSync3 } from "node:fs";
8455
- import { basename as basename6, join as join8 } from "node:path";
8931
+ import { createHash as createHash2 } from "node:crypto";
8932
+ import { existsSync as existsSync9, readFileSync as readFileSync6, rmSync as rmSync3, statSync as statSync6, utimesSync, writeFileSync as writeFileSync3 } from "node:fs";
8933
+ import { basename as basename6, join as join10 } from "node:path";
8456
8934
  function forceNextImport(projectPath, resourcePath) {
8457
8935
  const relative = resourcePath.replace(/^res:\/\//, "");
8458
- const sidecar = join8(projectPath, `${relative}.import`);
8459
- if (!existsSync7(sidecar)) {
8936
+ const sidecar = join10(projectPath, `${relative}.import`);
8937
+ if (!existsSync9(sidecar)) {
8460
8938
  return;
8461
8939
  }
8462
- const { atime, mtimeMs } = statSync5(sidecar);
8463
- writeFileSync3(sidecar, readFileSync4(sidecar, "utf8").replace(/^valid=false\r?\n/m, ""));
8940
+ const { atime, mtimeMs } = statSync6(sidecar);
8941
+ writeFileSync3(sidecar, readFileSync6(sidecar, "utf8").replace(/^valid=false\r?\n/m, ""));
8464
8942
  utimesSync(sidecar, atime, new Date(mtimeMs + 2000));
8465
8943
  rmSync3(importRecord(projectPath, resourcePath), { force: true });
8466
8944
  }
8467
8945
  function importRecord(projectPath, resourcePath) {
8468
- const digest = createHash("md5").update(resourcePath).digest("hex");
8469
- return join8(projectPath, ".godot", "imported", `${basename6(resourcePath)}-${digest}.md5`);
8946
+ const digest = createHash2("md5").update(resourcePath).digest("hex");
8947
+ return join10(projectPath, ".godot", "imported", `${basename6(resourcePath)}-${digest}.md5`);
8470
8948
  }
8471
8949
  var init_reimport = () => {};
8472
8950
 
@@ -8496,17 +8974,17 @@ function alive(pid) {
8496
8974
  var init_alive = () => {};
8497
8975
 
8498
8976
  // src/scratch.ts
8499
- import { existsSync as existsSync8, mkdtempSync, readdirSync as readdirSync5, rmSync as rmSync4 } from "node:fs";
8977
+ import { existsSync as existsSync10, mkdtempSync, readdirSync as readdirSync7, rmSync as rmSync4 } from "node:fs";
8500
8978
  import { tmpdir as tmpdir2 } from "node:os";
8501
- import { join as join9 } from "node:path";
8979
+ import { join as join11 } from "node:path";
8502
8980
  import process8 from "node:process";
8503
8981
  function scratchDirectory(kind) {
8504
- return mkdtempSync(join9(tmpdir2(), `gdharness-${kind}-${process8.pid}-`));
8982
+ return mkdtempSync(join11(tmpdir2(), `gdharness-${kind}-${process8.pid}-`));
8505
8983
  }
8506
8984
  function sweepAbandonedScratch(directory = tmpdir2()) {
8507
8985
  let entries;
8508
8986
  try {
8509
- entries = readdirSync5(directory);
8987
+ entries = readdirSync7(directory);
8510
8988
  } catch {
8511
8989
  return;
8512
8990
  }
@@ -8517,7 +8995,7 @@ function sweepAbandonedScratch(directory = tmpdir2()) {
8517
8995
  continue;
8518
8996
  }
8519
8997
  try {
8520
- remove(join9(directory, entry));
8998
+ remove(join11(directory, entry));
8521
8999
  } catch {}
8522
9000
  }
8523
9001
  }
@@ -8555,7 +9033,7 @@ function tryAgain(path, attempt) {
8555
9033
  lastTryOnExit();
8556
9034
  }
8557
9035
  const after = AGAIN_AFTER_MS[attempt];
8558
- if (after === undefined || !existsSync8(path)) {
9036
+ if (after === undefined || !existsSync10(path)) {
8559
9037
  waiting.delete(path);
8560
9038
  if (after === undefined) {
8561
9039
  atExit.add(path);
@@ -8744,6 +9222,29 @@ function ignoredYetRan(report, ignored) {
8744
9222
  }
8745
9223
  return found;
8746
9224
  }
9225
+ function ignoredLeftOut(report, ignored, declared) {
9226
+ const nameOf = (path) => (path.split("/").at(-1) ?? path).replace(/\.gd$/, "");
9227
+ const leftOut = new Set;
9228
+ const unmatched = [];
9229
+ for (const entry of ignored) {
9230
+ const bare = entry.replace("res://", "");
9231
+ const at = bare.lastIndexOf(":");
9232
+ const suiteNamed = at === -1 ? bare : bare.slice(0, at);
9233
+ const testNamed = at === -1 ? null : bare.slice(at + 1);
9234
+ const named = declared.filter((suite) => nameOf(suite.path) === suiteNamed || suite.path.includes(suiteNamed)).flatMap((suite) => suite.cases.filter((one) => testNamed === null || one === testNamed).map((test) => ({ suite, test })));
9235
+ if (named.length === 0) {
9236
+ unmatched.push(entry);
9237
+ continue;
9238
+ }
9239
+ for (const { suite, test } of named) {
9240
+ const reported = report.suites.filter((ran) => ran.path === suite.path || ran.path === null && ran.name === nameOf(suite.path)).some((ran) => ran.cases.some((one) => one.name === test || new RegExp(`^${test}\\W`).test(one.name)));
9241
+ if (!reported) {
9242
+ leftOut.add(`${nameOf(suite.path)}:${test}`);
9243
+ }
9244
+ }
9245
+ }
9246
+ return { leftOut: [...leftOut].sort(), unmatched };
9247
+ }
8747
9248
  function caseOf(suite, element) {
8748
9249
  const of = (kind) => element.children.filter((child) => child.name === kind).map((child) => {
8749
9250
  const detail = child.text.trim();
@@ -9104,7 +9605,7 @@ var init_junit = __esm(() => {
9104
9605
  });
9105
9606
 
9106
9607
  // src/script-origins.ts
9107
- import { join as join10 } from "node:path";
9608
+ import { join as join12 } from "node:path";
9108
9609
  function unresolvedClassIn(message) {
9109
9610
  return UNRESOLVED.exec(message)?.[1] ?? null;
9110
9611
  }
@@ -9166,7 +9667,7 @@ function checkWithEngine(godotPath, projectPath) {
9166
9667
  const ran = await runEngine(godotPath, [
9167
9668
  "--headless",
9168
9669
  "--log-file",
9169
- join10(logDir, "engine.log"),
9670
+ join12(logDir, "engine.log"),
9170
9671
  "--path",
9171
9672
  projectPath,
9172
9673
  "--check-only",
@@ -9212,8 +9713,8 @@ var init_script_origins = __esm(() => {
9212
9713
  });
9213
9714
 
9214
9715
  // src/headless.ts
9215
- import { existsSync as existsSync9, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
9216
- import { dirname as dirname7, join as join11 } from "node:path";
9716
+ import { existsSync as existsSync11, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
9717
+ import { dirname as dirname7, join as join13 } from "node:path";
9217
9718
  function snakeCased(params) {
9218
9719
  const result = emptyRecord();
9219
9720
  for (const [key, value] of Object.entries(params)) {
@@ -9225,8 +9726,10 @@ function snakeCased(params) {
9225
9726
  function engineExtras(outcome) {
9226
9727
  const after = outcome.afterAnswerMessages ?? [];
9227
9728
  const before = outcome.beforeOperationMessages ?? [];
9729
+ const notImported = notImportedNote(outcome.messages);
9228
9730
  return {
9229
9731
  ...outcome.messages.length > 0 ? { engine_messages: outcome.messages } : {},
9732
+ ...notImported === undefined ? {} : { engine_messages_note: notImported },
9230
9733
  ...before.length > 0 ? {
9231
9734
  engine_messages_before_operation: before,
9232
9735
  engine_messages_before_operation_note: withNotCompiled(BEFORE_OPERATION_NOTE, before)
@@ -9245,6 +9748,20 @@ function withNotCompiled(note, entries) {
9245
9748
  const path = entries.map(notCompiledIn).find((found) => found !== undefined);
9246
9749
  return path === undefined ? note : `${note} ${path} did not compile, so every run of this project starts its autoloads without it until it is fixed; its errors are among these.`;
9247
9750
  }
9751
+ function notImportedNote(entries) {
9752
+ const paths = [
9753
+ ...new Set(entries.flatMap((entry) => {
9754
+ const path = /^No loader found for resource: (res:\/\/\S+)/.exec(entry.text)?.[1];
9755
+ const extension = path?.split(".").pop()?.toLowerCase() ?? "";
9756
+ return path !== undefined && IMPORTABLE_EXTENSIONS.includes(extension) ? [path] : [];
9757
+ }))
9758
+ ];
9759
+ if (paths.length === 0) {
9760
+ return;
9761
+ }
9762
+ const one = paths.length === 1;
9763
+ return `${paths.join(", ")} ${one ? "has" : "have"} no import the engine can load, which is how a file it imports stands before it is first imported, so what uses ${one ? "it" : "them"} reads ${one ? "it" : "them"} as missing although ${one ? "it is" : "they are"} on disk: project_import reimport imports ${one ? "it" : "them"}.`;
9764
+ }
9248
9765
  function withoutAsides(entries) {
9249
9766
  const kept = [];
9250
9767
  let aside = false;
@@ -9351,7 +9868,7 @@ async function importOnce(godotPath, projectPath) {
9351
9868
  ran = await runEngine(godotPath, [
9352
9869
  "--headless",
9353
9870
  "--log-file",
9354
- join11(logDir, "engine.log"),
9871
+ join13(logDir, "engine.log"),
9355
9872
  "--path",
9356
9873
  projectPath,
9357
9874
  "--import"
@@ -9373,13 +9890,13 @@ async function importOnce(godotPath, projectPath) {
9373
9890
  }
9374
9891
  async function runOperation(engine, operation, params, projectPath) {
9375
9892
  const paramsDir = scratchDirectory("params");
9376
- const paramsFile = join11(paramsDir, `${operation}.json`);
9377
- const answerFile = join11(paramsDir, "answer.json");
9893
+ const paramsFile = join13(paramsDir, `${operation}.json`);
9894
+ const answerFile = join13(paramsDir, "answer.json");
9378
9895
  writeFileSync4(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
9379
9896
  const args = [
9380
9897
  "--headless",
9381
9898
  "--log-file",
9382
- join11(paramsDir, "engine.log"),
9899
+ join13(paramsDir, "engine.log"),
9383
9900
  "--path",
9384
9901
  projectPath,
9385
9902
  "--script",
@@ -9467,10 +9984,10 @@ async function runOperation(engine, operation, params, projectPath) {
9467
9984
  };
9468
9985
  }
9469
9986
  function answerIn(file) {
9470
- if (!existsSync9(file)) {
9987
+ if (!existsSync11(file)) {
9471
9988
  return { kind: "none" };
9472
9989
  }
9473
- const text = readFileSync5(file, "utf8");
9990
+ const text = readFileSync7(file, "utf8");
9474
9991
  try {
9475
9992
  const parsed = JSON.parse(text);
9476
9993
  return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? { kind: "answer", payload: parsed } : { kind: "unreadable", why: `it is JSON but not an object: ${text.slice(0, 200)}` };
@@ -9485,6 +10002,7 @@ var AFTER_ANSWER_NOTE = "Printed after this operation had written its answer, wh
9485
10002
  var init_headless = __esm(() => {
9486
10003
  init_doc_cache();
9487
10004
  init_engine_run();
10005
+ init_import_freshness();
9488
10006
  init_library_copies();
9489
10007
  init_paths();
9490
10008
  init_reimport();
@@ -9493,9 +10011,9 @@ var init_headless = __esm(() => {
9493
10011
  });
9494
10012
 
9495
10013
  // src/update-check.ts
9496
- import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
10014
+ import { existsSync as existsSync12, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "node:fs";
9497
10015
  import { homedir as homedir4, tmpdir as tmpdir3 } from "node:os";
9498
- import { join as join12 } from "node:path";
10016
+ import { join as join14 } from "node:path";
9499
10017
  function packageUrl(root) {
9500
10018
  return `${root.replace(/\/+$/, "")}/gdharness/latest`;
9501
10019
  }
@@ -9517,28 +10035,28 @@ function cacheDirectory(environment) {
9517
10035
  };
9518
10036
  const home = set("HOME") ?? homedir4();
9519
10037
  if (process.platform === "win32") {
9520
- return join12(set("LOCALAPPDATA") ?? home, "gdharness");
10038
+ return join14(set("LOCALAPPDATA") ?? home, "gdharness");
9521
10039
  }
9522
10040
  if (process.platform === "darwin") {
9523
- return join12(home, "Library", "Caches", "gdharness");
10041
+ return join14(home, "Library", "Caches", "gdharness");
9524
10042
  }
9525
- return join12(set("XDG_CACHE_HOME") ?? join12(home, ".cache"), "gdharness");
10043
+ return join14(set("XDG_CACHE_HOME") ?? join14(home, ".cache"), "gdharness");
9526
10044
  }
9527
10045
  function cacheFile(environment = process.env) {
9528
10046
  try {
9529
10047
  const directory = cacheDirectory(environment);
9530
10048
  mkdirSync3(directory, { recursive: true, mode: 448 });
9531
- return join12(directory, "update-check.json");
10049
+ return join14(directory, "update-check.json");
9532
10050
  } catch {
9533
- return join12(tmpdir3(), "gdharness-update-check.json");
10051
+ return join14(tmpdir3(), "gdharness-update-check.json");
9534
10052
  }
9535
10053
  }
9536
10054
  function readCache(path) {
9537
10055
  try {
9538
- if (!existsSync10(path)) {
10056
+ if (!existsSync12(path)) {
9539
10057
  return null;
9540
10058
  }
9541
- const parsed = JSON.parse(readFileSync6(path, "utf8"));
10059
+ const parsed = JSON.parse(readFileSync8(path, "utf8"));
9542
10060
  if (typeof parsed !== "object" || parsed === null) {
9543
10061
  return null;
9544
10062
  }
@@ -9686,7 +10204,7 @@ var init_update_check = __esm(() => {
9686
10204
  });
9687
10205
 
9688
10206
  // src/server-version.ts
9689
- import { readFileSync as readFileSync7 } from "node:fs";
10207
+ import { readFileSync as readFileSync9 } from "node:fs";
9690
10208
  function staleAddon(addonDigest, shippedDigest) {
9691
10209
  return addonDigest !== shippedDigest;
9692
10210
  }
@@ -9745,7 +10263,7 @@ var init_server_version = __esm(() => {
9745
10263
  DEBUG_MODE = process.env["GDHARNESS_DEBUG"] === "1";
9746
10264
  SERVER_VERSION = (() => {
9747
10265
  try {
9748
- const pkg = JSON.parse(readFileSync7(new URL("../package.json", import.meta.url), "utf8"));
10266
+ const pkg = JSON.parse(readFileSync9(new URL("../package.json", import.meta.url), "utf8"));
9749
10267
  return typeof pkg.version === "string" ? pkg.version : "0.0.0";
9750
10268
  } catch {
9751
10269
  return "0.0.0";
@@ -9754,7 +10272,7 @@ var init_server_version = __esm(() => {
9754
10272
  });
9755
10273
 
9756
10274
  // src/issues.ts
9757
- import { createHash as createHash2 } from "node:crypto";
10275
+ import { createHash as createHash3 } from "node:crypto";
9758
10276
  import process9 from "node:process";
9759
10277
  function feedbackNotice(every) {
9760
10278
  return JSON.stringify({
@@ -9773,7 +10291,7 @@ function runtime() {
9773
10291
  function defectSignature(where, message) {
9774
10292
  const normalised = `${where}
9775
10293
  ${message}`.toLowerCase().replaceAll("\\", "/").replaceAll(/(?:[a-z]:)?(?:\/+[\w.-]+){2,}/gu, "<path>").replaceAll(/\d+/gu, "0");
9776
- return createHash2("sha256").update(normalised).digest("hex").slice(0, 8);
10294
+ return createHash3("sha256").update(normalised).digest("hex").slice(0, 8);
9777
10295
  }
9778
10296
  function facts(where, message, godotVersion) {
9779
10297
  return [
@@ -10530,8 +11048,8 @@ var init_gdscript_source = __esm(() => {
10530
11048
  });
10531
11049
 
10532
11050
  // src/class-cache.ts
10533
- import { existsSync as existsSync11, readdirSync as readdirSync6, readFileSync as readFileSync8, statSync as statSync6 } from "node:fs";
10534
- import { join as join13, posix } from "node:path";
11051
+ import { existsSync as existsSync13, readdirSync as readdirSync8, readFileSync as readFileSync10, statSync as statSync7 } from "node:fs";
11052
+ import { join as join15, posix } from "node:path";
10535
11053
  function isToolScript(source) {
10536
11054
  return TOOL.test(codeOf(source));
10537
11055
  }
@@ -10550,18 +11068,18 @@ function declaredClassOf(source) {
10550
11068
  }
10551
11069
  function eachScript(projectPath, each) {
10552
11070
  const visit = (directory, prefix) => {
10553
- if (existsSync11(join13(directory, ".gdignore"))) {
11071
+ if (existsSync13(join15(directory, ".gdignore"))) {
10554
11072
  return;
10555
11073
  }
10556
- for (const entry of readdirSync6(directory, { withFileTypes: true })) {
11074
+ for (const entry of readdirSync8(directory, { withFileTypes: true })) {
10557
11075
  if (entry.name.startsWith(".")) {
10558
11076
  continue;
10559
11077
  }
10560
- const path = join13(directory, entry.name);
11078
+ const path = join15(directory, entry.name);
10561
11079
  if (entry.isDirectory()) {
10562
11080
  visit(path, `${prefix}${entry.name}/`);
10563
11081
  } else if (entry.isFile() && entry.name.endsWith(".gd")) {
10564
- each(`res://${prefix}${entry.name}`, readFileSync8(path, "utf8"));
11082
+ each(`res://${prefix}${entry.name}`, readFileSync10(path, "utf8"));
10565
11083
  }
10566
11084
  }
10567
11085
  };
@@ -10583,12 +11101,12 @@ function scriptsNaming(projectPath, classes) {
10583
11101
  return naming.sort((a, b) => a.script.localeCompare(b.script));
10584
11102
  }
10585
11103
  function cachedClasses(projectPath) {
10586
- const cache = join13(projectPath, ".godot", "global_script_class_cache.cfg");
10587
- if (!existsSync11(cache)) {
11104
+ const cache = join15(projectPath, ".godot", "global_script_class_cache.cfg");
11105
+ if (!existsSync13(cache)) {
10588
11106
  return null;
10589
11107
  }
10590
11108
  const listed = new Map;
10591
- const text = readFileSync8(cache, "utf8");
11109
+ const text = readFileSync10(cache, "utf8");
10592
11110
  for (const entry of text.matchAll(/"class":\s*&"([^"]+)"[\s\S]*?"path":\s*"([^"]+)"/g)) {
10593
11111
  listed.set(entry[1] ?? "", entry[2] ?? "");
10594
11112
  }
@@ -10881,14 +11399,14 @@ function uncachedForTheEngineNote(projectPath, types) {
10881
11399
  }
10882
11400
  const one = types.length === 1;
10883
11401
  const named = types.map((type) => `${type.type} (${type.declaredIn})`).join(", ");
10884
- const cacheExists = existsSync11(join13(projectPath, ".godot", "global_script_class_cache.cfg"));
11402
+ const cacheExists = existsSync13(join15(projectPath, ".godot", "global_script_class_cache.cfg"));
10885
11403
  const state = cacheExists ? `missing from .godot/global_script_class_cache.cfg, which is where the engine that compiled these scripts resolves a class name from, so the error is the cache being behind the files rather than the script.` : `the engine that compiled these scripts resolves a class name from .godot/global_script_class_cache.cfg, which has not been written yet, as in a project no editor has opened, so the error is the missing cache rather than the script.`;
10886
11404
  return `${named} ${one ? "is" : "are"} declared in this project${cacheExists ? " and" : ", but"} ${state} ` + "project_import refresh_classes writes the cache from the declarations on disk; editor_run start " + "writes it before a game, and an editor opening the project writes it when it scans.";
10887
11405
  }
10888
11406
  function cacheWrittenAt(projectPath) {
10889
- const cache = join13(projectPath, ".godot", "global_script_class_cache.cfg");
11407
+ const cache = join15(projectPath, ".godot", "global_script_class_cache.cfg");
10890
11408
  try {
10891
- return statSync6(cache).mtimeMs;
11409
+ return statSync7(cache).mtimeMs;
10892
11410
  } catch {
10893
11411
  return null;
10894
11412
  }
@@ -10900,14 +11418,14 @@ function declaredSince(projectPath, paths, names, since) {
10900
11418
  return false;
10901
11419
  }
10902
11420
  try {
10903
- return statSync6(join13(projectPath, path.slice("res://".length))).mtimeMs > since;
11421
+ return statSync7(join15(projectPath, path.slice("res://".length))).mtimeMs > since;
10904
11422
  } catch {
10905
11423
  return false;
10906
11424
  }
10907
11425
  });
10908
11426
  }
10909
11427
  function cachedAtMissingPaths(cached, projectPath) {
10910
- return [...cached].filter(([, path]) => path.startsWith("res://") && !existsSync11(join13(projectPath, path.slice("res://".length)))).map(([name]) => name).sort();
11428
+ return [...cached].filter(([, path]) => path.startsWith("res://") && !existsSync13(join15(projectPath, path.slice("res://".length)))).map(([name]) => name).sort();
10911
11429
  }
10912
11430
  function staleAgainst(cached, projectPath) {
10913
11431
  return [...declaredClasses(projectPath)].filter(([name, path]) => cached.get(name) !== path).map(([name]) => name);
@@ -10931,314 +11449,6 @@ var init_class_cache = __esm(() => {
10931
11449
  TOOL = /^[ \t]*(?:@[a-z_]+(?:\([^)\n]*\))?[ \t]+)*@tool\b/m;
10932
11450
  });
10933
11451
 
10934
- // src/project-scan.ts
10935
- import { existsSync as existsSync12, readdirSync as readdirSync7, readFileSync as readFileSync9 } from "node:fs";
10936
- import { extname, join as join14 } from "node:path";
10937
- function skipped(name) {
10938
- return name.startsWith(".") || SKIPPED.has(name);
10939
- }
10940
- function steppedOver(directory) {
10941
- return gdignored(directory) || installedByUs(directory);
10942
- }
10943
- function gdignored(directory) {
10944
- return existsSync12(join14(directory, ".gdignore"));
10945
- }
10946
- function installedByUs(directory) {
10947
- return existsSync12(join14(directory, VERSION_MARKER));
10948
- }
10949
- function projectStructure(projectPath) {
10950
- const structure = { scenes: 0, scripts: 0, assets: 0, other: 0 };
10951
- const visit = (directory) => {
10952
- if (steppedOver(directory)) {
10953
- return;
10954
- }
10955
- for (const entry of readdirSync7(directory, { withFileTypes: true })) {
10956
- if (skipped(entry.name)) {
10957
- continue;
10958
- }
10959
- if (entry.isDirectory()) {
10960
- visit(join14(directory, entry.name));
10961
- } else if (entry.isFile()) {
10962
- const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
10963
- if (SCENE_EXTENSIONS.has(extension)) {
10964
- structure.scenes += 1;
10965
- } else if (extension === "gd" || extension === "cs") {
10966
- structure.scripts += 1;
10967
- } else if (ASSET_EXTENSIONS.has(extension)) {
10968
- structure.assets += 1;
10969
- } else {
10970
- structure.other += 1;
10971
- }
10972
- }
10973
- }
10974
- };
10975
- visit(projectPath);
10976
- return structure;
10977
- }
10978
- function projectFiles(projectPath, extensions) {
10979
- const found = [];
10980
- const visit = (directory, prefix) => {
10981
- if (steppedOver(directory)) {
10982
- return;
10983
- }
10984
- for (const entry of readdirSync7(directory, { withFileTypes: true })) {
10985
- if (skipped(entry.name)) {
10986
- continue;
10987
- }
10988
- const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
10989
- if (entry.isDirectory()) {
10990
- visit(join14(directory, entry.name), spelled);
10991
- } else if (entry.isFile() && extensions.includes(extname(entry.name).toLowerCase())) {
10992
- found.push(spelled);
10993
- }
10994
- }
10995
- };
10996
- visit(projectPath, "");
10997
- return found.sort();
10998
- }
10999
- function scriptsWithoutUid(projectPath) {
11000
- const found = [];
11001
- const visit = (directory, prefix) => {
11002
- if (steppedOver(directory)) {
11003
- return;
11004
- }
11005
- for (const entry of readdirSync7(directory, { withFileTypes: true })) {
11006
- if (skipped(entry.name)) {
11007
- continue;
11008
- }
11009
- const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
11010
- if (entry.isDirectory()) {
11011
- visit(join14(directory, entry.name), spelled);
11012
- continue;
11013
- }
11014
- if (!entry.isFile()) {
11015
- continue;
11016
- }
11017
- const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
11018
- if (CARRIES_A_UID.has(extension) && !existsSync12(join14(directory, `${entry.name}.uid`))) {
11019
- found.push(spelled);
11020
- }
11021
- }
11022
- };
11023
- visit(projectPath, "");
11024
- return found.sort();
11025
- }
11026
- function pathsInUidCache(projectPath) {
11027
- let data;
11028
- try {
11029
- data = readFileSync9(join14(projectPath, ".godot", "uid_cache.bin"));
11030
- } catch {
11031
- return null;
11032
- }
11033
- if (data.length < 4) {
11034
- return null;
11035
- }
11036
- const count = data.readUInt32LE(0);
11037
- const paths = new Map;
11038
- let at = 4;
11039
- for (let entry = 0;entry < count; entry++) {
11040
- if (at + 12 > data.length) {
11041
- return null;
11042
- }
11043
- const id = data.readBigUInt64LE(at);
11044
- const length = data.readUInt32LE(at + 8);
11045
- at += 12;
11046
- if (at + length > data.length) {
11047
- return null;
11048
- }
11049
- paths.set(id, data.toString("utf8", at, at + length));
11050
- at += length;
11051
- }
11052
- return new Set(paths.values());
11053
- }
11054
- function isBinary(bytes) {
11055
- return bytes.subarray(0, 8000).includes(0);
11056
- }
11057
- function projectTexts(projectPath) {
11058
- const found = [];
11059
- const visit = (directory, prefix) => {
11060
- if (steppedOver(directory)) {
11061
- return;
11062
- }
11063
- for (const entry of readdirSync7(directory, { withFileTypes: true })) {
11064
- if (skipped(entry.name)) {
11065
- continue;
11066
- }
11067
- const absolute = join14(directory, entry.name);
11068
- const spelled = `${prefix}${entry.name}`;
11069
- if (entry.isDirectory()) {
11070
- visit(absolute, `${spelled}/`);
11071
- continue;
11072
- }
11073
- const extension = entry.name.includes(".") ? entry.name.split(".").pop()?.toLowerCase() ?? "" : "";
11074
- if (!entry.isFile() || BINARY_EXTENSIONS.has(extension)) {
11075
- continue;
11076
- }
11077
- const bytes = readFileSync9(absolute);
11078
- if (!isBinary(bytes)) {
11079
- found.push({ path: `res://${spelled}`, absolute, text: bytes.toString("utf8") });
11080
- }
11081
- }
11082
- };
11083
- visit(projectPath, "");
11084
- return found;
11085
- }
11086
- function ignoredTexts(projectPath) {
11087
- const found = [];
11088
- const visit = (directory, prefix, ignored) => {
11089
- if (installedByUs(directory)) {
11090
- return;
11091
- }
11092
- const under = ignored || gdignored(directory);
11093
- for (const entry of readdirSync7(directory, { withFileTypes: true })) {
11094
- if (skipped(entry.name)) {
11095
- continue;
11096
- }
11097
- const absolute = join14(directory, entry.name);
11098
- const spelled = `${prefix}${entry.name}`;
11099
- if (entry.isDirectory()) {
11100
- visit(absolute, `${spelled}/`, under);
11101
- continue;
11102
- }
11103
- const extension = entry.name.includes(".") ? entry.name.split(".").pop()?.toLowerCase() ?? "" : "";
11104
- if (!under || !entry.isFile() || BINARY_EXTENSIONS.has(extension) || entry.name === ".gdignore") {
11105
- continue;
11106
- }
11107
- const bytes = readFileSync9(absolute);
11108
- if (!isBinary(bytes)) {
11109
- found.push({ path: `res://${spelled}`, absolute, text: bytes.toString("utf8") });
11110
- }
11111
- }
11112
- };
11113
- visit(projectPath, "", false);
11114
- return found;
11115
- }
11116
- function plainMatch(line, needle, caseSensitive) {
11117
- if (caseSensitive) {
11118
- return line.includes(needle) ? [needle] : null;
11119
- }
11120
- const lowered = line.toLowerCase();
11121
- const at = lowered.indexOf(needle);
11122
- if (at < 0) {
11123
- return null;
11124
- }
11125
- return [lowered.length === line.length ? line.slice(at, at + needle.length) : needle];
11126
- }
11127
- function compiledQuery(query, caseSensitive) {
11128
- try {
11129
- return new RegExp(query, caseSensitive ? "" : "i");
11130
- } catch (error) {
11131
- const why = errorMessage(error).split(": ").at(-1) ?? "";
11132
- throw new Error(`'${query}' is not a regular expression: ${why.charAt(0).toLowerCase()}${why.slice(1)}. With regex true, a character such as ( ) [ ] { } . * + ? | ^ $ or \\ stands for itself only after a backslash, as in \\(; or leave regex out to search for the text as written.`);
11133
- }
11134
- }
11135
- function searchProject(projectPath, options) {
11136
- const extensions = options.fileTypes === undefined ? null : new Set(options.fileTypes.map((ext) => ext.replace(/^\./, "").toLowerCase()).filter(Boolean));
11137
- const result = {
11138
- query: options.query,
11139
- results: [],
11140
- summary: { files_searched: 0, files_with_matches: 0, total_matches: 0, truncated: false }
11141
- };
11142
- const regex = options.regex ? compiledQuery(options.query, options.caseSensitive) : null;
11143
- const needle = options.caseSensitive ? options.query : options.query.toLowerCase();
11144
- const full = () => result.summary.truncated;
11145
- const visit = (directory) => {
11146
- if (steppedOver(directory)) {
11147
- return;
11148
- }
11149
- for (const entry of readdirSync7(directory, { withFileTypes: true })) {
11150
- if (full()) {
11151
- return;
11152
- }
11153
- if (skipped(entry.name)) {
11154
- continue;
11155
- }
11156
- const entryPath = join14(directory, entry.name);
11157
- if (entry.isDirectory()) {
11158
- visit(entryPath);
11159
- continue;
11160
- }
11161
- const extension = entry.name.includes(".") ? entry.name.split(".").pop()?.toLowerCase() ?? "" : "";
11162
- if (!entry.isFile() || extensions !== null && !extensions.has(extension)) {
11163
- continue;
11164
- }
11165
- const bytes = readFileSync9(entryPath);
11166
- if (extensions === null && isBinary(bytes)) {
11167
- continue;
11168
- }
11169
- result.summary.files_searched += 1;
11170
- const matches = [];
11171
- for (const [index, line] of bytes.toString("utf8").split(/\r?\n/).entries()) {
11172
- const found = regex ? regex.exec(line) : plainMatch(line, needle, options.caseSensitive);
11173
- if (found === null) {
11174
- continue;
11175
- }
11176
- if (result.summary.total_matches >= options.maxResults) {
11177
- result.summary.truncated = true;
11178
- break;
11179
- }
11180
- matches.push({ line: index + 1, content: line.trim(), match: found[0] });
11181
- result.summary.total_matches += 1;
11182
- }
11183
- if (matches.length > 0) {
11184
- const relativePath = entryPath.slice(projectPath.length + 1).replace(/\\/g, "/");
11185
- result.results.push({ file: `res://${relativePath}`, matches });
11186
- result.summary.files_with_matches += 1;
11187
- }
11188
- }
11189
- };
11190
- visit(projectPath);
11191
- return result;
11192
- }
11193
- var SKIPPED, VERSION_MARKER = ".gdharness-version", SCENE_EXTENSIONS, ASSET_EXTENSIONS, CARRIES_A_UID, BINARY_EXTENSIONS;
11194
- var init_project_scan = __esm(() => {
11195
- init_errors();
11196
- SKIPPED = new Set(["node_modules"]);
11197
- SCENE_EXTENSIONS = new Set(["tscn", "scn"]);
11198
- ASSET_EXTENSIONS = new Set([
11199
- "png",
11200
- "jpg",
11201
- "jpeg",
11202
- "webp",
11203
- "svg",
11204
- "bmp",
11205
- "tga",
11206
- "exr",
11207
- "hdr",
11208
- "ktx",
11209
- "dds",
11210
- "ttf",
11211
- "otf",
11212
- "woff",
11213
- "woff2",
11214
- "fnt",
11215
- "wav",
11216
- "mp3",
11217
- "ogg",
11218
- "glb",
11219
- "gltf",
11220
- "obj",
11221
- "fbx",
11222
- "blend",
11223
- "dae",
11224
- "ogv",
11225
- "gdshader",
11226
- "shader"
11227
- ]);
11228
- CARRIES_A_UID = new Set(["gd", "shader", "gdshader"]);
11229
- BINARY_EXTENSIONS = new Set([
11230
- ...[...ASSET_EXTENSIONS].filter((extension) => !["svg", "gltf", "gdshader", "shader", "fnt"].includes(extension)),
11231
- "scn",
11232
- "res",
11233
- "zip",
11234
- "pck",
11235
- "exe",
11236
- "dll",
11237
- "so",
11238
- "dylib"
11239
- ]);
11240
- });
11241
-
11242
11452
  // src/answer-json.ts
11243
11453
  function answerJson(value) {
11244
11454
  return JSON.stringify(value, null, 2).replace(SCALAR_ARRAY, (_whole, items) => `[${items.split(/,\n\s+/).join(", ")}]`);
@@ -11250,7 +11460,7 @@ var init_answer_json = __esm(() => {
11250
11460
  });
11251
11461
 
11252
11462
  // src/resources.ts
11253
- import { readFileSync as readFileSync10, realpathSync as realpathSync2 } from "node:fs";
11463
+ import { readFileSync as readFileSync11, realpathSync as realpathSync2 } from "node:fs";
11254
11464
  import { extname as extname2, resolve as resolve3 } from "node:path";
11255
11465
  function ensureProjectPath(getProjectPath) {
11256
11466
  const projectPath = getProjectPath();
@@ -11446,7 +11656,7 @@ function readResourceText(uri, getProjectPath) {
11446
11656
  const parsedUri = parseGodotUri(uri);
11447
11657
  if (parsedUri.kind === "project-info") {
11448
11658
  const projectFilePath = resolveProjectFile(projectPath, "project.godot");
11449
- const rawProject = readFileSync10(projectFilePath, "utf-8");
11659
+ const rawProject = readFileSync11(projectFilePath, "utf-8");
11450
11660
  const parsedProject = parseProjectGodot(rawProject);
11451
11661
  return {
11452
11662
  mimeType: "application/json",
@@ -11455,7 +11665,7 @@ function readResourceText(uri, getProjectPath) {
11455
11665
  }
11456
11666
  const filePath = resolveProjectFile(projectPath, parsedUri.resourcePath);
11457
11667
  ensureAllowedExtension(parsedUri.kind, filePath);
11458
- const text = readFileSync10(filePath, "utf-8");
11668
+ const text = readFileSync11(filePath, "utf-8");
11459
11669
  return {
11460
11670
  mimeType: parsedUri.kind === "script" ? "text/x-gdscript" : "text/plain",
11461
11671
  text
@@ -11596,19 +11806,19 @@ function readParams(params, key) {
11596
11806
  }
11597
11807
 
11598
11808
  // src/setup.ts
11599
- import { createHash as createHash3 } from "node:crypto";
11809
+ import { createHash as createHash4 } from "node:crypto";
11600
11810
  import {
11601
11811
  cpSync,
11602
- existsSync as existsSync13,
11812
+ existsSync as existsSync14,
11603
11813
  mkdirSync as mkdirSync4,
11604
- readdirSync as readdirSync8,
11605
- readFileSync as readFileSync11,
11814
+ readdirSync as readdirSync9,
11815
+ readFileSync as readFileSync12,
11606
11816
  rmdirSync,
11607
11817
  rmSync as rmSync5,
11608
- statSync as statSync7,
11818
+ statSync as statSync8,
11609
11819
  writeFileSync as writeFileSync6
11610
11820
  } from "node:fs";
11611
- import { dirname as dirname8, join as join15 } from "node:path";
11821
+ import { dirname as dirname8, join as join16 } from "node:path";
11612
11822
  import { fileURLToPath } from "node:url";
11613
11823
  function editorAddonDigest(from = shippedAddonsDirectory()) {
11614
11824
  return addonsDigest(EDITOR_PLUGINS, from);
@@ -11617,27 +11827,27 @@ function runtimeAddonDigest(from = shippedAddonsDirectory()) {
11617
11827
  return addonsDigest([RUNTIME_ADDON], from);
11618
11828
  }
11619
11829
  function addonsDigest(names, from) {
11620
- const hash = createHash3("sha256");
11830
+ const hash = createHash4("sha256");
11621
11831
  const walk = (directory, relative) => {
11622
- for (const name of readdirSync8(directory).sort()) {
11832
+ for (const name of readdirSync9(directory).sort()) {
11623
11833
  if (name === VERSION_MARKER || name === DIGEST_MARKER) {
11624
11834
  continue;
11625
11835
  }
11626
- const path = join15(directory, name);
11836
+ const path = join16(directory, name);
11627
11837
  const inside = `${relative}/${name}`;
11628
- if (statSync7(path).isDirectory()) {
11838
+ if (statSync8(path).isDirectory()) {
11629
11839
  walk(path, inside);
11630
11840
  } else {
11631
11841
  hash.update(`${inside}
11632
11842
  `);
11633
- hash.update(readFileSync11(path));
11843
+ hash.update(readFileSync12(path));
11634
11844
  hash.update(`
11635
11845
  `);
11636
11846
  }
11637
11847
  }
11638
11848
  };
11639
11849
  for (const name of names) {
11640
- walk(join15(from, name), name);
11850
+ walk(join16(from, name), name);
11641
11851
  }
11642
11852
  return hash.digest("hex");
11643
11853
  }
@@ -11656,28 +11866,28 @@ function installedRuntimeDigest(projectPath) {
11656
11866
  return digestMarker(projectPath, RUNTIME_ADDON);
11657
11867
  }
11658
11868
  function digestMarker(projectPath, addon) {
11659
- const marker = join15(projectPath, "addons", addon, DIGEST_MARKER);
11660
- if (!existsSync13(marker)) {
11869
+ const marker = join16(projectPath, "addons", addon, DIGEST_MARKER);
11870
+ if (!existsSync14(marker)) {
11661
11871
  return null;
11662
11872
  }
11663
- const said = readFileSync11(marker, "utf8").trim();
11873
+ const said = readFileSync12(marker, "utf8").trim();
11664
11874
  return said === "" ? null : said;
11665
11875
  }
11666
11876
  function shippedAddonsDirectory() {
11667
- return join15(dirname8(fileURLToPath(import.meta.url)), "godot", "addons");
11877
+ return join16(dirname8(fileURLToPath(import.meta.url)), "godot", "addons");
11668
11878
  }
11669
11879
  function shippedOperationsScript() {
11670
- return join15(dirname8(fileURLToPath(import.meta.url)), "godot", "operations", "godot_operations.gd");
11880
+ return join16(dirname8(fileURLToPath(import.meta.url)), "godot", "operations", "godot_operations.gd");
11671
11881
  }
11672
11882
  function shippedListedRunner() {
11673
- return join15(dirname8(fileURLToPath(import.meta.url)), "godot", "tests", "listed.gd");
11883
+ return join16(dirname8(fileURLToPath(import.meta.url)), "godot", "tests", "listed.gd");
11674
11884
  }
11675
11885
  function gdUnit4Unusable(projectPath) {
11676
11886
  const marker = "addons/gdUnit4/plugin.cfg";
11677
- if (!existsSync13(join15(projectPath, ...marker.split("/")))) {
11887
+ if (!existsSync14(join16(projectPath, ...marker.split("/")))) {
11678
11888
  return { missing: true, reason: `gdUnit4 is not installed in this project: no ${marker}` };
11679
11889
  }
11680
- if (!existsSync13(join15(projectPath, ...GDUNIT4_CI_RUNNER.split("/")))) {
11890
+ if (!existsSync14(join16(projectPath, ...GDUNIT4_CI_RUNNER.split("/")))) {
11681
11891
  return {
11682
11892
  missing: false,
11683
11893
  reason: `gdUnit4 in this project is older than 6.0, which the shipped runner needs: no ${GDUNIT4_CI_RUNNER}`
@@ -11688,44 +11898,44 @@ function gdUnit4Unusable(projectPath) {
11688
11898
  function installAddons(projectPath, from = shippedAddonsDirectory()) {
11689
11899
  const installed = [];
11690
11900
  for (const name of ADDONS) {
11691
- const source = join15(from, name);
11692
- if (!existsSync13(join15(source, name === RUNTIME_ADDON ? "runtime_autoload.gd" : "plugin.cfg"))) {
11901
+ const source = join16(from, name);
11902
+ if (!existsSync14(join16(source, name === RUNTIME_ADDON ? "runtime_autoload.gd" : "plugin.cfg"))) {
11693
11903
  throw new Error(`The package holds no ${name} addon at ${source}.`);
11694
11904
  }
11695
- const target = join15(projectPath, "addons", name);
11696
- const replaced = existsSync13(target);
11905
+ const target = join16(projectPath, "addons", name);
11906
+ const replaced = existsSync14(target);
11697
11907
  rmSync5(target, { recursive: true, force: true });
11698
11908
  mkdirSync4(dirname8(target), { recursive: true });
11699
11909
  cpSync(source, target, { recursive: true });
11700
- writeFileSync6(join15(target, VERSION_MARKER), `${SERVER_VERSION}
11910
+ writeFileSync6(join16(target, VERSION_MARKER), `${SERVER_VERSION}
11701
11911
  `);
11702
11912
  installed.push({ name, path: target, replaced });
11703
11913
  }
11704
- writeFileSync6(join15(projectPath, "addons", ADDONS[0], DIGEST_MARKER), `${editorAddonDigest(from)}
11914
+ writeFileSync6(join16(projectPath, "addons", ADDONS[0], DIGEST_MARKER), `${editorAddonDigest(from)}
11705
11915
  `);
11706
- writeFileSync6(join15(projectPath, "addons", RUNTIME_ADDON, DIGEST_MARKER), `${runtimeAddonDigest(from)}
11916
+ writeFileSync6(join16(projectPath, "addons", RUNTIME_ADDON, DIGEST_MARKER), `${runtimeAddonDigest(from)}
11707
11917
  `);
11708
11918
  return installed;
11709
11919
  }
11710
11920
  function installedAddonVersion(projectPath) {
11711
- const marker = join15(projectPath, "addons", ADDONS[0], VERSION_MARKER);
11712
- if (!existsSync13(marker)) {
11921
+ const marker = join16(projectPath, "addons", ADDONS[0], VERSION_MARKER);
11922
+ if (!existsSync14(marker)) {
11713
11923
  return null;
11714
11924
  }
11715
- const said = readFileSync11(marker, "utf8").trim();
11925
+ const said = readFileSync12(marker, "utf8").trim();
11716
11926
  return said === "" ? null : said;
11717
11927
  }
11718
11928
  function removeAddons(projectPath) {
11719
11929
  const removed = [];
11720
11930
  for (const name of ADDONS) {
11721
- const target = join15(projectPath, "addons", name);
11722
- if (existsSync13(target)) {
11931
+ const target = join16(projectPath, "addons", name);
11932
+ if (existsSync14(target)) {
11723
11933
  rmSync5(target, { recursive: true, force: true });
11724
11934
  removed.push(target);
11725
11935
  }
11726
11936
  }
11727
- const addons = join15(projectPath, "addons");
11728
- if (removed.length > 0 && existsSync13(addons) && readdirSync8(addons).length === 0) {
11937
+ const addons = join16(projectPath, "addons");
11938
+ if (removed.length > 0 && existsSync14(addons) && readdirSync9(addons).length === 0) {
11729
11939
  rmdirSync(addons);
11730
11940
  removed.push(addons);
11731
11941
  }
@@ -11759,7 +11969,7 @@ function loaderAmong(projectPath, named) {
11759
11969
  }
11760
11970
  let source = null;
11761
11971
  try {
11762
- source = readFileSync11(join15(projectPath, path), "utf8");
11972
+ source = readFileSync12(join16(projectPath, path), "utf8");
11763
11973
  } catch {}
11764
11974
  if (source?.includes(RUNTIME_AUTOLOAD.path)) {
11765
11975
  return { name: entry, path, exists: true };
@@ -11801,10 +12011,10 @@ function enabledPlugins(settings) {
11801
12011
  function inspectProject(projectPath) {
11802
12012
  const problems = [];
11803
12013
  const addons = ADDONS.map((name) => {
11804
- const directory = join15(projectPath, "addons", name);
11805
- const marker = join15(directory, VERSION_MARKER);
11806
- const installed = existsSync13(directory);
11807
- const version = existsSync13(marker) ? readFileSync11(marker, "utf8").trim() : null;
12014
+ const directory = join16(projectPath, "addons", name);
12015
+ const marker = join16(directory, VERSION_MARKER);
12016
+ const installed = existsSync14(directory);
12017
+ const version = existsSync14(marker) ? readFileSync12(marker, "utf8").trim() : null;
11808
12018
  const current = version === SERVER_VERSION;
11809
12019
  if (!installed) {
11810
12020
  problems.push(`addons/${name} is not installed; run gdharness setup`);
@@ -11813,7 +12023,7 @@ function inspectProject(projectPath) {
11813
12023
  }
11814
12024
  return { name, installed, version, current };
11815
12025
  });
11816
- const settings = parseProjectGodot(readFileSync11(join15(projectPath, "project.godot"), "utf8"));
12026
+ const settings = parseProjectGodot(readFileSync12(join16(projectPath, "project.godot"), "utf8"));
11817
12027
  const pluginsEnabled = enabledPlugins(settings);
11818
12028
  for (const name of EDITOR_PLUGINS) {
11819
12029
  if (!pluginsEnabled.includes(name)) {
@@ -11829,7 +12039,7 @@ function inspectProject(projectPath) {
11829
12039
  const tracked = trackedByGit(projectPath, [...named.values()]);
11830
12040
  const addonMissing = (path) => addons.some((addon) => !addon.installed && path.startsWith(`addons/${addon.name}/`));
11831
12041
  for (const [name, path] of named) {
11832
- if (!existsSync13(join15(projectPath, path))) {
12042
+ if (!existsSync14(join16(projectPath, path))) {
11833
12043
  if (!addonMissing(path)) {
11834
12044
  problems.push(`the ${name} autoload names res://${path}, which is not in this project, so it boots with a missing script; restore that file, or point the autoload at one that is here`);
11835
12045
  }
@@ -11982,6 +12192,7 @@ var init_tool_definitions = __esm(() => {
11982
12192
  init_launch();
11983
12193
  PROJECT_INFO_SECTIONS = [
11984
12194
  "autoloads",
12195
+ "classes",
11985
12196
  "input_actions",
11986
12197
  "plugins",
11987
12198
  "export_presets",
@@ -12149,13 +12360,13 @@ var init_tool_definitions = __esm(() => {
12149
12360
  name: "project_info",
12150
12361
  title: "Project info",
12151
12362
  effect: "read",
12152
- description: "What a project is: its name and main scene from project.godot, the Godot that answers, and how many scenes, scripts and assets it holds, other counting every remaining file, leaving out the addons gdharness installed and any folder holding a .gdignore, with optional sections on top.",
12363
+ description: "What a project is: its name and main scene from project.godot, the Godot that answers, and how many scenes, scripts and assets it holds, other counting every remaining file, leaving out the .import and .uid files the engine writes beside others, the addons gdharness installed and any folder holding a .gdignore, with optional sections on top.",
12153
12364
  parameters: {
12154
12365
  projectPath: PROJECT_PATH,
12155
12366
  include: {
12156
12367
  type: "array",
12157
12368
  items: { type: "string", enum: [...PROJECT_INFO_SECTIONS] },
12158
- description: "Extra sections: registered autoloads, the input actions project.godot holds with their events (an engine ui_ action only when the project rebinds it, as engineAction), addons and whether each is enabled, export presets, the audio bus layout, a health report, or export validation. health and validation compile every script of the project in headless engines and name each that fails with its first error, and each file a scene, resource or script refers to that does not exist."
12369
+ description: "Extra sections: registered autoloads, the classes the project's scripts declare with class_name, each with its script, read from the files, the input actions project.godot holds with their events (an engine ui_ action only when the project rebinds it, as engineAction), addons and whether each is enabled, export presets, the audio bus layout, a health report, or export validation. health and validation compile every script of the project in headless engines and name each that fails with its first error, and each file a scene, resource or script refers to that does not exist."
12159
12370
  },
12160
12371
  preset: { type: "string", description: "For validation: the export preset to validate against." },
12161
12372
  detail: {
@@ -12300,7 +12511,7 @@ var init_tool_definitions = __esm(() => {
12300
12511
  name: "project_search",
12301
12512
  title: "Search the project",
12302
12513
  effect: "read",
12303
- 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. The addons gdharness installed are not searched either, since nobody on the project wrote them.",
12514
+ 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, which not_searched names when the search passed one. 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. The addons gdharness installed are not searched either, since nobody on the project wrote them.",
12304
12515
  parameters: {
12305
12516
  projectPath: PROJECT_PATH,
12306
12517
  query: { type: "string", description: "The text or pattern to find." },
@@ -12326,7 +12537,7 @@ var init_tool_definitions = __esm(() => {
12326
12537
  direction: {
12327
12538
  type: "string",
12328
12539
  enum: ["forward", "reverse"],
12329
- description: "forward: what this resource loads, with cycles reported, a script's dependencies including every script whose class_name it uses, marked by_class with the name. summary.total_resources is the file asked about, so 1; total_dependencies counts each file reached, directly or through another, once however many places load it; total_references counts the entries in the tree, and missing names every file referenced that does not exist, with the files referring to it. reverse: every file that refers to it and how, for a file that is gone as well while anything still refers to it, answered with exists: false (a missing file nothing refers to is refused, naming the nearest files), a scene instancing it, a script extending, preloading or loading it, and for a script with a class_name every use of that name. Each reference carries a kind: ext_resource, extends, preload, load or path for a reference by path; setting, plugin, export_preset and import for one in project.godot, a plugin.cfg, export_presets.cfg or an import sidecar; class_name for a use of the declared name, the declaration itself being the script asked about. Two kinds are mentions rather than uses: doc is a name inside a ## documentation comment, which renaming would break in the generated docs, and comment is one in ordinary prose, which it would only make stale. summary.in_code is the count with both of those left out, which is the number to read when the question is whether anything still uses this. Default forward."
12540
+ description: "forward: what this resource loads, with cycles reported, a script's dependencies including every script whose class_name it uses, marked by_class with the name, and the script of every autoload it names, marked by_autoload with the autoload's name. summary.total_resources is the file asked about, so 1; total_dependencies counts each file reached, directly or through another, once however many places load it; total_references counts the entries in the tree, and missing names every file referenced that does not exist, with the files referring to it. reverse: every file that refers to it and how, for a file that is gone as well while anything still refers to it, answered with exists: false (a missing file nothing refers to is refused, naming the nearest files), a scene instancing it, a script extending, preloading or loading it, and for a script with a class_name every use of that name, and for an autoload's script every use of the autoload's name, with autoload naming it. Each reference carries a kind: ext_resource, extends, preload, load or path for a reference by path; setting, plugin, export_preset and import for one in project.godot, a plugin.cfg, export_presets.cfg or an import sidecar; class_name for a use of the declared name, the declaration itself being the script asked about; autoload for code naming the autoload. Two kinds are mentions rather than uses: doc is a name inside a ## documentation comment, which renaming would break in the generated docs, and comment is one in ordinary prose, which it would only make stale. summary.in_code is the count with both of those left out, which is the number to read when the question is whether anything still uses this. Default forward."
12330
12541
  },
12331
12542
  depth: { type: "number", description: "forward: how many levels to follow. Default unlimited." },
12332
12543
  includeBuiltin: {
@@ -12356,7 +12567,7 @@ var init_tool_definitions = __esm(() => {
12356
12567
  options: {
12357
12568
  type: "object",
12358
12569
  ops: ["set_options"],
12359
- description: `set_options: import options keyed as the .import file spells them, {"compress/mode": 1}. A dictionary option whose keys are numbers, a font's opentype_features or any option already holding number keys, takes them written as strings of digits, and opentype_features also takes an OpenType tag by its four-letter name: {"opentype_features": {"liga": 0}} turns ligatures off. Both are written as the numbers the engine reads. One already holding the value given is answered under unchanged_options rather than updated_options, and when none changed nothing is written or reimported.`,
12570
+ description: `set_options: import options keyed as the .import file spells them, {"compress/mode": 1}. A dictionary option whose keys are numbers, a font's opentype_features or any option already holding number keys, takes them written as strings of digits, and opentype_features also takes an OpenType tag by its four-letter name: {"opentype_features": {"liga": 0}} turns ligatures off. Both are written as the numbers the engine reads. One already holding the value given is answered under unchanged_options rather than updated_options, and when none changed nothing is written or reimported. changed gives each option written as {was, now}, was null for one the import file did not hold.`,
12360
12571
  additionalProperties: true
12361
12572
  },
12362
12573
  reimport: {
@@ -12414,7 +12625,7 @@ var init_tool_definitions = __esm(() => {
12414
12625
  {
12415
12626
  name: "project_export",
12416
12627
  title: "Export",
12417
- description: "Export presets and exports. add_preset writes a preset to export_presets.cfg with the keys the editor's Project > Export writes for a new one, every platform option left at its default, and makes it the platform's runnable preset when the platform has none. It answers where this engine's export templates belong and whether the preset's platform has them, since an export needs them; list says the same for every preset. An editor open on the project does not see a preset added this way until it restarts. run names, as alsoWritten, what else the export wrote beside its output, such as the .pck a Windows or Linux build cannot start without.",
12628
+ description: "Export presets and exports. add_preset writes a preset to export_presets.cfg with the keys the editor's Project > Export writes for a new one, every platform option left at its default, and makes it the platform's runnable preset when the platform has none. It answers where this engine's export templates belong and whether the preset's platform has them, since an export needs them; list says the same for every preset. An editor open on the project does not see a preset added this way until it restarts. run names, as alsoWritten, what else the export wrote beside its output, such as the .pck a Windows or Linux build cannot start without. Its entries are the errors and warnings the export printed, the newest two hundred distinct ones, one printed more than once carrying times, and errors and warnings count them; exported is true only when this run wrote the output and printed no error.",
12418
12629
  parameters: {
12419
12630
  projectPath: PROJECT_PATH,
12420
12631
  preset: {
@@ -12451,7 +12662,7 @@ var init_tool_definitions = __esm(() => {
12451
12662
  title: "Run tests",
12452
12663
  effect: "additive",
12453
12664
  description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Each entry under failed carries its first report as message and detail, and, when it has more than one, its reports under reports in the order the case met them: gdUnit4 goes on past a failed assertion, and a case can fail several and then stop on a runtime error, which makes its status error. Identical reports fold into one with times, at most twenty different ones are kept (reportsLeftOut counts the rest), and past about a megabyte the largest cases keep only their first message (failedWithoutDetail). failures counts the cases that failed an assertion and errors the cases that hit an error, so such a case counts in both. Suites where everything passed are counted, not listed, and an engine message keeps the frames above gdUnit4, not the twenty inside it; identical engine messages come back once under engineEntries, with times saying how often they were printed. The class list is rebuilt first, so a suite written a moment ago is found, and the rebuild's answer is under classes, with the classes each addon declares counted under addedInAddons and changedInAddons rather than named: a class it puts back that the cache had after the last rebuild is named under classes.lostSinceLastRebuild, which is the editor writing its shorter list over the file on every save until restarted. What changed on disk since it was imported is then imported before any suite runs, and named under importedFirst. On Windows and Linux the run gets a user:// of its own, away from a player's saves; on macOS the engine reads user:// off HOME, so it writes where the player keeps theirs, and savesNote says so. A run that found nothing to run is never called a pass, and names the path it looked in. A script that does not load stops gdUnit4 before any suite runs: its errors come back in full under scriptErrors, and the scripts that failed only because one they depend on did are listed by path under failedThroughDependency. When they failed on a class whose own script has a parser error, which gdUnit4 never names, that script is checked by the engine and its error comes first under scriptErrors, with the class it declares under declares, and the errors a script has only because that class did not resolve, which the engine reads as Variant, are kept apart under followFromUnresolved. An autoload that does not compile stops the run at once, its errors listed the same way. A client sending a progress token is told as each suite finishes, and at least every twenty seconds. A run that ends without a report, at its timeout or otherwise, names the suites it finished under suitesFinished, the failing ones with their failed cases under suitesFailing, and the one it was in under suiteRunning. When a client cuts a run off, that is kept in the project and the next project_test there carries it under previousRunCut. A run where nothing failed but nodes were left in the tree comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. A run whose every case passed is still not called passed when it printed a script error that no case failed on: gdUnit4 fails a case on the script errors its logger sees, and it cannot see a stack overflow, so the cases pass on whatever the faulted calls returned. Those errors come back under uncaughtScriptErrors, and under engineEntries by index with detailUnder rather than with their backtrace a second time; a backtrace that repeats, as a recursion's does, has each repeated frame or cycle of frames written once with the frames it covers and how many times. One raised inside assert_error(...).is_runtime_error is the test's own and is not listed there, and none are when the project turns off gdUnit4's report/godot/script_error setting; a suite declares them all the same. Needs gdUnit4 under addons/gdUnit4.",
12454
- answer: "time is gdUnit4's own sum of the seconds its cases took, from its report. tookMs is the whole call in milliseconds, from the class rebuild to the engine's exit, which is what a caller waits for. The run is judged as gdharness test judges it: undeclaredErrors names each engine error that no suite declared in its GDHARNESS_EXPECTED_ERRORS constant, with the suite and the case that printed it, and declaredNotPrinted each declared error its case did not print. Either fails the run, and each suite holding one is listed with its count. engineErrors counts every engine error the run printed but those a suite declared. " + IMPORTED_FIRST,
12665
+ answer: "passed is the verdict to read. exitCode is the engine's own exit, gdUnit4's code for what it saw, and is 0 beside passed false when the run is failed for something gdUnit4 does not judge, such as an engine error no suite declared. time is gdUnit4's own sum of the seconds its cases took, from its report. tookMs is the whole call in milliseconds, from the class rebuild to the engine's exit, which is what a caller waits for. The run is judged as gdharness test judges it: undeclaredErrors names each engine error that no suite declared in its GDHARNESS_EXPECTED_ERRORS constant, with the suite and the case that printed it, and declaredNotPrinted each declared error its case did not print. Either fails the run, and each suite holding one is listed with its count. engineErrors counts every engine error the run printed but those a suite declared. " + IMPORTED_FIRST,
12455
12666
  parameters: {
12456
12667
  projectPath: PROJECT_PATH,
12457
12668
  path: {
@@ -12462,7 +12673,7 @@ var init_tool_definitions = __esm(() => {
12462
12673
  ignore: {
12463
12674
  type: "array",
12464
12675
  items: { type: "string" },
12465
- description: 'Suites or cases to leave out, as "suite_name" or "suite_name:test_name". A bare test name is read as a suite name and leaves out nothing; an entry that named a case which ran all the same comes back under ignoreNotApplied with the form that would have left it out.'
12676
+ description: 'Suites or cases to leave out, as "suite_name" or "suite_name:test_name". A bare test name is read as a suite name and leaves out nothing; an entry that named a case which ran all the same comes back under ignoreNotApplied with the form that would have left it out. Every case the list left out comes back under ignored as suite_name:test_name, so a run called passed says what it did not run, and an entry naming no suite or case under path comes back under ignoreUnmatched.'
12466
12677
  },
12467
12678
  tests: {
12468
12679
  type: "array",
@@ -12906,7 +13117,7 @@ var init_tool_definitions = __esm(() => {
12906
13117
  operations: {
12907
13118
  structure: {
12908
13119
  effect: "read",
12909
- summary: "functions, variables (static ones marked is_static; one written with := has the type of a literal or built-in constructor it is given, marked inferred, as a constant always does), signals, constants, enums, inner_classes (each with its name, line and extends, and its own members in these same fields), class_name, extends and line_count, read from the file, and dependencies: the paths it loads or preloads as written, then the scripts declaring each project class_name its code uses, which classes_used names with the class; autoloads_used names each autoload the code uses by its global name, with the autoload's script, which the project loads rather than this script, so it is not under dependencies",
13120
+ summary: "functions, variables (static ones marked is_static; one written with := has the type of a literal or built-in constructor it is given, marked inferred, as a constant always does), signals, constants, enums, inner_classes (each with its name, line and extends, and its own members in these same fields), class_name, extends and line_count, read from the file, and dependencies: the paths it loads or preloads as written, then the scripts declaring each project class_name its code uses, which classes_used names with the class; autoloads_used names each autoload the code uses by its global name, with the autoload's script, which the project loads rather than this script, so it is not under dependencies. A member or inner class with a ## documentation comment directly above it carries the comment's text as doc, and the script's own, the ## block above class_name or extends or one followed by a blank line before any member, comes back as class_doc",
12910
13121
  requires: []
12911
13122
  },
12912
13123
  symbols: {
@@ -13615,7 +13826,7 @@ var init_tool_definitions = __esm(() => {
13615
13826
  name: "runtime_input",
13616
13827
  title: "Send input to the game",
13617
13828
  description: "Input to the running game: a whole click on a Control or a 3D node named by path or on a control named by the words on it, an item chosen out of a menu, typing into whatever has the focus, or a raw action, key, mouse button, mouse motion or drag. All of it works headless, where the viewport is the project's window size and the GUI only takes what is inside it. On gdharness's own desktop, where a windowed run goes, Windows refuses to say where the pointer is: every pointer answer there carries pointer_on_hidden_desktop, and the first of the run pointer_note saying why it matters: get_mouse_position() on the root window, and the mouse positions derived from it, read a meaningless position, while what reads the event itself (the control under it, hover, tooltips, input handlers) gets the injected position. A native window no real pointer has entered gets no hover from Godot, so the runtime supplies it there and the answers say hover_supplied.",
13618
- answer: `click: what was under the pointer, and what became of the control: in_tree, removed or freed. Named by says, found says which match it was, its index being the index argument that presses the same match again, found.item names the tab, list entry, tree row or menu bar title pressed (kind; index, the item's own number in its control, which is not found.index; says; and for a row the column of the cell saying the words), and found.pressedOn says "the words" for words in a RichTextLabel pressed where they are drawn, or "the middle of the label" with a note saying why they could not be placed; on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control scrolled into view first is scrolled each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A button in toggle mode, a CheckBox or CheckButton included, answers pressed with before and after, its button_pressed either side of the click: the same value twice is a click the game undid, and after is null when the click freed it. A refusal for a pointer turned off names where mouse_behavior_recursive was set. On a 3D node landed says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room. choose: what was chosen, read before the press, which is heard rather than assumed; what became of the control named (in_tree, removed or freed, since a game can rebuild its screen on the pick); and what the button in front of the menu shows now when it is still there. A menu is opened rather than clicked because its items are drawn rather than built, and the item is looked for as the menu stands once open, since a game can fill a menu as it opens. action, key, mouse_click and joypad_button: the event as it was sent, with whole saying whether the press was let go of again and pressed whether it is left held down (true only after pressed true), for a key the keycode, physical_keycode and unicode it carried, and for a controller button its number, its name and the device. mouse_motion: the position and the relative movement sent. mouse_drag: from, to, motions sent and relative_total. joypad_motion: the axis by number and name, the value and the device. None of these is aimed at anything, so none says what the event reached: click on a nodePath or says answers what was under the pointer. A mouse_click, mouse_motion or mouse_drag with a point outside the window's viewport says so under outside_viewport, with the viewport's rectangle, since no control there receives it.`,
13829
+ answer: `click: what was under the pointer, and what became of the control: in_tree, removed or freed. Named by says, found says which match it was, its index being the index argument that presses the same match again, found.item names the tab, list entry, tree row or menu bar title pressed (kind; index, the item's own number in its control, which is not found.index; says; and for a row the column of the cell saying the words), and found.pressedOn says "the words" for words in a RichTextLabel pressed where they are drawn, or "the middle of the label" with a note saying why they could not be placed; on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control scrolled into view first is scrolled each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A button in toggle mode, a CheckBox or CheckButton included, answers pressed with before and after, its button_pressed either side of the click: the same value twice is a click the game undid, and after is null when the click freed it. A refusal for a pointer turned off names where mouse_behavior_recursive was set. On a 3D node landed says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room. choose: what was chosen, read before the press, which is heard rather than assumed; what became of the control named (in_tree, removed or freed, since a game can rebuild its screen on the pick); and what the button in front of the menu shows now when it is still there. A menu is opened rather than clicked because its items are drawn rather than built, and the item is looked for as the menu stands once open, since a game can fill a menu as it opens. action, key, mouse_click and joypad_button: the event as it was sent, with whole saying whether the press was let go of again and pressed whether it is left held down (true only after pressed true), for a key the keycode, physical_keycode and unicode it carried, and for a controller button its number, its name and the device. mouse_motion: the position and the relative movement sent. mouse_drag: from, to, motions sent and relative_total. joypad_motion: the axis by number and name, the value and the device. None of these is aimed at anything, so none says what the event reached: click on a nodePath or says answers what was under the pointer. An action or a key answers focused with the path of the Control holding the keyboard focus when it went in, since that Control gets it before the game's _unhandled_input: Space or Enter on a focused button presses the button. A mouse_click, mouse_motion or mouse_drag with a point outside the window's viewport says so under outside_viewport, with the viewport's rectangle, since no control there receives it.`,
13619
13830
  parameters: {
13620
13831
  projectPath: RUNNING_PROJECT_PATH,
13621
13832
  pid: RUNNING_PID,
@@ -14051,9 +14262,9 @@ function whatItCanChange(tool) {
14051
14262
  if (destroying.length === 0) {
14052
14263
  return "Adds or starts things, and overwrites and removes nothing itself.";
14053
14264
  }
14054
- return `Can overwrite, remove or end what is there, through ${listed(destroying)}.`;
14265
+ return `Can overwrite, remove or end what is there, through ${listed2(destroying)}.`;
14055
14266
  }
14056
- function listed(items) {
14267
+ function listed2(items) {
14057
14268
  const last = items.at(-1) ?? "";
14058
14269
  return items.length === 1 ? last : `${items.slice(0, -1).join(", ")} and ${last}`;
14059
14270
  }
@@ -14111,30 +14322,30 @@ var init_tool_reference = __esm(() => {
14111
14322
  });
14112
14323
 
14113
14324
  // src/desktop.ts
14114
- import { createHash as createHash4 } from "node:crypto";
14325
+ import { createHash as createHash5 } from "node:crypto";
14115
14326
  import {
14116
14327
  closeSync,
14117
- existsSync as existsSync15,
14328
+ existsSync as existsSync16,
14118
14329
  mkdtempSync as mkdtempSync2,
14119
14330
  openSync,
14120
- readFileSync as readFileSync12,
14331
+ readFileSync as readFileSync13,
14121
14332
  rmSync as rmSync7,
14122
- statSync as statSync8,
14333
+ statSync as statSync9,
14123
14334
  writeFileSync as writeFileSync8,
14124
14335
  writeSync
14125
14336
  } from "node:fs";
14126
14337
  import { tmpdir as tmpdir4 } from "node:os";
14127
- import { join as join17 } from "node:path";
14338
+ import { join as join18 } from "node:path";
14128
14339
  import { setTimeout as delay } from "node:timers/promises";
14129
14340
  function helperAssembly(directory = tmpdir4()) {
14130
- return join17(directory, `gdharness-desktop-${DIGEST}.dll`);
14341
+ return join18(directory, `gdharness-desktop-${DIGEST}.dll`);
14131
14342
  }
14132
14343
  async function ensureHelperCompiled(directory = tmpdir4(), budgetMs = HELPER_COMPILE_MS, wait = true) {
14133
14344
  const assembly = helperAssembly(directory);
14134
14345
  const lock = `${assembly}.compiling`;
14135
14346
  const until = Date.now() + budgetMs;
14136
14347
  while (Date.now() < until) {
14137
- if (existsSync15(assembly)) {
14348
+ if (existsSync16(assembly)) {
14138
14349
  return null;
14139
14350
  }
14140
14351
  let mine = false;
@@ -14152,8 +14363,8 @@ async function ensureHelperCompiled(directory = tmpdir4(), budgetMs = HELPER_COM
14152
14363
  }
14153
14364
  }
14154
14365
  try {
14155
- const holder = Number(readFileSync12(lock, "utf8").trim());
14156
- const old = Date.now() - statSync8(lock).mtimeMs > budgetMs;
14366
+ const holder = Number(readFileSync13(lock, "utf8").trim());
14367
+ const old = Date.now() - statSync9(lock).mtimeMs > budgetMs;
14157
14368
  if (old || Number.isInteger(holder) && holder > 0 && !alive(holder)) {
14158
14369
  rmSync7(lock, { force: true });
14159
14370
  continue;
@@ -14167,8 +14378,8 @@ async function ensureHelperCompiled(directory = tmpdir4(), budgetMs = HELPER_COM
14167
14378
  return `the desktop helper was not compiled within ${String(budgetMs)} ms`;
14168
14379
  }
14169
14380
  async function compileHelper(directory, assembly, budgetMs) {
14170
- const scratch = mkdtempSync2(join17(directory, "gdharness-desktop-source-"));
14171
- const source = join17(scratch, "helper.cs");
14381
+ const scratch = mkdtempSync2(join18(directory, "gdharness-desktop-source-"));
14382
+ const source = join18(scratch, "helper.cs");
14172
14383
  writeFileSync8(source, SOURCE, "utf8");
14173
14384
  const literal = (path) => `'${path.replaceAll("'", "''")}'`;
14174
14385
  const script = [
@@ -14206,7 +14417,7 @@ async function compileHelper(directory, assembly, budgetMs) {
14206
14417
  });
14207
14418
  compiler.once("close", (code) => {
14208
14419
  clearTimeout(timer);
14209
- resolve(existsSync15(assembly) ? null : `Windows PowerShell could not compile it${code === null ? " in time" : ""}: ${complained.trim() || `exit code ${String(code)}`}`);
14420
+ resolve(existsSync16(assembly) ? null : `Windows PowerShell could not compile it${code === null ? " in time" : ""}: ${complained.trim() || `exit code ${String(code)}`}`);
14210
14421
  });
14211
14422
  });
14212
14423
  } finally {
@@ -14431,11 +14642,11 @@ public static class GdharnessDesktop {
14431
14642
  var init_desktop = __esm(() => {
14432
14643
  init_alive();
14433
14644
  init_spawn();
14434
- DIGEST = createHash4("sha256").update(SOURCE).digest("hex").slice(0, 16);
14645
+ DIGEST = createHash5("sha256").update(SOURCE).digest("hex").slice(0, 16);
14435
14646
  });
14436
14647
 
14437
14648
  // src/suite-judge.ts
14438
- import { readFileSync as readFileSync15 } from "node:fs";
14649
+ import { readFileSync as readFileSync16 } from "node:fs";
14439
14650
  import { stripVTControlCharacters } from "node:util";
14440
14651
  function secondsOf(time) {
14441
14652
  let ms = 0;
@@ -14542,7 +14753,7 @@ function readRun(printed) {
14542
14753
  }
14543
14754
  function readExpected(file) {
14544
14755
  try {
14545
- const parsed = JSON.parse(readFileSync15(file, "utf8"));
14756
+ const parsed = JSON.parse(readFileSync16(file, "utf8"));
14546
14757
  return typeof parsed === "object" && parsed !== null ? parsed : {};
14547
14758
  } catch {
14548
14759
  return {};
@@ -14776,7 +14987,7 @@ var init_whole_file = __esm(() => {
14776
14987
  });
14777
14988
 
14778
14989
  // src/suite-plan.ts
14779
- import { existsSync as existsSync17, mkdirSync as mkdirSync7, readFileSync as readFileSync16 } from "node:fs";
14990
+ import { existsSync as existsSync18, mkdirSync as mkdirSync7, readFileSync as readFileSync17 } from "node:fs";
14780
14991
  import { dirname as dirname10, posix as posix3 } from "node:path";
14781
14992
 
14782
14993
  class ProjectScripts {
@@ -14865,7 +15076,7 @@ class ProjectScripts {
14865
15076
  }
14866
15077
  function readTimes(file) {
14867
15078
  try {
14868
- const parsed = JSON.parse(readFileSync16(file, "utf8"));
15079
+ const parsed = JSON.parse(readFileSync17(file, "utf8"));
14869
15080
  return typeof parsed === "object" && parsed !== null ? parsed : {};
14870
15081
  } catch {
14871
15082
  return {};
@@ -14946,19 +15157,27 @@ function unitsOf(suites, times, splitPast = SPLIT_PAST_SECONDS) {
14946
15157
  function dealt(units, declared, most) {
14947
15158
  const windowed = units.filter((unit) => unit.windowed);
14948
15159
  const headless = units.filter((unit) => !unit.windowed).sort((a, b) => b.cost - a.cost || a.suite.localeCompare(b.suite) || (a.cases[0] ?? "").localeCompare(b.cases[0] ?? ""));
14949
- const total = headless.reduce((sum, unit) => sum + unit.cost, 0);
14950
- const slowest = headless[0]?.cost ?? 0;
14951
- const room = Math.max(1, most - (windowed.length > 0 && most > 1 ? 1 : 0));
14952
- const count = headless.length === 0 ? 0 : Math.max(1, Math.min(room, headless.length, Math.ceil(total / Math.max(slowest, ENGINE_START_SECONDS))));
14953
- const bins = Array.from({ length: count }, () => []);
14954
- const loads = Array.from({ length: count }, () => 0);
15160
+ if (units.length === 0) {
15161
+ return [];
15162
+ }
15163
+ const together = windowed.reduce((sum, unit) => sum + unit.cost, 0);
15164
+ const total = units.reduce((sum, unit) => sum + unit.cost, 0);
15165
+ const slowest = Math.max(together, headless[0]?.cost ?? 0);
15166
+ const count = Math.max(1, Math.min(most, headless.length + (windowed.length > 0 ? 1 : 0), Math.ceil(total / Math.max(slowest, ENGINE_START_SECONDS))));
15167
+ const bins = Array.from({ length: count }, (_, index) => index === 0 ? [...windowed] : []);
15168
+ const loads = Array.from({ length: count }, (_, index) => index === 0 ? together : 0);
14955
15169
  for (const unit of headless) {
14956
15170
  const lightest = loads.indexOf(Math.min(...loads));
14957
15171
  bins[lightest]?.push(unit);
14958
15172
  loads[lightest] = (loads[lightest] ?? 0) + unit.cost;
14959
15173
  }
14960
- const plans = bins.map((bin) => planOf(bin, declared, false));
14961
- return windowed.length > 0 ? [planOf(windowed, declared, true), ...plans] : plans;
15174
+ return bins.map((bin, index) => planOf(bin, declared, index === 0 && windowed.length > 0));
15175
+ }
15176
+ function windowedSaid(plans) {
15177
+ if (!plans.some((plan) => plan.windowed)) {
15178
+ return "";
15179
+ }
15180
+ return plans.length === 1 ? ", with a window" : ", one of them windowed";
14962
15181
  }
14963
15182
  function inOneEngine(units, declared) {
14964
15183
  return planOf(units, declared, units.some((unit) => unit.windowed));
@@ -14978,10 +15197,10 @@ function planOf(bin, declared, windowed) {
14978
15197
  }
14979
15198
  function viewportOf(projectPath) {
14980
15199
  const file = `${projectPath}/project.godot`;
14981
- if (!existsSync17(file)) {
15200
+ if (!existsSync18(file)) {
14982
15201
  return null;
14983
15202
  }
14984
- const text = readFileSync16(file, "utf8");
15203
+ const text = readFileSync17(file, "utf8");
14985
15204
  const width = /^window\/size\/viewport_width\s*=\s*(\d+)/m.exec(text)?.[1];
14986
15205
  const height = /^window\/size\/viewport_height\s*=\s*(\d+)/m.exec(text)?.[1];
14987
15206
  return width === undefined || height === undefined ? null : { width: Number(width), height: Number(height) };
@@ -36597,13 +36816,13 @@ var init_stdio = __esm(() => {
36597
36816
  });
36598
36817
 
36599
36818
  // src/autoload-failure.ts
36600
- import { existsSync as existsSync19, readFileSync as readFileSync19 } from "node:fs";
36601
- import { join as join22 } from "node:path";
36819
+ import { existsSync as existsSync20, readFileSync as readFileSync20 } from "node:fs";
36820
+ import { join as join23 } from "node:path";
36602
36821
  function scriptsAutoloaded(projectPath) {
36603
36822
  const scripts = new Map;
36604
36823
  let settings;
36605
36824
  try {
36606
- settings = parseProjectGodot(readFileSync19(join22(projectPath, "project.godot"), "utf8"));
36825
+ settings = parseProjectGodot(readFileSync20(join23(projectPath, "project.godot"), "utf8"));
36607
36826
  } catch {
36608
36827
  return scripts;
36609
36828
  }
@@ -36612,12 +36831,12 @@ function scriptsAutoloaded(projectPath) {
36612
36831
  scripts.set(`res://${path}`, name);
36613
36832
  continue;
36614
36833
  }
36615
- const scene = join22(projectPath, path);
36616
- if (!existsSync19(scene)) {
36834
+ const scene = join23(projectPath, path);
36835
+ if (!existsSync20(scene)) {
36617
36836
  continue;
36618
36837
  }
36619
36838
  try {
36620
- for (const named of readFileSync19(scene, "utf8").matchAll(/^\[ext_resource\b[^\]]*\btype="Script"[^\]]*\bpath="(res:\/\/[^"]+)"/gm)) {
36839
+ for (const named of readFileSync20(scene, "utf8").matchAll(/^\[ext_resource\b[^\]]*\btype="Script"[^\]]*\bpath="(res:\/\/[^"]+)"/gm)) {
36621
36840
  scripts.set(named[1] ?? "", name);
36622
36841
  }
36623
36842
  } catch {}
@@ -36636,10 +36855,10 @@ var init_autoload_failure = __esm(() => {
36636
36855
  });
36637
36856
 
36638
36857
  // src/runtime-client.ts
36639
- import { existsSync as existsSync20, readdirSync as readdirSync12, readFileSync as readFileSync20, statSync as statSync12, unlinkSync as unlinkSync2 } from "node:fs";
36858
+ import { existsSync as existsSync21, readdirSync as readdirSync13, readFileSync as readFileSync21, statSync as statSync13, unlinkSync as unlinkSync2 } from "node:fs";
36640
36859
  import { createConnection } from "node:net";
36641
36860
  import { tmpdir as tmpdir5 } from "node:os";
36642
- import { dirname as dirname12, join as join23, resolve as resolve5 } from "node:path";
36861
+ import { dirname as dirname12, join as join24, resolve as resolve5 } from "node:path";
36643
36862
  import { setTimeout as delay4 } from "node:timers/promises";
36644
36863
  function runtimeDirectory(variables = process.env) {
36645
36864
  const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
@@ -36647,20 +36866,20 @@ function runtimeDirectory(variables = process.env) {
36647
36866
  return explicit;
36648
36867
  }
36649
36868
  const perUser = envValue("XDG_RUNTIME_DIR", variables);
36650
- return join23(perUser ?? tmpdir5(), "gdharness");
36869
+ return join24(perUser ?? tmpdir5(), "gdharness");
36651
36870
  }
36652
36871
  function runtimeDirectories(variables = process.env) {
36653
36872
  const candidates = [runtimeDirectory(variables)];
36654
36873
  const fallbacks = process.platform === "win32" ? [envValue("SystemRoot", variables) ?? envValue("windir", variables) ?? "C:\\Windows"] : ["/tmp", "/var/tmp"];
36655
36874
  for (const base of fallbacks) {
36656
- candidates.push(join23(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
36875
+ candidates.push(join24(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
36657
36876
  }
36658
36877
  return [...new Set(candidates.map((path) => resolve5(path)))];
36659
36878
  }
36660
36879
  function errorReportOf(pid, directories = runtimeDirectories()) {
36661
36880
  for (const directory of directories) {
36662
- const path = join23(directory, `runtime-${pid}.log`);
36663
- if (existsSync20(path)) {
36881
+ const path = join24(directory, `runtime-${pid}.log`);
36882
+ if (existsSync21(path)) {
36664
36883
  return path;
36665
36884
  }
36666
36885
  }
@@ -36691,7 +36910,7 @@ function reportAlreadyInTranscript(transcript, report) {
36691
36910
  function parseAnnouncement(file, pid) {
36692
36911
  let fields;
36693
36912
  try {
36694
- fields = asParams(JSON.parse(readFileSync20(file, "utf8")));
36913
+ fields = asParams(JSON.parse(readFileSync21(file, "utf8")));
36695
36914
  } catch {
36696
36915
  return { kind: "rubbish" };
36697
36916
  }
@@ -36708,7 +36927,7 @@ function parseAnnouncement(file, pid) {
36708
36927
  };
36709
36928
  let announcedAt;
36710
36929
  try {
36711
- announcedAt = statSync12(file).mtimeMs;
36930
+ announcedAt = statSync13(file).mtimeMs;
36712
36931
  } catch {}
36713
36932
  const when = announcedAt === undefined ? {} : { announcedAt };
36714
36933
  if (protocol !== RUNTIME_PROTOCOL) {
@@ -36761,7 +36980,7 @@ function discoverRuntimes(directories = runtimeDirectories()) {
36761
36980
  function wentAway(file, pid) {
36762
36981
  let project = "";
36763
36982
  try {
36764
- const fields = asParams(JSON.parse(readFileSync20(file, "utf8")));
36983
+ const fields = asParams(JSON.parse(readFileSync21(file, "utf8")));
36765
36984
  project = readString(readParams(fields, "project") ?? {}, "path") ?? "";
36766
36985
  } catch {
36767
36986
  return;
@@ -36804,29 +37023,29 @@ function strangersAmong(candidates, now, startTimes = startTimesOf) {
36804
37023
  }).map((one) => one.file));
36805
37024
  }
36806
37025
  function announcedIn(directory) {
36807
- if (!existsSync20(directory)) {
37026
+ if (!existsSync21(directory)) {
36808
37027
  return [];
36809
37028
  }
36810
37029
  const found = [];
36811
37030
  const candidates = [];
36812
37031
  const entries = [];
36813
- for (const entry of readdirSync12(directory)) {
37032
+ for (const entry of readdirSync13(directory)) {
36814
37033
  const report = ERROR_REPORT_PATTERN.exec(entry);
36815
37034
  if (report) {
36816
- sweepErrorReport(join23(directory, entry), Number.parseInt(report[1] ?? "", 10));
37035
+ sweepErrorReport(join24(directory, entry), Number.parseInt(report[1] ?? "", 10));
36817
37036
  continue;
36818
37037
  }
36819
37038
  const match = ANNOUNCEMENT_PATTERN.exec(entry);
36820
37039
  if (!match) {
36821
37040
  continue;
36822
37041
  }
36823
- const file = join23(directory, entry);
37042
+ const file = join24(directory, entry);
36824
37043
  const pid = Number.parseInt(match[1] ?? "", 10);
36825
37044
  const running = alive(pid);
36826
37045
  entries.push({ file, pid, running });
36827
37046
  if (running) {
36828
37047
  try {
36829
- candidates.push({ file, pid, writtenAt: statSync12(file).mtimeMs });
37048
+ candidates.push({ file, pid, writtenAt: statSync13(file).mtimeMs });
36830
37049
  } catch {}
36831
37050
  }
36832
37051
  }
@@ -36848,7 +37067,7 @@ function announcedIn(directory) {
36848
37067
  found.push(announced);
36849
37068
  }
36850
37069
  const held = new Set(entries.map((one) => one.file));
36851
- const here = join23(directory, ".");
37070
+ const here = join24(directory, ".");
36852
37071
  for (const file of verdicts.keys()) {
36853
37072
  if (dirname12(file) === here && !held.has(file)) {
36854
37073
  verdicts.delete(file);
@@ -36857,7 +37076,7 @@ function announcedIn(directory) {
36857
37076
  return found;
36858
37077
  }
36859
37078
  async function announcementEnded(pid, directories = runtimeDirectories()) {
36860
- const files = directories.map((directory) => join23(directory, `runtime-${pid}.json`)).filter((file) => existsSync20(file));
37079
+ const files = directories.map((directory) => join24(directory, `runtime-${pid}.json`)).filter((file) => existsSync21(file));
36861
37080
  const first = files[0];
36862
37081
  if (first === undefined) {
36863
37082
  return;
@@ -36883,7 +37102,7 @@ function sweepErrorReport(path, pid, now = Date.now()) {
36883
37102
  return;
36884
37103
  }
36885
37104
  try {
36886
- if (now - statSync12(path).mtimeMs >= ERROR_REPORT_KEEP_MS) {
37105
+ if (now - statSync13(path).mtimeMs >= ERROR_REPORT_KEEP_MS) {
36887
37106
  unlinkSync2(path);
36888
37107
  }
36889
37108
  } catch {}
@@ -37276,14 +37495,14 @@ var init_runtime_client = __esm(() => {
37276
37495
  });
37277
37496
 
37278
37497
  // src/boot-note.ts
37279
- import { mkdirSync as mkdirSync9, readFileSync as readFileSync21, writeFileSync as writeFileSync11 } from "node:fs";
37280
- import { dirname as dirname13, join as join24 } from "node:path";
37498
+ import { mkdirSync as mkdirSync9, readFileSync as readFileSync22, writeFileSync as writeFileSync11 } from "node:fs";
37499
+ import { dirname as dirname13, join as join25 } from "node:path";
37281
37500
  function bootNotePath(projectPath) {
37282
- return join24(projectPath, ".godot", "gdharness-boot.json");
37501
+ return join25(projectPath, ".godot", "gdharness-boot.json");
37283
37502
  }
37284
37503
  function readScenes(projectPath) {
37285
37504
  try {
37286
- return asParams(asParams(JSON.parse(readFileSync21(bootNotePath(projectPath), "utf8")))["scenes"]);
37505
+ return asParams(asParams(JSON.parse(readFileSync22(bootNotePath(projectPath), "utf8")))["scenes"]);
37287
37506
  } catch {
37288
37507
  return {};
37289
37508
  }
@@ -37325,10 +37544,10 @@ var init_boot_note = __esm(() => {
37325
37544
  });
37326
37545
 
37327
37546
  // src/breakpoint-note.ts
37328
- import { mkdirSync as mkdirSync10, readFileSync as readFileSync22, rmSync as rmSync11, writeFileSync as writeFileSync12 } from "node:fs";
37329
- import { dirname as dirname14, isAbsolute as isAbsolute5, join as join25, relative as relative4 } from "node:path";
37547
+ import { mkdirSync as mkdirSync10, readFileSync as readFileSync23, rmSync as rmSync11, writeFileSync as writeFileSync12 } from "node:fs";
37548
+ import { dirname as dirname14, isAbsolute as isAbsolute5, join as join26, relative as relative4 } from "node:path";
37330
37549
  function breakpointNotePath(projectPath) {
37331
- return join25(projectPath, ".godot", "gdharness-breakpoints.json");
37550
+ return join26(projectPath, ".godot", "gdharness-breakpoints.json");
37332
37551
  }
37333
37552
  function writeBreakpointNote(projectPath, held) {
37334
37553
  const path = breakpointNotePath(projectPath);
@@ -37354,7 +37573,7 @@ function writeBreakpointNote(projectPath, held) {
37354
37573
  }
37355
37574
  function readBreakpointNote(projectPath) {
37356
37575
  try {
37357
- const parsed = JSON.parse(readFileSync22(breakpointNotePath(projectPath), "utf8"));
37576
+ const parsed = JSON.parse(readFileSync23(breakpointNotePath(projectPath), "utf8"));
37358
37577
  if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.breakpoints)) {
37359
37578
  return [];
37360
37579
  }
@@ -37369,7 +37588,7 @@ function readBreakpointNote(projectPath) {
37369
37588
  }
37370
37589
  const lines = Array.isArray(listed) ? listed.filter((line) => Number.isInteger(line) && line > 0) : [];
37371
37590
  if (lines.length > 0) {
37372
- held.push({ scriptPath: realAsFarAsItExists(join25(projectPath, script)), lines });
37591
+ held.push({ scriptPath: realAsFarAsItExists(join26(projectPath, script)), lines });
37373
37592
  }
37374
37593
  }
37375
37594
  return held;
@@ -37387,14 +37606,14 @@ var init_breakpoint_note = __esm(() => {
37387
37606
  });
37388
37607
 
37389
37608
  // src/class-note.ts
37390
- import { mkdirSync as mkdirSync11, readFileSync as readFileSync23 } from "node:fs";
37391
- import { dirname as dirname15, join as join26 } from "node:path";
37609
+ import { mkdirSync as mkdirSync11, readFileSync as readFileSync24 } from "node:fs";
37610
+ import { dirname as dirname15, join as join27 } from "node:path";
37392
37611
  function classNotePath(projectPath) {
37393
- return join26(projectPath, ".godot", "gdharness-classes.json");
37612
+ return join27(projectPath, ".godot", "gdharness-classes.json");
37394
37613
  }
37395
37614
  function readClassNote(projectPath) {
37396
37615
  try {
37397
- const fields = asParams(JSON.parse(readFileSync23(classNotePath(projectPath), "utf8")));
37616
+ const fields = asParams(JSON.parse(readFileSync24(classNotePath(projectPath), "utf8")));
37398
37617
  const rebuiltWith = readArray(fields, "rebuiltWith");
37399
37618
  if (rebuiltWith === undefined || !rebuiltWith.every((name) => typeof name === "string")) {
37400
37619
  return null;
@@ -37436,16 +37655,16 @@ var init_class_note = __esm(() => {
37436
37655
  });
37437
37656
 
37438
37657
  // src/config-pin.ts
37439
- import { existsSync as existsSync21, readFileSync as readFileSync24 } from "node:fs";
37440
- import { join as join27 } from "node:path";
37658
+ import { existsSync as existsSync22, readFileSync as readFileSync25 } from "node:fs";
37659
+ import { join as join28 } from "node:path";
37441
37660
  function versionsNamedInConfig(projectPath) {
37442
- const config = join27(projectPath, ".mcp.json");
37443
- if (!existsSync21(config)) {
37661
+ const config = join28(projectPath, ".mcp.json");
37662
+ if (!existsSync22(config)) {
37444
37663
  return [];
37445
37664
  }
37446
37665
  let text;
37447
37666
  try {
37448
- text = readFileSync24(config, "utf8");
37667
+ text = readFileSync25(config, "utf8");
37449
37668
  JSON.parse(text);
37450
37669
  } catch {
37451
37670
  return [];
@@ -37466,10 +37685,10 @@ var init_config_pin = __esm(() => {
37466
37685
  });
37467
37686
 
37468
37687
  // src/cut-test-run.ts
37469
- import { existsSync as existsSync22, mkdirSync as mkdirSync12, readFileSync as readFileSync25, rmSync as rmSync12, writeFileSync as writeFileSync13 } from "node:fs";
37470
- import { dirname as dirname16, join as join28 } from "node:path";
37688
+ import { existsSync as existsSync23, mkdirSync as mkdirSync12, readFileSync as readFileSync26, rmSync as rmSync12, writeFileSync as writeFileSync13 } from "node:fs";
37689
+ import { dirname as dirname16, join as join29 } from "node:path";
37471
37690
  function keptAt(projectPath) {
37472
- return join28(projectPath, ".godot", "gdharness-reports", "cut-run.json");
37691
+ return join29(projectPath, ".godot", "gdharness-reports", "cut-run.json");
37473
37692
  }
37474
37693
  function keepCutTestRun(projectPath, run) {
37475
37694
  const path = keptAt(projectPath);
@@ -37481,11 +37700,11 @@ function keepCutTestRun(projectPath, run) {
37481
37700
  }
37482
37701
  function takeCutTestRun(projectPath) {
37483
37702
  const path = keptAt(projectPath);
37484
- if (!existsSync22(path)) {
37703
+ if (!existsSync23(path)) {
37485
37704
  return null;
37486
37705
  }
37487
37706
  try {
37488
- const run = JSON.parse(readFileSync25(path, "utf8"));
37707
+ const run = JSON.parse(readFileSync26(path, "utf8"));
37489
37708
  rmSync12(path, { force: true });
37490
37709
  return typeof run.cutAt === "string" && Array.isArray(run.paths) && run.paths.every((path) => typeof path === "string") ? run : null;
37491
37710
  } catch {
@@ -38298,25 +38517,25 @@ var init_dap_client = __esm(() => {
38298
38517
  // src/editor-log.ts
38299
38518
  import {
38300
38519
  closeSync as closeSync4,
38301
- existsSync as existsSync23,
38520
+ existsSync as existsSync24,
38302
38521
  mkdirSync as mkdirSync13,
38303
38522
  openSync as openSync4,
38304
- readFileSync as readFileSync26,
38523
+ readFileSync as readFileSync27,
38305
38524
  readSync as readSync2,
38306
38525
  rmSync as rmSync13,
38307
- statSync as statSync13,
38526
+ statSync as statSync14,
38308
38527
  writeFileSync as writeFileSync14
38309
38528
  } from "node:fs";
38310
- import { dirname as dirname17, join as join29 } from "node:path";
38529
+ import { dirname as dirname17, join as join30 } from "node:path";
38311
38530
  function editorLogPath(projectPath) {
38312
- return join29(projectPath, ".godot", "gdharness-editor.log");
38531
+ return join30(projectPath, ".godot", "gdharness-editor.log");
38313
38532
  }
38314
38533
  function editorLogNotePath(projectPath) {
38315
- return join29(projectPath, ".godot", "gdharness-editor.json");
38534
+ return join30(projectPath, ".godot", "gdharness-editor.json");
38316
38535
  }
38317
38536
  function readEditorLogNote(projectPath) {
38318
38537
  try {
38319
- const fields = asParams(JSON.parse(readFileSync26(editorLogNotePath(projectPath), "utf8")));
38538
+ const fields = asParams(JSON.parse(readFileSync27(editorLogNotePath(projectPath), "utf8")));
38320
38539
  const pid = readNumber(fields, "pid");
38321
38540
  const startedAt = readString(fields, "startedAt");
38322
38541
  return pid !== undefined && Number.isInteger(pid) && pid > 0 && startedAt !== undefined ? { pid, startedAt } : null;
@@ -38357,7 +38576,7 @@ function editorConsole(projectPath, editorPid, openedByAServer) {
38357
38576
  return { kind: "another editor", notedPid: noted?.pid ?? null };
38358
38577
  }
38359
38578
  const path = editorLogPath(projectPath);
38360
- if (!existsSync23(path)) {
38579
+ if (!existsSync24(path)) {
38361
38580
  return { kind: "not written" };
38362
38581
  }
38363
38582
  const log = new GameLog;
@@ -38413,9 +38632,9 @@ function foldProgress(text) {
38413
38632
  `);
38414
38633
  }
38415
38634
  function theFirstOf(path) {
38416
- const whole = statSync13(path).size;
38635
+ const whole = statSync14(path).size;
38417
38636
  if (whole <= MOST_CONSOLE_BYTES) {
38418
- return { text: readFileSync26(path, "utf8") };
38637
+ return { text: readFileSync27(path, "utf8") };
38419
38638
  }
38420
38639
  const handle = openSync4(path, "r");
38421
38640
  try {
@@ -38576,7 +38795,7 @@ var init_editor_log = __esm(() => {
38576
38795
  });
38577
38796
 
38578
38797
  // src/rename.ts
38579
- import { existsSync as existsSync24, mkdirSync as mkdirSync14, readFileSync as readFileSync27, renameSync as renameSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync15 } from "node:fs";
38798
+ import { existsSync as existsSync25, mkdirSync as mkdirSync14, readFileSync as readFileSync28, renameSync as renameSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync15 } from "node:fs";
38580
38799
  import { dirname as dirname18, posix as posix4 } from "node:path";
38581
38800
 
38582
38801
  class Script {
@@ -39281,11 +39500,11 @@ function rewritesOf(plan, texts, move) {
39281
39500
  }
39282
39501
  function applyRewrites(rewrites) {
39283
39502
  for (const rewrite of rewrites) {
39284
- const now = readFileSync27(rewrite.absolute, "utf8");
39503
+ const now = readFileSync28(rewrite.absolute, "utf8");
39285
39504
  if (now !== rewrite.before) {
39286
39505
  throw new Error(`${rewrite.path} changed on disk while the rename was being worked out; nothing was written`);
39287
39506
  }
39288
- if (rewrite.writeTo !== rewrite.absolute && existsSync24(rewrite.writeTo)) {
39507
+ if (rewrite.writeTo !== rewrite.absolute && existsSync25(rewrite.writeTo)) {
39289
39508
  throw new Error(`${rewrite.writeTo} already exists; nothing was written`);
39290
39509
  }
39291
39510
  }
@@ -39299,7 +39518,7 @@ function applyRewrites(rewrites) {
39299
39518
  let movedUid = false;
39300
39519
  if (rewrite.writeTo !== rewrite.absolute) {
39301
39520
  unlinkSync3(rewrite.absolute);
39302
- if (existsSync24(`${rewrite.absolute}.uid`)) {
39521
+ if (existsSync25(`${rewrite.absolute}.uid`)) {
39303
39522
  renameSync3(`${rewrite.absolute}.uid`, `${rewrite.writeTo}.uid`);
39304
39523
  movedUid = true;
39305
39524
  }
@@ -39345,10 +39564,10 @@ var init_rename = __esm(() => {
39345
39564
  });
39346
39565
 
39347
39566
  // src/lsp_client.ts
39348
- import { existsSync as existsSync25, realpathSync as realpathSync3 } from "node:fs";
39567
+ import { existsSync as existsSync26, realpathSync as realpathSync3 } from "node:fs";
39349
39568
  import { readFile, realpath } from "node:fs/promises";
39350
39569
  import { createConnection as createConnection3 } from "node:net";
39351
- import { dirname as dirname19, join as join30, resolve as resolve6 } from "node:path";
39570
+ import { dirname as dirname19, join as join31, resolve as resolve6 } from "node:path";
39352
39571
  import { setTimeout as delay6 } from "node:timers/promises";
39353
39572
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
39354
39573
  function initializeTimeoutMs() {
@@ -40026,7 +40245,7 @@ async function resolveLSPPaths(projectPathValue, scriptPathValue) {
40026
40245
  } catch {
40027
40246
  throw new ArgumentRefusal(`Project path does not exist: ${requestedProjectPath}`);
40028
40247
  }
40029
- if (!existsSync25(join30(projectPath, "project.godot"))) {
40248
+ if (!existsSync26(join31(projectPath, "project.godot"))) {
40030
40249
  throw new ArgumentRefusal(`Not a Godot project: ${projectPath}. Point projectPath at the directory holding project.godot.`);
40031
40250
  }
40032
40251
  const contained = resolveWithinProject(projectPath, scriptPathValue);
@@ -40186,8 +40405,8 @@ var init_lsp_client = __esm(() => {
40186
40405
  });
40187
40406
 
40188
40407
  // src/rename-symbol.ts
40189
- import { existsSync as existsSync26, mkdirSync as mkdirSync15, readdirSync as readdirSync13, realpathSync as realpathSync4, rmSync as rmSync14, writeFileSync as writeFileSync16 } from "node:fs";
40190
- import { basename as basename8, dirname as dirname20, join as join31, relative as relative5, resolve as resolve7 } from "node:path";
40408
+ import { existsSync as existsSync27, mkdirSync as mkdirSync15, readdirSync as readdirSync14, realpathSync as realpathSync4, rmSync as rmSync14, writeFileSync as writeFileSync16 } from "node:fs";
40409
+ import { basename as basename8, dirname as dirname20, join as join32, relative as relative5, resolve as resolve7 } from "node:path";
40191
40410
  function resourcePathOf(projectPath, absolute) {
40192
40411
  const inside = relative5(onDiskSpelling(projectPath), onDiskSpelling(absolute));
40193
40412
  if (inside === "" || inside.startsWith("..") || /^[A-Za-z]:/.test(inside)) {
@@ -40200,7 +40419,7 @@ function onDiskSpelling(path) {
40200
40419
  let at = resolve7(path);
40201
40420
  for (;; ) {
40202
40421
  try {
40203
- return join31(realpathSync4.native(at), ...missing.reverse());
40422
+ return join32(realpathSync4.native(at), ...missing.reverse());
40204
40423
  } catch {
40205
40424
  const parent = dirname20(at);
40206
40425
  if (parent === at) {
@@ -40254,7 +40473,7 @@ async function renameSymbol(request, services) {
40254
40473
  reason: `newScriptPath must be a .gd path inside the project, and ${request.newScriptPath} is not.`
40255
40474
  };
40256
40475
  }
40257
- if (existsSync26(request.newScriptPath)) {
40476
+ if (existsSync27(request.newScriptPath)) {
40258
40477
  return { ok: false, reason: `${moveTo} already exists, so the script cannot be moved there.` };
40259
40478
  }
40260
40479
  }
@@ -40430,7 +40649,7 @@ async function renameSymbol(request, services) {
40430
40649
  scripts,
40431
40650
  moves: new Map([[declaringPath, moveTo]]),
40432
40651
  named: [declaringPath],
40433
- exists: (path) => existsSync26(`${projectPath}/${path.slice("res://".length)}`)
40652
+ exists: (path) => existsSync27(`${projectPath}/${path.slice("res://".length)}`)
40434
40653
  }, plan);
40435
40654
  }
40436
40655
  plan = mentionIgnored(plan, ignoredTexts(projectPath), symbol, moveTo === null ? [] : [declaringPath]);
@@ -40629,15 +40848,15 @@ ${mention.kind}`;
40629
40848
  };
40630
40849
  }
40631
40850
  function writeLeftAloneDetail(projectPath, symbol, detail) {
40632
- const directory = join31(projectPath, ".godot", "gdharness-renames");
40851
+ const directory = join32(projectPath, ".godot", "gdharness-renames");
40633
40852
  mkdirSync15(directory, { recursive: true });
40634
40853
  detailWrites += 1;
40635
- const path = join31(directory, `${String(Date.now())}-${String(detailWrites).padStart(6, "0")}-${String(process.pid)}-${symbol}.json`);
40854
+ const path = join32(directory, `${String(Date.now())}-${String(detailWrites).padStart(6, "0")}-${String(process.pid)}-${symbol}.json`);
40636
40855
  writeFileSync16(path, `${JSON.stringify(detail, null, 2)}
40637
40856
  `, "utf8");
40638
- const kept = readdirSync13(directory).filter((name) => name.endsWith(".json")).sort().reverse();
40857
+ const kept = readdirSync14(directory).filter((name) => name.endsWith(".json")).sort().reverse();
40639
40858
  for (const old of kept.slice(10)) {
40640
- rmSync14(join31(directory, old), { force: true });
40859
+ rmSync14(join32(directory, old), { force: true });
40641
40860
  }
40642
40861
  return path;
40643
40862
  }
@@ -40678,25 +40897,25 @@ var init_rename_symbol = __esm(() => {
40678
40897
 
40679
40898
  // src/file-move.ts
40680
40899
  import {
40681
- existsSync as existsSync27,
40900
+ existsSync as existsSync28,
40682
40901
  mkdirSync as mkdirSync16,
40683
- readdirSync as readdirSync14,
40684
- readFileSync as readFileSync28,
40902
+ readdirSync as readdirSync15,
40903
+ readFileSync as readFileSync29,
40685
40904
  renameSync as renameSync4,
40686
- statSync as statSync14,
40905
+ statSync as statSync15,
40687
40906
  writeFileSync as writeFileSync17
40688
40907
  } from "node:fs";
40689
- import { basename as basename9, dirname as dirname21, extname as extname3, join as join32, relative as relative6 } from "node:path";
40908
+ import { basename as basename9, dirname as dirname21, extname as extname3, join as join33, relative as relative6 } from "node:path";
40690
40909
  function filesUnder(directory) {
40691
- return readdirSync14(directory, { withFileTypes: true }).flatMap((entry) => {
40692
- const path = join32(directory, entry.name);
40910
+ return readdirSync15(directory, { withFileTypes: true }).flatMap((entry) => {
40911
+ const path = join33(directory, entry.name);
40693
40912
  return entry.isDirectory() ? filesUnder(path) : [path];
40694
40913
  });
40695
40914
  }
40696
40915
  function sameFile(one, other) {
40697
40916
  try {
40698
- const a = statSync14(one);
40699
- const b = statSync14(other);
40917
+ const a = statSync15(one);
40918
+ const b = statSync15(other);
40700
40919
  return a.ino === b.ino && a.dev === b.dev;
40701
40920
  } catch {
40702
40921
  return false;
@@ -40710,8 +40929,8 @@ function unseenByTheEngine(projectPath, path) {
40710
40929
  return `${path} is under ${step}, and the engine passes over every directory whose name starts with a dot`;
40711
40930
  }
40712
40931
  if (at < steps.length - 1) {
40713
- directory = join32(directory, step);
40714
- if (existsSync27(join32(directory, ".gdignore"))) {
40932
+ directory = join33(directory, step);
40933
+ if (existsSync28(join33(directory, ".gdignore"))) {
40715
40934
  return `${path} is under a directory holding a .gdignore, which the engine does not load`;
40716
40935
  }
40717
40936
  }
@@ -40723,21 +40942,21 @@ function installedAddonOf(projectPath, path) {
40723
40942
  if (steps[0] !== "addons" || steps[1] === undefined) {
40724
40943
  return null;
40725
40944
  }
40726
- return existsSync27(join32(projectPath, "addons", steps[1], VERSION_MARKER)) ? `res://addons/${steps[1]}` : null;
40945
+ return existsSync28(join33(projectPath, "addons", steps[1], VERSION_MARKER)) ? `res://addons/${steps[1]}` : null;
40727
40946
  }
40728
40947
  function binaryResources(projectPath) {
40729
40948
  const found = [];
40730
40949
  const visit = (directory, prefix) => {
40731
- if (existsSync27(join32(directory, ".gdignore"))) {
40950
+ if (existsSync28(join33(directory, ".gdignore"))) {
40732
40951
  return;
40733
40952
  }
40734
- for (const entry of readdirSync14(directory, { withFileTypes: true })) {
40953
+ for (const entry of readdirSync15(directory, { withFileTypes: true })) {
40735
40954
  if (entry.name.startsWith(".")) {
40736
40955
  continue;
40737
40956
  }
40738
40957
  const path = `${prefix}${entry.name}`;
40739
40958
  if (entry.isDirectory()) {
40740
- visit(join32(directory, entry.name), `${path}/`);
40959
+ visit(join33(directory, entry.name), `${path}/`);
40741
40960
  } else if (/\.(scn|res)$/i.test(entry.name)) {
40742
40961
  found.push(path);
40743
40962
  }
@@ -40747,7 +40966,7 @@ function binaryResources(projectPath) {
40747
40966
  return found;
40748
40967
  }
40749
40968
  function targetPathOf(projectPath, absolute) {
40750
- const inside = relative6(onDiskSpelling(projectPath), join32(onDiskSpelling(dirname21(absolute)), basename9(absolute)));
40969
+ const inside = relative6(onDiskSpelling(projectPath), join33(onDiskSpelling(dirname21(absolute)), basename9(absolute)));
40751
40970
  if (inside === "" || inside.startsWith("..") || /^[A-Za-z]:/.test(inside)) {
40752
40971
  return null;
40753
40972
  }
@@ -40765,13 +40984,13 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
40765
40984
  if (to === null) {
40766
40985
  return { ok: false, reason: `newPath must be a path inside the project, and ${absoluteTo} is not.` };
40767
40986
  }
40768
- if (!existsSync27(absoluteFrom)) {
40987
+ if (!existsSync28(absoluteFrom)) {
40769
40988
  return { ok: false, reason: `There is nothing at ${from} to move.` };
40770
40989
  }
40771
40990
  if (from === to) {
40772
40991
  return { ok: false, reason: `${from} is already there; nothing to move.` };
40773
40992
  }
40774
- const isDirectory = statSync14(absoluteFrom).isDirectory();
40993
+ const isDirectory = statSync15(absoluteFrom).isDirectory();
40775
40994
  if (from === "res://project.godot") {
40776
40995
  return {
40777
40996
  ok: false,
@@ -40794,7 +41013,7 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
40794
41013
  if (isDirectory && to.startsWith(`${from}/`)) {
40795
41014
  return { ok: false, reason: `${from} cannot be moved into itself.` };
40796
41015
  }
40797
- if (existsSync27(absoluteTo) && !sameFile(absoluteFrom, absoluteTo)) {
41016
+ if (existsSync28(absoluteTo) && !sameFile(absoluteFrom, absoluteTo)) {
40798
41017
  return { ok: false, reason: `${to} already exists, so nothing was moved there.` };
40799
41018
  }
40800
41019
  const unseen = unseenByTheEngine(projectPath, to);
@@ -40802,7 +41021,7 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
40802
41021
  return { ok: false, reason: `${unseen}, so nothing there could load what was moved.` };
40803
41022
  }
40804
41023
  if (!isDirectory) {
40805
- const owner = SIDECARS.map((sidecar) => from.endsWith(sidecar) ? from.slice(0, -sidecar.length) : null).find((path) => path !== null && existsSync27(join32(projectPath, path.slice("res://".length))));
41024
+ const owner = SIDECARS.map((sidecar) => from.endsWith(sidecar) ? from.slice(0, -sidecar.length) : null).find((path) => path !== null && existsSync28(join33(projectPath, path.slice("res://".length))));
40806
41025
  if (owner !== undefined && owner !== null) {
40807
41026
  return { ok: false, reason: `${from} belongs to ${owner} and moves with it: move ${owner} instead.` };
40808
41027
  }
@@ -40824,7 +41043,7 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
40824
41043
  } else {
40825
41044
  moves.set(from, to);
40826
41045
  for (const sidecar of SIDECARS) {
40827
- if (existsSync27(`${absoluteFrom}${sidecar}`)) {
41046
+ if (existsSync28(`${absoluteFrom}${sidecar}`)) {
40828
41047
  moves.set(`${from}${sidecar}`, `${to}${sidecar}`);
40829
41048
  }
40830
41049
  }
@@ -40836,12 +41055,12 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
40836
41055
  scripts,
40837
41056
  moves,
40838
41057
  named: [from],
40839
- exists: (path) => existsSync27(join32(projectPath, path.slice("res://".length)))
41058
+ exists: (path) => existsSync28(join33(projectPath, path.slice("res://".length)))
40840
41059
  }, { edits: [], mentions: [] });
40841
41060
  plan = mentionIgnored(plan, ignoredTexts(projectPath), null, [from]);
40842
41061
  const binaries = [];
40843
41062
  for (const path of binaryResources(projectPath)) {
40844
- const bytes = readFileSync28(join32(projectPath, path.slice("res://".length)));
41063
+ const bytes = readFileSync29(join33(projectPath, path.slice("res://".length)));
40845
41064
  const names = [...moves.keys()].find((moving) => bytes.includes(moving));
40846
41065
  if (names !== undefined) {
40847
41066
  binaries.push({
@@ -40860,7 +41079,7 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
40860
41079
  }));
40861
41080
  const left = leftAloneReport(mentions);
40862
41081
  const rewrites = rewritesOf(plan, texts, null);
40863
- const absoluteOf = (path) => join32(projectPath, path.slice("res://".length));
41082
+ const absoluteOf = (path) => join33(projectPath, path.slice("res://".length));
40864
41083
  const rewritten = new Map(rewrites.map((rewrite) => [rewrite.path, rewrite]));
40865
41084
  const moved = [...moves].map(([source, target]) => {
40866
41085
  const rewrite = rewritten.get(source);
@@ -40902,18 +41121,18 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
40902
41121
  }
40903
41122
  function applyFileMove(outcome) {
40904
41123
  for (const rewrite of outcome.inPlace) {
40905
- if (readFileSync28(rewrite.absolute, "utf8") !== rewrite.before) {
41124
+ if (readFileSync29(rewrite.absolute, "utf8") !== rewrite.before) {
40906
41125
  throw new Error(`${rewrite.path} changed on disk while the move was being worked out; nothing was written`);
40907
41126
  }
40908
41127
  }
40909
41128
  for (const file of outcome.moved) {
40910
- if (!existsSync27(file.absoluteFrom)) {
41129
+ if (!existsSync28(file.absoluteFrom)) {
40911
41130
  throw new Error(`${file.from} is gone from disk; nothing was written`);
40912
41131
  }
40913
- if (file.text !== null && readFileSync28(file.absoluteFrom, "utf8") !== file.text.before) {
41132
+ if (file.text !== null && readFileSync29(file.absoluteFrom, "utf8") !== file.text.before) {
40914
41133
  throw new Error(`${file.from} changed on disk while the move was being worked out; nothing was written`);
40915
41134
  }
40916
- if (existsSync27(file.absoluteTo) && !sameFile(file.absoluteFrom, file.absoluteTo)) {
41135
+ if (existsSync28(file.absoluteTo) && !sameFile(file.absoluteFrom, file.absoluteTo)) {
40917
41136
  throw new Error(`${file.to} already exists; nothing was written`);
40918
41137
  }
40919
41138
  }
@@ -44580,168 +44799,6 @@ var init_godot_bridge = __esm(() => {
44580
44799
  };
44581
44800
  });
44582
44801
 
44583
- // src/import-freshness.ts
44584
- import { createHash as createHash6 } from "node:crypto";
44585
- import { existsSync as existsSync28, readdirSync as readdirSync15, readFileSync as readFileSync29, statSync as statSync15 } from "node:fs";
44586
- import { join as join33 } from "node:path";
44587
- function md5Of(file) {
44588
- let stats;
44589
- try {
44590
- stats = statSync15(file);
44591
- } catch {
44592
- return null;
44593
- }
44594
- const known = hashes.get(file);
44595
- if (known !== undefined && known.size === stats.size && known.mtimeMs === stats.mtimeMs) {
44596
- return known.md5;
44597
- }
44598
- let md5;
44599
- try {
44600
- md5 = createHash6("md5").update(readFileSync29(file)).digest("hex");
44601
- } catch {
44602
- return null;
44603
- }
44604
- hashes.set(file, { size: stats.size, mtimeMs: stats.mtimeMs, md5 });
44605
- return md5;
44606
- }
44607
- function sectionOf(text, section) {
44608
- const keys = new Map;
44609
- let inside = false;
44610
- for (const line of text.split(/\r?\n/)) {
44611
- const header = /^\[([^\]]+)\]\s*$/.exec(line);
44612
- if (header !== null) {
44613
- inside = header[1] === section;
44614
- continue;
44615
- }
44616
- const pair = inside ? /^([A-Za-z0-9_./]+)=(.*)$/.exec(line) : null;
44617
- if (pair?.[1] !== undefined && pair[2] !== undefined) {
44618
- keys.set(pair[1], pair[2]);
44619
- }
44620
- }
44621
- return keys;
44622
- }
44623
- function unquoted(value) {
44624
- return value === undefined ? "" : value.replace(/^"(.*)"$/, "$1");
44625
- }
44626
- function listed2(value) {
44627
- return [...(value ?? "").matchAll(/"((?:[^"\\]|\\.)*)"/g)].map((match) => match[1] ?? "");
44628
- }
44629
- function onDisk(projectPath, resPath) {
44630
- return join33(projectPath, resPath.slice("res://".length));
44631
- }
44632
- function behind(projectPath, resPath, sidecar) {
44633
- const remap = sectionOf(sidecar, "remap");
44634
- const importer = unquoted(remap.get("importer"));
44635
- if (importer === "keep" || importer === "skip") {
44636
- return null;
44637
- }
44638
- const outputs = listed2(sectionOf(sidecar, "deps").get("dest_files"));
44639
- if (outputs.some((output) => !existsSync28(onDisk(projectPath, output)))) {
44640
- return "its imported output is not on disk";
44641
- }
44642
- const name = resPath.slice(resPath.lastIndexOf("/") + 1);
44643
- const record = join33(projectPath, ".godot", "imported", `${name}-${createHash6("md5").update(resPath, "utf8").digest("hex")}.md5`);
44644
- let recorded = "";
44645
- try {
44646
- recorded = /^source_md5="([0-9a-f]*)"/m.exec(readFileSync29(record, "utf8"))?.[1] ?? "";
44647
- } catch {
44648
- recorded = "";
44649
- }
44650
- if (recorded === "") {
44651
- return "its import recorded no hash of its source";
44652
- }
44653
- const current = md5Of(onDisk(projectPath, resPath));
44654
- return current === null || current === recorded ? null : "the source changed after it was imported";
44655
- }
44656
- function staleImports(projectPath) {
44657
- const stale = [];
44658
- const visit = (directory, resDirectory) => {
44659
- if (existsSync28(join33(directory, ".gdignore")) || existsSync28(join33(directory, VERSION_MARKER))) {
44660
- return;
44661
- }
44662
- let entries;
44663
- try {
44664
- entries = readdirSync15(directory, { withFileTypes: true });
44665
- } catch {
44666
- return;
44667
- }
44668
- const names = new Set(entries.map((entry) => entry.name));
44669
- for (const entry of entries) {
44670
- if (entry.name.startsWith(".")) {
44671
- continue;
44672
- }
44673
- const resPath = `${resDirectory}${entry.name}`;
44674
- if (entry.isDirectory()) {
44675
- visit(join33(directory, entry.name), `${resPath}/`);
44676
- continue;
44677
- }
44678
- if (!entry.isFile()) {
44679
- continue;
44680
- }
44681
- if (entry.name.endsWith(".import")) {
44682
- const source = resPath.slice(0, -".import".length);
44683
- if (!names.has(entry.name.slice(0, -".import".length))) {
44684
- continue;
44685
- }
44686
- let sidecar;
44687
- try {
44688
- sidecar = readFileSync29(join33(directory, entry.name), "utf8");
44689
- } catch {
44690
- continue;
44691
- }
44692
- const reason = behind(projectPath, source, sidecar);
44693
- if (reason !== null) {
44694
- stale.push({ path: source, reason });
44695
- }
44696
- continue;
44697
- }
44698
- const extension = entry.name.slice(entry.name.lastIndexOf(".") + 1).toLowerCase();
44699
- if (IMPORTABLE.has(extension) && !names.has(`${entry.name}.import`)) {
44700
- stale.push({ path: resPath, reason: "it has not been imported" });
44701
- }
44702
- }
44703
- };
44704
- visit(projectPath, "res://");
44705
- return stale.sort((one, other) => one.path.localeCompare(other.path));
44706
- }
44707
- var IMPORTABLE_EXTENSIONS, IMPORTABLE, hashes;
44708
- var init_import_freshness = __esm(() => {
44709
- init_project_scan();
44710
- IMPORTABLE_EXTENSIONS = [
44711
- "png",
44712
- "jpg",
44713
- "jpeg",
44714
- "webp",
44715
- "svg",
44716
- "bmp",
44717
- "tga",
44718
- "exr",
44719
- "hdr",
44720
- "dds",
44721
- "ktx",
44722
- "wav",
44723
- "mp3",
44724
- "ogg",
44725
- "ttf",
44726
- "otf",
44727
- "ttc",
44728
- "otc",
44729
- "woff",
44730
- "woff2",
44731
- "pfb",
44732
- "pfm",
44733
- "fnt",
44734
- "font",
44735
- "glb",
44736
- "gltf",
44737
- "fbx",
44738
- "obj",
44739
- "dae"
44740
- ];
44741
- IMPORTABLE = new Set(IMPORTABLE_EXTENSIONS);
44742
- hashes = new Map;
44743
- });
44744
-
44745
44802
  // src/keeper-channel.ts
44746
44803
  import { connect as connect2, createServer as createServer2 } from "node:net";
44747
44804
  async function askTheKeeperToStop(address, withinMs) {
@@ -49580,6 +49637,9 @@ ${error.text}`;
49580
49637
  const uncaughtFolded = foldedRepeats(uncaught.map(aboveTheRunner));
49581
49638
  const listedAsUncaught = new Set(uncaughtFolded.map((entry) => entry.index));
49582
49639
  const notApplied = ignoredYetRan(report, ignored);
49640
+ const scripts = ignored.length === 0 ? null : new ProjectScripts(project.value.path);
49641
+ const ignoreRead = ignoredLeftOut(report, ignored, asked.flatMap((path) => scripts === null ? [] : path.endsWith(".gd") ? scripts.suite(path) ?? [] : scripts.suitesUnder([path])));
49642
+ const unmatched = ignoreRead.unmatched.filter((entry) => !notApplied.some((one) => one.entry === entry));
49583
49643
  const testsNotRun = [
49584
49644
  ...new Set((selection.value?.selected ?? []).filter((one) => !report.suites.some((suite) => (suite.path === one.suite || suite.name === basename12(one.suite, ".gd")) && suite.cases.some((ran) => ran.name === one.test || new RegExp(`^${one.test}\\W`).test(ran.name)))).map((one) => `${basename12(one.suite, ".gd")}:${one.test}`))
49585
49645
  ];
@@ -49613,6 +49673,8 @@ ${error.text}`;
49613
49673
  orphans: orphans.total > 0 ? orphans.total : undefined,
49614
49674
  notRun: notRun > 0 ? notRun : undefined,
49615
49675
  ignoreNotApplied: notApplied.length > 0 ? notApplied : undefined,
49676
+ ignored: ignoreRead.leftOut.length > 0 ? ignoreRead.leftOut : undefined,
49677
+ ignoreUnmatched: unmatched.length > 0 ? unmatched : undefined,
49616
49678
  ...printedLines,
49617
49679
  savesNote,
49618
49680
  ...scanned,
@@ -49621,6 +49683,7 @@ ${error.text}`;
49621
49683
  testsNotRun.length > 0 ? `${testsNotRun.join(", ")} ${testsNotRun.length === 1 ? "was" : "were"} asked for and not run, so the run is not called passed: gdUnit4 runs a test_ function only in a script that extends GdUnitTestSuite, and leaves out a suite whose script does not load.` : undefined,
49622
49684
  briefed > 0 ? `${briefed} of the ${failed.length} failing cases, those with the largest reports, are named with their first message only, since their reports would have made the answer too large to deliver; pass one case's suite path to see its reports.` : undefined,
49623
49685
  notApplied.length > 0 ? "An ignore entry named a case that ran all the same: gdUnit4 leaves a case out only as suite_name:test_name, and reads an entry with no colon as a suite name or a piece of a suite path. ignoreNotApplied gives the form for each case that ran." : undefined,
49686
+ unmatched.length > 0 ? `${unmatched.join(", ")} ${unmatched.length === 1 ? "names" : "name"} no suite or case under the paths run, so ${unmatched.length === 1 ? "it" : "they"} left nothing out.` : undefined,
49624
49687
  uncaughtNote(uncaught),
49625
49688
  undeclaredErrors.length > 0 ? `An engine error a suite does not declare fails the run, as gdharness test judges it; ${declaringAdvice(undeclaredErrors[0]?.case ?? "test_name")}, once for each time it prints it.` : undefined,
49626
49689
  declaredNotPrinted.length > 0 ? "A declared error the case did not print fails the run too, since the case no longer reaches what it was written to test." : undefined,
@@ -53807,6 +53870,7 @@ var init_server = __esm(() => {
53807
53870
  });
53808
53871
  PROJECT_INFO_SECTIONS2 = dictionary({
53809
53872
  autoloads: { operation: "list_autoloads", params: () => ({}) },
53873
+ classes: { operation: "list_script_classes", params: () => ({}) },
53810
53874
  input_actions: { operation: "list_input_actions", params: () => ({}) },
53811
53875
  plugins: { operation: "list_plugins", params: () => ({}) },
53812
53876
  export_presets: { operation: "list_export_presets", params: () => ({}) },
@@ -54819,10 +54883,10 @@ init_setup();
54819
54883
  init_setup();
54820
54884
  init_tool_definitions();
54821
54885
  init_tool_reference();
54822
- import { existsSync as existsSync14, mkdirSync as mkdirSync5, readdirSync as readdirSync9, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "node:fs";
54823
- import { basename as basename7, dirname as dirname9, join as join16 } from "node:path";
54886
+ import { existsSync as existsSync15, mkdirSync as mkdirSync5, readdirSync as readdirSync10, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "node:fs";
54887
+ import { basename as basename7, dirname as dirname9, join as join17 } from "node:path";
54824
54888
  var SKILL_NAME = "gdharness";
54825
- var SHARED_SKILLS = join16(".agents", "skills");
54889
+ var SHARED_SKILLS = join17(".agents", "skills");
54826
54890
  function skillMarkdown(version) {
54827
54891
  return `---
54828
54892
  name: ${SKILL_NAME}
@@ -55059,27 +55123,27 @@ what they report while starting comes back under \`engine_messages_before_operat
55059
55123
  function skillFiles(version) {
55060
55124
  return new Map([
55061
55125
  ["SKILL.md", skillMarkdown(version)],
55062
- [join16("references", "tools.md"), renderToolsMarkdown()]
55126
+ [join17("references", "tools.md"), renderToolsMarkdown()]
55063
55127
  ]);
55064
55128
  }
55065
55129
  function skillDirectories(harnesses, projectPath, exists) {
55066
- const directories = [join16(projectPath, SHARED_SKILLS, SKILL_NAME)];
55130
+ const directories = [join17(projectPath, SHARED_SKILLS, SKILL_NAME)];
55067
55131
  for (const harness of harnesses) {
55068
55132
  if (harness.skills === undefined) {
55069
55133
  continue;
55070
55134
  }
55071
- const own = join16(projectPath, harness.skills.dir);
55072
- if ((!harness.skills.shared || exists(own)) && !directories.includes(join16(own, SKILL_NAME))) {
55073
- directories.push(join16(own, SKILL_NAME));
55135
+ const own = join17(projectPath, harness.skills.dir);
55136
+ if ((!harness.skills.shared || exists(own)) && !directories.includes(join17(own, SKILL_NAME))) {
55137
+ directories.push(join17(own, SKILL_NAME));
55074
55138
  }
55075
55139
  }
55076
55140
  return directories;
55077
55141
  }
55078
55142
  function everySkillDirectory(projectPath, harnesses) {
55079
- const directories = new Set([join16(projectPath, SHARED_SKILLS, SKILL_NAME)]);
55143
+ const directories = new Set([join17(projectPath, SHARED_SKILLS, SKILL_NAME)]);
55080
55144
  for (const harness of harnesses) {
55081
55145
  if (harness.skills !== undefined) {
55082
- directories.add(join16(projectPath, harness.skills.dir, SKILL_NAME));
55146
+ directories.add(join17(projectPath, harness.skills.dir, SKILL_NAME));
55083
55147
  }
55084
55148
  }
55085
55149
  return [...directories];
@@ -55087,7 +55151,7 @@ function everySkillDirectory(projectPath, harnesses) {
55087
55151
  function removeSkill(directories, projectPath) {
55088
55152
  const removed = [];
55089
55153
  for (const directory of directories) {
55090
- if (existsSync14(directory)) {
55154
+ if (existsSync15(directory)) {
55091
55155
  rmSync6(directory, { recursive: true, force: true });
55092
55156
  pruneEmpty(dirname9(directory), projectPath);
55093
55157
  removed.push(directory);
@@ -55096,10 +55160,10 @@ function removeSkill(directories, projectPath) {
55096
55160
  return removed;
55097
55161
  }
55098
55162
  function pruneEmpty(directory, projectPath) {
55099
- const shared = join16(projectPath, ".agents");
55163
+ const shared = join17(projectPath, ".agents");
55100
55164
  let path = directory;
55101
55165
  while (path !== projectPath && (basename7(path) === "skills" || path === shared)) {
55102
- if (readdirSync9(path).length > 0) {
55166
+ if (readdirSync10(path).length > 0) {
55103
55167
  return;
55104
55168
  }
55105
55169
  rmSync6(path, { recursive: true, force: true });
@@ -55109,10 +55173,10 @@ function pruneEmpty(directory, projectPath) {
55109
55173
  function writeSkill(directories, version) {
55110
55174
  const files = skillFiles(version);
55111
55175
  return directories.map((directory) => {
55112
- const replaced = existsSync14(directory);
55176
+ const replaced = existsSync15(directory);
55113
55177
  rmSync6(directory, { recursive: true, force: true });
55114
55178
  for (const [name, contents] of files) {
55115
- const path = join16(directory, name);
55179
+ const path = join17(directory, name);
55116
55180
  mkdirSync5(dirname9(path), { recursive: true });
55117
55181
  writeFileSync7(path, contents, "utf8");
55118
55182
  }
@@ -55125,27 +55189,27 @@ init_alive();
55125
55189
  init_desktop();
55126
55190
  import {
55127
55191
  closeSync as closeSync3,
55128
- existsSync as existsSync18,
55192
+ existsSync as existsSync19,
55129
55193
  fstatSync,
55130
55194
  mkdirSync as mkdirSync8,
55131
55195
  openSync as openSync3,
55132
- readdirSync as readdirSync11,
55133
- readFileSync as readFileSync18,
55196
+ readdirSync as readdirSync12,
55197
+ readFileSync as readFileSync19,
55134
55198
  readSync,
55135
55199
  rmSync as rmSync10,
55136
- statSync as statSync11,
55200
+ statSync as statSync12,
55137
55201
  writeFileSync as writeFileSync10
55138
55202
  } from "node:fs";
55139
55203
  import { availableParallelism, freemem, totalmem } from "node:os";
55140
- import { isAbsolute as isAbsolute4, join as join21, relative as relative3, resolve as resolve4, sep as sep2 } from "node:path";
55204
+ import { isAbsolute as isAbsolute4, join as join22, relative as relative3, resolve as resolve4, sep as sep2 } from "node:path";
55141
55205
  import process12 from "node:process";
55142
55206
  import { setTimeout as delay3 } from "node:timers/promises";
55143
55207
 
55144
55208
  // src/engine-slots.ts
55145
55209
  init_alive();
55146
55210
  init_process_children();
55147
- import { closeSync as closeSync2, mkdirSync as mkdirSync6, openSync as openSync2, readFileSync as readFileSync13, rmSync as rmSync8, statSync as statSync9, writeSync as writeSync2 } from "node:fs";
55148
- import { join as join18 } from "node:path";
55211
+ import { closeSync as closeSync2, mkdirSync as mkdirSync6, openSync as openSync2, readFileSync as readFileSync14, rmSync as rmSync8, statSync as statSync10, writeSync as writeSync2 } from "node:fs";
55212
+ import { join as join19 } from "node:path";
55149
55213
  import process11 from "node:process";
55150
55214
  import { setTimeout as delay2 } from "node:timers/promises";
55151
55215
  var START_TOLERANCE_MS = 2000;
@@ -55196,7 +55260,7 @@ class EngineSlots {
55196
55260
  for (;; ) {
55197
55261
  signal?.throwIfAborted();
55198
55262
  for (let slot = 0;slot < this.count; slot += 1) {
55199
- const lock = join18(this.directory, `${String(slot)}.lock`);
55263
+ const lock = join19(this.directory, `${String(slot)}.lock`);
55200
55264
  if (this.made(lock) || await this.takenOver(lock)) {
55201
55265
  this.held.add(lock);
55202
55266
  return lock;
@@ -55213,7 +55277,7 @@ class EngineSlots {
55213
55277
  release(lock) {
55214
55278
  this.held.delete(lock);
55215
55279
  try {
55216
- if (holderOf(readFileSync13(lock, "utf8"))?.pid === this.holder.pid) {
55280
+ if (holderOf(readFileSync14(lock, "utf8"))?.pid === this.holder.pid) {
55217
55281
  rmSync8(lock, { force: true });
55218
55282
  }
55219
55283
  } catch {}
@@ -55239,7 +55303,7 @@ class EngineSlots {
55239
55303
  const steal = `${lock}.steal`;
55240
55304
  if (!this.made(steal)) {
55241
55305
  try {
55242
- if (Date.now() - statSync9(steal).mtimeMs > UNWRITTEN_MS) {
55306
+ if (Date.now() - statSync10(steal).mtimeMs > UNWRITTEN_MS) {
55243
55307
  rmSync8(steal, { force: true });
55244
55308
  }
55245
55309
  } catch {}
@@ -55263,8 +55327,8 @@ class EngineSlots {
55263
55327
  let content;
55264
55328
  let writtenAt;
55265
55329
  try {
55266
- content = readFileSync13(lock, "utf8");
55267
- writtenAt = statSync9(lock).mtimeMs;
55330
+ content = readFileSync14(lock, "utf8");
55331
+ writtenAt = statSync10(lock).mtimeMs;
55268
55332
  } catch {
55269
55333
  return false;
55270
55334
  }
@@ -55291,9 +55355,9 @@ init_spawn();
55291
55355
 
55292
55356
  // src/suite-fingerprint.ts
55293
55357
  init_class_cache();
55294
- import { createHash as createHash5 } from "node:crypto";
55295
- import { existsSync as existsSync16, readdirSync as readdirSync10, readFileSync as readFileSync14, statSync as statSync10 } from "node:fs";
55296
- import { join as join19, posix as posix2 } from "node:path";
55358
+ import { createHash as createHash6 } from "node:crypto";
55359
+ import { existsSync as existsSync17, readdirSync as readdirSync11, readFileSync as readFileSync15, statSync as statSync11 } from "node:fs";
55360
+ import { join as join20, posix as posix2 } from "node:path";
55297
55361
  var RES_LITERAL = /res:\/\/[^"'\s)\]<>]*/g;
55298
55362
  var RELATIVE_LOAD = /\b(?:pre)?load\(\s*["']((?!res:\/\/|uid:\/\/)[^"']+)["']/g;
55299
55363
  var REACHES_AT_RUN_TIME = [
@@ -55313,7 +55377,7 @@ class SuiteFingerprints {
55313
55377
  constructor(scripts, always) {
55314
55378
  this.scripts = scripts;
55315
55379
  this.everywhere = ["res://project.godot", ...extensionsOf(scripts.projectPath)];
55316
- this.alwaysDigest = createHash5("sha256").update(always.join(`
55380
+ this.alwaysDigest = createHash6("sha256").update(always.join(`
55317
55381
  `)).digest("hex");
55318
55382
  }
55319
55383
  of(suite) {
@@ -55330,7 +55394,7 @@ class SuiteFingerprints {
55330
55394
  }
55331
55395
  pending.push(...reached.reaches);
55332
55396
  }
55333
- const hash = createHash5("sha256").update(this.alwaysDigest);
55397
+ const hash = createHash6("sha256").update(this.alwaysDigest);
55334
55398
  for (const path of [...seen].sort()) {
55335
55399
  hash.update(`${path}
55336
55400
  ${this.read(path).digest}
@@ -55349,20 +55413,20 @@ ${this.read(path).digest}
55349
55413
  return reached;
55350
55414
  }
55351
55415
  addon(root) {
55352
- const hash = createHash5("sha256");
55416
+ const hash = createHash6("sha256");
55353
55417
  const walk = (directory, under) => {
55354
- for (const entry of readdirSync10(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
55355
- const file = join19(directory, entry.name);
55418
+ for (const entry of readdirSync11(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
55419
+ const file = join20(directory, entry.name);
55356
55420
  if (entry.isDirectory()) {
55357
55421
  walk(file, `${under}${entry.name}/`);
55358
55422
  } else if (entry.isFile()) {
55359
55423
  hash.update(`${under}${entry.name}
55360
- `).update(readFileSync14(file));
55424
+ `).update(readFileSync15(file));
55361
55425
  }
55362
55426
  }
55363
55427
  };
55364
- const directory = join19(this.scripts.projectPath, root.slice("res://".length));
55365
- if (existsSync16(directory)) {
55428
+ const directory = join20(this.scripts.projectPath, root.slice("res://".length));
55429
+ if (existsSync17(directory)) {
55366
55430
  walk(directory, "");
55367
55431
  }
55368
55432
  return { digest: hash.digest("hex"), reaches: [], unreadable: null };
@@ -55371,26 +55435,26 @@ ${this.read(path).digest}
55371
55435
  if (PLACEHOLDER.test(path)) {
55372
55436
  return { digest: "", reaches: [], unreadable: `${path} is filled in at run time` };
55373
55437
  }
55374
- const file = join19(this.scripts.projectPath, path.slice("res://".length));
55375
- if (!existsSync16(file)) {
55438
+ const file = join20(this.scripts.projectPath, path.slice("res://".length));
55439
+ if (!existsSync17(file)) {
55376
55440
  return { digest: "missing", reaches: [], unreadable: null };
55377
55441
  }
55378
- if (statSync10(file).isDirectory()) {
55379
- const inside = readdirSync10(file, { withFileTypes: true }).filter((entry) => !entry.name.startsWith(".")).map((entry) => `${path.endsWith("/") ? path : `${path}/`}${entry.name}`).sort();
55442
+ if (statSync11(file).isDirectory()) {
55443
+ const inside = readdirSync11(file, { withFileTypes: true }).filter((entry) => !entry.name.startsWith(".")).map((entry) => `${path.endsWith("/") ? path : `${path}/`}${entry.name}`).sort();
55380
55444
  return { digest: inside.join(`
55381
55445
  `), reaches: inside, unreadable: null };
55382
55446
  }
55383
55447
  if (BINARY_RESOURCE.test(path)) {
55384
55448
  return { digest: "", reaches: [], unreadable: `${path} is a binary resource` };
55385
55449
  }
55386
- const bytes = readFileSync14(file);
55387
- const digest = createHash5("sha256").update(bytes).digest("hex");
55450
+ const bytes = readFileSync15(file);
55451
+ const digest = createHash6("sha256").update(bytes).digest("hex");
55388
55452
  const text = bytes.toString("latin1");
55389
55453
  const reaches = new Set;
55390
55454
  for (const [literal] of text.matchAll(RES_LITERAL)) {
55391
55455
  reaches.add(literal);
55392
55456
  }
55393
- if (existsSync16(`${file}.import`)) {
55457
+ if (existsSync17(`${file}.import`)) {
55394
55458
  reaches.add(`${path}.import`);
55395
55459
  }
55396
55460
  if (path.endsWith(".gd")) {
@@ -55420,8 +55484,8 @@ ${this.read(path).digest}
55420
55484
  }
55421
55485
  }
55422
55486
  function extensionsOf(projectPath) {
55423
- const list = join19(projectPath, ".godot", "extension_list.cfg");
55424
- return existsSync16(list) ? readFileSync14(list, "utf8").split(`
55487
+ const list = join20(projectPath, ".godot", "extension_list.cfg");
55488
+ return existsSync17(list) ? readFileSync15(list, "utf8").split(`
55425
55489
  `).map((line) => line.trim()).filter((line) => line.startsWith("res://")) : [];
55426
55490
  }
55427
55491
 
@@ -55430,18 +55494,18 @@ init_suite_judge();
55430
55494
  init_suite_plan();
55431
55495
 
55432
55496
  // src/version.ts
55433
- import { readFileSync as readFileSync17 } from "node:fs";
55434
- import { dirname as dirname11, join as join20 } from "node:path";
55497
+ import { readFileSync as readFileSync18 } from "node:fs";
55498
+ import { dirname as dirname11, join as join21 } from "node:path";
55435
55499
  import { fileURLToPath as fileURLToPath2 } from "node:url";
55436
55500
  function getLocalVersion() {
55437
55501
  const currentDirectory = dirname11(fileURLToPath2(import.meta.url));
55438
55502
  const packageCandidates = [
55439
- join20(currentDirectory, "..", "package.json"),
55440
- join20(currentDirectory, "..", "..", "package.json")
55503
+ join21(currentDirectory, "..", "package.json"),
55504
+ join21(currentDirectory, "..", "..", "package.json")
55441
55505
  ];
55442
55506
  for (const packagePath of packageCandidates) {
55443
55507
  try {
55444
- const value = JSON.parse(readFileSync17(packagePath, "utf-8"));
55508
+ const value = JSON.parse(readFileSync18(packagePath, "utf-8"));
55445
55509
  if (typeof value === "object" && value !== null && "name" in value && value.name === "gdharness" && "version" in value && typeof value.version === "string") {
55446
55510
  return value.version;
55447
55511
  }
@@ -55493,9 +55557,9 @@ function testOptionsOf(args, cwd) {
55493
55557
  }
55494
55558
  const first = positional[0];
55495
55559
  const named = first === undefined ? null : resolve4(cwd, first);
55496
- const projectNamed = named !== null && existsSync18(join21(named, "project.godot"));
55560
+ const projectNamed = named !== null && existsSync19(join22(named, "project.godot"));
55497
55561
  const projectPath = projectNamed ? named : resolve4(cwd);
55498
- if (!existsSync18(join21(projectPath, "project.godot"))) {
55562
+ if (!existsSync19(join22(projectPath, "project.godot"))) {
55499
55563
  throw new TestUsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
55500
55564
  }
55501
55565
  const last = (flag) => valued.get(flag)?.at(-1);
@@ -55539,7 +55603,7 @@ function resPathOf(entry, projectPath, cwd) {
55539
55603
  const file = caseAt?.[1] ?? entry;
55540
55604
  const fromProject = resolve4(projectPath, file);
55541
55605
  const fromHere = resolve4(cwd, file);
55542
- const absolute = isAbsolute4(file) || !existsSync18(fromProject) && existsSync18(fromHere) ? fromHere : fromProject;
55606
+ const absolute = isAbsolute4(file) || !existsSync19(fromProject) && existsSync19(fromHere) ? fromHere : fromProject;
55543
55607
  const inside = relative3(projectPath, absolute);
55544
55608
  if (inside.startsWith("..") || isAbsolute4(inside)) {
55545
55609
  throw new TestUsageError(`${entry} is not inside the project at ${projectPath}.`);
@@ -55551,7 +55615,7 @@ function homeOf(projectPath) {
55551
55615
  return mainCheckoutOf(projectPath) ?? projectPath;
55552
55616
  }
55553
55617
  function selectedSuites(scripts, options) {
55554
- const roots = options.roots.length > 0 ? options.roots : TEST_DIRECTORIES.filter((root) => existsSync18(join21(options.projectPath, root))).map((root) => `res://${root}`);
55618
+ const roots = options.roots.length > 0 ? options.roots : TEST_DIRECTORIES.filter((root) => existsSync19(join22(options.projectPath, root))).map((root) => `res://${root}`);
55555
55619
  if (options.selected.length === 0) {
55556
55620
  const found = scripts.suitesUnder(roots);
55557
55621
  if (found.length === 0) {
@@ -55563,10 +55627,10 @@ function selectedSuites(scripts, options) {
55563
55627
  for (const entry of options.selected) {
55564
55628
  const caseAt = /^(res:\/\/.*\.gd):(\w+)$/.exec(entry);
55565
55629
  const path = caseAt?.[1] ?? entry;
55566
- const directory = join21(options.projectPath, path.slice(6));
55567
- const suites = existsSync18(directory) && statSync11(directory).isDirectory() ? scripts.suitesUnder([path.replace(/\/$/, "")]) : [scripts.suite(path)].filter((suite) => suite !== null);
55630
+ const directory = join22(options.projectPath, path.slice(6));
55631
+ const suites = existsSync19(directory) && statSync12(directory).isDirectory() ? scripts.suitesUnder([path.replace(/\/$/, "")]) : [scripts.suite(path)].filter((suite) => suite !== null);
55568
55632
  if (suites.length === 0) {
55569
- throw new TestUsageError(existsSync18(directory) ? `${path} holds no gdUnit4 suite: nothing there extends GdUnitTestSuite and declares a test_ function.` : `Nothing at ${path} in the project at ${options.projectPath}.`);
55633
+ throw new TestUsageError(existsSync19(directory) ? `${path} holds no gdUnit4 suite: nothing there extends GdUnitTestSuite and declares a test_ function.` : `Nothing at ${path} in the project at ${options.projectPath}.`);
55570
55634
  }
55571
55635
  for (const suite of suites) {
55572
55636
  const picked = chosen.get(suite.path) ?? { suite, cases: new Set };
@@ -55643,17 +55707,17 @@ async function runTestCommand(options, godotPath) {
55643
55707
  const startedAt = Date.now();
55644
55708
  const scripts = new ProjectScripts(options.projectPath);
55645
55709
  const home = homeOf(options.projectPath);
55646
- const state = join21(home, ".godot", "gdharness");
55647
- const timesFile = join21(state, "suite-times.json");
55648
- const passedFile = join21(state, "suite-passed.json");
55710
+ const state = join22(home, ".godot", "gdharness");
55711
+ const timesFile = join22(state, "suite-times.json");
55712
+ const passedFile = join22(state, "suite-passed.json");
55649
55713
  let suites = selectedSuites(scripts, options);
55650
55714
  const wholeSuites = suites.map((suite) => scripts.suite(suite.path) ?? suite);
55651
55715
  const declared = new Map(wholeSuites.map((suite) => [suite.path, suite.cases]));
55652
- const testRuns = join21(options.projectPath, ".godot", "gdharness", "test-runs");
55716
+ const testRuns = join22(options.projectPath, ".godot", "gdharness", "test-runs");
55653
55717
  sweepRunDirectories(testRuns);
55654
- const runDirectory = join21(testRuns, `${String(startedAt)}-${String(process12.pid)}`);
55718
+ const runDirectory = join22(testRuns, `${String(startedAt)}-${String(process12.pid)}`);
55655
55719
  mkdirSync8(runDirectory, { recursive: true });
55656
- const slots = new EngineSlots(options.slotsDirectory ?? join21(home, ".godot", "gate", "slots"), options.slotCount ?? Math.min(MOST_ENGINES, availableParallelism()), { pid: process12.pid, started: await ownStart() });
55720
+ const slots = new EngineSlots(options.slotsDirectory ?? join22(home, ".godot", "gate", "slots"), options.slotCount ?? Math.min(MOST_ENGINES, availableParallelism()), { pid: process12.pid, started: await ownStart() });
55657
55721
  const running = new Set;
55658
55722
  const interrupted = () => {
55659
55723
  for (const end of running) {
@@ -55682,9 +55746,9 @@ async function runTestCommand(options, godotPath) {
55682
55746
  }
55683
55747
  const fingerprints = new SuiteFingerprints(scripts, [
55684
55748
  getLocalVersion(),
55685
- readFileSync18(shippedListedRunner(), "utf8"),
55749
+ readFileSync19(shippedListedRunner(), "utf8"),
55686
55750
  ...options.reachAlso.map((path) => `${path}
55687
- ${existsSync18(path) ? readFileSync18(path, "latin1") : "missing"}`)
55751
+ ${existsSync19(path) ? readFileSync19(path, "latin1") : "missing"}`)
55688
55752
  ]);
55689
55753
  const printsOf = new Map(suites.map((suite) => [suite.path, fingerprints.of(suite)]));
55690
55754
  if (options.changedOnly) {
@@ -55709,7 +55773,7 @@ ${existsSync18(path) ? readFileSync18(path, "latin1") : "missing"}`)
55709
55773
  const plans = options.order === null ? dealt(units, declared, room.engines) : [inOneEngine(ordered(units, options.order), declared)];
55710
55774
  const functions = suites.reduce((sum, suite) => sum + suite.cases.length, 0);
55711
55775
  const held = options.order === null && room.held !== undefined && plans.length === room.engines ? `, of the ${String(options.engines)} asked for, since ${room.held}` : "";
55712
- const how = options.order === null ? `${plans.some((plan) => plan.windowed) ? ", one of them windowed" : ""}${held}` : options.order === "reversed" ? ", in reverse order of path" : `, shuffled by ${String(options.order)}`;
55776
+ const how = options.order === null ? `${windowedSaid(plans)}${held}` : options.order === "reversed" ? ", in reverse order of path" : `, shuffled by ${String(options.order)}`;
55713
55777
  console.log(`${plural(functions, "test function")} in ${plural(suites.length, "suite")} over ${plural(plans.length, "engine")}${how}.`);
55714
55778
  const split = [...new Set(units.map((unit) => unit.suite))].filter((path) => units.filter((unit) => unit.suite === path).length > 1);
55715
55779
  if (plans.length > 1 && split.length > 0) {
@@ -55737,7 +55801,7 @@ function plural(count, noun) {
55737
55801
  }
55738
55802
  function readRecord(file) {
55739
55803
  try {
55740
- const parsed = JSON.parse(readFileSync18(file, "utf8"));
55804
+ const parsed = JSON.parse(readFileSync19(file, "utf8"));
55741
55805
  return typeof parsed === "object" && parsed !== null ? parsed : {};
55742
55806
  } catch {
55743
55807
  return {};
@@ -55764,20 +55828,20 @@ function recordPassed(file, ran, printsOf) {
55764
55828
  writeJson(file, passed);
55765
55829
  }
55766
55830
  function sweepRunDirectories(testRuns) {
55767
- if (!existsSync18(testRuns)) {
55831
+ if (!existsSync19(testRuns)) {
55768
55832
  return;
55769
55833
  }
55770
- for (const entry of readdirSync11(testRuns)) {
55834
+ for (const entry of readdirSync12(testRuns)) {
55771
55835
  const pid = Number(/^\d+-(\d+)$/.exec(entry)?.[1]);
55772
55836
  if (!Number.isInteger(pid) || !alive(pid)) {
55773
- rmSync10(join21(testRuns, entry), { recursive: true, force: true });
55837
+ rmSync10(join22(testRuns, entry), { recursive: true, force: true });
55774
55838
  }
55775
55839
  }
55776
55840
  }
55777
55841
  async function importProject(launch) {
55778
- const directory = join21(launch.runDirectory, "import");
55779
- mkdirSync8(join21(directory, "user"), { recursive: true });
55780
- const log = join21(directory, "import.log");
55842
+ const directory = join22(launch.runDirectory, "import");
55843
+ mkdirSync8(join22(directory, "user"), { recursive: true });
55844
+ const log = join22(directory, "import.log");
55781
55845
  const lock = await launch.slots.take();
55782
55846
  try {
55783
55847
  const ended = await watched(launch, ["--headless", "--audio-driver", "Dummy", "--path", launch.projectPath, "--import"], directory, log, false, launch.silenceFloorSeconds);
@@ -55790,11 +55854,11 @@ async function importProject(launch) {
55790
55854
  }
55791
55855
  }
55792
55856
  async function runEngine2(launch, plan, index, units, again = "") {
55793
- const directory = join21(launch.runDirectory, `engine-${String(index)}${again === "" ? "" : `-${again}`}`);
55794
- mkdirSync8(join21(directory, "user"), { recursive: true });
55795
- const suitesFile = join21(directory, "suites.txt");
55796
- const skippingFile = join21(directory, "skipping.txt");
55797
- const expectedFile = join21(directory, "expected.json");
55857
+ const directory = join22(launch.runDirectory, `engine-${String(index)}${again === "" ? "" : `-${again}`}`);
55858
+ mkdirSync8(join22(directory, "user"), { recursive: true });
55859
+ const suitesFile = join22(directory, "suites.txt");
55860
+ const skippingFile = join22(directory, "skipping.txt");
55861
+ const expectedFile = join22(directory, "expected.json");
55798
55862
  writeFileSync10(suitesFile, `${plan.suites.map((suite) => suite.path).join(`
55799
55863
  `)}
55800
55864
  `);
@@ -55814,7 +55878,7 @@ async function runEngine2(launch, plan, index, units, again = "") {
55814
55878
  ...plan.windowed ? [] : ["--ignoreHeadlessMode"],
55815
55879
  "-c",
55816
55880
  "-rd",
55817
- `res://${relative3(launch.projectPath, join21(directory, "reports")).split(sep2).join("/")}`,
55881
+ `res://${relative3(launch.projectPath, join22(directory, "reports")).split(sep2).join("/")}`,
55818
55882
  "-rc",
55819
55883
  "1",
55820
55884
  "--",
@@ -55831,7 +55895,7 @@ async function runEngine2(launch, plan, index, units, again = "") {
55831
55895
  const held = new Set(plan.suites.map((suite) => suite.path));
55832
55896
  const slowest = Math.max(0, ...units.filter((unit) => held.has(unit.suite)).map((unit) => unit.cost));
55833
55897
  const silence = Math.max(launch.silenceFloorSeconds, SILENCE_TIMES_SLOWEST * slowest);
55834
- const log = join21(directory, "engine.log");
55898
+ const log = join22(directory, "engine.log");
55835
55899
  const lock = await launch.slots.take();
55836
55900
  const began = Date.now();
55837
55901
  let end;
@@ -55840,7 +55904,7 @@ async function runEngine2(launch, plan, index, units, again = "") {
55840
55904
  } finally {
55841
55905
  launch.slots.release(lock);
55842
55906
  }
55843
- const printed = readRun(readFileSync18(log, "utf8"));
55907
+ const printed = readRun(readFileSync19(log, "utf8"));
55844
55908
  const assignment = { suites: plan.suites, expected: readExpected(expectedFile) };
55845
55909
  return {
55846
55910
  index,
@@ -55852,7 +55916,7 @@ async function runEngine2(launch, plan, index, units, again = "") {
55852
55916
  };
55853
55917
  }
55854
55918
  async function watched(launch, args, directory, log, windowed, silenceSeconds) {
55855
- const user = join21(directory, "user");
55919
+ const user = join22(directory, "user");
55856
55920
  const env = { ...process12.env, APPDATA: user, XDG_DATA_HOME: user };
55857
55921
  const output = openSync3(log, "w");
55858
55922
  let child;
@@ -55864,12 +55928,12 @@ async function watched(launch, args, directory, log, windowed, silenceSeconds) {
55864
55928
  const how = windowed ? "on a desktop of its own" : "through the desktop helper";
55865
55929
  return { code: null, signal: null, ended: `the engine could not be started ${how}: ${compiled}` };
55866
55930
  }
55867
- const stopFile = join21(directory, "stop");
55931
+ const stopFile = join22(directory, "stop");
55868
55932
  const how = windowed ? { desktop: HIDDEN_DESKTOP, directory: launch.projectPath } : { directory: launch.projectPath };
55869
55933
  const wrapper = throughHelper(how, launch.godotPath, args, {
55870
- spec: join21(directory, "spec.json"),
55871
- pid: join21(directory, "pid"),
55872
- error: join21(directory, "error"),
55934
+ spec: join22(directory, "spec.json"),
55935
+ pid: join22(directory, "pid"),
55936
+ error: join22(directory, "error"),
55873
55937
  stop: stopFile
55874
55938
  });
55875
55939
  child = spawnHidden(wrapper.command, wrapper.args, {