gdharness 1.2.39 → 1.2.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/build/cli.js +1435 -1410
  2. package/build/godot/addons/gdharness_editor/animation_reads.gd +11 -2
  3. package/build/godot/addons/gdharness_editor/bridge_client.gd +2 -1
  4. package/build/godot/addons/gdharness_editor/headless_check.gd +22 -11
  5. package/build/godot/addons/gdharness_editor/property_values.gd +1 -1
  6. package/build/godot/addons/gdharness_editor/reading.gd +15 -0
  7. package/build/godot/addons/gdharness_editor/scene_file.gd +9 -0
  8. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +39 -5
  9. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +68 -15
  10. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +12 -12
  11. package/build/godot/addons/gdharness_runtime/reading.gd +15 -0
  12. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +3 -1
  13. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +2 -2
  14. package/build/godot/addons/gdharness_runtime/runtime_input.gd +15 -2
  15. package/build/godot/addons/gdharness_runtime/runtime_items.gd +175 -0
  16. package/build/godot/addons/gdharness_runtime/runtime_items.gd.uid +1 -0
  17. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +43 -10
  18. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +61 -20
  19. package/build/godot/addons/gdharness_runtime/runtime_values.gd +3 -1
  20. package/build/godot/addons/gdharness_runtime/runtime_words.gd +48 -18
  21. package/build/godot/operations/animation_reads.gd +11 -2
  22. package/build/godot/operations/editor_tools.gd +9 -0
  23. package/build/godot/operations/godot_operations.gd +2 -1
  24. package/build/godot/operations/property_values.gd +1 -1
  25. package/build/godot/operations/reading.gd +15 -0
  26. package/build/index.js +2789 -2765
  27. package/package.json +1 -1
package/build/cli.js CHANGED
@@ -8577,512 +8577,1127 @@ var init_scratch = __esm(() => {
8577
8577
  ];
8578
8578
  });
8579
8579
 
