gdharness 1.2.39 → 1.2.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/cli.js CHANGED
@@ -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 = [
@@ -11412,7 +12027,7 @@ var init_tool_definitions = __esm(() => {
11412
12027
  additionalProperties: false
11413
12028
  }
11414
12029
  };
11415
- COLOUR = {
12030
+ COLOUR2 = {
11416
12031
  type: "object",
11417
12032
  description: "set_theme_color: r, g and b from 0 to 1, with a optional and opaque by default.",
11418
12033
  properties: { r: { type: "number" }, g: { type: "number" }, b: { type: "number" }, a: { type: "number" } },
@@ -11725,7 +12340,8 @@ var init_tool_definitions = __esm(() => {
11725
12340
  name: "project_test",
11726
12341
  title: "Run tests",
11727
12342
  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.",
12343
+ 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.",
12344
+ 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
12345
  parameters: {
11730
12346
  projectPath: PROJECT_PATH,
11731
12347
  path: {
@@ -12210,7 +12826,7 @@ var init_tool_definitions = __esm(() => {
12210
12826
  ops: ["set_theme_color"],
12211
12827
  description: 'set_theme_color: such as "font_color".'
12212
12828
  },
12213
- color: { ...COLOUR, ops: ["set_theme_color"] },
12829
+ color: { ...COLOUR2, ops: ["set_theme_color"] },
12214
12830
  fontSizeName: {
12215
12831
  type: "string",
12216
12832
  ops: ["set_theme_font_size"],
@@ -12421,7 +13037,7 @@ var init_tool_definitions = __esm(() => {
12421
13037
  requires: []
12422
13038
  }
12423
13039
  },
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.",
13040
+ answer: "start: 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
13041
  defaultOperation: "start"
12426
13042
  },
12427
13043
  {
@@ -12589,7 +13205,7 @@ var init_tool_definitions = __esm(() => {
12589
13205
  property: {
12590
13206
  type: "string",
12591
13207
  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.'
13208
+ 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
13209
  },
12594
13210
  depth: {
12595
13211
  type: "integer",
@@ -12809,7 +13425,7 @@ var init_tool_definitions = __esm(() => {
12809
13425
  name: "runtime_input",
12810
13426
  title: "Send input to the game",
12811
13427
  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.',
13428
+ 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, found.item names the tab, list entry, tree row or menu bar title pressed (kind, index where its control numbers them, 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
13429
  parameters: {
12814
13430
  projectPath: RUNNING_PROJECT_PATH,
12815
13431
  pid: RUNNING_PID,
@@ -12821,7 +13437,7 @@ var init_tool_definitions = __esm(() => {
12821
13437
  says: {
12822
13438
  type: "string",
12823
13439
  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."
13440
+ 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
13441
  },
12826
13442
  action: {
12827
13443
  type: "string",
@@ -34901,12 +35517,12 @@ var init_stdio = __esm(() => {
34901
35517
 
34902
35518
  // src/autoload-failure.ts
34903
35519
  import { existsSync as existsSync15, readFileSync as readFileSync13 } from "node:fs";
34904
- import { join as join16 } from "node:path";
35520
+ import { join as join17 } from "node:path";
34905
35521
  function scriptsAutoloaded(projectPath) {
34906
35522
  const scripts = new Map;
34907
35523
  let settings;
34908
35524
  try {
34909
- settings = parseProjectGodot(readFileSync13(join16(projectPath, "project.godot"), "utf8"));
35525
+ settings = parseProjectGodot(readFileSync13(join17(projectPath, "project.godot"), "utf8"));
34910
35526
  } catch {
34911
35527
  return scripts;
34912
35528
  }
@@ -34915,7 +35531,7 @@ function scriptsAutoloaded(projectPath) {
34915
35531
  scripts.set(`res://${path}`, name);
34916
35532
  continue;
34917
35533
  }
34918
- const scene = join16(projectPath, path);
35534
+ const scene = join17(projectPath, path);
34919
35535
  if (!existsSync15(scene)) {
34920
35536
  continue;
34921
35537
  }
@@ -34928,22 +35544,21 @@ function scriptsAutoloaded(projectPath) {
34928
35544
  return scripts;
34929
35545
  }
34930
35546
  function autoloadNotCompiled(entry, autoloaded) {
34931
- const path = NOT_COMPILED.exec(entry.text)?.[1];
35547
+ const path = notCompiledIn(entry);
34932
35548
  const name = path === undefined ? undefined : autoloaded.get(path);
34933
35549
  return path === undefined || name === undefined ? null : { name, path };
34934
35550
  }
34935
- var NOT_COMPILED;
34936
35551
  var init_autoload_failure = __esm(() => {
34937
35552
  init_resources();
35553
+ init_script_origins();
34938
35554
  init_setup();
34939
- NOT_COMPILED = /^Failed to load script "(res:\/\/[^"]+)" with error "Compilation failed"/;
34940
35555
  });
34941
35556
 
34942
35557
  // src/runtime-client.ts
34943
35558
  import { existsSync as existsSync16, readdirSync as readdirSync10, readFileSync as readFileSync14, statSync as statSync8, unlinkSync as unlinkSync2 } from "node:fs";
34944
35559
  import { createConnection } from "node:net";
34945
35560
  import { tmpdir as tmpdir4 } from "node:os";
34946
- import { dirname as dirname10, join as join17, resolve as resolve4 } from "node:path";
35561
+ import { dirname as dirname10, join as join18, resolve as resolve4 } from "node:path";
34947
35562
  import { setTimeout as delay } from "node:timers/promises";
34948
35563
  function runtimeDirectory(variables = process.env) {
34949
35564
  const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
@@ -34951,19 +35566,19 @@ function runtimeDirectory(variables = process.env) {
34951
35566
  return explicit;
34952
35567
  }
34953
35568
  const perUser = envValue("XDG_RUNTIME_DIR", variables);
34954
- return join17(perUser ?? tmpdir4(), "gdharness");
35569
+ return join18(perUser ?? tmpdir4(), "gdharness");
34955
35570
  }
34956
35571
  function runtimeDirectories(variables = process.env) {
34957
35572
  const candidates = [runtimeDirectory(variables)];
34958
35573
  const fallbacks = process.platform === "win32" ? [envValue("SystemRoot", variables) ?? envValue("windir", variables) ?? "C:\\Windows"] : ["/tmp", "/var/tmp"];
34959
35574
  for (const base of fallbacks) {
34960
- candidates.push(join17(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
35575
+ candidates.push(join18(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
34961
35576
  }
34962
35577
  return [...new Set(candidates.map((path) => resolve4(path)))];
34963
35578
  }
34964
35579
  function errorReportOf(pid, directories = runtimeDirectories()) {
34965
35580
  for (const directory of directories) {
34966
- const path = join17(directory, `runtime-${pid}.log`);
35581
+ const path = join18(directory, `runtime-${pid}.log`);
34967
35582
  if (existsSync16(path)) {
34968
35583
  return path;
34969
35584
  }
@@ -35113,14 +35728,14 @@ function announcedIn(directory) {
35113
35728
  for (const entry of readdirSync10(directory)) {
35114
35729
  const report = ERROR_REPORT_PATTERN.exec(entry);
35115
35730
  if (report) {
35116
- sweepErrorReport(join17(directory, entry), Number.parseInt(report[1] ?? "", 10));
35731
+ sweepErrorReport(join18(directory, entry), Number.parseInt(report[1] ?? "", 10));
35117
35732
  continue;
35118
35733
  }
35119
35734
  const match = ANNOUNCEMENT_PATTERN.exec(entry);
35120
35735
  if (!match) {
35121
35736
  continue;
35122
35737
  }
35123
- const file = join17(directory, entry);
35738
+ const file = join18(directory, entry);
35124
35739
  const pid = Number.parseInt(match[1] ?? "", 10);
35125
35740
  const running = alive(pid);
35126
35741
  entries.push({ file, pid, running });
@@ -35148,7 +35763,7 @@ function announcedIn(directory) {
35148
35763
  found.push(announced);
35149
35764
  }
35150
35765
  const held = new Set(entries.map((one) => one.file));
35151
- const here = join17(directory, ".");
35766
+ const here = join18(directory, ".");
35152
35767
  for (const file of verdicts.keys()) {
35153
35768
  if (dirname10(file) === here && !held.has(file)) {
35154
35769
  verdicts.delete(file);
@@ -35157,7 +35772,7 @@ function announcedIn(directory) {
35157
35772
  return found;
35158
35773
  }
35159
35774
  async function announcementEnded(pid, directories = runtimeDirectories()) {
35160
- const files = directories.map((directory) => join17(directory, `runtime-${pid}.json`)).filter((file) => existsSync16(file));
35775
+ const files = directories.map((directory) => join18(directory, `runtime-${pid}.json`)).filter((file) => existsSync16(file));
35161
35776
  const first = files[0];
35162
35777
  if (first === undefined) {
35163
35778
  return;
@@ -35569,9 +36184,9 @@ var init_runtime_client = __esm(() => {
35569
36184
 
35570
36185
  // src/boot-note.ts
35571
36186
  import { mkdirSync as mkdirSync6, readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "node:fs";
35572
- import { dirname as dirname11, join as join18 } from "node:path";
36187
+ import { dirname as dirname11, join as join19 } from "node:path";
35573
36188
  function bootNotePath(projectPath) {
35574
- return join18(projectPath, ".godot", "gdharness-boot.json");
36189
+ return join19(projectPath, ".godot", "gdharness-boot.json");
35575
36190
  }
35576
36191
  function readScenes(projectPath) {
35577
36192
  try {
@@ -35618,9 +36233,9 @@ var init_boot_note = __esm(() => {
35618
36233
 
35619
36234
  // src/breakpoint-note.ts
35620
36235
  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";
36236
+ import { dirname as dirname12, isAbsolute as isAbsolute4, join as join20, relative as relative3 } from "node:path";
35622
36237
  function breakpointNotePath(projectPath) {
35623
- return join19(projectPath, ".godot", "gdharness-breakpoints.json");
36238
+ return join20(projectPath, ".godot", "gdharness-breakpoints.json");
35624
36239
  }
35625
36240
  function writeBreakpointNote(projectPath, held) {
35626
36241
  const path = breakpointNotePath(projectPath);
@@ -35661,7 +36276,7 @@ function readBreakpointNote(projectPath) {
35661
36276
  }
35662
36277
  const lines = Array.isArray(listed) ? listed.filter((line) => Number.isInteger(line) && line > 0) : [];
35663
36278
  if (lines.length > 0) {
35664
- held.push({ scriptPath: realPathOr(join19(projectPath, script)), lines });
36279
+ held.push({ scriptPath: realPathOr(join20(projectPath, script)), lines });
35665
36280
  }
35666
36281
  }
35667
36282
  return held;
@@ -35711,9 +36326,9 @@ var init_whole_file = __esm(() => {
35711
36326
 
35712
36327
  // src/class-note.ts
35713
36328
  import { mkdirSync as mkdirSync8, readFileSync as readFileSync17 } from "node:fs";
35714
- import { dirname as dirname13, join as join20 } from "node:path";
36329
+ import { dirname as dirname13, join as join21 } from "node:path";
35715
36330
  function classNotePath(projectPath) {
35716
- return join20(projectPath, ".godot", "gdharness-classes.json");
36331
+ return join21(projectPath, ".godot", "gdharness-classes.json");
35717
36332
  }
35718
36333
  function readClassNote(projectPath) {
35719
36334
  try {
@@ -35760,9 +36375,9 @@ var init_class_note = __esm(() => {
35760
36375
 
35761
36376
  // src/config-pin.ts
35762
36377
  import { existsSync as existsSync17, readFileSync as readFileSync18 } from "node:fs";
35763
- import { join as join21 } from "node:path";
36378
+ import { join as join22 } from "node:path";
35764
36379
  function versionsNamedInConfig(projectPath) {
35765
- const config = join21(projectPath, ".mcp.json");
36380
+ const config = join22(projectPath, ".mcp.json");
35766
36381
  if (!existsSync17(config)) {
35767
36382
  return [];
35768
36383
  }
@@ -35790,9 +36405,9 @@ var init_config_pin = __esm(() => {
35790
36405
 
35791
36406
  // src/cut-test-run.ts
35792
36407
  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";
36408
+ import { dirname as dirname14, join as join23 } from "node:path";
35794
36409
  function keptAt(projectPath) {
35795
- return join22(projectPath, ".godot", "gdharness-reports", "cut-run.json");
36410
+ return join23(projectPath, ".godot", "gdharness-reports", "cut-run.json");
35796
36411
  }
35797
36412
  function keepCutTestRun(projectPath, run) {
35798
36413
  const path = keptAt(projectPath);
@@ -36631,10 +37246,10 @@ import {
36631
37246
  writeSync
36632
37247
  } from "node:fs";
36633
37248
  import { tmpdir as tmpdir5 } from "node:os";
36634
- import { join as join23 } from "node:path";
37249
+ import { join as join24 } from "node:path";
36635
37250
  import { setTimeout as delay3 } from "node:timers/promises";
36636
37251
  function helperAssembly(directory = tmpdir5()) {
36637
- return join23(directory, `gdharness-desktop-${DIGEST}.dll`);
37252
+ return join24(directory, `gdharness-desktop-${DIGEST}.dll`);
36638
37253
  }
36639
37254
  async function ensureHelperCompiled(directory = tmpdir5(), budgetMs = HELPER_COMPILE_MS, wait = true) {
36640
37255
  const assembly = helperAssembly(directory);
@@ -36674,8 +37289,8 @@ async function ensureHelperCompiled(directory = tmpdir5(), budgetMs = HELPER_COM
36674
37289
  return `the desktop helper was not compiled within ${String(budgetMs)} ms`;
36675
37290
  }
36676
37291
  async function compileHelper(directory, assembly, budgetMs) {
36677
- const scratch = mkdtempSync2(join23(directory, "gdharness-desktop-source-"));
36678
- const source = join23(scratch, "helper.cs");
37292
+ const scratch = mkdtempSync2(join24(directory, "gdharness-desktop-source-"));
37293
+ const source = join24(scratch, "helper.cs");
36679
37294
  writeFileSync12(source, SOURCE, "utf8");
36680
37295
  const literal = (path) => `'${path.replaceAll("'", "''")}'`;
36681
37296
  const script = [
@@ -36889,12 +37504,12 @@ import {
36889
37504
  statSync as statSync10,
36890
37505
  writeFileSync as writeFileSync13
36891
37506
  } from "node:fs";
36892
- import { dirname as dirname15, join as join24 } from "node:path";
37507
+ import { dirname as dirname15, join as join25 } from "node:path";
36893
37508
  function editorLogPath(projectPath) {
36894
- return join24(projectPath, ".godot", "gdharness-editor.log");
37509
+ return join25(projectPath, ".godot", "gdharness-editor.log");
36895
37510
  }
36896
37511
  function editorLogNotePath(projectPath) {
36897
- return join24(projectPath, ".godot", "gdharness-editor.json");
37512
+ return join25(projectPath, ".godot", "gdharness-editor.json");
36898
37513
  }
36899
37514
  function readEditorLogNote(projectPath) {
36900
37515
  try {
@@ -37930,7 +38545,7 @@ var init_rename = __esm(() => {
37930
38545
  import { existsSync as existsSync22, realpathSync as realpathSync4 } from "node:fs";
37931
38546
  import { readFile, realpath } from "node:fs/promises";
37932
38547
  import { createConnection as createConnection3 } from "node:net";
37933
- import { dirname as dirname17, join as join25, resolve as resolve5 } from "node:path";
38548
+ import { dirname as dirname17, join as join26, resolve as resolve5 } from "node:path";
37934
38549
  import { setTimeout as delay4 } from "node:timers/promises";
37935
38550
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
37936
38551
  function initializeTimeoutMs() {
@@ -38608,7 +39223,7 @@ async function resolveLSPPaths(projectPathValue, scriptPathValue) {
38608
39223
  } catch {
38609
39224
  throw new ArgumentRefusal(`Project path does not exist: ${requestedProjectPath}`);
38610
39225
  }
38611
- if (!existsSync22(join25(projectPath, "project.godot"))) {
39226
+ if (!existsSync22(join26(projectPath, "project.godot"))) {
38612
39227
  throw new ArgumentRefusal(`Not a Godot project: ${projectPath}. Point projectPath at the directory holding project.godot.`);
38613
39228
  }
38614
39229
  const contained = resolveWithinProject(projectPath, scriptPathValue);
@@ -38769,7 +39384,7 @@ var init_lsp_client = __esm(() => {
38769
39384
 
38770
39385
  // src/rename-symbol.ts
38771
39386
  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";
39387
+ import { basename as basename7, dirname as dirname18, join as join27, relative as relative4, resolve as resolve6 } from "node:path";
38773
39388
  function resourcePathOf(projectPath, absolute) {
38774
39389
  const inside = relative4(onDiskSpelling(projectPath), onDiskSpelling(absolute));
38775
39390
  if (inside === "" || inside.startsWith("..") || /^[A-Za-z]:/.test(inside)) {
@@ -38782,7 +39397,7 @@ function onDiskSpelling(path) {
38782
39397
  let at = resolve6(path);
38783
39398
  for (;; ) {
38784
39399
  try {
38785
- return join26(realpathSync5.native(at), ...missing.reverse());
39400
+ return join27(realpathSync5.native(at), ...missing.reverse());
38786
39401
  } catch {
38787
39402
  const parent = dirname18(at);
38788
39403
  if (parent === at) {
@@ -39210,15 +39825,15 @@ ${mention.kind}`;
39210
39825
  };
39211
39826
  }
39212
39827
  function writeLeftAloneDetail(projectPath, symbol, detail) {
39213
- const directory = join26(projectPath, ".godot", "gdharness-renames");
39828
+ const directory = join27(projectPath, ".godot", "gdharness-renames");
39214
39829
  mkdirSync12(directory, { recursive: true });
39215
39830
  detailWrites += 1;
39216
- const path = join26(directory, `${String(Date.now())}-${String(detailWrites).padStart(6, "0")}-${String(process.pid)}-${symbol}.json`);
39831
+ const path = join27(directory, `${String(Date.now())}-${String(detailWrites).padStart(6, "0")}-${String(process.pid)}-${symbol}.json`);
39217
39832
  writeFileSync15(path, `${JSON.stringify(detail, null, 2)}
39218
39833
  `, "utf8");
39219
39834
  const kept = readdirSync11(directory).filter((name) => name.endsWith(".json")).sort().reverse();
39220
39835
  for (const old of kept.slice(10)) {
39221
- rmSync12(join26(directory, old), { force: true });
39836
+ rmSync12(join27(directory, old), { force: true });
39222
39837
  }
39223
39838
  return path;
39224
39839
  }
@@ -39267,10 +39882,10 @@ import {
39267
39882
  statSync as statSync11,
39268
39883
  writeFileSync as writeFileSync16
39269
39884
  } from "node:fs";
39270
- import { basename as basename8, dirname as dirname19, extname as extname3, join as join27, relative as relative5 } from "node:path";
39885
+ import { basename as basename8, dirname as dirname19, extname as extname3, join as join28, relative as relative5 } from "node:path";
39271
39886
  function filesUnder(directory) {
39272
39887
  return readdirSync12(directory, { withFileTypes: true }).flatMap((entry) => {
39273
- const path = join27(directory, entry.name);
39888
+ const path = join28(directory, entry.name);
39274
39889
  return entry.isDirectory() ? filesUnder(path) : [path];
39275
39890
  });
39276
39891
  }
@@ -39291,8 +39906,8 @@ function unseenByTheEngine(projectPath, path) {
39291
39906
  return `${path} is under ${step}, and the engine passes over every directory whose name starts with a dot`;
39292
39907
  }
39293
39908
  if (at < steps.length - 1) {
39294
- directory = join27(directory, step);
39295
- if (existsSync24(join27(directory, ".gdignore"))) {
39909
+ directory = join28(directory, step);
39910
+ if (existsSync24(join28(directory, ".gdignore"))) {
39296
39911
  return `${path} is under a directory holding a .gdignore, which the engine does not load`;
39297
39912
  }
39298
39913
  }
@@ -39304,12 +39919,12 @@ function installedAddonOf(projectPath, path) {
39304
39919
  if (steps[0] !== "addons" || steps[1] === undefined) {
39305
39920
  return null;
39306
39921
  }
39307
- return existsSync24(join27(projectPath, "addons", steps[1], VERSION_MARKER)) ? `res://addons/${steps[1]}` : null;
39922
+ return existsSync24(join28(projectPath, "addons", steps[1], VERSION_MARKER)) ? `res://addons/${steps[1]}` : null;
39308
39923
  }
39309
39924
  function binaryResources(projectPath) {
39310
39925
  const found = [];
39311
39926
  const visit = (directory, prefix) => {
39312
- if (existsSync24(join27(directory, ".gdignore"))) {
39927
+ if (existsSync24(join28(directory, ".gdignore"))) {
39313
39928
  return;
39314
39929
  }
39315
39930
  for (const entry of readdirSync12(directory, { withFileTypes: true })) {
@@ -39318,7 +39933,7 @@ function binaryResources(projectPath) {
39318
39933
  }
39319
39934
  const path = `${prefix}${entry.name}`;
39320
39935
  if (entry.isDirectory()) {
39321
- visit(join27(directory, entry.name), `${path}/`);
39936
+ visit(join28(directory, entry.name), `${path}/`);
39322
39937
  } else if (/\.(scn|res)$/i.test(entry.name)) {
39323
39938
  found.push(path);
39324
39939
  }
@@ -39328,7 +39943,7 @@ function binaryResources(projectPath) {
39328
39943
  return found;
39329
39944
  }
39330
39945
  function targetPathOf(projectPath, absolute) {
39331
- const inside = relative5(onDiskSpelling(projectPath), join27(onDiskSpelling(dirname19(absolute)), basename8(absolute)));
39946
+ const inside = relative5(onDiskSpelling(projectPath), join28(onDiskSpelling(dirname19(absolute)), basename8(absolute)));
39332
39947
  if (inside === "" || inside.startsWith("..") || /^[A-Za-z]:/.test(inside)) {
39333
39948
  return null;
39334
39949
  }
@@ -39383,7 +39998,7 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
39383
39998
  return { ok: false, reason: `${unseen}, so nothing there could load what was moved.` };
39384
39999
  }
39385
40000
  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))));
40001
+ 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
40002
  if (owner !== undefined && owner !== null) {
39388
40003
  return { ok: false, reason: `${from} belongs to ${owner} and moves with it: move ${owner} instead.` };
39389
40004
  }
@@ -39417,12 +40032,12 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
39417
40032
  scripts,
39418
40033
  moves,
39419
40034
  named: [from],
39420
- exists: (path) => existsSync24(join27(projectPath, path.slice("res://".length)))
40035
+ exists: (path) => existsSync24(join28(projectPath, path.slice("res://".length)))
39421
40036
  }, { edits: [], mentions: [] });
39422
40037
  plan = mentionIgnored(plan, ignoredTexts(projectPath), null, [from]);
39423
40038
  const binaries = [];
39424
40039
  for (const path of binaryResources(projectPath)) {
39425
- const bytes = readFileSync23(join27(projectPath, path.slice("res://".length)));
40040
+ const bytes = readFileSync23(join28(projectPath, path.slice("res://".length)));
39426
40041
  const names = [...moves.keys()].find((moving) => bytes.includes(moving));
39427
40042
  if (names !== undefined) {
39428
40043
  binaries.push({
@@ -39441,7 +40056,7 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
39441
40056
  }));
39442
40057
  const left = leftAloneReport(mentions);
39443
40058
  const rewrites = rewritesOf(plan, texts, null);
39444
- const absoluteOf = (path) => join27(projectPath, path.slice("res://".length));
40059
+ const absoluteOf = (path) => join28(projectPath, path.slice("res://".length));
39445
40060
  const rewritten = new Map(rewrites.map((rewrite) => [rewrite.path, rewrite]));
39446
40061
  const moved = [...moves].map(([source, target]) => {
39447
40062
  const rewrite = rewritten.get(source);
@@ -42882,742 +43497,243 @@ var init_godot_bridge = __esm(() => {
42882
43497
  this.connectionInfo.greeted = true;
42883
43498
  const pid = this.connectionInfo.editorPid;
42884
43499
  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;
43500
+ this.identify(pid);
43501
+ return;
43502
+ }
43503
+ this.emitBridgeEvent("godot_connected", { projectPath: message.project_path });
43504
+ }
43505
+ return;
43506
+ case "pong":
43507
+ if (this.connectionInfo) {
43508
+ this.connectionInfo.lastPongAt = new Date;
43509
+ }
43510
+ return;
43511
+ }
43409
43512
  }
43410
- let detail = entry.detail;
43411
- let recovered = false;
43412
- for (const [index, diff] of diffs.entries()) {
43413
- if (used.has(index)) {
43414
- continue;
43513
+ invokeToolDirect(toolName, args, counted, resourceKey) {
43514
+ if (!this.isConnected()) {
43515
+ return Promise.reject(new Error("Godot is not connected"));
43415
43516
  }
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);
43517
+ const requestId = randomUUID();
43518
+ const message = {
43519
+ type: "tool_invoke",
43520
+ id: requestId,
43521
+ tool: toolName,
43522
+ args
43523
+ };
43524
+ return new Promise((resolve, reject) => {
43525
+ const timeout = setTimeout(() => {
43526
+ this.pendingRequests.delete(requestId);
43527
+ reject(new BridgeTimeout(toolName, this.timeoutMs));
43528
+ }, this.timeoutMs);
43529
+ this.pendingRequests.set(requestId, {
43530
+ toolName,
43531
+ timeout,
43532
+ resolve,
43533
+ reject,
43534
+ startedAt: Date.now(),
43535
+ resourceKey,
43536
+ counted
43537
+ });
43538
+ this.emitBridgeEvent("tool_start", {
43539
+ tool: toolName,
43540
+ id: requestId,
43541
+ args
43542
+ });
43543
+ try {
43544
+ this.sendMessage(message);
43545
+ } catch (error) {
43546
+ clearTimeout(timeout);
43547
+ this.pendingRequests.delete(requestId);
43548
+ reject(toError(error));
43549
+ }
43550
+ });
43551
+ }
43552
+ sendMessage(message) {
43553
+ if (this.socket?.readyState !== import_websocket.default.OPEN) {
43554
+ throw new Refusal("Godot is not connected");
43425
43555
  }
43556
+ this.socket.send(JSON.stringify(message));
43426
43557
  }
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}`;
43558
+ startKeepalive() {
43559
+ this.stopKeepalive();
43560
+ this.pingInterval = setInterval(() => {
43561
+ if (!this.isConnected()) {
43562
+ return;
43563
+ }
43564
+ try {
43565
+ const ping = { type: "ping" };
43566
+ this.sendMessage(ping);
43567
+ } catch (error) {
43568
+ this.log("warn", `Failed to send ping: ${errorMessage(error)}`);
43569
+ }
43570
+ }, KEEPALIVE_INTERVAL_MS);
43433
43571
  }
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);
43572
+ stopKeepalive() {
43573
+ if (!this.pingInterval) {
43574
+ return;
43575
+ }
43576
+ clearInterval(this.pingInterval);
43577
+ this.pingInterval = null;
43438
43578
  }
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;
43579
+ belongsElsewhere(theirProject) {
43580
+ if (this.ownProject === null || theirProject.trim() === "") {
43581
+ return false;
43582
+ }
43583
+ return !isSameDirectory(this.ownProject, theirProject);
43458
43584
  }
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;
43585
+ sendElsewhere(theirProject) {
43586
+ const said = `this server serves ${this.ownProject ?? ""}, and that editor has ${theirProject} open`;
43587
+ this.log("warn", `Turning away an editor from another project: ${said}`);
43588
+ const stranger = this.socket;
43589
+ this.socket = null;
43590
+ this.connectionInfo = null;
43591
+ this.stopKeepalive();
43592
+ const fits = closeReason(said, "start") === said;
43593
+ stranger?.close(OTHER_PROJECT_CLOSE_CODE, fits ? said : closeReason(`this server serves ${this.ownProject ?? ""}`, "end"));
43496
43594
  }
43497
- spent += size;
43498
- whole.add(index);
43499
- }
43500
- const kept = failed.map((entry, index) => {
43501
- if (whole.has(index)) {
43502
- return entry;
43595
+ elsewhereUnlessOneShot(theirProject, pid, openedByAServer) {
43596
+ const known = pid === undefined ? undefined : this.oneShots.get(pid);
43597
+ if (pid !== undefined && known !== undefined) {
43598
+ this.turnAway(pid, known);
43599
+ return;
43600
+ }
43601
+ const info = this.connectionInfo;
43602
+ const socket = this.socket;
43603
+ const reader = this.oneShotReader;
43604
+ if (pid === undefined || openedByAServer || info === null || reader === null) {
43605
+ this.sendElsewhere(theirProject);
43606
+ return;
43607
+ }
43608
+ info.identifying = true;
43609
+ reader(pid).catch(() => {
43610
+ return;
43611
+ }).then((option) => {
43612
+ if (this.socket !== socket || this.connectionInfo !== info) {
43613
+ return;
43614
+ }
43615
+ if (typeof option === "string") {
43616
+ this.oneShots.set(pid, option);
43617
+ this.turnAway(pid, option);
43618
+ return;
43619
+ }
43620
+ this.sendElsewhere(theirProject);
43621
+ });
43503
43622
  }
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;
43623
+ identify(pid) {
43624
+ const known = this.oneShots.get(pid);
43625
+ if (known !== undefined) {
43626
+ this.turnAway(pid, known);
43627
+ return;
43628
+ }
43629
+ const info = this.connectionInfo;
43630
+ const socket = this.socket;
43631
+ const reader = this.oneShotReader;
43632
+ if (info === null || reader === null) {
43633
+ return;
43634
+ }
43635
+ info.identifying = true;
43636
+ reader(pid).catch(() => {
43637
+ return;
43638
+ }).then((option) => {
43639
+ if (this.socket !== socket || this.connectionInfo !== info) {
43640
+ return;
43641
+ }
43642
+ if (typeof option === "string") {
43643
+ this.oneShots.set(pid, option);
43644
+ this.turnAway(pid, option);
43645
+ return;
43646
+ }
43647
+ info.identifying = false;
43648
+ this.log("info", `Godot editor identified: pid ${pid}`);
43649
+ this.emitBridgeEvent("godot_connected", { projectPath: info.projectPath });
43650
+ });
43522
43651
  }
43523
- const counted = ORPHANS.exec(line);
43524
- if (counted?.[1] === undefined) {
43525
- continue;
43652
+ turnAway(pid, option) {
43653
+ const said = `pid ${pid} was started with ${option}, which does one job and quits, so it is not an editor`;
43654
+ this.log("info", `Turning away an engine run: ${said}`);
43655
+ this.turnedAway = { pid, option, at: new Date };
43656
+ const run = this.socket;
43657
+ this.socket = null;
43658
+ this.connectionInfo = null;
43659
+ this.stopKeepalive();
43660
+ run?.close(ONE_SHOT_CLOSE_CODE, closeReason(said, "start"));
43526
43661
  }
43527
- const orphans = Number(counted[1]);
43528
- if (line.includes(OVERALL)) {
43529
- total = orphans;
43530
- running = null;
43531
- continue;
43662
+ handleDisconnect(disconnectedSocket, reason) {
43663
+ if (disconnectedSocket && this.socket && disconnectedSocket !== this.socket) {
43664
+ this.log("debug", "Ignoring stale Godot socket disconnect event");
43665
+ return;
43666
+ }
43667
+ this.stopKeepalive();
43668
+ this.socket = null;
43669
+ this.connectionInfo = null;
43670
+ this.emitBridgeEvent("godot_disconnected", {});
43671
+ this.rejectAllPending(reason);
43672
+ this.resourceQueues.clear();
43532
43673
  }
43533
- if (running !== null && orphans > 0) {
43534
- suites.push({ path: running, orphans });
43674
+ emitBridgeEvent(eventName, payload) {
43675
+ this.emit(eventName, payload);
43535
43676
  }
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;
43677
+ rejectAllPending(error) {
43678
+ for (const pending of this.pendingRequests.values()) {
43679
+ clearTimeout(pending.timeout);
43680
+ pending.reject(error);
43681
+ }
43682
+ this.pendingRequests.clear();
43556
43683
  }
43557
- if (running === null) {
43558
- continue;
43684
+ enqueueResourceRequest(resourceKey, task) {
43685
+ const previous = this.resourceQueues.get(resourceKey) ?? Promise.resolve();
43686
+ const taskPromise = previous.catch(() => {
43687
+ return;
43688
+ }).then(task);
43689
+ const tail = taskPromise.then(() => {
43690
+ return;
43691
+ }, () => {
43692
+ return;
43693
+ });
43694
+ this.resourceQueues.set(resourceKey, tail);
43695
+ return taskPromise.finally(() => {
43696
+ if (this.resourceQueues.get(resourceKey) === tail) {
43697
+ this.resourceQueues.delete(resourceKey);
43698
+ }
43699
+ });
43559
43700
  }
43560
- const failing = CASE_FAILED.exec(line)?.[1];
43561
- if (failing !== undefined) {
43562
- failed.push(failing);
43563
- continue;
43701
+ getResourceKey(args) {
43702
+ const scenePath = this.getStringArg(args, "scenePath") ?? this.getStringArg(args, "scene_path");
43703
+ if (scenePath) {
43704
+ return `scene:${scenePath}`;
43705
+ }
43706
+ const resourcePath = this.getStringArg(args, "resourcePath") ?? this.getStringArg(args, "resource_path");
43707
+ if (resourcePath) {
43708
+ return `resource:${resourcePath}`;
43709
+ }
43710
+ return;
43564
43711
  }
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;
43712
+ getStringArg(args, key) {
43713
+ const value = args[key];
43714
+ return typeof value === "string" && value.length > 0 ? value : undefined;
43575
43715
  }
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";
43716
+ isIncomingMessage(value) {
43717
+ if (!value || typeof value !== "object") {
43718
+ return false;
43719
+ }
43720
+ const message = value;
43721
+ const type = message["type"];
43722
+ if (type !== "tool_result" && type !== "pong" && type !== "godot_ready") {
43723
+ return false;
43724
+ }
43725
+ if (type === "pong") {
43726
+ return true;
43727
+ }
43728
+ if (type === "godot_ready") {
43729
+ return typeof message["project_path"] === "string";
43730
+ }
43731
+ return typeof message["id"] === "string" && typeof message["success"] === "boolean" && (message["error"] === undefined || typeof message["error"] === "string") && (message["busy"] === undefined || typeof message["busy"] === "boolean");
43732
+ }
43733
+ log(level, message) {
43734
+ console.error(`[${new Date().toISOString()}] [GodotBridge:${level.toUpperCase()}] ${message}`);
43593
43735
  }
43594
43736
  };
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
43737
  });
43622
43738
 
43623
43739
  // src/keeper-channel.ts
@@ -43687,10 +43803,10 @@ var init_keeper_channel = () => {};
43687
43803
 
43688
43804
  // src/lifecycle-log.ts
43689
43805
  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";
43806
+ import { dirname as dirname20, join as join29 } from "node:path";
43691
43807
  import process11 from "node:process";
43692
43808
  function lifecycleLogPath() {
43693
- return join28(runtimeDirectory(), "servers.log");
43809
+ return join29(runtimeDirectory(), "servers.log");
43694
43810
  }
43695
43811
  function writeLifecycle(event, fields = {}) {
43696
43812
  const path = lifecycleLogPath();
@@ -44011,9 +44127,9 @@ var init_prompts = __esm(() => {
44011
44127
 
44012
44128
  // src/restart-note.ts
44013
44129
  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";
44130
+ import { dirname as dirname21, join as join30 } from "node:path";
44015
44131
  function restartNotePath(projectPath) {
44016
- return join29(projectPath, ".godot", "gdharness-restart.json");
44132
+ return join30(projectPath, ".godot", "gdharness-restart.json");
44017
44133
  }
44018
44134
  function noteRestartBegun(note) {
44019
44135
  const path = restartNotePath(note.projectPath);
@@ -44054,16 +44170,16 @@ var init_restart_note = __esm(() => {
44054
44170
  // src/run-record.ts
44055
44171
  import { createHash as createHash5 } from "node:crypto";
44056
44172
  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";
44173
+ import { basename as basename9, dirname as dirname22, join as join31, resolve as resolve7 } from "node:path";
44058
44174
  function runsDirectory() {
44059
- return join30(runtimeDirectory(), "runs");
44175
+ return join31(runtimeDirectory(), "runs");
44060
44176
  }
44061
44177
  function realAsFarAsItExists(path) {
44062
44178
  const missing = [];
44063
44179
  let at = path;
44064
44180
  for (;; ) {
44065
44181
  try {
44066
- return join30(realpathSync6.native(at), ...missing);
44182
+ return join31(realpathSync6.native(at), ...missing);
44067
44183
  } catch {
44068
44184
  const parent = dirname22(at);
44069
44185
  if (parent === at) {
@@ -44080,10 +44196,10 @@ function projectKey(projectPath) {
44080
44196
  return createHash5("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
44081
44197
  }
44082
44198
  function runRecordPath(projectPath, directory = runtimeDirectory()) {
44083
- return join30(directory, "runs", `note-${projectKey(projectPath)}.json`);
44199
+ return join31(directory, "runs", `note-${projectKey(projectPath)}.json`);
44084
44200
  }
44085
44201
  function latestRunPath(servedBy, directory = runtimeDirectory()) {
44086
- return join30(directory, "runs", `latest-${projectKey(servedBy)}.json`);
44202
+ return join31(directory, "runs", `latest-${projectKey(servedBy)}.json`);
44087
44203
  }
44088
44204
  function writeLatestRun(servedBy, latest) {
44089
44205
  try {
@@ -44108,20 +44224,20 @@ function readLatestRun(servedBy, directory = runtimeDirectory()) {
44108
44224
  }
44109
44225
  function openTranscript(startedAt) {
44110
44226
  mkdirSync16(runsDirectory(), { recursive: true });
44111
- const path = join30(runsDirectory(), `run-${startedAt}.log`);
44227
+ const path = join31(runsDirectory(), `run-${startedAt}.log`);
44112
44228
  return { path, fd: openSync3(path, "a") };
44113
44229
  }
44114
44230
  function profilePath(startedAt) {
44115
- return join30(runsDirectory(), `run-${startedAt}.profile.json`);
44231
+ return join31(runsDirectory(), `run-${startedAt}.profile.json`);
44116
44232
  }
44117
44233
  function profileMarkPath(startedAt) {
44118
- return join30(runsDirectory(), `run-${startedAt}.profile-mark.json`);
44234
+ return join31(runsDirectory(), `run-${startedAt}.profile-mark.json`);
44119
44235
  }
44120
44236
  function noteIsAbout(note, playing) {
44121
44237
  return note.play !== undefined && playing !== null && note.play.play === playing.play && note.play.editorPid === playing.editorPid;
44122
44238
  }
44123
44239
  function editorNotePath(projectPath, directory = runtimeDirectory()) {
44124
- return join30(directory, "runs", `editor-${projectKey(projectPath)}.json`);
44240
+ return join31(directory, "runs", `editor-${projectKey(projectPath)}.json`);
44125
44241
  }
44126
44242
  function writeEditorRunNote(note) {
44127
44243
  mkdirSync16(runsDirectory(), { recursive: true });
@@ -44132,7 +44248,7 @@ function writeEditorRunNote(note) {
44132
44248
  }
44133
44249
  function readEditorRunNote(projectPath) {
44134
44250
  for (const directory of runtimeDirectories()) {
44135
- const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join30(directory, "runs", "editor-run.json"));
44251
+ const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join31(directory, "runs", "editor-run.json"));
44136
44252
  if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
44137
44253
  return note;
44138
44254
  }
@@ -44166,7 +44282,7 @@ function readRunRecord(projectPath) {
44166
44282
  if (own !== null) {
44167
44283
  return own;
44168
44284
  }
44169
- const shared = recordAt(join30(directory, "runs", SHARED_NOTE));
44285
+ const shared = recordAt(join31(directory, "runs", SHARED_NOTE));
44170
44286
  if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
44171
44287
  return shared;
44172
44288
  }
@@ -44178,13 +44294,13 @@ function everyRunRecord() {
44178
44294
  for (const directory of runtimeDirectories()) {
44179
44295
  let entries;
44180
44296
  try {
44181
- entries = readdirSync13(join30(directory, "runs"));
44297
+ entries = readdirSync13(join31(directory, "runs"));
44182
44298
  } catch {
44183
44299
  continue;
44184
44300
  }
44185
44301
  for (const entry of entries) {
44186
44302
  if (entry === SHARED_NOTE || entry.startsWith("note-") && entry.endsWith(".json")) {
44187
- const record = recordAt(join30(directory, "runs", entry));
44303
+ const record = recordAt(join31(directory, "runs", entry));
44188
44304
  if (record !== null) {
44189
44305
  found.push(record);
44190
44306
  }
@@ -44392,7 +44508,7 @@ function judgeAnnouncedGame(running, projectPath, announcedAt) {
44392
44508
  }
44393
44509
  function clearRunRecord(projectPath) {
44394
44510
  for (const directory of runtimeDirectories()) {
44395
- const shared = join30(directory, "runs", SHARED_NOTE);
44511
+ const shared = join31(directory, "runs", SHARED_NOTE);
44396
44512
  const sharedRecord = recordAt(shared);
44397
44513
  const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
44398
44514
  for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
@@ -44427,7 +44543,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
44427
44543
  if (!transcript && !entry.endsWith(".partial")) {
44428
44544
  continue;
44429
44545
  }
44430
- const path = join30(directory, entry);
44546
+ const path = join31(directory, entry);
44431
44547
  if (kept.has(path) || ageMs(path, now) < keepMs) {
44432
44548
  continue;
44433
44549
  }
@@ -44447,110 +44563,6 @@ var init_run_record = __esm(() => {
44447
44563
  init_windows_ask();
44448
44564
  });
44449
44565
 
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
44566
  // src/script-profile.ts
44555
44567
  import { readFileSync as readFileSync26 } from "node:fs";
44556
44568
  function readProfileFile(path) {
@@ -45660,7 +45672,8 @@ function repeatingScriptError(entries, byScript) {
45660
45672
  if (last === undefined || !byScript(last)) {
45661
45673
  return null;
45662
45674
  }
45663
- const where = last.detail[0] ?? null;
45675
+ const whole = last.detail.length > 0 ? last : entries.findLast((entry) => entry.text === last.text && entry.detail.length > 0 && byScript(entry)) ?? last;
45676
+ const where = whole.detail[0] ?? null;
45664
45677
  const times = entries.filter((entry) => entry.text === last.text && (entry.detail[0] ?? null) === where && byScript(entry)).length;
45665
45678
  return times >= LOOPING_AFTER ? { text: last.text, where: where?.replace(/^at: /, "") ?? null, times } : null;
45666
45679
  }
@@ -46056,9 +46069,9 @@ function scriptsUnder(directory, prefix) {
46056
46069
  }
46057
46070
  return found;
46058
46071
  }
46059
- function hasMainScene(projectFile) {
46072
+ function mainSceneOf(projectFile) {
46060
46073
  const scene = parseProjectGodot(readFileSync27(projectFile, "utf8"))["application"]?.["run/main_scene"];
46061
- return typeof scene === "string" && scene !== "";
46074
+ return typeof scene === "string" && scene !== "" ? scene : null;
46062
46075
  }
46063
46076
  function runtimeAddonAnnouncesEditors(projectPath) {
46064
46077
  if (projectPath === null) {
@@ -48038,6 +48051,7 @@ class GodotServer {
48038
48051
  if (!engine.ok) {
48039
48052
  return engine.response;
48040
48053
  }
48054
+ const began = Date.now();
48041
48055
  const classes = await this.rebuildClassCache(project.value.path);
48042
48056
  if (!classes.ok) {
48043
48057
  return this.answer(classes);
@@ -48111,6 +48125,7 @@ class GodotServer {
48111
48125
  await untilGone([run.process.pid]);
48112
48126
  }
48113
48127
  await run.settled;
48128
+ const tookMs = Date.now() - began;
48114
48129
  const notCompiled = stopped.for ?? run.log.all.map((entry) => autoloadNotCompiled(entry, autoloaded)).find((found) => found !== null) ?? null;
48115
48130
  const reportsDir = join32(project.value.path, ".godot", ...ours.split("/"));
48116
48131
  let report = null;
@@ -48200,6 +48215,7 @@ class GodotServer {
48200
48215
  passed: false,
48201
48216
  verdict,
48202
48217
  tests: 0,
48218
+ tookMs,
48203
48219
  exitCode,
48204
48220
  exitSignal: run.exitSignal ?? undefined,
48205
48221
  hung,
@@ -48257,6 +48273,7 @@ class GodotServer {
48257
48273
  errors: report.errors,
48258
48274
  skipped: report.skipped,
48259
48275
  time: report.time,
48276
+ tookMs,
48260
48277
  failed,
48261
48278
  failedWithoutDetail: briefed > 0 ? briefed : undefined,
48262
48279
  hookFailures: hookFailures.length > 0 ? hookFailures : undefined,
@@ -49363,7 +49380,7 @@ class GodotServer {
49363
49380
  if (!asked.ok) {
49364
49381
  return asked.response;
49365
49382
  }
49366
- if (exported === null && !sceneToRun && !hasMainScene(project.value.file)) {
49383
+ if (exported === null && !sceneToRun && mainSceneOf(project.value.file) === null) {
49367
49384
  return this.createErrorResponse("The project sets no main scene, so there is nothing to run.", [
49368
49385
  "Pass scene to run one scene",
49369
49386
  "Choose the main scene with project_settings set_main_scene"
@@ -49440,6 +49457,7 @@ class GodotServer {
49440
49457
  alsoRunning: stranded.length === 0 ? undefined : stranded.map((one) => one.pid),
49441
49458
  pid: started.pid,
49442
49459
  executable: exported?.ok === true ? exported.relativePath : undefined,
49460
+ scene: exported !== null ? undefined : sceneArgument === null ? mainSceneOf(project.value.file) : `res://${sceneArgument}`,
49443
49461
  arguments: cmdArgs,
49444
49462
  argsNote: engineFlagsNote(given.value),
49445
49463
  transcript: started.transcript,
@@ -53360,9 +53378,9 @@ init_setup();
53360
53378
  init_tool_definitions();
53361
53379
  init_tool_reference();
53362
53380
  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";
53381
+ import { basename as basename6, dirname as dirname8, join as join15 } from "node:path";
53364
53382
  var SKILL_NAME = "gdharness";
53365
- var SHARED_SKILLS = join14(".agents", "skills");
53383
+ var SHARED_SKILLS = join15(".agents", "skills");
53366
53384
  function skillMarkdown(version) {
53367
53385
  return `---
53368
53386
  name: ${SKILL_NAME}
@@ -53587,27 +53605,27 @@ what they report while starting comes back under \`engine_messages_before_operat
53587
53605
  function skillFiles(version) {
53588
53606
  return new Map([
53589
53607
  ["SKILL.md", skillMarkdown(version)],
53590
- [join14("references", "tools.md"), renderToolsMarkdown()]
53608
+ [join15("references", "tools.md"), renderToolsMarkdown()]
53591
53609
  ]);
53592
53610
  }
53593
53611
  function skillDirectories(harnesses, projectPath, exists) {
53594
- const directories = [join14(projectPath, SHARED_SKILLS, SKILL_NAME)];
53612
+ const directories = [join15(projectPath, SHARED_SKILLS, SKILL_NAME)];
53595
53613
  for (const harness of harnesses) {
53596
53614
  if (harness.skills === undefined) {
53597
53615
  continue;
53598
53616
  }
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));
53617
+ const own = join15(projectPath, harness.skills.dir);
53618
+ if ((!harness.skills.shared || exists(own)) && !directories.includes(join15(own, SKILL_NAME))) {
53619
+ directories.push(join15(own, SKILL_NAME));
53602
53620
  }
53603
53621
  }
53604
53622
  return directories;
53605
53623
  }
53606
53624
  function everySkillDirectory(projectPath, harnesses) {
53607
- const directories = new Set([join14(projectPath, SHARED_SKILLS, SKILL_NAME)]);
53625
+ const directories = new Set([join15(projectPath, SHARED_SKILLS, SKILL_NAME)]);
53608
53626
  for (const harness of harnesses) {
53609
53627
  if (harness.skills !== undefined) {
53610
- directories.add(join14(projectPath, harness.skills.dir, SKILL_NAME));
53628
+ directories.add(join15(projectPath, harness.skills.dir, SKILL_NAME));
53611
53629
  }
53612
53630
  }
53613
53631
  return [...directories];
@@ -53624,7 +53642,7 @@ function removeSkill(directories, projectPath) {
53624
53642
  return removed;
53625
53643
  }
53626
53644
  function pruneEmpty(directory, projectPath) {
53627
- const shared = join14(projectPath, ".agents");
53645
+ const shared = join15(projectPath, ".agents");
53628
53646
  let path = directory;
53629
53647
  while (path !== projectPath && (basename6(path) === "skills" || path === shared)) {
53630
53648
  if (readdirSync9(path).length > 0) {
@@ -53640,7 +53658,7 @@ function writeSkill(directories, version) {
53640
53658
  const replaced = existsSync14(directory);
53641
53659
  rmSync6(directory, { recursive: true, force: true });
53642
53660
  for (const [name, contents] of files) {
53643
- const path = join14(directory, name);
53661
+ const path = join15(directory, name);
53644
53662
  mkdirSync5(dirname8(path), { recursive: true });
53645
53663
  writeFileSync7(path, contents, "utf8");
53646
53664
  }
@@ -53650,13 +53668,13 @@ function writeSkill(directories, version) {
53650
53668
 
53651
53669
  // src/version.ts
53652
53670
  import { readFileSync as readFileSync12 } from "node:fs";
53653
- import { dirname as dirname9, join as join15 } from "node:path";
53671
+ import { dirname as dirname9, join as join16 } from "node:path";
53654
53672
  import { fileURLToPath as fileURLToPath2 } from "node:url";
53655
53673
  function getLocalVersion() {
53656
53674
  const currentDirectory = dirname9(fileURLToPath2(import.meta.url));
53657
53675
  const packageCandidates = [
53658
- join15(currentDirectory, "..", "package.json"),
53659
- join15(currentDirectory, "..", "..", "package.json")
53676
+ join16(currentDirectory, "..", "package.json"),
53677
+ join16(currentDirectory, "..", "..", "package.json")
53660
53678
  ];
53661
53679
  for (const packagePath of packageCandidates) {
53662
53680
  try {