8580
- // src/headless.ts
8581
- import { existsSync as existsSync9, readFileSync as readFileSync5, realpathSync as realpathSync2, writeFileSync as writeFileSync4 } from "node:fs";
8582
- import { dirname as dirname6, join as join9 } from "node:path";
8583
- function snakeCased(params) {
8584
- const result = emptyRecord();
8585
- for (const [key, value] of Object.entries(params)) {
8586
- const name = key.startsWith("_") ? key : key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
8587
- result[name] = value;
8588
- }
8589
- return result;
8590
- }
8591
- function engineExtras(outcome) {
8592
- const after = outcome.afterAnswerMessages ?? [];
8593
- const before = outcome.beforeOperationMessages ?? [];
8594
- return {
8595
- ...outcome.messages.length > 0 ? { engine_messages: outcome.messages } : {},
8596
- ...before.length > 0 ? {
8597
- engine_messages_before_operation: before,
8598
- engine_messages_before_operation_note: BEFORE_OPERATION_NOTE
8599
- } : {},
8600
- ...outcome.beforeOperationOmitted === undefined ? {} : { engine_messages_before_operation_omitted: outcome.beforeOperationOmitted },
8601
- ...outcome.messagesOmitted === undefined ? {} : { engine_messages_omitted: outcome.messagesOmitted },
8602
- ...after.length > 0 ? { engine_messages_after_answer: after, engine_messages_after_answer_note: AFTER_ANSWER_NOTE } : {},
8603
- ...outcome.afterAnswerOmitted === undefined ? {} : { engine_messages_after_answer_omitted: outcome.afterAnswerOmitted },
8604
- ...outcome.afterAnswer === undefined ? {} : { engine_after_answer: outcome.afterAnswer }
8605
- };
8606
- }
8607
- function problemsIn(log) {
8608
- return problemsAmong(log.everything());
8609
- }
8610
- function problemsAmong(entries) {
8611
- return entries.flatMap((entry) => {
8612
- if (entry.severity !== "info") {
8613
- return [entry];
8580
+ // src/junit.ts
8581
+ function decoded(text) {
8582
+ return text.replace(/&(#x[0-9a-fA-F]+|#[0-9]+|[a-zA-Z]+);/g, (whole, entity) => {
8583
+ if (entity.startsWith("#")) {
8584
+ const code = entity.startsWith("#x") ? Number.parseInt(entity.slice(2), 16) : Number.parseInt(entity.slice(1), 10);
8585
+ if (!Number.isInteger(code) || code < 0 || code > 1114111) {
8586
+ throw new MalformedReportError(`Not a character: ${whole}`);
8587
+ }
8588
+ return String.fromCodePoint(code);
8614
8589
  }
8615
- if (entry.source === "stderr" && entry.text.startsWith(OWN_ERROR)) {
8616
- return [{ ...entry, severity: "error", text: entry.text.slice(OWN_ERROR.length) }];
8590
+ const named = ENTITIES[entity];
8591
+ if (named === undefined) {
8592
+ throw new MalformedReportError(`Unknown entity ${whole}`);
8617
8593
  }
8618
- return [];
8619
- });
8620
- }
8621
- function reported2(problems) {
8622
- const omitted = Math.max(0, problems.length - MESSAGES_KEPT);
8623
- return omitted === 0 ? { messages: problems } : { messages: problems.slice(omitted), messagesOmitted: omitted };
8624
- }
8625
- function reason(log, problems) {
8626
- const own = refusalsIn(log);
8627
- if (own.length > 0) {
8628
- return own.map((entry) => entry.text.slice(OWN_ERROR.length)).join("; ");
8629
- }
8630
- const errors = problems.filter((entry) => entry.severity === "error").map((entry) => entry.text);
8631
- if (errors.length > 0) {
8632
- const rest = errors.length - ERRORS_NAMED;
8633
- return rest > 0 ? `${errors.slice(0, ERRORS_NAMED).join("; ")}; and ${rest} more error${rest === 1 ? "" : "s"}` : errors.join("; ");
8634
- }
8635
- const printed = log.everything().filter((entry) => entry.source === "stdout");
8636
- return printed.at(-1)?.text ?? "no output at all";
8637
- }
8638
- function refusalsIn(log) {
8639
- return log.everything().filter((entry) => entry.source === "stderr" && entry.severity === "info" && entry.text.startsWith(OWN_ERROR));
8640
- }
8641
- function scriptFault(problems, script) {
8642
- const spelled = (path) => `(${path.replaceAll("\\", "/").toLowerCase()}/`;
8643
- const directories = [...new Set([spelled(dirname6(script)), spelled(resolvedOr(dirname6(script)))])];
8644
- return problems.find((entry) => {
8645
- const at = entry.detail.find((line) => line.startsWith("at: "))?.replaceAll("\\", "/").toLowerCase();
8646
- return entry.severity === "error" && at !== undefined && directories.some((directory) => at.includes(directory));
8594
+ return named;
8647
8595
  });
8648
8596
  }
8649
- function resolvedOr(path) {
8650
- try {
8651
- return realpathSync2.native(path);
8652
- } catch {
8653
- return path;
8597
+ function tagEnd(source, from) {
8598
+ let quote = null;
8599
+ for (let at = from + 1;at < source.length; at += 1) {
8600
+ const character = source[at];
8601
+ if (quote !== null) {
8602
+ if (character === quote) {
8603
+ quote = null;
8604
+ }
8605
+ } else if (character === '"' || character === "'") {
8606
+ quote = character;
8607
+ } else if (character === ">") {
8608
+ return at;
8609
+ }
8654
8610
  }
8611
+ return -1;
8655
8612
  }
8656
- function librariesNotCopied(messages) {
8657
- const found = new Set;
8658
- for (const entry of messages) {
8659
- const library = /Error copying library: (.+)$/.exec(entry.text)?.[1]?.trim();
8660
- if (library !== undefined && library !== "") {
8661
- found.add(library);
8613
+ function parseXml(source) {
8614
+ const root = { name: "", attributes: {}, children: [], text: "" };
8615
+ const open = [root];
8616
+ let at = 0;
8617
+ const current = () => {
8618
+ const element = open.at(-1);
8619
+ if (element === undefined) {
8620
+ throw new MalformedReportError("A close tag with nothing open");
8621
+ }
8622
+ return element;
8623
+ };
8624
+ while (at < source.length) {
8625
+ const next = source.indexOf("<", at);
8626
+ if (next === -1) {
8627
+ current().text += decoded(source.slice(at));
8628
+ break;
8629
+ }
8630
+ current().text += decoded(source.slice(at, next));
8631
+ at = next;
8632
+ if (source.startsWith("<![CDATA[", at)) {
8633
+ const end = source.indexOf("]]>", at);
8634
+ if (end === -1) {
8635
+ throw new MalformedReportError("CDATA that never ends");
8636
+ }
8637
+ current().text += source.slice(at + "<![CDATA[".length, end);
8638
+ at = end + "]]>".length;
8639
+ continue;
8640
+ }
8641
+ if (source.startsWith("<!--", at)) {
8642
+ const end = source.indexOf("-->", at);
8643
+ if (end === -1) {
8644
+ throw new MalformedReportError("A comment that never ends");
8645
+ }
8646
+ at = end + "-->".length;
8647
+ continue;
8648
+ }
8649
+ if (source.startsWith("<?", at) || source.startsWith("<!", at)) {
8650
+ const end = source.indexOf(">", at);
8651
+ if (end === -1) {
8652
+ throw new MalformedReportError("A declaration that never ends");
8653
+ }
8654
+ at = end + 1;
8655
+ continue;
8656
+ }
8657
+ const end = tagEnd(source, at);
8658
+ if (end === -1) {
8659
+ throw new MalformedReportError("A tag that never ends");
8660
+ }
8661
+ const tag = source.slice(at + 1, end);
8662
+ at = end + 1;
8663
+ if (tag.startsWith("/")) {
8664
+ const closing = tag.slice(1).trim();
8665
+ const element = current();
8666
+ if (element === root || element.name !== closing) {
8667
+ throw new MalformedReportError(`</${closing}> closes <${element.name || "nothing"}>`);
8668
+ }
8669
+ open.pop();
8670
+ continue;
8671
+ }
8672
+ const selfClosing = tag.endsWith("/");
8673
+ const body = selfClosing ? tag.slice(0, -1) : tag;
8674
+ const nameMatch = /^([^\s/>]+)/.exec(body);
8675
+ if (!nameMatch?.[1]) {
8676
+ throw new MalformedReportError(`A tag with no name: <${tag}>`);
8677
+ }
8678
+ const attributes = {};
8679
+ let read = nameMatch[0].length;
8680
+ ATTRIBUTE.lastIndex = read;
8681
+ let attribute = ATTRIBUTE.exec(body);
8682
+ while (attribute) {
8683
+ attributes[attribute[1] ?? ""] = decoded(attribute[2] ?? attribute[3] ?? "");
8684
+ read = ATTRIBUTE.lastIndex;
8685
+ attribute = ATTRIBUTE.exec(body);
8686
+ }
8687
+ if (body.slice(read).trim() !== "") {
8688
+ throw new MalformedReportError(`Unreadable attributes in <${tag}>`);
8689
+ }
8690
+ const element = { name: nameMatch[1], attributes, children: [], text: "" };
8691
+ current().children.push(element);
8692
+ if (!selfClosing) {
8693
+ open.push(element);
8662
8694
  }
8663
8695
  }
8664
- return [...found];
8696
+ if (open.length !== 1) {
8697
+ throw new MalformedReportError(`<${current().name}> is never closed`);
8698
+ }
8699
+ return root;
8665
8700
  }
8666
- function extensionRetryNote(libraries, stillNotLoaded = []) {
8667
- const second = stillNotLoaded.length === 0 ? "so the import was run again and loaded the extension, and the messages are from that second run." : `so the import was run again, and it still could not load ${stillNotLoaded.join(" and ")}: this answer is from an import without ${stillNotLoaded.length === 1 ? "it" : "them"}, and its messages say what the engine hit.`;
8668
- return `The first import could not load ${libraries.join(" and ")}: on Windows an engine running as the editor ` + "loads a GDExtension from a ~ copy beside the library, and the editor open on this project holds its own " + "copy there, so the first engine started beside it cannot make one. That failed attempt moves the " + `editor's copy aside, ${second} The editor's copy is left beside the library as a ~RF….TMP file, which ` + "the next import, export or editor this server starts on the project deletes once the editor has exited.";
8701
+ function count(attributes, name) {
8702
+ const value = Number(attributes[name] ?? "0");
8703
+ return Number.isFinite(value) ? value : 0;
8669
8704
  }
8670
- async function runImport(godotPath, projectPath, options = {}) {
8671
- const once = options.once ?? importOnce;
8672
- const pass = () => {
8673
- for (const target of options.forcing ?? []) {
8674
- forceNextImport(projectPath, target);
8705
+ function ignoredYetRan(report, ignored) {
8706
+ const found = [];
8707
+ for (const entry of ignored) {
8708
+ const bare = entry.replace("res://", "");
8709
+ const at = bare.lastIndexOf(":");
8710
+ const suiteNamed = at === -1 ? null : bare.slice(0, at);
8711
+ const testNamed = at === -1 ? bare : bare.slice(at + 1);
8712
+ for (const suite of report.suites) {
8713
+ const suiteMatches = suiteNamed === null || suite.name === suiteNamed || (suite.path ?? "").includes(suiteNamed);
8714
+ const ran = suiteMatches ? suite.cases.find((one) => one.name === testNamed) : undefined;
8715
+ if (ran !== undefined) {
8716
+ found.push({ entry, ran: `${suite.name}:${ran.name}` });
8717
+ }
8675
8718
  }
8676
- freeLibraryCopies(projectPath);
8677
- return once(godotPath, projectPath);
8678
- };
8679
- const first = await pass();
8680
- const collided = librariesNotCopied(first.messages);
8681
- const crash = first.docCacheNote;
8682
- if (collided.length === 0 && crash === undefined) {
8683
- return first;
8684
8719
  }
8685
- const { docCacheNote: _, ...again } = await pass();
8686
- const second = again.ok || crash === undefined ? again : { ...again, message: `${again.message.replace(/[.\s]+$/, "")}. ${crash}` };
8687
- const still = librariesNotCopied(second.messages);
8688
- return {
8689
- ...second,
8690
- ...collided.length === 0 ? {} : {
8691
- librariesRetried: collided,
8692
- ...still.length === 0 ? {} : { librariesNotLoaded: still },
8693
- extensionNote: extensionRetryNote(collided, still)
8694
- },
8695
- ...crash === undefined ? {} : { docCacheNote: crash }
8696
- };
8720
+ return found;
8697
8721
  }
8698
- async function importOnce(godotPath, projectPath) {
8699
- const logDir = scratchDirectory("import");
8700
- const caches = editorCacheDirectory(godotPath);
8701
- const cachesBefore = docCachesWritten(caches);
8702
- let ran;
8703
- try {
8704
- ran = await runEngine(godotPath, [
8705
- "--headless",
8706
- "--log-file",
8707
- join9(logDir, "engine.log"),
8708
- "--path",
8709
- projectPath,
8710
- "--import"
8711
- ]);
8712
- } finally {
8713
- discard(logDir);
8714
- }
8715
- const problems = problemsIn(ran.log);
8716
- const crash = docCacheCrash(ran, cachesBefore, docCachesWritten(caches), "import");
8717
- if (ran.exitCode === 0 && ran.failure === null) {
8718
- return { ok: true, ...reported2(problems), ...crash === null ? {} : { docCacheNote: crash } };
8719
- }
8722
+ function caseOf(suite, element) {
8723
+ const of = (kind) => element.children.filter((child) => child.name === kind).map((child) => {
8724
+ const detail = child.text.trim();
8725
+ return { kind, message: child.attributes["message"] ?? null, detail: detail === "" ? null : detail };
8726
+ });
8727
+ const reports = [...of("failure"), ...of("error"), ...of("skipped")];
8728
+ const status = reports.some((report) => report.kind === "error") ? "error" : reports.some((report) => report.kind === "failure") ? "failed" : reports.length > 0 ? "skipped" : "passed";
8720
8729
  return {
8721
- ok: false,
8722
- message: `the import pass failed (${howItEnded(ran)}): ${reason(ran.log, problems)}`,
8723
- ...reported2(problems),
8724
- ...crash === null ? {} : { docCacheNote: crash }
8730
+ suite: suite.attributes["name"] ?? "",
8731
+ name: element.attributes["name"] ?? "",
8732
+ status,
8733
+ time: count(element.attributes, "time"),
8734
+ message: reports[0]?.message ?? null,
8735
+ detail: reports[0]?.detail ?? null,
8736
+ reports
8725
8737
  };
8726
8738
  }
8727
- async function runOperation(engine, operation, params, projectPath) {
8728
- const paramsDir = scratchDirectory("params");
8729
- const paramsFile = join9(paramsDir, `${operation}.json`);
8730
- const answerFile = join9(paramsDir, "answer.json");
8731
- writeFileSync4(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
8732
- const args = [
8733
- "--headless",
8734
- "--log-file",
8735
- join9(paramsDir, "engine.log"),
8736
- "--path",
8737
- projectPath,
8738
- "--script",
8739
- engine.script,
8740
- operation,
8741
- `@file:${paramsFile}`,
8742
- answerFile,
8743
- ...engine.debug ? ["--debug-godot"] : []
8744
- ];
8745
- let ran;
8746
- let answer;
8747
- try {
8748
- ran = await runEngine(engine.godotPath, args);
8749
- answer = answerIn(answerFile);
8750
- } finally {
8751
- discard(paramsDir);
8752
- }
8753
- const everything = ran.log.everything();
8754
- const marked = (text) => everything.findIndex((entry) => entry.source === "stderr" && entry.text === text);
8755
- const answered = marked(ANSWERED);
8756
- const waiting = marked(WAITING);
8757
- const begun = marked(BEGUN);
8758
- const ended = answered === -1 ? everything.length : answered;
8759
- const problems = waiting === -1 ? problemsAmong(everything.slice(0, ended)) : problemsAmong([
8760
- ...everything.slice(0, waiting),
8761
- ...begun === -1 ? [] : everything.slice(begun + 1, ended)
8762
- ]);
8763
- const startup = waiting === -1 ? [] : problemsAmong(everything.slice(waiting + 1, begun === -1 ? ended : begun));
8764
- const after = answered === -1 ? [] : problemsAmong(everything.slice(answered + 1));
8765
- const afterAnswered = {
8766
- ...after.length === 0 ? {} : { afterAnswerMessages: after.slice(-MESSAGES_KEPT) },
8767
- ...after.length > MESSAGES_KEPT ? { afterAnswerOmitted: after.length - MESSAGES_KEPT } : {},
8768
- ...startup.length === 0 ? {} : { beforeOperationMessages: startup.slice(-MESSAGES_KEPT) },
8769
- ...startup.length > MESSAGES_KEPT ? { beforeOperationOmitted: startup.length - MESSAGES_KEPT } : {}
8770
- };
8771
- if (answer.kind === "unreadable") {
8772
- return {
8773
- ok: false,
8774
- message: `${operation} wrote an answer that could not be read (${answer.why})`,
8775
- ...reported2(problems),
8776
- ...afterAnswered
8777
- };
8778
- }
8779
- if (answer.kind === "none") {
8780
- if (waiting !== -1 && begun === -1) {
8781
- return {
8782
- ok: false,
8783
- message: `${operation} never ran: the engine ended (${howItEnded(ran)}) while it started the project's ` + `autoloads, which this operation waits for. The last it reported: ${reason(ran.log, startup)}`,
8784
- ...reported2(problems),
8785
- ...afterAnswered
8786
- };
8787
- }
8788
- const refusals = refusalsIn(ran.log);
8789
- if (refusals.length > 0) {
8790
- const said = new Set(refusals.map((entry) => entry.index));
8791
- return {
8792
- ok: false,
8793
- message: refusals.map((entry) => entry.text.slice(OWN_ERROR.length)).join("; "),
8794
- ...reported2(problems.filter((entry) => !said.has(entry.index)))
8795
- };
8796
- }
8797
- const ending = ran.exitCode === 0 && ran.failure === null ? "produced no result" : `failed (${howItEnded(ran)})`;
8798
- return {
8799
- ok: false,
8800
- message: `${operation} ${ending}: ${reason(ran.log, problems)}`,
8801
- ...reported2(problems)
8802
- };
8739
+ function failedAnAssertion(entry) {
8740
+ return entry.reports.some((report) => report.kind === "failure");
8741
+ }
8742
+ function pathOf(suite) {
8743
+ const directory = suite.attributes["package"];
8744
+ const name = suite.attributes["name"];
8745
+ return directory && name ? `res://${directory}/${name}.gd` : null;
8746
+ }
8747
+ function parseJUnit(xml) {
8748
+ const document = parseXml(xml);
8749
+ const top = document.children.find((child) => child.name === "testsuites" || child.name === "testsuite");
8750
+ if (top === undefined) {
8751
+ throw new MalformedReportError("No <testsuites> or <testsuite> in the report");
8803
8752
  }
8804
- const fault = scriptFault(problems, engine.script);
8805
- if (fault !== undefined) {
8753
+ const suiteElements = top.name === "testsuite" ? [top] : top.children.filter((child) => child.name === "testsuite");
8754
+ const suites = suiteElements.map((suite) => {
8755
+ const cases = suite.children.filter((child) => child.name === "testcase").map((element) => caseOf(suite, element));
8756
+ const failures = cases.filter(failedAnAssertion).length;
8757
+ const errors = cases.filter((entry) => entry.status === "error").length;
8758
+ const written = (kind) => suite.children.filter((child) => child.name === "testcase").reduce((sum, element) => sum + element.children.filter((child) => child.name === kind).length, 0);
8806
8759
  return {
8807
- ok: false,
8808
- message: `${operation} hit an error in the operations script, so its answer is left out: part of it was ` + `never computed. ${fault.text} (${fault.detail[0]?.slice("at: ".length) ?? "no location given"})`,
8809
- ...reported2(problems),
8810
- ...afterAnswered
8760
+ name: suite.attributes["name"] ?? "",
8761
+ path: pathOf(suite),
8762
+ tests: cases.length,
8763
+ discovered: count(suite.attributes, "tests"),
8764
+ failures,
8765
+ errors,
8766
+ hookFailures: Math.max(0, count(suite.attributes, "failures") - written("failure")),
8767
+ hookErrors: Math.max(0, count(suite.attributes, "errors") - written("error")),
8768
+ skipped: cases.filter((entry) => entry.status === "skipped").length,
8769
+ time: count(suite.attributes, "time"),
8770
+ cases
8811
8771
  };
8812
- }
8813
- const afterAnswer = ran.exitCode === 0 && ran.failure === null ? undefined : `The operation finished and wrote this answer, and then the engine did not exit cleanly (${howItEnded(ran)}). Anything it reported on the way out is under ${answered === -1 ? "engine_messages" : "engine_messages_after_answer"}.`;
8772
+ });
8773
+ const all = suites.flatMap((suite) => suite.cases);
8814
8774
  return {
8815
- ok: true,
8816
- payload: answer.payload,
8817
- ...reported2(problems),
8818
- ...afterAnswered,
8819
- ...afterAnswer === undefined ? {} : { afterAnswer }
8775
+ tests: all.length,
8776
+ failures: all.filter(failedAnAssertion).length,
8777
+ errors: all.filter((entry) => entry.status === "error").length,
8778
+ hookFailures: suites.reduce((sum, suite) => sum + suite.hookFailures, 0),
8779
+ hookErrors: suites.reduce((sum, suite) => sum + suite.hookErrors, 0),
8780
+ skipped: all.filter((entry) => entry.status === "skipped").length,
8781
+ time: suites.reduce((sum, suite) => sum + suite.time, 0),
8782
+ suites
8820
8783
  };
8821
8784
  }
8822
- function answerIn(file) {
8823
- if (!existsSync9(file)) {
8824
- return { kind: "none" };
8825
- }
8826
- const text = readFileSync5(file, "utf8");
8827
- try {
8828
- const parsed = JSON.parse(text);
8829
- 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)}` };
8830
- } catch (error) {
8831
- return {
8832
- kind: "unreadable",
8833
- why: `${error instanceof Error ? error.message : String(error)}: ${text.slice(0, 200)}`
8834
- };
8835
- }
8836
- }
8837
- var AFTER_ANSWER_NOTE = "Printed after this operation had written its answer, while the engine ran the project's autoloads and shut down: Godot starts a project's autoloads in every script run and cannot be told not to. An error raised in a project script here is that autoload's own, and none of these changes the answer.", BEFORE_OPERATION_NOTE = "Printed while the engine started the project's autoloads, before this operation ran: an operation that compiles the project's scripts waits for them, because the engine knows an autoload's name only once it is in the tree. An error raised in a project script here is that autoload's own.", OWN_ERROR = "[ERROR] ", ANSWERED = "[gdharness] answered", WAITING = "[gdharness] waiting for the autoloads", BEGUN = "[gdharness] begun", MESSAGES_KEPT = 200, ERRORS_NAMED = 3;
8838
- var init_headless = __esm(() => {
8839
- init_doc_cache();
8840
- init_engine_run();
8841
- init_library_copies();
8842
- init_reimport();
8843
- init_scratch();
8844
- });
8845
-
8846
- // src/update-check.ts
8847
- import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
8848
- import { homedir as homedir4, tmpdir as tmpdir3 } from "node:os";
8849
- import { join as join10 } from "node:path";
8850
- function packageUrl(root) {
8851
- return `${root.replace(/\/+$/, "")}/gdharness/latest`;
8785
+ function hookFailuresPrinted(printed) {
8786
+ return printed.flatMap((line) => {
8787
+ const found = HOOK_FAILURE.exec(line);
8788
+ return found === null ? [] : [{ hook: found[2] ?? "", path: found[3] ?? "", line: Number(found[4]), message: found[1] ?? "" }];
8789
+ });
8852
8790
  }
8853
- function registryFor(environment) {
8854
- const named = environment["GDHARNESS_REGISTRY"] ?? "";
8855
- if (named === "") {
8856
- return packageUrl(REGISTRY_ROOT);
8857
- }
8858
- try {
8859
- return new URL(named).protocol === "https:" ? packageUrl(named) : packageUrl(REGISTRY_ROOT);
8860
- } catch {
8861
- return packageUrl(REGISTRY_ROOT);
8791
+ function whyNoReport(printed, asked) {
8792
+ const missing = printed.map((line) => NO_SUCH_PATH.exec(line)).find((found) => found !== null);
8793
+ if (missing !== undefined) {
8794
+ return `nothing at ${missing[1] ?? asked}`;
8862
8795
  }
8796
+ return printed.some((line) => NOTHING_RAN.test(line)) ? `no test cases found at ${asked}` : null;
8863
8797
  }
8864
- function cacheDirectory(environment) {
8865
- const set = (name) => {
8866
- const value = environment[name];
8867
- return value !== undefined && value !== "" ? value : null;
8868
- };
8869
- const home = set("HOME") ?? homedir4();
8870
- if (process.platform === "win32") {
8871
- return join10(set("LOCALAPPDATA") ?? home, "gdharness");
8872
- }
8873
- if (process.platform === "darwin") {
8874
- return join10(home, "Library", "Caches", "gdharness");
8875
- }
8876
- return join10(set("XDG_CACHE_HOME") ?? join10(home, ".cache"), "gdharness");
8798
+ function scriptErrorsPrinted(printed) {
8799
+ return discoveryListing(printed)?.errors ?? [];
8877
8800
  }
8878
- function cacheFile(environment = process.env) {
8879
- try {
8880
- const directory = cacheDirectory(environment);
8881
- mkdirSync3(directory, { recursive: true, mode: 448 });
8882
- return join10(directory, "update-check.json");
8883
- } catch {
8884
- return join10(tmpdir3(), "gdharness-update-check.json");
8801
+ function discoveryListing(printed) {
8802
+ const start = printed.findIndex((line) => line.includes(DISCOVERY_FAILED));
8803
+ if (start < 0) {
8804
+ return null;
8885
8805
  }
8886
- }
8887
- function readCache(path) {
8888
- try {
8889
- if (!existsSync10(path)) {
8890
- return null;
8891
- }
8892
- const parsed = JSON.parse(readFileSync6(path, "utf8"));
8893
- if (typeof parsed !== "object" || parsed === null) {
8894
- return null;
8895
- }
8896
- const record = parsed;
8897
- const checkedAt = record["checkedAt"];
8898
- const latest = record["latest"];
8899
- if (typeof checkedAt !== "number" || typeof latest !== "string" || !VERSION.test(latest)) {
8900
- return null;
8806
+ const found = [];
8807
+ let message = null;
8808
+ let to = start + 1;
8809
+ for (const line of printed.slice(start + 1)) {
8810
+ const at = /^\s*at (res:\/\/.+):(\d+)\s*$/.exec(line);
8811
+ if (at !== null && message !== null) {
8812
+ found.push({ path: at[1] ?? "", line: Number(at[2]) === 0 ? null : Number(at[2]), message });
8813
+ message = null;
8814
+ } else if (at === null && message === null && /^ {2}\S/.test(line)) {
8815
+ message = line.trim();
8816
+ } else {
8817
+ break;
8901
8818
  }
8902
- return { checkedAt, latest };
8903
- } catch {
8904
- return null;
8819
+ to += 1;
8905
8820
  }
8821
+ const errors = found.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : (a.line ?? 0) - (b.line ?? 0));
8822
+ return { errors, from: start, to };
8906
8823
  }
8907
- function writeCache(path, entry) {
8908
- try {
8909
- writeFileSync5(path, JSON.stringify(entry), { encoding: "utf8", mode: 384 });
8910
- } catch {}
8824
+ function unspelled(text) {
8825
+ return text.replace(/<(?:LF|CR|TAB|BS|FF|VT|BEL|ESC|DEL|0x[0-9A-F]{2})>/g, (spelling) => spelling.startsWith("<0x") ? String.fromCharCode(Number.parseInt(spelling.slice(3, 5), 16)) : SPELLED[spelling] ?? spelling);
8911
8826
  }
8912
- function parts(version) {
8913
- const withoutBuild = version.split("+")[0] ?? version;
8914
- const dash = withoutBuild.indexOf("-");
8915
- const numeric = dash === -1 ? withoutBuild : withoutBuild.slice(0, dash);
8916
- return {
8917
- numbers: numeric.split(".").map((piece) => Number.parseInt(piece, 10) || 0),
8918
- prerelease: dash !== -1
8919
- };
8920
- }
8921
- function isNewer(candidate, current) {
8922
- const left = parts(candidate);
8923
- const right = parts(current);
8924
- for (let index = 0;index < 3; index += 1) {
8925
- const a = left.numbers[index] ?? 0;
8926
- const b = right.numbers[index] ?? 0;
8927
- if (a !== b) {
8928
- return a > b;
8827
+ function readDiff(printed) {
8828
+ let merged = "";
8829
+ let actual = "";
8830
+ let background = null;
8831
+ let last = 0;
8832
+ const take = (text) => {
8833
+ merged += text;
8834
+ if (background === null) {
8835
+ actual += text;
8836
+ } else if (background === "added") {
8837
+ actual += unspelled(text);
8838
+ }
8839
+ };
8840
+ for (const match of printed.matchAll(COLOUR)) {
8841
+ take(printed.slice(last, match.index));
8842
+ last = match.index + match[0].length;
8843
+ const codes = (match[1] ?? "").split(";");
8844
+ if (codes[0] === "48" && codes[1] === "2") {
8845
+ background = Number(codes[2]) > Number(codes[3]) ? "missing" : "added";
8846
+ } else if (codes[0] === "0" || codes[0] === "") {
8847
+ background = null;
8929
8848
  }
8930
8849
  }
8931
- return !left.prerelease && right.prerelease;
8850
+ take(printed.slice(last));
8851
+ return { merged, actual };
8932
8852
  }
8933
- async function fetchLatest(registry) {
8934
- const response = await fetch(registry, {
8935
- headers: { accept: "application/vnd.npm.install-v1+json, application/json" },
8936
- redirect: "error",
8937
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
8853
+ function withActualsPrinted(failed, printed) {
8854
+ const diffs = [...printed.replaceAll(`\r
8855
+ `, `
8856
+ `).matchAll(PRINTED_ACTUAL)].map((match) => ({
8857
+ suffix: match[3] ?? "",
8858
+ at: match[5] ?? "",
8859
+ ...readDiff(match[1] ?? "")
8860
+ }));
8861
+ const used = new Set;
8862
+ return failed.map((entry) => {
8863
+ if (entry.detail === null) {
8864
+ return entry;
8865
+ }
8866
+ let detail = entry.detail;
8867
+ let recovered = false;
8868
+ for (const [index, diff] of diffs.entries()) {
8869
+ if (used.has(index)) {
8870
+ continue;
8871
+ }
8872
+ const wrong = ` but was
8873
+ '${diff.merged.replaceAll("\r", "")}'${diff.suffix}
8874
+ at ${diff.at}`;
8875
+ if (detail.includes(wrong)) {
8876
+ detail = detail.replace(wrong, () => ` but was
8877
+ '${diff.actual}'${diff.suffix}
8878
+ at ${diff.at}`);
8879
+ recovered = true;
8880
+ used.add(index);
8881
+ }
8882
+ }
8883
+ const quoted = QUOTED_EQUALITY.exec(detail);
8884
+ if (quoted !== null) {
8885
+ const alike = quoted[1] === quoted[2];
8886
+ const note = recovered ? alike ? RENDERED_AWAY_NOTE : null : alike ? BOTH_SIDES_ALIKE_NOTE : VALUE_NOT_RECOVERED_NOTE;
8887
+ detail = note === null ? detail : `${detail}
8888
+ ${note}`;
8889
+ }
8890
+ if (quoted !== null && masked(quoted[1] ?? "", quoted[2] ?? "")) {
8891
+ const end = quoted[0].length - `
8892
+ at `.length;
8893
+ detail = detail.slice(0, end).replaceAll(MASKED_BRACKET, "[") + detail.slice(end);
8894
+ }
8895
+ return detail === entry.detail ? entry : { ...entry, detail };
8938
8896
  });
8939
- if (!response.ok || response.body === null) {
8940
- return null;
8897
+ }
8898
+ function shortened(detail) {
8899
+ if (detail === null || detail.length <= LONGEST_DETAIL) {
8900
+ return detail;
8941
8901
  }
8942
- const declared = Number.parseInt(response.headers.get("content-length") ?? "", 10);
8943
- if (Number.isFinite(declared) && declared > MAX_BODY_BYTES) {
8944
- return null;
8902
+ return `${detail.slice(0, LONGEST_DETAIL)}
8903
+ [${detail.length - LONGEST_DETAIL} more characters left out]`;
8904
+ }
8905
+ function foldedReports(reports) {
8906
+ const first = new Map;
8907
+ for (const report of reports) {
8908
+ const key = JSON.stringify([report.kind, report.message, report.detail]);
8909
+ const seen = first.get(key);
8910
+ if (seen === undefined) {
8911
+ first.set(key, { report, times: 1 });
8912
+ } else {
8913
+ seen.times += 1;
8914
+ }
8945
8915
  }
8946
- const chunks = [];
8947
- let size = 0;
8948
- for await (const chunk of response.body) {
8949
- size += chunk.byteLength;
8950
- if (size > MAX_BODY_BYTES) {
8951
- return null;
8916
+ return [...first.values()].map(({ report, times }) => {
8917
+ const kept = { ...report, detail: shortened(report.detail) };
8918
+ return times === 1 ? kept : { ...kept, times };
8919
+ });
8920
+ }
8921
+ function failedCases(report, printed) {
8922
+ const cases = report.suites.flatMap((suite) => suite.cases.filter((entry) => entry.status === "failed" || entry.status === "error").map((entry) => ({ entry, path: suite.path })));
8923
+ const read = withActualsPrinted(cases.flatMap(({ entry }) => entry.reports), printed);
8924
+ let next = 0;
8925
+ return cases.map(({ entry, path }) => {
8926
+ const reports = read.slice(next, next + entry.reports.length);
8927
+ next += entry.reports.length;
8928
+ const folded = foldedReports(reports);
8929
+ const shown = folded.slice(0, MOST_REPORTS_SHOWN);
8930
+ return {
8931
+ suite: entry.suite,
8932
+ name: entry.name,
8933
+ status: entry.status,
8934
+ time: entry.time,
8935
+ path,
8936
+ message: reports[0]?.message ?? null,
8937
+ detail: shortened(reports[0]?.detail ?? null),
8938
+ ...reports.length > 1 ? { reports: shown, reportCount: reports.length } : {},
8939
+ ...folded.length > shown.length ? { reportsLeftOut: folded.length - shown.length } : {}
8940
+ };
8941
+ });
8942
+ }
8943
+ function failedWithin(failed, budget) {
8944
+ const sizes = failed.map((entry) => JSON.stringify(entry).length);
8945
+ const smallestFirst = sizes.map((_, index) => index).sort((a, b) => (sizes[a] ?? 0) - (sizes[b] ?? 0));
8946
+ const whole = new Set;
8947
+ let spent = 0;
8948
+ for (const index of smallestFirst) {
8949
+ const size = sizes[index] ?? 0;
8950
+ if (spent + size > budget) {
8951
+ break;
8952
8952
  }
8953
- chunks.push(chunk);
8953
+ spent += size;
8954
+ whole.add(index);
8954
8955
  }
8955
- const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
8956
- if (typeof parsed !== "object" || parsed === null) {
8957
- return null;
8956
+ const kept = failed.map((entry, index) => {
8957
+ if (whole.has(index)) {
8958
+ return entry;
8959
+ }
8960
+ const { detail: _detail, reports: _reports, reportsLeftOut: _left, ...named } = entry;
8961
+ return { ...named, message: entry.message?.slice(0, 500) ?? null, detail: null };
8962
+ });
8963
+ return { failed: kept, briefed: failed.length - whole.size };
8964
+ }
8965
+ function masked(expected, actual) {
8966
+ const allSpelled = (value) => !PRINTED_DICTIONARY.test(value) && value.split("[").length === value.split(MASKED_BRACKET).length;
8967
+ return `${expected}${actual}`.includes(MASKED_BRACKET) && allSpelled(expected) && allSpelled(actual);
8968
+ }
8969
+ function orphansPrinted(printed) {
8970
+ const suites = [];
8971
+ let total = null;
8972
+ let running = null;
8973
+ for (const line of printed) {
8974
+ const started = RUNNING_SUITE.exec(line);
8975
+ if (started?.[1] !== undefined) {
8976
+ running = started[1];
8977
+ continue;
8978
+ }
8979
+ const counted = ORPHANS.exec(line);
8980
+ if (counted?.[1] === undefined) {
8981
+ continue;
8982
+ }
8983
+ const orphans = Number(counted[1]);
8984
+ if (line.includes(OVERALL)) {
8985
+ total = orphans;
8986
+ running = null;
8987
+ continue;
8988
+ }
8989
+ if (running !== null && orphans > 0) {
8990
+ suites.push({ path: running, orphans });
8991
+ }
8992
+ running = null;
8958
8993
  }
8959
- const version = parsed["version"];
8960
- return typeof version === "string" && VERSION.test(version) ? version : null;
8994
+ return {
8995
+ total: total ?? suites.reduce((sum, suite) => sum + suite.orphans, 0),
8996
+ suites
8997
+ };
8961
8998
  }
8962
-
8963
- class UpdateCheck {
8964
- latest = null;
8965
- checking = false;
8966
- asked = false;
8967
- checkedAt = 0;
8968
- retryAt = 0;
8969
- backoffMs = FIRST_RETRY_MS;
8970
- enabled;
8971
- current;
8972
- cachePath;
8973
- ask;
8974
- constructor(current, environment = process.env, ask = () => fetchLatest(registryFor(environment))) {
8975
- this.current = current;
8976
- this.enabled = (environment["GDHARNESS_NO_UPDATE_CHECK"] ?? "") === "";
8977
- this.cachePath = cacheFile(environment);
8978
- this.ask = ask;
8979
- const cached = this.enabled ? readCache(this.cachePath) : null;
8980
- if (cached !== null) {
8981
- this.latest = cached.latest;
8982
- this.checkedAt = cached.checkedAt;
8999
+ function finishesASuite(line) {
9000
+ return SUITE_COUNTS.test(line) && !line.includes(OVERALL);
9001
+ }
9002
+ function suitesPrinted(printed) {
9003
+ const finished = [];
9004
+ let running = null;
9005
+ let failed = [];
9006
+ for (const line of printed) {
9007
+ const started = RUNNING_SUITE.exec(line);
9008
+ if (started?.[1] !== undefined) {
9009
+ running = started[1];
9010
+ failed = [];
9011
+ continue;
9012
+ }
9013
+ if (running === null) {
9014
+ continue;
9015
+ }
9016
+ const failing = CASE_FAILED.exec(line)?.[1];
9017
+ if (failing !== undefined) {
9018
+ failed.push(failing);
9019
+ continue;
9020
+ }
9021
+ const counted = finishesASuite(line) ? SUITE_COUNTS.exec(line) : null;
9022
+ if (counted !== null) {
9023
+ finished.push({
9024
+ path: running,
9025
+ cases: Number(counted[1]),
9026
+ errors: Number(counted[2]),
9027
+ failures: Number(counted[3]),
9028
+ failed
9029
+ });
9030
+ running = null;
8983
9031
  }
8984
9032
  }
8985
- refresh(now = Date.now()) {
8986
- if (!this.enabled || this.checking || now < this.retryAt || this.asked && !this.stale(now)) {
8987
- return;
9033
+ return { finished, running };
9034
+ }
9035
+ function testProgressMessage(printed, elapsedMs) {
9036
+ const { finished, running } = suitesPrinted(printed);
9037
+ const failing = finished.filter((suite) => suite.errors + suite.failures > 0).length;
9038
+ const seconds = Math.floor(elapsedMs / 1000);
9039
+ const took = seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
9040
+ const done = finished.length === 0 ? "No suite has finished" : `${finished.length === 1 ? "One suite has" : `${finished.length} suites have`} finished${failing === 0 ? "" : `, ${failing} with failures`}`;
9041
+ return `${done}${running === null ? "" : `; running ${running}`}; ${took} in.`;
9042
+ }
9043
+ var MalformedReportError, ENTITIES, ATTRIBUTE, HOOK_FAILURE, NOTHING_RAN, NO_SUCH_PATH, DISCOVERY_FAILED = "Script errors were detected during test discovery!", ESCAPE, LOCATION = `'[^'\\n]*' in \\S+:\\d+`, IGNORING_CASE = " \\(ignoring case\\)", PRINTED_ACTUAL, QUOTED_EQUALITY, COLOUR, SPELLED, BOTH_SIDES_ALIKE_NOTE = "[gdharness: the expected and the actual printed alike because gdUnit4 drops the marks that tell them apart when it writes its report, and the runner printed no copy of this one, so neither side above is the value the assertion found.]", VALUE_NOT_RECOVERED_NOTE = '[gdharness: the value after "but was" may not be the one the assertion found. gdUnit4 writes a failing string into its report as a character diff against the expected one with the marks dropped, which merges the two, and the runner printed no copy of this one to read the value back from.]', RENDERED_AWAY_NOTE = '[gdharness: the two sides print alike, so what tells them apart is something gdUnit4 does not print. The usual cause is BBCode in the expected string, which gdUnit4 renders as formatting instead of showing; the value after "but was" is the one the assertion found.]', MOST_REPORTS_SHOWN = 20, LONGEST_DETAIL = 20000, MASKED_BRACKET = "[lb]", PRINTED_DICTIONARY, RUNNING_SUITE, ORPHANS, OVERALL = "Overall Summary", SUITE_COUNTS, CASE_FAILED;
9044
+ var init_junit = __esm(() => {
9045
+ MalformedReportError = class MalformedReportError extends Error {
9046
+ constructor(message) {
9047
+ super(message);
9048
+ this.name = "MalformedReportError";
8988
9049
  }
8989
- this.asked = true;
8990
- this.checking = true;
8991
- this.ask().then((version) => {
8992
- if (version === null) {
8993
- this.scheduleRetry(now);
8994
- return;
9050
+ };
9051
+ ENTITIES = { lt: "<", gt: ">", amp: "&", quot: '"', apos: "'" };
9052
+ ATTRIBUTE = /\s*([^\s=/>]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/y;
9053
+ HOOK_FAILURE = /^\s*(.*?)\s*\tat '(before|after)' in (res:\/\/.+?):(\d+)\s*$/;
9054
+ NOTHING_RAN = /no test cases found/i;
9055
+ NO_SUCH_PATH = /given directory or file does not exists:\s*(.+?)\s*$/i;
9056
+ ESCAPE = String.fromCharCode(27);
9057
+ PRINTED_ACTUAL = new RegExp(` but was\\n '((?:(?!\\tat ${LOCATION})[\\s\\S])*?)'((?:${ESCAPE}\\[[0-9;]*m)*)(${IGNORING_CASE})?((?:${ESCAPE}\\[[0-9;]*m)*)\\tat (${LOCATION})`, "g");
9058
+ QUOTED_EQUALITY = new RegExp(`^Expecting(?: error message)?:\\n '([\\s\\S]*)'\\n but was\\n '([\\s\\S]*)'(?:${IGNORING_CASE})?\\n\\tat `);
9059
+ COLOUR = new RegExp(`${ESCAPE}\\[([0-9;]*)m`, "g");
9060
+ SPELLED = {
9061
+ "<LF>": `
9062
+ `,
9063
+ "<CR>": "\r",
9064
+ "<TAB>": "\t",
9065
+ "<BS>": "\b",
9066
+ "<FF>": "\f",
9067
+ "<VT>": "\v",
9068
+ "<BEL>": "\x07",
9069
+ "<ESC>": ESCAPE,
9070
+ "<DEL>": ""
9071
+ };
9072
+ PRINTED_DICTIONARY = /^\{\n\t[\s\S]*\n {2}\}$/;
9073
+ RUNNING_SUITE = /Run Test Suite:\s*(res:\/\/.+?\.(?:gd|cs))(?=\s|$)/;
9074
+ ORPHANS = /(\d+)\s+orphans/;
9075
+ SUITE_COUNTS = /(\d+)\s+test cases\s*\|\s*(\d+)\s+errors\s*\|\s*(\d+)\s+failures/;
9076
+ CASE_FAILED = /^\s*res:\/\/.+?\.(?:gd|cs) > (.+?) (?:FAILED|ERROR)(?:\s|$)/;
9077
+ });
9078
+
9079
+ // src/script-origins.ts
9080
+ import { join as join9 } from "node:path";
9081
+ function unresolvedClassIn(message) {
9082
+ return UNRESOLVED.exec(message)?.[1] ?? null;
9083
+ }
9084
+ async function originsOf(errors, declared, check) {
9085
+ const listed = new Set(errors.map((error) => error.path));
9086
+ const origins = [];
9087
+ const unconfirmed = [];
9088
+ const asked = new Set;
9089
+ const waiting = errors.map((error) => unresolvedClassIn(error.message)).filter((name) => name !== null);
9090
+ while (waiting.length > 0 && asked.size < MOST_CHECKED) {
9091
+ const name = waiting.shift() ?? "";
9092
+ const path = declared.get(name);
9093
+ if (path === undefined || listed.has(path) || asked.has(path)) {
9094
+ continue;
9095
+ }
9096
+ asked.add(path);
9097
+ const checked = await check(path);
9098
+ const own = checked.errors.filter((error) => error.path === path);
9099
+ if (own.length === 0) {
9100
+ unconfirmed.push({
9101
+ path,
9102
+ declares: name,
9103
+ why: checked.failure === null ? "the engine's check of it found no error" : `the engine's check of it did not finish (${checked.failure})`
9104
+ });
9105
+ continue;
9106
+ }
9107
+ for (const error of own) {
9108
+ origins.push({ ...error, declares: name });
9109
+ const deeper = unresolvedClassIn(error.message);
9110
+ if (deeper !== null) {
9111
+ waiting.push(deeper);
8995
9112
  }
8996
- this.latest = version;
8997
- this.checkedAt = Date.now();
8998
- this.backoffMs = FIRST_RETRY_MS;
8999
- this.retryAt = 0;
9000
- writeCache(this.cachePath, { checkedAt: this.checkedAt, latest: version });
9001
- }).catch(() => {
9002
- this.scheduleRetry(now);
9003
- }).finally(() => {
9004
- this.checking = false;
9005
- });
9113
+ }
9006
9114
  }
9007
- stale(now) {
9008
- return now - this.checkedAt >= CACHE_MS;
9115
+ return { origins, unconfirmed };
9116
+ }
9117
+ function notCompiledIn(entry) {
9118
+ return NOT_COMPILED.exec(entry.text)?.[1];
9119
+ }
9120
+ function compileErrorsIn(entries) {
9121
+ return entries.flatMap((entry) => {
9122
+ const at = entry.severity === "error" ? reloadedAt(entry) : null;
9123
+ return at === null ? [] : [{ ...at, message: entry.text }];
9124
+ });
9125
+ }
9126
+ function reloadedAt(entry) {
9127
+ for (const line of entry.detail) {
9128
+ const at = RELOADED_AT.exec(line.trim());
9129
+ if (at !== null) {
9130
+ return { path: at[1] ?? "", line: Number(at[2]) === 0 ? null : Number(at[2]) };
9131
+ }
9009
9132
  }
9010
- scheduleRetry(now) {
9011
- this.retryAt = now + this.backoffMs;
9012
- this.backoffMs = Math.min(this.backoffMs * 2, MAX_RETRY_MS);
9133
+ return null;
9134
+ }
9135
+ function checkWithEngine(godotPath, projectPath) {
9136
+ return async (path) => {
9137
+ const logDir = scratchDirectory("tests");
9138
+ try {
9139
+ const ran = await runEngine(godotPath, [
9140
+ "--headless",
9141
+ "--log-file",
9142
+ join9(logDir, "engine.log"),
9143
+ "--path",
9144
+ projectPath,
9145
+ "--check-only",
9146
+ "-s",
9147
+ path
9148
+ ], { timeout: { ms: 60000, said: "minute" } });
9149
+ const errors = ran.log.select({ severity: "error", sinceLastCall: false, limit: 50 }).entries.flatMap((entry) => {
9150
+ const at = reloadedAt(entry);
9151
+ return at === null ? [] : [{ ...at, message: entry.text }];
9152
+ });
9153
+ return { errors, failure: ran.failure === null ? null : howItEnded(ran) };
9154
+ } finally {
9155
+ discard(logDir);
9156
+ }
9157
+ };
9158
+ }
9159
+ function withoutListed(entries, errors) {
9160
+ if (errors.length === 0) {
9161
+ return [...entries];
9013
9162
  }
9014
- notice(now = Date.now()) {
9015
- const latest = this.latest;
9016
- if (!this.enabled || latest === null || !isNewer(latest, this.current)) {
9017
- return null;
9163
+ const listing = discoveryListing(entries.map((entry) => entry.text));
9164
+ const paths = new Set(errors.map((error) => error.path));
9165
+ return entries.filter((entry, at) => {
9166
+ if (listing !== null && at >= listing.from && at < listing.to) {
9167
+ return false;
9018
9168
  }
9019
- if (this.checking && this.stale(now)) {
9020
- return null;
9169
+ const reloaded = reloadedAt(entry);
9170
+ if (reloaded !== null && errors.some((error) => error.path === reloaded.path && error.line === reloaded.line && error.message === entry.text)) {
9171
+ return false;
9021
9172
  }
9022
- return {
9023
- current: this.current,
9024
- latest,
9025
- releaseNotes: `${RELEASES}/v${latest}`,
9026
- upgrade: runLine(currentRunner(), latest, "upgrade")
9027
- };
9028
- }
9173
+ const failedToLoad = /^Failed to load script "(res:\/\/[^"]+)" with error "[^"]+"\.$/.exec(entry.text);
9174
+ return failedToLoad === null || !paths.has(failedToLoad[1] ?? "");
9175
+ });
9029
9176
  }
9030
- var REGISTRY_ROOT = "https://registry.npmjs.org", RELEASES = "https://github.com/Aureliolo/gdharness/releases/tag", CACHE_MS, REQUEST_TIMEOUT_MS = 1e4, MAX_BODY_BYTES, FIRST_RETRY_MS = 30000, MAX_RETRY_MS, VERSION;
9031
- var init_update_check = __esm(() => {
9032
- init_runner();
9033
- CACHE_MS = 10 * 60 * 1000;
9034
- MAX_BODY_BYTES = 1 << 20;
9035
- MAX_RETRY_MS = 60 * 60 * 1000;
9036
- VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
9177
+ var UNRESOLVED, MOST_CHECKED = 8, RELOADED_AT, NOT_COMPILED;
9178
+ var init_script_origins = __esm(() => {
9179
+ init_engine_run();
9180
+ init_junit();
9181
+ init_scratch();
9182
+ UNRESOLVED = /Could not resolve class "([A-Za-z_][A-Za-z0-9_]*)", because of a parser error/;
9183
+ RELOADED_AT = /GDScript::reload \((res:\/\/.+):(\d+)\)$/;
9184
+ NOT_COMPILED = /^Failed to load script "(res:\/\/[^"]+)" with error "(?:Parse error|Compilation failed)"/;
9037
9185
  });
9038
9186
 
9039
- // src/server-version.ts
9040
- import { readFileSync as readFileSync7 } from "node:fs";
9041
- function editorIsStale(addonVersion, serverVersion, addonDigest, shippedDigest) {
9042
- if (addonDigest !== undefined && addonDigest !== "" && shippedDigest !== undefined) {
9043
- return addonDigest !== shippedDigest;
9187
+ // src/headless.ts
9188
+ import { existsSync as existsSync9, readFileSync as readFileSync5, realpathSync as realpathSync2, writeFileSync as writeFileSync4 } from "node:fs";
9189
+ import { dirname as dirname6, join as join10 } from "node:path";
9190
+ function snakeCased(params) {
9191
+ const result = emptyRecord();
9192
+ for (const [key, value] of Object.entries(params)) {
9193
+ const name = key.startsWith("_") ? key : key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
9194
+ result[name] = value;
9044
9195
  }
9045
- return addonVersion !== serverVersion;
9196
+ return result;
9046
9197
  }
9047
- function sameCodeNote(addonVersion, serverVersion, addonDigest, shippedDigest) {
9048
- if (addonVersion === undefined || addonVersion === serverVersion || addonDigest === undefined || addonDigest === "" || addonDigest !== shippedDigest) {
9049
- return;
9050
- }
9051
- const same = `The editor loaded the ${addonVersion} addons, which are the same code this ${serverVersion} server ships, so nothing needs restarting`;
9052
- return isNewer(addonVersion, serverVersion) ? `${same}. This server is the older of the two, which a reconnect in your harness settles once the project pins ${addonVersion}.` : `${same}; addonVersion is the version the editor loaded when it started.`;
9198
+ function engineExtras(outcome) {
9199
+ const after = outcome.afterAnswerMessages ?? [];
9200
+ const before = outcome.beforeOperationMessages ?? [];
9201
+ return {
9202
+ ...outcome.messages.length > 0 ? { engine_messages: outcome.messages } : {},
9203
+ ...before.length > 0 ? {
9204
+ engine_messages_before_operation: before,
9205
+ engine_messages_before_operation_note: withNotCompiled(BEFORE_OPERATION_NOTE, before)
9206
+ } : {},
9207
+ ...outcome.beforeOperationOmitted === undefined ? {} : { engine_messages_before_operation_omitted: outcome.beforeOperationOmitted },
9208
+ ...outcome.messagesOmitted === undefined ? {} : { engine_messages_omitted: outcome.messagesOmitted },
9209
+ ...after.length > 0 ? {
9210
+ engine_messages_after_answer: after,
9211
+ engine_messages_after_answer_note: withNotCompiled(AFTER_ANSWER_NOTE, after)
9212
+ } : {},
9213
+ ...outcome.afterAnswerOmitted === undefined ? {} : { engine_messages_after_answer_omitted: outcome.afterAnswerOmitted },
9214
+ ...outcome.afterAnswer === undefined ? {} : { engine_after_answer: outcome.afterAnswer }
9215
+ };
9053
9216
  }
9054
- function addonMismatch(addonVersion, serverVersion, addonDigest, shippedDigest, onDisk) {
9055
- if (!editorIsStale(addonVersion, serverVersion, addonDigest, shippedDigest)) {
9056
- return;
9057
- }
9058
- const reported = addonVersion ?? "";
9059
- const restart = onDisk !== undefined && onDisk !== shippedDigest ? `The project's own addons are not this server's either, so a restart would load the same code again: run ${gdharnessCommand(serverVersion, "upgrade")} in a shell in the project directory, which installs the ${serverVersion} addons, and editor_launch restart then picks them up.` : "Restart it with editor_launch restart to pick this one up.";
9060
- if (reported === serverVersion) {
9061
- return `The editor is running a different build of the ${reported} addon from the one this server ships. ${restart}`;
9062
- }
9063
- const editor = reported === "" ? UNVERSIONED : `the ${reported} addon`;
9064
- const both = `The editor is running ${editor} while this server ships ${serverVersion}.`;
9065
- if (reported !== "" && isNewer(reported, serverVersion)) {
9066
- return `${both} This server is the older half: reconnect it in your harness so it spawns ${reported}.`;
9067
- }
9068
- return `${both} ${restart}`;
9217
+ function withNotCompiled(note, entries) {
9218
+ const path = entries.map(notCompiledIn).find((found) => found !== undefined);
9219
+ 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.`;
9069
9220
  }
9070
- function gdharnessCommand(version, args) {
9071
- return `npx -y gdharness@${version} ${args}`;
9221
+ function problemsIn(log) {
9222
+ return problemsAmong(log.everything());
9072
9223
  }
9073
- function runtimeAddonUnknown(addon) {
9074
- return addon?.version === "" && addon.digest === "";
9224
+ function problemsAmong(entries) {
9225
+ return entries.flatMap((entry) => {
9226
+ if (entry.severity !== "info") {
9227
+ return [entry];
9228
+ }
9229
+ if (entry.source === "stderr" && entry.text.startsWith(OWN_ERROR)) {
9230
+ return [{ ...entry, severity: "error", text: entry.text.slice(OWN_ERROR.length) }];
9231
+ }
9232
+ return [];
9233
+ });
9075
9234
  }
9076
- function runtimeMismatch(pid, addon, serverVersion, shippedDigest, onDisk, projectPath) {
9077
- if (runtimeAddonUnknown(addon)) {
9078
- return;
9079
- }
9080
- if (addon !== undefined && !editorIsStale(addon.version, serverVersion, addon.digest, shippedDigest)) {
9081
- return;
9235
+ function reported2(problems) {
9236
+ const omitted = Math.max(0, problems.length - MESSAGES_KEPT);
9237
+ return omitted === 0 ? { messages: problems } : { messages: problems.slice(omitted), messagesOmitted: omitted };
9238
+ }
9239
+ function reason(log, problems) {
9240
+ const own = refusalsIn(log);
9241
+ if (own.length > 0) {
9242
+ return own.map((entry) => entry.text.slice(OWN_ERROR.length)).join("; ");
9082
9243
  }
9083
- const game = `The game (pid ${pid})`;
9084
- if (addon !== undefined && addon.version !== "" && isNewer(addon.version, serverVersion)) {
9085
- return `${game} is running the ${addon.version} runtime addon while this server ships ${serverVersion}. This server is the older half: reconnect it in your harness so it spawns ${addon.version}.`;
9244
+ const errors = problems.filter((entry) => entry.severity === "error").map((entry) => entry.text);
9245
+ if (errors.length > 0) {
9246
+ const rest = errors.length - ERRORS_NAMED;
9247
+ return rest > 0 ? `${errors.slice(0, ERRORS_NAMED).join("; ")}; and ${rest} more error${rest === 1 ? "" : "s"}` : errors.join("; ");
9248
+ }
9249
+ const printed = log.everything().filter((entry) => entry.source === "stdout");
9250
+ return printed.at(-1)?.text ?? "no output at all";
9251
+ }
9252
+ function refusalsIn(log) {
9253
+ return log.everything().filter((entry) => entry.source === "stderr" && entry.severity === "info" && entry.text.startsWith(OWN_ERROR));
9254
+ }
9255
+ function scriptFault(problems, script) {
9256
+ const spelled = (path) => `(${path.replaceAll("\\", "/").toLowerCase()}/`;
9257
+ const directories = [...new Set([spelled(dirname6(script)), spelled(resolvedOr(dirname6(script)))])];
9258
+ return problems.find((entry) => {
9259
+ const at = entry.detail.find((line) => line.startsWith("at: "))?.replaceAll("\\", "/").toLowerCase();
9260
+ return entry.severity === "error" && at !== undefined && directories.some((directory) => at.includes(directory));
9261
+ });
9262
+ }
9263
+ function resolvedOr(path) {
9264
+ try {
9265
+ return realpathSync2.native(path);
9266
+ } catch {
9267
+ return path;
9268
+ }
9269
+ }
9270
+ function librariesNotCopied(messages) {
9271
+ const found = new Set;
9272
+ for (const entry of messages) {
9273
+ const library = /Error copying library: (.+)$/.exec(entry.text)?.[1]?.trim();
9274
+ if (library !== undefined && library !== "") {
9275
+ found.add(library);
9276
+ }
9277
+ }
9278
+ return [...found];
9279
+ }
9280
+ function extensionRetryNote(libraries, stillNotLoaded = []) {
9281
+ const second = stillNotLoaded.length === 0 ? "so the import was run again and loaded the extension, and the messages are from that second run." : `so the import was run again, and it still could not load ${stillNotLoaded.join(" and ")}: this answer is from an import without ${stillNotLoaded.length === 1 ? "it" : "them"}, and its messages say what the engine hit.`;
9282
+ return `The first import could not load ${libraries.join(" and ")}: on Windows an engine running as the editor ` + "loads a GDExtension from a ~ copy beside the library, and the editor open on this project holds its own " + "copy there, so the first engine started beside it cannot make one. That failed attempt moves the " + `editor's copy aside, ${second} The editor's copy is left beside the library as a ~RF….TMP file, which ` + "the next import, export or editor this server starts on the project deletes once the editor has exited.";
9283
+ }
9284
+ async function runImport(godotPath, projectPath, options = {}) {
9285
+ const once = options.once ?? importOnce;
9286
+ const pass = () => {
9287
+ for (const target of options.forcing ?? []) {
9288
+ forceNextImport(projectPath, target);
9289
+ }
9290
+ freeLibraryCopies(projectPath);
9291
+ return once(godotPath, projectPath);
9292
+ };
9293
+ const first = await pass();
9294
+ const collided = librariesNotCopied(first.messages);
9295
+ const crash = first.docCacheNote;
9296
+ if (collided.length === 0 && crash === undefined) {
9297
+ return first;
9298
+ }
9299
+ const { docCacheNote: _, ...again } = await pass();
9300
+ const second = again.ok || crash === undefined ? again : { ...again, message: `${again.message.replace(/[.\s]+$/, "")}. ${crash}` };
9301
+ const still = librariesNotCopied(second.messages);
9302
+ return {
9303
+ ...second,
9304
+ ...collided.length === 0 ? {} : {
9305
+ librariesRetried: collided,
9306
+ ...still.length === 0 ? {} : { librariesNotLoaded: still },
9307
+ extensionNote: extensionRetryNote(collided, still)
9308
+ },
9309
+ ...crash === undefined ? {} : { docCacheNote: crash }
9310
+ };
9311
+ }
9312
+ async function importOnce(godotPath, projectPath) {
9313
+ const logDir = scratchDirectory("import");
9314
+ const caches = editorCacheDirectory(godotPath);
9315
+ const cachesBefore = docCachesWritten(caches);
9316
+ let ran;
9317
+ try {
9318
+ ran = await runEngine(godotPath, [
9319
+ "--headless",
9320
+ "--log-file",
9321
+ join10(logDir, "engine.log"),
9322
+ "--path",
9323
+ projectPath,
9324
+ "--import"
9325
+ ]);
9326
+ } finally {
9327
+ discard(logDir);
9328
+ }
9329
+ const problems = problemsIn(ran.log);
9330
+ const crash = docCacheCrash(ran, cachesBefore, docCachesWritten(caches), "import");
9331
+ if (ran.exitCode === 0 && ran.failure === null) {
9332
+ return { ok: true, ...reported2(problems), ...crash === null ? {} : { docCacheNote: crash } };
9333
+ }
9334
+ return {
9335
+ ok: false,
9336
+ message: `the import pass failed (${howItEnded(ran)}): ${reason(ran.log, problems)}`,
9337
+ ...reported2(problems),
9338
+ ...crash === null ? {} : { docCacheNote: crash }
9339
+ };
9340
+ }
9341
+ async function runOperation(engine, operation, params, projectPath) {
9342
+ const paramsDir = scratchDirectory("params");
9343
+ const paramsFile = join10(paramsDir, `${operation}.json`);
9344
+ const answerFile = join10(paramsDir, "answer.json");
9345
+ writeFileSync4(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
9346
+ const args = [
9347
+ "--headless",
9348
+ "--log-file",
9349
+ join10(paramsDir, "engine.log"),
9350
+ "--path",
9351
+ projectPath,
9352
+ "--script",
9353
+ engine.script,
9354
+ operation,
9355
+ `@file:${paramsFile}`,
9356
+ answerFile,
9357
+ ...engine.debug ? ["--debug-godot"] : []
9358
+ ];
9359
+ let ran;
9360
+ let answer;
9361
+ try {
9362
+ ran = await runEngine(engine.godotPath, args);
9363
+ answer = answerIn(answerFile);
9364
+ } finally {
9365
+ discard(paramsDir);
9366
+ }
9367
+ const everything = ran.log.everything();
9368
+ const marked = (text) => everything.findIndex((entry) => entry.source === "stderr" && entry.text === text);
9369
+ const answered = marked(ANSWERED);
9370
+ const waiting = marked(WAITING);
9371
+ const begun = marked(BEGUN);
9372
+ const ended = answered === -1 ? everything.length : answered;
9373
+ const problems = waiting === -1 ? problemsAmong(everything.slice(0, ended)) : problemsAmong([
9374
+ ...everything.slice(0, waiting),
9375
+ ...begun === -1 ? [] : everything.slice(begun + 1, ended)
9376
+ ]);
9377
+ const startup = waiting === -1 ? [] : problemsAmong(everything.slice(waiting + 1, begun === -1 ? ended : begun));
9378
+ const after = answered === -1 ? [] : problemsAmong(everything.slice(answered + 1));
9379
+ const afterAnswered = {
9380
+ ...after.length === 0 ? {} : { afterAnswerMessages: after.slice(-MESSAGES_KEPT) },
9381
+ ...after.length > MESSAGES_KEPT ? { afterAnswerOmitted: after.length - MESSAGES_KEPT } : {},
9382
+ ...startup.length === 0 ? {} : { beforeOperationMessages: startup.slice(-MESSAGES_KEPT) },
9383
+ ...startup.length > MESSAGES_KEPT ? { beforeOperationOmitted: startup.length - MESSAGES_KEPT } : {}
9384
+ };
9385
+ if (answer.kind === "unreadable") {
9386
+ return {
9387
+ ok: false,
9388
+ message: `${operation} wrote an answer that could not be read (${answer.why})`,
9389
+ ...reported2(problems),
9390
+ ...afterAnswered
9391
+ };
9392
+ }
9393
+ if (answer.kind === "none") {
9394
+ if (waiting !== -1 && begun === -1) {
9395
+ return {
9396
+ ok: false,
9397
+ message: `${operation} never ran: the engine ended (${howItEnded(ran)}) while it started the project's ` + `autoloads, which this operation waits for. The last it reported: ${reason(ran.log, startup)}`,
9398
+ ...reported2(problems),
9399
+ ...afterAnswered
9400
+ };
9401
+ }
9402
+ const refusals = refusalsIn(ran.log);
9403
+ if (refusals.length > 0) {
9404
+ const said = new Set(refusals.map((entry) => entry.index));
9405
+ return {
9406
+ ok: false,
9407
+ message: refusals.map((entry) => entry.text.slice(OWN_ERROR.length)).join("; "),
9408
+ ...reported2(problems.filter((entry) => !said.has(entry.index)))
9409
+ };
9410
+ }
9411
+ const ending = ran.exitCode === 0 && ran.failure === null ? "produced no result" : `failed (${howItEnded(ran)})`;
9412
+ return {
9413
+ ok: false,
9414
+ message: `${operation} ${ending}: ${reason(ran.log, problems)}`,
9415
+ ...reported2(problems)
9416
+ };
9417
+ }
9418
+ const fault = scriptFault(problems, engine.script);
9419
+ if (fault !== undefined) {
9420
+ return {
9421
+ ok: false,
9422
+ message: `${operation} hit an error in the operations script, so its answer is left out: part of it was ` + `never computed. ${fault.text} (${fault.detail[0]?.slice("at: ".length) ?? "no location given"})`,
9423
+ ...reported2(problems),
9424
+ ...afterAnswered
9425
+ };
9426
+ }
9427
+ const afterAnswer = ran.exitCode === 0 && ran.failure === null ? undefined : `The operation finished and wrote this answer, and then the engine did not exit cleanly (${howItEnded(ran)}). Anything it reported on the way out is under ${answered === -1 ? "engine_messages" : "engine_messages_after_answer"}.`;
9428
+ return {
9429
+ ok: true,
9430
+ payload: answer.payload,
9431
+ ...reported2(problems),
9432
+ ...afterAnswered,
9433
+ ...afterAnswer === undefined ? {} : { afterAnswer }
9434
+ };
9435
+ }
9436
+ function answerIn(file) {
9437
+ if (!existsSync9(file)) {
9438
+ return { kind: "none" };
9439
+ }
9440
+ const text = readFileSync5(file, "utf8");
9441
+ try {
9442
+ const parsed = JSON.parse(text);
9443
+ 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)}` };
9444
+ } catch (error) {
9445
+ return {
9446
+ kind: "unreadable",
9447
+ why: `${error instanceof Error ? error.message : String(error)}: ${text.slice(0, 200)}`
9448
+ };
9449
+ }
9450
+ }
9451
+ var AFTER_ANSWER_NOTE = "Printed after this operation had written its answer, while the engine ran the project's autoloads and shut down: Godot starts a project's autoloads in every script run and cannot be told not to. An error raised in a project script here is that autoload's own, and none of these changes the answer.", BEFORE_OPERATION_NOTE = "Printed while the engine started the project's autoloads, before this operation ran: an operation that compiles the project's scripts waits for them, because the engine knows an autoload's name only once it is in the tree. An error raised in a project script here is that autoload's own.", OWN_ERROR = "[ERROR] ", ANSWERED = "[gdharness] answered", WAITING = "[gdharness] waiting for the autoloads", BEGUN = "[gdharness] begun", MESSAGES_KEPT = 200, ERRORS_NAMED = 3;
9452
+ var init_headless = __esm(() => {
9453
+ init_doc_cache();
9454
+ init_engine_run();
9455
+ init_library_copies();
9456
+ init_reimport();
9457
+ init_scratch();
9458
+ init_script_origins();
9459
+ });
9460
+
9461
+ // src/update-check.ts
9462
+ import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
9463
+ import { homedir as homedir4, tmpdir as tmpdir3 } from "node:os";
9464
+ import { join as join11 } from "node:path";
9465
+ function packageUrl(root) {
9466
+ return `${root.replace(/\/+$/, "")}/gdharness/latest`;
9467
+ }
9468
+ function registryFor(environment) {
9469
+ const named = environment["GDHARNESS_REGISTRY"] ?? "";
9470
+ if (named === "") {
9471
+ return packageUrl(REGISTRY_ROOT);
9472
+ }
9473
+ try {
9474
+ return new URL(named).protocol === "https:" ? packageUrl(named) : packageUrl(REGISTRY_ROOT);
9475
+ } catch {
9476
+ return packageUrl(REGISTRY_ROOT);
9477
+ }
9478
+ }
9479
+ function cacheDirectory(environment) {
9480
+ const set = (name) => {
9481
+ const value = environment[name];
9482
+ return value !== undefined && value !== "" ? value : null;
9483
+ };
9484
+ const home = set("HOME") ?? homedir4();
9485
+ if (process.platform === "win32") {
9486
+ return join11(set("LOCALAPPDATA") ?? home, "gdharness");
9487
+ }
9488
+ if (process.platform === "darwin") {
9489
+ return join11(home, "Library", "Caches", "gdharness");
9490
+ }
9491
+ return join11(set("XDG_CACHE_HOME") ?? join11(home, ".cache"), "gdharness");
9492
+ }
9493
+ function cacheFile(environment = process.env) {
9494
+ try {
9495
+ const directory = cacheDirectory(environment);
9496
+ mkdirSync3(directory, { recursive: true, mode: 448 });
9497
+ return join11(directory, "update-check.json");
9498
+ } catch {
9499
+ return join11(tmpdir3(), "gdharness-update-check.json");
9500
+ }
9501
+ }
9502
+ function readCache(path) {
9503
+ try {
9504
+ if (!existsSync10(path)) {
9505
+ return null;
9506
+ }
9507
+ const parsed = JSON.parse(readFileSync6(path, "utf8"));
9508
+ if (typeof parsed !== "object" || parsed === null) {
9509
+ return null;
9510
+ }
9511
+ const record = parsed;
9512
+ const checkedAt = record["checkedAt"];
9513
+ const latest = record["latest"];
9514
+ if (typeof checkedAt !== "number" || typeof latest !== "string" || !VERSION.test(latest)) {
9515
+ return null;
9516
+ }
9517
+ return { checkedAt, latest };
9518
+ } catch {
9519
+ return null;
9520
+ }
9521
+ }
9522
+ function writeCache(path, entry) {
9523
+ try {
9524
+ writeFileSync5(path, JSON.stringify(entry), { encoding: "utf8", mode: 384 });
9525
+ } catch {}
9526
+ }
9527
+ function parts(version) {
9528
+ const withoutBuild = version.split("+")[0] ?? version;
9529
+ const dash = withoutBuild.indexOf("-");
9530
+ const numeric = dash === -1 ? withoutBuild : withoutBuild.slice(0, dash);
9531
+ return {
9532
+ numbers: numeric.split(".").map((piece) => Number.parseInt(piece, 10) || 0),
9533
+ prerelease: dash !== -1
9534
+ };
9535
+ }
9536
+ function isNewer(candidate, current) {
9537
+ const left = parts(candidate);
9538
+ const right = parts(current);
9539
+ for (let index = 0;index < 3; index += 1) {
9540
+ const a = left.numbers[index] ?? 0;
9541
+ const b = right.numbers[index] ?? 0;
9542
+ if (a !== b) {
9543
+ return a > b;
9544
+ }
9545
+ }
9546
+ return !left.prerelease && right.prerelease;
9547
+ }
9548
+ async function fetchLatest(registry) {
9549
+ const response = await fetch(registry, {
9550
+ headers: { accept: "application/vnd.npm.install-v1+json, application/json" },
9551
+ redirect: "error",
9552
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
9553
+ });
9554
+ if (!response.ok || response.body === null) {
9555
+ return null;
9556
+ }
9557
+ const declared = Number.parseInt(response.headers.get("content-length") ?? "", 10);
9558
+ if (Number.isFinite(declared) && declared > MAX_BODY_BYTES) {
9559
+ return null;
9560
+ }
9561
+ const chunks = [];
9562
+ let size = 0;
9563
+ for await (const chunk of response.body) {
9564
+ size += chunk.byteLength;
9565
+ if (size > MAX_BODY_BYTES) {
9566
+ return null;
9567
+ }
9568
+ chunks.push(chunk);
9569
+ }
9570
+ const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
9571
+ if (typeof parsed !== "object" || parsed === null) {
9572
+ return null;
9573
+ }
9574
+ const version = parsed["version"];
9575
+ return typeof version === "string" && VERSION.test(version) ? version : null;
9576
+ }
9577
+
9578
+ class UpdateCheck {
9579
+ latest = null;
9580
+ checking = false;
9581
+ asked = false;
9582
+ checkedAt = 0;
9583
+ retryAt = 0;
9584
+ backoffMs = FIRST_RETRY_MS;
9585
+ enabled;
9586
+ current;
9587
+ cachePath;
9588
+ ask;
9589
+ constructor(current, environment = process.env, ask = () => fetchLatest(registryFor(environment))) {
9590
+ this.current = current;
9591
+ this.enabled = (environment["GDHARNESS_NO_UPDATE_CHECK"] ?? "") === "";
9592
+ this.cachePath = cacheFile(environment);
9593
+ this.ask = ask;
9594
+ const cached = this.enabled ? readCache(this.cachePath) : null;
9595
+ if (cached !== null) {
9596
+ this.latest = cached.latest;
9597
+ this.checkedAt = cached.checkedAt;
9598
+ }
9599
+ }
9600
+ refresh(now = Date.now()) {
9601
+ if (!this.enabled || this.checking || now < this.retryAt || this.asked && !this.stale(now)) {
9602
+ return;
9603
+ }
9604
+ this.asked = true;
9605
+ this.checking = true;
9606
+ this.ask().then((version) => {
9607
+ if (version === null) {
9608
+ this.scheduleRetry(now);
9609
+ return;
9610
+ }
9611
+ this.latest = version;
9612
+ this.checkedAt = Date.now();
9613
+ this.backoffMs = FIRST_RETRY_MS;
9614
+ this.retryAt = 0;
9615
+ writeCache(this.cachePath, { checkedAt: this.checkedAt, latest: version });
9616
+ }).catch(() => {
9617
+ this.scheduleRetry(now);
9618
+ }).finally(() => {
9619
+ this.checking = false;
9620
+ });
9621
+ }
9622
+ stale(now) {
9623
+ return now - this.checkedAt >= CACHE_MS;
9624
+ }
9625
+ scheduleRetry(now) {
9626
+ this.retryAt = now + this.backoffMs;
9627
+ this.backoffMs = Math.min(this.backoffMs * 2, MAX_RETRY_MS);
9628
+ }
9629
+ notice(now = Date.now()) {
9630
+ const latest = this.latest;
9631
+ if (!this.enabled || latest === null || !isNewer(latest, this.current)) {
9632
+ return null;
9633
+ }
9634
+ if (this.checking && this.stale(now)) {
9635
+ return null;
9636
+ }
9637
+ return {
9638
+ current: this.current,
9639
+ latest,
9640
+ releaseNotes: `${RELEASES}/v${latest}`,
9641
+ upgrade: runLine(currentRunner(), latest, "upgrade")
9642
+ };
9643
+ }
9644
+ }
9645
+ var REGISTRY_ROOT = "https://registry.npmjs.org", RELEASES = "https://github.com/Aureliolo/gdharness/releases/tag", CACHE_MS, REQUEST_TIMEOUT_MS = 1e4, MAX_BODY_BYTES, FIRST_RETRY_MS = 30000, MAX_RETRY_MS, VERSION;
9646
+ var init_update_check = __esm(() => {
9647
+ init_runner();
9648
+ CACHE_MS = 10 * 60 * 1000;
9649
+ MAX_BODY_BYTES = 1 << 20;
9650
+ MAX_RETRY_MS = 60 * 60 * 1000;
9651
+ VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
9652
+ });
9653
+
9654
+ // src/server-version.ts
9655
+ import { readFileSync as readFileSync7 } from "node:fs";
9656
+ function editorIsStale(addonVersion, serverVersion, addonDigest, shippedDigest) {
9657
+ if (addonDigest !== undefined && addonDigest !== "" && shippedDigest !== undefined) {
9658
+ return addonDigest !== shippedDigest;
9659
+ }
9660
+ return addonVersion !== serverVersion;
9661
+ }
9662
+ function sameCodeNote(addonVersion, serverVersion, addonDigest, shippedDigest) {
9663
+ if (addonVersion === undefined || addonVersion === serverVersion || addonDigest === undefined || addonDigest === "" || addonDigest !== shippedDigest) {
9664
+ return;
9665
+ }
9666
+ const same = `The editor loaded the ${addonVersion} addons, which are the same code this ${serverVersion} server ships, so nothing needs restarting`;
9667
+ return isNewer(addonVersion, serverVersion) ? `${same}. This server is the older of the two, which a reconnect in your harness settles once the project pins ${addonVersion}.` : `${same}; addonVersion is the version the editor loaded when it started.`;
9668
+ }
9669
+ function addonMismatch(addonVersion, serverVersion, addonDigest, shippedDigest, onDisk) {
9670
+ if (!editorIsStale(addonVersion, serverVersion, addonDigest, shippedDigest)) {
9671
+ return;
9672
+ }
9673
+ const reported = addonVersion ?? "";
9674
+ const restart = onDisk !== undefined && onDisk !== shippedDigest ? `The project's own addons are not this server's either, so a restart would load the same code again: run ${gdharnessCommand(serverVersion, "upgrade")} in a shell in the project directory, which installs the ${serverVersion} addons, and editor_launch restart then picks them up.` : "Restart it with editor_launch restart to pick this one up.";
9675
+ if (reported === serverVersion) {
9676
+ return `The editor is running a different build of the ${reported} addon from the one this server ships. ${restart}`;
9677
+ }
9678
+ const editor = reported === "" ? UNVERSIONED : `the ${reported} addon`;
9679
+ const both = `The editor is running ${editor} while this server ships ${serverVersion}.`;
9680
+ if (reported !== "" && isNewer(reported, serverVersion)) {
9681
+ return `${both} This server is the older half: reconnect it in your harness so it spawns ${reported}.`;
9682
+ }
9683
+ return `${both} ${restart}`;
9684
+ }
9685
+ function gdharnessCommand(version, args) {
9686
+ return `npx -y gdharness@${version} ${args}`;
9687
+ }
9688
+ function runtimeAddonUnknown(addon) {
9689
+ return addon?.version === "" && addon.digest === "";
9690
+ }
9691
+ function runtimeMismatch(pid, addon, serverVersion, shippedDigest, onDisk, projectPath) {
9692
+ if (runtimeAddonUnknown(addon)) {
9693
+ return;
9694
+ }
9695
+ if (addon !== undefined && !editorIsStale(addon.version, serverVersion, addon.digest, shippedDigest)) {
9696
+ return;
9697
+ }
9698
+ const game = `The game (pid ${pid})`;
9699
+ if (addon !== undefined && addon.version !== "" && isNewer(addon.version, serverVersion)) {
9700
+ return `${game} is running the ${addon.version} runtime addon while this server ships ${serverVersion}. This server is the older half: reconnect it in your harness so it spawns ${addon.version}.`;
9086
9701
  }
9087
9702
  let loaded;
9088
9703
  if (addon === undefined) {
@@ -9897,7 +10512,7 @@ var init_gdscript_source = __esm(() => {
9897
10512
 
9898
10513
  // src/class-cache.ts
9899
10514
  import { existsSync as existsSync11, readdirSync as readdirSync6, readFileSync as readFileSync8, statSync as statSync6 } from "node:fs";
9900
- import { join as join11, posix } from "node:path";
10515
+ import { join as join12, posix } from "node:path";
9901
10516
  function isToolScript(source) {
9902
10517
  return TOOL.test(codeOf(source));
9903
10518
  }
@@ -9913,14 +10528,14 @@ function declaredClasses(projectPath) {
9913
10528
  }
9914
10529
  function eachScript(projectPath, each) {
9915
10530
  const visit = (directory, prefix) => {
9916
- if (existsSync11(join11(directory, ".gdignore"))) {
10531
+ if (existsSync11(join12(directory, ".gdignore"))) {
9917
10532
  return;
9918
10533
  }
9919
10534
  for (const entry of readdirSync6(directory, { withFileTypes: true })) {
9920
10535
  if (entry.name.startsWith(".")) {
9921
10536
  continue;
9922
10537
  }
9923
- const path = join11(directory, entry.name);
10538
+ const path = join12(directory, entry.name);
9924
10539
  if (entry.isDirectory()) {
9925
10540
  visit(path, `${prefix}${entry.name}/`);
9926
10541
  } else if (entry.isFile() && entry.name.endsWith(".gd")) {
@@ -9946,7 +10561,7 @@ function scriptsNaming(projectPath, classes) {
9946
10561
  return naming.sort((a, b) => a.script.localeCompare(b.script));
9947
10562
  }
9948
10563
  function cachedClasses(projectPath) {
9949
- const cache = join11(projectPath, ".godot", "global_script_class_cache.cfg");
10564
+ const cache = join12(projectPath, ".godot", "global_script_class_cache.cfg");
9950
10565
  if (!existsSync11(cache)) {
9951
10566
  return null;
9952
10567
  }
@@ -10217,7 +10832,7 @@ function uncachedClassNote(types) {
10217
10832
  return `${types.join(", ")} ${one ? "is declared" : "are declared"} in this project and missing from ` + `.godot/global_script_class_cache.cfg, so a game launched now would not resolve ${one ? "it" : "them"} ` + "either. Try editor_rescan first: it asks the editor already running and starts nothing, and it " + "cleared this in 314ms in a second project with a 31-worker fan-out still importing. It is not " + "always enough, because a file another engine has already imported reads as settled and the " + "walk does not look inside it. project_import refresh_classes rewrites the cache from the " + "declarations on disk and does reach that case, at the cost of a short headless engine, which " + "is worth knowing when something else is already running against this project.";
10218
10833
  }
10219
10834
  function cacheWrittenAt(projectPath) {
10220
- const cache = join11(projectPath, ".godot", "global_script_class_cache.cfg");
10835
+ const cache = join12(projectPath, ".godot", "global_script_class_cache.cfg");
10221
10836
  try {
10222
10837
  return statSync6(cache).mtimeMs;
10223
10838
  } catch {
@@ -10231,14 +10846,14 @@ function declaredSince(projectPath, paths, names, since) {
10231
10846
  return false;
10232
10847
  }
10233
10848
  try {
10234
- return statSync6(join11(projectPath, path.slice("res://".length))).mtimeMs > since;
10849
+ return statSync6(join12(projectPath, path.slice("res://".length))).mtimeMs > since;
10235
10850
  } catch {
10236
10851
  return false;
10237
10852
  }
10238
10853
  });
10239
10854
  }
10240
10855
  function cachedAtMissingPaths(cached, projectPath) {
10241
- return [...cached].filter(([, path]) => path.startsWith("res://") && !existsSync11(join11(projectPath, path.slice("res://".length)))).map(([name]) => name).sort();
10856
+ return [...cached].filter(([, path]) => path.startsWith("res://") && !existsSync11(join12(projectPath, path.slice("res://".length)))).map(([name]) => name).sort();
10242
10857
  }
10243
10858
  function staleAgainst(cached, projectPath) {
10244
10859
  return [...declaredClasses(projectPath)].filter(([name, path]) => cached.get(name) !== path).map(([name]) => name);
@@ -10264,7 +10879,7 @@ var init_class_cache = __esm(() => {
10264
10879
 
10265
10880
  // src/project-scan.ts
10266
10881
  import { existsSync as existsSync12, readdirSync as readdirSync7, readFileSync as readFileSync9 } from "node:fs";
10267
- import { extname, join as join12 } from "node:path";
10882
+ import { extname, join as join13 } from "node:path";
10268
10883
  function skipped(name) {
10269
10884
  return name.startsWith(".") || SKIPPED.has(name);
10270
10885
  }
@@ -10272,10 +10887,10 @@ function steppedOver(directory) {
10272
10887
  return gdignored(directory) || installedByUs(directory);
10273
10888
  }
10274
10889
  function gdignored(directory) {
10275
- return existsSync12(join12(directory, ".gdignore"));
10890
+ return existsSync12(join13(directory, ".gdignore"));
10276
10891
  }
10277
10892
  function installedByUs(directory) {
10278
- return existsSync12(join12(directory, VERSION_MARKER));
10893
+ return existsSync12(join13(directory, VERSION_MARKER));
10279
10894
  }
10280
10895
  function projectStructure(projectPath) {
10281
10896
  const structure = { scenes: 0, scripts: 0, assets: 0, other: 0 };
@@ -10288,7 +10903,7 @@ function projectStructure(projectPath) {
10288
10903
  continue;
10289
10904
  }
10290
10905
  if (entry.isDirectory()) {
10291
- visit(join12(directory, entry.name));
10906
+ visit(join13(directory, entry.name));
10292
10907
  } else if (entry.isFile()) {
10293
10908
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
10294
10909
  if (SCENE_EXTENSIONS.has(extension)) {
@@ -10318,7 +10933,7 @@ function projectFiles(projectPath, extensions) {
10318
10933
  }
10319
10934
  const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
10320
10935
  if (entry.isDirectory()) {
10321
- visit(join12(directory, entry.name), spelled);
10936
+ visit(join13(directory, entry.name), spelled);
10322
10937
  } else if (entry.isFile() && extensions.includes(extname(entry.name).toLowerCase())) {
10323
10938
  found.push(spelled);
10324
10939
  }
@@ -10339,14 +10954,14 @@ function scriptsWithoutUid(projectPath) {
10339
10954
  }
10340
10955
  const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
10341
10956
  if (entry.isDirectory()) {
10342
- visit(join12(directory, entry.name), spelled);
10957
+ visit(join13(directory, entry.name), spelled);
10343
10958
  continue;
10344
10959
  }
10345
10960
  if (!entry.isFile()) {
10346
10961
  continue;
10347
10962
  }
10348
10963
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
10349
- if (CARRIES_A_UID.has(extension) && !existsSync12(join12(directory, `${entry.name}.uid`))) {
10964
+ if (CARRIES_A_UID.has(extension) && !existsSync12(join13(directory, `${entry.name}.uid`))) {
10350
10965
  found.push(spelled);
10351
10966
  }
10352
10967
  }
@@ -10367,7 +10982,7 @@ function projectTexts(projectPath) {
10367
10982
  if (skipped(entry.name)) {
10368
10983
  continue;
10369
10984
  }
10370
- const absolute = join12(directory, entry.name);
10985
+ const absolute = join13(directory, entry.name);
10371
10986
  const spelled = `${prefix}${entry.name}`;
10372
10987
  if (entry.isDirectory()) {
10373
10988
  visit(absolute, `${spelled}/`);
@@ -10397,7 +11012,7 @@ function ignoredTexts(projectPath) {
10397
11012
  if (skipped(entry.name)) {
10398
11013
  continue;
10399
11014
  }
10400
- const absolute = join12(directory, entry.name);
11015
+ const absolute = join13(directory, entry.name);
10401
11016
  const spelled = `${prefix}${entry.name}`;
10402
11017
  if (entry.isDirectory()) {
10403
11018
  visit(absolute, `${spelled}/`, under);
@@ -10437,7 +11052,7 @@ function searchProject(projectPath, options) {
10437
11052
  if (skipped(entry.name)) {
10438
11053
  continue;
10439
11054
  }
10440
- const entryPath = join12(directory, entry.name);
11055
+ const entryPath = join13(directory, entry.name);
10441
11056
  if (entry.isDirectory()) {
10442
11057
  visit(entryPath);
10443
11058
  continue;
@@ -10891,7 +11506,7 @@ import {
10891
11506
  statSync as statSync7,
10892
11507
  writeFileSync as writeFileSync6
10893
11508
  } from "node:fs";
10894
- import { dirname as dirname7, join as join13 } from "node:path";
11509
+ import { dirname as dirname7, join as join14 } from "node:path";
10895
11510
  import { fileURLToPath } from "node:url";
10896
11511
  function editorAddonDigest(from = shippedAddonsDirectory()) {
10897
11512
  return addonsDigest(EDITOR_PLUGINS, from);
@@ -10906,7 +11521,7 @@ function addonsDigest(names, from) {
10906
11521
  if (name === VERSION_MARKER || name === DIGEST_MARKER) {
10907
11522
  continue;
10908
11523
  }
10909
- const path = join13(directory, name);
11524
+ const path = join14(directory, name);
10910
11525
  const inside = `${relative}/${name}`;
10911
11526
  if (statSync7(path).isDirectory()) {
10912
11527
  walk(path, inside);
@@ -10920,7 +11535,7 @@ function addonsDigest(names, from) {
10920
11535
  }
10921
11536
  };
10922
11537
  for (const name of names) {
10923
- walk(join13(from, name), name);
11538
+ walk(join14(from, name), name);
10924
11539
  }
10925
11540
  return hash.digest("hex");
10926
11541
  }
@@ -10951,7 +11566,7 @@ function installedRuntimeDigest(projectPath) {
10951
11566
  return digestMarker(projectPath, RUNTIME_ADDON);
10952
11567
  }
10953
11568
  function digestMarker(projectPath, addon) {
10954
- const marker = join13(projectPath, "addons", addon, DIGEST_MARKER);
11569
+ const marker = join14(projectPath, "addons", addon, DIGEST_MARKER);
10955
11570
  if (!existsSync13(marker)) {
10956
11571
  return null;
10957
11572
  }
@@ -10959,35 +11574,35 @@ function digestMarker(projectPath, addon) {
10959
11574
  return said === "" ? null : said;
10960
11575
  }
10961
11576
  function shippedAddonsDirectory() {
10962
- return join13(dirname7(fileURLToPath(import.meta.url)), "godot", "addons");
11577
+ return join14(dirname7(fileURLToPath(import.meta.url)), "godot", "addons");
10963
11578
  }
10964
11579
  function shippedOperationsScript() {
10965
- return join13(dirname7(fileURLToPath(import.meta.url)), "godot", "operations", "godot_operations.gd");
11580
+ return join14(dirname7(fileURLToPath(import.meta.url)), "godot", "operations", "godot_operations.gd");
10966
11581
  }
10967
11582
  function installAddons(projectPath, from = shippedAddonsDirectory()) {
10968
11583
  const installed = [];
10969
11584
  for (const name of ADDONS) {
10970
- const source = join13(from, name);
10971
- if (!existsSync13(join13(source, name === RUNTIME_ADDON ? "runtime_autoload.gd" : "plugin.cfg"))) {
11585
+ const source = join14(from, name);
11586
+ if (!existsSync13(join14(source, name === RUNTIME_ADDON ? "runtime_autoload.gd" : "plugin.cfg"))) {
10972
11587
  throw new Error(`The package holds no ${name} addon at ${source}.`);
10973
11588
  }
10974
- const target = join13(projectPath, "addons", name);
11589
+ const target = join14(projectPath, "addons", name);
10975
11590
  const replaced = existsSync13(target);
10976
11591
  rmSync5(target, { recursive: true, force: true });
10977
11592
  mkdirSync4(dirname7(target), { recursive: true });
10978
11593
  cpSync(source, target, { recursive: true });
10979
- writeFileSync6(join13(target, VERSION_MARKER), `${SERVER_VERSION}
11594
+ writeFileSync6(join14(target, VERSION_MARKER), `${SERVER_VERSION}
10980
11595
  `);
10981
11596
  installed.push({ name, path: target, replaced });
10982
11597
  }
10983
- writeFileSync6(join13(projectPath, "addons", ADDONS[0], DIGEST_MARKER), `${editorAddonDigest(from)}
11598
+ writeFileSync6(join14(projectPath, "addons", ADDONS[0], DIGEST_MARKER), `${editorAddonDigest(from)}
10984
11599
  `);
10985
- writeFileSync6(join13(projectPath, "addons", RUNTIME_ADDON, DIGEST_MARKER), `${runtimeAddonDigest(from)}
11600
+ writeFileSync6(join14(projectPath, "addons", RUNTIME_ADDON, DIGEST_MARKER), `${runtimeAddonDigest(from)}
10986
11601
  `);
10987
11602
  return installed;
10988
11603
  }
10989
11604
  function installedAddonVersion(projectPath) {
10990
- const marker = join13(projectPath, "addons", ADDONS[0], VERSION_MARKER);
11605
+ const marker = join14(projectPath, "addons", ADDONS[0], VERSION_MARKER);
10991
11606
  if (!existsSync13(marker)) {
10992
11607
  return null;
10993
11608
  }
@@ -10997,13 +11612,13 @@ function installedAddonVersion(projectPath) {
10997
11612
  function removeAddons(projectPath) {
10998
11613
  const removed = [];
10999
11614
  for (const name of ADDONS) {
11000
- const target = join13(projectPath, "addons", name);
11615
+ const target = join14(projectPath, "addons", name);
11001
11616
  if (existsSync13(target)) {
11002
11617
  rmSync5(target, { recursive: true, force: true });
11003
11618
  removed.push(target);
11004
11619
  }
11005
11620
  }
11006
- const addons = join13(projectPath, "addons");
11621
+ const addons = join14(projectPath, "addons");
11007
11622
  if (removed.length > 0 && existsSync13(addons) && readdirSync8(addons).length === 0) {
11008
11623
  rmdirSync(addons);
11009
11624
  removed.push(addons);
@@ -11038,7 +11653,7 @@ function loaderAmong(projectPath, named) {
11038
11653
  }
11039
11654
  let source = null;
11040
11655
  try {
11041
- source = readFileSync11(join13(projectPath, path), "utf8");
11656
+ source = readFileSync11(join14(projectPath, path), "utf8");
11042
11657
  } catch {}
11043
11658
  if (source?.includes(RUNTIME_AUTOLOAD.path)) {
11044
11659
  return { name: entry, path, exists: true };
@@ -11080,8 +11695,8 @@ function enabledPlugins(settings) {
11080
11695
  function inspectProject(projectPath) {
11081
11696
  const problems = [];
11082
11697
  const addons = ADDONS.map((name) => {
11083
- const directory = join13(projectPath, "addons", name);
11084
- const marker = join13(directory, VERSION_MARKER);
11698
+ const directory = join14(projectPath, "addons", name);
11699
+ const marker = join14(directory, VERSION_MARKER);
11085
11700
  const installed = existsSync13(directory);
11086
11701
  const version = existsSync13(marker) ? readFileSync11(marker, "utf8").trim() : null;
11087
11702
  const current = version === SERVER_VERSION;
@@ -11092,7 +11707,7 @@ function inspectProject(projectPath) {
11092
11707
  }
11093
11708
  return { name, installed, version, current };
11094
11709
  });
11095
- const settings = parseProjectGodot(readFileSync11(join13(projectPath, "project.godot"), "utf8"));
11710
+ const settings = parseProjectGodot(readFileSync11(join14(projectPath, "project.godot"), "utf8"));
11096
11711
  const pluginsEnabled = enabledPlugins(settings);
11097
11712
  for (const name of EDITOR_PLUGINS) {
11098
11713
  if (!pluginsEnabled.includes(name)) {
@@ -11108,7 +11723,7 @@ function inspectProject(projectPath) {
11108
11723
  const tracked = trackedByGit(projectPath, [...named.values()]);
11109
11724
  const addonMissing = (path) => addons.some((addon) => !addon.installed && path.startsWith(`addons/${addon.name}/`));
11110
11725
  for (const [name, path] of named) {
11111
- if (!existsSync13(join13(projectPath, path))) {
11726
+ if (!existsSync13(join14(projectPath, path))) {
11112
11727
  if (!addonMissing(path)) {
11113
11728
  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`);
11114
11729
  }
@@ -11256,7 +11871,7 @@ function annotationsOf(spec) {
11256
11871
  openWorldHint: false
11257
11872
  };
11258
11873
  }
11259
- var PROJECT_INFO_SECTIONS, PROJECT_PATH, RUNNING_PROJECT_PATH, RUNNING_PID, SCENE_PATH, WITHOUT_AN_EDITOR = "Through the editor when one serves the project; with none, a headless engine runs the editor's own tool and the answer says so under writtenBy, or readBy for a read. That engine runs every script of the file, where the editor runs only tool scripts, so what a script changes as the file loads or as a property is set is put back before anything is saved, and a file where that cannot be done, such as a scene whose script does not compile, is refused and nothing is written. An editor of the project that is running and has not reached this server is waited for rather than written around.", WHOLE_NUMBERS = 'A whole number of 2^53 or more, such as a 64-bit seed or a resource uid, is written as its digits, {"_type": "int", "value": "9007199254740993"}, the form a read answers with: a JSON number that large can arrive as a neighbour of the one sent, so sent as a number it is refused.', SCRIPT_PATH, RESOURCE_FILE = 'File inside the project, such as "sprites/hero.png" or "materials/steel.tres".', RESOURCE_PATH, NODE_PATH, PROPERTIES, XY, ANIMATION_TRACK, SCRIPT_MODIFICATIONS, INPUT_EVENTS, TILESET_SOURCES, TILEMAP_CELLS, COLOUR, TOOL_SPECS, SPECS_BY_NAME;
11874
+ var PROJECT_INFO_SECTIONS, PROJECT_PATH, RUNNING_PROJECT_PATH, RUNNING_PID, SCENE_PATH, WITHOUT_AN_EDITOR = "Through the editor when one serves the project; with none, a headless engine runs the editor's own tool and the answer says so under writtenBy, or readBy for a read. That engine runs every script of the file, where the editor runs only tool scripts, so what a script changes as the file loads or as a property is set is put back before anything is saved, and a file where that cannot be done, such as a scene whose script does not compile, is refused and nothing is written. An editor of the project that is running and has not reached this server is waited for rather than written around.", WHOLE_NUMBERS = 'A whole number of 2^53 or more, such as a 64-bit seed or a resource uid, is written as its digits, {"_type": "int", "value": "9007199254740993"}, the form a read answers with: a JSON number that large can arrive as a neighbour of the one sent, so sent as a number it is refused.', SCRIPT_PATH, RESOURCE_FILE = 'File inside the project, such as "sprites/hero.png" or "materials/steel.tres".', RESOURCE_PATH, NODE_PATH, PROPERTIES, XY, ANIMATION_TRACK, SCRIPT_MODIFICATIONS, INPUT_EVENTS, TILESET_SOURCES, TILEMAP_CELLS, COLOUR2, TOOL_SPECS, SPECS_BY_NAME;
11260
11875
  var init_tool_definitions = __esm(() => {
11261
11876
  init_launch();
11262
11877
  PROJECT_INFO_SECTIONS = [
@@ -11310,7 +11925,10 @@ var init_tool_definitions = __esm(() => {
11310
11925
  type: { type: "string", enum: ["property", "method"] },
11311
11926
  nodePath: { type: "string", description: "Target node, relative to the AnimationPlayer root." },
11312
11927
  property: { type: "string", description: "For property tracks: the property to animate." },
11313
- method: { type: "string", description: "For method tracks: the method to call." },
11928
+ method: {
11929
+ type: "string",
11930
+ description: "For method tracks: the method to call. The answer's methodFound says whether the target node has it; a method it lacks is still added, since it may be written next, and until it exists each call the track makes fails."
11931
+ },
11314
11932
  keyframes: {
11315
11933
  type: "array",
11316
11934
  items: {
@@ -11412,7 +12030,7 @@ var init_tool_definitions = __esm(() => {
11412
12030
  additionalProperties: false
11413
12031
  }
11414
12032
  };
11415
- COLOUR = {
12033
+ COLOUR2 = {
11416
12034
  type: "object",
11417
12035
  description: "set_theme_color: r, g and b from 0 to 1, with a optional and opaque by default.",
11418
12036
  properties: { r: { type: "number" }, g: { type: "number" }, b: { type: "number" }, a: { type: "number" } },
@@ -11725,7 +12343,8 @@ var init_tool_definitions = __esm(() => {
11725
12343
  name: "project_test",
11726
12344
  title: "Run tests",
11727
12345
  effect: "additive",
11728
- 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 rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines; identical engine messages come back once under engineEntries, with times saying how often they were printed, and engineErrors counts every one. 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. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. On macOS the engine reads user:// off HOME and the run writes where the player keeps theirs, which the answer says under savesNote rather than leaving to be found in a save list. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so 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. A client that sends a progress token is told as each suite finishes, and at least every twenty seconds through a long one, so an idle limit does not end a long tier. 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 does not count, and none count when the project turns off gdUnit4's report/godot/script_error setting. Needs gdUnit4 under addons/gdUnit4.",
12346
+ 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, and engineErrors counts every one. 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. 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: gdUnit4 exits cleanly for one, so the answer says so 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, so an idle limit does not end a long tier. 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 does not count, and none count when the project turns off gdUnit4's report/godot/script_error setting. Needs gdUnit4 under addons/gdUnit4.",
12347
+ 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.",
11729
12348
  parameters: {
11730
12349
  projectPath: PROJECT_PATH,
11731
12350
  path: {
@@ -11891,7 +12510,7 @@ var init_tool_definitions = __esm(() => {
11891
12510
  {
11892
12511
  name: "scene_signal",
11893
12512
  title: "Scene signals",
11894
- description: `Signal connections in a scene file. Each connection answers with its flags as the number and as flagNames; persist is on every connection, since a scene saves no other kind. A connect of a pair already connected says alreadyConnected, with flagsBefore when it changed the flags. ${WITHOUT_AN_EDITOR}`,
12513
+ description: `Signal connections in a scene file. Each connection answers with its flags as the number and as flagNames; persist is on every connection, since a scene saves no other kind. A connect of a pair already connected says alreadyConnected, with flagsBefore when it changed the flags. A connect to a method the target does not have is still written, as the editor's own dialog allows, since the method may be written next; methodFound false says so, and until the method exists the signal fails each time it is emitted. list marks editable false on a connection an instanced scene makes inside itself, which only that scene can change. ${WITHOUT_AN_EDITOR}`,
11895
12514
  parameters: {
11896
12515
  projectPath: PROJECT_PATH,
11897
12516
  scenePath: SCENE_PATH,
@@ -11936,7 +12555,7 @@ var init_tool_definitions = __esm(() => {
11936
12555
  {
11937
12556
  name: "scene_animation",
11938
12557
  title: "Animations",
11939
- description: "Animations in an AnimationPlayer and states in an AnimationTree state machine. read answers with every track of an animation in the shape add_track takes, so a track read off one animation can be added to another. read and read_states need no editor: one serving the project answers and says whether it holds unsaved changes to the scene, and with none a headless engine reads the file without running the scene's scripts. The ops that write need the editor connected.",
12558
+ description: "Animations in an AnimationPlayer and states in an AnimationTree state machine. read answers with every track of an animation in the shape add_track takes, so a track read off one animation can be added to another. read and read_states need no editor: one serving the project answers and says whether it holds unsaved changes to the scene, and with none a headless engine reads the file without running the scene's scripts. The ops that write go through the editor when one serves the project; with none, a headless engine runs the editor's own tool and the answer says so under writtenBy. That engine runs every script of the file, where the editor runs only tool scripts, so what a script changes as the file loads is put back before anything is saved, and a file where that cannot be done, such as a scene whose script does not compile, is refused and nothing is written. An editor of the project that is running and has not reached this server is waited for rather than written around.",
11940
12559
  parameters: {
11941
12560
  projectPath: PROJECT_PATH,
11942
12561
  scenePath: SCENE_PATH,
@@ -11948,7 +12567,7 @@ var init_tool_definitions = __esm(() => {
11948
12567
  animationName: {
11949
12568
  type: "string",
11950
12569
  ops: ["read", "create", "add_track", "add_state"],
11951
- description: "The animation in the player. read: the one to answer with whole; without it, read lists them. add_state: the one that state plays."
12570
+ description: "The animation in the player. read: the one to answer with whole, its tracks a list; without it, read lists them, each with the number of its tracks under tracks. add_state: the one that state plays."
11952
12571
  },
11953
12572
  length: { type: "number", ops: ["create"], description: "create: seconds. Default 1." },
11954
12573
  loopMode: {
@@ -12203,20 +12822,24 @@ var init_tool_definitions = __esm(() => {
12203
12822
  controlType: {
12204
12823
  type: "string",
12205
12824
  ops: ["set_theme_color", "set_theme_font_size"],
12206
- description: 'Theme ops: the Control class, such as "Button".'
12825
+ description: 'Theme ops: the theme type, a Control or Window class such as "Button", or the name of a type variation, which can be any name. A name that is no such class is set as a variation and answered with a note saying so.'
12207
12826
  },
12208
12827
  colorName: {
12209
12828
  type: "string",
12210
12829
  ops: ["set_theme_color"],
12211
12830
  description: 'set_theme_color: such as "font_color".'
12212
12831
  },
12213
- color: { ...COLOUR, ops: ["set_theme_color"] },
12832
+ color: { ...COLOUR2, ops: ["set_theme_color"] },
12214
12833
  fontSizeName: {
12215
12834
  type: "string",
12216
12835
  ops: ["set_theme_font_size"],
12217
12836
  description: 'set_theme_font_size: such as "font_size".'
12218
12837
  },
12219
- size: { type: "number", ops: ["set_theme_font_size"], description: "set_theme_font_size: pixels." }
12838
+ size: {
12839
+ type: "number",
12840
+ ops: ["set_theme_font_size"],
12841
+ description: "set_theme_font_size: pixels, a whole number of 1 or more."
12842
+ }
12220
12843
  },
12221
12844
  requires: ["projectPath", "resourcePath"],
12222
12845
  operations: {
@@ -12421,7 +13044,7 @@ var init_tool_definitions = __esm(() => {
12421
13044
  requires: []
12422
13045
  }
12423
13046
  },
12424
- answer: "stop: endedPid is, for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled, the note naming which property it failed on, and false with the number under stillGoing when what was told to end was still running ten seconds later. endedThrough says how the stop ended a run that was going: close (the game was asked to close and exited, so exitCode is its own), keeper (the process that started the game ended it through the handle it holds), signal (its pid was signalled once shown to still be the run), or editor (the editor playing it was asked). A start that ends the run before it says how under endedPreviousRunThrough, with the same values, and one that refused to signal that run names the property it failed on under previousRunLeftBecause. check: hung is a run killed at timeoutMs, and errors and warnings count what it printed, which comes back under entries with repeats folded together. entries holds the errors and warnings and not the ordinary lines the run printed, which printedLinesNotShown counts; a check given --verbose holds those too, and editor_run start then editor_output reads everything. An engine crash is an error whatever it prints after: the crash handler's report is the entry, and the engine version and the native and GDScript backtraces are its detail, so a crashed run is never booted or clean. arguments is the engine command line the check ran, as a start's is. profile: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, with functionsCounted, scriptMs (the self time of them all) and coveredMs. The runtime addon's own functions, gdharness answering its tools inside the game, are not among them: their self time is runtimeAddonMs, over runtimeAddonFunctions functions. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. frames counts the frames of the profile that arrived, and the engine's totals also hold the work after the last of them, so once complete a function run every frame can show a call more than frames. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. They are also summed over every thread: a function run by WorkerThreadPool or a Thread on several threads at once counts each thread's time, the engine does not say which thread a call ran on, and a function waiting for them has the wait in its own self time, so selfMs is not time taken from frames, and scriptMs can exceed coveredMs, which the note then says. A property's get and set blocks are named Class.@name_getter and Class.@name_setter, as the engine names them, and their rows carry property, the property's name. A game the editor plays that was not started with profile: true says, in the answer, that it was profiled from the moment it was asked. A profile with mark answers marked: true; one with since answers since: \"mark\" and markSetMsAgo, and lessThanAtMark counts the functions left out for reading less than at the mark, which the engine's own totals, capped, can do when they replace the frames' sum.",
13047
+ answer: "start: through is gdharness for a game this server started itself, and editor for one the editor plays. scene is the scene the game started in, the project's main scene when none was given. refreshedClasses names the class_name declarations on disk that the engine's class cache was missing, which were added to it before the game started so the game could resolve them; it is empty when the cache was current. stop: endedPid is, for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled, the note naming which property it failed on, and false with the number under stillGoing when what was told to end was still running ten seconds later. endedThrough says how the stop ended a run that was going: close (the game was asked to close and exited, so exitCode is its own), keeper (the process that started the game ended it through the handle it holds), signal (its pid was signalled once shown to still be the run), or editor (the editor playing it was asked). A start that ends the run before it says how under endedPreviousRunThrough, with the same values, and one that refused to signal that run names the property it failed on under previousRunLeftBecause. check: hung is a run killed at timeoutMs, and errors and warnings count what it printed, which comes back under entries with repeats folded together. entries holds the errors and warnings and not the ordinary lines the run printed, which printedLinesNotShown counts; a check given --verbose holds those too, and editor_run start then editor_output reads everything. An engine crash is an error whatever it prints after: the crash handler's report is the entry, and the engine version and the native and GDScript backtraces are its detail, so a crashed run is never booted or clean. arguments is the engine command line the check ran, as a start's is. profile: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, with functionsCounted, scriptMs (the self time of them all) and coveredMs. The runtime addon's own functions, gdharness answering its tools inside the game, are not among them: their self time is runtimeAddonMs, over runtimeAddonFunctions functions. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. frames counts the frames of the profile that arrived, and the engine's totals also hold the work after the last of them, so once complete a function run every frame can show a call more than frames. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. They are also summed over every thread: a function run by WorkerThreadPool or a Thread on several threads at once counts each thread's time, the engine does not say which thread a call ran on, and a function waiting for them has the wait in its own self time, so selfMs is not time taken from frames, and scriptMs can exceed coveredMs, which the note then says. A property's get and set blocks are named Class.@name_getter and Class.@name_setter, as the engine names them, and their rows carry property, the property's name. A game the editor plays that was not started with profile: true says, in the answer, that it was profiled from the moment it was asked. A profile with mark answers marked: true; one with since answers since: \"mark\" and markSetMsAgo, and lessThanAtMark counts the functions left out for reading less than at the mark, which the engine's own totals, capped, can do when they replace the frames' sum.",
12425
13048
  defaultOperation: "start"
12426
13049
  },
12427
13050
  {
@@ -12589,7 +13212,7 @@ var init_tool_definitions = __esm(() => {
12589
13212
  property: {
12590
13213
  type: "string",
12591
13214
  ops: ["property", "find"],
12592
- description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A packed list, such as a PackedStringArray, steps the same way. A negative number counts from the end. A vector, a colour, a rectangle or a transform is stepped into by its components as GDScript names them, so one axis is one number: "size:x", "global_position:y", "modulate:a", "global_transform:origin:z"; a name the value does not have is refused, naming the ones it has. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that only reads (an engine method the engine marks as reading, or a game method not declared -> void, so queue_free() is refused) and walks into what it returned, which is how a question only a method answers is read in one call: which control holds the focus in the viewport this one is in. An engine method that reads takes literal arguments in its brackets, words in double quotes, numbers, true, false or null, so a control\'s theme is read where it is drawn: \'get_theme_stylebox("panel"):content_margin_top\' or \'get_theme_constant("separation")\'. A game method is given none in a path, since nothing marks one as only reading; runtime_invoke call takes those. A list or a map answers the calls that read it and take no arguments, "_game:run:board:size()" or "_game:inbox:keys()", and not those that change it, such as clear() or sort(). A step that is not there is named, and says what was there instead.'
13215
+ description: `property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A key holding a colon, a bracket or a quote is written in double quotes, 'stock:"base:dew"', as a refusal listing the keys writes it. A packed list, such as a PackedStringArray, steps the same way. A negative number counts from the end. A vector, a colour, a rectangle or a transform is stepped into by its components as GDScript names them, so one axis is one number: "size:x", "global_position:y", "modulate:a", "global_transform:origin:z"; a name the value does not have is refused, naming the ones it has. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that only reads (an engine method the engine marks as reading, or a game method not declared -> void, so queue_free() is refused) and walks into what it returned, which is how a question only a method answers is read in one call: which control holds the focus in the viewport this one is in. An engine method that reads takes literal arguments in its brackets, words in double quotes, numbers, true, false or null, so a control's theme is read where it is drawn: 'get_theme_stylebox("panel"):content_margin_top' or 'get_theme_constant("separation")'. A game method is given none in a path, since nothing marks one as only reading; runtime_invoke call takes those. A list or a map answers the calls that read it and take no arguments, "_game:run:board:size()" or "_game:inbox:keys()", and not those that change it, such as clear() or sort(). A step that is not there is named, and says what was there instead.`
12593
13216
  },
12594
13217
  depth: {
12595
13218
  type: "integer",
@@ -12809,7 +13432,7 @@ var init_tool_definitions = __esm(() => {
12809
13432
  name: "runtime_input",
12810
13433
  title: "Send input to the game",
12811
13434
  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.",
12812
- 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, 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, 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.',
13435
+ 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, 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.`,
12813
13436
  parameters: {
12814
13437
  projectPath: RUNNING_PROJECT_PATH,
12815
13438
  pid: RUNNING_PID,
@@ -12821,7 +13444,7 @@ var init_tool_definitions = __esm(() => {
12821
13444
  says: {
12822
13445
  type: "string",
12823
13446
  ops: ["click"],
12824
- description: "click: the words on the control to click, matched the way runtime_inspect find matches says (a plain word is contained in the text, a pattern with * or ? is a glob over it, and | separates alternatives), among the controls on screen, and pressed in the same frame it was found in, so a panel rebuilding between a find and a click cannot free it first. Words on a label inside a button press the button. A control with something drawn over its centre that would take the pointer, such as a screen shown over it or a dialog stacked above its own, is not on screen; nor is one on a hidden canvas layer, one outside the viewport, or one a container clips away that a click does not scroll. The refusal counts those apart. The matches are ranked the way a person picks what to press: an enabled button before anything else, a disabled one included, then a control whose whole text, or one whole line of it such as the title on a card, is the words before one saying them as part of more, and the best one is pressed; found.picked says why. Several matches at the best rank are refused with each one listed, and index picks one. A match saying the words only as part of more is named under found.partOf, and is refused when another control says exactly the words (hidden, off the screen, covered, or on screen taking clicks without being a button), since it would stand in for that one, and when it lets the pointer through to what is under it; index presses it anyway. Words found only inside longer words (\"out\" in \"Scout\") rank after every other match and are refused unless index is given; words starting a longer word (\"Recruit\" in \"Recruits\") count. An empty field says its placeholder, and a control with no text of its own, a picture on a button, says its accessibility name. An item of an open menu is drawn by the menu rather than held as a control, so a click finds none, and the refusal names the menu and the choose call that picks it."
13447
+ description: "click: the words on the control to click, matched the way runtime_inspect find matches says (a plain word is contained in the text, a pattern with * or ? is a glob over it, and | separates alternatives), among the controls on screen, and pressed in the same frame it was found in, so a panel rebuilding between a find and a click cannot free it first. Words on a label inside a button press the button. A control with something drawn over its centre that would take the pointer, such as a screen shown over it or a dialog stacked above its own, is not on screen; nor is one on a hidden canvas layer, one outside the viewport, or one a container clips away that a click does not scroll. The refusal counts those apart. The matches are ranked the way a person picks what to press: an enabled button before anything else, a disabled one included, then a control whose whole text, or one whole line of it such as the title on a card, is the words before one saying them as part of more, and the best one is pressed; found.picked says why. Several matches at the best rank are refused with each one listed, and index picks one. A match saying the words only as part of more is named under found.partOf, and is refused when another control says exactly the words (hidden, off the screen, covered, or on screen taking clicks without being a button), since it would stand in for that one, and when it lets the pointer through to what is under it; index presses it anyway. Words found only inside longer words (\"out\" in \"Scout\") rank after every other match and are refused unless index is given; words starting a longer word (\"Recruit\" in \"Recruits\") count. An empty field says its placeholder, and a control with no text of its own, a picture on a button, says its accessibility name. A tab, a list entry, a tree row and a menu bar title each count as a match of their own and are pressed where they are drawn, after their control scrolls them into view; a disabled one is refused. An item of an open menu is drawn by the menu rather than held as a control, so a click finds none, and the refusal names the menu and the choose call that picks it."
12825
13448
  },
12826
13449
  action: {
12827
13450
  type: "string",
@@ -34901,12 +35524,12 @@ var init_stdio = __esm(() => {
34901
35524
 
34902
35525
  // src/autoload-failure.ts
34903
35526
  import { existsSync as existsSync15, readFileSync as readFileSync13 } from "node:fs";
34904
- import { join as join16 } from "node:path";
35527
+ import { join as join17 } from "node:path";
34905
35528
  function scriptsAutoloaded(projectPath) {
34906
35529
  const scripts = new Map;
34907
35530
  let settings;
34908
35531
  try {
34909
- settings = parseProjectGodot(readFileSync13(join16(projectPath, "project.godot"), "utf8"));
35532
+ settings = parseProjectGodot(readFileSync13(join17(projectPath, "project.godot"), "utf8"));
34910
35533
  } catch {
34911
35534
  return scripts;
34912
35535
  }
@@ -34915,7 +35538,7 @@ function scriptsAutoloaded(projectPath) {
34915
35538
  scripts.set(`res://${path}`, name);
34916
35539
  continue;
34917
35540
  }
34918
- const scene = join16(projectPath, path);
35541
+ const scene = join17(projectPath, path);
34919
35542
  if (!existsSync15(scene)) {
34920
35543
  continue;
34921
35544
  }
@@ -34928,22 +35551,21 @@ function scriptsAutoloaded(projectPath) {
34928
35551
  return scripts;
34929
35552
  }
34930
35553
  function autoloadNotCompiled(entry, autoloaded) {
34931
- const path = NOT_COMPILED.exec(entry.text)?.[1];
35554
+ const path = notCompiledIn(entry);
34932
35555
  const name = path === undefined ? undefined : autoloaded.get(path);
34933
35556
  return path === undefined || name === undefined ? null : { name, path };
34934
35557
  }
34935
- var NOT_COMPILED;
34936
35558
  var init_autoload_failure = __esm(() => {
34937
35559
  init_resources();
35560
+ init_script_origins();
34938
35561
  init_setup();
34939
- NOT_COMPILED = /^Failed to load script "(res:\/\/[^"]+)" with error "Compilation failed"/;
34940
35562
  });
34941
35563
 
34942
35564
  // src/runtime-client.ts
34943
35565
  import { existsSync as existsSync16, readdirSync as readdirSync10, readFileSync as readFileSync14, statSync as statSync8, unlinkSync as unlinkSync2 } from "node:fs";
34944
35566
  import { createConnection } from "node:net";
34945
35567
  import { tmpdir as tmpdir4 } from "node:os";
34946
- import { dirname as dirname10, join as join17, resolve as resolve4 } from "node:path";
35568
+ import { dirname as dirname10, join as join18, resolve as resolve4 } from "node:path";
34947
35569
  import { setTimeout as delay } from "node:timers/promises";
34948
35570
  function runtimeDirectory(variables = process.env) {
34949
35571
  const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
@@ -34951,19 +35573,19 @@ function runtimeDirectory(variables = process.env) {
34951
35573
  return explicit;
34952
35574
  }
34953
35575
  const perUser = envValue("XDG_RUNTIME_DIR", variables);
34954
- return join17(perUser ?? tmpdir4(), "gdharness");
35576
+ return join18(perUser ?? tmpdir4(), "gdharness");
34955
35577
  }
34956
35578
  function runtimeDirectories(variables = process.env) {
34957
35579
  const candidates = [runtimeDirectory(variables)];
34958
35580
  const fallbacks = process.platform === "win32" ? [envValue("SystemRoot", variables) ?? envValue("windir", variables) ?? "C:\\Windows"] : ["/tmp", "/var/tmp"];
34959
35581
  for (const base of fallbacks) {
34960
- candidates.push(join17(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
35582
+ candidates.push(join18(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
34961
35583
  }
34962
35584
  return [...new Set(candidates.map((path) => resolve4(path)))];
34963
35585
  }
34964
35586
  function errorReportOf(pid, directories = runtimeDirectories()) {
34965
35587
  for (const directory of directories) {
34966
- const path = join17(directory, `runtime-${pid}.log`);
35588
+ const path = join18(directory, `runtime-${pid}.log`);
34967
35589
  if (existsSync16(path)) {
34968
35590
  return path;
34969
35591
  }
@@ -35113,14 +35735,14 @@ function announcedIn(directory) {
35113
35735
  for (const entry of readdirSync10(directory)) {
35114
35736
  const report = ERROR_REPORT_PATTERN.exec(entry);
35115
35737
  if (report) {
35116
- sweepErrorReport(join17(directory, entry), Number.parseInt(report[1] ?? "", 10));
35738
+ sweepErrorReport(join18(directory, entry), Number.parseInt(report[1] ?? "", 10));
35117
35739
  continue;
35118
35740
  }
35119
35741
  const match = ANNOUNCEMENT_PATTERN.exec(entry);
35120
35742
  if (!match) {
35121
35743
  continue;
35122
35744
  }
35123
- const file = join17(directory, entry);
35745
+ const file = join18(directory, entry);
35124
35746
  const pid = Number.parseInt(match[1] ?? "", 10);
35125
35747
  const running = alive(pid);
35126
35748
  entries.push({ file, pid, running });
@@ -35148,7 +35770,7 @@ function announcedIn(directory) {
35148
35770
  found.push(announced);
35149
35771
  }
35150
35772
  const held = new Set(entries.map((one) => one.file));
35151
- const here = join17(directory, ".");
35773
+ const here = join18(directory, ".");
35152
35774
  for (const file of verdicts.keys()) {
35153
35775
  if (dirname10(file) === here && !held.has(file)) {
35154
35776
  verdicts.delete(file);
@@ -35157,7 +35779,7 @@ function announcedIn(directory) {
35157
35779
  return found;
35158
35780
  }
35159
35781
  async function announcementEnded(pid, directories = runtimeDirectories()) {
35160
- const files = directories.map((directory) => join17(directory, `runtime-${pid}.json`)).filter((file) => existsSync16(file));
35782
+ const files = directories.map((directory) => join18(directory, `runtime-${pid}.json`)).filter((file) => existsSync16(file));
35161
35783
  const first = files[0];
35162
35784
  if (first === undefined) {
35163
35785
  return;
@@ -35569,9 +36191,9 @@ var init_runtime_client = __esm(() => {
35569
36191
 
35570
36192
  // src/boot-note.ts
35571
36193
  import { mkdirSync as mkdirSync6, readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "node:fs";
35572
- import { dirname as dirname11, join as join18 } from "node:path";
36194
+ import { dirname as dirname11, join as join19 } from "node:path";
35573
36195
  function bootNotePath(projectPath) {
35574
- return join18(projectPath, ".godot", "gdharness-boot.json");
36196
+ return join19(projectPath, ".godot", "gdharness-boot.json");
35575
36197
  }
35576
36198
  function readScenes(projectPath) {
35577
36199
  try {
@@ -35618,9 +36240,9 @@ var init_boot_note = __esm(() => {
35618
36240
 
35619
36241
  // src/breakpoint-note.ts
35620
36242
  import { mkdirSync as mkdirSync7, readFileSync as readFileSync16, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "node:fs";
35621
- import { dirname as dirname12, isAbsolute as isAbsolute4, join as join19, relative as relative3 } from "node:path";
36243
+ import { dirname as dirname12, isAbsolute as isAbsolute4, join as join20, relative as relative3 } from "node:path";
35622
36244
  function breakpointNotePath(projectPath) {
35623
- return join19(projectPath, ".godot", "gdharness-breakpoints.json");
36245
+ return join20(projectPath, ".godot", "gdharness-breakpoints.json");
35624
36246
  }
35625
36247
  function writeBreakpointNote(projectPath, held) {
35626
36248
  const path = breakpointNotePath(projectPath);
@@ -35661,7 +36283,7 @@ function readBreakpointNote(projectPath) {
35661
36283
  }
35662
36284
  const lines = Array.isArray(listed) ? listed.filter((line) => Number.isInteger(line) && line > 0) : [];
35663
36285
  if (lines.length > 0) {
35664
- held.push({ scriptPath: realPathOr(join19(projectPath, script)), lines });
36286
+ held.push({ scriptPath: realPathOr(join20(projectPath, script)), lines });
35665
36287
  }
35666
36288
  }
35667
36289
  return held;
@@ -35711,9 +36333,9 @@ var init_whole_file = __esm(() => {
35711
36333
 
35712
36334
  // src/class-note.ts
35713
36335
  import { mkdirSync as mkdirSync8, readFileSync as readFileSync17 } from "node:fs";
35714
- import { dirname as dirname13, join as join20 } from "node:path";
36336
+ import { dirname as dirname13, join as join21 } from "node:path";
35715
36337
  function classNotePath(projectPath) {
35716
- return join20(projectPath, ".godot", "gdharness-classes.json");
36338
+ return join21(projectPath, ".godot", "gdharness-classes.json");
35717
36339
  }
35718
36340
  function readClassNote(projectPath) {
35719
36341
  try {
@@ -35760,9 +36382,9 @@ var init_class_note = __esm(() => {
35760
36382
 
35761
36383
  // src/config-pin.ts
35762
36384
  import { existsSync as existsSync17, readFileSync as readFileSync18 } from "node:fs";
35763
- import { join as join21 } from "node:path";
36385
+ import { join as join22 } from "node:path";
35764
36386
  function versionsNamedInConfig(projectPath) {
35765
- const config = join21(projectPath, ".mcp.json");
36387
+ const config = join22(projectPath, ".mcp.json");
35766
36388
  if (!existsSync17(config)) {
35767
36389
  return [];
35768
36390
  }
@@ -35790,9 +36412,9 @@ var init_config_pin = __esm(() => {
35790
36412
 
35791
36413
  // src/cut-test-run.ts
35792
36414
  import { existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync19, rmSync as rmSync9, writeFileSync as writeFileSync11 } from "node:fs";
35793
- import { dirname as dirname14, join as join22 } from "node:path";
36415
+ import { dirname as dirname14, join as join23 } from "node:path";
35794
36416
  function keptAt(projectPath) {
35795
- return join22(projectPath, ".godot", "gdharness-reports", "cut-run.json");
36417
+ return join23(projectPath, ".godot", "gdharness-reports", "cut-run.json");
35796
36418
  }
35797
36419
  function keepCutTestRun(projectPath, run) {
35798
36420
  const path = keptAt(projectPath);
@@ -36631,10 +37253,10 @@ import {
36631
37253
  writeSync
36632
37254
  } from "node:fs";
36633
37255
  import { tmpdir as tmpdir5 } from "node:os";
36634
- import { join as join23 } from "node:path";
37256
+ import { join as join24 } from "node:path";
36635
37257
  import { setTimeout as delay3 } from "node:timers/promises";
36636
37258
  function helperAssembly(directory = tmpdir5()) {
36637
- return join23(directory, `gdharness-desktop-${DIGEST}.dll`);
37259
+ return join24(directory, `gdharness-desktop-${DIGEST}.dll`);
36638
37260
  }
36639
37261
  async function ensureHelperCompiled(directory = tmpdir5(), budgetMs = HELPER_COMPILE_MS, wait = true) {
36640
37262
  const assembly = helperAssembly(directory);
@@ -36674,8 +37296,8 @@ async function ensureHelperCompiled(directory = tmpdir5(), budgetMs = HELPER_COM
36674
37296
  return `the desktop helper was not compiled within ${String(budgetMs)} ms`;
36675
37297
  }
36676
37298
  async function compileHelper(directory, assembly, budgetMs) {
36677
- const scratch = mkdtempSync2(join23(directory, "gdharness-desktop-source-"));
36678
- const source = join23(scratch, "helper.cs");
37299
+ const scratch = mkdtempSync2(join24(directory, "gdharness-desktop-source-"));
37300
+ const source = join24(scratch, "helper.cs");
36679
37301
  writeFileSync12(source, SOURCE, "utf8");
36680
37302
  const literal = (path) => `'${path.replaceAll("'", "''")}'`;
36681
37303
  const script = [
@@ -36889,12 +37511,12 @@ import {
36889
37511
  statSync as statSync10,
36890
37512
  writeFileSync as writeFileSync13
36891
37513
  } from "node:fs";
36892
- import { dirname as dirname15, join as join24 } from "node:path";
37514
+ import { dirname as dirname15, join as join25 } from "node:path";
36893
37515
  function editorLogPath(projectPath) {
36894
- return join24(projectPath, ".godot", "gdharness-editor.log");
37516
+ return join25(projectPath, ".godot", "gdharness-editor.log");
36895
37517
  }
36896
37518
  function editorLogNotePath(projectPath) {
36897
- return join24(projectPath, ".godot", "gdharness-editor.json");
37519
+ return join25(projectPath, ".godot", "gdharness-editor.json");
36898
37520
  }
36899
37521
  function readEditorLogNote(projectPath) {
36900
37522
  try {
@@ -37930,7 +38552,7 @@ var init_rename = __esm(() => {
37930
38552
  import { existsSync as existsSync22, realpathSync as realpathSync4 } from "node:fs";
37931
38553
  import { readFile, realpath } from "node:fs/promises";
37932
38554
  import { createConnection as createConnection3 } from "node:net";
37933
- import { dirname as dirname17, join as join25, resolve as resolve5 } from "node:path";
38555
+ import { dirname as dirname17, join as join26, resolve as resolve5 } from "node:path";
37934
38556
  import { setTimeout as delay4 } from "node:timers/promises";
37935
38557
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
37936
38558
  function initializeTimeoutMs() {
@@ -38608,7 +39230,7 @@ async function resolveLSPPaths(projectPathValue, scriptPathValue) {
38608
39230
  } catch {
38609
39231
  throw new ArgumentRefusal(`Project path does not exist: ${requestedProjectPath}`);
38610
39232
  }
38611
- if (!existsSync22(join25(projectPath, "project.godot"))) {
39233
+ if (!existsSync22(join26(projectPath, "project.godot"))) {
38612
39234
  throw new ArgumentRefusal(`Not a Godot project: ${projectPath}. Point projectPath at the directory holding project.godot.`);
38613
39235
  }
38614
39236
  const contained = resolveWithinProject(projectPath, scriptPathValue);
@@ -38769,7 +39391,7 @@ var init_lsp_client = __esm(() => {
38769
39391
 
38770
39392
  // src/rename-symbol.ts
38771
39393
  import { existsSync as existsSync23, mkdirSync as mkdirSync12, readdirSync as readdirSync11, realpathSync as realpathSync5, rmSync as rmSync12, writeFileSync as writeFileSync15 } from "node:fs";
38772
- import { basename as basename7, dirname as dirname18, join as join26, relative as relative4, resolve as resolve6 } from "node:path";
39394
+ import { basename as basename7, dirname as dirname18, join as join27, relative as relative4, resolve as resolve6 } from "node:path";
38773
39395
  function resourcePathOf(projectPath, absolute) {
38774
39396
  const inside = relative4(onDiskSpelling(projectPath), onDiskSpelling(absolute));
38775
39397
  if (inside === "" || inside.startsWith("..") || /^[A-Za-z]:/.test(inside)) {
@@ -38782,7 +39404,7 @@ function onDiskSpelling(path) {
38782
39404
  let at = resolve6(path);
38783
39405
  for (;; ) {
38784
39406
  try {
38785
- return join26(realpathSync5.native(at), ...missing.reverse());
39407
+ return join27(realpathSync5.native(at), ...missing.reverse());
38786
39408
  } catch {
38787
39409
  const parent = dirname18(at);
38788
39410
  if (parent === at) {
@@ -39210,15 +39832,15 @@ ${mention.kind}`;
39210
39832
  };
39211
39833
  }
39212
39834
  function writeLeftAloneDetail(projectPath, symbol, detail) {
39213
- const directory = join26(projectPath, ".godot", "gdharness-renames");
39835
+ const directory = join27(projectPath, ".godot", "gdharness-renames");
39214
39836
  mkdirSync12(directory, { recursive: true });
39215
39837
  detailWrites += 1;
39216
- const path = join26(directory, `${String(Date.now())}-${String(detailWrites).padStart(6, "0")}-${String(process.pid)}-${symbol}.json`);
39838
+ const path = join27(directory, `${String(Date.now())}-${String(detailWrites).padStart(6, "0")}-${String(process.pid)}-${symbol}.json`);
39217
39839
  writeFileSync15(path, `${JSON.stringify(detail, null, 2)}
39218
39840
  `, "utf8");
39219
39841
  const kept = readdirSync11(directory).filter((name) => name.endsWith(".json")).sort().reverse();
39220
39842
  for (const old of kept.slice(10)) {
39221
- rmSync12(join26(directory, old), { force: true });
39843
+ rmSync12(join27(directory, old), { force: true });
39222
39844
  }
39223
39845
  return path;
39224
39846
  }
@@ -39267,10 +39889,10 @@ import {
39267
39889
  statSync as statSync11,
39268
39890
  writeFileSync as writeFileSync16
39269
39891
  } from "node:fs";
39270
- import { basename as basename8, dirname as dirname19, extname as extname3, join as join27, relative as relative5 } from "node:path";
39892
+ import { basename as basename8, dirname as dirname19, extname as extname3, join as join28, relative as relative5 } from "node:path";
39271
39893
  function filesUnder(directory) {
39272
39894
  return readdirSync12(directory, { withFileTypes: true }).flatMap((entry) => {
39273
- const path = join27(directory, entry.name);
39895
+ const path = join28(directory, entry.name);
39274
39896
  return entry.isDirectory() ? filesUnder(path) : [path];
39275
39897
  });
39276
39898
  }
@@ -39291,8 +39913,8 @@ function unseenByTheEngine(projectPath, path) {
39291
39913
  return `${path} is under ${step}, and the engine passes over every directory whose name starts with a dot`;
39292
39914
  }
39293
39915
  if (at < steps.length - 1) {
39294
- directory = join27(directory, step);
39295
- if (existsSync24(join27(directory, ".gdignore"))) {
39916
+ directory = join28(directory, step);
39917
+ if (existsSync24(join28(directory, ".gdignore"))) {
39296
39918
  return `${path} is under a directory holding a .gdignore, which the engine does not load`;
39297
39919
  }
39298
39920
  }
@@ -39304,12 +39926,12 @@ function installedAddonOf(projectPath, path) {
39304
39926
  if (steps[0] !== "addons" || steps[1] === undefined) {
39305
39927
  return null;
39306
39928
  }
39307
- return existsSync24(join27(projectPath, "addons", steps[1], VERSION_MARKER)) ? `res://addons/${steps[1]}` : null;
39929
+ return existsSync24(join28(projectPath, "addons", steps[1], VERSION_MARKER)) ? `res://addons/${steps[1]}` : null;
39308
39930
  }
39309
39931
  function binaryResources(projectPath) {
39310
39932
  const found = [];
39311
39933
  const visit = (directory, prefix) => {
39312
- if (existsSync24(join27(directory, ".gdignore"))) {
39934
+ if (existsSync24(join28(directory, ".gdignore"))) {
39313
39935
  return;
39314
39936
  }
39315
39937
  for (const entry of readdirSync12(directory, { withFileTypes: true })) {
@@ -39318,7 +39940,7 @@ function binaryResources(projectPath) {
39318
39940
  }
39319
39941
  const path = `${prefix}${entry.name}`;
39320
39942
  if (entry.isDirectory()) {
39321
- visit(join27(directory, entry.name), `${path}/`);
39943
+ visit(join28(directory, entry.name), `${path}/`);
39322
39944
  } else if (/\.(scn|res)$/i.test(entry.name)) {
39323
39945
  found.push(path);
39324
39946
  }
@@ -39328,7 +39950,7 @@ function binaryResources(projectPath) {
39328
39950
  return found;
39329
39951
  }
39330
39952
  function targetPathOf(projectPath, absolute) {
39331
- const inside = relative5(onDiskSpelling(projectPath), join27(onDiskSpelling(dirname19(absolute)), basename8(absolute)));
39953
+ const inside = relative5(onDiskSpelling(projectPath), join28(onDiskSpelling(dirname19(absolute)), basename8(absolute)));
39332
39954
  if (inside === "" || inside.startsWith("..") || /^[A-Za-z]:/.test(inside)) {
39333
39955
  return null;
39334
39956
  }
@@ -39383,7 +40005,7 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
39383
40005
  return { ok: false, reason: `${unseen}, so nothing there could load what was moved.` };
39384
40006
  }
39385
40007
  if (!isDirectory) {
39386
- const owner = SIDECARS.map((sidecar) => from.endsWith(sidecar) ? from.slice(0, -sidecar.length) : null).find((path) => path !== null && existsSync24(join27(projectPath, path.slice("res://".length))));
40008
+ const owner = SIDECARS.map((sidecar) => from.endsWith(sidecar) ? from.slice(0, -sidecar.length) : null).find((path) => path !== null && existsSync24(join28(projectPath, path.slice("res://".length))));
39387
40009
  if (owner !== undefined && owner !== null) {
39388
40010
  return { ok: false, reason: `${from} belongs to ${owner} and moves with it: move ${owner} instead.` };
39389
40011
  }
@@ -39417,12 +40039,12 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
39417
40039
  scripts,
39418
40040
  moves,
39419
40041
  named: [from],
39420
- exists: (path) => existsSync24(join27(projectPath, path.slice("res://".length)))
40042
+ exists: (path) => existsSync24(join28(projectPath, path.slice("res://".length)))
39421
40043
  }, { edits: [], mentions: [] });
39422
40044
  plan = mentionIgnored(plan, ignoredTexts(projectPath), null, [from]);
39423
40045
  const binaries = [];
39424
40046
  for (const path of binaryResources(projectPath)) {
39425
- const bytes = readFileSync23(join27(projectPath, path.slice("res://".length)));
40047
+ const bytes = readFileSync23(join28(projectPath, path.slice("res://".length)));
39426
40048
  const names = [...moves.keys()].find((moving) => bytes.includes(moving));
39427
40049
  if (names !== undefined) {
39428
40050
  binaries.push({
@@ -39441,7 +40063,7 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
39441
40063
  }));
39442
40064
  const left = leftAloneReport(mentions);
39443
40065
  const rewrites = rewritesOf(plan, texts, null);
39444
- const absoluteOf = (path) => join27(projectPath, path.slice("res://".length));
40066
+ const absoluteOf = (path) => join28(projectPath, path.slice("res://".length));
39445
40067
  const rewritten = new Map(rewrites.map((rewrite) => [rewrite.path, rewrite]));
39446
40068
  const moved = [...moves].map(([source, target]) => {
39447
40069
  const rewrite = rewritten.get(source);
@@ -42882,742 +43504,243 @@ var init_godot_bridge = __esm(() => {
42882
43504
  this.connectionInfo.greeted = true;
42883
43505
  const pid = this.connectionInfo.editorPid;
42884
43506
  if (!this.connectionInfo.openedByAServer && pid !== undefined && this.oneShotReader !== null) {
42885
- this.identify(pid);
42886
- return;
42887
- }
42888
- this.emitBridgeEvent("godot_connected", { projectPath: message.project_path });
42889
- }
42890
- return;
42891
- case "pong":
42892
- if (this.connectionInfo) {
42893
- this.connectionInfo.lastPongAt = new Date;
42894
- }
42895
- return;
42896
- }
42897
- }
42898
- invokeToolDirect(toolName, args, counted, resourceKey) {
42899
- if (!this.isConnected()) {
42900
- return Promise.reject(new Error("Godot is not connected"));
42901
- }
42902
- const requestId = randomUUID();
42903
- const message = {
42904
- type: "tool_invoke",
42905
- id: requestId,
42906
- tool: toolName,
42907
- args
42908
- };
42909
- return new Promise((resolve, reject) => {
42910
- const timeout = setTimeout(() => {
42911
- this.pendingRequests.delete(requestId);
42912
- reject(new BridgeTimeout(toolName, this.timeoutMs));
42913
- }, this.timeoutMs);
42914
- this.pendingRequests.set(requestId, {
42915
- toolName,
42916
- timeout,
42917
- resolve,
42918
- reject,
42919
- startedAt: Date.now(),
42920
- resourceKey,
42921
- counted
42922
- });
42923
- this.emitBridgeEvent("tool_start", {
42924
- tool: toolName,
42925
- id: requestId,
42926
- args
42927
- });
42928
- try {
42929
- this.sendMessage(message);
42930
- } catch (error) {
42931
- clearTimeout(timeout);
42932
- this.pendingRequests.delete(requestId);
42933
- reject(toError(error));
42934
- }
42935
- });
42936
- }
42937
- sendMessage(message) {
42938
- if (this.socket?.readyState !== import_websocket.default.OPEN) {
42939
- throw new Refusal("Godot is not connected");
42940
- }
42941
- this.socket.send(JSON.stringify(message));
42942
- }
42943
- startKeepalive() {
42944
- this.stopKeepalive();
42945
- this.pingInterval = setInterval(() => {
42946
- if (!this.isConnected()) {
42947
- return;
42948
- }
42949
- try {
42950
- const ping = { type: "ping" };
42951
- this.sendMessage(ping);
42952
- } catch (error) {
42953
- this.log("warn", `Failed to send ping: ${errorMessage(error)}`);
42954
- }
42955
- }, KEEPALIVE_INTERVAL_MS);
42956
- }
42957
- stopKeepalive() {
42958
- if (!this.pingInterval) {
42959
- return;
42960
- }
42961
- clearInterval(this.pingInterval);
42962
- this.pingInterval = null;
42963
- }
42964
- belongsElsewhere(theirProject) {
42965
- if (this.ownProject === null || theirProject.trim() === "") {
42966
- return false;
42967
- }
42968
- return !isSameDirectory(this.ownProject, theirProject);
42969
- }
42970
- sendElsewhere(theirProject) {
42971
- const said = `this server serves ${this.ownProject ?? ""}, and that editor has ${theirProject} open`;
42972
- this.log("warn", `Turning away an editor from another project: ${said}`);
42973
- const stranger = this.socket;
42974
- this.socket = null;
42975
- this.connectionInfo = null;
42976
- this.stopKeepalive();
42977
- const fits = closeReason(said, "start") === said;
42978
- stranger?.close(OTHER_PROJECT_CLOSE_CODE, fits ? said : closeReason(`this server serves ${this.ownProject ?? ""}`, "end"));
42979
- }
42980
- elsewhereUnlessOneShot(theirProject, pid, openedByAServer) {
42981
- const known = pid === undefined ? undefined : this.oneShots.get(pid);
42982
- if (pid !== undefined && known !== undefined) {
42983
- this.turnAway(pid, known);
42984
- return;
42985
- }
42986
- const info = this.connectionInfo;
42987
- const socket = this.socket;
42988
- const reader = this.oneShotReader;
42989
- if (pid === undefined || openedByAServer || info === null || reader === null) {
42990
- this.sendElsewhere(theirProject);
42991
- return;
42992
- }
42993
- info.identifying = true;
42994
- reader(pid).catch(() => {
42995
- return;
42996
- }).then((option) => {
42997
- if (this.socket !== socket || this.connectionInfo !== info) {
42998
- return;
42999
- }
43000
- if (typeof option === "string") {
43001
- this.oneShots.set(pid, option);
43002
- this.turnAway(pid, option);
43003
- return;
43004
- }
43005
- this.sendElsewhere(theirProject);
43006
- });
43007
- }
43008
- identify(pid) {
43009
- const known = this.oneShots.get(pid);
43010
- if (known !== undefined) {
43011
- this.turnAway(pid, known);
43012
- return;
43013
- }
43014
- const info = this.connectionInfo;
43015
- const socket = this.socket;
43016
- const reader = this.oneShotReader;
43017
- if (info === null || reader === null) {
43018
- return;
43019
- }
43020
- info.identifying = true;
43021
- reader(pid).catch(() => {
43022
- return;
43023
- }).then((option) => {
43024
- if (this.socket !== socket || this.connectionInfo !== info) {
43025
- return;
43026
- }
43027
- if (typeof option === "string") {
43028
- this.oneShots.set(pid, option);
43029
- this.turnAway(pid, option);
43030
- return;
43031
- }
43032
- info.identifying = false;
43033
- this.log("info", `Godot editor identified: pid ${pid}`);
43034
- this.emitBridgeEvent("godot_connected", { projectPath: info.projectPath });
43035
- });
43036
- }
43037
- turnAway(pid, option) {
43038
- const said = `pid ${pid} was started with ${option}, which does one job and quits, so it is not an editor`;
43039
- this.log("info", `Turning away an engine run: ${said}`);
43040
- this.turnedAway = { pid, option, at: new Date };
43041
- const run = this.socket;
43042
- this.socket = null;
43043
- this.connectionInfo = null;
43044
- this.stopKeepalive();
43045
- run?.close(ONE_SHOT_CLOSE_CODE, closeReason(said, "start"));
43046
- }
43047
- handleDisconnect(disconnectedSocket, reason) {
43048
- if (disconnectedSocket && this.socket && disconnectedSocket !== this.socket) {
43049
- this.log("debug", "Ignoring stale Godot socket disconnect event");
43050
- return;
43051
- }
43052
- this.stopKeepalive();
43053
- this.socket = null;
43054
- this.connectionInfo = null;
43055
- this.emitBridgeEvent("godot_disconnected", {});
43056
- this.rejectAllPending(reason);
43057
- this.resourceQueues.clear();
43058
- }
43059
- emitBridgeEvent(eventName, payload) {
43060
- this.emit(eventName, payload);
43061
- }
43062
- rejectAllPending(error) {
43063
- for (const pending of this.pendingRequests.values()) {
43064
- clearTimeout(pending.timeout);
43065
- pending.reject(error);
43066
- }
43067
- this.pendingRequests.clear();
43068
- }
43069
- enqueueResourceRequest(resourceKey, task) {
43070
- const previous = this.resourceQueues.get(resourceKey) ?? Promise.resolve();
43071
- const taskPromise = previous.catch(() => {
43072
- return;
43073
- }).then(task);
43074
- const tail = taskPromise.then(() => {
43075
- return;
43076
- }, () => {
43077
- return;
43078
- });
43079
- this.resourceQueues.set(resourceKey, tail);
43080
- return taskPromise.finally(() => {
43081
- if (this.resourceQueues.get(resourceKey) === tail) {
43082
- this.resourceQueues.delete(resourceKey);
43083
- }
43084
- });
43085
- }
43086
- getResourceKey(args) {
43087
- const scenePath = this.getStringArg(args, "scenePath") ?? this.getStringArg(args, "scene_path");
43088
- if (scenePath) {
43089
- return `scene:${scenePath}`;
43090
- }
43091
- const resourcePath = this.getStringArg(args, "resourcePath") ?? this.getStringArg(args, "resource_path");
43092
- if (resourcePath) {
43093
- return `resource:${resourcePath}`;
43094
- }
43095
- return;
43096
- }
43097
- getStringArg(args, key) {
43098
- const value = args[key];
43099
- return typeof value === "string" && value.length > 0 ? value : undefined;
43100
- }
43101
- isIncomingMessage(value) {
43102
- if (!value || typeof value !== "object") {
43103
- return false;
43104
- }
43105
- const message = value;
43106
- const type = message["type"];
43107
- if (type !== "tool_result" && type !== "pong" && type !== "godot_ready") {
43108
- return false;
43109
- }
43110
- if (type === "pong") {
43111
- return true;
43112
- }
43113
- if (type === "godot_ready") {
43114
- return typeof message["project_path"] === "string";
43115
- }
43116
- return typeof message["id"] === "string" && typeof message["success"] === "boolean" && (message["error"] === undefined || typeof message["error"] === "string") && (message["busy"] === undefined || typeof message["busy"] === "boolean");
43117
- }
43118
- log(level, message) {
43119
- console.error(`[${new Date().toISOString()}] [GodotBridge:${level.toUpperCase()}] ${message}`);
43120
- }
43121
- };
43122
- });
43123
-
43124
- // src/junit.ts
43125
- function decoded(text) {
43126
- return text.replace(/&(#x[0-9a-fA-F]+|#[0-9]+|[a-zA-Z]+);/g, (whole, entity) => {
43127
- if (entity.startsWith("#")) {
43128
- const code = entity.startsWith("#x") ? Number.parseInt(entity.slice(2), 16) : Number.parseInt(entity.slice(1), 10);
43129
- if (!Number.isInteger(code) || code < 0 || code > 1114111) {
43130
- throw new MalformedReportError(`Not a character: ${whole}`);
43131
- }
43132
- return String.fromCodePoint(code);
43133
- }
43134
- const named = ENTITIES[entity];
43135
- if (named === undefined) {
43136
- throw new MalformedReportError(`Unknown entity ${whole}`);
43137
- }
43138
- return named;
43139
- });
43140
- }
43141
- function tagEnd(source, from) {
43142
- let quote = null;
43143
- for (let at = from + 1;at < source.length; at += 1) {
43144
- const character = source[at];
43145
- if (quote !== null) {
43146
- if (character === quote) {
43147
- quote = null;
43148
- }
43149
- } else if (character === '"' || character === "'") {
43150
- quote = character;
43151
- } else if (character === ">") {
43152
- return at;
43153
- }
43154
- }
43155
- return -1;
43156
- }
43157
- function parseXml(source) {
43158
- const root = { name: "", attributes: {}, children: [], text: "" };
43159
- const open = [root];
43160
- let at = 0;
43161
- const current = () => {
43162
- const element = open.at(-1);
43163
- if (element === undefined) {
43164
- throw new MalformedReportError("A close tag with nothing open");
43165
- }
43166
- return element;
43167
- };
43168
- while (at < source.length) {
43169
- const next = source.indexOf("<", at);
43170
- if (next === -1) {
43171
- current().text += decoded(source.slice(at));
43172
- break;
43173
- }
43174
- current().text += decoded(source.slice(at, next));
43175
- at = next;
43176
- if (source.startsWith("<![CDATA[", at)) {
43177
- const end = source.indexOf("]]>", at);
43178
- if (end === -1) {
43179
- throw new MalformedReportError("CDATA that never ends");
43180
- }
43181
- current().text += source.slice(at + "<![CDATA[".length, end);
43182
- at = end + "]]>".length;
43183
- continue;
43184
- }
43185
- if (source.startsWith("<!--", at)) {
43186
- const end = source.indexOf("-->", at);
43187
- if (end === -1) {
43188
- throw new MalformedReportError("A comment that never ends");
43189
- }
43190
- at = end + "-->".length;
43191
- continue;
43192
- }
43193
- if (source.startsWith("<?", at) || source.startsWith("<!", at)) {
43194
- const end = source.indexOf(">", at);
43195
- if (end === -1) {
43196
- throw new MalformedReportError("A declaration that never ends");
43197
- }
43198
- at = end + 1;
43199
- continue;
43200
- }
43201
- const end = tagEnd(source, at);
43202
- if (end === -1) {
43203
- throw new MalformedReportError("A tag that never ends");
43204
- }
43205
- const tag = source.slice(at + 1, end);
43206
- at = end + 1;
43207
- if (tag.startsWith("/")) {
43208
- const closing = tag.slice(1).trim();
43209
- const element = current();
43210
- if (element === root || element.name !== closing) {
43211
- throw new MalformedReportError(`</${closing}> closes <${element.name || "nothing"}>`);
43212
- }
43213
- open.pop();
43214
- continue;
43215
- }
43216
- const selfClosing = tag.endsWith("/");
43217
- const body = selfClosing ? tag.slice(0, -1) : tag;
43218
- const nameMatch = /^([^\s/>]+)/.exec(body);
43219
- if (!nameMatch?.[1]) {
43220
- throw new MalformedReportError(`A tag with no name: <${tag}>`);
43221
- }
43222
- const attributes = {};
43223
- let read = nameMatch[0].length;
43224
- ATTRIBUTE.lastIndex = read;
43225
- let attribute = ATTRIBUTE.exec(body);
43226
- while (attribute) {
43227
- attributes[attribute[1] ?? ""] = decoded(attribute[2] ?? attribute[3] ?? "");
43228
- read = ATTRIBUTE.lastIndex;
43229
- attribute = ATTRIBUTE.exec(body);
43230
- }
43231
- if (body.slice(read).trim() !== "") {
43232
- throw new MalformedReportError(`Unreadable attributes in <${tag}>`);
43233
- }
43234
- const element = { name: nameMatch[1], attributes, children: [], text: "" };
43235
- current().children.push(element);
43236
- if (!selfClosing) {
43237
- open.push(element);
43238
- }
43239
- }
43240
- if (open.length !== 1) {
43241
- throw new MalformedReportError(`<${current().name}> is never closed`);
43242
- }
43243
- return root;
43244
- }
43245
- function count(attributes, name) {
43246
- const value = Number(attributes[name] ?? "0");
43247
- return Number.isFinite(value) ? value : 0;
43248
- }
43249
- function ignoredYetRan(report, ignored) {
43250
- const found = [];
43251
- for (const entry of ignored) {
43252
- const bare = entry.replace("res://", "");
43253
- const at = bare.lastIndexOf(":");
43254
- const suiteNamed = at === -1 ? null : bare.slice(0, at);
43255
- const testNamed = at === -1 ? bare : bare.slice(at + 1);
43256
- for (const suite of report.suites) {
43257
- const suiteMatches = suiteNamed === null || suite.name === suiteNamed || (suite.path ?? "").includes(suiteNamed);
43258
- const ran = suiteMatches ? suite.cases.find((one) => one.name === testNamed) : undefined;
43259
- if (ran !== undefined) {
43260
- found.push({ entry, ran: `${suite.name}:${ran.name}` });
43261
- }
43262
- }
43263
- }
43264
- return found;
43265
- }
43266
- function caseOf(suite, element) {
43267
- const of = (kind) => element.children.filter((child) => child.name === kind).map((child) => {
43268
- const detail = child.text.trim();
43269
- return { kind, message: child.attributes["message"] ?? null, detail: detail === "" ? null : detail };
43270
- });
43271
- const reports = [...of("failure"), ...of("error"), ...of("skipped")];
43272
- const status = reports.some((report) => report.kind === "error") ? "error" : reports.some((report) => report.kind === "failure") ? "failed" : reports.length > 0 ? "skipped" : "passed";
43273
- return {
43274
- suite: suite.attributes["name"] ?? "",
43275
- name: element.attributes["name"] ?? "",
43276
- status,
43277
- time: count(element.attributes, "time"),
43278
- message: reports[0]?.message ?? null,
43279
- detail: reports[0]?.detail ?? null,
43280
- reports
43281
- };
43282
- }
43283
- function failedAnAssertion(entry) {
43284
- return entry.reports.some((report) => report.kind === "failure");
43285
- }
43286
- function pathOf(suite) {
43287
- const directory = suite.attributes["package"];
43288
- const name = suite.attributes["name"];
43289
- return directory && name ? `res://${directory}/${name}.gd` : null;
43290
- }
43291
- function parseJUnit(xml) {
43292
- const document = parseXml(xml);
43293
- const top = document.children.find((child) => child.name === "testsuites" || child.name === "testsuite");
43294
- if (top === undefined) {
43295
- throw new MalformedReportError("No <testsuites> or <testsuite> in the report");
43296
- }
43297
- const suiteElements = top.name === "testsuite" ? [top] : top.children.filter((child) => child.name === "testsuite");
43298
- const suites = suiteElements.map((suite) => {
43299
- const cases = suite.children.filter((child) => child.name === "testcase").map((element) => caseOf(suite, element));
43300
- const failures = cases.filter(failedAnAssertion).length;
43301
- const errors = cases.filter((entry) => entry.status === "error").length;
43302
- const written = (kind) => suite.children.filter((child) => child.name === "testcase").reduce((sum, element) => sum + element.children.filter((child) => child.name === kind).length, 0);
43303
- return {
43304
- name: suite.attributes["name"] ?? "",
43305
- path: pathOf(suite),
43306
- tests: cases.length,
43307
- discovered: count(suite.attributes, "tests"),
43308
- failures,
43309
- errors,
43310
- hookFailures: Math.max(0, count(suite.attributes, "failures") - written("failure")),
43311
- hookErrors: Math.max(0, count(suite.attributes, "errors") - written("error")),
43312
- skipped: cases.filter((entry) => entry.status === "skipped").length,
43313
- time: count(suite.attributes, "time"),
43314
- cases
43315
- };
43316
- });
43317
- const all = suites.flatMap((suite) => suite.cases);
43318
- return {
43319
- tests: all.length,
43320
- failures: all.filter(failedAnAssertion).length,
43321
- errors: all.filter((entry) => entry.status === "error").length,
43322
- hookFailures: suites.reduce((sum, suite) => sum + suite.hookFailures, 0),
43323
- hookErrors: suites.reduce((sum, suite) => sum + suite.hookErrors, 0),
43324
- skipped: all.filter((entry) => entry.status === "skipped").length,
43325
- time: suites.reduce((sum, suite) => sum + suite.time, 0),
43326
- suites
43327
- };
43328
- }
43329
- function hookFailuresPrinted(printed) {
43330
- return printed.flatMap((line) => {
43331
- const found = HOOK_FAILURE.exec(line);
43332
- return found === null ? [] : [{ hook: found[2] ?? "", path: found[3] ?? "", line: Number(found[4]), message: found[1] ?? "" }];
43333
- });
43334
- }
43335
- function whyNoReport(printed, asked) {
43336
- const missing = printed.map((line) => NO_SUCH_PATH.exec(line)).find((found) => found !== null);
43337
- if (missing !== undefined) {
43338
- return `nothing at ${missing[1] ?? asked}`;
43339
- }
43340
- return printed.some((line) => NOTHING_RAN.test(line)) ? `no test cases found at ${asked}` : null;
43341
- }
43342
- function scriptErrorsPrinted(printed) {
43343
- return discoveryListing(printed)?.errors ?? [];
43344
- }
43345
- function discoveryListing(printed) {
43346
- const start = printed.findIndex((line) => line.includes(DISCOVERY_FAILED));
43347
- if (start < 0) {
43348
- return null;
43349
- }
43350
- const found = [];
43351
- let message = null;
43352
- let to = start + 1;
43353
- for (const line of printed.slice(start + 1)) {
43354
- const at = /^\s*at (res:\/\/.+):(\d+)\s*$/.exec(line);
43355
- if (at !== null && message !== null) {
43356
- found.push({ path: at[1] ?? "", line: Number(at[2]) === 0 ? null : Number(at[2]), message });
43357
- message = null;
43358
- } else if (at === null && message === null && /^ {2}\S/.test(line)) {
43359
- message = line.trim();
43360
- } else {
43361
- break;
43362
- }
43363
- to += 1;
43364
- }
43365
- const errors = found.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : (a.line ?? 0) - (b.line ?? 0));
43366
- return { errors, from: start, to };
43367
- }
43368
- function unspelled(text) {
43369
- return text.replace(/<(?:LF|CR|TAB|BS|FF|VT|BEL|ESC|DEL|0x[0-9A-F]{2})>/g, (spelling) => spelling.startsWith("<0x") ? String.fromCharCode(Number.parseInt(spelling.slice(3, 5), 16)) : SPELLED[spelling] ?? spelling);
43370
- }
43371
- function readDiff(printed) {
43372
- let merged = "";
43373
- let actual = "";
43374
- let background = null;
43375
- let last = 0;
43376
- const take = (text) => {
43377
- merged += text;
43378
- if (background === null) {
43379
- actual += text;
43380
- } else if (background === "added") {
43381
- actual += unspelled(text);
43382
- }
43383
- };
43384
- for (const match of printed.matchAll(COLOUR2)) {
43385
- take(printed.slice(last, match.index));
43386
- last = match.index + match[0].length;
43387
- const codes = (match[1] ?? "").split(";");
43388
- if (codes[0] === "48" && codes[1] === "2") {
43389
- background = Number(codes[2]) > Number(codes[3]) ? "missing" : "added";
43390
- } else if (codes[0] === "0" || codes[0] === "") {
43391
- background = null;
43392
- }
43393
- }
43394
- take(printed.slice(last));
43395
- return { merged, actual };
43396
- }
43397
- function withActualsPrinted(failed, printed) {
43398
- const diffs = [...printed.replaceAll(`\r
43399
- `, `
43400
- `).matchAll(PRINTED_ACTUAL)].map((match) => ({
43401
- suffix: match[3] ?? "",
43402
- at: match[5] ?? "",
43403
- ...readDiff(match[1] ?? "")
43404
- }));
43405
- const used = new Set;
43406
- return failed.map((entry) => {
43407
- if (entry.detail === null) {
43408
- return entry;
43507
+ this.identify(pid);
43508
+ return;
43509
+ }
43510
+ this.emitBridgeEvent("godot_connected", { projectPath: message.project_path });
43511
+ }
43512
+ return;
43513
+ case "pong":
43514
+ if (this.connectionInfo) {
43515
+ this.connectionInfo.lastPongAt = new Date;
43516
+ }
43517
+ return;
43518
+ }
43409
43519
  }
43410
- let detail = entry.detail;
43411
- let recovered = false;
43412
- for (const [index, diff] of diffs.entries()) {
43413
- if (used.has(index)) {
43414
- continue;
43520
+ invokeToolDirect(toolName, args, counted, resourceKey) {
43521
+ if (!this.isConnected()) {
43522
+ return Promise.reject(new Error("Godot is not connected"));
43415
43523
  }
43416
- const wrong = ` but was
43417
- '${diff.merged.replaceAll("\r", "")}'${diff.suffix}
43418
- at ${diff.at}`;
43419
- if (detail.includes(wrong)) {
43420
- detail = detail.replace(wrong, () => ` but was
43421
- '${diff.actual}'${diff.suffix}
43422
- at ${diff.at}`);
43423
- recovered = true;
43424
- used.add(index);
43524
+ const requestId = randomUUID();
43525
+ const message = {
43526
+ type: "tool_invoke",
43527
+ id: requestId,
43528
+ tool: toolName,
43529
+ args
43530
+ };
43531
+ return new Promise((resolve, reject) => {
43532
+ const timeout = setTimeout(() => {
43533
+ this.pendingRequests.delete(requestId);
43534
+ reject(new BridgeTimeout(toolName, this.timeoutMs));
43535
+ }, this.timeoutMs);
43536
+ this.pendingRequests.set(requestId, {
43537
+ toolName,
43538
+ timeout,
43539
+ resolve,
43540
+ reject,
43541
+ startedAt: Date.now(),
43542
+ resourceKey,
43543
+ counted
43544
+ });
43545
+ this.emitBridgeEvent("tool_start", {
43546
+ tool: toolName,
43547
+ id: requestId,
43548
+ args
43549
+ });
43550
+ try {
43551
+ this.sendMessage(message);
43552
+ } catch (error) {
43553
+ clearTimeout(timeout);
43554
+ this.pendingRequests.delete(requestId);
43555
+ reject(toError(error));
43556
+ }
43557
+ });
43558
+ }
43559
+ sendMessage(message) {
43560
+ if (this.socket?.readyState !== import_websocket.default.OPEN) {
43561
+ throw new Refusal("Godot is not connected");
43425
43562
  }
43563
+ this.socket.send(JSON.stringify(message));
43426
43564
  }
43427
- const quoted = QUOTED_EQUALITY.exec(detail);
43428
- if (quoted !== null) {
43429
- const alike = quoted[1] === quoted[2];
43430
- const note = recovered ? alike ? RENDERED_AWAY_NOTE : null : alike ? BOTH_SIDES_ALIKE_NOTE : VALUE_NOT_RECOVERED_NOTE;
43431
- detail = note === null ? detail : `${detail}
43432
- ${note}`;
43565
+ startKeepalive() {
43566
+ this.stopKeepalive();
43567
+ this.pingInterval = setInterval(() => {
43568
+ if (!this.isConnected()) {
43569
+ return;
43570
+ }
43571
+ try {
43572
+ const ping = { type: "ping" };
43573
+ this.sendMessage(ping);
43574
+ } catch (error) {
43575
+ this.log("warn", `Failed to send ping: ${errorMessage(error)}`);
43576
+ }
43577
+ }, KEEPALIVE_INTERVAL_MS);
43433
43578
  }
43434
- if (quoted !== null && masked(quoted[1] ?? "", quoted[2] ?? "")) {
43435
- const end = quoted[0].length - `
43436
- at `.length;
43437
- detail = detail.slice(0, end).replaceAll(MASKED_BRACKET, "[") + detail.slice(end);
43579
+ stopKeepalive() {
43580
+ if (!this.pingInterval) {
43581
+ return;
43582
+ }
43583
+ clearInterval(this.pingInterval);
43584
+ this.pingInterval = null;
43438
43585
  }
43439
- return detail === entry.detail ? entry : { ...entry, detail };
43440
- });
43441
- }
43442
- function shortened(detail) {
43443
- if (detail === null || detail.length <= LONGEST_DETAIL) {
43444
- return detail;
43445
- }
43446
- return `${detail.slice(0, LONGEST_DETAIL)}
43447
- [${detail.length - LONGEST_DETAIL} more characters left out]`;
43448
- }
43449
- function foldedReports(reports) {
43450
- const first = new Map;
43451
- for (const report of reports) {
43452
- const key = JSON.stringify([report.kind, report.message, report.detail]);
43453
- const seen = first.get(key);
43454
- if (seen === undefined) {
43455
- first.set(key, { report, times: 1 });
43456
- } else {
43457
- seen.times += 1;
43586
+ belongsElsewhere(theirProject) {
43587
+ if (this.ownProject === null || theirProject.trim() === "") {
43588
+ return false;
43589
+ }
43590
+ return !isSameDirectory(this.ownProject, theirProject);
43458
43591
  }
43459
- }
43460
- return [...first.values()].map(({ report, times }) => {
43461
- const kept = { ...report, detail: shortened(report.detail) };
43462
- return times === 1 ? kept : { ...kept, times };
43463
- });
43464
- }
43465
- function failedCases(report, printed) {
43466
- const cases = report.suites.flatMap((suite) => suite.cases.filter((entry) => entry.status === "failed" || entry.status === "error").map((entry) => ({ entry, path: suite.path })));
43467
- const read = withActualsPrinted(cases.flatMap(({ entry }) => entry.reports), printed);
43468
- let next = 0;
43469
- return cases.map(({ entry, path }) => {
43470
- const reports = read.slice(next, next + entry.reports.length);
43471
- next += entry.reports.length;
43472
- const folded = foldedReports(reports);
43473
- const shown = folded.slice(0, MOST_REPORTS_SHOWN);
43474
- return {
43475
- suite: entry.suite,
43476
- name: entry.name,
43477
- status: entry.status,
43478
- time: entry.time,
43479
- path,
43480
- message: reports[0]?.message ?? null,
43481
- detail: shortened(reports[0]?.detail ?? null),
43482
- ...reports.length > 1 ? { reports: shown, reportCount: reports.length } : {},
43483
- ...folded.length > shown.length ? { reportsLeftOut: folded.length - shown.length } : {}
43484
- };
43485
- });
43486
- }
43487
- function failedWithin(failed, budget) {
43488
- const sizes = failed.map((entry) => JSON.stringify(entry).length);
43489
- const smallestFirst = sizes.map((_, index) => index).sort((a, b) => (sizes[a] ?? 0) - (sizes[b] ?? 0));
43490
- const whole = new Set;
43491
- let spent = 0;
43492
- for (const index of smallestFirst) {
43493
- const size = sizes[index] ?? 0;
43494
- if (spent + size > budget) {
43495
- break;
43592
+ sendElsewhere(theirProject) {
43593
+ const said = `this server serves ${this.ownProject ?? ""}, and that editor has ${theirProject} open`;
43594
+ this.log("warn", `Turning away an editor from another project: ${said}`);
43595
+ const stranger = this.socket;
43596
+ this.socket = null;
43597
+ this.connectionInfo = null;
43598
+ this.stopKeepalive();
43599
+ const fits = closeReason(said, "start") === said;
43600
+ stranger?.close(OTHER_PROJECT_CLOSE_CODE, fits ? said : closeReason(`this server serves ${this.ownProject ?? ""}`, "end"));
43496
43601
  }
43497
- spent += size;
43498
- whole.add(index);
43499
- }
43500
- const kept = failed.map((entry, index) => {
43501
- if (whole.has(index)) {
43502
- return entry;
43602
+ elsewhereUnlessOneShot(theirProject, pid, openedByAServer) {
43603
+ const known = pid === undefined ? undefined : this.oneShots.get(pid);
43604
+ if (pid !== undefined && known !== undefined) {
43605
+ this.turnAway(pid, known);
43606
+ return;
43607
+ }
43608
+ const info = this.connectionInfo;
43609
+ const socket = this.socket;
43610
+ const reader = this.oneShotReader;
43611
+ if (pid === undefined || openedByAServer || info === null || reader === null) {
43612
+ this.sendElsewhere(theirProject);
43613
+ return;
43614
+ }
43615
+ info.identifying = true;
43616
+ reader(pid).catch(() => {
43617
+ return;
43618
+ }).then((option) => {
43619
+ if (this.socket !== socket || this.connectionInfo !== info) {
43620
+ return;
43621
+ }
43622
+ if (typeof option === "string") {
43623
+ this.oneShots.set(pid, option);
43624
+ this.turnAway(pid, option);
43625
+ return;
43626
+ }
43627
+ this.sendElsewhere(theirProject);
43628
+ });
43503
43629
  }
43504
- const { detail: _detail, reports: _reports, reportsLeftOut: _left, ...named } = entry;
43505
- return { ...named, message: entry.message?.slice(0, 500) ?? null, detail: null };
43506
- });
43507
- return { failed: kept, briefed: failed.length - whole.size };
43508
- }
43509
- function masked(expected, actual) {
43510
- const allSpelled = (value) => !PRINTED_DICTIONARY.test(value) && value.split("[").length === value.split(MASKED_BRACKET).length;
43511
- return `${expected}${actual}`.includes(MASKED_BRACKET) && allSpelled(expected) && allSpelled(actual);
43512
- }
43513
- function orphansPrinted(printed) {
43514
- const suites = [];
43515
- let total = null;
43516
- let running = null;
43517
- for (const line of printed) {
43518
- const started = RUNNING_SUITE.exec(line);
43519
- if (started?.[1] !== undefined) {
43520
- running = started[1];
43521
- continue;
43630
+ identify(pid) {
43631
+ const known = this.oneShots.get(pid);
43632
+ if (known !== undefined) {
43633
+ this.turnAway(pid, known);
43634
+ return;
43635
+ }
43636
+ const info = this.connectionInfo;
43637
+ const socket = this.socket;
43638
+ const reader = this.oneShotReader;
43639
+ if (info === null || reader === null) {
43640
+ return;
43641
+ }
43642
+ info.identifying = true;
43643
+ reader(pid).catch(() => {
43644
+ return;
43645
+ }).then((option) => {
43646
+ if (this.socket !== socket || this.connectionInfo !== info) {
43647
+ return;
43648
+ }
43649
+ if (typeof option === "string") {
43650
+ this.oneShots.set(pid, option);
43651
+ this.turnAway(pid, option);
43652
+ return;
43653
+ }
43654
+ info.identifying = false;
43655
+ this.log("info", `Godot editor identified: pid ${pid}`);
43656
+ this.emitBridgeEvent("godot_connected", { projectPath: info.projectPath });
43657
+ });
43522
43658
  }
43523
- const counted = ORPHANS.exec(line);
43524
- if (counted?.[1] === undefined) {
43525
- continue;
43659
+ turnAway(pid, option) {
43660
+ const said = `pid ${pid} was started with ${option}, which does one job and quits, so it is not an editor`;
43661
+ this.log("info", `Turning away an engine run: ${said}`);
43662
+ this.turnedAway = { pid, option, at: new Date };
43663
+ const run = this.socket;
43664
+ this.socket = null;
43665
+ this.connectionInfo = null;
43666
+ this.stopKeepalive();
43667
+ run?.close(ONE_SHOT_CLOSE_CODE, closeReason(said, "start"));
43526
43668
  }
43527
- const orphans = Number(counted[1]);
43528
- if (line.includes(OVERALL)) {
43529
- total = orphans;
43530
- running = null;
43531
- continue;
43669
+ handleDisconnect(disconnectedSocket, reason) {
43670
+ if (disconnectedSocket && this.socket && disconnectedSocket !== this.socket) {
43671
+ this.log("debug", "Ignoring stale Godot socket disconnect event");
43672
+ return;
43673
+ }
43674
+ this.stopKeepalive();
43675
+ this.socket = null;
43676
+ this.connectionInfo = null;
43677
+ this.emitBridgeEvent("godot_disconnected", {});
43678
+ this.rejectAllPending(reason);
43679
+ this.resourceQueues.clear();
43532
43680
  }
43533
- if (running !== null && orphans > 0) {
43534
- suites.push({ path: running, orphans });
43681
+ emitBridgeEvent(eventName, payload) {
43682
+ this.emit(eventName, payload);
43535
43683
  }
43536
- running = null;
43537
- }
43538
- return {
43539
- total: total ?? suites.reduce((sum, suite) => sum + suite.orphans, 0),
43540
- suites
43541
- };
43542
- }
43543
- function finishesASuite(line) {
43544
- return SUITE_COUNTS.test(line) && !line.includes(OVERALL);
43545
- }
43546
- function suitesPrinted(printed) {
43547
- const finished = [];
43548
- let running = null;
43549
- let failed = [];
43550
- for (const line of printed) {
43551
- const started = RUNNING_SUITE.exec(line);
43552
- if (started?.[1] !== undefined) {
43553
- running = started[1];
43554
- failed = [];
43555
- continue;
43684
+ rejectAllPending(error) {
43685
+ for (const pending of this.pendingRequests.values()) {
43686
+ clearTimeout(pending.timeout);
43687
+ pending.reject(error);
43688
+ }
43689
+ this.pendingRequests.clear();
43556
43690
  }
43557
- if (running === null) {
43558
- continue;
43691
+ enqueueResourceRequest(resourceKey, task) {
43692
+ const previous = this.resourceQueues.get(resourceKey) ?? Promise.resolve();
43693
+ const taskPromise = previous.catch(() => {
43694
+ return;
43695
+ }).then(task);
43696
+ const tail = taskPromise.then(() => {
43697
+ return;
43698
+ }, () => {
43699
+ return;
43700
+ });
43701
+ this.resourceQueues.set(resourceKey, tail);
43702
+ return taskPromise.finally(() => {
43703
+ if (this.resourceQueues.get(resourceKey) === tail) {
43704
+ this.resourceQueues.delete(resourceKey);
43705
+ }
43706
+ });
43559
43707
  }
43560
- const failing = CASE_FAILED.exec(line)?.[1];
43561
- if (failing !== undefined) {
43562
- failed.push(failing);
43563
- continue;
43708
+ getResourceKey(args) {
43709
+ const scenePath = this.getStringArg(args, "scenePath") ?? this.getStringArg(args, "scene_path");
43710
+ if (scenePath) {
43711
+ return `scene:${scenePath}`;
43712
+ }
43713
+ const resourcePath = this.getStringArg(args, "resourcePath") ?? this.getStringArg(args, "resource_path");
43714
+ if (resourcePath) {
43715
+ return `resource:${resourcePath}`;
43716
+ }
43717
+ return;
43564
43718
  }
43565
- const counted = finishesASuite(line) ? SUITE_COUNTS.exec(line) : null;
43566
- if (counted !== null) {
43567
- finished.push({
43568
- path: running,
43569
- cases: Number(counted[1]),
43570
- errors: Number(counted[2]),
43571
- failures: Number(counted[3]),
43572
- failed
43573
- });
43574
- running = null;
43719
+ getStringArg(args, key) {
43720
+ const value = args[key];
43721
+ return typeof value === "string" && value.length > 0 ? value : undefined;
43575
43722
  }
43576
- }
43577
- return { finished, running };
43578
- }
43579
- function testProgressMessage(printed, elapsedMs) {
43580
- const { finished, running } = suitesPrinted(printed);
43581
- const failing = finished.filter((suite) => suite.errors + suite.failures > 0).length;
43582
- const seconds = Math.floor(elapsedMs / 1000);
43583
- const took = seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
43584
- const done = finished.length === 0 ? "No suite has finished" : `${finished.length === 1 ? "One suite has" : `${finished.length} suites have`} finished${failing === 0 ? "" : `, ${failing} with failures`}`;
43585
- return `${done}${running === null ? "" : `; running ${running}`}; ${took} in.`;
43586
- }
43587
- var MalformedReportError, ENTITIES, ATTRIBUTE, HOOK_FAILURE, NOTHING_RAN, NO_SUCH_PATH, DISCOVERY_FAILED = "Script errors were detected during test discovery!", ESCAPE, LOCATION = `'[^'\\n]*' in \\S+:\\d+`, IGNORING_CASE = " \\(ignoring case\\)", PRINTED_ACTUAL, QUOTED_EQUALITY, COLOUR2, SPELLED, BOTH_SIDES_ALIKE_NOTE = "[gdharness: the expected and the actual printed alike because gdUnit4 drops the marks that tell them apart when it writes its report, and the runner printed no copy of this one, so neither side above is the value the assertion found.]", VALUE_NOT_RECOVERED_NOTE = '[gdharness: the value after "but was" may not be the one the assertion found. gdUnit4 writes a failing string into its report as a character diff against the expected one with the marks dropped, which merges the two, and the runner printed no copy of this one to read the value back from.]', RENDERED_AWAY_NOTE = '[gdharness: the two sides print alike, so what tells them apart is something gdUnit4 does not print. The usual cause is BBCode in the expected string, which gdUnit4 renders as formatting instead of showing; the value after "but was" is the one the assertion found.]', MOST_REPORTS_SHOWN = 20, LONGEST_DETAIL = 20000, MASKED_BRACKET = "[lb]", PRINTED_DICTIONARY, RUNNING_SUITE, ORPHANS, OVERALL = "Overall Summary", SUITE_COUNTS, CASE_FAILED;
43588
- var init_junit = __esm(() => {
43589
- MalformedReportError = class MalformedReportError extends Error {
43590
- constructor(message) {
43591
- super(message);
43592
- this.name = "MalformedReportError";
43723
+ isIncomingMessage(value) {
43724
+ if (!value || typeof value !== "object") {
43725
+ return false;
43726
+ }
43727
+ const message = value;
43728
+ const type = message["type"];
43729
+ if (type !== "tool_result" && type !== "pong" && type !== "godot_ready") {
43730
+ return false;
43731
+ }
43732
+ if (type === "pong") {
43733
+ return true;
43734
+ }
43735
+ if (type === "godot_ready") {
43736
+ return typeof message["project_path"] === "string";
43737
+ }
43738
+ return typeof message["id"] === "string" && typeof message["success"] === "boolean" && (message["error"] === undefined || typeof message["error"] === "string") && (message["busy"] === undefined || typeof message["busy"] === "boolean");
43739
+ }
43740
+ log(level, message) {
43741
+ console.error(`[${new Date().toISOString()}] [GodotBridge:${level.toUpperCase()}] ${message}`);
43593
43742
  }
43594
43743
  };
43595
- ENTITIES = { lt: "<", gt: ">", amp: "&", quot: '"', apos: "'" };
43596
- ATTRIBUTE = /\s*([^\s=/>]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/y;
43597
- HOOK_FAILURE = /^\s*(.*?)\s*\tat '(before|after)' in (res:\/\/.+?):(\d+)\s*$/;
43598
- NOTHING_RAN = /no test cases found/i;
43599
- NO_SUCH_PATH = /given directory or file does not exists:\s*(.+?)\s*$/i;
43600
- ESCAPE = String.fromCharCode(27);
43601
- PRINTED_ACTUAL = new RegExp(` but was\\n '((?:(?!\\tat ${LOCATION})[\\s\\S])*?)'((?:${ESCAPE}\\[[0-9;]*m)*)(${IGNORING_CASE})?((?:${ESCAPE}\\[[0-9;]*m)*)\\tat (${LOCATION})`, "g");
43602
- QUOTED_EQUALITY = new RegExp(`^Expecting(?: error message)?:\\n '([\\s\\S]*)'\\n but was\\n '([\\s\\S]*)'(?:${IGNORING_CASE})?\\n\\tat `);
43603
- COLOUR2 = new RegExp(`${ESCAPE}\\[([0-9;]*)m`, "g");
43604
- SPELLED = {
43605
- "<LF>": `
43606
- `,
43607
- "<CR>": "\r",
43608
- "<TAB>": "\t",
43609
- "<BS>": "\b",
43610
- "<FF>": "\f",
43611
- "<VT>": "\v",
43612
- "<BEL>": "\x07",
43613
- "<ESC>": ESCAPE,
43614
- "<DEL>": ""
43615
- };
43616
- PRINTED_DICTIONARY = /^\{\n\t[\s\S]*\n {2}\}$/;
43617
- RUNNING_SUITE = /Run Test Suite:\s*(res:\/\/.+?\.(?:gd|cs))(?=\s|$)/;
43618
- ORPHANS = /(\d+)\s+orphans/;
43619
- SUITE_COUNTS = /(\d+)\s+test cases\s*\|\s*(\d+)\s+errors\s*\|\s*(\d+)\s+failures/;
43620
- CASE_FAILED = /^\s*res:\/\/.+?\.(?:gd|cs) > (.+?) (?:FAILED|ERROR)(?:\s|$)/;
43621
43744
  });
43622
43745
 
43623
43746
  // src/keeper-channel.ts
@@ -43687,10 +43810,10 @@ var init_keeper_channel = () => {};
43687
43810
 
43688
43811
  // src/lifecycle-log.ts
43689
43812
  import { appendFileSync, mkdirSync as mkdirSync14, renameSync as renameSync5, statSync as statSync12 } from "node:fs";
43690
- import { dirname as dirname20, join as join28 } from "node:path";
43813
+ import { dirname as dirname20, join as join29 } from "node:path";
43691
43814
  import process11 from "node:process";
43692
43815
  function lifecycleLogPath() {
43693
- return join28(runtimeDirectory(), "servers.log");
43816
+ return join29(runtimeDirectory(), "servers.log");
43694
43817
  }
43695
43818
  function writeLifecycle(event, fields = {}) {
43696
43819
  const path = lifecycleLogPath();
@@ -44011,9 +44134,9 @@ var init_prompts = __esm(() => {
44011
44134
 
44012
44135
  // src/restart-note.ts
44013
44136
  import { mkdirSync as mkdirSync15, readFileSync as readFileSync24, rmSync as rmSync13, writeFileSync as writeFileSync17 } from "node:fs";
44014
- import { dirname as dirname21, join as join29 } from "node:path";
44137
+ import { dirname as dirname21, join as join30 } from "node:path";
44015
44138
  function restartNotePath(projectPath) {
44016
- return join29(projectPath, ".godot", "gdharness-restart.json");
44139
+ return join30(projectPath, ".godot", "gdharness-restart.json");
44017
44140
  }
44018
44141
  function noteRestartBegun(note) {
44019
44142
  const path = restartNotePath(note.projectPath);
@@ -44054,16 +44177,16 @@ var init_restart_note = __esm(() => {
44054
44177
  // src/run-record.ts
44055
44178
  import { createHash as createHash5 } from "node:crypto";
44056
44179
  import { mkdirSync as mkdirSync16, openSync as openSync3, readdirSync as readdirSync13, readFileSync as readFileSync25, realpathSync as realpathSync6, rmSync as rmSync14, statSync as statSync13 } from "node:fs";
44057
- import { basename as basename9, dirname as dirname22, join as join30, resolve as resolve7 } from "node:path";
44180
+ import { basename as basename9, dirname as dirname22, join as join31, resolve as resolve7 } from "node:path";
44058
44181
  function runsDirectory() {
44059
- return join30(runtimeDirectory(), "runs");
44182
+ return join31(runtimeDirectory(), "runs");
44060
44183
  }
44061
44184
  function realAsFarAsItExists(path) {
44062
44185
  const missing = [];
44063
44186
  let at = path;
44064
44187
  for (;; ) {
44065
44188
  try {
44066
- return join30(realpathSync6.native(at), ...missing);
44189
+ return join31(realpathSync6.native(at), ...missing);
44067
44190
  } catch {
44068
44191
  const parent = dirname22(at);
44069
44192
  if (parent === at) {
@@ -44080,10 +44203,10 @@ function projectKey(projectPath) {
44080
44203
  return createHash5("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
44081
44204
  }
44082
44205
  function runRecordPath(projectPath, directory = runtimeDirectory()) {
44083
- return join30(directory, "runs", `note-${projectKey(projectPath)}.json`);
44206
+ return join31(directory, "runs", `note-${projectKey(projectPath)}.json`);
44084
44207
  }
44085
44208
  function latestRunPath(servedBy, directory = runtimeDirectory()) {
44086
- return join30(directory, "runs", `latest-${projectKey(servedBy)}.json`);
44209
+ return join31(directory, "runs", `latest-${projectKey(servedBy)}.json`);
44087
44210
  }
44088
44211
  function writeLatestRun(servedBy, latest) {
44089
44212
  try {
@@ -44108,20 +44231,20 @@ function readLatestRun(servedBy, directory = runtimeDirectory()) {
44108
44231
  }
44109
44232
  function openTranscript(startedAt) {
44110
44233
  mkdirSync16(runsDirectory(), { recursive: true });
44111
- const path = join30(runsDirectory(), `run-${startedAt}.log`);
44234
+ const path = join31(runsDirectory(), `run-${startedAt}.log`);
44112
44235
  return { path, fd: openSync3(path, "a") };
44113
44236
  }
44114
44237
  function profilePath(startedAt) {
44115
- return join30(runsDirectory(), `run-${startedAt}.profile.json`);
44238
+ return join31(runsDirectory(), `run-${startedAt}.profile.json`);
44116
44239
  }
44117
44240
  function profileMarkPath(startedAt) {
44118
- return join30(runsDirectory(), `run-${startedAt}.profile-mark.json`);
44241
+ return join31(runsDirectory(), `run-${startedAt}.profile-mark.json`);
44119
44242
  }
44120
44243
  function noteIsAbout(note, playing) {
44121
44244
  return note.play !== undefined && playing !== null && note.play.play === playing.play && note.play.editorPid === playing.editorPid;
44122
44245
  }
44123
44246
  function editorNotePath(projectPath, directory = runtimeDirectory()) {
44124
- return join30(directory, "runs", `editor-${projectKey(projectPath)}.json`);
44247
+ return join31(directory, "runs", `editor-${projectKey(projectPath)}.json`);
44125
44248
  }
44126
44249
  function writeEditorRunNote(note) {
44127
44250
  mkdirSync16(runsDirectory(), { recursive: true });
@@ -44132,7 +44255,7 @@ function writeEditorRunNote(note) {
44132
44255
  }
44133
44256
  function readEditorRunNote(projectPath) {
44134
44257
  for (const directory of runtimeDirectories()) {
44135
- const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join30(directory, "runs", "editor-run.json"));
44258
+ const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join31(directory, "runs", "editor-run.json"));
44136
44259
  if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
44137
44260
  return note;
44138
44261
  }
@@ -44166,7 +44289,7 @@ function readRunRecord(projectPath) {
44166
44289
  if (own !== null) {
44167
44290
  return own;
44168
44291
  }
44169
- const shared = recordAt(join30(directory, "runs", SHARED_NOTE));
44292
+ const shared = recordAt(join31(directory, "runs", SHARED_NOTE));
44170
44293
  if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
44171
44294
  return shared;
44172
44295
  }
@@ -44178,13 +44301,13 @@ function everyRunRecord() {
44178
44301
  for (const directory of runtimeDirectories()) {
44179
44302
  let entries;
44180
44303
  try {
44181
- entries = readdirSync13(join30(directory, "runs"));
44304
+ entries = readdirSync13(join31(directory, "runs"));
44182
44305
  } catch {
44183
44306
  continue;
44184
44307
  }
44185
44308
  for (const entry of entries) {
44186
44309
  if (entry === SHARED_NOTE || entry.startsWith("note-") && entry.endsWith(".json")) {
44187
- const record = recordAt(join30(directory, "runs", entry));
44310
+ const record = recordAt(join31(directory, "runs", entry));
44188
44311
  if (record !== null) {
44189
44312
  found.push(record);
44190
44313
  }
@@ -44392,7 +44515,7 @@ function judgeAnnouncedGame(running, projectPath, announcedAt) {
44392
44515
  }
44393
44516
  function clearRunRecord(projectPath) {
44394
44517
  for (const directory of runtimeDirectories()) {
44395
- const shared = join30(directory, "runs", SHARED_NOTE);
44518
+ const shared = join31(directory, "runs", SHARED_NOTE);
44396
44519
  const sharedRecord = recordAt(shared);
44397
44520
  const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
44398
44521
  for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
@@ -44427,7 +44550,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
44427
44550
  if (!transcript && !entry.endsWith(".partial")) {
44428
44551
  continue;
44429
44552
  }
44430
- const path = join30(directory, entry);
44553
+ const path = join31(directory, entry);
44431
44554
  if (kept.has(path) || ageMs(path, now) < keepMs) {
44432
44555
  continue;
44433
44556
  }
@@ -44447,110 +44570,6 @@ var init_run_record = __esm(() => {
44447
44570
  init_windows_ask();
44448
44571
  });
44449
44572
 
44450
- // src/script-origins.ts
44451
- import { join as join31 } from "node:path";
44452
- function unresolvedClassIn(message) {
44453
- return UNRESOLVED.exec(message)?.[1] ?? null;
44454
- }
44455
- async function originsOf(errors, declared, check) {
44456
- const listed = new Set(errors.map((error) => error.path));
44457
- const origins = [];
44458
- const unconfirmed = [];
44459
- const asked = new Set;
44460
- const waiting = errors.map((error) => unresolvedClassIn(error.message)).filter((name) => name !== null);
44461
- while (waiting.length > 0 && asked.size < MOST_CHECKED) {
44462
- const name = waiting.shift() ?? "";
44463
- const path = declared.get(name);
44464
- if (path === undefined || listed.has(path) || asked.has(path)) {
44465
- continue;
44466
- }
44467
- asked.add(path);
44468
- const checked = await check(path);
44469
- const own = checked.errors.filter((error) => error.path === path);
44470
- if (own.length === 0) {
44471
- unconfirmed.push({
44472
- path,
44473
- declares: name,
44474
- why: checked.failure === null ? "the engine's check of it found no error" : `the engine's check of it did not finish (${checked.failure})`
44475
- });
44476
- continue;
44477
- }
44478
- for (const error of own) {
44479
- origins.push({ ...error, declares: name });
44480
- const deeper = unresolvedClassIn(error.message);
44481
- if (deeper !== null) {
44482
- waiting.push(deeper);
44483
- }
44484
- }
44485
- }
44486
- return { origins, unconfirmed };
44487
- }
44488
- function compileErrorsIn(entries) {
44489
- return entries.flatMap((entry) => {
44490
- const at = entry.severity === "error" ? reloadedAt(entry) : null;
44491
- return at === null ? [] : [{ ...at, message: entry.text }];
44492
- });
44493
- }
44494
- function reloadedAt(entry) {
44495
- for (const line of entry.detail) {
44496
- const at = RELOADED_AT.exec(line.trim());
44497
- if (at !== null) {
44498
- return { path: at[1] ?? "", line: Number(at[2]) === 0 ? null : Number(at[2]) };
44499
- }
44500
- }
44501
- return null;
44502
- }
44503
- function checkWithEngine(godotPath, projectPath) {
44504
- return async (path) => {
44505
- const logDir = scratchDirectory("tests");
44506
- try {
44507
- const ran = await runEngine(godotPath, [
44508
- "--headless",
44509
- "--log-file",
44510
- join31(logDir, "engine.log"),
44511
- "--path",
44512
- projectPath,
44513
- "--check-only",
44514
- "-s",
44515
- path
44516
- ], { timeout: { ms: 60000, said: "minute" } });
44517
- const errors = ran.log.select({ severity: "error", sinceLastCall: false, limit: 50 }).entries.flatMap((entry) => {
44518
- const at = reloadedAt(entry);
44519
- return at === null ? [] : [{ ...at, message: entry.text }];
44520
- });
44521
- return { errors, failure: ran.failure === null ? null : howItEnded(ran) };
44522
- } finally {
44523
- discard(logDir);
44524
- }
44525
- };
44526
- }
44527
- function withoutListed(entries, errors) {
44528
- if (errors.length === 0) {
44529
- return [...entries];
44530
- }
44531
- const listing = discoveryListing(entries.map((entry) => entry.text));
44532
- const paths = new Set(errors.map((error) => error.path));
44533
- return entries.filter((entry, at) => {
44534
- if (listing !== null && at >= listing.from && at < listing.to) {
44535
- return false;
44536
- }
44537
- const reloaded = reloadedAt(entry);
44538
- if (reloaded !== null && errors.some((error) => error.path === reloaded.path && error.line === reloaded.line && error.message === entry.text)) {
44539
- return false;
44540
- }
44541
- const failedToLoad = /^Failed to load script "(res:\/\/[^"]+)" with error "[^"]+"\.$/.exec(entry.text);
44542
- return failedToLoad === null || !paths.has(failedToLoad[1] ?? "");
44543
- });
44544
- }
44545
- var UNRESOLVED, MOST_CHECKED = 8, RELOADED_AT;
44546
- var init_script_origins = __esm(() => {
44547
- init_engine_run();
44548
- init_junit();
44549
- init_scratch();
44550
- UNRESOLVED = /Could not resolve class "([A-Za-z_][A-Za-z0-9_]*)", because of a parser error/;
44551
- RELOADED_AT = /GDScript::reload \((res:\/\/.+):(\d+)\)$/;
44552
- });
44553
-
44554
44573
  // src/script-profile.ts
44555
44574
  import { readFileSync as readFileSync26 } from "node:fs";
44556
44575
  function readProfileFile(path) {
@@ -45660,7 +45679,8 @@ function repeatingScriptError(entries, byScript) {
45660
45679
  if (last === undefined || !byScript(last)) {
45661
45680
  return null;
45662
45681
  }
45663
- const where = last.detail[0] ?? null;
45682
+ const whole = last.detail.length > 0 ? last : entries.findLast((entry) => entry.text === last.text && entry.detail.length > 0 && byScript(entry)) ?? last;
45683
+ const where = whole.detail[0] ?? null;
45664
45684
  const times = entries.filter((entry) => entry.text === last.text && (entry.detail[0] ?? null) === where && byScript(entry)).length;
45665
45685
  return times >= LOOPING_AFTER ? { text: last.text, where: where?.replace(/^at: /, "") ?? null, times } : null;
45666
45686
  }
@@ -46056,9 +46076,9 @@ function scriptsUnder(directory, prefix) {
46056
46076
  }
46057
46077
  return found;
46058
46078
  }
46059
- function hasMainScene(projectFile) {
46079
+ function mainSceneOf(projectFile) {
46060
46080
  const scene = parseProjectGodot(readFileSync27(projectFile, "utf8"))["application"]?.["run/main_scene"];
46061
- return typeof scene === "string" && scene !== "";
46081
+ return typeof scene === "string" && scene !== "" ? scene : null;
46062
46082
  }
46063
46083
  function runtimeAddonAnnouncesEditors(projectPath) {
46064
46084
  if (projectPath === null) {
@@ -46888,13 +46908,13 @@ class GodotServer {
46888
46908
  stateMachinePath: readString(args, "stateMachinePath") ?? ""
46889
46909
  }), async () => await bridge("read_animation_states"));
46890
46910
  case "create":
46891
- return await bridge("create_animation");
46911
+ return await editorOrEngine("create_animation", "writes");
46892
46912
  case "add_track":
46893
- return await bridge("add_animation_track");
46913
+ return await editorOrEngine("add_animation_track", "writes");
46894
46914
  case "add_state":
46895
- return await bridge("add_animation_state");
46915
+ return await editorOrEngine("add_animation_state", "writes");
46896
46916
  default:
46897
- return await bridge("connect_animation_states");
46917
+ return await editorOrEngine("connect_animation_states", "writes");
46898
46918
  }
46899
46919
  case "script_edit":
46900
46920
  return await this.handleRenameSymbol(args);
@@ -48038,6 +48058,7 @@ class GodotServer {
48038
48058
  if (!engine.ok) {
48039
48059
  return engine.response;
48040
48060
  }
48061
+ const began = Date.now();
48041
48062
  const classes = await this.rebuildClassCache(project.value.path);
48042
48063
  if (!classes.ok) {
48043
48064
  return this.answer(classes);
@@ -48111,6 +48132,7 @@ class GodotServer {
48111
48132
  await untilGone([run.process.pid]);
48112
48133
  }
48113
48134
  await run.settled;
48135
+ const tookMs = Date.now() - began;
48114
48136
  const notCompiled = stopped.for ?? run.log.all.map((entry) => autoloadNotCompiled(entry, autoloaded)).find((found) => found !== null) ?? null;
48115
48137
  const reportsDir = join32(project.value.path, ".godot", ...ours.split("/"));
48116
48138
  let report = null;
@@ -48200,6 +48222,7 @@ class GodotServer {
48200
48222
  passed: false,
48201
48223
  verdict,
48202
48224
  tests: 0,
48225
+ tookMs,
48203
48226
  exitCode,
48204
48227
  exitSignal: run.exitSignal ?? undefined,
48205
48228
  hung,
@@ -48257,6 +48280,7 @@ class GodotServer {
48257
48280
  errors: report.errors,
48258
48281
  skipped: report.skipped,
48259
48282
  time: report.time,
48283
+ tookMs,
48260
48284
  failed,
48261
48285
  failedWithoutDetail: briefed > 0 ? briefed : undefined,
48262
48286
  hookFailures: hookFailures.length > 0 ? hookFailures : undefined,
@@ -49363,7 +49387,7 @@ class GodotServer {
49363
49387
  if (!asked.ok) {
49364
49388
  return asked.response;
49365
49389
  }
49366
- if (exported === null && !sceneToRun && !hasMainScene(project.value.file)) {
49390
+ if (exported === null && !sceneToRun && mainSceneOf(project.value.file) === null) {
49367
49391
  return this.createErrorResponse("The project sets no main scene, so there is nothing to run.", [
49368
49392
  "Pass scene to run one scene",
49369
49393
  "Choose the main scene with project_settings set_main_scene"
@@ -49440,6 +49464,7 @@ class GodotServer {
49440
49464
  alsoRunning: stranded.length === 0 ? undefined : stranded.map((one) => one.pid),
49441
49465
  pid: started.pid,
49442
49466
  executable: exported?.ok === true ? exported.relativePath : undefined,
49467
+ scene: exported !== null ? undefined : sceneArgument === null ? mainSceneOf(project.value.file) : `res://${sceneArgument}`,
49443
49468
  arguments: cmdArgs,
49444
49469
  argsNote: engineFlagsNote(given.value),
49445
49470
  transcript: started.transcript,
@@ -53360,9 +53385,9 @@ init_setup();
53360
53385
  init_tool_definitions();
53361
53386
  init_tool_reference();
53362
53387
  import { existsSync as existsSync14, mkdirSync as mkdirSync5, readdirSync as readdirSync9, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "node:fs";
53363
- import { basename as basename6, dirname as dirname8, join as join14 } from "node:path";
53388
+ import { basename as basename6, dirname as dirname8, join as join15 } from "node:path";
53364
53389
  var SKILL_NAME = "gdharness";
53365
- var SHARED_SKILLS = join14(".agents", "skills");
53390
+ var SHARED_SKILLS = join15(".agents", "skills");
53366
53391
  function skillMarkdown(version) {
53367
53392
  return `---
53368
53393
  name: ${SKILL_NAME}
@@ -53587,27 +53612,27 @@ what they report while starting comes back under \`engine_messages_before_operat
53587
53612
  function skillFiles(version) {
53588
53613
  return new Map([
53589
53614
  ["SKILL.md", skillMarkdown(version)],
53590
- [join14("references", "tools.md"), renderToolsMarkdown()]
53615
+ [join15("references", "tools.md"), renderToolsMarkdown()]
53591
53616
  ]);
53592
53617
  }
53593
53618
  function skillDirectories(harnesses, projectPath, exists) {
53594
- const directories = [join14(projectPath, SHARED_SKILLS, SKILL_NAME)];
53619
+ const directories = [join15(projectPath, SHARED_SKILLS, SKILL_NAME)];
53595
53620
  for (const harness of harnesses) {
53596
53621
  if (harness.skills === undefined) {
53597
53622
  continue;
53598
53623
  }
53599
- const own = join14(projectPath, harness.skills.dir);
53600
- if ((!harness.skills.shared || exists(own)) && !directories.includes(join14(own, SKILL_NAME))) {
53601
- directories.push(join14(own, SKILL_NAME));
53624
+ const own = join15(projectPath, harness.skills.dir);
53625
+ if ((!harness.skills.shared || exists(own)) && !directories.includes(join15(own, SKILL_NAME))) {
53626
+ directories.push(join15(own, SKILL_NAME));
53602
53627
  }
53603
53628
  }
53604
53629
  return directories;
53605
53630
  }
53606
53631
  function everySkillDirectory(projectPath, harnesses) {
53607
- const directories = new Set([join14(projectPath, SHARED_SKILLS, SKILL_NAME)]);
53632
+ const directories = new Set([join15(projectPath, SHARED_SKILLS, SKILL_NAME)]);
53608
53633
  for (const harness of harnesses) {
53609
53634
  if (harness.skills !== undefined) {
53610
- directories.add(join14(projectPath, harness.skills.dir, SKILL_NAME));
53635
+ directories.add(join15(projectPath, harness.skills.dir, SKILL_NAME));
53611
53636
  }
53612
53637
  }
53613
53638
  return [...directories];
@@ -53624,7 +53649,7 @@ function removeSkill(directories, projectPath) {
53624
53649
  return removed;
53625
53650
  }
53626
53651
  function pruneEmpty(directory, projectPath) {
53627
- const shared = join14(projectPath, ".agents");
53652
+ const shared = join15(projectPath, ".agents");
53628
53653
  let path = directory;
53629
53654
  while (path !== projectPath && (basename6(path) === "skills" || path === shared)) {
53630
53655
  if (readdirSync9(path).length > 0) {
@@ -53640,7 +53665,7 @@ function writeSkill(directories, version) {
53640
53665
  const replaced = existsSync14(directory);
53641
53666
  rmSync6(directory, { recursive: true, force: true });
53642
53667
  for (const [name, contents] of files) {
53643
- const path = join14(directory, name);
53668
+ const path = join15(directory, name);
53644
53669
  mkdirSync5(dirname8(path), { recursive: true });
53645
53670
  writeFileSync7(path, contents, "utf8");
53646
53671
  }
@@ -53650,13 +53675,13 @@ function writeSkill(directories, version) {
53650
53675
 
53651
53676
  // src/version.ts
53652
53677
  import { readFileSync as readFileSync12 } from "node:fs";
53653
- import { dirname as dirname9, join as join15 } from "node:path";
53678
+ import { dirname as dirname9, join as join16 } from "node:path";
53654
53679
  import { fileURLToPath as fileURLToPath2 } from "node:url";
53655
53680
  function getLocalVersion() {
53656
53681
  const currentDirectory = dirname9(fileURLToPath2(import.meta.url));
53657
53682
  const packageCandidates = [
53658
- join15(currentDirectory, "..", "package.json"),
53659
- join15(currentDirectory, "..", "..", "package.json")
53683
+ join16(currentDirectory, "..", "package.json"),
53684
+ join16(currentDirectory, "..", "..", "package.json")
53660
53685
  ];
53661
53686
  for (const packagePath of packageCandidates) {
53662
53687
  try {